├── .babelrc ├── .editorconfig ├── .eslintrc ├── .gitignore ├── .prettierignore ├── .prettierrc ├── .stylelintrc ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── config ├── babel.legacy.config.js ├── webpack.dev.config.js └── webpack.prod.config.js ├── package-lock.json ├── package.json ├── plato-node.js ├── plato ├── @types │ ├── global.d.ts │ ├── index.d.ts │ └── route.d.ts ├── commands │ ├── build-critical.ts │ ├── build-javascript.ts │ ├── build.ts │ ├── develop-watchers.ts │ └── develop.ts ├── core │ ├── buildHTML.ts │ ├── createPages.ts │ ├── saveData.ts │ └── updateRoutes.ts ├── index.ts ├── tsconfig.json └── utils │ ├── reporter.ts │ └── routes.ts ├── postcss.config.cjs ├── reports ├── plain-report.txt └── stats.json ├── shared └── templates │ ├── about.art │ ├── filters │ └── index.js │ ├── homepage.art │ ├── layout.art │ ├── notfound.art │ ├── partials │ ├── footer.art │ ├── header.art │ ├── responsive-image.art │ └── srcset.art │ └── svgs │ └── instagram.art ├── site-config.js ├── size-plugin.json └── src ├── .htaccess ├── assets ├── favicon │ ├── android-chrome-192x192.png │ ├── android-chrome-256x256.png │ ├── apple-touch-icon.png │ ├── browserconfig.xml │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ ├── favicon.ico │ ├── mstile-150x150.png │ ├── safari-pinned-tab.svg │ └── site.webmanifest ├── fonts │ ├── UntitledSans-Regular.woff │ └── UntitledSans-Regular.woff2 ├── images │ ├── image.gif │ └── test.jpg └── svgs │ ├── instagram.svg │ └── twitter.svg ├── css ├── app.scss ├── base │ ├── _base.scss │ ├── _typography.scss │ ├── _ui-defs.scss │ ├── _ui-resets.scss │ ├── mixins │ │ ├── _aspect_ratio.scss │ │ ├── _font_style.scss │ │ ├── _mixins.scss │ │ └── _responsive_font.scss │ ├── utilities │ │ ├── _u-align.scss │ │ ├── _u-grab.scss │ │ └── _u-overlay.scss │ └── vars │ │ ├── _colors.scss │ │ ├── _fonts.scss │ │ ├── _icons.scss │ │ ├── _media-queries.scss │ │ ├── _sizes.scss │ │ └── _transitions.scss └── components │ ├── _footer.scss │ ├── _header.scss │ └── _page.scss ├── data ├── 404.json ├── about.json └── index.json └── scripts ├── abstract ├── base.js ├── component.js └── page.js ├── app ├── App.js ├── Cache.js ├── actions.js ├── constants.js ├── reducers.js └── selectors.js ├── components └── header │ ├── Header.js │ ├── actions.js │ ├── constants.js │ └── reducers.js ├── constants ├── config.js └── langs.js ├── index.js ├── layout ├── Layout.js ├── Prefetch.js ├── Prevent.js ├── actions.js ├── constants.js └── reducers.js ├── pages ├── about │ └── About.js ├── homepage │ └── Homepage.js └── notfound │ └── Notfound.js ├── reducers └── index.js ├── router.js ├── store ├── globalStore.js ├── index.js ├── store.js └── storeWatcher.js └── utils ├── cleanURL.js ├── is.js ├── loadImages.js ├── loadScript.js ├── misc.js ├── offset.js ├── scrollPrevent.js ├── uniqueId.js └── url.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/env", 5 | { 6 | "targets": { 7 | // https://jamie.build/last-2-versions 8 | // "browsers": [">0.25%", "not op_mini all", "not ie 11"] 9 | // https://web.dev/serve-modern-code-to-modern-browsers/ 10 | // https://philipwalton.com/articles/deploying-es2015-code-in-production-today/ 11 | "esmodules": true 12 | }, 13 | // "debug": true, 14 | "useBuiltIns": "usage", 15 | "bugfixes": true, 16 | "corejs": 3 17 | } 18 | ] 19 | ], 20 | "plugins": ["@babel/plugin-transform-runtime", "minify-dead-code-elimination"] 21 | } 22 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_size = 2 7 | indent_style = tab 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["google", "prettier"], 3 | "parser": "@babel/eslint-parser", 4 | "rules": { 5 | "global-require": 0, 6 | "curly": [2, "multi-line"], 7 | "no-mixed-spaces-and-tabs": [2, "smart-tabs"], 8 | "key-spacing": 0, 9 | "indent": [2, "tab", { "SwitchCase": 1 }], 10 | "strict": [2, "global"], 11 | "quotes": [2, "single"], 12 | "quote-props": [2, "as-needed"], 13 | "max-len": [2, 200], 14 | "no-use-before-define": [0], 15 | "no-negated-condition": [0], 16 | "no-lonely-if": [0], 17 | "no-debugger": [0], 18 | "padded-blocks": [0, "always"], 19 | "spaced-comment": [1], 20 | "space-before-blocks": [0], 21 | "space-in-parens": [0], 22 | "require-jsdoc": [0], 23 | "no-undef": [0], 24 | "no-tabs": [0], 25 | "valid-jsdoc": [1] 26 | }, 27 | "env": { 28 | "es6": true, 29 | "node": true, 30 | "browser": true 31 | }, 32 | "parserOptions": { 33 | "requireConfigFile": false, 34 | "babelOptions": { 35 | "babelrc": false, 36 | "configFile": false, 37 | // your babel options 38 | "presets": ["@babel/preset-env"] 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # OS 2 | .DS_Store 3 | *.pyc 4 | *.swp 5 | *~ 6 | *.zip 7 | 8 | # installation 9 | node_modules 10 | vendor 11 | 12 | npm-debug.log.* 13 | *.map 14 | *.log 15 | ======= 16 | node_modules 17 | .DS_Store 18 | npm-debug.log.* 19 | 20 | build 21 | public 22 | public/assets/js 23 | public/assets/css 24 | plato-dist 25 | .plato 26 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | src/scripts/vendors/ 2 | node_modules -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "bracketSpacing": true, 3 | "semi": true, 4 | "singleQuote": true, 5 | "printWidth": 120, 6 | "trailingComma": "es5", 7 | "useTabs": false 8 | } 9 | -------------------------------------------------------------------------------- /.stylelintrc: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "at-rule-empty-line-before": [ "always", { 4 | except: [ 5 | "blockless-after-same-name-blockless", 6 | "first-nested", 7 | ], 8 | ignore: ["after-comment"], 9 | } ], 10 | "at-rule-name-case": "lower", 11 | "at-rule-name-space-after": "always-single-line", 12 | "at-rule-semicolon-newline-after": "always", 13 | "block-closing-brace-empty-line-before": "never", 14 | "block-closing-brace-newline-after": "always", 15 | "block-closing-brace-newline-before": "always-multi-line", 16 | "block-closing-brace-space-before": "always-single-line", 17 | "block-no-empty": true, 18 | "block-opening-brace-newline-after": "always-multi-line", 19 | "block-opening-brace-space-after": "always-single-line", 20 | "block-opening-brace-space-before": "always", 21 | "color-hex-case": "lower", 22 | "color-hex-length": "short", 23 | "color-no-invalid-hex": true, 24 | "comment-empty-line-before": [ "always", { 25 | except: ["first-nested"], 26 | ignore: ["stylelint-commands"], 27 | } ], 28 | "comment-no-empty": true, 29 | "comment-whitespace-inside": "always", 30 | "custom-property-empty-line-before": [ "always", { 31 | except: [ 32 | "after-custom-property", 33 | "first-nested", 34 | ], 35 | ignore: [ 36 | "after-comment", 37 | "inside-single-line-block", 38 | ], 39 | } ], 40 | "declaration-bang-space-after": "never", 41 | "declaration-bang-space-before": "always", 42 | "declaration-block-no-duplicate-properties": [ true, { 43 | ignore: ["consecutive-duplicates-with-different-values"], 44 | } ], 45 | "declaration-block-no-redundant-longhand-properties": true, 46 | "declaration-block-no-shorthand-property-overrides": true, 47 | "declaration-block-semicolon-newline-after": "always-multi-line", 48 | "declaration-block-semicolon-space-after": "always-single-line", 49 | "declaration-block-semicolon-space-before": "never", 50 | "declaration-block-single-line-max-declarations": 1, 51 | "declaration-block-trailing-semicolon": "always", 52 | "declaration-colon-newline-after": "always-multi-line", 53 | "declaration-colon-space-after": "always-single-line", 54 | "declaration-colon-space-before": "never", 55 | "declaration-empty-line-before": [ "always", { 56 | except: [ 57 | "after-declaration", 58 | "first-nested", 59 | ], 60 | ignore: [ 61 | "after-comment", 62 | "inside-single-line-block", 63 | ], 64 | } ], 65 | "font-family-no-duplicate-names": true, 66 | "function-calc-no-unspaced-operator": true, 67 | "function-comma-newline-after": "always-multi-line", 68 | "function-comma-space-after": "always-single-line", 69 | "function-comma-space-before": "never", 70 | "function-linear-gradient-no-nonstandard-direction": true, 71 | "function-max-empty-lines": 0, 72 | "function-name-case": "lower", 73 | "function-parentheses-newline-inside": "always-multi-line", 74 | "function-parentheses-space-inside": "never-single-line", 75 | "function-whitespace-after": "always", 76 | "indentation": 2, 77 | "keyframe-declaration-no-important": true, 78 | "length-zero-no-unit": true, 79 | "max-empty-lines": 1, 80 | "media-feature-colon-space-after": "always", 81 | "media-feature-colon-space-before": "never", 82 | "media-feature-name-case": "lower", 83 | "media-feature-name-no-unknown": true, 84 | "media-feature-parentheses-space-inside": "never", 85 | "media-feature-range-operator-space-after": "always", 86 | "media-feature-range-operator-space-before": "always", 87 | "media-query-list-comma-newline-after": "always-multi-line", 88 | "media-query-list-comma-space-after": "always-single-line", 89 | "media-query-list-comma-space-before": "never", 90 | "no-empty-source": true, 91 | "no-eol-whitespace": true, 92 | "no-extra-semicolons": true, 93 | "no-invalid-double-slash-comments": true, 94 | "no-missing-end-of-source-newline": true, 95 | "number-leading-zero": "always", 96 | "number-no-trailing-zeros": true, 97 | "property-case": "lower", 98 | "property-no-unknown": true, 99 | "rule-empty-line-before": [ "always-multi-line", { 100 | except: ["first-nested"], 101 | ignore: ["after-comment"], 102 | } ], 103 | "selector-attribute-brackets-space-inside": "never", 104 | "selector-attribute-operator-space-after": "never", 105 | "selector-attribute-operator-space-before": "never", 106 | "selector-combinator-space-after": "always", 107 | "selector-combinator-space-before": "always", 108 | "selector-descendant-combinator-no-non-space": true, 109 | "selector-list-comma-newline-after": "always", 110 | "selector-list-comma-space-before": "never", 111 | "selector-max-empty-lines": 0, 112 | "selector-pseudo-class-case": "lower", 113 | "selector-pseudo-class-no-unknown": true, 114 | "selector-pseudo-class-parentheses-space-inside": "never", 115 | "selector-pseudo-element-case": "lower", 116 | "selector-pseudo-element-colon-notation": "double", 117 | "selector-pseudo-element-no-unknown": true, 118 | "selector-type-case": "lower", 119 | "selector-type-no-unknown": true, 120 | "shorthand-property-no-redundant-values": true, 121 | "string-no-newline": true, 122 | "unit-case": "lower", 123 | "unit-no-unknown": true, 124 | "value-list-comma-newline-after": "always-multi-line", 125 | "value-list-comma-space-after": "always-single-line", 126 | "value-list-comma-space-before": "never", 127 | "value-list-max-empty-lines": 0, 128 | }, 129 | } 130 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true, 3 | "javascript.format.enable": true, 4 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Tim ROUSSILHE 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. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Plato: Starter 2 | 3 | - **Creator:** Timothee Roussilhe 4 | - **Twitter:** [@timroussilhe](https://twitter.com/TimRoussilhe) 5 | 6 | ![](https://media1.giphy.com/media/eLudircQfgGEU/giphy.gif?cid=3640f6095c0e8e92753069696f5d90c5) 7 | 8 | --- 9 | 10 | ## CES Naming Convention 11 | 12 | Component classes should be named following a CES naming convention: component\_\_element--state. 13 | 14 | - Component: A standalone entity that is meaningful on its own. 15 | - Element: A part of the component that we want to style. 16 | - State: The state of the element. 17 | - Should NOT be modified by javascript once instantiated. 18 | - If a state IS modified by javascript, it doesn't need to follow CES (ex. toggling `is-active`) 19 | - Should be positive (ex. `--has-wrapping` instead of `--without-wrapping`) 20 | Unlike BEM, `__element` should be at most one level below the component. This avoids deep nesting but keeps an important level of specificity. 21 | It's fine to not use the top-level component name if we are styling a nuclear component by itself like a `button`. Specific Component scope className would then be used to add more style that's needed in the context of the parent component. 22 | For utility classes that embody some logic, we use a simple pattern of the utility description separated by a dash, no need to always attach this to the component as a modifier if it's something global: `justify-start` or `font-style-title`. 23 | Example: 24 | 25 | ``` 26 |
27 |
28 | 29 |
30 |
31 |

ChitChat

32 |

You have a new message!

