Browse Source

Login, preview, enhancements, and fixes

Markus Ochel 12 years ago
parent
commit
dce30d7db8

+ 1 - 0
.gitignore

@@ -3,3 +3,4 @@
 *.sublime-*
 packages
 node_modules
+old-data

+ 1 - 0
admin/controllers/authors.coffee

@@ -137,6 +137,7 @@ class AuthorList extends Spine.Controller
 
   filter: (@filterObj) =>
     @render()
+    @el.scrollTop(0, 0)
 
 
 class Authors extends Spine.Stack

+ 1 - 0
admin/controllers/blocks.coffee

@@ -141,6 +141,7 @@ class BlockList extends Spine.Controller
 
   filter: (@filterObj) =>
     @render()
+    @el.scrollTop(0, 0)
 
 
 class Blocks extends Spine.Stack

+ 1 - 0
admin/controllers/collections.coffee

@@ -152,6 +152,7 @@ class CollectionList extends Spine.Controller
 
   filter: (@filterObj) =>
     @render()
+    @el.scrollTop(0, 0)
 
 
 class Collections extends Spine.Stack

+ 1 - 0
admin/controllers/contacts.coffee

@@ -102,6 +102,7 @@ class ContactList extends Spine.Controller
 
   filter: (@filterObj) =>
     @render()
+    @el.scrollTop(0, 0)
 
 
 class Contacts extends Spine.Stack

+ 24 - 3
admin/controllers/dashboard.coffee

@@ -3,6 +3,7 @@ Spine       = require('spine/core')
 templates   = require('duality/templates')
 
 Essay       = require('models/essay')
+Scene       = require('models/scene')
 
 
 class DashboardOne extends Spine.Controller
@@ -12,12 +13,32 @@ class DashboardOne extends Spine.Controller
     super
     # @active @render
     Essay.bind 'change refresh', @render
+    Scene.bind 'change refresh', @render
+    Spine.bind 'filterbox:change', @filter
 
   render: =>
     context = 
-      drafts: Essay.findAllByAttribute('published', false)
-    @el.html templates.render('dashboard.html', {}, context)
-    @
+      essays: Essay.select(@selectFilter)
+      scenes: Scene.select(@selectFilter)
+    @html templates.render('dashboard.html', {}, context)
+
+  selectFilter: (item) =>
+    query = @filterObj?.query.toLowerCase()
+    siteId = @filterObj?.siteId.toLowerCase()
+    matchedQuery = query and item.title.toLowerCase().indexOf(query) isnt -1
+    matchedSite = siteId and item.site is siteId
+    if query and siteId
+      matchedQuery and matchedSite and not item.published
+    else if query
+      matchedQuery and not item.published
+    else if siteId
+      matchedSite and not item.published
+    else
+      not item.published
+
+  filter: (@filterObj) =>
+    @render()
+    @el.scrollTop(0, 0)
 
 
 class Dashboard extends Spine.Stack

+ 13 - 5
admin/controllers/essays.coffee

@@ -9,6 +9,7 @@ require('lib/jquery-xdomainajax')
 
 MultiSelectUI = require('controllers/ui/multi-select')
 FileUploadUI  = require('controllers/ui/file-upload')
+PreviewUI     = require('controllers/ui/preview')
 
 Essay       = require('models/essay')
 Author      = require('models/author')
@@ -127,14 +128,17 @@ class EssayForm extends Spine.Controller
     if @form.hasClass('fullscreen')
       @form.removeClass('fullscreen')
       @fullscreenButton.html @fullscreenButtonText
+      @previewUI?.release()
     else
       @form.addClass('fullscreen')
       @fullscreenButton.html "< Exit #{@fullscreenButtonText}"
+      @previewUI = new PreviewUI(@formBody)
+      @form.append @previewUI.el
 
   import: (e) =>
     # For importing old HTML to Markdown directly from old location
     e?.preventDefault()
-    url = $.trim prompt("Paste a URL from #{@formSite.val()}")
+    url = $.trim prompt("Paste a URL from #{@formSite.val()}", @item.old_url or '')
     if url
       $.ajax
         type: 'GET'
@@ -166,10 +170,13 @@ class EssayForm extends Spine.Controller
             markdown = reMarker.render($content.html())
             @formBody.val(markdown)
 
-          @formTitle.val($title.text()) if $title
-          @form.find('input[name=slug]').val($title.attr('href').replace("http://#{@formSite.val()}", '')) if $title
-          @formAuthorId.val($author.text()) if $author
-          @form.find('input[name=published_at]').val($date.text()) if $date
+          if not @item.old_url
+            @formTitle.val($title.text()) if $title
+            $slug = @form.find('input[name=slug]')
+            unless slug.val()
+              $slug.val($title.attr('href').replace('www.', '').replace("http://#{@formSite.val().replace('www.', '')}", '')) if $title
+            @formAuthorId.val($author.text()) if $author
+            @form.find('input[name=published_at]').val($date.text()) if $date
 
   save: (e) ->
     e.preventDefault()
