summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Procfile3
-rw-r--r--config.json.example1
-rw-r--r--package.json38
-rw-r--r--public/assets/javascripts/ui/site/EditSubscriptionModal.js21
-rwxr-xr-xpublic/assets/stylesheets/app.css24
-rw-r--r--public/assets/stylesheets/staff.css18
-rw-r--r--server/index.js2
-rw-r--r--server/lib/api/index.js1
-rw-r--r--server/lib/api/profile.js1
-rw-r--r--server/lib/api/subscription.js22
-rw-r--r--server/lib/middleware.js2
-rw-r--r--server/lib/schemas/Plan.js44
-rw-r--r--server/lib/schemas/Subscription.js26
-rw-r--r--server/lib/schemas/User.js6
-rw-r--r--server/lib/views/index.js28
-rw-r--r--server/lib/views/staff.js231
-rw-r--r--server/lib/webhook/index.js44
-rw-r--r--server/lib/webhook/recurly-config.js6
-rw-r--r--server/lib/webhook/webhook.js140
-rw-r--r--server/repl.js2
-rw-r--r--views/about/_blank.ejs4
-rw-r--r--views/about/about.ejs18
-rw-r--r--views/about/brochure.ejs137
-rw-r--r--views/about/howto.ejs2
-rw-r--r--views/builder.ejs2
-rw-r--r--views/docs.ejs2
-rwxr-xr-xviews/editor.ejs2
-rwxr-xr-xviews/home.ejs2
-rw-r--r--views/modal.ejs2
-rw-r--r--views/partials/edit-subscription.ejs52
-rw-r--r--views/profile.ejs84
-rw-r--r--views/reader.ejs2
-rw-r--r--views/staff/_header.ejs2
-rw-r--r--views/staff/_nav.ejs8
-rw-r--r--views/staff/index.ejs6
-rw-r--r--views/staff/media/index.ejs7
-rw-r--r--views/staff/media/show.ejs7
-rw-r--r--views/staff/media/show_404.ejs9
-rw-r--r--views/staff/plans/_form.ejs149
-rw-r--r--views/staff/plans/edit.ejs13
-rw-r--r--views/staff/plans/index.ejs65
-rw-r--r--views/staff/plans/new.ejs13
-rw-r--r--views/staff/projects/index.ejs7
-rw-r--r--views/staff/projects/show.ejs9
-rw-r--r--views/staff/projects/show_404.ejs7
-rw-r--r--views/staff/subscriptions/index.ejs36
-rw-r--r--views/staff/subscriptions/show.ejs12
-rw-r--r--views/staff/users/index.ejs7
-rw-r--r--views/staff/users/media.ejs13
-rw-r--r--views/staff/users/show.ejs7
-rw-r--r--views/staff/users/show_404.ejs14
51 files changed, 1187 insertions, 173 deletions
diff --git a/Procfile b/Procfile
index 983db91..bab08fb 100644
--- a/Procfile
+++ b/Procfile
@@ -1 +1,2 @@
-web: node server \ No newline at end of file
+web: node server
+webhook: node server/lib/webhook \ No newline at end of file
diff --git a/config.json.example b/config.json.example
index 6028021..b8c310c 100644
--- a/config.json.example
+++ b/config.json.example
@@ -3,6 +3,7 @@
"hostName": "lvh.me",
"port": 3000,
"socketPort": 1337,
+ "webhookPort": 5000,
"databaseHost": "lvh.me",
"pageSize": 10,
"env": { "development": 1 }
diff --git a/package.json b/package.json
index adefb82..8afb96e 100644
--- a/package.json
+++ b/package.json
@@ -6,32 +6,34 @@
"url": "git://github.com/okfocus/vvalls.git"
},
"dependencies": {
- "express": "~3.4.8",
- "monk": "~0.7.1",
- "socket.io": "~0.9.16",
+ "body-parser": "1.3.0",
"connect-mongo": "~0.4.1",
- "passport": "~0.2.0",
- "passport-local": "~1.0.0",
- "passport-twitter": "~1.0.2",
- "passport-facebook": "~1.0.3",
- "passport.socketio": "~3.0.1",
- "node-restful": "~0.1.14",
"ejs": "^0.8.8",
- "useful-string": "0.0.1",
+ "emailjs": "~0.3.6",
+ "express": "~3.4.8",
"express-subdomain-handler": "~0.1.0",
"express-subdomains": "0.0.5",
+ "html-entities": "~1.0.10",
+ "intro.js": "^0.9.0",
+ "knox": "~0.8.10",
"lodash": "~2.4.1",
+ "marked": "~0.3.2",
+ "moment": "~2.6.0",
"mongoose": "~3.8.8",
- "mongoose-unique-validator": "~0.3.0",
"mongoose-lifecycle": "~1.0.0",
- "knox": "~0.8.10",
- "moment": "~2.6.0",
- "html-entities": "~1.0.10",
+ "mongoose-unique-validator": "~0.3.0",
+ "monk": "~0.7.1",
"multer": "~0.1.0",
- "body-parser": "1.3.0",
- "marked": "~0.3.2",
- "emailjs": "~0.3.6",
- "intro.js": "^0.9.0"
+ "node-recurly": "^2.1.0",
+ "node-restful": "~0.1.14",
+ "passport": "~0.2.0",
+ "passport-facebook": "~1.0.3",
+ "passport-local": "~1.0.0",
+ "passport-twitter": "~1.0.2",
+ "passport.socketio": "~3.0.1",
+ "socket.io": "~0.9.16",
+ "useful-string": "0.0.1",
+ "xml2js": "^0.4.4"
},
"devDependencies": {
"grunt": "~0.4.1",
diff --git a/public/assets/javascripts/ui/site/EditSubscriptionModal.js b/public/assets/javascripts/ui/site/EditSubscriptionModal.js
new file mode 100644
index 0000000..1b3b859
--- /dev/null
+++ b/public/assets/javascripts/ui/site/EditSubscriptionModal.js
@@ -0,0 +1,21 @@
+
+var EditSubscriptionModal = ModalFormView.extend({
+ el: ".mediaDrawer.editSubscription",
+ action: "/api/subscription",
+ method: "put",
+
+ fixedClose: true,
+
+ events: {
+ "click [data-role='changePasswordToggle']": 'togglePasswordFields'
+ },
+
+ load: function(){
+ this.reset()
+ $.get("/api/subscription", function(data){
+
+ this.show()
+ }.bind(this))
+ },
+
+})
diff --git a/public/assets/stylesheets/app.css b/public/assets/stylesheets/app.css
index 62a5682..0ce2c5e 100755
--- a/public/assets/stylesheets/app.css
+++ b/public/assets/stylesheets/app.css
@@ -604,7 +604,9 @@ iframe.embed {
.page h1:nth-child(2) {
margin-top: 40px;
}
-
+.page h1.leader {
+ margin-top: 60px;
+}
.page p {
margin: 20px;
}
@@ -673,7 +675,21 @@ iframe.embed {
font-weight: 300;
}
+.aboutintro {
+ text-align: center;
+ line-height: 43px;
+ font-size: 24px;
+ padding: 5% 0;
+ font-weight: 300;
+}
+.aboutintro .inner {
+ max-width: 800px;
+ margin: 0 auto;
+ text-align: center;
+}
+
/* PROFILE PAGE */
+
.profilePic {
background-size: cover;
background-position: center;
@@ -2468,7 +2484,7 @@ form li {
form label {
float:left;
}
-form input[type="text"],form input[type="password"] {
+form input[type="text"],form input[type="password"],form input[type="number"] {
border: 1px solid;
font-size: 20px;
padding: 5px;
@@ -3038,8 +3054,8 @@ a[data-role="forgot-password"] {
form li {
font-size: 16px;
}
- form input[type="text"], form input[type="password"] {
- font-size: 15px;
+ form input[type="text"],form input[type="password"],form input[type="number"] {
+ font-size: 15px;
}
.page h1 {
font-size: 26px;
diff --git a/public/assets/stylesheets/staff.css b/public/assets/stylesheets/staff.css
index de31571..fffadbf 100644
--- a/public/assets/stylesheets/staff.css
+++ b/public/assets/stylesheets/staff.css
@@ -67,10 +67,26 @@ hr {
.staff .body a {
border-bottom: 1px dotted;
}
-.staff .editLinks a {
+.staff .editLinks a, .staff a.bluelink {
color: #00f;
border-bottom: 1px solid;
}
+.staff form {
+ max-width: none;
+ width: 600px;
+ padding: 20px;
+}
+.staff form label {
+ float: none;
+}
+.staff form p {
+ width: 350px;
+ margin: 5px 0;
+ color: #444;
+}
+.staff form div li {
+ width: 180px;
+}
#iframe-embed, #iframe-embed tr, #iframe-embed td {
width: 79vw;
}
diff --git a/server/index.js b/server/index.js
index 9a9323c..02fea3c 100644
--- a/server/index.js
+++ b/server/index.js
@@ -72,7 +72,7 @@ site.setup = function(){
server = http.createServer(app)
server.listen(app.get('port'), function () {
- console.log('Express server listening on port ' + app.get('port'));
+ console.log('Vvalls server listening on port ' + app.get('port'));
});
// var io = websocket.listen(server)
diff --git a/server/lib/api/index.js b/server/lib/api/index.js
index 11e13fc..9478d9b 100644
--- a/server/lib/api/index.js
+++ b/server/lib/api/index.js
@@ -8,6 +8,7 @@ var api = {
projects: require('./projects'),
rooms: require('./rooms'),
collaborator: require('./collaborator'),
+ subscription: require('./subscription'),
}
module.exports = api
diff --git a/server/lib/api/profile.js b/server/lib/api/profile.js
index 996505f..d72a2c3 100644
--- a/server/lib/api/profile.js
+++ b/server/lib/api/profile.js
@@ -32,6 +32,7 @@ var profile = {
delete data.old_password
delete data.new_password
delete data.isStaff
+ delete data.plan_level
data.updated_at = new Date ()
if (req.files.avatar) {
diff --git a/server/lib/api/subscription.js b/server/lib/api/subscription.js
new file mode 100644
index 0000000..6fe8c61
--- /dev/null
+++ b/server/lib/api/subscription.js
@@ -0,0 +1,22 @@
+/* jshint node: true */
+
+var _ = require('lodash'),
+ util = require('../util'),
+ upload = require('../upload'),
+ config = require('../../../config.json'),
+ User = require('../schemas/User'),
+ Project = require('../schemas/Project'),
+ Layout = require('../schemas/Layout'),
+ Subscription = require('../schemas/Subscription');
+
+var subscription = module.exports = {
+
+/*
+ index: function(req, res){
+ Project.find({ user_id: req.user._id }, function(err, docs){
+ res.json(docs)
+ })
+ },
+*/
+
+}; \ No newline at end of file
diff --git a/server/lib/middleware.js b/server/lib/middleware.js
index 4848ab0..797d677 100644
--- a/server/lib/middleware.js
+++ b/server/lib/middleware.js
@@ -75,7 +75,7 @@ var middleware = {
}
next()
})
- }
+ }
else {
req.project = null
next()
diff --git a/server/lib/schemas/Plan.js b/server/lib/schemas/Plan.js
new file mode 100644
index 0000000..8a19b99
--- /dev/null
+++ b/server/lib/schemas/Plan.js
@@ -0,0 +1,44 @@
+/* jshint node: true */
+
+var mongoose = require('mongoose'),
+ _ = require('lodash'),
+ crypto = require('crypto'),
+ config = require('../../../config.json'),
+ util = require('../util');
+
+var PlanSchema = new mongoose.Schema({
+ name: { type: String },
+ slug: { type: String },
+
+ level: { type: Number },
+
+ monthly_price: { type: Number },
+ yearly_price: { type: Number },
+
+ basic_layout_monthly_price: { type: Number },
+ basic_layout_yearly_price: { type: Number },
+
+ pro_layout_monthly_price: { type: Number },
+ pro_layout_yearly_price: { type: Number },
+
+ basic_layout_limit: { type: Number },
+ pro_layout_limit: { type: Number },
+
+ stock_project_limit: { type: Number },
+ basic_project_limit: { type: Number },
+ pro_project_limit: { type: Number },
+
+ permissions: {
+ basic_editor: { type: Boolean, default: false },
+ pro_editor: { type: Boolean, default: false },
+ solids: { type: Boolean, default: false },
+ collaborators: { type: Boolean, default: false },
+ no_logo: { type: Boolean, default: false },
+ },
+
+ created_at: { type: Date, default: Date.now },
+ updated_at: { type: Date, default: Date.now },
+})
+
+module.exports = exports = mongoose.model('plan', PlanSchema);
+exports.schema = PlanSchema;
diff --git a/server/lib/schemas/Subscription.js b/server/lib/schemas/Subscription.js
new file mode 100644
index 0000000..2f49ea1
--- /dev/null
+++ b/server/lib/schemas/Subscription.js
@@ -0,0 +1,26 @@
+/* jshint node: true */
+
+var mongoose = require('mongoose'),
+ _ = require('lodash'),
+ crypto = require('crypto'),
+ config = require('../../../config.json'),
+ util = require('../util');
+
+var SubscriptionSchema = new mongoose.Schema({
+ user_id: { type: mongoose.Schema.ObjectId, index: true },
+
+ plan_type: { type: String, default: "free" },
+ plan_period: { type: String, default: "monthly" },
+
+ subscription_uuid: { type: String },
+ subscription_add_ons: [{
+ name: { type: String },
+ quantity: { type: Number },
+ }],
+
+ created_at: { type: Date, default: Date.now },
+ updated_at: { type: Date, default: Date.now },
+})
+
+module.exports = exports = mongoose.model('subscription', SubscriptionSchema);
+exports.schema = SubscriptionSchema;
diff --git a/server/lib/schemas/User.js b/server/lib/schemas/User.js
index 180a140..06ad33d 100644
--- a/server/lib/schemas/User.js
+++ b/server/lib/schemas/User.js
@@ -54,7 +54,11 @@ var UserSchema = new mongoose.Schema({
type: String,
default: "",
},
-
+
+ plan_level: { type: Number, default: 0 },
+ plan_type: { type: String, default: "free" },
+ last_charged: { type: Date, default: null },
+
location: { type: String, default: "" },
photo: { type: String, default: "" },
bio: { type: String, default: "" },
diff --git a/server/lib/views/index.js b/server/lib/views/index.js
index 8c3e63d..0ce0357 100644
--- a/server/lib/views/index.js
+++ b/server/lib/views/index.js
@@ -4,6 +4,7 @@ var User = require('../schemas/User'),
Project = require('../schemas/Project'),
Documentation = require('../schemas/Documentation'),
Collaborator = require('../schemas/Collaborator'),
+ Plan = require('../schemas/Plan'),
config = require('../../../config'),
marked = require('marked'),
util = require('../util'),
@@ -88,13 +89,6 @@ var views = module.exports = {
},
home: function (req, res) {
- // while in development, blank homepage if not logged in
-/*
- if (! req.user) {
- res.send("<html></html>")
- return
- }
-*/
views_middleware.fetchProjects({ featured: true }, null, null, function(err, projects){
res.render('home', {
projects: projects || []
@@ -117,6 +111,13 @@ var views = module.exports = {
res.render('about/' + name)
return
}
+ if (name == "brochure" || name == "plans") {
+ // TODO: fetch plans
+ views_middleware.ensurePlans(req, res, function(){
+ res.render('about/' + name)
+ })
+ return
+ }
if (name == "about" || name == "index") {
res.render('about/' + name)
return
@@ -225,6 +226,19 @@ var views = module.exports = {
}
var views_middleware = {
+ ensurePlans: function(req, res, next){
+ Plan.find(function (err, plans) {
+ res.locals.plans = {}
+ plans.forEach(function(plan){
+ res.locals.plans[ plan.slug ] = plan
+ "monthly_price yearly_price basic_layout_monthly_price basic_layout_yearly_price pro_layout_monthly_price pro_layout_yearly_price".split(" ").forEach(function(key){
+ plan[key] = (plan[key]/100).toFixed(2)+""
+ })
+ })
+ next()
+ })
+ },
+
fetchProjects: function (criteria, limit, offset, next) {
limit = limit || 7
offset = offset || 0
diff --git a/server/lib/views/staff.js b/server/lib/views/staff.js
index 49f492b..74dd7cd 100644
--- a/server/lib/views/staff.js
+++ b/server/lib/views/staff.js
@@ -4,6 +4,8 @@ var User = require('../schemas/User'),
Project = require('../schemas/Project'),
Media = require('../schemas/Media'),
Collaborator = require('../schemas/Collaborator'),
+ Plan = require('../schemas/Plan'),
+ Subscription = require('../schemas/Subscription'),
config = require('../../../config'),
middleware = require('../middleware'),
util = require('../util'),
@@ -16,6 +18,10 @@ 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",
+ plans: "monthly_price yearly_price basic_layout_monthly_price basic_layout_yearly_price " +
+ "pro_layout_monthly_price pro_layout_yearly_price " +
+ "basic_layout_limit pro_layout_limit stock_project_limit basic_project_limit pro_project_limit",
+ plans_permissions: "basic_editor pro_editor solids collaborators no_logo",
},
defaults: {
@@ -119,6 +125,77 @@ var staff = module.exports = {
})
},
+ ensureSubscriptions: 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 'created':
+ sort = {'created_at': -1}
+ break
+ default:
+ case 'date':
+ sort = {'updated_at': -1}
+ break
+ }
+ Subscription.find(criteria)
+ .select(staff.fields.project)
+ .sort(sort)
+ .skip(offset)
+ .limit(limit)
+ .exec(function (err, subscriptions) {
+ res.locals.subscriptions = subscriptions.map(staff.helpers.subscription)
+ next()
+ })
+ },
+
+ ensurePlans: function(req, res, next){
+ Plan.find(function (err, plans) {
+ res.locals.plans = (plans || []).map(staff.helpers.plan)
+ res.locals.plans.sort(function(a,b){ return a.monthly_price })
+ next()
+ })
+ },
+ ensurePlan: function (req, res, next) {
+ if (req.params.slug) {
+ Plan.findOne({ slug: req.params.slug }, function(err, plan){
+ if (err || ! plan) {
+ console.error(err)
+ res.redirect("/staff/plans/")
+ }
+ else {
+ req.plan = plan
+ next()
+ }
+ })
+ }
+ else {
+ res.redirect("/staff/plans/")
+ }
+ },
+
+ ensureSubscription: function (req, res, next) {
+ if (req.params.id) {
+ Subscription.findOne({ _id: req.params.id }, function(err, subscription){
+ if (err || ! subscription) {
+ console.error(err)
+ res.redirect("/staff/subscriptions/")
+ }
+ else {
+ req.subscription = subscription
+ next()
+ }
+ })
+ }
+ else {
+ res.redirect("/staff/subscriptions/")
+ }
+ },
+
ensureRecentProjects: function(req, res, next){
var dreq = { params: { sort: 'created_at', limit: 20, offset: 0 } }
staff.middleware.ensureProjects(dreq, res, next)
@@ -129,11 +206,23 @@ var staff = module.exports = {
staff.middleware.ensureObjectsUsers(res.locals.projects, next)
},
+ ensureSubscriptionsUsers: function(req, res, next){
+ if (! res.locals.subscriptions || ! res.locals.subscriptions.length) { return next() }
+ staff.middleware.ensureObjectsUsers(res.locals.subscriptions, next)
+ },
+
ensureMediaUsers: function(req, res, next){
if (! res.locals.media || ! res.locals.media.length) { return next() }
staff.middleware.ensureObjectsUsers(res.locals.media, next)
},
+ ensureSubscriptionUser: function(req, res, next){
+ if (! res.locals.subscription) { return next() }
+ staff.middleware.ensureObjectsUsers([ res.locals.subscription ], function(){
+ next()
+ })
+ },
+
ensureMediaUser: function(req, res, next){
if (! res.locals.media) { return next() }
staff.middleware.ensureObjectsUsers([ res.locals.media ], function(){
@@ -299,7 +388,21 @@ var staff = module.exports = {
media.user = {}
media.shortUrl = media.url.replace(/^http.:\/\//,"")
return media
- }
+ },
+
+ plan: function(plan){
+ plan = plan.toObject()
+ plan.date = moment( plan.updated_at || plan.created_at ).format("M/DD/YYYY hh:mm a")
+ plan.user = {}
+ return plan
+ },
+
+ subscription: function(subscription){
+ subscription = subscription.toObject()
+ subscription.date = moment( subscription.updated_at || subscription.created_at ).format("M/DD/YYYY hh:mm a")
+ subscription.user = {}
+ return subscription
+ },
},
route: function(app){
@@ -422,7 +525,67 @@ var staff = module.exports = {
staff.media.show
);
+
+ //
+ // plans
+
+ app.get('/staff/plans',
+ middleware.ensureAuthenticated,
+ middleware.ensureIsStaff,
+
+ staff.middleware.ensurePlans,
+
+ staff.plans.index
+ );
+ app.get('/staff/plans/new',
+ middleware.ensureAuthenticated,
+ middleware.ensureIsStaff,
+
+ staff.plans.new
+ );
+ app.post('/staff/plans/new',
+ middleware.ensureAuthenticated,
+ middleware.ensureIsStaff,
+
+ staff.plans.create
+ );
+ app.get('/staff/plans/:slug',
+ middleware.ensureAuthenticated,
+ middleware.ensureIsStaff,
+
+ staff.middleware.ensurePlan,
+ staff.plans.edit
+ );
+ app.post('/staff/plans/:slug',
+ middleware.ensureAuthenticated,
+ middleware.ensureIsStaff,
+
+ staff.middleware.ensurePlan,
+
+ staff.plans.update
+ );
+
+ //
+ // subscriptions
+ app.get('/staff/subscriptions',
+ middleware.ensureAuthenticated,
+ middleware.ensureIsStaff,
+
+ staff.middleware.ensureSubscriptions,
+ staff.middleware.ensureSubscriptionsUsers,
+
+ staff.subscriptions.index
+ );
+ app.get('/staff/subscriptions/:id',
+ middleware.ensureAuthenticated,
+ middleware.ensureIsStaff,
+
+ staff.middleware.ensureSubscription,
+ staff.middleware.ensureSubscriptionUser,
+
+ staff.subscriptions.show
+ );
},
paginate: function(req, res){
@@ -540,6 +703,70 @@ var staff = module.exports = {
res.render('staff/media/show_404')
}
},
- }
+ },
+
+ plans: {
+ index: function(req, res){
+ res.locals.fields = staff.fields.plans.split(" ")
+ res.locals.permissions = staff.fields.plans_permissions.split(" ")
+ res.render('staff/plans/index')
+ },
+ new: function(req, res){
+ res.locals.plan = new Plan ()
+ res.render('staff/plans/new')
+ },
+ edit: function(req, res){
+ res.locals.plan = req.plan
+ res.render('staff/plans/edit')
+ },
+ create: function(req, res){
+ var plan = new Plan ()
+ var fields = staff.fields.plans.split(" ")
+ var permissions = staff.fields.plans_permissions.split(" ")
+
+ var data = util.cleanQuery(req.body)
+ data.name = util.sanitize(data.name)
+ data.slug = util.sanitize(data.slug.toLowerCase())
+
+ permissions.forEach(function(field){
+ data[field] = data["permissions_" + field]
+ })
+
+ new Plan (data).save(function(err, doc){
+ if (err || ! doc) { return res.json({ error: err }) }
+ res.redirect("/staff/plans/")
+ })
+ },
+ update: function(req, res){
+ var fields = staff.fields.plans.split(" ")
+ var permissions = staff.fields.plans_permissions.split(" ")
+
+ var data = util.cleanQuery(req.body)
+ data.name = util.sanitize(data.name)
+ data.slug = util.sanitize(data.slug.toLowerCase())
+
+ _.extend(req.plan, data)
+ permissions.forEach(function(field){
+ req.plan.permissions[field] = data["permissions_" + field].length == 2
+ })
+
+ req.plan.save(function(err, doc){
+ if (err || ! doc) { return res.json({ error: err }) }
+ res.redirect("/staff/plans/")
+ })
+ },
+ },
+
+ subscriptions: {
+ index: function(req, res){
+ res.locals.pagination.count = res.locals.subscriptions.length
+ res.locals.pagination.max = res.locals.subscriptionCount
+ staff.paginate(req, res)
+ res.render('staff/subscriptions/index')
+ },
+ show: function(req, res){
+ res.render('staff/subscriptions/show')
+ },
+ },
}
diff --git a/server/lib/webhook/index.js b/server/lib/webhook/index.js
new file mode 100644
index 0000000..11419c2
--- /dev/null
+++ b/server/lib/webhook/index.js
@@ -0,0 +1,44 @@
+var config = require('../../../config.json'),
+ http = require('http'),
+ express = require('express'),
+ bodyParser = require('body-parser'),
+ mongoose = require('mongoose');
+
+var http = require('http'),
+ express = require('express'),
+ bodyParser = require('body-parser'),
+ multer = require('multer'),
+ MongoStore = require('connect-mongo')(express),
+ passport = require('passport'),
+ path = require('path'),
+ mongoose = require('mongoose');
+
+var webhook = require('./webhook');
+
+var app = express()
+var server
+var DATABASE_URI = process.env.MONGOLAB_URI || ('mongodb://' + config.databaseHost + '/vvalls')
+
+var site = {}
+
+site.init = function(){
+ mongoose.connect(DATABASE_URI, {}, site.ready);
+}
+
+site.ready = function(){
+ app.set('port', config.webhookPort);
+ app.use(bodyParser());
+ app.use(express.query());
+ app.set('env', config.env.production ? "production" : "development")
+ app.get('env') === 'development' && app.use(express.errorHandler());
+
+ server = http.createServer(app)
+ server.listen(app.get('port'), function () {
+ console.log('Webhook server listening on port ' + app.get('port'));
+ });
+
+ app.get('/', function(req,res){ res.send('HI THERE') })
+ webhook.route(app)
+}
+
+site.init()
diff --git a/server/lib/webhook/recurly-config.js b/server/lib/webhook/recurly-config.js
new file mode 100644
index 0000000..3d7e1c5
--- /dev/null
+++ b/server/lib/webhook/recurly-config.js
@@ -0,0 +1,6 @@
+module.exports = {
+ API_KEY: process.env['VVALLS_RECURLY_SECRET'],
+ SUBDOMAIN: 'vvalls',
+ ENVIRONMENT: 'sandbox',
+ DEBUG: true,
+};
diff --git a/server/lib/webhook/webhook.js b/server/lib/webhook/webhook.js
new file mode 100644
index 0000000..e9a7925
--- /dev/null
+++ b/server/lib/webhook/webhook.js
@@ -0,0 +1,140 @@
+// // where should this live?
+// app.get('/subscribe/webhook', views.subscription.webhook);
+
+/*
+app.use(express.basicAuth(function(user, pass, callback) {
+ var result = (user === 'testUser' && pass === 'testPass');
+ callback(null, result);
+}));
+*/
+
+
+/* jshint node: true */
+
+var User = require('../schemas/User'),
+ Subscription = require('../schemas/Subscription'),
+ config = require('../../../config'),
+ middleware = require('../middleware'),
+ util = require('../util'),
+ _ = require('lodash'),
+ moment = require('moment'),
+ xml2js = require('xml2js'),
+ Recurly = require('node-recurly'),
+ recurly = new Recurly(require('./recurly-config'));
+
+var parser = new xml2js.Parser();
+
+var subscribe = module.exports = {
+ plan_level: {
+ free: 0,
+ basic: 1,
+ pro: 2,
+ },
+
+ callbacks: {
+/*
+ // accounts
+ new_account_notification: function(data, user){
+ // fires on successful signup
+ },
+ canceled_account_notification: function(data, user){
+ },
+ billing_info_updated_notification: function(data, user){
+ },
+ reactivated_account_notification: function(data, user){
+ },
+
+ // invoices
+ new_invoice_notification: function(data, user){
+ },
+ closed_invoice_notification: function(data, user){
+ },
+ past_due_invoice_notification: function(data, user){
+ },
+*/
+
+ // subscriptions
+ new_subscription_notification: function(data, user){
+ var account = data.account
+ Subscription.findOne({ "uuid": data.subscription.uuid }, function(err, subscription){
+ if (err || subscription) return;
+
+ var plan = data.subscription.plan.plan_code.split("-")
+ var plan_type = plan[0]
+ var plan_period = plan[1]
+
+ user.plan_type = plan_type
+ user.plan_level = subscribe.plan_level[plan_type]
+
+ var subscriber = new Subscription ()
+ subscriber.uuid = data.subscription.uuid
+ subscriber.user_id = user._id
+ subscriber.plan_type = plan_type
+ subscriber.plan_period = plan_period
+ subscriber.plan_level = subscribe.plan_level[plan_type]
+ subscriber.add_ons = subscription.add_ons.map(function(add_on){
+ return {
+ name: add_on.plan,
+ quantity: add_on.quantity,
+ }
+ })
+ subscriber.save(function(err, data){
+ if (err) return;
+ user.save(function(err){
+ // saved!
+ })
+ })
+ })
+ },
+
+/*
+ updated_subscription_notification: function(data, user){
+ },
+ canceled_subscription_notification: function(data, user){
+ },
+ expired_subscription_notification: function(data, user){
+ },
+ renewed_subscription_notification: function(data, user){
+ },
+*/
+ // payments
+ successful_payment_notification: function(data, user){
+ var account = data.account
+ user.last_charged = new Date(data.transaction.date)
+ user.save(function(){
+ })
+ },
+/*
+ failed_payment_notification: function(data, user){
+ },
+ successful_refund_notification: function(data, user){
+ },
+ void_payment_notification: function(data, user){
+ },
+*/
+ },
+
+ execute: function(action, data){
+ User.findOne({ _id: data.account.account_code }, function(err, user){
+ if (err) { return }
+ subscribe.callbacks[action](data, user)
+ })
+ },
+
+ // then calls to get appropriate info from the recurly api
+ handle: function(req, res){
+ res.status(200).end()
+ parser.parseString(data, function (err, result) {
+ console.log(inspect(result, { colors: true, depth: Infinity }));
+ for (var action in result) {
+ if (subscribe.callbacks[action]) {
+ subscribe.execute(action, result[action]);
+ }
+ }
+ });
+ },
+
+ route: function(app){
+ app.post('/subscribe/webhook', subscribe.handle);
+ },
+}
diff --git a/server/repl.js b/server/repl.js
index ba94d45..353d8c5 100644
--- a/server/repl.js
+++ b/server/repl.js
@@ -9,6 +9,8 @@ mongoose.connect('mongodb://' + DB_HOST + '/vvalls', {}, function(){
"./lib/schemas/Layout",
"./lib/schemas/Media",
"./lib/schemas/Project",
+ "./lib/schemas/Plan",
+ "./lib/schemas/Subscription",
].forEach(function(modName){
// console.log(name, modName)
var namez = modName.split("/"), name = namez[namez.length-1];
diff --git a/views/about/_blank.ejs b/views/about/_blank.ejs
index 0e9ea7e..732ea3f 100644
--- a/views/about/_blank.ejs
+++ b/views/about/_blank.ejs
@@ -1,14 +1,14 @@
<!doctype html>
<html>
<head>
- <title>vvalls</title>
+ <title>VValls</title>
[[ include ../partials/meta ]]
</head>
<body class="loading">
<div class="rapper page home">
[[ include ../partials/header ]]
-
+ <h1 class="leader">Hallo</h1>
<!-- put stuff here -->
diff --git a/views/about/about.ejs b/views/about/about.ejs
index dd536be..6d5f011 100644
--- a/views/about/about.ejs
+++ b/views/about/about.ejs
@@ -1,14 +1,14 @@
<!doctype html>
<html>
<head>
- <title>vvalls</title>
+ <title>About VValls</title>
[[ include ../partials/meta ]]
</head>
<body class="loading">
<div class="rapper page">
[[ include ../partials/header ]]
- <h1>About VValls</h1>
+ <h1 class="leader">About VValls</h1>
<div class="projectList about aboutintro">
<div class="inner">
@@ -50,17 +50,3 @@
</body>
[[ include ../partials/scripts ]]
</html>
-<style>
-.aboutintro {
- text-align: center;
- line-height: 43px;
- font-size: 24px;
- padding: 5% 0;
- font-weight: 300;
-}
-.aboutintro .inner {
- max-width: 800px;
- margin: 0 auto;
- text-align: center;
-}
-</style> \ No newline at end of file
diff --git a/views/about/brochure.ejs b/views/about/brochure.ejs
new file mode 100644
index 0000000..1c808f8
--- /dev/null
+++ b/views/about/brochure.ejs
@@ -0,0 +1,137 @@
+<!doctype html>
+<html>
+<head>
+ <title>VValls Subscriptions</title>
+ [[ include ../partials/meta ]]
+</head>
+<body class="loading">
+ <div class="rapper page">
+ [[ include ../partials/header ]]
+
+ <h1 class="leader">Subscriptions</h1>
+
+ <div class="projectList about aboutintro">
+ <div class="inner">
+ Want to get more out of VValls? Consider becoming a subscription user.
+ <br><br>
+ </div>
+
+ <div class="about_plan planbox">
+ <h3>[[- plans.free.name ]]</h3>
+ <ul>
+ <li> [[- plans.free.stock_project_limit ]] exhibition with pre-designed template floor plan
+ </ul>
+ </div>
+
+ <div class="about_plan planbox">
+ <h3>[[- plans.basic.name ]]</h3>
+ <ul>
+ <li> $[[- plans.basic.monthly_price ]]/mo or $[[- plans.basic.yearly_price ]]/year
+ <li> Comes with [[- plans.basic.basic_layout_limit ]] basic floor plan and [[- plans.basic.basic_project_limit ]] exhibitions
+ <li> Each new basic floor plan costs $[[- plans.basic.basic_layout_monthly_price ]]/mo
+ or $[[- plans.basic.basic_layout_yearly_price ]]/year, minimum 3 months
+ <li> Each new floor plan can have up to [[- plans.basic.basic_project_limit ]] exhibitions
+ <li> VValls logo appears when embedding an exhibition on a web page
+ <li>
+ [[ if (! logged_in) { ]]
+ <button href="/signup">Sign Up</button>
+ [[ } else if (! user.plan_level || user.plan_level < plan.level) { ]]
+ <button href="https://vvalls.recurly.com/subscribe/basic/[[- user._id ]]/[[- user.username ]]">Buy Now</button>
+ [[ } else if (user.plan_level == plan.level) { ]]
+ Current Level
+ [[ } else { ]]
+ [[ } ]]
+ </ul>
+ </div>
+
+ <div class="about_plan planbox">
+ <h3>[[- plans.pro.name ]]</h3>
+ <ul>
+ <li> $[[- plans.pro.monthly_price ]]/mo or $[[- plans.pro.yearly_price ]]/year
+ <li> Comes with [[- plans.pro.pro_layout_limit ]] pro floor plan and [[- plans.pro.pro_project_limit ]] exhibitions
+ <li> Each new pro floor plan costs $[[- plans.pro.pro_layout_monthly_price ]]/mo
+ or $[[- plans.pro.pro_layout_yearly_price ]]/year, minimum 3 months
+ <li> Each new pro floor plan can have up to [[- plans.pro.pro_project_limit ]] exhibitions
+ <li> Includes planning for 3D objects in the room
+ <li> No VValls logo on embed
+ <li>
+ [[ if (! logged_in) { ]]
+ <button href="/signup">Sign Up</button>
+ [[ } else if (! user.plan_level || user.plan_level < plan.level) { ]]
+ <button href="https://vvalls.recurly.com/subscribe/pro/[[- user._id ]]/[[- user.username ]]">Buy Now</button>
+ [[ } else if (user.plan_level == plan.level) { ]]
+ Current Level
+ [[ } ]]
+ </ul>
+ </div>
+
+ <div class="about_custom planbox miscbox">
+ <ul>
+ <li> Buying any extra floor plan unlocks collaboration. Invite an artist or curator to work on the exhibition with you.
+ <li> Basic Floor plan: Rectangle-based design of any dimension.
+ <li> Pro Floor plan: Trace an arbitrary floor plan from image.
+ </ul>
+ </div>
+
+ <div class="about_custom planbox">
+ <h3>Want Something Custom?</h3>
+ <li> We offer customized white-label options for business and educational uses.
+ <li> <a href="mailto:hello@vvalls.com">Contact us</a> for more information.
+ </div>
+ </div>
+
+
+ [[ include ../partials/confirm-modal ]]
+ [[ include ../projects/layouts-modal ]]
+ [[ include ../partials/sign-in ]]
+ [[ include ../partials/footer ]]
+
+ </div>
+</body>
+[[ include ../partials/scripts ]]
+</html>
+<style>
+/* nb these styles should be fixed for narrower screens/mobile layout */
+.about_plan {
+ width: 28vw;
+ margin: 2vw;
+ float: left;
+ min-height: 28vw;
+ position: relative;
+}
+.about_plan button {
+ position: absolute;
+ bottom: 5%;
+ left: 5%;
+ width: 90%;
+}
+.about_custom {
+ clear: both;
+ width: 76vw;
+ margin: 2vw 10vw;
+}
+.planbox {
+ padding: 2vw;
+ border: 1px solid #ddd;
+ background: white;
+ border-radius: 25px;
+ font-size: 15px;
+ line-height: 23px;
+ text-align: center;
+}
+.planbox h3 {
+ text-align: center;
+ margin-bottom: 10px;
+}
+.planbox li {
+ list-style-type: none;
+ margin-bottom: 5px;
+}
+.planbox.miscbox {
+ border: 0;
+ background: #f8f8f8;
+}
+.about_custom a {
+ border-bottom: 1px solid;
+}
+</style> \ No newline at end of file
diff --git a/views/about/howto.ejs b/views/about/howto.ejs
index 5278a40..914c3b3 100644
--- a/views/about/howto.ejs
+++ b/views/about/howto.ejs
@@ -1,7 +1,7 @@
<!doctype html>
<html>
<head>
- <title>vvalls</title>
+ <title>How to Use VValls</title>
[[ include ../partials/meta ]]
</head>
<body class="loading">
diff --git a/views/builder.ejs b/views/builder.ejs
index afb8c66..0ba4238 100644
--- a/views/builder.ejs
+++ b/views/builder.ejs
@@ -1,7 +1,7 @@
<!doctype html>
<html>
<head>
- <title>vvalls</title>
+ <title>VValls</title>
[[ include partials/meta ]]
</head>
<body class="editing loading">
diff --git a/views/docs.ejs b/views/docs.ejs
index b3ead82..a1f081f 100644
--- a/views/docs.ejs
+++ b/views/docs.ejs
@@ -1,7 +1,7 @@
<!doctype html>
<html>
<head>
- <title>vvalls</title>
+ <title>VValls</title>
[[ include partials/meta ]]
</head>
<body class="loading">
diff --git a/views/editor.ejs b/views/editor.ejs
index 656615c..74e4d6d 100755
--- a/views/editor.ejs
+++ b/views/editor.ejs
@@ -1,7 +1,7 @@
<!doctype html>
<html>
<head>
- <title>vvalls</title>
+ <title>VValls</title>
[[ include partials/meta ]]
</head>
<body class="editing loading">
diff --git a/views/home.ejs b/views/home.ejs
index 36fc2fc..ffb0976 100755
--- a/views/home.ejs
+++ b/views/home.ejs
@@ -1,7 +1,7 @@
<!doctype html>
<html>
<head>
- <title>vvalls</title>
+ <title>VValls</title>
[[ include partials/meta ]]
</head>
<body class="loading">
diff --git a/views/modal.ejs b/views/modal.ejs
index 7ca869c..732953d 100644
--- a/views/modal.ejs
+++ b/views/modal.ejs
@@ -1,7 +1,7 @@
<!doctype html>
<html>
<head>
- <title>vvalls</title>
+ <title>VValls</title>
[[ include partials/meta ]]
</head>
<body class="loading">
diff --git a/views/partials/edit-subscription.ejs b/views/partials/edit-subscription.ejs
new file mode 100644
index 0000000..1f8db62
--- /dev/null
+++ b/views/partials/edit-subscription.ejs
@@ -0,0 +1,52 @@
+<div class="mediaDrawer fixed animate editSubscription">
+ <span class="close">X</span>
+ <div id="form_container">
+ <form enctype="multipart/form-data" method="post">
+ <input type="hidden" name="_csrf" value="[[- token ]]">
+ <ul>
+ <li class="section_break">
+ <h3>Edit Subscription</h3>
+ </li>
+ <div id="free_plan">
+ You are currently using the free plan. For access to all of Vvalls features,
+ consider upgrading to a paid plan.
+ <p>
+ <a href="/about/brochure">View the Plans</a>
+ </div>
+ <div id="free_plan">
+ Your current plan level is <span id="user_plan_type"></span>
+
+ <table>
+ <tr>
+ <td>Basic plan</td>
+ <td></td>
+ <td>@ $<span></span>/<span></span></td>
+ </tr>
+ <tr>
+ <td>Additional basic layouts</td>
+ <td></td>
+ <td>@ $<span></span>/<span></span></td>
+ <td>Buy more</td>
+ </tr>
+ <tr>
+ <td>Additional PRO layouts</td>
+ <td></td>
+ <td>$<span></span>/<span></span></td>
+ <td>Buy more</td>
+ </tr>
+ <tr>
+ <td>Total</td>
+ <td></td>
+ <td>$<span></span>/<span></span></td>
+ </tr>
+ </table>
+
+ <button>Upgrade your subscription</button>
+
+ <button>Cancel your subscription</button>
+ [[ } ]]
+ </li>
+ </ul>
+ </form>
+ </div>
+</div>
diff --git a/views/profile.ejs b/views/profile.ejs
index a62652c..e149847 100644
--- a/views/profile.ejs
+++ b/views/profile.ejs
@@ -1,7 +1,7 @@
<!doctype html>
<html>
<head>
- <title>vvalls</title>
+ <title>VValls | [[- profile.displayName ]]</title>
[[ include partials/meta ]]
</head>
<body class="loading">
@@ -9,48 +9,53 @@
[[- include partials/header ]]
<div class="profilepage">
- [[ if (profile.photo && profile.photo.length) { ]]
- <div class="profilePic" style="background-image:url([[- profile.photo ]])">
- </div>
- [[ } else { ]]
- <div class="profilePic noPic">
- <span class="holder">
- <span class="ion-ios7-person-outline"></span>
- [[ if (isOwnProfile) { ]]
- <div>click to add profile pic</div>
- <input id="profile_avatar" name="avatar" class="element file" type="file">
- [[ } ]]
- </span>
- </div>
+ [[ if (profile.photo && profile.photo.length) { ]]
+ <div class="profilePic" style="background-image:url([[- profile.photo ]])">
+ </div>
+ [[ } else { ]]
+ <div class="profilePic noPic">
+ <span class="holder">
+ <span class="ion-ios7-person-outline"></span>
+ [[ if (isOwnProfile) { ]]
+ <div>click to add profile pic</div>
+ <input id="profile_avatar" name="avatar" class="element file" type="file">
[[ } ]]
- <div class="bio">
- <div class="holder">
- <h2>[[- profile.displayName ]]</h2>
- [[ if (profile.location) { ]]
- <span>
- [[- profile.location ]]
- </span>
- [[ } ]]
- [[ if (profile.website && profile.website.length) { ]]
- <span>
- <a href="[[- profile.website ]]" target="_blank">[[- profile.website ]]</a>
- </span>
- [[ } ]]
- [[ if (profile.twitterName && profile.twitterName.length) { ]]
- <span>
- <a href="https://twitter.com/[[- profile.twitterName ]]" target="_blank">@[[- profile.twitterName ]]</a>
- </span>
- [[ } ]]
- </div>
- </div>
-
+ </span>
+ </div>
+ [[ } ]]
+ <div class="bio">
+ <div class="holder">
+ <h2>[[- profile.displayName ]]</h2>
+ [[ if (profile.location) { ]]
+ <span>
+ [[- profile.location ]]
+ </span>
+ [[ } ]]
+ [[ if (profile.website && profile.website.length) { ]]
+ <span>
+ <a href="[[- profile.website ]]" target="_blank">[[- profile.website ]]</a>
+ </span>
+ [[ } ]]
+ [[ if (profile.twitterName && profile.twitterName.length) { ]]
+ <span>
+ <a href="https://twitter.com/[[- profile.twitterName ]]" target="_blank">@[[- profile.twitterName ]]</a>
+ </span>
+ [[ } ]]
+ [[ if (profile.plan_level == 1) { ]]
+ <span class="plan_level premium">PREMIUM</span>
+ [[ } else if (profile.plan_level == 2) { ]]
+ <span class="plan_level pro">PRO</span>
+ [[ } ]]
+ </div>
+ </div>
[[ if (projects.length) { ]]
+
<h1>[[- profile.username ]] has [[- projectCount ]] project[[- projectCount != 1 ? "s" : "" ]]</h1>
-
[[ include projects/list-projects ]]
+
[[ } else { ]]
-
+
<h1>Welcome to VVALLS</h1>
<div class="projectList about">
<h2>
@@ -69,8 +74,11 @@
<h3>This person has no projects.</h3>
[[ } ]]
</div>
+
[[ } ]]
- </div>
+
+ </div>
+ [[ include partials/edit-subscription ]]
[[ include partials/edit-profile ]]
[[ include projects/layouts-modal ]]
[[ include projects/edit-project ]]
diff --git a/views/reader.ejs b/views/reader.ejs
index 6c9856a..7035356 100644
--- a/views/reader.ejs
+++ b/views/reader.ejs
@@ -1,7 +1,7 @@
<!doctype html>
<html>
<head>
- <title>vvalls</title>
+ <title>VValls</title>
[[ include partials/meta ]]
</head>
<body class="loading reader">
diff --git a/views/staff/_header.ejs b/views/staff/_header.ejs
index 3bbf4f1..a73c12e 100644
--- a/views/staff/_header.ejs
+++ b/views/staff/_header.ejs
@@ -1,7 +1,7 @@
<!doctype html>
<html>
<head>
- <title>vvalls | staff</title>
+ <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>
diff --git a/views/staff/_nav.ejs b/views/staff/_nav.ejs
new file mode 100644
index 0000000..e79ff69
--- /dev/null
+++ b/views/staff/_nav.ejs
@@ -0,0 +1,8 @@
+<nav>
+ <a href="/staff">home</a>
+ <a href="/staff/users">users</a>
+ <a href="/staff/projects">projects</a>
+ <a href="/staff/media">media</a>
+ <a href="/staff/plans">plans</a>
+ <a href="/staff/subscriptions">subscriptions</a>
+</nav> \ No newline at end of file
diff --git a/views/staff/index.ejs b/views/staff/index.ejs
index 5ca7269..1b73641 100644
--- a/views/staff/index.ejs
+++ b/views/staff/index.ejs
@@ -2,11 +2,7 @@
<h1>Staff Area</h1>
- <nav>
- <a href="/staff/users">users</a>
- <a href="/staff/projects">projects</a>
- <a href="/staff/media">media</a>
- </nav>
+ [[ include _nav ]]
<hr>
diff --git a/views/staff/media/index.ejs b/views/staff/media/index.ejs
index 516af2d..3805c8e 100644
--- a/views/staff/media/index.ejs
+++ b/views/staff/media/index.ejs
@@ -2,12 +2,7 @@
<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>
+[[ include ../_nav ]]
<hr>
diff --git a/views/staff/media/show.ejs b/views/staff/media/show.ejs
index 76dcd32..9d05cb9 100644
--- a/views/staff/media/show.ejs
+++ b/views/staff/media/show.ejs
@@ -2,12 +2,7 @@
<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>
+[[ include ../_nav ]]
<hr>
diff --git a/views/staff/media/show_404.ejs b/views/staff/media/show_404.ejs
index f07cef2..c6bac6f 100644
--- a/views/staff/media/show_404.ejs
+++ b/views/staff/media/show_404.ejs
@@ -2,13 +2,8 @@
<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>
-
+[[ include ../_nav ]]
+
<hr>
[[ include ../_footer ]]
diff --git a/views/staff/plans/_form.ejs b/views/staff/plans/_form.ejs
new file mode 100644
index 0000000..ae5ca5a
--- /dev/null
+++ b/views/staff/plans/_form.ejs
@@ -0,0 +1,149 @@
+<input type="hidden" id="_csrf" name="_csrf" value="[[- token ]]">
+
+<ul>
+
+<li class="section_break">
+ <h3>New Plan</h3>
+</li>
+
+<li>
+ <label for="plan_name">Name</label>
+ <div><input id="plan_name" name="name" type="text" value="[[- plan.name ]]"></div>
+</li>
+
+<li>
+ <label for="plan_slug">Slug</label>
+ <div><input id="plan_slug" name="slug" type="text" value="[[- plan.slug ]]"></div>
+</li>
+
+<!-- - - - - -->
+
+<li class="section_break">
+ <h3>Plan Pricing</h3>
+</li>
+
+<li>
+ <label for="plan_monthly_price">Monthly Price</label>
+ <div><input id="plan_monthly_price" name="monthly_price" type="number" value="[[- plan.monthly_price ]]"></div>
+</li>
+
+<li>
+ <label for="plan_yearly_price">Yearly Price</label>
+ <div><input id="plan_yearly_price" name="yearly_price" type="number" value="[[- plan.yearly_price ]]"></div>
+</li>
+
+<p>
+ <i>Note:</i> Pricing should be in cents, i.e. a price of $10.00 should be entered as 1000.
+</p>
+
+<!-- - - - - -->
+
+<li class="section_break">
+ <h3>Additional Template Pricing</h3>
+</li>
+
+<li>
+ <label for="plan_basic_layout_monthly_price">Basic Template Price (Monthly)</label>
+ <div><input id="plan_basic_layout_monthly_price" name="basic_layout_monthly_price" type="number" value="[[- plan.basic_layout_monthly_price ]]"></div>
+</li>
+
+<li>
+ <label for="plan_basic_layout_yearly_price">Basic Template Price (Yearly)</label>
+ <div><input id="plan_basic_layout_yearly_price" name="basic_layout_yearly_price" type="number" value="[[- plan.basic_layout_yearly_price ]]"></div>
+</li>
+
+<li>
+ <label for="plan_pro_layout_monthly_price">Pro Template Price (Monthly)</label>
+ <div><input id="plan_pro_layout_monthly_price" name="pro_layout_monthly_price" type="number" value="[[- plan.pro_layout_monthly_price ]]"></div>
+</li>
+
+<li>
+ <label for="plan_pro_layout_yearly_price">Pro Template Price (Yearly)</label>
+ <div><input id="plan_pro_layout_yearly_price" name="pro_layout_yearly_price" type="number" value="[[- plan.pro_layout_yearly_price ]]"></div>
+</li>
+
+<!-- - - - - -->
+
+<li class="section_break">
+ <h3>Per-Plan Template Limits</h3>
+</li>
+
+<li>
+ <label for="plan_basic_layout_limit">Basic Template Limit</label>
+ <div><input id="plan_basic_layout_limit" name="basic_layout_limit" type="number" value="[[- plan.basic_layout_limit ]]"></div>
+</li>
+
+<li>
+ <label for="plan_pro_layout_limit">Pro Template Limit</label>
+ <div><input id="plan_pro_layout_limit" name="pro_layout_limit" type="number" value="[[- plan.pro_layout_limit ]]"></div>
+</li>
+
+<!-- - - - - -->
+
+<li class="section_break">
+ <h3>Per-Plan Project Limits</h3>
+</li>
+
+<li>
+ <label for="plan_stock_project_limit">Stock Project Limit</label>
+ <div><input id="plan_stock_project_limit" name="stock_project_limit" type="number" value="[[- plan.stock_project_limit ]]"></div>
+</li>
+
+<li>
+ <label for="plan_basic_project_limit">Basic Project Limit</label>
+ <div><input id="plan_basic_project_limit" name="basic_project_limit" type="number" value="[[- plan.basic_project_limit ]]"></div>
+</li>
+
+<li>
+ <label for="plan_pro_project_limit">Pro Layout Limit</label>
+ <div><input id="plan_pro_project_limit" name="pro_project_limit" type="number" value="[[- plan.pro_project_limit ]]"></div>
+</li>
+
+<!-- - - - - -->
+
+<li class="section_break">
+ <h3>Permissions</h3>
+</li>
+
+<div>
+<li>
+ <input name="permissions_basic_editor" type="hidden" value="0">
+ <input id="plan_permissions_basic_editor" name="permissions_basic_editor" type="checkbox" value="1" [[ if (plan.permissions.basic_editor) { ]]checked[[ } ]]>
+ <label for="plan_permissions_basic_editor">Basic Editor</label>
+</li>
+
+<li>
+ <input name="permissions_pro_editor" type="hidden" value="0">
+ <input id="plan_permissions_pro_editor" name="permissions_pro_editor" type="checkbox" value="1" [[ if (plan.permissions.pro_editor) { ]]checked[[ } ]]>
+ <label for="plan_permissions_pro_editor">Pro Editor</label>
+</li>
+
+<li>
+ <input name="permissions_solids" type="hidden" value="0">
+ <input id="plan_permissions_solids" name="permissions_solids" type="checkbox" value="1" [[ if (plan.permissions.solids) { ]]checked[[ } ]]>
+ <label for="plan_permissions_solids">3D Objects</label>
+</li>
+
+<li>
+ <input name="permissions_collaborators" type="hidden" value="0">
+ <input id="plan_permissions_collaborators" name="permissions_collaborators" type="checkbox" value="1" [[ if (plan.permissions.collaborators) { ]]checked[[ } ]]>
+ <label for="plan_permissions_collaborators">Collaborators</label>
+</li>
+
+<li>
+ <input name="permissions_no_logo" type="hidden" value="0">
+ <input id="plan_permissions_no_logo" name="permissions_no_logo" type="checkbox" value="1" [[ if (plan.permissions.no_logo) { ]]checked[[ } ]]>
+ <label for="plan_permissions_no_logo">No Logo</label>
+</li>
+
+<p>
+ These permissions should harmonize with the restrictions on layouts set above.
+</p>
+
+</div>
+
+<li>
+ <input type="submit" value="Save Changes">
+</li>
+
+</ul>
diff --git a/views/staff/plans/edit.ejs b/views/staff/plans/edit.ejs
new file mode 100644
index 0000000..9848873
--- /dev/null
+++ b/views/staff/plans/edit.ejs
@@ -0,0 +1,13 @@
+[[ include ../_header ]]
+
+ <h1>Edit Plan</h1>
+
+[[ include ../_nav ]]
+
+ <hr>
+
+<form action="" method="post">
+[[- include _form ]]
+</form>
+
+[[ include ../_footer ]]
diff --git a/views/staff/plans/index.ejs b/views/staff/plans/index.ejs
new file mode 100644
index 0000000..16fcf14
--- /dev/null
+++ b/views/staff/plans/index.ejs
@@ -0,0 +1,65 @@
+[[ include ../_header ]]
+
+ <h1>Plans</h1>
+
+[[ include ../_nav ]]
+
+ <hr>
+
+<table>
+ <tr>
+ <td>
+ </td>
+ [[ plans.forEach(function(plan){ ]]
+ <td style="text-align:right; min-width: 100px;">
+ <b style="text-transform:uppercase">[[- plan.name ]]</b>
+ </td>
+ [[ }) ]]
+ </tr>
+
+ <tr class="editLinks">
+ <td>
+ <a href="/staff/plans/new">New Plan</a>
+ </td>
+ [[ plans.forEach(function(plan){ ]]
+ <td style="text-align:right">
+ [<a href="/staff/plans/[[- plan.slug ]]">edit</a>]
+ </td>
+ [[ }) ]]
+ </tr>
+
+ [[ fields.forEach(function(field){ ]]
+ <tr>
+ <td style="text-transform:capitalize">[[- field.replace(/_/g," ") ]]</td>
+ [[ plans.forEach(function(plan){ ]]
+ <td style="text-align:right">
+ [[ if (field.indexOf("_price") != -1) { ]]
+ [[- plan[field] == 0 ? "" : "$" + (plan[field]/100).toFixed(2) ]]
+ [[ } else { ]]
+ [[- plan[field] ]]
+ [[ } ]]
+ </td>
+ [[ }) ]]
+ </tr>
+ [[ }) ]]
+
+ <tr>
+ <td style="border-bottom: 1px dotted #888;" colspan='10'></td>
+ </tr>
+
+ [[ permissions.forEach(function(permission){ ]]
+ <tr>
+ <td style="text-transform:capitalize">[[- permission.replace(/_/g," ") ]]</td>
+ [[ plans.forEach(function(plan){ ]]
+ <td style="text-align:right">
+ [[- plan.permissions[permission] ? "<b>x</b>" : " " ]]
+ </td>
+ [[ }) ]]
+ </tr>
+ [[ }) ]]
+</table>
+<br clear="all">
+
+ <hr>
+
+[[ include ../_footer ]]
diff --git a/views/staff/plans/new.ejs b/views/staff/plans/new.ejs
new file mode 100644
index 0000000..297d3d6
--- /dev/null
+++ b/views/staff/plans/new.ejs
@@ -0,0 +1,13 @@
+[[ include ../_header ]]
+
+ <h1>New Plan</h1>
+
+[[ include ../_nav ]]
+
+ <hr>
+
+<form action="" method="post">
+[[- include _form ]]
+</form>
+
+[[ include ../_footer ]]
diff --git a/views/staff/projects/index.ejs b/views/staff/projects/index.ejs
index 482ea25..e4ba469 100644
--- a/views/staff/projects/index.ejs
+++ b/views/staff/projects/index.ejs
@@ -2,12 +2,7 @@
<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>
+[[ include ../_nav ]]
<hr>
diff --git a/views/staff/projects/show.ejs b/views/staff/projects/show.ejs
index 1034b31..b090a41 100644
--- a/views/staff/projects/show.ejs
+++ b/views/staff/projects/show.ejs
@@ -2,13 +2,8 @@
<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>
-
+[[ include ../_nav ]]
+
<hr>
<table>
diff --git a/views/staff/projects/show_404.ejs b/views/staff/projects/show_404.ejs
index 70320c0..f5e1658 100644
--- a/views/staff/projects/show_404.ejs
+++ b/views/staff/projects/show_404.ejs
@@ -2,12 +2,7 @@
<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>
+[[ include ../_nav ]]
<hr>
diff --git a/views/staff/subscriptions/index.ejs b/views/staff/subscriptions/index.ejs
new file mode 100644
index 0000000..d1c0588
--- /dev/null
+++ b/views/staff/subscriptions/index.ejs
@@ -0,0 +1,36 @@
+[[ include ../_header ]]
+
+ <h1>Users</h1>
+
+[[ include ../_nav ]]
+
+ <hr>
+
+[[ include ../_pagination ]]
+
+<table id="users">
+[[ subscriptions.forEach(function(subscription){ ]]
+ <tr>
+ <td>
+ <a href="/staff/subscriptions/[[- subscription._id ]]"><div style="background-image:url([[- subscription.user.photo ]])" class="avatar"></div></a>
+ </td>
+ <td>
+ <a href="/staff/subscriptions/[[- subscription._id ]]">[[- subscription.user.username ]]</a>
+ </td>
+ <td>
+ [[- subscription.user.displayName ]]
+ </td>
+ <td class="editLinks">
+ <a href="/profile/[[- user.username ]]">[view profile]</a>
+ </td>
+ <td>
+ [[- subscription.user.last_seen ]]
+ </td>
+ </tr>
+[[ }) ]]
+</table>
+
+
+[[ include ../_pagination ]]
+
+[[ include ../_footer ]]
diff --git a/views/staff/subscriptions/show.ejs b/views/staff/subscriptions/show.ejs
new file mode 100644
index 0000000..e2839a6
--- /dev/null
+++ b/views/staff/subscriptions/show.ejs
@@ -0,0 +1,12 @@
+[[ include ../_header ]]
+ <h1>User: [[- subscription.user.username ]]</h1>
+
+[[ include ../_nav ]]
+
+ <hr>
+
+<pre>
+info to show..
+- link to recurly profile
+- link to vvalls profile
+- subscription tier + add-ons
diff --git a/views/staff/users/index.ejs b/views/staff/users/index.ejs
index f14d666..1795dde 100644
--- a/views/staff/users/index.ejs
+++ b/views/staff/users/index.ejs
@@ -2,12 +2,7 @@
<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>
+[[ include ../_nav ]]
<hr>
diff --git a/views/staff/users/media.ejs b/views/staff/users/media.ejs
index c1097dd..8927c00 100644
--- a/views/staff/users/media.ejs
+++ b/views/staff/users/media.ejs
@@ -1,15 +1,10 @@
[[ include ../_header ]]
- <h1>User Media: [[- profile.username ]]</h1>
+<h1>User Media: [[- 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>
+[[ include ../_nav ]]
+
+<hr>
[[ include ../_pagination ]]
[[ include ../_gallery ]]
diff --git a/views/staff/users/show.ejs b/views/staff/users/show.ejs
index 8e9b447..4ce1d9a 100644
--- a/views/staff/users/show.ejs
+++ b/views/staff/users/show.ejs
@@ -1,12 +1,7 @@
[[ 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>
+[[ include ../_nav ]]
<hr>
diff --git a/views/staff/users/show_404.ejs b/views/staff/users/show_404.ejs
index bcd0271..11663fa 100644
--- a/views/staff/users/show_404.ejs
+++ b/views/staff/users/show_404.ejs
@@ -1,13 +1,9 @@
[[ 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>
+<h1>User not found</h1>
+
+[[ include ../_nav ]]
+
+<hr>
[[ include ../_footer ]]