33 |
34 |
35 | ``` 36 | 37 | ## Component State 38 | 39 | Each state comes with a simple local state management. 40 | The state is not attached to any update or render methods but you can pass a callback method when using `setState`. 41 | 42 | ``` 43 | // Declare initial states in constructor 44 | this.state = { 45 | canUpdate: false, 46 | isInit: false, 47 | isAnimating: false, 48 | isShown: false, 49 | } 50 | 51 | // Update State 52 | this.setState({ isAnimating: false}) 53 | 54 | // you can also pass a callback or render the component 55 | this.setState({ isAnimating: true }, () => this.onIsAnimatingUpdate())) 56 | 57 | // when you call setState, after that the state been 58 | 59 | // Read the state 60 | if ( this.state.canUpdate ) this.onUpdate() 61 | 62 | ``` 63 | -------------------------------------------------------------------------------- /config/babel.legacy.config.js: -------------------------------------------------------------------------------- 1 | export const babelLegacyLoaderRules = { 2 | test: /\.js$/, 3 | exclude: /node_modules/, 4 | 5 | loader: 'babel-loader', 6 | options: { 7 | presets: [ 8 | [ 9 | '@babel/preset-env', 10 | { 11 | useBuiltIns: 'usage', 12 | targets: { 13 | esmodules: false, 14 | }, 15 | corejs: 3, 16 | }, 17 | ], 18 | ], 19 | }, 20 | }; 21 | -------------------------------------------------------------------------------- /config/webpack.dev.config.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import { fileURLToPath } from 'url'; 3 | import webpack from 'webpack'; 4 | import SizePlugin from 'size-plugin'; 5 | 6 | const __filename = fileURLToPath(import.meta.url); 7 | const __dirname = path.dirname(__filename); 8 | 9 | const appEntryPoint = path.join(__dirname, '../src/scripts/index.js'); 10 | const outputPath = path.join(__dirname, '../public/assets/js/'); 11 | const filename = 'bundle.js'; 12 | const entryPoints = appEntryPoint; 13 | 14 | export default { 15 | mode: 'development', 16 | entry: entryPoints, 17 | 18 | // if multiple outputs, use [name] and it will use the name of the entry point, and loop through them 19 | output: { 20 | path: outputPath, 21 | filename: filename, 22 | publicPath: '/assets/js/', 23 | chunkFilename: '[name].bundle.js', 24 | }, 25 | 26 | optimization: { 27 | emitOnErrors: true, 28 | }, 29 | 30 | plugins: [new SizePlugin(), new webpack.HotModuleReplacementPlugin()], 31 | 32 | // i. e. through the resolve.alias option 33 | // will be included in the bundle, no need to add and load vendor 34 | resolve: { 35 | extensions: ['.js', '.json', '.art', '.html'], 36 | modules: ['src/scripts/', 'src/scripts/vendors/', '.plato/', 'shared/', 'public/assets/', 'node_modules'], 37 | }, 38 | 39 | module: { 40 | rules: [ 41 | // copy fonts for us, this is needed to avoid webpack renaing the font via css-loader 42 | // https://webpack.js.org/guides/asset-modules/ 43 | { 44 | test: /\.(woff(2)?|ttf|eot)$/, 45 | type: 'asset/resource', 46 | generator: { 47 | filename: './../fonts/[name][ext]', 48 | }, 49 | }, 50 | { 51 | test: /\.js$/, 52 | exclude: /node_modules/, 53 | use: { 54 | loader: 'babel-loader', 55 | }, 56 | }, 57 | { test: /\.art$/, use: 'art-template-loader' }, 58 | { 59 | test: /\.(scss|css)$/, 60 | use: [ 61 | { 62 | loader: 'style-loader', 63 | }, 64 | { 65 | loader: 'css-loader', 66 | }, 67 | { 68 | loader: 'postcss-loader', 69 | options: { 70 | postcssOptions: { 71 | ident: 'postcss', 72 | plugins: () => [require('autoprefixer')], 73 | }, 74 | }, 75 | }, 76 | { 77 | loader: 'sass-loader', 78 | options: { 79 | sourceMap: true, 80 | }, 81 | }, 82 | ], 83 | }, 84 | ], 85 | }, 86 | 87 | stats: { 88 | // Nice colored output 89 | colors: true, 90 | }, 91 | 92 | // Create Sourcemaps for the bundle 93 | devtool: 'eval-source-map', 94 | }; 95 | -------------------------------------------------------------------------------- /config/webpack.prod.config.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import { fileURLToPath } from 'url'; 3 | import webpack from 'webpack'; 4 | import TerserPlugin from 'terser-webpack-plugin'; 5 | import MiniCssExtractPlugin from 'mini-css-extract-plugin'; 6 | import CssMinimizerPlugin from 'css-minimizer-webpack-plugin'; 7 | 8 | import { WebpackManifestPlugin } from 'webpack-manifest-plugin'; 9 | 10 | import { WebpackBundleSizeAnalyzerPlugin } from 'webpack-bundle-size-analyzer'; 11 | import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer'; 12 | 13 | const __filename = fileURLToPath(import.meta.url); 14 | const __dirname = path.dirname(__filename); 15 | const appEntryPoint = path.join(__dirname, '../src/scripts/index.js'); 16 | const outputPath = path.join(__dirname, '../build/assets/'); 17 | const reportPath = path.join(__dirname, '../reports/plain-report.txt'); 18 | 19 | import { babelLegacyLoaderRules } from './babel.legacy.config.js'; 20 | const entryPoints = appEntryPoint; 21 | 22 | export default (env) => { 23 | const envPlugins = []; 24 | if (env && env.bundleSize) 25 | envPlugins.push( 26 | new BundleAnalyzerPlugin({ 27 | analyzerMode: 'static', 28 | generateStatsFile: 'true', 29 | }) 30 | ); 31 | 32 | if (env !== 'legacy') { 33 | envPlugins.push(new WebpackBundleSizeAnalyzerPlugin(reportPath)); 34 | envPlugins.push( 35 | new WebpackManifestPlugin({ 36 | publicPath: '/assets/', 37 | }) 38 | ); 39 | } 40 | 41 | return { 42 | mode: 'production', 43 | entry: entryPoints, 44 | 45 | // if multiple outputs, use [name] and it will use the name of the entry point, and loop through them 46 | output: { 47 | path: outputPath, 48 | filename: env !== 'legacy' ? 'js/[name].[contenthash].js' : 'js/legacy.js', 49 | chunkFilename: env !== 'legacy' ? 'js/[name].[contenthash].js' : 'js/[name].[contenthash]-legacy.js', 50 | publicPath: '/assets/', 51 | }, 52 | 53 | optimization: { 54 | emitOnErrors: false, 55 | concatenateModules: true, 56 | minimizer: [ 57 | new TerserPlugin({ 58 | terserOptions: { 59 | compress: { 60 | drop_console: true, 61 | dead_code: true, 62 | }, 63 | keep_classnames: false, 64 | keep_fnames: false, 65 | output: { 66 | comments: false, 67 | }, 68 | }, 69 | }), 70 | new CssMinimizerPlugin({ 71 | minimizerOptions: { 72 | preset: [ 73 | 'default', 74 | { 75 | discardComments: { removeAll: true }, 76 | }, 77 | ], 78 | }, 79 | }), 80 | ], 81 | moduleIds: 'deterministic', 82 | }, 83 | 84 | plugins: [ 85 | new MiniCssExtractPlugin({ 86 | // Options similar to the same options in webpackOptions.output 87 | // both options are optional 88 | filename: 'css/[name].[contenthash].css', 89 | }), 90 | new webpack.optimize.ModuleConcatenationPlugin(), 91 | ...envPlugins, 92 | ], 93 | resolve: { 94 | extensions: ['.js', '.json', '.art', '.html'], 95 | modules: ['src/scripts/', 'src/scripts/vendors/', 'shared/', 'public/assets/', 'node_modules'], 96 | }, 97 | 98 | module: { 99 | rules: [ 100 | // copy fonts for us, this is needed to avoid webpack renaing the font via css-loader 101 | // https://webpack.js.org/guides/asset-modules/ 102 | { 103 | test: /\.(woff(2)?|ttf|eot)$/, 104 | type: 'asset/resource', 105 | generator: { 106 | filename: './fonts/[name][ext]', 107 | }, 108 | }, 109 | ...(env !== 'legacy' 110 | ? [ 111 | { 112 | test: /\.js?$/, 113 | exclude: /node_modules/, 114 | use: 'babel-loader', 115 | }, 116 | ] 117 | : [babelLegacyLoaderRules]), 118 | { test: /\.art$/, use: 'art-template-loader' }, 119 | { 120 | test: /\.scss$/, 121 | use: [MiniCssExtractPlugin.loader, 'css-loader', 'postcss-loader', 'sass-loader'], 122 | }, 123 | ], 124 | }, 125 | 126 | stats: { 127 | // Nice colored output 128 | colors: true, 129 | }, 130 | 131 | devtool: false, 132 | }; 133 | }; 134 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "plato", 3 | "description": "Plato Boilerplate", 4 | "version": "0.0.2", 5 | "private": true, 6 | "author": "Tim Roussilhe", 7 | "contributors": [ 8 | "Tim Roussilhe" 9 | ], 10 | "type": "module", 11 | "scripts": { 12 | "start": "npm run build-plato && npm run dev", 13 | "clean-plato": "rm -rf plato-dist/* ", 14 | "dev-plato": "npm run clean-plato && tsc -w --project ./plato/tsconfig.json", 15 | "build-plato": "npm run clean-plato && tsc --project ./plato/tsconfig.json", 16 | "dev": "node plato-dist develop -v", 17 | "develop": "node plato-dist develop -v", 18 | "serve": "webpack-dev-server --config ./config/webpack.dev.config.js", 19 | "build": "npm run build-plato && node plato-dist build -v", 20 | "build-and-test": "node plato-dist build -v -o", 21 | "bundleSizeAnalyser": "webpack --config ./config/webpack.prod.config.js --env bundleSize", 22 | "prettier:check": "prettier --config ./.prettierrc --check src/**/**.{js,scss}", 23 | "prettier:write": "prettier --config ./.prettierrc --write src/**/**.{js,scss}" 24 | }, 25 | "devDependencies": { 26 | "@babel/core": "^7.18.10", 27 | "@babel/eslint-parser": "^7.18.9", 28 | "@babel/plugin-syntax-dynamic-import": "^7.2.0", 29 | "@babel/plugin-transform-runtime": "^7.18.10", 30 | "@babel/preset-env": "^7.18.10", 31 | "@types/fs-extra": "^9.0.13", 32 | "@types/node": "^18.7.2", 33 | "@types/node-fetch": "^2.6.2", 34 | "art-template-loader": "^1.4.3", 35 | "autoprefixer": "^10.4.8", 36 | "babel-loader": "^8.2.5", 37 | "babel-plugin-minify-dead-code-elimination": "^0.5.2", 38 | "boxen": "^4.1.0", 39 | "commander": "^2.19.0", 40 | "connect": "^3.7.0", 41 | "convert-hrtime": "^2.0.0", 42 | "critical": "^5.1.1", 43 | "css-loader": "^6.7.1", 44 | "css-minimizer-webpack-plugin": "^4.0.0", 45 | "eslint": "^8.21.0", 46 | "eslint-config-google": "^0.14.0", 47 | "fs-extra": "^10.1.0", 48 | "gsap": "^3.10.4", 49 | "html-minifier": "^3.5.21", 50 | "mini-css-extract-plugin": "^2.6.1", 51 | "node-fetch": "^2.3.0", 52 | "opn": "^5.4.0", 53 | "page": "^1.11.6", 54 | "postcss-loader": "^7.0.1", 55 | "prettier": "^2.7.1", 56 | "sass": "^1.54.0", 57 | "sass-loader": "^13.0.2", 58 | "serve-static": "^1.14.1", 59 | "size-plugin": "2.0.2", 60 | "style-loader": "^3.3.1", 61 | "terminal-link": "^2.0.0", 62 | "terser-webpack-plugin": "^5.3.3", 63 | "webpack": "^5.74.0", 64 | "webpack-bundle-analyzer": "^4.5.0", 65 | "webpack-bundle-size-analyzer": "^3.1.0", 66 | "webpack-cli": "^4.10.0", 67 | "webpack-dev-server": "^4.9.3", 68 | "webpack-manifest-plugin": "^5.0.0" 69 | }, 70 | "dependencies": { 71 | "@babel/runtime": "^7.18.9", 72 | "art-template": "^4.13.2", 73 | "core-js": "^3.24.1", 74 | "dotenv": "^16.3.1", 75 | "eslint-config-prettier": "^6.3.0", 76 | "fast-average-color-node": "^2.4.0", 77 | "html-minifier-terser": "^7.2.0" 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /plato-node.js: -------------------------------------------------------------------------------- 1 | import fetch from 'node-fetch'; 2 | 3 | export const getStaticPagesProps = async () => { 4 | const res = await fetch('https://pokeapi.co/api/v2/pokemon/?limit=6'); 5 | const posts = await res.json(); 6 | 7 | const pages = posts.results.map((pokemon, i) => { 8 | return { 9 | id: pokemon.name, 10 | url: pokemon.name, 11 | template: 'about', 12 | data: pokemon, 13 | }; 14 | }); 15 | 16 | return pages; 17 | }; 18 | 19 | export const createGlobalData = () => { 20 | return new Promise((resolve, reject) => { 21 | resolve({ 22 | globals: { 23 | array: [1, 2, 3, 4, 5], 24 | }, 25 | }); 26 | }); 27 | }; 28 | -------------------------------------------------------------------------------- /plato/@types/global.d.ts: -------------------------------------------------------------------------------- 1 | // global.d.ts 2 | declare module globalThis { 3 | var appRoot: string; 4 | var srcPath: string; 5 | var siteDir: string; 6 | var routeDest: string; 7 | } 8 | -------------------------------------------------------------------------------- /plato/@types/index.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'opn'; 2 | declare module 'critical'; 3 | declare module 'convert-hrtime'; 4 | declare module 'html-minifier'; 5 | -------------------------------------------------------------------------------- /plato/@types/route.d.ts: -------------------------------------------------------------------------------- 1 | export interface Route { 2 | url: string; 3 | id: string; 4 | template: string; 5 | json: string; 6 | // dynamic filename (useful for 404) 7 | fileName?: string; 8 | data?: object; 9 | // data source for static routes 10 | dataSource?: string; 11 | } 12 | 13 | export interface Routes { 14 | routes: Array; 15 | } 16 | -------------------------------------------------------------------------------- /plato/commands/build-critical.ts: -------------------------------------------------------------------------------- 1 | import { generate } from 'critical'; 2 | import { minify } from 'html-minifier-terser'; 3 | import fse from 'fs-extra'; 4 | 5 | export default (filename: string, dest: string) => { 6 | return new Promise(async (resolve, reject) => { 7 | let criticalHTML = ''; 8 | try { 9 | const { css, html, uncritical } = await generate({ 10 | inline: true, 11 | base: 'build/', 12 | src: filename, 13 | width: 1600, 14 | height: 900, 15 | }); 16 | 17 | criticalHTML = html; 18 | } catch (err) { 19 | console.log('caught error from criticalHTML: ', err); 20 | } 21 | 22 | // You now have critical-path CSS as well as the modified HTML. 23 | // Works with and without target specified. 24 | // You now have critical-path CSS 25 | // Works with and without dest specified 26 | minify(criticalHTML, { 27 | removeComments: true, 28 | // collapseWhitespace: true, 29 | conservativeCollapse: true, 30 | }) 31 | .then((html) => { 32 | fse.writeFileSync(dest, html, { 33 | encoding: 'utf8', 34 | flag: 'w', 35 | }); 36 | resolve(); 37 | }) 38 | .catch((err) => { 39 | // sometime the render of critical will come out to early / truncated and will error out, 40 | // so in this case we want to just write the original file 41 | console.log('skip write FILE BUG', err); 42 | resolve(); 43 | // reject(err); 44 | }); 45 | }); 46 | }; 47 | -------------------------------------------------------------------------------- /plato/commands/build-javascript.ts: -------------------------------------------------------------------------------- 1 | import webpack from 'webpack'; 2 | import webpackConfig from '../../config/webpack.prod.config.js'; 3 | 4 | export default async (env: string) => { 5 | return new Promise((resolve, reject) => { 6 | webpack(webpackConfig(env)).run((err, stats) => { 7 | if (err) { 8 | console.error(err.stack || err); 9 | reject(err); 10 | return; 11 | } 12 | 13 | if (stats) { 14 | const info = stats.toJson(); 15 | 16 | if (stats.hasErrors()) { 17 | console.error(info.errors); 18 | reject(err); 19 | return; 20 | } 21 | 22 | if (stats.hasWarnings()) { 23 | console.warn(info.warnings); 24 | } 25 | } 26 | 27 | resolve(); 28 | }); 29 | }); 30 | }; 31 | -------------------------------------------------------------------------------- /plato/commands/build.ts: -------------------------------------------------------------------------------- 1 | import fse from 'fs-extra'; 2 | import path from 'path'; 3 | import opn from 'opn'; 4 | import connect from 'connect'; 5 | import serveStatic from 'serve-static'; 6 | 7 | import { Routes, Route } from '../@types/route.js'; 8 | 9 | import reporter from '../utils/reporter.js'; 10 | import { saveRemoteDataFromSource } from '../core/saveData.js'; 11 | import { updateRoutes } from '../core/updateRoutes.js'; 12 | import { createPages } from '../core/createPages.js'; 13 | import buildHTML from '../core/buildHTML.js'; 14 | import buildProductionBundle from './build-javascript.js'; 15 | import buildCritical from './build-critical.js'; 16 | 17 | import config from './../../site-config.js'; 18 | 19 | export default async function build(verbose = false, open = false) { 20 | reporter.verbose = verbose; 21 | 22 | // Grab static routes from config 23 | const staticRoutes = config.staticRoutes as Route[]; 24 | 25 | let globalActivity = reporter.activity('Plato Build', '🤔'); 26 | globalActivity.start(); 27 | 28 | /** 29 | * Clean repo 30 | * Empty directories and move source files 31 | */ 32 | let activity = reporter.activity('Cleaning Repo', '🧽'); 33 | activity.start(); 34 | 35 | // clear destination folder 36 | fse.emptyDirSync(global.siteDir); 37 | 38 | // remove real_routes files 39 | fse.emptyDirSync(global.routeDest); 40 | 41 | // copy assets folder 42 | fse.copySync(path.resolve(global.srcPath, './.htaccess'), path.resolve(global.siteDir, './.htaccess')); 43 | fse.copySync(path.resolve(global.srcPath, './assets'), path.resolve(global.siteDir, './assets')); 44 | fse.copySync(path.resolve(global.srcPath, './data'), path.resolve(global.siteDir, './data')); 45 | 46 | activity.end(); 47 | 48 | /** 49 | * Build dynamic routes file 50 | * And save remote date from the static route file 51 | */ 52 | activity = reporter.activity('Build Routes and save remote Data from Static routes', '🛣️'); 53 | activity.start(); 54 | 55 | // add static routes to final_routes 56 | // save remote endpoint for static routes 57 | try { 58 | await updateRoutes(staticRoutes); 59 | } catch (err) { 60 | reporter.failure('Error during updating routes: ', err as string); 61 | } 62 | 63 | // save remote endpoint for static routes 64 | try { 65 | for (const route of staticRoutes) { 66 | if (route.dataSource) await saveRemoteDataFromSource(route.dataSource, route.json, global.siteDir + '/data/'); 67 | } 68 | } catch (err) { 69 | reporter.failure('Error during saving static file: ', err as string); 70 | } 71 | activity.end(); 72 | 73 | /** 74 | * Get Pages data from Plato Node API getStaticPagesProps method 75 | */ 76 | activity = reporter.activity('Create Pages from Plato API', '🤖'); 77 | activity.start(true); 78 | 79 | let pagesProps: Route[] = []; 80 | try { 81 | const { getStaticPagesProps } = await import('./../../plato-node.js'); 82 | if (getStaticPagesProps) { 83 | pagesProps = await getStaticPagesProps(); 84 | } 85 | } catch (err) { 86 | reporter.info('Error during getStaticPagesProps call'); 87 | } 88 | 89 | /** 90 | * Create Pages from Plato Node API getStaticPagesProps method 91 | */ 92 | let finalRoutes: Routes = { routes: [] }; 93 | try { 94 | if (pagesProps && pagesProps.length > 0) { 95 | finalRoutes = await createPages(pagesProps, global.siteDir); 96 | } 97 | } catch (err) { 98 | reporter.error('Error during page creation ' + err); 99 | } 100 | 101 | // check if node API is used and if so, check if createGlobalData is used 102 | let globalData = {}; 103 | try { 104 | const { createGlobalData } = await import('./../../plato-node.js'); 105 | 106 | if (createGlobalData) { 107 | globalData = await createGlobalData(); 108 | } 109 | } catch (err) { 110 | console.log('No API found: ' + err); 111 | } 112 | 113 | // on plato complete Hook 114 | try { 115 | const { onPageCreatedHook } = await import('./../../plato-node.js'); 116 | 117 | if (onPageCreatedHook) { 118 | await onPageCreatedHook(); 119 | } 120 | } catch (err) { 121 | console.log('Error during onPageCreatedHook: ' + err); 122 | } 123 | activity.end(); 124 | 125 | /** 126 | * Build production javascript using webpack 127 | */ 128 | activity = reporter.activity('Build Javascript', '📁'); 129 | activity.start(); 130 | // Build Javascript and CSS Production Bundle Return the manifest with files and 131 | // their hashed path. 132 | await buildProductionBundle('').catch((err) => { 133 | reporter.failure('Generating JavaScript bundles failed', err as string); 134 | }); 135 | 136 | activity.end(); 137 | 138 | /** 139 | * Build legacy javascript using webpack 140 | */ 141 | activity = reporter.activity('Build Legacy Javascript', '👴'); 142 | activity.start(); 143 | await buildProductionBundle('legacy').catch((err) => { 144 | reporter.failure('Generating Legacy JavaScript bundles failed', err as string); 145 | }); 146 | 147 | activity.end(); 148 | 149 | activity = reporter.activity('Build HTML', '💻'); 150 | activity.start(); 151 | 152 | // manifest created by webpack prod build 153 | const files: any[] = []; 154 | const manifestFile = fse.readJsonSync('./build/assets/manifest.json'); 155 | try { 156 | for (let page of finalRoutes.routes) { 157 | const filename = await buildHTML(page, manifestFile, 'production', global.siteDir, globalData); 158 | files.push(filename); 159 | } 160 | } catch (err) { 161 | reporter.failure('Error during page generation: ', err as string); 162 | } 163 | activity.end(); 164 | 165 | activity = reporter.activity('Build Critical CSS and minify HTML', '🎨'); 166 | activity.start(); 167 | 168 | function startNewJob(): Promise { 169 | const file = files.pop(); // NOTE: mutates file array 170 | if (!file) { 171 | // no more new jobs to process (might still be jobs currently in process) 172 | return Promise.resolve(); 173 | } 174 | const dest = path.join(global.siteDir, file); 175 | return buildCritical(file, dest) 176 | .then(() => { 177 | // Then call to see if there are more jobs to process 178 | reporter.info(`Critical Built : ${file}`); 179 | return startNewJob(); 180 | }) 181 | .catch((err) => { 182 | reporter.failure('Error during Critical generation: ', err); 183 | }); 184 | } 185 | // how many jobs do we want to handle in parallel? 186 | // Below, 3: 187 | await Promise.all([startNewJob(), startNewJob(), startNewJob()]).catch((err) => 188 | reporter.failure('Error during Critical generations: ', err) 189 | ); 190 | 191 | activity.end(); 192 | globalActivity.end(); 193 | 194 | // open HTTP server serving our local files 195 | if (open) { 196 | const port = 8080; 197 | connect() 198 | .use(serveStatic(global.siteDir)) 199 | .listen(port, function () { 200 | reporter.displayUrl(`Server running on ${port}`, 'http://localhost:' + port); 201 | opn('http://localhost:' + port); 202 | }); 203 | } 204 | } 205 | -------------------------------------------------------------------------------- /plato/commands/develop-watchers.ts: -------------------------------------------------------------------------------- 1 | import fse from 'fs-extra'; 2 | import chokidar from 'chokidar'; 3 | 4 | import buildHTML from '../core/buildHTML.js'; 5 | import { Route } from '../@types/route.js'; 6 | import { getRoutesByTemplatePath } from '../utils/routes.js'; 7 | 8 | import reporter from '../utils/reporter.js'; 9 | 10 | function copyAssetToPublicFolder(path: string) { 11 | const publicPath = path.replace('src/', 'public/'); 12 | fse 13 | .copy(path, publicPath) 14 | .then(() => reporter.log('📗 File Copied: ' + path)) 15 | .catch((err: string) => { 16 | reporter.failure('Error during asset copy : ', err); 17 | }); 18 | } 19 | 20 | export default function createWatchers(routes: Route[], globalData: object) { 21 | /** 22 | * Add Watchers 23 | * Watcher for assets 24 | * Watcher for templates change 25 | */ 26 | // Initialize watcher for template files 27 | let watcher = chokidar.watch('./shared/templates/', { 28 | ignored: /(^|[\/\\])\../, 29 | persistent: true, 30 | }); 31 | watcher 32 | .on('add', (path) => reporter.log(`File ${path} has been added`)) 33 | .on('change', (path) => { 34 | const activeRoutes = getRoutesByTemplatePath(routes, path); 35 | if (activeRoutes !== null) { 36 | for (let page of activeRoutes) { 37 | buildHTML(page, null, 'development', global.siteDir, globalData).catch(console.error); 38 | } 39 | } else { 40 | // change from a partial => rebuild everything 41 | // TODO : check if change is not happening in an unlink template 42 | for (let page of routes) { 43 | buildHTML(page, null, 'development', global.siteDir, globalData).catch((err) => { 44 | reporter.log(`Error: ${err}`); 45 | }); 46 | } 47 | } 48 | reporter.log(`File ${path} has been changed`); 49 | }); 50 | 51 | watcher.add('./shared/partials/'); 52 | 53 | // Initialize watcher for assets files 54 | let watcherAsset = chokidar.watch('./src/assets/', { 55 | ignored: /(^|[\/\\])\../, 56 | persistent: true, 57 | ignoreInitial: true, 58 | }); 59 | 60 | watcherAsset.on('add', (path) => copyAssetToPublicFolder(path)); 61 | watcherAsset.on('change', (path) => copyAssetToPublicFolder(path)); 62 | } 63 | -------------------------------------------------------------------------------- /plato/commands/develop.ts: -------------------------------------------------------------------------------- 1 | import fse from 'fs-extra'; 2 | import path from 'path'; 3 | import webpack from 'webpack'; 4 | import WebpackDevServer from 'webpack-dev-server'; 5 | import opn from 'opn'; 6 | 7 | import { Routes, Route } from '../@types/route.js'; 8 | 9 | import config from '../../site-config.js'; 10 | import webpackConfig from '../../config/webpack.dev.config.js'; 11 | 12 | import buildHTML from '../core/buildHTML.js'; 13 | import { createPages } from '../core/createPages.js'; 14 | import { saveRemoteDataFromSource } from '../core/saveData.js'; 15 | import { updateRoutes } from '../core/updateRoutes.js'; 16 | import createWatchers from './develop-watchers.js'; 17 | 18 | import reporter from '../utils/reporter.js'; 19 | const port = 9090; 20 | 21 | export default async function develop(verbose: boolean = false, open: boolean = false) { 22 | reporter.verbose = verbose; 23 | 24 | // Grab static routes 25 | const staticRoutes = config.staticRoutes as Route[]; 26 | 27 | let globalActivity = reporter.activity('Plato Develop', '🤔'); 28 | globalActivity.start(true); 29 | 30 | /** 31 | * Clean repo 32 | * Empty directories and move source files 33 | */ 34 | let activity = reporter.activity('Cleaning Repo', '🧽'); 35 | activity.start(); 36 | 37 | // clear destination folder 38 | fse.emptyDirSync(global.siteDir); 39 | 40 | // remove real_routes files 41 | fse.emptyDirSync(global.routeDest); 42 | 43 | // copy assets folder 44 | fse.copySync(path.resolve(global.srcPath, './.htaccess'), path.resolve(global.siteDir, './.htaccess')); 45 | fse.copySync(path.resolve(global.srcPath, './assets'), path.resolve(global.siteDir, './assets')); 46 | fse.copySync(path.resolve(global.srcPath, './data'), path.resolve(global.siteDir, './data')); 47 | 48 | activity.end(); 49 | 50 | /** 51 | * Build dynamic routes file 52 | * And save remote date from the static route file 53 | */ 54 | activity = reporter.activity('Build Routes and save remote Data from Static routes', '🛣️'); 55 | activity.start(); 56 | 57 | // add static routes to final_routes 58 | // save remote endpoint for static routes 59 | try { 60 | await updateRoutes(staticRoutes); 61 | } catch (err) { 62 | reporter.failure('Error during updating routes: ', err as string); 63 | } 64 | 65 | // save remote endpoint for static routes 66 | try { 67 | for (const route of staticRoutes) { 68 | if (route.dataSource) await saveRemoteDataFromSource(route.dataSource, route.json, global.siteDir + '/data/'); 69 | } 70 | } catch (err) { 71 | reporter.failure('Error during saving static file: ', err as string); 72 | } 73 | activity.end(); 74 | 75 | /** 76 | * Get Pages data from Plato Node API getStaticPagesProps method 77 | */ 78 | activity = reporter.activity('Create Pages from Plato API', '🤖'); 79 | activity.start(true); 80 | 81 | let pagesProps: Route[] = []; 82 | try { 83 | const { getStaticPagesProps } = await import('./../../plato-node.js'); 84 | if (getStaticPagesProps) { 85 | //TODO: add validation on user data 86 | pagesProps = await getStaticPagesProps(); 87 | } 88 | } catch (err) { 89 | reporter.failure('Error during getStaticPagesProps call', err as string); 90 | } 91 | 92 | /** 93 | * Create Pages from pagesProps 94 | */ 95 | let finalRoutes: Routes = { routes: [] }; 96 | if (pagesProps && pagesProps.length > 0) { 97 | try { 98 | finalRoutes = await createPages(pagesProps, global.siteDir); 99 | } catch (err) { 100 | reporter.failure('Error during page creation ', err as string); 101 | } 102 | } 103 | 104 | // on plato complete Hook 105 | try { 106 | const { onPageCreatedHook } = await import('./../../plato-node.js'); 107 | 108 | if (onPageCreatedHook) { 109 | await onPageCreatedHook(); 110 | } 111 | } catch (err) { 112 | console.log('Error during onPageCreatedHook: ' + err); 113 | } 114 | 115 | // check if node API is used and if so, check if createGlobalData is used 116 | let globalData = {}; 117 | try { 118 | const { createGlobalData } = await import('./../../plato-node.js'); 119 | if (createGlobalData) { 120 | globalData = await createGlobalData(); 121 | } 122 | } catch (err) { 123 | reporter.failure('Error createGlobalData ', err as string); 124 | } 125 | activity.end(); 126 | 127 | /** 128 | * Build static HTML 129 | */ 130 | activity = reporter.activity('Build Static site', '🚀'); 131 | activity.start(); 132 | 133 | try { 134 | for (let page of finalRoutes.routes) { 135 | await buildHTML(page, null, 'development', global.siteDir, globalData); 136 | } 137 | } catch (err) { 138 | reporter.failure('Error during page generation: ', err as string); 139 | } 140 | activity.end(); 141 | 142 | /** 143 | * Create file watchers to rebuild HTML when templates changes 144 | */ 145 | createWatchers(finalRoutes.routes, globalData); 146 | 147 | //TODO: provide dynamicRewrite support via API 148 | let dynamicRewrite: any[] = []; 149 | // dynamicRewrite.push({ from: '/furniture/*', to: '/furniture/' }); 150 | 151 | const options = { 152 | port, 153 | hot: true, 154 | headers: { 155 | 'Cache-Control': 'max-age=0', 156 | }, 157 | host: 'localhost', 158 | allowedHosts: 'all', 159 | historyApiFallback: { 160 | rewrites: dynamicRewrite, 161 | }, 162 | static: { 163 | directory: 'public', 164 | watch: true, 165 | serveIndex: true, 166 | }, 167 | }; 168 | 169 | const compiler = webpack(webpackConfig); 170 | const server = new WebpackDevServer(options, compiler); 171 | globalActivity.end(); 172 | 173 | const runServer = async () => { 174 | await server.start(); 175 | reporter.displayUrl('Development server started', 'http://localhost:' + port); 176 | if (open) opn('http://localhost:' + port); 177 | }; 178 | 179 | runServer(); 180 | } 181 | -------------------------------------------------------------------------------- /plato/core/buildHTML.ts: -------------------------------------------------------------------------------- 1 | import fse from 'fs-extra'; 2 | import path from 'path'; 3 | import template from 'art-template'; 4 | 5 | import reporter from '../utils/reporter.js'; 6 | import config from '../../site-config.js'; 7 | import { Route } from '../@types/route.js'; 8 | 9 | // Set ART FILTERS 10 | import { filters } from '../../shared/templates/filters/index.js'; 11 | for (let [key, value] of Object.entries(filters)) { 12 | // @ts-expect-error 13 | template.defaults.imports[key] = value; 14 | } 15 | 16 | // A function that returns a promise to resolve into the data //fetched from the API or an error 17 | let artTemplatePromise = (templatePath: string, data: {}) => { 18 | return new Promise((resolve, reject) => { 19 | try { 20 | let htmlArt = template(templatePath, data); 21 | resolve(htmlArt); 22 | } catch (ex) { 23 | reject(ex); 24 | } 25 | }); 26 | }; 27 | 28 | function writeFileWithDirectory(dirPath: string, contents: string) { 29 | return new Promise((resolve, reject) => { 30 | // return value is a Promise resolving to the first directory created 31 | fse 32 | .mkdirp(path.dirname(dirPath)) 33 | .then(() => { 34 | fse.writeFileSync(dirPath, contents); 35 | resolve(); 36 | }) 37 | .catch((err) => reject(err)); 38 | }); 39 | } 40 | 41 | export default ( 42 | page: Route, 43 | manifest: string | null, 44 | mode = 'development', 45 | siteDir: string, 46 | globalData: { serverData?: {} } 47 | ) => { 48 | return new Promise((resolve, reject) => { 49 | let destPath: string = ''; 50 | // this will be provided to critical 51 | let source: string; 52 | 53 | // needed to genere 404.html 54 | let fileName = page.fileName || 'index.html'; 55 | if (page.id === 'index' || page.id === '404') { 56 | destPath = path.join(siteDir, '/'); 57 | source = fileName; 58 | } else { 59 | try { 60 | destPath = path.join(siteDir, page.url, '/'); 61 | source = `${page.url}/${fileName}`; 62 | } catch (err) { 63 | reject(err); 64 | } 65 | } 66 | 67 | fse.mkdirs(destPath).catch((err: string) => { 68 | reject(err); 69 | }); 70 | 71 | let data = {}; 72 | if (page.json) { 73 | try { 74 | data = fse.readJsonSync(path.resolve(siteDir, './data/', page.json)); 75 | } catch (err) { 76 | reject(err); 77 | } 78 | } 79 | 80 | const templatePath = path.resolve(global.appRoot, `./shared/templates/${page.template}.art`); 81 | const exists = fse.existsSync(templatePath); 82 | if (!exists) reject(new Error('Template file does not exists')); 83 | 84 | artTemplatePromise(templatePath, { 85 | data, 86 | globalData, 87 | }) 88 | .then((html) => { 89 | // TODO: remove globals we don't need in JS 90 | // adding serverData for the first render here 91 | globalData.serverData = data; 92 | artTemplatePromise(path.resolve('./shared/templates/layout.art'), { 93 | html, 94 | config, 95 | data, 96 | mode, 97 | manifest, 98 | globals: globalData, 99 | globalDataString: JSON.stringify(globalData), 100 | location: page.id, 101 | type: page.template, 102 | }) 103 | .then((html) => { 104 | writeFileWithDirectory(path.resolve(destPath, fileName), html) 105 | .then(() => { 106 | reporter.info(`Page Built : ${destPath}${fileName}`); 107 | resolve(source); 108 | }) 109 | .catch((err) => { 110 | reject(err); 111 | }); 112 | }) 113 | .catch((err) => reject(err)); 114 | }) 115 | .catch((err) => reject(err)); 116 | }); 117 | }; 118 | -------------------------------------------------------------------------------- /plato/core/createPages.ts: -------------------------------------------------------------------------------- 1 | import { saveRemoteData } from './saveData.js'; 2 | import { updateRoutes } from './updateRoutes.js'; 3 | import { Routes, Route } from '../@types/route.js'; 4 | 5 | /** 6 | * Create a page. 7 | * This method let you create a page from the plato-node.js file 8 | * A page, is an item of the routes Object 9 | * A page is composed of the following element: 10 | * - index ( used to match the route later etc...) 11 | * - url ( the slug of the page ) 12 | * - template id ( template name to be match ) 13 | * - data *optional ( the path of remote data ) 14 | * - json ( the name of the json local data ) 15 | */ 16 | export const createPages = (pagesProps: Route[], siteDir: string) => { 17 | return new Promise(async (resolve, reject) => { 18 | // create JSON Files 19 | pagesProps.forEach(({ data, id }) => { 20 | saveRemoteData(JSON.stringify(data), id + '.json', siteDir + '/data/'); 21 | }); 22 | 23 | const routes = pagesProps.map(({ id, url, template }): Route => { 24 | return { 25 | id, 26 | url, 27 | template, 28 | json: id + '.json', 29 | }; 30 | }); 31 | 32 | try { 33 | const activeRoutes = await updateRoutes(routes); 34 | resolve(activeRoutes); 35 | } catch (error) { 36 | reject(error); 37 | } 38 | }); 39 | }; 40 | -------------------------------------------------------------------------------- /plato/core/saveData.ts: -------------------------------------------------------------------------------- 1 | import fse from 'fs-extra'; 2 | import path from 'path'; 3 | import fetch from 'node-fetch'; 4 | import reporter from '../utils/reporter.js'; 5 | 6 | const saveFile = (destinationPath: string, data: string) => { 7 | fse.writeFileSync(destinationPath, data, 'utf8'); 8 | }; 9 | 10 | export const saveRemoteData = (data: string, fileName: string, siteDir: string) => { 11 | const destinationPath = path.join(siteDir, fileName); 12 | return saveFile(destinationPath, data); 13 | }; 14 | 15 | export const saveRemoteDataFromSource = (source: string, fileName: string, siteDir: string) => { 16 | return new Promise((resolve, reject) => { 17 | const start = process.hrtime(); 18 | 19 | fetch(source, {}) 20 | .then((res) => res.json()) 21 | .then((body) => { 22 | const destinationPath = path.join(siteDir, fileName); 23 | saveFile(destinationPath, JSON.stringify(body)); 24 | const end = process.hrtime(start); 25 | reporter.info(`Saved remote data ${fileName} in ${end[1] / 1000000}ms`); 26 | resolve(fileName); 27 | }) 28 | .catch((error) => { 29 | reject(error); 30 | }); 31 | }); 32 | }; 33 | -------------------------------------------------------------------------------- /plato/core/updateRoutes.ts: -------------------------------------------------------------------------------- 1 | import fse from 'fs-extra'; 2 | import path from 'path'; 3 | import { Routes, Route } from '../@types/route.js'; 4 | 5 | // create main route JSON file 6 | // will create the route 7 | // or add to it if it already exists to deal with static + dynamic routes 8 | export const updateRoutes = (routes: Route[]) => { 9 | return new Promise((resolve, reject) => { 10 | const destinationPath = path.resolve(global.routeDest, './routes.json'); 11 | const exists = fse.existsSync(destinationPath); 12 | 13 | if (exists) { 14 | fse.readFile(destinationPath).then((data: Buffer) => { 15 | let obj: Routes = { routes: [] }; 16 | try { 17 | obj = JSON.parse(data.toString()); // now it's an object 18 | } catch (e) { 19 | reject(e); 20 | } 21 | 22 | obj.routes = [...obj.routes, ...routes]; // add some data 23 | const json = JSON.stringify(obj); // convert it back to json 24 | fse 25 | .writeFile(destinationPath, json, 'utf8') // write it back 26 | .then(() => resolve(obj)) 27 | .catch((err) => reject(err)); 28 | }); 29 | } else { 30 | const routesObject = { 31 | routes, 32 | }; 33 | 34 | fse.mkdirSync(path.dirname(destinationPath), { recursive: true }); 35 | const json = JSON.stringify(routesObject); 36 | 37 | try { 38 | fse.writeFileSync(destinationPath, json, 'utf8'); 39 | resolve(routesObject); 40 | } catch (error) { 41 | reject(); 42 | } 43 | } 44 | }); 45 | }; 46 | -------------------------------------------------------------------------------- /plato/index.ts: -------------------------------------------------------------------------------- 1 | // We import the config here to avoid order issue 2 | // In Node modules are hoisted before 3 | // but dotenv needs to be preloaded 4 | // https://stackoverflow.com/a/63541230 5 | import 'dotenv/config'; 6 | 7 | import program from 'commander'; 8 | import develop from './commands/develop.js'; 9 | import build from './commands/build.js'; 10 | import path from 'path'; 11 | import { fileURLToPath } from 'url'; 12 | 13 | const __filename = fileURLToPath(import.meta.url); 14 | const __dirname = path.dirname(__filename); 15 | 16 | program 17 | .command('develop') 18 | .option('-v, --verbose', 'output extra debugging') 19 | .option('-o, --open', 'open dev') 20 | .description('Develop website') 21 | // create global path data that will be used across our scripts 22 | .action(() => { 23 | global.appRoot = path.resolve(__dirname + './../'); 24 | global.srcPath = path.resolve(global.appRoot, './src/'); 25 | global.siteDir = path.resolve(global.appRoot, './public/'); 26 | global.routeDest = path.resolve(global.appRoot, './.plato/'); 27 | }) 28 | .action((cmdObj: any) => { 29 | develop(cmdObj.verbose, cmdObj.open); 30 | }); 31 | 32 | program 33 | .command('build') 34 | .option('-v, --verbose', 'output extra debugging') 35 | .option('-o, --open', 'open local http server to QA build') 36 | // create global path data that will be used across our scripts 37 | .action(() => { 38 | global.appRoot = path.resolve(__dirname + './../'); 39 | global.srcPath = path.resolve(global.appRoot, './src/'); 40 | global.siteDir = path.resolve(global.appRoot, './build/'); 41 | global.routeDest = path.resolve(global.appRoot, './.plato/'); 42 | }) 43 | .action((cmdObj: any) => { 44 | build(cmdObj.verbose, cmdObj.open); 45 | }); 46 | 47 | program.parse(process.argv); 48 | -------------------------------------------------------------------------------- /plato/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig to read more about this file */ 4 | 5 | /* Projects */ 6 | // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ 7 | // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ 8 | // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ 9 | // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ 10 | // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ 11 | // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ 12 | 13 | /* Language and Environment */ 14 | "target": "es2020" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, 15 | // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ 16 | // "jsx": "preserve", /* Specify what JSX code is generated. */ 17 | // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ 18 | // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ 19 | // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ 20 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ 21 | // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ 22 | // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ 23 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ 24 | // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ 25 | // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ 26 | 27 | /* Modules */ 28 | "module": "ES2020" /* Specify what module code is generated. */, 29 | // "rootDir": "./", /* Specify the root folder within your source files. */ 30 | "moduleResolution": "node" /* Specify how TypeScript looks up a file from a given module specifier. */, 31 | // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ 32 | // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ 33 | // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ 34 | "typeRoots": ["./@types/"] /* Specify multiple folders that act like './node_modules/@types'. */, 35 | // "types": [], /* Specify type package names to be included without being referenced in a source file. */ 36 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 37 | // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ 38 | // "resolveJsonModule": true, /* Enable importing .json files. */ 39 | // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ 40 | 41 | /* JavaScript Support */ 42 | // "allowJs": true /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */, 43 | // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ 44 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ 45 | 46 | /* Emit */ 47 | // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ 48 | // "declarationMap": true, /* Create sourcemaps for d.ts files. */ 49 | // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ 50 | // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ 51 | // "outFile": "./" /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */, 52 | "outDir": "./../plato-dist/" /* Specify an output folder for all emitted files. */, 53 | // "removeComments": true, /* Disable emitting comments. */ 54 | // "noEmit": true, /* Disable emitting files from a compilation. */ 55 | // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ 56 | // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */ 57 | // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ 58 | // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ 59 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 60 | // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ 61 | // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ 62 | // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ 63 | // "newLine": "crlf", /* Set the newline character for emitting files. */ 64 | // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ 65 | // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ 66 | // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ 67 | // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ 68 | // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ 69 | // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ 70 | 71 | /* Interop Constraints */ 72 | // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ 73 | // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ 74 | "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */, 75 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ 76 | "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, 77 | 78 | /* Type Checking */ 79 | "strict": true /* Enable all strict type-checking options. */, 80 | "noImplicitAny": false /* Enable error reporting for expressions and declarations with an implied 'any' type. */, 81 | // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ 82 | // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ 83 | // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ 84 | // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ 85 | // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ 86 | // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ 87 | // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ 88 | // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ 89 | // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ 90 | // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ 91 | // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ 92 | // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ 93 | // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ 94 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ 95 | // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ 96 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ 97 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ 98 | 99 | /* Completeness */ 100 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ 101 | "skipLibCheck": true /* Skip type checking all .d.ts files. */ 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /plato/utils/reporter.ts: -------------------------------------------------------------------------------- 1 | import convertHrtime from 'convert-hrtime'; 2 | import chalk from 'chalk'; 3 | import terminalLink from 'terminal-link'; 4 | import boxen from 'boxen'; 5 | const { BorderStyle } = boxen; 6 | 7 | const log = console.log; 8 | 9 | const defaultColors = { 10 | activityStart: chalk.magentaBright, 11 | log: chalk.white, 12 | info: chalk.grey, 13 | warn: chalk.yellow, 14 | error: chalk.red, 15 | success: chalk.green, 16 | bigError: chalk.bold.red, 17 | bigSucces: chalk.bold.blue, 18 | }; 19 | 20 | class Reporter { 21 | verbose = false; 22 | colors = defaultColors; 23 | 24 | updateColors(colors: {}) { 25 | this.colors = Object.assign(colors, defaultColors); 26 | } 27 | 28 | log(message: string) { 29 | log(this.colors.log(message)); 30 | } 31 | 32 | info(message: string) { 33 | // info is only printed if verbose is precised 34 | this.verbose && log(this.colors.info(message)); 35 | } 36 | 37 | warn(message: string) { 38 | log(this.colors.warn(message)); 39 | } 40 | 41 | error(message: string, exit = false) { 42 | log(this.colors.error(message)); 43 | if (exit) process.exit(1); 44 | } 45 | 46 | failure(message: string, error: string) { 47 | log(this.colors.bigError(message)); 48 | this.error(error); 49 | process.exit(1); 50 | } 51 | 52 | success(message: string) { 53 | log(this.colors.success(message)); 54 | } 55 | 56 | displayUrl = function (message: string, url: string) { 57 | log(boxen(`${message} \n ${terminalLink(url, url)}`, { padding: 1, margin: 0, borderStyle: BorderStyle.Double })); 58 | }; 59 | 60 | activity(activityName: string, activityEmoji: string) { 61 | return Activity(activityName, activityEmoji, this); 62 | } 63 | } 64 | 65 | function Activity(activityName: string, activityEmoji = '', reporter: Reporter) { 66 | let startTime; 67 | return { 68 | start: (verbose = false) => { 69 | startTime = process.hrtime(); 70 | verbose && log(reporter.colors.activityStart(`starting ${activityEmoji} ${activityName}`)); 71 | }, 72 | update: (verbose = false) => { 73 | const elapsedTime = () => { 74 | let elapsed = process.hrtime(startTime); 75 | return `${convertHrtime(elapsed)['seconds'].toFixed(3)} s`; 76 | }; 77 | verbose && 78 | log(reporter.colors.info(`update ${activityEmoji}`), reporter.colors.log(`${activityName} - ${elapsedTime()}`)); 79 | }, 80 | end: () => { 81 | const elapsedTime = () => { 82 | let elapsed = process.hrtime(startTime); 83 | return `${convertHrtime(elapsed)['seconds'].toFixed(3)} s`; 84 | }; 85 | log( 86 | reporter.colors.success(`success ${activityEmoji}`), 87 | reporter.colors.log(`${activityName} - ${elapsedTime()}`) 88 | ); 89 | }, 90 | }; 91 | } 92 | 93 | const reporter = new Reporter(); 94 | export default reporter; 95 | -------------------------------------------------------------------------------- /plato/utils/routes.ts: -------------------------------------------------------------------------------- 1 | import { Route } from '../@types/route.js'; 2 | 3 | export function getRoutesByTemplatePath(routes: Route[], templatePath: string): Route[] | null { 4 | const testRegexp = templatePath.split('/').slice(-1); 5 | const name = testRegexp[0].substring(0, testRegexp[0].lastIndexOf('.')) || testRegexp[0]; 6 | let activeRoutes: Route[] = []; 7 | 8 | for (let route of routes) { 9 | if (route.template === name) activeRoutes.push(route); 10 | } 11 | 12 | if (activeRoutes.length > 0) return activeRoutes; 13 | return null; 14 | } 15 | -------------------------------------------------------------------------------- /postcss.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: [require('autoprefixer')], 3 | }; 4 | -------------------------------------------------------------------------------- /reports/plain-report.txt: -------------------------------------------------------------------------------- 1 | gsap: 278.86 KB (48.6%) 2 | core-js: 139.01 KB (24.2%) 3 | page: 31.01 KB (5.40%) 4 | css-loader: 2.91 KB (0.507%) 5 | art-template: 2.35 KB (0.409%) 6 | @babel/runtime: 269 B (0.0458%) 7 | : 119.63 KB (20.8%) 8 | -------------------------------------------------------------------------------- /shared/templates/about.art: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | {{if data.content}} 5 |

