Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

pushing the refactored version of the codebase with changes by Alex M…

…acCraw in place
  • Loading branch information...
commit 3251bfe3c60c3f354c930bac1a59d718ea50a7ac 1 parent 3bce32d
Addy Osmani authored
5  README.md
Source Rendered
... ...
@@ -1 +1,4 @@
1  
-A Spine.js demo application that uses the bit.ly API to help shorten URLs and provide a means to archive them offline. This demo was created as part of a Spine introduction that will be up on http://addyosmani.com shortly.
  1
+Update: Many thanks to Spine author Alex MacCraw for heavily refactoring the code written. It now employs some Spine best practices.
  2
+
  3
+A Spine.js demo application that uses the bit.ly API to help shorten URLs and provide a means to archive them offline. This demo was created as part of a Spine introduction that will be up on http://addyosmani.com shortly.
  4
+
164  _archive/css/application.css
... ...
@@ -0,0 +1,164 @@
  1
+html, body {
  2
+  margin: 0;
  3
+  padding: 0;
  4
+}
  5
+
  6
+body {
  7
+  font-family: "Helvetica Neue", helvetica, arial, sans-serif;
  8
+  font-size: 14px;
  9
+  line-height: 1.4em;
  10
+  background: #eeeeee;
  11
+  color: #333333;
  12
+}
  13
+
  14
+#views {
  15
+  width: 520px;
  16
+  margin: 0 auto 40px auto;
  17
+  background: white;
  18
+  
  19
+  -moz-box-shadow: rgba(0, 0, 0, 0.2) 0 2px 6px 0;
  20
+  -webkit-box-shadow: rgba(0, 0, 0, 0.2) 0 2px 6px 0;
  21
+  -o-box-shadow: rgba(0, 0, 0, 0.2) 0 2px 6px 0;
  22
+  box-shadow: rgba(0, 0, 0, 0.2) 0 2px 6px 0;
  23
+  
  24
+  -moz-border-radius: 0 0 5px 5px;
  25
+  -o-border-radius: 0 0 5px 5px;
  26
+  -webkit-border-radius: 0 0 5px 5px;
  27
+  border-radius: 0 0 5px 5px;
  28
+}
  29
+
  30
+#tasks {
  31
+  padding: 20px;
  32
+}
  33
+
  34
+#tasks h1 {
  35
+  font-size: 36px;
  36
+  font-weight: bold;
  37
+  text-align: center;
  38
+  padding: 0 0 10px 0;
  39
+}
  40
+
  41
+#tasks input[type="text"] {
  42
+  width: 466px;
  43
+  font-size: 24px;
  44
+  font-family: inherit;
  45
+  line-height: 1.4em;
  46
+  border: 0;
  47
+  outline: none;
  48
+  padding: 6px;
  49
+  border: 1px solid #999999;
  50
+  
  51
+  -moz-box-shadow: rgba(0, 0, 0, 0.2) 0 1px 2px 0 inset;
  52
+  -webkit-box-shadow: rgba(0, 0, 0, 0.2) 0 1px 2px 0 inset;
  53
+  -o-box-shadow: rgba(0, 0, 0, 0.2) 0 1px 2px 0 inset;
  54
+  box-shadow: rgba(0, 0, 0, 0.2) 0 1px 2px 0 inset;
  55
+}
  56
+
  57
+#tasks input::-webkit-input-placeholder {
  58
+  font-style: italic;
  59
+}
  60
+
  61
+#tasks .items {
  62
+  margin: 10px 0;
  63
+  list-style: none;
  64
+}
  65
+
  66
+#tasks .item {
  67
+  padding: 15px 20px 15px 0;
  68
+  position: relative;
  69
+  font-size: 24px;
  70
+  border-bottom: 1px solid #cccccc;
  71
+}
  72
+
  73
+#tasks .item.done span {
  74
+  color: #777777;
  75
+  text-decoration: line-through;
  76
+}
  77
+
  78
+#tasks .item .destroy {
  79
+  position: absolute;
  80
+  right: 10px;
  81
+  top: 16px;
  82
+  display: none;
  83
+  cursor: pointer;
  84
+  width: 20px;
  85
+  height: 20px;
  86
+  background: url(../img/destroy.png) no-repeat center center;
  87
+}
  88
+
  89
+#tasks .item:hover .destroy {
  90
+  display: block;
  91
+}
  92
+
  93
+#tasks .item .edit { display: none; }
  94
+#tasks .item.editing .edit { display: block; }
  95
+#tasks .item.editing .view { display: none; }
  96
+#tasks .item .original { clear:both; font-size:11px;}
  97
+#tasks .item .clicks { font-size: 10px;  display: none;position: absolute; right: 35px; top: 16px; cursor:pointer; background-color:#ffcccc;}
  98
+#tasks .item:hover .clicks{
  99
+	display:block;
  100
+}
  101
+
  102
+#tasks footer {
  103
+  display: block;
  104
+  margin: 20px -20px -20px -20px;
  105
+  overflow: hidden;
  106
+  
  107
+  color: #555555;
  108
+  background: #f4fce8;
  109
+  border-top: 1px solid #ededed;
  110
+  padding: 0 20px;
  111
+  line-height: 36px;
  112
+  
  113
+  -moz-border-radius: 0 0 5px 5px;
  114
+  -o-border-radius: 0 0 5px 5px;
  115
+  -webkit-border-radius: 0 0 5px 5px;
  116
+  border-radius: 0 0 5px 5px;
  117
+}
  118
+
  119
+#tasks .clear {
  120
+  display: block;
  121
+  float: right;
  122
+  line-height: 20px;
  123
+  text-decoration: none;
  124
