From 2adecd13f727c86739c3f0b6f9b338639dabb040 Mon Sep 17 00:00:00 2001 From: Ben Grant Date: Fri, 19 May 2023 23:59:44 +1000 Subject: [PATCH 01/39] Start setting up Next js with the new app router --- frontend/.eslintrc.js | 73 - frontend/.eslintrc.json | 27 + frontend/.gcloudignore | 10 - frontend/.gitignore | 4 +- frontend/.vscode/settings.json | 3 + frontend/app.yaml | 15 - frontend/index.html | 51 - frontend/jsconfig.json | 13 - frontend/next-env.d.ts | 5 + frontend/package.json | 65 +- .../{public/index.css => src/app/global.css} | 30 +- frontend/src/app/home.module.scss | 4 + frontend/src/app/layout.tsx | 36 + frontend/src/app/page.tsx | 24 + frontend/src/components/Button/Button.jsx | 33 - .../src/components/Button/Button.module.scss | 130 + .../src/components/Button/Button.styles.js | 134 - frontend/src/components/Button/Button.tsx | 59 + frontend/src/components/Donate/Donate.jsx | 159 - .../src/components/Donate/Donate.styles.js | 64 - frontend/src/components/Error/Error.jsx | 17 - .../{Error.styles.js => Error.module.scss} | 28 +- frontend/src/components/Error/Error.tsx | 25 + frontend/src/components/Footer/Footer.jsx | 17 - .../src/components/Footer/Footer.module.scss | 24 + .../src/components/Footer/Footer.styles.js | 26 - frontend/src/components/Footer/Footer.tsx | 30 + .../src/components/Header/Header.module.scss | 124 + frontend/src/components/Header/Header.tsx | 32 + frontend/src/components/Logo/Logo.jsx | 31 - frontend/src/components/Logo/Logo.styles.js | 69 - frontend/src/components/Recents/Recents.jsx | 34 - .../components/Recents/Recents.module.scss | 35 + .../src/components/Recents/Recents.styles.js | 42 - frontend/src/components/Recents/Recents.tsx | 35 + ...{Settings.styles.js => Settings.styles.ts} | 0 .../Settings/{Settings.jsx => Settings.tsx} | 4 +- ...og.styles.js => TranslateDialog.styles.ts} | 0 ...ranslateDialog.jsx => TranslateDialog.tsx} | 0 frontend/src/components/index.js | 24 - frontend/src/components/index.ts | 23 + frontend/src/config/dayjs.ts | 6 + frontend/src/i18n/client.ts | 25 + frontend/src/i18n/index.js | 28 - .../i18n => src/i18n/locales}/de/common.json | 0 .../i18n => src/i18n/locales}/de/event.json | 0 .../i18n => src/i18n/locales}/de/help.json | 0 .../i18n => src/i18n/locales}/de/home.json | 0 .../i18n => src/i18n/locales}/de/privacy.json | 0 .../i18n/locales}/en-GB/common.json | 0 .../i18n/locales}/en-GB/event.json | 0 .../i18n => src/i18n/locales}/en-GB/help.json | 0 .../i18n => src/i18n/locales}/en-GB/home.json | 0 .../i18n/locales}/en-GB/privacy.json | 0 .../i18n => src/i18n/locales}/en/common.json | 13 +- .../i18n => src/i18n/locales}/en/event.json | 0 .../i18n => src/i18n/locales}/en/help.json | 0 .../i18n => src/i18n/locales}/en/home.json | 0 .../i18n => src/i18n/locales}/en/privacy.json | 0 .../i18n => src/i18n/locales}/es/common.json | 0 .../i18n => src/i18n/locales}/es/event.json | 0 .../i18n => src/i18n/locales}/es/help.json | 0 .../i18n => src/i18n/locales}/es/home.json | 0 .../i18n => src/i18n/locales}/es/privacy.json | 0 .../i18n => src/i18n/locales}/fr/common.json | 0 .../i18n => src/i18n/locales}/fr/event.json | 0 .../i18n => src/i18n/locales}/fr/help.json | 0 .../i18n => src/i18n/locales}/fr/home.json | 0 .../i18n => src/i18n/locales}/fr/privacy.json | 0 .../i18n => src/i18n/locales}/hi/common.json | 0 .../i18n => src/i18n/locales}/hi/event.json | 0 .../i18n => src/i18n/locales}/hi/help.json | 0 .../i18n => src/i18n/locales}/hi/home.json | 0 .../i18n => src/i18n/locales}/hi/privacy.json | 0 .../i18n/locales}/hu-HU/privacy.json | 0 .../i18n => src/i18n/locales}/id/common.json | 0 .../i18n => src/i18n/locales}/id/event.json | 0 .../i18n => src/i18n/locales}/id/help.json | 0 .../i18n => src/i18n/locales}/id/home.json | 0 .../i18n => src/i18n/locales}/id/privacy.json | 0 .../i18n => src/i18n/locales}/it/common.json | 0 .../i18n => src/i18n/locales}/it/event.json | 0 .../i18n => src/i18n/locales}/it/help.json | 0 .../i18n => src/i18n/locales}/it/home.json | 0 .../i18n => src/i18n/locales}/it/privacy.json | 0 .../i18n => src/i18n/locales}/ja/common.json | 0 .../i18n => src/i18n/locales}/ja/event.json | 0 .../i18n => src/i18n/locales}/ja/home.json | 0 .../i18n => src/i18n/locales}/ja/privacy.json | 0 .../i18n => src/i18n/locales}/ko/common.json | 0 .../i18n => src/i18n/locales}/ko/event.json | 0 .../i18n => src/i18n/locales}/ko/help.json | 0 .../i18n => src/i18n/locales}/ko/home.json | 0 .../i18n => src/i18n/locales}/ko/privacy.json | 0 .../i18n => src/i18n/locales}/pl/common.json | 0 .../i18n => src/i18n/locales}/pl/home.json | 0 .../i18n => src/i18n/locales}/pl/privacy.json | 0 .../i18n/locales}/pt-BR/common.json | 0 .../i18n/locales}/pt-BR/event.json | 0 .../i18n => src/i18n/locales}/pt-BR/help.json | 0 .../i18n => src/i18n/locales}/pt-BR/home.json | 0 .../i18n/locales}/pt-BR/privacy.json | 0 .../i18n/locales}/pt_PT/common.json | 0 .../i18n/locales}/pt_PT/event.json | 0 .../i18n => src/i18n/locales}/pt_PT/help.json | 0 .../i18n => src/i18n/locales}/pt_PT/home.json | 0 .../i18n/locales}/pt_PT/privacy.json | 0 .../i18n => src/i18n/locales}/ru/common.json | 0 .../i18n => src/i18n/locales}/ru/event.json | 0 .../i18n => src/i18n/locales}/ru/help.json | 0 .../i18n => src/i18n/locales}/ru/home.json | 0 .../i18n => src/i18n/locales}/ru/privacy.json | 0 frontend/src/i18n/{locales.js => options.ts} | 49 +- frontend/src/i18n/server.ts | 36 + frontend/src/index.jsx | 24 - .../{pages => pages-old}/Create/Create.jsx | 0 .../Create/Create.styles.js | 0 .../src/{pages => pages-old}/Event/Event.jsx | 0 .../Event/Event.styles.js | 0 .../src/{pages => pages-old}/Help/Help.jsx | 0 .../{pages => pages-old}/Help/Help.styles.js | 0 .../src/{pages => pages-old}/Home/Home.jsx | 63 +- .../{pages => pages-old}/Home/Home.styles.js | 0 .../{pages => pages-old}/Privacy/Privacy.jsx | 0 .../Privacy/Privacy.styles.js | 0 frontend/src/{pages => pages-old}/index.js | 0 frontend/src/res/paypal.svg | 39 - frontend/src/services/index.js | 63 - frontend/src/stores/index.js | 5 - frontend/src/stores/index.ts | 22 + frontend/src/stores/localeUpdateStore.js | 8 - frontend/src/stores/localeUpdateStore.ts | 13 + frontend/src/stores/recentsStore.js | 23 - frontend/src/stores/recentsStore.ts | 53 + frontend/src/stores/settingsStore.js | 21 - frontend/src/stores/settingsStore.ts | 38 + frontend/src/stores/translateStore.js | 23 - frontend/src/stores/translateStore.ts | 28 + frontend/src/stores/twaStore.js | 8 - frontend/src/stores/twaStore.ts | 14 + .../src/utils/{index.js => detectBrowser.ts} | 10 +- frontend/src/utils/index.ts | 3 + frontend/src/utils/makeClass.ts | 2 + frontend/src/utils/unhyphenate.ts | 4 + frontend/tsconfig.json | 43 + frontend/vite.config.js | 14 - frontend/yarn.lock | 4067 +++++++---------- 147 files changed, 2637 insertions(+), 3667 deletions(-) delete mode 100644 frontend/.eslintrc.js create mode 100644 frontend/.eslintrc.json delete mode 100644 frontend/.gcloudignore create mode 100644 frontend/.vscode/settings.json delete mode 100644 frontend/app.yaml delete mode 100644 frontend/index.html delete mode 100644 frontend/jsconfig.json create mode 100644 frontend/next-env.d.ts rename frontend/{public/index.css => src/app/global.css} (82%) create mode 100644 frontend/src/app/home.module.scss create mode 100644 frontend/src/app/layout.tsx create mode 100644 frontend/src/app/page.tsx delete mode 100644 frontend/src/components/Button/Button.jsx create mode 100644 frontend/src/components/Button/Button.module.scss delete mode 100644 frontend/src/components/Button/Button.styles.js create mode 100644 frontend/src/components/Button/Button.tsx delete mode 100644 frontend/src/components/Donate/Donate.jsx delete mode 100644 frontend/src/components/Donate/Donate.styles.js delete mode 100644 frontend/src/components/Error/Error.jsx rename frontend/src/components/Error/{Error.styles.js => Error.module.scss} (62%) create mode 100644 frontend/src/components/Error/Error.tsx delete mode 100644 frontend/src/components/Footer/Footer.jsx create mode 100644 frontend/src/components/Footer/Footer.module.scss delete mode 100644 frontend/src/components/Footer/Footer.styles.js create mode 100644 frontend/src/components/Footer/Footer.tsx create mode 100644 frontend/src/components/Header/Header.module.scss create mode 100644 frontend/src/components/Header/Header.tsx delete mode 100644 frontend/src/components/Logo/Logo.jsx delete mode 100644 frontend/src/components/Logo/Logo.styles.js delete mode 100644 frontend/src/components/Recents/Recents.jsx create mode 100644 frontend/src/components/Recents/Recents.module.scss delete mode 100644 frontend/src/components/Recents/Recents.styles.js create mode 100644 frontend/src/components/Recents/Recents.tsx rename frontend/src/components/Settings/{Settings.styles.js => Settings.styles.ts} (100%) rename frontend/src/components/Settings/{Settings.jsx => Settings.tsx} (98%) rename frontend/src/components/TranslateDialog/{TranslateDialog.styles.js => TranslateDialog.styles.ts} (100%) rename frontend/src/components/TranslateDialog/{TranslateDialog.jsx => TranslateDialog.tsx} (100%) delete mode 100644 frontend/src/components/index.js create mode 100644 frontend/src/components/index.ts create mode 100644 frontend/src/config/dayjs.ts create mode 100644 frontend/src/i18n/client.ts delete mode 100644 frontend/src/i18n/index.js rename frontend/{public/i18n => src/i18n/locales}/de/common.json (100%) rename frontend/{public/i18n => src/i18n/locales}/de/event.json (100%) rename frontend/{public/i18n => src/i18n/locales}/de/help.json (100%) rename frontend/{public/i18n => src/i18n/locales}/de/home.json (100%) rename frontend/{public/i18n => src/i18n/locales}/de/privacy.json (100%) rename frontend/{public/i18n => src/i18n/locales}/en-GB/common.json (100%) rename frontend/{public/i18n => src/i18n/locales}/en-GB/event.json (100%) rename frontend/{public/i18n => src/i18n/locales}/en-GB/help.json (100%) rename frontend/{public/i18n => src/i18n/locales}/en-GB/home.json (100%) rename frontend/{public/i18n => src/i18n/locales}/en-GB/privacy.json (100%) rename frontend/{public/i18n => src/i18n/locales}/en/common.json (68%) rename frontend/{public/i18n => src/i18n/locales}/en/event.json (100%) rename frontend/{public/i18n => src/i18n/locales}/en/help.json (100%) rename frontend/{public/i18n => src/i18n/locales}/en/home.json (100%) rename frontend/{public/i18n => src/i18n/locales}/en/privacy.json (100%) rename frontend/{public/i18n => src/i18n/locales}/es/common.json (100%) rename frontend/{public/i18n => src/i18n/locales}/es/event.json (100%) rename frontend/{public/i18n => src/i18n/locales}/es/help.json (100%) rename frontend/{public/i18n => src/i18n/locales}/es/home.json (100%) rename frontend/{public/i18n => src/i18n/locales}/es/privacy.json (100%) rename frontend/{public/i18n => src/i18n/locales}/fr/common.json (100%) rename frontend/{public/i18n => src/i18n/locales}/fr/event.json (100%) rename frontend/{public/i18n => src/i18n/locales}/fr/help.json (100%) rename frontend/{public/i18n => src/i18n/locales}/fr/home.json (100%) rename frontend/{public/i18n => src/i18n/locales}/fr/privacy.json (100%) rename frontend/{public/i18n => src/i18n/locales}/hi/common.json (100%) rename frontend/{public/i18n => src/i18n/locales}/hi/event.json (100%) rename frontend/{public/i18n => src/i18n/locales}/hi/help.json (100%) rename frontend/{public/i18n => src/i18n/locales}/hi/home.json (100%) rename frontend/{public/i18n => src/i18n/locales}/hi/privacy.json (100%) rename frontend/{public/i18n => src/i18n/locales}/hu-HU/privacy.json (100%) rename frontend/{public/i18n => src/i18n/locales}/id/common.json (100%) rename frontend/{public/i18n => src/i18n/locales}/id/event.json (100%) rename frontend/{public/i18n => src/i18n/locales}/id/help.json (100%) rename frontend/{public/i18n => src/i18n/locales}/id/home.json (100%) rename frontend/{public/i18n => src/i18n/locales}/id/privacy.json (100%) rename frontend/{public/i18n => src/i18n/locales}/it/common.json (100%) rename frontend/{public/i18n => src/i18n/locales}/it/event.json (100%) rename frontend/{public/i18n => src/i18n/locales}/it/help.json (100%) rename frontend/{public/i18n => src/i18n/locales}/it/home.json (100%) rename frontend/{public/i18n => src/i18n/locales}/it/privacy.json (100%) rename frontend/{public/i18n => src/i18n/locales}/ja/common.json (100%) rename frontend/{public/i18n => src/i18n/locales}/ja/event.json (100%) rename frontend/{public/i18n => src/i18n/locales}/ja/home.json (100%) rename frontend/{public/i18n => src/i18n/locales}/ja/privacy.json (100%) rename frontend/{public/i18n => src/i18n/locales}/ko/common.json (100%) rename frontend/{public/i18n => src/i18n/locales}/ko/event.json (100%) rename frontend/{public/i18n => src/i18n/locales}/ko/help.json (100%) rename frontend/{public/i18n => src/i18n/locales}/ko/home.json (100%) rename frontend/{public/i18n => src/i18n/locales}/ko/privacy.json (100%) rename frontend/{public/i18n => src/i18n/locales}/pl/common.json (100%) rename frontend/{public/i18n => src/i18n/locales}/pl/home.json (100%) rename frontend/{public/i18n => src/i18n/locales}/pl/privacy.json (100%) rename frontend/{public/i18n => src/i18n/locales}/pt-BR/common.json (100%) rename frontend/{public/i18n => src/i18n/locales}/pt-BR/event.json (100%) rename frontend/{public/i18n => src/i18n/locales}/pt-BR/help.json (100%) rename frontend/{public/i18n => src/i18n/locales}/pt-BR/home.json (100%) rename frontend/{public/i18n => src/i18n/locales}/pt-BR/privacy.json (100%) rename frontend/{public/i18n => src/i18n/locales}/pt_PT/common.json (100%) rename frontend/{public/i18n => src/i18n/locales}/pt_PT/event.json (100%) rename frontend/{public/i18n => src/i18n/locales}/pt_PT/help.json (100%) rename frontend/{public/i18n => src/i18n/locales}/pt_PT/home.json (100%) rename frontend/{public/i18n => src/i18n/locales}/pt_PT/privacy.json (100%) rename frontend/{public/i18n => src/i18n/locales}/ru/common.json (100%) rename frontend/{public/i18n => src/i18n/locales}/ru/event.json (100%) rename frontend/{public/i18n => src/i18n/locales}/ru/help.json (100%) rename frontend/{public/i18n => src/i18n/locales}/ru/home.json (100%) rename frontend/{public/i18n => src/i18n/locales}/ru/privacy.json (100%) rename frontend/src/i18n/{locales.js => options.ts} (66%) create mode 100644 frontend/src/i18n/server.ts delete mode 100644 frontend/src/index.jsx rename frontend/src/{pages => pages-old}/Create/Create.jsx (100%) rename frontend/src/{pages => pages-old}/Create/Create.styles.js (100%) rename frontend/src/{pages => pages-old}/Event/Event.jsx (100%) rename frontend/src/{pages => pages-old}/Event/Event.styles.js (100%) rename frontend/src/{pages => pages-old}/Help/Help.jsx (100%) rename frontend/src/{pages => pages-old}/Help/Help.styles.js (100%) rename frontend/src/{pages => pages-old}/Home/Home.jsx (98%) rename frontend/src/{pages => pages-old}/Home/Home.styles.js (100%) rename frontend/src/{pages => pages-old}/Privacy/Privacy.jsx (100%) rename frontend/src/{pages => pages-old}/Privacy/Privacy.styles.js (100%) rename frontend/src/{pages => pages-old}/index.js (100%) delete mode 100644 frontend/src/res/paypal.svg delete mode 100644 frontend/src/services/index.js delete mode 100644 frontend/src/stores/index.js create mode 100644 frontend/src/stores/index.ts delete mode 100644 frontend/src/stores/localeUpdateStore.js create mode 100644 frontend/src/stores/localeUpdateStore.ts delete mode 100644 frontend/src/stores/recentsStore.js create mode 100644 frontend/src/stores/recentsStore.ts delete mode 100644 frontend/src/stores/settingsStore.js create mode 100644 frontend/src/stores/settingsStore.ts delete mode 100644 frontend/src/stores/translateStore.js create mode 100644 frontend/src/stores/translateStore.ts delete mode 100644 frontend/src/stores/twaStore.js create mode 100644 frontend/src/stores/twaStore.ts rename frontend/src/utils/{index.js => detectBrowser.ts} (80%) create mode 100644 frontend/src/utils/index.ts create mode 100644 frontend/src/utils/makeClass.ts create mode 100644 frontend/src/utils/unhyphenate.ts create mode 100644 frontend/tsconfig.json delete mode 100644 frontend/vite.config.js diff --git a/frontend/.eslintrc.js b/frontend/.eslintrc.js deleted file mode 100644 index 0dfd57e..0000000 --- a/frontend/.eslintrc.js +++ /dev/null @@ -1,73 +0,0 @@ -/* 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'], - } -} diff --git a/frontend/.eslintrc.json b/frontend/.eslintrc.json new file mode 100644 index 0000000..58035d5 --- /dev/null +++ b/frontend/.eslintrc.json @@ -0,0 +1,27 @@ +{ + "extends": ["next/core-web-vitals", "plugin:@typescript-eslint/recommended"], + "parser": "@typescript-eslint/parser", + "plugins": ["@typescript-eslint", "simple-import-sort"], + "rules": { + "react/no-unescaped-entities": "off", + "simple-import-sort/imports": "warn", + "@next/next/no-img-element": "off" + }, + "overrides": [ + { + "files": ["*.ts", "*.tsx"], + "rules": { + "simple-import-sort/imports": [ + "warn", + { + "groups": [ + ["^react", "^next", "^@", "^[a-z]"], + ["^/src/"], + ["^./", "^.", "^../"] + ] + } + ] + } + } + ] +} diff --git a/frontend/.gcloudignore b/frontend/.gcloudignore deleted file mode 100644 index f970c16..0000000 --- a/frontend/.gcloudignore +++ /dev/null @@ -1,10 +0,0 @@ -node_modules -.DS_Store -.git -.gitignore -.gcloudignore -src -public -.eslintrc.js -yarn.lock -package.json diff --git a/frontend/.gitignore b/frontend/.gitignore index 8b79af8..dec23b6 100644 --- a/frontend/.gitignore +++ b/frontend/.gitignore @@ -1,8 +1,6 @@ node_modules dist -build -dev-dist +.next -npm-debug.log* yarn-debug.log* yarn-error.log* diff --git a/frontend/.vscode/settings.json b/frontend/.vscode/settings.json new file mode 100644 index 0000000..25fa621 --- /dev/null +++ b/frontend/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "typescript.tsdk": "node_modules/typescript/lib" +} diff --git a/frontend/app.yaml b/frontend/app.yaml deleted file mode 100644 index b3ee472..0000000 --- a/frontend/app.yaml +++ /dev/null @@ -1,15 +0,0 @@ -runtime: nodejs16 -handlers: -# Serve all static files with url ending with a file extension -- url: /(.*\..+)$ - static_files: dist/\1 - upload: (.*\..+)$ - secure: always - redirect_http_response_code: 301 - -# Catch all handler to index.html -- url: /.* - static_files: dist/index.html - upload: dist/index.html - secure: always - redirect_http_response_code: 301 diff --git a/frontend/index.html b/frontend/index.html deleted file mode 100644 index b0f99ac..0000000 --- a/frontend/index.html +++ /dev/null @@ -1,51 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - Crab Fit - - - - - - -
- - - - - diff --git a/frontend/jsconfig.json b/frontend/jsconfig.json deleted file mode 100644 index 064fef1..0000000 --- a/frontend/jsconfig.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "compilerOptions": { - "baseUrl": "./", - "paths": { - "/*": ["./*"] - } - }, - "exclude": [ - "**/node_modules/*", - "**/dist/*", - "**/.git/*" - ] -} diff --git a/frontend/next-env.d.ts b/frontend/next-env.d.ts new file mode 100644 index 0000000..4f11a03 --- /dev/null +++ b/frontend/next-env.d.ts @@ -0,0 +1,5 @@ +/// +/// + +// NOTE: This file should not be edited +// see https://nextjs.org/docs/basic-features/typescript for more information. diff --git a/frontend/package.json b/frontend/package.json index a7482ba..9057510 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,51 +1,46 @@ { "name": "crabfit-frontend", - "version": "1.0.0", + "version": "2.0.0", "private": true, "license": "GPL-3.0-only", "scripts": { - "dev": "vite", - "build": "vite build", - "lint": "eslint --ext .js,.jsx ./src" + "dev": "next dev --port 1234", + "build": "next build", + "start": "next start", + "lint": "next lint" }, "dependencies": { - "@azure/msal-browser": "^2.28.1", - "@microsoft/microsoft-graph-client": "^3.0.2", - "dayjs": "^1.11.5", + "@azure/msal-browser": "^2.37.0", + "@microsoft/microsoft-graph-client": "^3.0.5", + "accept-language": "^3.0.18", + "dayjs": "^1.11.7", "gapi-script": "^1.2.0", - "goober": "^2.1.10", + "goober": "^2.1.13", "hue-map": "^1.0.0", - "i18next": "^21.9.0", - "i18next-browser-languagedetector": "^6.1.5", - "i18next-http-backend": "^1.4.1", - "lucide-react": "^0.84.0", + "i18next": "^22.5.0", + "i18next-browser-languagedetector": "^7.0.1", + "i18next-http-backend": "^2.2.1", + "i18next-resources-to-backend": "^1.1.4", + "lucide-react": "^0.220.0", + "next": "^13.4.3", "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" + "react-hook-form": "^7.43.9", + "react-i18next": "^12.3.1", + "zustand": "^4.3.8" }, "devDependencies": { - "@vitejs/plugin-react": "^2.0.1", - "eslint": "^8.22.0", - "eslint-plugin-react": "^7.30.1", - "vite": "^3.0.7", - "vite-plugin-pwa": "^0.12.3", - "workbox-webpack-plugin": "^6.5.4" + "@types/node": "^20.2.1", + "@types/react": "^18.2.6", + "@types/react-dom": "^18.2.4", + "@typescript-eslint/eslint-plugin": "^5.59.6", + "@typescript-eslint/parser": "^5.59.6", + "eslint": "^8.40.0", + "eslint-config-next": "^13.4.3", + "eslint-plugin-simple-import-sort": "^10.0.0", + "sass": "^1.62.1", + "typescript": "^5.0.4", + "typescript-plugin-css-modules": "^5.0.1" }, "browserslist": { "production": [ diff --git a/frontend/public/index.css b/frontend/src/app/global.css similarity index 82% rename from frontend/public/index.css rename to frontend/src/app/global.css index 2a1da93..eb2e31f 100644 --- a/frontend/public/index.css +++ b/frontend/src/app/global.css @@ -1,21 +1,21 @@ @font-face { font-family: 'Karla'; - src: url('fonts/karla-variable.ttf') format('truetype'); + src: url('/fonts/karla-variable.ttf') format('truetype'); font-weight: 200 800; } @font-face { font-family: 'Samurai Bob'; - src: url('fonts/samuraibob.woff2') format('woff2'), - url('fonts/samuraibob.woff') format('woff'); + src: url('/fonts/samuraibob.woff2') format('woff2'), + url('/fonts/samuraibob.woff') format('woff'); font-weight: 400; font-style: normal; } @font-face { font-family: 'Molot'; - src: url('fonts/molot.woff2') format('woff2'), - url('fonts/molot.woff') format('woff'); + src: url('/fonts/molot.woff2') format('woff2'), + url('/fonts/molot.woff') format('woff'); font-weight: 400; font-style: normal; } @@ -152,23 +152,3 @@ a { *::-webkit-scrollbar-thumb:active { background: var(--secondary); } - -/* IE 10+ */ -@media all and (-ms-high-contrast: none), (-ms-high-contrast: active) { - #app { - text-align: center; - margin: 20vh auto; - font-size: 1.3em; - font-weight: 600; - } - #app::before { - content: '🦀'; - font-size: 1.5em; - display: block; - padding: 20px; - } - #app::after { - display: block; - content: 'Crab Fit doesn\'t work in Internet Explorer. Please try using a modern browser.'; - } -} diff --git a/frontend/src/app/home.module.scss b/frontend/src/app/home.module.scss new file mode 100644 index 0000000..85e7903 --- /dev/null +++ b/frontend/src/app/home.module.scss @@ -0,0 +1,4 @@ +.nav { + text-align: center; + margin: 20px 0; +} diff --git a/frontend/src/app/layout.tsx b/frontend/src/app/layout.tsx new file mode 100644 index 0000000..c55a3cf --- /dev/null +++ b/frontend/src/app/layout.tsx @@ -0,0 +1,36 @@ +import { Metadata } from 'next' + +import { fallbackLng } from '/src/i18n/options' +import { useTranslation } from '/src/i18n/server' + +import './global.css' + +export const metadata: Metadata = { + metadataBase: new URL('https://crab.fit'), + title: 'Crab Fit', + keywords: ['crab', 'fit', 'crabfit', 'schedule', 'availability', 'availabilities', 'when2meet', 'doodle', 'meet', 'plan', 'time', 'timezone'], + description: 'Enter your availability to find a time that works for everyone!', + themeColor: '#F79E00', + manifest: 'manifest.json', + openGraph: { + title: 'Crab Fit', + description: 'Enter your availability to find a time that works for everyone!', + url: '/', + }, + icons: { + icon: 'favicon.ico', + apple: 'logo192.png', + }, +} + +const RootLayout = async ({ children }: { children: React.ReactNode }) => { + const { i18n } = await useTranslation([]) + + return + + {children} + + +} + +export default RootLayout diff --git a/frontend/src/app/page.tsx b/frontend/src/app/page.tsx new file mode 100644 index 0000000..85deea2 --- /dev/null +++ b/frontend/src/app/page.tsx @@ -0,0 +1,24 @@ +import { Button, Footer, Header, Recents } from '/src/components' +import { useTranslation } from '/src/i18n/server' + +import styles from './home.module.scss' + +const Page = async () => { + const { t } = await useTranslation('home') + + return
+
+ + + + + +
+
+} + +export default Page diff --git a/frontend/src/components/Button/Button.jsx b/frontend/src/components/Button/Button.jsx deleted file mode 100644 index c069309..0000000 --- a/frontend/src/components/Button/Button.jsx +++ /dev/null @@ -1,33 +0,0 @@ -import { Pressable } from './Button.styles' - -const Button = ({ - href, - type = 'button', - icon, - children, - secondary, - primaryColor, - secondaryColor, - small, - size, - isLoading, - ...props -}) => ( - - {icon} - {children} - -) - -export default Button diff --git a/frontend/src/components/Button/Button.module.scss b/frontend/src/components/Button/Button.module.scss new file mode 100644 index 0000000..f32d3d4 --- /dev/null +++ b/frontend/src/components/Button/Button.module.scss @@ -0,0 +1,130 @@ +.button { + position: relative; + display: inline-flex; + align-items: center; + justify-content: center; + text-align: center; + cursor: pointer; + border: 0; + text-decoration: none; + font: inherit; + box-sizing: border-box; + background: var(--override-surface-color, var(--primary)); + color: var(--override-text-color, var(--background)); + font-weight: 600; + transition: transform 150ms cubic-bezier(0, 0, 0.58, 1); + border-radius: 3px; + padding: .6em 1.5em; + transform-style: preserve-3d; + margin-bottom: 5px; + + & svg, & img { + height: 1.2em; + width: 1.2em; + margin-right: .5em; + } + + &::before { + content: ''; + position: absolute; + height: 100%; + width: 100%; + top: 0; + left: 0; + background: var(--override-shadow-color, var(--shadow)); + border-radius: inherit; + transform: translate3d(0, 5px, -1em); + transition: transform 150ms cubic-bezier(0, 0, 0.58, 1), box-shadow 150ms cubic-bezier(0, 0, 0.58, 1); + } + + &:hover, &:focus { + transform: translate(0, 1px); + &::before { + transform: translate3d(0, 4px, -1em); + } + } + + &:active { + transform: translate(0, 5px); + &::before { + transform: translate3d(0, 0, -1em); + } + } + + @media print { + &::before { + display: none; + } + } +} + +.small { + padding: .4em 1.3em; +} + +.loading { + color: transparent; + cursor: wait; + + & img { + opacity: 0; + } + + @keyframes load { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } + } + + &::after { + content: ''; + position: absolute; + top: calc(50% - 12px); + left: calc(50% - 12px); + height: 18px; + width: 18px; + border: 3px solid var(--override-text-color, var(--background)); + border-left-color: transparent; + border-radius: 100px; + animation: load .5s linear infinite; + } + + @media (prefers-reduced-motion: reduce) { + &::after { + content: 'loading...'; + color: var(--override-text-color, var(--background)); + animation: none; + width: initial; + height: initial; + left: 50%; + transform: translateX(-50%); + border: 0; + top: 0; + bottom: 0; + display: flex; + align-items: center; + justify-content: center; + } + } +} + +.secondary { + background: transparent; + border: 1px solid var(--override-surface-color, var(--secondary)); + color: var(--override-surface-color, var(--secondary)); + margin-bottom: 0; + + &::before { + content: none; + } + &:hover, &:active, &:focus { + transform: none; + } + + @media print { + box-shadow: 0 4px 0 0 var(--override-shadow-color, var(--secondary)); + } +} diff --git a/frontend/src/components/Button/Button.styles.js b/frontend/src/components/Button/Button.styles.js deleted file mode 100644 index 295c25f..0000000 --- a/frontend/src/components/Button/Button.styles.js +++ /dev/null @@ -1,134 +0,0 @@ -import { styled } from 'goober' - -export const Pressable = styled('button')` - position: relative; - display: inline-flex; - align-items: center; - justify-content: center; - text-align: center; - cursor: pointer; - border: 0; - text-decoration: none; - font: inherit; - box-sizing: border-box; - background: ${props => props.$primaryColor || 'var(--primary)'}; - color: ${props => props.$primaryColor ? '#FFF' : 'var(--background)'}; - font-weight: 600; - transition: transform 150ms cubic-bezier(0, 0, 0.58, 1); - border-radius: 3px; - padding: ${props => props.$small ? '.4em 1.3em' : '.6em 1.5em'}; - transform-style: preserve-3d; - margin-bottom: 5px; - - & svg, & img { - height: 1.2em; - width: 1.2em; - margin-right: .5em; - } - - ${props => props.$size && ` - padding: 0; - height: ${props.$size}; - width: ${props.$size}; - `} - - &::before { - content: ''; - position: absolute; - height: 100%; - width: 100%; - top: 0; - left: 0; - background: ${props => props.$secondaryColor || 'var(--shadow)'}; - border-radius: inherit; - transform: translate3d(0, 5px, -1em); - transition: transform 150ms cubic-bezier(0, 0, 0.58, 1), box-shadow 150ms cubic-bezier(0, 0, 0.58, 1); - } - - &:hover, &:focus { - transform: translate(0, 1px); - &::before { - transform: translate3d(0, 4px, -1em); - } - } - - &:active { - transform: translate(0, 5px); - &::before { - transform: translate3d(0, 0, -1em); - } - } - - ${props => props.$isLoading && ` - color: transparent; - cursor: wait; - - & img { - opacity: 0; - } - - @keyframes load { - from { - transform: rotate(0deg); - } - to { - transform: rotate(360deg); - } - } - - &:after { - content: ''; - position: absolute; - top: calc(50% - 12px); - left: calc(50% - 12px); - height: 18px; - width: 18px; - border: 3px solid ${props.$primaryColor ? '#FFF' : 'var(--background)'}; - border-left-color: transparent; - border-radius: 100px; - animation: load .5s linear infinite; - } - - @media (prefers-reduced-motion: reduce) { - &:after { - content: 'loading...'; - color: ${props.$primaryColor ? '#FFF' : 'var(--background)'}; - animation: none; - width: initial; - height: initial; - left: 50%; - transform: translateX(-50%); - border: 0; - top: 0; - bottom: 0; - display: flex; - align-items: center; - justify-content: center; - } - } - `} - - ${props => props.$secondary && ` - background: transparent; - border: 1px solid ${props.$primaryColor || 'var(--secondary)'}; - color: ${props.$primaryColor || 'var(--secondary)'}; - margin-bottom: 0; - - &::before { - content: none; - } - &:hover, &:active, &:focus { - transform: none; - } - `} - - @media print { - ${props => !props.$secondary && ` - box-shadow: 0 4px 0 0 ${props.$secondaryColor || 'var(--secondary)'}; - `} - - &::before { - display: none; - } - } -` diff --git a/frontend/src/components/Button/Button.tsx b/frontend/src/components/Button/Button.tsx new file mode 100644 index 0000000..46f6349 --- /dev/null +++ b/frontend/src/components/Button/Button.tsx @@ -0,0 +1,59 @@ +import Link from 'next/link' + +import { makeClass } from '/src/utils' + +import styles from './Button.module.scss' + +type ButtonProps = { + /** If provided, will render a link that looks like a button */ + href?: string + icon?: React.ReactNode + children?: React.ReactNode + isSecondary?: boolean + isSmall?: boolean + isLoading?: boolean + /** Override the surface color of the button. Will force the text to #FFFFFF. */ + surfaceColor?: string + /** Override the shadow color of the button */ + shadowColor?: string + // TODO: evaluate + size?: string +} & Omit & React.ComponentProps<'a'>, 'ref'> + +const Button: React.FC = ({ + href, + type = 'button', + icon, + children, + isSecondary, + isSmall, + isLoading, + surfaceColor, + shadowColor, + size, + style, + ...props +}) => { + const sharedProps = { + className: makeClass( + styles.button, + isSecondary && styles.secondary, + isSmall && styles.small, + isLoading && styles.loading, + ), + style: { + ...surfaceColor && { '--override-surface-color': surfaceColor, '--override-text-color': '#FFFFFF' }, + ...shadowColor && { '--override-shadow-color': shadowColor }, + ...size && { padding: 0, height: size, width: size }, + ...style, + }, + children: [icon, children], + ...props, + } + + return href + ? + : - - { - if (modalRef.current?.contains(e.relatedTarget)) return - setIsOpen(false) - if (e.relatedTarget && e.relatedTarget.id === 'donate_button') { - setClosed(true) - } - }} - > - Donate with PayPal - {t('donate.options.$2')} - {t('donate.options.$5')} - {t('donate.options.$10')} - {t('donate.options.choose')} - - - ) -} - -export default Donate diff --git a/frontend/src/components/Donate/Donate.styles.js b/frontend/src/components/Donate/Donate.styles.js deleted file mode 100644 index ee22153..0000000 --- a/frontend/src/components/Donate/Donate.styles.js +++ /dev/null @@ -1,64 +0,0 @@ -import { styled } from 'goober' -import { forwardRef } from 'react' - -export const Wrapper = styled('div')` - margin-top: 6px; - margin-left: 12px; - position: relative; -` - -export const Options = styled('div', forwardRef)` - position: absolute; - bottom: calc(100% + 20px); - right: 0; - background-color: var(--background); - border: 1px solid var(--surface); - z-index: 60; - padding: 4px 10px; - border-radius: 14px; - box-sizing: border-box; - max-width: calc(100vw - 20px); - box-shadow: 0 3px 6px 0 rgba(0,0,0,.3); - - visibility: hidden; - pointer-events: none; - opacity: 0; - transform: translateY(5px); - transition: opacity .15s, transform .15s, visibility .15s; - - ${props => props.$isOpen && ` - pointer-events: all; - opacity: 1; - transform: translateY(0); - visibility: visible; - `} - - & img { - width: 80px; - margin: 10px auto 0; - display: block; - } - - & a { - display: block; - white-space: nowrap; - text-align: center; - padding: 4px 20px; - margin: 6px 0; - text-decoration: none; - border-radius: 100px; - background-color: var(--primary); - color: var(--background); - - &:hover { - text-decoration: underline; - } - & strong { - font-weight: 800; - } - } - - @media (prefers-reduced-motion: reduce) { - transition: none; - } -` diff --git a/frontend/src/components/Error/Error.jsx b/frontend/src/components/Error/Error.jsx deleted file mode 100644 index fdea594..0000000 --- a/frontend/src/components/Error/Error.jsx +++ /dev/null @@ -1,17 +0,0 @@ -import { X } from 'lucide-react' - -import { Wrapper, CloseButton } from './Error.styles' - -const Error = ({ - children, - onClose, - open = true, - ...props -}) => ( - - {children} - - -) - -export default Error diff --git a/frontend/src/components/Error/Error.styles.js b/frontend/src/components/Error/Error.module.scss similarity index 62% rename from frontend/src/components/Error/Error.styles.js rename to frontend/src/components/Error/Error.module.scss index 4164a3b..0a9e38f 100644 --- a/frontend/src/components/Error/Error.styles.js +++ b/frontend/src/components/Error/Error.module.scss @@ -1,6 +1,4 @@ -import { styled } from 'goober' - -export const Wrapper = styled('div')` +.error { border-radius: 3px; background-color: var(--error); color: #FFFFFF; @@ -15,21 +13,21 @@ export const Wrapper = styled('div')` visibility: hidden; transition: margin .2s, padding .2s, max-height .2s; - ${props => props.open && ` - opacity: 1; - visibility: visible; - margin: 20px 0; - padding: 12px 16px; - max-height: 60px; - transition: opacity .15s .2s, max-height .2s, margin .2s, padding .2s, visibility .2s; - `} - @media (prefers-reduced-motion: reduce) { transition: none; } -` +} -export const CloseButton = styled('button')` +.open { + opacity: 1; + visibility: visible; + margin: 20px 0; + padding: 12px 16px; + max-height: 60px; + transition: opacity .15s .2s, max-height .2s, margin .2s, padding .2s, visibility .2s; +} + +.closeButton { border: 0; background: none; height: 30px; @@ -41,4 +39,4 @@ export const CloseButton = styled('button')` justify-content: center; margin-left: 16px; padding: 0; -` +} diff --git a/frontend/src/components/Error/Error.tsx b/frontend/src/components/Error/Error.tsx new file mode 100644 index 0000000..0c644c4 --- /dev/null +++ b/frontend/src/components/Error/Error.tsx @@ -0,0 +1,25 @@ +'use client' + +import { X } from 'lucide-react' + +import { makeClass } from '/src/utils' + +import styles from './Error.module.scss' + +interface ErrorProps { + children?: React.ReactNode + onClose: () => void +} + +const Error = ({ children, onClose }: ErrorProps) => +
+ {children} + +
+ +export default Error diff --git a/frontend/src/components/Footer/Footer.jsx b/frontend/src/components/Footer/Footer.jsx deleted file mode 100644 index 4666fe7..0000000 --- a/frontend/src/components/Footer/Footer.jsx +++ /dev/null @@ -1,17 +0,0 @@ -import { useTranslation } from 'react-i18next' - -import { Donate } from '/src/components' -import { Wrapper } from './Footer.styles' - -const Footer = props => { - const { t } = useTranslation('common') - - return ( - - ) -} - -export default Footer diff --git a/frontend/src/components/Footer/Footer.module.scss b/frontend/src/components/Footer/Footer.module.scss new file mode 100644 index 0000000..e1dbea5 --- /dev/null +++ b/frontend/src/components/Footer/Footer.module.scss @@ -0,0 +1,24 @@ +.footer { + width: 600px; + margin: 20px auto; + max-width: calc(100% - 60px); + display: flex; + align-items: center; + justify-content: space-between; + + @media print { + display: none; + } +} + +.small { + margin: 60px auto 0; + width: 250px; + max-width: initial; + display: block; + + & span { + display: block; + margin-bottom: 20px; + } +} diff --git a/frontend/src/components/Footer/Footer.styles.js b/frontend/src/components/Footer/Footer.styles.js deleted file mode 100644 index b9b9884..0000000 --- a/frontend/src/components/Footer/Footer.styles.js +++ /dev/null @@ -1,26 +0,0 @@ -import { styled } from 'goober' - -export const Wrapper = styled('footer')` - width: 600px; - margin: 20px auto; - max-width: calc(100% - 60px); - display: flex; - align-items: center; - justify-content: space-between; - - ${props => props.small && ` - margin: 60px auto 0; - width: 250px; - max-width: initial; - display: block; - - & span { - display: block; - margin-bottom: 20px; - } - `} - - @media print { - display: none; - } -` diff --git a/frontend/src/components/Footer/Footer.tsx b/frontend/src/components/Footer/Footer.tsx new file mode 100644 index 0000000..ef5eb6c --- /dev/null +++ b/frontend/src/components/Footer/Footer.tsx @@ -0,0 +1,30 @@ +import { Button } from '/src/components' +import { useTranslation } from '/src/i18n/server' +import { makeClass } from '/src/utils' + +import styles from './Footer.module.scss' + +interface FooterProps { + isSmall?: boolean +} + +const Footer = async ({ isSmall }: FooterProps) => { + const { t } = await useTranslation('common') + + return
+ {t('donate.info')} + +
+} + +export default Footer diff --git a/frontend/src/components/Header/Header.module.scss b/frontend/src/components/Header/Header.module.scss new file mode 100644 index 0000000..9f8aa40 --- /dev/null +++ b/frontend/src/components/Header/Header.module.scss @@ -0,0 +1,124 @@ +.header { + display: flex; + align-items: center; + justify-content: center; + flex-direction: column; +} + +@keyframes jelly { + from,to { + transform: scale(1,1) + } + 25% { + transform: scale(.9,1.1) + } + 50% { + transform: scale(1.1,.9) + } + 75% { + transform: scale(.95,1.05) + } +} + +.link { + text-decoration: none; + + &:hover img { + animation: jelly .5s 1; + } + @media (prefers-reduced-motion: reduce) { + &:hover img { + animation: none; + } + } +} + +.top { + display: inline-flex; + justify-content: center; + align-items: center; +} + +.logo { + width: 2.5rem; + margin-right: 16px; +} + +.title { + display: block; + font-size: 2rem; + color: var(--primary); + font-family: 'Molot', sans-serif; + font-weight: 400; + text-shadow: 0 2px 0 var(--shadow); + line-height: 1em; +} + +.tagline { + text-decoration: underline; + font-size: 14px; + padding-top: 2px; + display: flex; + align-items: center; + justify-content: center; + + @media print { + display: none; + } +} + +.subtitle { + display: block; + margin: 0; + font-size: 3rem; + text-align: center; + font-family: 'Samurai Bob', sans-serif; + font-weight: 400; + color: var(--secondary); + line-height: 1em; + text-transform: uppercase; +} + +.hasAltChars { + font-family: sans-serif; + font-size: 2rem; + font-weight: 600; + line-height: 1.2em; + padding-top: .3em; +} + +.bigTitle { + margin: 0; + font-size: 4rem; + text-align: center; + color: var(--primary); + font-family: 'Molot', sans-serif; + font-weight: 400; + text-shadow: 0 4px 0 var(--shadow); + line-height: 1em; + text-transform: uppercase; + + @media (max-width: 350px) { + font-size: 3.5rem; + } +} + +.bigLogo { + width: 80px; + transition: transform .15s; + animation: jelly .5s 1 .05s; + user-select: none; + + &:active { + animation: none; + transform: scale(.85); + } + + @media (prefers-reduced-motion: reduce) { + animation: none; + transition: none; + &:active { + transform: none; + } + } +} diff --git a/frontend/src/components/Header/Header.tsx b/frontend/src/components/Header/Header.tsx new file mode 100644 index 0000000..7026aea --- /dev/null +++ b/frontend/src/components/Header/Header.tsx @@ -0,0 +1,32 @@ +import Link from 'next/link' + +import { useTranslation } from '/src/i18n/server' +import logo from '/src/res/logo.svg' +import { makeClass } from '/src/utils' + +import styles from './Header.module.scss' + +interface HeaderProps { + /** Show the full header */ + isFull?: boolean +} + +const Header = async ({ isFull }: HeaderProps) => { + const { t } = await useTranslation(['common', 'home']) + + return
+ {isFull ? <> + + {t('home:create')} +

CRAB FIT

+ : +
+ + CRAB FIT +
+ {t('common:tagline')} + } +
+} + +export default Header diff --git a/frontend/src/components/Logo/Logo.jsx b/frontend/src/components/Logo/Logo.jsx deleted file mode 100644 index 88c8c12..0000000 --- a/frontend/src/components/Logo/Logo.jsx +++ /dev/null @@ -1,31 +0,0 @@ -import { useTranslation } from 'react-i18next' -import { Link } from 'react-router-dom' - -import { - Wrapper, - A, - Top, - Image, - Title, - Tagline, -} from './Logo.styles' - -import image from '/src/res/logo.svg' - -const Logo = () => { - const { t } = useTranslation('common') - - return ( - - - - - CRAB FIT - - {t('common:tagline')} - - - ) -} - -export default Logo diff --git a/frontend/src/components/Logo/Logo.styles.js b/frontend/src/components/Logo/Logo.styles.js deleted file mode 100644 index 99afb41..0000000 --- a/frontend/src/components/Logo/Logo.styles.js +++ /dev/null @@ -1,69 +0,0 @@ -import { styled } from 'goober' - -export const Wrapper = styled('div')` - display: flex; - align-items: center; - justify-content: center; -` - -export const A = styled('a')` - text-decoration: none; - - @keyframes jelly { - from,to { - transform: scale(1,1) - } - 25% { - transform: scale(.9,1.1) - } - 50% { - transform: scale(1.1,.9) - } - 75% { - transform: scale(.95,1.05) - } - } - - &:hover img { - animation: jelly .5s 1; - } - @media (prefers-reduced-motion: reduce) { - &:hover img { - animation: none; - } - } -` - -export const Top = styled('div')` - display: inline-flex; - justify-content: center; - align-items: center; -` - -export const Image = styled('img')` - width: 2.5rem; - margin-right: 16px; -` - -export const Title = styled('span')` - display: block; - font-size: 2rem; - color: var(--primary); - font-family: 'Molot', sans-serif; - font-weight: 400; - text-shadow: 0 2px 0 var(--shadow); - line-height: 1em; -` - -export const Tagline = styled('span')` - text-decoration: underline; - font-size: 14px; - padding-top: 2px; - display: flex; - align-items: center; - justify-content: center; - - @media print { - display: none; - } -` diff --git a/frontend/src/components/Recents/Recents.jsx b/frontend/src/components/Recents/Recents.jsx deleted file mode 100644 index 9d4a9fe..0000000 --- a/frontend/src/components/Recents/Recents.jsx +++ /dev/null @@ -1,34 +0,0 @@ -import { useTranslation } from 'react-i18next' -import dayjs from 'dayjs' -import relativeTime from 'dayjs/plugin/relativeTime' - -import { useRecentsStore, useLocaleUpdateStore } from '/src/stores' - -import { AboutSection, StyledMain } from '../../pages/Home/Home.styles' -import { Wrapper, Recent } from './Recents.styles' - -dayjs.extend(relativeTime) - -const Recents = ({ target }) => { - const recents = useRecentsStore(state => state.recents) - const locale = useLocaleUpdateStore(state => state.locale) - const { t } = useTranslation(['home', 'common']) - - return !!recents.length && ( - - - -

{t('home:recently_visited')}

- {recents.map(event => ( - - {event.name} - {t('common:created', { date: dayjs.unix(event.created).fromNow() })} - - ))} -
-
-
- ) -} - -export default Recents diff --git a/frontend/src/components/Recents/Recents.module.scss b/frontend/src/components/Recents/Recents.module.scss new file mode 100644 index 0000000..c8049c2 --- /dev/null +++ b/frontend/src/components/Recents/Recents.module.scss @@ -0,0 +1,35 @@ +.recent { + text-decoration: none; + display: flex; + align-items: center; + justify-content: space-between; + padding: 5px 0; + flex-wrap: wrap; + + &:hover .name { + text-decoration: underline; + } + + @media (max-width: 500px) { + display: block; + } +} + +.name { + font-weight: 700; + font-size: 1.1em; + color: var(--secondary); + flex: 1; + display: block; +} + +.date { + font-weight: 400; + opacity: .8; + white-space: nowrap; + color: var(--text); + + @media (max-width: 500px) { + white-space: normal; + } +} diff --git a/frontend/src/components/Recents/Recents.styles.js b/frontend/src/components/Recents/Recents.styles.js deleted file mode 100644 index fd0eec8..0000000 --- a/frontend/src/components/Recents/Recents.styles.js +++ /dev/null @@ -1,42 +0,0 @@ -import { styled } from 'goober' - -export const Wrapper = styled('div')` - @media print { - display: none; - } -` - -export const Recent = styled('a')` - text-decoration: none; - display: flex; - align-items: center; - justify-content: space-between; - padding: 5px 0; - flex-wrap: wrap; - - & .name { - font-weight: 700; - font-size: 1.1em; - color: var(--secondary); - flex: 1; - display: block; - } - & .date { - font-weight: 400; - opacity: .8; - white-space: nowrap; - color: var(--text); - } - - &:hover .name { - text-decoration: underline; - } - - @media (max-width: 500px) { - display: block; - - & .date { - white-space: normal; - } - } -` diff --git a/frontend/src/components/Recents/Recents.tsx b/frontend/src/components/Recents/Recents.tsx new file mode 100644 index 0000000..502eb75 --- /dev/null +++ b/frontend/src/components/Recents/Recents.tsx @@ -0,0 +1,35 @@ +'use client' + +import Link from 'next/link' + +import dayjs from '/src/config/dayjs' +import { useTranslation } from '/src/i18n/client' +import { useRecentsStore, useStore } from '/src/stores' + +import styles from './Recents.module.scss' + +interface RecentsProps { + target?: React.ComponentProps<'a'>['target'] +} + +const Recents = ({ target }: RecentsProps) => { + const recents = useStore(useRecentsStore, state => state.recents) + const { t } = useTranslation(['home', 'common']) + + return recents?.length ?
+
+

{t('home:recently_visited')}

+ {recents.map(event => ( + + {event.name} + {t('common:created', { date: dayjs.unix(event.created_at).fromNow() })} + + ))} +
+
: null +} + +export default Recents diff --git a/frontend/src/components/Settings/Settings.styles.js b/frontend/src/components/Settings/Settings.styles.ts similarity index 100% rename from frontend/src/components/Settings/Settings.styles.js rename to frontend/src/components/Settings/Settings.styles.ts diff --git a/frontend/src/components/Settings/Settings.jsx b/frontend/src/components/Settings/Settings.tsx similarity index 98% rename from frontend/src/components/Settings/Settings.jsx rename to frontend/src/components/Settings/Settings.tsx index 7112194..6c0e00a 100644 --- a/frontend/src/components/Settings/Settings.jsx +++ b/frontend/src/components/Settings/Settings.tsx @@ -1,5 +1,4 @@ import { useState, useEffect, useRef } from 'react' -import { useLocation } from 'react-router-dom' import { useTranslation } from 'react-i18next' import dayjs from 'dayjs' import { Settings as SettingsIcon } from 'lucide-react' @@ -18,6 +17,7 @@ import { import locales from '/src/i18n/locales' import { unhyphenate } from '/src/utils' +import { useRouter } from 'next/router' // Language specific options const setDefaults = (lang, store) => { @@ -28,7 +28,7 @@ const setDefaults = (lang, store) => { } const Settings = () => { - const { pathname } = useLocation() + const { pathname } = useRouter() const store = useSettingsStore() const [isOpen, _setIsOpen] = useState(false) const { t, i18n } = useTranslation('common') diff --git a/frontend/src/components/TranslateDialog/TranslateDialog.styles.js b/frontend/src/components/TranslateDialog/TranslateDialog.styles.ts similarity index 100% rename from frontend/src/components/TranslateDialog/TranslateDialog.styles.js rename to frontend/src/components/TranslateDialog/TranslateDialog.styles.ts diff --git a/frontend/src/components/TranslateDialog/TranslateDialog.jsx b/frontend/src/components/TranslateDialog/TranslateDialog.tsx similarity index 100% rename from frontend/src/components/TranslateDialog/TranslateDialog.jsx rename to frontend/src/components/TranslateDialog/TranslateDialog.tsx diff --git a/frontend/src/components/index.js b/frontend/src/components/index.js deleted file mode 100644 index a0cb606..0000000 --- a/frontend/src/components/index.js +++ /dev/null @@ -1,24 +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 TranslateDialog } from './TranslateDialog/TranslateDialog' - -export const _GoogleCalendar = () => import('./GoogleCalendar/GoogleCalendar') -export const _OutlookCalendar = () => import('./OutlookCalendar/OutlookCalendar') diff --git a/frontend/src/components/index.ts b/frontend/src/components/index.ts new file mode 100644 index 0000000..28458a9 --- /dev/null +++ b/frontend/src/components/index.ts @@ -0,0 +1,23 @@ +// 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 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 Header } from './Header/Header' +// export { default as TranslateDialog } from './TranslateDialog/TranslateDialog' + +// export const _GoogleCalendar = () => import('./GoogleCalendar/GoogleCalendar') +// export const _OutlookCalendar = () => import('./OutlookCalendar/OutlookCalendar') diff --git a/frontend/src/config/dayjs.ts b/frontend/src/config/dayjs.ts new file mode 100644 index 0000000..0cd2d81 --- /dev/null +++ b/frontend/src/config/dayjs.ts @@ -0,0 +1,6 @@ +import dayjs from 'dayjs' +import relativeTime from 'dayjs/plugin/relativeTime' + +dayjs.extend(relativeTime) + +export default dayjs diff --git a/frontend/src/i18n/client.ts b/frontend/src/i18n/client.ts new file mode 100644 index 0000000..eae3a3b --- /dev/null +++ b/frontend/src/i18n/client.ts @@ -0,0 +1,25 @@ +'use client' + +import { initReactI18next, useTranslation as useTranslationHook } from 'react-i18next' +import i18next from 'i18next' +import LanguageDetector from 'i18next-browser-languagedetector' +import resourcesToBackend from 'i18next-resources-to-backend' + +import { getOptions } from './options' + + +i18next + .use(initReactI18next) + .use(LanguageDetector) + .use(resourcesToBackend((language: string, namespace: string) => + import(`./locales/${language}/${namespace}.json`) + )) + .init({ + ...getOptions(), + lng: undefined, + detection: { + order: ['htmlTag', 'cookie', 'navigator'], + }, + }) + +export const useTranslation: typeof useTranslationHook = (ns, options) => useTranslationHook(ns, options) diff --git a/frontend/src/i18n/index.js b/frontend/src/i18n/index.js deleted file mode 100644 index e18ebe1..0000000 --- a/frontend/src/i18n/index.js +++ /dev/null @@ -1,28 +0,0 @@ -import i18n from 'i18next' -import { initReactI18next } from 'react-i18next' -import LanguageDetector from 'i18next-browser-languagedetector' -import Backend from 'i18next-http-backend' - -import locales from './locales' - -const storedLang = localStorage.getItem('i18nextLng') - -i18n - .use(LanguageDetector) - .use(Backend) - .use(initReactI18next) - .init({ - fallbackLng: 'en', - supportedLngs: Object.keys(locales), - ns: 'common', - debug: process.env.NODE_ENV !== 'production', - interpolation: { - escapeValue: false, - }, - backend: { - loadPath: '/i18n/{{lng}}/{{ns}}.json', - }, - storedLang, - }).then(() => document.documentElement.setAttribute('lang', i18n.language)) - -export default i18n diff --git a/frontend/public/i18n/de/common.json b/frontend/src/i18n/locales/de/common.json similarity index 100% rename from frontend/public/i18n/de/common.json rename to frontend/src/i18n/locales/de/common.json diff --git a/frontend/public/i18n/de/event.json b/frontend/src/i18n/locales/de/event.json similarity index 100% rename from frontend/public/i18n/de/event.json rename to frontend/src/i18n/locales/de/event.json diff --git a/frontend/public/i18n/de/help.json b/frontend/src/i18n/locales/de/help.json similarity index 100% rename from frontend/public/i18n/de/help.json rename to frontend/src/i18n/locales/de/help.json diff --git a/frontend/public/i18n/de/home.json b/frontend/src/i18n/locales/de/home.json similarity index 100% rename from frontend/public/i18n/de/home.json rename to frontend/src/i18n/locales/de/home.json diff --git a/frontend/public/i18n/de/privacy.json b/frontend/src/i18n/locales/de/privacy.json similarity index 100% rename from frontend/public/i18n/de/privacy.json rename to frontend/src/i18n/locales/de/privacy.json diff --git a/frontend/public/i18n/en-GB/common.json b/frontend/src/i18n/locales/en-GB/common.json similarity index 100% rename from frontend/public/i18n/en-GB/common.json rename to frontend/src/i18n/locales/en-GB/common.json diff --git a/frontend/public/i18n/en-GB/event.json b/frontend/src/i18n/locales/en-GB/event.json similarity index 100% rename from frontend/public/i18n/en-GB/event.json rename to frontend/src/i18n/locales/en-GB/event.json diff --git a/frontend/public/i18n/en-GB/help.json b/frontend/src/i18n/locales/en-GB/help.json similarity index 100% rename from frontend/public/i18n/en-GB/help.json rename to frontend/src/i18n/locales/en-GB/help.json diff --git a/frontend/public/i18n/en-GB/home.json b/frontend/src/i18n/locales/en-GB/home.json similarity index 100% rename from frontend/public/i18n/en-GB/home.json rename to frontend/src/i18n/locales/en-GB/home.json diff --git a/frontend/public/i18n/en-GB/privacy.json b/frontend/src/i18n/locales/en-GB/privacy.json similarity index 100% rename from frontend/public/i18n/en-GB/privacy.json rename to frontend/src/i18n/locales/en-GB/privacy.json diff --git a/frontend/public/i18n/en/common.json b/frontend/src/i18n/locales/en/common.json similarity index 68% rename from frontend/public/i18n/en/common.json rename to frontend/src/i18n/locales/en/common.json index 0972bf2..60bfb49 100644 --- a/frontend/public/i18n/en/common.json +++ b/frontend/src/i18n/locales/en/common.json @@ -6,18 +6,7 @@ "donate": { "info": "Thank you for using Crab Fit. If you like it, consider donating.", "button": "Donate", - "title": "Every amount counts :)", - "options": { - "$2": "Donate $2", - "$5": "Donate $5", - "$10": "Donate $10", - "choose": "Choose an amount" - }, - "messages": { - "about": "If it's helped you out at all, consider donating to help keep it running. 🦀", - "success": "Thank you for your donation! Without you, Crab Fit wouldn't be free, so thank you and keep being super awesome!", - "error": "Cannot make donation through Google. Please try donating through the website crab.fit 🦀" - } + "title": "Every amount counts :)" }, "options": { "name": "Options", diff --git a/frontend/public/i18n/en/event.json b/frontend/src/i18n/locales/en/event.json similarity index 100% rename from frontend/public/i18n/en/event.json rename to frontend/src/i18n/locales/en/event.json diff --git a/frontend/public/i18n/en/help.json b/frontend/src/i18n/locales/en/help.json similarity index 100% rename from frontend/public/i18n/en/help.json rename to frontend/src/i18n/locales/en/help.json diff --git a/frontend/public/i18n/en/home.json b/frontend/src/i18n/locales/en/home.json similarity index 100% rename from frontend/public/i18n/en/home.json rename to frontend/src/i18n/locales/en/home.json diff --git a/frontend/public/i18n/en/privacy.json b/frontend/src/i18n/locales/en/privacy.json similarity index 100% rename from frontend/public/i18n/en/privacy.json rename to frontend/src/i18n/locales/en/privacy.json diff --git a/frontend/public/i18n/es/common.json b/frontend/src/i18n/locales/es/common.json similarity index 100% rename from frontend/public/i18n/es/common.json rename to frontend/src/i18n/locales/es/common.json diff --git a/frontend/public/i18n/es/event.json b/frontend/src/i18n/locales/es/event.json similarity index 100% rename from frontend/public/i18n/es/event.json rename to frontend/src/i18n/locales/es/event.json diff --git a/frontend/public/i18n/es/help.json b/frontend/src/i18n/locales/es/help.json similarity index 100% rename from frontend/public/i18n/es/help.json rename to frontend/src/i18n/locales/es/help.json diff --git a/frontend/public/i18n/es/home.json b/frontend/src/i18n/locales/es/home.json similarity index 100% rename from frontend/public/i18n/es/home.json rename to frontend/src/i18n/locales/es/home.json diff --git a/frontend/public/i18n/es/privacy.json b/frontend/src/i18n/locales/es/privacy.json similarity index 100% rename from frontend/public/i18n/es/privacy.json rename to frontend/src/i18n/locales/es/privacy.json diff --git a/frontend/public/i18n/fr/common.json b/frontend/src/i18n/locales/fr/common.json similarity index 100% rename from frontend/public/i18n/fr/common.json rename to frontend/src/i18n/locales/fr/common.json diff --git a/frontend/public/i18n/fr/event.json b/frontend/src/i18n/locales/fr/event.json similarity index 100% rename from frontend/public/i18n/fr/event.json rename to frontend/src/i18n/locales/fr/event.json diff --git a/frontend/public/i18n/fr/help.json b/frontend/src/i18n/locales/fr/help.json similarity index 100% rename from frontend/public/i18n/fr/help.json rename to frontend/src/i18n/locales/fr/help.json diff --git a/frontend/public/i18n/fr/home.json b/frontend/src/i18n/locales/fr/home.json similarity index 100% rename from frontend/public/i18n/fr/home.json rename to frontend/src/i18n/locales/fr/home.json diff --git a/frontend/public/i18n/fr/privacy.json b/frontend/src/i18n/locales/fr/privacy.json similarity index 100% rename from frontend/public/i18n/fr/privacy.json rename to frontend/src/i18n/locales/fr/privacy.json diff --git a/frontend/public/i18n/hi/common.json b/frontend/src/i18n/locales/hi/common.json similarity index 100% rename from frontend/public/i18n/hi/common.json rename to frontend/src/i18n/locales/hi/common.json diff --git a/frontend/public/i18n/hi/event.json b/frontend/src/i18n/locales/hi/event.json similarity index 100% rename from frontend/public/i18n/hi/event.json rename to frontend/src/i18n/locales/hi/event.json diff --git a/frontend/public/i18n/hi/help.json b/frontend/src/i18n/locales/hi/help.json similarity index 100% rename from frontend/public/i18n/hi/help.json rename to frontend/src/i18n/locales/hi/help.json diff --git a/frontend/public/i18n/hi/home.json b/frontend/src/i18n/locales/hi/home.json similarity index 100% rename from frontend/public/i18n/hi/home.json rename to frontend/src/i18n/locales/hi/home.json diff --git a/frontend/public/i18n/hi/privacy.json b/frontend/src/i18n/locales/hi/privacy.json similarity index 100% rename from frontend/public/i18n/hi/privacy.json rename to frontend/src/i18n/locales/hi/privacy.json diff --git a/frontend/public/i18n/hu-HU/privacy.json b/frontend/src/i18n/locales/hu-HU/privacy.json similarity index 100% rename from frontend/public/i18n/hu-HU/privacy.json rename to frontend/src/i18n/locales/hu-HU/privacy.json diff --git a/frontend/public/i18n/id/common.json b/frontend/src/i18n/locales/id/common.json similarity index 100% rename from frontend/public/i18n/id/common.json rename to frontend/src/i18n/locales/id/common.json diff --git a/frontend/public/i18n/id/event.json b/frontend/src/i18n/locales/id/event.json similarity index 100% rename from frontend/public/i18n/id/event.json rename to frontend/src/i18n/locales/id/event.json diff --git a/frontend/public/i18n/id/help.json b/frontend/src/i18n/locales/id/help.json similarity index 100% rename from frontend/public/i18n/id/help.json rename to frontend/src/i18n/locales/id/help.json diff --git a/frontend/public/i18n/id/home.json b/frontend/src/i18n/locales/id/home.json similarity index 100% rename from frontend/public/i18n/id/home.json rename to frontend/src/i18n/locales/id/home.json diff --git a/frontend/public/i18n/id/privacy.json b/frontend/src/i18n/locales/id/privacy.json similarity index 100% rename from frontend/public/i18n/id/privacy.json rename to frontend/src/i18n/locales/id/privacy.json diff --git a/frontend/public/i18n/it/common.json b/frontend/src/i18n/locales/it/common.json similarity index 100% rename from frontend/public/i18n/it/common.json rename to frontend/src/i18n/locales/it/common.json diff --git a/frontend/public/i18n/it/event.json b/frontend/src/i18n/locales/it/event.json similarity index 100% rename from frontend/public/i18n/it/event.json rename to frontend/src/i18n/locales/it/event.json diff --git a/frontend/public/i18n/it/help.json b/frontend/src/i18n/locales/it/help.json similarity index 100% rename from frontend/public/i18n/it/help.json rename to frontend/src/i18n/locales/it/help.json diff --git a/frontend/public/i18n/it/home.json b/frontend/src/i18n/locales/it/home.json similarity index 100% rename from frontend/public/i18n/it/home.json rename to frontend/src/i18n/locales/it/home.json diff --git a/frontend/public/i18n/it/privacy.json b/frontend/src/i18n/locales/it/privacy.json similarity index 100% rename from frontend/public/i18n/it/privacy.json rename to frontend/src/i18n/locales/it/privacy.json diff --git a/frontend/public/i18n/ja/common.json b/frontend/src/i18n/locales/ja/common.json similarity index 100% rename from frontend/public/i18n/ja/common.json rename to frontend/src/i18n/locales/ja/common.json diff --git a/frontend/public/i18n/ja/event.json b/frontend/src/i18n/locales/ja/event.json similarity index 100% rename from frontend/public/i18n/ja/event.json rename to frontend/src/i18n/locales/ja/event.json diff --git a/frontend/public/i18n/ja/home.json b/frontend/src/i18n/locales/ja/home.json similarity index 100% rename from frontend/public/i18n/ja/home.json rename to frontend/src/i18n/locales/ja/home.json diff --git a/frontend/public/i18n/ja/privacy.json b/frontend/src/i18n/locales/ja/privacy.json similarity index 100% rename from frontend/public/i18n/ja/privacy.json rename to frontend/src/i18n/locales/ja/privacy.json diff --git a/frontend/public/i18n/ko/common.json b/frontend/src/i18n/locales/ko/common.json similarity index 100% rename from frontend/public/i18n/ko/common.json rename to frontend/src/i18n/locales/ko/common.json diff --git a/frontend/public/i18n/ko/event.json b/frontend/src/i18n/locales/ko/event.json similarity index 100% rename from frontend/public/i18n/ko/event.json rename to frontend/src/i18n/locales/ko/event.json diff --git a/frontend/public/i18n/ko/help.json b/frontend/src/i18n/locales/ko/help.json similarity index 100% rename from frontend/public/i18n/ko/help.json rename to frontend/src/i18n/locales/ko/help.json diff --git a/frontend/public/i18n/ko/home.json b/frontend/src/i18n/locales/ko/home.json similarity index 100% rename from frontend/public/i18n/ko/home.json rename to frontend/src/i18n/locales/ko/home.json diff --git a/frontend/public/i18n/ko/privacy.json b/frontend/src/i18n/locales/ko/privacy.json similarity index 100% rename from frontend/public/i18n/ko/privacy.json rename to frontend/src/i18n/locales/ko/privacy.json diff --git a/frontend/public/i18n/pl/common.json b/frontend/src/i18n/locales/pl/common.json similarity index 100% rename from frontend/public/i18n/pl/common.json rename to frontend/src/i18n/locales/pl/common.json diff --git a/frontend/public/i18n/pl/home.json b/frontend/src/i18n/locales/pl/home.json similarity index 100% rename from frontend/public/i18n/pl/home.json rename to frontend/src/i18n/locales/pl/home.json diff --git a/frontend/public/i18n/pl/privacy.json b/frontend/src/i18n/locales/pl/privacy.json similarity index 100% rename from frontend/public/i18n/pl/privacy.json rename to frontend/src/i18n/locales/pl/privacy.json diff --git a/frontend/public/i18n/pt-BR/common.json b/frontend/src/i18n/locales/pt-BR/common.json similarity index 100% rename from frontend/public/i18n/pt-BR/common.json rename to frontend/src/i18n/locales/pt-BR/common.json diff --git a/frontend/public/i18n/pt-BR/event.json b/frontend/src/i18n/locales/pt-BR/event.json similarity index 100% rename from frontend/public/i18n/pt-BR/event.json rename to frontend/src/i18n/locales/pt-BR/event.json diff --git a/frontend/public/i18n/pt-BR/help.json b/frontend/src/i18n/locales/pt-BR/help.json similarity index 100% rename from frontend/public/i18n/pt-BR/help.json rename to frontend/src/i18n/locales/pt-BR/help.json diff --git a/frontend/public/i18n/pt-BR/home.json b/frontend/src/i18n/locales/pt-BR/home.json similarity index 100% rename from frontend/public/i18n/pt-BR/home.json rename to frontend/src/i18n/locales/pt-BR/home.json diff --git a/frontend/public/i18n/pt-BR/privacy.json b/frontend/src/i18n/locales/pt-BR/privacy.json similarity index 100% rename from frontend/public/i18n/pt-BR/privacy.json rename to frontend/src/i18n/locales/pt-BR/privacy.json diff --git a/frontend/public/i18n/pt_PT/common.json b/frontend/src/i18n/locales/pt_PT/common.json similarity index 100% rename from frontend/public/i18n/pt_PT/common.json rename to frontend/src/i18n/locales/pt_PT/common.json diff --git a/frontend/public/i18n/pt_PT/event.json b/frontend/src/i18n/locales/pt_PT/event.json similarity index 100% rename from frontend/public/i18n/pt_PT/event.json rename to frontend/src/i18n/locales/pt_PT/event.json diff --git a/frontend/public/i18n/pt_PT/help.json b/frontend/src/i18n/locales/pt_PT/help.json similarity index 100% rename from frontend/public/i18n/pt_PT/help.json rename to frontend/src/i18n/locales/pt_PT/help.json diff --git a/frontend/public/i18n/pt_PT/home.json b/frontend/src/i18n/locales/pt_PT/home.json similarity index 100% rename from frontend/public/i18n/pt_PT/home.json rename to frontend/src/i18n/locales/pt_PT/home.json diff --git a/frontend/public/i18n/pt_PT/privacy.json b/frontend/src/i18n/locales/pt_PT/privacy.json similarity index 100% rename from frontend/public/i18n/pt_PT/privacy.json rename to frontend/src/i18n/locales/pt_PT/privacy.json diff --git a/frontend/public/i18n/ru/common.json b/frontend/src/i18n/locales/ru/common.json similarity index 100% rename from frontend/public/i18n/ru/common.json rename to frontend/src/i18n/locales/ru/common.json diff --git a/frontend/public/i18n/ru/event.json b/frontend/src/i18n/locales/ru/event.json similarity index 100% rename from frontend/public/i18n/ru/event.json rename to frontend/src/i18n/locales/ru/event.json diff --git a/frontend/public/i18n/ru/help.json b/frontend/src/i18n/locales/ru/help.json similarity index 100% rename from frontend/public/i18n/ru/help.json rename to frontend/src/i18n/locales/ru/help.json diff --git a/frontend/public/i18n/ru/home.json b/frontend/src/i18n/locales/ru/home.json similarity index 100% rename from frontend/public/i18n/ru/home.json rename to frontend/src/i18n/locales/ru/home.json diff --git a/frontend/public/i18n/ru/privacy.json b/frontend/src/i18n/locales/ru/privacy.json similarity index 100% rename from frontend/public/i18n/ru/privacy.json rename to frontend/src/i18n/locales/ru/privacy.json diff --git a/frontend/src/i18n/locales.js b/frontend/src/i18n/options.ts similarity index 66% rename from frontend/src/i18n/locales.js rename to frontend/src/i18n/options.ts index 651e7e6..99bd319 100644 --- a/frontend/src/i18n/locales.js +++ b/frontend/src/i18n/options.ts @@ -1,10 +1,39 @@ -const locales = { - 'de': { // German - name: 'Deutsch', - import: () => import('dayjs/locale/de'), - weekStart: 1, - timeFormat: '24h', +import { InitOptions } from 'i18next' + +export const fallbackLng = 'en' +export const defaultNS = 'common' +export const cookieName = 'i18next' +export const languages = [ + fallbackLng, + 'en-GB', 'de', 'es', 'fr', 'hi', 'id', 'ja', 'ko', 'pl', 'pt-BR', 'ru', +] as const + +export const getOptions = (lng = fallbackLng, ns: InitOptions['ns'] = defaultNS): InitOptions => ({ + lng, + fallbackLng, + supportedLngs: languages, + ns, + fallbackNS: defaultNS, + defaultNS, + debug: process.env.NODE_ENV !== 'production', + interpolation: { + escapeValue: false, }, +}) + +interface LanguageDetails { + /** Name of the language in it's own language */ + name: string + /** 0: Sunday, 1: Monday */ + weekStart: 0 | 1 + timeFormat: '12h' | '24h' + /** TODO: document */ + separator?: string + /** Day.js locale import */ + import: () => unknown +} + +export const languageDetails: Record = { 'en': { // English (US) name: 'English (US)', import: () => import('dayjs/locale/en'), @@ -17,6 +46,12 @@ const locales = { weekStart: 1, timeFormat: '12h', }, + 'de': { // German + name: 'Deutsch', + import: () => import('dayjs/locale/de'), + weekStart: 1, + timeFormat: '24h', + }, 'es': { // Spanish name: 'Español', import: () => import('dayjs/locale/es'), @@ -79,5 +114,3 @@ const locales = { // timeFormat: '12h', // }, } - -export default locales diff --git a/frontend/src/i18n/server.ts b/frontend/src/i18n/server.ts new file mode 100644 index 0000000..1d43aad --- /dev/null +++ b/frontend/src/i18n/server.ts @@ -0,0 +1,36 @@ +import { UseTranslationOptions } from 'react-i18next' +import { cookies, headers } from 'next/headers' +import acceptLanguage from 'accept-language' +import { createInstance } from 'i18next' +import resourcesToBackend from 'i18next-resources-to-backend' + +import { cookieName, fallbackLng, getOptions, languages } from './options' + +type Mutable = { -readonly [K in keyof T]: Mutable } + +acceptLanguage.languages(languages as Mutable) + +const initI18next = async (language: string, ns: string | string []) => { + const i18nInstance = createInstance() + await i18nInstance + .use(resourcesToBackend((language: string, namespace: string) => + import(`./locales/${language}/${namespace}.json`) + )) + .init({ + ...getOptions(language, ns), + debug: false, + }) + return i18nInstance +} + +export const useTranslation = async (ns: string | string[], options: UseTranslationOptions = {}) => { + const language = cookies().get(cookieName)?.value + ?? acceptLanguage.get(headers().get('Accept-Language')) + ?? fallbackLng + + const i18nextInstance = await initI18next(language, ns) + return { + t: i18nextInstance.getFixedT(language, Array.isArray(ns) ? ns[0] : ns, options.keyPrefix), + i18n: i18nextInstance + } +} diff --git a/frontend/src/index.jsx b/frontend/src/index.jsx deleted file mode 100644 index daef4a1..0000000 --- a/frontend/src/index.jsx +++ /dev/null @@ -1,24 +0,0 @@ -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( - - - - - -) diff --git a/frontend/src/pages/Create/Create.jsx b/frontend/src/pages-old/Create/Create.jsx similarity index 100% rename from frontend/src/pages/Create/Create.jsx rename to frontend/src/pages-old/Create/Create.jsx diff --git a/frontend/src/pages/Create/Create.styles.js b/frontend/src/pages-old/Create/Create.styles.js similarity index 100% rename from frontend/src/pages/Create/Create.styles.js rename to frontend/src/pages-old/Create/Create.styles.js diff --git a/frontend/src/pages/Event/Event.jsx b/frontend/src/pages-old/Event/Event.jsx similarity index 100% rename from frontend/src/pages/Event/Event.jsx rename to frontend/src/pages-old/Event/Event.jsx diff --git a/frontend/src/pages/Event/Event.styles.js b/frontend/src/pages-old/Event/Event.styles.js similarity index 100% rename from frontend/src/pages/Event/Event.styles.js rename to frontend/src/pages-old/Event/Event.styles.js diff --git a/frontend/src/pages/Help/Help.jsx b/frontend/src/pages-old/Help/Help.jsx similarity index 100% rename from frontend/src/pages/Help/Help.jsx rename to frontend/src/pages-old/Help/Help.jsx diff --git a/frontend/src/pages/Help/Help.styles.js b/frontend/src/pages-old/Help/Help.styles.js similarity index 100% rename from frontend/src/pages/Help/Help.styles.js rename to frontend/src/pages-old/Help/Help.styles.js diff --git a/frontend/src/pages/Home/Home.jsx b/frontend/src/pages-old/Home/Home.jsx similarity index 98% rename from frontend/src/pages/Home/Home.jsx rename to frontend/src/pages-old/Home/Home.jsx index 6e309bb..86051f2 100644 --- a/frontend/src/pages/Home/Home.jsx +++ b/frontend/src/pages-old/Home/Home.jsx @@ -1,52 +1,49 @@ -import { useEffect, useState } from 'react' -import { useNavigate, Link } from 'react-router-dom' -import { useForm } from 'react-hook-form' -import { useTranslation, Trans } from 'react-i18next' - import dayjs from 'dayjs' -import utc from 'dayjs/plugin/utc' -import timezone from 'dayjs/plugin/timezone' import customParseFormat from 'dayjs/plugin/customParseFormat' +import timezone from 'dayjs/plugin/timezone' +import utc from 'dayjs/plugin/utc' +import { useEffect, useState } from 'react' +import { useForm } from 'react-hook-form' +import { Trans,useTranslation } from 'react-i18next' +import { Link,useNavigate } from 'react-router-dom' import { - TextField, - CalendarField, - TimeRangeField, - SelectField, Button, + CalendarField, Center, Error, Footer, Recents, + SelectField, + TextField, + TimeRangeField, } from '/src/components' +import logo from '/src/res/logo.svg' +import timezones from '/src/res/timezones.json' +import video_thumb from '/src/res/video_thumb.jpg' +import api from '/src/services' +import { useTWAStore } from '/src/stores' +import { detect_browser } from '/src/utils' import { - StyledMain, - CreateForm, - TitleSmall, - TitleLarge, - Logo, - Links, AboutSection, - P, - Stats, - Stat, - StatNumber, - StatLabel, - OfflineMessage, ButtonArea, - VideoWrapper, + CreateForm, + Links, + Logo, + OfflineMessage, + P, + Stat, + StatLabel, + StatNumber, + Stats, + StyledMain, + TitleLarge, + TitleSmall, VideoLink, + VideoWrapper, } from './Home.styles' -import api from '/src/services' -import { detect_browser } from '/src/utils' -import { useTWAStore } from '/src/stores' - -import logo from '/src/res/logo.svg' -import video_thumb from '/src/res/video_thumb.jpg' -import timezones from '/src/res/timezones.json' - dayjs.extend(utc) dayjs.extend(timezone) dayjs.extend(customParseFormat) @@ -255,7 +252,7 @@ const Home = ({ offline }) => { )} - {isTWA !== true && ( + {!document.referrer.includes('android-app://fit.crab') && ( {['chrome', 'firefox', 'safari'].includes(browser) && ( + + + Form here + + + +
+ +

{t('about.name')}

+ + {/* @ts-expect-error Async Server Component */} + + +

Crab Fit helps you fit your event around everyone's schedules. Simply create an event above and send the link to everyone that is participating. Results update live and you will be able to see a heat-map of when everyone is free.
Learn more about how to Crab Fit.

+ +
+
+ + {/* @ts-expect-error Async Server Component */}