Add translate dialog for users with other languages
This commit is contained in:
parent
00c7ae16f2
commit
d7971d27df
|
|
@ -1,62 +0,0 @@
|
||||||
import { useState, useEffect, useCallback, Suspense } from 'react'
|
|
||||||
import { Route, Routes } from 'react-router-dom'
|
|
||||||
|
|
||||||
import * as Pages from '/src/pages'
|
|
||||||
import { Settings, Loading, Egg, TranslateDialog } from '/src/components'
|
|
||||||
|
|
||||||
import { useSettingsStore, useTranslateStore } from '/src/stores'
|
|
||||||
|
|
||||||
const EGG_PATTERN = ['ArrowUp', 'ArrowUp', 'ArrowDown', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'ArrowLeft', 'ArrowRight', 'b', 'a']
|
|
||||||
|
|
||||||
const App = () => {
|
|
||||||
const [eggCount, setEggCount] = useState(0)
|
|
||||||
const [eggVisible, setEggVisible] = useState(false)
|
|
||||||
const [eggKey, setEggKey] = useState(0)
|
|
||||||
|
|
||||||
const languageSupported = useTranslateStore(state => state.navigatorSupported)
|
|
||||||
const translateDialogDismissed = useTranslateStore(state => state.translateDialogDismissed)
|
|
||||||
|
|
||||||
const eggHandler = useCallback(e => {
|
|
||||||
if (EGG_PATTERN.indexOf(e.key) < 0 || e.key !== EGG_PATTERN[eggCount]) return setEggCount(0)
|
|
||||||
setEggCount(eggCount+1)
|
|
||||||
if (EGG_PATTERN.length === eggCount+1) {
|
|
||||||
setEggKey(eggKey+1)
|
|
||||||
setEggCount(0)
|
|
||||||
setEggVisible(true)
|
|
||||||
}
|
|
||||||
}, [eggCount, eggKey])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
document.addEventListener('keyup', eggHandler, false)
|
|
||||||
return () => document.removeEventListener('keyup', eggHandler, false)
|
|
||||||
}, [eggHandler])
|
|
||||||
|
|
||||||
// Use user theme preference
|
|
||||||
const theme = useSettingsStore(state => state.theme)
|
|
||||||
useEffect(() => {
|
|
||||||
document.body.classList.toggle('light', theme === 'Light')
|
|
||||||
document.body.classList.toggle('dark', theme === 'Dark')
|
|
||||||
}, [theme])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{!languageSupported && !translateDialogDismissed && <TranslateDialog />}
|
|
||||||
|
|
||||||
<Suspense fallback={<Loading />}>
|
|
||||||
<Settings />
|
|
||||||
|
|
||||||
<Routes>
|
|
||||||
<Route path="/" element={<Pages.Home />} />
|
|
||||||
<Route path="/how-to" element={<Pages.Help />} />
|
|
||||||
<Route path="/privacy" element={<Pages.Privacy />} />
|
|
||||||
<Route path="/create" element={<Pages.Create />} />
|
|
||||||
<Route path="/:id" element={<Pages.Event />} />
|
|
||||||
</Routes>
|
|
||||||
</Suspense>
|
|
||||||
|
|
||||||
{eggVisible && <Egg eggKey={eggKey} onClose={() => setEggVisible(false)} />}
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default App
|
|
||||||
|
|
@ -2,6 +2,7 @@ import { Metadata } from 'next'
|
||||||
|
|
||||||
import Egg from '/src/components/Egg/Egg'
|
import Egg from '/src/components/Egg/Egg'
|
||||||
import Settings from '/src/components/Settings/Settings'
|
import Settings from '/src/components/Settings/Settings'
|
||||||
|
import TranslateDialog from '/src/components/TranslateDialog/TranslateDialog'
|
||||||
import { fallbackLng } from '/src/i18n/options'
|
import { fallbackLng } from '/src/i18n/options'
|
||||||
import { useTranslation } from '/src/i18n/server'
|
import { useTranslation } from '/src/i18n/server'
|
||||||
|
|
||||||
|
|
@ -33,6 +34,7 @@ const RootLayout = async ({ children }: { children: React.ReactNode }) => {
|
||||||
<Settings />
|
<Settings />
|
||||||
{children}
|
{children}
|
||||||
<Egg />
|
<Egg />
|
||||||
|
<TranslateDialog />
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,11 +5,15 @@
|
||||||
outline: none;
|
outline: none;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
display: flex;
|
display: none;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
overflow: visible;
|
overflow: visible;
|
||||||
|
|
||||||
|
&[open] {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
&::backdrop {
|
&::backdrop {
|
||||||
background: rgba(0,0,0,.6);
|
background: rgba(0,0,0,.6);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,7 @@
|
||||||
import { styled } from 'goober'
|
.popup {
|
||||||
|
|
||||||
export const Wrapper = styled('div')`
|
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 20px;
|
bottom: 20px;
|
||||||
left: 20px;
|
right: 20px;
|
||||||
background-color: var(--background);
|
background-color: var(--background);
|
||||||
border: 1px solid var(--surface);
|
border: 1px solid var(--surface);
|
||||||
z-index: 900;
|
z-index: 900;
|
||||||
|
|
@ -28,9 +26,9 @@ export const Wrapper = styled('div')`
|
||||||
@media (max-width: 400px) {
|
@media (max-width: 400px) {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
`
|
}
|
||||||
|
|
||||||
export const ButtonWrapper = styled('div')`
|
.buttons {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
|
|
@ -44,4 +42,4 @@ export const ButtonWrapper = styled('div')`
|
||||||
margin: 20px 0 0;
|
margin: 20px 0 0;
|
||||||
white-space: normal;
|
white-space: normal;
|
||||||
}
|
}
|
||||||
`
|
}
|
||||||
|
|
@ -1,32 +1,52 @@
|
||||||
import { Button } from '/src/components'
|
'use client'
|
||||||
|
|
||||||
import { useTranslateStore } from '/src/stores'
|
import { useEffect, useState } from 'react'
|
||||||
|
import { create } from 'zustand'
|
||||||
|
import { persist } from 'zustand/middleware'
|
||||||
|
|
||||||
import {
|
import Button from '/src/components/Button/Button'
|
||||||
Wrapper,
|
import { languages } from '/src/i18n/options'
|
||||||
ButtonWrapper,
|
import { useStore } from '/src/stores'
|
||||||
} from './TranslateDialog.styles'
|
|
||||||
|
import styles from './TranslateDialog.module.scss'
|
||||||
|
|
||||||
|
interface TranslateStore {
|
||||||
|
translateDialogDismissed: boolean
|
||||||
|
dismissDialog: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const useTranslateStore = create<TranslateStore>()(persist(
|
||||||
|
set => ({
|
||||||
|
translateDialogDismissed: false,
|
||||||
|
dismissDialog: () => set({ translateDialogDismissed: true }),
|
||||||
|
}),
|
||||||
|
{ name: 'crabfit-translate' },
|
||||||
|
))
|
||||||
|
|
||||||
const TranslateDialog = () => {
|
const TranslateDialog = () => {
|
||||||
const navigatorLang = useTranslateStore(state => state.navigatorLang)
|
const [isSupported, setIsSupported] = useState(true)
|
||||||
const setDialogDismissed = useTranslateStore(state => state.setDialogDismissed)
|
const store = useStore(useTranslateStore, state => state)
|
||||||
|
|
||||||
return (
|
// Check if current language has translations
|
||||||
<Wrapper>
|
useEffect(() => {
|
||||||
<div>
|
setIsSupported((languages as readonly string[]).includes(navigator.language.substring(0, 2)))
|
||||||
<h2>Translate Crab Fit</h2>
|
}, [])
|
||||||
<p>Crab Fit hasn't been translated to your language yet.</p>
|
|
||||||
</div>
|
return (store?.translateDialogDismissed === false && !isSupported) ? <div className={styles.popup}>
|
||||||
<ButtonWrapper>
|
<div>
|
||||||
<Button
|
<h2>Translate Crab Fit</h2>
|
||||||
target="_blank"
|
<p>Crab Fit hasn't been translated to your language yet.</p>
|
||||||
rel="noreferrer noopener"
|
</div>
|
||||||
href={`https://docs.google.com/forms/d/e/1FAIpQLSd5bcs8LTP_8Ydrh2e4iMlZft5x81qSfAxekuuQET27A2mBhA/viewform?usp=pp_url&entry.1530835706=__other_option__&entry.1530835706.other_option_response=${encodeURIComponent(navigatorLang)}`}
|
|
||||||
>Help translate!</Button>
|
<div className={styles.buttons}>
|
||||||
<Button secondary onClick={() => setDialogDismissed(true)}>Close</Button>
|
<Button
|
||||||
</ButtonWrapper>
|
target="_blank"
|
||||||
</Wrapper>
|
rel="noreferrer noopener"
|
||||||
)
|
href={`https://docs.google.com/forms/d/e/1FAIpQLSd5bcs8LTP_8Ydrh2e4iMlZft5x81qSfAxekuuQET27A2mBhA/viewform?usp=pp_url&entry.1530835706=__other_option__&entry.1530835706.other_option_response=${encodeURIComponent(navigator.language)}`}
|
||||||
|
>Help translate!</Button>
|
||||||
|
<Button isSecondary onClick={() => store.dismissDialog()}>Close</Button>
|
||||||
|
</div>
|
||||||
|
</div> : null
|
||||||
}
|
}
|
||||||
|
|
||||||
export default TranslateDialog
|
export default TranslateDialog
|
||||||
|
|
|
||||||
|
|
@ -1,13 +0,0 @@
|
||||||
import { create } from 'zustand'
|
|
||||||
|
|
||||||
interface LocaleUpdateStore {
|
|
||||||
locale: string
|
|
||||||
setLocale: (locale: string) => void
|
|
||||||
}
|
|
||||||
|
|
||||||
const useLocaleUpdateStore = create<LocaleUpdateStore>()(set => ({
|
|
||||||
locale: 'en',
|
|
||||||
setLocale: locale => set({ locale }),
|
|
||||||
}))
|
|
||||||
|
|
||||||
export default useLocaleUpdateStore
|
|
||||||
|
|
@ -1,28 +0,0 @@
|
||||||
import { create } from 'zustand'
|
|
||||||
import { persist } from 'zustand/middleware'
|
|
||||||
|
|
||||||
import locales from '/src/i18n/locales'
|
|
||||||
|
|
||||||
interface TranslateStore {
|
|
||||||
navigatorLang: string
|
|
||||||
navigatorSupported: boolean
|
|
||||||
translateDialogDismissed: boolean
|
|
||||||
|
|
||||||
setDialogDismissed: (isDismissed: boolean) => void
|
|
||||||
}
|
|
||||||
|
|
||||||
const useTranslateStore = create<TranslateStore>()(persist(
|
|
||||||
set => ({
|
|
||||||
navigatorLang: navigator.language,
|
|
||||||
navigatorSupported: Object.keys(locales).includes(navigator.language.substring(0, 2)),
|
|
||||||
translateDialogDismissed: false,
|
|
||||||
|
|
||||||
setDialogDismissed: isDismissed => set({ translateDialogDismissed: isDismissed }),
|
|
||||||
}),
|
|
||||||
{
|
|
||||||
name: 'crabfit-translate',
|
|
||||||
partialize: state => ({ translateDialogDismissed: state.translateDialogDismissed }),
|
|
||||||
},
|
|
||||||
))
|
|
||||||
|
|
||||||
export default useTranslateStore
|
|
||||||
|
|
@ -1,14 +0,0 @@
|
||||||
import { create } from 'zustand'
|
|
||||||
|
|
||||||
interface TWAStore {
|
|
||||||
/** Is the site running in a trusted web activity? */
|
|
||||||
isTWA: boolean | undefined
|
|
||||||
setIsTWA: (isTWA: boolean | undefined) => void
|
|
||||||
}
|
|
||||||
|
|
||||||
const useTWAStore = create<TWAStore>()(set => ({
|
|
||||||
isTWA: undefined,
|
|
||||||
setIsTWA: isTWA => set({ isTWA }),
|
|
||||||
}))
|
|
||||||
|
|
||||||
export default useTWAStore
|
|
||||||
Loading…
Reference in a new issue