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
+ ?
+ :
+}
+
+export default Button
diff --git a/frontend/src/components/Donate/Donate.jsx b/frontend/src/components/Donate/Donate.jsx
deleted file mode 100644
index a40f9d6..0000000
--- a/frontend/src/components/Donate/Donate.jsx
+++ /dev/null
@@ -1,159 +0,0 @@
-import { useState, useEffect, useRef } from 'react'
-import { useTranslation } from 'react-i18next'
-
-import { Button } from '/src/components'
-import { useTWAStore } from '/src/stores'
-
-import {
- Wrapper,
- Options,
-} from './Donate.styles'
-
-import paypal_logo from '/src/res/paypal.svg'
-
-const PAYMENT_METHOD = 'https://play.google.com/billing'
-const SKU = 'crab_donation'
-
-const Donate = () => {
- const store = useTWAStore()
- const { t } = useTranslation('common')
-
- const firstLinkRef = useRef()
- const modalRef = useRef()
- const [isOpen, _setIsOpen] = useState(false)
- const [closed, setClosed] = useState(false)
-
- const setIsOpen = open => {
- _setIsOpen(open)
-
- if (open) {
- window.setTimeout(() => firstLinkRef.current.focus(), 150)
- }
- }
-
- const linkPressed = () => {
- setIsOpen(false)
- gtag('event', 'donate', { 'event_category': 'donate' })
- }
-
- useEffect(() => {
- if (store.TWA === undefined) {
- store.setTWA(document.referrer.includes('android-app://fit.crab'))
- }
- }, [store])
-
- const acknowledge = async (token, type='repeatable', onComplete = () => {}) => {
- try {
- const service = await window.getDigitalGoodsService(PAYMENT_METHOD)
- await service.acknowledge(token, type)
- if ('acknowledge' in service) {
- // DGAPI 1.0
- service.acknowledge(token, type)
- } else {
- // DGAPI 2.0
- service.consume(token)
- }
- onComplete()
- } catch (error) {
- console.error(error)
- }
- }
-
- const purchase = () => {
- if (!window.PaymentRequest) return false
- if (!window.getDigitalGoodsService) return false
-
- const supportedInstruments = [{
- supportedMethods: PAYMENT_METHOD,
- data: {
- sku: SKU
- }
- }]
-
- const details = {
- total: {
- label: 'Total',
- amount: { currency: 'AUD', value: '0' }
- },
- }
-
- const request = new PaymentRequest(supportedInstruments, details)
-
- request.show()
- .then(response => {
- response
- .complete('success')
- .then(() => {
- console.log(`Payment done: ${JSON.stringify(response, undefined, 2)}`)
- if (response.details && response.details.token) {
- const token = response.details.token
- console.log(`Read Token: ${token.substring(0, 6)}...`)
- alert(t('donate.messages.success'))
- acknowledge(token)
- }
- })
- .catch(e => {
- console.error(e.message)
- alert(t('donate.messages.error'))
- })
- })
- .catch(e => {
- console.error(e)
- alert(t('donate.messages.error'))
- })
- }
-
- return (
-
-
-
- {
- if (modalRef.current?.contains(e.relatedTarget)) return
- setIsOpen(false)
- if (e.relatedTarget && e.relatedTarget.id === 'donate_button') {
- setClosed(true)
- }
- }}
- >
-
- {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 (
-
- {t('donate.info')}
-
-
- )
-}
-
-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
+}
+
+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) && (