+  
  125
+  background: rgba(0, 0, 0, 0.1);
  126
+  color: #555555;
  127
+  font-size: 11px;
  128
+  margin-top: 8px;
  129
+  padding: 0 10px 1px;
  130
+  
  131
+  -moz-border-radius: 12px;
  132
+  -webkit-border-radius: 12px;
  133
+  -o-border-radius: 12px;
  134
+  border-radius: 12px;
  135
+  
  136
+  -moz-box-shadow: rgba(0, 0, 0, 0.2) 0 -1px 0 0;
  137
+  -webkit-box-shadow: rgba(0, 0, 0, 0.2) 0 -1px 0 0;
  138
+  -o-box-shadow: rgba(0, 0, 0, 0.2) 0 -1px 0 0;
  139
+  box-shadow: rgba(0, 0, 0, 0.2) 0 -1px 0 0; 
  140
+  
  141
+  cursor: pointer;
  142
+}
  143
+
  144
+#tasks .clear:hover {
  145
+  background: rgba(0, 0, 0, 0.15);
  146
+  -moz-box-shadow: rgba(0, 0, 0, 0.3) 0 -1px 0 0;
  147
+  -webkit-box-shadow: rgba(0, 0, 0, 0.3) 0 -1px 0 0;
  148
+  -o-box-shadow: rgba(0, 0, 0, 0.3) 0 -1px 0 0;
  149
+  box-shadow: rgba(0, 0, 0, 0.3) 0 -1px 0 0;
  150
+}
  151
+
  152
+#tasks .clear:active {
  153
+  position: relative;
  154
+  top: 1px;
  155
+}
  156
+
  157
+#tasks .count span {
  158
+  font-weight: bold;
  159
+}
  160
+
  161
+.bitly-summary span{
  162
+	clear:both;
  163
+	display:block;
  164
+}
0  img/destroy.png → _archive/img/destroy.png
File renamed without changes
242  _archive/index.html
... ...
@@ -0,0 +1,242 @@
  1
+<!DOCTYPE html>
  2
+<html>
  3
+<head> 
  4
+	<title>ShorterUrl</title>	
  5
+  	<script src="js/jquery-1.5.2.min.js" type="text/javascript" charset="utf-8"></script>
  6
+  	<script src="js/jquery.tmpl.js" type="text/javascript" charset="utf-8"></script>
  7
+	<script src="js/spine.tmpl.js" type="text/javascript" charset="utf-8"></script>
  8
+  	<script src="js/spine.min.js" type="text/javascript" charset="utf-8"></script>
  9
+  	<script src="js/spine.route.js" type="text/javascript" charset="utf-8"></script>
  10
+  	<script src="js/json.js" type="text/javascript" charset="utf-8"></script>
  11
+  	<script src="js/store.min.js" type="text/javascript" charset="utf-8"></script>
  12
+  	<script src="js/jquery.bitlydfd.0.1.js" type="text/javascript"></script>
  13
+	<script src="js/spine.model.local.js" type="text/javascript" charset="utf-8"></script>
  14
+	<script src="js/spine.controller.manager.js" type="text/javascript"></script>
  15
+
  16
+	<link rel="stylesheet" type="text/css" href="css/application.css"/>
  17
+
  18
+<script type="text/javascript">
  19
+// Create the Task model.
  20
+var Task = Spine.Model.setup("Task", ["name", "done", "original"]);
  21
+
  22
+Spine.Route.setup();
  23
+
  24
+Task.extend(Spine.Model.Local);
  25
+
  26
+Task.extend({
  27
+  active: function(){
  28
+    return(this.select(function(item){ return !item.done; }));
  29
+  },
  30
+  
  31
+  done: function(){
  32
+    return(this.select(function(item){ return !!item.done; }));    
  33
+  },
  34
+  
  35
+  destroyDone: function(){
  36
+    this.done().forEach(function(rec){ rec.destroy() });
  37
+  }
  38
+});
  39
+
  40