Dynamic content: {{ data.content.title}}

6 | {{/if}} 7 |

ABOUT PAGE Static content

8 |
9 | 10 |
-------------------------------------------------------------------------------- /shared/templates/filters/index.js: -------------------------------------------------------------------------------- 1 | // Reminders: FILTERS don’t seem to work when using with Set, just as output… 2 | // The idea is then to move the conditional logic to the "back-end" side 3 | export const filters = { 4 | round: function (value) { 5 | return Math.round(value); 6 | }, 7 | round2decimals: function (value) { 8 | return Math.round(value * 100) / 100; 9 | }, 10 | }; 11 | -------------------------------------------------------------------------------- /shared/templates/homepage.art: -------------------------------------------------------------------------------- 1 |
2 |
3 |

Dynamic content: {{ data.content.title }}

4 |

INDEX RELOAD Static content

5 | {{set test = 5.4}} 6 |

test filter: {{test|round }}

7 | {{ include './svgs/instagram.art' }} 8 |
9 |
-------------------------------------------------------------------------------- /shared/templates/layout.art: -------------------------------------------------------------------------------- 1 | {{ include './partials/header.art' }} 2 | 3 |
4 | {{@ html}} 5 |
6 | 7 | {{ include './partials/footer.art' }} -------------------------------------------------------------------------------- /shared/templates/notfound.art: -------------------------------------------------------------------------------- 1 |
2 |
3 |

Not the page you are looking for

