summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJules Laplace <jules@okfoc.us>2014-09-10 09:26:36 -0400
committerJules Laplace <jules@okfoc.us>2014-09-10 09:26:36 -0400
commitfcc74fbe841d542da252d5688e7b90b1e2799224 (patch)
tree5a6cee797637fa4cf3ca2dd57e5cbb0669cecd57
parent6663ede5b27c2d4aa0caa1072463b97af8de8b57 (diff)
parent6d2746ad8a24f1ac3da5e9cb2ed452b73da20b71 (diff)
merge
-rw-r--r--bower.json3
-rw-r--r--public/assets/img/profile.pngbin0 -> 9320 bytes
-rw-r--r--public/assets/javascripts/ui/lib/Parser.js27
-rw-r--r--public/assets/javascripts/ui/site/ProjectList.js3
-rw-r--r--public/assets/javascripts/ui/site/StaffView.js49
-rwxr-xr-xpublic/assets/stylesheets/app.css19
-rw-r--r--public/assets/stylesheets/staff.css103
-rw-r--r--server/index.js6
-rw-r--r--server/lib/api/projects.js2
-rw-r--r--server/lib/auth/index.js4
-rw-r--r--server/lib/middleware.js2
-rw-r--r--server/lib/schemas/User.js1
-rw-r--r--server/lib/util.js5
-rw-r--r--server/lib/views/index.js (renamed from server/lib/views.js)28
-rw-r--r--server/lib/views/staff.js515
-rw-r--r--views/partials/footer.ejs3
-rw-r--r--views/projects/list-projects.ejs2
-rw-r--r--views/staff.ejs26
-rw-r--r--views/staff/_footer.ejs16
-rw-r--r--views/staff/_header.ejs15
-rw-r--r--views/staff/_media.ejs37
-rw-r--r--views/staff/_pagination.ejs17
-rw-r--r--views/staff/_projects.ejs24
-rw-r--r--views/staff/_stats.ejs17
-rw-r--r--views/staff/_users.ejs21
-rw-r--r--views/staff/_users_recent.ejs26
-rw-r--r--views/staff/index.ejs16
-rw-r--r--views/staff/media/index.ejs18
-rw-r--r--views/staff/media/show.ejs45
-rw-r--r--views/staff/media/show_404.ejs14
-rw-r--r--views/staff/projects/index.ejs18
-rw-r--r--views/staff/projects/show.ejs73
-rw-r--r--views/staff/projects/show_404.ejs14
-rw-r--r--views/staff/users/index.ejs18
-rw-r--r--views/staff/users/media.ejs18
-rw-r--r--views/staff/users/show.ejs103
-rw-r--r--views/staff/users/show_404.ejs13
37 files changed, 1252 insertions, 69 deletions
diff --git a/bower.json b/bower.json
index 0927672..ee8f9ba 100644
--- a/bower.json
+++ b/bower.json
@@ -6,6 +6,7 @@
"jquery": "1.11.0",
"momentjs": "~2.5.1",
"lodash": "",
- "fiber": ""
+ "fiber": "",
+ "jquery-jsonview": "1.2.0"
}
}
diff --git a/public/assets/img/profile.png b/public/assets/img/profile.png
new file mode 100644
index 0000000..bde68e0
--- /dev/null
+++ b/public/assets/img/profile.png
Binary files differ
diff --git a/public/assets/javascripts/ui/lib/Parser.js b/public/assets/javascripts/ui/lib/Parser.js
index 1cf0418..52c96e6 100644
--- a/public/assets/javascripts/ui/lib/Parser.js
+++ b/public/assets/javascripts/ui/lib/Parser.js
@@ -21,7 +21,7 @@ var Parser = {
}
},
tag: function (media) {
- return '<img src="' + media.url + '" onerror="imgError(this);">';
+ return '<img src="' + media.url + '">';
}
}, {
type: 'video',
@@ -43,7 +43,7 @@ var Parser = {
video.load()
},
tag: function (media) {
- return '<video src="' + media.url + '" onerror="imgError(this);">';
+ return '<video src="' + media.url + '">';
}
}, {
type: 'youtube',
@@ -73,7 +73,8 @@ var Parser = {
})
},
tag: function (media) {
- return '<img class="video" type="youtube" vid="'+media.token+'" src="'+media.thumbnail+'"><span class="playvid">&#9654;</span>';
+ // return '<img class="video" type="youtube" vid="'+media.token+'" src="'+media.thumbnail+'"><span class="playvid">&#9654;</span>';
+ return '<div class="video" style="width: ' + media.width + 'px; height: ' + media.height + 'px; overflow: hidden; position: relative;"><iframe frameborder="0" scrolling="no" seamless="seamless" webkitallowfullscreen="webkitAllowFullScreen" mozallowfullscreen="mozallowfullscreen" allowfullscreen="allowfullscreen" id="okplayer" width="' + media.width + '" height="' + media.height + '" src="http://youtube.com/embed/' + media.token + '?showinfo=0" style="position: absolute; top: 0px; left: 0px; width: ' + media.width + 'px; height: ' + media.height + 'px;"></iframe></div>'
}
}, {
type: 'vimeo',
@@ -101,7 +102,8 @@ var Parser = {
})
},
tag: function (media) {
- return '<img class="video" type="vimeo" vid="'+media.token+'" src="'+media.thumbnail+'"><span class="playvid">&#9654;</span>';
+ // return '<img class="video" type="vimeo" vid="'+media.token+'" src="'+media.thumbnail+'"><span class="playvid">&#9654;</span>';
+ return '<div class="video" style="width: ' + media.width + 'px; height: ' + media.height + 'px; overflow: hidden; position: relative;"><iframe frameborder="0" scrolling="no" seamless="seamless" webkitallowfullscreen="webkitAllowFullScreen" mozallowfullscreen="mozallowfullscreen" allowfullscreen="allowfullscreen" id="okplayer" src="http://player.vimeo.com/video/' + media.token + '?api=1&js_api=1&title=0&byline=0&portrait=0&playbar=0&player_id=okplayer&loop=0&autoplay=0" width="' + media.width + '" height="' + media.height + '" style="position: absolute; top: 0px; left: 0px; width: ' + media.width + 'px; height: ' + media.height + 'px;"></iframe></div>'
}
},
/*
@@ -165,5 +167,18 @@ var Parser = {
if (! matched) {
cb(null)
}
- }
-} \ No newline at end of file
+ },
+
+ tag: function (media){
+ if (media.type in Parser.lookup) {
+ return Parser.lookup[media.type].tag(media)
+ }
+ return ""
+ },
+
+ thumbnail: function (media) {
+ return '<img src="' + (media.thumbnail || media.url) + '" class="thumb">';
+ },
+
+};
+Parser.lookup = _.indexBy(Parser.integrations, 'type');
diff --git a/public/assets/javascripts/ui/site/ProjectList.js b/public/assets/javascripts/ui/site/ProjectList.js
index d772b20..ee1b89f 100644
--- a/public/assets/javascripts/ui/site/ProjectList.js
+++ b/public/assets/javascripts/ui/site/ProjectList.js
@@ -1,7 +1,7 @@
var ProjectList = View.extend({
- el: "#projectList",
+ el: ".projectList",
events: {
"mouseenter td.border": 'spinOn',
@@ -24,4 +24,3 @@ var ProjectList = View.extend({
}
})
-
diff --git a/public/assets/javascripts/ui/site/StaffView.js b/public/assets/javascripts/ui/site/StaffView.js
new file mode 100644
index 0000000..fdf39d2
--- /dev/null
+++ b/public/assets/javascripts/ui/site/StaffView.js
@@ -0,0 +1,49 @@
+var StaffView = View.extend({
+ el: ".page",
+
+ events: {
+ "click #toggle-staff": "toggleStaff",
+ },
+
+ initialize: function() {
+ this.$toggleStaff = $("#toggle-staff")
+ this.$mediaEmbed = $("#media-embed")
+ if (this.$toggleStaff.length && this.$toggleStaff.data().isstaff) {
+ this.$toggleStaff.html("Is Staff")
+ }
+ if (this.$mediaEmbed.length) {
+ var media = this.$mediaEmbed.data()
+ this.$mediaEmbed.html( Parser.tag( media ) )
+ }
+ },
+
+ load: function() {
+ $(".json").each(function(){
+ $(this).JSONView( this.innerText )
+ }).show()
+
+ this.projectList = new ProjectList ()
+ },
+
+ toggleStaff: function(){
+ var state = ! this.$toggleStaff.data().isstaff
+ var verb = state ? "promote this user to staff?" : "remove this user from staff?"
+ ConfirmModal.confirm("Are you sure you want to " + verb, function(){
+ $.ajax({
+ type: "put",
+ dataType: "json",
+ url: window.location.href + "/bless",
+ data: {
+ state: state,
+ _csrf: $("#_csrf").val(),
+ },
+ success: function(data){
+ this.$toggleStaff.data("isstaff", data.state)
+ this.$toggleStaff.html(data.state ? "Is Staff" : "Make Staff")
+ $("#is-staff").html(data.state ? "yes" : "no")
+ }.bind(this)
+ })
+ }.bind(this))
+ },
+
+})
diff --git a/public/assets/stylesheets/app.css b/public/assets/stylesheets/app.css
index 5922ab5..17a7dc0 100755
--- a/public/assets/stylesheets/app.css
+++ b/public/assets/stylesheets/app.css
@@ -161,23 +161,26 @@ h5 {
text-align:center;
}
-.page .profile {
+.page.profile {
color:white;
}
-.page table {
+.page table.demo,
+.page table.profilepage,
+.page table.projectList {
width: 100%;
border-top: 1px solid;
margin: 40px 0 0 0;
border-spacing: 0;
- clear:nboth;
+ clear: both;
}
-.page tr {
+.page table.profilepage tr,
+.page table.projectList tr {
height: 400px;
}
.page table.showcase {
height:70vh;
}
-.page table td.border {
+.page table.projectList td.border {
position: relative;
border-right: 1px solid;
}
@@ -191,7 +194,9 @@ iframe.embed {
z-index: -1;
pointer-events: none;
}
-.page table td {
+.page table.demo td,
+.page table.profilepage td,
+.page table.projectList td {
width: 33.3333%;
background-size: cover;
background-repeat: no-repeat;
@@ -238,7 +243,7 @@ iframe.embed {
color:white;
}
-#projectList .editBtn {
+.projectList .editBtn {
position: absolute;
right: 10px;
top: 10px;
diff --git a/public/assets/stylesheets/staff.css b/public/assets/stylesheets/staff.css
new file mode 100644
index 0000000..aa21f9b
--- /dev/null
+++ b/public/assets/stylesheets/staff.css
@@ -0,0 +1,103 @@
+* {
+ font-weight: 300;
+}
+th, b {
+ font-weight: bold;
+}
+table {
+ display: inline-block;
+ margin-right: 40px;
+ vertical-align: top;
+}
+td, th {
+ text-align: left;
+ padding: 2px 5px;
+}
+.page {
+ text-align: left;
+}
+.footer {
+ text-align: center;
+}
+h1 {
+ text-align: left;
+ display: inline-block;
+}
+nav {
+ display: inline-block;
+ text-align: left;
+}
+nav a {
+ padding-left: 20px;
+}
+hr {
+ border: 1px solid #bbb;
+ margin: 5px auto 10px;
+}
+.body {
+ width: 80%;
+ margin: 0 auto;
+}
+.json {
+ display: none;
+}
+.jsonview {
+ border: 1px solid #ddd;
+ background: rgba(238,238,238,0.5);
+ padding: 4px;
+ margin: 0 auto;
+ max-width: 100%;
+ overflow-x: auto;
+ margin-bottom: 20px;
+ max-height: 60vh;
+}
+.jsonview * {
+ font-family: monospace !important;
+ font-size: 12px;
+}
+.jsonview .collapser {
+ -webkit-user-select: none;
+ -moz-user-select: none;
+ user-select: none;
+}
+.staff {
+ font-size: 15px;
+}
+.staff .editLinks a {
+ color: #00f;
+}
+#iframe-embed, #iframe-embed tr, #iframe-embed td {
+ width: 79vw;
+}
+#iframe-embed td {
+ padding: 0;
+}
+h2 {
+ margin: 20px auto;
+}
+.avatar {
+ height: 40px;
+ width: 40px;
+ display: inline-block;
+ background-size: cover;
+}
+#actions button {
+ float: none;
+ width: auto;
+}
+iframe.embed {
+ position: static;
+ width: 100%;
+ height: 44vw;
+ border: 1px solid black;
+}
+.page table.projectList,
+.page table.projectList td.border {
+ border: 0;
+}
+#pagination {
+ margin: 10px 0;
+}
+#pagination a {
+ color: #00f;
+} \ No newline at end of file
diff --git a/server/index.js b/server/index.js
index e9efef0..952ade9 100644
--- a/server/index.js
+++ b/server/index.js
@@ -97,7 +97,7 @@ site.route = function () {
app.get('/profile', views.profile)
app.get('/profile/edit', views.profile)
- app.get('/profile/:name', views.profile)
+ app.get('/profile/:username', views.profile)
app.get('/about', views.docs);
app.get('/about/:name/edit', views.docs);
@@ -106,9 +106,6 @@ site.route = function () {
app.get('/api/profile', middleware.ensureAuthenticated, api.profile.show)
app.put('/api/profile', middleware.ensureAuthenticated, api.profile.update)
- app.get('/staff', middleware.ensureAuthenticated, middleware.ensureIsStaff, views.staff.index);
- app.get('/staff/bless', middleware.ensureAuthenticated, views.staff.bless);
-
app.get('/api/docs', middleware.ensureAuthenticated, middleware.ensureIsStaff, api.docs.show)
app.post('/api/docs/new', middleware.ensureAuthenticated, middleware.ensureIsStaff, api.docs.create)
app.post('/api/docs/edit', middleware.ensureAuthenticated, middleware.ensureIsStaff, api.docs.update)
@@ -148,6 +145,7 @@ site.route = function () {
app.get('/test/*', middleware.ensureAuthenticated, middleware.ensureIsStaff, views.modal)
+ views.staff.route(app)
}
diff --git a/server/lib/api/projects.js b/server/lib/api/projects.js
index 2a5beff..da41b48 100644
--- a/server/lib/api/projects.js
+++ b/server/lib/api/projects.js
@@ -41,6 +41,7 @@ var projects = {
data.media = JSON.parse(data.media)
data.colors = JSON.parse(data.colors)
data.startPosition = JSON.parse(data.startPosition)
+ data.created_at = new Date ()
upload.put("projects", req.files.thumbnail, {
unacceptable: function(err){
@@ -72,6 +73,7 @@ var projects = {
data.name = util.sanitize(data.name)
data.slug = util.slugify(data.name)
data.description = util.sanitize(data.description)
+ data.updated_at = new Date ()
if (req.files.thumbnail) {
upload.put("projects", req.files.thumbnail, {
diff --git a/server/lib/auth/index.js b/server/lib/auth/index.js
index 99af9b5..c2275ff 100644
--- a/server/lib/auth/index.js
+++ b/server/lib/auth/index.js
@@ -111,6 +111,7 @@ var auth = {
return info ? res.json(info) : res.redirect("/login");
}
+ user.last_seen = new Date ()
user.last_ip = util.ip2num( req.ip )
user.save(function(err, data){ if (err) console.err('error setting ip for user') })
@@ -173,7 +174,8 @@ var auth = {
email: email,
created_ip: util.ip2num( req.ip ),
last_ip: util.ip2num( req.ip ),
- created_at: new Date ()
+ created_at: new Date (),
+ last_seen: new Date (),
}
new User(data).save(function(err, user){
if (err || ! data) { return res.json({ error: err }) }
diff --git a/server/lib/middleware.js b/server/lib/middleware.js
index 9d6236a..9790f8f 100644
--- a/server/lib/middleware.js
+++ b/server/lib/middleware.js
@@ -89,7 +89,7 @@ var middleware = {
})
}
},
-
+
}
module.exports = middleware
diff --git a/server/lib/schemas/User.js b/server/lib/schemas/User.js
index b64f8fc..180a140 100644
--- a/server/lib/schemas/User.js
+++ b/server/lib/schemas/User.js
@@ -64,6 +64,7 @@ var UserSchema = new mongoose.Schema({
isStaff: { type: Boolean, default: false },
created_at: { type: Date },
updated_at: { type: Date },
+ last_seen: { type: Date },
created_ip: { type: Number },
last_ip: { type: Number },
});
diff --git a/server/lib/util.js b/server/lib/util.js
index 87e2d54..791d3e2 100644
--- a/server/lib/util.js
+++ b/server/lib/util.js
@@ -8,7 +8,6 @@ var nonAlphanumerics = new RegExp('[^-_a-zA-Z0-9]', 'g')
var consecutiveDashes = new RegExp("-+", 'g')
var entities = new RegExp("[<>&]", 'g')
-
var util = {}
util.trim = function (s){ return (s || "").replace(whitespaceHead,"").replace(whitespaceTail,"") }
@@ -19,6 +18,9 @@ util.slugify = function (s){
util.sanitize = function (s){
return (s || "").replace(entities, "")
}
+util.escape = function (s){
+ return (s || "").replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;")
+}
util.capitalize = function (s) {
return (s || "").split(" ").map(util.capitalizeWord).join(" ");
}
@@ -46,6 +48,7 @@ util.ip2num = function(dot) {
}
util.num2ip = function(num) {
+ if (! num) return ""
var d = num % 256;
for (var i = 3; i > 0; i--) {
num = Math.floor(num/256);
diff --git a/server/lib/views.js b/server/lib/views/index.js
index b3c1d18..99be956 100644
--- a/server/lib/views.js
+++ b/server/lib/views/index.js
@@ -1,12 +1,12 @@
/* jshint node: true */
-var User = require('./schemas/User'),
- Project = require('./schemas/Project'),
- Documentation = require('./schemas/Documentation'),
- Collaborator = require('./schemas/Collaborator'),
- config = require('../../config'),
+var User = require('../schemas/User'),
+ Project = require('../schemas/Project'),
+ Documentation = require('../schemas/Documentation'),
+ Collaborator = require('../schemas/Collaborator'),
+ config = require('../../../config'),
marked = require('marked'),
- util = require('./util'),
+ util = require('../util'),
_ = require('lodash'),
moment = require('moment');
@@ -20,6 +20,8 @@ marked.setOptions({
var views = {}
+views.staff = require('./staff')
+
views.editor_new = function (req, res) {
if (! req.user) {
res.redirect('/')
@@ -115,7 +117,7 @@ views.docs = function (req, res){
}
views.profile = function (req, res) {
- var username = req.params[0] || (req.user && req.user.username)
+ var username = req.params.username || (req.user && req.user.username)
if (username) {
User.findOne({ username: username }, function (err, user) {
user ? next(user) : done(err, {}, [])
@@ -149,16 +151,4 @@ views.profile = function (req, res) {
}
}
-views.staff = {
- index: function(req, res){
- res.render('staff')
- },
- bless: function(req, res){
- req.user.isStaff = true
- req.user.save(function(){
- res.redirect("/staff")
- })
- },
-}
-
module.exports = views
diff --git a/server/lib/views/staff.js b/server/lib/views/staff.js
new file mode 100644
index 0000000..2ffea0d
--- /dev/null
+++ b/server/lib/views/staff.js
@@ -0,0 +1,515 @@
+/* jshint node: true */
+
+var User = require('../schemas/User'),
+ Project = require('../schemas/Project'),
+ Media = require('../schemas/Media'),
+ Collaborator = require('../schemas/Collaborator'),
+ config = require('../../../config'),
+ middleware = require('../middleware'),
+ util = require('../util'),
+ _ = require('lodash'),
+ moment = require('moment');
+
+
+var staff = module.exports = {
+
+ fields: {
+ user: "_id username displayName photo created_at updated_at last_seen created_ip last_ip",
+ project: "_id name slug user_id privacy created_at updated_at",
+ },
+
+ defaults: {
+ user: {
+ _id: "", username: "", displayName: "",
+ created_at: "", updated_at: "", created_ip: "", last_ip: "",
+ },
+ },
+
+ middleware: {
+
+ ensureUsers: function(req, res, next){
+ var paginationInfo = res.locals.pagination = {}
+ var criteria = req.criteria || {}
+ var limit = paginationInfo.limit = Math.min( Number(req.query.limit) || 50, 200 )
+ var offset = paginationInfo.offset = Number(req.query.offset) || 0
+ var sort
+ paginationInfo.sort = req.query.sort
+ paginationInfo.sortOptions = ["date", "last_seen", "username"]
+ switch (req.query.sort) {
+ case 'date':
+ sort = {'created_at': -1}
+ break
+ case 'last_seen':
+ sort = {'last_seen': -1}
+ break
+ case 'username':
+ default:
+ sort = {'username': 1}
+ paginationInfo.sort = "username"
+ break
+ }
+ User.find(criteria)
+ .select(staff.fields.user)
+ .sort(sort)
+ .skip(offset)
+ .limit(limit)
+ .exec(function (err, users) {
+ res.locals.users = users.map(staff.helpers.user)
+ next()
+ })
+ },
+
+ ensureRecentUsers: function(req, res, next){
+ var dreq = { query: { sort: 'last_seen', limit: 20, offset: 0 } }
+ staff.middleware.ensureUsers(dreq, res, next)
+ },
+
+ ensureProjects: function(req, res, next){
+ var paginationInfo = res.locals.pagination = {}
+ var criteria = req.criteria || {}
+ var limit = paginationInfo.limit = Math.min( Number(req.query.limit) || 50, 200 )
+ var offset = paginationInfo.offset = Number(req.query.offset) || 0
+ var sort
+ paginationInfo.sort = req.query.sort
+ paginationInfo.sortOptions = ["date", "name"]
+ switch (req.query.sort) {
+ case 'date':
+ sort = {'created_at': -1}
+ break
+ case 'name':
+ default:
+ paginationInfo.sort = "name"
+ sort = {'slug': 1}
+ break
+ }
+ Project.find(criteria)
+ .select(staff.fields.project)
+ .sort(sort)
+ .skip(offset)
+ .limit(limit)
+ .exec(function (err, projects) {
+ res.locals.projects = projects.map(staff.helpers.project)
+ next()
+ })
+ },
+
+ ensureMedia: function(req, res, next){
+ var paginationInfo = res.locals.pagination = {}
+ var criteria = req.criteria || {}
+ var limit = paginationInfo.limit = Math.min( Number(req.query.limit) || 50, 200 )
+ var offset = paginationInfo.offset = Number(req.query.offset) || 0
+ var sort
+ paginationInfo.sort = req.query.sort
+ paginationInfo.sortOptions = ["date"]
+ switch (req.query.sort) {
+ default:
+ case 'date':
+ paginationInfo.sort = "date"
+ sort = {'created_at': -1}
+ break
+ }
+ Media.find(criteria)
+ // .select(staff.fields.media)
+ .sort(sort)
+ .skip(offset)
+ .limit(limit)
+ .exec(function (err, media) {
+ res.locals.media = media.map(staff.helpers.media)
+ next()
+ })
+ },
+
+ ensureRecentProjects: function(req, res, next){
+ var dreq = { params: { sort: 'created_at', limit: 20, offset: 0 } }
+ staff.middleware.ensureProjects(dreq, res, next)
+ },
+
+ ensureProjectsUsers: function(req, res, next){
+ if (! res.locals.projects || ! res.locals.projects.length) { return next() }
+ staff.middleware.ensureObjectsUsers(res.locals.projects, next)
+ },
+
+ ensureMediaUsers: function(req, res, next){
+ if (! res.locals.media || ! res.locals.media.length) { return next() }
+ staff.middleware.ensureObjectsUsers(res.locals.media, next)
+ },
+
+ ensureMediaUser: function(req, res, next){
+ if (! res.locals.media) { return next() }
+ staff.middleware.ensureObjectsUsers([ res.locals.media ], function(){
+ res.locals.mediaUser = res.locals.media.User
+ next()
+ })
+ },
+
+ ensureObjectsUsers: function(objects, next){
+ var dedupe = {}, user_ids
+ objects.forEach(function(obj){
+ dedupe[ obj.user_id ] = dedupe[ obj.user_id ] || []
+ dedupe[ obj.user_id ].push(obj)
+ })
+ user_ids = _.keys(dedupe)
+ User.find({ _id: user_ids })
+ .select(staff.fields.user)
+ .exec(function (err, users) {
+ users.forEach(function(user){
+ dedupe[user._id].forEach(function(obj){
+ obj.user = user
+ })
+ })
+ next()
+ })
+ },
+
+ ensureProfile: function(req, res, next){
+ var username = req.params.username
+ if (username) {
+ User.findOne({ username: username }, function (err, user) {
+ if (user) {
+ res.locals.profile = req.method == "GET" ? staff.helpers.user(user) : user
+ }
+ else {
+ res.locals.profile = null
+ }
+ next()
+ })
+ }
+ else {
+ res.locals.profile = null
+ next()
+ }
+ },
+
+ ensureSingleMedia: function(req, res, next){
+ var id = req.params.id
+ if (id) {
+ Media.findOne({ _id: id }, function (err, media) {
+ if (media) {
+ res.locals.media = req.method == "GET" ? staff.helpers.media(media) : media
+ }
+ else {
+ res.locals.media = null
+ }
+ next()
+ })
+ }
+ else {
+ res.locals.media = null
+ next()
+ }
+ },
+
+ ensureUsersCount: function(req, res, next){
+ User.count({}, function(err, count){
+ res.locals.userCount = count || 0
+ next()
+ })
+ },
+
+ ensureProjectsCount: function(req, res, next){
+ Project.count({}, function(err, count){
+ res.locals.projectCount = count || 0
+ next()
+ })
+ },
+
+ ensureMediaCount: function(req, res, next){
+ Media.count({}, function(err, count){
+ res.locals.mediaCount = count || 0
+ next()
+ })
+ },
+
+ ensureProfileProjectCount: function(req, res, next){
+ if (! res.locals.profile) { return next() }
+ Project.count({ user_id: res.locals.profile._id}, function(err, count){
+ res.locals.profile.projectCount = count || 0
+ next()
+ })
+ },
+
+ ensureProfileMediaCount: function(req, res, next){
+ if (! res.locals.profile) { return next() }
+ Media.count({ user_id: res.locals.profile._id}, function(err, count){
+ res.locals.profile.mediaCount = count || 0
+ next()
+ })
+ },
+
+ ensureProfileProjects: function(req, res, next){
+ if (! res.locals.profile) { return next() }
+ Project.find({ user_id: res.locals.profile._id }, staff.fields.project, function(err, projects){
+ res.locals.projects = projects.map(staff.helpers.project)
+ next()
+ })
+ },
+
+ ensureProfileMedia: function(req, res, next){
+ if (! res.locals.profile) { return next() }
+ req.criteria = { user_id: res.locals.profile._id }
+ staff.middleware.ensureMedia(req, res, next)
+ },
+
+ ensureProject: function(req, res, next){
+ res.locals.project = req.project
+ next()
+ },
+
+ ensureProjectUser: function(req, res, next){
+ if (! res.locals.project) { return next() }
+ User.findOne({ _id: res.locals.project.user_id }, staff.fields.user, function(err, user){
+ res.locals.projectUser = staff.helpers.user(user) || staff.defaults.user
+ next()
+ })
+ },
+
+ ensureProjectCollaborators: function(req, res, next){
+ if (! res.locals.project) {
+ res.locals.collaborators = []
+ return next()
+ }
+ Collaborator.find({ project_id: res.locals.project._id}, function(err, collaborators){
+ res.locals.collaborators = collaborators || []
+ next()
+ })
+ },
+ },
+
+ helpers: {
+ user: function(user){
+ user = user.toObject()
+ user.last_seen = moment( user.last_seen || user.updated_at || user.created_at ).fromNow()
+ user.created_ip = util.num2ip( user.created_ip )
+ user.last_ip = util.num2ip( user.last_ip )
+ return user
+ },
+
+ project: function(project){
+ project = project.toObject()
+ project.date = moment( project.updated_at || project.created_at ).format("M/DD/YYYY H:MM")
+ project.user = {}
+ return project
+ },
+
+ media: function(media){
+ media = media.toObject()
+ media.date = moment( media.updated_at || media.created_at ).format("M/DD/YYYY H:MM")
+ media.user = {}
+ media.shortUrl = media.url.replace(/^http.:\/\//,"")
+ return media
+ }
+ },
+
+ route: function(app){
+ app.get('/staff',
+ middleware.ensureAuthenticated,
+ middleware.ensureIsStaff,
+
+ staff.middleware.ensureRecentUsers,
+ staff.middleware.ensureUsersCount,
+ staff.middleware.ensureProjectsCount,
+ staff.middleware.ensureMediaCount,
+
+ staff.index
+ );
+
+ //
+ // users
+
+ app.get('/staff/users',
+ middleware.ensureAuthenticated,
+ middleware.ensureIsStaff,
+
+ staff.middleware.ensureUsersCount,
+ staff.middleware.ensureUsers,
+
+ staff.users.index
+ );
+ app.get('/staff/users/:username',
+ middleware.ensureAuthenticated,
+ middleware.ensureIsStaff,
+
+ staff.middleware.ensureProfile,
+ staff.middleware.ensureProfileProjectCount,
+ staff.middleware.ensureProfileMediaCount,
+ staff.middleware.ensureProfileProjects,
+
+ staff.users.show
+ );
+ app.get('/staff/users/:username/media',
+ middleware.ensureAuthenticated,
+ middleware.ensureIsStaff,
+
+ staff.middleware.ensureProfile,
+ staff.middleware.ensureProfileMedia,
+ staff.middleware.ensureProfileMediaCount,
+
+ staff.users.media
+ );
+ app.put('/staff/users/:username/bless',
+ middleware.ensureAuthenticated,
+ middleware.ensureIsStaff,
+
+ staff.middleware.ensureProfile,
+
+ staff.users.bless
+ );
+
+ //
+ // projects
+
+ app.get('/staff/projects',
+ middleware.ensureAuthenticated,
+ middleware.ensureIsStaff,
+
+ staff.middleware.ensureProjectsCount,
+
+ staff.middleware.ensureProjects,
+ staff.middleware.ensureProjectsUsers,
+
+ staff.projects.index
+ );
+ app.get('/staff/projects/:slug',
+ middleware.ensureAuthenticated,
+ middleware.ensureIsStaff,
+
+ middleware.ensureProject,
+ staff.middleware.ensureProject,
+ staff.middleware.ensureProjectUser,
+ staff.middleware.ensureProjectCollaborators,
+
+ staff.projects.show
+ );
+
+ //
+ // media
+
+ app.get('/staff/media',
+ middleware.ensureAuthenticated,
+ middleware.ensureIsStaff,
+
+ staff.middleware.ensureMediaCount,
+
+ staff.middleware.ensureMedia,
+ staff.middleware.ensureMediaUsers,
+
+ staff.media.index
+ );
+ app.get('/staff/media/:id',
+ middleware.ensureAuthenticated,
+ middleware.ensureIsStaff,
+
+ staff.middleware.ensureSingleMedia,
+ staff.middleware.ensureMediaUser,
+
+ staff.media.show
+ );
+
+ },
+
+ paginate: function(req, res){
+ var info = res.locals.pagination
+ info.query = "sort=" + info.sort + "&limit=" + info.limit
+ info.first_page = 0
+ info.last_page = Math.max(0, info.max - info.limit)
+ info.sortOptions = info.sortOptions
+ if (info.offset > 0) {
+ info.prev_page = Math.max(0, info.offset - info.limit)
+ }
+ else {
+ info.prev_page = -1
+ }
+ if (info.count == info.limit && info.offset + info.limit < info.max) {
+ info.next_page = info.offset + info.limit
+ }
+ else {
+ info.next_page = -1
+ }
+ },
+
+ index: function(req, res){
+ res.render('staff/index')
+ },
+
+ // /staff/users/
+ // /staff/users/:username
+ users: {
+ index: function(req, res){
+ res.locals.pagination.count = res.locals.users.length
+ res.locals.pagination.max = res.locals.userCount
+ staff.paginate(req, res)
+ res.render('staff/users/index')
+ },
+ show: function(req, res){
+ if (res.locals.profile) {
+ res.render('staff/users/show', {
+ profileJSON: util.escape( JSON.stringify( res.locals.profile ) )
+ })
+ }
+ else {
+ res.render('staff/users/show_404')
+ }
+ },
+ media: function(req, res){
+ if (res.locals.profile) {
+ res.locals.pagination.count = res.locals.media.length
+ res.locals.pagination.max = res.locals.profile.mediaCount
+ staff.paginate(req, res)
+ res.render('staff/users/media')
+ }
+ else {
+ res.render('staff/users/show_404')
+ }
+ },
+ bless: function(req, res){
+ res.locals.profile.isStaff = req.body.state == "true"
+ res.locals.profile.save(function(err, user){
+ res.json({ state: user.isStaff })
+ })
+ },
+ },
+
+ // /staff/projects/
+ // /staff/projects/:name
+ projects: {
+ index: function(req, res){
+ res.locals.pagination.count = res.locals.projects.length
+ res.locals.pagination.max = res.locals.projectCount
+ staff.paginate(req, res)
+ res.render('staff/projects/index')
+ },
+ show: function(req, res){
+ if (res.locals.project) {
+ res.render('staff/projects/show', {
+ projectJSON: util.escape( JSON.stringify( res.locals.project ) ),
+ projectUserJSON: util.escape( JSON.stringify( res.locals.projectUser ) ),
+ collaboratorsJSON: util.escape( JSON.stringify( res.locals.collaborators ) ),
+ })
+ }
+ else {
+ res.render('staff/projects/show_404')
+ }
+ },
+ },
+
+ media: {
+ index: function(req, res){
+ res.locals.pagination.count = res.locals.media.length
+ res.locals.pagination.max = res.locals.mediaCount
+ staff.paginate(req, res)
+ res.render('staff/media/index')
+ },
+ show: function(req, res){
+ if (res.locals.media) {
+ res.render('staff/media/show', {
+ mediaJSON: util.escape( JSON.stringify( res.locals.media ) ),
+ mediaUserJSON: util.escape( JSON.stringify( res.locals.mediaUser ) ),
+ })
+ }
+ else {
+ res.render('staff/media/show_404')
+ }
+ },
+ }
+
+} \ No newline at end of file
diff --git a/views/partials/footer.ejs b/views/partials/footer.ejs
index 685aec1..3f816f0 100644
--- a/views/partials/footer.ejs
+++ b/views/partials/footer.ejs
@@ -13,6 +13,9 @@
<span>
you are signed in as &rarr;
<a href="/profile/[[- user.username ]]">[[- user.displayName ]]</a>
+ [[ if (user.isStaff) { ]]
+ <a href="/staff">Staff Area</a>
+ [[ } ]]
<a href="/logout" class="topLink">Sign Out?</a>
</span>
[[ } ]]
diff --git a/views/projects/list-projects.ejs b/views/projects/list-projects.ejs
index c78bf9f..c41ae07 100644
--- a/views/projects/list-projects.ejs
+++ b/views/projects/list-projects.ejs
@@ -1,6 +1,6 @@
[[ if (projects.length) { ]]
- <table id="projectList">
+ <table class="projectList">
<tr>
[[ projects.forEach(function(project, i) { ]]
diff --git a/views/staff.ejs b/views/staff.ejs
deleted file mode 100644
index 0db8ebc..0000000
--- a/views/staff.ejs
+++ /dev/null
@@ -1,26 +0,0 @@
-<!doctype html>
-<html>
-<head>
- <title>vvalls</title>
- [[ include partials/meta ]]
-</head>
-<body class="loading">
-<div class="rapper page">
- [[ include partials/header ]]
-
- <br clear="all">
- <h1>Staff Area</h1>
- <!--
- - recent users
- - rooms list
- - projects list
- -->
-
- [[ include partials/confirm-modal ]]
- [[ include partials/footer ]]
-</div>
-
-
-</body>
-[[ include partials/scripts ]]
-</html>
diff --git a/views/staff/_footer.ejs b/views/staff/_footer.ejs
new file mode 100644
index 0000000..839db4a
--- /dev/null
+++ b/views/staff/_footer.ejs
@@ -0,0 +1,16 @@
+ </div>
+ [[ include ../partials/confirm-modal ]]
+ [[ include ../partials/footer ]]
+</div>
+
+</body>
+[[ include ../partials/scripts ]]
+<script type="text/javascript" src="/assets/javascripts/vendor/bower_components/jquery-jsonview/dist/jquery.jsonview.js"></script>
+<script type="text/javascript" src="/assets/javascripts/ui/site/StaffView.js"></script>
+<script>
+$(function(){
+ var staffView = new StaffView ()
+ staffView.load()
+})
+</script>
+</html>
diff --git a/views/staff/_header.ejs b/views/staff/_header.ejs
new file mode 100644
index 0000000..3bbf4f1
--- /dev/null
+++ b/views/staff/_header.ejs
@@ -0,0 +1,15 @@
+<!doctype html>
+<html>
+<head>
+ <title>vvalls | staff</title>
+ [[ include ../partials/meta ]]
+ <link rel="stylesheet" href="/assets/javascripts/vendor/bower_components/jquery-jsonview/dist/jquery.jsonview.css"></script>
+ <link rel="stylesheet" href="/assets/stylesheets/staff.css"></script>
+ <input type="hidden" id="_csrf" name="_csrf" value="[[- token ]]">
+</head>
+<body class="loading">
+<div class="rapper page staff">
+ [[ include ../partials/header ]]
+
+ <br clear="all">
+ <div class="body">
diff --git a/views/staff/_media.ejs b/views/staff/_media.ejs
new file mode 100644
index 0000000..19e9d0b
--- /dev/null
+++ b/views/staff/_media.ejs
@@ -0,0 +1,37 @@
+<table id="users">
+[[ media.forEach(function(media){ ]]
+ <tr>
+ <td class="editLinks">
+ <a href="/staff/media/[[- media._id ]]">[view]</a>
+ </td>
+ <td>
+ <a href="/staff/users/[[- media.user.username ]]">[[- media.user.username ]]</a>
+ </td>
+ <td>
+ [[- media.date ]]
+ </td>
+ <td>
+ [[- media.width ]]x[[- media.height ]]
+ </td>
+ <td>
+ [[- media.type ]]
+ </td>
+ <td>
+ [[- media.token ]]
+ </td>
+ <td>
+ <a class="medialink" href="[[- media.url ]]" target="_blank">[[- media.shortUrl ]]</a>
+ </td>
+ </tr>
+[[ }) ]]
+</table>
+
+<style>
+.medialink {
+ max-width: 30vw;
+ display: inline-block;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+</style> \ No newline at end of file
diff --git a/views/staff/_pagination.ejs b/views/staff/_pagination.ejs
new file mode 100644
index 0000000..6c3bfb1
--- /dev/null
+++ b/views/staff/_pagination.ejs
@@ -0,0 +1,17 @@
+[[ if (pagination.prev_page !== -1 && pagination.next_page !== -1) { ]]
+ <div id="pagination">
+
+ [[ if (pagination.prev_page !== -1) { ]]
+ <a href="?[[- pagination.query ]]&offset=[[- pagination.prev_page ]]">&larr;</a>
+ [[ } else { ]]
+ &larr;
+ [[ } ]]
+ |
+ [[ if (pagination.next_page !== -1) { ]]
+ <a href="?[[- pagination.query ]]&offset=[[- pagination.next_page ]]">Next page &rarr;</a>
+ [[ } else { ]]
+ &rarr;
+ [[ } ]]
+
+ </div>
+[[ } ]] \ No newline at end of file
diff --git a/views/staff/_projects.ejs b/views/staff/_projects.ejs
new file mode 100644
index 0000000..9e37a6c
--- /dev/null
+++ b/views/staff/_projects.ejs
@@ -0,0 +1,24 @@
+<table id="users">
+[[ projects.forEach(function(project){ ]]
+ <tr>
+ <td>
+ <a href="/staff/projects/[[- project.slug ]]">[[- project.name ]]</a>
+ </td>
+ <td class="editLinks">
+ <a href="/project/[[- project.slug ]]">[view]</a>
+ <a href="/project/[[- project.slug ]]/edit">[edit]</a>
+ </td>
+ [[ if (project.user) { ]]
+ <td>
+ <a href="/staff/users/[[- project.user.username ]]">[[- project.user.username ]]</a>
+ </td>
+ [[ } ]]
+ <td>
+ [[- project.date ]]
+ </td>
+ <td>
+ [[- project.privacy ? "private" : "" ]]
+ </td>
+ </tr>
+[[ }) ]]
+</table>
diff --git a/views/staff/_stats.ejs b/views/staff/_stats.ejs
new file mode 100644
index 0000000..d1ad96f
--- /dev/null
+++ b/views/staff/_stats.ejs
@@ -0,0 +1,17 @@
+<table>
+ <tr>
+ <th>stats</th>
+ </tr>
+ <tr>
+ <td><a href="/staff/users">users</a></td>
+ <td>[[- userCount ]]</td>
+ </tr>
+ <tr>
+ <td><a href="/staff/projects">projects</a></td>
+ <td>[[- projectCount ]]</td>
+ </tr>
+ <tr>
+ <td><a href="/staff/media">media</a></td>
+ <td>[[- mediaCount ]]</td>
+ </tr>
+</table>
diff --git a/views/staff/_users.ejs b/views/staff/_users.ejs
new file mode 100644
index 0000000..d46058f
--- /dev/null
+++ b/views/staff/_users.ejs
@@ -0,0 +1,21 @@
+<table id="users">
+[[ users.forEach(function(user){ ]]
+ <tr>
+ <td>
+ <a href="/staff/users/[[- user.username ]]"><div style="background-image:url([[- user.photo ]])" class="avatar"></div></a>
+ </td>
+ <td>
+ <a href="/staff/users/[[- user.username ]]">[[- user.username ]]</a>
+ </td>
+ <td>
+ [[- user.displayName ]]
+ </td>
+ <td class="editLinks">
+ <a href="/profile/[[- user.username ]]">[view profile]</a>
+ </td>
+ <td>
+ [[- user.last_seen ]]
+ </td>
+ </tr>
+[[ }) ]]
+</table>
diff --git a/views/staff/_users_recent.ejs b/views/staff/_users_recent.ejs
new file mode 100644
index 0000000..b452c08
--- /dev/null
+++ b/views/staff/_users_recent.ejs
@@ -0,0 +1,26 @@
+<table id="users">
+ <tr>
+ <th colspan="4">
+ recent users
+ </th>
+ </tr>
+[[ users.forEach(function(user){ ]]
+ <tr>
+ <td>
+ <a href="/staff/users/[[- user.username ]]"><div style="background-image:url([[- user.photo || '/assets/img/profile.png' ]])" class="avatar"></div></a>
+ </td>
+ <td>
+ <a href="/staff/users/[[- user.username ]]">[[- user.username ]]</a>
+ </td>
+ <td class="editLinks">
+ <a href="/profile/[[- user.username ]]">[view profile]</a>
+ </td>
+ <td>
+ [[- user.displayName ]]
+ </td>
+ <td>
+ [[- user.last_seen ]]
+ </td>
+ </tr>
+[[ }) ]]
+</table>
diff --git a/views/staff/index.ejs b/views/staff/index.ejs
new file mode 100644
index 0000000..5ca7269
--- /dev/null
+++ b/views/staff/index.ejs
@@ -0,0 +1,16 @@
+[[ include _header ]]
+
+ <h1>Staff Area</h1>
+
+ <nav>
+ <a href="/staff/users">users</a>
+ <a href="/staff/projects">projects</a>
+ <a href="/staff/media">media</a>
+ </nav>
+
+ <hr>
+
+ [[ include _users_recent ]]
+ [[ include _stats ]]
+
+[[ include _footer ]] \ No newline at end of file
diff --git a/views/staff/media/index.ejs b/views/staff/media/index.ejs
new file mode 100644
index 0000000..516af2d
--- /dev/null
+++ b/views/staff/media/index.ejs
@@ -0,0 +1,18 @@
+[[ include ../_header ]]
+
+ <h1>Media</h1>
+
+ <nav>
+ <a href="/staff">home</a>
+ <a href="/staff/users">users</a>
+ <a href="/staff/projects">projects</a>
+ <a href="/staff/media">media</a>
+ </nav>
+
+ <hr>
+
+[[ include ../_pagination ]]
+[[ include ../_media ]]
+[[ include ../_pagination ]]
+
+[[ include ../_footer ]]
diff --git a/views/staff/media/show.ejs b/views/staff/media/show.ejs
new file mode 100644
index 0000000..76dcd32
--- /dev/null
+++ b/views/staff/media/show.ejs
@@ -0,0 +1,45 @@
+[[ include ../_header ]]
+
+ <h1>Media: [[- media.type ]]</h1>
+
+ <nav>
+ <a href="/staff">home</a>
+ <a href="/staff/users">users</a>
+ <a href="/staff/projects">projects</a>
+ <a href="/staff/media">media</a>
+ </nav>
+
+ <hr>
+
+ <center>
+ <div id="media-embed" data-type="[[- media.type ]]" data-token="[[- media.token ]]" data-url="[[- media.url ]]" data-width="[[- media.width ]]" data-height="[[- media.height ]]" style="width: [[- media.width ]]px; height: [[- media.height ]]px">
+ </div>
+ </center>
+
+ <table>
+ <tr>
+ <td>
+ uploaded by
+ </td>
+ <td>
+ [[ if (media.user.photo) { ]]
+ <a href="/staff/users/[[- media.user.username ]]"><div style="background-image:url([[- media.user.photo ]])" class="avatar"></div></a>
+ [[ } ]]
+ </td>
+ <td>
+ <a href="/staff/users/[[- media.user.username ]]">[[- media.user.username ]]</a>
+ </td>
+ <td>
+ [[- media.user.displayName ]]
+ </td>
+ <td class="editLinks">
+ <a href="/profile/[[- media.user.username ]]">[view profile]</a>
+ </td>
+ </tr>
+ </table>
+
+ <div class="json">
+ [[- mediaJSON ]]
+ </div>
+
+[[ include ../_footer ]]
diff --git a/views/staff/media/show_404.ejs b/views/staff/media/show_404.ejs
new file mode 100644
index 0000000..f07cef2
--- /dev/null
+++ b/views/staff/media/show_404.ejs
@@ -0,0 +1,14 @@
+[[ include ../_header ]]
+
+ <h1>Media not found</h1>
+
+ <nav>
+ <a href="/staff">home</a>
+ <a href="/staff/users">users</a>
+ <a href="/staff/projects">projects</a>
+ <a href="/staff/media">media</a>
+ </nav>
+
+ <hr>
+
+[[ include ../_footer ]]
diff --git a/views/staff/projects/index.ejs b/views/staff/projects/index.ejs
new file mode 100644
index 0000000..482ea25
--- /dev/null
+++ b/views/staff/projects/index.ejs
@@ -0,0 +1,18 @@
+[[ include ../_header ]]
+
+ <h1>Projects</h1>
+
+ <nav>
+ <a href="/staff">home</a>
+ <a href="/staff/users">users</a>
+ <a href="/staff/projects">projects</a>
+ <a href="/staff/media">media</a>
+ </nav>
+
+ <hr>
+
+[[ include ../_pagination ]]
+[[ include ../_projects ]]
+[[ include ../_pagination ]]
+
+[[ include ../_footer ]]
diff --git a/views/staff/projects/show.ejs b/views/staff/projects/show.ejs
new file mode 100644
index 0000000..0fdb00b
--- /dev/null
+++ b/views/staff/projects/show.ejs
@@ -0,0 +1,73 @@
+[[ include ../_header ]]
+
+ <h1>[[- project.name ]]</h1>
+
+ <nav>
+ <a href="/staff">home</a>
+ <a href="/staff/users">users</a>
+ <a href="/staff/projects">projects</a>
+ <a href="/staff/media">media</a>
+ </nav>
+
+ <hr>
+
+ <table>
+ <tr>
+ <td>
+ <a href="/staff/projects/[[- project.slug ]]">[[- project.name ]]</a>
+ </td>
+ <td class="editLinks">
+ <a href="/project/[[- project.slug ]]">[view]</a>
+ <a href="/project/[[- project.slug ]]/edit">[edit]</a>
+ </td>
+ <td>
+ [[- project.date ]]
+ </td>
+ <td>
+ [[- project.privacy ? "private" : "" ]]
+ </td>
+ </tr>
+ <tr>
+ <td>
+ [[ if (projectUser.photo) { ]]
+ <a href="/staff/users/[[- projectUser.username ]]"><div style="background-image:url([[- projectUser.photo ]])" class="avatar"></div></a>
+ [[ } ]]
+ <a href="/staff/users/[[- projectUser.username ]]">[[- projectUser.username ]]</a>
+ </td>
+ </tr>
+ <tr>
+ <td colspan="999" class="description">
+ "[[- project.description ]]"
+ </td>
+ </tr>
+ </table>
+
+ <br>
+ <br>
+ <table id="iframe-embed" class="projectList">
+ <tr>
+ <td class="border">
+ <iframe src="/project/fafafa/view?noui=1&mute=1" class="embed"></iframe>
+ </td>
+ </tr>
+ </table>
+ <br>
+ <br>
+
+ <div class="json">
+ [[- projectJSON ]]
+ </div>
+
+ <h2>Author</h2>
+
+ <div class="json">
+ [[- projectUserJSON ]]
+ </div>
+
+ <h2>Collaborators</h2>
+
+ <div class="json">
+ [[- collaborators.length ? collaboratorsJSON : '"no collaborators"' ]]
+ </div>
+
+[[ include ../_footer ]]
diff --git a/views/staff/projects/show_404.ejs b/views/staff/projects/show_404.ejs
new file mode 100644
index 0000000..70320c0
--- /dev/null
+++ b/views/staff/projects/show_404.ejs
@@ -0,0 +1,14 @@
+[[ include ../_header ]]
+
+ <h1>Project not found</h1>
+
+ <nav>
+ <a href="/staff">home</a>
+ <a href="/staff/users">users</a>
+ <a href="/staff/projects">projects</a>
+ <a href="/staff/media">media</a>
+ </nav>
+
+ <hr>
+
+[[ include ../_footer ]]
diff --git a/views/staff/users/index.ejs b/views/staff/users/index.ejs
new file mode 100644
index 0000000..f14d666
--- /dev/null
+++ b/views/staff/users/index.ejs
@@ -0,0 +1,18 @@
+[[ include ../_header ]]
+
+ <h1>Users</h1>
+
+ <nav>
+ <a href="/staff">home</a>
+ <a href="/staff/users">users</a>
+ <a href="/staff/projects">projects</a>
+ <a href="/staff/media">media</a>
+ </nav>
+
+ <hr>
+
+[[ include ../_pagination ]]
+[[ include ../_users ]]
+[[ include ../_pagination ]]
+
+[[ include ../_footer ]]
diff --git a/views/staff/users/media.ejs b/views/staff/users/media.ejs
new file mode 100644
index 0000000..b13e5fb
--- /dev/null
+++ b/views/staff/users/media.ejs
@@ -0,0 +1,18 @@
+[[ include ../_header ]]
+
+ <h1>Users</h1>
+
+ <nav>
+ <a href="/staff">home</a>
+ <a href="/staff/users">users</a>
+ <a href="/staff/projects">projects</a>
+ <a href="/staff/media">media</a>
+ </nav>
+
+ <hr>
+
+[[ include ../_pagination ]]
+[[ include ../_media ]]
+[[ include ../_pagination ]]
+
+[[ include ../_footer ]]
diff --git a/views/staff/users/show.ejs b/views/staff/users/show.ejs
new file mode 100644
index 0000000..8e9b447
--- /dev/null
+++ b/views/staff/users/show.ejs
@@ -0,0 +1,103 @@
+[[ include ../_header ]]
+ <h1>User: [[- profile.username ]]</h1>
+
+ <nav>
+ <a href="/staff">home</a>
+ <a href="/staff/users">users</a>
+ <a href="/staff/projects">projects</a>
+ <a href="/staff/media">media</a>
+ </nav>
+
+ <hr>
+
+ <table id="users">
+ <tr>
+ <td rowspan="999" valign="top">
+ <a href="/staff/users/[[- profile.username ]]"><div style="background-image:url([[- profile.photo ]])" class="avatar"></div></a>
+ </td>
+ <td>
+ <a href="/staff/users/[[- profile.username ]]">[[- profile.username ]]</a>
+ </td>
+ <td>
+ [[- profile.displayName ]]
+ </td>
+ <td class="editLinks">
+ <a href="/profile/[[- profile.username ]]">[view profile]</a>
+ <a href="/staff/users/[[- profile.username ]]/media">[view media]</a>
+ </td>
+ </tr>
+ </table>
+
+ <h2>Profile</h2>
+
+ <table>
+ <tr>
+ <th>
+ location
+ </th>
+ <td>
+ [[- profile.location ]]
+ </td>
+ </tr>
+ <tr>
+ <th>
+ last seen
+ </th>
+ <td>
+ [[- profile.last_seen ]]
+ </td>
+ </tr>
+ <tr>
+ <th>
+ projects
+ </th>
+ <td>
+ [[- profile.projectCount ]]
+ </td>
+ </tr>
+ <tr>
+ <th>
+ media
+ </th>
+ <td>
+ [[- profile.mediaCount ]] <a href="/staff/users/[[- profile.username ]]/media">[show]</a>
+ </td>
+ </tr>
+ <tr>
+ <th>
+ ip addresses
+ </th>
+ <td>
+ [[- profile.created_ip ]]
+ [[- profile.last_ip ]]
+ </td>
+ </tr>
+ <tr>
+ <th>
+ is admin?
+ </th>
+ <td id="is-staff">
+ [[- profile.isStaff ? "yes" : "no" ]]
+ </td>
+ </tr>
+ </table>
+
+ <br><br>
+ <div id="actions">
+ [[ if (String(user._id) != String(profile._id)) { ]]
+ <button id="toggle-staff" data-isStaff="[[- !! profile.isStaff ]]">Make Staff</button>
+ [[ } ]]
+ </div>
+
+ <h2>Projects</h2>
+
+ [[- include ../_projects ]]
+
+ <br>
+ <br>
+
+ <div class="json">
+ [[- profileJSON ]]
+ </div>
+
+[[ include ../_footer ]]
diff --git a/views/staff/users/show_404.ejs b/views/staff/users/show_404.ejs
new file mode 100644
index 0000000..bcd0271
--- /dev/null
+++ b/views/staff/users/show_404.ejs
@@ -0,0 +1,13 @@
+[[ include ../_header ]]
+ <h1>User not found</h1>
+
+ <nav>
+ <a href="/staff">home</a>
+ <a href="/staff/users">users</a>
+ <a href="/staff/projects">projects</a>
+ <a href="/staff/media">media</a>
+ </nav>
+
+ <hr>
+
+[[ include ../_footer ]]