+jQuery(function($){
  41
+	
  42
+
  43
+  window.Tasks = Spine.Controller.create({
  44
+    tag: "li",
  45
+    
  46
+    proxied: ["render", "remove"],
  47
+    
  48
+    events: {
  49
+      "change   input[type=checkbox]": "toggle",
  50
+      "click    .destroy":             "destroy",
  51
+      "dblclick .view":                "edit",
  52
+      "keypress input[type=text]":     "blurOnEnter",
  53
+      "blur     input[type=text]":     "close",
  54
+	  "click 	.clicks":              "viewclicks"
  55
+    },
  56
+    
  57
+    elements: {
  58
+      "input[type=text]": "input",
  59
+      ".item": "wrapper"
  60
+    },
  61
+    
  62
+    init: function(){
  63
+      this.item.bind("update",  this.render);
  64
+      this.item.bind("destroy", this.remove);
  65
+    },
  66
+
  67
+    
  68
+    render: function(){
  69
+      var elements = $("#taskTemplate").tmpl(this.item);
  70
+      this.el.html(elements);
  71
+      this.refreshElements();
  72
+      return this;
  73
+    },
  74
+    
  75
+    toggle: function(){
  76
+      this.item.done = !this.item.done;
  77
+      this.item.save();     
  78
+    },
  79
+    
  80
+    destroy: function(){
  81
+      this.item.destroy();
  82
+    },
  83
+
  84
+	viewclicks: function(){
  85
+	 $('.items').hide();
  86
+ 	var elements = $("#clicksTemplate").tmpl(this.item);
  87
+  	$('#tasks').append(elements);
  88
+  	$('#tasks .clicksView').bitlyDFD({utility:'clicks', shortUrl:this.item.name}); 
  89
+  	this.refreshElements();
  90
+
  91
+      return this;
  92
+	},
  93
+    
  94
+    edit: function(){
  95
+      this.wrapper.addClass("editing");
  96
+      this.input.focus();
  97
+    },
  98
+    
  99
+    blurOnEnter: function(e) {	
  100
+      if (e.keyCode == 13) e.target.blur();
  101
+    },
  102
+    
  103
+    close: function(){
  104
+      this.wrapper.removeClass("editing");
  105
+      this.item.updateAttributes({name: this.input.val()});
  106
+    },
  107
+    
  108
+    remove: function(){
  109
+      this.el.remove();
  110
+    }
  111
+  });
  112
+  
  113
+  window.TaskApp = Spine.Controller.create({
  114
+    el: $("#tasks"),
  115
+    
  116
+    proxied: ["addOne", "addAll", "renderCount"],
  117
+
  118
+    events: {
  119
+      "submit form":   "create",
  120
+      "click  .clear": "clear"
  121
+    },
  122
+
  123
+    elements: {
  124
+      ".items":     "items",
  125
+      ".countVal":  "count",
  126
+      ".clear":     "clear",
  127
+      "form input": "input"
  128
+    },
  129
+    
  130
+    init: function(){
  131
+      Task.bind("create",  this.addOne);
  132
+      Task.bind("refresh", this.addAll);
  133
+      Task.bind("refresh change", this.renderCount);
  134
+      Task.fetch();
  135
+	
  136
+		this.routes({
  137
+	      "/ui/clicks/:id": function(id){
  138
+	        //console.log("/ui/clicks/", id)
  139
+	
  140
+	      },
  141
+		 "" : function(){
  142
+			console.log('catchall');
  143
+			$('.items').show();
  144
+			$('.clicksView').remove();
  145
+		}
  146
+	});
  147
+
  148
+    },
  149
+    
  150
+    addOne: function(task) {
  151
+      var view = Tasks.init({item: task});
  152
+      this.items.append(view.render().el);
  153
+      
  154
+    },
  155
+
  156
+    addAll: function() {
  157
+      Task.each(this.addOne);
  158
+    },
  159
+        
  160
+    create: function(){
  161
+	  var q = this.input;
  162
+	  q.bitlyDFD({utility:'shorten', 
  163
+				   longUrl:this.input.val(), 
  164
+					callback:function(s,l){
  165
+					Task.create({name: s, original:l}
  166
+				 );
  167
+			}
  168
+		});
  169
+
  170
+      this.input.val("");
  171
+      return false;
  172
+    },
  173
+    
  174
+    clear: function(){
  175
+      Task.destroyDone();
  176
+    },
  177
+    
  178
+    renderCount: function(){
  179
+      var active = Task.active().length;
  180
+      this.count.text(active);
  181
+      var inactive = Task.done().length;
  182
+      this.clear[inactive ? "show" : "hide"]();
  183
+    }
  184
+  });
  185
+  
  186
+  window.App = TaskApp.init();
  187
+ 
  188
+
  189
+});
  190
+
  191
+</script>
  192
+</head>
  193
+<body>
  194
+  	
  195
+
  196
+	 <script type="text/x-jquery-tmpl" id="taskTemplate">
  197
+	    <div class="item {{if done}}done{{/if}}">
  198
+	      <div class="view" title="Double click to edit...">
  199
+	        <input type="checkbox" {{if done}}checked="checked"{{/if}}> 
  200
+			
  201
+	        <span>${name}</span> 
  202
+			<span class="original">${original}</span>
  203
+			<a class="clicks" href="#/ui/clicks/${id}">Click Statistics</a>
  204
+			<a class="destroy"></a>
  205
+	      </div>
  206
+
  207
+	      <div class="edit">
  208
+	        <input type="text" value="${name}">
  209
+	      </div>
  210
+	    </div>
  211
+	  </script>
  212
+	
  213
+	<script type="text/x-jquery-tmpl" id="clicksTemplate">
  214
+		<div class="clicksView">
  215
+		 <h2>Click Statistics</h2>
  216
+			<span><strong>Short URL: </strong> <a href="${name}">${name}</a> (<a href="#">Edit Mode</a>)</span><br>
  217
+			<span><strong>Original URL: </strong> <a href="${original}">${original}</a></span>
  218
+			<div class="clicksInfo"></div>
  219
+		
  220
+		</div>
  221
+	</script>
  222
+
  223
+
  224
+
  225
+
  226
+	  <div id="views">
  227
+	    <div id="tasks">
  228
+	      <h1>ShorterUrl</h1>
  229
+
  230
+	      <form>
  231
+	        <input class="inputUrl" type="text" placeholder="Enter the URL you would like shortened">
  232
+	      </form>
  233
+
  234
+	      <div class="items"></div>
  235
+	      <footer>
  236
+	        <a class="clear">Clear entries</a>
  237
+	      </footer>
  238
+	    </div>
  239
+	  </div>
  240
+	
  241
+</body>
  242
+</html>
0  js/jquery-1.5.2.min.js → _archive/js/jquery-1.5.2.min.js
File renamed without changes
0  js/jquery.bitlydfd.0.1.js → _archive/js/jquery.bitlydfd.0.1.js
File renamed without changes
0  js/jquery.tmpl.js → _archive/js/jquery.tmpl.js
File renamed without changes
0  js/json.js → _archive/js/json.js
File renamed without changes
0  js/spine.controller.manager.js → _archive/js/spine.controller.manager.js
File renamed without changes
0  js/spine.list.js → _archive/js/spine.list.js
File renamed without changes
0  js/spine.min.js → _archive/js/spine.min.js
File renamed without changes
0  js/spine.model.ajax.js → _archive/js/spine.model.ajax.js
File renamed without changes
0  js/spine.model.local.js → _archive/js/spine.model.local.js
File renamed without changes
0  js/spine.route.js → _archive/js/spine.route.js
File renamed without changes
0  js/spine.tmpl.js → _archive/js/spine.tmpl.js
File renamed without changes
0  js/store.min.js → _archive/js/store.min.js
File renamed without changes
146  app/application.js
... ...
@@ -0,0 +1,146 @@
  1
