mirror of
https://github.com/roleypoly/roleypoly.git
synced 2025-04-24 19:39:11 +00:00
chore: update prettier tab width for consistency (#175)
This commit is contained in:
parent
a931f8c69c
commit
f24d2fcc99
247 changed files with 7224 additions and 7375 deletions
|
@ -1,3 +1,3 @@
|
||||||
module.exports = {
|
module.exports = {
|
||||||
plugins: ['styled-components'],
|
plugins: ['styled-components'],
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,34 +1,34 @@
|
||||||
{
|
{
|
||||||
"name": "Roleypoly (Go, Node)",
|
"name": "Roleypoly (Go, Node)",
|
||||||
"image": "ghcr.io/roleypoly/dev-container:main",
|
"image": "ghcr.io/roleypoly/dev-container:main",
|
||||||
// Set *default* container specific settings.json values on container create.
|
// Set *default* container specific settings.json values on container create.
|
||||||
"settings": {
|
"settings": {
|
||||||
"terminal.integrated.shell.linux": "/bin/bash"
|
"terminal.integrated.shell.linux": "/bin/bash"
|
||||||
},
|
},
|
||||||
// Add the IDs of extensions you want installed when the container is created.
|
// Add the IDs of extensions you want installed when the container is created.
|
||||||
"extensions": [
|
"extensions": [
|
||||||
"dbaeumer.vscode-eslint",
|
"dbaeumer.vscode-eslint",
|
||||||
"golang.go",
|
"golang.go",
|
||||||
"hashicorp.terraform",
|
"hashicorp.terraform",
|
||||||
"firsttris.vscode-jest-runner",
|
"firsttris.vscode-jest-runner",
|
||||||
"esbenp.prettier-vscode",
|
"esbenp.prettier-vscode",
|
||||||
"jpoissonnier.vscode-styled-components",
|
"jpoissonnier.vscode-styled-components",
|
||||||
"eg2.vscode-npm-script",
|
"eg2.vscode-npm-script",
|
||||||
"christian-kohler.npm-intellisense",
|
"christian-kohler.npm-intellisense",
|
||||||
"ms-azuretools.vscode-docker",
|
"ms-azuretools.vscode-docker",
|
||||||
"eamodio.gitlens",
|
"eamodio.gitlens",
|
||||||
"davidanson.vscode-markdownlint",
|
"davidanson.vscode-markdownlint",
|
||||||
"stylelint.vscode-stylelint",
|
"stylelint.vscode-stylelint",
|
||||||
"pflannery.vscode-versionlens",
|
"pflannery.vscode-versionlens",
|
||||||
"visualstudioexptteam.vscodeintellicode",
|
"visualstudioexptteam.vscodeintellicode",
|
||||||
"bungcip.better-toml"
|
"bungcip.better-toml"
|
||||||
],
|
],
|
||||||
// Use 'forwardPorts' to make a list of ports inside the container available locally.
|
// Use 'forwardPorts' to make a list of ports inside the container available locally.
|
||||||
// "forwardPorts": [],
|
// "forwardPorts": [],
|
||||||
// Use 'postCreateCommand' to run commands after the container is created.
|
// Use 'postCreateCommand' to run commands after the container is created.
|
||||||
// "postCreateCommand": "uname -a",
|
// "postCreateCommand": "uname -a",
|
||||||
// Uncomment when using a ptrace-based debugger like C++, Go, and Rust
|
// Uncomment when using a ptrace-based debugger like C++, Go, and Rust
|
||||||
// "runArgs": [ "--cap-add=SYS_PTRACE", "--security-opt", "seccomp=unconfined" ],
|
// "runArgs": [ "--cap-add=SYS_PTRACE", "--security-opt", "seccomp=unconfined" ],
|
||||||
// Uncomment to connect as a non-root user. See https://aka.ms/vscode-remote/containers/non-root.
|
// Uncomment to connect as a non-root user. See https://aka.ms/vscode-remote/containers/non-root.
|
||||||
"remoteUser": "vscode"
|
"remoteUser": "vscode"
|
||||||
}
|
}
|
||||||
|
|
250
.eslintrc.js
250
.eslintrc.js
|
@ -1,128 +1,128 @@
|
||||||
module.exports = {
|
module.exports = {
|
||||||
env: {
|
env: {
|
||||||
browser: true,
|
browser: true,
|
||||||
es6: true,
|
es6: true,
|
||||||
node: true,
|
node: true,
|
||||||
},
|
},
|
||||||
extends: ['prettier', 'prettier/@typescript-eslint'],
|
extends: ['prettier', 'prettier/@typescript-eslint'],
|
||||||
parser: '@typescript-eslint/parser',
|
parser: '@typescript-eslint/parser',
|
||||||
parserOptions: {
|
parserOptions: {
|
||||||
project: 'tsconfig.json',
|
project: 'tsconfig.json',
|
||||||
sourceType: 'module',
|
sourceType: 'module',
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
'eslint-plugin-import',
|
'eslint-plugin-import',
|
||||||
'eslint-plugin-jsdoc',
|
'eslint-plugin-jsdoc',
|
||||||
'eslint-plugin-react',
|
'eslint-plugin-react',
|
||||||
'@typescript-eslint',
|
'@typescript-eslint',
|
||||||
'@typescript-eslint/tslint',
|
'@typescript-eslint/tslint',
|
||||||
|
],
|
||||||
|
rules: {
|
||||||
|
'react/jsx-uses-react': 'off',
|
||||||
|
'react/react-in-jsx-scope': 'off',
|
||||||
|
'@typescript-eslint/await-thenable': 'error',
|
||||||
|
'@typescript-eslint/consistent-type-assertions': 'error',
|
||||||
|
'@typescript-eslint/indent': 'off',
|
||||||
|
'@typescript-eslint/member-delimiter-style': [
|
||||||
|
'off',
|
||||||
|
{
|
||||||
|
multiline: {
|
||||||
|
delimiter: 'none',
|
||||||
|
requireLast: true,
|
||||||
|
},
|
||||||
|
singleline: {
|
||||||
|
delimiter: 'semi',
|
||||||
|
requireLast: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
],
|
],
|
||||||
rules: {
|
'@typescript-eslint/no-empty-function': 'error',
|
||||||
'react/jsx-uses-react': 'off',
|
'@typescript-eslint/no-floating-promises': 'error',
|
||||||
'react/react-in-jsx-scope': 'off',
|
'@typescript-eslint/no-misused-new': 'error',
|
||||||
'@typescript-eslint/await-thenable': 'error',
|
'@typescript-eslint/no-unnecessary-qualifier': 'error',
|
||||||
'@typescript-eslint/consistent-type-assertions': 'error',
|
'@typescript-eslint/no-unnecessary-type-assertion': 'error',
|
||||||
'@typescript-eslint/indent': 'off',
|
'@typescript-eslint/no-unused-expressions': [
|
||||||
'@typescript-eslint/member-delimiter-style': [
|
'error',
|
||||||
'off',
|
{
|
||||||
{
|
allowTaggedTemplates: true,
|
||||||
multiline: {
|
allowShortCircuit: true,
|
||||||
delimiter: 'none',
|
},
|
||||||
requireLast: true,
|
],
|
||||||
},
|
'@typescript-eslint/prefer-namespace-keyword': 'error',
|
||||||
singleline: {
|
'@typescript-eslint/quotes': 'off',
|
||||||
delimiter: 'semi',
|
'@typescript-eslint/semi': ['off', null],
|
||||||
requireLast: false,
|
'@typescript-eslint/triple-slash-reference': [
|
||||||
},
|
'error',
|
||||||
},
|
{
|
||||||
],
|
path: 'always',
|
||||||
'@typescript-eslint/no-empty-function': 'error',
|
types: 'prefer-import',
|
||||||
'@typescript-eslint/no-floating-promises': 'error',
|
lib: 'always',
|
||||||
'@typescript-eslint/no-misused-new': 'error',
|
},
|
||||||
'@typescript-eslint/no-unnecessary-qualifier': 'error',
|
],
|
||||||
'@typescript-eslint/no-unnecessary-type-assertion': 'error',
|
'@typescript-eslint/type-annotation-spacing': 'off',
|
||||||
'@typescript-eslint/no-unused-expressions': [
|
'@typescript-eslint/unified-signatures': 'error',
|
||||||
'error',
|
'arrow-parens': ['off', 'always'],
|
||||||
{
|
'brace-style': ['off', 'off'],
|
||||||
allowTaggedTemplates: true,
|
'comma-dangle': 'off',
|
||||||
allowShortCircuit: true,
|
curly: ['error', 'multi-line'],
|
||||||
},
|
'eol-last': 'off',
|
||||||
],
|
eqeqeq: ['error', 'smart'],
|
||||||
'@typescript-eslint/prefer-namespace-keyword': 'error',
|
'id-blacklist': [
|
||||||
'@typescript-eslint/quotes': 'off',
|
'error',
|
||||||
'@typescript-eslint/semi': ['off', null],
|
'any',
|
||||||
'@typescript-eslint/triple-slash-reference': [
|
'Number',
|
||||||
'error',
|
'number',
|
||||||
{
|
'String',
|
||||||
path: 'always',
|
'string',
|
||||||
types: 'prefer-import',
|
'Boolean',
|
||||||
lib: 'always',
|
'boolean',
|
||||||
},
|
'Undefined',
|
||||||
],
|
'undefined',
|
||||||
'@typescript-eslint/type-annotation-spacing': 'off',
|
],
|
||||||
'@typescript-eslint/unified-signatures': 'error',
|
'id-match': 'error',
|
||||||
'arrow-parens': ['off', 'always'],
|
'import/no-deprecated': 'error',
|
||||||
'brace-style': ['off', 'off'],
|
'jsdoc/check-alignment': 'error',
|
||||||
'comma-dangle': 'off',
|
'jsdoc/check-indentation': 'error',
|
||||||
curly: ['error', 'multi-line'],
|
'jsdoc/newline-after-description': 'error',
|
||||||
'eol-last': 'off',
|
'linebreak-style': 'off',
|
||||||
eqeqeq: ['error', 'smart'],
|
'max-len': 'off',
|
||||||
'id-blacklist': [
|
'new-parens': 'off',
|
||||||
'error',
|
'newline-per-chained-call': 'off',
|
||||||
'any',
|
'no-caller': 'error',
|
||||||
'Number',
|
'no-cond-assign': 'error',
|
||||||
'number',
|
'no-constant-condition': 'error',
|
||||||
'String',
|
'no-control-regex': 'error',
|
||||||
'string',
|
'no-duplicate-imports': 'error',
|
||||||
'Boolean',
|
'no-empty': 'error',
|
||||||
'boolean',
|
'no-eval': 'error',
|
||||||
'Undefined',
|
'no-extra-semi': 'off',
|
||||||
'undefined',
|
'no-fallthrough': 'error',
|
||||||
],
|
'no-invalid-regexp': 'error',
|
||||||
'id-match': 'error',
|
'no-irregular-whitespace': 'off',
|
||||||
'import/no-deprecated': 'error',
|
'no-multiple-empty-lines': 'off',
|
||||||
'jsdoc/check-alignment': 'error',
|
'no-redeclare': 'error',
|
||||||
'jsdoc/check-indentation': 'error',
|
'no-regex-spaces': 'error',
|
||||||
'jsdoc/newline-after-description': 'error',
|
'no-return-await': 'error',
|
||||||
'linebreak-style': 'off',
|
'no-throw-literal': 'error',
|
||||||
'max-len': 'off',
|
'no-trailing-spaces': 'off',
|
||||||
'new-parens': 'off',
|
'no-underscore-dangle': 'error',
|
||||||
'newline-per-chained-call': 'off',
|
'no-unused-labels': 'error',
|
||||||
'no-caller': 'error',
|
'no-var': 'error',
|
||||||
'no-cond-assign': 'error',
|
'one-var': ['error', 'never'],
|
||||||
'no-constant-condition': 'error',
|
'quote-props': 'off',
|
||||||
'no-control-regex': 'error',
|
radix: 'error',
|
||||||
'no-duplicate-imports': 'error',
|
'react/jsx-curly-spacing': 'off',
|
||||||
'no-empty': 'error',
|
'react/jsx-equals-spacing': 'off',
|
||||||
'no-eval': 'error',
|
'react/jsx-wrap-multilines': 'off',
|
||||||
'no-extra-semi': 'off',
|
'space-before-function-paren': 'off',
|
||||||
'no-fallthrough': 'error',
|
'space-in-parens': ['off', 'never'],
|
||||||
'no-invalid-regexp': 'error',
|
'spaced-comment': [
|
||||||
'no-irregular-whitespace': 'off',
|
'error',
|
||||||
'no-multiple-empty-lines': 'off',
|
'always',
|
||||||
'no-redeclare': 'error',
|
{
|
||||||
'no-regex-spaces': 'error',
|
markers: ['/'],
|
||||||
'no-return-await': 'error',
|
},
|
||||||
'no-throw-literal': 'error',
|
],
|
||||||
'no-trailing-spaces': 'off',
|
'use-isnan': 'error',
|
||||||
'no-underscore-dangle': 'error',
|
},
|
||||||
'no-unused-labels': 'error',
|
|
||||||
'no-var': 'error',
|
|
||||||
'one-var': ['error', 'never'],
|
|
||||||
'quote-props': 'off',
|
|
||||||
radix: 'error',
|
|
||||||
'react/jsx-curly-spacing': 'off',
|
|
||||||
'react/jsx-equals-spacing': 'off',
|
|
||||||
'react/jsx-wrap-multilines': 'off',
|
|
||||||
'space-before-function-paren': 'off',
|
|
||||||
'space-in-parens': ['off', 'never'],
|
|
||||||
'spaced-comment': [
|
|
||||||
'error',
|
|
||||||
'always',
|
|
||||||
{
|
|
||||||
markers: ['/'],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
'use-isnan': 'error',
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
32
.github/dependabot.yml
vendored
32
.github/dependabot.yml
vendored
|
@ -1,21 +1,21 @@
|
||||||
version: 2
|
version: 2
|
||||||
updates:
|
updates:
|
||||||
- package-ecosystem: 'npm'
|
- package-ecosystem: 'npm'
|
||||||
directory: '/'
|
directory: '/'
|
||||||
schedule:
|
schedule:
|
||||||
interval: 'daily'
|
interval: 'daily'
|
||||||
|
|
||||||
- package-ecosystem: 'github-actions'
|
- package-ecosystem: 'github-actions'
|
||||||
directory: '/'
|
directory: '/'
|
||||||
schedule:
|
schedule:
|
||||||
interval: 'daily'
|
interval: 'daily'
|
||||||
|
|
||||||
- package-ecosystem: 'gomod'
|
- package-ecosystem: 'gomod'
|
||||||
directory: '/'
|
directory: '/'
|
||||||
schedule:
|
schedule:
|
||||||
interval: 'daily'
|
interval: 'daily'
|
||||||
|
|
||||||
- package-ecosystem: 'terraform'
|
- package-ecosystem: 'terraform'
|
||||||
directory: '/terraform'
|
directory: '/terraform'
|
||||||
schedule:
|
schedule:
|
||||||
interval: 'daily'
|
interval: 'daily'
|
||||||
|
|
354
.github/workflows/build.yml
vendored
354
.github/workflows/build.yml
vendored
|
@ -3,211 +3,211 @@ name: Roleypoly CI
|
||||||
on: push
|
on: push
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
go_test:
|
go_test:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
name: Go CI
|
name: Go CI
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@master
|
- uses: actions/checkout@master
|
||||||
- uses: actions/cache@v2.1.4
|
- uses: actions/cache@v2.1.4
|
||||||
with:
|
with:
|
||||||
path: ~/go/pkg/mod
|
path: ~/go/pkg/mod
|
||||||
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
|
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
|
||||||
restore-keys: |
|
restore-keys: |
|
||||||
${{ runner.os }}-go-
|
${{ runner.os }}-go-
|
||||||
- uses: actions/setup-go@v2
|
- uses: actions/setup-go@v2
|
||||||
with:
|
with:
|
||||||
go-version: '^1.15.5'
|
go-version: '^1.15.5'
|
||||||
|
|
||||||
- run: go vet ./...
|
- run: go vet ./...
|
||||||
|
|
||||||
- run: go test ./...
|
- run: go test ./...
|
||||||
|
|
||||||
node_test:
|
node_test:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
name: Node CI
|
name: Node CI
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@master
|
- uses: actions/checkout@master
|
||||||
|
|
||||||
- uses: actions/setup-node@v2.1.5
|
- uses: actions/setup-node@v2.1.5
|
||||||
with:
|
with:
|
||||||
node-version: '14'
|
node-version: '14'
|
||||||
|
|
||||||
- name: Get yarn cache directory path
|
- name: Get yarn cache directory path
|
||||||
id: yarn-cache-dir-path
|
id: yarn-cache-dir-path
|
||||||
run: echo "::set-output name=dir::$(yarn cache dir)"
|
run: echo "::set-output name=dir::$(yarn cache dir)"
|
||||||
|
|
||||||
- uses: actions/cache@v2.1.4
|
- uses: actions/cache@v2.1.4
|
||||||
id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`)
|
id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`)
|
||||||
with:
|
with:
|
||||||
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
|
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
|
||||||
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
|
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
|
||||||
restore-keys: |
|
restore-keys: |
|
||||||
${{ runner.os }}-yarn-
|
${{ runner.os }}-yarn-
|
||||||
|
|
||||||
- run: yarn install --frozen-lockfile
|
- run: yarn install --frozen-lockfile
|
||||||
|
|
||||||
- run: yarn lint
|
- run: yarn lint
|
||||||
|
|
||||||
- run: yarn test
|
- run: yarn test
|
||||||
|
|
||||||
worker_build:
|
worker_build:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
name: Worker Build & Publish
|
name: Worker Build & Publish
|
||||||
needs:
|
needs:
|
||||||
- node_test
|
- node_test
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@master
|
- uses: actions/checkout@master
|
||||||
|
|
||||||
- uses: actions/setup-node@v2.1.5
|
- uses: actions/setup-node@v2.1.5
|
||||||
with:
|
with:
|
||||||
node-version: '14'
|
node-version: '14'
|
||||||
|
|
||||||
- name: Set up Cloud SDK
|
- name: Set up Cloud SDK
|
||||||
uses: google-github-actions/setup-gcloud@master
|
uses: google-github-actions/setup-gcloud@master
|
||||||
with:
|
with:
|
||||||
project_id: ${{ secrets.GCS_PROJECT_ID }}
|
project_id: ${{ secrets.GCS_PROJECT_ID }}
|
||||||
service_account_key: ${{ secrets.GCS_TF_KEY }}
|
service_account_key: ${{ secrets.GCS_TF_KEY }}
|
||||||
export_default_credentials: true
|
export_default_credentials: true
|
||||||
|
|
||||||
- name: Check if already deployed
|
- name: Check if already deployed
|
||||||
id: check
|
id: check
|
||||||
run: |
|
run: |
|
||||||
gsutil stat gs://roleypoly-artifacts/backend-worker/${{ github.sha }}/script.js \
|
gsutil stat gs://roleypoly-artifacts/backend-worker/${{ github.sha }}/script.js \
|
||||||
&& echo ::set-output name=skip::1 \
|
&& echo ::set-output name=skip::1 \
|
||||||
|| echo ::set-output name=skip::0
|
|| echo ::set-output name=skip::0
|
||||||
|
|
||||||
- run: npm i -g @cloudflare/wrangler
|
- run: npm i -g @cloudflare/wrangler
|
||||||
if: steps.check.outputs.skip == '0'
|
if: steps.check.outputs.skip == '0'
|
||||||
|
|
||||||
- name: Get yarn cache directory path
|
- name: Get yarn cache directory path
|
||||||
if: steps.check.outputs.skip == '0'
|
if: steps.check.outputs.skip == '0'
|
||||||
id: yarn-cache-dir-path
|
id: yarn-cache-dir-path
|
||||||
run: echo "::set-output name=dir::$(yarn cache dir)"
|
run: echo "::set-output name=dir::$(yarn cache dir)"
|
||||||
|
|
||||||
- uses: actions/cache@v2.1.4
|
- uses: actions/cache@v2.1.4
|
||||||
if: steps.check.outputs.skip == '0'
|
if: steps.check.outputs.skip == '0'
|
||||||
id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`)
|
id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`)
|
||||||
with:
|
with:
|
||||||
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
|
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
|
||||||
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
|
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
|
||||||
restore-keys: |
|
restore-keys: |
|
||||||
${{ runner.os }}-yarn-
|
${{ runner.os }}-yarn-
|
||||||
|
|
||||||
- run: yarn install --frozen-lockfile
|
- run: yarn install --frozen-lockfile
|
||||||
if: steps.check.outputs.skip == '0'
|
if: steps.check.outputs.skip == '0'
|
||||||
|
|
||||||
- run: |
|
- run: |
|
||||||
wrangler init
|
wrangler init
|
||||||
echo 'webpack_config = "packages/api/webpack.config.js"' | tee -a wrangler.toml
|
echo 'webpack_config = "packages/api/webpack.config.js"' | tee -a wrangler.toml
|
||||||
wrangler build
|
wrangler build
|
||||||
if: steps.check.outputs.skip == '0'
|
if: steps.check.outputs.skip == '0'
|
||||||
|
|
||||||
- id: upload-file
|
- id: upload-file
|
||||||
if: steps.check.outputs.skip == '0'
|
if: steps.check.outputs.skip == '0'
|
||||||
uses: google-github-actions/upload-cloud-storage@main
|
uses: google-github-actions/upload-cloud-storage@main
|
||||||
with:
|
with:
|
||||||
path: worker/script.js
|
path: worker/script.js
|
||||||
destination: roleypoly-artifacts/backend-worker/${{ github.sha }}
|
destination: roleypoly-artifacts/backend-worker/${{ github.sha }}
|
||||||
credentials: ${{ secrets.GCS_TF_KEY }}
|
credentials: ${{ secrets.GCS_TF_KEY }}
|
||||||
|
|
||||||
docker_build:
|
docker_build:
|
||||||
name: Docker Build & Publish
|
name: Docker Build & Publish
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs:
|
needs:
|
||||||
- go_test
|
- go_test
|
||||||
- node_test
|
- node_test
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
dockerfile:
|
dockerfile:
|
||||||
- bot
|
- bot
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@master
|
- uses: actions/checkout@master
|
||||||
|
|
||||||
- uses: actions/cache@v2.1.4
|
- uses: actions/cache@v2.1.4
|
||||||
with:
|
with:
|
||||||
path: /tmp/.buildx-cache
|
path: /tmp/.buildx-cache
|
||||||
key: ${{ runner.os }}-buildx-${{ github.sha }}
|
key: ${{ runner.os }}-buildx-${{ github.sha }}
|
||||||
restore-keys: |
|
restore-keys: |
|
||||||
${{ runner.os }}-buildx-
|
${{ runner.os }}-buildx-
|
||||||
|
|
||||||
- name: Docker meta
|
- name: Docker meta
|
||||||
id: docker_meta
|
id: docker_meta
|
||||||
uses: crazy-max/ghaction-docker-meta@v1
|
uses: crazy-max/ghaction-docker-meta@v1
|
||||||
with:
|
with:
|
||||||
images: |
|
images: |
|
||||||
ghcr.io/roleypoly/${{matrix.dockerfile}}
|
ghcr.io/roleypoly/${{matrix.dockerfile}}
|
||||||
tag-sha: true
|
tag-sha: true
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
id: buildx
|
id: buildx
|
||||||
uses: docker/setup-buildx-action@v1
|
uses: docker/setup-buildx-action@v1
|
||||||
with:
|
with:
|
||||||
install: true
|
install: true
|
||||||
|
|
||||||
- name: Login to GHCR
|
- name: Login to GHCR
|
||||||
uses: docker/login-action@v1
|
uses: docker/login-action@v1
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: roleypoly
|
username: roleypoly
|
||||||
password: ${{ secrets.GHCR_PAT }}
|
password: ${{ secrets.GHCR_PAT }}
|
||||||
|
|
||||||
- name: Build and push
|
- name: Build and push
|
||||||
uses: docker/build-push-action@v2
|
uses: docker/build-push-action@v2
|
||||||
id: docker
|
id: docker
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
file: ./hack/dockerfiles/${{matrix.dockerfile}}.Dockerfile
|
file: ./hack/dockerfiles/${{matrix.dockerfile}}.Dockerfile
|
||||||
push: true
|
push: true
|
||||||
cache-from: type=local,src=/tmp/.buildx-cache
|
cache-from: type=local,src=/tmp/.buildx-cache
|
||||||
cache-to: type=local,dest=/tmp/.buildx-cache
|
cache-to: type=local,dest=/tmp/.buildx-cache
|
||||||
tags: ${{ steps.docker_meta.outputs.tags }}
|
tags: ${{ steps.docker_meta.outputs.tags }}
|
||||||
labels: ${{ steps.docker_meta.outputs.labels }}
|
labels: ${{ steps.docker_meta.outputs.labels }}
|
||||||
|
|
||||||
- name: Pre-deploy - Save digest.txt
|
- name: Pre-deploy - Save digest.txt
|
||||||
run: |
|
run: |
|
||||||
echo "${{ steps.docker.outputs.digest }}" > digest.txt
|
echo "${{ steps.docker.outputs.digest }}" > digest.txt
|
||||||
|
|
||||||
- name: Pre-deploy - Make digest artifact
|
- name: Pre-deploy - Make digest artifact
|
||||||
uses: actions/upload-artifact@v2.2.2
|
uses: actions/upload-artifact@v2.2.2
|
||||||
with:
|
with:
|
||||||
name: ${{ matrix.dockerfile }}-digest
|
name: ${{ matrix.dockerfile }}-digest
|
||||||
path: digest.txt
|
path: digest.txt
|
||||||
|
|
||||||
trigger_deploy:
|
trigger_deploy:
|
||||||
name: Deploy to Stage
|
name: Deploy to Stage
|
||||||
needs:
|
needs:
|
||||||
- docker_build
|
- docker_build
|
||||||
- worker_build
|
- worker_build
|
||||||
if: github.ref == 'refs/heads/main'
|
if: github.ref == 'refs/heads/main'
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Get Bot digest
|
- name: Get Bot digest
|
||||||
uses: actions/download-artifact@v2
|
uses: actions/download-artifact@v2
|
||||||
with:
|
with:
|
||||||
name: bot-digest
|
name: bot-digest
|
||||||
path: .digests/bot
|
path: .digests/bot
|
||||||
|
|
||||||
- name: Set digests as addressable
|
- name: Set digests as addressable
|
||||||
id: digests
|
id: digests
|
||||||
env:
|
env:
|
||||||
IMAGES: bot
|
IMAGES: bot
|
||||||
run: |
|
run: |
|
||||||
set_digest_output() {
|
set_digest_output() {
|
||||||
echo ::set-output name=$1::@$(cat .digests/$1/digest.txt)
|
echo ::set-output name=$1::@$(cat .digests/$1/digest.txt)
|
||||||
}
|
}
|
||||||
|
|
||||||
for image in $IMAGES; do
|
for image in $IMAGES; do
|
||||||
set_digest_output $image
|
set_digest_output $image
|
||||||
done
|
done
|
||||||
|
|
||||||
- name: Invoke Deploy workflow
|
- name: Invoke Deploy workflow
|
||||||
uses: benc-uk/workflow-dispatch@v1
|
uses: benc-uk/workflow-dispatch@v1
|
||||||
with:
|
with:
|
||||||
workflow: Deploy
|
workflow: Deploy
|
||||||
token: ${{ secrets.GITOPS_TOKEN }}
|
token: ${{ secrets.GITOPS_TOKEN }}
|
||||||
inputs: |-
|
inputs: |-
|
||||||
{
|
{
|
||||||
"environment": "stage",
|
"environment": "stage",
|
||||||
"worker_tag": "${{ github.sha }}",
|
"worker_tag": "${{ github.sha }}",
|
||||||
"bot_tag": "${{ steps.digests.output.bot }}"
|
"bot_tag": "${{ steps.digests.output.bot }}"
|
||||||
}
|
}
|
||||||
|
|
78
.github/workflows/codeql-analysis.yml
vendored
78
.github/workflows/codeql-analysis.yml
vendored
|
@ -1,51 +1,51 @@
|
||||||
name: 'Code Scanning - Action'
|
name: 'Code Scanning - Action'
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
pull_request:
|
pull_request:
|
||||||
schedule:
|
schedule:
|
||||||
# ┌───────────── minute (0 - 59)
|
# ┌───────────── minute (0 - 59)
|
||||||
# │ ┌───────────── hour (0 - 23)
|
# │ ┌───────────── hour (0 - 23)
|
||||||
# │ │ ┌───────────── day of the month (1 - 31)
|
# │ │ ┌───────────── day of the month (1 - 31)
|
||||||
# │ │ │ ┌───────────── month (1 - 12 or JAN-DEC)
|
# │ │ │ ┌───────────── month (1 - 12 or JAN-DEC)
|
||||||
# │ │ │ │ ┌───────────── day of the week (0 - 6 or SUN-SAT)
|
# │ │ │ │ ┌───────────── day of the week (0 - 6 or SUN-SAT)
|
||||||
# │ │ │ │ │
|
# │ │ │ │ │
|
||||||
# │ │ │ │ │
|
# │ │ │ │ │
|
||||||
# │ │ │ │ │
|
# │ │ │ │ │
|
||||||
# * * * * *
|
# * * * * *
|
||||||
- cron: '30 1 * * 0'
|
- cron: '30 1 * * 0'
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
CodeQL-Build:
|
CodeQL-Build:
|
||||||
# CodeQL runs on ubuntu-latest, windows-latest, and macos-latest
|
# CodeQL runs on ubuntu-latest, windows-latest, and macos-latest
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
# Initializes the CodeQL tools for scanning.
|
# Initializes the CodeQL tools for scanning.
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
uses: github/codeql-action/init@v1
|
uses: github/codeql-action/init@v1
|
||||||
# Override language selection by uncommenting this and choosing your languages
|
# Override language selection by uncommenting this and choosing your languages
|
||||||
# with:
|
# with:
|
||||||
# languages: go, javascript, csharp, python, cpp, java
|
# languages: go, javascript, csharp, python, cpp, java
|
||||||
|
|
||||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||||
# If this step fails, then you should remove it and run the build manually (see below).
|
# If this step fails, then you should remove it and run the build manually (see below).
|
||||||
- name: Autobuild
|
- name: Autobuild
|
||||||
uses: github/codeql-action/autobuild@v1
|
uses: github/codeql-action/autobuild@v1
|
||||||
|
|
||||||
# ℹ️ Command-line programs to run using the OS shell.
|
# ℹ️ Command-line programs to run using the OS shell.
|
||||||
# 📚 https://git.io/JvXDl
|
# 📚 https://git.io/JvXDl
|
||||||
|
|
||||||
# ✏️ If the Autobuild fails above, remove it and uncomment the following
|
# ✏️ If the Autobuild fails above, remove it and uncomment the following
|
||||||
# three lines and modify them (or add more) to build your code if your
|
# three lines and modify them (or add more) to build your code if your
|
||||||
# project uses a compiled language
|
# project uses a compiled language
|
||||||
|
|
||||||
#- run: |
|
#- run: |
|
||||||
# make bootstrap
|
# make bootstrap
|
||||||
# make release
|
# make release
|
||||||
|
|
||||||
- name: Perform CodeQL Analysis
|
- name: Perform CodeQL Analysis
|
||||||
uses: github/codeql-action/analyze@v1
|
uses: github/codeql-action/analyze@v1
|
||||||
|
|
218
.github/workflows/deploy.yml
vendored
218
.github/workflows/deploy.yml
vendored
|
@ -1,128 +1,128 @@
|
||||||
name: Deploy
|
name: Deploy
|
||||||
|
|
||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
inputs:
|
inputs:
|
||||||
environment:
|
environment:
|
||||||
description: 'One of: stage, prod'
|
description: 'One of: stage, prod'
|
||||||
required: true
|
required: true
|
||||||
default: stage
|
default: stage
|
||||||
bot_tag:
|
bot_tag:
|
||||||
description: 'tag/digest reference to a UI container build'
|
description: 'tag/digest reference to a UI container build'
|
||||||
required: false
|
required: false
|
||||||
default: ':main'
|
default: ':main'
|
||||||
worker_tag:
|
worker_tag:
|
||||||
description: 'bucket key to fetch worker from'
|
description: 'bucket key to fetch worker from'
|
||||||
required: false
|
required: false
|
||||||
default: '' # Empty will try using current main branch hash
|
default: '' # Empty will try using current main branch hash
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
deploy_terraform:
|
deploy_terraform:
|
||||||
name: Deploy Terraform
|
name: Deploy Terraform
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@master
|
- uses: actions/checkout@master
|
||||||
|
|
||||||
- uses: hashicorp/setup-terraform@v1.3.2
|
- uses: hashicorp/setup-terraform@v1.3.2
|
||||||
with:
|
with:
|
||||||
terraform_version: ^0.14.0
|
terraform_version: ^0.14.0
|
||||||
|
|
||||||
- name: Set up Cloud SDK
|
- name: Set up Cloud SDK
|
||||||
uses: google-github-actions/setup-gcloud@master
|
uses: google-github-actions/setup-gcloud@master
|
||||||
with:
|
with:
|
||||||
project_id: ${{ secrets.GCS_PROJECT_ID }}
|
project_id: ${{ secrets.GCS_PROJECT_ID }}
|
||||||
service_account_key: ${{ secrets.GCS_TF_KEY }}
|
service_account_key: ${{ secrets.GCS_TF_KEY }}
|
||||||
export_default_credentials: true
|
export_default_credentials: true
|
||||||
|
|
||||||
- name: Get Google Secrets (they keep them in a box under a tree)
|
- name: Get Google Secrets (they keep them in a box under a tree)
|
||||||
id: secrets
|
id: secrets
|
||||||
uses: google-github-actions/get-secretmanager-secrets@main
|
uses: google-github-actions/get-secretmanager-secrets@main
|
||||||
with:
|
with:
|
||||||
secrets: |-
|
secrets: |-
|
||||||
secretJSON:${{ secrets.GCS_PROJECT_ID }}/${{github.event.inputs.environment}}-tfvars
|
secretJSON:${{ secrets.GCS_PROJECT_ID }}/${{github.event.inputs.environment}}-tfvars
|
||||||
|
|
||||||
- name: Pull necessary artifacts
|
- name: Pull necessary artifacts
|
||||||
working-directory: ./terraform
|
working-directory: ./terraform
|
||||||
run: |
|
run: |
|
||||||
currentHash=${{ github.sha }}
|
currentHash=${{ github.sha }}
|
||||||
targetArtifact=${{ github.event.inputs.worker_tag }}
|
targetArtifact=${{ github.event.inputs.worker_tag }}
|
||||||
selected="${targetArtifact:-$currentHash}"
|
selected="${targetArtifact:-$currentHash}"
|
||||||
|
|
||||||
mkdir worker-dist
|
mkdir worker-dist
|
||||||
gsutil cp gs://roleypoly-artifacts/backend-worker/$selected/script.js worker-dist/backend-worker.js
|
gsutil cp gs://roleypoly-artifacts/backend-worker/$selected/script.js worker-dist/backend-worker.js
|
||||||
|
|
||||||
- name: Terraform init
|
- name: Terraform init
|
||||||
working-directory: ./terraform
|
working-directory: ./terraform
|
||||||
run: |
|
run: |
|
||||||
terraform init --backend-config "prefix=${{github.event.inputs.environment}}"
|
terraform init --backend-config "prefix=${{github.event.inputs.environment}}"
|
||||||
|
|
||||||
- name: Write *.auto.tfvars.json files
|
- name: Write *.auto.tfvars.json files
|
||||||
working-directory: ./terraform
|
working-directory: ./terraform
|
||||||
run: |
|
run: |
|
||||||
echo \
|
echo \
|
||||||
'{"bot_tag": "${{github.event.inputs.bot_tag}}", "api_path_to_worker": "./worker-dist/backend-worker.js"}' \
|
'{"bot_tag": "${{github.event.inputs.bot_tag}}", "api_path_to_worker": "./worker-dist/backend-worker.js"}' \
|
||||||
| jq . \
|
| jq . \
|
||||||
| tee tags.auto.tfvars.json
|
| tee tags.auto.tfvars.json
|
||||||
|
|
||||||
echo ${SECRET_TFVARS} > secrets.auto.tfvars.json
|
echo ${SECRET_TFVARS} > secrets.auto.tfvars.json
|
||||||
env:
|
env:
|
||||||
SECRET_TFVARS: ${{ steps.secrets.outputs.secretJSON }}
|
SECRET_TFVARS: ${{ steps.secrets.outputs.secretJSON }}
|
||||||
|
|
||||||
- name: Terraform plan
|
- name: Terraform plan
|
||||||
working-directory: ./terraform
|
working-directory: ./terraform
|
||||||
run: |
|
run: |
|
||||||
terraform plan \
|
terraform plan \
|
||||||
-var-file variables/global.tfvars \
|
-var-file variables/global.tfvars \
|
||||||
-var-file variables/${{github.event.inputs.environment}}.tfvars \
|
-var-file variables/${{github.event.inputs.environment}}.tfvars \
|
||||||
-out=./deployment.tfplan
|
-out=./deployment.tfplan
|
||||||
|
|
||||||
- name: Terraform apply
|
- name: Terraform apply
|
||||||
working-directory: ./terraform
|
working-directory: ./terraform
|
||||||
run: |
|
run: |
|
||||||
terraform apply \
|
terraform apply \
|
||||||
-auto-approve \
|
-auto-approve \
|
||||||
deployment.tfplan
|
deployment.tfplan
|
||||||
|
|
||||||
- name: Yell Success at Discord
|
- name: Yell Success at Discord
|
||||||
if: success()
|
if: success()
|
||||||
run: |
|
run: |
|
||||||
DATA='{
|
DATA='{
|
||||||
"embeds": [
|
"embeds": [
|
||||||
{
|
{
|
||||||
"title": "Roleypoly Deployment Success",
|
"title": "Roleypoly Deployment Success",
|
||||||
"description": "Roleypoly was successfully deployed at '$(date)'",
|
"description": "Roleypoly was successfully deployed at '$(date)'",
|
||||||
"color": 4634182,
|
"color": 4634182,
|
||||||
"author": {
|
"author": {
|
||||||
"name": "Deployment Notification",
|
"name": "Deployment Notification",
|
||||||
"url": "https://github.com/roleypoly/roleypoly/actions/runs/${{ github.run_id }}"
|
"url": "https://github.com/roleypoly/roleypoly/actions/runs/${{ github.run_id }}"
|
||||||
},
|
},
|
||||||
"footer": {
|
"footer": {
|
||||||
"text": "GitHub Actions"
|
"text": "GitHub Actions"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}'
|
}'
|
||||||
|
|
||||||
curl -X POST -H "content-type: application/json" --data "$DATA" ${{ secrets.DEPLOYMENT_WEBHOOK_URL }}
|
curl -X POST -H "content-type: application/json" --data "$DATA" ${{ secrets.DEPLOYMENT_WEBHOOK_URL }}
|
||||||
|
|
||||||
- name: Yell Failure at Discord
|
- name: Yell Failure at Discord
|
||||||
if: failure()
|
if: failure()
|
||||||
run: |
|
run: |
|
||||||
DATA='{
|
DATA='{
|
||||||
"embeds": [
|
"embeds": [
|
||||||
{
|
{
|
||||||
"title": "Roleypoly Deployment Failed",
|
"title": "Roleypoly Deployment Failed",
|
||||||
"description": "Roleypoly failed to be deployed at '$(date)'",
|
"description": "Roleypoly failed to be deployed at '$(date)'",
|
||||||
"color": 15291219,
|
"color": 15291219,
|
||||||
"author": {
|
"author": {
|
||||||
"name": "Deployment Notification",
|
"name": "Deployment Notification",
|
||||||
"url": "https://github.com/roleypoly/roleypoly/actions/runs/${{ github.run_id }}"
|
"url": "https://github.com/roleypoly/roleypoly/actions/runs/${{ github.run_id }}"
|
||||||
},
|
},
|
||||||
"footer": {
|
"footer": {
|
||||||
"text": "GitHub Actions"
|
"text": "GitHub Actions"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}'
|
}'
|
||||||
|
|
||||||
curl -X POST -H "content-type: application/json" --data "$DATA" ${{ secrets.DEPLOYMENT_WEBHOOK_URL }}
|
curl -X POST -H "content-type: application/json" --data "$DATA" ${{ secrets.DEPLOYMENT_WEBHOOK_URL }}
|
||||||
|
|
88
.github/workflows/dev-container.yml
vendored
88
.github/workflows/dev-container.yml
vendored
|
@ -1,53 +1,53 @@
|
||||||
name: Dev Container
|
name: Dev Container
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
paths:
|
paths:
|
||||||
- hack/dockerfiles/dev-container.Dockerfile
|
- hack/dockerfiles/dev-container.Dockerfile
|
||||||
- .github/workflows/dev-container.yml
|
- .github/workflows/dev-container.yml
|
||||||
schedule:
|
schedule:
|
||||||
- cron: '0 12 * * 2' # 12 noon every tuesday
|
- cron: '0 12 * * 2' # 12 noon every tuesday
|
||||||
jobs:
|
jobs:
|
||||||
docker_build:
|
docker_build:
|
||||||
name: Docker Build & Publish
|
name: Docker Build & Publish
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@master
|
- uses: actions/checkout@master
|
||||||
|
|
||||||
- uses: actions/cache@v2.1.4
|
- uses: actions/cache@v2.1.4
|
||||||
with:
|
with:
|
||||||
path: /tmp/.buildx-cache
|
path: /tmp/.buildx-cache
|
||||||
key: ${{ runner.os }}-buildx-${{ github.sha }}
|
key: ${{ runner.os }}-buildx-${{ github.sha }}
|
||||||
restore-keys: |
|
restore-keys: |
|
||||||
${{ runner.os }}-buildx-
|
${{ runner.os }}-buildx-
|
||||||
|
|
||||||
- name: Docker meta
|
- name: Docker meta
|
||||||
id: docker_meta
|
id: docker_meta
|
||||||
uses: crazy-max/ghaction-docker-meta@v1
|
uses: crazy-max/ghaction-docker-meta@v1
|
||||||
with:
|
with:
|
||||||
images: ghcr.io/roleypoly/dev-container
|
images: ghcr.io/roleypoly/dev-container
|
||||||
tag-sha: true
|
tag-sha: true
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
id: buildx
|
id: buildx
|
||||||
uses: docker/setup-buildx-action@v1
|
uses: docker/setup-buildx-action@v1
|
||||||
with:
|
with:
|
||||||
install: true
|
install: true
|
||||||
|
|
||||||
- name: Login to GitHub Packages Docker Registry
|
- name: Login to GitHub Packages Docker Registry
|
||||||
uses: docker/login-action@v1
|
uses: docker/login-action@v1
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: roleypoly
|
username: roleypoly
|
||||||
password: ${{ secrets.GHCR_PAT }}
|
password: ${{ secrets.GHCR_PAT }}
|
||||||
|
|
||||||
- name: Build and push
|
- name: Build and push
|
||||||
uses: docker/build-push-action@v2
|
uses: docker/build-push-action@v2
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
file: ./hack/dockerfiles/dev-container.Dockerfile
|
file: ./hack/dockerfiles/dev-container.Dockerfile
|
||||||
push: true
|
push: true
|
||||||
cache-from: type=local,src=/tmp/.buildx-cache
|
cache-from: type=local,src=/tmp/.buildx-cache
|
||||||
cache-to: type=local,dest=/tmp/.buildx-cache
|
cache-to: type=local,dest=/tmp/.buildx-cache
|
||||||
tags: ${{ steps.docker_meta.outputs.tags }}
|
tags: ${{ steps.docker_meta.outputs.tags }}
|
||||||
labels: ${{ steps.docker_meta.outputs.labels }}
|
labels: ${{ steps.docker_meta.outputs.labels }}
|
||||||
|
|
|
@ -1,17 +1,9 @@
|
||||||
module.exports = {
|
module.exports = {
|
||||||
printWidth: 90,
|
printWidth: 90,
|
||||||
useTabs: false,
|
useTabs: false,
|
||||||
tabWidth: 4,
|
tabWidth: 2,
|
||||||
singleQuote: true,
|
singleQuote: true,
|
||||||
trailingComma: 'es5',
|
trailingComma: 'es5',
|
||||||
bracketSpacing: true,
|
bracketSpacing: true,
|
||||||
semi: true,
|
semi: true,
|
||||||
overrides: [
|
|
||||||
{
|
|
||||||
files: '*.md',
|
|
||||||
options: {
|
|
||||||
tabWidth: 2,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
};
|
||||||
|
|
26
.vscode/settings.json
vendored
26
.vscode/settings.json
vendored
|
@ -1,15 +1,15 @@
|
||||||
{
|
{
|
||||||
"[starlark]": {
|
"[starlark]": {
|
||||||
"editor.tabSize": 4
|
"editor.tabSize": 4
|
||||||
},
|
},
|
||||||
"bazel.buildifierFixOnFormat": true,
|
"bazel.buildifierFixOnFormat": true,
|
||||||
"editor.formatOnSave": true,
|
"editor.formatOnSave": true,
|
||||||
"editor.insertSpaces": true,
|
"editor.insertSpaces": true,
|
||||||
"editor.tabSize": 2,
|
"editor.tabSize": 2,
|
||||||
"go.inferGopath": false,
|
"go.inferGopath": false,
|
||||||
"search.exclude": {
|
"search.exclude": {
|
||||||
"**/.yarn": true,
|
"**/.yarn": true,
|
||||||
"**/.pnp.*": true
|
"**/.pnp.*": true
|
||||||
},
|
},
|
||||||
"typescript.enablePromptUseWorkspaceTsdk": true
|
"typescript.enablePromptUseWorkspaceTsdk": true
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,13 +2,13 @@
|
||||||
version: '3.8'
|
version: '3.8'
|
||||||
|
|
||||||
services:
|
services:
|
||||||
dev:
|
dev:
|
||||||
image: node:14
|
image: node:14
|
||||||
volumes:
|
volumes:
|
||||||
- '.:/src'
|
- '.:/src'
|
||||||
ports:
|
ports:
|
||||||
- 6609:6609
|
- 6609:6609
|
||||||
- 6601:6601
|
- 6601:6601
|
||||||
- 6006:6006
|
- 6006:6006
|
||||||
working_dir: /src
|
working_dir: /src
|
||||||
command: yarn start
|
command: yarn start
|
||||||
|
|
|
@ -3,8 +3,8 @@ FROM golang:1.15-alpine AS builder
|
||||||
# Create the user and group files that will be used in the running container to
|
# Create the user and group files that will be used in the running container to
|
||||||
# run the process as an unprivileged user.
|
# run the process as an unprivileged user.
|
||||||
RUN mkdir /user \
|
RUN mkdir /user \
|
||||||
&& echo 'nobody:x:65534:65534:nobody:/:' >/user/passwd \
|
&& echo 'nobody:x:65534:65534:nobody:/:' >/user/passwd \
|
||||||
&& echo 'nobody:x:65534:' >/user/group
|
&& echo 'nobody:x:65534:' >/user/group
|
||||||
|
|
||||||
# Install the Certificate-Authority certificates for the app to be able to make
|
# Install the Certificate-Authority certificates for the app to be able to make
|
||||||
# calls to HTTPS endpoints.
|
# calls to HTTPS endpoints.
|
||||||
|
@ -24,8 +24,8 @@ COPY ./ ./
|
||||||
|
|
||||||
# Build the executable to `/app`. Mark the build as statically linked.
|
# Build the executable to `/app`. Mark the build as statically linked.
|
||||||
RUN CGO_ENABLED=0 go build \
|
RUN CGO_ENABLED=0 go build \
|
||||||
-installsuffix "static" \
|
-installsuffix "static" \
|
||||||
-o /app ./src/discord-bot
|
-o /app ./src/discord-bot
|
||||||
|
|
||||||
# Final stage: the running container.
|
# Final stage: the running container.
|
||||||
FROM scratch AS final
|
FROM scratch AS final
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
module.exports = {
|
module.exports = {
|
||||||
preset: 'ts-jest/presets/js-with-babel',
|
preset: 'ts-jest/presets/js-with-babel',
|
||||||
testEnvironment: 'enzyme',
|
testEnvironment: 'enzyme',
|
||||||
reporters: ['default'],
|
reporters: ['default'],
|
||||||
setupFilesAfterEnv: ['jest-enzyme', 'jest-styled-components', './hack/jestSetup.ts'],
|
setupFilesAfterEnv: ['jest-enzyme', 'jest-styled-components', './hack/jestSetup.ts'],
|
||||||
snapshotSerializers: ['enzyme-to-json/serializer'],
|
snapshotSerializers: ['enzyme-to-json/serializer'],
|
||||||
globals: {
|
globals: {
|
||||||
'ts-jest': {
|
'ts-jest': {
|
||||||
tsconfig: './tsconfig.test.json',
|
tsconfig: './tsconfig.test.json',
|
||||||
},
|
|
||||||
},
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
154
package.json
154
package.json
|
@ -1,80 +1,80 @@
|
||||||
{
|
{
|
||||||
"name": "roleypoly",
|
"name": "roleypoly",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"description": "https://roleypoly.com",
|
"description": "https://roleypoly.com",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "git+https://github.com/roleypoly/roleypoly.git"
|
"url": "git+https://github.com/roleypoly/roleypoly.git"
|
||||||
},
|
},
|
||||||
"homepage": "https://github.com/roleypoly/roleypoly#readme",
|
"homepage": "https://github.com/roleypoly/roleypoly#readme",
|
||||||
"bugs": {
|
"bugs": {
|
||||||
"url": "https://github.com/roleypoly/roleypoly/issues"
|
"url": "https://github.com/roleypoly/roleypoly/issues"
|
||||||
},
|
},
|
||||||
"author": "Katalina Okano <git@kat.cafe>",
|
"author": "Katalina Okano <git@kat.cafe>",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"private": true,
|
"private": true,
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
"packages/*"
|
"packages/*"
|
||||||
|
],
|
||||||
|
"scripts": {
|
||||||
|
"build": "run-p -c build:*",
|
||||||
|
"build:design-system": "yarn workspace @roleypoly/design-system run build",
|
||||||
|
"build:web": "yarn workspace @roleypoly/web run build",
|
||||||
|
"lint": "run-p -c lint:* --",
|
||||||
|
"lint:eslint": "eslint",
|
||||||
|
"lint:go": "go fmt ./...",
|
||||||
|
"lint:prettier": "cross-env prettier -c '**/*.{ts,tsx,css,yml,yaml,md,json,js,jsx,sh,gitignore,mdx,Dockerfile}'",
|
||||||
|
"lint:stylelint": "cross-env stylelint '**/*.{ts,tsx}'",
|
||||||
|
"lint:terraform": "terraform fmt -recursive -check ./terraform",
|
||||||
|
"lint:types": "tsc --noEmit",
|
||||||
|
"lint:types-api": "yarn workspace @roleypoly/api run lint:types",
|
||||||
|
"postinstall": "is-ci || husky install",
|
||||||
|
"start": "run-p -c start:*",
|
||||||
|
"start:design-system": "yarn workspace @roleypoly/design-system start",
|
||||||
|
"start:web": "yarn workspace @roleypoly/web start",
|
||||||
|
"start:worker": "yarn workspace @roleypoly/api start",
|
||||||
|
"test": "jest"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/enzyme": "^3.10.8",
|
||||||
|
"@types/lodash": "^4.14.168",
|
||||||
|
"@wojtekmaj/enzyme-adapter-react-17": "^0.4.1",
|
||||||
|
"enzyme": "^3.11.0",
|
||||||
|
"enzyme-adapter-react-16": "^1.15.6",
|
||||||
|
"husky": "^5.1.3",
|
||||||
|
"is-ci": "^3.0.0",
|
||||||
|
"jest-enzyme": "^7.1.2",
|
||||||
|
"jest-react-hooks-shallow": "^1.5.1",
|
||||||
|
"jest-styled-components": "^7.0.3",
|
||||||
|
"lint-staged": "^10.5.4",
|
||||||
|
"npm-run-all": "^4.1.5",
|
||||||
|
"prettier": "^2.2.1",
|
||||||
|
"prettier-plugin-organize-imports": "^1.1.1",
|
||||||
|
"prettier-plugin-pkg": "^0.8.0",
|
||||||
|
"prettier-plugin-sh": "^0.6.0",
|
||||||
|
"stylelint": "^13.12.0",
|
||||||
|
"stylelint-config-prettier": "^8.0.2",
|
||||||
|
"stylelint-config-standard": "^21.0.0",
|
||||||
|
"stylelint-config-styled-components": "^0.1.1",
|
||||||
|
"stylelint-prettier": "^1.2.0",
|
||||||
|
"ts-jest": "^26.5.3",
|
||||||
|
"typescript": "^4.2.3"
|
||||||
|
},
|
||||||
|
"lint-staged": {
|
||||||
|
"*.{ts,tsx,js,jsx}": [
|
||||||
|
"prettier --write"
|
||||||
],
|
],
|
||||||
"scripts": {
|
"*.go": [
|
||||||
"build": "run-p -c build:*",
|
"go fmt"
|
||||||
"build:design-system": "yarn workspace @roleypoly/design-system run build",
|
],
|
||||||
"build:web": "yarn workspace @roleypoly/web run build",
|
"*.{json,Dockerfile,sh,md,env,mdx,yml,html}": [
|
||||||
"lint": "run-p -c lint:* --",
|
"prettier --write"
|
||||||
"lint:eslint": "eslint",
|
],
|
||||||
"lint:go": "go fmt ./...",
|
".*/*.{json,Dockerfile,sh,md,env,mdx,yml,html}": [
|
||||||
"lint:prettier": "cross-env prettier -c '**/*.{ts,tsx,css,yml,yaml,md,json,js,jsx,sh,gitignore,mdx,Dockerfile}'",
|
"prettier --write"
|
||||||
"lint:stylelint": "cross-env stylelint '**/*.{ts,tsx}'",
|
],
|
||||||
"lint:terraform": "terraform fmt -recursive -check ./terraform",
|
".husky/pre-commit": [
|
||||||
"lint:types": "tsc --noEmit",
|
"prettier --write"
|
||||||
"lint:types-api": "yarn workspace @roleypoly/api run lint:types",
|
]
|
||||||
"postinstall": "is-ci || husky install",
|
}
|
||||||
"start": "run-p -c start:*",
|
|
||||||
"start:design-system": "yarn workspace @roleypoly/design-system start",
|
|
||||||
"start:web": "yarn workspace @roleypoly/web start",
|
|
||||||
"start:worker": "yarn workspace @roleypoly/api start",
|
|
||||||
"test": "jest"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@types/enzyme": "^3.10.8",
|
|
||||||
"@types/lodash": "^4.14.168",
|
|
||||||
"@wojtekmaj/enzyme-adapter-react-17": "^0.4.1",
|
|
||||||
"enzyme": "^3.11.0",
|
|
||||||
"enzyme-adapter-react-16": "^1.15.6",
|
|
||||||
"husky": "^5.1.3",
|
|
||||||
"is-ci": "^3.0.0",
|
|
||||||
"jest-enzyme": "^7.1.2",
|
|
||||||
"jest-react-hooks-shallow": "^1.5.1",
|
|
||||||
"jest-styled-components": "^7.0.3",
|
|
||||||
"lint-staged": "^10.5.4",
|
|
||||||
"npm-run-all": "^4.1.5",
|
|
||||||
"prettier": "^2.2.1",
|
|
||||||
"prettier-plugin-organize-imports": "^1.1.1",
|
|
||||||
"prettier-plugin-pkg": "^0.8.0",
|
|
||||||
"prettier-plugin-sh": "^0.6.0",
|
|
||||||
"stylelint": "^13.12.0",
|
|
||||||
"stylelint-config-prettier": "^8.0.2",
|
|
||||||
"stylelint-config-standard": "^21.0.0",
|
|
||||||
"stylelint-config-styled-components": "^0.1.1",
|
|
||||||
"stylelint-prettier": "^1.2.0",
|
|
||||||
"ts-jest": "^26.5.3",
|
|
||||||
"typescript": "^4.2.3"
|
|
||||||
},
|
|
||||||
"lint-staged": {
|
|
||||||
"*.{ts,tsx,js,jsx}": [
|
|
||||||
"prettier --write"
|
|
||||||
],
|
|
||||||
"*.go": [
|
|
||||||
"go fmt"
|
|
||||||
],
|
|
||||||
"*.{json,Dockerfile,sh,md,env,mdx,yml,html}": [
|
|
||||||
"prettier --write"
|
|
||||||
],
|
|
||||||
".*/*.{json,Dockerfile,sh,md,env,mdx,yml,html}": [
|
|
||||||
"prettier --write"
|
|
||||||
],
|
|
||||||
".husky/pre-commit": [
|
|
||||||
"prettier --write"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
18
packages/api/bindings.d.ts
vendored
18
packages/api/bindings.d.ts
vendored
|
@ -1,14 +1,14 @@
|
||||||
export {};
|
export {};
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
const BOT_CLIENT_ID: string;
|
const BOT_CLIENT_ID: string;
|
||||||
const BOT_CLIENT_SECRET: string;
|
const BOT_CLIENT_SECRET: string;
|
||||||
const UI_PUBLIC_URI: string;
|
const UI_PUBLIC_URI: string;
|
||||||
const API_PUBLIC_URI: string;
|
const API_PUBLIC_URI: string;
|
||||||
const ROOT_USERS: string;
|
const ROOT_USERS: string;
|
||||||
const ALLOWED_CALLBACK_HOSTS: string;
|
const ALLOWED_CALLBACK_HOSTS: string;
|
||||||
|
|
||||||
const KV_SESSIONS: KVNamespace;
|
const KV_SESSIONS: KVNamespace;
|
||||||
const KV_GUILDS: KVNamespace;
|
const KV_GUILDS: KVNamespace;
|
||||||
const KV_GUILD_DATA: KVNamespace;
|
const KV_GUILD_DATA: KVNamespace;
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,33 +4,33 @@ import { botClientID } from '../utils/config';
|
||||||
const validGuildID = /^[0-9]+$/;
|
const validGuildID = /^[0-9]+$/;
|
||||||
|
|
||||||
type URLParams = {
|
type URLParams = {
|
||||||
clientID: string;
|
clientID: string;
|
||||||
permissions: number;
|
permissions: number;
|
||||||
guildID?: string;
|
guildID?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const buildURL = (params: URLParams) => {
|
const buildURL = (params: URLParams) => {
|
||||||
let url = `https://discord.com/api/oauth2/authorize?client_id=${params.clientID}&scope=bot&permissions=${params.permissions}`;
|
let url = `https://discord.com/api/oauth2/authorize?client_id=${params.clientID}&scope=bot&permissions=${params.permissions}`;
|
||||||
|
|
||||||
if (params.guildID) {
|
if (params.guildID) {
|
||||||
url += `&guild_id=${params.guildID}&disable_guild_select=true`;
|
url += `&guild_id=${params.guildID}&disable_guild_select=true`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return url;
|
return url;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const BotJoin = (request: Request): Response => {
|
export const BotJoin = (request: Request): Response => {
|
||||||
let guildID = new URL(request.url).searchParams.get('guild') || '';
|
let guildID = new URL(request.url).searchParams.get('guild') || '';
|
||||||
|
|
||||||
if (guildID && !validGuildID.test(guildID)) {
|
if (guildID && !validGuildID.test(guildID)) {
|
||||||
guildID = '';
|
guildID = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
return Bounce(
|
return Bounce(
|
||||||
buildURL({
|
buildURL({
|
||||||
clientID: botClientID,
|
clientID: botClientID,
|
||||||
permissions: 268435456,
|
permissions: 268435456,
|
||||||
guildID,
|
guildID,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -5,90 +5,78 @@ import { GuildData } from '../utils/kv';
|
||||||
|
|
||||||
// Temporary use.
|
// Temporary use.
|
||||||
export const CreateRoleypolyData = onlyRootUsers(
|
export const CreateRoleypolyData = onlyRootUsers(
|
||||||
async (request: Request): Promise<Response> => {
|
async (request: Request): Promise<Response> => {
|
||||||
const data: GuildDataT = {
|
const data: GuildDataT = {
|
||||||
id: '386659935687147521',
|
id: '386659935687147521',
|
||||||
message:
|
message:
|
||||||
'Hey, this is kind of a demo setup so features/use cases can be shown off.\n\nThanks for using Roleypoly <3',
|
'Hey, this is kind of a demo setup so features/use cases can be shown off.\n\nThanks for using Roleypoly <3',
|
||||||
features: Features.Preview,
|
features: Features.Preview,
|
||||||
categories: [
|
categories: [
|
||||||
{
|
{
|
||||||
id: KSUID.randomSync().string,
|
id: KSUID.randomSync().string,
|
||||||
name: 'Demo Roles',
|
name: 'Demo Roles',
|
||||||
type: CategoryType.Multi,
|
type: CategoryType.Multi,
|
||||||
hidden: false,
|
hidden: false,
|
||||||
position: 0,
|
position: 0,
|
||||||
roles: [
|
roles: [
|
||||||
'557825026406088717',
|
'557825026406088717',
|
||||||
'557824994269200384',
|
'557824994269200384',
|
||||||
'557824893241131029',
|
'557824893241131029',
|
||||||
'557812915386843170',
|
'557812915386843170',
|
||||||
'557812901717737472',
|
'557812901717737472',
|
||||||
'557812805546541066',
|
'557812805546541066',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: KSUID.randomSync().string,
|
id: KSUID.randomSync().string,
|
||||||
name: 'Colors',
|
name: 'Colors',
|
||||||
type: CategoryType.Single,
|
type: CategoryType.Single,
|
||||||
hidden: false,
|
hidden: false,
|
||||||
position: 1,
|
position: 1,
|
||||||
roles: [
|
roles: ['394060232893923349', '394060145799331851', '394060192846839809'],
|
||||||
'394060232893923349',
|
},
|
||||||
'394060145799331851',
|
{
|
||||||
'394060192846839809',
|
id: KSUID.randomSync().string,
|
||||||
],
|
name: 'Test Roles',
|
||||||
},
|
type: CategoryType.Multi,
|
||||||
{
|
hidden: false,
|
||||||
id: KSUID.randomSync().string,
|
position: 5,
|
||||||
name: 'Test Roles',
|
roles: ['558104828216213505', '558103534453653514', '558297233582194728'],
|
||||||
type: CategoryType.Multi,
|
},
|
||||||
hidden: false,
|
{
|
||||||
position: 5,
|
id: KSUID.randomSync().string,
|
||||||
roles: [
|
name: 'Region',
|
||||||
'558104828216213505',
|
type: CategoryType.Multi,
|
||||||
'558103534453653514',
|
hidden: false,
|
||||||
'558297233582194728',
|
position: 3,
|
||||||
],
|
roles: [
|
||||||
},
|
'397296181803483136',
|
||||||
{
|
'397296137066774529',
|
||||||
id: KSUID.randomSync().string,
|
'397296218809827329',
|
||||||
name: 'Region',
|
'397296267283267605',
|
||||||
type: CategoryType.Multi,
|
],
|
||||||
hidden: false,
|
},
|
||||||
position: 3,
|
{
|
||||||
roles: [
|
id: KSUID.randomSync().string,
|
||||||
'397296181803483136',
|
name: 'Opt-in Channels',
|
||||||
'397296137066774529',
|
type: CategoryType.Multi,
|
||||||
'397296218809827329',
|
hidden: false,
|
||||||
'397296267283267605',
|
position: 4,
|
||||||
],
|
roles: ['414514823959674890', '764230661904007219'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: KSUID.randomSync().string,
|
id: KSUID.randomSync().string,
|
||||||
name: 'Opt-in Channels',
|
name: 'Pronouns',
|
||||||
type: CategoryType.Multi,
|
type: CategoryType.Multi,
|
||||||
hidden: false,
|
hidden: false,
|
||||||
position: 4,
|
position: 2,
|
||||||
roles: ['414514823959674890', '764230661904007219'],
|
roles: ['485916566790340608', '485916566941335583', '485916566311927808'],
|
||||||
},
|
},
|
||||||
{
|
],
|
||||||
id: KSUID.randomSync().string,
|
};
|
||||||
name: 'Pronouns',
|
|
||||||
type: CategoryType.Multi,
|
|
||||||
hidden: false,
|
|
||||||
position: 2,
|
|
||||||
roles: [
|
|
||||||
'485916566790340608',
|
|
||||||
'485916566941335583',
|
|
||||||
'485916566311927808',
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
await GuildData.put(data.id, data);
|
await GuildData.put(data.id, data);
|
||||||
|
|
||||||
return respond({ ok: true });
|
return respond({ ok: true });
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
|
@ -5,52 +5,52 @@ import { getGuild, getGuildData, getGuildMemberRoles } from '../utils/guild';
|
||||||
const fail = () => respond({ error: 'guild not found' }, { status: 404 });
|
const fail = () => respond({ error: 'guild not found' }, { status: 404 });
|
||||||
|
|
||||||
export const GetPickerData = withSession(
|
export const GetPickerData = withSession(
|
||||||
(session: SessionData) => async (request: Request): Promise<Response> => {
|
(session: SessionData) => async (request: Request): Promise<Response> => {
|
||||||
const url = new URL(request.url);
|
const url = new URL(request.url);
|
||||||
const [, , guildID] = url.pathname.split('/');
|
const [, , guildID] = url.pathname.split('/');
|
||||||
|
|
||||||
if (!guildID) {
|
if (!guildID) {
|
||||||
return respond({ error: 'missing guild id' }, { status: 400 });
|
return respond({ error: 'missing guild id' }, { status: 400 });
|
||||||
}
|
|
||||||
|
|
||||||
const { id: userID } = session.user as DiscordUser;
|
|
||||||
const guilds = session.guilds as GuildSlug[];
|
|
||||||
|
|
||||||
// Save a Discord API request by checking if this user is a member by session first
|
|
||||||
const checkGuild = guilds.find((guild) => guild.id === guildID);
|
|
||||||
if (!checkGuild) {
|
|
||||||
return fail();
|
|
||||||
}
|
|
||||||
|
|
||||||
const guild = await getGuild(guildID, {
|
|
||||||
skipCachePull: url.searchParams.has('__no_cache'),
|
|
||||||
});
|
|
||||||
if (!guild) {
|
|
||||||
return fail();
|
|
||||||
}
|
|
||||||
|
|
||||||
const memberRolesP = getGuildMemberRoles({
|
|
||||||
serverID: guildID,
|
|
||||||
userID,
|
|
||||||
});
|
|
||||||
|
|
||||||
const guildDataP = getGuildData(guildID);
|
|
||||||
|
|
||||||
const [guildData, memberRoles] = await Promise.all([guildDataP, memberRolesP]);
|
|
||||||
if (!memberRoles) {
|
|
||||||
return fail();
|
|
||||||
}
|
|
||||||
|
|
||||||
const presentableGuild: PresentableGuild = {
|
|
||||||
id: guildID,
|
|
||||||
guild: checkGuild,
|
|
||||||
roles: guild.roles,
|
|
||||||
member: {
|
|
||||||
roles: memberRoles,
|
|
||||||
},
|
|
||||||
data: guildData,
|
|
||||||
};
|
|
||||||
|
|
||||||
return respond(presentableGuild);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { id: userID } = session.user as DiscordUser;
|
||||||
|
const guilds = session.guilds as GuildSlug[];
|
||||||
|
|
||||||
|
// Save a Discord API request by checking if this user is a member by session first
|
||||||
|
const checkGuild = guilds.find((guild) => guild.id === guildID);
|
||||||
|
if (!checkGuild) {
|
||||||
|
return fail();
|
||||||
|
}
|
||||||
|
|
||||||
|
const guild = await getGuild(guildID, {
|
||||||
|
skipCachePull: url.searchParams.has('__no_cache'),
|
||||||
|
});
|
||||||
|
if (!guild) {
|
||||||
|
return fail();
|
||||||
|
}
|
||||||
|
|
||||||
|
const memberRolesP = getGuildMemberRoles({
|
||||||
|
serverID: guildID,
|
||||||
|
userID,
|
||||||
|
});
|
||||||
|
|
||||||
|
const guildDataP = getGuildData(guildID);
|
||||||
|
|
||||||
|
const [guildData, memberRoles] = await Promise.all([guildDataP, memberRolesP]);
|
||||||
|
if (!memberRoles) {
|
||||||
|
return fail();
|
||||||
|
}
|
||||||
|
|
||||||
|
const presentableGuild: PresentableGuild = {
|
||||||
|
id: guildID,
|
||||||
|
guild: checkGuild,
|
||||||
|
roles: guild.roles,
|
||||||
|
member: {
|
||||||
|
roles: memberRoles,
|
||||||
|
},
|
||||||
|
data: guildData,
|
||||||
|
};
|
||||||
|
|
||||||
|
return respond(presentableGuild);
|
||||||
|
}
|
||||||
);
|
);
|
||||||
|
|
|
@ -2,11 +2,11 @@ import { SessionData } from '@roleypoly/types';
|
||||||
import { respond, withSession } from '../utils/api-tools';
|
import { respond, withSession } from '../utils/api-tools';
|
||||||
|
|
||||||
export const GetSession = withSession((session?: SessionData) => (): Response => {
|
export const GetSession = withSession((session?: SessionData) => (): Response => {
|
||||||
const { user, guilds, sessionID } = session || {};
|
const { user, guilds, sessionID } = session || {};
|
||||||
|
|
||||||
return respond({
|
return respond({
|
||||||
user,
|
user,
|
||||||
guilds,
|
guilds,
|
||||||
sessionID,
|
sessionID,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -3,38 +3,38 @@ import { respond } from '../utils/api-tools';
|
||||||
import { getGuild } from '../utils/guild';
|
import { getGuild } from '../utils/guild';
|
||||||
|
|
||||||
export const GetSlug = async (request: Request): Promise<Response> => {
|
export const GetSlug = async (request: Request): Promise<Response> => {
|
||||||
const reqURL = new URL(request.url);
|
const reqURL = new URL(request.url);
|
||||||
const [, , serverID] = reqURL.pathname.split('/');
|
const [, , serverID] = reqURL.pathname.split('/');
|
||||||
|
|
||||||
if (!serverID) {
|
if (!serverID) {
|
||||||
return respond(
|
return respond(
|
||||||
{
|
{
|
||||||
error: 'missing server ID',
|
error: 'missing server ID',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
status: 400,
|
status: 400,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const guild = await getGuild(serverID);
|
const guild = await getGuild(serverID);
|
||||||
if (!guild) {
|
if (!guild) {
|
||||||
return respond(
|
return respond(
|
||||||
{
|
{
|
||||||
error: 'guild not found',
|
error: 'guild not found',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
status: 404,
|
status: 404,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { id, name, icon } = guild;
|
const { id, name, icon } = guild;
|
||||||
const guildSlug: GuildSlug = {
|
const guildSlug: GuildSlug = {
|
||||||
id,
|
id,
|
||||||
name,
|
name,
|
||||||
icon,
|
icon,
|
||||||
permissionLevel: 0,
|
permissionLevel: 0,
|
||||||
};
|
};
|
||||||
return respond(guildSlug);
|
return respond(guildSlug);
|
||||||
};
|
};
|
||||||
|
|
|
@ -4,30 +4,30 @@ import { Bounce } from '../utils/bounce';
|
||||||
import { apiPublicURI, botClientID } from '../utils/config';
|
import { apiPublicURI, botClientID } from '../utils/config';
|
||||||
|
|
||||||
type URLParams = {
|
type URLParams = {
|
||||||
clientID: string;
|
clientID: string;
|
||||||
redirectURI: string;
|
redirectURI: string;
|
||||||
state: string;
|
state: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const buildURL = (params: URLParams) =>
|
const buildURL = (params: URLParams) =>
|
||||||
`https://discord.com/api/oauth2/authorize?client_id=${
|
`https://discord.com/api/oauth2/authorize?client_id=${
|
||||||
params.clientID
|
params.clientID
|
||||||
}&response_type=code&scope=identify%20guilds&redirect_uri=${encodeURIComponent(
|
}&response_type=code&scope=identify%20guilds&redirect_uri=${encodeURIComponent(
|
||||||
params.redirectURI
|
params.redirectURI
|
||||||
)}&state=${params.state}`;
|
)}&state=${params.state}`;
|
||||||
|
|
||||||
export const LoginBounce = async (request: Request): Promise<Response> => {
|
export const LoginBounce = async (request: Request): Promise<Response> => {
|
||||||
const stateSessionData: StateSession = {};
|
const stateSessionData: StateSession = {};
|
||||||
|
|
||||||
const { cbh: callbackHost } = getQuery(request);
|
const { cbh: callbackHost } = getQuery(request);
|
||||||
if (callbackHost && isAllowedCallbackHost(callbackHost)) {
|
if (callbackHost && isAllowedCallbackHost(callbackHost)) {
|
||||||
stateSessionData.callbackHost = callbackHost;
|
stateSessionData.callbackHost = callbackHost;
|
||||||
}
|
}
|
||||||
|
|
||||||
const state = await setupStateSession(stateSessionData);
|
const state = await setupStateSession(stateSessionData);
|
||||||
|
|
||||||
const redirectURI = `${apiPublicURI}/login-callback`;
|
const redirectURI = `${apiPublicURI}/login-callback`;
|
||||||
const clientID = botClientID;
|
const clientID = botClientID;
|
||||||
|
|
||||||
return Bounce(buildURL({ state, redirectURI, clientID }));
|
return Bounce(buildURL({ state, redirectURI, clientID }));
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,159 +1,159 @@
|
||||||
import {
|
import {
|
||||||
AuthTokenResponse,
|
AuthTokenResponse,
|
||||||
DiscordUser,
|
DiscordUser,
|
||||||
GuildSlug,
|
GuildSlug,
|
||||||
SessionData,
|
SessionData,
|
||||||
StateSession,
|
StateSession,
|
||||||
} from '@roleypoly/types';
|
} from '@roleypoly/types';
|
||||||
import KSUID from 'ksuid';
|
import KSUID from 'ksuid';
|
||||||
import {
|
import {
|
||||||
AuthType,
|
AuthType,
|
||||||
discordFetch,
|
discordFetch,
|
||||||
formData,
|
formData,
|
||||||
getStateSession,
|
getStateSession,
|
||||||
isAllowedCallbackHost,
|
isAllowedCallbackHost,
|
||||||
parsePermissions,
|
parsePermissions,
|
||||||
resolveFailures,
|
resolveFailures,
|
||||||
userAgent,
|
userAgent,
|
||||||
} from '../utils/api-tools';
|
} from '../utils/api-tools';
|
||||||
import { Bounce } from '../utils/bounce';
|
import { Bounce } from '../utils/bounce';
|
||||||
import { apiPublicURI, botClientID, botClientSecret, uiPublicURI } from '../utils/config';
|
import { apiPublicURI, botClientID, botClientSecret, uiPublicURI } from '../utils/config';
|
||||||
import { Sessions } from '../utils/kv';
|
import { Sessions } from '../utils/kv';
|
||||||
|
|
||||||
const AuthErrorResponse = (extra?: string) =>
|
const AuthErrorResponse = (extra?: string) =>
|
||||||
Bounce(
|
Bounce(
|
||||||
uiPublicURI +
|
uiPublicURI +
|
||||||
`/machinery/error?error_code=authFailure${extra ? `&extra=${extra}` : ''}`
|
`/machinery/error?error_code=authFailure${extra ? `&extra=${extra}` : ''}`
|
||||||
);
|
);
|
||||||
|
|
||||||
export const LoginCallback = resolveFailures(
|
export const LoginCallback = resolveFailures(
|
||||||
AuthErrorResponse,
|
AuthErrorResponse,
|
||||||
async (request: Request): Promise<Response> => {
|
async (request: Request): Promise<Response> => {
|
||||||
let bounceBaseUrl = uiPublicURI;
|
let bounceBaseUrl = uiPublicURI;
|
||||||
|
|
||||||
const query = new URL(request.url).searchParams;
|
const query = new URL(request.url).searchParams;
|
||||||
const stateValue = query.get('state');
|
const stateValue = query.get('state');
|
||||||
|
|
||||||
if (stateValue === null) {
|
if (stateValue === null) {
|
||||||
return AuthErrorResponse('state missing');
|
return AuthErrorResponse('state missing');
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const state = KSUID.parse(stateValue);
|
|
||||||
const stateExpiry = state.date.getTime() + 1000 * 60 * 5;
|
|
||||||
const currentTime = Date.now();
|
|
||||||
|
|
||||||
if (currentTime > stateExpiry) {
|
|
||||||
return AuthErrorResponse('state expired');
|
|
||||||
}
|
|
||||||
|
|
||||||
const stateSession = await getStateSession<StateSession>(state.string);
|
|
||||||
if (
|
|
||||||
stateSession?.callbackHost &&
|
|
||||||
isAllowedCallbackHost(stateSession.callbackHost)
|
|
||||||
) {
|
|
||||||
bounceBaseUrl = stateSession.callbackHost;
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
return AuthErrorResponse('state invalid');
|
|
||||||
}
|
|
||||||
|
|
||||||
const code = query.get('code');
|
|
||||||
if (!code) {
|
|
||||||
return AuthErrorResponse('code missing');
|
|
||||||
}
|
|
||||||
|
|
||||||
const tokenRequest = {
|
|
||||||
client_id: botClientID,
|
|
||||||
client_secret: botClientSecret,
|
|
||||||
grant_type: 'authorization_code',
|
|
||||||
redirect_uri: apiPublicURI + '/login-callback',
|
|
||||||
scope: 'identify guilds',
|
|
||||||
code,
|
|
||||||
};
|
|
||||||
|
|
||||||
const tokenFetch = await fetch('https://discord.com/api/v8/oauth2/token', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'content-type': 'application/x-www-form-urlencoded',
|
|
||||||
'user-agent': userAgent,
|
|
||||||
},
|
|
||||||
body: formData(tokenRequest),
|
|
||||||
});
|
|
||||||
|
|
||||||
const tokens = (await tokenFetch.json()) as AuthTokenResponse;
|
|
||||||
|
|
||||||
if (!tokens.access_token) {
|
|
||||||
return AuthErrorResponse('token response invalid');
|
|
||||||
}
|
|
||||||
|
|
||||||
const [sessionID, user, guilds] = await Promise.all([
|
|
||||||
KSUID.random(),
|
|
||||||
getUser(tokens.access_token),
|
|
||||||
getGuilds(tokens.access_token),
|
|
||||||
]);
|
|
||||||
|
|
||||||
if (!user) {
|
|
||||||
return AuthErrorResponse('failed to fetch user');
|
|
||||||
}
|
|
||||||
|
|
||||||
const sessionData: SessionData = {
|
|
||||||
tokens,
|
|
||||||
sessionID: sessionID.string,
|
|
||||||
user,
|
|
||||||
guilds,
|
|
||||||
};
|
|
||||||
|
|
||||||
await Sessions.put(sessionID.string, sessionData, 60 * 60 * 6);
|
|
||||||
|
|
||||||
return Bounce(
|
|
||||||
bounceBaseUrl + '/machinery/new-session?session_id=' + sessionID.string
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const state = KSUID.parse(stateValue);
|
||||||
|
const stateExpiry = state.date.getTime() + 1000 * 60 * 5;
|
||||||
|
const currentTime = Date.now();
|
||||||
|
|
||||||
|
if (currentTime > stateExpiry) {
|
||||||
|
return AuthErrorResponse('state expired');
|
||||||
|
}
|
||||||
|
|
||||||
|
const stateSession = await getStateSession<StateSession>(state.string);
|
||||||
|
if (
|
||||||
|
stateSession?.callbackHost &&
|
||||||
|
isAllowedCallbackHost(stateSession.callbackHost)
|
||||||
|
) {
|
||||||
|
bounceBaseUrl = stateSession.callbackHost;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
return AuthErrorResponse('state invalid');
|
||||||
|
}
|
||||||
|
|
||||||
|
const code = query.get('code');
|
||||||
|
if (!code) {
|
||||||
|
return AuthErrorResponse('code missing');
|
||||||
|
}
|
||||||
|
|
||||||
|
const tokenRequest = {
|
||||||
|
client_id: botClientID,
|
||||||
|
client_secret: botClientSecret,
|
||||||
|
grant_type: 'authorization_code',
|
||||||
|
redirect_uri: apiPublicURI + '/login-callback',
|
||||||
|
scope: 'identify guilds',
|
||||||
|
code,
|
||||||
|
};
|
||||||
|
|
||||||
|
const tokenFetch = await fetch('https://discord.com/api/v8/oauth2/token', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'content-type': 'application/x-www-form-urlencoded',
|
||||||
|
'user-agent': userAgent,
|
||||||
|
},
|
||||||
|
body: formData(tokenRequest),
|
||||||
|
});
|
||||||
|
|
||||||
|
const tokens = (await tokenFetch.json()) as AuthTokenResponse;
|
||||||
|
|
||||||
|
if (!tokens.access_token) {
|
||||||
|
return AuthErrorResponse('token response invalid');
|
||||||
|
}
|
||||||
|
|
||||||
|
const [sessionID, user, guilds] = await Promise.all([
|
||||||
|
KSUID.random(),
|
||||||
|
getUser(tokens.access_token),
|
||||||
|
getGuilds(tokens.access_token),
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return AuthErrorResponse('failed to fetch user');
|
||||||
|
}
|
||||||
|
|
||||||
|
const sessionData: SessionData = {
|
||||||
|
tokens,
|
||||||
|
sessionID: sessionID.string,
|
||||||
|
user,
|
||||||
|
guilds,
|
||||||
|
};
|
||||||
|
|
||||||
|
await Sessions.put(sessionID.string, sessionData, 60 * 60 * 6);
|
||||||
|
|
||||||
|
return Bounce(
|
||||||
|
bounceBaseUrl + '/machinery/new-session?session_id=' + sessionID.string
|
||||||
|
);
|
||||||
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const getUser = async (accessToken: string): Promise<DiscordUser | null> => {
|
const getUser = async (accessToken: string): Promise<DiscordUser | null> => {
|
||||||
const user = await discordFetch<DiscordUser>(
|
const user = await discordFetch<DiscordUser>(
|
||||||
'/users/@me',
|
'/users/@me',
|
||||||
accessToken,
|
accessToken,
|
||||||
AuthType.Bearer
|
AuthType.Bearer
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { id, username, discriminator, bot, avatar } = user;
|
const { id, username, discriminator, bot, avatar } = user;
|
||||||
|
|
||||||
return { id, username, discriminator, bot, avatar };
|
return { id, username, discriminator, bot, avatar };
|
||||||
};
|
};
|
||||||
|
|
||||||
type UserGuildsPayload = {
|
type UserGuildsPayload = {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
icon: string;
|
icon: string;
|
||||||
owner: boolean;
|
owner: boolean;
|
||||||
permissions: number;
|
permissions: number;
|
||||||
features: string[];
|
features: string[];
|
||||||
}[];
|
}[];
|
||||||
|
|
||||||
const getGuilds = async (accessToken: string) => {
|
const getGuilds = async (accessToken: string) => {
|
||||||
const guilds = await discordFetch<UserGuildsPayload>(
|
const guilds = await discordFetch<UserGuildsPayload>(
|
||||||
'/users/@me/guilds',
|
'/users/@me/guilds',
|
||||||
accessToken,
|
accessToken,
|
||||||
AuthType.Bearer
|
AuthType.Bearer
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!guilds) {
|
if (!guilds) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
const guildSlugs = guilds.map<GuildSlug>((guild) => ({
|
const guildSlugs = guilds.map<GuildSlug>((guild) => ({
|
||||||
id: guild.id,
|
id: guild.id,
|
||||||
name: guild.name,
|
name: guild.name,
|
||||||
icon: guild.icon,
|
icon: guild.icon,
|
||||||
permissionLevel: parsePermissions(BigInt(guild.permissions), guild.owner),
|
permissionLevel: parsePermissions(BigInt(guild.permissions), guild.owner),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
return guildSlugs;
|
return guildSlugs;
|
||||||
};
|
};
|
||||||
|
|
|
@ -4,24 +4,24 @@ import { botClientID, botClientSecret } from '../utils/config';
|
||||||
import { Sessions } from '../utils/kv';
|
import { Sessions } from '../utils/kv';
|
||||||
|
|
||||||
export const RevokeSession = withSession(
|
export const RevokeSession = withSession(
|
||||||
(session: SessionData) => async (request: Request) => {
|
(session: SessionData) => async (request: Request) => {
|
||||||
const tokenRequest = {
|
const tokenRequest = {
|
||||||
token: session.tokens.access_token,
|
token: session.tokens.access_token,
|
||||||
client_id: botClientID,
|
client_id: botClientID,
|
||||||
client_secret: botClientSecret,
|
client_secret: botClientSecret,
|
||||||
};
|
};
|
||||||
|
|
||||||
await fetch('https://discord.com/api/v8/oauth2/token/revoke', {
|
await fetch('https://discord.com/api/v8/oauth2/token/revoke', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'content-type': 'application/x-www-form-urlencoded',
|
'content-type': 'application/x-www-form-urlencoded',
|
||||||
'user-agent': userAgent,
|
'user-agent': userAgent,
|
||||||
},
|
},
|
||||||
body: formData(tokenRequest),
|
body: formData(tokenRequest),
|
||||||
});
|
});
|
||||||
|
|
||||||
await Sessions.delete(session.sessionID);
|
await Sessions.delete(session.sessionID);
|
||||||
|
|
||||||
return respond({ ok: true });
|
return respond({ ok: true });
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,141 +1,135 @@
|
||||||
import {
|
import {
|
||||||
GuildData,
|
GuildData,
|
||||||
Member,
|
Member,
|
||||||
Role,
|
Role,
|
||||||
RoleSafety,
|
RoleSafety,
|
||||||
RoleTransaction,
|
RoleTransaction,
|
||||||
RoleUpdate,
|
RoleUpdate,
|
||||||
SessionData,
|
SessionData,
|
||||||
TransactionType,
|
TransactionType,
|
||||||
} from '@roleypoly/types';
|
} from '@roleypoly/types';
|
||||||
import { difference, groupBy, keyBy, union } from 'lodash';
|
import { difference, groupBy, keyBy, union } from 'lodash';
|
||||||
import { AuthType, discordFetch, respond, withSession } from '../utils/api-tools';
|
import { AuthType, discordFetch, respond, withSession } from '../utils/api-tools';
|
||||||
import { botToken } from '../utils/config';
|
import { botToken } from '../utils/config';
|
||||||
import {
|
import {
|
||||||
getGuild,
|
getGuild,
|
||||||
getGuildData,
|
getGuildData,
|
||||||
getGuildMemberRoles,
|
getGuildMemberRoles,
|
||||||
updateGuildMemberRoles,
|
updateGuildMemberRoles,
|
||||||
} from '../utils/guild';
|
} from '../utils/guild';
|
||||||
|
|
||||||
const notFound = () => respond({ error: 'guild not found' }, { status: 404 });
|
const notFound = () => respond({ error: 'guild not found' }, { status: 404 });
|
||||||
|
|
||||||
export const UpdateRoles = withSession(
|
export const UpdateRoles = withSession(
|
||||||
({ guilds, user: { id: userID } }: SessionData) => async (request: Request) => {
|
({ guilds, user: { id: userID } }: SessionData) => async (request: Request) => {
|
||||||
const updateRequest = (await request.json()) as RoleUpdate;
|
const updateRequest = (await request.json()) as RoleUpdate;
|
||||||
const [, , guildID] = new URL(request.url).pathname.split('/');
|
const [, , guildID] = new URL(request.url).pathname.split('/');
|
||||||
|
|
||||||
if (!guildID) {
|
if (!guildID) {
|
||||||
return respond({ error: 'guild ID missing from URL' }, { status: 400 });
|
return respond({ error: 'guild ID missing from URL' }, { status: 400 });
|
||||||
}
|
|
||||||
|
|
||||||
if (updateRequest.transactions.length === 0) {
|
|
||||||
return respond(
|
|
||||||
{ error: 'must have as least one transaction' },
|
|
||||||
{ status: 400 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const guildCheck = guilds.find((guild) => guild.id === guildID);
|
|
||||||
if (!guildCheck) {
|
|
||||||
return notFound();
|
|
||||||
}
|
|
||||||
|
|
||||||
const guild = await getGuild(guildID);
|
|
||||||
if (!guild) {
|
|
||||||
return notFound();
|
|
||||||
}
|
|
||||||
|
|
||||||
const guildMemberRoles = await getGuildMemberRoles(
|
|
||||||
{ serverID: guildID, userID },
|
|
||||||
{ skipCachePull: true }
|
|
||||||
);
|
|
||||||
if (!guildMemberRoles) {
|
|
||||||
return notFound();
|
|
||||||
}
|
|
||||||
|
|
||||||
const newRoles = calculateNewRoles({
|
|
||||||
currentRoles: guildMemberRoles,
|
|
||||||
guildRoles: guild.roles,
|
|
||||||
guildData: await getGuildData(guildID),
|
|
||||||
updateRequest,
|
|
||||||
});
|
|
||||||
|
|
||||||
const patchMemberRoles = await discordFetch<Member>(
|
|
||||||
`/guilds/${guildID}/members/${userID}`,
|
|
||||||
botToken,
|
|
||||||
AuthType.Bot,
|
|
||||||
{
|
|
||||||
method: 'PATCH',
|
|
||||||
headers: {
|
|
||||||
'content-type': 'application/json',
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
roles: newRoles,
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!patchMemberRoles) {
|
|
||||||
return respond({ error: 'discord rejected the request' }, { status: 500 });
|
|
||||||
}
|
|
||||||
|
|
||||||
const updatedMember: Member = {
|
|
||||||
roles: patchMemberRoles.roles,
|
|
||||||
};
|
|
||||||
|
|
||||||
await updateGuildMemberRoles(
|
|
||||||
{ serverID: guildID, userID },
|
|
||||||
patchMemberRoles.roles
|
|
||||||
);
|
|
||||||
|
|
||||||
return respond(updatedMember);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (updateRequest.transactions.length === 0) {
|
||||||
|
return respond({ error: 'must have as least one transaction' }, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const guildCheck = guilds.find((guild) => guild.id === guildID);
|
||||||
|
if (!guildCheck) {
|
||||||
|
return notFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
const guild = await getGuild(guildID);
|
||||||
|
if (!guild) {
|
||||||
|
return notFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
const guildMemberRoles = await getGuildMemberRoles(
|
||||||
|
{ serverID: guildID, userID },
|
||||||
|
{ skipCachePull: true }
|
||||||
|
);
|
||||||
|
if (!guildMemberRoles) {
|
||||||
|
return notFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
const newRoles = calculateNewRoles({
|
||||||
|
currentRoles: guildMemberRoles,
|
||||||
|
guildRoles: guild.roles,
|
||||||
|
guildData: await getGuildData(guildID),
|
||||||
|
updateRequest,
|
||||||
|
});
|
||||||
|
|
||||||
|
const patchMemberRoles = await discordFetch<Member>(
|
||||||
|
`/guilds/${guildID}/members/${userID}`,
|
||||||
|
botToken,
|
||||||
|
AuthType.Bot,
|
||||||
|
{
|
||||||
|
method: 'PATCH',
|
||||||
|
headers: {
|
||||||
|
'content-type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
roles: newRoles,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!patchMemberRoles) {
|
||||||
|
return respond({ error: 'discord rejected the request' }, { status: 500 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatedMember: Member = {
|
||||||
|
roles: patchMemberRoles.roles,
|
||||||
|
};
|
||||||
|
|
||||||
|
await updateGuildMemberRoles({ serverID: guildID, userID }, patchMemberRoles.roles);
|
||||||
|
|
||||||
|
return respond(updatedMember);
|
||||||
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const calculateNewRoles = ({
|
const calculateNewRoles = ({
|
||||||
currentRoles,
|
currentRoles,
|
||||||
guildData,
|
guildData,
|
||||||
guildRoles,
|
guildRoles,
|
||||||
updateRequest,
|
updateRequest,
|
||||||
}: {
|
}: {
|
||||||
currentRoles: string[];
|
currentRoles: string[];
|
||||||
guildRoles: Role[];
|
guildRoles: Role[];
|
||||||
guildData: GuildData;
|
guildData: GuildData;
|
||||||
updateRequest: RoleUpdate;
|
updateRequest: RoleUpdate;
|
||||||
}): string[] => {
|
}): string[] => {
|
||||||
const roleMap = keyBy(guildRoles, 'id');
|
const roleMap = keyBy(guildRoles, 'id');
|
||||||
|
|
||||||
// These roles were ones changed between knownState (role picker page load/cache) and current (fresh from discord).
|
// These roles were ones changed between knownState (role picker page load/cache) and current (fresh from discord).
|
||||||
// We could cause issues, so we'll re-add them later.
|
// We could cause issues, so we'll re-add them later.
|
||||||
// const diffRoles = difference(updateRequest.knownState, currentRoles);
|
// const diffRoles = difference(updateRequest.knownState, currentRoles);
|
||||||
|
|
||||||
// Only these are safe
|
// Only these are safe
|
||||||
const allSafeRoles = guildData.categories.reduce<string[]>(
|
const allSafeRoles = guildData.categories.reduce<string[]>(
|
||||||
(categorizedRoles, category) =>
|
(categorizedRoles, category) =>
|
||||||
!category.hidden
|
!category.hidden
|
||||||
? [
|
? [
|
||||||
...categorizedRoles,
|
...categorizedRoles,
|
||||||
...category.roles.filter(
|
...category.roles.filter(
|
||||||
(roleID) => roleMap[roleID]?.safety === RoleSafety.Safe
|
(roleID) => roleMap[roleID]?.safety === RoleSafety.Safe
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
: categorizedRoles,
|
: categorizedRoles,
|
||||||
[]
|
[]
|
||||||
);
|
);
|
||||||
|
|
||||||
const safeTransactions = updateRequest.transactions.filter((tx: RoleTransaction) =>
|
const safeTransactions = updateRequest.transactions.filter((tx: RoleTransaction) =>
|
||||||
allSafeRoles.includes(tx.id)
|
allSafeRoles.includes(tx.id)
|
||||||
);
|
);
|
||||||
|
|
||||||
const changesByAction = groupBy(safeTransactions, 'action');
|
const changesByAction = groupBy(safeTransactions, 'action');
|
||||||
|
|
||||||
const rolesToAdd = (changesByAction[TransactionType.Add] ?? []).map((tx) => tx.id);
|
const rolesToAdd = (changesByAction[TransactionType.Add] ?? []).map((tx) => tx.id);
|
||||||
const rolesToRemove = (changesByAction[TransactionType.Remove] ?? []).map(
|
const rolesToRemove = (changesByAction[TransactionType.Remove] ?? []).map(
|
||||||
(tx) => tx.id
|
(tx) => tx.id
|
||||||
);
|
);
|
||||||
|
|
||||||
const final = union(difference(currentRoles, rolesToRemove), rolesToAdd);
|
const final = union(difference(currentRoles, rolesToRemove), rolesToAdd);
|
||||||
|
|
||||||
return final;
|
return final;
|
||||||
};
|
};
|
||||||
|
|
|
@ -32,30 +32,30 @@ router.add('GET', 'x-create-roleypoly-data', CreateRoleypolyData);
|
||||||
|
|
||||||
// Tester Routes
|
// Tester Routes
|
||||||
router.add('GET', 'x-headers', (request) => {
|
router.add('GET', 'x-headers', (request) => {
|
||||||
const headers: { [x: string]: string } = {};
|
const headers: { [x: string]: string } = {};
|
||||||
|
|
||||||
for (let [key, value] of request.headers.entries()) {
|
for (let [key, value] of request.headers.entries()) {
|
||||||
headers[key] = value;
|
headers[key] = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Response(JSON.stringify(headers));
|
return new Response(JSON.stringify(headers));
|
||||||
});
|
});
|
||||||
|
|
||||||
// Root Zen <3
|
// Root Zen <3
|
||||||
router.addFallback('root', () => {
|
router.addFallback('root', () => {
|
||||||
return respond({
|
return respond({
|
||||||
__warning: '🦊',
|
__warning: '🦊',
|
||||||
this: 'is',
|
this: 'is',
|
||||||
a: 'fox-based',
|
a: 'fox-based',
|
||||||
web: 'application',
|
web: 'application',
|
||||||
please: 'be',
|
please: 'be',
|
||||||
mindful: 'of',
|
mindful: 'of',
|
||||||
your: 'surroundings',
|
your: 'surroundings',
|
||||||
warning__: '🦊',
|
warning__: '🦊',
|
||||||
meta: uiPublicURI,
|
meta: uiPublicURI,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
addEventListener('fetch', (event: FetchEvent) => {
|
addEventListener('fetch', (event: FetchEvent) => {
|
||||||
event.respondWith(router.handle(event.request));
|
event.respondWith(router.handle(event.request));
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,18 +1,18 @@
|
||||||
{
|
{
|
||||||
"name": "@roleypoly/api",
|
"name": "@roleypoly/api",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "yarn workspace @roleypoly/worker-emulator build --basePath `pwd`",
|
"build": "yarn workspace @roleypoly/worker-emulator build --basePath `pwd`",
|
||||||
"lint:types": "tsc --noEmit",
|
"lint:types": "tsc --noEmit",
|
||||||
"start": "yarn workspace @roleypoly/worker-emulator start --basePath `pwd`"
|
"start": "yarn workspace @roleypoly/worker-emulator start --basePath `pwd`"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@cloudflare/workers-types": "^2.1.0",
|
"@cloudflare/workers-types": "^2.1.0",
|
||||||
"@roleypoly/misc-utils": "*",
|
"@roleypoly/misc-utils": "*",
|
||||||
"@roleypoly/types": "*",
|
"@roleypoly/types": "*",
|
||||||
"@roleypoly/worker-emulator": "*",
|
"@roleypoly/worker-emulator": "*",
|
||||||
"ksuid": "^2.0.0",
|
"ksuid": "^2.0.0",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"ts-loader": "^8.0.18"
|
"ts-loader": "^8.0.18"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,81 +4,81 @@ import { uiPublicURI } from './utils/config';
|
||||||
export type Handler = (request: Request) => Promise<Response> | Response;
|
export type Handler = (request: Request) => Promise<Response> | Response;
|
||||||
|
|
||||||
type RoutingTree = {
|
type RoutingTree = {
|
||||||
[method: string]: {
|
[method: string]: {
|
||||||
[path: string]: Handler;
|
[path: string]: Handler;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
type Fallbacks = {
|
type Fallbacks = {
|
||||||
root: Handler;
|
root: Handler;
|
||||||
404: Handler;
|
404: Handler;
|
||||||
500: Handler;
|
500: Handler;
|
||||||
};
|
};
|
||||||
|
|
||||||
export class Router {
|
export class Router {
|
||||||
private routingTree: RoutingTree = {};
|
private routingTree: RoutingTree = {};
|
||||||
private fallbacks: Fallbacks = {
|
private fallbacks: Fallbacks = {
|
||||||
root: this.respondToRoot,
|
root: this.respondToRoot,
|
||||||
404: this.notFound,
|
404: this.notFound,
|
||||||
500: this.serverError,
|
500: this.serverError,
|
||||||
};
|
};
|
||||||
|
|
||||||
private uiURL = new URL(uiPublicURI);
|
private uiURL = new URL(uiPublicURI);
|
||||||
|
|
||||||
addFallback(which: keyof Fallbacks, handler: Handler) {
|
addFallback(which: keyof Fallbacks, handler: Handler) {
|
||||||
this.fallbacks[which] = handler;
|
this.fallbacks[which] = handler;
|
||||||
|
}
|
||||||
|
|
||||||
|
add(method: string, rootPath: string, handler: Handler) {
|
||||||
|
const lowerMethod = method.toLowerCase();
|
||||||
|
|
||||||
|
if (!this.routingTree[lowerMethod]) {
|
||||||
|
this.routingTree[lowerMethod] = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
add(method: string, rootPath: string, handler: Handler) {
|
this.routingTree[lowerMethod][rootPath] = handler;
|
||||||
const lowerMethod = method.toLowerCase();
|
}
|
||||||
|
|
||||||
if (!this.routingTree[lowerMethod]) {
|
async handle(request: Request): Promise<Response> {
|
||||||
this.routingTree[lowerMethod] = {};
|
const url = new URL(request.url);
|
||||||
}
|
|
||||||
|
|
||||||
this.routingTree[lowerMethod][rootPath] = handler;
|
if (url.pathname === '/' || url.pathname === '') {
|
||||||
|
return this.fallbacks.root(request);
|
||||||
|
}
|
||||||
|
const lowerMethod = request.method.toLowerCase();
|
||||||
|
const rootPath = url.pathname.split('/')[1];
|
||||||
|
const handler = this.routingTree[lowerMethod]?.[rootPath];
|
||||||
|
|
||||||
|
if (handler) {
|
||||||
|
try {
|
||||||
|
const response = await handler(request);
|
||||||
|
return response;
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
return this.fallbacks[500](request);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async handle(request: Request): Promise<Response> {
|
if (lowerMethod === 'options') {
|
||||||
const url = new URL(request.url);
|
return new Response(null, addCORS({}));
|
||||||
|
|
||||||
if (url.pathname === '/' || url.pathname === '') {
|
|
||||||
return this.fallbacks.root(request);
|
|
||||||
}
|
|
||||||
const lowerMethod = request.method.toLowerCase();
|
|
||||||
const rootPath = url.pathname.split('/')[1];
|
|
||||||
const handler = this.routingTree[lowerMethod]?.[rootPath];
|
|
||||||
|
|
||||||
if (handler) {
|
|
||||||
try {
|
|
||||||
const response = await handler(request);
|
|
||||||
return response;
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
return this.fallbacks[500](request);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (lowerMethod === 'options') {
|
|
||||||
return new Response(null, addCORS({}));
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.fallbacks[404](request);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private respondToRoot(): Response {
|
return this.fallbacks[404](request);
|
||||||
return new Response('Hi there!');
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private notFound(): Response {
|
private respondToRoot(): Response {
|
||||||
return new Response(JSON.stringify({ error: 'not_found' }), {
|
return new Response('Hi there!');
|
||||||
status: 404,
|
}
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private serverError(): Response {
|
private notFound(): Response {
|
||||||
return new Response(JSON.stringify({ error: 'internal_server_error' }), {
|
return new Response(JSON.stringify({ error: 'not_found' }), {
|
||||||
status: 500,
|
status: 404,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private serverError(): Response {
|
||||||
|
return new Response(JSON.stringify({ error: 'internal_server_error' }), {
|
||||||
|
status: 500,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,15 +1,15 @@
|
||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"outDir": "./dist",
|
"outDir": "./dist",
|
||||||
"lib": ["esnext", "webworker", "ES2020.BigInt", "ES2020.Promise"],
|
"lib": ["esnext", "webworker", "ES2020.BigInt", "ES2020.Promise"],
|
||||||
"types": ["@cloudflare/workers-types"],
|
"types": ["@cloudflare/workers-types"],
|
||||||
"target": "ES2019"
|
"target": "ES2019"
|
||||||
},
|
},
|
||||||
"include": [
|
"include": [
|
||||||
"./*.ts",
|
"./*.ts",
|
||||||
"./**/*.ts",
|
"./**/*.ts",
|
||||||
"../../node_modules/@cloudflare/workers-types/index.d.ts"
|
"../../node_modules/@cloudflare/workers-types/index.d.ts"
|
||||||
],
|
],
|
||||||
"exclude": ["./**/*.spec.ts", "./dist/**"],
|
"exclude": ["./**/*.spec.ts", "./dist/**"],
|
||||||
"extends": "../../tsconfig.json"
|
"extends": "../../tsconfig.json"
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import {
|
import {
|
||||||
evaluatePermission,
|
evaluatePermission,
|
||||||
permissions as Permissions,
|
permissions as Permissions,
|
||||||
} from '@roleypoly/misc-utils/hasPermission';
|
} from '@roleypoly/misc-utils/hasPermission';
|
||||||
import { SessionData, UserGuildPermissions } from '@roleypoly/types';
|
import { SessionData, UserGuildPermissions } from '@roleypoly/types';
|
||||||
import KSUID from 'ksuid';
|
import KSUID from 'ksuid';
|
||||||
|
@ -9,207 +9,204 @@ import { allowedCallbackHosts, apiPublicURI, rootUsers } from './config';
|
||||||
import { Sessions, WrappedKVNamespace } from './kv';
|
import { Sessions, WrappedKVNamespace } from './kv';
|
||||||
|
|
||||||
export const formData = (obj: Record<string, any>): string => {
|
export const formData = (obj: Record<string, any>): string => {
|
||||||
return Object.keys(obj)
|
return Object.keys(obj)
|
||||||
.map((key) => `${encodeURIComponent(key)}=${encodeURIComponent(obj[key])}`)
|
.map((key) => `${encodeURIComponent(key)}=${encodeURIComponent(obj[key])}`)
|
||||||
.join('&');
|
.join('&');
|
||||||
};
|
};
|
||||||
|
|
||||||
export const addCORS = (init: ResponseInit = {}) => ({
|
export const addCORS = (init: ResponseInit = {}) => ({
|
||||||
...init,
|
...init,
|
||||||
headers: {
|
headers: {
|
||||||
...(init.headers || {}),
|
...(init.headers || {}),
|
||||||
'access-control-allow-origin': '*',
|
'access-control-allow-origin': '*',
|
||||||
'access-control-allow-methods': '*',
|
'access-control-allow-methods': '*',
|
||||||
'access-control-allow-headers': '*',
|
'access-control-allow-headers': '*',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const respond = (obj: Record<string, any>, init: ResponseInit = {}) =>
|
export const respond = (obj: Record<string, any>, init: ResponseInit = {}) =>
|
||||||
new Response(JSON.stringify(obj), addCORS(init));
|
new Response(JSON.stringify(obj), addCORS(init));
|
||||||
|
|
||||||
export const resolveFailures = (
|
export const resolveFailures = (
|
||||||
handleWith: () => Response,
|
handleWith: () => Response,
|
||||||
handler: (request: Request) => Promise<Response> | Response
|
handler: (request: Request) => Promise<Response> | Response
|
||||||
) => async (request: Request): Promise<Response> => {
|
) => async (request: Request): Promise<Response> => {
|
||||||
try {
|
try {
|
||||||
return handler(request);
|
return handler(request);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
return (
|
return handleWith() || respond({ error: 'internal server error' }, { status: 500 });
|
||||||
handleWith() || respond({ error: 'internal server error' }, { status: 500 })
|
}
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const parsePermissions = (
|
export const parsePermissions = (
|
||||||
permissions: bigint,
|
permissions: bigint,
|
||||||
owner: boolean = false
|
owner: boolean = false
|
||||||
): UserGuildPermissions => {
|
): UserGuildPermissions => {
|
||||||
if (owner || evaluatePermission(permissions, Permissions.ADMINISTRATOR)) {
|
if (owner || evaluatePermission(permissions, Permissions.ADMINISTRATOR)) {
|
||||||
return UserGuildPermissions.Admin;
|
return UserGuildPermissions.Admin;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (evaluatePermission(permissions, Permissions.MANAGE_ROLES)) {
|
if (evaluatePermission(permissions, Permissions.MANAGE_ROLES)) {
|
||||||
return UserGuildPermissions.Manager;
|
return UserGuildPermissions.Manager;
|
||||||
}
|
}
|
||||||
|
|
||||||
return UserGuildPermissions.User;
|
return UserGuildPermissions.User;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getSessionID = (request: Request): { type: string; id: string } | null => {
|
export const getSessionID = (request: Request): { type: string; id: string } | null => {
|
||||||
const sessionID = request.headers.get('authorization');
|
const sessionID = request.headers.get('authorization');
|
||||||
if (!sessionID) {
|
if (!sessionID) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const [type, id] = sessionID.split(' ');
|
const [type, id] = sessionID.split(' ');
|
||||||
if (type !== 'Bearer') {
|
if (type !== 'Bearer') {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return { type, id };
|
return { type, id };
|
||||||
};
|
};
|
||||||
|
|
||||||
export const userAgent =
|
export const userAgent =
|
||||||
'DiscordBot (https://github.com/roleypoly/roleypoly, git-main) (+https://roleypoly.com)';
|
'DiscordBot (https://github.com/roleypoly/roleypoly, git-main) (+https://roleypoly.com)';
|
||||||
|
|
||||||
export enum AuthType {
|
export enum AuthType {
|
||||||
Bearer = 'Bearer',
|
Bearer = 'Bearer',
|
||||||
Bot = 'Bot',
|
Bot = 'Bot',
|
||||||
}
|
}
|
||||||
|
|
||||||
export const discordFetch = async <T>(
|
export const discordFetch = async <T>(
|
||||||
url: string,
|
url: string,
|
||||||
auth: string,
|
auth: string,
|
||||||
authType: AuthType = AuthType.Bearer,
|
authType: AuthType = AuthType.Bearer,
|
||||||
init?: RequestInit
|
init?: RequestInit
|
||||||
): Promise<T | null> => {
|
): Promise<T | null> => {
|
||||||
const response = await fetch('https://discord.com/api/v8' + url, {
|
const response = await fetch('https://discord.com/api/v8' + url, {
|
||||||
...(init || {}),
|
...(init || {}),
|
||||||
headers: {
|
headers: {
|
||||||
...(init?.headers || {}),
|
...(init?.headers || {}),
|
||||||
authorization: `${AuthType[authType]} ${auth}`,
|
authorization: `${AuthType[authType]} ${auth}`,
|
||||||
'user-agent': userAgent,
|
'user-agent': userAgent,
|
||||||
},
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.status >= 400) {
|
||||||
|
console.error('discordFetch failed', {
|
||||||
|
url,
|
||||||
|
authType,
|
||||||
|
payload: await response.text(),
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (response.status >= 400) {
|
if (response.ok) {
|
||||||
console.error('discordFetch failed', {
|
return (await response.json()) as T;
|
||||||
url,
|
} else {
|
||||||
authType,
|
return null;
|
||||||
payload: await response.text(),
|
}
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (response.ok) {
|
|
||||||
return (await response.json()) as T;
|
|
||||||
} else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const cacheLayer = <Identity, Data>(
|
export const cacheLayer = <Identity, Data>(
|
||||||
kv: WrappedKVNamespace,
|
kv: WrappedKVNamespace,
|
||||||
keyFactory: (identity: Identity) => string,
|
keyFactory: (identity: Identity) => string,
|
||||||
missHandler: (identity: Identity) => Promise<Data | null>,
|
missHandler: (identity: Identity) => Promise<Data | null>,
|
||||||
ttlSeconds?: number
|
ttlSeconds?: number
|
||||||
) => async (
|
) => async (
|
||||||
identity: Identity,
|
identity: Identity,
|
||||||
options: { skipCachePull?: boolean } = {}
|
options: { skipCachePull?: boolean } = {}
|
||||||
): Promise<Data | null> => {
|
): Promise<Data | null> => {
|
||||||
const key = keyFactory(identity);
|
const key = keyFactory(identity);
|
||||||
|
|
||||||
if (!options.skipCachePull) {
|
if (!options.skipCachePull) {
|
||||||
const value = await kv.get<Data>(key);
|
const value = await kv.get<Data>(key);
|
||||||
if (value) {
|
if (value) {
|
||||||
return value;
|
return value;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const fallbackValue = await missHandler(identity);
|
const fallbackValue = await missHandler(identity);
|
||||||
if (!fallbackValue) {
|
if (!fallbackValue) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
await kv.put(key, fallbackValue, ttlSeconds);
|
await kv.put(key, fallbackValue, ttlSeconds);
|
||||||
|
|
||||||
return fallbackValue;
|
return fallbackValue;
|
||||||
};
|
};
|
||||||
|
|
||||||
const NotAuthenticated = (extra?: string) =>
|
const NotAuthenticated = (extra?: string) =>
|
||||||
respond(
|
respond(
|
||||||
{
|
{
|
||||||
error: extra || 'not authenticated',
|
error: extra || 'not authenticated',
|
||||||
},
|
},
|
||||||
{ status: 403 }
|
{ status: 403 }
|
||||||
);
|
);
|
||||||
|
|
||||||
export const withSession = (
|
export const withSession = (
|
||||||
wrappedHandler: (session: SessionData) => Handler
|
wrappedHandler: (session: SessionData) => Handler
|
||||||
): Handler => async (request: Request): Promise<Response> => {
|
): Handler => async (request: Request): Promise<Response> => {
|
||||||
const sessionID = getSessionID(request);
|
const sessionID = getSessionID(request);
|
||||||
if (!sessionID) {
|
if (!sessionID) {
|
||||||
return NotAuthenticated('missing authentication');
|
return NotAuthenticated('missing authentication');
|
||||||
}
|
}
|
||||||
|
|
||||||
const session = await Sessions.get<SessionData>(sessionID.id);
|
const session = await Sessions.get<SessionData>(sessionID.id);
|
||||||
if (!session) {
|
if (!session) {
|
||||||
return NotAuthenticated('authentication expired or not found');
|
return NotAuthenticated('authentication expired or not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
return await wrappedHandler(session)(request);
|
return await wrappedHandler(session)(request);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const setupStateSession = async <T>(data: T): Promise<string> => {
|
export const setupStateSession = async <T>(data: T): Promise<string> => {
|
||||||
const stateID = (await KSUID.random()).string;
|
const stateID = (await KSUID.random()).string;
|
||||||
|
|
||||||
await Sessions.put(`state_${stateID}`, { data }, 60 * 5);
|
await Sessions.put(`state_${stateID}`, { data }, 60 * 5);
|
||||||
|
|
||||||
return stateID;
|
return stateID;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getStateSession = async <T>(stateID: string): Promise<T | undefined> => {
|
export const getStateSession = async <T>(stateID: string): Promise<T | undefined> => {
|
||||||
const stateSession = await Sessions.get<{ data: T }>(`state_${stateID}`);
|
const stateSession = await Sessions.get<{ data: T }>(`state_${stateID}`);
|
||||||
|
|
||||||
return stateSession?.data;
|
return stateSession?.data;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const isRoot = (userID: string): boolean => rootUsers.includes(userID);
|
export const isRoot = (userID: string): boolean => rootUsers.includes(userID);
|
||||||
|
|
||||||
export const onlyRootUsers = (handler: Handler): Handler =>
|
export const onlyRootUsers = (handler: Handler): Handler =>
|
||||||
withSession((session) => (request: Request) => {
|
withSession((session) => (request: Request) => {
|
||||||
if (isRoot(session.user.id)) {
|
if (isRoot(session.user.id)) {
|
||||||
return handler(request);
|
return handler(request);
|
||||||
}
|
|
||||||
|
|
||||||
return respond(
|
|
||||||
{
|
|
||||||
error: 'not_found',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
status: 404,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
export const getQuery = (request: Request): { [x: string]: string } => {
|
|
||||||
const output: { [x: string]: string } = {};
|
|
||||||
|
|
||||||
for (let [key, value] of new URL(request.url).searchParams.entries()) {
|
|
||||||
output[key] = value;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return output;
|
return respond(
|
||||||
|
{
|
||||||
|
error: 'not_found',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
status: 404,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
export const getQuery = (request: Request): { [x: string]: string } => {
|
||||||
|
const output: { [x: string]: string } = {};
|
||||||
|
|
||||||
|
for (let [key, value] of new URL(request.url).searchParams.entries()) {
|
||||||
|
output[key] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return output;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const isAllowedCallbackHost = (host: string): boolean => {
|
export const isAllowedCallbackHost = (host: string): boolean => {
|
||||||
return (
|
return (
|
||||||
host === apiPublicURI ||
|
host === apiPublicURI ||
|
||||||
allowedCallbackHosts.includes(host) ||
|
allowedCallbackHosts.includes(host) ||
|
||||||
allowedCallbackHosts
|
allowedCallbackHosts
|
||||||
.filter((callbackHost) => callbackHost.includes('*'))
|
.filter((callbackHost) => callbackHost.includes('*'))
|
||||||
.find((wildcard) =>
|
.find((wildcard) => new RegExp(wildcard.replace('*', '[a-z0-9-]+')).test(host)) !==
|
||||||
new RegExp(wildcard.replace('*', '[a-z0-9-]+')).test(host)
|
null
|
||||||
) !== null
|
);
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
export const Bounce = (url: string): Response =>
|
export const Bounce = (url: string): Response =>
|
||||||
new Response(null, {
|
new Response(null, {
|
||||||
status: 303,
|
status: 303,
|
||||||
headers: {
|
headers: {
|
||||||
location: url,
|
location: url,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,163 +1,163 @@
|
||||||
import { evaluatePermission, permissions } from '@roleypoly/misc-utils/hasPermission';
|
import { evaluatePermission, permissions } from '@roleypoly/misc-utils/hasPermission';
|
||||||
import {
|
import {
|
||||||
Features,
|
Features,
|
||||||
Guild,
|
Guild,
|
||||||
GuildData as GuildDataT,
|
GuildData as GuildDataT,
|
||||||
OwnRoleInfo,
|
OwnRoleInfo,
|
||||||
Role,
|
Role,
|
||||||
RoleSafety,
|
RoleSafety,
|
||||||
} from '@roleypoly/types';
|
} from '@roleypoly/types';
|
||||||
import { AuthType, cacheLayer, discordFetch } from './api-tools';
|
import { AuthType, cacheLayer, discordFetch } from './api-tools';
|
||||||
import { botClientID, botToken } from './config';
|
import { botClientID, botToken } from './config';
|
||||||
import { GuildData, Guilds } from './kv';
|
import { GuildData, Guilds } from './kv';
|
||||||
|
|
||||||
type APIGuild = {
|
type APIGuild = {
|
||||||
// Only relevant stuff
|
// Only relevant stuff
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
icon: string;
|
icon: string;
|
||||||
roles: APIRole[];
|
roles: APIRole[];
|
||||||
};
|
};
|
||||||
|
|
||||||
type APIRole = {
|
type APIRole = {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
color: number;
|
color: number;
|
||||||
position: number;
|
position: number;
|
||||||
permissions: string;
|
permissions: string;
|
||||||
managed: boolean;
|
managed: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getGuild = cacheLayer(
|
export const getGuild = cacheLayer(
|
||||||
Guilds,
|
Guilds,
|
||||||
(id: string) => `guilds/${id}`,
|
(id: string) => `guilds/${id}`,
|
||||||
async (id: string) => {
|
async (id: string) => {
|
||||||
const guildRaw = await discordFetch<APIGuild>(
|
const guildRaw = await discordFetch<APIGuild>(
|
||||||
`/guilds/${id}`,
|
`/guilds/${id}`,
|
||||||
botToken,
|
botToken,
|
||||||
AuthType.Bot
|
AuthType.Bot
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!guildRaw) {
|
if (!guildRaw) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const botMemberRoles =
|
const botMemberRoles =
|
||||||
(await getGuildMemberRoles({
|
(await getGuildMemberRoles({
|
||||||
serverID: id,
|
serverID: id,
|
||||||
userID: botClientID,
|
userID: botClientID,
|
||||||
})) || [];
|
})) || [];
|
||||||
|
|
||||||
const highestRolePosition = botMemberRoles.reduce<number>((highest, roleID) => {
|
const highestRolePosition = botMemberRoles.reduce<number>((highest, roleID) => {
|
||||||
const role = guildRaw.roles.find((guildRole) => guildRole.id === roleID);
|
const role = guildRaw.roles.find((guildRole) => guildRole.id === roleID);
|
||||||
if (!role) {
|
if (!role) {
|
||||||
return highest;
|
return highest;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If highest is a bigger number, it stays the highest.
|
// If highest is a bigger number, it stays the highest.
|
||||||
if (highest > role.position) {
|
if (highest > role.position) {
|
||||||
return highest;
|
return highest;
|
||||||
}
|
}
|
||||||
|
|
||||||
return role.position;
|
return role.position;
|
||||||
}, 0);
|
}, 0);
|
||||||
|
|
||||||
const roles = guildRaw.roles.map<Role>((role) => ({
|
const roles = guildRaw.roles.map<Role>((role) => ({
|
||||||
id: role.id,
|
id: role.id,
|
||||||
name: role.name,
|
name: role.name,
|
||||||
color: role.color,
|
color: role.color,
|
||||||
managed: role.managed,
|
managed: role.managed,
|
||||||
position: role.position,
|
position: role.position,
|
||||||
permissions: role.permissions,
|
permissions: role.permissions,
|
||||||
safety: calculateRoleSafety(role, highestRolePosition),
|
safety: calculateRoleSafety(role, highestRolePosition),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Filters the raw guild data into data we actually want
|
// Filters the raw guild data into data we actually want
|
||||||
const guild: Guild & OwnRoleInfo = {
|
const guild: Guild & OwnRoleInfo = {
|
||||||
id: guildRaw.id,
|
id: guildRaw.id,
|
||||||
name: guildRaw.name,
|
name: guildRaw.name,
|
||||||
icon: guildRaw.icon,
|
icon: guildRaw.icon,
|
||||||
roles,
|
roles,
|
||||||
highestRolePosition,
|
highestRolePosition,
|
||||||
};
|
};
|
||||||
|
|
||||||
return guild;
|
return guild;
|
||||||
},
|
},
|
||||||
60 * 60 * 2 // 2 hour TTL
|
60 * 60 * 2 // 2 hour TTL
|
||||||
);
|
);
|
||||||
|
|
||||||
type GuildMemberIdentity = {
|
type GuildMemberIdentity = {
|
||||||
serverID: string;
|
serverID: string;
|
||||||
userID: string;
|
userID: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type APIMember = {
|
type APIMember = {
|
||||||
// Only relevant stuff, again.
|
// Only relevant stuff, again.
|
||||||
roles: string[];
|
roles: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
const guildMemberRolesIdentity = ({ serverID, userID }: GuildMemberIdentity) =>
|
const guildMemberRolesIdentity = ({ serverID, userID }: GuildMemberIdentity) =>
|
||||||
`guilds/${serverID}/members/${userID}/roles`;
|
`guilds/${serverID}/members/${userID}/roles`;
|
||||||
|
|
||||||
export const getGuildMemberRoles = cacheLayer<GuildMemberIdentity, Role['id'][]>(
|
export const getGuildMemberRoles = cacheLayer<GuildMemberIdentity, Role['id'][]>(
|
||||||
Guilds,
|
Guilds,
|
||||||
guildMemberRolesIdentity,
|
guildMemberRolesIdentity,
|
||||||
async ({ serverID, userID }) => {
|
async ({ serverID, userID }) => {
|
||||||
const discordMember = await discordFetch<APIMember>(
|
const discordMember = await discordFetch<APIMember>(
|
||||||
`/guilds/${serverID}/members/${userID}`,
|
`/guilds/${serverID}/members/${userID}`,
|
||||||
botToken,
|
botToken,
|
||||||
AuthType.Bot
|
AuthType.Bot
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!discordMember) {
|
if (!discordMember) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return discordMember.roles;
|
return discordMember.roles;
|
||||||
},
|
},
|
||||||
60 * 5 // 5 minute TTL
|
60 * 5 // 5 minute TTL
|
||||||
);
|
);
|
||||||
|
|
||||||
export const updateGuildMemberRoles = async (
|
export const updateGuildMemberRoles = async (
|
||||||
identity: GuildMemberIdentity,
|
identity: GuildMemberIdentity,
|
||||||
roles: Role['id'][]
|
roles: Role['id'][]
|
||||||
) => {
|
) => {
|
||||||
await Guilds.put(guildMemberRolesIdentity(identity), roles, 60 * 5);
|
await Guilds.put(guildMemberRolesIdentity(identity), roles, 60 * 5);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getGuildData = async (id: string): Promise<GuildDataT> => {
|
export const getGuildData = async (id: string): Promise<GuildDataT> => {
|
||||||
const guildData = await GuildData.get<GuildDataT>(id);
|
const guildData = await GuildData.get<GuildDataT>(id);
|
||||||
|
|
||||||
if (!guildData) {
|
if (!guildData) {
|
||||||
return {
|
return {
|
||||||
id,
|
id,
|
||||||
message: '',
|
message: '',
|
||||||
categories: [],
|
categories: [],
|
||||||
features: Features.None,
|
features: Features.None,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return guildData;
|
return guildData;
|
||||||
};
|
};
|
||||||
|
|
||||||
const calculateRoleSafety = (role: Role | APIRole, highestBotRolePosition: number) => {
|
const calculateRoleSafety = (role: Role | APIRole, highestBotRolePosition: number) => {
|
||||||
let safety = RoleSafety.Safe;
|
let safety = RoleSafety.Safe;
|
||||||
|
|
||||||
if (role.managed) {
|
if (role.managed) {
|
||||||
safety |= RoleSafety.ManagedRole;
|
safety |= RoleSafety.ManagedRole;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (role.position > highestBotRolePosition) {
|
if (role.position > highestBotRolePosition) {
|
||||||
safety |= RoleSafety.HigherThanBot;
|
safety |= RoleSafety.HigherThanBot;
|
||||||
}
|
}
|
||||||
|
|
||||||
const permBigInt = BigInt(role.permissions);
|
const permBigInt = BigInt(role.permissions);
|
||||||
if (
|
if (
|
||||||
evaluatePermission(permBigInt, permissions.ADMINISTRATOR) ||
|
evaluatePermission(permBigInt, permissions.ADMINISTRATOR) ||
|
||||||
evaluatePermission(permBigInt, permissions.MANAGE_ROLES)
|
evaluatePermission(permBigInt, permissions.MANAGE_ROLES)
|
||||||
) {
|
) {
|
||||||
safety |= RoleSafety.DangerousPermissions;
|
safety |= RoleSafety.DangerousPermissions;
|
||||||
}
|
}
|
||||||
|
|
||||||
return safety;
|
return safety;
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,87 +1,87 @@
|
||||||
export class WrappedKVNamespace {
|
export class WrappedKVNamespace {
|
||||||
constructor(private kvNamespace: KVNamespace) {}
|
constructor(private kvNamespace: KVNamespace) {}
|
||||||
|
|
||||||
async get<T>(key: string): Promise<T | null> {
|
async get<T>(key: string): Promise<T | null> {
|
||||||
const data = await this.kvNamespace.get(key, 'text');
|
const data = await this.kvNamespace.get(key, 'text');
|
||||||
if (!data) {
|
if (!data) {
|
||||||
return null;
|
return null;
|
||||||
}
|
|
||||||
|
|
||||||
return JSON.parse(data) as T;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async put<T>(key: string, value: T, ttlSeconds?: number) {
|
return JSON.parse(data) as T;
|
||||||
await this.kvNamespace.put(key, JSON.stringify(value), {
|
}
|
||||||
expirationTtl: ttlSeconds,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
list = this.kvNamespace.list;
|
async put<T>(key: string, value: T, ttlSeconds?: number) {
|
||||||
getWithMetadata = this.kvNamespace.getWithMetadata;
|
await this.kvNamespace.put(key, JSON.stringify(value), {
|
||||||
delete = this.kvNamespace.delete;
|
expirationTtl: ttlSeconds,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
list = this.kvNamespace.list;
|
||||||
|
getWithMetadata = this.kvNamespace.getWithMetadata;
|
||||||
|
delete = this.kvNamespace.delete;
|
||||||
}
|
}
|
||||||
|
|
||||||
class EmulatedKV implements KVNamespace {
|
class EmulatedKV implements KVNamespace {
|
||||||
constructor() {
|
constructor() {
|
||||||
console.warn('EmulatedKV used. Data will be lost.');
|
console.warn('EmulatedKV used. Data will be lost.');
|
||||||
|
}
|
||||||
|
|
||||||
|
private data: Map<string, any> = new Map();
|
||||||
|
|
||||||
|
async get<T>(key: string): Promise<T | null> {
|
||||||
|
if (!this.data.has(key)) {
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private data: Map<string, any> = new Map();
|
return this.data.get(key);
|
||||||
|
}
|
||||||
|
|
||||||
async get<T>(key: string): Promise<T | null> {
|
async getWithMetadata<T, Metadata = unknown>(
|
||||||
if (!this.data.has(key)) {
|
key: string
|
||||||
return null;
|
): KVValueWithMetadata<T, Metadata> {
|
||||||
}
|
return {
|
||||||
|
value: await this.get<T>(key),
|
||||||
|
metadata: {} as Metadata,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
return this.data.get(key);
|
async put(key: string, value: string | ReadableStream<any> | ArrayBuffer | FormData) {
|
||||||
|
this.data.set(key, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
async delete(key: string) {
|
||||||
|
this.data.delete(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
async list(options?: {
|
||||||
|
prefix?: string;
|
||||||
|
limit?: number;
|
||||||
|
cursor?: string;
|
||||||
|
}): Promise<{
|
||||||
|
keys: { name: string; expiration?: number; metadata?: unknown }[];
|
||||||
|
list_complete: boolean;
|
||||||
|
cursor: string;
|
||||||
|
}> {
|
||||||
|
let keys: { name: string }[] = [];
|
||||||
|
|
||||||
|
for (let key of this.data.keys()) {
|
||||||
|
if (options?.prefix && !key.startsWith(options.prefix)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
keys.push({ name: key });
|
||||||
}
|
}
|
||||||
|
|
||||||
async getWithMetadata<T, Metadata = unknown>(
|
return {
|
||||||
key: string
|
keys,
|
||||||
): KVValueWithMetadata<T, Metadata> {
|
cursor: '0',
|
||||||
return {
|
list_complete: true,
|
||||||
value: await this.get<T>(key),
|
};
|
||||||
metadata: {} as Metadata,
|
}
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async put(key: string, value: string | ReadableStream<any> | ArrayBuffer | FormData) {
|
|
||||||
this.data.set(key, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
async delete(key: string) {
|
|
||||||
this.data.delete(key);
|
|
||||||
}
|
|
||||||
|
|
||||||
async list(options?: {
|
|
||||||
prefix?: string;
|
|
||||||
limit?: number;
|
|
||||||
cursor?: string;
|
|
||||||
}): Promise<{
|
|
||||||
keys: { name: string; expiration?: number; metadata?: unknown }[];
|
|
||||||
list_complete: boolean;
|
|
||||||
cursor: string;
|
|
||||||
}> {
|
|
||||||
let keys: { name: string }[] = [];
|
|
||||||
|
|
||||||
for (let key of this.data.keys()) {
|
|
||||||
if (options?.prefix && !key.startsWith(options.prefix)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
keys.push({ name: key });
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
keys,
|
|
||||||
cursor: '0',
|
|
||||||
list_complete: true,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const kvOrLocal = (namespace: KVNamespace | null): KVNamespace =>
|
const kvOrLocal = (namespace: KVNamespace | null): KVNamespace =>
|
||||||
namespace || new EmulatedKV();
|
namespace || new EmulatedKV();
|
||||||
|
|
||||||
const self = (global as any) as Record<string, any>;
|
const self = (global as any) as Record<string, any>;
|
||||||
|
|
||||||
|
|
|
@ -3,26 +3,26 @@ const path = require('path');
|
||||||
const mode = process.env.NODE_ENV || 'production';
|
const mode = process.env.NODE_ENV || 'production';
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
target: 'webworker',
|
target: 'webworker',
|
||||||
entry: path.join(__dirname, 'index.ts'),
|
entry: path.join(__dirname, 'index.ts'),
|
||||||
output: {
|
output: {
|
||||||
filename: `worker.${mode}.js`,
|
filename: `worker.${mode}.js`,
|
||||||
path: path.join(__dirname, 'dist'),
|
path: path.join(__dirname, 'dist'),
|
||||||
},
|
},
|
||||||
mode,
|
mode,
|
||||||
resolve: {
|
resolve: {
|
||||||
extensions: ['.ts', '.tsx', '.js'],
|
extensions: ['.ts', '.tsx', '.js'],
|
||||||
},
|
},
|
||||||
module: {
|
module: {
|
||||||
rules: [
|
rules: [
|
||||||
{
|
{
|
||||||
test: /\.tsx?$/,
|
test: /\.tsx?$/,
|
||||||
loader: 'ts-loader',
|
loader: 'ts-loader',
|
||||||
options: {
|
options: {
|
||||||
transpileOnly: true,
|
transpileOnly: true,
|
||||||
configFile: path.join(__dirname, 'tsconfig.json'),
|
configFile: path.join(__dirname, 'tsconfig.json'),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,16 +1,16 @@
|
||||||
const reexportEnv = (keys = []) => {
|
const reexportEnv = (keys = []) => {
|
||||||
return keys.reduce((acc, key) => ({ ...acc, [key]: process.env[key] }), {});
|
return keys.reduce((acc, key) => ({ ...acc, [key]: process.env[key] }), {});
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
environment: reexportEnv([
|
environment: reexportEnv([
|
||||||
'BOT_CLIENT_ID',
|
'BOT_CLIENT_ID',
|
||||||
'BOT_CLIENT_SECRET',
|
'BOT_CLIENT_SECRET',
|
||||||
'BOT_TOKEN',
|
'BOT_TOKEN',
|
||||||
'UI_PUBLIC_URI',
|
'UI_PUBLIC_URI',
|
||||||
'API_PUBLIC_URI',
|
'API_PUBLIC_URI',
|
||||||
'ROOT_USERS',
|
'ROOT_USERS',
|
||||||
'ALLOWED_CALLBACK_HOSTS',
|
'ALLOWED_CALLBACK_HOSTS',
|
||||||
]),
|
]),
|
||||||
kv: ['KV_SESSIONS', 'KV_GUILDS', 'KV_GUILD_DATA'],
|
kv: ['KV_SESSIONS', 'KV_GUILDS', 'KV_GUILD_DATA'],
|
||||||
};
|
};
|
||||||
|
|
|
@ -5,98 +5,98 @@ const fs = require('fs');
|
||||||
let hasWarned = false;
|
let hasWarned = false;
|
||||||
|
|
||||||
const getConversion = {
|
const getConversion = {
|
||||||
text: (x) => x,
|
text: (x) => x,
|
||||||
json: (x) => JSON.parse(x),
|
json: (x) => JSON.parse(x),
|
||||||
arrayBuffer: (x) => Buffer.from(x).buffer,
|
arrayBuffer: (x) => Buffer.from(x).buffer,
|
||||||
stream: (x) => Buffer.from(x),
|
stream: (x) => Buffer.from(x),
|
||||||
};
|
};
|
||||||
|
|
||||||
class KVShim {
|
class KVShim {
|
||||||
constructor(namespace) {
|
constructor(namespace) {
|
||||||
this.namespace = namespace;
|
this.namespace = namespace;
|
||||||
|
|
||||||
fs.mkdirSync(path.resolve(__dirname, '../../.devdbs'), {
|
fs.mkdirSync(path.resolve(__dirname, '../../.devdbs'), {
|
||||||
recursive: true,
|
recursive: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
this.level = level(path.resolve(__dirname, '../../.devdbs', namespace));
|
this.level = level(path.resolve(__dirname, '../../.devdbs', namespace));
|
||||||
})();
|
})();
|
||||||
|
}
|
||||||
|
|
||||||
|
makeValue(value, expirationTtl) {
|
||||||
|
if (!expirationTtl) {
|
||||||
|
return JSON.stringify({
|
||||||
|
value,
|
||||||
|
expires: false,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
makeValue(value, expirationTtl) {
|
return JSON.stringify({
|
||||||
if (!expirationTtl) {
|
value,
|
||||||
return JSON.stringify({
|
expires: Date.now() + 1000 * expirationTtl,
|
||||||
value,
|
});
|
||||||
expires: false,
|
}
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return JSON.stringify({
|
validate(value) {
|
||||||
value,
|
if (!value) {
|
||||||
expires: Date.now() + 1000 * expirationTtl,
|
return false;
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
validate(value) {
|
if (value.expires && value.expires < Date.now()) {
|
||||||
if (!value) {
|
return false;
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (value.expires && value.expires < Date.now()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async get(key, type = 'text') {
|
return true;
|
||||||
try {
|
}
|
||||||
const result = JSON.parse(await this.level.get(key));
|
|
||||||
|
|
||||||
if (!this.validate(result)) {
|
async get(key, type = 'text') {
|
||||||
return null;
|
try {
|
||||||
}
|
const result = JSON.parse(await this.level.get(key));
|
||||||
|
|
||||||
return getConversion[type](result.value);
|
if (!this.validate(result)) {
|
||||||
} catch (e) {
|
return null;
|
||||||
return null;
|
}
|
||||||
}
|
|
||||||
|
return getConversion[type](result.value);
|
||||||
|
} catch (e) {
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async getWithMetadata(key, type) {
|
async getWithMetadata(key, type) {
|
||||||
return {
|
return {
|
||||||
value: await this.get(key, type),
|
value: await this.get(key, type),
|
||||||
metadata: {},
|
metadata: {},
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async put(key, value, { expirationTtl, expiration, metadata }) {
|
|
||||||
if ((expiration || metadata) && !hasWarned) {
|
|
||||||
console.warn(
|
|
||||||
'expiration and metadata is lost in the emulator. Use expirationTtl, please.'
|
|
||||||
);
|
|
||||||
hasWarned = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return await this.level.put(key, this.makeValue(value, expirationTtl));
|
|
||||||
}
|
|
||||||
|
|
||||||
// This loses scope for some unknown reason
|
|
||||||
delete = async (key) => {
|
|
||||||
return this.level.del(key);
|
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|
||||||
list() {
|
async put(key, value, { expirationTtl, expiration, metadata }) {
|
||||||
console.warn('List is frowned upon and will fail to fetch keys in the emulator.');
|
if ((expiration || metadata) && !hasWarned) {
|
||||||
return {
|
console.warn(
|
||||||
keys: [],
|
'expiration and metadata is lost in the emulator. Use expirationTtl, please.'
|
||||||
cursor: '0',
|
);
|
||||||
list_complete: true,
|
hasWarned = true;
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return await this.level.put(key, this.makeValue(value, expirationTtl));
|
||||||
|
}
|
||||||
|
|
||||||
|
// This loses scope for some unknown reason
|
||||||
|
delete = async (key) => {
|
||||||
|
return this.level.del(key);
|
||||||
|
};
|
||||||
|
|
||||||
|
list() {
|
||||||
|
console.warn('List is frowned upon and will fail to fetch keys in the emulator.');
|
||||||
|
return {
|
||||||
|
keys: [],
|
||||||
|
cursor: '0',
|
||||||
|
list_complete: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
KVShim,
|
KVShim,
|
||||||
};
|
};
|
||||||
|
|
|
@ -13,17 +13,17 @@ const args = require('minimist')(process.argv.slice(2));
|
||||||
|
|
||||||
const basePath = args.basePath;
|
const basePath = args.basePath;
|
||||||
if (!basePath) {
|
if (!basePath) {
|
||||||
throw new Error('--basePath is not set.');
|
throw new Error('--basePath is not set.');
|
||||||
}
|
}
|
||||||
|
|
||||||
const workerConfig = require(`${basePath}/worker.config.js`);
|
const workerConfig = require(`${basePath}/worker.config.js`);
|
||||||
|
|
||||||
const getKVs = (namespaces = []) =>
|
const getKVs = (namespaces = []) =>
|
||||||
namespaces.reduce((acc, ns) => ({ ...acc, [ns]: new KVShim(ns) }), {});
|
namespaces.reduce((acc, ns) => ({ ...acc, [ns]: new KVShim(ns) }), {});
|
||||||
|
|
||||||
const workerShims = {
|
const workerShims = {
|
||||||
...workerConfig.environment,
|
...workerConfig.environment,
|
||||||
...getKVs(workerConfig.kv),
|
...getKVs(workerConfig.kv),
|
||||||
};
|
};
|
||||||
|
|
||||||
let listeners = [];
|
let listeners = [];
|
||||||
|
@ -35,160 +35,158 @@ let isResponseConstructorAllowed = false;
|
||||||
* Cloudflare will reject all Response objects that aren't created during a request, so no pre-generation is allowed.
|
* Cloudflare will reject all Response objects that aren't created during a request, so no pre-generation is allowed.
|
||||||
*/
|
*/
|
||||||
class SafeResponse extends fetch.Response {
|
class SafeResponse extends fetch.Response {
|
||||||
constructor(...args) {
|
constructor(...args) {
|
||||||
super(...args);
|
super(...args);
|
||||||
|
|
||||||
if (!isResponseConstructorAllowed) {
|
if (!isResponseConstructorAllowed) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
'Response object created outside of request context. This will be rejected by Cloudflare.'
|
'Response object created outside of request context. This will be rejected by Cloudflare.'
|
||||||
);
|
);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const context = () =>
|
const context = () =>
|
||||||
vm.createContext(
|
vm.createContext(
|
||||||
{
|
{
|
||||||
addEventListener: (a, fn) => {
|
addEventListener: (a, fn) => {
|
||||||
if (a === 'fetch') {
|
if (a === 'fetch') {
|
||||||
console.log('addEventListeners: added fetch');
|
console.log('addEventListeners: added fetch');
|
||||||
listeners.push(fn);
|
listeners.push(fn);
|
||||||
}
|
|
||||||
},
|
|
||||||
Response: SafeResponse,
|
|
||||||
URL: URL,
|
|
||||||
crypto: crypto,
|
|
||||||
setTimeout: setTimeout,
|
|
||||||
setInterval: setInterval,
|
|
||||||
clearInterval: clearInterval,
|
|
||||||
clearTimeout: clearTimeout,
|
|
||||||
fetch: fetch,
|
|
||||||
console: console,
|
|
||||||
...workerShims,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
codeGeneration: {
|
|
||||||
strings: false,
|
|
||||||
wasm: false,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
);
|
},
|
||||||
|
Response: SafeResponse,
|
||||||
|
URL: URL,
|
||||||
|
crypto: crypto,
|
||||||
|
setTimeout: setTimeout,
|
||||||
|
setInterval: setInterval,
|
||||||
|
clearInterval: clearInterval,
|
||||||
|
clearTimeout: clearTimeout,
|
||||||
|
fetch: fetch,
|
||||||
|
console: console,
|
||||||
|
...workerShims,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
codeGeneration: {
|
||||||
|
strings: false,
|
||||||
|
wasm: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
const server = http.createServer((req, res) => {
|
const server = http.createServer((req, res) => {
|
||||||
const event = {
|
const event = {
|
||||||
respondWith: async (value) => {
|
respondWith: async (value) => {
|
||||||
const timeStart = Date.now();
|
const timeStart = Date.now();
|
||||||
let loggedStatus;
|
let loggedStatus;
|
||||||
try {
|
try {
|
||||||
const response = await value;
|
const response = await value;
|
||||||
if (!response) {
|
if (!response) {
|
||||||
throw new Error(
|
throw new Error(`response was invalid, got ${JSON.stringify(response)}`);
|
||||||
`response was invalid, got ${JSON.stringify(response)}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
res.statusCode = response.status;
|
|
||||||
loggedStatus = String(response.status);
|
|
||||||
response.headers.forEach((value, key) => res.setHeader(key, value));
|
|
||||||
res.end(response.body);
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
res.statusCode = 500;
|
|
||||||
loggedStatus = '500';
|
|
||||||
res.end(JSON.stringify({ error: 'internal server error' }));
|
|
||||||
}
|
|
||||||
const timeEnd = Date.now();
|
|
||||||
console.log(
|
|
||||||
`${loggedStatus} [${timeEnd - timeStart}ms] - ${req.method} ${req.url}`
|
|
||||||
);
|
|
||||||
isResponseConstructorAllowed = false;
|
|
||||||
},
|
|
||||||
request: new fetch.Request(
|
|
||||||
new URL(`http://${req.headers.host || 'localhost'}${req.url}`),
|
|
||||||
{
|
|
||||||
body: ['GET', 'HEAD'].includes(req.method) ? undefined : req,
|
|
||||||
headers: req.headers,
|
|
||||||
method: req.method,
|
|
||||||
}
|
|
||||||
),
|
|
||||||
};
|
|
||||||
|
|
||||||
event.request.headers.set('cf-client-ip', req.connection.remoteAddress);
|
|
||||||
|
|
||||||
if (listeners.length === 0) {
|
|
||||||
res.statusCode = 503;
|
|
||||||
res.end('No handlers are available.');
|
|
||||||
console.error('No handlers are available');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
isResponseConstructorAllowed = true;
|
|
||||||
for (let listener of listeners) {
|
|
||||||
try {
|
|
||||||
listener(event);
|
|
||||||
} catch (e) {
|
|
||||||
console.error('listener errored', e);
|
|
||||||
}
|
}
|
||||||
|
res.statusCode = response.status;
|
||||||
|
loggedStatus = String(response.status);
|
||||||
|
response.headers.forEach((value, key) => res.setHeader(key, value));
|
||||||
|
res.end(response.body);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
res.statusCode = 500;
|
||||||
|
loggedStatus = '500';
|
||||||
|
res.end(JSON.stringify({ error: 'internal server error' }));
|
||||||
|
}
|
||||||
|
const timeEnd = Date.now();
|
||||||
|
console.log(
|
||||||
|
`${loggedStatus} [${timeEnd - timeStart}ms] - ${req.method} ${req.url}`
|
||||||
|
);
|
||||||
|
isResponseConstructorAllowed = false;
|
||||||
|
},
|
||||||
|
request: new fetch.Request(
|
||||||
|
new URL(`http://${req.headers.host || 'localhost'}${req.url}`),
|
||||||
|
{
|
||||||
|
body: ['GET', 'HEAD'].includes(req.method) ? undefined : req,
|
||||||
|
headers: req.headers,
|
||||||
|
method: req.method,
|
||||||
|
}
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
event.request.headers.set('cf-client-ip', req.connection.remoteAddress);
|
||||||
|
|
||||||
|
if (listeners.length === 0) {
|
||||||
|
res.statusCode = 503;
|
||||||
|
res.end('No handlers are available.');
|
||||||
|
console.error('No handlers are available');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
isResponseConstructorAllowed = true;
|
||||||
|
for (let listener of listeners) {
|
||||||
|
try {
|
||||||
|
listener(event);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('listener errored', e);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const fork = async (fn) => fn();
|
const fork = async (fn) => fn();
|
||||||
|
|
||||||
const reload = () => {
|
const reload = () => {
|
||||||
// Clear listeners...
|
// Clear listeners...
|
||||||
listeners = [];
|
listeners = [];
|
||||||
|
|
||||||
// Fork and re-run
|
// Fork and re-run
|
||||||
fork(async () =>
|
fork(async () =>
|
||||||
vm.runInContext(
|
vm.runInContext(
|
||||||
fs.readFileSync(path.resolve(__dirname, `${basePath}/dist/worker.js`)),
|
fs.readFileSync(path.resolve(__dirname, `${basePath}/dist/worker.js`)),
|
||||||
context(),
|
context(),
|
||||||
{
|
{
|
||||||
displayErrors: true,
|
displayErrors: true,
|
||||||
filename: 'worker.js',
|
filename: 'worker.js',
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const rebuild = () =>
|
const rebuild = () =>
|
||||||
new Promise((resolve, reject) => {
|
new Promise((resolve, reject) => {
|
||||||
const webpackConfig = require(`${basePath}/webpack.config.js`);
|
const webpackConfig = require(`${basePath}/webpack.config.js`);
|
||||||
webpackConfig.output.filename = 'worker.js';
|
webpackConfig.output.filename = 'worker.js';
|
||||||
webpack(webpackConfig).run((err, stats) => {
|
webpack(webpackConfig).run((err, stats) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
console.log('Compilation failed.', err);
|
console.log('Compilation failed.', err);
|
||||||
reject(err);
|
reject(err);
|
||||||
} else {
|
} else {
|
||||||
if (stats.hasErrors()) {
|
if (stats.hasErrors()) {
|
||||||
console.error('Compilation errored:', stats.compilation.errors);
|
console.error('Compilation errored:', stats.compilation.errors);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('Compilation done.');
|
console.log('Compilation done.');
|
||||||
resolve();
|
resolve();
|
||||||
}
|
}
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
const watcher = chokidar.watch(path.resolve(__dirname, basePath), {
|
const watcher = chokidar.watch(path.resolve(__dirname, basePath), {
|
||||||
ignoreInitial: true,
|
ignoreInitial: true,
|
||||||
ignore: '**/dist',
|
ignore: '**/dist',
|
||||||
});
|
});
|
||||||
|
|
||||||
watcher.on('all', async (type, path) => {
|
watcher.on('all', async (type, path) => {
|
||||||
if (path.includes('dist')) {
|
if (path.includes('dist')) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('change detected, rebuilding and reloading', { type, path });
|
console.log('change detected, rebuilding and reloading', { type, path });
|
||||||
|
|
||||||
await rebuild();
|
await rebuild();
|
||||||
reload();
|
reload();
|
||||||
});
|
});
|
||||||
|
|
||||||
fork(async () => {
|
fork(async () => {
|
||||||
await rebuild();
|
await rebuild();
|
||||||
reload();
|
reload();
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log('starting on http://localhost:6609');
|
console.log('starting on http://localhost:6609');
|
||||||
|
|
|
@ -1,17 +1,17 @@
|
||||||
{
|
{
|
||||||
"name": "@roleypoly/worker-emulator",
|
"name": "@roleypoly/worker-emulator",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "node main.js --build",
|
"build": "node main.js --build",
|
||||||
"start": "node main.js"
|
"start": "node main.js"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@peculiar/webcrypto": "^1.1.6",
|
"@peculiar/webcrypto": "^1.1.6",
|
||||||
"chokidar": "^3.5.1",
|
"chokidar": "^3.5.1",
|
||||||
"dotenv": "^8.2.0",
|
"dotenv": "^8.2.0",
|
||||||
"level": "^6.0.1",
|
"level": "^6.0.1",
|
||||||
"minimist": "^1.2.5",
|
"minimist": "^1.2.5",
|
||||||
"node-fetch": "^2.6.1",
|
"node-fetch": "^2.6.1",
|
||||||
"webpack": "^4.x"
|
"webpack": "^4.x"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
module.exports = {
|
module.exports = {
|
||||||
stories: ['../**/*.stories.mdx', '../**/*.stories.@(js|jsx|ts|tsx)'],
|
stories: ['../**/*.stories.mdx', '../**/*.stories.@(js|jsx|ts|tsx)'],
|
||||||
addons: ['@storybook/addon-links', '@storybook/addon-essentials'],
|
addons: ['@storybook/addon-links', '@storybook/addon-essentials'],
|
||||||
};
|
};
|
||||||
|
|
|
@ -2,5 +2,5 @@ import { addons } from '@storybook/addons';
|
||||||
import { roleypolyTheme } from './theme';
|
import { roleypolyTheme } from './theme';
|
||||||
|
|
||||||
addons.setConfig({
|
addons.setConfig({
|
||||||
theme: roleypolyTheme,
|
theme: roleypolyTheme,
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
};
|
};
|
||||||
const Link = (props: Props) => <>{props.children}</>;
|
const Link = (props: Props) => <>{props.children}</>;
|
||||||
|
|
||||||
|
|
|
@ -2,9 +2,9 @@ import { roleypolyTheme } from './theme';
|
||||||
import { mdxComponents } from '../atoms/typography/mdx';
|
import { mdxComponents } from '../atoms/typography/mdx';
|
||||||
|
|
||||||
export const parameters = {
|
export const parameters = {
|
||||||
actions: { argTypesRegex: '^on[A-Z].*' },
|
actions: { argTypesRegex: '^on[A-Z].*' },
|
||||||
docs: {
|
docs: {
|
||||||
theme: roleypolyTheme,
|
theme: roleypolyTheme,
|
||||||
components: mdxComponents,
|
components: mdxComponents,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -2,33 +2,33 @@ import { create } from '@storybook/theming';
|
||||||
import { palette } from '../atoms/colors';
|
import { palette } from '../atoms/colors';
|
||||||
|
|
||||||
export const roleypolyTheme = create({
|
export const roleypolyTheme = create({
|
||||||
base: 'dark',
|
base: 'dark',
|
||||||
|
|
||||||
colorPrimary: palette.green400,
|
colorPrimary: palette.green400,
|
||||||
colorSecondary: palette.taupe200,
|
colorSecondary: palette.taupe200,
|
||||||
|
|
||||||
// UI
|
// UI
|
||||||
appBg: palette.taupe300,
|
appBg: palette.taupe300,
|
||||||
appContentBg: palette.taupe200,
|
appContentBg: palette.taupe200,
|
||||||
appBorderColor: palette.taupe100,
|
appBorderColor: palette.taupe100,
|
||||||
appBorderRadius: 0,
|
appBorderRadius: 0,
|
||||||
|
|
||||||
// Typography
|
// Typography
|
||||||
fontBase: 'system-ui, sans-serif',
|
fontBase: 'system-ui, sans-serif',
|
||||||
fontCode: 'monospace',
|
fontCode: 'monospace',
|
||||||
|
|
||||||
// Text colors
|
// Text colors
|
||||||
textColor: palette.grey600,
|
textColor: palette.grey600,
|
||||||
textInverseColor: palette.grey100,
|
textInverseColor: palette.grey100,
|
||||||
|
|
||||||
// Toolbar default and active colors
|
// Toolbar default and active colors
|
||||||
barTextColor: palette.taupe500,
|
barTextColor: palette.taupe500,
|
||||||
barSelectedColor: palette.taupe600,
|
barSelectedColor: palette.taupe600,
|
||||||
barBg: palette.taupe100,
|
barBg: palette.taupe100,
|
||||||
|
|
||||||
// Form colors
|
// Form colors
|
||||||
inputBg: 'rgba(0,0,0,0.24)',
|
inputBg: 'rgba(0,0,0,0.24)',
|
||||||
inputBorder: palette.taupe100,
|
inputBorder: palette.taupe100,
|
||||||
inputTextColor: palette.grey600,
|
inputTextColor: palette.grey600,
|
||||||
inputBorderRadius: 0,
|
inputBorderRadius: 0,
|
||||||
});
|
});
|
||||||
|
|
|
@ -2,33 +2,33 @@ import * as React from 'react';
|
||||||
import { Avatar, AvatarProps } from './Avatar';
|
import { Avatar, AvatarProps } from './Avatar';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
title: 'Atoms/Avatar',
|
title: 'Atoms/Avatar',
|
||||||
component: Avatar,
|
component: Avatar,
|
||||||
argTypes: {
|
argTypes: {
|
||||||
initials: { control: 'text' },
|
initials: { control: 'text' },
|
||||||
},
|
},
|
||||||
args: {
|
args: {
|
||||||
initials: 'KR',
|
initials: 'KR',
|
||||||
hash: 'aa',
|
hash: 'aa',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
type StoryArgs = {
|
type StoryArgs = {
|
||||||
initials?: string;
|
initials?: string;
|
||||||
} & AvatarProps;
|
} & AvatarProps;
|
||||||
|
|
||||||
export const WithInitials = ({ initials, ...rest }: StoryArgs) => (
|
export const WithInitials = ({ initials, ...rest }: StoryArgs) => (
|
||||||
<Avatar src="https://i.imgur.com/epMSRQH.png" size={48} {...rest}>
|
<Avatar src="https://i.imgur.com/epMSRQH.png" size={48} {...rest}>
|
||||||
{initials}
|
{initials}
|
||||||
</Avatar>
|
</Avatar>
|
||||||
);
|
);
|
||||||
|
|
||||||
export const WithText = ({ initials, ...rest }: StoryArgs) => (
|
export const WithText = ({ initials, ...rest }: StoryArgs) => (
|
||||||
<Avatar size={48} {...rest}>
|
<Avatar size={48} {...rest}>
|
||||||
{initials}
|
{initials}
|
||||||
</Avatar>
|
</Avatar>
|
||||||
);
|
);
|
||||||
export const Empty = (args: StoryArgs) => <Avatar size={48} {...args}></Avatar>;
|
export const Empty = (args: StoryArgs) => <Avatar size={48} {...args}></Avatar>;
|
||||||
export const DeliberatelyEmpty = (args: StoryArgs) => (
|
export const DeliberatelyEmpty = (args: StoryArgs) => (
|
||||||
<Avatar size={48} deliberatelyEmpty={true} {...args}></Avatar>
|
<Avatar size={48} deliberatelyEmpty={true} {...args}></Avatar>
|
||||||
);
|
);
|
||||||
|
|
|
@ -4,41 +4,41 @@ import { AvatarProps } from './Avatar';
|
||||||
|
|
||||||
type ContainerProps = Pick<AvatarProps, 'size'> & Pick<AvatarProps, 'deliberatelyEmpty'>;
|
type ContainerProps = Pick<AvatarProps, 'size'> & Pick<AvatarProps, 'deliberatelyEmpty'>;
|
||||||
export const Container = styled.div<ContainerProps>`
|
export const Container = styled.div<ContainerProps>`
|
||||||
border-radius: 100%;
|
border-radius: 100%;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
width: ${(props: ContainerProps) => props.size || 48}px;
|
width: ${(props: ContainerProps) => props.size || 48}px;
|
||||||
height: ${(props: ContainerProps) => props.size || 48}px;
|
height: ${(props: ContainerProps) => props.size || 48}px;
|
||||||
min-width: ${(props: ContainerProps) => props.size || 48}px;
|
min-width: ${(props: ContainerProps) => props.size || 48}px;
|
||||||
min-height: ${(props: ContainerProps) => props.size || 48}px;
|
min-height: ${(props: ContainerProps) => props.size || 48}px;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
color: ${palette.grey100};
|
color: ${palette.grey100};
|
||||||
position: relative;
|
position: relative;
|
||||||
background-color: ${palette.grey500};
|
background-color: ${palette.grey500};
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
font-size: ${(props: ContainerProps) => props.size};
|
font-size: ${(props: ContainerProps) => props.size};
|
||||||
${(props) =>
|
${(props) =>
|
||||||
props.deliberatelyEmpty &&
|
props.deliberatelyEmpty &&
|
||||||
css`
|
css`
|
||||||
border: 4px solid rgba(0, 0, 0, 0.25);
|
border: 4px solid rgba(0, 0, 0, 0.25);
|
||||||
background-color: ${palette.taupe400};
|
background-color: ${palette.taupe400};
|
||||||
color: ${palette.taupe600};
|
color: ${palette.taupe600};
|
||||||
`}
|
`}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
type ImageProps = Pick<AvatarProps, 'src'>;
|
type ImageProps = Pick<AvatarProps, 'src'>;
|
||||||
export const Image = styled.div<ImageProps>`
|
export const Image = styled.div<ImageProps>`
|
||||||
background-size: cover;
|
background-size: cover;
|
||||||
background-repeat: no-repeat;
|
background-repeat: no-repeat;
|
||||||
background-position: 50% 50%;
|
background-position: 50% 50%;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
border-radius: 100%;
|
border-radius: 100%;
|
||||||
`;
|
`;
|
||||||
|
|
|
@ -2,28 +2,28 @@ import React from 'react';
|
||||||
import { Container, Image } from './Avatar.styled';
|
import { Container, Image } from './Avatar.styled';
|
||||||
|
|
||||||
export type AvatarProps = {
|
export type AvatarProps = {
|
||||||
src?: string;
|
src?: string;
|
||||||
children?: string | React.ReactNode;
|
children?: string | React.ReactNode;
|
||||||
size?: number;
|
size?: number;
|
||||||
hash?: string;
|
hash?: string;
|
||||||
deliberatelyEmpty?: boolean;
|
deliberatelyEmpty?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Chuldren is recommended to not be larger than 2 uppercase letters. */
|
/** Chuldren is recommended to not be larger than 2 uppercase letters. */
|
||||||
export const Avatar = (props: AvatarProps) => (
|
export const Avatar = (props: AvatarProps) => (
|
||||||
<Container size={props.size} deliberatelyEmpty={props.deliberatelyEmpty}>
|
<Container size={props.size} deliberatelyEmpty={props.deliberatelyEmpty}>
|
||||||
{props.src && props.hash && (
|
{props.src && props.hash && (
|
||||||
<Image
|
<Image
|
||||||
style={{
|
style={{
|
||||||
backgroundImage: `url(${props.src})`,
|
backgroundImage: `url(${props.src})`,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<div>
|
<div>
|
||||||
{props.children || (
|
{props.children || (
|
||||||
/* needs specifically to prevent layout issues. */
|
/* needs specifically to prevent layout issues. */
|
||||||
<> </>
|
<> </>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</Container>
|
</Container>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,16 +1,16 @@
|
||||||
export const initialsFromName = (name: string) =>
|
export const initialsFromName = (name: string) =>
|
||||||
!!name
|
!!name
|
||||||
? name
|
? name
|
||||||
.split(' ')
|
.split(' ')
|
||||||
.slice(0, 2)
|
.slice(0, 2)
|
||||||
.map((x) => x[0])
|
.map((x) => x[0])
|
||||||
.join('')
|
.join('')
|
||||||
.toUpperCase()
|
.toUpperCase()
|
||||||
: '';
|
: '';
|
||||||
|
|
||||||
export const avatarHash = (
|
export const avatarHash = (
|
||||||
id: string,
|
id: string,
|
||||||
hash: string,
|
hash: string,
|
||||||
bucket: 'icons' | 'avatars' = 'icons',
|
bucket: 'icons' | 'avatars' = 'icons',
|
||||||
size: number = 256
|
size: number = 256
|
||||||
) => `https://cdn.discordapp.com/${bucket}/${id}/${hash}.webp?size=${size}`;
|
) => `https://cdn.discordapp.com/${bucket}/${id}/${hash}.webp?size=${size}`;
|
||||||
|
|
|
@ -4,22 +4,22 @@ import { palette } from '../colors';
|
||||||
import { Logomark as BrandingLogomark, Logotype as BrandingLogotype } from './Branding';
|
import { Logomark as BrandingLogomark, Logotype as BrandingLogotype } from './Branding';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
title: 'Atoms/Branding',
|
title: 'Atoms/Branding',
|
||||||
};
|
};
|
||||||
|
|
||||||
const Wrapper = styled.div`
|
const Wrapper = styled.div`
|
||||||
background-color: ${palette.taupe100};
|
background-color: ${palette.taupe100};
|
||||||
padding: 2em;
|
padding: 2em;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const Logomark = () => (
|
export const Logomark = () => (
|
||||||
<Wrapper>
|
<Wrapper>
|
||||||
<BrandingLogomark />
|
<BrandingLogomark />
|
||||||
</Wrapper>
|
</Wrapper>
|
||||||
);
|
);
|
||||||
|
|
||||||
export const Logotype = () => (
|
export const Logotype = () => (
|
||||||
<Wrapper>
|
<Wrapper>
|
||||||
<BrandingLogotype />
|
<BrandingLogotype />
|
||||||
</Wrapper>
|
</Wrapper>
|
||||||
);
|
);
|
||||||
|
|
|
@ -2,153 +2,153 @@ import { palette } from '@roleypoly/design-system/atoms/colors';
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
|
|
||||||
export type LogoProps = {
|
export type LogoProps = {
|
||||||
fill: string;
|
fill: string;
|
||||||
width: number;
|
width: number;
|
||||||
height: number;
|
height: number;
|
||||||
circleFill: string;
|
circleFill: string;
|
||||||
circleOuterFill: string;
|
circleOuterFill: string;
|
||||||
typeFill: string;
|
typeFill: string;
|
||||||
style: object;
|
style: object;
|
||||||
className: string;
|
className: string;
|
||||||
'data-for'?: string;
|
'data-for'?: string;
|
||||||
'data-tip'?: string;
|
'data-tip'?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Logotype = ({
|
export const Logotype = ({
|
||||||
typeFill = palette.taupe400,
|
typeFill = palette.taupe400,
|
||||||
circleFill = palette.red200,
|
circleFill = palette.red200,
|
||||||
circleOuterFill = palette.green200,
|
circleOuterFill = palette.green200,
|
||||||
...props
|
...props
|
||||||
}: Partial<LogoProps>) => (
|
}: Partial<LogoProps>) => (
|
||||||
<svg
|
<svg
|
||||||
style={props.style}
|
style={props.style}
|
||||||
className={props.className}
|
className={props.className}
|
||||||
width={props.width}
|
width={props.width}
|
||||||
height={props.height}
|
height={props.height}
|
||||||
data-for={props['data-for']}
|
data-for={props['data-for']}
|
||||||
data-tip={props['data-tip']}
|
data-tip={props['data-tip']}
|
||||||
viewBox="45 25 400 88"
|
viewBox="45 25 400 88"
|
||||||
fill="none"
|
fill="none"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
>
|
>
|
||||||
<g clipPath="url(#clip0)">
|
<g clipPath="url(#clip0)">
|
||||||
<path
|
<path
|
||||||
d="M179.855 95.49V96H170.845L154.95 74.495H146.79V96H138.97V40.92H156.905C161.212 40.92 164.838 41.6 167.785 42.96C170.788 44.32 173.027 46.2183 174.5 48.655C176.03 51.0917 176.795 53.925 176.795 57.155C176.795 59.7617 176.285 62.17 175.265 64.38C174.302 66.59 172.828 68.5167 170.845 70.16C168.862 71.7467 166.397 72.9083 163.45 73.645L179.855 95.49ZM146.79 68.035H155.715C159.965 68.035 163.167 67.1283 165.32 65.315C167.53 63.445 168.635 60.8383 168.635 57.495C168.635 54.265 167.615 51.8 165.575 50.1C163.592 48.4 160.645 47.55 156.735 47.55H146.79V68.035Z"
|
d="M179.855 95.49V96H170.845L154.95 74.495H146.79V96H138.97V40.92H156.905C161.212 40.92 164.838 41.6 167.785 42.96C170.788 44.32 173.027 46.2183 174.5 48.655C176.03 51.0917 176.795 53.925 176.795 57.155C176.795 59.7617 176.285 62.17 175.265 64.38C174.302 66.59 172.828 68.5167 170.845 70.16C168.862 71.7467 166.397 72.9083 163.45 73.645L179.855 95.49ZM146.79 68.035H155.715C159.965 68.035 163.167 67.1283 165.32 65.315C167.53 63.445 168.635 60.8383 168.635 57.495C168.635 54.265 167.615 51.8 165.575 50.1C163.592 48.4 160.645 47.55 156.735 47.55H146.79V68.035Z"
|
||||||
fill={typeFill}
|
fill={typeFill}
|
||||||
/>
|
/>
|
||||||
<path
|
<path
|
||||||
d="M197.154 97.02C193.188 97.02 189.873 96.17 187.209 94.47C184.546 92.7133 182.563 90.3617 181.259 87.415C179.956 84.4117 179.304 81.04 179.304 77.3C179.304 72.7667 180.154 69.055 181.854 66.165C183.554 63.2183 185.849 61.0933 188.739 59.79C191.629 58.43 194.916 57.75 198.599 57.75C202.566 57.75 205.881 58.6283 208.544 60.385C211.208 62.085 213.191 64.4083 214.494 67.355C215.798 70.3017 216.449 73.645 216.449 77.385C216.449 81.975 215.599 85.7433 213.899 88.69C212.199 91.58 209.904 93.705 207.014 95.065C204.124 96.3683 200.838 97.02 197.154 97.02ZM197.834 91.07C205.144 91.07 208.799 86.48 208.799 77.3C208.799 73.1633 207.893 69.8767 206.079 67.44C204.323 64.9467 201.659 63.7 198.089 63.7C194.123 63.7 191.261 64.9183 189.504 67.355C187.804 69.735 186.954 73.05 186.954 77.3C186.954 81.4933 187.861 84.8367 189.674 87.33C191.488 89.8233 194.208 91.07 197.834 91.07Z"
|
d="M197.154 97.02C193.188 97.02 189.873 96.17 187.209 94.47C184.546 92.7133 182.563 90.3617 181.259 87.415C179.956 84.4117 179.304 81.04 179.304 77.3C179.304 72.7667 180.154 69.055 181.854 66.165C183.554 63.2183 185.849 61.0933 188.739 59.79C191.629 58.43 194.916 57.75 198.599 57.75C202.566 57.75 205.881 58.6283 208.544 60.385C211.208 62.085 213.191 64.4083 214.494 67.355C215.798 70.3017 216.449 73.645 216.449 77.385C216.449 81.975 215.599 85.7433 213.899 88.69C212.199 91.58 209.904 93.705 207.014 95.065C204.124 96.3683 200.838 97.02 197.154 97.02ZM197.834 91.07C205.144 91.07 208.799 86.48 208.799 77.3C208.799 73.1633 207.893 69.8767 206.079 67.44C204.323 64.9467 201.659 63.7 198.089 63.7C194.123 63.7 191.261 64.9183 189.504 67.355C187.804 69.735 186.954 73.05 186.954 77.3C186.954 81.4933 187.861 84.8367 189.674 87.33C191.488 89.8233 194.208 91.07 197.834 91.07Z"
|
||||||
fill={typeFill}
|
fill={typeFill}
|
||||||
/>
|
/>
|
||||||
<path d="M221.961 96V37.52L229.271 36.67V96H221.961Z" fill={typeFill} />
|
<path d="M221.961 96V37.52L229.271 36.67V96H221.961Z" fill={typeFill} />
|
||||||
<path
|
<path
|
||||||
d="M268.08 78.66H242.155C242.212 82.91 243.232 85.9983 245.215 87.925C247.255 89.8517 249.862 90.815 253.035 90.815C255.302 90.815 257.257 90.5317 258.9 89.965C260.6 89.3983 262.47 88.52 264.51 87.33L267.315 92.515C262.669 95.5183 257.795 97.02 252.695 97.02C247.255 97.02 242.892 95.5183 239.605 92.515C236.375 89.455 234.76 84.6383 234.76 78.065C234.76 74.325 235.44 70.925 236.8 67.865C238.16 64.805 240.172 62.3683 242.835 60.555C245.555 58.685 248.87 57.75 252.78 57.75C256.067 57.75 258.872 58.43 261.195 59.79C263.519 61.0933 265.275 62.9067 266.465 65.23C267.655 67.5533 268.25 70.2167 268.25 73.22L268.08 78.66ZM252.78 63.445C249.834 63.445 247.51 64.3233 245.81 66.08C244.167 67.8367 243.09 70.245 242.58 73.305H261.11V72.965C261.11 69.7917 260.374 67.4117 258.9 65.825C257.484 64.2383 255.444 63.445 252.78 63.445Z"
|
d="M268.08 78.66H242.155C242.212 82.91 243.232 85.9983 245.215 87.925C247.255 89.8517 249.862 90.815 253.035 90.815C255.302 90.815 257.257 90.5317 258.9 89.965C260.6 89.3983 262.47 88.52 264.51 87.33L267.315 92.515C262.669 95.5183 257.795 97.02 252.695 97.02C247.255 97.02 242.892 95.5183 239.605 92.515C236.375 89.455 234.76 84.6383 234.76 78.065C234.76 74.325 235.44 70.925 236.8 67.865C238.16 64.805 240.172 62.3683 242.835 60.555C245.555 58.685 248.87 57.75 252.78 57.75C256.067 57.75 258.872 58.43 261.195 59.79C263.519 61.0933 265.275 62.9067 266.465 65.23C267.655 67.5533 268.25 70.2167 268.25 73.22L268.08 78.66ZM252.78 63.445C249.834 63.445 247.51 64.3233 245.81 66.08C244.167 67.8367 243.09 70.245 242.58 73.305H261.11V72.965C261.11 69.7917 260.374 67.4117 258.9 65.825C257.484 64.2383 255.444 63.445 252.78 63.445Z"
|
||||||
fill={typeFill}
|
fill={typeFill}
|
||||||
/>
|
/>
|
||||||
<path
|
<path
|
||||||
d="M305.129 58.77V59.28L290.934 98.635C289.687 102.092 288.356 104.84 286.939 106.88C285.522 108.977 283.907 110.478 282.094 111.385C280.337 112.348 278.241 112.83 275.804 112.83C273.311 112.83 270.846 112.405 268.409 111.555L270.364 105.945C272.007 106.455 273.622 106.71 275.209 106.71C276.512 106.71 277.617 106.512 278.524 106.115C279.431 105.718 280.309 104.953 281.159 103.82C282.066 102.687 282.944 100.987 283.794 98.72L284.814 96H282.859L268.579 59.28V58.77H276.314L285.069 82.57L286.004 85.12L287.364 89.115L289.489 82.57L297.649 58.77H305.129Z"
|
d="M305.129 58.77V59.28L290.934 98.635C289.687 102.092 288.356 104.84 286.939 106.88C285.522 108.977 283.907 110.478 282.094 111.385C280.337 112.348 278.241 112.83 275.804 112.83C273.311 112.83 270.846 112.405 268.409 111.555L270.364 105.945C272.007 106.455 273.622 106.71 275.209 106.71C276.512 106.71 277.617 106.512 278.524 106.115C279.431 105.718 280.309 104.953 281.159 103.82C282.066 102.687 282.944 100.987 283.794 98.72L284.814 96H282.859L268.579 59.28V58.77H276.314L285.069 82.57L286.004 85.12L287.364 89.115L289.489 82.57L297.649 58.77H305.129Z"
|
||||||
fill={typeFill}
|
fill={typeFill}
|
||||||
/>
|
/>
|
||||||
<path
|
<path
|
||||||
d="M327.505 57.75C331.018 57.75 333.936 58.6 336.26 60.3C338.583 62 340.283 64.2667 341.36 67.1C342.493 69.9333 343.06 73.0217 343.06 76.365C343.06 80.6717 342.21 84.3833 340.51 87.5C338.866 90.6167 336.6 92.9967 333.71 94.64C330.82 96.2267 327.533 97.02 323.85 97.02C321.073 97.02 318.381 96.6517 315.775 95.915V111.81H308.465V58.77L315.775 57.92V61.235C317.815 60.1017 319.826 59.2517 321.81 58.685C323.85 58.0617 325.748 57.75 327.505 57.75ZM323.255 91.24C325.578 91.24 327.618 90.73 329.375 89.71C331.188 88.6333 332.605 86.99 333.625 84.78C334.701 82.57 335.24 79.8217 335.24 76.535C335.24 72.285 334.39 69.14 332.69 67.1C330.99 65.06 328.581 64.04 325.465 64.04C322.178 64.04 318.948 64.975 315.775 66.845V89.965C318.438 90.815 320.931 91.24 323.255 91.24Z"
|
d="M327.505 57.75C331.018 57.75 333.936 58.6 336.26 60.3C338.583 62 340.283 64.2667 341.36 67.1C342.493 69.9333 343.06 73.0217 343.06 76.365C343.06 80.6717 342.21 84.3833 340.51 87.5C338.866 90.6167 336.6 92.9967 333.71 94.64C330.82 96.2267 327.533 97.02 323.85 97.02C321.073 97.02 318.381 96.6517 315.775 95.915V111.81H308.465V58.77L315.775 57.92V61.235C317.815 60.1017 319.826 59.2517 321.81 58.685C323.85 58.0617 325.748 57.75 327.505 57.75ZM323.255 91.24C325.578 91.24 327.618 90.73 329.375 89.71C331.188 88.6333 332.605 86.99 333.625 84.78C334.701 82.57 335.24 79.8217 335.24 76.535C335.24 72.285 334.39 69.14 332.69 67.1C330.99 65.06 328.581 64.04 325.465 64.04C322.178 64.04 318.948 64.975 315.775 66.845V89.965C318.438 90.815 320.931 91.24 323.255 91.24Z"
|
||||||
fill={typeFill}
|
fill={typeFill}
|
||||||
/>
|
/>
|
||||||
<path
|
<path
|
||||||
d="M363.436 97.02C359.469 97.02 356.154 96.17 353.491 94.47C350.827 92.7133 348.844 90.3617 347.541 87.415C346.237 84.4117 345.586 81.04 345.586 77.3C345.586 72.7667 346.436 69.055 348.136 66.165C349.836 63.2183 352.131 61.0933 355.021 59.79C357.911 58.43 361.197 57.75 364.881 57.75C368.847 57.75 372.162 58.6283 374.826 60.385C377.489 62.085 379.472 64.4083 380.776 67.355C382.079 70.3017 382.731 73.645 382.731 77.385C382.731 81.975 381.881 85.7433 380.181 88.69C378.481 91.58 376.186 93.705 373.296 95.065C370.406 96.3683 367.119 97.02 363.436 97.02ZM364.116 91.07C371.426 91.07 375.081 86.48 375.081 77.3C375.081 73.1633 374.174 69.8767 372.361 67.44C370.604 64.9467 367.941 63.7 364.371 63.7C360.404 63.7 357.542 64.9183 355.786 67.355C354.086 69.735 353.236 73.05 353.236 77.3C353.236 81.4933 354.142 84.8367 355.956 87.33C357.769 89.8233 360.489 91.07 364.116 91.07Z"
|
d="M363.436 97.02C359.469 97.02 356.154 96.17 353.491 94.47C350.827 92.7133 348.844 90.3617 347.541 87.415C346.237 84.4117 345.586 81.04 345.586 77.3C345.586 72.7667 346.436 69.055 348.136 66.165C349.836 63.2183 352.131 61.0933 355.021 59.79C357.911 58.43 361.197 57.75 364.881 57.75C368.847 57.75 372.162 58.6283 374.826 60.385C377.489 62.085 379.472 64.4083 380.776 67.355C382.079 70.3017 382.731 73.645 382.731 77.385C382.731 81.975 381.881 85.7433 380.181 88.69C378.481 91.58 376.186 93.705 373.296 95.065C370.406 96.3683 367.119 97.02 363.436 97.02ZM364.116 91.07C371.426 91.07 375.081 86.48 375.081 77.3C375.081 73.1633 374.174 69.8767 372.361 67.44C370.604 64.9467 367.941 63.7 364.371 63.7C360.404 63.7 357.542 64.9183 355.786 67.355C354.086 69.735 353.236 73.05 353.236 77.3C353.236 81.4933 354.142 84.8367 355.956 87.33C357.769 89.8233 360.489 91.07 364.116 91.07Z"
|
||||||
fill={typeFill}
|
fill={typeFill}
|
||||||
/>
|
/>
|
||||||
<path d="M388.242 96V37.52L395.552 36.67V96H388.242Z" fill={typeFill} />
|
<path d="M388.242 96V37.52L395.552 36.67V96H388.242Z" fill={typeFill} />
|
||||||
<path
|
<path
|
||||||
d="M435.382 58.77V59.28L421.187 98.635C419.94 102.092 418.608 104.84 417.192 106.88C415.775 108.977 414.16 110.478 412.347 111.385C410.59 112.348 408.493 112.83 406.057 112.83C403.563 112.83 401.098 112.405 398.662 111.555L400.617 105.945C402.26 106.455 403.875 106.71 405.462 106.71C406.765 106.71 407.87 106.512 408.777 106.115C409.683 105.718 410.562 104.953 411.412 103.82C412.318 102.687 413.197 100.987 414.047 98.72L415.067 96H413.112L398.832 59.28V58.77H406.567L415.322 82.57L416.257 85.12L417.617 89.115L419.742 82.57L427.902 58.77H435.382Z"
|
d="M435.382 58.77V59.28L421.187 98.635C419.94 102.092 418.608 104.84 417.192 106.88C415.775 108.977 414.16 110.478 412.347 111.385C410.59 112.348 408.493 112.83 406.057 112.83C403.563 112.83 401.098 112.405 398.662 111.555L400.617 105.945C402.26 106.455 403.875 106.71 405.462 106.71C406.765 106.71 407.87 106.512 408.777 106.115C409.683 105.718 410.562 104.953 411.412 103.82C412.318 102.687 413.197 100.987 414.047 98.72L415.067 96H413.112L398.832 59.28V58.77H406.567L415.322 82.57L416.257 85.12L417.617 89.115L419.742 82.57L427.902 58.77H435.382Z"
|
||||||
fill={typeFill}
|
fill={typeFill}
|
||||||
/>
|
/>
|
||||||
<mask
|
<mask
|
||||||
id="mask0"
|
id="mask0"
|
||||||
mask-type="alpha"
|
mask-type="alpha"
|
||||||
maskUnits="userSpaceOnUse"
|
maskUnits="userSpaceOnUse"
|
||||||
x="45"
|
x="45"
|
||||||
y="25"
|
y="25"
|
||||||
width="142"
|
width="142"
|
||||||
height="142"
|
height="142"
|
||||||
>
|
>
|
||||||
<rect
|
<rect
|
||||||
x="115.711"
|
x="115.711"
|
||||||
y="25"
|
y="25"
|
||||||
width="100"
|
width="100"
|
||||||
height="100"
|
height="100"
|
||||||
transform="rotate(45 115.711 25)"
|
transform="rotate(45 115.711 25)"
|
||||||
fill="#C4C4C4"
|
fill="#C4C4C4"
|
||||||
/>
|
/>
|
||||||
</mask>
|
</mask>
|
||||||
<g mask="url(#mask0)">
|
<g mask="url(#mask0)">
|
||||||
<circle
|
<circle
|
||||||
cx="79.9999"
|
cx="79.9999"
|
||||||
cy="60"
|
cy="60"
|
||||||
r="46"
|
r="46"
|
||||||
fill={circleFill}
|
fill={circleFill}
|
||||||
stroke={circleOuterFill}
|
stroke={circleOuterFill}
|
||||||
strokeWidth="8"
|
strokeWidth="8"
|
||||||
/>
|
/>
|
||||||
</g>
|
</g>
|
||||||
</g>
|
</g>
|
||||||
<defs>
|
<defs>
|
||||||
<clipPath id="clip0">
|
<clipPath id="clip0">
|
||||||
<rect width="487" height="143" fill="white" />
|
<rect width="487" height="143" fill="white" />
|
||||||
</clipPath>
|
</clipPath>
|
||||||
</defs>
|
</defs>
|
||||||
</svg>
|
</svg>
|
||||||
);
|
);
|
||||||
|
|
||||||
export const Logomark = ({
|
export const Logomark = ({
|
||||||
circleFill = palette.red200,
|
circleFill = palette.red200,
|
||||||
circleOuterFill = palette.green200,
|
circleOuterFill = palette.green200,
|
||||||
...props
|
...props
|
||||||
}: Partial<LogoProps>) => (
|
}: Partial<LogoProps>) => (
|
||||||
<svg
|
<svg
|
||||||
data-for={props['data-for']}
|
data-for={props['data-for']}
|
||||||
data-tip={props['data-tip']}
|
data-tip={props['data-tip']}
|
||||||
style={props.style}
|
style={props.style}
|
||||||
className={props.className}
|
className={props.className}
|
||||||
width={props.width}
|
width={props.width}
|
||||||
height={props.height}
|
height={props.height}
|
||||||
viewBox="30 10 100 100"
|
viewBox="30 10 100 100"
|
||||||
fill="none"
|
fill="none"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
>
|
>
|
||||||
<g clipPath="url(#clip0)">
|
<g clipPath="url(#clip0)">
|
||||||
<mask
|
<mask
|
||||||
id="mask0"
|
id="mask0"
|
||||||
mask-type="alpha"
|
mask-type="alpha"
|
||||||
maskUnits="userSpaceOnUse"
|
maskUnits="userSpaceOnUse"
|
||||||
x="45"
|
x="45"
|
||||||
y="25"
|
y="25"
|
||||||
width="142"
|
width="142"
|
||||||
height="142"
|
height="142"
|
||||||
>
|
>
|
||||||
<rect
|
<rect
|
||||||
x="115.711"
|
x="115.711"
|
||||||
y="25"
|
y="25"
|
||||||
width="100"
|
width="100"
|
||||||
height="100"
|
height="100"
|
||||||
transform="rotate(45 115.711 25)"
|
transform="rotate(45 115.711 25)"
|
||||||
fill="#C4C4C4"
|
fill="#C4C4C4"
|
||||||
/>
|
/>
|
||||||
</mask>
|
</mask>
|
||||||
<g mask="url(#mask0)">
|
<g mask="url(#mask0)">
|
||||||
<circle
|
<circle
|
||||||
cx="79.9999"
|
cx="79.9999"
|
||||||
cy="60"
|
cy="60"
|
||||||
r="46"
|
r="46"
|
||||||
fill={circleFill}
|
fill={circleFill}
|
||||||
stroke={circleOuterFill}
|
stroke={circleOuterFill}
|
||||||
strokeWidth="8"
|
strokeWidth="8"
|
||||||
/>
|
/>
|
||||||
</g>
|
</g>
|
||||||
</g>
|
</g>
|
||||||
<defs>
|
<defs>
|
||||||
<clipPath id="clip0">
|
<clipPath id="clip0">
|
||||||
<rect width="130" height="110" fill="white" />
|
<rect width="130" height="110" fill="white" />
|
||||||
</clipPath>
|
</clipPath>
|
||||||
</defs>
|
</defs>
|
||||||
</svg>
|
</svg>
|
||||||
);
|
);
|
||||||
|
|
File diff suppressed because one or more lines are too long
|
@ -7,72 +7,72 @@ import { Logomark, Logotype } from './Branding';
|
||||||
import { AllVariants, DynamicLogomark, DynamicLogotype } from './DynamicBranding';
|
import { AllVariants, DynamicLogomark, DynamicLogotype } from './DynamicBranding';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
title: 'Atoms/Branding/Dynamic',
|
title: 'Atoms/Branding/Dynamic',
|
||||||
component: DynamicLogotype,
|
component: DynamicLogotype,
|
||||||
};
|
};
|
||||||
|
|
||||||
const WrapperDiv = styled.div`
|
const WrapperDiv = styled.div`
|
||||||
background-color: ${palette.taupe100};
|
background-color: ${palette.taupe100};
|
||||||
padding: 2em;
|
padding: 2em;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const Wrapper = (props: { children: React.ReactNode }) => (
|
const Wrapper = (props: { children: React.ReactNode }) => (
|
||||||
<>
|
<>
|
||||||
<WrapperDiv>{props.children}</WrapperDiv>
|
<WrapperDiv>{props.children}</WrapperDiv>
|
||||||
<ReactTooltip />
|
<ReactTooltip />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
||||||
export const dynamicLogotype = (args) => {
|
export const dynamicLogotype = (args) => {
|
||||||
return (
|
return (
|
||||||
<Wrapper>
|
<Wrapper>
|
||||||
<DynamicLogotype {...args} />
|
<DynamicLogotype {...args} />
|
||||||
</Wrapper>
|
</Wrapper>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const dynamicLogomark = (args) => {
|
export const dynamicLogomark = (args) => {
|
||||||
return (
|
return (
|
||||||
<Wrapper>
|
<Wrapper>
|
||||||
<DynamicLogomark {...args} />
|
<DynamicLogomark {...args} />
|
||||||
</Wrapper>
|
</Wrapper>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const AllCustomizedLogotypes = () => {
|
export const AllCustomizedLogotypes = () => {
|
||||||
return (
|
return (
|
||||||
<Wrapper>
|
<Wrapper>
|
||||||
<div>
|
<div>
|
||||||
<Text>Base Logo</Text>
|
<Text>Base Logo</Text>
|
||||||
<br />
|
<br />
|
||||||
<Logotype height={50} />
|
<Logotype height={50} />
|
||||||
</div>
|
</div>
|
||||||
{AllVariants.map((variant, idx) => (
|
{AllVariants.map((variant, idx) => (
|
||||||
<div key={idx} style={{ marginTop: 5 }}>
|
<div key={idx} style={{ marginTop: 5 }}>
|
||||||
<Text>{variant.name}</Text>
|
<Text>{variant.name}</Text>
|
||||||
<br />
|
<br />
|
||||||
<variant.Logotype height={50} />
|
<variant.Logotype height={50} />
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</Wrapper>
|
</Wrapper>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const AllCustomizedLogomarks = () => {
|
export const AllCustomizedLogomarks = () => {
|
||||||
return (
|
return (
|
||||||
<Wrapper>
|
<Wrapper>
|
||||||
<div>
|
<div>
|
||||||
<Text>Base Logo</Text>
|
<Text>Base Logo</Text>
|
||||||
<br />
|
<br />
|
||||||
<Logomark height={50} />
|
<Logomark height={50} />
|
||||||
</div>
|
</div>
|
||||||
{AllVariants.map((variant, idx) => (
|
{AllVariants.map((variant, idx) => (
|
||||||
<div key={idx}>
|
<div key={idx}>
|
||||||
<Text>{variant.name}</Text>
|
<Text>{variant.name}</Text>
|
||||||
<br />
|
<br />
|
||||||
<variant.Logomark height={50} />
|
<variant.Logomark height={50} />
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</Wrapper>
|
</Wrapper>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -6,65 +6,65 @@ import { Logomark, Logotype } from './Branding';
|
||||||
import { LogoFlagProps, LogomarkFlag, LogotypeFlag } from './FlagBranding';
|
import { LogoFlagProps, LogomarkFlag, LogotypeFlag } from './FlagBranding';
|
||||||
|
|
||||||
type DynamicLogoProps = LogoFlagProps & {
|
type DynamicLogoProps = LogoFlagProps & {
|
||||||
currentDate?: Date;
|
currentDate?: Date;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DynamicLogomark = (props: Partial<DynamicLogoProps>) => {
|
export const DynamicLogomark = (props: Partial<DynamicLogoProps>) => {
|
||||||
const variant = React.useMemo(() => getRelevantVariant(props.currentDate), [
|
const variant = React.useMemo(() => getRelevantVariant(props.currentDate), [
|
||||||
props.currentDate,
|
props.currentDate,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (!variant) {
|
if (!variant) {
|
||||||
return <Logomark {...props} />;
|
return <Logomark {...props} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
let tooltipProps = {};
|
let tooltipProps = {};
|
||||||
if (variant.tooltip) {
|
if (variant.tooltip) {
|
||||||
tooltipProps = {
|
tooltipProps = {
|
||||||
'data-tip': variant.tooltip,
|
'data-tip': variant.tooltip,
|
||||||
// 'data-for': 'dynamic-logomark',
|
// 'data-for': 'dynamic-logomark',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<variant.Logomark {...tooltipProps} {...props} />
|
<variant.Logomark {...tooltipProps} {...props} />
|
||||||
<ReactTooltip id="dynamic-logomark" />
|
<ReactTooltip id="dynamic-logomark" />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DynamicLogotype = (props: Partial<DynamicLogoProps>) => {
|
export const DynamicLogotype = (props: Partial<DynamicLogoProps>) => {
|
||||||
const variant = React.useMemo(() => getRelevantVariant(props.currentDate), [
|
const variant = React.useMemo(() => getRelevantVariant(props.currentDate), [
|
||||||
props.currentDate,
|
props.currentDate,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (!variant) {
|
if (!variant) {
|
||||||
return <Logotype {...props} />;
|
return <Logotype {...props} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
let tooltipProps = {};
|
let tooltipProps = {};
|
||||||
if (variant.tooltip) {
|
if (variant.tooltip) {
|
||||||
tooltipProps = {
|
tooltipProps = {
|
||||||
'data-tip': variant.tooltip,
|
'data-tip': variant.tooltip,
|
||||||
// 'data-for': 'dynamic-logomark',
|
// 'data-for': 'dynamic-logomark',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<variant.Logotype {...tooltipProps} {...props} />
|
<variant.Logotype {...tooltipProps} {...props} />
|
||||||
<ReactTooltip id="dynamic-logomark" />
|
<ReactTooltip id="dynamic-logomark" />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const getRelevantVariant = (currentDate?: Date) => {
|
const getRelevantVariant = (currentDate?: Date) => {
|
||||||
for (let variant of AllVariants) {
|
for (let variant of AllVariants) {
|
||||||
if (variant.activeIf(currentDate)) return variant;
|
if (variant.activeIf(currentDate)) return variant;
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
// These should be updated for 2021.
|
// These should be updated for 2021.
|
||||||
|
@ -76,352 +76,338 @@ const getRelevantVariant = (currentDate?: Date) => {
|
||||||
// - 4/20 is just for 4/20.
|
// - 4/20 is just for 4/20.
|
||||||
|
|
||||||
const matchDay = (
|
const matchDay = (
|
||||||
start: Date,
|
start: Date,
|
||||||
end: Date,
|
end: Date,
|
||||||
currentDate: Date = new Date(),
|
currentDate: Date = new Date(),
|
||||||
staticDate: boolean = false
|
staticDate: boolean = false
|
||||||
) => {
|
) => {
|
||||||
if (!staticDate) {
|
if (!staticDate) {
|
||||||
// pre-fill start/end years to simplify
|
// pre-fill start/end years to simplify
|
||||||
start.setFullYear(currentDate.getFullYear());
|
start.setFullYear(currentDate.getFullYear());
|
||||||
end.setFullYear(currentDate.getFullYear());
|
end.setFullYear(currentDate.getFullYear());
|
||||||
}
|
}
|
||||||
|
|
||||||
start.setHours(0, 0, 0, 0);
|
start.setHours(0, 0, 0, 0);
|
||||||
end.setHours(0, 0, 0, 0);
|
end.setHours(0, 0, 0, 0);
|
||||||
|
|
||||||
if (currentDate > start && currentDate < end) {
|
if (currentDate > start && currentDate < end) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
type Variant = {
|
type Variant = {
|
||||||
name: string;
|
name: string;
|
||||||
activeIf: (currentDate?: Date) => boolean;
|
activeIf: (currentDate?: Date) => boolean;
|
||||||
sharedProps?: Partial<DynamicLogoProps>;
|
sharedProps?: Partial<DynamicLogoProps>;
|
||||||
flagStripes?: string[];
|
flagStripes?: string[];
|
||||||
tooltip?: string;
|
tooltip?: string;
|
||||||
Logomark: React.FunctionComponent<any>;
|
Logomark: React.FunctionComponent<any>;
|
||||||
Logotype: React.FunctionComponent<any>;
|
Logotype: React.FunctionComponent<any>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Trans: Variant = {
|
export const Trans: Variant = {
|
||||||
// March 31, Nov 13-20+1
|
// March 31, Nov 13-20+1
|
||||||
name: 'Trans Pride',
|
name: 'Trans Pride',
|
||||||
activeIf: (currentDate?: Date) =>
|
activeIf: (currentDate?: Date) =>
|
||||||
matchDay(new Date('2021-Mar-31'), new Date('2021-Apr-1'), currentDate) ||
|
matchDay(new Date('2021-Mar-31'), new Date('2021-Apr-1'), currentDate) ||
|
||||||
matchDay(new Date('2021-Nov-13'), new Date('2021-Nov-22'), currentDate),
|
matchDay(new Date('2021-Nov-13'), new Date('2021-Nov-22'), currentDate),
|
||||||
sharedProps: {
|
sharedProps: {
|
||||||
circleFill: '#F7A8B8',
|
circleFill: '#F7A8B8',
|
||||||
circleOuterFill: palette.taupe200,
|
circleOuterFill: palette.taupe200,
|
||||||
typeFill: palette.grey500,
|
typeFill: palette.grey500,
|
||||||
stripes: ['#55CDFC', '#F7A8B8', palette.grey600, '#F7A8B8', '#55CDFC'],
|
stripes: ['#55CDFC', '#F7A8B8', palette.grey600, '#F7A8B8', '#55CDFC'],
|
||||||
},
|
},
|
||||||
tooltip: 'Roleypoly says trans rights!',
|
tooltip: 'Roleypoly says trans rights!',
|
||||||
Logomark: (props: DynamicLogoProps) => (
|
Logomark: (props: DynamicLogoProps) => (
|
||||||
<LogomarkFlag {...props} {...Trans.sharedProps} />
|
<LogomarkFlag {...props} {...Trans.sharedProps} />
|
||||||
),
|
),
|
||||||
Logotype: (props: DynamicLogoProps) => (
|
Logotype: (props: DynamicLogoProps) => (
|
||||||
<LogotypeFlag {...props} {...Trans.sharedProps} />
|
<LogotypeFlag {...props} {...Trans.sharedProps} />
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Bi: Variant = {
|
export const Bi: Variant = {
|
||||||
// Sept 16-23
|
// Sept 16-23
|
||||||
name: 'Bi Week',
|
name: 'Bi Week',
|
||||||
activeIf: (currentDate?: Date) =>
|
activeIf: (currentDate?: Date) =>
|
||||||
matchDay(new Date('2021-Sep-16'), new Date('2021-Sep-24'), currentDate),
|
matchDay(new Date('2021-Sep-16'), new Date('2021-Sep-24'), currentDate),
|
||||||
sharedProps: {
|
sharedProps: {
|
||||||
circleFill: '#D60270',
|
circleFill: '#D60270',
|
||||||
circleOuterFill: palette.taupe200,
|
circleOuterFill: palette.taupe200,
|
||||||
typeFill: '#9B4F96',
|
typeFill: '#9B4F96',
|
||||||
stripes: ['#0038A8', '#0038A8', '#9B4F96', '#D60270', '#D60270'],
|
stripes: ['#0038A8', '#0038A8', '#9B4F96', '#D60270', '#D60270'],
|
||||||
},
|
},
|
||||||
tooltip: 'Being bi is a lot like a riding a bicycle since they can go both ways.',
|
tooltip: 'Being bi is a lot like a riding a bicycle since they can go both ways.',
|
||||||
Logomark: (props: DynamicLogoProps) => (
|
Logomark: (props: DynamicLogoProps) => <LogomarkFlag {...props} {...Bi.sharedProps} />,
|
||||||
<LogomarkFlag {...props} {...Bi.sharedProps} />
|
Logotype: (props: DynamicLogoProps) => <LogotypeFlag {...props} {...Bi.sharedProps} />,
|
||||||
),
|
|
||||||
Logotype: (props: DynamicLogoProps) => (
|
|
||||||
<LogotypeFlag {...props} {...Bi.sharedProps} />
|
|
||||||
),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Lesbian: Variant = {
|
export const Lesbian: Variant = {
|
||||||
// Apr 26
|
// Apr 26
|
||||||
name: 'Lesbian Pride',
|
name: 'Lesbian Pride',
|
||||||
activeIf: (currentDate?: Date) =>
|
activeIf: (currentDate?: Date) =>
|
||||||
matchDay(new Date('2021-Apr-25'), new Date('2021-Apt-27'), currentDate),
|
matchDay(new Date('2021-Apr-25'), new Date('2021-Apt-27'), currentDate),
|
||||||
sharedProps: {
|
sharedProps: {
|
||||||
circleFill: '#D362A4',
|
circleFill: '#D362A4',
|
||||||
circleOuterFill: palette.taupe200,
|
circleOuterFill: palette.taupe200,
|
||||||
typeFill: '#FF9A56',
|
typeFill: '#FF9A56',
|
||||||
stripes: ['#D52D00', '#FF9A56', palette.grey600, '#D362A4', '#A30262'],
|
stripes: ['#D52D00', '#FF9A56', palette.grey600, '#D362A4', '#A30262'],
|
||||||
},
|
},
|
||||||
tooltip: "I'm a lesbiab... lesbiam... Less Bien... Girls.",
|
tooltip: "I'm a lesbiab... lesbiam... Less Bien... Girls.",
|
||||||
Logomark: (props: DynamicLogoProps) => (
|
Logomark: (props: DynamicLogoProps) => (
|
||||||
<LogomarkFlag {...props} {...Lesbian.sharedProps} />
|
<LogomarkFlag {...props} {...Lesbian.sharedProps} />
|
||||||
),
|
),
|
||||||
Logotype: (props: DynamicLogoProps) => (
|
Logotype: (props: DynamicLogoProps) => (
|
||||||
<LogotypeFlag {...props} {...Lesbian.sharedProps} />
|
<LogotypeFlag {...props} {...Lesbian.sharedProps} />
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Ace: Variant = {
|
export const Ace: Variant = {
|
||||||
// Oct 24-30
|
// Oct 24-30
|
||||||
name: 'Ace Week',
|
name: 'Ace Week',
|
||||||
activeIf: (currentDate?: Date) =>
|
activeIf: (currentDate?: Date) =>
|
||||||
matchDay(new Date('2021-Oct-24'), new Date('2021-Oct-31'), currentDate),
|
matchDay(new Date('2021-Oct-24'), new Date('2021-Oct-31'), currentDate),
|
||||||
sharedProps: {
|
sharedProps: {
|
||||||
circleFill: '#333',
|
circleFill: '#333',
|
||||||
circleOuterFill: palette.taupe200,
|
circleOuterFill: palette.taupe200,
|
||||||
typeFill: '#CCC',
|
typeFill: '#CCC',
|
||||||
stripes: ['#84067C', palette.grey600, '#CCCCCC', palette.grey100],
|
stripes: ['#84067C', palette.grey600, '#CCCCCC', palette.grey100],
|
||||||
},
|
},
|
||||||
tooltip: "Sexualn't",
|
tooltip: "Sexualn't",
|
||||||
Logomark: (props: DynamicLogoProps) => (
|
Logomark: (props: DynamicLogoProps) => <LogomarkFlag {...props} {...Ace.sharedProps} />,
|
||||||
<LogomarkFlag {...props} {...Ace.sharedProps} />
|
Logotype: (props: DynamicLogoProps) => <LogotypeFlag {...props} {...Ace.sharedProps} />,
|
||||||
),
|
|
||||||
Logotype: (props: DynamicLogoProps) => (
|
|
||||||
<LogotypeFlag {...props} {...Ace.sharedProps} />
|
|
||||||
),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Birthday: Variant = {
|
export const Birthday: Variant = {
|
||||||
// Jan 15
|
// Jan 15
|
||||||
name: "Roleypoly's Birthday",
|
name: "Roleypoly's Birthday",
|
||||||
activeIf: (currentDate?: Date) =>
|
activeIf: (currentDate?: Date) =>
|
||||||
matchDay(new Date('2021-Jan-15'), new Date('2021-Jan-16'), currentDate),
|
matchDay(new Date('2021-Jan-15'), new Date('2021-Jan-16'), currentDate),
|
||||||
sharedProps: {
|
sharedProps: {
|
||||||
circleFill: 'none',
|
circleFill: 'none',
|
||||||
circleOuterFill: palette.taupe300,
|
circleOuterFill: palette.taupe300,
|
||||||
typeFill: palette.taupe500,
|
typeFill: palette.taupe500,
|
||||||
},
|
},
|
||||||
tooltip: '🎉 HAPPY BIRTHDAY ROLEYPOLY 🎉',
|
tooltip: '🎉 HAPPY BIRTHDAY ROLEYPOLY 🎉',
|
||||||
Logomark: (props: DynamicLogoProps) => (
|
Logomark: (props: DynamicLogoProps) => (
|
||||||
<svg
|
<svg
|
||||||
style={props.style}
|
style={props.style}
|
||||||
className={props.className}
|
className={props.className}
|
||||||
width={props.width}
|
width={props.width}
|
||||||
height={props.height}
|
height={props.height}
|
||||||
data-for={props['data-for']}
|
data-for={props['data-for']}
|
||||||
data-tip={props['data-tip']}
|
data-tip={props['data-tip']}
|
||||||
viewBox="30 10 100 100"
|
viewBox="30 10 100 100"
|
||||||
fill="none"
|
fill="none"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
>
|
>
|
||||||
<g clipPath="url(#clip0)">
|
<g clipPath="url(#clip0)">
|
||||||
<path
|
<path
|
||||||
d="M104.737 85.406V86H94.243L75.73 60.953H66.226V86H57.118V21.848H78.007C83.023 21.848 87.247 22.64 90.679 24.224C94.177 25.808 96.784 28.019 98.5 30.857C100.282 33.695 101.173 36.995 101.173 40.757C101.173 43.793 100.579 46.598 99.391 49.172C98.269 51.746 96.553 53.99 94.243 55.904C91.933 57.752 89.062 59.105 85.63 59.963L104.737 85.406ZM66.226 53.429H76.621C81.571 53.429 85.3 52.373 87.808 50.261C90.382 48.083 91.669 45.047 91.669 41.153C91.669 37.391 90.481 34.52 88.105 32.54C85.795 30.56 82.363 29.57 77.809 29.57H66.226V53.429Z"
|
d="M104.737 85.406V86H94.243L75.73 60.953H66.226V86H57.118V21.848H78.007C83.023 21.848 87.247 22.64 90.679 24.224C94.177 25.808 96.784 28.019 98.5 30.857C100.282 33.695 101.173 36.995 101.173 40.757C101.173 43.793 100.579 46.598 99.391 49.172C98.269 51.746 96.553 53.99 94.243 55.904C91.933 57.752 89.062 59.105 85.63 59.963L104.737 85.406ZM66.226 53.429H76.621C81.571 53.429 85.3 52.373 87.808 50.261C90.382 48.083 91.669 45.047 91.669 41.153C91.669 37.391 90.481 34.52 88.105 32.54C85.795 30.56 82.363 29.57 77.809 29.57H66.226V53.429Z"
|
||||||
fill={props.typeFill || Birthday.sharedProps?.typeFill}
|
fill={props.typeFill || Birthday.sharedProps?.typeFill}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<mask
|
<mask
|
||||||
id="mask0"
|
id="mask0"
|
||||||
mask-type="alpha"
|
mask-type="alpha"
|
||||||
maskUnits="userSpaceOnUse"
|
maskUnits="userSpaceOnUse"
|
||||||
x="45"
|
x="45"
|
||||||
y="25"
|
y="25"
|
||||||
width="142"
|
width="142"
|
||||||
height="142"
|
height="142"
|
||||||
>
|
>
|
||||||
<path
|
<path
|
||||||
d="M115.711 25L186.421 95.7107L115.711 166.421L45 95.7107L115.711 25Z"
|
d="M115.711 25L186.421 95.7107L115.711 166.421L45 95.7107L115.711 25Z"
|
||||||
fill={
|
fill={props.circleOuterFill || Birthday.sharedProps?.circleOuterFill}
|
||||||
props.circleOuterFill || Birthday.sharedProps?.circleOuterFill
|
/>
|
||||||
}
|
</mask>
|
||||||
/>
|
<g mask="url(#mask0)">
|
||||||
</mask>
|
<path
|
||||||
<g mask="url(#mask0)">
|
fillRule="evenodd"
|
||||||
<path
|
clipRule="evenodd"
|
||||||
fillRule="evenodd"
|
d="M79.9998 102C103.196 102 122 83.196 122 60C122 36.804 103.196 18 79.9998 18C56.8039 18 37.9998 36.804 37.9998 60C37.9998 83.196 56.8039 102 79.9998 102ZM79.9998 110C107.614 110 130 87.6142 130 60C130 32.3858 107.614 10 79.9998 10C52.3856 10 29.9998 32.3858 29.9998 60C29.9998 87.6142 52.3856 110 79.9998 110Z"
|
||||||
clipRule="evenodd"
|
fill={props.circleOuterFill || Birthday.sharedProps?.circleOuterFill}
|
||||||
d="M79.9998 102C103.196 102 122 83.196 122 60C122 36.804 103.196 18 79.9998 18C56.8039 18 37.9998 36.804 37.9998 60C37.9998 83.196 56.8039 102 79.9998 102ZM79.9998 110C107.614 110 130 87.6142 130 60C130 32.3858 107.614 10 79.9998 10C52.3856 10 29.9998 32.3858 29.9998 60C29.9998 87.6142 52.3856 110 79.9998 110Z"
|
/>
|
||||||
fill={
|
</g>
|
||||||
props.circleOuterFill || Birthday.sharedProps?.circleOuterFill
|
</g>
|
||||||
}
|
<defs>
|
||||||
/>
|
<clipPath id="clip0">
|
||||||
</g>
|
<rect width="130" height="110" fill="white" />
|
||||||
</g>
|
</clipPath>
|
||||||
<defs>
|
</defs>
|
||||||
<clipPath id="clip0">
|
</svg>
|
||||||
<rect width="130" height="110" fill="white" />
|
),
|
||||||
</clipPath>
|
Logotype: (props: DynamicLogoProps) => (
|
||||||
</defs>
|
<div style={{ display: 'inline-block', maxWidth: props.width }}>
|
||||||
</svg>
|
<SparkleOverlay strokeColor={palette.discord400}>
|
||||||
),
|
<Logotype {...props} {...Birthday.sharedProps} />
|
||||||
Logotype: (props: DynamicLogoProps) => (
|
</SparkleOverlay>
|
||||||
<div style={{ display: 'inline-block', maxWidth: props.width }}>
|
</div>
|
||||||
<SparkleOverlay strokeColor={palette.discord400}>
|
),
|
||||||
<Logotype {...props} {...Birthday.sharedProps} />
|
|
||||||
</SparkleOverlay>
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DevilsLettuce: Variant = {
|
export const DevilsLettuce: Variant = {
|
||||||
name: 'Meme #1',
|
name: 'Meme #1',
|
||||||
// Apr 20
|
// Apr 20
|
||||||
activeIf: (currentDate?: Date) =>
|
activeIf: (currentDate?: Date) =>
|
||||||
matchDay(new Date('2021-Apr-20'), new Date('2021-Apr-21'), currentDate),
|
matchDay(new Date('2021-Apr-20'), new Date('2021-Apr-21'), currentDate),
|
||||||
sharedProps: {
|
sharedProps: {
|
||||||
circleFill: palette.green400,
|
circleFill: palette.green400,
|
||||||
circleOuterFill: palette.green200,
|
circleOuterFill: palette.green200,
|
||||||
typeFill: palette.green400,
|
typeFill: palette.green400,
|
||||||
},
|
},
|
||||||
tooltip: 'Legalize it.',
|
tooltip: 'Legalize it.',
|
||||||
Logomark: (props: DynamicLogoProps) => (
|
Logomark: (props: DynamicLogoProps) => (
|
||||||
<Logomark {...props} {...DevilsLettuce.sharedProps} />
|
<Logomark {...props} {...DevilsLettuce.sharedProps} />
|
||||||
),
|
),
|
||||||
Logotype: (props: DynamicLogoProps) => (
|
Logotype: (props: DynamicLogoProps) => (
|
||||||
<Logotype {...props} {...DevilsLettuce.sharedProps} />
|
<Logotype {...props} {...DevilsLettuce.sharedProps} />
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
export const BicycleDay: Variant = {
|
export const BicycleDay: Variant = {
|
||||||
name: 'Meme #2',
|
name: 'Meme #2',
|
||||||
// Apr 19
|
// Apr 19
|
||||||
// TODO: hexagon is bestagon
|
// TODO: hexagon is bestagon
|
||||||
activeIf: (currentDate?: Date) =>
|
activeIf: (currentDate?: Date) =>
|
||||||
matchDay(new Date('2021-Apr-19'), new Date('2021-Apr-20'), currentDate),
|
matchDay(new Date('2021-Apr-19'), new Date('2021-Apr-20'), currentDate),
|
||||||
sharedProps: {
|
sharedProps: {
|
||||||
circleFill: palette.gold400,
|
circleFill: palette.gold400,
|
||||||
circleOuterFill: palette.taupe200,
|
circleOuterFill: palette.taupe200,
|
||||||
typeFill: palette.discord400,
|
typeFill: palette.discord400,
|
||||||
stripes: Object.values(palette),
|
stripes: Object.values(palette),
|
||||||
},
|
},
|
||||||
tooltip: 'It increases brain complexity.',
|
tooltip: 'It increases brain complexity.',
|
||||||
Logomark: (props: DynamicLogoProps) => (
|
Logomark: (props: DynamicLogoProps) => (
|
||||||
<LogomarkFlag {...props} {...BicycleDay.sharedProps} />
|
<LogomarkFlag {...props} {...BicycleDay.sharedProps} />
|
||||||
),
|
),
|
||||||
Logotype: (props: DynamicLogoProps) => (
|
Logotype: (props: DynamicLogoProps) => (
|
||||||
<LogotypeFlag {...props} {...BicycleDay.sharedProps} />
|
<LogotypeFlag {...props} {...BicycleDay.sharedProps} />
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Christmas: Variant = {
|
export const Christmas: Variant = {
|
||||||
name: 'Christmas!',
|
name: 'Christmas!',
|
||||||
// Dec 20-27
|
// Dec 20-27
|
||||||
activeIf: (currentDate?: Date) =>
|
activeIf: (currentDate?: Date) =>
|
||||||
matchDay(new Date('2021-Dec-20'), new Date('2021-Dec-28'), currentDate),
|
matchDay(new Date('2021-Dec-20'), new Date('2021-Dec-28'), currentDate),
|
||||||
sharedProps: {
|
sharedProps: {
|
||||||
circleFill: palette.green200,
|
circleFill: palette.green200,
|
||||||
circleOuterFill: palette.red200,
|
circleOuterFill: palette.red200,
|
||||||
typeFill: palette.green400,
|
typeFill: palette.green400,
|
||||||
stripes: [
|
stripes: [
|
||||||
palette.grey600,
|
palette.grey600,
|
||||||
palette.red400,
|
palette.red400,
|
||||||
palette.grey600,
|
palette.grey600,
|
||||||
palette.green400,
|
palette.green400,
|
||||||
palette.grey600,
|
palette.grey600,
|
||||||
palette.red400,
|
palette.red400,
|
||||||
palette.grey600,
|
palette.grey600,
|
||||||
palette.green400,
|
palette.green400,
|
||||||
palette.grey600,
|
palette.grey600,
|
||||||
palette.red400,
|
palette.red400,
|
||||||
palette.grey600,
|
palette.grey600,
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
tooltip: 'Have yourself a merry little Christmas~',
|
tooltip: 'Have yourself a merry little Christmas~',
|
||||||
Logomark: (props: DynamicLogoProps) => (
|
Logomark: (props: DynamicLogoProps) => (
|
||||||
<SparkleOverlay strokeColor={'white'}>
|
<SparkleOverlay strokeColor={'white'}>
|
||||||
<LogomarkFlag {...props} {...Christmas.sharedProps} />
|
<LogomarkFlag {...props} {...Christmas.sharedProps} />
|
||||||
</SparkleOverlay>
|
</SparkleOverlay>
|
||||||
),
|
),
|
||||||
Logotype: (props: DynamicLogoProps) => (
|
Logotype: (props: DynamicLogoProps) => (
|
||||||
<SparkleOverlay strokeColor={'white'}>
|
<SparkleOverlay strokeColor={'white'}>
|
||||||
<LogotypeFlag {...props} {...Christmas.sharedProps} />
|
<LogotypeFlag {...props} {...Christmas.sharedProps} />
|
||||||
</SparkleOverlay>
|
</SparkleOverlay>
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
export const NewYear: Variant = {
|
export const NewYear: Variant = {
|
||||||
name: "New Year's Day",
|
name: "New Year's Day",
|
||||||
// Dec 30 - Jan 2
|
// Dec 30 - Jan 2
|
||||||
// TODO: sparkle
|
// TODO: sparkle
|
||||||
activeIf: (currentDate?: Date) =>
|
activeIf: (currentDate?: Date) =>
|
||||||
matchDay(new Date('2021-Dec-30'), new Date('2021-Jan-3'), currentDate),
|
matchDay(new Date('2021-Dec-30'), new Date('2021-Jan-3'), currentDate),
|
||||||
sharedProps: {
|
sharedProps: {
|
||||||
circleFill: '#222',
|
circleFill: '#222',
|
||||||
circleOuterFill: palette.red400,
|
circleOuterFill: palette.red400,
|
||||||
typeFill: '#aaa',
|
typeFill: '#aaa',
|
||||||
},
|
},
|
||||||
tooltip: 'Fuck 2020. 🎆🎇🎆🎇',
|
tooltip: 'Fuck 2020. 🎆🎇🎆🎇',
|
||||||
Logomark: (props: DynamicLogoProps) => (
|
Logomark: (props: DynamicLogoProps) => <Logomark {...props} {...NewYear.sharedProps} />,
|
||||||
<Logomark {...props} {...NewYear.sharedProps} />
|
Logotype: (props: DynamicLogoProps) => (
|
||||||
),
|
<div style={{ display: 'inline-block', maxWidth: props.width }}>
|
||||||
Logotype: (props: DynamicLogoProps) => (
|
<SparkleOverlay>
|
||||||
<div style={{ display: 'inline-block', maxWidth: props.width }}>
|
<Logotype {...props} {...NewYear.sharedProps} />
|
||||||
<SparkleOverlay>
|
</SparkleOverlay>
|
||||||
<Logotype {...props} {...NewYear.sharedProps} />
|
</div>
|
||||||
</SparkleOverlay>
|
),
|
||||||
</div>
|
|
||||||
),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const LunarNewYear: Variant = {
|
export const LunarNewYear: Variant = {
|
||||||
name: 'Lunar New Year',
|
name: 'Lunar New Year',
|
||||||
// Feb 12, 2021
|
// Feb 12, 2021
|
||||||
// Feb 1, 2022
|
// Feb 1, 2022
|
||||||
activeIf: (currentDate?: Date) =>
|
activeIf: (currentDate?: Date) =>
|
||||||
matchDay(new Date('2021-Feb-10'), new Date('2021-Feb-13'), currentDate, true) ||
|
matchDay(new Date('2021-Feb-10'), new Date('2021-Feb-13'), currentDate, true) ||
|
||||||
matchDay(new Date('2022-Jan-30'), new Date('2022-Feb-3'), currentDate, true),
|
matchDay(new Date('2022-Jan-30'), new Date('2022-Feb-3'), currentDate, true),
|
||||||
sharedProps: {
|
sharedProps: {
|
||||||
circleFill: palette.red200,
|
circleFill: palette.red200,
|
||||||
circleOuterFill: palette.gold400,
|
circleOuterFill: palette.gold400,
|
||||||
typeFill: palette.taupe300,
|
typeFill: palette.taupe300,
|
||||||
},
|
},
|
||||||
tooltip: '恭喜发财! 🎊🎆🎇',
|
tooltip: '恭喜发财! 🎊🎆🎇',
|
||||||
Logomark: (props: DynamicLogoProps) => (
|
Logomark: (props: DynamicLogoProps) => (
|
||||||
<Logomark {...props} {...LunarNewYear.sharedProps} />
|
<Logomark {...props} {...LunarNewYear.sharedProps} />
|
||||||
),
|
),
|
||||||
Logotype: (props: DynamicLogoProps) => (
|
Logotype: (props: DynamicLogoProps) => (
|
||||||
<div style={{ display: 'inline-block', maxWidth: props.width }}>
|
<div style={{ display: 'inline-block', maxWidth: props.width }}>
|
||||||
<SparkleOverlay>
|
<SparkleOverlay>
|
||||||
<Logotype {...props} {...LunarNewYear.sharedProps} />
|
<Logotype {...props} {...LunarNewYear.sharedProps} />
|
||||||
</SparkleOverlay>
|
</SparkleOverlay>
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Pride: Variant = {
|
export const Pride: Variant = {
|
||||||
name: 'LGBTQPOC Pride Month',
|
name: 'LGBTQPOC Pride Month',
|
||||||
// June
|
// June
|
||||||
activeIf: (currentDate?: Date) =>
|
activeIf: (currentDate?: Date) =>
|
||||||
matchDay(new Date('2021-Jun-1'), new Date('2021-Jul-1'), currentDate),
|
matchDay(new Date('2021-Jun-1'), new Date('2021-Jul-1'), currentDate),
|
||||||
sharedProps: {
|
sharedProps: {
|
||||||
circleOuterFill: palette.taupe200,
|
circleOuterFill: palette.taupe200,
|
||||||
typeFill: palette.grey500,
|
typeFill: palette.grey500,
|
||||||
stripes: [
|
stripes: [
|
||||||
'#593BB5',
|
'#593BB5',
|
||||||
'#26B186',
|
'#26B186',
|
||||||
'#FFC468',
|
'#FFC468',
|
||||||
'#F97C21',
|
'#F97C21',
|
||||||
'#F62C8B',
|
'#F62C8B',
|
||||||
'#8B5950',
|
'#8B5950',
|
||||||
'#2D3234',
|
'#2D3234',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
tooltip: 'LOVE WINS. 💖🌈🌟',
|
tooltip: 'LOVE WINS. 💖🌈🌟',
|
||||||
Logomark: (props: DynamicLogoProps) => (
|
Logomark: (props: DynamicLogoProps) => (
|
||||||
<LogomarkFlag {...props} {...Pride.sharedProps} />
|
<LogomarkFlag {...props} {...Pride.sharedProps} />
|
||||||
),
|
),
|
||||||
Logotype: (props: DynamicLogoProps) => (
|
Logotype: (props: DynamicLogoProps) => (
|
||||||
<LogotypeFlag {...props} {...Pride.sharedProps} />
|
<LogotypeFlag {...props} {...Pride.sharedProps} />
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
export const AllVariants: Variant[] = [
|
export const AllVariants: Variant[] = [
|
||||||
Trans,
|
Trans,
|
||||||
Pride,
|
Pride,
|
||||||
Bi,
|
Bi,
|
||||||
Lesbian,
|
Lesbian,
|
||||||
Ace,
|
Ace,
|
||||||
Birthday,
|
Birthday,
|
||||||
DevilsLettuce,
|
DevilsLettuce,
|
||||||
BicycleDay,
|
BicycleDay,
|
||||||
Christmas,
|
Christmas,
|
||||||
NewYear,
|
NewYear,
|
||||||
LunarNewYear,
|
LunarNewYear,
|
||||||
];
|
];
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
import { LogoFlagProps, LogomarkFlag, LogotypeFlag } from './FlagBranding';
|
import { LogoFlagProps, LogomarkFlag, LogotypeFlag } from './FlagBranding';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
title: 'Atoms/Branding/Flags',
|
title: 'Atoms/Branding/Flags',
|
||||||
component: LogomarkFlag,
|
component: LogomarkFlag,
|
||||||
args: {
|
args: {
|
||||||
stripes: ['#F9238B', '#FB7B04', '#FFCA66', '#00B289', '#5A38B5', '#B413F5'],
|
stripes: ['#F9238B', '#FB7B04', '#FFCA66', '#00B289', '#5A38B5', '#B413F5'],
|
||||||
height: 50,
|
height: 50,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const logomarkFlag = (args: LogoFlagProps) => <LogomarkFlag {...args} />;
|
export const logomarkFlag = (args: LogoFlagProps) => <LogomarkFlag {...args} />;
|
||||||
|
|
|
@ -3,190 +3,190 @@ import { palette } from '../colors';
|
||||||
import { LogoProps } from './Branding';
|
import { LogoProps } from './Branding';
|
||||||
|
|
||||||
export type LogoFlagProps = LogoProps & {
|
export type LogoFlagProps = LogoProps & {
|
||||||
stripes: string[];
|
stripes: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export const generateStripes = (stripes: string[]) => {
|
export const generateStripes = (stripes: string[]) => {
|
||||||
const barWidth = 100 / stripes.length;
|
const barWidth = 100 / stripes.length;
|
||||||
return (
|
return (
|
||||||
<g transform="scale(0.9) rotate(-45 65 65) ">
|
<g transform="scale(0.9) rotate(-45 65 65) ">
|
||||||
{stripes.map((stripeFill, idx) => (
|
{stripes.map((stripeFill, idx) => (
|
||||||
<rect
|
<rect
|
||||||
key={idx}
|
key={idx}
|
||||||
x={30 + idx * barWidth}
|
x={30 + idx * barWidth}
|
||||||
y={30}
|
y={30}
|
||||||
width={barWidth + 1}
|
width={barWidth + 1}
|
||||||
height={100}
|
height={100}
|
||||||
fill={stripeFill}
|
fill={stripeFill}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</g>
|
</g>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const LogomarkFlag = (props: LogoFlagProps) => (
|
export const LogomarkFlag = (props: LogoFlagProps) => (
|
||||||
<svg
|
<svg
|
||||||
style={props.style}
|
style={props.style}
|
||||||
className={props.className}
|
className={props.className}
|
||||||
width={props.width}
|
width={props.width}
|
||||||
height={props.height}
|
height={props.height}
|
||||||
data-for={props['data-for']}
|
data-for={props['data-for']}
|
||||||
data-tip={props['data-tip']}
|
data-tip={props['data-tip']}
|
||||||
viewBox="30 10 100 100"
|
viewBox="30 10 100 100"
|
||||||
fill="none"
|
fill="none"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
>
|
>
|
||||||
<g clipPath="url(#clip0)">
|
<g clipPath="url(#clip0)">
|
||||||
<mask
|
<mask
|
||||||
id="mask0"
|
id="mask0"
|
||||||
mask-type="alpha"
|
mask-type="alpha"
|
||||||
maskUnits="userSpaceOnUse"
|
maskUnits="userSpaceOnUse"
|
||||||
x="45"
|
x="45"
|
||||||
y="25"
|
y="25"
|
||||||
width="142"
|
width="142"
|
||||||
height="142"
|
height="142"
|
||||||
>
|
>
|
||||||
<rect
|
<rect
|
||||||
x="115.711"
|
x="115.711"
|
||||||
y="25"
|
y="25"
|
||||||
width="100"
|
width="100"
|
||||||
height="100"
|
height="100"
|
||||||
transform="rotate(45 115.711 25)"
|
transform="rotate(45 115.711 25)"
|
||||||
fill="#C4C4C4"
|
fill="#C4C4C4"
|
||||||
/>
|
/>
|
||||||
</mask>
|
</mask>
|
||||||
<g mask="url(#mask0)">
|
<g mask="url(#mask0)">
|
||||||
<circle
|
<circle
|
||||||
cx="79.9999"
|
cx="79.9999"
|
||||||
cy="60"
|
cy="60"
|
||||||
r="46"
|
r="46"
|
||||||
fill="none"
|
fill="none"
|
||||||
stroke="#1D8227"
|
stroke="#1D8227"
|
||||||
strokeWidth="8"
|
strokeWidth="8"
|
||||||
/>
|
/>
|
||||||
<mask
|
<mask
|
||||||
id="mask1"
|
id="mask1"
|
||||||
mask-type="alpha"
|
mask-type="alpha"
|
||||||
maskUnits="userSpaceOnUse"
|
maskUnits="userSpaceOnUse"
|
||||||
x="30"
|
x="30"
|
||||||
y="10"
|
y="10"
|
||||||
width="100"
|
width="100"
|
||||||
height="100"
|
height="100"
|
||||||
>
|
>
|
||||||
<circle
|
<circle
|
||||||
cx="80"
|
cx="80"
|
||||||
cy="60"
|
cy="60"
|
||||||
r="46"
|
r="46"
|
||||||
fill="#F14343"
|
fill="#F14343"
|
||||||
stroke="#1D8227"
|
stroke="#1D8227"
|
||||||
strokeWidth="8"
|
strokeWidth="8"
|
||||||
/>
|
/>
|
||||||
</mask>
|
</mask>
|
||||||
<g mask="url(#mask1)">
|
<g mask="url(#mask1)">
|
||||||
{generateStripes(props.stripes)}
|
{generateStripes(props.stripes)}
|
||||||
<circle
|
<circle
|
||||||
cx="80"
|
cx="80"
|
||||||
cy="60"
|
cy="60"
|
||||||
r="46"
|
r="46"
|
||||||
stroke={props.circleOuterFill || palette.green200}
|
stroke={props.circleOuterFill || palette.green200}
|
||||||
strokeWidth="8"
|
strokeWidth="8"
|
||||||
/>
|
/>
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</g>
|
</g>
|
||||||
<defs>
|
</g>
|
||||||
<clipPath id="clip0">
|
</g>
|
||||||
<rect width="130" height="110" fill="white" />
|
<defs>
|
||||||
</clipPath>
|
<clipPath id="clip0">
|
||||||
</defs>
|
<rect width="130" height="110" fill="white" />
|
||||||
</svg>
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
);
|
);
|
||||||
|
|
||||||
export const LogotypeFlag = (props: LogoFlagProps) => (
|
export const LogotypeFlag = (props: LogoFlagProps) => (
|
||||||
<svg
|
<svg
|
||||||
style={props.style}
|
style={props.style}
|
||||||
className={props.className}
|
className={props.className}
|
||||||
width={props.width}
|
width={props.width}
|
||||||
height={props.height}
|
height={props.height}
|
||||||
data-for={props['data-for']}
|
data-for={props['data-for']}
|
||||||
data-tip={props['data-tip']}
|
data-tip={props['data-tip']}
|
||||||
viewBox="45 25 400 88"
|
viewBox="45 25 400 88"
|
||||||
fill="none"
|
fill="none"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
>
|
>
|
||||||
<g clipPath="url(#clip0)">
|
<g clipPath="url(#clip0)">
|
||||||
<g fill={props.typeFill || palette.taupe400}>
|
<g fill={props.typeFill || palette.taupe400}>
|
||||||
<path d="M179.855 95.49V96H170.845L154.95 74.495H146.79V96H138.97V40.92H156.905C161.212 40.92 164.838 41.6 167.785 42.96C170.788 44.32 173.027 46.2183 174.5 48.655C176.03 51.0917 176.795 53.925 176.795 57.155C176.795 59.7617 176.285 62.17 175.265 64.38C174.302 66.59 172.828 68.5167 170.845 70.16C168.862 71.7467 166.397 72.9083 163.45 73.645L179.855 95.49ZM146.79 68.035H155.715C159.965 68.035 163.167 67.1283 165.32 65.315C167.53 63.445 168.635 60.8383 168.635 57.495C168.635 54.265 167.615 51.8 165.575 50.1C163.592 48.4 160.645 47.55 156.735 47.55H146.79V68.035Z" />
|
<path d="M179.855 95.49V96H170.845L154.95 74.495H146.79V96H138.97V40.92H156.905C161.212 40.92 164.838 41.6 167.785 42.96C170.788 44.32 173.027 46.2183 174.5 48.655C176.03 51.0917 176.795 53.925 176.795 57.155C176.795 59.7617 176.285 62.17 175.265 64.38C174.302 66.59 172.828 68.5167 170.845 70.16C168.862 71.7467 166.397 72.9083 163.45 73.645L179.855 95.49ZM146.79 68.035H155.715C159.965 68.035 163.167 67.1283 165.32 65.315C167.53 63.445 168.635 60.8383 168.635 57.495C168.635 54.265 167.615 51.8 165.575 50.1C163.592 48.4 160.645 47.55 156.735 47.55H146.79V68.035Z" />
|
||||||
<path d="M197.154 97.02C193.188 97.02 189.873 96.17 187.209 94.47C184.546 92.7133 182.563 90.3617 181.259 87.415C179.956 84.4117 179.304 81.04 179.304 77.3C179.304 72.7667 180.154 69.055 181.854 66.165C183.554 63.2183 185.849 61.0933 188.739 59.79C191.629 58.43 194.916 57.75 198.599 57.75C202.566 57.75 205.881 58.6283 208.544 60.385C211.208 62.085 213.191 64.4083 214.494 67.355C215.798 70.3017 216.449 73.645 216.449 77.385C216.449 81.975 215.599 85.7433 213.899 88.69C212.199 91.58 209.904 93.705 207.014 95.065C204.124 96.3683 200.838 97.02 197.154 97.02ZM197.834 91.07C205.144 91.07 208.799 86.48 208.799 77.3C208.799 73.1633 207.893 69.8767 206.079 67.44C204.323 64.9467 201.659 63.7 198.089 63.7C194.123 63.7 191.261 64.9183 189.504 67.355C187.804 69.735 186.954 73.05 186.954 77.3C186.954 81.4933 187.861 84.8367 189.674 87.33C191.488 89.8233 194.208 91.07 197.834 91.07Z" />
|
<path d="M197.154 97.02C193.188 97.02 189.873 96.17 187.209 94.47C184.546 92.7133 182.563 90.3617 181.259 87.415C179.956 84.4117 179.304 81.04 179.304 77.3C179.304 72.7667 180.154 69.055 181.854 66.165C183.554 63.2183 185.849 61.0933 188.739 59.79C191.629 58.43 194.916 57.75 198.599 57.75C202.566 57.75 205.881 58.6283 208.544 60.385C211.208 62.085 213.191 64.4083 214.494 67.355C215.798 70.3017 216.449 73.645 216.449 77.385C216.449 81.975 215.599 85.7433 213.899 88.69C212.199 91.58 209.904 93.705 207.014 95.065C204.124 96.3683 200.838 97.02 197.154 97.02ZM197.834 91.07C205.144 91.07 208.799 86.48 208.799 77.3C208.799 73.1633 207.893 69.8767 206.079 67.44C204.323 64.9467 201.659 63.7 198.089 63.7C194.123 63.7 191.261 64.9183 189.504 67.355C187.804 69.735 186.954 73.05 186.954 77.3C186.954 81.4933 187.861 84.8367 189.674 87.33C191.488 89.8233 194.208 91.07 197.834 91.07Z" />
|
||||||
<path d="M221.961 96V37.52L229.271 36.67V96H221.961Z" />
|
<path d="M221.961 96V37.52L229.271 36.67V96H221.961Z" />
|
||||||
<path d="M268.08 78.66H242.155C242.212 82.91 243.232 85.9983 245.215 87.925C247.255 89.8517 249.862 90.815 253.035 90.815C255.302 90.815 257.257 90.5317 258.9 89.965C260.6 89.3983 262.47 88.52 264.51 87.33L267.315 92.515C262.669 95.5183 257.795 97.02 252.695 97.02C247.255 97.02 242.892 95.5183 239.605 92.515C236.375 89.455 234.76 84.6383 234.76 78.065C234.76 74.325 235.44 70.925 236.8 67.865C238.16 64.805 240.172 62.3683 242.835 60.555C245.555 58.685 248.87 57.75 252.78 57.75C256.067 57.75 258.872 58.43 261.195 59.79C263.519 61.0933 265.275 62.9067 266.465 65.23C267.655 67.5533 268.25 70.2167 268.25 73.22L268.08 78.66ZM252.78 63.445C249.834 63.445 247.51 64.3233 245.81 66.08C244.167 67.8367 243.09 70.245 242.58 73.305H261.11V72.965C261.11 69.7917 260.374 67.4117 258.9 65.825C257.484 64.2383 255.444 63.445 252.78 63.445Z" />
|
<path d="M268.08 78.66H242.155C242.212 82.91 243.232 85.9983 245.215 87.925C247.255 89.8517 249.862 90.815 253.035 90.815C255.302 90.815 257.257 90.5317 258.9 89.965C260.6 89.3983 262.47 88.52 264.51 87.33L267.315 92.515C262.669 95.5183 257.795 97.02 252.695 97.02C247.255 97.02 242.892 95.5183 239.605 92.515C236.375 89.455 234.76 84.6383 234.76 78.065C234.76 74.325 235.44 70.925 236.8 67.865C238.16 64.805 240.172 62.3683 242.835 60.555C245.555 58.685 248.87 57.75 252.78 57.75C256.067 57.75 258.872 58.43 261.195 59.79C263.519 61.0933 265.275 62.9067 266.465 65.23C267.655 67.5533 268.25 70.2167 268.25 73.22L268.08 78.66ZM252.78 63.445C249.834 63.445 247.51 64.3233 245.81 66.08C244.167 67.8367 243.09 70.245 242.58 73.305H261.11V72.965C261.11 69.7917 260.374 67.4117 258.9 65.825C257.484 64.2383 255.444 63.445 252.78 63.445Z" />
|
||||||
<path d="M305.129 58.77V59.28L290.934 98.635C289.687 102.092 288.356 104.84 286.939 106.88C285.522 108.977 283.907 110.478 282.094 111.385C280.337 112.348 278.241 112.83 275.804 112.83C273.311 112.83 270.846 112.405 268.409 111.555L270.364 105.945C272.007 106.455 273.622 106.71 275.209 106.71C276.512 106.71 277.617 106.512 278.524 106.115C279.431 105.718 280.309 104.953 281.159 103.82C282.066 102.687 282.944 100.987 283.794 98.72L284.814 96H282.859L268.579 59.28V58.77H276.314L285.069 82.57L286.004 85.12L287.364 89.115L289.489 82.57L297.649 58.77H305.129Z" />
|
<path d="M305.129 58.77V59.28L290.934 98.635C289.687 102.092 288.356 104.84 286.939 106.88C285.522 108.977 283.907 110.478 282.094 111.385C280.337 112.348 278.241 112.83 275.804 112.83C273.311 112.83 270.846 112.405 268.409 111.555L270.364 105.945C272.007 106.455 273.622 106.71 275.209 106.71C276.512 106.71 277.617 106.512 278.524 106.115C279.431 105.718 280.309 104.953 281.159 103.82C282.066 102.687 282.944 100.987 283.794 98.72L284.814 96H282.859L268.579 59.28V58.77H276.314L285.069 82.57L286.004 85.12L287.364 89.115L289.489 82.57L297.649 58.77H305.129Z" />
|
||||||
<path d="M327.505 57.75C331.018 57.75 333.936 58.6 336.26 60.3C338.583 62 340.283 64.2667 341.36 67.1C342.493 69.9333 343.06 73.0217 343.06 76.365C343.06 80.6717 342.21 84.3833 340.51 87.5C338.866 90.6167 336.6 92.9967 333.71 94.64C330.82 96.2267 327.533 97.02 323.85 97.02C321.073 97.02 318.381 96.6517 315.775 95.915V111.81H308.465V58.77L315.775 57.92V61.235C317.815 60.1017 319.826 59.2517 321.81 58.685C323.85 58.0617 325.748 57.75 327.505 57.75ZM323.255 91.24C325.578 91.24 327.618 90.73 329.375 89.71C331.188 88.6333 332.605 86.99 333.625 84.78C334.701 82.57 335.24 79.8217 335.24 76.535C335.24 72.285 334.39 69.14 332.69 67.1C330.99 65.06 328.581 64.04 325.465 64.04C322.178 64.04 318.948 64.975 315.775 66.845V89.965C318.438 90.815 320.931 91.24 323.255 91.24Z" />
|
<path d="M327.505 57.75C331.018 57.75 333.936 58.6 336.26 60.3C338.583 62 340.283 64.2667 341.36 67.1C342.493 69.9333 343.06 73.0217 343.06 76.365C343.06 80.6717 342.21 84.3833 340.51 87.5C338.866 90.6167 336.6 92.9967 333.71 94.64C330.82 96.2267 327.533 97.02 323.85 97.02C321.073 97.02 318.381 96.6517 315.775 95.915V111.81H308.465V58.77L315.775 57.92V61.235C317.815 60.1017 319.826 59.2517 321.81 58.685C323.85 58.0617 325.748 57.75 327.505 57.75ZM323.255 91.24C325.578 91.24 327.618 90.73 329.375 89.71C331.188 88.6333 332.605 86.99 333.625 84.78C334.701 82.57 335.24 79.8217 335.24 76.535C335.24 72.285 334.39 69.14 332.69 67.1C330.99 65.06 328.581 64.04 325.465 64.04C322.178 64.04 318.948 64.975 315.775 66.845V89.965C318.438 90.815 320.931 91.24 323.255 91.24Z" />
|
||||||
<path d="M363.436 97.02C359.469 97.02 356.154 96.17 353.491 94.47C350.827 92.7133 348.844 90.3617 347.541 87.415C346.237 84.4117 345.586 81.04 345.586 77.3C345.586 72.7667 346.436 69.055 348.136 66.165C349.836 63.2183 352.131 61.0933 355.021 59.79C357.911 58.43 361.197 57.75 364.881 57.75C368.847 57.75 372.162 58.6283 374.826 60.385C377.489 62.085 379.472 64.4083 380.776 67.355C382.079 70.3017 382.731 73.645 382.731 77.385C382.731 81.975 381.881 85.7433 380.181 88.69C378.481 91.58 376.186 93.705 373.296 95.065C370.406 96.3683 367.119 97.02 363.436 97.02ZM364.116 91.07C371.426 91.07 375.081 86.48 375.081 77.3C375.081 73.1633 374.174 69.8767 372.361 67.44C370.604 64.9467 367.941 63.7 364.371 63.7C360.404 63.7 357.542 64.9183 355.786 67.355C354.086 69.735 353.236 73.05 353.236 77.3C353.236 81.4933 354.142 84.8367 355.956 87.33C357.769 89.8233 360.489 91.07 364.116 91.07Z" />
|
<path d="M363.436 97.02C359.469 97.02 356.154 96.17 353.491 94.47C350.827 92.7133 348.844 90.3617 347.541 87.415C346.237 84.4117 345.586 81.04 345.586 77.3C345.586 72.7667 346.436 69.055 348.136 66.165C349.836 63.2183 352.131 61.0933 355.021 59.79C357.911 58.43 361.197 57.75 364.881 57.75C368.847 57.75 372.162 58.6283 374.826 60.385C377.489 62.085 379.472 64.4083 380.776 67.355C382.079 70.3017 382.731 73.645 382.731 77.385C382.731 81.975 381.881 85.7433 380.181 88.69C378.481 91.58 376.186 93.705 373.296 95.065C370.406 96.3683 367.119 97.02 363.436 97.02ZM364.116 91.07C371.426 91.07 375.081 86.48 375.081 77.3C375.081 73.1633 374.174 69.8767 372.361 67.44C370.604 64.9467 367.941 63.7 364.371 63.7C360.404 63.7 357.542 64.9183 355.786 67.355C354.086 69.735 353.236 73.05 353.236 77.3C353.236 81.4933 354.142 84.8367 355.956 87.33C357.769 89.8233 360.489 91.07 364.116 91.07Z" />
|
||||||
<path d="M388.242 96V37.52L395.552 36.67V96H388.242Z" />
|
<path d="M388.242 96V37.52L395.552 36.67V96H388.242Z" />
|
||||||
<path d="M435.382 58.77V59.28L421.187 98.635C419.94 102.092 418.608 104.84 417.192 106.88C415.775 108.977 414.16 110.478 412.347 111.385C410.59 112.348 408.493 112.83 406.057 112.83C403.563 112.83 401.098 112.405 398.662 111.555L400.617 105.945C402.26 106.455 403.875 106.71 405.462 106.71C406.765 106.71 407.87 106.512 408.777 106.115C409.683 105.718 410.562 104.953 411.412 103.82C412.318 102.687 413.197 100.987 414.047 98.72L415.067 96H413.112L398.832 59.28V58.77H406.567L415.322 82.57L416.257 85.12L417.617 89.115L419.742 82.57L427.902 58.77H435.382Z" />
|
<path d="M435.382 58.77V59.28L421.187 98.635C419.94 102.092 418.608 104.84 417.192 106.88C415.775 108.977 414.16 110.478 412.347 111.385C410.59 112.348 408.493 112.83 406.057 112.83C403.563 112.83 401.098 112.405 398.662 111.555L400.617 105.945C402.26 106.455 403.875 106.71 405.462 106.71C406.765 106.71 407.87 106.512 408.777 106.115C409.683 105.718 410.562 104.953 411.412 103.82C412.318 102.687 413.197 100.987 414.047 98.72L415.067 96H413.112L398.832 59.28V58.77H406.567L415.322 82.57L416.257 85.12L417.617 89.115L419.742 82.57L427.902 58.77H435.382Z" />
|
||||||
</g>
|
</g>
|
||||||
<mask
|
<mask
|
||||||
id="mask0"
|
id="mask0"
|
||||||
mask-type="alpha"
|
mask-type="alpha"
|
||||||
maskUnits="userSpaceOnUse"
|
maskUnits="userSpaceOnUse"
|
||||||
x="45"
|
x="45"
|
||||||
y="25"
|
y="25"
|
||||||
width="142"
|
width="142"
|
||||||
height="142"
|
height="142"
|
||||||
>
|
>
|
||||||
<rect
|
<rect
|
||||||
x="115.711"
|
x="115.711"
|
||||||
y="25"
|
y="25"
|
||||||
width="100"
|
width="100"
|
||||||
height="100"
|
height="100"
|
||||||
transform="rotate(45 115.711 25)"
|
transform="rotate(45 115.711 25)"
|
||||||
fill="#C4C4C4"
|
fill="#C4C4C4"
|
||||||
/>
|
/>
|
||||||
</mask>
|
</mask>
|
||||||
<g mask="url(#mask0)">
|
<g mask="url(#mask0)">
|
||||||
<circle
|
<circle
|
||||||
cx="79.9999"
|
cx="79.9999"
|
||||||
cy="60"
|
cy="60"
|
||||||
r="46"
|
r="46"
|
||||||
fill="#F14343"
|
fill="#F14343"
|
||||||
stroke="#1D8227"
|
stroke="#1D8227"
|
||||||
strokeWidth="8"
|
strokeWidth="8"
|
||||||
/>
|
/>
|
||||||
<mask
|
<mask
|
||||||
id="mask1"
|
id="mask1"
|
||||||
mask-type="alpha"
|
mask-type="alpha"
|
||||||
maskUnits="userSpaceOnUse"
|
maskUnits="userSpaceOnUse"
|
||||||
x="30"
|
x="30"
|
||||||
y="10"
|
y="10"
|
||||||
width="100"
|
width="100"
|
||||||
height="100"
|
height="100"
|
||||||
>
|
>
|
||||||
<circle
|
<circle
|
||||||
cx="80"
|
cx="80"
|
||||||
cy="60"
|
cy="60"
|
||||||
r="46"
|
r="46"
|
||||||
fill="#F14343"
|
fill="#F14343"
|
||||||
stroke="#1D8227"
|
stroke="#1D8227"
|
||||||
strokeWidth="8"
|
strokeWidth="8"
|
||||||
/>
|
/>
|
||||||
</mask>
|
</mask>
|
||||||
<g mask="url(#mask1)">
|
<g mask="url(#mask1)">
|
||||||
{generateStripes(props.stripes)}
|
{generateStripes(props.stripes)}
|
||||||
<circle
|
<circle
|
||||||
cx="80"
|
cx="80"
|
||||||
cy="60"
|
cy="60"
|
||||||
r="46"
|
r="46"
|
||||||
stroke={props.circleOuterFill || palette.green200}
|
stroke={props.circleOuterFill || palette.green200}
|
||||||
strokeWidth="8"
|
strokeWidth="8"
|
||||||
/>
|
/>
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</g>
|
</g>
|
||||||
<defs>
|
</g>
|
||||||
<clipPath id="clip0">
|
</g>
|
||||||
<rect width="487" height="143" fill="white" />
|
<defs>
|
||||||
</clipPath>
|
<clipPath id="clip0">
|
||||||
</defs>
|
<rect width="487" height="143" fill="white" />
|
||||||
</svg>
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
);
|
);
|
||||||
|
|
|
@ -3,66 +3,64 @@ import { mediaQueryDefs } from './Breakpoints';
|
||||||
import { BreakpointContext, ScreenSize } from './Context';
|
import { BreakpointContext, ScreenSize } from './Context';
|
||||||
|
|
||||||
const resetScreen: ScreenSize = {
|
const resetScreen: ScreenSize = {
|
||||||
onSmallScreen: false,
|
onSmallScreen: false,
|
||||||
onTablet: false,
|
onTablet: false,
|
||||||
onDesktop: false,
|
onDesktop: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
export class BreakpointsProvider extends React.Component<{}, ScreenSize> {
|
export class BreakpointsProvider extends React.Component<{}, ScreenSize> {
|
||||||
public state = {
|
public state = {
|
||||||
...resetScreen,
|
...resetScreen,
|
||||||
onSmallScreen: true,
|
onSmallScreen: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
private mediaQueries: { [key in keyof ScreenSize]: MediaQueryList } = {
|
private mediaQueries: { [key in keyof ScreenSize]: MediaQueryList } = {
|
||||||
onSmallScreen: window.matchMedia(
|
onSmallScreen: window.matchMedia(
|
||||||
mediaQueryDefs.onSmallScreen.replace('@media screen and', '')
|
mediaQueryDefs.onSmallScreen.replace('@media screen and', '')
|
||||||
),
|
),
|
||||||
onTablet: window.matchMedia(
|
onTablet: window.matchMedia(mediaQueryDefs.onTablet.replace('@media screen and', '')),
|
||||||
mediaQueryDefs.onTablet.replace('@media screen and', '')
|
onDesktop: window.matchMedia(
|
||||||
),
|
mediaQueryDefs.onDesktop.replace('@media screen and', '')
|
||||||
onDesktop: window.matchMedia(
|
),
|
||||||
mediaQueryDefs.onDesktop.replace('@media screen and', '')
|
};
|
||||||
),
|
|
||||||
};
|
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
Object.entries(this.mediaQueries).forEach(([key, mediaQuery]) =>
|
Object.entries(this.mediaQueries).forEach(([key, mediaQuery]) =>
|
||||||
mediaQuery.addEventListener('change', this.handleMediaEvent)
|
mediaQuery.addEventListener('change', this.handleMediaEvent)
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
Object.entries(this.mediaQueries).forEach(([key, mediaQuery]) =>
|
||||||
|
mediaQuery.removeEventListener('change', this.handleMediaEvent)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleMediaEvent = (event: MediaQueryListEvent) => {
|
||||||
|
console.log('handleMediaEvent', { event });
|
||||||
|
this.setState({
|
||||||
|
...resetScreen,
|
||||||
|
...this.calculateScreen(),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
calculateScreen = () => {
|
||||||
|
if (this.mediaQueries.onDesktop.matches) {
|
||||||
|
return { onDesktop: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
if (this.mediaQueries.onTablet.matches) {
|
||||||
Object.entries(this.mediaQueries).forEach(([key, mediaQuery]) =>
|
return { onTablet: true };
|
||||||
mediaQuery.removeEventListener('change', this.handleMediaEvent)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
handleMediaEvent = (event: MediaQueryListEvent) => {
|
return { onSmallScreen: true };
|
||||||
console.log('handleMediaEvent', { event });
|
};
|
||||||
this.setState({
|
|
||||||
...resetScreen,
|
|
||||||
...this.calculateScreen(),
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
calculateScreen = () => {
|
render() {
|
||||||
if (this.mediaQueries.onDesktop.matches) {
|
return (
|
||||||
return { onDesktop: true };
|
<BreakpointContext.Provider value={{ screenSize: { ...this.state } }}>
|
||||||
}
|
{this.props.children}
|
||||||
|
</BreakpointContext.Provider>
|
||||||
if (this.mediaQueries.onTablet.matches) {
|
);
|
||||||
return { onTablet: true };
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return { onSmallScreen: true };
|
|
||||||
};
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<BreakpointContext.Provider value={{ screenSize: { ...this.state } }}>
|
|
||||||
{this.props.children}
|
|
||||||
</BreakpointContext.Provider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,9 +3,9 @@ import { BreakpointsProvider } from './BreakpointProvider';
|
||||||
import { BreakpointDebugTool } from './DebugTool';
|
import { BreakpointDebugTool } from './DebugTool';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
title: 'Atoms/Breakpoints',
|
title: 'Atoms/Breakpoints',
|
||||||
decorators: [(story) => <BreakpointsProvider>{story()}</BreakpointsProvider>],
|
decorators: [(story) => <BreakpointsProvider>{story()}</BreakpointsProvider>],
|
||||||
component: BreakpointDebugTool,
|
component: BreakpointDebugTool,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DebugTool = () => <BreakpointDebugTool />;
|
export const DebugTool = () => <BreakpointDebugTool />;
|
||||||
|
|
|
@ -1,16 +1,16 @@
|
||||||
export const breakpoints = {
|
export const breakpoints = {
|
||||||
onTablet: 768,
|
onTablet: 768,
|
||||||
onDesktop: 1024,
|
onDesktop: 1024,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const mediaQueryDefs = {
|
export const mediaQueryDefs = {
|
||||||
onSmallScreen: `@media screen and (max-width: ${breakpoints.onTablet - 1}px)`,
|
onSmallScreen: `@media screen and (max-width: ${breakpoints.onTablet - 1}px)`,
|
||||||
onTablet: `@media screen and (min-width: ${breakpoints.onTablet}px)`,
|
onTablet: `@media screen and (min-width: ${breakpoints.onTablet}px)`,
|
||||||
onDesktop: `@media screen and (min-width: ${breakpoints.onDesktop}px)`,
|
onDesktop: `@media screen and (min-width: ${breakpoints.onDesktop}px)`,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const onTablet = (...expressions: any) => {
|
export const onTablet = (...expressions: any) => {
|
||||||
return `
|
return `
|
||||||
${mediaQueryDefs.onTablet} {
|
${mediaQueryDefs.onTablet} {
|
||||||
${expressions.join()}
|
${expressions.join()}
|
||||||
}
|
}
|
||||||
|
@ -18,7 +18,7 @@ export const onTablet = (...expressions: any) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const onDesktop = (...expressions: any) => {
|
export const onDesktop = (...expressions: any) => {
|
||||||
return `
|
return `
|
||||||
${mediaQueryDefs.onDesktop} {
|
${mediaQueryDefs.onDesktop} {
|
||||||
${expressions.join()}
|
${expressions.join()}
|
||||||
}
|
}
|
||||||
|
@ -26,7 +26,7 @@ export const onDesktop = (...expressions: any) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const onSmallScreen = (...expressions: any) => {
|
export const onSmallScreen = (...expressions: any) => {
|
||||||
return `
|
return `
|
||||||
${mediaQueryDefs.onSmallScreen} {
|
${mediaQueryDefs.onSmallScreen} {
|
||||||
${expressions.join()}
|
${expressions.join()}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,21 +2,21 @@ import { withContext } from '@roleypoly/misc-utils/withContext';
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
|
|
||||||
export type ScreenSize = {
|
export type ScreenSize = {
|
||||||
onSmallScreen: boolean;
|
onSmallScreen: boolean;
|
||||||
onTablet: boolean;
|
onTablet: boolean;
|
||||||
onDesktop: boolean;
|
onDesktop: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type BreakpointProps = {
|
export type BreakpointProps = {
|
||||||
screenSize: ScreenSize;
|
screenSize: ScreenSize;
|
||||||
};
|
};
|
||||||
|
|
||||||
const defaultScreenSize: BreakpointProps = {
|
const defaultScreenSize: BreakpointProps = {
|
||||||
screenSize: {
|
screenSize: {
|
||||||
onSmallScreen: true,
|
onSmallScreen: true,
|
||||||
onDesktop: false,
|
onDesktop: false,
|
||||||
onTablet: false,
|
onTablet: false,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const BreakpointContext = React.createContext(defaultScreenSize);
|
export const BreakpointContext = React.createContext(defaultScreenSize);
|
||||||
|
@ -24,4 +24,4 @@ export const BreakpointContext = React.createContext(defaultScreenSize);
|
||||||
export const useBreakpointContext = () => React.useContext(BreakpointContext);
|
export const useBreakpointContext = () => React.useContext(BreakpointContext);
|
||||||
|
|
||||||
export const withBreakpoints = <T>(Component: React.ComponentType<T>) =>
|
export const withBreakpoints = <T>(Component: React.ComponentType<T>) =>
|
||||||
withContext(BreakpointContext, Component as any);
|
withContext(BreakpointContext, Component as any);
|
||||||
|
|
|
@ -4,53 +4,53 @@ import { onDesktop, onTablet } from './Breakpoints';
|
||||||
import { useBreakpointContext } from './Context';
|
import { useBreakpointContext } from './Context';
|
||||||
|
|
||||||
const DebuggerPosition = styled.div`
|
const DebuggerPosition = styled.div`
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
font-family: monospace;
|
font-family: monospace;
|
||||||
& > div {
|
& > div {
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const OnSmallScreen = styled.div`
|
const OnSmallScreen = styled.div`
|
||||||
display: block;
|
display: block;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const OnTablet = styled.div`
|
const OnTablet = styled.div`
|
||||||
display: none;
|
display: none;
|
||||||
${onTablet(`display: block;`)}
|
${onTablet(`display: block;`)}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const OnDesktop = styled.div`
|
const OnDesktop = styled.div`
|
||||||
display: none;
|
display: none;
|
||||||
${onDesktop`display: block;`}
|
${onDesktop`display: block;`}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const CSSBreakpointDebugger = () => (
|
const CSSBreakpointDebugger = () => (
|
||||||
<div>
|
<div>
|
||||||
<OnSmallScreen style={{ backgroundColor: 'red' }}>S</OnSmallScreen>
|
<OnSmallScreen style={{ backgroundColor: 'red' }}>S</OnSmallScreen>
|
||||||
<OnTablet style={{ backgroundColor: 'green' }}>T</OnTablet>
|
<OnTablet style={{ backgroundColor: 'green' }}>T</OnTablet>
|
||||||
<OnDesktop style={{ backgroundColor: 'blue' }}>D</OnDesktop>
|
<OnDesktop style={{ backgroundColor: 'blue' }}>D</OnDesktop>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
const JSBreakpointDebugger = () => {
|
const JSBreakpointDebugger = () => {
|
||||||
const {
|
const {
|
||||||
screenSize: { onTablet, onDesktop, onSmallScreen },
|
screenSize: { onTablet, onDesktop, onSmallScreen },
|
||||||
} = useBreakpointContext();
|
} = useBreakpointContext();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{onSmallScreen && <div style={{ backgroundColor: 'red' }}>S</div>}
|
{onSmallScreen && <div style={{ backgroundColor: 'red' }}>S</div>}
|
||||||
{onTablet && <div style={{ backgroundColor: 'green' }}>T</div>}
|
{onTablet && <div style={{ backgroundColor: 'green' }}>T</div>}
|
||||||
{onDesktop && <div style={{ backgroundColor: 'blue' }}>D</div>}
|
{onDesktop && <div style={{ backgroundColor: 'blue' }}>D</div>}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
export const BreakpointDebugTool = () => (
|
export const BreakpointDebugTool = () => (
|
||||||
<DebuggerPosition>
|
<DebuggerPosition>
|
||||||
<JSBreakpointDebugger />
|
<JSBreakpointDebugger />
|
||||||
<CSSBreakpointDebugger />
|
<CSSBreakpointDebugger />
|
||||||
</DebuggerPosition>
|
</DebuggerPosition>
|
||||||
);
|
);
|
||||||
|
|
|
@ -3,9 +3,9 @@ import * as React from 'react';
|
||||||
import { Button } from './Button';
|
import { Button } from './Button';
|
||||||
|
|
||||||
it('fires an onClick callback when clicked', () => {
|
it('fires an onClick callback when clicked', () => {
|
||||||
const mock = jest.fn();
|
const mock = jest.fn();
|
||||||
const view = shallow(<Button onClick={mock}>Button</Button>);
|
const view = shallow(<Button onClick={mock}>Button</Button>);
|
||||||
|
|
||||||
view.simulate('click');
|
view.simulate('click');
|
||||||
expect(mock).toBeCalled();
|
expect(mock).toBeCalled();
|
||||||
});
|
});
|
||||||
|
|
|
@ -2,24 +2,24 @@ import * as React from 'react';
|
||||||
import { Button as ButtonComponent } from './Button';
|
import { Button as ButtonComponent } from './Button';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
title: 'Atoms/Button',
|
title: 'Atoms/Button',
|
||||||
component: ButtonComponent,
|
component: ButtonComponent,
|
||||||
argTypes: {
|
argTypes: {
|
||||||
content: { control: 'text' },
|
content: { control: 'text' },
|
||||||
},
|
},
|
||||||
args: {
|
args: {
|
||||||
content: 'Press me!',
|
content: 'Press me!',
|
||||||
size: 'large',
|
size: 'large',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Large = ({ content, ...args }) => (
|
export const Large = ({ content, ...args }) => (
|
||||||
<ButtonComponent {...args}>{content}</ButtonComponent>
|
<ButtonComponent {...args}>{content}</ButtonComponent>
|
||||||
);
|
);
|
||||||
|
|
||||||
export const Small = ({ content, ...args }) => (
|
export const Small = ({ content, ...args }) => (
|
||||||
<ButtonComponent {...args}>{content}</ButtonComponent>
|
<ButtonComponent {...args}>{content}</ButtonComponent>
|
||||||
);
|
);
|
||||||
Small.args = {
|
Small.args = {
|
||||||
size: 'small',
|
size: 'small',
|
||||||
};
|
};
|
||||||
|
|
|
@ -4,105 +4,105 @@ import { text300, text400 } from '@roleypoly/design-system/atoms/typography';
|
||||||
import styled, { css } from 'styled-components';
|
import styled, { css } from 'styled-components';
|
||||||
|
|
||||||
export const IconContainer = styled.div`
|
export const IconContainer = styled.div`
|
||||||
margin-right: 0.6rem;
|
margin-right: 0.6rem;
|
||||||
font-size: 1.75em;
|
font-size: 1.75em;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const base = css`
|
const base = css`
|
||||||
${fontCSS}
|
${fontCSS}
|
||||||
|
|
||||||
appearance: none;
|
appearance: none;
|
||||||
display: block;
|
display: block;
|
||||||
background-color: ${palette.taupe300};
|
background-color: ${palette.taupe300};
|
||||||
color: ${palette.grey500};
|
color: ${palette.grey500};
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
border: 2px solid rgba(0, 0, 0, 0.55);
|
border: 2px solid rgba(0, 0, 0, 0.55);
|
||||||
|
transition: all 0.15s ease-in-out;
|
||||||
|
outline: 0;
|
||||||
|
position: relative;
|
||||||
|
user-select: none;
|
||||||
|
cursor: pointer;
|
||||||
|
white-space: nowrap;
|
||||||
|
|
||||||
|
::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
background-color: #000;
|
||||||
|
opacity: 0;
|
||||||
transition: all 0.15s ease-in-out;
|
transition: all 0.15s ease-in-out;
|
||||||
outline: 0;
|
}
|
||||||
position: relative;
|
|
||||||
user-select: none;
|
|
||||||
cursor: pointer;
|
|
||||||
white-space: nowrap;
|
|
||||||
|
|
||||||
|
:hover {
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
:active {
|
||||||
|
transform: translateY(1px);
|
||||||
|
box-shadow: 0 0 2px rgba(0, 0, 0, 0.25);
|
||||||
::after {
|
::after {
|
||||||
content: '';
|
opacity: 0.1;
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
bottom: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
background-color: #000;
|
|
||||||
opacity: 0;
|
|
||||||
transition: all 0.15s ease-in-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
:hover {
|
|
||||||
transform: translateY(-1px);
|
|
||||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.15);
|
|
||||||
}
|
|
||||||
|
|
||||||
:active {
|
|
||||||
transform: translateY(1px);
|
|
||||||
box-shadow: 0 0 2px rgba(0, 0, 0, 0.25);
|
|
||||||
::after {
|
|
||||||
opacity: 0.1;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const colors = {
|
const colors = {
|
||||||
primary: css`
|
primary: css`
|
||||||
background-color: ${palette.green400};
|
background-color: ${palette.green400};
|
||||||
color: ${palette.taupe100};
|
color: ${palette.taupe100};
|
||||||
`,
|
`,
|
||||||
secondary: css``,
|
secondary: css``,
|
||||||
discord: css`
|
discord: css`
|
||||||
background-color: ${palette.discord400};
|
background-color: ${palette.discord400};
|
||||||
border: 2px solid ${palette.discord200};
|
border: 2px solid ${palette.discord200};
|
||||||
`,
|
`,
|
||||||
muted: css`
|
muted: css`
|
||||||
border: 2px solid rgba(0, 0, 0, 0.15);
|
border: 2px solid rgba(0, 0, 0, 0.15);
|
||||||
background: none;
|
background: none;
|
||||||
:hover {
|
:hover {
|
||||||
background-color: ${palette.taupe200};
|
background-color: ${palette.taupe200};
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
};
|
};
|
||||||
|
|
||||||
const sizes = {
|
const sizes = {
|
||||||
small: css`
|
small: css`
|
||||||
${text300}
|
${text300}
|
||||||
|
|
||||||
padding: 4px 8px;
|
padding: 4px 8px;
|
||||||
`,
|
`,
|
||||||
large: css`
|
large: css`
|
||||||
${text400}
|
${text400}
|
||||||
|
|
||||||
padding: 12px 32px;
|
padding: 12px 32px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
`,
|
`,
|
||||||
};
|
};
|
||||||
|
|
||||||
const modifiers = {
|
const modifiers = {
|
||||||
withIcon: css`
|
withIcon: css`
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
`,
|
`,
|
||||||
withLoading: css`
|
withLoading: css`
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
`,
|
`,
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ButtonComposerOptions = {
|
export type ButtonComposerOptions = {
|
||||||
size: keyof typeof sizes;
|
size: keyof typeof sizes;
|
||||||
color: keyof typeof colors;
|
color: keyof typeof colors;
|
||||||
modifiers?: Array<keyof typeof modifiers>;
|
modifiers?: Array<keyof typeof modifiers>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Button = styled.button<ButtonComposerOptions>`
|
export const Button = styled.button<ButtonComposerOptions>`
|
||||||
${base}
|
${base}
|
||||||
${(props) => props.size in sizes && sizes[props.size]}
|
${(props) => props.size in sizes && sizes[props.size]}
|
||||||
${(props) => props.color in colors && colors[props.color]}
|
${(props) => props.color in colors && colors[props.color]}
|
||||||
${(props) => props.modifiers?.map((m) => modifiers[m])}
|
${(props) => props.modifiers?.map((m) => modifiers[m])}
|
||||||
`;
|
`;
|
||||||
|
|
|
@ -1,38 +1,38 @@
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import {
|
import {
|
||||||
Button as StyledButton,
|
Button as StyledButton,
|
||||||
ButtonComposerOptions,
|
ButtonComposerOptions,
|
||||||
IconContainer,
|
IconContainer,
|
||||||
} from './Button.styled';
|
} from './Button.styled';
|
||||||
|
|
||||||
export type ButtonProps = Partial<ButtonComposerOptions> & {
|
export type ButtonProps = Partial<ButtonComposerOptions> & {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
icon?: React.ReactNode;
|
icon?: React.ReactNode;
|
||||||
loading?: boolean;
|
loading?: boolean;
|
||||||
onClick?: () => void;
|
onClick?: () => void;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Button = (props: ButtonProps) => {
|
export const Button = (props: ButtonProps) => {
|
||||||
const modifiers: ButtonProps['modifiers'] = [];
|
const modifiers: ButtonProps['modifiers'] = [];
|
||||||
if (props.loading) {
|
if (props.loading) {
|
||||||
modifiers.push('withLoading');
|
modifiers.push('withLoading');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (props.icon) {
|
if (props.icon) {
|
||||||
modifiers.push('withIcon');
|
modifiers.push('withIcon');
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledButton
|
<StyledButton
|
||||||
size={props.size || 'large'}
|
size={props.size || 'large'}
|
||||||
color={props.color || 'primary'}
|
color={props.color || 'primary'}
|
||||||
modifiers={modifiers}
|
modifiers={modifiers}
|
||||||
onClick={props.onClick}
|
onClick={props.onClick}
|
||||||
disabled={props.disabled}
|
disabled={props.disabled}
|
||||||
>
|
>
|
||||||
{props.icon && <IconContainer>{props.icon}</IconContainer>}
|
{props.icon && <IconContainer>{props.icon}</IconContainer>}
|
||||||
<div>{props.children}</div>
|
<div>{props.children}</div>
|
||||||
</StyledButton>
|
</StyledButton>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -2,12 +2,12 @@ import { SmallTitle } from '@roleypoly/design-system/atoms/typography';
|
||||||
import { Collapse } from './Collapse';
|
import { Collapse } from './Collapse';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
title: 'Atoms/Collapse',
|
title: 'Atoms/Collapse',
|
||||||
component: Collapse,
|
component: Collapse,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const collapse = (args) => (
|
export const collapse = (args) => (
|
||||||
<SmallTitle>
|
<SmallTitle>
|
||||||
Hello, <Collapse {...args}>small</Collapse> world!
|
Hello, <Collapse {...args}>small</Collapse> world!
|
||||||
</SmallTitle>
|
</SmallTitle>
|
||||||
);
|
);
|
||||||
|
|
|
@ -2,9 +2,9 @@ import styled, { css } from 'styled-components';
|
||||||
import { onSmallScreen } from '../breakpoints';
|
import { onSmallScreen } from '../breakpoints';
|
||||||
|
|
||||||
export const Collapse = styled.span<{ preventCollapse?: boolean }>`
|
export const Collapse = styled.span<{ preventCollapse?: boolean }>`
|
||||||
${(props) =>
|
${(props) =>
|
||||||
!props.preventCollapse &&
|
!props.preventCollapse &&
|
||||||
onSmallScreen(css`
|
onSmallScreen(css`
|
||||||
display: none;
|
display: none;
|
||||||
`)}
|
`)}
|
||||||
`;
|
`;
|
||||||
|
|
|
@ -5,158 +5,158 @@ import styled from 'styled-components';
|
||||||
import { palette } from './colors';
|
import { palette } from './colors';
|
||||||
|
|
||||||
type RatioList = {
|
type RatioList = {
|
||||||
color1: string[];
|
color1: string[];
|
||||||
color2: string[];
|
color2: string[];
|
||||||
ratio: string;
|
ratio: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
title: 'Atoms/Colors',
|
title: 'Atoms/Colors',
|
||||||
};
|
};
|
||||||
|
|
||||||
const Swatch = styled.div`
|
const Swatch = styled.div`
|
||||||
box-shadow: 1px 1px 2px rgba(0, 0, 0, 0.25);
|
box-shadow: 1px 1px 2px rgba(0, 0, 0, 0.25);
|
||||||
width: 250px;
|
width: 250px;
|
||||||
height: 100px;
|
height: 100px;
|
||||||
margin: 10px;
|
margin: 10px;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
background-color: #fff;
|
background-color: #fff;
|
||||||
border: 1px solid #fff;
|
border: 1px solid #fff;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const SwatchColor = styled.div`
|
const SwatchColor = styled.div`
|
||||||
height: 72px;
|
height: 72px;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const Label = styled.div`
|
const Label = styled.div`
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
padding: 6px;
|
padding: 6px;
|
||||||
color: ${palette.taupe100};
|
color: ${palette.taupe100};
|
||||||
p {
|
p {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const Colors = () => {
|
export const Colors = () => {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{Object.entries(palette).map(([name, color], i) => (
|
{Object.entries(palette).map(([name, color], i) => (
|
||||||
<Swatch key={i}>
|
<Swatch key={i}>
|
||||||
<SwatchColor style={{ backgroundColor: color }} />
|
<SwatchColor style={{ backgroundColor: color }} />
|
||||||
<Label>
|
<Label>
|
||||||
<p>{name}</p>
|
<p>{name}</p>
|
||||||
<p>
|
<p>
|
||||||
<code>var(--{name})</code>
|
<code>var(--{name})</code>
|
||||||
</p>
|
</p>
|
||||||
</Label>
|
</Label>
|
||||||
</Swatch>
|
</Swatch>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ContrastRatios = () => {
|
export const ContrastRatios = () => {
|
||||||
const allRatios = getAllRatios(palette);
|
const allRatios = getAllRatios(palette);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<p>
|
<p>
|
||||||
<b>WCAG Contrast Calculations.</b>
|
<b>WCAG Contrast Calculations.</b>
|
||||||
<br />
|
<br />
|
||||||
Marked in <span style={getWCAGStyle(7.1)}>Green</span> is 7.0+ or AAA.
|
Marked in <span style={getWCAGStyle(7.1)}>Green</span> is 7.0+ or AAA. Acceptable
|
||||||
Acceptable for Text.
|
for Text.
|
||||||
<br />
|
<br />
|
||||||
Marked in <span style={getWCAGStyle(4.6)}>Orange</span> is 4.5+ or AA.
|
Marked in <span style={getWCAGStyle(4.6)}>Orange</span> is 4.5+ or AA. Acceptable
|
||||||
Acceptable for UI.
|
for UI.
|
||||||
<br />
|
<br />
|
||||||
All below 4.5 is unacceptable.
|
All below 4.5 is unacceptable.
|
||||||
<br />
|
<br />
|
||||||
<AmbientSmall>WCAG Contrast testing disabled for this page.</AmbientSmall>
|
<AmbientSmall>WCAG Contrast testing disabled for this page.</AmbientSmall>
|
||||||
</p>
|
</p>
|
||||||
<ContrastTable>
|
<ContrastTable>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th colSpan={2}>Swatch</th>
|
<th colSpan={2}>Swatch</th>
|
||||||
<th>Ratio</th>
|
<th>Ratio</th>
|
||||||
<th>Color 1</th>
|
<th>Color 1</th>
|
||||||
<th>Color 2</th>
|
<th>Color 2</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{allRatios.map((ratio, i) => (
|
{allRatios.map((ratio, i) => (
|
||||||
<tr key={i}>
|
<tr key={i}>
|
||||||
<td style={{ backgroundColor: ratio.color1[1] }}> </td>
|
<td style={{ backgroundColor: ratio.color1[1] }}> </td>
|
||||||
<td style={{ backgroundColor: ratio.color2[1] }}> </td>
|
<td style={{ backgroundColor: ratio.color2[1] }}> </td>
|
||||||
<td style={getWCAGStyle(+ratio.ratio)}>{ratio.ratio}</td>
|
<td style={getWCAGStyle(+ratio.ratio)}>{ratio.ratio}</td>
|
||||||
<td>{ratio.color1[0]}</td>
|
<td>{ratio.color1[0]}</td>
|
||||||
<td>{ratio.color2[0]}</td>
|
<td>{ratio.color2[0]}</td>
|
||||||
<td
|
<td
|
||||||
style={{
|
style={{
|
||||||
color: ratio.color1[1],
|
color: ratio.color1[1],
|
||||||
backgroundColor: ratio.color2[1],
|
backgroundColor: ratio.color2[1],
|
||||||
paddingRight: '0.1em',
|
paddingRight: '0.1em',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
oh my god my
|
oh my god my
|
||||||
</td>
|
</td>
|
||||||
<td
|
<td
|
||||||
style={{
|
style={{
|
||||||
color: ratio.color2[1],
|
color: ratio.color2[1],
|
||||||
backgroundColor: ratio.color1[1],
|
backgroundColor: ratio.color1[1],
|
||||||
paddingLeft: '0.1em',
|
paddingLeft: '0.1em',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
shin how dare you
|
shin how dare you
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
</tbody>
|
</tbody>
|
||||||
</ContrastTable>
|
</ContrastTable>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const ContrastTable = styled.table`
|
const ContrastTable = styled.table`
|
||||||
td,
|
td,
|
||||||
th {
|
th {
|
||||||
padding: 6px 10px;
|
padding: 6px 10px;
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const getWCAGStyle = (ratio: number): React.CSSProperties => {
|
const getWCAGStyle = (ratio: number): React.CSSProperties => {
|
||||||
if (ratio >= 7) {
|
if (ratio >= 7) {
|
||||||
return { color: 'green', fontWeight: 'bold' };
|
return { color: 'green', fontWeight: 'bold' };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ratio >= 4.5) {
|
if (ratio >= 4.5) {
|
||||||
return { color: 'orange', fontWeight: 'bold' };
|
return { color: 'orange', fontWeight: 'bold' };
|
||||||
}
|
}
|
||||||
|
|
||||||
return {};
|
return {};
|
||||||
};
|
};
|
||||||
|
|
||||||
const getAllRatios = (input: typeof palette) =>
|
const getAllRatios = (input: typeof palette) =>
|
||||||
Object.entries(input)
|
Object.entries(input)
|
||||||
.filter(([name]) => !name.startsWith('discord'))
|
.filter(([name]) => !name.startsWith('discord'))
|
||||||
.reduce((acc, [name, color]) => {
|
.reduce((acc, [name, color]) => {
|
||||||
return [
|
return [
|
||||||
...acc,
|
...acc,
|
||||||
...Object.entries(palette)
|
...Object.entries(palette)
|
||||||
.filter(([name]) => !name.startsWith('discord'))
|
.filter(([name]) => !name.startsWith('discord'))
|
||||||
.map(([matchName, matchColor]) => ({
|
.map(([matchName, matchColor]) => ({
|
||||||
color1: [name, color],
|
color1: [name, color],
|
||||||
color2: [matchName, matchColor],
|
color2: [matchName, matchColor],
|
||||||
ratio: chroma.contrast(color, matchColor).toFixed(2),
|
ratio: chroma.contrast(color, matchColor).toFixed(2),
|
||||||
})),
|
})),
|
||||||
];
|
];
|
||||||
}, [] as RatioList[])
|
}, [] as RatioList[])
|
||||||
.filter(({ ratio }) => +ratio !== 1)
|
.filter(({ ratio }) => +ratio !== 1)
|
||||||
.sort((a, b) => {
|
.sort((a, b) => {
|
||||||
if (+a.ratio > +b.ratio) {
|
if (+a.ratio > +b.ratio) {
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
return 1;
|
return 1;
|
||||||
})
|
})
|
||||||
.filter((_, i) => i % 2 === 0);
|
.filter((_, i) => i % 2 === 0);
|
||||||
|
|
|
@ -2,36 +2,36 @@ import chroma from 'chroma-js';
|
||||||
import { createGlobalStyle, css } from 'styled-components';
|
import { createGlobalStyle, css } from 'styled-components';
|
||||||
|
|
||||||
export const palette = {
|
export const palette = {
|
||||||
taupe100: '#332D2D',
|
taupe100: '#332D2D',
|
||||||
taupe200: '#453E3D',
|
taupe200: '#453E3D',
|
||||||
taupe300: '#5D5352',
|
taupe300: '#5D5352',
|
||||||
taupe400: '#756867',
|
taupe400: '#756867',
|
||||||
taupe500: '#AB9B9A',
|
taupe500: '#AB9B9A',
|
||||||
taupe600: '#EBD6D4',
|
taupe600: '#EBD6D4',
|
||||||
|
|
||||||
discord100: '#23272A',
|
discord100: '#23272A',
|
||||||
discord200: '#2C2F33',
|
discord200: '#2C2F33',
|
||||||
discord400: '#7289DA',
|
discord400: '#7289DA',
|
||||||
discord500: '#99AAB5',
|
discord500: '#99AAB5',
|
||||||
|
|
||||||
green400: '#46B646',
|
green400: '#46B646',
|
||||||
green200: '#1D8227',
|
green200: '#1D8227',
|
||||||
|
|
||||||
red400: '#E95353',
|
red400: '#E95353',
|
||||||
red200: '#F14343',
|
red200: '#F14343',
|
||||||
|
|
||||||
gold400: '#EFCF24',
|
gold400: '#EFCF24',
|
||||||
|
|
||||||
grey100: '#1C1010',
|
grey100: '#1C1010',
|
||||||
grey500: '#DBD9D9',
|
grey500: '#DBD9D9',
|
||||||
grey600: '#F2EFEF',
|
grey600: '#F2EFEF',
|
||||||
};
|
};
|
||||||
|
|
||||||
const getPaletteCSS = () =>
|
const getPaletteCSS = () =>
|
||||||
Object.entries(palette).reduce(
|
Object.entries(palette).reduce(
|
||||||
(acc, [key, color]) => ({ ...acc, [`--${key}`]: color }),
|
(acc, [key, color]) => ({ ...acc, [`--${key}`]: color }),
|
||||||
{}
|
{}
|
||||||
);
|
);
|
||||||
|
|
||||||
export const colorVars = css(getPaletteCSS());
|
export const colorVars = css(getPaletteCSS());
|
||||||
|
|
||||||
|
@ -42,5 +42,5 @@ export const GlobalStyleColors = createGlobalStyle`
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const numberToChroma = (colorInt: number) => {
|
export const numberToChroma = (colorInt: number) => {
|
||||||
return chroma(colorInt);
|
return chroma(colorInt);
|
||||||
};
|
};
|
||||||
|
|
|
@ -3,9 +3,9 @@ import styled from 'styled-components';
|
||||||
import { colorVars } from './colors';
|
import { colorVars } from './colors';
|
||||||
|
|
||||||
const ColorsContainer = styled.div`
|
const ColorsContainer = styled.div`
|
||||||
${colorVars}
|
${colorVars}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const withColors = (storyFn: () => React.ReactNode) => (
|
export const withColors = (storyFn: () => React.ReactNode) => (
|
||||||
<ColorsContainer>{storyFn()}</ColorsContainer>
|
<ColorsContainer>{storyFn()}</ColorsContainer>
|
||||||
);
|
);
|
||||||
|
|
|
@ -2,7 +2,7 @@ import * as React from 'react';
|
||||||
import { DotOverlay } from './DotOverlay';
|
import { DotOverlay } from './DotOverlay';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
title: 'Atoms/Dot Overlay',
|
title: 'Atoms/Dot Overlay',
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Dark = () => <DotOverlay />;
|
export const Dark = () => <DotOverlay />;
|
||||||
|
|
|
@ -2,37 +2,37 @@ import * as React from 'react';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
|
|
||||||
const dotOverlayBase = styled.div`
|
const dotOverlayBase = styled.div`
|
||||||
opacity: 0.6;
|
opacity: 0.6;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 0;
|
top: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
z-index: -10;
|
z-index: -10;
|
||||||
background-size: 27px 27px;
|
background-size: 27px 27px;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const DotOverlayDark = styled(dotOverlayBase)`
|
const DotOverlayDark = styled(dotOverlayBase)`
|
||||||
background-image: radial-gradient(
|
background-image: radial-gradient(
|
||||||
circle,
|
circle,
|
||||||
#332d2d,
|
#332d2d,
|
||||||
#332d2d 1px,
|
#332d2d 1px,
|
||||||
transparent 1px,
|
transparent 1px,
|
||||||
transparent
|
transparent
|
||||||
);
|
);
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const DotOverlayLight = styled(dotOverlayBase)`
|
const DotOverlayLight = styled(dotOverlayBase)`
|
||||||
background-image: radial-gradient(
|
background-image: radial-gradient(
|
||||||
circle,
|
circle,
|
||||||
#dbd9d9,
|
#dbd9d9,
|
||||||
#dbd9d9 1px,
|
#dbd9d9 1px,
|
||||||
transparent 1px,
|
transparent 1px,
|
||||||
transparent
|
transparent
|
||||||
);
|
);
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const DotOverlay = ({ light }: { light?: boolean }) => {
|
export const DotOverlay = ({ light }: { light?: boolean }) => {
|
||||||
return light ? <DotOverlayLight /> : <DotOverlayDark />;
|
return light ? <DotOverlayLight /> : <DotOverlayDark />;
|
||||||
};
|
};
|
||||||
|
|
|
@ -4,25 +4,25 @@ import * as React from 'react';
|
||||||
import { FaderOpacity, FaderSlide } from './Fader';
|
import { FaderOpacity, FaderSlide } from './Fader';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
title: 'Atoms/Fader',
|
title: 'Atoms/Fader',
|
||||||
component: FaderSlide,
|
component: FaderSlide,
|
||||||
args: {
|
args: {
|
||||||
isVisible: true,
|
isVisible: true,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Opacity = (args) => {
|
export const Opacity = (args) => {
|
||||||
return (
|
return (
|
||||||
<FaderOpacity {...args}>
|
<FaderOpacity {...args}>
|
||||||
<Button onClick={action('onClick')}>Click me!</Button>
|
<Button onClick={action('onClick')}>Click me!</Button>
|
||||||
</FaderOpacity>
|
</FaderOpacity>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Slide = (args) => {
|
export const Slide = (args) => {
|
||||||
return (
|
return (
|
||||||
<FaderSlide {...args}>
|
<FaderSlide {...args}>
|
||||||
<Button onClick={action('onClick')}>Click me!</Button>
|
<Button onClick={action('onClick')}>Click me!</Button>
|
||||||
</FaderSlide>
|
</FaderSlide>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -2,34 +2,32 @@ import * as React from 'react';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
|
|
||||||
export type FaderProps = {
|
export type FaderProps = {
|
||||||
isVisible: boolean;
|
isVisible: boolean;
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
};
|
};
|
||||||
|
|
||||||
const FaderOpacityStyled = styled.div<Pick<FaderProps, 'isVisible'>>`
|
const FaderOpacityStyled = styled.div<Pick<FaderProps, 'isVisible'>>`
|
||||||
opacity: ${(props) => (props.isVisible ? 1 : 0)};
|
opacity: ${(props) => (props.isVisible ? 1 : 0)};
|
||||||
pointer-events: ${(props) => (props.isVisible ? 'unset' : 'none')};
|
pointer-events: ${(props) => (props.isVisible ? 'unset' : 'none')};
|
||||||
transition: opacity 0.35s ease-in-out;
|
transition: opacity 0.35s ease-in-out;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const FaderOpacity = (props: FaderProps) => {
|
export const FaderOpacity = (props: FaderProps) => {
|
||||||
return (
|
return (
|
||||||
<FaderOpacityStyled isVisible={props.isVisible}>
|
<FaderOpacityStyled isVisible={props.isVisible}>{props.children}</FaderOpacityStyled>
|
||||||
{props.children}
|
);
|
||||||
</FaderOpacityStyled>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const FaderSlideStyled = styled.div<Pick<FaderProps, 'isVisible'>>`
|
const FaderSlideStyled = styled.div<Pick<FaderProps, 'isVisible'>>`
|
||||||
max-height: ${(props) => (props.isVisible ? '4em' : '0')};
|
max-height: ${(props) => (props.isVisible ? '4em' : '0')};
|
||||||
pointer-events: ${(props) => (props.isVisible ? 'unset' : 'none')};
|
pointer-events: ${(props) => (props.isVisible ? 'unset' : 'none')};
|
||||||
transition: max-height 0.35s ease-in-out;
|
transition: max-height 0.35s ease-in-out;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
transform: translateZ(0);
|
transform: translateZ(0);
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const FaderSlide = (props: FaderProps) => {
|
export const FaderSlide = (props: FaderProps) => {
|
||||||
return (
|
return (
|
||||||
<FaderSlideStyled isVisible={props.isVisible}>{props.children}</FaderSlideStyled>
|
<FaderSlideStyled isVisible={props.isVisible}>{props.children}</FaderSlideStyled>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -3,14 +3,14 @@ import * as React from 'react';
|
||||||
import { FeatureGate } from './FeatureGate';
|
import { FeatureGate } from './FeatureGate';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
title: 'Atoms/Feature Gate',
|
title: 'Atoms/Feature Gate',
|
||||||
decorators: [FeatureFlagDecorator(['AllowListBlockList'])],
|
decorators: [FeatureFlagDecorator(['AllowListBlockList'])],
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ActiveGate = () => (
|
export const ActiveGate = () => (
|
||||||
<FeatureGate featureFlag="AllowListBlockList">{() => <div>hello!</div>}</FeatureGate>
|
<FeatureGate featureFlag="AllowListBlockList">{() => <div>hello!</div>}</FeatureGate>
|
||||||
);
|
);
|
||||||
|
|
||||||
export const InactiveGate = () => (
|
export const InactiveGate = () => (
|
||||||
<FeatureGate featureFlag="aaa">{() => <div>hello!</div>}</FeatureGate>
|
<FeatureGate featureFlag="aaa">{() => <div>hello!</div>}</FeatureGate>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,20 +1,20 @@
|
||||||
import {
|
import {
|
||||||
FeatureFlag,
|
FeatureFlag,
|
||||||
FeatureFlagsContext,
|
FeatureFlagsContext,
|
||||||
} from '@roleypoly/misc-utils/featureFlags/react';
|
} from '@roleypoly/misc-utils/featureFlags/react';
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
|
|
||||||
export type FeatureGateProps = {
|
export type FeatureGateProps = {
|
||||||
featureFlag: FeatureFlag;
|
featureFlag: FeatureFlag;
|
||||||
children: () => React.ReactNode;
|
children: () => React.ReactNode;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const FeatureGate = (props: FeatureGateProps) => {
|
export const FeatureGate = (props: FeatureGateProps) => {
|
||||||
const featureContext = React.useContext(FeatureFlagsContext);
|
const featureContext = React.useContext(FeatureFlagsContext);
|
||||||
|
|
||||||
if (featureContext.has(props.featureFlag)) {
|
if (featureContext.has(props.featureFlag)) {
|
||||||
return props.children();
|
return props.children();
|
||||||
} else {
|
} else {
|
||||||
return <></>;
|
return <></>;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import {
|
import {
|
||||||
MediumTitle,
|
MediumTitle,
|
||||||
Text as TextBlock,
|
Text as TextBlock,
|
||||||
} from '@roleypoly/design-system/atoms/typography';
|
} from '@roleypoly/design-system/atoms/typography';
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
|
@ -9,54 +9,52 @@ import { UseFontStyled } from './fonts';
|
||||||
const resetFont = (storyFn: () => React.ReactNode) => <FontReset>{storyFn()}</FontReset>;
|
const resetFont = (storyFn: () => React.ReactNode) => <FontReset>{storyFn()}</FontReset>;
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
title: 'Atoms/Fonts',
|
title: 'Atoms/Fonts',
|
||||||
decorators: [resetFont],
|
decorators: [resetFont],
|
||||||
};
|
};
|
||||||
|
|
||||||
const FontReset = styled.div`
|
const FontReset = styled.div`
|
||||||
font-family: sans-serif;
|
font-family: sans-serif;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const CorrectlyFontedH2 = (props: { children: React.ReactNode }) => (
|
const CorrectlyFontedH2 = (props: { children: React.ReactNode }) => (
|
||||||
<UseFontStyled>
|
<UseFontStyled>
|
||||||
<MediumTitle>{props.children}</MediumTitle>
|
<MediumTitle>{props.children}</MediumTitle>
|
||||||
</UseFontStyled>
|
</UseFontStyled>
|
||||||
);
|
);
|
||||||
|
|
||||||
const Text = () => (
|
const Text = () => (
|
||||||
<>
|
<>
|
||||||
<p>
|
<p>
|
||||||
Lorem ipsum dolor sit, amet consectetur adipisicing elit. Et facilis alias
|
Lorem ipsum dolor sit, amet consectetur adipisicing elit. Et facilis alias placeat
|
||||||
placeat cumque sapiente ad delectus omnis quae. Reiciendis quibusdam deserunt
|
cumque sapiente ad delectus omnis quae. Reiciendis quibusdam deserunt repellat.
|
||||||
repellat. Exercitationem modi incidunt autem nemo tempore eaque soluta.
|
Exercitationem modi incidunt autem nemo tempore eaque soluta.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
帯カノ需混モイ一録43旧百12共ドレ能生ホクユ禁度ヨ材図クほはそ護関ラト郵張エノヨ議件クめざ県読れみとぶ論税クょンど慎転リつぎみ松期ほへド.
|
帯カノ需混モイ一録43旧百12共ドレ能生ホクユ禁度ヨ材図クほはそ護関ラト郵張エノヨ議件クめざ県読れみとぶ論税クょンど慎転リつぎみ松期ほへド.
|
||||||
縦投記ふで覧速っだせあ過先課フ演無ぎぱべ習併相ーす気6元ゆる領気希ぎ投代ラ我関レ森郎由系堂ず.
|
縦投記ふで覧速っだせあ過先課フ演無ぎぱべ習併相ーす気6元ゆる領気希ぎ投代ラ我関レ森郎由系堂ず.
|
||||||
読ケリ夜指ーっトせ認平引ウシ間花ヱクム年6台ぐ山婦ラスエ子著コア掲中ロ像属戸メソユ職諏ルど詐児題たに書希ク幕値長ラそめド.
|
読ケリ夜指ーっトせ認平引ウシ間花ヱクム年6台ぐ山婦ラスエ子著コア掲中ロ像属戸メソユ職諏ルど詐児題たに書希ク幕値長ラそめド.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
🔸🐕🔺💱🎊👽🐛 👨📼🕦📞 👱👆🍗👚🌈 🔝🔟🍉🔰🍲🏁🕗 🎡🐉🍲📻🔢🔄 💟💲🍻💜💩🔼
|
🔸🐕🔺💱🎊👽🐛 👨📼🕦📞 👱👆🍗👚🌈 🔝🔟🍉🔰🍲🏁🕗 🎡🐉🍲📻🔢🔄 💟💲🍻💜💩🔼
|
||||||
🎱🌸📛👫🌻 🗽🕜🐥👕🍈. 🐒🍚🔓📱🏦 🎦🌑🔛💙👣🔚 🔆🗻🌿🎳📲🍯 🌞💟🎌🍌 🔪📯🐎💮
|
🎱🌸📛👫🌻 🗽🕜🐥👕🍈. 🐒🍚🔓📱🏦 🎦🌑🔛💙👣🔚 🔆🗻🌿🎳📲🍯 🌞💟🎌🍌 🔪📯🐎💮
|
||||||
👌👭🎋🏉🏰 📓🕃🎂💉🔩 🐟🌇👺🌊🌒 📪👅🍂🍁 🌖🐮🔽🌒📊. 🔤🍍🌸📷🎴 💏🍌📎👥👉👒
|
👌👭🎋🏉🏰 📓🕃🎂💉🔩 🐟🌇👺🌊🌒 📪👅🍂🍁 🌖🐮🔽🌒📊. 🔤🍍🌸📷🎴 💏🍌📎👥👉👒
|
||||||
👝💜🔶🍣 💨🗼👈💉💉💰 🍐🕖🌰👝🕓🏊🐕 🏀📅📼📒 🐕🌈👋
|
👝💜🔶🍣 💨🗼👈💉💉💰 🍐🕖🌰👝🕓🏊🐕 🏀📅📼📒 🐕🌈👋
|
||||||
</p>
|
</p>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
||||||
export const Fonts = () => (
|
export const Fonts = () => (
|
||||||
<TextBlock>
|
<TextBlock>
|
||||||
<section>
|
<section>
|
||||||
<CorrectlyFontedH2>Unstyled Default</CorrectlyFontedH2>
|
<CorrectlyFontedH2>Unstyled Default</CorrectlyFontedH2>
|
||||||
<Text />
|
<Text />
|
||||||
</section>
|
</section>
|
||||||
<section>
|
<section>
|
||||||
<CorrectlyFontedH2>
|
<CorrectlyFontedH2>Main (Source Han Sans Japanese, Source Sans)</CorrectlyFontedH2>
|
||||||
Main (Source Han Sans Japanese, Source Sans)
|
<UseFontStyled>
|
||||||
</CorrectlyFontedH2>
|
<Text />
|
||||||
<UseFontStyled>
|
</UseFontStyled>
|
||||||
<Text />
|
</section>
|
||||||
</UseFontStyled>
|
</TextBlock>
|
||||||
</section>
|
|
||||||
</TextBlock>
|
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
import styled, { css } from 'styled-components';
|
import styled, { css } from 'styled-components';
|
||||||
|
|
||||||
export const fontCSS = css`
|
export const fontCSS = css`
|
||||||
font-family: 'source-han-sans-japanese', 'Source Sans Pro', sans-serif,
|
font-family: 'source-han-sans-japanese', 'Source Sans Pro', sans-serif,
|
||||||
'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol' !important;
|
'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol' !important;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const UseFontStyled = styled.div`
|
export const UseFontStyled = styled.div`
|
||||||
${fontCSS}
|
${fontCSS}
|
||||||
`;
|
`;
|
||||||
|
|
|
@ -2,12 +2,12 @@ import * as React from 'react';
|
||||||
import { HalfsiesContainer, HalfsiesItem } from './Halfsies';
|
import { HalfsiesContainer, HalfsiesItem } from './Halfsies';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
title: 'Atoms/Halfsies',
|
title: 'Atoms/Halfsies',
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Container = () => (
|
export const Container = () => (
|
||||||
<HalfsiesContainer>
|
<HalfsiesContainer>
|
||||||
<HalfsiesItem>Lefty doo</HalfsiesItem>
|
<HalfsiesItem>Lefty doo</HalfsiesItem>
|
||||||
<HalfsiesItem>Righty doo</HalfsiesItem>
|
<HalfsiesItem>Righty doo</HalfsiesItem>
|
||||||
</HalfsiesContainer>
|
</HalfsiesContainer>
|
||||||
);
|
);
|
||||||
|
|
|
@ -2,16 +2,16 @@ import { onTablet } from '@roleypoly/design-system/atoms/breakpoints';
|
||||||
import styled, { css } from 'styled-components';
|
import styled, { css } from 'styled-components';
|
||||||
|
|
||||||
export const HalfsiesContainer = styled.div`
|
export const HalfsiesContainer = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const HalfsiesItem = styled.div`
|
export const HalfsiesItem = styled.div`
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
flex: 1 1 100%;
|
flex: 1 1 100%;
|
||||||
${onTablet(css`
|
${onTablet(css`
|
||||||
flex: 1 2 50%;
|
flex: 1 2 50%;
|
||||||
`)}
|
`)}
|
||||||
`;
|
`;
|
||||||
|
|
|
@ -2,69 +2,69 @@ import * as React from 'react';
|
||||||
import { Hero as HeroComponent } from './Hero';
|
import { Hero as HeroComponent } from './Hero';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
title: 'Atoms/Hero',
|
title: 'Atoms/Hero',
|
||||||
component: HeroComponent,
|
component: HeroComponent,
|
||||||
args: {
|
args: {
|
||||||
topSpacing: 75,
|
topSpacing: 75,
|
||||||
bottomSpacing: 25,
|
bottomSpacing: 25,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Hero = ({ topSpacing, bottomSpacing }) => {
|
export const Hero = ({ topSpacing, bottomSpacing }) => {
|
||||||
return (
|
return (
|
||||||
<StoryWrapper topSpacing={topSpacing} bottomSpacing={bottomSpacing}>
|
<StoryWrapper topSpacing={topSpacing} bottomSpacing={bottomSpacing}>
|
||||||
<HeroComponent topSpacing={topSpacing} bottomSpacing={bottomSpacing}>
|
<HeroComponent topSpacing={topSpacing} bottomSpacing={bottomSpacing}>
|
||||||
<h1>This is it.</h1>
|
<h1>This is it.</h1>
|
||||||
</HeroComponent>
|
</HeroComponent>
|
||||||
</StoryWrapper>
|
</StoryWrapper>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
type WrapperProps = {
|
type WrapperProps = {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
topSpacing: number;
|
topSpacing: number;
|
||||||
bottomSpacing: number;
|
bottomSpacing: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
const StoryWrapper = ({ topSpacing, bottomSpacing, ...props }: WrapperProps) => (
|
const StoryWrapper = ({ topSpacing, bottomSpacing, ...props }: WrapperProps) => (
|
||||||
<div>
|
<div>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
top: 0,
|
top: 0,
|
||||||
bottom: 0,
|
bottom: 0,
|
||||||
left: 0,
|
left: 0,
|
||||||
right: 0,
|
right: 0,
|
||||||
height: '100vh',
|
height: '100vh',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
height: topSpacing,
|
height: topSpacing,
|
||||||
backgroundColor: 'rgba(255,0,0,0.25)',
|
backgroundColor: 'rgba(255,0,0,0.25)',
|
||||||
top: 0,
|
top: 0,
|
||||||
left: 0,
|
left: 0,
|
||||||
right: 0,
|
right: 0,
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
topSpacing
|
topSpacing
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
height: bottomSpacing,
|
height: bottomSpacing,
|
||||||
backgroundColor: 'rgba(0,0,255,0.25)',
|
backgroundColor: 'rgba(0,0,255,0.25)',
|
||||||
bottom: 0,
|
bottom: 0,
|
||||||
left: 0,
|
left: 0,
|
||||||
right: 0,
|
right: 0,
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
bottomSpacing
|
bottomSpacing
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
{props.children}
|
|
||||||
</div>
|
</div>
|
||||||
|
{props.children}
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -2,29 +2,29 @@ import * as React from 'react';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
|
|
||||||
type HeroContainerProps = {
|
type HeroContainerProps = {
|
||||||
topSpacing: number;
|
topSpacing: number;
|
||||||
bottomSpacing: number;
|
bottomSpacing: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
type HeroProps = Partial<HeroContainerProps> & {
|
type HeroProps = Partial<HeroContainerProps> & {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
};
|
};
|
||||||
|
|
||||||
const HeroContainer = styled.div<HeroContainerProps>`
|
const HeroContainer = styled.div<HeroContainerProps>`
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
min-height: calc(100vh - ${(props) => props.topSpacing + props.bottomSpacing}px);
|
min-height: calc(100vh - ${(props) => props.topSpacing + props.bottomSpacing}px);
|
||||||
margin-top: ${(props) => props.topSpacing}px;
|
margin-top: ${(props) => props.topSpacing}px;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const Hero = (props: HeroProps) => (
|
export const Hero = (props: HeroProps) => (
|
||||||
<HeroContainer
|
<HeroContainer
|
||||||
topSpacing={props.topSpacing || 0}
|
topSpacing={props.topSpacing || 0}
|
||||||
bottomSpacing={props.bottomSpacing || 0}
|
bottomSpacing={props.bottomSpacing || 0}
|
||||||
>
|
>
|
||||||
{props.children}
|
{props.children}
|
||||||
</HeroContainer>
|
</HeroContainer>
|
||||||
);
|
);
|
||||||
|
|
|
@ -2,32 +2,32 @@ import * as React from 'react';
|
||||||
import { HorizontalSwitch } from './HorizontalSwitch';
|
import { HorizontalSwitch } from './HorizontalSwitch';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
title: 'Atoms/Horizontal Switch',
|
title: 'Atoms/Horizontal Switch',
|
||||||
component: HorizontalSwitch,
|
component: HorizontalSwitch,
|
||||||
args: {
|
args: {
|
||||||
items: ['true', 'false'],
|
items: ['true', 'false'],
|
||||||
value: 'true',
|
value: 'true',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const Story = (args) => {
|
const Story = (args) => {
|
||||||
const [value, setValue] = React.useState(args.value);
|
const [value, setValue] = React.useState(args.value);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<HorizontalSwitch
|
<HorizontalSwitch
|
||||||
{...args}
|
{...args}
|
||||||
value={value}
|
value={value}
|
||||||
onChange={(a) => {
|
onChange={(a) => {
|
||||||
setValue(a);
|
setValue(a);
|
||||||
args.onChange(a);
|
args.onChange(a);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Switch = Story.bind({});
|
export const Switch = Story.bind({});
|
||||||
export const SwitchThree = Story.bind({});
|
export const SwitchThree = Story.bind({});
|
||||||
SwitchThree.args = {
|
SwitchThree.args = {
|
||||||
items: ['aaa', 'bbb', 'ccc'],
|
items: ['aaa', 'bbb', 'ccc'],
|
||||||
value: 'aaa',
|
value: 'aaa',
|
||||||
};
|
};
|
||||||
|
|
|
@ -3,21 +3,21 @@ import { transitions } from '@roleypoly/design-system/atoms/timings';
|
||||||
import styled, { css } from 'styled-components';
|
import styled, { css } from 'styled-components';
|
||||||
|
|
||||||
export const Item = styled.div<{ selected: boolean }>`
|
export const Item = styled.div<{ selected: boolean }>`
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
transition: background-color ease-in-out ${transitions.actionable}s;
|
transition: background-color ease-in-out ${transitions.actionable}s;
|
||||||
${(props) =>
|
${(props) =>
|
||||||
props.selected &&
|
props.selected &&
|
||||||
css`
|
css`
|
||||||
background-color: ${palette.taupe300};
|
background-color: ${palette.taupe300};
|
||||||
`}
|
`}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const Wrapper = styled.div`
|
export const Wrapper = styled.div`
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
border: 1px solid ${palette.taupe200};
|
border: 1px solid ${palette.taupe200};
|
||||||
border-radius: calc(1em + 20px);
|
border-radius: calc(1em + 20px);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
`;
|
`;
|
||||||
|
|
|
@ -2,27 +2,23 @@ import * as React from 'react';
|
||||||
import { Item, Wrapper } from './HorizontalSwitch.styled';
|
import { Item, Wrapper } from './HorizontalSwitch.styled';
|
||||||
|
|
||||||
export type SwitchProps = {
|
export type SwitchProps = {
|
||||||
items: string[];
|
items: string[];
|
||||||
value: string;
|
value: string;
|
||||||
onChange: (value: string) => void;
|
onChange: (value: string) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const HorizontalSwitch = (props: SwitchProps) => {
|
export const HorizontalSwitch = (props: SwitchProps) => {
|
||||||
const handleClick = (item: typeof props.value) => () => {
|
const handleClick = (item: typeof props.value) => () => {
|
||||||
props.onChange?.(item);
|
props.onChange?.(item);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Wrapper>
|
<Wrapper>
|
||||||
{props.items.map((item, idx) => (
|
{props.items.map((item, idx) => (
|
||||||
<Item
|
<Item key={idx} selected={item === props.value} onClick={handleClick(item)}>
|
||||||
key={idx}
|
{item}
|
||||||
selected={item === props.value}
|
</Item>
|
||||||
onClick={handleClick(item)}
|
))}
|
||||||
>
|
</Wrapper>
|
||||||
{item}
|
);
|
||||||
</Item>
|
|
||||||
))}
|
|
||||||
</Wrapper>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,19 +1,19 @@
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
|
|
||||||
export const globalOnKeyUp = (
|
export const globalOnKeyUp = (
|
||||||
key: string[],
|
key: string[],
|
||||||
action: () => any,
|
action: () => any,
|
||||||
isActive: boolean = true
|
isActive: boolean = true
|
||||||
) => {
|
) => {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const onKeyUp = (event: KeyboardEvent) => {
|
const onKeyUp = (event: KeyboardEvent) => {
|
||||||
if (isActive && key.includes(event.key)) {
|
if (isActive && key.includes(event.key)) {
|
||||||
action();
|
action();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
document.body.addEventListener('keyup', onKeyUp);
|
document.body.addEventListener('keyup', onKeyUp);
|
||||||
|
|
||||||
return () => document.body.removeEventListener('keyup', onKeyUp);
|
return () => document.body.removeEventListener('keyup', onKeyUp);
|
||||||
}, [key, action, isActive]);
|
}, [key, action, isActive]);
|
||||||
};
|
};
|
||||||
|
|
|
@ -3,32 +3,32 @@ import * as React from 'react';
|
||||||
import { Popover as PopoverComponent } from './Popover';
|
import { Popover as PopoverComponent } from './Popover';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
title: 'Atoms/Popover',
|
title: 'Atoms/Popover',
|
||||||
argTypes: {
|
argTypes: {
|
||||||
canDefocus: { control: 'boolean' },
|
canDefocus: { control: 'boolean' },
|
||||||
},
|
},
|
||||||
args: {
|
args: {
|
||||||
canDefocus: true,
|
canDefocus: true,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Popover = ({ canDefocus }) => {
|
export const Popover = ({ canDefocus }) => {
|
||||||
const [isOpen, setIsOpen] = React.useState(false);
|
const [isOpen, setIsOpen] = React.useState(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ padding: 50 }}>
|
<div style={{ padding: 50 }}>
|
||||||
<Button size="small" onClick={() => setIsOpen(!isOpen)}>
|
<Button size="small" onClick={() => setIsOpen(!isOpen)}>
|
||||||
{!isOpen ? 'Open' : 'Close'} me!
|
{!isOpen ? 'Open' : 'Close'} me!
|
||||||
</Button>
|
</Button>
|
||||||
<PopoverComponent
|
<PopoverComponent
|
||||||
position="top right"
|
position="top right"
|
||||||
active={isOpen}
|
active={isOpen}
|
||||||
onExit={() => setIsOpen(false)}
|
onExit={() => setIsOpen(false)}
|
||||||
canDefocus={canDefocus}
|
canDefocus={canDefocus}
|
||||||
headContent={<>Hello c:</>}
|
headContent={<>Hello c:</>}
|
||||||
>
|
>
|
||||||
stuff
|
stuff
|
||||||
</PopoverComponent>
|
</PopoverComponent>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -4,87 +4,87 @@ import { transitions } from '@roleypoly/design-system/atoms/timings';
|
||||||
import styled, { css } from 'styled-components';
|
import styled, { css } from 'styled-components';
|
||||||
|
|
||||||
type PopoverStyledProps = {
|
type PopoverStyledProps = {
|
||||||
active: boolean;
|
active: boolean;
|
||||||
preferredWidth?: number;
|
preferredWidth?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const PopoverBase = styled.div<PopoverStyledProps>`
|
export const PopoverBase = styled.div<PopoverStyledProps>`
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
background-color: ${palette.taupe100};
|
background-color: ${palette.taupe100};
|
||||||
padding: 5px;
|
padding: 5px;
|
||||||
border: 2px solid rgba(0, 0, 0, 0.15);
|
border: 2px solid rgba(0, 0, 0, 0.15);
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
transition: opacity ${transitions.out2in}s ease-in,
|
transition: opacity ${transitions.out2in}s ease-in,
|
||||||
transform ${transitions.out2in}s ease-in;
|
transform ${transitions.out2in}s ease-in;
|
||||||
min-width: ${(props) => props.preferredWidth || 320}px;
|
min-width: ${(props) => props.preferredWidth || 320}px;
|
||||||
max-width: 100vw;
|
max-width: 100vw;
|
||||||
${(props) =>
|
${(props) =>
|
||||||
!props.active &&
|
!props.active &&
|
||||||
css`
|
css`
|
||||||
transform: translateY(-2vh);
|
transform: translateY(-2vh);
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
`}
|
`}
|
||||||
${onSmallScreen(
|
${onSmallScreen(
|
||||||
css`
|
css`
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
min-width: unset;
|
min-width: unset;
|
||||||
width: 100vw;
|
width: 100vw;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
`
|
`
|
||||||
)};
|
)};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const DefocusHandler = styled.div<PopoverStyledProps>`
|
export const DefocusHandler = styled.div<PopoverStyledProps>`
|
||||||
background-color: rgba(0, 0, 0, 0.01);
|
background-color: rgba(0, 0, 0, 0.01);
|
||||||
position: fixed;
|
position: fixed;
|
||||||
z-index: -1;
|
z-index: -1;
|
||||||
top: 0;
|
top: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
${(props) =>
|
${(props) =>
|
||||||
!props.active &&
|
!props.active &&
|
||||||
css`
|
css`
|
||||||
display: none;
|
display: none;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
`}
|
`}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const PopoverHead = styled.div`
|
export const PopoverHead = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const PopoverHeadCloser = styled.div`
|
export const PopoverHeadCloser = styled.div`
|
||||||
flex: 0;
|
flex: 0;
|
||||||
font-size: 2em;
|
font-size: 2em;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
margin-right: 10px;
|
margin-right: 10px;
|
||||||
border-radius: 2em;
|
border-radius: 2em;
|
||||||
min-width: 1.4em;
|
min-width: 1.4em;
|
||||||
height: 1.4em;
|
height: 1.4em;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
${onTablet(
|
${onTablet(
|
||||||
css`
|
css`
|
||||||
display: none;
|
display: none;
|
||||||
`
|
`
|
||||||
)}
|
)}
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background: rgba(0, 0, 0, 0.1);
|
background: rgba(0, 0, 0, 0.1);
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const PopoverContent = styled.div`
|
export const PopoverContent = styled.div`
|
||||||
padding: 5px;
|
padding: 5px;
|
||||||
overflow-y: hidden;
|
overflow-y: hidden;
|
||||||
`;
|
`;
|
||||||
|
|
|
@ -2,42 +2,39 @@ import { globalOnKeyUp } from '@roleypoly/design-system/atoms/key-events';
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { IoMdClose } from 'react-icons/io';
|
import { IoMdClose } from 'react-icons/io';
|
||||||
import {
|
import {
|
||||||
DefocusHandler,
|
DefocusHandler,
|
||||||
PopoverBase,
|
PopoverBase,
|
||||||
PopoverContent,
|
PopoverContent,
|
||||||
PopoverHead,
|
PopoverHead,
|
||||||
PopoverHeadCloser,
|
PopoverHeadCloser,
|
||||||
} from './Popover.styled';
|
} from './Popover.styled';
|
||||||
|
|
||||||
type PopoverProps = {
|
type PopoverProps = {
|
||||||
children: () => React.ReactNode;
|
children: () => React.ReactNode;
|
||||||
position: 'top left' | 'top right' | 'bottom left' | 'bottom right';
|
position: 'top left' | 'top right' | 'bottom left' | 'bottom right';
|
||||||
active: boolean;
|
active: boolean;
|
||||||
canDefocus?: boolean;
|
canDefocus?: boolean;
|
||||||
onExit?: (type: 'escape' | 'defocus' | 'explicit') => void;
|
onExit?: (type: 'escape' | 'defocus' | 'explicit') => void;
|
||||||
headContent: React.ReactNode;
|
headContent: React.ReactNode;
|
||||||
preferredWidth?: number;
|
preferredWidth?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Popover = (props: PopoverProps) => {
|
export const Popover = (props: PopoverProps) => {
|
||||||
globalOnKeyUp(['Escape'], () => props.onExit?.('escape'), props.active);
|
globalOnKeyUp(['Escape'], () => props.onExit?.('escape'), props.active);
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<PopoverBase active={props.active} preferredWidth={props.preferredWidth}>
|
<PopoverBase active={props.active} preferredWidth={props.preferredWidth}>
|
||||||
<PopoverHead>
|
<PopoverHead>
|
||||||
<PopoverHeadCloser onClick={() => props.onExit?.('explicit')}>
|
<PopoverHeadCloser onClick={() => props.onExit?.('explicit')}>
|
||||||
<IoMdClose />
|
<IoMdClose />
|
||||||
</PopoverHeadCloser>
|
</PopoverHeadCloser>
|
||||||
<div>{props.headContent}</div>
|
<div>{props.headContent}</div>
|
||||||
</PopoverHead>
|
</PopoverHead>
|
||||||
<PopoverContent>{props.children()}</PopoverContent>
|
<PopoverContent>{props.children()}</PopoverContent>
|
||||||
</PopoverBase>
|
</PopoverBase>
|
||||||
{props.canDefocus && (
|
{props.canDefocus && (
|
||||||
<DefocusHandler
|
<DefocusHandler active={props.active} onClick={() => props.onExit?.('defocus')} />
|
||||||
active={props.active}
|
)}
|
||||||
onClick={() => props.onExit?.('defocus')}
|
</>
|
||||||
/>
|
);
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
|
@ -4,10 +4,10 @@ import { roleCategory } from '../../fixtures/storyData';
|
||||||
import { Role } from './Role';
|
import { Role } from './Role';
|
||||||
|
|
||||||
it('fires an OnClick handler when clicked', () => {
|
it('fires an OnClick handler when clicked', () => {
|
||||||
const onClickMock = jest.fn();
|
const onClickMock = jest.fn();
|
||||||
const view = shallow(
|
const view = shallow(
|
||||||
<Role role={roleCategory[0]} selected={true} onClick={onClickMock} />
|
<Role role={roleCategory[0]} selected={true} onClick={onClickMock} />
|
||||||
);
|
);
|
||||||
view.simulate('click');
|
view.simulate('click');
|
||||||
expect(onClickMock).toBeCalledWith(false);
|
expect(onClickMock).toBeCalledWith(false);
|
||||||
});
|
});
|
||||||
|
|
|
@ -5,75 +5,65 @@ import { roleCategory } from '../../fixtures/storyData';
|
||||||
import { Role as RoleComponent } from './Role';
|
import { Role as RoleComponent } from './Role';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
title: 'Atoms/Role',
|
title: 'Atoms/Role',
|
||||||
component: RoleComponent,
|
component: RoleComponent,
|
||||||
decorators: [withColors],
|
decorators: [withColors],
|
||||||
};
|
};
|
||||||
|
|
||||||
const Demo = styled.div`
|
const Demo = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const RoleWithState = (props: any) => {
|
const RoleWithState = (props: any) => {
|
||||||
const [selected, updateSelected] = React.useState(false);
|
const [selected, updateSelected] = React.useState(false);
|
||||||
return (
|
return (
|
||||||
<div style={{ padding: 5 }}>
|
<div style={{ padding: 5 }}>
|
||||||
<RoleComponent
|
<RoleComponent
|
||||||
{...props}
|
{...props}
|
||||||
selected={selected}
|
selected={selected}
|
||||||
onClick={(next) => updateSelected(next)}
|
onClick={(next) => updateSelected(next)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Role = () => (
|
export const Role = () => (
|
||||||
<Demo>
|
<Demo>
|
||||||
{roleCategory.map((c, idx) => (
|
{roleCategory.map((c, idx) => (
|
||||||
<RoleWithState key={idx} role={c} />
|
<RoleWithState key={idx} role={c} />
|
||||||
))}
|
))}
|
||||||
</Demo>
|
</Demo>
|
||||||
);
|
);
|
||||||
|
|
||||||
export const Selected = () => (
|
export const Selected = () => (
|
||||||
<Demo>
|
<Demo>
|
||||||
{roleCategory.map((c, idx) => (
|
{roleCategory.map((c, idx) => (
|
||||||
<RoleComponent key={idx} role={c} selected={true} />
|
<RoleComponent key={idx} role={c} selected={true} />
|
||||||
))}
|
))}
|
||||||
</Demo>
|
</Demo>
|
||||||
);
|
);
|
||||||
|
|
||||||
export const Unselected = () => (
|
export const Unselected = () => (
|
||||||
<Demo>
|
<Demo>
|
||||||
{roleCategory.map((c, idx) => (
|
{roleCategory.map((c, idx) => (
|
||||||
<RoleComponent key={idx} role={c} selected={false} />
|
<RoleComponent key={idx} role={c} selected={false} />
|
||||||
))}
|
))}
|
||||||
</Demo>
|
</Demo>
|
||||||
);
|
);
|
||||||
|
|
||||||
export const DisabledByPosition = () => (
|
export const DisabledByPosition = () => (
|
||||||
<Demo>
|
<Demo>
|
||||||
{roleCategory.map((c, idx) => (
|
{roleCategory.map((c, idx) => (
|
||||||
<RoleComponent
|
<RoleComponent key={idx} role={{ ...c, safety: 1 }} selected={false} disabled />
|
||||||
key={idx}
|
))}
|
||||||
role={{ ...c, safety: 1 }}
|
</Demo>
|
||||||
selected={false}
|
|
||||||
disabled
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</Demo>
|
|
||||||
);
|
);
|
||||||
|
|
||||||
export const DisabledByDanger = () => (
|
export const DisabledByDanger = () => (
|
||||||
<Demo>
|
<Demo>
|
||||||
{roleCategory.map((c, idx) => (
|
{roleCategory.map((c, idx) => (
|
||||||
<RoleComponent
|
<RoleComponent key={idx} role={{ ...c, safety: 2 }} selected={false} disabled />
|
||||||
key={idx}
|
))}
|
||||||
role={{ ...c, safety: 2 }}
|
</Demo>
|
||||||
selected={false}
|
|
||||||
disabled
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</Demo>
|
|
||||||
);
|
);
|
||||||
|
|
|
@ -3,80 +3,80 @@ import { transitions } from '@roleypoly/design-system/atoms/timings';
|
||||||
import styled, { css } from 'styled-components';
|
import styled, { css } from 'styled-components';
|
||||||
|
|
||||||
export type StyledProps = {
|
export type StyledProps = {
|
||||||
selected: boolean;
|
selected: boolean;
|
||||||
defaultColor: boolean;
|
defaultColor: boolean;
|
||||||
disabled: boolean;
|
disabled: boolean;
|
||||||
type?: 'delete';
|
type?: 'delete';
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Circle = styled.div<StyledProps>`
|
export const Circle = styled.div<StyledProps>`
|
||||||
width: 24px;
|
width: 24px;
|
||||||
height: 24px;
|
height: 24px;
|
||||||
border-radius: 25px;
|
border-radius: 25px;
|
||||||
background-color: ${(props) =>
|
background-color: ${(props) =>
|
||||||
props.defaultColor && !props.selected ? 'transparent' : 'var(--role-color)'};
|
props.defaultColor && !props.selected ? 'transparent' : 'var(--role-color)'};
|
||||||
border: 1px solid
|
border: 1px solid
|
||||||
${(props) =>
|
${(props) =>
|
||||||
props.defaultColor
|
props.defaultColor
|
||||||
? 'var(--role-color)'
|
? 'var(--role-color)'
|
||||||
: props.selected
|
: props.selected
|
||||||
? 'var(--role-accent)'
|
? 'var(--role-accent)'
|
||||||
: 'transparent'};
|
: 'transparent'};
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
transition: border ${transitions.in2in}s ease-in-out,
|
transition: border ${transitions.in2in}s ease-in-out,
|
||||||
background-color ${transitions.in2in}s ease-in-out;
|
background-color ${transitions.in2in}s ease-in-out;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
|
||||||
svg {
|
svg {
|
||||||
width: 10px;
|
width: 10px;
|
||||||
height: 10px;
|
height: 10px;
|
||||||
fill-opacity: ${(props) => (props.selected || props.disabled ? 1 : 0)};
|
fill-opacity: ${(props) => (props.selected || props.disabled ? 1 : 0)};
|
||||||
transition: fill-opacity ${transitions.in2in}s ease-in-out;
|
transition: fill-opacity ${transitions.in2in}s ease-in-out;
|
||||||
fill: ${(props) =>
|
fill: ${(props) =>
|
||||||
props.disabled && props.defaultColor
|
props.disabled && props.defaultColor
|
||||||
? 'var(--role-color)'
|
? 'var(--role-color)'
|
||||||
: 'var(--role-contrast)'};
|
: 'var(--role-contrast)'};
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const Outer = styled.div<StyledProps>`
|
export const Outer = styled.div<StyledProps>`
|
||||||
border-radius: 24px;
|
border-radius: 24px;
|
||||||
background-color: ${(props) =>
|
background-color: ${(props) =>
|
||||||
props.selected && !props.defaultColor ? 'var(--role-color)' : palette.taupe100};
|
props.selected && !props.defaultColor ? 'var(--role-color)' : palette.taupe100};
|
||||||
color: ${(props) => (props.selected ? 'var(--role-contrast)' : palette.grey600)};
|
color: ${(props) => (props.selected ? 'var(--role-contrast)' : palette.grey600)};
|
||||||
transition: color ${transitions.in2in}s ease-in-out,
|
transition: color ${transitions.in2in}s ease-in-out,
|
||||||
background-color ${transitions.in2in}s ease-in-out,
|
background-color ${transitions.in2in}s ease-in-out,
|
||||||
transform ${transitions.actionable}s ease-in-out,
|
transform ${transitions.actionable}s ease-in-out,
|
||||||
box-shadow ${transitions.actionable}s ease-in-out;
|
box-shadow ${transitions.actionable}s ease-in-out;
|
||||||
display: flex;
|
display: flex;
|
||||||
padding: 4px;
|
padding: 4px;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
${(props) =>
|
${(props) =>
|
||||||
!props.disabled
|
!props.disabled
|
||||||
? css`
|
? css`
|
||||||
&:hover {
|
&:hover {
|
||||||
transform: translateY(-2px);
|
transform: translateY(-2px);
|
||||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.15);
|
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.15);
|
||||||
${Circle} svg {
|
${Circle} svg {
|
||||||
fill-opacity: 1;
|
fill-opacity: 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&:active {
|
&:active {
|
||||||
transform: translateY(0);
|
transform: translateY(0);
|
||||||
box-shadow: 0 0 0 transparent;
|
box-shadow: 0 0 0 transparent;
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
: null};
|
: null};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const Text = styled.div`
|
export const Text = styled.div`
|
||||||
padding: 0 4px;
|
padding: 0 4px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
`;
|
`;
|
||||||
|
|
|
@ -7,94 +7,94 @@ import { FaCheck, FaTimes } from 'react-icons/fa';
|
||||||
import * as styled from './Role.styled';
|
import * as styled from './Role.styled';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
role: RPCRole;
|
role: RPCRole;
|
||||||
selected: boolean;
|
selected: boolean;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
onClick?: (newState: boolean) => void;
|
onClick?: (newState: boolean) => void;
|
||||||
tooltipId?: string;
|
tooltipId?: string;
|
||||||
type?: 'delete';
|
type?: 'delete';
|
||||||
};
|
};
|
||||||
|
|
||||||
const getColorsFromBase = (baseColor: chroma.Color, contrastCheckThrow: number = 5) => {
|
const getColorsFromBase = (baseColor: chroma.Color, contrastCheckThrow: number = 5) => {
|
||||||
// Which has more contrast? Stepping up or stepping down?
|
// Which has more contrast? Stepping up or stepping down?
|
||||||
const contrastColorUp = baseColor.brighten(contrastCheckThrow);
|
const contrastColorUp = baseColor.brighten(contrastCheckThrow);
|
||||||
const contrastColorDown = baseColor.darken(contrastCheckThrow);
|
const contrastColorDown = baseColor.darken(contrastCheckThrow);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
chroma.contrast(baseColor, contrastColorUp) >
|
chroma.contrast(baseColor, contrastColorUp) >
|
||||||
chroma.contrast(baseColor, contrastColorDown)
|
chroma.contrast(baseColor, contrastColorDown)
|
||||||
) {
|
) {
|
||||||
return {
|
return {
|
||||||
contrastColor: baseColor.brighten(3),
|
contrastColor: baseColor.brighten(3),
|
||||||
accentColor: baseColor.brighten(2),
|
accentColor: baseColor.brighten(2),
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
return {
|
return {
|
||||||
contrastColor: baseColor.darken(3),
|
contrastColor: baseColor.darken(3),
|
||||||
accentColor: baseColor.darken(2),
|
accentColor: baseColor.darken(2),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Role = (props: Props) => {
|
export const Role = (props: Props) => {
|
||||||
const colorVars = {
|
const colorVars = {
|
||||||
'--role-color': 'white',
|
'--role-color': 'white',
|
||||||
'--role-contrast': 'hsl(0,0,0%)',
|
'--role-contrast': 'hsl(0,0,0%)',
|
||||||
'--role-accent': 'hsl(0,0,70%)',
|
'--role-accent': 'hsl(0,0,70%)',
|
||||||
};
|
};
|
||||||
|
|
||||||
if (props.role.color !== 0) {
|
if (props.role.color !== 0) {
|
||||||
const baseColor = numberToChroma(props.role.color);
|
const baseColor = numberToChroma(props.role.color);
|
||||||
const { accentColor, contrastColor } = getColorsFromBase(baseColor, 5);
|
const { accentColor, contrastColor } = getColorsFromBase(baseColor, 5);
|
||||||
colorVars['--role-color'] = baseColor.css();
|
colorVars['--role-color'] = baseColor.css();
|
||||||
colorVars['--role-accent'] = accentColor.css();
|
colorVars['--role-accent'] = accentColor.css();
|
||||||
colorVars['--role-contrast'] = contrastColor.css();
|
colorVars['--role-contrast'] = contrastColor.css();
|
||||||
}
|
}
|
||||||
|
|
||||||
const styledProps: styled.StyledProps = {
|
const styledProps: styled.StyledProps = {
|
||||||
selected: props.selected,
|
selected: props.selected,
|
||||||
defaultColor: props.role.color === 0,
|
defaultColor: props.role.color === 0,
|
||||||
disabled: !!props.disabled,
|
disabled: !!props.disabled,
|
||||||
type: props.type,
|
type: props.type,
|
||||||
};
|
};
|
||||||
|
|
||||||
const extra = !props.disabled
|
const extra = !props.disabled
|
||||||
? {}
|
? {}
|
||||||
: {
|
: {
|
||||||
'data-tip': disabledReason(props.role),
|
'data-tip': disabledReason(props.role),
|
||||||
'data-for': props.tooltipId,
|
'data-for': props.tooltipId,
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<styled.Outer
|
<styled.Outer
|
||||||
{...styledProps}
|
{...styledProps}
|
||||||
style={colorVars as any}
|
style={colorVars as any}
|
||||||
onClick={() => !props.disabled && props.onClick?.(!props.selected)}
|
onClick={() => !props.disabled && props.onClick?.(!props.selected)}
|
||||||
{...extra}
|
{...extra}
|
||||||
>
|
>
|
||||||
<styled.Circle {...styledProps}>
|
<styled.Circle {...styledProps}>
|
||||||
{!props.disabled && props.type !== 'delete' ? <FaCheck /> : <FaTimes />}
|
{!props.disabled && props.type !== 'delete' ? <FaCheck /> : <FaTimes />}
|
||||||
</styled.Circle>
|
</styled.Circle>
|
||||||
<styled.Text>{props.role.name}</styled.Text>
|
<styled.Text>{props.role.name}</styled.Text>
|
||||||
</styled.Outer>
|
</styled.Outer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const disabledReason = (role: RPCRole) => {
|
const disabledReason = (role: RPCRole) => {
|
||||||
switch (role.safety) {
|
switch (role.safety) {
|
||||||
case RoleSafety.HigherThanBot:
|
case RoleSafety.HigherThanBot:
|
||||||
return `This role is above Roleypoly's own role.`;
|
return `This role is above Roleypoly's own role.`;
|
||||||
case RoleSafety.DangerousPermissions:
|
case RoleSafety.DangerousPermissions:
|
||||||
const rolePermissions = BigInt(role.permissions);
|
const rolePermissions = BigInt(role.permissions);
|
||||||
let permissionHits: string[] = [];
|
let permissionHits: string[] = [];
|
||||||
|
|
||||||
evaluatePermission(rolePermissions, permissions.ADMINISTRATOR) &&
|
evaluatePermission(rolePermissions, permissions.ADMINISTRATOR) &&
|
||||||
permissionHits.push('Administrator');
|
permissionHits.push('Administrator');
|
||||||
evaluatePermission(rolePermissions, permissions.MANAGE_ROLES) &&
|
evaluatePermission(rolePermissions, permissions.MANAGE_ROLES) &&
|
||||||
permissionHits.push('Manage Roles');
|
permissionHits.push('Manage Roles');
|
||||||
|
|
||||||
return `This role has unsafe permissions: ${permissionHits.join(', ')}`;
|
return `This role has unsafe permissions: ${permissionHits.join(', ')}`;
|
||||||
default:
|
default:
|
||||||
return `This role is disabled.`;
|
return `This role is disabled.`;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -2,13 +2,13 @@ import * as React from 'react';
|
||||||
import { Space as SpaceComponent } from './Space';
|
import { Space as SpaceComponent } from './Space';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
title: 'Atoms',
|
title: 'Atoms',
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Space = () => (
|
export const Space = () => (
|
||||||
<>
|
<>
|
||||||
hello world
|
hello world
|
||||||
<SpaceComponent />
|
<SpaceComponent />
|
||||||
but im over here
|
but im over here
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
|
|
||||||
export const Space = styled.div`
|
export const Space = styled.div`
|
||||||
height: 15px;
|
height: 15px;
|
||||||
`;
|
`;
|
||||||
|
|
|
@ -2,203 +2,203 @@ import * as React from 'react';
|
||||||
import { CSSProperties } from 'styled-components';
|
import { CSSProperties } from 'styled-components';
|
||||||
|
|
||||||
type SparkleProps = {
|
type SparkleProps = {
|
||||||
height: string;
|
height: string;
|
||||||
strokeColor: string;
|
strokeColor: string;
|
||||||
repeatCount?: number;
|
repeatCount?: number;
|
||||||
style?: CSSProperties;
|
style?: CSSProperties;
|
||||||
delay?: number;
|
delay?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
const Animation = (props: SparkleProps) => (
|
const Animation = (props: SparkleProps) => (
|
||||||
<>
|
<>
|
||||||
<animateTransform
|
<animateTransform
|
||||||
fill="freeze"
|
fill="freeze"
|
||||||
attributeName="transform"
|
attributeName="transform"
|
||||||
type="scale"
|
type="scale"
|
||||||
begin={`${props.delay || 0}s`}
|
begin={`${props.delay || 0}s`}
|
||||||
additive="sum"
|
additive="sum"
|
||||||
dur="1s"
|
dur="1s"
|
||||||
repeatCount={props.repeatCount || 'indefinite'}
|
repeatCount={props.repeatCount || 'indefinite'}
|
||||||
from="-1 -1"
|
from="-1 -1"
|
||||||
to="1 1"
|
to="1 1"
|
||||||
/>
|
/>
|
||||||
<animateTransform
|
<animateTransform
|
||||||
fill="freeze"
|
fill="freeze"
|
||||||
attributeName="transform"
|
attributeName="transform"
|
||||||
type="translate"
|
type="translate"
|
||||||
begin={`${props.delay || 0}s`}
|
begin={`${props.delay || 0}s`}
|
||||||
additive="sum"
|
additive="sum"
|
||||||
dur="1s"
|
dur="1s"
|
||||||
repeatCount={props.repeatCount || 'indefinite'}
|
repeatCount={props.repeatCount || 'indefinite'}
|
||||||
from="-1 -1"
|
from="-1 -1"
|
||||||
to="1 1"
|
to="1 1"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<animateTransform
|
<animateTransform
|
||||||
// additive="sum"
|
// additive="sum"
|
||||||
fill="freeze"
|
fill="freeze"
|
||||||
attributeName="transform"
|
attributeName="transform"
|
||||||
type="scale"
|
type="scale"
|
||||||
to="0 0"
|
to="0 0"
|
||||||
dur="0.3s"
|
dur="0.3s"
|
||||||
begin={props.repeatCount || 10}
|
begin={props.repeatCount || 10}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
||||||
const SparkleCircle = (props: SparkleProps) => (
|
const SparkleCircle = (props: SparkleProps) => (
|
||||||
<svg height={props.height} style={props.style} viewBox="-16 -16 16 16" fill="none">
|
<svg height={props.height} style={props.style} viewBox="-16 -16 16 16" fill="none">
|
||||||
<g transform="translate(-8 -8)">
|
<g transform="translate(-8 -8)">
|
||||||
<circle cx="0" cy="0" r="6.5" stroke={props.strokeColor} strokeWidth="2">
|
<circle cx="0" cy="0" r="6.5" stroke={props.strokeColor} strokeWidth="2">
|
||||||
<Animation {...props} />
|
<Animation {...props} />
|
||||||
</circle>
|
</circle>
|
||||||
</g>
|
</g>
|
||||||
</svg>
|
</svg>
|
||||||
);
|
);
|
||||||
|
|
||||||
const SparkleStar = (props: SparkleProps) => (
|
const SparkleStar = (props: SparkleProps) => (
|
||||||
<svg height={props.height} style={props.style} viewBox="-30 -30 30 30" fill="none">
|
<svg height={props.height} style={props.style} viewBox="-30 -30 30 30" fill="none">
|
||||||
<g transform="translate(0 0)">
|
<g transform="translate(0 0)">
|
||||||
<path
|
<path
|
||||||
d="M15.5 3.23607L18.0289 11.0193L18.2534 11.7102H18.98H27.1637L20.5429 16.5205L19.9551 16.9476L20.1796 17.6385L22.7086 25.4217L16.0878 20.6115L15.5 20.1844L14.9122 20.6115L8.29144 25.4217L10.8204 17.6385L11.0449 16.9476L10.4571 16.5205L3.83631 11.7102H12.02H12.7466L12.9711 11.0193L15.5 3.23607Z"
|
d="M15.5 3.23607L18.0289 11.0193L18.2534 11.7102H18.98H27.1637L20.5429 16.5205L19.9551 16.9476L20.1796 17.6385L22.7086 25.4217L16.0878 20.6115L15.5 20.1844L14.9122 20.6115L8.29144 25.4217L10.8204 17.6385L11.0449 16.9476L10.4571 16.5205L3.83631 11.7102H12.02H12.7466L12.9711 11.0193L15.5 3.23607Z"
|
||||||
stroke={props.strokeColor}
|
stroke={props.strokeColor}
|
||||||
strokeWidth="2"
|
strokeWidth="2"
|
||||||
>
|
>
|
||||||
<Animation {...props} />
|
<Animation {...props} />
|
||||||
<animateTransform
|
<animateTransform
|
||||||
fill="freeze"
|
fill="freeze"
|
||||||
attributeName="transform"
|
attributeName="transform"
|
||||||
type="translate"
|
type="translate"
|
||||||
begin={`${props.delay || 0}s`}
|
begin={`${props.delay || 0}s`}
|
||||||
additive="sum"
|
additive="sum"
|
||||||
dur="0.5s"
|
dur="0.5s"
|
||||||
repeatCount={props.repeatCount || 'indefinite'}
|
repeatCount={props.repeatCount || 'indefinite'}
|
||||||
from="30 30"
|
from="30 30"
|
||||||
to="0 0"
|
to="0 0"
|
||||||
/>
|
/>
|
||||||
</path>
|
</path>
|
||||||
</g>
|
</g>
|
||||||
</svg>
|
</svg>
|
||||||
);
|
);
|
||||||
|
|
||||||
const SparkleCross = (props: SparkleProps) => (
|
const SparkleCross = (props: SparkleProps) => (
|
||||||
<svg height={props.height} style={props.style} viewBox="-15 -15 15 15" fill="none">
|
<svg height={props.height} style={props.style} viewBox="-15 -15 15 15" fill="none">
|
||||||
<g transform="translate(-15 -15)">
|
<g transform="translate(-15 -15)">
|
||||||
<path
|
<path
|
||||||
d="M-3.27836e-07 7.5L5 7.5M7.5 4.99999L7.5 2.53319e-06M9.99999 7.49999L15 7.49999M7.5 15L7.5 10"
|
d="M-3.27836e-07 7.5L5 7.5M7.5 4.99999L7.5 2.53319e-06M9.99999 7.49999L15 7.49999M7.5 15L7.5 10"
|
||||||
stroke={props.strokeColor}
|
stroke={props.strokeColor}
|
||||||
strokeWidth="2"
|
strokeWidth="2"
|
||||||
>
|
>
|
||||||
<Animation {...props} />
|
<Animation {...props} />
|
||||||
</path>
|
</path>
|
||||||
</g>
|
</g>
|
||||||
</svg>
|
</svg>
|
||||||
);
|
);
|
||||||
|
|
||||||
const patternBase: CSSProperties = {
|
const patternBase: CSSProperties = {
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
};
|
};
|
||||||
|
|
||||||
const shapeMixin: CSSProperties = {
|
const shapeMixin: CSSProperties = {
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SparklePatternAlpha = ({ style, ...props }: SparkleProps) => (
|
export const SparklePatternAlpha = ({ style, ...props }: SparkleProps) => (
|
||||||
<div style={patternBase}>
|
<div style={patternBase}>
|
||||||
<SparkleCircle
|
<SparkleCircle
|
||||||
{...props}
|
{...props}
|
||||||
height="20%"
|
height="20%"
|
||||||
style={{
|
style={{
|
||||||
...shapeMixin,
|
...shapeMixin,
|
||||||
|
|
||||||
top: 0,
|
top: 0,
|
||||||
left: 0,
|
left: 0,
|
||||||
}}
|
}}
|
||||||
delay={0.24}
|
delay={0.24}
|
||||||
/>
|
/>
|
||||||
<SparkleCross
|
<SparkleCross
|
||||||
{...props}
|
{...props}
|
||||||
height="20%"
|
height="20%"
|
||||||
style={{
|
style={{
|
||||||
...shapeMixin,
|
...shapeMixin,
|
||||||
|
|
||||||
top: '45%',
|
top: '45%',
|
||||||
left: '15%',
|
left: '15%',
|
||||||
}}
|
}}
|
||||||
delay={0.55}
|
delay={0.55}
|
||||||
/>
|
/>
|
||||||
<SparkleCross
|
<SparkleCross
|
||||||
{...props}
|
{...props}
|
||||||
height="10%"
|
height="10%"
|
||||||
style={{
|
style={{
|
||||||
...shapeMixin,
|
...shapeMixin,
|
||||||
|
|
||||||
top: '30%',
|
top: '30%',
|
||||||
left: '-15%',
|
left: '-15%',
|
||||||
transform: 'rotate(30deg)',
|
transform: 'rotate(30deg)',
|
||||||
}}
|
}}
|
||||||
delay={0.33}
|
delay={0.33}
|
||||||
/>
|
/>
|
||||||
<SparkleStar
|
<SparkleStar
|
||||||
{...props}
|
{...props}
|
||||||
height="30%"
|
height="30%"
|
||||||
style={{
|
style={{
|
||||||
...shapeMixin,
|
...shapeMixin,
|
||||||
|
|
||||||
bottom: '0%',
|
bottom: '0%',
|
||||||
right: '10%',
|
right: '10%',
|
||||||
}}
|
}}
|
||||||
delay={0.75}
|
delay={0.75}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
export const SparklePatternBeta = ({ style, ...props }: SparkleProps) => (
|
export const SparklePatternBeta = ({ style, ...props }: SparkleProps) => (
|
||||||
<div style={patternBase}>
|
<div style={patternBase}>
|
||||||
<SparkleCircle
|
<SparkleCircle
|
||||||
{...props}
|
{...props}
|
||||||
height="15%"
|
height="15%"
|
||||||
style={{
|
style={{
|
||||||
...shapeMixin,
|
...shapeMixin,
|
||||||
|
|
||||||
top: '60%',
|
top: '60%',
|
||||||
left: '20%',
|
left: '20%',
|
||||||
}}
|
}}
|
||||||
delay={0.9}
|
delay={0.9}
|
||||||
/>
|
/>
|
||||||
<SparkleCross
|
<SparkleCross
|
||||||
{...props}
|
{...props}
|
||||||
height="20%"
|
height="20%"
|
||||||
style={{
|
style={{
|
||||||
...shapeMixin,
|
...shapeMixin,
|
||||||
|
|
||||||
top: 0,
|
top: 0,
|
||||||
right: 0,
|
right: 0,
|
||||||
}}
|
}}
|
||||||
delay={0.11}
|
delay={0.11}
|
||||||
/>
|
/>
|
||||||
<SparkleCross
|
<SparkleCross
|
||||||
{...props}
|
{...props}
|
||||||
height="15%"
|
height="15%"
|
||||||
style={{
|
style={{
|
||||||
...shapeMixin,
|
...shapeMixin,
|
||||||
|
|
||||||
top: '80%',
|
top: '80%',
|
||||||
right: 0,
|
right: 0,
|
||||||
}}
|
}}
|
||||||
delay={0.15}
|
delay={0.15}
|
||||||
/>
|
/>
|
||||||
<SparkleStar
|
<SparkleStar
|
||||||
{...props}
|
{...props}
|
||||||
height="30%"
|
height="30%"
|
||||||
style={{
|
style={{
|
||||||
...shapeMixin,
|
...shapeMixin,
|
||||||
|
|
||||||
top: '20%',
|
top: '20%',
|
||||||
left: '30%',
|
left: '30%',
|
||||||
transform: 'rotate(30deg)',
|
transform: 'rotate(30deg)',
|
||||||
}}
|
}}
|
||||||
delay={0.6}
|
delay={0.6}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -4,19 +4,19 @@ import * as React from 'react';
|
||||||
import { SparkleOverlay } from './Sparkle';
|
import { SparkleOverlay } from './Sparkle';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
title: 'Atoms/Sparkle',
|
title: 'Atoms/Sparkle',
|
||||||
component: SparkleOverlay,
|
component: SparkleOverlay,
|
||||||
args: {
|
args: {
|
||||||
size: -10,
|
size: -10,
|
||||||
opacity: 1,
|
opacity: 1,
|
||||||
repeatCount: 3,
|
repeatCount: 3,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ExampleButton = (args) => (
|
export const ExampleButton = (args) => (
|
||||||
<Hero>
|
<Hero>
|
||||||
<SparkleOverlay {...args}>
|
<SparkleOverlay {...args}>
|
||||||
<Button>Yo check this!</Button>
|
<Button>Yo check this!</Button>
|
||||||
</SparkleOverlay>
|
</SparkleOverlay>
|
||||||
</Hero>
|
</Hero>
|
||||||
);
|
);
|
||||||
|
|
|
@ -4,49 +4,49 @@ import styled from 'styled-components';
|
||||||
import { SparklePatternAlpha, SparklePatternBeta } from './Shapes';
|
import { SparklePatternAlpha, SparklePatternBeta } from './Shapes';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
size?: number;
|
size?: number;
|
||||||
opacity?: number;
|
opacity?: number;
|
||||||
repeatCount?: number;
|
repeatCount?: number;
|
||||||
strokeColor?: string;
|
strokeColor?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const SparkleContainer = styled.div`
|
const SparkleContainer = styled.div`
|
||||||
position: relative;
|
position: relative;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
type EffectProps = {
|
type EffectProps = {
|
||||||
effectSize: Props['size'];
|
effectSize: Props['size'];
|
||||||
effectOpacity: Props['opacity'];
|
effectOpacity: Props['opacity'];
|
||||||
};
|
};
|
||||||
|
|
||||||
const SparkleEffect = styled.div<EffectProps>`
|
const SparkleEffect = styled.div<EffectProps>`
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: ${(props) => props.effectSize}px;
|
top: ${(props) => props.effectSize}px;
|
||||||
bottom: ${(props) => props.effectSize}px;
|
bottom: ${(props) => props.effectSize}px;
|
||||||
left: ${(props) => props.effectSize}px;
|
left: ${(props) => props.effectSize}px;
|
||||||
right: ${(props) => props.effectSize}px;
|
right: ${(props) => props.effectSize}px;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
z-index: 5;
|
z-index: 5;
|
||||||
opacity: ${(props) => props.effectOpacity};
|
opacity: ${(props) => props.effectOpacity};
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const SparkleOverlay = ({ strokeColor = palette.gold400, ...props }: Props) => (
|
export const SparkleOverlay = ({ strokeColor = palette.gold400, ...props }: Props) => (
|
||||||
<SparkleContainer>
|
<SparkleContainer>
|
||||||
<SparkleEffect effectSize={props.size || 0} effectOpacity={props.opacity || 1}>
|
<SparkleEffect effectSize={props.size || 0} effectOpacity={props.opacity || 1}>
|
||||||
<SparklePatternAlpha
|
<SparklePatternAlpha
|
||||||
repeatCount={props.repeatCount}
|
repeatCount={props.repeatCount}
|
||||||
height="100%"
|
height="100%"
|
||||||
strokeColor={strokeColor}
|
strokeColor={strokeColor}
|
||||||
/>
|
/>
|
||||||
<SparklePatternBeta
|
<SparklePatternBeta
|
||||||
repeatCount={props.repeatCount}
|
repeatCount={props.repeatCount}
|
||||||
height="100%"
|
height="100%"
|
||||||
strokeColor={strokeColor}
|
strokeColor={strokeColor}
|
||||||
/>
|
/>
|
||||||
</SparkleEffect>
|
</SparkleEffect>
|
||||||
{props.children}
|
{props.children}
|
||||||
</SparkleContainer>
|
</SparkleContainer>
|
||||||
);
|
);
|
||||||
|
|
|
@ -4,36 +4,36 @@ import { Tab, TabView, TabViewProps } from './TabView';
|
||||||
import { TabContent, TabTitle } from './TabView.styled';
|
import { TabContent, TabTitle } from './TabView.styled';
|
||||||
|
|
||||||
const makeView = (props: Partial<TabViewProps> = {}) =>
|
const makeView = (props: Partial<TabViewProps> = {}) =>
|
||||||
shallow(
|
shallow(
|
||||||
<TabView {...props}>
|
<TabView {...props}>
|
||||||
<Tab title="Tab 1">{() => <div>tab 1</div>}</Tab>
|
<Tab title="Tab 1">{() => <div>tab 1</div>}</Tab>
|
||||||
<Tab title="Tab 2">{() => <div>tab 2</div>}</Tab>,
|
<Tab title="Tab 2">{() => <div>tab 2</div>}</Tab>,
|
||||||
</TabView>
|
</TabView>
|
||||||
);
|
);
|
||||||
|
|
||||||
it('renders tab content correctly', () => {
|
it('renders tab content correctly', () => {
|
||||||
const view = makeView();
|
const view = makeView();
|
||||||
|
|
||||||
expect(view.find(Tab).renderProp('children')().text()).toBe('tab 1');
|
expect(view.find(Tab).renderProp('children')().text()).toBe('tab 1');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('automatically picks preselected tab content', () => {
|
it('automatically picks preselected tab content', () => {
|
||||||
const view = makeView({ initialTab: 1 });
|
const view = makeView({ initialTab: 1 });
|
||||||
|
|
||||||
expect(view.find(Tab).renderProp('children')().text()).toBe('tab 2');
|
expect(view.find(Tab).renderProp('children')().text()).toBe('tab 2');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('automatically uses the first tab when preselected tab is not present', () => {
|
it('automatically uses the first tab when preselected tab is not present', () => {
|
||||||
const view = makeView({ initialTab: -1 });
|
const view = makeView({ initialTab: -1 });
|
||||||
|
|
||||||
view.find(TabContent).find('i').simulate('load');
|
view.find(TabContent).find('i').simulate('load');
|
||||||
expect(view.find(Tab).renderProp('children')().text()).toBe('tab 1');
|
expect(view.find(Tab).renderProp('children')().text()).toBe('tab 1');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('changes between tabs when tab is clicked', () => {
|
it('changes between tabs when tab is clicked', () => {
|
||||||
const view = makeView();
|
const view = makeView();
|
||||||
|
|
||||||
view.find(TabTitle).at(1).simulate('click');
|
view.find(TabTitle).at(1).simulate('click');
|
||||||
|
|
||||||
expect(view.find(Tab).renderProp('children')().text()).toBe('tab 2');
|
expect(view.find(Tab).renderProp('children')().text()).toBe('tab 2');
|
||||||
});
|
});
|
||||||
|
|
|
@ -2,26 +2,26 @@ import * as React from 'react';
|
||||||
import { Tab, TabView } from './TabView';
|
import { Tab, TabView } from './TabView';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
title: 'Atoms/Tab View',
|
title: 'Atoms/Tab View',
|
||||||
argTypes: {
|
argTypes: {
|
||||||
tabCount: { control: 'range', min: 1, max: 100 },
|
tabCount: { control: 'range', min: 1, max: 100 },
|
||||||
},
|
},
|
||||||
args: {
|
args: {
|
||||||
tabCount: 10,
|
tabCount: 10,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ManyTabs = ({ tabCount }) => {
|
export const ManyTabs = ({ tabCount }) => {
|
||||||
const tabs = [...'0'.repeat(tabCount)].map((_, i) => (
|
const tabs = [...'0'.repeat(tabCount)].map((_, i) => (
|
||||||
<Tab title={`tab ${i}`}>
|
<Tab title={`tab ${i}`}>
|
||||||
{() => (
|
{() => (
|
||||||
<>
|
<>
|
||||||
<h1>tab {i}</h1>
|
<h1>tab {i}</h1>
|
||||||
<p>hello!!!!!</p>
|
<p>hello!!!!!</p>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Tab>
|
</Tab>
|
||||||
));
|
));
|
||||||
|
|
||||||
return <TabView>{tabs}</TabView>;
|
return <TabView>{tabs}</TabView>;
|
||||||
};
|
};
|
||||||
|
|
|
@ -6,37 +6,37 @@ import styled, { css } from 'styled-components';
|
||||||
export const TabViewStyled = styled.div``;
|
export const TabViewStyled = styled.div``;
|
||||||
|
|
||||||
export const TabTitleRow = styled.div`
|
export const TabTitleRow = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
border-bottom: 1px solid ${palette.taupe100};
|
border-bottom: 1px solid ${palette.taupe100};
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
overflow-y: hidden;
|
overflow-y: hidden;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const TabTitle = styled.div<{ selected: boolean }>`
|
export const TabTitle = styled.div<{ selected: boolean }>`
|
||||||
flex: 1;
|
flex: 1;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: 0.7em 1em;
|
padding: 0.7em 1em;
|
||||||
border-bottom: 3px solid transparent;
|
border-bottom: 3px solid transparent;
|
||||||
transition: border-color ${transitions.in2out}s ease-in-out,
|
transition: border-color ${transitions.in2out}s ease-in-out,
|
||||||
color ${transitions.in2out}s ease-in-out;
|
color ${transitions.in2out}s ease-in-out;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
color: ${palette.taupe500};
|
color: ${palette.taupe500};
|
||||||
${(props) =>
|
${(props) =>
|
||||||
props.selected
|
props.selected
|
||||||
? css`
|
? css`
|
||||||
color: unset;
|
color: unset;
|
||||||
border-bottom-color: ${palette.taupe500};
|
border-bottom-color: ${palette.taupe500};
|
||||||
`
|
`
|
||||||
: css`
|
: css`
|
||||||
&:hover {
|
&:hover {
|
||||||
border-bottom-color: ${palette.taupe300};
|
border-bottom-color: ${palette.taupe300};
|
||||||
color: unset;
|
color: unset;
|
||||||
}
|
}
|
||||||
`};
|
`};
|
||||||
${onTablet(css`
|
${onTablet(css`
|
||||||
padding: 0.45em 1em;
|
padding: 0.45em 1em;
|
||||||
`)}
|
`)}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const TabContent = styled.div``;
|
export const TabContent = styled.div``;
|
||||||
|
|
|
@ -2,52 +2,52 @@ import * as React from 'react';
|
||||||
import { TabContent, TabTitle, TabTitleRow, TabViewStyled } from './TabView.styled';
|
import { TabContent, TabTitle, TabTitleRow, TabViewStyled } from './TabView.styled';
|
||||||
|
|
||||||
export type TabViewProps = {
|
export type TabViewProps = {
|
||||||
children: React.ReactNode[];
|
children: React.ReactNode[];
|
||||||
initialTab?: number;
|
initialTab?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
type TabProps = {
|
type TabProps = {
|
||||||
title: string;
|
title: string;
|
||||||
children: () => React.ReactNode;
|
children: () => React.ReactNode;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const TabView = (props: TabViewProps) => {
|
export const TabView = (props: TabViewProps) => {
|
||||||
const tabNames = React.Children.map(props.children, (child) => {
|
const tabNames = React.Children.map(props.children, (child) => {
|
||||||
if (!React.isValidElement(child)) {
|
if (!React.isValidElement(child)) {
|
||||||
return '(Oops)';
|
return '(Oops)';
|
||||||
}
|
|
||||||
|
|
||||||
return child.props.title;
|
|
||||||
}) as string[];
|
|
||||||
|
|
||||||
if (tabNames.length === 0) {
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const [currentTab, setCurrentTab] = React.useState<number>(props.initialTab ?? 0);
|
return child.props.title;
|
||||||
|
}) as string[];
|
||||||
|
|
||||||
return (
|
if (tabNames.length === 0) {
|
||||||
<TabViewStyled>
|
return null;
|
||||||
<TabTitleRow>
|
}
|
||||||
{tabNames.map((tabName, idx) => (
|
|
||||||
<TabTitle
|
const [currentTab, setCurrentTab] = React.useState<number>(props.initialTab ?? 0);
|
||||||
selected={currentTab === idx}
|
|
||||||
onClick={() => setCurrentTab(idx)}
|
return (
|
||||||
key={`tab${tabName}${idx}`}
|
<TabViewStyled>
|
||||||
>
|
<TabTitleRow>
|
||||||
{tabName}
|
{tabNames.map((tabName, idx) => (
|
||||||
</TabTitle>
|
<TabTitle
|
||||||
))}
|
selected={currentTab === idx}
|
||||||
</TabTitleRow>
|
onClick={() => setCurrentTab(idx)}
|
||||||
<TabContent>
|
key={`tab${tabName}${idx}`}
|
||||||
{props.children[currentTab] || (
|
>
|
||||||
<i onLoad={() => setCurrentTab(0)}>
|
{tabName}
|
||||||
Tabs were misconfigured, resetting to zero.
|
</TabTitle>
|
||||||
</i>
|
))}
|
||||||
)}
|
</TabTitleRow>
|
||||||
</TabContent>
|
<TabContent>
|
||||||
</TabViewStyled>
|
{props.children[currentTab] || (
|
||||||
);
|
<i onLoad={() => setCurrentTab(0)}>
|
||||||
|
Tabs were misconfigured, resetting to zero.
|
||||||
|
</i>
|
||||||
|
)}
|
||||||
|
</TabContent>
|
||||||
|
</TabViewStyled>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Tab = (props: TabProps) => <div>{props.children()}</div>;
|
export const Tab = (props: TabProps) => <div>{props.children()}</div>;
|
||||||
|
|
|
@ -4,33 +4,33 @@ import { FiKey } from 'react-icons/fi';
|
||||||
import { TextInput, TextInputWithIcon } from './TextInput';
|
import { TextInput, TextInputWithIcon } from './TextInput';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
title: 'Atoms/Text Input',
|
title: 'Atoms/Text Input',
|
||||||
argTypes: {
|
argTypes: {
|
||||||
placeholder: { control: 'text' },
|
placeholder: { control: 'text' },
|
||||||
},
|
},
|
||||||
args: {
|
args: {
|
||||||
placeholder: 'Fill me in!',
|
placeholder: 'Fill me in!',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Common = (args) => (
|
export const Common = (args) => (
|
||||||
|
<div>
|
||||||
|
<SmallTitle>TextInput</SmallTitle>
|
||||||
<div>
|
<div>
|
||||||
<SmallTitle>TextInput</SmallTitle>
|
<TextInput {...args} />
|
||||||
<div>
|
|
||||||
<TextInput {...args} />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<TextInput {...args} disabled />
|
|
||||||
</div>
|
|
||||||
<SmallTitle>TextInputWithIcon</SmallTitle>
|
|
||||||
<div>
|
|
||||||
<TextInputWithIcon icon={<FiKey />} {...args} />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<TextInputWithIcon icon={<FiKey />} {...args} disabled />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<TextInputWithIcon icon={<FiKey />} {...args} type="password" />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<TextInput {...args} disabled />
|
||||||
|
</div>
|
||||||
|
<SmallTitle>TextInputWithIcon</SmallTitle>
|
||||||
|
<div>
|
||||||
|
<TextInputWithIcon icon={<FiKey />} {...args} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<TextInputWithIcon icon={<FiKey />} {...args} disabled />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<TextInputWithIcon icon={<FiKey />} {...args} type="password" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue