├── .meteor ├── .finished-upgraders ├── .gitignore ├── .id ├── packages ├── platforms ├── release └── versions ├── README.md ├── thebutton.css ├── thebutton.html ├── thebutton.js └── thebutton.png /.meteor/.finished-upgraders: -------------------------------------------------------------------------------- 1 | # This file contains information which helps Meteor properly upgrade your 2 | # app when you run 'meteor update'. You should check it into version control 3 | # with your project. 4 | 5 | notices-for-0.9.0 6 | notices-for-0.9.1 7 | 0.9.4-platform-file 8 | notices-for-facebook-graph-api-2 9 | -------------------------------------------------------------------------------- /.meteor/.gitignore: -------------------------------------------------------------------------------- 1 | local 2 | -------------------------------------------------------------------------------- /.meteor/.id: -------------------------------------------------------------------------------- 1 | # This file contains a token that is unique to your project. 2 | # Check it into your repository along with the rest of this directory. 3 | # It can be used for purposes such as: 4 | # - ensuring you don't accidentally deploy one app on top of another 5 | # - providing package authors with aggregated statistics 6 | 7 | ali0ss1by812p14g4kz6 8 | -------------------------------------------------------------------------------- /.meteor/packages: -------------------------------------------------------------------------------- 1 | # Meteor packages used by this project, one per line. 2 | # Check this file (and the other files in this directory) into your repository. 3 | # 4 | # 'meteor add' and 'meteor remove' will edit this file for you, 5 | # but you can also edit it by hand. 6 | 7 | meteor-platform 8 | reactive-var 9 | accounts-ui 10 | accounts-password 11 | d3 12 | momentjs:moment 13 | -------------------------------------------------------------------------------- /.meteor/platforms: -------------------------------------------------------------------------------- 1 | server 2 | browser 3 | -------------------------------------------------------------------------------- /.meteor/release: -------------------------------------------------------------------------------- 1 | METEOR@1.1.0.2 2 | -------------------------------------------------------------------------------- /.meteor/versions: -------------------------------------------------------------------------------- 1 | accounts-base@1.2.0 2 | accounts-password@1.1.1 3 | accounts-ui@1.1.5 4 | accounts-ui-unstyled@1.1.7 5 | autoupdate@1.2.1 6 | base64@1.0.3 7 | binary-heap@1.0.3 8 | blaze@2.1.2 9 | blaze-tools@1.0.3 10 | boilerplate-generator@1.0.3 11 | callback-hook@1.0.3 12 | check@1.0.5 13 | d3@1.0.0 14 | ddp@1.1.0 15 | deps@1.0.7 16 | ejson@1.0.6 17 | email@1.0.6 18 | fastclick@1.0.3 19 | geojson-utils@1.0.3 20 | html-tools@1.0.4 21 | htmljs@1.0.4 22 | http@1.1.0 23 | id-map@1.0.3 24 | jquery@1.11.3_2 25 | json@1.0.3 26 | launch-screen@1.0.2 27 | less@1.0.14 28 | livedata@1.0.13 29 | localstorage@1.0.3 30 | logging@1.0.7 31 | meteor@1.1.6 32 | meteor-platform@1.2.2 33 | minifiers@1.1.5 34 | minimongo@1.0.8 35 | mobile-status-bar@1.0.3 36 | momentjs:moment@2.10.2 37 | mongo@1.1.0 38 | npm-bcrypt@0.7.8_2 39 | observe-sequence@1.0.6 40 | ordered-dict@1.0.3 41 | random@1.0.3 42 | reactive-dict@1.1.0 43 | reactive-var@1.0.5 44 | reload@1.1.3 45 | retry@1.0.3 46 | routepolicy@1.0.5 47 | service-configuration@1.0.4 48 | session@1.1.0 49 | sha@1.0.3 50 | spacebars@1.0.6 51 | spacebars-compiler@1.0.6 52 | srp@1.0.3 53 | templating@1.1.1 54 | tracker@1.0.7 55 | ui@1.0.6 56 | underscore@1.0.3 57 | url@1.0.4 58 | webapp@1.2.0 59 | webapp-hashing@1.0.3 60 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Build your own /r/thebutton 2 | 3 | ![A picture of the app](thebutton.png) 4 | 5 | This is the codebase accompanying the article [Build Your Own /r/thebutton](https://medium.com/@Rahul/build-your-own-r-thebutton-with-meteor-d4b878fbb0d2) and the [demo](http://thebutton.meteor.com). It's not intended as a full reproduction of Reddit's experiment, but as an educational exploration of what kind of approach one can use to build something like The Button. I plan to use this in Meteor workshops in the future. 6 | 7 | Leave me any feedback here in the issues or on [Twitter](http://twitter.com/rahul). 8 | 9 | # License 10 | 11 | WTFPL. 12 | -------------------------------------------------------------------------------- /thebutton.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-size: 14px; 3 | font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; 4 | font-weight: 200; 5 | margin: 3rem 2rem; 6 | line-height: 1.5; 7 | } 8 | 9 | nav { 10 | display: flex; 11 | align-items: center; 12 | justify-content: space-between; 13 | width: 600px; 14 | text-align: right; 15 | margin: 1rem auto; 16 | } 17 | 18 | .thebutton-container { 19 | display: flex; 20 | width: 600px; 21 | padding: 2rem; 22 | margin: 0 auto; 23 | background: #f9f9f9; 24 | border: solid 1px #ccc; 25 | box-shadow: 0 2px 5px rgba(0,0,0,.2); 26 | align-items: center; 27 | } 28 | 29 | .thebutton-container > div { 30 | flex: 1; 31 | text-align: center; 32 | } 33 | 34 | .thebutton-countdown { 35 | padding: 1rem 0; 36 | background: #eee; 37 | font-size: 24px; 38 | font-family: monospace; 39 | border-radius: 4px; 40 | box-shadow: inset 0 2px 1px rgba(0,0,0,.1); 41 | } 42 | 43 | .thebutton-countdown span { 44 | background: white; 45 | border-radius: 4px; 46 | padding: .4rem; 47 | padding-bottom: .2rem; 48 | box-shadow: inset 0 1px 2px rgba(0,0,0,.3); 49 | } 50 | .countdown-100ms, .countdown-10ms { color: #999;} 51 | 52 | span[class^='remaining-'] { 53 | color: white; 54 | padding: 2px 4px; 55 | border-radius: 4px; 56 | } 57 | span[class^='remaining-5'] { background: purple; } 58 | span[class^='remaining-4'] { background: brown; } 59 | span[class^='remaining-3'] { background: green; } 60 | span[class^='remaining-2'] { background: #ffd100; } 61 | span[class^='remaining-1'] { background: #69f; } 62 | span[class^='remaining-0'] { background: hotpink; } 63 | 64 | button { 65 | cursor: pointer; 66 | font-size: 14px; 67 | padding: 1.2rem; 68 | text-align: center; 69 | width: 150px; 70 | border-radius: 4px; 71 | border: solid 1px #69c; 72 | background: #3E9EFF -webkit-linear-gradient(top, #3E9EFF, #2276CA); 73 | color: #999; 74 | -webkit-font-smoothing: antialiased; 75 | box-shadow: 0 2px 2px rgba(0,0,0,.5), inset 0 -3px 0 rgba(0,0,0,.2), inset 0 1px 0 rgba(255,255,255,.2); 76 | -webkit-transition: ease .25s; 77 | } 78 | button:hover { 79 | border-color: #369; 80 | background: #3E9EFF; 81 | box-shadow: 0 1px 5px rgba(0,0,0,.5), inset 0 -3px 0 rgba(0,0,0,.2), inset 0 1px 0 rgba(255,255,255,.2); 82 | } 83 | button:focus { 84 | margin-top: 2px; 85 | outline: none; 86 | box-shadow: inset 0 1px rgba(0,0,0,.2); 87 | } 88 | button:disabled { 89 | background: #ccc; 90 | border-color: #bbb; 91 | cursor: default; 92 | box-shadow: none; 93 | } 94 | 95 | .thebutton-piechart-container { 96 | margin: 0 auto; 97 | width: 120px; 98 | height: 120px; 99 | border-radius: 50%; 100 | background: #eee; 101 | box-shadow: inset 0 2px 1px rgba(0,0,0,.1); 102 | } 103 | .thebutton-piechart { 104 | width: 120px; 105 | height: 120px; 106 | } 107 | 108 | .what-is-this { 109 | width: 600px; 110 | margin: 5rem auto 0 auto; 111 | text-align: center; 112 | } -------------------------------------------------------------------------------- /thebutton.html: -------------------------------------------------------------------------------- 1 | 2 | Build Your Own /r/thebutton 3 | 4 | 5 | 6 | 7 | 8 | 17 | {{> thebutton}} 18 | 19 |
20 |