+var exports = this;
  2
+
  3
+jQuery(function($){
  4
+  
  5
+  $.fn.toggleDisplay = function(bool){
  6
+    if ( typeof bool == "undefined" ) {
  7
+      bool = !$(this).filter(":first:visible")[0];
  8
+    }
  9
+    return $(this)[bool ? "show" : "hide"]();
  10
+  };
  11
+  
  12
+  exports.Urls = Spine.Controller.create({
  13
+    events: {
  14
+      "click .destroy": "destroy",
  15
+      "click .toggleStats": "toggleStats"
  16
+    },
  17
+    
  18
+    proxied: ["render", "remove"],
  19
+
  20
+    template: function(items){
  21
+      return $("#urlTemplate").tmpl(items);
  22
+    },
  23
+    
  24
+    init: function(){
  25
+      this.item.bind("update",  this.render);
  26
+      this.item.bind("destroy", this.remove);
  27
+    },
  28
+    
  29
+    render: function(){
  30
+      this.el.html(this.template(this.item.reload()));
  31
+      return this;
  32
+    },
  33
+    
  34
+    toggleStats: function(){
  35
+      this.navigate("/stats", this.item.id, true);
  36
+    },
  37
+    
  38
+    remove: function(){
  39
+      this.el.remove();
  40
+    },
  41
+    
  42
+    destroy: function(){
  43
+      this.item.destroy();
  44
+    }
  45
+  });
  46
+
  47
+  exports.UrlsList = Spine.Controller.create({
  48
+    elements: {
  49
+      ".items": "items",
  50
+      "form":   "form",
  51
+      "input":  "input"
  52
+    },
  53
+    
  54
+    events: {
  55
+      "submit form": "create",
  56
+    },
  57
+    
  58
+    proxied: ["render", "addAll", "addOne"],
  59
+    
  60
+    init: function(){
  61
+      Url.bind("create",  this.addOne);
  62
+      Url.bind("refresh", this.addAll);
  63
+    },
  64
+    
  65
+    addOne: function(url){
  66
+      var view = Urls.init({item: url});
  67
+      this.items.append(view.render().el);
  68
+    },
  69
+    
  70
+    addAll: function(){
  71
+      Url.each(this.addOne);
  72
+    },
  73
+        
  74
+    create: function(e){
  75
+      e.preventDefault();
  76
+      var value = this.input.val();
  77
+      
  78
+      if (value)
  79
+        Url.create({long_url: value});
  80
+
  81
+      this.input.val("");
  82
+      this.input.focus();
  83
+    }
  84
+  });
  85
+  
  86
+  exports.Stats = Spine.Controller.create({
  87
+    events: {
  88
+      "click .back": "back"
  89
+    },
  90
+    
  91
+    proxied: ["change", "render"],
  92
+    
  93
+    init: function(){
  94
+      Url.bind("update", this.render);
  95
+    },
  96
+    
  97
+    template: function(items){
  98
+      return $("#statsTemplate").tmpl(items);
  99
+    },
  100
+    
  101
+    render: function(){
  102
+      if ( !this.item ) return;
  103
+      this.el.html(this.template(this.item.reload()));
  104
+    },
  105
+    
  106
+    change: function(item){
  107
+      this.item = item;
  108
+      this.navigate("/stats", item.id);
  109
+      this.item.fetchStats();
  110
+      this.render();
  111
+      this.active();
  112
+    },
  113
+    
  114
+    back: function(){
  115
+      this.navigate("/list", true);
  116
+    }
  117
+  })
  118
+  
  119
+  exports.UrlApp = Spine.Controller.create({
  120
+    el: $("body"),
  121
+    
  122
+    elements: {
  123
+      "#urls": "urlsEl",
  124
+      "#stats": "statsEl"
  125
+    },
  126
+    
  127
+    init: function(){
  128
+      this.list = UrlsList.init({el: this.urlsEl});
  129
+      this.stats = Stats.init({el: this.statsEl});
  130
+      
  131
+      this.manager = Spine.Controller.Manager.init();
  132
+      this.manager.addAll(this.list, this.stats);
  133
+      
  134
+      this.routes({
  135
+        "": function(){ this.list.active() },
  136
+        "/list": function(){ this.list.active() },
  137
+        "/stats/:id": function(id){ this.stats.change(Url.find(id)) }
  138
+      });
  139
+      
  140
+      Url.fetch();
  141
+      Spine.Route.setup();
  142
+    }
  143
+  });
  144
+  
  145
+  exports.App = UrlApp.init();
  146
+});
31  app/models/url.js
... ...
@@ -0,0 +1,31 @@
  1
+var Url = Spine.Model.setup("Url", ["short_url", "long_url", "stats"]);
  2
+
  3
+Url.extend(Spine.Model.Local);
  4
+
  5
