├── .nvmrc
├── src
├── _client
│ ├── js
│ │ └── .gitkeep
│ ├── postcss
│ │ ├── routes-includes.postcss
│ │ ├── global.postcss
│ │ ├── modules
│ │ │ ├── _structure.postcss
│ │ │ └── _buttons.postcss
│ │ ├── main
│ │ │ └── _layout.postcss
│ │ └── shared
│ │ │ ├── _helpers.postcss
│ │ │ └── _variables.postcss
│ ├── svg
│ │ ├── compiled
│ │ │ ├── arrow-q.svg
│ │ │ ├── arrow-down.svg
│ │ │ ├── select-handle-on.svg
│ │ │ ├── select-handle.svg
│ │ │ ├── select-handle-off.svg
│ │ │ ├── valid-good.svg
│ │ │ ├── social-gitlab.svg
│ │ │ ├── social-email.svg
│ │ │ ├── social-google.svg
│ │ │ ├── social-facebook.svg
│ │ │ ├── social-youtube.svg
│ │ │ ├── valid-bad.svg
│ │ │ ├── social-linkedin.svg
│ │ │ ├── social-twitter.svg
│ │ │ ├── social-github.svg
│ │ │ ├── logo-public.svg
│ │ │ └── social-instagram.svg
│ │ └── originals
│ │ │ ├── social-gitlab.svg
│ │ │ ├── social-google.svg
│ │ │ ├── social-facebook.svg
│ │ │ ├── social-youtube.svg
│ │ │ ├── arrow-q.svg
│ │ │ ├── social-linkedin.svg
│ │ │ ├── arrow-down.svg
│ │ │ ├── social-twitter.svg
│ │ │ ├── social-github.svg
│ │ │ ├── select-handle-off.svg
│ │ │ ├── select-handle-on.svg
│ │ │ ├── select-handle.svg
│ │ │ ├── social-email.svg
│ │ │ ├── valid-good.svg
│ │ │ ├── valid-bad.svg
│ │ │ ├── social-instagram.svg
│ │ │ └── logo-public.svg
│ └── .eslintrc.json
├── routes
│ ├── settings
│ │ ├── account
│ │ │ ├── profile.svelte
│ │ │ └── notifications.svelte
│ │ ├── admin
│ │ │ ├── _actions
│ │ │ │ ├── helpers.js
│ │ │ │ └── collections.js
│ │ │ ├── collections
│ │ │ │ ├── [collection].svelte
│ │ │ │ └── index.svelte
│ │ │ ├── actions.json.js
│ │ │ └── users.svelte
│ │ └── _layout.svelte
│ ├── _home-private.svelte
│ ├── contents
│ │ ├── edit
│ │ │ ├── _fields
│ │ │ │ ├── Date.svelte
│ │ │ │ ├── Email.svelte
│ │ │ │ ├── Media.svelte
│ │ │ │ ├── Boolean.svelte
│ │ │ │ ├── String.svelte
│ │ │ │ ├── Text.svelte
│ │ │ │ ├── Number.svelte
│ │ │ │ └── Relation.svelte
│ │ │ ├── _options.js
│ │ │ ├── [name].svelte
│ │ │ └── _AddFieldForm.svelte
│ │ ├── [content].svelte
│ │ ├── index.svelte
│ │ └── create.svelte
│ ├── _services
│ │ ├── auth-check.js
│ │ └── redirect-handler.js
│ ├── api
│ │ ├── paywall.json.js
│ │ ├── contents
│ │ │ ├── list.json.js
│ │ │ ├── [name].json.js
│ │ │ ├── delete.json.js
│ │ │ └── create.json.js
│ │ ├── restart.json.js
│ │ ├── signup.json.js
│ │ ├── signup-validate.json.js
│ │ └── [profile]
│ │ │ └── [dataset].js
│ ├── index.svelte
│ ├── _error.svelte
│ ├── _home-public.svelte
│ ├── profiles.svelte
│ ├── login.svelte
│ ├── cm
│ │ └── [profile]
│ │ │ └── index.svelte
│ ├── _layout.svelte
│ └── setup
│ │ └── index.svelte
├── _server
│ ├── graphql-api
│ │ ├── models
│ │ │ ├── _queries.map.js
│ │ │ ├── _mutations.map.js
│ │ │ ├── _queries.gql
│ │ │ ├── _mutations.gql
│ │ │ ├── posts
│ │ │ │ ├── posts.gql
│ │ │ │ └── posts.map.js
│ │ │ └── users
│ │ │ │ ├── users.gql
│ │ │ │ └── users.map.js
│ │ ├── utils
│ │ │ ├── mappers.js
│ │ │ └── graphql-utils.js
│ │ └── schema.js
│ ├── build
│ │ ├── babel-register-compiler.js
│ │ ├── postcss.config.js
│ │ ├── rollup.vars.js
│ │ ├── config.js
│ │ ├── rollup.preprocess.js
│ │ ├── postcss.config.vars.js
│ │ ├── svgo-config-inline.yml
│ │ └── svgo-config-src.yml
│ ├── db
│ │ ├── validators
│ │ │ ├── graphql.js
│ │ │ └── generic.js
│ │ ├── arangodb-driver.js
│ │ └── arangodb-api.js
│ ├── services
│ │ ├── graphql-setup.js
│ │ ├── app-setup.js
│ │ └── auth-setup.js
│ ├── utils
│ │ ├── db-tools.js
│ │ ├── migration-template.js
│ │ ├── stallion-utils.js
│ │ ├── thumbnail-generator.js
│ │ ├── loaders.js
│ │ ├── migration-shell.js
│ │ └── clip-utils.js
│ └── serverless
│ │ └── img-upload.js
├── client.js
├── stores
│ ├── app-store.js
│ └── local-store.js
├── components
│ ├── svg
│ │ ├── Close.svelte
│ │ └── Loader.svelte
│ ├── shared
│ │ ├── ReloadBlock.svelte
│ │ └── FlexInput.svelte
│ ├── forms
│ │ ├── Form.svelte
│ │ ├── LabelCheckbox.svelte
│ │ ├── LabelRadio.svelte
│ │ ├── MiniForm.svelte
│ │ ├── Popup.svelte
│ │ ├── LabelSelect.svelte
│ │ ├── LabelInput.svelte
│ │ └── LabelTextarea.svelte
│ ├── List.svelte
│ ├── layout
│ │ ├── Header.svelte
│ │ ├── Sidebar.svelte
│ │ ├── Nav.svelte
│ │ └── NavMenu.svelte
│ └── settings
│ │ └── ListItem.svelte
├── template.html
├── _service-worker.js
└── server.js
├── static
├── favicon.ico
├── favicon-16x16.png
├── favicon-32x32.png
├── mstile-150x150.png
├── apple-touch-icon.png
├── android-chrome-192x192.png
├── android-chrome-256x256.png
├── svg
│ ├── nav-next.svg
│ ├── nav-prev.svg
│ ├── arrow-q.svg
│ ├── arrow-down.svg
│ ├── select-handle-on.svg
│ ├── select-handle.svg
│ ├── select-handle-off.svg
│ ├── social-gitlab.svg
│ ├── valid-good.svg
│ ├── social-email.svg
│ ├── social-google.svg
│ ├── social-facebook.svg
│ ├── social-youtube.svg
│ ├── valid-bad.svg
│ ├── social-linkedin.svg
│ ├── social-twitter.svg
│ ├── social-github.svg
│ ├── logo-public.svg
│ └── social-instagram.svg
├── browserconfig.xml
└── manifest.json
├── .gitignore
├── .env.defaults
├── README.md
├── LICENSE
├── rollup.config.js
├── .eslintrc.yml
└── package.json
/.nvmrc:
--------------------------------------------------------------------------------
1 | 11.12.0
2 |
--------------------------------------------------------------------------------
/src/_client/js/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/routes/settings/account/profile.svelte:
--------------------------------------------------------------------------------
1 |
Profile
--------------------------------------------------------------------------------
/src/routes/_home-private.svelte:
--------------------------------------------------------------------------------
1 | Welcome to Stallion
--------------------------------------------------------------------------------
/src/routes/contents/edit/_fields/Date.svelte:
--------------------------------------------------------------------------------
1 | Date
2 |
--------------------------------------------------------------------------------
/src/routes/contents/edit/_fields/Email.svelte:
--------------------------------------------------------------------------------
1 | Email
2 |
--------------------------------------------------------------------------------
/src/routes/contents/edit/_fields/Media.svelte:
--------------------------------------------------------------------------------
1 | Media
2 |
--------------------------------------------------------------------------------
/src/routes/settings/account/notifications.svelte:
--------------------------------------------------------------------------------
1 | Notifications
--------------------------------------------------------------------------------
/src/_client/postcss/routes-includes.postcss:
--------------------------------------------------------------------------------
1 | @import 'shared/variables';
2 |
--------------------------------------------------------------------------------
/static/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/arxpoetica/stallion/HEAD/static/favicon.ico
--------------------------------------------------------------------------------
/src/_server/graphql-api/models/_queries.map.js:
--------------------------------------------------------------------------------
1 | export default {
2 | Query: {
3 | },
4 | }
5 |
--------------------------------------------------------------------------------
/src/_server/graphql-api/models/_mutations.map.js:
--------------------------------------------------------------------------------
1 | export default {
2 | Mutation: {
3 | },
4 | }
5 |
--------------------------------------------------------------------------------
/static/favicon-16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/arxpoetica/stallion/HEAD/static/favicon-16x16.png
--------------------------------------------------------------------------------
/static/favicon-32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/arxpoetica/stallion/HEAD/static/favicon-32x32.png
--------------------------------------------------------------------------------
/static/mstile-150x150.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/arxpoetica/stallion/HEAD/static/mstile-150x150.png
--------------------------------------------------------------------------------
/static/apple-touch-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/arxpoetica/stallion/HEAD/static/apple-touch-icon.png
--------------------------------------------------------------------------------
/src/client.js:
--------------------------------------------------------------------------------
1 | import * as sapper from '@sapper/app'
2 | sapper.start({ target: document.querySelector('#sapper') })
3 |
--------------------------------------------------------------------------------
/static/android-chrome-192x192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/arxpoetica/stallion/HEAD/static/android-chrome-192x192.png
--------------------------------------------------------------------------------
/static/android-chrome-256x256.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/arxpoetica/stallion/HEAD/static/android-chrome-256x256.png
--------------------------------------------------------------------------------
/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/_mutations.gql:
--------------------------------------------------------------------------------
1 | type Mutation {
2 | addUser(first: String, last: String, email: String, picture: String, friends: [String]): User
3 | }
4 |
--------------------------------------------------------------------------------
/src/_client/svg/compiled/arrow-q.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/src/_client/svg/compiled/arrow-down.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/static/svg/nav-next.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/static/svg/nav-prev.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/static/svg/arrow-q.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/static/svg/arrow-down.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/src/_client/svg/compiled/select-handle-on.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/src/_client/svg/compiled/select-handle.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/src/_client/svg/compiled/select-handle-off.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/static/svg/select-handle-on.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/static/svg/select-handle.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/static/svg/select-handle-off.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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/_client/svg/compiled/valid-good.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/src/_client/svg/compiled/social-gitlab.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/static/browserconfig.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | #ffffff
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/src/routes/_services/auth-check.js:
--------------------------------------------------------------------------------
1 | import jwt from 'jsonwebtoken'
2 |
3 | export const validate = function(req) {
4 | try {
5 | return jwt.verify(req.cookies.stallion, process.env.STALLION_JWT_SECRET)
6 | } catch (error) {
7 | return {
8 | unauthorized: true,
9 | message: 'Unauthorized',
10 | }
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/src/stores/app-store.js:
--------------------------------------------------------------------------------
1 | import { writable/* , readable, derive */ } from 'svelte/store'
2 |
3 | export const target = writable(null)
4 | export const contents = writable([])
5 |
6 | // if (process.env.NODE_ENV === 'development') {
7 | // store.set({ dump: json => JSON.stringify(json) })
8 | // window.store = store
9 | // }
10 |
--------------------------------------------------------------------------------
/src/_client/svg/compiled/social-email.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/static/svg/social-gitlab.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/static/svg/valid-good.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/src/routes/api/paywall.json.js:
--------------------------------------------------------------------------------
1 | import { validate } from '../_services/auth-check'
2 |
3 | export async function get(req, res) {
4 | res.writeHead(200, { 'Content-Type': 'application/json' })
5 | const test = validate(req)
6 | if (test.unauthorized) {
7 | res.end(JSON.stringify(test))
8 | } else {
9 | res.end(JSON.stringify({ authentic: true }))
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/src/routes/settings/admin/_actions/helpers.js:
--------------------------------------------------------------------------------
1 | export const post = async function(fetch, body) {
2 | body = body || {}
3 | const res = await fetch('/settings/admin/actions.json', {
4 | method: 'POST',
5 | headers: { 'Content-Type': 'application/json' },
6 | credentials: 'same-origin',
7 | body: JSON.stringify(body),
8 | })
9 | return await res.json()
10 | }
11 |
--------------------------------------------------------------------------------
/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/routes/index.svelte:
--------------------------------------------------------------------------------
1 | Stallion - Home
2 |
3 | {#if $session.user}
4 |
5 | {:else}
6 |
7 | {/if}
8 |
9 |
15 |
--------------------------------------------------------------------------------
/static/svg/social-email.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/src/_client/svg/compiled/social-google.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/src/_client/svg/compiled/social-facebook.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/src/_client/svg/originals/social-gitlab.svg:
--------------------------------------------------------------------------------
1 | GitLab icon
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/static/svg/social-google.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/static/svg/social-facebook.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/src/_client/svg/originals/social-google.svg:
--------------------------------------------------------------------------------
1 | Google icon
--------------------------------------------------------------------------------
/static/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "background_color": "#ffffff",
3 | "theme_color": "#ffffff",
4 | "name": "Stallion",
5 | "short_name": "Stallion",
6 | "display": "standalone",
7 | "start_url": ".",
8 | "icons": [{
9 | "src": "android-chrome-192x192.png?v=1",
10 | "sizes": "192x192",
11 | "type": "image/png"
12 | },
13 | {
14 | "src": "android-chrome-256x256.png?v=1",
15 | "sizes": "256x256",
16 | "type": "image/png"
17 | }
18 | ]
19 | }
20 |
--------------------------------------------------------------------------------
/src/_client/svg/compiled/social-youtube.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/src/_client/svg/originals/social-facebook.svg:
--------------------------------------------------------------------------------
1 | Facebook icon
--------------------------------------------------------------------------------
/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/_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/routes/api/contents/list.json.js:
--------------------------------------------------------------------------------
1 | import { driver } from '../../../_server/db/arangodb-driver.js'
2 | const db = driver.connect()
3 |
4 | export async function get(req, res) {
5 | const Contents = db.collection('contents')
6 | const cursor = await Contents.all()
7 | const list = await cursor.all()
8 | const contents = list.map(item => {
9 | return {
10 | id: item._key,
11 | name: item.name,
12 | description: item.description,
13 | }
14 | })
15 | res.json(contents)
16 | }
17 |
--------------------------------------------------------------------------------
/src/_client/svg/compiled/valid-bad.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/static/svg/social-youtube.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/src/components/svg/Close.svelte:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
11 |
--------------------------------------------------------------------------------
/src/_client/svg/compiled/social-linkedin.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/src/routes/contents/[content].svelte:
--------------------------------------------------------------------------------
1 |
2 |
Content Type
3 |
Edit
4 |
5 |
6 | {$page.params.content}
7 |
8 |
11 |
12 |
24 |
--------------------------------------------------------------------------------
/static/svg/valid-bad.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/static/svg/social-linkedin.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/src/routes/api/contents/[name].json.js:
--------------------------------------------------------------------------------
1 | import { driver } from '../../../_server/db/arangodb-driver.js'
2 | const db = driver.connect()
3 | const Contents = db.collection('contents')
4 |
5 | export async function post(req, res) {
6 |
7 | try {
8 | let { name } = req.body
9 | const content = await Contents.firstExample({ name })
10 | if (content) {
11 | res.json(content)
12 | } else {
13 | throw new Error(`Can't find content with name: ${name}.`)
14 | }
15 | } catch (error) {
16 | res.json({ error: 1, message: error.message })
17 | }
18 |
19 | }
20 |
--------------------------------------------------------------------------------
/src/_client/svg/originals/social-youtube.svg:
--------------------------------------------------------------------------------
1 |
2 | YouTube icon
3 |
4 |
5 |
--------------------------------------------------------------------------------
/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/svg/compiled/social-twitter.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/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/components/shared/ReloadBlock.svelte:
--------------------------------------------------------------------------------
1 |
2 |
Changes have been applied.
3 | Please restart your app.
4 |
5 |
6 |
28 |
--------------------------------------------------------------------------------
/src/routes/_services/redirect-handler.js:
--------------------------------------------------------------------------------
1 | export default function(options) {
2 |
3 | options = options ? options : {}
4 | options = typeof options === 'function' ? { cb: options } : options
5 | // default is to hide 'private'
6 | options.hide = options.hide || 'private'
7 |
8 | return function(vars) {
9 | const { user } = this.store.get()
10 | if (options.hide === 'private') {
11 | if (!user) { return this.redirect(302, '') }
12 | } else {
13 | if (user) { return this.redirect(302, '') }
14 | }
15 | if (typeof options.cb === 'function') {
16 | options.cb(vars)
17 | }
18 | }
19 |
20 | }
21 |
--------------------------------------------------------------------------------
/static/svg/social-twitter.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/src/_client/svg/originals/arrow-q.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | arrow-down
5 | Created with Sketch.
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/src/routes/_error.svelte:
--------------------------------------------------------------------------------
1 |
2 | Stallion - {status}
3 |
4 |
5 | {status}
6 |
7 | {error.message}
8 |
9 | {#if dev && error.stack}
10 | {error.stack}
11 | {/if}
12 |
13 |
18 |
19 |
34 |
--------------------------------------------------------------------------------
/src/_client/svg/originals/social-linkedin.svg:
--------------------------------------------------------------------------------
1 |
2 | LinkedIn icon
3 |
4 |
--------------------------------------------------------------------------------
/src/routes/_home-public.svelte:
--------------------------------------------------------------------------------
1 |
2 |
Stallion
3 | Headless CMS built atop Svelte and Sapper.
4 |
5 |
6 |
7 |
10 |
11 |
32 |
--------------------------------------------------------------------------------
/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/_client/svg/originals/arrow-down.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | arrow-down
5 | Created with Sketch.
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/src/_client/svg/compiled/social-github.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/src/_client/svg/originals/social-twitter.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/static/svg/social-github.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/src/_client/svg/compiled/logo-public.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/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/svg/originals/social-github.svg:
--------------------------------------------------------------------------------
1 | GitHub icon
--------------------------------------------------------------------------------
/static/svg/logo-public.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/src/_client/svg/originals/select-handle-off.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Shape
5 | Created with Sketch.
6 |
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/src/_client/svg/originals/select-handle-on.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Shape
5 | Created with Sketch.
6 |
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/src/_client/svg/originals/select-handle.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Shape
5 | Created with Sketch.
6 |
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/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/components/forms/Form.svelte:
--------------------------------------------------------------------------------
1 |
14 |
15 |
25 |
26 |
40 |
--------------------------------------------------------------------------------
/src/routes/contents/edit/_fields/Boolean.svelte:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
21 |
--------------------------------------------------------------------------------
/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/routes/contents/edit/_options.js:
--------------------------------------------------------------------------------
1 | export const options = [{
2 | value: 'String',
3 | description: 'Titles, names, paragraphs, list of names',
4 | }, {
5 | value: 'Text',
6 | description: 'Descriptions, text paragraphs, articles',
7 | }, {
8 | value: 'Number',
9 | description: 'Everything that is number',
10 | }, {
11 | value: 'Boolean',
12 | description: 'Yes or no, 1 or 0, true or false',
13 | }, {
14 | value: 'Date',
15 | description: 'Event date, opening hours',
16 | // }, {
17 | // value: 'JSON',
18 | // description: 'Data in JSON format',
19 | }, {
20 | value: 'Email',
21 | description: 'User\'s email...',
22 | // }, {
23 | // value: 'Password',
24 | // description: 'User password...',
25 | }, {
26 | value: 'Media',
27 | description: 'Images, videos, PDFs and other files',
28 | }, {
29 | value: 'Relation',
30 | description: 'Refers to a Content Type',
31 | // }, {
32 | // value: 'Enumeration',
33 | // description: 'List of choices',
34 | }]
35 |
--------------------------------------------------------------------------------
/src/routes/api/contents/delete.json.js:
--------------------------------------------------------------------------------
1 | import { log } from '../../../_server/utils/db-tools.js'
2 | import { driver } from '../../../_server/db/arangodb-driver.js'
3 | const db = driver.connect()
4 |
5 | export async function post(req, res) {
6 |
7 | try {
8 | const { _key, name } = req.body
9 | const contentName = `stallion-${name}`
10 | const Content = db.collection(contentName)
11 | const exists = await Content.exists()
12 | if (exists) {
13 | // delete the content
14 | log(await Content.drop(), `Dropping "${name}" Content collection`)
15 |
16 | // delete the content information
17 | const Contents = db.collection('contents')
18 | log(await Contents.remove(_key), `Removing "${name}" doc info from Contents content`)
19 |
20 | res.json({ success: 1, message: 'All went well.' })
21 | } else {
22 | throw new Error('Content does not exist.')
23 | }
24 | } catch (error) {
25 | res.json({ error: 1, message: error.message })
26 | }
27 |
28 | }
29 |
--------------------------------------------------------------------------------
/src/routes/settings/admin/collections/[collection].svelte:
--------------------------------------------------------------------------------
1 | Collection: {$page.params.collection}
2 |
3 |
4 |
5 |
6 | {#if dataset.length}
7 | {#each dataset as set}
8 | {set._key}
9 | {JSON.stringify(set, undefined, 2)}
10 | {/each}
11 | {:else}
12 | There is no data in this collection.
13 | {/if}
14 |
15 |
22 |
26 |
27 |
37 |
--------------------------------------------------------------------------------
/src/routes/settings/admin/actions.json.js:
--------------------------------------------------------------------------------
1 | // FIXME: these mutations might be more easily
2 | // handled with GraphQL mutations????
3 |
4 | import { validate } from '../../_services/auth-check'
5 | import collections from './_actions/collections'
6 |
7 | export async function post(req, res) {
8 |
9 | const user = validate(req)
10 | if (user.role === 'admin') {
11 | const action = req.body.action
12 | const key = req.body.key
13 | let actionResponse = { errorcode: 3, message: 'Unknown error.' }
14 |
15 |
16 | if (action === 'collections.list') {
17 | actionResponse = await collections.list()
18 | } else if (action === 'collections.getData') {
19 | actionResponse = await collections.getData(key)
20 | // } else if (action === 'schema.remove') {
21 | // actionResponse = await schema.remove(key)
22 | }
23 |
24 | res.json(actionResponse)
25 |
26 | } else {
27 | // TODO: set codes. I'm assuming `2` means "not admin"
28 | res.json({ errorcode: 2, error: 'Access denied' })
29 | }
30 |
31 | }
32 |
--------------------------------------------------------------------------------
/src/_client/svg/originals/social-email.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | social-email
5 | Created with Sketch.
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/src/routes/settings/admin/_actions/collections.js:
--------------------------------------------------------------------------------
1 | // import { log } from '../../../../_server/utils/db-tools.js'
2 | import { getApi } from '../../../../_server/db/arangodb-api'
3 | const api = getApi()
4 | import { driver } from '../../../../_server/db/arangodb-driver'
5 | const db = driver.connect()
6 |
7 | export default {
8 |
9 | list: async(collection) => await db.listCollections(true),
10 |
11 | getData: async(collection) => await api.getAll(collection),
12 |
13 | // remove: async(key) => {
14 | // try {
15 | // log(await api.remove('schemas', key), `ADMIN: "${key}" schema document removed`)
16 | // const edgeExists = await api.get('repo-has-schema', key)
17 | // if (edgeExists) {
18 | // log(await api.remove('repo-has-schema', key), `ADMIN: repo has schema "${key}" edge removed`)
19 | // }
20 | // return { removed: true, id: key }
21 | // } catch (error) {
22 | // // TODO: set codes. I'm assuming `1` means "500"???
23 | // return { errorcode: 1, error: 'Schema not deleted.' }
24 | // }
25 | // },
26 |
27 | }
28 |
--------------------------------------------------------------------------------
/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/components/forms/LabelCheckbox.svelte:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | {label}
5 |
6 | {#if help}
7 | {help}
8 | {/if}
9 |
10 |
11 |
29 |
30 |
50 |
--------------------------------------------------------------------------------
/src/stores/local-store.js:
--------------------------------------------------------------------------------
1 | /* global localStorage */
2 |
3 | import { writable } from 'svelte/store'
4 | const storage = typeof localStorage !== 'undefined' ? localStorage : {
5 | removeItem: key => { if (storage[key]) { delete storage[key] } },
6 | }
7 |
8 | /**
9 | * Tracks storage both in `localStorage` and in svelte's `writable` stores
10 | * Usage: `const name = storable('name', 'jimmy')`
11 | * @param {string} key - `localStorage` key
12 | * @param {any} value - default/initial value (if value is already set in `localStorage`, it will load that value instead)
13 | * @param {Function} fn - handed off to `writable`
14 | */
15 |
16 | export const storable = (key, value, fn) => {
17 | key = `stallion.store.${key}`
18 | if (storage[key]) { value = JSON.parse(storage[key]) }
19 |
20 | const store = writable(value, fn)
21 | store.subscribe(value => {
22 | if (value === undefined) {
23 | storage.removeItem(key)
24 | } else {
25 | storage[key] = JSON.stringify(value)
26 | }
27 | })
28 |
29 | store.remove = () => store.set(undefined)
30 |
31 | return store
32 | }
33 |
--------------------------------------------------------------------------------
/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/routes/settings/admin/users.svelte:
--------------------------------------------------------------------------------
1 | Users
2 |
3 | {#if items && items.length}
4 |
5 | {#each items as item}
6 |
7 | {/each}
8 |
9 | {/if}
10 |
11 |
18 |
19 |
38 |
39 |
40 |
--------------------------------------------------------------------------------
/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.
--------------------------------------------------------------------------------
/src/components/forms/LabelRadio.svelte:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | {label}
5 |
6 | {#if help}
7 | {help}
8 | {/if}
9 |
10 |
11 |
30 |
31 |
51 |
--------------------------------------------------------------------------------
/src/_client/svg/originals/valid-good.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | valid-good
5 | Created with Sketch.
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/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/routes/contents/edit/_fields/String.svelte:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
35 |
--------------------------------------------------------------------------------
/src/routes/api/contents/create.json.js:
--------------------------------------------------------------------------------
1 | import { log } from '../../../_server/utils/db-tools.js'
2 | import { driver } from '../../../_server/db/arangodb-driver.js'
3 | const db = driver.connect()
4 |
5 | export async function post(req, res) {
6 |
7 | try {
8 | let { name, description } = req.body
9 |
10 | // purifying it's soul
11 | name = name
12 | .toLowerCase()
13 | .replace(/[^a-z0-9-]+/g, '')
14 | .replace(/-+/g, '-')
15 | .replace(/^-|-$/g, '')
16 |
17 | const contentName = `stallion-${name}`
18 | const Content = db.collection(contentName)
19 | const exists = await Content.exists()
20 | if (!exists) {
21 | // create the content
22 | log(await Content.create(), `Creating "${name}" Content collection`)
23 |
24 | // save the content information
25 | const Contents = db.collection('contents')
26 | const info = await Contents.save({ name, description })
27 | log(info, `Saving "${name}" doc info to Contents collection`)
28 |
29 | res.json({
30 | id: info._key,
31 | name,
32 | description,
33 | })
34 | } else {
35 | throw new Error('Content already exists.')
36 | }
37 | } catch (error) {
38 | res.json({ error: 1, message: error.message })
39 | }
40 |
41 | }
42 |
--------------------------------------------------------------------------------
/src/components/forms/MiniForm.svelte:
--------------------------------------------------------------------------------
1 |
18 |
19 |
32 |
33 |
58 |
--------------------------------------------------------------------------------
/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/routes/contents/index.svelte:
--------------------------------------------------------------------------------
1 | Content Types
2 |
3 |
4 |
5 |
31 |
32 |
55 |
--------------------------------------------------------------------------------
/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/template.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | %sapper.base%
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 | %sapper.styles%
25 | %sapper.head%
26 |
27 |
28 | %sapper.html%
29 |
30 | %sapper.scripts%
31 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/src/components/shared/FlexInput.svelte:
--------------------------------------------------------------------------------
1 |
9 |
10 |
24 |
25 |
51 |
--------------------------------------------------------------------------------
/src/routes/contents/edit/_fields/Text.svelte:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
39 |
--------------------------------------------------------------------------------
/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/routes/api/restart.json.js:
--------------------------------------------------------------------------------
1 | import child from 'child_process'
2 |
3 | // import config from '../../_server/build/config.js'
4 | // import { getApi } from '../../_server/db/arangodb-api.js'
5 | // const api = getApi()
6 | // const prod = process.env.NODE_ENV === 'production'
7 |
8 | export async function get(req, res) {
9 | try {
10 |
11 | // const { username, email, password } = req.body
12 |
13 |
14 | // process.on('SIGINT', function() {
15 | // console.log('restarting...');
16 | // child.fork(__filename)
17 | // process.exit(0)
18 | // })
19 |
20 | // console.log('Running as %d', process.pid)
21 |
22 | // just some code to keep the process running
23 | // setTimeout(function() {}, 1000000)
24 | debugger
25 |
26 | // const debugArgRegex = /--inspect(?:-brk|-port)?|--debug-port/
27 | // const execArgv = process.execArgv.slice()
28 | // if (execArgv.some(arg => !!arg.match(debugArgRegex))) {
29 | // debugger
30 | // execArgv.push('--inspect-port=9222')
31 | // }
32 |
33 | debugger
34 | console.log('This is pid ' + process.pid)
35 | process.on('exit', function() {
36 | debugger
37 | child.spawn(process.argv.shift(), process.argv, {
38 | cwd: process.cwd(),
39 | env: Object.assign({ PORT: 3001 }, process.env),
40 | detached: true,
41 | stdio: 'inherit',
42 | // execArgv,
43 | })
44 | })
45 | debugger
46 | process.exit(0)
47 |
48 |
49 | res.status(200).send({ filename: __filename })
50 | } catch (error) {
51 | res.status(400).send({ error: 'Not sure what went wrong...' })
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/src/_client/svg/compiled/social-instagram.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/src/components/List.svelte:
--------------------------------------------------------------------------------
1 |
2 | {#each items as item}
3 |
4 |
5 |
6 | {#if item.description}
7 |
{item.description}
8 | {/if}
9 |
10 | {#if $session.user.role === 'admin'}
11 |
17 | {/if}
18 |
19 | {/each}
20 |
21 |
22 |
34 |
35 |
68 |
--------------------------------------------------------------------------------
/static/svg/social-instagram.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/src/components/svg/Loader.svelte:
--------------------------------------------------------------------------------
1 |
11 |
12 |
23 |
24 |
54 |
--------------------------------------------------------------------------------
/src/routes/contents/edit/_fields/Number.svelte:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
50 |
--------------------------------------------------------------------------------
/src/_client/svg/originals/valid-bad.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | valid-bad
5 | Created with Sketch.
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/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/routes/settings/admin/collections/index.svelte:
--------------------------------------------------------------------------------
1 | Collections
2 |
3 |
4 |
5 | Node Collections
6 |
7 | {#each collections.nodes as node}
8 | {node.name}
9 | {/each}
10 |
11 |
12 | Edge Collections
13 |
14 | {#each collections.edges as edge}
15 | {edge.name}
16 | {/each}
17 |
18 |
19 |
36 |
39 |
40 |
63 |
--------------------------------------------------------------------------------
/src/routes/contents/create.svelte:
--------------------------------------------------------------------------------
1 | Create a New Content Type
2 |
3 |
12 |
13 |
42 |
43 |
53 |
--------------------------------------------------------------------------------
/src/routes/api/signup.json.js:
--------------------------------------------------------------------------------
1 | import jwt from 'jsonwebtoken'
2 | import bcrypt from 'bcryptjs'
3 | import config from '../../_server/build/config.js'
4 | import { getApi } from '../../_server/db/arangodb-api.js'
5 | const api = getApi()
6 | const prod = process.env.NODE_ENV === 'production'
7 |
8 | export async function post(req, res) {
9 | try {
10 | const { username, email, password } = req.body
11 |
12 | // FIXME: perform *** ALL *** validation tasks
13 | const keyExists = await api.get('users', username)
14 | const emailExists = await api.find('users', { email })
15 |
16 | if (keyExists || emailExists) {
17 | res.end(JSON.stringify({ keyExists: !!keyExists, emailExists: !!emailExists }))
18 | }
19 |
20 | const user = await api.set('users', {
21 | _key: username,
22 | created: Date.now(),
23 | modified: Date.now(),
24 | email,
25 | // bio: '',
26 | avatar: `/img/no-avatar-${Math.floor(Math.random() * 6) + 1}.png`,
27 | displayName: username,
28 | // first: '',
29 | // last: '',
30 | hash: await bcrypt.hash(password, 10),
31 | role: 'free', // for now
32 | })
33 |
34 | user.username = user._key
35 | delete user._id
36 | delete user._key
37 | delete user._rev
38 | delete user.hash
39 |
40 | // generate a signed son web token with the contents of user object and return it in the response
41 | const month = 60 * 60 * 24 * 30
42 | const token = jwt.sign(user, config.STALLION_JWT_SECRET, { expiresIn: month })
43 | res.cookie('stallion', token, {
44 | httpOnly: prod,
45 | secure: prod,
46 | maxAge: 1000 * month,
47 | })
48 | res.status(200).send({ user })
49 | } catch (error) {
50 | res.status(400).send({ error: 'req body should take the form { username, password }' })
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/src/routes/contents/edit/[name].svelte:
--------------------------------------------------------------------------------
1 |
2 |
3 |
{content.name}
4 |
9 |
10 | {#if content.description}
11 |
{content.description}
12 | {/if}
13 |
14 |
15 |
16 | {#if content.fields && content.fields.length}
17 |
18 | {#each content.fields as field}
19 | {field}
20 | {/each}
21 |
22 | {:else}
23 |
There are no fields yet
24 |
Add your first field to content type "{content.name}".
25 | {/if}
26 | {#if !adding}
27 |
28 | adding = true}>Add New Field
29 |
30 | {/if}
31 |
32 |
33 | {#if adding}
34 |
35 | {/if}
36 |
37 |
44 |
45 |
50 |
51 |
65 |
--------------------------------------------------------------------------------
/src/components/forms/Popup.svelte:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
9 |
74 |
--------------------------------------------------------------------------------
/src/components/layout/Header.svelte:
--------------------------------------------------------------------------------
1 |
14 |
15 |
25 |
26 |
78 |
--------------------------------------------------------------------------------
/src/_client/svg/originals/social-instagram.svg:
--------------------------------------------------------------------------------
1 | Instagram icon
--------------------------------------------------------------------------------
/src/routes/api/signup-validate.json.js:
--------------------------------------------------------------------------------
1 | import { emailRegex } from '../../_server/db/validators/generic.js'
2 | import { getApi } from '../../_server/db/arangodb-api.js'
3 | const api = getApi()
4 |
5 | export async function post(req, res) {
6 | res.writeHead(200, { 'Content-Type': 'application/json' })
7 | try {
8 | res.end(req.body.type === 'username' ? await checkUsername(req) : await checkEmail(req))
9 | } catch (error) {
10 | res.end({ valid: false, message: 'Something went wrong. Please contact us for help.' })
11 | }
12 | }
13 |
14 | async function checkUsername(req) {
15 | let message = ''
16 | let valid = true
17 | const found = await api.find('users', req.body.filters)
18 | if (found && !found.error) {
19 | message = 'That username is already in use.'
20 | valid = false
21 | }
22 | return JSON.stringify({ valid, message })
23 | }
24 |
25 | async function checkEmail(req) {
26 | let message = ''
27 | let valid = emailRegex.test(req.body.filters.email)
28 | if (!valid) {
29 | // TODO: move this to the front end?
30 | message = 'Email is invalid.'
31 | } else {
32 | const found = await api.find('users', req.body.filters)
33 | if (found && !found.error) {
34 | message = 'Email is already taken.'
35 | valid = false
36 | }
37 | }
38 | return JSON.stringify({ valid, message })
39 | }
40 |
41 |
42 | // if (!collection) { throw new Error('No collection set in `find`') }
43 | // if (!filters) { throw new Error('No filters set in `find`') }
44 | // let query = `FOR document IN \`${collection}\``
45 | // for (let key in filters) {
46 | // const value = filters[key]
47 | // if (typeof value === 'string') {
48 | // query += ` FILTER document.${key} == "${value}"`
49 | // } else {
50 | // query += ` FILTER document.${key} == ${value}`
51 | // }
52 | // }
53 | // query += ' RETURN document'
54 | // const cursor = await db.query(query)
55 | // return cursor.next()
56 |
--------------------------------------------------------------------------------
/src/routes/contents/edit/_fields/Relation.svelte:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | {#each radios as radio}
8 |
9 | {/each}
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
44 |
45 |
68 |
--------------------------------------------------------------------------------
/src/routes/profiles.svelte:
--------------------------------------------------------------------------------
1 | Stallion - Browse Profiles
2 |
3 | Browse Profiles
4 | {#if users}
5 |
6 |
7 | {#each users as user}
8 |
9 |
10 | {#if user.avatar}
11 |
12 | {/if}
13 |
14 |
15 |
16 |
17 |
{user.username}
18 | {#if user.bio}
19 |
{user.bio}
20 | {/if}
21 |
22 |
23 | {/each}
24 |
25 |
26 | {:else}
27 | LOADING
28 | {/if}
29 |
30 |
37 |
38 |
41 |
42 |
73 |
--------------------------------------------------------------------------------
/src/components/layout/Sidebar.svelte:
--------------------------------------------------------------------------------
1 |
15 |
16 |
32 |
33 |
76 |
--------------------------------------------------------------------------------
/src/_client/svg/originals/logo-public.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Path 2 Copy
5 | Created with Sketch.
6 |
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/src/components/forms/LabelSelect.svelte:
--------------------------------------------------------------------------------
1 |
2 | {label}
3 |
4 |
5 | {#each options as option}
6 | {option.name || option.value}
7 | {/each}
8 |
9 | {#if help}
10 | {help}
11 | {/if}
12 | {#if !valid}
13 |
14 | {/if}
15 |
16 |
17 |
18 |
40 |
41 |
88 |
--------------------------------------------------------------------------------
/src/routes/login.svelte:
--------------------------------------------------------------------------------
1 |
2 | Username
3 |
4 |
5 |
6 | Password
7 |
8 |
9 | Log In
10 |
11 |
12 |
18 |
19 |
64 |
65 |
78 |
--------------------------------------------------------------------------------
/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/components/forms/LabelInput.svelte:
--------------------------------------------------------------------------------
1 |
2 | {label}
3 |
4 | {#if placeholder}
5 |
6 | {:else}
7 |
8 | {/if}
9 | {#if help}
10 | {help}
11 | {/if}
12 | {#if !valid}
13 |
14 | {/if}
15 |
16 |
17 |
18 |
45 |
46 |
95 |
--------------------------------------------------------------------------------
/src/components/forms/LabelTextarea.svelte:
--------------------------------------------------------------------------------
1 |
2 | {label}
3 |
4 | {#if placeholder}
5 |
6 | {:else}
7 |
8 | {/if}
9 | {#if help}
10 | {help}
11 | {/if}
12 | {#if !valid}
13 |
14 | {/if}
15 |
16 |
17 |
18 |
42 |
43 |
94 |
--------------------------------------------------------------------------------
/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/components/layout/Nav.svelte:
--------------------------------------------------------------------------------
1 |
2 |
3 | {#if $session.user}
4 |
5 | Log Out
6 | {:else}
7 | Log In
8 | {/if}
9 |
10 |
11 |
12 |
23 |
24 |
100 |
--------------------------------------------------------------------------------
/src/routes/settings/_layout.svelte:
--------------------------------------------------------------------------------
1 |
2 |
3 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
29 |
30 |
36 |
37 |
92 |
--------------------------------------------------------------------------------
/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/routes/contents/edit/_AddFieldForm.svelte:
--------------------------------------------------------------------------------
1 |
2 |
3 | Add New Field
4 |
5 |
6 |
7 |
8 |
9 | {#if type === 'String'}
10 |
11 | {:else if type === 'Text'}
12 |
13 | {:else if type === 'Number'}
14 |
15 | {:else if type === 'Boolean'}
16 |
17 | {:else if type === 'Date'}
18 |
19 | {:else if type === 'Email'}
20 |
21 | {:else if type === 'Media'}
22 |
23 | {:else if type === 'Relation'}
24 |
25 | {/if}
26 |
27 |
28 |
29 |
30 | {JSON.stringify(recipe)}
31 |
32 |
33 |
75 |
76 |
--------------------------------------------------------------------------------
/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/routes/cm/[profile]/index.svelte:
--------------------------------------------------------------------------------
1 |
2 | {user && user.username ? user.username : 'Loading...'} Profile
3 |
4 |
5 |
6 | {#if user}
7 |
8 |
9 |
{user.displayName}
10 |
{user.username}
11 | {#if user.bio}
12 |
{user.bio}
13 |
14 | {/if}
15 |
16 | {#if user.posts && user.posts.length}
17 |
18 |
Select a project:
19 |
20 | {#each user.posts as post}
21 |
{post.name}
22 | {/each}
23 |
24 |
25 | {/if}
26 | {:else}
27 |
LOADING
28 | {/if}
29 |
30 |
31 |
49 |
50 |
53 |
54 |
101 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/routes/_layout.svelte:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | {#if $session.user}
5 |
6 |
7 |
8 |
9 | {:else}
10 |
11 | {/if}
12 |
13 |
14 |
15 |
63 |
64 |
114 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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.js:
--------------------------------------------------------------------------------
1 | // FIXME: config should work
2 | // import config from './_server/build/config'
3 | import { white, green } from 'ansi-colors'
4 | import express from 'express'
5 | import morgan from 'morgan'
6 | import helmet from 'helmet'
7 | import compression from 'compression'
8 | import sirv from 'sirv'
9 | import bodyParser from 'body-parser'
10 | import cookieParser from 'cookie-parser'
11 | import { appSetup } from './_server/services/app-setup.js'
12 | import { authSetup } from './_server/services/auth-setup.js'
13 | import { graphqlSetup } from './_server/services/graphql-setup.js'
14 | import { validate } from './routes/_services/auth-check.js'
15 | import * as sapper from '@sapper/server'
16 |
17 | const { PORT, NODE_ENV } = process.env
18 | const development = NODE_ENV === 'development'
19 |
20 | // https://www.joyent.com/node-js/production/design/errors
21 | // TODO: https://shapeshed.com/uncaught-exceptions-in-node/
22 | // handle all uncaught exceptions
23 | // see - https://nodejs.org/api/process.html#process_event_uncaughtexception
24 | process.on('uncaughtException', error => console.error('Uncaught Exception:'.red, error))
25 | // handle all unhandled promise rejections
26 | // see - http://bluebirdjs.com/docs/api/error-management-configuration.html#global-rejection-events
27 | // or for latest node - https://nodejs.org/api/process.html#process_event_unhandledrejection
28 | process.on('unhandledRejection', error => console.error('Unhandled Rejection:'.red, error))
29 |
30 | // ---> FOOTGUN!?
31 | async function start() {
32 |
33 | // NOTE: order matters!!! https://github.com/jaredhanson/passport/issues/14#issuecomment-4863459
34 | const app = express()
35 |
36 | app.use(morgan(development ? 'dev' : 'combined', {
37 | skip: (req, res) => development ? false : res.statusCode < 400,
38 | }))
39 |
40 | // SEE: https://expressjs.com/en/advanced/best-practice-security.html
41 | app.use(helmet())
42 | app.set('trust proxy', 1) // trust first proxy
43 |
44 | app.use(compression({ threshold: 0 }))
45 | app.use(sirv('static', { development }))
46 |
47 | // app.use(bodyParser.text())
48 | app.use(bodyParser.json())
49 | app.use(bodyParser.urlencoded({ extended: true }))
50 | app.use(cookieParser())
51 |
52 | await appSetup(app)
53 | await authSetup(app)
54 | await graphqlSetup(app)
55 |
56 | app.use(sapper.middleware({
57 | session: req => {
58 | const user = validate(req)
59 | return {
60 | env: NODE_ENV,
61 | user: user.unauthorized ? null : user,
62 | // req: req,
63 | }
64 | },
65 | }))
66 |
67 | app.listen(PORT, setTimeout(() => {
68 | console.log()
69 | console.log(' ,~~_')
70 | console.log(' |/\\ =_ _ ~')
71 | console.log(' _( )_( )\\~~')
72 | console.log(' \\,\\ _|\\ \\~~~')
73 | console.log(' \\` \\')
74 | console.log(' ` `')
75 | console.log()
76 | console.log(' ~~~ Stallion ~~~ Headless CMS built atop Svelte and Sapper.')
77 | console.log()
78 | console.log(green(` listening at ${white(process.env.STALLION_HOST)} in ${white(NODE_ENV)} mode on process ${white(process.pid)}`))
79 | // console.log(JSON.stringify(process.env, null, 2))
80 | if (development) {
81 | console.log(green(` Running ${white(`${process.env.STALLION_HOST}/graphiql`)} in development mode only...`))
82 | }
83 | console.log()
84 | }, 100))
85 |
86 | }
87 |
88 | start()
89 |
--------------------------------------------------------------------------------
/src/components/settings/ListItem.svelte:
--------------------------------------------------------------------------------
1 |
2 |
3 |
{item.title}
4 | {#if item.subtitle}
5 |
{item.subtitle}
6 | {/if}
7 | {#if item.description}
8 |
{@html item.description}
9 | {/if}
10 | {#if item.code}
11 |
{item.code}
12 | {/if}
13 | {#if fields.length}
14 |
22 | {/if}
23 |
24 |
28 | {#if deleteTry}
29 |
30 |
Are you sure you want to delete {type} `{id}`?
31 |
This action cannot be undone!
32 |
33 | deleteTry = false}>Cancel!
34 | dispatch('delete', { id })}>Delete
35 |
36 |
37 | {/if}
38 |
39 |
40 |
59 |
60 |
155 |
--------------------------------------------------------------------------------
/src/routes/api/[profile]/[dataset].js:
--------------------------------------------------------------------------------
1 | // FIXME: no longer needed...but the technique for dynamic GQL queries could be VERY useful...
2 | // FIXME: no longer needed...but the technique for dynamic GQL queries could be VERY useful...
3 | // FIXME: no longer needed...but the technique for dynamic GQL queries could be VERY useful...
4 | // FIXME: no longer needed...but the technique for dynamic GQL queries could be VERY useful...
5 | // FIXME: no longer needed...but the technique for dynamic GQL queries could be VERY useful...
6 | // FIXME: no longer needed...but the technique for dynamic GQL queries could be VERY useful...
7 | // FIXME: no longer needed...but the technique for dynamic GQL queries could be VERY useful...
8 |
9 |
10 | import { validate } from '../../_services/auth-check'
11 | import { graphql } from 'graphql'
12 | import { makeExecutableSchema } from 'graphql-tools'
13 | import { getApi } from '../../../_server/db/arangodb-api'
14 | const api = getApi()
15 | // import mappers from '../../../_server/graphql-api/utils/mappers'
16 |
17 | const matchTypes = {
18 | string: 'String',
19 | integer: 'Int',
20 | number: 'Int',
21 | // float: 'Float',
22 | // boolean: 'Boolean',
23 | }
24 |
25 | export async function get(req, res) {
26 | parseRequest(req, res)
27 | }
28 |
29 | export async function post(req, res) {
30 | parseRequest(req, res)
31 | }
32 |
33 | async function parseRequest(req, res) {
34 | res.writeHead(200, { 'Content-Type': 'application/json' })
35 |
36 | // TODO: check some kind of auth setting
37 | // SSH? Token? bearer? password? etc.?
38 | const { params } = req
39 |
40 | if (params.profile) {
41 | if (params.dataset) {
42 |
43 | const key = `${params.profile}-${params.dataset}`
44 | const repo = await api.get('repos', key)
45 | const schema = await api.get('repos', key)
46 |
47 | if (repo.private) {
48 | const test = validate(req)
49 | if (test.unauthorized) {
50 | res.end(test.message)
51 | return
52 | }
53 | }
54 |
55 | const typeDefParts = repo.headers.map(header => {
56 | // FIXME: what to do with multiple types?
57 | // SEE: https://github.com/apollographql/apollo-server/issues/71
58 | // ALSO: https://graphql.org/learn/schema/#union-types
59 | // ALSO: https://medium.com/the-graphqlhub/graphql-tour-interfaces-and-unions-7dd5be35de0d
60 | // FOR NOW, just allowing first type - - LAME
61 | let type = schema.properties[header].type
62 | type = Array.isArray(type) ? type[0] : type
63 | const normalizedHeader = header.replace(/\W/g, '')
64 | return `${normalizedHeader}: ${matchTypes[type]}`
65 | }).join('\n')
66 |
67 | // POTENTIAL QUERIES:
68 | // arxpoetica-star-wars-films/17127344
69 | // arxpoetica-narnia-books/17127376
70 | // arxpoetica-narnia-books/17127382
71 | // http://localhost:3000/api/arxpoetica/star-wars-films?query={rows{Title}}
72 | // http://localhost:3000/api/arxpoetica/star-wars-films?query={row(id:"17127344"){Title}}
73 |
74 | const query = req.body.query || req.query.query
75 | // console.log(query)
76 |
77 | // TODO: create & load the schema from the database
78 | const typeDefs = `
79 | type Row {
80 | ${typeDefParts}
81 | }
82 | type Query {
83 | rows: [Row]
84 | row(id: String!): Row
85 | }
86 | `
87 | const resolvers = {
88 | Query: {
89 | rows: async() => await datasetsApi.getAll(key),
90 | row: async(_, { id }) => await datasetsApi.get(key, id),
91 | },
92 | }
93 | // FIXME: logger should be dev only:
94 | const logger = { log: error => console.log(error) }
95 | const GQLSchema = makeExecutableSchema({ typeDefs, resolvers, logger })
96 | const response = await graphql(GQLSchema, query)
97 | res.end(JSON.stringify(response.data))
98 | } else {
99 | sendApiError(res, 'TODO: send down a list of all datasets? And other useful profile information?')
100 | }
101 | } else {
102 | sendApiError(res, 'Incorrect endpoint')
103 | }
104 | }
105 |
106 | function sendApiError(res, message) {
107 | res.end(JSON.stringify({ message }))
108 | }
109 |
--------------------------------------------------------------------------------
/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/_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/components/layout/NavMenu.svelte:
--------------------------------------------------------------------------------
1 |
29 |
30 |
66 |
67 |
179 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/routes/setup/index.svelte:
--------------------------------------------------------------------------------
1 |
2 | Pick a username, email, and password
3 |
4 | Pick a username
5 |
6 | {#if usernameValid === false}
7 |
8 | {/if}
9 |
10 |
11 | Email address
12 |
13 | {#if emailValid === false}
14 |
15 | {/if}
16 |
17 |
18 | Create a password
19 |
20 | {#if passwordValid === false}
21 |
22 | {/if}
23 |
24 | Sign Up
25 |
26 |
27 | {#if reload}
28 |
29 | {/if}
30 |
31 |
130 |
131 |
206 |
--------------------------------------------------------------------------------