├── .dockerignore ├── .gitignore ├── Dockerfile ├── LICENSE ├── LocalTodoApp.js ├── Makefile ├── README.md ├── TodoApp.js ├── TodoAppServer.js ├── TodoRouter.js ├── css ├── add.css ├── base.css ├── bg.png ├── local.css └── touch.css ├── fake_auth_init.js ├── favicon.ico ├── local.html ├── model ├── TodoItem.js └── TodoList.js ├── package.json ├── run.sh ├── serv.js ├── todo.appcache └── view ├── Footer.jsx ├── Header.jsx ├── MainSection.jsx ├── TodoAppView.jsx ├── TodoItemView.jsx ├── TodoListView.jsx └── index.html /.dockerignore: -------------------------------------------------------------------------------- 1 | *.js 2 | *.html 3 | *.json 4 | *.sh 5 | *.ico 6 | *.md 7 | *.appcache 8 | view/* 9 | css/* 10 | model/* 11 | Makefile 12 | LICENSE 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .swarm 2 | dist 3 | node_modules 4 | frames.html 5 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node 2 | LABEL maintainer ian.miell@gmail.com 3 | RUN git clone https://github.com/docker-in-practice/todo.git 4 | WORKDIR todo 5 | RUN npm install 6 | RUN chmod -R 777 /todo 7 | EXPOSE 8000 8 | CMD ["npm","start"] 9 | 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012-2014 Victor Grishchenko, Citrea LLC 2 | Copyright (c) 2012-2014 Aleksei Balandin, Citrea LLC 3 | 4 | Permission is hereby granted, free of charge, to any person 5 | obtaining a copy of this software and associated documentation 6 | files (the "Software"), to deal in the Software without 7 | restriction, including without limitation the rights to use, 8 | copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the 10 | Software is furnished to do so, subject to the following 11 | conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 18 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 20 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 21 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 22 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 23 | OTHER DEALINGS IN THE SOFTWARE. 24 | -------------------------------------------------------------------------------- /LocalTodoApp.js: -------------------------------------------------------------------------------- 1 | var Swarm = require('swarm'); 2 | var Spec = Swarm.Spec; 3 | var TodoList = require('./model/TodoList'); 4 | var TodoItem = require('./model/TodoItem'); 5 | var TodoApp = require('./TodoApp'); 6 | 7 | module.exports = window.TodoApp = (function(superclass){ 8 | var defaultModels = []; 9 | // TODO: default english version 10 | defaultModels.push({text:'распределенное приложение как локальное', completed: location.hash !== "#focused"}); 11 | defaultModels.push({text:'всё очень быстро', completed: true}); 12 | defaultModels.push({text:'теперь доступно каждому!', completed: true}); 13 | 14 | var prototype = extend$((import$(S, superclass), S), superclass).prototype, constructor = S; 15 | 16 | function S(ssnid, itemId){ 17 | this.path = []; 18 | this.ssnid = ssnid; 19 | this.moving = false; 20 | 21 | this.initSwarm(); 22 | this.installListeners(); 23 | this.parseUri(); 24 | if (location.hash === '#focused') { 25 | this.selectItem(0); 26 | } 27 | } 28 | 29 | prototype.initSwarm = function () { 30 | this.storage = new Swarm.SharedWebStorage(); 31 | this.storage.authoritative = true; 32 | this.host = Swarm.env.localhost = 33 | new Swarm.Host(this.ssnid,'',this.storage); 34 | }; 35 | 36 | prototype.installListeners = function () { 37 | var self = this; 38 | document.addEventListener('keydown', function (ev) { 39 | switch (ev.keyCode) { 40 | // case 9: self.forward();break; // tab 41 | // case 27: self.back(); break; // esc 42 | case 40: self.down(); break; // down arrow 43 | case 38: self.up(); break; // up arrow 44 | case 45: self.toggle(); break; // insert 45 | case 13: self.create(); break; // enter 46 | //case 46: self.delete(); break; // delete 47 | default: return true; 48 | } 49 | ev.preventDefault(); 50 | return false; 51 | }); 52 | }; 53 | 54 | prototype.parseUri = function () { 55 | var hash = window.localStorage.getItem(".itemId"); 56 | var path = window.localStorage.getItem(".listId"); 57 | var idre = Spec.reQTokExt; 58 | idre.lastIndex = 0; 59 | var ml = idre.exec(path), listId = ml&&ml[2]; 60 | idre.lastIndex = 0; 61 | var mi = idre.exec(hash), itemId = mi&&mi[2]; 62 | if (!listId) { 63 | var list = new TodoList(); 64 | var item = new TodoItem(); 65 | list.addObject(item); 66 | listId = list._id; 67 | itemId = item._id; 68 | } 69 | this.forward(listId,itemId); 70 | }; 71 | 72 | prototype.forward = function (listId, itemId) { 73 | var self = this; 74 | var fwdList; 75 | if (!listId) { 76 | var item = this.getItem(); 77 | listId = item.childList; 78 | } 79 | if (!listId) { 80 | fwdList = new TodoList(); 81 | listId = fwdList._id; 82 | item.set({childList: listId}); 83 | } else { 84 | fwdList = this.host.get('/TodoList#'+listId); // TODO fn+id sig 85 | } 86 | // we may need to fetch the data from the server so we use a callback, yes 87 | fwdList.once('.init',function(){ 88 | if (!fwdList.length()) { 89 | defaultModels.forEach(function(i){ 90 | fwdList.addObject(new TodoItem(i)); 91 | }) 92 | } 93 | itemId = itemId || fwdList.objectAt(0)._id; 94 | window.localStorage.setItem(".itemId", "#" + itemId); 95 | window.localStorage.setItem(".listId", "/" + listId); 96 | self.path.push({ 97 | listId: listId, 98 | itemId: itemId 99 | }); 100 | self.refresh(); 101 | }); 102 | }; 103 | return S; 104 | }(TodoApp)); 105 | 106 | function extend$(sub, sup){ 107 | function fun(){} fun.prototype = (sub.superclass = sup).prototype; 108 | (sub.prototype = new fun).constructor = sub; 109 | if (typeof sup.extended == 'function') sup.extended(sub); 110 | return sub; 111 | } 112 | function import$(obj, src){ 113 | var own = {}.hasOwnProperty; 114 | for (var key in src) if (own.call(src, key)) obj[key] = src[key]; 115 | return obj; 116 | } 117 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | BIN = ./node_modules/.bin 2 | JSX_SOURCES = $(wildcard view/*.jsx) 3 | SOURCES = *.js model/*.js view/*.jsx 4 | TARGETS = dist/TodoApp.app.js dist/LocalTodoApp.app.js 5 | 6 | all: libs dist todo 7 | 8 | libs: 9 | npm install 10 | if [ ! -e dist/ ]; then mkdir dist; fi 11 | cp node_modules/react/dist/react.min.js dist/react.min.js 12 | 13 | dist: $(TARGETS) 14 | 15 | dist/%.app.js: $(SOURCES) 16 | @mkdir -p $(@D) 17 | @$(BIN)/browserify -x react -e $(patsubst dist/%.app.js,%.js,$@) -o $@ 18 | 19 | clean: 20 | @rm -rf dist 21 | 22 | todo: 23 | @echo 24 | @git grep -w --color -n 'TO\DO' 25 | @echo 26 | 27 | lint: 28 | $(BIN)/jshint $(SOURCES) 29 | 30 | 31 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TodoMVC app using React.js+Swarm.js combo 2 | 3 | [Forked from here](https://github.com/gritzko/swarm) 4 | 5 | and used to illustrate basic Docker concepts. 6 | 7 | -------------------------------------------------------------------------------- /TodoApp.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2013-2014 Victor Grishchenko 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | * @jsx React.DOM 17 | */ 18 | 19 | var React = require('react'); 20 | var Swarm = require('swarm'); 21 | var Spec = Swarm.Spec; 22 | 23 | var TodoRouter = require('./TodoRouter'); 24 | 25 | var TodoList = require('./model/TodoList'); 26 | var TodoItem = require('./model/TodoItem'); 27 | 28 | Swarm.env.debug = false; 29 | var isEmbed = (window.parent!==window); 30 | 31 | var TodoAppView = require('./view/TodoAppView.jsx'); 32 | 33 | function TodoApp (ssnid, listId) { 34 | this.path = []; 35 | this.active = false; 36 | this.ssnid = ssnid; 37 | 38 | this.router = new TodoRouter(); 39 | this.refreshBound = this.refresh.bind(this); 40 | this.initSwarm(); 41 | this.parseUri(); 42 | this.isTouch = ('ontouchstart' in window) 43 | || (navigator.MaxTouchPoints > 0) 44 | || (navigator.msMaxTouchPoints > 0); 45 | } 46 | 47 | TodoApp.prototype.initSwarm = function () { 48 | //this.storage = null; 49 | this.storage = new Swarm.SharedWebStorage('webst',{persistent:true}); 50 | this.wsServerUri = 'ws://'+window.location.host; 51 | this.host = Swarm.env.localhost = new Swarm.Host(this.ssnid,'',this.storage); 52 | this.host.connect(this.wsServerUri, {delay: 50}); 53 | }; 54 | 55 | TodoApp.prototype.parseUri = function () { 56 | var route = window.location.pathname + window.location.hash; 57 | this.router.load(route, this.refreshBound); 58 | }; 59 | 60 | TodoApp.prototype.installListeners = function () { 61 | var self = this; 62 | document.addEventListener('keydown', function (ev) { 63 | switch (ev.keyCode) { 64 | case 9: ev.shiftKey ? self.back(1) : self.forward(); break; // [shift+]tab 65 | case 27: self.back(1); break; // esc 66 | case 40: self.down(); break; // down arrow 67 | case 38: self.up(); break; // up arrow 68 | case 45: self.toggle(); break; // insert 69 | case 107: // numpad plus 70 | case 13: self.create(); break; // enter 71 | case 109:self.delete(); break; // numpad minus 72 | default: return true; 73 | } 74 | ev.preventDefault(); 75 | return false; 76 | }); 77 | }; 78 | 79 | TodoApp.prototype.getItem = function (itemId) { 80 | if (!itemId) { 81 | var state = this.path[this.path.length-1]; 82 | itemId = state.itemId; 83 | } 84 | return this.host.get('/TodoItem#'+itemId); 85 | }; 86 | 87 | TodoApp.prototype.getList = function (listId) { 88 | if (!listId) { 89 | var state = this.path[this.path.length-1]; 90 | listId = state.listId; 91 | } 92 | return this.host.get('/TodoList#'+listId); 93 | }; 94 | 95 | TodoApp.prototype.refresh = function (path) { 96 | var self = this; 97 | self.path = path || self.path; 98 | if (!self.active) { 99 | self.installListeners(); 100 | self.active = true; 101 | } 102 | // rerender DOM 103 | React.renderComponent( 104 | TodoAppView ({ 105 | key: 'TodoApp', 106 | app: self 107 | }), 108 | document.getElementById('todoapp') 109 | ); 110 | // recover focus 111 | var item = this.getItem(); 112 | var edit = document.getElementById(item._id); 113 | if (edit) { 114 | edit.focus(); 115 | // safari text select fix 116 | edit.value = edit.value; 117 | // TODO scroll into view 118 | } 119 | // set URI 120 | var route = this.router.buildRoute(this.path); 121 | var newLink = window.location.origin + route; 122 | window.history.replaceState({},"",newLink); 123 | if (isEmbed) { 124 | var link = document.getElementById("self"); 125 | link.setAttribute('href', newLink); 126 | link.innerHTML = 'link'; 127 | } 128 | }; 129 | 130 | // Suddenly jump to some entry in some list. 131 | // Invoked by onClick and onHashChange 132 | TodoApp.prototype.go = function (listId, itemId) { 133 | // find in history 134 | var path = this.path; 135 | var backSteps = 0; 136 | while (backSteps < path.length && path[path.length-backSteps-1].listId !== listId) { 137 | backSteps++; 138 | } 139 | this.back(backSteps); 140 | this.selectItem(itemId); 141 | }; 142 | 143 | TodoApp.prototype.back = function (steps) { 144 | if (this.path.length <= 1) return; 145 | 146 | for (var i = 0; i < steps && i < this.path.length; i++) { 147 | this.path.pop(); 148 | } 149 | this.refresh(); 150 | }; 151 | 152 | /** Go deeper into child lists (may create them if necessary). 153 | * itemIds must be an id (selected entry) or an Array of ids 154 | * (chain of selections) or falsy (1st item on the list) 155 | * */ 156 | TodoApp.prototype.forward = function (listId, itemIds) { 157 | var fwdList; 158 | if (!listId) { // default Tab behavior 159 | var item = this.getItem(); 160 | listId = item.childList; 161 | } 162 | if (!listId) { // create a new list if none exists 163 | fwdList = new TodoList(); 164 | listId = fwdList._id; 165 | item.set({childList: listId}); 166 | } 167 | 168 | this.router.addPathItem(listId, itemIds, this.path, this.refreshBound); 169 | }; 170 | 171 | TodoApp.prototype.selectItem = function (itemId) { 172 | var list = this.getList(); 173 | if (itemId.constructor===Number) { 174 | var i = itemId; 175 | if (i<0) { i=0; } 176 | if (i>=list.length()) { i=list.length()-1; } // TODO .length 177 | itemId = i>=0 ? list.objectAt(i)._id : ''; 178 | } if (itemId._id) { 179 | itemId = itemId._id; 180 | } 181 | var state = this.path[this.path.length-1]; 182 | state.itemId = itemId; 183 | this.refresh(); 184 | }; 185 | 186 | TodoApp.prototype.up = function () { 187 | var list = this.getList(); 188 | var item = this.getItem(); 189 | var i = list.indexOf(item); 190 | if (i>0) { 191 | this.selectItem(i-1); 192 | } 193 | }; 194 | 195 | TodoApp.prototype.down = function () { 196 | var list = this.getList(); 197 | var item = this.getItem(); 198 | var i = list.indexOf(item); 199 | if (i+1'); 147 | } 148 | 149 | // Some ungodly magic to send prerendered displayable HTML in one 150 | // piece. Less RTTs => faster load times, you know. 151 | function loadHtmlTemplate () { 152 | // The code depends on the version of React. 153 | // Better if React versions match (here and in package.json) 154 | var reactUrl = argv.cdn ? 'http://fb.me/react-0.11.2.min.js' : '/dist/react.min.js' 155 | var css1 = fs.readFileSync('./css/base.css').toString(); 156 | var css2 = fs.readFileSync('./css/add.css').toString(); 157 | var css3 = fs.readFileSync('./css/touch.css').toString(); 158 | var css = css1 + css2 + css3; // the license differs :( 159 | var template = fs.readFileSync('./view/index.html').toString(); 160 | template = template.replace('$CSS',css); 161 | template = template.replace('$REACT',reactUrl); 162 | var i = template.indexOf('$VIEW'); 163 | return { 164 | head: template.substr(0,i), 165 | tail: template.substr(i+'$VIEW'.length) 166 | }; 167 | } 168 | -------------------------------------------------------------------------------- /TodoRouter.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var TodoList = require('./model/TodoList'); 4 | var TodoItem = require('./model/TodoItem'); 5 | var Swarm = require('swarm'); 6 | var Spec = Swarm.Spec; 7 | 8 | function TodoRouter() { 9 | this.itemIds = null; 10 | } 11 | 12 | TodoRouter.prototype.buildRoute = function (path) { 13 | if (path.length === 0) { 14 | return '/'; 15 | } 16 | return '/' + path[0].listId + 17 | '/' + path.map(function (el) {return el.itemId;}).join('/'); 18 | }; 19 | 20 | TodoRouter.prototype.addPathItem = function (listId, itemIds, path, cb) { 21 | // normalize 2nd argument 22 | if (!itemIds) { 23 | itemIds = []; 24 | } else if (itemIds.constructor===String) { 25 | itemIds = [itemIds]; 26 | } else if (itemIds.constructor!==Array) { 27 | throw new Error('incorrect argument'); 28 | } 29 | 30 | var self = this; 31 | self.itemIds = itemIds; 32 | 33 | var list = Swarm.env.localhost.get('/TodoList#' + listId); 34 | list.onObjectStateReady(function () { 35 | if (self.itemIds != itemIds) { 36 | // prevent parallel loading of several routes 37 | return; 38 | } 39 | var item; 40 | if (!list.length()) { 41 | item = new TodoItem(); 42 | list.addObject(item); 43 | itemIds.length = 0; 44 | } else if (itemIds.length === 0) { 45 | item = list.objectAt(0); 46 | } else { 47 | item = list.getObject(itemIds.shift()); 48 | } 49 | 50 | if (item) { 51 | // item found 52 | path.push({ 53 | listId: listId, 54 | itemId: item._id 55 | }); 56 | if (item.childList && itemIds.length) { 57 | self.addPathItem(item.childList, itemIds, path, cb); 58 | return; 59 | } 60 | } 61 | self.itemIds = null; 62 | cb(path); 63 | }); 64 | }; 65 | 66 | TodoRouter.prototype.load = function (route, cb) { 67 | var rootListId = null; 68 | var itemIds = []; 69 | var m; 70 | Spec.reQTokExt.lastIndex = 0; 71 | while (m = Spec.reQTokExt.exec(route)) { 72 | var id = m && m[2]; 73 | if (!rootListId) { 74 | rootListId = id; 75 | } else { 76 | itemIds.push(id); 77 | } 78 | } 79 | if (!rootListId) { 80 | // create new list 81 | var list = new TodoList(); 82 | rootListId = list._id; 83 | // and create new item in it 84 | var item = new TodoItem(); 85 | list.addObject(item); 86 | itemIds.push(item._id); 87 | } 88 | this.addPathItem(rootListId, itemIds, [], cb); 89 | }; 90 | 91 | module.exports = TodoRouter; 92 | -------------------------------------------------------------------------------- /css/add.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2013-2014 Facebook, Inc. 3 | * Copyright 2014 V. Grishchenko 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | * 17 | * base.css overrides 18 | */ 19 | 20 | html, 21 | body { 22 | width: 100%; 23 | height: 100%; 24 | } 25 | 26 | #todoapp { 27 | width: 100%; 28 | height: 100%; 29 | } 30 | 31 | .todopane { 32 | white-space: nowrap; 33 | margin: auto; 34 | overflow: auto; 35 | width: 100%; 36 | height: 100%; 37 | } 38 | 39 | .todo-list { 40 | display: inline-block; 41 | vertical-align: top; 42 | margin: 10px; 43 | background: #f8f8f2; 44 | position: relative; 45 | width: 550px; 46 | 47 | margin: 60px 10px 40px 10px; 48 | border: 1px solid #ccc; 49 | border-top-left-radius: 2px; 50 | border-top-right-radius: 2px; 51 | box-shadow: 0 2px 6px 0 rgba(0, 0, 0, 0.2), 52 | 0 25px 50px 0 rgba(0, 0, 0, 0.15); 53 | 54 | } 55 | 56 | .todo-list.top { 57 | opacity: 1; 58 | } 59 | 60 | .todo-list ul li { 61 | color: #5d5d4d; 62 | } 63 | 64 | .todo-list ul li .edit { 65 | margin-top: 6px; 66 | margin-bottom: 6px; 67 | display: inline; 68 | border: none; 69 | box-shadow: none; 70 | width: 455px; 71 | max-width: 455px; 72 | position: static; 73 | margin-left: 12px; 74 | background: transparent; 75 | font-size: 24px; 76 | outline: none; 77 | text-overflow: ellipsis; 78 | } 79 | 80 | input:focus { 81 | } 82 | 83 | .todo-list ul li.selected { 84 | background: #f2f2ff; 85 | position: relative; 86 | } 87 | 88 | .todo-list.top ul li.selected { 89 | background: #e0e0ff; 90 | } 91 | 92 | .todo-list ul li.completed .edit { 93 | color: #aaa; 94 | } 95 | 96 | span.bookmark { 97 | position: absolute; 98 | right: -5px; 99 | top: -3px; 100 | font-size: 20px; 101 | background: #ff9; 102 | box-shadow: 1px 1px 1px grey; 103 | z-index: -5; 104 | position: relative; 105 | min-height: 24px; 106 | font-family: mono; 107 | /* border: grey 1px; */ 108 | color: #888; 109 | padding: 4px; 110 | white-space: pre; 111 | width: 40px; 112 | display: inline-block; 113 | text-align: right; 114 | } 115 | 116 | .todo-list ul li.selected span.bookmark { 117 | width: 50px; 118 | right: -5px; 119 | } 120 | 121 | .todo-list.top ul li.selected span.bookmark { 122 | width: 40px; 123 | right: -15px; 124 | } 125 | 126 | .footer { 127 | z-index: auto; 128 | } 129 | 130 | span.help, a#self, span.golink { 131 | position: absolute; 132 | font-family: sans-serif; 133 | font-size: 16px; 134 | color: #aaa; 135 | max-width: 70%; 136 | white-space: pre; 137 | } 138 | 139 | span.golink { 140 | right: 24px; 141 | text-align: right; 142 | z-index: 2; 143 | top: 24px; 144 | opacity: 0.5; 145 | } 146 | 147 | span.help { 148 | right: 24px; 149 | text-align: right; 150 | z-index: -2; 151 | bottom: 24px; 152 | } 153 | 154 | a#self { 155 | left: 24px; 156 | text-align: left; 157 | z-index: 2; 158 | bottom: 24px; 159 | } 160 | 161 | .clear-completed { 162 | position: absolute; 163 | right: 10px; 164 | } 165 | -------------------------------------------------------------------------------- /css/base.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | margin: 0; 4 | padding: 0; 5 | } 6 | 7 | button { 8 | margin: 0; 9 | padding: 0; 10 | border: 0; 11 | background: none; 12 | font-size: 100%; 13 | vertical-align: baseline; 14 | font-family: inherit; 15 | color: inherit; 16 | -webkit-appearance: none; 17 | -ms-appearance: none; 18 | -o-appearance: none; 19 | appearance: none; 20 | } 21 | 22 | body { 23 | font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif; 24 | line-height: 1.4em; 25 | background: #eaeaea url('/css/bg.png'); 26 | color: #4d4d4d; 27 | margin: 0 auto; 28 | -webkit-font-smoothing: antialiased; 29 | -moz-font-smoothing: antialiased; 30 | -ms-font-smoothing: antialiased; 31 | -o-font-smoothing: antialiased; 32 | font-smoothing: antialiased; 33 | } 34 | 35 | button, 36 | input[type="checkbox"] { 37 | outline: none; 38 | } 39 | 40 | .todo-list ul li:before { 41 | content: ''; 42 | border-left: 1px solid #f5d6d6; 43 | border-right: 1px solid #f5d6d6; 44 | width: 2px; 45 | position: absolute; 46 | top: 0; 47 | left: 40px; 48 | height: 100%; 49 | z-index: 1; 50 | } 51 | 52 | #todoapp input::-webkit-input-placeholder { 53 | font-style: italic; 54 | } 55 | 56 | #todoapp input::-moz-placeholder { 57 | font-style: italic; 58 | color: #a9a9a9; 59 | } 60 | 61 | #todoapp h1 { 62 | position: absolute; 63 | top: -120px; 64 | width: 100%; 65 | font-size: 70px; 66 | font-weight: bold; 67 | text-align: center; 68 | color: #b3b3b3; 69 | color: rgba(255, 255, 255, 0.3); 70 | text-shadow: -1px -1px rgba(0, 0, 0, 0.2); 71 | -webkit-text-rendering: optimizeLegibility; 72 | -moz-text-rendering: optimizeLegibility; 73 | -ms-text-rendering: optimizeLegibility; 74 | -o-text-rendering: optimizeLegibility; 75 | text-rendering: optimizeLegibility; 76 | } 77 | 78 | .header { 79 | padding-top: 15px; 80 | border-radius: inherit; 81 | } 82 | 83 | .header:before { 84 | content: ''; 85 | position: absolute; 86 | top: 0; 87 | right: 0; 88 | left: 0; 89 | height: 15px; 90 | z-index: 2; 91 | border-bottom: 1px solid #6c615c; 92 | background: #8d7d77; 93 | background: -webkit-gradient(linear, left top, left bottom, from(rgba(132, 110, 100, 0.8)),to(rgba(101, 84, 76, 0.8))); 94 | background: -webkit-linear-gradient(top, rgba(132, 110, 100, 0.8), rgba(101, 84, 76, 0.8)); 95 | background: linear-gradient(top, rgba(132, 110, 100, 0.8), rgba(101, 84, 76, 0.8)); 96 | filter: progid:DXImageTransform.Microsoft.gradient(GradientType=0,StartColorStr='#9d8b83', EndColorStr='#847670'); 97 | border-top-left-radius: 1px; 98 | border-top-right-radius: 1px; 99 | } 100 | 101 | #main { 102 | position: relative; 103 | z-index: 2; 104 | border-top: 1px dotted #adadad; 105 | } 106 | 107 | label[for='toggle-all'] { 108 | display: none; 109 | } 110 | 111 | .toggle-all { 112 | position: absolute; 113 | top: -42px; 114 | left: -4px; 115 | width: 40px; 116 | text-align: center; 117 | /* Mobile Safari */ 118 | border: none; 119 | } 120 | 121 | .toggle-all:before { 122 | content: '»'; 123 | font-size: 28px; 124 | color: #d9d9d9; 125 | padding: 0 25px 7px; 126 | } 127 | 128 | .toggle-all:checked:before { 129 | color: #737373; 130 | } 131 | 132 | .todo-list ul { 133 | margin: 0; 134 | padding: 0; 135 | list-style: none; 136 | } 137 | 138 | .todo-list ul li { 139 | position: relative; 140 | font-size: 24px; 141 | border-bottom: 1px dotted #ccc; 142 | } 143 | 144 | .todo-list ul li:last-child { 145 | } 146 | 147 | .todo-list ul li.editing { 148 | border-bottom: none; 149 | padding: 0; 150 | } 151 | 152 | .todo-list ul li.editing .edit { 153 | display: block; 154 | width: 506px; 155 | padding: 13px 17px 12px 17px; 156 | margin: 0 0 0 43px; 157 | } 158 | 159 | .todo-list ul li.editing .view { 160 | display: none; 161 | } 162 | 163 | .todo-list ul li .toggle { 164 | text-align: center; 165 | width: 40px; 166 | /* auto, since non-WebKit browsers doesn't support input styling */ 167 | height: auto; 168 | top: 0; 169 | bottom: 0; 170 | margin: auto 0; 171 | /* Mobile Safari */ 172 | border: none; 173 | -webkit-appearance: none; 174 | -ms-appearance: none; 175 | -o-appearance: none; 176 | appearance: none; 177 | } 178 | 179 | .todo-list ul li .toggle:after { 180 | content: '✔'; 181 | /* 40 + a couple of pixels visual adjustment */ 182 | line-height: 43px; 183 | font-size: 20px; 184 | color: #d9d9d9; 185 | text-shadow: 0 -1px 0 #bfbfbf; 186 | } 187 | 188 | .todo-list ul li .toggle:checked:after { 189 | color: #85ada7; 190 | text-shadow: 0 1px 0 #669991; 191 | bottom: 1px; 192 | position: relative; 193 | } 194 | 195 | .todo-list ul li label { 196 | white-space: pre; 197 | word-break: break-word; 198 | padding: 15px 60px 15px 15px; 199 | margin-left: 45px; 200 | display: block; 201 | line-height: 1.2; 202 | -webkit-transition: color 0.4s; 203 | transition: color 0.4s; 204 | } 205 | 206 | .todo-list ul li.completed label { 207 | color: #a9a9a9; 208 | text-decoration: line-through; 209 | } 210 | 211 | .todo-list ul li .destroy { 212 | display: none; 213 | position: absolute; 214 | top: 0; 215 | right: 10px; 216 | bottom: 0; 217 | width: 40px; 218 | height: 40px; 219 | margin: auto 0; 220 | font-size: 22px; 221 | color: #a88a8a; 222 | -webkit-transition: all 0.2s; 223 | transition: all 0.2s; 224 | } 225 | 226 | .todo-list ul li .destroy:hover { 227 | text-shadow: 0 0 1px #000, 228 | 0 0 10px rgba(199, 107, 107, 0.8); 229 | -webkit-transform: scale(1.3); 230 | -ms-transform: scale(1.3); 231 | transform: scale(1.3); 232 | color: #900; 233 | } 234 | 235 | .todo-list ul li .destroy:after { 236 | content: '✖'; 237 | } 238 | 239 | .todo-list ul li:hover .destroy { 240 | display: block; 241 | } 242 | 243 | .todo-list ul li .edit { 244 | display: none; 245 | } 246 | 247 | .todo-list ul li.editing:last-child { 248 | margin-bottom: -1px; 249 | } 250 | 251 | .footer { 252 | color: #777; 253 | padding: 0 15px; 254 | position: absolute; 255 | right: 0; 256 | bottom: -31px; 257 | left: 0; 258 | height: 20px; 259 | z-index: -2; 260 | text-align: center; 261 | } 262 | 263 | .footer:before { 264 | content: ''; 265 | position: absolute; 266 | right: 0; 267 | bottom: 31px; 268 | left: 0; 269 | height: 50px; 270 | z-index: -1; 271 | box-shadow: 0 1px 1px rgba(0, 0, 0, 0.3), 272 | 0 6px 0 -3px rgba(255, 255, 255, 0.8), 273 | 0 7px 1px -3px rgba(0, 0, 0, 0.3), 274 | 0 43px 0 -6px rgba(255, 255, 255, 0.8), 275 | 0 44px 2px -6px rgba(0, 0, 0, 0.2); 276 | } 277 | 278 | .todo-count { 279 | float: left; 280 | text-align: left; 281 | } 282 | 283 | .filters { 284 | margin: 0; 285 | padding: 0; 286 | list-style: none; 287 | position: absolute; 288 | right: 0; 289 | left: 0; 290 | } 291 | 292 | .filters li { 293 | display: inline; 294 | } 295 | 296 | .filters li a { 297 | color: #83756f; 298 | margin: 2px; 299 | text-decoration: none; 300 | } 301 | 302 | .filters li a.selected { 303 | font-weight: bold; 304 | } 305 | 306 | .clear-completed { 307 | float: right; 308 | position: relative; 309 | line-height: 20px; 310 | text-decoration: none; 311 | background: rgba(0, 0, 0, 0.1); 312 | font-size: 11px; 313 | padding: 0 10px; 314 | border-radius: 3px; 315 | box-shadow: 0 -1px 0 0 rgba(0, 0, 0, 0.2); 316 | } 317 | 318 | .clear-completed:hover { 319 | background: rgba(0, 0, 0, 0.15); 320 | box-shadow: 0 -1px 0 0 rgba(0, 0, 0, 0.3); 321 | } 322 | 323 | .info { 324 | margin: 65px auto 0; 325 | color: #a6a6a6; 326 | font-size: 12px; 327 | text-shadow: 0 1px 0 rgba(255, 255, 255, 0.7); 328 | text-align: center; 329 | } 330 | 331 | .info a { 332 | color: inherit; 333 | } 334 | 335 | /* 336 | Hack to remove background from Mobile Safari. 337 | Can't use it globally since it destroys checkboxes in Firefox and Opera 338 | */ 339 | 340 | @media screen and (-webkit-min-device-pixel-ratio:0) { 341 | .toggle-all, 342 | .todo-list ul li .toggle { 343 | background: none; 344 | } 345 | 346 | .todo-list ul li .toggle { 347 | height: 40px; 348 | } 349 | 350 | .toggle-all { 351 | top: -45px; 352 | left: -20px; 353 | width: 65px; 354 | height: 41px; 355 | -webkit-transform: rotate(90deg); 356 | -ms-transform: rotate(90deg); 357 | transform: rotate(90deg); 358 | -webkit-appearance: none; 359 | appearance: none; 360 | } 361 | } 362 | 363 | .hidden { 364 | display: none; 365 | } 366 | 367 | hr { 368 | margin: 20px 0; 369 | border: 0; 370 | border-top: 1px dashed #C5C5C5; 371 | border-bottom: 1px dashed #F7F7F7; 372 | } 373 | 374 | .learn a { 375 | font-weight: normal; 376 | text-decoration: none; 377 | color: #b83f45; 378 | } 379 | 380 | .learn a:hover { 381 | text-decoration: underline; 382 | color: #787e7e; 383 | } 384 | 385 | .learn h3, 386 | .learn h4, 387 | .learn h5 { 388 | margin: 10px 0; 389 | font-weight: 500; 390 | line-height: 1.2; 391 | color: #000; 392 | } 393 | 394 | .learn h3 { 395 | font-size: 24px; 396 | } 397 | 398 | .learn h4 { 399 | font-size: 18px; 400 | } 401 | 402 | .learn h5 { 403 | margin-bottom: 0; 404 | font-size: 14px; 405 | } 406 | 407 | .learn ul { 408 | padding: 0; 409 | margin: 0 0 30px 25px; 410 | } 411 | 412 | .learn li { 413 | line-height: 20px; 414 | } 415 | 416 | .learn p { 417 | font-size: 15px; 418 | font-weight: 300; 419 | line-height: 1.3; 420 | margin-top: 0; 421 | margin-bottom: 0; 422 | } 423 | 424 | .quote { 425 | border: none; 426 | margin: 20px 0 60px 0; 427 | } 428 | 429 | .quote p { 430 | font-style: italic; 431 | } 432 | 433 | .quote p:before { 434 | content: '“'; 435 | font-size: 50px; 436 | opacity: .15; 437 | position: absolute; 438 | top: -20px; 439 | left: 3px; 440 | } 441 | 442 | .quote p:after { 443 | content: '”'; 444 | font-size: 50px; 445 | opacity: .15; 446 | position: absolute; 447 | bottom: -42px; 448 | right: 3px; 449 | } 450 | 451 | .quote footer { 452 | position: absolute; 453 | bottom: -40px; 454 | right: 0; 455 | } 456 | 457 | .quote footer img { 458 | border-radius: 3px; 459 | } 460 | 461 | .quote footer a { 462 | margin-left: 5px; 463 | vertical-align: middle; 464 | } 465 | 466 | .speech-bubble { 467 | position: relative; 468 | padding: 10px; 469 | background: rgba(0, 0, 0, .04); 470 | border-radius: 5px; 471 | } 472 | 473 | .speech-bubble:after { 474 | content: ''; 475 | position: absolute; 476 | top: 100%; 477 | right: 30px; 478 | border: 13px solid transparent; 479 | border-top-color: rgba(0, 0, 0, .04); 480 | } 481 | 482 | .learn-bar > .learn { 483 | position: absolute; 484 | width: 272px; 485 | top: 8px; 486 | left: -300px; 487 | padding: 10px; 488 | border-radius: 5px; 489 | background-color: rgba(255, 255, 255, .6); 490 | -webkit-transition-property: left; 491 | transition-property: left; 492 | -webkit-transition-duration: 500ms; 493 | transition-duration: 500ms; 494 | } 495 | 496 | @media (min-width: 899px) { 497 | .learn-bar { 498 | width: auto; 499 | margin: 0 0 0 300px; 500 | } 501 | 502 | .learn-bar > .learn { 503 | left: 8px; 504 | } 505 | 506 | .learn-bar #todoapp { 507 | width: 550px; 508 | margin: 130px auto 40px auto; 509 | } 510 | } 511 | -------------------------------------------------------------------------------- /css/bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/docker-in-practice/todo/f84c61c10e1f0c82bd1c8cbe5ec6ed2ef5ffc9bd/css/bg.png -------------------------------------------------------------------------------- /css/local.css: -------------------------------------------------------------------------------- 1 | /* ipad */ 2 | @media (max-width: 570px) { 3 | .help { 4 | position: relative; 5 | } 6 | #todoapp { 7 | width: 100%; 8 | overflow: hidden; 9 | } 10 | .todo-list { 11 | width: 95%; 12 | } 13 | .todo-list ul li .edit { 14 | max-width: 86%; 15 | } 16 | /* .clear-completed{ */ 17 | /* display: none; */ 18 | /* } */ 19 | 20 | .todo-list ul li input { 21 | font-size: 12px!important; 22 | position: relative; 23 | top: -6px; 24 | } 25 | .todo-list ul li div { 26 | height: 30px; 27 | } 28 | 29 | .todo-list ul li:before { 30 | left: 30px; 31 | } 32 | .todo-list ul li .toggle{ 33 | width: 30px; 34 | position: relative; 35 | top: -6px; 36 | } 37 | .todo-list ul li .toggle:after { 38 | font-size: 10px!important; 39 | } 40 | .footer { 41 | font-size: 10px; 42 | bottom: -19px; 43 | height: 16px; 44 | } 45 | 46 | .todo-list ul li .destroy { 47 | font-size: 13px; 48 | right: -5px; 49 | } 50 | .clear-completed { 51 | font-size: 9px; 52 | } 53 | 54 | 55 | .todo-list ul li { 56 | height: 30px; 57 | } 58 | } 59 | 60 | /* iphone */ 61 | @media (max-width: 210px) { 62 | .help { 63 | position: relative; 64 | } 65 | #todoapp { 66 | width: 100%; 67 | overflow: hidden; 68 | } 69 | .todo-list { 70 | width: 190px; 71 | } 72 | .todo-list ul li .edit { 73 | max-width: 120px; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /css/touch.css: -------------------------------------------------------------------------------- 1 | .todo-list ul li.selected { position: relative } 2 | .todo-list ul li span.tab { display: none } 3 | .todo-list ul li.selected span.tab { 4 | display: block; 5 | position: absolute; 6 | width: 100px; 7 | height: 20px; 8 | right: -120px; 9 | top: 10px; 10 | font-size: 50px; 11 | border: none; 12 | color: #d9d9d9; 13 | } 14 | .todo-list ul li.selected span.tab.child-list { right: -130px } 15 | -------------------------------------------------------------------------------- /fake_auth_init.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | var sessionId = window.localStorage.getItem('localuser'); 3 | if (!sessionId) { 4 | var rnd = Math.floor(Math.random()*(1<<30)); 5 | sessionId = 'A~' + Swarm.Spec.int2base(rnd); 6 | window.localStorage.setItem('localuser',sessionId); 7 | } 8 | window.app = new window.TodoApp(sessionId); 9 | }()); 10 | -------------------------------------------------------------------------------- /favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/docker-in-practice/todo/f84c61c10e1f0c82bd1c8cbe5ec6ed2ef5ffc9bd/favicon.ico -------------------------------------------------------------------------------- /local.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Swarm+React • TodoMVC 6 | 7 | 8 | 9 | 10 | 11 |
12 |
13 | 14 | Up/Down: change item, 15 | Left/Right: change list, 16 | Space: toggle item, 17 | Enter: new item, 18 | Esc: back 19 | 20 | 21 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /model/TodoItem.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var Swarm = require('swarm'); 4 | 5 | var TodoItem = Swarm.Model.extend('TodoItem', { 6 | defaults: { 7 | text: String, 8 | completed: false, 9 | childList: '' 10 | }, 11 | 12 | toggle: function () { 13 | this.set({ completed: !this.completed }); 14 | }, 15 | 16 | complete: function () { 17 | this.set({ completed: true }); 18 | }, 19 | 20 | uncomplete: function () { 21 | this.set({ completed: false }); 22 | } 23 | 24 | }); 25 | 26 | module.exports = TodoItem; 27 | -------------------------------------------------------------------------------- /model/TodoList.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var Swarm = require('swarm'); 4 | var TodoItem = require('./TodoItem'); 5 | 6 | var TodoList = Swarm.Vector.extend('TodoList', { 7 | 8 | objectType: TodoItem, 9 | 10 | completeAll: function() { 11 | var stats = this.stats(); 12 | if (stats.left === 0) { 13 | // all todos completed, so uncomplete them 14 | this.forEach(function (obj) { 15 | if (obj && obj._version) { 16 | obj.uncomplete(); 17 | } 18 | }); 19 | } else { 20 | this.forEach(function (obj) { 21 | if (obj && obj._version && !obj.completed) { 22 | obj.complete(); 23 | } 24 | }); 25 | } 26 | }, 27 | 28 | removeCompleted: function () { 29 | // TODO one op - repeated spec? long spec? 30 | var rms = [], rm; 31 | this.forEach(function(obj){ 32 | if (obj.completed) { 33 | rms.push(obj); 34 | } 35 | }); 36 | while (rm = rms.pop()) { 37 | this.remove(rm); 38 | } 39 | }, 40 | 41 | stats: function() { 42 | var ret = { 43 | entries: 0, 44 | completed: 0, 45 | left: 0 46 | }; 47 | this.forEach(function (obj) { 48 | ret.entries++; 49 | if (obj.completed) { 50 | ret.completed++; 51 | } else { 52 | ret.left++; 53 | } 54 | }); 55 | return ret; 56 | } 57 | }); 58 | 59 | module.exports = TodoList; 60 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "todomvc-swarm", 3 | "version": "0.0.1", 4 | "description": "Example React+Swarm TodoMVC app.", 5 | "main": "dist/TodoApp.app.js", 6 | "dependencies": { 7 | "compression": "^1.1.0", 8 | "express": "^4.6.1", 9 | "express-react-views": "^0.5.0", 10 | "browserify-shim": "~3.2.0", 11 | "minimist": "^1.1.0", 12 | "node-jsx": "^0.11.0", 13 | "react": "^0.11.0", 14 | "react-tools": "^0.11.0", 15 | "swarm": "^0.3.25", 16 | "ws": "^0.4.31" 17 | }, 18 | "devDependencies": { 19 | "browserify": "^2.36.0", 20 | "envify": "~1.2.0", 21 | "minifyify": "^4.4.0", 22 | "reactify": "~0.4.0", 23 | "statics": "~0.1.0" 24 | }, 25 | "files": [ 26 | "css/*.css", 27 | "css/*png", 28 | "model/*.js", 29 | "view/*.jsx", 30 | "view/index.html", 31 | "TodoApp.js", 32 | "TodoAppServer.js", 33 | "TodoRouter.js", 34 | "README.md", 35 | "LICENSE", 36 | "Makefile", 37 | "fake_auth_init.js", 38 | "todo.appcache", 39 | "run.sh" 40 | ], 41 | "scripts": { 42 | "prestart": "make all", 43 | "start": "node TodoAppServer.js", 44 | "preuninstall": "make clean" 45 | }, 46 | "author": "Bill Fisher, Andrew Popp and finally Victor Grishchenko", 47 | "license": "Apache 2", 48 | "browserify": { 49 | "transform": [ 50 | "reactify", 51 | "envify", 52 | "browserify-shim" 53 | ] 54 | }, 55 | "browserify-shim": { 56 | "react": "global:React" 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | npm install 4 | 5 | while [ /bin/true ]; do 6 | LOGDATE=`date +%Y.%m.%d_%H:%M:%S` 7 | echo starting instance $LOGDATE 8 | node TodoAppServer.js $@ > $LOGDATE.out 2> $LOGDATE.err 9 | gzip $LOGDATE.out & 10 | gzip $LOGDATE.err & 11 | sleep 1; 12 | done 13 | 14 | -------------------------------------------------------------------------------- /serv.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | // Simple Swarm sync server: picks model classes from a directory, 4 | // starts a WebSocket server at a port. Serves some static content, 5 | // although I'd recomment to shield it with nginx. 6 | var fs = require('fs'); 7 | var path = require('path'); 8 | var url = require('url'); 9 | var http = require('http'); 10 | 11 | var nopt = require('nopt'); 12 | var ws_lib = require('ws'); 13 | var pushserve = require('pushserve'); 14 | 15 | var Swarm = require('swarm'); 16 | var EinarosWSStream = Swarm.EinarosWSStream; 17 | 18 | var options = nopt({ 19 | models : path, 20 | index : path, 21 | port : Number 22 | }); 23 | 24 | // boot model classes 25 | var modelPathList = options.models||'model/'; 26 | modelPathList.split(/[\:;,]/g).forEach(function (modelPath) { 27 | modelPath = path.resolve(modelPath); 28 | console.log('scanning',modelPath); 29 | var modelClasses = fs.readdirSync(modelPath), modelFile; 30 | while (modelFile = modelClasses.pop()) { 31 | if (!/^\w+\.js$/.test(modelFile)) { continue; } 32 | var modpath = path.join(modelPath, modelFile); 33 | var fn = require(modpath); 34 | if (fn.constructor !== Function) { continue; } 35 | if (fn.extend !== Swarm.Syncable.extend) { continue; } 36 | console.log('Model loaded', fn.prototype._type, ' at ', modpath); 37 | } 38 | }); 39 | 40 | // use file storage 41 | var fileStorage = new Swarm.FileStorage('.swarm'); 42 | 43 | // create Swarm Host 44 | var swarmHost = new Swarm.Host('swarm~nodejs', 0, fileStorage); 45 | Swarm.localhost = swarmHost; 46 | 47 | // start the HTTP server 48 | var httpServer = pushserve(options); 49 | console.log('HTTP server started'); 50 | 51 | // start WebSocket server 52 | var wsServer = new ws_lib.Server({ 53 | server: httpServer 54 | }); 55 | console.log('Swarm server started'); 56 | 57 | // add pipes 58 | wsServer.on('connection', function(ws) { 59 | var params = url.parse(ws.upgradeReq.url,true); 60 | console.log('incomingWS %s', params.path); 61 | // check the secret 62 | // FIXME grant ssn 63 | swarmHost.accept(new EinarosWSStream(ws), { delay: 50 }); 64 | }); 65 | 66 | -------------------------------------------------------------------------------- /todo.appcache: -------------------------------------------------------------------------------- 1 | CACHE MANIFEST 2 | #2014-10-17 v0.0.7 3 | 4 | CACHE: 5 | /dist/react.min.js 6 | /dist/TodoApp.app.js 7 | /fake_auth_init.js 8 | /css/base.css 9 | /css/add.css 10 | /css/bg.png 11 | 12 | FALLBACK: 13 | / /offline.html 14 | -------------------------------------------------------------------------------- /view/Footer.jsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2013-2014 Facebook, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | * @jsx React.DOM 17 | */ 18 | 19 | var React = require('react'); 20 | var Swarm = require('swarm'); 21 | var TodoItem = require('../model/TodoItem'); 22 | 23 | var Footer = React.createClass({ 24 | 25 | getDefaultProps: function () { 26 | return { 27 | listenEntries: true 28 | }; 29 | }, 30 | 31 | mixins: [Swarm.ReactMixin], 32 | 33 | /** 34 | * @return {object} 35 | */ 36 | render: function() { 37 | var todoList = this.sync; 38 | var stats = todoList.stats(); 39 | 40 | if (stats.entries === 0) { 41 | return