+Url.include({
  6
+  validate: function(){
  7
+    if ( !this.long_url )
  8
+      return "long_url required"
  9
+      
  10
+    if ( !this.long_url.match(/:\/\//))
  11
+      this.long_url = "http://" + this.long_url
  12
+  },
  13
+  
  14
+  fetchUrl: function(){
  15
+    if ( !this.short_url )
  16
+      $.bitly(this.long_url, this.proxy(function(result){
  17
+        this.updateAttributes({short_url: result});
  18
+      }));
  19
+  },
  20
+  
  21
+  fetchStats: function(){
  22
+    if ( !this.short_url ) return;
  23
+    $.bitly.stats(this.short_url, this.proxy(function(result){
  24
+      this.updateAttributes({stats: result});
  25
+    }));
  26
+  }
  27
+});
  28
+
  29
+Url.bind("create", function(rec){
  30
+  rec.fetchUrl();
  31
+});
122  css/application.css
@@ -11,6 +11,20 @@ body {
11 11
   color: #333333;
12 12
 }
13 13
 
  14
+a {
  15
+  color: #261A3B;
  16
+  text-decoration: underline;
  17
+  cursor: pointer;
  18
+}
  19
+
  20
+
  21
+h1 {
  22
+  font-size: 36px;
  23
+  font-weight: bold;
  24
+  text-align: center;
  25
+  padding: 0 0 10px 0;
  26
+}
  27
+
14 28
 #views {
15 29
   width: 520px;
16 30
   margin: 0 auto 40px auto;
@@ -27,18 +41,15 @@ body {
27 41
   border-radius: 0 0 5px 5px;
28 42
 }
29 43
 
30  
-#tasks {
31  
-  padding: 20px;
  44
+#views > *:not(.active) {
  45
+  display: none;
32 46
 }
33 47
 
34  
-#tasks h1 {
35  
-  font-size: 36px;
36  
-  font-weight: bold;
37  
-  text-align: center;
38  
-  padding: 0 0 10px 0;
  48
+#urls, #stats {
  49
+  padding: 20px;
39 50
 }
40 51
 
41  
-#tasks input[type="text"] {
  52
+#urls input[type="text"] {
42 53
   width: 466px;
43 54
   font-size: 24px;
44 55
   font-family: inherit;
@@ -54,28 +65,23 @@ body {
54 65
   box-shadow: rgba(0, 0, 0, 0.2) 0 1px 2px 0 inset;
55 66
 }
56 67
 
57  
-#tasks input::-webkit-input-placeholder {
  68
+#urls input::-webkit-input-placeholder {
58 69
   font-style: italic;
59 70
 }
60 71
 
61  
-#tasks .items {
  72
+#urls .items {
62 73
   margin: 10px 0;
63 74
   list-style: none;
64 75
 }
65 76
 
66  
-#tasks .item {
  77
+#urls .item {
67 78
   padding: 15px 20px 15px 0;
68 79
   position: relative;
69 80
   font-size: 24px;
70 81
   border-bottom: 1px solid #cccccc;
71 82
 }
72 83
 
73  
-#tasks .item.done span {
74  
-  color: #777777;
75  
-  text-decoration: line-through;
76  
-}
77  
-
78  
-#tasks .item .destroy {
  84
+#urls .item .destroy {
79 85
   position: absolute;
80 86
   right: 10px;
81 87
   top: 16px;
@@ -83,82 +89,28 @@ body {
83 89
   cursor: pointer;
84 90
   width: 20px;
85 91
   height: 20px;
86  
-  background: url(../img/destroy.png) no-repeat center center;
87  
-}
88  
-
89  
-#tasks .item:hover .destroy {
90  
-  display: block;
91  
-}
92  
-
93  
-#tasks .item .edit { display: none; }
94  
-#tasks .item.editing .edit { display: block; }
95  
-#tasks .item.editing .view { display: none; }
96  
-#tasks .item .original { clear:both; font-size:11px;}
97  
-#tasks .item .clicks { font-size: 10px;  display: none;position: absolute; right: 35px; top: 16px; cursor:pointer; background-color:#ffcccc;}
98  
-#tasks .item:hover .clicks{
99  
-	display:block;
  92
+  background: url(../images/destroy.png) no-repeat center center;
100 93
 }
101 94
 
102  
-#tasks footer {
  95
+#urls .item:hover .destroy {
103 96
   display: block;
104  
-  margin: 20px -20px -20px -20px;
105  
-  overflow: hidden;
106  
-  
107  
-  color: #555555;
108  
-  background: #f4fce8;
109  
-  border-top: 1px solid #ededed;
110  
-  padding: 0 20px;
111  
-  line-height: 36px;
112  
-  
113  
-  -moz-border-radius: 0 0 5px 5px;
114  
-  -o-border-radius: 0 0 5px 5px;
115  
-  -webkit-border-radius: 0 0 5px 5px;
116  
-  border-radius: 0 0 5px 5px;
117 97
 }
118 98
 
119  
-#tasks .clear {
120  
-  display: block;
121  
-  float: right;
122  
-  line-height: 20px;
123  
-  text-decoration: none;
124  
-  
125  
-  background: rgba(0, 0, 0, 0.1);
126  
-  color: #555555;
127  
-  font-size: 11px;
128  
-  margin-top: 8px;
129  
-  padding: 0 10px 1px;
130  
-  
131  
-  -moz-border-radius: 12px;
132  
-  -webkit-border-radius: 12px;
133  
-  -o-border-radius: 12px;
134  
-  border-radius: 12px;
135  
-  
136  
-  -moz-box-shadow: rgba(0, 0, 0, 0.2) 0 -1px 0 0;
137  
-  -webkit-box-shadow: rgba(0, 0, 0, 0.2) 0 -1px 0 0;
138  
-  -o-box-shadow: rgba(0, 0, 0, 0.2) 0 -1px 0 0;
139  
-  box-shadow: rgba(0, 0, 0, 0.2) 0 -1px 0 0; 
140  
-  
  99
