diff --git a/.buildpacks b/.buildpacks new file mode 100644 index 0000000000..b57b4bd3b0 --- /dev/null +++ b/.buildpacks @@ -0,0 +1,2 @@ +https://github.com/heroku/heroku-buildpack-nodejs.git +https://github.com/stomita/heroku-buildpack-phantomjs.git diff --git a/config.json.example b/config.json.example index 821bb91fc3..aefb56d1b9 100644 --- a/config.json.example +++ b/config.json.example @@ -25,5 +25,11 @@ "PAYPAL_SIGNATURE": "PAYPAL_SIGNATURE", "EMAIL_SERVER_URL": "http://example.com", "EMAIL_SERVER_AUTH_USER": "user", - "EMAIL_SERVER_AUTH_PASSWORD": "password" + "EMAIL_SERVER_AUTH_PASSWORD": "password", + "S3":{ + "bucket":"bucket", + "accessKeyId":"accessKeyId", + "secretAccessKey":"secretAccessKey" + } + } diff --git a/package.json b/package.json index 002cd497b3..48503271e0 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,7 @@ "main": "./src/server.js", "dependencies": { "async": "~0.9.0", + "aws-sdk": "^2.0.25", "bower": "~1.3.12", "coffee-script": "1.6.x", "connect-ratelimit": "0.0.7", @@ -25,17 +26,18 @@ "grunt-nodemon": "~0.3.0", "habitrpg-shared": "git://github.com/HabitRPG/habitrpg-shared#develop", "icalendar": "git://github.com/lefnire/node-icalendar#master", + "jade": "~1.7.0", "js2xmlparser": "~0.1.2", "lodash": "~2.4.1", - "jade": "~1.7.0", "method-override": "~2.2.0", - "mongoose": "~3.8.17", "moment": "~2.8.3", + "mongoose": "~3.8.17", "mongoose-id-autoinc": "~2013.7.14-4", "nconf": "~0.6.9", "newrelic": "~1.11.2", "nib": "~1.0.1", "nodemailer": "~0.5.2", + "pageres": "^1.0.1", "passport": "~0.2.1", "passport-facebook": "Fonger/passport-facebook#a8f98adcddad99caa9a918bc7b76462c92c5c9fd", "paypal-express-checkout": "git://github.com/HabitRPG/node-paypal-express-checkout#habitrpg", @@ -43,6 +45,7 @@ "paypal-recurring": "git://github.com/jaybryant/paypal-recurring#656b496f43440893c984700191666a5c5c535dca", "pretty-data": "git://github.com/vkiryukhin/pretty-data#master", "request": "~2.44.0", + "s3-upload-stream": "^1.0.6", "stripe": "*", "swagger-node-express": "git://github.com/lefnire/swagger-node-express#habitrpg", "universal-analytics": "~0.3.2", diff --git a/public/css/avatar.styl b/public/css/avatar.styl index 10432e1c77..20f03c3a0f 100644 --- a/public/css/avatar.styl +++ b/public/css/avatar.styl @@ -73,7 +73,17 @@ future re: pets and whatnot, this is just temporary. &.isUser background: $color-herobox // Set a different background color for the current user &:hover, &:focus - background: lighten($color-herobox, 16.18%) + background: lighten($color-herobox, 16.18%) + + .addthis_pill_style + width: 50px !important + .addthis_native_toolbox + position: absolute + top:2px + right:2px + opacity:0 + &:hover .addthis_native_toolbox + opacity:1 //Need to find a way to indicate who is leader without using background-color as it won't work when a background image is applied //.herobox.isLeader.noBackgroundImage diff --git a/public/js/controllers/footerCtrl.js b/public/js/controllers/footerCtrl.js index f1e4f7f0b5..9017602aec 100644 --- a/public/js/controllers/footerCtrl.js +++ b/public/js/controllers/footerCtrl.js @@ -39,7 +39,17 @@ // Scripts only for desktop if (!window.env.IS_MOBILE) { // Add This - $.getScript("//s7.addthis.com/js/250/addthis_widget.js#pubid=lefnire"); + //$.getScript("//s7.addthis.com/js/300/addthis_widget.js#pubid=ra-5016f6cc44ad68a4"); //FIXME why isn't this working when here? instead it's now in + var addthisServices = 'facebook,twitter,googleplus,tumblr,'+window.env.BASE_URL.replace('https://','').replace('http://',''); + window.addthis_config = { + services_custom:{ + name: "Download", + url: window.env.BASE_URL+"/export/avatar-"+User.user._id+".png", + icon: window.env.BASE_URL+"/favicon.ico" + }, + services_expanded:addthisServices, + services_compact:addthisServices + }; // Google Charts $.getScript("//www.google.com/jsapi", function() { diff --git a/public/js/static.js b/public/js/static.js index 66e2b8ee72..860dbf5587 100644 --- a/public/js/static.js +++ b/public/js/static.js @@ -6,6 +6,21 @@ window.habitrpgStatic = angular.module('habitrpgStatic', ['notificationServices' .constant("STORAGE_SETTINGS_ID", 'habit-mobile-settings') .constant("MOBILE_APP", false) +.controller("RootCtrl", ['$scope', '$location', '$modal', '$http', function($scope, $location, $modal, $http){ + // must be #?memberId=xx, see https://github.com/angular/angular.js/issues/7239 + var memberId = $location.search()['memberId']; + if (memberId) { + $http.get('/api/v2/members/'+memberId).success(function(data, status, headers, config){ + $scope.profile = data; + $scope.Content = window.habitrpgShared.content; + $modal.open({ + templateUrl: 'modals/member.html', + scope: $scope + }); + }) + } + }]) + .controller("PlansCtrl", ['$rootScope', function($rootScope) { $rootScope.clickContact = function(){ diff --git a/public/manifest.json b/public/manifest.json index aaa890463c..b2a06332a8 100644 --- a/public/manifest.json +++ b/public/manifest.json @@ -99,6 +99,8 @@ "bower_components/bootstrap/dist/css/bootstrap.css", "bower_components/css-social-buttons/css/zocial.css", "bower_components/jquery-colorbox/example1/colorbox.css", + "app.css", + "bower_components/habitrpg-shared/dist/habitrpg-shared.css", "static.css" ] } diff --git a/src/controllers/dataexport.js b/src/controllers/dataexport.js index 48a08b1d38..8a48ee1638 100644 --- a/src/controllers/dataexport.js +++ b/src/controllers/dataexport.js @@ -8,6 +8,13 @@ var js2xmlparser = require("js2xmlparser"); var pd = require('pretty-data').pd; var User = require('../models/user').model; +// Avatar screenshot/static-page includes +var Pageres = require('pageres'); //https://github.com/sindresorhus/pageres +var AWS = require('aws-sdk'); +AWS.config.update({accessKeyId: nconf.get("S3:accessKeyId"), secretAccessKey: nconf.get("S3:secretAccessKey")}); +var s3Stream = require('s3-upload-stream')(new AWS.S3()); //https://github.com/nathanpeck/s3-upload-stream +var bucket = nconf.get("S3:bucket"); +var request = require('request'); /* ------------------------------------------------------------------------ @@ -83,3 +90,49 @@ expressres.jsonstring = function(obj, headers, status) { body = pd.json(JSON.stringify(obj)); return this.send(body, headers, status); }; + +/* + ------------------------------------------------------------------------ + Static page and image screenshot of avatar + ------------------------------------------------------------------------ + */ + + +dataexport.avatarPage = function(req, res) { + User.findById(req.params.uuid).select('stats profile items achievements preferences backer contributor').exec(function(err, user){ + res.render('avatar-static', { + title: user.profile.name, + env: _.defaults({user:user},res.locals.habitrpg) + }); + }) +}; + +dataexport.avatarImage = function(req, res, next) { + var filename = 'avatar-'+req.params.uuid+'.png'; + request.head('https://'+bucket+'.s3.amazonaws.com/'+filename, function(err,response,body) { + // cache images for 10 minutes on aws, else upload a new one + if (response.statusCode==200 && moment().diff(response.headers['last-modified'], 'minutes') < 10) + return res.redirect(301, 'https://' + bucket + '.s3.amazonaws.com/' + filename); + new Pageres()//{delay:1} + .src(nconf.get('BASE_URL') + '/export/avatar-' + req.params.uuid + '.html', ['140x147'], {crop: true, filename: filename.replace('.png', '')}) + .run(function (err, file) { + if (err) return next(err); + // see http://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/S3.html#createMultipartUpload-property + var upload = s3Stream.upload({ + Bucket: bucket, + Key: filename, + ACL: "public-read", + StorageClass: "REDUCED_REDUNDANCY", + ContentType: "image/png", + Expires: +moment().add({minutes: 3}) + }); + upload.on('error', function (err) { + next(err); + }); + upload.on('uploaded', function (details) { + res.redirect(details.Location); + }); + file[0].pipe(upload); + }); + }) +}; diff --git a/src/routes/dataexport.js b/src/routes/dataexport.js index ad2e2797d0..ba27957124 100644 --- a/src/routes/dataexport.js +++ b/src/routes/dataexport.js @@ -3,11 +3,14 @@ var router = new express.Router(); var dataexport = require('../controllers/dataexport'); var auth = require('../controllers/auth'); var nconf = require('nconf'); -var i18n = require('../i18n') +var i18n = require('../i18n'); +var middleware = require('../middleware.js'); /* Data export */ router.get('/history.csv',auth.authWithSession,i18n.getUserLanguage,dataexport.history); //[todo] encode data output options in the data controller and use these to build routes router.get('/userdata.xml',auth.authWithSession,i18n.getUserLanguage,dataexport.leanuser,dataexport.userdata.xml); router.get('/userdata.json',auth.authWithSession,i18n.getUserLanguage,dataexport.leanuser,dataexport.userdata.json); +router.get('/avatar-:uuid.html', i18n.getUserLanguage, middleware.locals, dataexport.avatarPage); +router.get('/avatar-:uuid.png', i18n.getUserLanguage, middleware.locals, dataexport.avatarImage); module.exports = router; diff --git a/views/avatar-static.jade b/views/avatar-static.jade new file mode 100644 index 0000000000..9223081dba --- /dev/null +++ b/views/avatar-static.jade @@ -0,0 +1,28 @@ +doctype html +html(ng-app="habitrpg") + head + title=title + link(rel='shortcut icon', href='#{env.getBuildUrl("favicon.ico")}?v=3') + + meta(charset='utf-8') + meta(name='viewport', content='width=device-width, initial-scale=1.0') + meta(name='apple-mobile-web-app-capable', content='yes') + + script(type='text/javascript'). + window.env = !{JSON.stringify(env)}; + + != env.getManifestFiles("app") + + script(type='text/javascript'). + window.habitrpg + .controller('StaticAvatarCtrl', ['$scope', function($scope){ + $scope.profile = window.env.user; + }]) + + //webfonts + link(href='//fonts.googleapis.com/css?family=Lato:300,400,700,400italic,700italic', rel='stylesheet', type='text/css') + + body(ng-cloak) + include ./shared/header/avatar + div(ng-controller='StaticAvatarCtrl') + +herobox({main:true}) diff --git a/views/index.jade b/views/index.jade index 802e3873eb..bd9f42f9f5 100644 --- a/views/index.jade +++ b/views/index.jade @@ -10,6 +10,9 @@ html(ng-app="habitrpg", ng-controller="RootCtrl", ng-class='{"applying-action":a meta(name='viewport', content='width=device-width, initial-scale=1.0') meta(name='apple-mobile-web-app-capable', content='yes') + //FIXME for some reason this won't load when in footerCtrl.js#deferredScripts() + script(type="text/javascript", src="//s7.addthis.com/js/300/addthis_widget.js#pubid=ra-5016f6cc44ad68a4", async="async") + script(type='text/javascript'). window.env = !{JSON.stringify(env)}; diff --git a/views/shared/header/avatar.jade b/views/shared/header/avatar.jade index 415abe6ede..41d70ba1ea 100644 --- a/views/shared/header/avatar.jade +++ b/views/shared/header/avatar.jade @@ -7,6 +7,8 @@ mixin avatar(opts) .character-sprites + .addthis_native_toolbox(ng-if='profile._id==user._id', data-url="#{env.BASE_URL}/static/front/#?memberId={{profile._id}}", data-title="Check out my HabitRPG progress!") + // Mount Body if !opts.minimal span(ng-if='profile.items.currentMount', class='Mount_Body_{{profile.items.currentMount}}') diff --git a/views/static/front.jade b/views/static/front.jade index 2e03b1fef6..9927ef3551 100644 --- a/views/static/front.jade +++ b/views/static/front.jade @@ -8,41 +8,45 @@ block title title=env.t('titleFront') block content - .marketing - //we need to use something that's not jumbotron for this, but still keep it centered - //could someone write something else to make it pretty? - img(src='/bower_components/habitrpg-shared/img/logo/habitrpg_pixel.png', alt='HabitRPG logo') - //this image needs to be replaced by something more enticing, that shows off the features of hRPG - //while acting similarly to a logo - h1#tagline=env.t('tagline') - p.lead - button.btn.btn-primary.btn-lg#frontpage-play-button(ng-click='playButtonClick()')=env.t('playButton') - hr - img(src='/marketing/devices.png') - //we'd want the tagline centered, for sure, and a bit more pop, but without using jumbotron - //it could also be part of the image, as long as the alt text included it - //in fact, I think I really want it on the image, rather than as text, but language issues - br - p.lead=env.t('landingp1') - h2=env.t('landingp2header') - //images in these parts could be useful, too - //if there's a language workaround, image headers? people like pictures! - p.lead - =env.t('landingp2') - |  - h2=env.t('landingp3header') - //I'm not sold on "Consquences as the title here. Anyone got a better idea? - p.lead - =env.t('landingp3') - |  - h2=env.t('landingp4header') - p.lead=env.t('landingp4') - //- TODO - h2=env.t('landingend') + div(ng-controller='RootCtrl') + include ../shared/header/avatar + include ../shared/modals/members + + .marketing + //we need to use something that's not jumbotron for this, but still keep it centered + //could someone write something else to make it pretty? + img(src='/bower_components/habitrpg-shared/img/logo/habitrpg_pixel.png', alt='HabitRPG logo') + //this image needs to be replaced by something more enticing, that shows off the features of hRPG + //while acting similarly to a logo + h1#tagline=env.t('tagline') p.lead - =env.t('landingend2') - a(href="FEATURESPAGEHERE")=env.t('landingfeatureslink') - =env.t('landingend3') - a(href="ENTERPRISEPAGEHERE")=env.t('landingadminlink') - |  - =env.t('landingend4') + button.btn.btn-primary.btn-lg#frontpage-play-button(ng-click='playButtonClick()')=env.t('playButton') + hr + img(src='/marketing/devices.png') + //we'd want the tagline centered, for sure, and a bit more pop, but without using jumbotron + //it could also be part of the image, as long as the alt text included it + //in fact, I think I really want it on the image, rather than as text, but language issues + br + p.lead=env.t('landingp1') + h2=env.t('landingp2header') + //images in these parts could be useful, too + //if there's a language workaround, image headers? people like pictures! + p.lead + =env.t('landingp2') + |  + h2=env.t('landingp3header') + //I'm not sold on "Consquences as the title here. Anyone got a better idea? + p.lead + =env.t('landingp3') + |  + h2=env.t('landingp4header') + p.lead=env.t('landingp4') + //- TODO + h2=env.t('landingend') + p.lead + =env.t('landingend2') + a(href="FEATURESPAGEHERE")=env.t('landingfeatureslink') + =env.t('landingend3') + a(href="ENTERPRISEPAGEHERE")=env.t('landingadminlink') + |  + =env.t('landingend4')