feat: add i18next and locales
This commit is contained in:
parent
db5f1015ed
commit
a43bbe719a
120
package-lock.json
generated
120
package-lock.json
generated
@ -8,9 +8,11 @@
|
||||
"name": "aurawebapp",
|
||||
"version": "0.0.0",
|
||||
"dependencies": {
|
||||
"i18next": "^22.4.15",
|
||||
"react": "^18.2.0",
|
||||
"react-circular-progressbar": "^2.1.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-i18next": "^12.2.2",
|
||||
"react-router-dom": "^6.11.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
@ -339,6 +341,17 @@
|
||||
"@babel/core": "^7.0.0-0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/runtime": {
|
||||
"version": "7.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.21.5.tgz",
|
||||
"integrity": "sha512-8jI69toZqqcsnqGGqwGS4Qb1VwLOEp4hz+CXPywcvjs60u3B4Pom/U/7rm4W8tMOYEB+E9wgD0mW1l3r8qlI9Q==",
|
||||
"dependencies": {
|
||||
"regenerator-runtime": "^0.13.11"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/template": {
|
||||
"version": "7.20.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.20.7.tgz",
|
||||
@ -2066,6 +2079,36 @@
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/html-parse-stringify": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz",
|
||||
"integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==",
|
||||
"dependencies": {
|
||||
"void-elements": "3.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/i18next": {
|
||||
"version": "22.4.15",
|
||||
"resolved": "https://registry.npmjs.org/i18next/-/i18next-22.4.15.tgz",
|
||||
"integrity": "sha512-yYudtbFrrmWKLEhl6jvKUYyYunj4bTBCe2qIUYAxbXoPusY7YmdwPvOE6fx6UIfWvmlbCWDItr7wIs8KEBZ5Zg==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://locize.com"
|
||||
},
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://locize.com/i18next.html"
|
||||
},
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project"
|
||||
}
|
||||
],
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.20.6"
|
||||
}
|
||||
},
|
||||
"node_modules/ignore": {
|
||||
"version": "5.2.4",
|
||||
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz",
|
||||
@ -2573,6 +2616,27 @@
|
||||
"react": "^18.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-i18next": {
|
||||
"version": "12.2.2",
|
||||
"resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-12.2.2.tgz",
|
||||
"integrity": "sha512-KBB6buBmVKXUWNxXHdnthp+38gPyBT46hJCAIQ8rX19NFL/m2ahte2KARfIDf2tMnSAL7wwck6eDOd/9zn6aFg==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.20.6",
|
||||
"html-parse-stringify": "^3.0.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"i18next": ">= 19.0.0",
|
||||
"react": ">= 16.8.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"react-dom": {
|
||||
"optional": true
|
||||
},
|
||||
"react-native": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/react-refresh": {
|
||||
"version": "0.14.0",
|
||||
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.0.tgz",
|
||||
@ -2612,6 +2676,11 @@
|
||||
"react-dom": ">=16.8"
|
||||
}
|
||||
},
|
||||
"node_modules/regenerator-runtime": {
|
||||
"version": "0.13.11",
|
||||
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz",
|
||||
"integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg=="
|
||||
},
|
||||
"node_modules/resolve-from": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
|
||||
@ -2985,6 +3054,14 @@
|
||||
"node": ">=14.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/void-elements": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz",
|
||||
"integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/which": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||
@ -3262,6 +3339,14 @@
|
||||
"@babel/helper-plugin-utils": "^7.19.0"
|
||||
}
|
||||
},
|
||||
"@babel/runtime": {
|
||||
"version": "7.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.21.5.tgz",
|
||||
"integrity": "sha512-8jI69toZqqcsnqGGqwGS4Qb1VwLOEp4hz+CXPywcvjs60u3B4Pom/U/7rm4W8tMOYEB+E9wgD0mW1l3r8qlI9Q==",
|
||||
"requires": {
|
||||
"regenerator-runtime": "^0.13.11"
|
||||
}
|
||||
},
|
||||
"@babel/template": {
|
||||
"version": "7.20.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.20.7.tgz",
|
||||
@ -4411,6 +4496,22 @@
|
||||
"integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
|
||||
"dev": true
|
||||
},
|
||||
"html-parse-stringify": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz",
|
||||
"integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==",
|
||||
"requires": {
|
||||
"void-elements": "3.1.0"
|
||||
}
|
||||
},
|
||||
"i18next": {
|
||||
"version": "22.4.15",
|
||||
"resolved": "https://registry.npmjs.org/i18next/-/i18next-22.4.15.tgz",
|
||||
"integrity": "sha512-yYudtbFrrmWKLEhl6jvKUYyYunj4bTBCe2qIUYAxbXoPusY7YmdwPvOE6fx6UIfWvmlbCWDItr7wIs8KEBZ5Zg==",
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.20.6"
|
||||
}
|
||||
},
|
||||
"ignore": {
|
||||
"version": "5.2.4",
|
||||
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz",
|
||||
@ -4761,6 +4862,15 @@
|
||||
"scheduler": "^0.23.0"
|
||||
}
|
||||
},
|
||||
"react-i18next": {
|
||||
"version": "12.2.2",
|
||||
"resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-12.2.2.tgz",
|
||||
"integrity": "sha512-KBB6buBmVKXUWNxXHdnthp+38gPyBT46hJCAIQ8rX19NFL/m2ahte2KARfIDf2tMnSAL7wwck6eDOd/9zn6aFg==",
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.20.6",
|
||||
"html-parse-stringify": "^3.0.1"
|
||||
}
|
||||
},
|
||||
"react-refresh": {
|
||||
"version": "0.14.0",
|
||||
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.0.tgz",
|
||||
@ -4784,6 +4894,11 @@
|
||||
"react-router": "6.11.0"
|
||||
}
|
||||
},
|
||||
"regenerator-runtime": {
|
||||
"version": "0.13.11",
|
||||
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz",
|
||||
"integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg=="
|
||||
},
|
||||
"resolve-from": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
|
||||
@ -5005,6 +5120,11 @@
|
||||
"fast-glob": "^3.2.7"
|
||||
}
|
||||
},
|
||||
"void-elements": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz",
|
||||
"integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w=="
|
||||
},
|
||||
"which": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||
|
||||
@ -10,9 +10,11 @@
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"i18next": "^22.4.15",
|
||||
"react": "^18.2.0",
|
||||
"react-circular-progressbar": "^2.1.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-i18next": "^12.2.2",
|
||||
"react-router-dom": "^6.11.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import routes from '../../routes'
|
||||
import Policy from '../Policy'
|
||||
import Purposes from '../Purposes'
|
||||
@ -9,7 +10,8 @@ import MainButton from '../MainButton'
|
||||
import './styles.css'
|
||||
|
||||
function BirthdayPage(): JSX.Element {
|
||||
const navigate = useNavigate();
|
||||
const { t } = useTranslation()
|
||||
const navigate = useNavigate()
|
||||
const [birthdate, setBirthdate] = useState('')
|
||||
const [isDisabled, setIsDisabled] = useState(true)
|
||||
const links = [
|
||||
@ -29,19 +31,17 @@ function BirthdayPage(): JSX.Element {
|
||||
|
||||
return (
|
||||
<section className='page'>
|
||||
<Title variant='h3' className='mt-24'>Let's start!</Title>
|
||||
<Title variant='h2'>What's your date of birth?</Title>
|
||||
<Title variant='h3' className='mt-24'>{t('letsStart')}</Title>
|
||||
<Title variant='h2'>{t('dateOfBirth')}</Title>
|
||||
<DatePicker
|
||||
name='birthdate'
|
||||
value={birthdate}
|
||||
onValid={handleValid}
|
||||
onInvalid={() => setIsDisabled(true)}
|
||||
/>
|
||||
<MainButton label='Next' onClick={handleNext} disabled={isDisabled}/>
|
||||
<MainButton label={t('next')} onClick={handleNext} disabled={isDisabled}/>
|
||||
<footer className='footer'>
|
||||
<Policy links={links}>
|
||||
By continuing, you agree to our EULA and Privacy Notice. Have a question? Reach our support team here
|
||||
</Policy>
|
||||
<Policy links={links}>{t('privacyText')}</Policy>
|
||||
<Purposes />
|
||||
</footer>
|
||||
</section>
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { useState, useEffect } from "react"
|
||||
import { useNavigate } from "react-router-dom"
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Title from "../Title"
|
||||
import MainButton from "../MainButton"
|
||||
import { TimePicker } from "../DateTimePicker"
|
||||
@ -7,6 +8,7 @@ import routes from "../../routes"
|
||||
import './styles.css'
|
||||
|
||||
function BirthtimePage(): JSX.Element {
|
||||
const { t } = useTranslation()
|
||||
const navigate = useNavigate();
|
||||
const [birthtime, setBirhtime] = useState('')
|
||||
const [isDisabled, setIsDisabled] = useState(true)
|
||||
@ -23,12 +25,10 @@ function BirthtimePage(): JSX.Element {
|
||||
|
||||
return (
|
||||
<section className='page'>
|
||||
<Title variant="h2" className="mt-24">What time were you born?</Title>
|
||||
<p className="description">
|
||||
We use NASA data to determine the exact position of the planets in the sky at the time of your birth to create wallpapers that are just right for you.
|
||||
</p>
|
||||
<Title variant="h2" className="mt-24">{t('bornTimeQuestion')}</Title>
|
||||
<p className="description">{t('nasaDataUsing')}</p>
|
||||
<TimePicker onChange={(value: string) => setBirhtime(value)}/>
|
||||
<MainButton label='Next' onClick={handleNext} disabled={isDisabled}/>
|
||||
<MainButton label={t('next')} onClick={handleNext} disabled={isDisabled}/>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
@ -1,10 +1,12 @@
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import './styles.css'
|
||||
|
||||
function CallToAction(): JSX.Element {
|
||||
const { t } = useTranslation()
|
||||
return (
|
||||
<div className='call-to-action mb-24'>
|
||||
<h1>Start your 7-day trial</h1>
|
||||
<p>No pressure. Cancel anytime.</p>
|
||||
<h1>{t('ctaTitle')}</h1>
|
||||
<p>{t('ctaSubtitle')}</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import './styles.css'
|
||||
|
||||
type CountdownProps = {
|
||||
@ -6,6 +7,7 @@ type CountdownProps = {
|
||||
}
|
||||
|
||||
function Countdown({ start }: CountdownProps): JSX.Element {
|
||||
const { t } = useTranslation()
|
||||
const [time, setTime] = useState(start * 60 - 1)
|
||||
const formatTime = (seconds: number) => {
|
||||
const minutes = Math.floor(seconds / 60)
|
||||
@ -21,7 +23,7 @@ function Countdown({ start }: CountdownProps): JSX.Element {
|
||||
|
||||
return (
|
||||
<div className="countdown mb-24">
|
||||
<p>Reserved for {formatTime(time)}</p>
|
||||
<p>{t('reservedFor')}{formatTime(time)}</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import { useState } from "react"
|
||||
import { useNavigate } from "react-router-dom"
|
||||
import { CircularProgressbar, buildStyles } from 'react-circular-progressbar'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import ProcessFlow from "./ProcessFlow"
|
||||
import Title from "../Title"
|
||||
import routes from "../../routes"
|
||||
@ -9,12 +10,13 @@ import './styles.css'
|
||||
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms))
|
||||
|
||||
function CreateProfilePage(): JSX.Element {
|
||||
const { t } = useTranslation()
|
||||
const navigate = useNavigate()
|
||||
const [progress, setProgress] = useState(0)
|
||||
const processItems = [
|
||||
{ task: () => sleep(3300).then(() => setProgress(35)), label: 'Zodiac data analysis' },
|
||||
{ task: () => sleep(2550).then(() => setProgress(61)), label: 'Drawing Wallpapers' },
|
||||
{ task: () => sleep(3789).then(() => setProgress(98)), label: 'Preparing results' },
|
||||
{ task: () => sleep(3300).then(() => setProgress(35)), label: t('zodiacAnalysis') },
|
||||
{ task: () => sleep(2550).then(() => setProgress(61)), label: t('drawingWallpaper') },
|
||||
{ task: () => sleep(3789).then(() => setProgress(98)), label: t('preparingResults') },
|
||||
]
|
||||
const handleDone = () => Promise.resolve()
|
||||
.then(() => setProgress(100))
|
||||
@ -23,7 +25,7 @@ function CreateProfilePage(): JSX.Element {
|
||||
|
||||
return (
|
||||
<section className='page'>
|
||||
<Title variant="h2" className="mt-24">Creating your profile</Title>
|
||||
<Title variant="h2" className="mt-24">{t('creatingProfile')}</Title>
|
||||
<div className="progressbar">
|
||||
<CircularProgressbar
|
||||
value={progress}
|
||||
|
||||
@ -1,10 +1,12 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { FormField } from '../../types'
|
||||
import DateInput from './DateInput'
|
||||
import ErrorText from './ErrorText'
|
||||
import { stringify, getMaxYear, isNotTheDate, getDaysInMonth } from './utils'
|
||||
|
||||
export function DatePicker(props: FormField<string>): JSX.Element {
|
||||
const { t } = useTranslation()
|
||||
const { name, value, onValid, onInvalid } = props
|
||||
|
||||
const date = new Date(value)
|
||||
@ -35,7 +37,7 @@ export function DatePicker(props: FormField<string>): JSX.Element {
|
||||
value={year}
|
||||
max={getMaxYear()}
|
||||
maxLength={4}
|
||||
label='Year'
|
||||
label={t('year')}
|
||||
placeholder='YYYY'
|
||||
onChange={(year: number) => setYear(year)}
|
||||
/>
|
||||
@ -44,7 +46,7 @@ export function DatePicker(props: FormField<string>): JSX.Element {
|
||||
value={month}
|
||||
max={12}
|
||||
maxLength={2}
|
||||
label='Month'
|
||||
label={t('month')}
|
||||
placeholder='MM'
|
||||
onChange={(month: number) => setMonth(month)}
|
||||
/>
|
||||
@ -53,14 +55,14 @@ export function DatePicker(props: FormField<string>): JSX.Element {
|
||||
value={day}
|
||||
max={getDaysInMonth(year, month)}
|
||||
maxLength={2}
|
||||
label='Day'
|
||||
label={t('day')}
|
||||
placeholder='DD'
|
||||
onChange={(day: number) => setDay(day)}
|
||||
/>
|
||||
</div>
|
||||
<ErrorText
|
||||
isShown={hasError}
|
||||
message='Date not found. Please check your details and try again.'
|
||||
message={t('invalidDate')}
|
||||
/>
|
||||
</form>
|
||||
)
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Title from '../Title'
|
||||
import Policy from '../Policy'
|
||||
import EmailInput from '../EmailInput'
|
||||
@ -7,6 +8,7 @@ import MainButton from '../MainButton'
|
||||
import routes from '../../routes'
|
||||
|
||||
function EmailEnterPage(): JSX.Element {
|
||||
const { t } = useTranslation()
|
||||
const navigate = useNavigate()
|
||||
const [email, setEmail] = useState('')
|
||||
const [isDisabled, setIsDisabled] = useState(true)
|
||||
@ -26,21 +28,17 @@ function EmailEnterPage(): JSX.Element {
|
||||
|
||||
return (
|
||||
<section className='page'>
|
||||
<Title variant='h2' className='mt-24'>
|
||||
We will email you a copy of your wallpaper for easy access.
|
||||
</Title>
|
||||
<Title variant='h2' className='mt-24'>{t('weWillEmailYou')}</Title>
|
||||
<EmailInput
|
||||
name="email"
|
||||
value={email}
|
||||
placeholder="Your Email"
|
||||
placeholder={t('yourEmail')}
|
||||
onValid={handleValidEmail}
|
||||
onInvalid={() => setIsDisabled(true)}
|
||||
/>
|
||||
<p>We don't share any personal information.</p>
|
||||
<Policy links={links} sizing='medium'>
|
||||
By clicking "Continue" below, you agree to our EULA and Privacy Policy.
|
||||
</Policy>
|
||||
<MainButton label='Continue' onClick={handleClick} disabled={isDisabled} />
|
||||
<p>{t('weDontShare')}</p>
|
||||
<Policy links={links} sizing='medium'>{t('continueAgree')}</Policy>
|
||||
<MainButton label={t('continue')} onClick={handleClick} disabled={isDisabled} />
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
@ -1,11 +1,13 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useNavigate, useLocation } from 'react-router-dom'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import routes, { isNotEntrypoint } from '../../routes'
|
||||
import BackButton from '../BackButton'
|
||||
import iconUrl from './icon.png'
|
||||
import './styles.css'
|
||||
|
||||
function Header(): JSX.Element {
|
||||
const { t } = useTranslation()
|
||||
const navigate = useNavigate()
|
||||
const location = useLocation()
|
||||
const [initialPath, setInitialPath] = useState<string | null>(null);
|
||||
@ -33,7 +35,7 @@ function Header(): JSX.Element {
|
||||
<header className="header">
|
||||
{ showBackButton ? <BackButton className="pa" onClick={goBack} /> : null }
|
||||
<img src={iconUrl} alt="logo" width="40" height="40" />
|
||||
<span className="header__title">Aura</span>
|
||||
<span className="header__title">{t('appName')}</span>
|
||||
</header>
|
||||
)
|
||||
}
|
||||
|
||||
@ -1,8 +1,11 @@
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
function NotFoundPage() {
|
||||
const { t } = useTranslation()
|
||||
return (
|
||||
<>
|
||||
<h1>Oops!</h1>
|
||||
<p>Sorry, an unexpected error has occurred.</p>
|
||||
<h1>{t('oops')}</h1>
|
||||
<p>{t('unexpectedError')}</p>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Price, { Currency, Locale } from './Price'
|
||||
import './styles.css'
|
||||
|
||||
@ -14,6 +15,7 @@ type PaymentProps = {
|
||||
}
|
||||
|
||||
function Payment({ currency, locale, items }: PaymentProps): JSX.Element {
|
||||
const { t } = useTranslation()
|
||||
const total = items.reduce((acc, item) => acc + item.price, 0)
|
||||
const totalPrice = new Price(total, currency, locale)
|
||||
const toItem = (item: typeof items[0], idx: number) => {
|
||||
@ -35,16 +37,14 @@ function Payment({ currency, locale, items }: PaymentProps): JSX.Element {
|
||||
<div className='payment'>
|
||||
<div className='payment__table'>
|
||||
<div className='payment__total'>
|
||||
<div className='payment__total-title'>Total today</div>
|
||||
<div className='payment__total-title'>{t('totalToday')}</div>
|
||||
<div className='payment__total-price'>{totalPrice.format()}</div>
|
||||
</div>
|
||||
<div className='payment__items'>
|
||||
{items.map(toItem)}
|
||||
</div>
|
||||
</div>
|
||||
<div className='payment__information'>
|
||||
You will be charged only $1 for your 7-day trial. We'll email you a reminder before your trial period ends. Cancel anytime.
|
||||
</div>
|
||||
<div className='payment__information'>{t('chargedOnly')}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@ -1,7 +1,9 @@
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import './styles.css'
|
||||
|
||||
function Purposes(): JSX.Element {
|
||||
return <small className="purposes">For entertaiment purposes only</small>
|
||||
const { t } = useTranslation()
|
||||
return <small className="purposes">{t('purposes')}</small>
|
||||
}
|
||||
|
||||
export default Purposes
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import MainButton from '../MainButton'
|
||||
import Policy from '../Policy'
|
||||
import Countdown from '../Countdown'
|
||||
@ -6,6 +7,7 @@ import UserHeader from '../UserHeader'
|
||||
import CallToAction from '../CallToAction'
|
||||
|
||||
function SubscriptionPage(): JSX.Element {
|
||||
const { t } = useTranslation()
|
||||
const userEmail = 'some@email.com'
|
||||
const links = [
|
||||
{ text: 'Subscription policy', href: 'https://aura.wit.life/' },
|
||||
@ -27,10 +29,8 @@ function SubscriptionPage(): JSX.Element {
|
||||
<CallToAction />
|
||||
<Countdown start={10}/>
|
||||
<Payment items={paymentItems} currency={currency} locale={locale}/>
|
||||
<MainButton label='Get access' onClick={handleClick} />
|
||||
<Policy links={links}>
|
||||
By proceeding, you agree that if you do not cancel your subscription before the end of the 7-day trial period, you will be automatically charged nineteen US dollars zero cents every 2 weeks until you cancel the subscription in the settings. Learn more about cancellation and refund policy in Subscription policy
|
||||
</Policy>
|
||||
<MainButton label={t('getAccess')} onClick={handleClick} />
|
||||
<Policy links={links}>{t('subscriptionPolicy')}</Policy>
|
||||
</section>
|
||||
</>
|
||||
)
|
||||
|
||||
19
src/init.tsx
19
src/init.tsx
@ -1,13 +1,26 @@
|
||||
import React from 'react'
|
||||
import { BrowserRouter } from 'react-router-dom'
|
||||
import i18next from 'i18next'
|
||||
import { I18nextProvider, initReactI18next } from 'react-i18next'
|
||||
import resources from './locales'
|
||||
import routes from './routes'
|
||||
import App from './components/App'
|
||||
|
||||
const init = async () => {
|
||||
const response = await fetch(routes.server.translations())
|
||||
const data = await response.json()
|
||||
const defaultLanguage = data.meta.locale
|
||||
// TODO: add translations from data.translations
|
||||
const i18nextInstance = i18next.createInstance()
|
||||
const options = { lng: defaultLanguage, resources }
|
||||
await i18nextInstance.use(initReactI18next).init(options)
|
||||
return (
|
||||
<React.StrictMode>
|
||||
<BrowserRouter>
|
||||
<App />
|
||||
</BrowserRouter>
|
||||
<I18nextProvider i18n={i18nextInstance}>
|
||||
<BrowserRouter>
|
||||
<App />
|
||||
</BrowserRouter>
|
||||
</I18nextProvider>
|
||||
</React.StrictMode>
|
||||
)
|
||||
}
|
||||
|
||||
34
src/locales/en.ts
Normal file
34
src/locales/en.ts
Normal file
@ -0,0 +1,34 @@
|
||||
export default {
|
||||
translation: {
|
||||
letsStart: "Let's start!",
|
||||
next: "Next",
|
||||
dateOfBirth: "What's your date of birth?",
|
||||
privacyText: "By continuing, you agree to our EULA and Privacy Notice. Have a question? Reach our support team here",
|
||||
bornTimeQuestion: "What time were you born?",
|
||||
nasaDataUsing: "We use NASA data to determine the exact position of the planets in the sky at the time of your birth to create wallpapers that are just right for you.",
|
||||
ctaTitle: "Start your 7-day trial",
|
||||
ctaSubtitle: "No pressure. Cancel anytime.",
|
||||
reservedFor: "Reserved for ",
|
||||
creatingProfile: "Creating your profile",
|
||||
zodiacAnalysis: "Zodiac data analysis",
|
||||
drawingWallpaper: "Drawing Wallpapers",
|
||||
preparingResults: "Preparing results",
|
||||
invalidDate: "Date not found. Please check your details and try again.",
|
||||
year: "Year",
|
||||
month: "Month",
|
||||
day: "Day",
|
||||
weWillEmailYou: "We will email you a copy of your wallpaper for easy access.",
|
||||
yourEmail: "Your email",
|
||||
weDontShare: "We don't share any personal information.",
|
||||
continueAgree: 'By clicking "Continue" below, you agree to our EULA and Privacy Policy.',
|
||||
continue: 'Continue',
|
||||
appName: "Aura",
|
||||
unexpectedError: 'Sorry, an unexpected error has occurred.',
|
||||
oops: "Oops!",
|
||||
totalToday: 'Total today',
|
||||
chargedOnly: "You will be charged only $1 for your 7-day trial. We'll email you a reminder before your trial period ends. Cancel anytime.",
|
||||
purposes: 'For entertaiment purposes only.',
|
||||
getAccess: 'Get access',
|
||||
subscriptionPolicy: 'By proceeding, you agree that if you do not cancel your subscription before the end of the 7-day trial period, you will be automatically charged nineteen US dollars zero cents every 2 weeks until you cancel the subscription in the settings. Learn more about cancellation and refund policy in Subscription policy',
|
||||
},
|
||||
}
|
||||
3
src/locales/index.ts
Normal file
3
src/locales/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
import en from './en.ts'
|
||||
|
||||
export default { en }
|
||||
@ -1,5 +1,6 @@
|
||||
const host = '';
|
||||
const prefix = 'api/v1';
|
||||
const host = ''
|
||||
const apiHost = 'https://aura.wit.life'
|
||||
const prefix = 'api/v1'
|
||||
|
||||
const routes = {
|
||||
client: {
|
||||
@ -11,9 +12,9 @@ const routes = {
|
||||
createProfile: () => [host, 'profile', 'create'].join('/'),
|
||||
},
|
||||
server: {
|
||||
locales: () => [host, prefix, 'locales.json'].join('/'),
|
||||
translations: () => [host, prefix, 't.json'].join('/'),
|
||||
userRegistration: () => [host, prefix, 'user', 'registration.json'].join('/'),
|
||||
locales: () => [apiHost, prefix, 'locales.json'].join('/'),
|
||||
translations: () => [apiHost, prefix, 't.json'].join('/'),
|
||||
userRegistration: () => [apiHost, prefix, 'user', 'registration.json'].join('/'),
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
export interface FormField<T> {
|
||||
name: string
|
||||
value: T
|
||||
label?: string
|
||||
placeholder?: string
|
||||
label?: string | null
|
||||
placeholder?: string | null
|
||||
onValid: (value: T) => void
|
||||
onInvalid: () => void
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user