@@ -236,6 +243,7 @@ class EssayList extends Spine.Controller
 
   filter: (@filterObj) =>
     @render()
+    @el.scrollTop(0, 0)
 
 
 class Essays extends Spine.Stack

+ 1 - 1
admin/controllers/filter-box.coffee

@@ -20,7 +20,7 @@ class FilterBox extends Spine.Controller
   render: =>
     context = 
       sites: Site.all()
-    @el.html templates.render('filter-box.html', {}, context)
+    @html templates.render('filter-box.html', {}, context)
     @setup()
     @
 

+ 54 - 27
admin/controllers/index.coffee

@@ -23,26 +23,50 @@ Sponsor     = require('models/sponsor')
 class App extends Spine.Controller
   
   constructor: ->
-    super    
-    @checkSession()
+    super
+    @mainNav = new MainNav
+    @append @mainNav
+    @initApp()
 
-  checkSession: ->
+  setupSession: ->
+    session.on 'change', @checkRole
+    
     session.info (err, info) =>
-      if 'manager' in info.userCtx.roles
-        @startApp()
-      else
-        username = 'evita'
-        pass = 'n3wst@rt'
-        session.login username, pass, (err, resp) =>
-          if err
-            alert "Error logging in as #{username}: #{err}"
-          else
-            if 'manager' in resp.roles
-              @startApp()
-            else
-              alert "User #{username} does not have permission"
-
-  startApp: ->
+      @checkRole info.userCtx
+
+  checkRole: (userCtx) =>
+    if 'manager' in userCtx.roles
+      @mainNav.hideLogin()
+      @startApp() unless @appStarted
+      @loadData() unless @dataLoaded
+    else
+      @mainNav.showLogin()
+      @unloadData() if @dataLoaded
+      @endApp() if @appStarted
+
+  initApp: =>
+    @setupSession()
+
+    @mainStack = new MainStack
+    @helpUI    = new HelpUI
+    Spine.Route.setup()
+
+    @doOtherStuff()
+
+  startApp: =>
+    @loadData() unless @dataLoaded
+    unless @appStarted
+      @append @mainStack, @helpUI
+      @navigate('/')
+      @appStarted = true
+
+  endApp: =>
+    if @appStarted
+      @mainStack.el.remove()
+      @helpUI.el.remove()
+      @appStarted = false
+
+  loadData: =>
     # Load data models
     Site.fetch()
     Author.fetch()
@@ -52,16 +76,19 @@ class App extends Spine.Controller
     Block.fetch()
     Contact.fetch()
     Sponsor.fetch()
+    @dataLoaded = true
 
-    @mainNav   = new MainNav
-    @mainStack = new MainStack
-    @helpUI    = new HelpUI
-
-    @append @mainNav, @mainStack, @helpUI
-
-    Spine.Route.setup()
-
-    @doOtherStuff()
+  unloadData: =>
+    # Empty data from client models
+    Site.deleteAll()
+    Author.deleteAll()
+    Collection.deleteAll()
+    Essay.deleteAll()
+    Scene.deleteAll()
+    Block.deleteAll()
+    Contact.deleteAll()
+    Sponsor.deleteAll()
+    @dataLoaded = false
 
   doOtherStuff: ->
     # Use the fastclick module for touch devices.

+ 47 - 2
admin/controllers/main-nav.coffee

@@ -1,19 +1,29 @@
 Spine       = require('spine/core')
 $           = Spine.$
 templates   = require('duality/templates')
+session     = require('session')
 
 
 class MainNav extends Spine.Controller
   className: 'main navbar'
 
+  elements:
+    'form.login':             'loginForm'
+    'input[name=username]':   'formUsername'
+    'input[name=password]':   'formPassword'
+
+  events:
+    'submit form.login':      'login'
+    'click .logout-button':   'logout'
+
   constructor: ->
     super
+    @granted = false
     @render()
     @setup()
 
   render: =>
-    @el.html templates.render('main-nav.html', {}, {})
-    @
+    @html templates.render('main-nav.html', {}, {})
 
   setup: =>
     links = @el.find('li a')
@@ -21,5 +31,40 @@ class MainNav extends Spine.Controller
       links.removeClass('active')
       $(@).addClass('active')
 
+  showLogin: =>
+    @resetLoginForm()
+    @el.addClass('show-login')
+
+  hideLogin: =>
+    @el.removeClass('show-login')
+    @resetLoginForm(false)
+
+  login: (e) =>
+    e.preventDefault()
+    username = $.trim @formUsername.val()
+    password = $.trim @formPassword.val()
+    session.login username, password, (err, resp) =>
+      if err
+        alert "Oops! #{err}"
+      else
+        if 'manager' in resp.roles
+          @granted = true
+        else
+          alert "User #{username} does not have permission"
+      @resetLoginForm()
+
+  logout: =>
+    session.logout (err, resp) =>
+      if err
+        alert "Error signing out: #{err}"
+      else
+        @granted = false
+      @resetLoginForm()
+
+  resetLoginForm: (focus = true) =>
+    @formUsername.val('')
+    @formPassword.val('')
+    @formUsername.focus() if focus
+
 
 module.exports = MainNav