How does this work? Read Build your own /r/thebutton

21 |

22 | Made by @Rahul with Meteor. 23 |

24 |

25 | 26 | 27 |

28 |
29 | 30 | 31 | 58 | 59 | 66 | 67 | 75 | -------------------------------------------------------------------------------- /thebutton.js: -------------------------------------------------------------------------------- 1 | Clicks = new Mongo.Collection("clicks"); 2 | Timer = new Mongo.Collection("timer"); 3 | var TIMER_INIT = 1000 * 60; 4 | 5 | if (Meteor.isClient) { 6 | 7 | // We'll use this to update the pie chart more 8 | // frequently (every 10ms) without hitting the server 9 | var clientTimer = new ReactiveVar(TIMER_INIT); 10 | 11 | Meteor.startup(function() { 12 | Meteor.subscribe("userData"); 13 | Meteor.subscribe("clicks"); 14 | Meteor.subscribe("timer"); 15 | 16 | var interval; 17 | Tracker.autorun(function() { 18 | var timer = Timer.findOne(); 19 | if (timer && timer.value > 0 && timer.value % 1000 == 0) { 20 | clearInterval(interval); 21 | clientTimer.set(timer.value); 22 | interval = Meteor.setInterval(function() { 23 | clientTimer.set(clientTimer.get() - 10); 24 | }, 10); 25 | } 26 | }); 27 | 28 | google.setOnLoadCallback(function() { 29 | var options = { 30 | backgroundColor: "transparent", 31 | pieSliceBorderColor: "transparent", 32 | slices: { 0: {color: "#C8C8C8"}, 1: {color: "#4A4A4A"} }, 33 | width: 120, 34 | height: 120, 35 | legend: 'none', 36 | pieSliceText: 'none', 37 | enableInteractivity: false 38 | }; 39 | 40 | var chart = new google.visualization.PieChart($(".thebutton-piechart")[0]); 41 | chart.draw(google.visualization.arrayToDataTable([ 42 | ['', ''], 43 | ["gone", TIMER_INIT - clientTimer.get()], 44 | ["remaining", clientTimer.get()] 45 | ]), options); 46 | 47 | Tracker.autorun(function() { 48 | if (clientTimer.get() <= 0) return; 49 | chart.draw(google.visualization.arrayToDataTable([ 50 | ['', ''], 51 | ["gone", TIMER_INIT - clientTimer.get()], 52 | ["remaining", clientTimer.get()] 53 | ]), options); 54 | }); 55 | }); 56 | }); 57 | 58 | Template.body.helpers({ 59 | numParticipants: function() { 60 | return Clicks.find().count(); 61 | } 62 | }); 63 | 64 | Template.thebutton.helpers({ 65 | timeRemainingWhenClicked: function() { 66 | var click = Clicks.findOne({userId: Meteor.userId()}); 67 | if (click) { 68 | var rem = Math.round(click.timeRemaining / 1000); 69 | if (rem < 10) 70 | rem = "0" + rem; 71 | return rem; 72 | } 73 | }, 74 | clicked: function() { 75 | if (!Meteor.userId()) return; 76 | return moment(Meteor.user().date).format("MMM DD \\a\\t HH:mm:ss"); 77 | }, 78 | timeRemaining: function() { 79 | if (!Timer.findOne()) return; 80 | return Timer.findOne().value > 0; 81 | } 82 | }); 83 | 84 | Template.thebutton.events({ 85 | 'click button': function(evt) { 86 | if (!Meteor.user()) return; 87 | var change = { timeRemaining: Timer.findOne().value, date: new Date() }; 88 | var userId = { userId: Meteor.userId() }; 89 | 90 | var click = Clicks.findOne(userId); 91 | if (!click) { 92 | Clicks.insert(_.extend(change, userId)); 93 | Meteor.users.update(Meteor.userId(), {$set: change}); 94 | } 95 | } 96 | }); 97 | 98 | Template.countdown.helpers({ 99 | countdown60s: function() { 100 | if (!Timer.findOne()) return; 101 | var secs = clientTimer.get() / 1000; 102 | return secs >= 10 ? String(secs)[0] : 0; 103 | }, 104 | countdown10s: function() { 105 | if (!Timer.findOne()) return; 106 | var secs = clientTimer.get() / 1000; 107 | return secs >= 10 ? String(secs)[1] : String(secs)[0]; 108 | }, 109 | countdown100ms: function() { 110 | if (!Timer.findOne()) return; 111 | var ms = clientTimer.get() % 1000; 112 | return ms > 100 ? String(ms)[0] : 0; 113 | }, 114 | countdown10ms: function() { 115 | if (!Timer.findOne()) return; 116 | var ms = clientTimer.get() % 1000; 117 | return ms > 100 ? String(ms)[1] : String(ms)[0]; 118 | } 119 | }); 120 | 121 | } 122 | 123 | if (Meteor.isServer) { 124 | 125 | var serverTimer; 126 | 127 | Meteor.startup(function() { 128 | serverTimer = TIMER_INIT; 129 | var timer = Timer.findOne(); 130 | if (timer) { 131 | Meteor.setInterval(function() { 132 | Timer.update(timer._id, {$set: {value: serverTimer}}); 133 | serverTimer -= 1000; 134 | if (Timer.findOne().value < 0) { 135 | Timer.update(timer._id, {$set: {value: TIMER_INIT}}); 136 | serverTimer = TIMER_INIT; 137 | } 138 | }, 1000); 139 | } 140 | else 141 | Timer.insert({value: TIMER_INIT}); 142 | }); 143 | 144 | Meteor.methods({ 145 | reset: function() { 146 | var timer = Timer.findOne(); 147 | if (timer) { 148 | Timer.update(timer._id, {$set: {value: TIMER_INIT}}); 149 | serverTimer = TIMER_INIT; 150 | } 151 | } 152 | }); 153 | 154 | Meteor.publish("timer", function() { 155 | return Timer.find(); 156 | }); 157 | 158 | Meteor.publish("userData", function() { 159 | return Meteor.users.find({}, {fields: {date: 1}}); 160 | }); 161 | 162 | Meteor.users.allow({ 163 | update: function(userId, doc) { 164 | return Timer.findOne().value > 0 && userId === doc._id; 165 | } 166 | }); 167 | 168 | Meteor.publish("clicks", function() { 169 | return Clicks.find(); 170 | }); 171 | 172 | Clicks.allow({ 173 | insert: function(userId, doc) { 174 | if (serverTimer > 0 && userId === doc.userId) { 175 | var timer = Timer.findOne(); 176 | Timer.update(timer._id, {$set: {value: TIMER_INIT}}); 177 | serverTimer = TIMER_INIT; 178 | return true; 179 | } 180 | return false; 181 | } 182 | }) 183 | 184 | } -------------------------------------------------------------------------------- /thebutton.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Primigenus/thebutton/a8964cbdcba88d3ab8d238024126ed6b1603b201/thebutton.png --------------------------------------------------------------------------------