",{"class":"amaran-wrapper-inner"}).appendTo(r)),"object"==typeof this.config.content?c=null!=this.config.themeTemplate?this.config.themeTemplate(this.config.content):n[this.config.theme.split(" ")[0]+"Theme"](this.config.content):(this.config.content={},this.config.content.message=this.config.message,this.config.content.color="#27ae60",c=n.defaultTheme(this.config.content)),i={"class":this.config.themeTemplate?"amaran "+this.config.content.themeName:this.config.theme&&!this.config.themeTemplate?"amaran "+this.config.theme:"amaran",html:this.buildHTML(c)},this.config.clearAll&&t(".amaran").remove(),a=t("
",i).appendTo(s),"center"===o[0]&&this.centerCalculate(r,s),this.animation(this.config.inEffect,a,"show"),this.config.onClick&&(e=this,t(a).css({cursor:"default"}),t(a).on("click",function(i){return t(i.target).is(".amaran-close")||t(i.target).is(".amaran-sticky")?void i.preventDefault():void e.config.onClick()})),this.config.resetTimeout&&(e=this,t(a).on("mouseenter",function(){return e.resetTimeout()}),t(a).on("mouseleave",function(){return e.resumeTimeout(a)})),this.config.overlay&&t(".amaran-overlay").length<=0&&t("body").prepend('
'),this.config.stickyButton&&(e=this,t(a).find(".amaran-sticky").on("click",function(){return t(this).hasClass("sticky")?(e.resumeTimeout(a),t(this).removeClass("sticky")):(e.resetTimeout(),t(this).addClass("sticky"))})),this.config.sticky!==!0&&this.hideDiv(a)},resetTimeout:function(){var t;return t=this,clearTimeout(t.timeout)},resumeTimeout:function(t){var i;return i=this,i.timeout=setTimeout(function(){return i.animation(i.config.outEffect,t,"hide")},i.config.delay)},buildHTML:function(t){return this.config.closeButton&&(t='
'+t),this.config.stickyButton&&(t='
'+t),t},centerCalculate:function(t,i){var e,n,o;n=i.find(".amaran").length,o=i.height(),e=(t.height()-o)/2,i.find(".amaran:first-child").animate({"margin-top":e},200)},animation:function(t,i,e){return"fadeIn"===t||"fadeOut"===t?this.fade(i,e):"show"===t?this.cssanimate(i,e):this.slide(t,i,e)},fade:function(t,i){var e;return e=this,"show"===i?this.config.cssanimationIn?t.addClass("animated "+this.config.cssanimationIn).show():t.fadeIn():this.config.cssanimationOut?(t.addClass("animated "+this.config.cssanimationOut),t.css({"min-height":0,height:t.outerHeight()}),void t.animate({opacity:0},function(){t.animate({height:0},function(){e.removeIt(t)})})):(t.css({"min-height":0,height:t.outerHeight()}),void t.animate({opacity:0},function(){t.animate({height:0},function(){e.removeIt(t)})}))},removeIt:function(i){var e,n;clearTimeout(this.timeout),i.remove(),n=t(this.config.wrapper+"."+this.config.position.split(" ")[0]+"."+this.config.position.split(" ")[1]),e=n.find(".amaran-wrapper-inner"),"center"===this.config.position.split(" ")[0]&&this.centerCalculate(n,e),this.config.afterEnd(),this.config.overlay&&0===t(".amaran").length&&t(".amaran-overlay").fadeOut(400,function(){return t(this).remove()})},getWidth:function(t){var i,e;return i=t.clone().hide().appendTo("body"),e=i.outerWidth()+i.outerWidth()/2,i.remove(),e},getInfo:function(i){var e,n;return e=i.offset(),n=t(this.config.wrapper).offset(),{t:e.top,l:e.left,h:i.height(),w:i.outerWidth(),wT:n.top,wL:n.left,wH:t(this.config.wrapper).outerHeight(),wW:t(this.config.wrapper).outerWidth()}},getPosition:function(e,n){var o,a,s;return o=this.getInfo(e),a=this.config.position.split(" ")[1],s={slideTop:{start:{top:-(o.wT+o.wH+2*o.h)},move:{top:0},hide:{top:-(o.t+2*o.h)},height:o.h},slideBottom:{start:{top:t(i).height()-o.wH+2*o.h},move:{top:0},hide:{top:t(i).height()-o.wH+2*o.h},height:o.h},slideLeft:{start:{left:"left"===a?1.5*-o.w:-t(i).width()},move:{left:0},hide:{left:"left"===a?1.5*-o.w:-t(i).width()},height:o.h},slideRight:{start:{left:"right"===a?1.5*o.w:t(i).width()},move:{left:0},hide:{left:"right"===a?1.5*o.w:t(i).width()},height:o.h}},s[n]?s[n]:0},slide:function(t,i,e){var n,o;return o=this.getPosition(i,t),"show"!==e?(n=this,i.animate(o.hide,function(){i.css({"min-height":0,height:o.height},function(){i.html(" ")})}).animate({height:0},function(){return n.removeIt(i)})):void i.show().css(o.start).animate(o.move)},close:function(){var i;return i=this,t("[data-amaran-close]").on("click",function(){i.animation(i.config.outEffect,t(this).closest("div.amaran"),"hide")}),!this.config.closeOnClick&&this.config.closeButton?void i.animation(i.config.outEffect,t(this).parent("div.amaran"),"hide"):void(this.config.closeOnClick&&t(".amaran").on("click",function(){i.animation(i.config.outEffect,t(this),"hide")}))},hideDiv:function(t){var i;i=this,i.timeout=setTimeout(function(){i.animation(i.config.outEffect,t,"hide")},i.config.delay)}},n={defaultTheme:function(t){var i;return i="","undefined"!=typeof t.color&&(i=t.color),"
"+t.message+"
"},awesomeTheme:function(t){return'
'+t.title+"
"+t.message+' '+t.info+"
"},userTheme:function(t){return'
'+t.user+" "+t.message+"
"},colorfulTheme:function(t){var i,e;return"undefined"!=typeof t.color&&(e=t.color),"undefined"!=typeof t.bgcolor&&(i=t.bgcolor),"
"+t.message+"
"},tumblrTheme:function(t){return'
'+t.title+'
'+t.message+"
"}},t.amaran=function(t){var i;return i=new e(t)},t.amaran.close=function(){return t(".amaran-wrapper").remove(),!1}}(jQuery,window,document)}).call(this);
--------------------------------------------------------------------------------
/client/compatibility/jquery.easing.min.js:
--------------------------------------------------------------------------------
1 | /*
2 | * jQuery Easing v1.3 - http://gsgd.co.uk/sandbox/jquery/easing/
3 | *
4 | * Uses the built in easing capabilities added In jQuery 1.1
5 | * to offer multiple easing options
6 | *
7 | * TERMS OF USE - EASING EQUATIONS
8 | *
9 | * Open source under the BSD License.
10 | *
11 | * Copyright © 2001 Robert Penner
12 | * All rights reserved.
13 | *
14 | * TERMS OF USE - jQuery Easing
15 | *
16 | * Open source under the BSD License.
17 | *
18 | * Copyright © 2008 George McGinley Smith
19 | * All rights reserved.
20 | *
21 | * Redistribution and use in source and binary forms, with or without modification,
22 | * are permitted provided that the following conditions are met:
23 | *
24 | * Redistributions of source code must retain the above copyright notice, this list of
25 | * conditions and the following disclaimer.
26 | * Redistributions in binary form must reproduce the above copyright notice, this list
27 | * of conditions and the following disclaimer in the documentation and/or other materials
28 | * provided with the distribution.
29 | *
30 | * Neither the name of the author nor the names of contributors may be used to endorse
31 | * or promote products derived from this software without specific prior written permission.
32 | *
33 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
34 | * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
35 | * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
36 | * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
37 | * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
38 | * GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
39 | * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
40 | * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
41 | * OF THE POSSIBILITY OF SUCH DAMAGE.
42 | *
43 | */
44 | jQuery.easing.jswing=jQuery.easing.swing;jQuery.extend(jQuery.easing,{def:"easeOutQuad",swing:function(e,f,a,h,g){return jQuery.easing[jQuery.easing.def](e,f,a,h,g)},easeInQuad:function(e,f,a,h,g){return h*(f/=g)*f+a},easeOutQuad:function(e,f,a,h,g){return -h*(f/=g)*(f-2)+a},easeInOutQuad:function(e,f,a,h,g){if((f/=g/2)<1){return h/2*f*f+a}return -h/2*((--f)*(f-2)-1)+a},easeInCubic:function(e,f,a,h,g){return h*(f/=g)*f*f+a},easeOutCubic:function(e,f,a,h,g){return h*((f=f/g-1)*f*f+1)+a},easeInOutCubic:function(e,f,a,h,g){if((f/=g/2)<1){return h/2*f*f*f+a}return h/2*((f-=2)*f*f+2)+a},easeInQuart:function(e,f,a,h,g){return h*(f/=g)*f*f*f+a},easeOutQuart:function(e,f,a,h,g){return -h*((f=f/g-1)*f*f*f-1)+a},easeInOutQuart:function(e,f,a,h,g){if((f/=g/2)<1){return h/2*f*f*f*f+a}return -h/2*((f-=2)*f*f*f-2)+a},easeInQuint:function(e,f,a,h,g){return h*(f/=g)*f*f*f*f+a},easeOutQuint:function(e,f,a,h,g){return h*((f=f/g-1)*f*f*f*f+1)+a},easeInOutQuint:function(e,f,a,h,g){if((f/=g/2)<1){return h/2*f*f*f*f*f+a}return h/2*((f-=2)*f*f*f*f+2)+a},easeInSine:function(e,f,a,h,g){return -h*Math.cos(f/g*(Math.PI/2))+h+a},easeOutSine:function(e,f,a,h,g){return h*Math.sin(f/g*(Math.PI/2))+a},easeInOutSine:function(e,f,a,h,g){return -h/2*(Math.cos(Math.PI*f/g)-1)+a},easeInExpo:function(e,f,a,h,g){return(f==0)?a:h*Math.pow(2,10*(f/g-1))+a},easeOutExpo:function(e,f,a,h,g){return(f==g)?a+h:h*(-Math.pow(2,-10*f/g)+1)+a},easeInOutExpo:function(e,f,a,h,g){if(f==0){return a}if(f==g){return a+h}if((f/=g/2)<1){return h/2*Math.pow(2,10*(f-1))+a}return h/2*(-Math.pow(2,-10*--f)+2)+a},easeInCirc:function(e,f,a,h,g){return -h*(Math.sqrt(1-(f/=g)*f)-1)+a},easeOutCirc:function(e,f,a,h,g){return h*Math.sqrt(1-(f=f/g-1)*f)+a},easeInOutCirc:function(e,f,a,h,g){if((f/=g/2)<1){return -h/2*(Math.sqrt(1-f*f)-1)+a}return h/2*(Math.sqrt(1-(f-=2)*f)+1)+a},easeInElastic:function(f,h,e,l,k){var i=1.70158;var j=0;var g=l;if(h==0){return e}if((h/=k)==1){return e+l}if(!j){j=k*0.3}if(g
i.push(t)||(n.code.length=0;r--)e.insertBefore(n[r],i),i=n[r]}function i(e,t,n){for(var i=[],r=0;r0&&(e.splice(l-1,2),l-=2)}e=e.join("/")}if((n||s)&&i){o=e.split("/");for(l=o.length;l>0;l-=1){u=o.slice(0,l).join("/");if(n)for(c=n.length;c>0;c-=1){a=i[n.slice(0,c).join("/")];if(a){a=a[u];if(a){f=a;break}}}f=f||s[u];if(f){o.splice(0,l,f),e=o.join("/");break}}}return e}function f(t,n){return function(){return u.apply(e,s.call(arguments,0).concat([t,n]))}}function l(e){return function(t){return a(t,e)}}function c(e){return function(n){t[e]=n}}function h(r){if(n.hasOwnProperty(r)){var s=n[r];delete n[r],i[r]=!0,o.apply(e,s)}if(!t.hasOwnProperty(r))throw new Error("No "+r);return t[r]}function p(e,t){var n,r,i=e.indexOf("!");return i!==-1?(n=a(e.slice(0,i),t),e=e.slice(i+1),r=h(n),r&&r.normalize?e=r.normalize(e,l(t)):e=a(e,t)):e=a(e,t),{f:n?n+"!"+e:e,n:e,p:r}}function d(e){return function(){return r&&r.config&&r.config[e]||{}}}var t={},n={},r={},i={},s=[].slice,o,u;o=function(r,s,o,u){var a=[],l,v,m,g,y,b;u=u||r,typeof o=="string"&&(o=__inflate(r,o));if(typeof o=="function"){s=!s.length&&o.length?["require","exports","module"]:s;for(b=0;b-1,s=new p(e),f.push(new d(s,e,i)),s)},v.Events=o,window.SC=window.SC||{},window.SC.Widget=v,d=function(e,t,n){this.instance=e,this.element=t,this.domain=E(t.getAttribute("src")),this.isReady=!!n,this.callbacks={}},p=function(){},p.prototype={constructor:p,load:function(e,t){if(!e)return;t=t||{};var n=this,r=k(this),i=r.element,s=i.src,a=s.substr(0,s.indexOf("?"));r.isReady=!1,r.playEventFired=!1,i.onload=function(){n.bind(o.READY,function(){var e,n=r.callbacks;for(e in n)n.hasOwnProperty(e)&&e!==o.READY&&C(u.ADD_LISTENER,e,r.element);t.callback&&t.callback()})},i.src=M(a,e,t)},bind:function(e,t){var n=this,r=k(this);return r&&r.element&&(e===o.READY&&r.isReady?setTimeout(t,1):r.isReady?(T(e,t,r),C(u.ADD_LISTENER,e,r.element)):T(l,function(){n.bind(e,t)},r)),this},unbind:function(e){var t=k(this),n;t&&t.element&&(n=N(e,t),e!==o.READY&&n&&C(u.REMOVE_LISTENER,e,t.element))}},O(p.prototype,x(i)),O(p.prototype,x(s),!0)}),window.SC=window.SC||{},window.SC.Widget=require("lib/api/api")})()
--------------------------------------------------------------------------------
/client/errors.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
Sorry, we can't find the story you're looking for!
5 | {{> random_story}}
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
Sorry, we can't find the user you're looking for!
15 | {{> random_story}}
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
Oops, we can't find the page you're looking for!
24 | {{> random_story}}
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/client/fun.html:
--------------------------------------------------------------------------------
1 |
2 | {{#if rolling}}{{> dice_icon}}{{else}}Take me to a random story{{/if}}
3 |
4 |
5 |
6 | {{#if rolling}}{{> dice_icon}}{{else}}Show me someone new{{/if}}
7 |
8 |
--------------------------------------------------------------------------------
/client/fun.js:
--------------------------------------------------------------------------------
1 | var handpickedStories = [
2 | "/read/riascience/fifty-years-of-walking-in-space-and-what-we-found-there-uRTtQWQo",
3 | "/read/FOLD/how-close-are-we-to-the-martian-Bret9g44",
4 | "/read/BDatta/this-is-not-a-hologram-w5hosSJa",
5 | "/read/twelvefifths/reaction-diffusion-systems-rvJzfQ6k",
6 | "/read/kimsmith/automating-creativity-n5E8qJeF",
7 | "/read/CorySchmitz/how-i-make-textures-kLiQK8se",
8 | "/read/trainbabie/what-is-hipsterdom-nqeiz7XP",
9 | "/read/HannahRajnicek/friday-the-13th-in-chicago-superstitions-tattoo-culture-MoEmXgMM",
10 | "/read/timdunlop/the-first-time-ever-i-saw-your-face-apFj8gt9",
11 | "/read/smwat/dada-data-and-the-internet-of-paternalistic-things-TsZQXLjK",
12 | "/read/SproutsIO/finding-flavor-2Pf475CJ",
13 | "/read/aminobiotech/why-you-should-grow-your-own-bacteria-at-home-JodcMMXB",
14 | "/read/APCollector/on-a-mans-modular-synth-iRaJbfjY",
15 | "/read/EthanZ/choosing-the-appropriate-extreme-metal-music-to-listen-to-while-grading-masters-theses-SESbL2qK",
16 | "/read/CorySchmitz/how-i-make-halftones-nmQRSPe5",
17 | "/read/manuelaristaran/digital-public-services-user-experience-matters-WwpPfdJq",
18 | "/read/sammireinstein/it-is-what-it-is-conversations-about-iraq-WotpdjNa",
19 | "/read/AnnieHuang/shepard-faireys-obey-NnmADS2Z",
20 | "/read/JanineKwoh/why-diversity-matters-in-the-card-aisle-cR9WaHQe",
21 | "/read/cesifoti/three-women-scholars-you-should-know-but-you-probably-dont-evYYD35C",
22 | "/read/Jeremy/proof-of-work-20-He8cm2WC",
23 | "/read/sgenner/why-screens-can-ruin-your-sleep-XuiGfrJi",
24 | "/read/sultanalqassemi/sultan-al-qassemi-on-mit-media-lab-imagination-realized-i8ZS3Dtg",
25 | "/read/MattCarroll/mr-spock-to-the-rescue-how-a-star-trek-star-earned-the-admiration-of-a-young-fan-v5Rr3gGf"
26 | ];
27 |
28 | var handpickedPeople = [
29 | "/profile/twelvefifths",
30 | "/profile/FOLD",
31 | "/profile/alexishope",
32 | "/profile/EthanZ",
33 | "/profile/Rochelle",
34 | "/profile/jbobrow",
35 | "/profile/DestinyInFocus",
36 | "/profile/SproutsIO",
37 | "/profile/Cristian_jf",
38 | "/profile/cjaffe",
39 | "/profile/mpetitchou",
40 | "/profile/tor",
41 | "/profile/JanineKwoh",
42 | "/profile/trainbabie",
43 | "/profile/CorySchmitz",
44 | "/profile/MattCarroll",
45 | "/profile/HannahRajnicek",
46 | "/profile/delong",
47 | "/profile/aminobiotech",
48 | "/profile/cesifoti",
49 | "/profile/jovialjoy",
50 | "/profile/shailin",
51 | "/profile/sannabh",
52 | "/profile/sgenner",
53 | "/profile/BDatta",
54 | "/profile/smwat",
55 | "/profile/APCollector",
56 | "/profile/MikeMoschella"
57 | ];
58 |
59 |
60 | Template.random_story.onCreated(function(){
61 | this.options = handpickedStories;
62 | });
63 |
64 | Template.random_person.onCreated(function(){
65 | this.options = handpickedPeople;
66 | });
67 |
68 |
69 | _.each(['random_story', 'random_person'], function(templateName){
70 | Template[templateName].onCreated(function() {
71 | this.randomizedLink = new ReactiveVar();
72 | this.rolling = new ReactiveVar();
73 | });
74 |
75 | Template[templateName].onRendered(function(){
76 | this.autorun(() => {
77 | var currentUrl = Router.current().url;
78 | this.links = _.reject(this.options, function(url){
79 | return _s.include(url, idFromPathSegment(currentUrl));
80 | });
81 |
82 | this.randomizedLink.set(_.sample(this.links));
83 | });
84 |
85 | this.rollTheDice = (cb) => {
86 | this.rolling.set(true);
87 | //var keepRolling = Meteor.setInterval(function(){
88 | // this.randomizedLink.set(_.sample(this.links));
89 | //}, 50);
90 | Meteor.setTimeout(() => {
91 | //clearInterval(keepRolling);
92 | this.rolling.set(false);
93 | if(cb){
94 | cb();
95 | }
96 | }, 1100);
97 | }
98 | });
99 |
100 | Template[templateName].helpers({
101 | rolling (){
102 | return Template.instance().rolling.get();
103 | },
104 | randomizedLink (){
105 | return Template.instance().randomizedLink.get();
106 | }
107 | });
108 |
109 | Template[templateName].events({
110 | 'click' (e, t){
111 | e.preventDefault();
112 | t.rollTheDice(function(){
113 | Router.go(t.randomizedLink.get());
114 | })
115 | trackEvent('Click random story button');
116 | }
117 | });
118 | })
119 |
120 |
121 |
--------------------------------------------------------------------------------
/client/helpers.js:
--------------------------------------------------------------------------------
1 | Handlebars.registerHelper("debugContext", function() {
2 | return console.log(this);
3 | });
4 |
5 | Handlebars.registerHelper("log", function(v) {
6 | return console.log(v);
7 | });
8 |
9 | Handlebars.registerHelper("hasContext", function(v) {
10 | return !_.isEmpty(this);
11 | });
12 |
13 | Handlebars.registerHelper("pastHeader", function() {
14 | return Session.get("pastHeader");
15 | });
16 |
17 | Handlebars.registerHelper("read", function() {
18 | return Session.get("read");
19 | });
20 |
21 | Handlebars.registerHelper("notRead", function() {
22 | return !Session.get("read");
23 | });
24 |
25 | Handlebars.registerHelper("showPublished", function() {
26 | return !Session.get("showDraft");
27 | });
28 |
29 | Handlebars.registerHelper("showDraft", function() {
30 | return Session.get("showDraft");
31 | });
32 |
33 | Handlebars.registerHelper("saving", function() {
34 | return Session.get("saving");
35 | });
36 |
37 | Handlebars.registerHelper("signingIn", function() {
38 | return window.signingIn();
39 | });
40 |
41 | Handlebars.registerHelper("currentXReadableIndex", function() {
42 | return Session.get("currentX") + 1;
43 | });
44 |
45 | Handlebars.registerHelper("currentYId", function() {
46 | return Session.get("currentYId");
47 | });
48 |
49 | Handlebars.registerHelper("addingContext", function() {
50 | return Session.get("addingContext");
51 | });
52 |
53 | Handlebars.registerHelper("editingThisContext", function() {
54 | var editingContext = Session.get("editingContext");
55 | if (editingContext){
56 | return editingContext === this._id;
57 | }
58 | });
59 |
60 | Handlebars.registerHelper("UsersCollection", Meteor.users);
61 |
62 | Handlebars.registerHelper("isAuthor", function() {
63 | var userId = Meteor.userId();
64 | return userId && userId === this.authorId;
65 | });
66 |
67 | Handlebars.registerHelper("isAuthorOrAdmin", function() {
68 | var userId = Meteor.userId();
69 | if (userId && userId === this.authorId){
70 | return true
71 | } else {
72 | var user = Meteor.user();
73 | return user && user.admin;
74 | }
75 | });
76 |
77 | Handlebars.registerHelper("cardWidth", function() {
78 | return Session.get("cardWidth");
79 | });
80 |
81 | Handlebars.registerHelper("cardHeight", function() { // for context cards, particularly in mobile
82 | return Session.get("cardHeight");
83 | });
84 |
85 | Handlebars.registerHelper("windowWidth", function() {
86 | return Session.get("windowWidth");
87 | });
88 |
89 | Handlebars.registerHelper("windowHeight", function() {
90 | return Session.get("windowHeight");
91 | });
92 |
93 | Handlebars.registerHelper("verticalLeft", function() {
94 | return getVerticalLeft();
95 | });
96 |
97 | Handlebars.registerHelper("adminMode", function() {
98 | return adminMode();
99 | });
100 |
101 | Handlebars.registerHelper("audioPopoutExists", function() {
102 | return Session.equals('poppedOutContextType', 'audio');
103 | });
104 |
105 | Handlebars.registerHelper("videoPopoutExists", function() {
106 | return Session.equals('poppedOutContextType', 'video');
107 | });
108 |
109 | Handlebars.registerHelper("reactiveStory", function(){
110 | return Stories.findOne(Session.get('storyId'));
111 | });
112 |
113 | Handlebars.registerHelper("twitterUser", function() {
114 | var user = Meteor.user();
115 | return user && user.services && user.services.twitter && user.services.twitter.id;
116 | });
117 |
118 | Handlebars.registerHelper("firstName", function(user) {
119 | if (user && user.profile) {
120 | return user.profile.name.split(' ')[0];
121 | }
122 | });
123 |
124 | Handlebars.registerHelper("userFavorited", function() {
125 | return Meteor.user() && _.contains(Meteor.user().profile.favorites, this._id);
126 | });
127 |
128 | Handlebars.registerHelper("userFollowing", function(id) {
129 | return id === Meteor.userId() || Meteor.user() && _.contains(Meteor.user().profile.following, id);
130 | });
131 |
132 | Handlebars.registerHelper("showStorySandwichFooter", function () {
133 | return !Meteor.Device.isPhone() && (embedMode() || hiddenContextMode());
134 | });
135 |
136 |
137 | Handlebars.registerHelper("profileImage", function(user, size) {
138 | var profilePicture = (user && user.profile) ? user.profile.profilePicture : null;
139 | var twitterId = (user && user.services && user.services.twitter) ? user.services.twitter.id : null;
140 | return getProfileImage(profilePicture, twitterId, size);
141 | });
142 |
143 |
144 | Handlebars.registerHelper("formatNumber", function(num){
145 | if(!num){
146 | return 0;
147 | }
148 | return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
149 | });
150 |
151 | Handlebars.registerHelper("formatDate", window.formatDate);
152 | Handlebars.registerHelper("formatDateNice", window.formatDateNice);
153 | Handlebars.registerHelper("formatDateCompact", window.formatDateCompact);
154 |
155 | Handlebars.registerHelper("prettyDateInPast", window.prettyDateInPast)
156 |
157 | Handlebars.registerHelper('$eq',
158 | function(v1, v2) {
159 | return (v1 === v2);
160 | }
161 | );
162 |
163 | Handlebars.registerHelper('capitalize',
164 | function(s) {
165 | return _s.capitalize(s);
166 | }
167 | );
168 |
169 | Handlebars.registerHelper("hiddenContextMode", function () {
170 | return window.hiddenContextMode();
171 | });
172 |
173 | Handlebars.registerHelper("hiddenContextShown", function () {
174 | return window.hiddenContextShown();
175 | });
176 |
177 | Handlebars.registerHelper("sandwichMode", function () {
178 | return window.sandwichMode();
179 | });
180 |
181 | Handlebars.registerHelper("embedMode", function () {
182 | return window.embedMode();
183 | });
184 |
185 | Handlebars.registerHelper("mobileOrTablet", function () {
186 | return window.mobileOrTablet();
187 | });
188 |
189 | Handlebars.registerHelper("searchOverlayShown", function () {
190 | return Session.get('searchOverlayShown');
191 | });
192 |
193 |
194 | Handlebars.registerHelper("menuOverlayShown", function () {
195 | return Session.get('menuOverlayShown');
196 | });
197 |
198 | Handlebars.registerHelper("embedOverlayShown", function () {
199 | return Session.get('embedOverlayShown');
200 | });
201 |
202 | Handlebars.registerHelper("howToOverlayShown", function () {
203 | return Session.get('howToOverlayShown');
204 | });
205 |
206 | Handlebars.registerHelper("analyticsMode", function () {
207 | return window.analyticsMode();
208 | });
209 |
210 | Handlebars.registerHelper("linkActivityShown", function () {
211 | return window.linkActivityShown();
212 | });
213 |
214 | Handlebars.registerHelper("cardDataShown", function () {
215 | return window.cardDataShown();
216 | });
217 |
--------------------------------------------------------------------------------
/client/lib/constants.js:
--------------------------------------------------------------------------------
1 | window.GOOGLE_API_CLIENT_KEY = Meteor.settings["public"].GOOGLE_API_CLIENT_KEY;
2 |
3 | if (!GOOGLE_API_CLIENT_KEY) {
4 | console.error('Settings must be loaded for apis to work');
5 | throw new Meteor.Error('Settings must be loaded for apis to work');
6 | }
7 |
8 | window.panelColor = "#815ed9";
9 | window.remixColor = panelColor;
10 | window.orangeColor = "#fc521f";
11 | window.actionColor = '#00c976';
12 | window.dangerColor = '#fc521f';
13 | window.whiteColor = "white";
14 |
--------------------------------------------------------------------------------
/client/lib/devices.js:
--------------------------------------------------------------------------------
1 | Meteor.Device.emptyUserAgentDeviceName = 'bot';
2 | Meteor.Device.botUserAgentDeviceName = 'bot';
3 | Meteor.Device.unknownUserAgentDeviceType = 'bot';
4 |
5 | // Don't forget to re-detect the device!
6 | Meteor.Device.detectDevice();
7 |
--------------------------------------------------------------------------------
/client/lib/notifications.js:
--------------------------------------------------------------------------------
1 | window.notifyRemix = function(message){
2 | $.amaran({
3 | content: {
4 | message: message,
5 | color: 'white',
6 | bgcolor: '#EA1D75' // social-color
7 | },
8 | 'position' :'top right',
9 | theme:'colorful'
10 | }
11 | );
12 | };
13 |
14 | window.notifyFeature = window.notifyRemix;
15 |
16 | window.notifySuccess = function(message){
17 | $.amaran({
18 | content: {
19 | message: message,
20 | color: 'white',
21 | bgcolor: '#1DB259' // action-color
22 | },
23 | 'position' :'top right',
24 | theme:'colorful'
25 | }
26 | );
27 | };
28 |
29 | window.notifyLogin = function(){
30 | var user = Meteor.user();
31 | var name = user.profile.name ? user.profile.name.split(' ')[0] : user.profile.displayUsername;
32 | notifySuccess('Welcome ' + name + '!');
33 | };
34 |
35 |
36 | window.notifyError = function(message){
37 | $.amaran({
38 | content: {
39 | message: message,
40 | color: 'white',
41 | bgcolor: '#ff1b0c' // danger-color
42 | },
43 | 'position' :'top right',
44 | theme:'colorful',
45 | delay: 8000
46 | }
47 | );
48 | };
49 |
50 | window.notifyInfo = function(message){
51 | $.amaran({
52 | content: {
53 | message: message,
54 | color: 'white',
55 | bgcolor: '#585094' // social-color
56 | },
57 | 'position' :'top right',
58 | theme:'colorful'
59 | }
60 | );
61 | };
62 |
63 | window.notifyBrowser = function(){
64 | $.amaran({
65 | content: {
66 | message: "Hi! We're so glad you're writing a story on FOLD. Feel free to try out our editor in any browser and give us feedback, but for the best experience right now, we recommend using Chrome!",
67 | color: 'white',
68 | bgcolor: '#585094' // social-color
69 | },
70 | sticky: true,
71 | 'position' :'top right',
72 | theme:'colorful'
73 | }
74 | );
75 | };
76 |
77 | window.notifyDeploy = function(message, sticky){
78 | $.amaran({
79 | content: {
80 | message: message,
81 | color: 'white',
82 | bgcolor: '#585094' // social-color
83 | },
84 | 'position' :'top right',
85 | theme:'colorful',
86 | sticky: sticky,
87 | clearAll: true
88 | }
89 | );
90 | $('.amaran').addClass('migration-notification');
91 | };
92 |
93 | window.notifyImageSizeError = function(){
94 | notifyError("Wow, that's a really big file! Can you make it any smaller? We support files up to " + CLOUDINARY_FILE_SIZE/1000000 + ' MB');
95 | };
96 |
--------------------------------------------------------------------------------
/client/lib/reload.js:
--------------------------------------------------------------------------------
1 | window.readyToMigrate = new ReactiveVar(false);
2 |
3 | var reloadDelay = Meteor.settings['public'].NODE_ENV === 'development' ? 0 : 2000;
4 |
5 | Reload._onMigrate('fold', function (retry) {
6 | if (readyToMigrate.get()) {
7 | return [true, {codeReloaded: true}];
8 | } else {
9 | //if (Router.current().route.getName() === 'edit') {
10 | if (Meteor.settings['public'].NODE_ENV !== 'development') {
11 | notifyDeploy("We've just made an improvement! Click here to sync up the latest code.", true);
12 | trackEvent('Reload notification happened', {label: 'Reload on click'});
13 | $('.migration-notification').click(function () {
14 | saveCallback(null, true);
15 | setTimeout(function () {
16 | readyToMigrate.set(true);
17 | retry();
18 | }, 300);
19 | });
20 | Router.onRun(function () {
21 | readyToMigrate.set(true);
22 | retry();
23 | });
24 | return [false];
25 | } else {
26 | notifyDeploy("We've made an improvement! Wait just a moment while we sync up the latest code.", false);
27 | trackEvent('Reload notification happened', {label: 'Immediate reload', nonInteraction: 1});
28 | setTimeout(function () {
29 | readyToMigrate.set(true);
30 | retry();
31 | }, reloadDelay);
32 | return [false]
33 | }
34 | }
35 | });
36 |
37 | var migrationData = Reload._migrationData('fold');
38 |
39 | if (migrationData){
40 | window.codeReloaded = migrationData.codeReloaded;
41 | }
42 |
--------------------------------------------------------------------------------
/client/login.html:
--------------------------------------------------------------------------------
1 |
2 | {{#if signingIn}}
3 |
4 |
5 |
6 |
✕
7 | {{#if onSignupStage}}
8 |
{{> fold_title}}
9 | {{#if explanation}}
10 |
{{explanation}}
11 | {{else}}
12 |
Create, Remix, Discover.
13 | {{/if}}
14 |
18 |
Already have an account? Sign in.
19 | {{/if}}
20 |
21 | {{#if onLoginStage}}
22 | {{> login_form}}
23 | {{/if}}
24 |
25 | {{#if onInfoStage}}
26 | {{> info_form}}
27 | {{/if}}
28 |
29 | {{#if onPasswordStage}}
30 | {{> password_form}}
31 | {{/if}}
32 |
33 | {{#if onOnboardingStage}}
34 | {{> onboarding_screen}}
35 | {{/if}}
36 |
37 | {{#if onForgotStage}}
38 | {{> recover_password_form}}
39 | {{/if}}
40 |
41 |
42 |
43 |
44 | {{/if}}
45 |
46 |
47 |
48 | {{#unless twitterUser}}
49 | Go back
50 | {{/unless}}
51 | {{#if twitterUser}}
52 | {{> fold_title}}
53 | {{/if}}
54 |
117 |
118 |
119 |
120 |
121 | Go back
122 |
157 |
158 |
159 |
160 |
161 | {{#with currentUser}}
162 |
163 |
164 |
165 |
166 |
167 | To edit your photo, display name,
or bio, visit
your profile !
168 |
169 | Thanks!
170 | {{/with}}
171 |
172 |
173 |
174 |
175 |
176 | Go back
177 |
197 |
198 |
199 |
200 |
--------------------------------------------------------------------------------
/client/privacy.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | {{> top_banner}}
4 | {{> privacy_content}}
5 | {{> contact_footer}}
6 |
7 |
8 |
9 |
10 |
11 |
12 |
Privacy Policy
13 |
The FOLD Team is part of the Civic Media Group in the Media Laboratory at the Massachusetts Institute of Technology ("MIT"). FOLD is a reading, authoring, and publishing platform for creating branching, multimedia stories. FOLD is committed to respecting the privacy of users who access the FOLD website (the “Site,” which includes all pages within readfold.com).
14 |
15 |
16 |
Web Server Logs
17 |
When you visit the Site, our web server may record the following information in its server log:
18 |
• your IP Address,
19 |
• the URLs you have requested to access,
20 |
• the dates and methods of requests,
21 |
• the status code of your requests,
22 |
• URLs of pages that referred you to the Site,
23 |
• number of bytes transferred, and
24 |
• your web browser and operating system platform.
25 |
We use server log information to help diagnose problems with our server and to administer our website by identifying which parts of our site are most heavily used. We also use this information to tailor site content to user needs and to generate aggregate statistical reports. Web server logs are retained on a temporary basis, during which time their contents are accessible to Site administrators, and then deleted completely from our systems. Unless required by legal process, we do not link IP addresses to any personally identifiable information. This means that user sessions will be tracked by IP address, but a user’s identity will remain anonymous.
26 |
In addition, we ordinarily do not disclose to third parties site usage by individual IP addresses, but we may do so in very limited circumstances when complying with law or legal process, working with consultants assisting us with fixing or improving the Site, or monitoring and improving the security of our network.
27 |
28 |
29 |
User Registration and User Generated Content
30 |
To obtain user registration, you must submit your email address to the Site, along with a user name and password that you create for user authentication purposes. You may also sign in with an existing Twitter account, in which we will gain access to the list of people you follow to improve your discovery of stories on FOLD.
31 |
The Site allows you to write stories and other user-generated content if you are a registered user. Stories posted on the site will be viewable by anyone who accesses the site, and will identify you as the author by the user name you have selected. If you do not wish to be identified as the source of content you post to the Site, you should select a pseudonymous user name.
32 |
We will not disclose your email address anywhere on the Site (see below, “Email”).
33 |
From time to time we may solicit feedback from you about your use of the Site and its features (your “Feedback”). You are not required to provide Feedback. We solicit Feedback for internal purposes only, so that we can evaluation the Site and its features, and we will not publish or otherwise disclose your Feedback without first obtaining your consent to do so.
34 |
35 |
36 |
Programming Analytics
37 |
In order to refine the resources on offer through the Site and to optimize FOLD’s programming, FOLD may elect to conduct internal analytics of content submitted by users. Any such internal analytics will be conducted on an anonymized set of user-generated content.
38 |
39 |
40 |
Analytics
41 |
We use Segment.io software to perform Site usage analytics. Segment.io collects anonymous information from users to help us track Site usage and referrals from other websites. These data are used primarily to optimize the website experience for our visitors, but we may use the data as well to assist us in our marketing of the Site.
42 |
Information collected and processed by Segment.io includes the user’s IP address, network location, and geographic location. Segment.io acquires all its information directly from the user, by installing a cookie (see below) on JavaScript-enabled computers. The Site does not share any information it collects with Segment.io, and Segment.io does not collect any personal identifying information such as names, contact information, social security numbers or financial information.
43 |
44 |
45 |
Cookies
46 |
Cookies are unique bits of computer data that many major websites will transfer to your computer the first time that you visit. Cookies are stored on your hard drive and may be later accessed by the website to track prior usage. As noted above, Segment.io will install a cookie on the hard drives of Site visitors.
47 |
48 |
49 |
E-mail
50 |
We will only use your email address for the purpose for which you have provided it — i.e., to respond to a message from you or to communicate with you regarding your user account.
51 |
In the event we contract with a third-party service to assist with email delivery of newsletters and other mailings containing information about the Site, that service will be prohibited from using or sharing Site user information for any purpose other than facilitating communications on behalf of the Site.
52 |
53 |
54 |
Disclosure to Third Parties
55 |
We will not sell, lend, or disclose to third parties any personally identifiable information collected from visitors, except as disclosed in this Policy or in the event we are required by law to do so. We may disclose information to employees, fellows, students, consultants and agents who have a legitimate need to know the information for the purpose of fixing or improving the Site and monitoring and improving the security of our network. We may also disclose this information when special circumstances call for it, such as when disclosure is required by law or court order or when disclosure is, in our sole discretion, necessary to protect our legal rights, including intellectual property rights.
56 |
57 |
58 |
Other Websites
59 |
This Site may contain links to other web resources, including websites of organizations other than the Massachusetts Institute of Technology. The websites to which the Site links may also install cookies on your computer, log your access to their web pages, or collect user-identifying information directly from you, once you proceed to browse those sites. We are not responsible for the privacy policies of other sites or businesses to which the Site provides links. Please visit the relevant sites to review their privacy policies.
60 |
61 |
62 |
Data Security
63 |
We have in place physical, electronic and managerial procedures to protect the information we collect online. However, as effective as these measures are, no security system is impenetrable. We cannot completely guarantee the security of our database, nor can we guarantee that the information you supply will not be intercepted while being transmitted to us over the Internet.
64 |
65 |
66 |
Notification of Changes to the Privacy Policy
67 |
We will review our security measures and Privacy Policy on a periodic basis, and we may modify our policies as appropriate. We may also change or update our Privacy Policy if we add new services or features. If any changes are made, we will make appropriate amendments to this policy and post them at the Site. We encourage you to review our Privacy Policy on a regular basis.
68 |
If you have any questions about this Privacy Policy, the practices of this Site, or your dealings with this Site, you can contact the FOLD team at fold@media.mit.edu.
69 |
70 |
71 |
Effective Date
72 |
This Privacy Policy is in effect as of April 21, 2015.
73 |
74 |
75 |
Thanks for reading, and let us know if you have any feedback or concerns!
76 |
77 |
78 |
Love,
79 | The FOLD Team
80 |
81 |
82 |
83 |
--------------------------------------------------------------------------------
/client/profile.html:
--------------------------------------------------------------------------------
1 |
2 | {{> top_banner}}
3 |
4 |
5 | {{> user_profile}}
6 |
7 |
8 |
9 |
10 | {{firstName user}}'s latest
11 | Favorites
12 | Following
13 | Followers
14 |
15 |
16 | {{#if showLatest}}
17 | {{> user_stories}}
18 | {{else}}
19 | {{#if showFavorites}}
20 | {{> user_favorite_stories}}
21 | {{else}}
22 | {{#if showFollowing}}
23 | {{> user_following}}
24 | {{else}}
25 | {{#if showFollowers}}
26 | {{> user_followers}}
27 | {{/if}}
28 | {{/if}}
29 | {{/if}}
30 | {{/if}}
31 |
32 |
33 |
34 |
35 |
36 |
37 | {{#if editing}}
38 |
56 | {{else}}
57 | {{#if ownProfile}}
58 | Edit Profile
59 | {{/if}}
60 |
61 |
62 |
63 | {{name}}
64 | {{{bioHtml}}}
65 |
66 |
Following {{#if user.followingTotal}}{{user.followingTotal}}{{else}}0{{/if}}
67 |
Followers {{#if user.followersTotal}}{{user.followersTotal}}{{else}}0{{/if}}
68 |
69 |
70 | {{> follow_button userId=user._id}}
71 |
72 | {{#if adminMode}}
73 |
74 | {{email}}
75 | {{user.services.twitter.screenName}}
76 |
77 | {{/if}}
78 | {{/if}}
79 |
80 |
81 |
82 | {{#if hasPublished}}
83 |
84 | {{#each publishedStories}}
85 |
86 | {{> _story_preview_content}}
87 |
88 | {{/each}}
89 |
90 |
91 |
92 |
93 |
94 | {{else}}
95 |
96 | {{#if ownProfile}}
97 | You haven't published anything yet, but getting started is easy!
98 | Here are some examples to inspire you:
99 |
104 | {{#if hasDrafts}}
105 |
View your drafts
106 | {{else}}
107 | {{#unless mobileOrTablet}}
108 | {{> create_story}}
109 | {{/unless}}
110 | {{/if}}
111 | {{else}}
112 | {{user.profile.name}} hasn't published any stories yet. Any minute now!
113 | {{/if}}
114 |
115 | {{/if}}
116 |
117 |
118 |
119 | {{#if hasFavorites}}
120 |
121 | {{#each favoriteStories}}
122 |
123 | {{> _story_preview_content}}
124 |
125 | {{/each}}
126 |
127 |
128 |
129 |
130 |
131 | {{else}}
132 |
133 | {{#if ownProfile}}
134 | You haven't favorited any stories yet. Try your luck? {{> random_story}}
135 | {{else}}
136 | {{user.profile.name}} hasn't favorited any stories yet, but they just got here. Give 'em some time!
137 | {{/if}}
138 |
139 | {{/if}}
140 |
141 |
142 |
143 | {{#each usersFollowing}}
144 | {{> person_card person=this onProfilePage=true}}
145 | {{else}}
146 |
147 | {{#if ownProfile}}
148 | You don't follow anyone yet... {{> random_person}}
149 | {{else}}
150 | {{user.profile.name}} hasn't followed anyone yet... ¯\_(ツ)_/¯
151 | {{/if}}
152 |
153 | {{/each}}
154 |
155 |
156 |
157 |
158 | {{#each followers}}
159 | {{> person_card person=this onProfilePage=true}}
160 | {{else}}
161 |
162 | {{#if ownProfile}}
163 | You don't have any followers yet. Try writing a story! {{> create_story}}
164 | {{else}}
165 | Nobody follows {{user.profile.name}} yet... ಠ╭╮ಠ
166 | {{/if}}
167 |
168 | {{/each}}
169 |
170 |
171 |
172 |
173 | {{> top_banner}}
174 |
175 |
176 |
177 | {{> my_stories}}
178 |
179 |
180 |
181 |
182 |
183 |
184 |
185 |
Published Stories
186 | {{#each publishedStories}}
187 |
188 | {{> _story_preview_content}}
189 |
195 |
196 | {{/each}}
197 |
198 |
199 | {{#if unpublishedStories}}
200 |
Not Published Yet
201 | {{#each unpublishedStories}}
202 |
203 | {{> _story_preview_content useDraftStory=true draftStory=draftStory}}
204 |
209 |
210 | {{/each}}
211 | {{/if}}
212 |
213 |
214 |
215 |
216 |
217 |
218 |
219 | {{person.profile.name}}
220 | ({{person.displayUsername}})
221 |
222 |
{{>follow_button userId=person._id}}
223 | {{> person_icon}}
224 |
225 |
226 |
--------------------------------------------------------------------------------
/client/profile.js:
--------------------------------------------------------------------------------
1 | var formatDate, weekDays;
2 |
3 | var numStoriesToDisplay = 12;
4 |
5 | weekDays = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
6 |
7 | formatDate = function(date) {
8 | var hms;
9 | hms = date.toTimeString().replace(/.*(\d{2}:\d{2}:\d{2}).*/, "$1");
10 | return weekDays[date.getDay()] + " " + (date.getMonth() + 1) + "/" + date.getDate() + "/" + date.getFullYear() + " " + hms;
11 | };
12 |
13 | Template.profile.onCreated(function(){
14 | this.sectionToShow = new ReactiveVar('latest');
15 | this.autorun(() => {
16 | if(adminMode()){
17 | this.subscribe('adminOtherUserPub', this.data.user._id);
18 | }
19 | });
20 | });
21 |
22 | Template.profile.events({
23 | "click .show-latest" (e, t) {
24 | t.sectionToShow.set('latest');
25 | },
26 | "click .show-favorites" (e, t) {
27 | t.sectionToShow.set('favorites');
28 | },
29 | "click .show-following" (e, t) {
30 | t.sectionToShow.set('following');
31 | },
32 | "click .show-followers" (e, t) {
33 | t.sectionToShow.set('followers');
34 | },
35 | "click .followers-total" (e, t) {
36 | t.sectionToShow.set('followers');
37 | },
38 | "click .following-total" (e, t) {
39 | t.sectionToShow.set('following');
40 | }
41 | });
42 |
43 | Template.profile.helpers({
44 | "showLatest" (){
45 | return Template.instance().sectionToShow.get() === 'latest';
46 | },
47 | "showFavorites" (){
48 | return Template.instance().sectionToShow.get() === 'favorites';
49 | },
50 | "showFollowing" (){
51 | return Template.instance().sectionToShow.get() === 'following';
52 | },
53 | "showFollowers" (){
54 | return Template.instance().sectionToShow.get() === 'followers';
55 | }
56 | });
57 |
58 |
59 | Template.my_stories.events({
60 | 'click .unpublish' (){
61 | if (confirm('Are you sure you want to unpublish this story?')){
62 | $('.story[data-story-id=' + this._id + ']').fadeOut(500, () => {
63 | Meteor.call('unpublishStory', this._id, (err, result) => {
64 | if(err || !result){
65 | notifyError('Unpublish failed.');
66 | }
67 | });
68 | })
69 |
70 | }
71 | },
72 | 'click .delete' (){
73 | if (confirm('Are you sure you want to delete this story? This cannot be undone.')){
74 | $('.story[data-story-id=' + this._id + ']').fadeOut(500, () => {
75 | Meteor.call('deleteStory', this._id, (err, result) => {
76 | if(err || !result){
77 | notifyError('Delete failed.');
78 | }
79 | });
80 | })
81 |
82 | }
83 | }
84 | });
85 | Template.my_stories.helpers({
86 | publishedStories () {
87 | if (Meteor.user()) {
88 | return Stories.find({
89 | authorId: Meteor.userId(),
90 | published : true
91 | });
92 | }
93 | },
94 | unpublishedStories () {
95 | if (Meteor.user()) {
96 | return Stories.find({
97 | authorId: Meteor.userId(),
98 | published : false
99 | });
100 | }
101 | },
102 | lastEditDate () {
103 | return prettyDateInPast(this.savedAt);
104 | },
105 | lastPublishDate () {
106 | return prettyDateInPast(this.publishedAt);
107 | }
108 | });
109 |
110 | Template.my_stories.events({
111 | "click div#delete" (d) {
112 | var srcE, storyId;
113 | srcE = d.srcElement ? d.srcElement : d.target;
114 | storyId = $(srcE).closest('div.story').data('story-id');
115 | return Stories.remove({
116 | _id: storyId
117 | });
118 | }
119 | });
120 |
121 | Template.user_profile.onCreated(function(){
122 |
123 | this.autorun(() => { // TODO this sometimes runs twice unnecessarily if coming from home (first one does not have full profile user loaded with favorites)
124 | var user = Meteor.users.findOne(this.data.user._id);
125 | var usersFromStories = Stories.find({ published: true, _id: {$in: user.profile.favorites || []}}, {fields: {authorId:1}, reactive: false}).map(function(story){return story.authorId});
126 |
127 | var usersToSubscribeTo = _.compact(_.union(usersFromStories, user.profile.following, user.followers));
128 |
129 | this.subscribe('minimalUsersPub', _.sortBy(usersToSubscribeTo, _.identity));
130 | });
131 |
132 | this.editing = new ReactiveVar(false);
133 | this.uploadPreview = new ReactiveVar();
134 | this.uploadingPicture = new ReactiveVar();
135 | this.pictureId = new ReactiveVar();
136 | });
137 |
138 | Template.user_profile.onRendered(function(){
139 | this.$('.bio').linkify({linkAttributes: {rel : 'nofollow'}});
140 | });
141 |
142 |
143 | var ownProfile = function() {
144 | var user = Meteor.user();
145 | return (user && (user.username == this.user.username)) ? true : false
146 | };
147 |
148 | Template.user_profile.helpers({
149 | editing () {
150 | return Template.instance().editing.get()
151 | },
152 | ownProfile: ownProfile,
153 | name () {
154 | return this.user.profile.name
155 | },
156 | uploadPreview (){
157 | return Template.instance().uploadPreview.get();
158 | },
159 | uploadingPicture (){
160 | return Template.instance().uploadingPicture.get();
161 | },
162 | "email" (){
163 | return this.user.emails ? this.user.emails[0].address : null;
164 | },
165 | bioHtml (){
166 | return _.escape(this.user.profile.bio).replace(/(@\w+)/g, "$1 ");
167 | }
168 | });
169 |
170 | Template.user_profile.events({
171 | "click .edit-profile" (d, template) {
172 | template.editing.set(true);
173 | },
174 | "click .save-profile-button" (d, template) {
175 | template.editing.set(false);
176 | if (template.pictureId.get()) {
177 | Meteor.call('saveProfilePicture', this.user._id, template.pictureId.get());
178 | }
179 | },
180 | "change input[type=file]" (e, template){
181 | var file = _.first(e.target.files);
182 | if (file) {
183 | if(file.size > CLOUDINARY_FILE_SIZE){
184 | return notifyImageSizeError();
185 | }
186 | template.uploadingPicture.set(true);
187 | // actual upload
188 | Cloudinary.upload([file], {}, function(err, doc) {
189 | template.uploadingPicture.set(false);
190 | if(err){
191 | var input = template.$('input[type=file]');
192 | input.val(null);
193 | input.change();
194 | notifyError('Image upload failed');
195 | } else {
196 | template.uploadPreview.set('//res.cloudinary.com/' + Meteor.settings['public'].CLOUDINARY_CLOUD_NAME + '/image/upload/w_150,h_150,c_fill,g_face/' + doc.public_id);
197 | template.pictureId.set(doc.public_id);
198 | }
199 | })
200 | } else {
201 | template.uploadPreview.set(null);
202 | }
203 | }
204 | });
205 |
206 | Template.user_stories.onCreated(function(){
207 | this.seeAllPublished = new ReactiveVar(false);
208 | });
209 |
210 | Template.user_stories.events({
211 | "click .toggle-published" (d, template) {
212 | return template.seeAllPublished.set(!template.seeAllPublished.get())
213 | }
214 | });
215 |
216 | Template.user_stories.helpers({
217 | seeAllPublished () {
218 | return Template.instance().seeAllPublished.get()
219 | },
220 | publishedStories () {
221 | var limit = 0; // = Template.instance().seeAllPublished.get() ? 0 : numStoriesToDisplay; //when limit=0 -> no limit on stories
222 | return Stories.find({authorId : this.user._id, published : true}, {
223 | sort: {
224 | publishedAt: -1
225 | },
226 | limit: limit
227 | })
228 | },
229 | showAllPublishedButton () {
230 | return Stories.find({authorId : this.user._id, published : true}).count() > numStoriesToDisplay
231 | },
232 | hasPublished () {
233 | return Stories.findOne({authorId : this.user._id, published : true})
234 | },
235 | hasDrafts (){
236 | return Stories.findOne({authorId : this.user._id}, {published: false})
237 | },
238 | ownProfile: ownProfile
239 | });
240 |
241 | Template.user_favorite_stories.onCreated(function(){
242 | this.seeAllFavorites = new ReactiveVar(false);
243 | });
244 |
245 | Template.user_favorite_stories.events({
246 | "click .toggle-favorites" (d, template) {
247 | return template.seeAllFavorites.set(!template.seeAllFavorites.get())
248 | }
249 | });
250 |
251 | Template.user_favorite_stories.helpers({
252 | seeAllFavorites () {
253 | return Template.instance().seeAllFavorites.get()
254 | },
255 | favoriteStories () {
256 | var limit = 0; // Template.instance().seeAllFavorites.get() ? 0 : numStoriesToDisplay;
257 | var favorites = this.user.profile.favorites;
258 | if (favorites && favorites.length) {
259 | return Stories.find({
260 | _id: {
261 | $in: this.user.profile.favorites
262 | }}, {
263 | sort: {
264 | publishedAt: -1
265 | },
266 | limit: limit
267 | })
268 | } else {
269 | return [];
270 | }
271 | },
272 | showAllFavoritesButton () {
273 | var favorites = this.user.profile.favorites;
274 | if (favorites && favorites.length) {
275 | return favorites.length > numStoriesToDisplay
276 | }
277 | },
278 | hasFavorites () {
279 | return !_.isEmpty(this.user.profile.favorites);
280 | },
281 | ownProfile: ownProfile
282 | });
283 |
284 | Template.user_following.helpers({
285 | usersFollowing () {
286 | var following = this.user.profile.following;
287 | if (following && following.length) {
288 | return Meteor.users.find({
289 | _id: {
290 | $in: following
291 | }})
292 | } else {
293 | return [];
294 | }
295 | },
296 | ownProfile: ownProfile
297 | });
298 |
299 | Template.user_followers.helpers({
300 | followers () {
301 | var followers = this.user.followers;
302 | if (followers && followers.length) {
303 | return Meteor.users.find({
304 | _id: {
305 | $in: followers
306 | }})
307 | } else {
308 | return [];
309 | }
310 | },
311 | ownProfile: ownProfile
312 | });
313 |
314 | Template.person_card.helpers({
315 | profileUrl (){
316 | return '/profile/' + (Template.instance().data.person.displayUsername);
317 | },
318 | });
319 |
--------------------------------------------------------------------------------
/client/read.html:
--------------------------------------------------------------------------------
1 |
2 | {{!hasContext suppresses a firefox error when story not found because page gets rendered without any context, probably right before story not found gets shown, should probably not be necessary}}
3 | {{#if showEmbedPlaceholder}}
4 | {{> embed_placeholder}}
5 | {{else}}
6 | {{#if hasContext}}
7 | {{> story_header}}
8 | {{> story}}
9 | {{> read_options}}
10 | {{#if showStorySandwichFooter}}
11 | {{> story_sandwich_footer}}
12 | {{/if}}
13 | {{/if}}
14 | {{/if}}
15 |
16 |
17 |
18 |
19 |
27 |
28 |
29 |
30 |
31 |
32 | {{emailAddress}}
33 | {{#if twitterHandle}}
34 | {{twitterHandle}}
35 | {{/if}}
36 | {{#if published}}
37 | {{analytics.views.byIP}} unique views
38 | {{analytics.reads.byIP}} unique reads
39 | {{analytics.shares.total}} shares
40 | {{favoritedTotal}} favorites
41 | {{else}}
42 | Unpublished Draft
43 | Last saved: {{formatDate savedAt}}
44 | {{/if}}
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
Story Stats (beta) ?
54 |
{{analytics.views.byIP}} people viewed this story
55 | {{#if showReadPercentage}}
56 |
{{readPercentage}}% of them stuck around
57 | {{else}}
58 |
--% of them stuck around
59 | {{/if}}
60 |
61 |
Which links are people clicking?
62 | {{#if linkActivityShown}}
Hide link activity {{else}}
Show link activity {{/if}}
63 |
64 |
Which cards are people spending the most time on?
65 | {{#if cardDataShown}}
Hide card data {{else}}
Show card data {{/if}}
66 |
67 |
68 | {{> minimap}}
69 |
70 |
{{> x_icon}}
71 |
72 |
73 |
74 |
75 |
76 | {{#unless isPhone}}
77 | {{#unless showStorySandwichFooter}}
78 | {{#with reactiveStory}}
79 | {{#if isAuthorOrAdmin}}
80 | {{#unless analyticsMode}}
81 | Story Stats
82 | {{/unless}}
83 | {{/if}}
84 | {{#if analyticsMode}}
85 | {{#if isAuthorOrAdmin}}
86 | {{> read_analytics_ui}}
87 | {{else}}
88 | {{/if}}
89 | {{else}}
90 | {{> share_buttons}}
91 | {{> favorite_button}}
92 | {{/if}}
93 | {{#if adminMode}}
94 | {{> editors_pick_button}}
95 | {{/if}}
96 | {{/with}}
97 | {{/unless}}
98 | {{/unless}}
99 |
100 |
101 |
102 |
103 | {{#if isAuthor}}
104 | {{#unless mobileOrTablet}}
105 | {{#linkTo route="edit" data=this}}
106 |
107 | Edit
108 |
109 | {{/linkTo}}
110 | {{/unless}}
111 | {{/if}}
112 |
113 |
114 |
115 |
116 |
117 | {{#with headerImageVideoObject}}
118 | {{> looping_video}}
119 | {{else}}
120 |
121 | {{/with}}
122 |
124 |
125 | {{> favorite_button}}
126 | {{#if embedMode}}
127 |
{{> popout_icon}}
128 | {{/if}}
129 | {{> share_buttons}}
130 |
131 |
132 | {{#if embedMode}}
133 |
134 | {{> fold_title_icon}}
135 |
136 | {{else}}
137 |
138 | {{> fold_title_icon}}
139 |
140 |
141 | {{/if}}
142 |
143 |
144 |
145 |
--------------------------------------------------------------------------------
/client/recover_password.html:
--------------------------------------------------------------------------------
1 |
2 | Go back
3 |
8 | {{#if message}}{{message}}
{{/if}}
9 |
10 |
--------------------------------------------------------------------------------
/client/recover_password.js:
--------------------------------------------------------------------------------
1 | Template.recover_password_form.onCreated(function() {
2 | this.message = new ReactiveVar('');
3 | })
4 |
5 | Template.recover_password_form.helpers({
6 | message () {
7 | return Template.instance().message.get();
8 | }
9 | })
10 |
11 | Template.recover_password_form.events({
12 | 'submit #recover-password-form' (e, t) {
13 | e.preventDefault();
14 |
15 | var forgotPasswordForm = $(e.currentTarget);
16 | var email = t.$('#recover-password-email').val().toLowerCase();
17 |
18 | if(_.isEmpty(email)) {
19 | t.message.set('Please fill in all required fields.');
20 | return;
21 | }
22 |
23 | if(!SimpleSchema.RegEx.Email.test(email)) {
24 | t.message.set('Please enter a valid email address.');
25 | return;
26 | }
27 |
28 | if (t.disableSubmit){
29 | return false
30 | } else {
31 | t.disableSubmit = true;
32 | }
33 |
34 | Accounts.forgotPassword({email: email}, function(err) {
35 | t.disableSubmit = false;
36 | if (err) {
37 | if (err.message === 'User not found [403]') {
38 | t.message.set('This email does not exist.');
39 | } else {
40 | t.message.set('We are sorry but something went wrong.');
41 | }
42 | } else {
43 | t.message.set('Email sent, expect it within a few minutes.');
44 | t.disableSubmit = true; // prevent double submit
45 | }
46 | });
47 | return false
48 | },
49 | });
50 |
--------------------------------------------------------------------------------
/client/reset_password.html:
--------------------------------------------------------------------------------
1 |
2 | {{> top_banner_simple}}
3 |
4 |
5 | {{> reset_password_form}}
6 |
7 |
8 |
9 |
10 |
11 |
24 |
--------------------------------------------------------------------------------
/client/reset_password.js:
--------------------------------------------------------------------------------
1 | Template.reset_password_form.onCreated(function() {
2 | this.message = new ReactiveVar('');
3 | })
4 |
5 | Template.reset_password_form.helpers({
6 | message () {
7 | return Template.instance().message.get();
8 | },
9 | resetPassword (){
10 | return Session.get('resetPasswordToken');
11 | }
12 | })
13 |
14 | Template.reset_password_form.events({
15 | 'submit #reset-password-form' (e, t) {
16 | e.preventDefault();
17 |
18 | var password = t.$('#reset-password-password').val();
19 | var passwordConfirm = t.$('#reset-password-password-confirm').val();
20 |
21 | if (_.isEmpty(password)) {
22 | t.message.set('Please fill in all required fields.');
23 | return;
24 | }
25 |
26 | if (!isValidPassword(password)) {
27 | t.message.set('Please enter a valid password.');
28 | return;
29 | }
30 |
31 | if (password !== passwordConfirm) {
32 | t.message.set('Your two passwords are not equivalent.');
33 | return;
34 | }
35 |
36 | Accounts.resetPassword(Session.get('resetPasswordToken'), password, function(err) {
37 | if (err) {
38 | t.message.set('We are sorry but something went wrong.');
39 | } else {
40 | t.message.set('Your password has been successfully changed. Welcome back!');
41 | Meteor.setTimeout( function(){
42 | Router.go('home')
43 | }, 1500);
44 | }
45 | });
46 | }
47 | });
48 |
--------------------------------------------------------------------------------
/client/search-results.js:
--------------------------------------------------------------------------------
1 |
2 | SearchResults = new Mongo.Collection(null, {
3 | transform (doc) { return window.newTypeSpecificContextBlock(doc) }
4 | });
5 |
6 | var options = {
7 | keepHistory: 1000 * 60 * 5,
8 | localSearch: true
9 | };
10 | var fields = ['title', 'keywords', 'authorName', 'authorDisplayUsername'];
11 |
12 | StorySearch = new SearchSource('stories', fields, options);
13 | PersonSearch = new SearchSource('people', ['profile.name', 'username'], options);
14 |
--------------------------------------------------------------------------------
/client/signup.html:
--------------------------------------------------------------------------------
1 |
2 | {{> top_banner_simple_no_links}}
3 |
4 |
5 | {{> signup_form}}
6 | {{> contact_footer}}
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/client/styles/MyFontsWebfontsKit.css:
--------------------------------------------------------------------------------
1 | /**
2 | * @license
3 | * MyFonts Webfont Build ID 3109905, 2015-10-18T16:18:30-0400
4 | *
5 | * The fonts listed in this notice are subject to the End User License
6 | * Agreement(s) entered into by the website owner. All other parties are
7 | * explicitly restricted from using the Licensed Webfonts(s).
8 | *
9 | * You may obtain a valid license at the URLs below.
10 | *
11 | * Webfont: FF Mark Web Italic by FontFont
12 | * URL: http://www.myfonts.com/fonts/fontfont/mark/ot-italic/
13 | *
14 | * Webfont: FF Mark Web Bold Italic by FontFont
15 | * URL: http://www.myfonts.com/fonts/fontfont/mark/ot-bold-italic/
16 | *
17 | * Webfont: FF Mark Web Bold by FontFont
18 | * URL: http://www.myfonts.com/fonts/fontfont/mark/ot-bold/
19 | *
20 | * Webfont: FF Mark Web Light Italic by FontFont
21 | * URL: http://www.myfonts.com/fonts/fontfont/mark/ot-light-italic/
22 | *
23 | * Webfont: FF Mark Web Light by FontFont
24 | * URL: http://www.myfonts.com/fonts/fontfont/mark/ot-light/
25 | *
26 | * Webfont: FF Mark Web Medium Italic by FontFont
27 | * URL: http://www.myfonts.com/fonts/fontfont/mark/ot-medium-italic/
28 | *
29 | * Webfont: FF Mark Web Medium by FontFont
30 | * URL: http://www.myfonts.com/fonts/fontfont/mark/ot-medium/
31 | *
32 | * Webfont: FF Mark Web by FontFont
33 | * URL: http://www.myfonts.com/fonts/fontfont/mark/ot-regular/
34 | *
35 | *
36 | * License: http://www.myfonts.com/viewlicense?type=web&buildid=3109905
37 | * Licensed pageviews: 50,000
38 | * Webfonts copyright: 2013 published by FontShop International GmbH
39 | *
40 | * © 2015 MyFonts Inc
41 | */
42 |
43 |
44 | /*
45 | * @license
46 | * MyFonts Webfont Build ID 3109936, 2015-10-18T18:52:23-0400
47 | *
48 | * The fonts listed in this notice are subject to the End User License
49 | * Agreement(s) entered into by the website owner. All other parties are
50 | * explicitly restricted from using the Licensed Webfonts(s).
51 | *
52 | * You may obtain a valid license at the URLs below.
53 | *
54 | * Webfont: FF Magda Clean Mono Web Pro Regular by FontFont
55 | * URL: http://www.myfonts.com/fonts/fontfont/ff-magda-clean-mono/pro-regular/
56 | * Copyright: 2011 Critzla, Cornel Windlin, Henning Krause published by FSI FontShop International GmbH
57 | * Licensed pageviews: 50,000
58 | *
59 | *
60 | * License: http://www.myfonts.com/viewlicense?type=web&buildid=3109936
61 | *
62 | * © 2015 MyFonts Inc
63 | */
64 |
65 |
66 | @font-face {font-family: 'FFMagdaCleanMonoWebProRegular';src: url('webfonts/2F7430_0_0.eot');src: url('webfonts/2F7430_0_0.eot?#iefix') format('embedded-opentype'),url('webfonts/2F7430_0_0.woff2') format('woff2'),url('webfonts/2F7430_0_0.woff') format('woff'),url('webfonts/2F7430_0_0.ttf') format('truetype');}
67 |
68 |
69 | @font-face {font-family: 'FFMarkWebItalic';src: url('webfonts/2F7411_0_0.eot');src: url('webfonts/2F7411_0_0.eot?#iefix') format('embedded-opentype'),url('webfonts/2F7411_0_0.woff2') format('woff2'),url('webfonts/2F7411_0_0.woff') format('woff'),url('webfonts/2F7411_0_0.ttf') format('truetype');}
70 |
71 |
72 | @font-face {font-family: 'FFMarkWebBoldItalic';src: url('webfonts/2F7411_1_0.eot');src: url('webfonts/2F7411_1_0.eot?#iefix') format('embedded-opentype'),url('webfonts/2F7411_1_0.woff2') format('woff2'),url('webfonts/2F7411_1_0.woff') format('woff'),url('webfonts/2F7411_1_0.ttf') format('truetype');}
73 |
74 |
75 | @font-face {font-family: 'FFMarkWebBold';src: url('webfonts/2F7411_2_0.eot');src: url('webfonts/2F7411_2_0.eot?#iefix') format('embedded-opentype'),url('webfonts/2F7411_2_0.woff2') format('woff2'),url('webfonts/2F7411_2_0.woff') format('woff'),url('webfonts/2F7411_2_0.ttf') format('truetype');}
76 |
77 |
78 | @font-face {font-family: 'FFMarkWebLightItalic';src: url('webfonts/2F7411_3_0.eot');src: url('webfonts/2F7411_3_0.eot?#iefix') format('embedded-opentype'),url('webfonts/2F7411_3_0.woff2') format('woff2'),url('webfonts/2F7411_3_0.woff') format('woff'),url('webfonts/2F7411_3_0.ttf') format('truetype');}
79 |
80 |
81 | @font-face {font-family: 'FFMarkWebLight';src: url('webfonts/2F7411_4_0.eot');src: url('webfonts/2F7411_4_0.eot?#iefix') format('embedded-opentype'),url('webfonts/2F7411_4_0.woff2') format('woff2'),url('webfonts/2F7411_4_0.woff') format('woff'),url('webfonts/2F7411_4_0.ttf') format('truetype');}
82 |
83 |
84 | @font-face {font-family: 'FFMarkWebMediumItalic';src: url('webfonts/2F7411_5_0.eot');src: url('webfonts/2F7411_5_0.eot?#iefix') format('embedded-opentype'),url('webfonts/2F7411_5_0.woff2') format('woff2'),url('webfonts/2F7411_5_0.woff') format('woff'),url('webfonts/2F7411_5_0.ttf') format('truetype');}
85 |
86 |
87 | @font-face {font-family: 'FFMarkWebMedium';src: url('webfonts/2F7411_6_0.eot');src: url('webfonts/2F7411_6_0.eot?#iefix') format('embedded-opentype'),url('webfonts/2F7411_6_0.woff2') format('woff2'),url('webfonts/2F7411_6_0.woff') format('woff'),url('webfonts/2F7411_6_0.ttf') format('truetype');}
88 |
89 |
90 | @font-face {font-family: 'FFMarkWeb';src: url('webfonts/2F7411_7_0.eot');src: url('webfonts/2F7411_7_0.eot?#iefix') format('embedded-opentype'),url('webfonts/2F7411_7_0.woff2') format('woff2'),url('webfonts/2F7411_7_0.woff') format('woff'),url('webfonts/2F7411_7_0.ttf') format('truetype');}
91 |
92 | /* FOLD Edit: Set bold weight of markweb to the bold version of the font*/
93 | @font-face {font-family: 'FFMarkWeb';src: url('webfonts/2F7411_2_0.eot');src: url('webfonts/2F7411_2_0.eot?#iefix') format('embedded-opentype'),url('webfonts/2F7411_2_0.woff2') format('woff2'),url('webfonts/2F7411_2_0.woff') format('woff'),url('webfonts/2F7411_2_0.ttf') format('truetype');
94 | font-weight: bold;}
95 |
--------------------------------------------------------------------------------
/client/styles/about.lessimport:
--------------------------------------------------------------------------------
1 | a.green {
2 | &:hover {
3 | color: @action-color;
4 | }
5 | }
6 |
7 | a.underline{
8 | text-decoration: underline;
9 | }
10 |
11 | .contact-footer {
12 | position: relative;
13 | bottom:0;
14 | height: 50px;
15 | width: 100%;
16 | margin-top: 45px;
17 | text-align: center;
18 | font-size: 12px;
19 | padding: 15px;
20 | background-color: white;
21 | z-index: 5;
22 | a {
23 | color: @action-color;
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/client/styles/amaran.min.css:
--------------------------------------------------------------------------------
1 | .amaran-overlay{position:fixed;width:100%;height:100%;top:0;left:0;background:rgba(153,204,51,.9);display:block;z-index:777}.amaran-overlay .amaran-wrapper{z-index:9999}
2 | .amaran.awesome{width:300px;min-height:65px;background:#f3f3f3;color:#222;margin:15px;padding:5px 5px 5px 70px;font-family:"Open Sans",Helvetica,Arial,sans-serif;font-size:16px;font-weight:600;box-shadow:1px 1px 1px #000}.amaran.awesome .icon{width:50px;height:50px;position:absolute;top:50%;left:10px;background:#000;margin-top:-25px;border-radius:50%;text-align:center;line-height:50px;font-size:22px}.amaran.awesome p{padding:0;margin:0}.amaran.awesome p span{font-weight:300}.amaran.awesome p span.light{font-size:13px;display:block;color:#777}.amaran.awesome.ok p.bold{color:#178B13}.amaran.awesome.ok .icon{background-color:#178B13;color:#fff}.amaran.awesome.error p.bold{color:#D82222}.amaran.awesome.error .icon{background-color:#D82222;color:#fff}.amaran.awesome.warning p.bold{color:#9F6000}.amaran.awesome.warning .icon{background-color:#9F6000;color:#fff}.amaran.awesome.yellow p.bold{color:#CFA846}.amaran.awesome.yellow .icon{background-color:#CFA846;color:#fff}.amaran.awesome.blue p.bold{color:#2980b9}.amaran.awesome.blue .icon{background-color:#2980b9;color:#fff}.amaran.awesome.green p.bold{color:#27ae60}.amaran.awesome.green .icon{background-color:#27ae60;color:#fff}.amaran.awesome.purple p.bold{color:#5B54AA}.amaran.awesome.purple .icon{background-color:#5B54AA;color:#fff}
3 | .amaran.colorful{width:300px;min-height:45px;overflow:hidden;background-color:transparent;z-index:1}.amaran.colorful .colorful-inner{width:100%;min-height:45px;display:block;position:relative;background-color:#484860;padding:15px 25px 15px 15px;color:#fff;font-size:14px;border-bottom:1px solid rgba(0,0,0,.2);border-radius:4px}.amaran.colorful .amaran-close{color:#fff;z-index:2;top:8px;right:8px;text-align:center;line-height:18px}.amaran-wrapper.center .amaran.colorful{margin:0 auto}
4 | .amaran.default{width:300px;min-height:45px;background:#1B1E24;background:-webkit-linear-gradient(left,#111213,#111213 15%,#1b1e24 15%,#1b1e24);background:linear-gradient(left,#111213,#111213 15%,#1b1e24 15%,#1b1e24);color:#fff;font-family:"Open Sans",Helvetica,Arial,sans-serif;font-size:13px;font-weight:300;margin:5px;overflow:hidden;border-bottom:1px solid #111213;border-radius:6px}.amaran.default .default-spinner{width:45px;min-height:45px;display:block;float:left;position:relative}.amaran.default .default-spinner span{width:18px;height:18px;background:#27ae60;display:block;border-radius:50%;position:absolute;top:50%;left:50%;margin-left:-11px;margin-top:-9px}.amaran.default .default-message{float:left}.amaran.default .default-message span{padding:3px;line-height:43px}.amaran.default .default-message:after{clear:both}
5 | @charset "UTF-8";.amaran-close,.amaran-sticky{height:20px;top:2px;cursor:pointer}.amaran-wrapper *{box-sizing:border-box}.amaran-wrapper{position:fixed;z-index:9999}.amaran-wrapper.top{top:0;bottom:auto}.amaran-wrapper.bottom{bottom:0;top:auto}.amaran-wrapper.left{left:0}.amaran-wrapper.right{right:0;left:auto}.amaran-wrapper.center{width:50%;height:50%;margin:auto;position:fixed;top:0;left:0;bottom:0;right:0}.amaran{width:200px;background:rgba(0,0,0,.7);padding:3px;color:#fff;border-radius:4px;display:none;font-size:13px;cursor:pointer;position:relative;text-align:left;min-height:50px;margin:10px}.amaran-close,.amaran-sticky{width:20px;display:block;position:absolute}.amaran-close{right:2px}.amaran-close:before{content:"x";color:#fff;font-weight:700;font-family:Arial,sans-serif;font-size:18px}.amaran-sticky{right:20px}.amaran-sticky:before{content:"●";color:#fff;font-weight:700;font-family:Arial,sans-serif;font-size:18px}.amaran-sticky.sticky:before{color:#27ae60}
6 | .amaran.tumblr{width:300px;min-height:45px;overflow:hidden;background-color:#fff;color:#444;border-radius:3px;box-shadow:0 1px 4px rgba(0,0,0,.3);z-index:1}.amaran.tumblr .title{position:relative;font-size:15px;line-height:15px;height:28px;padding:5px 10px;border-bottom:1px solid rgba(0,0,0,.1);font-weight:700;z-index:1}.amaran.tumblr .content{padding:5px}.amaran.tumblr .image{float:left}.amaran.tumblr .amaran-close{z-index:2}.amaran.tumblr .amaran-close:before{color:#000}
7 | .amaran.user{width:300px;min-height:100px;background:#f3f3f3;color:#222;margin:15px;font-family:"Open Sans",Helvetica,Arial,sans-serif;font-size:13px;font-weight:300;box-shadow:1px 1px 1px #000;border-radius:0;padding:0}.amaran.user .icon{width:100px;height:100px;position:relative;background:#000;float:left}.amaran.user img{max-width:100%}.amaran.user .info{padding-left:110px;padding-top:10px}.amaran.user b{display:block;font-size:16px}.amaran.user.blue{background:#2773ed;color:#fff}.amaran.user.yellow{background:#f4b300;color:#fff}.amaran.user.green{background:#78ba00;color:#fff}
8 |
--------------------------------------------------------------------------------
/client/styles/icons.lessimport:
--------------------------------------------------------------------------------
1 | .standard-icon-colors;
2 |
3 | .active{
4 | .bg{
5 | fill: @action-color;
6 | }
7 | }
8 |
9 | .back-arrow{
10 | .fg{
11 | fill: @white-color;
12 | }
13 | }
14 |
15 | svg.icon {
16 | .size-to-fit;
17 | }
18 | svg.add-card-icon{
19 | height:20px;
20 | width:20px;
21 | }
22 |
23 | svg.browse-arrow {
24 | .size(20px);
25 | .fg{
26 | fill: @white-color;
27 | }
28 | }
29 | svg.search-icon {
30 | height: 15px;
31 | width: 15px;
32 | margin-top: 2px;
33 | }
34 |
35 | svg.babyburger-icon{
36 | .size(20px);
37 | &:hover {
38 | .bg{
39 | fill: @action-color;
40 | }
41 | }
42 | }
43 |
44 | svg.mobile-back-icon{
45 | .bg{
46 | fill: @action-color;
47 | }
48 | }
49 |
50 | .star-button{
51 | @height: 20px;
52 | display: inline-block;
53 | font-size: 18px;
54 | height: @height;
55 | line-height: @height;
56 | button {
57 | height: @height;
58 | width: @height;
59 | svg{
60 | width: auto;
61 | }
62 | padding: 0;
63 | background-color: transparent;
64 | border: none;
65 | }
66 | }
67 |
68 | .share-button{
69 | .star-button;
70 | }
71 |
72 | .favorite-button{
73 | .star-button;
74 | .favorite {
75 | .fg {
76 | fill: @medium-color;
77 | &:hover {
78 | fill: @favorite-color;
79 | }
80 | }
81 | }
82 | .unfavorite{
83 | .fg{
84 | fill: @favorite-color;
85 | }
86 | }
87 | .just-favorited{
88 | .fg {
89 | fill: @favorite-color;
90 | }
91 | svg{
92 | .animation(grow 0.5s 1);
93 | }
94 | }
95 | .just-unfavorited{
96 | .fg {
97 | &:hover {
98 | fill: @medium-color;
99 | }
100 | }
101 | }
102 | @keyframes grow{
103 | 50%{
104 | .transform(scale(1.25));
105 | }
106 | }
107 |
108 |
109 | }
110 |
111 | .editors-pick-button{
112 | .star-button;
113 | .pick{
114 | .fg{
115 | fill: @light-color;
116 | &:hover{
117 | fill: saturate(@orange-color, 80%);;
118 | }
119 | }
120 | }
121 | .unpick{
122 | .fg{
123 | fill: @orange-color;
124 | &:hover{
125 | fill: fade(@orange-color, 80%);
126 | }
127 | }
128 | }
129 | }
130 |
131 | i.loading-icon{
132 | color: @action-color;
133 | }
134 |
135 | .facebook-social-icon{
136 | .fg{
137 | fill: @inactive-color;
138 | }
139 | &:hover{
140 | .fg{
141 | fill: @facebook-color;
142 | }
143 | }
144 | }
145 |
146 | .instagram-social-icon{
147 | .fg{
148 | fill: @inactive-color;
149 | }
150 | &:hover{
151 | .fg{
152 | fill: @instagram-color;
153 | }
154 | }
155 | }
156 | .twitter-social-icon{
157 | .fg{
158 | fill: @inactive-color;
159 | }
160 | &:hover{
161 | .fg{
162 | fill: @twitter-color;
163 | }
164 | }
165 | }
166 |
167 | .clear-search-icon{
168 | .fg{
169 | fill: @inactive-color;
170 | }
171 | .bg{
172 | fill: @background-color;
173 | }
174 | }
175 |
176 | .follow-flag{
177 | .fg{
178 | fill: @white-color;
179 | }
180 | }
181 |
182 | .follow-flag-check{
183 | .bg{
184 | fill: @social-color;
185 | }
186 | }
187 |
188 | .follow-flag-plus{
189 | .bg{
190 | fill: @inactive-color;
191 | }
192 | }
193 |
194 | .follow-flag-x{
195 | .bg{
196 | fill: @orange-color;
197 | }
198 | }
199 |
200 | .social-option-triangle{
201 | .fg{
202 | fill: @dark-color
203 | }
204 | }
205 |
206 | .embed-icon{
207 | .fg{
208 | fill: @dark-color
209 | }
210 | &:hover{
211 | .fg{
212 | fill: @action-color;
213 | }
214 | }
215 | }
216 |
217 | .fold-title{
218 | height: 100%;
219 | .fg{
220 | fill: @action-color
221 | }
222 | }
223 |
--------------------------------------------------------------------------------
/client/styles/login.lessimport:
--------------------------------------------------------------------------------
1 | @button-height: 45px;
2 |
3 | div.login, div.reset-password, div.recover-password {
4 | width: 100%;
5 | padding-top: 240px;
6 | height: auto;
7 | margin-bottom: 45px;
8 | @media @mobile {
9 | padding-top: 200px;
10 | }
11 | }
12 |
13 | input.error, input.required {
14 | //&:after{
15 | // content:'no';
16 | //}
17 | }
18 | input.success{
19 | //&:after{
20 | // content:'no';
21 | //}
22 | }
23 |
24 | @media @not-mobile{
25 | div.login-left {
26 | position: relative;
27 | float: left;
28 | width: 45%;
29 | }
30 |
31 | div.login-right {
32 | position: relative;
33 | float: left;
34 | width: 45%;
35 | }
36 | }
37 |
38 | a.green {
39 | color: @action-color;
40 | }
41 |
42 | .save-profile-button, .reset-button {
43 | margin-top: 20px;
44 | width: 100px;
45 | background-color: @action-color;
46 | color: @white-color;
47 | height: @button-height;
48 | .FFMarkWebBold;
49 |
50 | &:hover {
51 | background-color: @dark-color;
52 | }
53 | }
54 |
55 |
56 | div.log-in-error {
57 | margin-top: 10px;
58 | }
59 |
60 | @vertical-center: 90px;
61 |
62 | div.divider {
63 | @font-size: 20px;
64 | position: relative;
65 | float: left;
66 | width: 10%;
67 | font-size: @font-size;
68 | text-align: center;
69 | margin-top: @vertical-center - @font-size/2;
70 |
71 | @media @mobile {
72 | width: 100%;
73 | margin-top: @font-size;
74 | margin-bottom: @font-size;
75 | visibility: hidden;
76 | }
77 | }
78 |
79 | div.sign-up-options {
80 | margin: auto;
81 | width: 350px;
82 | a{
83 | .FFMarkWebBold;
84 | background-color: @action-color;
85 | height: @button-height;
86 | display: block;
87 | color: @white-color;
88 | text-align: center;
89 | line-height: @button-height;
90 | &:hover {
91 | background-color: @dark-color;
92 | }
93 | }
94 | }
95 |
96 |
97 | .signin-overlay{
98 | z-index: 99999;
99 | position: fixed;
100 | top: 0;
101 | left: 0;
102 | background-color: fade(@dark-color, 80%);
103 | .FFMarkWebMedium;
104 |
105 | .signin-modal{
106 |
107 | @max-width: calc(100% ~"-" @magic-styling-distance);
108 | @height: 60px;
109 | @width: 380px;
110 |
111 | margin:auto;
112 | display:block;
113 | text-align: center;
114 | background-color: @white-color;
115 | font-size: 18px;
116 | @media @mobile {
117 | font-size: 16px;
118 | }
119 |
120 | .size(500px);
121 |
122 |
123 | button{
124 | font-size: 18px;
125 |
126 | background-color: @action-color;
127 | color: @white-color;
128 | .loading-icon{
129 | color: @white-color;
130 | }
131 | .FFMarkWebBold;
132 | &:hover{
133 | &:not(.no-hover-state){
134 | background-color: @black-color;
135 | }
136 | }
137 | &:disabled{
138 | background-color: @inactive-color;
139 | cursor: auto;
140 | }
141 | }
142 | a, button.text{
143 | background: none !important;
144 | color: @social-color;
145 | padding: 0;
146 | &:hover{
147 | text-decoration: underline;
148 | }
149 | }
150 |
151 | max-height: 100%;
152 | max-width: 100%;
153 |
154 | .center-over-parent-div;
155 |
156 | .title{
157 | margin-top: 93px;
158 | &.has-explanation{
159 | margin-top: 68px;
160 | }
161 | &.absolute{
162 | position: absolute;
163 | width: 100%;
164 | top: @magic-styling-distance + 5px;
165 | margin-top:0;
166 | text-align: center;
167 | }
168 | img{
169 | width: 120px;
170 | }
171 | line-height: 0;
172 | }
173 |
174 | .slogan, .explanation{
175 | margin-top: @magic-styling-distance;
176 | @media @mobile {
177 | margin-top: @magic-styling-distance / 2;
178 | }
179 | line-height: normal;
180 | }
181 | .slogan{
182 | color: @medium-color;
183 | }
184 |
185 | .explanation{
186 | color: @dark-color;
187 | line-height: 28px;
188 | white-space: pre-wrap;
189 | }
190 |
191 | .user-menu{
192 | width: 100%;
193 | margin-top: @magic-styling-distance;
194 | @media @mobile {
195 | margin-top: @magic-styling-distance / 2;
196 | }
197 | text-align: center;
198 | }
199 |
200 | button.signin{
201 | display: inline-block;
202 | margin-bottom: @magic-styling-distance / 2;
203 | width: @width;
204 | height: @height;
205 | max-width: @max-width;
206 | }
207 |
208 | @input-width: 335px;
209 | @status-width: 60px;
210 |
211 | input{
212 | height: @height;
213 | width: @input-width;
214 | max-width: calc(100% ~"-" (@status-width + @magic-styling-distance));
215 | &::-webkit-input-placeholder {
216 | .FFMarkWebBold;
217 | }
218 | &:-moz-placeholder {
219 | .FFMarkWebBold;
220 | }
221 | &:-ms-input-placeholder {
222 | .FFMarkWebBold;
223 | }
224 | margin-top: 30px;
225 | margin-bottom: 0px;
226 | &:first-child{
227 | margin-top: 0;
228 | }
229 | }
230 |
231 |
232 | .status{
233 | width: @status-width;
234 | height: 20px;
235 | text-align: center;
236 | display: inline-block;
237 | position: relative;
238 | left: -20px;
239 | svg{
240 | position: absolute;
241 | }
242 | }
243 | .field-info{
244 | height: 0;
245 | span{
246 | width: @input-width + @status-width;
247 | max-width: calc(100% ~"-" (@magic-styling-distance));
248 | display: inline-block;
249 | text-align: left;
250 | }
251 | text-align: center;
252 | font-size: 12px;
253 | color: @placeholder-color;
254 | }
255 | div.error{
256 | color: @orange-color;
257 | }
258 |
259 | .back{
260 | position: absolute;
261 | left: @magic-styling-distance;
262 | top: @magic-styling-distance + 13px;
263 | @media @mobile{
264 | left: @magic-styling-distance / 2;
265 | top: @magic-styling-distance / 2 + 13px;
266 | }
267 | color: @medium-color !important;
268 | }
269 |
270 | .already-have{
271 | margin-top: @magic-styling-distance / 2;
272 | max-width: 100%;
273 | }
274 | .close{
275 | .FFMarkWeb;
276 | position: absolute;
277 | top: @magic-styling-distance;
278 | right: @magic-styling-distance;
279 | @media @mobile{
280 | top: @magic-styling-distance / 2;
281 | right: @magic-styling-distance / 2;
282 | }
283 | background-color: @medium-color;
284 | .size(@magic-styling-distance);
285 | font-size: @magic-styling-distance/2;
286 | padding: 0;
287 | text-align: center;
288 | }
289 |
290 | form{
291 | margin-top: 105px;
292 | }
293 |
294 | .accept{
295 | font-size: 12px;
296 | margin: 0;
297 | margin-top: 15px;
298 | &.above{
299 | margin-top: 35px;
300 | }
301 | .padding-sides(@magic-styling-distance/2);
302 | color: @placeholder-color;
303 | }
304 |
305 | hr{
306 | width: @width;
307 | border: 0;
308 | border-top: 1px solid @medium-color;
309 | margin-top: @magic-styling-distance / 2;
310 | margin-bottom: @magic-styling-distance / 2;
311 | }
312 |
313 | #login-form{
314 | line-height: 0;
315 | input, .lost-or-login{
316 | width: @width;
317 | max-width: @max-width;
318 | }
319 | .lost-or-login{
320 | display: inline-block;
321 | line-height: normal;
322 | position: relative;
323 | .forgot-password{
324 | margin-top: 17px;
325 | float: left;
326 | cursor: pointer;
327 | &:hover{
328 | text-decoration: underline !important;
329 | }
330 | }
331 | .login-button{
332 | float: right;
333 | }
334 | .error{
335 | position: absolute;
336 | font-size: 14px;
337 | line-height: 18px;
338 | bottom: 0;
339 | white-space: pre-line;
340 |
341 |
342 | @media @mobile{
343 | font-size: 14px;
344 | line-height: 15px;
345 | bottom: 0px;
346 | white-space: normal;
347 | }
348 | text-align: left;
349 | width: calc(100% ~"-" 130px);
350 | }
351 | }
352 | }
353 |
354 | .twitter-signin{
355 | svg{
356 | .size(auto);
357 | margin-left: 13px;
358 | display: inline;
359 | vertical-align: middle;
360 | }
361 | span{
362 | display: inline;
363 | vertical-align: middle;
364 | }
365 | }
366 |
367 |
368 | .signup-button, .login-button{
369 | display: inline-block;
370 | height: @height;
371 | width: 120px;
372 | text-align: center;
373 | }
374 | .signup-button{
375 | margin-top: @magic-styling-distance;
376 | }
377 | .login-button{
378 | margin-top: @magic-styling-distance / 2;
379 | }
380 |
381 | //onboarding
382 | .welcome{
383 | font-size: 36px;
384 | margin-top: 120px;
385 | margin-bottom: 35px;
386 | }
387 | .author-image{
388 | height: 95px;
389 | img{
390 | height: 100%;
391 | }
392 | }
393 | .edit-prompt{
394 | margin-top: 35px;
395 | margin-bottom: 40px;
396 | }
397 | .finish{
398 | height: @height;
399 | width: 120px;
400 | }
401 |
402 | //recover-password
403 | #recover-password-form{
404 | margin-top: 120px;
405 | }
406 | #recover-password-email{
407 | width: @width;
408 | max-width: @max-width;
409 | }
410 | .recover-button{
411 | height: @height;
412 | margin-top: @magic-styling-distance/2;
413 | width: @width;
414 | max-width: @max-width;
415 | }
416 | .message{
417 | margin-top: @magic-styling-distance/2;
418 | }
419 | }
420 | }
421 |
--------------------------------------------------------------------------------
/client/styles/metaview.lessimport:
--------------------------------------------------------------------------------
1 | @metaview-padding: 50px;
2 |
3 | .metaview {
4 | position: fixed;
5 | overflow: scroll;
6 | padding-top: @metaview-padding;
7 | padding-left: @metaview-padding;
8 | background-color: rgba(0,0,0,0.9);
9 | z-index: 100;
10 | min-height: 100%;
11 | min-width: 100%;
12 |
13 | button.close {
14 | position: absolute;
15 | top: 20px;
16 | right: 20px;
17 | i {
18 | font-size: 50px;
19 | }
20 | }
21 |
22 |
23 | .cards {
24 |
25 | margin: 0 auto;
26 | overflow: scroll;
27 | display: inline-block;
28 |
29 | .vertical-block, .horizontal-block {
30 | cursor: move;
31 | background-color: @medium-color;
32 | border: 2px solid transparent;
33 |
34 | &:hover {
35 | background-color: @action-color;
36 | border-color: @action-color;
37 | }
38 | }
39 |
40 | h4{
41 | margin-top: 4px;
42 | margin-bottom: 4px;
43 | }
44 |
45 | p{
46 | margin-top: 4px;
47 | }
48 |
49 | .row {
50 | display: table;
51 | height: 100px;
52 | margin-bottom: 5px;
53 |
54 | .vertical-block {
55 | padding: 5px 3px;
56 | font-size: 10px;
57 | height: 100%;
58 | width: 160px;
59 | float: left;
60 | white-space: normal;
61 | font-size: 12px;
62 | line-height: 14px;
63 | overflow: auto;
64 | }
65 |
66 | .horizontal-section {
67 | display: inline-block;
68 | min-width: 100px;
69 | height: 90px;
70 | .horizontal-block {
71 | margin-top: 5px;
72 | margin-left: 5px;
73 | height: 80px;
74 | width: 120px;
75 | display: inline-block;
76 | white-space: normal;
77 | line-height: 14px;
78 | font-size: 12px;
79 | overflow: auto;
80 | position: relative;
81 |
82 | &.has-image{
83 | .vertically-center-images;
84 | text-align: center;
85 | background-color: @dark-color;
86 | }
87 |
88 | background-position: center center;
89 | background-repeat: no-repeat;
90 | -webkit-background-size: cover;
91 | -moz-background-size: cover;
92 | -o-background-size: cover;
93 | background-size: cover;
94 |
95 | img {
96 | width: auto;
97 | max-width: 100%;
98 | margin: auto;
99 | vertical-align: middle;
100 | display: inline-block;
101 | max-height: 100%;
102 | }
103 | }
104 | }
105 | }
106 | }
107 | }
108 |
--------------------------------------------------------------------------------
/client/styles/profile.lessimport:
--------------------------------------------------------------------------------
1 | @picture-radius: 126px;
2 | @profile-width: 340px;
3 |
4 |
5 | h1 {
6 | font-size: 30px;
7 | margin-bottom: 45px;
8 | @media (max-width: 800px) {
9 | text-align: center;
10 | }
11 | }
12 |
13 | .message {
14 | ul{
15 | margin-top: 10px;
16 | }
17 | li{
18 | list-style: disc;
19 | margin-left: 20px;
20 | color: @social-color;
21 | }
22 | &.stories-message{
23 | a{
24 | &:hover{
25 | text-decoration: underline !important;
26 | }
27 | }
28 | }
29 | button{
30 | color: @white-color;
31 | height: @magic-styling-distance;
32 | .FFMarkWebBold;
33 | font-size: 13px;
34 | .padding-sides(20px);
35 | }
36 | button.create-story, button.view-drafts, .random-story, .random-person{
37 | margin-top: @magic-styling-distance/2;
38 | }
39 | .create-story{
40 | background-color: @action-color;
41 | }
42 | .view-drafts{
43 | background-color: @social-color;
44 | }
45 |
46 | }
47 |
48 | div.profile-section {
49 | padding-top: 120px;
50 | height: 100%;
51 | position: fixed;
52 |
53 | @media (max-width: 800px) {
54 | position: inherit;
55 | }
56 | @media @mobile {
57 | padding-top: @magic-styling-distance * 2;
58 | }
59 | }
60 |
61 |
62 | div.user-profile {
63 | position: fixed;
64 | padding: @magic-styling-distance/2;
65 | background-color: @white-color;
66 | width: @profile-width;
67 | max-width: 340px;
68 | min-height: 700px;
69 | height: 100%;
70 | z-index: 5;
71 | border-right: 1px solid @inactive-color;
72 |
73 | @media (max-width: 800px) {
74 | @margin-side: 5%;
75 | margin-left: @margin-side;
76 | max-width: 100%;
77 | width: calc(100% ~"-" @margin-side*2);
78 | height: auto;
79 | min-height: 0;
80 | position: initial;
81 | margin-top: 30px;
82 | border-right: none;
83 | }
84 |
85 | @media (max-height: 640px) {
86 | position: initial;
87 |
88 | }
89 |
90 | div.picture {
91 | background-color: @light-transparent-color;
92 | width: @picture-radius;
93 | height: @picture-radius;
94 | border-radius: @picture-radius;
95 | margin: @magic-styling-distance/2 calc(50% ~"-" @picture-radius*0.5) 20px;
96 | overflow: hidden;
97 | position: relative;
98 | margin-bottom: 35px;
99 |
100 | .profile-picture-large {
101 | height: @picture-radius;
102 | width: @picture-radius;
103 | }
104 | }
105 |
106 | .following-followers{
107 | margin-top: @magic-styling-distance;
108 | .padding-sides(35px);
109 | .display-flex;
110 | text-align: center;
111 | color: @social-color;
112 | font-size: 13px;
113 | line-height: 17px;
114 | .FFMarkWebBold;
115 | div{
116 | display: inline-block;
117 | .flex(1);
118 | cursor: pointer;
119 | }
120 | }
121 |
122 | .follow-button-container{
123 | text-align: center;
124 | margin-top: @magic-styling-distance - 2px;
125 | }
126 | .follow-button{
127 | display: inline-block;
128 | }
129 |
130 | div.name, div.bio {
131 | text-align: center;
132 | word-wrap:break-word;
133 | margin: 15px 30px;
134 | }
135 | div.name {
136 | font-size: 20px;
137 | .FFMarkWebBold;
138 | margin-bottom: 10px;
139 | line-height: 120%;
140 | }
141 | div.bio {
142 | .FFMarkWeb;
143 | line-height: 160%;
144 | a{
145 | color: @action-color;
146 | }
147 | }
148 | .edit-profile {
149 | color: @social-color;
150 | position: absolute;
151 | .FFMarkWebBold;
152 | font-size: 13px;
153 | &:hover{
154 | text-decoration: underline;
155 | }
156 | }
157 | .save-profile-button {
158 | position: absolute;
159 | top: 0px;
160 | @media (max-width: 800px), (max-height: 640px) {
161 | top: 150px;
162 | }
163 | }
164 | .bio-form {
165 | .FFMarkWeb;
166 | margin-top: 5px;
167 | margin-bottom: 15px;
168 | height: 110px;
169 | border: 1px solid @medium-color;
170 | padding: 10px 18px;
171 | font-size: 16px;
172 | line-height: 22px;
173 | }
174 | }
175 |
176 | .toggle-published, .toggle-favorites {
177 | .FFMarkWebBold;
178 | font-size: 13px;
179 | padding: 0;
180 | color: @dark-color;
181 | display: block;
182 | text-align: center;
183 |
184 | @media (max-width: 800px) {
185 | top: -14px;
186 | left: 0px;
187 | margin: 0 auto;
188 | width: 100%;
189 | }
190 | &:hover {
191 | text-decoration: underline;
192 | color: @action-color;
193 | }
194 |
195 | }
196 |
197 | .my-stories-buttons{
198 | position: absolute;
199 | width: 100%;
200 | bottom: 12px;
201 | padding-top: 70px;
202 | button{
203 | color: white;
204 | padding: 10px;
205 | }
206 | .button-group{
207 | float:right;
208 | }
209 | .unpublish{
210 | margin-right: 10px;
211 | background-color: @social-color;
212 | &:hover{
213 | background-color: darken(@social-color, 10%);
214 | }
215 | }
216 | .delete{
217 | margin-right: 15px;
218 | background-color: @less-danger-color;
219 | &:hover{
220 | background-color: darken(@less-danger-color, 10%);
221 | }
222 | }
223 | }
224 |
225 | .profile{
226 | .message{
227 | @media @mobile {
228 | .padding-sides(20px);
229 | }
230 | }
231 | }
232 |
--------------------------------------------------------------------------------
/client/styles/reset_password.lessimport:
--------------------------------------------------------------------------------
1 | .reset-password, .recover-password {
2 | div.form-container {
3 | width: 350px;
4 | margin: 0 auto;
5 |
6 | h1 {
7 | text-align: center;
8 | margin-bottom: 50px;
9 | font-size: 23px;
10 | }
11 |
12 | form {
13 | width: 350px;
14 | font-size: 15px;
15 | .FFMarkWeb;
16 | margin: auto;
17 | }
18 |
19 | .message {
20 | margin-top: 10px;
21 | }
22 |
23 | .reset-button {
24 | width: 150px;
25 | }
26 |
27 | .recover-button {
28 | margin-top: 20px;
29 | width: 250px;
30 | background-color: @action-color;
31 | color: @white-color;
32 | height: @button-height;
33 |
34 | &:hover {
35 | background-color: @dark-color;
36 | }
37 | }
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/client/styles/story.lessimport:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/readFOLD/FOLD/4da7a49c8eee1c7f627ad700e130cfd7605c83ec/client/styles/story.lessimport
--------------------------------------------------------------------------------
/client/styles/styles.less:
--------------------------------------------------------------------------------
1 | // Top-level universal elements (e.g. header and footer), mixins, utility functions
2 | // For now, includes re-used elements
3 | @import "icons.lessimport";
4 | @import "layout.lessimport";
5 |
6 | // Re-used elements in all story views
7 | @import "story.lessimport";
8 |
9 | // Page-specific
10 | @import "create.lessimport";
11 | @import "home.lessimport";
12 | @import "about.lessimport";
13 | @import "login.lessimport";
14 | @import "profile.lessimport";
15 | @import "reset_password.lessimport";
16 |
17 | // Thing-specific
18 | @import "metaview.lessimport";
19 | @import "widgets.lessimport";
20 |
--------------------------------------------------------------------------------
/client/styles/widgets.lessimport:
--------------------------------------------------------------------------------
1 |
2 | .sod_select, .sod_select * {
3 | -webkit-box-sizing: border-box;
4 | -moz-box-sizing: border-box;
5 | box-sizing: border-box;
6 | -webkit-touch-callout: none;
7 | -webkit-user-select: none;
8 | -moz-user-select: none;
9 | -ms-user-select: none;
10 | user-select: none;
11 | }
12 |
13 | /* The SoD - Please keep this first three lines intact, otherwise all hell will break looooooose */
14 | .sod_select {
15 | display: inline-block;
16 | position: relative;
17 | line-height: 1;
18 |
19 | width: 180px;
20 | padding: 14px 10px;
21 | border: 1px solid @medium-color;
22 | background: #ffffff;
23 | color: #444444;
24 | font-size: 14px;
25 | .text-align-start;
26 | outline: 0;
27 | outline-offset: -2px; /* Opera */
28 | cursor: default;
29 |
30 | // Up/down arrows
31 | @arrow-size: 5px;
32 | &:before, &:after {
33 | content: "";
34 | border-left: @arrow-size solid transparent;
35 | border-right: @arrow-size solid transparent;
36 | border-top: @arrow-size solid @dark-color;
37 | position: absolute;
38 | right: 15px;
39 | top: 18px;
40 | }
41 |
42 | &.open {
43 | &:before, &:after {
44 | border-bottom: @arrow-size solid @action-color;
45 | border-top: none;
46 | }
47 | }
48 |
49 |
50 | // // Down arrow
51 | // &:after {
52 | // content: "▾";
53 | // top: auto;
54 | // bottom: 12px;
55 | // }
56 |
57 | // Change the border color on hover, focus and when open
58 | &:hover, &.open, &.focus {
59 | border-color: #000000;
60 | }
61 | &.open { color: #919191; }
62 | &.focus { box-shadow: 0 0 5px rgba(0,0,0,.2); }
63 |
64 | // When the entire SoD is disabled, go crazy!
65 | &.disabled {
66 | border-color: #828282;
67 | color: #b2b2b2;
68 | cursor: not-allowed;
69 | }
70 |
71 | // The "label", or whatever we should call it. Keep the first three lines for truncating.
72 | .sod_label {
73 | display: block;
74 | overflow: hidden;
75 | white-space: nowrap;
76 | text-overflow: ellipsis;
77 |
78 | padding-right: 15px;
79 | }
80 |
81 | // Options list wrapper
82 | .sod_list_wrapper {
83 | position: absolute;
84 | top: 100%;
85 | left: 0;
86 | display: none;
87 | height: auto;
88 | width: 180px;
89 | margin: 0 0 0 -1px;
90 | background: #ffffff;
91 | border: 1px solid black;
92 | border-top: none;
93 | color: #444444;
94 | z-index: 1;
95 | }
96 |
97 | /* Shows the option list (don't edit) */
98 | &.open .sod_list_wrapper { display: block; }
99 |
100 | /* Don't display the options when */
101 | &.disabled.open .sod_list_wrapper { display: none; }
102 |
103 | /* When the option list is displayed above the SoD */
104 | &.above .sod_list_wrapper {
105 | top: auto;
106 | bottom: 100%;
107 | border-top: 3px solid #000000;
108 | border-bottom: none;
109 | }
110 |
111 | // Options list container
112 | .sod_list {
113 | display: block;
114 | overflow-y: auto;
115 | padding: 0;
116 | margin: 0;
117 | }
118 |
119 | .sod_option {
120 | display: block;
121 | overflow: hidden;
122 | white-space: nowrap;
123 | text-overflow: ellipsis;
124 | position: relative;
125 | padding: 14px 10px;
126 | list-style-type: none;
127 |
128 | &.optgroup, &.optgroup.disabled {
129 | background: inherit;
130 | color: #939393;
131 | font-size: 10px;
132 | font-style: italic;
133 | }
134 |
135 | &.groupchild { padding-left: 20px; }
136 |
137 | &.is-placeholder { display: none; }
138 |
139 | &.disabled {
140 | background: inherit;
141 | color: #cccccc
142 | }
143 |
144 | &.active {
145 | background: #f7f7f7;
146 | color: #333333;
147 |
148 | }
149 |
150 | &.selected {
151 | font-weight: 500;
152 | padding-right: 25px;
153 | }
154 |
155 | &.selected:before {
156 | content: "";
157 | position: absolute;
158 | right: 10px;
159 | top: 50%;
160 | .transform(translateY(-50%));
161 | display: inline-block;
162 | color: #808080;
163 | height: 9px;
164 | width: 10px;
165 | background: url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4NCjwhLS0gR2VuZXJhdG9yOiBBZG9iZSBJbGx1c3RyYXRvciAxNy4xLjAsIFNWRyBFeHBvcnQgUGx1Zy1JbiAuIFNWRyBWZXJzaW9uOiA2LjAwIEJ1aWxkIDApICAtLT4NCjwhRE9DVFlQRSBzdmcgUFVCTElDICItLy9XM0MvL0RURCBTVkcgMS4xLy9FTiIgImh0dHA6Ly93d3cudzMub3JnL0dyYXBoaWNzL1NWRy8xLjEvRFREL3N2ZzExLmR0ZCI+DQo8c3ZnIHZlcnNpb249IjEuMSIgaWQ9IkxheWVyXzEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHg9IjBweCIgeT0iMHB4Ig0KCSB2aWV3Qm94PSIwIDAgMTAgOSIgZW5hYmxlLWJhY2tncm91bmQ9Im5ldyAwIDAgMTAgOSIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+DQo8Zz4NCgk8cGF0aCBmaWxsPSIjRDlEOUQ4IiBkPSJNNCw2LjdDMy42LDYuMywzLjUsNi4xLDMuMSw1LjdDMi42LDUuMiwyLDQuNiwxLjUsNC4xYy0wLjgtMC44LTIsMC40LTEuMiwxLjJjMC45LDAuOSwxLjksMS45LDIuOCwyLjgNCgkJYzAuNywwLjcsMS4zLDEsMiwwQzYuNyw2LDguMywzLjcsOS44LDEuNUMxMC41LDAuNSw5LTAuMyw4LjMsMC42bDAsMEM2LjcsMi45LDUuNyw0LjQsNCw2LjciLz4NCjwvZz4NCjwvc3ZnPg0K);
166 | }
167 | }
168 |
169 |
170 | // Hide native select
171 | select { display: none !important; }
172 |
173 | // The native select in touch mode. Keep this first line. Sorry, keep everything.
174 | &.touch select {
175 | -webkit-appearance: menulist-button;
176 |
177 | position: absolute;
178 | top: 0;
179 | left: 0;
180 | display: block !important;
181 | height: 100%;
182 | width: 100%;
183 | opacity: 0;
184 | z-index: 1;
185 | }
186 | }
187 |
--------------------------------------------------------------------------------
/client/unsubscribe.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | {{> top_banner}}
4 |
5 | {{#if resubscribed}}
6 | You have resubscribed to {{humanReadableEmailType}} emails! °\(^o^)/°
7 | {{else}}
8 | {{#if unsubscribed}}
9 | You have successfully unsubscribed from {{humanReadableEmailType}} emails.
10 | If this was an accident, click resubscribe
11 | {{else}}
12 | Unsubscribing you from {{humanReadableEmailType}} emails.
13 | {{/if}}
14 | {{/if}}
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/client/unsubscribe.js:
--------------------------------------------------------------------------------
1 | Template.unsubscribe.onCreated(function(){
2 | this.unsubscribed = new ReactiveVar(false);
3 | this.resubscribed = new ReactiveVar(false);
4 |
5 | this.autorun(() => {
6 | if(Meteor.userId()){
7 | Meteor.call('unsubscribe', Router.current().params.query.email_type, (err, success) => {
8 | if(err || !success){
9 | notifyError('Unsubscribe failed. Please email us at info@readfold.com')
10 | } else {
11 | this.unsubscribed.set(true);
12 | }
13 | })
14 | } else {
15 | openSignInOverlay('Please sign in to unsubscribe from emails');
16 | }
17 | })
18 |
19 | });
20 |
21 | Template.unsubscribe.events({
22 | 'click .resubscribe' (e, t){
23 | Meteor.call('resubscribe', Router.current().params.query.email_type, (err, success) => {
24 | if (err) {
25 | notifyError('Resubscribe failed. Please email us at info@readfold.com')
26 | } else {
27 | t.resubscribed.set(true);
28 | }
29 | })
30 | }
31 | });
32 |
33 | Template.unsubscribe.helpers({
34 | 'unsubscribed' (){
35 | return Template.instance().unsubscribed.get();
36 | },
37 | 'resubscribed' (){
38 | return Template.instance().resubscribed.get();
39 | },
40 | 'humanReadableEmailType' (){
41 |
42 | switch (Router.current().params.query.email_type){
43 | case 'followed-you':
44 | return 'Follower Notifications';
45 | break;
46 | case 'following-published':
47 | return 'Notifications When Someone You Follow Publishes a Story';
48 | break;
49 | default:
50 | return Router.current().params.query.email_type;
51 | }
52 | }
53 | });
54 |
--------------------------------------------------------------------------------
/collections/user-collections.js:
--------------------------------------------------------------------------------
1 | if(!this.Schema){
2 | Schema = {};
3 | }
4 |
5 | Schema.UserProfile = new SimpleSchema({
6 | name: {
7 | type: String,
8 | optional: true,
9 | min: 2,
10 | max: 127,
11 | autoValue () { // trim off whitespace
12 | if (this.isSet && typeof this.value === "string") {
13 | return this.value.trim();
14 | } else {
15 | this.unset()
16 | }
17 | }
18 | },
19 | bio: {
20 | type: String,
21 | optional: true,
22 | max: 160,
23 | autoValue () { // trim off whitespace
24 | if (this.isSet && typeof this.value === "string") {
25 | return this.value.trim();
26 | } else {
27 | this.unset()
28 | }
29 | },
30 | autoform: {
31 | rows: 7
32 | }
33 | },
34 | favorites: {
35 | type: [String],
36 | optional: true,
37 | defaultValue: []
38 | },
39 | following: {
40 | type: [String],
41 | optional: true,
42 | defaultValue: []
43 | },
44 | profilePicture: {
45 | type: String,
46 | autoValue () {
47 | var monster = _.random(1,10).toString();
48 | if (this.isSet) {
49 | return this.value;
50 | } else if (this.isInsert) {
51 | return this.value || monster;
52 | } else if (this.isUpsert) {
53 | return {$setOnInsert: this.value || monster};
54 | } else {
55 | this.unset();
56 | }
57 | }
58 | }
59 | });
60 |
61 | Schema.User = new SimpleSchema({
62 | username: {
63 | type: String,
64 | regEx: /^[a-z0-9_]*$/,
65 | min: 3,
66 | max: 15,
67 | optional: true,
68 | autoValue () {
69 | if (this.isSet && typeof this.value === "string") {
70 | return this.value.toLowerCase().trim();
71 | } else {
72 | this.unset()
73 | }
74 | }
75 | },
76 | displayUsername: { // allows for caps
77 | type: String,
78 | optional: true,
79 | autoValue () { // TODO ensure this matches username except for capitalization
80 | if (this.isSet && typeof this.value === "string") {
81 | return this.value.trim();
82 | } else {
83 | this.unset()
84 | }
85 | }
86 | },
87 | tempUsername: {
88 | type: String,
89 | optional: true
90 | },
91 | emails: {
92 | type: [Object],
93 | optional: true
94 | },
95 | "emails.$.address": {
96 | type: String,
97 | regEx: SimpleSchema.RegEx.Email,
98 | label: "Email address",
99 | autoValue () {
100 | if (this.isSet && typeof this.value === "string") {
101 | return this.value.toLowerCase();
102 | } else {
103 | this.unset();
104 | }
105 | },
106 | autoform: {
107 | afFieldInput: {
108 | readOnly: true,
109 | disabled: true
110 | }
111 | }
112 | },
113 | "emails.$.verified": {
114 | type: Boolean
115 | },
116 | createdAt: {
117 | type: Date,
118 | autoValue () {
119 | if (this.isInsert) {
120 | return new Date;
121 | } else if (this.isUpsert) {
122 | return {$setOnInsert: new Date};
123 | } else {
124 | this.unset();
125 | }
126 | }
127 | },
128 | admin: {
129 | type: Boolean,
130 | optional: true,
131 | autoValue (){
132 | this.unset(); // don't allow to be set from anywhere within the code
133 | }
134 | },
135 | accessPriority: {
136 | type: Number,
137 | optional: true
138 | },
139 | profile: {
140 | type: Schema.UserProfile,
141 | optional: true,
142 | defaultValue: {}
143 | },
144 | followers: {
145 | type: [String],
146 | optional: true,
147 | defaultValue: []
148 | },
149 | followersTotal: {
150 | type: Number,
151 | optional: true,
152 | defaultValue: 0
153 | },
154 | followingTotal: {
155 | type: Number,
156 | optional: true,
157 | defaultValue: 0
158 | },
159 | services: {
160 | type: Object,
161 | optional: true,
162 | blackbox: true
163 | },
164 | unsubscribes: {
165 | type: [String],
166 | allowedValues: ['followed-you', 'following-published'],
167 | optional: true
168 | }
169 | });
170 |
171 |
172 | Meteor.users.attachSchema(Schema.User);
173 |
174 | SimpleSchema.messages({
175 | "regEx username": "Username may only contain letters, numbers, and underscores"
176 | });
177 |
--------------------------------------------------------------------------------
/collections/user-methods.js:
--------------------------------------------------------------------------------
1 | Meteor.methods({
2 | saveProfilePicture (userId, pictureId) {
3 | check(userId, String);
4 | check(pictureId, String);
5 | if (this.userId === userId) {
6 | Meteor.users.update({
7 | _id: this.userId
8 | }, {
9 | $set: {
10 | "profile.profilePicture": pictureId
11 | }
12 | });
13 | } else {
14 | throw new Meteor.Error("Only the account owner may edit this profile")
15 | }
16 | },
17 | updateProfile (modifier, userId) { // TO-DO cleanup
18 | check(userId, String);
19 | check(modifier, Object);
20 |
21 | var bio, name, newName;
22 | var modifierSet = modifier.$set;
23 | var modifierUnset = modifier.$unset;
24 |
25 | var setObject = {};
26 | if (bio = modifierSet['profile.bio']) {
27 | check(bio, String);
28 | setObject['profile.bio'] = bio;
29 | } else if (modifierUnset['profile.bio'] === "") {
30 | setObject['profile.bio'] = '';
31 | }
32 |
33 | if (name = modifierSet['profile.name']) {
34 | check(name, String);
35 | setObject['profile.name'] = name;
36 | } else if (modifierUnset['profile.name'] === "") {
37 | setObject['profile.name'] = '';
38 | }
39 |
40 | if (this.userId === userId) {
41 | if (newName = setObject['profile.name']) {
42 | if (newName !== Meteor.user().profile.name) {
43 | Stories.update({authorId: this.userId}, { // update authorName on stories if name changed
44 | $set: {
45 | authorName: newName
46 | }
47 | })
48 | }
49 | }
50 |
51 | Meteor.users.update({
52 | _id: this.userId
53 | }, {
54 | $set: setObject
55 | });
56 | } else {
57 | throw new Meteor.Error("Only the account owner may edit this profile")
58 | }
59 | }
60 | });
61 |
62 |
63 |
--------------------------------------------------------------------------------
/lib/activities.js:
--------------------------------------------------------------------------------
1 | infoFor = function(type, id){
2 | switch(type){
3 | case 'Person':
4 | var user = Meteor.users.findOne(id, {fields: {'profile.name' : 1, 'profile.profilePicture' : 1, 'services.twitter.id' : 1, 'displayUsername': 1}});
5 | var userInfo = {
6 | id: user._id,
7 | type: 'Person',
8 | name: user.profile.name,
9 | urlPath: '/profile/' + user.displayUsername,
10 | imageId: user.profile.profilePicture
11 | };
12 |
13 | if (user.services && user.services.twitter){
14 | _.extend(userInfo, {
15 | twitterId: user.services.twitter.id
16 | });
17 | }
18 |
19 | return userInfo;
20 | case 'Story':
21 | var story = Stories.findOne({_id: id, published: true}, {fields: {'title' : 1, 'userPathSegment': 1, 'storyPathSegment': 1, 'headerImage': 1, authorId: 1, authorDisplayUsername: 1, authorUsername: 1 }});
22 | return {
23 | id: story._id,
24 | type: 'Story',
25 | name: story.title,
26 | urlPath: '/read/' + story.userPathSegment + '/' + story.storyPathSegment,
27 | imageId: story.headerImage,
28 | attributedTo: {
29 | id: story.authorId,
30 | type: 'Person',
31 | name: story.authorDisplayUsername || story.authorUsername,
32 | urlPath: '/profile/' + story.authorDisplayUsername || story.authorUsername
33 | }
34 | };
35 | default:
36 | throw new Meteor.Error('Type not found for infoFor')
37 | }
38 | };
39 |
40 | generateFavoriteActivity = function(userId, storyId){
41 | if(Meteor.isServer){
42 | Meteor.defer(function(){ // make non-blocking
43 | check(userId, String);
44 | check(storyId, String);
45 |
46 | generateActivity('Favorite', {
47 | actor: infoFor('Person', userId),
48 | object: infoFor('Story', storyId)
49 | })
50 | })
51 | }
52 | };
53 |
54 | generateFollowActivity = function(userId, userToFollowId){
55 | if(Meteor.isServer){
56 |
57 | Meteor.defer(function(){ // make non-blocking
58 | check(userId, String);
59 | check(userToFollowId, String);
60 |
61 | var userToFollow = Meteor.users.findOne(userToFollowId, {fields: {'profile.following': 1}});
62 | var activityType = _.contains(userToFollow.profile.following, userId) ? 'FollowBack' : 'Follow';
63 |
64 | generateActivity(activityType, {
65 | actor: infoFor('Person', userId),
66 | object: infoFor('Person', userToFollowId)
67 | })
68 | })
69 | }
70 | };
71 |
72 | generatePublishActivity = function(userId, storyId){
73 | if(Meteor.isServer){
74 | Meteor.defer(function(){ // make non-blocking
75 | check(userId, String);
76 | check(storyId, String);
77 |
78 | generateActivity('Publish', {
79 | actor: infoFor('Person', userId),
80 | object: infoFor('Story', storyId)
81 | })
82 | })
83 | }
84 | };
85 |
86 | generateShareActivity = function(storyId, service){
87 | if(Meteor.isServer){
88 | Meteor.defer(function(){ // make non-blocking
89 | check(storyId, String);
90 | check(service, String);
91 |
92 | generateActivity('Share', {
93 | content: service,
94 | object: infoFor('Story', storyId)
95 | });
96 | })
97 | }
98 | };
99 |
100 | generateViewThresholdActivity = function(storyId, viewCount){
101 | if(Meteor.isServer){
102 | Meteor.defer(function(){ // make non-blocking
103 | check(storyId, String);
104 | check(viewCount, Number);
105 |
106 | generateActivity('ViewThreshold', {
107 | content: viewCount,
108 | object: infoFor('Story', storyId)
109 | });
110 | })
111 | }
112 | };
113 |
--------------------------------------------------------------------------------
/lib/constants.js:
--------------------------------------------------------------------------------
1 | PUB_SIZE = 30;
2 | if (Meteor.isClient){
3 | window.PUB_SIZE = PUB_SIZE;
4 | }
5 |
6 | CLOUDINARY_FILE_SIZE = 20000000; // bytes
7 |
8 | VIEW_THRESHOLDS = [
9 | 25,
10 | 50,
11 | 75,
12 | 100,
13 | 150,
14 | 200,
15 | 250,
16 | 300,
17 | 350,
18 | 400,
19 | 450,
20 | 500,
21 | 600,
22 | 700,
23 | 800,
24 | 900,
25 | 1000,
26 | 1250,
27 | 1500,
28 | 1750,
29 | 2000,
30 | 2250,
31 | 2500,
32 | 2750,
33 | 3000,
34 | 3250,
35 | 3500,
36 | 3750,
37 | 4000,
38 | 4250,
39 | 4500,
40 | 4750,
41 | 5000,
42 | 5500,
43 | 6000,
44 | 6500,
45 | 7000,
46 | 7500,
47 | 8000,
48 | 8500,
49 | 9000,
50 | 9500,
51 | 10000,
52 | 11000,
53 | 12000,
54 | 13000,
55 | 14000,
56 | 15000,
57 | 20000,
58 | 30000,
59 | 40000,
60 | 50000,
61 | 60000,
62 | 70000,
63 | 80000,
64 | 90000,
65 | 100000,
66 | 200000,
67 | 500000,
68 | 1000000
69 | ];
70 |
--------------------------------------------------------------------------------
/lib/helpers.js:
--------------------------------------------------------------------------------
1 | newTypeSpecificContextBlock = function (doc) {
2 | switch (doc.type) {
3 | case 'stream':
4 | return new Stream(doc);
5 | case 'video':
6 | return new VideoBlock(doc);
7 | case 'text':
8 | return new TextBlock(doc);
9 | case 'map':
10 | return new MapBlock(doc);
11 | case 'image':
12 | return new ImageBlock(doc);
13 | case 'gif':
14 | return new GifBlock(doc);
15 | case 'audio':
16 | return new AudioBlock(doc);
17 | case 'viz':
18 | return new VizBlock(doc);
19 | case 'twitter':
20 | return new TwitterBlock(doc);
21 | case 'link':
22 | return new LinkBlock(doc);
23 | case 'news':
24 | return new NewsBlock(doc);
25 | case 'action':
26 | return new ActionBlock(doc);
27 | default:
28 | return new ContextBlock(doc);
29 | }
30 | };
31 |
32 | idFromPathSegment = function(pathSegment) { // everything after last dash
33 | return pathSegment.substring(pathSegment.lastIndexOf('-') + 1);
34 | };
35 |
36 | sum = function(a,b){ return a+b; };
37 |
38 | if(Meteor.isServer){
39 | import cheerio from 'cheerio';
40 | }
41 |
42 | getProfileImage = function(profilePicture, twitterId, size, forEmail){
43 | var diameter;
44 | if (size === 'large'){
45 | diameter = 150;
46 | } else {
47 | diameter = 60;
48 | }
49 | var defaultProfilePic = 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=='; // transparent gif
50 | var dprSetting = ((typeof window == 'undefined') || window.isHighDensity) ? ',dpr_2.0' : '';
51 | var twitterPic;
52 | if (twitterId) {
53 | twitterPic = '//res.cloudinary.com/' + Meteor.settings['public'].CLOUDINARY_CLOUD_NAME + '/image/twitter/w_' + diameter + ',h_' + diameter + ',c_fill,g_face' + dprSetting + '/' + twitterId
54 | }
55 |
56 |
57 | if (profilePicture || twitterId) {
58 | if ( profilePicture) {
59 | if ( profilePicture < 20) { // it's a monster
60 | if (twitterPic){
61 | return twitterPic
62 | } else { // show monster
63 | if(forEmail){
64 | return '//res.cloudinary.com/' + Meteor.settings['public'].CLOUDINARY_CLOUD_NAME + '/w_' + diameter + ',h_' + diameter + dprSetting + '/static/profile_monster_' + profilePicture + '.png';
65 | } else {
66 | return '//res.cloudinary.com/' + Meteor.settings['public'].CLOUDINARY_CLOUD_NAME + '/static/profile_monster_' + profilePicture + '.svg';
67 | }
68 | }
69 | } else {
70 | return '//res.cloudinary.com/' + Meteor.settings['public'].CLOUDINARY_CLOUD_NAME + '/image/upload/w_' + diameter + ',h_' + diameter + ',c_fill,g_face' + dprSetting + '/' + profilePicture
71 | }
72 | } else if (twitterPic) {
73 | return twitterPic
74 | }
75 | }
76 |
77 | // if nothing else served up
78 | return defaultProfilePic
79 | }
80 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "fold",
3 | "version": "1.0.0",
4 | "description": "FOLD is a platform allowing storytellers to structure and contextualize stories",
5 | "main": "./start",
6 | "scripts": {
7 | "test": "echo \"Error: no test specified\" && exit 1"
8 | },
9 | "repository": {
10 | "type": "git",
11 | "url": "git+https://github.com/readFOLD/FOLD.git"
12 | },
13 | "author": "Joe Goldbeck & Alexis Hope",
14 | "bugs": {
15 | "url": "https://github.com/readFOLD/FOLD/issues"
16 | },
17 | "homepage": "https://readfold.com",
18 | "dependencies": {
19 | "babel-runtime": "^6.20.0",
20 | "bcrypt": "^1.0.1",
21 | "cheerio": "0.19.0",
22 | "meteor-node-stubs": "^0.2.4",
23 | "prerender-node": "2.0.2",
24 | "twit": "1.1.20",
25 | "vimeo-api": "1.1.2"
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/public/2014_Ebola_virus_epidemic_in_West_Africa.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/readFOLD/FOLD/4da7a49c8eee1c7f627ad700e130cfd7605c83ec/public/2014_Ebola_virus_epidemic_in_West_Africa.png
--------------------------------------------------------------------------------
/public/Deceased_per_day_Ebola_2014.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/readFOLD/FOLD/4da7a49c8eee1c7f627ad700e130cfd7605c83ec/public/Deceased_per_day_Ebola_2014.png
--------------------------------------------------------------------------------
/public/EbolaCycle.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/readFOLD/FOLD/4da7a49c8eee1c7f627ad700e130cfd7605c83ec/public/EbolaCycle.png
--------------------------------------------------------------------------------
/public/Ebola_Betten_Isolation.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/readFOLD/FOLD/4da7a49c8eee1c7f627ad700e130cfd7605c83ec/public/Ebola_Betten_Isolation.jpg
--------------------------------------------------------------------------------
/public/Ebola_Virus.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/readFOLD/FOLD/4da7a49c8eee1c7f627ad700e130cfd7605c83ec/public/Ebola_Virus.jpg
--------------------------------------------------------------------------------
/public/alright_sans.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/readFOLD/FOLD/4da7a49c8eee1c7f627ad700e130cfd7605c83ec/public/alright_sans.woff
--------------------------------------------------------------------------------
/public/alright_sans_bold.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/readFOLD/FOLD/4da7a49c8eee1c7f627ad700e130cfd7605c83ec/public/alright_sans_bold.woff
--------------------------------------------------------------------------------
/public/batsmonkeys.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/readFOLD/FOLD/4da7a49c8eee1c7f627ad700e130cfd7605c83ec/public/batsmonkeys.jpg
--------------------------------------------------------------------------------
/public/cdc_doctor_discards.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/readFOLD/FOLD/4da7a49c8eee1c7f627ad700e130cfd7605c83ec/public/cdc_doctor_discards.jpg
--------------------------------------------------------------------------------
/public/ebola_isolation_chamber.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/readFOLD/FOLD/4da7a49c8eee1c7f627ad700e130cfd7605c83ec/public/ebola_isolation_chamber.jpg
--------------------------------------------------------------------------------
/public/embedtest.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Runsudshdushud!!
7 |
8 |
9 |
--------------------------------------------------------------------------------
/public/js/responsive-embed.js:
--------------------------------------------------------------------------------
1 | var minWidthBeforeShrink = 435;
2 | var minHeightBeforeShrink = 435;
3 | var shrunkenWidth = 300;
4 | var shrunkenHeight = 200;
5 |
6 | var isMobile = false; //initiate as false
7 | // device detection
8 | if(/(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|ipad|iris|kindle|Android|Silk|lge |maemo|midp|mmp|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows (ce|phone)|xda|xiino/i.test(navigator.userAgent)
9 | || /1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i.test(navigator.userAgent.substr(0,4))) isMobile = true;
10 |
11 | var forEach = function (array, callback, scope) {
12 | for (var i = 0; i < array.length; i++) {
13 | callback.call(scope, i, array[i]);
14 | }
15 | };
16 |
17 | var iterateOverFoldIFrames = function(cb){
18 | forEach(document.querySelectorAll('iframe[src*="//fold.cm"]'), cb)
19 | };
20 |
21 | // from underscorejs.org
22 | var throttle = function(func, wait, options) {
23 | var context, args, result;
24 | var timeout = null;
25 | var previous = 0;
26 | if (!options) options = {};
27 | var later = function() {
28 | previous = options.leading === false ? 0 : Date.now();
29 | timeout = null;
30 | result = func.apply(context, args);
31 | if (!timeout) context = args = null;
32 | };
33 | return function() {
34 | var now = Date.now();
35 | if (!previous && options.leading === false) previous = now;
36 | var remaining = wait - (now - previous);
37 | context = this;
38 | args = arguments;
39 | if (remaining <= 0 || remaining > wait) {
40 | if (timeout) {
41 | clearTimeout(timeout);
42 | timeout = null;
43 | }
44 | previous = now;
45 | result = func.apply(context, args);
46 | if (!timeout) context = args = null;
47 | } else if (!timeout && options.trailing !== false) {
48 | timeout = setTimeout(later, remaining);
49 | }
50 | return result;
51 | };
52 | };
53 |
54 | var resizeIFrames = function(){
55 | iterateOverFoldIFrames(function(i, iframe){
56 | iframe.style.maxHeight = '';
57 | iframe.style.maxWidth = '';
58 | if(iframe.offsetWidth < minWidthBeforeShrink || iframe.offsetHeight < minHeightBeforeShrink || isMobile){
59 | iframe.style.maxHeight = shrunkenHeight + "px";
60 | iframe.style.maxWidth = shrunkenWidth + "px";
61 | }
62 | })
63 | };
64 |
65 | var throttledResize = throttle(resizeIFrames, 100);
66 |
67 | window.addEventListener("load", function(event) {
68 | resizeIFrames();
69 | if(!isMobile){
70 | window.addEventListener('resize', throttledResize, true);
71 | }
72 | });
73 |
--------------------------------------------------------------------------------
/public/nurses_1976.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/readFOLD/FOLD/4da7a49c8eee1c7f627ad700e130cfd7605c83ec/public/nurses_1976.jpg
--------------------------------------------------------------------------------
/public/webfonts/2F7411_0_0.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/readFOLD/FOLD/4da7a49c8eee1c7f627ad700e130cfd7605c83ec/public/webfonts/2F7411_0_0.eot
--------------------------------------------------------------------------------
/public/webfonts/2F7411_0_0.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/readFOLD/FOLD/4da7a49c8eee1c7f627ad700e130cfd7605c83ec/public/webfonts/2F7411_0_0.ttf
--------------------------------------------------------------------------------
/public/webfonts/2F7411_0_0.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/readFOLD/FOLD/4da7a49c8eee1c7f627ad700e130cfd7605c83ec/public/webfonts/2F7411_0_0.woff
--------------------------------------------------------------------------------
/public/webfonts/2F7411_0_0.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/readFOLD/FOLD/4da7a49c8eee1c7f627ad700e130cfd7605c83ec/public/webfonts/2F7411_0_0.woff2
--------------------------------------------------------------------------------
/public/webfonts/2F7411_1_0.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/readFOLD/FOLD/4da7a49c8eee1c7f627ad700e130cfd7605c83ec/public/webfonts/2F7411_1_0.eot
--------------------------------------------------------------------------------
/public/webfonts/2F7411_1_0.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/readFOLD/FOLD/4da7a49c8eee1c7f627ad700e130cfd7605c83ec/public/webfonts/2F7411_1_0.ttf
--------------------------------------------------------------------------------
/public/webfonts/2F7411_1_0.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/readFOLD/FOLD/4da7a49c8eee1c7f627ad700e130cfd7605c83ec/public/webfonts/2F7411_1_0.woff
--------------------------------------------------------------------------------
/public/webfonts/2F7411_1_0.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/readFOLD/FOLD/4da7a49c8eee1c7f627ad700e130cfd7605c83ec/public/webfonts/2F7411_1_0.woff2
--------------------------------------------------------------------------------
/public/webfonts/2F7411_2_0.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/readFOLD/FOLD/4da7a49c8eee1c7f627ad700e130cfd7605c83ec/public/webfonts/2F7411_2_0.eot
--------------------------------------------------------------------------------
/public/webfonts/2F7411_2_0.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/readFOLD/FOLD/4da7a49c8eee1c7f627ad700e130cfd7605c83ec/public/webfonts/2F7411_2_0.ttf
--------------------------------------------------------------------------------
/public/webfonts/2F7411_2_0.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/readFOLD/FOLD/4da7a49c8eee1c7f627ad700e130cfd7605c83ec/public/webfonts/2F7411_2_0.woff
--------------------------------------------------------------------------------
/public/webfonts/2F7411_2_0.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/readFOLD/FOLD/4da7a49c8eee1c7f627ad700e130cfd7605c83ec/public/webfonts/2F7411_2_0.woff2
--------------------------------------------------------------------------------
/public/webfonts/2F7411_3_0.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/readFOLD/FOLD/4da7a49c8eee1c7f627ad700e130cfd7605c83ec/public/webfonts/2F7411_3_0.eot
--------------------------------------------------------------------------------
/public/webfonts/2F7411_3_0.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/readFOLD/FOLD/4da7a49c8eee1c7f627ad700e130cfd7605c83ec/public/webfonts/2F7411_3_0.ttf
--------------------------------------------------------------------------------
/public/webfonts/2F7411_3_0.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/readFOLD/FOLD/4da7a49c8eee1c7f627ad700e130cfd7605c83ec/public/webfonts/2F7411_3_0.woff
--------------------------------------------------------------------------------
/public/webfonts/2F7411_3_0.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/readFOLD/FOLD/4da7a49c8eee1c7f627ad700e130cfd7605c83ec/public/webfonts/2F7411_3_0.woff2
--------------------------------------------------------------------------------
/public/webfonts/2F7411_4_0.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/readFOLD/FOLD/4da7a49c8eee1c7f627ad700e130cfd7605c83ec/public/webfonts/2F7411_4_0.eot
--------------------------------------------------------------------------------
/public/webfonts/2F7411_4_0.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/readFOLD/FOLD/4da7a49c8eee1c7f627ad700e130cfd7605c83ec/public/webfonts/2F7411_4_0.ttf
--------------------------------------------------------------------------------
/public/webfonts/2F7411_4_0.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/readFOLD/FOLD/4da7a49c8eee1c7f627ad700e130cfd7605c83ec/public/webfonts/2F7411_4_0.woff
--------------------------------------------------------------------------------
/public/webfonts/2F7411_4_0.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/readFOLD/FOLD/4da7a49c8eee1c7f627ad700e130cfd7605c83ec/public/webfonts/2F7411_4_0.woff2
--------------------------------------------------------------------------------
/public/webfonts/2F7411_5_0.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/readFOLD/FOLD/4da7a49c8eee1c7f627ad700e130cfd7605c83ec/public/webfonts/2F7411_5_0.eot
--------------------------------------------------------------------------------
/public/webfonts/2F7411_5_0.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/readFOLD/FOLD/4da7a49c8eee1c7f627ad700e130cfd7605c83ec/public/webfonts/2F7411_5_0.ttf
--------------------------------------------------------------------------------
/public/webfonts/2F7411_5_0.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/readFOLD/FOLD/4da7a49c8eee1c7f627ad700e130cfd7605c83ec/public/webfonts/2F7411_5_0.woff
--------------------------------------------------------------------------------
/public/webfonts/2F7411_5_0.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/readFOLD/FOLD/4da7a49c8eee1c7f627ad700e130cfd7605c83ec/public/webfonts/2F7411_5_0.woff2
--------------------------------------------------------------------------------
/public/webfonts/2F7411_6_0.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/readFOLD/FOLD/4da7a49c8eee1c7f627ad700e130cfd7605c83ec/public/webfonts/2F7411_6_0.eot
--------------------------------------------------------------------------------
/public/webfonts/2F7411_6_0.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/readFOLD/FOLD/4da7a49c8eee1c7f627ad700e130cfd7605c83ec/public/webfonts/2F7411_6_0.ttf
--------------------------------------------------------------------------------
/public/webfonts/2F7411_6_0.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/readFOLD/FOLD/4da7a49c8eee1c7f627ad700e130cfd7605c83ec/public/webfonts/2F7411_6_0.woff
--------------------------------------------------------------------------------
/public/webfonts/2F7411_6_0.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/readFOLD/FOLD/4da7a49c8eee1c7f627ad700e130cfd7605c83ec/public/webfonts/2F7411_6_0.woff2
--------------------------------------------------------------------------------
/public/webfonts/2F7411_7_0.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/readFOLD/FOLD/4da7a49c8eee1c7f627ad700e130cfd7605c83ec/public/webfonts/2F7411_7_0.eot
--------------------------------------------------------------------------------
/public/webfonts/2F7411_7_0.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/readFOLD/FOLD/4da7a49c8eee1c7f627ad700e130cfd7605c83ec/public/webfonts/2F7411_7_0.ttf
--------------------------------------------------------------------------------
/public/webfonts/2F7411_7_0.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/readFOLD/FOLD/4da7a49c8eee1c7f627ad700e130cfd7605c83ec/public/webfonts/2F7411_7_0.woff
--------------------------------------------------------------------------------
/public/webfonts/2F7411_7_0.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/readFOLD/FOLD/4da7a49c8eee1c7f627ad700e130cfd7605c83ec/public/webfonts/2F7411_7_0.woff2
--------------------------------------------------------------------------------
/public/webfonts/2F7430_0_0.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/readFOLD/FOLD/4da7a49c8eee1c7f627ad700e130cfd7605c83ec/public/webfonts/2F7430_0_0.eot
--------------------------------------------------------------------------------
/public/webfonts/2F7430_0_0.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/readFOLD/FOLD/4da7a49c8eee1c7f627ad700e130cfd7605c83ec/public/webfonts/2F7430_0_0.ttf
--------------------------------------------------------------------------------
/public/webfonts/2F7430_0_0.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/readFOLD/FOLD/4da7a49c8eee1c7f627ad700e130cfd7605c83ec/public/webfonts/2F7430_0_0.woff
--------------------------------------------------------------------------------
/public/webfonts/2F7430_0_0.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/readFOLD/FOLD/4da7a49c8eee1c7f627ad700e130cfd7605c83ec/public/webfonts/2F7430_0_0.woff2
--------------------------------------------------------------------------------
/reset:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | meteor reset
3 | exit
4 |
--------------------------------------------------------------------------------
/server/accounts.js:
--------------------------------------------------------------------------------
1 | validateNewUser = function(user){
2 | if (user.username){ // only if an email user. if twitter user will do this later
3 | if (user.emails && user.emails[0]){
4 | return checkUserSignup(user.username, user.emails[0].address);
5 | } else {
6 | throw new Meteor.Error('Please enter your email')
7 | }
8 | } else {
9 | return true
10 | }
11 | };
12 |
13 | Accounts.validateNewUser(validateNewUser);
14 |
15 | if (!Meteor.settings.NEW_USER_ACCESS_PRIORITY) {
16 | throw new Meteor.Error('Meteor.settings.NEW_USER_ACCESS_PRIORITY is required')
17 | }
18 |
19 | Accounts.onCreateUser(function(options, user) {
20 | if(!options || !user) {
21 | throw new Meteor.Error('Error creating user');
22 | return;
23 | }
24 |
25 | if (options.profile) {
26 | user.profile = options.profile;
27 | } else {
28 | user.profile = {};
29 | }
30 |
31 | if (user.username === 'author') {
32 | user.accessPriority = options.accessPriority;
33 | } else {
34 | user.accessPriority = parseInt(Meteor.settings.NEW_USER_ACCESS_PRIORITY);
35 | }
36 |
37 | if (user.services.twitter) { // twitter signup
38 | user.tempUsername = user.services.twitter.screenName;
39 | } else { // email signup
40 | user.displayUsername = options.username;
41 | Meteor.defer(function(){
42 | sendWelcomeEmail(user);
43 | });
44 | }
45 |
46 | return user;
47 | });
48 |
49 | // Password Reset E-mail
50 | Accounts.emailTemplates.from = 'FOLD Accounts ';
51 | Accounts.emailTemplates.siteName = 'readfold.com',
52 |
53 | Accounts.emailTemplates.resetPassword.subject = function(user, url) {
54 | return 'FOLD Password Reset';
55 | };
56 |
57 | Accounts.emailTemplates.resetPassword.text = function(user, url) {
58 | url = url.replace('#/', '')
59 | return "To reset your password, simply click the link below:\n\n" + url + "\n\n" + "Happy FOLDing!\nFOLD Team\nhttps://readfold.com";
60 | };
61 |
--------------------------------------------------------------------------------
/server/activities.js:
--------------------------------------------------------------------------------
1 | generateActivity = function(type, details){
2 | check(type, String);
3 | check(details, Object);
4 |
5 | if(details.fanout){
6 | throw new Meteor.Error('Fanout should not be set');
7 | }
8 | var fullDetails = _.extend({}, details, {type: type});
9 |
10 | var dedupDetails = {
11 | type: type
12 | };
13 |
14 | _.each(['actor', 'object', 'target'], function(key){
15 | if(details[key]){
16 | dedupDetails[key +'.id'] = details[key].id;
17 | }
18 | });
19 |
20 | switch(type){
21 | case 'Share':
22 | // pass through
23 | break;
24 | case 'Message':
25 | // pass through
26 | break;
27 | default: // don't allow duplicate activities
28 | if(details.content){
29 | dedupDetails.content = details.content.toString();
30 | }
31 | if(Activities.find(dedupDetails, {limit: 1}).count()){
32 | return // if this is a duplicate. stop here.
33 | }
34 |
35 | }
36 |
37 | Activities.insert(fullDetails);
38 | };
39 |
40 |
41 | generateActivityFeedItem = function(userId, activityId, relevancy){
42 | check(userId, String);
43 | check(activityId, String);
44 | check(relevancy, Date);
45 |
46 | return ActivityFeedItems.insert({
47 | uId: userId,
48 | aId: activityId,
49 | r: relevancy
50 | })
51 | };
52 |
53 |
54 | fanToObject = function(activity){
55 | check(activity.object, Object);
56 | generateActivityFeedItem(activity.object.id, activity._id, activity.published);
57 | };
58 |
59 | fanToObjectAuthor = function(activity){
60 | check(activity.object, Object);
61 |
62 | var populatedObject;
63 |
64 | switch (activity.object.type){
65 | case 'Story':
66 | populatedObject = Stories.findOne(activity.object.id, {fields: {authorId: 1}});
67 | break;
68 | default:
69 | throw new Meteor.Error('Object not found in database for activity: ' + activity._id);
70 | }
71 |
72 | if(populatedObject){
73 | generateActivityFeedItem(populatedObject.authorId, activity._id, activity.published); // fan to author
74 | }
75 | };
76 |
77 |
78 | fanoutActivity = function(activity){
79 | check(activity, Object);
80 | check(activity.published, Date);
81 |
82 | Activities.update(activity._id, {$set: {fanout: 'in_progress'}});
83 |
84 | switch(activity.type){
85 | case 'Favorite':
86 | fanToObjectAuthor(activity);
87 | break;
88 | case 'Follow':
89 | fanToObject(activity);
90 | sendFollowedYouEmail(activity.object.id, activity.actor.id);
91 | break;
92 | case 'FollowBack':
93 | fanToObject(activity);
94 | sendFollowedYouBackEmail(activity.object.id, activity.actor.id);
95 | break;
96 | case 'Publish':
97 | var author = Meteor.users.findOne(activity.actor.id, {fields: {followers: 1}}); // fan to followers
98 | if(author.followers && author.followers.length){
99 | _.each(author.followers, function(follower){
100 | generateActivityFeedItem(follower, activity._id, activity.published);
101 | });
102 | sendFollowingPublishedEmail(author.followers, activity.object.id);
103 | }
104 | break;
105 | case 'Share':
106 | fanToObjectAuthor(activity);
107 | break;
108 | case 'ViewThreshold':
109 | fanToObjectAuthor(activity);
110 | break;
111 | default:
112 | throw new Error('Activity type not matched for activity: ' + activity._id + ' Type: ' + activity.type);
113 | }
114 |
115 | // if get here, nothing has thrown
116 | return Activities.update(activity._id, {$set: {fanout: 'done'}});
117 | };
118 |
--------------------------------------------------------------------------------
/server/browser-policy.js:
--------------------------------------------------------------------------------
1 | BrowserPolicy.framing.allowAll(); // allow all sites to embed FOLD
2 | BrowserPolicy.content.disallowInlineScripts(); // this provides a backstop against XSS
3 | BrowserPolicy.content.disallowEval(); // never allow eval
4 | BrowserPolicy.content.allowInlineStyles(); // we use inline styles a fair bit
5 | BrowserPolicy.content.allowImageOrigin('*'); // allowing all images is easiest and seems safe
6 |
7 | // allow videos from specific sources only
8 | BrowserPolicy.content.allowMediaOrigin('res.cloudinary.com');
9 | BrowserPolicy.content.allowMediaOrigin('*.imgur.com');
10 | BrowserPolicy.content.allowMediaOrigin('*.giphy.com');
11 |
12 | // allow iframes from everywhere (needed for various browser bookmarklets)
13 | BrowserPolicy.content.allowFrameOrigin('*');
14 |
15 | // allow iframes from specific sources only (why not)
16 | BrowserPolicy.content.allowFontOrigin('*.gstatic.com');
17 | BrowserPolicy.content.allowFontOrigin('*.bootstrapcdn.com');
18 |
19 | // allow scripts from everywhere (we already don't allow inline above)
20 | BrowserPolicy.content.allowScriptOrigin('*');
21 |
22 | // allow styles from specific sources only
23 | BrowserPolicy.content.allowStyleOrigin('*.bootstrapcdn.com');
24 |
25 | // disallow objects (until we need them)
26 | BrowserPolicy.content.disallowObject();
27 |
28 | // allow connect everywhere
29 | BrowserPolicy.content.allowConnectOrigin('*');
30 |
--------------------------------------------------------------------------------
/server/email.js:
--------------------------------------------------------------------------------
1 | sendWelcomeEmail = function(user){ // this takes actual user instead of userId because user might be in process of being created in db
2 | var email = user.emails[0].address;
3 | var emailName = user.profile.name;
4 |
5 | Mandrill.messages.sendTemplate({
6 | template_name: 'welcome-e-mail',
7 | template_content: [
8 | ],
9 | message: {
10 | to: [
11 | {
12 | email: email,
13 | name: emailName
14 | }
15 | ]
16 | }
17 | });
18 | };
19 |
20 | var emailTypeForUnsubscribe = function(emailType){
21 | switch(emailType){
22 | case 'followed-you-back':
23 | return 'followed-you' // these are effectively the same
24 | break;
25 | default:
26 | return emailType;
27 | }
28 | };
29 |
30 | var getToFromUserIds = function(userIds, emailType){
31 | var unsubscribeCheck = emailTypeForUnsubscribe(emailType);
32 | var users = Meteor.users.find({_id: {$in: userIds}, unsubscribes: {$ne: unsubscribeCheck}}, {fields: {'emails': 1, 'profile.name': 1}});
33 | return users.map(function(user){
34 | return {
35 | email: user.emails[0].address,
36 | name: user.profile.name
37 | }
38 | });
39 | };
40 |
41 | var getMergeVarsFromObj = function(obj){
42 | return _.chain(obj)
43 | .pairs()
44 | .map(function(pair){
45 | return {
46 | name: pair[0],
47 | content: pair[1]
48 | }
49 | })
50 | .value()
51 | }
52 |
53 | var sendEmail = function(emailType, userIds, subject, bareMergeVars){
54 | var to = getToFromUserIds(userIds, emailType);
55 | if(to.length === 0){
56 | return
57 | }
58 |
59 | if(process.env.NODE_ENV === 'production'){
60 | Mandrill.messages.sendTemplate({
61 | template_name: emailType,
62 | template_content: [
63 | ],
64 | message: {
65 | to: to,
66 | subject: subject,
67 | global_merge_vars: getMergeVarsFromObj(_.extend({ unsubscribeUrl: Meteor.absoluteUrl('unsubscribe?email_type=' + emailTypeForUnsubscribe(emailType))}, bareMergeVars))
68 | },
69 | preserve_recipients: false
70 | });
71 | } else {
72 | console.log('Would have sent email')
73 | console.log(arguments)
74 | }
75 | }
76 |
77 |
78 | sendFollowingPublishedEmail = function(userIds, storyId){
79 | var story = Stories.findOne(storyId, {fields: readStoryFields});
80 |
81 | var title = story.title;
82 | var authorName = story.authorName;
83 | var longContentPreview = story.contentPreview();
84 | var subject = authorName + ' just published "' + title + '" on FOLD';
85 |
86 | var bareMergeVars = {};
87 |
88 | bareMergeVars.title = title;
89 | bareMergeVars.authorName = authorName;
90 | bareMergeVars.subject = subject;
91 |
92 | bareMergeVars.headerImageUrl = 'https:' + story.headerImageUrl();
93 | if(longContentPreview){
94 | bareMergeVars.contentPreview = longContentPreview.length > 203 ? longContentPreview.substring(0, 200).replace(/\s+\S*$/, "...") : longContentPreview;
95 | }
96 | bareMergeVars.profileUrl = Meteor.absoluteUrl('profile/' + (story.authorDisplayUsername || story.authorUsername));
97 | bareMergeVars.storyUrl = Meteor.absoluteUrl('read/' + story.userPathSegment + '/' + story.storyPathSegment);
98 |
99 | sendEmail('following-published', userIds, subject, bareMergeVars);
100 | };
101 |
102 | sendFollowedYouEmail = function(userId, followingUserId){
103 | var followingUser = Meteor.users.findOne(followingUserId, {fields: {'profile.name': 1,'profile.bio': 1,'profile.profilePicture': 1, 'displayUsername': 1, 'services.twitter.id': 1}});
104 |
105 | var fullName = followingUser.profile.name; // = story.authorName;
106 | var username = followingUser.displayUsername; // = story.authorName;
107 | var subject = fullName + ' (' + username + ') just followed you on FOLD';
108 |
109 | var bareMergeVars = {};
110 |
111 | bareMergeVars.fullName = fullName;
112 | bareMergeVars.subject = subject;
113 | bareMergeVars.bio = followingUser.profile.bio || '';
114 | bareMergeVars.firstName = fullName.split(' ')[0];
115 | bareMergeVars.profilePicUrl = 'https:' + getProfileImage(followingUser.profile.profilePicture, (followingUser.services && followingUser.services.twitter) ? followingUser.services.twitter.id : null, 'large', true);
116 | bareMergeVars.profileUrl = Meteor.absoluteUrl('profile/' + followingUser.displayUsername);
117 |
118 |
119 | sendEmail('followed-you', [userId], subject, bareMergeVars);
120 |
121 | };
122 |
123 | sendFollowedYouBackEmail = function(userId, followingUserId){
124 | var followingUser = Meteor.users.findOne(followingUserId, {fields: {'profile.name': 1,'profile.bio': 1,'profile.profilePicture': 1, 'displayUsername': 1, 'services.twitter.id': 1}});
125 |
126 | var fullName = followingUser.profile.name; // = story.authorName;
127 | var username = followingUser.displayUsername; // = story.authorName;
128 | var subject = fullName + ' (' + username + ') just followed you back on FOLD';
129 |
130 | var bareMergeVars = {};
131 |
132 | bareMergeVars.fullName = fullName;
133 | bareMergeVars.subject = subject;
134 | bareMergeVars.bio = followingUser.profile.bio || '';
135 | bareMergeVars.firstName = fullName.split(' ')[0];
136 | bareMergeVars.profilePicUrl = 'https:' + getProfileImage(followingUser.profile.profilePicture, (followingUser.services && followingUser.services.twitter) ? followingUser.services.twitter.id : null, 'large', true);
137 | bareMergeVars.profileUrl = Meteor.absoluteUrl('profile/' + followingUser.displayUsername);
138 |
139 |
140 | sendEmail('followed-you-back', [userId], subject, bareMergeVars);
141 |
142 | };
143 |
144 |
--------------------------------------------------------------------------------
/server/fanout.js:
--------------------------------------------------------------------------------
1 | var runFanout = function (options) {
2 | options = options || {};
3 | _.defaults(options, {logging: true});
4 | if(options.logging){
5 | console.log('Running fanout...');
6 | }
7 |
8 | var startTime = Date.now();
9 | var previousTimepoint = Date.now();
10 |
11 | var timeLogs = [];
12 |
13 | var pendingActivities;
14 |
15 | if (options.cleanup) {
16 | pendingActivities = Activities.find({fanout: "in_progress"}); // find partially fanned out activities
17 | pendingActivities.forEach(function(activity){
18 | ActivityFeedItems.remove({aId: activity._id}); // remove the related feed items
19 | });
20 | // then try to fan them out again
21 |
22 | timeLogs.push('in progress activities fetch and activity feed cleanup time: ' + ((Date.now() - previousTimepoint) / 1000) + ' seconds');
23 | previousTimepoint = Date.now();
24 | } else {
25 | pendingActivities = Activities.find({fanout: "pending"}); // this is the default
26 | }
27 |
28 | timeLogs.push('pending activities fetch time: ' + ((Date.now() - previousTimepoint) / 1000) + ' seconds');
29 | previousTimepoint = Date.now();
30 |
31 | pendingActivities.forEach(fanoutActivity);
32 | timeLogs.push('activity fanout time: ' + ((Date.now() - previousTimepoint) / 1000) + ' seconds');
33 | previousTimepoint = Date.now();
34 |
35 | if(options.logging) {
36 | _.each(timeLogs, function (str) {
37 | console.log(str);
38 | });
39 |
40 | console.log('Total time to run fanout: ' + ((Date.now() - startTime) / 1000) + ' seconds');
41 | }
42 |
43 | };
44 |
45 |
46 | var fanOutWaitInSeconds = parseInt(process.env.FANOUT_WAIT) || 5 * 60; // default is every 5 minutes
47 |
48 |
49 | if (process.env.PROCESS_TYPE === 'fanout_worker') { // if a worker process
50 | Meteor.startup(function () {
51 | while (true) {
52 | runFanout();
53 | Meteor._sleepForMs(fanOutWaitInSeconds * 1000);
54 | }
55 | });
56 | } else if (process.env.PROCESS_TYPE === 'cleanup_fanout_worker') { // don't run this while fanout worker is running
57 | Meteor.startup(function () {
58 | runFanout({cleanup: true});
59 | process.exit();
60 | });
61 | } else if (process.env.NODE_ENV === 'development') { // however, in developement, run fanout more quickly
62 | Meteor.startup(function () {
63 | var backgroundFanout = function(){
64 | Meteor.setTimeout(function(){
65 | runFanout({logging: false});
66 | backgroundFanout();
67 | }, 1000);
68 | };
69 | backgroundFanout();
70 | });
71 | }
72 |
--------------------------------------------------------------------------------
/server/index.js:
--------------------------------------------------------------------------------
1 | import prerenderIO from 'prerender-node';
2 |
3 | // If use ssl, will need to check that too
4 | // TO-DO change this to a 301 redirect once totally sure
5 | WebApp.connectHandlers.use(function(req, res, next) {
6 | if (req.method === 'GET' && req.headers.host.match(/^www/) !== null ) {
7 | res.writeHead(307, {Location: 'https://' + req.headers.host.replace(/^www\./, '') + req.url});
8 | res.end();
9 | } else {
10 | next();
11 | }
12 | });
13 |
14 | if (_.contains([true, 'true'], process.env.ALLOW_BOTS)){
15 | robots.addLine('User-agent: *\nDisallow: /create/');
16 | robots.addLine('Disallow: /admin/');
17 | } else {
18 | robots.addLine('User-agent: *\nDisallow: /');
19 | }
20 |
21 | WebApp.connectHandlers.use(prerenderIO);
22 |
23 | // if (process.env.PRERENDER_TOKEN) {
24 | // prerenderio.set('prerenderToken', process.env.PRERENDER_TOKEN);
25 | // }
26 |
--------------------------------------------------------------------------------
/server/methods.js:
--------------------------------------------------------------------------------
1 | var countStat = function(storyId, stat, details) {
2 |
3 | var connectionId = this.connection.id;
4 | var clientIP = this.connection.httpHeaders['x-forwarded-for'] || this.connection.clientAddress;
5 |
6 | var story = Stories.findOne({_id: storyId, published: true});
7 |
8 | if (!story){
9 | throw new Meteor.error('Story not found for count ' + stat + ': ' + storyId); // this mostly confirms the story has been published
10 | }
11 |
12 | var stats = StoryStats.findOne({storyId: storyId}, {fields: {all: 0}});
13 |
14 | if(!stats){
15 | stats = {};
16 | }
17 |
18 | if (!stats.deepAnalytics){
19 | stats.deepAnalytics= {};
20 | }
21 |
22 | if (!stats.deepAnalytics[stat]){
23 | stats.deepAnalytics[stat] = {};
24 | }
25 |
26 | var addToSet = {};
27 | var inc = {};
28 | inc['analytics.' + stat + '.total'] = 1;
29 |
30 | if(!_.contains(stats.deepAnalytics[stat].uniqueViewersByConnection, connectionId)){
31 | addToSet['deepAnalytics.' + stat + '.uniqueViewersByConnection'] = connectionId ;
32 | inc['analytics.' + stat + '.byConnection'] = 1;
33 | if(stat === 'shares'){
34 | generateShareActivity(story._id, details.service);
35 | }
36 | }
37 |
38 | if(!_.contains(stats.deepAnalytics[stat].uniqueViewersByIP, clientIP)){
39 | addToSet['deepAnalytics.' + stat + '.uniqueViewersByIP'] = clientIP ;
40 | inc['analytics.' + stat + '.byIP'] = 1;
41 | if((stat === 'views') && stats.analytics && stats.analytics.views){
42 | var uniqueViews = stats.analytics.views.byIP + 1;
43 | if(_.contains(VIEW_THRESHOLDS, uniqueViews)){
44 | generateViewThresholdActivity(story._id, uniqueViews);
45 | }
46 | }
47 | }
48 |
49 | if (this.userId && !_.contains(stats.deepAnalytics[stat].uniqueViewersByUserId, this.userId)){
50 | addToSet['deepAnalytics.' + stat + '.uniqueViewersByUserId'] = this.userId ;
51 | inc['analytics.' + stat + '.byId'] = 1;
52 | }
53 |
54 | var push = {};
55 |
56 | var fullData = _.extend({}, _.omit(this.connection, ['close', 'onClose']), {date: new Date});
57 |
58 | if (this.userId){
59 | _.extend(fullData, {
60 | userId: this.userId,
61 | username: Meteor.user().username
62 | });
63 | };
64 | if (details){
65 | _.extend(fullData, details);
66 | };
67 |
68 | push['deepAnalytics.' + stat + '.all'] = fullData;
69 |
70 | Stories.update( {_id: storyId}, {$inc: inc });
71 | StoryStats.upsert( {storyId: storyId} , {$inc: inc, $addToSet: addToSet, $push: push} );
72 | };
73 |
74 | var checkCountMap = function(countMap){
75 | check(countMap, Object);
76 | _.keys(countMap, function (e) {
77 | check(e, String); // these should be ids
78 | check(e, Match.Where(function (str) {
79 | return (/^[^.]*$/).test(str); // check has no periods
80 | }))
81 | });
82 | _.values(countMap, function (e) {
83 | check(e, Number);
84 | check(e, Match.Where(function (num) {
85 | return num > 0; // check only positive numbers
86 | }));
87 | });
88 | };
89 |
90 | Meteor.methods({
91 | countStoryView: function(storyId) {
92 | this.unblock();
93 | check(storyId, String);
94 | countStat.call(this, storyId, 'views');
95 | },
96 | countStoryShare: function(storyId, service) {
97 | this.unblock();
98 | check(storyId, String);
99 | countStat.call(this, storyId, 'shares', {service: service});
100 | },
101 | countStoryRead: function(storyId, service) {
102 | this.unblock();
103 | check(storyId, String);
104 | countStat.call(this, storyId, 'reads', {service: service});
105 | },
106 | countStoryAnalytics: function(storyId, analytics) {
107 | this.unblock();
108 | check(storyId, String);
109 |
110 | var activeHeartbeatCountMap = analytics.activeHeartbeats;
111 | checkCountMap(activeHeartbeatCountMap);
112 |
113 | var anchorClickCountMap = analytics.anchorClicks;
114 | checkCountMap(anchorClickCountMap);
115 | var maxClicks = Math.ceil(activeHeartbeatCountMap.story / 5);
116 | _.keys(anchorClickCountMap, (k) => {
117 | anchorClickCountMap[k] = Math.min(anchorClickCountMap[k], maxClicks)
118 | });
119 |
120 | var contextInteractionCountMap = analytics.contextInteractions;
121 | checkCountMap(contextInteractionCountMap);
122 | var maxInteractions = Math.ceil(activeHeartbeatCountMap.story / 10);
123 | _.keys(contextInteractionCountMap, (k) => {
124 | contextInteractionCountMap[k] = Math.min(contextInteractionCountMap[k], maxInteractions)
125 | });
126 |
127 | var incMap = {};
128 | _.each(_.keys(activeHeartbeatCountMap), function (k) {
129 | incMap['analytics.heartbeats.active.' + k] = activeHeartbeatCountMap[k];
130 | });
131 | if(!_.isEmpty(anchorClickCountMap)){
132 | _.each(_.keys(anchorClickCountMap), function (k) {
133 | incMap['analytics.anchorClicks.' + k] = anchorClickCountMap[k];
134 | });
135 | }
136 | if(!_.isEmpty(contextInteractionCountMap)){
137 | _.each(_.keys(contextInteractionCountMap), function (k) {
138 | incMap['analytics.contextInteractions.' + k] = contextInteractionCountMap[k];
139 | });
140 | }
141 |
142 | StoryStats.upsert({storyId: storyId}, {$inc: incMap});
143 | return Stories.update({_id: storyId}, {$inc: incMap});
144 | },
145 | impersonate: function(username) {
146 | check(username, String);
147 |
148 | var user = Meteor.user();
149 | if (!user || !user.admin || !user.privileges || !user.privileges.impersonation){
150 | throw new Meteor.Error(403, 'Permission denied');
151 | }
152 |
153 | var otherUser;
154 | if (!(otherUser = Meteor.users.findOne({username: username}))){
155 | throw new Meteor.Error(404, 'User not found');
156 | }
157 |
158 | this.setUserId(otherUser._id);
159 | return otherUser._id
160 | },
161 | getActivityFeed: function(aId){
162 | check(aId, Match.Optional(String));
163 | if(!this.userId){
164 | throw new Meteor.Error("Only users may get their activity feed");
165 | }
166 |
167 | var query = aId ? {uId: this.userId, aId: aId} : {uId: this.userId};
168 |
169 | var activityIds = ActivityFeedItems.find(query, {sort:{r: -1}, limit: 50, fields: {'aId' : 1}}).map(function(i){return i.aId});
170 | return Activities.find({_id: {$in: activityIds}}).fetch();
171 | }
172 | });
173 |
--------------------------------------------------------------------------------
/server/search.js:
--------------------------------------------------------------------------------
1 | SearchSource.defineSource('stories', function(searchText, options) {
2 | options = options || {};
3 | _.defaults(options, {
4 | page: 0
5 | });
6 | var findOptions = {
7 | sort: [
8 | ["editorsPickAt", "desc"],
9 | ["favoritedTotal", "desc"],
10 | ["savedAt", "desc"]
11 | ],
12 | limit: PUB_SIZE * (options.page + 1),
13 | fields: previewStoryFields
14 | };
15 |
16 | if(searchText) {
17 | var regExp = buildRegExp(searchText);
18 | var selector = {$or: [{title: regExp},{ keywords: regExp},{ authorName: regExp},{ authorDisplayUsername: regExp}],
19 | published: true
20 | };
21 | return Stories.find(selector, findOptions).fetch();
22 | } else {
23 | return []
24 | }
25 | });
26 |
27 | SearchSource.defineSource('people', function(searchText, options) {
28 | options = options || {};
29 | _.defaults(options, {
30 | page: 0
31 | });
32 | var findOptions = {
33 | sort: [
34 | ["followersTotal", "desc"],
35 | ["followingTotal", "desc"],
36 | ["favoritesTotal", "desc"],
37 | ["createdAt", "desc"]
38 | ],
39 | limit: 3 * (options.page + 1),
40 | fields: minimalUserFields
41 | };
42 |
43 | if(searchText) {
44 | var regExp = buildRegExp(searchText);
45 | var selector = {
46 | username: {$exists: true},
47 | $or: [{username: regExp},{ 'profile.name': regExp}]
48 | };
49 | return Meteor.users.find(selector, findOptions).fetch();
50 | } else {
51 | return []
52 | }
53 | });
54 |
55 | function buildRegExp(searchText) {
56 | var words = searchText.trim().split(/[ \-\:]+/);
57 | var exps = _.map(words, function(word) {
58 | return "(?=.*" + word + ")";
59 | });
60 | var fullExp = exps.join('') + ".+";
61 | return new RegExp(fullExp, "i");
62 | }
63 |
--------------------------------------------------------------------------------
/server/settings.js:
--------------------------------------------------------------------------------
1 | // get segment key to the client, while allowing it to be set from environment variable
2 | // NOTE: this hack may not be 100% reliable (for ex when initially deploy won't update clients)
3 | if (process.env.GA_TRACKING_KEY){
4 | Meteor.settings['public'].GA_TRACKING_KEY = process.env.GA_TRACKING_KEY;
5 | }
6 |
7 | if (process.env.NODE_ENV){
8 | Meteor.settings['public'].NODE_ENV = process.env.NODE_ENV;
9 | }
10 |
11 | // SMTP Config
12 | smtp = {
13 | username: Meteor.settings.SMTP_USERNAME,
14 | password: Meteor.settings.SMTP_API_KEY,
15 | server: Meteor.settings.SMTP_SERVER,
16 | port: Meteor.settings.SMTP_PORT
17 | };
18 |
19 | process.env.MAIL_URL = 'smtp://' + encodeURIComponent(smtp.username) + ':' + encodeURIComponent(smtp.password) + '@' + encodeURIComponent(smtp.server) + ':' + smtp.port;
20 |
21 | Mandrill.config({
22 | key: Meteor.settings.MANDRILL_API_KEY // get your Mandrill key from https://mandrillapp.com/settings/index
23 | });
24 |
25 | if (Meteor.settings.CLOUDINARY_API_SECRET){
26 | Cloudinary.config({
27 | cloud_name: Meteor.settings['public'].CLOUDINARY_CLOUD_NAME,
28 | api_key: Meteor.settings.CLOUDINARY_API_KEY,
29 | api_secret: Meteor.settings.CLOUDINARY_API_SECRET
30 | });
31 | };
32 |
--------------------------------------------------------------------------------
/server/user-methods.js:
--------------------------------------------------------------------------------
1 | var TWITTER_API_KEY = process.env.TWITTER_API_KEY || Meteor.settings.TWITTER_API_KEY;
2 | var TWITTER_API_SECRET = process.env.TWITTER_API_SECRET || Meteor.settings.TWITTER_API_SECRET;
3 |
4 | import Twit from 'twit';
5 |
6 | var makeTwitterCall = function (apiCall, params) {
7 | var res;
8 | var user = Meteor.user();
9 | var client = new Twit({
10 | consumer_key: TWITTER_API_KEY,
11 | consumer_secret: TWITTER_API_SECRET,
12 | access_token: user.services.twitter.accessToken,
13 | access_token_secret: user.services.twitter.accessTokenSecret
14 | });
15 |
16 | var twitterResultsSync = Meteor.wrapAsync(client.get, client);
17 | try {
18 | res = twitterResultsSync(apiCall, params);
19 | }
20 | catch (err) {
21 | if (err.statusCode !== 404) {
22 | throw err;
23 | }
24 | res = {};
25 | }
26 | return res;
27 | };
28 |
29 | Meteor.methods({
30 | updateInitialTwitterUserInfo: function (userInfo) {
31 | check(userInfo, Object);
32 |
33 | var user = Meteor.user();
34 | if (!user.tempUsername) {
35 | return
36 | }
37 | var username = userInfo.username,
38 | email = userInfo.email;
39 |
40 | if (!email) {
41 | throw new Meteor.Error('Please enter your email');
42 | }
43 | check(username, String);
44 | check(email, String);
45 |
46 |
47 | checkUserSignup(username, email);
48 |
49 | //get twitter info
50 | var res;
51 | if (user.services.twitter) {
52 | var twitterParams = {
53 | user_id: user.services.twitter.id
54 | };
55 | try {
56 | res = makeTwitterCall("users/show", twitterParams);
57 | }
58 | catch (err) {
59 | res = {};
60 | }
61 | }
62 |
63 | var bio = (res && res.description) ? res.description : "";
64 |
65 | var success = Meteor.users.update({
66 | _id: this.userId
67 | }, {
68 | $set: {
69 | "profile.name": userInfo.name || username,
70 | "displayUsername": username,
71 | "username": username,
72 | "profile.bio": bio
73 | },
74 | $unset: {"tempUsername": ""},
75 | $push: {
76 | "emails": {"address": userInfo.email, "verified": false}
77 | }
78 | });
79 |
80 | if(success){
81 | Meteor.defer(() => {
82 | sendWelcomeEmail(Meteor.users.findOne(this.userId));
83 | });
84 | }
85 |
86 | return success
87 | },
88 | setBioFromTwitter: function () {
89 | var user = Meteor.user();
90 | if (user && user.profile && user.services.twitter) {
91 | var res;
92 | var twitterParams = {
93 | user_id: user.services.twitter.id
94 | };
95 | res = makeTwitterCall("users/show", twitterParams);
96 |
97 | var bio = res.description;
98 |
99 | if (bio) {
100 | return Meteor.users.update({
101 | _id: this.userId
102 | }, {
103 | $set: {
104 | "profile.bio": bio
105 | }
106 | });
107 | }
108 | }
109 | },
110 | validateUserInfo: function(userInfo){
111 | check(userInfo.email, String);
112 | userInfo.emails = [{address: userInfo.email}];
113 | return validateNewUser(userInfo);
114 | },
115 | unsubscribe (emailType){
116 | check(emailType, String);
117 | return Meteor.users.update({
118 | _id: this.userId
119 | }, {
120 | $addToSet: {
121 | "unsubscribes": emailType
122 | }
123 | });
124 | },
125 | resubscribe (emailType){
126 | check(emailType, String);
127 | return Meteor.users.update({
128 | _id: this.userId
129 | }, {
130 | $pull: {
131 | "unsubscribes": emailType
132 | }
133 | });
134 | }
135 | });
136 |
--------------------------------------------------------------------------------
/start:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | meteor --settings settings.json
3 | exit
4 |
--------------------------------------------------------------------------------