diff --git a/Gruntfile.coffee b/Gruntfile.coffee index 302f0c2..2bb22dd 100644 --- a/Gruntfile.coffee +++ b/Gruntfile.coffee @@ -19,6 +19,7 @@ module.exports = (grunt) -> 'src/styles/fonts.styl' 'src/styles/icons.styl' 'src/styles/chart.styl' + 'src/styles/notification.styl' 'src/styles/app.styl' ] dest: 'public/css/app.css' diff --git a/Makefile b/Makefile index abbfefa..9bcef3c 100644 --- a/Makefile +++ b/Makefile @@ -6,6 +6,7 @@ build: grunt watch: + grunt grunt watch serve: diff --git a/README.md b/README.md index a256cdf..dd3819e 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,8 @@ GitHub Burndown Chart as a service. Public repos are free, for private access au - [ ] Check that we have not run out of requests to make - [ ] Show loading sign on top of [browser window](https://github.com/buunguyen/topbar) which is unobtrusive enough we can show it immediately. - [ ] show a countdown clock towards the end of the milestone or show overdue +- [ ] highlight for a moment recently changed milestone +- [ ] smooth animation when transitioning between icons - [x] format milestone titles prepending "Milestone" word if appropriate - [x] Variable document.title on different pages - [x] be able to go back to homepage @@ -38,7 +40,6 @@ GitHub Burndown Chart as a service. Public repos are free, for private access au - [x] work for `mbostock/d3` - [x] allow people to go straight to a URL that fetches the repo, if public, for them; to demo our app without adding a repo (add it behind the scenes); *req* cache repos - [x] closed issues can be moved to a newly created milestone, this messes up the chart since we assume milestone is created first! -- [ ] highlight for a moment recently changed milestone ### Extras diff --git a/bower.json b/bower.json index ff618c6..06c3ed3 100644 --- a/bower.json +++ b/bower.json @@ -1,21 +1,21 @@ { - "name": "burnchart", - "version": "0.0.0", - "dependencies": { - "lodash": "2.3.0", - "normalize-css": "2.1.3", - "ractive": "~0.5.5", - "ractive-adaptor": "radekstepan/ractive-adaptor-ractive", - "firebase": "~1.0.21", - "firebase-simple-login": "~1.6.3", - "grapnel": "~0.4.2", - "github": "~0.9.0", - "localforage": "~0.9.2", - "superagent": "~0.19.0", - "async": "~0.9.0", - "moment": "~2.8.3", - "d3": "~3.4.11", - "d3-tip": "~0.6.5", - "marked": "~0.3.2" - } + "name": "burnchart", + "version": "0.0.0", + "dependencies": { + "lodash": "2.3.0", + "normalize-css": "2.1.3", + "ractive": "~0.5.5", + "ractive-adaptor": "radekstepan/ractive-adaptor-ractive", + "firebase": "~1.0.21", + "firebase-simple-login": "~1.6.3", + "grapnel": "~0.4.2", + "github": "~0.9.0", + "localforage": "~0.9.2", + "superagent": "~0.19.0", + "async": "~0.9.0", + "moment": "~2.8.3", + "d3": "~3.4.11", + "d3-tip": "~0.6.5", + "marked": "~0.3.2" + } } diff --git a/public/css/app.bundle.css b/public/css/app.bundle.css index ba28df4..f1cfbd1 100644 --- a/public/css/app.bundle.css +++ b/public/css/app.bundle.css @@ -409,18 +409,9 @@ table { @font-face{font-family:'MuseoSans500Regular';src:url("../fonts/museo-sans-500.eot");src:url("../fonts/museo-sans-500.eot?#iefix") format('embedded-opentype'),url("../fonts/museo-sans-500.woff") format('woff'),url("../fonts/museo-sans-500.ttf") format('truetype'),url("../fonts/museo-sans-500.svg#MuseoSans500Regular") format('svg');font-weight:normal;font-style:normal} @font-face{font-family:'Fontello';src:url("../fonts/fontello.eot?74672344");src:url("../fonts/fontello.eot?74672344#iefix") format('embedded-opentype'),url("../fonts/fontello.woff?74672344") format('woff'),url("../fonts/fontello.ttf?74672344") format('truetype'),url("../fonts/fontello.svg?74672344#fontello") format('svg');font-weight:normal;font-style:normal} -.icon{vertical-align:middle;} -.icon:before{font-family:"Fontello";font-style:normal;font-weight:normal;speak:none;display:inline-block;text-decoration:inherit;text-align:center;font-variant:normal;text-transform:none;line-height:1em} -.icon.cog:before{content:'\e800'} -.icon.search:before{content:'\e801'} -.icon.github:before{content:'\e802'} -.icon.address:before{content:'\e803'} -.icon.plus-circled:before{content:'\e804'} -.icon.fire-station:before{content:'\e805'} -.icon.sort-alphabet:before{content:'\e806'} -.icon.down-open:before{content:'\e807'} -.icon.spin6{-webkit-animation:spin 2s infinite linear;-moz-animation:spin 2s infinite linear;-o-animation:spin 2s infinite linear;-ms-animation:spin 2s infinite linear;animation:spin 2s infinite linear;} -.icon.spin6:before{content:'\e808'} +.icon{vertical-align:middle;font-family:"Fontello";font-style:normal;font-weight:normal;speak:none;display:inline-block;text-decoration:inherit;text-align:center;font-variant:normal;text-transform:none;} +.icon.spin6{-webkit-animation:spin 2s infinite linear;-moz-animation:spin 2s infinite linear;-o-animation:spin 2s infinite linear;-ms-animation:spin 2s infinite linear;animation:spin 2s infinite linear} +.icon.spin4{-webkit-animation:spin 2s infinite linear;-moz-animation:spin 2s infinite linear;-o-animation:spin 2s infinite linear;-ms-animation:spin 2s infinite linear;animation:spin 2s infinite linear} @-moz-keyframes spin{0%{-moz-transform:rotate(0)} 100%{-moz-transform:rotate(360deg)} }@-webkit-keyframes spin{0%{-webkit-transform:rotate(0)} @@ -457,12 +448,9 @@ a{text-decoration:none;color:#aaafbf;cursor:pointer} h1,h2,h3,p{margin:0} ul{list-style-type:none;margin:0;padding:0;} ul li{display:inline-block} -.icon{margin-right:4px} .wrap{width:800px;margin:0 auto} #head{background:#c1041c;height:64px;} -#head h1{font-size:26px;margin:0;padding:10px 24px;line-height:44px;height:44px;background:#77000e;display:inline-block;} -#head h1 a{color:#c1041c;} -#head h1 a .icon{margin:0} +#head #icon{font-size:26px;margin:0;padding:10px 0;line-height:44px;height:44px;width:74px;background:#77000e;display:inline-block;color:#c1041c;margin:0;text-align:center} #head .q{position:relative;display:inline-block;margin:13px 20px 0 20px;vertical-align:top;} #head .q .icon{position:absolute;color:#c1041c;} #head .q .icon.search{top:8px;left:12px} diff --git a/public/css/app.css b/public/css/app.css index 8da7b2c..fc1e00a 100644 --- a/public/css/app.css +++ b/public/css/app.css @@ -2,18 +2,9 @@ @font-face{font-family:'MuseoSans500Regular';src:url("../fonts/museo-sans-500.eot");src:url("../fonts/museo-sans-500.eot?#iefix") format('embedded-opentype'),url("../fonts/museo-sans-500.woff") format('woff'),url("../fonts/museo-sans-500.ttf") format('truetype'),url("../fonts/museo-sans-500.svg#MuseoSans500Regular") format('svg');font-weight:normal;font-style:normal} @font-face{font-family:'Fontello';src:url("../fonts/fontello.eot?74672344");src:url("../fonts/fontello.eot?74672344#iefix") format('embedded-opentype'),url("../fonts/fontello.woff?74672344") format('woff'),url("../fonts/fontello.ttf?74672344") format('truetype'),url("../fonts/fontello.svg?74672344#fontello") format('svg');font-weight:normal;font-style:normal} -.icon{vertical-align:middle;} -.icon:before{font-family:"Fontello";font-style:normal;font-weight:normal;speak:none;display:inline-block;text-decoration:inherit;text-align:center;font-variant:normal;text-transform:none;line-height:1em} -.icon.cog:before{content:'\e800'} -.icon.search:before{content:'\e801'} -.icon.github:before{content:'\e802'} -.icon.address:before{content:'\e803'} -.icon.plus-circled:before{content:'\e804'} -.icon.fire-station:before{content:'\e805'} -.icon.sort-alphabet:before{content:'\e806'} -.icon.down-open:before{content:'\e807'} -.icon.spin6{-webkit-animation:spin 2s infinite linear;-moz-animation:spin 2s infinite linear;-o-animation:spin 2s infinite linear;-ms-animation:spin 2s infinite linear;animation:spin 2s infinite linear;} -.icon.spin6:before{content:'\e808'} +.icon{vertical-align:middle;font-family:"Fontello";font-style:normal;font-weight:normal;speak:none;display:inline-block;text-decoration:inherit;text-align:center;font-variant:normal;text-transform:none;} +.icon.spin6{-webkit-animation:spin 2s infinite linear;-moz-animation:spin 2s infinite linear;-o-animation:spin 2s infinite linear;-ms-animation:spin 2s infinite linear;animation:spin 2s infinite linear} +.icon.spin4{-webkit-animation:spin 2s infinite linear;-moz-animation:spin 2s infinite linear;-o-animation:spin 2s infinite linear;-ms-animation:spin 2s infinite linear;animation:spin 2s infinite linear} @-moz-keyframes spin{0%{-moz-transform:rotate(0)} 100%{-moz-transform:rotate(360deg)} }@-webkit-keyframes spin{0%{-webkit-transform:rotate(0)} @@ -50,12 +41,9 @@ a{text-decoration:none;color:#aaafbf;cursor:pointer} h1,h2,h3,p{margin:0} ul{list-style-type:none;margin:0;padding:0;} ul li{display:inline-block} -.icon{margin-right:4px} .wrap{width:800px;margin:0 auto} #head{background:#c1041c;height:64px;} -#head h1{font-size:26px;margin:0;padding:10px 24px;line-height:44px;height:44px;background:#77000e;display:inline-block;} -#head h1 a{color:#c1041c;} -#head h1 a .icon{margin:0} +#head #icon{font-size:26px;margin:0;padding:10px 0;line-height:44px;height:44px;width:74px;background:#77000e;display:inline-block;color:#c1041c;margin:0;text-align:center} #head .q{position:relative;display:inline-block;margin:13px 20px 0 20px;vertical-align:top;} #head .q .icon{position:absolute;color:#c1041c;} #head .q .icon.search{top:8px;left:12px} diff --git a/public/fonts/fontello.eot b/public/fonts/fontello.eot index a50ae65..bc607a7 100644 Binary files a/public/fonts/fontello.eot and b/public/fonts/fontello.eot differ diff --git a/public/fonts/fontello.svg b/public/fonts/fontello.svg index 1ef0e13..0c223e2 100644 --- a/public/fonts/fontello.svg +++ b/public/fonts/fontello.svg @@ -15,6 +15,8 @@ + + \ No newline at end of file diff --git a/public/fonts/fontello.ttf b/public/fonts/fontello.ttf index 4e18fb0..27ebd73 100644 Binary files a/public/fonts/fontello.ttf and b/public/fonts/fontello.ttf differ diff --git a/public/fonts/fontello.woff b/public/fonts/fontello.woff index 2c16d7c..21d10b7 100644 Binary files a/public/fonts/fontello.woff and b/public/fonts/fontello.woff differ diff --git a/public/index.html b/public/index.html index c2a8c41..9af3245 100644 --- a/public/index.html +++ b/public/index.html @@ -1,17 +1,17 @@ - - - - - - + + + + + + - + \ No newline at end of file diff --git a/public/js/app.bundle.js b/public/js/app.bundle.js index eba7514..648e3a4 100644 --- a/public/js/app.bundle.js +++ b/public/js/app.bundle.js @@ -29175,7 +29175,7 @@ requireModule('promise/polyfill').polyfill(); }).call(this); ;!function() { var d3 = { - version: "3.4.11" + version: "3.4.12" }; if (!Date.now) Date.now = function() { return +new Date(); @@ -29447,7 +29447,7 @@ requireModule('promise/polyfill').polyfill(); size: d3_map_size, empty: d3_map_empty, forEach: function(f) { - for (var key in this) if (key.charCodeAt(0) === d3_map_prefixCode) f.call(this, key.substring(1), this[key]); + for (var key in this) if (key.charCodeAt(0) === d3_map_prefixCode) f.call(this, key.slice(1), this[key]); } }); var d3_map_prefix = "\x00", d3_map_prefixCode = d3_map_prefix.charCodeAt(0); @@ -29557,7 +29557,7 @@ requireModule('promise/polyfill').polyfill(); size: d3_map_size, empty: d3_map_empty, forEach: function(f) { - for (var value in this) if (value.charCodeAt(0) === d3_map_prefixCode) f.call(this, value.substring(1)); + for (var value in this) if (value.charCodeAt(0) === d3_map_prefixCode) f.call(this, value.slice(1)); } }); d3.behavior = {}; @@ -29574,7 +29574,7 @@ requireModule('promise/polyfill').polyfill(); } function d3_vendorSymbol(object, name) { if (name in object) return name; - name = name.charAt(0).toUpperCase() + name.substring(1); + name = name.charAt(0).toUpperCase() + name.slice(1); for (var i = 0, n = d3_vendorPrefixes.length; i < n; ++i) { var prefixName = d3_vendorPrefixes[i] + name; if (prefixName in object) return prefixName; @@ -29591,8 +29591,8 @@ requireModule('promise/polyfill').polyfill(); d3_dispatch.prototype.on = function(type, listener) { var i = type.indexOf("."), name = ""; if (i >= 0) { - name = type.substring(i + 1); - type = type.substring(0, i); + name = type.slice(i + 1); + type = type.slice(0, i); } if (type) return arguments.length < 2 ? this[type].on(name) : this[type].on(name, listener); if (arguments.length === 2) { @@ -29733,8 +29733,8 @@ requireModule('promise/polyfill').polyfill(); qualify: function(name) { var i = name.indexOf(":"), prefix = name; if (i >= 0) { - prefix = name.substring(0, i); - name = name.substring(i + 1); + prefix = name.slice(0, i); + name = name.slice(i + 1); } return d3_nsPrefix.hasOwnProperty(prefix) ? { space: d3_nsPrefix[prefix], @@ -30086,7 +30086,7 @@ requireModule('promise/polyfill').polyfill(); }; d3_selectionPrototype.size = function() { var n = 0; - this.each(function() { + d3_selection_each(this, function() { ++n; }); return n; @@ -30183,7 +30183,7 @@ requireModule('promise/polyfill').polyfill(); }; function d3_selection_on(type, listener, capture) { var name = "__on" + type, i = type.indexOf("."), wrap = d3_selection_onListener; - if (i > 0) type = type.substring(0, i); + if (i > 0) type = type.slice(0, i); var filter = d3_selection_onFilters.get(type); if (filter) type = filter, wrap = d3_selection_onFilter; function onRemove() { @@ -30291,13 +30291,13 @@ requireModule('promise/polyfill').polyfill(); var rect = container.getBoundingClientRect(); return [ e.clientX - rect.left - container.clientLeft, e.clientY - rect.top - container.clientTop ]; } - d3.touches = function(container, touches) { - if (arguments.length < 2) touches = d3_eventSource().touches; - return touches ? d3_array(touches).map(function(touch) { - var point = d3_mousePoint(container, touch); - point.identifier = touch.identifier; - return point; - }) : []; + d3.touch = function(container, touches, identifier) { + if (arguments.length < 3) identifier = touches, touches = d3_eventSource().changedTouches; + if (touches) for (var i = 0, n = touches.length, touch; i < n; ++i) { + if ((touch = touches[i]).identifier === identifier) { + return d3_mousePoint(container, touch); + } + } }; d3.behavior.drag = function() { var event = d3_eventDispatch(drag, "drag", "dragstart", "dragend"), origin = null, mousedown = dragstart(d3_noop, d3.mouse, d3_behavior_dragMouseSubject, "mousemove", "mouseup"), touchstart = dragstart(d3_behavior_dragTouchId, d3.touch, d3_behavior_dragTouchSubject, "touchmove", "touchend"); @@ -30357,6 +30357,14 @@ requireModule('promise/polyfill').polyfill(); function d3_behavior_dragMouseSubject() { return d3_window; } + d3.touches = function(container, touches) { + if (arguments.length < 2) touches = d3_eventSource().touches; + return touches ? d3_array(touches).map(function(touch) { + var point = d3_mousePoint(container, touch); + point.identifier = touch.identifier; + return point; + }) : []; + }; var π = Math.PI, τ = 2 * π, halfπ = π / 2, ε = 1e-6, ε2 = ε * ε, d3_radians = π / 180, d3_degrees = 180 / π; function d3_sgn(x) { return x > 0 ? 1 : x < 0 ? -1 : 0; @@ -30552,10 +30560,11 @@ requireModule('promise/polyfill').polyfill(); } } function touchstarted() { - var that = this, dispatch = event.of(that, arguments), locations0 = {}, distance0 = 0, scale0, zoomName = ".zoom-" + d3.event.changedTouches[0].identifier, touchmove = "touchmove" + zoomName, touchend = "touchend" + zoomName, targets = [], subject = d3.select(that).on(mousedown, null).on(touchstart, started), dragRestore = d3_event_dragSuppress(); + var that = this, dispatch = event.of(that, arguments), locations0 = {}, distance0 = 0, scale0, zoomName = ".zoom-" + d3.event.changedTouches[0].identifier, touchmove = "touchmove" + zoomName, touchend = "touchend" + zoomName, targets = [], subject = d3.select(that), dragRestore = d3_event_dragSuppress(); d3_selection_interrupt.call(that); started(); zoomstarted(dispatch); + subject.on(mousedown, null).on(touchstart, started); function relocate() { var touches = d3.touches(that); scale0 = view.k; @@ -30798,7 +30807,7 @@ requireModule('promise/polyfill').polyfill(); } } if (color = d3_rgb_names.get(format)) return rgb(color.r, color.g, color.b); - if (format != null && format.charAt(0) === "#" && !isNaN(color = parseInt(format.substring(1), 16))) { + if (format != null && format.charAt(0) === "#" && !isNaN(color = parseInt(format.slice(1), 16))) { if (format.length === 4) { r = (color & 3840) >> 4; r = r >> 4 | r; @@ -31017,7 +31026,7 @@ requireModule('promise/polyfill').polyfill(); }; function respond() { var status = request.status, result; - if (!status && request.responseText || status >= 200 && status < 300 || status === 304) { + if (!status && d3_xhrHasResponse(request) || status >= 200 && status < 300 || status === 304) { try { result = response.call(xhr, request); } catch (e) { @@ -31089,6 +31098,10 @@ requireModule('promise/polyfill').polyfill(); callback(error == null ? request : null); } : callback; } + function d3_xhrHasResponse(request) { + var type = request.responseType; + return type && type !== "text" ? request.response : request.responseText; + } d3.dsv = function(delimiter, mimeType) { var reFormat = new RegExp('["' + delimiter + "\n]"), delimiterCode = delimiter.charCodeAt(0); function dsv(url, row, callback) { @@ -31141,7 +31154,7 @@ requireModule('promise/polyfill').polyfill(); } else if (c === 10) { eol = true; } - return text.substring(j + 1, i).replace(/""/g, '"'); + return text.slice(j + 1, i).replace(/""/g, '"'); } while (I < N) { var c = text.charCodeAt(I++), k = 1; @@ -31149,9 +31162,9 @@ requireModule('promise/polyfill').polyfill(); eol = true; if (text.charCodeAt(I) === 10) ++I, ++k; } else if (c !== delimiterCode) continue; - return text.substring(j, I - k); + return text.slice(j, I - k); } - return text.substring(j); + return text.slice(j); } while ((t = token()) !== EOF) { var a = []; @@ -31193,14 +31206,6 @@ requireModule('promise/polyfill').polyfill(); }; d3.csv = d3.dsv(",", "text/csv"); d3.tsv = d3.dsv(" ", "text/tab-separated-values"); - d3.touch = function(container, touches, identifier) { - if (arguments.length < 3) identifier = touches, touches = d3_eventSource().changedTouches; - if (touches) for (var i = 0, n = touches.length, touch; i < n; ++i) { - if ((touch = touches[i]).identifier === identifier) { - return d3_mousePoint(container, touch); - } - } - }; var d3_timer_queueHead, d3_timer_queueTail, d3_timer_interval, d3_timer_timeout, d3_timer_active, d3_timer_frame = d3_window[d3_vendorSymbol(d3_window, "requestAnimationFrame")] || function(callback) { setTimeout(callback, 17); }; @@ -31292,7 +31297,7 @@ requireModule('promise/polyfill').polyfill(); function d3_locale_numberFormat(locale) { var locale_decimal = locale.decimal, locale_thousands = locale.thousands, locale_grouping = locale.grouping, locale_currency = locale.currency, formatGroup = locale_grouping ? function(value) { var i = value.length, t = [], j = 0, g = locale_grouping[0]; - while (i > 0 && g > 0) { + while (g > 0 && i > 0) { t.push(value.substring(i -= g, i + g)); g = locale_grouping[j = (j + 1) % locale_grouping.length]; } @@ -31586,14 +31591,14 @@ requireModule('promise/polyfill').polyfill(); var string = [], i = -1, j = 0, c, p, f; while (++i < n) { if (template.charCodeAt(i) === 37) { - string.push(template.substring(j, i)); + string.push(template.slice(j, i)); if ((p = d3_time_formatPads[c = template.charAt(++i)]) != null) c = template.charAt(++i); if (f = d3_time_formats[c]) c = f(date, p == null ? c === "e" ? " " : "0" : p); string.push(c); j = i + 1; } } - string.push(template.substring(j, i)); + string.push(template.slice(j, i)); return string.join(""); } format.parse = function(string) { @@ -31614,7 +31619,7 @@ requireModule('promise/polyfill').polyfill(); date.setFullYear(d.y, 0, 1); date.setFullYear(d.y, 0, "W" in d ? (d.w + 6) % 7 + d.W * 7 - (date.getDay() + 5) % 7 : d.w + d.U * 7 - (date.getDay() + 6) % 7); } else date.setFullYear(d.y, d.m, d.d); - date.setHours(d.H + Math.floor(d.Z / 100), d.M + d.Z % 100, d.S, d.L); + date.setHours(d.H + (d.Z / 100 | 0), d.M + d.Z % 100, d.S, d.L); return localZ ? date._ : date; }; format.toString = function() { @@ -31760,22 +31765,22 @@ requireModule('promise/polyfill').polyfill(); }; function d3_time_parseWeekdayAbbrev(date, string, i) { d3_time_dayAbbrevRe.lastIndex = 0; - var n = d3_time_dayAbbrevRe.exec(string.substring(i)); + var n = d3_time_dayAbbrevRe.exec(string.slice(i)); return n ? (date.w = d3_time_dayAbbrevLookup.get(n[0].toLowerCase()), i + n[0].length) : -1; } function d3_time_parseWeekday(date, string, i) { d3_time_dayRe.lastIndex = 0; - var n = d3_time_dayRe.exec(string.substring(i)); + var n = d3_time_dayRe.exec(string.slice(i)); return n ? (date.w = d3_time_dayLookup.get(n[0].toLowerCase()), i + n[0].length) : -1; } function d3_time_parseMonthAbbrev(date, string, i) { d3_time_monthAbbrevRe.lastIndex = 0; - var n = d3_time_monthAbbrevRe.exec(string.substring(i)); + var n = d3_time_monthAbbrevRe.exec(string.slice(i)); return n ? (date.m = d3_time_monthAbbrevLookup.get(n[0].toLowerCase()), i + n[0].length) : -1; } function d3_time_parseMonth(date, string, i) { d3_time_monthRe.lastIndex = 0; - var n = d3_time_monthRe.exec(string.substring(i)); + var n = d3_time_monthRe.exec(string.slice(i)); return n ? (date.m = d3_time_monthLookup.get(n[0].toLowerCase()), i + n[0].length) : -1; } function d3_time_parseLocaleFull(date, string, i) { @@ -31788,7 +31793,7 @@ requireModule('promise/polyfill').polyfill(); return d3_time_parse(date, d3_time_formats.X.toString(), string, i); } function d3_time_parseAmPm(date, string, i) { - var n = d3_time_periodLookup.get(string.substring(i, i += 2).toLowerCase()); + var n = d3_time_periodLookup.get(string.slice(i, i += 2).toLowerCase()); return n == null ? -1 : (date.p = n, i); } return d3_time_format; @@ -31812,31 +31817,31 @@ requireModule('promise/polyfill').polyfill(); } function d3_time_parseWeekdayNumber(date, string, i) { d3_time_numberRe.lastIndex = 0; - var n = d3_time_numberRe.exec(string.substring(i, i + 1)); + var n = d3_time_numberRe.exec(string.slice(i, i + 1)); return n ? (date.w = +n[0], i + n[0].length) : -1; } function d3_time_parseWeekNumberSunday(date, string, i) { d3_time_numberRe.lastIndex = 0; - var n = d3_time_numberRe.exec(string.substring(i)); + var n = d3_time_numberRe.exec(string.slice(i)); return n ? (date.U = +n[0], i + n[0].length) : -1; } function d3_time_parseWeekNumberMonday(date, string, i) { d3_time_numberRe.lastIndex = 0; - var n = d3_time_numberRe.exec(string.substring(i)); + var n = d3_time_numberRe.exec(string.slice(i)); return n ? (date.W = +n[0], i + n[0].length) : -1; } function d3_time_parseFullYear(date, string, i) { d3_time_numberRe.lastIndex = 0; - var n = d3_time_numberRe.exec(string.substring(i, i + 4)); + var n = d3_time_numberRe.exec(string.slice(i, i + 4)); return n ? (date.y = +n[0], i + n[0].length) : -1; } function d3_time_parseYear(date, string, i) { d3_time_numberRe.lastIndex = 0; - var n = d3_time_numberRe.exec(string.substring(i, i + 2)); + var n = d3_time_numberRe.exec(string.slice(i, i + 2)); return n ? (date.y = d3_time_expandYear(+n[0]), i + n[0].length) : -1; } function d3_time_parseZone(date, string, i) { - return /^[+-]\d{4}$/.test(string = string.substring(i, i + 5)) ? (date.Z = -string, + return /^[+-]\d{4}$/.test(string = string.slice(i, i + 5)) ? (date.Z = -string, i + 5) : -1; } function d3_time_expandYear(d) { @@ -31844,46 +31849,46 @@ requireModule('promise/polyfill').polyfill(); } function d3_time_parseMonthNumber(date, string, i) { d3_time_numberRe.lastIndex = 0; - var n = d3_time_numberRe.exec(string.substring(i, i + 2)); + var n = d3_time_numberRe.exec(string.slice(i, i + 2)); return n ? (date.m = n[0] - 1, i + n[0].length) : -1; } function d3_time_parseDay(date, string, i) { d3_time_numberRe.lastIndex = 0; - var n = d3_time_numberRe.exec(string.substring(i, i + 2)); + var n = d3_time_numberRe.exec(string.slice(i, i + 2)); return n ? (date.d = +n[0], i + n[0].length) : -1; } function d3_time_parseDayOfYear(date, string, i) { d3_time_numberRe.lastIndex = 0; - var n = d3_time_numberRe.exec(string.substring(i, i + 3)); + var n = d3_time_numberRe.exec(string.slice(i, i + 3)); return n ? (date.j = +n[0], i + n[0].length) : -1; } function d3_time_parseHour24(date, string, i) { d3_time_numberRe.lastIndex = 0; - var n = d3_time_numberRe.exec(string.substring(i, i + 2)); + var n = d3_time_numberRe.exec(string.slice(i, i + 2)); return n ? (date.H = +n[0], i + n[0].length) : -1; } function d3_time_parseMinutes(date, string, i) { d3_time_numberRe.lastIndex = 0; - var n = d3_time_numberRe.exec(string.substring(i, i + 2)); + var n = d3_time_numberRe.exec(string.slice(i, i + 2)); return n ? (date.M = +n[0], i + n[0].length) : -1; } function d3_time_parseSeconds(date, string, i) { d3_time_numberRe.lastIndex = 0; - var n = d3_time_numberRe.exec(string.substring(i, i + 2)); + var n = d3_time_numberRe.exec(string.slice(i, i + 2)); return n ? (date.S = +n[0], i + n[0].length) : -1; } function d3_time_parseMilliseconds(date, string, i) { d3_time_numberRe.lastIndex = 0; - var n = d3_time_numberRe.exec(string.substring(i, i + 3)); + var n = d3_time_numberRe.exec(string.slice(i, i + 3)); return n ? (date.L = +n[0], i + n[0].length) : -1; } function d3_time_zone(d) { - var z = d.getTimezoneOffset(), zs = z > 0 ? "-" : "+", zh = ~~(abs(z) / 60), zm = abs(z) % 60; + var z = d.getTimezoneOffset(), zs = z > 0 ? "-" : "+", zh = abs(z) / 60 | 0, zm = abs(z) % 60; return zs + d3_time_formatPad(zh, "0", 2) + d3_time_formatPad(zm, "0", 2); } function d3_time_parseLiteralPercent(date, string, i) { d3_time_percentRe.lastIndex = 0; - var n = d3_time_percentRe.exec(string.substring(i, i + 1)); + var n = d3_time_percentRe.exec(string.slice(i, i + 1)); return n ? i + n[0].length : -1; } function d3_time_formatMulti(formats) { @@ -32491,35 +32496,6 @@ requireModule('promise/polyfill').polyfill(); function d3_geo_clipSort(a, b) { return ((a = a.x)[0] < 0 ? a[1] - halfπ - ε : halfπ - a[1]) - ((b = b.x)[0] < 0 ? b[1] - halfπ - ε : halfπ - b[1]); } - function d3_geo_pointInPolygon(point, polygon) { - var meridian = point[0], parallel = point[1], meridianNormal = [ Math.sin(meridian), -Math.cos(meridian), 0 ], polarAngle = 0, winding = 0; - d3_geo_areaRingSum.reset(); - for (var i = 0, n = polygon.length; i < n; ++i) { - var ring = polygon[i], m = ring.length; - if (!m) continue; - var point0 = ring[0], λ0 = point0[0], φ0 = point0[1] / 2 + π / 4, sinφ0 = Math.sin(φ0), cosφ0 = Math.cos(φ0), j = 1; - while (true) { - if (j === m) j = 0; - point = ring[j]; - var λ = point[0], φ = point[1] / 2 + π / 4, sinφ = Math.sin(φ), cosφ = Math.cos(φ), dλ = λ - λ0, sdλ = dλ >= 0 ? 1 : -1, adλ = sdλ * dλ, antimeridian = adλ > π, k = sinφ0 * sinφ; - d3_geo_areaRingSum.add(Math.atan2(k * sdλ * Math.sin(adλ), cosφ0 * cosφ + k * Math.cos(adλ))); - polarAngle += antimeridian ? dλ + sdλ * τ : dλ; - if (antimeridian ^ λ0 >= meridian ^ λ >= meridian) { - var arc = d3_geo_cartesianCross(d3_geo_cartesian(point0), d3_geo_cartesian(point)); - d3_geo_cartesianNormalize(arc); - var intersection = d3_geo_cartesianCross(meridianNormal, arc); - d3_geo_cartesianNormalize(intersection); - var φarc = (antimeridian ^ dλ >= 0 ? -1 : 1) * d3_asin(intersection[2]); - if (parallel > φarc || parallel === φarc && (arc[0] || arc[1])) { - winding += antimeridian ^ dλ >= 0 ? 1 : -1; - } - } - if (!j++) break; - λ0 = λ, sinφ0 = sinφ, cosφ0 = cosφ, point0 = point; - } - } - return (polarAngle < -ε || polarAngle < ε && d3_geo_areaRingSum < 0) ^ winding & 1; - } var d3_geo_clipAntimeridian = d3_geo_clip(d3_true, d3_geo_clipAntimeridianLine, d3_geo_clipAntimeridianInterpolate, [ -π, -π / 2 ]); function d3_geo_clipAntimeridianLine(listener) { var λ0 = NaN, φ0 = NaN, sλ0 = NaN, clean; @@ -32587,6 +32563,35 @@ requireModule('promise/polyfill').polyfill(); listener.point(to[0], to[1]); } } + function d3_geo_pointInPolygon(point, polygon) { + var meridian = point[0], parallel = point[1], meridianNormal = [ Math.sin(meridian), -Math.cos(meridian), 0 ], polarAngle = 0, winding = 0; + d3_geo_areaRingSum.reset(); + for (var i = 0, n = polygon.length; i < n; ++i) { + var ring = polygon[i], m = ring.length; + if (!m) continue; + var point0 = ring[0], λ0 = point0[0], φ0 = point0[1] / 2 + π / 4, sinφ0 = Math.sin(φ0), cosφ0 = Math.cos(φ0), j = 1; + while (true) { + if (j === m) j = 0; + point = ring[j]; + var λ = point[0], φ = point[1] / 2 + π / 4, sinφ = Math.sin(φ), cosφ = Math.cos(φ), dλ = λ - λ0, sdλ = dλ >= 0 ? 1 : -1, adλ = sdλ * dλ, antimeridian = adλ > π, k = sinφ0 * sinφ; + d3_geo_areaRingSum.add(Math.atan2(k * sdλ * Math.sin(adλ), cosφ0 * cosφ + k * Math.cos(adλ))); + polarAngle += antimeridian ? dλ + sdλ * τ : dλ; + if (antimeridian ^ λ0 >= meridian ^ λ >= meridian) { + var arc = d3_geo_cartesianCross(d3_geo_cartesian(point0), d3_geo_cartesian(point)); + d3_geo_cartesianNormalize(arc); + var intersection = d3_geo_cartesianCross(meridianNormal, arc); + d3_geo_cartesianNormalize(intersection); + var φarc = (antimeridian ^ dλ >= 0 ? -1 : 1) * d3_asin(intersection[2]); + if (parallel > φarc || parallel === φarc && (arc[0] || arc[1])) { + winding += antimeridian ^ dλ >= 0 ? 1 : -1; + } + } + if (!j++) break; + λ0 = λ, sinφ0 = sinφ, cosφ0 = cosφ, point0 = point; + } + } + return (polarAngle < -ε || polarAngle < ε && d3_geo_areaRingSum < 0) ^ winding & 1; + } function d3_geo_clipCircle(radius) { var cr = Math.cos(radius), smallRadius = cr > 0, notHemisphere = abs(cr) > ε, interpolate = d3_geo_circleInterpolate(radius, 6 * d3_radians); return d3_geo_clip(visible, clipLine, interpolate, smallRadius ? [ 0, -radius ] : [ -π, radius - π ]); @@ -34786,7 +34791,7 @@ requireModule('promise/polyfill').polyfill(); a = a + "", b = b + ""; while ((am = d3_interpolate_numberA.exec(a)) && (bm = d3_interpolate_numberB.exec(b))) { if ((bs = bm.index) > bi) { - bs = b.substring(bi, bs); + bs = b.slice(bi, bs); if (s[i]) s[i] += bs; else s[++i] = bs; } if ((am = am[0]) === (bm = bm[0])) { @@ -34801,7 +34806,7 @@ requireModule('promise/polyfill').polyfill(); bi = d3_interpolate_numberB.lastIndex; } if (bi < b.length) { - bs = b.substring(bi); + bs = b.slice(bi); if (s[i]) s[i] += bs; else s[++i] = bs; } return s.length < 2 ? q[0] ? (b = q[0].x, function(t) { @@ -34871,7 +34876,7 @@ requireModule('promise/polyfill').polyfill(); } }); d3.ease = function(name) { - var i = name.indexOf("-"), t = i >= 0 ? name.substring(0, i) : name, m = i >= 0 ? name.substring(i + 1) : "in"; + var i = name.indexOf("-"), t = i >= 0 ? name.slice(0, i) : name, m = i >= 0 ? name.slice(i + 1) : "in"; t = d3_ease.get(t) || d3_ease_default; m = d3_ease_mode.get(m) || d3_identity; return d3_ease_clamp(m(t.apply(null, d3_arraySlice.call(arguments, 1)))); @@ -40084,6 +40089,21 @@ if (typeof exports === 'object') { }); + // state.coffee + root.require.register('burnchart/src/models/state.js', function(exports, require, module) { + + var Model; + + Model = require('../utils/model'); + + module.exports = new Model({ + 'data': { + 'loading': false + } + }); + + }); + // user.coffee root.require.register('burnchart/src/models/user.js', function(exports, require, module) { @@ -40688,10 +40708,12 @@ if (typeof exports === 'object') { // router.coffee root.require.register('burnchart/src/modules/router.js', function(exports, require, module) { - var el, mediator, route, router; + var el, mediator, route, router, state; mediator = require('./mediator'); + state = require('../models/state'); + el = '#page'; route = function(page, req, evt) { @@ -40712,6 +40734,10 @@ if (typeof exports === 'object') { 'reset': function() { mediator.fire('!projects/clear'); return window.location.hash = '#'; + }, + 'notify': function() { + window.location.hash = '#'; + return state.set('loading', true); } }; @@ -40725,43 +40751,55 @@ if (typeof exports === 'object') { // header.mustache root.require.register('burnchart/src/templates/header.js', function(exports, require, module) { - module.exports = ["
","
"," {{#user.displayName}}"," {{user.displayName}} logged in"," {{else}}"," Sign In"," {{/user.displayName}}","
","","

","","
"," "," "," ","
",""," ","
"].join("\n"); + module.exports = ["
","
"," {{#user.displayName}}"," {{user.displayName}} logged in"," {{else}}"," Sign In"," {{/user.displayName}}","
",""," "," "," ","","
"," "," "," ","
",""," ","
"].join("\n"); }); // hero.mustache root.require.register('burnchart/src/templates/hero.js', function(exports, require, module) { - module.exports = ["{{^projects.list}}","
","
"," ","

See your project progress

","

Not sure where to start? Just add a demo repo to see a chart. There are many variations of passages of Lorem Ipsum available, but the majority have suffered alteration in some form, by injected humour, or randomised words which don't look even slightly believable.

"," ","
","
","{{/projects.list}}"].join("\n"); + module.exports = ["{{^projects.list}}","
","
"," ","

See your project progress

","

Not sure where to start? Just add a demo repo to see a chart. There are many variations of passages of Lorem Ipsum available, but the majority have suffered alteration in some form, by injected humour, or randomised words which don't look even slightly believable.

"," ","
","
","{{/projects.list}}"].join("\n"); + }); + + // icons.mustache + root.require.register('burnchart/src/templates/icons.js', function(exports, require, module) { + + module.exports = ["{{#code}}"," {{{ '&#' + code + ';' }}}","{{/code}}"].join("\n"); }); // layout.mustache root.require.register('burnchart/src/templates/layout.js', function(exports, require, module) { - module.exports = ["
","","
"," ","
","","
","
"," © 2012-2014 Cloudfire Systems","
","
"].join("\n"); + module.exports = ["
","","
"," ","
","","
","
"," © 2012-2014 Cloudfire Systems","
","
"].join("\n"); + }); + + // notify.mustache + root.require.register('burnchart/src/templates/notify.js', function(exports, require, module) { + + module.exports = ["","

You have some interesting news in your inbox. Go check it out now.

"].join("\n"); }); // addProject.mustache root.require.register('burnchart/src/templates/pages/addProject.js', function(exports, require, module) { - module.exports = ["
","
","
","

Add a Project

","

Type in the name of the repository as you would normally. If you'd like to add a private GitHub project, Sign In first.

","
","","
"," "," "," "," "," ","
"," "," "," Add","
","
","
","
"].join("\n"); + module.exports = ["
","
","
","

Add a Project

","

Type in the name of the repository as you would normally. If you'd like to add a private GitHub project, Sign In first.

","
","","
"," "," "," "," "," ","
"," "," "," Add","
","
","
","
"].join("\n"); }); // index.mustache root.require.register('burnchart/src/templates/pages/index.js', function(exports, require, module) { - module.exports = ["
"," "," ","
"].join("\n"); + module.exports = ["
"," "," ","
"].join("\n"); }); // showChart.mustache root.require.register('burnchart/src/templates/pages/showChart.js', function(exports, require, module) { - module.exports = ["
","
","

{{ format.title(milestone.title) }}

"," {{{ format.due(milestone.due_on) }}}","

{{{ format.markdown(milestone.description) }}}

","
","
","","
","
","
","
","
"].join("\n"); + module.exports = ["
","
","

{{ format.title(milestone.title) }}

"," {{{ format.due(milestone.due_on) }}}","

{{{ format.markdown(milestone.description) }}}

","
","
","","
","
","
","
","
"].join("\n"); }); // projects.mustache root.require.register('burnchart/src/templates/projects.js', function(exports, require, module) { - module.exports = ["{{#projects.list.length}}","
","
"," Sorted by priority","

Projects

","
",""," "," {{#projects.list}}"," {{#milestones}}"," "," "," "," "," "," {{/milestones}}"," {{/projects.list}}",""," ","
{{owner}}/{{name}}"," {{ title }}"," ","
"," {{Math.floor(format.progress(closed_issues, open_issues))}}%"," {{{ format.due(due_on) }}}","
","
","
","
","
","","
"," Edit","
","
","{{/projects.list}}"].join("\n"); + module.exports = ["{{#projects.list.length}}","
","
"," Sorted by priority","

Projects

","
",""," "," {{#projects.list}}"," {{#milestones}}"," "," "," "," "," "," {{/milestones}}"," {{/projects.list}}",""," ","
{{owner}}/{{name}}"," {{ title }}"," ","
"," {{Math.floor(format.progress(closed_issues, open_issues))}}%"," {{{ format.due(due_on) }}}","
","
","
","
","
","","
"," Edit","
","
","{{/projects.list}}"].join("\n"); }); // date.coffee @@ -40818,6 +40856,9 @@ if (typeof exports === 'object') { } else { return ['Milestone', text].join(' '); } + }, + hexToDecimal: function(hex) { + return parseInt(hex, 16); } }; @@ -40860,7 +40901,7 @@ if (typeof exports === 'object') { // header.coffee root.require.register('burnchart/src/views/header.js', function(exports, require, module) { - var firebase, mediator, user; + var Icons, firebase, mediator, state, user; firebase = require('../modules/firebase'); @@ -40868,19 +40909,31 @@ if (typeof exports === 'object') { user = require('../models/user'); + state = require('../models/state'); + + Icons = require('./icons'); + module.exports = Ractive.extend({ 'template': require('../templates/header'), + 'data': { + 'user': user, + 'icon': 'fire-station' + }, init: function() { - return this.on('!login', function() { + var _this = this; + this.on('!login', function() { return firebase.login(function(err) { if (err) { throw err; } }); }); + return state.observe('loading', function(val) { + return _this.set('icon', val ? 'spin4' : 'fire-station'); + }); }, - 'data': { - user: user + 'components': { + Icons: Icons }, 'adapt': [Ractive.adaptors.Ractive] }); @@ -40890,22 +40943,65 @@ if (typeof exports === 'object') { // hero.coffee root.require.register('burnchart/src/views/hero.js', function(exports, require, module) { - var mediator, projects; + var Icons, mediator, projects; mediator = require('../modules/mediator'); projects = require('../models/projects'); + Icons = require('./icons'); + module.exports = Ractive.extend({ 'template': require('../templates/hero'), 'data': { projects: projects }, + 'components': { + Icons: Icons + }, 'adapt': [Ractive.adaptors.Ractive] }); }); + // icons.coffee + root.require.register('burnchart/src/views/icons.js', function(exports, require, module) { + + var codes, utils; + + utils = require('../utils/format'); + + codes = { + 'cog': '\e800', + 'search': '\e801', + 'github': '\e802', + 'address': '\e803', + 'plus-circled': '\e804', + 'fire-station': '\e805', + 'sort-alphabet': '\e806', + 'down-open': '\e807', + 'spin6': '\e808', + 'megaphone': '\e809', + 'spin4': '\e80a' + }; + + module.exports = Ractive.extend({ + 'template': require('../templates/icons'), + 'isolated': true, + init: function() { + return this.observe('icon', function(icon) { + var hex; + if (icon && (hex = codes[icon])) { + return this.set('code', utils.hexToDecimal(hex)); + } else { + return this.set('code', null); + } + }); + } + }); + + }); + // addProject.coffee root.require.register('burnchart/src/views/pages/addProject.js', function(exports, require, module) { @@ -41017,17 +41113,22 @@ if (typeof exports === 'object') { // projects.coffee root.require.register('burnchart/src/views/projects.js', function(exports, require, module) { - var mediator, projects; + var Icons, mediator, projects; mediator = require('../modules/mediator'); projects = require('../models/projects'); + Icons = require('./icons'); + module.exports = Ractive.extend({ 'template': require('../templates/projects'), 'data': { projects: projects }, + 'components': { + Icons: Icons + }, 'adapt': [Ractive.adaptors.Ractive] }); diff --git a/public/js/app.js b/public/js/app.js index 2298c1b..d842f57 100644 --- a/public/js/app.js +++ b/public/js/app.js @@ -118,6 +118,21 @@ }); + // state.coffee + root.require.register('burnchart/src/models/state.js', function(exports, require, module) { + + var Model; + + Model = require('../utils/model'); + + module.exports = new Model({ + 'data': { + 'loading': false + } + }); + + }); + // user.coffee root.require.register('burnchart/src/models/user.js', function(exports, require, module) { @@ -722,10 +737,12 @@ // router.coffee root.require.register('burnchart/src/modules/router.js', function(exports, require, module) { - var el, mediator, route, router; + var el, mediator, route, router, state; mediator = require('./mediator'); + state = require('../models/state'); + el = '#page'; route = function(page, req, evt) { @@ -746,6 +763,10 @@ 'reset': function() { mediator.fire('!projects/clear'); return window.location.hash = '#'; + }, + 'notify': function() { + window.location.hash = '#'; + return state.set('loading', true); } }; @@ -759,43 +780,55 @@ // header.mustache root.require.register('burnchart/src/templates/header.js', function(exports, require, module) { - module.exports = ["
","
"," {{#user.displayName}}"," {{user.displayName}} logged in"," {{else}}"," Sign In"," {{/user.displayName}}","
","","

","","
"," "," "," ","
",""," ","
"].join("\n"); + module.exports = ["
","
"," {{#user.displayName}}"," {{user.displayName}} logged in"," {{else}}"," Sign In"," {{/user.displayName}}","
",""," "," "," ","","
"," "," "," ","
",""," ","
"].join("\n"); }); // hero.mustache root.require.register('burnchart/src/templates/hero.js', function(exports, require, module) { - module.exports = ["{{^projects.list}}","
","
"," ","

See your project progress

","

Not sure where to start? Just add a demo repo to see a chart. There are many variations of passages of Lorem Ipsum available, but the majority have suffered alteration in some form, by injected humour, or randomised words which don't look even slightly believable.

"," ","
","
","{{/projects.list}}"].join("\n"); + module.exports = ["{{^projects.list}}","
","
"," ","

See your project progress

","

Not sure where to start? Just add a demo repo to see a chart. There are many variations of passages of Lorem Ipsum available, but the majority have suffered alteration in some form, by injected humour, or randomised words which don't look even slightly believable.

"," ","
","
","{{/projects.list}}"].join("\n"); + }); + + // icons.mustache + root.require.register('burnchart/src/templates/icons.js', function(exports, require, module) { + + module.exports = ["{{#code}}"," {{{ '&#' + code + ';' }}}","{{/code}}"].join("\n"); }); // layout.mustache root.require.register('burnchart/src/templates/layout.js', function(exports, require, module) { - module.exports = ["
","","
"," ","
","","
","
"," © 2012-2014 Cloudfire Systems","
","
"].join("\n"); + module.exports = ["
","","
"," ","
","","
","
"," © 2012-2014 Cloudfire Systems","
","
"].join("\n"); + }); + + // notify.mustache + root.require.register('burnchart/src/templates/notify.js', function(exports, require, module) { + + module.exports = ["","

You have some interesting news in your inbox. Go check it out now.

"].join("\n"); }); // addProject.mustache root.require.register('burnchart/src/templates/pages/addProject.js', function(exports, require, module) { - module.exports = ["
","
","
","

Add a Project

","

Type in the name of the repository as you would normally. If you'd like to add a private GitHub project, Sign In first.

","
","","
"," "," "," "," "," ","
"," "," "," Add","
","
","
","
"].join("\n"); + module.exports = ["
","
","
","

Add a Project

","

Type in the name of the repository as you would normally. If you'd like to add a private GitHub project, Sign In first.

","
","","
"," "," "," "," "," ","
"," "," "," Add","
","
","
","
"].join("\n"); }); // index.mustache root.require.register('burnchart/src/templates/pages/index.js', function(exports, require, module) { - module.exports = ["
"," "," ","
"].join("\n"); + module.exports = ["
"," "," ","
"].join("\n"); }); // showChart.mustache root.require.register('burnchart/src/templates/pages/showChart.js', function(exports, require, module) { - module.exports = ["
","
","

{{ format.title(milestone.title) }}

"," {{{ format.due(milestone.due_on) }}}","

{{{ format.markdown(milestone.description) }}}

","
","
","","
","
","
","
","
"].join("\n"); + module.exports = ["
","
","

{{ format.title(milestone.title) }}

"," {{{ format.due(milestone.due_on) }}}","

{{{ format.markdown(milestone.description) }}}

","
","
","","
","
","
","
","
"].join("\n"); }); // projects.mustache root.require.register('burnchart/src/templates/projects.js', function(exports, require, module) { - module.exports = ["{{#projects.list.length}}","
","
"," Sorted by priority","

Projects

","
",""," "," {{#projects.list}}"," {{#milestones}}"," "," "," "," "," "," {{/milestones}}"," {{/projects.list}}",""," ","
{{owner}}/{{name}}"," {{ title }}"," ","
"," {{Math.floor(format.progress(closed_issues, open_issues))}}%"," {{{ format.due(due_on) }}}","
","
","
","
","
","","
"," Edit","
","
","{{/projects.list}}"].join("\n"); + module.exports = ["{{#projects.list.length}}","
","
"," Sorted by priority","

Projects

","
",""," "," {{#projects.list}}"," {{#milestones}}"," "," "," "," "," "," {{/milestones}}"," {{/projects.list}}",""," ","
{{owner}}/{{name}}"," {{ title }}"," ","
"," {{Math.floor(format.progress(closed_issues, open_issues))}}%"," {{{ format.due(due_on) }}}","
","
","
","
","
","","
"," Edit","
","
","{{/projects.list}}"].join("\n"); }); // date.coffee @@ -852,6 +885,9 @@ } else { return ['Milestone', text].join(' '); } + }, + hexToDecimal: function(hex) { + return parseInt(hex, 16); } }; @@ -894,7 +930,7 @@ // header.coffee root.require.register('burnchart/src/views/header.js', function(exports, require, module) { - var firebase, mediator, user; + var Icons, firebase, mediator, state, user; firebase = require('../modules/firebase'); @@ -902,19 +938,31 @@ user = require('../models/user'); + state = require('../models/state'); + + Icons = require('./icons'); + module.exports = Ractive.extend({ 'template': require('../templates/header'), + 'data': { + 'user': user, + 'icon': 'fire-station' + }, init: function() { - return this.on('!login', function() { + var _this = this; + this.on('!login', function() { return firebase.login(function(err) { if (err) { throw err; } }); }); + return state.observe('loading', function(val) { + return _this.set('icon', val ? 'spin4' : 'fire-station'); + }); }, - 'data': { - user: user + 'components': { + Icons: Icons }, 'adapt': [Ractive.adaptors.Ractive] }); @@ -924,22 +972,65 @@ // hero.coffee root.require.register('burnchart/src/views/hero.js', function(exports, require, module) { - var mediator, projects; + var Icons, mediator, projects; mediator = require('../modules/mediator'); projects = require('../models/projects'); + Icons = require('./icons'); + module.exports = Ractive.extend({ 'template': require('../templates/hero'), 'data': { projects: projects }, + 'components': { + Icons: Icons + }, 'adapt': [Ractive.adaptors.Ractive] }); }); + // icons.coffee + root.require.register('burnchart/src/views/icons.js', function(exports, require, module) { + + var codes, utils; + + utils = require('../utils/format'); + + codes = { + 'cog': '\e800', + 'search': '\e801', + 'github': '\e802', + 'address': '\e803', + 'plus-circled': '\e804', + 'fire-station': '\e805', + 'sort-alphabet': '\e806', + 'down-open': '\e807', + 'spin6': '\e808', + 'megaphone': '\e809', + 'spin4': '\e80a' + }; + + module.exports = Ractive.extend({ + 'template': require('../templates/icons'), + 'isolated': true, + init: function() { + return this.observe('icon', function(icon) { + var hex; + if (icon && (hex = codes[icon])) { + return this.set('code', utils.hexToDecimal(hex)); + } else { + return this.set('code', null); + } + }); + } + }); + + }); + // addProject.coffee root.require.register('burnchart/src/views/pages/addProject.js', function(exports, require, module) { @@ -1051,17 +1142,22 @@ // projects.coffee root.require.register('burnchart/src/views/projects.js', function(exports, require, module) { - var mediator, projects; + var Icons, mediator, projects; mediator = require('../modules/mediator'); projects = require('../models/projects'); + Icons = require('./icons'); + module.exports = Ractive.extend({ 'template': require('../templates/projects'), 'data': { projects: projects }, + 'components': { + Icons: Icons + }, 'adapt': [Ractive.adaptors.Ractive] }); diff --git a/src/app.coffee b/src/app.coffee index a040ae3..fdfcf60 100644 --- a/src/app.coffee +++ b/src/app.coffee @@ -1,6 +1,6 @@ ( require "./#{key}" for key in [ - 'utils/mixins' - 'models/projects' + 'utils/mixins' + 'models/projects' ] ) @@ -8,12 +8,12 @@ Router = require './modules/router' Header = require './views/header' App = Ractive.extend - - 'template': require './templates/layout' + + 'template': require './templates/layout' - 'components': { Header } + 'components': { Header } - init: -> - new Router() + init: -> + new Router() module.exports = new App() \ No newline at end of file diff --git a/src/models/config.coffee b/src/models/config.coffee index aa1102d..ad87342 100644 --- a/src/models/config.coffee +++ b/src/models/config.coffee @@ -2,32 +2,32 @@ Model = require '../utils/model' module.exports = new Model - "data": - # Firebase app name. - "firebase": "burnchart" - # Data source provider. - "provider": "github" - # Fields to keep from GH responses. - "fields": - "milestone": [ - "closed_issues" - "created_at" - "description" - "due_on" - "number" - "open_issues" - "title" - "updated_at" - ] - # Chart configuration. - "chart": - # Days we are not working. - "off_days": [ ] - # How do we parse GitHub dates? - "datetime": /^(\d{4}-\d{2}-\d{2})T(.*)/ - # How does a size label look like? - "size_label": /^size (\d+)$/ - # How do we specify which user/repo/(milestone) we want? - "location": /^#!((\/[^\/]+){2,3})$/ - # Process all issues as one size or use labels. - "points": 'ONE_SIZE' \ No newline at end of file + "data": + # Firebase app name. + "firebase": "burnchart" + # Data source provider. + "provider": "github" + # Fields to keep from GH responses. + "fields": + "milestone": [ + "closed_issues" + "created_at" + "description" + "due_on" + "number" + "open_issues" + "title" + "updated_at" + ] + # Chart configuration. + "chart": + # Days we are not working. + "off_days": [ ] + # How do we parse GitHub dates? + "datetime": /^(\d{4}-\d{2}-\d{2})T(.*)/ + # How does a size label look like? + "size_label": /^size (\d+)$/ + # How do we specify which user/repo/(milestone) we want? + "location": /^#!((\/[^\/]+){2,3})$/ + # Process all issues as one size or use labels. + "points": 'ONE_SIZE' \ No newline at end of file diff --git a/src/models/projects.coffee b/src/models/projects.coffee index 4e8ab2d..a2e58d8 100644 --- a/src/models/projects.coffee +++ b/src/models/projects.coffee @@ -7,38 +7,38 @@ user = require './user' module.exports = new Model - 'data': - 'list': [] + 'data': + 'list': [] - init: -> - # Initialize with items stored locally. - localforage.getItem 'projects', (projects=[]) => - # Fetch milestones for each of these projects. - async.each projects, (project, cb) -> - mediator.fire '!projects/add', project - , (err) -> - throw err if err + init: -> + # Initialize with items stored locally. + localforage.getItem 'projects', (projects=[]) => + # Fetch milestones for each of these projects. + async.each projects, (project, cb) -> + mediator.fire '!projects/add', project + , (err) -> + throw err if err - # Persist projects in local storage. - @observe 'list', (projects) -> - localforage.setItem 'projects', projects + # Persist projects in local storage. + @observe 'list', (projects) -> + localforage.setItem 'projects', projects - mediator.on '!projects/add', (repo, done) => - # TODO: warn when we are adding an existing repo (or - # silently go to index again). + mediator.on '!projects/add', (repo, done) => + # TODO: warn when we are adding an existing repo (or + # silently go to index again). - # Fetch milestones (which validates repo too). - request.allMilestones repo, (err, res) => - return done err if err + # Fetch milestones (which validates repo too). + request.allMilestones repo, (err, res) => + return done err if err - # Pluck these fields for milestones. - milestones = _.pluckMany res, config.get('fields.milestone') + # Pluck these fields for milestones. + milestones = _.pluckMany res, config.get('fields.milestone') - # Push to the stack. - @push 'list', _.merge repo, { milestones } + # Push to the stack. + @push 'list', _.merge repo, { milestones } - # Call back. - do done + # Call back. + do done - mediator.on '!projects/clear', => - @set 'list', [] \ No newline at end of file + mediator.on '!projects/clear', => + @set 'list', [] \ No newline at end of file diff --git a/src/models/state.coffee b/src/models/state.coffee new file mode 100644 index 0000000..b36a105 --- /dev/null +++ b/src/models/state.coffee @@ -0,0 +1,7 @@ +Model = require '../utils/model' + +module.exports = new Model + + 'data': + # Are we loading stuff? + 'loading': no \ No newline at end of file diff --git a/src/models/user.coffee b/src/models/user.coffee index f55e015..88ddc78 100644 --- a/src/models/user.coffee +++ b/src/models/user.coffee @@ -1,12 +1,12 @@ mediator = require '../modules/mediator' -Model = require '../utils/model' +Model = require '../utils/model' # Currently logged-in user. module.exports = new Model - # Default to a local user. - 'data': - 'provider': "local" - 'id': "0" - 'uid': "local:0" - 'token': null \ No newline at end of file + # Default to a local user. + 'data': + 'provider': "local" + 'id': "0" + 'uid': "local:0" + 'token': null \ No newline at end of file diff --git a/src/modules/chart.coffee b/src/modules/chart.coffee index be7b037..fe116f5 100644 --- a/src/modules/chart.coffee +++ b/src/modules/chart.coffee @@ -2,243 +2,243 @@ config = require '../models/config' module.exports = + + # A graph of closed issues. + # `collection`: issues + # `created_at`: milestone start date + # `total`: total number of points (open & closed issues) + 'actual': (collection, created_at, total, cb) -> + head = [ { + 'date': new Date created_at + 'points': total + } ] - # A graph of closed issues. - # `collection`: issues - # `created_at`: milestone start date - # `total`: total number of points (open & closed issues) - 'actual': (collection, created_at, total, cb) -> - head = [ { - 'date': new Date created_at - 'points': total - } ] - - min = +Infinity ; max = -Infinity + min = +Infinity ; max = -Infinity - # Generate the actual closes. - rest = _.map collection, (issue) -> - { size, closed_at } = issue - # Determine the range. - min = size if size < min - max = size if size > max + # Generate the actual closes. + rest = _.map collection, (issue) -> + { size, closed_at } = issue + # Determine the range. + min = size if size < min + max = size if size > max - # Dropping points remaining. - issue.date = new Date closed_at - issue.points = total -= size - issue - - # Now add a radius in a range (will be used for a circle). - range = d3.scale.linear().domain([ min, max ]).range([ 5, 8 ]) + # Dropping points remaining. + issue.date = new Date closed_at + issue.points = total -= size + issue + + # Now add a radius in a range (will be used for a circle). + range = d3.scale.linear().domain([ min, max ]).range([ 5, 8 ]) - rest = _.map rest, (issue) -> - issue.radius = range issue.size - issue + rest = _.map rest, (issue) -> + issue.radius = range issue.size + issue - cb null, [].concat head, rest + cb null, [].concat head, rest - # A graph of an ideal progression.. - # `a`: milestone start date - # `b`: milestone end date - # `total`: total number of points (open & closed issues) - 'ideal': (a, b, total, cb) -> - # Swap? - [ b, a ] = [ a, b ] if b < a + # A graph of an ideal progression.. + # `a`: milestone start date + # `b`: milestone end date + # `total`: total number of points (open & closed issues) + 'ideal': (a, b, total, cb) -> + # Swap? + [ b, a ] = [ a, b ] if b < a - # We start here adding days to `d`. - [ y, m, d ] = _.map a.match(config.get('chart.datetime'))[1].split('-'), (v) -> parseInt v - # We want to end here. - cutoff = new Date(b) + # We start here adding days to `d`. + [ y, m, d ] = _.map a.match(config.get('chart.datetime'))[1].split('-'), (v) -> parseInt v + # We want to end here. + cutoff = new Date(b) - # Go through the beginning to the end skipping off days. - days = [] ; length = 0 - do once = (inc = 0) -> - # A new day. - day = new Date y, m - 1, d + inc - - # Does this day count? - day_of = 7 if !day_of = day.getDay() - if day_of in config.get('chart.off_days') - days.push { date: day, off_day: yes } - else - length += 1 - days.push { date: day } - - # Go again? - once(inc + 1) unless day > cutoff + # Go through the beginning to the end skipping off days. + days = [] ; length = 0 + do once = (inc = 0) -> + # A new day. + day = new Date y, m - 1, d + inc + + # Does this day count? + day_of = 7 if !day_of = day.getDay() + if day_of in config.get('chart.off_days') + days.push { date: day, off_day: yes } + else + length += 1 + days.push { date: day } + + # Go again? + once(inc + 1) unless day > cutoff - # Map points on the array of days now. - velocity = total / (length - 1) + # Map points on the array of days now. + velocity = total / (length - 1) - days = _.map days, (day, i) -> - day.points = total - total -= velocity if days[i] and not days[i].off_day - day + days = _.map days, (day, i) -> + day.points = total + total -= velocity if days[i] and not days[i].off_day + day - # Do we need to make a link to right now? - days.push { date: now, points: 0 } if (now = new Date()) > cutoff + # Do we need to make a link to right now? + days.push { date: now, points: 0 } if (now = new Date()) > cutoff - cb null, days + cb null, days - # Graph representing a trendling of actual issues. - 'trendline': (actual, created_at, due_on) -> - start = +actual[0].date + # Graph representing a trendling of actual issues. + 'trendline': (actual, created_at, due_on) -> + start = +actual[0].date - # Values is a list of time from the start and points remaining. - values = _.map actual, ({ date, points }) -> - [ +date - start, points ] + # Values is a list of time from the start and points remaining. + values = _.map actual, ({ date, points }) -> + [ +date - start, points ] - # Now is an actual point too. - last = actual[actual.length - 1] - values.push [ + new Date() - start, last.points ] + # Now is an actual point too. + last = actual[actual.length - 1] + values.push [ + new Date() - start, last.points ] - # http://classroom.synonym.com/calculate-trendline-2709.html - b1 = 0 ; e = 0 ; c1 = 0 - a = (l = values.length) * _.reduce(values, (sum, [ a, b ]) -> - b1 += a ; e += b - c1 += Math.pow(a, 2) - sum + (a * b) - , 0) + # http://classroom.synonym.com/calculate-trendline-2709.html + b1 = 0 ; e = 0 ; c1 = 0 + a = (l = values.length) * _.reduce(values, (sum, [ a, b ]) -> + b1 += a ; e += b + c1 += Math.pow(a, 2) + sum + (a * b) + , 0) - slope = (a - (b1 * e)) / ((l * c1) - (Math.pow(b1, 2))) - intercept = (e - (slope * b1)) / l - fn = (x) -> slope * x + intercept + slope = (a - (b1 * e)) / ((l * c1) - (Math.pow(b1, 2))) + intercept = (e - (slope * b1)) / l + fn = (x) -> slope * x + intercept - # Milestone always has a creation date. - created_at = new Date created_at - # Due date can be empty. - due_on = if due_on then new Date(due_on) else new Date() + # Milestone always has a creation date. + created_at = new Date created_at + # Due date can be empty. + due_on = if due_on then new Date(due_on) else new Date() - a = created_at - start - b = due_on - start + a = created_at - start + b = due_on - start - [ - { - date: created_at - points: fn(a) - }, { - date: due_on - points: fn(b) - } - ] + [ + { + date: created_at + points: fn(a) + }, { + date: due_on + points: fn(b) + } + ] - # The graph as a whole. - 'render': ([ actual, ideal, trendline ], cb) -> - document.querySelector('#svg').innerHTML = '' + # The graph as a whole. + 'render': ([ actual, ideal, trendline ], cb) -> + document.querySelector('#svg').innerHTML = '' - # Get available space. - { height, width } = document.querySelector('#chart').getBoundingClientRect() + # Get available space. + { height, width } = document.querySelector('#chart').getBoundingClientRect() - margin = { 'top': 30, 'right': 30, 'bottom': 40, 'left': 50 } - width -= margin.left + margin.right - height -= margin.top + margin.bottom + margin = { 'top': 30, 'right': 30, 'bottom': 40, 'left': 50 } + width -= margin.left + margin.right + height -= margin.top + margin.bottom - # Scales. - x = d3.time.scale().range([ 0, width ]) - y = d3.scale.linear().range([ height, 0 ]) + # Scales. + x = d3.time.scale().range([ 0, width ]) + y = d3.scale.linear().range([ height, 0 ]) - # Axes. - xAxis = d3.svg.axis().scale(x) - .orient("bottom") - # Show vertical lines... - .tickSize(-height) - # ...with day of the month... - .tickFormat( (d) -> d.getDate() ) - # ...and give us a spacer. - .tickPadding(10) + # Axes. + xAxis = d3.svg.axis().scale(x) + .orient("bottom") + # Show vertical lines... + .tickSize(-height) + # ...with day of the month... + .tickFormat( (d) -> d.getDate() ) + # ...and give us a spacer. + .tickPadding(10) - yAxis = d3.svg.axis().scale(y) - .orient("left") - .tickSize(-width) - .ticks(5) - .tickPadding(10) - - # Line generator. - line = d3.svg.line() - .interpolate("linear") - .x( (d) -> x(d.date) ) - .y( (d) -> y(d.points) ) + yAxis = d3.svg.axis().scale(y) + .orient("left") + .tickSize(-width) + .ticks(5) + .tickPadding(10) + + # Line generator. + line = d3.svg.line() + .interpolate("linear") + .x( (d) -> x(d.date) ) + .y( (d) -> y(d.points) ) - # Get the minimum and maximum date, and initial points. - x.domain([ ideal[0].date, ideal[ideal.length - 1].date ]) - y.domain([ 0, ideal[0].points ]).nice() + # Get the minimum and maximum date, and initial points. + x.domain([ ideal[0].date, ideal[ideal.length - 1].date ]) + y.domain([ 0, ideal[0].points ]).nice() - # Add an SVG element with the desired dimensions and margin. - svg = d3.select("#svg").append("svg") - .attr("width", width + margin.left + margin.right) - .attr("height", height + margin.top + margin.bottom) - .append("g") - .attr("transform", "translate(" + margin.left + "," + margin.top + ")") + # Add an SVG element with the desired dimensions and margin. + svg = d3.select("#svg").append("svg") + .attr("width", width + margin.left + margin.right) + .attr("height", height + margin.top + margin.bottom) + .append("g") + .attr("transform", "translate(" + margin.left + "," + margin.top + ")") - # Add the days x-axis. - svg.append("g") - .attr("class", "x axis day") - .attr("transform", "translate(0,#{height})") - .call(xAxis) + # Add the days x-axis. + svg.append("g") + .attr("class", "x axis day") + .attr("transform", "translate(0,#{height})") + .call(xAxis) - # Add the months x-axis. - m = [ - 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', - 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec' - ] + # Add the months x-axis. + m = [ + 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', + 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec' + ] - mAxis = xAxis - .orient("top") - .tickSize(height) - .tickFormat( (d) -> m[d.getMonth()] ) - .ticks(2) - - svg.append("g") - .attr("class", "x axis month") - .attr("transform", "translate(0,#{height})") - .call(mAxis) + mAxis = xAxis + .orient("top") + .tickSize(height) + .tickFormat( (d) -> m[d.getMonth()] ) + .ticks(2) + + svg.append("g") + .attr("class", "x axis month") + .attr("transform", "translate(0,#{height})") + .call(mAxis) - # Add the y-axis. - svg.append("g") - .attr("class", "y axis") - .call(yAxis) + # Add the y-axis. + svg.append("g") + .attr("class", "y axis") + .call(yAxis) - # Add a line showing where we are now. - svg.append("svg:line") - .attr("class", "today") - .attr("x1", x(new Date())) - .attr("y1", 0) - .attr("x2", x(new Date())) - .attr("y2", height) + # Add a line showing where we are now. + svg.append("svg:line") + .attr("class", "today") + .attr("x1", x(new Date())) + .attr("y1", 0) + .attr("x2", x(new Date())) + .attr("y2", height) - # Add the ideal line path. - svg.append("path") - .attr("class", "ideal line") - .attr("d", line.interpolate("basis")(ideal)) + # Add the ideal line path. + svg.append("path") + .attr("class", "ideal line") + .attr("d", line.interpolate("basis")(ideal)) - # Add the trendline path. - svg.append("path") - .attr("class", "trendline line") - .attr("d", line.interpolate("linear")(trendline)) + # Add the trendline path. + svg.append("path") + .attr("class", "trendline line") + .attr("d", line.interpolate("linear")(trendline)) - # Add the actual line path. - svg.append("path") - .attr("class", "actual line") - .attr("d", line.interpolate("linear").y( (d) -> y(d.points) )(actual)) + # Add the actual line path. + svg.append("path") + .attr("class", "actual line") + .attr("d", line.interpolate("linear").y( (d) -> y(d.points) )(actual)) - # Collect the tooltip here. - tooltip = d3.tip().attr('class', 'd3-tip').html ({ number, title }) -> - "##{number}: #{title}" + # Collect the tooltip here. + tooltip = d3.tip().attr('class', 'd3-tip').html ({ number, title }) -> + "##{number}: #{title}" - svg.call(tooltip) + svg.call(tooltip) - # Show when we closed an issue. - svg.selectAll("a.issue") - .data(actual.slice(1)) # skip the starting point - .enter() - # A wrapping link. - .append('svg:a') - .attr("xlink:href", ({ html_url }) -> html_url ) - .attr("xlink:show", 'new') - .append('svg:circle') - .attr("cx", ({ date }) -> x date ) - .attr("cy", ({ points }) -> y points ) - .attr("r", ({ radius }) -> 5 ) # fixed for now - .on('mouseover', tooltip.show) - .on('mouseout', tooltip.hide) + # Show when we closed an issue. + svg.selectAll("a.issue") + .data(actual.slice(1)) # skip the starting point + .enter() + # A wrapping link. + .append('svg:a') + .attr("xlink:href", ({ html_url }) -> html_url ) + .attr("xlink:show", 'new') + .append('svg:circle') + .attr("cx", ({ date }) -> x date ) + .attr("cy", ({ points }) -> y points ) + .attr("r", ({ radius }) -> 5 ) # fixed for now + .on('mouseover', tooltip.show) + .on('mouseout', tooltip.hide) - cb null \ No newline at end of file + cb null \ No newline at end of file diff --git a/src/modules/firebase.coffee b/src/modules/firebase.coffee index 66a622a..5c7081d 100644 --- a/src/modules/firebase.coffee +++ b/src/modules/firebase.coffee @@ -3,36 +3,36 @@ config = require '../models/config' # Default "silent" callback for auth. class Class + + constructor: -> + # Setup a new client. + @client = new Firebase "https://#{config.get('firebase')}.firebaseio.com" - constructor: -> - # Setup a new client. - @client = new Firebase "https://#{config.get('firebase')}.firebaseio.com" - - # Check if we have a user in session. - @auth = new FirebaseSimpleLogin @client, (err, obj) => - return @authCb err if err or not obj + # Check if we have a user in session. + @auth = new FirebaseSimpleLogin @client, (err, obj) => + return @authCb err if err or not obj - # Save user. - user.set obj + # Save user. + user.set obj - # Default "blank" callback. - authCb: -> + # Default "blank" callback. + authCb: -> - # Login a user. - login: (cb) -> - return cb 'Client is not setup' unless @client - - # Override the default auth callback. - @authCb = cb + # Login a user. + login: (cb) -> + return cb 'Client is not setup' unless @client + + # Override the default auth callback. + @authCb = cb - # Login. - @auth.login config.get('provider'), - 'rememberMe': yes - 'scope': 'public_repo' + # Login. + @auth.login config.get('provider'), + 'rememberMe': yes + 'scope': 'public_repo' - # Logout a user. - logout: -> - @auth?.logout - do user.reset + # Logout a user. + logout: -> + @auth?.logout + do user.reset module.exports = new Class() \ No newline at end of file diff --git a/src/modules/issues.coffee b/src/modules/issues.coffee index 6d84cd1..243d22a 100644 --- a/src/modules/issues.coffee +++ b/src/modules/issues.coffee @@ -4,67 +4,67 @@ request = require './request' module.exports = - # Used on an initial fetch of issues for a repo. - 'get_all': (opts, cb) -> - # For each state... - one_status = (state, cb) -> - # Concat them here. - results = [] - # One pageful fetch (next pages in series). - do fetch_page = (page = 1) -> - request.allIssues opts, { - 'milestone': opts.milestone.number, - state, page - }, (err, data) -> - # Errors? - return cb err if err - # Empty? - return cb null, results unless data.length - # Concat sorted (API does not sort on closed_at!). - results = results.concat _.sortBy data, 'closed_at' - # < 100 results? - return cb null, results if data.length < 100 - # Fetch the next page then. - fetch_page page + 1 + # Used on an initial fetch of issues for a repo. + 'get_all': (opts, cb) -> + # For each state... + one_status = (state, cb) -> + # Concat them here. + results = [] + # One pageful fetch (next pages in series). + do fetch_page = (page = 1) -> + request.allIssues opts, { + 'milestone': opts.milestone.number, + state, page + }, (err, data) -> + # Errors? + return cb err if err + # Empty? + return cb null, results unless data.length + # Concat sorted (API does not sort on closed_at!). + results = results.concat _.sortBy data, 'closed_at' + # < 100 results? + return cb null, results if data.length < 100 + # Fetch the next page then. + fetch_page page + 1 - # For each `open` and `closed` issues in parallel. - async.parallel [ - _.partial one_status, 'open' - _.partial one_status, 'closed' - ], cb + # For each `open` and `closed` issues in parallel. + async.parallel [ + _.partial one_status, 'open' + _.partial one_status, 'closed' + ], cb - # Filter an array of incoming issues based on a regex & save size on them. - 'filter': (collection, cb) -> - # The total size of all issues. - total = 0 + # Filter an array of incoming issues based on a regex & save size on them. + 'filter': (collection, cb) -> + # The total size of all issues. + total = 0 - # Which point counting mode are we in? - switch config.get 'chart.points' - # All issues are the same size - when 'ONE_SIZE' - total = collection.length - filtered = _.map collection, (issue) -> - issue.size = 1 - issue - - # Take the points size from issue label. - when 'LABELS' - filtered = _.filter collection, (issue) -> - # Skip if no labels exist. - return no unless labels = issue.labels + # Which point counting mode are we in? + switch config.get 'chart.points' + # All issues are the same size + when 'ONE_SIZE' + total = collection.length + filtered = _.map collection, (issue) -> + issue.size = 1 + issue + + # Take the points size from issue label. + when 'LABELS' + filtered = _.filter collection, (issue) -> + # Skip if no labels exist. + return no unless labels = issue.labels - # Determine the total issue size from all labels. - issue.size = _.reduce labels, (sum, label) -> - # Not matching. - return sum unless matches = label.name.match config.get 'chart.size_label' - # Increase sum. - sum += parseInt matches[1] - , 0 - - # Increase the total. - total += issue.size + # Determine the total issue size from all labels. + issue.size = _.reduce labels, (sum, label) -> + # Not matching. + return sum unless matches = label.name.match config.get 'chart.size_label' + # Increase sum. + sum += parseInt matches[1] + , 0 + + # Increase the total. + total += issue.size - # Are we saving it? - !!issue.size + # Are we saving it? + !!issue.size - cb null, filtered, total \ No newline at end of file + cb null, filtered, total \ No newline at end of file diff --git a/src/modules/milestone.coffee b/src/modules/milestone.coffee index 8d6c1ce..a3e314b 100644 --- a/src/modules/milestone.coffee +++ b/src/modules/milestone.coffee @@ -2,34 +2,34 @@ request = require './request' module.exports = - # Get current/specified milestone for a repo. - get: (repo, cb) -> - # Get a specific milestone. - if repo.milestone - request.oneMilestone repo, repo.milestone, (err, m) -> - # Errors? - return cb err if err - # Empty milestone? - if m.open_issues + m.closed_issues is 0 - return cb null, "No issues for milestone `#{m.title}`" + # Get current/specified milestone for a repo. + get: (repo, cb) -> + # Get a specific milestone. + if repo.milestone + request.oneMilestone repo, repo.milestone, (err, m) -> + # Errors? + return cb err if err + # Empty milestone? + if m.open_issues + m.closed_issues is 0 + return cb null, "No issues for milestone `#{m.title}`" - cb null, null, m + cb null, null, m - # Get the current milestone out of many. - else - request.allMilestones repo, (err, data) -> - # Errors? - return cb err if err - # Empty warning? - return cb null, "No open milestones for repo #{repo.path}" unless data.length - # The first milestone should be ending soonest. - m = data[0] - # Filter milestones without due date. - m = _.rest data, { 'due_on' : null } - # The first milestone should be ending soonest. Prefer milestones with due dates. - m = if m[0] then m[0] else data[0] - # Empty milestone? - if m.open_issues + m.closed_issues is 0 - return cb null, "No issues for milestone `#{m.title}`" + # Get the current milestone out of many. + else + request.allMilestones repo, (err, data) -> + # Errors? + return cb err if err + # Empty warning? + return cb null, "No open milestones for repo #{repo.path}" unless data.length + # The first milestone should be ending soonest. + m = data[0] + # Filter milestones without due date. + m = _.rest data, { 'due_on' : null } + # The first milestone should be ending soonest. Prefer milestones with due dates. + m = if m[0] then m[0] else data[0] + # Empty milestone? + if m.open_issues + m.closed_issues is 0 + return cb null, "No issues for milestone `#{m.title}`" - cb null, null, m \ No newline at end of file + cb null, null, m \ No newline at end of file diff --git a/src/modules/project.coffee b/src/modules/project.coffee index 79a656d..3cda5b9 100644 --- a/src/modules/project.coffee +++ b/src/modules/project.coffee @@ -1,62 +1,62 @@ #!/usr/bin/env coffee -issues = require './issues' -chart = require './chart' +issues = require './issues' +chart = require './chart' # Setup a project and render it. module.exports = (opts, cb) -> - # Get all issues. - async.waterfall [ (cb) -> - issues.get_all opts, cb - - # Filter them to labeled ones. - (all, cb) -> - async.map all, (array, cb) -> - issues.filter array, (err, filtered, total) -> - cb err, [ filtered, total ] - , (err, [ open, closed ]) -> - return cb err if err - # Empty? - return cb 'No matching issues found' if open[1] + closed[1] is 0 - # Save the open/closed on us first. - opts.issues = - 'closed': { 'points': closed[1], 'data': closed[0] } - 'open': { 'points': open[1], 'data': open[0] } - # Do we need to move the milestone start date? - if (start = closed[0][0].closed_at) < opts.milestone.created_at - opts.milestone.created_at = start + # Get all issues. + async.waterfall [ (cb) -> + issues.get_all opts, cb + + # Filter them to labeled ones. + (all, cb) -> + async.map all, (array, cb) -> + issues.filter array, (err, filtered, total) -> + cb err, [ filtered, total ] + , (err, [ open, closed ]) -> + return cb err if err + # Empty? + return cb 'No matching issues found' if open[1] + closed[1] is 0 + # Save the open/closed on us first. + opts.issues = + 'closed': { 'points': closed[1], 'data': closed[0] } + 'open': { 'points': open[1], 'data': open[0] } + # Do we need to move the milestone start date? + if (start = closed[0][0].closed_at) < opts.milestone.created_at + opts.milestone.created_at = start - cb null - - # Create actual and ideal lines & render. - (cb) -> - total = opts.issues.open.points + opts.issues.closed.points + cb null + + # Create actual and ideal lines & render. + (cb) -> + total = opts.issues.open.points + opts.issues.closed.points - async.parallel [ - _.partial( - chart.actual, # actual line - opts.issues.closed.data, - opts.milestone.created_at, - total - ) - _.partial( - chart.ideal, # ideal line - opts.milestone.created_at, - opts.milestone.due_on, - total - ) - ], (err, values) -> - # Generate a trendline? - values.push(chart.trendline( - values[0], - opts.milestone.created_at, - opts.milestone.due_on - )) if values[0].length + async.parallel [ + _.partial( + chart.actual, # actual line + opts.issues.closed.data, + opts.milestone.created_at, + total + ) + _.partial( + chart.ideal, # ideal line + opts.milestone.created_at, + opts.milestone.due_on, + total + ) + ], (err, values) -> + # Generate a trendline? + values.push(chart.trendline( + values[0], + opts.milestone.created_at, + opts.milestone.due_on + )) if values[0].length - # Render the chart. - chart.render values, cb + # Render the chart. + chart.render values, cb - # Watch window resize from now on? - # window.onresize = doit if 'onresize' of window + # Watch window resize from now on? + # window.onresize = doit if 'onresize' of window - ], cb \ No newline at end of file + ], cb \ No newline at end of file diff --git a/src/modules/request.coffee b/src/modules/request.coffee index aaa478a..adbb396 100644 --- a/src/modules/request.coffee +++ b/src/modules/request.coffee @@ -2,124 +2,124 @@ user = require '../models/user' # Custom JSON parser. superagent.parse = - 'application/json': (res) -> - try - JSON.parse res - catch e - {} # it was not to be... + 'application/json': (res) -> + try + JSON.parse res + catch e + {} # it was not to be... # Default args. defaults = - 'github': - 'host': 'api.github.com' - 'protocol': 'https' + 'github': + 'host': 'api.github.com' + 'protocol': 'https' # Public api. module.exports = - - # Get a repo. - 'repo': (repo, cb) -> - data = _.defaults - 'path': "/repos/#{repo.owner}/#{repo.name}" - 'headers': headers user.get('token') - , defaults.github + + # Get a repo. + 'repo': (repo, cb) -> + data = _.defaults + 'path': "/repos/#{repo.owner}/#{repo.name}" + 'headers': headers user.get('token') + , defaults.github - request data, cb + request data, cb - # Get all open milestones. - 'allMilestones': (repo, cb) -> - data = _.defaults - 'path': "/repos/#{repo.owner}/#{repo.name}/milestones" - 'query': { 'state': 'open', 'sort': 'due_date', 'direction': 'asc' } - 'headers': headers user.get('token') - , defaults.github + # Get all open milestones. + 'allMilestones': (repo, cb) -> + data = _.defaults + 'path': "/repos/#{repo.owner}/#{repo.name}/milestones" + 'query': { 'state': 'open', 'sort': 'due_date', 'direction': 'asc' } + 'headers': headers user.get('token') + , defaults.github - request data, cb - - # Get one open milestone. - 'oneMilestone': (repo, number, cb) -> - data = _.defaults - 'path': "/repos/#{repo.owner}/#{repo.name}/milestones/#{number}" - 'query': { 'state': 'open', 'sort': 'due_date', 'direction': 'asc' } - 'headers': headers user.get('token') - , defaults.github + request data, cb + + # Get one open milestone. + 'oneMilestone': (repo, number, cb) -> + data = _.defaults + 'path': "/repos/#{repo.owner}/#{repo.name}/milestones/#{number}" + 'query': { 'state': 'open', 'sort': 'due_date', 'direction': 'asc' } + 'headers': headers user.get('token') + , defaults.github - request data, cb + request data, cb - # Get all issues for a state. - 'allIssues': (repo, query, cb) -> - data = _.defaults - 'path': "/repos/#{repo.owner}/#{repo.name}/issues" - 'query': _.extend query, { 'per_page': '100' } - 'headers': headers user.get('token') - , defaults.github + # Get all issues for a state. + 'allIssues': (repo, query, cb) -> + data = _.defaults + 'path': "/repos/#{repo.owner}/#{repo.name}/issues" + 'query': _.extend query, { 'per_page': '100' } + 'headers': headers user.get('token') + , defaults.github - request data, cb + request data, cb # Make a request using SuperAgent. request = ({ protocol, host, path, query, headers }, cb) -> - exited = no + exited = no - # Make the query params. - q = if query then '?' + ( "#{k}=#{v}" for k, v of query ).join('&') else '' + # Make the query params. + q = if query then '?' + ( "#{k}=#{v}" for k, v of query ).join('&') else '' - # The URI. - req = superagent.get("#{protocol}://#{host}#{path}#{q}") - # Add headers. - ( req.set(k, v) for k, v of headers ) - - # Timeout for requests that do not finish... see #32. - timeout = setTimeout -> - exited = yes - cb 'Request has timed out' - , 1e4 # give us 10s + # The URI. + req = superagent.get("#{protocol}://#{host}#{path}#{q}") + # Add headers. + ( req.set(k, v) for k, v of headers ) + + # Timeout for requests that do not finish... see #32. + timeout = setTimeout -> + exited = yes + cb 'Request has timed out' + , 1e4 # give us 10s - # Send. - req.end (err, data) -> - # Arrived too late. - return if exited - # All fine. - exited = yes - clearTimeout timeout - # Actually process the response. - response err, data, cb + # Send. + req.end (err, data) -> + # Arrived too late. + return if exited + # All fine. + exited = yes + clearTimeout timeout + # Actually process the response. + response err, data, cb # How do we respond to a response? response = (err, data, cb) -> - return cb error err if err - # 2xx? - if data.statusType isnt 2 - # Do we have a message from GitHub? - return cb data.body.message if data?.body?.message? - # Use SA one. - return cb data.error.message - # All good. - cb null, data.body + return cb error err if err + # 2xx? + if data.statusType isnt 2 + # Do we have a message from GitHub? + return cb data.body.message if data?.body?.message? + # Use SA one. + return cb data.error.message + # All good. + cb null, data.body # Give us headers. headers = (token) -> - # The defaults. - h = _.extend {}, - 'Content-Type': 'application/json' - 'Accept': 'application/vnd.github.v3' - # Add token? - h.Authorization = "token #{token}" if token? - h + # The defaults. + h = _.extend {}, + 'Content-Type': 'application/json' + 'Accept': 'application/vnd.github.v3' + # Add token? + h.Authorization = "token #{token}" if token? + h # Parse an error. error = (err) -> - switch - when _.isString err - message = err - when _.isArray err - message = err[1] - when _.isObject(err) and _.isString(err.message) - message = err.message + switch + when _.isString err + message = err + when _.isArray err + message = err[1] + when _.isObject(err) and _.isString(err.message) + message = err.message - unless message - try - message = JSON.stringify err - catch - message = do err.toString + unless message + try + message = JSON.stringify err + catch + message = do err.toString - message \ No newline at end of file + message \ No newline at end of file diff --git a/src/modules/router.coffee b/src/modules/router.coffee index 46c1920..093602b 100644 --- a/src/modules/router.coffee +++ b/src/modules/router.coffee @@ -1,22 +1,26 @@ mediator = require './mediator' +state = require '../models/state' el = '#page' route = (page, req, evt) -> - Page = require "../views/pages/#{page}" - new Page { el, 'data': { 'route': req.params } } + Page = require "../views/pages/#{page}" + new Page { el, 'data': { 'route': req.params } } router = - '': _.partial route, 'index' - 'project/add': _.partial route, 'addProject' - 'chart/:owner/:name/:milestone': _.partial route, 'showChart' - # TODO: remove in production. - 'reset': -> - mediator.fire '!projects/clear' - window.location.hash = '#' + '': _.partial route, 'index' + 'project/add': _.partial route, 'addProject' + 'chart/:owner/:name/:milestone': _.partial route, 'showChart' + # TODO: remove in production. + 'reset': -> + mediator.fire '!projects/clear' + window.location.hash = '#' + 'notify': -> + window.location.hash = '#' + state.set 'loading', yes module.exports = -> - # Init the routes. - Grapnel.listen router + # Init the routes. + Grapnel.listen router - router \ No newline at end of file + router \ No newline at end of file diff --git a/src/styles/app.styl b/src/styles/app.styl index 2d2bb9f..c408ff3 100644 --- a/src/styles/app.styl +++ b/src/styles/app.styl @@ -7,321 +7,316 @@ $strong_color = #C1041C $highlight_color = #FFBB2A body - color: #3E4457 - font-family: $sans_serif_font + color: #3E4457 + font-family: $sans_serif_font a - text-decoration: none - color: #AAAFBF - cursor: pointer + text-decoration: none + color: #AAAFBF + cursor: pointer h1, h2, h3, p - margin: 0 + margin: 0 ul - list-style-type: none - margin: 0 - padding: 0 + list-style-type: none + margin: 0 + padding: 0 - li - display: inline-block - -.icon - margin-right: 4px + li + display: inline-block .wrap - width: 800px - margin: 0 auto + width: 800px + margin: 0 auto #head - background: $strong_color - height: 64px + background: $strong_color + height: 64px - h1 - font-size: 26px - margin: 0 - padding: 10px 24px - line-height: 44px - height: 44px - background: #77000E - display: inline-block + #icon + font-size: 26px + margin: 0 + padding: 10px 0 + line-height: 44px + height: 44px + width: 74px + background: #77000E + display: inline-block + color: $strong_color + margin: 0 + text-align: center - a - color: $strong_color + .q + position: relative + display: inline-block + margin: 13px 20px 0 20px + vertical-align: top - .icon - margin: 0 + .icon + position: absolute + color: $strong_color - .q - position: relative - display: inline-block - margin: 13px 20px 0 20px - vertical-align: top + &.search + top: 8px + left: 12px - .icon - position: absolute - color: $strong_color + &.down-open + top: 8px + right: 12px - &.search - top: 8px - left: 12px + input + background: #77000E + border: 0 + padding: 10px 12px 10px 36px + font-size: 14px + border-radius: 2px + color: #FFF + width: 220px - &.down-open - top: 8px - right: 12px + ul + display: inline-block - input - background: #77000E - border: 0 - padding: 10px 12px 10px 36px - font-size: 14px - border-radius: 2px - color: #FFF - width: 220px + li + margin-left: 30px - ul - display: inline-block + a + color: #E0808D + font-weight: bold - li - margin-left: 30px + &.active, &:hover + color: #FFF + + .right + float: right + margin-right: 20px + line-height: 64px + color: #E0808D a - color: #E0808D - font-weight: bold - - &.active, &:hover - color: #FFF - - .right - float: right - margin-right: 20px - line-height: 64px - color: #E0808D - - a - border-radius: 2px - background: $highlight_color - color: $strong_color - padding: 11px 20px + border-radius: 2px + background: $highlight_color + color: $strong_color + padding: 11px 20px #title - border-bottom: 3px solid #F3F4F8 + border-bottom: 3px solid #F3F4F8 - .title - border-bottom: 3px solid #AAAFBF - margin: 30px 0 -3px 0 - display: inline-block - padding-bottom: 20px + .title + border-bottom: 3px solid #AAAFBF + margin: 30px 0 -3px 0 + display: inline-block + padding-bottom: 20px - .sub - font-size: 16px - font-weight: bold - margin: 0 20px + .sub + font-size: 16px + font-weight: bold + margin: 0 20px - .description - display: inline-block - font-family: $serif_font - white-space: nowrap - color: #B1B6C4 + .description + display: inline-block + font-family: $serif_font + white-space: nowrap + color: #B1B6C4 #content - padding: 20px - margin-top: 20px + padding: 20px + margin-top: 20px - #hero - background: url('../img/hires/2.jpg') center - background-size: cover - border-radius: 2px - margin-bottom: 30px + #hero + background: url('../img/hires/2.jpg') center + background-size: cover + border-radius: 2px + margin-bottom: 30px - .content - border-radius: 2px + .content + border-radius: 2px + color: #FFF + padding: 30px + background: rgba(#000, 30%) + box-shadow: inset 0 1px 2px rgba(#000, 20%) + + h2 + margin-bottom: 20px + margin-left: 140px + + p + font-family: $serif_font + font-size: 18px + line-height: 24px + margin-left: 140px + text-align: justify + text-justify: inter-word + + .address + font-size: 120px + float: left + + .cta + text-align: center + margin-top: 10px + + a + font-family: $serif_font + padding: 11px 20px + border-radius: 2px + display: inline-block + margin: 0 4px + + &.primary + font-weight: bold + background: $strong_color color: #FFF - padding: 30px - background: rgba(#000, 30%) - box-shadow: inset 0 1px 2px rgba(#000, 20%) + + &.secondary + background: #FFF + color: $strong_color - h2 - margin-bottom: 20px - margin-left: 140px + #add + h2 + color: #3E4457 - p - font-family: $serif_font - font-size: 18px - line-height: 24px - margin-left: 140px - text-align: justify - text-justify: inter-word + p + font-family: $serif_font + color: #B1B6C4 + margin-top: 10px + line-height: 20px + text-align: justify + text-justify: inter-word - .address - font-size: 120px - float: left + a + color: #3E4457 - .cta - text-align: center - margin-top: 10px + .form + margin-top: 20px + + table + width: 100% - a - font-family: $serif_font - padding: 11px 20px - border-radius: 2px - display: inline-block - margin: 0 4px + tr + td + &:first-child + width: 100% - &.primary - font-weight: bold - background: $strong_color - color: #FFF - - &.secondary - background: #FFF - color: $strong_color + input + box-sizing: border-box + padding: 10px + width: 100% + border-radius: 2px 0 0 2px + border: 1px solid #DDE1ED + border-right: 0 + box-shadow: inset 0 1px 2px rgba(#000, 20%) - #add - h2 - color: #3E4457 + a + margin-left: -2px + font-family: $serif_font + padding: 11px 20px + border-radius: 0 2px 2px 0 + display: inline-block + font-weight: bold + background: $strong_color + color: #FFF - p - font-family: $serif_font - color: #B1B6C4 - margin-top: 10px - line-height: 20px - text-align: justify - text-justify: inter-word + #projects + border: 1px solid #CDCECF + border-radius: 2px - a - color: #3E4457 + h2 + color: #3E4457 + display: inline-block - .form - margin-top: 20px - - table - width: 100% + .sort + float: right + line-height: 30px - tr - td - &:first-child - width: 100% + table + width: 100% - input - box-sizing: border-box - padding: 10px - width: 100% - border-radius: 2px 0 0 2px - border: 1px solid #DDE1ED - border-right: 0 + tr + td + background: #FCFCFC + padding: 20px 30px + border-bottom: 1px solid #EAECF2 + + .milestone + .icon + font-size: 10px + margin: 0 + + .progress + width: 200px + + .percent, .due + color: darken(#C1C4D0, 20%) + font-size: 13px + + .percent + float: right + + .bar + border-radius: 4px + background: #EAECF2 + height: 10px + width: 100% + + &.inner box-shadow: inset 0 1px 2px rgba(#000, 20%) - a - margin-left: -2px - font-family: $serif_font - padding: 11px 20px - border-radius: 0 2px 2px 0 - display: inline-block - font-weight: bold + &.red background: $strong_color - color: #FFF - #projects - border: 1px solid #CDCECF - border-radius: 2px + &.green + background: #00B361 - h2 + .due + &.red + color: $strong_color + font-weight: bold + + &:first-child color: #3E4457 - display: inline-block + font-weight: bold - .sort - float: right - line-height: 30px + &:nth-child(2) + td + background: lighten(#FCFCFC, 60%) - table - width: 100% + &:last-child + td + border: 0 - tr - td - background: #FCFCFC - padding: 20px 30px - border-bottom: 1px solid #EAECF2 + &.done + td + background: #EBF6F1 - .milestone - .icon - font-size: 10px - margin: 0 + .milestone, .percent, .due + color: #00B361 - .progress - width: 200px + .header, .footer + padding: 20px 30px - .percent, .due - color: darken(#C1C4D0, 20%) - font-size: 13px + .header + box-shadow: 0 1px 2px rgba(#DDE1ED, 50%) + margin-bottom: 2px + border-bottom: 1px solid #DDE1ED - .percent - float: right + a + font-family: $serif_font - .bar - border-radius: 4px - background: #EAECF2 - height: 10px - width: 100% + .footer + background: #F9FAFB + color: #AAAFBF + box-shadow: inset 0 1px 2px rgba(#DDE1ED, 20%) + border-top: 1px solid #DDE1ED + text-align: right + font-family: $serif_font - &.inner - box-shadow: inset 0 1px 2px rgba(#000, 20%) - - &.red - background: $strong_color - - &.green - background: #00B361 - - .due - &.red - color: $strong_color - font-weight: bold - - &:first-child - color: #3E4457 - font-weight: bold - - &:nth-child(2) - td - background: lighten(#FCFCFC, 60%) - - &:last-child - td - border: 0 - - &.done - td - background: #EBF6F1 - - .milestone, .percent, .due - color: #00B361 - - .header, .footer - padding: 20px 30px - - .header - box-shadow: 0 1px 2px rgba(#DDE1ED, 50%) - margin-bottom: 2px - border-bottom: 1px solid #DDE1ED - - a - font-family: $serif_font - - .footer - background: #F9FAFB - color: #AAAFBF - box-shadow: inset 0 1px 2px rgba(#DDE1ED, 20%) - border-top: 1px solid #DDE1ED - text-align: right - font-family: $serif_font - - .icon - color: #AAAFBF + .icon + color: #AAAFBF #footer - border-top: 1px solid #F3F4F8 - text-align: center - padding: 30px - margin-top: 30px - font-family: $serif_font \ No newline at end of file + border-top: 1px solid #F3F4F8 + text-align: center + padding: 30px + margin-top: 30px + font-family: $serif_font \ No newline at end of file diff --git a/src/styles/chart.styl b/src/styles/chart.styl index 0d60149..ad316be 100644 --- a/src/styles/chart.styl +++ b/src/styles/chart.styl @@ -1,95 +1,95 @@ @import 'nib' // color definitions -$closed = #4DAF7C -$opened = #E55F3A -$grey = #CACACA -$brown = #64584C +$closed = #4DAF7C +$opened = #E55F3A +$grey = #CACACA +$brown = #64584C $background1 = #D7BCAB $background2 = #CC9485 // where D3 renders to #chart - height: 300px - position: relative + height: 300px + position: relative - // position will be adjusted dynamically - #tooltip - position: absolute - top: 0 - left: 0 + // position will be adjusted dynamically + #tooltip + position: absolute + top: 0 + left: 0 - svg - path - &.line - fill: none - stroke-width: 1px - clip-path: url(#clip) + svg + path + &.line + fill: none + stroke-width: 1px + clip-path: url(#clip) - // actual progress - &.actual - stroke: $brown - stroke-width: 3px + // actual progress + &.actual + stroke: $brown + stroke-width: 3px - // ideal velocity throughout the sprint - &.ideal - stroke: $grey - stroke-width: 3px + // ideal velocity throughout the sprint + &.ideal + stroke: $grey + stroke-width: 3px - // trend of actual issue closures - &.trendline - stroke: $brown - stroke-width: 1.5px - stroke-dasharray: 5,5 + // trend of actual issue closures + &.trendline + stroke: $brown + stroke-width: 1.5px + stroke-dasharray: 5,5 - // right now - line - &.today - stroke: $grey - stroke-width: 1px - shape-rendering: crispEdges - stroke-dasharray: 5,5 + // right now + line + &.today + stroke: $grey + stroke-width: 1px + shape-rendering: crispEdges + stroke-dasharray: 5,5 - // represents one issue closed - circle - fill: $brown - // make it easier to click - stroke: transparent - stroke-width: 15px - cursor: pointer + // represents one issue closed + circle + fill: $brown + // make it easier to click + stroke: transparent + stroke-width: 15px + cursor: pointer - // axes... - .axis - shape-rendering: crispEdges - - line - stroke: rgba($grey, 0.25) - shape-rendering: crispEdges + // axes... + .axis + shape-rendering: crispEdges + + line + stroke: rgba($grey, 0.25) + shape-rendering: crispEdges - text - font-weight: bold - fill: $grey + text + font-weight: bold + fill: $grey - path - display: none + path + display: none // tooltips .d3-tip - margin-top: -10px - font-size: 11px - padding: 8px 10px 7px 10px - text-align: center - background: rgba(0,0,0,0.75) - color: #fff - border-radius: 3px + margin-top: -10px + font-size: 11px + padding: 8px 10px 7px 10px + text-align: center + background: rgba(0,0,0,0.75) + color: #fff + border-radius: 3px - &:after - width: 100% - color: rgba(0,0,0,0.8) - content: "\25BC" - position: absolute + &:after + width: 100% + color: rgba(0,0,0,0.8) + content: "\25BC" + position: absolute - &.n:after - margin: -3px 0 0 0 - top: 100% - left: 0 \ No newline at end of file + &.n:after + margin: -3px 0 0 0 + top: 100% + left: 0 \ No newline at end of file diff --git a/src/styles/fonts.styl b/src/styles/fonts.styl index d32f9b7..7b2efd2 100644 --- a/src/styles/fonts.styl +++ b/src/styles/fonts.styl @@ -1,32 +1,32 @@ @font-face { - font-family: 'MuseoSlab500Regular'; - src: url('../fonts/museo-slab-500.eot'); - src: url('../fonts/museo-slab-500.eot?#iefix') format('embedded-opentype'), - url('../fonts/museo-slab-500.woff') format('woff'), - url('../fonts/museo-slab-500.ttf') format('truetype'), - url('../fonts/museo-slab-500.svg#MuseoSlab500Regular') format('svg'); - font-weight: normal; - font-style: normal; + font-family: 'MuseoSlab500Regular'; + src: url('../fonts/museo-slab-500.eot'); + src: url('../fonts/museo-slab-500.eot?#iefix') format('embedded-opentype'), + url('../fonts/museo-slab-500.woff') format('woff'), + url('../fonts/museo-slab-500.ttf') format('truetype'), + url('../fonts/museo-slab-500.svg#MuseoSlab500Regular') format('svg'); + font-weight: normal; + font-style: normal; } @font-face { - font-family: 'MuseoSans500Regular'; - src: url('../fonts/museo-sans-500.eot'); - src: url('../fonts/museo-sans-500.eot?#iefix') format('embedded-opentype'), - url('../fonts/museo-sans-500.woff') format('woff'), - url('../fonts/museo-sans-500.ttf') format('truetype'), - url('../fonts/museo-sans-500.svg#MuseoSans500Regular') format('svg'); - font-weight: normal; - font-style: normal; + font-family: 'MuseoSans500Regular'; + src: url('../fonts/museo-sans-500.eot'); + src: url('../fonts/museo-sans-500.eot?#iefix') format('embedded-opentype'), + url('../fonts/museo-sans-500.woff') format('woff'), + url('../fonts/museo-sans-500.ttf') format('truetype'), + url('../fonts/museo-sans-500.svg#MuseoSans500Regular') format('svg'); + font-weight: normal; + font-style: normal; } @font-face { - font-family: 'Fontello'; - src: url('../fonts/fontello.eot?74672344'); - src: url('../fonts/fontello.eot?74672344#iefix') format('embedded-opentype'), - url('../fonts/fontello.woff?74672344') format('woff'), - url('../fonts/fontello.ttf?74672344') format('truetype'), - url('../fonts/fontello.svg?74672344#fontello') format('svg'); - font-weight: normal; - font-style: normal; + font-family: 'Fontello'; + src: url('../fonts/fontello.eot?74672344'); + src: url('../fonts/fontello.eot?74672344#iefix') format('embedded-opentype'), + url('../fonts/fontello.woff?74672344') format('woff'), + url('../fonts/fontello.ttf?74672344') format('truetype'), + url('../fonts/fontello.svg?74672344#fontello') format('svg'); + font-weight: normal; + font-style: normal; } \ No newline at end of file diff --git a/src/styles/icons.styl b/src/styles/icons.styl index bd52389..f4c1839 100644 --- a/src/styles/icons.styl +++ b/src/styles/icons.styl @@ -1,72 +1,35 @@ @import 'nib' .icon - vertical-align: middle + vertical-align: middle + font-family: "Fontello" + font-style: normal + font-weight: normal + speak: none - &:before - font-family: "Fontello" - font-style: normal - font-weight: normal - speak: none + display: inline-block + text-decoration: inherit + text-align: center - display: inline-block - text-decoration: inherit - text-align: center + /* For safety - reset parent styles, that can break glyph codes */ + font-variant: normal + text-transform: none - /* For safety - reset parent styles, that can break glyph codes */ - font-variant: normal - text-transform: none - - /* fix buttons height, for twitter bootstrap */ - line-height: 1em + &.spin6 + animation: spin 2s infinite linear - &.cog - &:before - content: '\e800' - - &.search - &:before - content: '\e801' - - &.github - &:before - content: '\e802' - - &.address - &:before - content: '\e803' - - &.plus-circled - &:before - content: '\e804' - - &.fire-station - &:before - content: '\e805' - - &.sort-alphabet - &:before - content: '\e806' - - &.down-open - &:before - content: '\e807' - - &.spin6 - animation: spin 2s infinite linear - - &:before - content: '\e808' + &.spin4 + animation: spin 2s infinite linear @-moz-keyframes spin { - from { -moz-transform: rotate(0deg) } - to { -moz-transform: rotate(360deg) } + from { -moz-transform: rotate(0deg) } + to { -moz-transform: rotate(360deg) } } @-webkit-keyframes spin { - from { -webkit-transform: rotate(0deg) } - to { -webkit-transform: rotate(360deg) } + from { -webkit-transform: rotate(0deg) } + to { -webkit-transform: rotate(360deg) } } @keyframes spin { - from { transform:rotate(0deg) } - to { transform:rotate(360deg) } + from { transform:rotate(0deg) } + to { transform:rotate(360deg) } } \ No newline at end of file diff --git a/src/styles/notify.styl b/src/styles/notify.styl new file mode 100644 index 0000000..e69de29 diff --git a/src/templates/header.mustache b/src/templates/header.mustache index 2d644d2..f9b1145 100644 --- a/src/templates/header.mustache +++ b/src/templates/header.mustache @@ -1,23 +1,26 @@ \ No newline at end of file diff --git a/src/templates/hero.mustache b/src/templates/hero.mustache index 45b671e..d101be9 100644 --- a/src/templates/hero.mustache +++ b/src/templates/hero.mustache @@ -1,13 +1,13 @@ {{^projects.list}} -
-
- -

See your project progress

-

Not sure where to start? Just add a demo repo to see a chart. There are many variations of passages of Lorem Ipsum available, but the majority have suffered alteration in some form, by injected humour, or randomised words which don't look even slightly believable.

- -
+
+
+ +

See your project progress

+

Not sure where to start? Just add a demo repo to see a chart. There are many variations of passages of Lorem Ipsum available, but the majority have suffered alteration in some form, by injected humour, or randomised words which don't look even slightly believable.

+
+
{{/projects.list}} \ No newline at end of file diff --git a/src/templates/icons.mustache b/src/templates/icons.mustache new file mode 100644 index 0000000..ea21760 --- /dev/null +++ b/src/templates/icons.mustache @@ -0,0 +1,3 @@ +{{#code}} + {{{ '&#' + code + ';' }}} +{{/code}} \ No newline at end of file diff --git a/src/templates/layout.mustache b/src/templates/layout.mustache index 4b81a0a..a917352 100644 --- a/src/templates/layout.mustache +++ b/src/templates/layout.mustache @@ -1,11 +1,11 @@
- +
\ No newline at end of file diff --git a/src/templates/notify.mustache b/src/templates/notify.mustache new file mode 100644 index 0000000..24a4965 --- /dev/null +++ b/src/templates/notify.mustache @@ -0,0 +1,2 @@ + +

You have some interesting news in your inbox. Go check it out now.

\ No newline at end of file diff --git a/src/templates/pages/addProject.mustache b/src/templates/pages/addProject.mustache index f5e68e1..36acdaa 100644 --- a/src/templates/pages/addProject.mustache +++ b/src/templates/pages/addProject.mustache @@ -1,21 +1,21 @@
-
-
-

Add a Project

-

Type in the name of the repository as you would normally. If you'd like to add a private GitHub project, Sign In first.

-
- -
- - - - - -
- - - Add -
-
+
+
+

Add a Project

+

Type in the name of the repository as you would normally. If you'd like to add a private GitHub project, Sign In first.

+ +
+ + + + + +
+ + + Add +
+
+
\ No newline at end of file diff --git a/src/templates/pages/index.mustache b/src/templates/pages/index.mustache index dc3f35f..9074e0b 100644 --- a/src/templates/pages/index.mustache +++ b/src/templates/pages/index.mustache @@ -1,4 +1,4 @@
- - + +
\ No newline at end of file diff --git a/src/templates/pages/showChart.mustache b/src/templates/pages/showChart.mustache index 3e5f164..6936591 100644 --- a/src/templates/pages/showChart.mustache +++ b/src/templates/pages/showChart.mustache @@ -1,13 +1,13 @@
-
-

{{ format.title(milestone.title) }}

- {{{ format.due(milestone.due_on) }}} -

{{{ format.markdown(milestone.description) }}}

-
+
+

{{ format.title(milestone.title) }}

+ {{{ format.due(milestone.due_on) }}} +

{{{ format.markdown(milestone.description) }}}

+
-
-
-
+
+
+
\ No newline at end of file diff --git a/src/templates/projects.mustache b/src/templates/projects.mustache index d8da39f..400cb6a 100644 --- a/src/templates/projects.mustache +++ b/src/templates/projects.mustache @@ -1,89 +1,89 @@ {{#projects.list.length}} -
-
- Sorted by priority -

Projects

-
- - - {{#projects.list}} - {{#milestones}} - - - - - - {{/milestones}} - {{/projects.list}} - - -
{{owner}}/{{name}} - {{ title }} - -
- {{Math.floor(format.progress(closed_issues, open_issues))}}% - {{{ format.due(due_on) }}} -
-
-
-
-
- - +
+
+ Sorted by priority +

Projects

+ + + {{#projects.list}} + {{#milestones}} + + + + + + {{/milestones}} + {{/projects.list}} + + +
{{owner}}/{{name}} + {{ title }} + +
+ {{Math.floor(format.progress(closed_issues, open_issues))}}% + {{{ format.due(due_on) }}} +
+
+
+
+
+ + +
{{/projects.list}} \ No newline at end of file diff --git a/src/utils/date.coffee b/src/utils/date.coffee index 2349263..f447e3b 100644 --- a/src/utils/date.coffee +++ b/src/utils/date.coffee @@ -1,2 +1,2 @@ module.exports = - now: -> new Date().toJSON() \ No newline at end of file + now: -> new Date().toJSON() \ No newline at end of file diff --git a/src/utils/format.coffee b/src/utils/format.coffee index 24c23dd..fbbf3a5 100644 --- a/src/utils/format.coffee +++ b/src/utils/format.coffee @@ -2,46 +2,50 @@ config = require '../models/config' module.exports = - # Progress in percentages. - 'progress': _.memoize (a, b) -> - 100 * (a / (b + a)) + # Progress in percentages. + 'progress': _.memoize (a, b) -> + 100 * (a / (b + a)) - # Is a milestone on time? - 'onTime': _.memoize (milestone) -> - # Milestones with no due date are always on track. - return 'green' unless milestone.due_on + # Is a milestone on time? + 'onTime': _.memoize (milestone) -> + # Milestones with no due date are always on track. + return 'green' unless milestone.due_on - # Progress in points. - points = @progress milestone.closed_issues, milestone.open_issues + # Progress in points. + points = @progress milestone.closed_issues, milestone.open_issues - # Calculate the progress in days. - a = +new Date milestone.created_at - b = +new Date - c = +new Date milestone.due_on + # Calculate the progress in days. + a = +new Date milestone.created_at + b = +new Date + c = +new Date milestone.due_on - # Progress in time. - time = @progress b - a, c - b + # Progress in time. + time = @progress b - a, c - b - [ 'red', 'green' ][ +(points > time) ] - , (m) -> # resolver - [ m.created_at, m.number ].join '/' + [ 'red', 'green' ][ +(points > time) ] + , (m) -> # resolver + [ m.created_at, m.number ].join '/' - # Time from now. - 'fromNow': _.memoize (jsonDate) -> - moment(new Date(jsonDate)).fromNow() + # Time from now. + 'fromNow': _.memoize (jsonDate) -> + moment(new Date(jsonDate)).fromNow() - # When is a milestone due? - 'due': (jsonDate) -> - return ' ' unless jsonDate - [ 'due', @fromNow jsonDate ].join(' ') + # When is a milestone due? + 'due': (jsonDate) -> + return ' ' unless jsonDate + [ 'due', @fromNow jsonDate ].join(' ') - # Markdown formatting. - 'markdown': (markup) -> - marked markup + # Markdown formatting. + 'markdown': (markup) -> + marked markup - # Format milestone title. - 'title': (text) -> - if text.toLowerCase().indexOf('milestone') > -1 - text - else - [ 'Milestone', text ].join(' ') \ No newline at end of file + # Format milestone title. + 'title': (text) -> + if text.toLowerCase().indexOf('milestone') > -1 + text + else + [ 'Milestone', text ].join(' ') + + # Hex to decimal. + hexToDecimal: (hex) -> + parseInt hex, 16 \ No newline at end of file diff --git a/src/utils/mixins.coffee b/src/utils/mixins.coffee index c5ea265..69808f9 100644 --- a/src/utils/mixins.coffee +++ b/src/utils/mixins.coffee @@ -1,8 +1,8 @@ _.mixin - 'pluckMany': (source, keys) -> - throw '`keys` needs to be an Array' unless _.isArray keys - _.map source, (item) -> - obj = {} - _.each keys, (key) -> - obj[key] = item[key] - obj \ No newline at end of file + 'pluckMany': (source, keys) -> + throw '`keys` needs to be an Array' unless _.isArray keys + _.map source, (item) -> + obj = {} + _.each keys, (key) -> + obj[key] = item[key] + obj \ No newline at end of file diff --git a/src/utils/model.coffee b/src/utils/model.coffee index b7bc96f..9ea2f55 100644 --- a/src/utils/model.coffee +++ b/src/utils/model.coffee @@ -1,5 +1,5 @@ module.exports = (opts) -> - Model = Ractive.extend(opts) - model = new Model() - model.render() - model \ No newline at end of file + Model = Ractive.extend(opts) + model = new Model() + model.render() + model \ No newline at end of file diff --git a/src/views/header.coffee b/src/views/header.coffee index f2e2422..07f6a30 100644 --- a/src/views/header.coffee +++ b/src/views/header.coffee @@ -1,17 +1,28 @@ firebase = require '../modules/firebase' mediator = require '../modules/mediator' user = require '../models/user' +state = require '../models/state' +Icons = require './icons' module.exports = Ractive.extend - 'template': require '../templates/header' + 'template': require '../templates/header' - init: -> - # Login user. - @on '!login', -> - firebase.login (err) -> - throw err if err + 'data': + 'user': user + # Default app icon. + 'icon': 'fire-station' - 'data': { user } + init: -> + # Login user. + @on '!login', -> + firebase.login (err) -> + throw err if err - 'adapt': [ Ractive.adaptors.Ractive ] \ No newline at end of file + # Switch loading icon with app icon. + state.observe 'loading', (val) => + @set 'icon', if val then 'spin4' else 'fire-station' + + 'components': { Icons } + + 'adapt': [ Ractive.adaptors.Ractive ] \ No newline at end of file diff --git a/src/views/hero.coffee b/src/views/hero.coffee index 2b39e43..dbd36ea 100644 --- a/src/views/hero.coffee +++ b/src/views/hero.coffee @@ -1,10 +1,13 @@ mediator = require '../modules/mediator' projects = require '../models/projects' +Icons = require './icons' module.exports = Ractive.extend - 'template': require '../templates/hero' + 'template': require '../templates/hero' - 'data': { projects } + 'data': { projects } - 'adapt': [ Ractive.adaptors.Ractive ] \ No newline at end of file + 'components': { Icons } + + 'adapt': [ Ractive.adaptors.Ractive ] \ No newline at end of file diff --git a/src/views/icons.coffee b/src/views/icons.coffee new file mode 100644 index 0000000..0454e2a --- /dev/null +++ b/src/views/icons.coffee @@ -0,0 +1,28 @@ +utils = require '../utils/format' + +# Fontello icon hex codes. +codes = + 'cog': '\e800' + 'search': '\e801' + 'github': '\e802' + 'address': '\e803' + 'plus-circled': '\e804' + 'fire-station': '\e805' + 'sort-alphabet': '\e806' + 'down-open': '\e807' + 'spin6': '\e808' + 'megaphone': '\e809' + 'spin4': '\e80a' + +module.exports = Ractive.extend + + 'template': require '../templates/icons' + + 'isolated': yes + + init: -> + @observe 'icon', (icon) -> + if icon and hex = codes[icon] + @set 'code', utils.hexToDecimal hex + else + @set 'code', null \ No newline at end of file diff --git a/src/views/pages/addProject.coffee b/src/views/pages/addProject.coffee index 23527d6..a598c38 100644 --- a/src/views/pages/addProject.coffee +++ b/src/views/pages/addProject.coffee @@ -3,29 +3,29 @@ user = require '../../models/user' module.exports = Ractive.extend - 'template': require '../../templates/pages/addProject' + 'template': require '../../templates/pages/addProject' - 'data': { 'value': 'radekstepan/disposable', user } + 'data': { 'value': 'radekstepan/disposable', user } - 'adapt': [ Ractive.adaptors.Ractive ] + 'adapt': [ Ractive.adaptors.Ractive ] - init: -> - document.title = 'Add a new project' + init: -> + document.title = 'Add a new project' - # TODO: autocomplete on our username if we are logged in or based - # on repos we already have. - autocomplete = (value) -> + # TODO: autocomplete on our username if we are logged in or based + # on repos we already have. + autocomplete = (value) -> - @observe 'value', _.debounce(autocomplete, 200), { 'init': no } + @observe 'value', _.debounce(autocomplete, 200), { 'init': no } - # TODO: focus on the input field + # TODO: focus on the input field - # TODO: listen to Enter keypress. - @on 'submit', -> - [ owner, name ] = @get('value').split('/') + # TODO: listen to Enter keypress. + @on 'submit', -> + [ owner, name ] = @get('value').split('/') - # TODO: save repo & persist. - mediator.fire '!projects/add', { owner, name }, -> - # Redirect to the dashboard. - # TODO: trigger a named route - window.location.hash = '#' \ No newline at end of file + # TODO: save repo & persist. + mediator.fire '!projects/add', { owner, name }, -> + # Redirect to the dashboard. + # TODO: trigger a named route + window.location.hash = '#' \ No newline at end of file diff --git a/src/views/pages/index.coffee b/src/views/pages/index.coffee index 44e07eb..ab26937 100644 --- a/src/views/pages/index.coffee +++ b/src/views/pages/index.coffee @@ -4,11 +4,11 @@ format = require '../../utils/format' module.exports = Ractive.extend - 'template': require '../../templates/pages/index' + 'template': require '../../templates/pages/index' - 'components': { Hero, Projects } + 'components': { Hero, Projects } - 'data': { format } + 'data': { format } - init: -> - document.title = 'BurnChart: GitHub Burndown Chart as a Service' \ No newline at end of file + init: -> + document.title = 'BurnChart: GitHub Burndown Chart as a Service' \ No newline at end of file diff --git a/src/views/pages/showChart.coffee b/src/views/pages/showChart.coffee index 5aca0dd..b1ada07 100644 --- a/src/views/pages/showChart.coffee +++ b/src/views/pages/showChart.coffee @@ -4,24 +4,24 @@ format = require '../../utils/format' module.exports = Ractive.extend - 'template': require '../../templates/pages/showChart' + 'template': require '../../templates/pages/showChart' - 'adapt': [ Ractive.adaptors.Ractive ] + 'adapt': [ Ractive.adaptors.Ractive ] - 'data': { format } + 'data': { format } - init: -> - route = @get 'route' - - document.title = "#{route.owner}/#{route.name}" + init: -> + route = @get 'route' + + document.title = "#{route.owner}/#{route.name}" - milestone.get route, (err, warn, obj) => - throw err if err - throw warn if warn - # Save the milestone on the route. - @set 'milestone', obj - route.milestone = obj + milestone.get route, (err, warn, obj) => + throw err if err + throw warn if warn + # Save the milestone on the route. + @set 'milestone', obj + route.milestone = obj - project route, (err) -> - throw err if err - console.log 'Done' \ No newline at end of file + project route, (err) -> + throw err if err + console.log 'Done' \ No newline at end of file diff --git a/src/views/projects.coffee b/src/views/projects.coffee index 181927d..34a8a1e 100644 --- a/src/views/projects.coffee +++ b/src/views/projects.coffee @@ -1,10 +1,13 @@ mediator = require '../modules/mediator' projects = require '../models/projects' +Icons = require './icons' module.exports = Ractive.extend - 'template': require '../templates/projects' + 'template': require '../templates/projects' - 'data': { projects } + 'data': { projects } - 'adapt': [ Ractive.adaptors.Ractive ] \ No newline at end of file + 'components': { Icons } + + 'adapt': [ Ractive.adaptors.Ractive ] \ No newline at end of file