Commit f00d3624 authored by Sindre Sorhus's avatar Sindre Sorhus

Add SocketStream app

parent 8a841776
node_modules
\ No newline at end of file
# Ignore file for Nodemon: https://github.com/remy/nodemon
# Install with 'npm install -g nodemon' then start your app with 'nodemon app.js'
# From then on, all changes you make to /server will cause your app to automatically restart
/client/*
./README.md
# SocketStream TodoMVC app
> A fast, modular Node.js web framework dedicated to building realtime single-page apps
## Getting Started
Running this app requires [Node.js](http://nodejs.org)
- `cd` into this folder and run `npm install` to fetch all the required dependencies
- `node app.js` to start the app
- Open `localhost:3000` in two side-by-side browser windows, add a todo and see the magic
\ No newline at end of file
var http = require('http'),
ss = require('socketstream');
// Define a single-page client
ss.client.define('main', {
view: 'app.html',
css: ['base.css'],
code: [ 'libs', 'app' ],
tmpl: '*'
});
// Serve this client on the root URL
ss.http.route( '/', function( req, res ) {
res.serveClient('main');
});
// Use server-side compiled Hogan (Mustache) templates. Others engines available
ss.client.templateEngine.use( require('ss-hogan') );
// Minimize and pack assets if you type: SS_ENV=production node app.js
if ( ss.env === 'production' ) {
ss.client.packAssets();
}
// Start web server
var server = http.Server( ss.http.middleware );
server.listen(3000);
// Start SocketStream
ss.start( server );
\ No newline at end of file
/*global $, ss */
'use strict';
var Utils = {
// https://gist.github.com/1308368
uuid: function(a,b){for(b=a='';a++<36;b+=a*51&52?(a^15?8^Math.random()*(a^20?16:4):4).toString(16):'-');return b},
pluralize: function( count, word ) {
return count === 1 ? word : word + 's';
}
};
var App = {
init: function() {
var self = this;
this.ENTER_KEY = 13;
ss.rpc('todos.getAll', function( todos ) {
self.todos = todos;
self.cacheElements();
self.bindEvents();
self.render();
});
},
cacheElements: function() {
this.$todoApp = $('#todoapp');
this.$newTodo = $('#new-todo');
this.$toggleAll = $('#toggle-all');
this.$main = $('#main');
this.$todoList = $('#todo-list');
this.$footer = this.$todoApp.find('#footer');
this.$count = $('#todo-count');
this.$clearBtn = $('#clear-completed');
},
bindEvents: function() {
var list = this.$todoList;
this.$newTodo.on( 'keyup', this.create );
this.$toggleAll.on( 'change', this.toggleAll );
this.$footer.on( 'click', '#clear-completed', this.destroyCompleted );
list.on( 'change', '.toggle', this.toggle );
list.on( 'dblclick', '.view', this.edit );
list.on( 'keypress', '.edit', this.blurOnEnter );
list.on( 'blur', '.edit', this.update );
list.on( 'click', '.destroy', this.destroy );
ss.event.on( 'updateTodos', this.updateTodos );
},
updateTodos: function( todos ) {
App.todos = todos;
App.render( true );
},
render: function( preventRpc ) {
var html = this.todos.map(function( el ) {
return ss.tmpl.todo.render( el );
}).join('');
this.$todoList.html( html );
this.$main.toggle( !!this.todos.length );
this.$toggleAll.prop( 'checked', !this.activeTodoCount() );
this.renderFooter();
if ( !preventRpc ) {
ss.rpc( 'todos.update', this.todos );
}
},
renderFooter: function() {
var todoCount = this.todos.length,
activeTodoCount = this.activeTodoCount(),
footer = {
activeTodoCount: activeTodoCount,
activeTodoWord: Utils.pluralize( activeTodoCount, 'item' ),
completedTodos: todoCount - activeTodoCount
};
this.$footer.toggle( !!todoCount );
this.$footer.html( ss.tmpl.footer.render( footer ) );
},
toggleAll: function() {
var isChecked = $( this ).prop('checked');
$.each( App.todos, function( i, val ) {
val.completed = isChecked;
});
App.render();
},
activeTodoCount: function() {
var count = 0;
$.each( this.todos, function( i, val ) {
if ( !val.completed ) {
count++;
}
});
return count;
},
destroyCompleted: function() {
var todos = App.todos,
l = todos.length;
while ( l-- ) {
if ( todos[l].completed ) {
todos.splice( l, 1 );
}
}
App.render();
},
// Accepts an element from inside the ".item" div and
// returns the corresponding todo in the todos array
getTodo: function( elem, callback ) {
var id = $( elem ).closest('li').data('id');
$.each( this.todos, function( i, val ) {
if ( val.id === id ) {
callback.apply( App, arguments );
return false;
}
});
},
create: function(e) {
var $input = $(this),
val = $.trim( $input.val() );
if ( e.which !== App.ENTER_KEY || !val ) {
return;
}
App.todos.push({
id: Utils.uuid(),
title: val,
completed: false
});
$input.val('');
App.render();
},
toggle: function() {
App.getTodo( this, function( i, val ) {
val.completed = !val.completed;
});
App.render();
},
edit: function() {
$(this).closest('li').addClass('editing').find('.edit').focus();
},
blurOnEnter: function( e ) {
if ( e.keyCode === App.ENTER_KEY ) {
e.target.blur();
}
},
update: function() {
var val = $.trim( $(this).removeClass('editing').val() );
App.getTodo( this, function( i ) {
if ( val ) {
this.todos[ i ].title = val;
} else {
this.todos.splice( i, 1 );
}
this.render();
});
},
destroy: function() {
App.getTodo( this, function( i ) {
this.todos.splice( i, 1 );
this.render();
});
}
};
App.init();
\ No newline at end of file
// This file automatically gets called first by SocketStream and must always exist
// Make 'ss' available to all modules and the browser console
window.ss = require('socketstream');
ss.server.on('disconnect', function() {
console.log('Connection down :-(');
});
ss.server.on('reconnect', function() {
console.log('Connection back up :-)');
});
ss.server.on('ready', function() {
// Wait for the DOM to finish loading
$(function() {
// Load app
require('/app');
});
});
\ No newline at end of file
(function( window ) {
'use strict';
if ( location.hostname === 'todomvc.com' ) {
var _gaq=[['_setAccount','UA-31081062-1'],['_trackPageview']];(function(d,t){var g=d.createElement(t),s=d.getElementsByTagName(t)[0];g.src='//www.google-analytics.com/ga.js';s.parentNode.insertBefore(g,s)}(document,'script'));
}
})( window );
\ No newline at end of file
This diff is collapsed.
html,
body {
margin: 0;
padding: 0;
}
button {
margin: 0;
padding: 0;
border: 0;
background: none;
font-size: 100%;
vertical-align: baseline;
font-family: inherit;
color: inherit;
-webkit-appearance: none;
/*-moz-appearance: none;*/
-ms-appearance: none;
-o-appearance: none;
appearance: none;
}
body {
font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif;
line-height: 1.4em;
background: #eaeaea url('/bg.png');
color: #4d4d4d;
width: 550px;
margin: 0 auto;
-webkit-font-smoothing: antialiased;
-moz-font-smoothing: antialiased;
-ms-font-smoothing: antialiased;
-o-font-smoothing: antialiased;
font-smoothing: antialiased;
}
#todoapp {
background: #fff;
background: rgba(255, 255, 255, 0.9);
margin: 130px 0 40px 0;
border: 1px solid #ccc;
position: relative;
border-top-left-radius: 2px;
border-top-right-radius: 2px;
box-shadow: 0 2px 6px 0 rgba(0, 0, 0, 0.2),
0 25px 50px 0 rgba(0, 0, 0, 0.15);
}
#todoapp:before {
content: '';
border-left: 1px solid #f5d6d6;
border-right: 1px solid #f5d6d6;
width: 2px;
position: absolute;
top: 0;
left: 40px;
height: 100%;
}
#todoapp input::-webkit-input-placeholder {
font-style: italic;
}
#todoapp input:-moz-placeholder {
font-style: italic;
color: #a9a9a9;
}
#todoapp h1 {
position: absolute;
top: -120px;
width: 100%;
font-size: 70px;
font-weight: bold;
text-align: center;
color: #b3b3b3;
color: rgba(255, 255, 255, 0.3);
text-shadow: -1px -1px rgba(0, 0, 0, 0.2);
-webkit-text-rendering: optimizeLegibility;
-moz-text-rendering: optimizeLegibility;
-ms-text-rendering: optimizeLegibility;
-o-text-rendering: optimizeLegibility;
text-rendering: optimizeLegibility;
}
#header {
padding-top: 15px;
border-radius: inherit;
}
#header:before {
content: '';
position: absolute;
top: 0;
right: 0;
left: 0;
height: 15px;
z-index: 2;
border-bottom: 1px solid #6c615c;
background: #8d7d77;
background: -webkit-gradient(linear, left top, left bottom, from(rgba(132, 110, 100, 0.8)),to(rgba(101, 84, 76, 0.8)));
background: -webkit-linear-gradient(top, rgba(132, 110, 100, 0.8), rgba(101, 84, 76, 0.8));
background: -moz-linear-gradient(top, rgba(132, 110, 100, 0.8), rgba(101, 84, 76, 0.8));
background: -o-linear-gradient(top, rgba(132, 110, 100, 0.8), rgba(101, 84, 76, 0.8));
background: -ms-linear-gradient(top, rgba(132, 110, 100, 0.8), rgba(101, 84, 76, 0.8));
background: linear-gradient(top, rgba(132, 110, 100, 0.8), rgba(101, 84, 76, 0.8));
filter: progid:DXImageTransform.Microsoft.gradient(GradientType=0,StartColorStr='#9d8b83', EndColorStr='#847670');
border-top-left-radius: 1px;
border-top-right-radius: 1px;
}
#new-todo,
.edit {
position: relative;
margin: 0;
width: 100%;
font-size: 24px;
font-family: inherit;
line-height: 1.4em;
border: 0;
outline: none;
color: inherit;
padding: 6px;
border: 1px solid #999;
box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2);
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
-ms-box-sizing: border-box;
-o-box-sizing: border-box;
box-sizing: border-box;
-webkit-font-smoothing: antialiased;
-moz-font-smoothing: antialiased;
-ms-font-smoothing: antialiased;
-o-font-smoothing: antialiased;
font-smoothing: antialiased;
}
#new-todo {
padding: 16px 16px 16px 60px;
border: none;
background: rgba(0, 0, 0, 0.02);
z-index: 2;
box-shadow: none;
}
#main {
position: relative;
z-index: 2;
border-top: 1px dotted #adadad;
}
label[for='toggle-all'] {
display: none;
}
#toggle-all {
position: absolute;
top: -56px;
left: -15px;
width: 65px;
height: 41px;
text-align: center;
border: none; /* Mobile Safari */
-webkit-appearance: none;
/*-moz-appearance: none;*/
-ms-appearance: none;
-o-appearance: none;
appearance: none;
-webkit-transform: rotate(90deg);
/*-moz-transform: rotate(90deg);*/
-ms-transform: rotate(90deg);
/*-o-transform: rotate(90deg);*/
transform: rotate(90deg);
}
#toggle-all:before {
content: '»';
font-size: 28px;
color: #d9d9d9;
padding: 0 25px 7px;
}
#toggle-all:checked:before {
color: #737373;
}
#todo-list {
margin: 0;
padding: 0;
list-style: none;
}
#todo-list li {
position: relative;
font-size: 24px;
border-bottom: 1px dotted #ccc;
}
#todo-list li:last-child {
border-bottom: none;
}
#todo-list li.editing {
border-bottom: none;
padding: 0;
}
#todo-list li.editing .edit {
display: block;
width: 506px;
padding: 13px 17px 12px 17px;
margin: 0 0 0 43px;
}
#todo-list li.editing .view {
display: none;
}
#todo-list li .toggle {
text-align: center;
width: 40px;
height: 40px;
position: absolute;
top: 0;
bottom: 0;
margin: auto 0;
border: none; /* Mobile Safari */
-webkit-appearance: none;
/*-moz-appearance: none;*/
-ms-appearance: none;
-o-appearance: none;
appearance: none;
}
#todo-list li .toggle:after {
font-size: 18px;
content: '✔';
line-height: 43px; /* 40 + a couple of pixels visual adjustment */
font-size: 20px;
color: #d9d9d9;
text-shadow: 0 -1px 0 #bfbfbf;
}
#todo-list li .toggle:checked:after {
color: #85ada7;
text-shadow: 0 1px 0 #669991;
bottom: 1px;
position: relative;
}
#todo-list li label {
word-break: break-word;
margin: 15px 15px 15px 60px;
display: inline-block;
line-height: 1.2;
-webkit-transition: color 0.4s;
-moz-transition: color 0.4s;
-ms-transition: color 0.4s;
-o-transition: color 0.4s;
transition: color 0.4s;
}
#todo-list li.completed label {
color: #a9a9a9;
text-decoration: line-through;
}
#todo-list li .destroy {
display: none;
position: absolute;
top: 0;
right: 10px;
bottom: 0;
width: 40px;
height: 40px;
margin: auto 0;
font-size: 22px;
color: #a88a8a;
-webkit-transition: all 0.2s;
-moz-transition: all 0.2s;
-ms-transition: all 0.2s;
-o-transition: all 0.2s;
transition: all 0.2s;
}
#todo-list li .destroy:hover {
text-shadow: 0 0 1px #000,
0 0 10px rgba(199, 107, 107, 0.8);
-webkit-transform: scale(1.3);
-moz-transform: scale(1.3);
-ms-transform: scale(1.3);
-o-transform: scale(1.3);
transform: scale(1.3);
}
#todo-list li .destroy:after {
content: '✖';
}
#todo-list li:hover .destroy {
display: block;
}
#todo-list li .edit {
display: none;
}
#todo-list li.editing:last-child {
margin-bottom: -1px;
}
#footer {
color: #777;
padding: 0 15px;
position: absolute;
right: 0;
bottom: -31px;
left: 0;
height: 20px;
z-index: 1;
text-align: center;
}
#footer:before {
content: '';
position: absolute;
right: 0;
bottom: 31px;
left: 0;
height: 50px;
z-index: -1;
box-shadow: 0 1px 1px rgba(0, 0, 0, 0.3),
0 6px 0 -3px rgba(255, 255, 255, 0.8),
0 7px 1px -3px rgba(0, 0, 0, 0.3),
0 43px 0 -6px rgba(255, 255, 255, 0.8),
0 44px 2px -6px rgba(0, 0, 0, 0.2);
}
#todo-count {
float: left;
text-align: left;
}
#filters {
margin: 0;
padding: 0;
list-style: none;
position: absolute;
right: 0;
left: 0;
}
#filters li {
display: inline;
}
#filters li a {
color: #83756f;
margin: 2px;
text-decoration: none;
}
#filters li a.selected {
font-weight: bold;
}
#clear-completed {
float: right;
position: relative;
line-height: 20px;
text-decoration: none;
background: rgba(0, 0, 0, 0.1);
font-size: 11px;
padding: 0 10px;
border-radius: 3px;
box-shadow: 0 -1px 0 0 rgba(0, 0, 0, 0.2);
}
#clear-completed:hover {
background: rgba(0, 0, 0, 0.15);
box-shadow: 0 -1px 0 0 rgba(0, 0, 0, 0.3);
}
#info {
margin: 65px auto 0;
color: #a6a6a6;
font-size: 12px;
text-shadow: 0 1px 0 rgba(255, 255, 255, 0.7);
text-align: center;
}
#info a {
color: inherit;
}
/*
Hack to remove background from Mobile Safari.
Can't use it globally since it destroys checkboxes in Firefox and Opera
*/
@media screen and (-webkit-min-device-pixel-ratio:0) {
#toggle-all,
#todo-list li .toggle {
background: none;
}
}
\ No newline at end of file
<span id="todo-count"><strong>{{activeTodoCount}}</strong> {{activeTodoWord}} left</span>
{{#completedTodos}}<button id="clear-completed">Clear completed ({{completedTodos}})</button>{{/completedTodos}}
\ No newline at end of file
<li {{#completed}}class="completed"{{/completed}} data-id="{{id}}">
<div class="view">
<input class="toggle" type="checkbox" {{#completed}}checked{{/completed}}>
<label>{{title}}</label>
<button class="destroy"></button>
</div>
<input class="edit" value="{{title}}">
</li>
\ No newline at end of file
<!doctype html>
<html lang="en">
<head>
<SocketStream>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<title>SocketStream • TodoMVC</title>
</head>
<body>
<section id="todoapp">
<header id="header">
<h1>todos</h1>
<input id="new-todo" placeholder="What needs to be done?" autofocus>
</header>
<section id="main">
<input id="toggle-all" type="checkbox">
<label for="toggle-all">Mark all as complete</label>
<ul id="todo-list"></ul>
</section>
<footer id="footer">
<span id="todo-count"><strong>0</strong> item left</span>
<button id="clear-completed">Clear completed</button>
</footer>
</section>
<footer id="info">
<p>Double-click to edit a todo</p>
<p>Created by <a href="http://github.com/sindresorhus">Sindre Sorhus</a></p>
<p>Part of <a href="http://todomvc.com">TodoMVC</a></p>
</footer>
</body>
</html>
\ No newline at end of file
{
"name": "todos",
"description": "SocketStream TodoMVC app",
"version": "1.0.0",
"author": {
"name": "Sindre Sorhus",
"email": "sindresorhus@gmail.com",
"url": "http://sindresorhus.com"
},
"private": true,
"engines": {
"node": ">=0.6.0"
},
"dependencies": {
"socketstream": "0.3.x",
"ss-stylus": "0.1.x",
"ss-hogan": "0.1.x"
}
}
\ No newline at end of file
// Server-side code
// All the todos are stored in an in-memory array on the server
// This should not be done in production apps
var todos = [];
// Define actions which can be called from the client using
// ss.rpc('demo.ACTIONNAME', param1, param2...)
exports.actions = function( req, res, ss ) {
return {
getAll: function() {
res( todos );
},
update: function( clientTodos ) {
todos = clientTodos;
ss.publish.all( 'updateTodos', todos );
}
};
};
\ No newline at end of file
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment