├── .babelrc ├── .gitignore ├── 404.html ├── LICENSE ├── README.md ├── assets ├── build │ ├── app.min.04d3e4b7cc53af1c.js │ ├── conf.js │ ├── index_dev.html │ └── index_prod.html ├── css │ ├── bootstrap-reboot.css │ └── fa.css ├── font │ ├── fa.eot │ ├── fa.svg │ ├── fa.ttf │ ├── fa.woff │ └── fa.woff2 ├── html │ ├── 404_no_root.html │ ├── 404_with_root.html │ ├── index_dev.html │ └── index_prod.html ├── images │ ├── default-about.jpg │ ├── default-contact.jpg │ ├── default-sidebar.jpg │ ├── favicon.ico │ ├── profile-1.jpg │ └── react_logo.png └── js │ ├── updater.js │ └── vendors │ ├── aphrodite.js │ ├── history.js │ ├── lodash.debounce.js │ ├── prop-types.js │ ├── react-helmet.js │ ├── react-redux.js │ ├── react-router-dom.js │ ├── react.js │ ├── recompose.js │ ├── redux-thunk.js │ └── redux.js ├── conf.json ├── dev ├── build │ └── build-app.js ├── dev-server.js └── middleware │ ├── cache.js │ ├── resolveToUrl.js │ ├── send.js │ ├── transform-file.js │ ├── transform-middleware.js │ ├── update-middleware.js │ └── update.js ├── index.html ├── package-lock.json ├── package.json └── src ├── app.js ├── components ├── blocks │ ├── article.js │ └── category.js ├── disqus │ ├── disqusCount.js │ └── disqusThread.js ├── form │ └── baseInput.js └── layout │ ├── footer.js │ ├── menu.js │ ├── page.js │ └── sidebar.js ├── lib ├── api.js ├── drive.js └── mail.js ├── modules ├── article │ ├── actionCreators.js │ ├── actionTypes.js │ ├── reducer.js │ └── selectors.js ├── category │ ├── actionCreators.js │ ├── actionTypes.js │ ├── reducer.js │ └── selectors.js ├── main │ ├── containers │ │ ├── noMatch.js │ │ └── root.js │ ├── rootReducer.js │ └── store │ │ └── configureStore.js └── route │ ├── ConnectedRouter.js │ ├── actionCreators.js │ ├── actionTypes.js │ ├── middleware.js │ ├── reducer.js │ └── selectors.js ├── routes ├── about.js ├── article.js ├── category.js ├── contact.js ├── home.js └── routes.js ├── styles ├── blocks.js ├── buttons.js └── input.js └── utils ├── capitalize.js ├── jsonpCall.js └── uuid.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/react"], 3 | "plugins": [ 4 | ["@babel/plugin-proposal-class-properties"], 5 | ["@babel/plugin-syntax-object-rest-spread"], 6 | ["module-resolver", { 7 | "root": ["./"], 8 | "alias": { 9 | "conf": "./conf.js", 10 | "components": "./src/components", 11 | "article": "./src/modules/article", 12 | "category": "./src/modules/category", 13 | "contact": "./src/modules/contact", 14 | "display": "./src/modules/display", 15 | "lib": "./src/lib", 16 | "main": "./src/modules/main", 17 | "route": "./src/modules/route", 18 | "routes": "./src/routes", 19 | "styles": "./src/styles", 20 | "utils": "./src/utils" 21 | } 22 | }] 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | node_modules 3 | 4 | -------------------------------------------------------------------------------- /404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Single Page Apps for GitHub Pages 6 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Antoine S 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React without Webpack 2 | 3 | The lightest React Starter Kit. Using native modules, we can avoid bundling and have instant reloading during development. 4 | 5 | ![alt text](https://i.imgur.com/fVjl1jV.gif "Loading native JS modules") 6 | 7 | ### Usage 8 | #### 1. Installation 9 | Clone the project and install dependencies. 10 | ```bash 11 | git clone git@github.com:misterfresh/react-without-webpack.git 12 | cd react-without-webpack 13 | npm install 14 | ``` 15 | #### 2. Development 16 | Start the development server, edit the code in the /src folder. 17 | ```bash 18 | npm run dev 19 | ``` 20 | #### 3. Deploy 21 | Build the project: 22 | ```bash 23 | npm run build 24 | ``` 25 | Run the built project: 26 | ```bash 27 | npm run local 28 | ``` 29 | Deploy to GitHub Pages by pushing to a "gh-pages" branch. 30 | 31 | ### How it works 32 | Some details in this medium article : [React without Webpack](https://medium.com/@antoine.stollsteiner/react-without-webpack-a-dream-come-true-6cf24a1ff766) 33 | 34 | Live demo here: 35 | http://misterfresh.github.io/react-drive-cms/ 36 | -------------------------------------------------------------------------------- /assets/build/conf.js: -------------------------------------------------------------------------------- 1 | export default {"author":"React Drive CMS","dashboardId":"1-on_GfmvaEcOk7HcWfKb8B6KFRv166RkLN2YmDEtDn4","sendContactMessageUrlId":"AKfycbyL4vW1UWs4mskuDjLoLmf1Hjan1rTLEca6i2Hi2H_4CtKUN84d","shortname":"easydrivecms","root":"react-drive-cms"} -------------------------------------------------------------------------------- /assets/build/index_dev.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 8 | React Drive CMS 9 | 10 | 11 | 12 | 34 | 35 | 36 | 81 | 82 | 83 |
84 | 85 | 86 | -------------------------------------------------------------------------------- /assets/build/index_prod.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 8 | React Drive CMS 9 | 10 | 11 | 12 | 34 | 35 | 81 | 82 | 83 |
84 | 85 | 86 | -------------------------------------------------------------------------------- /assets/css/bootstrap-reboot.css: -------------------------------------------------------------------------------- 1 | *, 2 | *::before, 3 | *::after { 4 | box-sizing: border-box; 5 | } 6 | 7 | html { 8 | font-family: sans-serif; 9 | line-height: 1.15; 10 | -webkit-text-size-adjust: 100%; 11 | -ms-text-size-adjust: 100%; 12 | -ms-overflow-style: scrollbar; 13 | -webkit-tap-highlight-color: rgba(0, 0, 0, 0); 14 | } 15 | 16 | article, aside, figcaption, figure, footer, header, hgroup, main, nav, section { 17 | display: block; 18 | } 19 | 20 | body { 21 | margin: 0; 22 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; 23 | font-size: 1rem; 24 | font-weight: 400; 25 | line-height: 1.5; 26 | color: #212529; 27 | text-align: left; 28 | background-color: #fff; 29 | } 30 | 31 | [tabindex="-1"]:focus { 32 | outline: 0 !important; 33 | } 34 | 35 | hr { 36 | box-sizing: content-box; 37 | height: 0; 38 | overflow: visible; 39 | } 40 | 41 | h1, h2, h3, h4, h5, h6 { 42 | margin-top: 0; 43 | margin-bottom: 0.5rem; 44 | } 45 | 46 | p { 47 | margin-top: 0; 48 | margin-bottom: 1rem; 49 | } 50 | 51 | abbr[title], 52 | abbr[data-original-title] { 53 | text-decoration: underline; 54 | -webkit-text-decoration: underline dotted; 55 | text-decoration: underline dotted; 56 | cursor: help; 57 | border-bottom: 0; 58 | } 59 | 60 | address { 61 | margin-bottom: 1rem; 62 | font-style: normal; 63 | line-height: inherit; 64 | } 65 | 66 | ol, 67 | ul, 68 | dl { 69 | margin-top: 0; 70 | margin-bottom: 1rem; 71 | } 72 | 73 | ol ol, 74 | ul ul, 75 | ol ul, 76 | ul ol { 77 | margin-bottom: 0; 78 | } 79 | 80 | dt { 81 | font-weight: 700; 82 | } 83 | 84 | dd { 85 | margin-bottom: .5rem; 86 | margin-left: 0; 87 | } 88 | 89 | blockquote { 90 | margin: 0 0 1rem; 91 | } 92 | 93 | dfn { 94 | font-style: italic; 95 | } 96 | 97 | b, 98 | strong { 99 | font-weight: bolder; 100 | } 101 | 102 | small { 103 | font-size: 80%; 104 | } 105 | 106 | sub, 107 | sup { 108 | position: relative; 109 | font-size: 75%; 110 | line-height: 0; 111 | vertical-align: baseline; 112 | } 113 | 114 | sub { 115 | bottom: -.25em; 116 | } 117 | 118 | sup { 119 | top: -.5em; 120 | } 121 | 122 | a { 123 | color: #007bff; 124 | text-decoration: none; 125 | background-color: transparent; 126 | -webkit-text-decoration-skip: objects; 127 | } 128 | 129 | a:hover { 130 | color: #0056b3; 131 | text-decoration: underline; 132 | } 133 | 134 | a:not([href]):not([tabindex]) { 135 | color: inherit; 136 | text-decoration: none; 137 | } 138 | 139 | a:not([href]):not([tabindex]):hover, a:not([href]):not([tabindex]):focus { 140 | color: inherit; 141 | text-decoration: none; 142 | } 143 | 144 | a:not([href]):not([tabindex]):focus { 145 | outline: 0; 146 | } 147 | 148 | pre, 149 | code, 150 | kbd, 151 | samp { 152 | font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; 153 | font-size: 1em; 154 | } 155 | 156 | pre { 157 | margin-top: 0; 158 | margin-bottom: 1rem; 159 | overflow: auto; 160 | -ms-overflow-style: scrollbar; 161 | } 162 | 163 | figure { 164 | margin: 0 0 1rem; 165 | } 166 | 167 | img { 168 | vertical-align: middle; 169 | border-style: none; 170 | } 171 | 172 | svg { 173 | overflow: hidden; 174 | vertical-align: middle; 175 | } 176 | 177 | table { 178 | border-collapse: collapse; 179 | } 180 | 181 | caption { 182 | padding-top: 0.75rem; 183 | padding-bottom: 0.75rem; 184 | color: #6c757d; 185 | text-align: left; 186 | caption-side: bottom; 187 | } 188 | 189 | th { 190 | text-align: inherit; 191 | } 192 | 193 | label { 194 | display: inline-block; 195 | margin-bottom: 0.5rem; 196 | } 197 | 198 | button { 199 | border-radius: 0; 200 | } 201 | 202 | button:focus { 203 | outline: 1px dotted; 204 | outline: 5px auto -webkit-focus-ring-color; 205 | } 206 | 207 | input, 208 | button, 209 | select, 210 | optgroup, 211 | textarea { 212 | margin: 0; 213 | font-family: inherit; 214 | font-size: inherit; 215 | line-height: inherit; 216 | } 217 | 218 | button, 219 | input { 220 | overflow: visible; 221 | } 222 | 223 | button, 224 | select { 225 | text-transform: none; 226 | } 227 | 228 | button, 229 | html [type="button"], 230 | [type="reset"], 231 | [type="submit"] { 232 | -webkit-appearance: button; 233 | } 234 | 235 | button::-moz-focus-inner, 236 | [type="button"]::-moz-focus-inner, 237 | [type="reset"]::-moz-focus-inner, 238 | [type="submit"]::-moz-focus-inner { 239 | padding: 0; 240 | border-style: none; 241 | } 242 | 243 | input[type="radio"], 244 | input[type="checkbox"] { 245 | box-sizing: border-box; 246 | padding: 0; 247 | } 248 | 249 | input[type="date"], 250 | input[type="time"], 251 | input[type="datetime-local"], 252 | input[type="month"] { 253 | -webkit-appearance: listbox; 254 | } 255 | 256 | textarea { 257 | overflow: auto; 258 | resize: vertical; 259 | } 260 | 261 | fieldset { 262 | min-width: 0; 263 | padding: 0; 264 | margin: 0; 265 | border: 0; 266 | } 267 | 268 | legend { 269 | display: block; 270 | width: 100%; 271 | max-width: 100%; 272 | padding: 0; 273 | margin-bottom: .5rem; 274 | font-size: 1.5rem; 275 | line-height: inherit; 276 | color: inherit; 277 | white-space: normal; 278 | } 279 | 280 | progress { 281 | vertical-align: baseline; 282 | } 283 | 284 | [type="number"]::-webkit-inner-spin-button, 285 | [type="number"]::-webkit-outer-spin-button { 286 | height: auto; 287 | } 288 | 289 | [type="search"] { 290 | outline-offset: -2px; 291 | -webkit-appearance: none; 292 | } 293 | 294 | [type="search"]::-webkit-search-cancel-button, 295 | [type="search"]::-webkit-search-decoration { 296 | -webkit-appearance: none; 297 | } 298 | 299 | ::-webkit-file-upload-button { 300 | font: inherit; 301 | -webkit-appearance: button; 302 | } 303 | 304 | output { 305 | display: inline-block; 306 | } 307 | 308 | summary { 309 | display: list-item; 310 | cursor: pointer; 311 | } 312 | 313 | template { 314 | display: none; 315 | } 316 | 317 | [hidden] { 318 | display: none !important; 319 | } 320 | -------------------------------------------------------------------------------- /assets/css/fa.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'fontello'; 3 | src: url('../font/fa.eot?76652049'); 4 | src: url('../font/fa.eot?76652049#iefix') format('embedded-opentype'), 5 | url('../font/fa.woff2?76652049') format('woff2'), 6 | url('../font/fa.woff?76652049') format('woff'), 7 | url('../font/fa.ttf?76652049') format('truetype'), 8 | url('../font/fa.svg?76652049#fontello') format('svg'); 9 | font-weight: normal; 10 | font-style: normal; 11 | } 12 | /* Chrome hack: SVG is rendered more smooth in Windozze. 100% magic, uncomment if you need it. */ 13 | /* Note, that will break hinting! In other OS-es font will be not as sharp as it could be */ 14 | /* 15 | @media screen and (-webkit-min-device-pixel-ratio:0) { 16 | @font-face { 17 | font-family: 'fontello'; 18 | src: url('../font/fontello.svg?76652049#fontello') format('svg'); 19 | } 20 | } 21 | */ 22 | 23 | [class^="icon-"]:before, [class*=" icon-"]:before { 24 | font-family: "fontello"; 25 | font-style: normal; 26 | font-weight: normal; 27 | speak: none; 28 | 29 | display: inline-block; 30 | text-decoration: inherit; 31 | width: 1em; 32 | margin-right: .2em; 33 | text-align: center; 34 | /* opacity: .8; */ 35 | 36 | /* For safety - reset parent styles, that can break glyph codes*/ 37 | font-variant: normal; 38 | text-transform: none; 39 | 40 | /* fix buttons height, for twitter bootstrap */ 41 | line-height: 1em; 42 | 43 | /* Animation center compensation - margins should be symmetric */ 44 | /* remove if not needed */ 45 | margin-left: .2em; 46 | 47 | /* you can be more comfortable with increased icons size */ 48 | /* font-size: 120%; */ 49 | 50 | /* Font smoothing. That was taken from TWBS */ 51 | -webkit-font-smoothing: antialiased; 52 | -moz-osx-font-smoothing: grayscale; 53 | 54 | /* Uncomment for 3D effect */ 55 | /* text-shadow: 1px 1px 1px rgba(127, 127, 127, 0.3); */ 56 | } 57 | 58 | .icon-bookmark:before { content: '\e800'; } /* '' */ 59 | .icon-user:before { content: '\e801'; } /* '' */ 60 | .icon-home:before { content: '\e802'; } /* '' */ 61 | .icon-right-open:before { content: '\e803'; } /* '' */ 62 | .icon-left-open:before { content: '\e804'; } /* '' */ 63 | .icon-up-open:before { content: '\e805'; } /* '' */ 64 | .icon-down-open:before { content: '\e806'; } /* '' */ 65 | .icon-star:before { content: '\e807'; } /* '' */ 66 | .icon-heart:before { content: '\e808'; } /* '' */ 67 | .icon-link:before { content: '\e809'; } /* '' */ 68 | .icon-picture:before { content: '\e80a'; } /* '' */ 69 | .icon-phone:before { content: '\e80b'; } /* '' */ 70 | .icon-twitter:before { content: '\f099'; } /* '' */ 71 | .icon-mail-alt:before { content: '\f0e0'; } /* '' */ 72 | .icon-paper-plane:before { content: '\f1d8'; } /* '' */ 73 | .icon-facebook-official:before { content: '\f230'; } /* '' */ 74 | .icon-facebook-squared:before { content: '\f308'; } /* '' */ 75 | -------------------------------------------------------------------------------- /assets/font/fa.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/misterfresh/react-without-webpack/e7b93cd9e8537db10c848f8f57cc085c34e1fb3e/assets/font/fa.eot -------------------------------------------------------------------------------- /assets/font/fa.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Copyright (C) 2019 by original authors @ fontello.com 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /assets/font/fa.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/misterfresh/react-without-webpack/e7b93cd9e8537db10c848f8f57cc085c34e1fb3e/assets/font/fa.ttf -------------------------------------------------------------------------------- /assets/font/fa.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/misterfresh/react-without-webpack/e7b93cd9e8537db10c848f8f57cc085c34e1fb3e/assets/font/fa.woff -------------------------------------------------------------------------------- /assets/font/fa.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/misterfresh/react-without-webpack/e7b93cd9e8537db10c848f8f57cc085c34e1fb3e/assets/font/fa.woff2 -------------------------------------------------------------------------------- /assets/html/404_no_root.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Single Page Apps for GitHub Pages 6 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /assets/html/404_with_root.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Single Page Apps for GitHub Pages 6 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /assets/html/index_dev.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 8 | React Drive CMS 9 | 10 | 11 | 12 | 34 | 35 | 36 | 81 | 82 | 83 |
84 | 85 | 86 | -------------------------------------------------------------------------------- /assets/html/index_prod.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 8 | React Drive CMS 9 | 10 | 11 | 12 | 34 | 35 | 81 | 82 | 83 |
84 | 85 | 86 | -------------------------------------------------------------------------------- /assets/images/default-about.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/misterfresh/react-without-webpack/e7b93cd9e8537db10c848f8f57cc085c34e1fb3e/assets/images/default-about.jpg -------------------------------------------------------------------------------- /assets/images/default-contact.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/misterfresh/react-without-webpack/e7b93cd9e8537db10c848f8f57cc085c34e1fb3e/assets/images/default-contact.jpg -------------------------------------------------------------------------------- /assets/images/default-sidebar.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/misterfresh/react-without-webpack/e7b93cd9e8537db10c848f8f57cc085c34e1fb3e/assets/images/default-sidebar.jpg -------------------------------------------------------------------------------- /assets/images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/misterfresh/react-without-webpack/e7b93cd9e8537db10c848f8f57cc085c34e1fb3e/assets/images/favicon.ico -------------------------------------------------------------------------------- /assets/images/profile-1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/misterfresh/react-without-webpack/e7b93cd9e8537db10c848f8f57cc085c34e1fb3e/assets/images/profile-1.jpg -------------------------------------------------------------------------------- /assets/images/react_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/misterfresh/react-without-webpack/e7b93cd9e8537db10c848f8f57cc085c34e1fb3e/assets/images/react_logo.png -------------------------------------------------------------------------------- /assets/js/updater.js: -------------------------------------------------------------------------------- 1 | let source = new EventSource('/stream') 2 | 3 | source.addEventListener( 4 | 'message', 5 | function(event) { 6 | console.log('updating!', event.data) 7 | location.reload(true) 8 | }, 9 | false 10 | ) 11 | -------------------------------------------------------------------------------- /assets/js/vendors/history.js: -------------------------------------------------------------------------------- 1 | function valueEqual(t,e){if(t===e)return!0;if(null==t||null==e)return!1;if(Array.isArray(t))return Array.isArray(e)&&t.length===e.length&&t.every(function(t,n){return valueEqual(t,e[n])});const n=typeof t;if(n!==typeof e)return!1;if("object"===n){const n=t.valueOf(),a=e.valueOf();if(n!==t||a!==e)return valueEqual(n,a);const o=Object.keys(t),r=Object.keys(e);return o.length===r.length&&o.every(function(n){return valueEqual(t[n],e[n])})}return!1}function isAbsolute(t){return"/"===t.charAt(0)}function spliceOne(t,e){for(let n=e,a=n+1,o=t.length;a=0;t--){const e=a[t];"."===e?spliceOne(a,t):".."===e?(spliceOne(a,t),c++):c&&(spliceOne(a,t),c--)}if(!i)for(;c--;c)a.unshift("..");!i||""===a[0]||a[0]&&isAbsolute(a[0])||a.unshift("");let h=a.join("/");return s&&"/"!==h.substr(-1)&&(h+="/"),h}const warning=function(t,e,n){var a=arguments.length;n=new Array(a>2?a-2:0);for(var o=2;ot.addEventListener?t.addEventListener(e,n,!1):t.attachEvent("on"+e,n);export const removeEventListener=(t,e,n)=>t.removeEventListener?t.removeEventListener(e,n,!1):t.detachEvent("on"+e,n);export const getConfirmation=(t,e)=>e(window.confirm(t));export const supportsHistory=()=>{const t=window.navigator.userAgent;return(-1===t.indexOf("Android 2.")&&-1===t.indexOf("Android 4.0")||-1===t.indexOf("Mobile Safari")||-1!==t.indexOf("Chrome")||-1!==t.indexOf("Windows Phone"))&&(window.history&&"pushState"in window.history)};export const supportsPopStateOnHashChange=()=>-1===window.navigator.userAgent.indexOf("Trident");export const supportsGoWithoutReloadUsingHash=()=>-1===window.navigator.userAgent.indexOf("Firefox");export const isExtraneousPopstateEvent=t=>void 0===t.state&&-1===navigator.userAgent.indexOf("CriOS");export const addLeadingSlash=t=>"/"===t.charAt(0)?t:"/"+t;export const stripLeadingSlash=t=>"/"===t.charAt(0)?t.substr(1):t;export const hasBasename=(t,e)=>new RegExp("^"+e+"(\\/|\\?|#|$)","i").test(t);export const stripBasename=(t,e)=>hasBasename(t,e)?t.substr(e.length):t;export const stripTrailingSlash=t=>"/"===t.charAt(t.length-1)?t.slice(0,-1):t;export const parsePath=t=>{let e=t||"/",n="",a="";const o=e.indexOf("#");-1!==o&&(a=e.substr(o),e=e.substr(0,o));const r=e.indexOf("?");return-1!==r&&(n=e.substr(r),e=e.substr(0,r)),{pathname:e,search:"?"===n?"":n,hash:"#"===a?"":a}};export const createPath=t=>{const{pathname:e,search:n,hash:a}=t;let o=e||"/";return n&&"?"!==n&&(o+="?"===n.charAt(0)?n:`?${n}`),a&&"#"!==a&&(o+="#"===a.charAt(0)?a:`#${a}`),o};export const createLocation=(t,e,n,a)=>{let o;"string"==typeof t?(o=parsePath(t)).state=e:(void 0===(o={...t}).pathname&&(o.pathname=""),o.search?"?"!==o.search.charAt(0)&&(o.search="?"+o.search):o.search="",o.hash?"#"!==o.hash.charAt(0)&&(o.hash="#"+o.hash):o.hash="",void 0!==e&&void 0===o.state&&(o.state=e));try{o.pathname=decodeURI(o.pathname)}catch(t){throw t instanceof URIError?new URIError('Pathname "'+o.pathname+'" could not be decoded. This is likely caused by an invalid percent-encoding.'):t}return n&&(o.key=n),a?o.pathname?"/"!==o.pathname.charAt(0)&&(o.pathname=resolvePathname(o.pathname,a.pathname)):o.pathname=a.pathname:o.pathname||(o.pathname="/"),o};export const locationsAreEqual=(t,e)=>t.pathname===e.pathname&&t.search===e.search&&t.hash===e.hash&&t.key===e.key&&valueEqual(t.state,e.state);const createTransitionManager=()=>{let t=null;let e=[];return{setPrompt:e=>(warning(null==t,"A history supports only one prompt at a time"),t=e,()=>{t===e&&(t=null)}),confirmTransitionTo:(e,n,a,o)=>{if(null!=t){const r="function"==typeof t?t(e,n):t;"string"==typeof r?"function"==typeof a?a(r,o):(warning(!1,"A history needs a getUserConfirmation function in order to use a prompt message"),o(!0)):o(!1!==r)}else o(!0)},appendListener:t=>{let n=!0;const a=(...e)=>{n&&t(...e)};return e.push(a),()=>{n=!1,e=e.filter(t=>t!==a)}},notifyListeners:(...t)=>{e.forEach(e=>e(...t))}}},PopStateEvent="popstate",HashChangeEvent="hashchange",getHistoryState=()=>{try{return window.history.state||{}}catch(t){return{}}};export const createBrowserHistory=(t={})=>{invariant(canUseDOM,"Browser history needs a DOM");const e=window.history,n=supportsHistory(),a=!supportsPopStateOnHashChange(),{forceRefresh:o=!1,getUserConfirmation:r=getConfirmation,keyLength:i=6}=t,s=t.basename?stripTrailingSlash(addLeadingSlash(t.basename)):"",c=t=>{const{key:e,state:n}=t||{},{pathname:a,search:o,hash:r}=window.location;let i=a+o+r;return warning(!s||hasBasename(i,s),'You are attempting to use a basename on a page whose URL path does not begin with the basename. Expected path "'+i+'" to begin with "'+s+'".'),s&&(i=stripBasename(i,s)),createLocation(i,n,e)},h=()=>Math.random().toString(36).substr(2,i),l=createTransitionManager(),d=t=>{Object.assign(L,t),L.length=e.length,l.notifyListeners(L.location,L.action)},p=t=>{isExtraneousPopstateEvent(t)||f(c(t.state))},u=()=>{f(c(getHistoryState()))};let g=!1;const f=t=>{if(g)g=!1,d();else{l.confirmTransitionTo(t,"POP",r,e=>{e?d({action:"POP",location:t}):w(t)})}},w=t=>{const e=L.location;let n=v.indexOf(e.key);-1===n&&(n=0);let a=v.indexOf(t.key);-1===a&&(a=0);const o=n-a;o&&(g=!0,P(o))},m=c(getHistoryState());let v=[m.key];const y=t=>s+createPath(t),P=t=>{e.go(t)};let x=0;const b=t=>{1===(x+=t)?(addEventListener(window,"popstate",p),a&&addEventListener(window,"hashchange",u)):0===x&&(removeEventListener(window,"popstate",p),a&&removeEventListener(window,"hashchange",u))};let E=!1;const L={length:e.length,action:"POP",location:m,createHref:y,push:(t,a)=>{warning(!("object"==typeof t&&void 0!==t.state&&void 0!==a),"You should avoid providing a 2nd state argument to push when the 1st argument is a location-like object that already has state; it is ignored");const i=createLocation(t,a,h(),L.location);l.confirmTransitionTo(i,"PUSH",r,t=>{if(!t)return;const a=y(i),{key:r,state:s}=i;if(n)if(e.pushState({key:r,state:s},null,a),o)window.location.href=a;else{const t=v.indexOf(L.location.key),e=v.slice(0,-1===t?0:t+1);e.push(i.key),v=e,d({action:"PUSH",location:i})}else warning(void 0===s,"Browser history cannot push state in browsers that do not support HTML5 history"),window.location.href=a})},replace:(t,a)=>{warning(!("object"==typeof t&&void 0!==t.state&&void 0!==a),"You should avoid providing a 2nd state argument to replace when the 1st argument is a location-like object that already has state; it is ignored");const i=createLocation(t,a,h(),L.location);l.confirmTransitionTo(i,"REPLACE",r,t=>{if(!t)return;const a=y(i),{key:r,state:s}=i;if(n)if(e.replaceState({key:r,state:s},null,a),o)window.location.replace(a);else{const t=v.indexOf(L.location.key);-1!==t&&(v[t]=i.key),d({action:"REPLACE",location:i})}else warning(void 0===s,"Browser history cannot replace state in browsers that do not support HTML5 history"),window.location.replace(a)})},go:P,goBack:()=>P(-1),goForward:()=>P(1),block:(t=!1)=>{const e=l.setPrompt(t);return E||(b(1),E=!0),()=>(E&&(E=!1,b(-1)),e())},listen:t=>{const e=l.appendListener(t);return b(1),()=>{b(-1),e()}}};return L};const HashPathCoders={hashbang:{encodePath:t=>"!"===t.charAt(0)?t:"!/"+stripLeadingSlash(t),decodePath:t=>"!"===t.charAt(0)?t.substr(1):t},noslash:{encodePath:stripLeadingSlash,decodePath:addLeadingSlash},slash:{encodePath:addLeadingSlash,decodePath:addLeadingSlash}},getHashPath=()=>{const t=window.location.href,e=t.indexOf("#");return-1===e?"":t.substring(e+1)},pushHashPath=t=>window.location.hash=t,replaceHashPath=t=>{const e=window.location.href.indexOf("#");window.location.replace(window.location.href.slice(0,e>=0?e:0)+"#"+t)};export const createHashHistory=(t={})=>{invariant(canUseDOM,"Hash history needs a DOM");const e=window.history,n=supportsGoWithoutReloadUsingHash(),{getUserConfirmation:a=getConfirmation,hashType:o="slash"}=t,r=t.basename?stripTrailingSlash(addLeadingSlash(t.basename)):"",{encodePath:i,decodePath:s}=HashPathCoders[o],c=()=>{let t=s(getHashPath());return warning(!r||hasBasename(t,r),'You are attempting to use a basename on a page whose URL path does not begin with the basename. Expected path "'+t+'" to begin with "'+r+'".'),r&&(t=stripBasename(t,r)),createLocation(t)},h=createTransitionManager(),l=t=>{Object.assign(L,t),L.length=e.length,h.notifyListeners(L.location,L.action)};let d=!1,p=null;const u=()=>{const t=getHashPath(),e=i(t);if(t!==e)replaceHashPath(e);else{const t=c(),e=L.location;if(!d&&locationsAreEqual(e,t))return;if(p===createPath(t))return;p=null,g(t)}},g=t=>{if(d)d=!1,l();else{h.confirmTransitionTo(t,"POP",a,e=>{e?l({action:"POP",location:t}):f(t)})}},f=t=>{const e=L.location;let n=y.lastIndexOf(createPath(e));-1===n&&(n=0);let a=y.lastIndexOf(createPath(t));-1===a&&(a=0);const o=n-a;o&&(d=!0,P(o))},w=getHashPath(),m=i(w);w!==m&&replaceHashPath(m);const v=c();let y=[createPath(v)];const P=t=>{warning(n,"Hash history go(n) causes a full page reload in this browser"),e.go(t)};let x=0;const b=t=>{1===(x+=t)?addEventListener(window,"hashchange",u):0===x&&removeEventListener(window,"hashchange",u)};let E=!1;const L={length:e.length,action:"POP",location:v,createHref:t=>"#"+i(r+createPath(t)),push:(t,e)=>{warning(void 0===e,"Hash history cannot push state; it is ignored");const n=createLocation(t,void 0,void 0,L.location);h.confirmTransitionTo(n,"PUSH",a,t=>{if(!t)return;const e=createPath(n),a=i(r+e);if(getHashPath()!==a){p=e,pushHashPath(a);const t=y.lastIndexOf(createPath(L.location)),o=y.slice(0,-1===t?0:t+1);o.push(e),y=o,l({action:"PUSH",location:n})}else warning(!1,"Hash history cannot PUSH the same path; a new entry will not be added to the history stack"),l()})},replace:(t,e)=>{warning(void 0===e,"Hash history cannot replace state; it is ignored");const n=createLocation(t,void 0,void 0,L.location);h.confirmTransitionTo(n,"REPLACE",a,t=>{if(!t)return;const e=createPath(n),a=i(r+e);getHashPath()!==a&&(p=e,replaceHashPath(a));const o=y.indexOf(createPath(L.location));-1!==o&&(y[o]=e),l({action:"REPLACE",location:n})})},go:P,goBack:()=>P(-1),goForward:()=>P(1),block:(t=!1)=>{const e=h.setPrompt(t);return E||(b(1),E=!0),()=>(E&&(E=!1,b(-1)),e())},listen:t=>{const e=h.appendListener(t);return b(1),()=>{b(-1),e()}}};return L};const clamp=(t,e,n)=>Math.min(Math.max(t,e),n);export const createMemoryHistory=(t={})=>{const{getUserConfirmation:e,initialEntries:n=["/"],initialIndex:a=0,keyLength:o=6}=t,r=createTransitionManager(),i=t=>{Object.assign(p,t),p.length=p.entries.length,r.notifyListeners(p.location,p.action)},s=()=>Math.random().toString(36).substr(2,o),c=clamp(a,0,n.length-1),h=n.map(t=>"string"==typeof t?createLocation(t,void 0,s()):createLocation(t,void 0,t.key||s())),l=createPath,d=t=>{const n=clamp(p.index+t,0,p.entries.length-1),a=p.entries[n];r.confirmTransitionTo(a,"POP",e,t=>{t?i({action:"POP",location:a,index:n}):i()})},p={length:h.length,action:"POP",location:h[c],index:c,entries:h,createHref:l,push:(t,n)=>{warning(!("object"==typeof t&&void 0!==t.state&&void 0!==n),"You should avoid providing a 2nd state argument to push when the 1st argument is a location-like object that already has state; it is ignored");const a=createLocation(t,n,s(),p.location);r.confirmTransitionTo(a,"PUSH",e,t=>{if(!t)return;const e=p.index+1,n=p.entries.slice(0);n.length>e?n.splice(e,n.length-e,a):n.push(a),i({action:"PUSH",location:a,index:e,entries:n})})},replace:(t,n)=>{warning(!("object"==typeof t&&void 0!==t.state&&void 0!==n),"You should avoid providing a 2nd state argument to replace when the 1st argument is a location-like object that already has state; it is ignored");const a=createLocation(t,n,s(),p.location);r.confirmTransitionTo(a,"REPLACE",e,t=>{t&&(p.entries[p.index]=a,i({action:"REPLACE",location:a}))})},go:d,goBack:()=>d(-1),goForward:()=>d(1),canGo:t=>{const e=p.index+t;return e>=0&&er.setPrompt(t),listen:t=>r.appendListener(t)};return p}; -------------------------------------------------------------------------------- /assets/js/vendors/lodash.debounce.js: -------------------------------------------------------------------------------- 1 | function isObject(t){var e=typeof t;return null!=t&&("object"==e||"function"==e)}function isObjectLike(t){return null!=t&&"object"==typeof t}function objectToString(t){return nativeObjectToString.call(t)}function getRawTag(t){var e=hasOwnProperty.call(t,symToStringTag),n=t[symToStringTag];try{t[symToStringTag]=void 0;var o=!0}catch(t){}var r=nativeObjectToString.call(t);return o&&(e?t[symToStringTag]=n:delete t[symToStringTag]),r}function baseGetTag(t){return null==t?void 0===t?undefinedTag:nullTag:symToStringTag&&symToStringTag in Object(t)?getRawTag(t):objectToString(t)}function isSymbol(t){return"symbol"==typeof t||isObjectLike(t)&&baseGetTag(t)==symbolTag}function toNumber(t){if("number"==typeof t)return t;if(isSymbol(t))return NAN;if(isObject(t)){var e="function"==typeof t.valueOf?t.valueOf():t;t=isObject(e)?e+"":e}if("string"!=typeof t)return 0===t?t:+t;t=t.replace(reTrim,"");var n=reIsBinary.test(t);return n||reIsOctal.test(t)?freeParseInt(t.slice(2),n?2:8):reIsBadHex.test(t)?NAN:+t}function debounce(t,e,n){function o(e){var n=f,o=b;return f=b=void 0,m=e,g=t.apply(o,n)}function r(t){return m=t,T=setTimeout(u,e),v?o(t):g}function i(t){var n=t-m,o=e-(t-y);return j?nativeMin(o,s-n):o}function a(t){var n=t-y,o=t-m;return void 0===y||n>=e||n<0||j&&o>=s}function u(){var t=now();if(a(t))return c(t);T=setTimeout(u,i(t))}function c(t){return T=void 0,S&&f?o(t):(f=b=void 0,g)}function l(){var t=now(),n=a(t);if(f=arguments,b=this,y=t,n){if(void 0===T)return r(y);if(j)return T=setTimeout(u,e),o(y)}return void 0===T&&(T=setTimeout(u,e)),g}var f,b,s,g,T,y,m=0,v=!1,j=!1,S=!0;if("function"!=typeof t)throw new TypeError(FUNC_ERROR_TEXT);return e=toNumber(e)||0,isObject(n)&&(v=!!n.leading,s=(j="maxWait"in n)?nativeMax(toNumber(n.maxWait)||0,e):s,S="trailing"in n?!!n.trailing:S),l.cancel=function(){void 0!==T&&clearTimeout(T),m=0,f=y=b=T=void 0},l.flush=function(){return void 0===T?g:c(now())},l}var objectProto=Object.prototype,nativeObjectToString=objectProto.toString,freeGlobal="object"==typeof global&&global&&global.Object===Object&&global,freeSelf="object"==typeof self&&self&&self.Object===Object&&self,root=freeGlobal||freeSelf||Function("return this")(),Symbol=root.Symbol,hasOwnProperty=(objectProto=Object.prototype).hasOwnProperty,nativeObjectToString=objectProto.toString,symToStringTag=Symbol?Symbol.toStringTag:void 0,now=function(){return root.Date.now()},nullTag="[object Null]",undefinedTag="[object Undefined]",symToStringTag=Symbol?Symbol.toStringTag:void 0,symbolTag="[object Symbol]",NAN=NaN,reTrim=/^\s+|\s+$/g,reIsBadHex=/^[-+]0x[0-9a-f]+$/i,reIsBinary=/^0b[01]+$/i,reIsOctal=/^0o[0-7]+$/i,freeParseInt=parseInt,FUNC_ERROR_TEXT="Expected a function",nativeMax=Math.max,nativeMin=Math.min;export default debounce; -------------------------------------------------------------------------------- /assets/js/vendors/prop-types.js: -------------------------------------------------------------------------------- 1 | function createCommonjsModule(e,n){return n={exports:{}},e(n,n.exports),n.exports}function makeEmptyFunction(e){return function(){return e}}function invariant(e,n,r,t,o,a,i,u){if(validateFormat(n),!e){var c;if(void 0===n)c=new Error("Minified exception occurred; use the non-minified dev environment for the full error message and additional helpful warnings.");else{var p=[r,t,o,a,i,u],s=0;(c=new Error(n.replace(/%s/g,function(){return p[s++]}))).name="Invariant Violation"}throw c.framesToPop=1,c}}function toObject(e){if(null===e||void 0===e)throw new TypeError("Object.assign cannot be called with null or undefined");return Object(e)}function shouldUseNative(){try{if(!Object.assign)return!1;var e=new String("abc");if(e[5]="de","5"===Object.getOwnPropertyNames(e)[0])return!1;for(var n={},r=0;r<10;r++)n["_"+String.fromCharCode(r)]=r;if("0123456789"!==Object.getOwnPropertyNames(n).map(function(e){return n[e]}).join(""))return!1;var t={};return"abcdefghijklmnopqrst".split("").forEach(function(e){t[e]=e}),"abcdefghijklmnopqrst"===Object.keys(Object.assign({},t)).join("")}catch(e){return!1}}function checkPropTypes(e,n,r,t,o){for(var a in e)if(e.hasOwnProperty(a)){var i;try{invariant$1("function"==typeof e[a],"%s: %s type `%s` is invalid; it must be a function, usually from the `prop-types` package, but received `%s`.",t||"React class",r,a,typeof e[a]),i=e[a](n,a,t,r,null,ReactPropTypesSecret$1)}catch(e){i=e}if(warning$1(!i||i instanceof Error,"%s: type specification of %s `%s` is invalid; the type checker function must return `null` or an `Error` but returned a %s. You may have forgotten to pass an argument to the type checker creator (arrayOf, instanceOf, objectOf, oneOf, oneOfType, and shape all require an argument).",t||"React class",r,a,typeof i),i instanceof Error&&!(i.message in loggedTypeFailures)){loggedTypeFailures[i.message]=!0;var u=o?o():"";warning$1(!1,"Failed %s type: %s%s",r,i.message,null!=u?u:"")}}}var emptyFunction=function(){};emptyFunction.thatReturns=makeEmptyFunction,emptyFunction.thatReturnsFalse=makeEmptyFunction(!1),emptyFunction.thatReturnsTrue=makeEmptyFunction(!0),emptyFunction.thatReturnsNull=makeEmptyFunction(null),emptyFunction.thatReturnsThis=function(){return this},emptyFunction.thatReturnsArgument=function(e){return e};var emptyFunction_1=emptyFunction,validateFormat=function(e){};validateFormat=function(e){if(void 0===e)throw new Error("invariant requires an error message argument")};var invariant_1=invariant,warning=emptyFunction_1,printWarning=function(e){for(var n=arguments.length,r=Array(n>1?n-1:0),t=1;t2?r-2:0),o=2;o0&&"number"!=typeof t[0]))}function objEquiv(t,e,r){var n,s;if(isUndefinedOrNull(t)||isUndefinedOrNull(e))return!1;if(t.prototype!==e.prototype)return!1;if(isArguments(t))return!!isArguments(e)&&(t=pSlice.call(t),e=pSlice.call(e),deepEqual(t,e,r));if(isBuffer(t)){if(!isBuffer(e))return!1;if(t.length!==e.length)return!1;for(n=0;n=0;n--)if(o[n]!=T[n])return!1;for(n=o.length-1;n>=0;n--)if(s=o[n],!deepEqual(t[s],e[s],r))return!1;return typeof t==typeof e}function withSideEffect(t,e,r){function n(t){return t.displayName||t.name||"Component"}function s(t,e){for(let r in t)if(!(r in e))return!0;for(let r in e)if(t[r]!==e[r])return!0;return!1}if("function"!=typeof t)throw new Error("Expected reducePropsToState to be a function.");if("function"!=typeof e)throw new Error("Expected handleStateChangeOnClient to be a function.");if(void 0!==r&&"function"!=typeof r)throw new Error("Expected mapStateOnServer to either be undefined or a function.");return function(o){function T(){a=t(i.map(function(t){return t.props})),l.canUseDOM?e(a):r&&(a=r(a))}if("function"!=typeof o)throw new Error("Expected WrappedComponent to be a React component.");let a,i=[];class l extends Component{shouldComponentUpdate(t){let{children:e,...r}=t;return e&&e.length&&(r.children=e),s(r,this.props)}componentWillMount(){i.push(this),T()}componentDidUpdate(){T()}componentWillUnmount(){const t=i.indexOf(this);i.splice(t,1),T()}render(){return h(o,{...this.props})}}return l.displayName=`SideEffect(${n(o)})`,l.canUseDOM=!("undefined"==typeof window||!window.document||!window.document.createElement),l.peek=(()=>a),l.rewind=(()=>{if(l.canUseDOM)throw new Error("You may only call rewind() on the server. Call peek() to read the current state.");let t=a;return a=void 0,i=[],t}),l}}import{h,Component}from "./react.js";var pSlice=Array.prototype.slice,isArguments=function(t){return"[object Arguments]"==Object.prototype.toString.call(t)},deepEqual=function(t, e, r){return r||(r={}),t===e||(t instanceof Date&&e instanceof Date?t.getTime()===e.getTime():!t||!e||"object"!=typeof t&&"object"!=typeof e?r.strict?t===e:t==e:objEquiv(t,e,r))};export const TAG_NAMES={HTML:"htmlAttributes",TITLE:"title",BASE:"base",META:"meta",LINK:"link",SCRIPT:"script",NOSCRIPT:"noscript",STYLE:"style"};export const TAG_PROPERTIES={NAME:"name",CHARSET:"charset",HTTPEQUIV:"http-equiv",REL:"rel",HREF:"href",PROPERTY:"property",SRC:"src",INNER_HTML:"innerHTML",CSS_TEXT:"cssText",ITEM_PROP:"itemprop"};export const PREACT_TAG_MAP={charset:"charSet","http-equiv":"httpEquiv",itemprop:"itemProp",class:"className"};const HELMET_ATTRIBUTE="data-preact-helmet",encodeSpecialCharacters=t=>String(t).replace(/&/g,"&").replace(//g,">").replace(/"/g,""").replace(/'/g,"'"),getInnermostProperty=(t,e)=>{for(let r=t.length-1;r>=0;r--){const n=t[r];if(n[e])return n[e]}return null},getTitleFromPropsList=t=>{const e=getInnermostProperty(t,"title"),r=getInnermostProperty(t,"titleTemplate");if(r&&e)return r.replace(/%s/g,()=>e);const n=getInnermostProperty(t,"defaultTitle");return e||n||""},getOnChangeClientState=t=>getInnermostProperty(t,"onChangeClientState")||(()=>{}),getAttributesFromPropsList=(t,e)=>e.filter(e=>void 0!==e[t]).map(e=>e[t]).reduce((t,e)=>({...t,...e}),{}),getBaseTagFromPropsList=(t,e)=>e.filter(t=>void 0!==t[TAG_NAMES.BASE]).map(t=>t[TAG_NAMES.BASE]).reverse().reduce((e,r)=>{if(!e.length){const n=Object.keys(r);for(let s=0;s{const n={};return r.filter(e=>void 0!==e[t]).map(e=>e[t]).reverse().reduce((t,r)=>{const s={};r.filter(t=>{let r;const o=Object.keys(t);for(let n=0;nt.push(e));const o=Object.keys(s);for(let t=0;t{document.title=t||document.title,updateAttributes(TAG_NAMES.TITLE,e)},updateAttributes=(t,e)=>{const r=document.getElementsByTagName(t)[0],n=r.getAttribute(HELMET_ATTRIBUTE),s=n?n.split(","):[],o=[].concat(s),T=Object.keys(e);for(let t=0;t=0;t--)r.removeAttribute(o[t]);s.length===o.length?r.removeAttribute(HELMET_ATTRIBUTE):r.setAttribute(HELMET_ATTRIBUTE,s.join(","))},updateTags=(t,e)=>{const r=document.head||document.querySelector("head"),n=r.querySelectorAll(`${t}[data-preact-helmet]`),s=Array.prototype.slice.call(n),o=[];let T;return e&&e.length&&e.forEach(e=>{const r=document.createElement(t);for(const t in e)if(e.hasOwnProperty(t))if("innerHTML"===t)r.innerHTML=e.innerHTML;else if("cssText"===t)r.styleSheet?r.styleSheet.cssText=e.cssText:r.appendChild(document.createTextNode(e.cssText));else{const n=void 0===e[t]?"":e[t];r.setAttribute(t,n)}r.setAttribute(HELMET_ATTRIBUTE,"true"),s.some((t,e)=>(T=e,r.isEqualNode(t)))?s.splice(T,1):o.push(r)}),s.forEach(t=>t.parentNode.removeChild(t)),o.forEach(t=>r.appendChild(t)),{oldTags:s,newTags:o}},generateHtmlAttributesAsString=t=>Object.keys(t).reduce((e,r)=>{const n=void 0!==t[r]?`${r}="${t[r]}"`:`${r}`;return e?`${e} ${n}`:n},""),generateTitleAsString=(t,e,r)=>{const n=generateHtmlAttributesAsString(r);return n?`<${t} data-preact-helmet ${n}>${encodeSpecialCharacters(e)}`:`<${t} data-preact-helmet>${encodeSpecialCharacters(e)}`},generateTagsAsString=(t,e)=>e.reduce((e,r)=>{const n=Object.keys(r).filter(t=>!("innerHTML"===t||"cssText"===t)).reduce((t,e)=>{const n=void 0===r[e]?e:`${e}="${encodeSpecialCharacters(r[e])}"`;return t?`${t} ${n}`:n},""),s=r.innerHTML||r.cssText||"",o=-1===[TAG_NAMES.NOSCRIPT,TAG_NAMES.SCRIPT,TAG_NAMES.STYLE].indexOf(t);return`${e}<${t} data-preact-helmet ${n}${o?`>`:`>${s}`}`},""),generateTitleAsPreactComponent=(t,e,r)=>{const n={key:e,[HELMET_ATTRIBUTE]:!0},s=Object.keys(r).reduce((t,e)=>(t[e]=r[e],t),n);return[h(TAG_NAMES.TITLE,s,e)]},generateTagsAsPreactComponent=(t,e)=>e.map((e,r)=>{const n={key:r,[HELMET_ATTRIBUTE]:!0};return Object.keys(e).forEach(t=>{const r=t;if("innerHTML"===r||"cssText"===r){const t=e.innerHTML||e.cssText;n.dangerouslySetInnerHTML={__html:t}}else n[r]=e[t]}),h(t,n)}),getMethodsForTag=(t,e)=>{switch(t){case TAG_NAMES.TITLE:return{toComponent:()=>generateTitleAsPreactComponent(0,e.title,e.titleAttributes),toString:()=>generateTitleAsString(t,e.title,e.titleAttributes)};case TAG_NAMES.HTML:return{toComponent:()=>e,toString:()=>generateHtmlAttributesAsString(e)};default:return{toComponent:()=>generateTagsAsPreactComponent(t,e),toString:()=>generateTagsAsString(t,e)}}},mapStateOnServer=({htmlAttributes:t,title:e,titleAttributes:r,baseTag:n,metaTags:s,linkTags:o,scriptTags:T,noscriptTags:a,styleTags:i})=>({htmlAttributes:getMethodsForTag(TAG_NAMES.HTML,t),title:getMethodsForTag(TAG_NAMES.TITLE,{title:e,titleAttributes:r}),base:getMethodsForTag(TAG_NAMES.BASE,n),meta:getMethodsForTag(TAG_NAMES.META,s),link:getMethodsForTag(TAG_NAMES.LINK,o),script:getMethodsForTag(TAG_NAMES.SCRIPT,T),noscript:getMethodsForTag(TAG_NAMES.NOSCRIPT,a),style:getMethodsForTag(TAG_NAMES.STYLE,i)}),Helmet=t=>{class e extends Component{static set canUseDOM(e){t.canUseDOM=e}shouldComponentUpdate(t){const e={...t};return e.children&&e.children.length||delete e.children,!deepEqual(this.props,e)}render(){return h(t,{...this.props})}}return e.peek=((...e)=>t.peek(...e)),e.rewind=(()=>{let e=t.rewind();return e||(e=mapStateOnServer({htmlAttributes:{},title:"",titleAttributes:{},baseTag:[],metaTags:[],linkTags:[],scriptTags:[],noscriptTags:[],styleTags:[]})),e}),e},reducePropsToState=t=>({htmlAttributes:getAttributesFromPropsList(TAG_NAMES.HTML,t),title:getTitleFromPropsList(t),titleAttributes:getAttributesFromPropsList("titleAttributes",t),baseTag:getBaseTagFromPropsList([TAG_PROPERTIES.HREF],t),metaTags:getTagsFromPropsList(TAG_NAMES.META,[TAG_PROPERTIES.NAME,TAG_PROPERTIES.CHARSET,TAG_PROPERTIES.HTTPEQUIV,TAG_PROPERTIES.PROPERTY,TAG_PROPERTIES.ITEM_PROP],t),linkTags:getTagsFromPropsList(TAG_NAMES.LINK,[TAG_PROPERTIES.REL,TAG_PROPERTIES.HREF],t),scriptTags:getTagsFromPropsList(TAG_NAMES.SCRIPT,[TAG_PROPERTIES.SRC,TAG_PROPERTIES.INNER_HTML],t),noscriptTags:getTagsFromPropsList(TAG_NAMES.NOSCRIPT,[TAG_PROPERTIES.INNER_HTML],t),styleTags:getTagsFromPropsList(TAG_NAMES.STYLE,[TAG_PROPERTIES.CSS_TEXT],t),onChangeClientState:getOnChangeClientState(t)}),handleClientStateChange=t=>{const{htmlAttributes:e,title:r,titleAttributes:n,baseTag:s,metaTags:o,linkTags:T,scriptTags:a,noscriptTags:i,styleTags:l,onChangeClientState:c}=t;updateAttributes("html",e),updateTitle(r,n);const g={baseTag:updateTags(TAG_NAMES.BASE,s),metaTags:updateTags(TAG_NAMES.META,o),linkTags:updateTags(TAG_NAMES.LINK,T),scriptTags:updateTags(TAG_NAMES.SCRIPT,a),noscriptTags:updateTags(TAG_NAMES.NOSCRIPT,i),styleTags:updateTags(TAG_NAMES.STYLE,l)},E={},p={};Object.keys(g).forEach(t=>{const{newTags:e,oldTags:r}=g[t];e.length&&(E[t]=e),r.length&&(p[t]=g[t].oldTags)}),c(t,E,p)},NullComponent=()=>null,HelmetSideEffects=withSideEffect(t=>({htmlAttributes:getAttributesFromPropsList(TAG_NAMES.HTML,t),title:getTitleFromPropsList(t),titleAttributes:getAttributesFromPropsList("titleAttributes",t),baseTag:getBaseTagFromPropsList([TAG_PROPERTIES.HREF],t),metaTags:getTagsFromPropsList(TAG_NAMES.META,[TAG_PROPERTIES.NAME,TAG_PROPERTIES.CHARSET,TAG_PROPERTIES.HTTPEQUIV,TAG_PROPERTIES.PROPERTY,TAG_PROPERTIES.ITEM_PROP],t),linkTags:getTagsFromPropsList(TAG_NAMES.LINK,[TAG_PROPERTIES.REL,TAG_PROPERTIES.HREF],t),scriptTags:getTagsFromPropsList(TAG_NAMES.SCRIPT,[TAG_PROPERTIES.SRC,TAG_PROPERTIES.INNER_HTML],t),noscriptTags:getTagsFromPropsList(TAG_NAMES.NOSCRIPT,[TAG_PROPERTIES.INNER_HTML],t),styleTags:getTagsFromPropsList(TAG_NAMES.STYLE,[TAG_PROPERTIES.CSS_TEXT],t),onChangeClientState:getOnChangeClientState(t)}),t=>{const{htmlAttributes:e,title:r,titleAttributes:n,baseTag:s,metaTags:o,linkTags:T,scriptTags:a,noscriptTags:i,styleTags:l,onChangeClientState:c}=t;updateAttributes("html",e),updateTitle(r,n);const g={baseTag:updateTags(TAG_NAMES.BASE,s),metaTags:updateTags(TAG_NAMES.META,o),linkTags:updateTags(TAG_NAMES.LINK,T),scriptTags:updateTags(TAG_NAMES.SCRIPT,a),noscriptTags:updateTags(TAG_NAMES.NOSCRIPT,i),styleTags:updateTags(TAG_NAMES.STYLE,l)},E={},p={};Object.keys(g).forEach(t=>{const{newTags:e,oldTags:r}=g[t];e.length&&(E[t]=e),r.length&&(p[t]=g[t].oldTags)}),c(t,E,p)},mapStateOnServer)(()=>null);let HelmetW=(t=>{class e extends Component{static set canUseDOM(e){t.canUseDOM=e}shouldComponentUpdate(t){const e={...t};return e.children&&e.children.length||delete e.children,!deepEqual(this.props,e)}render(){return h(t,{...this.props})}}return e.peek=((...e)=>t.peek(...e)),e.rewind=(()=>{let e=t.rewind();return e||(e=mapStateOnServer({htmlAttributes:{},title:"",titleAttributes:{},baseTag:[],metaTags:[],linkTags:[],scriptTags:[],noscriptTags:[],styleTags:[]})),e}),e})(HelmetSideEffects);export{HelmetW as Helmet};export default HelmetW; 2 | -------------------------------------------------------------------------------- /assets/js/vendors/recompose.js: -------------------------------------------------------------------------------- 1 | import{default as React,Component}from "./react.js";let createFactory=React.createFactory;const hasOwnProperty=Object.prototype.hasOwnProperty;export function is(e, t){return e===t?0!==e||0!==t||1/e==1/t:e!==e&&t!==t};export function shallowEqual(e, t){if(is(e,t))return!0;if("object"!=typeof e||null===e||"object"!=typeof t||null===t)return!1;const r=Object.keys(e),o=Object.keys(t);if(r.length!==o.length)return!1;for(let o=0; o r=>(r[e]=t,r);export const setDisplayName=e=>setStatic("displayName",e);export const getDisplayName=e=>{if("string"==typeof e)return e;if(e)return e.displayName||e.name||"Component"};const wrapDisplayName=(e,t)=>`${t}(${getDisplayName(e)})`;export const shouldUpdate=e=>t=>{const r=createFactory(t);class o extends Component{shouldComponentUpdate(t){return e(this.props,t)}render(){return r(this.props)}}return setDisplayName(wrapDisplayName(t,"shouldUpdate"))(o)};export const pure=e=>{const t=shouldUpdate((e,t)=>!shallowEqual(e,t));return setDisplayName(wrapDisplayName(e,"pure"))(t(e))};export function compose(...e){return 0===e.length?e=>e:1===e.length?e[0]:e.reduce((e,t)=>(...r)=>e(t(...r)))}; 2 | -------------------------------------------------------------------------------- /assets/js/vendors/redux-thunk.js: -------------------------------------------------------------------------------- 1 | function createThunkMiddleware(t){return({dispatch:e,getState:n})=>r=>u=>"function"==typeof u?u(e,n,t):r(u)}const thunk=createThunkMiddleware();thunk.withExtraArgument=createThunkMiddleware;export default thunk; -------------------------------------------------------------------------------- /assets/js/vendors/redux.js: -------------------------------------------------------------------------------- 1 | function objectToString(e){return nativeObjectToString.call(e)}function getRawTag(e){var t=hasOwnProperty.call(e,symToStringTag),n=e[symToStringTag];try{e[symToStringTag]=void 0;var o=!0}catch(e){}var r=nativeObjectToString.call(e);return o&&(t?e[symToStringTag]=n:delete e[symToStringTag]),r}function baseGetTag(e){return null==e?void 0===e?undefinedTag:nullTag:symToStringTag&&symToStringTag in Object(e)?getRawTag(e):objectToString(e)}function overArg(e,t){return function(n){return e(t(n))}}function isObjectLike(e){return null!=e&&"object"==typeof e}function isPlainObject(e){if(!isObjectLike(e)||baseGetTag(e)!=objectTag)return!1;var t=getPrototype(e);if(null===t)return!0;var n=hasOwnProperty.call(t,"constructor")&&t.constructor;return"function"==typeof n&&n instanceof n&&funcToString.call(n)==objectCtorString}function symbolObservablePonyfill(e){var t,n=e.Symbol;return"function"==typeof n?n.observable?t=n.observable:(t=n("observable"),n.observable=t):t="@@observable",t}function warning(e){"undefined"!=typeof console&&"function"==typeof console.error&&console.error(e);try{throw new Error(e)}catch(e){}}function applyMiddleware(...e){return t=>(...n)=>{const o=t(...n);let r=o.dispatch,i=[];const c={getState:o.getState,dispatch:(...e)=>r(...e)};return i=e.map(e=>e(c)),r=compose(...i)(o.dispatch),{...o,dispatch:r}}}function bindActionCreator(e,t){return function(){return t(e.apply(this,arguments))}}function bindActionCreators(e,t){if("function"==typeof e)return bindActionCreator(e,t);if("object"!=typeof e||null===e)throw new Error(`bindActionCreators expected an object or a function, instead received ${null===e?"null":typeof e}. `+`Did you write "import ActionCreators from" instead of "import * as ActionCreators from"?`);const n=Object.keys(e),o={};for(let r=0;re:1===e.length?e[0]:e.reduce((e,t)=>(...n)=>e(t(...n)))}function createStore(e,t,n){function o(){f===u&&(f=u.slice())}function r(){return s}function i(e){if("function"!=typeof e)throw new Error("Expected listener to be a function.");let t=!0;return o(),f.push(e),function(){if(!t)return;t=!1,o();const n=f.indexOf(e);f.splice(n,1)}}function c(e){if(!isPlainObject(e))throw new Error("Actions must be plain objects. Use custom middleware for async actions.");if(void 0===e.type)throw new Error('Actions may not have an undefined "type" property. Have you misspelled a constant?');if(l)throw new Error("Reducers may not dispatch actions.");try{l=!0,s=a(s,e)}finally{l=!1}const t=u=f;for(let e=0;e!t.hasOwnProperty(e)&&!o[e]);return c.forEach(e=>{o[e]=!0}),c.length>0?`Unexpected ${c.length>1?"keys":"key"} `+`"${c.join('", "')}" found in ${i}. `+`Expected to find one of the known reducer keys instead: `+`"${r.join('", "')}". Unexpected keys will be ignored.`:void 0}function assertReducerShape(e){Object.keys(e).forEach(t=>{const n=e[t];if(void 0===n(void 0,{type:ActionTypes.INIT}))throw new Error(`Reducer "${t}" returned undefined during initialization. `+`If the state passed to the reducer is undefined, you must `+`explicitly return the initial state. The initial state may `+`not be undefined. If you don't want to set a value for this reducer, `+`you can use null instead of undefined.`);if(void 0===n(void 0,{type:"@@redux/PROBE_UNKNOWN_ACTION_"+Math.random().toString(36).substring(7).split("").join(".")}))throw new Error(`Reducer "${t}" returned undefined when probed with a random type. `+`Don't try to handle ${ActionTypes.INIT} or other actions in "redux/*" `+`namespace. They are considered private. Instead, you must return the `+`current state for any unknown actions, unless it is undefined, `+`in which case you must return the initial state, regardless of the `+`action type. The initial state may not be undefined, but can be null.`)})}function combineReducers(e){const t=Object.keys(e),n={};for(let o=0;o { 18 | dependencies[file.slice(0, -3)] = `./assets/js/vendors/${file}` 19 | }) 20 | let conf = JSON.parse( 21 | fs.readFileSync(path.join(process.cwd(), '.babelrc'), 'utf-8') 22 | ) 23 | let moduleResolverIndex = conf.plugins.findIndex( 24 | plugin => 25 | typeof plugin[0] !== 'undefined' && plugin[0] === 'module-resolver' 26 | ) 27 | conf.babelrc = false 28 | conf.exclude = 'node_modules/**' 29 | conf.plugins[moduleResolverIndex][1]['alias'] = { 30 | ...conf.plugins[moduleResolverIndex][1]['alias'], 31 | ...dependencies, 32 | conf: './assets/build/conf.js' 33 | } 34 | 35 | let index_prod = fs.readFileSync( 36 | path.join(process.cwd(), 'assets/html/index_prod.html'), 37 | 'utf-8' 38 | ) 39 | if (projectRoot) { 40 | index_prod = index_prod 41 | .replace(/\/assets\//g, `${projectRoot}assets/`) 42 | .replace('/src/', `${projectRoot}src/`) 43 | } 44 | fs.copyFileSync( 45 | path.join( 46 | process.cwd(), 47 | `assets/html/404_${projectRoot ? 'with_root' : 'no_root'}.html` 48 | ), 49 | path.join(process.cwd(), '404.html') 50 | ) 51 | 52 | rollup 53 | .rollup({ 54 | input: path.join(process.cwd(), 'src/app.js'), 55 | plugins: [ 56 | replace({ 57 | 'process.env.NODE_ENV': `"production"` 58 | }), 59 | resolve({ 60 | module: true, 61 | jsnext: true, 62 | extensions: ['.js'], 63 | browser: true 64 | }), 65 | babel(conf) 66 | ] 67 | }) 68 | .then(bundle => { 69 | bundle 70 | .generate({ 71 | format: 'iife', 72 | moduleName: 'ReactDriveCMS' 73 | }) 74 | .then(result => { 75 | let { code } = result.output[0] 76 | let hash = hasha(code, { algorithm: 'sha1' }).slice(0, 16) 77 | fs.writeFile( 78 | path.join(process.cwd(), 'assets/build/index_prod.html'), 79 | index_prod.replace('', hash), 80 | (err, res) => { 81 | if (err) { 82 | console.log(err) 83 | } else { 84 | fs.copyFile( 85 | path.join( 86 | process.cwd(), 87 | 'assets/build/index_prod.html' 88 | ), 89 | path.join(process.cwd(), 'index.html'), 90 | (err, res) => { 91 | if (err) { 92 | console.log(err) 93 | } 94 | } 95 | ) 96 | } 97 | } 98 | ) 99 | 100 | code = Terser.minify(code).code 101 | return fs.writeFile( 102 | path.join(process.cwd(), `assets/build/app.min.${hash}.js`), 103 | code, 104 | (err, res) => { 105 | if (err) { 106 | console.log(err) 107 | } else { 108 | console.log('Build complete.') 109 | } 110 | } 111 | ) 112 | }) 113 | }) 114 | .catch(error => console.log(error)) 115 | -------------------------------------------------------------------------------- /dev/dev-server.js: -------------------------------------------------------------------------------- 1 | let express = require('express') 2 | let path = require('path') 3 | let fs = require('fs') 4 | let projectConf = require('./../conf') 5 | let projectRoot = projectConf.root ? `/${projectConf.root}/` : false 6 | let cachedConf = 'export default ' + JSON.stringify(projectConf) 7 | 8 | process.on('unhandledRejection', (reason, promise) => { 9 | if (reason.stack) { 10 | console.log(reason.stack) 11 | } else { 12 | console.log({ err: reason, promise: promise }) 13 | } 14 | }) 15 | 16 | const watcher = require('chokidar').watch([path.join(process.cwd(), 'src')]) 17 | watcher.on('ready', function() { 18 | watcher.on('all', function() { 19 | Object.keys(require.cache).forEach(function(id) { 20 | if (/[\/\\]src[\/\\]/.test(id)) { 21 | delete require.cache[id] 22 | } 23 | }) 24 | }) 25 | }) 26 | 27 | let index_dev = fs.readFileSync( 28 | path.join(process.cwd(), 'assets/html/index_dev.html'), 29 | 'utf-8' 30 | ) 31 | if (projectRoot) { 32 | index_dev = index_dev 33 | .replace(/\/assets\//g, `${projectRoot}assets/`) 34 | .replace('/src/', `${projectRoot}src/`) 35 | } 36 | fs.writeFileSync( 37 | path.join(process.cwd(), 'assets/build/index_dev.html'), 38 | index_dev 39 | ) 40 | 41 | fs.copyFileSync( 42 | path.join( 43 | process.cwd(), 44 | `assets/build/index_${ 45 | process.env.NODE_ENV === 'production' ? 'prod' : 'dev' 46 | }.html` 47 | ), 48 | path.join(process.cwd(), 'index.html') 49 | ) 50 | 51 | fs.copyFileSync( 52 | path.join( 53 | process.cwd(), 54 | `assets/html/404_${projectRoot ? 'with_root' : 'no_root'}.html` 55 | ), 56 | path.join(process.cwd(), '404.html') 57 | ) 58 | 59 | const app = express() 60 | app.use(require('morgan')('dev')) 61 | app.use(function(req, res, next) { 62 | if (projectRoot && req.url.startsWith(projectRoot)) { 63 | req.url = req.url.slice(projectRoot.length - 1) 64 | } 65 | return next() 66 | }) 67 | app.use(require('./middleware/update-middleware')) 68 | app.use(require('./middleware/transform-middleware')) 69 | app.get('/stream', function(req, res) { 70 | res.sseSetup() 71 | }) 72 | app.get('/conf.js', function(req, res) { 73 | res.setHeader('Content-type', 'application/javascript') 74 | res.status(200).send(cachedConf) 75 | }) 76 | 77 | app.use(express.static(process.cwd())) 78 | app.use(function(req, res, next) { 79 | res.status(404).sendFile(path.join(process.cwd(), '404.html')) 80 | }) 81 | app.listen(8000, () => 82 | console.log( 83 | `React drive cms listening on url: http://localhost:8000${projectRoot}` 84 | ) 85 | ) 86 | -------------------------------------------------------------------------------- /dev/middleware/cache.js: -------------------------------------------------------------------------------- 1 | let cache = { 2 | cacheMap: {}, 3 | hashMap: {}, 4 | stale: {}, 5 | links: {} 6 | } 7 | 8 | function set(store, key, value) { 9 | cache[store][key] = value 10 | } 11 | 12 | function remove(store, key) { 13 | delete cache[store][key] 14 | } 15 | 16 | function get(store, key) { 17 | if (typeof cache[store][key] === 'undefined') { 18 | return false 19 | } 20 | return cache[store][key] 21 | } 22 | 23 | function getAll(store) { 24 | return cache[store] 25 | } 26 | 27 | function dump() { 28 | console.log('hash map', JSON.stringify(cache.hashMap)) 29 | console.log('cache map', JSON.stringify(Object.keys(cache.cacheMap))) 30 | } 31 | 32 | module.exports = { 33 | set, 34 | remove, 35 | get, 36 | getAll, 37 | dump 38 | } 39 | -------------------------------------------------------------------------------- /dev/middleware/resolveToUrl.js: -------------------------------------------------------------------------------- 1 | let fs = require('fs') 2 | let path = require('path') 3 | let dependencies = fs 4 | .readdirSync(path.join(process.cwd(), 'assets/js/vendors')) 5 | .map(file => file.slice(0, -3)) 6 | let plugins = JSON.parse( 7 | fs.readFileSync(path.join(process.cwd(), '.babelrc'), 'utf-8') 8 | ).plugins 9 | let moduleResolver = plugins.find( 10 | plugin => 11 | typeof plugin[0] !== 'undefined' && plugin[0] === 'module-resolver' 12 | ) 13 | let aliases = moduleResolver[1]['alias'] 14 | 15 | function resolver({ node: { source } }) { 16 | if (source !== null) { 17 | if (dependencies.includes(source.value)) { 18 | source.value = '/assets/js/vendors/' + source.value 19 | } else { 20 | if ( 21 | !source.value.startsWith('/') && 22 | !source.value.startsWith('./') 23 | ) { 24 | let alias = source.value.split('/')[0] 25 | if (typeof aliases[alias] !== 'undefined') { 26 | source.value = 27 | aliases[alias]['slice'](1) + 28 | source.value.slice(alias.length) 29 | } else { 30 | source.value = '/src/' + source.value 31 | } 32 | } 33 | } 34 | if (!source.value.endsWith('.js')) { 35 | source.value += '.js' 36 | } 37 | } 38 | } 39 | 40 | function resolveToUrl() { 41 | return { 42 | visitor: { 43 | ImportDeclaration: resolver 44 | } 45 | } 46 | } 47 | 48 | module.exports = resolveToUrl 49 | -------------------------------------------------------------------------------- /dev/middleware/send.js: -------------------------------------------------------------------------------- 1 | function send( 2 | res, 3 | transpiled, 4 | fromCache = false, 5 | contentType = 'application/javascript' 6 | ) { 7 | res.setHeader('Content-type', contentType) 8 | res.setHeader('Content-Encoding', 'gzip') 9 | res.setHeader('Accept-Ranges', 'bytes') 10 | res.setHeader( 11 | 'Cache-Control', 12 | 'private, no-cache, no-store, must-revalidate' 13 | ) 14 | res.setHeader('Expires', '-1') 15 | res.setHeader('Pragma', 'no-cache') 16 | res.setHeader('Connection', 'keep-alive') 17 | res.append('X-App-Cache-Hit', fromCache) 18 | res.status(200).send(transpiled) 19 | } 20 | 21 | module.exports = send 22 | -------------------------------------------------------------------------------- /dev/middleware/transform-file.js: -------------------------------------------------------------------------------- 1 | let path = require('path') 2 | let fs = require('fs') 3 | let crypto = require('crypto') 4 | let cache = require('./cache') 5 | let update = require('./update') 6 | let send = require('./send') 7 | 8 | function transformFile(req, res, next) { 9 | let uri = req.url.split('?').shift() 10 | let file = path.join(process.cwd(), uri) 11 | let src = file.replace(/\\/gi, '/') 12 | 13 | fs.lstat(file, function(err, stats) { 14 | if (err) { 15 | res.status(500).json(err) 16 | } else { 17 | let mtime = stats.mtime.getTime() 18 | let lastModifiedHash = crypto 19 | .createHash('md5') 20 | .update(mtime + '-' + src) 21 | .digest('hex') 22 | 23 | let lastKnownHash = cache.get('hashMap', src) 24 | if (lastKnownHash && lastKnownHash === lastModifiedHash) { 25 | send(res, cache.get('cacheMap', lastKnownHash), true) 26 | } else { 27 | update(file, lastModifiedHash, function(err, updated) { 28 | if (err) { 29 | res.status(500).json(err) 30 | } else { 31 | send(res, updated.file) 32 | } 33 | }) 34 | } 35 | } 36 | }) 37 | } 38 | 39 | module.exports = transformFile 40 | -------------------------------------------------------------------------------- /dev/middleware/transform-middleware.js: -------------------------------------------------------------------------------- 1 | let transformFile = require('./transform-file') 2 | 3 | function transformMiddleware(req, res, next) { 4 | if (req.url.startsWith('/src/')) { 5 | return transformFile(req, res, next) 6 | } else { 7 | return next() 8 | } 9 | } 10 | 11 | module.exports = transformMiddleware 12 | -------------------------------------------------------------------------------- /dev/middleware/update-middleware.js: -------------------------------------------------------------------------------- 1 | let chokidar = require('chokidar') 2 | let path = require('path') 3 | let fs = require('fs') 4 | let crypto = require('crypto') 5 | let serverStart = Date.now() 6 | let update = require('./update') 7 | let debounce = require('lodash.debounce') 8 | let cache = require('./cache') 9 | 10 | let refresh = debounce(function(res) { 11 | Object.keys(cache.getAll('stale')).forEach(file => { 12 | let src = file.replace(/\\/gi, '/') 13 | let fileUri = file.split(process.cwd())[1].replace(/\\/gi, '/') 14 | fs.lstat(file, function(err, stats) { 15 | if (err) { 16 | res.status(500).json(err) 17 | } else { 18 | let mtime = stats.mtime.getTime() 19 | let lastModifiedHash = crypto 20 | .createHash('md5') 21 | .update(mtime + '-' + src) 22 | .digest('hex') 23 | 24 | let lastKnownHash = cache.get('hashMap', src) 25 | if (!lastKnownHash && lastKnownHash === lastModifiedHash) { 26 | res.write('data: ' + fileUri + '\n\n') 27 | cache.remove('stale', file) 28 | } else { 29 | update(file, lastModifiedHash, function(err, updated) { 30 | if (err) { 31 | res.status(500).json(err) 32 | } else { 33 | res.write('data: ' + fileUri + '\n\n') 34 | cache.remove('stale', file) 35 | } 36 | }) 37 | } 38 | } 39 | }) 40 | }) 41 | }) 42 | 43 | function updater(req, res, next) { 44 | if (typeof res.sseSetup !== 'undefined') { 45 | return next() 46 | } 47 | 48 | res.sseSetup = function() { 49 | res.writeHead(200, { 50 | 'Content-Type': 'text/event-stream', 51 | 'Cache-Control': 'no-cache', 52 | Connection: 'keep-alive' 53 | }) 54 | serverStart = Date.now() 55 | chokidar 56 | .watch(path.join(process.cwd(), 'src'), { 57 | ignored: /(^|[\/\\])\../ 58 | }) 59 | .on('all', (event, filePath) => { 60 | if ( 61 | Date.now() - serverStart > 10000 && 62 | !( 63 | filePath.endsWith('___jb_tmp___') || 64 | filePath.endsWith('___jb_old___') 65 | ) 66 | ) { 67 | if (event === 'change' || event === 'add') { 68 | console.log(event, filePath) 69 | cache.set('stale', filePath) 70 | refresh(res) 71 | } else if (event === 'unlink') { 72 | console.log('unlink', filePath) 73 | cache.remove('stale', filePath) 74 | let link = 75 | '/src/' + 76 | filePath.replace(/\\/gi, '/').split('/src/')[1] 77 | cache.remove('links', link) 78 | } 79 | } 80 | }) 81 | } 82 | 83 | return next() 84 | } 85 | 86 | module.exports = updater 87 | -------------------------------------------------------------------------------- /dev/middleware/update.js: -------------------------------------------------------------------------------- 1 | let path = require('path') 2 | let zlib = require('zlib') 3 | let babel = require('@babel/core') 4 | 5 | let cache = require('./cache') 6 | let resolveToUrl = require('./resolveToUrl') 7 | let conf = JSON.parse( 8 | require('fs').readFileSync(path.join(process.cwd(), '.babelrc'), 'utf-8') 9 | ) 10 | conf.babelrc = false 11 | conf.plugins.pop() 12 | conf.plugins.push([resolveToUrl]) 13 | 14 | function update(file, hash, cb) { 15 | babel.transformFile(file, conf, function(err, transpiled) { 16 | if (err) { 17 | cb(err) 18 | } else { 19 | zlib.gzip(transpiled.code, function(err, gzipped) { 20 | if (err) { 21 | cb(err) 22 | } else { 23 | let path = file.replace(/\\/gi, '/') 24 | 25 | cache.remove('cacheMap', cache.get('hashMap', path)) 26 | cache.set('hashMap', path, hash) 27 | cache.set('cacheMap', hash, gzipped) 28 | 29 | let link = '/src/' + path.split('/src/')[1] 30 | let cached = { 31 | file: gzipped, 32 | type: 'application/javascript' 33 | } 34 | cache.set('links', link, cached) 35 | 36 | cb(null, cached) 37 | } 38 | }) 39 | } 40 | }) 41 | } 42 | 43 | module.exports = update 44 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 8 | React Drive CMS 9 | 10 | 11 | 12 | 34 | 35 | 36 | 81 | 82 | 83 |
84 | 85 | 86 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-without-webpack", 3 | "version": "2.0.0", 4 | "description": "React Without Webpack : The lightest React Starter Kit", 5 | "scripts": { 6 | "dev": "cross-env NODE_ENV=development node ./dev/dev-server.js", 7 | "local": "cross-env NODE_ENV=production node ./dev/dev-server.js", 8 | "build": "cross-env NODE_ENV=production node ./dev/build/build-app.js", 9 | "format": "./node_modules/.bin/prettier --write --no-semi --tab-width 4 --single-quote \"{,!(node_modules|assets)/**/}*.js\"" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "https://github.com/misterfresh/react-without-webpack" 14 | }, 15 | "keywords": [ 16 | "react", 17 | "import", 18 | "es-modules", 19 | "rollup" 20 | ], 21 | "author": "misterfresh", 22 | "license": "MIT", 23 | "bugs": { 24 | "url": "https://github.com/misterfresh/react-without-webpack" 25 | }, 26 | "homepage": "https://github.com/misterfresh/react-without-webpack#readme", 27 | "dependencies": { 28 | "chokidar": "^2.0.4", 29 | "express": "^4.16.4", 30 | "lodash.debounce": "^4.0.8", 31 | "morgan": "^1.9.1" 32 | }, 33 | "devDependencies": { 34 | "@babel/cli": "^7.2.3", 35 | "@babel/core": "^7.2.2", 36 | "@babel/plugin-proposal-class-properties": "^7.2.3", 37 | "@babel/plugin-syntax-object-rest-spread": "^7.2.0", 38 | "@babel/preset-react": "^7.0.0", 39 | "babel-plugin-module-resolver": "^3.1.1", 40 | "cross-env": "^5.2.0", 41 | "hasha": "^3.0.0", 42 | "prettier": "^1.15.3", 43 | "rollup": "^1.0.2", 44 | "rollup-plugin-babel": "^4.2.0", 45 | "rollup-plugin-node-resolve": "^4.0.0", 46 | "rollup-plugin-replace": "^2.1.0", 47 | "terser": "^3.14.1" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/app.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { render } from 'react' 3 | 4 | import { createBrowserHistory } from 'history' 5 | import { StyleSheet } from 'aphrodite' 6 | 7 | import routerMiddleware from 'route/middleware' 8 | 9 | import Root from 'main/containers/root' 10 | import configureStore from 'main/store/configureStore' 11 | 12 | let initialState = {} 13 | import conf from 'conf' 14 | let history = createBrowserHistory(conf.root ? { basename: conf.root } : {}) 15 | 16 | const store = configureStore(initialState, routerMiddleware(history)) 17 | 18 | render( 19 | , 20 | document.getElementById('app-mount'), 21 | document.getElementById('app-mount').firstElementChild 22 | ) 23 | -------------------------------------------------------------------------------- /src/components/blocks/article.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { StyleSheet, css } from 'aphrodite' 3 | import { pure } from 'recompose' 4 | import { Link } from 'react-router-dom' 5 | 6 | let Article = ({ article, category }) => ( 7 |
8 |

9 | 14 | {article.title} 15 | 16 |

17 |

{article.subtitle}

18 |

19 | 29 |  -  Published in :   30 | 35 | {category.title} 36 | 37 |

38 |
39 | ) 40 | 41 | export default pure(Article) 42 | 43 | let styles = StyleSheet.create({ 44 | article: { 45 | padding: '30px 0', 46 | display: 'block', 47 | borderBottom: 'solid 1px #f5f5f5' 48 | }, 49 | title: { 50 | textDecoration: 'none', 51 | color: '#333337', 52 | fontSize: '2.4rem', 53 | marginTop: 0, 54 | fontFamily: '"Source Sans Pro",Helvetica,Arial,sans-serif', 55 | fontWeight: 700, 56 | marginBottom: '10px', 57 | lineHeight: '1.1', 58 | cursor: 'pointer', 59 | backgroundColor: 'transparent', 60 | border: 'none', 61 | '@media (min-width: 992px)': { fontSize: '3.2rem' }, 62 | ':hover': { color: '#b6b6b6' } 63 | }, 64 | p: { 65 | margin: '0 0 10px', 66 | fontFamily: '"Droid Serif",serif', 67 | fontSize: '1.6rem', 68 | '@media (min-width: 992px)': { 69 | fontSize: '1.8rem' 70 | } 71 | }, 72 | description: { 73 | marginBottom: '30px' 74 | }, 75 | meta: { 76 | color: '#b6b6b6' 77 | }, 78 | comments: {}, 79 | category: { 80 | textDecoration: 'none', 81 | cursor: 'pointer', 82 | backgroundColor: 'transparent', 83 | outline: 0, 84 | transition: 'all .4s', 85 | color: '#b6b6b6', 86 | borderBottom: '1px solid #b6b6b6', 87 | ':hover': { 88 | textDecoration: 'none', 89 | cursor: 'pointer', 90 | backgroundColor: 'transparent', 91 | color: '#333337', 92 | outline: 0, 93 | transition: 'all .4s', 94 | borderBottom: '1px solid #b6b6b6' 95 | } 96 | } 97 | }) 98 | -------------------------------------------------------------------------------- /src/components/blocks/category.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { StyleSheet, css } from 'aphrodite' 3 | import { pure } from 'recompose' 4 | import { Link } from 'react-router-dom' 5 | 6 | let Category = ({ category }) => ( 7 |
13 |

14 | 19 | {category.title} 20 | 21 |

22 |
23 | ) 24 | 25 | export default pure(Category) 26 | 27 | let styles = StyleSheet.create({ 28 | category: { 29 | height: '30rem', 30 | backgroundPosition: 'center center', 31 | backgroundSize: 'cover', 32 | width: '100%', 33 | '@media (min-width: 768px)': { 34 | width: '100%' 35 | }, 36 | '@media (min-width: 992px)': { 37 | width: '48%' 38 | }, 39 | position: 'relative', 40 | marginBottom: '2rem', 41 | display: 'flex', 42 | alignItems: 'flex-end' 43 | }, 44 | link: { 45 | color: '#fff', 46 | cursor: 'pointer', 47 | textDecoration: 'none', 48 | backgroundColor: 'transparent', 49 | fontSize: '2rem', 50 | borderBottom: 'solid 1px #fAfafa', 51 | ':hover': { 52 | cursor: 'pointer', 53 | textDecoration: 'none', 54 | backgroundColor: 'transparent', 55 | outline: 0, 56 | color: '#999', 57 | borderBottom: 'solid 1px #999', 58 | transition: 'all .4s' 59 | } 60 | }, 61 | title: { 62 | background: 'rgba(50,50,50,.5)', 63 | width: '100%', 64 | fontSize: '2rem', 65 | color: '#fff', 66 | padding: '10px', 67 | fontWeight: 700, 68 | lineHeight: '1.1', 69 | bottom: 0, 70 | fontFamily: '"Source Sans Pro",Helvetica,Arial,sans-serif', 71 | marginBottom: 0 72 | } 73 | }) 74 | -------------------------------------------------------------------------------- /src/components/disqus/disqusCount.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | 3 | import conf from 'conf' 4 | 5 | export default class DisqusCount extends Component { 6 | constructor(props) { 7 | super(props) 8 | this.addDisqusScript = this.addDisqusScript.bind(this) 9 | this.removeDisqusScript = this.removeDisqusScript.bind(this) 10 | } 11 | 12 | addDisqusScript() { 13 | this.disqusCount = document.createElement('script') 14 | let child = this.disqusCount 15 | let parent = 16 | document.getElementsByTagName('head')[0] || 17 | document.getElementsByTagName('body')[0] 18 | child.async = true 19 | child.type = 'text/javascript' 20 | child.src = `https://${conf.shortname}.disqus.com/count.js` 21 | parent.appendChild(child) 22 | } 23 | 24 | removeDisqusScript() { 25 | if (this.disqusCount && this.disqusCount.parentNode) { 26 | this.disqusCount.parentNode.removeChild(this.disqusCount) 27 | this.disqusCount = null 28 | } 29 | } 30 | 31 | componentDidMount() { 32 | window.disqus_shortname = conf.shortname 33 | if (typeof window.DISQUSWIDGETS !== 'undefined') { 34 | window.DISQUSWIDGETS = undefined 35 | } 36 | this.addDisqusScript() 37 | } 38 | 39 | componentWillUnmount() { 40 | this.removeDisqusScript() 41 | } 42 | 43 | render() { 44 | return 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/components/disqus/disqusThread.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | 3 | import conf from 'conf' 4 | 5 | export default class DisqusThread extends Component { 6 | constructor() { 7 | super() 8 | this.addDisqusScript = this.addDisqusScript.bind(this) 9 | this.removeDisqusScript = this.removeDisqusScript.bind(this) 10 | } 11 | 12 | addDisqusScript() { 13 | let child = (this.disqus = document.createElement('script')) 14 | let parent = 15 | document.getElementsByTagName('head')[0] || 16 | document.getElementsByTagName('body')[0] 17 | child.async = true 18 | child.type = 'text/javascript' 19 | child.src = '//' + conf.shortname + '.disqus.com/embed.js' 20 | parent.appendChild(child) 21 | } 22 | 23 | removeDisqusScript() { 24 | if (this.disqus && this.disqus.parentNode) { 25 | this.disqus.parentNode.removeChild(this.disqus) 26 | this.disqus = null 27 | } 28 | } 29 | 30 | componentDidMount() { 31 | let { id, title } = this.props 32 | window.disqus_shortname = conf.shortname 33 | window.disqus_identifier = id 34 | window.disqus_title = title 35 | window.disqus_url = window.location.href 36 | 37 | if (typeof window.DISQUS !== 'undefined') { 38 | window.DISQUS.reset({ reload: true }) 39 | } else { 40 | this.addDisqusScript() 41 | } 42 | } 43 | 44 | componentWillUnmount() { 45 | this.removeDisqusScript() 46 | } 47 | 48 | render() { 49 | return
50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/components/form/baseInput.js: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from 'react' 2 | import debounce from 'lodash.debounce' 3 | 4 | import uuid from 'utils/uuid' 5 | import capitalize from 'utils/capitalize' 6 | 7 | class BaseInput extends PureComponent { 8 | constructor(props) { 9 | super(props) 10 | this.handleChange = this.handleChange.bind(this) 11 | this.updateValue = debounce(this.updateValue.bind(this), 700) 12 | this.state = { 13 | value: props.value 14 | } 15 | this.id = uuid() 16 | } 17 | 18 | handleChange(event) { 19 | let property = event.target.dataset.property 20 | let value = 21 | this.props.type === 'number' 22 | ? parseInt(event.target.value) 23 | : event.target.value 24 | this.setState( 25 | { 26 | value 27 | }, 28 | () => this.updateValue(property, value) 29 | ) 30 | } 31 | 32 | componentWillReceiveProps(nextProps) { 33 | if (this.id !== document.activeElement.id) { 34 | this.setState({ 35 | value: nextProps.value 36 | }) 37 | } 38 | } 39 | 40 | updateValue(property, value) { 41 | this.setState( 42 | { 43 | value 44 | }, 45 | () => this.props.onInput(property, value, this.props.id) 46 | ) 47 | } 48 | 49 | render() { 50 | let { 51 | className = '', 52 | style = {}, 53 | name = '', 54 | type = 'text', 55 | placeholder = '', 56 | Component = 'input', 57 | property = '', 58 | min = 0, 59 | step = 1 60 | } = this.props 61 | let { value } = this.state 62 | name = !!name ? name : property 63 | return ( 64 | 77 | ) 78 | } 79 | } 80 | 81 | export default BaseInput 82 | -------------------------------------------------------------------------------- /src/components/layout/footer.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { pure } from 'recompose' 3 | import { StyleSheet, css } from 'aphrodite' 4 | import { Link } from 'react-router-dom' 5 | import conf from 'conf' 6 | 7 | let Footer = ({ article, category, articles, menuVisible }) => ( 8 |
9 |
10 | 11 | user-image 20 | 21 | 22 |
28 |

29 | Published on the  30 | 31 | {article.date} 32 | 33 |  by  34 | 39 | {conf.author} 40 | 41 |  in  42 | 47 | {category.title} 48 | 49 |

50 |
51 | 52 |
53 |

Share this article

54 |
55 | 61 | 62 | 63 | 64 | 70 | 71 | 72 | 73 | 78 | 79 | 80 |
81 |
82 |
83 |
84 | {Object.values(articles).map(article => ( 85 |
92 |
93 | 98 | {article.title} 99 | 100 |
101 | ))} 102 |
103 |
104 | ) 105 | 106 | export default pure(Footer) 107 | 108 | let styles = StyleSheet.create({ 109 | footer: { 110 | width: '100%', 111 | backgroundColor: '#F5F5F5', 112 | borderTop: 'solid 1px #E9E9E9', 113 | padding: '3rem' 114 | }, 115 | footerTop: { 116 | display: 'flex', 117 | flexDirection: 'column', 118 | justifyContent: 'space-evenly', 119 | '@media (min-width: 768px)': { 120 | flexDirection: 'row' 121 | }, 122 | '@media (min-width: 992px)': { 123 | flexDirection: 'row' 124 | }, 125 | marginBottom: '3rem', 126 | alignItems: 'center' 127 | }, 128 | topNarrow: { 129 | '@media (min-width: 768px)': { 130 | flexDirection: 'column' 131 | }, 132 | '@media (min-width: 992px)': { 133 | flexDirection: 'row' 134 | } 135 | }, 136 | profile: { 137 | width: '6rem', 138 | padding: 0, 139 | border: 0, 140 | borderRadius: '50%', 141 | height: '6rem', 142 | marginBottom: '1rem' 143 | }, 144 | credits: { 145 | width: '100%', 146 | marginBottom: '1rem', 147 | '@media (min-width: 768px)': { 148 | borderRight: 'solid 4px #E9E9E9', 149 | padding: 0, 150 | width: '50%' 151 | }, 152 | '@media (min-width: 992px)': { 153 | borderRight: 'solid 4px #E9E9E9', 154 | padding: 0, 155 | width: '50%' 156 | } 157 | }, 158 | creditsNarrow: { 159 | '@media (min-width: 768px)': { 160 | borderRight: 'none', 161 | borderBottom: 'solid 4px #E9E9E9', 162 | padding: 0, 163 | width: '100%' 164 | }, 165 | '@media (min-width: 992px)': { 166 | borderBottom: 'none', 167 | borderRight: 'solid 4px #E9E9E9', 168 | padding: 0, 169 | width: '50%' 170 | } 171 | }, 172 | p: { 173 | paddingRight: '2rem', 174 | letterSpacing: '2px', 175 | fontFamily: '"Source Sans Pro",Helvetica,Arial,sans-serif', 176 | fontSize: '1.1rem', 177 | textTransform: 'uppercase', 178 | color: '#000000', 179 | lineHeight: '30px', 180 | margin: 0 181 | }, 182 | underline: { 183 | borderBottom: 'solid 1px #222' 184 | }, 185 | blueLink: { 186 | color: '#337ab7', 187 | textDecoration: 'none' 188 | }, 189 | share: {}, 190 | social: {}, 191 | socialLinks: { 192 | display: 'flex' 193 | }, 194 | socialIcon: { 195 | margin: '0 10px', 196 | display: 'inline-block', 197 | color: '#ccc', 198 | borderBottom: 'solid 1px #fAfafa', 199 | textDecoration: 'none', 200 | backgroundColor: 'transparent', 201 | fontSize: '2.4rem', 202 | ':hover': { 203 | color: '#aaa' 204 | } 205 | }, 206 | footerBottom: { 207 | display: 'flex', 208 | flexWrap: 'wrap', 209 | justifyContent: 'end' 210 | }, 211 | otherArticle: { 212 | display: 'flex', 213 | justifyContent: 'center', 214 | backgroundPosition: 'center center', 215 | backgroundSize: 'cover', 216 | height: '20rem', 217 | pointerEvents: 'auto', 218 | width: '80%', 219 | position: 'relative', 220 | marginRight: '10%', 221 | marginLeft: '10%', 222 | marginBottom: '3rem', 223 | '@media (min-width: 768px)': { 224 | width: '40%', 225 | marginRight: '5%', 226 | marginLeft: '5%' 227 | }, 228 | '@media (min-width: 992px)': { 229 | width: '27.3%', 230 | marginRight: '3%', 231 | marginLeft: '3%' 232 | }, 233 | '@media (min-width: 1200px)': { 234 | width: '27.3%', 235 | marginRight: '3%', 236 | marginLeft: '3%' 237 | } 238 | }, 239 | otherArticleNarrow: { 240 | width: '80%', 241 | marginRight: '10%', 242 | marginLeft: '10%', 243 | '@media (min-width: 768px)': { 244 | width: '80%', 245 | marginRight: '10%', 246 | marginLeft: '10%' 247 | }, 248 | '@media (min-width: 992px)': { 249 | width: '40%', 250 | marginRight: '5%', 251 | marginLeft: '5%' 252 | }, 253 | '@media (min-width: 1200px)': { 254 | width: '27.3%', 255 | marginRight: '3%', 256 | marginLeft: '3%' 257 | } 258 | }, 259 | 260 | overlay: { 261 | position: 'absolute', 262 | width: '100%', 263 | height: '100%', 264 | zIndex: 2, 265 | backgroundColor: 'rgba(50,50,50,.5)', 266 | top: 0, 267 | left: 0, 268 | pointerEvents: 'none' 269 | }, 270 | otherArticleTitle: { 271 | color: '#E9E9E9', 272 | marginRight: '5px', 273 | cursor: 'pointer', 274 | borderBottom: 'solid 1px #fAfafa', 275 | textDecoration: 'none', 276 | backgroundColor: 'transparent', 277 | fontSize: 'large', 278 | letterSpacing: '2px', 279 | fontFamily: '"Source Sans Pro",Helvetica,Arial,sans-serif', 280 | textTransform: 'uppercase', 281 | lineHeight: '30px', 282 | margin: 0, 283 | alignSelf: 'center', 284 | zIndex: 5, 285 | ':hover': { 286 | color: '#fff' 287 | } 288 | } 289 | }) 290 | -------------------------------------------------------------------------------- /src/components/layout/menu.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { StyleSheet, css } from 'aphrodite' 3 | import { Link } from 'react-router-dom' 4 | import { connect } from 'react-redux' 5 | import { bindActionCreators } from 'redux' 6 | 7 | class Menu extends Component { 8 | constructor() { 9 | super() 10 | this.toggleCategory = this.toggleCategory.bind(this) 11 | 12 | this.state = { 13 | activeCategory: false 14 | } 15 | } 16 | 17 | toggleCategory(event) { 18 | let { activeCategory } = this.state 19 | let category = event.target.dataset.category 20 | this.setState({ 21 | activeCategory: category !== activeCategory ? category : false 22 | }) 23 | } 24 | 25 | componentWillReceiveProps(nextProps) { 26 | let { categories } = nextProps 27 | if (categories && Object.values(categories).length) { 28 | this.setState({ 29 | activeCategory: Object.values(categories)[0]['id'] 30 | }) 31 | } 32 | } 33 | 34 | render() { 35 | let { categories, articles, menuVisible } = this.props 36 | let { activeCategory } = this.state 37 | return ( 38 | 125 | ) 126 | } 127 | } 128 | 129 | function mapStateToProps(state) { 130 | return { 131 | categories: state.category.categories, 132 | articles: state.article.articles 133 | } 134 | } 135 | 136 | function mapDispatchToProps(dispatch) { 137 | return Object.assign(bindActionCreators({}, dispatch), { dispatch }) 138 | } 139 | 140 | export default connect( 141 | mapStateToProps, 142 | mapDispatchToProps 143 | )(Menu) 144 | 145 | let styles = StyleSheet.create({ 146 | menu: { 147 | backgroundColor: '#333', 148 | overflow: 'hidden', 149 | zIndex: 10, 150 | display: 'block', 151 | top: 0, 152 | left: 0, 153 | height: '100%', 154 | boxShadow: '#000 2px 2px 10px', 155 | paddingTop: '5rem', 156 | transition: 'opacity linear 750ms,width linear 750ms', 157 | width: 0, 158 | opacity: 0, 159 | paddingRight: 0, 160 | position: 'fixed' 161 | }, 162 | menuOpen: { 163 | opacity: 1, 164 | width: '100%', 165 | '@media (min-width: 768px)': { 166 | width: '40%' 167 | }, 168 | '@media (min-width: 992px)': { 169 | width: '30%' 170 | }, 171 | '@media (min-width: 1200px)': { 172 | width: '25%' 173 | } 174 | }, 175 | icon: { 176 | padding: '0 20px', 177 | color: '#DADADA', 178 | fontSize: '1.6rem' 179 | }, 180 | list: { 181 | padding: '10px 0', 182 | fontSize: '1.6rem', 183 | marginBottom: 20, 184 | marginTop: 0 185 | }, 186 | item: { 187 | margin: 0, 188 | listStyle: 'none', 189 | padding: '10px 0', 190 | fontSize: '1.6rem' 191 | }, 192 | itemLink: { 193 | color: '#DADADA', 194 | fontWeight: 500, 195 | fontSize: 'large', 196 | borderBottom: '0 transparent', 197 | backgroundColor: 'transparent', 198 | outline: 0, 199 | border: 0, 200 | cursor: 'pointer', 201 | ':hover': { 202 | color: '#fff', 203 | outline: 0 204 | }, 205 | ':focus': { 206 | outline: 0 207 | }, 208 | fontFamily: 'Arial' 209 | }, 210 | separator: { 211 | margin: '20px auto', 212 | display: 'block', 213 | border: '1px solid #dededc', 214 | height: 0, 215 | width: '40%' 216 | }, 217 | subList: { 218 | marginLeft: '15px', 219 | paddingTop: 0, 220 | paddingBottom: 0, 221 | position: 'relative', 222 | padding: '10px 0', 223 | marginBottom: 0, 224 | fontSize: '1.6rem', 225 | marginTop: 0 226 | }, 227 | subItem: { 228 | padding: 0, 229 | height: 0, 230 | overflow: 'hidden', 231 | opacity: '.1', 232 | position: 'relative', 233 | fontSize: 'small', 234 | margin: 0, 235 | listStyle: 'none', 236 | transition: 'opacity ease 750ms,height linear 750ms' 237 | }, 238 | subItemExpanded: { 239 | opacity: 1, 240 | height: '4.5rem', 241 | transition: 'opacity ease 750ms,height linear 750ms' 242 | }, 243 | subItemLink: { 244 | fontSize: 'medium', 245 | position: 'relative', 246 | color: '#DADADA', 247 | fontWeight: 500, 248 | borderBottom: '0 transparent', 249 | textDecoration: 'none', 250 | backgroundColor: 'transparent', 251 | fontStyle: 'normal', 252 | top: '10px', 253 | ':hover': { 254 | borderBottom: 'none', 255 | color: '#fff' 256 | }, 257 | fontFamily: 'Arial' 258 | } 259 | }) 260 | -------------------------------------------------------------------------------- /src/components/layout/page.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { Helmet } from 'react-helmet' 3 | import { StyleSheet, css } from 'aphrodite' 4 | import { bindActionCreators } from 'redux' 5 | import { connect } from 'react-redux' 6 | 7 | import { getLocation } from 'route/selectors' 8 | import * as categoryActions from 'category/actionCreators' 9 | 10 | import Menu from 'components/layout/menu' 11 | import Sidebar from 'components/layout/sidebar' 12 | import blocks from 'styles/blocks' 13 | 14 | class Page extends Component { 15 | static readyOnActions(dispatch) { 16 | return Promise.all([ 17 | dispatch(categoryActions.fetchCategoriesIfNeeded()) 18 | ]) 19 | } 20 | 21 | constructor() { 22 | super() 23 | this.toggleMenu = this.toggleMenu.bind(this) 24 | this.state = { 25 | menuVisible: !( 26 | typeof window !== 'undefined' && window.innerWidth < 769 27 | ) 28 | } 29 | } 30 | 31 | componentDidMount() { 32 | Page.readyOnActions(this.props.dispatch) 33 | } 34 | 35 | toggleMenu() { 36 | let { menuVisible } = this.state 37 | this.setState( 38 | { 39 | menuVisible: !menuVisible 40 | }, 41 | param => param 42 | ) 43 | } 44 | 45 | render() { 46 | let { 47 | title, 48 | subtitle, 49 | description, 50 | sidebarImage, 51 | showLinks, 52 | children 53 | } = this.props 54 | let { menuVisible } = this.state 55 | 56 | return ( 57 |
58 | 66 | 74 | 75 |
83 | 91 |
97 | {children} 98 |
99 |
100 |
101 | ) 102 | } 103 | } 104 | 105 | function mapStateToProps(state) { 106 | return { 107 | location: getLocation(state) 108 | } 109 | } 110 | 111 | function mapDispatchToProps(dispatch) { 112 | return Object.assign( 113 | bindActionCreators( 114 | { 115 | ...categoryActions 116 | }, 117 | dispatch 118 | ), 119 | { dispatch } 120 | ) 121 | } 122 | 123 | export default connect( 124 | mapStateToProps, 125 | mapDispatchToProps 126 | )(Page) 127 | 128 | let styles = StyleSheet.create({ 129 | page: { 130 | display: 'flex', 131 | width: '100%', 132 | justifyContent: 'flex-end', 133 | overflowX: 'hidden', 134 | maxWidth: '100%' 135 | }, 136 | main: { 137 | opacity: 1, 138 | width: '100%', 139 | display: 'block', 140 | transition: 'width linear 750ms', 141 | '@media (min-width: 768px)': { 142 | display: 'block' 143 | }, 144 | '@media (min-width: 992px)': { 145 | flexDirection: 'row', 146 | display: 'flex', 147 | justifyContent: 'flex-end' 148 | }, 149 | margin: 0, 150 | padding: 0, 151 | overflowX: 'hidden', 152 | maxWidth: '100%' 153 | }, 154 | mainNarrow: { 155 | margin: 0, 156 | width: '100%', 157 | '@media (min-width: 768px)': { 158 | width: '60%', 159 | display: 'block' 160 | }, 161 | '@media (min-width: 992px)': { 162 | display: 'block', 163 | width: '70%' 164 | }, 165 | '@media (min-width: 1200px)': { 166 | width: '75%', 167 | flexDirection: 'row', 168 | display: 'flex', 169 | justifyContent: 'flex-end' 170 | } 171 | }, 172 | content: { 173 | padding: '5rem', 174 | overflowX: 'hidden', 175 | maxWidth: '100%', 176 | transition: 'width linear 750ms', 177 | width: '100%', 178 | marginLeft: 0, 179 | '@media (min-width: 768px)': { 180 | width: '100%' 181 | }, 182 | '@media (min-width: 992px)': { 183 | width: '60%' 184 | }, 185 | '@media (min-width: 1200px)': { 186 | width: '60%' 187 | } 188 | }, 189 | contentNarrow: { 190 | width: '100%', 191 | marginLeft: 0, 192 | '@media (min-width: 768px)': { 193 | width: '100%' 194 | }, 195 | '@media (min-width: 992px)': { 196 | width: '100%' 197 | }, 198 | '@media (min-width: 1200px)': { 199 | width: '52%' 200 | } 201 | }, 202 | menuBurger: { 203 | position: 'fixed', 204 | top: '1.5rem', 205 | left: '1.5rem', 206 | zIndex: '15', 207 | borderRadius: 5, 208 | height: '4rem', 209 | width: '4rem', 210 | background: '#333', 211 | paddingTop: 8, 212 | cursor: 'pointer', 213 | borderBottom: '0 transparent', 214 | boxShadow: '#948b8b 2px 2px 10px', 215 | color: '#fff', 216 | display: 'flex', 217 | flexDirection: 'column', 218 | alignItems: 'center', 219 | outline: 0, 220 | border: 0, 221 | ':hover': { 222 | color: '#fff', 223 | outline: 0, 224 | background: '#999' 225 | }, 226 | ':focus': { 227 | outline: 0 228 | } 229 | }, 230 | bar: { 231 | height: '0.5rem', 232 | width: '2.8rem', 233 | display: 'block', 234 | margin: '0 6px 5px', 235 | background: '#fff', 236 | borderRadius: '0.3rem' 237 | } 238 | }) 239 | -------------------------------------------------------------------------------- /src/components/layout/sidebar.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { pure } from 'recompose' 3 | import { StyleSheet, css } from 'aphrodite' 4 | import { Link } from 'react-router-dom' 5 | 6 | let Sidebar = ({ 7 | title, 8 | subtitle, 9 | description, 10 | sidebarImage, 11 | menuVisible, 12 | showLinks 13 | }) => ( 14 | 42 | ) 43 | 44 | export default pure(Sidebar) 45 | 46 | let styles = StyleSheet.create({ 47 | sidebar: { 48 | justifyContent: 'flex-start', 49 | alignItems: 'flex-end', 50 | transition: 51 | 'width linear 750ms, height linear 750ms, left linear 750ms', 52 | padding: 0, 53 | backgroundPosition: 'center', 54 | backgroundRepeat: 'no-repeat', 55 | backgroundSize: 'cover', 56 | display: 'flex', 57 | position: 'relative', 58 | width: '100%', 59 | '@media (min-width: 768px)': { 60 | height: '45rem', 61 | position: 'relative', 62 | width: '100%' 63 | }, 64 | '@media (min-width: 992px)': { 65 | height: '100vh', 66 | backgroundColor: '#f5f5f5', 67 | position: 'fixed', 68 | width: '40%', 69 | left: 0 70 | }, 71 | '@media (min-width: 1200px)': { 72 | height: '100vh', 73 | backgroundColor: '#f5f5f5', 74 | position: 'fixed', 75 | width: '40%', 76 | left: 0 77 | }, 78 | overflowX: 'hidden', 79 | maxWidth: '100%' 80 | }, 81 | sidebarNarrow: { 82 | padding: 0, 83 | backgroundPosition: 'center', 84 | backgroundRepeat: 'no-repeat', 85 | backgroundSize: 'cover', 86 | display: 'flex', 87 | width: '100%', 88 | '@media (min-width: 768px)': { 89 | height: '45rem', 90 | position: 'relative', 91 | width: '100%' 92 | }, 93 | '@media (min-width: 992px)': { 94 | height: '45rem', 95 | position: 'relative', 96 | width: '100%' 97 | }, 98 | '@media (min-width: 1200px)': { 99 | height: '100vh', 100 | backgroundColor: '#f5f5f5', 101 | position: 'fixed', 102 | width: '35%', 103 | left: '25%' 104 | } 105 | }, 106 | info: { 107 | padding: '5%', 108 | background: 'rgba(50,50,50,.5)', 109 | color: '#fafafa', 110 | height: '28rem', 111 | width: '100%', 112 | display: 'flex', 113 | justifyContent: 'flex-end', 114 | alignItems: 'end', 115 | flexDirection: 'column' 116 | }, 117 | 118 | primary: { 119 | borderBottom: 'solid 1px rgba(255,255,255,.3)', 120 | marginBottom: '1.6rem' 121 | }, 122 | h1: { 123 | letterSpacing: 0, 124 | marginBottom: 0, 125 | fontSize: '3.4rem', 126 | textShadow: '0 1px 3px rgba(0,0,0,.3)', 127 | fontWeight: 700, 128 | fontFamily: "'Source Sans Pro',Helvetica,Arial,sans-serif" 129 | }, 130 | p: { 131 | marginBottom: 10, 132 | textShadow: '0 1px 3px rgba(0,0,0,.3)', 133 | lineHeight: '2.4rem', 134 | fontSize: '1.8rem' 135 | }, 136 | links: { 137 | display: 'none' 138 | }, 139 | showLinks: { 140 | display: 'flex' 141 | }, 142 | button: { 143 | fontFamily: "'Source Sans Pro',Helvetica,Arial,sans-serif", 144 | display: 'inline-block', 145 | color: '#fff', 146 | marginRight: '20px', 147 | marginBottom: 0, 148 | backgroundColor: '#337ab7', 149 | borderColor: '#2e6da4', 150 | fontWeight: 400, 151 | textAlign: 'center', 152 | touchAction: 'manipulation', 153 | cursor: 'pointer', 154 | border: '1px solid transparent', 155 | whiteSpace: 'nowrap', 156 | padding: '6px 12px', 157 | fontSize: '14px', 158 | lineHeight: '1.42857', 159 | borderRadius: '4px', 160 | userSelect: 'none', 161 | ':hover': { 162 | color: '#fff', 163 | backgroundColor: '#286090', 164 | borderColor: '#204d74', 165 | textDecoration: 'none' 166 | }, 167 | ':focus': { 168 | color: '#fff', 169 | backgroundColor: '#286090', 170 | borderColor: '#122b40', 171 | textDecoration: 'none' 172 | } 173 | } 174 | }) 175 | -------------------------------------------------------------------------------- /src/lib/api.js: -------------------------------------------------------------------------------- 1 | export default class Api { 2 | constructor() { 3 | this.call = this.call.bind(this) 4 | this.get = this.get.bind(this) 5 | this.post = this.post.bind(this) 6 | } 7 | 8 | call( 9 | url, 10 | options = { 11 | method: 'GET', 12 | credentials: 'include', 13 | headers: {} 14 | } 15 | ) { 16 | let { method, credentials, headers } = options 17 | 18 | if (!credentials) { 19 | options = { ...options, credentials: 'include' } 20 | } 21 | 22 | options = Object.assign({}, options, { 23 | headers 24 | }) 25 | 26 | return fetch(url, options) 27 | } 28 | 29 | get( 30 | url, 31 | options = { 32 | method: 'GET', 33 | credentials: 'include' 34 | } 35 | ) { 36 | return this.call(url, { ...options, method: 'GET' }) 37 | } 38 | 39 | post( 40 | url, 41 | options = { 42 | method: 'POST', 43 | credentials: 'include' 44 | } 45 | ) { 46 | return this.call(url, { ...options, method: 'POST' }) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/lib/drive.js: -------------------------------------------------------------------------------- 1 | import Api from './api' 2 | import conf from 'conf' 3 | 4 | class Drive extends Api { 5 | constructor() { 6 | super() 7 | this.getSpreadsheet = this.getSpreadsheet.bind(this) 8 | this.getDocument = this.getDocument.bind(this) 9 | this.getCategories = this.getCategories.bind(this) 10 | this.getArticleHtml = this.getArticleHtml.bind(this) 11 | this.driveExportUrl = 'https://drive.google.com/uc?export=download&id=' 12 | this.slug = this.slug.bind(this) 13 | this.formatDate = this.formatDate.bind(this) 14 | } 15 | 16 | getSpreadsheet(fileId) { 17 | return this.get( 18 | `https://spreadsheets.google.com/feeds/list/${fileId}/od6/public/values?alt=json`, 19 | { 20 | credentials: 'omit' 21 | } 22 | ) 23 | .then(response => response.json()) 24 | .catch(error => console.log('error', error)) 25 | } 26 | 27 | getDocument(fileId) { 28 | return this.get( 29 | `https://docs.google.com/feeds/download/documents/export/Export?id=${fileId}&exportFormat=html`, 30 | { 31 | credentials: 'omit' 32 | } 33 | ) 34 | .then(response => { 35 | return response.text() 36 | }) 37 | .catch(error => console.log('error', error)) 38 | } 39 | 40 | getCategories() { 41 | return this.getSpreadsheet(conf.dashboardId).then(sheetData => { 42 | let articles = {} 43 | let categories = {} 44 | 45 | sheetData.feed.entry 46 | .map(row => ({ 47 | title: row.gsx$title.$t, 48 | subtitle: row.gsx$subtitle.$t, 49 | image: row.gsx$image.$t, 50 | category: row.gsx$category.$t, 51 | postId: row.gsx$postid.$t, 52 | imageId: row.gsx$imageid.$t, 53 | lastUpdated: row.gsx$lastupdated.$t 54 | })) 55 | .forEach(row => { 56 | let category = {} 57 | 58 | let categoryId = this.slug(row.category, 'category') 59 | 60 | let existingCategory = Object.values(categories).find( 61 | category => category.id === categoryId 62 | ) 63 | 64 | let article = { 65 | id: row.postId, 66 | title: row.title, 67 | subtitle: row.subtitle, 68 | imageName: row.image, 69 | image: this.driveExportUrl + row.imageId, 70 | categoryId, 71 | lastUpdated: row.lastUpdated, 72 | date: this.formatDate(row.lastUpdated), 73 | uri: `/articles/${row.postId}/${this.slug( 74 | row.title, 75 | 'article' 76 | )}` 77 | } 78 | 79 | if (existingCategory) { 80 | categories[categoryId].articles.push(row.postId) 81 | } else { 82 | category = { 83 | id: categoryId, 84 | title: row.category, 85 | imageName: row.image, 86 | image: this.driveExportUrl + row.imageId, 87 | articles: [row.postId], 88 | uri: `/categories/${categoryId}` 89 | } 90 | categories[categoryId] = category 91 | } 92 | articles[row.postId] = article 93 | }) 94 | return { 95 | articles, 96 | categories 97 | } 98 | }) 99 | } 100 | 101 | getArticleHtml(articleId) { 102 | return this.getDocument(articleId).then(doc => { 103 | let styleStart = '' 105 | let splitStyleStart = doc.split(styleStart) 106 | let splitStyleEnd = splitStyleStart[1].split(styleEnd) 107 | 108 | let htmlStart = '' + 122 | splitHtmlEnd[0] + 123 | '
' 124 | ) 125 | }) 126 | } 127 | 128 | slug(str, type = 'type') { 129 | str = str.replace(/^\s+|\s+$/g, '') 130 | str = str.toLowerCase() 131 | 132 | let from = 'ãàáäâẽèéëêìíïîõòóöôùúüûñç·/_,:;' 133 | let to = 'aaaaaeeeeeiiiiooooouuuunc------' 134 | for (let i = 0, l = from.length; i < l; i++) { 135 | str = str.replace(new RegExp(from.charAt(i), 'g'), to.charAt(i)) 136 | } 137 | 138 | str = str 139 | .replace(/[^a-z0-9 -]/g, '') 140 | .replace(/\s+/g, '-') 141 | .replace(/-+/g, '-') 142 | 143 | if (str.length < 4) { 144 | str = type + '_' + str 145 | } 146 | return str 147 | } 148 | 149 | formatDate(lastUpdated) { 150 | var fullDateSplit = lastUpdated.split(' ') 151 | var dateSplit = fullDateSplit[0].split('/') 152 | var day = parseInt(dateSplit[0]) 153 | var month = dateSplit[1] 154 | var year = dateSplit[2] 155 | var monthNames = [ 156 | 'January', 157 | 'February', 158 | 'March', 159 | 'April', 160 | 'May', 161 | 'June', 162 | 'July', 163 | 'August', 164 | 'September', 165 | 'October', 166 | 'November', 167 | 'December' 168 | ] 169 | var daySuffix = 'th' 170 | switch (day) { 171 | case 1: 172 | daySuffix = 'st' 173 | break 174 | case 2: 175 | daySuffix = 'nd' 176 | break 177 | case 3: 178 | daySuffix = 'rd' 179 | break 180 | } 181 | return day + daySuffix + ' of ' + monthNames[month - 1] + ' ' + year 182 | } 183 | } 184 | 185 | export default new Drive() 186 | -------------------------------------------------------------------------------- /src/lib/mail.js: -------------------------------------------------------------------------------- 1 | import Api from './api' 2 | import conf from 'conf' 3 | import jsonpCall from 'utils/jsonpCall' 4 | 5 | class Mail extends Api { 6 | constructor() { 7 | super() 8 | this.send = this.send.bind(this) 9 | } 10 | 11 | send(form) { 12 | jsonpCall('http://smart-ip.net/info-json', ipInfo => { 13 | form.ip = ipInfo.address 14 | form.country = ipInfo.countryName 15 | 16 | jsonpCall( 17 | `https://script.google.com/macros/s/${ 18 | conf.sendContactMessageUrlId 19 | }/exec?${Object.keys(form) 20 | .map( 21 | property => 22 | `${property}=${encodeURIComponent(form[property])}` 23 | ) 24 | .join('&')}`, 25 | response => { 26 | console.log(response) 27 | } 28 | ) 29 | }) 30 | } 31 | } 32 | 33 | export default new Mail() 34 | -------------------------------------------------------------------------------- /src/modules/article/actionCreators.js: -------------------------------------------------------------------------------- 1 | import { REQUEST_ARTICLE, RECEIVE_ARTICLE } from './actionTypes' 2 | import { shouldFetchArticle } from './selectors' 3 | import Drive from 'lib/drive' 4 | 5 | export function fetchArticleIfNeeded(articleId) { 6 | return (dispatch, getState) => { 7 | let state = getState() 8 | 9 | if (shouldFetchArticle(state, articleId)) { 10 | return fetchArticle(dispatch, articleId) 11 | } 12 | } 13 | } 14 | 15 | export function fetchArticle(dispatch, articleId) { 16 | dispatch(requestArticle(articleId)) 17 | return Drive.getArticleHtml(articleId) 18 | .then(article => { 19 | return dispatch(receiveArticle(articleId, article)) 20 | }) 21 | .catch(function(error) { 22 | console.log('code:', error.code, ' message:', error.message) 23 | }) 24 | } 25 | 26 | export function requestArticle(articleId) { 27 | return { 28 | type: REQUEST_ARTICLE, 29 | articleId 30 | } 31 | } 32 | 33 | export function receiveArticle(articleId, article) { 34 | return { 35 | type: RECEIVE_ARTICLE, 36 | articleId, 37 | article 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/modules/article/actionTypes.js: -------------------------------------------------------------------------------- 1 | export const REQUEST_ARTICLE = 'REQUEST_ARTICLE' 2 | export const RECEIVE_ARTICLE = 'RECEIVE_ARTICLE' 3 | -------------------------------------------------------------------------------- /src/modules/article/reducer.js: -------------------------------------------------------------------------------- 1 | import { REQUEST_ARTICLE, RECEIVE_ARTICLE } from './actionTypes' 2 | import { RECEIVE_CATEGORIES } from 'category/actionTypes' 3 | 4 | export default function article(state = initialState, action) { 5 | switch (action.type) { 6 | case REQUEST_ARTICLE: 7 | return { 8 | ...state, 9 | isFetching: { 10 | ...state.isFetching, 11 | [action.articleId]: true 12 | } 13 | } 14 | 15 | case RECEIVE_ARTICLE: 16 | return { 17 | ...state, 18 | isFetching: { 19 | ...state.isFetching, 20 | [action.articleId]: false 21 | }, 22 | fetched: { 23 | ...state.fetched, 24 | [action.articleId]: true 25 | }, 26 | texts: { 27 | ...state.texts, 28 | [action.articleId]: action.article 29 | } 30 | } 31 | 32 | case RECEIVE_CATEGORIES: 33 | return { 34 | ...state, 35 | articles: { 36 | ...state.articles, 37 | ...action.categories.articles 38 | } 39 | } 40 | 41 | default: 42 | return state 43 | } 44 | } 45 | 46 | const initialState = { 47 | isFetching: {}, 48 | fetched: {}, 49 | articles: {}, 50 | texts: {} 51 | } 52 | -------------------------------------------------------------------------------- /src/modules/article/selectors.js: -------------------------------------------------------------------------------- 1 | export const shouldFetchArticle = (state, articleId) => { 2 | if (!articleId) { 3 | return false 4 | } 5 | return ( 6 | !articleIsLoading(state, articleId) && 7 | typeof state.article.texts[articleId] === 'undefined' 8 | ) 9 | } 10 | 11 | export const articleIsLoading = (state, articleId) => { 12 | if (!articleId) { 13 | return false 14 | } 15 | return state.article.isFetching[articleId] 16 | } 17 | 18 | export const articleIsFetched = (state, articleId) => { 19 | if (!articleId) { 20 | return false 21 | } 22 | return state.article.fetched[articleId] 23 | } 24 | -------------------------------------------------------------------------------- /src/modules/category/actionCreators.js: -------------------------------------------------------------------------------- 1 | import { REQUEST_CATEGORIES, RECEIVE_CATEGORIES } from './actionTypes' 2 | import { shouldFetchCategories } from './selectors' 3 | import Drive from 'lib/drive' 4 | 5 | export function fetchCategoriesIfNeeded() { 6 | return (dispatch, getState) => { 7 | let state = getState() 8 | 9 | if (shouldFetchCategories(state)) { 10 | return fetchCategories(dispatch) 11 | } 12 | } 13 | } 14 | 15 | export function fetchCategories(dispatch) { 16 | dispatch(requestCategories()) 17 | return Drive.getCategories() 18 | .then(categories => { 19 | return dispatch(receiveCategories(categories)) 20 | }) 21 | .catch(function(error) { 22 | console.log('code:', error.code, ' message:', error.message) 23 | }) 24 | } 25 | 26 | export function requestCategories() { 27 | return { 28 | type: REQUEST_CATEGORIES 29 | } 30 | } 31 | 32 | export function receiveCategories(categories) { 33 | return { 34 | type: RECEIVE_CATEGORIES, 35 | categories 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/modules/category/actionTypes.js: -------------------------------------------------------------------------------- 1 | export const REQUEST_CATEGORIES = 'REQUEST_CATEGORIES' 2 | export const RECEIVE_CATEGORIES = 'RECEIVE_CATEGORIES' 3 | -------------------------------------------------------------------------------- /src/modules/category/reducer.js: -------------------------------------------------------------------------------- 1 | import { REQUEST_CATEGORIES, RECEIVE_CATEGORIES } from './actionTypes' 2 | 3 | export default function category(state = initialState, action) { 4 | switch (action.type) { 5 | case REQUEST_CATEGORIES: 6 | return { 7 | ...state, 8 | isFetching: true 9 | } 10 | 11 | case RECEIVE_CATEGORIES: 12 | return { 13 | ...state, 14 | isFetching: true, 15 | fetched: true, 16 | categories: { 17 | ...action.categories.categories 18 | } 19 | } 20 | 21 | default: 22 | return state 23 | } 24 | } 25 | 26 | const initialState = { 27 | isFetching: false, 28 | fetched: false, 29 | categories: {} 30 | } 31 | -------------------------------------------------------------------------------- /src/modules/category/selectors.js: -------------------------------------------------------------------------------- 1 | export const shouldFetchCategories = state => 2 | !state.category.isFetching && !state.category.fetched 3 | -------------------------------------------------------------------------------- /src/modules/main/containers/noMatch.js: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from 'react' 2 | import { Helmet } from 'react-helmet' 3 | 4 | class NoMatch extends PureComponent { 5 | render() { 6 | return ( 7 |
8 | 9 | Page was not found 10 |
11 | ) 12 | } 13 | } 14 | 15 | export default NoMatch 16 | -------------------------------------------------------------------------------- /src/modules/main/containers/root.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { Provider } from 'react-redux' 3 | import ConnectedRouter from 'route/ConnectedRouter' 4 | import { renderRoutes } from 'react-router-dom' 5 | import { css } from 'aphrodite' 6 | import routes from 'routes/routes' 7 | 8 | import blocks from 'styles/blocks' 9 | 10 | let Root = ({ store, history }) => ( 11 | 12 | 13 |
{renderRoutes(routes)}
14 |
15 |
16 | ) 17 | 18 | export default Root 19 | -------------------------------------------------------------------------------- /src/modules/main/rootReducer.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux' 2 | 3 | import route from 'route/reducer' 4 | import article from 'article/reducer' 5 | import category from 'category/reducer' 6 | 7 | const rootReducer = combineReducers({ 8 | article, 9 | category, 10 | route 11 | }) 12 | 13 | export default rootReducer 14 | -------------------------------------------------------------------------------- /src/modules/main/store/configureStore.js: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware, compose } from 'redux' 2 | import thunk from 'redux-thunk' 3 | 4 | import rootReducer from 'main/rootReducer' 5 | 6 | export default function configureStore(initialState, routerMiddleware) { 7 | return createStore( 8 | rootReducer, 9 | initialState, 10 | applyMiddleware(thunk, routerMiddleware) 11 | ) 12 | } 13 | -------------------------------------------------------------------------------- /src/modules/route/ConnectedRouter.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import PropTypes from 'prop-types' 3 | import { Router } from 'react-router-dom' 4 | import { LOCATION_CHANGE } from './actionTypes' 5 | 6 | class ConnectedRouter extends Component { 7 | static propTypes = { 8 | store: PropTypes.object, 9 | history: PropTypes.object, 10 | children: PropTypes.node 11 | } 12 | 13 | static contextTypes = { 14 | store: PropTypes.object 15 | } 16 | 17 | handleLocationChange = location => { 18 | this.store.dispatch({ 19 | type: LOCATION_CHANGE, 20 | location 21 | }) 22 | } 23 | 24 | componentWillMount() { 25 | const { store: propsStore, history } = this.props 26 | this.store = propsStore || this.context.store 27 | this.handleLocationChange(history.location) 28 | } 29 | 30 | componentDidMount() { 31 | const { history } = this.props 32 | this.unsubscribeFromHistory = history.listen(this.handleLocationChange) 33 | } 34 | 35 | componentWillUnmount() { 36 | if (this.unsubscribeFromHistory) this.unsubscribeFromHistory() 37 | } 38 | 39 | render() { 40 | return 41 | } 42 | } 43 | 44 | export default ConnectedRouter 45 | -------------------------------------------------------------------------------- /src/modules/route/actionCreators.js: -------------------------------------------------------------------------------- 1 | import { CALL_HISTORY_METHOD } from './actionTypes' 2 | 3 | import { getLocation, getQuery } from './selectors' 4 | import { getActiveEntityType } from 'entity/selectors' 5 | import { fetchEntityQueryIfNeeded } from 'entity/actionCreators' 6 | import serializeQuery from 'utils/serializeQuery' 7 | 8 | /** 9 | * This action type will be dispatched by the history actions below. 10 | * If you're writing a middleware to watch for navigation events, be sure to 11 | * look for actions of this type. 12 | */ 13 | 14 | function updateLocation(method) { 15 | return (...args) => ({ 16 | type: CALL_HISTORY_METHOD, 17 | location: { method, args } 18 | }) 19 | } 20 | 21 | /** 22 | * These actions correspond to the history API. 23 | * The associated routerMiddleware will capture these events before they get to 24 | * your reducer and reissue them as the matching function on your history. 25 | */ 26 | export const push = updateLocation('push') 27 | export const replace = updateLocation('replace') 28 | export const go = updateLocation('go') 29 | export const goBack = updateLocation('goBack') 30 | export const goForward = updateLocation('goForward') 31 | 32 | export const routerActions = { push, replace, go, goBack, goForward } 33 | 34 | export function updateQuery(property, value) { 35 | return (dispatch, getState) => { 36 | let state = getState() 37 | let query = getQuery(state) 38 | let location = getLocation(state) 39 | let entityType = getActiveEntityType(state) 40 | let queryValue = query[property] 41 | let label = '' 42 | 43 | if (!!value && typeof value === 'object') { 44 | label = value.label 45 | console.log('label is', label) 46 | value = value.value 47 | } 48 | if (!!value && (!queryValue || queryValue !== value)) { 49 | query = { ...query, [property]: value } 50 | } else { 51 | query = { ...query } 52 | delete query[property] 53 | } 54 | 55 | let requestQuery = serializeQuery(query) 56 | console.log({ reqqqqq: requestQuery, enttttt: entityType }) 57 | return Promise.resolve(true) 58 | .then(chained => 59 | dispatch( 60 | push({ 61 | pathname: location.pathname, 62 | query, 63 | search: requestQuery, 64 | state: { ...location.state, label } 65 | }) 66 | ) 67 | ) 68 | .then(updated => 69 | dispatch( 70 | fetchEntityQueryIfNeeded(requestQuery, entityType, true) 71 | ) 72 | ) 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/modules/route/actionTypes.js: -------------------------------------------------------------------------------- 1 | export const CALL_HISTORY_METHOD = '@@router/CALL_HISTORY_METHOD' 2 | export const LOCATION_CHANGE = '@@router/LOCATION_CHANGE' 3 | -------------------------------------------------------------------------------- /src/modules/route/middleware.js: -------------------------------------------------------------------------------- 1 | import { CALL_HISTORY_METHOD } from './actionTypes' 2 | 3 | /** 4 | * This middleware captures CALL_HISTORY_METHOD actions to redirect to the 5 | * provided history object. This will prevent these actions from reaching your 6 | * reducer or any middleware that comes after this one. 7 | */ 8 | export default function routerMiddleware(history) { 9 | return () => next => action => { 10 | if (action.type !== CALL_HISTORY_METHOD) { 11 | return next(action) 12 | } 13 | const { 14 | location: { method, args } 15 | } = action 16 | history[method](...args) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/modules/route/reducer.js: -------------------------------------------------------------------------------- 1 | import { LOCATION_CHANGE } from './actionTypes' 2 | 3 | const initialState = { 4 | location: { 5 | pathname: '/', 6 | query: {}, 7 | search: '' 8 | } 9 | } 10 | 11 | export default function route(state = initialState, action) { 12 | //console.log('route reducer') 13 | switch (action.type) { 14 | case LOCATION_CHANGE: 15 | let location = { ...action.location } 16 | if ( 17 | !!location && 18 | !!location.search && 19 | (!location.query || !Object.keys(location.query).length) 20 | ) { 21 | let search = location.search.slice(1) 22 | location.query = JSON.parse( 23 | '{"' + 24 | decodeURI(search.replace('+', ' ')) 25 | .replace(/"/g, '\\"') 26 | .replace(/&/g, '","') 27 | .replace(/=/g, '":"') + 28 | '"}' 29 | ) 30 | } 31 | return { 32 | ...state, 33 | location 34 | } 35 | 36 | default: 37 | return state 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/modules/route/selectors.js: -------------------------------------------------------------------------------- 1 | import { matchPath } from 'react-router-dom' 2 | 3 | export const getLocation = state => state.route.location 4 | 5 | export const createMatchSelector = path => { 6 | let lastPathname = null 7 | let lastMatch = null 8 | return state => { 9 | const { pathname } = getLocation(state) 10 | if (pathname === lastPathname) { 11 | return lastMatch 12 | } 13 | lastPathname = pathname 14 | const match = matchPath(pathname, path) 15 | if (!match || !lastMatch || match.url !== lastMatch.url) { 16 | lastMatch = match 17 | } 18 | return lastMatch 19 | } 20 | } 21 | 22 | export const getQuery = state => 23 | !!state.route.location.query ? state.route.location.query : {} 24 | 25 | export const getSerializedQuery = state => state.route.location.search.slice(1) 26 | 27 | export const getPath = state => state.route.location.pathname 28 | 29 | export const getPathParts = state => 30 | state.route.location.pathname.split('/').filter(part => !!part) 31 | 32 | export const getRoot = state => { 33 | let parts = getPathParts(state) 34 | let root = 'home' 35 | if (typeof parts[0] !== 'undefined') { 36 | root = parts[0] 37 | } 38 | return root 39 | } 40 | -------------------------------------------------------------------------------- /src/routes/about.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { StyleSheet, css } from 'aphrodite' 3 | import { Link } from 'react-router-dom' 4 | import Page from 'components/layout/page' 5 | import conf from 'conf' 6 | 7 | let About = () => ( 8 | 19 |
20 | 28 |
29 |

React Drive CMS Demo

30 |

31 | A demo site to showcase the use of Google Drive as a Content 32 | Management System. Write articles in Google Docs and publish 33 | them directly from there. 34 |

35 |

36 | Google Drive is the backend, only a few static files are 37 | hosted on GitHub Pages, and the content is displayed with 38 | React JS. 39 |

40 |
41 |
42 | 43 |
44 | 45 | Contact 46 | 47 |
48 |
49 | ) 50 | 51 | export default About 52 | 53 | let styles = StyleSheet.create({ 54 | content: { display: 'block' }, 55 | image: { 56 | borderRadius: '50%', 57 | width: '150px', 58 | border: 0, 59 | maxWidth: '100%', 60 | verticalAlign: 'middle', 61 | float: 'left', 62 | marginRight: '2rem' 63 | }, 64 | info: {}, 65 | title: { 66 | margin: '30px 0 20px', 67 | fontSize: '3.8rem', 68 | fontWeight: 700, 69 | lineHeight: '1.1', 70 | fontFamily: '"Source Sans Pro",Helvetica,Arial,sans-serif' 71 | }, 72 | p: { 73 | fontSize: '2rem', 74 | margin: '0 0 10px', 75 | marginBottom: '30px' 76 | }, 77 | footer: { 78 | padding: '10px 0', 79 | fontSize: '1.4rem', 80 | letterSpacing: '1px', 81 | fontWeight: 700, 82 | fontFamily: '"Source Sans Pro",Helvetica,Arial,sans-serif', 83 | textTransform: 'uppercase' 84 | }, 85 | contact: { 86 | textDecoration: 'none', 87 | backgroundColor: 'transparent', 88 | color: '#999', 89 | borderBottom: 'none', 90 | fontSize: '1.4rem', 91 | ':hover': { 92 | textDecoration: 'none', 93 | backgroundColor: 'transparent', 94 | color: '#333', 95 | outline: 0, 96 | transition: 'all .4s', 97 | borderBottom: 'none' 98 | } 99 | } 100 | }) 101 | -------------------------------------------------------------------------------- /src/routes/article.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { Helmet } from 'react-helmet' 3 | import { StyleSheet, css } from 'aphrodite' 4 | import { bindActionCreators } from 'redux' 5 | import { connect } from 'react-redux' 6 | 7 | import { getLocation } from 'route/selectors' 8 | import * as categoryActions from 'category/actionCreators' 9 | import * as articleActions from 'article/actionCreators' 10 | import DisqusThread from 'components/disqus/disqusThread' 11 | 12 | import Menu from 'components/layout/menu' 13 | import blocks from 'styles/blocks' 14 | import Footer from 'components/layout/footer' 15 | 16 | class Article extends Component { 17 | static readyOnActions(dispatch, activeArticleId) { 18 | return Promise.all([ 19 | dispatch(categoryActions.fetchCategoriesIfNeeded()), 20 | dispatch(articleActions.fetchArticleIfNeeded(activeArticleId)) 21 | ]) 22 | } 23 | 24 | constructor() { 25 | super() 26 | this.toggleMenu = this.toggleMenu.bind(this) 27 | this.state = { 28 | menuVisible: !( 29 | typeof window !== 'undefined' && window.innerWidth < 769 30 | ) 31 | } 32 | } 33 | 34 | componentDidMount() { 35 | let { match } = this.props 36 | let activeArticleId = match.params.articleId 37 | Article.readyOnActions(this.props.dispatch, activeArticleId) 38 | } 39 | 40 | componentWillReceiveProps(nextProps) { 41 | let { match } = this.props 42 | let activeArticleId = match.params.articleId 43 | if (nextProps.match.params.articleId !== activeArticleId) { 44 | Article.readyOnActions( 45 | this.props.dispatch, 46 | nextProps.match.params.articleId 47 | ).then(loaded => { 48 | let element = document.getElementById('article-header') 49 | element.scrollIntoView() 50 | }) 51 | } 52 | } 53 | 54 | toggleMenu() { 55 | let { menuVisible } = this.state 56 | this.setState( 57 | { 58 | menuVisible: !menuVisible 59 | }, 60 | param => param 61 | ) 62 | } 63 | 64 | render() { 65 | let { texts, articles, categories, match } = this.props 66 | let activeArticleId = match.params.articleId 67 | let activeArticle = { title: '', id: activeArticleId }, 68 | activeText, 69 | category = { title: '', uri: '/' } 70 | if ( 71 | typeof articles[activeArticleId] !== 'undefined' && 72 | typeof texts[activeArticleId] !== 'undefined' && 73 | typeof categories[articles[activeArticleId].categoryId] !== 74 | 'undefined' 75 | ) { 76 | activeArticle = articles[activeArticleId] 77 | activeText = texts[activeArticleId] 78 | category = categories[activeArticle.categoryId] 79 | } 80 | 81 | let { menuVisible } = this.state 82 | 83 | return ( 84 |
85 | 93 | 102 | 103 |
110 |
146 |
147 | ) 148 | } 149 | } 150 | 151 | function mapStateToProps(state) { 152 | return { 153 | location: getLocation(state), 154 | articles: state.article.articles, 155 | categories: state.category.categories, 156 | texts: state.article.texts 157 | } 158 | } 159 | 160 | function mapDispatchToProps(dispatch) { 161 | return Object.assign( 162 | bindActionCreators( 163 | { 164 | ...categoryActions 165 | }, 166 | dispatch 167 | ), 168 | { dispatch } 169 | ) 170 | } 171 | 172 | export default connect( 173 | mapStateToProps, 174 | mapDispatchToProps 175 | )(Article) 176 | 177 | const opacityKeyframes = { 178 | from: { 179 | opacity: 0 180 | }, 181 | 182 | to: { 183 | opacity: 1 184 | } 185 | } 186 | 187 | let styles = StyleSheet.create({ 188 | hero: { 189 | position: 'relative', 190 | display: 'block', 191 | height: '15rem', 192 | width: '100%', 193 | backgroundPosition: 'center', 194 | backgroundRepeat: 'no-repeat', 195 | backgroundSize: 'cover', 196 | '@media (min-width: 768px)': { 197 | height: '30rem' 198 | }, 199 | overflowX: 'hidden', 200 | maxWidth: '100%' 201 | }, 202 | title: { 203 | paddingTop: '20pt', 204 | color: '#000000', 205 | fontSize: '20pt', 206 | paddingBottom: '6pt', 207 | fontFamily: '"Arial"', 208 | lineHeight: '1.15', 209 | pageBreakAfter: 'avoid', 210 | orphans: 2, 211 | widows: 2, 212 | textAlign: 'left', 213 | letterSpacing: '-1pt', 214 | marginTop: '20px', 215 | margin: '.67em 0', 216 | fontWeight: 700, 217 | marginBottom: '10px' 218 | }, 219 | p: { 220 | margin: 0, 221 | paddingTop: '0pt', 222 | color: '#666666', 223 | fontSize: '15pt', 224 | paddingBottom: '16pt', 225 | fontFamily: '"Arial"', 226 | lineHeight: '1.15', 227 | pageBreakAfter: 'avoid', 228 | orphans: 2, 229 | widows: 2, 230 | textAlign: 'left' 231 | }, 232 | text: {}, 233 | page: { 234 | display: 'flex', 235 | width: '100%', 236 | justifyContent: 'flex-end', 237 | overflowX: 'hidden', 238 | maxWidth: '100%' 239 | }, 240 | main: { 241 | opacity: 1, 242 | width: '100%', 243 | overflowX: 'hidden', 244 | display: 'block', 245 | transition: 'width linear 750ms', 246 | margin: 0, 247 | padding: 0, 248 | animationName: [opacityKeyframes], 249 | animationDuration: '1s, 1s', 250 | animationIterationCount: 1, 251 | maxWidth: '100%' 252 | }, 253 | mainNarrow: { 254 | '@media (min-width: 768px)': { 255 | width: '60%' 256 | }, 257 | '@media (min-width: 992px)': { 258 | width: '70%' 259 | }, 260 | '@media (min-width: 1200px)': { 261 | width: '75%' 262 | } 263 | }, 264 | content: { 265 | display: 'block', 266 | width: '100%', 267 | padding: '3rem 8%', 268 | '@media (min-width: 768px)': { 269 | padding: '3rem 16%' 270 | }, 271 | '@media (min-width: 992px)': { 272 | padding: '3rem 18%' 273 | }, 274 | '@media (min-width: 1200px)': { 275 | padding: '3rem 24%' 276 | }, 277 | overflowX: 'hidden', 278 | maxWidth: '100%' 279 | }, 280 | contentNarrow: { 281 | display: 'block', 282 | width: '100%', 283 | padding: '3rem 8%', 284 | '@media (min-width: 768px)': { 285 | padding: '3rem 8%' 286 | }, 287 | '@media (min-width: 992px)': { 288 | padding: '3rem 12%' 289 | }, 290 | '@media (min-width: 1200px)': { 291 | padding: '3rem 20%' 292 | } 293 | }, 294 | 295 | menuBurger: { 296 | position: 'fixed', 297 | top: '1.5rem', 298 | left: '1.5rem', 299 | zIndex: '15', 300 | borderRadius: 5, 301 | height: '4rem', 302 | width: '4rem', 303 | background: '#333', 304 | paddingTop: 8, 305 | cursor: 'pointer', 306 | borderBottom: '0 transparent', 307 | boxShadow: '#948b8b 2px 2px 10px', 308 | color: '#fff', 309 | display: 'flex', 310 | flexDirection: 'column', 311 | alignItems: 'center', 312 | outline: 0, 313 | border: 0, 314 | ':hover': { 315 | color: '#fff', 316 | outline: 0, 317 | background: '#999' 318 | }, 319 | ':focus': { 320 | outline: 0 321 | } 322 | }, 323 | bar: { 324 | height: '0.5rem', 325 | width: '2.8rem', 326 | display: 'block', 327 | margin: '0 6px 5px', 328 | background: '#fff', 329 | borderRadius: '0.3rem' 330 | } 331 | }) 332 | -------------------------------------------------------------------------------- /src/routes/category.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { StyleSheet, css } from 'aphrodite' 3 | import { connect } from 'react-redux' 4 | import { bindActionCreators } from 'redux' 5 | import { Link } from 'react-router-dom' 6 | 7 | import { getLocation } from 'route/selectors' 8 | 9 | import Page from 'components/layout/page' 10 | import DisqusCount from 'components/disqus/disqusCount' 11 | import Article from 'components/blocks/article' 12 | import Category from 'components/blocks/category' 13 | 14 | class CategoryPage extends Component { 15 | constructor() { 16 | super() 17 | this.setActivePanel = this.setActivePanel.bind(this) 18 | this.state = { 19 | activePanel: 'posts' 20 | } 21 | } 22 | 23 | setActivePanel() { 24 | let panel = event.target.dataset.panel 25 | this.setState({ 26 | activePanel: panel 27 | }) 28 | } 29 | 30 | componentWillReceiveProps(nextProps) { 31 | let { match } = this.props 32 | let activeCategoryId = match.params.categoryId 33 | let nextCategoryId = nextProps.match.params.categoryId 34 | if (activeCategoryId !== nextCategoryId) { 35 | this.setState({ 36 | activePanel: 'posts' 37 | }) 38 | } 39 | } 40 | 41 | render() { 42 | let { location, categories, articles, match } = this.props 43 | let activeCategoryId = match.params.categoryId 44 | let activeCategory = categories[activeCategoryId] 45 | ? categories[activeCategoryId] 46 | : { 47 | title: '', 48 | image: `${window.location.protocol}//${ 49 | window.location.hostname 50 | }:${window.location.port}/assets/images/default-sidebar.jpg` 51 | } 52 | 53 | let { activePanel } = this.state 54 | return ( 55 | 61 |
62 | 72 | 82 |
83 |
84 | {activePanel === 'posts' && 85 | Object.values(articles) 86 | .filter( 87 | article => 88 | article.categoryId === activeCategoryId 89 | ) 90 | .map(article => ( 91 |
96 | ))} 97 | {activePanel === 'categories' && 98 | Object.values(categories).map(category => ( 99 | 100 | ))} 101 |
102 | 103 |
104 | ) 105 | } 106 | } 107 | 108 | function mapStateToProps(state) { 109 | return { 110 | location: getLocation(state), 111 | categories: state.category.categories, 112 | articles: state.article.articles 113 | } 114 | } 115 | 116 | function mapDispatchToProps(dispatch) { 117 | return Object.assign(bindActionCreators({}, dispatch), { dispatch }) 118 | } 119 | 120 | export default connect( 121 | mapStateToProps, 122 | mapDispatchToProps 123 | )(CategoryPage) 124 | 125 | let styles = StyleSheet.create({ 126 | subNav: { 127 | borderBottom: 'solid 1px #f5f5f5', 128 | lineHeight: '3rem' 129 | }, 130 | button: { 131 | borderBottom: 'solid 2px #000', 132 | display: 'inline-block', 133 | fontWeight: 700, 134 | marginRight: '10px', 135 | fontSize: '1.2rem', 136 | fontFamily: '"Source Sans Pro",Helvetica,Arial,sans-serif', 137 | textTransform: 'uppercase', 138 | cursor: 'pointer', 139 | backgroundColor: 'transparent', 140 | outline: 0, 141 | lineHeight: '30px', 142 | letterSpacing: '2pt', 143 | textDecoration: 'none', 144 | color: '#b6b6b6', 145 | border: 'none', 146 | ':active': { 147 | borderBottom: 'solid 2px #000', 148 | color: '#333337' 149 | }, 150 | ':hover': { 151 | borderBottom: 'solid 2px #000', 152 | color: '#333337' 153 | }, 154 | ':focus': { 155 | outline: 0 156 | } 157 | }, 158 | buttonActive: { 159 | borderBottom: 'solid 2px #000', 160 | color: '#333337' 161 | }, 162 | list: { 163 | animation: 'fadein 2s', 164 | display: 'flex', 165 | flexWrap: 'wrap', 166 | width: '100%', 167 | justifyContent: 'space-between' 168 | } 169 | }) 170 | -------------------------------------------------------------------------------- /src/routes/contact.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { StyleSheet, css } from 'aphrodite' 3 | import { connect } from 'react-redux' 4 | import { bindActionCreators } from 'redux' 5 | import { Link } from 'react-router-dom' 6 | 7 | import { getLocation } from 'route/selectors' 8 | import BaseInput from 'components/form/baseInput' 9 | import Page from 'components/layout/page' 10 | import input from 'styles/input' 11 | import buttons from 'styles/buttons' 12 | import Mail from 'lib/mail' 13 | import conf from 'conf' 14 | 15 | class Contact extends Component { 16 | constructor(props) { 17 | super(props) 18 | this.state = { 19 | values: { 20 | name: { 21 | value: '', 22 | error: false, 23 | required: true 24 | }, 25 | email: { 26 | value: '', 27 | error: false, 28 | required: true 29 | }, 30 | company: { 31 | value: '', 32 | error: false, 33 | required: false 34 | }, 35 | phone: { 36 | value: '', 37 | error: false, 38 | required: false 39 | }, 40 | message: { 41 | value: '', 42 | error: false, 43 | required: true 44 | } 45 | }, 46 | valid: false, 47 | sent: false 48 | } 49 | this.validateEmail = /^([a-zA-Z0-9_\.\-])+\@(([a-zA-Z0-9\-])+\.)+([a-zA-Z0-9]{2,4})+$/ 50 | this.updateFormProperty = this.updateFormProperty.bind(this) 51 | this.sendMessage = this.sendMessage.bind(this) 52 | } 53 | 54 | updateFormProperty(property, value) { 55 | let { values } = this.state 56 | let element = values[property] 57 | let { error, required } = element 58 | if (required && value.length < 4) { 59 | error = '4 characters minimum.' 60 | } else if (property === 'email' && !this.validateEmail.test(value)) { 61 | error = 'Enter valid email.' 62 | } else { 63 | error = false 64 | } 65 | values = { 66 | ...values, 67 | [property]: { 68 | required, 69 | value, 70 | error 71 | } 72 | } 73 | let valid = Object.values(values).every( 74 | item => !item.error && (!item.required || item.value.length > 3) 75 | ) 76 | this.setState({ 77 | values, 78 | valid 79 | }) 80 | } 81 | 82 | sendMessage(e) { 83 | e.preventDefault() 84 | e.stopPropagation() 85 | let { values } = this.state 86 | this.setState( 87 | { 88 | sent: true 89 | }, 90 | () => { 91 | let formatted = {} 92 | Object.keys(values).forEach(key => { 93 | formatted[key] = values[key]['value'] 94 | }) 95 | 96 | Mail.send(formatted) 97 | } 98 | ) 99 | } 100 | 101 | render() { 102 | let { name, email, company, phone, message } = this.state.values 103 | let { valid, sent } = this.state 104 | 105 | return ( 106 | 116 |

Send me an email

117 |
118 |
119 | 125 | {name.error && ( 126 | 127 |    {name.error} 128 | 129 | )} 130 | 140 |
141 |
142 | 148 | {email.error && ( 149 | 150 |    {email.error} 151 | 152 | )} 153 | 164 |
165 |
166 | 169 | {company.error && ( 170 | 171 |    {company.error} 172 | 173 | )} 174 | 184 |
185 |
186 | 189 | {phone.error && ( 190 | 191 |    {phone.error} 192 | 193 | )} 194 | 204 |
205 |
206 | 212 | {message.error && ( 213 | 214 |    {message.error} 215 | 216 | )} 217 | 228 |
229 | 230 | {valid && !sent ? ( 231 | 237 | ) : ( 238 | 247 | )} 248 |
249 |
250 | 251 | About 252 | 253 |
254 |
255 | ) 256 | } 257 | } 258 | 259 | function mapStateToProps(state) { 260 | return { 261 | location: getLocation(state) 262 | } 263 | } 264 | 265 | function mapDispatchToProps(dispatch) { 266 | return Object.assign(bindActionCreators({}, dispatch), { dispatch }) 267 | } 268 | 269 | export default connect( 270 | mapStateToProps, 271 | mapDispatchToProps 272 | )(Contact) 273 | 274 | let styles = StyleSheet.create({ 275 | title: { 276 | fontSize: '2.6rem', 277 | marginTop: '20px', 278 | fontFamily: 'inherit', 279 | fontWeight: 500, 280 | lineHeight: '1.1', 281 | color: 'inherit', 282 | marginBottom: '10px' 283 | }, 284 | 285 | label: { 286 | fontSize: '2rem', 287 | fontFamily: '"Source Sans Pro",Helvetica,Arial,sans-serif', 288 | fontWeight: 700, 289 | margin: '15px 0 0' 290 | }, 291 | button: { 292 | fontFamily: '"Source Sans Pro",Helvetica,Arial,sans-serif' 293 | }, 294 | error: { 295 | color: 'red' 296 | }, 297 | required: { 298 | ':after': { 299 | color: 'red', 300 | content: '" *"' 301 | } 302 | }, 303 | footer: { 304 | padding: '10px 0', 305 | fontSize: '1.4rem', 306 | letterSpacing: '1px', 307 | fontWeight: 700, 308 | fontFamily: '"Source Sans Pro",Helvetica,Arial,sans-serif', 309 | textTransform: 'uppercase' 310 | }, 311 | contact: { 312 | textDecoration: 'none', 313 | backgroundColor: 'transparent', 314 | color: '#999', 315 | borderBottom: 'none', 316 | fontSize: '1.4rem', 317 | ':hover': { 318 | textDecoration: 'none', 319 | backgroundColor: 'transparent', 320 | color: '#333', 321 | outline: 0, 322 | transition: 'all .4s', 323 | borderBottom: 'none' 324 | } 325 | }, 326 | form: { 327 | marginBottom: '5rem' 328 | } 329 | }) 330 | -------------------------------------------------------------------------------- /src/routes/home.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { StyleSheet, css } from 'aphrodite' 3 | import { connect } from 'react-redux' 4 | import { bindActionCreators } from 'redux' 5 | import { Link } from 'react-router-dom' 6 | 7 | import { getLocation } from 'route/selectors' 8 | 9 | import Page from 'components/layout/page' 10 | import DisqusCount from 'components/disqus/disqusCount' 11 | import Article from 'components/blocks/article' 12 | import Category from 'components/blocks/category' 13 | import blocks from 'styles/blocks' 14 | import conf from 'conf' 15 | 16 | class Home extends Component { 17 | constructor() { 18 | super() 19 | this.setActivePanel = this.setActivePanel.bind(this) 20 | this.state = { 21 | activePanel: 'posts' 22 | } 23 | } 24 | 25 | setActivePanel() { 26 | let panel = event.target.dataset.panel 27 | this.setState({ 28 | activePanel: panel 29 | }) 30 | } 31 | 32 | render() { 33 | let { location, categories, articles } = this.props 34 | let { activePanel } = this.state 35 | return ( 36 | 47 |
48 | 58 | 68 |
69 |
75 | {Object.values(articles).map(article => ( 76 |
81 | ))} 82 |
83 |
91 | {Object.values(categories).map(category => ( 92 | 93 | ))} 94 |
95 | 96 |
97 | ) 98 | } 99 | } 100 | 101 | function mapStateToProps(state) { 102 | return { 103 | location: getLocation(state), 104 | categories: state.category.categories, 105 | articles: state.article.articles 106 | } 107 | } 108 | 109 | function mapDispatchToProps(dispatch) { 110 | return Object.assign(bindActionCreators({}, dispatch), { dispatch }) 111 | } 112 | 113 | export default connect( 114 | mapStateToProps, 115 | mapDispatchToProps 116 | )(Home) 117 | 118 | let styles = StyleSheet.create({ 119 | subNav: { 120 | borderBottom: 'solid 1px #f5f5f5', 121 | lineHeight: '3rem' 122 | }, 123 | button: { 124 | borderBottom: 'solid 2px #000', 125 | display: 'inline-block', 126 | fontWeight: 700, 127 | marginRight: '10px', 128 | fontSize: '1.2rem', 129 | fontFamily: '"Source Sans Pro",Helvetica,Arial,sans-serif', 130 | textTransform: 'uppercase', 131 | cursor: 'pointer', 132 | backgroundColor: 'transparent', 133 | outline: 0, 134 | lineHeight: '30px', 135 | letterSpacing: '2pt', 136 | textDecoration: 'none', 137 | color: '#b6b6b6', 138 | border: 'none', 139 | ':active': { 140 | borderBottom: 'solid 2px #000', 141 | color: '#333337' 142 | }, 143 | ':hover': { 144 | borderBottom: 'solid 2px #000', 145 | color: '#333337' 146 | }, 147 | ':focus': { 148 | outline: 0 149 | } 150 | }, 151 | buttonActive: { 152 | borderBottom: 'solid 2px #000', 153 | color: '#333337' 154 | }, 155 | list: { 156 | animation: 'fadein 2s', 157 | display: 'flex', 158 | flexWrap: 'wrap', 159 | width: '100%', 160 | justifyContent: 'space-between' 161 | }, 162 | 163 | hide: { 164 | position: 'absolute', 165 | top: '-9999px', 166 | left: '-9999px', 167 | display: 'none' 168 | } 169 | }) 170 | -------------------------------------------------------------------------------- /src/routes/routes.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import Article from './article' 4 | import Category from './category' 5 | 6 | import About from './about' 7 | import Contact from './contact' 8 | import Home from './home' 9 | 10 | import NoMatch from 'main/containers/noMatch' 11 | 12 | export default [ 13 | { 14 | path: '/', 15 | component: Home, 16 | exact: true 17 | }, 18 | { 19 | path: '/articles/:articleId/:slug', 20 | component: Article 21 | }, 22 | { 23 | path: '/categories/:categoryId', 24 | component: Category 25 | }, 26 | { 27 | path: '/about', 28 | component: About, 29 | exact: true 30 | }, 31 | { 32 | path: '/contact', 33 | component: Contact, 34 | exact: true 35 | }, 36 | { 37 | path: '/*', 38 | component: NoMatch 39 | } 40 | ] 41 | -------------------------------------------------------------------------------- /src/styles/blocks.js: -------------------------------------------------------------------------------- 1 | import { StyleSheet } from 'aphrodite' 2 | 3 | const opacityKeyframes = { 4 | from: { 5 | opacity: 0 6 | }, 7 | 8 | to: { 9 | opacity: 1 10 | } 11 | } 12 | let blocks = StyleSheet.create({ 13 | outline: { 14 | border: '0.1rem solid transparent' 15 | }, 16 | image: { 17 | backgroundSize: 'cover', 18 | backgroundRepeat: 'no-repeat', 19 | backgroundPosition: 'center center' 20 | }, 21 | center: { 22 | width: '30%', 23 | margin: 'auto' 24 | }, 25 | container: { 26 | padding: '0.5rem' 27 | }, 28 | col: { 29 | flexGrow: 1 30 | }, 31 | row: { 32 | display: 'flex' 33 | }, 34 | wrapper: { 35 | position: 'relative', 36 | top: 0, 37 | bottom: 0, 38 | left: 0, 39 | right: 0, 40 | width: '100%', 41 | height: '100%', 42 | margin: 0, 43 | padding: 0, 44 | overflowX: 'hidden', 45 | maxWidth: '100%' 46 | }, 47 | block: { 48 | background: '#fff', 49 | padding: 10, 50 | borderRadius: 4, 51 | position: 'relative', 52 | cursor: 'default' 53 | }, 54 | fadein: { 55 | animationName: [opacityKeyframes], 56 | animationDuration: '1s, 1s', 57 | animationIterationCount: 1 58 | } 59 | }) 60 | 61 | export default blocks 62 | -------------------------------------------------------------------------------- /src/styles/buttons.js: -------------------------------------------------------------------------------- 1 | import { StyleSheet } from 'aphrodite' 2 | 3 | let buttons = StyleSheet.create({ 4 | base: { 5 | display: 'inline-block', 6 | fontWeight: '400', 7 | lineHeight: '1.25rem', 8 | textAlign: 'center', 9 | whiteSpace: 'nowrap', 10 | verticalAlign: 'middle', 11 | userSelect: 'none', 12 | border: '0.1rem solid transparent', 13 | padding: '1rem 1rem', 14 | fontSize: '1.6rem', 15 | borderRadius: '.5rem', 16 | transition: 'all .2s ease-in-out', 17 | cursor: 'pointer', 18 | textDecoration: 'none', 19 | color: '#fff', 20 | backgroundColor: '#0275d8', 21 | borderColor: '#0275d8', 22 | ':hover': { 23 | textDecoration: 'none', 24 | backgroundColor: '#025aa5', 25 | borderColor: '#01549b', 26 | color: '#fff' 27 | } 28 | }, 29 | large: { 30 | padding: '1.5rem', 31 | fontSize: '2rem', 32 | borderRadius: '.5rem' 33 | }, 34 | block: { 35 | display: 'block', 36 | width: '100%' 37 | }, 38 | disabled: { 39 | pointerEvents: 'none', 40 | backgroundColor: '#85c6f2', 41 | borderColor: '#85c6f2', 42 | ':hover': { 43 | backgroundColor: '#85c6f2', 44 | borderColor: '#85c6f2', 45 | cursor: 'not-allowed' 46 | } 47 | } 48 | }) 49 | 50 | export default buttons 51 | -------------------------------------------------------------------------------- /src/styles/input.js: -------------------------------------------------------------------------------- 1 | import { StyleSheet } from 'aphrodite' 2 | 3 | let input = StyleSheet.create({ 4 | base: { 5 | display: 'block', 6 | width: '100%', 7 | padding: '.5rem .75rem', 8 | fontSize: '1.6rem', 9 | lineHeight: '1.25', 10 | color: '#464a4c', 11 | backgroundColor: '#fff', 12 | backgroundImage: 'none', 13 | backgroundClip: 'padding-box', 14 | border: '.1rem solid rgba(0,0,0,.15)', 15 | borderRadius: '.25rem', 16 | transition: 17 | 'border-color ease-in-out .15s,box-shadow ease-in-out .15s,-webkit-box-shadow ease-in-out .15s', 18 | marginBottom: 10, 19 | '::placeholder': { 20 | maxWidth: '92%', 21 | overflowX: 'hidden', 22 | textOverflow: 'ellipsis' 23 | } 24 | }, 25 | error: { 26 | border: '.1rem solid red', 27 | '::placeholder': { 28 | maxWidth: '92%', 29 | overflowX: 'hidden', 30 | textOverflow: 'ellipsis', 31 | color: 'red' 32 | } 33 | } 34 | }) 35 | 36 | export default input 37 | -------------------------------------------------------------------------------- /src/utils/capitalize.js: -------------------------------------------------------------------------------- 1 | export default function capitalize(string) { 2 | if (!string) { 3 | return '' 4 | } 5 | return string.charAt(0).toUpperCase() + string.slice(1) 6 | } 7 | -------------------------------------------------------------------------------- /src/utils/jsonpCall.js: -------------------------------------------------------------------------------- 1 | export default function jsonpCall(url, callback) { 2 | let handleJsonpResults = 3 | 'handleJsonpResults_' + 4 | Date.now() + 5 | '_' + 6 | parseInt(Math.random() * 10000) 7 | 8 | window[handleJsonpResults] = function(json) { 9 | callback(json) 10 | 11 | const script = document.getElementById(handleJsonpResults) 12 | document.getElementsByTagName('head')[0].removeChild(script) 13 | delete window[handleJsonpResults] 14 | } 15 | 16 | let serviceUrl = `${url}${ 17 | url.indexOf('?') > -1 ? '&' : '?' 18 | }callback=${handleJsonpResults}` 19 | //console.log('service', serviceUrl) 20 | const jsonpScript = document.createElement('script') 21 | jsonpScript.setAttribute('src', serviceUrl) 22 | jsonpScript.id = handleJsonpResults 23 | document.getElementsByTagName('head')[0].appendChild(jsonpScript) 24 | } 25 | -------------------------------------------------------------------------------- /src/utils/uuid.js: -------------------------------------------------------------------------------- 1 | let lut = [] 2 | for (let i = 0; i < 256; i++) { 3 | lut[i] = (i < 16 ? '0' : '') + i.toString(16) 4 | } 5 | 6 | function uuid() { 7 | let d0 = (Math.random() * 0xffffffff) | 0 8 | let d1 = (Math.random() * 0xffffffff) | 0 9 | let d2 = (Math.random() * 0xffffffff) | 0 10 | let d3 = (Math.random() * 0xffffffff) | 0 11 | return ( 12 | lut[d0 & 0xff] + 13 | lut[(d0 >> 8) & 0xff] + 14 | lut[(d0 >> 16) & 0xff] + 15 | lut[(d0 >> 24) & 0xff] + 16 | '-' + 17 | lut[d1 & 0xff] + 18 | lut[(d1 >> 8) & 0xff] + 19 | '-' + 20 | lut[((d1 >> 16) & 0x0f) | 0x40] + 21 | lut[(d1 >> 24) & 0xff] + 22 | '-' + 23 | lut[(d2 & 0x3f) | 0x80] + 24 | lut[(d2 >> 8) & 0xff] + 25 | '-' + 26 | lut[(d2 >> 16) & 0xff] + 27 | lut[(d2 >> 24) & 0xff] + 28 | lut[d3 & 0xff] + 29 | lut[(d3 >> 8) & 0xff] + 30 | lut[(d3 >> 16) & 0xff] + 31 | lut[(d3 >> 24) & 0xff] 32 | ) 33 | } 34 | 35 | export default uuid 36 | --------------------------------------------------------------------------------