{item.title}
4 | {#if item.subtitle} 5 |{item.subtitle}
6 | {/if} 7 | {#if item.description} 8 |{item.code}
12 | {/if}
13 | {#if fields.length}
14 | {key} | 18 |19 | |
├── .env.defaults ├── .eslintrc.yml ├── .gitignore ├── .nvmrc ├── .scss-lint.yml ├── LICENSE ├── README.md ├── package.json ├── rollup.config.js ├── src ├── _client │ ├── .eslintrc.json │ ├── js │ │ └── .gitkeep │ ├── postcss │ │ ├── global.postcss │ │ ├── main │ │ │ ├── _basics.postcss │ │ │ └── _layout.postcss │ │ ├── modules │ │ │ ├── _buttons.postcss │ │ │ └── _structure.postcss │ │ ├── routes-includes.postcss │ │ └── shared │ │ │ ├── _helpers.postcss │ │ │ └── _variables.postcss │ └── svg │ │ ├── compiled │ │ ├── arrow-down.svg │ │ ├── arrow-q.svg │ │ ├── logo-public.svg │ │ ├── select-handle-off.svg │ │ ├── select-handle-on.svg │ │ ├── select-handle.svg │ │ ├── social-email.svg │ │ ├── social-facebook.svg │ │ ├── social-github.svg │ │ ├── social-gitlab.svg │ │ ├── social-google.svg │ │ ├── social-instagram.svg │ │ ├── social-linkedin.svg │ │ ├── social-twitter.svg │ │ ├── social-youtube.svg │ │ ├── valid-bad.svg │ │ └── valid-good.svg │ │ └── originals │ │ ├── arrow-down.svg │ │ ├── arrow-q.svg │ │ ├── logo-public.svg │ │ ├── select-handle-off.svg │ │ ├── select-handle-on.svg │ │ ├── select-handle.svg │ │ ├── social-email.svg │ │ ├── social-facebook.svg │ │ ├── social-github.svg │ │ ├── social-gitlab.svg │ │ ├── social-google.svg │ │ ├── social-instagram.svg │ │ ├── social-linkedin.svg │ │ ├── social-twitter.svg │ │ ├── social-youtube.svg │ │ ├── valid-bad.svg │ │ └── valid-good.svg ├── _server │ ├── build │ │ ├── babel-register-compiler.js │ │ ├── config.js │ │ ├── postcss.config.js │ │ ├── postcss.config.vars.js │ │ ├── rollup.preprocess.js │ │ ├── rollup.vars.js │ │ ├── svgo-config-inline.yml │ │ └── svgo-config-src.yml │ ├── db │ │ ├── arangodb-api.js │ │ ├── arangodb-driver.js │ │ └── validators │ │ │ ├── generic.js │ │ │ └── graphql.js │ ├── graphql-api │ │ ├── models │ │ │ ├── _mutations.gql │ │ │ ├── _mutations.map.js │ │ │ ├── _queries.gql │ │ │ ├── _queries.map.js │ │ │ ├── posts │ │ │ │ ├── posts.gql │ │ │ │ └── posts.map.js │ │ │ └── users │ │ │ │ ├── users.gql │ │ │ │ └── users.map.js │ │ ├── schema.js │ │ └── utils │ │ │ ├── graphql-utils.js │ │ │ └── mappers.js │ ├── serverless │ │ └── img-upload.js │ ├── services │ │ ├── app-setup.js │ │ ├── auth-setup.js │ │ └── graphql-setup.js │ └── utils │ │ ├── clip-utils.js │ │ ├── db-tools.js │ │ ├── loaders.js │ │ ├── migration-shell.js │ │ ├── migration-template.js │ │ ├── stallion-utils.js │ │ └── thumbnail-generator.js ├── _service-worker.js ├── client.js ├── components │ ├── List.svelte │ ├── forms │ │ ├── Form.svelte │ │ ├── LabelCheckbox.svelte │ │ ├── LabelInput.svelte │ │ ├── LabelRadio.svelte │ │ ├── LabelSelect.svelte │ │ ├── LabelTextarea.svelte │ │ ├── MiniForm.svelte │ │ └── Popup.svelte │ ├── layout │ │ ├── Header.svelte │ │ ├── Nav.svelte │ │ ├── NavMenu.svelte │ │ └── Sidebar.svelte │ ├── settings │ │ └── ListItem.svelte │ ├── shared │ │ ├── FlexInput.svelte │ │ └── ReloadBlock.svelte │ └── svg │ │ ├── Close.svelte │ │ └── Loader.svelte ├── routes │ ├── _error.svelte │ ├── _home-private.svelte │ ├── _home-public.svelte │ ├── _layout.svelte │ ├── _services │ │ ├── auth-check.js │ │ └── redirect-handler.js │ ├── api │ │ ├── [profile] │ │ │ └── [dataset].js │ │ ├── contents │ │ │ ├── [name].json.js │ │ │ ├── create.json.js │ │ │ ├── delete.json.js │ │ │ └── list.json.js │ │ ├── paywall.json.js │ │ ├── restart.json.js │ │ ├── signup-validate.json.js │ │ └── signup.json.js │ ├── cm │ │ └── [profile] │ │ │ └── index.svelte │ ├── contents │ │ ├── [content].svelte │ │ ├── create.svelte │ │ ├── edit │ │ │ ├── [name].svelte │ │ │ ├── _AddFieldForm.svelte │ │ │ ├── _fields │ │ │ │ ├── Boolean.svelte │ │ │ │ ├── Date.svelte │ │ │ │ ├── Email.svelte │ │ │ │ ├── Media.svelte │ │ │ │ ├── Number.svelte │ │ │ │ ├── Relation.svelte │ │ │ │ ├── String.svelte │ │ │ │ └── Text.svelte │ │ │ └── _options.js │ │ └── index.svelte │ ├── index.svelte │ ├── login.svelte │ ├── profiles.svelte │ ├── settings │ │ ├── _layout.svelte │ │ ├── account │ │ │ ├── notifications.svelte │ │ │ └── profile.svelte │ │ └── admin │ │ │ ├── _actions │ │ │ ├── collections.js │ │ │ └── helpers.js │ │ │ ├── actions.json.js │ │ │ ├── collections │ │ │ ├── [collection].svelte │ │ │ └── index.svelte │ │ │ └── users.svelte │ └── setup │ │ └── index.svelte ├── server.js ├── stores │ ├── app-store.js │ └── local-store.js └── template.html ├── static ├── android-chrome-192x192.png ├── android-chrome-256x256.png ├── apple-touch-icon.png ├── browserconfig.xml ├── css │ ├── global.css │ └── global.css.map ├── favicon-16x16.png ├── favicon-32x32.png ├── favicon.ico ├── manifest.json ├── mstile-150x150.png └── svg │ ├── arrow-down.svg │ ├── arrow-q.svg │ ├── logo-public.svg │ ├── nav-next.svg │ ├── nav-prev.svg │ ├── select-handle-off.svg │ ├── select-handle-on.svg │ ├── select-handle.svg │ ├── social-email.svg │ ├── social-facebook.svg │ ├── social-github.svg │ ├── social-gitlab.svg │ ├── social-google.svg │ ├── social-instagram.svg │ ├── social-linkedin.svg │ ├── social-twitter.svg │ ├── social-youtube.svg │ ├── valid-bad.svg │ └── valid-good.svg └── yarn.lock /.env.defaults: -------------------------------------------------------------------------------- 1 | NODE_ENV=development 2 | PORT=3111 3 | STALLION_HOST=http://localhost:3111 4 | STALLION_ARANGODB_CONNECTION=http://localhost:8529 5 | STALLION_ARANGODB_DB=stallion-dev 6 | STALLION_ARANGODB_PASSWORD= 7 | STALLION_ARANGODB_USERNAME=root 8 | STALLION_JWT_SECRET=replace-this-with-a-solid-and-unique-secret 9 | -------------------------------------------------------------------------------- /.eslintrc.yml: -------------------------------------------------------------------------------- 1 | root: true 2 | extends: eslint:recommended 3 | env: 4 | es6: true 5 | node: true 6 | parserOptions: 7 | sourceType: module 8 | ecmaVersion: 2017 9 | rules: 10 | 11 | # --->>> best practices 12 | 13 | curly: error 14 | eol-last: error 15 | newline-per-chained-call: 16 | - error 17 | - ignoreChainWithDepth: 3 18 | no-use-before-define: 19 | - error 20 | - nofunc 21 | no-console: 22 | - off 23 | 24 | # --->>> style enforcement 25 | 26 | camelcase: 27 | - error 28 | - properties: always 29 | comma-dangle: 30 | - error 31 | - always-multiline 32 | id-length: 33 | - error 34 | - min: 2 35 | max: 25 36 | exceptions: 37 | - $ 38 | - _ 39 | max-len: 40 | - error 41 | - code: 140 42 | ignoreUrls: true 43 | max-params: 44 | - error 45 | - max: 4 46 | max-statements-per-line: 47 | - error 48 | - max: 2 49 | no-extra-semi: error 50 | object-property-newline: 51 | - error 52 | - allowMultiplePropertiesPerLine: true 53 | one-var: 54 | - error 55 | - never 56 | quotes: 57 | - error 58 | - single 59 | semi: 60 | - error 61 | - never 62 | 63 | # --->>> white spacing pedantry 64 | 65 | indent: 66 | - error 67 | - tab 68 | no-multi-spaces: error 69 | 70 | # https://github.com/eslint/eslint/pull/8061 71 | no-trailing-spaces: 72 | - error 73 | - ignoreComments: true 74 | 75 | no-whitespace-before-property: error 76 | array-bracket-spacing: 77 | - error 78 | - never 79 | block-spacing: error 80 | comma-spacing: 81 | - error 82 | - before: false 83 | after: true 84 | computed-property-spacing: 85 | - error 86 | - never 87 | # 'func-call-spacing: error 88 | key-spacing: 89 | - error 90 | - beforeColon: false 91 | afterColon: true 92 | keyword-spacing: 93 | - error 94 | - before: true 95 | after: true 96 | object-curly-spacing: 97 | - error 98 | - always 99 | semi-spacing: 100 | - error 101 | - before: false 102 | space-before-blocks: error 103 | space-before-function-paren: 104 | - error 105 | - never 106 | space-in-parens: 107 | - error 108 | - never 109 | space-infix-ops: 110 | - error 111 | - int32Hint: false 112 | space-unary-ops: 113 | - error 114 | - words: true 115 | nonwords: false 116 | # spaced-comment: 117 | # - error 118 | # - always 119 | arrow-spacing: error 120 | generator-star-spacing: error 121 | rest-spread-spacing: 122 | - error 123 | - never 124 | template-curly-spacing: error 125 | yield-star-spacing: error 126 | 127 | # --->>> svelte pedantry 128 | 129 | no-unused-labels: error 130 | 131 | # globals: 132 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /__sapper__/ 4 | .esm-cache 5 | .env 6 | .env.* 7 | !.env.defaults 8 | /_hidden 9 | /src/_server/data/.migrate-development 10 | /src/_server/data/migrations-data/passwords.production.csv 11 | /src/_server/build/caddy/passwd 12 | /.vscode 13 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 11.12.0 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2019 [these people](https://github.com/sveltejs/svelte/graphs/contributors) 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Stallion 2 | 3 | Stallion is a headless CMS built atop Svelte and Sapper. 4 | 5 |  6 | 7 | ## Development 8 | 9 | The following is a bunch of rambled thoughts that I wanted to make sure to record and not forget, but I'll come back to later and better organize. 10 | 11 | ### Environment Variables 12 | 13 | Stallion uses `rollup-plugin-replace` for environment variable replacement. Any environment variable prefixed with `STALLION_` will automatically be replaced in the code if it's listed as a `process.env.*` variable. For example: 14 | 15 | process.env.STALLION_ARANGODB_CONNECTION 16 | 17 | Is replaced with with the connection string. 18 | 19 | `process.env.NODE_ENV` and `process.env.PORT` are the only non-prefixed variables to also be replaced. 20 | 21 | 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "stallion", 3 | "description": "Headless CMS built atop Svelte and Sapper.", 4 | "version": "0.0.1", 5 | "license": "MIT", 6 | "scripts": { 7 | "dev": "sapper dev -p 3111", 8 | "build": "npx sapper build --legacy", 9 | "export": "sapper export --legacy", 10 | "start": "node __sapper__/build", 11 | "debug": "sapper dev --inspect", 12 | "debug:build": "node --inspect --inspect-brk ./node_modules/sapper/sapper build --legacy", 13 | "dev:postcss:global": "NODE_ENV=development BUNDLE=App postcss src/_client/postcss/global.postcss -o static/css/global.css -m --verbose --config src/_server/build", 14 | "watch:postcss:global": "yarn run -s dev:postcss:global -w", 15 | "dev:svg:if": "if [ $e = \"unlink\" ]; then c=\"\\033[31m\" n=\"${f//src\\/_client\\/svg\\/originals\\/}\" yarn run -s dev:svg:unlink; else c=\"\\033[32m\" yarn run -s dev:svg:compile; fi;", 16 | "dev:svg:info": "echo \"\\033[0;35m---> SVG file event $c$e\\033[0;35m\\033[0m $f\"", 17 | "dev:svg:compile": "yarn run -s dev:svg:info; run-p -s dev:svg:inline dev:svg:src;", 18 | "dev:svg:unlink": "yarn run -s dev:svg:info; rm src/_client/svg/compiled/$n; rm static/svg/$n", 19 | "dev:svg:inline": "svgo -q --config=src/_server/build/svgo-config-inline.yml $f -o src/_client/svg/compiled", 20 | "dev:svg:src": "svgo -q --config=src/_server/build/svgo-config-src.yml $f -o static/svg", 21 | "dev:svg:cleanup": "rm -rf src/_client/svg/compiled; mkdir src/_client/svg/compiled; rm -rf static/svg; mkdir static/svg; for i in $(find src/_client/svg/originals -name '*.svg' -type f); do f=$i c=\"\\033[32m\" yarn run -s dev:svg:compile; done;", 22 | "watch:svg": "SHELL=/bin/bash chokidar 'src/_client/svg/originals/**.svg' -c 'f={path} e={event} run-s -s dev:svg:if' --silent", 23 | "m:up": "node -r esm src/_server/utils/migration-shell", 24 | "m:up:all": "node -r esm src/_server/utils/migration-shell", 25 | "m:down": "node -r esm src/_server/utils/migration-shell", 26 | "m:down:all": "node -r esm src/_server/utils/migration-shell", 27 | "m:create": "node -r esm src/_server/utils/migration-shell", 28 | "m:list": "node -r esm src/_server/utils/migration-shell", 29 | "m:test:up": "node -r esm src/_server/utils/migration-shell", 30 | "m:test:down": "node -r esm src/_server/utils/migration-shell" 31 | }, 32 | "dependencies": { 33 | "ansi-colors": "3.2.4", 34 | "arangojs": "6.10.0", 35 | "bcryptjs": "2.4.3", 36 | "body-parser": "1.18.3", 37 | "compression": "1.7.4", 38 | "cookie-parser": "1.4.4", 39 | "cross-fetch": "3.0.2", 40 | "esm": "3.2.18", 41 | "express": "4.16.4", 42 | "express-graphql": "0.7.1", 43 | "globby": "9.1.0", 44 | "graphql": "14.1.1", 45 | "graphql-tools": "4.0.4", 46 | "helmet": "3.16.0", 47 | "jsonwebtoken": "8.5.1", 48 | "just-debounce-it": "1.1.0", 49 | "lodash": "4.17.11", 50 | "lodash-es": "4.17.11", 51 | "morgan": "1.9.1", 52 | "passport": "0.4.0", 53 | "passport-local": "1.0.0", 54 | "sirv": "0.2.2" 55 | }, 56 | "devDependencies": { 57 | "@babel/core": "^7.0.0", 58 | "@babel/plugin-syntax-dynamic-import": "^7.0.0", 59 | "@babel/plugin-transform-runtime": "^7.0.0", 60 | "@babel/preset-env": "^7.0.0", 61 | "@babel/runtime": "^7.0.0", 62 | "babel-plugin-transform-runtime": "6.23.0", 63 | "babel-preset-env": "1.7.0", 64 | "babel-register": "6.26.0", 65 | "chokidar": "^2.0.4", 66 | "chokidar-cli": "1.2.2", 67 | "dotenv-expand": "5.1.0", 68 | "dotenv-extended": "2.4.0", 69 | "dotenv-parse-variables": "0.2.0", 70 | "livereload": "0.7.0", 71 | "migrate": "1.6.2", 72 | "npm-run-all": "^4.1.5", 73 | "papaparse": "4.6.3", 74 | "postcss": "7.0.14", 75 | "postcss-cli": "6.1.2", 76 | "postcss-custom-media": "7.0.7", 77 | "postcss-easy-import": "3.0.0", 78 | "postcss-functions": "3.0.0", 79 | "postcss-global-nested": "1.2.0", 80 | "postcss-hexrgba": "1.0.1", 81 | "postcss-math": "0.0.10", 82 | "postcss-media-minmax": "4.0.0", 83 | "postcss-nested": "4.1.2", 84 | "postcss-reporter": "6.0.1", 85 | "postcss-scss": "2.0.0", 86 | "postcss-simple-vars": "5.0.2", 87 | "postcss-strip-inline-comments": "0.1.5", 88 | "rollup": "1.6.0", 89 | "rollup-plugin-babel": "^4.0.2", 90 | "rollup-plugin-commonjs": "^9.1.6", 91 | "rollup-plugin-node-resolve": "^4.0.0", 92 | "rollup-plugin-replace": "2.1.1", 93 | "rollup-plugin-svelte": "^5.0.1", 94 | "rollup-plugin-terser": "^4.0.4", 95 | "sapper": "0.26.0-alpha.12", 96 | "svelte": "3.0.0-beta.19", 97 | "svgo": "1.2.0", 98 | "tinycolor2": "1.4.1" 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import chalk from 'chalk' 3 | import './src/_server/build/config' 4 | 5 | import resolve from 'rollup-plugin-node-resolve'; 6 | import replace from 'rollup-plugin-replace'; 7 | import commonjs from 'rollup-plugin-commonjs'; 8 | // import builtins from 'rollup-plugin-node-builtins' 9 | // import globals from 'rollup-plugin-node-globals' 10 | import svelte from 'rollup-plugin-svelte'; 11 | import babel from 'rollup-plugin-babel'; 12 | import { terser } from 'rollup-plugin-terser'; 13 | import config from 'sapper/config/rollup.js'; 14 | import pkg from './package.json'; 15 | 16 | import preprocess from './src/_server/build/rollup.preprocess' 17 | import sharedVars from './src/_server/build/rollup.vars' 18 | 19 | const mode = process.env.NODE_ENV; 20 | const dev = mode === 'development'; 21 | const legacy = !!process.env.SAPPER_LEGACY_BUILD; 22 | 23 | export default { 24 | client: { 25 | input: config.client.input(), 26 | output: config.client.output(), 27 | onwarn, 28 | plugins: [ 29 | replace(Object.assign({ 30 | 'process.browser': true, 31 | 'process.server': false, 32 | }, sharedVars)), 33 | svelte({ 34 | dev, 35 | extensions: ['.html', '.svelte', '.svg'], 36 | hydratable: true, 37 | emitCss: true, 38 | preprocess: preprocess('client'), 39 | }), 40 | // globals(), 41 | // builtins(), 42 | resolve({ browser: true }), 43 | commonjs(), 44 | 45 | legacy && babel({ 46 | extensions: ['.js', '.mjs', '.html', '.svelte', '.svg'], 47 | runtimeHelpers: true, 48 | exclude: ['node_modules/@babel/**'], 49 | presets: [ 50 | ['@babel/preset-env', { 51 | targets: '> 0.25%, not dead', 52 | }], 53 | ], 54 | plugins: [ 55 | '@babel/plugin-syntax-dynamic-import', 56 | ['@babel/plugin-transform-runtime', { 57 | useESModules: true, 58 | }], 59 | ], 60 | }), 61 | 62 | !dev && terser({ 63 | module: true, 64 | }), 65 | ], 66 | }, 67 | 68 | server: { 69 | input: config.server.input(), 70 | output: config.server.output(), 71 | onwarn, 72 | plugins: [ 73 | replace(Object.assign({ 74 | 'process.browser': false, 75 | 'process.server': true, 76 | }, sharedVars)), 77 | svelte({ 78 | dev, 79 | extensions: ['.html', '.svelte', '.svg'], 80 | generate: 'ssr', 81 | preprocess: preprocess('server'), 82 | }), 83 | resolve(), 84 | commonjs(), 85 | ], 86 | external: Object.keys(pkg.dependencies).concat( 87 | require('module').builtinModules || Object.keys(process.binding('natives')) 88 | ), 89 | }, 90 | 91 | }; 92 | 93 | function onwarn(warning) { 94 | // Silence circular dependency warning for moment package 95 | if ( 96 | warning.code === 'CIRCULAR_DEPENDENCY' && 97 | !warning.importer.indexOf(path.normalize('src/node_modules/@sapper/')) 98 | ) { 99 | return 100 | } 101 | console.log() 102 | console.log(chalk.yellow(`${warning.message} in:`)) 103 | console.log(warning.filename.split(__dirname)[1]) 104 | console.log(warning.frame) 105 | console.log() 106 | } 107 | -------------------------------------------------------------------------------- /src/_client/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../.eslintrc.yml", 3 | "env": { 4 | "browser": true, 5 | "node": false 6 | }, 7 | "rules": { 8 | "no-console": "error" 9 | }, 10 | "globals": { 11 | "process": true 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/_client/js/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arxpoetica/stallion/44fdfad775b0cfbd019ae1c6feb8720de2b5957c/src/_client/js/.gitkeep -------------------------------------------------------------------------------- /src/_client/postcss/global.postcss: -------------------------------------------------------------------------------- 1 | @import 'shared/variables'; 2 | @import 'shared/helpers'; 3 | @import 'main/basics'; 4 | 5 | @import 'modules/structure'; 6 | @import 'modules/buttons'; 7 | 8 | @import 'main/layout'; 9 | -------------------------------------------------------------------------------- /src/_client/postcss/main/_layout.postcss: -------------------------------------------------------------------------------- 1 | html { 2 | background-color: $white; 3 | font: 62.5%/1.2 $font; 4 | /* font-size: 100%; */ 5 | box-sizing: border-box; 6 | text-size-adjust: 100%; 7 | /* -ms-overflow-style: -ms-autohiding-scrollbar; */ 8 | &.whiteout { 9 | background-color: $white; 10 | pointer-events: none; 11 | body { 12 | opacity: 0; 13 | } 14 | } 15 | } 16 | body { 17 | margin: 0; 18 | background-color: $white; 19 | color: $black; 20 | font: 1.4rem/1.35 $font; 21 | /* -webkit-font-smoothing: antialiased; */ 22 | /* -moz-osx-font-smoothing: grayscale; */ 23 | } 24 | -------------------------------------------------------------------------------- /src/_client/postcss/modules/_buttons.postcss: -------------------------------------------------------------------------------- 1 | .buttons { 2 | display: flex; 3 | .btn { 4 | margin: 0 0.25rem; 5 | } 6 | } 7 | .btn { 8 | display: inline-flex; 9 | align-items: center; 10 | justify-content: center; 11 | margin: 0 0 1rem; 12 | padding: 0.8rem 1.6rem; 13 | background-color: $blue-l2; 14 | border: 0.1rem solid transparent; 15 | border-radius: 0.2rem; 16 | color: $white; 17 | font: $bold 1.4rem/1 $font; 18 | text-align: center; 19 | transition: background-color 0.15s ease-in-out, color 0.15s ease-in-out; 20 | cursor: pointer; 21 | -webkit-appearance: none; 22 | &:hover, 23 | &:focus { 24 | background-color: $blue-l4; 25 | } 26 | .icon { 27 | width: 22px; 28 | height: 22px; 29 | } 30 | .icon-text { 31 | margin: 0 0 0 12px; 32 | width: 75%; 33 | text-align: center; 34 | } 35 | &.tiny { 36 | padding: 0.4rem 0.8rem; 37 | font-size: 0.9rem; 38 | text-transform: uppercase; 39 | } 40 | &.small { 41 | padding: 0.5rem 0.8rem; 42 | font-size: 1.1rem; 43 | text-transform: uppercase; 44 | } 45 | &.large { 46 | padding: 1rem 3rem; 47 | font-size: 2.2rem; 48 | } 49 | &.huge { 50 | padding: 2rem 5rem; 51 | font-size: 3.4rem; 52 | } 53 | &.expanded { 54 | display: block; 55 | width: 100%; 56 | margin-right: 0; 57 | margin-left: 0; 58 | } 59 | &.outline { 60 | background-color: transparent; 61 | border: 3px solid $yellow-main; 62 | color: $black; 63 | &:hover, 64 | &:focus { 65 | background-color: $yellow-main; 66 | } 67 | } 68 | &.secondary { 69 | background-color: $green-main; 70 | color: $white; 71 | &:hover, 72 | &:focus { 73 | background-color: $green-l2; 74 | color: $white; 75 | } 76 | } 77 | &.black { 78 | background-color: $black; 79 | color: white; 80 | } 81 | &.success { 82 | background-color: $green-l2; 83 | color: $white; 84 | &:hover, 85 | &:focus { 86 | background-color: $green-l3; 87 | color: $white; 88 | } 89 | } 90 | &.warning { 91 | background-color: $yellow-l2; 92 | color: $black; 93 | &:hover, 94 | &:focus { 95 | background-color: $yellow-l4; 96 | color: $black; 97 | } 98 | } 99 | &.alert { 100 | background-color: $alert; 101 | color: #fefefe; 102 | &:hover, 103 | &:focus { 104 | background-color: $alert-light; 105 | color: #fefefe; 106 | } 107 | } 108 | &.inverse { 109 | background-color: rgba(255, 255, 255, 0.6); 110 | color: $black; 111 | border: 1px solid white; 112 | &:hover, 113 | &:focus { 114 | background-color: white; 115 | } 116 | } 117 | } 118 | 119 | .btn.disabled, 120 | .btn[disabled] { 121 | cursor: not-allowed; 122 | pointer-events: none; 123 | } 124 | 125 | .btn.disabled, 126 | .btn.disabled:hover, 127 | .btn.disabled:focus, 128 | .btn[disabled], 129 | .btn[disabled]:hover, 130 | .btn[disabled]:focus { 131 | background-color: $gray-5; 132 | color: white; 133 | } 134 | 135 | .btn.disabled.secondary, 136 | .btn[disabled].secondary { 137 | opacity: 0.25; 138 | cursor: not-allowed; 139 | } 140 | 141 | .btn.disabled.secondary, 142 | .btn.disabled.secondary:hover, 143 | .btn.disabled.secondary:focus, 144 | .btn[disabled].secondary, 145 | .btn[disabled].secondary:hover, 146 | .btn[disabled].secondary:focus { 147 | background-color: $green-l5; 148 | color: $black; 149 | } 150 | 151 | .btn.disabled.success, 152 | .btn[disabled].success { 153 | opacity: 0.25; 154 | cursor: not-allowed; 155 | } 156 | 157 | .btn.disabled.success, 158 | .btn.disabled.success:hover, 159 | .btn.disabled.success:focus, 160 | .btn[disabled].success, 161 | .btn[disabled].success:hover, 162 | .btn[disabled].success:focus { 163 | background-color: #3adb76; 164 | color: inherit; 165 | } 166 | 167 | .btn.disabled.warning, 168 | .btn[disabled].warning { 169 | opacity: 0.25; 170 | cursor: not-allowed; 171 | } 172 | 173 | .btn.disabled.warning, 174 | .btn.disabled.warning:hover, 175 | .btn.disabled.warning:focus, 176 | .btn[disabled].warning, 177 | .btn[disabled].warning:hover, 178 | .btn[disabled].warning:focus { 179 | background-color: $yellow-l4; 180 | color: inherit; 181 | } 182 | 183 | .btn.disabled.alert, 184 | .btn[disabled].alert { 185 | opacity: 0.25; 186 | cursor: not-allowed; 187 | } 188 | 189 | .btn.disabled.alert, 190 | .btn.disabled.alert:hover, 191 | .btn.disabled.alert:focus, 192 | .btn[disabled].alert, 193 | .btn[disabled].alert:hover, 194 | .btn[disabled].alert:focus { 195 | background-color: #cc4b37; 196 | color: #fefefe; 197 | } 198 | 199 | .btn.arrow-only::after { 200 | top: -0.1em; 201 | float: none; 202 | margin-left: 0; 203 | } 204 | 205 | a.btn:hover, 206 | a.btn:focus { 207 | text-decoration: none; 208 | } 209 | -------------------------------------------------------------------------------- /src/_client/postcss/modules/_structure.postcss: -------------------------------------------------------------------------------- 1 | .box { 2 | margin: 0 0 2rem; 3 | padding: 1rem; 4 | border: 1px solid $gray-6; 5 | background-color: $gray-light; 6 | :last-child { 7 | margin-bottom: 0; 8 | } 9 | &.warning { 10 | background-color: $yellow-light; 11 | border-color: $yellow-l4; 12 | } 13 | } 14 | 15 | .flex { 16 | display: flex; 17 | margin: 0 0 2rem; 18 | } 19 | .flex-split { 20 | flex-basis: 48%; 21 | &:last-child { 22 | margin-left: 4%; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/_client/postcss/routes-includes.postcss: -------------------------------------------------------------------------------- 1 | @import 'shared/variables'; 2 | -------------------------------------------------------------------------------- /src/_client/postcss/shared/_helpers.postcss: -------------------------------------------------------------------------------- 1 | 2 | /** >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> 3 | * `.ghost` helper to visually hide elements on the page 4 | * mostly used for accessibility purposes 5 | *** >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>*/ 6 | 7 | .ghost { 8 | position: absolute !important; 9 | width: 1px; 10 | height: 1px; 11 | padding: 0; 12 | overflow: hidden; 13 | clip: rect(0, 0, 0, 0); 14 | white-space: nowrap; 15 | -webkit-clip-path: inset(50%); 16 | clip-path: inset(50%); 17 | border: 0; 18 | } 19 | 20 | .visibility-hidden { 21 | visibility: hidden; 22 | } 23 | 24 | /** >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> 25 | * Helper to group JS for ease of development 26 | * @helper: .hidden-js 27 | *** >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>*/ 28 | 29 | .hidden-js { 30 | visibility: hidden; 31 | } 32 | -------------------------------------------------------------------------------- /src/_client/postcss/shared/_variables.postcss: -------------------------------------------------------------------------------- 1 | // https://app.frontify.com/document/84675 2 | 3 | // $body-width: 950px; 4 | // $content-width: 900px; 5 | 6 | // $font: 'Source Sans Pro', Helvetica, Arial, sans-serif; 7 | $font: 'Helvetica Neue', Helvetica, Arial, sans-serif; 8 | $table-font: Verdana, Geneva, monospace; 9 | $mono-font: Consolas, "Liberation Mono", Courier, monospace; 10 | // $input-font: Arial, "Helvetica Neue", Helvetica, sans-serif; 11 | // $input-font: "Trebuchet MS", "Lucida Grande", "Lucida Sans Unicode", "Lucida Sans", Tahoma, sans-serif; 12 | 13 | $light: 200; 14 | $normal: 400; 15 | $bold: 600; 16 | $heavy: 800; 17 | 18 | $white: white; 19 | $black: #1e3d43; 20 | 21 | $gray-dark: #3b5054; 22 | $gray-1: #4e5e61; 23 | $gray-2: #6b787b; 24 | $gray-3: #889294; 25 | $gray-4: #aeb6b8; 26 | $gray-5: #cbd0d1; 27 | $gray-6: #e1e5e6; 28 | $gray-7: #f0f4f5; 29 | $gray-light: #f7f9fa; 30 | 31 | $red-dark: #742d30; 32 | $red-d2: #a03136; 33 | $red-d1: #b33630; 34 | $red-main: #c93e37; 35 | $red-l1: #ed5c55; 36 | $red-l2: #dc443d; 37 | $red-l3: #f87d77; 38 | $red-l4: #fba4a0; 39 | $red-l5: #fec9c6; 40 | $red-light: #fce8e7; 41 | 42 | $yellow-dark: #997100; 43 | $yellow-d2: #ba8e15; 44 | $yellow-d1: #cfa123; 45 | $yellow-main: #e5b737; 46 | $yellow-l1: #fbd261; 47 | $yellow-l2: #f4c543; 48 | $yellow-l3: #f9da85; 49 | $yellow-l4: #fbe6ab; 50 | $yellow-l5: #fceec8; 51 | $yellow-light: #fff8e4; 52 | 53 | $blue-dark: #04243d; 54 | $blue-d2: #09365a; 55 | $blue-d1: #0c4370; 56 | $blue-main: #155284; 57 | $blue-l1: #4786ba; 58 | $blue-l2: #286ba2; 59 | $blue-l3: #5b99cc; 60 | $blue-l4: #89bbe4; 61 | $blue-l5: #b8d9f4; 62 | $blue-light: #dbedfb; 63 | 64 | $orange-dark: #a13d00; 65 | $orange-d1: #d75100; 66 | $orange-main: #ff6000; 67 | $orange-l1: #ff7624; 68 | $orange-l2: #ff9e63; 69 | $orange-l3: #ff8e4a; 70 | $orange-l4: #ffae7d; 71 | $orange-l5: #ffc39e; 72 | $orange-l6: #ffd9c2; 73 | $orange-light: #ffede3; 74 | 75 | $green-dark: #00451d; 76 | $green-d2: #095f2d; 77 | $green-d1: #217846; 78 | $green-main: #368457; 79 | $green-l1: #67b387; 80 | $green-l2: #529d71; 81 | $green-l3: #79bf97; 82 | $green-l4: #9cd5b4; 83 | $green-l5: #b5e6ca; 84 | $green-light: #ddf8e9; 85 | 86 | $links: $blue-l3; 87 | 88 | $alert: $red-main; 89 | $alert-light: $red-l3; 90 | 91 | $table-border: #ccc; 92 | $table-bg: #f3f3f3; 93 | 94 | $form-white: #fefefe; 95 | $access-blue: #c4efff; 96 | $access-blue-alt: #9cccdf; 97 | 98 | $facebook: #3b5998; 99 | $facebook-alt: #5e7bba; 100 | $google: #4285f4; 101 | $google-alt: #78a9f7; 102 | $github: #181717; 103 | $github-alt: #4f4f4f; 104 | 105 | $max: 1000px; 106 | 107 | $header-height: 60px; 108 | $header-height-noauth: 120px; 109 | $sidebar-width: 22rem; 110 | 111 | $z-back: 0; 112 | $z-middle: 500; 113 | $z-front: 1000; 114 | $z-absolute-front: 9999999999; 115 | 116 | // see: https://www.sitepoint.com/the-postcss-guide-to-improving-selectors-and-media-queries/ 117 | 118 | $res-smallest: 450px; 119 | $res-small: 768px; 120 | $res-medium: 1024px; 121 | $res-large: 1400px; 122 | $res-huge: 1900px; 123 | 124 | @custom-media --smallest (width < $res-smallest); 125 | @custom-media --small (width < $res-small); 126 | @custom-media --medium ($res-small <= width < $res-medium); 127 | @custom-media --medium-down (width < $res-medium); 128 | @custom-media --medium-up (width >= $res-small); 129 | @custom-media --large ($res-medium <= width < $res-large); 130 | @custom-media --large-up (width >= $res-medium); 131 | @custom-media --huge (width >= $res-large); 132 | @custom-media --max (width >= $res-huge); 133 | 134 | // @media (--small) { :global(body) { background-color: red !important; } } 135 | // @media (--medium) { :global(body) { background-color: orange !important; } } 136 | // @media (--large) { :global(body) { background-color: yellow !important; } } 137 | // @media (--huge) { :global(body) { background-color: green !important; } } 138 | // @media (--max) { :global(body) { background-color: blue !important; } } 139 | 140 | // @media (--small) { .c-App { background-color: red !important; } } 141 | // @media (--medium) { .c-App { background-color: orange !important; } } 142 | // @media (--large) { .c-App { background-color: yellow !important; } } 143 | // @media (--huge) { .c-App { background-color: green !important; } } 144 | // @media (--max) { .c-App { background-color: blue !important; } } 145 | -------------------------------------------------------------------------------- /src/_client/svg/compiled/arrow-down.svg: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /src/_client/svg/compiled/arrow-q.svg: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /src/_client/svg/compiled/logo-public.svg: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /src/_client/svg/compiled/select-handle-off.svg: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /src/_client/svg/compiled/select-handle-on.svg: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /src/_client/svg/compiled/select-handle.svg: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /src/_client/svg/compiled/social-email.svg: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /src/_client/svg/compiled/social-facebook.svg: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /src/_client/svg/compiled/social-github.svg: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /src/_client/svg/compiled/social-gitlab.svg: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /src/_client/svg/compiled/social-google.svg: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /src/_client/svg/compiled/social-instagram.svg: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /src/_client/svg/compiled/social-linkedin.svg: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /src/_client/svg/compiled/social-twitter.svg: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /src/_client/svg/compiled/social-youtube.svg: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /src/_client/svg/compiled/valid-bad.svg: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /src/_client/svg/compiled/valid-good.svg: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /src/_client/svg/originals/arrow-down.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/_client/svg/originals/arrow-q.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/_client/svg/originals/logo-public.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/_client/svg/originals/select-handle-off.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/_client/svg/originals/select-handle-on.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/_client/svg/originals/select-handle.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/_client/svg/originals/social-email.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/_client/svg/originals/social-facebook.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/_client/svg/originals/social-github.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/_client/svg/originals/social-gitlab.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/_client/svg/originals/social-google.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/_client/svg/originals/social-instagram.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/_client/svg/originals/social-linkedin.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/_client/svg/originals/social-twitter.svg: -------------------------------------------------------------------------------- 1 | 5 | -------------------------------------------------------------------------------- /src/_client/svg/originals/social-youtube.svg: -------------------------------------------------------------------------------- 1 | 5 | -------------------------------------------------------------------------------- /src/_client/svg/originals/valid-bad.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/_client/svg/originals/valid-good.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/_server/build/babel-register-compiler.js: -------------------------------------------------------------------------------- 1 | require('babel-core/register')({ 2 | presets: ['env'], 3 | ignore: 'node_modules', 4 | plugins: [ 5 | ['transform-runtime', { 6 | polyfill: false, 7 | regenerator: true, 8 | }], 9 | ], 10 | }) 11 | module.exports = () => {} 12 | -------------------------------------------------------------------------------- /src/_server/build/config.js: -------------------------------------------------------------------------------- 1 | import dotenv from 'dotenv-extended' 2 | import dotenvExpand from 'dotenv-expand' 3 | import dotenvParseVariables from 'dotenv-parse-variables' 4 | 5 | // NOTE: this doesn't actually set the environment variable -- it just tells dotenv whether to be silent or not 6 | const silent = !!(process.env.NODE_ENV || 'development').match(/(production|staging)/g) 7 | const settings = { silent } 8 | let config = dotenv.load(settings) 9 | 10 | // TODO: I'm not sure `dotenvExpand` and `dotenvParseVariables` are 11 | // working their way down to `process.env` 12 | config = dotenvExpand(config) 13 | config = dotenvParseVariables(config) 14 | config.self = true 15 | 16 | export const varsOnly = config 17 | export default process.env 18 | -------------------------------------------------------------------------------- /src/_server/build/postcss.config.js: -------------------------------------------------------------------------------- 1 | const plugins = require('./postcss.config.vars').plugins 2 | 3 | // const name = process.env.BUNDLE 4 | // const isLib = !!name.match(/Lib$/) 5 | 6 | module.exports = (ctx) => ({ 7 | syntax: require('postcss-scss'), 8 | map: ctx.options.map, 9 | plugins, 10 | }) 11 | -------------------------------------------------------------------------------- /src/_server/build/postcss.config.vars.js: -------------------------------------------------------------------------------- 1 | const tinycolor = require('tinycolor2') 2 | 3 | module.exports.plugins = [ 4 | require('postcss-easy-import')({ 5 | path: ['src/_client/postcss', 'router'], 6 | extensions: ['.css', '.scss', '.postcss'], 7 | prefix: '_', 8 | }), 9 | require('postcss-simple-vars'), 10 | require('postcss-functions')({ 11 | functions: { 12 | // url: path => { 13 | // return `url('../${path.replace(/["']/g, '')}')` 14 | // }, 15 | urlstatic: path => { 16 | return `url(${path})` 17 | }, 18 | em: (fontSize, parentFontSize) => { 19 | parentFontSize = parentFontSize || 10 20 | return (Math.round(fontSize / parentFontSize * 1000) / 1000) + 'em' 21 | }, 22 | fw: (targetFontSize, targetViewportWidth) => { 23 | targetViewportWidth = targetViewportWidth || 1000 24 | return (Math.round(1000 / targetViewportWidth * targetFontSize / 10 * 1000) / 1000) + 'vw' 25 | }, 26 | lh: (fontSize, lineHeight) => { 27 | return Math.round(lineHeight / fontSize * 100) / 100 28 | }, 29 | ratio: (divider, divided) => { 30 | return Number((divider / divided * 100).toFixed(3)) + '%' 31 | }, 32 | tinycolor: (color, method, ...theArgs) => { 33 | return tinycolor(color)[method](...theArgs)/* .toString() */ 34 | }, 35 | percent: (maths, placeValue) => { 36 | placeValue = placeValue || 100 37 | return `resolve(round(${maths} * 100 * ${placeValue}) / ${placeValue})%` 38 | }, 39 | }, 40 | }), 41 | require('postcss-hexrgba'), 42 | require('postcss-custom-media'), 43 | require('postcss-media-minmax'), 44 | require('postcss-nested'), 45 | require('postcss-global-nested'), 46 | require('postcss-math'), 47 | // (!isLib && require('autoprefixer')), 48 | // require('autoprefixer')({ 49 | // // browsers: ['last 2 versions', 'ie >= 9', 'Android >= 2.3', 'ios >= 7'], 50 | // browsers: [ 51 | // 'last 3 Chrome versions', 52 | // 'last 3 Firefox versions', 53 | // 'Safari >= 9', 54 | // 'Edge >= 13', 55 | // 'IE >= 11', 56 | // 'last 3 ChromeAndroid versions', 57 | // 'last 3 FirefoxAndroid versions', 58 | // 'iOS >= 9', 59 | // 'Android >= 4.4', 60 | // ], 61 | // }), 62 | require('postcss-strip-inline-comments'), 63 | require('postcss-reporter'), 64 | ] 65 | -------------------------------------------------------------------------------- /src/_server/build/rollup.preprocess.js: -------------------------------------------------------------------------------- 1 | import postcss from 'postcss' 2 | import postcssScssSyntax from 'postcss-scss' 3 | // UNFORTUNATELY THIS IS AN ABSOLUTE PATH, WEIRDLY 4 | const plugins = require('./src/_server/build/postcss.config.vars').plugins 5 | 6 | export default function(/* domain */) { 7 | // NOTE: `domain` is useful for debugging if it is SSR or DOM 8 | return { 9 | style: async({ content, attributes, filename }) => { 10 | if (attributes.type !== 'text/scss') { 11 | return { code: content/* , map: '' */ } 12 | } 13 | try { 14 | const result = await postcss(plugins).process('@import \'routes-includes\';\n' + content, { 15 | from: 'src', 16 | syntax: postcssScssSyntax, 17 | // TODO: unclear if maps are needed. ASK in the forum 18 | // map: true, 19 | }) 20 | if (result.css && typeof result.css === 'string') { 21 | return { 22 | code: result.css.toString(), 23 | // map: result.map.toString(), 24 | } 25 | } else { 26 | return { code: ''/* , map: '' */ } 27 | } 28 | 29 | } catch (error) { 30 | console.log('Error: something went wrong'.red) 31 | console.log('Error: something went wrong'.red) 32 | console.log('Error: something went wrong'.red) 33 | console.log('Error: something went wrong'.red) 34 | console.log(error) 35 | return { code: ''/* , map: '' */ } 36 | } 37 | }, 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/_server/build/rollup.vars.js: -------------------------------------------------------------------------------- 1 | import { varsOnly } from './config' 2 | const rollupVars = {} 3 | for (let key in varsOnly) { 4 | if (key.match(/^STALLION_/)) { 5 | rollupVars[`process.env.${key}`] = JSON.stringify(varsOnly[key]) 6 | } 7 | } 8 | rollupVars['process.env.NODE_ENV'] = JSON.stringify(process.env.NODE_ENV) 9 | rollupVars['process.env.PORT'] = JSON.stringify(process.env.PORT) 10 | // console.log(varsOnly) 11 | // console.log(rollupVars) 12 | 13 | export default rollupVars 14 | -------------------------------------------------------------------------------- /src/_server/build/svgo-config-inline.yml: -------------------------------------------------------------------------------- 1 | # SEE: https://raw.githubusercontent.com/svg/svgo/master/.svgo.yml 2 | 3 | # multipass: true 4 | # full: true 5 | 6 | plugins: 7 | # - addAttributesToSVGElement: false # default 8 | # - addClassesToSVGElement: 9 | # className: 'svg-inline' 10 | # - cleanupAttrs: true # default 11 | # - cleanupEnableBackground: true # default 12 | # - cleanupIDs: true # default 13 | # - cleanupListOfValues: false # default 14 | # - cleanupNumericValues: true # default 15 | # - collapseGroups: true # default 16 | # - convertColors: true # default 17 | # - convertPathData: true # default 18 | # - convertShapeToPath: true # default 19 | # - convertStyleToAttrs: true # default 20 | # - convertTransform: true # default 21 | # - inlineStyles: true # default 22 | # - mergePaths: true # default 23 | # - minifyStyles: true # default 24 | # - moveElemsAttrsToGroup: true # default 25 | # - moveGroupAttrsToElems: true # default 26 | # - prefixIds: false # default 27 | # - removeAttrs: false # default 28 | # - removeAttrs: 29 | # attrs: 30 | # - 'stroke' 31 | # - 'fill' 32 | # - 'fill-rule' 33 | # - removeComments: true # default 34 | # - removeDesc: true # default 35 | - removeDimensions: true 36 | # - removeDoctype: true # default 37 | # - removeEditorsNSData: true # default 38 | # - removeElementsByAttr: false # default 39 | # - removeEmptyAttrs: true # default 40 | # - removeEmptyContainers: true # default 41 | # - removeEmptyText: true # default 42 | # - removeHiddenElems: true # default 43 | # - removeMetadata: true # default 44 | # - removeNonInheritableGroupAttrs: true # default 45 | - removeRasterImages: true 46 | - removeScriptElement: true 47 | - removeStyleElement: true 48 | # - removeTitle: true # default 49 | # - removeUnknownsAndDefaults: true # default 50 | # - removeUnusedNS: true # default 51 | # - removeUselessDefs: true # default 52 | # - removeUselessStrokeAndFill: true # default 53 | - removeViewBox: false 54 | - removeXMLNS: true 55 | # - removeXMLProcInst: true # default 56 | # - sortAttrs: false # default 57 | 58 | # configure the indent (default 4 spaces) used by `--pretty` here: 59 | js2svg: 60 | pretty: true 61 | indent: ' ' 62 | # @see https://github.com/svg/svgo/blob/master/lib/svgo/js2svg.js#L6 for more config options 63 | -------------------------------------------------------------------------------- /src/_server/build/svgo-config-src.yml: -------------------------------------------------------------------------------- 1 | # SEE: https://raw.githubusercontent.com/svg/svgo/master/.svgo.yml 2 | 3 | # multipass: true 4 | # full: true 5 | 6 | plugins: 7 | # - addAttributesToSVGElement: false # default 8 | - addClassesToSVGElement: 9 | className: 'svg-src' 10 | # - cleanupAttrs: true # default 11 | # - cleanupEnableBackground: true # default 12 | # - cleanupIDs: true # default 13 | # - cleanupListOfValues: false # default 14 | # - cleanupNumericValues: true # default 15 | # - collapseGroups: true # default 16 | # - convertColors: true # default 17 | # - convertPathData: true # default 18 | # - convertShapeToPath: true # default 19 | # - convertStyleToAttrs: true # default 20 | # - convertTransform: true # default 21 | # - inlineStyles: true # default 22 | # - mergePaths: true # default 23 | # - minifyStyles: true # default 24 | # - moveElemsAttrsToGroup: true # default 25 | # - moveGroupAttrsToElems: true # default 26 | # - prefixIds: false # default 27 | # - removeAttrs: false # default 28 | # - removeAttrs: 29 | # attrs: 30 | # - 'stroke' 31 | # - 'fill' 32 | # - 'fill-rule' 33 | # - removeComments: true # default 34 | # - removeDesc: true # default 35 | # - removeDimensions: false # default 36 | # - removeDoctype: true # default 37 | # - removeEditorsNSData: true # default 38 | # - removeElementsByAttr: false # default 39 | # - removeEmptyAttrs: true # default 40 | # - removeEmptyContainers: true # default 41 | # - removeEmptyText: true # default 42 | # - removeHiddenElems: true # default 43 | # - removeMetadata: true # default 44 | # - removeNonInheritableGroupAttrs: true # default 45 | - removeRasterImages: true 46 | - removeScriptElement: true 47 | - removeStyleElement: true 48 | # - removeTitle: true # default 49 | # - removeUnknownsAndDefaults: true # default 50 | # - removeUnusedNS: true # default 51 | # - removeUselessDefs: true # default 52 | # - removeUselessStrokeAndFill: true # default 53 | # - removeViewBox: true # default 54 | # - removeXMLNS: false # default 55 | # - removeXMLProcInst: true # default 56 | # - sortAttrs: false # default 57 | 58 | # configure the indent (default 4 spaces) used by `--pretty` here: 59 | js2svg: 60 | pretty: true 61 | indent: ' ' 62 | # @see https://github.com/svg/svgo/blob/master/lib/svgo/js2svg.js#L6 for more config options 63 | -------------------------------------------------------------------------------- /src/_server/db/arangodb-api.js: -------------------------------------------------------------------------------- 1 | // TODO: OUTPUT IN DEVELOPMENT (AND PRODUCTION???) ARANGODB ACTIVITY LOGS 2 | // TODO: OUTPUT IN DEVELOPMENT (AND PRODUCTION???) ARANGODB ACTIVITY LOGS 3 | // TODO: OUTPUT IN DEVELOPMENT (AND PRODUCTION???) ARANGODB ACTIVITY LOGS 4 | 5 | // import { aql } from 'arangojs' 6 | import { driver } from './arangodb-driver' 7 | 8 | export const getApi = name => { 9 | 10 | const db = driver.connect(name) 11 | 12 | return { 13 | 14 | get: async function(collection, key) { 15 | if (!collection) { throw new Error('No collection set in `get`') } 16 | if (!key) { throw new Error('No key set in `get`') } 17 | const query = ` 18 | FOR document IN \`${collection}\` 19 | FILTER document._key == @key 20 | RETURN document 21 | ` 22 | const cursor = await db.query(query, { key }) 23 | return cursor.next() 24 | }, 25 | 26 | getAll: async function(collection, keys) { 27 | if (!collection) { throw new Error('No collection set in `getAll`') } 28 | keys = keys || [] 29 | keys = Array.isArray(keys) ? keys : [keys] 30 | const query = ` 31 | FOR document IN \`${collection}\` 32 | ${keys.length ? 'FILTER document._key IN @keys' : ''} 33 | LIMIT 100 34 | RETURN document 35 | ` 36 | const queryPromise = keys.length ? db.query(query, { keys }) : db.query(query) 37 | const cursor = await queryPromise 38 | return await cursor.all() 39 | }, 40 | 41 | set: async function(collection, doc) { 42 | if (!collection) { throw new Error('No collection set in `set`') } 43 | if (!doc) { throw new Error('No document set in `set`') } 44 | const query = ` 45 | INSERT ${JSON.stringify(doc)} 46 | INTO \`${collection}\` 47 | RETURN NEW 48 | ` 49 | const cursor = await db.query(query) 50 | return cursor.next() 51 | }, 52 | 53 | update: async function(collection, key, updates) { 54 | if (!collection) { throw new Error('No collection set in `update`') } 55 | if (!key) { throw new Error('No key set in `update`') } 56 | if (!updates) { throw new Error('No updates array set in `updates`') } 57 | const query = ` 58 | UPDATE DOCUMENT("${collection}/${key}") 59 | WITH ${JSON.stringify(updates)} 60 | IN ${collection} 61 | OPTIONS { keepNull: false } 62 | RETURN NEW 63 | ` 64 | const cursor = await db.query(query) 65 | return cursor.next() 66 | }, 67 | 68 | remove: async function(collection, key) { 69 | if (!collection) { throw new Error('No collection set in `remove`') } 70 | if (!key) { throw new Error('No key set in `remove`') } 71 | const query = `REMOVE '${key}' IN \`${collection}\`` 72 | const cursor = await db.query(query) 73 | return cursor.next() 74 | }, 75 | 76 | drop: async function(collection) { 77 | if (!collection) { throw new Error('No collection set in `drop`') } 78 | const Collection = db.collection(collection) 79 | return await Collection.drop() 80 | }, 81 | 82 | traverse: async function(originCollection, key, edgeCollection, depth) { 83 | if (!originCollection) { throw new Error('No origin collection set in `traverse`') } 84 | if (!key) { throw new Error('No key set in `traverse`') } 85 | if (!edgeCollection) { throw new Error('No edge collection set in `traverse`') } 86 | depth = depth || '1..1' 87 | const query = ` 88 | FOR document IN ${depth} OUTBOUND "${originCollection}/${key}" \`${edgeCollection}\` 89 | LIMIT 100 90 | RETURN document 91 | ` 92 | const cursor = await db.query(query) 93 | return await cursor.all() 94 | }, 95 | 96 | // find a document by key & value 97 | find: async function(collection, filters) { 98 | try { 99 | if (!collection) { throw new Error('No collection set in `find`') } 100 | if (!filters) { throw new Error('No filters set in `find`') } 101 | let query = `FOR document IN \`${collection}\`` 102 | for (let key in filters) { 103 | const value = filters[key] 104 | if (typeof value === 'string') { 105 | query += ` FILTER document.${key} == "${value}"` 106 | } else { 107 | query += ` FILTER document.${key} == ${value}` 108 | } 109 | } 110 | query += ' RETURN document' 111 | const cursor = await db.query(query) 112 | return cursor.next() 113 | } catch (error) { 114 | return { error: true, message: error.message ? error.message : 'Unknown error' } 115 | } 116 | }, 117 | 118 | } 119 | 120 | } 121 | -------------------------------------------------------------------------------- /src/_server/db/arangodb-driver.js: -------------------------------------------------------------------------------- 1 | // https://docs.arangodb.com/devel/Drivers/JS/Reference 2 | import arangojs from 'arangojs' 3 | const databases = {} 4 | 5 | export const driver = { 6 | 7 | connect: function(name) { 8 | if (databases[name]) { 9 | return databases[name] 10 | } 11 | databases[name] = arangojs({ url: process.env.STALLION_ARANGODB_CONNECTION }) 12 | databases[name].useDatabase(name || process.env.STALLION_ARANGODB_DB) 13 | databases[name].useBasicAuth(process.env.STALLION_ARANGODB_USERNAME, process.env.STALLION_ARANGODB_PASSWORD) 14 | return databases[name] 15 | }, 16 | 17 | } 18 | -------------------------------------------------------------------------------- /src/_server/db/validators/generic.js: -------------------------------------------------------------------------------- 1 | // SEE: http://www.ex-parrot.com/~pdw/Mail-RFC822-Address.html 2 | // const LOOSE_EMAIL_REQUIREMENT = /.+@[^.]+\.[^.]/ 3 | export const emailRegex = /(?:(?:\r\n)?[ \t])*(?:(?:(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\\".\[\]]))|"(?:[^\"\r\\]|\\.|(?:(?:\r\n)?[ \t]))*"(?:(?:\r\n)?[ \t])*)(?:\.(?:(?:\r\n)?[ \t])*(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\\".\[\]]))|"(?:[^\"\r\\]|\\.|(?:(?:\r\n)?[ \t]))*"(?:(?:\r\n)?[ \t])*))*@(?:(?:\r\n)?[ \t])*(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\\".\[\]]))|\[([^\[\]\r\\]|\\.)*\](?:(?:\r\n)?[ \t])*)(?:\.(?:(?:\r\n)?[ \t])*(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\\".\[\]]))|\[([^\[\]\r\\]|\\.)*\](?:(?:\r\n)?[ \t])*))*|(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\\".\[\]]))|"(?:[^\"\r\\]|\\.|(?:(?:\r\n)?[ \t]))*"(?:(?:\r\n)?[ \t])*)*\<(?:(?:\r\n)?[ \t])*(?:@(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\\".\[\]]))|\[([^\[\]\r\\]|\\.)*\](?:(?:\r\n)?[ \t])*)(?:\.(?:(?:\r\n)?[ \t])*(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\\".\[\]]))|\[([^\[\]\r\\]|\\.)*\](?:(?:\r\n)?[ \t])*))*(?:,@(?:(?:\r\n)?[ \t])*(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\\".\[\]]))|\[([^\[\]\r\\]|\\.)*\](?:(?:\r\n)?[ \t])*)(?:\.(?:(?:\r\n)?[ \t])*(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\\".\[\]]))|\[([^\[\]\r\\]|\\.)*\](?:(?:\r\n)?[ \t])*))*)*:(?:(?:\r\n)?[ \t])*)?(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\\".\[\]]))|"(?:[^\"\r\\]|\\.|(?:(?:\r\n)?[ \t]))*"(?:(?:\r\n)?[ \t])*)(?:\.(?:(?:\r\n)?[ \t])*(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\\".\[\]]))|"(?:[^\"\r\\]|\\.|(?:(?:\r\n)?[ \t]))*"(?:(?:\r\n)?[ \t])*))*@(?:(?:\r\n)?[ \t])*(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\\".\[\]]))|\[([^\[\]\r\\]|\\.)*\](?:(?:\r\n)?[ \t])*)(?:\.(?:(?:\r\n)?[ \t])*(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\\".\[\]]))|\[([^\[\]\r\\]|\\.)*\](?:(?:\r\n)?[ \t])*))*\>(?:(?:\r\n)?[ \t])*)|(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\\".\[\]]))|"(?:[^\"\r\\]|\\.|(?:(?:\r\n)?[ \t]))*"(?:(?:\r\n)?[ \t])*)*:(?:(?:\r\n)?[ \t])*(?:(?:(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\\".\[\]]))|"(?:[^\"\r\\]|\\.|(?:(?:\r\n)?[ \t]))*"(?:(?:\r\n)?[ \t])*)(?:\.(?:(?:\r\n)?[ \t])*(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\\".\[\]]))|"(?:[^\"\r\\]|\\.|(?:(?:\r\n)?[ \t]))*"(?:(?:\r\n)?[ \t])*))*@(?:(?:\r\n)?[ \t])*(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\\".\[\]]))|\[([^\[\]\r\\]|\\.)*\](?:(?:\r\n)?[ \t])*)(?:\.(?:(?:\r\n)?[ \t])*(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\\".\[\]]))|\[([^\[\]\r\\]|\\.)*\](?:(?:\r\n)?[ \t])*))*|(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\\".\[\]]))|"(?:[^\"\r\\]|\\.|(?:(?:\r\n)?[ \t]))*"(?:(?:\r\n)?[ \t])*)*\<(?:(?:\r\n)?[ \t])*(?:@(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\\".\[\]]))|\[([^\[\]\r\\]|\\.)*\](?:(?:\r\n)?[ \t])*)(?:\.(?:(?:\r\n)?[ \t])*(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\\".\[\]]))|\[([^\[\]\r\\]|\\.)*\](?:(?:\r\n)?[ \t])*))*(?:,@(?:(?:\r\n)?[ \t])*(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\\".\[\]]))|\[([^\[\]\r\\]|\\.)*\](?:(?:\r\n)?[ \t])*)(?:\.(?:(?:\r\n)?[ \t])*(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\\".\[\]]))|\[([^\[\]\r\\]|\\.)*\](?:(?:\r\n)?[ \t])*))*)*:(?:(?:\r\n)?[ \t])*)?(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\\".\[\]]))|"(?:[^\"\r\\]|\\.|(?:(?:\r\n)?[ \t]))*"(?:(?:\r\n)?[ \t])*)(?:\.(?:(?:\r\n)?[ \t])*(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\\".\[\]]))|"(?:[^\"\r\\]|\\.|(?:(?:\r\n)?[ \t]))*"(?:(?:\r\n)?[ \t])*))*@(?:(?:\r\n)?[ \t])*(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\\".\[\]]))|\[([^\[\]\r\\]|\\.)*\](?:(?:\r\n)?[ \t])*)(?:\.(?:(?:\r\n)?[ \t])*(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\\".\[\]]))|\[([^\[\]\r\\]|\\.)*\](?:(?:\r\n)?[ \t])*))*\>(?:(?:\r\n)?[ \t])*)(?:,\s*(?:(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\\".\[\]]))|"(?:[^\"\r\\]|\\.|(?:(?:\r\n)?[ \t]))*"(?:(?:\r\n)?[ \t])*)(?:\.(?:(?:\r\n)?[ \t])*(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\\".\[\]]))|"(?:[^\"\r\\]|\\.|(?:(?:\r\n)?[ \t]))*"(?:(?:\r\n)?[ \t])*))*@(?:(?:\r\n)?[ \t])*(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\\".\[\]]))|\[([^\[\]\r\\]|\\.)*\](?:(?:\r\n)?[ \t])*)(?:\.(?:(?:\r\n)?[ \t])*(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\\".\[\]]))|\[([^\[\]\r\\]|\\.)*\](?:(?:\r\n)?[ \t])*))*|(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\\".\[\]]))|"(?:[^\"\r\\]|\\.|(?:(?:\r\n)?[ \t]))*"(?:(?:\r\n)?[ \t])*)*\<(?:(?:\r\n)?[ \t])*(?:@(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\\".\[\]]))|\[([^\[\]\r\\]|\\.)*\](?:(?:\r\n)?[ \t])*)(?:\.(?:(?:\r\n)?[ \t])*(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\\".\[\]]))|\[([^\[\]\r\\]|\\.)*\](?:(?:\r\n)?[ \t])*))*(?:,@(?:(?:\r\n)?[ \t])*(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\\".\[\]]))|\[([^\[\]\r\\]|\\.)*\](?:(?:\r\n)?[ \t])*)(?:\.(?:(?:\r\n)?[ \t])*(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\\".\[\]]))|\[([^\[\]\r\\]|\\.)*\](?:(?:\r\n)?[ \t])*))*)*:(?:(?:\r\n)?[ \t])*)?(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\\".\[\]]))|"(?:[^\"\r\\]|\\.|(?:(?:\r\n)?[ \t]))*"(?:(?:\r\n)?[ \t])*)(?:\.(?:(?:\r\n)?[ \t])*(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\\".\[\]]))|"(?:[^\"\r\\]|\\.|(?:(?:\r\n)?[ \t]))*"(?:(?:\r\n)?[ \t])*))*@(?:(?:\r\n)?[ \t])*(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\\".\[\]]))|\[([^\[\]\r\\]|\\.)*\](?:(?:\r\n)?[ \t])*)(?:\.(?:(?:\r\n)?[ \t])*(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\\".\[\]]))|\[([^\[\]\r\\]|\\.)*\](?:(?:\r\n)?[ \t])*))*\>(?:(?:\r\n)?[ \t])*))*)?;\s*)/ 4 | 5 | export const usernameRegex = /^[a-zA-Z0-9][a-zA-Z0-9-]{1,37}[a-zA-Z0-9]$/ 6 | -------------------------------------------------------------------------------- /src/_server/db/validators/graphql.js: -------------------------------------------------------------------------------- 1 | 2 | /* SEE: http://facebook.github.io/graphql/June2018/#sec-Names 3 | * 4 | * GraphQL Documents are full of named things: 5 | * operations, fields, arguments, types, directives, 6 | * fragments, and variables. All names must follow the 7 | * same grammatical form. 8 | */ 9 | export const namesRegex = /^[_A-Za-z][_0-9A-Za-z]{0,39}$/ 10 | -------------------------------------------------------------------------------- /src/_server/graphql-api/models/_mutations.gql: -------------------------------------------------------------------------------- 1 | type Mutation { 2 | addUser(first: String, last: String, email: String, picture: String, friends: [String]): User 3 | } 4 | -------------------------------------------------------------------------------- /src/_server/graphql-api/models/_mutations.map.js: -------------------------------------------------------------------------------- 1 | export default { 2 | Mutation: { 3 | }, 4 | } 5 | -------------------------------------------------------------------------------- /src/_server/graphql-api/models/_queries.gql: -------------------------------------------------------------------------------- 1 | type Query { 2 | # FIXME: this shouldn't be here: 3 | dataset(id: String!): String 4 | } 5 | -------------------------------------------------------------------------------- /src/_server/graphql-api/models/_queries.map.js: -------------------------------------------------------------------------------- 1 | export default { 2 | Query: { 3 | }, 4 | } 5 | -------------------------------------------------------------------------------- /src/_server/graphql-api/models/posts/posts.gql: -------------------------------------------------------------------------------- 1 | type Post { 2 | id: String 3 | name: String 4 | aspectWidth: Int 5 | aspectHeight: Int 6 | # comments: [Comment] 7 | } 8 | 9 | extend type Query { 10 | post(id: String!): Post 11 | posts(ids: [String]): [Post] 12 | } 13 | -------------------------------------------------------------------------------- /src/_server/graphql-api/models/posts/posts.map.js: -------------------------------------------------------------------------------- 1 | import mappers from '../../utils/mappers' 2 | import { getApi } from '../../../db/arangodb-api' 3 | const api = getApi() 4 | 5 | export default { 6 | Query: { 7 | post: async function(_, { id }) { 8 | const post = await api.get('posts', id) 9 | return mappers.keyToId(post) 10 | }, 11 | posts: async function() { 12 | const posts = await api.getAll('posts') 13 | return posts.map(post => mappers.keyToId(post)) 14 | }, 15 | }, 16 | // Mutation: { 17 | // addUser: (_, data) => { 18 | // const uid = uuid() 19 | // users[uid] = Object.assign({}, data, { 20 | // id: uid, 21 | // }) 22 | // return friendsMapper(uid) 23 | // }, 24 | // }, 25 | // Post: { 26 | // comments: async(post) => { 27 | // // TODO: are we going to traverse at any time????? 28 | // // const comments = await api.traverse('posts', post._key, 'post-has-comment') 29 | // const comments = await api.getAll('comments', post.commentKeys) 30 | // return comments.map(comment => mappers.keyToId(comment)) 31 | // }, 32 | // }, 33 | } 34 | -------------------------------------------------------------------------------- /src/_server/graphql-api/models/users/users.gql: -------------------------------------------------------------------------------- 1 | type User { 2 | username: String! 3 | email: String 4 | bio: String 5 | avatar: String 6 | displayName: String 7 | first: String 8 | last: String 9 | posts: [Post] 10 | } 11 | 12 | extend type Query { 13 | user(username: String!): User 14 | users: [User] 15 | } 16 | -------------------------------------------------------------------------------- /src/_server/graphql-api/models/users/users.map.js: -------------------------------------------------------------------------------- 1 | import mappers from '../../utils/mappers' 2 | import { getApi } from '../../../db/arangodb-api' 3 | const api = getApi() 4 | 5 | export default { 6 | Query: { 7 | users: async function() { 8 | try { 9 | const users = await api.getAll('users') 10 | return users.map(user => mappers.user(user)) 11 | } catch (error) { 12 | return [] 13 | } 14 | }, 15 | user: async function(_, { username }) { 16 | try { 17 | const user = await api.get('users', username) 18 | return mappers.user(user) 19 | } catch (error) { 20 | return {} 21 | } 22 | }, 23 | }, 24 | // Mutation: { 25 | // addUser: (_, data) => { 26 | // const uid = uuid() 27 | // users[uid] = Object.assign({}, data, { 28 | // id: uid, 29 | // }) 30 | // return friendsMapper(uid) 31 | // }, 32 | // }, 33 | User: { 34 | posts: async(user) => { 35 | const posts = await api.traverse('users', user.username, 'user-has-post') 36 | return posts.map(post => mappers.keyToId(post)) 37 | }, 38 | }, 39 | } 40 | -------------------------------------------------------------------------------- /src/_server/graphql-api/schema.js: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import { loadTypes, loadResolvers } from './utils/graphql-utils' 3 | import { makeExecutableSchema } from 'graphql-tools' 4 | 5 | export async function getSchema() { 6 | 7 | try { 8 | // load all graphql type definitions from `.gql` files 9 | const typePaths = path.resolve(process.cwd(), 'src/_server/graphql-api/models/**/*.gql') 10 | const typeDefs = await loadTypes(typePaths) 11 | // console.log(typePaths) 12 | // console.log(typeDefs) 13 | 14 | // load all graphql resolvers from `.map.js` files 15 | const resolverPaths = path.resolve(process.cwd(), 'src/_server/graphql-api/models/**/*.map.js') 16 | const resolvers = await loadResolvers(resolverPaths) 17 | // console.log(resolverPaths) 18 | // console.log(resolvers) 19 | 20 | // Put together a schema 21 | return makeExecutableSchema({ 22 | typeDefs, 23 | resolvers, 24 | // FIXME: should be dev only: 25 | logger: { log: error => console.log(error) }, 26 | }) 27 | } catch (error) { 28 | throw new Error(error) 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /src/_server/graphql-api/utils/graphql-utils.js: -------------------------------------------------------------------------------- 1 | // SEE: https://github.com/tadejstanic/graphql-api-example/blob/f8a8dd4f19f25bee8a9d32005ae047e0b7edf4e0/src/lib/graphqlFileLoader.js 2 | 3 | import fs from 'fs' 4 | import path from 'path' 5 | import util from 'util' 6 | import { merge } from 'lodash' 7 | import globby from 'globby' 8 | const readFile = util.promisify(fs.readFile) 9 | 10 | // FIXME: intend to use dynamic imports in the future: 11 | import _mutationsMap from '../models/_mutations.map' 12 | import _queriesMap from '../models/_queries.map' 13 | import usersMap from '../models/users/users.map' 14 | import postsMap from '../models/posts/posts.map' 15 | 16 | export const loadTypes = async pattern => { 17 | try { 18 | const files = await globby(pattern) 19 | if (!files.length) { throw new Error('globby found zero GraphQL type definition files. What gives?') } 20 | const filePromises = files.map(filename => readFile(filename, 'utf8')) 21 | const loadedFiles = await Promise.all(filePromises) 22 | return loadedFiles.join('\n') 23 | } catch (error) { 24 | throw new Error(error) 25 | } 26 | } 27 | 28 | export const loadResolvers = async pattern => { 29 | try { 30 | const files = await globby(pattern) 31 | if (!files.length) { throw new Error } 32 | const imports = files.map(filename => filename.split('/src/_server/graphql-api/models/')[1]) 33 | 34 | 35 | // FIXME: this is just some warning: delete when below is also fixed 36 | // FIXME: this is just some warning: delete when below is also fixed 37 | // FIXME: this is just some warning: delete when below is also fixed 38 | setTimeout(() => { 39 | const declaredImports = [ 40 | '../models/_mutations.map.js', 41 | '../models/_queries.map.js', 42 | '../models/users/users.map.js', 43 | '../models/posts/posts.map.js', 44 | ] 45 | for (let item of imports) { 46 | const found = declaredImports.find(declared => declared.indexOf(item) > -1) 47 | // console.log(item, found) 48 | if (!found) { 49 | console.log('ERROR!'.red, `You need to include ${item} in the imports in the graphql-utils.js file`.yellow) 50 | } 51 | } 52 | }, 200) 53 | 54 | // FIXME: use dynamic import in the future 55 | const loadedModules = [ 56 | _mutationsMap, 57 | _queriesMap, 58 | usersMap, 59 | postsMap, 60 | ] 61 | 62 | // // FIXME: rollup doesn't yet do dynamic imports with variables 63 | // // FIXME: rollup doesn't yet do dynamic imports with variables 64 | // // FIXME: rollup doesn't yet do dynamic imports with variables 65 | // // SEE: https://github.com/rollup/rollup/issues/2097 66 | // // AND: https://github.com/rollup/rollup/issues/2092 67 | // const modulePromises = [ 68 | // import('../models/_mutations.map.js'), 69 | // import('../models/_queries.map.js'), 70 | // import('../models/users/users.map.js'), 71 | // import('../models/posts/posts.map.js'), 72 | // ] 73 | // // const modulePromises = imports.map(importPath => import('../models/' + importPath)) 74 | // // return merge(...loadedModules.default) 75 | // const loadedModules = await Promise.all(modulePromises) 76 | 77 | return merge(...loadedModules) 78 | } catch (error) { 79 | throw new Error(error) 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/_server/graphql-api/utils/mappers.js: -------------------------------------------------------------------------------- 1 | export default { 2 | 3 | // TODO: right now this only works on table-like data 4 | rawCleanup(row) { 5 | delete row._key 6 | delete row._id 7 | delete row._rev 8 | return row 9 | }, 10 | 11 | keyToId(obj) { 12 | obj.id = obj._key 13 | return obj 14 | }, 15 | 16 | user(user) { 17 | user.username = user._key 18 | return user 19 | }, 20 | 21 | } 22 | -------------------------------------------------------------------------------- /src/_server/serverless/img-upload.js: -------------------------------------------------------------------------------- 1 | // placeholder... 2 | // SEE: https://github.com/expressjs/multer#readme 3 | 4 | import multer from 'multer' 5 | const upload = multer({ dest: 'uploads/' }) 6 | 7 | export async function dataConverterRoute(app) { 8 | 9 | app.post('/profile', upload.single('avatar'), function(req, res, next) { 10 | // req.file is the `avatar` file 11 | // req.body will hold the text fields, if there were any 12 | }) 13 | 14 | app.post('/photos/upload', upload.array('photos', 12), function(req, res, next) { 15 | // req.files is array of `photos` files 16 | // req.body will contain the text fields, if there were any 17 | }) 18 | 19 | var cpUpload = upload.fields([{ 20 | name: 'avatar', 21 | maxCount: 1, 22 | }, { 23 | name: 'gallery', 24 | maxCount: 8, 25 | }]) 26 | app.post('/cool-profile', cpUpload, function(req, res, next) { 27 | // req.files is an object (String -> Array) where fieldname is the key, and the value is array of files 28 | // 29 | // e.g. 30 | // req.files['avatar'][0] -> File 31 | // req.files['gallery'] -> Array 32 | // 33 | // req.body will contain the text fields, if there were any 34 | }) 35 | 36 | } 37 | -------------------------------------------------------------------------------- /src/_server/services/app-setup.js: -------------------------------------------------------------------------------- 1 | import { log } from '../utils/db-tools.js' 2 | import { driver } from '../db/arangodb-driver.js' 3 | const db = driver.connect() 4 | 5 | export async function appSetup(app) { 6 | 7 | // FIXME: restart app after running the following: ?????????? 8 | 9 | const Users = db.collection('users') 10 | let exists = await Users.exists() 11 | if (!exists) { 12 | log(await Users.create(), 'Creating `Users` collection') 13 | } 14 | 15 | const Contents = db.collection('contents') 16 | exists = await Contents.exists() 17 | if (!exists) { 18 | log(await Contents.create(), 'Creating `Contents` collection') 19 | } 20 | const News = db.collection('stallion-news') 21 | exists = await News.exists() 22 | if (!exists) { 23 | log(await News.create(), 'Creating `News` collection') 24 | log(await Contents.save({ name: 'news', description: 'Add news or posts to your site.' })) 25 | } 26 | const Tags = db.collection('stallion-tags') 27 | exists = await Tags.exists() 28 | if (!exists) { 29 | log(await Tags.create(), 'Creating `Tags` collection') 30 | log(await Contents.save({ name: 'tags', description: 'Tag items for your site.' })) 31 | } 32 | 33 | // check if app needs to be initialized / setup 34 | const cursor = await Users.all() 35 | if (cursor.count === 0) { 36 | app.get('*', (req, res, next) => { 37 | if ( 38 | req.originalUrl.indexOf('/client/') === 0 || 39 | req.originalUrl.indexOf('/api/') === 0 || 40 | req.originalUrl === '/setup' 41 | ) { 42 | return next() 43 | } 44 | // server = res.connection.server 45 | res.redirect('/setup') 46 | }) 47 | } else { 48 | app.get('/setup', (req, res) => { 49 | res.redirect('/') 50 | }) 51 | } 52 | 53 | } 54 | -------------------------------------------------------------------------------- /src/_server/services/auth-setup.js: -------------------------------------------------------------------------------- 1 | import config from '../build/config' 2 | import passport from 'passport' 3 | import jwt from 'jsonwebtoken' 4 | import bcrypt from 'bcryptjs' 5 | import { Strategy as LocalStrategy } from 'passport-local' 6 | // import { Strategy as GitHubStrategy } from 'passport-github' 7 | import { driver } from '../db/arangodb-driver' 8 | const db = driver.connect() 9 | 10 | const Users = db.collection('users') 11 | const prod = process.env.NODE_ENV === 'production' 12 | 13 | export async function authSetup(app) { 14 | 15 | // FIXME: move this into routes!!! 16 | // FIXME: move this into routes!!! 17 | // FIXME: move this into AND SERVER!!! 18 | // FIXME: move this into routes!!! 19 | // FIXME: move this into routes!!! 20 | 21 | // SEE: https://medium.com/front-end-hacking/learn-using-jwt-with-passport-authentication-9761539c4314 22 | // AND: https://scotch.io/@devGson/api-authentication-with-json-web-tokensjwt-and-passport 23 | // AND: https://blog.usejournal.com/sessionless-authentication-withe-jwts-with-node-express-passport-js-69b059e4b22c 24 | 25 | passport.use(new LocalStrategy(async(username, password, done) => { 26 | // console.log(' --->>> LocalStrategy'.green) 27 | // console.log('username:', username) 28 | // console.log('password:', password) 29 | try { 30 | const cursor = await Users.byExample({ _key: username }) 31 | if (cursor.count === 1) { 32 | const user = await cursor.next() 33 | 34 | const match = await bcrypt.compare(password, user.hash) 35 | if (!match) { 36 | return done(null, false, { message: 'Incorrect password.', status: 1 }) 37 | } 38 | 39 | user.username = user._key 40 | delete user._id 41 | delete user._key 42 | delete user._rev 43 | delete user.hash 44 | // delete user.created 45 | delete user.modified 46 | done(null, user) 47 | } else { 48 | return done(null, false, { message: `${username} does not exist. Make it so?`, status: 2 }) 49 | } 50 | } catch (error) { 51 | done(error) 52 | } 53 | })) 54 | 55 | app.use(passport.initialize()) 56 | 57 | app.post('/auth/local/login', (req, res) => { 58 | passport.authenticate('local', { session: false, successRedirect: '/', failureRedirect: '/login' }, (err, user, results) => { 59 | if (err || !user) { 60 | console.log('----------------DEBUGGING >>> passport.authenticate') 61 | console.log('user', user, results) 62 | console.log('err', err) 63 | console.log('err JSON', JSON.stringify(err)) 64 | console.log('----------------DEBUGGING >>> passport.authenticate END END END') 65 | return res.status(400).json(Object.assign({ user: user ? user : false }, results)) 66 | } 67 | req.login(user, { session: false }, error => { 68 | if (error) { 69 | res.send(error) 70 | } 71 | // generate a signed son web token with the contents of user object and return it in the response 72 | const month = 60 * 60 * 24 * 30 73 | const token = jwt.sign(user, config.STALLION_JWT_SECRET, { expiresIn: month }) 74 | return res.cookie('stallion', token, { 75 | httpOnly: prod, 76 | secure: prod, 77 | maxAge: 1000 * month, 78 | }).json(user) 79 | }) 80 | })(req, res) 81 | }) 82 | 83 | app.post('/auth/logout', (req, res) => { 84 | res.clearCookie('stallion') 85 | req.logout() 86 | res.end('ok') 87 | }) 88 | 89 | } 90 | -------------------------------------------------------------------------------- /src/_server/services/graphql-setup.js: -------------------------------------------------------------------------------- 1 | import { getSchema } from '../graphql-api/schema.js' 2 | import graphqlHTTP from 'express-graphql' 3 | 4 | const env = process.env.NODE_ENV 5 | const development = env === 'development' 6 | 7 | export async function graphqlSetup(app) { 8 | 9 | // TODO: add try / catch block? 10 | let schema = await getSchema() 11 | 12 | // TODO: use `graphql-crunch` https://github.com/banterfm/graphql-crunch 13 | // https://github.com/graphql/express-graphql/issues/71 14 | app.use('/api/pure-graphql', graphqlHTTP({ 15 | schema, 16 | graphiql: process.env.STALLION_USE_GRAPHIQL === 'true', 17 | pretty: development, 18 | // rootValue: {}, // ??? 19 | // context: { req, res }, // ??? 20 | // context: ???, 21 | // credentials: 'same-origin', 22 | })) 23 | 24 | } 25 | -------------------------------------------------------------------------------- /src/_server/utils/clip-utils.js: -------------------------------------------------------------------------------- 1 | // TODO: rename 2 | 3 | // https://developer.mozilla.org/en-US/docs/Web/CSS/length 4 | export const units = [/* 'auto', */'%', 'px', 'em', 'rem', 'vh', 'vw', 'vmin', 'vmax'] 5 | export const transformUnits = [...units, 'deg', 'turn'] 6 | 7 | export const dimensions = ['width', 'height'] 8 | export const position = ['top', 'right', 'bottom', 'left'] 9 | export const properties = [...dimensions, ...position] 10 | 11 | // TODO: parse out unit??? 12 | export const parseDeclaration = function(property, value) { 13 | 14 | const decl = { 15 | property, 16 | // default is one segment, but note there could be more than one, as is the case w/ transform 17 | segments: [], 18 | } 19 | 20 | let parsedValue = parseFloat(value) 21 | const isNumber = !Number.isNaN(parsedValue) 22 | 23 | if (property === 'transform') { 24 | const segments = value.split(' ').map(segment => { 25 | const parts = segment.split(/[()]/g) 26 | const value = parseFloat(parts[1]) 27 | const unit = parts[1].split(value)[1] 28 | return { 29 | prefix: parts[0] + '(', 30 | value, 31 | unit, 32 | suffix: ')', 33 | } 34 | }) 35 | decl.segments = segments 36 | } else if (isNumber && typeof value === 'string') { 37 | const parts = value.split(parsedValue) 38 | decl.segments.push({ 39 | prefix: parts[0], 40 | value: parsedValue, 41 | unit: parts[1], 42 | suffix: '', 43 | }) 44 | } else { 45 | decl.segments.push({ prefix: '', value, unit: '', suffix: '' }) 46 | } 47 | return decl 48 | } 49 | 50 | export const getDeclarationParts = parsedDecl => { 51 | return { 52 | property: parsedDecl.property, 53 | // TODO: some of these will be joined by commas, not just spaces 54 | value: parsedDecl.segments.map(segment => `${segment.prefix}${segment.value}${segment.unit}${segment.suffix}`).join(' '), 55 | } 56 | } 57 | 58 | export const getDeclaration = (property, value/* , decl */) => { 59 | // decl = decl || parseDeclaration(property, value) 60 | const parsedDecl = parseDeclaration(property, value) 61 | const declParts = getDeclarationParts(parsedDecl) 62 | return `${declParts.property}:${declParts.value};` 63 | } 64 | 65 | export const getIntervalKeyframe = (property, interval, locus) => { 66 | const { start, stop, keyframes } = interval 67 | const duration = stop - start 68 | const startValue = keyframes[0][property] 69 | const stopValue = keyframes[1][property] 70 | const startDecl = parseDeclaration(property, startValue) 71 | const stopDecl = parseDeclaration(property, stopValue) 72 | 73 | // NOTE: mutating startDecl and passing it BACK as the interpolated declaration 74 | startDecl.segments = startDecl.segments.map((segment, index) => { 75 | // (locus / duration * (stopValue - startValue)) + startValue 76 | segment.value = (locus / duration * (stopDecl.segments[index].value - segment.value)) + segment.value 77 | return segment 78 | }) 79 | const decl = {} 80 | const declParts = getDeclarationParts(startDecl) 81 | return decl[declParts.property] = declParts.value 82 | } 83 | -------------------------------------------------------------------------------- /src/_server/utils/db-tools.js: -------------------------------------------------------------------------------- 1 | import { yellow, magenta } from 'ansi-colors' 2 | 3 | const start = yellow('----- >>>') 4 | const stop = yellow('----- ^^^') 5 | 6 | export const log = (logs, grouping) => { 7 | logs = Array.isArray(logs) ? logs : [logs] 8 | grouping = grouping || 'migration log' 9 | console.log() 10 | console.log(`${start} ${magenta(grouping)}`) 11 | logs.forEach(log => console.dir(log, { depth: null, colors: true })) 12 | console.log(stop) 13 | console.log() 14 | } 15 | 16 | // const Papa = require('papaparse') 17 | // module.exports.papaParseAsync = function(file, options) { 18 | // options = options || {} 19 | // return new Promise(function(complete, error) { 20 | // options.complete = complete 21 | // options.error = error 22 | // Papa.parse(file, options) 23 | // }) 24 | // } 25 | -------------------------------------------------------------------------------- /src/_server/utils/loaders.js: -------------------------------------------------------------------------------- 1 | import fetch from 'cross-fetch' 2 | 3 | export const graphQuery = async function(query, convertRawKeys) { 4 | convertRawKeys = convertRawKeys ? convertRawKeys : [] 5 | convertRawKeys = Array.isArray(convertRawKeys) ? convertRawKeys : [convertRawKeys] 6 | const res = await fetch(`${process.env.STALLION_HOST}/api/pure-graphql`, { 7 | method: 'POST', 8 | headers: { 'Content-Type': 'application/json' }, 9 | credentials: 'same-origin', 10 | body: JSON.stringify({ query }), 11 | }) 12 | const json = await res.json() 13 | convertRawKeys.forEach(key => { 14 | json.data[key] = JSON.parse(json.data[key]) 15 | }) 16 | return json.data 17 | } 18 | 19 | export const graphqlFormat = data => { 20 | let props = [] 21 | for (let key in data) { 22 | props.push(`${key}:${JSON.stringify(data[key])}`) 23 | } 24 | return `{ ${props.join(',')} }` 25 | } 26 | 27 | export const GET = async function(url) { 28 | const res = await fetch(process.env.STALLION_HOST + url, { 29 | method: 'GET', 30 | headers: { 'Content-Type': 'application/json' }, 31 | credentials: 'same-origin', 32 | }) 33 | try { return await res.json() } catch (error) { return undefined } 34 | } 35 | 36 | export const POST = async function(url, body) { 37 | body = body || {} 38 | const res = await fetch(process.env.STALLION_HOST + url, { 39 | method: 'POST', 40 | headers: { 'Content-Type': 'application/json' }, 41 | credentials: 'same-origin', 42 | body: JSON.stringify(body), 43 | }) 44 | try { return await res.json() } catch (error) { return undefined } 45 | } 46 | -------------------------------------------------------------------------------- /src/_server/utils/migration-shell.js: -------------------------------------------------------------------------------- 1 | // excellent article on all this: https://medium.freecodecamp.org/node-js-child-processes-everything-you-need-to-know-e69498fe970a 2 | 3 | import fs from 'fs' 4 | import globby from 'globby' 5 | import config from '../build/config' 6 | 7 | // console.log(config) 8 | const env = config.NODE_ENV || 'development' 9 | const script = config.npm_lifecycle_event 10 | 11 | const { spawn } = require('child_process') 12 | 13 | let command 14 | const compiler = '--compiler="js:./src/_server/build/babel-register-compiler.js"' 15 | const dirs = '--migrations-dir=src/_server/data/migrations' 16 | const filename = `src/_server/data/.migrate-${env}` 17 | const shared = `${dirs} -f ${filename} ${compiler}` 18 | 19 | if (script === 'm:up') { 20 | command = `migrate up ${shared} ${getMigration('up')}` 21 | } else if (script === 'm:down') { 22 | command = `migrate down ${shared} ${getMigration('down')}` 23 | } else if (script === 'm:up:all') { 24 | command = `migrate up ${shared}` 25 | } else if (script === 'm:down:all') { 26 | command = `migrate down ${shared}` 27 | } else if (script === 'm:create') { 28 | const args = JSON.parse(config.npm_config_argv).original 29 | command = `migrate create ${args[1]} ${shared} --template-file src/_server/utils/migration-template.js` 30 | } else if (script === 'm:list') { 31 | command = `migrate list ${shared}` 32 | } 33 | 34 | // RUN IT! 35 | if (script.match(/^m:test:/)) { 36 | const command = script.split('m:test:')[1] 37 | console.log(`--- TESTING '${command}' MIGRATION ONLY --->`) 38 | require(`../data/migrations/${getMigration('up')}`)[command]() 39 | // spawn('node -r esm src/_server/data/migrations/1527054780080-add-repo-metadata.js', { stdio: 'inherit', shell: true }) 40 | } else { 41 | console.log(command) 42 | spawn(command, { stdio: 'inherit', shell: true }) 43 | } 44 | 45 | // migration helper ----- >>>>> 46 | 47 | function getMigration(direction) { 48 | let migrateFile = {} 49 | if (fs.existsSync(filename)) { 50 | migrateFile = JSON.parse(fs.readFileSync(filename, 'utf8')) 51 | } 52 | const migrations = globby.sync('./src/_server/data/migrations/**/*.js').map(path => path.split('/migrations/')[1]) 53 | const lastRun = migrateFile.lastRun 54 | 55 | // console.log(migrations) 56 | // console.log(lastRun) 57 | 58 | if (direction === 'up') { 59 | const index = migrations.indexOf(lastRun) 60 | let next = migrations[index + 1] 61 | return next ? next : migrations[index] 62 | } else if (direction === 'down') { 63 | return lastRun || migrations[0] 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/_server/utils/migration-template.js: -------------------------------------------------------------------------------- 1 | import { log } from '../../utils/db-tools.js' 2 | // import { getApi } from '../../db/arangodb-api.js' 3 | // const api = getApi() 4 | import { driver } from '../../db/arangodb-driver' 5 | const db = driver.connect() 6 | 7 | // const Graph = db.graph('graph') 8 | // const Collection = db.collection('collection') 9 | 10 | export const up = async function() { 11 | 12 | try { 13 | 14 | // ------------------------------ >>> 15 | // data loading 16 | // ------------------------------ >>> 17 | 18 | // const migrationData = require('../migrations-data/___') 19 | 20 | // ------------------------------ >>> 21 | // main_action 22 | // ------------------------------ >>> 23 | 24 | // ----- >>> sub_actions 25 | 26 | // log() 27 | 28 | } catch (error) { 29 | console.log('ERROR:', error) 30 | } 31 | 32 | } 33 | 34 | export const down = async function() { 35 | } 36 | -------------------------------------------------------------------------------- /src/_server/utils/stallion-utils.js: -------------------------------------------------------------------------------- 1 | // TODO: throw this in an npm repository ??? 2 | 3 | export const camelToKebab = str => str.replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase() 4 | export const kebabToCamel = str => str.replace(/-([a-z])/g, $1 => $1[1].toUpperCase()) 5 | export const fastClone = obj => JSON.parse(JSON.stringify(obj)) // https://stackoverflow.com/questions/122102/what-is-the-most-efficient-way-to-deep-clone-an-object-in-javascript/5344074#5344074 6 | 7 | export const concatStyles = styles => { 8 | let concatenatedStyles = '' 9 | for (let index in styles) { 10 | // console.log(`${index}:${styles[index]};`) 11 | concatenatedStyles += `${index}:${styles[index]};` 12 | } 13 | return concatenatedStyles 14 | } 15 | 16 | export const wrapTag = (tag, content) => `<${tag}>${content}${tag}>` 17 | 18 | 19 | 20 | // NOTE: might write my own helpers? 21 | // https://medium.com/@abustamam/for-loops-vs-foreach-in-javascript-7a977278a39e 22 | -------------------------------------------------------------------------------- /src/_server/utils/thumbnail-generator.js: -------------------------------------------------------------------------------- 1 | const Jimp = require(`jimp`) 2 | const imagemin = require('imagemin') 3 | const imageminPngquant = require('imagemin-pngquant') 4 | const { readdirSync } = require(`fs`) 5 | const { join } = require(`path`) 6 | 7 | const relativeToThisFile = relativePath => join(__dirname, relativePath) 8 | 9 | const inputPath = relativeToThisFile(`../../book-cover-image`) 10 | const outputPath = relativeToThisFile(`../../Markdown/Web/book-image/thumbnail`) 11 | 12 | const files = readdirSync(inputPath) 13 | 14 | const toTransform = files.filter(file => /\.png$/.test(file)) 15 | 16 | toTransform.forEach(filename => { 17 | const path = join(inputPath, filename) 18 | 19 | Jimp.read(path).then(image => { 20 | console.log(`Writing`, filename) 21 | return new Promise((resolve, reject) => { 22 | const filePath = join(outputPath, filename) 23 | image 24 | .autocrop() 25 | .resize(400, 600) 26 | .crop(0, 0, 400, 400) 27 | // .quality(80) 28 | .write(filePath, err => err ? reject(err) : resolve(filePath)) 29 | }) 30 | }).then(filePath => { 31 | imagemin([filePath], outputPath, { 32 | plugins: [ 33 | imageminPngquant({ quality: '70-80' }) 34 | ] 35 | }) 36 | }) 37 | }) -------------------------------------------------------------------------------- /src/_service-worker.js: -------------------------------------------------------------------------------- 1 | import { timestamp, files, shell, routes } from '@sapper/service-worker'; 2 | 3 | const ASSETS = `cache${timestamp}`; 4 | 5 | // `shell` is an array of all the files generated by the bundler, 6 | // `files` is an array of everything in the `static` directory 7 | const to_cache = shell.concat(files); 8 | const cached = new Set(to_cache); 9 | 10 | self.addEventListener('install', event => { 11 | event.waitUntil( 12 | caches 13 | .open(ASSETS) 14 | .then(cache => cache.addAll(to_cache)) 15 | .then(() => { 16 | self.skipWaiting(); 17 | }) 18 | ); 19 | }); 20 | 21 | self.addEventListener('activate', event => { 22 | event.waitUntil( 23 | caches.keys().then(async keys => { 24 | // delete old caches 25 | for (const key of keys) { 26 | if (key !== ASSETS) await caches.delete(key); 27 | } 28 | 29 | self.clients.claim(); 30 | }) 31 | ); 32 | }); 33 | 34 | self.addEventListener('fetch', event => { 35 | if (event.request.method !== 'GET' || event.request.headers.has('range')) return; 36 | 37 | const url = new URL(event.request.url); 38 | 39 | // don't try to handle e.g. data: URIs 40 | if (!url.protocol.startsWith('http')) return; 41 | 42 | // ignore dev server requests 43 | if (url.hostname === self.location.hostname && url.port !== self.location.port) return; 44 | 45 | // always serve static files and bundler-generated assets from cache 46 | if (url.host === self.location.host && cached.has(url.pathname)) { 47 | event.respondWith(caches.match(event.request)); 48 | return; 49 | } 50 | 51 | // for pages, you might want to serve a shell `service-worker-index.html` file, 52 | // which Sapper has generated for you. It's not right for every 53 | // app, but if it's right for yours then uncomment this section 54 | /* 55 | if (url.origin === self.origin && routes.find(route => route.pattern.test(url.pathname))) { 56 | event.respondWith(caches.match('/service-worker-index.html')); 57 | return; 58 | } 59 | */ 60 | 61 | if (event.request.cache === 'only-if-cached') return; 62 | 63 | // for everything else, try the network first, falling back to 64 | // cache if the user is offline. (If the pages never change, you 65 | // might prefer a cache-first approach to a network-first one.) 66 | event.respondWith( 67 | caches 68 | .open(`offline${timestamp}`) 69 | .then(async cache => { 70 | try { 71 | const response = await fetch(event.request); 72 | cache.put(event.request, response.clone()); 73 | return response; 74 | } catch(err) { 75 | const response = await cache.match(event.request); 76 | if (response) return response; 77 | 78 | throw err; 79 | } 80 | }) 81 | ); 82 | }); 83 | -------------------------------------------------------------------------------- /src/client.js: -------------------------------------------------------------------------------- 1 | import * as sapper from '@sapper/app' 2 | sapper.start({ target: document.querySelector('#sapper') }) 3 | -------------------------------------------------------------------------------- /src/components/List.svelte: -------------------------------------------------------------------------------- 1 |
{item.description}
8 | {/if} 9 |{subtitle}
5 | {/if} 6 |{item.code}
12 | {/if}
13 | {#if fields.length}
14 | {key} | 18 |19 | |
This action cannot be undone!
32 | 36 |{error.message}
8 | 9 | {#if dev && error.stack} 10 |{error.stack}11 | {/if} 12 | 13 | 18 | 19 | 34 | -------------------------------------------------------------------------------- /src/routes/_home-private.svelte: -------------------------------------------------------------------------------- 1 |
{$page.params.content}
7 | 8 | 11 | 12 | 24 | -------------------------------------------------------------------------------- /src/routes/contents/create.svelte: -------------------------------------------------------------------------------- 1 |{content.description}
12 | {/if} 13 |Add your first field to content type "{content.name}".
25 | {/if} 26 | {#if !adding} 27 | 30 | {/if} 31 |{user.bio}
20 | {/if} 21 |{JSON.stringify(set, undefined, 2)}10 | {/each} 11 | {:else} 12 |