commit
0dca1a5eda
30
.github/workflows/deploy_backend.yml
vendored
Normal file
30
.github/workflows/deploy_backend.yml
vendored
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
name: Deploy Backend
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ['main']
|
||||
paths: ['crabfit-backend/**']
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
defaults:
|
||||
run:
|
||||
working-directory: crabfit-backend
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- id: auth
|
||||
uses: google-github-actions/auth@v0
|
||||
with:
|
||||
credentials_json: '${{ secrets.GCP_SA_KEY }}'
|
||||
- id: deploy
|
||||
uses: google-github-actions/deploy-appengine@v0
|
||||
with:
|
||||
working_directory: crabfit-backend
|
||||
version: v1
|
||||
37
.github/workflows/deploy_frontend.yml
vendored
Normal file
37
.github/workflows/deploy_frontend.yml
vendored
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
name: Deploy Frontend
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ['main']
|
||||
paths: ['crabfit-frontend/**']
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
defaults:
|
||||
run:
|
||||
working-directory: crabfit-frontend
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 17
|
||||
cache: yarn
|
||||
cache-dependency-path: '**/yarn.lock'
|
||||
- run: yarn install --immutable
|
||||
- run: yarn build
|
||||
- id: auth
|
||||
uses: google-github-actions/auth@v0
|
||||
with:
|
||||
credentials_json: '${{ secrets.GCP_SA_KEY }}'
|
||||
- id: deploy
|
||||
uses: google-github-actions/deploy-appengine@v0
|
||||
with:
|
||||
working_directory: crabfit-frontend
|
||||
version: v1
|
||||
|
|
@ -1,2 +1,7 @@
|
|||
runtime: nodejs10
|
||||
runtime: nodejs16
|
||||
service: api
|
||||
handlers:
|
||||
- url: /.*
|
||||
secure: always
|
||||
redirect_http_response_code: 301
|
||||
script: auto
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ const taskRemoveOrphans = require('./routes/taskRemoveOrphans');
|
|||
const app = express();
|
||||
const port = 8080;
|
||||
const corsOptions = {
|
||||
origin: process.env.NODE_ENV === 'production' ? 'https://crab.fit' : 'http://localhost:3000',
|
||||
origin: process.env.NODE_ENV === 'production' ? 'https://crab.fit' : 'http://localhost:5173',
|
||||
};
|
||||
|
||||
const datastore = new Datastore({
|
||||
|
|
|
|||
73
crabfit-frontend/.eslintrc.js
Normal file
73
crabfit-frontend/.eslintrc.js
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
/* eslint-env node */
|
||||
module.exports = {
|
||||
'settings': {
|
||||
'react': {
|
||||
'version': 'detect'
|
||||
}
|
||||
},
|
||||
'env': {
|
||||
'browser': true,
|
||||
'es2021': true
|
||||
},
|
||||
'globals': {
|
||||
'process': true,
|
||||
'require': true,
|
||||
'gtag': true,
|
||||
},
|
||||
'extends': [
|
||||
'eslint:recommended',
|
||||
'plugin:react/recommended'
|
||||
],
|
||||
'parserOptions': {
|
||||
'ecmaFeatures': {
|
||||
'jsx': true
|
||||
},
|
||||
'ecmaVersion': 12,
|
||||
'sourceType': 'module'
|
||||
},
|
||||
'plugins': [
|
||||
'react'
|
||||
],
|
||||
'rules': {
|
||||
'react/display-name': 'off',
|
||||
'react/prop-types': 'off',
|
||||
'react/no-unescaped-entities': 'off',
|
||||
'react/react-in-jsx-scope': 'off',
|
||||
'eqeqeq': 2,
|
||||
'no-return-await': 1,
|
||||
'no-var': 2,
|
||||
'prefer-const': 1,
|
||||
'yoda': 2,
|
||||
'no-trailing-spaces': 1,
|
||||
'eol-last': [1, 'always'],
|
||||
'no-unused-vars': [
|
||||
1,
|
||||
{
|
||||
'args': 'all',
|
||||
'argsIgnorePattern': '^_',
|
||||
'ignoreRestSiblings': true
|
||||
}
|
||||
],
|
||||
'indent': [
|
||||
'error',
|
||||
2
|
||||
],
|
||||
'linebreak-style': [
|
||||
'error',
|
||||
'unix'
|
||||
],
|
||||
'quotes': [
|
||||
'error',
|
||||
'single'
|
||||
],
|
||||
'semi': [
|
||||
'error',
|
||||
'never'
|
||||
],
|
||||
'arrow-parens': [
|
||||
'error',
|
||||
'as-needed'
|
||||
],
|
||||
'jsx-quotes': [1, 'prefer-double'],
|
||||
}
|
||||
}
|
||||
10
crabfit-frontend/.gcloudignore
Normal file
10
crabfit-frontend/.gcloudignore
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
node_modules
|
||||
.DS_Store
|
||||
.git
|
||||
.gitignore
|
||||
.gcloudignore
|
||||
src
|
||||
public
|
||||
.eslintrc.js
|
||||
yarn.lock
|
||||
package.json
|
||||
22
crabfit-frontend/.gitignore
vendored
22
crabfit-frontend/.gitignore
vendored
|
|
@ -1,22 +1,6 @@
|
|||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
node_modules
|
||||
dist
|
||||
build
|
||||
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
|
|
|
|||
|
|
@ -1,70 +0,0 @@
|
|||
# Getting Started with Create React App
|
||||
|
||||
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
|
||||
|
||||
## Available Scripts
|
||||
|
||||
In the project directory, you can run:
|
||||
|
||||
### `yarn start`
|
||||
|
||||
Runs the app in the development mode.\
|
||||
Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
|
||||
|
||||
The page will reload if you make edits.\
|
||||
You will also see any lint errors in the console.
|
||||
|
||||
### `yarn test`
|
||||
|
||||
Launches the test runner in the interactive watch mode.\
|
||||
See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
|
||||
|
||||
### `yarn build`
|
||||
|
||||
Builds the app for production to the `build` folder.\
|
||||
It correctly bundles React in production mode and optimizes the build for the best performance.
|
||||
|
||||
The build is minified and the filenames include the hashes.\
|
||||
Your app is ready to be deployed!
|
||||
|
||||
See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
|
||||
|
||||
### `yarn eject`
|
||||
|
||||
**Note: this is a one-way operation. Once you `eject`, you can’t go back!**
|
||||
|
||||
If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
|
||||
|
||||
Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own.
|
||||
|
||||
You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it.
|
||||
|
||||
## Learn More
|
||||
|
||||
You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
|
||||
|
||||
To learn React, check out the [React documentation](https://reactjs.org/).
|
||||
|
||||
### Code Splitting
|
||||
|
||||
This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting)
|
||||
|
||||
### Analyzing the Bundle Size
|
||||
|
||||
This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size)
|
||||
|
||||
### Making a Progressive Web App
|
||||
|
||||
This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app)
|
||||
|
||||
### Advanced Configuration
|
||||
|
||||
This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration)
|
||||
|
||||
### Deployment
|
||||
|
||||
This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment)
|
||||
|
||||
### `yarn build` fails to minify
|
||||
|
||||
This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify)
|
||||
|
|
@ -1,15 +1,15 @@
|
|||
runtime: nodejs12
|
||||
runtime: nodejs16
|
||||
handlers:
|
||||
# Serve all static files with url ending with a file extension
|
||||
- url: /(.*\..+)$
|
||||
static_files: \1
|
||||
static_files: dist/\1
|
||||
upload: (.*\..+)$
|
||||
secure: always
|
||||
redirect_http_response_code: 301
|
||||
|
||||
# Catch all handler to index.html
|
||||
- url: /.*
|
||||
static_files: index.html
|
||||
upload: index.html
|
||||
static_files: dist/index.html
|
||||
upload: dist/index.html
|
||||
secure: always
|
||||
redirect_http_response_code: 301
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
yarn build
|
||||
|
||||
cd build
|
||||
|
||||
cat > app.yaml << EOF
|
||||
runtime: nodejs12
|
||||
handlers:
|
||||
- url: /(.*\..+)$
|
||||
static_files: \1
|
||||
upload: (.*\..+)$
|
||||
secure: always
|
||||
redirect_http_response_code: 301
|
||||
|
||||
- url: /.*
|
||||
static_files: index.html
|
||||
upload: index.html
|
||||
secure: always
|
||||
redirect_http_response_code: 301
|
||||
EOF
|
||||
|
||||
gcloud app deploy --project=crabfit --version=v1
|
||||
|
|
@ -2,7 +2,7 @@
|
|||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<link rel="icon" href="%PUBLIC_URL%/favicon.ico">
|
||||
<link rel="icon" href="favicon.ico">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
|
||||
<meta name="theme-color" content="#F79E00">
|
||||
<meta
|
||||
|
|
@ -16,14 +16,14 @@
|
|||
<meta name="monetization" content="$ilp.uphold.com/HjDULeBk9JnH">
|
||||
<!--V1.0--><meta http-equiv="origin-trial" content="ApibM5tjM3kUQQ2EQrkRcdTdWJRGAEKaUFzNhFmx+Of5H/cRyWuecMxs//Bikgo3WMSKs5kntElcM+U8kDy9cAEAAABOeyJvcmlnaW4iOiJodHRwczovL2NyYWIuZml0OjQ0MyIsImZlYXR1cmUiOiJEaWdpdGFsR29vZHMiLCJleHBpcnkiOjE2Mzk1MjYzOTl9">
|
||||
<!--V2.0--><meta http-equiv="origin-trial" content="AiZrT13ogLT63ah6Abb/aG6KhscY5PTf1HNTI2rcqpiFeqiQ3s6+xd+qCe3c+bp3udvvzh5QMHF4GqPAlG110gcAAABQeyJvcmlnaW4iOiJodHRwczovL2NyYWIuZml0OjQ0MyIsImZlYXR1cmUiOiJEaWdpdGFsR29vZHNWMiIsImV4cGlyeSI6MTY0Nzk5MzU5OX0=">
|
||||
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png">
|
||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json">
|
||||
<link rel="apple-touch-icon" href="logo192.png">
|
||||
<link rel="manifest" href="manifest.json">
|
||||
|
||||
<meta property="og:title" content="Crab Fit">
|
||||
<meta property="og:description" content="Enter your availability to find a time that works for everyone!">
|
||||
<meta property="og:url" content="https://crab.fit">
|
||||
|
||||
<link rel="stylesheet" href="%PUBLIC_URL%/index.css">
|
||||
<link rel="stylesheet" href="index.css">
|
||||
|
||||
<title>Crab Fit</title>
|
||||
|
||||
|
|
@ -38,9 +38,14 @@
|
|||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/index.jsx"></script>
|
||||
|
||||
<noscript>
|
||||
<div style="font-family: Karla, sans-serif; text-align: center; margin: 20vh 0; display: block;"><h1>🦀 Crab Fit doesn't work without Javascript 🏋️</h1><p>Enable Javascript or try a different browser.</p></div>
|
||||
<div style="font-family: Karla, sans-serif; text-align: center; margin: 20vh 0; display: block;">
|
||||
<h1>🦀 Crab Fit doesn't work without Javascript 🏋️</h1>
|
||||
<p>Enable Javascript or try a different browser.</p>
|
||||
</div>
|
||||
</noscript>
|
||||
<div id="root"></div>
|
||||
</body>
|
||||
</html>
|
||||
13
crabfit-frontend/jsconfig.json
Normal file
13
crabfit-frontend/jsconfig.json
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": "./",
|
||||
"paths": {
|
||||
"/*": ["./*"]
|
||||
}
|
||||
},
|
||||
"exclude": [
|
||||
"**/node_modules/*",
|
||||
"**/dist/*",
|
||||
"**/.git/*"
|
||||
]
|
||||
}
|
||||
|
|
@ -3,58 +3,47 @@
|
|||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"license": "GPL-3.0-only",
|
||||
"dependencies": {
|
||||
"@azure/msal-browser": "^2.14.2",
|
||||
"@emotion/react": "^11.1.5",
|
||||
"@emotion/styled": "^11.1.5",
|
||||
"@microsoft/microsoft-graph-client": "^2.2.1",
|
||||
"@testing-library/jest-dom": "^5.11.4",
|
||||
"@testing-library/react": "^11.1.0",
|
||||
"@testing-library/user-event": "^12.1.10",
|
||||
"@types/jest": "^26.0.20",
|
||||
"@types/node": "^14.14.31",
|
||||
"@types/react": "^17.0.2",
|
||||
"@types/react-dom": "^17.0.1",
|
||||
"axios": "^0.21.1",
|
||||
"dayjs": "^1.10.4",
|
||||
"gapi-script": "^1.2.0",
|
||||
"i18next": "^20.2.4",
|
||||
"i18next-browser-languagedetector": "^6.1.1",
|
||||
"i18next-http-backend": "^1.2.4",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-hook-form": "^7.8.1",
|
||||
"react-i18next": "^11.8.15",
|
||||
"react-router-dom": "^5.2.0",
|
||||
"react-scripts": "4.0.3",
|
||||
"typescript": "^4.2.2",
|
||||
"web-vitals": "^1.0.1",
|
||||
"workbox-background-sync": "^5.1.3",
|
||||
"workbox-broadcast-update": "^5.1.3",
|
||||
"workbox-cacheable-response": "^5.1.3",
|
||||
"workbox-core": "^5.1.3",
|
||||
"workbox-expiration": "^5.1.3",
|
||||
"workbox-google-analytics": "^5.1.3",
|
||||
"workbox-navigation-preload": "^5.1.3",
|
||||
"workbox-precaching": "^5.1.3",
|
||||
"workbox-range-requests": "^5.1.3",
|
||||
"workbox-routing": "^5.1.3",
|
||||
"workbox-strategies": "^5.1.3",
|
||||
"workbox-streams": "^5.1.3",
|
||||
"workbox-window": "^6.1.5",
|
||||
"zustand": "^3.3.2"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "react-app-rewired start",
|
||||
"build": "react-app-rewired build",
|
||||
"test": "react-app-rewired test",
|
||||
"eject": "react-scripts eject"
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"lint": "eslint --ext .js,.jsx ./src"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
"react-app",
|
||||
"react-app/jest"
|
||||
]
|
||||
"dependencies": {
|
||||
"@azure/msal-browser": "^2.28.1",
|
||||
"@microsoft/microsoft-graph-client": "^3.0.2",
|
||||
"dayjs": "^1.11.5",
|
||||
"gapi-script": "^1.2.0",
|
||||
"goober": "^2.1.10",
|
||||
"i18next": "^21.9.0",
|
||||
"i18next-browser-languagedetector": "^6.1.5",
|
||||
"i18next-http-backend": "^1.4.1",
|
||||
"lucide-react": "^0.84.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-hook-form": "^7.34.1",
|
||||
"react-i18next": "^11.18.4",
|
||||
"react-router-dom": "^6.3.0",
|
||||
"workbox-background-sync": "^6.5.4",
|
||||
"workbox-broadcast-update": "^6.5.4",
|
||||
"workbox-cacheable-response": "^6.5.4",
|
||||
"workbox-core": "^6.5.4",
|
||||
"workbox-expiration": "^6.5.4",
|
||||
"workbox-google-analytics": "^6.5.4",
|
||||
"workbox-navigation-preload": "^6.5.4",
|
||||
"workbox-precaching": "^6.5.4",
|
||||
"workbox-range-requests": "^6.5.4",
|
||||
"workbox-routing": "^6.5.4",
|
||||
"workbox-strategies": "^6.5.4",
|
||||
"workbox-streams": "^6.5.4",
|
||||
"workbox-window": "^6.5.4",
|
||||
"zustand": "^4.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-react": "^2.0.1",
|
||||
"eslint": "^8.22.0",
|
||||
"eslint-plugin-react": "^7.30.1",
|
||||
"vite": "^3.0.7",
|
||||
"workbox-webpack-plugin": "^6.5.4"
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
|
|
@ -67,9 +56,5 @@
|
|||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"react-app-rewired": "^2.1.8",
|
||||
"workbox-webpack-plugin": "^5.1.3"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
@font-face {
|
||||
font-family: Karla;
|
||||
font-family: 'Karla';
|
||||
src: url('fonts/karla-variable.ttf') format('truetype');
|
||||
font-weight: 1 999;
|
||||
font-weight: 200 800;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
|
|
@ -20,22 +20,154 @@
|
|||
font-style: normal;
|
||||
}
|
||||
|
||||
:root {
|
||||
color-scheme: light dark;
|
||||
|
||||
--primary: #F79E00;
|
||||
|
||||
/* LIGHT */
|
||||
--background-light: #FFFFFF;
|
||||
--text-light: #000000;
|
||||
--shadow-light: #F48600;
|
||||
--highlight-light: #F4BB60;
|
||||
--secondary-light: var(--shadow-light);
|
||||
--tertiary-light: var(--highlight-light);
|
||||
--surface-light: #FEF2DD;
|
||||
--error-light: #D32F2F;
|
||||
--loading-light: #DDDDDD;
|
||||
--font-weight-light: 600;
|
||||
|
||||
/* DARK */
|
||||
--background-dark: #111111;
|
||||
--text-dark: #DDDDDD;
|
||||
--shadow-dark: #CC7313;
|
||||
--highlight-dark: #F4BB60;
|
||||
--secondary-dark: var(--highlight-dark);
|
||||
--tertiary-dark: var(--shadow-dark);
|
||||
--surface-dark: #30240F;
|
||||
--error-dark: #E53935;
|
||||
--loading-dark: #444444;
|
||||
--font-weight-dark: 500;
|
||||
|
||||
/* Define light defaults */
|
||||
--background: var(--background-light);
|
||||
--text: var(--text-light);
|
||||
--shadow: var(--shadow-light);
|
||||
--highlight: var(--highlight-light);
|
||||
--secondary: var(--secondary-light);
|
||||
--tertiary: var(--tertiary-light);
|
||||
--surface: var(--surface-light);
|
||||
--error: var(--error-light);
|
||||
--loading: var(--loading-light);
|
||||
--font-weight: var(--font-weight-light);
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--background: var(--background-dark);
|
||||
--text: var(--text-dark);
|
||||
--shadow: var(--shadow-dark);
|
||||
--highlight: var(--highlight-dark);
|
||||
--secondary: var(--secondary-dark);
|
||||
--tertiary: var(--tertiary-dark);
|
||||
--surface: var(--surface-dark);
|
||||
--error: var(--error-dark);
|
||||
--loading: var(--loading-dark);
|
||||
--font-weight: var(--font-weight-dark);
|
||||
}
|
||||
}
|
||||
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
-webkit-print-color-adjust: exact;
|
||||
print-color-adjust: exact;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: 'Karla', sans-serif;
|
||||
background: var(--background);
|
||||
color: var(--text);
|
||||
font-weight: var(--font-weight);
|
||||
}
|
||||
|
||||
.light {
|
||||
color-scheme: light;
|
||||
|
||||
--background: var(--background-light);
|
||||
--text: var(--text-light);
|
||||
--shadow: var(--shadow-light);
|
||||
--highlight: var(--highlight-light);
|
||||
--secondary: var(--secondary-light);
|
||||
--tertiary: var(--tertiary-light);
|
||||
--surface: var(--surface-light);
|
||||
--error: var(--error-light);
|
||||
--loading: var(--loading-light);
|
||||
--font-weight: var(--font-weight-light);
|
||||
}
|
||||
|
||||
@media not print {
|
||||
.dark {
|
||||
color-scheme: dark;
|
||||
|
||||
--background: var(--background-dark);
|
||||
--text: var(--text-dark);
|
||||
--shadow: var(--shadow-dark);
|
||||
--highlight: var(--highlight-dark);
|
||||
--secondary: var(--secondary-dark);
|
||||
--tertiary: var(--tertiary-dark);
|
||||
--surface: var(--surface-dark);
|
||||
--error: var(--error-dark);
|
||||
--loading: var(--loading-dark);
|
||||
--font-weight: var(--font-weight-dark);
|
||||
}
|
||||
}
|
||||
|
||||
@media print {
|
||||
#app, .light, .dark {
|
||||
--background: white;
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
*::-webkit-scrollbar {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
*::-webkit-scrollbar-track {
|
||||
background: var(--surface);
|
||||
}
|
||||
*::-webkit-scrollbar-thumb {
|
||||
border-radius: 100px;
|
||||
border: 4px solid var(--surface);
|
||||
width: 12px;
|
||||
background: var(--tertiary);
|
||||
}
|
||||
*::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--primary);
|
||||
}
|
||||
*::-webkit-scrollbar-thumb:active {
|
||||
background: var(--secondary);
|
||||
}
|
||||
|
||||
/* IE 10+ */
|
||||
@media all and (-ms-high-contrast: none), (-ms-high-contrast: active) {
|
||||
#root {
|
||||
font-family: Karla, sans-serif;
|
||||
#app {
|
||||
text-align: center;
|
||||
margin: 20vh auto;
|
||||
font-size: 1.3em;
|
||||
font-weight: 600;
|
||||
}
|
||||
#root::before {
|
||||
#app::before {
|
||||
content: '🦀';
|
||||
font-size: 1.5em;
|
||||
display: block;
|
||||
padding: 20px;
|
||||
}
|
||||
#root::after {
|
||||
#app::after {
|
||||
display: block;
|
||||
content: 'Crab Fit doesn\'t work in Internet Explorer. Please try using a modern browser.';
|
||||
}
|
||||
|
|
|
|||
85
crabfit-frontend/src/App.jsx
Normal file
85
crabfit-frontend/src/App.jsx
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
import { useState, useEffect, useCallback, Suspense } from 'react'
|
||||
import { Route, Routes } from 'react-router-dom'
|
||||
import { Workbox } from 'workbox-window'
|
||||
|
||||
import * as Pages from '/src/pages'
|
||||
import { Settings, Loading, Egg, UpdateDialog, TranslateDialog } from '/src/components'
|
||||
|
||||
import { useSettingsStore, useTranslateStore } from '/src/stores'
|
||||
|
||||
const EGG_PATTERN = ['ArrowUp', 'ArrowUp', 'ArrowDown', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'ArrowLeft', 'ArrowRight', 'b', 'a']
|
||||
|
||||
const wb = new Workbox('sw.js')
|
||||
|
||||
const App = () => {
|
||||
const [eggCount, setEggCount] = useState(0)
|
||||
const [eggVisible, setEggVisible] = useState(false)
|
||||
const [eggKey, setEggKey] = useState(0)
|
||||
|
||||
const [updateAvailable, setUpdateAvailable] = useState(false)
|
||||
const languageSupported = useTranslateStore(state => state.navigatorSupported)
|
||||
const translateDialogDismissed = useTranslateStore(state => state.translateDialogDismissed)
|
||||
|
||||
const eggHandler = useCallback(e => {
|
||||
if (EGG_PATTERN.indexOf(e.key) < 0 || e.key !== EGG_PATTERN[eggCount]) return setEggCount(0)
|
||||
setEggCount(eggCount+1)
|
||||
if (EGG_PATTERN.length === eggCount+1) {
|
||||
setEggKey(eggKey+1)
|
||||
setEggCount(0)
|
||||
setEggVisible(true)
|
||||
}
|
||||
}, [eggCount, eggKey])
|
||||
|
||||
useEffect(() => {
|
||||
// Register service worker
|
||||
if ('serviceWorker' in navigator && process.env.NODE_ENV === 'production') {
|
||||
wb.addEventListener('installed', event => {
|
||||
if (event.isUpdate) {
|
||||
setUpdateAvailable(true)
|
||||
}
|
||||
})
|
||||
|
||||
wb.register()
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
document.addEventListener('keyup', eggHandler, false)
|
||||
return () => document.removeEventListener('keyup', eggHandler, false)
|
||||
}, [eggHandler])
|
||||
|
||||
// Use user theme preference
|
||||
const theme = useSettingsStore(state => state.theme)
|
||||
useEffect(() => {
|
||||
document.body.classList.toggle('light', theme === 'Light')
|
||||
document.body.classList.toggle('dark', theme === 'Dark')
|
||||
}, [theme])
|
||||
|
||||
return (
|
||||
<>
|
||||
{!languageSupported && !translateDialogDismissed && <TranslateDialog />}
|
||||
|
||||
<Suspense fallback={<Loading />}>
|
||||
<Settings />
|
||||
|
||||
<Routes>
|
||||
<Route path="/" element={<Pages.Home />} />
|
||||
<Route path="/how-to" element={<Pages.Help />} />
|
||||
<Route path="/privacy" element={<Pages.Privacy />} />
|
||||
<Route path="/create" element={<Pages.Create />} />
|
||||
<Route path="/:id" element={<Pages.Event />} />
|
||||
</Routes>
|
||||
</Suspense>
|
||||
|
||||
{updateAvailable && (
|
||||
<Suspense fallback={<Loading />}>
|
||||
<UpdateDialog onClose={() => setUpdateAvailable(false)} />
|
||||
</Suspense>
|
||||
)}
|
||||
|
||||
{eggVisible && <Egg eggKey={eggKey} onClose={() => setEggVisible(false)} />}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default App
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
import { render, screen } from '@testing-library/react';
|
||||
import App from './App';
|
||||
|
||||
test('renders learn react link', () => {
|
||||
render(<App />);
|
||||
const linkElement = screen.getByText(/learn react/i);
|
||||
expect(linkElement).toBeInTheDocument();
|
||||
});
|
||||
|
|
@ -1,178 +0,0 @@
|
|||
import { useState, useEffect, useCallback, Suspense, lazy } from 'react';
|
||||
import { BrowserRouter, Switch, Route } from 'react-router-dom';
|
||||
import { ThemeProvider, Global } from '@emotion/react';
|
||||
import { Workbox } from 'workbox-window';
|
||||
|
||||
import { Settings, Loading, Egg, UpdateDialog, TranslateDialog } from 'components';
|
||||
|
||||
import { useSettingsStore, useTranslateStore } from 'stores';
|
||||
import theme from 'theme';
|
||||
|
||||
const EGG_PATTERN = ['ArrowUp', 'ArrowUp', 'ArrowDown', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'ArrowLeft', 'ArrowRight', 'b', 'a'];
|
||||
|
||||
const Home = lazy(() => import('pages/Home/Home'));
|
||||
const Event = lazy(() => import('pages/Event/Event'));
|
||||
const Create = lazy(() => import('pages/Create/Create'));
|
||||
const Help = lazy(() => import('pages/Help/Help'));
|
||||
const Privacy = lazy(() => import('pages/Privacy/Privacy'));
|
||||
|
||||
const wb = new Workbox('sw.js');
|
||||
|
||||
const App = () => {
|
||||
const colortheme = useSettingsStore(state => state.theme);
|
||||
const darkQuery = window.matchMedia('(prefers-color-scheme: dark)');
|
||||
const [isDark, setIsDark] = useState(darkQuery.matches);
|
||||
const [offline, setOffline] = useState(!window.navigator.onLine);
|
||||
|
||||
const [eggCount, setEggCount] = useState(0);
|
||||
const [eggVisible, setEggVisible] = useState(false);
|
||||
const [eggKey, setEggKey] = useState(0);
|
||||
|
||||
const [updateAvailable, setUpdateAvailable] = useState(false);
|
||||
const languageSupported = useTranslateStore(state => state.navigatorSupported);
|
||||
const translateDialogDismissed = useTranslateStore(state => state.translateDialogDismissed);
|
||||
|
||||
const eggHandler = useCallback(
|
||||
event => {
|
||||
if (EGG_PATTERN.indexOf(event.key) < 0 || event.key !== EGG_PATTERN[eggCount]) {
|
||||
setEggCount(0);
|
||||
return;
|
||||
}
|
||||
setEggCount(eggCount+1);
|
||||
if (EGG_PATTERN.length === eggCount+1) {
|
||||
setEggKey(eggKey+1);
|
||||
setEggCount(0);
|
||||
setEggVisible(true);
|
||||
}
|
||||
},
|
||||
[eggCount, eggKey]
|
||||
);
|
||||
|
||||
darkQuery.addListener(e => colortheme === 'System' && setIsDark(e.matches));
|
||||
|
||||
useEffect(() => {
|
||||
const onOffline = () => setOffline(true);
|
||||
const onOnline = () => setOffline(false);
|
||||
|
||||
window.addEventListener('offline', onOffline, false);
|
||||
window.addEventListener('online', onOnline, false);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('offline', onOffline, false);
|
||||
window.removeEventListener('online', onOnline, false);
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
// Register service worker
|
||||
if ('serviceWorker' in navigator && process.env.NODE_ENV === 'production') {
|
||||
wb.addEventListener('installed', event => {
|
||||
if (event.isUpdate) {
|
||||
setUpdateAvailable(true);
|
||||
}
|
||||
});
|
||||
|
||||
wb.register();
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
document.addEventListener('keyup', eggHandler, false);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('keyup', eggHandler, false);
|
||||
};
|
||||
}, [eggHandler]);
|
||||
|
||||
useEffect(() => {
|
||||
setIsDark(colortheme === 'System' ? darkQuery.matches : colortheme === 'Dark');
|
||||
}, [colortheme, darkQuery.matches]);
|
||||
|
||||
return (
|
||||
<BrowserRouter>
|
||||
<ThemeProvider theme={theme[isDark ? 'dark' : 'light']}>
|
||||
<Global
|
||||
styles={theme => ({
|
||||
html: {
|
||||
scrollBehavior: 'smooth',
|
||||
WebkitPrintColorAdjust: 'exact',
|
||||
},
|
||||
body: {
|
||||
backgroundColor: theme.background,
|
||||
color: theme.text,
|
||||
fontFamily: `'Karla', sans-serif`,
|
||||
fontWeight: theme.mode === 'dark' ? 500 : 600,
|
||||
margin: 0,
|
||||
},
|
||||
a: {
|
||||
color: theme.primary,
|
||||
},
|
||||
'*::-webkit-scrollbar': {
|
||||
width: 16,
|
||||
height: 16,
|
||||
},
|
||||
'*::-webkit-scrollbar-track': {
|
||||
background: `${theme.primaryBackground}`,
|
||||
},
|
||||
'*::-webkit-scrollbar-thumb': {
|
||||
borderRadius: 100,
|
||||
border: `4px solid ${theme.primaryBackground}`,
|
||||
width: 12,
|
||||
background: `${theme.mode === 'light' ? theme.primaryLight : theme.primaryDark}AA`,
|
||||
},
|
||||
'*::-webkit-scrollbar-thumb:hover': {
|
||||
background: `${theme.mode === 'light' ? theme.primaryLight : theme.primaryDark}CC`,
|
||||
},
|
||||
'*::-webkit-scrollbar-thumb:active': {
|
||||
background: `${theme.mode === 'light' ? theme.primaryLight : theme.primaryDark}`,
|
||||
},
|
||||
})}
|
||||
/>
|
||||
|
||||
{!languageSupported && !translateDialogDismissed && <TranslateDialog />}
|
||||
|
||||
<Suspense fallback={<Loading />}>
|
||||
<Settings />
|
||||
</Suspense>
|
||||
|
||||
<Switch>
|
||||
<Route path="/" exact render={props => (
|
||||
<Suspense fallback={<Loading />}>
|
||||
<Home offline={offline} {...props} />
|
||||
</Suspense>
|
||||
)} />
|
||||
<Route path="/how-to" exact render={props => (
|
||||
<Suspense fallback={<Loading />}>
|
||||
<Help {...props} />
|
||||
</Suspense>
|
||||
)} />
|
||||
<Route path="/privacy" exact render={props => (
|
||||
<Suspense fallback={<Loading />}>
|
||||
<Privacy {...props} />
|
||||
</Suspense>
|
||||
)} />
|
||||
<Route path="/create" exact render={props => (
|
||||
<Suspense fallback={<Loading />}>
|
||||
<Create offline={offline} {...props} />
|
||||
</Suspense>
|
||||
)} />
|
||||
<Route path="/:id" exact render={props => (
|
||||
<Suspense fallback={<Loading />}>
|
||||
<Event offline={offline} {...props} />
|
||||
</Suspense>
|
||||
)} />
|
||||
</Switch>
|
||||
|
||||
{updateAvailable && (
|
||||
<Suspense fallback={<Loading />}>
|
||||
<UpdateDialog onClose={() => setUpdateAvailable(false)} />
|
||||
</Suspense>
|
||||
)}
|
||||
|
||||
{eggVisible && <Egg eggKey={eggKey} onClose={() => setEggVisible(false)} />}
|
||||
</ThemeProvider>
|
||||
</BrowserRouter>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
|
|
@ -1,12 +1,13 @@
|
|||
import { useState, useRef, Fragment, Suspense, lazy } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useLocaleUpdateStore } from 'stores';
|
||||
import dayjs from 'dayjs';
|
||||
import localeData from 'dayjs/plugin/localeData';
|
||||
import customParseFormat from 'dayjs/plugin/customParseFormat';
|
||||
import isBetween from 'dayjs/plugin/isBetween';
|
||||
import dayjs_timezone from 'dayjs/plugin/timezone';
|
||||
import utc from 'dayjs/plugin/utc';
|
||||
import { useState, useRef, Fragment, Suspense, lazy } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import dayjs from 'dayjs'
|
||||
import localeData from 'dayjs/plugin/localeData'
|
||||
import customParseFormat from 'dayjs/plugin/customParseFormat'
|
||||
import isBetween from 'dayjs/plugin/isBetween'
|
||||
import dayjs_timezone from 'dayjs/plugin/timezone'
|
||||
import utc from 'dayjs/plugin/utc'
|
||||
|
||||
import { useLocaleUpdateStore } from '/src/stores'
|
||||
|
||||
import {
|
||||
Wrapper,
|
||||
|
|
@ -21,20 +22,20 @@ import {
|
|||
TimeLabel,
|
||||
TimeSpace,
|
||||
StyledMain,
|
||||
} from 'components/AvailabilityViewer/availabilityViewerStyle';
|
||||
import { Time } from './availabilityEditorStyle';
|
||||
} from '/src/components/AvailabilityViewer/AvailabilityViewer.styles'
|
||||
import { Time } from './AvailabilityEditor.styles'
|
||||
|
||||
import { _GoogleCalendar, _OutlookCalendar, Center } from 'components';
|
||||
import { Loader } from '../Loading/loadingStyle';
|
||||
import { _GoogleCalendar, _OutlookCalendar, Center } from '/src/components'
|
||||
import { Loader } from '../Loading/Loading.styles'
|
||||
|
||||
const GoogleCalendar = lazy(() => _GoogleCalendar());
|
||||
const OutlookCalendar = lazy(() => _OutlookCalendar());
|
||||
const GoogleCalendar = lazy(() => _GoogleCalendar())
|
||||
const OutlookCalendar = lazy(() => _OutlookCalendar())
|
||||
|
||||
dayjs.extend(localeData);
|
||||
dayjs.extend(customParseFormat);
|
||||
dayjs.extend(isBetween);
|
||||
dayjs.extend(utc);
|
||||
dayjs.extend(dayjs_timezone);
|
||||
dayjs.extend(localeData)
|
||||
dayjs.extend(customParseFormat)
|
||||
dayjs.extend(isBetween)
|
||||
dayjs.extend(utc)
|
||||
dayjs.extend(dayjs_timezone)
|
||||
|
||||
const AvailabilityEditor = ({
|
||||
times,
|
||||
|
|
@ -44,25 +45,24 @@ const AvailabilityEditor = ({
|
|||
isSpecificDates,
|
||||
value = [],
|
||||
onChange,
|
||||
...props
|
||||
}) => {
|
||||
const { t } = useTranslation('event');
|
||||
const locale = useLocaleUpdateStore(state => state.locale);
|
||||
const { t } = useTranslation('event')
|
||||
const locale = useLocaleUpdateStore(state => state.locale)
|
||||
|
||||
const [selectingTimes, _setSelectingTimes] = useState([]);
|
||||
const staticSelectingTimes = useRef([]);
|
||||
const [selectingTimes, _setSelectingTimes] = useState([])
|
||||
const staticSelectingTimes = useRef([])
|
||||
const setSelectingTimes = newTimes => {
|
||||
staticSelectingTimes.current = newTimes;
|
||||
_setSelectingTimes(newTimes);
|
||||
};
|
||||
staticSelectingTimes.current = newTimes
|
||||
_setSelectingTimes(newTimes)
|
||||
}
|
||||
|
||||
const startPos = useRef({});
|
||||
const staticMode = useRef(null);
|
||||
const [mode, _setMode] = useState(staticMode.current);
|
||||
const startPos = useRef({})
|
||||
const staticMode = useRef(null)
|
||||
const [mode, _setMode] = useState(staticMode.current)
|
||||
const setMode = newMode => {
|
||||
staticMode.current = newMode;
|
||||
_setMode(newMode);
|
||||
};
|
||||
staticMode.current = newMode
|
||||
_setMode(newMode)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
|
|
@ -109,8 +109,8 @@ const AvailabilityEditor = ({
|
|||
)}
|
||||
</TimeLabels>
|
||||
{dates.map((date, x) => {
|
||||
const parsedDate = isSpecificDates ? dayjs(date, 'DDMMYYYY') : dayjs().day(date);
|
||||
const last = dates.length === x+1 || (isSpecificDates ? dayjs(dates[x+1], 'DDMMYYYY') : dayjs().day(dates[x+1])).diff(parsedDate, 'day') > 1;
|
||||
const parsedDate = isSpecificDates ? dayjs(date, 'DDMMYYYY') : dayjs().day(date)
|
||||
const last = dates.length === x+1 || (isSpecificDates ? dayjs(dates[x+1], 'DDMMYYYY') : dayjs().day(dates[x+1])).diff(parsedDate, 'day') > 1
|
||||
return (
|
||||
<Fragment key={x}>
|
||||
<Date>
|
||||
|
|
@ -118,55 +118,55 @@ const AvailabilityEditor = ({
|
|||
<DayLabel>{parsedDate.format('ddd')}</DayLabel>
|
||||
|
||||
<Times
|
||||
borderRight={last}
|
||||
borderLeft={x === 0 || (parsedDate).diff(isSpecificDates ? dayjs(dates[x-1], 'DDMMYYYY') : dayjs().day(dates[x-1]), 'day') > 1}
|
||||
$borderRight={last}
|
||||
$borderLeft={x === 0 || (parsedDate).diff(isSpecificDates ? dayjs(dates[x-1], 'DDMMYYYY') : dayjs().day(dates[x-1]), 'day') > 1}
|
||||
>
|
||||
{timeLabels.map((timeLabel, y) => {
|
||||
if (!timeLabel.time) return null;
|
||||
if (!timeLabel.time) return null
|
||||
if (!times.includes(`${timeLabel.time}-${date}`)) {
|
||||
return (
|
||||
<TimeSpace key={x+y} className='timespace' title={t('event:greyed_times')} />
|
||||
);
|
||||
<TimeSpace key={x+y} className="timespace" title={t('event:greyed_times')} />
|
||||
)
|
||||
}
|
||||
const time = `${timeLabel.time}-${date}`;
|
||||
const time = `${timeLabel.time}-${date}`
|
||||
|
||||
return (
|
||||
<Time
|
||||
key={x+y}
|
||||
time={time}
|
||||
$time={time}
|
||||
className="time"
|
||||
selected={value.includes(time)}
|
||||
selecting={selectingTimes.includes(time)}
|
||||
mode={mode}
|
||||
onPointerDown={(e) => {
|
||||
e.preventDefault();
|
||||
startPos.current = {x, y};
|
||||
setMode(value.includes(time) ? 'remove' : 'add');
|
||||
setSelectingTimes([time]);
|
||||
e.currentTarget.releasePointerCapture(e.pointerId);
|
||||
$selected={value.includes(time)}
|
||||
$selecting={selectingTimes.includes(time)}
|
||||
$mode={mode}
|
||||
onPointerDown={e => {
|
||||
e.preventDefault()
|
||||
startPos.current = {x, y}
|
||||
setMode(value.includes(time) ? 'remove' : 'add')
|
||||
setSelectingTimes([time])
|
||||
e.currentTarget.releasePointerCapture(e.pointerId)
|
||||
|
||||
document.addEventListener('pointerup', () => {
|
||||
if (staticMode.current === 'add') {
|
||||
onChange([...value, ...staticSelectingTimes.current]);
|
||||
onChange([...value, ...staticSelectingTimes.current])
|
||||
} else if (staticMode.current === 'remove') {
|
||||
onChange(value.filter(t => !staticSelectingTimes.current.includes(t)));
|
||||
onChange(value.filter(t => !staticSelectingTimes.current.includes(t)))
|
||||
}
|
||||
setMode(null);
|
||||
}, { once: true });
|
||||
setMode(null)
|
||||
}, { once: true })
|
||||
}}
|
||||
onPointerEnter={() => {
|
||||
if (staticMode.current) {
|
||||
let found = [];
|
||||
const found = []
|
||||
for (let cy = Math.min(startPos.current.y, y); cy < Math.max(startPos.current.y, y)+1; cy++) {
|
||||
for (let cx = Math.min(startPos.current.x, x); cx < Math.max(startPos.current.x, x)+1; cx++) {
|
||||
found.push({y: cy, x: cx});
|
||||
found.push({y: cy, x: cx})
|
||||
}
|
||||
}
|
||||
setSelectingTimes(found.filter(d => timeLabels[d.y].time?.length === 4).map(d => `${timeLabels[d.y].time}-${dates[d.x]}`));
|
||||
setSelectingTimes(found.filter(d => timeLabels[d.y].time?.length === 4).map(d => `${timeLabels[d.y].time}-${dates[d.x]}`))
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
)
|
||||
})}
|
||||
</Times>
|
||||
</Date>
|
||||
|
|
@ -174,13 +174,13 @@ const AvailabilityEditor = ({
|
|||
<Spacer />
|
||||
)}
|
||||
</Fragment>
|
||||
);
|
||||
)
|
||||
})}
|
||||
</Container>
|
||||
</ScrollWrapper>
|
||||
</Wrapper>
|
||||
</>
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
export default AvailabilityEditor;
|
||||
export default AvailabilityEditor
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
import { styled } from 'goober'
|
||||
|
||||
export const Time = styled('div')`
|
||||
height: 10px;
|
||||
touch-action: none;
|
||||
transition: background-color .1s;
|
||||
|
||||
${props => props.$time.slice(2, 4) === '00' && `
|
||||
border-top: 2px solid var(--text);
|
||||
`}
|
||||
${props => props.$time.slice(2, 4) !== '00' && `
|
||||
border-top: 2px solid transparent;
|
||||
`}
|
||||
${props => props.$time.slice(2, 4) === '30' && `
|
||||
border-top: 2px dotted var(--text);
|
||||
`}
|
||||
|
||||
${props => (props.$selected || (props.$mode === 'add' && props.$selecting)) && `
|
||||
background-color: var(--primary);
|
||||
`};
|
||||
${props => props.$mode === 'remove' && props.$selecting && `
|
||||
background-color: var(--background);
|
||||
`};
|
||||
`
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
import styled from '@emotion/styled';
|
||||
|
||||
export const Time = styled.div`
|
||||
height: 10px;
|
||||
touch-action: none;
|
||||
transition: background-color .1s;
|
||||
|
||||
${props => props.time.slice(2, 4) === '00' && `
|
||||
border-top: 2px solid ${props.theme.text};
|
||||
`}
|
||||
${props => props.time.slice(2, 4) !== '00' && `
|
||||
border-top: 2px solid transparent;
|
||||
`}
|
||||
${props => props.time.slice(2, 4) === '30' && `
|
||||
border-top: 2px dotted ${props.theme.text};
|
||||
`}
|
||||
|
||||
${props => (props.selected || (props.mode === 'add' && props.selecting)) && `
|
||||
background-color: ${props.theme.primary};
|
||||
`};
|
||||
${props => props.mode === 'remove' && props.selecting && `
|
||||
background-color: ${props.theme.background};
|
||||
`};
|
||||
`;
|
||||
|
|
@ -1,13 +1,13 @@
|
|||
import { useState, useEffect, useRef, useMemo, Fragment } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import dayjs from 'dayjs';
|
||||
import localeData from 'dayjs/plugin/localeData';
|
||||
import customParseFormat from 'dayjs/plugin/customParseFormat';
|
||||
import relativeTime from 'dayjs/plugin/relativeTime';
|
||||
import { useState, useEffect, useRef, useMemo, Fragment } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import dayjs from 'dayjs'
|
||||
import localeData from 'dayjs/plugin/localeData'
|
||||
import customParseFormat from 'dayjs/plugin/customParseFormat'
|
||||
import relativeTime from 'dayjs/plugin/relativeTime'
|
||||
|
||||
import { useSettingsStore, useLocaleUpdateStore } from 'stores';
|
||||
import { useSettingsStore, useLocaleUpdateStore } from '/src/stores'
|
||||
|
||||
import { Legend } from 'components';
|
||||
import { Legend } from '/src/components'
|
||||
import {
|
||||
Wrapper,
|
||||
ScrollWrapper,
|
||||
|
|
@ -30,13 +30,13 @@ import {
|
|||
Person,
|
||||
StyledMain,
|
||||
Info,
|
||||
} from './availabilityViewerStyle';
|
||||
} from './AvailabilityViewer.styles'
|
||||
|
||||
import locales from 'res/dayjs_locales';
|
||||
import locales from '/src/i18n/locales'
|
||||
|
||||
dayjs.extend(localeData);
|
||||
dayjs.extend(customParseFormat);
|
||||
dayjs.extend(relativeTime);
|
||||
dayjs.extend(localeData)
|
||||
dayjs.extend(customParseFormat)
|
||||
dayjs.extend(relativeTime)
|
||||
|
||||
const AvailabilityViewer = ({
|
||||
times,
|
||||
|
|
@ -46,25 +46,24 @@ const AvailabilityViewer = ({
|
|||
people = [],
|
||||
min = 0,
|
||||
max = 0,
|
||||
...props
|
||||
}) => {
|
||||
const [tooltip, setTooltip] = useState(null);
|
||||
const timeFormat = useSettingsStore(state => state.timeFormat);
|
||||
const highlight = useSettingsStore(state => state.highlight);
|
||||
const [filteredPeople, setFilteredPeople] = useState([]);
|
||||
const [touched, setTouched] = useState(false);
|
||||
const [tempFocus, setTempFocus] = useState(null);
|
||||
const [focusCount, setFocusCount] = useState(null);
|
||||
const [tooltip, setTooltip] = useState(null)
|
||||
const timeFormat = useSettingsStore(state => state.timeFormat)
|
||||
const highlight = useSettingsStore(state => state.highlight)
|
||||
const [filteredPeople, setFilteredPeople] = useState([])
|
||||
const [touched, setTouched] = useState(false)
|
||||
const [tempFocus, setTempFocus] = useState(null)
|
||||
const [focusCount, setFocusCount] = useState(null)
|
||||
|
||||
const { t } = useTranslation('event');
|
||||
const locale = useLocaleUpdateStore(state => state.locale);
|
||||
const { t } = useTranslation('event')
|
||||
const locale = useLocaleUpdateStore(state => state.locale)
|
||||
|
||||
const wrapper = useRef();
|
||||
const wrapper = useRef()
|
||||
|
||||
useEffect(() => {
|
||||
setFilteredPeople(people.map(p => p.name));
|
||||
setTouched(people.length <= 1);
|
||||
}, [people]);
|
||||
setFilteredPeople(people.map(p => p.name))
|
||||
setTouched(people.length <= 1)
|
||||
}, [people])
|
||||
|
||||
const heatmap = useMemo(() => (
|
||||
<Container>
|
||||
|
|
@ -76,8 +75,8 @@ const AvailabilityViewer = ({
|
|||
)}
|
||||
</TimeLabels>
|
||||
{dates.map((date, i) => {
|
||||
const parsedDate = isSpecificDates ? dayjs(date, 'DDMMYYYY') : dayjs().day(date);
|
||||
const last = dates.length === i+1 || (isSpecificDates ? dayjs(dates[i+1], 'DDMMYYYY') : dayjs().day(dates[i+1])).diff(parsedDate, 'day') > 1;
|
||||
const parsedDate = isSpecificDates ? dayjs(date, 'DDMMYYYY') : dayjs().day(date)
|
||||
const last = dates.length === i+1 || (isSpecificDates ? dayjs(dates[i+1], 'DDMMYYYY') : dayjs().day(dates[i+1])).diff(parsedDate, 'day') > 1
|
||||
return (
|
||||
<Fragment key={i}>
|
||||
<Date>
|
||||
|
|
@ -85,48 +84,48 @@ const AvailabilityViewer = ({
|
|||
<DayLabel>{parsedDate.format('ddd')}</DayLabel>
|
||||
|
||||
<Times
|
||||
borderRight={last}
|
||||
borderLeft={i === 0 || (parsedDate).diff(isSpecificDates ? dayjs(dates[i-1], 'DDMMYYYY') : dayjs().day(dates[i-1]), 'day') > 1}
|
||||
$borderRight={last}
|
||||
$borderLeft={i === 0 || (parsedDate).diff(isSpecificDates ? dayjs(dates[i-1], 'DDMMYYYY') : dayjs().day(dates[i-1]), 'day') > 1}
|
||||
>
|
||||
{timeLabels.map((timeLabel, i) => {
|
||||
if (!timeLabel.time) return null;
|
||||
if (!timeLabel.time) return null
|
||||
if (!times.includes(`${timeLabel.time}-${date}`)) {
|
||||
return (
|
||||
<TimeSpace className='timespace' key={i} title={t('event:greyed_times')} />
|
||||
);
|
||||
<TimeSpace className="timespace" key={i} title={t('event:greyed_times')} />
|
||||
)
|
||||
}
|
||||
const time = `${timeLabel.time}-${date}`;
|
||||
const time = `${timeLabel.time}-${date}`
|
||||
const peopleHere = tempFocus !== null
|
||||
? people.filter(person => person.availability.includes(time) && tempFocus === person.name).map(person => person.name)
|
||||
: people.filter(person => person.availability.includes(time) && filteredPeople.includes(person.name)).map(person => person.name);
|
||||
: people.filter(person => person.availability.includes(time) && filteredPeople.includes(person.name)).map(person => person.name)
|
||||
|
||||
return (
|
||||
<Time
|
||||
key={i}
|
||||
time={time}
|
||||
$time={time}
|
||||
className="time"
|
||||
peopleCount={focusCount !== null && focusCount !== peopleHere.length ? 0 : peopleHere.length}
|
||||
$peopleCount={focusCount !== null && focusCount !== peopleHere.length ? 0 : peopleHere.length}
|
||||
aria-label={peopleHere.join(', ')}
|
||||
maxPeople={tempFocus !== null ? 1 : Math.min(max, filteredPeople.length)}
|
||||
minPeople={tempFocus !== null ? 0 : Math.min(min, filteredPeople.length)}
|
||||
highlight={highlight}
|
||||
onMouseEnter={(e) => {
|
||||
const cellBox = e.currentTarget.getBoundingClientRect();
|
||||
const wrapperBox = wrapper?.current?.getBoundingClientRect() ?? { x: 0, y: 0 };
|
||||
const timeText = timeFormat === '12h' ? `h${locales[locale].separator ?? ':'}mma` : `HH${locales[locale].separator ?? ':'}mm`;
|
||||
$maxPeople={tempFocus !== null ? 1 : Math.min(max, filteredPeople.length)}
|
||||
$minPeople={tempFocus !== null ? 0 : Math.min(min, filteredPeople.length)}
|
||||
$highlight={highlight}
|
||||
onMouseEnter={e => {
|
||||
const cellBox = e.currentTarget.getBoundingClientRect()
|
||||
const wrapperBox = wrapper?.current?.getBoundingClientRect() ?? { x: 0, y: 0 }
|
||||
const timeText = timeFormat === '12h' ? `h${locales[locale].separator ?? ':'}mma` : `HH${locales[locale].separator ?? ':'}mm`
|
||||
setTooltip({
|
||||
x: Math.round(cellBox.x-wrapperBox.x + cellBox.width/2),
|
||||
y: Math.round(cellBox.y-wrapperBox.y + cellBox.height)+6,
|
||||
available: `${peopleHere.length} / ${people.length} ${t('event:available')}`,
|
||||
date: parsedDate.hour(time.slice(0, 2)).minute(time.slice(2, 4)).format(isSpecificDates ? `${timeText} ddd, D MMM YYYY` : `${timeText} ddd`),
|
||||
people: peopleHere,
|
||||
});
|
||||
})
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
setTooltip(null);
|
||||
setTooltip(null)
|
||||
}}
|
||||
/>
|
||||
);
|
||||
)
|
||||
})}
|
||||
</Times>
|
||||
</Date>
|
||||
|
|
@ -134,7 +133,7 @@ const AvailabilityViewer = ({
|
|||
<Spacer />
|
||||
)}
|
||||
</Fragment>
|
||||
);
|
||||
)
|
||||
})}
|
||||
</Container>
|
||||
), [
|
||||
|
|
@ -152,7 +151,7 @@ const AvailabilityViewer = ({
|
|||
timeFormat,
|
||||
timeLabels,
|
||||
times,
|
||||
]);
|
||||
])
|
||||
|
||||
return (
|
||||
<>
|
||||
|
|
@ -171,18 +170,18 @@ const AvailabilityViewer = ({
|
|||
{people.map((person, i) =>
|
||||
<Person
|
||||
key={i}
|
||||
filtered={filteredPeople.includes(person.name)}
|
||||
$filtered={filteredPeople.includes(person.name)}
|
||||
onClick={() => {
|
||||
setTempFocus(null);
|
||||
setTempFocus(null)
|
||||
if (filteredPeople.includes(person.name)) {
|
||||
if (!touched) {
|
||||
setTouched(true);
|
||||
setFilteredPeople([person.name]);
|
||||
setTouched(true)
|
||||
setFilteredPeople([person.name])
|
||||
} else {
|
||||
setFilteredPeople(filteredPeople.filter(n => n !== person.name));
|
||||
setFilteredPeople(filteredPeople.filter(n => n !== person.name))
|
||||
}
|
||||
} else {
|
||||
setFilteredPeople([...filteredPeople, person.name]);
|
||||
setFilteredPeople([...filteredPeople, person.name])
|
||||
}
|
||||
}}
|
||||
onMouseOver={() => setTempFocus(person.name)}
|
||||
|
|
@ -201,8 +200,8 @@ const AvailabilityViewer = ({
|
|||
|
||||
{tooltip && (
|
||||
<Tooltip
|
||||
x={tooltip.x}
|
||||
y={tooltip.y}
|
||||
$x={tooltip.x}
|
||||
$y={tooltip.y}
|
||||
>
|
||||
<TooltipTitle>{tooltip.available}</TooltipTitle>
|
||||
<TooltipDate>{tooltip.date}</TooltipDate>
|
||||
|
|
@ -221,7 +220,7 @@ const AvailabilityViewer = ({
|
|||
</ScrollWrapper>
|
||||
</Wrapper>
|
||||
</>
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
export default AvailabilityViewer;
|
||||
export default AvailabilityViewer
|
||||
|
|
@ -1,16 +1,17 @@
|
|||
import styled from '@emotion/styled';
|
||||
import { styled } from 'goober'
|
||||
import { forwardRef } from 'react'
|
||||
|
||||
export const Wrapper = styled.div`
|
||||
export const Wrapper = styled('div', forwardRef)`
|
||||
overflow-y: visible;
|
||||
margin: 20px 0;
|
||||
position: relative;
|
||||
`;
|
||||
`
|
||||
|
||||
export const ScrollWrapper = styled.div`
|
||||
export const ScrollWrapper = styled('div')`
|
||||
overflow-x: auto;
|
||||
`;
|
||||
`
|
||||
|
||||
export const Container = styled.div`
|
||||
export const Container = styled('div')`
|
||||
display: inline-flex;
|
||||
box-sizing: border-box;
|
||||
min-width: 100%;
|
||||
|
|
@ -21,147 +22,147 @@ export const Container = styled.div`
|
|||
@media (max-width: 660px) {
|
||||
padding: 0 30px;
|
||||
}
|
||||
`;
|
||||
`
|
||||
|
||||
export const Date = styled.div`
|
||||
export const Date = styled('div')`
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 60px;
|
||||
min-width: 60px;
|
||||
margin-bottom: 10px;
|
||||
`;
|
||||
`
|
||||
|
||||
export const Times = styled.div`
|
||||
export const Times = styled('div')`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
border-bottom: 2px solid ${props => props.theme.text};
|
||||
border-left: 1px solid ${props => props.theme.text};
|
||||
border-right: 1px solid ${props => props.theme.text};
|
||||
border-bottom: 2px solid var(--text);
|
||||
border-left: 1px solid var(--text);
|
||||
border-right: 1px solid var(--text);
|
||||
|
||||
${props => props.borderLeft && `
|
||||
border-left: 2px solid ${props.theme.text};
|
||||
${props => props.$borderLeft && `
|
||||
border-left: 2px solid var(--text);
|
||||
border-top-left-radius: 3px;
|
||||
border-bottom-left-radius: 3px;
|
||||
`}
|
||||
${props => props.borderRight && `
|
||||
border-right: 2px solid ${props.theme.text};
|
||||
${props => props.$borderRight && `
|
||||
border-right: 2px solid var(--text);
|
||||
border-top-right-radius: 3px;
|
||||
border-bottom-right-radius: 3px;
|
||||
`}
|
||||
|
||||
& .time + .timespace, & .timespace:first-of-type {
|
||||
border-top: 2px solid ${props => props.theme.text};
|
||||
border-top: 2px solid var(--text);
|
||||
}
|
||||
`;
|
||||
`
|
||||
|
||||
export const DateLabel = styled.label`
|
||||
export const DateLabel = styled('label')`
|
||||
display: block;
|
||||
font-size: 12px;
|
||||
text-align: center;
|
||||
user-select: none;
|
||||
`;
|
||||
`
|
||||
|
||||
export const DayLabel = styled.label`
|
||||
export const DayLabel = styled('label')`
|
||||
display: block;
|
||||
font-size: 15px;
|
||||
text-align: center;
|
||||
user-select: none;
|
||||
`;
|
||||
`
|
||||
|
||||
export const Time = styled.div`
|
||||
export const Time = styled('div')`
|
||||
height: 10px;
|
||||
background-origin: border-box;
|
||||
transition: background-color .1s;
|
||||
|
||||
${props => props.time.slice(2, 4) === '00' && `
|
||||
border-top: 2px solid ${props.theme.text};
|
||||
${props => props.$time.slice(2, 4) === '00' && `
|
||||
border-top: 2px solid var(--text);
|
||||
`}
|
||||
${props => props.time.slice(2, 4) !== '00' && `
|
||||
${props => props.$time.slice(2, 4) !== '00' && `
|
||||
border-top: 2px solid transparent;
|
||||
`}
|
||||
${props => props.time.slice(2, 4) === '30' && `
|
||||
border-top: 2px dotted ${props.theme.text};
|
||||
${props => props.$time.slice(2, 4) === '30' && `
|
||||
border-top: 2px dotted var(--text);
|
||||
`}
|
||||
|
||||
background-color: ${props => `${props.theme.primary}${Math.round((props.peopleCount/props.maxPeople)*255).toString(16)}`};
|
||||
background-color: ${props => `#F79E00${Math.round((props.$peopleCount/props.$maxPeople)*255).toString(16)}`};
|
||||
|
||||
${props => props.highlight && props.peopleCount === props.maxPeople && props.peopleCount > 0 && `
|
||||
${props => props.$highlight && props.$peopleCount === props.$maxPeople && props.$peopleCount > 0 && `
|
||||
background-image: repeating-linear-gradient(
|
||||
45deg,
|
||||
transparent,
|
||||
transparent 4.3px,
|
||||
${props.theme.primaryDark} 4.3px,
|
||||
${props.theme.primaryDark} 8.6px
|
||||
var(--shadow) 4.3px,
|
||||
var(--shadow) 8.6px
|
||||
);
|
||||
`}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
transition: none;
|
||||
}
|
||||
`;
|
||||
`
|
||||
|
||||
export const Spacer = styled.div`
|
||||
export const Spacer = styled('div')`
|
||||
width: 12px;
|
||||
flex-shrink: 0;
|
||||
`;
|
||||
`
|
||||
|
||||
export const Tooltip = styled.div`
|
||||
export const Tooltip = styled('div')`
|
||||
position: absolute;
|
||||
top: ${props => props.y}px;
|
||||
left: ${props => props.x}px;
|
||||
top: ${props => props.$y}px;
|
||||
left: ${props => props.$x}px;
|
||||
transform: translateX(-50%);
|
||||
border: 1px solid ${props => props.theme.text};
|
||||
border: 1px solid var(--text);
|
||||
border-radius: 3px;
|
||||
padding: 4px 8px;
|
||||
background-color: ${props => props.theme.background}${props => props.theme.mode === 'light' ? 'EE' : 'DD'};
|
||||
background-color: var(--background);
|
||||
max-width: 200px;
|
||||
pointer-events: none;
|
||||
z-index: 100;
|
||||
user-select: none;
|
||||
`;
|
||||
`
|
||||
|
||||
export const TooltipTitle = styled.span`
|
||||
export const TooltipTitle = styled('span')`
|
||||
font-size: 15px;
|
||||
display: block;
|
||||
font-weight: 700;
|
||||
`;
|
||||
`
|
||||
|
||||
export const TooltipDate = styled.span`
|
||||
export const TooltipDate = styled('span')`
|
||||
font-size: 13px;
|
||||
display: block;
|
||||
opacity: .8;
|
||||
font-weight: 600;
|
||||
`;
|
||||
`
|
||||
|
||||
export const TooltipContent = styled.div`
|
||||
export const TooltipContent = styled('div')`
|
||||
font-size: 13px;
|
||||
padding: 4px 0;
|
||||
`;
|
||||
`
|
||||
|
||||
export const TooltipPerson = styled.span`
|
||||
export const TooltipPerson = styled('span')`
|
||||
display: inline-block;
|
||||
margin: 2px;
|
||||
padding: 1px 4px;
|
||||
border: 1px solid ${props => props.theme.primary};
|
||||
border: 1px solid var(--primary);
|
||||
border-radius: 3px;
|
||||
|
||||
${props => props.disabled && `
|
||||
opacity: .5;
|
||||
border-color: ${props.theme.text}
|
||||
border-color: var(--text);
|
||||
`}
|
||||
`;
|
||||
`
|
||||
|
||||
export const TimeLabels = styled.div`
|
||||
export const TimeLabels = styled('div')`
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 40px;
|
||||
padding-right: 6px;
|
||||
`;
|
||||
`
|
||||
|
||||
export const TimeSpace = styled.div`
|
||||
export const TimeSpace = styled('div')`
|
||||
height: 10px;
|
||||
position: relative;
|
||||
border-top: 2px solid transparent;
|
||||
|
|
@ -172,13 +173,13 @@ export const TimeSpace = styled.div`
|
|||
45deg,
|
||||
transparent,
|
||||
transparent 4.3px,
|
||||
${props => props.theme.loading} 4.3px,
|
||||
${props => props.theme.loading} 8.6px
|
||||
var(--loading) 4.3px,
|
||||
var(--loading) 8.6px
|
||||
);
|
||||
}
|
||||
`;
|
||||
`
|
||||
|
||||
export const TimeLabel = styled.label`
|
||||
export const TimeLabel = styled('label')`
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: -.7em;
|
||||
|
|
@ -186,46 +187,46 @@ export const TimeLabel = styled.label`
|
|||
text-align: right;
|
||||
user-select: none;
|
||||
width: 100%;
|
||||
`;
|
||||
`
|
||||
|
||||
export const StyledMain = styled.div`
|
||||
export const StyledMain = styled('div')`
|
||||
width: 600px;
|
||||
margin: 20px auto;
|
||||
max-width: calc(100% - 60px);
|
||||
`;
|
||||
`
|
||||
|
||||
export const People = styled.div`
|
||||
export const People = styled('div')`
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 5px;
|
||||
justify-content: center;
|
||||
margin: 14px auto;
|
||||
`;
|
||||
`
|
||||
|
||||
export const Person = styled.button`
|
||||
export const Person = styled('button')`
|
||||
font: inherit;
|
||||
font-size: 15px;
|
||||
border-radius: 3px;
|
||||
border: 1px solid ${props => props.theme.text};
|
||||
color: ${props => props.theme.text};
|
||||
border: 1px solid var(--text);
|
||||
color: var(--text);
|
||||
font-weight: 500;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
padding: 2px 8px;
|
||||
user-select: none;
|
||||
|
||||
${props => props.filtered && `
|
||||
background: ${props.theme.primary};
|
||||
${props => props.$filtered && `
|
||||
background: var(--primary);
|
||||
color: #FFFFFF;
|
||||
border-color: ${props.theme.primary};
|
||||
border-color: var(--primary);
|
||||
`}
|
||||
`;
|
||||
`
|
||||
|
||||
export const Info = styled.span`
|
||||
export const Info = styled('span')`
|
||||
display: block;
|
||||
text-align: center;
|
||||
|
||||
@media print {
|
||||
display: none;
|
||||
}
|
||||
`;
|
||||
`
|
||||
33
crabfit-frontend/src/components/Button/Button.jsx
Normal file
33
crabfit-frontend/src/components/Button/Button.jsx
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
import { Pressable } from './Button.styles'
|
||||
|
||||
const Button = ({
|
||||
href,
|
||||
type = 'button',
|
||||
icon,
|
||||
children,
|
||||
secondary,
|
||||
primaryColor,
|
||||
secondaryColor,
|
||||
small,
|
||||
size,
|
||||
isLoading,
|
||||
...props
|
||||
}) => (
|
||||
<Pressable
|
||||
type={type}
|
||||
as={href ? 'a' : 'button'}
|
||||
href={href}
|
||||
$secondary={secondary}
|
||||
$primaryColor={primaryColor}
|
||||
$secondaryColor={secondaryColor}
|
||||
$small={small}
|
||||
$size={size}
|
||||
$isLoading={isLoading}
|
||||
{...props}
|
||||
>
|
||||
{icon}
|
||||
{children}
|
||||
</Pressable>
|
||||
)
|
||||
|
||||
export default Button
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import styled from '@emotion/styled';
|
||||
import { styled } from 'goober'
|
||||
|
||||
export const Pressable = styled.button`
|
||||
export const Pressable = styled('button')`
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
|
|
@ -11,12 +11,12 @@ export const Pressable = styled.button`
|
|||
text-decoration: none;
|
||||
font: inherit;
|
||||
box-sizing: border-box;
|
||||
background: ${props => props.primaryColor || props.theme.primary};
|
||||
color: ${props => props.primaryColor ? '#FFF' : props.theme.background};
|
||||
background: ${props => props.$primaryColor || 'var(--primary)'};
|
||||
color: ${props => props.$primaryColor ? '#FFF' : 'var(--background)'};
|
||||
font-weight: 600;
|
||||
transition: transform 150ms cubic-bezier(0, 0, 0.58, 1);
|
||||
border-radius: 3px;
|
||||
padding: ${props => props.small ? '.4em 1.3em' : '.6em 1.5em'};
|
||||
padding: ${props => props.$small ? '.4em 1.3em' : '.6em 1.5em'};
|
||||
transform-style: preserve-3d;
|
||||
margin-bottom: 5px;
|
||||
|
||||
|
|
@ -26,10 +26,10 @@ export const Pressable = styled.button`
|
|||
margin-right: .5em;
|
||||
}
|
||||
|
||||
${props => props.size && `
|
||||
${props => props.$size && `
|
||||
padding: 0;
|
||||
height: ${props.size};
|
||||
width: ${props.size};
|
||||
height: ${props.$size};
|
||||
width: ${props.$size};
|
||||
`}
|
||||
|
||||
&::before {
|
||||
|
|
@ -39,7 +39,7 @@ export const Pressable = styled.button`
|
|||
width: 100%;
|
||||
top: 0;
|
||||
left: 0;
|
||||
background: ${props => props.secondaryColor || props.theme.primaryDark};
|
||||
background: ${props => props.$secondaryColor || 'var(--shadow)'};
|
||||
border-radius: inherit;
|
||||
transform: translate3d(0, 5px, -1em);
|
||||
transition: transform 150ms cubic-bezier(0, 0, 0.58, 1), box-shadow 150ms cubic-bezier(0, 0, 0.58, 1);
|
||||
|
|
@ -59,7 +59,7 @@ export const Pressable = styled.button`
|
|||
}
|
||||
}
|
||||
|
||||
${props => props.isLoading && `
|
||||
${props => props.$isLoading && `
|
||||
color: transparent;
|
||||
cursor: wait;
|
||||
|
||||
|
|
@ -83,7 +83,7 @@ export const Pressable = styled.button`
|
|||
left: calc(50% - 12px);
|
||||
height: 18px;
|
||||
width: 18px;
|
||||
border: 3px solid ${props.primaryColor ? '#FFF' : props.theme.background};
|
||||
border: 3px solid ${props.$primaryColor ? '#FFF' : 'var(--background)'};
|
||||
border-left-color: transparent;
|
||||
border-radius: 100px;
|
||||
animation: load .5s linear infinite;
|
||||
|
|
@ -92,7 +92,7 @@ export const Pressable = styled.button`
|
|||
@media (prefers-reduced-motion: reduce) {
|
||||
&:after {
|
||||
content: 'loading...';
|
||||
color: ${props.primaryColor ? '#FFF' : props.theme.background};
|
||||
color: ${props.$primaryColor ? '#FFF' : 'var(--background)'};
|
||||
animation: none;
|
||||
width: initial;
|
||||
height: initial;
|
||||
|
|
@ -108,10 +108,10 @@ export const Pressable = styled.button`
|
|||
}
|
||||
`}
|
||||
|
||||
${props => props.secondary && `
|
||||
${props => props.$secondary && `
|
||||
background: transparent;
|
||||
border: 1px solid ${props.primaryColor || props.theme.mode === 'light' ? props.theme.primaryDark : props.theme.primaryLight};
|
||||
color: ${props.primaryColor || props.theme.mode === 'light' ? props.theme.primaryDark : props.theme.primaryLight};
|
||||
border: 1px solid ${props.$primaryColor || 'var(--secondary)'};
|
||||
color: ${props.$primaryColor || 'var(--secondary)'};
|
||||
margin-bottom: 0;
|
||||
|
||||
&::before {
|
||||
|
|
@ -123,12 +123,12 @@ export const Pressable = styled.button`
|
|||
`}
|
||||
|
||||
@media print {
|
||||
${props => !props.secondary && `
|
||||
box-shadow: 0 4px 0 0 ${props.secondaryColor || props.theme.primaryDark};
|
||||
${props => !props.$secondary && `
|
||||
box-shadow: 0 4px 0 0 ${props.$secondaryColor || 'var(--secondary)'};
|
||||
`}
|
||||
|
||||
&::before {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
`;
|
||||
`
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
import { Pressable } from './buttonStyle';
|
||||
|
||||
const Button = ({ href, type = 'button', icon, children, ...props }) => (
|
||||
<Pressable
|
||||
type={type}
|
||||
as={href ? 'a' : 'button'}
|
||||
href={href}
|
||||
{...props}
|
||||
>
|
||||
{icon}
|
||||
{children}
|
||||
</Pressable>
|
||||
);
|
||||
|
||||
export default Button;
|
||||
|
|
@ -1,12 +1,12 @@
|
|||
import { useState, useEffect, useRef, forwardRef } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import dayjs from 'dayjs';
|
||||
import isToday from 'dayjs/plugin/isToday';
|
||||
import localeData from 'dayjs/plugin/localeData';
|
||||
import updateLocale from 'dayjs/plugin/updateLocale';
|
||||
import { useState, useEffect, useRef, forwardRef } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import dayjs from 'dayjs'
|
||||
import isToday from 'dayjs/plugin/isToday'
|
||||
import localeData from 'dayjs/plugin/localeData'
|
||||
import updateLocale from 'dayjs/plugin/updateLocale'
|
||||
|
||||
import { Button, ToggleField } from 'components';
|
||||
import { useSettingsStore, useLocaleUpdateStore } from 'stores';
|
||||
import { Button, ToggleField } from '/src/components'
|
||||
import { useSettingsStore, useLocaleUpdateStore } from '/src/stores'
|
||||
|
||||
import {
|
||||
Wrapper,
|
||||
|
|
@ -17,35 +17,35 @@ import {
|
|||
CalendarBody,
|
||||
Date,
|
||||
Day,
|
||||
} from './calendarFieldStyle';
|
||||
} from './CalendarField.styles'
|
||||
|
||||
dayjs.extend(isToday);
|
||||
dayjs.extend(localeData);
|
||||
dayjs.extend(updateLocale);
|
||||
dayjs.extend(isToday)
|
||||
dayjs.extend(localeData)
|
||||
dayjs.extend(updateLocale)
|
||||
|
||||
const calculateMonth = (month, year, weekStart) => {
|
||||
const date = dayjs().month(month).year(year);
|
||||
const daysInMonth = date.daysInMonth();
|
||||
const daysBefore = date.date(1).day() - weekStart;
|
||||
const daysAfter = 6 - date.date(daysInMonth).day() + weekStart;
|
||||
const date = dayjs().month(month).year(year)
|
||||
const daysInMonth = date.daysInMonth()
|
||||
const daysBefore = date.date(1).day() - weekStart
|
||||
const daysAfter = 6 - date.date(daysInMonth).day() + weekStart
|
||||
|
||||
let dates = [];
|
||||
let curDate = date.date(1).subtract(daysBefore, 'day');
|
||||
let y = 0;
|
||||
let x = 0;
|
||||
const dates = []
|
||||
let curDate = date.date(1).subtract(daysBefore, 'day')
|
||||
let y = 0
|
||||
let x = 0
|
||||
for (let i = 0; i < daysBefore + daysInMonth + daysAfter; i++) {
|
||||
if (x === 0) dates[y] = [];
|
||||
dates[y][x] = curDate.clone();
|
||||
curDate = curDate.add(1, 'day');
|
||||
x++;
|
||||
if (x === 0) dates[y] = []
|
||||
dates[y][x] = curDate.clone()
|
||||
curDate = curDate.add(1, 'day')
|
||||
x++
|
||||
if (x > 6) {
|
||||
x = 0;
|
||||
y++;
|
||||
x = 0
|
||||
y++
|
||||
}
|
||||
}
|
||||
|
||||
return dates;
|
||||
};
|
||||
return dates
|
||||
}
|
||||
|
||||
const CalendarField = forwardRef(({
|
||||
label,
|
||||
|
|
@ -54,48 +54,48 @@ const CalendarField = forwardRef(({
|
|||
setValue,
|
||||
...props
|
||||
}, ref) => {
|
||||
const weekStart = useSettingsStore(state => state.weekStart);
|
||||
const locale = useLocaleUpdateStore(state => state.locale);
|
||||
const { t } = useTranslation('home');
|
||||
const weekStart = useSettingsStore(state => state.weekStart)
|
||||
const locale = useLocaleUpdateStore(state => state.locale)
|
||||
const { t } = useTranslation('home')
|
||||
|
||||
const [type, setType] = useState(0);
|
||||
const [type, setType] = useState(0)
|
||||
|
||||
const [dates, setDates] = useState(calculateMonth(dayjs().month(), dayjs().year(), weekStart));
|
||||
const [month, setMonth] = useState(dayjs().month());
|
||||
const [year, setYear] = useState(dayjs().year());
|
||||
const [dates, setDates] = useState(calculateMonth(dayjs().month(), dayjs().year(), weekStart))
|
||||
const [month, setMonth] = useState(dayjs().month())
|
||||
const [year, setYear] = useState(dayjs().year())
|
||||
|
||||
const [selectedDates, setSelectedDates] = useState([]);
|
||||
const [selectingDates, _setSelectingDates] = useState([]);
|
||||
const staticSelectingDates = useRef([]);
|
||||
const [selectedDates, setSelectedDates] = useState([])
|
||||
const [selectingDates, _setSelectingDates] = useState([])
|
||||
const staticSelectingDates = useRef([])
|
||||
const setSelectingDates = newDates => {
|
||||
staticSelectingDates.current = newDates;
|
||||
_setSelectingDates(newDates);
|
||||
};
|
||||
staticSelectingDates.current = newDates
|
||||
_setSelectingDates(newDates)
|
||||
}
|
||||
|
||||
const [selectedDays, setSelectedDays] = useState([]);
|
||||
const [selectingDays, _setSelectingDays] = useState([]);
|
||||
const staticSelectingDays = useRef([]);
|
||||
const [selectedDays, setSelectedDays] = useState([])
|
||||
const [selectingDays, _setSelectingDays] = useState([])
|
||||
const staticSelectingDays = useRef([])
|
||||
const setSelectingDays = newDays => {
|
||||
staticSelectingDays.current = newDays;
|
||||
_setSelectingDays(newDays);
|
||||
};
|
||||
staticSelectingDays.current = newDays
|
||||
_setSelectingDays(newDays)
|
||||
}
|
||||
|
||||
const startPos = useRef({});
|
||||
const staticMode = useRef(null);
|
||||
const [mode, _setMode] = useState(staticMode.current);
|
||||
const startPos = useRef({})
|
||||
const staticMode = useRef(null)
|
||||
const [mode, _setMode] = useState(staticMode.current)
|
||||
const setMode = newMode => {
|
||||
staticMode.current = newMode;
|
||||
_setMode(newMode);
|
||||
};
|
||||
staticMode.current = newMode
|
||||
_setMode(newMode)
|
||||
}
|
||||
|
||||
useEffect(() => setValue(props.name, type ? JSON.stringify(selectedDays) : JSON.stringify(selectedDates)), [type, selectedDays, selectedDates, setValue, props.name]);
|
||||
useEffect(() => setValue(props.name, type ? JSON.stringify(selectedDays) : JSON.stringify(selectedDates)), [type, selectedDays, selectedDates, setValue, props.name])
|
||||
|
||||
useEffect(() => {
|
||||
if (dayjs.Ls.hasOwnProperty(locale) && weekStart !== dayjs.Ls[locale].weekStart) {
|
||||
dayjs.updateLocale(locale, { weekStart });
|
||||
if (dayjs.Ls?.[locale] && weekStart !== dayjs.Ls[locale].weekStart) {
|
||||
dayjs.updateLocale(locale, { weekStart })
|
||||
}
|
||||
setDates(calculateMonth(month, year, weekStart));
|
||||
}, [weekStart, month, year, locale]);
|
||||
setDates(calculateMonth(month, year, weekStart))
|
||||
}, [weekStart, month, year, locale])
|
||||
|
||||
return (
|
||||
<Wrapper locale={locale}>
|
||||
|
|
@ -128,10 +128,10 @@ const CalendarField = forwardRef(({
|
|||
title={t('form.dates.tooltips.previous')}
|
||||
onClick={() => {
|
||||
if (month-1 < 0) {
|
||||
setYear(year-1);
|
||||
setMonth(11);
|
||||
setYear(year-1)
|
||||
setMonth(11)
|
||||
} else {
|
||||
setMonth(month-1);
|
||||
setMonth(month-1)
|
||||
}
|
||||
}}
|
||||
><</Button>
|
||||
|
|
@ -141,10 +141,10 @@ const CalendarField = forwardRef(({
|
|||
title={t('form.dates.tooltips.next')}
|
||||
onClick={() => {
|
||||
if (month+1 > 11) {
|
||||
setYear(year+1);
|
||||
setMonth(0);
|
||||
setYear(year+1)
|
||||
setMonth(0)
|
||||
} else {
|
||||
setMonth(month+1);
|
||||
setMonth(month+1)
|
||||
}
|
||||
}}
|
||||
>></Button>
|
||||
|
|
@ -160,47 +160,47 @@ const CalendarField = forwardRef(({
|
|||
dateRow.map((date, x) =>
|
||||
<Date
|
||||
key={y+x}
|
||||
otherMonth={date.month() !== month}
|
||||
isToday={date.isToday()}
|
||||
$otherMonth={date.month() !== month}
|
||||
$isToday={date.isToday()}
|
||||
title={`${date.date()} ${dayjs.months()[date.month()]}${date.isToday() ? ` (${t('form.dates.tooltips.today')})` : ''}`}
|
||||
selected={selectedDates.includes(date.format('DDMMYYYY'))}
|
||||
selecting={selectingDates.includes(date)}
|
||||
mode={mode}
|
||||
$selected={selectedDates.includes(date.format('DDMMYYYY'))}
|
||||
$selecting={selectingDates.includes(date)}
|
||||
$mode={mode}
|
||||
type="button"
|
||||
onKeyPress={e => {
|
||||
if (e.key === ' ' || e.key === 'Enter') {
|
||||
if (selectedDates.includes(date.format('DDMMYYYY'))) {
|
||||
setSelectedDates(selectedDates.filter(d => d !== date.format('DDMMYYYY')));
|
||||
setSelectedDates(selectedDates.filter(d => d !== date.format('DDMMYYYY')))
|
||||
} else {
|
||||
setSelectedDates([...selectedDates, date.format('DDMMYYYY')]);
|
||||
setSelectedDates([...selectedDates, date.format('DDMMYYYY')])
|
||||
}
|
||||
}
|
||||
}}
|
||||
onPointerDown={e => {
|
||||
startPos.current = {x, y};
|
||||
setMode(selectedDates.includes(date.format('DDMMYYYY')) ? 'remove' : 'add');
|
||||
setSelectingDates([date]);
|
||||
e.currentTarget.releasePointerCapture(e.pointerId);
|
||||
startPos.current = {x, y}
|
||||
setMode(selectedDates.includes(date.format('DDMMYYYY')) ? 'remove' : 'add')
|
||||
setSelectingDates([date])
|
||||
e.currentTarget.releasePointerCapture(e.pointerId)
|
||||
|
||||
document.addEventListener('pointerup', () => {
|
||||
if (staticMode.current === 'add') {
|
||||
setSelectedDates([...selectedDates, ...staticSelectingDates.current.map(d => d.format('DDMMYYYY'))]);
|
||||
setSelectedDates([...selectedDates, ...staticSelectingDates.current.map(d => d.format('DDMMYYYY'))])
|
||||
} else if (staticMode.current === 'remove') {
|
||||
const toRemove = staticSelectingDates.current.map(d => d.format('DDMMYYYY'));
|
||||
setSelectedDates(selectedDates.filter(d => !toRemove.includes(d)));
|
||||
const toRemove = staticSelectingDates.current.map(d => d.format('DDMMYYYY'))
|
||||
setSelectedDates(selectedDates.filter(d => !toRemove.includes(d)))
|
||||
}
|
||||
setMode(null);
|
||||
}, { once: true });
|
||||
setMode(null)
|
||||
}, { once: true })
|
||||
}}
|
||||
onPointerEnter={() => {
|
||||
if (staticMode.current) {
|
||||
let found = [];
|
||||
const found = []
|
||||
for (let cy = Math.min(startPos.current.y, y); cy < Math.max(startPos.current.y, y)+1; cy++) {
|
||||
for (let cx = Math.min(startPos.current.x, x); cx < Math.max(startPos.current.x, x)+1; cx++) {
|
||||
found.push({y: cy, x: cx});
|
||||
found.push({y: cy, x: cx})
|
||||
}
|
||||
}
|
||||
setSelectingDates(found.map(d => dates[d.y][d.x]));
|
||||
setSelectingDates(found.map(d => dates[d.y][d.x]))
|
||||
}
|
||||
}}
|
||||
>{date.date()}</Date>
|
||||
|
|
@ -213,44 +213,44 @@ const CalendarField = forwardRef(({
|
|||
{(weekStart ? [...dayjs.weekdaysShort().filter((_,i) => i !== 0), dayjs.weekdaysShort()[0]] : dayjs.weekdaysShort()).map((name, i) =>
|
||||
<Date
|
||||
key={name}
|
||||
isToday={(weekStart ? [...dayjs.weekdaysShort().filter((_,i) => i !== 0), dayjs.weekdaysShort()[0]] : dayjs.weekdaysShort())[dayjs().day()-weekStart === -1 ? 6 : dayjs().day()-weekStart] === name}
|
||||
$isToday={(weekStart ? [...dayjs.weekdaysShort().filter((_,i) => i !== 0), dayjs.weekdaysShort()[0]] : dayjs.weekdaysShort())[dayjs().day()-weekStart === -1 ? 6 : dayjs().day()-weekStart] === name}
|
||||
title={(weekStart ? [...dayjs.weekdaysShort().filter((_,i) => i !== 0), dayjs.weekdaysShort()[0]] : dayjs.weekdaysShort())[dayjs().day()-weekStart === -1 ? 6 : dayjs().day()-weekStart] === name ? t('form.dates.tooltips.today') : ''}
|
||||
selected={selectedDays.includes(((i + weekStart) % 7 + 7) % 7)}
|
||||
selecting={selectingDays.includes(((i + weekStart) % 7 + 7) % 7)}
|
||||
mode={mode}
|
||||
$selected={selectedDays.includes(((i + weekStart) % 7 + 7) % 7)}
|
||||
$selecting={selectingDays.includes(((i + weekStart) % 7 + 7) % 7)}
|
||||
$mode={mode}
|
||||
type="button"
|
||||
onKeyPress={e => {
|
||||
if (e.key === ' ' || e.key === 'Enter') {
|
||||
if (selectedDays.includes(((i + weekStart) % 7 + 7) % 7)) {
|
||||
setSelectedDays(selectedDays.filter(d => d !== ((i + weekStart) % 7 + 7) % 7));
|
||||
setSelectedDays(selectedDays.filter(d => d !== ((i + weekStart) % 7 + 7) % 7))
|
||||
} else {
|
||||
setSelectedDays([...selectedDays, ((i + weekStart) % 7 + 7) % 7]);
|
||||
setSelectedDays([...selectedDays, ((i + weekStart) % 7 + 7) % 7])
|
||||
}
|
||||
}
|
||||
}}
|
||||
onPointerDown={(e) => {
|
||||
startPos.current = i;
|
||||
setMode(selectedDays.includes(((i + weekStart) % 7 + 7) % 7) ? 'remove' : 'add');
|
||||
setSelectingDays([((i + weekStart) % 7 + 7) % 7]);
|
||||
e.currentTarget.releasePointerCapture(e.pointerId);
|
||||
onPointerDown={e => {
|
||||
startPos.current = i
|
||||
setMode(selectedDays.includes(((i + weekStart) % 7 + 7) % 7) ? 'remove' : 'add')
|
||||
setSelectingDays([((i + weekStart) % 7 + 7) % 7])
|
||||
e.currentTarget.releasePointerCapture(e.pointerId)
|
||||
|
||||
document.addEventListener('pointerup', () => {
|
||||
if (staticMode.current === 'add') {
|
||||
setSelectedDays([...selectedDays, ...staticSelectingDays.current]);
|
||||
setSelectedDays([...selectedDays, ...staticSelectingDays.current])
|
||||
} else if (staticMode.current === 'remove') {
|
||||
const toRemove = staticSelectingDays.current;
|
||||
setSelectedDays(selectedDays.filter(d => !toRemove.includes(d)));
|
||||
const toRemove = staticSelectingDays.current
|
||||
setSelectedDays(selectedDays.filter(d => !toRemove.includes(d)))
|
||||
}
|
||||
setMode(null);
|
||||
}, { once: true });
|
||||
setMode(null)
|
||||
}, { once: true })
|
||||
}}
|
||||
onPointerEnter={() => {
|
||||
if (staticMode.current) {
|
||||
let found = [];
|
||||
const found = []
|
||||
for (let ci = Math.min(startPos.current, i); ci < Math.max(startPos.current, i)+1; ci++) {
|
||||
found.push(((ci + weekStart) % 7 + 7) % 7);
|
||||
found.push(((ci + weekStart) % 7 + 7) % 7)
|
||||
}
|
||||
setSelectingDays(found);
|
||||
setSelectingDays(found)
|
||||
}
|
||||
}}
|
||||
>{name}</Date>
|
||||
|
|
@ -258,7 +258,7 @@ const CalendarField = forwardRef(({
|
|||
</CalendarBody>
|
||||
)}
|
||||
</Wrapper>
|
||||
);
|
||||
});
|
||||
)
|
||||
})
|
||||
|
||||
export default CalendarField;
|
||||
export default CalendarField
|
||||
|
|
@ -1,22 +1,22 @@
|
|||
import styled from '@emotion/styled';
|
||||
import { styled } from 'goober'
|
||||
|
||||
export const Wrapper = styled.div`
|
||||
export const Wrapper = styled('div')`
|
||||
margin: 30px 0;
|
||||
`;
|
||||
`
|
||||
|
||||
export const StyledLabel = styled.label`
|
||||
export const StyledLabel = styled('label')`
|
||||
display: block;
|
||||
padding-bottom: 4px;
|
||||
font-size: 18px;
|
||||
`;
|
||||
`
|
||||
|
||||
export const StyledSubLabel = styled.label`
|
||||
export const StyledSubLabel = styled('label')`
|
||||
display: block;
|
||||
font-size: 13px;
|
||||
opacity: .6;
|
||||
`;
|
||||
`
|
||||
|
||||
export const CalendarHeader = styled.div`
|
||||
export const CalendarHeader = styled('div')`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
|
@ -24,15 +24,15 @@ export const CalendarHeader = styled.div`
|
|||
padding: 6px 0;
|
||||
font-size: 1.2em;
|
||||
font-weight: bold;
|
||||
`;
|
||||
`
|
||||
|
||||
export const CalendarDays = styled.div`
|
||||
export const CalendarDays = styled('div')`
|
||||
display: grid;
|
||||
grid-template-columns: repeat(7, 1fr);
|
||||
grid-gap: 2px;
|
||||
`;
|
||||
`
|
||||
|
||||
export const Day = styled.div`
|
||||
export const Day = styled('div')`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
|
@ -44,9 +44,9 @@ export const Day = styled.div`
|
|||
@media (max-width: 350px) {
|
||||
font-size: 12px;
|
||||
}
|
||||
`;
|
||||
`
|
||||
|
||||
export const CalendarBody = styled.div`
|
||||
export const CalendarBody = styled('div')`
|
||||
display: grid;
|
||||
grid-template-columns: repeat(7, 1fr);
|
||||
grid-gap: 2px;
|
||||
|
|
@ -63,9 +63,9 @@ export const CalendarBody = styled.div`
|
|||
& button:last-of-type {
|
||||
border-bottom-right-radius: 3px;
|
||||
}
|
||||
`;
|
||||
`
|
||||
|
||||
export const Date = styled.button`
|
||||
export const Date = styled('button')`
|
||||
font: inherit;
|
||||
color: inherit;
|
||||
background: none;
|
||||
|
|
@ -77,8 +77,8 @@ export const Date = styled.button`
|
|||
transition: none;
|
||||
}
|
||||
|
||||
background-color: ${props => props.theme.primaryBackground};
|
||||
border: 1px solid ${props => props.theme.primary};
|
||||
background-color: var(--surface);
|
||||
border: 1px solid var(--primary);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
|
@ -86,19 +86,19 @@ export const Date = styled.button`
|
|||
user-select: none;
|
||||
touch-action: none;
|
||||
|
||||
${props => props.otherMonth && `
|
||||
color: ${props.theme.mode === 'light' ? props.theme.primaryLight : props.theme.primaryDark};
|
||||
${props => props.$otherMonth && `
|
||||
color: var(--tertiary);
|
||||
`}
|
||||
${props => props.isToday && `
|
||||
${props => props.$isToday && `
|
||||
font-weight: 900;
|
||||
color: ${props.theme.mode === 'light' ? props.theme.primaryDark : props.theme.primaryLight};
|
||||
color: var(--secondary);
|
||||
`}
|
||||
${props => (props.selected || (props.mode === 'add' && props.selecting)) && `
|
||||
color: ${props.otherMonth ? 'rgba(255,255,255,.5)' : '#FFF'};
|
||||
background-color: ${props.theme.primary};
|
||||
${props => (props.$selected || (props.$mode === 'add' && props.$selecting)) && `
|
||||
color: ${props.$otherMonth ? 'rgba(255,255,255,.5)' : '#FFF'};
|
||||
background-color: var(--primary);
|
||||
`}
|
||||
${props => props.mode === 'remove' && props.selecting && `
|
||||
background-color: ${props.theme.primaryBackground};
|
||||
color: ${props.isToday ? props.theme.primaryDark : (props.otherMonth ? props.theme.primaryLight : 'inherit')};
|
||||
${props => props.$mode === 'remove' && props.$selecting && `
|
||||
background-color: var(--surface);
|
||||
color: ${props.$isToday ? 'var(--secondary)' : (props.$otherMonth ? 'var(--tertiary)' : 'inherit')};
|
||||
`}
|
||||
`;
|
||||
`
|
||||
9
crabfit-frontend/src/components/Center/Center.js
Normal file
9
crabfit-frontend/src/components/Center/Center.js
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
import { styled } from 'goober'
|
||||
|
||||
const Center = styled('div')`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
`
|
||||
|
||||
export default Center
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
import styled from '@emotion/styled';
|
||||
|
||||
const Center = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
`;
|
||||
|
||||
export default Center;
|
||||
|
|
@ -1,106 +1,107 @@
|
|||
import { useState, useEffect, useRef } from 'react';
|
||||
import { Button } from 'components';
|
||||
import { useTWAStore } from 'stores';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useState, useEffect, useRef } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import { Button } from '/src/components'
|
||||
import { useTWAStore } from '/src/stores'
|
||||
|
||||
import {
|
||||
Wrapper,
|
||||
Options,
|
||||
} from './donateStyle';
|
||||
} from './Donate.styles'
|
||||
|
||||
import paypal_logo from 'res/paypal.svg';
|
||||
import paypal_logo from '/src/res/paypal.svg'
|
||||
|
||||
const PAYMENT_METHOD = 'https://play.google.com/billing';
|
||||
const SKU = 'crab_donation';
|
||||
const PAYMENT_METHOD = 'https://play.google.com/billing'
|
||||
const SKU = 'crab_donation'
|
||||
|
||||
const Donate = () => {
|
||||
const store = useTWAStore();
|
||||
const { t } = useTranslation('common');
|
||||
const store = useTWAStore()
|
||||
const { t } = useTranslation('common')
|
||||
|
||||
const firstLinkRef = useRef();
|
||||
const modalRef = useRef();
|
||||
const [isOpen, _setIsOpen] = useState(false);
|
||||
const [closed, setClosed] = useState(false);
|
||||
const firstLinkRef = useRef()
|
||||
const modalRef = useRef()
|
||||
const [isOpen, _setIsOpen] = useState(false)
|
||||
const [closed, setClosed] = useState(false)
|
||||
|
||||
const setIsOpen = open => {
|
||||
_setIsOpen(open);
|
||||
_setIsOpen(open)
|
||||
|
||||
if (open) {
|
||||
window.setTimeout(() => firstLinkRef.current.focus(), 150);
|
||||
window.setTimeout(() => firstLinkRef.current.focus(), 150)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const linkPressed = () => {
|
||||
setIsOpen(false);
|
||||
gtag('event', 'donate', { 'event_category': 'donate' });
|
||||
};
|
||||
setIsOpen(false)
|
||||
gtag('event', 'donate', { 'event_category': 'donate' })
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (store.TWA === undefined) {
|
||||
store.setTWA(document.referrer.includes('android-app://fit.crab'));
|
||||
store.setTWA(document.referrer.includes('android-app://fit.crab'))
|
||||
}
|
||||
}, [store]);
|
||||
}, [store])
|
||||
|
||||
const acknowledge = async (token, type='repeatable', onComplete = () => {}) => {
|
||||
try {
|
||||
let service = await window.getDigitalGoodsService(PAYMENT_METHOD);
|
||||
await service.acknowledge(token, type);
|
||||
const service = await window.getDigitalGoodsService(PAYMENT_METHOD)
|
||||
await service.acknowledge(token, type)
|
||||
if ('acknowledge' in service) {
|
||||
// DGAPI 1.0
|
||||
service.acknowledge(token, type);
|
||||
service.acknowledge(token, type)
|
||||
} else {
|
||||
// DGAPI 2.0
|
||||
service.consume(token);
|
||||
service.consume(token)
|
||||
}
|
||||
onComplete();
|
||||
onComplete()
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
const purchase = () => {
|
||||
if (!window.PaymentRequest) return false;
|
||||
if (!window.getDigitalGoodsService) return false;
|
||||
if (!window.PaymentRequest) return false
|
||||
if (!window.getDigitalGoodsService) return false
|
||||
|
||||
const supportedInstruments = [{
|
||||
supportedMethods: PAYMENT_METHOD,
|
||||
data: {
|
||||
sku: SKU
|
||||
}
|
||||
}];
|
||||
}]
|
||||
|
||||
const details = {
|
||||
total: {
|
||||
label: 'Total',
|
||||
amount: { currency: 'AUD', value: '0' }
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const request = new PaymentRequest(supportedInstruments, details);
|
||||
const request = new PaymentRequest(supportedInstruments, details)
|
||||
|
||||
request.show()
|
||||
.then(response => {
|
||||
response
|
||||
.complete('success')
|
||||
.then(() => {
|
||||
console.log(`Payment done: ${JSON.stringify(response, undefined, 2)}`);
|
||||
console.log(`Payment done: ${JSON.stringify(response, undefined, 2)}`)
|
||||
if (response.details && response.details.token) {
|
||||
const token = response.details.token;
|
||||
console.log(`Read Token: ${token.substring(0, 6)}...`);
|
||||
alert(t('donate.messages.success'));
|
||||
acknowledge(token);
|
||||
const token = response.details.token
|
||||
console.log(`Read Token: ${token.substring(0, 6)}...`)
|
||||
alert(t('donate.messages.success'))
|
||||
acknowledge(token)
|
||||
}
|
||||
})
|
||||
.catch(e => {
|
||||
console.error(e.message);
|
||||
alert(t('donate.messages.error'));
|
||||
});
|
||||
console.error(e.message)
|
||||
alert(t('donate.messages.error'))
|
||||
})
|
||||
})
|
||||
.catch(e => {
|
||||
console.error(e);
|
||||
alert(t('donate.messages.error'));
|
||||
});
|
||||
};
|
||||
console.error(e)
|
||||
alert(t('donate.messages.error'))
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<Wrapper>
|
||||
|
|
@ -109,20 +110,20 @@ const Donate = () => {
|
|||
title={t('donate.title')}
|
||||
onClick={event => {
|
||||
if (closed) {
|
||||
event.preventDefault();
|
||||
return setClosed(false);
|
||||
event.preventDefault()
|
||||
return setClosed(false)
|
||||
}
|
||||
if (store.TWA) {
|
||||
gtag('event', 'donate', { 'event_category': 'donate' });
|
||||
event.preventDefault();
|
||||
gtag('event', 'donate', { 'event_category': 'donate' })
|
||||
event.preventDefault()
|
||||
if (window.confirm(t('donate.messages.about'))) {
|
||||
if (purchase() === false) {
|
||||
alert(t('donate.messages.error'));
|
||||
alert(t('donate.messages.error'))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
event.preventDefault();
|
||||
setIsOpen(true);
|
||||
event.preventDefault()
|
||||
setIsOpen(true)
|
||||
}
|
||||
}}
|
||||
href="https://www.paypal.com/donate?business=N89X6YXRT5HKW&item_name=Crab+Fit+Donation¤cy_code=AUD&amount=5"
|
||||
|
|
@ -135,13 +136,13 @@ const Donate = () => {
|
|||
>{t('donate.button')}</Button>
|
||||
|
||||
<Options
|
||||
isOpen={isOpen}
|
||||
$isOpen={isOpen}
|
||||
ref={modalRef}
|
||||
onBlur={e => {
|
||||
if (modalRef.current.contains(e.relatedTarget)) return;
|
||||
setIsOpen(false);
|
||||
if (modalRef.current?.contains(e.relatedTarget)) return
|
||||
setIsOpen(false)
|
||||
if (e.relatedTarget && e.relatedTarget.id === 'donate_button') {
|
||||
setClosed(true);
|
||||
setClosed(true)
|
||||
}
|
||||
}}
|
||||
>
|
||||
|
|
@ -152,7 +153,7 @@ const Donate = () => {
|
|||
<a onClick={linkPressed} href="https://www.paypal.com/donate?business=N89X6YXRT5HKW&item_name=Crab+Fit+Donation¤cy_code=AUD" target="_blank" rel="noreferrer noopener payment">{t('donate.options.choose')}</a>
|
||||
</Options>
|
||||
</Wrapper>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
export default Donate;
|
||||
export default Donate
|
||||
|
|
@ -1,19 +1,18 @@
|
|||
import styled from '@emotion/styled';
|
||||
import { styled } from 'goober'
|
||||
import { forwardRef } from 'react'
|
||||
|
||||
export const Wrapper = styled.div`
|
||||
export const Wrapper = styled('div')`
|
||||
margin-top: 6px;
|
||||
margin-left: 12px;
|
||||
position: relative;
|
||||
`;
|
||||
`
|
||||
|
||||
export const Options = styled.div`
|
||||
export const Options = styled('div', forwardRef)`
|
||||
position: absolute;
|
||||
bottom: calc(100% + 20px);
|
||||
right: 0;
|
||||
background-color: ${props => props.theme.background};
|
||||
${props => props.theme.mode === 'dark' && `
|
||||
border: 1px solid ${props.theme.primaryBackground};
|
||||
`}
|
||||
background-color: var(--background);
|
||||
border: 1px solid var(--surface);
|
||||
z-index: 60;
|
||||
padding: 4px 10px;
|
||||
border-radius: 14px;
|
||||
|
|
@ -27,7 +26,7 @@ export const Options = styled.div`
|
|||
transform: translateY(5px);
|
||||
transition: opacity .15s, transform .15s, visibility .15s;
|
||||
|
||||
${props => props.isOpen && `
|
||||
${props => props.$isOpen && `
|
||||
pointer-events: all;
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
|
|
@ -48,8 +47,8 @@ export const Options = styled.div`
|
|||
margin: 6px 0;
|
||||
text-decoration: none;
|
||||
border-radius: 100px;
|
||||
background-color: ${props => props.theme.primary};
|
||||
color: ${props => props.theme.background};
|
||||
background-color: var(--primary);
|
||||
color: var(--background);
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
|
|
@ -62,4 +61,4 @@ export const Options = styled.div`
|
|||
@media (prefers-reduced-motion: reduce) {
|
||||
transition: none;
|
||||
}
|
||||
`;
|
||||
`
|
||||
|
|
@ -1,10 +1,10 @@
|
|||
import { useState } from 'react';
|
||||
import { useState } from 'react'
|
||||
|
||||
import { Loading } from 'components';
|
||||
import { Image, Wrapper } from './eggStyle';
|
||||
import { Loading } from '/src/components'
|
||||
import { Image, Wrapper } from './Egg.styles'
|
||||
|
||||
const Egg = ({ eggKey, onClose }) => {
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
|
||||
return (
|
||||
<Wrapper title="Click anywhere to close" onClick={() => onClose()}>
|
||||
|
|
@ -15,7 +15,7 @@ const Egg = ({ eggKey, onClose }) => {
|
|||
/>
|
||||
{isLoading && <Loading />}
|
||||
</Wrapper>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
export default Egg;
|
||||
export default Egg
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import styled from '@emotion/styled';
|
||||
import { styled } from 'goober'
|
||||
|
||||
export const Wrapper = styled.div`
|
||||
export const Wrapper = styled('div')`
|
||||
position: fixed;
|
||||
background: rgba(0,0,0,.6);
|
||||
top: 0;
|
||||
|
|
@ -14,10 +14,10 @@ export const Wrapper = styled.div`
|
|||
align-items: center;
|
||||
z-index: 1000;
|
||||
cursor: pointer;
|
||||
`;
|
||||
`
|
||||
|
||||
export const Image = styled.img`
|
||||
export const Image = styled('img')`
|
||||
max-width: 80%;
|
||||
max-height: 80%;
|
||||
position: absolute;
|
||||
`;
|
||||
`
|
||||
17
crabfit-frontend/src/components/Error/Error.jsx
Normal file
17
crabfit-frontend/src/components/Error/Error.jsx
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
import { X } from 'lucide-react'
|
||||
|
||||
import { Wrapper, CloseButton } from './Error.styles'
|
||||
|
||||
const Error = ({
|
||||
children,
|
||||
onClose,
|
||||
open = true,
|
||||
...props
|
||||
}) => (
|
||||
<Wrapper role="alert" open={open} {...props}>
|
||||
{children}
|
||||
<CloseButton type="button" onClick={onClose} title="Close error"><X /></CloseButton>
|
||||
</Wrapper>
|
||||
)
|
||||
|
||||
export default Error
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
import styled from '@emotion/styled';
|
||||
import { styled } from 'goober'
|
||||
|
||||
export const Wrapper = styled.div`
|
||||
export const Wrapper = styled('div')`
|
||||
border-radius: 3px;
|
||||
background-color: ${props => props.theme.error};
|
||||
background-color: var(--error);
|
||||
color: #FFFFFF;
|
||||
padding: 0 16px;
|
||||
display: flex;
|
||||
|
|
@ -27,9 +27,9 @@ export const Wrapper = styled.div`
|
|||
@media (prefers-reduced-motion: reduce) {
|
||||
transition: none;
|
||||
}
|
||||
`;
|
||||
`
|
||||
|
||||
export const CloseButton = styled.button`
|
||||
export const CloseButton = styled('button')`
|
||||
border: 0;
|
||||
background: none;
|
||||
height: 30px;
|
||||
|
|
@ -40,4 +40,5 @@ export const CloseButton = styled.button`
|
|||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-left: 16px;
|
||||
`;
|
||||
padding: 0;
|
||||
`
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
import { Wrapper, CloseButton } from './errorStyle';
|
||||
|
||||
const Error = ({
|
||||
children,
|
||||
onClose,
|
||||
open = true,
|
||||
...props
|
||||
}) => (
|
||||
<Wrapper role="alert" open={open} {...props}>
|
||||
{children}
|
||||
<CloseButton type="button" onClick={onClose} title="Close error">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>
|
||||
</CloseButton>
|
||||
</Wrapper>
|
||||
);
|
||||
|
||||
export default Error;
|
||||
17
crabfit-frontend/src/components/Footer/Footer.jsx
Normal file
17
crabfit-frontend/src/components/Footer/Footer.jsx
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import { Donate } from '/src/components'
|
||||
import { Wrapper } from './Footer.styles'
|
||||
|
||||
const Footer = props => {
|
||||
const { t } = useTranslation('common')
|
||||
|
||||
return (
|
||||
<Wrapper id="donate" {...props}>
|
||||
<span>{t('donate.info')}</span>
|
||||
<Donate />
|
||||
</Wrapper>
|
||||
)
|
||||
}
|
||||
|
||||
export default Footer
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import styled from '@emotion/styled';
|
||||
import { styled } from 'goober'
|
||||
|
||||
export const Wrapper = styled.footer`
|
||||
export const Wrapper = styled('footer')`
|
||||
width: 600px;
|
||||
margin: 20px auto;
|
||||
max-width: calc(100% - 60px);
|
||||
|
|
@ -23,4 +23,4 @@ export const Wrapper = styled.footer`
|
|||
@media print {
|
||||
display: none;
|
||||
}
|
||||
`;
|
||||
`
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { Donate } from 'components';
|
||||
import { Wrapper } from './footerStyle';
|
||||
|
||||
const Footer = (props) => {
|
||||
const { t } = useTranslation('common');
|
||||
|
||||
return (
|
||||
<Wrapper id="donate" {...props}>
|
||||
<span>{t('donate.info')}</span>
|
||||
<Donate />
|
||||
</Wrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default Footer;
|
||||
|
|
@ -1,9 +1,9 @@
|
|||
import { useState, useEffect } from 'react';
|
||||
import { loadGapiInsideDOM } from 'gapi-script';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useState, useEffect } from 'react'
|
||||
import { loadGapiInsideDOM } from 'gapi-script'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import { Button, Center } from 'components';
|
||||
import { Loader } from '../Loading/loadingStyle';
|
||||
import { Button, Center } from '/src/components'
|
||||
import { Loader } from '../Loading/Loading.styles'
|
||||
import {
|
||||
CalendarList,
|
||||
CheckboxInput,
|
||||
|
|
@ -14,22 +14,22 @@ import {
|
|||
Title,
|
||||
Icon,
|
||||
LinkButton,
|
||||
} from './googleCalendarStyle';
|
||||
} from './GoogleCalendar.styles'
|
||||
|
||||
import googleLogo from 'res/google.svg';
|
||||
import googleLogo from '/src/res/google.svg'
|
||||
|
||||
const signIn = () => window.gapi.auth2.getAuthInstance().signIn();
|
||||
const signIn = () => window.gapi.auth2.getAuthInstance().signIn()
|
||||
|
||||
const signOut = () => window.gapi.auth2.getAuthInstance().signOut();
|
||||
const signOut = () => window.gapi.auth2.getAuthInstance().signOut()
|
||||
|
||||
const GoogleCalendar = ({ timeZone, timeMin, timeMax, onImport }) => {
|
||||
const [signedIn, setSignedIn] = useState(undefined);
|
||||
const [calendars, setCalendars] = useState(undefined);
|
||||
const [freeBusyLoading, setFreeBusyLoading] = useState(false);
|
||||
const { t } = useTranslation('event');
|
||||
const [signedIn, setSignedIn] = useState(undefined)
|
||||
const [calendars, setCalendars] = useState(undefined)
|
||||
const [freeBusyLoading, setFreeBusyLoading] = useState(false)
|
||||
const { t } = useTranslation('event')
|
||||
|
||||
const calendarLogin = async () => {
|
||||
const gapi = await loadGapiInsideDOM();
|
||||
const gapi = await loadGapiInsideDOM()
|
||||
gapi.load('client:auth2', () => {
|
||||
window.gapi.client.init({
|
||||
clientId: '276505195333-9kjl7e48m272dljbspkobctqrpet0n8m.apps.googleusercontent.com',
|
||||
|
|
@ -38,23 +38,23 @@ const GoogleCalendar = ({ timeZone, timeMin, timeMax, onImport }) => {
|
|||
})
|
||||
.then(() => {
|
||||
// Listen for state changes
|
||||
window.gapi.auth2.getAuthInstance().isSignedIn.listen(isSignedIn => setSignedIn(isSignedIn));
|
||||
window.gapi.auth2.getAuthInstance().isSignedIn.listen(isSignedIn => setSignedIn(isSignedIn))
|
||||
|
||||
// Handle initial sign-in state
|
||||
setSignedIn(window.gapi.auth2.getAuthInstance().isSignedIn.get());
|
||||
setSignedIn(window.gapi.auth2.getAuthInstance().isSignedIn.get())
|
||||
})
|
||||
.catch(e => {
|
||||
console.error(e);
|
||||
setSignedIn(false);
|
||||
});
|
||||
});
|
||||
};
|
||||
console.error(e)
|
||||
setSignedIn(false)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const importAvailability = () => {
|
||||
setFreeBusyLoading(true);
|
||||
setFreeBusyLoading(true)
|
||||
gtag('event', 'google_cal_sync', {
|
||||
'event_category': 'event',
|
||||
});
|
||||
})
|
||||
window.gapi.client.calendar.freebusy.query({
|
||||
timeMin,
|
||||
timeMax,
|
||||
|
|
@ -62,15 +62,15 @@ const GoogleCalendar = ({ timeZone, timeMin, timeMax, onImport }) => {
|
|||
items: calendars.filter(c => c.checked).map(c => ({id: c.id})),
|
||||
})
|
||||
.then(response => {
|
||||
onImport(response.result.calendars ? Object.values(response.result.calendars).reduce((busy, c) => [...busy, ...c.busy], []) : []);
|
||||
setFreeBusyLoading(false);
|
||||
onImport(response.result.calendars ? Object.values(response.result.calendars).reduce((busy, c) => [...busy, ...c.busy], []) : [])
|
||||
setFreeBusyLoading(false)
|
||||
}, e => {
|
||||
console.error(e);
|
||||
setFreeBusyLoading(false);
|
||||
});
|
||||
};
|
||||
console.error(e)
|
||||
setFreeBusyLoading(false)
|
||||
})
|
||||
}
|
||||
|
||||
useEffect(() => calendarLogin(), []);
|
||||
useEffect(() => void calendarLogin(), [])
|
||||
|
||||
useEffect(() => {
|
||||
if (signedIn) {
|
||||
|
|
@ -83,15 +83,15 @@ const GoogleCalendar = ({ timeZone, timeMin, timeMax, onImport }) => {
|
|||
'description': item.description,
|
||||
'id': item.id,
|
||||
'color': item.backgroundColor,
|
||||
'checked': item.hasOwnProperty('primary') && item.primary === true,
|
||||
})));
|
||||
'checked': item.primary === true,
|
||||
})))
|
||||
})
|
||||
.catch(e => {
|
||||
console.error(e);
|
||||
signOut();
|
||||
});
|
||||
console.error(e)
|
||||
signOut()
|
||||
})
|
||||
}
|
||||
}, [signedIn]);
|
||||
}, [signedIn])
|
||||
|
||||
return (
|
||||
<>
|
||||
|
|
@ -113,21 +113,21 @@ const GoogleCalendar = ({ timeZone, timeMin, timeMax, onImport }) => {
|
|||
<Icon src={googleLogo} alt="" />
|
||||
<strong>{t('event:you.google_cal.login')}</strong>
|
||||
(<LinkButton type="button" onClick={e => {
|
||||
e.preventDefault();
|
||||
signOut();
|
||||
e.preventDefault()
|
||||
signOut()
|
||||
}}>{t('event:you.google_cal.logout')}</LinkButton>)
|
||||
</Title>
|
||||
<Options>
|
||||
{calendars !== undefined && !calendars.every(c => c.checked) && (
|
||||
<LinkButton type="button" onClick={e => {
|
||||
e.preventDefault();
|
||||
setCalendars(calendars.map(c => ({...c, checked: true})));
|
||||
e.preventDefault()
|
||||
setCalendars(calendars.map(c => ({...c, checked: true})))
|
||||
}}>{t('event:you.google_cal.select_all')}</LinkButton>
|
||||
)}
|
||||
{calendars !== undefined && calendars.every(c => c.checked) && (
|
||||
<LinkButton type="button" onClick={e => {
|
||||
e.preventDefault();
|
||||
setCalendars(calendars.map(c => ({...c, checked: false})));
|
||||
e.preventDefault()
|
||||
setCalendars(calendars.map(c => ({...c, checked: false})))
|
||||
}}>{t('event:you.google_cal.select_none')}</LinkButton>
|
||||
)}
|
||||
</Options>
|
||||
|
|
@ -139,7 +139,7 @@ const GoogleCalendar = ({ timeZone, timeMin, timeMax, onImport }) => {
|
|||
id={calendar.id}
|
||||
color={calendar.color}
|
||||
checked={calendar.checked}
|
||||
onChange={e => setCalendars(calendars.map(c => c.id === calendar.id ? {...c, checked: !c.checked} : c))}
|
||||
onChange={() => setCalendars(calendars.map(c => c.id === calendar.id ? {...c, checked: !c.checked} : c))}
|
||||
/>
|
||||
<CheckboxLabel htmlFor={calendar.id} color={calendar.color} />
|
||||
<CalendarLabel htmlFor={calendar.id}>{calendar.name}</CalendarLabel>
|
||||
|
|
@ -161,7 +161,7 @@ const GoogleCalendar = ({ timeZone, timeMin, timeMax, onImport }) => {
|
|||
</CalendarList>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
export default GoogleCalendar;
|
||||
export default GoogleCalendar
|
||||
|
|
@ -1,14 +1,14 @@
|
|||
import styled from '@emotion/styled';
|
||||
import { styled } from 'goober'
|
||||
|
||||
export const CalendarList = styled.div`
|
||||
export const CalendarList = styled('div')`
|
||||
width: 100%;
|
||||
& > div {
|
||||
display: flex;
|
||||
margin: 2px 0;
|
||||
}
|
||||
`;
|
||||
`
|
||||
|
||||
export const CheckboxInput = styled.input`
|
||||
export const CheckboxInput = styled('input')`
|
||||
height: 0px;
|
||||
width: 0px;
|
||||
margin: 0;
|
||||
|
|
@ -27,8 +27,8 @@ export const CheckboxInput = styled.input`
|
|||
opacity: .6;
|
||||
}
|
||||
&[disabled] + label:after {
|
||||
border: 2px solid ${props => props.theme.text};
|
||||
background-color: ${props => props.theme.text};
|
||||
border: 2px solid var(--text);
|
||||
background-color: var(--text);
|
||||
}
|
||||
&:focus + label {
|
||||
box-shadow: 0 0 0 2px ${props => props.theme.text}44;
|
||||
|
|
@ -39,9 +39,9 @@ export const CheckboxInput = styled.input`
|
|||
box-shadow: 0 0 0 2px ${props => props.color || props.theme.primary}44;
|
||||
background-color: ${props => props.color || props.theme.primary}44;
|
||||
}
|
||||
`;
|
||||
`
|
||||
|
||||
export const CheckboxLabel = styled.label`
|
||||
export const CheckboxLabel = styled('label')`
|
||||
display: inline-block;
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
|
|
@ -55,7 +55,7 @@ export const CheckboxLabel = styled.label`
|
|||
display: inline-block;
|
||||
height: 14px;
|
||||
width: 14px;
|
||||
border: 2px solid ${props => props.theme.text};
|
||||
border: 2px solid var(--text);
|
||||
border-radius: 2px;
|
||||
position: absolute;
|
||||
top: 3px;
|
||||
|
|
@ -66,8 +66,8 @@ export const CheckboxLabel = styled.label`
|
|||
display: inline-block;
|
||||
height: 14px;
|
||||
width: 14px;
|
||||
border: 2px solid ${props => props.color || props.theme.primary};
|
||||
background-color: ${props => props.color || props.theme.primary};
|
||||
border: 2px solid ${props => props.color || 'var(--primary)'};
|
||||
background-color: ${props => props.color || 'var(--primary)'};
|
||||
border-radius: 2px;
|
||||
position: absolute;
|
||||
top: 3px;
|
||||
|
|
@ -80,37 +80,37 @@ export const CheckboxLabel = styled.label`
|
|||
transform: scale(.5);
|
||||
transition: opacity 0.15s, transform 0.15s;
|
||||
}
|
||||
`;
|
||||
`
|
||||
|
||||
export const CalendarLabel = styled.label`
|
||||
export const CalendarLabel = styled('label')`
|
||||
margin-left: .6em;
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
line-height: 24px;
|
||||
`;
|
||||
`
|
||||
|
||||
export const Info = styled.div`
|
||||
export const Info = styled('div')`
|
||||
font-size: 14px;
|
||||
opacity: .6;
|
||||
font-weight: 500;
|
||||
padding: 14px 0 10px;
|
||||
`;
|
||||
`
|
||||
|
||||
export const Options = styled.div`
|
||||
export const Options = styled('div')`
|
||||
font-size: 14px;
|
||||
padding: 0 0 5px;
|
||||
`;
|
||||
`
|
||||
|
||||
export const Title = styled.p`
|
||||
export const Title = styled('p')`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
& strong {
|
||||
margin-right: 1ex;
|
||||
}
|
||||
`;
|
||||
`
|
||||
|
||||
export const Icon = styled.img`
|
||||
export const Icon = styled('img')`
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
margin-right: 12px;
|
||||
|
|
@ -118,11 +118,11 @@ export const Icon = styled.img`
|
|||
${props => props.theme.mode === 'light' && `
|
||||
filter: invert(1);
|
||||
`}
|
||||
`;
|
||||
`
|
||||
|
||||
export const LinkButton = styled.button`
|
||||
export const LinkButton = styled('button')`
|
||||
font: inherit;
|
||||
color: ${props => props.theme.primary};
|
||||
color: var(--primary);
|
||||
border: 0;
|
||||
background: none;
|
||||
text-decoration: underline;
|
||||
|
|
@ -131,4 +131,4 @@ export const LinkButton = styled.button`
|
|||
display: inline;
|
||||
cursor: pointer;
|
||||
appearance: none;
|
||||
`;
|
||||
`
|
||||
|
|
@ -1,25 +1,23 @@
|
|||
import { useTheme } from '@emotion/react';
|
||||
import { useSettingsStore } from 'stores';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import { useSettingsStore } from '/src/stores'
|
||||
|
||||
import {
|
||||
Wrapper,
|
||||
Label,
|
||||
Bar,
|
||||
Grade,
|
||||
} from './legendStyle';
|
||||
} from './Legend.styles'
|
||||
|
||||
const Legend = ({
|
||||
min,
|
||||
max,
|
||||
total,
|
||||
onSegmentFocus,
|
||||
...props
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation('event');
|
||||
const highlight = useSettingsStore(state => state.highlight);
|
||||
const setHighlight = useSettingsStore(state => state.setHighlight);
|
||||
const { t } = useTranslation('event')
|
||||
const highlight = useSettingsStore(state => state.highlight)
|
||||
const setHighlight = useSettingsStore(state => state.setHighlight)
|
||||
|
||||
return (
|
||||
<Wrapper>
|
||||
|
|
@ -33,8 +31,8 @@ const Legend = ({
|
|||
{[...Array(max+1-min).keys()].map(i => i+min).map(i =>
|
||||
<Grade
|
||||
key={i}
|
||||
color={`${theme.primary}${Math.round((i/(max))*255).toString(16)}`}
|
||||
highlight={highlight && i === max && max > 0}
|
||||
$color={`#F79E00${Math.round((i/(max))*255).toString(16)}`}
|
||||
$highlight={highlight && i === max && max > 0}
|
||||
onMouseOver={() => onSegmentFocus(i)}
|
||||
/>
|
||||
)}
|
||||
|
|
@ -42,7 +40,7 @@ const Legend = ({
|
|||
|
||||
<Label>{max}/{total} {t('event:available')}</Label>
|
||||
</Wrapper>
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
export default Legend;
|
||||
export default Legend
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import styled from '@emotion/styled';
|
||||
import { styled } from 'goober'
|
||||
|
||||
export const Wrapper = styled.div`
|
||||
export const Wrapper = styled('div')`
|
||||
margin: 10px 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
|
@ -13,40 +13,40 @@ export const Wrapper = styled.div`
|
|||
@media (max-width: 400px) {
|
||||
display: block;
|
||||
}
|
||||
`;
|
||||
`
|
||||
|
||||
export const Label = styled.label`
|
||||
export const Label = styled('label')`
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
text-align: left;
|
||||
`;
|
||||
`
|
||||
|
||||
export const Bar = styled.div`
|
||||
export const Bar = styled('div')`
|
||||
display: flex;
|
||||
width: 40%;
|
||||
height: 20px;
|
||||
border-radius: 3px;
|
||||
overflow: hidden;
|
||||
margin: 0 8px;
|
||||
border: 1px solid ${props => props.theme.text};
|
||||
border: 1px solid var(--text);
|
||||
|
||||
@media (max-width: 400px) {
|
||||
width: 100%;
|
||||
margin: 8px 0;
|
||||
}
|
||||
`;
|
||||
`
|
||||
|
||||
export const Grade = styled.div`
|
||||
export const Grade = styled('div')`
|
||||
flex: 1;
|
||||
background-color: ${props => props.color};
|
||||
background-color: ${props => props.$color};
|
||||
|
||||
${props => props.highlight && `
|
||||
${props => props.$highlight && `
|
||||
background-image: repeating-linear-gradient(
|
||||
45deg,
|
||||
${props.theme.primary},
|
||||
${props.theme.primary} 4.5px,
|
||||
${props.theme.primaryDark} 4.5px,
|
||||
${props.theme.primaryDark} 9px
|
||||
var(--primary),
|
||||
var(--primary) 4.5px,
|
||||
var(--shadow) 4.5px,
|
||||
var(--shadow) 9px
|
||||
);
|
||||
`}
|
||||
`;
|
||||
`
|
||||
5
crabfit-frontend/src/components/Loading/Loading.jsx
Normal file
5
crabfit-frontend/src/components/Loading/Loading.jsx
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import { Wrapper, Loader } from './Loading.styles'
|
||||
|
||||
const Loading = () => <Wrapper><Loader /></Wrapper>
|
||||
|
||||
export default Loading
|
||||
|
|
@ -1,13 +1,13 @@
|
|||
import styled from '@emotion/styled';
|
||||
import { styled } from 'goober'
|
||||
|
||||
export const Wrapper = styled.main`
|
||||
export const Wrapper = styled('main')`
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 100vh;
|
||||
`;
|
||||
`
|
||||
|
||||
export const Loader = styled.div`
|
||||
export const Loader = styled('div')`
|
||||
@keyframes load {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
|
|
@ -19,7 +19,7 @@ export const Loader = styled.div`
|
|||
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
border: 3px solid ${props => props.theme.primary};
|
||||
border: 3px solid var(--primary);
|
||||
border-left-color: transparent;
|
||||
border-radius: 100px;
|
||||
animation: load .5s linear infinite;
|
||||
|
|
@ -32,4 +32,4 @@ export const Loader = styled.div`
|
|||
content: 'loading...';
|
||||
}
|
||||
}
|
||||
`;
|
||||
`
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
import {
|
||||
Wrapper,
|
||||
Loader,
|
||||
} from './loadingStyle';
|
||||
|
||||
const Loading = () => (
|
||||
<Wrapper>
|
||||
<Loader />
|
||||
</Wrapper>
|
||||
);
|
||||
|
||||
export default Loading;
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import { useTranslation } from 'react-i18next';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Link } from 'react-router-dom'
|
||||
|
||||
import {
|
||||
Wrapper,
|
||||
|
|
@ -8,12 +8,12 @@ import {
|
|||
Image,
|
||||
Title,
|
||||
Tagline,
|
||||
} from './logoStyle';
|
||||
} from './Logo.styles'
|
||||
|
||||
import image from 'res/logo.svg';
|
||||
import image from '/src/res/logo.svg'
|
||||
|
||||
const Logo = () => {
|
||||
const { t } = useTranslation('common');
|
||||
const { t } = useTranslation('common')
|
||||
|
||||
return (
|
||||
<Wrapper>
|
||||
|
|
@ -25,7 +25,7 @@ const Logo = () => {
|
|||
<Tagline>{t('common:tagline')}</Tagline>
|
||||
</A>
|
||||
</Wrapper>
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
export default Logo;
|
||||
export default Logo
|
||||
|
|
@ -1,12 +1,12 @@
|
|||
import styled from '@emotion/styled';
|
||||
import { styled } from 'goober'
|
||||
|
||||
export const Wrapper = styled.div`
|
||||
export const Wrapper = styled('div')`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
`;
|
||||
`
|
||||
|
||||
export const A = styled.a`
|
||||
export const A = styled('a')`
|
||||
text-decoration: none;
|
||||
|
||||
@keyframes jelly {
|
||||
|
|
@ -32,30 +32,30 @@ export const A = styled.a`
|
|||
animation: none;
|
||||
}
|
||||
}
|
||||
`;
|
||||
`
|
||||
|
||||
export const Top = styled.div`
|
||||
export const Top = styled('div')`
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
`;
|
||||
`
|
||||
|
||||
export const Image = styled.img`
|
||||
export const Image = styled('img')`
|
||||
width: 2.5rem;
|
||||
margin-right: 16px;
|
||||
`;
|
||||
`
|
||||
|
||||
export const Title = styled.span`
|
||||
export const Title = styled('span')`
|
||||
display: block;
|
||||
font-size: 2rem;
|
||||
color: ${props => props.theme.primary};
|
||||
color: var(--primary);
|
||||
font-family: 'Molot', sans-serif;
|
||||
font-weight: 400;
|
||||
text-shadow: 0 2px 0 ${props => props.theme.primaryDark};
|
||||
text-shadow: 0 2px 0 var(--shadow);
|
||||
line-height: 1em;
|
||||
`;
|
||||
`
|
||||
|
||||
export const Tagline = styled.span`
|
||||
export const Tagline = styled('span')`
|
||||
text-decoration: underline;
|
||||
font-size: 14px;
|
||||
padding-top: 2px;
|
||||
|
|
@ -66,4 +66,4 @@ export const Tagline = styled.span`
|
|||
@media print {
|
||||
display: none;
|
||||
}
|
||||
`;
|
||||
`
|
||||
|
|
@ -1,10 +1,10 @@
|
|||
import { useState, useEffect } from 'react';
|
||||
import { PublicClientApplication } from "@azure/msal-browser";
|
||||
import { Client } from "@microsoft/microsoft-graph-client";
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useState, useEffect } from 'react'
|
||||
import { PublicClientApplication } from '@azure/msal-browser'
|
||||
import { Client } from '@microsoft/microsoft-graph-client'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import { Button, Center } from 'components';
|
||||
import { Loader } from '../Loading/loadingStyle';
|
||||
import { Button, Center } from '/src/components'
|
||||
import { Loader } from '../Loading/Loading.styles'
|
||||
import {
|
||||
CalendarList,
|
||||
CheckboxInput,
|
||||
|
|
@ -15,11 +15,11 @@ import {
|
|||
Title,
|
||||
Icon,
|
||||
LinkButton,
|
||||
} from '../GoogleCalendar/googleCalendarStyle';
|
||||
} from '../GoogleCalendar/GoogleCalendar.styles'
|
||||
|
||||
import outlookLogo from 'res/outlook.svg';
|
||||
import outlookLogo from '/src/res/outlook.svg'
|
||||
|
||||
const scopes = ['Calendars.Read', 'Calendars.Read.Shared'];
|
||||
const scopes = ['Calendars.Read', 'Calendars.Read.Shared']
|
||||
|
||||
// Initialise the MSAL object
|
||||
const publicClientApplication = new PublicClientApplication({
|
||||
|
|
@ -31,69 +31,69 @@ const publicClientApplication = new PublicClientApplication({
|
|||
cacheLocation: 'sessionStorage',
|
||||
storeAuthStateInCookie: true,
|
||||
},
|
||||
});
|
||||
})
|
||||
|
||||
const getAuthenticatedClient = accessToken => {
|
||||
const client = Client.init({
|
||||
authProvider: done => done(null, accessToken),
|
||||
});
|
||||
return client;
|
||||
};
|
||||
})
|
||||
return client
|
||||
}
|
||||
|
||||
const OutlookCalendar = ({ timeZone, timeMin, timeMax, onImport }) => {
|
||||
const [client, setClient] = useState(undefined);
|
||||
const [calendars, setCalendars] = useState(undefined);
|
||||
const [freeBusyLoading, setFreeBusyLoading] = useState(false);
|
||||
const { t } = useTranslation('event');
|
||||
const [client, setClient] = useState(undefined)
|
||||
const [calendars, setCalendars] = useState(undefined)
|
||||
const [freeBusyLoading, setFreeBusyLoading] = useState(false)
|
||||
const { t } = useTranslation('event')
|
||||
|
||||
const checkLogin = async () => {
|
||||
const accounts = publicClientApplication.getAllAccounts();
|
||||
const accounts = publicClientApplication.getAllAccounts()
|
||||
if (accounts && accounts.length > 0) {
|
||||
try {
|
||||
const accessToken = await getAccessToken();
|
||||
setClient(getAuthenticatedClient(accessToken));
|
||||
const accessToken = await getAccessToken()
|
||||
setClient(getAuthenticatedClient(accessToken))
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
signOut();
|
||||
console.error(e)
|
||||
signOut()
|
||||
}
|
||||
} else {
|
||||
setClient(null);
|
||||
setClient(null)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const signIn = async () => {
|
||||
try {
|
||||
await publicClientApplication.loginPopup({ scopes });
|
||||
await publicClientApplication.loginPopup({ scopes })
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
console.error(e)
|
||||
} finally {
|
||||
checkLogin();
|
||||
checkLogin()
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const signOut = async () => {
|
||||
try {
|
||||
await publicClientApplication.logoutRedirect({
|
||||
onRedirectNavigate: () => false,
|
||||
});
|
||||
})
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
console.error(e)
|
||||
} finally {
|
||||
checkLogin();
|
||||
checkLogin()
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const getAccessToken = async () => {
|
||||
try {
|
||||
const accounts = publicClientApplication.getAllAccounts();
|
||||
if (accounts.length <= 0) throw new Error('login_required');
|
||||
const accounts = publicClientApplication.getAllAccounts()
|
||||
if (accounts.length <= 0) throw new Error('login_required')
|
||||
|
||||
// Try to get silently
|
||||
const result = await publicClientApplication.acquireTokenSilent({
|
||||
scopes,
|
||||
account: accounts[0],
|
||||
});
|
||||
return result.accessToken;
|
||||
})
|
||||
return result.accessToken
|
||||
} catch (e) {
|
||||
if ([
|
||||
'consent_required',
|
||||
|
|
@ -102,19 +102,19 @@ const OutlookCalendar = ({ timeZone, timeMin, timeMax, onImport }) => {
|
|||
'no_account_in_silent_request'
|
||||
].includes(e.message)) {
|
||||
// Try to get with popup
|
||||
const result = await publicClientApplication.acquireTokenPopup({ scopes });
|
||||
return result.accessToken;
|
||||
const result = await publicClientApplication.acquireTokenPopup({ scopes })
|
||||
return result.accessToken
|
||||
} else {
|
||||
throw e;
|
||||
throw e
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const importAvailability = () => {
|
||||
setFreeBusyLoading(true);
|
||||
setFreeBusyLoading(true)
|
||||
gtag('event', 'outlook_cal_sync', {
|
||||
'event_category': 'event',
|
||||
});
|
||||
})
|
||||
client.api('/me/calendar/getSchedule').post({
|
||||
schedules: calendars.filter(c => c.checked).map(c => c.id),
|
||||
startTime: {
|
||||
|
|
@ -128,17 +128,16 @@ const OutlookCalendar = ({ timeZone, timeMin, timeMax, onImport }) => {
|
|||
availabilityViewInterval: 30,
|
||||
})
|
||||
.then(response => {
|
||||
onImport(response.value.reduce((busy, c) => c.hasOwnProperty('error') ? busy : [...busy, ...c.scheduleItems.filter(item => item.status === 'busy' || item.status === 'tentative')], []));
|
||||
onImport(response.value.reduce((busy, c) => c.error ? busy : [...busy, ...c.scheduleItems.filter(item => item.status === 'busy' || item.status === 'tentative')], []))
|
||||
})
|
||||
.catch(e => {
|
||||
console.error(e);
|
||||
signOut();
|
||||
console.error(e)
|
||||
signOut()
|
||||
})
|
||||
.finally(() => setFreeBusyLoading(false));
|
||||
};
|
||||
.finally(() => setFreeBusyLoading(false))
|
||||
}
|
||||
|
||||
// eslint-disable-next-line
|
||||
useEffect(() => checkLogin(), []);
|
||||
useEffect(() => void checkLogin(), [])
|
||||
|
||||
useEffect(() => {
|
||||
if (client) {
|
||||
|
|
@ -150,15 +149,14 @@ const OutlookCalendar = ({ timeZone, timeMin, timeMax, onImport }) => {
|
|||
'id': item.owner.address,
|
||||
'color': item.hexColor,
|
||||
'checked': item.isDefaultCalendar === true,
|
||||
})));
|
||||
})))
|
||||
})
|
||||
.catch(e => {
|
||||
console.error(e);
|
||||
signOut();
|
||||
});
|
||||
console.error(e)
|
||||
signOut()
|
||||
})
|
||||
}
|
||||
// eslint-disable-next-line
|
||||
}, [client]);
|
||||
}, [client])
|
||||
|
||||
return (
|
||||
<>
|
||||
|
|
@ -170,9 +168,7 @@ const OutlookCalendar = ({ timeZone, timeMin, timeMax, onImport }) => {
|
|||
primaryColor="#0364B9"
|
||||
secondaryColor="#02437D"
|
||||
icon={<img aria-hidden="true" focusable="false" src={outlookLogo} alt="" />}
|
||||
>
|
||||
{t('event:you.outlook_cal')}
|
||||
</Button>
|
||||
>{t('event:you.outlook_cal')}</Button>
|
||||
</Center>
|
||||
) : (
|
||||
<CalendarList>
|
||||
|
|
@ -180,21 +176,21 @@ const OutlookCalendar = ({ timeZone, timeMin, timeMax, onImport }) => {
|
|||
<Icon src={outlookLogo} alt="" />
|
||||
<strong>{t('event:you.outlook_cal')}</strong>
|
||||
(<LinkButton type="button" onClick={e => {
|
||||
e.preventDefault();
|
||||
signOut();
|
||||
e.preventDefault()
|
||||
signOut()
|
||||
}}>{t('event:you.google_cal.logout')}</LinkButton>)
|
||||
</Title>
|
||||
<Options>
|
||||
{calendars !== undefined && !calendars.every(c => c.checked) && (
|
||||
<LinkButton type="button" onClick={e => {
|
||||
e.preventDefault();
|
||||
setCalendars(calendars.map(c => ({...c, checked: true})));
|
||||
e.preventDefault()
|
||||
setCalendars(calendars.map(c => ({...c, checked: true})))
|
||||
}}>{t('event:you.google_cal.select_all')}</LinkButton>
|
||||
)}
|
||||
{calendars !== undefined && calendars.every(c => c.checked) && (
|
||||
<LinkButton type="button" onClick={e => {
|
||||
e.preventDefault();
|
||||
setCalendars(calendars.map(c => ({...c, checked: false})));
|
||||
e.preventDefault()
|
||||
setCalendars(calendars.map(c => ({...c, checked: false})))
|
||||
}}>{t('event:you.google_cal.select_none')}</LinkButton>
|
||||
)}
|
||||
</Options>
|
||||
|
|
@ -206,14 +202,12 @@ const OutlookCalendar = ({ timeZone, timeMin, timeMax, onImport }) => {
|
|||
id={calendar.id}
|
||||
color={calendar.color}
|
||||
checked={calendar.checked}
|
||||
onChange={e => setCalendars(calendars.map(c => c.id === calendar.id ? {...c, checked: !c.checked} : c))}
|
||||
onChange={() => setCalendars(calendars.map(c => c.id === calendar.id ? {...c, checked: !c.checked} : c))}
|
||||
/>
|
||||
<CheckboxLabel htmlFor={calendar.id} color={calendar.color} />
|
||||
<CalendarLabel htmlFor={calendar.id}>{calendar.name}</CalendarLabel>
|
||||
</div>
|
||||
)) : (
|
||||
<Loader />
|
||||
)}
|
||||
)) : <Loader />}
|
||||
{calendars !== undefined && (
|
||||
<>
|
||||
<Info>{t('event:you.google_cal.info')}</Info>
|
||||
|
|
@ -228,7 +222,7 @@ const OutlookCalendar = ({ timeZone, timeMin, timeMax, onImport }) => {
|
|||
</CalendarList>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
export default OutlookCalendar;
|
||||
export default OutlookCalendar
|
||||
|
|
@ -1,17 +1,18 @@
|
|||
import { useTranslation } from 'react-i18next';
|
||||
import { useRecentsStore, useLocaleUpdateStore } from 'stores';
|
||||
import dayjs from 'dayjs';
|
||||
import relativeTime from 'dayjs/plugin/relativeTime';
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import dayjs from 'dayjs'
|
||||
import relativeTime from 'dayjs/plugin/relativeTime'
|
||||
|
||||
import { AboutSection, StyledMain } from '../../pages/Home/homeStyle';
|
||||
import { Wrapper, Recent } from './recentsStyle';
|
||||
import { useRecentsStore, useLocaleUpdateStore } from '/src/stores'
|
||||
|
||||
dayjs.extend(relativeTime);
|
||||
import { AboutSection, StyledMain } from '../../pages/Home/Home.styles'
|
||||
import { Wrapper, Recent } from './Recents.styles'
|
||||
|
||||
dayjs.extend(relativeTime)
|
||||
|
||||
const Recents = ({ target }) => {
|
||||
const recents = useRecentsStore(state => state.recents);
|
||||
const locale = useLocaleUpdateStore(state => state.locale);
|
||||
const { t } = useTranslation(['home', 'common']);
|
||||
const recents = useRecentsStore(state => state.recents)
|
||||
const locale = useLocaleUpdateStore(state => state.locale)
|
||||
const { t } = useTranslation(['home', 'common'])
|
||||
|
||||
return !!recents.length && (
|
||||
<Wrapper>
|
||||
|
|
@ -27,7 +28,7 @@ const Recents = ({ target }) => {
|
|||
</StyledMain>
|
||||
</AboutSection>
|
||||
</Wrapper>
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
export default Recents;
|
||||
export default Recents
|
||||
|
|
@ -1,12 +1,12 @@
|
|||
import styled from '@emotion/styled';
|
||||
import { styled } from 'goober'
|
||||
|
||||
export const Wrapper = styled.div`
|
||||
export const Wrapper = styled('div')`
|
||||
@media print {
|
||||
display: none;
|
||||
}
|
||||
`;
|
||||
`
|
||||
|
||||
export const Recent = styled.a`
|
||||
export const Recent = styled('a')`
|
||||
text-decoration: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
|
@ -17,7 +17,7 @@ export const Recent = styled.a`
|
|||
& .name {
|
||||
font-weight: 700;
|
||||
font-size: 1.1em;
|
||||
color: ${props => props.theme.mode === 'light' ? props.theme.primaryDark : props.theme.primaryLight};
|
||||
color: var(--secondary);
|
||||
flex: 1;
|
||||
display: block;
|
||||
}
|
||||
|
|
@ -25,7 +25,7 @@ export const Recent = styled.a`
|
|||
font-weight: 400;
|
||||
opacity: .8;
|
||||
white-space: nowrap;
|
||||
color: ${props => props.theme.text};
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
&:hover .name {
|
||||
|
|
@ -39,4 +39,4 @@ export const Recent = styled.a`
|
|||
white-space: normal;
|
||||
}
|
||||
}
|
||||
`;
|
||||
`
|
||||
|
|
@ -1,10 +1,11 @@
|
|||
import { forwardRef } from 'react';
|
||||
import { forwardRef } from 'react'
|
||||
|
||||
import {
|
||||
Wrapper,
|
||||
StyledLabel,
|
||||
StyledSubLabel,
|
||||
StyledSelect,
|
||||
} from './selectFieldStyle';
|
||||
} from './SelectField.styles'
|
||||
|
||||
const SelectField = forwardRef(({
|
||||
label,
|
||||
|
|
@ -16,13 +17,13 @@ const SelectField = forwardRef(({
|
|||
defaultOption,
|
||||
...props
|
||||
}, ref) => (
|
||||
<Wrapper inline={inline} small={small}>
|
||||
{label && <StyledLabel htmlFor={id} inline={inline} small={small}>{label}</StyledLabel>}
|
||||
<Wrapper $inline={inline} $small={small}>
|
||||
{label && <StyledLabel htmlFor={id} $inline={inline} $small={small}>{label}</StyledLabel>}
|
||||
{subLabel && <StyledSubLabel htmlFor={id}>{subLabel}</StyledSubLabel>}
|
||||
|
||||
<StyledSelect
|
||||
id={id}
|
||||
small={small}
|
||||
$small={small}
|
||||
ref={ref}
|
||||
{...props}
|
||||
>
|
||||
|
|
@ -38,6 +39,6 @@ const SelectField = forwardRef(({
|
|||
)}
|
||||
</StyledSelect>
|
||||
</Wrapper>
|
||||
));
|
||||
))
|
||||
|
||||
export default SelectField;
|
||||
export default SelectField
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
import { styled } from 'goober'
|
||||
import { forwardRef } from 'react'
|
||||
|
||||
export const Wrapper = styled('div')`
|
||||
margin: 30px 0;
|
||||
|
||||
${props => props.$inline && `
|
||||
margin: 0;
|
||||
`}
|
||||
${props => props.$small && `
|
||||
margin: 10px 0;
|
||||
`}
|
||||
`
|
||||
|
||||
export const StyledLabel = styled('label')`
|
||||
display: block;
|
||||
padding-bottom: 4px;
|
||||
font-size: 18px;
|
||||
|
||||
${props => props.$inline && `
|
||||
font-size: 16px;
|
||||
`}
|
||||
${props => props.$small && `
|
||||
font-size: .9rem;
|
||||
`}
|
||||
`
|
||||
|
||||
export const StyledSubLabel = styled('label')`
|
||||
display: block;
|
||||
padding-bottom: 6px;
|
||||
font-size: 13px;
|
||||
opacity: .6;
|
||||
`
|
||||
|
||||
export const StyledSelect = styled('select', forwardRef)`
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
font: inherit;
|
||||
background: var(--surface);
|
||||
color: inherit;
|
||||
padding: 10px 14px;
|
||||
border: 1px solid var(--primary);
|
||||
box-shadow: inset 0 0 0 0 var(--primary);
|
||||
border-radius: 3px;
|
||||
outline: none;
|
||||
transition: border-color .15s, box-shadow .15s;
|
||||
appearance: none;
|
||||
background-image: url('data:image/svg+xml,${encodeURIComponent('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><foreignObject width="100px" height="100px"><div xmlns="http://www.w3.org/1999/xhtml" style="color:#F79E00;font-size:60px;display:flex;align-items:center;justify-content:center;height:100%;width:100%;">▼</div></foreignObject></svg>')}');
|
||||
background-repeat: no-repeat;
|
||||
background-position: right 10px center;
|
||||
background-size: 1em;
|
||||
|
||||
&:focus {
|
||||
border: 1px solid var(--secondary);
|
||||
box-shadow: inset 0 -3px 0 0 var(--secondary);
|
||||
}
|
||||
|
||||
${props => props.$small && `
|
||||
padding: 6px 8px;
|
||||
`}
|
||||
`
|
||||
|
|
@ -1,60 +0,0 @@
|
|||
import styled from '@emotion/styled';
|
||||
|
||||
export const Wrapper = styled.div`
|
||||
margin: 30px 0;
|
||||
|
||||
${props => props.inline && `
|
||||
margin: 0;
|
||||
`}
|
||||
${props => props.small && `
|
||||
margin: 10px 0;
|
||||
`}
|
||||
`;
|
||||
|
||||
export const StyledLabel = styled.label`
|
||||
display: block;
|
||||
padding-bottom: 4px;
|
||||
font-size: 18px;
|
||||
|
||||
${props => props.inline && `
|
||||
font-size: 16px;
|
||||
`}
|
||||
${props => props.small && `
|
||||
font-size: .9rem;
|
||||
`}
|
||||
`;
|
||||
|
||||
export const StyledSubLabel = styled.label`
|
||||
display: block;
|
||||
padding-bottom: 6px;
|
||||
font-size: 13px;
|
||||
opacity: .6;
|
||||
`;
|
||||
|
||||
export const StyledSelect = styled.select`
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
font: inherit;
|
||||
background: ${props => props.theme.primaryBackground};
|
||||
color: inherit;
|
||||
padding: 10px 14px;
|
||||
border: 1px solid ${props => props.theme.primary};
|
||||
box-shadow: inset 0 0 0 0 ${props => props.theme.primary};
|
||||
border-radius: 3px;
|
||||
outline: none;
|
||||
transition: border-color .15s, box-shadow .15s;
|
||||
appearance: none;
|
||||
background-image: url("data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><foreignObject width=%22100px%22 height=%22100px%22><div xmlns=%22http://www.w3.org/1999/xhtml%22 style=%22color:${props => encodeURIComponent(props.theme.primary)};font-size:60px;display:flex;align-items:center;justify-content:center;height:100%25;width:100%25%22>▼</div></foreignObject></svg>");
|
||||
background-repeat: no-repeat;
|
||||
background-position: right 10px center;
|
||||
background-size: 1em;
|
||||
|
||||
&:focus {
|
||||
border: 1px solid ${props => props.theme.mode === 'light' ? props.theme.primaryDark : props.theme.primaryLight};
|
||||
box-shadow: inset 0 -3px 0 0 ${props => props.theme.mode === 'light' ? props.theme.primaryDark : props.theme.primaryLight};
|
||||
}
|
||||
|
||||
${props => props.small && `
|
||||
padding: 6px 8px;
|
||||
`}
|
||||
`;
|
||||
|
|
@ -1,93 +1,90 @@
|
|||
import { useState, useEffect, useRef } from 'react';
|
||||
import { useTheme } from '@emotion/react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import dayjs from 'dayjs';
|
||||
import { useState, useEffect, useRef } from 'react'
|
||||
import { useLocation } from 'react-router-dom'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import dayjs from 'dayjs'
|
||||
import { Settings as SettingsIcon } from 'lucide-react'
|
||||
|
||||
import { ToggleField, SelectField } from 'components';
|
||||
import { ToggleField, SelectField } from '/src/components'
|
||||
|
||||
import { useSettingsStore, useLocaleUpdateStore } from 'stores';
|
||||
import { useSettingsStore, useLocaleUpdateStore } from '/src/stores'
|
||||
|
||||
import {
|
||||
OpenButton,
|
||||
Modal,
|
||||
Heading,
|
||||
Cover,
|
||||
} from './settingsStyle';
|
||||
} from './Settings.styles'
|
||||
|
||||
import locales from 'res/dayjs_locales';
|
||||
import locales from '/src/i18n/locales'
|
||||
|
||||
// Language specific options
|
||||
const setDefaults = (lang, store) => {
|
||||
if (locales.hasOwnProperty(lang)) {
|
||||
store.setWeekStart(locales[lang].weekStart);
|
||||
store.setTimeFormat(locales[lang].timeFormat);
|
||||
if (locales[lang]) {
|
||||
store.setWeekStart(locales[lang].weekStart)
|
||||
store.setTimeFormat(locales[lang].timeFormat)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const Settings = () => {
|
||||
const { pathname } = useLocation();
|
||||
const theme = useTheme();
|
||||
const store = useSettingsStore();
|
||||
const [isOpen, _setIsOpen] = useState(false);
|
||||
const { t, i18n } = useTranslation('common');
|
||||
const setLocale = useLocaleUpdateStore(state => state.setLocale);
|
||||
const firstControlRef = useRef();
|
||||
const { pathname } = useLocation()
|
||||
const store = useSettingsStore()
|
||||
const [isOpen, _setIsOpen] = useState(false)
|
||||
const { t, i18n } = useTranslation('common')
|
||||
const setLocale = useLocaleUpdateStore(state => state.setLocale)
|
||||
const firstControlRef = useRef()
|
||||
|
||||
const onEsc = e => {
|
||||
if (e.key === 'Escape') {
|
||||
setIsOpen(false);
|
||||
setIsOpen(false)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const setIsOpen = open => {
|
||||
_setIsOpen(open);
|
||||
_setIsOpen(open)
|
||||
|
||||
if (open) {
|
||||
window.setTimeout(() => firstControlRef.current.focus(), 150);
|
||||
document.addEventListener('keyup', onEsc, true);
|
||||
window.setTimeout(() => firstControlRef.current?.focus(), 150)
|
||||
document.addEventListener('keyup', onEsc, true)
|
||||
} else {
|
||||
document.removeEventListener('keyup', onEsc);
|
||||
document.removeEventListener('keyup', onEsc)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (Object.keys(locales).includes(i18n.language)) {
|
||||
locales[i18n.language].import().then(() => {
|
||||
dayjs.locale(i18n.language);
|
||||
setLocale(dayjs.locale());
|
||||
document.documentElement.setAttribute('lang', i18n.language);
|
||||
});
|
||||
dayjs.locale(i18n.language)
|
||||
setLocale(dayjs.locale())
|
||||
document.documentElement.setAttribute('lang', i18n.language)
|
||||
})
|
||||
} else {
|
||||
setLocale('en');
|
||||
setLocale('en')
|
||||
document.documentElement.setAttribute('lang', 'en')
|
||||
}
|
||||
}, [i18n.language, setLocale]);
|
||||
}, [i18n.language, setLocale])
|
||||
|
||||
if (!i18n.options.storedLang) {
|
||||
setDefaults(i18n.language, store);
|
||||
i18n.options.storedLang = i18n.language;
|
||||
setDefaults(i18n.language, store)
|
||||
i18n.options.storedLang = i18n.language
|
||||
}
|
||||
|
||||
i18n.on('languageChanged', lang => {
|
||||
setDefaults(lang, store);
|
||||
});
|
||||
setDefaults(lang, store)
|
||||
})
|
||||
|
||||
// Reset scroll on navigation
|
||||
useEffect(() => window.scrollTo(0, 0), [pathname]);
|
||||
useEffect(() => window.scrollTo(0, 0), [pathname])
|
||||
|
||||
return (
|
||||
<>
|
||||
<OpenButton
|
||||
isOpen={isOpen}
|
||||
$isOpen={isOpen}
|
||||
type="button"
|
||||
onClick={() => setIsOpen(!isOpen)} title={t('options.name')}
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke={theme.text} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><circle cx="12" cy="12" r="3"></circle><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path></svg>
|
||||
</OpenButton>
|
||||
><SettingsIcon /></OpenButton>
|
||||
|
||||
<Cover isOpen={isOpen} onClick={() => setIsOpen(false)} />
|
||||
<Modal isOpen={isOpen}>
|
||||
<Cover $isOpen={isOpen} onClick={() => setIsOpen(false)} />
|
||||
<Modal $isOpen={isOpen}>
|
||||
<Heading>{t('options.name')}</Heading>
|
||||
|
||||
<ToggleField
|
||||
|
|
@ -147,8 +144,8 @@ const Settings = () => {
|
|||
id="language"
|
||||
options={{
|
||||
...Object.keys(locales).reduce((ls, l) => {
|
||||
ls[l] = locales[l].name;
|
||||
return ls;
|
||||
ls[l] = locales[l].name
|
||||
return ls
|
||||
}, {}),
|
||||
...process.env.NODE_ENV !== 'production' && { 'cimode': 'DEV' },
|
||||
}}
|
||||
|
|
@ -158,7 +155,7 @@ const Settings = () => {
|
|||
/>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
export default Settings;
|
||||
export default Settings
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import styled from '@emotion/styled';
|
||||
import { styled } from 'goober'
|
||||
|
||||
export const OpenButton = styled.button`
|
||||
export const OpenButton = styled('button')`
|
||||
border: 0;
|
||||
background: none;
|
||||
height: 50px;
|
||||
|
|
@ -19,15 +19,8 @@ export const OpenButton = styled.button`
|
|||
transition: transform .15s;
|
||||
padding: 0;
|
||||
|
||||
&:focus {
|
||||
outline: 0;
|
||||
}
|
||||
&:focus-visible {
|
||||
background-color: ${props => props.theme.text}22;
|
||||
}
|
||||
|
||||
${props => props.isOpen && `
|
||||
transform: rotate(-45deg);
|
||||
${props => props.$isOpen && `
|
||||
transform: rotate(-60deg);
|
||||
`}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
|
|
@ -36,9 +29,9 @@ export const OpenButton = styled.button`
|
|||
@media print {
|
||||
display: none;
|
||||
}
|
||||
`;
|
||||
`
|
||||
|
||||
export const Cover = styled.div`
|
||||
export const Cover = styled('div')`
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
|
|
@ -47,19 +40,17 @@ export const Cover = styled.div`
|
|||
z-index: 100;
|
||||
display: none;
|
||||
|
||||
${props => props.isOpen && `
|
||||
${props => props.$isOpen && `
|
||||
display: block;
|
||||
`}
|
||||
`;
|
||||
`
|
||||
|
||||
export const Modal = styled.div`
|
||||
export const Modal = styled('div')`
|
||||
position: absolute;
|
||||
top: 70px;
|
||||
right: 12px;
|
||||
background-color: ${props => props.theme.background};
|
||||
${props => props.theme.mode === 'dark' && `
|
||||
border: 1px solid ${props.theme.primaryBackground};
|
||||
`}
|
||||
background-color: var(--background);
|
||||
border: 1px solid var(--surface);
|
||||
z-index: 150;
|
||||
padding: 10px 18px;
|
||||
border-radius: 3px;
|
||||
|
|
@ -74,7 +65,7 @@ export const Modal = styled.div`
|
|||
visibility: hidden;
|
||||
transition: opacity .15s, transform .15s, visibility .15s;
|
||||
|
||||
${props => props.isOpen && `
|
||||
${props => props.$isOpen && `
|
||||
pointer-events: all;
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
|
|
@ -87,11 +78,11 @@ export const Modal = styled.div`
|
|||
@media print {
|
||||
display: none;
|
||||
}
|
||||
`;
|
||||
`
|
||||
|
||||
export const Heading = styled.span`
|
||||
export const Heading = styled('span')`
|
||||
font-size: 1.5rem;
|
||||
display: block;
|
||||
margin: 6px 0;
|
||||
line-height: 1em;
|
||||
`;
|
||||
`
|
||||
|
|
@ -1,10 +1,11 @@
|
|||
import { forwardRef } from 'react';
|
||||
import { forwardRef } from 'react'
|
||||
|
||||
import {
|
||||
Wrapper,
|
||||
StyledLabel,
|
||||
StyledSubLabel,
|
||||
StyledInput,
|
||||
} from './textFieldStyle';
|
||||
} from './TextField.styles'
|
||||
|
||||
const TextField = forwardRef(({
|
||||
label,
|
||||
|
|
@ -13,11 +14,11 @@ const TextField = forwardRef(({
|
|||
inline = false,
|
||||
...props
|
||||
}, ref) => (
|
||||
<Wrapper inline={inline}>
|
||||
{label && <StyledLabel htmlFor={id} inline={inline}>{label}</StyledLabel>}
|
||||
<Wrapper $inline={inline}>
|
||||
{label && <StyledLabel htmlFor={id} $inline={inline}>{label}</StyledLabel>}
|
||||
{subLabel && <StyledSubLabel htmlFor={id}>{subLabel}</StyledSubLabel>}
|
||||
<StyledInput id={id} ref={ref} {...props} />
|
||||
</Wrapper>
|
||||
));
|
||||
))
|
||||
|
||||
export default TextField;
|
||||
export default TextField
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
import { styled } from 'goober'
|
||||
import { forwardRef } from 'react'
|
||||
|
||||
export const Wrapper = styled('div')`
|
||||
margin: 30px 0;
|
||||
|
||||
${props => props.$inline && `
|
||||
margin: 0;
|
||||
`}
|
||||
`
|
||||
|
||||
export const StyledLabel = styled('label')`
|
||||
display: block;
|
||||
padding-bottom: 4px;
|
||||
font-size: 18px;
|
||||
|
||||
${props => props.$inline && `
|
||||
font-size: 16px;
|
||||
`}
|
||||
`
|
||||
|
||||
export const StyledSubLabel = styled('label')`
|
||||
display: block;
|
||||
padding-bottom: 6px;
|
||||
font-size: 13px;
|
||||
opacity: .6;
|
||||
`
|
||||
|
||||
export const StyledInput = styled('input', forwardRef)`
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
font: inherit;
|
||||
background: var(--surface);
|
||||
color: inherit;
|
||||
padding: 10px 14px;
|
||||
border: 1px solid var(--primary);
|
||||
box-shadow: inset 0 0 0 0 var(--primary);
|
||||
border-radius: 3px;
|
||||
font-size: 18px;
|
||||
outline: none;
|
||||
transition: border-color .15s, box-shadow .15s;
|
||||
|
||||
&:focus {
|
||||
border: 1px solid var(--secondary);
|
||||
box-shadow: inset 0 -3px 0 0 var(--secondary);
|
||||
}
|
||||
`
|
||||
|
|
@ -1,46 +0,0 @@
|
|||
import styled from '@emotion/styled';
|
||||
|
||||
export const Wrapper = styled.div`
|
||||
margin: 30px 0;
|
||||
|
||||
${props => props.inline && `
|
||||
margin: 0;
|
||||
`}
|
||||
`;
|
||||
|
||||
export const StyledLabel = styled.label`
|
||||
display: block;
|
||||
padding-bottom: 4px;
|
||||
font-size: 18px;
|
||||
|
||||
${props => props.inline && `
|
||||
font-size: 16px;
|
||||
`}
|
||||
`;
|
||||
|
||||
export const StyledSubLabel = styled.label`
|
||||
display: block;
|
||||
padding-bottom: 6px;
|
||||
font-size: 13px;
|
||||
opacity: .6;
|
||||
`;
|
||||
|
||||
export const StyledInput = styled.input`
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
font: inherit;
|
||||
background: ${props => props.theme.primaryBackground};
|
||||
color: inherit;
|
||||
padding: 10px 14px;
|
||||
border: 1px solid ${props => props.theme.primary};
|
||||
box-shadow: inset 0 0 0 0 ${props => props.theme.primary};
|
||||
border-radius: 3px;
|
||||
font-size: 18px;
|
||||
outline: none;
|
||||
transition: border-color .15s, box-shadow .15s;
|
||||
|
||||
&:focus {
|
||||
border: 1px solid ${props => props.theme.mode === 'light' ? props.theme.primaryDark : props.theme.primaryLight};
|
||||
box-shadow: inset 0 -3px 0 0 ${props => props.theme.mode === 'light' ? props.theme.primaryDark : props.theme.primaryLight};
|
||||
}
|
||||
`;
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
import { useState, useEffect, useRef, forwardRef } from 'react';
|
||||
import dayjs from 'dayjs';
|
||||
import { useState, useEffect, useRef, forwardRef } from 'react'
|
||||
import dayjs from 'dayjs'
|
||||
|
||||
import { useSettingsStore, useLocaleUpdateStore } from 'stores';
|
||||
import { useSettingsStore, useLocaleUpdateStore } from '/src/stores'
|
||||
|
||||
import {
|
||||
Wrapper,
|
||||
|
|
@ -10,9 +10,9 @@ import {
|
|||
Range,
|
||||
Handle,
|
||||
Selected,
|
||||
} from './timeRangeFieldStyle';
|
||||
} from './TimeRangeField.styles'
|
||||
|
||||
const times = ['00','01','02','03','04','05','06','07','08','09','10','11','12','13','14','15','16','17','18','19','20','21','22','23','24'];
|
||||
const times = ['00','01','02','03','04','05','06','07','08','09','10','11','12','13','14','15','16','17','18','19','20','21','22','23','24']
|
||||
|
||||
const TimeRangeField = forwardRef(({
|
||||
label,
|
||||
|
|
@ -21,39 +21,39 @@ const TimeRangeField = forwardRef(({
|
|||
setValue,
|
||||
...props
|
||||
}, ref) => {
|
||||
const timeFormat = useSettingsStore(state => state.timeFormat);
|
||||
const locale = useLocaleUpdateStore(state => state.locale);
|
||||
const timeFormat = useSettingsStore(state => state.timeFormat)
|
||||
const locale = useLocaleUpdateStore(state => state.locale)
|
||||
|
||||
const [start, setStart] = useState(9);
|
||||
const [end, setEnd] = useState(17);
|
||||
const [start, setStart] = useState(9)
|
||||
const [end, setEnd] = useState(17)
|
||||
|
||||
const isStartMoving = useRef(false);
|
||||
const isEndMoving = useRef(false);
|
||||
const rangeRef = useRef();
|
||||
const rangeRect = useRef();
|
||||
const isStartMoving = useRef(false)
|
||||
const isEndMoving = useRef(false)
|
||||
const rangeRef = useRef()
|
||||
const rangeRect = useRef()
|
||||
|
||||
useEffect(() => {
|
||||
if (rangeRef.current) {
|
||||
rangeRect.current = rangeRef.current.getBoundingClientRect();
|
||||
rangeRect.current = rangeRef.current.getBoundingClientRect()
|
||||
}
|
||||
}, [rangeRef]);
|
||||
}, [rangeRef])
|
||||
|
||||
useEffect(() => setValue(props.name, JSON.stringify({start, end})), [start, end, setValue, props.name]);
|
||||
useEffect(() => setValue(props.name, JSON.stringify({start, end})), [start, end, setValue, props.name])
|
||||
|
||||
const handleMouseMove = e => {
|
||||
if (isStartMoving.current || isEndMoving.current) {
|
||||
let step = Math.round(((e.pageX - rangeRect.current.left) / rangeRect.current.width) * 24);
|
||||
if (step < 0) step = 0;
|
||||
if (step > 24) step = 24;
|
||||
step = Math.abs(step);
|
||||
let step = Math.round(((e.pageX - rangeRect.current.left) / rangeRect.current.width) * 24)
|
||||
if (step < 0) step = 0
|
||||
if (step > 24) step = 24
|
||||
step = Math.abs(step)
|
||||
|
||||
if (isStartMoving.current) {
|
||||
setStart(step);
|
||||
setStart(step)
|
||||
} else if (isEndMoving.current) {
|
||||
setEnd(step);
|
||||
setEnd(step)
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return (
|
||||
<Wrapper locale={locale}>
|
||||
|
|
@ -68,79 +68,79 @@ const TimeRangeField = forwardRef(({
|
|||
/>
|
||||
|
||||
<Range ref={rangeRef}>
|
||||
<Selected start={start} end={start > end ? 24 : end} />
|
||||
{start > end && <Selected start={start > end ? 0 : start} end={end} />}
|
||||
<Selected $start={start} $end={start > end ? 24 : end} />
|
||||
{start > end && <Selected $start={start > end ? 0 : start} $end={end} />}
|
||||
<Handle
|
||||
value={start}
|
||||
$value={start}
|
||||
label={timeFormat === '24h' ? times[start] : dayjs().hour(times[start]).format('ha')}
|
||||
extraPadding={end - start === 1 ? 'padding-right: 20px;' : (start - end === 1 ? 'padding-left: 20px;' : '')}
|
||||
$extraPadding={end - start === 1 ? 'padding-right: 20px;' : (start - end === 1 ? 'padding-left: 20px;' : '')}
|
||||
onMouseDown={() => {
|
||||
document.addEventListener('mousemove', handleMouseMove);
|
||||
isStartMoving.current = true;
|
||||
document.addEventListener('mousemove', handleMouseMove)
|
||||
isStartMoving.current = true
|
||||
|
||||
document.addEventListener('mouseup', () => {
|
||||
isStartMoving.current = false;
|
||||
document.removeEventListener('mousemove', handleMouseMove);
|
||||
}, { once: true });
|
||||
isStartMoving.current = false
|
||||
document.removeEventListener('mousemove', handleMouseMove)
|
||||
}, { once: true })
|
||||
}}
|
||||
onTouchMove={(e) => {
|
||||
const touch = e.targetTouches[0];
|
||||
onTouchMove={e => {
|
||||
const touch = e.targetTouches[0]
|
||||
|
||||
let step = Math.round(((touch.pageX - rangeRect.current.left) / rangeRect.current.width) * 24);
|
||||
if (step < 0) step = 0;
|
||||
if (step > 24) step = 24;
|
||||
step = Math.abs(step);
|
||||
setStart(step);
|
||||
let step = Math.round(((touch.pageX - rangeRect.current.left) / rangeRect.current.width) * 24)
|
||||
if (step < 0) step = 0
|
||||
if (step > 24) step = 24
|
||||
step = Math.abs(step)
|
||||
setStart(step)
|
||||
}}
|
||||
tabIndex="0"
|
||||
onKeyDown={e => {
|
||||
if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') {
|
||||
e.preventDefault();
|
||||
setStart(Math.max(start-1, 0));
|
||||
e.preventDefault()
|
||||
setStart(Math.max(start-1, 0))
|
||||
}
|
||||
if (e.key === 'ArrowRight' || e.key === 'ArrowDown') {
|
||||
e.preventDefault();
|
||||
setStart(Math.min(start+1, 24));
|
||||
e.preventDefault()
|
||||
setStart(Math.min(start+1, 24))
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Handle
|
||||
value={end}
|
||||
$value={end}
|
||||
label={timeFormat === '24h' ? times[end] : dayjs().hour(times[end]).format('ha')}
|
||||
extraPadding={end - start === 1 ? 'padding-left: 20px;' : (start - end === 1 ? 'padding-right: 20px;' : '')}
|
||||
$extraPadding={end - start === 1 ? 'padding-left: 20px;' : (start - end === 1 ? 'padding-right: 20px;' : '')}
|
||||
onMouseDown={() => {
|
||||
document.addEventListener('mousemove', handleMouseMove);
|
||||
isEndMoving.current = true;
|
||||
document.addEventListener('mousemove', handleMouseMove)
|
||||
isEndMoving.current = true
|
||||
|
||||
document.addEventListener('mouseup', () => {
|
||||
isEndMoving.current = false;
|
||||
document.removeEventListener('mousemove', handleMouseMove);
|
||||
}, { once: true });
|
||||
isEndMoving.current = false
|
||||
document.removeEventListener('mousemove', handleMouseMove)
|
||||
}, { once: true })
|
||||
}}
|
||||
onTouchMove={(e) => {
|
||||
const touch = e.targetTouches[0];
|
||||
onTouchMove={e => {
|
||||
const touch = e.targetTouches[0]
|
||||
|
||||
let step = Math.round(((touch.pageX - rangeRect.current.left) / rangeRect.current.width) * 24);
|
||||
if (step < 0) step = 0;
|
||||
if (step > 24) step = 24;
|
||||
step = Math.abs(step);
|
||||
setEnd(step);
|
||||
let step = Math.round(((touch.pageX - rangeRect.current.left) / rangeRect.current.width) * 24)
|
||||
if (step < 0) step = 0
|
||||
if (step > 24) step = 24
|
||||
step = Math.abs(step)
|
||||
setEnd(step)
|
||||
}}
|
||||
tabIndex="0"
|
||||
onKeyDown={e => {
|
||||
if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') {
|
||||
e.preventDefault();
|
||||
setEnd(Math.max(end-1, 0));
|
||||
e.preventDefault()
|
||||
setEnd(Math.max(end-1, 0))
|
||||
}
|
||||
if (e.key === 'ArrowRight' || e.key === 'ArrowDown') {
|
||||
e.preventDefault();
|
||||
setEnd(Math.min(end+1, 24));
|
||||
e.preventDefault()
|
||||
setEnd(Math.min(end+1, 24))
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Range>
|
||||
</Wrapper>
|
||||
);
|
||||
});
|
||||
)
|
||||
})
|
||||
|
||||
export default TimeRangeField;
|
||||
export default TimeRangeField
|
||||
|
|
@ -1,41 +1,42 @@
|
|||
import styled from '@emotion/styled';
|
||||
import { styled } from 'goober'
|
||||
import { forwardRef } from 'react'
|
||||
|
||||
export const Wrapper = styled.div`
|
||||
export const Wrapper = styled('div')`
|
||||
margin: 30px 0;
|
||||
`;
|
||||
`
|
||||
|
||||
export const StyledLabel = styled.label`
|
||||
export const StyledLabel = styled('label')`
|
||||
display: block;
|
||||
padding-bottom: 4px;
|
||||
font-size: 18px;
|
||||
`;
|
||||
`
|
||||
|
||||
export const StyledSubLabel = styled.label`
|
||||
export const StyledSubLabel = styled('label')`
|
||||
display: block;
|
||||
padding-bottom: 6px;
|
||||
font-size: 13px;
|
||||
opacity: .6;
|
||||
`;
|
||||
`
|
||||
|
||||
export const Range = styled.div`
|
||||
export const Range = styled('div', forwardRef)`
|
||||
user-select: none;
|
||||
background-color: ${props => props.theme.primaryBackground};
|
||||
border: 1px solid ${props => props.theme.primary};
|
||||
background-color: var(--surface);
|
||||
border: 1px solid var(--primary);
|
||||
border-radius: 3px;
|
||||
height: 50px;
|
||||
position: relative;
|
||||
margin: 38px 6px 18px;
|
||||
`;
|
||||
`
|
||||
|
||||
export const Handle = styled.div`
|
||||
export const Handle = styled('div')`
|
||||
height: calc(100% + 20px);
|
||||
width: 20px;
|
||||
border: 1px solid ${props => props.theme.primary};
|
||||
background-color: ${props => props.theme.primaryLight};
|
||||
border: 1px solid var(--primary);
|
||||
background-color: var(--highlight);
|
||||
border-radius: 3px;
|
||||
position: absolute;
|
||||
top: -10px;
|
||||
left: calc(${props => props.value * 4.1666666666666666}% - 11px);
|
||||
left: calc(${props => props.$value * 4.166}% - 11px);
|
||||
cursor: ew-resize;
|
||||
touch-action: none;
|
||||
transition: left .1s;
|
||||
|
|
@ -54,7 +55,7 @@ export const Handle = styled.div`
|
|||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: ${props => props.theme.primaryDark};
|
||||
color: var(--shadow);
|
||||
}
|
||||
|
||||
&:before {
|
||||
|
|
@ -65,20 +66,20 @@ export const Handle = styled.div`
|
|||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
white-space: nowrap;
|
||||
${props => props.extraPadding}
|
||||
${props => props.$extraPadding}
|
||||
}
|
||||
`;
|
||||
`
|
||||
|
||||
export const Selected = styled.div`
|
||||
export const Selected = styled('div')`
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
left: ${props => props.start * 4.1666666666666666}%;
|
||||
right: calc(100% - ${props => props.end * 4.1666666666666666}%);
|
||||
left: ${props => props.$start * 4.166}%;
|
||||
right: calc(100% - ${props => props.$end * 4.166}%);
|
||||
top: 0;
|
||||
background-color: ${props => props.theme.primary};
|
||||
background-color: var(--primary);
|
||||
border-radius: 2px;
|
||||
transition: left .1s, right .1s;
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
transition: none;
|
||||
}
|
||||
`;
|
||||
`
|
||||
|
|
@ -1,3 +1,5 @@
|
|||
import { Info } from 'lucide-react'
|
||||
|
||||
import {
|
||||
Wrapper,
|
||||
ToggleContainer,
|
||||
|
|
@ -5,21 +7,19 @@ import {
|
|||
Option,
|
||||
HiddenInput,
|
||||
LabelButton,
|
||||
} from './toggleFieldStyle';
|
||||
} from './ToggleField.styles'
|
||||
|
||||
const ToggleField = ({
|
||||
label,
|
||||
id,
|
||||
name,
|
||||
title = '',
|
||||
options = [],
|
||||
value,
|
||||
onChange,
|
||||
inputRef,
|
||||
...props
|
||||
}) => (
|
||||
<Wrapper>
|
||||
{label && <StyledLabel title={title}>{label} {title !== '' && <svg viewBox="0 0 24 24"><path fill="currentColor" d="M11,9H13V7H11M12,20C7.59,20 4,16.41 4,12C4,7.59 7.59,4 12,4C16.41,4 20,7.59 20,12C20,16.41 16.41,20 12,20M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2M11,17H13V11H11V17Z" /></svg>}</StyledLabel>}
|
||||
{label && <StyledLabel title={title}>{label} {title !== '' && <Info />}</StyledLabel>}
|
||||
|
||||
<ToggleContainer>
|
||||
{Object.entries(options).map(([key, label]) =>
|
||||
|
|
@ -38,6 +38,6 @@ const ToggleField = ({
|
|||
)}
|
||||
</ToggleContainer>
|
||||
</Wrapper>
|
||||
);
|
||||
)
|
||||
|
||||
export default ToggleField;
|
||||
export default ToggleField
|
||||
|
|
@ -1,19 +1,20 @@
|
|||
import styled from '@emotion/styled';
|
||||
import { styled } from 'goober'
|
||||
import { forwardRef } from 'react'
|
||||
|
||||
export const Wrapper = styled.div`
|
||||
export const Wrapper = styled('div')`
|
||||
margin: 10px 0;
|
||||
`;
|
||||
`
|
||||
|
||||
export const ToggleContainer = styled.div`
|
||||
export const ToggleContainer = styled('div')`
|
||||
display: flex;
|
||||
border: 1px solid ${props => props.theme.primary};
|
||||
border: 1px solid var(--primary);
|
||||
border-radius: 3px;
|
||||
overflow: hidden;
|
||||
--focus-color: ${props => props.theme.primary};
|
||||
--focus-color: var(--primary);
|
||||
transition: border .15s;
|
||||
|
||||
&:focus-within {
|
||||
--focus-color: ${props => props.theme.mode === 'light' ? props.theme.primaryDark : props.theme.primaryLight};
|
||||
--focus-color: var(--secondary);
|
||||
border: 1px solid var(--focus-color);
|
||||
& label {
|
||||
box-shadow: inset 0 -3px 0 0 var(--focus-color);
|
||||
|
|
@ -26,9 +27,9 @@ export const ToggleContainer = styled.div`
|
|||
& > div:last-of-type label {
|
||||
border-end-end-radius: 2px;
|
||||
}
|
||||
`;
|
||||
`
|
||||
|
||||
export const StyledLabel = styled.label`
|
||||
export const StyledLabel = styled('label')`
|
||||
display: block;
|
||||
padding-bottom: 4px;
|
||||
font-size: .9rem;
|
||||
|
|
@ -38,14 +39,14 @@ export const StyledLabel = styled.label`
|
|||
width: 1em;
|
||||
vertical-align: middle;
|
||||
}
|
||||
`;
|
||||
`
|
||||
|
||||
export const Option = styled.div`
|
||||
export const Option = styled('div')`
|
||||
flex: 1;
|
||||
position: relative;
|
||||
`;
|
||||
`
|
||||
|
||||
export const HiddenInput = styled.input`
|
||||
export const HiddenInput = styled('input', forwardRef)`
|
||||
height: 0;
|
||||
width: 0;
|
||||
position: absolute;
|
||||
|
|
@ -55,12 +56,12 @@ export const HiddenInput = styled.input`
|
|||
appearance: none;
|
||||
|
||||
&:checked + label {
|
||||
color: ${props => props.theme.background};
|
||||
color: var(--background);
|
||||
background-color: var(--focus-color);
|
||||
}
|
||||
`;
|
||||
`
|
||||
|
||||
export const LabelButton = styled.label`
|
||||
export const LabelButton = styled('label')`
|
||||
padding: 6px;
|
||||
display: flex;
|
||||
text-align: center;
|
||||
|
|
@ -71,4 +72,4 @@ export const LabelButton = styled.label`
|
|||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: box-shadow .15s, background-color .15s;
|
||||
`;
|
||||
`
|
||||
|
|
@ -1,15 +1,15 @@
|
|||
import { Button } from 'components';
|
||||
import { Button } from '/src/components'
|
||||
|
||||
import { useTranslateStore } from 'stores';
|
||||
import { useTranslateStore } from '/src/stores'
|
||||
|
||||
import {
|
||||
Wrapper,
|
||||
ButtonWrapper,
|
||||
} from './translateDialogStyle';
|
||||
} from './TranslateDialog.styles'
|
||||
|
||||
const TranslateDialog = ({ onClose }) => {
|
||||
const navigatorLang = useTranslateStore(state => state.navigatorLang);
|
||||
const setDialogDismissed = useTranslateStore(state => state.setDialogDismissed);
|
||||
const TranslateDialog = () => {
|
||||
const navigatorLang = useTranslateStore(state => state.navigatorLang)
|
||||
const setDialogDismissed = useTranslateStore(state => state.setDialogDismissed)
|
||||
|
||||
return (
|
||||
<Wrapper>
|
||||
|
|
@ -26,7 +26,7 @@ const TranslateDialog = ({ onClose }) => {
|
|||
<Button secondary onClick={() => setDialogDismissed(true)}>Close</Button>
|
||||
</ButtonWrapper>
|
||||
</Wrapper>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
export default TranslateDialog;
|
||||
export default TranslateDialog
|
||||
|
|
@ -1,13 +1,11 @@
|
|||
import styled from '@emotion/styled';
|
||||
import { styled } from 'goober'
|
||||
|
||||
export const Wrapper = styled.div`
|
||||
export const Wrapper = styled('div')`
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
left: 20px;
|
||||
background-color: ${props => props.theme.background};
|
||||
${props => props.theme.mode === 'dark' && `
|
||||
border: 1px solid ${props.theme.primaryBackground};
|
||||
`}
|
||||
background-color: var(--background);
|
||||
border: 1px solid var(--surface);
|
||||
z-index: 900;
|
||||
padding: 20px;
|
||||
border-radius: 3px;
|
||||
|
|
@ -30,9 +28,9 @@ export const Wrapper = styled.div`
|
|||
@media (max-width: 400px) {
|
||||
display: block;
|
||||
}
|
||||
`;
|
||||
`
|
||||
|
||||
export const ButtonWrapper = styled.div`
|
||||
export const ButtonWrapper = styled('div')`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
|
|
@ -46,4 +44,4 @@ export const ButtonWrapper = styled.div`
|
|||
margin: 20px 0 0;
|
||||
white-space: normal;
|
||||
}
|
||||
`;
|
||||
`
|
||||
|
|
@ -1,13 +1,14 @@
|
|||
import { Button } from 'components';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import { Button } from '/src/components'
|
||||
|
||||
import {
|
||||
Wrapper,
|
||||
ButtonWrapper,
|
||||
} from './updateDialogStyle';
|
||||
} from './UpdateDialog.styles'
|
||||
|
||||
const UpdateDialog = ({ onClose }) => {
|
||||
const { t } = useTranslation('common');
|
||||
const { t } = useTranslation('common')
|
||||
|
||||
return (
|
||||
<Wrapper>
|
||||
|
|
@ -18,7 +19,7 @@ const UpdateDialog = ({ onClose }) => {
|
|||
<Button onClick={() => window.location.reload()}>{t('common:update.buttons.reload')}</Button>
|
||||
</ButtonWrapper>
|
||||
</Wrapper>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
export default UpdateDialog;
|
||||
export default UpdateDialog
|
||||
|
|
@ -1,13 +1,11 @@
|
|||
import styled from '@emotion/styled';
|
||||
import { styled } from 'goober'
|
||||
|
||||
export const Wrapper = styled.div`
|
||||
export const Wrapper = styled('div')`
|
||||
position: fixed;
|
||||
bottom: 20px;
|
||||
right: 20px;
|
||||
background-color: ${props => props.theme.background};
|
||||
${props => props.theme.mode === 'dark' && `
|
||||
border: 1px solid ${props.theme.primaryBackground};
|
||||
`}
|
||||
background-color: var(--background);
|
||||
border: 1px solid var(--surface);
|
||||
z-index: 900;
|
||||
padding: 20px 26px;
|
||||
border-radius: 3px;
|
||||
|
|
@ -24,12 +22,12 @@ export const Wrapper = styled.div`
|
|||
margin: 16px 0 24px;
|
||||
font-size: 1rem;
|
||||
}
|
||||
`;
|
||||
`
|
||||
|
||||
export const ButtonWrapper = styled.div`
|
||||
export const ButtonWrapper = styled('div')`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 16px;
|
||||
flex-wrap: wrap;
|
||||
`;
|
||||
`
|
||||
25
crabfit-frontend/src/components/index.js
Normal file
25
crabfit-frontend/src/components/index.js
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
export { default as TextField } from './TextField/TextField'
|
||||
export { default as SelectField } from './SelectField/SelectField'
|
||||
export { default as CalendarField } from './CalendarField/CalendarField'
|
||||
export { default as TimeRangeField } from './TimeRangeField/TimeRangeField'
|
||||
export { default as ToggleField } from './ToggleField/ToggleField'
|
||||
|
||||
export { default as Button } from './Button/Button'
|
||||
export { default as Legend } from './Legend/Legend'
|
||||
export { default as AvailabilityViewer } from './AvailabilityViewer/AvailabilityViewer'
|
||||
export { default as AvailabilityEditor } from './AvailabilityEditor/AvailabilityEditor'
|
||||
export { default as Error } from './Error/Error'
|
||||
export { default as Loading } from './Loading/Loading'
|
||||
|
||||
export { default as Center } from './Center/Center'
|
||||
export { default as Donate } from './Donate/Donate'
|
||||
export { default as Settings } from './Settings/Settings'
|
||||
export { default as Egg } from './Egg/Egg'
|
||||
export { default as Footer } from './Footer/Footer'
|
||||
export { default as Recents } from './Recents/Recents'
|
||||
export { default as Logo } from './Logo/Logo'
|
||||
export { default as UpdateDialog } from './UpdateDialog/UpdateDialog'
|
||||
export { default as TranslateDialog } from './TranslateDialog/TranslateDialog'
|
||||
|
||||
export const _GoogleCalendar = () => import('./GoogleCalendar/GoogleCalendar')
|
||||
export const _OutlookCalendar = () => import('./OutlookCalendar/OutlookCalendar')
|
||||
|
|
@ -1,25 +0,0 @@
|
|||
export { default as TextField } from './TextField/TextField';
|
||||
export { default as SelectField } from './SelectField/SelectField';
|
||||
export { default as CalendarField } from './CalendarField/CalendarField';
|
||||
export { default as TimeRangeField } from './TimeRangeField/TimeRangeField';
|
||||
export { default as ToggleField } from './ToggleField/ToggleField';
|
||||
|
||||
export { default as Button } from './Button/Button';
|
||||
export { default as Legend } from './Legend/Legend';
|
||||
export { default as AvailabilityViewer } from './AvailabilityViewer/AvailabilityViewer';
|
||||
export { default as AvailabilityEditor } from './AvailabilityEditor/AvailabilityEditor';
|
||||
export { default as Error } from './Error/Error';
|
||||
export { default as Loading } from './Loading/Loading';
|
||||
|
||||
export { default as Center } from './Center/Center';
|
||||
export { default as Donate } from './Donate/Donate';
|
||||
export { default as Settings } from './Settings/Settings';
|
||||
export { default as Egg } from './Egg/Egg';
|
||||
export { default as Footer } from './Footer/Footer';
|
||||
export { default as Recents } from './Recents/Recents';
|
||||
export { default as Logo } from './Logo/Logo';
|
||||
export { default as UpdateDialog } from './UpdateDialog/UpdateDialog';
|
||||
export { default as TranslateDialog } from './TranslateDialog/TranslateDialog';
|
||||
|
||||
export const _GoogleCalendar = () => import('./GoogleCalendar/GoogleCalendar');
|
||||
export const _OutlookCalendar = () => import('./OutlookCalendar/OutlookCalendar');
|
||||
|
|
@ -1,11 +1,11 @@
|
|||
import i18n from 'i18next';
|
||||
import { initReactI18next } from 'react-i18next';
|
||||
import LanguageDetector from 'i18next-browser-languagedetector';
|
||||
import Backend from 'i18next-http-backend';
|
||||
import i18n from 'i18next'
|
||||
import { initReactI18next } from 'react-i18next'
|
||||
import LanguageDetector from 'i18next-browser-languagedetector'
|
||||
import Backend from 'i18next-http-backend'
|
||||
|
||||
import locales from 'res/dayjs_locales';
|
||||
import locales from './locales'
|
||||
|
||||
const storedLang = localStorage.getItem('i18nextLng');
|
||||
const storedLang = localStorage.getItem('i18nextLng')
|
||||
|
||||
i18n
|
||||
.use(LanguageDetector)
|
||||
|
|
@ -15,7 +15,6 @@ i18n
|
|||
fallbackLng: 'en',
|
||||
supportedLngs: Object.keys(locales),
|
||||
ns: 'common',
|
||||
defaultNS: 'common',
|
||||
debug: process.env.NODE_ENV !== 'production',
|
||||
interpolation: {
|
||||
escapeValue: false,
|
||||
|
|
@ -24,6 +23,6 @@ i18n
|
|||
loadPath: '/i18n/{{lng}}/{{ns}}.json',
|
||||
},
|
||||
storedLang,
|
||||
}).then(() => document.documentElement.setAttribute('lang', i18n.language));
|
||||
}).then(() => document.documentElement.setAttribute('lang', i18n.language))
|
||||
|
||||
export default i18n;
|
||||
export default i18n
|
||||
|
|
@ -60,12 +60,24 @@ const locales = {
|
|||
weekStart: 1,
|
||||
timeFormat: '12h',
|
||||
},
|
||||
'pt-BR': { // Portuguese (Brazil)
|
||||
name: 'Português (do Brasil)',
|
||||
import: () => import('dayjs/locale/pt-br'),
|
||||
weekStart: 0,
|
||||
timeFormat: '24h',
|
||||
},
|
||||
'ru': { // Russian
|
||||
name: 'Pусский',
|
||||
import: () => import('dayjs/locale/ru'),
|
||||
weekStart: 1,
|
||||
timeFormat: '24h',
|
||||
},
|
||||
};
|
||||
// 'zh-CN': { // Chinese
|
||||
// name: '中文',
|
||||
// import: () => import('dayjs/locale/zh-cn'),
|
||||
// weekStart: 1,
|
||||
// timeFormat: '12h',
|
||||
// },
|
||||
}
|
||||
|
||||
export default locales;
|
||||
export default locales
|
||||
24
crabfit-frontend/src/index.jsx
Normal file
24
crabfit-frontend/src/index.jsx
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
import { StrictMode, createElement } from 'react'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import { setup } from 'goober'
|
||||
import { shouldForwardProp } from 'goober/should-forward-prop'
|
||||
import { BrowserRouter } from 'react-router-dom'
|
||||
import '/src/i18n'
|
||||
|
||||
import App from './App'
|
||||
|
||||
setup(
|
||||
createElement,
|
||||
undefined, undefined,
|
||||
shouldForwardProp(prop => !prop.startsWith('$'))
|
||||
)
|
||||
|
||||
const root = createRoot(document.getElementById('app'))
|
||||
|
||||
root.render(
|
||||
<StrictMode>
|
||||
<BrowserRouter>
|
||||
<App />
|
||||
</BrowserRouter>
|
||||
</StrictMode>
|
||||
)
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import App from './App';
|
||||
import 'i18n';
|
||||
|
||||
ReactDOM.render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>,
|
||||
document.getElementById('root')
|
||||
);
|
||||
|
|
@ -1,12 +1,12 @@
|
|||
import { useEffect, useState } from 'react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useTranslation, Trans } from 'react-i18next';
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { useForm } from 'react-hook-form'
|
||||
import { useTranslation, Trans } from 'react-i18next'
|
||||
|
||||
import dayjs from 'dayjs';
|
||||
import utc from 'dayjs/plugin/utc';
|
||||
import timezone from 'dayjs/plugin/timezone';
|
||||
import customParseFormat from 'dayjs/plugin/customParseFormat';
|
||||
import dayjs from 'dayjs'
|
||||
import utc from 'dayjs/plugin/utc'
|
||||
import timezone from 'dayjs/plugin/timezone'
|
||||
import customParseFormat from 'dayjs/plugin/customParseFormat'
|
||||
|
||||
import {
|
||||
TextField,
|
||||
|
|
@ -17,7 +17,7 @@ import {
|
|||
Error,
|
||||
Recents,
|
||||
Footer,
|
||||
} from 'components';
|
||||
} from '/src/components'
|
||||
|
||||
import {
|
||||
StyledMain,
|
||||
|
|
@ -27,80 +27,80 @@ import {
|
|||
P,
|
||||
OfflineMessage,
|
||||
ShareInfo,
|
||||
} from './createStyle';
|
||||
} from './Create.styles'
|
||||
|
||||
import api from 'services';
|
||||
import { useRecentsStore } from 'stores';
|
||||
import api from '/src/services'
|
||||
import { useRecentsStore } from '/src/stores'
|
||||
|
||||
import timezones from 'res/timezones.json';
|
||||
import timezones from '/src/res/timezones.json'
|
||||
|
||||
dayjs.extend(utc);
|
||||
dayjs.extend(timezone);
|
||||
dayjs.extend(customParseFormat);
|
||||
dayjs.extend(utc)
|
||||
dayjs.extend(timezone)
|
||||
dayjs.extend(customParseFormat)
|
||||
|
||||
const Create = ({ offline }) => {
|
||||
const { register, handleSubmit, setValue } = useForm({
|
||||
defaultValues: {
|
||||
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
||||
},
|
||||
});
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState(null);
|
||||
const [createdEvent, setCreatedEvent] = useState(null);
|
||||
const [copied, setCopied] = useState(null);
|
||||
const [showFooter, setShowFooter] = useState(true);
|
||||
})
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [error, setError] = useState(null)
|
||||
const [createdEvent, setCreatedEvent] = useState(null)
|
||||
const [copied, setCopied] = useState(null)
|
||||
const [showFooter, setShowFooter] = useState(true)
|
||||
|
||||
const { push } = useHistory();
|
||||
const { t } = useTranslation(['common', 'home', 'event']);
|
||||
const navigate = useNavigate()
|
||||
const { t } = useTranslation(['common', 'home', 'event'])
|
||||
|
||||
const addRecent = useRecentsStore(state => state.addRecent);
|
||||
const addRecent = useRecentsStore(state => state.addRecent)
|
||||
|
||||
useEffect(() => {
|
||||
if (window.self === window.top) {
|
||||
push('/');
|
||||
navigate('/')
|
||||
}
|
||||
document.title = 'Create a Crab Fit';
|
||||
document.title = 'Create a Crab Fit'
|
||||
|
||||
if (window.parent) {
|
||||
window.parent.postMessage('crabfit-create', '*');
|
||||
window.parent.postMessage('crabfit-create', '*')
|
||||
window.addEventListener('message', e => {
|
||||
if (e.data === 'safari-extension') {
|
||||
setShowFooter(false);
|
||||
setShowFooter(false)
|
||||
}
|
||||
}, {
|
||||
once: true
|
||||
});
|
||||
})
|
||||
}
|
||||
}, [push]);
|
||||
}, [navigate])
|
||||
|
||||
const onSubmit = async data => {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
setIsLoading(true)
|
||||
setError(null)
|
||||
try {
|
||||
const { start, end } = JSON.parse(data.times);
|
||||
const dates = JSON.parse(data.dates);
|
||||
const { start, end } = JSON.parse(data.times)
|
||||
const dates = JSON.parse(data.dates)
|
||||
|
||||
if (dates.length === 0) {
|
||||
return setError(t('home:form.errors.no_dates'));
|
||||
return setError(t('home:form.errors.no_dates'))
|
||||
}
|
||||
const isSpecificDates = typeof dates[0] === 'string' && dates[0].length === 8;
|
||||
const isSpecificDates = typeof dates[0] === 'string' && dates[0].length === 8
|
||||
if (start === end) {
|
||||
return setError(t('home:form.errors.same_times'));
|
||||
return setError(t('home:form.errors.same_times'))
|
||||
}
|
||||
|
||||
let times = dates.reduce((times, date) => {
|
||||
let day = [];
|
||||
const times = dates.reduce((times, date) => {
|
||||
const day = []
|
||||
for (let i = start; i < (start > end ? 24 : end); i++) {
|
||||
if (isSpecificDates) {
|
||||
day.push(
|
||||
dayjs.tz(date, 'DDMMYYYY', data.timezone)
|
||||
.hour(i).minute(0).utc().format('HHmm-DDMMYYYY')
|
||||
);
|
||||
.hour(i).minute(0).utc().format('HHmm-DDMMYYYY')
|
||||
)
|
||||
} else {
|
||||
day.push(
|
||||
dayjs().tz(data.timezone)
|
||||
.day(date).hour(i).minute(0).utc().format('HHmm-d')
|
||||
);
|
||||
.day(date).hour(i).minute(0).utc().format('HHmm-d')
|
||||
)
|
||||
}
|
||||
}
|
||||
if (start > end) {
|
||||
|
|
@ -108,46 +108,46 @@ const Create = ({ offline }) => {
|
|||
if (isSpecificDates) {
|
||||
day.push(
|
||||
dayjs.tz(date, 'DDMMYYYY', data.timezone)
|
||||
.hour(i).minute(0).utc().format('HHmm-DDMMYYYY')
|
||||
);
|
||||
.hour(i).minute(0).utc().format('HHmm-DDMMYYYY')
|
||||
)
|
||||
} else {
|
||||
day.push(
|
||||
dayjs().tz(data.timezone)
|
||||
.day(date).hour(i).minute(0).utc().format('HHmm-d')
|
||||
);
|
||||
.day(date).hour(i).minute(0).utc().format('HHmm-d')
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
return [...times, ...day];
|
||||
}, []);
|
||||
return [...times, ...day]
|
||||
}, [])
|
||||
|
||||
if (times.length === 0) {
|
||||
return setError(t('home:form.errors.no_time'));
|
||||
return setError(t('home:form.errors.no_time'))
|
||||
}
|
||||
|
||||
const response = await api.post('/event', {
|
||||
const event = await api.post('/event', {
|
||||
event: {
|
||||
name: data.name,
|
||||
times: times,
|
||||
timezone: data.timezone,
|
||||
},
|
||||
});
|
||||
setCreatedEvent(response.data);
|
||||
})
|
||||
setCreatedEvent(event)
|
||||
addRecent({
|
||||
id: response.data.id,
|
||||
created: response.data.created,
|
||||
name: response.data.name,
|
||||
});
|
||||
id: event.id,
|
||||
created: event.created,
|
||||
name: event.name,
|
||||
})
|
||||
gtag('event', 'create_event', {
|
||||
'event_category': 'create',
|
||||
});
|
||||
})
|
||||
} catch (e) {
|
||||
setError(t('home:form.errors.unknown'));
|
||||
console.error(e);
|
||||
setError(t('home:form.errors.unknown'))
|
||||
console.error(e)
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
setIsLoading(false)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
|
|
@ -162,16 +162,16 @@ const Create = ({ offline }) => {
|
|||
<h2>{createdEvent?.name}</h2>
|
||||
<ShareInfo
|
||||
onClick={() => navigator.clipboard?.writeText(`https://crab.fit/${createdEvent.id}`)
|
||||
.then(() => {
|
||||
setCopied(t('event:nav.copied'));
|
||||
setTimeout(() => setCopied(null), 1000);
|
||||
gtag('event', 'copy_link', {
|
||||
'event_category': 'event',
|
||||
});
|
||||
.then(() => {
|
||||
setCopied(t('event:nav.copied'))
|
||||
setTimeout(() => setCopied(null), 1000)
|
||||
gtag('event', 'copy_link', {
|
||||
'event_category': 'event',
|
||||
})
|
||||
.catch((e) => console.error('Failed to copy', e))
|
||||
})
|
||||
.catch(e => console.error('Failed to copy', e))
|
||||
}
|
||||
title={!!navigator.clipboard ? t('event:nav.title') : ''}
|
||||
title={navigator.clipboard ? t('event:nav.title') : ''}
|
||||
>{copied ?? `https://crab.fit/${createdEvent?.id}`}</ShareInfo>
|
||||
<ShareInfo>
|
||||
{/* eslint-disable-next-line */}
|
||||
|
|
@ -236,7 +236,7 @@ const Create = ({ offline }) => {
|
|||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
export default Create;
|
||||
export default Create
|
||||
|
|
@ -1,50 +1,50 @@
|
|||
import styled from '@emotion/styled';
|
||||
import { styled } from 'goober'
|
||||
|
||||
export const StyledMain = styled.div`
|
||||
export const StyledMain = styled('div')`
|
||||
width: 600px;
|
||||
margin: 10px auto;
|
||||
max-width: calc(100% - 30px);
|
||||
`;
|
||||
`
|
||||
|
||||
export const CreateForm = styled.form`
|
||||
export const CreateForm = styled('form')`
|
||||
margin: 0 0 30px;
|
||||
`;
|
||||
`
|
||||
|
||||
export const TitleSmall = styled.span`
|
||||
export const TitleSmall = styled('span')`
|
||||
display: block;
|
||||
margin: 0;
|
||||
font-size: 2rem;
|
||||
text-align: center;
|
||||
font-family: 'Samurai Bob', sans-serif;
|
||||
font-weight: 400;
|
||||
color: ${props => props.theme.primaryDark};
|
||||
color: var(--secondary);
|
||||
line-height: 1em;
|
||||
text-transform: uppercase;
|
||||
`;
|
||||
`
|
||||
|
||||
export const TitleLarge = styled.h1`
|
||||
export const TitleLarge = styled('h1')`
|
||||
margin: 0;
|
||||
font-size: 2rem;
|
||||
text-align: center;
|
||||
color: ${props => props.theme.primary};
|
||||
color: var(--primary);
|
||||
font-family: 'Molot', sans-serif;
|
||||
font-weight: 400;
|
||||
text-shadow: 0 3px 0 ${props => props.theme.primaryDark};
|
||||
text-shadow: 0 3px 0 var(--secondary);
|
||||
line-height: 1em;
|
||||
text-transform: uppercase;
|
||||
`;
|
||||
`
|
||||
|
||||
export const P = styled.p`
|
||||
export const P = styled('p')`
|
||||
font-weight: 500;
|
||||
line-height: 1.6em;
|
||||
`;
|
||||
`
|
||||
|
||||
export const OfflineMessage = styled.div`
|
||||
export const OfflineMessage = styled('div')`
|
||||
text-align: center;
|
||||
margin: 50px 0 20px;
|
||||
`;
|
||||
`
|
||||
|
||||
export const ShareInfo = styled.p`
|
||||
export const ShareInfo = styled('p')`
|
||||
margin: 6px 0;
|
||||
text-align: center;
|
||||
font-size: 15px;
|
||||
|
|
@ -54,7 +54,7 @@ export const ShareInfo = styled.p`
|
|||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
color: ${props.theme.primaryDark};
|
||||
color: var(--secondary);
|
||||
}
|
||||
`}
|
||||
`;
|
||||
`
|
||||
|
|
@ -1,12 +1,13 @@
|
|||
import { useForm } from 'react-hook-form';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useTranslation, Trans } from 'react-i18next';
|
||||
import { useForm } from 'react-hook-form'
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useTranslation, Trans } from 'react-i18next'
|
||||
import { useParams } from 'react-router-dom'
|
||||
|
||||
import dayjs from 'dayjs';
|
||||
import utc from 'dayjs/plugin/utc';
|
||||
import timezone from 'dayjs/plugin/timezone';
|
||||
import customParseFormat from 'dayjs/plugin/customParseFormat';
|
||||
import relativeTime from 'dayjs/plugin/relativeTime';
|
||||
import dayjs from 'dayjs'
|
||||
import utc from 'dayjs/plugin/utc'
|
||||
import timezone from 'dayjs/plugin/timezone'
|
||||
import customParseFormat from 'dayjs/plugin/customParseFormat'
|
||||
import relativeTime from 'dayjs/plugin/relativeTime'
|
||||
|
||||
import {
|
||||
Footer,
|
||||
|
|
@ -17,9 +18,9 @@ import {
|
|||
AvailabilityEditor,
|
||||
Error,
|
||||
Logo,
|
||||
} from 'components';
|
||||
} from '/src/components'
|
||||
|
||||
import { StyledMain } from '../Home/homeStyle';
|
||||
import { StyledMain } from '../Home/Home.styles'
|
||||
|
||||
import {
|
||||
EventName,
|
||||
|
|
@ -30,232 +31,226 @@ import {
|
|||
ShareInfo,
|
||||
Tabs,
|
||||
Tab,
|
||||
} from './eventStyle';
|
||||
} from './Event.styles'
|
||||
|
||||
import api from 'services';
|
||||
import { useSettingsStore, useRecentsStore, useLocaleUpdateStore } from 'stores';
|
||||
import api from '/src/services'
|
||||
import { useSettingsStore, useRecentsStore, useLocaleUpdateStore } from '/src/stores'
|
||||
|
||||
import timezones from 'res/timezones.json';
|
||||
import timezones from '/src/res/timezones.json'
|
||||
|
||||
dayjs.extend(utc);
|
||||
dayjs.extend(timezone);
|
||||
dayjs.extend(customParseFormat);
|
||||
dayjs.extend(relativeTime);
|
||||
dayjs.extend(utc)
|
||||
dayjs.extend(timezone)
|
||||
dayjs.extend(customParseFormat)
|
||||
dayjs.extend(relativeTime)
|
||||
|
||||
const Event = (props) => {
|
||||
const timeFormat = useSettingsStore(state => state.timeFormat);
|
||||
const weekStart = useSettingsStore(state => state.weekStart);
|
||||
const Event = () => {
|
||||
const timeFormat = useSettingsStore(state => state.timeFormat)
|
||||
const weekStart = useSettingsStore(state => state.weekStart)
|
||||
|
||||
const addRecent = useRecentsStore(state => state.addRecent);
|
||||
const removeRecent = useRecentsStore(state => state.removeRecent);
|
||||
const locale = useLocaleUpdateStore(state => state.locale);
|
||||
const addRecent = useRecentsStore(state => state.addRecent)
|
||||
const removeRecent = useRecentsStore(state => state.removeRecent)
|
||||
const locale = useLocaleUpdateStore(state => state.locale)
|
||||
|
||||
const { t } = useTranslation(['common', 'event']);
|
||||
const { t } = useTranslation(['common', 'event'])
|
||||
|
||||
const { register, handleSubmit, setFocus, reset } = useForm();
|
||||
const { id } = props.match.params;
|
||||
const { offline } = props;
|
||||
const [timezone, setTimezone] = useState(Intl.DateTimeFormat().resolvedOptions().timeZone);
|
||||
const [user, setUser] = useState(null);
|
||||
const [password, setPassword] = useState(null);
|
||||
const [tab, setTab] = useState(user ? 'you' : 'group');
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [isLoginLoading, setIsLoginLoading] = useState(false);
|
||||
const [error, setError] = useState(null);
|
||||
const [event, setEvent] = useState(null);
|
||||
const [people, setPeople] = useState([]);
|
||||
const { register, handleSubmit, setFocus, reset } = useForm()
|
||||
const { id } = useParams()
|
||||
const [timezone, setTimezone] = useState(Intl.DateTimeFormat().resolvedOptions().timeZone)
|
||||
const [user, setUser] = useState(null)
|
||||
const [password, setPassword] = useState(null)
|
||||
const [tab, setTab] = useState(user ? 'you' : 'group')
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
const [isLoginLoading, setIsLoginLoading] = useState(false)
|
||||
const [error, setError] = useState(null)
|
||||
const [event, setEvent] = useState(null)
|
||||
const [people, setPeople] = useState([])
|
||||
|
||||
const [times, setTimes] = useState([]);
|
||||
const [timeLabels, setTimeLabels] = useState([]);
|
||||
const [dates, setDates] = useState([]);
|
||||
const [min, setMin] = useState(0);
|
||||
const [max, setMax] = useState(0);
|
||||
const [times, setTimes] = useState([])
|
||||
const [timeLabels, setTimeLabels] = useState([])
|
||||
const [dates, setDates] = useState([])
|
||||
const [min, setMin] = useState(0)
|
||||
const [max, setMax] = useState(0)
|
||||
|
||||
const [copied, setCopied] = useState(null);
|
||||
const [copied, setCopied] = useState(null)
|
||||
|
||||
useEffect(() => {
|
||||
const fetchEvent = async () => {
|
||||
try {
|
||||
const response = await api.get(`/event/${id}`);
|
||||
const event = await api.get(`/event/${id}`)
|
||||
|
||||
setEvent(response.data);
|
||||
setEvent(event)
|
||||
addRecent({
|
||||
id: response.data.id,
|
||||
created: response.data.created,
|
||||
name: response.data.name,
|
||||
});
|
||||
document.title = `${response.data.name} | Crab Fit`;
|
||||
id: event.id,
|
||||
created: event.created,
|
||||
name: event.name,
|
||||
})
|
||||
document.title = `${event.name} | Crab Fit`
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
console.error(e)
|
||||
if (e.status === 404) {
|
||||
removeRecent(id);
|
||||
removeRecent(id)
|
||||
}
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
setIsLoading(false)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
fetchEvent();
|
||||
}, [id, addRecent, removeRecent]);
|
||||
fetchEvent()
|
||||
}, [id, addRecent, removeRecent])
|
||||
|
||||
useEffect(() => {
|
||||
const fetchPeople = async () => {
|
||||
try {
|
||||
const response = await api.get(`/event/${id}/people`);
|
||||
const adjustedPeople = response.data.people.map(person => ({
|
||||
const { people } = await api.get(`/event/${id}/people`)
|
||||
const adjustedPeople = people.map(person => ({
|
||||
...person,
|
||||
availability: (!!person.availability.length && person.availability[0].length === 13)
|
||||
? person.availability.map(date => dayjs(date, 'HHmm-DDMMYYYY').utc(true).tz(timezone).format('HHmm-DDMMYYYY'))
|
||||
: person.availability.map(date => dayjs(date, 'HHmm').day(date.substring(5)).utc(true).tz(timezone).format('HHmm-d')),
|
||||
}));
|
||||
setPeople(adjustedPeople);
|
||||
}))
|
||||
setPeople(adjustedPeople)
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
|
||||
if (tab === 'group') {
|
||||
fetchPeople();
|
||||
fetchPeople()
|
||||
}
|
||||
}, [tab, id, timezone]);
|
||||
}, [tab, id, timezone])
|
||||
|
||||
// Convert to timezone and expand minute segments
|
||||
useEffect(() => {
|
||||
if (event) {
|
||||
const isSpecificDates = event.times[0].length === 13;
|
||||
const isSpecificDates = event.times[0].length === 13
|
||||
setTimes(event.times.reduce(
|
||||
(allTimes, time) => {
|
||||
const date = isSpecificDates ?
|
||||
dayjs(time, 'HHmm-DDMMYYYY').utc(true).tz(timezone)
|
||||
: dayjs(time, 'HHmm').day(time.substring(5)).utc(true).tz(timezone);
|
||||
const format = isSpecificDates ? 'HHmm-DDMMYYYY' : 'HHmm-d';
|
||||
: dayjs(time, 'HHmm').day(time.substring(5)).utc(true).tz(timezone)
|
||||
const format = isSpecificDates ? 'HHmm-DDMMYYYY' : 'HHmm-d'
|
||||
return [
|
||||
...allTimes,
|
||||
date.minute(0).format(format),
|
||||
date.minute(15).format(format),
|
||||
date.minute(30).format(format),
|
||||
date.minute(45).format(format),
|
||||
];
|
||||
]
|
||||
},
|
||||
[]
|
||||
).sort((a, b) => {
|
||||
if (isSpecificDates) {
|
||||
return dayjs(a, 'HHmm-DDMMYYYY').diff(dayjs(b, 'HHmm-DDMMYYYY'));
|
||||
return dayjs(a, 'HHmm-DDMMYYYY').diff(dayjs(b, 'HHmm-DDMMYYYY'))
|
||||
} else {
|
||||
return dayjs(a, 'HHmm').day((parseInt(a.substring(5))-weekStart % 7 + 7) % 7)
|
||||
.diff(dayjs(b, 'HHmm').day((parseInt(b.substring(5))-weekStart % 7 + 7) % 7));
|
||||
.diff(dayjs(b, 'HHmm').day((parseInt(b.substring(5))-weekStart % 7 + 7) % 7))
|
||||
}
|
||||
}));
|
||||
}))
|
||||
}
|
||||
}, [event, timezone, weekStart]);
|
||||
}, [event, timezone, weekStart])
|
||||
|
||||
useEffect(() => {
|
||||
if (!!times.length && !!people.length) {
|
||||
setMin(times.reduce((min, time) => {
|
||||
let total = people.reduce(
|
||||
(total, person) => person.availability.includes(time) ? total+1 : total,
|
||||
0
|
||||
);
|
||||
return total < min ? total : min;
|
||||
},
|
||||
Infinity
|
||||
));
|
||||
const total = people.reduce(
|
||||
(total, person) => person.availability.includes(time) ? total+1 : total,
|
||||
0
|
||||
)
|
||||
return total < min ? total : min
|
||||
}, Infinity))
|
||||
setMax(times.reduce((max, time) => {
|
||||
let total = people.reduce(
|
||||
(total, person) => person.availability.includes(time) ? total+1 : total,
|
||||
0
|
||||
);
|
||||
return total > max ? total : max;
|
||||
},
|
||||
-Infinity
|
||||
));
|
||||
const total = people.reduce(
|
||||
(total, person) => person.availability.includes(time) ? total+1 : total,
|
||||
0
|
||||
)
|
||||
return total > max ? total : max
|
||||
}, -Infinity))
|
||||
}
|
||||
}, [times, people]);
|
||||
}, [times, people])
|
||||
|
||||
useEffect(() => {
|
||||
if (!!times.length) {
|
||||
if (times.length) {
|
||||
setTimeLabels(times.reduce((labels, datetime) => {
|
||||
const time = datetime.substring(0, 4);
|
||||
if (labels.includes(time)) return labels;
|
||||
return [...labels, time];
|
||||
const time = datetime.substring(0, 4)
|
||||
if (labels.includes(time)) return labels
|
||||
return [...labels, time]
|
||||
}, [])
|
||||
.sort((a, b) => parseInt(a) - parseInt(b))
|
||||
.reduce((labels, time, i, allTimes) => {
|
||||
if (time.substring(2) === '30') return [...labels, { label: '', time }];
|
||||
if (allTimes.length - 1 === i) return [
|
||||
...labels,
|
||||
{ label: '', time },
|
||||
{ label: dayjs(time, 'HHmm').add(1, 'hour').format(timeFormat === '12h' ? 'h A' : 'HH'), time: null }
|
||||
];
|
||||
if (allTimes.length - 1 > i && parseInt(allTimes[i+1].substring(0, 2))-1 > parseInt(time.substring(0, 2))) return [
|
||||
...labels,
|
||||
{ label: '', time },
|
||||
{ label: dayjs(time, 'HHmm').add(1, 'hour').format(timeFormat === '12h' ? 'h A' : 'HH'), time: 'space' },
|
||||
{ label: '', time: 'space' },
|
||||
{ label: '', time: 'space' },
|
||||
];
|
||||
if (time.substring(2) !== '00') return [...labels, { label: '', time }];
|
||||
return [...labels, { label: dayjs(time, 'HHmm').format(timeFormat === '12h' ? 'h A' : 'HH'), time }];
|
||||
}, []));
|
||||
.sort((a, b) => parseInt(a) - parseInt(b))
|
||||
.reduce((labels, time, i, allTimes) => {
|
||||
if (time.substring(2) === '30') return [...labels, { label: '', time }]
|
||||
if (allTimes.length - 1 === i) return [
|
||||
...labels,
|
||||
{ label: '', time },
|
||||
{ label: dayjs(time, 'HHmm').add(1, 'hour').format(timeFormat === '12h' ? 'h A' : 'HH'), time: null }
|
||||
]
|
||||
if (allTimes.length - 1 > i && parseInt(allTimes[i+1].substring(0, 2))-1 > parseInt(time.substring(0, 2))) return [
|
||||
...labels,
|
||||
{ label: '', time },
|
||||
{ label: dayjs(time, 'HHmm').add(1, 'hour').format(timeFormat === '12h' ? 'h A' : 'HH'), time: 'space' },
|
||||
{ label: '', time: 'space' },
|
||||
{ label: '', time: 'space' },
|
||||
]
|
||||
if (time.substring(2) !== '00') return [...labels, { label: '', time }]
|
||||
return [...labels, { label: dayjs(time, 'HHmm').format(timeFormat === '12h' ? 'h A' : 'HH'), time }]
|
||||
}, []))
|
||||
|
||||
setDates(times.reduce((allDates, time) => {
|
||||
if (time.substring(2, 4) !== '00') return allDates;
|
||||
const date = time.substring(5);
|
||||
if (allDates.includes(date)) return allDates;
|
||||
return [...allDates, date];
|
||||
}, []));
|
||||
if (time.substring(2, 4) !== '00') return allDates
|
||||
const date = time.substring(5)
|
||||
if (allDates.includes(date)) return allDates
|
||||
return [...allDates, date]
|
||||
}, []))
|
||||
}
|
||||
}, [times, timeFormat, locale]);
|
||||
}, [times, timeFormat, locale])
|
||||
|
||||
useEffect(() => {
|
||||
const fetchUser = async () => {
|
||||
try {
|
||||
const response = await api.post(`/event/${id}/people/${user.name}`, { person: { password } });
|
||||
const resUser = await api.post(`/event/${id}/people/${user.name}`, { person: { password } })
|
||||
const adjustedUser = {
|
||||
...response.data,
|
||||
availability: (!!response.data.availability.length && response.data.availability[0].length === 13)
|
||||
? response.data.availability.map(date => dayjs(date, 'HHmm-DDMMYYYY').utc(true).tz(timezone).format('HHmm-DDMMYYYY'))
|
||||
: response.data.availability.map(date => dayjs(date, 'HHmm').day(date.substring(5)).utc(true).tz(timezone).format('HHmm-d')),
|
||||
};
|
||||
setUser(adjustedUser);
|
||||
...resUser,
|
||||
availability: (!!resUser.availability.length && resUser.availability[0].length === 13)
|
||||
? resUser.availability.map(date => dayjs(date, 'HHmm-DDMMYYYY').utc(true).tz(timezone).format('HHmm-DDMMYYYY'))
|
||||
: resUser.availability.map(date => dayjs(date, 'HHmm').day(date.substring(5)).utc(true).tz(timezone).format('HHmm-d')),
|
||||
}
|
||||
setUser(adjustedUser)
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
console.log(e)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
if (user) {
|
||||
fetchUser();
|
||||
fetchUser()
|
||||
}
|
||||
// eslint-disable-next-line
|
||||
}, [timezone]);
|
||||
}, [timezone])
|
||||
|
||||
const onSubmit = async data => {
|
||||
if (!data.name || data.name.length === 0) {
|
||||
setFocus('name');
|
||||
return setError(t('event:form.errors.name_required'));
|
||||
setFocus('name')
|
||||
return setError(t('event:form.errors.name_required'))
|
||||
}
|
||||
|
||||
setIsLoginLoading(true);
|
||||
setError(null);
|
||||
setIsLoginLoading(true)
|
||||
setError(null)
|
||||
|
||||
try {
|
||||
const response = await api.post(`/event/${id}/people/${data.name}`, {
|
||||
const resUser = await api.post(`/event/${id}/people/${data.name}`, {
|
||||
person: {
|
||||
password: data.password,
|
||||
},
|
||||
});
|
||||
setPassword(data.password);
|
||||
})
|
||||
setPassword(data.password)
|
||||
const adjustedUser = {
|
||||
...response.data,
|
||||
availability: (!!response.data.availability.length && response.data.availability[0].length === 13)
|
||||
? response.data.availability.map(date => dayjs(date, 'HHmm-DDMMYYYY').utc(true).tz(timezone).format('HHmm-DDMMYYYY'))
|
||||
: response.data.availability.map(date => dayjs(date, 'HHmm').day(date.substring(5)).utc(true).tz(timezone).format('HHmm-d')),
|
||||
};
|
||||
setUser(adjustedUser);
|
||||
setTab('you');
|
||||
...resUser,
|
||||
availability: (!!resUser.availability.length && resUser.availability[0].length === 13)
|
||||
? resUser.availability.map(date => dayjs(date, 'HHmm-DDMMYYYY').utc(true).tz(timezone).format('HHmm-DDMMYYYY'))
|
||||
: resUser.availability.map(date => dayjs(date, 'HHmm').day(date.substring(5)).utc(true).tz(timezone).format('HHmm-d')),
|
||||
}
|
||||
setUser(adjustedUser)
|
||||
setTab('you')
|
||||
} catch (e) {
|
||||
if (e.status === 401) {
|
||||
setError(t('event:form.errors.password_incorrect'));
|
||||
setError(t('event:form.errors.password_incorrect'))
|
||||
} else if (e.status === 404) {
|
||||
// Create user
|
||||
try {
|
||||
|
|
@ -264,25 +259,25 @@ const Event = (props) => {
|
|||
name: data.name,
|
||||
password: data.password,
|
||||
},
|
||||
});
|
||||
setPassword(data.password);
|
||||
})
|
||||
setPassword(data.password)
|
||||
setUser({
|
||||
name: data.name,
|
||||
availability: [],
|
||||
});
|
||||
setTab('you');
|
||||
})
|
||||
setTab('you')
|
||||
} catch (e) {
|
||||
setError(t('event:form.errors.unknown'));
|
||||
setError(t('event:form.errors.unknown'))
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
setIsLoginLoading(false);
|
||||
setIsLoginLoading(false)
|
||||
gtag('event', 'login', {
|
||||
'event_category': 'event',
|
||||
});
|
||||
reset();
|
||||
})
|
||||
reset()
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
|
|
@ -291,39 +286,32 @@ const Event = (props) => {
|
|||
|
||||
{(!!event || isLoading) ? (
|
||||
<>
|
||||
<EventName isLoading={isLoading}>{event?.name}</EventName>
|
||||
<EventDate isLoading={isLoading} locale={locale} title={event?.created && dayjs.unix(event?.created).format('D MMMM, YYYY')}>{event?.created && t('common:created', { date: dayjs.unix(event?.created).fromNow() })}</EventDate>
|
||||
<EventName $isLoading={isLoading}>{event?.name}</EventName>
|
||||
<EventDate $isLoading={isLoading} locale={locale} title={event?.created && dayjs.unix(event?.created).format('D MMMM, YYYY')}>{event?.created && t('common:created', { date: dayjs.unix(event?.created).fromNow() })}</EventDate>
|
||||
<ShareInfo
|
||||
onClick={() => navigator.clipboard?.writeText(`https://crab.fit/${id}`)
|
||||
.then(() => {
|
||||
setCopied(t('event:nav.copied'));
|
||||
setTimeout(() => setCopied(null), 1000);
|
||||
gtag('event', 'copy_link', {
|
||||
'event_category': 'event',
|
||||
});
|
||||
.then(() => {
|
||||
setCopied(t('event:nav.copied'))
|
||||
setTimeout(() => setCopied(null), 1000)
|
||||
gtag('event', 'copy_link', {
|
||||
'event_category': 'event',
|
||||
})
|
||||
.catch((e) => console.error('Failed to copy', e))
|
||||
})
|
||||
.catch(e => console.error('Failed to copy', e))
|
||||
}
|
||||
title={!!navigator.clipboard ? t('event:nav.title') : ''}
|
||||
title={navigator.clipboard ? t('event:nav.title') : ''}
|
||||
>{copied ?? `https://crab.fit/${id}`}</ShareInfo>
|
||||
<ShareInfo isLoading={isLoading} className="instructions">
|
||||
<ShareInfo $isLoading={isLoading} className="instructions">
|
||||
{!!event?.name &&
|
||||
<Trans i18nKey="event:nav.shareinfo">Copy the link to this page, or share via <a onClick={() => gtag('event', 'send_email', { 'event_category': 'event' })} href={`mailto:?subject=${encodeURIComponent(t('event:nav.email_subject', { event_name: event?.name }))}&body=${encodeURIComponent(`${t('event:nav.email_body')} https://crab.fit/${id}`)}`}>email</a>.</Trans>
|
||||
}
|
||||
</ShareInfo>
|
||||
</>
|
||||
) : (
|
||||
offline ? (
|
||||
<div style={{ margin: '100px 0' }}>
|
||||
<EventName>{t('event:offline.title')}</EventName>
|
||||
<ShareInfo><Trans i18nKey="event:offline.body" /></ShareInfo>
|
||||
</div>
|
||||
) : (
|
||||
<div style={{ margin: '100px 0' }}>
|
||||
<EventName>{t('event:error.title')}</EventName>
|
||||
<ShareInfo>{t('event:error.body')}</ShareInfo>
|
||||
</div>
|
||||
)
|
||||
<div style={{ margin: '100px 0' }}>
|
||||
<EventName>{t('event:error.title')}</EventName>
|
||||
<ShareInfo>{t('event:error.body')}</ShareInfo>
|
||||
</div>
|
||||
)}
|
||||
</StyledMain>
|
||||
|
||||
|
|
@ -335,9 +323,9 @@ const Event = (props) => {
|
|||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', margin: '20px 0', flexWrap: 'wrap', gap: '10px' }}>
|
||||
<h2 style={{ margin: 0 }}>{t('event:form.signed_in', { name: user.name })}</h2>
|
||||
<Button small onClick={() => {
|
||||
setTab('group');
|
||||
setUser(null);
|
||||
setPassword(null);
|
||||
setTab('group')
|
||||
setUser(null)
|
||||
setPassword(null)
|
||||
}}>{t('event:form.logout_button')}</Button>
|
||||
</div>
|
||||
) : (
|
||||
|
|
@ -363,7 +351,7 @@ const Event = (props) => {
|
|||
|
||||
<Button
|
||||
type="submit"
|
||||
isLoading={isLoginLoading}
|
||||
$isLoading={isLoginLoading}
|
||||
disabled={isLoginLoading || isLoading}
|
||||
>{t('event:form.button')}</Button>
|
||||
</LoginForm>
|
||||
|
|
@ -383,8 +371,8 @@ const Event = (props) => {
|
|||
/>
|
||||
{/* eslint-disable-next-line */}
|
||||
{event?.timezone && event.timezone !== timezone && <p><Trans i18nKey="event:form.created_in_timezone">This event was created in the timezone <strong>{{timezone: event.timezone}}</strong>. <a href="#" onClick={e => {
|
||||
e.preventDefault();
|
||||
setTimezone(event.timezone);
|
||||
e.preventDefault()
|
||||
setTimezone(event.timezone)
|
||||
}}>Click here</a> to use it.</Trans></p>}
|
||||
{((
|
||||
Intl.DateTimeFormat().resolvedOptions().timeZone !== timezone
|
||||
|
|
@ -395,8 +383,8 @@ const Event = (props) => {
|
|||
)) && (
|
||||
/* eslint-disable-next-line */
|
||||
<p><Trans i18nKey="event:form.local_timezone">Your local timezone is detected to be <strong>{{timezone: Intl.DateTimeFormat().resolvedOptions().timeZone}}</strong>. <a href="#" onClick={e => {
|
||||
e.preventDefault();
|
||||
setTimezone(Intl.DateTimeFormat().resolvedOptions().timeZone);
|
||||
e.preventDefault()
|
||||
setTimezone(Intl.DateTimeFormat().resolvedOptions().timeZone)
|
||||
}}>Click here</a> to use it.</Trans></p>
|
||||
)}
|
||||
</StyledMain>
|
||||
|
|
@ -407,24 +395,24 @@ const Event = (props) => {
|
|||
<Tab
|
||||
href="#you"
|
||||
onClick={e => {
|
||||
e.preventDefault();
|
||||
e.preventDefault()
|
||||
if (user) {
|
||||
setTab('you');
|
||||
setTab('you')
|
||||
} else {
|
||||
setFocus('name');
|
||||
setFocus('name')
|
||||
}
|
||||
}}
|
||||
selected={tab === 'you'}
|
||||
$selected={tab === 'you'}
|
||||
disabled={!user}
|
||||
title={user ? '' : t('event:tabs.you_tooltip')}
|
||||
>{t('event:tabs.you')}</Tab>
|
||||
<Tab
|
||||
href="#group"
|
||||
onClick={e => {
|
||||
e.preventDefault();
|
||||
setTab('group');
|
||||
e.preventDefault()
|
||||
setTab('group')
|
||||
}}
|
||||
selected={tab === 'group'}
|
||||
$selected={tab === 'group'}
|
||||
>{t('event:tabs.group')}</Tab>
|
||||
</Tabs>
|
||||
</StyledMain>
|
||||
|
|
@ -451,21 +439,21 @@ const Event = (props) => {
|
|||
isSpecificDates={!!dates.length && dates[0].length === 8}
|
||||
value={user.availability}
|
||||
onChange={async availability => {
|
||||
const oldAvailability = [...user.availability];
|
||||
const oldAvailability = [...user.availability]
|
||||
const utcAvailability = (!!availability.length && availability[0].length === 13)
|
||||
? availability.map(date => dayjs.tz(date, 'HHmm-DDMMYYYY', timezone).utc().format('HHmm-DDMMYYYY'))
|
||||
: availability.map(date => dayjs.tz(date, 'HHmm', timezone).day(date.substring(5)).utc().format('HHmm-d'));
|
||||
setUser({ ...user, availability });
|
||||
: availability.map(date => dayjs.tz(date, 'HHmm', timezone).day(date.substring(5)).utc().format('HHmm-d'))
|
||||
setUser({ ...user, availability })
|
||||
try {
|
||||
await api.patch(`/event/${id}/people/${user.name}`, {
|
||||
person: {
|
||||
password,
|
||||
availability: utcAvailability,
|
||||
},
|
||||
});
|
||||
})
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
setUser({ ...user, oldAvailability });
|
||||
console.log(e)
|
||||
setUser({ ...user, oldAvailability })
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
|
@ -476,7 +464,7 @@ const Event = (props) => {
|
|||
|
||||
<Footer />
|
||||
</>
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
export default Event;
|
||||
export default Event
|
||||
|
|
@ -1,24 +1,24 @@
|
|||
import styled from '@emotion/styled';
|
||||
import { styled } from 'goober'
|
||||
|
||||
export const EventName = styled.h1`
|
||||
export const EventName = styled('h1')`
|
||||
text-align: center;
|
||||
font-weight: 800;
|
||||
margin: 20px 0 5px;
|
||||
|
||||
${props => props.isLoading && `
|
||||
${props => props.$isLoading && `
|
||||
&:after {
|
||||
content: '';
|
||||
display: inline-block;
|
||||
height: 1em;
|
||||
width: 400px;
|
||||
max-width: 100%;
|
||||
background-color: ${props.theme.loading};
|
||||
background-color: var(--loading);
|
||||
border-radius: 3px;
|
||||
}
|
||||
`}
|
||||
`;
|
||||
`
|
||||
|
||||
export const EventDate = styled.span`
|
||||
export const EventDate = styled('span')`
|
||||
display: block;
|
||||
text-align: center;
|
||||
font-size: 14px;
|
||||
|
|
@ -27,14 +27,14 @@ export const EventDate = styled.span`
|
|||
font-weight: 500;
|
||||
letter-spacing: .01em;
|
||||
|
||||
${props => props.isLoading && `
|
||||
${props => props.$isLoading && `
|
||||
&:after {
|
||||
content: '';
|
||||
display: inline-block;
|
||||
height: 1em;
|
||||
width: 200px;
|
||||
max-width: 100%;
|
||||
background-color: ${props.theme.loading};
|
||||
background-color: var(--loading);
|
||||
border-radius: 3px;
|
||||
}
|
||||
`}
|
||||
|
|
@ -44,9 +44,9 @@ export const EventDate = styled.span`
|
|||
content: ' - ' attr(title);
|
||||
}
|
||||
}
|
||||
`;
|
||||
`
|
||||
|
||||
export const LoginForm = styled.form`
|
||||
export const LoginForm = styled('form')`
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr auto;
|
||||
align-items: flex-end;
|
||||
|
|
@ -62,36 +62,36 @@ export const LoginForm = styled.form`
|
|||
--btn-width: 100%;
|
||||
}
|
||||
}
|
||||
`;
|
||||
`
|
||||
|
||||
export const LoginSection = styled.section`
|
||||
background-color: ${props => props.theme.primaryBackground};
|
||||
export const LoginSection = styled('section')`
|
||||
background-color: var(--surface);
|
||||
padding: 10px 0;
|
||||
|
||||
@media print {
|
||||
display: none;
|
||||
}
|
||||
`;
|
||||
`
|
||||
|
||||
export const Info = styled.p`
|
||||
export const Info = styled('p')`
|
||||
margin: 18px 0;
|
||||
opacity: .6;
|
||||
font-size: 12px;
|
||||
`;
|
||||
`
|
||||
|
||||
export const ShareInfo = styled.p`
|
||||
export const ShareInfo = styled('p')`
|
||||
margin: 6px 0;
|
||||
text-align: center;
|
||||
font-size: 15px;
|
||||
|
||||
${props => props.isLoading && `
|
||||
${props => props.$isLoading && `
|
||||
&:after {
|
||||
content: '';
|
||||
display: inline-block;
|
||||
height: 1em;
|
||||
width: 300px;
|
||||
max-width: 100%;
|
||||
background-color: ${props.theme.loading};
|
||||
background-color: var(--loading);
|
||||
border-radius: 3px;
|
||||
}
|
||||
`}
|
||||
|
|
@ -100,7 +100,7 @@ export const ShareInfo = styled.p`
|
|||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
color: ${props.theme.mode === 'light' ? props.theme.primaryDark : props.theme.primaryLight};
|
||||
color: var(--secondary);
|
||||
}
|
||||
`}
|
||||
|
||||
|
|
@ -109,9 +109,9 @@ export const ShareInfo = styled.p`
|
|||
display: none;
|
||||
}
|
||||
}
|
||||
`;
|
||||
`
|
||||
|
||||
export const Tabs = styled.div`
|
||||
export const Tabs = styled('div')`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
|
@ -120,29 +120,29 @@ export const Tabs = styled.div`
|
|||
@media print {
|
||||
display: none;
|
||||
}
|
||||
`;
|
||||
`
|
||||
|
||||
export const Tab = styled.a`
|
||||
export const Tab = styled('a')`
|
||||
user-select: none;
|
||||
text-decoration: none;
|
||||
display: block;
|
||||
color: ${props => props.theme.text};
|
||||
color: var(--text);
|
||||
padding: 8px 18px;
|
||||
background-color: ${props => props.theme.primaryBackground};
|
||||
border: 1px solid ${props => props.theme.primary};
|
||||
background-color: var(--surface);
|
||||
border: 1px solid var(--primary);
|
||||
border-bottom: 0;
|
||||
margin: 0 4px;
|
||||
border-top-left-radius: 5px;
|
||||
border-top-right-radius: 5px;
|
||||
|
||||
${props => props.selected && `
|
||||
${props => props.$selected && `
|
||||
color: #FFF;
|
||||
background-color: ${props.theme.primary};
|
||||
border-color: ${props.theme.primary};
|
||||
background-color: var(--primary);
|
||||
border-color: var(--primary);
|
||||
`}
|
||||
|
||||
${props => props.disabled && `
|
||||
opacity: .5;
|
||||
cursor: not-allowed;
|
||||
`}
|
||||
`;
|
||||
`
|
||||
101
crabfit-frontend/src/pages/Help/Help.jsx
Normal file
101
crabfit-frontend/src/pages/Help/Help.jsx
Normal file
File diff suppressed because one or more lines are too long
|
|
@ -1,13 +1,13 @@
|
|||
import styled from '@emotion/styled';
|
||||
import { styled } from 'goober'
|
||||
|
||||
export const Step = styled.h2`
|
||||
text-decoration-color: ${props => props.theme.primary};
|
||||
export const Step = styled('h2')`
|
||||
text-decoration-color: var(--primary);
|
||||
text-decoration-style: solid;
|
||||
text-decoration-line: underline;
|
||||
margin-top: 30px;
|
||||
`;
|
||||
`
|
||||
|
||||
export const FakeCalendar = styled.div`
|
||||
export const FakeCalendar = styled('div')`
|
||||
user-select: none;
|
||||
|
||||
& div {
|
||||
|
|
@ -28,8 +28,8 @@ export const FakeCalendar = styled.div`
|
|||
}
|
||||
}
|
||||
& .dates span {
|
||||
background-color: ${props => props.theme.primaryBackground};
|
||||
border: 1px solid ${props => props.theme.primary};
|
||||
background-color: var(--surface);
|
||||
border: 1px solid var(--primary);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
|
@ -37,7 +37,7 @@ export const FakeCalendar = styled.div`
|
|||
|
||||
&.selected {
|
||||
color: #FFF;
|
||||
background-color: ${props => props.theme.primary};
|
||||
background-color: var(--primary);
|
||||
}
|
||||
}
|
||||
& .dates span:first-of-type {
|
||||
|
|
@ -48,12 +48,12 @@ export const FakeCalendar = styled.div`
|
|||
border-end-end-radius: 3px;
|
||||
border-start-end-radius: 3px;
|
||||
}
|
||||
`;
|
||||
`
|
||||
|
||||
export const FakeTimeRange = styled.div`
|
||||
export const FakeTimeRange = styled('div')`
|
||||
user-select: none;
|
||||
background-color: ${props => props.theme.primaryBackground};
|
||||
border: 1px solid ${props => props.theme.primary};
|
||||
background-color: var(--surface);
|
||||
border: 1px solid var(--primary);
|
||||
border-radius: 3px;
|
||||
height: 50px;
|
||||
position: relative;
|
||||
|
|
@ -62,8 +62,8 @@ export const FakeTimeRange = styled.div`
|
|||
& div {
|
||||
height: calc(100% + 20px);
|
||||
width: 20px;
|
||||
border: 1px solid ${props => props.theme.primary};
|
||||
background-color: ${props => props.theme.primaryLight};
|
||||
border: 1px solid var(--primary);
|
||||
background-color: var(--highlight);
|
||||
border-radius: 3px;
|
||||
position: absolute;
|
||||
top: -10px;
|
||||
|
|
@ -79,7 +79,7 @@ export const FakeTimeRange = styled.div`
|
|||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: ${props => props.theme.primaryDark};
|
||||
color: var(--shadow);
|
||||
}
|
||||
|
||||
&:before {
|
||||
|
|
@ -92,25 +92,25 @@ export const FakeTimeRange = styled.div`
|
|||
}
|
||||
}
|
||||
& .start {
|
||||
left: calc(${11 * 4.1666666666666666}% - 11px);
|
||||
left: calc(${11 * 4.166}% - 11px);
|
||||
}
|
||||
& .end {
|
||||
left: calc(${17 * 4.1666666666666666}% - 11px);
|
||||
left: calc(${17 * 4.166}% - 11px);
|
||||
}
|
||||
&:before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
left: ${11 * 4.1666666666666666}%;
|
||||
right: calc(100% - ${17 * 4.1666666666666666}%);
|
||||
left: ${11 * 4.166}%;
|
||||
right: calc(100% - ${17 * 4.166}%);
|
||||
top: 0;
|
||||
background-color: ${props => props.theme.primary};
|
||||
background-color: var(--primary);
|
||||
border-radius: 2px;
|
||||
}
|
||||
`;
|
||||
`
|
||||
|
||||
export const ButtonArea = styled.div`
|
||||
export const ButtonArea = styled('div')`
|
||||
@media print {
|
||||
display: none;
|
||||
}
|
||||
`;
|
||||
`
|
||||
File diff suppressed because one or more lines are too long
|
|
@ -1,12 +1,12 @@
|
|||
import { useEffect, useState } from 'react';
|
||||
import { useHistory, Link } from 'react-router-dom';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useTranslation, Trans } from 'react-i18next';
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useNavigate, Link } from 'react-router-dom'
|
||||
import { useForm } from 'react-hook-form'
|
||||
import { useTranslation, Trans } from 'react-i18next'
|
||||
|
||||
import dayjs from 'dayjs';
|
||||
import utc from 'dayjs/plugin/utc';
|
||||
import timezone from 'dayjs/plugin/timezone';
|
||||
import customParseFormat from 'dayjs/plugin/customParseFormat';
|
||||
import dayjs from 'dayjs'
|
||||
import utc from 'dayjs/plugin/utc'
|
||||
import timezone from 'dayjs/plugin/timezone'
|
||||
import customParseFormat from 'dayjs/plugin/customParseFormat'
|
||||
|
||||
import {
|
||||
TextField,
|
||||
|
|
@ -18,7 +18,7 @@ import {
|
|||
Error,
|
||||
Footer,
|
||||
Recents,
|
||||
} from 'components';
|
||||
} from '/src/components'
|
||||
|
||||
import {
|
||||
StyledMain,
|
||||
|
|
@ -37,82 +37,82 @@ import {
|
|||
ButtonArea,
|
||||
VideoWrapper,
|
||||
VideoLink,
|
||||
} from './homeStyle';
|
||||
} from './Home.styles'
|
||||
|
||||
import api from 'services';
|
||||
import { detect_browser } from 'utils';
|
||||
import { useTWAStore } from 'stores';
|
||||
import api from '/src/services'
|
||||
import { detect_browser } from '/src/utils'
|
||||
import { useTWAStore } from '/src/stores'
|
||||
|
||||
import logo from 'res/logo.svg';
|
||||
import video_thumb from 'res/video_thumb.jpg';
|
||||
import timezones from 'res/timezones.json';
|
||||
import logo from '/src/res/logo.svg'
|
||||
import video_thumb from '/src/res/video_thumb.jpg'
|
||||
import timezones from '/src/res/timezones.json'
|
||||
|
||||
dayjs.extend(utc);
|
||||
dayjs.extend(timezone);
|
||||
dayjs.extend(customParseFormat);
|
||||
dayjs.extend(utc)
|
||||
dayjs.extend(timezone)
|
||||
dayjs.extend(customParseFormat)
|
||||
|
||||
const Home = ({ offline }) => {
|
||||
const { register, handleSubmit, setValue } = useForm({
|
||||
defaultValues: {
|
||||
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
||||
},
|
||||
});
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState(null);
|
||||
})
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [error, setError] = useState(null)
|
||||
const [stats, setStats] = useState({
|
||||
eventCount: null,
|
||||
personCount: null,
|
||||
version: 'loading...',
|
||||
});
|
||||
const [browser, setBrowser] = useState(undefined);
|
||||
const [videoPlay, setVideoPlay] = useState(false);
|
||||
const { push } = useHistory();
|
||||
const { t } = useTranslation(['common', 'home']);
|
||||
const isTWA = useTWAStore(state => state.TWA);
|
||||
})
|
||||
const [browser, setBrowser] = useState(undefined)
|
||||
const [videoPlay, setVideoPlay] = useState(false)
|
||||
const navigate = useNavigate()
|
||||
const { t } = useTranslation(['common', 'home'])
|
||||
const isTWA = useTWAStore(state => state.TWA)
|
||||
|
||||
useEffect(() => {
|
||||
const fetch = async () => {
|
||||
try {
|
||||
const response = await api.get('/stats');
|
||||
setStats(response.data);
|
||||
const response = await api.get('/stats')
|
||||
setStats(response)
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
console.error(e)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
fetch();
|
||||
document.title = 'Crab Fit';
|
||||
setBrowser(detect_browser());
|
||||
}, []);
|
||||
fetch()
|
||||
document.title = 'Crab Fit'
|
||||
setBrowser(detect_browser())
|
||||
}, [])
|
||||
|
||||
const onSubmit = async data => {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
setIsLoading(true)
|
||||
setError(null)
|
||||
try {
|
||||
const { start, end } = JSON.parse(data.times);
|
||||
const dates = JSON.parse(data.dates);
|
||||
const { start, end } = JSON.parse(data.times)
|
||||
const dates = JSON.parse(data.dates)
|
||||
|
||||
if (dates.length === 0) {
|
||||
return setError(t('home:form.errors.no_dates'));
|
||||
return setError(t('home:form.errors.no_dates'))
|
||||
}
|
||||
const isSpecificDates = typeof dates[0] === 'string' && dates[0].length === 8;
|
||||
const isSpecificDates = typeof dates[0] === 'string' && dates[0].length === 8
|
||||
if (start === end) {
|
||||
return setError(t('home:form.errors.same_times'));
|
||||
return setError(t('home:form.errors.same_times'))
|
||||
}
|
||||
|
||||
let times = dates.reduce((times, date) => {
|
||||
let day = [];
|
||||
const times = dates.reduce((times, date) => {
|
||||
const day = []
|
||||
for (let i = start; i < (start > end ? 24 : end); i++) {
|
||||
if (isSpecificDates) {
|
||||
day.push(
|
||||
dayjs.tz(date, 'DDMMYYYY', data.timezone)
|
||||
.hour(i).minute(0).utc().format('HHmm-DDMMYYYY')
|
||||
);
|
||||
.hour(i).minute(0).utc().format('HHmm-DDMMYYYY')
|
||||
)
|
||||
} else {
|
||||
day.push(
|
||||
dayjs().tz(data.timezone)
|
||||
.day(date).hour(i).minute(0).utc().format('HHmm-d')
|
||||
);
|
||||
.day(date).hour(i).minute(0).utc().format('HHmm-d')
|
||||
)
|
||||
}
|
||||
}
|
||||
if (start > end) {
|
||||
|
|
@ -120,21 +120,21 @@ const Home = ({ offline }) => {
|
|||
if (isSpecificDates) {
|
||||
day.push(
|
||||
dayjs.tz(date, 'DDMMYYYY', data.timezone)
|
||||
.hour(i).minute(0).utc().format('HHmm-DDMMYYYY')
|
||||
);
|
||||
.hour(i).minute(0).utc().format('HHmm-DDMMYYYY')
|
||||
)
|
||||
} else {
|
||||
day.push(
|
||||
dayjs().tz(data.timezone)
|
||||
.day(date).hour(i).minute(0).utc().format('HHmm-d')
|
||||
);
|
||||
.day(date).hour(i).minute(0).utc().format('HHmm-d')
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
return [...times, ...day];
|
||||
}, []);
|
||||
return [...times, ...day]
|
||||
}, [])
|
||||
|
||||
if (times.length === 0) {
|
||||
return setError(t('home:form.errors.no_time'));
|
||||
return setError(t('home:form.errors.no_time'))
|
||||
}
|
||||
|
||||
const response = await api.post('/event', {
|
||||
|
|
@ -143,18 +143,18 @@ const Home = ({ offline }) => {
|
|||
times: times,
|
||||
timezone: data.timezone,
|
||||
},
|
||||
});
|
||||
push(`/${response.data.id}`);
|
||||
})
|
||||
navigate(`/${response.id}`)
|
||||
gtag('event', 'create_event', {
|
||||
'event_category': 'home',
|
||||
});
|
||||
})
|
||||
} catch (e) {
|
||||
setError(t('home:form.errors.unknown'));
|
||||
console.error(e);
|
||||
setError(t('home:form.errors.unknown'))
|
||||
console.error(e)
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
setIsLoading(false)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
|
|
@ -162,7 +162,7 @@ const Home = ({ offline }) => {
|
|||
<Center>
|
||||
<Logo src={logo} alt="" />
|
||||
</Center>
|
||||
<TitleSmall altChars={/^[A-Za-z ]+$/.test(t('home:create'))}>{t('home:create')}</TitleSmall>
|
||||
<TitleSmall $altChars={/^[A-Za-z ]+$/.test(t('home:create'))}>{t('home:create')}</TitleSmall>
|
||||
<TitleLarge>CRAB FIT</TitleLarge>
|
||||
<Links>
|
||||
<a href="#about">{t('home:nav.about')}</a> / <a href="#donate">{t('home:nav.donate')}</a>
|
||||
|
|
@ -228,11 +228,11 @@ const Home = ({ offline }) => {
|
|||
<h2>{t('home:about.name')}</h2>
|
||||
<Stats>
|
||||
<Stat>
|
||||
<StatNumber>{stats.eventCount ?? '1100+'}</StatNumber>
|
||||
<StatNumber>{new Intl.NumberFormat().format(stats.eventCount ?? 7000)}{!stats.eventCount && '+'}</StatNumber>
|
||||
<StatLabel>{t('home:about.events')}</StatLabel>
|
||||
</Stat>
|
||||
<Stat>
|
||||
<StatNumber>{stats.personCount ?? '3700+'}</StatNumber>
|
||||
<StatNumber>{new Intl.NumberFormat().format(stats.personCount ?? 25000)}{!stats.personCount && '+'}</StatNumber>
|
||||
<StatLabel>{t('home:about.availabilities')}</StatLabel>
|
||||
</Stat>
|
||||
</Stats>
|
||||
|
|
@ -240,14 +240,14 @@ const Home = ({ offline }) => {
|
|||
|
||||
{videoPlay ? (
|
||||
<VideoWrapper>
|
||||
<iframe width="560" height="315" src="https://www.youtube.com/embed/yXGd4VXZzcY?modestbranding=1&rel=0&autoplay=1" title={t('common:video.title')} frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
|
||||
<iframe width="560" height="315" src="https://www.youtube.com/embed/yXGd4VXZzcY?modestbranding=1&rel=0&autoplay=1" title={t('common:video.title')} frameBorder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowFullScreen></iframe>
|
||||
</VideoWrapper>
|
||||
) : (
|
||||
<VideoLink
|
||||
href="https://www.youtube.com/watch?v=yXGd4VXZzcY"
|
||||
onClick={e => {
|
||||
e.preventDefault();
|
||||
setVideoPlay(true);
|
||||
e.preventDefault()
|
||||
setVideoPlay(true)
|
||||
}}
|
||||
>
|
||||
<img src={video_thumb} alt={t('common:video.button')} />
|
||||
|
|
@ -274,10 +274,10 @@ const Home = ({ offline }) => {
|
|||
rel="noreferrer noopener"
|
||||
secondary
|
||||
>{{
|
||||
chrome: t('home:about.chrome_extension'),
|
||||
firefox: t('home:about.firefox_extension'),
|
||||
safari: t('home:about.safari_extension'),
|
||||
}[browser]}</Button>
|
||||
chrome: t('home:about.chrome_extension'),
|
||||
firefox: t('home:about.firefox_extension'),
|
||||
safari: t('home:about.safari_extension'),
|
||||
}[browser]}</Button>
|
||||
)}
|
||||
<Button
|
||||
href="https://play.google.com/store/apps/details?id=fit.crab"
|
||||
|
|
@ -298,7 +298,7 @@ const Home = ({ offline }) => {
|
|||
|
||||
<Footer />
|
||||
</>
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
export default Home;
|
||||
export default Home
|
||||
|
|
@ -1,72 +1,72 @@
|
|||
import styled from '@emotion/styled';
|
||||
import { keyframes, styled } from 'goober'
|
||||
|
||||
export const StyledMain = styled.div`
|
||||
export const StyledMain = styled('div')`
|
||||
width: 600px;
|
||||
margin: 20px auto;
|
||||
max-width: calc(100% - 60px);
|
||||
`;
|
||||
`
|
||||
|
||||
export const CreateForm = styled.form`
|
||||
export const CreateForm = styled('form')`
|
||||
margin: 0 0 60px;
|
||||
`;
|
||||
`
|
||||
|
||||
export const TitleSmall = styled.span`
|
||||
export const TitleSmall = styled('span')`
|
||||
display: block;
|
||||
margin: 0;
|
||||
font-size: 3rem;
|
||||
text-align: center;
|
||||
font-family: 'Samurai Bob', sans-serif;
|
||||
font-weight: 400;
|
||||
color: ${props => props.theme.mode === 'light' ? props.theme.primaryDark : props.theme.primaryLight};
|
||||
color: var(--secondary);
|
||||
line-height: 1em;
|
||||
text-transform: uppercase;
|
||||
|
||||
${props => !props.altChars && `
|
||||
${props => !props.$altChars && `
|
||||
font-family: sans-serif;
|
||||
font-size: 2rem;
|
||||
font-weight: 600;
|
||||
line-height: 1.2em;
|
||||
padding-top: .3em;
|
||||
`}
|
||||
`;
|
||||
`
|
||||
|
||||
export const TitleLarge = styled.h1`
|
||||
export const TitleLarge = styled('h1')`
|
||||
margin: 0;
|
||||
font-size: 4rem;
|
||||
text-align: center;
|
||||
color: ${props => props.theme.primary};
|
||||
color: var(--primary);
|
||||
font-family: 'Molot', sans-serif;
|
||||
font-weight: 400;
|
||||
text-shadow: 0 4px 0 ${props => props.theme.primaryDark};
|
||||
text-shadow: 0 4px 0 var(--shadow);
|
||||
line-height: 1em;
|
||||
text-transform: uppercase;
|
||||
|
||||
@media (max-width: 350px) {
|
||||
font-size: 3.5rem;
|
||||
}
|
||||
`;
|
||||
`
|
||||
|
||||
export const Logo = styled.img`
|
||||
const jelly = keyframes`
|
||||
from,to {
|
||||
transform: scale(1,1);
|
||||
}
|
||||
25% {
|
||||
transform: scale(.9,1.1);
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.1,.9);
|
||||
}
|
||||
75% {
|
||||
transform: scale(.95,1.05);
|
||||
}
|
||||
`
|
||||
|
||||
export const Logo = styled('img')`
|
||||
width: 80px;
|
||||
transition: transform .15s;
|
||||
animation: jelly .5s 1 .05s;
|
||||
animation: ${jelly} .5s 1 .05s;
|
||||
user-select: none;
|
||||
|
||||
@keyframes jelly {
|
||||
from,to {
|
||||
transform: scale(1,1)
|
||||
}
|
||||
25% {
|
||||
transform: scale(.9,1.1)
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.1,.9)
|
||||
}
|
||||
75% {
|
||||
transform: scale(.95,1.05)
|
||||
}
|
||||
}
|
||||
|
||||
&:active {
|
||||
animation: none;
|
||||
transform: scale(.85);
|
||||
|
|
@ -79,68 +79,68 @@ export const Logo = styled.img`
|
|||
transform: none;
|
||||
}
|
||||
}
|
||||
`;
|
||||
`
|
||||
|
||||
export const Links = styled.nav`
|
||||
export const Links = styled('nav')`
|
||||
text-align: center;
|
||||
margin: 20px 0;
|
||||
`;
|
||||
`
|
||||
|
||||
export const AboutSection = styled.section`
|
||||
export const AboutSection = styled('section')`
|
||||
margin: 30px 0 0;
|
||||
background-color: ${props => props.theme.primaryBackground};
|
||||
background-color: var(--surface);
|
||||
padding: 20px 0;
|
||||
|
||||
& a {
|
||||
color: ${props => props.theme.mode === 'light' ? props.theme.primaryDark : props.theme.primaryLight};
|
||||
color: var(--secondary);
|
||||
}
|
||||
`;
|
||||
`
|
||||
|
||||
export const P = styled.p`
|
||||
export const P = styled('p')`
|
||||
font-weight: 500;
|
||||
line-height: 1.6em;
|
||||
`;
|
||||
`
|
||||
|
||||
export const Stats = styled.div`
|
||||
export const Stats = styled('div')`
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
align-items: flex-start;
|
||||
flex-wrap: wrap;
|
||||
`;
|
||||
`
|
||||
|
||||
export const Stat = styled.div`
|
||||
export const Stat = styled('div')`
|
||||
text-align: center;
|
||||
padding: 0 6px;
|
||||
min-width: 160px;
|
||||
margin: 10px 0;
|
||||
`;
|
||||
`
|
||||
|
||||
export const StatNumber = styled.span`
|
||||
export const StatNumber = styled('span')`
|
||||
display: block;
|
||||
font-weight: 900;
|
||||
color: ${props => props.theme.mode === 'light' ? props.theme.primaryDark : props.theme.primaryLight};
|
||||
color: var(--secondary);
|
||||
font-size: 2em;
|
||||
`;
|
||||
`
|
||||
|
||||
export const StatLabel = styled.span`
|
||||
export const StatLabel = styled('span')`
|
||||
display: block;
|
||||
`;
|
||||
`
|
||||
|
||||
export const OfflineMessage = styled.div`
|
||||
export const OfflineMessage = styled('div')`
|
||||
text-align: center;
|
||||
margin: 50px 0 20px;
|
||||
`;
|
||||
`
|
||||
|
||||
export const ButtonArea = styled.div`
|
||||
export const ButtonArea = styled('div')`
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 12px;
|
||||
margin: 30px 0;
|
||||
`;
|
||||
`
|
||||
|
||||
export const VideoWrapper = styled.div`
|
||||
export const VideoWrapper = styled('div')`
|
||||
margin: 0 auto;
|
||||
position: relative;
|
||||
padding-bottom: 56.4%;
|
||||
|
|
@ -152,9 +152,9 @@ export const VideoWrapper = styled.div`
|
|||
height: 100%;
|
||||
border-radius: 10px;
|
||||
}
|
||||
`;
|
||||
`
|
||||
|
||||
export const VideoLink = styled.a`
|
||||
export const VideoLink = styled('a')`
|
||||
display: block;
|
||||
text-decoration: none;
|
||||
position: relative;
|
||||
|
|
@ -163,10 +163,10 @@ export const VideoLink = styled.a`
|
|||
margin: 0 auto;
|
||||
transition: transform .15s;
|
||||
|
||||
:hover, :focus {
|
||||
&:hover, &:focus {
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
:active {
|
||||
&:active {
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
|
|
@ -188,7 +188,7 @@ export const VideoLink = styled.a`
|
|||
text-shadow: 0 0 20px rgba(0,0,0,.8);
|
||||
user-select: none;
|
||||
|
||||
::before {
|
||||
&::before {
|
||||
content: '';
|
||||
display: block;
|
||||
height: 2em;
|
||||
|
|
@ -196,11 +196,11 @@ export const VideoLink = styled.a`
|
|||
background: currentColor;
|
||||
border-radius: 100%;
|
||||
margin: 0 auto .4em;
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='${props => encodeURIComponent(props.theme.primaryDark)}' stroke-width='2' stroke-linecap='round' stroke-linejoin='round' class='feather feather-play'%3E%3Cpolygon points='5 3 19 12 5 21 5 3'%3E%3C/polygon%3E%3C/svg%3E");
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%23F79E00' stroke-width='2' stroke-linecap='round' stroke-linejoin='round' class='feather feather-play'%3E%3Cpolygon points='5 3 19 12 5 21 5 3'%3E%3C/polygon%3E%3C/svg%3E");
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: 1em;
|
||||
box-shadow: 0 0 20px 0 rgba(0,0,0,.3);
|
||||
}
|
||||
}
|
||||
`;
|
||||
`
|
||||
103
crabfit-frontend/src/pages/Privacy/Privacy.jsx
Normal file
103
crabfit-frontend/src/pages/Privacy/Privacy.jsx
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
import { useState, useEffect, useRef } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import { Button, Center, Footer, Logo } from '/src/components'
|
||||
|
||||
import { StyledMain, AboutSection, P } from '../Home/Home.styles'
|
||||
import { Note, ButtonArea } from './Privacy.styles'
|
||||
|
||||
const translationDisclaimer = 'While the translated document is provided for your convenience, the English version as displayed at https://crab.fit is legally binding.'
|
||||
|
||||
const Privacy = () => {
|
||||
const navigate = useNavigate()
|
||||
const { t, i18n } = useTranslation(['common', 'privacy'])
|
||||
const contentRef = useRef()
|
||||
const [content, setContent] = useState('')
|
||||
|
||||
useEffect(() => {
|
||||
document.title = `${t('privacy:name')} - Crab Fit`
|
||||
}, [t])
|
||||
|
||||
useEffect(() => setContent(contentRef.current?.innerText || ''), [contentRef])
|
||||
|
||||
return <>
|
||||
<StyledMain>
|
||||
<Logo />
|
||||
</StyledMain>
|
||||
|
||||
<StyledMain>
|
||||
<h1>{t('privacy:name')}</h1>
|
||||
|
||||
{!i18n.language.startsWith('en') && (
|
||||
<p>
|
||||
<a
|
||||
href={`https://translate.google.com/?sl=en&tl=${i18n.language.substring(0, 2)}&text=${encodeURIComponent(`${translationDisclaimer}\n\n${content}`)}&op=translate`}
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
>{t('privacy:translate')}</a>
|
||||
</p>
|
||||
)}
|
||||
|
||||
<h3>Crab Fit</h3>
|
||||
<div ref={contentRef}>
|
||||
<P>This SERVICE is provided by Benjamin Grant at no cost and is intended for use as is.</P>
|
||||
<P>This page is used to inform visitors regarding the policies of the collection, use, and disclosure of Personal Information if using the Service.</P>
|
||||
<P>If you choose to use the Service, then you agree to the collection and use of information in relation to this policy. The Personal Information that is collected is used for providing and improving the Service. Your information will not be used or shared with anyone except as described in this Privacy Policy.</P>
|
||||
|
||||
<h2>Information Collection and Use</h2>
|
||||
<P>The Service uses third party services that may collect information used to identify you.</P>
|
||||
<P>Links to privacy policies of the third party service providers used by the Service:</P>
|
||||
<P as="ul">
|
||||
<li><a href="https://www.google.com/policies/privacy/" target="blank">Google Play Services</a></li>
|
||||
</P>
|
||||
|
||||
<h2>Log Data</h2>
|
||||
<P>When you use the Service, in the case of an error, data and information is collected to improve the Service, which may include your IP address, device name, operating system version, app configuration and the time and date of the error.</P>
|
||||
|
||||
<h2>Cookies</h2>
|
||||
<P>Cookies are files with a small amount of data that are commonly used as anonymous unique identifiers. These are sent to your browser from the websites that you visit and are stored on your device's internal memory.</P>
|
||||
<P>Cookies are used by Google Analytics to track you across the web and provide anonymous statistics to improve the Service.</P>
|
||||
|
||||
<h2>Service Providers</h2>
|
||||
<P>Third-party companies may be employed for the following reasons:</P>
|
||||
<P as="ul">
|
||||
<li>To facilitate the Service</li>
|
||||
<li>To provide the Service on our behalf</li>
|
||||
<li>To perform Service-related services</li>
|
||||
<li>To assist in analyzing how the Service is used</li>
|
||||
</P>
|
||||
<P>To perform these tasks, the third parties may have access to your Personal Information, but are obligated not to disclose or use this information for any purpose except the above.</P>
|
||||
|
||||
<h2>Security</h2>
|
||||
<P>Personal Information that is shared via the Service is protected, however remember that no method of transmission over the internet, or method of electronic storage is 100% secure and reliable, so take care when sharing Personal Information.</P>
|
||||
<Note>Events that are created will be automatically permanently erased from storage after <strong>3 months</strong> of inactivity.</Note>
|
||||
|
||||
<h2>Links to Other Sites</h2>
|
||||
<P>The Service may contain links to other sites. If you click on a third-party link, you will be directed to that site. Note that these external sites are not operated by the Service. Therefore, you are advised to review the Privacy Policy of these websites.</P>
|
||||
|
||||
<h2>Children's Privacy</h2>
|
||||
<P>The Service does not address anyone under the age of 13. Personally identifiable information is not knowingly collected from children under 13. If discovered that a child under 13 has provided the Service with personal information, such information will be immediately deleted from the servers. If you are a parent or guardian and you are aware that your child has provided the Service with personal information, please contact us using the details below so that this information can be removed.</P>
|
||||
|
||||
<h2>Changes to This Privacy Policy</h2>
|
||||
<P>This Privacy Policy may be updated from time to time. Thus, you are advised to review this page periodically for any changes.</P>
|
||||
<P>Last updated: 2021-06-16</P>
|
||||
|
||||
<h2>Contact Us</h2>
|
||||
<P>If you have any questions or suggestions about the Privacy Policy, do not hesitate to contact us at <a href="mailto:contact@crab.fit">contact@crab.fit</a>.</P>
|
||||
</div>
|
||||
</StyledMain>
|
||||
|
||||
<ButtonArea>
|
||||
<AboutSection>
|
||||
<StyledMain>
|
||||
<Center><Button onClick={() => navigate('/')}>{t('common:cta')}</Button></Center>
|
||||
</StyledMain>
|
||||
</AboutSection>
|
||||
</ButtonArea>
|
||||
|
||||
<Footer />
|
||||
</>
|
||||
}
|
||||
|
||||
export default Privacy
|
||||
22
crabfit-frontend/src/pages/Privacy/Privacy.styles.js
Normal file
22
crabfit-frontend/src/pages/Privacy/Privacy.styles.js
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
import { styled } from 'goober'
|
||||
|
||||
export const Note = styled('p')`
|
||||
background-color: var(--surface);
|
||||
border: 1px solid var(--primary);
|
||||
border-radius: 10px;
|
||||
padding: 12px 16px;
|
||||
margin: 16px 0;
|
||||
box-sizing: border-box;
|
||||
font-weight: 500;
|
||||
line-height: 1.6em;
|
||||
|
||||
& a {
|
||||
color: var(--secondary);
|
||||
}
|
||||
`
|
||||
|
||||
export const ButtonArea = styled('div')`
|
||||
@media print {
|
||||
display: none;
|
||||
}
|
||||
`
|
||||
|
|
@ -1,114 +0,0 @@
|
|||
import { useState, useEffect, useRef } from 'react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import {
|
||||
Button,
|
||||
Center,
|
||||
Footer,
|
||||
Logo,
|
||||
} from 'components';
|
||||
|
||||
import {
|
||||
StyledMain,
|
||||
AboutSection,
|
||||
P,
|
||||
} from '../Home/homeStyle';
|
||||
import { Note, ButtonArea } from './privacyStyle';
|
||||
|
||||
const translationDisclaimer = 'While the translated document is provided for your convenience, the English version as displayed at https://crab.fit is legally binding.';
|
||||
|
||||
const Privacy = () => {
|
||||
const { push } = useHistory();
|
||||
const { t, i18n } = useTranslation(['common', 'privacy']);
|
||||
const contentRef = useRef();
|
||||
const [content, setContent] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
document.title = `${t('privacy:name')} - Crab Fit`;
|
||||
}, [t]);
|
||||
|
||||
useEffect(() => setContent(contentRef.current?.innerText || ''), [contentRef]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<StyledMain>
|
||||
<Logo />
|
||||
</StyledMain>
|
||||
|
||||
<StyledMain>
|
||||
<h1>{t('privacy:name')}</h1>
|
||||
|
||||
{!i18n.language.startsWith('en') && (
|
||||
<p>
|
||||
<a
|
||||
href={`https://translate.google.com/?sl=en&tl=${i18n.language.substring(0, 2)}&text=${encodeURIComponent(`${translationDisclaimer}\n\n${content}`)}&op=translate`}
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
>{t('privacy:translate')}</a>
|
||||
</p>
|
||||
)}
|
||||
|
||||
<h3>Crab Fit</h3>
|
||||
<div ref={contentRef}>
|
||||
<P>This SERVICE is provided by Benjamin Grant at no cost and is intended for use as is.</P>
|
||||
<P>This page is used to inform visitors regarding the policies of the collection, use, and disclosure of Personal Information if using the Service.</P>
|
||||
<P>If you choose to use the Service, then you agree to the collection and use of information in relation to this policy. The Personal Information that is collected is used for providing and improving the Service. Your information will not be used or shared with anyone except as described in this Privacy Policy.</P>
|
||||
|
||||
<h2>Information Collection and Use</h2>
|
||||
<P>The Service uses third party services that may collect information used to identify you.</P>
|
||||
<P>Links to privacy policies of the third party service providers used by the Service:</P>
|
||||
<P as="ul">
|
||||
<li><a href="https://www.google.com/policies/privacy/" target="blank">Google Play Services</a></li>
|
||||
</P>
|
||||
|
||||
<h2>Log Data</h2>
|
||||
<P>When you use the Service, in the case of an error, data and information is collected to improve the Service, which may include your IP address, device name, operating system version, app configuration and the time and date of the error.</P>
|
||||
|
||||
<h2>Cookies</h2>
|
||||
<P>Cookies are files with a small amount of data that are commonly used as anonymous unique identifiers. These are sent to your browser from the websites that you visit and are stored on your device's internal memory.</P>
|
||||
<P>Cookies are used by Google Analytics to track you across the web and provide anonymous statistics to improve the Service.</P>
|
||||
|
||||
<h2>Service Providers</h2>
|
||||
<P>Third-party companies may be employed for the following reasons:</P>
|
||||
<P as="ul">
|
||||
<li>To facilitate the Service</li>
|
||||
<li>To provide the Service on our behalf</li>
|
||||
<li>To perform Service-related services</li>
|
||||
<li>To assist in analyzing how the Service is used</li>
|
||||
</P>
|
||||
<P>To perform these tasks, the third parties may have access to your Personal Information, but are obligated not to disclose or use this information for any purpose except the above.</P>
|
||||
|
||||
<h2>Security</h2>
|
||||
<P>Personal Information that is shared via the Service is protected, however remember that no method of transmission over the internet, or method of electronic storage is 100% secure and reliable, so take care when sharing Personal Information.</P>
|
||||
<Note>Events that are created will be automatically permanently erased from storage after <strong>3 months</strong> of inactivity.</Note>
|
||||
|
||||
<h2>Links to Other Sites</h2>
|
||||
<P>The Service may contain links to other sites. If you click on a third-party link, you will be directed to that site. Note that these external sites are not operated by the Service. Therefore, you are advised to review the Privacy Policy of these websites.</P>
|
||||
|
||||
<h2>Children's Privacy</h2>
|
||||
<P>The Service does not address anyone under the age of 13. Personally identifiable information is not knowingly collected from children under 13. If discovered that a child under 13 has provided the Service with personal information, such information will be immediately deleted from the servers. If you are a parent or guardian and you are aware that your child has provided the Service with personal information, please contact us using the details below so that this information can be removed.</P>
|
||||
|
||||
<h2>Changes to This Privacy Policy</h2>
|
||||
<P>This Privacy Policy may be updated from time to time. Thus, you are advised to review this page periodically for any changes.</P>
|
||||
<P>Last updated: 2021-06-16</P>
|
||||
|
||||
<h2>Contact Us</h2>
|
||||
<P>If you have any questions or suggestions about the Privacy Policy, do not hesitate to contact us at <a href="mailto:contact@crab.fit">contact@crab.fit</a>.</P>
|
||||
</div>
|
||||
</StyledMain>
|
||||
|
||||
<ButtonArea>
|
||||
<AboutSection>
|
||||
<StyledMain>
|
||||
<Center><Button onClick={() => push('/')}>{t('common:cta')}</Button></Center>
|
||||
</StyledMain>
|
||||
</AboutSection>
|
||||
</ButtonArea>
|
||||
|
||||
<Footer />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Privacy;
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
import styled from '@emotion/styled';
|
||||
|
||||
export const Note = styled.p`
|
||||
background-color: ${props => props.theme.primaryBackground};
|
||||
border: 1px solid ${props => props.theme.primary};
|
||||
border-radius: 10px;
|
||||
padding: 12px 16px;
|
||||
margin: 16px 0;
|
||||
box-sizing: border-box;
|
||||
font-weight: 500;
|
||||
line-height: 1.6em;
|
||||
|
||||
& a {
|
||||
color: ${props => props.theme.mode === 'light' ? props.theme.primaryDark : props.theme.primaryLight};
|
||||
}
|
||||
`;
|
||||
|
||||
export const ButtonArea = styled.div`
|
||||
@media print {
|
||||
display: none;
|
||||
}
|
||||
`;
|
||||
7
crabfit-frontend/src/pages/index.js
Normal file
7
crabfit-frontend/src/pages/index.js
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
import { lazy } from 'react'
|
||||
|
||||
export const Home = lazy(() => import('./Home/Home'))
|
||||
export const Event = lazy(() => import('./Event/Event'))
|
||||
export const Create = lazy(() => import('./Create/Create'))
|
||||
export const Help = lazy(() => import('./Help/Help'))
|
||||
export const Privacy = lazy(() => import('./Privacy/Privacy'))
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
export { default as Home } from './Home/Home';
|
||||
export { default as Event } from './Event/Event';
|
||||
export { default as Create } from './Create/Create';
|
||||
export { default as Help } from './Help/Help';
|
||||
export { default as Privacy } from './Privacy/Privacy';
|
||||
1
crabfit-frontend/src/react-app-env.d.ts
vendored
1
crabfit-frontend/src/react-app-env.d.ts
vendored
|
|
@ -1 +0,0 @@
|
|||
/// <reference types="react-scripts" />
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
const reportWebVitals = onPerfEntry => {
|
||||
if (onPerfEntry && onPerfEntry instanceof Function) {
|
||||
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
|
||||
getCLS(onPerfEntry);
|
||||
getFID(onPerfEntry);
|
||||
getFCP(onPerfEntry);
|
||||
getLCP(onPerfEntry);
|
||||
getTTFB(onPerfEntry);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export default reportWebVitals;
|
||||
|
|
@ -1 +0,0 @@
|
|||
["af","am","ar-dz","ar-kw","ar-ly","ar-ma","ar-sa","ar-tn","ar","az","be","bg","bi","bm","bn","bo","br","bs","ca","cs","cv","cy","da","de-at","de-ch","de","dv","el","en-au","en-ca","en-gb","en-ie","en-il","en-in","en-nz","en-sg","en-tt","en","eo","es-do","es-pr","es-us","es","et","eu","fa","fi","fo","fr-ca","fr-ch","fr","fy","ga","gd","gl","gom-latn","gu","he","hi","hr","ht","hu","hy-am","id","is","it-ch","it","ja","jv","ka","kk","km","kn","ko","ku","ky","lb","lo","lt","lv","me","mi","mk","ml","mn","mr","ms-my","ms","mt","my","nb","ne","nl-be","nl","nn","oc-lnc","pa-in","pl","pt-br","pt","ro","ru","rw","sd","se","si","sk","sl","sq","sr-cyrl","sr","ss","sv","sw","ta","te","tet","tg","th","tk","tl-ph","tlh","tr","tzl","tzm-latn","tzm","ug-cn","uk","ur","uz-latn","uz","vi","x-pseudo","yo","zh-cn","zh-hk","zh-tw","zh"]
|
||||
|
|
@ -1 +1,20 @@
|
|||
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><defs><style>.cls-1,.cls-4{fill:#f79e00;}.cls-2,.cls-4{isolation:isolate;}.cls-3{fill:#f48600;}</style></defs><path class="cls-1" d="M181.11,311a123,123,0,0,1-123-123h0a123,123,0,0,1,123-123h64.32L211.66,98.73h32.46c0,79.47-108.36,32.47-136.43,50.75C73.65,171.64,181.11,311,181.11,311Z"/><path class="cls-1" d="M404,149.48C375.94,131.2,267.58,178.2,267.58,98.73H300L266.27,65h64.32a123,123,0,0,1,123,123h0a123,123,0,0,1-123,123S438.05,171.64,404,149.48Z"/><rect class="cls-1" x="266.27" y="200.39" width="20.89" height="57.44" rx="10.44"/><rect class="cls-1" x="224.49" y="200.39" width="20.89" height="57.44" rx="10.44"/><path class="cls-1" d="M190.55,229.11H321.11A108.35,108.35,0,0,1,429.47,337.47h0A108.36,108.36,0,0,1,321.11,445.83H190.55A108.37,108.37,0,0,1,82.19,337.47h0A108.36,108.36,0,0,1,190.55,229.11Z"/><g class="cls-2"><path class="cls-3" d="M293.69,268.29a68.83,68.83,0,0,0-37.86,11.29,69.19,69.19,0,1,0,0,115.8,69.19,69.19,0,1,0,37.86-127.09Z"/></g><ellipse class="cls-4" cx="255.83" cy="337.48" rx="31.32" ry="57.9"/><path class="cls-1" d="M402.77,386.23c36,12.31,66,40.07,59.44,60.75C440,431.17,413.11,416.91,389,409.83Z"/><path class="cls-1" d="M420.05,345.89c37.37,7.15,71,30.45,67.35,51.85-24.24-12.55-52.82-22.92-77.67-26.57Z"/><path class="cls-1" d="M417.26,303.44c38.05.39,75.26,17.34,75.5,39-26.08-8-56.05-13.16-81.15-12.32Z"/><path class="cls-1" d="M109.23,386.23c-36,12.31-66,40.07-59.44,60.75C72,431.17,98.89,416.91,123,409.83Z"/><path class="cls-1" d="M92,345.89C54.58,353,21,376.34,24.6,397.74c24.24-12.55,52.82-22.92,77.67-26.57Z"/><path class="cls-1" d="M94.74,303.44c-38,.39-75.26,17.34-75.5,39,26.08-8,56.05-13.16,81.15-12.32Z"/></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
|
||||
<defs>
|
||||
<style>.cls-1,.cls-4{fill:#f79e00;}.cls-2,.cls-4{isolation:isolate;}.cls-3{fill:#f48600;}</style>
|
||||
</defs>
|
||||
<path class="cls-1" d="M181.11,311a123,123,0,0,1-123-123h0a123,123,0,0,1,123-123h64.32L211.66,98.73h32.46c0,79.47-108.36,32.47-136.43,50.75C73.65,171.64,181.11,311,181.11,311Z"/>
|
||||
<path class="cls-1" d="M404,149.48C375.94,131.2,267.58,178.2,267.58,98.73H300L266.27,65h64.32a123,123,0,0,1,123,123h0a123,123,0,0,1-123,123S438.05,171.64,404,149.48Z"/>
|
||||
<rect class="cls-1" x="266.27" y="200.39" width="20.89" height="57.44" rx="10.44"/>
|
||||
<rect class="cls-1" x="224.49" y="200.39" width="20.89" height="57.44" rx="10.44"/>
|
||||
<path class="cls-1" d="M190.55,229.11H321.11A108.35,108.35,0,0,1,429.47,337.47h0A108.36,108.36,0,0,1,321.11,445.83H190.55A108.37,108.37,0,0,1,82.19,337.47h0A108.36,108.36,0,0,1,190.55,229.11Z"/>
|
||||
<g class="cls-2">
|
||||
<path class="cls-3" d="M293.69,268.29a68.83,68.83,0,0,0-37.86,11.29,69.19,69.19,0,1,0,0,115.8,69.19,69.19,0,1,0,37.86-127.09Z"/>
|
||||
</g>
|
||||
<ellipse class="cls-4" cx="255.83" cy="337.48" rx="31.32" ry="57.9"/>
|
||||
<path class="cls-1" d="M402.77,386.23c36,12.31,66,40.07,59.44,60.75C440,431.17,413.11,416.91,389,409.83Z"/>
|
||||
<path class="cls-1" d="M420.05,345.89c37.37,7.15,71,30.45,67.35,51.85-24.24-12.55-52.82-22.92-77.67-26.57Z"/>
|
||||
<path class="cls-1" d="M417.26,303.44c38.05.39,75.26,17.34,75.5,39-26.08-8-56.05-13.16-81.15-12.32Z"/>
|
||||
<path class="cls-1" d="M109.23,386.23c-36,12.31-66,40.07-59.44,60.75C72,431.17,98.89,416.91,123,409.83Z"/>
|
||||
<path class="cls-1" d="M92,345.89C54.58,353,21,376.34,24.6,397.74c24.24-12.55,52.82-22.92,77.67-26.57Z"/>
|
||||
<path class="cls-1" d="M94.74,303.44c-38,.39-75.26,17.34-75.5,39,26.08-8,56.05-13.16,81.15-12.32Z"/>
|
||||
</svg>
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.7 KiB |
|
|
@ -1,45 +1,75 @@
|
|||
import axios from 'axios';
|
||||
|
||||
export const instance = axios.create({
|
||||
baseURL: process.env.NODE_ENV === 'production' ? 'https://api-dot-crabfit.uc.r.appspot.com' : 'http://localhost:8080',
|
||||
timeout: 1000 * 300,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
const API_URL = process.env.NODE_ENV === 'production' ? 'https://api.crab.fit' : 'http://localhost:8080'
|
||||
|
||||
const handleError = error => {
|
||||
if (error.response && error.response.status) {
|
||||
console.log('[Error handler] res:', error.response);
|
||||
if (error && error.status) {
|
||||
console.error('[Error handler] res:', error)
|
||||
}
|
||||
return Promise.reject(error.response);
|
||||
};
|
||||
return Promise.reject(error)
|
||||
}
|
||||
|
||||
const api = {
|
||||
get: async (endpoint, data) => {
|
||||
get: async endpoint => {
|
||||
try {
|
||||
const response = await instance.get(endpoint, data);
|
||||
return Promise.resolve(response);
|
||||
const response = await fetch(API_URL + endpoint)
|
||||
if (!response.ok) {
|
||||
throw response
|
||||
}
|
||||
const json = await response.json()
|
||||
return Promise.resolve(json)
|
||||
} catch (error) {
|
||||
return handleError(error);
|
||||
return handleError(error)
|
||||
}
|
||||
},
|
||||
post: async (endpoint, data, options = {}) => {
|
||||
try {
|
||||
const response = await instance.post(endpoint, data, options);
|
||||
return Promise.resolve(response);
|
||||
} catch (error) {
|
||||
return handleError(error);
|
||||
}
|
||||
},
|
||||
patch: async (endpoint, data) => {
|
||||
try {
|
||||
const response = await instance.patch(endpoint, data);
|
||||
return Promise.resolve(response);
|
||||
} catch (error) {
|
||||
return handleError(error);
|
||||
}
|
||||
},
|
||||
};
|
||||
const response = await fetch(API_URL + endpoint, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
...options
|
||||
})
|
||||
if (!response.ok) {
|
||||
throw response
|
||||
}
|
||||
|
||||
export default api;
|
||||
//TODO: hack until api update
|
||||
try {
|
||||
const json = await response.json()
|
||||
return Promise.resolve(json)
|
||||
} catch (e) {
|
||||
return Promise.resolve(response)
|
||||
}
|
||||
} catch (error) {
|
||||
return handleError(error)
|
||||
}
|
||||
},
|
||||
patch: async (endpoint, data, options = {}) => {
|
||||
try {
|
||||
const response = await fetch(API_URL + endpoint, {
|
||||
method: 'PATCH',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
...options
|
||||
})
|
||||
if (!response.ok) {
|
||||
throw response
|
||||
}
|
||||
|
||||
//TODO: hack until api update
|
||||
try {
|
||||
const json = await response.json()
|
||||
return Promise.resolve(json)
|
||||
} catch (e) {
|
||||
return Promise.resolve(response)
|
||||
}
|
||||
} catch (error) {
|
||||
return handleError(error)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
export default api
|
||||
|
|
|
|||
|
|
@ -1,5 +0,0 @@
|
|||
// jest-dom adds custom jest matchers for asserting on DOM nodes.
|
||||
// allows you to do things like:
|
||||
// expect(element).toHaveTextContent(/react/i)
|
||||
// learn more: https://github.com/testing-library/jest-dom
|
||||
import '@testing-library/jest-dom';
|
||||
5
crabfit-frontend/src/stores/index.js
Normal file
5
crabfit-frontend/src/stores/index.js
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
export { default as useSettingsStore } from './settingsStore'
|
||||
export { default as useRecentsStore } from './recentsStore'
|
||||
export { default as useTWAStore } from './twaStore'
|
||||
export { default as useLocaleUpdateStore } from './localeUpdateStore'
|
||||
export { default as useTranslateStore } from './translateStore'
|
||||
|
|
@ -1,64 +0,0 @@
|
|||
import create from 'zustand';
|
||||
import { persist } from 'zustand/middleware';
|
||||
import locales from 'res/dayjs_locales';
|
||||
|
||||
export const useSettingsStore = create(persist(
|
||||
set => ({
|
||||
weekStart: 0,
|
||||
timeFormat: '12h',
|
||||
theme: 'System',
|
||||
highlight: false,
|
||||
|
||||
setWeekStart: weekStart => set({ weekStart }),
|
||||
setTimeFormat: timeFormat => set({ timeFormat }),
|
||||
setTheme: theme => set({ theme }),
|
||||
setHighlight: highlight => set({ highlight }),
|
||||
}),
|
||||
{ name: 'crabfit-settings' },
|
||||
));
|
||||
|
||||
export const useRecentsStore = create(persist(
|
||||
set => ({
|
||||
recents: [],
|
||||
|
||||
addRecent: event => set(state => {
|
||||
const recents = state.recents.filter(e => e.id !== event.id);
|
||||
recents.unshift(event);
|
||||
recents.length = Math.min(recents.length, 5);
|
||||
return { recents };
|
||||
}),
|
||||
removeRecent: id => set(state => {
|
||||
const recents = state.recents.filter(e => e.id !== id);
|
||||
return { recents };
|
||||
}),
|
||||
clearRecents: () => set({ recents: [] }),
|
||||
}),
|
||||
{ name: 'crabfit-recent' },
|
||||
));
|
||||
|
||||
export const useTWAStore = create(set => ({
|
||||
TWA: undefined,
|
||||
setTWA: TWA => set({ TWA }),
|
||||
}));
|
||||
|
||||
export const useLocaleUpdateStore = create(set => ({
|
||||
locale: 'en',
|
||||
setLocale: locale => set({ locale }),
|
||||
}));
|
||||
|
||||
export const useTranslateStore = create(persist(
|
||||
set => ({
|
||||
navigatorLang: navigator.language,
|
||||
navigatorSupported: Object.keys(locales).includes(navigator.language.substring(0, 2)),
|
||||
translateDialogDismissed: false,
|
||||
|
||||
setDialogDismissed: value => set({ translateDialogDismissed: value }),
|
||||
}),
|
||||
{
|
||||
name: 'crabfit-translate',
|
||||
blacklist: [
|
||||
'navigatorLang',
|
||||
'navigatorSupported',
|
||||
],
|
||||
},
|
||||
));
|
||||
8
crabfit-frontend/src/stores/localeUpdateStore.js
Normal file
8
crabfit-frontend/src/stores/localeUpdateStore.js
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
import create from 'zustand'
|
||||
|
||||
const useLocaleUpdateStore = create(set => ({
|
||||
locale: 'en',
|
||||
setLocale: locale => set({ locale }),
|
||||
}))
|
||||
|
||||
export default useLocaleUpdateStore
|
||||
23
crabfit-frontend/src/stores/recentsStore.js
Normal file
23
crabfit-frontend/src/stores/recentsStore.js
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
import create from 'zustand'
|
||||
import { persist } from 'zustand/middleware'
|
||||
|
||||
const useRecentsStore = create(persist(
|
||||
set => ({
|
||||
recents: [],
|
||||
|
||||
addRecent: event => set(state => {
|
||||
const recents = state.recents.filter(e => e.id !== event.id)
|
||||
recents.unshift(event)
|
||||
recents.length = Math.min(recents.length, 5)
|
||||
return { recents }
|
||||
}),
|
||||
removeRecent: id => set(state => {
|
||||
const recents = state.recents.filter(e => e.id !== id)
|
||||
return { recents }
|
||||
}),
|
||||
clearRecents: () => set({ recents: [] }),
|
||||
}),
|
||||
{ name: 'crabfit-recent' },
|
||||
))
|
||||
|
||||
export default useRecentsStore
|
||||
19
crabfit-frontend/src/stores/settingsStore.js
Normal file
19
crabfit-frontend/src/stores/settingsStore.js
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
import create from 'zustand'
|
||||
import { persist } from 'zustand/middleware'
|
||||
|
||||
const useSettingsStore = create(persist(
|
||||
set => ({
|
||||
weekStart: 0,
|
||||
timeFormat: '12h',
|
||||
theme: 'System',
|
||||
highlight: false,
|
||||
|
||||
setWeekStart: weekStart => set({ weekStart }),
|
||||
setTimeFormat: timeFormat => set({ timeFormat }),
|
||||
setTheme: theme => set({ theme }),
|
||||
setHighlight: highlight => set({ highlight }),
|
||||
}),
|
||||
{ name: 'crabfit-settings' },
|
||||
))
|
||||
|
||||
export default useSettingsStore
|
||||
23
crabfit-frontend/src/stores/translateStore.js
Normal file
23
crabfit-frontend/src/stores/translateStore.js
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
import create from 'zustand'
|
||||
import { persist } from 'zustand/middleware'
|
||||
|
||||
import locales from '/src/i18n/locales'
|
||||
|
||||
const useTranslateStore = create(persist(
|
||||
set => ({
|
||||
navigatorLang: navigator.language,
|
||||
navigatorSupported: Object.keys(locales).includes(navigator.language.substring(0, 2)),
|
||||
translateDialogDismissed: false,
|
||||
|
||||
setDialogDismissed: value => set({ translateDialogDismissed: value }),
|
||||
}),
|
||||
{
|
||||
name: 'crabfit-translate',
|
||||
blacklist: [
|
||||
'navigatorLang',
|
||||
'navigatorSupported',
|
||||
],
|
||||
},
|
||||
))
|
||||
|
||||
export default useTranslateStore
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue