Update components
This commit is contained in:
parent
4382f559f3
commit
a67aee24dc
4
.github/workflows/deploy_frontend.yml
vendored
4
.github/workflows/deploy_frontend.yml
vendored
|
|
@ -27,7 +27,7 @@ jobs:
|
||||||
- run: yarn install --immutable
|
- run: yarn install --immutable
|
||||||
- run: yarn build
|
- run: yarn build
|
||||||
- name: Copy app.yaml to build
|
- name: Copy app.yaml to build
|
||||||
run: 'cp app.yaml ./build/app.yaml'
|
run: 'cp app.yaml ./dist/app.yaml'
|
||||||
- id: auth
|
- id: auth
|
||||||
uses: google-github-actions/auth@v0
|
uses: google-github-actions/auth@v0
|
||||||
with:
|
with:
|
||||||
|
|
@ -35,5 +35,5 @@ jobs:
|
||||||
- id: deploy
|
- id: deploy
|
||||||
uses: google-github-actions/deploy-appengine@v0
|
uses: google-github-actions/deploy-appengine@v0
|
||||||
with:
|
with:
|
||||||
working_directory: crabfit-frontend/build
|
working_directory: crabfit-frontend
|
||||||
version: v1
|
version: v1
|
||||||
|
|
|
||||||
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.
|
node_modules
|
||||||
|
dist
|
||||||
# dependencies
|
build
|
||||||
/node_modules
|
|
||||||
/.pnp
|
|
||||||
.pnp.js
|
|
||||||
|
|
||||||
# testing
|
|
||||||
/coverage
|
|
||||||
|
|
||||||
# production
|
|
||||||
/build
|
|
||||||
|
|
||||||
# misc
|
|
||||||
.DS_Store
|
|
||||||
.env.local
|
|
||||||
.env.development.local
|
|
||||||
.env.test.local
|
|
||||||
.env.production.local
|
|
||||||
|
|
||||||
npm-debug.log*
|
npm-debug.log*
|
||||||
yarn-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:
|
handlers:
|
||||||
# Serve all static files with url ending with a file extension
|
# Serve all static files with url ending with a file extension
|
||||||
- url: /(.*\..+)$
|
- url: /(.*\..+)$
|
||||||
static_files: \1
|
static_files: dist/\1
|
||||||
upload: (.*\..+)$
|
upload: (.*\..+)$
|
||||||
secure: always
|
secure: always
|
||||||
redirect_http_response_code: 301
|
redirect_http_response_code: 301
|
||||||
|
|
||||||
# Catch all handler to index.html
|
# Catch all handler to index.html
|
||||||
- url: /.*
|
- url: /.*
|
||||||
static_files: index.html
|
static_files: dist/index.html
|
||||||
upload: index.html
|
upload: dist/index.html
|
||||||
secure: always
|
secure: always
|
||||||
redirect_http_response_code: 301
|
redirect_http_response_code: 301
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<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="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
|
||||||
<meta name="theme-color" content="#F79E00">
|
<meta name="theme-color" content="#F79E00">
|
||||||
<meta
|
<meta
|
||||||
|
|
@ -16,14 +16,14 @@
|
||||||
<meta name="monetization" content="$ilp.uphold.com/HjDULeBk9JnH">
|
<meta name="monetization" content="$ilp.uphold.com/HjDULeBk9JnH">
|
||||||
<!--V1.0--><meta http-equiv="origin-trial" content="ApibM5tjM3kUQQ2EQrkRcdTdWJRGAEKaUFzNhFmx+Of5H/cRyWuecMxs//Bikgo3WMSKs5kntElcM+U8kDy9cAEAAABOeyJvcmlnaW4iOiJodHRwczovL2NyYWIuZml0OjQ0MyIsImZlYXR1cmUiOiJEaWdpdGFsR29vZHMiLCJleHBpcnkiOjE2Mzk1MjYzOTl9">
|
<!--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=">
|
<!--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="apple-touch-icon" href="logo192.png">
|
||||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json">
|
<link rel="manifest" href="manifest.json">
|
||||||
|
|
||||||
<meta property="og:title" content="Crab Fit">
|
<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:description" content="Enter your availability to find a time that works for everyone!">
|
||||||
<meta property="og:url" content="https://crab.fit">
|
<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>
|
<title>Crab Fit</title>
|
||||||
|
|
||||||
|
|
@ -38,9 +38,14 @@
|
||||||
</script>
|
</script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
<script type="module" src="/src/index.jsx"></script>
|
||||||
|
|
||||||
<noscript>
|
<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>
|
</noscript>
|
||||||
<div id="root"></div>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</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",
|
"version": "1.0.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"license": "GPL-3.0-only",
|
"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": {
|
"scripts": {
|
||||||
"start": "react-app-rewired start",
|
"dev": "vite",
|
||||||
"build": "react-app-rewired build",
|
"build": "vite build",
|
||||||
"test": "react-app-rewired test",
|
"lint": "eslint --ext .js,.jsx ./src"
|
||||||
"eject": "react-scripts eject"
|
|
||||||
},
|
},
|
||||||
"eslintConfig": {
|
"dependencies": {
|
||||||
"extends": [
|
"@azure/msal-browser": "^2.28.1",
|
||||||
"react-app",
|
"@microsoft/microsoft-graph-client": "^3.0.2",
|
||||||
"react-app/jest"
|
"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": {
|
"browserslist": {
|
||||||
"production": [
|
"production": [
|
||||||
|
|
@ -67,9 +56,5 @@
|
||||||
"last 1 firefox version",
|
"last 1 firefox version",
|
||||||
"last 1 safari version"
|
"last 1 safari version"
|
||||||
]
|
]
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"react-app-rewired": "^2.1.8",
|
|
||||||
"workbox-webpack-plugin": "^5.1.3"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: Karla;
|
font-family: 'Karla';
|
||||||
src: url('fonts/karla-variable.ttf') format('truetype');
|
src: url('fonts/karla-variable.ttf') format('truetype');
|
||||||
font-weight: 1 999;
|
font-weight: 1 999;
|
||||||
}
|
}
|
||||||
|
|
@ -20,22 +20,146 @@
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:root {
|
||||||
|
color-scheme: light dark;
|
||||||
|
|
||||||
|
/* LIGHT */
|
||||||
|
--background-light: #FFFFFF;
|
||||||
|
--text-light: #000000;
|
||||||
|
--primary-light: #F79E00;
|
||||||
|
--secondary-light: #F48600;
|
||||||
|
--tertiary-light: #F4BB60;
|
||||||
|
--surface-light: #FEF2DD;
|
||||||
|
--error-light: #D32F2F;
|
||||||
|
--loading-light: #DDDDDD;
|
||||||
|
--font-weight-light: 600;
|
||||||
|
|
||||||
|
/* DARK */
|
||||||
|
--background-dark: #111111;
|
||||||
|
--text-dark: #DDDDDD;
|
||||||
|
--primary-dark: #F79E00;
|
||||||
|
--secondary-dark: #F4BB60;
|
||||||
|
--tertiary-dark: #CC7313;
|
||||||
|
--surface-dark: #30240F;
|
||||||
|
--error-dark: #E53935;
|
||||||
|
--loading-dark: #444444;
|
||||||
|
--font-weight-dark: 500;
|
||||||
|
|
||||||
|
/* Define light defaults */
|
||||||
|
--background: var(--background-light);
|
||||||
|
--text: var(--text-light);
|
||||||
|
--primary: var(--primary-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);
|
||||||
|
--primary: var(--primary-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);
|
||||||
|
--primary: var(--primary-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);
|
||||||
|
--primary: var(--primary-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(--secondary);
|
||||||
|
}
|
||||||
|
*::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: var(--tertiary);
|
||||||
|
}
|
||||||
|
*::-webkit-scrollbar-thumb:active {
|
||||||
|
background: red;
|
||||||
|
}
|
||||||
|
|
||||||
/* IE 10+ */
|
/* IE 10+ */
|
||||||
@media all and (-ms-high-contrast: none), (-ms-high-contrast: active) {
|
@media all and (-ms-high-contrast: none), (-ms-high-contrast: active) {
|
||||||
#root {
|
#app {
|
||||||
font-family: Karla, sans-serif;
|
|
||||||
text-align: center;
|
text-align: center;
|
||||||
margin: 20vh auto;
|
margin: 20vh auto;
|
||||||
font-size: 1.3em;
|
font-size: 1.3em;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
#root::before {
|
#app::before {
|
||||||
content: '🦀';
|
content: '🦀';
|
||||||
font-size: 1.5em;
|
font-size: 1.5em;
|
||||||
display: block;
|
display: block;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
}
|
}
|
||||||
#root::after {
|
#app::after {
|
||||||
display: block;
|
display: block;
|
||||||
content: 'Crab Fit doesn\'t work in Internet Explorer. Please try using a modern browser.';
|
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="/" element={<Pages.Help />} /> */}
|
||||||
|
<Route path="/" 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 { useState, useRef, Fragment, Suspense, lazy } from 'react'
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next'
|
||||||
import { useLocaleUpdateStore } from 'stores';
|
import dayjs from 'dayjs'
|
||||||
import dayjs from 'dayjs';
|
import localeData from 'dayjs/plugin/localeData'
|
||||||
import localeData from 'dayjs/plugin/localeData';
|
import customParseFormat from 'dayjs/plugin/customParseFormat'
|
||||||
import customParseFormat from 'dayjs/plugin/customParseFormat';
|
import isBetween from 'dayjs/plugin/isBetween'
|
||||||
import isBetween from 'dayjs/plugin/isBetween';
|
import dayjs_timezone from 'dayjs/plugin/timezone'
|
||||||
import dayjs_timezone from 'dayjs/plugin/timezone';
|
import utc from 'dayjs/plugin/utc'
|
||||||
import utc from 'dayjs/plugin/utc';
|
|
||||||
|
import { useLocaleUpdateStore } from '/src/stores'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Wrapper,
|
Wrapper,
|
||||||
|
|
@ -21,20 +22,20 @@ import {
|
||||||
TimeLabel,
|
TimeLabel,
|
||||||
TimeSpace,
|
TimeSpace,
|
||||||
StyledMain,
|
StyledMain,
|
||||||
} from 'components/AvailabilityViewer/availabilityViewerStyle';
|
} from '/src/components/AvailabilityViewer/AvailabilityViewer.styles'
|
||||||
import { Time } from './availabilityEditorStyle';
|
import { Time } from './AvailabilityEditor.styles'
|
||||||
|
|
||||||
import { _GoogleCalendar, _OutlookCalendar, Center } from 'components';
|
import { _GoogleCalendar, _OutlookCalendar, Center } from '/src/components'
|
||||||
import { Loader } from '../Loading/loadingStyle';
|
import { Loader } from '../Loading/Loading.styles'
|
||||||
|
|
||||||
const GoogleCalendar = lazy(() => _GoogleCalendar());
|
const GoogleCalendar = lazy(() => _GoogleCalendar())
|
||||||
const OutlookCalendar = lazy(() => _OutlookCalendar());
|
const OutlookCalendar = lazy(() => _OutlookCalendar())
|
||||||
|
|
||||||
dayjs.extend(localeData);
|
dayjs.extend(localeData)
|
||||||
dayjs.extend(customParseFormat);
|
dayjs.extend(customParseFormat)
|
||||||
dayjs.extend(isBetween);
|
dayjs.extend(isBetween)
|
||||||
dayjs.extend(utc);
|
dayjs.extend(utc)
|
||||||
dayjs.extend(dayjs_timezone);
|
dayjs.extend(dayjs_timezone)
|
||||||
|
|
||||||
const AvailabilityEditor = ({
|
const AvailabilityEditor = ({
|
||||||
times,
|
times,
|
||||||
|
|
@ -44,25 +45,24 @@ const AvailabilityEditor = ({
|
||||||
isSpecificDates,
|
isSpecificDates,
|
||||||
value = [],
|
value = [],
|
||||||
onChange,
|
onChange,
|
||||||
...props
|
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation('event');
|
const { t } = useTranslation('event')
|
||||||
const locale = useLocaleUpdateStore(state => state.locale);
|
const locale = useLocaleUpdateStore(state => state.locale)
|
||||||
|
|
||||||
const [selectingTimes, _setSelectingTimes] = useState([]);
|
const [selectingTimes, _setSelectingTimes] = useState([])
|
||||||
const staticSelectingTimes = useRef([]);
|
const staticSelectingTimes = useRef([])
|
||||||
const setSelectingTimes = newTimes => {
|
const setSelectingTimes = newTimes => {
|
||||||
staticSelectingTimes.current = newTimes;
|
staticSelectingTimes.current = newTimes
|
||||||
_setSelectingTimes(newTimes);
|
_setSelectingTimes(newTimes)
|
||||||
};
|
}
|
||||||
|
|
||||||
const startPos = useRef({});
|
const startPos = useRef({})
|
||||||
const staticMode = useRef(null);
|
const staticMode = useRef(null)
|
||||||
const [mode, _setMode] = useState(staticMode.current);
|
const [mode, _setMode] = useState(staticMode.current)
|
||||||
const setMode = newMode => {
|
const setMode = newMode => {
|
||||||
staticMode.current = newMode;
|
staticMode.current = newMode
|
||||||
_setMode(newMode);
|
_setMode(newMode)
|
||||||
};
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|
@ -109,8 +109,8 @@ const AvailabilityEditor = ({
|
||||||
)}
|
)}
|
||||||
</TimeLabels>
|
</TimeLabels>
|
||||||
{dates.map((date, x) => {
|
{dates.map((date, x) => {
|
||||||
const parsedDate = isSpecificDates ? dayjs(date, 'DDMMYYYY') : dayjs().day(date);
|
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 last = dates.length === x+1 || (isSpecificDates ? dayjs(dates[x+1], 'DDMMYYYY') : dayjs().day(dates[x+1])).diff(parsedDate, 'day') > 1
|
||||||
return (
|
return (
|
||||||
<Fragment key={x}>
|
<Fragment key={x}>
|
||||||
<Date>
|
<Date>
|
||||||
|
|
@ -122,13 +122,13 @@ const AvailabilityEditor = ({
|
||||||
borderLeft={x === 0 || (parsedDate).diff(isSpecificDates ? dayjs(dates[x-1], 'DDMMYYYY') : dayjs().day(dates[x-1]), 'day') > 1}
|
borderLeft={x === 0 || (parsedDate).diff(isSpecificDates ? dayjs(dates[x-1], 'DDMMYYYY') : dayjs().day(dates[x-1]), 'day') > 1}
|
||||||
>
|
>
|
||||||
{timeLabels.map((timeLabel, y) => {
|
{timeLabels.map((timeLabel, y) => {
|
||||||
if (!timeLabel.time) return null;
|
if (!timeLabel.time) return null
|
||||||
if (!times.includes(`${timeLabel.time}-${date}`)) {
|
if (!times.includes(`${timeLabel.time}-${date}`)) {
|
||||||
return (
|
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 (
|
return (
|
||||||
<Time
|
<Time
|
||||||
|
|
@ -138,35 +138,35 @@ const AvailabilityEditor = ({
|
||||||
selected={value.includes(time)}
|
selected={value.includes(time)}
|
||||||
selecting={selectingTimes.includes(time)}
|
selecting={selectingTimes.includes(time)}
|
||||||
mode={mode}
|
mode={mode}
|
||||||
onPointerDown={(e) => {
|
onPointerDown={e => {
|
||||||
e.preventDefault();
|
e.preventDefault()
|
||||||
startPos.current = {x, y};
|
startPos.current = {x, y}
|
||||||
setMode(value.includes(time) ? 'remove' : 'add');
|
setMode(value.includes(time) ? 'remove' : 'add')
|
||||||
setSelectingTimes([time]);
|
setSelectingTimes([time])
|
||||||
e.currentTarget.releasePointerCapture(e.pointerId);
|
e.currentTarget.releasePointerCapture(e.pointerId)
|
||||||
|
|
||||||
document.addEventListener('pointerup', () => {
|
document.addEventListener('pointerup', () => {
|
||||||
if (staticMode.current === 'add') {
|
if (staticMode.current === 'add') {
|
||||||
onChange([...value, ...staticSelectingTimes.current]);
|
onChange([...value, ...staticSelectingTimes.current])
|
||||||
} else if (staticMode.current === 'remove') {
|
} else if (staticMode.current === 'remove') {
|
||||||
onChange(value.filter(t => !staticSelectingTimes.current.includes(t)));
|
onChange(value.filter(t => !staticSelectingTimes.current.includes(t)))
|
||||||
}
|
}
|
||||||
setMode(null);
|
setMode(null)
|
||||||
}, { once: true });
|
}, { once: true })
|
||||||
}}
|
}}
|
||||||
onPointerEnter={() => {
|
onPointerEnter={() => {
|
||||||
if (staticMode.current) {
|
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 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++) {
|
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>
|
</Times>
|
||||||
</Date>
|
</Date>
|
||||||
|
|
@ -174,13 +174,13 @@ const AvailabilityEditor = ({
|
||||||
<Spacer />
|
<Spacer />
|
||||||
)}
|
)}
|
||||||
</Fragment>
|
</Fragment>
|
||||||
);
|
)
|
||||||
})}
|
})}
|
||||||
</Container>
|
</Container>
|
||||||
</ScrollWrapper>
|
</ScrollWrapper>
|
||||||
</Wrapper>
|
</Wrapper>
|
||||||
</>
|
</>
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|
||||||
export default AvailabilityEditor;
|
export default AvailabilityEditor
|
||||||
|
|
@ -1,24 +1,24 @@
|
||||||
import styled from '@emotion/styled';
|
import { styled } from 'goober'
|
||||||
|
|
||||||
export const Time = styled.div`
|
export const Time = styled('div')`
|
||||||
height: 10px;
|
height: 10px;
|
||||||
touch-action: none;
|
touch-action: none;
|
||||||
transition: background-color .1s;
|
transition: background-color .1s;
|
||||||
|
|
||||||
${props => props.time.slice(2, 4) === '00' && `
|
${props => props.time.slice(2, 4) === '00' && `
|
||||||
border-top: 2px solid ${props.theme.text};
|
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;
|
border-top: 2px solid transparent;
|
||||||
`}
|
`}
|
||||||
${props => props.time.slice(2, 4) === '30' && `
|
${props => props.time.slice(2, 4) === '30' && `
|
||||||
border-top: 2px dotted ${props.theme.text};
|
border-top: 2px dotted var(--text);
|
||||||
`}
|
`}
|
||||||
|
|
||||||
${props => (props.selected || (props.mode === 'add' && props.selecting)) && `
|
${props => (props.selected || (props.mode === 'add' && props.selecting)) && `
|
||||||
background-color: ${props.theme.primary};
|
background-color: var(--primary);
|
||||||
`};
|
`};
|
||||||
${props => props.mode === 'remove' && props.selecting && `
|
${props => props.mode === 'remove' && props.selecting && `
|
||||||
background-color: ${props.theme.background};
|
background-color: var(--background);
|
||||||
`};
|
`};
|
||||||
`;
|
`
|
||||||
|
|
@ -1,13 +1,13 @@
|
||||||
import { useState, useEffect, useRef, useMemo, Fragment } from 'react';
|
import { useState, useEffect, useRef, useMemo, Fragment } from 'react'
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next'
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs'
|
||||||
import localeData from 'dayjs/plugin/localeData';
|
import localeData from 'dayjs/plugin/localeData'
|
||||||
import customParseFormat from 'dayjs/plugin/customParseFormat';
|
import customParseFormat from 'dayjs/plugin/customParseFormat'
|
||||||
import relativeTime from 'dayjs/plugin/relativeTime';
|
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 {
|
import {
|
||||||
Wrapper,
|
Wrapper,
|
||||||
ScrollWrapper,
|
ScrollWrapper,
|
||||||
|
|
@ -30,13 +30,13 @@ import {
|
||||||
Person,
|
Person,
|
||||||
StyledMain,
|
StyledMain,
|
||||||
Info,
|
Info,
|
||||||
} from './availabilityViewerStyle';
|
} from './AvailabilityViewer.styles'
|
||||||
|
|
||||||
import locales from 'res/dayjs_locales';
|
import locales from '/src/i18n/locales'
|
||||||
|
|
||||||
dayjs.extend(localeData);
|
dayjs.extend(localeData)
|
||||||
dayjs.extend(customParseFormat);
|
dayjs.extend(customParseFormat)
|
||||||
dayjs.extend(relativeTime);
|
dayjs.extend(relativeTime)
|
||||||
|
|
||||||
const AvailabilityViewer = ({
|
const AvailabilityViewer = ({
|
||||||
times,
|
times,
|
||||||
|
|
@ -46,25 +46,24 @@ const AvailabilityViewer = ({
|
||||||
people = [],
|
people = [],
|
||||||
min = 0,
|
min = 0,
|
||||||
max = 0,
|
max = 0,
|
||||||
...props
|
|
||||||
}) => {
|
}) => {
|
||||||
const [tooltip, setTooltip] = useState(null);
|
const [tooltip, setTooltip] = useState(null)
|
||||||
const timeFormat = useSettingsStore(state => state.timeFormat);
|
const timeFormat = useSettingsStore(state => state.timeFormat)
|
||||||
const highlight = useSettingsStore(state => state.highlight);
|
const highlight = useSettingsStore(state => state.highlight)
|
||||||
const [filteredPeople, setFilteredPeople] = useState([]);
|
const [filteredPeople, setFilteredPeople] = useState([])
|
||||||
const [touched, setTouched] = useState(false);
|
const [touched, setTouched] = useState(false)
|
||||||
const [tempFocus, setTempFocus] = useState(null);
|
const [tempFocus, setTempFocus] = useState(null)
|
||||||
const [focusCount, setFocusCount] = useState(null);
|
const [focusCount, setFocusCount] = useState(null)
|
||||||
|
|
||||||
const { t } = useTranslation('event');
|
const { t } = useTranslation('event')
|
||||||
const locale = useLocaleUpdateStore(state => state.locale);
|
const locale = useLocaleUpdateStore(state => state.locale)
|
||||||
|
|
||||||
const wrapper = useRef();
|
const wrapper = useRef()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setFilteredPeople(people.map(p => p.name));
|
setFilteredPeople(people.map(p => p.name))
|
||||||
setTouched(people.length <= 1);
|
setTouched(people.length <= 1)
|
||||||
}, [people]);
|
}, [people])
|
||||||
|
|
||||||
const heatmap = useMemo(() => (
|
const heatmap = useMemo(() => (
|
||||||
<Container>
|
<Container>
|
||||||
|
|
@ -76,8 +75,8 @@ const AvailabilityViewer = ({
|
||||||
)}
|
)}
|
||||||
</TimeLabels>
|
</TimeLabels>
|
||||||
{dates.map((date, i) => {
|
{dates.map((date, i) => {
|
||||||
const parsedDate = isSpecificDates ? dayjs(date, 'DDMMYYYY') : dayjs().day(date);
|
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 last = dates.length === i+1 || (isSpecificDates ? dayjs(dates[i+1], 'DDMMYYYY') : dayjs().day(dates[i+1])).diff(parsedDate, 'day') > 1
|
||||||
return (
|
return (
|
||||||
<Fragment key={i}>
|
<Fragment key={i}>
|
||||||
<Date>
|
<Date>
|
||||||
|
|
@ -89,16 +88,16 @@ const AvailabilityViewer = ({
|
||||||
borderLeft={i === 0 || (parsedDate).diff(isSpecificDates ? dayjs(dates[i-1], 'DDMMYYYY') : dayjs().day(dates[i-1]), 'day') > 1}
|
borderLeft={i === 0 || (parsedDate).diff(isSpecificDates ? dayjs(dates[i-1], 'DDMMYYYY') : dayjs().day(dates[i-1]), 'day') > 1}
|
||||||
>
|
>
|
||||||
{timeLabels.map((timeLabel, i) => {
|
{timeLabels.map((timeLabel, i) => {
|
||||||
if (!timeLabel.time) return null;
|
if (!timeLabel.time) return null
|
||||||
if (!times.includes(`${timeLabel.time}-${date}`)) {
|
if (!times.includes(`${timeLabel.time}-${date}`)) {
|
||||||
return (
|
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
|
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) && 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 (
|
return (
|
||||||
<Time
|
<Time
|
||||||
|
|
@ -110,23 +109,23 @@ const AvailabilityViewer = ({
|
||||||
maxPeople={tempFocus !== null ? 1 : Math.min(max, filteredPeople.length)}
|
maxPeople={tempFocus !== null ? 1 : Math.min(max, filteredPeople.length)}
|
||||||
minPeople={tempFocus !== null ? 0 : Math.min(min, filteredPeople.length)}
|
minPeople={tempFocus !== null ? 0 : Math.min(min, filteredPeople.length)}
|
||||||
highlight={highlight}
|
highlight={highlight}
|
||||||
onMouseEnter={(e) => {
|
onMouseEnter={e => {
|
||||||
const cellBox = e.currentTarget.getBoundingClientRect();
|
const cellBox = e.currentTarget.getBoundingClientRect()
|
||||||
const wrapperBox = wrapper?.current?.getBoundingClientRect() ?? { x: 0, y: 0 };
|
const wrapperBox = wrapper?.current?.getBoundingClientRect() ?? { x: 0, y: 0 }
|
||||||
const timeText = timeFormat === '12h' ? `h${locales[locale].separator ?? ':'}mma` : `HH${locales[locale].separator ?? ':'}mm`;
|
const timeText = timeFormat === '12h' ? `h${locales[locale].separator ?? ':'}mma` : `HH${locales[locale].separator ?? ':'}mm`
|
||||||
setTooltip({
|
setTooltip({
|
||||||
x: Math.round(cellBox.x-wrapperBox.x + cellBox.width/2),
|
x: Math.round(cellBox.x-wrapperBox.x + cellBox.width/2),
|
||||||
y: Math.round(cellBox.y-wrapperBox.y + cellBox.height)+6,
|
y: Math.round(cellBox.y-wrapperBox.y + cellBox.height)+6,
|
||||||
available: `${peopleHere.length} / ${people.length} ${t('event:available')}`,
|
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`),
|
date: parsedDate.hour(time.slice(0, 2)).minute(time.slice(2, 4)).format(isSpecificDates ? `${timeText} ddd, D MMM YYYY` : `${timeText} ddd`),
|
||||||
people: peopleHere,
|
people: peopleHere,
|
||||||
});
|
})
|
||||||
}}
|
}}
|
||||||
onMouseLeave={() => {
|
onMouseLeave={() => {
|
||||||
setTooltip(null);
|
setTooltip(null)
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
)
|
||||||
})}
|
})}
|
||||||
</Times>
|
</Times>
|
||||||
</Date>
|
</Date>
|
||||||
|
|
@ -134,7 +133,7 @@ const AvailabilityViewer = ({
|
||||||
<Spacer />
|
<Spacer />
|
||||||
)}
|
)}
|
||||||
</Fragment>
|
</Fragment>
|
||||||
);
|
)
|
||||||
})}
|
})}
|
||||||
</Container>
|
</Container>
|
||||||
), [
|
), [
|
||||||
|
|
@ -152,7 +151,7 @@ const AvailabilityViewer = ({
|
||||||
timeFormat,
|
timeFormat,
|
||||||
timeLabels,
|
timeLabels,
|
||||||
times,
|
times,
|
||||||
]);
|
])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|
@ -173,16 +172,16 @@ const AvailabilityViewer = ({
|
||||||
key={i}
|
key={i}
|
||||||
filtered={filteredPeople.includes(person.name)}
|
filtered={filteredPeople.includes(person.name)}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setTempFocus(null);
|
setTempFocus(null)
|
||||||
if (filteredPeople.includes(person.name)) {
|
if (filteredPeople.includes(person.name)) {
|
||||||
if (!touched) {
|
if (!touched) {
|
||||||
setTouched(true);
|
setTouched(true)
|
||||||
setFilteredPeople([person.name]);
|
setFilteredPeople([person.name])
|
||||||
} else {
|
} else {
|
||||||
setFilteredPeople(filteredPeople.filter(n => n !== person.name));
|
setFilteredPeople(filteredPeople.filter(n => n !== person.name))
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
setFilteredPeople([...filteredPeople, person.name]);
|
setFilteredPeople([...filteredPeople, person.name])
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
onMouseOver={() => setTempFocus(person.name)}
|
onMouseOver={() => setTempFocus(person.name)}
|
||||||
|
|
@ -221,7 +220,7 @@ const AvailabilityViewer = ({
|
||||||
</ScrollWrapper>
|
</ScrollWrapper>
|
||||||
</Wrapper>
|
</Wrapper>
|
||||||
</>
|
</>
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|
||||||
export default AvailabilityViewer;
|
export default AvailabilityViewer
|
||||||
|
|
@ -1,16 +1,16 @@
|
||||||
import styled from '@emotion/styled';
|
import { styled } from 'goober'
|
||||||
|
|
||||||
export const Wrapper = styled.div`
|
export const Wrapper = styled('div')`
|
||||||
overflow-y: visible;
|
overflow-y: visible;
|
||||||
margin: 20px 0;
|
margin: 20px 0;
|
||||||
position: relative;
|
position: relative;
|
||||||
`;
|
`
|
||||||
|
|
||||||
export const ScrollWrapper = styled.div`
|
export const ScrollWrapper = styled('div')`
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
`;
|
`
|
||||||
|
|
||||||
export const Container = styled.div`
|
export const Container = styled('div')`
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
min-width: 100%;
|
min-width: 100%;
|
||||||
|
|
@ -21,147 +21,147 @@ export const Container = styled.div`
|
||||||
@media (max-width: 660px) {
|
@media (max-width: 660px) {
|
||||||
padding: 0 30px;
|
padding: 0 30px;
|
||||||
}
|
}
|
||||||
`;
|
`
|
||||||
|
|
||||||
export const Date = styled.div`
|
export const Date = styled('div')`
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
width: 60px;
|
width: 60px;
|
||||||
min-width: 60px;
|
min-width: 60px;
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
`;
|
`
|
||||||
|
|
||||||
export const Times = styled.div`
|
export const Times = styled('div')`
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
||||||
border-bottom: 2px solid ${props => props.theme.text};
|
border-bottom: 2px solid var(--text);
|
||||||
border-left: 1px solid ${props => props.theme.text};
|
border-left: 1px solid var(--text);
|
||||||
border-right: 1px solid ${props => props.theme.text};
|
border-right: 1px solid var(--text);
|
||||||
|
|
||||||
${props => props.borderLeft && `
|
${props => props.borderLeft && `
|
||||||
border-left: 2px solid ${props.theme.text};
|
border-left: 2px solid var(--text);
|
||||||
border-top-left-radius: 3px;
|
border-top-left-radius: 3px;
|
||||||
border-bottom-left-radius: 3px;
|
border-bottom-left-radius: 3px;
|
||||||
`}
|
`}
|
||||||
${props => props.borderRight && `
|
${props => props.borderRight && `
|
||||||
border-right: 2px solid ${props.theme.text};
|
border-right: 2px solid var(--text);
|
||||||
border-top-right-radius: 3px;
|
border-top-right-radius: 3px;
|
||||||
border-bottom-right-radius: 3px;
|
border-bottom-right-radius: 3px;
|
||||||
`}
|
`}
|
||||||
|
|
||||||
& .time + .timespace, & .timespace:first-of-type {
|
& .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;
|
display: block;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
`;
|
`
|
||||||
|
|
||||||
export const DayLabel = styled.label`
|
export const DayLabel = styled('label')`
|
||||||
display: block;
|
display: block;
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
`;
|
`
|
||||||
|
|
||||||
export const Time = styled.div`
|
export const Time = styled('div')`
|
||||||
height: 10px;
|
height: 10px;
|
||||||
background-origin: border-box;
|
background-origin: border-box;
|
||||||
transition: background-color .1s;
|
transition: background-color .1s;
|
||||||
|
|
||||||
${props => props.time.slice(2, 4) === '00' && `
|
${props => props.time.slice(2, 4) === '00' && `
|
||||||
border-top: 2px solid ${props.theme.text};
|
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;
|
border-top: 2px solid transparent;
|
||||||
`}
|
`}
|
||||||
${props => props.time.slice(2, 4) === '30' && `
|
${props => props.time.slice(2, 4) === '30' && `
|
||||||
border-top: 2px dotted ${props.theme.text};
|
border-top: 2px dotted var(--text);
|
||||||
`}
|
`}
|
||||||
|
|
||||||
background-color: ${props => `${props.theme.primary}${Math.round((props.peopleCount/props.maxPeople)*255).toString(16)}`};
|
background-color: ${props => `#FF0000${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(
|
background-image: repeating-linear-gradient(
|
||||||
45deg,
|
45deg,
|
||||||
transparent,
|
transparent,
|
||||||
transparent 4.3px,
|
transparent 4.3px,
|
||||||
${props.theme.primaryDark} 4.3px,
|
var(--secondary) 4.3px,
|
||||||
${props.theme.primaryDark} 8.6px
|
var(--secondary) 8.6px
|
||||||
);
|
);
|
||||||
`}
|
`}
|
||||||
|
|
||||||
@media (prefers-reduced-motion: reduce) {
|
@media (prefers-reduced-motion: reduce) {
|
||||||
transition: none;
|
transition: none;
|
||||||
}
|
}
|
||||||
`;
|
`
|
||||||
|
|
||||||
export const Spacer = styled.div`
|
export const Spacer = styled('div')`
|
||||||
width: 12px;
|
width: 12px;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
`;
|
`
|
||||||
|
|
||||||
export const Tooltip = styled.div`
|
export const Tooltip = styled('div')`
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: ${props => props.y}px;
|
top: ${props => props.y}px;
|
||||||
left: ${props => props.x}px;
|
left: ${props => props.x}px;
|
||||||
transform: translateX(-50%);
|
transform: translateX(-50%);
|
||||||
border: 1px solid ${props => props.theme.text};
|
border: 1px solid var(--text);
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
padding: 4px 8px;
|
padding: 4px 8px;
|
||||||
background-color: ${props => props.theme.background}${props => props.theme.mode === 'light' ? 'EE' : 'DD'};
|
background-color: var(--background);
|
||||||
max-width: 200px;
|
max-width: 200px;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
z-index: 100;
|
z-index: 100;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
`;
|
`
|
||||||
|
|
||||||
export const TooltipTitle = styled.span`
|
export const TooltipTitle = styled('span')`
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
display: block;
|
display: block;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
`;
|
`
|
||||||
|
|
||||||
export const TooltipDate = styled.span`
|
export const TooltipDate = styled('span')`
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
display: block;
|
display: block;
|
||||||
opacity: .8;
|
opacity: .8;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
`;
|
`
|
||||||
|
|
||||||
export const TooltipContent = styled.div`
|
export const TooltipContent = styled('div')`
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
padding: 4px 0;
|
padding: 4px 0;
|
||||||
`;
|
`
|
||||||
|
|
||||||
export const TooltipPerson = styled.span`
|
export const TooltipPerson = styled('span')`
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
margin: 2px;
|
margin: 2px;
|
||||||
padding: 1px 4px;
|
padding: 1px 4px;
|
||||||
border: 1px solid ${props => props.theme.primary};
|
border: 1px solid var(--primary);
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
|
|
||||||
${props => props.disabled && `
|
${props => props.disabled && `
|
||||||
opacity: .5;
|
opacity: .5;
|
||||||
border-color: ${props.theme.text}
|
border-color: var(--text);
|
||||||
`}
|
`}
|
||||||
`;
|
`
|
||||||
|
|
||||||
export const TimeLabels = styled.div`
|
export const TimeLabels = styled('div')`
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
width: 40px;
|
width: 40px;
|
||||||
padding-right: 6px;
|
padding-right: 6px;
|
||||||
`;
|
`
|
||||||
|
|
||||||
export const TimeSpace = styled.div`
|
export const TimeSpace = styled('div')`
|
||||||
height: 10px;
|
height: 10px;
|
||||||
position: relative;
|
position: relative;
|
||||||
border-top: 2px solid transparent;
|
border-top: 2px solid transparent;
|
||||||
|
|
@ -172,13 +172,13 @@ export const TimeSpace = styled.div`
|
||||||
45deg,
|
45deg,
|
||||||
transparent,
|
transparent,
|
||||||
transparent 4.3px,
|
transparent 4.3px,
|
||||||
${props => props.theme.loading} 4.3px,
|
var(--loading) 4.3px,
|
||||||
${props => props.theme.loading} 8.6px
|
var(--loading) 8.6px
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
`;
|
`
|
||||||
|
|
||||||
export const TimeLabel = styled.label`
|
export const TimeLabel = styled('label')`
|
||||||
display: block;
|
display: block;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: -.7em;
|
top: -.7em;
|
||||||
|
|
@ -186,28 +186,28 @@ export const TimeLabel = styled.label`
|
||||||
text-align: right;
|
text-align: right;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
`;
|
`
|
||||||
|
|
||||||
export const StyledMain = styled.div`
|
export const StyledMain = styled('div')`
|
||||||
width: 600px;
|
width: 600px;
|
||||||
margin: 20px auto;
|
margin: 20px auto;
|
||||||
max-width: calc(100% - 60px);
|
max-width: calc(100% - 60px);
|
||||||
`;
|
`
|
||||||
|
|
||||||
export const People = styled.div`
|
export const People = styled('div')`
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 5px;
|
gap: 5px;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
margin: 14px auto;
|
margin: 14px auto;
|
||||||
`;
|
`
|
||||||
|
|
||||||
export const Person = styled.button`
|
export const Person = styled('button')`
|
||||||
font: inherit;
|
font: inherit;
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
border: 1px solid ${props => props.theme.text};
|
border: 1px solid var(--text);
|
||||||
color: ${props => props.theme.text};
|
color: var(--text);
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
@ -215,17 +215,17 @@ export const Person = styled.button`
|
||||||
user-select: none;
|
user-select: none;
|
||||||
|
|
||||||
${props => props.filtered && `
|
${props => props.filtered && `
|
||||||
background: ${props.theme.primary};
|
background: var(--primary);
|
||||||
color: #FFFFFF;
|
color: #FFFFFF;
|
||||||
border-color: ${props.theme.primary};
|
border-color: var(--primary);
|
||||||
`}
|
`}
|
||||||
`;
|
`
|
||||||
|
|
||||||
export const Info = styled.span`
|
export const Info = styled('span')`
|
||||||
display: block;
|
display: block;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|
||||||
@media print {
|
@media print {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
`;
|
`
|
||||||
31
crabfit-frontend/src/components/Button/Button.jsx
Normal file
31
crabfit-frontend/src/components/Button/Button.jsx
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
import { Pressable } from './Button.styles'
|
||||||
|
|
||||||
|
const Button = ({
|
||||||
|
href,
|
||||||
|
type = 'button',
|
||||||
|
icon,
|
||||||
|
children,
|
||||||
|
secondary,
|
||||||
|
primaryColor,
|
||||||
|
secondaryColor,
|
||||||
|
small,
|
||||||
|
size,
|
||||||
|
...props
|
||||||
|
}) => (
|
||||||
|
<Pressable
|
||||||
|
type={type}
|
||||||
|
as={href ? 'a' : 'button'}
|
||||||
|
href={href}
|
||||||
|
$secondary={secondary}
|
||||||
|
$primaryColor={primaryColor}
|
||||||
|
$secondaryColor={secondaryColor}
|
||||||
|
$small={small}
|
||||||
|
$size={size}
|
||||||
|
{...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;
|
position: relative;
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
@ -11,12 +11,12 @@ export const Pressable = styled.button`
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
font: inherit;
|
font: inherit;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
background: ${props => props.primaryColor || props.theme.primary};
|
background: ${props => props.$primaryColor || 'var(--primary)'};
|
||||||
color: ${props => props.primaryColor ? '#FFF' : props.theme.background};
|
color: ${props => props.$primaryColor ? '#FFF' : 'var(--background)'};
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
transition: transform 150ms cubic-bezier(0, 0, 0.58, 1);
|
transition: transform 150ms cubic-bezier(0, 0, 0.58, 1);
|
||||||
border-radius: 3px;
|
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;
|
transform-style: preserve-3d;
|
||||||
margin-bottom: 5px;
|
margin-bottom: 5px;
|
||||||
|
|
||||||
|
|
@ -26,10 +26,10 @@ export const Pressable = styled.button`
|
||||||
margin-right: .5em;
|
margin-right: .5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
${props => props.size && `
|
${props => props.$size && `
|
||||||
padding: 0;
|
padding: 0;
|
||||||
height: ${props.size};
|
height: ${props.$size};
|
||||||
width: ${props.size};
|
width: ${props.$size};
|
||||||
`}
|
`}
|
||||||
|
|
||||||
&::before {
|
&::before {
|
||||||
|
|
@ -39,7 +39,7 @@ export const Pressable = styled.button`
|
||||||
width: 100%;
|
width: 100%;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
background: ${props => props.secondaryColor || props.theme.primaryDark};
|
background: ${props => props.$secondaryColor || 'var(--secondary)'};
|
||||||
border-radius: inherit;
|
border-radius: inherit;
|
||||||
transform: translate3d(0, 5px, -1em);
|
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);
|
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;
|
color: transparent;
|
||||||
cursor: wait;
|
cursor: wait;
|
||||||
|
|
||||||
|
|
@ -83,7 +83,7 @@ export const Pressable = styled.button`
|
||||||
left: calc(50% - 12px);
|
left: calc(50% - 12px);
|
||||||
height: 18px;
|
height: 18px;
|
||||||
width: 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-left-color: transparent;
|
||||||
border-radius: 100px;
|
border-radius: 100px;
|
||||||
animation: load .5s linear infinite;
|
animation: load .5s linear infinite;
|
||||||
|
|
@ -92,7 +92,7 @@ export const Pressable = styled.button`
|
||||||
@media (prefers-reduced-motion: reduce) {
|
@media (prefers-reduced-motion: reduce) {
|
||||||
&:after {
|
&:after {
|
||||||
content: 'loading...';
|
content: 'loading...';
|
||||||
color: ${props.primaryColor ? '#FFF' : props.theme.background};
|
color: ${props.$primaryColor ? '#FFF' : 'var(--background)'};
|
||||||
animation: none;
|
animation: none;
|
||||||
width: initial;
|
width: initial;
|
||||||
height: initial;
|
height: initial;
|
||||||
|
|
@ -108,10 +108,10 @@ export const Pressable = styled.button`
|
||||||
}
|
}
|
||||||
`}
|
`}
|
||||||
|
|
||||||
${props => props.secondary && `
|
${props => props.$secondary && `
|
||||||
background: transparent;
|
background: transparent;
|
||||||
border: 1px solid ${props.primaryColor || props.theme.mode === 'light' ? props.theme.primaryDark : props.theme.primaryLight};
|
border: 1px solid ${props.$primaryColor || 'var(--secondary)'};
|
||||||
color: ${props.primaryColor || props.theme.mode === 'light' ? props.theme.primaryDark : props.theme.primaryLight};
|
color: ${props.$primaryColor || 'var(--secondary)'};
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
|
|
||||||
&::before {
|
&::before {
|
||||||
|
|
@ -123,12 +123,12 @@ export const Pressable = styled.button`
|
||||||
`}
|
`}
|
||||||
|
|
||||||
@media print {
|
@media print {
|
||||||
${props => !props.secondary && `
|
${props => !props.$secondary && `
|
||||||
box-shadow: 0 4px 0 0 ${props.secondaryColor || props.theme.primaryDark};
|
box-shadow: 0 4px 0 0 ${props.$secondaryColor || 'var(--secondary)'};
|
||||||
`}
|
`}
|
||||||
|
|
||||||
&::before {
|
&::before {
|
||||||
display: none;
|
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 { useState, useEffect, useRef, forwardRef } from 'react'
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next'
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs'
|
||||||
import isToday from 'dayjs/plugin/isToday';
|
import isToday from 'dayjs/plugin/isToday'
|
||||||
import localeData from 'dayjs/plugin/localeData';
|
import localeData from 'dayjs/plugin/localeData'
|
||||||
import updateLocale from 'dayjs/plugin/updateLocale';
|
import updateLocale from 'dayjs/plugin/updateLocale'
|
||||||
|
|
||||||
import { Button, ToggleField } from 'components';
|
import { Button, ToggleField } from '/src/components'
|
||||||
import { useSettingsStore, useLocaleUpdateStore } from 'stores';
|
import { useSettingsStore, useLocaleUpdateStore } from '/src/stores'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Wrapper,
|
Wrapper,
|
||||||
|
|
@ -17,35 +17,35 @@ import {
|
||||||
CalendarBody,
|
CalendarBody,
|
||||||
Date,
|
Date,
|
||||||
Day,
|
Day,
|
||||||
} from './calendarFieldStyle';
|
} from './CalendarField.styles'
|
||||||
|
|
||||||
dayjs.extend(isToday);
|
dayjs.extend(isToday)
|
||||||
dayjs.extend(localeData);
|
dayjs.extend(localeData)
|
||||||
dayjs.extend(updateLocale);
|
dayjs.extend(updateLocale)
|
||||||
|
|
||||||
const calculateMonth = (month, year, weekStart) => {
|
const calculateMonth = (month, year, weekStart) => {
|
||||||
const date = dayjs().month(month).year(year);
|
const date = dayjs().month(month).year(year)
|
||||||
const daysInMonth = date.daysInMonth();
|
const daysInMonth = date.daysInMonth()
|
||||||
const daysBefore = date.date(1).day() - weekStart;
|
const daysBefore = date.date(1).day() - weekStart
|
||||||
const daysAfter = 6 - date.date(daysInMonth).day() + weekStart;
|
const daysAfter = 6 - date.date(daysInMonth).day() + weekStart
|
||||||
|
|
||||||
let dates = [];
|
const dates = []
|
||||||
let curDate = date.date(1).subtract(daysBefore, 'day');
|
let curDate = date.date(1).subtract(daysBefore, 'day')
|
||||||
let y = 0;
|
let y = 0
|
||||||
let x = 0;
|
let x = 0
|
||||||
for (let i = 0; i < daysBefore + daysInMonth + daysAfter; i++) {
|
for (let i = 0; i < daysBefore + daysInMonth + daysAfter; i++) {
|
||||||
if (x === 0) dates[y] = [];
|
if (x === 0) dates[y] = []
|
||||||
dates[y][x] = curDate.clone();
|
dates[y][x] = curDate.clone()
|
||||||
curDate = curDate.add(1, 'day');
|
curDate = curDate.add(1, 'day')
|
||||||
x++;
|
x++
|
||||||
if (x > 6) {
|
if (x > 6) {
|
||||||
x = 0;
|
x = 0
|
||||||
y++;
|
y++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return dates;
|
return dates
|
||||||
};
|
}
|
||||||
|
|
||||||
const CalendarField = forwardRef(({
|
const CalendarField = forwardRef(({
|
||||||
label,
|
label,
|
||||||
|
|
@ -54,48 +54,48 @@ const CalendarField = forwardRef(({
|
||||||
setValue,
|
setValue,
|
||||||
...props
|
...props
|
||||||
}, ref) => {
|
}, ref) => {
|
||||||
const weekStart = useSettingsStore(state => state.weekStart);
|
const weekStart = useSettingsStore(state => state.weekStart)
|
||||||
const locale = useLocaleUpdateStore(state => state.locale);
|
const locale = useLocaleUpdateStore(state => state.locale)
|
||||||
const { t } = useTranslation('home');
|
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 [dates, setDates] = useState(calculateMonth(dayjs().month(), dayjs().year(), weekStart))
|
||||||
const [month, setMonth] = useState(dayjs().month());
|
const [month, setMonth] = useState(dayjs().month())
|
||||||
const [year, setYear] = useState(dayjs().year());
|
const [year, setYear] = useState(dayjs().year())
|
||||||
|
|
||||||
const [selectedDates, setSelectedDates] = useState([]);
|
const [selectedDates, setSelectedDates] = useState([])
|
||||||
const [selectingDates, _setSelectingDates] = useState([]);
|
const [selectingDates, _setSelectingDates] = useState([])
|
||||||
const staticSelectingDates = useRef([]);
|
const staticSelectingDates = useRef([])
|
||||||
const setSelectingDates = newDates => {
|
const setSelectingDates = newDates => {
|
||||||
staticSelectingDates.current = newDates;
|
staticSelectingDates.current = newDates
|
||||||
_setSelectingDates(newDates);
|
_setSelectingDates(newDates)
|
||||||
};
|
}
|
||||||
|
|
||||||
const [selectedDays, setSelectedDays] = useState([]);
|
const [selectedDays, setSelectedDays] = useState([])
|
||||||
const [selectingDays, _setSelectingDays] = useState([]);
|
const [selectingDays, _setSelectingDays] = useState([])
|
||||||
const staticSelectingDays = useRef([]);
|
const staticSelectingDays = useRef([])
|
||||||
const setSelectingDays = newDays => {
|
const setSelectingDays = newDays => {
|
||||||
staticSelectingDays.current = newDays;
|
staticSelectingDays.current = newDays
|
||||||
_setSelectingDays(newDays);
|
_setSelectingDays(newDays)
|
||||||
};
|
}
|
||||||
|
|
||||||
const startPos = useRef({});
|
const startPos = useRef({})
|
||||||
const staticMode = useRef(null);
|
const staticMode = useRef(null)
|
||||||
const [mode, _setMode] = useState(staticMode.current);
|
const [mode, _setMode] = useState(staticMode.current)
|
||||||
const setMode = newMode => {
|
const setMode = newMode => {
|
||||||
staticMode.current = newMode;
|
staticMode.current = newMode
|
||||||
_setMode(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(() => {
|
useEffect(() => {
|
||||||
if (dayjs.Ls.hasOwnProperty(locale) && weekStart !== dayjs.Ls[locale].weekStart) {
|
if (dayjs.Ls?.[locale] && weekStart !== dayjs.Ls[locale].weekStart) {
|
||||||
dayjs.updateLocale(locale, { weekStart });
|
dayjs.updateLocale(locale, { weekStart })
|
||||||
}
|
}
|
||||||
setDates(calculateMonth(month, year, weekStart));
|
setDates(calculateMonth(month, year, weekStart))
|
||||||
}, [weekStart, month, year, locale]);
|
}, [weekStart, month, year, locale])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Wrapper locale={locale}>
|
<Wrapper locale={locale}>
|
||||||
|
|
@ -128,10 +128,10 @@ const CalendarField = forwardRef(({
|
||||||
title={t('form.dates.tooltips.previous')}
|
title={t('form.dates.tooltips.previous')}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (month-1 < 0) {
|
if (month-1 < 0) {
|
||||||
setYear(year-1);
|
setYear(year-1)
|
||||||
setMonth(11);
|
setMonth(11)
|
||||||
} else {
|
} else {
|
||||||
setMonth(month-1);
|
setMonth(month-1)
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
><</Button>
|
><</Button>
|
||||||
|
|
@ -141,10 +141,10 @@ const CalendarField = forwardRef(({
|
||||||
title={t('form.dates.tooltips.next')}
|
title={t('form.dates.tooltips.next')}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (month+1 > 11) {
|
if (month+1 > 11) {
|
||||||
setYear(year+1);
|
setYear(year+1)
|
||||||
setMonth(0);
|
setMonth(0)
|
||||||
} else {
|
} else {
|
||||||
setMonth(month+1);
|
setMonth(month+1)
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>></Button>
|
>></Button>
|
||||||
|
|
@ -170,37 +170,37 @@ const CalendarField = forwardRef(({
|
||||||
onKeyPress={e => {
|
onKeyPress={e => {
|
||||||
if (e.key === ' ' || e.key === 'Enter') {
|
if (e.key === ' ' || e.key === 'Enter') {
|
||||||
if (selectedDates.includes(date.format('DDMMYYYY'))) {
|
if (selectedDates.includes(date.format('DDMMYYYY'))) {
|
||||||
setSelectedDates(selectedDates.filter(d => d !== date.format('DDMMYYYY')));
|
setSelectedDates(selectedDates.filter(d => d !== date.format('DDMMYYYY')))
|
||||||
} else {
|
} else {
|
||||||
setSelectedDates([...selectedDates, date.format('DDMMYYYY')]);
|
setSelectedDates([...selectedDates, date.format('DDMMYYYY')])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
onPointerDown={e => {
|
onPointerDown={e => {
|
||||||
startPos.current = {x, y};
|
startPos.current = {x, y}
|
||||||
setMode(selectedDates.includes(date.format('DDMMYYYY')) ? 'remove' : 'add');
|
setMode(selectedDates.includes(date.format('DDMMYYYY')) ? 'remove' : 'add')
|
||||||
setSelectingDates([date]);
|
setSelectingDates([date])
|
||||||
e.currentTarget.releasePointerCapture(e.pointerId);
|
e.currentTarget.releasePointerCapture(e.pointerId)
|
||||||
|
|
||||||
document.addEventListener('pointerup', () => {
|
document.addEventListener('pointerup', () => {
|
||||||
if (staticMode.current === 'add') {
|
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') {
|
} else if (staticMode.current === 'remove') {
|
||||||
const toRemove = staticSelectingDates.current.map(d => d.format('DDMMYYYY'));
|
const toRemove = staticSelectingDates.current.map(d => d.format('DDMMYYYY'))
|
||||||
setSelectedDates(selectedDates.filter(d => !toRemove.includes(d)));
|
setSelectedDates(selectedDates.filter(d => !toRemove.includes(d)))
|
||||||
}
|
}
|
||||||
setMode(null);
|
setMode(null)
|
||||||
}, { once: true });
|
}, { once: true })
|
||||||
}}
|
}}
|
||||||
onPointerEnter={() => {
|
onPointerEnter={() => {
|
||||||
if (staticMode.current) {
|
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 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++) {
|
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>
|
>{date.date()}</Date>
|
||||||
|
|
@ -222,35 +222,35 @@ const CalendarField = forwardRef(({
|
||||||
onKeyPress={e => {
|
onKeyPress={e => {
|
||||||
if (e.key === ' ' || e.key === 'Enter') {
|
if (e.key === ' ' || e.key === 'Enter') {
|
||||||
if (selectedDays.includes(((i + weekStart) % 7 + 7) % 7)) {
|
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 {
|
} else {
|
||||||
setSelectedDays([...selectedDays, ((i + weekStart) % 7 + 7) % 7]);
|
setSelectedDays([...selectedDays, ((i + weekStart) % 7 + 7) % 7])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
onPointerDown={(e) => {
|
onPointerDown={e => {
|
||||||
startPos.current = i;
|
startPos.current = i
|
||||||
setMode(selectedDays.includes(((i + weekStart) % 7 + 7) % 7) ? 'remove' : 'add');
|
setMode(selectedDays.includes(((i + weekStart) % 7 + 7) % 7) ? 'remove' : 'add')
|
||||||
setSelectingDays([((i + weekStart) % 7 + 7) % 7]);
|
setSelectingDays([((i + weekStart) % 7 + 7) % 7])
|
||||||
e.currentTarget.releasePointerCapture(e.pointerId);
|
e.currentTarget.releasePointerCapture(e.pointerId)
|
||||||
|
|
||||||
document.addEventListener('pointerup', () => {
|
document.addEventListener('pointerup', () => {
|
||||||
if (staticMode.current === 'add') {
|
if (staticMode.current === 'add') {
|
||||||
setSelectedDays([...selectedDays, ...staticSelectingDays.current]);
|
setSelectedDays([...selectedDays, ...staticSelectingDays.current])
|
||||||
} else if (staticMode.current === 'remove') {
|
} else if (staticMode.current === 'remove') {
|
||||||
const toRemove = staticSelectingDays.current;
|
const toRemove = staticSelectingDays.current
|
||||||
setSelectedDays(selectedDays.filter(d => !toRemove.includes(d)));
|
setSelectedDays(selectedDays.filter(d => !toRemove.includes(d)))
|
||||||
}
|
}
|
||||||
setMode(null);
|
setMode(null)
|
||||||
}, { once: true });
|
}, { once: true })
|
||||||
}}
|
}}
|
||||||
onPointerEnter={() => {
|
onPointerEnter={() => {
|
||||||
if (staticMode.current) {
|
if (staticMode.current) {
|
||||||
let found = [];
|
const found = []
|
||||||
for (let ci = Math.min(startPos.current, i); ci < Math.max(startPos.current, i)+1; ci++) {
|
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>
|
>{name}</Date>
|
||||||
|
|
@ -258,7 +258,7 @@ const CalendarField = forwardRef(({
|
||||||
</CalendarBody>
|
</CalendarBody>
|
||||||
)}
|
)}
|
||||||
</Wrapper>
|
</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;
|
margin: 30px 0;
|
||||||
`;
|
`
|
||||||
|
|
||||||
export const StyledLabel = styled.label`
|
export const StyledLabel = styled('label')`
|
||||||
display: block;
|
display: block;
|
||||||
padding-bottom: 4px;
|
padding-bottom: 4px;
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
`;
|
`
|
||||||
|
|
||||||
export const StyledSubLabel = styled.label`
|
export const StyledSubLabel = styled('label')`
|
||||||
display: block;
|
display: block;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
opacity: .6;
|
opacity: .6;
|
||||||
`;
|
`
|
||||||
|
|
||||||
export const CalendarHeader = styled.div`
|
export const CalendarHeader = styled('div')`
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
|
@ -24,15 +24,15 @@ export const CalendarHeader = styled.div`
|
||||||
padding: 6px 0;
|
padding: 6px 0;
|
||||||
font-size: 1.2em;
|
font-size: 1.2em;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
`;
|
`
|
||||||
|
|
||||||
export const CalendarDays = styled.div`
|
export const CalendarDays = styled('div')`
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(7, 1fr);
|
grid-template-columns: repeat(7, 1fr);
|
||||||
grid-gap: 2px;
|
grid-gap: 2px;
|
||||||
`;
|
`
|
||||||
|
|
||||||
export const Day = styled.div`
|
export const Day = styled('div')`
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|
@ -44,9 +44,9 @@ export const Day = styled.div`
|
||||||
@media (max-width: 350px) {
|
@media (max-width: 350px) {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
`;
|
`
|
||||||
|
|
||||||
export const CalendarBody = styled.div`
|
export const CalendarBody = styled('div')`
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(7, 1fr);
|
grid-template-columns: repeat(7, 1fr);
|
||||||
grid-gap: 2px;
|
grid-gap: 2px;
|
||||||
|
|
@ -63,9 +63,9 @@ export const CalendarBody = styled.div`
|
||||||
& button:last-of-type {
|
& button:last-of-type {
|
||||||
border-bottom-right-radius: 3px;
|
border-bottom-right-radius: 3px;
|
||||||
}
|
}
|
||||||
`;
|
`
|
||||||
|
|
||||||
export const Date = styled.button`
|
export const Date = styled('button')`
|
||||||
font: inherit;
|
font: inherit;
|
||||||
color: inherit;
|
color: inherit;
|
||||||
background: none;
|
background: none;
|
||||||
|
|
@ -77,8 +77,8 @@ export const Date = styled.button`
|
||||||
transition: none;
|
transition: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
background-color: ${props => props.theme.primaryBackground};
|
background-color: var(--surface);
|
||||||
border: 1px solid ${props => props.theme.primary};
|
border: 1px solid var(--primary);
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|
@ -87,18 +87,18 @@ export const Date = styled.button`
|
||||||
touch-action: none;
|
touch-action: none;
|
||||||
|
|
||||||
${props => props.otherMonth && `
|
${props => props.otherMonth && `
|
||||||
color: ${props.theme.mode === 'light' ? props.theme.primaryLight : props.theme.primaryDark};
|
color: var(--tertiary);
|
||||||
`}
|
`}
|
||||||
${props => props.isToday && `
|
${props => props.isToday && `
|
||||||
font-weight: 900;
|
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)) && `
|
${props => (props.selected || (props.mode === 'add' && props.selecting)) && `
|
||||||
color: ${props.otherMonth ? 'rgba(255,255,255,.5)' : '#FFF'};
|
color: ${props.otherMonth ? 'rgba(255,255,255,.5)' : '#FFF'};
|
||||||
background-color: ${props.theme.primary};
|
background-color: var(--primary);
|
||||||
`}
|
`}
|
||||||
${props => props.mode === 'remove' && props.selecting && `
|
${props => props.mode === 'remove' && props.selecting && `
|
||||||
background-color: ${props.theme.primaryBackground};
|
background-color: var(--surface);
|
||||||
color: ${props.isToday ? props.theme.primaryDark : (props.otherMonth ? props.theme.primaryLight : 'inherit')};
|
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 { useState, useEffect, useRef } from 'react'
|
||||||
import { Button } from 'components';
|
import { useTranslation } from 'react-i18next'
|
||||||
import { useTWAStore } from 'stores';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
import { Button } from '/src/components'
|
||||||
|
import { useTWAStore } from '/src/stores'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Wrapper,
|
Wrapper,
|
||||||
Options,
|
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 PAYMENT_METHOD = 'https://play.google.com/billing'
|
||||||
const SKU = 'crab_donation';
|
const SKU = 'crab_donation'
|
||||||
|
|
||||||
const Donate = () => {
|
const Donate = () => {
|
||||||
const store = useTWAStore();
|
const store = useTWAStore()
|
||||||
const { t } = useTranslation('common');
|
const { t } = useTranslation('common')
|
||||||
|
|
||||||
const firstLinkRef = useRef();
|
const firstLinkRef = useRef()
|
||||||
const modalRef = useRef();
|
const modalRef = useRef()
|
||||||
const [isOpen, _setIsOpen] = useState(false);
|
const [isOpen, _setIsOpen] = useState(false)
|
||||||
const [closed, setClosed] = useState(false);
|
const [closed, setClosed] = useState(false)
|
||||||
|
|
||||||
const setIsOpen = open => {
|
const setIsOpen = open => {
|
||||||
_setIsOpen(open);
|
_setIsOpen(open)
|
||||||
|
|
||||||
if (open) {
|
if (open) {
|
||||||
window.setTimeout(() => firstLinkRef.current.focus(), 150);
|
window.setTimeout(() => firstLinkRef.current.focus(), 150)
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
const linkPressed = () => {
|
const linkPressed = () => {
|
||||||
setIsOpen(false);
|
setIsOpen(false)
|
||||||
gtag('event', 'donate', { 'event_category': 'donate' });
|
gtag('event', 'donate', { 'event_category': 'donate' })
|
||||||
};
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (store.TWA === undefined) {
|
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 = () => {}) => {
|
const acknowledge = async (token, type='repeatable', onComplete = () => {}) => {
|
||||||
try {
|
try {
|
||||||
let service = await window.getDigitalGoodsService(PAYMENT_METHOD);
|
const service = await window.getDigitalGoodsService(PAYMENT_METHOD)
|
||||||
await service.acknowledge(token, type);
|
await service.acknowledge(token, type)
|
||||||
if ('acknowledge' in service) {
|
if ('acknowledge' in service) {
|
||||||
// DGAPI 1.0
|
// DGAPI 1.0
|
||||||
service.acknowledge(token, type);
|
service.acknowledge(token, type)
|
||||||
} else {
|
} else {
|
||||||
// DGAPI 2.0
|
// DGAPI 2.0
|
||||||
service.consume(token);
|
service.consume(token)
|
||||||
}
|
}
|
||||||
onComplete();
|
onComplete()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const purchase = () => {
|
const purchase = () => {
|
||||||
if (!window.PaymentRequest) return false;
|
if (!window.PaymentRequest) return false
|
||||||
if (!window.getDigitalGoodsService) return false;
|
if (!window.getDigitalGoodsService) return false
|
||||||
|
|
||||||
const supportedInstruments = [{
|
const supportedInstruments = [{
|
||||||
supportedMethods: PAYMENT_METHOD,
|
supportedMethods: PAYMENT_METHOD,
|
||||||
data: {
|
data: {
|
||||||
sku: SKU
|
sku: SKU
|
||||||
}
|
}
|
||||||
}];
|
}]
|
||||||
|
|
||||||
const details = {
|
const details = {
|
||||||
total: {
|
total: {
|
||||||
label: 'Total',
|
label: 'Total',
|
||||||
amount: { currency: 'AUD', value: '0' }
|
amount: { currency: 'AUD', value: '0' }
|
||||||
},
|
},
|
||||||
};
|
}
|
||||||
|
|
||||||
const request = new PaymentRequest(supportedInstruments, details);
|
const request = new PaymentRequest(supportedInstruments, details)
|
||||||
|
|
||||||
request.show()
|
request.show()
|
||||||
.then(response => {
|
.then(response => {
|
||||||
response
|
response
|
||||||
.complete('success')
|
.complete('success')
|
||||||
.then(() => {
|
.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) {
|
if (response.details && response.details.token) {
|
||||||
const token = response.details.token;
|
const token = response.details.token
|
||||||
console.log(`Read Token: ${token.substring(0, 6)}...`);
|
console.log(`Read Token: ${token.substring(0, 6)}...`)
|
||||||
alert(t('donate.messages.success'));
|
alert(t('donate.messages.success'))
|
||||||
acknowledge(token);
|
acknowledge(token)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(e => {
|
.catch(e => {
|
||||||
console.error(e.message);
|
console.error(e.message)
|
||||||
alert(t('donate.messages.error'));
|
alert(t('donate.messages.error'))
|
||||||
});
|
})
|
||||||
})
|
})
|
||||||
.catch(e => {
|
.catch(e => {
|
||||||
console.error(e);
|
console.error(e)
|
||||||
alert(t('donate.messages.error'));
|
alert(t('donate.messages.error'))
|
||||||
});
|
})
|
||||||
};
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Wrapper>
|
<Wrapper>
|
||||||
|
|
@ -109,20 +110,20 @@ const Donate = () => {
|
||||||
title={t('donate.title')}
|
title={t('donate.title')}
|
||||||
onClick={event => {
|
onClick={event => {
|
||||||
if (closed) {
|
if (closed) {
|
||||||
event.preventDefault();
|
event.preventDefault()
|
||||||
return setClosed(false);
|
return setClosed(false)
|
||||||
}
|
}
|
||||||
if (store.TWA) {
|
if (store.TWA) {
|
||||||
gtag('event', 'donate', { 'event_category': 'donate' });
|
gtag('event', 'donate', { 'event_category': 'donate' })
|
||||||
event.preventDefault();
|
event.preventDefault()
|
||||||
if (window.confirm(t('donate.messages.about'))) {
|
if (window.confirm(t('donate.messages.about'))) {
|
||||||
if (purchase() === false) {
|
if (purchase() === false) {
|
||||||
alert(t('donate.messages.error'));
|
alert(t('donate.messages.error'))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
event.preventDefault();
|
event.preventDefault()
|
||||||
setIsOpen(true);
|
setIsOpen(true)
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
href="https://www.paypal.com/donate?business=N89X6YXRT5HKW&item_name=Crab+Fit+Donation¤cy_code=AUD&amount=5"
|
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>
|
>{t('donate.button')}</Button>
|
||||||
|
|
||||||
<Options
|
<Options
|
||||||
isOpen={isOpen}
|
$isOpen={isOpen}
|
||||||
ref={modalRef}
|
ref={modalRef}
|
||||||
onBlur={e => {
|
onBlur={e => {
|
||||||
if (modalRef.current.contains(e.relatedTarget)) return;
|
if (modalRef.current?.contains(e.relatedTarget)) return
|
||||||
setIsOpen(false);
|
setIsOpen(false)
|
||||||
if (e.relatedTarget && e.relatedTarget.id === 'donate_button') {
|
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>
|
<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>
|
</Options>
|
||||||
</Wrapper>
|
</Wrapper>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Donate;
|
export default Donate
|
||||||
|
|
@ -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-top: 6px;
|
margin-top: 6px;
|
||||||
margin-left: 12px;
|
margin-left: 12px;
|
||||||
position: relative;
|
position: relative;
|
||||||
`;
|
`
|
||||||
|
|
||||||
export const Options = styled.div`
|
export const Options = styled('div', forwardRef)`
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: calc(100% + 20px);
|
bottom: calc(100% + 20px);
|
||||||
right: 0;
|
right: 0;
|
||||||
background-color: ${props => props.theme.background};
|
background-color: var(--background);
|
||||||
${props => props.theme.mode === 'dark' && `
|
${/* FIXME: ${props => props.theme.mode === 'dark' && `
|
||||||
border: 1px solid ${props.theme.primaryBackground};
|
border: 1px solid ${props.theme.primaryBackground};
|
||||||
`}
|
`} */''}
|
||||||
z-index: 60;
|
z-index: 60;
|
||||||
padding: 4px 10px;
|
padding: 4px 10px;
|
||||||
border-radius: 14px;
|
border-radius: 14px;
|
||||||
|
|
@ -27,7 +28,7 @@ export const Options = styled.div`
|
||||||
transform: translateY(5px);
|
transform: translateY(5px);
|
||||||
transition: opacity .15s, transform .15s, visibility .15s;
|
transition: opacity .15s, transform .15s, visibility .15s;
|
||||||
|
|
||||||
${props => props.isOpen && `
|
${props => props.$isOpen && `
|
||||||
pointer-events: all;
|
pointer-events: all;
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
transform: translateY(0);
|
transform: translateY(0);
|
||||||
|
|
@ -48,8 +49,8 @@ export const Options = styled.div`
|
||||||
margin: 6px 0;
|
margin: 6px 0;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
border-radius: 100px;
|
border-radius: 100px;
|
||||||
background-color: ${props => props.theme.primary};
|
background-color: var(--primary);
|
||||||
color: ${props => props.theme.background};
|
color: var(--background);
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
|
|
@ -62,4 +63,4 @@ export const Options = styled.div`
|
||||||
@media (prefers-reduced-motion: reduce) {
|
@media (prefers-reduced-motion: reduce) {
|
||||||
transition: none;
|
transition: none;
|
||||||
}
|
}
|
||||||
`;
|
`
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
import { useState } from 'react';
|
import { useState } from 'react'
|
||||||
|
|
||||||
import { Loading } from 'components';
|
import { Loading } from '/src/components'
|
||||||
import { Image, Wrapper } from './eggStyle';
|
import { Image, Wrapper } from './Egg.styles'
|
||||||
|
|
||||||
const Egg = ({ eggKey, onClose }) => {
|
const Egg = ({ eggKey, onClose }) => {
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Wrapper title="Click anywhere to close" onClick={() => onClose()}>
|
<Wrapper title="Click anywhere to close" onClick={() => onClose()}>
|
||||||
|
|
@ -15,7 +15,7 @@ const Egg = ({ eggKey, onClose }) => {
|
||||||
/>
|
/>
|
||||||
{isLoading && <Loading />}
|
{isLoading && <Loading />}
|
||||||
</Wrapper>
|
</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;
|
position: fixed;
|
||||||
background: rgba(0,0,0,.6);
|
background: rgba(0,0,0,.6);
|
||||||
top: 0;
|
top: 0;
|
||||||
|
|
@ -14,10 +14,10 @@ export const Wrapper = styled.div`
|
||||||
align-items: center;
|
align-items: center;
|
||||||
z-index: 1000;
|
z-index: 1000;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
`;
|
`
|
||||||
|
|
||||||
export const Image = styled.img`
|
export const Image = styled('img')`
|
||||||
max-width: 80%;
|
max-width: 80%;
|
||||||
max-height: 80%;
|
max-height: 80%;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
`;
|
`
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { Wrapper, CloseButton } from './errorStyle';
|
import { Wrapper, CloseButton } from './Error.styles'
|
||||||
|
|
||||||
const Error = ({
|
const Error = ({
|
||||||
children,
|
children,
|
||||||
|
|
@ -12,6 +12,6 @@ const 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>
|
<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>
|
</CloseButton>
|
||||||
</Wrapper>
|
</Wrapper>
|
||||||
);
|
)
|
||||||
|
|
||||||
export default Error;
|
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;
|
border-radius: 3px;
|
||||||
background-color: ${props => props.theme.error};
|
background-color: var(--error);
|
||||||
color: #FFFFFF;
|
color: #FFFFFF;
|
||||||
padding: 0 16px;
|
padding: 0 16px;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
@ -27,9 +27,9 @@ export const Wrapper = styled.div`
|
||||||
@media (prefers-reduced-motion: reduce) {
|
@media (prefers-reduced-motion: reduce) {
|
||||||
transition: none;
|
transition: none;
|
||||||
}
|
}
|
||||||
`;
|
`
|
||||||
|
|
||||||
export const CloseButton = styled.button`
|
export const CloseButton = styled('button')`
|
||||||
border: 0;
|
border: 0;
|
||||||
background: none;
|
background: none;
|
||||||
height: 30px;
|
height: 30px;
|
||||||
|
|
@ -40,4 +40,4 @@ export const CloseButton = styled.button`
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
margin-left: 16px;
|
margin-left: 16px;
|
||||||
`;
|
`
|
||||||
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;
|
width: 600px;
|
||||||
margin: 20px auto;
|
margin: 20px auto;
|
||||||
max-width: calc(100% - 60px);
|
max-width: calc(100% - 60px);
|
||||||
|
|
@ -23,4 +23,4 @@ export const Wrapper = styled.footer`
|
||||||
@media print {
|
@media print {
|
||||||
display: none;
|
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 { useState, useEffect } from 'react'
|
||||||
import { loadGapiInsideDOM } from 'gapi-script';
|
import { loadGapiInsideDOM } from 'gapi-script'
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
import { Button, Center } from 'components';
|
import { Button, Center } from '/src/components'
|
||||||
import { Loader } from '../Loading/loadingStyle';
|
import { Loader } from '../Loading/Loading.styles'
|
||||||
import {
|
import {
|
||||||
CalendarList,
|
CalendarList,
|
||||||
CheckboxInput,
|
CheckboxInput,
|
||||||
|
|
@ -14,22 +14,22 @@ import {
|
||||||
Title,
|
Title,
|
||||||
Icon,
|
Icon,
|
||||||
LinkButton,
|
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 GoogleCalendar = ({ timeZone, timeMin, timeMax, onImport }) => {
|
||||||
const [signedIn, setSignedIn] = useState(undefined);
|
const [signedIn, setSignedIn] = useState(undefined)
|
||||||
const [calendars, setCalendars] = useState(undefined);
|
const [calendars, setCalendars] = useState(undefined)
|
||||||
const [freeBusyLoading, setFreeBusyLoading] = useState(false);
|
const [freeBusyLoading, setFreeBusyLoading] = useState(false)
|
||||||
const { t } = useTranslation('event');
|
const { t } = useTranslation('event')
|
||||||
|
|
||||||
const calendarLogin = async () => {
|
const calendarLogin = async () => {
|
||||||
const gapi = await loadGapiInsideDOM();
|
const gapi = await loadGapiInsideDOM()
|
||||||
gapi.load('client:auth2', () => {
|
gapi.load('client:auth2', () => {
|
||||||
window.gapi.client.init({
|
window.gapi.client.init({
|
||||||
clientId: '276505195333-9kjl7e48m272dljbspkobctqrpet0n8m.apps.googleusercontent.com',
|
clientId: '276505195333-9kjl7e48m272dljbspkobctqrpet0n8m.apps.googleusercontent.com',
|
||||||
|
|
@ -38,23 +38,23 @@ const GoogleCalendar = ({ timeZone, timeMin, timeMax, onImport }) => {
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
// Listen for state changes
|
// 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
|
// Handle initial sign-in state
|
||||||
setSignedIn(window.gapi.auth2.getAuthInstance().isSignedIn.get());
|
setSignedIn(window.gapi.auth2.getAuthInstance().isSignedIn.get())
|
||||||
})
|
})
|
||||||
.catch(e => {
|
.catch(e => {
|
||||||
console.error(e);
|
console.error(e)
|
||||||
setSignedIn(false);
|
setSignedIn(false)
|
||||||
});
|
})
|
||||||
});
|
})
|
||||||
};
|
}
|
||||||
|
|
||||||
const importAvailability = () => {
|
const importAvailability = () => {
|
||||||
setFreeBusyLoading(true);
|
setFreeBusyLoading(true)
|
||||||
gtag('event', 'google_cal_sync', {
|
gtag('event', 'google_cal_sync', {
|
||||||
'event_category': 'event',
|
'event_category': 'event',
|
||||||
});
|
})
|
||||||
window.gapi.client.calendar.freebusy.query({
|
window.gapi.client.calendar.freebusy.query({
|
||||||
timeMin,
|
timeMin,
|
||||||
timeMax,
|
timeMax,
|
||||||
|
|
@ -62,15 +62,15 @@ const GoogleCalendar = ({ timeZone, timeMin, timeMax, onImport }) => {
|
||||||
items: calendars.filter(c => c.checked).map(c => ({id: c.id})),
|
items: calendars.filter(c => c.checked).map(c => ({id: c.id})),
|
||||||
})
|
})
|
||||||
.then(response => {
|
.then(response => {
|
||||||
onImport(response.result.calendars ? Object.values(response.result.calendars).reduce((busy, c) => [...busy, ...c.busy], []) : []);
|
onImport(response.result.calendars ? Object.values(response.result.calendars).reduce((busy, c) => [...busy, ...c.busy], []) : [])
|
||||||
setFreeBusyLoading(false);
|
setFreeBusyLoading(false)
|
||||||
}, e => {
|
}, e => {
|
||||||
console.error(e);
|
console.error(e)
|
||||||
setFreeBusyLoading(false);
|
setFreeBusyLoading(false)
|
||||||
});
|
})
|
||||||
};
|
}
|
||||||
|
|
||||||
useEffect(() => calendarLogin(), []);
|
useEffect(() => calendarLogin(), [])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (signedIn) {
|
if (signedIn) {
|
||||||
|
|
@ -83,15 +83,15 @@ const GoogleCalendar = ({ timeZone, timeMin, timeMax, onImport }) => {
|
||||||
'description': item.description,
|
'description': item.description,
|
||||||
'id': item.id,
|
'id': item.id,
|
||||||
'color': item.backgroundColor,
|
'color': item.backgroundColor,
|
||||||
'checked': item.hasOwnProperty('primary') && item.primary === true,
|
'checked': item.primary === true,
|
||||||
})));
|
})))
|
||||||
})
|
})
|
||||||
.catch(e => {
|
.catch(e => {
|
||||||
console.error(e);
|
console.error(e)
|
||||||
signOut();
|
signOut()
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
}, [signedIn]);
|
}, [signedIn])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|
@ -113,21 +113,21 @@ const GoogleCalendar = ({ timeZone, timeMin, timeMax, onImport }) => {
|
||||||
<Icon src={googleLogo} alt="" />
|
<Icon src={googleLogo} alt="" />
|
||||||
<strong>{t('event:you.google_cal.login')}</strong>
|
<strong>{t('event:you.google_cal.login')}</strong>
|
||||||
(<LinkButton type="button" onClick={e => {
|
(<LinkButton type="button" onClick={e => {
|
||||||
e.preventDefault();
|
e.preventDefault()
|
||||||
signOut();
|
signOut()
|
||||||
}}>{t('event:you.google_cal.logout')}</LinkButton>)
|
}}>{t('event:you.google_cal.logout')}</LinkButton>)
|
||||||
</Title>
|
</Title>
|
||||||
<Options>
|
<Options>
|
||||||
{calendars !== undefined && !calendars.every(c => c.checked) && (
|
{calendars !== undefined && !calendars.every(c => c.checked) && (
|
||||||
<LinkButton type="button" onClick={e => {
|
<LinkButton type="button" onClick={e => {
|
||||||
e.preventDefault();
|
e.preventDefault()
|
||||||
setCalendars(calendars.map(c => ({...c, checked: true})));
|
setCalendars(calendars.map(c => ({...c, checked: true})))
|
||||||
}}>{t('event:you.google_cal.select_all')}</LinkButton>
|
}}>{t('event:you.google_cal.select_all')}</LinkButton>
|
||||||
)}
|
)}
|
||||||
{calendars !== undefined && calendars.every(c => c.checked) && (
|
{calendars !== undefined && calendars.every(c => c.checked) && (
|
||||||
<LinkButton type="button" onClick={e => {
|
<LinkButton type="button" onClick={e => {
|
||||||
e.preventDefault();
|
e.preventDefault()
|
||||||
setCalendars(calendars.map(c => ({...c, checked: false})));
|
setCalendars(calendars.map(c => ({...c, checked: false})))
|
||||||
}}>{t('event:you.google_cal.select_none')}</LinkButton>
|
}}>{t('event:you.google_cal.select_none')}</LinkButton>
|
||||||
)}
|
)}
|
||||||
</Options>
|
</Options>
|
||||||
|
|
@ -139,7 +139,7 @@ const GoogleCalendar = ({ timeZone, timeMin, timeMax, onImport }) => {
|
||||||
id={calendar.id}
|
id={calendar.id}
|
||||||
color={calendar.color}
|
color={calendar.color}
|
||||||
checked={calendar.checked}
|
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} />
|
<CheckboxLabel htmlFor={calendar.id} color={calendar.color} />
|
||||||
<CalendarLabel htmlFor={calendar.id}>{calendar.name}</CalendarLabel>
|
<CalendarLabel htmlFor={calendar.id}>{calendar.name}</CalendarLabel>
|
||||||
|
|
@ -161,7 +161,7 @@ const GoogleCalendar = ({ timeZone, timeMin, timeMax, onImport }) => {
|
||||||
</CalendarList>
|
</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%;
|
width: 100%;
|
||||||
& > div {
|
& > div {
|
||||||
display: flex;
|
display: flex;
|
||||||
margin: 2px 0;
|
margin: 2px 0;
|
||||||
}
|
}
|
||||||
`;
|
`
|
||||||
|
|
||||||
export const CheckboxInput = styled.input`
|
export const CheckboxInput = styled('input')`
|
||||||
height: 0px;
|
height: 0px;
|
||||||
width: 0px;
|
width: 0px;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
|
@ -27,8 +27,8 @@ export const CheckboxInput = styled.input`
|
||||||
opacity: .6;
|
opacity: .6;
|
||||||
}
|
}
|
||||||
&[disabled] + label:after {
|
&[disabled] + label:after {
|
||||||
border: 2px solid ${props => props.theme.text};
|
border: 2px solid var(--text);
|
||||||
background-color: ${props => props.theme.text};
|
background-color: var(--text);
|
||||||
}
|
}
|
||||||
&:focus + label {
|
&:focus + label {
|
||||||
box-shadow: 0 0 0 2px ${props => props.theme.text}44;
|
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;
|
box-shadow: 0 0 0 2px ${props => props.color || props.theme.primary}44;
|
||||||
background-color: ${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;
|
display: inline-block;
|
||||||
height: 24px;
|
height: 24px;
|
||||||
width: 24px;
|
width: 24px;
|
||||||
|
|
@ -55,7 +55,7 @@ export const CheckboxLabel = styled.label`
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
height: 14px;
|
height: 14px;
|
||||||
width: 14px;
|
width: 14px;
|
||||||
border: 2px solid ${props => props.theme.text};
|
border: 2px solid var(--text);
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 3px;
|
top: 3px;
|
||||||
|
|
@ -66,8 +66,8 @@ export const CheckboxLabel = styled.label`
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
height: 14px;
|
height: 14px;
|
||||||
width: 14px;
|
width: 14px;
|
||||||
border: 2px solid ${props => props.color || props.theme.primary};
|
border: 2px solid ${props => props.color || 'var(--primary)'};
|
||||||
background-color: ${props => props.color || props.theme.primary};
|
background-color: ${props => props.color || 'var(--primary)'};
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 3px;
|
top: 3px;
|
||||||
|
|
@ -80,37 +80,37 @@ export const CheckboxLabel = styled.label`
|
||||||
transform: scale(.5);
|
transform: scale(.5);
|
||||||
transition: opacity 0.15s, transform 0.15s;
|
transition: opacity 0.15s, transform 0.15s;
|
||||||
}
|
}
|
||||||
`;
|
`
|
||||||
|
|
||||||
export const CalendarLabel = styled.label`
|
export const CalendarLabel = styled('label')`
|
||||||
margin-left: .6em;
|
margin-left: .6em;
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
line-height: 24px;
|
line-height: 24px;
|
||||||
`;
|
`
|
||||||
|
|
||||||
export const Info = styled.div`
|
export const Info = styled('div')`
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
opacity: .6;
|
opacity: .6;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
padding: 14px 0 10px;
|
padding: 14px 0 10px;
|
||||||
`;
|
`
|
||||||
|
|
||||||
export const Options = styled.div`
|
export const Options = styled('div')`
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
padding: 0 0 5px;
|
padding: 0 0 5px;
|
||||||
`;
|
`
|
||||||
|
|
||||||
export const Title = styled.p`
|
export const Title = styled('p')`
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
& strong {
|
& strong {
|
||||||
margin-right: 1ex;
|
margin-right: 1ex;
|
||||||
}
|
}
|
||||||
`;
|
`
|
||||||
|
|
||||||
export const Icon = styled.img`
|
export const Icon = styled('img')`
|
||||||
height: 24px;
|
height: 24px;
|
||||||
width: 24px;
|
width: 24px;
|
||||||
margin-right: 12px;
|
margin-right: 12px;
|
||||||
|
|
@ -118,11 +118,11 @@ export const Icon = styled.img`
|
||||||
${props => props.theme.mode === 'light' && `
|
${props => props.theme.mode === 'light' && `
|
||||||
filter: invert(1);
|
filter: invert(1);
|
||||||
`}
|
`}
|
||||||
`;
|
`
|
||||||
|
|
||||||
export const LinkButton = styled.button`
|
export const LinkButton = styled('button')`
|
||||||
font: inherit;
|
font: inherit;
|
||||||
color: ${props => props.theme.primary};
|
color: var(--primary);
|
||||||
border: 0;
|
border: 0;
|
||||||
background: none;
|
background: none;
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
|
|
@ -131,4 +131,4 @@ export const LinkButton = styled.button`
|
||||||
display: inline;
|
display: inline;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
appearance: none;
|
appearance: none;
|
||||||
`;
|
`
|
||||||
|
|
@ -1,25 +1,23 @@
|
||||||
import { useTheme } from '@emotion/react';
|
import { useTranslation } from 'react-i18next'
|
||||||
import { useSettingsStore } from 'stores';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useSettingsStore } from '/src/stores'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Wrapper,
|
Wrapper,
|
||||||
Label,
|
Label,
|
||||||
Bar,
|
Bar,
|
||||||
Grade,
|
Grade,
|
||||||
} from './legendStyle';
|
} from './Legend.styles'
|
||||||
|
|
||||||
const Legend = ({
|
const Legend = ({
|
||||||
min,
|
min,
|
||||||
max,
|
max,
|
||||||
total,
|
total,
|
||||||
onSegmentFocus,
|
onSegmentFocus,
|
||||||
...props
|
|
||||||
}) => {
|
}) => {
|
||||||
const theme = useTheme();
|
const { t } = useTranslation('event')
|
||||||
const { t } = useTranslation('event');
|
const highlight = useSettingsStore(state => state.highlight)
|
||||||
const highlight = useSettingsStore(state => state.highlight);
|
const setHighlight = useSettingsStore(state => state.setHighlight)
|
||||||
const setHighlight = useSettingsStore(state => state.setHighlight);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Wrapper>
|
<Wrapper>
|
||||||
|
|
@ -33,7 +31,7 @@ const Legend = ({
|
||||||
{[...Array(max+1-min).keys()].map(i => i+min).map(i =>
|
{[...Array(max+1-min).keys()].map(i => i+min).map(i =>
|
||||||
<Grade
|
<Grade
|
||||||
key={i}
|
key={i}
|
||||||
color={`${theme.primary}${Math.round((i/(max))*255).toString(16)}`}
|
color={`#FF0000${Math.round((i/(max))*255).toString(16)}`}
|
||||||
highlight={highlight && i === max && max > 0}
|
highlight={highlight && i === max && max > 0}
|
||||||
onMouseOver={() => onSegmentFocus(i)}
|
onMouseOver={() => onSegmentFocus(i)}
|
||||||
/>
|
/>
|
||||||
|
|
@ -42,7 +40,7 @@ const Legend = ({
|
||||||
|
|
||||||
<Label>{max}/{total} {t('event:available')}</Label>
|
<Label>{max}/{total} {t('event:available')}</Label>
|
||||||
</Wrapper>
|
</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;
|
margin: 10px 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
@ -13,40 +13,40 @@ export const Wrapper = styled.div`
|
||||||
@media (max-width: 400px) {
|
@media (max-width: 400px) {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
`;
|
`
|
||||||
|
|
||||||
export const Label = styled.label`
|
export const Label = styled('label')`
|
||||||
display: block;
|
display: block;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
`;
|
`
|
||||||
|
|
||||||
export const Bar = styled.div`
|
export const Bar = styled('div')`
|
||||||
display: flex;
|
display: flex;
|
||||||
width: 40%;
|
width: 40%;
|
||||||
height: 20px;
|
height: 20px;
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
margin: 0 8px;
|
margin: 0 8px;
|
||||||
border: 1px solid ${props => props.theme.text};
|
border: 1px solid var(--text);
|
||||||
|
|
||||||
@media (max-width: 400px) {
|
@media (max-width: 400px) {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin: 8px 0;
|
margin: 8px 0;
|
||||||
}
|
}
|
||||||
`;
|
`
|
||||||
|
|
||||||
export const Grade = styled.div`
|
export const Grade = styled('div')`
|
||||||
flex: 1;
|
flex: 1;
|
||||||
background-color: ${props => props.color};
|
background-color: ${props => props.color};
|
||||||
|
|
||||||
${props => props.highlight && `
|
${props => props.highlight && `
|
||||||
background-image: repeating-linear-gradient(
|
background-image: repeating-linear-gradient(
|
||||||
45deg,
|
45deg,
|
||||||
${props.theme.primary},
|
var(--primary),
|
||||||
${props.theme.primary} 4.5px,
|
var(--primary) 4.5px,
|
||||||
${props.theme.primaryDark} 4.5px,
|
var(--secondary) 4.5px,
|
||||||
${props.theme.primaryDark} 9px
|
var(--secondary) 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;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
`;
|
`
|
||||||
|
|
||||||
export const Loader = styled.div`
|
export const Loader = styled('div')`
|
||||||
@keyframes load {
|
@keyframes load {
|
||||||
from {
|
from {
|
||||||
transform: rotate(0deg);
|
transform: rotate(0deg);
|
||||||
|
|
@ -19,7 +19,7 @@ export const Loader = styled.div`
|
||||||
|
|
||||||
height: 24px;
|
height: 24px;
|
||||||
width: 24px;
|
width: 24px;
|
||||||
border: 3px solid ${props => props.theme.primary};
|
border: 3px solid var(--primary);
|
||||||
border-left-color: transparent;
|
border-left-color: transparent;
|
||||||
border-radius: 100px;
|
border-radius: 100px;
|
||||||
animation: load .5s linear infinite;
|
animation: load .5s linear infinite;
|
||||||
|
|
@ -32,4 +32,4 @@ export const Loader = styled.div`
|
||||||
content: 'loading...';
|
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 { useTranslation } from 'react-i18next'
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Wrapper,
|
Wrapper,
|
||||||
|
|
@ -8,12 +8,12 @@ import {
|
||||||
Image,
|
Image,
|
||||||
Title,
|
Title,
|
||||||
Tagline,
|
Tagline,
|
||||||
} from './logoStyle';
|
} from './Logo.styles'
|
||||||
|
|
||||||
import image from 'res/logo.svg';
|
import image from '/src/res/logo.svg'
|
||||||
|
|
||||||
const Logo = () => {
|
const Logo = () => {
|
||||||
const { t } = useTranslation('common');
|
const { t } = useTranslation('common')
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Wrapper>
|
<Wrapper>
|
||||||
|
|
@ -25,7 +25,7 @@ const Logo = () => {
|
||||||
<Tagline>{t('common:tagline')}</Tagline>
|
<Tagline>{t('common:tagline')}</Tagline>
|
||||||
</A>
|
</A>
|
||||||
</Wrapper>
|
</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;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
`;
|
`
|
||||||
|
|
||||||
export const A = styled.a`
|
export const A = styled('a')`
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
|
|
||||||
@keyframes jelly {
|
@keyframes jelly {
|
||||||
|
|
@ -32,30 +32,30 @@ export const A = styled.a`
|
||||||
animation: none;
|
animation: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`;
|
`
|
||||||
|
|
||||||
export const Top = styled.div`
|
export const Top = styled('div')`
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
`;
|
`
|
||||||
|
|
||||||
export const Image = styled.img`
|
export const Image = styled('img')`
|
||||||
width: 2.5rem;
|
width: 2.5rem;
|
||||||
margin-right: 16px;
|
margin-right: 16px;
|
||||||
`;
|
`
|
||||||
|
|
||||||
export const Title = styled.span`
|
export const Title = styled('span')`
|
||||||
display: block;
|
display: block;
|
||||||
font-size: 2rem;
|
font-size: 2rem;
|
||||||
color: ${props => props.theme.primary};
|
color: var(--primary);
|
||||||
font-family: 'Molot', sans-serif;
|
font-family: 'Molot', sans-serif;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
text-shadow: 0 2px 0 ${props => props.theme.primaryDark};
|
text-shadow: 0 2px 0 var(--secondary);
|
||||||
line-height: 1em;
|
line-height: 1em;
|
||||||
`;
|
`
|
||||||
|
|
||||||
export const Tagline = styled.span`
|
export const Tagline = styled('span')`
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
padding-top: 2px;
|
padding-top: 2px;
|
||||||
|
|
@ -66,4 +66,4 @@ export const Tagline = styled.span`
|
||||||
@media print {
|
@media print {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
`;
|
`
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react'
|
||||||
import { PublicClientApplication } from "@azure/msal-browser";
|
import { PublicClientApplication } from '@azure/msal-browser'
|
||||||
import { Client } from "@microsoft/microsoft-graph-client";
|
import { Client } from '@microsoft/microsoft-graph-client'
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
import { Button, Center } from 'components';
|
import { Button, Center } from '/src/components'
|
||||||
import { Loader } from '../Loading/loadingStyle';
|
import { Loader } from '../Loading/Loading.styles'
|
||||||
import {
|
import {
|
||||||
CalendarList,
|
CalendarList,
|
||||||
CheckboxInput,
|
CheckboxInput,
|
||||||
|
|
@ -15,11 +15,11 @@ import {
|
||||||
Title,
|
Title,
|
||||||
Icon,
|
Icon,
|
||||||
LinkButton,
|
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
|
// Initialise the MSAL object
|
||||||
const publicClientApplication = new PublicClientApplication({
|
const publicClientApplication = new PublicClientApplication({
|
||||||
|
|
@ -31,69 +31,69 @@ const publicClientApplication = new PublicClientApplication({
|
||||||
cacheLocation: 'sessionStorage',
|
cacheLocation: 'sessionStorage',
|
||||||
storeAuthStateInCookie: true,
|
storeAuthStateInCookie: true,
|
||||||
},
|
},
|
||||||
});
|
})
|
||||||
|
|
||||||
const getAuthenticatedClient = accessToken => {
|
const getAuthenticatedClient = accessToken => {
|
||||||
const client = Client.init({
|
const client = Client.init({
|
||||||
authProvider: done => done(null, accessToken),
|
authProvider: done => done(null, accessToken),
|
||||||
});
|
})
|
||||||
return client;
|
return client
|
||||||
};
|
}
|
||||||
|
|
||||||
const OutlookCalendar = ({ timeZone, timeMin, timeMax, onImport }) => {
|
const OutlookCalendar = ({ timeZone, timeMin, timeMax, onImport }) => {
|
||||||
const [client, setClient] = useState(undefined);
|
const [client, setClient] = useState(undefined)
|
||||||
const [calendars, setCalendars] = useState(undefined);
|
const [calendars, setCalendars] = useState(undefined)
|
||||||
const [freeBusyLoading, setFreeBusyLoading] = useState(false);
|
const [freeBusyLoading, setFreeBusyLoading] = useState(false)
|
||||||
const { t } = useTranslation('event');
|
const { t } = useTranslation('event')
|
||||||
|
|
||||||
const checkLogin = async () => {
|
const checkLogin = async () => {
|
||||||
const accounts = publicClientApplication.getAllAccounts();
|
const accounts = publicClientApplication.getAllAccounts()
|
||||||
if (accounts && accounts.length > 0) {
|
if (accounts && accounts.length > 0) {
|
||||||
try {
|
try {
|
||||||
const accessToken = await getAccessToken();
|
const accessToken = await getAccessToken()
|
||||||
setClient(getAuthenticatedClient(accessToken));
|
setClient(getAuthenticatedClient(accessToken))
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e)
|
||||||
signOut();
|
signOut()
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
setClient(null);
|
setClient(null)
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
const signIn = async () => {
|
const signIn = async () => {
|
||||||
try {
|
try {
|
||||||
await publicClientApplication.loginPopup({ scopes });
|
await publicClientApplication.loginPopup({ scopes })
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e)
|
||||||
} finally {
|
} finally {
|
||||||
checkLogin();
|
checkLogin()
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
const signOut = async () => {
|
const signOut = async () => {
|
||||||
try {
|
try {
|
||||||
await publicClientApplication.logoutRedirect({
|
await publicClientApplication.logoutRedirect({
|
||||||
onRedirectNavigate: () => false,
|
onRedirectNavigate: () => false,
|
||||||
});
|
})
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e)
|
||||||
} finally {
|
} finally {
|
||||||
checkLogin();
|
checkLogin()
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
const getAccessToken = async () => {
|
const getAccessToken = async () => {
|
||||||
try {
|
try {
|
||||||
const accounts = publicClientApplication.getAllAccounts();
|
const accounts = publicClientApplication.getAllAccounts()
|
||||||
if (accounts.length <= 0) throw new Error('login_required');
|
if (accounts.length <= 0) throw new Error('login_required')
|
||||||
|
|
||||||
// Try to get silently
|
// Try to get silently
|
||||||
const result = await publicClientApplication.acquireTokenSilent({
|
const result = await publicClientApplication.acquireTokenSilent({
|
||||||
scopes,
|
scopes,
|
||||||
account: accounts[0],
|
account: accounts[0],
|
||||||
});
|
})
|
||||||
return result.accessToken;
|
return result.accessToken
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if ([
|
if ([
|
||||||
'consent_required',
|
'consent_required',
|
||||||
|
|
@ -102,19 +102,19 @@ const OutlookCalendar = ({ timeZone, timeMin, timeMax, onImport }) => {
|
||||||
'no_account_in_silent_request'
|
'no_account_in_silent_request'
|
||||||
].includes(e.message)) {
|
].includes(e.message)) {
|
||||||
// Try to get with popup
|
// Try to get with popup
|
||||||
const result = await publicClientApplication.acquireTokenPopup({ scopes });
|
const result = await publicClientApplication.acquireTokenPopup({ scopes })
|
||||||
return result.accessToken;
|
return result.accessToken
|
||||||
} else {
|
} else {
|
||||||
throw e;
|
throw e
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
const importAvailability = () => {
|
const importAvailability = () => {
|
||||||
setFreeBusyLoading(true);
|
setFreeBusyLoading(true)
|
||||||
gtag('event', 'outlook_cal_sync', {
|
gtag('event', 'outlook_cal_sync', {
|
||||||
'event_category': 'event',
|
'event_category': 'event',
|
||||||
});
|
})
|
||||||
client.api('/me/calendar/getSchedule').post({
|
client.api('/me/calendar/getSchedule').post({
|
||||||
schedules: calendars.filter(c => c.checked).map(c => c.id),
|
schedules: calendars.filter(c => c.checked).map(c => c.id),
|
||||||
startTime: {
|
startTime: {
|
||||||
|
|
@ -128,17 +128,16 @@ const OutlookCalendar = ({ timeZone, timeMin, timeMax, onImport }) => {
|
||||||
availabilityViewInterval: 30,
|
availabilityViewInterval: 30,
|
||||||
})
|
})
|
||||||
.then(response => {
|
.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 => {
|
.catch(e => {
|
||||||
console.error(e);
|
console.error(e)
|
||||||
signOut();
|
signOut()
|
||||||
})
|
})
|
||||||
.finally(() => setFreeBusyLoading(false));
|
.finally(() => setFreeBusyLoading(false))
|
||||||
};
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line
|
useEffect(() => checkLogin(), [])
|
||||||
useEffect(() => checkLogin(), []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (client) {
|
if (client) {
|
||||||
|
|
@ -150,15 +149,14 @@ const OutlookCalendar = ({ timeZone, timeMin, timeMax, onImport }) => {
|
||||||
'id': item.owner.address,
|
'id': item.owner.address,
|
||||||
'color': item.hexColor,
|
'color': item.hexColor,
|
||||||
'checked': item.isDefaultCalendar === true,
|
'checked': item.isDefaultCalendar === true,
|
||||||
})));
|
})))
|
||||||
})
|
})
|
||||||
.catch(e => {
|
.catch(e => {
|
||||||
console.error(e);
|
console.error(e)
|
||||||
signOut();
|
signOut()
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line
|
}, [client])
|
||||||
}, [client]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|
@ -170,9 +168,7 @@ const OutlookCalendar = ({ timeZone, timeMin, timeMax, onImport }) => {
|
||||||
primaryColor="#0364B9"
|
primaryColor="#0364B9"
|
||||||
secondaryColor="#02437D"
|
secondaryColor="#02437D"
|
||||||
icon={<img aria-hidden="true" focusable="false" src={outlookLogo} alt="" />}
|
icon={<img aria-hidden="true" focusable="false" src={outlookLogo} alt="" />}
|
||||||
>
|
>{t('event:you.outlook_cal')}</Button>
|
||||||
{t('event:you.outlook_cal')}
|
|
||||||
</Button>
|
|
||||||
</Center>
|
</Center>
|
||||||
) : (
|
) : (
|
||||||
<CalendarList>
|
<CalendarList>
|
||||||
|
|
@ -180,21 +176,21 @@ const OutlookCalendar = ({ timeZone, timeMin, timeMax, onImport }) => {
|
||||||
<Icon src={outlookLogo} alt="" />
|
<Icon src={outlookLogo} alt="" />
|
||||||
<strong>{t('event:you.outlook_cal')}</strong>
|
<strong>{t('event:you.outlook_cal')}</strong>
|
||||||
(<LinkButton type="button" onClick={e => {
|
(<LinkButton type="button" onClick={e => {
|
||||||
e.preventDefault();
|
e.preventDefault()
|
||||||
signOut();
|
signOut()
|
||||||
}}>{t('event:you.google_cal.logout')}</LinkButton>)
|
}}>{t('event:you.google_cal.logout')}</LinkButton>)
|
||||||
</Title>
|
</Title>
|
||||||
<Options>
|
<Options>
|
||||||
{calendars !== undefined && !calendars.every(c => c.checked) && (
|
{calendars !== undefined && !calendars.every(c => c.checked) && (
|
||||||
<LinkButton type="button" onClick={e => {
|
<LinkButton type="button" onClick={e => {
|
||||||
e.preventDefault();
|
e.preventDefault()
|
||||||
setCalendars(calendars.map(c => ({...c, checked: true})));
|
setCalendars(calendars.map(c => ({...c, checked: true})))
|
||||||
}}>{t('event:you.google_cal.select_all')}</LinkButton>
|
}}>{t('event:you.google_cal.select_all')}</LinkButton>
|
||||||
)}
|
)}
|
||||||
{calendars !== undefined && calendars.every(c => c.checked) && (
|
{calendars !== undefined && calendars.every(c => c.checked) && (
|
||||||
<LinkButton type="button" onClick={e => {
|
<LinkButton type="button" onClick={e => {
|
||||||
e.preventDefault();
|
e.preventDefault()
|
||||||
setCalendars(calendars.map(c => ({...c, checked: false})));
|
setCalendars(calendars.map(c => ({...c, checked: false})))
|
||||||
}}>{t('event:you.google_cal.select_none')}</LinkButton>
|
}}>{t('event:you.google_cal.select_none')}</LinkButton>
|
||||||
)}
|
)}
|
||||||
</Options>
|
</Options>
|
||||||
|
|
@ -206,14 +202,12 @@ const OutlookCalendar = ({ timeZone, timeMin, timeMax, onImport }) => {
|
||||||
id={calendar.id}
|
id={calendar.id}
|
||||||
color={calendar.color}
|
color={calendar.color}
|
||||||
checked={calendar.checked}
|
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} />
|
<CheckboxLabel htmlFor={calendar.id} color={calendar.color} />
|
||||||
<CalendarLabel htmlFor={calendar.id}>{calendar.name}</CalendarLabel>
|
<CalendarLabel htmlFor={calendar.id}>{calendar.name}</CalendarLabel>
|
||||||
</div>
|
</div>
|
||||||
)) : (
|
)) : <Loader />}
|
||||||
<Loader />
|
|
||||||
)}
|
|
||||||
{calendars !== undefined && (
|
{calendars !== undefined && (
|
||||||
<>
|
<>
|
||||||
<Info>{t('event:you.google_cal.info')}</Info>
|
<Info>{t('event:you.google_cal.info')}</Info>
|
||||||
|
|
@ -228,7 +222,7 @@ const OutlookCalendar = ({ timeZone, timeMin, timeMax, onImport }) => {
|
||||||
</CalendarList>
|
</CalendarList>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|
||||||
export default OutlookCalendar;
|
export default OutlookCalendar
|
||||||
|
|
@ -1,17 +1,18 @@
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next'
|
||||||
import { useRecentsStore, useLocaleUpdateStore } from 'stores';
|
import dayjs from 'dayjs'
|
||||||
import dayjs from 'dayjs';
|
import relativeTime from 'dayjs/plugin/relativeTime'
|
||||||
import relativeTime from 'dayjs/plugin/relativeTime';
|
|
||||||
|
|
||||||
import { AboutSection, StyledMain } from '../../pages/Home/homeStyle';
|
import { useRecentsStore, useLocaleUpdateStore } from '/src/stores'
|
||||||
import { Wrapper, Recent } from './recentsStyle';
|
|
||||||
|
|
||||||
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 = ({ target }) => {
|
||||||
const recents = useRecentsStore(state => state.recents);
|
const recents = useRecentsStore(state => state.recents)
|
||||||
const locale = useLocaleUpdateStore(state => state.locale);
|
const locale = useLocaleUpdateStore(state => state.locale)
|
||||||
const { t } = useTranslation(['home', 'common']);
|
const { t } = useTranslation(['home', 'common'])
|
||||||
|
|
||||||
return !!recents.length && (
|
return !!recents.length && (
|
||||||
<Wrapper>
|
<Wrapper>
|
||||||
|
|
@ -27,7 +28,7 @@ const Recents = ({ target }) => {
|
||||||
</StyledMain>
|
</StyledMain>
|
||||||
</AboutSection>
|
</AboutSection>
|
||||||
</Wrapper>
|
</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 {
|
@media print {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
`;
|
`
|
||||||
|
|
||||||
export const Recent = styled.a`
|
export const Recent = styled('a')`
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
@ -17,7 +17,7 @@ export const Recent = styled.a`
|
||||||
& .name {
|
& .name {
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
font-size: 1.1em;
|
font-size: 1.1em;
|
||||||
color: ${props => props.theme.mode === 'light' ? props.theme.primaryDark : props.theme.primaryLight};
|
color: var(--secondary);
|
||||||
flex: 1;
|
flex: 1;
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
@ -25,7 +25,7 @@ export const Recent = styled.a`
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
opacity: .8;
|
opacity: .8;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
color: ${props => props.theme.text};
|
color: var(--text);
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover .name {
|
&:hover .name {
|
||||||
|
|
@ -39,4 +39,4 @@ export const Recent = styled.a`
|
||||||
white-space: normal;
|
white-space: normal;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`;
|
`
|
||||||
|
|
@ -1,10 +1,11 @@
|
||||||
import { forwardRef } from 'react';
|
import { forwardRef } from 'react'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Wrapper,
|
Wrapper,
|
||||||
StyledLabel,
|
StyledLabel,
|
||||||
StyledSubLabel,
|
StyledSubLabel,
|
||||||
StyledSelect,
|
StyledSelect,
|
||||||
} from './selectFieldStyle';
|
} from './SelectField.styles'
|
||||||
|
|
||||||
const SelectField = forwardRef(({
|
const SelectField = forwardRef(({
|
||||||
label,
|
label,
|
||||||
|
|
@ -16,13 +17,13 @@ const SelectField = forwardRef(({
|
||||||
defaultOption,
|
defaultOption,
|
||||||
...props
|
...props
|
||||||
}, ref) => (
|
}, ref) => (
|
||||||
<Wrapper inline={inline} small={small}>
|
<Wrapper $inline={inline} $small={small}>
|
||||||
{label && <StyledLabel htmlFor={id} inline={inline} small={small}>{label}</StyledLabel>}
|
{label && <StyledLabel htmlFor={id} $inline={inline} $small={small}>{label}</StyledLabel>}
|
||||||
{subLabel && <StyledSubLabel htmlFor={id}>{subLabel}</StyledSubLabel>}
|
{subLabel && <StyledSubLabel htmlFor={id}>{subLabel}</StyledSubLabel>}
|
||||||
|
|
||||||
<StyledSelect
|
<StyledSelect
|
||||||
id={id}
|
id={id}
|
||||||
small={small}
|
$small={small}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
|
|
@ -38,6 +39,6 @@ const SelectField = forwardRef(({
|
||||||
)}
|
)}
|
||||||
</StyledSelect>
|
</StyledSelect>
|
||||||
</Wrapper>
|
</Wrapper>
|
||||||
));
|
))
|
||||||
|
|
||||||
export default SelectField;
|
export default SelectField
|
||||||
|
|
@ -0,0 +1,63 @@
|
||||||
|
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;
|
||||||
|
/* FIXME:
|
||||||
|
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:red;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 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,91 @@
|
||||||
import { useState, useEffect, useRef } from 'react';
|
import { useState, useEffect, useRef } from 'react'
|
||||||
import { useTheme } from '@emotion/react';
|
import { useLocation } from 'react-router-dom'
|
||||||
import { useLocation } from 'react-router-dom';
|
import { useTranslation } from 'react-i18next'
|
||||||
import { useTranslation } from 'react-i18next';
|
import dayjs from 'dayjs'
|
||||||
import dayjs from 'dayjs';
|
|
||||||
|
|
||||||
import { ToggleField, SelectField } from 'components';
|
import { ToggleField, SelectField } from '/src/components'
|
||||||
|
|
||||||
import { useSettingsStore, useLocaleUpdateStore } from 'stores';
|
import { useSettingsStore, useLocaleUpdateStore } from '/src/stores'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
OpenButton,
|
OpenButton,
|
||||||
Modal,
|
Modal,
|
||||||
Heading,
|
Heading,
|
||||||
Cover,
|
Cover,
|
||||||
} from './settingsStyle';
|
} from './Settings.styles'
|
||||||
|
|
||||||
import locales from 'res/dayjs_locales';
|
import locales from '/src/i18n/locales'
|
||||||
|
|
||||||
// Language specific options
|
// Language specific options
|
||||||
const setDefaults = (lang, store) => {
|
const setDefaults = (lang, store) => {
|
||||||
if (locales.hasOwnProperty(lang)) {
|
if (locales[lang]) {
|
||||||
store.setWeekStart(locales[lang].weekStart);
|
store.setWeekStart(locales[lang].weekStart)
|
||||||
store.setTimeFormat(locales[lang].timeFormat);
|
store.setTimeFormat(locales[lang].timeFormat)
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
const Settings = () => {
|
const Settings = () => {
|
||||||
const { pathname } = useLocation();
|
const { pathname } = useLocation()
|
||||||
const theme = useTheme();
|
const store = useSettingsStore()
|
||||||
const store = useSettingsStore();
|
const [isOpen, _setIsOpen] = useState(false)
|
||||||
const [isOpen, _setIsOpen] = useState(false);
|
const { t, i18n } = useTranslation('common')
|
||||||
const { t, i18n } = useTranslation('common');
|
const setLocale = useLocaleUpdateStore(state => state.setLocale)
|
||||||
const setLocale = useLocaleUpdateStore(state => state.setLocale);
|
const firstControlRef = useRef()
|
||||||
const firstControlRef = useRef();
|
|
||||||
|
|
||||||
const onEsc = e => {
|
const onEsc = e => {
|
||||||
if (e.key === 'Escape') {
|
if (e.key === 'Escape') {
|
||||||
setIsOpen(false);
|
setIsOpen(false)
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
const setIsOpen = open => {
|
const setIsOpen = open => {
|
||||||
_setIsOpen(open);
|
_setIsOpen(open)
|
||||||
|
|
||||||
if (open) {
|
if (open) {
|
||||||
window.setTimeout(() => firstControlRef.current.focus(), 150);
|
window.setTimeout(() => firstControlRef.current?.focus(), 150)
|
||||||
document.addEventListener('keyup', onEsc, true);
|
document.addEventListener('keyup', onEsc, true)
|
||||||
} else {
|
} else {
|
||||||
document.removeEventListener('keyup', onEsc);
|
document.removeEventListener('keyup', onEsc)
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (Object.keys(locales).includes(i18n.language)) {
|
if (Object.keys(locales).includes(i18n.language)) {
|
||||||
locales[i18n.language].import().then(() => {
|
locales[i18n.language].import().then(() => {
|
||||||
dayjs.locale(i18n.language);
|
dayjs.locale(i18n.language)
|
||||||
setLocale(dayjs.locale());
|
setLocale(dayjs.locale())
|
||||||
document.documentElement.setAttribute('lang', i18n.language);
|
document.documentElement.setAttribute('lang', i18n.language)
|
||||||
});
|
})
|
||||||
} else {
|
} else {
|
||||||
setLocale('en');
|
setLocale('en')
|
||||||
document.documentElement.setAttribute('lang', 'en')
|
document.documentElement.setAttribute('lang', 'en')
|
||||||
}
|
}
|
||||||
}, [i18n.language, setLocale]);
|
}, [i18n.language, setLocale])
|
||||||
|
|
||||||
if (!i18n.options.storedLang) {
|
if (!i18n.options.storedLang) {
|
||||||
setDefaults(i18n.language, store);
|
setDefaults(i18n.language, store)
|
||||||
i18n.options.storedLang = i18n.language;
|
i18n.options.storedLang = i18n.language
|
||||||
}
|
}
|
||||||
|
|
||||||
i18n.on('languageChanged', lang => {
|
i18n.on('languageChanged', lang => {
|
||||||
setDefaults(lang, store);
|
setDefaults(lang, store)
|
||||||
});
|
})
|
||||||
|
|
||||||
// Reset scroll on navigation
|
// Reset scroll on navigation
|
||||||
useEffect(() => window.scrollTo(0, 0), [pathname]);
|
useEffect(() => window.scrollTo(0, 0), [pathname])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<OpenButton
|
<OpenButton
|
||||||
isOpen={isOpen}
|
$isOpen={isOpen}
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setIsOpen(!isOpen)} title={t('options.name')}
|
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>
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="var(--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>
|
</OpenButton>
|
||||||
|
|
||||||
<Cover isOpen={isOpen} onClick={() => setIsOpen(false)} />
|
<Cover $isOpen={isOpen} onClick={() => setIsOpen(false)} />
|
||||||
<Modal isOpen={isOpen}>
|
<Modal $isOpen={isOpen}>
|
||||||
<Heading>{t('options.name')}</Heading>
|
<Heading>{t('options.name')}</Heading>
|
||||||
|
|
||||||
<ToggleField
|
<ToggleField
|
||||||
|
|
@ -147,8 +145,8 @@ const Settings = () => {
|
||||||
id="language"
|
id="language"
|
||||||
options={{
|
options={{
|
||||||
...Object.keys(locales).reduce((ls, l) => {
|
...Object.keys(locales).reduce((ls, l) => {
|
||||||
ls[l] = locales[l].name;
|
ls[l] = locales[l].name
|
||||||
return ls;
|
return ls
|
||||||
}, {}),
|
}, {}),
|
||||||
...process.env.NODE_ENV !== 'production' && { 'cimode': 'DEV' },
|
...process.env.NODE_ENV !== 'production' && { 'cimode': 'DEV' },
|
||||||
}}
|
}}
|
||||||
|
|
@ -158,7 +156,7 @@ const Settings = () => {
|
||||||
/>
|
/>
|
||||||
</Modal>
|
</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;
|
border: 0;
|
||||||
background: none;
|
background: none;
|
||||||
height: 50px;
|
height: 50px;
|
||||||
|
|
@ -23,10 +23,10 @@ export const OpenButton = styled.button`
|
||||||
outline: 0;
|
outline: 0;
|
||||||
}
|
}
|
||||||
&:focus-visible {
|
&:focus-visible {
|
||||||
background-color: ${props => props.theme.text}22;
|
/* FIXME: background-color: props => props.theme.text22; */
|
||||||
}
|
}
|
||||||
|
|
||||||
${props => props.isOpen && `
|
${props => props.$isOpen && `
|
||||||
transform: rotate(-45deg);
|
transform: rotate(-45deg);
|
||||||
`}
|
`}
|
||||||
|
|
||||||
|
|
@ -36,9 +36,9 @@ export const OpenButton = styled.button`
|
||||||
@media print {
|
@media print {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
`;
|
`
|
||||||
|
|
||||||
export const Cover = styled.div`
|
export const Cover = styled('div')`
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
|
|
@ -47,19 +47,19 @@ export const Cover = styled.div`
|
||||||
z-index: 100;
|
z-index: 100;
|
||||||
display: none;
|
display: none;
|
||||||
|
|
||||||
${props => props.isOpen && `
|
${props => props.$isOpen && `
|
||||||
display: block;
|
display: block;
|
||||||
`}
|
`}
|
||||||
`;
|
`
|
||||||
|
|
||||||
export const Modal = styled.div`
|
export const Modal = styled('div')`
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 70px;
|
top: 70px;
|
||||||
right: 12px;
|
right: 12px;
|
||||||
background-color: ${props => props.theme.background};
|
background-color: var(--background);
|
||||||
${props => props.theme.mode === 'dark' && `
|
${/* FIXME: props => props.theme.mode === 'dark' && `
|
||||||
border: 1px solid ${props.theme.primaryBackground};
|
border: 1px solid props.theme.primaryBackground;
|
||||||
`}
|
` */''}
|
||||||
z-index: 150;
|
z-index: 150;
|
||||||
padding: 10px 18px;
|
padding: 10px 18px;
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
|
|
@ -74,7 +74,7 @@ export const Modal = styled.div`
|
||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
transition: opacity .15s, transform .15s, visibility .15s;
|
transition: opacity .15s, transform .15s, visibility .15s;
|
||||||
|
|
||||||
${props => props.isOpen && `
|
${props => props.$isOpen && `
|
||||||
pointer-events: all;
|
pointer-events: all;
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
transform: translateY(0);
|
transform: translateY(0);
|
||||||
|
|
@ -87,11 +87,11 @@ export const Modal = styled.div`
|
||||||
@media print {
|
@media print {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
`;
|
`
|
||||||
|
|
||||||
export const Heading = styled.span`
|
export const Heading = styled('span')`
|
||||||
font-size: 1.5rem;
|
font-size: 1.5rem;
|
||||||
display: block;
|
display: block;
|
||||||
margin: 6px 0;
|
margin: 6px 0;
|
||||||
line-height: 1em;
|
line-height: 1em;
|
||||||
`;
|
`
|
||||||
|
|
@ -1,10 +1,11 @@
|
||||||
import { forwardRef } from 'react';
|
import { forwardRef } from 'react'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Wrapper,
|
Wrapper,
|
||||||
StyledLabel,
|
StyledLabel,
|
||||||
StyledSubLabel,
|
StyledSubLabel,
|
||||||
StyledInput,
|
StyledInput,
|
||||||
} from './textFieldStyle';
|
} from './TextField.styles'
|
||||||
|
|
||||||
const TextField = forwardRef(({
|
const TextField = forwardRef(({
|
||||||
label,
|
label,
|
||||||
|
|
@ -18,6 +19,6 @@ const TextField = forwardRef(({
|
||||||
{subLabel && <StyledSubLabel htmlFor={id}>{subLabel}</StyledSubLabel>}
|
{subLabel && <StyledSubLabel htmlFor={id}>{subLabel}</StyledSubLabel>}
|
||||||
<StyledInput id={id} ref={ref} {...props} />
|
<StyledInput id={id} ref={ref} {...props} />
|
||||||
</Wrapper>
|
</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 { useState, useEffect, useRef, forwardRef } from 'react'
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs'
|
||||||
|
|
||||||
import { useSettingsStore, useLocaleUpdateStore } from 'stores';
|
import { useSettingsStore, useLocaleUpdateStore } from '/src/stores'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Wrapper,
|
Wrapper,
|
||||||
|
|
@ -10,9 +10,9 @@ import {
|
||||||
Range,
|
Range,
|
||||||
Handle,
|
Handle,
|
||||||
Selected,
|
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(({
|
const TimeRangeField = forwardRef(({
|
||||||
label,
|
label,
|
||||||
|
|
@ -21,39 +21,39 @@ const TimeRangeField = forwardRef(({
|
||||||
setValue,
|
setValue,
|
||||||
...props
|
...props
|
||||||
}, ref) => {
|
}, ref) => {
|
||||||
const timeFormat = useSettingsStore(state => state.timeFormat);
|
const timeFormat = useSettingsStore(state => state.timeFormat)
|
||||||
const locale = useLocaleUpdateStore(state => state.locale);
|
const locale = useLocaleUpdateStore(state => state.locale)
|
||||||
|
|
||||||
const [start, setStart] = useState(9);
|
const [start, setStart] = useState(9)
|
||||||
const [end, setEnd] = useState(17);
|
const [end, setEnd] = useState(17)
|
||||||
|
|
||||||
const isStartMoving = useRef(false);
|
const isStartMoving = useRef(false)
|
||||||
const isEndMoving = useRef(false);
|
const isEndMoving = useRef(false)
|
||||||
const rangeRef = useRef();
|
const rangeRef = useRef()
|
||||||
const rangeRect = useRef();
|
const rangeRect = useRef()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (rangeRef.current) {
|
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 => {
|
const handleMouseMove = e => {
|
||||||
if (isStartMoving.current || isEndMoving.current) {
|
if (isStartMoving.current || isEndMoving.current) {
|
||||||
let step = Math.round(((e.pageX - rangeRect.current.left) / rangeRect.current.width) * 24);
|
let step = Math.round(((e.pageX - rangeRect.current.left) / rangeRect.current.width) * 24)
|
||||||
if (step < 0) step = 0;
|
if (step < 0) step = 0
|
||||||
if (step > 24) step = 24;
|
if (step > 24) step = 24
|
||||||
step = Math.abs(step);
|
step = Math.abs(step)
|
||||||
|
|
||||||
if (isStartMoving.current) {
|
if (isStartMoving.current) {
|
||||||
setStart(step);
|
setStart(step)
|
||||||
} else if (isEndMoving.current) {
|
} else if (isEndMoving.current) {
|
||||||
setEnd(step);
|
setEnd(step)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Wrapper locale={locale}>
|
<Wrapper locale={locale}>
|
||||||
|
|
@ -75,32 +75,32 @@ const TimeRangeField = forwardRef(({
|
||||||
label={timeFormat === '24h' ? times[start] : dayjs().hour(times[start]).format('ha')}
|
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={() => {
|
onMouseDown={() => {
|
||||||
document.addEventListener('mousemove', handleMouseMove);
|
document.addEventListener('mousemove', handleMouseMove)
|
||||||
isStartMoving.current = true;
|
isStartMoving.current = true
|
||||||
|
|
||||||
document.addEventListener('mouseup', () => {
|
document.addEventListener('mouseup', () => {
|
||||||
isStartMoving.current = false;
|
isStartMoving.current = false
|
||||||
document.removeEventListener('mousemove', handleMouseMove);
|
document.removeEventListener('mousemove', handleMouseMove)
|
||||||
}, { once: true });
|
}, { once: true })
|
||||||
}}
|
}}
|
||||||
onTouchMove={(e) => {
|
onTouchMove={e => {
|
||||||
const touch = e.targetTouches[0];
|
const touch = e.targetTouches[0]
|
||||||
|
|
||||||
let step = Math.round(((touch.pageX - rangeRect.current.left) / rangeRect.current.width) * 24);
|
let step = Math.round(((touch.pageX - rangeRect.current.left) / rangeRect.current.width) * 24)
|
||||||
if (step < 0) step = 0;
|
if (step < 0) step = 0
|
||||||
if (step > 24) step = 24;
|
if (step > 24) step = 24
|
||||||
step = Math.abs(step);
|
step = Math.abs(step)
|
||||||
setStart(step);
|
setStart(step)
|
||||||
}}
|
}}
|
||||||
tabIndex="0"
|
tabIndex="0"
|
||||||
onKeyDown={e => {
|
onKeyDown={e => {
|
||||||
if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') {
|
if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') {
|
||||||
e.preventDefault();
|
e.preventDefault()
|
||||||
setStart(Math.max(start-1, 0));
|
setStart(Math.max(start-1, 0))
|
||||||
}
|
}
|
||||||
if (e.key === 'ArrowRight' || e.key === 'ArrowDown') {
|
if (e.key === 'ArrowRight' || e.key === 'ArrowDown') {
|
||||||
e.preventDefault();
|
e.preventDefault()
|
||||||
setStart(Math.min(start+1, 24));
|
setStart(Math.min(start+1, 24))
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
@ -109,38 +109,38 @@ const TimeRangeField = forwardRef(({
|
||||||
label={timeFormat === '24h' ? times[end] : dayjs().hour(times[end]).format('ha')}
|
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={() => {
|
onMouseDown={() => {
|
||||||
document.addEventListener('mousemove', handleMouseMove);
|
document.addEventListener('mousemove', handleMouseMove)
|
||||||
isEndMoving.current = true;
|
isEndMoving.current = true
|
||||||
|
|
||||||
document.addEventListener('mouseup', () => {
|
document.addEventListener('mouseup', () => {
|
||||||
isEndMoving.current = false;
|
isEndMoving.current = false
|
||||||
document.removeEventListener('mousemove', handleMouseMove);
|
document.removeEventListener('mousemove', handleMouseMove)
|
||||||
}, { once: true });
|
}, { once: true })
|
||||||
}}
|
}}
|
||||||
onTouchMove={(e) => {
|
onTouchMove={e => {
|
||||||
const touch = e.targetTouches[0];
|
const touch = e.targetTouches[0]
|
||||||
|
|
||||||
let step = Math.round(((touch.pageX - rangeRect.current.left) / rangeRect.current.width) * 24);
|
let step = Math.round(((touch.pageX - rangeRect.current.left) / rangeRect.current.width) * 24)
|
||||||
if (step < 0) step = 0;
|
if (step < 0) step = 0
|
||||||
if (step > 24) step = 24;
|
if (step > 24) step = 24
|
||||||
step = Math.abs(step);
|
step = Math.abs(step)
|
||||||
setEnd(step);
|
setEnd(step)
|
||||||
}}
|
}}
|
||||||
tabIndex="0"
|
tabIndex="0"
|
||||||
onKeyDown={e => {
|
onKeyDown={e => {
|
||||||
if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') {
|
if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') {
|
||||||
e.preventDefault();
|
e.preventDefault()
|
||||||
setEnd(Math.max(end-1, 0));
|
setEnd(Math.max(end-1, 0))
|
||||||
}
|
}
|
||||||
if (e.key === 'ArrowRight' || e.key === 'ArrowDown') {
|
if (e.key === 'ArrowRight' || e.key === 'ArrowDown') {
|
||||||
e.preventDefault();
|
e.preventDefault()
|
||||||
setEnd(Math.min(end+1, 24));
|
setEnd(Math.min(end+1, 24))
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Range>
|
</Range>
|
||||||
</Wrapper>
|
</Wrapper>
|
||||||
);
|
)
|
||||||
});
|
})
|
||||||
|
|
||||||
export default TimeRangeField;
|
export default TimeRangeField
|
||||||
|
|
@ -1,41 +1,41 @@
|
||||||
import styled from '@emotion/styled';
|
import { styled } from 'goober'
|
||||||
|
|
||||||
export const Wrapper = styled.div`
|
export const Wrapper = styled('div')`
|
||||||
margin: 30px 0;
|
margin: 30px 0;
|
||||||
`;
|
`
|
||||||
|
|
||||||
export const StyledLabel = styled.label`
|
export const StyledLabel = styled('label')`
|
||||||
display: block;
|
display: block;
|
||||||
padding-bottom: 4px;
|
padding-bottom: 4px;
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
`;
|
`
|
||||||
|
|
||||||
export const StyledSubLabel = styled.label`
|
export const StyledSubLabel = styled('label')`
|
||||||
display: block;
|
display: block;
|
||||||
padding-bottom: 6px;
|
padding-bottom: 6px;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
opacity: .6;
|
opacity: .6;
|
||||||
`;
|
`
|
||||||
|
|
||||||
export const Range = styled.div`
|
export const Range = styled('div')`
|
||||||
user-select: none;
|
user-select: none;
|
||||||
background-color: ${props => props.theme.primaryBackground};
|
background-color: var(--surface);
|
||||||
border: 1px solid ${props => props.theme.primary};
|
border: 1px solid var(--primary);
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
height: 50px;
|
height: 50px;
|
||||||
position: relative;
|
position: relative;
|
||||||
margin: 38px 6px 18px;
|
margin: 38px 6px 18px;
|
||||||
`;
|
`
|
||||||
|
|
||||||
export const Handle = styled.div`
|
export const Handle = styled('div')`
|
||||||
height: calc(100% + 20px);
|
height: calc(100% + 20px);
|
||||||
width: 20px;
|
width: 20px;
|
||||||
border: 1px solid ${props => props.theme.primary};
|
border: 1px solid var(--primary);
|
||||||
background-color: ${props => props.theme.primaryLight};
|
background-color: var(--tertiary);
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: -10px;
|
top: -10px;
|
||||||
left: calc(${props => props.value * 4.1666666666666666}% - 11px);
|
left: calc(${props => props.value * 4.166}% - 11px);
|
||||||
cursor: ew-resize;
|
cursor: ew-resize;
|
||||||
touch-action: none;
|
touch-action: none;
|
||||||
transition: left .1s;
|
transition: left .1s;
|
||||||
|
|
@ -54,7 +54,7 @@ export const Handle = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
color: ${props => props.theme.primaryDark};
|
color: var(--secondary);
|
||||||
}
|
}
|
||||||
|
|
||||||
&:before {
|
&:before {
|
||||||
|
|
@ -67,18 +67,18 @@ export const Handle = styled.div`
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
${props => props.extraPadding}
|
${props => props.extraPadding}
|
||||||
}
|
}
|
||||||
`;
|
`
|
||||||
|
|
||||||
export const Selected = styled.div`
|
export const Selected = styled('div')`
|
||||||
position: absolute;
|
position: absolute;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
left: ${props => props.start * 4.1666666666666666}%;
|
left: ${props => props.start * 4.166}%;
|
||||||
right: calc(100% - ${props => props.end * 4.1666666666666666}%);
|
right: calc(100% - ${props => props.end * 4.166}%);
|
||||||
top: 0;
|
top: 0;
|
||||||
background-color: ${props => props.theme.primary};
|
background-color: var(--primary);
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
transition: left .1s, right .1s;
|
transition: left .1s, right .1s;
|
||||||
@media (prefers-reduced-motion: reduce) {
|
@media (prefers-reduced-motion: reduce) {
|
||||||
transition: none;
|
transition: none;
|
||||||
}
|
}
|
||||||
`;
|
`
|
||||||
|
|
@ -5,18 +5,16 @@ import {
|
||||||
Option,
|
Option,
|
||||||
HiddenInput,
|
HiddenInput,
|
||||||
LabelButton,
|
LabelButton,
|
||||||
} from './toggleFieldStyle';
|
} from './ToggleField.styles'
|
||||||
|
|
||||||
const ToggleField = ({
|
const ToggleField = ({
|
||||||
label,
|
label,
|
||||||
id,
|
|
||||||
name,
|
name,
|
||||||
title = '',
|
title = '',
|
||||||
options = [],
|
options = [],
|
||||||
value,
|
value,
|
||||||
onChange,
|
onChange,
|
||||||
inputRef,
|
inputRef,
|
||||||
...props
|
|
||||||
}) => (
|
}) => (
|
||||||
<Wrapper>
|
<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 !== '' && <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>}
|
||||||
|
|
@ -38,6 +36,6 @@ const ToggleField = ({
|
||||||
)}
|
)}
|
||||||
</ToggleContainer>
|
</ToggleContainer>
|
||||||
</Wrapper>
|
</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;
|
margin: 10px 0;
|
||||||
`;
|
`
|
||||||
|
|
||||||
export const ToggleContainer = styled.div`
|
export const ToggleContainer = styled('div')`
|
||||||
display: flex;
|
display: flex;
|
||||||
border: 1px solid ${props => props.theme.primary};
|
border: 1px solid var(--primary);
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
--focus-color: ${props => props.theme.primary};
|
--focus-color: var(--primary);
|
||||||
transition: border .15s;
|
transition: border .15s;
|
||||||
|
|
||||||
&:focus-within {
|
&: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);
|
border: 1px solid var(--focus-color);
|
||||||
& label {
|
& label {
|
||||||
box-shadow: inset 0 -3px 0 0 var(--focus-color);
|
box-shadow: inset 0 -3px 0 0 var(--focus-color);
|
||||||
|
|
@ -26,9 +27,9 @@ export const ToggleContainer = styled.div`
|
||||||
& > div:last-of-type label {
|
& > div:last-of-type label {
|
||||||
border-end-end-radius: 2px;
|
border-end-end-radius: 2px;
|
||||||
}
|
}
|
||||||
`;
|
`
|
||||||
|
|
||||||
export const StyledLabel = styled.label`
|
export const StyledLabel = styled('label')`
|
||||||
display: block;
|
display: block;
|
||||||
padding-bottom: 4px;
|
padding-bottom: 4px;
|
||||||
font-size: .9rem;
|
font-size: .9rem;
|
||||||
|
|
@ -38,14 +39,14 @@ export const StyledLabel = styled.label`
|
||||||
width: 1em;
|
width: 1em;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
`;
|
`
|
||||||
|
|
||||||
export const Option = styled.div`
|
export const Option = styled('div')`
|
||||||
flex: 1;
|
flex: 1;
|
||||||
position: relative;
|
position: relative;
|
||||||
`;
|
`
|
||||||
|
|
||||||
export const HiddenInput = styled.input`
|
export const HiddenInput = styled('input', forwardRef)`
|
||||||
height: 0;
|
height: 0;
|
||||||
width: 0;
|
width: 0;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
|
@ -55,12 +56,12 @@ export const HiddenInput = styled.input`
|
||||||
appearance: none;
|
appearance: none;
|
||||||
|
|
||||||
&:checked + label {
|
&:checked + label {
|
||||||
color: ${props => props.theme.background};
|
color: var(--background);
|
||||||
background-color: var(--focus-color);
|
background-color: var(--focus-color);
|
||||||
}
|
}
|
||||||
`;
|
`
|
||||||
|
|
||||||
export const LabelButton = styled.label`
|
export const LabelButton = styled('label')`
|
||||||
padding: 6px;
|
padding: 6px;
|
||||||
display: flex;
|
display: flex;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|
@ -71,4 +72,4 @@ export const LabelButton = styled.label`
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
transition: box-shadow .15s, background-color .15s;
|
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 {
|
import {
|
||||||
Wrapper,
|
Wrapper,
|
||||||
ButtonWrapper,
|
ButtonWrapper,
|
||||||
} from './translateDialogStyle';
|
} from './TranslateDialog.styles'
|
||||||
|
|
||||||
const TranslateDialog = ({ onClose }) => {
|
const TranslateDialog = () => {
|
||||||
const navigatorLang = useTranslateStore(state => state.navigatorLang);
|
const navigatorLang = useTranslateStore(state => state.navigatorLang)
|
||||||
const setDialogDismissed = useTranslateStore(state => state.setDialogDismissed);
|
const setDialogDismissed = useTranslateStore(state => state.setDialogDismissed)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Wrapper>
|
<Wrapper>
|
||||||
|
|
@ -26,7 +26,7 @@ const TranslateDialog = ({ onClose }) => {
|
||||||
<Button secondary onClick={() => setDialogDismissed(true)}>Close</Button>
|
<Button secondary onClick={() => setDialogDismissed(true)}>Close</Button>
|
||||||
</ButtonWrapper>
|
</ButtonWrapper>
|
||||||
</Wrapper>
|
</Wrapper>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default TranslateDialog;
|
export default TranslateDialog
|
||||||
|
|
@ -1,13 +1,13 @@
|
||||||
import styled from '@emotion/styled';
|
import { styled } from 'goober'
|
||||||
|
|
||||||
export const Wrapper = styled.div`
|
export const Wrapper = styled('div')`
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 20px;
|
top: 20px;
|
||||||
left: 20px;
|
left: 20px;
|
||||||
background-color: ${props => props.theme.background};
|
background-color: var(--background);
|
||||||
${props => props.theme.mode === 'dark' && `
|
/* FIXME: ${props => props.theme.mode === 'dark' && `
|
||||||
border: 1px solid ${props.theme.primaryBackground};
|
border: 1px solid ${props.theme.primaryBackground};
|
||||||
`}
|
`} */
|
||||||
z-index: 900;
|
z-index: 900;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
|
|
@ -30,9 +30,9 @@ export const Wrapper = styled.div`
|
||||||
@media (max-width: 400px) {
|
@media (max-width: 400px) {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
`;
|
`
|
||||||
|
|
||||||
export const ButtonWrapper = styled.div`
|
export const ButtonWrapper = styled('div')`
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
|
|
@ -46,4 +46,4 @@ export const ButtonWrapper = styled.div`
|
||||||
margin: 20px 0 0;
|
margin: 20px 0 0;
|
||||||
white-space: normal;
|
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 {
|
import {
|
||||||
Wrapper,
|
Wrapper,
|
||||||
ButtonWrapper,
|
ButtonWrapper,
|
||||||
} from './updateDialogStyle';
|
} from './UpdateDialog.styles'
|
||||||
|
|
||||||
const UpdateDialog = ({ onClose }) => {
|
const UpdateDialog = ({ onClose }) => {
|
||||||
const { t } = useTranslation('common');
|
const { t } = useTranslation('common')
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Wrapper>
|
<Wrapper>
|
||||||
|
|
@ -18,7 +19,7 @@ const UpdateDialog = ({ onClose }) => {
|
||||||
<Button onClick={() => window.location.reload()}>{t('common:update.buttons.reload')}</Button>
|
<Button onClick={() => window.location.reload()}>{t('common:update.buttons.reload')}</Button>
|
||||||
</ButtonWrapper>
|
</ButtonWrapper>
|
||||||
</Wrapper>
|
</Wrapper>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default UpdateDialog;
|
export default UpdateDialog
|
||||||
|
|
@ -1,13 +1,13 @@
|
||||||
import styled from '@emotion/styled';
|
import { styled } from 'goober'
|
||||||
|
|
||||||
export const Wrapper = styled.div`
|
export const Wrapper = styled('div')`
|
||||||
position: fixed;
|
position: fixed;
|
||||||
bottom: 20px;
|
bottom: 20px;
|
||||||
right: 20px;
|
right: 20px;
|
||||||
background-color: ${props => props.theme.background};
|
background-color: var(--background);
|
||||||
${props => props.theme.mode === 'dark' && `
|
/* FIXME: ${props => props.theme.mode === 'dark' && `
|
||||||
border: 1px solid ${props.theme.primaryBackground};
|
border: 1px solid ${props.theme.primaryBackground};
|
||||||
`}
|
`} */
|
||||||
z-index: 900;
|
z-index: 900;
|
||||||
padding: 20px 26px;
|
padding: 20px 26px;
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
|
|
@ -24,12 +24,12 @@ export const Wrapper = styled.div`
|
||||||
margin: 16px 0 24px;
|
margin: 16px 0 24px;
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
}
|
}
|
||||||
`;
|
`
|
||||||
|
|
||||||
export const ButtonWrapper = styled.div`
|
export const ButtonWrapper = styled('div')`
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
gap: 16px;
|
gap: 16px;
|
||||||
flex-wrap: wrap;
|
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 i18n from 'i18next'
|
||||||
import { initReactI18next } from 'react-i18next';
|
import { initReactI18next } from 'react-i18next'
|
||||||
import LanguageDetector from 'i18next-browser-languagedetector';
|
import LanguageDetector from 'i18next-browser-languagedetector'
|
||||||
import Backend from 'i18next-http-backend';
|
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
|
i18n
|
||||||
.use(LanguageDetector)
|
.use(LanguageDetector)
|
||||||
|
|
@ -15,7 +15,6 @@ i18n
|
||||||
fallbackLng: 'en',
|
fallbackLng: 'en',
|
||||||
supportedLngs: Object.keys(locales),
|
supportedLngs: Object.keys(locales),
|
||||||
ns: 'common',
|
ns: 'common',
|
||||||
defaultNS: 'common',
|
|
||||||
debug: process.env.NODE_ENV !== 'production',
|
debug: process.env.NODE_ENV !== 'production',
|
||||||
interpolation: {
|
interpolation: {
|
||||||
escapeValue: false,
|
escapeValue: false,
|
||||||
|
|
@ -24,6 +23,6 @@ i18n
|
||||||
loadPath: '/i18n/{{lng}}/{{ns}}.json',
|
loadPath: '/i18n/{{lng}}/{{ns}}.json',
|
||||||
},
|
},
|
||||||
storedLang,
|
storedLang,
|
||||||
}).then(() => document.documentElement.setAttribute('lang', i18n.language));
|
}).then(() => document.documentElement.setAttribute('lang', i18n.language))
|
||||||
|
|
||||||
export default i18n;
|
export default i18n
|
||||||
|
|
@ -78,6 +78,6 @@ const locales = {
|
||||||
// weekStart: 1,
|
// weekStart: 1,
|
||||||
// timeFormat: '12h',
|
// 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')
|
|
||||||
);
|
|
||||||
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`
|
export const Step = styled('h2')`
|
||||||
text-decoration-color: ${props => props.theme.primary};
|
text-decoration-color: var(--primary);
|
||||||
text-decoration-style: solid;
|
text-decoration-style: solid;
|
||||||
text-decoration-line: underline;
|
text-decoration-line: underline;
|
||||||
margin-top: 30px;
|
margin-top: 30px;
|
||||||
`;
|
`
|
||||||
|
|
||||||
export const FakeCalendar = styled.div`
|
export const FakeCalendar = styled('div')`
|
||||||
user-select: none;
|
user-select: none;
|
||||||
|
|
||||||
& div {
|
& div {
|
||||||
|
|
@ -28,8 +28,8 @@ export const FakeCalendar = styled.div`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
& .dates span {
|
& .dates span {
|
||||||
background-color: ${props => props.theme.primaryBackground};
|
background-color: var(--surface);
|
||||||
border: 1px solid ${props => props.theme.primary};
|
border: 1px solid var(--primary);
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|
@ -37,7 +37,7 @@ export const FakeCalendar = styled.div`
|
||||||
|
|
||||||
&.selected {
|
&.selected {
|
||||||
color: #FFF;
|
color: #FFF;
|
||||||
background-color: ${props => props.theme.primary};
|
background-color: var(--primary);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
& .dates span:first-of-type {
|
& .dates span:first-of-type {
|
||||||
|
|
@ -48,12 +48,12 @@ export const FakeCalendar = styled.div`
|
||||||
border-end-end-radius: 3px;
|
border-end-end-radius: 3px;
|
||||||
border-start-end-radius: 3px;
|
border-start-end-radius: 3px;
|
||||||
}
|
}
|
||||||
`;
|
`
|
||||||
|
|
||||||
export const FakeTimeRange = styled.div`
|
export const FakeTimeRange = styled('div')`
|
||||||
user-select: none;
|
user-select: none;
|
||||||
background-color: ${props => props.theme.primaryBackground};
|
background-color: var(--surface);
|
||||||
border: 1px solid ${props => props.theme.primary};
|
border: 1px solid var(--primary);
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
height: 50px;
|
height: 50px;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
@ -62,8 +62,8 @@ export const FakeTimeRange = styled.div`
|
||||||
& div {
|
& div {
|
||||||
height: calc(100% + 20px);
|
height: calc(100% + 20px);
|
||||||
width: 20px;
|
width: 20px;
|
||||||
border: 1px solid ${props => props.theme.primary};
|
border: 1px solid var(--primary);
|
||||||
background-color: ${props => props.theme.primaryLight};
|
background-color: var(--secondary);
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: -10px;
|
top: -10px;
|
||||||
|
|
@ -79,7 +79,7 @@ export const FakeTimeRange = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
color: ${props => props.theme.primaryDark};
|
color: var(--tertiary);
|
||||||
}
|
}
|
||||||
|
|
||||||
&:before {
|
&:before {
|
||||||
|
|
@ -92,25 +92,25 @@ export const FakeTimeRange = styled.div`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
& .start {
|
& .start {
|
||||||
left: calc(${11 * 4.1666666666666666}% - 11px);
|
left: calc(${11 * 4.166}% - 11px);
|
||||||
}
|
}
|
||||||
& .end {
|
& .end {
|
||||||
left: calc(${17 * 4.1666666666666666}% - 11px);
|
left: calc(${17 * 4.166}% - 11px);
|
||||||
}
|
}
|
||||||
&:before {
|
&:before {
|
||||||
content: '';
|
content: '';
|
||||||
position: absolute;
|
position: absolute;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
left: ${11 * 4.1666666666666666}%;
|
left: ${11 * 4.166}%;
|
||||||
right: calc(100% - ${17 * 4.1666666666666666}%);
|
right: calc(100% - ${17 * 4.166}%);
|
||||||
top: 0;
|
top: 0;
|
||||||
background-color: ${props => props.theme.primary};
|
background-color: var(--primary);
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
}
|
}
|
||||||
`;
|
`
|
||||||
|
|
||||||
export const ButtonArea = styled.div`
|
export const ButtonArea = styled('div')`
|
||||||
@media print {
|
@media print {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
`;
|
`
|
||||||
File diff suppressed because one or more lines are too long
|
|
@ -1,23 +1,23 @@
|
||||||
import styled from '@emotion/styled';
|
import { styled } from 'goober'
|
||||||
|
|
||||||
export const StyledMain = styled.div`
|
export const StyledMain = styled('div')`
|
||||||
width: 600px;
|
width: 600px;
|
||||||
margin: 20px auto;
|
margin: 20px auto;
|
||||||
max-width: calc(100% - 60px);
|
max-width: calc(100% - 60px);
|
||||||
`;
|
`
|
||||||
|
|
||||||
export const CreateForm = styled.form`
|
export const CreateForm = styled('form')`
|
||||||
margin: 0 0 60px;
|
margin: 0 0 60px;
|
||||||
`;
|
`
|
||||||
|
|
||||||
export const TitleSmall = styled.span`
|
export const TitleSmall = styled('span')`
|
||||||
display: block;
|
display: block;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 3rem;
|
font-size: 3rem;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
font-family: 'Samurai Bob', sans-serif;
|
font-family: 'Samurai Bob', sans-serif;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
color: ${props => props.theme.mode === 'light' ? props.theme.primaryDark : props.theme.primaryLight};
|
color: var(--secondary);
|
||||||
line-height: 1em;
|
line-height: 1em;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
|
|
||||||
|
|
@ -28,25 +28,25 @@ export const TitleSmall = styled.span`
|
||||||
line-height: 1.2em;
|
line-height: 1.2em;
|
||||||
padding-top: .3em;
|
padding-top: .3em;
|
||||||
`}
|
`}
|
||||||
`;
|
`
|
||||||
|
|
||||||
export const TitleLarge = styled.h1`
|
export const TitleLarge = styled('h1')`
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 4rem;
|
font-size: 4rem;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
color: ${props => props.theme.primary};
|
color: var(--primary);
|
||||||
font-family: 'Molot', sans-serif;
|
font-family: 'Molot', sans-serif;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
text-shadow: 0 4px 0 ${props => props.theme.primaryDark};
|
text-shadow: 0 4px 0 var(--secondary);
|
||||||
line-height: 1em;
|
line-height: 1em;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
|
|
||||||
@media (max-width: 350px) {
|
@media (max-width: 350px) {
|
||||||
font-size: 3.5rem;
|
font-size: 3.5rem;
|
||||||
}
|
}
|
||||||
`;
|
`
|
||||||
|
|
||||||
export const Logo = styled.img`
|
export const Logo = styled('img')`
|
||||||
width: 80px;
|
width: 80px;
|
||||||
transition: transform .15s;
|
transition: transform .15s;
|
||||||
animation: jelly .5s 1 .05s;
|
animation: jelly .5s 1 .05s;
|
||||||
|
|
@ -79,68 +79,68 @@ export const Logo = styled.img`
|
||||||
transform: none;
|
transform: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`;
|
`
|
||||||
|
|
||||||
export const Links = styled.nav`
|
export const Links = styled('nav')`
|
||||||
text-align: center;
|
text-align: center;
|
||||||
margin: 20px 0;
|
margin: 20px 0;
|
||||||
`;
|
`
|
||||||
|
|
||||||
export const AboutSection = styled.section`
|
export const AboutSection = styled('section')`
|
||||||
margin: 30px 0 0;
|
margin: 30px 0 0;
|
||||||
background-color: ${props => props.theme.primaryBackground};
|
background-color: var(--surface);
|
||||||
padding: 20px 0;
|
padding: 20px 0;
|
||||||
|
|
||||||
& a {
|
& 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;
|
font-weight: 500;
|
||||||
line-height: 1.6em;
|
line-height: 1.6em;
|
||||||
`;
|
`
|
||||||
|
|
||||||
export const Stats = styled.div`
|
export const Stats = styled('div')`
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-around;
|
justify-content: space-around;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
`;
|
`
|
||||||
|
|
||||||
export const Stat = styled.div`
|
export const Stat = styled('div')`
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: 0 6px;
|
padding: 0 6px;
|
||||||
min-width: 160px;
|
min-width: 160px;
|
||||||
margin: 10px 0;
|
margin: 10px 0;
|
||||||
`;
|
`
|
||||||
|
|
||||||
export const StatNumber = styled.span`
|
export const StatNumber = styled('span')`
|
||||||
display: block;
|
display: block;
|
||||||
font-weight: 900;
|
font-weight: 900;
|
||||||
color: ${props => props.theme.mode === 'light' ? props.theme.primaryDark : props.theme.primaryLight};
|
color: var(--secondary);
|
||||||
font-size: 2em;
|
font-size: 2em;
|
||||||
`;
|
`
|
||||||
|
|
||||||
export const StatLabel = styled.span`
|
export const StatLabel = styled('span')`
|
||||||
display: block;
|
display: block;
|
||||||
`;
|
`
|
||||||
|
|
||||||
export const OfflineMessage = styled.div`
|
export const OfflineMessage = styled('div')`
|
||||||
text-align: center;
|
text-align: center;
|
||||||
margin: 50px 0 20px;
|
margin: 50px 0 20px;
|
||||||
`;
|
`
|
||||||
|
|
||||||
export const ButtonArea = styled.div`
|
export const ButtonArea = styled('div')`
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
margin: 30px 0;
|
margin: 30px 0;
|
||||||
`;
|
`
|
||||||
|
|
||||||
export const VideoWrapper = styled.div`
|
export const VideoWrapper = styled('div')`
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
position: relative;
|
position: relative;
|
||||||
padding-bottom: 56.4%;
|
padding-bottom: 56.4%;
|
||||||
|
|
@ -152,9 +152,9 @@ export const VideoWrapper = styled.div`
|
||||||
height: 100%;
|
height: 100%;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
}
|
}
|
||||||
`;
|
`
|
||||||
|
|
||||||
export const VideoLink = styled.a`
|
export const VideoLink = styled('a')`
|
||||||
display: block;
|
display: block;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
@ -203,4 +203,4 @@ export const VideoLink = styled.a`
|
||||||
box-shadow: 0 0 20px 0 rgba(0,0,0,.3);
|
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,27 +1,19 @@
|
||||||
import axios from 'axios';
|
const API_URL = process.env.NODE_ENV === 'production' ? 'https://api-dot-crabfit.uc.r.appspot.com' : 'http://localhost:8080'
|
||||||
|
|
||||||
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 handleError = error => {
|
const handleError = error => {
|
||||||
if (error.response && error.response.status) {
|
if (error.response && error.response.status) {
|
||||||
console.log('[Error handler] res:', error.response);
|
console.error('[Error handler] res:', error.response)
|
||||||
}
|
}
|
||||||
return Promise.reject(error.response);
|
return Promise.reject(error.response)
|
||||||
};
|
}
|
||||||
|
|
||||||
const api = {
|
const api = {
|
||||||
get: async (endpoint, data) => {
|
get: async (endpoint, data) => {
|
||||||
try {
|
try {
|
||||||
const response = await instance.get(endpoint, data);
|
const response = await fetch(API_URL + endpoint)
|
||||||
return Promise.resolve(response);
|
return Promise.resolve(response)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return handleError(error);
|
return handleError(error)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
post: async (endpoint, data, options = {}) => {
|
post: async (endpoint, data, options = {}) => {
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
8
crabfit-frontend/src/stores/twaStore.js
Normal file
8
crabfit-frontend/src/stores/twaStore.js
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
import create from 'zustand'
|
||||||
|
|
||||||
|
const useTWAStore = create(set => ({
|
||||||
|
TWA: undefined,
|
||||||
|
setTWA: TWA => set({ TWA }),
|
||||||
|
}))
|
||||||
|
|
||||||
|
export default useTWAStore
|
||||||
32
crabfit-frontend/src/utils/index.js
Normal file
32
crabfit-frontend/src/utils/index.js
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
export const detect_browser = () => {
|
||||||
|
// Opera 8.0+
|
||||||
|
// eslint-disable-next-line no-undef
|
||||||
|
const isOpera = (!!window.opr && !!opr.addons) || !!window.opera || navigator.userAgent.indexOf(' OPR/') >= 0
|
||||||
|
|
||||||
|
// Firefox 1.0+
|
||||||
|
const isFirefox = typeof InstallTrigger !== 'undefined'
|
||||||
|
|
||||||
|
// Safari 3.0+ "[object HTMLElementConstructor]"
|
||||||
|
const isSafari = /constructor/i.test(window.HTMLElement) || (function (p) { return p.toString() === '[object SafariRemoteNotification]' })(!window['safari'] || (typeof safari !== 'undefined' && window['safari'].pushNotification))
|
||||||
|
|
||||||
|
// Internet Explorer 6-11
|
||||||
|
const isIE = /*@cc_on!@*/false || !!document.documentMode
|
||||||
|
|
||||||
|
// Edge 20+
|
||||||
|
const isEdge = !isIE && !!window.StyleMedia
|
||||||
|
|
||||||
|
// Chrome 1 - 79
|
||||||
|
const isChrome = !!window.chrome
|
||||||
|
|
||||||
|
// Edge (based on chromium) detection
|
||||||
|
// eslint-disable-next-line
|
||||||
|
const isEdgeChromium = isChrome && (navigator.userAgent.indexOf("Edg") != -1)
|
||||||
|
|
||||||
|
if (isEdgeChromium) return 'edge_chromium'
|
||||||
|
if (isChrome) return 'chrome'
|
||||||
|
if (isEdge) return 'edge'
|
||||||
|
if (isIE) return 'ie'
|
||||||
|
if (isSafari) return 'safari'
|
||||||
|
if (isFirefox) return 'firefox'
|
||||||
|
if (isOpera) return 'opera'
|
||||||
|
}
|
||||||
|
|
@ -1,31 +0,0 @@
|
||||||
export const detect_browser = () => {
|
|
||||||
// Opera 8.0+
|
|
||||||
const isOpera = (!!window.opr && !!opr.addons) || !!window.opera || navigator.userAgent.indexOf(' OPR/') >= 0;
|
|
||||||
|
|
||||||
// Firefox 1.0+
|
|
||||||
const isFirefox = typeof InstallTrigger !== 'undefined';
|
|
||||||
|
|
||||||
// Safari 3.0+ "[object HTMLElementConstructor]"
|
|
||||||
const isSafari = /constructor/i.test(window.HTMLElement) || (function (p) { return p.toString() === "[object SafariRemoteNotification]"; })(!window['safari'] || (typeof safari !== 'undefined' && window['safari'].pushNotification));
|
|
||||||
|
|
||||||
// Internet Explorer 6-11
|
|
||||||
const isIE = /*@cc_on!@*/false || !!document.documentMode;
|
|
||||||
|
|
||||||
// Edge 20+
|
|
||||||
const isEdge = !isIE && !!window.StyleMedia;
|
|
||||||
|
|
||||||
// Chrome 1 - 79
|
|
||||||
const isChrome = !!window.chrome;
|
|
||||||
|
|
||||||
// Edge (based on chromium) detection
|
|
||||||
// eslint-disable-next-line
|
|
||||||
const isEdgeChromium = isChrome && (navigator.userAgent.indexOf("Edg") != -1);
|
|
||||||
|
|
||||||
if (isEdgeChromium) return 'edge_chromium';
|
|
||||||
if (isChrome) return 'chrome';
|
|
||||||
if (isEdge) return 'edge';
|
|
||||||
if (isIE) return 'ie';
|
|
||||||
if (isSafari) return 'safari';
|
|
||||||
if (isFirefox) return 'firefox';
|
|
||||||
if (isOpera) return 'opera';
|
|
||||||
};
|
|
||||||
|
|
@ -1,27 +0,0 @@
|
||||||
{
|
|
||||||
"compilerOptions": {
|
|
||||||
"target": "es5",
|
|
||||||
"lib": [
|
|
||||||
"dom",
|
|
||||||
"dom.iterable",
|
|
||||||
"esnext"
|
|
||||||
],
|
|
||||||
"allowJs": true,
|
|
||||||
"skipLibCheck": true,
|
|
||||||
"esModuleInterop": true,
|
|
||||||
"allowSyntheticDefaultImports": true,
|
|
||||||
"strict": true,
|
|
||||||
"forceConsistentCasingInFileNames": true,
|
|
||||||
"noFallthroughCasesInSwitch": true,
|
|
||||||
"module": "esnext",
|
|
||||||
"moduleResolution": "node",
|
|
||||||
"resolveJsonModule": true,
|
|
||||||
"isolatedModules": true,
|
|
||||||
"noEmit": true,
|
|
||||||
"jsx": "react-jsx",
|
|
||||||
"baseUrl": "src"
|
|
||||||
},
|
|
||||||
"include": [
|
|
||||||
"src"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
6
crabfit-frontend/vite.config.js
Normal file
6
crabfit-frontend/vite.config.js
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
import { defineConfig } from 'vite'
|
||||||
|
import react from '@vitejs/plugin-react'
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
})
|
||||||
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue