feat: add payment and wallpaper pages
@ -8,6 +8,8 @@ import {
|
||||
Assets,
|
||||
AssetCategories,
|
||||
DailyForecasts,
|
||||
SubscriptionItems,
|
||||
SubscriptionCheckout,
|
||||
} from './resources'
|
||||
|
||||
export interface ApiContextValue {
|
||||
@ -19,6 +21,8 @@ export interface ApiContextValue {
|
||||
getAssetCategories: ReturnType<typeof createMethod<AssetCategories.Payload, AssetCategories.Response>>
|
||||
getDailyForecasts: ReturnType<typeof createMethod<DailyForecasts.Payload, DailyForecasts.Response>>
|
||||
getAuras: ReturnType<typeof createMethod<Auras.Payload, Auras.Response>>
|
||||
getSubscriptionItems: ReturnType<typeof createMethod<SubscriptionItems.Payload, SubscriptionItems.Response>>
|
||||
getSubscriptionCheckout: ReturnType<typeof createMethod<SubscriptionCheckout.Payload, SubscriptionCheckout.Response>>
|
||||
}
|
||||
|
||||
export const ApiContext = createContext<ApiContextValue>({} as ApiContextValue)
|
||||
|
||||
@ -8,17 +8,23 @@ import {
|
||||
Assets,
|
||||
AssetCategories,
|
||||
DailyForecasts,
|
||||
SubscriptionItems,
|
||||
SubscriptionCheckout,
|
||||
} from './resources'
|
||||
|
||||
export function createApi(): ApiContextValue {
|
||||
return {
|
||||
auth: createMethod<AuthTokens.Payload, AuthTokens.Response>(AuthTokens.createRequest),
|
||||
getElements: createMethod<Elements.Payload, Elements.Response>(Elements.createRequest),
|
||||
getUser: createMethod<User.GetPayload, User.Response>(User.createGetRequest),
|
||||
updateUser: createMethod<User.PatchPayload, User.Response>(User.createPatchRequest),
|
||||
getAssets: createMethod<Assets.Payload, Assets.Response>(Assets.createRequest),
|
||||
getAssetCategories: createMethod<AssetCategories.Payload, AssetCategories.Response>(AssetCategories.createRequest),
|
||||
getDailyForecasts: createMethod<DailyForecasts.Payload, DailyForecasts.Response>(DailyForecasts.createRequest),
|
||||
getAuras: createMethod<Auras.Payload, Auras.Response>(Auras.createRequest),
|
||||
}
|
||||
const api = {
|
||||
auth: createMethod<AuthTokens.Payload, AuthTokens.Response>(AuthTokens.createRequest),
|
||||
getElements: createMethod<Elements.Payload, Elements.Response>(Elements.createRequest),
|
||||
getUser: createMethod<User.GetPayload, User.Response>(User.createGetRequest),
|
||||
updateUser: createMethod<User.PatchPayload, User.Response>(User.createPatchRequest),
|
||||
getAssets: createMethod<Assets.Payload, Assets.Response>(Assets.createRequest),
|
||||
getAssetCategories: createMethod<AssetCategories.Payload, AssetCategories.Response>(AssetCategories.createRequest),
|
||||
getDailyForecasts: createMethod<DailyForecasts.Payload, DailyForecasts.Response>(DailyForecasts.createRequest),
|
||||
getAuras: createMethod<Auras.Payload, Auras.Response>(Auras.createRequest),
|
||||
getSubscriptionItems: createMethod<SubscriptionItems.Payload, SubscriptionItems.Response>(SubscriptionItems.createRequest),
|
||||
getSubscriptionCheckout: createMethod<SubscriptionCheckout.Payload, SubscriptionCheckout.Response>(SubscriptionCheckout.createRequest),
|
||||
}
|
||||
|
||||
export function createApi(): ApiContextValue {
|
||||
return api
|
||||
}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
export * from './useApi'
|
||||
export * from './useApiCall'
|
||||
export * from './ApiContext'
|
||||
export * from './api'
|
||||
export * from './types'
|
||||
export type { User } from './resources/User'
|
||||
export type { Payload as SignUpPayload } from './resources/AuthTokens'
|
||||
export * from './resources'
|
||||
|
||||
36
src/api/resources/UserSubscriptionCheckout.ts
Normal file
@ -0,0 +1,36 @@
|
||||
import routes from "../../routes"
|
||||
import { AuthToken } from "../types"
|
||||
import { getAuthHeaders } from "../utils"
|
||||
|
||||
export interface Payload {
|
||||
token: AuthToken
|
||||
embed?: boolean
|
||||
locale: string
|
||||
itemPriceId: string
|
||||
}
|
||||
|
||||
export interface Response {
|
||||
hosted_page: string
|
||||
}
|
||||
|
||||
export interface HostedPage {
|
||||
id: string
|
||||
url: string
|
||||
embed: boolean
|
||||
type: string
|
||||
object: string
|
||||
state: string
|
||||
resource_version: number
|
||||
created_at: number
|
||||
updated_at: number
|
||||
expires_at: number
|
||||
}
|
||||
|
||||
export const createRequest = ({ locale, token, itemPriceId, embed = false }: Payload): Request => {
|
||||
const url = new URL(routes.server.subscriptionCheckout())
|
||||
const query = new URLSearchParams({ locale, item_price_id: itemPriceId, embed: embed.toString() })
|
||||
|
||||
url.search = query.toString()
|
||||
|
||||
return new Request(url, { method: 'GET', headers: getAuthHeaders(token) })
|
||||
}
|
||||
41
src/api/resources/UserSubscriptionItemPrices.ts
Normal file
@ -0,0 +1,41 @@
|
||||
import routes from "../../routes"
|
||||
import { AuthToken } from "../types"
|
||||
import { getAuthHeaders } from "../utils"
|
||||
|
||||
export interface Payload {
|
||||
locale: string
|
||||
token: AuthToken
|
||||
}
|
||||
|
||||
export interface Response {
|
||||
item_prices: ItemPrice[]
|
||||
}
|
||||
|
||||
export interface ItemPrice {
|
||||
currency_code: string
|
||||
external_name: string
|
||||
free_quantity: number
|
||||
id: string
|
||||
is_taxable: boolean
|
||||
item_id: string
|
||||
item_type: string
|
||||
name: string
|
||||
object: string
|
||||
period: number
|
||||
period_unit: string
|
||||
price: number
|
||||
pricing_model: string
|
||||
resource_version: number
|
||||
status: string
|
||||
created_at: number
|
||||
updated_at: number
|
||||
}
|
||||
|
||||
export const createRequest = ({ locale, token }: Payload): Request => {
|
||||
const url = new URL(routes.server.subscriptionItems())
|
||||
const query = new URLSearchParams({ locale })
|
||||
|
||||
url.search = query.toString()
|
||||
|
||||
return new Request(url, { method: 'GET', headers: getAuthHeaders(token) })
|
||||
}
|
||||
@ -5,3 +5,5 @@ export * as DailyForecasts from './UserDailyForecasts'
|
||||
export * as Auras from './Auras'
|
||||
export * as Elements from './Elements'
|
||||
export * as AuthTokens from './AuthTokens'
|
||||
export * as SubscriptionItems from './UserSubscriptionItemPrices'
|
||||
export * as SubscriptionCheckout from './UserSubscriptionCheckout'
|
||||
|
||||
24
src/api/useApiCall.ts
Normal file
@ -0,0 +1,24 @@
|
||||
import { useState, useEffect } from "react";
|
||||
|
||||
interface HookResult<T> {
|
||||
isPending: boolean
|
||||
error: Error | null
|
||||
data: T
|
||||
}
|
||||
|
||||
type ApiMethod<T> = () => Promise<T>
|
||||
|
||||
export function useApiCall<T>(apiMethod: ApiMethod<T>): HookResult<T> {
|
||||
const [data, setData] = useState<T>({} as T)
|
||||
const [error, setError] = useState<Error | null>(null)
|
||||
const [isPending, setIsPending] = useState<boolean>(true)
|
||||
|
||||
useEffect(() => {
|
||||
apiMethod()
|
||||
.then((data: T) => setData(data))
|
||||
.catch((error: Error) => setError(error))
|
||||
.finally(() => setIsPending(false))
|
||||
}, [apiMethod])
|
||||
|
||||
return { isPending, error, data }
|
||||
}
|
||||
@ -1,12 +1,11 @@
|
||||
import { createContext } from 'react'
|
||||
import { AuthToken, User, SignUpPayload } from '../api'
|
||||
import { AuthToken, User } from '../api'
|
||||
|
||||
export interface AuthContextValue {
|
||||
user: User | null
|
||||
user: User.User | null
|
||||
token: AuthToken
|
||||
logout: () => void
|
||||
signUp: (payload: SignUpPayload) => Promise<AuthToken>
|
||||
addBirthday: (birthday: string, token: AuthToken) => Promise<void>
|
||||
signUp: (token: AuthToken, user: User.User) => AuthToken
|
||||
}
|
||||
|
||||
export const AuthContext = createContext<AuthContextValue>({} as AuthContextValue)
|
||||
|
||||
@ -1,26 +1,25 @@
|
||||
import { useCallback, useMemo } from 'react'
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
import { RootState, actions } from '../store'
|
||||
import { useApi, AuthToken, SignUpPayload } from '../api'
|
||||
import { actions, selectors } from '../store'
|
||||
import { AuthToken, User } from '../api'
|
||||
import { AuthContext } from './AuthContext'
|
||||
|
||||
export function AuthProvider({ children }: React.PropsWithChildren<unknown>): JSX.Element {
|
||||
const api = useApi()
|
||||
const dispatch = useDispatch()
|
||||
const token = useSelector((state: RootState) => state.token)
|
||||
const user = useSelector((state: RootState) => state.user)
|
||||
const signUp = async (payload: SignUpPayload): Promise<AuthToken> => {
|
||||
const { auth: { token, user } } = await api.auth(payload)
|
||||
const token = useSelector(selectors.selectToken)
|
||||
const user = useSelector(selectors.selectUser)
|
||||
const signUp = useCallback((token: AuthToken, user: User.User): AuthToken => {
|
||||
dispatch(actions.token.update(token))
|
||||
dispatch(actions.user.update(user))
|
||||
return token
|
||||
}
|
||||
const addBirthday = async (birthday: string, token: AuthToken): Promise<void> => {
|
||||
const payload = { user: { profile_attributes: { birthday } }, token }
|
||||
const { user } = await api.updateUser(payload)
|
||||
dispatch(actions.user.update(user))
|
||||
}
|
||||
const logout = () => dispatch(actions.reset())
|
||||
const auth = { signUp, logout, addBirthday, token, user: user.id ? user : null }
|
||||
}, [dispatch])
|
||||
const logout = useCallback(() => dispatch(actions.reset()), [dispatch])
|
||||
const auth = useMemo(() => ({
|
||||
signUp,
|
||||
logout,
|
||||
token,
|
||||
user: user.id ? user : null
|
||||
}), [token, user, signUp, logout])
|
||||
return (
|
||||
<AuthContext.Provider value={auth}>
|
||||
{children}
|
||||
|
||||
@ -1,41 +1,62 @@
|
||||
import { Routes, Route, Navigate, Outlet } from 'react-router-dom'
|
||||
import { useState } from 'react'
|
||||
import { Routes, Route, Navigate, Outlet, useLocation } from 'react-router-dom'
|
||||
import { useAuth } from '../../auth'
|
||||
import BirthdayPage from '../BirthdayPage'
|
||||
import BirthtimePage from '../BirthtimePage'
|
||||
import CreateProfilePage from '../CreateProfilePage'
|
||||
import EmailEnterPage from '../EmailEnterPage'
|
||||
import SubscriptionPage from '../SubscriptionPage'
|
||||
import PaymentPage from '../PaymentPage'
|
||||
import WallpaperPage from '../WallpaperPage'
|
||||
import NotFoundPage from '../NotFoundPage'
|
||||
import Header from '../Header'
|
||||
import routes from '../../routes'
|
||||
import Navbar from '../Navbar'
|
||||
import routes, { hasNavigation } from '../../routes'
|
||||
import './styles.css'
|
||||
|
||||
function App(): JSX.Element {
|
||||
return (
|
||||
<Routes>
|
||||
<Route element={<Layout />}>
|
||||
<Route path={routes.client.root()} element={
|
||||
<Navigate to={routes.client.birthday()} />
|
||||
} />
|
||||
<Route path={routes.client.birthday()} element={<BirthdayPage />} />
|
||||
<Route path={routes.client.birthtime()} element={<BirthtimePage />} />
|
||||
<Route path={routes.client.createProfile()} element={<SkipStep />} />
|
||||
<Route path={routes.client.emailEnter()} element={<EmailEnterPage />} />
|
||||
<Route element={<PrivateOutlet />}>
|
||||
<Route path={routes.client.subscription()} element={<SubscriptionPage />} />
|
||||
<Route path={routes.client.paymentMethod()} element={<PaymentPage />} />
|
||||
<Route path={routes.client.wallpaper()} element={<WallpaperPage />} />
|
||||
</Route>
|
||||
<Route path="*" element={<NotFoundPage />} />
|
||||
</Route>
|
||||
</Routes>
|
||||
)
|
||||
}
|
||||
|
||||
function Layout(): JSX.Element {
|
||||
const location = useLocation()
|
||||
const showNavbar = hasNavigation(location.pathname)
|
||||
const [isMenuOpen, setIsMenuOpen] = useState<boolean>(false)
|
||||
return (
|
||||
<div className='container'>
|
||||
<Header />
|
||||
<main className='content'>
|
||||
<Routes>
|
||||
<Route path={routes.client.root()} element={
|
||||
<Navigate to={routes.client.birthday()} />
|
||||
} />
|
||||
<Route path={routes.client.birthday()} element={<BirthdayPage />} />
|
||||
<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={<PrivateOutlet />}>
|
||||
<Route path='' element={<SubscriptionPage />} />
|
||||
</Route>
|
||||
<Route path="*" element={<NotFoundPage />} />
|
||||
</Routes>
|
||||
</main>
|
||||
<Header openMenu={() => setIsMenuOpen(true)}/>
|
||||
<main className='content'><Outlet /></main>
|
||||
{ showNavbar ? <Navbar isOpen={isMenuOpen} closeMenu={() => setIsMenuOpen(false)} /> : null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function PrivateOutlet(): JSX.Element {
|
||||
const { user } = useAuth()
|
||||
return user ? <Outlet /> : <Navigate to={routes.client.root()} />
|
||||
return user ? <Outlet /> : <Navigate to={routes.client.root()} replace={true} />
|
||||
}
|
||||
|
||||
function SkipStep(): JSX.Element {
|
||||
const { user } = useAuth()
|
||||
return user ? <Navigate to={routes.client.emailEnter()} replace={true} /> : <CreateProfilePage />
|
||||
}
|
||||
|
||||
export default App
|
||||
|
||||
@ -22,3 +22,7 @@
|
||||
overflow: hidden;
|
||||
padding: 15px 32px;
|
||||
}
|
||||
|
||||
.page-responsive {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@ -2,8 +2,10 @@ import { useState } from 'react'
|
||||
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 { actions, selectors } from '../../store'
|
||||
import { getClientTimezone } from '../../locales'
|
||||
import { useAuth } from '../../auth'
|
||||
import { useApi } from '../../api'
|
||||
import Title from '../Title'
|
||||
import Policy from '../Policy'
|
||||
import EmailInput from './EmailInput'
|
||||
@ -11,17 +13,19 @@ 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 api = useApi()
|
||||
const { t, i18n } = useTranslation()
|
||||
const dispatch = useDispatch()
|
||||
const navigate = useNavigate()
|
||||
const { user, signUp, addBirthday } = useAuth()
|
||||
const { email, birthdate, birthtime } = useSelector((state: RootState) => state.form)
|
||||
const { user, signUp } = useAuth()
|
||||
const { email, birthdate, birthtime } = useSelector(selectors.selectForm)
|
||||
const [isDisabled, setIsDisabled] = useState(true)
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [error, setError] = useState<Error | null>(null)
|
||||
const timezone = getClientTimezone()
|
||||
const locale = i18n.language
|
||||
const links = [
|
||||
{ text: 'EULA', href: 'https://aura.wit.life/terms' },
|
||||
{ text: 'Privacy Policy', href: 'https://aura.wit.life/privacy' },
|
||||
@ -37,15 +41,17 @@ function EmailEnterPage(): JSX.Element {
|
||||
}
|
||||
setError(null)
|
||||
setIsLoading(true)
|
||||
signUp({
|
||||
email,
|
||||
timezone: getClientTimezone(),
|
||||
locale: getClientLocale(),
|
||||
})
|
||||
.then((token) => addBirthday(`${birthdate} ${birthtime}`, token))
|
||||
.then(() => navigate(routes.client.subscription()))
|
||||
.catch((error: Error) => setError(error))
|
||||
.finally(() => setIsLoading(false))
|
||||
api.auth({ email, timezone, locale })
|
||||
.then(({ auth: { token, user } }) => signUp(token, user))
|
||||
.then((token) => {
|
||||
const birthday = `${birthdate} ${birthtime}`
|
||||
const payload = { user: { profile_attributes: { birthday } }, token }
|
||||
return api.updateUser(payload)
|
||||
})
|
||||
.then(({ user }) => dispatch(actions.user.update(user)))
|
||||
.then(() => navigate(routes.client.subscription()))
|
||||
.catch((error: Error) => setError(error))
|
||||
.finally(() => setIsLoading(false))
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -1,18 +1,24 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useNavigate, useLocation } from 'react-router-dom'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import routes, { isNotEntrypoint } from '../../routes'
|
||||
import routes, { hasNavigation, isNotEntrypoint } from '../../routes'
|
||||
import BackButton from '../BackButton'
|
||||
import iconUrl from './icon.png'
|
||||
import menuUrl from './menu.png'
|
||||
import './styles.css'
|
||||
|
||||
function Header(): JSX.Element {
|
||||
type HeaderProps = {
|
||||
openMenu: () => void
|
||||
}
|
||||
|
||||
function Header({ openMenu }: HeaderProps): JSX.Element {
|
||||
const { t } = useTranslation()
|
||||
const navigate = useNavigate()
|
||||
const location = useLocation()
|
||||
const [initialPath, setInitialPath] = useState<string | null>(null);
|
||||
const [isNavigated, setIsNavigated] = useState<boolean>(false);
|
||||
const showBackButton = isNotEntrypoint(location.pathname)
|
||||
const showMenuButton = hasNavigation(location.pathname)
|
||||
|
||||
useEffect(() => {
|
||||
if (!initialPath) {
|
||||
@ -36,6 +42,9 @@ function Header(): JSX.Element {
|
||||
{ showBackButton ? <BackButton className="pa" onClick={goBack} /> : null }
|
||||
<img src={iconUrl} alt="logo" width="40" height="40" />
|
||||
<span className="header__title">{t('appName')}</span>
|
||||
{showMenuButton ? <div className="header__menu-btn" onClick={openMenu}>
|
||||
<img src={menuUrl} alt="menu" width="40" height="40" />
|
||||
</div> : null}
|
||||
</header>
|
||||
)
|
||||
}
|
||||
|
||||
BIN
src/components/Header/menu.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
@ -1,12 +1,13 @@
|
||||
.header {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
position: relative;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #eff1fd;
|
||||
width: 100%;
|
||||
height: 50px;
|
||||
min-height: 50px;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.header__title {
|
||||
@ -15,3 +16,12 @@
|
||||
margin-left: 10px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.header__menu-btn {
|
||||
position: absolute;
|
||||
top: 5px;
|
||||
right: 28px;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
34
src/components/Navbar/index.tsx
Normal file
@ -0,0 +1,34 @@
|
||||
import './styles.css'
|
||||
|
||||
type NavbarProps = {
|
||||
isOpen: boolean
|
||||
closeMenu: () => void
|
||||
}
|
||||
|
||||
function Navbar({ isOpen, closeMenu }: NavbarProps): JSX.Element {
|
||||
const combinedClassNames = ['navbar', isOpen && 'navbar--open'].filter(Boolean).join(' ')
|
||||
return (
|
||||
<aside className={combinedClassNames}>
|
||||
<div className='navbar__overlay' onClick={closeMenu}></div>
|
||||
<div className='navbar__panel'>
|
||||
<div className="navbar__close-btn" onClick={closeMenu}>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
|
||||
<title>cross</title>
|
||||
<path d="M10.051 12l-10.051 10.051 1.949 1.949 10.051-10.051 10.051 10.051 1.949-1.949-10.051-10.051 10.051-10.051-1.949-1.949-10.051 10.051-10.051-10.051-1.949 1.949 10.051 10.051z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<nav className='navbar__nav'>
|
||||
<a href='#' target="_blank" rel="noreferrer nofollow">Privacy policy</a>
|
||||
<a href='#' target="_blank" rel="noreferrer nofollow">Terms of use</a>
|
||||
<a href='#' target="_blank" rel="noreferrer nofollow">Payment terms</a>
|
||||
<a href='#' target="_blank" rel="noreferrer nofollow">Subscription terms</a>
|
||||
<a href='#' target="_blank" rel="noreferrer nofollow">Money back policy</a>
|
||||
<a href='#' target="_blank" rel="noreferrer nofollow">FAQ</a>
|
||||
<a href='#' target="_blank" rel="noreferrer nofollow">Contact us</a>
|
||||
</nav>
|
||||
</div>
|
||||
</aside>
|
||||
)
|
||||
}
|
||||
|
||||
export default Navbar
|
||||
53
src/components/Navbar/styles.css
Normal file
@ -0,0 +1,53 @@
|
||||
.navbar {
|
||||
position: fixed;
|
||||
overflow: hidden;
|
||||
z-index: -1;
|
||||
transition: z-index .2s cubic-bezier(0.22, 0.61, 0.36, 1);
|
||||
}
|
||||
|
||||
.navbar.navbar--open {
|
||||
z-index: 5;
|
||||
}
|
||||
|
||||
.navbar.navbar--open .navbar__panel {
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.navbar.navbar--open .navbar__overlay {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.navbar__panel {
|
||||
position: relative;
|
||||
width: 312px;
|
||||
height: 100vh;
|
||||
padding: 20px;
|
||||
color: #000;
|
||||
transform: translateX(-100%);
|
||||
transition: transform .2s cubic-bezier(0.22, 0.61, 0.36, 1);
|
||||
background-color: rgb(255, 255, 255);
|
||||
box-shadow: rgba(0, 0, 0, 0.14) 0px 16px 24px 2px;
|
||||
|
||||
}
|
||||
|
||||
.navbar__nav > a {
|
||||
display: block;
|
||||
margin-bottom: 10px;
|
||||
font-size: 16px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.navbar__close-btn {
|
||||
cursor: pointer;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.navbar__overlay {
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0,0,0,.4);
|
||||
}
|
||||
BIN
src/components/PaymentPage/Apple-Pay.png
Normal file
|
After Width: | Height: | Size: 74 KiB |
15
src/components/PaymentPage/Apple-Pay.svg
Normal file
@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<svg version="1.1" baseProfile="tiny" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
x="0px" y="0px" viewBox="0 0 512 210.2" xml:space="preserve">
|
||||
<path fill="#fff" id="XMLID_34_" d="M93.6,27.1C87.6,34.2,78,39.8,68.4,39c-1.2-9.6,3.5-19.8,9-26.1c6-7.3,16.5-12.5,25-12.9
|
||||
C103.4,10,99.5,19.8,93.6,27.1 M102.3,40.9c-13.9-0.8-25.8,7.9-32.4,7.9c-6.7,0-16.8-7.5-27.8-7.3c-14.3,0.2-27.6,8.3-34.9,21.2
|
||||
c-15,25.8-3.9,64,10.6,85c7.1,10.4,15.6,21.8,26.8,21.4c10.6-0.4,14.8-6.9,27.6-6.9c12.9,0,16.6,6.9,27.8,6.7
|
||||
c11.6-0.2,18.9-10.4,26-20.8c8.1-11.8,11.4-23.3,11.6-23.9c-0.2-0.2-22.4-8.7-22.6-34.3c-0.2-21.4,17.5-31.6,18.3-32.2
|
||||
C123.3,42.9,107.7,41.3,102.3,40.9 M182.6,11.9v155.9h24.2v-53.3h33.5c30.6,0,52.1-21,52.1-51.4c0-30.4-21.1-51.2-51.3-51.2H182.6z
|
||||
M206.8,32.3h27.9c21,0,33,11.2,33,30.9c0,19.7-12,31-33.1,31h-27.8V32.3z M336.6,169c15.2,0,29.3-7.7,35.7-19.9h0.5v18.7h22.4V90.2
|
||||
c0-22.5-18-37-45.7-37c-25.7,0-44.7,14.7-45.4,34.9h21.8c1.8-9.6,10.7-15.9,22.9-15.9c14.8,0,23.1,6.9,23.1,19.6v8.6l-30.2,1.8
|
||||
c-28.1,1.7-43.3,13.2-43.3,33.2C298.4,155.6,314.1,169,336.6,169z M343.1,150.5c-12.9,0-21.1-6.2-21.1-15.7c0-9.8,7.9-15.5,23-16.4
|
||||
l26.9-1.7v8.8C371.9,140.1,359.5,150.5,343.1,150.5z M425.1,210.2c23.6,0,34.7-9,44.4-36.3L512,54.7h-24.6l-28.5,92.1h-0.5
|
||||
l-28.5-92.1h-25.3l41,113.5l-2.2,6.9c-3.7,11.7-9.7,16.2-20.4,16.2c-1.9,0-5.6-0.2-7.1-0.4v18.7C417.3,210,423.3,210.2,425.1,210.2z
|
||||
"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
27
src/components/PaymentPage/G-Pay.svg
Normal file
@ -0,0 +1,27 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 80 38.1" style="enable-background:new 0 0 80 38.1;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#5F6368;}
|
||||
.st1{fill:#4285F4;}
|
||||
.st2{fill:#34A853;}
|
||||
.st3{fill:#FBBC04;}
|
||||
.st4{fill:#EA4335;}
|
||||
</style>
|
||||
<path class="st0" d="M37.8,19.7V29h-3V6h7.8c1.9,0,3.7,0.7,5.1,2c1.4,1.2,2.1,3,2.1,4.9c0,1.9-0.7,3.6-2.1,4.9c-1.4,1.3-3.1,2-5.1,2
|
||||
L37.8,19.7L37.8,19.7z M37.8,8.8v8h5c1.1,0,2.2-0.4,2.9-1.2c1.6-1.5,1.6-4,0.1-5.5c0,0-0.1-0.1-0.1-0.1c-0.8-0.8-1.8-1.3-2.9-1.2
|
||||
L37.8,8.8L37.8,8.8z"/>
|
||||
<path class="st0" d="M56.7,12.8c2.2,0,3.9,0.6,5.2,1.8s1.9,2.8,1.9,4.8V29H61v-2.2h-0.1c-1.2,1.8-2.9,2.7-4.9,2.7
|
||||
c-1.7,0-3.2-0.5-4.4-1.5c-1.1-1-1.8-2.4-1.8-3.9c0-1.6,0.6-2.9,1.8-3.9c1.2-1,2.9-1.4,4.9-1.4c1.8,0,3.2,0.3,4.3,1v-0.7
|
||||
c0-1-0.4-2-1.2-2.6c-0.8-0.7-1.8-1.1-2.9-1.1c-1.7,0-3,0.7-3.9,2.1l-2.6-1.6C51.8,13.8,53.9,12.8,56.7,12.8z M52.9,24.2
|
||||
c0,0.8,0.4,1.5,1,1.9c0.7,0.5,1.5,0.8,2.3,0.8c1.2,0,2.4-0.5,3.3-1.4c1-0.9,1.5-2,1.5-3.2c-0.9-0.7-2.2-1.1-3.9-1.1
|
||||
c-1.2,0-2.2,0.3-3,0.9C53.3,22.6,52.9,23.3,52.9,24.2z"/>
|
||||
<path class="st0" d="M80,13.3l-9.9,22.7h-3l3.7-7.9l-6.5-14.7h3.2l4.7,11.3h0.1l4.6-11.3H80z"/>
|
||||
<path class="st1" d="M25.9,17.7c0-0.9-0.1-1.8-0.2-2.7H13.2v5.1h7.1c-0.3,1.6-1.2,3.1-2.6,4v3.3H22C24.5,25.1,25.9,21.7,25.9,17.7z"
|
||||
/>
|
||||
<path class="st2" d="M13.2,30.6c3.6,0,6.6-1.2,8.8-3.2l-4.3-3.3c-1.2,0.8-2.7,1.3-4.5,1.3c-3.4,0-6.4-2.3-7.4-5.5H1.4v3.4
|
||||
C3.7,27.8,8.2,30.6,13.2,30.6z"/>
|
||||
<path class="st3" d="M5.8,19.9c-0.6-1.6-0.6-3.4,0-5.1v-3.4H1.4c-1.9,3.7-1.9,8.1,0,11.9L5.8,19.9z"/>
|
||||
<path class="st4" d="M13.2,9.4c1.9,0,3.7,0.7,5.1,2l0,0l3.8-3.8c-2.4-2.2-5.6-3.5-8.8-3.4c-5,0-9.6,2.8-11.8,7.3l4.4,3.4
|
||||
C6.8,11.7,9.8,9.4,13.2,9.4z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.8 KiB |
BIN
src/components/PaymentPage/Google-Pay.png
Normal file
|
After Width: | Height: | Size: 74 KiB |
BIN
src/components/PaymentPage/Secure.png
Normal file
|
After Width: | Height: | Size: 65 KiB |
5
src/components/PaymentPage/card.svg
Normal file
@ -0,0 +1,5 @@
|
||||
<svg width="23" height="16" viewBox="0 0 23 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="0.5" width="22" height="16" rx="2" fill="white"/>
|
||||
<rect x="0.5" y="2.66406" width="22" height="2.66667" fill="#9FB8FF"/>
|
||||
<rect x="3" y="7.35938" width="17" height="2" rx="1" fill="#CEDBFF"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 303 B |
67
src/components/PaymentPage/index.tsx
Normal file
@ -0,0 +1,67 @@
|
||||
import { useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { useSelector } from 'react-redux'
|
||||
import { selectors } from '../../store'
|
||||
import { useAuth } from '../../auth'
|
||||
import { useApi, useApiCall, SubscriptionCheckout } from '../../api'
|
||||
import UserHeader from '../UserHeader'
|
||||
import Title from '../Title'
|
||||
import Loader from '../Loader'
|
||||
import MainButton from '../MainButton'
|
||||
import applePaySafeCheckout from './Apple-Pay.png'
|
||||
import gPaySafeCheckout from './Google-Pay.png'
|
||||
import secure from './Secure.png'
|
||||
import ApplePay from './Apple-Pay.svg'
|
||||
import GooglePay from './G-Pay.svg'
|
||||
import card from './card.svg'
|
||||
import routes from '../../routes'
|
||||
import './styles.css'
|
||||
|
||||
const isAndroid = () => /Android/i.test(navigator.userAgent)
|
||||
const isApple = () => /Macintosh|iPhone|iPad|iPod/i.test(navigator.userAgent)
|
||||
|
||||
function PaymentPage(): JSX.Element {
|
||||
const api = useApi()
|
||||
const { token } = useAuth()
|
||||
const { i18n } = useTranslation()
|
||||
const locale = i18n.language
|
||||
const navigate = useNavigate()
|
||||
const email = useSelector(selectors.selectEmail)
|
||||
const handleClick = () => navigate(routes.client.wallpaper())
|
||||
const loadData = useCallback(() => {
|
||||
return api.getSubscriptionItems({ locale, token })
|
||||
.then(({ item_prices }) => item_prices.find(({ id }) => id === 'aura-membership-2-week-USD'))
|
||||
.then((item) => api.getSubscriptionCheckout({ locale, token, itemPriceId: item?.id || '' }))
|
||||
}, [api, locale, token])
|
||||
const { data, isPending } = useApiCall<SubscriptionCheckout.Response>(loadData)
|
||||
console.log(data, isPending)
|
||||
return (
|
||||
<>
|
||||
<UserHeader email={email} />
|
||||
<section className='page'>
|
||||
<div className='page-header'>
|
||||
{isAndroid() && <img src={gPaySafeCheckout} alt='Guaranteed safe checkout' />}
|
||||
{isApple() && <img src={applePaySafeCheckout} alt='Guaranteed safe checkout' />}
|
||||
<img src={secure} alt='100% Secure' />
|
||||
</div>
|
||||
<Title variant='h1' className='mb-45'>Choose Payment Method</Title>
|
||||
{isPending ? <Loader /> : (
|
||||
<>
|
||||
<MainButton onClick={handleClick}>
|
||||
{isAndroid() && <img className='payment-btn' src={GooglePay} alt='Google Pay' />}
|
||||
{isApple() && <img className='payment-btn' src={ApplePay} alt='Apple Pay' />}
|
||||
</MainButton>
|
||||
<div className='payment-divider'>OR</div>
|
||||
<MainButton color='blue' onClick={handleClick}>
|
||||
<img className='payment-card' src={card} alt='Credit / Debit Card' />
|
||||
Credit / Debit Card
|
||||
</MainButton>
|
||||
</>
|
||||
)}
|
||||
</section>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default PaymentPage
|
||||
32
src/components/PaymentPage/styles.css
Normal file
@ -0,0 +1,32 @@
|
||||
.page-header {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
margin: 10px 0 30px;
|
||||
}
|
||||
|
||||
.page-header > img {
|
||||
max-width: 400px;
|
||||
width: 100%;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.page-header > img:last-child {
|
||||
max-width: 100px;
|
||||
}
|
||||
|
||||
.payment-card {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.payment-divider {
|
||||
width: 100%;
|
||||
height: 30px;
|
||||
font-size: 14px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.payment-btn {
|
||||
height: 25px;
|
||||
}
|
||||
@ -1,16 +1,19 @@
|
||||
import { useSelector } from 'react-redux'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { RootState } from '../../store'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { selectors } from '../../store'
|
||||
import MainButton from '../MainButton'
|
||||
import Policy from '../Policy'
|
||||
import Countdown from '../Countdown'
|
||||
import Payment, { Currency, Locale } from '../Payment'
|
||||
import UserHeader from '../UserHeader'
|
||||
import CallToAction from '../CallToAction'
|
||||
import routes from '../../routes'
|
||||
|
||||
function SubscriptionPage(): JSX.Element {
|
||||
const { t } = useTranslation()
|
||||
const email = useSelector((state: RootState) => state.form.email)
|
||||
const navigate = useNavigate()
|
||||
const email = useSelector(selectors.selectEmail)
|
||||
const links = [
|
||||
{ text: 'Subscription policy', href: 'https://aura.wit.life/' },
|
||||
]
|
||||
@ -23,7 +26,7 @@ function SubscriptionPage(): JSX.Element {
|
||||
description: '2-Week Plan',
|
||||
},
|
||||
]
|
||||
const handleClick = () => console.log('What we will do?')
|
||||
const handleClick = () => navigate(routes.client.paymentMethod())
|
||||
return (
|
||||
<>
|
||||
<UserHeader email={email} />
|
||||
|
||||
BIN
src/components/WallpaperPage/Dowload.png
Normal file
|
After Width: | Height: | Size: 20 KiB |
61
src/components/WallpaperPage/index.tsx
Normal file
@ -0,0 +1,61 @@
|
||||
import { useCallback } from 'react'
|
||||
import { useApi, useApiCall, Assets, DailyForecasts } from '../../api'
|
||||
import { useAuth } from '../../auth'
|
||||
import { saveFile } from './utils'
|
||||
import Loader, { LoaderColor } from '../Loader'
|
||||
import './styles.css'
|
||||
|
||||
type Forecasts = DailyForecasts.Forecast[]
|
||||
type PersonalAssets = Assets.Asset[]
|
||||
interface WallpaperData {
|
||||
assets: PersonalAssets
|
||||
forecasts: Forecasts
|
||||
}
|
||||
|
||||
function WallpaperPage(): JSX.Element {
|
||||
const api = useApi()
|
||||
const { user, token } = useAuth()
|
||||
const category = user?.profile.sign?.sign || ''
|
||||
const loadData = useCallback(() => {
|
||||
return Promise.all([
|
||||
api.getAssets({ token, category }),
|
||||
api.getDailyForecasts({ token }),
|
||||
])
|
||||
.then(([{ assets }, { user_daily_forecast }]) => ({
|
||||
assets,
|
||||
forecasts: user_daily_forecast.forecasts,
|
||||
}))
|
||||
}, [api, category, token])
|
||||
const { data, isPending } = useApiCall<WallpaperData>(loadData)
|
||||
const { assets, forecasts } = data
|
||||
const asset = assets?.at(0)
|
||||
|
||||
const handleDownload = () => {
|
||||
asset && saveFile(asset.url, asset.asset_data.id)
|
||||
}
|
||||
|
||||
return (
|
||||
<section className='wallpaper-page'>
|
||||
<div className='wallpaper-image'>
|
||||
{asset ? <img src={asset.url} alt={category} /> : null}
|
||||
{asset ? <div className='btn-download' onClick={handleDownload}></div> : null}
|
||||
{isPending ? <Loader color={LoaderColor.White}/> : null}
|
||||
</div>
|
||||
<div className='wallpaper-content'>
|
||||
{isPending ? null : (
|
||||
<>
|
||||
<h1 className='wallpaper-title'>Analysis of personal background</h1>
|
||||
{forecasts.map((forecast) => (
|
||||
<div key={forecast.category_name} className='wallpaper-forecast'>
|
||||
<h2 className='wallpaper-subtitle'>{forecast.category}</h2>
|
||||
<p className='wallpaper-text'>{forecast.body}</p>
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
export default WallpaperPage
|
||||
68
src/components/WallpaperPage/styles.css
Normal file
@ -0,0 +1,68 @@
|
||||
.wallpaper-page {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.wallpaper-image > img {
|
||||
width: 100%;
|
||||
height: 500px;
|
||||
object-fit: cover;
|
||||
object-position: center;
|
||||
}
|
||||
|
||||
.wallpaper-image {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background-color: #04040a;
|
||||
width: 100%;
|
||||
height: 500px;
|
||||
}
|
||||
|
||||
.wallpaper-content {
|
||||
color: #fff;
|
||||
background-color: #04040a;
|
||||
line-height: 1.3;
|
||||
padding-bottom: 15px;
|
||||
position: relative;
|
||||
min-height: calc(100vh - 500px);
|
||||
}
|
||||
|
||||
.wallpaper-content::before {
|
||||
content: '';
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: -30px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 30px;
|
||||
background: linear-gradient(0deg, #04040a, transparent);
|
||||
}
|
||||
|
||||
.wallpaper-title,
|
||||
.wallpaper-subtitle {
|
||||
color: #ea445a;
|
||||
}
|
||||
|
||||
.wallpaper-subtitle {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.wallpaper-title,
|
||||
.wallpaper-forecast {
|
||||
padding: 0 32px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.btn-download {
|
||||
position: absolute;
|
||||
cursor: pointer;
|
||||
top: 28px;
|
||||
right: 32px;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
background-image: url(./Dowload.png);
|
||||
background-position: center;
|
||||
background-size: cover;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
13
src/components/WallpaperPage/utils.ts
Normal file
@ -0,0 +1,13 @@
|
||||
export const saveFile = (url: string, href = 'file.ext'): void => {
|
||||
fetch(url, { mode: 'no-cors' })
|
||||
.then((response) => response.arrayBuffer())
|
||||
.then((buffer) => {
|
||||
const url = URL.createObjectURL(new Blob([buffer]))
|
||||
const link = document.createElement('a')
|
||||
link.href = url
|
||||
link.setAttribute('download', href)
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
document.body.removeChild(link)
|
||||
})
|
||||
}
|
||||
@ -55,6 +55,7 @@ ol,ul {
|
||||
}
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
@ -98,6 +99,10 @@ a,button,div,input,select,textarea {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.mb-45 {
|
||||
margin-bottom: 45px;
|
||||
}
|
||||
|
||||
.pa {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
@ -19,13 +19,18 @@ const routes = {
|
||||
token: () => [apiHost, prefix, 'auth', 'token.json'].join('/'),
|
||||
assets: (category: string) => [apiHost, prefix, 'assets', 'categories', `${category}.json`].join('/'),
|
||||
assetCategories: () => [apiHost, prefix, 'assets', 'categories.json'].join('/'),
|
||||
dailyForecasts: () => [apiHost, prefix, 'user', 'daily_forecasts.json'].join('/'),
|
||||
dailyForecasts: () => [apiHost, prefix, 'user', 'daily_forecast.json'].join('/'),
|
||||
auras: () => [apiHost, prefix, 'user', 'aura.json'].join('/'),
|
||||
subscriptionItems: () => [apiHost, prefix, 'user', 'subscription', 'item_prices.json'].join('/'),
|
||||
subscriptionCheckout: () => [apiHost, prefix, 'user', 'subscription', 'checkout', 'new.json'].join('/'),
|
||||
},
|
||||
}
|
||||
|
||||
export const entrypoints = [routes.client.root(), routes.client.birthday()]
|
||||
export const entrypoints = [routes.client.root(), routes.client.birthday(), routes.client.wallpaper()]
|
||||
export const isEntrypoint = (path: string) => entrypoints.includes(path)
|
||||
export const isNotEntrypoint = (path: string) => !isEntrypoint(path)
|
||||
export const withNavigationRoutes = [routes.client.wallpaper()]
|
||||
export const hasNavigation = (path: string) => withNavigationRoutes.includes(path)
|
||||
export const hasNoNavigation = (path: string) => !hasNavigation(path)
|
||||
|
||||
export default routes
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { createSlice } from '@reduxjs/toolkit'
|
||||
import { createSlice, createSelector } from '@reduxjs/toolkit'
|
||||
import type { PayloadAction } from '@reduxjs/toolkit'
|
||||
import { SignupForm } from '../types'
|
||||
|
||||
@ -29,4 +29,32 @@ const formSlice = createSlice({
|
||||
})
|
||||
|
||||
export const { actions } = formSlice
|
||||
const selectForm = createSelector(
|
||||
(state: { form: SignupForm }) => state.form,
|
||||
(form) => form
|
||||
)
|
||||
const selectEmail = createSelector(
|
||||
(state: { form: SignupForm }) => state.form.email,
|
||||
(email) => email
|
||||
)
|
||||
const selectBirthdate = createSelector(
|
||||
(state: { form: SignupForm }) => state.form.birthdate,
|
||||
(birthdate) => birthdate
|
||||
)
|
||||
const selectBirthtime = createSelector(
|
||||
(state: { form: SignupForm }) => state.form.birthtime,
|
||||
(birthtime) => birthtime
|
||||
)
|
||||
const selectBirthday = createSelector(
|
||||
selectBirthdate,
|
||||
selectBirthtime,
|
||||
(birthdate, birthtime) => `${birthdate} ${birthtime}`
|
||||
)
|
||||
export const selectors = {
|
||||
selectForm,
|
||||
selectEmail,
|
||||
selectBirthdate,
|
||||
selectBirthtime,
|
||||
selectBirthday,
|
||||
}
|
||||
export default formSlice.reducer
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
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 token, { actions as tokenActions, selectToken } from './token'
|
||||
import user, { actions as userActions, selectUser } from './user'
|
||||
import form, { actions as formActions, selectors as formSelectors } from './form'
|
||||
import { loadStore, backupStore } from './storageHelper'
|
||||
|
||||
const preloadedState = loadStore()
|
||||
@ -12,6 +12,7 @@ export const actions = {
|
||||
form: formActions,
|
||||
reset: createAction('reset'),
|
||||
}
|
||||
export const selectors = { selectToken, selectUser, ...formSelectors }
|
||||
export type RootState = ReturnType<typeof reducer>
|
||||
export const store = configureStore({
|
||||
reducer,
|
||||
|
||||
@ -10,6 +10,7 @@ export const backupStore = (store: ToolkitStore) => {
|
||||
localStorage.setItem(storageKey, serializedState)
|
||||
} catch (err) {
|
||||
// nothing to do
|
||||
console.error('Error while saving state', err)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { createSlice } from '@reduxjs/toolkit'
|
||||
import { createSlice, createSelector } from '@reduxjs/toolkit'
|
||||
import type { PayloadAction } from '@reduxjs/toolkit'
|
||||
import type { AuthToken } from '../api'
|
||||
|
||||
@ -17,4 +17,8 @@ const authTokenSlice = createSlice({
|
||||
})
|
||||
|
||||
export const { actions } = authTokenSlice
|
||||
export const selectToken = createSelector(
|
||||
(state: { token: AuthToken }) => state.token,
|
||||
(token) => token
|
||||
)
|
||||
export default authTokenSlice.reducer
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
import { createSlice } from '@reduxjs/toolkit'
|
||||
import { createSlice, createSelector } from '@reduxjs/toolkit'
|
||||
import type { PayloadAction } from '@reduxjs/toolkit'
|
||||
import type { User } from '../api'
|
||||
import { User } from '../api'
|
||||
import { getClientLocale, getClientTimezone } from '../locales'
|
||||
|
||||
const initialState: User = {
|
||||
const initialState: User.User = {
|
||||
id: undefined,
|
||||
username: null,
|
||||
email: '',
|
||||
@ -34,7 +34,7 @@ const userSlice = createSlice({
|
||||
name: 'user',
|
||||
initialState,
|
||||
reducers: {
|
||||
update(state, action: PayloadAction<Partial<User>>) {
|
||||
update(state, action: PayloadAction<Partial<User.User>>) {
|
||||
return { ...state, ...action.payload }
|
||||
},
|
||||
},
|
||||
@ -42,4 +42,8 @@ const userSlice = createSlice({
|
||||
})
|
||||
|
||||
export const { actions } = userSlice
|
||||
export const selectUser = createSelector(
|
||||
(state: { user: User.User }) => state.user,
|
||||
(user) => user
|
||||
)
|
||||
export default userSlice.reducer
|
||||
|
||||