0;){var m=String(s),g=String(a),E=b(n,m);if(E){var w=n[m];n[g]=w}else delete n[g];s+=v,a+=v,d-=1}return n});var yt="entries"in Array.prototype&&"next"in[].entries();o(Array.prototype,"entries",function(){return x(this,"key+value")},!yt),o(Array.prototype,"fill",function(t){var e=arguments[1],r=arguments[2],n=f(this),o=n.length,i=l(o);i=K(i,0);var a,u=c(e);a=u<0?K(i+u,0):J(u,i);var s;s=r===k?i:c(r);var p;for(p=s<0?K(i+s,0):J(s,i);a1?arguments[1]:k,i=0;i1?arguments[1]:k,i=0;i=l)return i(t,"[[ArrayIteratorNextIndex]]",1/0),j(k,!0);if(r=a,i(t,"[[ArrayIteratorNextIndex]]",a+1),c.indexOf("value")!==-1&&(n=o[r]),c.indexOf("key+value")!==-1)return j([r,n],!1);if(c.indexOf("key")!==-1)return j(r,!1);if("value"===c)return j(n,!1);throw Error("Internal error")}),o(vt,lt,"Array Iterator"),["Int8Array","Uint8Array","Uint8ClampedArray","Int16Array","Uint16Array","Int32Array","Uint32Array","Float32Array","Float64Array"].forEach(function(r){if(r in t){var n=t[r];o(n,"from",function(t){var r=arguments[1],n=arguments[2],o=e(this);if(!h(o))throw TypeError();if(r===k)var i=!1;else{if(p(r))throw TypeError();var a=n;i=!0}var u=m(t,at);if(u!==k){for(var c=E(t,u),s=[],y=!0;y!==!1;)if(y=T(c),y!==!1){var v=S(y);s.push(v)}for(var d=s.length,b=new o(d),g=0;gr?1:0}var e=arguments[0];return Array.prototype.sort.call(this,t)}),o(n.prototype,"values",Array.prototype.values),o(n.prototype,at,n.prototype.values),o(n.prototype,lt,r)}}),function(){function r(){var t=e(this),r=arguments[0];if("object"!==u(t))throw TypeError();if("[[MapData]]"in t)throw TypeError();if(r!==k){var n=t.set;if(!p(n))throw TypeError();var o=E(f(r))}if(i(t,"[[MapData]]",{keys:[],values:[]}),o===k)return t;for(;;){var a=T(o);if(a===!1)return t;var c=S(a);if("object"!==u(c))throw TypeError();var s=c[0],l=c[1];n.call(t,s,l)}return t}function n(t,e){var r;if(e===e)return t.keys.indexOf(e);for(r=0;r=0?o.values[i]:k}),o(r.prototype,"has",function(t){var r=e(this);if("object"!==u(r))throw TypeError();if(!("[[MapData]]"in r))throw TypeError();if(r["[[MapData]]"]===k)throw TypeError();var o=r["[[MapData]]"];return n(o,t)>=0}),o(r.prototype,"keys",function(){var t=e(this);if("object"!==u(t))throw TypeError();return c(t,"key")}),o(r.prototype,"set",function(t,r){var o=e(this);if("object"!==u(o))throw TypeError();if(!("[[MapData]]"in o))throw TypeError();if(o["[[MapData]]"]===k)throw TypeError();var i=o["[[MapData]]"],a=n(i,t);return a<0&&(a=i.keys.length),y(t,-0)&&(t=0),i.keys[a]=t,i.values[a]=r,o}),Object.defineProperty(r.prototype,"size",{get:function(){var t=e(this);if("object"!==u(t))throw TypeError();if(!("[[MapData]]"in t))throw TypeError();if(t["[[MapData]]"]===k)throw TypeError();for(var r=t["[[MapData]]"],n=0,o=0;o=0)var s=c;else s=u+c,s<0&&(s=0);for(;s>16&255)),o.push(String.fromCharCode(i>>8&255)),o.push(String.fromCharCode(255&i)),a=0,i=0),r+=1;return 12===a?(i>>=4,o.push(String.fromCharCode(255&i))):18===a&&(i>>=2,o.push(String.fromCharCode(i>>8&255)),o.push(String.fromCharCode(255&i))),o.join("")}function r(t){t=String(t);var e,r,o,i,a,u,c,s=0,f=[];if(/[^\x00-\xFF]/.test(t))throw Error("InvalidCharacterError");for(;s>2,a=(3&e)<<4|r>>4,u=(15&r)<<2|o>>6,c=63&o,s===t.length+2?(u=64,c=64):s===t.length+1&&(c=64),f.push(n.charAt(i),n.charAt(a),n.charAt(u),n.charAt(c));return f.join("")}if(!("atob"in t&&"btoa"in t)){var n="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";t.atob=e,t.btoa=r}}(),function(){function e(t){return t.offsetWidth>0&&t.offsetHeight>0}function r(){var t=a;a=Object.create(null),c=-1,Object.keys(t).forEach(function(r){var n=t[r];n.element&&!e(n.element)||n.callback(Date.now())})}function n(e,n){var o=++u;return a[o]={callback:e,element:n},c===-1&&(c=t.setTimeout(r,1e3/i)),o}function o(e){delete a[e],0===Object.keys(a).length&&(t.clearTimeout(c),c=-1)}if(!("requestAnimationFrame"in t)){var i=60,a=Object.create(null),u=0,c=-1;t.requestAnimationFrame=n,t.cancelAnimationFrame=o}}())}(self),function(t){"use strict";function e(t,e){t&&Object.keys(e).forEach(function(r){if(!(r in t||r in t.prototype))try{Object.defineProperty(t.prototype,r,Object.getOwnPropertyDescriptor(e,r))}catch(n){t[r]=e[r]}})}function r(t){var e=null;return t=t.map(function(t){return t instanceof Node?t:document.createTextNode(t)}),1===t.length?e=t[0]:(e=document.createDocumentFragment(),t.forEach(function(t){e.appendChild(t)})),e}if("window"in t&&"document"in t){document.querySelectorAll||(document.querySelectorAll=function(t){var e,r=document.createElement("style"),n=[];for(document.documentElement.firstChild.appendChild(r),document._qsa=[],r.styleSheet.cssText=t+"{x-qsa:expression(document._qsa && document._qsa.push(this))}",window.scrollBy(0,0),r.parentNode.removeChild(r);document._qsa.length;)e=document._qsa.shift(),e.style.removeAttribute("x-qsa"),n.push(e);return document._qsa=null,n}),document.querySelector||(document.querySelector=function(t){var e=document.querySelectorAll(t);return e.length?e[0]:null}),document.getElementsByClassName||(document.getElementsByClassName=function(t){return t=String(t).replace(/^|\s+/g,"."),document.querySelectorAll(t)}),t.Node=t.Node||function(){throw TypeError("Illegal constructor")},[["ELEMENT_NODE",1],["ATTRIBUTE_NODE",2],["TEXT_NODE",3],["CDATA_SECTION_NODE",4],["ENTITY_REFERENCE_NODE",5],["ENTITY_NODE",6],["PROCESSING_INSTRUCTION_NODE",7],["COMMENT_NODE",8],["DOCUMENT_NODE",9],["DOCUMENT_TYPE_NODE",10],["DOCUMENT_FRAGMENT_NODE",11],["NOTATION_NODE",12]].forEach(function(e){e[0]in t.Node||(t.Node[e[0]]=e[1])}),t.DOMException=t.DOMException||function(){throw TypeError("Illegal constructor")},[["INDEX_SIZE_ERR",1],["DOMSTRING_SIZE_ERR",2],["HIERARCHY_REQUEST_ERR",3],["WRONG_DOCUMENT_ERR",4],["INVALID_CHARACTER_ERR",5],["NO_DATA_ALLOWED_ERR",6],["NO_MODIFICATION_ALLOWED_ERR",7],["NOT_FOUND_ERR",8],["NOT_SUPPORTED_ERR",9],["INUSE_ATTRIBUTE_ERR",10],["INVALID_STATE_ERR",11],["SYNTAX_ERR",12],["INVALID_MODIFICATION_ERR",13],["NAMESPACE_ERR",14],["INVALID_ACCESS_ERR",15]].forEach(function(e){e[0]in t.DOMException||(t.DOMException[e[0]]=e[1])}),function(){function e(t,e,r){if("function"==typeof e){"DOMContentLoaded"===t&&(t="load");var n=this,o=function(t){t._timeStamp=Date.now(),t._currentTarget=n,e.call(this,t),t._currentTarget=null};this["_"+t+e]=o,this.attachEvent("on"+t,o)}}function r(t,e,r){if("function"==typeof e){"DOMContentLoaded"===t&&(t="load");var n=this["_"+t+e];n&&(this.detachEvent("on"+t,n),this["_"+t+e]=null)}}"Element"in t&&!Element.prototype.addEventListener&&Object.defineProperty&&(Event.CAPTURING_PHASE=1,Event.AT_TARGET=2,Event.BUBBLING_PHASE=3,Object.defineProperties(Event.prototype,{CAPTURING_PHASE:{get:function(){return 1}},AT_TARGET:{get:function(){return 2}},BUBBLING_PHASE:{get:function(){return 3}},target:{get:function(){return this.srcElement}},currentTarget:{get:function(){return this._currentTarget}},eventPhase:{get:function(){return this.srcElement===this.currentTarget?Event.AT_TARGET:Event.BUBBLING_PHASE}},bubbles:{get:function(){switch(this.type){case"click":case"dblclick":case"mousedown":case"mouseup":case"mouseover":case"mousemove":case"mouseout":case"mousewheel":case"keydown":case"keypress":case"keyup":case"resize":case"scroll":case"select":case"change":case"submit":case"reset":return!0}return!1}},cancelable:{get:function(){switch(this.type){case"click":case"dblclick":case"mousedown":case"mouseup":case"mouseover":case"mouseout":case"mousewheel":case"keydown":case"keypress":case"keyup":case"submit":return!0}return!1}},timeStamp:{get:function(){return this._timeStamp}},stopPropagation:{value:function(){this.cancelBubble=!0}},preventDefault:{value:function(){this.returnValue=!1}},defaultPrevented:{get:function(){return this.returnValue===!1}}}),[Window,HTMLDocument,Element].forEach(function(t){t.prototype.addEventListener=e,t.prototype.removeEventListener=r}))}(),function(){function e(t,e){e=e||{bubbles:!1,cancelable:!1,detail:void 0};var r=document.createEvent("CustomEvent");return r.initCustomEvent(t,e.bubbles,e.cancelable,e.detail),r}"CustomEvent"in t&&"function"==typeof t.CustomEvent||(e.prototype=t.Event.prototype,t.CustomEvent=e)}(),window.addEvent=function(t,e,r){t.addEventListener?t.addEventListener(e,r,!1):t.attachEvent&&(t["e"+e+r]=r,t[e+r]=function(){var n=window.event;n.currentTarget=t,n.preventDefault=function(){n.returnValue=!1},n.stopPropagation=function(){n.cancelBubble=!0},n.target=n.srcElement,n.timeStamp=Date.now(),t["e"+e+r].call(this,n)},t.attachEvent("on"+e,t[e+r]))},window.removeEvent=function(t,e,r){t.removeEventListener?t.removeEventListener(e,r,!1):t.detachEvent&&(t.detachEvent("on"+e,t[e+r]),t[e+r]=null,t["e"+e+r]=null)},function(){function e(t,e){function r(t){return t.length?t.split(/\s+/g):[]}function n(t,e){var n=r(e),o=n.indexOf(t);return o!==-1&&n.splice(o,1),n.join(" ")}if(Object.defineProperties(this,{length:{get:function(){return r(t[e]).length}},item:{value:function(n){var o=r(t[e]);return 0<=n&&n=0&&i.item(e)!==this;);return e>-1})),window.Element&&!Element.prototype.closest&&(Element.prototype.closest=function(t){var e,r=(this.document||this.ownerDocument).querySelectorAll(t),n=this;do for(e=r.length;--e>=0&&r.item(e)!==n;);while(e<0&&(n=n.parentElement));return n});var n={prepend:function(){var t=[].slice.call(arguments);t=r(t),this.insertBefore(t,this.firstChild)},append:function(){var t=[].slice.call(arguments);t=r(t),this.appendChild(t)}};e(t.Document||t.HTMLDocument,n),e(t.DocumentFragment,n),e(t.Element,n);var o={before:function(){var t=[].slice.call(arguments),e=this.parentNode;if(e){for(var n=this.previousSibling;t.indexOf(n)!==-1;)n=n.previousSibling;var o=r(t);e.insertBefore(o,n?n.nextSibling:e.firstChild)}},after:function(){var t=[].slice.call(arguments),e=this.parentNode;if(e){for(var n=this.nextSibling;t.indexOf(n)!==-1;)n=n.nextSibling;var o=r(t);e.insertBefore(o,n)}},replaceWith:function(){var t=[].slice.call(arguments),e=this.parentNode;if(e){for(var n=this.nextSibling;t.indexOf(n)!==-1;)n=n.nextSibling;var o=r(t);this.parentNode===e?e.replaceChild(o,this):e.insertBefore(o,n)}},remove:function(){this.parentNode&&this.parentNode.removeChild(this)}};e(t.DocumentType,o),e(t.Element,o),e(t.CharacterData,o)}}(self),function(t){"use strict";"window"in t&&"document"in t&&(t.XMLHttpRequest=t.XMLHttpRequest||function(){try{return new ActiveXObject("Msxml2.XMLHTTP.6.0")}catch(t){}try{return new ActiveXObject("Msxml2.XMLHTTP.3.0")}catch(t){}try{return new ActiveXObject("Msxml2.XMLHTTP")}catch(t){}throw Error("This browser does not support XMLHttpRequest.")},[["UNSENT",0],["OPENED",1],["HEADERS_RECEIVED",2],["LOADING",3],["DONE",4]].forEach(function(e){e[0]in t.XMLHttpRequest||(t.XMLHttpRequest[e[0]]=e[1])}),function(){function e(t){if(this._data=[],t)for(var e=0;e=t.length)return{done:!0,value:void 0};var n=t[r++];return{done:!1,value:"key"===e?n.name:"value"===e?n.value:[n.name,n.value]}}}function c(e,r){function n(){var t=c.href.replace(/#$|\?$|\?(?=#)/g,"");c.href!==t&&(c.href=t)}function u(){h._setList(c.search?o(c.search.substring(1)):[]),h._update_steps()}if(!(this instanceof t.URL))throw new TypeError("Failed to construct 'URL': Please use the 'new' operator.");r&&(e=function(){if(s)return new f(e,r).href;var t;if(document.implementation&&document.implementation.createHTMLDocument?t=document.implementation.createHTMLDocument(""):document.implementation&&document.implementation.createDocument?(t=document.implementation.createDocument("http://www.w3.org/1999/xhtml","html",null),t.documentElement.appendChild(t.createElement("head")),t.documentElement.appendChild(t.createElement("body"))):window.ActiveXObject&&(t=new window.ActiveXObject("htmlfile"),t.write(""),t.close()),!t)throw Error("base not supported");var n=t.createElement("base");n.href=r,t.getElementsByTagName("head")[0].appendChild(n);var o=t.createElement("a");return o.href=e,o.href}());var c=i(e||""),l=function(){if(!("defineProperties"in Object))return!1;try{var t={};return Object.defineProperties(t,{prop:{get:function(){return!0}}}),t.prop}catch(e){return!1}}(),p=l?this:document.createElement("a"),h=new a(c.search?c.search.substring(1):null);return h._url_object=p,Object.defineProperties(p,{href:{get:function(){return c.href},set:function(t){c.href=t,n(),u()},enumerable:!0,configurable:!0},origin:{get:function(){return"origin"in c?c.origin:this.protocol+"//"+this.host},enumerable:!0,configurable:!0},protocol:{get:function(){return c.protocol},set:function(t){c.protocol=t},enumerable:!0,configurable:!0},username:{get:function(){return c.username},set:function(t){c.username=t},enumerable:!0,configurable:!0},password:{get:function(){return c.password},set:function(t){c.password=t},enumerable:!0,configurable:!0},host:{get:function(){var t={"http:":/:80$/,"https:":/:443$/,"ftp:":/:21$/}[c.protocol];return t?c.host.replace(t,""):c.host},set:function(t){c.host=t},enumerable:!0,configurable:!0},hostname:{get:function(){return c.hostname},set:function(t){c.hostname=t},enumerable:!0,configurable:!0},port:{get:function(){return c.port},set:function(t){c.port=t},enumerable:!0,configurable:!0},pathname:{get:function(){return"/"!==c.pathname.charAt(0)?"/"+c.pathname:c.pathname},set:function(t){c.pathname=t},enumerable:!0,configurable:!0},search:{get:function(){return c.search},set:function(t){c.search!==t&&(c.search=t,n(),u())},enumerable:!0,configurable:!0},searchParams:{get:function(){return h},enumerable:!0,configurable:!0},hash:{get:function(){return c.hash},set:function(t){c.hash=t,n()},enumerable:!0,configurable:!0},toString:{value:function(){return c.toString()},enumerable:!1,configurable:!0},valueOf:{value:function(){return c.valueOf()},enumerable:!1,configurable:!0}}),p}var s,f=t.URL;try{if(f){if(s=new t.URL("http://example.com"),"searchParams"in s)return;"href"in s||(s=void 0)}}catch(l){}if(Object.defineProperties(a.prototype,{append:{value:function(t,e){this._list.push({name:t,value:e}),this._update_steps()},writable:!0,enumerable:!0,configurable:!0},"delete":{value:function(t){for(var e=0;e1?arguments[1]:void 0;this._list.forEach(function(r,n){t.call(e,r.value,r.name)})},writable:!0,enumerable:!0,configurable:!0},toString:{value:function(){return n(this._list)},writable:!0,enumerable:!1,configurable:!0}}),"Symbol"in t&&"iterator"in t.Symbol&&(Object.defineProperty(a.prototype,t.Symbol.iterator,{value:a.prototype.entries,writable:!0,enumerable:!0,configurable:!0}),Object.defineProperty(u.prototype,t.Symbol.iterator,{value:function(){return this},writable:!0,enumerable:!0,configurable:!0})),f)for(var p in f)f.hasOwnProperty(p)&&"function"==typeof f[p]&&(c[p]=f[p]);t.URL=c,t.URLSearchParams=a}(),function(){if("1"!==new t.URLSearchParams([["a",1]]).get("a")||"1"!==new t.URLSearchParams({a:1}).get("a")){var n=t.URLSearchParams;t.URLSearchParams=function(t){if(t&&"object"==typeof t&&e(t)){var o=new n;return r(t).forEach(function(t){if(!e(t))throw TypeError();var n=r(t);if(2!==n.length)throw TypeError();o.append(n[0],n[1])}),o}return t&&"object"==typeof t?(o=new n,Object.keys(t).forEach(function(e){o.set(e,t[e])}),o):new n(t)}}}()}(self),function(t){"use strict";function e(t){if(t=String(t),t.match(/[^\x00-\xFF]/))throw TypeError("Not a valid ByteString");return t}function r(t){return t=String(t),t.replace(/([\u0000-\uD7FF\uE000-\uFFFF]|[\uD800-\uDBFF][\uDC00-\uDFFF]|[\uD800-\uDFFF])/g,function(t){return/^[\uD800-\uDFFF]$/.test(t)?"�":t})}function n(t){return 65535&t}function o(t){return String(t).replace(/[a-z]/g,function(t){return t.toUpperCase()})}function i(t){return t=o(t),"CONNECT"===t||"TRACE"===t||"TRACK"===t}function a(t){var e=o(t);return"DELETE"===e||"GET"===e||"HEAD"===e||"OPTIONS"===e||"POST"===e||"PUT"===e?e:t}function u(t){return/^[!#$%&'*+\-.09A-Z^_`a-z|~]+$/.test(t)}function c(t){return!0}function s(t){t=String(t).toLowerCase();var e={"accept-charset":!0,"accept-encoding":!0,"access-control-request-headers":!0,"access-control-request-method":!0,connection:!0,"content-length":!0,cookie:!0,cookie2:!0,date:!0,dnt:!0,expect:!0,host:!0,"keep-alive":!0,origin:!0,referer:!0,te:!0,trailer:!0,"transfer-encoding":!0,upgrade:!0,"user-agent":!0,via:!0};return e[t]||"proxy-"===t.substring(0,6)||"sec-"===t.substring(0,4)}function f(t){t=String(t).toLowerCase();var e={"set-cookie":!0,"set-cookie2":!0};return e[t]}function l(t,e){return t=String(t).toLowerCase(),"accept"===t||"accept-language"===t||"content-language"===t||"content-type"===t&&["application/x-www-form-encoded","multipart/form-data","text/plain"].indexOf(e)!==-1}function p(t){this._guard="none",this._headerList=[],t&&h(this,t)}function h(t,e){e instanceof p?e._headerList.forEach(function(e){t.append(e[0],e[1])}):Array.isArray(e)?e.forEach(function(e){if(!Array.isArray(e)||2!==e.length)throw TypeError();t.append(e[0],e[1])}):(e=Object(e),Object.keys(e).forEach(function(r){t.append(r,e[r])}))}function y(t){this._headers=t,
3 | this._index=0}function v(t){this._stream=t,this.bodyUsed=!1}function d(t,n){if(arguments.length<1)throw TypeError("Not enough arguments");if(v.call(this,null),this.method="GET",this.url="",this.headers=new p,this.headers._guard="request",this.referrer=null,this.mode=null,this.credentials="omit",t instanceof d){if(t.bodyUsed)throw TypeError();t.bodyUsed=!0,this.method=t.method,this.url=t.url,this.headers=new p(t.headers),this.headers._guard=t.headers._guard,this.credentials=t.credentials,this._stream=t._stream}else t=r(t),this.url=String(new URL(t,self.location));if(n=Object(n),"method"in n){var o=e(n.method);if(i(o))throw TypeError();this.method=a(o)}"headers"in n&&(this.headers=new p,h(this.headers,n.headers)),"body"in n&&(this._stream=n.body),"credentials"in n&&["omit","same-origin","include"].indexOf(n.credentials)!==-1&&(this.credentials=n.credentials)}function m(t,e){if(arguments.length<1&&(t=""),this.headers=new p,this.headers._guard="response",t instanceof XMLHttpRequest&&"_url"in t){var o=t;return this.type="basic",this.url=r(o._url),this.status=o.status,this.ok=200<=this.status&&this.status<=299,this.statusText=o.statusText,o.getAllResponseHeaders().split(/\r?\n/).filter(function(t){return t.length}).forEach(function(t){var e=t.indexOf(":");this.headers.append(t.substring(0,e),t.substring(e+2))},this),void v.call(this,o.responseText)}v.call(this,t),e=Object(e)||{},this.url="";var i="status"in e?n(e.status):200;if(i<200||i>599)throw RangeError();this.status=i,this.ok=200<=this.status&&this.status<=299;var a="statusText"in e?String(e.statusText):"OK";if(/[^\x00-\xFF]/.test(a))throw TypeError();this.statusText=a,"headers"in e&&h(this.headers,e),this.type="basic"}function b(t,e){return new Promise(function(r,n){var o=new d(t,e),i=new XMLHttpRequest,a=!0;i._url=o.url;try{i.open(o.method,o.url,a)}catch(u){throw TypeError(u.message)}for(var c=o.headers[Symbol.iterator](),s=c.next();!s.done;s=c.next())i.setRequestHeader(s.value[0],s.value[1]);"include"===o.credentials&&(i.withCredentials=!0),i.onreadystatechange=function(){i.readyState===XMLHttpRequest.DONE&&(0===i.status?n(new TypeError("Network error")):r(new m(i)))},i.send(o._stream)})}p.prototype={append:function(t,r){if(t=e(t),!u(t)||!c(r))throw TypeError();if("immutable"===this._guard)throw TypeError();"request"===this._guard&&s(t)||("request-no-CORS"!==this._guard||l(t,r))&&("response"===this._guard&&f(t)||(t=t.toLowerCase(),this._headerList.push([t,r])))},"delete":function(t){if(t=e(t),!u(t))throw TypeError();if("immutable"===this._guard)throw TypeError();if(("request"!==this._guard||!s(t))&&("request-no-CORS"!==this._guard||l(t,"invalid"))&&("response"!==this._guard||!f(t))){t=t.toLowerCase();for(var r=0;r=this._headers._headerList.length?{value:void 0,done:!0}:{value:this._headers._headerList[this._index++],done:!1}},y.prototype[Symbol.iterator]=function(){return this},v.prototype={arrayBuffer:function(){if(this.bodyUsed)return Promise.reject(TypeError());if(this.bodyUsed=!0,this._stream instanceof ArrayBuffer)return Promise.resolve(this._stream);var t=this._stream;return new Promise(function(e,r){var n=unescape(encodeURIComponent(t)).split("").map(function(t){return t.charCodeAt(0)});e(new Uint8Array(n).buffer)})},blob:function(){return this.bodyUsed?Promise.reject(TypeError()):(this.bodyUsed=!0,this._stream instanceof Blob?Promise.resolve(this._stream):Promise.resolve(new Blob([this._stream])))},formData:function(){return this.bodyUsed?Promise.reject(TypeError()):(this.bodyUsed=!0,this._stream instanceof FormData?Promise.resolve(this._stream):Promise.reject(Error("Not yet implemented")))},json:function(){if(this.bodyUsed)return Promise.reject(TypeError());this.bodyUsed=!0;var t=this;return new Promise(function(e,r){e(JSON.parse(t._stream))})},text:function(){return this.bodyUsed?Promise.reject(TypeError()):(this.bodyUsed=!0,Promise.resolve(String(this._stream)))}},d.prototype=v.prototype,m.prototype=v.prototype,m.redirect=function(){throw Error("Not supported")},"fetch"in t||(t.Headers=p,t.Request=d,t.Response=m,t.fetch=b)}(self);
4 |
--------------------------------------------------------------------------------
/packages/web/screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/miguelcast/cra-init-dashboard/52ef1e8832cfe0da798cec78e50406234f55eeac/packages/web/screenshot.png
--------------------------------------------------------------------------------
/packages/web/src/components/Auth/Login.js:
--------------------------------------------------------------------------------
1 | import { Button, Form, Icon, Input } from 'antd';
2 | import PropTypes from 'prop-types';
3 | import React from 'react';
4 | import { useTranslation } from 'react-i18next';
5 | import { Link } from 'react-router-dom';
6 | import { Title } from '../Shared';
7 |
8 | function Login(props) {
9 | const { loading } = props;
10 | const { getFieldDecorator } = props.form;
11 | const { t } = useTranslation();
12 | const handleSubmit = e => {
13 | e.preventDefault();
14 | props.form.validateFields((err, values) => {
15 | if (!err) {
16 | props.authentication(values.userName, values.password);
17 | }
18 | });
19 | };
20 |
21 | return (
22 |
25 | {getFieldDecorator('userName', {
26 | rules: [
27 | { required: true, message: t('common.usernameRequired') },
28 | { type: 'email', message: t('common.invalidEmail') },
29 | ],
30 | })(
31 | }
33 | placeholder={t('common.email')}
34 | size="large"
35 | />,
36 | )}
37 |
38 |
39 | {getFieldDecorator('password', {
40 | rules: [{ required: true, message: t('common.passwordRequired') }],
41 | })(
42 | }
44 | type="password"
45 | size="large"
46 | placeholder={t('common.password')}
47 | />,
48 | )}
49 |
50 |
51 |
52 | {t('login.forgotPassword')}
53 |
54 |
62 | {t('common.or')} {t('login.registerNow')}
63 |
64 |
65 | );
66 | }
67 |
68 | const formShape = {
69 | validateFields: PropTypes.func,
70 | getFieldDecorator: PropTypes.func,
71 | };
72 |
73 | Login.propTypes = {
74 | form: PropTypes.shape(formShape).isRequired,
75 | loading: PropTypes.bool,
76 | authentication: PropTypes.func,
77 | };
78 |
79 | export default Form.create({ name: 'normal_login' })(Login);
80 |
--------------------------------------------------------------------------------
/packages/web/src/components/Auth/Register.js:
--------------------------------------------------------------------------------
1 | import { Button, Form, Icon, Input } from 'antd';
2 | import PropTypes from 'prop-types';
3 | import React from 'react';
4 | import { useTranslation } from 'react-i18next';
5 | import { Title } from '../Shared';
6 |
7 | const formShape = {
8 | validateFields: PropTypes.func,
9 | getFieldDecorator: PropTypes.func,
10 | };
11 |
12 | function Register(props) {
13 | const { getFieldDecorator } = props.form;
14 | const { t } = useTranslation();
15 |
16 | const handleSubmit = e => {
17 | e.preventDefault();
18 | props.form.validateFields((err, values) => {
19 | if (!err) {
20 | console.log('Received values of form: ', values);
21 | }
22 | });
23 | };
24 |
25 | return (
26 |
29 | {getFieldDecorator('userName', {
30 | rules: [
31 | { required: true, message: t('common.usernameRequired') },
32 | { type: 'email', message: t('common.invalidEmail') },
33 | ],
34 | })(
35 | }
37 | placeholder={t('common.email')}
38 | size="large"
39 | />,
40 | )}
41 |
42 |
43 | {getFieldDecorator('name', {
44 | rules: [{ required: true, message: t('register.nameRequired') }],
45 | })(
46 | }
48 | placeholder={t('register.name')}
49 | size="large"
50 | />,
51 | )}
52 |
53 |
54 | {getFieldDecorator('password', {
55 | rules: [{ required: true, message: t('common.passwordRequired') }],
56 | })(
57 | }
59 | type="password"
60 | size="large"
61 | placeholder={t('common.password')}
62 | />,
63 | )}
64 |
65 |
66 |
73 |
74 |
75 | );
76 | }
77 |
78 | Register.propTypes = {
79 | form: PropTypes.shape(formShape).isRequired,
80 | };
81 |
82 | export default Form.create({ name: 'register' })(Register);
83 |
--------------------------------------------------------------------------------
/packages/web/src/components/Auth/hooks.js:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from 'react';
2 | import store from '../../config/store';
3 |
4 | export function useAuthenticated() {
5 | const [auth, setAuth] = useState(store.getState().auth);
6 |
7 | useEffect(() => {
8 | const unsubscribe = store.subscribe(() => {
9 | const newAuth = store.getState().auth;
10 | if (auth !== newAuth) {
11 | setAuth(newAuth);
12 | }
13 | });
14 | return unsubscribe;
15 | }, [auth]);
16 | return { ...(auth || {}) };
17 | }
18 |
--------------------------------------------------------------------------------
/packages/web/src/components/Auth/index.js:
--------------------------------------------------------------------------------
1 | export { default as Login } from './Login';
2 | export { default as Register } from './Register';
3 | export { useAuthenticated } from './hooks';
4 |
--------------------------------------------------------------------------------
/packages/web/src/components/Layout/Header.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Icon, Layout, Row, Col } from 'antd';
3 | import PropTypes from 'prop-types';
4 | import MenuHeader from './MenuHeader';
5 |
6 | const Header = ({ pathname, isCollapsed, showDrawer, drawerVisible }) => (
7 |
8 |
9 |
10 | {isCollapsed && (
11 |
16 | )}
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 | );
26 |
27 | Header.propTypes = {
28 | pathname: PropTypes.string,
29 | isCollapsed: PropTypes.bool,
30 | drawerVisible: PropTypes.bool,
31 | showDrawer: PropTypes.func,
32 | };
33 |
34 | Header.defaultProps = {
35 | isCollapsed: false,
36 | drawerVisible: false,
37 | };
38 |
39 | export default Header;
40 |
--------------------------------------------------------------------------------
/packages/web/src/components/Layout/HeaderUser.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import { Avatar, Dropdown, Menu, Icon } from 'antd';
4 | import { connect } from 'react-redux';
5 | import { useTranslation } from 'react-i18next';
6 |
7 | const MenuDrop = ({ logout }) => {
8 | const { t } = useTranslation();
9 | return (
10 |
16 | );
17 | };
18 | MenuDrop.propTypes = {
19 | logout: PropTypes.func,
20 | };
21 |
22 | const HeaderUser = ({ logout, user }) => {
23 | return (
24 | }
26 | placement="bottomRight">
27 |
28 |
29 |
{user.name || user.username}
30 |
31 |
32 | );
33 | };
34 |
35 | HeaderUser.propTypes = {
36 | logout: PropTypes.func,
37 | user: PropTypes.object,
38 | };
39 |
40 | HeaderUser.displayName = 'HeaderUser';
41 |
42 | export default connect(
43 | state => ({ user: state.auth.user }),
44 | dispatch => ({
45 | logout: () => dispatch.auth.logout(),
46 | }),
47 | )(HeaderUser);
48 |
--------------------------------------------------------------------------------
/packages/web/src/components/Layout/Logo.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import logo from '../../img/logo.png';
3 |
4 | const Logo = () => (
5 |
6 |

7 |
8 | );
9 |
10 | export default Logo;
11 |
--------------------------------------------------------------------------------
/packages/web/src/components/Layout/MenuHeader.js:
--------------------------------------------------------------------------------
1 | import React, { Fragment } from 'react';
2 | import { Menu, Icon } from 'antd';
3 | import PropTypes from 'prop-types';
4 | import { Link } from 'react-router-dom';
5 | import { useTranslation } from 'react-i18next';
6 | import { useMenu } from './hooks';
7 |
8 | const MenuHeader = ({ pathname, isCollapse, ...rest }) => {
9 | const menus = useMenu('header');
10 | const { t } = useTranslation();
11 | return (
12 |
13 |
37 | {menus.map(
38 | ({ component, index }) =>
39 | component && React.createElement(component(), { key: index }, null),
40 | )}
41 |
42 | );
43 | };
44 |
45 | MenuHeader.propTypes = {
46 | pathname: PropTypes.string,
47 | isCollapse: PropTypes.bool,
48 | };
49 |
50 | export default MenuHeader;
51 |
--------------------------------------------------------------------------------
/packages/web/src/components/Layout/MenuPrimary.js:
--------------------------------------------------------------------------------
1 | import React, { Fragment } from 'react';
2 | import { Menu, Icon } from 'antd';
3 | import PropTypes from 'prop-types';
4 | import { useTranslation } from 'react-i18next';
5 | import { Link } from 'react-router-dom';
6 | import { useMenu } from './hooks';
7 |
8 | const MenuPrimary = ({ pathname, ...rest }) => {
9 | const menus = useMenu('primary');
10 | const { t } = useTranslation();
11 | return (
12 |
13 |
28 | {menus.map(
29 | ({ component, index }) =>
30 | component && React.createElement(component(), { key: index }, null),
31 | )}
32 |
33 | );
34 | };
35 |
36 | MenuPrimary.propTypes = {
37 | pathname: PropTypes.string,
38 | };
39 |
40 | export default MenuPrimary;
41 |
--------------------------------------------------------------------------------
/packages/web/src/components/Layout/hooks.js:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from 'react';
2 | import { useAuthenticated } from '../Auth';
3 | import { GUEST, LOGGED } from '../../config/constants';
4 | import myMenus from '../../config/menus';
5 |
6 | export function useMenu(position) {
7 | const { isAuthenticated } = useAuthenticated();
8 | const [menus, setMenus] = useState(myMenus[position]);
9 |
10 | useEffect(() => {
11 | setMenus(
12 | myMenus[position].filter(menu => {
13 | return (
14 | menu.when === undefined ||
15 | (isAuthenticated === false && menu.when === GUEST) ||
16 | (isAuthenticated === true && menu.when === LOGGED)
17 | );
18 | }),
19 | );
20 | }, [isAuthenticated, position]);
21 |
22 | return menus;
23 | }
24 |
--------------------------------------------------------------------------------
/packages/web/src/components/Layout/index.js:
--------------------------------------------------------------------------------
1 | export { default as MenuPrimary } from './MenuPrimary';
2 | export { default as Logo } from './Logo';
3 | export { default as Header } from './Header';
4 | export { default as HeaderUser } from './HeaderUser';
5 |
--------------------------------------------------------------------------------
/packages/web/src/components/Shared/ChangeLanguage.js:
--------------------------------------------------------------------------------
1 | import React, { Fragment } from 'react';
2 | import { Button } from 'antd';
3 | import '../../config/localization/i18n';
4 | import { useTranslation } from 'react-i18next';
5 | import { LANGUAGES } from '../../config/constants';
6 |
7 | const ChangeLanguage = () => {
8 | const { t, i18n } = useTranslation();
9 | return (
10 |
11 |
18 | {' '}
24 |
25 | );
26 | };
27 |
28 | export default ChangeLanguage;
29 |
--------------------------------------------------------------------------------
/packages/web/src/components/Shared/Title.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 |
4 | const Title = ({ text }) => {text}
;
5 |
6 | Title.propTypes = {
7 | text: PropTypes.string.isRequired,
8 | };
9 |
10 | export default Title;
11 |
--------------------------------------------------------------------------------
/packages/web/src/components/Shared/index.js:
--------------------------------------------------------------------------------
1 | export { default as Title } from './Title';
2 | export { default as ChangeLanguage } from './ChangeLanguage';
3 |
--------------------------------------------------------------------------------
/packages/web/src/config/constants.js:
--------------------------------------------------------------------------------
1 | export const GUEST = 'guest';
2 | export const LOGGED = 'logged';
3 |
4 | export const URL_LOCALIZATION = '/locales/{{lng}}/{{ns}}.json';
5 | export const VERSION_LOCALIZATION = 5;
6 | export const TIME_CACHE_LOCALIZATION = 7 * 24 * 60 * 60 * 1000;
7 | export const LANGUAGES = {
8 | es: 'es',
9 | en: 'en',
10 | default: 'en',
11 | };
12 |
--------------------------------------------------------------------------------
/packages/web/src/config/cruds/user.js:
--------------------------------------------------------------------------------
1 | const user = {
2 | keyName: 'key',
3 | getList: { url: '/users' },
4 | getByKey: { url: '/user/{keyName}' },
5 | upsert: { url: '/postUser.json' },
6 | delete: { url: '/deleteUser.json' },
7 | fields: [
8 | {
9 | title: 'Name',
10 | key: 'name',
11 | sorter: true,
12 | filter: true,
13 | type: 'string',
14 | initialValue: 'My Name',
15 | rules: [
16 | { required: true, message: 'Is required!' },
17 | { type: 'string', message: 'Should be string!' },
18 | { max: 50, message: 'Max 50 characters!' },
19 | ],
20 | },
21 | {
22 | title: 'Age',
23 | key: 'age',
24 | sorter: true,
25 | filter: true,
26 | type: 'number',
27 | columnStyle: {
28 | align: 'right',
29 | width: 80,
30 | },
31 | rules: [{ type: 'integer', message: 'Should be integer!' }],
32 | },
33 | {
34 | title: 'Address',
35 | key: 'address',
36 | sorter: true,
37 | filter: true,
38 | hidden: ['column', 'form'],
39 | type: 'string',
40 | rules: [
41 | { required: true, message: 'Is required!' },
42 | { max: 150, message: 'Max 150 characters!' },
43 | ],
44 | },
45 | {
46 | title: 'Color',
47 | key: 'color',
48 | sorter: true,
49 | filter: true,
50 | type: 'select',
51 | options: {
52 | red: 'Red',
53 | green: 'Green',
54 | yellow: 'Yellow',
55 | black: 'Black',
56 | },
57 | rules: [{ required: true, message: 'Is required!' }],
58 | },
59 | {
60 | title: 'Country async load',
61 | key: 'country',
62 | columnKey: 'countryName',
63 | sorter: true,
64 | filter: true,
65 | type: 'select',
66 | options: {},
67 | configOptions: {
68 | url: '/countries',
69 | // { key: text }, mapper with loaded data
70 | map: item => ({ [item.key]: item.name }),
71 | // default get, you can use get or post;
72 | method: 'get',
73 | },
74 | dependencies: {
75 | fields: ['color'],
76 | onChange: () => ({
77 | disabled: false,
78 | }),
79 | },
80 | disabled: true,
81 | rules: [{ required: true, message: 'Is required!' }],
82 | },
83 | {
84 | title: 'Gender',
85 | key: 'gender',
86 | type: 'radio',
87 | sorter: true,
88 | filter: true,
89 | options: {
90 | male: 'Male',
91 | female: 'Female',
92 | },
93 | rules: [{ required: true, message: 'Is required!' }],
94 | },
95 | {
96 | title: 'Birthday',
97 | key: 'birthday',
98 | type: 'date',
99 | sorter: true,
100 | filter: true,
101 | },
102 | {
103 | title: 'Status',
104 | key: 'status',
105 | sorter: true,
106 | filter: true,
107 | type: 'bool',
108 | initialValue: true,
109 | options: {
110 | true: 'Active',
111 | false: 'Inactive',
112 | },
113 | },
114 | ],
115 | };
116 |
117 | export default user;
118 |
--------------------------------------------------------------------------------
/packages/web/src/config/localization/antdLocale.js:
--------------------------------------------------------------------------------
1 | /**
2 | * List of Ant design locales
3 | * https://ant.design/docs/react/i18n
4 | */
5 | import { LANGUAGES } from '../constants';
6 |
7 | export default (language = LANGUAGES.default) => {
8 | switch (language) {
9 | case LANGUAGES.es: {
10 | return require('antd/lib/locale-provider/es_ES').default;
11 | }
12 | default:
13 | case LANGUAGES.en: {
14 | return require('antd/lib/locale-provider/en_US').default;
15 | }
16 | }
17 | };
18 |
--------------------------------------------------------------------------------
/packages/web/src/config/localization/i18n.js:
--------------------------------------------------------------------------------
1 | import axios from 'axios';
2 | import i18next from 'i18next';
3 | import Backend from 'i18next-chained-backend';
4 | import LocalStorageBackend from 'i18next-localstorage-backend';
5 | import XHR from 'i18next-xhr-backend';
6 | import LngDetector from 'i18next-browser-languagedetector';
7 | import { initReactI18next } from 'react-i18next';
8 | import {
9 | LANGUAGES,
10 | URL_LOCALIZATION,
11 | TIME_CACHE_LOCALIZATION,
12 | VERSION_LOCALIZATION,
13 | } from '../constants';
14 |
15 | const URL_API_LOCALIZATION =
16 | process.env.REACT_APP_API_LOCALIZATION || 'http://localhost:3005';
17 |
18 | function loadLocales(url, options, callback) {
19 | axios
20 | .get(`${url}?v=${options.queryStringParams.v}`, {
21 | headers: {
22 | Accept: 'application/json',
23 | 'Content-Type': 'application/json',
24 | },
25 | })
26 | .then(res => {
27 | callback(res.data, { status: '200' });
28 | })
29 | .catch(err => {
30 | callback(null, { status: '404', message: err.message });
31 | });
32 | }
33 |
34 | export default i18next
35 | .use(LngDetector)
36 | .use(Backend)
37 | .use(initReactI18next)
38 | .init(
39 | {
40 | whitelist: Object.values(LANGUAGES),
41 | fallbackLng: LANGUAGES.default,
42 | // have a common namespace used around the full app
43 | ns: 'translation',
44 | defaultNS: 'translation',
45 | debug: process.env.NODE_ENV === 'development',
46 | backend: {
47 | backends: [LocalStorageBackend, XHR],
48 | backendOptions: [
49 | {
50 | expirationTime: TIME_CACHE_LOCALIZATION,
51 | prefix: 'i18next_res_',
52 | },
53 | {
54 | queryStringParams: { v: VERSION_LOCALIZATION },
55 | parse: data => data,
56 | loadPath: `${URL_API_LOCALIZATION}${URL_LOCALIZATION}`,
57 | ajax: loadLocales,
58 | },
59 | ],
60 | },
61 | },
62 | err => {
63 | if (err) return console.log('something went wrong loading', err);
64 | },
65 | );
66 |
--------------------------------------------------------------------------------
/packages/web/src/config/menus.js:
--------------------------------------------------------------------------------
1 | import { ChangeLanguage } from '../components/Shared';
2 | import { createMenu, createComponent } from '../utils/general';
3 | import { GUEST, LOGGED } from './constants';
4 | import { HeaderUser } from '../components/Layout';
5 |
6 | const menus = {
7 | primary: [
8 | createMenu('/', 'menu.home', 'home'),
9 | createMenu('/about', 'menu.about', 'rocket'),
10 | createMenu('/list', 'menu.users', 'team', LOGGED),
11 | ],
12 | header: [
13 | createMenu('/register', 'menu.register', 'user', GUEST),
14 | createMenu('/login', 'menu.login', 'login', GUEST),
15 | createComponent(() => ChangeLanguage),
16 | createComponent(() => HeaderUser, LOGGED),
17 | ],
18 | };
19 |
20 | export default menus;
21 |
--------------------------------------------------------------------------------
/packages/web/src/config/routes.js:
--------------------------------------------------------------------------------
1 | import { lazy } from 'react';
2 | import { createRoute } from '../utils/general';
3 | import Home from '../pages/Home';
4 | import { LOGGED, GUEST } from './constants';
5 |
6 | const AsyncAbout = lazy(() => import('../pages/About.js'));
7 | const AsyncRegister = lazy(() => import('../pages/Register.js'));
8 | const AsyncLogin = lazy(() => import('../pages/Login.js'));
9 | const AsyncForgotPass = lazy(() => import('../pages/ForgotPassword.js'));
10 | const AsyncList = lazy(() => import('../pages/List.js'));
11 | const AsyncForm = lazy(() => import('../pages/Form.js'));
12 |
13 | export default [
14 | createRoute('/', Home, null, true),
15 | createRoute('/about', AsyncAbout),
16 | createRoute('/register', AsyncRegister, GUEST),
17 | createRoute('/login', AsyncLogin, GUEST),
18 | createRoute('/forgotPassword', AsyncForgotPass, GUEST),
19 | createRoute('/list', AsyncList, LOGGED),
20 | createRoute('/form/:id?', AsyncForm, LOGGED),
21 | ];
22 |
--------------------------------------------------------------------------------
/packages/web/src/config/services.js:
--------------------------------------------------------------------------------
1 | export const auth = {
2 | login: '/auth/login',
3 | logout: '/auth/logout',
4 | };
5 |
--------------------------------------------------------------------------------
/packages/web/src/config/store.js:
--------------------------------------------------------------------------------
1 | import { init } from '@rematch/core';
2 | import createRematchPersist from '@rematch/persist';
3 | import createLoadingPlugin from '@rematch/loading';
4 | import * as models from '../models';
5 |
6 | const persistPlugin = createRematchPersist({
7 | whitelist: ['auth'],
8 | keyPrefix: '--persist-key-',
9 | throttle: 500,
10 | version: 1,
11 | });
12 |
13 | const loadingPlugin = createLoadingPlugin({});
14 |
15 | const store = init({
16 | models,
17 | plugins: [persistPlugin, loadingPlugin],
18 | });
19 |
20 | export default store;
21 |
--------------------------------------------------------------------------------
/packages/web/src/img/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/miguelcast/cra-init-dashboard/52ef1e8832cfe0da798cec78e50406234f55eeac/packages/web/src/img/logo.png
--------------------------------------------------------------------------------
/packages/web/src/index.js:
--------------------------------------------------------------------------------
1 | import React, { Suspense } from 'react';
2 | import ReactDOM from 'react-dom';
3 | import { Provider } from 'react-redux';
4 | import { BrowserRouter as Router } from 'react-router-dom';
5 | import { ConfigProvider, Spin } from 'antd';
6 | import { getPersistor } from '@rematch/persist';
7 | import { PersistGate } from 'redux-persist/integration/react';
8 | import { useTranslation } from 'react-i18next';
9 | import { ProviderEasyCrud } from "react-easy-crud";
10 | import Layout from './pages/Layout';
11 | import store from './config/store';
12 | import client from './services/instance';
13 | import getLocalesAntd from './config/localization/antdLocale';
14 | import * as registerServiceWorker from './registerServiceWorker';
15 | import './styles/index.less';
16 | import './config/localization/i18n';
17 |
18 | const persistor = getPersistor();
19 |
20 | const AppSuspense = () => (
21 | }>
22 |
23 |
24 | );
25 |
26 | const App = () => {
27 | const { i18n } = useTranslation();
28 | return (
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 | );
41 | };
42 |
43 | ReactDOM.render(, document.getElementById('root'));
44 | registerServiceWorker.register();
45 |
--------------------------------------------------------------------------------
/packages/web/src/models/auth.js:
--------------------------------------------------------------------------------
1 | import instance from '../services/instance';
2 | import { authenticationService } from '../services/auth';
3 |
4 | const auth = {
5 | state: {
6 | token: null,
7 | user: {},
8 | isAuthenticated: false,
9 | },
10 | reducers: {
11 | setAuthenticated: (state, payload) => ({
12 | ...state,
13 | ...payload,
14 | isAuthenticated: !!payload.token,
15 | }),
16 | setLogout: () => ({
17 | token: null,
18 | user: {},
19 | isAuthenticated: false,
20 | }),
21 | },
22 | effects: {
23 | async authentication(credentials) {
24 | try {
25 | const promise = await authenticationService(
26 | credentials.username,
27 | credentials.password,
28 | );
29 | const { user, token } = await promise.data;
30 | this.setAuthenticated({ user, token });
31 | instance.setToken(token);
32 | } catch (e) {
33 | this.setAuthenticated({ user: {}, token: null });
34 | }
35 | },
36 | logout() {
37 | instance.removeToken();
38 | this.setLogout();
39 | },
40 | },
41 | };
42 |
43 | export default auth;
44 |
--------------------------------------------------------------------------------
/packages/web/src/models/home.js:
--------------------------------------------------------------------------------
1 | const home = {
2 | state: 'Hello from store.',
3 | };
4 |
5 | export default home;
6 |
--------------------------------------------------------------------------------
/packages/web/src/models/index.js:
--------------------------------------------------------------------------------
1 | export { default as home } from './home';
2 | export { default as auth } from './auth';
3 |
--------------------------------------------------------------------------------
/packages/web/src/pages/404.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Row, Col } from 'antd';
3 | import { useTranslation } from 'react-i18next';
4 |
5 | const NotFound404 = () => {
6 | const { t } = useTranslation();
7 |
8 | return (
9 |
14 |
15 |
16 | {t('common.pageNotFound')} | 404
17 |
18 |
19 |
20 | );
21 | };
22 |
23 | export default NotFound404;
24 |
--------------------------------------------------------------------------------
/packages/web/src/pages/About.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Button } from 'antd';
3 | import { Link } from 'react-router-dom';
4 | import { useTranslation } from 'react-i18next';
5 | import { Title } from '../components/Shared';
6 |
7 | const About = () => {
8 | const { t } = useTranslation();
9 | return (
10 |
11 |
12 |
13 |
16 |
17 |
18 | );
19 | };
20 |
21 | export default About;
22 |
--------------------------------------------------------------------------------
/packages/web/src/pages/ForgotPassword.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import { Button, Form, Icon, Input } from 'antd';
4 | import { withRouter } from 'react-router-dom';
5 | import { useTranslation } from 'react-i18next';
6 | import { Title } from '../components/Shared';
7 |
8 | const FormItem = Form.Item;
9 |
10 | const ForgotPassword = ({ form, history }) => {
11 | const { getFieldDecorator } = form;
12 | const { t } = useTranslation();
13 |
14 | function onSubmit(e) {
15 | e.preventDefault();
16 | form.validateFields((err, values) => {
17 | if (!err) {
18 | console.log('Received values of form: ', values);
19 | }
20 | });
21 | }
22 |
23 | return (
24 |
60 | );
61 | };
62 |
63 | ForgotPassword.propTypes = {
64 | form: PropTypes.object,
65 | history: PropTypes.object,
66 | };
67 |
68 | export default withRouter(Form.create()(ForgotPassword));
69 |
--------------------------------------------------------------------------------
/packages/web/src/pages/Form.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import { Form as FormCrud, useCrudForm } from 'react-easy-crud';
4 | import userConfig from '../config/cruds/user';
5 |
6 | const Form = ({ match }) => {
7 | console.log(match.params.id);
8 | const propsForm = useCrudForm(userConfig, match.params.id || null);
9 | if (match.params.id > 0 && !propsForm.fields[1].hasOwnProperty('value')) {
10 | return 'Loading...';
11 | }
12 | return (
13 |
14 | );
15 | };
16 |
17 | Form.propTypes = {
18 | match: PropTypes.object,
19 | };
20 |
21 | export default Form;
22 |
--------------------------------------------------------------------------------
/packages/web/src/pages/Home.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Link } from 'react-router-dom';
3 | import { useTranslation } from 'react-i18next';
4 | import { Button } from 'antd';
5 | import { Title } from '../components/Shared';
6 |
7 | const Home = () => {
8 | const { t } = useTranslation();
9 | return (
10 |
11 |
12 |
13 |
16 |
17 |
18 | );
19 | };
20 |
21 | export default Home;
22 |
--------------------------------------------------------------------------------
/packages/web/src/pages/Layout.js:
--------------------------------------------------------------------------------
1 | import React, { useState, Suspense } from 'react';
2 | import PropTypes from 'prop-types';
3 | import { Route, Switch, withRouter, Redirect } from 'react-router-dom';
4 | import { Layout, Drawer, Spin } from 'antd';
5 | import { GUEST, LOGGED } from '../config/constants';
6 | import routes from '../config/routes';
7 | import { MenuPrimary, Logo, Header } from '../components/Layout';
8 | import { useAuthenticated } from '../components/Auth';
9 | import PageNotFound404 from './404';
10 |
11 | const { Footer, Content, Sider } = Layout;
12 |
13 | const Routes = () => {
14 | const { isAuthenticated } = useAuthenticated();
15 | return (
16 | }>
17 |
18 | {routes.map(route => (
19 |
24 | route.when === undefined ||
25 | route.when === null ||
26 | (isAuthenticated === false && route.when === GUEST) ||
27 | (isAuthenticated === true && route.when === LOGGED) ? (
28 | React.createElement(route.component, props, null)
29 | ) : (
30 |
31 | )
32 | }
33 | />
34 | ))}
35 |
36 |
37 |
38 | );
39 | };
40 |
41 | const Document = props => {
42 | const {
43 | location: { pathname },
44 | } = props;
45 |
46 | const [visible, setVisible] = useState(false);
47 | const [isCollapsed, setIsCollapsed] = useState(false);
48 |
49 | const showDrawer = () => setVisible(true);
50 | const onClose = () => setVisible(false);
51 | const toggleCollapse = () => setIsCollapsed(!isCollapsed);
52 |
53 | return (
54 |
55 |
62 |
63 | Dashboard
64 |
65 |
66 | }
68 | placement="left"
69 | onClose={onClose}
70 | visible={visible}
71 | bodyStyle={{ padding: 0, margin: 0 }}>
72 |
73 |
74 |
75 |
81 |
82 |
83 |
84 |
85 |
86 |
87 | );
88 | };
89 |
90 | Document.propTypes = {
91 | location: PropTypes.object,
92 | };
93 |
94 | export default withRouter(Document);
95 |
--------------------------------------------------------------------------------
/packages/web/src/pages/List.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import { List as ListCrud, useCrudList } from 'react-easy-crud';
4 | import userConfig from '../config/cruds/user';
5 |
6 | const List = props => {
7 | const { history } = props;
8 | const { columns, dataSource, onDelete, loading } = useCrudList(userConfig);
9 | return (
10 | history.push('/form'),
19 | },
20 | ]}
21 | addActions={[
22 | {
23 | text: 'Edit',
24 | icon: 'edit',
25 | type: 'primary',
26 | onClick: record =>
27 | history.push(`/form/${record[userConfig.keyName]}`),
28 | },
29 | {
30 | text: 'Delete',
31 | icon: 'delete',
32 | type: 'danger',
33 | confirm: 'Are you sure?',
34 | onClick: record => onDelete(record[userConfig.keyName]),
35 | },
36 | ]}
37 | loading={loading}
38 | pagination={{
39 | pageSize: 20,
40 | showQuickJumper: true,
41 | showSizeChanger: true,
42 | }}
43 | />
44 | );
45 | };
46 |
47 | List.propTypes = {
48 | history: PropTypes.object,
49 | };
50 |
51 | export default List;
52 |
--------------------------------------------------------------------------------
/packages/web/src/pages/Login.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { connect } from 'react-redux';
3 | import { Login as LoginForm } from '../components/Auth';
4 |
5 | const Login = props => {
6 | return ;
7 | };
8 |
9 | const mapState = state => ({
10 | loading: state.loading.effects.auth.authentication,
11 | });
12 |
13 | const mapDispatch = dispatch => ({
14 | authentication: (username, password) =>
15 | dispatch.auth.authentication({ username, password }),
16 | });
17 |
18 | export default connect(
19 | mapState,
20 | mapDispatch,
21 | )(Login);
22 |
--------------------------------------------------------------------------------
/packages/web/src/pages/Register.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Register as RegisterForm } from '../components/Auth';
3 |
4 | const Register = () => ;
5 |
6 | export default Register;
7 |
--------------------------------------------------------------------------------
/packages/web/src/registerServiceWorker.js:
--------------------------------------------------------------------------------
1 | // This optional code is used to register a service worker.
2 | // register() is not called by default.
3 |
4 | // This lets the app load faster on subsequent visits in production, and gives
5 | // it offline capabilities. However, it also means that developers (and users)
6 | // will only see deployed updates on subsequent visits to a page, after all the
7 | // existing tabs open on the page have been closed, since previously cached
8 | // resources are updated in the background.
9 |
10 | // To learn more about the benefits of this model and instructions on how to
11 | // opt-in, read http://bit.ly/CRA-PWA
12 |
13 | const isLocalhost = Boolean(
14 | window.location.hostname === 'localhost' ||
15 | // [::1] is the IPv6 localhost address.
16 | window.location.hostname === '[::1]' ||
17 | // 127.0.0.1/8 is considered localhost for IPv4.
18 | window.location.hostname.match(
19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/,
20 | ),
21 | );
22 |
23 | export function register(config) {
24 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
25 | // The URL constructor is available in all browsers that support SW.
26 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href);
27 | if (publicUrl.origin !== window.location.origin) {
28 | // Our service worker won't work if PUBLIC_URL is on a different origin
29 | // from what our page is served on. This might happen if a CDN is used to
30 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374
31 | return;
32 | }
33 |
34 | window.addEventListener('load', () => {
35 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
36 |
37 | if (isLocalhost) {
38 | // This is running on localhost. Let's check if a service worker still exists or not.
39 | checkValidServiceWorker(swUrl, config);
40 |
41 | // Add some additional logging to localhost, pointing developers to the
42 | // service worker/PWA documentation.
43 | navigator.serviceWorker.ready.then(() => {
44 | console.log(
45 | 'This web app is being served cache-first by a service ' +
46 | 'worker. To learn more, visit http://bit.ly/CRA-PWA',
47 | );
48 | });
49 | } else {
50 | // Is not localhost. Just register service worker
51 | registerValidSW(swUrl, config);
52 | }
53 | });
54 | }
55 | }
56 |
57 | function registerValidSW(swUrl, config) {
58 | navigator.serviceWorker
59 | .register(swUrl)
60 | .then(registration => {
61 | registration.onupdatefound = () => {
62 | const installingWorker = registration.installing;
63 | if (installingWorker == null) {
64 | return;
65 | }
66 | installingWorker.onstatechange = () => {
67 | if (installingWorker.state === 'installed') {
68 | if (navigator.serviceWorker.controller) {
69 | // At this point, the updated precached content has been fetched,
70 | // but the previous service worker will still serve the older
71 | // content until all client tabs are closed.
72 | console.log(
73 | 'New content is available and will be used when all ' +
74 | 'tabs for this page are closed. See http://bit.ly/CRA-PWA.',
75 | );
76 |
77 | // Execute callback
78 | if (config && config.onUpdate) {
79 | config.onUpdate(registration);
80 | }
81 | } else {
82 | // At this point, everything has been precached.
83 | // It's the perfect time to display a
84 | // "Content is cached for offline use." message.
85 | console.log('Content is cached for offline use.');
86 |
87 | // Execute callback
88 | if (config && config.onSuccess) {
89 | config.onSuccess(registration);
90 | }
91 | }
92 | }
93 | };
94 | };
95 | })
96 | .catch(error => {
97 | console.error('Error during service worker registration:', error);
98 | });
99 | }
100 |
101 | function checkValidServiceWorker(swUrl, config) {
102 | // Check if the service worker can be found. If it can't reload the page.
103 | fetch(swUrl)
104 | .then(response => {
105 | // Ensure service worker exists, and that we really are getting a JS file.
106 | const contentType = response.headers.get('content-type');
107 | if (
108 | response.status === 404 ||
109 | (contentType != null && contentType.indexOf('javascript') === -1)
110 | ) {
111 | // No service worker found. Probably a different app. Reload the page.
112 | navigator.serviceWorker.ready.then(registration => {
113 | registration.unregister().then(() => {
114 | window.location.reload();
115 | });
116 | });
117 | } else {
118 | // Service worker found. Proceed as normal.
119 | registerValidSW(swUrl, config);
120 | }
121 | })
122 | .catch(() => {
123 | console.log(
124 | 'No internet connection found. App is running in offline mode.',
125 | );
126 | });
127 | }
128 |
129 | export function unregister() {
130 | if ('serviceWorker' in navigator) {
131 | navigator.serviceWorker.ready.then(registration => {
132 | registration.unregister();
133 | });
134 | }
135 | }
136 |
--------------------------------------------------------------------------------
/packages/web/src/services/auth.js:
--------------------------------------------------------------------------------
1 | import instance from './instance';
2 | import { auth } from '../config/services';
3 |
4 | export const authenticationService = (userName, password) =>
5 | instance.post(auth.login, { userName, password });
6 |
--------------------------------------------------------------------------------
/packages/web/src/services/instance.js:
--------------------------------------------------------------------------------
1 | import axios from 'axios';
2 |
3 | class AxiosCreate {
4 | static _instance = null;
5 |
6 | constructor() {
7 | const headers = {};
8 |
9 | this._instance = axios.create({
10 | baseURL: process.env.REACT_APP_API || 'http://localhost:4000',
11 | headers,
12 | });
13 | }
14 |
15 | get instance() {
16 | return this._instance;
17 | }
18 | }
19 |
20 | const InstAx = new AxiosCreate();
21 |
22 | InstAx.instance.setToken = function(token) {
23 | InstAx.instance.defaults.headers.Authorizations = `Bearer ${token}`;
24 | };
25 |
26 | InstAx.instance.removeToken = function() {
27 | InstAx.instance.defaults.headers.Authorizations = null;
28 | };
29 |
30 | export default InstAx.instance;
31 |
--------------------------------------------------------------------------------
/packages/web/src/styles/auth.less:
--------------------------------------------------------------------------------
1 | @import (once) "../../../../node_modules/antd/dist/antd.less";
2 |
3 | .custom-auth-form {
4 | max-width: 400px;
5 | margin: 0 auto;
6 |
7 | .custom-prefix-icon {
8 | color: rgba(0,0,0,.25);
9 | }
10 |
11 | .custom-button {
12 | width: 100%;
13 | }
14 | }
15 |
16 | .custom-form-login {
17 | .custom-auth-form();
18 |
19 | .custom-forgot-link {
20 | float: right;
21 | }
22 | }
23 |
24 | .custom-form-register {
25 | .custom-auth-form();
26 | }
27 |
28 | .custom-form-forgot {
29 | .custom-auth-form();
30 | }
31 |
--------------------------------------------------------------------------------
/packages/web/src/styles/index.less:
--------------------------------------------------------------------------------
1 | @import (once) "../../../../node_modules/antd/dist/antd.less";
2 |
3 | @primary-color: #2779ff;
4 | @layout-header-background: #ffffff;
5 | @layout-sider-background: #ffffff;
6 | @menu-dark-color: #ffffff;
7 | @menu-dark-item-active-bg: lighten(@primary-color, 5%);
8 | @table-border-radius-base: 1rem;
9 | @form-item-margin-bottom: 20px;
10 |
11 | @import "layout.less";
12 | @import "table.less";
13 | @import "title.less";
14 | @import "auth.less";
15 |
16 | .custom-full-width {
17 | width: 100%;
18 | }
19 |
20 | .custom-align-right {
21 | text-align: right;
22 | }
23 |
24 | .custom-align-middle {
25 | vertical-align: middle;
26 | }
27 |
--------------------------------------------------------------------------------
/packages/web/src/styles/layout.less:
--------------------------------------------------------------------------------
1 | @import (once) "../../../../node_modules/antd/dist/antd.less";
2 |
3 | .custom-layout-spin {
4 | width: 100%;
5 | height: 300px;
6 | }
7 |
8 | .custom-layout {
9 | .custom-layout-sider {
10 | overflow: auto;
11 | height: 100vh;
12 | position: fixed;
13 | left: 0;
14 | box-shadow: 0 64px 5px rgba(0, 0, 0, 0.15);
15 | border-right: 1px solid #f0f0f0;
16 | }
17 |
18 | .custom-layout-content {
19 | padding: 1rem;
20 | min-height: calc(~"100vh - 64px - 69px");
21 | }
22 |
23 | .custom-menu-title {
24 | padding: 0.8rem 0 1rem 1.5rem;
25 | display: inline-block;
26 | }
27 |
28 | .custom-header {
29 | padding-left: 1rem;
30 | padding-right: 1rem;
31 | box-shadow: 0 1px 5px rgba(0, 0, 0, 0.15);
32 |
33 | .custom-header-toggle-icon {
34 | font-size: 1.5rem;
35 | margin-top: 1.22rem;
36 | color: grey;
37 | }
38 | }
39 |
40 | .custom-logo {
41 | width: 100%;
42 | padding: 1rem;
43 | text-align: center;
44 | }
45 |
46 | .custom-menu-item-header {
47 | height: 64px;
48 | padding-top: 8px;
49 |
50 | .custom-menu-header-item-icon {
51 | font-size: 1.2rem;
52 | vertical-align: middle;
53 | }
54 | }
55 | }
56 |
57 | .custom-header-user {
58 | display: inline-flex;
59 | justify-content: flex-end;
60 | align-items: center;
61 | margin-left: 5px;
62 | height: 100%;
63 | text-align: right;
64 | cursor: pointer;
65 | }
66 |
--------------------------------------------------------------------------------
/packages/web/src/styles/table.less:
--------------------------------------------------------------------------------
1 | @import (once) "../../../../node_modules/antd/dist/antd.less";
2 |
3 | .ant-table td { white-space: nowrap; }
4 |
5 | .custom-table {
6 | .custom-search-filter {
7 | padding: 8px;
8 | width: 220px;
9 | }
10 |
11 | .ant-table-filter-dropdown-btns {
12 | & > a {
13 | padding: 0 5px;
14 | }
15 | }
16 |
17 | .ant-table-thead > tr > th.ant-table-column-sort {
18 | background-color: transparent;
19 | }
20 | .ant-table-tbody > tr > td.ant-table-column-sort {
21 | background-color: lighten(@primary-color, 40%);
22 | }
23 |
24 | .ant-table-body{
25 | overflow: auto !important;
26 | }
27 |
28 | table {
29 | border-collapse: separate;
30 | border-spacing: 0 5px;
31 |
32 | tr {
33 | & > td {
34 | padding: 10px;
35 | background-color: #ffffff;
36 | }
37 | & > th { background-color: transparent; }
38 | & > td, & > th { border: none !important; }
39 | & > td:first-child, & > th:first-child { border-top-left-radius: 8px; }
40 | & > td:last-child, & > th:last-child { border-top-right-radius: 8px; }
41 | & > td:first-child, & > th:first-child { border-bottom-left-radius: 8px; }
42 | & > td:last-child, & > th:last-child { border-bottom-right-radius: 8px; }
43 | }
44 |
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/packages/web/src/styles/title.less:
--------------------------------------------------------------------------------
1 | @import (once) "../../../../node_modules/antd/dist/antd.less";
2 |
3 | .custom-line-bottom {
4 | content:'';
5 | position: absolute;
6 | bottom: 0;
7 | display: inline-block;
8 | height: 2px;
9 | background-color: @primary-color;
10 | }
11 |
12 | .custom-title {
13 | position: relative;
14 | color: @primary-color;
15 | font-size: 2.5rem;
16 |
17 | @media screen and (max-width: @screen-sm-max) {
18 | font-size: 2rem;
19 | }
20 |
21 | &::before {
22 | .custom-line-bottom();
23 | left: 0;
24 | width: 120px;
25 | @media screen and (max-width: @screen-sm-max) {
26 | width: 80px;
27 | }
28 | }
29 |
30 | &::after {
31 | .custom-line-bottom();
32 | left: 124px;
33 | width: 4px;
34 | @media screen and (max-width: @screen-sm-max) {
35 | left: 84px;
36 | }
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/packages/web/src/utils/general.js:
--------------------------------------------------------------------------------
1 | export const nextNumber = (next = 1) => () => next++;
2 |
3 | const nextRouteIndex = nextNumber();
4 | export const createRoute = (url, component, when = null, exact = false) => ({
5 | index: nextRouteIndex(),
6 | path: url,
7 | component,
8 | when,
9 | exact,
10 | });
11 |
12 | const nextMenuIndex = nextNumber();
13 | export const createMenu = (url, title, icon, when) => ({
14 | index: nextMenuIndex(),
15 | title,
16 | path: url,
17 | icon,
18 | when,
19 | });
20 |
21 | export const createComponent = (component, when) => ({
22 | index: nextMenuIndex(),
23 | component,
24 | when,
25 | });
26 |
27 | export const sortString = key => (a, b) =>
28 | a[key] ? a[key].localeCompare(b[key]) : true;
29 | export const sortNumber = key => (a, b) => a[key] - b[key];
30 | export const sortBool = key => (a, b) => b[key] - a[key];
31 |
--------------------------------------------------------------------------------