diff --git a/src/api/ApiContext.ts b/src/api/ApiContext.ts index bccb55c..6761fbb 100644 --- a/src/api/ApiContext.ts +++ b/src/api/ApiContext.ts @@ -8,6 +8,8 @@ import { Assets, AssetCategories, DailyForecasts, + SubscriptionItems, + SubscriptionCheckout, } from './resources' export interface ApiContextValue { @@ -19,6 +21,8 @@ export interface ApiContextValue { getAssetCategories: ReturnType> getDailyForecasts: ReturnType> getAuras: ReturnType> + getSubscriptionItems: ReturnType> + getSubscriptionCheckout: ReturnType> } export const ApiContext = createContext({} as ApiContextValue) diff --git a/src/api/api.ts b/src/api/api.ts index a09c175..0567009 100644 --- a/src/api/api.ts +++ b/src/api/api.ts @@ -8,17 +8,23 @@ import { Assets, AssetCategories, DailyForecasts, + SubscriptionItems, + SubscriptionCheckout, } from './resources' -export function createApi(): ApiContextValue { - return { - auth: createMethod(AuthTokens.createRequest), - getElements: createMethod(Elements.createRequest), - getUser: createMethod(User.createGetRequest), - updateUser: createMethod(User.createPatchRequest), - getAssets: createMethod(Assets.createRequest), - getAssetCategories: createMethod(AssetCategories.createRequest), - getDailyForecasts: createMethod(DailyForecasts.createRequest), - getAuras: createMethod(Auras.createRequest), - } +const api = { + auth: createMethod(AuthTokens.createRequest), + getElements: createMethod(Elements.createRequest), + getUser: createMethod(User.createGetRequest), + updateUser: createMethod(User.createPatchRequest), + getAssets: createMethod(Assets.createRequest), + getAssetCategories: createMethod(AssetCategories.createRequest), + getDailyForecasts: createMethod(DailyForecasts.createRequest), + getAuras: createMethod(Auras.createRequest), + getSubscriptionItems: createMethod(SubscriptionItems.createRequest), + getSubscriptionCheckout: createMethod(SubscriptionCheckout.createRequest), +} + +export function createApi(): ApiContextValue { + return api } diff --git a/src/api/index.ts b/src/api/index.ts index b1ee7c6..6f66119 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -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' diff --git a/src/api/resources/UserSubscriptionCheckout.ts b/src/api/resources/UserSubscriptionCheckout.ts new file mode 100644 index 0000000..6cf417f --- /dev/null +++ b/src/api/resources/UserSubscriptionCheckout.ts @@ -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) }) +} diff --git a/src/api/resources/UserSubscriptionItemPrices.ts b/src/api/resources/UserSubscriptionItemPrices.ts new file mode 100644 index 0000000..9af780f --- /dev/null +++ b/src/api/resources/UserSubscriptionItemPrices.ts @@ -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) }) +} diff --git a/src/api/resources/index.ts b/src/api/resources/index.ts index b1519cd..e1b77d6 100644 --- a/src/api/resources/index.ts +++ b/src/api/resources/index.ts @@ -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' diff --git a/src/api/useApiCall.ts b/src/api/useApiCall.ts new file mode 100644 index 0000000..2048539 --- /dev/null +++ b/src/api/useApiCall.ts @@ -0,0 +1,24 @@ +import { useState, useEffect } from "react"; + +interface HookResult { + isPending: boolean + error: Error | null + data: T +} + +type ApiMethod = () => Promise + +export function useApiCall(apiMethod: ApiMethod): HookResult { + const [data, setData] = useState({} as T) + const [error, setError] = useState(null) + const [isPending, setIsPending] = useState(true) + + useEffect(() => { + apiMethod() + .then((data: T) => setData(data)) + .catch((error: Error) => setError(error)) + .finally(() => setIsPending(false)) + }, [apiMethod]) + + return { isPending, error, data } +} diff --git a/src/auth/AuthContext.ts b/src/auth/AuthContext.ts index 528b138..e7c16f6 100644 --- a/src/auth/AuthContext.ts +++ b/src/auth/AuthContext.ts @@ -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 - addBirthday: (birthday: string, token: AuthToken) => Promise + signUp: (token: AuthToken, user: User.User) => AuthToken } export const AuthContext = createContext({} as AuthContextValue) diff --git a/src/auth/AuthProvider.tsx b/src/auth/AuthProvider.tsx index 63a33e3..c861e85 100644 --- a/src/auth/AuthProvider.tsx +++ b/src/auth/AuthProvider.tsx @@ -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): 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 => { - 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 => { - 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 ( {children} diff --git a/src/components/App/index.tsx b/src/components/App/index.tsx index 0f9b9f8..afa7bd3 100644 --- a/src/components/App/index.tsx +++ b/src/components/App/index.tsx @@ -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 ( + + }> + + } /> + } /> + } /> + } /> + } /> + }> + } /> + } /> + } /> + + } /> + + + ) +} + +function Layout(): JSX.Element { + const location = useLocation() + const showNavbar = hasNavigation(location.pathname) + const [isMenuOpen, setIsMenuOpen] = useState(false) return (
-
-
- - - } /> - } /> - } /> - } /> - } /> - }> - } /> - - } /> - -
+
setIsMenuOpen(true)}/> +
+ { showNavbar ? setIsMenuOpen(false)} /> : null}
) } function PrivateOutlet(): JSX.Element { const { user } = useAuth() - return user ? : + return user ? : +} + +function SkipStep(): JSX.Element { + const { user } = useAuth() + return user ? : } export default App diff --git a/src/components/App/styles.css b/src/components/App/styles.css index 9421b69..3e1902c 100644 --- a/src/components/App/styles.css +++ b/src/components/App/styles.css @@ -22,3 +22,7 @@ overflow: hidden; padding: 15px 32px; } + +.page-responsive { + width: 100%; +} diff --git a/src/components/EmailEnterPage/index.tsx b/src/components/EmailEnterPage/index.tsx index 7b3a25d..df0251e 100644 --- a/src/components/EmailEnterPage/index.tsx +++ b/src/components/EmailEnterPage/index.tsx @@ -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(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)) } diff --git a/src/components/Header/index.tsx b/src/components/Header/index.tsx index 258fd64..859bb26 100644 --- a/src/components/Header/index.tsx +++ b/src/components/Header/index.tsx @@ -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(null); const [isNavigated, setIsNavigated] = useState(false); const showBackButton = isNotEntrypoint(location.pathname) + const showMenuButton = hasNavigation(location.pathname) useEffect(() => { if (!initialPath) { @@ -36,6 +42,9 @@ function Header(): JSX.Element { { showBackButton ? : null } logo {t('appName')} + {showMenuButton ?
+ menu +
: null} ) } diff --git a/src/components/Header/menu.png b/src/components/Header/menu.png new file mode 100644 index 0000000..b0b04c2 Binary files /dev/null and b/src/components/Header/menu.png differ diff --git a/src/components/Header/styles.css b/src/components/Header/styles.css index 0c5773b..5da34bc 100644 --- a/src/components/Header/styles.css +++ b/src/components/Header/styles.css @@ -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; +} diff --git a/src/components/Navbar/index.tsx b/src/components/Navbar/index.tsx new file mode 100644 index 0000000..27f5308 --- /dev/null +++ b/src/components/Navbar/index.tsx @@ -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 ( + + ) +} + +export default Navbar diff --git a/src/components/Navbar/styles.css b/src/components/Navbar/styles.css new file mode 100644 index 0000000..35b8b60 --- /dev/null +++ b/src/components/Navbar/styles.css @@ -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); +} \ No newline at end of file diff --git a/src/components/PaymentPage/Apple-Pay.png b/src/components/PaymentPage/Apple-Pay.png new file mode 100644 index 0000000..2e7c7dd Binary files /dev/null and b/src/components/PaymentPage/Apple-Pay.png differ diff --git a/src/components/PaymentPage/Apple-Pay.svg b/src/components/PaymentPage/Apple-Pay.svg new file mode 100644 index 0000000..7baf5e2 --- /dev/null +++ b/src/components/PaymentPage/Apple-Pay.svg @@ -0,0 +1,15 @@ + + + + diff --git a/src/components/PaymentPage/G-Pay.svg b/src/components/PaymentPage/G-Pay.svg new file mode 100644 index 0000000..f01db7a --- /dev/null +++ b/src/components/PaymentPage/G-Pay.svg @@ -0,0 +1,27 @@ + + + + + + + + + + + diff --git a/src/components/PaymentPage/Google-Pay.png b/src/components/PaymentPage/Google-Pay.png new file mode 100644 index 0000000..7973303 Binary files /dev/null and b/src/components/PaymentPage/Google-Pay.png differ diff --git a/src/components/PaymentPage/Secure.png b/src/components/PaymentPage/Secure.png new file mode 100644 index 0000000..32c7725 Binary files /dev/null and b/src/components/PaymentPage/Secure.png differ diff --git a/src/components/PaymentPage/card.svg b/src/components/PaymentPage/card.svg new file mode 100644 index 0000000..0568eee --- /dev/null +++ b/src/components/PaymentPage/card.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/components/PaymentPage/index.tsx b/src/components/PaymentPage/index.tsx new file mode 100644 index 0000000..fef493d --- /dev/null +++ b/src/components/PaymentPage/index.tsx @@ -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(loadData) + console.log(data, isPending) + return ( + <> + +
+
+ {isAndroid() && Guaranteed safe checkout} + {isApple() && Guaranteed safe checkout} + 100% Secure +
+ Choose Payment Method + {isPending ? : ( + <> + + {isAndroid() && Google Pay} + {isApple() && Apple Pay} + +
OR
+ + Credit / Debit Card + Credit / Debit Card + + + )} +
+ + ) +} + +export default PaymentPage diff --git a/src/components/PaymentPage/styles.css b/src/components/PaymentPage/styles.css new file mode 100644 index 0000000..31513a9 --- /dev/null +++ b/src/components/PaymentPage/styles.css @@ -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; +} diff --git a/src/components/SubscriptionPage/index.tsx b/src/components/SubscriptionPage/index.tsx index a7edb8d..08f6ac9 100644 --- a/src/components/SubscriptionPage/index.tsx +++ b/src/components/SubscriptionPage/index.tsx @@ -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 ( <> diff --git a/src/components/WallpaperPage/Dowload.png b/src/components/WallpaperPage/Dowload.png new file mode 100644 index 0000000..1885e46 Binary files /dev/null and b/src/components/WallpaperPage/Dowload.png differ diff --git a/src/components/WallpaperPage/index.tsx b/src/components/WallpaperPage/index.tsx new file mode 100644 index 0000000..a471d80 --- /dev/null +++ b/src/components/WallpaperPage/index.tsx @@ -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(loadData) + const { assets, forecasts } = data + const asset = assets?.at(0) + + const handleDownload = () => { + asset && saveFile(asset.url, asset.asset_data.id) + } + + return ( +
+
+ {asset ? {category} : null} + {asset ?
: null} + {isPending ? : null} +
+
+ {isPending ? null : ( + <> +

Analysis of personal background

+ {forecasts.map((forecast) => ( +
+

{forecast.category}

+

{forecast.body}

+
+ ))} + + )} +
+
+ ) +} + +export default WallpaperPage diff --git a/src/components/WallpaperPage/styles.css b/src/components/WallpaperPage/styles.css new file mode 100644 index 0000000..11571a6 --- /dev/null +++ b/src/components/WallpaperPage/styles.css @@ -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; +} \ No newline at end of file diff --git a/src/components/WallpaperPage/utils.ts b/src/components/WallpaperPage/utils.ts new file mode 100644 index 0000000..bd421e3 --- /dev/null +++ b/src/components/WallpaperPage/utils.ts @@ -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) + }) +} diff --git a/src/index.css b/src/index.css index e154428..28b8d24 100644 --- a/src/index.css +++ b/src/index.css @@ -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; } diff --git a/src/routes.ts b/src/routes.ts index a3fd361..f8c7b47 100644 --- a/src/routes.ts +++ b/src/routes.ts @@ -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 diff --git a/src/store/form.ts b/src/store/form.ts index a5a421f..d0f8e18 100644 --- a/src/store/form.ts +++ b/src/store/form.ts @@ -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 diff --git a/src/store/index.ts b/src/store/index.ts index 25c52ca..27ab515 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -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 export const store = configureStore({ reducer, diff --git a/src/store/storageHelper.ts b/src/store/storageHelper.ts index 1d338b1..ff8b866 100644 --- a/src/store/storageHelper.ts +++ b/src/store/storageHelper.ts @@ -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) } } diff --git a/src/store/token.ts b/src/store/token.ts index 2eb3941..8cd61b7 100644 --- a/src/store/token.ts +++ b/src/store/token.ts @@ -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 diff --git a/src/store/user.ts b/src/store/user.ts index 41f9547..f1c332d 100644 --- a/src/store/user.ts +++ b/src/store/user.ts @@ -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>) { + update(state, action: PayloadAction>) { 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