+#urls .item .toggleStats {
  100
+  position: absolute;
  101
+  right: 40px;
  102
+  top: 16px;
  103
+  display: none;
141 104
   cursor: pointer;
  105
+  width: 20px;
  106
+  height: 20px;
  107
+  background: url(../images/destroy.png) no-repeat center center;
142 108
 }
143 109
 
144  
-#tasks .clear:hover {
145  
-  background: rgba(0, 0, 0, 0.15);
146  
-  -moz-box-shadow: rgba(0, 0, 0, 0.3) 0 -1px 0 0;
147  
-  -webkit-box-shadow: rgba(0, 0, 0, 0.3) 0 -1px 0 0;
148  
-  -o-box-shadow: rgba(0, 0, 0, 0.3) 0 -1px 0 0;
149  
-  box-shadow: rgba(0, 0, 0, 0.3) 0 -1px 0 0;
150  
-}
151  
-
152  
-#tasks .clear:active {
153  
-  position: relative;
154  
-  top: 1px;
155  
-}
156  
-
157  
-#tasks .count span {
158  
-  font-weight: bold;
  110
+#urls .item:hover .toggleStats {
  111
+  display: block;
159 112
 }
160 113
 
161  
-.bitly-summary span{
162  
-	clear:both;
163  
-	display:block;
  114
+#urls .item .stats {
  115
+  display: none;
164 116
 }
BIN  images/destroy.png
297  index.html
... ...
@@ -1,242 +1,79 @@
1 1
 <!DOCTYPE html>
2 2
 <html>
3  
-<head> 
4  
-	<title>ShorterUrl</title>	
5  
-  	<script src="js/jquery-1.5.2.min.js" type="text/javascript" charset="utf-8"></script>
6  
-  	<script src="js/jquery.tmpl.js" type="text/javascript" charset="utf-8"></script>
7  
-	<script src="js/spine.tmpl.js" type="text/javascript" charset="utf-8"></script>
8  
-  	<script src="js/spine.min.js" type="text/javascript" charset="utf-8"></script>
9  
-  	<script src="js/spine.route.js" type="text/javascript" charset="utf-8"></script>
10  
-  	<script src="js/json.js" type="text/javascript" charset="utf-8"></script>
11  
-  	<script src="js/store.min.js" type="text/javascript" charset="utf-8"></script>
12  
-  	<script src="js/jquery.bitlydfd.0.1.js" type="text/javascript"></script>
13  
-	<script src="js/spine.model.local.js" type="text/javascript" charset="utf-8"></script>
14  
-	<script src="js/spine.controller.manager.js" type="text/javascript"></script>
  3
+<head>
  4
+  <link rel="stylesheet" href="css/application.css" type="text/css" charset="utf-8">
15 5
 
16  
-	<link rel="stylesheet" type="text/css" href="css/application.css"/>
  6
+  <script src="lib/json2.js" type="text/javascript" charset="utf-8"></script>
  7
+  <script src="lib/jquery.js" type="text/javascript" charset="utf-8"></script>
  8
+  <script src="lib/jquery.tmpl.js" type="text/javascript" charset="utf-8"></script>
  9
+  <script src="lib/jquery.bitly.js" type="text/javascript" charset="utf-8"></script>
17 10
 
18  
-<script type="text/javascript">
19  
-// Create the Task model.
20  
-var Task = Spine.Model.setup("Task", ["name", "done", "original"]);
  11
+  <script src="lib/spine.js" type="text/javascript" charset="utf-8"></script>
  12
+  <script src="lib/spine.model.local.js" type="text/javascript" charset="utf-8"></script>
  13
+  <script src="lib/spine.controller.manager.js" type="text/javascript" charset="utf-8"></script>
  14
+  <script src="lib/spine.route.js" type="text/javascript" charset="utf-8"></script>
21 15
 