4 |
5 |
-------------------------------------------------------------------------------- /shared/templates/partials/footer.art: -------------------------------------------------------------------------------- 1 | 2 | {{ if globalDataString }} 3 | 6 | {{/if}} 7 | 8 | {{ if manifest }} 9 | 10 | 11 | 12 | 13 | 14 | {{else}} 15 | 16 | {{/if}} 17 | 18 | 19 | -------------------------------------------------------------------------------- /shared/templates/partials/header.art: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {{ set metaTitle = config.siteTitle }} 6 | {{if data.meta && data.meta.title}} 7 | {{ set metaTitle =data.meta.title }} 8 | {{/if}} 9 | 10 | {{ set metaDescription = config.siteDescription }} 11 | {{if data.meta && data.meta.description}} 12 | {{ set metaDescription =data.meta.description }} 13 | {{/if}} 14 | 15 | {{ set metaSiteName = config.metaSiteName }} 16 | {{if data.meta && data.meta.siteName}} 17 | {{ set metaSiteName =data.meta.siteName }} 18 | {{/if}} 19 | 20 | {{ set metaImage = config.metaImage }} 21 | {{if data.meta && data.meta.metaImage}} 22 | {{ set metaImage =data.meta.metaImage }} 23 | {{/if}} 24 | 25 | {{ set metaTwitterName = config.metaTwitterName }} 26 | {{if data.meta && data.meta.twitterName}} 27 | {{ set metaTwitterName =data.meta.twitterName }} 28 | {{/if}} 29 | 30 | {{ set metaImageTwitter = config.metaImage }} 31 | {{if data.meta && data.meta.twitterImage}} 32 | {{ set metaImageTwitter =data.meta.twitterImage }} 33 | {{/if}} 34 | 35 | 36 | {{ metaTitle }} 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | {{ if config.preload }} 72 | {{ each config.preload }} 73 | {{ if $value.type == 'font' }} 74 | 75 | {{/if}} 76 | {{/each}} 77 | {{/if}} 78 | 79 | 80 | {{ if manifest }} 81 | 82 | {{/if}} 83 | 84 | 85 | 86 | 87 | 98 | 99 |
-------------------------------------------------------------------------------- /shared/templates/partials/responsive-image.art: -------------------------------------------------------------------------------- 1 | {{if image}} 2 | {{ if heightBasedAspectRatio == true }} 3 | {{ set aspectRatioKey = 'width' }} 4 | {{ set aspectRatioStyle = image.metadata.dimensions.width / image.metadata.dimensions.height | round2decimals }} 5 | {{ set aspectRatioUnit = 'vh' }} 6 | {{ else }} 7 | {{ set aspectRatioKey = 'padding-top' }} 8 | {{ set aspectRatioStyle = image.metadata.dimensions.height/image.metadata.dimensions.width*100 | round2decimals }} 9 | {{ set aspectRatioUnit = '%' }} 10 | {{/if }} 11 |
14 | {{image.label || image.title}} 18 | 19 |
20 | {{/if }} -------------------------------------------------------------------------------- /shared/templates/partials/srcset.art: -------------------------------------------------------------------------------- 1 | {{set widthIncrement = widthIncrement || 200}} 2 | {{set maxWidth = maxWidth || image.metadata.dimensions.width}} 3 | {{set iterationIndex = maxWidth/widthIncrement}} 4 | <% for(var i = 1; i <= iterationIndex; i++){ %> 5 | {{ image.url }}?w={{widthIncrement*i}}&fit=max&auto=format&q=100 {{widthIncrement*i}}w, 6 | <% } %> -------------------------------------------------------------------------------- /shared/templates/svgs/instagram.art: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /site-config.js: -------------------------------------------------------------------------------- 1 | const config = { 2 | siteTitle: 'Plato', 3 | siteTitleShort: 'Plato Plato', 4 | siteDescription: 'Super Duper Static Site generator', 5 | siteUrl: 'https://github.com/TimRoussilhe/Plato/', 6 | metaSiteName: 'Plato', 7 | metaTwitterName: '@timroussilhe', 8 | metaImage: 'https://media1.giphy.com/media/eLudircQfgGEU/giphy.gif?cid=3640f6095c0e8e92753069696f5d90c5', 9 | metaImageTwitter: 'https://media1.giphy.com/media/eLudircQfgGEU/giphy.gif?cid=3640f6095c0e8e92753069696f5d90c5', 10 | themeColor: '#000', 11 | backgroundColor: '#fff', 12 | pathPrefix: null, 13 | social: { 14 | twitter: 'Plato', 15 | fbAppId: '550534890534809345890', 16 | }, 17 | preload: [ 18 | { 19 | type: 'font', 20 | href: '/assets/fonts/UntitledSans-Regular.woff2', 21 | format: 'woff2', 22 | }, 23 | ], 24 | staticRoutes: [ 25 | { 26 | id: 'index', 27 | url: '/', 28 | template: 'homepage', 29 | json: 'index.json', 30 | }, 31 | { 32 | id: 'about', 33 | url: '/about-no-data', 34 | template: 'about', 35 | }, 36 | { 37 | id: 'about', 38 | url: '/about', 39 | template: 'about', 40 | json: 'about.json', 41 | }, 42 | { 43 | id: 'pokemon', 44 | url: '/pokemon', 45 | template: 'about', 46 | dataSource: 'https://pokeapi.co/api/v2/pokemon/?limit=6', 47 | json: 'pokemon.json', 48 | }, 49 | { 50 | id: '404', 51 | fileName: '404.html', 52 | url: '/error', 53 | template: 'notfound', 54 | json: '404.json', 55 | }, 56 | ], 57 | }; 58 | 59 | export default config; 60 | -------------------------------------------------------------------------------- /size-plugin.json: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TimRoussilhe/Plato/f69d0d8d52045c9b871fae5424439f4374443887/size-plugin.json -------------------------------------------------------------------------------- /src/.htaccess: -------------------------------------------------------------------------------- 1 | RewriteEngine On 2 | 3 | # remove trailing slash 4 | RewriteCond %{REQUEST_FILENAME} !-d 5 | RewriteRule ^([^/]+)/$ http://example.com/folder/$1 [R=301,L] 6 | 7 | # Redirect external .html requests to extensionless url 8 | RewriteCond %{THE_REQUEST} ^(.+)\.html([#?][^\ ]*)?\ HTTP/ 9 | RewriteRule ^(.+)\.html$ http://example.com/folder/$1 [R=301,L] 10 | 11 | # Resolve .html file for extensionless html urls 12 | RewriteRule ^([^/.]+)$ $1.html[L] 13 | 14 | # 1 Month for all your static assets 15 | 16 | Header set Cache-Control "max-age=604800, public" 17 | 18 | 19 | 20 | Header set Cache-Control "max-age=2592000, public" 21 | 22 | 23 | 24 | Header set Cache-Control "max-age=31536000, public" 25 | 26 | 27 | 28 | AddOutputFilterByType DEFLATE "application/atom+xml" \ 29 | "application/javascript" \ 30 | "application/json" \ 31 | "application/ld+json" \ 32 | "application/manifest+json" \ 33 | "application/rdf+xml" \ 34 | "application/rss+xml" \ 35 | "application/schema+json" \ 36 | "application/vnd.geo+json" \ 37 | "application/vnd.ms-fontobject" \ 38 | "application/x-font-ttf" \ 39 | "application/x-javascript" \ 40 | "application/x-web-app-manifest+json" \ 41 | "application/xhtml+xml" \ 42 | "application/xml" \ 43 | "font/eot" \ 44 | "font/opentype" \ 45 | "image/bmp" \ 46 | "image/svg+xml" \ 47 | "image/vnd.microsoft.icon" \ 48 | "image/x-icon" \ 49 | "text/cache-manifest" \ 50 | "text/css" \ 51 | "text/html" \ 52 | "text/javascript" \ 53 | "text/plain" \ 54 | "text/vcard" \ 55 | "text/vnd.rim.location.xloc" \ 56 | "text/vtt" \ 57 | "text/x-component" \ 58 | "text/x-cross-domain-policy" \ 59 | "text/xml" 60 | -------------------------------------------------------------------------------- /src/assets/favicon/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TimRoussilhe/Plato/f69d0d8d52045c9b871fae5424439f4374443887/src/assets/favicon/android-chrome-192x192.png -------------------------------------------------------------------------------- /src/assets/favicon/android-chrome-256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TimRoussilhe/Plato/f69d0d8d52045c9b871fae5424439f4374443887/src/assets/favicon/android-chrome-256x256.png -------------------------------------------------------------------------------- /src/assets/favicon/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TimRoussilhe/Plato/f69d0d8d52045c9b871fae5424439f4374443887/src/assets/favicon/apple-touch-icon.png -------------------------------------------------------------------------------- /src/assets/favicon/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | #da532c 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/assets/favicon/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TimRoussilhe/Plato/f69d0d8d52045c9b871fae5424439f4374443887/src/assets/favicon/favicon-16x16.png -------------------------------------------------------------------------------- /src/assets/favicon/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TimRoussilhe/Plato/f69d0d8d52045c9b871fae5424439f4374443887/src/assets/favicon/favicon-32x32.png -------------------------------------------------------------------------------- /src/assets/favicon/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TimRoussilhe/Plato/f69d0d8d52045c9b871fae5424439f4374443887/src/assets/favicon/favicon.ico -------------------------------------------------------------------------------- /src/assets/favicon/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TimRoussilhe/Plato/f69d0d8d52045c9b871fae5424439f4374443887/src/assets/favicon/mstile-150x150.png -------------------------------------------------------------------------------- /src/assets/favicon/safari-pinned-tab.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 8 | Created by potrace 1.11, written by Peter Selinger 2001-2013 9 | 10 | 12 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/assets/favicon/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "", 3 | "short_name": "", 4 | "icons": [ 5 | { 6 | "src": "/assets/favicon/android-chrome-192x192.png", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "/assets/favicon/android-chrome-256x256.png", 12 | "sizes": "256x256", 13 | "type": "image/png" 14 | } 15 | ], 16 | "theme_color": "#ffffff", 17 | "background_color": "#ffffff", 18 | "display": "standalone" 19 | } 20 | -------------------------------------------------------------------------------- /src/assets/fonts/UntitledSans-Regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TimRoussilhe/Plato/f69d0d8d52045c9b871fae5424439f4374443887/src/assets/fonts/UntitledSans-Regular.woff -------------------------------------------------------------------------------- /src/assets/fonts/UntitledSans-Regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TimRoussilhe/Plato/f69d0d8d52045c9b871fae5424439f4374443887/src/assets/fonts/UntitledSans-Regular.woff2 -------------------------------------------------------------------------------- /src/assets/images/image.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TimRoussilhe/Plato/f69d0d8d52045c9b871fae5424439f4374443887/src/assets/images/image.gif -------------------------------------------------------------------------------- /src/assets/images/test.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TimRoussilhe/Plato/f69d0d8d52045c9b871fae5424439f4374443887/src/assets/images/test.jpg -------------------------------------------------------------------------------- /src/assets/svgs/instagram.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/svgs/twitter.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/css/app.scss: -------------------------------------------------------------------------------- 1 | @import 'base/base'; 2 | 3 | // components 4 | @import 'components/header'; 5 | @import 'components/footer'; 6 | 7 | // page 8 | @import 'components/page'; 9 | -------------------------------------------------------------------------------- /src/css/base/_base.scss: -------------------------------------------------------------------------------- 1 | @import 'ui-resets'; 2 | 3 | // import mixins 4 | // @import 'mixins/mixins'; 5 | // @import 'mixins/aspect_ratio'; 6 | @import 'mixins/font_style'; 7 | @import 'mixins/responsive_font'; 8 | 9 | // import variables 10 | @import 'vars/fonts'; 11 | @import 'vars/colors'; 12 | @import 'vars/transitions'; 13 | @import 'vars/sizes'; 14 | @import 'vars/media-queries'; 15 | @import 'vars/icons'; 16 | 17 | // import utilities 18 | @import 'utilities/_u-align'; 19 | @import 'utilities/_u-overlay'; 20 | 21 | // Definitions 22 | @import 'typography'; 23 | @import 'ui-defs'; 24 | -------------------------------------------------------------------------------- /src/css/base/_typography.scss: -------------------------------------------------------------------------------- 1 | // Fonts definition here, we define each style of font. 2 | // Create all font Styles as mixin here 3 | 4 | @mixin fs-title { 5 | @include ffRegular; 6 | @include responsiveFontSize(53, 1500, $bp-desktop-s, $bp-desktop-l); 7 | 8 | line-height: 1.33em; 9 | 10 | @media #{$mq-desktop-s} { 11 | @include responsiveFontSize(48, 1200, $bp-mobile, $bp-desktop-s); 12 | } 13 | 14 | @media #{$mq-mobile-landscape} { 15 | @include responsiveFontSize(50, 800, $bp-mobile, $bp-mobile-landscape); 16 | } 17 | } 18 | 19 | @mixin fs-subtitle { 20 | @include ffRegular; 21 | @include fontStyle(28, 36, 0); 22 | } 23 | 24 | @mixin fs-paragraph { 25 | @include ffRegular; 26 | @include fontStyle(12, $lh-base, 0); 27 | } 28 | 29 | // Apply the font style to re-usable class that represent element of the design 30 | .fs-title { 31 | @include fs-title; 32 | } 33 | 34 | .fs-subtitle { 35 | @include fs-subtitle; 36 | } 37 | 38 | .fs-paragraph, 39 | .fs-body { 40 | @include fs-paragraph; 41 | } 42 | -------------------------------------------------------------------------------- /src/css/base/_ui-defs.scss: -------------------------------------------------------------------------------- 1 | html { 2 | font-size: 62.5%; 3 | 4 | /* Adjust font size */ 5 | font-size: 62.5%; 6 | -webkit-text-size-adjust: 100%; 7 | 8 | /* Font varient */ 9 | font-variant-ligatures: none; 10 | -webkit-font-variant-ligatures: none; 11 | 12 | /* Smoothing */ 13 | text-rendering: optimizeLegibility; 14 | -moz-osx-font-smoothing: grayscale; 15 | font-smoothing: antialiased; 16 | -webkit-font-smoothing: antialiased; 17 | } 18 | 19 | html { 20 | height: 100%; 21 | touch-action: manipulation; 22 | } 23 | 24 | body { 25 | height: 100%; 26 | background-color: $c-white; 27 | color: $c-dark-grey; 28 | @include ffRegular(); 29 | font-size: $fs-base; 30 | line-height: $lh-base; 31 | 32 | .block-events { 33 | pointer-events: none !important; 34 | } 35 | } 36 | 37 | #content { 38 | min-height: 100%; 39 | height: auto; 40 | } 41 | 42 | img { 43 | display: block; 44 | width: 100%; 45 | height: auto; 46 | } 47 | -------------------------------------------------------------------------------- /src/css/base/_ui-resets.scss: -------------------------------------------------------------------------------- 1 | /*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */ 2 | 3 | /* Document 4 | ========================================================================== */ 5 | 6 | /** 7 | * 1. Correct the line height in all browsers. 8 | * 2. Prevent adjustments of font size after orientation changes in iOS. 9 | */ 10 | 11 | html { 12 | line-height: 1.15; /* 1 */ 13 | -webkit-text-size-adjust: 100%; /* 2 */ 14 | } 15 | 16 | /* Sections 17 | ========================================================================== */ 18 | 19 | /** 20 | * Remove the margin in all browsers. 21 | */ 22 | 23 | body { 24 | margin: 0; 25 | } 26 | 27 | /** 28 | * Render the `main` element consistently in IE. 29 | */ 30 | 31 | main { 32 | display: block; 33 | } 34 | 35 | /** 36 | * Correct the font size and margin on `h1` elements within `section` and 37 | * `article` contexts in Chrome, Firefox, and Safari. 38 | */ 39 | 40 | h1 { 41 | font-size: 2em; 42 | margin: 0.67em 0; 43 | } 44 | 45 | /* Grouping content 46 | ========================================================================== */ 47 | 48 | /** 49 | * 1. Add the correct box sizing in Firefox. 50 | * 2. Show the overflow in Edge and IE. 51 | */ 52 | 53 | hr { 54 | box-sizing: content-box; /* 1 */ 55 | height: 0; /* 1 */ 56 | overflow: visible; /* 2 */ 57 | } 58 | 59 | /** 60 | * 1. Correct the inheritance and scaling of font size in all browsers. 61 | * 2. Correct the odd `em` font sizing in all browsers. 62 | */ 63 | 64 | pre { 65 | font-family: monospace, monospace; /* 1 */ 66 | font-size: 1em; /* 2 */ 67 | } 68 | 69 | /* Text-level semantics 70 | ========================================================================== */ 71 | 72 | /** 73 | * Remove the gray background on active links in IE 10. 74 | */ 75 | 76 | a { 77 | background-color: transparent; 78 | } 79 | 80 | /** 81 | * 1. Remove the bottom border in Chrome 57- 82 | * 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari. 83 | */ 84 | 85 | abbr[title] { 86 | border-bottom: none; /* 1 */ 87 | text-decoration: underline; /* 2 */ 88 | text-decoration: underline dotted; /* 2 */ 89 | } 90 | 91 | /** 92 | * Add the correct font weight in Chrome, Edge, and Safari. 93 | */ 94 | 95 | b, 96 | strong { 97 | font-weight: bolder; 98 | } 99 | 100 | /** 101 | * 1. Correct the inheritance and scaling of font size in all browsers. 102 | * 2. Correct the odd `em` font sizing in all browsers. 103 | */ 104 | 105 | code, 106 | kbd, 107 | samp { 108 | font-family: monospace, monospace; /* 1 */ 109 | font-size: 1em; /* 2 */ 110 | } 111 | 112 | /** 113 | * Add the correct font size in all browsers. 114 | */ 115 | 116 | small { 117 | font-size: 80%; 118 | } 119 | 120 | /** 121 | * Prevent `sub` and `sup` elements from affecting the line height in 122 | * all browsers. 123 | */ 124 | 125 | sub, 126 | sup { 127 | font-size: 75%; 128 | line-height: 0; 129 | position: relative; 130 | vertical-align: baseline; 131 | } 132 | 133 | sub { 134 | bottom: -0.25em; 135 | } 136 | 137 | sup { 138 | top: -0.5em; 139 | } 140 | 141 | /* Embedded content 142 | ========================================================================== */ 143 | 144 | /** 145 | * Remove the border on images inside links in IE 10. 146 | */ 147 | 148 | img { 149 | border-style: none; 150 | } 151 | 152 | /* Forms 153 | ========================================================================== */ 154 | 155 | /** 156 | * 1. Change the font styles in all browsers. 157 | * 2. Remove the margin in Firefox and Safari. 158 | */ 159 | 160 | button, 161 | input, 162 | optgroup, 163 | select, 164 | textarea { 165 | font-family: inherit; /* 1 */ 166 | font-size: 100%; /* 1 */ 167 | line-height: 1.15; /* 1 */ 168 | margin: 0; /* 2 */ 169 | } 170 | 171 | /** 172 | * Show the overflow in IE. 173 | * 1. Show the overflow in Edge. 174 | */ 175 | 176 | button, 177 | input { 178 | /* 1 */ 179 | overflow: visible; 180 | } 181 | 182 | /** 183 | * Remove the inheritance of text transform in Edge, Firefox, and IE. 184 | * 1. Remove the inheritance of text transform in Firefox. 185 | */ 186 | 187 | button, 188 | select { 189 | /* 1 */ 190 | text-transform: none; 191 | } 192 | 193 | /** 194 | * Correct the inability to style clickable types in iOS and Safari. 195 | */ 196 | 197 | button, 198 | [type='button'], 199 | [type='reset'], 200 | [type='submit'] { 201 | -webkit-appearance: button; 202 | } 203 | 204 | /** 205 | * Remove the inner border and padding in Firefox. 206 | */ 207 | 208 | button::-moz-focus-inner, 209 | [type='button']::-moz-focus-inner, 210 | [type='reset']::-moz-focus-inner, 211 | [type='submit']::-moz-focus-inner { 212 | border-style: none; 213 | padding: 0; 214 | } 215 | 216 | /** 217 | * Restore the focus styles unset by the previous rule. 218 | */ 219 | 220 | button:-moz-focusring, 221 | [type='button']:-moz-focusring, 222 | [type='reset']:-moz-focusring, 223 | [type='submit']:-moz-focusring { 224 | outline: 1px dotted ButtonText; 225 | } 226 | 227 | /** 228 | * Correct the padding in Firefox. 229 | */ 230 | 231 | fieldset { 232 | padding: 0.35em 0.75em 0.625em; 233 | } 234 | 235 | /** 236 | * 1. Correct the text wrapping in Edge and IE. 237 | * 2. Correct the color inheritance from `fieldset` elements in IE. 238 | * 3. Remove the padding so developers are not caught out when they zero out 239 | * `fieldset` elements in all browsers. 240 | */ 241 | 242 | legend { 243 | box-sizing: border-box; /* 1 */ 244 | color: inherit; /* 2 */ 245 | display: table; /* 1 */ 246 | max-width: 100%; /* 1 */ 247 | padding: 0; /* 3 */ 248 | white-space: normal; /* 1 */ 249 | } 250 | 251 | /** 252 | * Add the correct vertical alignment in Chrome, Firefox, and Opera. 253 | */ 254 | 255 | progress { 256 | vertical-align: baseline; 257 | } 258 | 259 | /** 260 | * Remove the default vertical scrollbar in IE 10+. 261 | */ 262 | 263 | textarea { 264 | overflow: auto; 265 | } 266 | 267 | /** 268 | * 1. Add the correct box sizing in IE 10. 269 | * 2. Remove the padding in IE 10. 270 | */ 271 | 272 | [type='checkbox'], 273 | [type='radio'] { 274 | box-sizing: border-box; /* 1 */ 275 | padding: 0; /* 2 */ 276 | } 277 | 278 | /** 279 | * Correct the cursor style of increment and decrement buttons in Chrome. 280 | */ 281 | 282 | [type='number']::-webkit-inner-spin-button, 283 | [type='number']::-webkit-outer-spin-button { 284 | height: auto; 285 | } 286 | 287 | /** 288 | * 1. Correct the odd appearance in Chrome and Safari. 289 | * 2. Correct the outline style in Safari. 290 | */ 291 | 292 | [type='search'] { 293 | -webkit-appearance: textfield; /* 1 */ 294 | outline-offset: -2px; /* 2 */ 295 | } 296 | 297 | /** 298 | * Remove the inner padding in Chrome and Safari on macOS. 299 | */ 300 | 301 | [type='search']::-webkit-search-decoration { 302 | -webkit-appearance: none; 303 | } 304 | 305 | /** 306 | * 1. Correct the inability to style clickable types in iOS and Safari. 307 | * 2. Change font properties to `inherit` in Safari. 308 | */ 309 | 310 | ::-webkit-file-upload-button { 311 | -webkit-appearance: button; /* 1 */ 312 | font: inherit; /* 2 */ 313 | } 314 | 315 | /* Interactive 316 | ========================================================================== */ 317 | 318 | /* 319 | * Add the correct display in Edge, IE 10+, and Firefox. 320 | */ 321 | 322 | details { 323 | display: block; 324 | } 325 | 326 | /* 327 | * Add the correct display in all browsers. 328 | */ 329 | 330 | summary { 331 | display: list-item; 332 | } 333 | 334 | /* Misc 335 | ========================================================================== */ 336 | 337 | /** 338 | * Add the correct display in IE 10+. 339 | */ 340 | 341 | template { 342 | display: none; 343 | } 344 | 345 | /** 346 | * Add the correct display in IE 10. 347 | */ 348 | 349 | [hidden] { 350 | display: none; 351 | } 352 | 353 | * { 354 | vertical-align: baseline; 355 | border: 0 none; 356 | outline: 0; 357 | padding: 0; 358 | margin: 0; 359 | box-sizing: border-box; 360 | } 361 | 362 | html, 363 | body, 364 | div, 365 | span, 366 | applet, 367 | object, 368 | iframe, 369 | table, 370 | caption, 371 | tbody, 372 | tfoot, 373 | thead, 374 | tr, 375 | th, 376 | td, 377 | del, 378 | dfn, 379 | em, 380 | font, 381 | img, 382 | ins, 383 | kbd, 384 | q, 385 | s, 386 | samp, 387 | small, 388 | strike, 389 | strong, 390 | sub, 391 | sup, 392 | tt, 393 | var, 394 | h1, 395 | h2, 396 | h3, 397 | h4, 398 | h5, 399 | h6, 400 | p, 401 | blockquote, 402 | pre, 403 | a, 404 | abbr, 405 | acronym, 406 | address, 407 | big, 408 | cite, 409 | code, 410 | dl, 411 | dt, 412 | dd, 413 | ol, 414 | ul, 415 | li, 416 | fieldset, 417 | form, 418 | label, 419 | legend, 420 | input, 421 | button { 422 | font-size: 100%; 423 | font: inherit; 424 | font-weight: normal; 425 | font-style: normal; 426 | vertical-align: baseline; 427 | border: 0 none; 428 | outline: 0; 429 | padding: 0; 430 | margin: 0; 431 | } 432 | 433 | button { 434 | border: none; 435 | border-radius: 0; 436 | background-color: transparent; 437 | cursor: pointer; 438 | } 439 | 440 | [role='button'], 441 | input[type='submit'], 442 | input[type='reset'], 443 | input[type='button'], 444 | button { 445 | -webkit-box-sizing: content-box; 446 | -moz-box-sizing: content-box; 447 | box-sizing: content-box; 448 | } 449 | 450 | /* Reset `button` and button-style `input` default styles */ 451 | input[type='submit'], 452 | input[type='reset'], 453 | input[type='button'], 454 | button { 455 | background: none; 456 | border: 0; 457 | color: inherit; 458 | /* cursor: default; */ 459 | font: inherit; 460 | line-height: normal; 461 | overflow: visible; 462 | padding: 0; 463 | -webkit-appearance: button; /* for input */ 464 | -webkit-user-select: none; /* for button */ 465 | -moz-user-select: none; 466 | -ms-user-select: none; 467 | } 468 | input::-moz-focus-inner, 469 | button::-moz-focus-inner { 470 | border: 0; 471 | padding: 0; 472 | } 473 | 474 | li { 475 | list-style: none; 476 | } 477 | 478 | a { 479 | text-decoration: none; 480 | cursor: pointer; 481 | line-height: normal; 482 | color: inherit; 483 | } 484 | -------------------------------------------------------------------------------- /src/css/base/mixins/_aspect_ratio.scss: -------------------------------------------------------------------------------- 1 | @mixin aspect-ratio($width, $height, $useBefore: true) { 2 | position: relative; 3 | display: block; 4 | 5 | @if $useBefore { 6 | &:before { 7 | display: block; 8 | content: ''; 9 | width: 100%; 10 | $percent: ($height / $width) * 100%; 11 | padding-top: round($percent * 1000) / 1000; 12 | } 13 | } @else { 14 | width: 100%; 15 | $percent: ($height / $width) * 100%; 16 | padding-top: round($percent * 1000) / 1000; 17 | } 18 | 19 | > img, 20 | > video, 21 | > div, 22 | > a { 23 | position: absolute; 24 | top: 0; 25 | left: 0; 26 | right: 0; 27 | bottom: 0; 28 | width: 100%; 29 | height: 100%; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/css/base/mixins/_font_style.scss: -------------------------------------------------------------------------------- 1 | // simple function to get back rem value; 2 | // in our case body is set to 62.5% so our base font-size is a 10 multiple 3 | @function get-rem($font-size) { 4 | @return calc($font-size/10 * 1rem); 5 | } 6 | 7 | // helper function to make sure the unit type (px, rem) 8 | // is removed we want just the raw value no unit 9 | @function get-value($n) { 10 | @return calc($n / ($n * 0 + 1)); 11 | } 12 | 13 | // return ratio line-height / font-size 14 | @function unitless-lh($font-size, $line-height) { 15 | @return get-value(calc($line-height / $font-size)); 16 | } 17 | 18 | // mixin for use to easily input Font Style Guide 19 | // px is optional too! 20 | // letter spacing is optional for some edge cases 21 | @mixin fontStyle($font-size, $line-height, $letter-spacing: '') { 22 | font-size: get-rem(get-value($font-size)); 23 | 24 | @if $line-height != '' { 25 | @if unit($line-height) == 'px' { 26 | line-height: $line-height; 27 | } @else if unit($line-height) == 'em' { 28 | line-height: $line-height; 29 | } @else { 30 | line-height: unitless-lh($font-size, $line-height); 31 | } 32 | } 33 | 34 | //basic check for letter spacing 35 | @if $letter-spacing != '' { 36 | @if unit($letter-spacing) == 'px' { 37 | letter-spacing: $letter-spacing; 38 | } @else if unit($letter-spacing) == 'em' { 39 | letter-spacing: $letter-spacing; 40 | } @else { 41 | letter-spacing: $letter-spacing * 1px; 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/css/base/mixins/_mixins.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * Add Generic SASS mixin here 3 | */ 4 | 5 | @mixin replaceText() { 6 | display: block; 7 | text-indent: -99999px; 8 | } 9 | 10 | @mixin tracking($val, $font-size) { 11 | letter-spacing: ($val * $font-size / 1000) * 1px; 12 | } 13 | 14 | @mixin photoshop-letterspacing-to-ems($val) { 15 | letter-spacing: ($val / 1000) * 1em; 16 | } 17 | -------------------------------------------------------------------------------- /src/css/base/mixins/_responsive_font.scss: -------------------------------------------------------------------------------- 1 | /* 2 | * $size: size in pixel you want to have at $breakpoint 3 | * $min-size: min size of the responsive font: doesn't go smaller 4 | * $max-size: max size of the responsive font: doesn't go bigger 5 | * $breakpoint: breakpoint to start using responsive fontSize 6 | */ 7 | @mixin responsiveFontSize($size, $breakpoint, $min-size: false, $max-size: false, $fallback: true) { 8 | // value for vw 9 | $responsive: calc($size * 100 / $breakpoint); 10 | 11 | //min value 12 | @if $min-size { 13 | $min-width: calc($size / ($breakpoint / $min-size)); 14 | 15 | @media screen and (max-width: #{$min-size}px) { 16 | font-size: $min-width * 1px; 17 | } 18 | } 19 | 20 | @if $max-size { 21 | $max-width: calc($size / ($breakpoint / $max-size)); 22 | 23 | @media screen and (min-width: #{$max-size}px) { 24 | font-size: $max-width * 1px; 25 | } 26 | } 27 | 28 | @if $fallback { 29 | font-size: $size * 1px; 30 | } 31 | 32 | font-size: $responsive * 1vw; 33 | } 34 | -------------------------------------------------------------------------------- /src/css/base/utilities/_u-align.scss: -------------------------------------------------------------------------------- 1 | @mixin u-flex-align { 2 | display: flex; 3 | align-items: center; 4 | justify-content: center; 5 | } 6 | -------------------------------------------------------------------------------- /src/css/base/utilities/_u-grab.scss: -------------------------------------------------------------------------------- 1 | .u-grab { 2 | cursor: move; 3 | cursor: grab; 4 | 5 | &:active { 6 | cursor: grabbing; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/css/base/utilities/_u-overlay.scss: -------------------------------------------------------------------------------- 1 | @mixin u-box { 2 | position: absolute; 3 | top: 0; 4 | left: 0; 5 | width: 100%; 6 | height: 100%; 7 | } 8 | 9 | @mixin u-overlay { 10 | position: fixed; 11 | top: 0; 12 | left: 0; 13 | width: 100%; 14 | height: 100%; 15 | } 16 | -------------------------------------------------------------------------------- /src/css/base/vars/_colors.scss: -------------------------------------------------------------------------------- 1 | // colors 2 | $c-white: #fff; 3 | $c-black: #000; 4 | $c-dark-grey: #2b2d30; 5 | 6 | // Font color 7 | .--c-white { 8 | color: $c-white; 9 | } 10 | 11 | .--c-black { 12 | color: $c-black; 13 | } 14 | 15 | .--c-dark-grey { 16 | color: $c-dark-grey; 17 | } 18 | 19 | // $colors: ( 20 | // white: #ffffff, 21 | // black: #171717, 22 | // error: #f4361e, 23 | // ); 24 | 25 | // $textColors: ( 26 | // dark: #171717, 27 | // medium: #888888, 28 | // light: #cccccc, 29 | // white: #ffffff, 30 | // ); 31 | -------------------------------------------------------------------------------- /src/css/base/vars/_fonts.scss: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'Untitled Sans'; 3 | src: url('../assets/fonts/UntitledSans-Regular.woff2') format('woff2'), 4 | url('../assets/fonts/UntitledSans-Regular.woff') format('woff'); 5 | font-weight: normal; 6 | font-style: normal; 7 | font-display: swap; 8 | } 9 | 10 | // font families 11 | $ff-title: 'Untitled Sans', 'Helvetica', 'Arial', sans-serif; 12 | $ff-bold: 'Untitled Sans', 'Helvetica', 'Arial', sans-serif; 13 | $ff-regular: 'Untitled Sans', 'Helvetica', 'Arial', sans-serif; 14 | $ff-medium: 'Untitled Sans', 'Helvetica', 'Arial', sans-serif; 15 | 16 | $fw-regular: normal; 17 | 18 | // font sizes 19 | $fs-base: 1.4rem; 20 | $lh-base: 1.6em; 21 | $lh-title: 1.33em; 22 | 23 | // Font Definitions 24 | @mixin ffRegular() { 25 | font-family: $ff-regular; 26 | font-weight: normal; 27 | font-style: normal; 28 | } 29 | 30 | @mixin ffMedium() { 31 | font-family: $ff-medium; 32 | font-weight: 500; 33 | font-style: normal; 34 | } 35 | 36 | @mixin ffBold() { 37 | font-family: $ff-bold; 38 | font-weight: bold; 39 | font-style: normal; 40 | } 41 | 42 | @mixin ffTitle() { 43 | font-family: $ff-title; 44 | font-weight: bold; 45 | font-style: normal; 46 | } 47 | 48 | // Line Height Variables 49 | $ls-title: 0.5em; 50 | $ls-body: 0.01em; 51 | -------------------------------------------------------------------------------- /src/css/base/vars/_icons.scss: -------------------------------------------------------------------------------- 1 | .icon { 2 | max-width: 40px; 3 | display: block; 4 | &.instagram { 5 | fill: blue; 6 | margin: 50px; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/css/base/vars/_media-queries.scss: -------------------------------------------------------------------------------- 1 | // break points 2 | $bp-mobile: 480; 3 | $bp-mobile-landscape: 800; 4 | $bp-tablet: 960; 5 | $bp-tablet-landscape: 1024; 6 | $bp-desktop-s: 1250; 7 | $bp-desktop-l: 1600; 8 | 9 | // media queries 10 | $mq-mobile: '(max-width: #{$bp-mobile}px)'; 11 | $mq-mobile-landscape: '(max-width: #{$bp-mobile-landscape}px) and (orientation : landscape)'; 12 | $mq-tablet: '(max-width: #{$bp-tablet}px)'; 13 | $mq-tablet-landscape: '(max-width: #{$bp-tablet-landscape}px) and (orientation : landscape)'; 14 | $mq-desktop-s: '(max-width: #{$bp-desktop-s}px)'; 15 | $mq-desktop-l: '(max-width: #{$bp-desktop-l}px)'; 16 | -------------------------------------------------------------------------------- /src/css/base/vars/_sizes.scss: -------------------------------------------------------------------------------- 1 | // heights 2 | 3 | // width 4 | $padding: 80px; 5 | $padding-mobile: 30px; 6 | $nav-padding: 30px; 7 | -------------------------------------------------------------------------------- /src/css/base/vars/_transitions.scss: -------------------------------------------------------------------------------- 1 | // Easing 2 | 3 | $quad-ease-in: cubic-bezier(0.55, 0.085, 0.68, 0.53); 4 | $cubic-ease-in: cubic-bezier(0.55, 0.055, 0.675, 0.19); 5 | $quart-ease-in: cubic-bezier(0.895, 0.03, 0.685, 0.22); 6 | $quint-ease-in: cubic-bezier(0.755, 0.05, 0.855, 0.06); 7 | $sine-ease-in: cubic-bezier(0.47, 0, 0.745, 0.715); 8 | $expo-ease-in: cubic-bezier(0.95, 0.05, 0.795, 0.035); 9 | $circ-ease-in: cubic-bezier(0.6, 0.04, 0.98, 0.335); 10 | $back-ease-in: cubic-bezier(0.6, -0.28, 0.735, 0.045); 11 | 12 | $quad-ease-out: cubic-bezier(0.25, 0.46, 0.45, 0.94); 13 | $cubic-ease-out: cubic-bezier(0.215, 0.61, 0.355, 1); 14 | $quart-ease-out: cubic-bezier(0.165, 0.84, 0.44, 1); 15 | $quint-ease-out: cubic-bezier(0.23, 1, 0.32, 1); 16 | $sine-ease-out: cubic-bezier(0.39, 0.575, 0.565, 1); 17 | $expo-ease-out: cubic-bezier(0.19, 1, 0.22, 1); 18 | $circ-ease-out: cubic-bezier(0.075, 0.82, 0.165, 1); 19 | $back-ease-out: cubic-bezier(0.175, 0.885, 0.32, 1.275); 20 | 21 | $quad-ease-in-out: cubic-bezier(0.455, 0.03, 0.515, 0.955); 22 | $cubic-ease-in-out: cubic-bezier(0.645, 0.045, 0.355, 1); 23 | $quart-ease-in-out: cubic-bezier(0.77, 0, 0.175, 1); 24 | $quint-ease-in-out: cubic-bezier(0.86, 0, 0.07, 1); 25 | $sine-ease-in-out: cubic-bezier(0.445, 0.05, 0.55, 0.95); 26 | $expo-ease-in-out: cubic-bezier(1, 0, 0, 1); 27 | $circ-ease-in-out: cubic-bezier(0.785, 0.135, 0.15, 0.86); 28 | 29 | $cubic-out-transition: 0.3s $cubic-ease-out; 30 | $default-inout-transition: 0.5s ease-in-out; 31 | -------------------------------------------------------------------------------- /src/css/components/_footer.scss: -------------------------------------------------------------------------------- 1 | // Global Footer 2 | #footer { 3 | } 4 | -------------------------------------------------------------------------------- /src/css/components/_header.scss: -------------------------------------------------------------------------------- 1 | #main-nav { 2 | position: absolute; 3 | z-index: 99; 4 | 5 | padding-top: $nav-padding - 1px; 6 | padding-left: $nav-padding; 7 | 8 | .menu { 9 | li { 10 | border-bottom: 1px solid transparent; 11 | &.active { 12 | border-bottom: 1px solid black; 13 | } 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/css/components/_page.scss: -------------------------------------------------------------------------------- 1 | .page-wrapper { 2 | opacity: 0; 3 | padding-top: 200px; 4 | } 5 | 6 | .next-page { 7 | position: absolute; 8 | top: 0; 9 | right: 0; 10 | bottom: 0; 11 | left: 0; 12 | height: 100%; 13 | overflow: hidden; 14 | } 15 | -------------------------------------------------------------------------------- /src/data/404.json: -------------------------------------------------------------------------------- 1 | { 2 | "meta": { 3 | "title": "404", 4 | "description": "Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmodproident, sunt in culpa qui officia deserunt mollit anim id est laborum." 5 | }, 6 | "content": { 7 | "title": "Sorry this page doesn't exist" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/data/about.json: -------------------------------------------------------------------------------- 1 | { 2 | "meta": { 3 | "title": "About title" 4 | }, 5 | "content": { 6 | "title": "Hello World About" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/data/index.json: -------------------------------------------------------------------------------- 1 | { 2 | "meta": { 3 | "title": "Local Title for Meta", 4 | "description": "Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmodproident, sunt in culpa qui officia deserunt mollit anim id est laborum." 5 | }, 6 | "content": { 7 | "title": "Hello World Index", 8 | "number": 2 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/scripts/abstract/base.js: -------------------------------------------------------------------------------- 1 | import store from 'store'; 2 | import { storeWatcher } from 'store/storeWatcher.js'; 3 | /** 4 | * Component: Defines a component with basic methods 5 | * @constructor 6 | */ 7 | class Base { 8 | set promises(newPromises) { 9 | if (!this._promises) this._promises = {}; 10 | for (const promise in newPromises) { 11 | if ({}.hasOwnProperty.call(newPromises, promise)) { 12 | this._promises[promise] = newPromises[promise]; 13 | } 14 | } 15 | } 16 | 17 | get promises() { 18 | return this._promises; 19 | } 20 | 21 | set state(state) { 22 | if (!this._state) this._state = {}; 23 | for (const subState in state) { 24 | if ({}.hasOwnProperty.call(state, subState)) { 25 | this._state[subState] = state[subState]; 26 | } 27 | } 28 | } 29 | 30 | get state() { 31 | return this._state; 32 | } 33 | 34 | set storeEvents(storeEvents) { 35 | for (const objectPath in storeEvents) { 36 | if (!storeEvents[objectPath]) continue; 37 | this._storeEvents[objectPath] = storeEvents[objectPath]; 38 | } 39 | this.subscribe(); 40 | } 41 | 42 | get storeEvents() { 43 | return this._storeEvents; 44 | } 45 | 46 | constructor() { 47 | /** 48 | * Object as associative array of all the objects 49 | * @type {Object} 50 | */ 51 | this._promises = {}; 52 | 53 | /** 54 | * Object as associative array of all objects 55 | * @type {Object} 56 | */ 57 | this._storeEvents = {}; 58 | 59 | /** 60 | * Object as associative array of all objects 61 | * @type {Object} 62 | */ 63 | this.subscriptions = {}; 64 | 65 | this.promises = { 66 | init: { 67 | resolve: null, 68 | reject: null, 69 | }, 70 | }; 71 | 72 | this.state = { 73 | isInit: false, 74 | }; 75 | } 76 | 77 | /** 78 | * Init 79 | * @return {Promise} A Promise the component is init 80 | */ 81 | init() { 82 | return new Promise((resolve, reject) => { 83 | this.promises.init.resolve = resolve; 84 | this.promises.init.reject = reject; 85 | 86 | const { isInit } = this.state; 87 | 88 | if (isInit) { 89 | this.promises.init.reject(); 90 | return; 91 | } 92 | 93 | this.initComponent(); 94 | }); 95 | } 96 | 97 | initComponent() { 98 | this.onInit(); 99 | } 100 | 101 | /** 102 | * Once the component is init 103 | */ 104 | onInit() { 105 | this.setState({ isInit: true }); 106 | this.promises.init.resolve(); 107 | } 108 | 109 | setState(partialState = {}, callback, needRender = false) { 110 | if (typeof partialState !== 'object' && typeof partialState !== 'function' && partialState !== null) { 111 | console.error( 112 | 'setState(...): takes an object of state variables to update or a ' + 113 | 'function which returns an object of state variables.' 114 | ); 115 | return; 116 | } 117 | const prevState = Object.assign({}, this.state); 118 | this.state = { ...this.state, ...partialState }; 119 | 120 | if (callback) callback(); 121 | if (needRender) this.render(); 122 | this.stateDidUpdate(this.state, prevState); 123 | } 124 | 125 | stateDidUpdate(state, oldState) {} 126 | 127 | subscribe(o) { 128 | // When an object is givin for a specific subscription 129 | if (o) { 130 | if (this.subscriptions[o.path]) { 131 | // To unsubscribe the change listener, invoke the function returned by subscribe. 132 | this.subscriptions[o.path](); 133 | } 134 | 135 | let method = o.cb; 136 | 137 | if (typeof method !== 'function') method = this[method]; 138 | if (!method) return; 139 | 140 | this._storeEvents[o.path] = method; 141 | 142 | const watcher = storeWatcher(store.getState, path); 143 | this.subscriptions[path] = store.subscribe(() => watcher(method)); 144 | 145 | return; 146 | } 147 | 148 | for (const path in this.storeEvents) { 149 | if ({}.hasOwnProperty.call(this.storeEvents, path)) { 150 | if (!this.storeEvents[path]) continue; 151 | // To unsubscribe the change listener, invoke the function returned by subscribe. 152 | if (this.subscriptions[path]) this.subscriptions[path](); 153 | 154 | let method = this.storeEvents[path]; 155 | 156 | if (typeof method !== 'function') method = this[method]; 157 | if (!method) continue; 158 | 159 | const watcher = storeWatcher(store.getState, path); 160 | this.subscriptions[path] = store.subscribe(watcher(method)); 161 | } 162 | } 163 | } 164 | 165 | unsubscribe(path_ = null) { 166 | if (path_) { 167 | if (this.subscriptions[path_]) { 168 | this.subscriptions[path_](); 169 | delete this.subscriptions[path_]; 170 | } 171 | return; 172 | } 173 | 174 | for (const path in this.subscriptions) { 175 | if (!this.subscriptions[path]) continue; 176 | // To unsubscribe the change listener, invoke the function returned by subscribe. 177 | // Ex : let unsubscribe = store.subscribe(handleChange) 178 | this.subscriptions[path](); 179 | } 180 | this.subscriptions = {}; 181 | } 182 | 183 | dispatch(action) { 184 | store.dispatch(action); 185 | } 186 | 187 | dispose() { 188 | this.unsubscribe(); 189 | } 190 | 191 | resize() {} 192 | } 193 | 194 | export default Base; 195 | -------------------------------------------------------------------------------- /src/scripts/abstract/component.js: -------------------------------------------------------------------------------- 1 | import store from 'store'; 2 | import Base from './base.js'; 3 | import uniqueId from 'utils/uniqueId.js'; 4 | 5 | /** 6 | * Component: Defines a component with basic methods 7 | * @constructor 8 | */ 9 | 10 | class Component extends Base { 11 | constructor(props) { 12 | super(props); 13 | 14 | /** 15 | * Object as associative array of all the objects 16 | * @type {Object} 17 | */ 18 | this.handlers = {}; 19 | 20 | /** 21 | * Object as associative array of all the objects 22 | * @type {Object} 23 | */ 24 | this.promises = { 25 | show: { 26 | resolve: null, 27 | reject: null, 28 | }, 29 | hidden: { 30 | resolve: null, 31 | reject: null, 32 | }, 33 | }; 34 | 35 | /** 36 | * Object as associative array of all the timelines 37 | * @type {Object} 38 | */ 39 | this.TL = {}; 40 | 41 | /** 42 | * Object as associative array of all the timers 43 | * Meant to be used with gsap delayedCall timers. 44 | * @type {Object} 45 | */ 46 | this.timers = {}; 47 | 48 | /** 49 | * uniqueId 50 | * @type {String} 51 | */ 52 | this.cid = uniqueId('component'); 53 | 54 | this.props = props; 55 | this.state = { 56 | canUpdate: false, 57 | isAnimating: false, 58 | isShown: false, 59 | }; 60 | 61 | /** 62 | * El 63 | * If el is passed from parent, this means the DOM is already render 64 | and we just need to scope it 65 | * @type {DOM} 66 | */ 67 | this.el = props.el ? props.el : null; 68 | this.template = props.template ? props.template : null; 69 | this.data = props.data ? props.data : this.data; 70 | } 71 | 72 | /** 73 | * Init the component. 74 | * Override and trigger onInit when we have to wait for computer processing, like canvas initialization for instance. 75 | */ 76 | initComponent() { 77 | this.render(); 78 | } 79 | 80 | /** 81 | * Call render function if you wanna change the component 82 | * based on state/data 83 | */ 84 | render() { 85 | // Default components just need to scope a piece of DOM from constructor 86 | this.setElement(); 87 | setTimeout(() => this.onRender(), 0); 88 | } 89 | 90 | /** 91 | * Render your component 92 | * This is where we scope the main elements 93 | */ 94 | setElement() { 95 | if (this.el === null && this.template === null) { 96 | console.error('You must provide a template or an el to scope a component. Creating an empty div instead'); 97 | this.el = document.createElement('div'); 98 | } 99 | 100 | if (this.el !== null) { 101 | return; 102 | } 103 | 104 | if (this.template !== null) { 105 | this.renderTemplate(); 106 | return; 107 | } 108 | } 109 | 110 | /** 111 | * Render your template 112 | */ 113 | renderTemplate() { 114 | const html = this.template({ data: this.data }); 115 | 116 | // String to DOM Element 117 | let wrapper = document.createElement('div'); 118 | wrapper.innerHTML = html; 119 | this.el = wrapper.firstChild; 120 | } 121 | 122 | onRender() { 123 | if (this.el) { 124 | const links = this.el.querySelectorAll('a'); 125 | links.forEach((link) => link.addEventListener('click', this.handleHyperLink)); 126 | } 127 | 128 | this.initDOM(); 129 | this.setupDOM(); 130 | this.initTL(); 131 | setTimeout(() => this.onDOMInit(), 0); 132 | } 133 | 134 | /** 135 | * Init all your DOM elements here 136 | */ 137 | initDOM() {} 138 | 139 | /** 140 | * Setup your DOM elements here ( for example defaut style before animation ) 141 | */ 142 | setupDOM() {} 143 | 144 | /** 145 | * Init the Timeline here 146 | */ 147 | initTL() {} 148 | 149 | onDOMInit() { 150 | this.bindEvents(); 151 | this.onInit(); 152 | this.setState({ 153 | canUpdate: true, 154 | }); 155 | } 156 | 157 | /** 158 | * Bind your events here 159 | */ 160 | bindEvents() {} 161 | 162 | /** 163 | * Unbind your events here 164 | */ 165 | unbindEvents() {} 166 | 167 | /** 168 | * Update 169 | * 170 | */ 171 | update() { 172 | if (this.state.canUpdate) this.onUpdate(); 173 | } 174 | 175 | /** 176 | * Called on update 177 | */ 178 | onUpdate() {} 179 | 180 | /** 181 | * Show the component 182 | */ 183 | show() { 184 | return new Promise((resolve, reject) => { 185 | this.promises.show.resolve = resolve; 186 | this.promises.show.reject = reject; 187 | this.setState({ 188 | isAnimating: true, 189 | }); 190 | this.showComponent(); 191 | }); 192 | } 193 | 194 | showComponent() { 195 | this.onShown(); 196 | } 197 | 198 | /** 199 | * The component is shown 200 | */ 201 | onShown() { 202 | this.setState({ 203 | isShown: true, 204 | isAnimating: false, 205 | }); 206 | this.promises.show.resolve(); 207 | } 208 | 209 | /** 210 | * Hide the component 211 | */ 212 | hide() { 213 | return new Promise((resolve, reject) => { 214 | this.promises.hidden.resolve = resolve; 215 | this.promises.hidden.reject = reject; 216 | this.setState({ 217 | isAnimating: true, 218 | }); 219 | this.hideComponent(); 220 | }); 221 | } 222 | 223 | hideComponent() { 224 | this.onHidden(); 225 | } 226 | 227 | /** 228 | * The component is shown 229 | */ 230 | onHidden() { 231 | this.setState({ 232 | isAnimating: false, 233 | isShown: false, 234 | canUpdate: false, 235 | }); 236 | this.promises.hidden.resolve(); 237 | } 238 | 239 | handleHyperLink = (e) => this.hyperlink(e); 240 | 241 | hyperlink(e) { 242 | const isAnimating = store.getState().app.isAnimating; 243 | if (isAnimating) { 244 | e.preventDefault(); 245 | } 246 | } 247 | 248 | /** 249 | * Kill a timeline by name 250 | * @param {string} name of the timeline stocked in this.TL. 251 | */ 252 | killTL(name) { 253 | if (this.TL[name] === undefined || this.TL[name] === null) return; 254 | 255 | let tl = this.TL[name]; 256 | 257 | tl.pause(); 258 | tl.kill(); 259 | tl.clear(); 260 | tl = null; 261 | 262 | this.TL[name] = null; 263 | } 264 | 265 | /** 266 | * Kill all the timelines 267 | */ 268 | destroyTL() { 269 | for (const name in this.TL) { 270 | if (this.TL[name]) this.killTL(name); 271 | } 272 | this.TL = {}; 273 | } 274 | 275 | /** 276 | * Kill all the timers 277 | * When using gsap.delayedCall() 278 | */ 279 | destroyTimers() { 280 | for (const name in this.timers) { 281 | if (this.timers[name]) this.timers[name].kill(); 282 | } 283 | this.timers = {}; 284 | } 285 | 286 | /** 287 | * Dispose the component 288 | */ 289 | dispose() { 290 | this.setState({ 291 | isInit: false, 292 | isShown: false, 293 | canUpdate: false, 294 | }); 295 | this.unbindEvents(); 296 | 297 | this.handlers = {}; 298 | this.promises = {}; 299 | 300 | this.destroyTL(); 301 | this.destroyTimers(); 302 | 303 | const links = this.el.querySelectorAll('a'); 304 | links.forEach((link) => link.removeEventListener('click', this.handleHyperLink)); 305 | 306 | this.el.parentNode.removeChild(this.el); 307 | this.el = null; 308 | super.dispose(); 309 | } 310 | } 311 | 312 | export default Component; 313 | -------------------------------------------------------------------------------- /src/scripts/abstract/page.js: -------------------------------------------------------------------------------- 1 | import AbstractDOMComponent from 'abstract/component.js'; 2 | import { gsap, Cubic } from 'gsap'; 3 | import store from 'store'; 4 | 5 | import { JSON_ENDPOINTS } from 'constants/config.js'; 6 | import { getRoute } from './../app/selectors.js'; 7 | import Cache from './../app/Cache.js'; 8 | 9 | // Actions 10 | import { setMeta } from './../app/actions.js'; 11 | 12 | /** 13 | * PageComponent: Defines a page 14 | * @extends AbstractDOMComponent 15 | * @constructor 16 | */ 17 | class PageComponent extends AbstractDOMComponent { 18 | constructor(props) { 19 | super(props); 20 | this.type = props.type || 'default'; 21 | /** 22 | * Data Object 23 | * @type {Object} 24 | */ 25 | this.data = props.data ? props.data : {}; 26 | 27 | this.promises = { 28 | data: { 29 | resolve: null, 30 | reject: null, 31 | }, 32 | }; 33 | } 34 | 35 | initComponent() { 36 | this.getData().then(() => { 37 | this.initData(); 38 | super.initComponent(); 39 | }); 40 | } 41 | 42 | getData() { 43 | return new Promise((resolve, reject) => { 44 | this.promises.data.resolve = resolve; 45 | this.promises.data.reject = reject; 46 | 47 | this.fetchData(); 48 | }); 49 | } 50 | 51 | initData() { 52 | if (this.data.meta) { 53 | store.dispatch(setMeta(this.data.meta)); 54 | } 55 | this.props.data = this.data; 56 | } 57 | 58 | fetchData() { 59 | const endPoint = this.props.endPoint; 60 | 61 | if (!endPoint) { 62 | this.promises.data.resolve(); 63 | return; 64 | } 65 | 66 | const { oldPage, location } = store.getState().app; 67 | const currentRoute = getRoute(location); 68 | 69 | if (oldPage) { 70 | const url = JSON_ENDPOINTS + this.props.endPoint; 71 | 72 | const data = Cache.has(currentRoute.id) ? Cache.getData(currentRoute.id) : null; 73 | if (data) { 74 | if (Promise.resolve(data) == data) { 75 | data.then(() => { 76 | this.setData(Cache.getData(currentRoute.id)); 77 | }); 78 | } else { 79 | this.setData(data); 80 | } 81 | } else { 82 | fetch(url) 83 | .then((response) => { 84 | return response.json(); 85 | }) 86 | .then((json) => { 87 | this.setData(json); 88 | Cache.set(currentRoute.id, json); 89 | }) 90 | .catch((ex) => { 91 | this.promises.data.reject(); 92 | }); 93 | } 94 | } else { 95 | const { globalData } = store.getState().app; 96 | if (globalData && globalData.serverData) { 97 | this.setData(globalData.serverData); 98 | // Save server data in the Cache 99 | Cache.set(currentRoute.id, globalData.serverData); 100 | } else { 101 | // probably not needed since serverData will always be defined from the server side 102 | this.promises.data.reject(); 103 | } 104 | } 105 | } 106 | 107 | setData(data) { 108 | this.data = data; 109 | this.promises.data.resolve(); 110 | } 111 | 112 | setupDOM() { 113 | gsap.set(this.el, { autoAlpha: 0 }); 114 | } 115 | 116 | initTL() { 117 | this.TL.show = new gsap.timeline({ paused: true, onComplete: () => this.onShown() }); 118 | this.TL.show.to(this.el, 0.3, { autoAlpha: 1, ease: Cubic.easeOut }); 119 | 120 | this.TL.hide = new gsap.timeline({ paused: true, onComplete: () => this.onHidden() }); 121 | this.TL.hide.to(this.el, 0.3, { autoAlpha: 0, ease: Cubic.easeOut }); 122 | } 123 | 124 | onDOMInit() { 125 | // append to main container 126 | const { oldPage } = store.getState().app; 127 | if (oldPage) { 128 | this.el.classList.add('next-page'); 129 | } 130 | document.getElementById('content').appendChild(this.el); 131 | super.onDOMInit(); 132 | } 133 | 134 | showComponent() { 135 | setTimeout(() => { 136 | this.TL.show.play(0); 137 | }, 0); 138 | } 139 | 140 | hideComponent() { 141 | setTimeout(() => { 142 | this.TL.hide.play(0); 143 | }, 0); 144 | } 145 | } 146 | 147 | export default PageComponent; 148 | -------------------------------------------------------------------------------- /src/scripts/app/App.js: -------------------------------------------------------------------------------- 1 | import store from 'store'; 2 | 3 | // Abstract 4 | import Base from 'abstract/base.js'; 5 | 6 | // Containers 7 | import Layout from 'layout/Layout.js'; 8 | 9 | // Selector 10 | import { getRoute } from './selectors.js'; 11 | 12 | // Actions 13 | import { setAnimating, setPage, setOldPage, setGlobalData } from './actions.js'; 14 | 15 | function pageLoader(chunkName, path) { 16 | return new Promise((resolve, reject) => { 17 | const ComponentName = path.charAt(0).toUpperCase() + path.slice(1); 18 | switch (ComponentName) { 19 | case 'Homepage': 20 | return import(/* webpackPrefetch: true */ /* webpackChunkName: "Homepage" */ 'pages/homepage/Homepage.js') 21 | .then(({ default: Page }) => { 22 | resolve(Page); 23 | }) 24 | .catch((error) => reject('An error occurred while loading the component')); 25 | case 'About': 26 | return import(/* webpackPrefetch: true */ /* webpackChunkName: "About" */ 'pages/about/About.js') 27 | .then(({ default: Page }) => { 28 | resolve(Page); 29 | }) 30 | .catch((error) => reject('An error occurred while loading the component')); 31 | case 'Notfound': 32 | return import(/* webpackPrefetch: true */ /* webpackChunkName: "Notfound" */ 'pages/notfound/Notfound.js') 33 | .then(({ default: Page }) => { 34 | resolve(Page); 35 | }) 36 | .catch((error) => reject('An error occurred while loading the component')); 37 | } 38 | }); 39 | } 40 | 41 | class App extends Base { 42 | constructor(props) { 43 | super(props); 44 | 45 | this.prevLocation = null; 46 | this.location = null; 47 | this.layout = null; 48 | // this.loader = null; 49 | 50 | this.page = null; 51 | this.oldPage = null; 52 | 53 | this.storeEvents = { 54 | 'app.location': (location, prevLocation) => this.onLocationChanged(location, prevLocation), 55 | }; 56 | } 57 | 58 | init() { 59 | // grab server data 60 | const data = JSON.parse(document.getElementById('__PLATO_DATA__').innerHTML); 61 | 62 | if (data) { 63 | store.dispatch(setGlobalData(data)); 64 | } 65 | this.layout = new Layout({ el: document.body }); 66 | 67 | // return layout promise 68 | return this.layout.init(); 69 | } 70 | 71 | onLocationChanged(location, prevLocation) { 72 | this.prevLocation = prevLocation; 73 | if (location !== prevLocation) { 74 | this.location = location; 75 | this.routing(location, false); 76 | } 77 | } 78 | 79 | async routing(location) { 80 | console.log('-------------- routing ---------------', location); 81 | 82 | let Page = null; 83 | const currentRoute = getRoute(location); 84 | 85 | try { 86 | // we use template to define page Type 87 | const PageAsync = await pageLoader(currentRoute.template, currentRoute.template); 88 | Page = PageAsync; 89 | } catch (error) { 90 | console.error(error); 91 | } 92 | 93 | store.dispatch(setAnimating(true)); 94 | 95 | // First Render from the server 96 | let el = null; 97 | if (this.oldPage === null && this.page === null) { 98 | el = document.getElementsByClassName('page-wrapper')[0]; 99 | } 100 | 101 | // SAFETY HERE 102 | // IF THERE IS STILL AN OLD PAGE HERE IT MEANS SOMETHING BEEN WRONG 103 | // SO WE JUST KILL IT 104 | // USUALLY HAPPENS IF USER PLAY WITH BROWSER BACK ARROWS WHILE ANIMATING 105 | if (this.oldPage) { 106 | this.oldPage.dispose(); 107 | this.oldPage = null; 108 | this.page.dispose(); 109 | this.page = null; 110 | console.log('KILL PAGE !!!!!!'); 111 | 112 | // This will just reload the page actually. 113 | window.location.assign(window.location.href); 114 | } 115 | 116 | if (this.page) { 117 | this.oldPage = this.page; 118 | store.dispatch(setOldPage(this.oldPage)); 119 | } 120 | 121 | // Define first page and pass el if the page el is already in the dom 122 | this.page = new Page({ 123 | el: el ? el : null, 124 | endPoint: currentRoute && currentRoute.json ? currentRoute.json : null, 125 | type: currentRoute && currentRoute.template ? currentRoute.template : null, 126 | }); 127 | 128 | // Init the next page now 129 | this.page.init().then(() => { 130 | store.dispatch(setPage(this.page)); 131 | 132 | // Resize the current page for position 133 | this.layout.triggerResize(); 134 | 135 | if (this.oldPage) { 136 | this.hidePage(); 137 | } else { 138 | this.showPage(); 139 | } 140 | }); 141 | } 142 | 143 | showPage() { 144 | // Show next 145 | this.page.show().then(() => { 146 | store.dispatch(setAnimating(false)); 147 | 148 | // at this point, dispose 149 | if (this.oldPage) { 150 | this.oldPage.dispose(); 151 | this.oldPage = null; 152 | } 153 | }); 154 | } 155 | 156 | hidePage() { 157 | this.oldPage.hide().then(() => { 158 | this.page.el.classList.remove('next-page'); 159 | this.oldPage.el.classList.add('next-page'); 160 | this.oldPage.dispose(); 161 | this.oldPage = null; 162 | this.showPage(); 163 | }); 164 | } 165 | } 166 | 167 | export default App; 168 | -------------------------------------------------------------------------------- /src/scripts/app/Cache.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Cache for storing URL / DATA. 3 | */ 4 | 5 | class Cache { 6 | constructor() { 7 | this.cache = new Map(); 8 | } 9 | 10 | /** 11 | * Set value to cache 12 | */ 13 | set(href, data) { 14 | this.cache.set(href, { 15 | data, 16 | }); 17 | return { 18 | data, 19 | }; 20 | } 21 | 22 | /** 23 | * Get data from cache 24 | */ 25 | get(href) { 26 | return this.cache.get(href); 27 | } 28 | 29 | /** 30 | * Get data from cache 31 | */ 32 | getData(href) { 33 | return this.cache.get(href).data; 34 | } 35 | 36 | /** 37 | * Check if value exists into cache 38 | */ 39 | has(href) { 40 | console.log('href', href); 41 | return this.cache.has(href); 42 | } 43 | 44 | /** 45 | * Delete value from cache 46 | */ 47 | delete(href) { 48 | return this.cache.delete(href); 49 | } 50 | 51 | /** 52 | * Update cache value 53 | */ 54 | update(href, data) { 55 | const state = { 56 | ...this.cache.get(href), 57 | ...data, 58 | }; 59 | this.cache.set(href, state); 60 | 61 | return state; 62 | } 63 | } 64 | 65 | const cache = new Cache(); 66 | export default cache; 67 | -------------------------------------------------------------------------------- /src/scripts/app/actions.js: -------------------------------------------------------------------------------- 1 | import { 2 | SET_ROUTES, 3 | NAVIGATION, 4 | SET_ANIMATING, 5 | SET_META, 6 | SET_PAGE, 7 | SET_OLDPAGE, 8 | SET_DEVICE_TYPE, 9 | SET_GLOBAL_DATA, 10 | } from './constants.js'; 11 | 12 | export function navigate(location, params = {}) { 13 | return { 14 | type: NAVIGATION, 15 | location: location, 16 | params: params, 17 | }; 18 | } 19 | 20 | export function setMeta(meta = null, isDefault = false) { 21 | return { 22 | type: SET_META, 23 | meta, 24 | isDefault, 25 | }; 26 | } 27 | 28 | export function setRoutes(routes) { 29 | return { 30 | type: SET_ROUTES, 31 | routes, 32 | }; 33 | } 34 | 35 | export function setAnimating(animatingState) { 36 | return { 37 | type: SET_ANIMATING, 38 | isAnimating: animatingState, 39 | }; 40 | } 41 | 42 | export function setPage(page) { 43 | return { 44 | type: SET_PAGE, 45 | page: { 46 | type: page.type, 47 | data: page.data, 48 | }, 49 | }; 50 | } 51 | 52 | export function setOldPage(page) { 53 | return { 54 | type: SET_OLDPAGE, 55 | page: { 56 | type: page.type, 57 | data: page.data, 58 | }, 59 | }; 60 | } 61 | 62 | export function setDeviceType(deviceType) { 63 | return { 64 | type: SET_DEVICE_TYPE, 65 | deviceType, 66 | }; 67 | } 68 | 69 | export function setGlobalData(data) { 70 | return { 71 | type: SET_GLOBAL_DATA, 72 | data, 73 | }; 74 | } 75 | -------------------------------------------------------------------------------- /src/scripts/app/constants.js: -------------------------------------------------------------------------------- 1 | // ACTION 2 | export const NAVIGATION = 'NAVIGATION'; 3 | export const SET_ROUTES = 'SET_ROUTES'; 4 | export const SET_META = 'SET_META'; 5 | export const SET_ANIMATING = 'SET_ANIMATING'; 6 | export const SET_PAGE = 'SET_PAGE'; 7 | export const SET_OLDPAGE = 'SET_OLDPAGE'; 8 | export const SET_DEVICE_TYPE = 'SET_DEVICE_TYPE'; 9 | export const SET_GLOBAL_DATA = 'SET_GLOBAL_DATA'; 10 | -------------------------------------------------------------------------------- /src/scripts/app/reducers.js: -------------------------------------------------------------------------------- 1 | import { 2 | SET_ROUTES, 3 | SET_PAGE, 4 | SET_OLDPAGE, 5 | NAVIGATION, 6 | SET_ANIMATING, 7 | SET_META, 8 | SET_DEVICE_TYPE, 9 | SET_GLOBAL_DATA, 10 | } from './constants.js'; 11 | 12 | const InitialState = { 13 | routes: [], 14 | params: null, 15 | location: null, 16 | isAnimating: false, 17 | // This is updated when the page is added to the DOM 18 | page: null, 19 | oldPage: null, 20 | meta: {}, 21 | deviceType: null, 22 | globalData: {}, 23 | }; 24 | 25 | // Updates an entity cache in response to any action with response.entities. 26 | export const app = (state = InitialState, action) => { 27 | switch (action.type) { 28 | case SET_META: { 29 | let meta = action.meta !== null ? action.meta : {}; 30 | // if no meta from data we used default one 31 | if (action.meta === null) { 32 | meta.title = 'DEFAULT_META_TITLE'; 33 | meta.description = 'DEFAULT_META_DESCRIPTION'; 34 | } 35 | 36 | return { 37 | ...state, 38 | meta: meta, 39 | }; 40 | } 41 | 42 | case SET_ROUTES: { 43 | return { 44 | ...state, 45 | routes: action.routes, 46 | }; 47 | } 48 | case NAVIGATION: { 49 | return { 50 | ...state, 51 | params: action.params, 52 | location: action.location, 53 | }; 54 | } 55 | case SET_ANIMATING: { 56 | return { 57 | ...state, 58 | isAnimating: action.isAnimating, 59 | }; 60 | } 61 | case SET_PAGE: { 62 | return { 63 | ...state, 64 | page: action.page, 65 | }; 66 | } 67 | case SET_OLDPAGE: { 68 | return { 69 | ...state, 70 | oldPage: action.page, 71 | }; 72 | } 73 | case SET_DEVICE_TYPE: { 74 | return { 75 | ...state, 76 | deviceType: action.deviceType, 77 | }; 78 | } 79 | case SET_GLOBAL_DATA: { 80 | return { 81 | ...state, 82 | globalData: action.data, 83 | }; 84 | } 85 | default: { 86 | return state; 87 | } 88 | } 89 | }; 90 | 91 | export default app; 92 | -------------------------------------------------------------------------------- /src/scripts/app/selectors.js: -------------------------------------------------------------------------------- 1 | import store from 'store'; 2 | 3 | export const getRoute = (location, params = null) => { 4 | const routes = store.getState().app.routes; 5 | let currentRoute = null; 6 | 7 | for (let key in routes) { 8 | if (!routes.hasOwnProperty(key)) continue; 9 | 10 | let route = routes[key]; 11 | if (route.id === location) currentRoute = route; 12 | } 13 | 14 | return currentRoute; 15 | }; 16 | 17 | export const getRouteByURL = (href) => { 18 | const routes = store.getState().app.routes; 19 | 20 | let currentRoute = null; 21 | for (let key in routes) { 22 | if (!routes.hasOwnProperty(key)) continue; 23 | 24 | let route = routes[key]; 25 | if (route.url === href) { 26 | currentRoute = route; 27 | break; 28 | } 29 | } 30 | 31 | return currentRoute; 32 | }; 33 | -------------------------------------------------------------------------------- /src/scripts/components/header/Header.js: -------------------------------------------------------------------------------- 1 | import AbstractDOMComponent from 'abstract/component.js'; 2 | 3 | class Header extends AbstractDOMComponent { 4 | constructor(props) { 5 | super(props); 6 | 7 | this.storeEvents = { 8 | 'app.location': (location, prevLocation) => this.setActiveLink(location, prevLocation), 9 | }; 10 | } 11 | 12 | initDOM() { 13 | this.$logo = this.el.querySelector('.logo'); 14 | this.$navItems = this.el.querySelectorAll('.menu li a'); 15 | } 16 | 17 | bindEvents() { 18 | this.$logo.addEventListener('click', this.handleClickLogo); 19 | } 20 | 21 | unbindEvents() { 22 | this.$logo.removeEventListener('click', this.handleClickLogo); 23 | } 24 | 25 | handleClickLogo = () => { 26 | console.log('clickHome'); 27 | }; 28 | 29 | setActiveLink(location) { 30 | this.resetCurrentNavItem(); 31 | 32 | let $currentNavItem = null; 33 | [...this.$navItems].forEach((navItem) => { 34 | if (navItem.dataset.page === location) $currentNavItem = navItem.parentNode; 35 | }); 36 | 37 | // if no nav item SKIP. this would happen when rendering legacy and 404. 38 | if ($currentNavItem === null) return; 39 | 40 | $currentNavItem.classList.add('active'); 41 | } 42 | 43 | resetCurrentNavItem() { 44 | [...this.$navItems].forEach((navItem) => { 45 | navItem.parentNode.classList.remove('active'); 46 | }); 47 | } 48 | } 49 | 50 | export default Header; 51 | -------------------------------------------------------------------------------- /src/scripts/components/header/actions.js: -------------------------------------------------------------------------------- 1 | import { SHOW_HEADER, HIDE_HEADER } from './constants.js'; 2 | 3 | export function showHeader() { 4 | return { 5 | type: SHOW_HEADER, 6 | }; 7 | } 8 | 9 | export function hideHeader() { 10 | return { 11 | type: HIDE_HEADER, 12 | }; 13 | } 14 | -------------------------------------------------------------------------------- /src/scripts/components/header/constants.js: -------------------------------------------------------------------------------- 1 | // ACTION 2 | export const SHOW_HEADER = 'SHOW_HEADER'; 3 | export const HIDE_HEADER = 'HIDE_HEADER'; 4 | -------------------------------------------------------------------------------- /src/scripts/components/header/reducers.js: -------------------------------------------------------------------------------- 1 | import { SHOW_HEADER, HIDE_HEADER } from './constants.js'; 2 | 3 | const InitialState = { 4 | isShown: true, 5 | }; 6 | 7 | // Updates an entity cache in response to any action with response.entities. 8 | export const header = (state = InitialState, action) => { 9 | switch (action.type) { 10 | case SHOW_HEADER: { 11 | return { 12 | ...state, 13 | isShown: true, 14 | }; 15 | } 16 | case HIDE_HEADER: { 17 | return { 18 | ...state, 19 | isShown: false, 20 | }; 21 | } 22 | default: { 23 | return state; 24 | } 25 | } 26 | }; 27 | 28 | export default header; 29 | -------------------------------------------------------------------------------- /src/scripts/constants/config.js: -------------------------------------------------------------------------------- 1 | export const JSON_ENDPOINTS = '/data/'; 2 | -------------------------------------------------------------------------------- /src/scripts/constants/langs.js: -------------------------------------------------------------------------------- 1 | // ACTION 2 | export const LANGS = ['en-US']; 3 | -------------------------------------------------------------------------------- /src/scripts/index.js: -------------------------------------------------------------------------------- 1 | import { isMobile, isTablet } from 'utils/is.js'; 2 | import App from 'app/App.js'; 3 | import Router from 'router.js'; 4 | 5 | import { setDeviceType } from 'app/actions.js'; 6 | import store from 'store'; 7 | 8 | import { cleanURL } from 'utils/cleanURL.js'; 9 | import { loadScript, browserSupportsAllFeatures } from 'utils/loadScript.js'; 10 | 11 | import './../css/app.scss'; 12 | 13 | // add Art Template filters 14 | import runtime from 'art-template/lib/runtime.js'; 15 | import { filters } from 'templates/filters/index.js'; 16 | for (let [key, value] of Object.entries(filters)) { 17 | runtime[key] = value; 18 | } 19 | 20 | class Entry { 21 | constructor() { 22 | console.log('--- APP STARTED ---'); 23 | console.log('\n\n\n'); 24 | this.app = null; 25 | } 26 | 27 | init() { 28 | const router = Router.configureRoute(); 29 | this.app = new App(); 30 | const root = document.documentElement; 31 | 32 | isMobile() && root.classList.add('isMobile'); 33 | isTablet() && root.classList.add('isTablet'); 34 | 35 | let deviceType = 'desktop'; 36 | if (isMobile()) deviceType = 'mobile'; 37 | if (isTablet()) deviceType = 'tablet'; 38 | 39 | store.dispatch(setDeviceType(deviceType)); 40 | 41 | Router.initRouter().then(() => { 42 | this.app.init().then(() => { 43 | router.start(); 44 | }); 45 | }); 46 | } 47 | } 48 | 49 | (function () { 50 | cleanURL(window.location.href.split('?')[0]); 51 | })(); 52 | 53 | if (browserSupportsAllFeatures()) { 54 | // Browsers that support all features run `main()` immediately. 55 | main(); 56 | } else { 57 | // All other browsers loads polyfills and then run `main()`. 58 | loadScript('https://cdn.jsdelivr.net/npm/whatwg-fetch@3.6.2/dist/fetch.umd.min.js', main); 59 | } 60 | 61 | function main(err) { 62 | // Initiate all other code paths. 63 | // If there's an error loading the polyfills, handle that 64 | // case gracefully and track that the error occurred. 65 | 66 | // initialize the APP do not make a global reference to it. 67 | const entry = new Entry(); 68 | entry.init(); 69 | } 70 | -------------------------------------------------------------------------------- /src/scripts/layout/Layout.js: -------------------------------------------------------------------------------- 1 | import DOMComponent from 'abstract/component.js'; 2 | import store from 'store'; 3 | 4 | // Containers 5 | import Header from 'components/header/Header.js'; 6 | 7 | // Actions 8 | import { setOrientation, calculateResponsiveState } from './actions.js'; 9 | 10 | import { debounce } from 'utils/misc.js'; 11 | import { Prefetch } from './Prefetch.js'; 12 | 13 | class Layout extends DOMComponent { 14 | constructor(props) { 15 | super(props); 16 | 17 | this.header = null; 18 | this.el = document.documentElement; 19 | 20 | this.storeEvents = { 21 | 'app.location': (location, prevLocation) => this.onLocationUpdate(location, prevLocation), 22 | 'app.page': (page) => this.onPageUpdate(page), 23 | 'app.meta': (newVal, oldVal) => this.setMeta(newVal, oldVal), 24 | }; 25 | 26 | this.prefect = new Prefetch(); 27 | } 28 | 29 | initDOM() { 30 | this.$title = this.el.querySelector('head > title'); 31 | this.$metaDescription = this.el.querySelector('head > meta[name=description]'); 32 | 33 | this.$metaOGTitle = this.el.querySelector('head > meta[property="og:title"]'); 34 | this.$metaOGDescription = this.el.querySelector('head > meta[property="og:description"]'); 35 | this.$metaOGImage = this.el.querySelector('head > meta[property="og:image"]'); 36 | this.$metaTwitterTitle = this.el.querySelector('head > meta[name="twitter:title"]'); 37 | this.$metaTwitterDescription = this.el.querySelector('head > meta[name="twitter:description"]'); 38 | this.$metaTwitterImage = this.el.querySelector('head > meta[name="twitter:image"]'); 39 | } 40 | 41 | onDOMInit() { 42 | const aInitPromises = []; 43 | 44 | this.header = new Header({ 45 | el: document.getElementById('main-nav'), 46 | }); 47 | 48 | aInitPromises.push(this.header.init()); 49 | 50 | // scroll top 51 | window.scrollTo(0, 0); 52 | 53 | Promise.all(aInitPromises).then(() => { 54 | super.onDOMInit(); 55 | }); 56 | } 57 | 58 | bindEvents() { 59 | window.addEventListener( 60 | 'orientationchange', 61 | debounce(() => { 62 | this.dispatch(setOrientation(window)); 63 | this.dispatch(calculateResponsiveState(window)); 64 | }, 300), 65 | false 66 | ); 67 | 68 | window.addEventListener( 69 | 'resize', 70 | debounce(() => { 71 | this.dispatch(calculateResponsiveState(window)); 72 | }, 300), 73 | false 74 | ); 75 | 76 | this.dispatch(calculateResponsiveState(window)); 77 | this.prefect.bindPrefetch(); 78 | } 79 | 80 | showComponent() { 81 | if (this.state.isShown) return; 82 | 83 | setTimeout(() => { 84 | super.showComponent(); 85 | }, 0); 86 | } 87 | 88 | triggerResize() { 89 | window.dispatchEvent(new Event('resize')); 90 | } 91 | 92 | setMeta(meta) { 93 | const { oldPage } = store.getState().app; 94 | 95 | if (!oldPage) { 96 | return; 97 | } 98 | 99 | if (meta.title) { 100 | this.$title.textContent = meta.title; 101 | this.$metaOGTitle.setAttribute('content', meta.title); 102 | this.$metaTwitterTitle.setAttribute('content', meta.title); 103 | } 104 | if (meta.description) { 105 | this.$metaDescription.setAttribute('content', meta.description); 106 | this.$metaOGDescription.setAttribute('content', meta.description); 107 | this.$metaTwitterDescription.setAttribute('content', meta.description); 108 | } 109 | if (meta.shareImage) { 110 | this.$metaOGImage.setAttribute('content', meta.shareImage); 111 | this.$metaTwitterImage.setAttribute('content', meta.shareImage); 112 | } 113 | } 114 | 115 | onLocationUpdate(location) { 116 | this.el.setAttribute('location', location); 117 | 118 | // Analytics single app page view 119 | if (window.gtag) { 120 | const routes = store.getState().app.routes; 121 | let pagePath = '/'; 122 | routes.forEach((route) => { 123 | if (route.id === location) pagePath = route.url; 124 | }); 125 | 126 | gtag('config', 'UA-XXX', { 127 | page_title: location, 128 | page_path: pagePath, 129 | }); 130 | } 131 | } 132 | 133 | onPageUpdate(page) { 134 | this.el.setAttribute('type', page.type); 135 | this.prefect && this.prefect.resetPrefetch(); 136 | } 137 | 138 | resize() { 139 | if (!this.state.isShown) return; 140 | } 141 | } 142 | 143 | export default Layout; 144 | -------------------------------------------------------------------------------- /src/scripts/layout/Prefetch.js: -------------------------------------------------------------------------------- 1 | import Cache from 'app/Cache.js'; 2 | import { getPath, clean } from 'utils/url.js'; 3 | import { getRouteByURL } from 'app/selectors.js'; 4 | import { Prevent } from './Prevent.js'; 5 | import { JSON_ENDPOINTS } from 'constants/config.js'; 6 | 7 | export class Prefetch { 8 | constructor() { 9 | this.prevent = new Prevent(); 10 | } 11 | 12 | /** 13 | * Bind Prefetch event listeners. 14 | */ 15 | bindPrefetch() { 16 | this.onLinkEnterHandler = this.onLinkEnter.bind(this); 17 | const links = document.querySelectorAll('a'); 18 | this._links = [...links]; 19 | this._links.forEach((link) => { 20 | link.addEventListener('mouseenter', this.onLinkEnterHandler); 21 | }); 22 | } 23 | 24 | /** 25 | * Unbind Prefetch event listeners. 26 | */ 27 | unbindPrefetch() { 28 | this._links.forEach((link) => { 29 | link.removeEventListener('mouseenter', this.onLinkEnterHandler); 30 | }); 31 | } 32 | 33 | /** 34 | * Reset Prefetch event listeners. 35 | */ 36 | resetPrefetch() { 37 | this.unbindPrefetch(); 38 | this.bindPrefetch(); 39 | } 40 | 41 | /** 42 | * When a element is entered. 43 | */ 44 | onLinkEnter(e) { 45 | const link = this._getLinkElement(e); 46 | if (!link) { 47 | return; 48 | } 49 | 50 | const href = this.getHref(link); 51 | // Already in cache 52 | if (Cache.has(href)) { 53 | return; 54 | } 55 | let path = getPath(clean(href)); 56 | if (path === '') path = '/'; 57 | 58 | if (!path) { 59 | return; 60 | } 61 | 62 | const route = getRouteByURL(path); 63 | 64 | if (!route || !route.json) { 65 | return; 66 | } 67 | 68 | this.prefetch(route.id, JSON_ENDPOINTS + route.json); 69 | } 70 | 71 | /** 72 | * Get a valid link ancestor. 73 | * 74 | * Check for a "href" attribute. 75 | * Then check if eligible. 76 | */ 77 | _getLinkElement(e) { 78 | let el = e.target; 79 | 80 | while (el && !this.getHref(el)) { 81 | el = el.parentNode; 82 | } 83 | 84 | // Check prevent 85 | if (!el || this.prevent.checkLink(el, e, this.getHref(el))) { 86 | return; 87 | } 88 | 89 | return el; 90 | } 91 | 92 | /** 93 | * Get URL from `href` value. 94 | */ 95 | getHref(el) { 96 | // HTML tagName is UPPERCASE, xhtml tagName keeps existing case. 97 | if (el.tagName && el.tagName.toLowerCase() === 'a') { 98 | // HTMLAnchorElement, full URL available 99 | if (typeof el.href === 'string') { 100 | return el.href; 101 | } 102 | } 103 | return null; 104 | } 105 | 106 | /** 107 | * Prefetch a page. 108 | */ 109 | prefetch(href, JSONUrl) { 110 | // Already in cache 111 | if (Cache.has(href)) { 112 | return; 113 | } 114 | Cache.set( 115 | href, 116 | this.request(JSONUrl) 117 | .then((data) => { 118 | Cache.set(href, data); 119 | }) 120 | .catch((error) => { 121 | console.error(error); 122 | }) 123 | ); 124 | } 125 | 126 | /** 127 | * Init a page request. 128 | * Fetch the page and returns a promise with the text content. 129 | */ 130 | request(url) { 131 | return new Promise(async (resolve, reject) => { 132 | fetch(url) 133 | .then((response) => { 134 | return response.json(); 135 | }) 136 | .then((json) => { 137 | resolve(json); 138 | }) 139 | .catch((ex) => { 140 | reject(ex); 141 | }); 142 | }); 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /src/scripts/layout/Prevent.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Make sure the browser supports `history.pushState`. 3 | */ 4 | const pushState = () => !window.history.pushState; 5 | 6 | /** 7 | * Make sure there is an `el` and `href`. 8 | */ 9 | const exists = ({ el, href }) => !el || !href; 10 | 11 | /** 12 | * If the user is pressing ctrl + click, the browser will open a new tab. 13 | */ 14 | const newTab = ({ event }) => event.which > 1 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey; 15 | 16 | /** 17 | * If the link has `_blank` target. 18 | */ 19 | const blank = ({ el }) => el.hasAttribute('target') && el.target === '_blank'; 20 | 21 | /** 22 | * If the link has download attribute. 23 | */ 24 | const download = ({ el }) => el.getAttribute && typeof el.getAttribute('download') === 'string'; 25 | 26 | /** 27 | * If the links contains [data-transition-prevent] or [data-transition-prevent="self"]. 28 | */ 29 | const preventSelf = ({ el }) => el.hasAttribute('data-prevent'); 30 | 31 | export class Prevent { 32 | constructor() { 33 | this.suite = []; 34 | this.tests = new Map(); 35 | 36 | this.init(); 37 | } 38 | 39 | init() { 40 | // Add defaults 41 | this.add('pushState', pushState); 42 | this.add('exists', exists); 43 | this.add('newTab', newTab); 44 | this.add('blank', blank); 45 | this.add('download', download); 46 | this.add('preventSelf', preventSelf); 47 | } 48 | 49 | add(name, check, suite = true) { 50 | this.tests.set(name, check); 51 | suite && this.suite.push(name); 52 | } 53 | 54 | /** 55 | * Run individual test 56 | */ 57 | run(name, el, event, href) { 58 | return this.tests.get(name)({ 59 | el, 60 | event, 61 | href, 62 | }); 63 | } 64 | 65 | /** 66 | * Run test suite 67 | */ 68 | checkLink(el, event, href) { 69 | return this.suite.some((name) => this.run(name, el, event, href)); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/scripts/layout/actions.js: -------------------------------------------------------------------------------- 1 | import { SET_ORIENTATION, CALCULATE_RESPONSIVE_STATE } from './constants.js'; 2 | 3 | export function setOrientation(window) { 4 | return { 5 | type: SET_ORIENTATION, 6 | window, 7 | }; 8 | } 9 | 10 | export function calculateResponsiveState(window) { 11 | return { 12 | type: CALCULATE_RESPONSIVE_STATE, 13 | window, 14 | }; 15 | } 16 | -------------------------------------------------------------------------------- /src/scripts/layout/constants.js: -------------------------------------------------------------------------------- 1 | // ACTION 2 | export const SET_ORIENTATION = 'SET_ORIENTATION'; 3 | export const CALCULATE_RESPONSIVE_STATE = 'CALCULATE_RESPONSIVE_STATE'; 4 | 5 | // we assume `min-width` logic here 6 | export const BREAKPOINTS = { 7 | mobile: 0, 8 | tablet: 600, 9 | smallDesktop: 960, 10 | desktop: 1280, 11 | }; 12 | -------------------------------------------------------------------------------- /src/scripts/layout/reducers.js: -------------------------------------------------------------------------------- 1 | import { SET_ORIENTATION, CALCULATE_RESPONSIVE_STATE, BREAKPOINTS } from './constants.js'; 2 | 3 | const InitialState = { 4 | orientation: null, 5 | window: {}, 6 | }; 7 | 8 | // Updates an entity cache in response to any action with response.entities. 9 | export const layout = (state = InitialState, action) => { 10 | switch (action.type) { 11 | case SET_ORIENTATION: { 12 | const orientation = window.innerWidth > window.innerHeight ? 'landscape' : 'portrait'; 13 | return { 14 | ...state, 15 | orientation: orientation, 16 | }; 17 | } 18 | case CALCULATE_RESPONSIVE_STATE: { 19 | const width = window.innerWidth; 20 | const height = window.innerHeight; 21 | let activeBreakpoint; 22 | for (const breakpoint in BREAKPOINTS) { 23 | if (Object.hasOwnProperty.call(BREAKPOINTS, breakpoint)) { 24 | const minWidth = BREAKPOINTS[breakpoint]; 25 | if (width >= minWidth) activeBreakpoint = breakpoint; 26 | } 27 | } 28 | return { 29 | ...state, 30 | window: { 31 | width, 32 | height, 33 | breakpoint: activeBreakpoint, 34 | }, 35 | }; 36 | } 37 | default: { 38 | return state; 39 | } 40 | } 41 | }; 42 | 43 | export default layout; 44 | -------------------------------------------------------------------------------- /src/scripts/pages/about/About.js: -------------------------------------------------------------------------------- 1 | import Page from 'abstract/page.js'; 2 | import Template from 'templates/about.art'; 3 | 4 | class AboutContainer extends Page { 5 | constructor(props) { 6 | super(props); 7 | 8 | this.template = Template; 9 | } 10 | } 11 | 12 | export default AboutContainer; 13 | -------------------------------------------------------------------------------- /src/scripts/pages/homepage/Homepage.js: -------------------------------------------------------------------------------- 1 | import Page from 'abstract/page.js'; 2 | import template from 'templates/homepage.art'; 3 | // Constants 4 | 5 | // Utils 6 | 7 | // Selectors 8 | 9 | // Actions 10 | 11 | // Lib 12 | 13 | class Homepage extends Page { 14 | constructor(props) { 15 | super(props); 16 | this.template = template; 17 | } 18 | } 19 | 20 | export default Homepage; 21 | -------------------------------------------------------------------------------- /src/scripts/pages/notfound/Notfound.js: -------------------------------------------------------------------------------- 1 | import Page from 'abstract/page.js'; 2 | import Tpl from 'templates/notfound.art'; 3 | 4 | class NotFound extends Page { 5 | constructor(props) { 6 | super(props); 7 | 8 | this.template = Tpl; 9 | } 10 | } 11 | 12 | export default NotFound; 13 | -------------------------------------------------------------------------------- /src/scripts/reducers/index.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'store/store.js'; 2 | 3 | import app from 'app/reducers.js'; 4 | import layout from 'layout/reducers.js'; 5 | 6 | const rootReducer = combineReducers({ 7 | app, 8 | layout, 9 | }); 10 | 11 | export default rootReducer; 12 | -------------------------------------------------------------------------------- /src/scripts/router.js: -------------------------------------------------------------------------------- 1 | import page from 'page'; 2 | import store from 'store'; 3 | 4 | import realRoutes from './../../.plato/routes.json'; 5 | const routes = realRoutes.routes; 6 | 7 | // Actions 8 | import { navigate, setRoutes } from 'app/actions.js'; 9 | 10 | // Utils 11 | import { isString } from 'utils/is.js'; 12 | 13 | class Router { 14 | preRouting(ctx, next) { 15 | // here the path will contains query parameter so we can remove them suing this : 16 | // const path = this.getPathFromUrl(ctx.path); 17 | // if (path === '/'){} 18 | 19 | // Example if there's a query string 20 | // ctx.query = qs.parse(window.location.search.slice(1)); 21 | 22 | // store.dispatch(setQuery(ctx.query)); 23 | next(); 24 | } 25 | 26 | initRouter() { 27 | return new Promise((resolve, reject) => { 28 | store.dispatch(setRoutes(routes)); 29 | 30 | // Setup routing from Routes JSON 31 | for (let key in routes) { 32 | if (!routes.hasOwnProperty(key)) continue; 33 | let route = routes[key]; 34 | 35 | page( 36 | route.url, 37 | (ctx, next) => this.preRouting(ctx, next), 38 | (ctx) => { 39 | store.dispatch(navigate(route.id, ctx.params)); 40 | } 41 | ); 42 | } 43 | 44 | // 404 45 | page( 46 | '*', 47 | (ctx, next) => this.preRouting(ctx, next), 48 | (ctx) => { 49 | store.dispatch(navigate('404', ctx.params)); 50 | } 51 | ); 52 | resolve(); 53 | }); 54 | } 55 | 56 | configureRoute(options = {}) { 57 | if (options.base) page.base(options.base); 58 | return page; 59 | } 60 | 61 | getPathFromUrl(url) { 62 | return url.split('?')[0]; 63 | } 64 | 65 | /** 66 | * Navigate using the router 67 | */ 68 | navigate(url, options = {}) { 69 | console.log('isString(url)', isString(url)); 70 | 71 | if (!isString(url)) return false; 72 | // if absolute, make sure to add the root 73 | if (url.indexOf(window.location.origin) >= 0) { 74 | url = url.replace(window.location.origin, ''); 75 | } 76 | 77 | const re = new RegExp(/^.*\//); 78 | const rootUrl = re.exec(window.location.href); 79 | 80 | // If internal 81 | if (url.indexOf(rootUrl) >= 0) { 82 | // make it relative 83 | url = url.replace(window.location.origin, ''); 84 | url = url.replace(rootUrl, ''); 85 | } 86 | 87 | page('/' + url); 88 | } 89 | } 90 | 91 | const router = new Router(); 92 | export default router; 93 | -------------------------------------------------------------------------------- /src/scripts/store/globalStore.js: -------------------------------------------------------------------------------- 1 | // Global store in addition of the STATE, Animation / mouse state when State is in charge of Application State. 2 | class GlobalStore { 3 | constructor() { 4 | this._type = 'CommonModel'; 5 | this._eventTypes = []; 6 | this._callbackFunctions = []; 7 | this._dataObj = { 8 | createdAt: new Date(), 9 | rafCallStack: [], 10 | scroll: { 11 | targetY: 0, 12 | currentY: 0, 13 | }, 14 | mouse: { 15 | x: 0, 16 | y: 0, 17 | }, 18 | }; 19 | } 20 | 21 | on(eventType, callback) { 22 | if (this._eventTypes.findIndex((x) => x === eventType) === -1) { 23 | this._eventTypes.push(eventType); 24 | } 25 | 26 | if (this._callbackFunctions[eventType]) { 27 | this._callbackFunctions[eventType].push(callback); 28 | } else { 29 | this._callbackFunctions[eventType] = []; 30 | this._callbackFunctions[eventType].push(callback); 31 | } 32 | } 33 | 34 | off(eventType, callback) { 35 | if (this._callbackFunctions[eventType] !== undefined) { 36 | for (let i = 0; i < this._callbackFunctions[eventType].length; i++) { 37 | if (callback === this._callbackFunctions[eventType][i]) { 38 | this._callbackFunctions[eventType].splice(i, 1); 39 | } 40 | } 41 | } 42 | } 43 | 44 | offRAF(callback) { 45 | for (let i = 0; i < this.get('rafCallStack').length; i++) { 46 | let current = this.get('rafCallStack')[i]; 47 | if (current === callback) { 48 | if (i > -1) { 49 | this.get('rafCallStack').splice(i, 1); 50 | } 51 | } 52 | } 53 | } 54 | 55 | set(attr, val, silent) { 56 | if (silent) { 57 | this._dataObj[attr] = val; 58 | } else { 59 | if (this._dataObj[attr] !== val) { 60 | const previous = this._dataObj[attr]; 61 | this._dataObj[attr] = val; 62 | this._eventTypes.forEach((eventType, index) => { 63 | this._callbackFunctions[eventType].forEach((callback, index) => { 64 | if (eventType.indexOf('change:') > -1) { 65 | if (eventType === 'change:' + attr) { 66 | callback.call(this, val, previous); 67 | } 68 | } else { 69 | callback.call(this, val, previous); 70 | } 71 | }); 72 | }); 73 | } 74 | } 75 | } 76 | 77 | get(attr) { 78 | return this._dataObj[attr]; 79 | } 80 | } 81 | 82 | export default new GlobalStore(); 83 | -------------------------------------------------------------------------------- /src/scripts/store/index.js: -------------------------------------------------------------------------------- 1 | import { createStore } from './store.js'; 2 | import rootReducer from 'reducers'; 3 | 4 | const IS_LOCALHOST = location.hostname === 'localhost' || location.hostname === '127.0.0.1'; 5 | const USE_DEV_TOOLS = IS_LOCALHOST; 6 | const store = createStore(rootReducer, {}, USE_DEV_TOOLS); 7 | 8 | export default store; 9 | -------------------------------------------------------------------------------- /src/scripts/store/store.js: -------------------------------------------------------------------------------- 1 | const combineReducers = (reducers) => { 2 | const nextState = {}; 3 | const reducerFunctions = {}; 4 | const reducersKeys = Object.keys(reducers); 5 | reducersKeys.forEach((reducerKey) => { 6 | if (typeof reducers[reducerKey] === 'function') { 7 | reducerFunctions[reducerKey] = reducers[reducerKey]; 8 | } 9 | }); 10 | const reducerFunctionsKeys = Object.keys(reducerFunctions); 11 | 12 | return (state = {}, action) => { 13 | reducerFunctionsKeys.forEach((reducerKey) => { 14 | const reducer = reducerFunctions[reducerKey]; 15 | nextState[reducerKey] = reducer(state[reducerKey], action); 16 | }); 17 | 18 | return nextState; 19 | }; 20 | }; 21 | 22 | const validateAction = (action) => { 23 | if (!action || typeof action !== 'object' || Array.isArray(action)) { 24 | throw new Error('Action must be an object!'); 25 | } 26 | if (typeof action.type === 'undefined') { 27 | throw new Error('Action must have a type!'); 28 | } 29 | }; 30 | 31 | const Logger = (previousState, state, action) => { 32 | console.groupCollapsed(`%c 🍳 action %c${action.type}`, 'color: #00b8d0', 'color: #ffa693'); 33 | console.groupCollapsed('%c 👴 previousState', 'color: #00d061', previousState); 34 | console.groupEnd(); 35 | console.groupCollapsed('%c 🎬 action', 'color: #00d061', action); 36 | console.groupEnd(); 37 | console.groupCollapsed('%c 🔮 state', 'color: #00d061', state); 38 | console.groupEnd(); 39 | console.groupEnd(); 40 | }; 41 | 42 | const createStore = (reducer, initialState, USE_DEV_TOOLS) => { 43 | const store = {}; 44 | 45 | store.state = initialState; 46 | store.listeners = []; 47 | 48 | store.subscribe = (listener) => { 49 | store.listeners.push(listener); 50 | return () => { 51 | store.listeners = store.listeners.filter((l) => l !== listener); 52 | }; 53 | }; 54 | 55 | store.dispatch = (action) => { 56 | validateAction(action); 57 | const previousState = USE_DEV_TOOLS && window.structuredClone ? structuredClone(store.state) : {}; 58 | 59 | store.state = reducer(store.state, action); 60 | USE_DEV_TOOLS && Logger(previousState, store.state, action); 61 | 62 | store.listeners.forEach((listener) => { 63 | listener(action); 64 | }); 65 | }; 66 | 67 | store.getState = () => store.state; 68 | store.dispatch({ type: 'INIT' }); 69 | 70 | return store; 71 | }; 72 | 73 | export { createStore, combineReducers }; 74 | -------------------------------------------------------------------------------- /src/scripts/store/storeWatcher.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Gets the value at `path` of `object`. 3 | * @param {Object} object 4 | * @param {string|Array} path 5 | * @returns {*} value if exists else undefined 6 | */ 7 | const get = (object, path) => { 8 | if (typeof path === 'string') path = path.split('.').filter((key) => key.length); 9 | return path.reduce((dive, key) => dive && dive[key], object); 10 | }; 11 | 12 | export const storeWatcher = (getState, path) => { 13 | let previousState; 14 | 15 | return (method) => { 16 | return () => { 17 | const state = get(getState(), path); 18 | if (state !== null && state !== undefined && state !== previousState) { 19 | method(state, previousState); 20 | } 21 | previousState = state; 22 | }; 23 | }; 24 | }; 25 | -------------------------------------------------------------------------------- /src/scripts/utils/cleanURL.js: -------------------------------------------------------------------------------- 1 | const expr = /\/+$/; 2 | 3 | const removeTrailingSlash = (str) => { 4 | return String(str).replace(expr, ''); 5 | }; 6 | const removeTrailingIndexHTML = (str) => { 7 | return str.split('index.html')[0]; 8 | }; 9 | 10 | export const cleanURL = (url, options = {}) => { 11 | // check if ending by index.html 12 | 13 | // check if trailling slash 14 | // const testUrl = removeTrailingSlash(url); 15 | const testNoIndex = removeTrailingIndexHTML(url); 16 | 17 | if (testNoIndex !== url) { 18 | window.history.replaceState(null, null, testNoIndex); 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /src/scripts/utils/is.js: -------------------------------------------------------------------------------- 1 | // all micro checking go there 2 | 3 | // is a given value window? 4 | // setInterval method is only available for window object 5 | const iswindowObject = (value) => { 6 | return value != null && typeof value === 'object' && 'setInterval' in value; 7 | }; 8 | 9 | let freeSelf = iswindowObject(typeof self == 'object' && self) && self; 10 | 11 | // store navigator properties to use later 12 | let navigator = freeSelf && freeSelf.navigator; 13 | let appVersion = ((navigator && navigator.appVersion) || '').toLowerCase(); 14 | let userAgent = ((navigator && navigator.userAgent) || '').toLowerCase(); 15 | // let vendor = (navigator && navigator.vendor || '').toLowerCase(); 16 | 17 | // MOBILE 18 | /* -------------------------------------------------------------------------- */ 19 | 20 | const isMobile = () => { 21 | return isIphone() || isIpod() || isAndroidPhone() || isBlackberry() || isWindowsPhone(); 22 | }; 23 | 24 | const isIphone = (range) => { 25 | // avoid false positive for Facebook in-app browser on ipad; 26 | // original iphone doesn't have the OS portion of the UA 27 | let match = isIpad() ? null : userAgent.match(/iphone(?:.+?os (\d+))?/); 28 | return match !== null && compareVersion(match[1] || 1, range); 29 | }; 30 | 31 | const isIpod = (range) => { 32 | let match = userAgent.match(/ipod.+?os (\d+)/); 33 | return match !== null && compareVersion(match[1], range); 34 | }; 35 | 36 | const isAndroidPhone = () => { 37 | return /android/.test(userAgent) && /mobile/.test(userAgent); 38 | }; 39 | 40 | const isBlackberry = () => { 41 | return /blackberry/.test(userAgent) || /bb10/.test(userAgent); 42 | }; 43 | 44 | const isWindowsPhone = () => { 45 | return isWindows() && /phone/.test(userAgent); 46 | }; 47 | 48 | const isWindows = () => { 49 | return /win/.test(appVersion); 50 | }; 51 | 52 | // TABLET 53 | /* -------------------------------------------------------------------------- */ 54 | 55 | // is current device tablet? 56 | export const isTablet = function () { 57 | return isIpad() || isAndroidTablet() || isWindowsTablet(); 58 | }; 59 | 60 | const isIpad = (range) => { 61 | let match = userAgent.match(/ipad.+?os (\d+)/); 62 | return match !== null && compareVersion(match[1], range); 63 | }; 64 | 65 | const isAndroidTablet = () => { 66 | return /android/.test(userAgent) && !/mobile/.test(userAgent); 67 | }; 68 | 69 | const isWindowsTablet = () => { 70 | return isWindows() && !isWindowsPhone() && /touch/.test(userAgent); 71 | }; 72 | 73 | // is current device supports touch? 74 | export const isTouchDevice = () => { 75 | return ( 76 | !!document && ('ontouchstart' in freeSelf || ('DocumentTouch' in freeSelf && document instanceof DocumentTouch)) 77 | ); 78 | }; 79 | 80 | // BROWSERS 81 | /* -------------------------------------------------------------------------- */ 82 | 83 | export const isDesktop = () => { 84 | return !isMobile() && !isTablet(); 85 | }; 86 | 87 | const isIE = (range) => { 88 | let match = userAgent.match(/(?:msie |trident.+?; rv:)(\d+)/); 89 | return match !== null && compareVersion(match[1], range); 90 | }; 91 | 92 | const isChrome = (range) => { 93 | let match = /google inc/.test(vendor) ? userAgent.match(/(?:chrome|crios)\/(\d+)/) : null; 94 | return match !== null && is.not.opera() && compareVersion(match[1], range); 95 | }; 96 | 97 | // is current browser edge? 98 | const isEdge = (range) => { 99 | let match = userAgent.match(/edge\/(\d+)/); 100 | return match !== null && compareVersion(match[1], range); 101 | }; 102 | 103 | const isFirefox = (range) => { 104 | let match = userAgent.match(/(?:firefox|fxios)\/(\d+)/); 105 | return match !== null && compareVersion(match[1], range); 106 | }; 107 | 108 | const isSafari = (range) => { 109 | let match = userAgent.match(/version\/(\d+).+?safari/); 110 | return match !== null && compareVersion(match[1], range); 111 | }; 112 | 113 | // helper function which compares a version to a range 114 | function compareVersion(version, range) { 115 | let string = range + ''; 116 | let n = +(string.match(/\d+/) || NaN); 117 | let op = string.match(/^[<>]=?|/)[0]; 118 | return comparator[op] ? comparator[op](version, n) : version == n || n !== n; 119 | } 120 | 121 | // build a 'comparator' object for various comparison checks 122 | const comparator = { 123 | '<': function (a, b) { 124 | return a < b; 125 | }, 126 | '<=': function (a, b) { 127 | return a <= b; 128 | }, 129 | '>': function (a, b) { 130 | return a > b; 131 | }, 132 | '>=': function (a, b) { 133 | return a >= b; 134 | }, 135 | }; 136 | 137 | export const isString = (value) => { 138 | return typeof value === 'string' || value instanceof String; 139 | }; 140 | 141 | export { isMobile }; 142 | -------------------------------------------------------------------------------- /src/scripts/utils/loadImages.js: -------------------------------------------------------------------------------- 1 | // import picturefill from 'picturefill'; 2 | // picturefill(); 3 | 4 | // const img = document.createElement('img'); 5 | // const isWSupported = 'sizes' in img; 6 | const isPictureSupported = !!window.HTMLPictureElement; 7 | 8 | // TODO: add error handlers 9 | // TODO: add is-loading 10 | // TODO: explore lazy with intersection observer 11 | 12 | const isImageValid = ($image) => { 13 | if (isPicture($image)) { 14 | return isPictureValid($image); 15 | } 16 | 17 | return isImgValid($image); 18 | }; 19 | 20 | const isImgValid = ($image) => { 21 | const nodeName = $image.nodeName.toLowerCase(); 22 | return ( 23 | ($image.hasAttribute('data-src') || $image.hasAttribute('data-srcset')) && 24 | (nodeName === 'img' || nodeName === 'source') 25 | ); 26 | }; 27 | 28 | const isPictureValid = ($image) => { 29 | const parent = $image.parentNode; 30 | 31 | let isSourcesValid = true; 32 | const sourceTags = getSourceTags(parent); 33 | sourceTags.forEach((sourceTag) => { 34 | if (!isImgValid(sourceTag)) isSourcesValid = false; 35 | }); 36 | 37 | return isSourcesValid; 38 | }; 39 | 40 | const isPicture = ($image) => { 41 | const parent = $image.parentNode; 42 | return parent && parent.tagName === 'PICTURE'; 43 | }; 44 | 45 | const getSourceTags = (parentTag) => { 46 | let sourceTags = []; 47 | 48 | let childTag; 49 | for (let i = 0; (childTag = parentTag.children[i]); i += 1) { 50 | if (childTag.tagName === 'SOURCE') { 51 | sourceTags.push(childTag); 52 | } 53 | } 54 | 55 | return sourceTags; 56 | }; 57 | 58 | export const loadImage = ($image) => { 59 | return new Promise((resolve) => { 60 | const onLoad = (event) => { 61 | // If the image is setting a background image, add loaded class to parent. 62 | // ImageLoader won't reset background image if the 'src' exists, so remove after load. 63 | // this will only works with data-src 64 | if ($image.hasAttribute('data-use-bg-image')) { 65 | $image.parentNode.classList.add('is-loaded'); 66 | $image.removeAttribute('src'); 67 | $image.style.display = 'none'; 68 | } else { 69 | $image.classList.add('is-loaded'); 70 | } 71 | 72 | // remove load event listener to prevent duplicates 73 | $image.removeEventListener('load', onLoad); 74 | resolve($image); 75 | }; 76 | 77 | $image.addEventListener('load', onLoad); 78 | 79 | if (isPicture($image)) { 80 | const sourceTags = getSourceTags($image.parentNode); 81 | sourceTags.forEach((sourceTag) => { 82 | setImageAttributes(sourceTag); 83 | }); 84 | 85 | // load fallback if not supported 86 | // we added this because otherwise fallback will be loaded with the source 87 | if (!isPictureSupported) { 88 | setImageAttributes($image); 89 | } 90 | } else { 91 | setImageAttributes($image); 92 | } 93 | }); 94 | }; 95 | 96 | const setImageAttributes = ($image) => { 97 | if ($image.hasAttribute('data-sizes')) { 98 | if (!$image.hasAttribute('sizes')) { 99 | $image.setAttribute('sizes', $image.dataset.sizes); 100 | } 101 | } 102 | 103 | if ($image.hasAttribute('data-srcset')) { 104 | $image.setAttribute('srcset', $image.dataset.srcset); 105 | } 106 | 107 | if ($image.hasAttribute('data-src')) { 108 | $image.setAttribute('src', $image.dataset.src); 109 | } 110 | }; 111 | 112 | /** 113 | * Image load one or more images using Promise.all. 114 | * @param {DOMElement[]} $images An array of images to load. 115 | * @param {function} afterImageLoad A function to be called after every image load 116 | * @return {Promise} Promise that resolves when all images are loaded 117 | */ 118 | export const loadImages = ($images, afterImageLoad) => { 119 | if (!Array.isArray($images)) { 120 | console.warn('Load images promise should take an array of images, instead got type', typeof $images); 121 | return; 122 | } 123 | 124 | if (Array.isArray($images) && $images.length === 0) { 125 | console.warn('Empty Array', typeof $images); 126 | return; 127 | } 128 | 129 | const imagePromises = $images.map(($image) => { 130 | if (!$image) return false; 131 | 132 | return new Promise((resolve) => { 133 | // check if image is image and has proper attributes 134 | if (!isImageValid($image)) { 135 | console.warn('ImageLoader: Missing proper attribute data-*'); 136 | resolve($image); 137 | return; 138 | } 139 | 140 | loadImage($image).then(($image) => { 141 | typeof afterImageLoad === 'function' && afterImageLoad($image); 142 | resolve($image); 143 | }); 144 | }); 145 | }); 146 | 147 | return Promise.all(imagePromises); 148 | }; 149 | 150 | export default loadImages; 151 | -------------------------------------------------------------------------------- /src/scripts/utils/loadScript.js: -------------------------------------------------------------------------------- 1 | export function loadScript(src, done) { 2 | var js = document.createElement('script'); 3 | js.src = src; 4 | js.onload = function () { 5 | done(); 6 | }; 7 | js.onerror = function () { 8 | done(new Error('Failed to load script ' + src)); 9 | }; 10 | document.head.appendChild(js); 11 | } 12 | 13 | //https://philipwalton.com/articles/loading-polyfills-only-when-needed/ 14 | export const browserSupportsAllFeatures = () => { 15 | return window.fetch; 16 | }; 17 | -------------------------------------------------------------------------------- /src/scripts/utils/misc.js: -------------------------------------------------------------------------------- 1 | export const rad = (x) => { 2 | return (x * Math.PI) / 180; 3 | }; 4 | 5 | export const metersToMiles = (meters) => { 6 | return meters * 0.00062137; 7 | }; 8 | 9 | export const fitAsset = (config_) => { 10 | const wi = config_.width; 11 | const hi = config_.height; 12 | const ri = wi / hi; 13 | const ws = config_.containerWidth; 14 | const hs = config_.containerHeight; 15 | const rs = ws / hs; 16 | const newDimensions = {}; 17 | 18 | if (ri > rs) { 19 | newDimensions.ratio = hs / hi; 20 | newDimensions.w = Math.ceil((wi * hs) / hi, (newDimensions.h = hs)); 21 | } else { 22 | newDimensions.ratio = ws / wi; 23 | newDimensions.w = ws; 24 | newDimensions.h = Math.ceil((hi * ws) / wi); 25 | } 26 | 27 | newDimensions.top = (hs - newDimensions.h) / 2; 28 | newDimensions.left = (ws - newDimensions.w) / 2; 29 | 30 | return newDimensions; 31 | }; 32 | 33 | export const fitAssetContains = (config_) => { 34 | const wi = config_.width; 35 | const hi = config_.height; 36 | const ri = wi / hi; 37 | const ws = config_.containerWidth; 38 | const hs = config_.containerHeight; 39 | const rs = ws / hs; 40 | const newDimensions = {}; 41 | 42 | // item is wider than container 43 | if (ri > rs) { 44 | newDimensions.ratio = hs / hi; 45 | newDimensions.w = ws; 46 | newDimensions.h = Math.ceil(ws / ri); 47 | } 48 | // item is taller than container 49 | else { 50 | newDimensions.ratio = ws / wi; 51 | newDimensions.w = Math.ceil(hs * ri); 52 | newDimensions.h = hs; 53 | } 54 | return newDimensions; 55 | }; 56 | 57 | // Returns a function, that, as long as it continues to be invoked, will not 58 | // be triggered. The function will be called after it stops being called for 59 | // N milliseconds. If `immediate` is passed, trigger the function on the 60 | // leading edge, instead of the trailing. 61 | // https://www.joshwcomeau.com/snippets/javascript/debounce/ 62 | export const debounce = (callback, wait) => { 63 | let timeoutId = null; 64 | return (...args) => { 65 | window.clearTimeout(timeoutId); 66 | timeoutId = window.setTimeout(() => { 67 | callback.apply(null, args); 68 | }, wait); 69 | }; 70 | }; 71 | 72 | export const throttle = (callback, wait, immediate = false) => { 73 | let timeout = null; 74 | let initialCall = true; 75 | 76 | return function () { 77 | const callNow = immediate && initialCall; 78 | const next = () => { 79 | callback.apply(this, arguments); 80 | timeout = null; 81 | }; 82 | 83 | if (callNow) { 84 | initialCall = false; 85 | next(); 86 | } 87 | 88 | if (!timeout) { 89 | timeout = setTimeout(next, wait); 90 | } 91 | }; 92 | }; 93 | 94 | export const getRandomIntInclusive = (min, max) => { 95 | min = Math.ceil(min); 96 | max = Math.floor(max); 97 | return Math.floor(Math.random() * (max - min + 1)) + min; 98 | }; 99 | -------------------------------------------------------------------------------- /src/scripts/utils/offset.js: -------------------------------------------------------------------------------- 1 | /* global document window */ 2 | 3 | const body = document.body; 4 | 5 | function getOffset(element) { 6 | let bodyRect = body.getBoundingClientRect(), 7 | elemRect = element.getBoundingClientRect(), 8 | offset = elemRect.top - bodyRect.top; 9 | 10 | return offset; 11 | } 12 | 13 | function getDocumentHeight() { 14 | let html = document.documentElement; 15 | 16 | return Math.max(body.scrollHeight, body.offsetHeight, html.clientHeight, html.scrollHeight, html.offsetHeight); 17 | } 18 | 19 | function getPositionStart(element, viewFactor = 0, wHeight = window.innerHeight) { 20 | const offset = getOffset(element); 21 | const elementHeight = element.offsetHeight; 22 | const windowHeight = wHeight; 23 | 24 | const positionTop = offset - windowHeight + elementHeight * viewFactor; 25 | return positionTop > 0 ? positionTop : 0; 26 | } 27 | 28 | function getPositionEnd(element, viewFactor = 0, wHeight = window.innerHeight) { 29 | let offset = getOffset(element); 30 | let elementHeight = element.offsetHeight; 31 | const documentHeight = getDocumentHeight(); 32 | let positionBottom = offset + elementHeight - elementHeight * viewFactor; 33 | 34 | return positionBottom > documentHeight - wHeight ? documentHeight - wHeight : positionBottom; 35 | } 36 | 37 | // pass an array of DOM elements 38 | // get a array back on the one currently in the viewport 39 | // TODO: add param to control if entirely visible 40 | function getElementsInViewport(elements) { 41 | // we check the current elements 42 | // we pass this here to avoid multiple calls ( not sure if usefull might be worth measure ) 43 | const pageXOffset = window.pageXOffset; 44 | const pageYOffset = window.pageYOffset; 45 | const innerWidth = window.innerWidth; 46 | const innerHeight = window.innerHeight; 47 | 48 | let elementInViewportElements = []; 49 | // const elements = [...this.tiles, ...this.sectionHeader]; 50 | 51 | for (let i = 0; i < elements.length; i++) { 52 | let element = elements[i]; 53 | const inViewport = elementInViewport(element, pageXOffset, pageYOffset, innerWidth, innerHeight); 54 | if (inViewport) { 55 | // element.style.transition = 'none'; 56 | elementInViewportElements.push(element); 57 | } 58 | } 59 | 60 | return elementInViewportElements; 61 | } 62 | 63 | function elementInViewport( 64 | el, 65 | pageXOffset = window.pageXOffset, 66 | pageYOffset = window.pageYOffset, 67 | innerWidth = window.innerWidth, 68 | innerHeight = window.innerHeight 69 | ) { 70 | let top = el.offsetTop; 71 | let left = el.offsetLeft; 72 | const width = el.offsetWidth; 73 | const height = el.offsetHeight; 74 | 75 | while (el.offsetParent) { 76 | el = el.offsetParent; 77 | top += el.offsetTop; 78 | left += el.offsetLeft; 79 | } 80 | 81 | return ( 82 | top < pageYOffset + innerHeight && 83 | left < pageXOffset + innerWidth && 84 | top + height > pageYOffset && 85 | left + width > pageXOffset 86 | ); 87 | } 88 | 89 | module.exports = { getOffset, getPositionStart, getPositionEnd, getDocumentHeight, getElementsInViewport }; 90 | -------------------------------------------------------------------------------- /src/scripts/utils/scrollPrevent.js: -------------------------------------------------------------------------------- 1 | import Lethargy from 'lethargy'; 2 | 3 | /* eslint-disable */ 4 | class ScrollPrevent { 5 | constructor() { 6 | this.blockDelta = false; 7 | this.currentDeltaY = 0; 8 | 9 | this.lethargy = new Lethargy.Lethargy(); 10 | 11 | this.cb = null; 12 | this.el = null; 13 | 14 | this.UP = 'UP'; 15 | this.DOWN = 'DOWN'; 16 | 17 | this.date = Date.now(); 18 | } 19 | 20 | watch(el, cb) { 21 | this.cb = cb; 22 | this.el = el; 23 | 24 | this.bindEvents(); 25 | } 26 | 27 | approveScroll(e) { 28 | if (e === null) return; 29 | 30 | const date = Date.now(); 31 | 32 | if (this.lethargy.check(e) !== false && date - this.date > 200) { 33 | this.date = date; 34 | const direction = e.deltaY >= 0 ? this.DOWN : this.UP; 35 | 36 | this.cb({ 37 | deltaY: this.currentDeltaY, 38 | direction: direction, 39 | }); 40 | } 41 | } 42 | 43 | bindEvents() { 44 | if (!this.el) return; 45 | 46 | this.el.addEventListener('mousewheel', () => this.approveScroll(), false); 47 | this.el.addEventListener('wheel', () => this.approveScroll(), false); 48 | } 49 | 50 | unbindEvents() { 51 | if (!this.el) return; 52 | 53 | this.el.removeEventListener('mousewheel', () => this.approveScroll(), false); 54 | this.el.removeEventListener('wheel', () => this.approveScroll(), false); 55 | } 56 | 57 | dispose() { 58 | this.unbindEvents(); 59 | } 60 | } 61 | 62 | export default ScrollPrevent; 63 | -------------------------------------------------------------------------------- /src/scripts/utils/uniqueId.js: -------------------------------------------------------------------------------- 1 | /** Used to generate unique IDs. */ 2 | const idCounter = {}; 3 | 4 | /** 5 | * Generates a unique ID. If `prefix` is given, the ID is appended to it. 6 | * 7 | * @since 0.1.0 8 | * @category Util 9 | * @param {string} [prefix=''] The value to prefix the ID with. 10 | * @returns {string} Returns the unique ID. 11 | * @see random 12 | * @example 13 | * 14 | * uniqueId('contact_') 15 | * // => 'contact_104' 16 | * 17 | * uniqueId() 18 | * // => '105' 19 | */ 20 | function uniqueId(prefix = '$uid$') { 21 | if (!idCounter[prefix]) { 22 | idCounter[prefix] = 0; 23 | } 24 | 25 | const id = ++idCounter[prefix]; 26 | if (prefix === '$uid$') { 27 | return `${id}`; 28 | } 29 | 30 | return `${prefix + id}`; 31 | } 32 | 33 | export default uniqueId; 34 | -------------------------------------------------------------------------------- /src/scripts/utils/url.js: -------------------------------------------------------------------------------- 1 | /** 2 | * URL utils. 3 | * 4 | * - Collect and structure information from URLs 5 | * 6 | */ 7 | 8 | /** 9 | * Get location href. 10 | */ 11 | export const getHref = () => window.location.href; 12 | 13 | /** 14 | * Get location origin. 15 | */ 16 | export const getOrigin = () => window.location.origin; 17 | 18 | /** 19 | * Get port based on URL or location. 20 | */ 21 | export const getPort = (url = window.location.href) => parse(url).port; 22 | 23 | /** 24 | * Get path from URL. 25 | */ 26 | export const getPath = (url = window.location.href) => parse(url).path; 27 | 28 | /** 29 | * Get query object from URL. 30 | */ 31 | // export const getQuery = (url: string): IGenericObject => parse(url).query; 32 | 33 | /** 34 | * Get hash from URL. 35 | */ 36 | // export const getHash = (url: string): string => parse(url).hash; 37 | 38 | /** 39 | * Parse URL for path, query and hash and more. 40 | */ 41 | export const parse = (url) => { 42 | // Port 43 | let port; 44 | const matches = url.match(/:\d+/); 45 | 46 | if (matches === null) { 47 | if (/^http/.test(url)) { 48 | port = 80; 49 | } 50 | 51 | if (/^https/.test(url)) { 52 | port = 443; 53 | } 54 | } else { 55 | const portString = matches[0].substring(1); 56 | 57 | port = parseInt(portString, 10); 58 | } 59 | 60 | // Path 61 | let path = url.replace(getOrigin(), ''); 62 | let hash; 63 | let query = {}; 64 | 65 | // Hash 66 | const hashIndex = path.indexOf('#'); 67 | 68 | if (hashIndex >= 0) { 69 | hash = path.slice(hashIndex + 1); 70 | path = path.slice(0, hashIndex); 71 | } 72 | 73 | // Query 74 | const queryIndex = path.indexOf('?'); 75 | 76 | if (queryIndex >= 0) { 77 | query = parseQuery(path.slice(queryIndex + 1)); 78 | path = path.slice(0, queryIndex); 79 | } 80 | 81 | return { 82 | hash, 83 | path, 84 | port, 85 | query, 86 | }; 87 | }; 88 | 89 | /** 90 | * Parse a query string to object. 91 | */ 92 | export const parseQuery = (str) => 93 | str.split('&').reduce((acc, el) => { 94 | const [key, value] = el.split('='); 95 | 96 | acc[key] = value; 97 | 98 | return acc; 99 | }, {}); 100 | 101 | /** 102 | * Clean URL, remove "hash" and/or "trailing slash". 103 | */ 104 | export const clean = (url = window.location.href) => url.replace(/(\/#.*|\/|#.*)$/, ''); 105 | --------------------------------------------------------------------------------