feat: integration with api server
This commit is contained in:
parent
ffeb365e8d
commit
70aac95661
@ -3,12 +3,12 @@
|
||||
"short_name": "",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/assets/android-chrome-192x192.png",
|
||||
"src": "/src/assets/android-chrome-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "/assets/android-chrome-512x512.png",
|
||||
"src": "/src/assets/android-chrome-512x512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png"
|
||||
}
|
||||
|
||||
@ -1,3 +1,26 @@
|
||||
import { createContext } from 'react'
|
||||
import { AuthToken, User } from '../types'
|
||||
|
||||
export const AuthContext = createContext<unknown>({})
|
||||
export interface AuthContextValue {
|
||||
user: User | null
|
||||
token: AuthToken
|
||||
logout: () => void
|
||||
signUp: (payload: SignUpPayload) => Promise<void>
|
||||
addBirthday: (birthday: string) => Promise<void>
|
||||
}
|
||||
|
||||
export interface SignUpPayload {
|
||||
email: string
|
||||
timezone: string
|
||||
locale: string
|
||||
}
|
||||
|
||||
const initialContext: AuthContextValue = {
|
||||
token: '',
|
||||
user: null,
|
||||
logout: () => undefined,
|
||||
signUp: () => Promise.resolve(),
|
||||
addBirthday: () => Promise.resolve(),
|
||||
}
|
||||
|
||||
export const AuthContext = createContext<AuthContextValue>(initialContext)
|
||||
|
||||
@ -1,7 +1,58 @@
|
||||
import { AuthContext } from './AuthContext'
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
import { AuthContext, SignUpPayload } from './AuthContext'
|
||||
import { RootState, actions } from '../store'
|
||||
import routes from '../routes'
|
||||
|
||||
export function AuthProvider({ children }: React.PropsWithChildren<unknown>): JSX.Element {
|
||||
const auth = {}
|
||||
const dispatch = useDispatch()
|
||||
const token = useSelector((state: RootState) => state.token)
|
||||
const user = useSelector((state: RootState) => state.user)
|
||||
const signUp = async ({ email, timezone, locale }: SignUpPayload): Promise<void> => {
|
||||
const response = await fetch(routes.server.token(), {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ auth: { email, timezone, locale } })
|
||||
})
|
||||
if (response.ok) {
|
||||
const { auth: { token, user } } = await response.json()
|
||||
dispatch(actions.token.update(token))
|
||||
dispatch(actions.user.update(user))
|
||||
} else {
|
||||
const { errors } = await response.json()
|
||||
if (Array.isArray(errors)) {
|
||||
const [error] = errors
|
||||
throw new Error(error.title)
|
||||
} else {
|
||||
const { base: [error] } = errors
|
||||
throw new Error(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
const addBirthday = async (birthday: string): Promise<void> => {
|
||||
const response = await fetch(routes.server.user(), {
|
||||
method: 'PATCH',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${token}`,
|
||||
},
|
||||
body: JSON.stringify({ user: { profile_attributes: { birthday } } })
|
||||
})
|
||||
if (response.ok) {
|
||||
const { user } = await response.json()
|
||||
dispatch(actions.user.update(user))
|
||||
} else {
|
||||
const { errors } = await response.json()
|
||||
if (Array.isArray(errors)) {
|
||||
const [error] = errors
|
||||
throw new Error(error.title)
|
||||
} else {
|
||||
const { base: [error] } = errors
|
||||
throw new Error(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
const logout = () => dispatch(actions.reset())
|
||||
const auth = { signUp, logout, addBirthday, token, user: user.id ? user : null }
|
||||
return (
|
||||
<AuthContext.Provider value={auth}>
|
||||
{children}
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { Routes, Route, Navigate } from 'react-router-dom'
|
||||
import { Routes, Route, Navigate, Outlet } from 'react-router-dom'
|
||||
import { useAuth } from '../../auth'
|
||||
import BirthdayPage from '../BirthdayPage'
|
||||
import BirthtimePage from '../BirthtimePage'
|
||||
import CreateProfilePage from '../CreateProfilePage'
|
||||
@ -22,7 +23,9 @@ function App(): JSX.Element {
|
||||
<Route path={routes.client.birthtime()} element={<BirthtimePage />} />
|
||||
<Route path={routes.client.createProfile()} element={<CreateProfilePage />} />
|
||||
<Route path={routes.client.emailEnter()} element={<EmailEnterPage />} />
|
||||
<Route path={routes.client.subscription()} element={<SubscriptionPage />} />
|
||||
<Route path={routes.client.subscription()} element={<PrivateOutlet />}>
|
||||
<Route path='' element={<SubscriptionPage />} />
|
||||
</Route>
|
||||
<Route path="*" element={<NotFoundPage />} />
|
||||
</Routes>
|
||||
</main>
|
||||
@ -30,4 +33,9 @@ function App(): JSX.Element {
|
||||
)
|
||||
}
|
||||
|
||||
function PrivateOutlet(): JSX.Element {
|
||||
const { user } = useAuth()
|
||||
return user ? <Outlet /> : <Navigate to={routes.client.root()} />
|
||||
}
|
||||
|
||||
export default App
|
||||
|
||||
@ -15,7 +15,7 @@ function BirthdayPage(): JSX.Element {
|
||||
const { t } = useTranslation()
|
||||
const dispatch = useDispatch()
|
||||
const navigate = useNavigate()
|
||||
const birthdate = useSelector((state: RootState) => state.birthdate)
|
||||
const birthdate = useSelector((state: RootState) => state.form.birthdate)
|
||||
const [isDisabled, setIsDisabled] = useState(true)
|
||||
const links = [
|
||||
{ text: 'EULA', href: 'https://aura.wit.life/terms' },
|
||||
@ -24,7 +24,7 @@ function BirthdayPage(): JSX.Element {
|
||||
]
|
||||
const handleNext = () => navigate(routes.client.birthtime())
|
||||
const handleValid = (birthdate: string) => {
|
||||
dispatch(actions.birthdate.update(birthdate))
|
||||
dispatch(actions.form.addDate(birthdate))
|
||||
setIsDisabled(birthdate === '')
|
||||
}
|
||||
|
||||
@ -38,7 +38,9 @@ function BirthdayPage(): JSX.Element {
|
||||
onValid={handleValid}
|
||||
onInvalid={() => setIsDisabled(true)}
|
||||
/>
|
||||
<MainButton label={t('next')} onClick={handleNext} disabled={isDisabled}/>
|
||||
<MainButton onClick={handleNext} disabled={isDisabled}>
|
||||
{t('next')}
|
||||
</MainButton>
|
||||
<footer className='footer'>
|
||||
<Policy links={links}>{t('privacyText')}</Policy>
|
||||
<Purposes />
|
||||
|
||||
@ -12,15 +12,15 @@ function BirthtimePage(): JSX.Element {
|
||||
const { t } = useTranslation()
|
||||
const dispatch = useDispatch()
|
||||
const navigate = useNavigate();
|
||||
const birthtime = useSelector((state: RootState) => state.birthtime)
|
||||
const birthtime = useSelector((state: RootState) => state.form.birthtime)
|
||||
const handleNext = () => navigate(routes.client.createProfile())
|
||||
const handleChange = (value: string) => dispatch(actions.birthtime.update(value))
|
||||
const handleChange = (value: string) => dispatch(actions.form.addTime(value))
|
||||
return (
|
||||
<section className='page'>
|
||||
<Title variant="h2" className="mt-24">{t('bornTimeQuestion')}</Title>
|
||||
<p className="description">{t('nasaDataUsing')}</p>
|
||||
<TimePicker value={birthtime} onChange={handleChange}/>
|
||||
<MainButton label={t('next')} onClick={handleNext}/>
|
||||
<MainButton onClick={handleNext}>{t('next')}</MainButton>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
import Loader from "../Loader"
|
||||
|
||||
type ProcessItemProps = {
|
||||
top: number
|
||||
label: string
|
||||
@ -12,7 +14,7 @@ function ProcessItem({ top, label, isDone }: ProcessItemProps): JSX.Element {
|
||||
{
|
||||
isDone
|
||||
? <div className='process-item__icon'>✅</div>
|
||||
: <div className="process-item__loader"><span></span></div>
|
||||
: <Loader />
|
||||
}
|
||||
</div>
|
||||
<div className='process-item__label'>{label}</div>
|
||||
|
||||
@ -21,71 +21,8 @@
|
||||
.process-item__pick {
|
||||
position: relative;
|
||||
margin-right: 15px;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
clear: both;
|
||||
}
|
||||
|
||||
.process-item__icon {
|
||||
font-size: 32px;
|
||||
}
|
||||
|
||||
.process-item__loader {
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
margin: auto;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
animation: loader-1-1 4.8s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes loader-1-1 {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.process-item__loader span {
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
margin: auto;
|
||||
height: 32px;
|
||||
width: 32px;
|
||||
clip: rect(0, 32px, 32px, 16px);
|
||||
animation: loader-1-2 1.2s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes loader-1-2 {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(220deg); }
|
||||
}
|
||||
|
||||
.process-item__loader span::after {
|
||||
position: absolute;
|
||||
box-sizing: border-box;
|
||||
content: "";
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
margin: auto;
|
||||
height: 32px;
|
||||
width: 32px;
|
||||
clip: rect(0, 32px, 32px, 16px);
|
||||
border: 3px solid #000;
|
||||
border-radius: 50%;
|
||||
animation: loader-1-3 1.2s cubic-bezier(0.770, 0.000, 0.175, 1.000) infinite;
|
||||
}
|
||||
|
||||
@keyframes loader-1-3 {
|
||||
0% { transform: rotate(-140deg); }
|
||||
50% { transform: rotate(-160deg); }
|
||||
100% { transform: rotate(140deg); }
|
||||
}
|
||||
|
||||
@ -2,7 +2,7 @@ import { useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { FormField } from '../../types'
|
||||
import DateInput from './DateInput'
|
||||
import ErrorText from './ErrorText'
|
||||
import ErrorText from '../ErrorText'
|
||||
import { stringify, getCurrentYear } from './utils'
|
||||
|
||||
export function DatePicker(props: FormField<string>): JSX.Element {
|
||||
@ -63,10 +63,7 @@ export function DatePicker(props: FormField<string>): JSX.Element {
|
||||
onChange={(day: string) => setDay(day)}
|
||||
/>
|
||||
</div>
|
||||
<ErrorText
|
||||
isShown={hasError}
|
||||
message={t('invalidDate')}
|
||||
/>
|
||||
<ErrorText isShown={hasError} message={t('invalidDate')} />
|
||||
</form>
|
||||
)
|
||||
}
|
||||
|
||||
@ -1,15 +0,0 @@
|
||||
type ErrorTextProps = {
|
||||
isShown: boolean
|
||||
message: string
|
||||
}
|
||||
|
||||
function ErrorText({ message, isShown }: ErrorTextProps): JSX.Element {
|
||||
const className = isShown ? 'date-picker__error--shown' : ''
|
||||
return (
|
||||
<p className={`date-picker__error ${className}`}>
|
||||
{message}
|
||||
</p>
|
||||
)
|
||||
}
|
||||
|
||||
export default ErrorText
|
||||
@ -97,19 +97,3 @@
|
||||
border: 2px solid #dee5f9;
|
||||
padding-top: 5px;
|
||||
}
|
||||
|
||||
.date-picker__error {
|
||||
color: #ff5c5d;
|
||||
font-size: 12px;
|
||||
left: 12px;
|
||||
line-height: 16px;
|
||||
margin-left: 12px;
|
||||
position: absolute;
|
||||
transform: translateY(-32px);
|
||||
transition: all .5s;
|
||||
}
|
||||
|
||||
.date-picker__error--shown {
|
||||
position: static;
|
||||
transform: translateY(6px);
|
||||
}
|
||||
|
||||
@ -3,27 +3,51 @@ import { useNavigate } from 'react-router-dom'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
import { actions, RootState } from '../../store'
|
||||
import { getClientLocale, getClientTimezone } from '../../locales'
|
||||
import Title from '../Title'
|
||||
import Policy from '../Policy'
|
||||
import EmailInput from './EmailInput'
|
||||
import MainButton from '../MainButton'
|
||||
import Loader, { LoaderColor } from '../Loader'
|
||||
import ErrorText from '../ErrorText'
|
||||
import routes from '../../routes'
|
||||
import { useAuth } from '../../auth'
|
||||
|
||||
function EmailEnterPage(): JSX.Element {
|
||||
const { t } = useTranslation()
|
||||
const dispatch = useDispatch()
|
||||
const navigate = useNavigate()
|
||||
const email = useSelector((state: RootState) => state.email)
|
||||
const { user, signUp, addBirthday } = useAuth()
|
||||
const { email, birthdate, birthtime } = useSelector((state: RootState) => state.form)
|
||||
const [isDisabled, setIsDisabled] = useState(true)
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [error, setError] = useState<Error | null>(null)
|
||||
const links = [
|
||||
{ text: 'EULA', href: 'https://aura.wit.life/terms' },
|
||||
{ text: 'Privacy Policy', href: 'https://aura.wit.life/privacy' },
|
||||
]
|
||||
const handleValidEmail = (email: string) => {
|
||||
dispatch(actions.email.update(email))
|
||||
dispatch(actions.form.addEmail(email))
|
||||
setIsDisabled(false)
|
||||
}
|
||||
const handleClick = () => navigate(routes.client.subscription())
|
||||
const handleClick = () => {
|
||||
if (user) {
|
||||
navigate(routes.client.subscription())
|
||||
return
|
||||
}
|
||||
setError(null)
|
||||
setIsLoading(true)
|
||||
signUp({
|
||||
email,
|
||||
timezone: getClientTimezone(),
|
||||
locale: getClientLocale(),
|
||||
})
|
||||
.then(() => addBirthday(`${birthdate} ${birthtime}`))
|
||||
.then(() => navigate(routes.client.subscription()))
|
||||
.catch((error: Error) => setError(error))
|
||||
.finally(() => setIsLoading(false))
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<section className='page'>
|
||||
@ -37,7 +61,10 @@ function EmailEnterPage(): JSX.Element {
|
||||
/>
|
||||
<p>{t('weDontShare')}</p>
|
||||
<Policy links={links} sizing='medium'>{t('continueAgree')}</Policy>
|
||||
<MainButton label={t('continue')} onClick={handleClick} disabled={isDisabled} />
|
||||
<MainButton onClick={handleClick} disabled={isDisabled}>
|
||||
{isLoading ? <Loader color={LoaderColor.White} /> : t('continue')}
|
||||
</MainButton>
|
||||
<ErrorText size='medium' isShown={Boolean(error)} message={error?.message} />
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
18
src/components/ErrorText/index.tsx
Normal file
18
src/components/ErrorText/index.tsx
Normal file
@ -0,0 +1,18 @@
|
||||
import './styles.css'
|
||||
|
||||
type ErrorTextProps = {
|
||||
isShown: boolean
|
||||
message: string | null | undefined
|
||||
size?: 'small' | 'medium' | 'large'
|
||||
}
|
||||
|
||||
function ErrorText({ message, isShown, size }: ErrorTextProps): JSX.Element {
|
||||
const classNames = ['error-text', isShown ? 'error--shown' : '', size ? `error--${size}` : '']
|
||||
return (
|
||||
<p className={classNames.filter(Boolean).join(' ')}>
|
||||
{message}
|
||||
</p>
|
||||
)
|
||||
}
|
||||
|
||||
export default ErrorText
|
||||
27
src/components/ErrorText/styles.css
Normal file
27
src/components/ErrorText/styles.css
Normal file
@ -0,0 +1,27 @@
|
||||
.error-text {
|
||||
color: #ff5c5d;
|
||||
font-size: 12px;
|
||||
left: 12px;
|
||||
line-height: 16px;
|
||||
margin-left: 12px;
|
||||
position: absolute;
|
||||
transform: translateY(-32px);
|
||||
transition: all .5s;
|
||||
}
|
||||
|
||||
.error--shown {
|
||||
position: static;
|
||||
transform: translateY(6px);
|
||||
}
|
||||
|
||||
.error--small {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.error--medium {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.error--large {
|
||||
font-size: 16px;
|
||||
}
|
||||
21
src/components/Loader/index.tsx
Normal file
21
src/components/Loader/index.tsx
Normal file
@ -0,0 +1,21 @@
|
||||
import './styles.css'
|
||||
|
||||
export enum LoaderColor {
|
||||
White = 'white',
|
||||
Black = 'black',
|
||||
}
|
||||
|
||||
type LoaderProps = {
|
||||
color?: LoaderColor
|
||||
}
|
||||
|
||||
function Loader({ color = LoaderColor.Black }: LoaderProps): JSX.Element {
|
||||
const colorClass = color === LoaderColor.White ? 'loader__white' : 'loader__black'
|
||||
return (
|
||||
<div className='loader-container'>
|
||||
<div className={`loader ${colorClass}`}><span></span></div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Loader
|
||||
74
src/components/Loader/styles.css
Normal file
74
src/components/Loader/styles.css
Normal file
@ -0,0 +1,74 @@
|
||||
.loader {
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
margin: auto;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
animation: loader-1-1 4.8s linear infinite;
|
||||
}
|
||||
|
||||
.loader-container {
|
||||
position: relative;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
clear: both;
|
||||
}
|
||||
|
||||
.loader.loader__black span::after {
|
||||
border-color: #000;
|
||||
}
|
||||
|
||||
.loader.loader__white span::after {
|
||||
border-color: #fff;
|
||||
}
|
||||
|
||||
@keyframes loader-1-1 {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.loader span {
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
margin: auto;
|
||||
height: 32px;
|
||||
width: 32px;
|
||||
clip: rect(0, 32px, 32px, 16px);
|
||||
animation: loader-1-2 1.2s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes loader-1-2 {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(220deg); }
|
||||
}
|
||||
|
||||
.loader span::after {
|
||||
position: absolute;
|
||||
box-sizing: border-box;
|
||||
content: "";
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
margin: auto;
|
||||
height: 32px;
|
||||
width: 32px;
|
||||
clip: rect(0, 32px, 32px, 16px);
|
||||
border: 3px solid transparent;
|
||||
border-radius: 50%;
|
||||
animation: loader-1-3 1.2s cubic-bezier(0.770, 0.000, 0.175, 1.000) infinite;
|
||||
}
|
||||
|
||||
@keyframes loader-1-3 {
|
||||
0% { transform: rotate(-140deg); }
|
||||
50% { transform: rotate(-160deg); }
|
||||
100% { transform: rotate(140deg); }
|
||||
}
|
||||
@ -1,13 +1,11 @@
|
||||
import './styles.css'
|
||||
|
||||
type ButtonProps = {
|
||||
label: string;
|
||||
} & React.ButtonHTMLAttributes<HTMLButtonElement>;
|
||||
type ButtonProps = React.ButtonHTMLAttributes<HTMLButtonElement>;
|
||||
|
||||
|
||||
function MainButton({ className, label, ...props}: ButtonProps): JSX.Element {
|
||||
function MainButton({ className, children, ...props}: ButtonProps): JSX.Element {
|
||||
const combinedClassNames = ['main-btn', className].filter(Boolean).join(' ')
|
||||
return <button className={combinedClassNames} {...props}>{label}</button>
|
||||
return <button className={combinedClassNames} {...props}>{children}</button>
|
||||
}
|
||||
|
||||
export default MainButton
|
||||
|
||||
@ -15,6 +15,8 @@
|
||||
min-width: 250px;
|
||||
padding: 12px 16px;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
.main-btn:disabled {
|
||||
|
||||
@ -1,4 +1,6 @@
|
||||
import { useSelector } from 'react-redux'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { RootState } from '../../store'
|
||||
import MainButton from '../MainButton'
|
||||
import Policy from '../Policy'
|
||||
import Countdown from '../Countdown'
|
||||
@ -8,7 +10,7 @@ import CallToAction from '../CallToAction'
|
||||
|
||||
function SubscriptionPage(): JSX.Element {
|
||||
const { t } = useTranslation()
|
||||
const userEmail = 'some@email.com'
|
||||
const email = useSelector((state: RootState) => state.form.email)
|
||||
const links = [
|
||||
{ text: 'Subscription policy', href: 'https://aura.wit.life/' },
|
||||
]
|
||||
@ -24,12 +26,12 @@ function SubscriptionPage(): JSX.Element {
|
||||
const handleClick = () => console.log('What we will do?')
|
||||
return (
|
||||
<>
|
||||
<UserHeader email={userEmail} />
|
||||
<UserHeader email={email} />
|
||||
<section className='page'>
|
||||
<CallToAction />
|
||||
<Countdown start={10}/>
|
||||
<Payment items={paymentItems} currency={currency} locale={locale}/>
|
||||
<MainButton label={t('getAccess')} onClick={handleClick} />
|
||||
<MainButton onClick={handleClick}>{t('getAccess')}</MainButton>
|
||||
<Policy links={links}>{t('subscriptionPolicy')}</Policy>
|
||||
</section>
|
||||
</>
|
||||
|
||||
@ -1,3 +1,6 @@
|
||||
import en from './en.ts'
|
||||
|
||||
export const getClientLocale = () => navigator.language.split('-')[0]
|
||||
export const getClientTimezone = () => Intl.DateTimeFormat().resolvedOptions().timeZone
|
||||
|
||||
export default { en }
|
||||
|
||||
@ -14,7 +14,9 @@ const routes = {
|
||||
server: {
|
||||
locales: () => [apiHost, prefix, 'locales.json'].join('/'),
|
||||
translations: () => [apiHost, prefix, 't.json'].join('/'),
|
||||
userRegistration: () => [apiHost, prefix, 'user', 'registration.json'].join('/'),
|
||||
elements: () => [apiHost, prefix, 'elements.json'].join('/'),
|
||||
user: () => [apiHost, prefix, 'user.json'].join('/'),
|
||||
token: () => [apiHost, prefix, 'auth', 'token.json'].join('/'),
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@ -1,20 +0,0 @@
|
||||
import { createSlice } from '@reduxjs/toolkit'
|
||||
import type { PayloadAction } from '@reduxjs/toolkit'
|
||||
|
||||
const birthdateSlice = createSlice({
|
||||
name: 'birthdate',
|
||||
initialState: '',
|
||||
reducers: {
|
||||
update(state, action: PayloadAction<string>) {
|
||||
state = action.payload
|
||||
return state
|
||||
},
|
||||
clear(state) {
|
||||
state = ''
|
||||
return state
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
export const { actions } = birthdateSlice
|
||||
export default birthdateSlice.reducer
|
||||
@ -1,20 +0,0 @@
|
||||
import { createSlice } from '@reduxjs/toolkit'
|
||||
import type { PayloadAction } from '@reduxjs/toolkit'
|
||||
|
||||
const birthtimeSlice = createSlice({
|
||||
name: 'birthtime',
|
||||
initialState: '12:00',
|
||||
reducers: {
|
||||
update(state, action: PayloadAction<string>) {
|
||||
state = action.payload
|
||||
return state
|
||||
},
|
||||
clear(state) {
|
||||
state = ''
|
||||
return state
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
export const { actions } = birthtimeSlice
|
||||
export default birthtimeSlice.reducer
|
||||
@ -1,20 +0,0 @@
|
||||
import { createSlice } from '@reduxjs/toolkit'
|
||||
import type { PayloadAction } from '@reduxjs/toolkit'
|
||||
|
||||
const emailSlice = createSlice({
|
||||
name: 'email',
|
||||
initialState: '',
|
||||
reducers: {
|
||||
update(state, action: PayloadAction<string>) {
|
||||
state = action.payload
|
||||
return state
|
||||
},
|
||||
clear(state) {
|
||||
state = ''
|
||||
return state
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
export const { actions } = emailSlice
|
||||
export default emailSlice.reducer
|
||||
32
src/store/form.ts
Normal file
32
src/store/form.ts
Normal file
@ -0,0 +1,32 @@
|
||||
import { createSlice } from '@reduxjs/toolkit'
|
||||
import type { PayloadAction } from '@reduxjs/toolkit'
|
||||
import { SignupForm } from '../types'
|
||||
|
||||
const initialState: SignupForm = {
|
||||
birthdate: '',
|
||||
birthtime: '12:00',
|
||||
email: '',
|
||||
}
|
||||
|
||||
const formSlice = createSlice({
|
||||
name: 'form',
|
||||
initialState,
|
||||
reducers: {
|
||||
addDate(state, action: PayloadAction<string>) {
|
||||
state.birthdate = action.payload
|
||||
return state
|
||||
},
|
||||
addTime(state, action: PayloadAction<string>) {
|
||||
state.birthtime = action.payload
|
||||
return state
|
||||
},
|
||||
addEmail(state, action: PayloadAction<string>) {
|
||||
state.email = action.payload
|
||||
return state
|
||||
},
|
||||
},
|
||||
extraReducers: (builder) => builder.addCase('reset', () => initialState),
|
||||
})
|
||||
|
||||
export const { actions } = formSlice
|
||||
export default formSlice.reducer
|
||||
@ -1,22 +1,23 @@
|
||||
import { combineReducers, configureStore } from '@reduxjs/toolkit'
|
||||
import birthdate, { actions as birthdateActions } from './birthdate'
|
||||
import birthtime, { actions as birthtimeActions } from './birthtime'
|
||||
import email, { actions as emailActions } from './email'
|
||||
import { combineReducers, configureStore, createAction } from '@reduxjs/toolkit'
|
||||
import token, { actions as tokenActions } from './token'
|
||||
import user, { actions as userActions } from './user'
|
||||
import form, { actions as formActions } from './form'
|
||||
import { loadStore, backupStore } from './storageHelper'
|
||||
|
||||
const preloadedState = loadStore()
|
||||
export const reducer = combineReducers({
|
||||
email,
|
||||
birthdate,
|
||||
birthtime,
|
||||
})
|
||||
export const reducer = combineReducers({ token, user, form })
|
||||
export const actions = {
|
||||
email: emailActions,
|
||||
birthdate: birthdateActions,
|
||||
birthtime: birthtimeActions,
|
||||
token: tokenActions,
|
||||
user: userActions,
|
||||
form: formActions,
|
||||
reset: createAction('reset'),
|
||||
}
|
||||
export type RootState = ReturnType<typeof reducer>
|
||||
export const store = configureStore({ reducer, preloadedState })
|
||||
export const store = configureStore({
|
||||
reducer,
|
||||
preloadedState,
|
||||
devTools: import.meta.env.DEV,
|
||||
})
|
||||
export type AppDispatch = typeof store.dispatch
|
||||
export type StoreType = typeof store
|
||||
export const unsubscribe = backupStore(store)
|
||||
|
||||
@ -6,7 +6,7 @@ export const backupStore = (store: ToolkitStore) => {
|
||||
const saveState = () => {
|
||||
const state = store.getState()
|
||||
try {
|
||||
const serializedState = JSON.stringify(state)
|
||||
const serializedState = window.btoa(JSON.stringify(state))
|
||||
localStorage.setItem(storageKey, serializedState)
|
||||
} catch (err) {
|
||||
// nothing to do
|
||||
@ -31,7 +31,7 @@ export const loadStore = () => {
|
||||
if (serializedState === null) {
|
||||
return undefined
|
||||
}
|
||||
return JSON.parse(serializedState)
|
||||
return JSON.parse(window.atob(serializedState))
|
||||
} catch (err) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
20
src/store/token.ts
Normal file
20
src/store/token.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import { createSlice } from '@reduxjs/toolkit'
|
||||
import type { PayloadAction } from '@reduxjs/toolkit'
|
||||
import type { AuthToken } from '../types'
|
||||
|
||||
const initialState: AuthToken = ''
|
||||
|
||||
const authTokenSlice = createSlice({
|
||||
name: 'token',
|
||||
initialState,
|
||||
reducers: {
|
||||
update(state, action: PayloadAction<string>) {
|
||||
state = action.payload
|
||||
return state
|
||||
},
|
||||
},
|
||||
extraReducers: (builder) => builder.addCase('reset', () => initialState)
|
||||
})
|
||||
|
||||
export const { actions } = authTokenSlice
|
||||
export default authTokenSlice.reducer
|
||||
45
src/store/user.ts
Normal file
45
src/store/user.ts
Normal file
@ -0,0 +1,45 @@
|
||||
import { createSlice } from '@reduxjs/toolkit'
|
||||
import type { PayloadAction } from '@reduxjs/toolkit'
|
||||
import type { User } from '../types'
|
||||
import { getClientLocale, getClientTimezone } from '../locales'
|
||||
|
||||
const initialState: User = {
|
||||
id: undefined,
|
||||
username: null,
|
||||
email: '',
|
||||
locale: getClientLocale(),
|
||||
state: '',
|
||||
timezone: getClientTimezone(),
|
||||
new_registration: false,
|
||||
stat: {
|
||||
last_online_at: null,
|
||||
prev_online_at: null
|
||||
},
|
||||
profile: {
|
||||
full_name: null,
|
||||
gender: null,
|
||||
birthday: null,
|
||||
birthplace: null,
|
||||
age: null,
|
||||
sign: null,
|
||||
userpic: null,
|
||||
userpic_mime_type: undefined,
|
||||
relationship_status: '',
|
||||
human_relationship_status: ''
|
||||
},
|
||||
daily_push_subs: []
|
||||
}
|
||||
|
||||
const userSlice = createSlice({
|
||||
name: 'user',
|
||||
initialState,
|
||||
reducers: {
|
||||
update(state, action: PayloadAction<Partial<User>>) {
|
||||
return { ...state, ...action.payload }
|
||||
},
|
||||
},
|
||||
extraReducers: (builder) => builder.addCase('reset', () => initialState),
|
||||
})
|
||||
|
||||
export const { actions } = userSlice
|
||||
export default userSlice.reducer
|
||||
83
src/types.ts
83
src/types.ts
@ -6,3 +6,86 @@ export interface FormField<T> {
|
||||
onValid: (value: T) => void
|
||||
onInvalid: () => void
|
||||
}
|
||||
|
||||
export interface SignupForm {
|
||||
email: string
|
||||
birthdate: string
|
||||
birthtime: string
|
||||
}
|
||||
|
||||
export type AuthToken = string
|
||||
|
||||
export interface User {
|
||||
id: string | null | undefined
|
||||
username: string | null
|
||||
email: string
|
||||
locale: string
|
||||
state: string
|
||||
timezone: string
|
||||
new_registration: boolean
|
||||
stat: {
|
||||
last_online_at: string | null
|
||||
prev_online_at: string | null
|
||||
}
|
||||
profile: UserProfile
|
||||
daily_push_subs: Subscription[]
|
||||
}
|
||||
|
||||
export interface UserProfile {
|
||||
full_name: string | null
|
||||
gender: string | null
|
||||
birthday: string | null
|
||||
birthplace: UserBirhplace | null
|
||||
age: UserAge | null
|
||||
sign: UserSign | null
|
||||
userpic: UserPic | null
|
||||
userpic_mime_type: string | undefined
|
||||
relationship_status: string
|
||||
human_relationship_status: string
|
||||
}
|
||||
|
||||
export interface UserAge {
|
||||
years: number
|
||||
days: number
|
||||
}
|
||||
|
||||
export interface UserSign {
|
||||
house: number
|
||||
ruler: string
|
||||
dates: {
|
||||
start: {
|
||||
month: number
|
||||
day: number
|
||||
}
|
||||
end: {
|
||||
month: number
|
||||
day: number
|
||||
}
|
||||
}
|
||||
sign: string
|
||||
char: string
|
||||
polarity: string
|
||||
modality: string
|
||||
triplicity: string
|
||||
}
|
||||
|
||||
export interface UserPic {
|
||||
th: string
|
||||
th2x: string
|
||||
lg: string
|
||||
}
|
||||
|
||||
export interface UserBirhplace {
|
||||
id: string
|
||||
address: string
|
||||
coords: string
|
||||
}
|
||||
|
||||
export interface Subscription {
|
||||
id: string
|
||||
daily_push_id: string
|
||||
time: string
|
||||
updated_at: string
|
||||
created_at: string
|
||||
last_sent_at: string | null
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user