22  
-Spine.Route.setup();
23  
-
24  
-Task.extend(Spine.Model.Local);
25  
-
26  
-Task.extend({
27  
-  active: function(){
28  
-    return(this.select(function(item){ return !item.done; }));
29  
-  },
30  
-  
31  
-  done: function(){
32  
-    return(this.select(function(item){ return !!item.done; }));    
33  
-  },
  16
+  <script src="app/models/url.js" type="text/javascript" charset="utf-8"></script>
  17
+  <script src="app/application.js" type="text/javascript" charset="utf-8"></script>
34 18
   
35  
-  destroyDone: function(){
36  
-    this.done().forEach(function(rec){ rec.destroy() });
37  
-  }
38  
-});
39  
-
40  
-jQuery(function($){
41  
-	
42  
-
43  
-  window.Tasks = Spine.Controller.create({
44  
-    tag: "li",
45  
-    
46  
-    proxied: ["render", "remove"],
47  
-    
48  
-    events: {
49  
-      "change   input[type=checkbox]": "toggle",
50  
-      "click    .destroy":             "destroy",
51  
-      "dblclick .view":                "edit",
52  
-      "keypress input[type=text]":     "blurOnEnter",
53  
-      "blur     input[type=text]":     "close",
54  
-	  "click 	.clicks":              "viewclicks"
55  
-    },
56  
-    
57  
-    elements: {
58  
-      "input[type=text]": "input",
59  
-      ".item": "wrapper"
60  
-    },
61  
-    
62  
-    init: function(){
63  
-      this.item.bind("update",  this.render);
64  
-      this.item.bind("destroy", this.remove);
65  
-    },
66  
-
67  
-    
68  
-    render: function(){
69  
-      var elements = $("#taskTemplate").tmpl(this.item);
70  
-      this.el.html(elements);
71  
-      this.refreshElements();
72  
-      return this;
73  
-    },
74  
-    
75  
-    toggle: function(){
76  
-      this.item.done = !this.item.done;
77  
-      this.item.save();     
78  
-    },
79  
-    
80  
-    destroy: function(){
81  
-      this.item.destroy();
82  
-    },
83  
-
84  
-	viewclicks: function(){
85  
-	 $('.items').hide();
86  
- 	var elements = $("#clicksTemplate").tmpl(this.item);
87  
-  	$('#tasks').append(elements);
88  
-  	$('#tasks .clicksView').bitlyDFD({utility:'clicks', shortUrl:this.item.name}); 
89  
-  	this.refreshElements();
90  
-
91  
-      return this;
92  
-	},
93  
-    
94  
-    edit: function(){
95  
-      this.wrapper.addClass("editing");
96  
-      this.input.focus();
97  
-    },
98  
-    
99  
-    blurOnEnter: function(e) {	
100  
-      if (e.keyCode == 13) e.target.blur();
101  
-    },
102  
-    
103  
-    close: function(){
104  
-      this.wrapper.removeClass("editing");
105  
-      this.item.updateAttributes({name: this.input.val()});
106  
-    },
107  
-    
108  
-    remove: function(){
109  
-      this.el.remove();
110  
-    }
111  
-  });
  19
+  <script type="text/x-jquery-tmpl" id="urlTemplate">
  20
+    <div class="item">
  21
+      <div class="show">
  22
+        <span class="short">
  23
+          ${long_url}
  24
+        </span>
  25
+      
  26
+        <span class="long">
  27
+          {{if short_url}}
  28
+            <a href="${short_url}">${short_url}</a>
  29
+          {{else}}
  30
+            Generating...
  31
+          {{/if}}
  32
+        </span>
  33
+      
  34
+        <a class="toggleStats"></a>
  35
+        <a class="destroy"></a>
  36
+      </div>
  37
+    </div>
  38
+  </script>  
112 39
   
113  
-  window.TaskApp = Spine.Controller.create({
114  
-    el: $("#tasks"),
115  
-    
116  
-    proxied: ["addOne", "addAll", "renderCount"],
117  
-
118  
-    events: {
119  
-      "submit form":   "create",
120  
-      "click  .clear": "clear"
121  
-    },
122  
-
123  
-    elements: {
124  
-      ".items":     "items",
125  
-      ".countVal":  "count",
126  
-      ".clear":     "clear",
127  
-      "form input": "input"
128  
-    },
129  
-    
130  
-    init: function(){
131  
-      Task.bind("create",  this.addOne);
132  
-      Task.bind("refresh", this.addAll);
133  
-      Task.bind("refresh change", this.renderCount);
134  
-      Task.fetch();
135  
-	
136  
-		this.routes({
137  
-	      "/ui/clicks/:id": function(id){
138  
-	        //console.log("/ui/clicks/", id)
139  
-	
140  
-	      },
141  
-		 "" : function(){
142  
-			console.log('catchall');
143  
-			$('.items').show();
144  
-			$('.clicksView').remove();
145  
-		}
146  
-	});
147  
-
148  
-    },
  40
+  <script type="text/x-jquery-tmpl" id="statsTemplate">  
  41
+    <div class="stats">
  42
+      <a class="back">Back</a>
149 43
     
150  
-    addOne: function(task) {
151  
-      var view = Tasks.init({item: task});
152  
-      this.items.append(view.render().el);
  44
+      <h1>${long_url}</h1>
153 45
       
154  
-    },
155  
-
156  
-    addAll: function() {
157  
-      Task.each(this.addOne);
158  
-    },
159  
-        
160  
-    create: function(){
161  
-	  var q = this.input;
162  
-	  q.bitlyDFD({utility:'shorten', 
163  
-				   longUrl:this.input.val(), 
164  
-					callback:function(s,l){
165  
-					Task.create({name: s, original:l}
166  
-				 );
167  
-			}
168  
-		});
169  
-
170  
-      this.input.val("");
171  
-      return false;
172  
-    },
173  
-    
174  
-    clear: function(){
175  
-      Task.destroyDone();
176  
-    },
177  
-    
178  
-    renderCount: function(){
179  
-      var active = Task.active().length;
180  
-      this.count.text(active);
181  
-      var inactive = Task.done().length;
182  
-      this.clear[inactive ? "show" : "hide"]();
183  
-    }
184  
-  });
185  
-  
186  
-  window.App = TaskApp.init();
187  
- 
188  
-
189  
-});
190  
-
191  
-</script>
  46
+      <h2 class="short">
  47
+        {{if short_url}}
  48
+          <a href="${short_url}">${short_url}</a>
  49
+        {{else}}
  50
+          Generating...
  51
+        {{/if}}
  52
+      </h2>
  53
+    
  54
+      {{if stats}}
  55
+        <p>Global clicks: ${stats.global_clicks}</p>
  56
+        <p>User clicks: ${stats.user_clicks}</p>
  57
+      {{else}}
  58
+        Fetching...
  59
+      {{/if}}
  60
+    </div>
  61
+  </script>
192 62
 </head>
193 63
 <body>
