1&&"string"==typeof h&&!mt.checkClone&&ee.test(h))return t.each(function(i){var o=t.eq(i);v&&(e[0]=h.call(this,i,o.html())),I(o,e,n,r)});if(p&&(i=T(e,t[0].ownerDocument,!1,t,r),o=i.firstChild,1===i.childNodes.length&&(i=o),o||r)){for(s=yt.map(x(i,"script"),O),u=s.length;f=0&&nw.cacheLength&&delete t[e.shift()],t[n+" "]=r}var e=[];return t}function r(t){return t[F]=!0,t}function i(t){var e=j.createElement("fieldset");try{return!!t(e)}catch(t){return!1}finally{e.parentNode&&e.parentNode.removeChild(e),e=null}}function o(t,e){for(var n=t.split("|"),r=n.length;r--;)w.attrHandle[n[r]]=e}function a(t,e){var n=e&&t,r=n&&1===t.nodeType&&1===e.nodeType&&t.sourceIndex-e.sourceIndex;if(r)return r;if(n)for(;n=n.nextSibling;)if(n===e)return-1;return t?1:-1}function s(t){return function(e){return"form"in e?e.parentNode&&!1===e.disabled?"label"in e?"label"in e.parentNode?e.parentNode.disabled===t:e.disabled===t:e.isDisabled===t||e.isDisabled!==!t&&xt(e)===t:e.disabled===t:"label"in e&&e.disabled===t}}function u(t){return r(function(e){return e=+e,r(function(n,r){for(var i,o=t([],n.length,e),a=o.length;a--;)n[i=o[a]]&&(n[i]=!(r[i]=n[i]))})})}function c(t){return t&&void 0!==t.getElementsByTagName&&t}function l(){}function f(t){for(var e=0,n=t.length,r="";e1?function(e,n,r){for(var i=t.length;i--;)if(!t[i](e,n,r))return!1;return!0}:t[0]}function h(t,n,r){for(var i=0,o=n.length;i-1&&(r[c]=!(a[c]=f))}}else b=v(b===a?b.splice(g,b.length):b),o?o(null,a,b,u):Q.apply(a,b)})}function m(t){for(var e,n,r,i=t.length,o=w.relative[t[0].type],a=o||w.relative[" "],s=o?1:0,u=p(function(t){return t===e},a,!0),c=p(function(t){return Z(e,t)>-1},a,!0),l=[function(t,n,r){var i=!o&&(r||n!==A)||((e=n).nodeType?u(t,n,r):c(t,n,r));return e=null,i}];s1&&d(l),s>1&&f(t.slice(0,s-1).concat({value:" "===t[s-2].type?"*":""})).replace(ot,"$1"),n,s0,o=t.length>0,a=function(r,a,s,u,c){var l,f,p,d=0,h="0",g=r&&[],m=[],y=A,b=r||o&&w.find.TAG("*",c),_=M+=null==y?1:Math.random()||.1,x=b.length;for(c&&(A=a===j||a||c);h!==x&&null!=(l=b[h]);h++){if(o&&l){for(f=0,a||l.ownerDocument===j||(O(l),s=!D);p=t[f++];)if(p(l,a||j,s)){u.push(l);break}c&&(M=_)}i&&((l=!p&&l)&&d--,r&&g.push(l))}if(d+=h,i&&h!==d){for(f=0;p=n[f++];)p(g,m,a,s);if(r){if(d>0)for(;h--;)g[h]||m[h]||(m[h]=K.call(u));m=v(m)}Q.apply(u,m),c&&!r&&m.length>0&&d+n.length>1&&e.uniqueSort(u)}return c&&(M=_,A=y),g};return i?r(a):a}var b,_,w,x,C,T,$,k,A,E,S,O,j,N,D,I,L,R,P,F="sizzle"+1*new Date,q=t.document,M=0,H=0,B=n(),U=n(),W=n(),z=function(t,e){return t===e&&(S=!0),0},V={}.hasOwnProperty,X=[],K=X.pop,J=X.push,Q=X.push,G=X.slice,Z=function(t,e){for(var n=0,r=t.length;n+~]|"+tt+")"+tt+"*"),ut=new RegExp("="+tt+"*([^\\]'\"]*?)"+tt+"*\\]","g"),ct=new RegExp(rt),lt=new RegExp("^"+et+"$"),ft={ID:new RegExp("^#("+et+")"),CLASS:new RegExp("^\\.("+et+")"),TAG:new RegExp("^("+et+"|[*])"),ATTR:new RegExp("^"+nt),PSEUDO:new RegExp("^"+rt),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+tt+"*(even|odd|(([+-]|)(\\d*)n|)"+tt+"*(?:([+-]|)"+tt+"*(\\d+)|))"+tt+"*\\)|)","i"),bool:new RegExp("^(?:"+Y+")$","i"),needsContext:new RegExp("^"+tt+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+tt+"*((?:-\\d)?\\d*)"+tt+"*\\)|)(?=[^-]|$)","i")},pt=/^(?:input|select|textarea|button)$/i,dt=/^h\d$/i,ht=/^[^{]+\{\s*\[native \w/,vt=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,gt=/[+~]/,mt=new RegExp("\\\\([\\da-f]{1,6}"+tt+"?|("+tt+")|.)","ig"),yt=function(t,e,n){var r="0x"+e-65536;return r!==r||n?e:r<0?String.fromCharCode(r+65536):String.fromCharCode(r>>10|55296,1023&r|56320)},bt=/([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g,_t=function(t,e){return e?"\0"===t?"ļæ½":t.slice(0,-1)+"\\"+t.charCodeAt(t.length-1).toString(16)+" ":"\\"+t},wt=function(){O()},xt=p(function(t){return!0===t.disabled&&("form"in t||"label"in t)},{dir:"parentNode",next:"legend"});try{Q.apply(X=G.call(q.childNodes),q.childNodes),X[q.childNodes.length].nodeType}catch(t){Q={apply:X.length?function(t,e){J.apply(t,G.call(e))}:function(t,e){for(var n=t.length,r=0;t[n++]=e[r++];);t.length=n-1}}}_=e.support={},C=e.isXML=function(t){var e=t&&(t.ownerDocument||t).documentElement;return!!e&&"HTML"!==e.nodeName},O=e.setDocument=function(t){var e,n,r=t?t.ownerDocument||t:q;return r!==j&&9===r.nodeType&&r.documentElement?(j=r,N=j.documentElement,D=!C(j),q!==j&&(n=j.defaultView)&&n.top!==n&&(n.addEventListener?n.addEventListener("unload",wt,!1):n.attachEvent&&n.attachEvent("onunload",wt)),_.attributes=i(function(t){return t.className="i",!t.getAttribute("className")}),_.getElementsByTagName=i(function(t){return t.appendChild(j.createComment("")),!t.getElementsByTagName("*").length}),_.getElementsByClassName=ht.test(j.getElementsByClassName),_.getById=i(function(t){return N.appendChild(t).id=F,!j.getElementsByName||!j.getElementsByName(F).length}),_.getById?(w.filter.ID=function(t){var e=t.replace(mt,yt);return function(t){return t.getAttribute("id")===e}},w.find.ID=function(t,e){if(void 0!==e.getElementById&&D){var n=e.getElementById(t);return n?[n]:[]}}):(w.filter.ID=function(t){var e=t.replace(mt,yt);return function(t){var n=void 0!==t.getAttributeNode&&t.getAttributeNode("id");return n&&n.value===e}},w.find.ID=function(t,e){if(void 0!==e.getElementById&&D){var n,r,i,o=e.getElementById(t);if(o){if((n=o.getAttributeNode("id"))&&n.value===t)return[o];for(i=e.getElementsByName(t),r=0;o=i[r++];)if((n=o.getAttributeNode("id"))&&n.value===t)return[o]}return[]}}),w.find.TAG=_.getElementsByTagName?function(t,e){return void 0!==e.getElementsByTagName?e.getElementsByTagName(t):_.qsa?e.querySelectorAll(t):void 0}:function(t,e){var n,r=[],i=0,o=e.getElementsByTagName(t);if("*"===t){for(;n=o[i++];)1===n.nodeType&&r.push(n);return r}return o},w.find.CLASS=_.getElementsByClassName&&function(t,e){if(void 0!==e.getElementsByClassName&&D)return e.getElementsByClassName(t)},L=[],I=[],(_.qsa=ht.test(j.querySelectorAll))&&(i(function(t){N.appendChild(t).innerHTML=" ",t.querySelectorAll("[msallowcapture^='']").length&&I.push("[*^$]="+tt+"*(?:''|\"\")"),t.querySelectorAll("[selected]").length||I.push("\\["+tt+"*(?:value|"+Y+")"),t.querySelectorAll("[id~="+F+"-]").length||I.push("~="),t.querySelectorAll(":checked").length||I.push(":checked"),t.querySelectorAll("a#"+F+"+*").length||I.push(".#.+[+~]")}),i(function(t){t.innerHTML=" ";var e=j.createElement("input");e.setAttribute("type","hidden"),t.appendChild(e).setAttribute("name","D"),t.querySelectorAll("[name=d]").length&&I.push("name"+tt+"*[*^$|!~]?="),2!==t.querySelectorAll(":enabled").length&&I.push(":enabled",":disabled"),N.appendChild(t).disabled=!0,2!==t.querySelectorAll(":disabled").length&&I.push(":enabled",":disabled"),t.querySelectorAll("*,:x"),I.push(",.*:")})),(_.matchesSelector=ht.test(R=N.matches||N.webkitMatchesSelector||N.mozMatchesSelector||N.oMatchesSelector||N.msMatchesSelector))&&i(function(t){_.disconnectedMatch=R.call(t,"*"),R.call(t,"[s!='']:x"),L.push("!=",rt)}),I=I.length&&new RegExp(I.join("|")),L=L.length&&new RegExp(L.join("|")),e=ht.test(N.compareDocumentPosition),P=e||ht.test(N.contains)?function(t,e){var n=9===t.nodeType?t.documentElement:t,r=e&&e.parentNode;return t===r||!(!r||1!==r.nodeType||!(n.contains?n.contains(r):t.compareDocumentPosition&&16&t.compareDocumentPosition(r)))}:function(t,e){if(e)for(;e=e.parentNode;)if(e===t)return!0;return!1},z=e?function(t,e){if(t===e)return S=!0,0;var n=!t.compareDocumentPosition-!e.compareDocumentPosition;return n||(n=(t.ownerDocument||t)===(e.ownerDocument||e)?t.compareDocumentPosition(e):1,1&n||!_.sortDetached&&e.compareDocumentPosition(t)===n?t===j||t.ownerDocument===q&&P(q,t)?-1:e===j||e.ownerDocument===q&&P(q,e)?1:E?Z(E,t)-Z(E,e):0:4&n?-1:1)}:function(t,e){if(t===e)return S=!0,0;var n,r=0,i=t.parentNode,o=e.parentNode,s=[t],u=[e];if(!i||!o)return t===j?-1:e===j?1:i?-1:o?1:E?Z(E,t)-Z(E,e):0;if(i===o)return a(t,e);for(n=t;n=n.parentNode;)s.unshift(n);for(n=e;n=n.parentNode;)u.unshift(n);for(;s[r]===u[r];)r++;return r?a(s[r],u[r]):s[r]===q?-1:u[r]===q?1:0},j):j},e.matches=function(t,n){return e(t,null,null,n)},e.matchesSelector=function(t,n){if((t.ownerDocument||t)!==j&&O(t),n=n.replace(ut,"='$1']"),_.matchesSelector&&D&&!W[n+" "]&&(!L||!L.test(n))&&(!I||!I.test(n)))try{var r=R.call(t,n);if(r||_.disconnectedMatch||t.document&&11!==t.document.nodeType)return r}catch(t){}return e(n,j,null,[t]).length>0},e.contains=function(t,e){return(t.ownerDocument||t)!==j&&O(t),P(t,e)},e.attr=function(t,e){(t.ownerDocument||t)!==j&&O(t);var n=w.attrHandle[e.toLowerCase()],r=n&&V.call(w.attrHandle,e.toLowerCase())?n(t,e,!D):void 0;return void 0!==r?r:_.attributes||!D?t.getAttribute(e):(r=t.getAttributeNode(e))&&r.specified?r.value:null},e.escape=function(t){return(t+"").replace(bt,_t)},e.error=function(t){throw new Error("Syntax error, unrecognized expression: "+t)},e.uniqueSort=function(t){var e,n=[],r=0,i=0;if(S=!_.detectDuplicates,E=!_.sortStable&&t.slice(0),t.sort(z),S){for(;e=t[i++];)e===t[i]&&(r=n.push(i));for(;r--;)t.splice(n[r],1)}return E=null,t},x=e.getText=function(t){var e,n="",r=0,i=t.nodeType;if(i){if(1===i||9===i||11===i){if("string"==typeof t.textContent)return t.textContent;for(t=t.firstChild;t;t=t.nextSibling)n+=x(t)}else if(3===i||4===i)return t.nodeValue}else for(;e=t[r++];)n+=x(e);return n},w=e.selectors={cacheLength:50,createPseudo:r,match:ft,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(t){return t[1]=t[1].replace(mt,yt),t[3]=(t[3]||t[4]||t[5]||"").replace(mt,yt),"~="===t[2]&&(t[3]=" "+t[3]+" "),t.slice(0,4)},CHILD:function(t){return t[1]=t[1].toLowerCase(),"nth"===t[1].slice(0,3)?(t[3]||e.error(t[0]),t[4]=+(t[4]?t[5]+(t[6]||1):2*("even"===t[3]||"odd"===t[3])),t[5]=+(t[7]+t[8]||"odd"===t[3])):t[3]&&e.error(t[0]),t},PSEUDO:function(t){var e,n=!t[6]&&t[2];return ft.CHILD.test(t[0])?null:(t[3]?t[2]=t[4]||t[5]||"":n&&ct.test(n)&&(e=T(n,!0))&&(e=n.indexOf(")",n.length-e)-n.length)&&(t[0]=t[0].slice(0,e),t[2]=n.slice(0,e)),t.slice(0,3))}},filter:{TAG:function(t){var e=t.replace(mt,yt).toLowerCase();return"*"===t?function(){return!0}:function(t){return t.nodeName&&t.nodeName.toLowerCase()===e}},CLASS:function(t){var e=B[t+" "];return e||(e=new RegExp("(^|"+tt+")"+t+"("+tt+"|$)"))&&B(t,function(t){return e.test("string"==typeof t.className&&t.className||void 0!==t.getAttribute&&t.getAttribute("class")||"")})},ATTR:function(t,n,r){return function(i){var o=e.attr(i,t);return null==o?"!="===n:!n||(o+="","="===n?o===r:"!="===n?o!==r:"^="===n?r&&0===o.indexOf(r):"*="===n?r&&o.indexOf(r)>-1:"$="===n?r&&o.slice(-r.length)===r:"~="===n?(" "+o.replace(it," ")+" ").indexOf(r)>-1:"|="===n&&(o===r||o.slice(0,r.length+1)===r+"-"))}},CHILD:function(t,e,n,r,i){var o="nth"!==t.slice(0,3),a="last"!==t.slice(-4),s="of-type"===e;return 1===r&&0===i?function(t){return!!t.parentNode}:function(e,n,u){var c,l,f,p,d,h,v=o!==a?"nextSibling":"previousSibling",g=e.parentNode,m=s&&e.nodeName.toLowerCase(),y=!u&&!s,b=!1;if(g){if(o){for(;v;){for(p=e;p=p[v];)if(s?p.nodeName.toLowerCase()===m:1===p.nodeType)return!1;h=v="only"===t&&!h&&"nextSibling"}return!0}if(h=[a?g.firstChild:g.lastChild],a&&y){for(p=g,f=p[F]||(p[F]={}),l=f[p.uniqueID]||(f[p.uniqueID]={}),c=l[t]||[],d=c[0]===M&&c[1],b=d&&c[2],p=d&&g.childNodes[d];p=++d&&p&&p[v]||(b=d=0)||h.pop();)if(1===p.nodeType&&++b&&p===e){l[t]=[M,d,b];break}}else if(y&&(p=e,f=p[F]||(p[F]={}),l=f[p.uniqueID]||(f[p.uniqueID]={}),c=l[t]||[],d=c[0]===M&&c[1],b=d),!1===b)for(;(p=++d&&p&&p[v]||(b=d=0)||h.pop())&&((s?p.nodeName.toLowerCase()!==m:1!==p.nodeType)||!++b||(y&&(f=p[F]||(p[F]={}),l=f[p.uniqueID]||(f[p.uniqueID]={}),l[t]=[M,b]),p!==e)););return(b-=i)===r||b%r==0&&b/r>=0}}},PSEUDO:function(t,n){var i,o=w.pseudos[t]||w.setFilters[t.toLowerCase()]||e.error("unsupported pseudo: "+t);return o[F]?o(n):o.length>1?(i=[t,t,"",n],w.setFilters.hasOwnProperty(t.toLowerCase())?r(function(t,e){for(var r,i=o(t,n),a=i.length;a--;)r=Z(t,i[a]),t[r]=!(e[r]=i[a])}):function(t){return o(t,0,i)}):o}},pseudos:{not:r(function(t){var e=[],n=[],i=$(t.replace(ot,"$1"));return i[F]?r(function(t,e,n,r){for(var o,a=i(t,null,r,[]),s=t.length;s--;)(o=a[s])&&(t[s]=!(e[s]=o))}):function(t,r,o){return e[0]=t,i(e,null,o,n),e[0]=null,!n.pop()}}),has:r(function(t){return function(n){return e(t,n).length>0}}),contains:r(function(t){return t=t.replace(mt,yt),function(e){return(e.textContent||e.innerText||x(e)).indexOf(t)>-1}}),lang:r(function(t){return lt.test(t||"")||e.error("unsupported lang: "+t),t=t.replace(mt,yt).toLowerCase(),function(e){var n;do{if(n=D?e.lang:e.getAttribute("xml:lang")||e.getAttribute("lang"))return(n=n.toLowerCase())===t||0===n.indexOf(t+"-")}while((e=e.parentNode)&&1===e.nodeType);return!1}}),target:function(e){var n=t.location&&t.location.hash;return n&&n.slice(1)===e.id},root:function(t){return t===N},focus:function(t){return t===j.activeElement&&(!j.hasFocus||j.hasFocus())&&!!(t.type||t.href||~t.tabIndex)},enabled:s(!1),disabled:s(!0),checked:function(t){var e=t.nodeName.toLowerCase();return"input"===e&&!!t.checked||"option"===e&&!!t.selected},selected:function(t){return t.parentNode&&t.parentNode.selectedIndex,!0===t.selected},empty:function(t){for(t=t.firstChild;t;t=t.nextSibling)if(t.nodeType<6)return!1;return!0},parent:function(t){return!w.pseudos.empty(t)},header:function(t){return dt.test(t.nodeName)},input:function(t){return pt.test(t.nodeName)},button:function(t){var e=t.nodeName.toLowerCase();return"input"===e&&"button"===t.type||"button"===e},text:function(t){var e;return"input"===t.nodeName.toLowerCase()&&"text"===t.type&&(null==(e=t.getAttribute("type"))||"text"===e.toLowerCase())},first:u(function(){return[0]}),last:u(function(t,e){return[e-1]}),eq:u(function(t,e,n){return[n<0?n+e:n]}),even:u(function(t,e){for(var n=0;n=0;)t.push(r);return t}),gt:u(function(t,e,n){for(var r=n<0?n+e:n;++r2&&"ID"===(a=o[0]).type&&9===e.nodeType&&D&&w.relative[o[1].type]){if(!(e=(w.find.ID(a.matches[0].replace(mt,yt),e)||[])[0]))return n;l&&(e=e.parentNode),t=t.slice(o.shift().value.length)}for(i=ft.needsContext.test(t)?0:o.length;i--&&(a=o[i],!w.relative[s=a.type]);)if((u=w.find[s])&&(r=u(a.matches[0].replace(mt,yt),gt.test(o[0].type)&&c(e.parentNode)||e))){if(o.splice(i,1),!(t=r.length&&f(o)))return Q.apply(n,r),n;break}}return(l||$(t,p))(r,e,!D,n,!e||gt.test(t)&&c(e.parentNode)||e),n},_.sortStable=F.split("").sort(z).join("")===F,_.detectDuplicates=!!S,O(),_.sortDetached=i(function(t){return 1&t.compareDocumentPosition(j.createElement("fieldset"))}),i(function(t){return t.innerHTML=" ","#"===t.firstChild.getAttribute("href")})||o("type|href|height|width",function(t,e,n){if(!n)return t.getAttribute(e,"type"===e.toLowerCase()?1:2)}),_.attributes&&i(function(t){return t.innerHTML=" ",t.firstChild.setAttribute("value",""),""===t.firstChild.getAttribute("value")})||o("value",function(t,e,n){if(!n&&"input"===t.nodeName.toLowerCase())return t.defaultValue}),i(function(t){return null==t.getAttribute("disabled")})||o(Y,function(t,e,n){var r;if(!n)return!0===t[e]?e.toLowerCase():(r=t.getAttributeNode(e))&&r.specified?r.value:null}),e}(n);yt.find=_t,yt.expr=_t.selectors,yt.expr[":"]=yt.expr.pseudos,yt.uniqueSort=yt.unique=_t.uniqueSort,yt.text=_t.getText,yt.isXMLDoc=_t.isXML,yt.contains=_t.contains,yt.escapeSelector=_t.escape;var wt=function(t,e,n){for(var r=[],i=void 0!==n;(t=t[e])&&9!==t.nodeType;)if(1===t.nodeType){if(i&&yt(t).is(n))break;r.push(t)}return r},xt=function(t,e){for(var n=[];t;t=t.nextSibling)1===t.nodeType&&t!==e&&n.push(t);return n},Ct=yt.expr.match.needsContext,Tt=/^<([a-z][^\/\0>:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i,$t=/^.[^:#\[\.,]*$/;yt.filter=function(t,e,n){var r=e[0];return n&&(t=":not("+t+")"),1===e.length&&1===r.nodeType?yt.find.matchesSelector(r,t)?[r]:[]:yt.find.matches(t,yt.grep(e,function(t){return 1===t.nodeType}))},yt.fn.extend({find:function(t){var e,n,r=this.length,i=this;if("string"!=typeof t)return this.pushStack(yt(t).filter(function(){for(e=0;e1?yt.uniqueSort(n):n},filter:function(t){return this.pushStack(c(this,t||[],!1))},not:function(t){return this.pushStack(c(this,t||[],!0))},is:function(t){return!!c(this,"string"==typeof t&&Ct.test(t)?yt(t):t||[],!1).length}});var kt,At=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]+))$/;(yt.fn.init=function(t,e,n){var r,i;if(!t)return this;if(n=n||kt,"string"==typeof t){if(!(r="<"===t[0]&&">"===t[t.length-1]&&t.length>=3?[null,t,null]:At.exec(t))||!r[1]&&e)return!e||e.jquery?(e||n).find(t):this.constructor(e).find(t);if(r[1]){if(e=e instanceof yt?e[0]:e,yt.merge(this,yt.parseHTML(r[1],e&&e.nodeType?e.ownerDocument||e:at,!0)),Tt.test(r[1])&&yt.isPlainObject(e))for(r in e)yt.isFunction(this[r])?this[r](e[r]):this.attr(r,e[r]);return this}return i=at.getElementById(r[2]),i&&(this[0]=i,this.length=1),this}return t.nodeType?(this[0]=t,this.length=1,this):yt.isFunction(t)?void 0!==n.ready?n.ready(t):t(yt):yt.makeArray(t,this)}).prototype=yt.fn,kt=yt(at);var Et=/^(?:parents|prev(?:Until|All))/,St={children:!0,contents:!0,next:!0,prev:!0};yt.fn.extend({has:function(t){var e=yt(t,this),n=e.length;return this.filter(function(){for(var t=0;t-1:1===n.nodeType&&yt.find.matchesSelector(n,t))){o.push(n);break}return this.pushStack(o.length>1?yt.uniqueSort(o):o)},index:function(t){return t?"string"==typeof t?ft.call(yt(t),this[0]):ft.call(this,t.jquery?t[0]:t):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(t,e){return this.pushStack(yt.uniqueSort(yt.merge(this.get(),yt(t,e))))},addBack:function(t){return this.add(null==t?this.prevObject:this.prevObject.filter(t))}}),yt.each({parent:function(t){var e=t.parentNode;return e&&11!==e.nodeType?e:null},parents:function(t){return wt(t,"parentNode")},parentsUntil:function(t,e,n){return wt(t,"parentNode",n)},next:function(t){return l(t,"nextSibling")},prev:function(t){return l(t,"previousSibling")},nextAll:function(t){return wt(t,"nextSibling")},prevAll:function(t){return wt(t,"previousSibling")},nextUntil:function(t,e,n){return wt(t,"nextSibling",n)},prevUntil:function(t,e,n){return wt(t,"previousSibling",n)},siblings:function(t){return xt((t.parentNode||{}).firstChild,t)},children:function(t){return xt(t.firstChild)},contents:function(t){return u(t,"iframe")?t.contentDocument:(u(t,"template")&&(t=t.content||t),yt.merge([],t.childNodes))}},function(t,e){yt.fn[t]=function(n,r){var i=yt.map(this,e,n);return"Until"!==t.slice(-5)&&(r=n),r&&"string"==typeof r&&(i=yt.filter(r,i)),this.length>1&&(St[t]||yt.uniqueSort(i),Et.test(t)&&i.reverse()),this.pushStack(i)}});var Ot=/[^\x20\t\r\n\f]+/g;yt.Callbacks=function(t){t="string"==typeof t?f(t):yt.extend({},t);var e,n,r,i,o=[],a=[],s=-1,u=function(){for(i=i||t.once,r=e=!0;a.length;s=-1)for(n=a.shift();++s-1;)o.splice(n,1),n<=s&&s--}),this},has:function(t){return t?yt.inArray(t,o)>-1:o.length>0},empty:function(){return o&&(o=[]),this},disable:function(){return i=a=[],o=n="",this},disabled:function(){return!o},lock:function(){return i=a=[],n||e||(o=n=""),this},locked:function(){return!!i},fireWith:function(t,n){return i||(n=n||[],n=[t,n.slice?n.slice():n],a.push(n),e||u()),this},fire:function(){return c.fireWith(this,arguments),this},fired:function(){return!!r}};return c},yt.extend({Deferred:function(t){var e=[["notify","progress",yt.Callbacks("memory"),yt.Callbacks("memory"),2],["resolve","done",yt.Callbacks("once memory"),yt.Callbacks("once memory"),0,"resolved"],["reject","fail",yt.Callbacks("once memory"),yt.Callbacks("once memory"),1,"rejected"]],r="pending",i={state:function(){return r},always:function(){return o.done(arguments).fail(arguments),this},catch:function(t){return i.then(null,t)},pipe:function(){var t=arguments;return yt.Deferred(function(n){yt.each(e,function(e,r){var i=yt.isFunction(t[r[4]])&&t[r[4]];o[r[1]](function(){var t=i&&i.apply(this,arguments);t&&yt.isFunction(t.promise)?t.promise().progress(n.notify).done(n.resolve).fail(n.reject):n[r[0]+"With"](this,i?[t]:arguments)})}),t=null}).promise()},then:function(t,r,i){function o(t,e,r,i){return function(){var s=this,u=arguments,c=function(){var n,c;if(!(t=a&&(r!==d&&(s=void 0,u=[n]),e.rejectWith(s,u))}};t?l():(yt.Deferred.getStackHook&&(l.stackTrace=yt.Deferred.getStackHook()),n.setTimeout(l))}}var a=0;return yt.Deferred(function(n){e[0][3].add(o(0,n,yt.isFunction(i)?i:p,n.notifyWith)),e[1][3].add(o(0,n,yt.isFunction(t)?t:p)),e[2][3].add(o(0,n,yt.isFunction(r)?r:d))}).promise()},promise:function(t){return null!=t?yt.extend(t,i):i}},o={};return yt.each(e,function(t,n){var a=n[2],s=n[5];i[n[1]]=a.add,s&&a.add(function(){r=s},e[3-t][2].disable,e[0][2].lock),a.add(n[3].fire),o[n[0]]=function(){return o[n[0]+"With"](this===o?void 0:this,arguments),this},o[n[0]+"With"]=a.fireWith}),i.promise(o),t&&t.call(o,o),o},when:function(t){var e=arguments.length,n=e,r=Array(n),i=ut.call(arguments),o=yt.Deferred(),a=function(t){return function(n){r[t]=this,i[t]=arguments.length>1?ut.call(arguments):n,--e||o.resolveWith(r,i)}};if(e<=1&&(h(t,o.done(a(n)).resolve,o.reject,!e),"pending"===o.state()||yt.isFunction(i[n]&&i[n].then)))return o.then();for(;n--;)h(i[n],a(n),o.reject);return o.promise()}});var jt=/^(Eval|Internal|Range|Reference|Syntax|Type|URI)Error$/;yt.Deferred.exceptionHook=function(t,e){n.console&&n.console.warn&&t&&jt.test(t.name)&&n.console.warn("jQuery.Deferred exception: "+t.message,t.stack,e)},yt.readyException=function(t){n.setTimeout(function(){throw t})};var Nt=yt.Deferred();yt.fn.ready=function(t){return Nt.then(t).catch(function(t){yt.readyException(t)}),this},yt.extend({isReady:!1,readyWait:1,ready:function(t){(!0===t?--yt.readyWait:yt.isReady)||(yt.isReady=!0,!0!==t&&--yt.readyWait>0||Nt.resolveWith(at,[yt]))}}),yt.ready.then=Nt.then,"complete"===at.readyState||"loading"!==at.readyState&&!at.documentElement.doScroll?n.setTimeout(yt.ready):(at.addEventListener("DOMContentLoaded",v),n.addEventListener("load",v));var Dt=function(t,e,n,r,i,o,a){var s=0,u=t.length,c=null==n;if("object"===yt.type(n)){i=!0;for(s in n)Dt(t,e,s,n[s],!0,o,a)}else if(void 0!==r&&(i=!0,yt.isFunction(r)||(a=!0),c&&(a?(e.call(t,r),e=null):(c=e,e=function(t,e,n){return c.call(yt(t),n)})),e))for(;s1,null,!0)},removeData:function(t){return this.each(function(){Rt.remove(this,t)})}}),yt.extend({queue:function(t,e,n){var r;if(t)return e=(e||"fx")+"queue",r=Lt.get(t,e),n&&(!r||Array.isArray(n)?r=Lt.access(t,e,yt.makeArray(n)):r.push(n)),r||[]},dequeue:function(t,e){e=e||"fx";var n=yt.queue(t,e),r=n.length,i=n.shift(),o=yt._queueHooks(t,e),a=function(){yt.dequeue(t,e)};"inprogress"===i&&(i=n.shift(),r--),i&&("fx"===e&&n.unshift("inprogress"),delete o.stop,i.call(t,a,o)),!r&&o&&o.empty.fire()},_queueHooks:function(t,e){var n=e+"queueHooks";return Lt.get(t,n)||Lt.access(t,n,{empty:yt.Callbacks("once memory").add(function(){Lt.remove(t,[e+"queue",n])})})}}),yt.fn.extend({queue:function(t,e){var n=2;return"string"!=typeof t&&(e=t,t="fx",n--),arguments.length\x20\t\r\n\f]+)/i,Xt=/^$|\/(?:java|ecma)script/i,Kt={option:[1,""," "],thead:[1,""],col:[2,""],tr:[2,""],td:[3,""],_default:[0,"",""]};Kt.optgroup=Kt.option,Kt.tbody=Kt.tfoot=Kt.colgroup=Kt.caption=Kt.thead,Kt.th=Kt.td;var Jt=/<|?\w+;/;!function(){var t=at.createDocumentFragment(),e=t.appendChild(at.createElement("div")),n=at.createElement("input");n.setAttribute("type","radio"),n.setAttribute("checked","checked"),n.setAttribute("name","t"),e.appendChild(n),mt.checkClone=e.cloneNode(!0).cloneNode(!0).lastChild.checked,e.innerHTML="",mt.noCloneChecked=!!e.cloneNode(!0).lastChild.defaultValue}();var Qt=at.documentElement,Gt=/^key/,Zt=/^(?:mouse|pointer|contextmenu|drag|drop)|click/,Yt=/^([^.]*)(?:\.(.+)|)/;yt.event={global:{},add:function(t,e,n,r,i){var o,a,s,u,c,l,f,p,d,h,v,g=Lt.get(t);if(g)for(n.handler&&(o=n,n=o.handler,i=o.selector),i&&yt.find.matchesSelector(Qt,i),n.guid||(n.guid=yt.guid++),(u=g.events)||(u=g.events={}),(a=g.handle)||(a=g.handle=function(e){return void 0!==yt&&yt.event.triggered!==e.type?yt.event.dispatch.apply(t,arguments):void 0}),e=(e||"").match(Ot)||[""],c=e.length;c--;)s=Yt.exec(e[c])||[],d=v=s[1],h=(s[2]||"").split(".").sort(),d&&(f=yt.event.special[d]||{},d=(i?f.delegateType:f.bindType)||d,f=yt.event.special[d]||{},l=yt.extend({type:d,origType:v,data:r,handler:n,guid:n.guid,selector:i,needsContext:i&&yt.expr.match.needsContext.test(i),namespace:h.join(".")},o),(p=u[d])||(p=u[d]=[],p.delegateCount=0,f.setup&&!1!==f.setup.call(t,r,h,a)||t.addEventListener&&t.addEventListener(d,a)),f.add&&(f.add.call(t,l),l.handler.guid||(l.handler.guid=n.guid)),i?p.splice(p.delegateCount++,0,l):p.push(l),yt.event.global[d]=!0)},remove:function(t,e,n,r,i){var o,a,s,u,c,l,f,p,d,h,v,g=Lt.hasData(t)&&Lt.get(t);if(g&&(u=g.events)){for(e=(e||"").match(Ot)||[""],c=e.length;c--;)if(s=Yt.exec(e[c])||[],d=v=s[1],h=(s[2]||"").split(".").sort(),d){for(f=yt.event.special[d]||{},d=(r?f.delegateType:f.bindType)||d,p=u[d]||[],s=s[2]&&new RegExp("(^|\\.)"+h.join("\\.(?:.*\\.|)")+"(\\.|$)"),a=o=p.length;o--;)l=p[o],!i&&v!==l.origType||n&&n.guid!==l.guid||s&&!s.test(l.namespace)||r&&r!==l.selector&&("**"!==r||!l.selector)||(p.splice(o,1),l.selector&&p.delegateCount--,f.remove&&f.remove.call(t,l));a&&!p.length&&(f.teardown&&!1!==f.teardown.call(t,h,g.handle)||yt.removeEvent(t,d,g.handle),delete u[d])}else for(d in u)yt.event.remove(t,d+e[c],n,r,!0);yt.isEmptyObject(u)&&Lt.remove(t,"handle events")}},dispatch:function(t){var e,n,r,i,o,a,s=yt.event.fix(t),u=new Array(arguments.length),c=(Lt.get(this,"events")||{})[s.type]||[],l=yt.event.special[s.type]||{};for(u[0]=s,e=1;e=1))for(;c!==this;c=c.parentNode||this)if(1===c.nodeType&&("click"!==t.type||!0!==c.disabled)){for(o=[],a={},n=0;n-1:yt.find(i,this,null,[c]).length),a[i]&&o.push(r);o.length&&s.push({elem:c,handlers:o})}return c=this,u\s*$/g;yt.extend({htmlPrefilter:function(t){return t.replace(/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([a-z][^\/\0>\x20\t\r\n\f]*)[^>]*)\/>/gi,"<$1>$2>")},clone:function(t,e,n){var r,i,o,a,s=t.cloneNode(!0),u=yt.contains(t.ownerDocument,t);if(!(mt.noCloneChecked||1!==t.nodeType&&11!==t.nodeType||yt.isXMLDoc(t)))for(a=x(s),o=x(t),r=0,i=o.length;r0&&C(a,!u&&x(t,"script")),s},cleanData:function(t){for(var e,n,r,i=yt.event.special,o=0;void 0!==(n=t[o]);o++)if(It(n)){if(e=n[Lt.expando]){if(e.events)for(r in e.events)i[r]?yt.event.remove(n,r):yt.removeEvent(n,r,e.handle);n[Lt.expando]=void 0}n[Rt.expando]&&(n[Rt.expando]=void 0)}}}),yt.fn.extend({detach:function(t){return L(this,t,!0)},remove:function(t){return L(this,t)},text:function(t){return Dt(this,function(t){return void 0===t?yt.text(this):this.empty().each(function(){1!==this.nodeType&&11!==this.nodeType&&9!==this.nodeType||(this.textContent=t)})},null,t,arguments.length)},append:function(){return I(this,arguments,function(t){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){S(this,t).appendChild(t)}})},prepend:function(){return I(this,arguments,function(t){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var e=S(this,t);e.insertBefore(t,e.firstChild)}})},before:function(){return I(this,arguments,function(t){this.parentNode&&this.parentNode.insertBefore(t,this)})},after:function(){return I(this,arguments,function(t){this.parentNode&&this.parentNode.insertBefore(t,this.nextSibling)})},empty:function(){for(var t,e=0;null!=(t=this[e]);e++)1===t.nodeType&&(yt.cleanData(x(t,!1)),t.textContent="");return this},clone:function(t,e){return t=null!=t&&t,e=null==e?t:e,this.map(function(){return yt.clone(this,t,e)})},html:function(t){return Dt(this,function(t){var e=this[0]||{},n=0,r=this.length;if(void 0===t&&1===e.nodeType)return e.innerHTML;if("string"==typeof t&&!te.test(t)&&!Kt[(Vt.exec(t)||["",""])[1].toLowerCase()]){t=yt.htmlPrefilter(t);try{for(;n1)}}),yt.Tween=U,U.prototype={constructor:U,init:function(t,e,n,r,i,o){this.elem=t,this.prop=n,this.easing=i||yt.easing._default,this.options=e,this.start=this.now=this.cur(),this.end=r,this.unit=o||(yt.cssNumber[n]?"":"px")},cur:function(){var t=U.propHooks[this.prop];return t&&t.get?t.get(this):U.propHooks._default.get(this)},run:function(t){var e,n=U.propHooks[this.prop];return this.options.duration?this.pos=e=yt.easing[this.easing](t,this.options.duration*t,0,1,this.options.duration):this.pos=e=t,this.now=(this.end-this.start)*e+this.start,this.options.step&&this.options.step.call(this.elem,this.now,this),n&&n.set?n.set(this):U.propHooks._default.set(this),this}},U.prototype.init.prototype=U.prototype,U.propHooks={_default:{get:function(t){var e;return 1!==t.elem.nodeType||null!=t.elem[t.prop]&&null==t.elem.style[t.prop]?t.elem[t.prop]:(e=yt.css(t.elem,t.prop,""),e&&"auto"!==e?e:0)},set:function(t){yt.fx.step[t.prop]?yt.fx.step[t.prop](t):1!==t.elem.nodeType||null==t.elem.style[yt.cssProps[t.prop]]&&!yt.cssHooks[t.prop]?t.elem[t.prop]=t.now:yt.style(t.elem,t.prop,t.now+t.unit)}}},U.propHooks.scrollTop=U.propHooks.scrollLeft={set:function(t){t.elem.nodeType&&t.elem.parentNode&&(t.elem[t.prop]=t.now)}},yt.easing={linear:function(t){return t},swing:function(t){return.5-Math.cos(t*Math.PI)/2},_default:"swing"},yt.fx=U.prototype.init,yt.fx.step={};var de,he,ve=/^(?:toggle|show|hide)$/,ge=/queueHooks$/;yt.Animation=yt.extend(Q,{tweeners:{"*":[function(t,e){var n=this.createTween(t,e);return b(n.elem,t,Mt.exec(e),n),n}]},tweener:function(t,e){yt.isFunction(t)?(e=t,t=["*"]):t=t.match(Ot);for(var n,r=0,i=t.length;r1)},removeAttr:function(t){return this.each(function(){yt.removeAttr(this,t)})}}),yt.extend({attr:function(t,e,n){var r,i,o=t.nodeType;if(3!==o&&8!==o&&2!==o)return void 0===t.getAttribute?yt.prop(t,e,n):(1===o&&yt.isXMLDoc(t)||(i=yt.attrHooks[e.toLowerCase()]||(yt.expr.match.bool.test(e)?me:void 0)),void 0!==n?null===n?void yt.removeAttr(t,e):i&&"set"in i&&void 0!==(r=i.set(t,n,e))?r:(t.setAttribute(e,n+""),n):i&&"get"in i&&null!==(r=i.get(t,e))?r:(r=yt.find.attr(t,e),null==r?void 0:r))},attrHooks:{type:{set:function(t,e){if(!mt.radioValue&&"radio"===e&&u(t,"input")){var n=t.value;return t.setAttribute("type",e),n&&(t.value=n),e}}}},removeAttr:function(t,e){var n,r=0,i=e&&e.match(Ot);if(i&&1===t.nodeType)for(;n=i[r++];)t.removeAttribute(n)}}),me={set:function(t,e,n){return!1===e?yt.removeAttr(t,n):t.setAttribute(n,n),n}},yt.each(yt.expr.match.bool.source.match(/\w+/g),function(t,e){var n=ye[e]||yt.find.attr;ye[e]=function(t,e,r){var i,o,a=e.toLowerCase();return r||(o=ye[a],ye[a]=i,i=null!=n(t,e,r)?a:null,ye[a]=o),i}});var be=/^(?:input|select|textarea|button)$/i,_e=/^(?:a|area)$/i;yt.fn.extend({prop:function(t,e){return Dt(this,yt.prop,t,e,arguments.length>1)},removeProp:function(t){return this.each(function(){delete this[yt.propFix[t]||t]})}}),yt.extend({prop:function(t,e,n){var r,i,o=t.nodeType;if(3!==o&&8!==o&&2!==o)return 1===o&&yt.isXMLDoc(t)||(e=yt.propFix[e]||e,i=yt.propHooks[e]),void 0!==n?i&&"set"in i&&void 0!==(r=i.set(t,n,e))?r:t[e]=n:i&&"get"in i&&null!==(r=i.get(t,e))?r:t[e]},propHooks:{tabIndex:{get:function(t){var e=yt.find.attr(t,"tabindex");return e?parseInt(e,10):be.test(t.nodeName)||_e.test(t.nodeName)&&t.href?0:-1}}},propFix:{for:"htmlFor",class:"className"}}),mt.optSelected||(yt.propHooks.selected={get:function(t){var e=t.parentNode;return e&&e.parentNode&&e.parentNode.selectedIndex,null},set:function(t){var e=t.parentNode;e&&(e.selectedIndex,e.parentNode&&e.parentNode.selectedIndex)}}),yt.each(["tabIndex","readOnly","maxLength","cellSpacing","cellPadding","rowSpan","colSpan","useMap","frameBorder","contentEditable"],function(){yt.propFix[this.toLowerCase()]=this}),yt.fn.extend({addClass:function(t){var e,n,r,i,o,a,s,u=0;if(yt.isFunction(t))return this.each(function(e){yt(this).addClass(t.call(this,e,Z(this)))});if("string"==typeof t&&t)for(e=t.match(Ot)||[];n=this[u++];)if(i=Z(n),r=1===n.nodeType&&" "+G(i)+" "){for(a=0;o=e[a++];)r.indexOf(" "+o+" ")<0&&(r+=o+" ");s=G(r),i!==s&&n.setAttribute("class",s)}return this},removeClass:function(t){var e,n,r,i,o,a,s,u=0;if(yt.isFunction(t))return this.each(function(e){yt(this).removeClass(t.call(this,e,Z(this)))});if(!arguments.length)return this.attr("class","");if("string"==typeof t&&t)for(e=t.match(Ot)||[];n=this[u++];)if(i=Z(n),r=1===n.nodeType&&" "+G(i)+" "){for(a=0;o=e[a++];)for(;r.indexOf(" "+o+" ")>-1;)r=r.replace(" "+o+" "," ");s=G(r),i!==s&&n.setAttribute("class",s)}return this},toggleClass:function(t,e){var n=typeof t;return"boolean"==typeof e&&"string"===n?e?this.addClass(t):this.removeClass(t):yt.isFunction(t)?this.each(function(n){yt(this).toggleClass(t.call(this,n,Z(this),e),e)}):this.each(function(){var e,r,i,o;if("string"===n)for(r=0,i=yt(this),o=t.match(Ot)||[];e=o[r++];)i.hasClass(e)?i.removeClass(e):i.addClass(e);else void 0!==t&&"boolean"!==n||(e=Z(this),e&&Lt.set(this,"__className__",e),this.setAttribute&&this.setAttribute("class",e||!1===t?"":Lt.get(this,"__className__")||""))})},hasClass:function(t){var e,n,r=0;for(e=" "+t+" ";n=this[r++];)if(1===n.nodeType&&(" "+G(Z(n))+" ").indexOf(e)>-1)return!0;return!1}});yt.fn.extend({val:function(t){var e,n,r,i=this[0];{if(arguments.length)return r=yt.isFunction(t),this.each(function(n){var i;1===this.nodeType&&(i=r?t.call(this,n,yt(this).val()):t,null==i?i="":"number"==typeof i?i+="":Array.isArray(i)&&(i=yt.map(i,function(t){return null==t?"":t+""})),(e=yt.valHooks[this.type]||yt.valHooks[this.nodeName.toLowerCase()])&&"set"in e&&void 0!==e.set(this,i,"value")||(this.value=i))});if(i)return(e=yt.valHooks[i.type]||yt.valHooks[i.nodeName.toLowerCase()])&&"get"in e&&void 0!==(n=e.get(i,"value"))?n:(n=i.value,"string"==typeof n?n.replace(/\r/g,""):null==n?"":n)}}}),yt.extend({valHooks:{option:{get:function(t){var e=yt.find.attr(t,"value");return null!=e?e:G(yt.text(t))}},select:{get:function(t){var e,n,r,i=t.options,o=t.selectedIndex,a="select-one"===t.type,s=a?null:[],c=a?o+1:i.length;for(r=o<0?c:a?o:0;r-1)&&(n=!0);return n||(t.selectedIndex=-1),o}}}}),yt.each(["radio","checkbox"],function(){yt.valHooks[this]={set:function(t,e){if(Array.isArray(e))return t.checked=yt.inArray(yt(t).val(),e)>-1}},mt.checkOn||(yt.valHooks[this].get=function(t){return null===t.getAttribute("value")?"on":t.value})});var we=/^(?:focusinfocus|focusoutblur)$/;yt.extend(yt.event,{trigger:function(t,e,r,i){var o,a,s,u,c,l,f,p=[r||at],d=ht.call(t,"type")?t.type:t,h=ht.call(t,"namespace")?t.namespace.split("."):[];if(a=s=r=r||at,3!==r.nodeType&&8!==r.nodeType&&!we.test(d+yt.event.triggered)&&(d.indexOf(".")>-1&&(h=d.split("."),d=h.shift(),h.sort()),c=d.indexOf(":")<0&&"on"+d,t=t[yt.expando]?t:new yt.Event(d,"object"==typeof t&&t),t.isTrigger=i?2:3,t.namespace=h.join("."),t.rnamespace=t.namespace?new RegExp("(^|\\.)"+h.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,t.result=void 0,t.target||(t.target=r),e=null==e?[t]:yt.makeArray(e,[t]),f=yt.event.special[d]||{},i||!f.trigger||!1!==f.trigger.apply(r,e))){if(!i&&!f.noBubble&&!yt.isWindow(r)){for(u=f.delegateType||d,we.test(u+d)||(a=a.parentNode);a;a=a.parentNode)p.push(a),s=a;s===(r.ownerDocument||at)&&p.push(s.defaultView||s.parentWindow||n)}for(o=0;(a=p[o++])&&!t.isPropagationStopped();)t.type=o>1?u:f.bindType||d,l=(Lt.get(a,"events")||{})[t.type]&&Lt.get(a,"handle"),l&&l.apply(a,e),(l=c&&a[c])&&l.apply&&It(a)&&(t.result=l.apply(a,e),!1===t.result&&t.preventDefault());return t.type=d,i||t.isDefaultPrevented()||f._default&&!1!==f._default.apply(p.pop(),e)||!It(r)||c&&yt.isFunction(r[d])&&!yt.isWindow(r)&&(s=r[c],s&&(r[c]=null),yt.event.triggered=d,r[d](),yt.event.triggered=void 0,s&&(r[c]=s)),t.result}},simulate:function(t,e,n){var r=yt.extend(new yt.Event,n,{type:t,isSimulated:!0});yt.event.trigger(r,null,e)}}),yt.fn.extend({trigger:function(t,e){return this.each(function(){yt.event.trigger(t,e,this)})},triggerHandler:function(t,e){var n=this[0];if(n)return yt.event.trigger(t,e,n,!0)}}),yt.each("blur focus focusin focusout resize scroll click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup contextmenu".split(" "),function(t,e){yt.fn[e]=function(t,n){return arguments.length>0?this.on(e,null,t,n):this.trigger(e)}}),yt.fn.extend({hover:function(t,e){return this.mouseenter(t).mouseleave(e||t)}}),mt.focusin="onfocusin"in n,mt.focusin||yt.each({focus:"focusin",blur:"focusout"},function(t,e){var n=function(t){yt.event.simulate(e,t.target,yt.event.fix(t))};yt.event.special[e]={setup:function(){var r=this.ownerDocument||this,i=Lt.access(r,e);i||r.addEventListener(t,n,!0),Lt.access(r,e,(i||0)+1)},teardown:function(){var r=this.ownerDocument||this,i=Lt.access(r,e)-1;i?Lt.access(r,e,i):(r.removeEventListener(t,n,!0),Lt.remove(r,e))}}});var xe=n.location,Ce=yt.now(),Te=/\?/;yt.parseXML=function(t){var e;if(!t||"string"!=typeof t)return null;try{e=(new n.DOMParser).parseFromString(t,"text/xml")}catch(t){e=void 0}return e&&!e.getElementsByTagName("parsererror").length||yt.error("Invalid XML: "+t),e};var $e=/\[\]$/,ke=/^(?:submit|button|image|reset|file)$/i,Ae=/^(?:input|select|textarea|keygen)/i;yt.param=function(t,e){var n,r=[],i=function(t,e){var n=yt.isFunction(e)?e():e;r[r.length]=encodeURIComponent(t)+"="+encodeURIComponent(null==n?"":n)};if(Array.isArray(t)||t.jquery&&!yt.isPlainObject(t))yt.each(t,function(){i(this.name,this.value)});else for(n in t)Y(n,t[n],e,i);return r.join("&")},yt.fn.extend({serialize:function(){return yt.param(this.serializeArray())},serializeArray:function(){return this.map(function(){var t=yt.prop(this,"elements");return t?yt.makeArray(t):this}).filter(function(){var t=this.type;return this.name&&!yt(this).is(":disabled")&&Ae.test(this.nodeName)&&!ke.test(t)&&(this.checked||!zt.test(t))}).map(function(t,e){var n=yt(this).val();return null==n?null:Array.isArray(n)?yt.map(n,function(t){return{name:e.name,value:t.replace(/\r?\n/g,"\r\n")}}):{name:e.name,value:n.replace(/\r?\n/g,"\r\n")}}).get()}});var Ee=/^(.*?):[ \t]*([^\r\n]*)$/gm,Se=/^(?:about|app|app-storage|.+-extension|file|res|widget):$/,Oe=/^(?:GET|HEAD)$/,je={},Ne={},De="*/".concat("*"),Ie=at.createElement("a");Ie.href=xe.href,yt.extend({active:0,lastModified:{},etag:{},ajaxSettings:{url:xe.href,type:"GET",isLocal:Se.test(xe.protocol),global:!0,processData:!0,async:!0,contentType:"application/x-www-form-urlencoded; charset=UTF-8",accepts:{"*":De,text:"text/plain",html:"text/html",xml:"application/xml, text/xml",json:"application/json, text/javascript"},contents:{xml:/\bxml\b/,html:/\bhtml/,json:/\bjson\b/},responseFields:{xml:"responseXML",text:"responseText",json:"responseJSON"},converters:{"* text":String,"text html":!0,"text json":JSON.parse,"text xml":yt.parseXML},flatOptions:{url:!0,context:!0}},ajaxSetup:function(t,e){return e?nt(nt(t,yt.ajaxSettings),e):nt(yt.ajaxSettings,t)},ajaxPrefilter:tt(je),ajaxTransport:tt(Ne),ajax:function(t,e){function r(t,e,r,s){var c,p,d,_,w,x=e;l||(l=!0,u&&n.clearTimeout(u),i=void 0,a=s||"",C.readyState=t>0?4:0,c=t>=200&&t<300||304===t,r&&(_=rt(h,C,r)),_=it(h,_,C,c),c?(h.ifModified&&(w=C.getResponseHeader("Last-Modified"),w&&(yt.lastModified[o]=w),(w=C.getResponseHeader("etag"))&&(yt.etag[o]=w)),204===t||"HEAD"===h.type?x="nocontent":304===t?x="notmodified":(x=_.state,p=_.data,d=_.error,c=!d)):(d=x,!t&&x||(x="error",t<0&&(t=0))),C.status=t,C.statusText=(e||x)+"",c?m.resolveWith(v,[p,x,C]):m.rejectWith(v,[C,x,d]),C.statusCode(b),b=void 0,f&&g.trigger(c?"ajaxSuccess":"ajaxError",[C,h,c?p:d]),y.fireWith(v,[C,x]),f&&(g.trigger("ajaxComplete",[C,h]),--yt.active||yt.event.trigger("ajaxStop")))}"object"==typeof t&&(e=t,t=void 0),e=e||{};var i,o,a,s,u,c,l,f,p,d,h=yt.ajaxSetup({},e),v=h.context||h,g=h.context&&(v.nodeType||v.jquery)?yt(v):yt.event,m=yt.Deferred(),y=yt.Callbacks("once memory"),b=h.statusCode||{},_={},w={},x="canceled",C={readyState:0,getResponseHeader:function(t){var e;if(l){if(!s)for(s={};e=Ee.exec(a);)s[e[1].toLowerCase()]=e[2];e=s[t.toLowerCase()]}return null==e?null:e},getAllResponseHeaders:function(){return l?a:null},setRequestHeader:function(t,e){return null==l&&(t=w[t.toLowerCase()]=w[t.toLowerCase()]||t,_[t]=e),this},overrideMimeType:function(t){return null==l&&(h.mimeType=t),this},statusCode:function(t){var e;if(t)if(l)C.always(t[C.status]);else for(e in t)b[e]=[b[e],t[e]];return this},abort:function(t){var e=t||x;return i&&i.abort(e),r(0,e),this}};if(m.promise(C),h.url=((t||h.url||xe.href)+"").replace(/^\/\//,xe.protocol+"//"),h.type=e.method||e.type||h.method||h.type,h.dataTypes=(h.dataType||"*").toLowerCase().match(Ot)||[""],null==h.crossDomain){c=at.createElement("a");try{c.href=h.url,c.href=c.href,h.crossDomain=Ie.protocol+"//"+Ie.host!=c.protocol+"//"+c.host}catch(t){h.crossDomain=!0}}if(h.data&&h.processData&&"string"!=typeof h.data&&(h.data=yt.param(h.data,h.traditional)),et(je,h,e,C),l)return C;f=yt.event&&h.global,f&&0==yt.active++&&yt.event.trigger("ajaxStart"),h.type=h.type.toUpperCase(),h.hasContent=!Oe.test(h.type),o=h.url.replace(/#.*$/,""),h.hasContent?h.data&&h.processData&&0===(h.contentType||"").indexOf("application/x-www-form-urlencoded")&&(h.data=h.data.replace(/%20/g,"+")):(d=h.url.slice(o.length),h.data&&(o+=(Te.test(o)?"&":"?")+h.data,delete h.data),!1===h.cache&&(o=o.replace(/([?&])_=[^&]*/,"$1"),d=(Te.test(o)?"&":"?")+"_="+Ce+++d),h.url=o+d),h.ifModified&&(yt.lastModified[o]&&C.setRequestHeader("If-Modified-Since",yt.lastModified[o]),yt.etag[o]&&C.setRequestHeader("If-None-Match",yt.etag[o])),(h.data&&h.hasContent&&!1!==h.contentType||e.contentType)&&C.setRequestHeader("Content-Type",h.contentType),C.setRequestHeader("Accept",h.dataTypes[0]&&h.accepts[h.dataTypes[0]]?h.accepts[h.dataTypes[0]]+("*"!==h.dataTypes[0]?", "+De+"; q=0.01":""):h.accepts["*"]);for(p in h.headers)C.setRequestHeader(p,h.headers[p]);if(h.beforeSend&&(!1===h.beforeSend.call(v,C,h)||l))return C.abort();if(x="abort",y.add(h.complete),C.done(h.success),C.fail(h.error),i=et(Ne,h,e,C)){if(C.readyState=1,f&&g.trigger("ajaxSend",[C,h]),l)return C;h.async&&h.timeout>0&&(u=n.setTimeout(function(){C.abort("timeout")},h.timeout));try{l=!1,i.send(_,r)}catch(t){if(l)throw t;r(-1,t)}}else r(-1,"No Transport");return C},getJSON:function(t,e,n){return yt.get(t,e,n,"json")},getScript:function(t,e){return yt.get(t,void 0,e,"script")}}),yt.each(["get","post"],function(t,e){yt[e]=function(t,n,r,i){return yt.isFunction(n)&&(i=i||r,r=n,n=void 0),yt.ajax(yt.extend({url:t,type:e,dataType:i,data:n,success:r},yt.isPlainObject(t)&&t))}}),yt._evalUrl=function(t){return yt.ajax({url:t,type:"GET",dataType:"script",cache:!0,async:!1,global:!1,throws:!0})},yt.fn.extend({wrapAll:function(t){var e;return this[0]&&(yt.isFunction(t)&&(t=t.call(this[0])),e=yt(t,this[0].ownerDocument).eq(0).clone(!0),this[0].parentNode&&e.insertBefore(this[0]),e.map(function(){for(var t=this;t.firstElementChild;)t=t.firstElementChild;return t}).append(this)),this},wrapInner:function(t){return yt.isFunction(t)?this.each(function(e){yt(this).wrapInner(t.call(this,e))}):this.each(function(){var e=yt(this),n=e.contents();n.length?n.wrapAll(t):e.append(t)})},wrap:function(t){var e=yt.isFunction(t);return this.each(function(n){yt(this).wrapAll(e?t.call(this,n):t)})},unwrap:function(t){return this.parent(t).not("body").each(function(){yt(this).replaceWith(this.childNodes)}),this}}),yt.expr.pseudos.hidden=function(t){return!yt.expr.pseudos.visible(t)},yt.expr.pseudos.visible=function(t){return!!(t.offsetWidth||t.offsetHeight||t.getClientRects().length)},yt.ajaxSettings.xhr=function(){try{return new n.XMLHttpRequest}catch(t){}};var Le={0:200,1223:204},Re=yt.ajaxSettings.xhr();mt.cors=!!Re&&"withCredentials"in Re,mt.ajax=Re=!!Re,yt.ajaxTransport(function(t){var e,r;if(mt.cors||Re&&!t.crossDomain)return{send:function(i,o){var a,s=t.xhr();if(s.open(t.type,t.url,t.async,t.username,t.password),t.xhrFields)for(a in t.xhrFields)s[a]=t.xhrFields[a];t.mimeType&&s.overrideMimeType&&s.overrideMimeType(t.mimeType),t.crossDomain||i["X-Requested-With"]||(i["X-Requested-With"]="XMLHttpRequest");for(a in i)s.setRequestHeader(a,i[a]);e=function(t){return function(){e&&(e=r=s.onload=s.onerror=s.onabort=s.onreadystatechange=null,"abort"===t?s.abort():"error"===t?"number"!=typeof s.status?o(0,"error"):o(s.status,s.statusText):o(Le[s.status]||s.status,s.statusText,"text"!==(s.responseType||"text")||"string"!=typeof s.responseText?{binary:s.response}:{text:s.responseText},s.getAllResponseHeaders()))}},s.onload=e(),r=s.onerror=e("error"),void 0!==s.onabort?s.onabort=r:s.onreadystatechange=function(){4===s.readyState&&n.setTimeout(function(){e&&r()})},e=e("abort");try{s.send(t.hasContent&&t.data||null)}catch(t){if(e)throw t}},abort:function(){e&&e()}}}),yt.ajaxPrefilter(function(t){t.crossDomain&&(t.contents.script=!1)}),yt.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/\b(?:java|ecma)script\b/},converters:{"text script":function(t){return yt.globalEval(t),t}}}),yt.ajaxPrefilter("script",function(t){void 0===t.cache&&(t.cache=!1),t.crossDomain&&(t.type="GET")}),yt.ajaxTransport("script",function(t){if(t.crossDomain){var e,n;return{send:function(r,i){e=yt("
diff --git a/api/resources/assets/sass/_variables.scss b/api/resources/assets/sass/_variables.scss
deleted file mode 100644
index 53202ac1..00000000
--- a/api/resources/assets/sass/_variables.scss
+++ /dev/null
@@ -1,38 +0,0 @@
-
-// Body
-$body-bg: #f5f8fa;
-
-// Borders
-$laravel-border-color: darken($body-bg, 10%);
-$list-group-border: $laravel-border-color;
-$navbar-default-border: $laravel-border-color;
-$panel-default-border: $laravel-border-color;
-$panel-inner-border: $laravel-border-color;
-
-// Brands
-$brand-primary: #3097D1;
-$brand-info: #8eb4cb;
-$brand-success: #2ab27b;
-$brand-warning: #cbb956;
-$brand-danger: #bf5329;
-
-// Typography
-$icon-font-path: "~bootstrap-sass/assets/fonts/bootstrap/";
-$font-family-sans-serif: "Raleway", sans-serif;
-$font-size-base: 14px;
-$line-height-base: 1.6;
-$text-color: #636b6f;
-
-// Navbar
-$navbar-default-bg: #fff;
-
-// Buttons
-$btn-default-color: $text-color;
-
-// Inputs
-$input-border: lighten($text-color, 40%);
-$input-border-focus: lighten($brand-primary, 25%);
-$input-color-placeholder: lighten($text-color, 30%);
-
-// Panels
-$panel-default-heading-bg: #fff;
diff --git a/api/resources/assets/sass/app.scss b/api/resources/assets/sass/app.scss
deleted file mode 100644
index 35ce2dc0..00000000
--- a/api/resources/assets/sass/app.scss
+++ /dev/null
@@ -1,9 +0,0 @@
-
-// Fonts
-@import url(https://fonts.googleapis.com/css?family=Raleway:300,400,600);
-
-// Variables
-@import "variables";
-
-// Bootstrap
-@import "node_modules/bootstrap-sass/assets/stylesheets/bootstrap";
diff --git a/api/resources/views/emails/password/reset.blade.php b/api/resources/views/emails/password/reset.blade.php
new file mode 100644
index 00000000..3ecce3af
--- /dev/null
+++ b/api/resources/views/emails/password/reset.blade.php
@@ -0,0 +1,16 @@
+@component('mail::message')
+# Reset your password
+
+Hi {{ $content['name'] }}!
+
+Please click the link below to reset your password.
+
+@component('mail::button', ['url' => $content['url'], 'color' => 'blue'])
+Reset your password!
+@endcomponent
+
+If you have not requested a new password, please ignore this email.
+
+Hope to see you soon,
+{{ config('app.name') }} team
+@endcomponent
diff --git a/api/resources/views/vendor/mail/html/button.blade.php b/api/resources/views/vendor/mail/html/button.blade.php
new file mode 100644
index 00000000..c7aae1b8
--- /dev/null
+++ b/api/resources/views/vendor/mail/html/button.blade.php
@@ -0,0 +1,19 @@
+
diff --git a/api/resources/views/vendor/mail/html/footer.blade.php b/api/resources/views/vendor/mail/html/footer.blade.php
new file mode 100644
index 00000000..648c64b4
--- /dev/null
+++ b/api/resources/views/vendor/mail/html/footer.blade.php
@@ -0,0 +1,11 @@
+
diff --git a/api/resources/views/vendor/mail/html/header.blade.php b/api/resources/views/vendor/mail/html/header.blade.php
new file mode 100644
index 00000000..e1c90860
--- /dev/null
+++ b/api/resources/views/vendor/mail/html/header.blade.php
@@ -0,0 +1,13 @@
+
+
+
diff --git a/api/resources/views/vendor/mail/html/layout.blade.php b/api/resources/views/vendor/mail/html/layout.blade.php
new file mode 100644
index 00000000..01bd14e3
--- /dev/null
+++ b/api/resources/views/vendor/mail/html/layout.blade.php
@@ -0,0 +1,56 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ $header or '' }}
+
+
+
+
+
+
+
+
+ {{ Illuminate\Mail\Markdown::parse($slot) }}
+
+ {{ $subcopy or '' }}
+
+
+
+
+
+
+ {{ $footer or '' }}
+
+
+
+
+
+
diff --git a/api/resources/views/vendor/mail/html/message.blade.php b/api/resources/views/vendor/mail/html/message.blade.php
new file mode 100644
index 00000000..2ad23a66
--- /dev/null
+++ b/api/resources/views/vendor/mail/html/message.blade.php
@@ -0,0 +1,27 @@
+@component('mail::layout')
+ {{-- Header --}}
+ @slot('header')
+ @component('mail::header', ['url' => config('app.url')])
+ {{ config('app.name') }}
+ @endcomponent
+ @endslot
+
+ {{-- Body --}}
+ {{ $slot }}
+
+ {{-- Subcopy --}}
+ @isset($subcopy)
+ @slot('subcopy')
+ @component('mail::subcopy')
+ {{ $subcopy }}
+ @endcomponent
+ @endslot
+ @endisset
+
+ {{-- Footer --}}
+ @slot('footer')
+ @component('mail::footer')
+ © {{ date('Y') }} {{ config('app.name') }}. All rights reserved.
+ @endcomponent
+ @endslot
+@endcomponent
diff --git a/api/resources/views/vendor/mail/html/panel.blade.php b/api/resources/views/vendor/mail/html/panel.blade.php
new file mode 100644
index 00000000..f3970802
--- /dev/null
+++ b/api/resources/views/vendor/mail/html/panel.blade.php
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+ {{ Illuminate\Mail\Markdown::parse($slot) }}
+
+
+
+
+
+
diff --git a/api/resources/views/vendor/mail/html/promotion.blade.php b/api/resources/views/vendor/mail/html/promotion.blade.php
new file mode 100644
index 00000000..0debcf8a
--- /dev/null
+++ b/api/resources/views/vendor/mail/html/promotion.blade.php
@@ -0,0 +1,7 @@
+
diff --git a/api/resources/views/vendor/mail/html/promotion/button.blade.php b/api/resources/views/vendor/mail/html/promotion/button.blade.php
new file mode 100644
index 00000000..8e79081c
--- /dev/null
+++ b/api/resources/views/vendor/mail/html/promotion/button.blade.php
@@ -0,0 +1,13 @@
+
diff --git a/api/resources/views/vendor/mail/html/subcopy.blade.php b/api/resources/views/vendor/mail/html/subcopy.blade.php
new file mode 100644
index 00000000..c3df7b4c
--- /dev/null
+++ b/api/resources/views/vendor/mail/html/subcopy.blade.php
@@ -0,0 +1,7 @@
+
+
+
+ {{ Illuminate\Mail\Markdown::parse($slot) }}
+
+
+
diff --git a/api/resources/views/vendor/mail/html/table.blade.php b/api/resources/views/vendor/mail/html/table.blade.php
new file mode 100644
index 00000000..a5f3348b
--- /dev/null
+++ b/api/resources/views/vendor/mail/html/table.blade.php
@@ -0,0 +1,3 @@
+
+{{ Illuminate\Mail\Markdown::parse($slot) }}
+
diff --git a/api/resources/views/vendor/mail/html/themes/default.css b/api/resources/views/vendor/mail/html/themes/default.css
new file mode 100644
index 00000000..271ec86b
--- /dev/null
+++ b/api/resources/views/vendor/mail/html/themes/default.css
@@ -0,0 +1,299 @@
+/* Base */
+
+body, body *:not(html):not(style):not(br):not(tr):not(code) {
+ font-family: 'Chivo', Helvetica, sans-serif;
+ box-sizing: border-box;
+}
+
+body {
+ background-color: #f5f8fa;
+ color: #74787E;
+ height: 100%;
+ hyphens: auto;
+ line-height: 1.4;
+ margin: 0;
+ -moz-hyphens: auto;
+ -ms-word-break: break-all;
+ width: 100% !important;
+ -webkit-hyphens: auto;
+ -webkit-text-size-adjust: none;
+ word-break: break-all;
+ word-break: break-word;
+}
+
+p,
+ul,
+ol,
+blockquote {
+ line-height: 1.4;
+ text-align: left;
+}
+
+a {
+ color: #3869D4;
+}
+
+a img {
+ border: none;
+}
+
+/* Typography */
+
+h1 {
+ color: #2F3133;
+ font-size: 19px;
+ font-weight: bold;
+ margin-top: 0;
+ text-align: left;
+}
+
+h2 {
+ color: #2F3133;
+ font-size: 16px;
+ font-weight: bold;
+ margin-top: 0;
+ text-align: left;
+}
+
+h3 {
+ color: #2F3133;
+ font-size: 14px;
+ font-weight: bold;
+ margin-top: 0;
+ text-align: left;
+}
+
+p {
+ color: #74787E;
+ font-size: 16px;
+ line-height: 1.5em;
+ margin-top: 0;
+ text-align: left;
+}
+
+p.sub {
+ font-size: 12px;
+}
+
+img {
+ max-width: 100%;
+}
+
+/* Layout */
+
+.wrapper {
+ background-color: #f5f8fa;
+ margin: 0;
+ padding: 0;
+ width: 100%;
+ -premailer-cellpadding: 0;
+ -premailer-cellspacing: 0;
+ -premailer-width: 100%;
+}
+
+.content {
+ margin: 0;
+ padding: 0;
+ width: 100%;
+ -premailer-cellpadding: 0;
+ -premailer-cellspacing: 0;
+ -premailer-width: 100%;
+}
+
+/* Header */
+
+.header {
+ padding: 25px 0;
+ text-align: center;
+ background-color: #BCCBD3;
+}
+
+.header a {
+ color: #fff;
+ font-size: 19px;
+ text-decoration: none;
+ text-shadow: 0 1px 0 white;
+}
+
+/* Body */
+
+.body {
+ background-color: #FFFFFF;
+ border-bottom: 1px solid #EDEFF2;
+ border-top: 1px solid #EDEFF2;
+ margin: 0;
+ padding: 0;
+ width: 100%;
+ -premailer-cellpadding: 0;
+ -premailer-cellspacing: 0;
+ -premailer-width: 100%;
+}
+
+.inner-body {
+ background-color: #FFFFFF;
+ margin: 0 auto;
+ padding: 0;
+ width: 570px;
+ -premailer-cellpadding: 0;
+ -premailer-cellspacing: 0;
+ -premailer-width: 570px;
+}
+
+/* Subcopy */
+
+.subcopy {
+ border-top: 1px solid #EDEFF2;
+ margin-top: 25px;
+ padding-top: 25px;
+}
+
+.subcopy p {
+ font-size: 12px;
+}
+
+/* Footer */
+
+.footer {
+ margin: 0 auto;
+ padding: 0;
+ text-align: center;
+ width: 570px;
+ -premailer-cellpadding: 0;
+ -premailer-cellspacing: 0;
+ -premailer-width: 570px;
+ background-color: #BCCBD3;
+}
+
+.footer p {
+ color: #fff;
+ font-size: 12px;
+ text-align: center;
+}
+
+/* Tables */
+
+.table table {
+ margin: 30px auto;
+ width: 100%;
+ -premailer-cellpadding: 0;
+ -premailer-cellspacing: 0;
+ -premailer-width: 100%;
+}
+
+.table th {
+ border-bottom: 1px solid #EDEFF2;
+ padding-bottom: 8px;
+}
+
+.table td {
+ color: #74787E;
+ font-size: 15px;
+ line-height: 18px;
+ padding: 10px 0;
+}
+
+.content-cell {
+ padding: 35px;
+}
+
+/* Buttons */
+
+.action {
+ margin: 30px auto;
+ padding: 0;
+ text-align: center;
+ width: 100%;
+ -premailer-cellpadding: 0;
+ -premailer-cellspacing: 0;
+ -premailer-width: 100%;
+}
+
+.button {
+ border-radius: 3px;
+ box-shadow: 0 2px 3px rgba(0, 0, 0, 0.16);
+ color: #67A0D6;
+ display: inline-block;
+ text-decoration: none;
+ -webkit-text-size-adjust: none;
+}
+
+.button-blue {
+ color: #fff;
+ background-color: #67A0D6;
+ border-top: 10px solid #67A0D6;
+ border-right: 18px solid #67A0D6;
+ border-bottom: 10px solid #67A0D6;
+ border-left: 18px solid #67A0D6;
+}
+
+.button-green {
+ background-color: #2ab27b;
+ border-top: 10px solid #2ab27b;
+ border-right: 18px solid #2ab27b;
+ border-bottom: 10px solid #2ab27b;
+ border-left: 18px solid #2ab27b;
+}
+
+.button-red {
+ background-color: #bf5329;
+ border-top: 10px solid #bf5329;
+ border-right: 18px solid #bf5329;
+ border-bottom: 10px solid #bf5329;
+ border-left: 18px solid #bf5329;
+}
+
+/* Panels */
+
+.panel {
+ margin: 0 0 21px;
+}
+
+.panel-content {
+ background-color: #EDEFF2;
+ padding: 16px;
+}
+
+.panel-item {
+ padding: 0;
+}
+
+.panel-item p:last-of-type {
+ margin-bottom: 0;
+ padding-bottom: 0;
+}
+
+/* Promotions */
+
+.promotion {
+ background-color: #FFFFFF;
+ border: 2px dashed #9BA2AB;
+ margin: 0;
+ margin-bottom: 25px;
+ margin-top: 25px;
+ padding: 24px;
+ width: 100%;
+ -premailer-cellpadding: 0;
+ -premailer-cellspacing: 0;
+ -premailer-width: 100%;
+}
+
+.promotion h1 {
+ text-align: center;
+}
+
+.promotion p {
+ font-size: 15px;
+ text-align: center;
+}
+
+.footer_fix {
+ background-color: #BCCBD3;
+}
+
+#image_fix {
+ display: inline-block;
+}
+
+.caption {
+ display: block;
+}
\ No newline at end of file
diff --git a/api/resources/views/vendor/mail/markdown/button.blade.php b/api/resources/views/vendor/mail/markdown/button.blade.php
new file mode 100644
index 00000000..97444ebd
--- /dev/null
+++ b/api/resources/views/vendor/mail/markdown/button.blade.php
@@ -0,0 +1 @@
+{{ $slot }}: {{ $url }}
diff --git a/api/resources/views/vendor/mail/markdown/footer.blade.php b/api/resources/views/vendor/mail/markdown/footer.blade.php
new file mode 100644
index 00000000..3338f620
--- /dev/null
+++ b/api/resources/views/vendor/mail/markdown/footer.blade.php
@@ -0,0 +1 @@
+{{ $slot }}
diff --git a/api/resources/views/vendor/mail/markdown/header.blade.php b/api/resources/views/vendor/mail/markdown/header.blade.php
new file mode 100644
index 00000000..aaa3e575
--- /dev/null
+++ b/api/resources/views/vendor/mail/markdown/header.blade.php
@@ -0,0 +1 @@
+[{{ $slot }}]({{ $url }})
diff --git a/api/resources/views/vendor/mail/markdown/layout.blade.php b/api/resources/views/vendor/mail/markdown/layout.blade.php
new file mode 100644
index 00000000..9378baa0
--- /dev/null
+++ b/api/resources/views/vendor/mail/markdown/layout.blade.php
@@ -0,0 +1,9 @@
+{!! strip_tags($header) !!}
+
+{!! strip_tags($slot) !!}
+@isset($subcopy)
+
+{!! strip_tags($subcopy) !!}
+@endisset
+
+{!! strip_tags($footer) !!}
diff --git a/api/resources/views/vendor/mail/markdown/message.blade.php b/api/resources/views/vendor/mail/markdown/message.blade.php
new file mode 100644
index 00000000..b409c71c
--- /dev/null
+++ b/api/resources/views/vendor/mail/markdown/message.blade.php
@@ -0,0 +1,27 @@
+@component('mail::layout')
+ {{-- Header --}}
+ @slot('header')
+ @component('mail::header', ['url' => config('app.url')])
+ {{ config('app.name') }}
+ @endcomponent
+ @endslot
+
+ {{-- Body --}}
+ {{ $slot }}
+
+ {{-- Subcopy --}}
+ @isset($subcopy)
+ @slot('subcopy')
+ @component('mail::subcopy')
+ {{ $subcopy }}
+ @endcomponent
+ @endslot
+ @endisset
+
+ {{-- Footer --}}
+ @slot('footer')
+ @component('mail::footer')
+ Ā© {{ date('Y') }} {{ config('app.name') }}. All rights reserved.
+ @endcomponent
+ @endslot
+@endcomponent
diff --git a/api/resources/views/vendor/mail/markdown/panel.blade.php b/api/resources/views/vendor/mail/markdown/panel.blade.php
new file mode 100644
index 00000000..3338f620
--- /dev/null
+++ b/api/resources/views/vendor/mail/markdown/panel.blade.php
@@ -0,0 +1 @@
+{{ $slot }}
diff --git a/api/resources/views/vendor/mail/markdown/promotion.blade.php b/api/resources/views/vendor/mail/markdown/promotion.blade.php
new file mode 100644
index 00000000..3338f620
--- /dev/null
+++ b/api/resources/views/vendor/mail/markdown/promotion.blade.php
@@ -0,0 +1 @@
+{{ $slot }}
diff --git a/api/resources/views/vendor/mail/markdown/promotion/button.blade.php b/api/resources/views/vendor/mail/markdown/promotion/button.blade.php
new file mode 100644
index 00000000..aaa3e575
--- /dev/null
+++ b/api/resources/views/vendor/mail/markdown/promotion/button.blade.php
@@ -0,0 +1 @@
+[{{ $slot }}]({{ $url }})
diff --git a/api/resources/views/vendor/mail/markdown/subcopy.blade.php b/api/resources/views/vendor/mail/markdown/subcopy.blade.php
new file mode 100644
index 00000000..3338f620
--- /dev/null
+++ b/api/resources/views/vendor/mail/markdown/subcopy.blade.php
@@ -0,0 +1 @@
+{{ $slot }}
diff --git a/api/resources/views/vendor/mail/markdown/table.blade.php b/api/resources/views/vendor/mail/markdown/table.blade.php
new file mode 100644
index 00000000..3338f620
--- /dev/null
+++ b/api/resources/views/vendor/mail/markdown/table.blade.php
@@ -0,0 +1 @@
+{{ $slot }}
diff --git a/api/routes/api.php b/api/routes/api.php
index 13267e84..10a9d49f 100644
--- a/api/routes/api.php
+++ b/api/routes/api.php
@@ -17,6 +17,8 @@
Route::get('documentation', 'DocumentationController@index');
Route::post('deploy', 'GithubWebhookController@deploy');
+ Route::get('leaderboard', 'LeaderboardController@index');
+
// Route = api/auth
Route::prefix('auth')->group(function () {
Route::post('/', 'AuthController@auth');
@@ -26,6 +28,9 @@
Route::post('register', 'AuthController@register');
Route::post('refresh', 'AuthController@refresh');
+
+ Route::post('reset', 'PasswordResetController@sendResetMail');
+ Route::post('reset/{token}', 'PasswordResetController@resetPassword');
});
// Authenticated url's for Installation devices
diff --git a/api/storage/example/observations/common-tern1.jpg b/api/storage/example/observations/common-tern1.jpg
new file mode 100644
index 00000000..aa151bba
Binary files /dev/null and b/api/storage/example/observations/common-tern1.jpg differ
diff --git a/api/storage/example/observations/common-tern2.jpg b/api/storage/example/observations/common-tern2.jpg
new file mode 100644
index 00000000..657fe9e2
Binary files /dev/null and b/api/storage/example/observations/common-tern2.jpg differ
diff --git a/api/storage/example/observations/common-tern3.jpg b/api/storage/example/observations/common-tern3.jpg
new file mode 100644
index 00000000..dffe925f
Binary files /dev/null and b/api/storage/example/observations/common-tern3.jpg differ
diff --git a/api/storage/example/observations/common-tern4.jpg b/api/storage/example/observations/common-tern4.jpg
new file mode 100644
index 00000000..03c33307
Binary files /dev/null and b/api/storage/example/observations/common-tern4.jpg differ
diff --git a/api/storage/example/observations/common-tern5.jpg b/api/storage/example/observations/common-tern5.jpg
new file mode 100644
index 00000000..315c3fef
Binary files /dev/null and b/api/storage/example/observations/common-tern5.jpg differ
diff --git a/api/storage/example/observations/duck.jpg b/api/storage/example/observations/duck.jpg
new file mode 100644
index 00000000..9552652d
Binary files /dev/null and b/api/storage/example/observations/duck.jpg differ
diff --git a/api/storage/example/observations/flamingo.jpg b/api/storage/example/observations/flamingo.jpg
new file mode 100644
index 00000000..804d04fd
Binary files /dev/null and b/api/storage/example/observations/flamingo.jpg differ
diff --git a/api/storage/example/observations/ostrich.jpg b/api/storage/example/observations/ostrich.jpg
new file mode 100644
index 00000000..bdb39b21
Binary files /dev/null and b/api/storage/example/observations/ostrich.jpg differ
diff --git a/api/storage/example/observations/penguin.jpg b/api/storage/example/observations/penguin.jpg
new file mode 100644
index 00000000..3601b711
Binary files /dev/null and b/api/storage/example/observations/penguin.jpg differ
diff --git a/api/storage/example/observations/pigeon.jpg b/api/storage/example/observations/pigeon.jpg
new file mode 100644
index 00000000..4477e560
Binary files /dev/null and b/api/storage/example/observations/pigeon.jpg differ
diff --git a/api/webpack.mix.js b/api/webpack.mix.js
deleted file mode 100644
index 3e6f5712..00000000
--- a/api/webpack.mix.js
+++ /dev/null
@@ -1,15 +0,0 @@
-const { mix } = require('laravel-mix');
-
-/*
- |--------------------------------------------------------------------------
- | Mix Asset Management
- |--------------------------------------------------------------------------
- |
- | Mix provides a clean, fluent API for defining some Webpack build steps
- | for your Laravel application. By default, we are compiling the Sass
- | file for the application as well as bundling up all the JS files.
- |
- */
-
-mix.js('resources/assets/js/app.js', 'public/js')
- .sass('resources/assets/sass/app.scss', 'public/css');
diff --git a/deploy.sh b/deploy.sh
index b3a1b32c..5c7d0046 100644
--- a/deploy.sh
+++ b/deploy.sh
@@ -34,7 +34,9 @@ cd ..
# Symlink the front-end to the back-end
CURRENT_DIR=$(pwd)
-ln -s $CURRENT_DIR/web-app/build $CURRENT_DIR/api/public
+
+ln -sf $CURRENT_DIR/web-app/build/**/* $CURRENT_DIR/api/public
+ln -sf $CURRENT_DIR/web-app/build/* $CURRENT_DIR/api/public
# Set live
cd api
diff --git a/gh-pages.sh b/gh-pages.sh
new file mode 100644
index 00000000..c22bed8b
--- /dev/null
+++ b/gh-pages.sh
@@ -0,0 +1,16 @@
+cd web-app
+
+yarn install
+yarn build-css
+
+NODE_ENV="production" PUBLIC_URL="https://osoc17.github.io/code9000/" REACT_APP_ROUTER="HASH" REACT_APP_API_URL="https://develop.birds.today/api" yarn build
+
+cd ..
+
+git branch -D gh-pages
+git push origin --delete gh-pages
+
+git add web-app/build && git commit -m "Initial dist subtree commit"
+
+git subtree split --prefix web-app/build -b gh-pages
+git push -f origin gh-pages:gh-pages
diff --git a/hardware/DEPLOYMENT.md b/hardware/DEPLOYMENT.md
new file mode 100644
index 00000000..8829341e
--- /dev/null
+++ b/hardware/DEPLOYMENT.md
@@ -0,0 +1,44 @@
+# Deployment guide
+*This guide will help you to get quickly the IoT deployed.*
+
+## I. Preparations
+### Hardware
+First things first, check if you the following things at your disposal:
+- [ ] NatureBytes Kit
+- [ ] CAT5 UTP cable
+- [ ] Power cables
+- [ ] Waterproof box
+- [ ] 4G router
+- [ ] Solar panel
+- [ ] Solar charger
+- [ ] Lead acid battery
+- [ ] Fuse
+
+:exclamation: **If you haven't configured the NatureBytes kit yet, click [here](https://www.github.com/oSoc17/code9000/hardware/SETUP.md).**
+
+:exclamation: **If you don't know which solar kit you need, click [here](https://www.github.com/oSoc17/code9000/hardware/SOLARKIT.md).**
+
+### Platform
+- The IoT performs the best when it's installed on an island with the **following dimensions**:
+
+
+
+ Island dimensions
+
+
+- The IoT needs to be **mounted at the same height** as were the birds can land. Most of the time, you can put it on the ground. You only need a tree stump to mount the IoT.
+
+- The IoT **can't be placed directly in the sunlight**, you need to build a roof for it. Too much sunlight will lead to overexposed pictures. You can always put the solar panel on top of the roof.
+
+- The solar panel needs to be mounted in an angle of **39Ā° facing South for Ghent, Belgium**.
+More details about this are explained [here](https://www.github.com/oSoc17/hardware/SOLARKIT.md). If you are having troubles with building a holder under the right angle you can always buy a holder for your solar panel ([example](http://www.conrad.be/ce/nl/product/110539/Modulehouder-Phaesun-102750?ref=list))
+
+## II. Install the IoT
+1. Mount the IoT on a tree stump or something else using the straps delivered with the NatureBytes kit.
+2. Connect the ethernet cable, power supply cable to the IoT device.
+3. Close the IoT properly and and make sure it's watertight!
+4. Switch on the power supply in the waterproof box.
+5. Close the waterproof box and make sure it's watertight! You can add a lock for extra security.
+6. Fix the waterproof box on the platform.
+7. Wait for the IoT until it's completely booted.
+8. Test if everything works: power, pictures are taken, pictures arrive at the API, ...
diff --git a/hardware/README.md b/hardware/README.md
index 025857f1..bb6678b2 100644
--- a/hardware/README.md
+++ b/hardware/README.md
@@ -1,21 +1,10 @@
-# Hardware
+# IoT hardware
-The IOT installation uses several components to get the job done. These
+The IoT installation uses several components to get the job done. These
components are mostly included in the NatureBytes kit but some other
things are bought somewhere else.
-## Hardware
-
-- NatureBytes kit (enclosure, Raspberry Pi, Raspberry Pi Camera V2)
-- Solar panels
-- Solar charger
-- Solar battery
-- 4G router
-
-An alternative setup could be achieved using a 3G/4G network shield for example: [Adafruit FONA 3G + GPS](https://learn.adafruit.com/adafruit-fona-3g-cellular-gps-breakout/overview) which also includes GPS. With this shield the setup becomes smaller and easier to hide.
-However, a separate 5V power supply will be needed to provide enough power for the shield while sending data over the cellular network.
-
-## How does thing work?
+## I. How does thing work?
1. PIR sensor detects a bird.
2. The Raspberry Pi wakes up.
@@ -23,50 +12,25 @@ However, a separate 5V power supply will be needed to provide enough power for t
4. A 4G connection is initiated.
5. The picture and the meta data is send to the API.
-## How to set this thing up?
+## II. Hardware
-1. Download the Raspbian OS from the official Raspberry Pi website:
-2. Run ```sudo apt-get update && sudo apt-get upgrade -y``` in the terminal to get the Raspberry Pi up to date.
-3. Install the following Python module with pip (if not installed):
- - Requests (```sudo pip install requests```)
-
-4. Clone the #code9000 project to the Raspberry Pi:
- - ```sudo apt-get install git```
- - ```git clone https://github.com/osoc17/code9000```
-5. Configure the Raspberry Pi: ```sudo raspi-config```
-
- **Switch to CLI mode**
- - Select ```Boot Options```
- - Select ```Desktop / CLI```
- - Select ```Console Autologin```
+- NatureBytes kit (enclosure, Raspberry Pi A+, Raspberry Pi Camera V2, PIR sensor)
+- Solar kit
+- Arduino UNO (Battery Guard)
+- A few resistors (2x 20K Ohm and 2x 10K Ohm) and some wires
+- 5V Relay
+- 4G router
+- Waterproof box
- **Enable camera**
- - Select ```Interfacing Options```
- - Select ```Camera```
- - Select ```Yes (enable camera interface) and OK```
+An alternative setup could be achieved using a 3G/4G network shield for example: [Adafruit FONA 3G + GPS](https://learn.adafruit.com/adafruit-fona-3g-cellular-gps-breakout/overview) which also includes GPS. With this shield the setup becomes smaller and easier to hide.
+However, a separate 5V power supply will be needed to provide enough power for the shield while sending data over the cellular network.
- **Timezone and keyboard layout**
- - Select ```Localisation Options```
- - Select ```Change Timezone```
- - Select ```Select your timezone from the list```
- - Select ```Localisation Options```
- - Select ```Change Keyboard Layout```
+You can find more information about the solar kit [here](https://github.com/oSoc17/code9000/hardware/SOLARKIT.md).
- **Splash screen**
- - Select ```Boot Options```
- - Select ```Splash Screen```
- - Select ```No```
- - Exit raspi-config: ```Finish and reboot```
-6. Add a crontab entry to launch the shell script when booting
+## III. Installation
- - Navigate to the launcher.sh path: ```cd /path/to/launcher.sh```
- - Make the script executable: ```chmod +x launcher.sh```
- - Edit crontab table: ```crontab -e```
- - Add this line at the end of the file: ```@reboot sh /path/to/launcher.sh > /path/to/cronlog 2>&1```
- - Close the file using CTRL+X (Nano editor)
- - You should see a message that a new entry was added to the crontab jobs
-7. Reboot the Raspberry Pi using ```sudo reboot```
+You can find the installation instructions for both the hardware and the software [here](https://github.com/oSoc17/code9000/hardware/SETUP.md).
-8. If everything goes well, you should see a message in the terminal that we're looking now for birds.
+## IV. Deployment
-9. To save power, you can disable the HDMI display by adding ```DISABLE reboot``` to the crontab (explained in 6). This setting doesn't survive a reboot.
+You can find the deployment instructions [here](https://github.com/oSoc17/code9000/hardware/DEPLOYMENT.md).
diff --git a/hardware/SETUP.md b/hardware/SETUP.md
new file mode 100644
index 00000000..0051fd28
--- /dev/null
+++ b/hardware/SETUP.md
@@ -0,0 +1,91 @@
+# Setup guide
+*This guide will help you to get quickly the IoT it's software installed and configured.*
+
+## I. NatureBytes kit
+
+1. Follow the installation guide from NatureBytes to setup the kit. You can find the guide [here](http://naturebytes.org/downloads/Naturebytes_Wildlife_Cam_Kit_Instructions_AplusPi1_101.pdf).
+1. Download the Raspbian OS from the official Raspberry Pi website:
+2. Run ```sudo apt-get update && sudo apt-get upgrade -y``` in the terminal to get the Raspberry Pi up to date.
+3. Install the following Python module with pip (if not installed):
+ - Requests (```sudo pip install requests```)
+4. Clone the #code9000 project to the Raspberry Pi:
+ - ```sudo apt-get install git```
+ - ```git clone https://github.com/osoc17/code9000```
+5. Configure the Raspberry Pi: ```sudo raspi-config```
+ **Switch to CLI mode**
+ - Select ```Boot Options```
+ - Select ```Desktop / CLI```
+ - Select ```Console Autologin```
+
+ **Enable camera**
+ - Select ```Interfacing Options```
+ - Select ```Camera```
+ - Select ```Yes (enable camera interface) and OK```
+IoT
+ **Timezone and keyboard layout**
+ - Select ```Localisation Options```
+ - Select ```Change Timezone```
+ - Select ```Select your timezone from the list```
+ - Select ```Localisation Options```
+ - Select ```Change Keyboard Layout```
+
+ **Splash screen**
+ - Select ```Boot Options```
+ - Select ```Splash Screen```
+ - Select ```No```
+ - Exit raspi-config: ```Finish and reboot```
+6. Modify ```constants.json``` to your own configuration.
+7. Add a crontab entry to launch the shell script when booting
+ - Navigate to the launcher.sh path: ```cd /path/to/launcher.sh```
+ - Make the script executable: ```chmod +x launcher.sh```
+ - Edit crontab table: ```crontab -e```
+ - Add this line at the end of the file: ```@reboot sh /path/to/launcher.sh > /path/to/cronlog 2>&1```
+ - Close the file using CTRL+X (Nano editor)
+ - You should see a message that a new entry was added to the crontab jobs
+8. Reboot the Raspberry Pi using ```sudo reboot```
+9. If everything goes well, you should see a message in the terminal that we're looking now for birds.
+10. To save power, you can disable the HDMI display by adding ```@reboot /opt/vc/bin/tvservice -o``` to the crontab (explained in 6). This setting doesn't survive a reboot.
+
+## II. Battery Guard (Arduino)
+
+
+
+1. Build the hardware from the schematic below:
+
+
+ Battery Guard schematic
+
+ :information_source: The shutdown signal and the relay will be connected later in this guide. You can measure the voltage dividers output to check if the circuit is correctly build.
+2. Download the [Arduino IDE](https://www.arduino.cc/en/Main/Software) and install it.
+3. Install the following Arduino libraries:
+ - [DS3231 by NorthernWidget](https://github.com/NorthernWidget/DS3231)
+ - [Adafruit SleepyDog by Adafruit](https://github.com/adafruit/Adafruit_SleepyDog)
+4. Open the ```powerManager.ino``` script in the Arduino IDE.
+5. Configure the script according to your configuration.
+6. Flash it on your Arduino. This script is tested **only** on an Arduino UNO R3.
+
+## III. Solar panels & charger
+
+
+
+
+ Solar schematic
+
+
+1. Connect the solar charger, solar panel and battery correctly according to the instructions from the manufacturer.
+2. Setup the router with a SIM card according to the instructions from the manufacturer.
+2. Connect the UTP cable between the IoT and the router.
+3. Connect the power and signal wires between the Battery Guard and the IoT device while **keeping an eye on the polarity**.
+4. Connect the 4G router to the battery while **keeping an eye on the polarity**.
+5. Connect the IoT device to the battery while **keeping an eye on the polarity**.
+6. If all goes well, the router and the IoT should boot and send the captured pictures to the API you configured in ```constants.json```.
+
+## IV. Hooking it up all together
+
+All the pieces are now ready to be connected with each other, you can find a simple schematic here:
+
+
+
+
+ Interconnection schematic
+
diff --git a/hardware/SOLARKIT.md b/hardware/SOLARKIT.md
new file mode 100644
index 00000000..693d95c8
--- /dev/null
+++ b/hardware/SOLARKIT.md
@@ -0,0 +1,186 @@
+# Solar kit
+*This guide will help you to choose the right solar kit for your IoT.*
+
+## I. Solar irradiance
+Every place on earth performs differently when it comes to solar panels.
+The reason for that is related to the amount of sun, clouds, shadow, ... for that specific place.
+
+You can find out quickly how your solar panel will perform in reality by using historical data.
+The website [solarelectricityhandbook.com](http://solarelectricityhandbook.com/solar-irradiance.html) allows you to check out the data easily for almost every location in the world.
+
+We used the 'Optimal Year Round' setting to have the best performance in every season of the year.
+
+
+
+
+ Solar irradiance for Ghent, Belgium (source )
+
+
+If you look at the months we see that December has the lowest amount of sunlight available for Ghent, Belgium: 1030 KWh/mĀ²/day.
+
+Tip: Always use the lowest values to make sure it works in even the worst possible conditions.
+
+## II. Determining technology
+Now that we know how much sunlight is available we can select our solar kit.
+The technology used in the solar panel plays a roll in how efficient the solar panel is (how much sunlight will be converted into electricity). Those information should be available in the datasheet of the solar panel provided by the manufacturer. However, our manufacturer didn't provide a detailed datasheet with this information so we used the following chart to determine the efficiency:
+
+
+
+
+ Solar efficiency for each technology through the years (source )
+
+
+We are using a 'Single Crystal' solar panel, the current efficiency is around 25-27 %.
+
+## III. Calculating the solar panel size
+1. Measure the voltage of each component that will be connected to the battery charged by the solar panel.
+2. Do the same with for the current of each component.
+3. We should calculate how much power in Watt's our device will use, this can be done pretty simple using the following formula: ```Power P = Voltage V * Current I``` Do this for every device that will be connected to the battery and take the sum of it. For example:
+
+ Device
+ Voltage
+ Current
+ Power
+
+
+ Raspberry Pi A+
+ 5 V
+ 0.15 A
+ 0.75 W
+
+
+ TP-Link TL-MR6400 (4G router)
+ 12 V
+ 0.15 A
+ 1.80 W
+
+
+
+TOTAL: 2.55 W power consumption for all the devices connected to the battery.
+
+4. Now we need to take the efficiency of the solar panel and DC-DC convertor into account. Since our battery works on 12 V we need to convert it into 5 V to power the Raspberry Pi A+. These convertors have an efficiency between 75-85%. To combine the efficiencies you need to multiply them:
+
+```TOTAL EFFICIENCY = PANEL EFFICIENCY * DC-DC EFFICIENCY```
+
+
+We will do this for our example above: TOTAL= 0.25 * 0.75 = 0.19 (19 %)
+
+Tip: Always use the lowest values to make sure it works in even the worst possible conditions.
+
+5. Do you want to run the device 24/24 or not? The amount of hours/day is important to determine how much the devices will consume of the battery. In our case, the device only needs to running during the day (12 hours).
+
+6. The actual size of the solar panel is determined by the efficiency, the power consumption of the devices, the amount of sunlight available for your location and how long the devices need to be active, let's bring them together!
+Convert the power consumption calculated in [step III](#iii-calculating-the-solar-panel-size) to Wh/day with this formula:
+```POWER USED DAY = POWER CONSUMPTION * HOURS OF OPERATION```
+In our example: CONSUMPTION EACH DAY = 2.55 W * 12 hours = 30.6 Wh/day We use the famous '[Rule of Three](https://en.wikipedia.org/wiki/Cross-multiplication#Rule_of_Three)' for this. For example:
+
+ Available power
+ Panel size
+
+
+ 1030 Wh/day
+ 1 mĀ²
+
+
+ 1 Wh/day
+ 0.000970874 mĀ²
+
+
+ 30.6 Wh/day
+ 0.029708738 mĀ²
+
+
+But we forgot something! The efficiency we calculated in [step III.5](#iii-calculating-the-solar-panel-size)! We can do that using this formula:
+
+```ACTUAL SIZE = CALCULATED SIZE * TOTAL EFFICIENCY * CHARGER EFFICIENCY```
+
+
+For example (CHARGER EFFICIENCY = 1): ACTUAL SIZE = 0.029708738 mĀ²/0.19 = 0.16 mĀ²
+See the further for the real charger efficiency.
+
+## IV. Battery & charger
+We selected a solar panel but we also need a battery to store the generated energy and a charger to keep the battery healthy.
+We will explain this using a simple example:
+
+From the datasheet of a [80Wp solar panel](http://www.conrad.be/ce/nl/product/1485914/):
+
+
+
+ No load voltage
+ Nominal voltage
+ Nominal current
+ Short-circuit current
+
+
+ 22.3 V
+ 12 V
+ 4.49 A
+ 4.85 A
+
+
+
+If the charger has an efficiency of 100% then the nominal current (4.49 A) of the solar panel will be delivered to the battery, in the real world this isn't true of course.
+
+### A) PWM based charger
+
+Since our system is small we used a PWM based charger because it's cheap for small solar systems.
+You need to choose a charger that can handle the current from your solar panel and can handle the battery. Most chargers are compatible with 12/24 V lead acid batteries.
+
+For our example we can buy a charger that can handle at least 6 A of charging current. We picked up this one: [solar charger PWM](http://www.conrad.be/ce/nl/product/110864/IVT-200001-Solar-laadregelaar-12-V-24-V-8-A?ref=list).
+
+### B) MPPT based charger
+
+For bigger solar systems a MPPT (Maximum Point to Point Tracker) charger is better because it's efficiency is much higher then in comparison with a PWM charger.
+Choosing a MPPT charger is almost the same process as with the PWM charger in the previous paragraph.
+
+### C) Which charger do I need?
+It all depends on how big you installation will be, in our case we only need to feed one device so a cheaper PWM charger is sufficient for our purpose.
+You can find more information [here](http://offgridham.com/2015/12/solar-charge-controller/).
+
+### D) Battery
+The size of your battery depends on how much generated energy you would like to store.
+We will explain the calculations using a simple example:
+
+From the datasheet of a [12V lead acid 6.6 Ah](http://www.conrad.be/ce/nl/product/110752/):
+
+
+
+ Voltage
+ Capacity
+
+
+ 12 V
+ 6.6 Ah
+
+
+
+In [step III](#iii-calculating-the-solar-panel-size) we did the calculations of the power consumption of our IoT, we can use that to figure out how long our IoT can run on a fully charged battery.
+
+The capacity of a battery is defined as the as '[Ampere hour](https://en.wikipedia.org/wiki/Ampere_hour)'. So the current that can flow in 1 hour is equal to 6.6 A, if our device uses 6.6 A then it runs exactly for 1 hour.
+To make the calculations easier we asume that the lead acid battery has a steady voltage of 12 V (in the real world this depends on the temperature, charging level, ...)
+
+Available power capacity: 12 V * 6.6 A = 79.2 Wh
+
+We use the famous '[Rule of Three](https://en.wikipedia.org/wiki/Cross-multiplication#Rule_of_Three)' for this in a slightly different way then above. For example:
+
+ Power
+ Time
+
+
+ 79.2 W
+ 1 hour
+
+
+ 1 W
+ 79.2 hours
+
+
+ 2.55 W
+ approx. 31 hours
+
+
+
+## V. Conclusion
+Using the calculations above, we determined that we need to buy a solar panel with a size of at least 0.16 mĀ² for our example. Because we haven't no information about the charger efficiency we over dimension the solar panel a little bit. It's always advised to add some overhead as a reserve.
+
+:exclamation: Make sure that your IoT has a fuse between the load and the battery, some solar charges include them by default. If not, you need to get a fuse that can handle the short-circuit current of your battery. You can find more information [here](http://www.judgeelectrical.co.uk/domestic-electrical/fuses/fuse-rating.html).
diff --git a/hardware/birds.py b/hardware/birds.py
index 9adb619a..56eed9f0 100755
--- a/hardware/birds.py
+++ b/hardware/birds.py
@@ -13,6 +13,7 @@
import threading
import os
import asyncio
+from subprocess import call
# Set current working directory
os.chdir(sys.path[0])
@@ -48,26 +49,48 @@
# Create a camera instance
camera = picamera.PiCamera()
-# Upload queue
+# Upload callback
uploadQueue = []
uploading = False
+uploadCounter = 0
+launch_time = time.time()
def takePicture(channel):
global camera
global uploadQueue
-
+ global uploadCounter
+ global launch_time
logging.debug('Bird detected!')
- logging.debug('Getting current time...')
- currentTimestamp = getTime() # Read the current time
- logging.debug('Taking picture...')
- camera.capture('{}/{}.jpg'.format(configData['pictureDir'], currentTimestamp)) # Take picture
- uploadQueue.append('{}/{}.jpg'.format(configData['pictureDir'], currentTimestamp))
- logging.debug('Bird event completed!')
+
+ if uploadCounter < configData['uploadLimit']:
+ logging.debug('Getting current time...')
+ currentTimestamp = getTime() # Read the current time
+ logging.debug('Taking picture...')
+ camera.capture('{}/{}.jpg'.format(configData['pictureDir'], currentTimestamp)) # Take picture
+ uploadQueue.append('{}/{}.jpg'.format(configData['pictureDir'], currentTimestamp))
+ logging.debug('Bird event completed!')
+ uploadCounter = uploadCounter + 1
+ else:
+ logging.warning('Daily upload limit reached!')
+ if launch_time - time.time() > configData['resetUploadTime']: # Reset limit if device is powered for more then 24 hours.
+ uploadCounter = 0
+
+
+# Shutdown callback
+running = True
+
+def shutdown(channel):
+ running = False
+ GPIO.remove_event_detect(configData['pirsensor'])
+ GPIO.cleanup()
+ call("sudo shutdown -h now", shell=True)
# Setup GPIO pins
GPIO.setmode(GPIO.BOARD) # Use BOARD GPIO numbers
GPIO.setup(configData['pirsensor'], GPIO.IN, pull_up_down=GPIO.PUD_DOWN) # Setup GPIO for PIR sensor
+GPIO.setup(configData['shutdown'], GPIO.IN, pull_up_down=GPIO.PUD_DOWN) # Setup GPIO for PIR sensor
GPIO.add_event_detect(configData['pirsensor'], GPIO.FALLING, bouncetime=configData['bouncetime'], callback=takePicture)
+GPIO.add_event_detect(configData['shutdown'], GPIO.FALLING, bouncetime=configData['bouncetime'], callback=shutdown)
@asyncio.coroutine
def uploadAsync():
@@ -97,11 +120,13 @@ def getTime():
return strftime('%Y-%m-%d %H:%M:%S', gmtime())
def loop():
+ global running
+
try:
logging.info('You can always exit this program by pressing CTRL + C')
logging.info('Looking for birds...')
busy = False
- while(True):
+ while(running):
if len(uploadQueue) and busy == False:
busy = True
asyncLoop = asyncio.get_event_loop()
diff --git a/hardware/constants.json b/hardware/constants.json
index c239a844..257daef8 100644
--- a/hardware/constants.json
+++ b/hardware/constants.json
@@ -5,11 +5,11 @@
"bouncetime": 1000,
"sleeptime": 10,
"pirsensor": 13,
- "timebetween": 10.0,
- "maxpictures": 10,
- "networkBuffer": 3,
+ "shutdown": 14,
+ "uploadLimit": 115,
+ "resetUploadTime": 86400,
"location": {
"lat": 50.8503,
"lon": 4.3517
}
-}
\ No newline at end of file
+}
diff --git a/hardware/images/islandDimensions.png b/hardware/images/islandDimensions.png
new file mode 100644
index 00000000..acdbc7d6
Binary files /dev/null and b/hardware/images/islandDimensions.png differ
diff --git a/hardware/images/schematicBatteryguard.png b/hardware/images/schematicBatteryguard.png
new file mode 100644
index 00000000..bb0f7832
Binary files /dev/null and b/hardware/images/schematicBatteryguard.png differ
diff --git a/hardware/images/schematicInterconnection.png b/hardware/images/schematicInterconnection.png
new file mode 100644
index 00000000..3165f1cc
Binary files /dev/null and b/hardware/images/schematicInterconnection.png differ
diff --git a/hardware/images/schematicSolar.png b/hardware/images/schematicSolar.png
new file mode 100644
index 00000000..cc2a4eb9
Binary files /dev/null and b/hardware/images/schematicSolar.png differ
diff --git a/hardware/images/solarIrradiance.png b/hardware/images/solarIrradiance.png
new file mode 100644
index 00000000..c82ddd94
Binary files /dev/null and b/hardware/images/solarIrradiance.png differ
diff --git a/hardware/islandDimensions.odg b/hardware/islandDimensions.odg
new file mode 100644
index 00000000..e423693f
Binary files /dev/null and b/hardware/islandDimensions.odg differ
diff --git a/hardware/powerManager.ino b/hardware/powerManager.ino
new file mode 100644
index 00000000..bd38e8f5
--- /dev/null
+++ b/hardware/powerManager.ino
@@ -0,0 +1,76 @@
+/*
+ #code9000 power manager for the IOT Bird installation
+*/
+
+#include
+#include "DS3231.h"
+#include
+
+RTClib RTC;
+
+/*
+ Settings
+*/
+#define HOUR_ON 7
+#define HOUR_OFF 19
+#define TIMEZONE_OFFSET 2
+#define SHUTDOWN_PIN 6
+#define BATTERY_PIN A0
+#define POWER_PIN 2
+#define MINIMUM_VOLTAGE 750
+#define SLEEP_TIME 8000
+#define SHUTDOWN_TIME 60000
+
+/*
+ Setup pins and RTC clock
+*/
+void setup () {
+ Serial.begin(115200);
+ Wire.begin();
+ pinMode(SHUTDOWN_PIN, OUTPUT);
+ pinMode(POWER_PIN, OUTPUT);
+ pinMode(BATTERY_PIN, INPUT);
+}
+
+/*
+ Loop
+*/
+void loop () {
+ Serial.println("Sleeping in a moment.");
+ delay(10); // Wait for going to sleep
+ Watchdog.sleep(SLEEP_TIME); // Sleep to save power
+ delay(10); // Wait for waking up
+
+ // Check if we need to go to sleep or battery is critical
+ if (getSleepState() || getBatteryState()) {
+ Serial.println("Shutting down IOT...");
+ digitalWrite(SHUTDOWN_PIN, HIGH);
+ delay(SHUTDOWN_TIME);
+ Serial.println("Powering down NOW...");
+ digitalWrite(POWER_PIN, LOW);
+ }
+ else {
+ Serial.println("System running...");
+ digitalWrite(SHUTDOWN_PIN, LOW);
+ digitalWrite(POWER_PIN, HIGH);
+ }
+}
+
+/*
+ Returns if in sleep mode
+ True = shutdown
+ False = keep power ON
+*/
+bool getSleepState() {
+ DateTime now = RTC.now();
+ return (now.hour() + TIMEZONE_OFFSET) <= HOUR_ON || (now.hour() + TIMEZONE_OFFSET) >= HOUR_OFF)
+}
+
+/*
+ Returns the status of the battery
+ True = shutdown
+ False = keep power ON
+*/
+bool getBatteryState() {
+ return analogRead(BATTERY_PIN) < MINIMUM_VOLTAGE)
+}
diff --git a/hardware/solarInterconnections.odg b/hardware/solarInterconnections.odg
new file mode 100644
index 00000000..fcfc472c
Binary files /dev/null and b/hardware/solarInterconnections.odg differ
diff --git a/hardware/solarSchematic.odg b/hardware/solarSchematic.odg
new file mode 100644
index 00000000..5ad7519c
Binary files /dev/null and b/hardware/solarSchematic.odg differ
diff --git a/web-app/.eslintrc b/web-app/.eslintrc
index 8eca0a4b..1bd0aeb9 100644
--- a/web-app/.eslintrc
+++ b/web-app/.eslintrc
@@ -11,6 +11,7 @@
"import/no-named-as-default": 0,
"jsx-a11y/label-has-for": 0,
"jsx-a11y/no-static-element-interactions": 0,
+ "jsx-a11y/no-noninteractive-element-interactions": 0,
"arrow-parens": "off",
"react/prefer-stateless-function": 0,
"react/prop-types": 0
diff --git a/web-app/.gitignore b/web-app/.gitignore
index 887bc7f4..e3edfff0 100644
--- a/web-app/.gitignore
+++ b/web-app/.gitignore
@@ -6,9 +6,6 @@
# testing
/coverage
-# production
-/build
-
# misc
.DS_Store
.env.local
@@ -21,4 +18,4 @@ yarn-debug.log*
yarn-error.log*
.env
-*.css
\ No newline at end of file
+/src/**/*.css
diff --git a/web-app/README.md b/web-app/README.md
index ff47d400..cc8c7c76 100644
--- a/web-app/README.md
+++ b/web-app/README.md
@@ -1,2 +1,31 @@
-# Web app
+Web application
+====================
+----------------
+Getting Started
+-------------
+----------
+
+#### Requirements
+
+- yarn (^v0.22.0)
+
+#### Installation
+
+To install the webapplication, first go the the folder "web-app". Then use yarn to install the required modules.
+
+```
+$ cd web-app/
+$ yarn install
+$ yarn start && yarn watch-css
+```
+
+
+
+
+Copy .env.example to .env, and change the URL to the one of your API.
+```
+$ cp .env.example .env
+$ nano .env
+$ yarn start
+```
diff --git a/web-app/package.json b/web-app/package.json
index e740e796..920d502b 100644
--- a/web-app/package.json
+++ b/web-app/package.json
@@ -1,18 +1,23 @@
{
"name": "web-app",
- "version": "0.2.0",
+ "version": "0.3.0",
"private": true,
- "homepage": "http://birds.today/build",
+ "homepage": "http://birds.today",
"dependencies": {
"axios": "^0.16.2",
+ "flickity": "^2.0.9",
"form-serialize": "^0.7.2",
"lodash": "^4.17.4",
"react": "^15.6.1",
"react-dom": "^15.6.1",
+ "react-modal": "^2.2.2",
"react-redux": "^5.0.5",
"react-router-dom": "^4.1.1",
+ "react-swing": "^0.0.7",
+ "react-transitive-number": "^3.0.1",
"redux": "^3.7.1",
- "redux-thunk": "^2.2.0"
+ "redux-thunk": "^2.2.0",
+ "swing": "^3.1.1"
},
"devDependencies": {
"eslint": "^3.19.0",
@@ -20,8 +25,8 @@
"eslint-plugin-import": "^2.6.1",
"eslint-plugin-jsx-a11y": "^5.1.1",
"eslint-plugin-react": "^7.1.0",
- "react-scripts": "1.0.10",
- "node-sass": "^4.5.0"
+ "node-sass": "^4.5.0",
+ "react-scripts": "1.0.10"
},
"scripts": {
"start": "react-scripts start",
diff --git a/web-app/public/android-icon-144x144.png b/web-app/public/android-icon-144x144.png
new file mode 100644
index 00000000..ad174ee7
Binary files /dev/null and b/web-app/public/android-icon-144x144.png differ
diff --git a/web-app/public/android-icon-192x192.png b/web-app/public/android-icon-192x192.png
new file mode 100644
index 00000000..f897db2b
Binary files /dev/null and b/web-app/public/android-icon-192x192.png differ
diff --git a/web-app/public/android-icon-36x36.png b/web-app/public/android-icon-36x36.png
new file mode 100644
index 00000000..6de61b54
Binary files /dev/null and b/web-app/public/android-icon-36x36.png differ
diff --git a/web-app/public/android-icon-48x48.png b/web-app/public/android-icon-48x48.png
new file mode 100644
index 00000000..0bb59596
Binary files /dev/null and b/web-app/public/android-icon-48x48.png differ
diff --git a/web-app/public/android-icon-72x72.png b/web-app/public/android-icon-72x72.png
new file mode 100644
index 00000000..d284d71c
Binary files /dev/null and b/web-app/public/android-icon-72x72.png differ
diff --git a/web-app/public/android-icon-96x96.png b/web-app/public/android-icon-96x96.png
new file mode 100644
index 00000000..5ad05f6c
Binary files /dev/null and b/web-app/public/android-icon-96x96.png differ
diff --git a/web-app/public/apple-icon-114x114.png b/web-app/public/apple-icon-114x114.png
new file mode 100644
index 00000000..6dd7379d
Binary files /dev/null and b/web-app/public/apple-icon-114x114.png differ
diff --git a/web-app/public/apple-icon-120x120.png b/web-app/public/apple-icon-120x120.png
new file mode 100644
index 00000000..5582ec50
Binary files /dev/null and b/web-app/public/apple-icon-120x120.png differ
diff --git a/web-app/public/apple-icon-144x144.png b/web-app/public/apple-icon-144x144.png
new file mode 100644
index 00000000..ad174ee7
Binary files /dev/null and b/web-app/public/apple-icon-144x144.png differ
diff --git a/web-app/public/apple-icon-152x152.png b/web-app/public/apple-icon-152x152.png
new file mode 100644
index 00000000..7d15c5f4
Binary files /dev/null and b/web-app/public/apple-icon-152x152.png differ
diff --git a/web-app/public/apple-icon-180x180.png b/web-app/public/apple-icon-180x180.png
new file mode 100644
index 00000000..b2c1f912
Binary files /dev/null and b/web-app/public/apple-icon-180x180.png differ
diff --git a/web-app/public/apple-icon-57x57.png b/web-app/public/apple-icon-57x57.png
new file mode 100644
index 00000000..c8b64e81
Binary files /dev/null and b/web-app/public/apple-icon-57x57.png differ
diff --git a/web-app/public/apple-icon-60x60.png b/web-app/public/apple-icon-60x60.png
new file mode 100644
index 00000000..dfeccee9
Binary files /dev/null and b/web-app/public/apple-icon-60x60.png differ
diff --git a/web-app/public/apple-icon-72x72.png b/web-app/public/apple-icon-72x72.png
new file mode 100644
index 00000000..d284d71c
Binary files /dev/null and b/web-app/public/apple-icon-72x72.png differ
diff --git a/web-app/public/apple-icon-76x76.png b/web-app/public/apple-icon-76x76.png
new file mode 100644
index 00000000..412213cd
Binary files /dev/null and b/web-app/public/apple-icon-76x76.png differ
diff --git a/web-app/public/apple-icon-precomposed.png b/web-app/public/apple-icon-precomposed.png
new file mode 100644
index 00000000..4ccb821b
Binary files /dev/null and b/web-app/public/apple-icon-precomposed.png differ
diff --git a/web-app/public/apple-icon.png b/web-app/public/apple-icon.png
new file mode 100644
index 00000000..4ccb821b
Binary files /dev/null and b/web-app/public/apple-icon.png differ
diff --git a/web-app/public/favicon-16x16.png b/web-app/public/favicon-16x16.png
new file mode 100644
index 00000000..f821c973
Binary files /dev/null and b/web-app/public/favicon-16x16.png differ
diff --git a/web-app/public/favicon-32x32.png b/web-app/public/favicon-32x32.png
new file mode 100644
index 00000000..d8a38dab
Binary files /dev/null and b/web-app/public/favicon-32x32.png differ
diff --git a/web-app/public/favicon-96x96.png b/web-app/public/favicon-96x96.png
new file mode 100644
index 00000000..5ad05f6c
Binary files /dev/null and b/web-app/public/favicon-96x96.png differ
diff --git a/web-app/public/favicon.ico b/web-app/public/favicon.ico
index c971ff99..d078d23c 100644
Binary files a/web-app/public/favicon.ico and b/web-app/public/favicon.ico differ
diff --git a/web-app/public/index.html b/web-app/public/index.html
index 43a00f9b..7f056ac7 100644
--- a/web-app/public/index.html
+++ b/web-app/public/index.html
@@ -9,17 +9,23 @@
homescreen on Android. See https://developers.google.com/web/fundamentals/engage-and-retain/web-app-manifest/
-->
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
#CODE9000
+
diff --git a/web-app/public/manifest.json b/web-app/public/manifest.json
index be607e41..2fe01ac8 100644
--- a/web-app/public/manifest.json
+++ b/web-app/public/manifest.json
@@ -1,13 +1,44 @@
{
- "short_name": "React App",
- "name": "Create React App Sample",
- "icons": [
- {
- "src": "favicon.ico",
- "sizes": "192x192",
- "type": "image/png"
- }
- ],
+ "short_name": "Birds.today",
+ "name": "#CODE9000 - Birds.today",
+ "icons": [
+ {
+ "src": "/android-icon-36x36.png",
+ "sizes": "36x36",
+ "type": "image/png",
+ "density": "0.75"
+ },
+ {
+ "src": "/android-icon-48x48.png",
+ "sizes": "48x48",
+ "type": "image/png",
+ "density": "1.0"
+ },
+ {
+ "src": "/android-icon-72x72.png",
+ "sizes": "72x72",
+ "type": "image/png",
+ "density": "1.5"
+ },
+ {
+ "src": "/android-icon-96x96.png",
+ "sizes": "96x96",
+ "type": "image/png",
+ "density": "2.0"
+ },
+ {
+ "src": "/android-icon-144x144.png",
+ "sizes": "144x144",
+ "type": "image/png",
+ "density": "3.0"
+ },
+ {
+ "src": "/android-icon-192x192.png",
+ "sizes": "192x192",
+ "type": "image/png",
+ "density": "4.0"
+ }
+ ],
"start_url": "./index.html",
"display": "standalone",
"theme_color": "#000000",
diff --git a/web-app/public/ms-icon-144x144.png b/web-app/public/ms-icon-144x144.png
new file mode 100644
index 00000000..ad174ee7
Binary files /dev/null and b/web-app/public/ms-icon-144x144.png differ
diff --git a/web-app/public/ms-icon-150x150.png b/web-app/public/ms-icon-150x150.png
new file mode 100644
index 00000000..026535e7
Binary files /dev/null and b/web-app/public/ms-icon-150x150.png differ
diff --git a/web-app/public/ms-icon-310x310.png b/web-app/public/ms-icon-310x310.png
new file mode 100644
index 00000000..cd7867cc
Binary files /dev/null and b/web-app/public/ms-icon-310x310.png differ
diff --git a/web-app/public/ms-icon-70x70.png b/web-app/public/ms-icon-70x70.png
new file mode 100644
index 00000000..b4ddab17
Binary files /dev/null and b/web-app/public/ms-icon-70x70.png differ
diff --git a/web-app/src/actions/index.js b/web-app/src/actions/index.js
index af1ce9a7..ded65cc0 100644
--- a/web-app/src/actions/index.js
+++ b/web-app/src/actions/index.js
@@ -1,2 +1,3 @@
export * from './observations';
export * from './application';
+export * from './ranking';
diff --git a/web-app/src/actions/ranking/index.js b/web-app/src/actions/ranking/index.js
new file mode 100644
index 00000000..2bc5a11a
--- /dev/null
+++ b/web-app/src/actions/ranking/index.js
@@ -0,0 +1,6 @@
+import { LOAD_RANKING } from './types';
+
+export const loadRanking = (ranking) => ({
+ type: LOAD_RANKING,
+ ranking,
+});
diff --git a/web-app/src/actions/ranking/types.js b/web-app/src/actions/ranking/types.js
new file mode 100644
index 00000000..179bb359
--- /dev/null
+++ b/web-app/src/actions/ranking/types.js
@@ -0,0 +1 @@
+export const LOAD_RANKING = 'LOAD_RANKING';
diff --git a/web-app/src/actions/types.js b/web-app/src/actions/types.js
index d651d86a..ac0ad668 100644
--- a/web-app/src/actions/types.js
+++ b/web-app/src/actions/types.js
@@ -1,2 +1,3 @@
export * from './observations/types';
export * from './application/types';
+export * from './ranking/types';
diff --git a/web-app/src/components/Alerts/Alerts.scss b/web-app/src/components/Alerts/Alerts.scss
new file mode 100644
index 00000000..0c3ace60
--- /dev/null
+++ b/web-app/src/components/Alerts/Alerts.scss
@@ -0,0 +1,39 @@
+@import "../../theme/_index.scss";
+
+.Alert {
+ border-radius: 3px;
+ padding: $padding * 2;
+ margin: $margin * 3 0;
+
+ background-repeat: no-repeat;
+ background-size: 20px 20px;
+ background-position: 12px center;
+ padding-left: 38px;
+}
+
+.Errors {
+ color: lighten($tabasco, 5);
+
+ background-color: lighten($tabasco, 45);
+ background-image: url('./icons/error.svg');
+}
+
+.Errors__List {
+ list-style-type: disc;
+ margin-left: $margin * 3;
+}
+
+.Errors__List li {
+ margin-bottom: $margin * 1.5;
+}
+
+.Errors__List li:last-child {
+ margin-bottom: 0px;
+}
+
+.Success {
+ color: darken($jordyBlue, 10);
+
+ background-color: lighten($jordyBlue, 10);
+ background-image: url('./icons/success.svg');
+}
\ No newline at end of file
diff --git a/web-app/src/components/Alerts/Errors.js b/web-app/src/components/Alerts/Errors.js
new file mode 100644
index 00000000..aa72b75f
--- /dev/null
+++ b/web-app/src/components/Alerts/Errors.js
@@ -0,0 +1,27 @@
+import React from 'react';
+
+import classNames from '../../utils/classNames';
+
+import './Alerts.css';
+
+export const Errors = ({ errors, className }) => {
+ const keys = Object.keys(errors);
+
+ if (keys.length > 1) {
+ return (
+
+
+ {keys.map((key) => {errors[key]} )}
+
+
+ );
+ }
+
+ return (
+
+ {keys.map((key) => (
+
{errors[key]}
+ ))}
+
+ );
+};
diff --git a/web-app/src/components/Alerts/Success.js b/web-app/src/components/Alerts/Success.js
new file mode 100644
index 00000000..f64dee76
--- /dev/null
+++ b/web-app/src/components/Alerts/Success.js
@@ -0,0 +1,18 @@
+import React from 'react';
+
+import './Alerts.css';
+
+export const Success = ({ title, body }) => {
+ return (
+
+ {title && (
+
+ {title}
+
+ )}
+
+ {body}
+
+
+ );
+};
diff --git a/web-app/src/components/Alerts/icons/error.svg b/web-app/src/components/Alerts/icons/error.svg
new file mode 100755
index 00000000..88b7c186
--- /dev/null
+++ b/web-app/src/components/Alerts/icons/error.svg
@@ -0,0 +1 @@
+error
\ No newline at end of file
diff --git a/web-app/src/components/Alerts/icons/success.svg b/web-app/src/components/Alerts/icons/success.svg
new file mode 100755
index 00000000..1ecc769f
--- /dev/null
+++ b/web-app/src/components/Alerts/icons/success.svg
@@ -0,0 +1 @@
+success
\ No newline at end of file
diff --git a/web-app/src/components/Alerts/index.js b/web-app/src/components/Alerts/index.js
new file mode 100644
index 00000000..9a61c0ad
--- /dev/null
+++ b/web-app/src/components/Alerts/index.js
@@ -0,0 +1,2 @@
+export * from './Errors';
+export * from './Success';
diff --git a/web-app/src/components/App/App.js b/web-app/src/components/App/App.js
index a4abefab..cdaf2e16 100644
--- a/web-app/src/components/App/App.js
+++ b/web-app/src/components/App/App.js
@@ -5,7 +5,9 @@ import { Route, Switch } from 'react-router-dom';
import Bootstrap from '../Bootstrap';
import Header from '../Header';
import Observations from '../Observations';
+import Ranking from '../Ranking';
import Installations from '../Installations';
+import NotFound from '../NotFound';
import './App.css';
@@ -19,12 +21,23 @@ class App extends Component {
return (
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
);
diff --git a/web-app/src/components/App/App.scss b/web-app/src/components/App/App.scss
index 81d89b29..2086e8e7 100644
--- a/web-app/src/components/App/App.scss
+++ b/web-app/src/components/App/App.scss
@@ -1,9 +1,28 @@
+@import "../../theme/_index.scss";
+
.App {
- width: 100%;
- height: 100%;
+ height: 100%;
+ min-height: 100%;
}
.App__Wrapper {
- width: 70%;
- margin: 0 auto;
+ min-height: 100%;
+
+ margin-bottom: -$footerHeight;
+}
+
+.App__Footer {
+ flex-shrink: 0;
+ height: $footerHeight - 1;
+
+ border-top: 1px solid rgb(222, 222, 222);
+}
+
+.App__Push {
+ height: $footerHeight;
}
+
+.App__Content {
+ padding: 30px;
+ padding-bottom: 0px;
+}
\ No newline at end of file
diff --git a/web-app/src/components/Bootstrap/Bootstrap.scss b/web-app/src/components/Bootstrap/Bootstrap.scss
new file mode 100644
index 00000000..46fb364c
--- /dev/null
+++ b/web-app/src/components/Bootstrap/Bootstrap.scss
@@ -0,0 +1,22 @@
+.Bootstrap {
+ height: 100%;
+ width: 100%;
+
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.Bootstrap__Feather {
+ animation: spin 1500ms infinite;
+ animation-timing-function: linear;
+ width: 120px;
+ height: 120px;
+}
+
+@keyframes spin {
+ 100% {
+ -webkit-transform: rotate(360deg);
+ transform: rotate(360deg);
+ }
+}
diff --git a/web-app/src/components/Bootstrap/BootstrapContainer.js b/web-app/src/components/Bootstrap/BootstrapContainer.js
index 89ff0683..9845c159 100644
--- a/web-app/src/components/Bootstrap/BootstrapContainer.js
+++ b/web-app/src/components/Bootstrap/BootstrapContainer.js
@@ -1,22 +1,36 @@
import React, { Component } from 'react';
import api from '../../utils/api';
+import takeAtLeast from '../../utils/takeAtLeast';
+import feather from '../../theme/icons/feather.svg';
+
+import './Bootstrap.css';
class BootstrapContainer extends Component {
componentWillMount() {
- const { loadObservations, loadUser, finishInitialLoading } = this.props;
+ const { loadObservations, loadUser, loadRanking, finishInitialLoading } = this.props;
- Promise.all([
- api.get('/auth/observations').then(({ data: paginationModel }) => loadObservations(paginationModel.data)),
- api.get('/auth/me').then(({ data }) => loadUser(data)),
- ])
+ api
+ .get('/auth/me')
+ .then(({ data }) => loadUser(data))
+ .then(() => {
+ return Promise.all([
+ takeAtLeast(800),
+ api.get('/auth/observations').then(({ data: paginationModel }) => loadObservations(paginationModel.data)),
+ api.get('/leaderboard').then(({ data }) => loadRanking(data)),
+ ]);
+ })
.then(finishInitialLoading);
}
render() {
return (
-
- Loading ...
+
+
);
}
diff --git a/web-app/src/components/Bootstrap/index.js b/web-app/src/components/Bootstrap/index.js
index 95144b09..685c0f50 100644
--- a/web-app/src/components/Bootstrap/index.js
+++ b/web-app/src/components/Bootstrap/index.js
@@ -5,14 +5,16 @@ import mapActionCreatorsToProps from '../../utils/mapActionCreatorsToProps';
import {
loadObservations,
- finishInitialLoading,
+ loadRanking,
loadUser,
+ finishInitialLoading,
} from '../../actions';
const actionCreators = mapActionCreatorsToProps({
loadObservations,
- finishInitialLoading,
+ loadRanking,
loadUser,
+ finishInitialLoading,
});
export default connect(
diff --git a/web-app/src/components/Form/Button.scss b/web-app/src/components/Form/Button.scss
index 59c387fe..6d72f5e0 100644
--- a/web-app/src/components/Form/Button.scss
+++ b/web-app/src/components/Form/Button.scss
@@ -17,6 +17,14 @@
cursor: pointer;
}
+.Form__Button--clean {
+ width: auto;
+ font-size: auto;
+ padding: auto;
+ background-color: initial;
+
+}
+
.Form__Button--circle {
border-radius: 50%;
}
@@ -29,11 +37,32 @@
cursor: not-allowed;
}
+.Form__Button:hover {
+ background-color: $jordyBlue;
+}
+
+.Form__Button:active, .Form__Button:focus {
+ background-color: $celestialBlue;
+}
+
+.Form__Button:disabled {
+ background-color: $lobLolly;
+}
+
.Form__FacebookButton {
background-color: $governorBay;
padding: 0px;
}
+.Form__FacebookButton:hover {
+ background-color: lighten($governorBay, 2);
+}
+
+.Form__FacebookButton:active, .Form__FacebookButton:focus {
+ background-color: darken($governorBay, 10);
+}
+
+
.Form__FacebookButton__Wrapper {
display: flex;
}
@@ -56,4 +85,3 @@
flex-grow: 1;
padding: 12px 0px;
}
-
diff --git a/web-app/src/components/Form/Form.scss b/web-app/src/components/Form/Form.scss
index 7b0801a5..e97aff34 100644
--- a/web-app/src/components/Form/Form.scss
+++ b/web-app/src/components/Form/Form.scss
@@ -11,14 +11,33 @@
width: 100%;
box-sizing: border-box;
- color: $lobLolly;
+ color: $jordyBlue;
font-size: 17px;
border: 1px solid $lobLolly;
border-radius: 3px;
+ padding: $margin*2;
+}
+
+.Form__Input__Icon {
+ background-repeat: no-repeat;
+ background-size: $margin*3 $margin*3;
+ background-position: $margin * 2 center;
- padding: 12px;
+ padding: $margin*2 $margin*7;
+}
+
+.Form__Input__Icon--email {
+ background-image: url(../../theme/icons/email.svg);
+}
+
+.Form__Input__Icon--lock {
+ background-image: url(../../theme/icons/lock.svg);
+}
+
+.Form__Input__Icon--person {
+ background-image: url(../../theme/icons/person.svg);
}
.Form__Input::placeholder {
@@ -27,9 +46,11 @@
.Form__Input:focus, .Form__Dropdown:focus, .Form__Textarea:focus {
outline: none !important;
+ border: 1px solid $jordyBlue;
}
-.Form__Field__Invalid {
- //border: 1.2px solid #F44336 !important;
- //color: #F44336 !important;
+
+.Form__Field__Invalid, .Form__Field__Invalid:focus, .Form__Field__Invalid::placeholder, {
+ border-color: $thunderbird;
+ color: $thunderbird !important;
}
diff --git a/web-app/src/components/Form/Input.js b/web-app/src/components/Form/Input.js
index 7dc8fec9..99d82884 100644
--- a/web-app/src/components/Form/Input.js
+++ b/web-app/src/components/Form/Input.js
@@ -3,11 +3,11 @@ import React from 'react';
import classNames from '../../utils/classNames';
import withInput from './withInput';
-export const Input = withInput(({ text, placeholder, className, rules, ...rest }) => {
+export const Input = withInput(({ text, placeholder, className, rules, icon, ...rest }) => {
return (
diff --git a/web-app/src/components/GuestMode/GuestMode.scss b/web-app/src/components/GuestMode/GuestMode.scss
index 3c862d52..7e2939f1 100644
--- a/web-app/src/components/GuestMode/GuestMode.scss
+++ b/web-app/src/components/GuestMode/GuestMode.scss
@@ -1,24 +1,42 @@
+@import "../../theme/_index.scss";
.GuestMode {
display: flex;
- align-items: center;
justify-content: center;
+ min-height: 100%;
}
.GuestMode__Wrapper {
width: 360px;
- height: 640px;
+ min-height: 100%;
padding: 30px 30px;
box-sizing: border-box;
display: flex;
flex-direction: column;
+
+ justify-content: center;
}
.GuestMode__Logo {
margin: 0 auto;
- width: 150px;
+}
+
+.GuestMode__Label {
+ color: $darkPastelBlue;
+}
+
+.GuestMode__GoBack {
+ color: $darkPastelBlue;
+ font-size: 13px;
+ margin-top: $margin * 2;
+ text-align: center;
+}
- padding: 20px 20px 50px;
+@media (min-width: 300px) {
+ .GuestMode__Logo {
+ width: 80%;
+ padding: 0px 20px 30px;
+ }
}
\ No newline at end of file
diff --git a/web-app/src/components/GuestMode/index.js b/web-app/src/components/GuestMode/index.js
index 7becb343..87de1641 100644
--- a/web-app/src/components/GuestMode/index.js
+++ b/web-app/src/components/GuestMode/index.js
@@ -1,4 +1,5 @@
import React from 'react';
+import { Link } from 'react-router-dom';
import './GuestMode.css';
import logo from '../../theme/crest.svg';
@@ -8,11 +9,24 @@ const GuestMode = ({ children, className }) => {
return (
-
+
+
+
{children}
);
};
+export const GoBack = ({ to, text }) => {
+ return (
+
{text}
+ );
+};
+
+GoBack.defaultProps = {
+ to: '/login',
+ text: 'Go Back',
+};
+
export default GuestMode;
diff --git a/web-app/src/components/Header/Header.js b/web-app/src/components/Header/Header.js
index 51bff60e..d9c1d97e 100644
--- a/web-app/src/components/Header/Header.js
+++ b/web-app/src/components/Header/Header.js
@@ -1,43 +1,65 @@
-/* global window */
import React, { Component } from 'react';
import { NavLink } from 'react-router-dom';
import './Header.css';
import logo from '../../theme/crest.svg';
+import logoNotAuthenticated from '../../theme/icons/crest_menu.svg';
+
import api, { removeToken } from '../../utils/api';
+import redirect from '../../utils/redirect';
+import authenticated from '../../utils/isAuthenticated';
class Header extends Component {
logout() {
- api
- .post('/auth/logout')
- .then(() => {
- removeToken();
- window.location = '/login';
- });
+ api.post('/auth/logout').then(() => {
+ removeToken();
+ redirect('/login');
+ });
}
render() {
- const { isAdmin } = this.props;
+ // const { isAdmin } = this.props;
+ // TODO: show icon for the IoT installations
return (
-
-
-
-
- Home
-
-
this.logout()}>
- Logout
-
- {isAdmin && (
-
-
Installations
+ {authenticated() && (
+
+
+
+
+
+
+ Ranking
+
+
+ Vote
+
+
this.logout()}
+ className="Header__Menu__Icon Header__Menu__SignOut"
+ >
+ Sign Out
- )}
+
-
+ )}
+
+ {!authenticated() && (
+
+
+
+ )}
);
}
diff --git a/web-app/src/components/Header/Header.scss b/web-app/src/components/Header/Header.scss
index 4fbd83bc..5466f365 100644
--- a/web-app/src/components/Header/Header.scss
+++ b/web-app/src/components/Header/Header.scss
@@ -1,31 +1,151 @@
+@import "../../theme/_index.scss";
+
.Header {
- width: 100%;
- border-bottom: 1px solid whitesmoke;
+ width: 100%;
+ border-bottom: 1px solid rgb(222, 222, 222);
+
+ display: flex;
+ justify-content: center;
+ align-items: center;
+
+ z-index: 10;
+ background-color: white;
+ flex-shrink: 0;
+ height: $headerHeight;
+ z-index: 9;
+ position: relative;
}
.Header__Wrapper {
- width: 80%;
- margin: 0 auto;
- display: flex;
- align-items: center;
- height: 100px;
- justify-content: space-between;
+ width: 100%;
+ height: 100%;
+ max-width: 1010px;
+ margin: 0 50px;
+
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+
+ background-color: inherit;
+ z-index: 10;
}
.Header__Logo {
- height: 80px;
+ height: 85%;
+}
+
+.Header__Logo img {
+ height: 100%;
+ width: auto;
+}
+
+.Header__Menu {
+ display: flex;
+ margin: 10px;
+}
+
+/**
+ * Prevent blinking, temp
+ * TODO: make use of image sprites
+**/
+.Header__Menu:after {
+ display: none;
+ content: url('./Icons/leader.svg') url('./Icons/leader_gray.svg')
+ url('./Icons/bird.svg') url('./Icons/bird_gray.svg')
+ url('./Icons/sign_out.svg') url('./Icons/sign_out_gray.svg');
+}
+
+.Header__Menu__Icon:hover,
+.Header__Menu__Icon:active,
+.Header__Menu__Icon:visited,
+.Header__Menu__Icon:focus {
+ text-decoration: none;
+}
+
+.Header__Menu__Icon {
+ height: 55px;
+ margin: 0 20px;
+
+ background-size: 30px 30px;
+ background-repeat: no-repeat;
+ background-position-x: center;
+
+ display: flex;
+ align-items: flex-end;
+
+ font-weight: bold;
+
+ color: rgb(125, 125, 125);
}
-.Header__Login {
- line-height: 100px;
- text-transform: uppercase;
- font-weight: bold;
+.Header__Menu__Leader {
+ background-image: url('./Icons/leader_gray.svg');
}
-.Header__Menu__Right {
- display: flex;
+.Header__Menu__Bird {
+ background-image: url('./Icons/bird_gray.svg');
}
-.Header__Menu__Right__Item {
- margin: 0px 10px;
-}
\ No newline at end of file
+.Header__Menu__SignOut {
+ cursor: pointer;
+ background-image: url('./Icons/sign_out_gray.svg');
+}
+
+.Header__Menu__Leader:hover,
+.Header__Menu__Leader:focus,
+.Header__Menu__Leader--active {
+ background-image: url('./Icons/leader.svg');
+ color: rgb(234, 181, 50);
+}
+
+.Header__Menu__Bird:hover,
+.Header__Menu__Bird:focus,
+.Header__Menu__Bird--active {
+ background-image: url('./Icons/bird.svg');
+ color: rgb(214, 102, 55);
+}
+
+.Header__Menu__SignOut:hover,
+.Header__Menu__SignOut:focus,
+.Header__Menu__SignOut--active {
+ background-image: url('./Icons/sign_out.svg');
+ color: $darkPastelBlue;
+}
+
+@include onTablet() {
+ .Header__Logo {
+ height: 60%;
+ }
+}
+
+@include onMobile() {
+ .Header__Logo {
+ display: none;
+ }
+
+ .Header__Menu {
+ width: 100%;
+ justify-content: space-between;
+ }
+
+ .Header__Menu__Icon {
+ margin: 0 5px;
+ }
+}
+
+.Header__LogoNotAuthenticated {
+ height: 100%;
+ width: 100%;
+
+ background-color: white;
+
+ display: flex;
+ align-items: center;
+ justify-content: center;
+
+ z-index: 10;
+
+ img {
+ height: 60%;
+ }
+}
diff --git a/web-app/src/components/Header/Icons/bird.svg b/web-app/src/components/Header/Icons/bird.svg
new file mode 100755
index 00000000..304eddf5
--- /dev/null
+++ b/web-app/src/components/Header/Icons/bird.svg
@@ -0,0 +1 @@
+
bird
\ No newline at end of file
diff --git a/web-app/src/components/Header/Icons/bird_gray.svg b/web-app/src/components/Header/Icons/bird_gray.svg
new file mode 100755
index 00000000..902b9945
--- /dev/null
+++ b/web-app/src/components/Header/Icons/bird_gray.svg
@@ -0,0 +1 @@
+
bird_gray
\ No newline at end of file
diff --git a/web-app/src/components/Header/Icons/leader.svg b/web-app/src/components/Header/Icons/leader.svg
new file mode 100755
index 00000000..fe5b641b
--- /dev/null
+++ b/web-app/src/components/Header/Icons/leader.svg
@@ -0,0 +1 @@
+
leader
\ No newline at end of file
diff --git a/web-app/src/components/Header/Icons/leader_gray.svg b/web-app/src/components/Header/Icons/leader_gray.svg
new file mode 100755
index 00000000..2a5ff5eb
--- /dev/null
+++ b/web-app/src/components/Header/Icons/leader_gray.svg
@@ -0,0 +1 @@
+
leader_gray
\ No newline at end of file
diff --git a/web-app/src/components/Header/Icons/sign_out.svg b/web-app/src/components/Header/Icons/sign_out.svg
new file mode 100755
index 00000000..6a4c2410
--- /dev/null
+++ b/web-app/src/components/Header/Icons/sign_out.svg
@@ -0,0 +1 @@
+
sign_out
\ No newline at end of file
diff --git a/web-app/src/components/Header/Icons/sign_out_gray.svg b/web-app/src/components/Header/Icons/sign_out_gray.svg
new file mode 100755
index 00000000..1b8e3309
--- /dev/null
+++ b/web-app/src/components/Header/Icons/sign_out_gray.svg
@@ -0,0 +1 @@
+
sign-out_gray
\ No newline at end of file
diff --git a/web-app/src/components/Header/index.js b/web-app/src/components/Header/index.js
index 661ee782..efa8079d 100644
--- a/web-app/src/components/Header/index.js
+++ b/web-app/src/components/Header/index.js
@@ -1,4 +1,5 @@
import { connect } from 'react-redux';
+import { withRouter } from 'react-router-dom';
import Header from './Header';
import { isAdmin } from '../../selectors/application';
@@ -7,7 +8,7 @@ const mapStateToProps = (state) => ({
isAdmin: isAdmin(state),
});
-export default connect(
+export default withRouter(connect(
mapStateToProps,
undefined,
-)(Header);
+)(Header));
diff --git a/web-app/src/components/Login/Login.scss b/web-app/src/components/Login/Login.scss
index 55613e82..c65cd650 100644
--- a/web-app/src/components/Login/Login.scss
+++ b/web-app/src/components/Login/Login.scss
@@ -1,34 +1,10 @@
@import "../../theme/_index.scss";
-.Login {
- display: flex;
- align-items: center;
- justify-content: center;
-}
-
-.Login__Wrapper {
- width: 360px;
- height: 640px;
-
- padding: 30px 30px;
- box-sizing: border-box;
-
- display: flex;
- flex-direction: column;
-}
-
-.Login__Logo {
- margin: 0 auto;
- width: 150px;
-
- padding: 20px 20px 50px;
-}
-
.Login__Password {
margin-top: 16px;
}
-.Login__ForgotPassword, .Login__SignUp {
+.Login__ForgotPassword {
margin-top: 4px;
font-size: 11px;
@@ -36,10 +12,19 @@
}
.Login__SignUp {
+ margin-top: 4px;
+
+ font-size: 15px;
+ color: $lobLolly;
+
width: 100%;
text-align: center;
}
.Login__LoginButton {
margin: 20px 0px;
+}
+
+.Login__GoBack {
+ margin-top: $margin * 3;
}
\ No newline at end of file
diff --git a/web-app/src/components/Login/LoginCallback.js b/web-app/src/components/Login/LoginCallback.js
index 4768163a..074a4ca1 100644
--- a/web-app/src/components/Login/LoginCallback.js
+++ b/web-app/src/components/Login/LoginCallback.js
@@ -1,5 +1,4 @@
-import React from 'react';
-import { Redirect } from 'react-router-dom';
+import redirect from '../../utils/redirect';
/* global window */
const setToken = (token) => {
@@ -9,9 +8,9 @@ const setToken = (token) => {
const LoginCallback = ({ match }) => {
setToken(match.params.token);
- window.location = '/';
+ redirect('/');
- return
;
+ return null;
};
export default LoginCallback;
diff --git a/web-app/src/components/Login/index.js b/web-app/src/components/Login/index.js
index 565796f6..5f538e50 100644
--- a/web-app/src/components/Login/index.js
+++ b/web-app/src/components/Login/index.js
@@ -4,11 +4,14 @@ import { Link } from 'react-router-dom';
import Title from '../Title';
import Divider from '../Divider';
+import GuestMode, { GoBack } from '../GuestMode';
+
+import { Errors } from '../Alerts';
import { Form, Input, Button, FacebookButton } from '../Form';
import api, { BASE_URL } from '../../utils/api';
+import redirect from '../../utils/redirect';
-import logo from '../../theme/crest.svg';
import './Login.css';
class Login extends Component {
@@ -17,14 +20,24 @@ class Login extends Component {
this.state = {
isValid: false,
+ errors: undefined,
};
}
+ onValidationChange(valid) {
+ this.setState({
+ errors: undefined,
+ isValid: valid,
+ });
+ }
+
login(body) {
api.post('/auth', body).then(({ data }) => {
- // TODO: make an easier jwt token manager
window.localStorage.setItem('jwt.token', data.token);
- window.location = '/';
+ redirect('/');
+ })
+ .catch(() => {
+ this.setState({ errors: ['Your credentials are incorrect.'] });
});
}
@@ -35,44 +48,46 @@ class Login extends Component {
}
render() {
- const { isValid } = this.state;
+ const { isValid, errors } = this.state;
return (
-
+
-
-
-
+
-
+
);
}
}
diff --git a/web-app/src/components/NotFound/NotFound.scss b/web-app/src/components/NotFound/NotFound.scss
new file mode 100644
index 00000000..6d458fca
--- /dev/null
+++ b/web-app/src/components/NotFound/NotFound.scss
@@ -0,0 +1,70 @@
+@import "../../theme/_index.scss";
+
+.NotFound {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.NotFound__Wrapper {
+ padding: 50px 30px;
+ box-sizing: border-box;
+
+ display: flex;
+ flex-direction: column;
+
+ height: 100%;
+}
+
+.NotFound__Text {
+ text-align: center;
+ font-weight: 900;
+ font-size: 22px;
+ color: $mountainMist;
+ margin: 20px 0px;
+}
+
+.NotFound__SubText {
+ text-align: center;
+ font-weight: 900;
+ font-size: 20px;
+ color: $mountainMist;
+ margin: 20px 0px;
+}
+
+.NotFound__Logo {
+ position: relative;
+ margin: 0 auto;
+ width: 500px;
+ height: 360px;
+ padding: 30px 100px;
+}
+
+.NotFound_Logo_Background {
+ position: absolute;
+}
+
+.NotFound_Logo_Bird {
+ position: absolute;
+
+ top: 0; left: 0; bottom: 0; right: 0;
+ margin: auto;
+
+ z-index: 2;
+
+ transition: top 1.5s;
+
+ background-image: url('../../theme/icons/crest_bird.svg');
+ background-repeat: no-repeat;
+ background-size: 170px 170px;
+ background-position: center center;
+}
+
+.NotFound_Logo_Bird--hide {
+ top: -900px;
+}
+
+.NotFound_Logo_Bird--wings {
+ background-image: url('../../theme/icons/crest_bird_flying.svg');
+ background-size: 500px 500px;
+}
diff --git a/web-app/src/components/NotFound/index.js b/web-app/src/components/NotFound/index.js
new file mode 100644
index 00000000..95de7d15
--- /dev/null
+++ b/web-app/src/components/NotFound/index.js
@@ -0,0 +1,52 @@
+import React, { Component } from 'react';
+
+import classNames from '../../utils/classNames';
+
+import crestBackground from '../../theme/icons/crest_red_empty.svg';
+
+import './NotFound.css';
+
+class NotFound extends Component {
+ constructor(...props) {
+ super(...props);
+
+ this.state = {
+ wings: false,
+ fly: false,
+ };
+ }
+
+ componentDidMount() {
+ setInterval(() => {
+ this.setState(({ wings }) => ({ wings: !wings }));
+ }, 400);
+
+ setInterval(() => {
+ this.setState(({ fly }) => ({ fly: !fly }));
+ }, 2000);
+ }
+
+ render() {
+ const { fly, wings } = this.state;
+
+ return (
+
+
+
+ 404 not found!
+
+
+
+
+
+
+ Woops!
+ Looks like the bird has flown.
+
+
+
+ );
+ }
+}
+
+export default NotFound;
diff --git a/web-app/src/components/Observations/Icons/book.svg b/web-app/src/components/Observations/Icons/book.svg
new file mode 100755
index 00000000..5327beb4
--- /dev/null
+++ b/web-app/src/components/Observations/Icons/book.svg
@@ -0,0 +1 @@
+
book
\ No newline at end of file
diff --git a/web-app/src/components/Observations/Icons/trash.svg b/web-app/src/components/Observations/Icons/trash.svg
new file mode 100755
index 00000000..0ca16b56
--- /dev/null
+++ b/web-app/src/components/Observations/Icons/trash.svg
@@ -0,0 +1 @@
+
red_trash
\ No newline at end of file
diff --git a/web-app/src/components/Observations/Observations.js b/web-app/src/components/Observations/Observations.js
index c232df07..2b34ad1b 100644
--- a/web-app/src/components/Observations/Observations.js
+++ b/web-app/src/components/Observations/Observations.js
@@ -1,64 +1,136 @@
import React, { Component } from 'react';
import _ from 'lodash';
+import Swing from 'react-swing';
import Title from '../Title';
-import { Button } from '../Form';
-import Icon from '../Icon';
-
-import api from '../../utils/api';
+import Polaroid from '../Polaroid';
import './Observations.css';
+import polaroid from '../../theme/icons/polaroid.svg';
+import trash from '../../theme/icons/trash.svg';
+import book from '../../theme/icons/book.svg';
+import feather from '../../theme/icons/feather.svg';
+import classNames from '../../utils/classNames';
class Observations extends Component {
-
- vote(value) {
- const observation = _.head(this.props.observations);
- const newObservations = [..._.drop(this.props.observations)];
- api.post('/votes', {
- body: {
- observation_id: observation.id,
- value,
+ constructor(props) {
+ super(props);
+ this.state = {
+ stack: null,
+ animation: true,
+ growTrash: false,
+ growBook: false,
+ config: {
+ throwOutConfidence: (xOffset, yOffset, element) => {
+ const xConfidence = Math.min((4 * Math.abs(xOffset)) / element.offsetWidth, 1);
+ const yConfidence = Math.min((Math.abs(yOffset)) / (element.offsetHeight * 10), 1);
+ if (xConfidence === 1) {
+ if (xOffset > 0) {
+ this.toggleBook();
+ } else {
+ this.toggleTrash();
+ }
+ } else {
+ this.setState({
+ growBook: false,
+ growTrash: false,
+ });
+ }
+ return Math.max(xConfidence, yConfidence);
+ },
},
- }).then(() => {
- this.props.loadObservations(newObservations);
+ };
+ }
- if (newObservations.length < 6) {
- this.fetch();
- }
+ vote(value) {
+ this.toggleAnimation(this.state.animation);
+ this.props.vote(value);
+ this.setState({
+ growBook: false,
+ growTrash: false,
});
+ setTimeout(() => this.toggleAnimation(), 50);
}
- fetch() {
- api.get('/auth/observations').then(({ data: paginationModel }) => {
- this.props.loadObservations(paginationModel.data);
- });
+ toggleAnimation() {
+ this.setState(({ animation }) => ({ animation: !animation }));
+ }
+
+ toggleTrash() {
+ this.setState({ growTrash: true });
+ }
+
+ toggleBook() {
+ this.setState({ growBook: true });
}
render() {
- const observation = _.head(this.props.observations);
+ const { observations, generateImageUrl, isDemo } = this.props;
+ if (observations.length <= 0) {
+ return (
+
+
+
+
+
+
All the birds have flown for today!
+
Thank you so much for your contribution.
+
+ Follow @TodayBirds on twitter or like the
+ page Birds Today on Facebook to
+ keep track of all the pictures Bert takes.
+
+
+
+ );
+ }
+
+ const observation = _.head(observations);
return (
-
- {observation === undefined && (
-
No observations left
- )}
- {observation && (
-
-
-
-
this.vote(1)}>
-
this.vote(0)} >SKIP
-
this.vote(-1)}>
+
+
+
+ {isDemo && (
+
+ {observation.demoText}
+ )}
+
this.setState({ stack })}
+ throwoutleft={(e) => {
+ this.vote(-1);
+ this.state.stack.getCard(e.target).throwIn(0, 0);
+ }}
+ throwoutright={(e) => {
+ this.vote(1);
+ this.state.stack.getCard(e.target).throwIn(0, 0);
+ }}
+ >
+
+
+
+
+
this.vote(-1)}>
+
+
+
this.vote(1)}>
+
- )}
+
);
}
}
+
+Observations.defaultProps = {
+ isDemo: false,
+};
+
export default Observations;
diff --git a/web-app/src/components/Observations/Observations.scss b/web-app/src/components/Observations/Observations.scss
index 661ae6f5..3a3cb13e 100644
--- a/web-app/src/components/Observations/Observations.scss
+++ b/web-app/src/components/Observations/Observations.scss
@@ -1,19 +1,141 @@
+@import "../../theme/_index.scss";
+
.Observations {
- margin-top: 50px;
+ min-height: calc(100vh - #{$headerHeight} - #{$footerHeight} - 60px - 2px);
+ width: 100%;
+
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+}
+
+.Observations__Top {
+ height: 100%;
+ flex: 1 0 auto;
+ width: 100%;
+}
+
+.Observations__PolaroidIcon {
+ display: block;
+ width: 100%;
+ max-width: 225px;
+
+ margin: 0 auto;
+ margin-bottom: 30px;
}
-.Observations__Picture {
- margin: 0 auto;
- display: block;
- width: 100%
+.Observations__Swing {
+ padding: 5px;
+ z-index: 5px;
}
-.Observations__Buttons {
- margin-top: 50px;
- display: flex;
- justify-content: space-between;
+.Observations__Footer {
+ width: 100%;
+ max-width: 500px;
+
+ margin: $margin * 2 0px;
+ flex-shrink: 0;
+
+ display: flex;
+ justify-content: space-between;
}
-.Observations__Buttons .Form__Button {
- margin: 0px 50px;
-}
\ No newline at end of file
+.Observations__Button {
+ height: 95px;
+ width: 95px;
+ z-index: 7;
+ background-color: #ffffff;
+ border-radius: 50%;
+ //box-shadow: 0 0 20px rgba(#000000, 0.3);
+
+ border: 1px solid whitesmoke;
+ box-shadow: 0 0 1px rgba(#000000, 0.2);
+
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ transition: all 0.3s;
+
+ cursor: pointer;
+}
+
+.Observations__Button__Grow{
+ animation: growButton 0.5s linear forwards;
+}
+
+@keyframes growButton {
+ 0% {
+ transform: scale(1.0); }
+ 100% {
+ transform: scale(1.5); }
+}
+
+.Observations__Button:hover {
+ box-shadow: 0 0 20px rgba(#000000, 0.2);
+}
+
+.Observations__Button img {
+ height: 60%;
+}
+
+@include onMobile() {
+ .Polaroid {
+ padding: 10px;
+ padding-bottom: 50px;
+ border-radius: 2px;
+ }
+}
+
+@include max-height(900px) {
+ .Observations {
+ margin-top: -30px;
+ }
+
+ .Observations__PolaroidIcon {
+ margin-top: -111px;
+ overflow: hidden;
+ z-index: -99;
+ }
+}
+
+.Observations__DemoText {
+ font-size: 17px;
+ max-width: 220px;
+ margin: 0 auto;
+ text-align: center;
+ margin-bottom: 30px;
+}
+
+.Observations__Empty {
+ height: 100%;
+}
+
+.Feather {
+ display: inline-block;
+ width: 10rem;
+ height: 10rem;
+
+ position: absolute;
+ top: -195px;
+ left: 50px;
+
+ z-index: 2;
+
+ animation: featherAnimation 7s infinite linear;
+ animation-delay: 1.3s;
+}
+
+@keyframes featherAnimation {
+ 0% {
+ opacity: 1;
+ transform: translate(0, 0px) rotateZ(0deg);
+ }
+ 75% {
+ opacity: 1;
+ transform: translate(100px, 600px) rotateZ(270deg);
+ }
+ 100% {
+ opacity: 0;
+ transform: translate(150px, 800px) rotateZ(360deg);
+ }
+}
diff --git a/web-app/src/components/Observations/ObservationsContainer.js b/web-app/src/components/Observations/ObservationsContainer.js
new file mode 100644
index 00000000..90c64f24
--- /dev/null
+++ b/web-app/src/components/Observations/ObservationsContainer.js
@@ -0,0 +1,46 @@
+import React, { Component } from 'react';
+import _ from 'lodash';
+
+import Observations from './Observations';
+import api from '../../utils/api';
+
+const generateImageUrl = (observationId) => {
+ return `${process.env.REACT_APP_API_URL}/observations/${observationId}/picture`;
+};
+
+class ObservationsContainer extends Component {
+ componentWillMount() {
+ this.fetch();
+ }
+
+ vote(value) {
+ const observation = _.head(this.props.observations);
+ const newObservations = [..._.drop(this.props.observations)];
+
+ const body = {
+ observation_id: observation.id,
+ value,
+ };
+
+ api.post('/votes', { body }).then(() => {
+ this.props.loadObservations(newObservations);
+
+ if (newObservations.length < 6) {
+ this.fetch();
+ }
+ });
+ }
+
+ fetch() {
+ api.get('/auth/observations').then(({ data: paginationModel }) => this.props.loadObservations(paginationModel.data));
+ }
+
+ render() {
+ return (
this.vote(value)}
+ generateImageUrl={generateImageUrl}
+ />);
+ }
+}
+export default ObservationsContainer;
diff --git a/web-app/src/components/Observations/index.js b/web-app/src/components/Observations/index.js
index 7ba8e015..84ca61e0 100644
--- a/web-app/src/components/Observations/index.js
+++ b/web-app/src/components/Observations/index.js
@@ -1,6 +1,6 @@
import { connect } from 'react-redux';
-import Observations from './Observations';
+import ObservationsContainer from './ObservationsContainer';
import mapActionCreatorsToProps from '../../utils/mapActionCreatorsToProps';
import { loadObservations } from '../../actions';
@@ -18,4 +18,4 @@ const actionCreators = mapActionCreatorsToProps({
export default connect(
mapStateToProps,
actionCreators,
-)(Observations);
+)(ObservationsContainer);
diff --git a/web-app/src/components/OnBoard/Fase1/OnBoard.scss b/web-app/src/components/OnBoard/Fase1/OnBoard.scss
new file mode 100644
index 00000000..2c4c6eb5
--- /dev/null
+++ b/web-app/src/components/OnBoard/Fase1/OnBoard.scss
@@ -0,0 +1,146 @@
+@import "../../../theme/_index.scss";
+
+@mixin centerHorizontal() {
+ position: absolute;
+ left: -50%;
+ right: -50%;
+
+ margin: 0 auto;
+}
+
+.OnBoard {
+ width: 100%;
+ height: 100%;
+
+ background-color: whitesmoke;
+ overflow: auto;
+ position: relative;
+
+ display: flex;
+ flex-direction: column;
+}
+
+.OnBoard__Wrapper {
+ position: relative;
+ width: 100%;
+ //height: 100%;
+
+ display: flex;
+ justify-content: center;
+ align-items: center;
+}
+
+.OnBoard__Content {
+ //height: 100%;
+ width: 100%;
+
+ height: 490px;
+ max-width: $xs;
+
+ position: relative;
+
+ overflow: hidden;
+}
+
+.Carrousel {
+ width: 100%;
+ height: 100%;
+}
+
+.Carrousel__Item {
+ width: 100% !important;
+ height: 100% !important;
+
+ overflow: hidden;
+
+ pointer-events: none;
+
+ .Carrousel__Item__Text {
+ max-width: 250px;
+
+ position: absolute;
+
+ p {
+ font-size: 20px;
+ margin-bottom: 25px;
+ }
+ }
+}
+
+.OnBoard__Bert {
+ @include centerHorizontal();
+ width: 250px;
+ bottom: 0;
+
+ transition: bottom 1.5s linear;
+}
+
+.OnBoard__Verkijker {
+ @include centerHorizontal();
+ width: 190px;
+ bottom: 75px;
+ left: calc(-50% - 35px);
+}
+
+.OnBoard__Polaroid {
+ @include centerHorizontal();
+ width: 150px;
+ height: auto;
+ bottom: 75px;
+ left: calc(-50% - 15px);
+
+ transition: bottom 1.5s linear;
+}
+
+.Carrousel__Dias {
+ .Polaroid {
+ padding: 10px;
+ padding-bottom: 25px;
+ border-radius: 0px;
+ }
+}
+
+.Carrousel__Dias__1 {
+ width: 55%;
+ height: 150px;
+
+ position: absolute;
+ bottom: 240px;
+ left: 15px;
+ transform: rotate(-10deg);
+}
+
+.Carrousel__Dias__2 {
+ width: 55%;
+ height: 150px;
+
+ position: absolute;
+ bottom: 240px;
+ right: 10px;
+ transform: rotate(6deg);
+}
+
+.Carrousel__Dias__3 {
+ @include centerHorizontal();
+
+ width: 70%;
+ height: 150px;
+
+ bottom: 145px;
+
+ position: absolute;
+}
+
+.Carrousel__Item__Slide4 {
+ display: flex;
+ align-items: center;
+}
+
+.Carrousel__Item__Action {
+ height: 70px;
+ margin: 30px;
+}
+
+.Observations__Empty {
+ overflow: hidden;
+}
diff --git a/web-app/src/components/OnBoard/Fase1/Slides/Slide1.js b/web-app/src/components/OnBoard/Fase1/Slides/Slide1.js
new file mode 100644
index 00000000..639f89bb
--- /dev/null
+++ b/web-app/src/components/OnBoard/Fase1/Slides/Slide1.js
@@ -0,0 +1,17 @@
+import React from 'react';
+import { Slide } from '../components/Slider';
+
+import verkijkerIcon from '../../../../theme/icons/verkijker.svg';
+
+export const Slide1 = () => (
+
+
+
Hi, my name is Bert.
+
I'm a birdspotter and I'm looking for the common tern.
+
Would you like to help me?
+
+
+
+);
+
+export default Slide1;
diff --git a/web-app/src/components/OnBoard/Fase1/Slides/Slide2.js b/web-app/src/components/OnBoard/Fase1/Slides/Slide2.js
new file mode 100644
index 00000000..e0a82b75
--- /dev/null
+++ b/web-app/src/components/OnBoard/Fase1/Slides/Slide2.js
@@ -0,0 +1,29 @@
+import React from 'react';
+
+import Polaroid from '../../../Polaroid';
+import { Slide } from '../components/Slider';
+
+import commonTern1 from '../pictures/common_tern_1.jpeg';
+import commonTern2 from '../pictures/common_tern_2.jpeg';
+import commonTern3 from '../pictures/common_tern_3.jpeg';
+
+const Slide2 = () => (
+
+
+
These are some pictures of the common tern so you know what we are looking for!
+
+
+
+);
+
+export default Slide2;
diff --git a/web-app/src/components/OnBoard/Fase1/Slides/Slide3.js b/web-app/src/components/OnBoard/Fase1/Slides/Slide3.js
new file mode 100644
index 00000000..ab9f8a22
--- /dev/null
+++ b/web-app/src/components/OnBoard/Fase1/Slides/Slide3.js
@@ -0,0 +1,17 @@
+import React from 'react';
+
+import { Slide } from '../components/Slider';
+
+import polaroidIcon from '../../../../theme/icons/polaroid_original.svg';
+
+const Slide3 = ({ showFixedPolaroid }) => (
+
+
+
I'm at the Houtdok in Ghent right now.
+
Let me show you how you can help me sort the pictures I take!
+
+ {!showFixedPolaroid && ( )}
+
+);
+
+export default Slide3;
diff --git a/web-app/src/components/OnBoard/Fase1/Slides/Slide4.js b/web-app/src/components/OnBoard/Fase1/Slides/Slide4.js
new file mode 100644
index 00000000..92dd2b98
--- /dev/null
+++ b/web-app/src/components/OnBoard/Fase1/Slides/Slide4.js
@@ -0,0 +1,9 @@
+import React from 'react';
+
+import { Slide } from '../components/Slider';
+
+const Slide4 = () => (
+
+);
+
+export default Slide4;
diff --git a/web-app/src/components/OnBoard/Fase1/components/Slider.js b/web-app/src/components/OnBoard/Fase1/components/Slider.js
new file mode 100644
index 00000000..d5d5aa23
--- /dev/null
+++ b/web-app/src/components/OnBoard/Fase1/components/Slider.js
@@ -0,0 +1,85 @@
+import React, { Component } from 'react';
+import Flickity from 'flickity';
+
+import 'flickity/dist/flickity.min.css';
+import './Slider.css';
+
+class Slider extends Component {
+
+ constructor(...props) {
+ super(...props);
+
+ this.state = {
+ selectedIndex: 0,
+ };
+ }
+
+ componentDidMount() {
+ this.flickity = new Flickity(this.carousel, this.props.options);
+
+ this.flickity.on('cellSelect', () => this.updateSelected());
+
+ this.scrollListener = (event, progress) => {
+ const percent = progress / (this.carousel.clientWidth * this.props.children.length);
+
+ this.progressChanged(parseFloat(percent.toFixed(4)));
+ };
+
+ this.flickity.on('scroll', this.scrollListener);
+ }
+
+ componentWillUnmount() {
+ this.flickity.off('cellSelect', () => this.updateSelected());
+ this.flickity.off('scroll', this.scrollListener);
+ this.flickity.destroy();
+ }
+
+ updateSelected() {
+ const index = this.flickity.selectedIndex;
+ this.setState({ selectedIndex: index });
+
+ this.props.currentIndex(index);
+ }
+
+ progressChanged(procent) {
+ const amountChildren = this.props.children.length;
+ const currentIndexProcent = (this.state.selectedIndex * (100 / amountChildren)) / 100;
+
+ this.props.process({
+ total: procent,
+ previous: parseFloat(((procent - currentIndexProcent) * amountChildren).toFixed(4)),
+ });
+ }
+
+ render() {
+ const { children, className, getRef } = this.props;
+
+ return (
+ { this.carousel = carousel; getRef(carousel); }}
+ className={className}
+ >
+ {children}
+
+ );
+ }
+}
+
+Slider.defaultProps = {
+ options: {
+ lazyLoad: false,
+ prevNextButtons: false,
+ pageDots: true,
+ dragThreshold: 10,
+ },
+ process: () => {},
+ currentIndex: () => {},
+};
+
+export const Slide = ({ children, className }) => (
+
+ {children}
+
+);
+
+export default Slider;
diff --git a/web-app/src/components/OnBoard/Fase1/components/Slider.scss b/web-app/src/components/OnBoard/Fase1/components/Slider.scss
new file mode 100644
index 00000000..b642702d
--- /dev/null
+++ b/web-app/src/components/OnBoard/Fase1/components/Slider.scss
@@ -0,0 +1,21 @@
+@import "../../../../theme/_index.scss";
+
+.flickity-page-dots {
+ bottom: 10px;
+}
+
+.flickity-page-dots .dot {
+ width: 14px;
+ height: 14px;
+ opacity: initial;
+ background-color: rgb(182, 182, 182); // TODO: ask Cynthia for the exact color
+}
+
+.flickity-page-dots > .is-selected {
+ background-color: $thunderbird !important;
+}
+
+.flickity-viewport {
+ height: 100% !important;
+ min-height: 100%;
+}
\ No newline at end of file
diff --git a/web-app/src/components/OnBoard/Fase1/pictures/common_tern_1.jpeg b/web-app/src/components/OnBoard/Fase1/pictures/common_tern_1.jpeg
new file mode 100644
index 00000000..fb7ef891
Binary files /dev/null and b/web-app/src/components/OnBoard/Fase1/pictures/common_tern_1.jpeg differ
diff --git a/web-app/src/components/OnBoard/Fase1/pictures/common_tern_2.jpeg b/web-app/src/components/OnBoard/Fase1/pictures/common_tern_2.jpeg
new file mode 100644
index 00000000..5a9574c8
Binary files /dev/null and b/web-app/src/components/OnBoard/Fase1/pictures/common_tern_2.jpeg differ
diff --git a/web-app/src/components/OnBoard/Fase1/pictures/common_tern_3.jpeg b/web-app/src/components/OnBoard/Fase1/pictures/common_tern_3.jpeg
new file mode 100644
index 00000000..c9c5f089
Binary files /dev/null and b/web-app/src/components/OnBoard/Fase1/pictures/common_tern_3.jpeg differ
diff --git a/web-app/src/components/OnBoard/Fase2/ObservationsContainer.js b/web-app/src/components/OnBoard/Fase2/ObservationsContainer.js
new file mode 100644
index 00000000..3b6c09f9
--- /dev/null
+++ b/web-app/src/components/OnBoard/Fase2/ObservationsContainer.js
@@ -0,0 +1,54 @@
+import React, { Component } from 'react';
+import _ from 'lodash';
+import { Redirect } from 'react-router-dom';
+import Observations from '../../Observations/Observations';
+
+import picture1 from './pictures/fase2_picture1.jpg';
+import picture2 from './pictures/fase2_picture2.jpg';
+
+const OBSERVATIONS = [
+ {
+ id: 1,
+ picture: picture1,
+ demoText: 'This is a common tern! Drag this to my photobook!',
+ },
+ {
+ id: 2,
+ picture: picture2,
+ demoText: 'This is NOT a common tern! Drag this to the trash!',
+ },
+];
+
+const generateImageUrl = (observationId) => {
+ return OBSERVATIONS.find((observation) => observation.id === observationId).picture;
+};
+
+class ObservationsContainer extends Component {
+ constructor(...props) {
+ super(...props);
+
+ this.state = {
+ observations: OBSERVATIONS,
+ };
+ }
+
+ vote() {
+ this.setState((prevState) => ({ observations: [..._.drop(prevState.observations)] }));
+ }
+
+ render() {
+ const { observations } = this.state;
+
+ if (observations.length === 0) {
+ return ;
+ }
+
+ return ( this.vote(value)}
+ generateImageUrl={generateImageUrl}
+ />);
+ }
+}
+export default ObservationsContainer;
diff --git a/web-app/src/components/OnBoard/Fase2/index.js b/web-app/src/components/OnBoard/Fase2/index.js
new file mode 100644
index 00000000..3ef206c0
--- /dev/null
+++ b/web-app/src/components/OnBoard/Fase2/index.js
@@ -0,0 +1,3 @@
+import ObservationsController from './ObservationsContainer';
+
+export default ObservationsController;
diff --git a/web-app/src/components/OnBoard/Fase2/pictures/fase2_picture1.jpg b/web-app/src/components/OnBoard/Fase2/pictures/fase2_picture1.jpg
new file mode 100755
index 00000000..253ff85c
Binary files /dev/null and b/web-app/src/components/OnBoard/Fase2/pictures/fase2_picture1.jpg differ
diff --git a/web-app/src/components/OnBoard/Fase2/pictures/fase2_picture2.jpg b/web-app/src/components/OnBoard/Fase2/pictures/fase2_picture2.jpg
new file mode 100755
index 00000000..3a72b3ad
Binary files /dev/null and b/web-app/src/components/OnBoard/Fase2/pictures/fase2_picture2.jpg differ
diff --git a/web-app/src/components/OnBoard/index.js b/web-app/src/components/OnBoard/index.js
new file mode 100644
index 00000000..3848d4bb
--- /dev/null
+++ b/web-app/src/components/OnBoard/index.js
@@ -0,0 +1,150 @@
+/* global */
+import React, { Component } from 'react';
+import Slider from './Fase1/components/Slider';
+
+import Slide1 from './Fase1/Slides/Slide1';
+import Slide2 from './Fase1/Slides/Slide2';
+import Slide3 from './Fase1/Slides/Slide3';
+import Slide4 from './Fase1/Slides/Slide4';
+
+import Header from '../Header';
+import './Fase1/OnBoard.css';
+
+import bertIcon from '../../theme/icons/little_bert.svg';
+import polaroidIcon from '../../theme/icons/polaroid_original.svg';
+
+import Fase2 from './Fase2';
+
+const BERT_POSITION_BOTTOM = 0;
+const POLAROID_POSITION_BOTTOM = 75;
+class OnBoard extends Component {
+ constructor(...props) {
+ super(...props);
+
+ this.state = {
+ index: 0,
+ progress: {},
+ fase2: false,
+ };
+ }
+
+ process(progress) {
+ this.setState({ progress });
+ }
+
+ currentIndex(index) {
+ this.setState({ index });
+ if (index === 3) {
+ setTimeout(() => this.showFase2(), 1500);
+ }
+ }
+
+ moveBottom(positionFrom, component) {
+ if (!this.slider || !component) {
+ return undefined;
+ }
+
+ if (this.state.progress.total >= 0.65) {
+ return {
+ bottom: positionFrom - component.clientHeight,
+ };
+ }
+
+ if (this.state.progress.total < 0.5 || this.state.progress.previous < 0) {
+ return undefined;
+ }
+
+ return {
+ bottom: positionFrom - ((component.clientHeight * this.state.progress.previous)),
+ };
+ }
+
+ moveTop(positionFrom, component) {
+ if (!this.slider || !component) {
+ return undefined;
+ }
+
+ if (this.state.progress.total >= 0.65) {
+ return {
+ bottom: this.slider.clientHeight,
+ };
+ }
+
+ if (this.state.progress.total < 0.5 || this.state.progress.previous < 0) {
+ return undefined;
+ }
+
+ return {
+ bottom:
+ positionFrom
+ + (((this.slider.clientHeight - component.clientHeight)
+ * this.state.progress.previous)),
+ };
+ }
+
+ moveBert() {
+ return this.moveBottom(BERT_POSITION_BOTTOM, this.fixedBert);
+ }
+
+ movePolaroid() {
+ return this.moveTop(POLAROID_POSITION_BOTTOM, this.fixedPolaroid);
+ }
+
+ showFase2() {
+ this.setState({ fase2: true });
+ }
+
+ render() {
+ const showFixedPolaroid = this.state.progress.total >= 0.5;
+
+ return (
+
+
+
+
+
+
+ {this.state.fase2 && (
+
+ )}
+ {!this.state.fase2 && (
+
+
+ {!false && (
this.fixedBert = ref}
+ style={this.moveBert()}
+ />)}
+
+ {showFixedPolaroid && (
this.fixedPolaroid = ref}
+ style={this.movePolaroid()}
+ />)}
+
+
this.process(percent)}
+ currentIndex={(index) => this.currentIndex(index)}
+ getRef={(ref) => this.slider = ref}
+ >
+
+
+
+
+
+
+
+ )}
+
+
+
+ );
+ }
+}
+
+export default OnBoard;
diff --git a/web-app/src/components/Polaroid/Polaroid.scss b/web-app/src/components/Polaroid/Polaroid.scss
new file mode 100644
index 00000000..352fbd90
--- /dev/null
+++ b/web-app/src/components/Polaroid/Polaroid.scss
@@ -0,0 +1,107 @@
+@import "../../theme/_index.scss";
+
+.Polaroid {
+ max-width: 500px;
+ margin: 0 auto;
+ display: block;
+
+ user-drag: none;
+ padding: 30px;
+ padding-bottom: 50px;
+
+ box-shadow: 0 3px 6px rgba(0, 0, 0, .25);
+ border-radius: 5px;
+
+ border-top: 0.5px solid whitesmoke;
+
+ background-color: white;
+
+ position: relative;
+}
+
+@keyframes developPolaroid {
+ 0% {
+ filter: brightness(1%);
+ }
+ 100% {
+ filter: brightness(100%);
+ }
+}
+
+.Polaroid__Image {
+ width: 100%;
+ user-select: none;
+
+ pointer-events: none;
+}
+
+.Polaroid__Animation > .Polaroid__Image {
+ animation: developPolaroid 2s linear;
+}
+
+.Polaroid__ZoomIcon {
+ position: absolute;
+ width: 25px;
+ right: 40px;
+ top: 40px;
+
+ cursor: pointer;
+}
+
+.ReactModal__Overlay {
+ z-index: 20;
+}
+
+.ReactModal__Content {
+ top: 0 !important;
+ left: 0 !important;
+ right: 0 !important;
+ bottom: 0 !important;
+
+ display: flex;
+
+ border: none !important;
+ background: transparent !important;
+
+ flex-direction: column;
+
+ align-items: center;
+ justify-content: center;
+
+ box-sizing: content-box !important;
+
+ img {
+ width: 100%;
+ max-width: 75%;
+ // height: 100%;
+ cursor: pointer;
+ }
+
+ .Polaroid__Modal__CloseButton {
+ width: 200px;
+ }
+}
+
+.Polaroid__Bottom {
+ text-align: right;
+ margin-top: $margin * 2;
+
+ span {
+ cursor: pointer;
+ }
+
+ img {
+ cursor: pointer;
+ width: 20px;
+
+ color: red;
+ }
+}
+
+@include onMobile() {
+ .ReactModal__Content {
+ img {
+ max-width: 100%;
+ }
+ }
+}
diff --git a/web-app/src/components/Polaroid/index.js b/web-app/src/components/Polaroid/index.js
new file mode 100644
index 00000000..3352f20a
--- /dev/null
+++ b/web-app/src/components/Polaroid/index.js
@@ -0,0 +1,52 @@
+import React, { Component } from 'react';
+import ReactModal from 'react-modal';
+import classNames from '../../utils/classNames';
+
+import zoomIcon from '../../theme/icons/zoom.svg';
+
+import './Polaroid.css';
+
+class Polaroid extends Component {
+ constructor(...props) {
+ super(...props);
+
+ this.state = {
+ showModal: false,
+ };
+ }
+
+ showModal(status) {
+ this.setState({ showModal: status });
+ }
+
+ render() {
+ const { img, toggle, zoomable } = this.props;
+ const { showModal } = this.state;
+
+ return (
+
+
+ {zoomable && (
+
+
this.showModal(true)} src={zoomIcon} alt="Zoom icon" className="Polaroid__ZoomIcon" />
+
+
+ this.showModal(false)} />
+
+
+ )}
+
+
+
+ );
+ }
+}
+
+Polaroid.defaultProps = {
+ zoomable: true,
+};
+
+export default Polaroid;
diff --git a/web-app/src/components/Ranking/Ranking.js b/web-app/src/components/Ranking/Ranking.js
new file mode 100644
index 00000000..6700c0c8
--- /dev/null
+++ b/web-app/src/components/Ranking/Ranking.js
@@ -0,0 +1,64 @@
+import React from 'react';
+import TransitiveNumber from 'react-transitive-number';
+
+import classNames from '../../utils/classNames';
+
+import './Ranking.css';
+
+import crestMenu from '../../theme/icons/crest_menu.svg';
+import first from '../../theme/icons/first.svg';
+import second from '../../theme/icons/second.svg';
+import third from '../../theme/icons/third.svg';
+
+const RankingRank = ({ index }) => {
+ const isTop = index <= 3;
+
+ return (
+
+ {index === 1 &&
}
+ {index === 2 &&
}
+ {index === 3 &&
}
+ {!isTop &&
{index} }
+
+ );
+};
+
+export const Ranking = ({ ranking, user }) => {
+ const userRankIndex = ranking.findIndex((rankingUser) => rankingUser.id === user.id);
+ const userRank = ranking[userRankIndex];
+
+ return (
+
+
+
+
Rank
+
{userRankIndex + 1}
+
+
+ {user.avatar_url === undefined || user.avatar_url === null
+ ?
+ :
+ }
+
{user.name}
+
+
+
Points
+
{userRank.points}
+
+
+ {ranking.map((rank, index) => (
+
+
+
+ {rank.name}
+
+
+ {rank.points}
+
+
+ ))}
+
+ );
+};
+
+export default Ranking;
diff --git a/web-app/src/components/Ranking/Ranking.scss b/web-app/src/components/Ranking/Ranking.scss
new file mode 100644
index 00000000..c731386a
--- /dev/null
+++ b/web-app/src/components/Ranking/Ranking.scss
@@ -0,0 +1,135 @@
+@import "../../theme/_index.scss";
+
+$borderRadius: 3px;
+
+.Ranking {
+ max-width: 560px;
+ min-height: 300px;
+
+ color: white;
+
+ margin: 0 auto;
+ margin-top: 30px;
+}
+
+.Ranking__Header {
+ display: flex;
+ background-color: $darkPastelBlue;
+
+ padding: 17px 30px;
+
+ font-weight: 600;
+ font-size: 25px;
+
+ border-radius: $borderRadius;
+ margin-bottom: 30px;
+}
+
+.Ranking__Header__Rank {
+ flex-shrink: 0;
+
+ display: flex;
+ flex-direction: column;
+}
+
+.Ranking__Header__Profile {
+ flex: 1 0 auto;
+ text-align: center;
+
+ position: relative;
+
+ > div {
+ margin-top: 40px;
+ }
+}
+
+.Ranking__Header__Points {
+ flex-shrink: 0;
+}
+
+.Ranking__Header__Rank,
+.Ranking__Header__Points {
+ > div:nth-child(1) {
+ font-weight: 500;
+ }
+
+ > div:nth-child(2) {
+ text-align: center;
+ padding-top: 10px;
+ }
+}
+
+.Profile__Header__Picture {
+ position: absolute;
+ top: -70px;
+ left: 50%;
+ right: 50%;
+ transform: translate(-50%, 0%);
+
+ width: 100px;
+ height: 100px;
+ border-radius: 50%;
+ border: solid 5px $rankingBlue;
+}
+
+.Ranking__Line {
+ background: $darkPastelBlue;
+ font-size: 25px;
+ line-height: 28px;
+
+ display: flex;
+
+ padding: 20px 20px;
+ margin-bottom: 10px;
+
+ border-radius: $borderRadius;
+
+ font-weight: 600;
+}
+
+.Ranking__Line__Rank--active {
+ background-color: $rankingBlue;
+}
+
+.Ranking__Line__Rank {
+ flex-shrink: 0;
+ position: relative;
+
+ z-index: 0;
+
+ width: 35px;
+
+ text-align: center;
+
+ img {
+ position: absolute;
+ z-index: -1;
+ width: 35px;
+
+ left: 50%;
+ right: 50%;
+ transform: translate(-50%, 0%);
+ }
+}
+
+.Ranking__Line__Rank--top > span {
+ font-size: 13px;
+ font-weight: normal;
+ display: block;
+ margin-top: -4px;
+ z-index: 3;
+}
+
+.Ranking__Line__Name {
+ flex-grow: 1;
+ text-align: center;
+}
+
+@include onMobile() {
+ .Ranking__Line,
+ .Ranking__Header {
+ font-weight: normal;
+ font-size: 15px;
+ line-height: 21px;
+ }
+}
diff --git a/web-app/src/components/Ranking/RankingContainer.js b/web-app/src/components/Ranking/RankingContainer.js
new file mode 100644
index 00000000..a463559e
--- /dev/null
+++ b/web-app/src/components/Ranking/RankingContainer.js
@@ -0,0 +1,18 @@
+import React, { Component } from 'react';
+
+import Ranking from './Ranking';
+import api from '../../utils/api';
+
+class RankingContainer extends Component {
+ componentWillMount() {
+ api.get('/leaderboard').then(({ data }) => this.props.loadRanking(data));
+ }
+
+ render() {
+ return (
+
+ );
+ }
+}
+
+export default RankingContainer;
diff --git a/web-app/src/components/Ranking/index.js b/web-app/src/components/Ranking/index.js
new file mode 100644
index 00000000..9941fff3
--- /dev/null
+++ b/web-app/src/components/Ranking/index.js
@@ -0,0 +1,21 @@
+import { connect } from 'react-redux';
+
+import mapActionCreatorsToProps from '../../utils/mapActionCreatorsToProps';
+import RankingContainer from './RankingContainer';
+
+import { loadRanking } from '../../actions';
+import { getRanking, getUser } from '../../selectors';
+
+const mapStateToProps = (state) => ({
+ ranking: getRanking(state),
+ user: getUser(state),
+});
+
+const actionCreators = mapActionCreatorsToProps({
+ loadRanking,
+});
+
+export default connect(
+ mapStateToProps,
+ actionCreators,
+)(RankingContainer);
diff --git a/web-app/src/components/RequestResetPassword/RequestResetPassword.scss b/web-app/src/components/RequestResetPassword/RequestResetPassword.scss
new file mode 100644
index 00000000..2bff9602
--- /dev/null
+++ b/web-app/src/components/RequestResetPassword/RequestResetPassword.scss
@@ -0,0 +1,13 @@
+@import "../../theme/_index.scss";
+
+.RequestResetPassword__Label {
+ color: $darkPastelBlue;
+}
+
+.RequestResetPassword__Input {
+ margin-top: $margin * 2;
+}
+
+.RequestResetPassword__Button {
+ margin-top: 16px;
+}
\ No newline at end of file
diff --git a/web-app/src/components/RequestResetPassword/index.js b/web-app/src/components/RequestResetPassword/index.js
new file mode 100644
index 00000000..83785e28
--- /dev/null
+++ b/web-app/src/components/RequestResetPassword/index.js
@@ -0,0 +1,55 @@
+import React, { Component } from 'react';
+
+import Title from '../Title';
+import GuestMode, { GoBack } from '../GuestMode';
+import { Form, Input, Button } from '../Form';
+import { Success } from '../Alerts';
+
+import api from '../../utils/api';
+
+import './RequestResetPassword.css';
+
+class RequestResetPassword extends Component {
+ constructor(...props) {
+ super(...props);
+
+ this.state = {
+ isValid: false,
+ success: false,
+ };
+ }
+
+ requestResetPassword(body) {
+ api.post('/auth/reset', body).then(() => {
+ this.setState({ success: true });
+ });
+ }
+
+ render() {
+ const { isValid, success } = this.state;
+
+ return (
+
+
+
+ {success && }
+
+ {!success && (
+
+ )}
+
+
+ );
+ }
+}
+
+export default RequestResetPassword;
diff --git a/web-app/src/components/ResetPassword/ResetPassword.scss b/web-app/src/components/ResetPassword/ResetPassword.scss
new file mode 100644
index 00000000..6faad55d
--- /dev/null
+++ b/web-app/src/components/ResetPassword/ResetPassword.scss
@@ -0,0 +1,13 @@
+@import "../../theme/_index.scss";
+
+.ResetPassword__Label {
+ color: $darkPastelBlue;
+}
+
+.ResetPassword__Input {
+ margin-top: $margin * 2;
+}
+
+.ResetPassword__Button {
+ margin-top: 16px;
+}
\ No newline at end of file
diff --git a/web-app/src/components/ResetPassword/index.js b/web-app/src/components/ResetPassword/index.js
new file mode 100644
index 00000000..ce6a4307
--- /dev/null
+++ b/web-app/src/components/ResetPassword/index.js
@@ -0,0 +1,71 @@
+import React, { Component } from 'react';
+
+import Title from '../Title';
+import GuestMode, { GoBack } from '../GuestMode';
+import { Form, Input, Button } from '../Form';
+import { Success, Errors } from '../Alerts';
+
+import api from '../../utils/api';
+
+import './ResetPassword.css';
+
+class RequestResetPassword extends Component {
+ constructor(...props) {
+ super(...props);
+
+ this.state = {
+ isValid: false,
+ success: false,
+ error: false,
+ };
+ }
+
+ requestResetPassword(body) {
+ const token = this.props.match.params.token;
+
+ api.post(`/auth/reset/${token}`, body).then(() => {
+ this.setState({ success: true });
+ })
+ .catch(() => {
+ this.setState({ error: true });
+ });
+ }
+
+ render() {
+ const { isValid, success, error } = this.state;
+
+ return (
+
+
+
+ {success && }
+
+ {error && }
+
+
+ {!success && (
+
+ )}
+
+
+ );
+ }
+}
+
+export default RequestResetPassword;
diff --git a/web-app/src/components/SignUp/SignUp.scss b/web-app/src/components/SignUp/SignUp.scss
new file mode 100644
index 00000000..bf0b6206
--- /dev/null
+++ b/web-app/src/components/SignUp/SignUp.scss
@@ -0,0 +1,16 @@
+@import "../../theme/_index.scss";
+
+.SignUp__Input {
+ margin-bottom: $margin * 2;
+}
+
+.SignUp__Button {
+ margin-top: $margin * 3;
+}
+
+.SignUp__PasswordInformation {
+ margin-top: -$margin;
+ font-size: 11px;
+
+ color: $jordyBlue;
+}
diff --git a/web-app/src/components/SignUp/index.js b/web-app/src/components/SignUp/index.js
new file mode 100644
index 00000000..c7310278
--- /dev/null
+++ b/web-app/src/components/SignUp/index.js
@@ -0,0 +1,75 @@
+/* global window */
+import React, { Component } from 'react';
+
+import GuestMode, { GoBack } from '../GuestMode';
+import Title from '../Title';
+import { Input, Form, Button } from '../Form';
+import { Errors } from '../Alerts';
+
+import redirect from '../../utils/redirect';
+import api from '../../utils/api';
+
+import './SignUp.css';
+
+class SignUp extends Component {
+ constructor(...props) {
+ super(...props);
+
+ this.state = {
+ isValid: false,
+ busy: false,
+ errors: undefined,
+ };
+ }
+
+ signUp(body) {
+ this.setState({
+ busy: true,
+ });
+
+ api.post('/auth/register', body)
+ .then(({ data }) => {
+ this.setState({ busy: false });
+
+ // TODO: make an easier jwt token manager
+ window.localStorage.setItem('jwt.token', data.token);
+ redirect('/');
+ })
+ .catch(({ data }) => {
+ this.setState({ busy: false, errors: data });
+ });
+ }
+
+ render() {
+ const { isValid, busy, success, errors } = this.state;
+
+ return (
+
+
+
+ {errors && }
+
+ {!success && (
+
+ )}
+
+
+ );
+ }
+}
+
+export default SignUp;
diff --git a/web-app/src/components/SignUpAfterOnBoarding/SignUpAfterOnBoarding.scss b/web-app/src/components/SignUpAfterOnBoarding/SignUpAfterOnBoarding.scss
new file mode 100644
index 00000000..2731202a
--- /dev/null
+++ b/web-app/src/components/SignUpAfterOnBoarding/SignUpAfterOnBoarding.scss
@@ -0,0 +1,69 @@
+@import "../../theme/_index.scss";
+
+.SignUpAfterOnBoarding__Content {
+ opacity: 0;
+ transition: all 5s linear;
+ pointer-events: none;
+}
+
+.SignUpAfterOnBoarding__Content--visible {
+ opacity: 1;
+ pointer-events: initial;
+}
+
+.SignUpAfterOnBoarding__Overlay {
+ opacity: 1;
+ transition: all 5s linear;
+ pointer-events: none;
+}
+
+.SignUpAfterOnBoarding__Overlay--hidden {
+ opacity: 0;
+}
+
+.SignUpAfterOnBoarding__Header {
+ width: 100%;
+ position: absolute;
+ top: 0;
+}
+
+.SignUpAfterOnBoarding__Text {
+ position: absolute;
+ margin-top: $headerHeight + 15px;
+ top: 0;
+
+ left: 50%;
+ right: 50%;
+
+ transform: translate(-50%, 0%);
+
+ width: 100%;
+ max-width: 400px;
+
+ font-size: 21px;
+
+ line-height: 28px;
+
+ text-align: center;
+}
+
+.SignUpAfterOnBoarding__Bert {
+ width: 100%;
+ max-width: 300px;
+
+ position: absolute;
+ bottom: 0;
+
+ left: 50%;
+ right: 50%;
+
+ transform: translate(-50%, 0%);
+
+ overflow: hidden;
+}
+
+@include max-height(674px) {
+ .SignUpAfterOnBoarding__Bert {
+ max-height: 50%;
+ }
+}
\ No newline at end of file
diff --git a/web-app/src/components/SignUpAfterOnBoarding/index.js b/web-app/src/components/SignUpAfterOnBoarding/index.js
new file mode 100644
index 00000000..b6cdb580
--- /dev/null
+++ b/web-app/src/components/SignUpAfterOnBoarding/index.js
@@ -0,0 +1,49 @@
+import React, { Component } from 'react';
+import SignUp from '../SignUp';
+import Header from '../Header';
+import classNames from '../../utils/classNames';
+
+import './SignUpAfterOnBoarding.css';
+
+import bertVerkijkerIcon from '../../theme/icons/bert_verkijker.svg';
+
+class SignUpAfterOnBoarding extends Component {
+ constructor(...props) {
+ super(...props);
+
+ this.state = {
+ showContent: false,
+ };
+ }
+
+ componentDidMount() {
+ setTimeout(() => {
+ this.setState({ showContent: true });
+ }, 4000);
+ }
+
+ render() {
+ const { showContent } = this.state;
+
+ return (
+
+
+
+
+
+
+
+
+
+ Awesome!
+ Let's get you signed up, so
+ you can keep helping me!
+
+
+
+
+ );
+ }
+}
+
+export default SignUpAfterOnBoarding;
diff --git a/web-app/src/components/StartScreen/StartScreen.scss b/web-app/src/components/StartScreen/StartScreen.scss
index 549e95fc..3af99613 100644
--- a/web-app/src/components/StartScreen/StartScreen.scss
+++ b/web-app/src/components/StartScreen/StartScreen.scss
@@ -1,52 +1,36 @@
@import "../../theme/_index.scss";
.StartScreen {
- display: flex;
-}
-
-.StartScreen__Item {
- width: 100%;
+ display: flex;
}
.StartScreen__Title {
- font-size: 29px;
- color: $darkPastelBlue;
- text-align: center;
+ font-size: 29px;
+ color: $darkPastelBlue;
+ text-align: center;
}
.StartScreen__SubTitle {
- font-size: 17.5px;
- color: $darkPastelBlue;
- text-align: center;
- margin-top: 16px;
-}
-
-.StartScreen__Button__Start {
- margin-top: 32px;
+ font-size: 17.5px;
+ color: $darkPastelBlue;
+ text-align: center;
+ margin-top: 16px;
}
-.StartScreen__Button__LogIn {
- margin-top: 16px;
+.StartScreen__Buttons {
+ margin-top: auto;
}
-.StartScreen__Dots {
- display: flex;
- justify-content: space-between;
- margin: 0px 80px;
- margin-top: 90px;
+.StartScreen__Buttons__Start {
+ margin-top: 32px;
}
-.StartScreen__Dot {
- display: flex;
- height: 15px;
- width: 15px;
-
- border: 1px solid $nevada;
- border-radius: 50%;
-
- cursor: pointer;
+.StartScreen__Buttons__LogIn {
+ margin-top: 16px;
}
-.StartScreen__Dot--active {
- background-color: $nevada;
+@media (min-height: 700px) {
+ .StartScreen__Buttons {
+ margin-top: 50px;
+ }
}
diff --git a/web-app/src/components/StartScreen/index.js b/web-app/src/components/StartScreen/index.js
index 71020c9f..1d5dfa4c 100644
--- a/web-app/src/components/StartScreen/index.js
+++ b/web-app/src/components/StartScreen/index.js
@@ -5,55 +5,52 @@ import GuestMode from '../GuestMode';
import Title from '../Title';
import { Button } from '../Form';
-import classNames from '../../utils/classNames';
-
import './StartScreen.css';
-const Dot = ({ className }) => {
- return (
-
- );
-};
-
class StartScreen extends Component {
constructor(...props) {
super(...props);
this.state = {
redirect: false,
+ to: undefined,
};
}
login() {
this.setState({
redirect: true,
+ to: '/login',
+ });
+ }
+
+ startOnboarding() {
+ this.setState({
+ redirect: true,
+ to: '/start',
});
}
render() {
- if (this.state.redirect) {
- return ;
+ const { redirect, to } = this.state;
+
+ if (redirect) {
+ return ;
}
return (
-
+
birds.today
-
+
A bird spotting app
-
-
-
-
-
-
+
+ this.startOnboarding()}>Start
+ this.login()}>Log in
-
-
Start
-
this.login()}>Log in
);
}
diff --git a/web-app/src/index.js b/web-app/src/index.js
index f35edf52..ca3bc602 100644
--- a/web-app/src/index.js
+++ b/web-app/src/index.js
@@ -1,7 +1,7 @@
/* global document, window */
import React from 'react';
import { render } from 'react-dom';
-import { BrowserRouter as Router, Switch, Route, Redirect } from 'react-router-dom';
+import { BrowserRouter, HashRouter, Switch, Route, Redirect } from 'react-router-dom';
import { createStore, applyMiddleware } from 'redux';
import { Provider } from 'react-redux';
@@ -12,6 +12,12 @@ import App from './components/App';
import Login from './components/Login';
import LoginCallback from './components/Login/LoginCallback';
import StartScreen from './components/StartScreen';
+import SignUp from './components/SignUp';
+import SignUpAfterOnBoarding from './components/SignUpAfterOnBoarding';
+import OnBoard from './components/OnBoard';
+import RequestResetPassword from './components/RequestResetPassword';
+import ResetPassword from './components/ResetPassword';
+import authenticated from './utils/isAuthenticated';
import './index.css';
@@ -28,25 +34,29 @@ const configureStore = () => {
};
const isAuthenticated = () => {
- const token = window.localStorage.getItem('jwt.token');
-
- if (window.location.pathname.startsWith('/') && !token) {
- return
;
- }
-
- if (token === null || token === undefined) {
- return
;
+ if (!authenticated()) {
+ return
;
}
return
;
};
+const Router = process.env.REACT_APP_ROUTER === 'HASH'
+ ? HashRouter
+ : BrowserRouter;
+
const Root = () => (
+
+
+
+
+
+
{isAuthenticated()}
diff --git a/web-app/src/index.scss b/web-app/src/index.scss
index c659d3fd..a799df36 100644
--- a/web-app/src/index.scss
+++ b/web-app/src/index.scss
@@ -1,18 +1,22 @@
-
@import url('https://fonts.googleapis.com/css?family=Chivo:400,700i,900');
@import "./theme/_index.scss";
@import "./_reset.scss";
-body, html, #root {
+body,
+html,
+#root {
height: 100%;
+ min-height: 100%;
width: 100%;
+ overflow: auto;
}
html * {
font-family: $font;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
+ box-sizing: border-box;
}
a {
@@ -21,14 +25,48 @@ a {
}
table {
- width: 100%;
+ width: 100%;
+}
+
+table th {
+ text-align: left;
+ font-weight: bold;
}
+table td,
table th {
- text-align: left;
- font-weight: bold;
+ padding: 10px;
+}
+
+h1, h2, h3, h4, h5, h6 {
+ color: rgba(0,0,0,.72);
+ margin-top: $margin * 2;
+ margin-bottom: 20px;
+}
+h1 {
+ font-size: 2em;
+}
+
+h2 {
+ font-size: 1.5em;
+}
+
+h3 {
+ font-size: 1.17em;
+}
+
+h4 {
+ font-size: 1.12em;
+}
+
+h5 {
+ font-size: .83em;
+}
+
+h6 {
+ font-size: .75em;
}
-table td, table th {
- padding: 10px;
+p {
+ margin-bottom: $margin * 2;
}
diff --git a/web-app/src/reducers/index.js b/web-app/src/reducers/index.js
index 36b48a17..f3affa96 100644
--- a/web-app/src/reducers/index.js
+++ b/web-app/src/reducers/index.js
@@ -1,10 +1,12 @@
import { combineReducers } from 'redux';
import application from './application';
import observations from './observations';
+import ranking from './ranking';
const rootReducer = combineReducers({
application,
observations,
+ ranking,
});
export default rootReducer;
diff --git a/web-app/src/reducers/ranking.js b/web-app/src/reducers/ranking.js
new file mode 100644
index 00000000..192fc4d6
--- /dev/null
+++ b/web-app/src/reducers/ranking.js
@@ -0,0 +1,16 @@
+import reducer from './reducer';
+
+import { LOAD_RANKING } from '../actions/types';
+
+const defaultState = {
+ all: [],
+};
+
+const ranking = reducer({
+ [LOAD_RANKING]: (state, action) => ({
+ ...state,
+ all: [...action.ranking],
+ }),
+}, defaultState);
+
+export default ranking;
diff --git a/web-app/src/selectors/index.js b/web-app/src/selectors/index.js
index fafe3104..062f4876 100644
--- a/web-app/src/selectors/index.js
+++ b/web-app/src/selectors/index.js
@@ -1,2 +1,3 @@
export * from './application';
export * from './observations';
+export * from './ranking';
diff --git a/web-app/src/selectors/ranking.js b/web-app/src/selectors/ranking.js
new file mode 100644
index 00000000..988d90e1
--- /dev/null
+++ b/web-app/src/selectors/ranking.js
@@ -0,0 +1 @@
+export const getRanking = (state) => state.ranking.all;
diff --git a/web-app/src/theme/_grid.scss b/web-app/src/theme/_grid.scss
new file mode 100644
index 00000000..2e9acabd
--- /dev/null
+++ b/web-app/src/theme/_grid.scss
@@ -0,0 +1,1017 @@
+/*!
+ * Bootstrap v3.3.7 (http://getbootstrap.com)
+ * Copyright 2011-2017 Twitter, Inc.
+ * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
+ */
+
+/*!
+ * Generated using the Bootstrap Customizer (http://getbootstrap.com/customize/?id=378ac3246e184cc3b1e62807e08c2936)
+ * Config saved to config.json and https://gist.github.com/378ac3246e184cc3b1e62807e08c2936
+ */
+/*!
+ * Bootstrap v3.3.7 (http://getbootstrap.com)
+ * Copyright 2011-2016 Twitter, Inc.
+ * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
+ */
+/*! normalize.css v3.0.3 | MIT License | github.com/necolas/normalize.css */
+html {
+ font-family: sans-serif;
+ -ms-text-size-adjust: 100%;
+ -webkit-text-size-adjust: 100%;
+}
+body {
+ margin: 0;
+}
+article,
+aside,
+details,
+figcaption,
+figure,
+footer,
+header,
+hgroup,
+main,
+menu,
+nav,
+section,
+summary {
+ display: block;
+}
+audio,
+canvas,
+progress,
+video {
+ display: inline-block;
+ vertical-align: baseline;
+}
+audio:not([controls]) {
+ display: none;
+ height: 0;
+}
+[hidden],
+template {
+ display: none;
+}
+a {
+ background-color: transparent;
+}
+a:active,
+a:hover {
+ outline: 0;
+}
+abbr[title] {
+ border-bottom: 1px dotted;
+}
+b,
+strong {
+ font-weight: bold;
+}
+dfn {
+ font-style: italic;
+}
+h1 {
+ font-size: 2em;
+ margin: 0.67em 0;
+}
+mark {
+ background: #ff0;
+ color: #000;
+}
+small {
+ font-size: 80%;
+}
+sub,
+sup {
+ font-size: 75%;
+ line-height: 0;
+ position: relative;
+ vertical-align: baseline;
+}
+sup {
+ top: -0.5em;
+}
+sub {
+ bottom: -0.25em;
+}
+img {
+ border: 0;
+}
+svg:not(:root) {
+ overflow: hidden;
+}
+figure {
+ margin: 1em 40px;
+}
+hr {
+ -webkit-box-sizing: content-box;
+ -moz-box-sizing: content-box;
+ box-sizing: content-box;
+ height: 0;
+}
+pre {
+ overflow: auto;
+}
+code,
+kbd,
+pre,
+samp {
+ font-family: monospace, monospace;
+ font-size: 1em;
+}
+button,
+input,
+optgroup,
+select,
+textarea {
+ color: inherit;
+ font: inherit;
+ margin: 0;
+}
+button {
+ overflow: visible;
+}
+button,
+select {
+ text-transform: none;
+}
+button,
+html input[type="button"],
+input[type="reset"],
+input[type="submit"] {
+ -webkit-appearance: button;
+ cursor: pointer;
+}
+button[disabled],
+html input[disabled] {
+ cursor: default;
+}
+button::-moz-focus-inner,
+input::-moz-focus-inner {
+ border: 0;
+ padding: 0;
+}
+input {
+ line-height: normal;
+}
+input[type="checkbox"],
+input[type="radio"] {
+ -webkit-box-sizing: border-box;
+ -moz-box-sizing: border-box;
+ box-sizing: border-box;
+ padding: 0;
+}
+input[type="number"]::-webkit-inner-spin-button,
+input[type="number"]::-webkit-outer-spin-button {
+ height: auto;
+}
+input[type="search"] {
+ -webkit-appearance: textfield;
+ -webkit-box-sizing: content-box;
+ -moz-box-sizing: content-box;
+ box-sizing: content-box;
+}
+input[type="search"]::-webkit-search-cancel-button,
+input[type="search"]::-webkit-search-decoration {
+ -webkit-appearance: none;
+}
+fieldset {
+ border: 1px solid #c0c0c0;
+ margin: 0 2px;
+ padding: 0.35em 0.625em 0.75em;
+}
+legend {
+ border: 0;
+ padding: 0;
+}
+textarea {
+ overflow: auto;
+}
+optgroup {
+ font-weight: bold;
+}
+table {
+ border-collapse: collapse;
+ border-spacing: 0;
+}
+td,
+th {
+ padding: 0;
+}
+* {
+ -webkit-box-sizing: border-box;
+ -moz-box-sizing: border-box;
+ box-sizing: border-box;
+}
+*:before,
+*:after {
+ -webkit-box-sizing: border-box;
+ -moz-box-sizing: border-box;
+ box-sizing: border-box;
+}
+html {
+ font-size: 10px;
+ -webkit-tap-highlight-color: rgba(0, 0, 0, 0);
+}
+body {
+ font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
+ font-size: 14px;
+ line-height: 1.42857143;
+ color: #333333;
+ background-color: #ffffff;
+}
+input,
+button,
+select,
+textarea {
+ font-family: inherit;
+ font-size: inherit;
+ line-height: inherit;
+}
+a {
+ color: #337ab7;
+ text-decoration: none;
+}
+a:hover,
+a:focus {
+ color: #23527c;
+ text-decoration: underline;
+}
+a:focus {
+ outline: 5px auto -webkit-focus-ring-color;
+ outline-offset: -2px;
+}
+figure {
+ margin: 0;
+}
+img {
+ vertical-align: middle;
+}
+.img-responsive {
+ display: block;
+ max-width: 100%;
+ height: auto;
+}
+.img-rounded {
+ border-radius: 6px;
+}
+.img-thumbnail {
+ padding: 4px;
+ line-height: 1.42857143;
+ background-color: #ffffff;
+ border: 1px solid #dddddd;
+ border-radius: 4px;
+ -webkit-transition: all 0.2s ease-in-out;
+ -o-transition: all 0.2s ease-in-out;
+ transition: all 0.2s ease-in-out;
+ display: inline-block;
+ max-width: 100%;
+ height: auto;
+}
+.img-circle {
+ border-radius: 50%;
+}
+hr {
+ margin-top: 20px;
+ margin-bottom: 20px;
+ border: 0;
+ border-top: 1px solid #eeeeee;
+}
+.sr-only {
+ position: absolute;
+ width: 1px;
+ height: 1px;
+ margin: -1px;
+ padding: 0;
+ overflow: hidden;
+ clip: rect(0, 0, 0, 0);
+ border: 0;
+}
+.sr-only-focusable:active,
+.sr-only-focusable:focus {
+ position: static;
+ width: auto;
+ height: auto;
+ margin: 0;
+ overflow: visible;
+ clip: auto;
+}
+[role="button"] {
+ cursor: pointer;
+}
+.container {
+ margin-right: auto;
+ margin-left: auto;
+ padding-left: 15px;
+ padding-right: 15px;
+}
+@media (min-width: 768px) {
+ .container {
+ width: 750px;
+ }
+}
+@media (min-width: 992px) {
+ .container {
+ width: 970px;
+ }
+}
+@media (min-width: 1200px) {
+ .container {
+ width: 1170px;
+ }
+}
+.container-fluid {
+ margin-right: auto;
+ margin-left: auto;
+ padding-left: 15px;
+ padding-right: 15px;
+}
+.row {
+ margin-left: -15px;
+ margin-right: -15px;
+}
+.col-xs-1, .col-sm-1, .col-md-1, .col-lg-1, .col-xs-2, .col-sm-2, .col-md-2, .col-lg-2, .col-xs-3, .col-sm-3, .col-md-3, .col-lg-3, .col-xs-4, .col-sm-4, .col-md-4, .col-lg-4, .col-xs-5, .col-sm-5, .col-md-5, .col-lg-5, .col-xs-6, .col-sm-6, .col-md-6, .col-lg-6, .col-xs-7, .col-sm-7, .col-md-7, .col-lg-7, .col-xs-8, .col-sm-8, .col-md-8, .col-lg-8, .col-xs-9, .col-sm-9, .col-md-9, .col-lg-9, .col-xs-10, .col-sm-10, .col-md-10, .col-lg-10, .col-xs-11, .col-sm-11, .col-md-11, .col-lg-11, .col-xs-12, .col-sm-12, .col-md-12, .col-lg-12 {
+ position: relative;
+ min-height: 1px;
+ padding-left: 15px;
+ padding-right: 15px;
+}
+.col-xs-1, .col-xs-2, .col-xs-3, .col-xs-4, .col-xs-5, .col-xs-6, .col-xs-7, .col-xs-8, .col-xs-9, .col-xs-10, .col-xs-11, .col-xs-12 {
+ float: left;
+}
+.col-xs-12 {
+ width: 100%;
+}
+.col-xs-11 {
+ width: 91.66666667%;
+}
+.col-xs-10 {
+ width: 83.33333333%;
+}
+.col-xs-9 {
+ width: 75%;
+}
+.col-xs-8 {
+ width: 66.66666667%;
+}
+.col-xs-7 {
+ width: 58.33333333%;
+}
+.col-xs-6 {
+ width: 50%;
+}
+.col-xs-5 {
+ width: 41.66666667%;
+}
+.col-xs-4 {
+ width: 33.33333333%;
+}
+.col-xs-3 {
+ width: 25%;
+}
+.col-xs-2 {
+ width: 16.66666667%;
+}
+.col-xs-1 {
+ width: 8.33333333%;
+}
+.col-xs-pull-12 {
+ right: 100%;
+}
+.col-xs-pull-11 {
+ right: 91.66666667%;
+}
+.col-xs-pull-10 {
+ right: 83.33333333%;
+}
+.col-xs-pull-9 {
+ right: 75%;
+}
+.col-xs-pull-8 {
+ right: 66.66666667%;
+}
+.col-xs-pull-7 {
+ right: 58.33333333%;
+}
+.col-xs-pull-6 {
+ right: 50%;
+}
+.col-xs-pull-5 {
+ right: 41.66666667%;
+}
+.col-xs-pull-4 {
+ right: 33.33333333%;
+}
+.col-xs-pull-3 {
+ right: 25%;
+}
+.col-xs-pull-2 {
+ right: 16.66666667%;
+}
+.col-xs-pull-1 {
+ right: 8.33333333%;
+}
+.col-xs-pull-0 {
+ right: auto;
+}
+.col-xs-push-12 {
+ left: 100%;
+}
+.col-xs-push-11 {
+ left: 91.66666667%;
+}
+.col-xs-push-10 {
+ left: 83.33333333%;
+}
+.col-xs-push-9 {
+ left: 75%;
+}
+.col-xs-push-8 {
+ left: 66.66666667%;
+}
+.col-xs-push-7 {
+ left: 58.33333333%;
+}
+.col-xs-push-6 {
+ left: 50%;
+}
+.col-xs-push-5 {
+ left: 41.66666667%;
+}
+.col-xs-push-4 {
+ left: 33.33333333%;
+}
+.col-xs-push-3 {
+ left: 25%;
+}
+.col-xs-push-2 {
+ left: 16.66666667%;
+}
+.col-xs-push-1 {
+ left: 8.33333333%;
+}
+.col-xs-push-0 {
+ left: auto;
+}
+.col-xs-offset-12 {
+ margin-left: 100%;
+}
+.col-xs-offset-11 {
+ margin-left: 91.66666667%;
+}
+.col-xs-offset-10 {
+ margin-left: 83.33333333%;
+}
+.col-xs-offset-9 {
+ margin-left: 75%;
+}
+.col-xs-offset-8 {
+ margin-left: 66.66666667%;
+}
+.col-xs-offset-7 {
+ margin-left: 58.33333333%;
+}
+.col-xs-offset-6 {
+ margin-left: 50%;
+}
+.col-xs-offset-5 {
+ margin-left: 41.66666667%;
+}
+.col-xs-offset-4 {
+ margin-left: 33.33333333%;
+}
+.col-xs-offset-3 {
+ margin-left: 25%;
+}
+.col-xs-offset-2 {
+ margin-left: 16.66666667%;
+}
+.col-xs-offset-1 {
+ margin-left: 8.33333333%;
+}
+.col-xs-offset-0 {
+ margin-left: 0%;
+}
+@media (min-width: 768px) {
+ .col-sm-1, .col-sm-2, .col-sm-3, .col-sm-4, .col-sm-5, .col-sm-6, .col-sm-7, .col-sm-8, .col-sm-9, .col-sm-10, .col-sm-11, .col-sm-12 {
+ float: left;
+ }
+ .col-sm-12 {
+ width: 100%;
+ }
+ .col-sm-11 {
+ width: 91.66666667%;
+ }
+ .col-sm-10 {
+ width: 83.33333333%;
+ }
+ .col-sm-9 {
+ width: 75%;
+ }
+ .col-sm-8 {
+ width: 66.66666667%;
+ }
+ .col-sm-7 {
+ width: 58.33333333%;
+ }
+ .col-sm-6 {
+ width: 50%;
+ }
+ .col-sm-5 {
+ width: 41.66666667%;
+ }
+ .col-sm-4 {
+ width: 33.33333333%;
+ }
+ .col-sm-3 {
+ width: 25%;
+ }
+ .col-sm-2 {
+ width: 16.66666667%;
+ }
+ .col-sm-1 {
+ width: 8.33333333%;
+ }
+ .col-sm-pull-12 {
+ right: 100%;
+ }
+ .col-sm-pull-11 {
+ right: 91.66666667%;
+ }
+ .col-sm-pull-10 {
+ right: 83.33333333%;
+ }
+ .col-sm-pull-9 {
+ right: 75%;
+ }
+ .col-sm-pull-8 {
+ right: 66.66666667%;
+ }
+ .col-sm-pull-7 {
+ right: 58.33333333%;
+ }
+ .col-sm-pull-6 {
+ right: 50%;
+ }
+ .col-sm-pull-5 {
+ right: 41.66666667%;
+ }
+ .col-sm-pull-4 {
+ right: 33.33333333%;
+ }
+ .col-sm-pull-3 {
+ right: 25%;
+ }
+ .col-sm-pull-2 {
+ right: 16.66666667%;
+ }
+ .col-sm-pull-1 {
+ right: 8.33333333%;
+ }
+ .col-sm-pull-0 {
+ right: auto;
+ }
+ .col-sm-push-12 {
+ left: 100%;
+ }
+ .col-sm-push-11 {
+ left: 91.66666667%;
+ }
+ .col-sm-push-10 {
+ left: 83.33333333%;
+ }
+ .col-sm-push-9 {
+ left: 75%;
+ }
+ .col-sm-push-8 {
+ left: 66.66666667%;
+ }
+ .col-sm-push-7 {
+ left: 58.33333333%;
+ }
+ .col-sm-push-6 {
+ left: 50%;
+ }
+ .col-sm-push-5 {
+ left: 41.66666667%;
+ }
+ .col-sm-push-4 {
+ left: 33.33333333%;
+ }
+ .col-sm-push-3 {
+ left: 25%;
+ }
+ .col-sm-push-2 {
+ left: 16.66666667%;
+ }
+ .col-sm-push-1 {
+ left: 8.33333333%;
+ }
+ .col-sm-push-0 {
+ left: auto;
+ }
+ .col-sm-offset-12 {
+ margin-left: 100%;
+ }
+ .col-sm-offset-11 {
+ margin-left: 91.66666667%;
+ }
+ .col-sm-offset-10 {
+ margin-left: 83.33333333%;
+ }
+ .col-sm-offset-9 {
+ margin-left: 75%;
+ }
+ .col-sm-offset-8 {
+ margin-left: 66.66666667%;
+ }
+ .col-sm-offset-7 {
+ margin-left: 58.33333333%;
+ }
+ .col-sm-offset-6 {
+ margin-left: 50%;
+ }
+ .col-sm-offset-5 {
+ margin-left: 41.66666667%;
+ }
+ .col-sm-offset-4 {
+ margin-left: 33.33333333%;
+ }
+ .col-sm-offset-3 {
+ margin-left: 25%;
+ }
+ .col-sm-offset-2 {
+ margin-left: 16.66666667%;
+ }
+ .col-sm-offset-1 {
+ margin-left: 8.33333333%;
+ }
+ .col-sm-offset-0 {
+ margin-left: 0%;
+ }
+}
+@media (min-width: 992px) {
+ .col-md-1, .col-md-2, .col-md-3, .col-md-4, .col-md-5, .col-md-6, .col-md-7, .col-md-8, .col-md-9, .col-md-10, .col-md-11, .col-md-12 {
+ float: left;
+ }
+ .col-md-12 {
+ width: 100%;
+ }
+ .col-md-11 {
+ width: 91.66666667%;
+ }
+ .col-md-10 {
+ width: 83.33333333%;
+ }
+ .col-md-9 {
+ width: 75%;
+ }
+ .col-md-8 {
+ width: 66.66666667%;
+ }
+ .col-md-7 {
+ width: 58.33333333%;
+ }
+ .col-md-6 {
+ width: 50%;
+ }
+ .col-md-5 {
+ width: 41.66666667%;
+ }
+ .col-md-4 {
+ width: 33.33333333%;
+ }
+ .col-md-3 {
+ width: 25%;
+ }
+ .col-md-2 {
+ width: 16.66666667%;
+ }
+ .col-md-1 {
+ width: 8.33333333%;
+ }
+ .col-md-pull-12 {
+ right: 100%;
+ }
+ .col-md-pull-11 {
+ right: 91.66666667%;
+ }
+ .col-md-pull-10 {
+ right: 83.33333333%;
+ }
+ .col-md-pull-9 {
+ right: 75%;
+ }
+ .col-md-pull-8 {
+ right: 66.66666667%;
+ }
+ .col-md-pull-7 {
+ right: 58.33333333%;
+ }
+ .col-md-pull-6 {
+ right: 50%;
+ }
+ .col-md-pull-5 {
+ right: 41.66666667%;
+ }
+ .col-md-pull-4 {
+ right: 33.33333333%;
+ }
+ .col-md-pull-3 {
+ right: 25%;
+ }
+ .col-md-pull-2 {
+ right: 16.66666667%;
+ }
+ .col-md-pull-1 {
+ right: 8.33333333%;
+ }
+ .col-md-pull-0 {
+ right: auto;
+ }
+ .col-md-push-12 {
+ left: 100%;
+ }
+ .col-md-push-11 {
+ left: 91.66666667%;
+ }
+ .col-md-push-10 {
+ left: 83.33333333%;
+ }
+ .col-md-push-9 {
+ left: 75%;
+ }
+ .col-md-push-8 {
+ left: 66.66666667%;
+ }
+ .col-md-push-7 {
+ left: 58.33333333%;
+ }
+ .col-md-push-6 {
+ left: 50%;
+ }
+ .col-md-push-5 {
+ left: 41.66666667%;
+ }
+ .col-md-push-4 {
+ left: 33.33333333%;
+ }
+ .col-md-push-3 {
+ left: 25%;
+ }
+ .col-md-push-2 {
+ left: 16.66666667%;
+ }
+ .col-md-push-1 {
+ left: 8.33333333%;
+ }
+ .col-md-push-0 {
+ left: auto;
+ }
+ .col-md-offset-12 {
+ margin-left: 100%;
+ }
+ .col-md-offset-11 {
+ margin-left: 91.66666667%;
+ }
+ .col-md-offset-10 {
+ margin-left: 83.33333333%;
+ }
+ .col-md-offset-9 {
+ margin-left: 75%;
+ }
+ .col-md-offset-8 {
+ margin-left: 66.66666667%;
+ }
+ .col-md-offset-7 {
+ margin-left: 58.33333333%;
+ }
+ .col-md-offset-6 {
+ margin-left: 50%;
+ }
+ .col-md-offset-5 {
+ margin-left: 41.66666667%;
+ }
+ .col-md-offset-4 {
+ margin-left: 33.33333333%;
+ }
+ .col-md-offset-3 {
+ margin-left: 25%;
+ }
+ .col-md-offset-2 {
+ margin-left: 16.66666667%;
+ }
+ .col-md-offset-1 {
+ margin-left: 8.33333333%;
+ }
+ .col-md-offset-0 {
+ margin-left: 0%;
+ }
+}
+@media (min-width: 1200px) {
+ .col-lg-1, .col-lg-2, .col-lg-3, .col-lg-4, .col-lg-5, .col-lg-6, .col-lg-7, .col-lg-8, .col-lg-9, .col-lg-10, .col-lg-11, .col-lg-12 {
+ float: left;
+ }
+ .col-lg-12 {
+ width: 100%;
+ }
+ .col-lg-11 {
+ width: 91.66666667%;
+ }
+ .col-lg-10 {
+ width: 83.33333333%;
+ }
+ .col-lg-9 {
+ width: 75%;
+ }
+ .col-lg-8 {
+ width: 66.66666667%;
+ }
+ .col-lg-7 {
+ width: 58.33333333%;
+ }
+ .col-lg-6 {
+ width: 50%;
+ }
+ .col-lg-5 {
+ width: 41.66666667%;
+ }
+ .col-lg-4 {
+ width: 33.33333333%;
+ }
+ .col-lg-3 {
+ width: 25%;
+ }
+ .col-lg-2 {
+ width: 16.66666667%;
+ }
+ .col-lg-1 {
+ width: 8.33333333%;
+ }
+ .col-lg-pull-12 {
+ right: 100%;
+ }
+ .col-lg-pull-11 {
+ right: 91.66666667%;
+ }
+ .col-lg-pull-10 {
+ right: 83.33333333%;
+ }
+ .col-lg-pull-9 {
+ right: 75%;
+ }
+ .col-lg-pull-8 {
+ right: 66.66666667%;
+ }
+ .col-lg-pull-7 {
+ right: 58.33333333%;
+ }
+ .col-lg-pull-6 {
+ right: 50%;
+ }
+ .col-lg-pull-5 {
+ right: 41.66666667%;
+ }
+ .col-lg-pull-4 {
+ right: 33.33333333%;
+ }
+ .col-lg-pull-3 {
+ right: 25%;
+ }
+ .col-lg-pull-2 {
+ right: 16.66666667%;
+ }
+ .col-lg-pull-1 {
+ right: 8.33333333%;
+ }
+ .col-lg-pull-0 {
+ right: auto;
+ }
+ .col-lg-push-12 {
+ left: 100%;
+ }
+ .col-lg-push-11 {
+ left: 91.66666667%;
+ }
+ .col-lg-push-10 {
+ left: 83.33333333%;
+ }
+ .col-lg-push-9 {
+ left: 75%;
+ }
+ .col-lg-push-8 {
+ left: 66.66666667%;
+ }
+ .col-lg-push-7 {
+ left: 58.33333333%;
+ }
+ .col-lg-push-6 {
+ left: 50%;
+ }
+ .col-lg-push-5 {
+ left: 41.66666667%;
+ }
+ .col-lg-push-4 {
+ left: 33.33333333%;
+ }
+ .col-lg-push-3 {
+ left: 25%;
+ }
+ .col-lg-push-2 {
+ left: 16.66666667%;
+ }
+ .col-lg-push-1 {
+ left: 8.33333333%;
+ }
+ .col-lg-push-0 {
+ left: auto;
+ }
+ .col-lg-offset-12 {
+ margin-left: 100%;
+ }
+ .col-lg-offset-11 {
+ margin-left: 91.66666667%;
+ }
+ .col-lg-offset-10 {
+ margin-left: 83.33333333%;
+ }
+ .col-lg-offset-9 {
+ margin-left: 75%;
+ }
+ .col-lg-offset-8 {
+ margin-left: 66.66666667%;
+ }
+ .col-lg-offset-7 {
+ margin-left: 58.33333333%;
+ }
+ .col-lg-offset-6 {
+ margin-left: 50%;
+ }
+ .col-lg-offset-5 {
+ margin-left: 41.66666667%;
+ }
+ .col-lg-offset-4 {
+ margin-left: 33.33333333%;
+ }
+ .col-lg-offset-3 {
+ margin-left: 25%;
+ }
+ .col-lg-offset-2 {
+ margin-left: 16.66666667%;
+ }
+ .col-lg-offset-1 {
+ margin-left: 8.33333333%;
+ }
+ .col-lg-offset-0 {
+ margin-left: 0%;
+ }
+}
+.clearfix:before,
+.clearfix:after,
+.container:before,
+.container:after,
+.container-fluid:before,
+.container-fluid:after,
+.row:before,
+.row:after {
+ content: " ";
+ display: table;
+}
+.clearfix:after,
+.container:after,
+.container-fluid:after,
+.row:after {
+ clear: both;
+}
+.center-block {
+ display: block;
+ margin-left: auto;
+ margin-right: auto;
+}
+.pull-right {
+ float: right !important;
+}
+.pull-left {
+ float: left !important;
+}
+.hide {
+ display: none !important;
+}
+.show {
+ display: block !important;
+}
+.invisible {
+ visibility: hidden;
+}
+.text-hide {
+ font: 0/0 a;
+ color: transparent;
+ text-shadow: none;
+ background-color: transparent;
+ border: 0;
+}
+.hidden {
+ display: none !important;
+}
+.affix {
+ position: fixed;
+}
diff --git a/web-app/src/theme/_index.scss b/web-app/src/theme/_index.scss
index 91b99ea6..b1868c10 100644
--- a/web-app/src/theme/_index.scss
+++ b/web-app/src/theme/_index.scss
@@ -1,2 +1,3 @@
@import "./_variables.scss";
@import "./_mixins.scss";
+@import "./_grid.scss";
\ No newline at end of file
diff --git a/web-app/src/theme/_mixins.scss b/web-app/src/theme/_mixins.scss
index c045489e..ffdc29f9 100644
--- a/web-app/src/theme/_mixins.scss
+++ b/web-app/src/theme/_mixins.scss
@@ -1,5 +1,11 @@
@mixin respond-to($size) {
- @media only screen and (min-width: $size) {
+ @media only screen and (max-width: $size) {
+ @content;
+ }
+}
+
+@mixin max-height($size) {
+ @media only screen and (max-height: $size) {
@content;
}
}
diff --git a/web-app/src/theme/_variables.scss b/web-app/src/theme/_variables.scss
index 85fdb264..9a598752 100644
--- a/web-app/src/theme/_variables.scss
+++ b/web-app/src/theme/_variables.scss
@@ -16,4 +16,16 @@ $governorBay: #455896;
$portGore: #324173;
$lobLolly: #BCCBD3;
$jordyBlue: #88bde6;
-$nevada: #6c6e78;
\ No newline at end of file
+$nevada: #6c6e78;
+$tabasco: #9d2720;
+$thunderbird: #bb3726;
+$mountainMist: #929493;
+$celestialBlue: #3e8dce;
+$rankingBlue: #147fc6;
+
+$padding: 6px;
+$margin: 6px;
+
+$appWrapperPadding: 30px;
+$headerHeight: 80px;
+$footerHeight: 80px;
\ No newline at end of file
diff --git a/web-app/src/theme/crest_red.svg b/web-app/src/theme/crest_red.svg
new file mode 100644
index 00000000..2afc8b61
--- /dev/null
+++ b/web-app/src/theme/crest_red.svg
@@ -0,0 +1 @@
+crest_red
\ No newline at end of file
diff --git a/web-app/src/theme/icons/arm.svg b/web-app/src/theme/icons/arm.svg
new file mode 100644
index 00000000..b8a4d822
--- /dev/null
+++ b/web-app/src/theme/icons/arm.svg
@@ -0,0 +1 @@
+arm
\ No newline at end of file
diff --git a/web-app/src/theme/icons/bert.svg b/web-app/src/theme/icons/bert.svg
new file mode 100644
index 00000000..5b3a774e
--- /dev/null
+++ b/web-app/src/theme/icons/bert.svg
@@ -0,0 +1 @@
+BERT_NEW
\ No newline at end of file
diff --git a/web-app/src/theme/icons/bert_verkijker.svg b/web-app/src/theme/icons/bert_verkijker.svg
new file mode 100644
index 00000000..1cfba3fc
--- /dev/null
+++ b/web-app/src/theme/icons/bert_verkijker.svg
@@ -0,0 +1 @@
+Bert_new_3
\ No newline at end of file
diff --git a/web-app/src/theme/icons/book.svg b/web-app/src/theme/icons/book.svg
new file mode 100755
index 00000000..5327beb4
--- /dev/null
+++ b/web-app/src/theme/icons/book.svg
@@ -0,0 +1 @@
+book
\ No newline at end of file
diff --git a/web-app/src/theme/icons/crest_bird.svg b/web-app/src/theme/icons/crest_bird.svg
new file mode 100644
index 00000000..3a1ea910
--- /dev/null
+++ b/web-app/src/theme/icons/crest_bird.svg
@@ -0,0 +1 @@
+bird alone
\ No newline at end of file
diff --git a/web-app/src/theme/icons/crest_bird_flying.svg b/web-app/src/theme/icons/crest_bird_flying.svg
new file mode 100644
index 00000000..84ced915
--- /dev/null
+++ b/web-app/src/theme/icons/crest_bird_flying.svg
@@ -0,0 +1 @@
+flying alone
\ No newline at end of file
diff --git a/web-app/src/theme/icons/crest_menu.svg b/web-app/src/theme/icons/crest_menu.svg
new file mode 100644
index 00000000..2fb0756f
--- /dev/null
+++ b/web-app/src/theme/icons/crest_menu.svg
@@ -0,0 +1 @@
+bird_menu
\ No newline at end of file
diff --git a/web-app/src/theme/icons/crest_red_empty.svg b/web-app/src/theme/icons/crest_red_empty.svg
new file mode 100644
index 00000000..11247c13
--- /dev/null
+++ b/web-app/src/theme/icons/crest_red_empty.svg
@@ -0,0 +1 @@
+button alone
\ No newline at end of file
diff --git a/web-app/src/theme/icons/email.svg b/web-app/src/theme/icons/email.svg
new file mode 100644
index 00000000..465f534a
--- /dev/null
+++ b/web-app/src/theme/icons/email.svg
@@ -0,0 +1 @@
+mail
\ No newline at end of file
diff --git a/web-app/src/theme/icons/feather.svg b/web-app/src/theme/icons/feather.svg
new file mode 100644
index 00000000..ea6799a8
--- /dev/null
+++ b/web-app/src/theme/icons/feather.svg
@@ -0,0 +1 @@
+feather
diff --git a/web-app/src/theme/icons/first.svg b/web-app/src/theme/icons/first.svg
new file mode 100755
index 00000000..002b8b56
--- /dev/null
+++ b/web-app/src/theme/icons/first.svg
@@ -0,0 +1 @@
+first
\ No newline at end of file
diff --git a/web-app/src/theme/icons/little_bert.svg b/web-app/src/theme/icons/little_bert.svg
new file mode 100644
index 00000000..b5be788b
--- /dev/null
+++ b/web-app/src/theme/icons/little_bert.svg
@@ -0,0 +1 @@
+little_bert
\ No newline at end of file
diff --git a/web-app/src/theme/icons/lock.svg b/web-app/src/theme/icons/lock.svg
new file mode 100644
index 00000000..38515956
--- /dev/null
+++ b/web-app/src/theme/icons/lock.svg
@@ -0,0 +1 @@
+lock
\ No newline at end of file
diff --git a/web-app/src/theme/icons/person.svg b/web-app/src/theme/icons/person.svg
new file mode 100644
index 00000000..a0764be7
--- /dev/null
+++ b/web-app/src/theme/icons/person.svg
@@ -0,0 +1 @@
+person
\ No newline at end of file
diff --git a/web-app/src/theme/icons/polaroid.svg b/web-app/src/theme/icons/polaroid.svg
new file mode 100644
index 00000000..f0c5b2e4
--- /dev/null
+++ b/web-app/src/theme/icons/polaroid.svg
@@ -0,0 +1,237 @@
+
+
+
+
+Polaroid
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/web-app/src/theme/icons/polaroid_down.svg b/web-app/src/theme/icons/polaroid_down.svg
new file mode 100644
index 00000000..65408d24
--- /dev/null
+++ b/web-app/src/theme/icons/polaroid_down.svg
@@ -0,0 +1 @@
+polaroid_ups
\ No newline at end of file
diff --git a/web-app/src/theme/icons/polaroid_original.svg b/web-app/src/theme/icons/polaroid_original.svg
new file mode 100644
index 00000000..0da2c046
--- /dev/null
+++ b/web-app/src/theme/icons/polaroid_original.svg
@@ -0,0 +1 @@
+polaroid
\ No newline at end of file
diff --git a/web-app/src/theme/icons/second.svg b/web-app/src/theme/icons/second.svg
new file mode 100755
index 00000000..1a88d032
--- /dev/null
+++ b/web-app/src/theme/icons/second.svg
@@ -0,0 +1 @@
+second
\ No newline at end of file
diff --git a/web-app/src/theme/icons/third.svg b/web-app/src/theme/icons/third.svg
new file mode 100755
index 00000000..0a6bc74c
--- /dev/null
+++ b/web-app/src/theme/icons/third.svg
@@ -0,0 +1 @@
+third
\ No newline at end of file
diff --git a/web-app/src/theme/icons/trash.svg b/web-app/src/theme/icons/trash.svg
new file mode 100755
index 00000000..0ca16b56
--- /dev/null
+++ b/web-app/src/theme/icons/trash.svg
@@ -0,0 +1 @@
+red_trash
\ No newline at end of file
diff --git a/web-app/src/theme/icons/verkijker.svg b/web-app/src/theme/icons/verkijker.svg
new file mode 100644
index 00000000..31abb85f
--- /dev/null
+++ b/web-app/src/theme/icons/verkijker.svg
@@ -0,0 +1 @@
+verrekijker_klein
\ No newline at end of file
diff --git a/web-app/src/theme/icons/zoom.svg b/web-app/src/theme/icons/zoom.svg
new file mode 100644
index 00000000..3f5bc454
--- /dev/null
+++ b/web-app/src/theme/icons/zoom.svg
@@ -0,0 +1 @@
+zoom_fixed
\ No newline at end of file
diff --git a/web-app/src/utils/api.js b/web-app/src/utils/api.js
index 84e85327..c23b0984 100644
--- a/web-app/src/utils/api.js
+++ b/web-app/src/utils/api.js
@@ -1,6 +1,8 @@
/* global window */
import axios from 'axios';
+import redirect from './redirect';
+
export const BASE_URL = process.env.REACT_APP_API_URL;
const getToken = () => {
@@ -30,7 +32,7 @@ const abstractRequest = (endpoint, { headers = {}, body, ...otherOptions }, meth
};
const checkForRefreshToken = (endpoint, content, method) => (error) => {
- if (error.response && error.response.status === 401) {
+ if (error.response && error.response.status === 401 && getToken()) {
return abstractRequest('/auth/refresh', {}, 'post').then(({ data }) => {
setToken(data);
@@ -42,16 +44,16 @@ const checkForRefreshToken = (endpoint, content, method) => (error) => {
const checkForRelogin = error => {
if (!error.response || !error.response.data || window.location.pathname === '/login') {
- return Promise.reject(error);
+ return Promise.reject(error.response);
}
const message = error.response.data.error;
- if (['token_expired', 'token_invalid', 'token_not_provided'].includes(message)) {
- window.location = '/login';
+ if (['token_expired', 'token_invalid', 'token_not_provided', 'token_blacklisted', 'user_not_found'].includes(message)) {
+ redirect('/login');
}
- return Promise.reject(error);
+ return Promise.reject(error.response);
};
const request = (endpoint, content, method) => {
diff --git a/web-app/src/utils/isAuthenticated.js b/web-app/src/utils/isAuthenticated.js
new file mode 100644
index 00000000..66053b72
--- /dev/null
+++ b/web-app/src/utils/isAuthenticated.js
@@ -0,0 +1,8 @@
+/* global window */
+const isAuthenticated = () => {
+ const token = window.localStorage.getItem('jwt.token');
+
+ return token !== undefined && token !== null;
+};
+
+export default isAuthenticated;
diff --git a/web-app/src/utils/redirect.js b/web-app/src/utils/redirect.js
new file mode 100644
index 00000000..229b8825
--- /dev/null
+++ b/web-app/src/utils/redirect.js
@@ -0,0 +1,4 @@
+/* global window */
+export default (path) => {
+ window.location = `${process.env.PUBLIC_URL}${path}`;
+};
diff --git a/web-app/src/utils/takeAtLeast.js b/web-app/src/utils/takeAtLeast.js
new file mode 100644
index 00000000..d6e97aed
--- /dev/null
+++ b/web-app/src/utils/takeAtLeast.js
@@ -0,0 +1,9 @@
+const takeAtLeast = (time) => {
+ return new Promise((resolve) => {
+ setTimeout(() => {
+ return resolve();
+ }, time);
+ });
+};
+
+export default takeAtLeast;
diff --git a/web-app/src/utils/validator/rules/index.js b/web-app/src/utils/validator/rules/index.js
index d5b037b5..188f325b 100644
--- a/web-app/src/utils/validator/rules/index.js
+++ b/web-app/src/utils/validator/rules/index.js
@@ -1,9 +1,11 @@
import * as required from './required';
import * as email from './email';
+import * as password from './password';
const rules = {
required,
email,
+ password,
};
export default rules;
diff --git a/web-app/src/utils/validator/rules/password.js b/web-app/src/utils/validator/rules/password.js
new file mode 100644
index 00000000..6056f06c
--- /dev/null
+++ b/web-app/src/utils/validator/rules/password.js
@@ -0,0 +1,5 @@
+export const rule = (input = '') => {
+ return input.length >= 5;
+};
+
+export const message = 'This field is needs to be at least 5 characters';
diff --git a/web-app/yarn.lock b/web-app/yarn.lock
index 78204e54..41d12574 100644
--- a/web-app/yarn.lock
+++ b/web-app/yarn.lock
@@ -44,8 +44,8 @@ acorn@^4.0.3, acorn@^4.0.4:
resolved "https://registry.yarnpkg.com/acorn/-/acorn-4.0.13.tgz#105495ae5361d697bd195c825192e1ad7f253787"
acorn@^5.0.0, acorn@^5.0.1:
- version "5.1.0"
- resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.1.0.tgz#e468bf609b0672700e02f878ae2f1b360fe24b4f"
+ version "5.1.1"
+ resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.1.1.tgz#53fe161111f912ab999ee887a90a0bc52822fd75"
address@1.0.2, address@^1.0.1:
version "1.0.2"
@@ -63,11 +63,11 @@ ajv@^4.7.0, ajv@^4.9.1:
json-stable-stringify "^1.0.1"
ajv@^5.0.0:
- version "5.2.0"
- resolved "https://registry.yarnpkg.com/ajv/-/ajv-5.2.0.tgz#c1735024c5da2ef75cc190713073d44f098bf486"
+ version "5.2.2"
+ resolved "https://registry.yarnpkg.com/ajv/-/ajv-5.2.2.tgz#47c68d69e86f5d953103b0074a9430dc63da5e39"
dependencies:
co "^4.6.0"
- fast-deep-equal "^0.1.0"
+ fast-deep-equal "^1.0.0"
json-schema-traverse "^0.3.0"
json-stable-stringify "^1.0.1"
@@ -237,8 +237,8 @@ arrify@^1.0.0, arrify@^1.0.1:
resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d"
asap@~2.0.3:
- version "2.0.5"
- resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.5.tgz#522765b50c3510490e52d7dcfe085ef9ba96958f"
+ version "2.0.6"
+ resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46"
asn1.js@^4.0.0:
version "4.9.1"
@@ -925,7 +925,7 @@ babel-register@^6.24.1:
mkdirp "^0.5.1"
source-map-support "^0.4.2"
-babel-runtime@6.23.0, babel-runtime@^6.18.0, babel-runtime@^6.22.0:
+babel-runtime@6.23.0, babel-runtime@^6.18.0, babel-runtime@^6.22.0, babel-runtime@^6.6.1:
version "6.23.0"
resolved "https://registry.yarnpkg.com/babel-runtime/-/babel-runtime-6.23.0.tgz#0a9489f144de70efb3ce4300accdb329e2fc543b"
dependencies:
@@ -1132,11 +1132,11 @@ browserslist@^1.3.6, browserslist@^1.5.2, browserslist@^1.7.6:
electron-to-chromium "^1.2.7"
browserslist@^2.1.2, browserslist@^2.1.3:
- version "2.1.5"
- resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-2.1.5.tgz#e882550df3d1cd6d481c1a3e0038f2baf13a4711"
+ version "2.2.1"
+ resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-2.2.1.tgz#709048c57bf3bf9b382105c396a737ad525d948e"
dependencies:
- caniuse-lite "^1.0.30000684"
- electron-to-chromium "^1.3.14"
+ caniuse-lite "^1.0.30000704"
+ electron-to-chromium "^1.3.16"
bser@1.0.2:
version "1.0.2"
@@ -1174,9 +1174,9 @@ builtin-status-codes@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz#85982878e21b98e1c66425e03d0174788f569ee8"
-bytes@2.3.0:
- version "2.3.0"
- resolved "https://registry.yarnpkg.com/bytes/-/bytes-2.3.0.tgz#d5b680a165b6201739acb611542aabc2d8ceb070"
+bytes@2.5.0:
+ version "2.5.0"
+ resolved "https://registry.yarnpkg.com/bytes/-/bytes-2.5.0.tgz#4c9423ea2d252c270c41b2bdefeff9bb6b62c06a"
caller-path@^0.1.0:
version "0.1.0"
@@ -1228,12 +1228,12 @@ caniuse-api@^1.5.2:
lodash.uniq "^4.5.0"
caniuse-db@^1.0.30000529, caniuse-db@^1.0.30000634, caniuse-db@^1.0.30000639:
- version "1.0.30000697"
- resolved "https://registry.yarnpkg.com/caniuse-db/-/caniuse-db-1.0.30000697.tgz#20ce6a9ceeef4ef4a15dc8e80f2e8fb9049e8d77"
+ version "1.0.30000704"
+ resolved "https://registry.yarnpkg.com/caniuse-db/-/caniuse-db-1.0.30000704.tgz#8c5aa6fed8058e65c70f2c1f5d63f7088650705c"
-caniuse-lite@^1.0.30000670, caniuse-lite@^1.0.30000684:
- version "1.0.30000697"
- resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30000697.tgz#125fb00604b63fbb188db96a667ce2922dcd6cdd"
+caniuse-lite@^1.0.30000670, caniuse-lite@^1.0.30000704:
+ version "1.0.30000704"
+ resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30000704.tgz#adb6ea01134515663682db93abab291d4c02946b"
capture-stack-trace@^1.0.0:
version "1.0.0"
@@ -1272,7 +1272,7 @@ chalk@^2.0.1:
escape-string-regexp "^1.0.5"
supports-color "^4.0.0"
-chokidar@^1.4.3, chokidar@^1.6.0:
+chokidar@^1.6.0, chokidar@^1.7.0:
version "1.7.0"
resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-1.7.0.tgz#798e689778151c8076b4b360e5edd28cda2bb468"
dependencies:
@@ -1292,10 +1292,11 @@ ci-info@^1.0.0:
resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-1.0.0.tgz#dc5285f2b4e251821683681c381c3388f46ec534"
cipher-base@^1.0.0, cipher-base@^1.0.1, cipher-base@^1.0.3:
- version "1.0.3"
- resolved "https://registry.yarnpkg.com/cipher-base/-/cipher-base-1.0.3.tgz#eeabf194419ce900da3018c207d212f2a6df0a07"
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/cipher-base/-/cipher-base-1.0.4.tgz#8760e4ecc272f4c363532f926d874aae2c1397de"
dependencies:
inherits "^2.0.1"
+ safe-buffer "^5.0.1"
circular-json@^0.3.1:
version "0.3.1"
@@ -1308,8 +1309,8 @@ clap@^1.0.9:
chalk "^1.1.3"
clean-css@4.1.x:
- version "4.1.5"
- resolved "https://registry.yarnpkg.com/clean-css/-/clean-css-4.1.5.tgz#d09a87a02a5375117589796ae76a063cacdb541a"
+ version "4.1.7"
+ resolved "https://registry.yarnpkg.com/clean-css/-/clean-css-4.1.7.tgz#b9aea4f85679889cf3eae8b40349ec4ebdfdd032"
dependencies:
source-map "0.5.x"
@@ -1358,8 +1359,8 @@ co@^4.6.0:
resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184"
coa@~1.0.1:
- version "1.0.3"
- resolved "https://registry.yarnpkg.com/coa/-/coa-1.0.3.tgz#1b54a5e1dcf77c990455d4deea98c564416dc893"
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/coa/-/coa-1.0.4.tgz#a9ef153660d6a86a8bdec0289a5c684d217432fd"
dependencies:
q "^1.1.2"
@@ -1374,8 +1375,8 @@ color-convert@^1.0.0, color-convert@^1.3.0:
color-name "^1.1.1"
color-name@^1.0.0, color-name@^1.1.1:
- version "1.1.2"
- resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.2.tgz#5c8ab72b64bd2215d617ae9559ebb148475cf98d"
+ version "1.1.3"
+ resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25"
color-string@^0.3.0:
version "0.3.0"
@@ -1419,22 +1420,23 @@ commondir@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b"
-compressible@~2.0.8:
+compressible@~2.0.10:
version "2.0.10"
resolved "https://registry.yarnpkg.com/compressible/-/compressible-2.0.10.tgz#feda1c7f7617912732b29bf8cf26252a20b9eecd"
dependencies:
mime-db ">= 1.27.0 < 2"
compression@^1.5.2:
- version "1.6.2"
- resolved "https://registry.yarnpkg.com/compression/-/compression-1.6.2.tgz#cceb121ecc9d09c52d7ad0c3350ea93ddd402bc3"
+ version "1.7.0"
+ resolved "https://registry.yarnpkg.com/compression/-/compression-1.7.0.tgz#030c9f198f1643a057d776a738e922da4373012d"
dependencies:
accepts "~1.3.3"
- bytes "2.3.0"
- compressible "~2.0.8"
- debug "~2.2.0"
+ bytes "2.5.0"
+ compressible "~2.0.10"
+ debug "2.6.8"
on-headers "~1.0.1"
- vary "~1.1.0"
+ safe-buffer "5.1.1"
+ vary "~1.1.1"
concat-map@0.0.1:
version "0.0.1"
@@ -1521,8 +1523,8 @@ core-util-is@~1.0.0:
resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7"
cosmiconfig@^2.1.0, cosmiconfig@^2.1.1:
- version "2.1.3"
- resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-2.1.3.tgz#952771eb0dddc1cb3fa2f6fbe51a522e93b3ee0a"
+ version "2.2.1"
+ resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-2.2.1.tgz#7fbdc6fb47597d5f88175de1df696b66d36e5944"
dependencies:
is-directory "^0.3.1"
js-yaml "^3.4.3"
@@ -1594,8 +1596,8 @@ cryptiles@2.x.x:
boom "2.x.x"
crypto-browserify@^3.11.0:
- version "3.11.0"
- resolved "https://registry.yarnpkg.com/crypto-browserify/-/crypto-browserify-3.11.0.tgz#3652a0906ab9b2a7e0c3ce66a408e957a2485522"
+ version "3.11.1"
+ resolved "https://registry.yarnpkg.com/crypto-browserify/-/crypto-browserify-3.11.1.tgz#948945efc6757a400d6e5e5af47194d10064279f"
dependencies:
browserify-cipher "^1.0.0"
browserify-sign "^4.0.0"
@@ -1748,12 +1750,6 @@ debug@2.6.8, debug@^2.1.1, debug@^2.2.0, debug@^2.4.5, debug@^2.6.0, debug@^2.6.
dependencies:
ms "2.0.0"
-debug@~2.2.0:
- version "2.2.0"
- resolved "https://registry.yarnpkg.com/debug/-/debug-2.2.0.tgz#f87057e995b1a1f6ae6a4960664137bc56f039da"
- dependencies:
- ms "0.7.1"
-
decamelize@^1.0.0, decamelize@^1.1.1, decamelize@^1.1.2:
version "1.2.0"
resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290"
@@ -1829,6 +1825,10 @@ des.js@^1.0.0:
inherits "^2.0.1"
minimalistic-assert "^1.0.0"
+desandro-matches-selector@^2.0.0:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/desandro-matches-selector/-/desandro-matches-selector-2.0.2.tgz#717beed4dc13e7d8f3762f707a6d58a6774218e1"
+
destroy@~1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80"
@@ -1851,8 +1851,8 @@ detect-port-alt@1.1.3:
debug "^2.6.0"
diff@^3.2.0:
- version "3.2.0"
- resolved "https://registry.yarnpkg.com/diff/-/diff-3.2.0.tgz#c9ce393a4b7cbd0b058a725c93df299027868ff9"
+ version "3.3.0"
+ resolved "https://registry.yarnpkg.com/diff/-/diff-3.3.0.tgz#056695150d7aa93237ca7e378ac3b1682b7963b9"
diffie-hellman@^5.0.0:
version "5.0.2"
@@ -1973,9 +1973,9 @@ ee-first@1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d"
-electron-to-chromium@^1.2.7, electron-to-chromium@^1.3.14:
- version "1.3.15"
- resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.15.tgz#08397934891cbcfaebbd18b82a95b5a481138369"
+electron-to-chromium@^1.2.7, electron-to-chromium@^1.3.16:
+ version "1.3.16"
+ resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.16.tgz#d0e026735754770901ae301a21664cba45d92f7d"
elliptic@^6.0.0:
version "6.4.0"
@@ -1990,8 +1990,8 @@ elliptic@^6.0.0:
minimalistic-crypto-utils "^1.0.0"
emoji-regex@^6.1.0:
- version "6.4.3"
- resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-6.4.3.tgz#6ac2ac58d4b78def5e39b33fcbf395688af3076c"
+ version "6.5.0"
+ resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-6.5.0.tgz#c1699e17f34154f7922219ea712ea76a2619c77b"
emojis-list@^2.0.0:
version "2.1.0"
@@ -2050,8 +2050,8 @@ es-to-primitive@^1.1.1:
is-symbol "^1.0.1"
es5-ext@^0.10.14, es5-ext@^0.10.9, es5-ext@~0.10.14:
- version "0.10.23"
- resolved "https://registry.yarnpkg.com/es5-ext/-/es5-ext-0.10.23.tgz#7578b51be974207a5487821b56538c224e4e7b38"
+ version "0.10.24"
+ resolved "https://registry.yarnpkg.com/es5-ext/-/es5-ext-0.10.24.tgz#a55877c9924bc0c8d9bd3c2cbe17495ac1709b14"
dependencies:
es6-iterator "2"
es6-symbol "~3.1"
@@ -2173,7 +2173,7 @@ eslint-loader@1.7.1:
object-hash "^1.1.4"
rimraf "^2.6.1"
-eslint-module-utils@^2.0.0:
+eslint-module-utils@^2.0.0, eslint-module-utils@^2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.1.1.tgz#abaec824177613b8a95b299639e1b6facf473449"
dependencies:
@@ -2202,15 +2202,15 @@ eslint-plugin-import@2.2.0:
pkg-up "^1.0.0"
eslint-plugin-import@^2.6.1:
- version "2.6.1"
- resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.6.1.tgz#f580be62bb809421d46e338372764afcc9f59bf6"
+ version "2.7.0"
+ resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.7.0.tgz#21de33380b9efb55f5ef6d2e210ec0e07e7fa69f"
dependencies:
builtin-modules "^1.1.1"
contains-path "^0.1.0"
debug "^2.6.8"
doctrine "1.5.0"
eslint-import-resolver-node "^0.3.1"
- eslint-module-utils "^2.0.0"
+ eslint-module-utils "^2.1.1"
has "^1.0.1"
lodash.cond "^4.3.0"
minimatch "^3.0.3"
@@ -2299,9 +2299,9 @@ esprima@^2.6.0, esprima@^2.7.1:
version "2.7.3"
resolved "https://registry.yarnpkg.com/esprima/-/esprima-2.7.3.tgz#96e3b70d5779f6ad49cd032673d1c312767ba581"
-esprima@^3.1.1:
- version "3.1.3"
- resolved "https://registry.yarnpkg.com/esprima/-/esprima-3.1.3.tgz#fdca51cee6133895e3c88d535ce49dbff62a4633"
+esprima@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.0.tgz#4499eddcd1110e0b218bacf2fa7f7f59f55ca804"
esquery@^1.0.0:
version "1.0.0"
@@ -2332,6 +2332,10 @@ etag@~1.8.0:
version "1.8.0"
resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.0.tgz#6f631aef336d6c46362b51764044ce216be3c051"
+ev-emitter@^1.0.1, ev-emitter@^1.0.2:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/ev-emitter/-/ev-emitter-1.1.1.tgz#8f18b0ce5c76a5d18017f71c0a795c65b9138f2a"
+
event-emitter@~0.3.5:
version "0.3.5"
resolved "https://registry.yarnpkg.com/event-emitter/-/event-emitter-0.3.5.tgz#df8c69eef1647923c7157b9ce83840610b02cc39"
@@ -2365,6 +2369,10 @@ exec-sh@^0.2.0:
dependencies:
merge "^1.1.3"
+exenv@1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/exenv/-/exenv-1.2.0.tgz#3835f127abf075bfe082d0aed4484057c78e3c89"
+
exit-hook@^1.0.0:
version "1.1.1"
resolved "https://registry.yarnpkg.com/exit-hook/-/exit-hook-1.1.1.tgz#f05ca233b48c05d54fff07765df8507e95c02ff8"
@@ -2445,9 +2453,9 @@ extsprintf@1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.0.2.tgz#e1080e0658e300b06294990cc70e1502235fd550"
-fast-deep-equal@^0.1.0:
- version "0.1.0"
- resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-0.1.0.tgz#5c6f4599aba6b333ee3342e2ed978672f1001f8d"
+fast-deep-equal@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-1.0.0.tgz#96256a3bc975595eb36d82e9929d060d893439ff"
fast-levenshtein@~2.0.4:
version "2.0.6"
@@ -2581,6 +2589,12 @@ find-up@^2.0.0, find-up@^2.1.0:
dependencies:
locate-path "^2.0.0"
+fizzy-ui-utils@^2.0.0:
+ version "2.0.5"
+ resolved "https://registry.yarnpkg.com/fizzy-ui-utils/-/fizzy-ui-utils-2.0.5.tgz#d72debc74d2c9d272dbcbb7b001707897f6c3210"
+ dependencies:
+ desandro-matches-selector "^2.0.0"
+
flat-cache@^1.2.1:
version "1.2.2"
resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-1.2.2.tgz#fa86714e72c21db88601761ecf2f555d1abc6b96"
@@ -2594,6 +2608,17 @@ flatten@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/flatten/-/flatten-1.0.2.tgz#dae46a9d78fbe25292258cc1e780a41d95c03782"
+flickity@^2.0.9:
+ version "2.0.9"
+ resolved "https://registry.yarnpkg.com/flickity/-/flickity-2.0.9.tgz#5932d33d4fe009c66621644d3f373400f90e7695"
+ dependencies:
+ desandro-matches-selector "^2.0.0"
+ ev-emitter "^1.0.2"
+ fizzy-ui-utils "^2.0.0"
+ get-size "^2.0.0"
+ tap-listener "^2.0.0"
+ unidragger "^2.2.3"
+
follow-redirects@^1.2.3:
version "1.2.4"
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.2.4.tgz#355e8f4d16876b43f577b0d5ce2668b9723214ea"
@@ -2721,6 +2746,10 @@ get-caller-file@^1.0.1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-1.0.2.tgz#f702e63127e7e231c160a80c1554acb70d5047e5"
+get-size@^2.0.0:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/get-size/-/get-size-2.0.2.tgz#555ea98ab8732e0c021e9e23e2219adcbe398e98"
+
get-stdin@^4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-4.0.1.tgz#b968c6b0a04384324902e8bf1a5df32579a450fe"
@@ -2826,6 +2855,10 @@ gzip-size@3.0.0:
dependencies:
duplexer "^0.1.1"
+hammerjs@^2.0.4:
+ version "2.0.8"
+ resolved "https://registry.yarnpkg.com/hammerjs/-/hammerjs-2.0.8.tgz#04ef77862cff2bb79d30f7692095930222bf60f1"
+
handle-thing@^1.2.5:
version "1.2.5"
resolved "https://registry.yarnpkg.com/handle-thing/-/handle-thing-1.2.5.tgz#fd7aad726bf1a5fd16dfc29b2f7a6601d27139c4"
@@ -3404,14 +3437,14 @@ isstream@~0.1.2:
resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a"
istanbul-api@^1.1.1:
- version "1.1.10"
- resolved "https://registry.yarnpkg.com/istanbul-api/-/istanbul-api-1.1.10.tgz#f27e5e7125c8de13f6a80661af78f512e5439b2b"
+ version "1.1.11"
+ resolved "https://registry.yarnpkg.com/istanbul-api/-/istanbul-api-1.1.11.tgz#fcc0b461e2b3bda71e305155138238768257d9de"
dependencies:
async "^2.1.4"
fileset "^2.0.2"
istanbul-lib-coverage "^1.1.1"
istanbul-lib-hook "^1.0.7"
- istanbul-lib-instrument "^1.7.3"
+ istanbul-lib-instrument "^1.7.4"
istanbul-lib-report "^1.1.1"
istanbul-lib-source-maps "^1.2.1"
istanbul-reports "^1.1.1"
@@ -3429,9 +3462,9 @@ istanbul-lib-hook@^1.0.7:
dependencies:
append-transform "^0.4.0"
-istanbul-lib-instrument@^1.4.2, istanbul-lib-instrument@^1.7.2, istanbul-lib-instrument@^1.7.3:
- version "1.7.3"
- resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-1.7.3.tgz#925b239163eabdd68cc4048f52c2fa4f899ecfa7"
+istanbul-lib-instrument@^1.4.2, istanbul-lib-instrument@^1.7.2, istanbul-lib-instrument@^1.7.4:
+ version "1.7.4"
+ resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-1.7.4.tgz#e9fd920e4767f3d19edc765e2d6b3f5ccbd0eea8"
dependencies:
babel-generator "^6.18.0"
babel-template "^6.16.0"
@@ -3549,8 +3582,8 @@ jest-environment-node@^20.0.3:
jest-util "^20.0.3"
jest-haste-map@^20.0.4:
- version "20.0.4"
- resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-20.0.4.tgz#653eb55c889ce3c021f7b94693f20a4159badf03"
+ version "20.0.5"
+ resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-20.0.5.tgz#abad74efb1a005974a7b6517e11010709cab9112"
dependencies:
fb-watchman "^2.0.0"
graceful-fs "^4.1.11"
@@ -3686,11 +3719,11 @@ js-tokens@^3.0.0:
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b"
js-yaml@^3.4.3, js-yaml@^3.5.1, js-yaml@^3.7.0:
- version "3.8.4"
- resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.8.4.tgz#520b4564f86573ba96662af85a8cafa7b4b5a6f6"
+ version "3.9.0"
+ resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.9.0.tgz#4ffbbf25c2ac963b8299dc74da7e3740de1c18ce"
dependencies:
argparse "^1.0.7"
- esprima "^3.1.1"
+ esprima "^4.0.0"
js-yaml@~3.7.0:
version "3.7.0"
@@ -3704,8 +3737,8 @@ jsbn@~0.1.0:
resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513"
jschardet@^1.4.2:
- version "1.4.2"
- resolved "https://registry.yarnpkg.com/jschardet/-/jschardet-1.4.2.tgz#2aa107f142af4121d145659d44f50830961e699a"
+ version "1.5.0"
+ resolved "https://registry.yarnpkg.com/jschardet/-/jschardet-1.5.0.tgz#a61f310306a5a71188e1b1acd08add3cfbb08b1e"
jsdom@^9.12.0:
version "9.12.0"
@@ -3740,8 +3773,8 @@ jsesc@~0.5.0:
resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-0.5.0.tgz#e7dee66e35d6fc16f710fe91d5cf69f70f08911d"
json-loader@^0.5.4:
- version "0.5.4"
- resolved "https://registry.yarnpkg.com/json-loader/-/json-loader-0.5.4.tgz#8baa1365a632f58a3c46d20175fc6002c96e37de"
+ version "0.5.6"
+ resolved "https://registry.yarnpkg.com/json-loader/-/json-loader-0.5.6.tgz#51697650c63f7d17e21561582117282e1e711db8"
json-schema-traverse@^0.3.0:
version "0.3.1"
@@ -3958,7 +3991,7 @@ lodash.uniq@^4.5.0:
version "4.5.0"
resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773"
-"lodash@>=3.5 <5", lodash@^4.0.0, lodash@^4.14.0, lodash@^4.15.0, lodash@^4.17.2, lodash@^4.17.3, lodash@^4.17.4, lodash@^4.2.0, lodash@^4.2.1, lodash@^4.3.0, lodash@~4.17.4:
+"lodash@>=3.5 <5", lodash@^4.0.0, lodash@^4.14.0, lodash@^4.15.0, lodash@^4.17.2, lodash@^4.17.3, lodash@^4.17.4, lodash@^4.2.0, lodash@^4.2.1, lodash@^4.3.0, lodash@^4.6.1, lodash@~4.17.4:
version "4.17.4"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.4.tgz#78203a4d1c328ae1d86dca6460e369b57f4055ae"
@@ -4075,11 +4108,7 @@ miller-rabin@^4.0.0:
bn.js "^4.0.0"
brorand "^1.0.1"
-"mime-db@>= 1.27.0 < 2":
- version "1.28.0"
- resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.28.0.tgz#fedd349be06d2865b7fc57d837c6de4f17d7ac3c"
-
-mime-db@~1.27.0:
+"mime-db@>= 1.27.0 < 2", mime-db@~1.27.0:
version "1.27.0"
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.27.0.tgz#820f572296bbd20ec25ed55e5b5de869e5436eb1"
@@ -4135,10 +4164,6 @@ mkdirp@0.5.1, mkdirp@0.5.x, "mkdirp@>=0.5 0", mkdirp@^0.5.0, mkdirp@^0.5.1, mkdi
dependencies:
minimist "0.0.8"
-ms@0.7.1:
- version "0.7.1"
- resolved "https://registry.yarnpkg.com/ms/-/ms-0.7.1.tgz#9cd13c03adbff25b65effde7ce864ee952017098"
-
ms@2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
@@ -4606,7 +4631,7 @@ pbkdf2@^3.0.3:
safe-buffer "^5.0.1"
sha.js "^2.4.8"
-performance-now@^0.2.0:
+performance-now@^0.2.0, performance-now@~0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-0.2.0.tgz#33ef30c5c77d4ea21c5a53869d91b56d8f2555e5"
@@ -4929,12 +4954,12 @@ postcss@^5.0.10, postcss@^5.0.11, postcss@^5.0.12, postcss@^5.0.13, postcss@^5.0
supports-color "^3.2.3"
postcss@^6.0.1, postcss@^6.0.2:
- version "6.0.6"
- resolved "https://registry.yarnpkg.com/postcss/-/postcss-6.0.6.tgz#bba4d58e884fc78c840d1539e10eddaabb8f73bd"
+ version "6.0.8"
+ resolved "https://registry.yarnpkg.com/postcss/-/postcss-6.0.8.tgz#89067a9ce8b11f8a84cbc5117efc30419a0857b3"
dependencies:
chalk "^2.0.1"
source-map "^0.5.6"
- supports-color "^4.1.0"
+ supports-color "^4.2.0"
prelude-ls@~1.1.2:
version "1.1.2"
@@ -5059,6 +5084,12 @@ querystringify@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-1.0.0.tgz#6286242112c5b712fa654e526652bf6a13ff05cb"
+raf@^3.1.0:
+ version "3.2.0"
+ resolved "https://registry.yarnpkg.com/raf/-/raf-3.2.0.tgz#2aba9a09bb70f8c2e9094dc60ace7c374fadec7e"
+ dependencies:
+ performance-now "~0.2.0"
+
randomatic@^1.1.3:
version "1.1.7"
resolved "https://registry.yarnpkg.com/randomatic/-/randomatic-1.1.7.tgz#c7abe9cc8b87c0baa876b19fde83fd464797e38c"
@@ -5108,6 +5139,10 @@ react-dev-utils@^3.0.2:
strip-ansi "3.0.1"
text-table "0.2.0"
+react-dom-factories@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/react-dom-factories/-/react-dom-factories-1.0.0.tgz#f43c05e5051b304f33251618d5bc859b29e46b6d"
+
react-dom@^15.6.1:
version "15.6.1"
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-15.6.1.tgz#2cb0ed4191038e53c209eb3a79a23e2a4cf99470"
@@ -5128,6 +5163,14 @@ react-error-overlay@^1.0.9:
settle-promise "1.0.0"
source-map "0.5.6"
+react-modal@^2.2.2:
+ version "2.2.2"
+ resolved "https://registry.yarnpkg.com/react-modal/-/react-modal-2.2.2.tgz#4bbf98bc506e61c446c9f57329c7a488ea7d504b"
+ dependencies:
+ exenv "1.2.0"
+ prop-types "^15.5.10"
+ react-dom-factories "^1.0.0"
+
react-redux@^5.0.5:
version "5.0.5"
resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-5.0.5.tgz#f8e8c7b239422576e52d6b7db06439469be9846a"
@@ -5141,8 +5184,8 @@ react-redux@^5.0.5:
prop-types "^15.5.10"
react-router-dom@^4.1.1:
- version "4.1.1"
- resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-4.1.1.tgz#3021ade1f2c160af97cf94e25594c5f294583025"
+ version "4.1.2"
+ resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-4.1.2.tgz#7f8a7ca868d32acadd19ca09543b40d26df8ec37"
dependencies:
history "^4.5.1"
loose-envify "^1.3.1"
@@ -5150,8 +5193,8 @@ react-router-dom@^4.1.1:
react-router "^4.1.1"
react-router@^4.1.1:
- version "4.1.1"
- resolved "https://registry.yarnpkg.com/react-router/-/react-router-4.1.1.tgz#d448f3b7c1b429a6fbb03395099949c606b1fe95"
+ version "4.1.2"
+ resolved "https://registry.yarnpkg.com/react-router/-/react-router-4.1.2.tgz#7ae027341abc42eb08ad9f7a8cac08d0563672ce"
dependencies:
history "^4.6.0"
hoist-non-react-statics "^1.2.0"
@@ -5204,6 +5247,17 @@ react-scripts@1.0.10:
optionalDependencies:
fsevents "1.1.2"
+react-swing@^0.0.7:
+ version "0.0.7"
+ resolved "https://registry.yarnpkg.com/react-swing/-/react-swing-0.0.7.tgz#b458242b1cd8480c282b0af28a9628e4723e1c7f"
+ dependencies:
+ babel-runtime "^6.6.1"
+ swing "^3.0.3"
+
+react-transitive-number@^3.0.1:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/react-transitive-number/-/react-transitive-number-3.0.1.tgz#ebee94ede712db6ab7d8163d896fddd5575b09ab"
+
react@^15.6.1:
version "15.6.1"
resolved "https://registry.yarnpkg.com/react/-/react-15.6.1.tgz#baa8434ec6780bde997cdc380b79cd33b96393df"
@@ -5289,6 +5343,10 @@ readline2@^1.0.1:
is-fullwidth-code-point "^1.0.0"
mute-stream "0.0.5"
+rebound@^0.0.13:
+ version "0.0.13"
+ resolved "https://registry.yarnpkg.com/rebound/-/rebound-0.0.13.tgz#4a225254caf7da756797b19c5817bf7a7941fac1"
+
rechoir@^0.6.2:
version "0.6.2"
resolved "https://registry.yarnpkg.com/rechoir/-/rechoir-0.6.2.tgz#85204b54dba82d5742e28c96756ef43af50e3384"
@@ -5327,8 +5385,8 @@ redux-thunk@^2.2.0:
resolved "https://registry.yarnpkg.com/redux-thunk/-/redux-thunk-2.2.0.tgz#e615a16e16b47a19a515766133d1e3e99b7852e5"
redux@^3.7.1:
- version "3.7.1"
- resolved "https://registry.yarnpkg.com/redux/-/redux-3.7.1.tgz#bfc535c757d3849562ead0af18ac52122cd7268e"
+ version "3.7.2"
+ resolved "https://registry.yarnpkg.com/redux/-/redux-3.7.2.tgz#06b73123215901d25d065be342eb026bc1c8537b"
dependencies:
lodash "^4.2.1"
lodash-es "^4.2.1"
@@ -5556,7 +5614,7 @@ rx-lite@^3.1.2:
version "3.1.2"
resolved "https://registry.yarnpkg.com/rx-lite/-/rx-lite-3.1.2.tgz#19ce502ca572665f3b647b10939f97fd1615f102"
-safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@~5.1.0, safe-buffer@~5.1.1:
+safe-buffer@5.1.1, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@~5.1.0, safe-buffer@~5.1.1:
version "5.1.1"
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.1.tgz#893312af69b2123def71f57889001671eeb2c853"
@@ -5712,6 +5770,10 @@ signal-exit@^3.0.0, signal-exit@^3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d"
+sister@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/sister/-/sister-3.0.0.tgz#88eb57047cb283da1e070b1a7cae8212f3bcb2db"
+
slash@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/slash/-/slash-1.0.0.tgz#c41f2f6c39fc16d1cd17ad4b5d896114ae470d55"
@@ -5900,8 +5962,8 @@ string-width@^1.0.1, string-width@^1.0.2:
strip-ansi "^3.0.0"
string-width@^2.0.0:
- version "2.1.0"
- resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.0.tgz#030664561fc146c9423ec7d978fe2457437fe6d0"
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e"
dependencies:
is-fullwidth-code-point "^2.0.0"
strip-ansi "^4.0.0"
@@ -5969,9 +6031,9 @@ supports-color@^3.1.0, supports-color@^3.1.1, supports-color@^3.1.2, supports-co
dependencies:
has-flag "^1.0.0"
-supports-color@^4.0.0, supports-color@^4.1.0:
- version "4.1.0"
- resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-4.1.0.tgz#92cc14bb3dad8928ca5656c33e19a19f20af5c7a"
+supports-color@^4.0.0, supports-color@^4.2.0:
+ version "4.2.1"
+ resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-4.2.1.tgz#65a4bb2631e90e02420dba5554c375a4754bb836"
dependencies:
has-flag "^2.0.0"
@@ -6017,6 +6079,17 @@ sw-toolbox@^3.4.0:
path-to-regexp "^1.0.1"
serviceworker-cache-polyfill "^4.0.0"
+swing@^3.0.3, swing@^3.1.1:
+ version "3.1.1"
+ resolved "https://registry.yarnpkg.com/swing/-/swing-3.1.1.tgz#ec6cb58b42891d7593686aa9910cc936188b0c89"
+ dependencies:
+ hammerjs "^2.0.4"
+ lodash "^4.6.1"
+ raf "^3.1.0"
+ rebound "^0.0.13"
+ sister "^3.0.0"
+ vendor-prefix "^0.1.0"
+
symbol-observable@^1.0.3:
version "1.0.4"
resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.0.4.tgz#29bf615d4aa7121bdd898b22d4b3f9bc4e2aa03d"
@@ -6036,9 +6109,15 @@ table@^3.7.8:
slice-ansi "0.0.4"
string-width "^2.0.0"
+tap-listener@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/tap-listener/-/tap-listener-2.0.0.tgz#d2e11d0d8e7c92b26a56ae4f35538a248b44ad63"
+ dependencies:
+ unipointer "^2.1.0"
+
tapable@^0.2.5, tapable@~0.2.5:
- version "0.2.6"
- resolved "https://registry.yarnpkg.com/tapable/-/tapable-0.2.6.tgz#206be8e188860b514425375e6f1ae89bfb01fd8d"
+ version "0.2.7"
+ resolved "https://registry.yarnpkg.com/tapable/-/tapable-0.2.7.tgz#e46c0daacbb2b8a98b9b0cea0f4052105817ed5c"
tar-pack@^3.4.0:
version "3.4.0"
@@ -6173,12 +6252,12 @@ typedarray@^0.0.6:
resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
ua-parser-js@^0.7.9:
- version "0.7.13"
- resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.13.tgz#cd9dd2f86493b3f44dbeeef3780fda74c5ee14be"
+ version "0.7.14"
+ resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.14.tgz#110d53fa4c3f326c121292bbeac904d2e03387ca"
uglify-js@3.0.x, uglify-js@^3.0.13:
- version "3.0.23"
- resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.0.23.tgz#a58c6b97e6d6763d94dbc265fe8e8c1725e64666"
+ version "3.0.25"
+ resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.0.25.tgz#3dc190b0ee437497e449bc6f785665b06afbe052"
dependencies:
commander "~2.9.0"
source-map "~0.5.1"
@@ -6200,6 +6279,18 @@ uid-number@^0.0.6:
version "0.0.6"
resolved "https://registry.yarnpkg.com/uid-number/-/uid-number-0.0.6.tgz#0ea10e8035e8eb5b8e4449f06da1c730663baa81"
+unidragger@^2.2.3:
+ version "2.2.3"
+ resolved "https://registry.yarnpkg.com/unidragger/-/unidragger-2.2.3.tgz#a618801b9b629e1045e3d3793713e7add0a1e16b"
+ dependencies:
+ unipointer "^2.2.0"
+
+unipointer@^2.1.0, unipointer@^2.2.0:
+ version "2.2.0"
+ resolved "https://registry.yarnpkg.com/unipointer/-/unipointer-2.2.0.tgz#3f6cf997f7d7dc78f75385106a5527efc5d71b61"
+ dependencies:
+ ev-emitter "^1.0.1"
+
uniq@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/uniq/-/uniq-1.0.1.tgz#b31c5ae8254844a3a8281541ce2b04b865a734ff"
@@ -6215,8 +6306,8 @@ uniqs@^2.0.0:
resolved "https://registry.yarnpkg.com/uniqs/-/uniqs-2.0.0.tgz#ffede4b36b25290696e6e165d4a59edb998e6b02"
universalify@^0.1.0:
- version "0.1.0"
- resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.0.tgz#9eb1c4651debcc670cc94f1a75762332bb967778"
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.1.tgz#fa71badd4437af4c148841e3b3b165f9e9e590b7"
unpipe@~1.0.0:
version "1.0.0"
@@ -6328,10 +6419,14 @@ value-equal@^0.2.0:
version "0.2.1"
resolved "https://registry.yarnpkg.com/value-equal/-/value-equal-0.2.1.tgz#c220a304361fce6994dbbedaa3c7e1a1b895871d"
-vary@~1.1.0, vary@~1.1.1:
+vary@~1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.1.tgz#67535ebb694c1d52257457984665323f587e8d37"
+vendor-prefix@^0.1.0:
+ version "0.1.0"
+ resolved "https://registry.yarnpkg.com/vendor-prefix/-/vendor-prefix-0.1.0.tgz#d7be53ab6e4879fb11f9ea33696e59c7dc29497c"
+
vendors@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/vendors/-/vendors-1.0.1.tgz#37ad73c8ee417fb3d580e785312307d274847f22"
@@ -6365,11 +6460,11 @@ watch@~0.10.0:
resolved "https://registry.yarnpkg.com/watch/-/watch-0.10.0.tgz#77798b2da0f9910d595f1ace5b0c2258521f21dc"
watchpack@^1.3.1:
- version "1.3.1"
- resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-1.3.1.tgz#7d8693907b28ce6013e7f3610aa2a1acf07dad87"
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-1.4.0.tgz#4a1472bcbb952bd0a9bb4036801f954dfb39faac"
dependencies:
async "^2.1.2"
- chokidar "^1.4.3"
+ chokidar "^1.7.0"
graceful-fs "^4.1.2"
wbuf@^1.1.0, wbuf@^1.7.2:
diff --git a/website/app/index.html b/website/app/index.html
new file mode 100644
index 00000000..e5d7fb59
--- /dev/null
+++ b/website/app/index.html
@@ -0,0 +1,92 @@
+
+
+ birds.today
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Spot birds from your couch
+
Help Bert to spot birds
+
+
+
+
+
+
Locations
+
Spot birds on locations you could never visit, until now.
+
+
+
+
Speed
+
Tired of waiting hours to spot that special bird? That's over now!
+
+
+
+
Community
+
Be a part of something larger while supporting the research on biodiversity.
+
+
+
+
+
+ Help Bert now!
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/website/app/stylesheet.css b/website/app/stylesheet.css
new file mode 100644
index 00000000..92cca6c6
--- /dev/null
+++ b/website/app/stylesheet.css
@@ -0,0 +1,81 @@
+html, body {
+ line-height: 30px;
+ font-family: "Roboto", serif;
+ height: 100%;
+ width: 100%;
+ margin: 0;
+}
+
+h1, h2, h3, h4, h5, h6 {
+ font-family: "Chivo", serif;
+}
+
+a {
+ text-decoration: none;
+}
+
+.bg-black {
+ background-color: #1D1D1D;
+}
+
+.text-white, .text-hover-white .text-hover-white2 {
+ color: #FFFFFF;
+}
+
+.text-hover-white:hover {
+ color: #1D1D1D;
+}
+
+.text-hover-white2:hover {
+ color: #929493;
+}
+
+.text-black, .text-hover-black {
+ color: #1D1D1D;
+}
+
+.text-hover-black:hover {
+ color: #FFFFFF;
+}
+
+.page {
+ width: 100%;
+ min-height: 100%;
+}
+
+.features {
+ width: 33%;
+}
+
+.box {
+ background-color: #67A0D6;
+ color: white;
+}
+
+.icon {
+ font-size: 100px;
+}
+
+.button {
+ min-height: 10%;
+ line-height: 100px;
+ border-radius: 3px;
+ margin-left: auto;
+ margin-right: auto;
+}
+
+.social {
+ width: 20%;
+ margin: auto;
+}
+
+.w3-display-middle {
+ white-space: nowrap;
+}
+
+/* Turn off parallax scrolling for tablets and phones */
+@media only screen and (max-device-width: 1024px) {
+ .page {
+ background-attachment: scroll;
+ }
+}
diff --git a/website/developers/images/hardware.png b/website/developers/images/hardware.png
new file mode 100644
index 00000000..86d10590
Binary files /dev/null and b/website/developers/images/hardware.png differ
diff --git a/website/developers/images/logo.svg b/website/developers/images/logo.svg
new file mode 100644
index 00000000..8d9ec581
--- /dev/null
+++ b/website/developers/images/logo.svg
@@ -0,0 +1 @@
+Bird_logo
\ No newline at end of file
diff --git a/website/developers/images/opensource.png b/website/developers/images/opensource.png
new file mode 100644
index 00000000..b8ae4661
Binary files /dev/null and b/website/developers/images/opensource.png differ
diff --git a/website/developers/images/webapp.png b/website/developers/images/webapp.png
new file mode 100644
index 00000000..d48909d3
Binary files /dev/null and b/website/developers/images/webapp.png differ
diff --git a/website/developers/index.html b/website/developers/index.html
new file mode 100644
index 00000000..0806089b
--- /dev/null
+++ b/website/developers/index.html
@@ -0,0 +1,99 @@
+
+
+ #code9000
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
The Future
+
Providing an IoT device that keeps track of the biodiversity in your area.
+
+
+
+
+
+
+
+
+
+
+
+
+
Web app
+
An easy to use platform to validate your data in a fun way.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
100% open source
+
Everything we make is available for anyone to reuse and is easy to set up.
+
+
+
+
+
+
+
Contribute today!
+
Fix an annoying bug or add new awesome features for our app, IoT, ... on Github.
+
Github
+
+
+
+
+
+
diff --git a/website/developers/stylesheet.css b/website/developers/stylesheet.css
new file mode 100644
index 00000000..a42ffa94
--- /dev/null
+++ b/website/developers/stylesheet.css
@@ -0,0 +1,83 @@
+body,h1,h2,h3,h4,h5,h6, p, a {
+ font-family: "Chivo", sans-serif !important;
+}
+
+body, html {
+ height: 100%;
+ width: 100%;
+ color: #6C6E78;
+ line-height: 1.0;
+}
+
+a {
+ text-decoration: none;
+}
+
+.bg-white {
+ background: #FFFFFF;
+}
+
+.bg-black {
+ background-color: #1D1D1D;
+}
+
+.text-white, .text-hover-white {
+ color: #FFFFFF;
+}
+
+.text-hover-white:hover {
+ color: #1D1D1D;
+}
+
+.text-black, .text-hover-black {
+ color: #1D1D1D;
+}
+
+.text-hover-black:hover {
+ color: #FFFFFF;
+}
+
+/* First image (Logo. Full height) */
+.page {
+ height: 100%;
+}
+
+.map {
+ position: absolute;
+ width: 100%;
+ height: 100%;
+ pointer-events: none;
+ z-index: -1;
+}
+
+.feature {
+ margin-left: 3%;
+ margin-right: 3%;
+ height: 45%;
+}
+
+.featureItem, .featureChild, .featureImage { /*Give childs access to %height*/
+ height: 100%;
+}
+
+.featureImage { /* Center image*/
+ display: block;
+ margin: auto;
+}
+
+.center {
+ padding-top: 10%;
+ padding-bottom: 10%;
+ text-align: center;
+}
+
+.w3-display-middle {
+ white-space: nowrap;
+}
+
+/* Turn off parallax scrolling for tablets and phones */
+@media only screen and (max-device-width: 1024px) {
+ .page {
+ background-attachment: scroll;
+ }
+}