+ 10 - 6
admin/controllers/scenes.coffee

@@ -134,7 +134,7 @@ class SceneForm extends Spine.Controller
   import: (e) =>
     # For importing old HTML to Markdown directly from old location
     e?.preventDefault()
-    url = $.trim prompt("Paste a URL from #{@formSite.val()}")
+    url = $.trim prompt("Paste a URL from #{@formSite.val()}", @item.old_url or '')
     if url
       $.ajax
         type: 'GET'
@@ -165,11 +165,14 @@ class SceneForm extends Spine.Controller
             reMarker = new reMarked(options)
             markdown = reMarker.render($content.html())
             @formBody.val(markdown)
-
-          @formTitle.val($title.text()) if $title
-          @form.find('input[name=slug]').val($title.attr('href').replace("http://#{@formSite.val()}", '')) if $title
-          @formAuthorId.val($author.text()) if $author
-          @form.find('input[name=published_at]').val($date.text()) if $date
+            
+          if not @item.old_url
+            @formTitle.val($title.text()) if $title
+            $slug = @form.find('input[name=slug]')
+            unless slug.val()
+              $slug.val($title.attr('href').replace('www.', '').replace("http://#{@formSite.val().replace('www.', '')}", '')) if $title
+            @formAuthorId.val($author.text()) if $author
+            @form.find('input[name=published_at]').val($date.text()) if $date
 
   save: (e) ->
     e.preventDefault()
@@ -236,6 +239,7 @@ class SceneList extends Spine.Controller
 
   filter: (@filterObj) =>
     @render()
+    @el.scrollTop(0, 0)
 
 
 class Scenes extends Spine.Stack

+ 1 - 0
admin/controllers/sites.coffee

@@ -111,6 +111,7 @@ class SiteList extends Spine.Controller
 
   filter: (@filterObj) =>
     @render()
+    @el.scrollTop(0, 0)
 
 
 class Sites extends Spine.Stack

+ 1 - 0
admin/controllers/sponsors.coffee

@@ -126,6 +126,7 @@ class SponsorList extends Spine.Controller
 
   filter: (@filterObj) =>
     @render()
+    @el.scrollTop(0, 0)
 
 
 class Sponsors extends Spine.Stack

+ 44 - 0
admin/controllers/ui/preview.coffee

@@ -0,0 +1,44 @@
+Spine       = require('spine/core')
+$           = Spine.$
+Showdown    = require('showdown')
+
+
+class PreviewUI extends Spine.Controller
+  tag: 'div'
+  className: 'ui-preview'
+  live: true
+  showClose: true
+
+  events:
+    'click .close-button':    'close'
+
+  constructor: (@field) ->
+    super
+    @field = $(@field) if typeof @field is String
+    @inner = $('<div class="inner" />')
+    @append @inner
+
+    @md = new Showdown.converter()
+    
+    @render()
+    @setupLivePreview() if @live
+    @setupCloseButton() if @showClose
+    @
+
+  render: ->
+    contentHtml = @md.makeHtml(@field.val())
+    @inner.html contentHtml
+  
+  setupLivePreview: ->
+    @field.on 'keyup', (e) =>
+      @render()
+
+  setupCloseButton: ->
+    @append $('<a class="close-button button small">x</a>')
+
+  close: (e) ->
+    e?.preventDefault()
+    @release()
+
+
+module.exports = PreviewUI

+ 0 - 563
admin/lib/html2markdown.js