194  
-  	
195  
-
196  
-	 <script type="text/x-jquery-tmpl" id="taskTemplate">
197  
-	    <div class="item {{if done}}done{{/if}}">
198  
-	      <div class="view" title="Double click to edit...">
199  
-	        <input type="checkbox" {{if done}}checked="checked"{{/if}}> 
200  
-			
201  
-	        <span>${name}</span> 
202  
-			<span class="original">${original}</span>
203  
-			<a class="clicks" href="#/ui/clicks/${id}">Click Statistics</a>
204  
-			<a class="destroy"></a>
205  
-	      </div>
206  
-
207  
-	      <div class="edit">
208  
-	        <input type="text" value="${name}">
209  
-	      </div>
210  
-	    </div>
211  
-	  </script>
212  
-	
213  
-	<script type="text/x-jquery-tmpl" id="clicksTemplate">
214  
-		<div class="clicksView">
215  
-		 <h2>Click Statistics</h2>
216  
-			<span><strong>Short URL: </strong> <a href="${name}">${name}</a> (<a href="#">Edit Mode</a>)</span><br>
217  
-			<span><strong>Original URL: </strong> <a href="${original}">${original}</a></span>
218  
-			<div class="clicksInfo"></div>
219  
-		
220  
-		</div>
221  
-	</script>
222  
-
223  
-
224  
-
225  
-
226  
-	  <div id="views">
227  
-	    <div id="tasks">
228  
-	      <h1>ShorterUrl</h1>
229  
-
230  
-	      <form>
231  
-	        <input class="inputUrl" type="text" placeholder="Enter the URL you would like shortened">
232  
-	      </form>
233  
-
234  
-	      <div class="items"></div>
235  
-	      <footer>
236  
-	        <a class="clear">Clear entries</a>
237  
-	      </footer>
238  
-	    </div>
239  
-	  </div>
240  
-	
  64
+  <div id="views">
  65
+    <div id="urls">
  66
+      <h1>Bitly Generator</h1>
  67
+      
  68
+      <form>
  69
+        <input type="text" placeholder="Enter a URL">
  70
+      </form>
  71
+      
  72
+      <div class="items"></div>
  73
+    </div>
  74
+    
  75
+    <div id="stats">
  76
+    </div>
  77
+  </div>
241 78
 </body>
242  
-</html>
  79
+</html>
32  lib/jquery.bitly.js
... ...
@@ -0,0 +1,32 @@
  1
+(function($){
  2
+  
  3
+  var defaults = {
  4
+    version:    "3.0",
  5
+    login:      "legacye",
  6
+    apiKey:     "R_32f60d09cccde1f266bcba8c242bfb5a",
  7
+    history:    "0",
  8
+    format:     "json"
  9
+  };
  10
+
  11
+  $.bitly = function( url, callback, params ) {
  12
+    if ( !url || !callback ) throw("url and callback required");
  13
+    
  14
+    var params = $.extend( defaults, params );
  15
+    params.longUrl = url;
  16
+    
  17
+    return $.getJSON("http://api.bit.ly/shorten?callback=?", params, function(data, status, xhr){
  18
+      callback(data.results[params.longUrl].shortUrl, data.results[params.longUrl], data);
  19
+    });
  20
+  };
  21
+  
  22
+  $.bitly.stats = function( url, callback, params ) {
  23
+    if ( !url || !callback ) throw("url and callback required");
  24
+    
  25
+    var params = $.extend( defaults, params );
  26
+    params.shortUrl = url;
  27
+    
  28
+    return $.getJSON("http://api.bitly.com/v3/clicks?callback=?", params, function(data, status, xhr){
  29
+      callback(data.data.clicks[0], data);
  30
+    });
  31
+  };
  32
+})(jQuery);
8,316  lib/jquery.js
8316 additions, 0 deletions not shown
503  lib/jquery.tmpl.js
... ...
@@ -0,0 +1,503 @@
  1
+/*!
  2
+ * jQuery Templates Plugin
  3
+ * http://github.com/jquery/jquery-tmpl
  4
+ *
  5
+ * Copyright Software Freedom Conservancy, Inc.
  6
+ * Dual licensed under the MIT or GPL Version 2 licenses.
  7
+ * http://jquery.org/license
  8
+ */
  9
+(function( jQuery, undefined ){
  10
+	var oldManip = jQuery.fn.domManip, tmplItmAtt = "_tmplitem", htmlExpr = /^[^<]*(<[\w\W]+>)[^>]*$|\{\{\! /,
  11
+		newTmplItems = {}, wrappedItems = {}, appendToTmplItems, topTmplItem = { key: 0, data: {} }, itemKey = 0, cloneIndex = 0, stack = [];
  12
+
  13
+	function newTmplItem( options, parentItem, fn, data ) {
  14
+		// Returns a template item data structure for a new rendered instance of a template (a 'template item').
  15
+		// The content field is a hierarchical array of strings and nested items (to be
  16
+		// removed and replaced by nodes field of dom elements, once inserted in DOM).
  17
+		var newItem = {
  18
+			data: data || (parentItem ? parentItem.data : {}),
  19
+			_wrap: parentItem ? parentItem._wrap : null,
  20
+			tmpl: null,
  21
+			parent: parentItem || null,
  22
+			nodes: [],
  23
+			calls: tiCalls,
  24
+			nest: tiNest,
  25
+			wrap: tiWrap,
  26
+			html: tiHtml,
  27
+			update: tiUpdate
  28
+		};
  29
+		if ( options ) {
  30
+			jQuery.extend( newItem, options, { nodes: [], parent: parentItem } );
  31
+		}
  32
+		if ( fn ) {
  33
+			// Build the hierarchical content to be used during insertion into DOM
  34
+			newItem.tmpl = fn;
  35
+			newItem._ctnt = newItem._ctnt || newItem.tmpl( jQuery, newItem );
  36
+			newItem.key = ++itemKey;
  37
+			// Keep track of new template item, until it is stored as jQuery Data on DOM element
  38
+			(stack.length ? wrappedItems : newTmplItems)[itemKey] = newItem;
  39
+		}
  40
+		return newItem;
  41