From 2adecd13f727c86739c3f0b6f9b338639dabb040 Mon Sep 17 00:00:00 2001 From: Ben Grant Date: Fri, 19 May 2023 23:59:44 +1000 Subject: [PATCH] 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) && (