@@ -1,563 +0,0 @@
-/**
- * HTML2Markdown - An HTML to Markdown converter.
- * 
- * This implementation uses HTML DOM parsing for conversion. Parsing code was
- * abstracted out in a parsing function which should be easy to remove in favor
- * of other parsing libraries.
- * 
- * Converted MarkDown was tested with ShowDown library for HTML rendering. And
- * it tries to create MarkDown that does not confuse ShowDown when certain
- * combination of HTML tags come together.
- * 
- * @author Himanshu Gilani
- * @author Kates Gasis (original author)
- * 
- */
-
-/**
- * HTML2Markdown
- * @param html - html string to convert
- * @return converted markdown text
- */
-function HTML2Markdown(html, opts) {
-	var logging = false;
-	var nodeList = [];
-	var listTagStack = [];
-	var linkAttrStack = [];
-	var blockquoteStack = [];
-	var preStack = [];
-	
-	var links = [];
-	
-	opts = opts || {};
-	var inlineStyle = opts['inlineStyle'] || false;
-
-	var markdownTags = {
-		"hr": "- - -\n\n",
-		"br": "  \n",
-		"title": "# ",
-		"h1": "# ",
-		"h2": "## ",
-		"h3": "### ",
-		"h4": "#### ",
-		"h5": "##### ",
-		"h6": "###### ",
-		"b": "**",
-		"strong": "**",
-		"i": "_",
-		"em": "_",
-		"dfn": "_",
-		"var": "_",	
-		"cite": "_",
-		"span": " ",
-		"ul": "* ",
-		"ol": "1. ",
-		"dl": "- ",
-		"blockquote": "> "
-	};
-
-	function getListMarkdownTag() {
-		var listItem = "";		
-		if(listTagStack) {
-			for ( var i = 0; i < listTagStack.length - 1; i++) {
-				listItem += "  ";
-			}			
-		}
-		listItem += peek(listTagStack);		
-		return listItem;
-	}
-	
-	function convertAttrs(attrs) {
-		var attributes = {};
-		for(var k in attrs) {
-			var attr = attrs[k];
-			attributes[attr.name] = attr;
-		}
-		return attributes;
-	}
-
-	function peek(list) {
-		if(list && list.length > 0) {
-			return list.slice(-1)[0];	
-		} 
-		return "";		
-	}
-
-	function peekTillNotEmpty(list) {
-		if(!list) {
-			return "";
-		}
-				
-		for(var i = list.length - 1; i>=0; i-- ){
-			if(list[i] != "") {
-				return list[i];
-			} 		
-		}		
-		return "";
-	}
-	
-	function removeIfEmptyTag(start) {
-		var cleaned = false;
-		if(start == peekTillNotEmpty(nodeList)) {
-			while(peek(nodeList) != start) {
-				nodeList.pop();
-			}
-			nodeList.pop();
-			cleaned = true;
-		} 
-		return cleaned;
-	}
-	
-	function sliceText(start) {
-		var text = [];
-		while(nodeList.length > 0 && peek(nodeList) != start) {
-			var t = nodeList.pop();
-			text.unshift(t);
-		}
-		return text.join("");
-	}
-	
-	function block(isEndBlock) {
-		var lastItem = nodeList.pop();
-		if (!lastItem) {
-			return;
-		}
-		
-		if(!isEndBlock) {
-			var block;
-			if(/\s*\n\n\s*$/.test(lastItem)) {
-				lastItem = lastItem.replace(/\s*\n\n\s*$/, "\n\n");
-				block = "";
-			} else if(/\s*\n\s*$/.test(lastItem)) {
-				lastItem = lastItem.replace(/\s*\n\s*$/, "\n");
-				block = "\n";
-			} else if(/\s+$/.test(lastItem)) {				
-				block = "\n\n";
-			} else {
-				block = "\n\n";
-			} 
-			
-			nodeList.push(lastItem);
-			nodeList.push(block);	
-		} else {
-			nodeList.push(lastItem);
-			if(!lastItem.endsWith("\n")) {
-				nodeList.push("\n\n");
-			}
-		}
- 	}
-	
-	function listBlock() {
-		if(nodeList.length > 0) {
-			var li = peek(nodeList);
-
-			if(!li.endsWith("\n")) {
-				nodeList.push("\n");
-			} 
-		} else {
-			nodeList.push("\n");
-		}
-	}
-	
-	try {
-		var dom;
-		if(html) {
-		var e = document.createElement('div');
-			e.innerHTML = html;
-			dom = e;
-		} else {
-			dom = window.document.body;
-		}
-
-		HTMLParser(dom,{
-			start: function(tag, attrs, unary) {
-				tag = tag.toLowerCase();
-				if(logging) {
-					console.log("start: "+ tag);
-				}
-				
-				if(unary && (tag != "br" && tag != "hr" && tag != "img")) {
-					return;
-				}
-				
-				switch (tag) {
-				case "br":
-					nodeList.push(markdownTags[tag]);
-					break;
-				case "hr":
-					block();
-					nodeList.push(markdownTags[tag]);
-					break;
-				case "title":	
-				case "h1":
-				case "h2":
-				case "h3":
-				case "h4":
-				case "h5":
-				case "h6":
-					block();
-					nodeList.push(markdownTags[tag]);
-					break;
-				case "b":
-				case "strong":
-				case "i":
-				case "em":
-				case "dfn": 
-				case "var": 	
-				case "cite":
-					nodeList.push(markdownTags[tag]);
-					break;
-				case "span":
-					if(! /\s+$/.test(peek(nodeList))) {
-						nodeList.push(markdownTags[tag]);	
-					}
-					break;
-				case "p":
-				case "div":				
-				case "td":
-					block();
-					break;
-				case "ul":
-				case "ol":
-				case "dl":	
-					listTagStack.push(markdownTags[tag]);
-					// lists are block elements
-					if(listTagStack.length > 1) {
-						listBlock();
-					} else {						
-						block();
-					}										
-					break;
-				case "li":
-				case "dt":
-					var li = getListMarkdownTag();
-					nodeList.push(li);
-					break;
-				case "a":					
-					var attribs = convertAttrs(attrs);
-					linkAttrStack.push(attribs);
-					nodeList.push("[");
-					break;
-				case "img":
-					var attribs = convertAttrs(attrs);
-					var alt, title, url; 
-					
-					attribs["src"] ? url = getNormalizedUrl(attribs["src"].value) : url = "";
-					if(!url) {
-						break;
-					}
-					
-					attribs['alt'] ? alt = attribs['alt'].value.trim() : alt = "";
-					attribs['title'] ? title = attribs['title'].value.trim() : title = "";
-										
-					// if parent of image tag is nested in anchor tag use inline style
-					if(!inlineStyle && !peekTillNotEmpty(nodeList).startsWith("[")) {						
-						var l = links.indexOf(url);
-						if(l == -1) {
-							links.push(url);
-							l=links.length-1;							 
-						}		
-						
-						block();
-						nodeList.push("![");
-						if(alt!= "") {
-							nodeList.push(alt);
-						} else if (title != null) {
-							nodeList.push(title);
-						} 
-						
-						nodeList.push("][" + l + "]");
-						block();
-					} else {
-						//if image is not a link image then treat images as block elements
-						if(!peekTillNotEmpty(nodeList).startsWith("[")) {
-							block();	
-						}
-						
-						nodeList.push("![" + alt + "](" + url + (title ? " \"" + title + "\"" : "") + ")");
-						
-						if(!peekTillNotEmpty(nodeList).startsWith("[")) {
-							block(true);	
-						}
-					}
-					break;	
-				case "blockquote":
-					block();
-					blockquoteStack.push(markdownTags[tag]);
-					nodeList.push(blockquoteStack.join(""));
-					break;
-				case "pre":
-				case "code":
-					block();
-					preStack.push(true);
-					break;
-				}				
-			},
-			chars: function(text) {			
-				if(preStack.length > 0) {
-					text = "    " + text.replace(/\n/g,"\n    ");
-				} else if(text.trim() != "") {
-					text = text.replace(/\s+/g, " ");
-					
-					var prevText = peekTillNotEmpty(nodeList);
-					if(/\s+$/.test(prevText)) {
-						text = text.replace(/^\s+/g, "");
-					}	
-				} else {
-					nodeList.push("");
-					return;
-				}
-
-				if(logging) {
-					console.log("text: "+ text);
-				}
-				
-				nodeList.push(text);
-			},
-			end: function(tag) {
-				tag = tag.toLowerCase();
-				if(logging) {
-					console.log("end: "+ tag);
-				}
-				switch (tag) {				
-				case "title":	
-				case "h1":
-				case "h2":
-				case "h3":
-				case "h4":
-				case "h5":
-				case "h6":
-					if(!removeIfEmptyTag(markdownTags[tag])) {
-						block(true);
-					}
-					break;
-				case "p":
-				case "div":
-				case "td":
-					while(nodeList.length > 0 && peek(nodeList).trim() == "") {
-						nodeList.pop();
-					}
-					block(true);
-					break;
-				case "b":
-				case "strong":
-				case "i":
-				case "em":
-				case "dfn": 
-				case "var": 	
-				case "cite":
-					if(!removeIfEmptyTag(markdownTags[tag])) {						
-						nodeList.push(sliceText(markdownTags[tag]).trim());
-						nodeList.push(markdownTags[tag]);
-					}
-					break;
-				case "a":
-					var text = sliceText("[");
-					text = text.replace(/\s+/g, " ");					
-					text = text.trim();
-					
-					if(text == "") {
-						nodeList.pop();
-						break;
-					}
-
-					var attrs = linkAttrStack.pop();
-					var url;
-					attrs["href"] &&  attrs["href"].value != "" ? url = getNormalizedUrl(attrs["href"].value) : url = "";
-					
-					if(url == "") {
-						nodeList.pop();
-						nodeList.push(text);
-						break;
-					}
-					
-					nodeList.push(text);
-					
-					if(!inlineStyle && !peek(nodeList).startsWith("!")){
-						var l = links.indexOf(url);
-						if(l == -1) {
-							links.push(url);
-							l=links.length-1;
-						}							
-						nodeList.push("][" + l + "]");
-					} else {
-						if(peek(nodeList).startsWith("!")){
-							var text = nodeList.pop();
-							text = nodeList.pop() + text;
-							block();
-							nodeList.push(text);
-						}
-						
-						var title = attrs["title"];						
-						nodeList.push("](" + url + (title ? " \"" + title.value.trim().replace(/\s+/g, " ") + "\"" : "") + ")");
-						
-						if(peek(nodeList).startsWith("!")){
-							block(true);
-						}
-					}
-					break;					
-				case "ul":
-				case "ol":
-				case "dl":	
-					listBlock();
-					listTagStack.pop();
-					break;
-				case "li":
-				case "dt":
-					var li = getListMarkdownTag();
-					if(!removeIfEmptyTag(li)) {
-						var text = sliceText(li).trim();
-						
-						if(text.startsWith("[![")) {
-							nodeList.pop();							
-							block();
-							nodeList.push(text);
-							block(true);
-						} else {
-							nodeList.push(text);
-							listBlock();
-						}
-					}	
-					break;
-				case "blockquote":
-					blockquoteStack.pop();
-					break;
-				case "pre":
-				case "code":
-					block(true);
-					preStack.pop();	
-					break;
-				case "span":
-					if(peek(nodeList).trim() == "") {
-						nodeList.pop();
-						if(peek(nodeList) == " ") {
-							nodeList.pop();	
-						} else {
-							nodeList.push(markdownTags[tag]);
-						}						
-					} else {
-						var text = nodeList.pop();
-						nodeList.push(text.trim());
-						nodeList.push(markdownTags[tag]);													
-					}
-					break;					
-				case "br":
-				case "hr":
-				case "img":
-				case "table":	
-				case "tr":
-					break;
-				}
-				
-			}
-		}, {"nodesToIgnore": ["script", "noscript", "object", "iframe", "frame", "head", "style", "label"]});
-		
-		if(!inlineStyle) {							
-			for ( var i = 0; i < links.length; i++) {
-				if(i == 0) {
-					var lastItem = nodeList.pop();
-					nodeList.push(lastItem.replace(/\s+$/g, ""));
-					nodeList.push("\n\n[" + i + "]: " + links[i]);
-				} else {
-					nodeList.push("\n[" + i + "]: " + links[i]);	
-				}
-			}
-		}
-	} catch(e) {
-		console.log(e.stack);
-		console.trace();
-	}
-	
-	return nodeList.join("");
-	
-}
-
-function getNormalizedUrl(s) {
-	var urlBase = location.href;
-	var urlDir  = urlBase.replace(/\/[^\/]*$/, '/');
-	var urlPage = urlBase.replace(/#[^\/#]*$/, '');
-
-	var url;
-	if(/^[a-zA-Z]([a-zA-Z0-9 -.])*:/.test(s)) {
-		// already absolute url
-		url = s;
-	} else if(/^\x2f/.test(s)) {// %2f --> /
-		// url is relative to site
-		location.protocol != "" ? url = location.protocol + "//" : url ="";		
-		url+= location.hostname;		
-		if(location.port != "80") {
-			url+=":"+location.port;
-		}				
-		url += s;
-	} else if(/^#/.test(s)) {
-		// url is relative to page
-		url = urlPage + s;
-	} else {
-		url = urlDir + s;
-	}
-	return encodeURI(url);
-}
-
-if (typeof exports != "undefined") {
-	exports.HTML2Markdown = HTML2Markdown;
-}
-		
-if (typeof exports != "undefined") {
-	exports.HTML2MarkDown = HTML2MarkDown;
-}
-
-/* add the useful functions to String object*/
-if (typeof String.prototype.trim != 'function') {
-	String.prototype.trim = function() {
-		return replace(/^\s+|\s+$/g,"");
-	};	
-}
-
-if (typeof String.prototype.isNotEmpty != 'function') {
-	String.prototype.isNotEmpty = function() {
-		if (/\S/.test(this)) {
-		    return true;
-		} else {
-			return false;
-		}		
-	};	
-}
-
-if (typeof String.prototype.replaceAll != 'function') {
-	String.prototype.replaceAll = function(stringToFind,stringToReplace){
-	    var temp = this;
-	    var index = temp.indexOf(stringToFind);
-	        while(index != -1){
-	            temp = temp.replace(stringToFind,stringToReplace);
-	            index = temp.indexOf(stringToFind);
-	        }
-	        return temp;
-	};	
-}
-
-if (typeof String.prototype.startsWith != 'function') {
-	String.prototype.startsWith = function(str) {
-		return this.indexOf(str) == 0;
-	};
-}
-
-if (typeof String.prototype.endsWith != 'function') {
-	String.prototype.endsWith = function(suffix) {
-	    return this.match(suffix+"$") == suffix;
-	};	
-}
-
-if (typeof Array.prototype.indexOf != 'function') {
-	Array.prototype.indexOf = function(obj, fromIndex) {
-		if (fromIndex == null) {
-			fromIndex = 0;
-		} else if (fromIndex < 0) {
-			fromIndex = Math.max(0, this.length + fromIndex);
-		}
-		for ( var i = fromIndex, j = this.length; i < j; i++) {
-			if (this[i] === obj)
-				return i;
-		}
-		return -1;
-	};
-}

+ 2 - 2
admin/server/validate.coffee

@@ -1,14 +1,14 @@
 utils    = require('lib/utils')
-settings = require('settings/root')
 
 exports.validate_doc_update = (newDoc, oldDoc, userCtx) ->
+  types = ['essay','scene','video','profile']
 
   access = if '_admin' in userCtx.roles or 'admin' in userCtx.roles or 'manager' in userCtx.roles then true else false
 
   if not access
     throw unauthorized: 'You must have the role admin or manager to make changes'
 
-  if newDoc.type in settings.app.content_types
+  if newDoc.type in types
     throw forbidden: 'site is a required field' unless newDoc.site
     throw forbidden: 'title is a required field' unless newDoc.title
 

+ 1 - 0
admin/static/css/common.styl

@@ -96,6 +96,7 @@ img
   border: 0
 
 input[type='text'],
+input[type='password'],
 input[type='search'],
 textarea,
 select

+ 77 - 3
admin/static/css/theme.styl

@@ -121,7 +121,9 @@ span.label
   margin: 0
   background: $primaryColor
   overflow: auto
+  box-shadow(-4px 4px 0 rgba(0, 0, 0, 0.1))
   -webkit-user-select: none
+  transition(all .3s)
 
   .app-logo
     padding: 16px 18px
@@ -133,10 +135,34 @@ span.label
     letter-spacing: -0.07em
     word-spacing: 0em
     white-space: nowrap
+    text-shadow: 0px -1px 0 rgba(0,0,0,0.2)
 
     a
       color: #fff
 
+  > form.login
+    display: none
+    padding: 20px 30px
+    text-shadow: 0px -1px 0 rgba(0,0,0,0.2)
+
+    > input
+      display: inline-block
+      width: 140px
+      margin: 10px
+      vertical-align: middle
+      color: #fff
+      font-weight: $boldFont
+      font-size: 1.2em
+
+      ::-webkit-input-placeholder
+        color: rgba(255,255,255,0.7)
+
+      &.button
+        background: darken($primaryColor, 5%)
+
+        &:hover
+          background: darken($primaryColor, 10%)
+
   > ul
     list-style-type: none
     padding: 0
@@ -161,6 +187,24 @@ span.label
         margin: 0.5em 0
         border-bottom: 1px solid rgba(255,255,255,0.5)
 
+  &.show-login
+    top: 30%
+    left: 30%
+    right: 30%
+    bottom: auto
+    width: initial
+    padding: 0 10px
+    z-index: 200
+
+    .app-logo
+      margin: 30px
+      padding: 0
+
+    > form.login
+      display: inline-block
+
+    > ul
+      display: none
 
 .main.stack
   position: absolute
@@ -406,9 +450,23 @@ span.label
           position: fixed
           top: 60px
           bottom: 0
-          left: 20%
-          right: 20%
-          width: auto
+          left: 10px
+          right: auto
+          width: 48%
+          height: 90%
+          padding: 10px
+          border: 0
+          background: white
+          // outline: 10px solid rgba(255, 255, 255, 0.5)
+          z-index: 100
+
+        .ui-preview
+          position: fixed
+          top: 60px
+          bottom: 0
+          left: auto
+          right: 10px
+          width: 48%
           height: 90%
           padding: 10px
           border: 0
@@ -543,6 +601,22 @@ ul.ui-multi-select
           font-weight: $boldFont
           font-size: 0.8em
 
+.ui-preview
+
+  position: absolute
+  top: 0
+  bottom: 0
+  right: 0
+  left: 0
+  background: #fff
+  padding: 20px
+  overflow: auto
+
+  .close-button
+    position: absolute
+    top: 10px
+    right: 10px
+
 .ui-help
   position: absolute
   top: 15%

+ 19 - 4
admin/templates/dashboard.html

@@ -1,14 +1,29 @@
 <div class="content">
   <h1>Dashboard</h1>
-  <ul class="drafts list">
-    {{#each drafts}}
+
+  <ul class="drafts essays list">
+    {{#each essays}}
+    <li>
+      <div class="actions">
+        {{#unless published}}<div><strong>In Draft</strong></div>{{/unless}}
+      </div>
+      <a href="#/{{type}}/{{id}}">{{title}}</a>
+      <div class="meta">
+        <div><a href="http://{{site}}/{{type}}/{{slug}}" target="_blank"><strong>{{type}}</strong> on {{site}}</a></div>
+      </div>
+    </li>
+    {{/each}}
+  </ul>
+
+  <ul class="drafts scenes list">
+    {{#each scenes}}
     <li>
       <div class="actions">
         {{#unless published}}<div><strong>In Draft</strong></div>{{/unless}}
       </div>
-      <a href="#/essay/{{id}}">{{title}}</a>
+      <a href="#/{{type}}/{{id}}">{{title}}</a>
       <div class="meta">
-        <div><a href="http://{{site}}/essay/{{slug}}" target="_blank">view on {{site}}</a></div>
+        <div><a href="http://{{site}}/{{type}}/{{slug}}" target="_blank"><strong>{{type}}</strong> on {{site}}</a></div>
       </div>
     </li>
     {{/each}}

+ 14 - 0
admin/templates/essay-form.html

@@ -93,6 +93,20 @@
         <input type="text" name="sponsor_end" value="{{sponsor_end}}" placeholder="Feb 20 2012 6:30 PM">
       </div>
     </div>
+
+    {{#if old_url}}
+    <h3 class="heading">Old Data</h3>
+    <div class="field">
+      <label>Old URL</label>
+      <div><a href="{{old_url}}" target="_blank" title="Open in New Tab">{{old_url}}</a></div>
+    </div>
+    <div class="field">
+      <label>Old Photos</label>
+      {{#each old_photos}}
+      <div><a href="{{.}}" target="_blank" title="Open in New Tab">{{.}}</a></div>
+      {{/each}}
+    </div>
+    {{/if}}
   </div>
 
 </form>

+ 7 - 0
admin/templates/main-nav.html

@@ -1,4 +1,9 @@
 <div class="app-logo"><a href="/">kleks</a></div>
+<form class="login">
+  <input type="text" name="username" placeholder="Username">
+  <input type="password" name="password" placeholder="Password">
+  <input type="submit" name="submit" value="Sign In" class="button">
+</form>
 <ul>
   <li class="dashboard"><a class="active" href="#/">Dashboard</a></li>
   <li class="essays"><a href="#/essays">Essays</a></li>
@@ -10,4 +15,6 @@
   <li class="blocks"><a href="#/blocks">Blocks</a></li>
   <li class="authors"><a href="#/authors">Authors</a></li>
   <li class="contacts"><a href="#/contacts">Contacts</a></li>
+  <li class="seperator"></li>
+  <li class="logout"><a class="logout-button">Sign Out</a></li>
 </ul>

+ 14 - 0
admin/templates/scene-form.html

@@ -89,6 +89,20 @@
         <input type="text" name="sponsor_end" value="{{sponsor_end}}" placeholder="Feb 20 2012 6:30 PM">
       </div>
     </div>
+
+    {{#if old_url}}
+    <h3 class="heading">Old Data</h3>
+    <div class="field">
+      <label>Old URL</label>
+      <div><a href="{{old_url}}" target="_blank" title="Open in New Tab">{{old_url}}</a></div>
+    </div>
+    <div class="field">
+      <label>Old Photos</label>
+      {{#each old_photos}}
+      <div><a href="{{.}}" target="_blank" title="Open in New Tab">{{.}}</a></div>
+      {{/each}}
+    </div>
+    {{/if}}
   </div>
 
 </form>

+ 28 - 23
site/server/lists.coffee

@@ -19,9 +19,10 @@ exports.home = (head, req) ->
 
   while row = getRow()
     doc = row.value
-    collections.push(doc) if doc.type is 'collection'
-    blocks[doc.code] = doc if doc.type is 'block'
-    site ?= doc if doc.type is 'site'
+    if doc
+      collections.push(doc) if doc.type is 'collection'
+      blocks[doc.code] = doc if doc.type is 'block'
+      site ?= doc if doc.type is 'site'
   
   collections = _.map collections, (doc) ->
     if doc.intro?
@@ -58,11 +59,12 @@ exports.collection = (head, req) ->
 
   while row = getRow()
     doc = row.doc
-    docs.push(doc) if doc.type in settings.app.content_types
-    collection ?= doc if doc.type is 'collection'
-    blocks[doc.code] = doc if doc.type is 'block'
-    sponsor ?= doc if doc.type is 'sponsor'
-    site ?= doc if doc.type is 'site'
+    if doc
+      docs.push(doc) if doc.type in settings.app.content_types
+      collection ?= doc if doc.type is 'collection'
+      blocks[doc.code] = doc if doc.type is 'block'
+      sponsor ?= doc if doc.type is 'sponsor'
+      site ?= doc if doc.type is 'site'
 
   if collection
     if collection.intro?
@@ -128,13 +130,14 @@ exports.docs = (head, req) ->
 
   while row = getRow()
     doc = row.doc
-    if doc.type in settings.app.content_types
-      doc.collection_docs = []
-      docs.push(doc)
-    else if doc.type is 'collection'
-      # Add the collection doc to the last doc pushed
-      docs[docs.length-1].collection_docs.push(doc)
-    site ?= doc if doc.type is 'site'
+    if doc
+      if doc.type in settings.app.content_types
+        doc.collection_docs = []
+        docs.push(doc)
+      else if doc.type is 'collection'
+        # Add the collection doc to the last doc pushed
+        docs[docs.length-1].collection_docs.push(doc)
+      site ?= doc if doc.type is 'site'
 
   docs = _.map docs, (doc) ->
     if doc.intro?
@@ -175,12 +178,13 @@ exports.doc = (head, req) ->
 
   while row = getRow()
     doc = row.doc
-    theDoc ?= doc if doc.type in settings.app.content_types
-    collections.push(doc) if doc.type is 'collection'
-    sponsor ?= doc if doc.type is 'sponsor'
-    author ?= doc if doc.type is 'author'
-    blocks[doc.code] = doc if doc.type is 'block'
-    site ?= doc if doc.type is 'site'
+    if doc
+      theDoc ?= doc if doc.type in settings.app.content_types
+      collections.push(doc) if doc.type is 'collection'
+      sponsor ?= doc if doc.type is 'sponsor'
+      author ?= doc if doc.type is 'author'
+      blocks[doc.code] = doc if doc.type is 'block'
+      site ?= doc if doc.type is 'site'
 
   # Let's just go back and use `doc` as the variable instead
   doc = theDoc
@@ -256,8 +260,9 @@ exports.rssfeed = (head, req) ->
 
   while row = getRow()
     doc = row.doc
-    docs.push(doc) if doc.type in settings.app.content_types
-    site ?= doc if doc.type is 'site'
+    if doc
+      docs.push(doc) if doc.type in settings.app.content_types
+      site ?= doc if doc.type is 'site'
 
   docs = _.map docs, (doc) ->
     doc.intro_html = md.makeHtml(