fix: add subscription page and made edits

This commit is contained in:
gofnnp 2023-09-14 22:51:06 +04:00
parent 6213bebda2
commit 792f64580b
12 changed files with 302 additions and 143 deletions

View File

@ -58,9 +58,9 @@ function App(): JSX.Element {
<Route path={routes.client.priceList()} element={<PriceListPage />} /> <Route path={routes.client.priceList()} element={<PriceListPage />} />
<Route path={routes.client.home()} element={<HomePage />} /> <Route path={routes.client.home()} element={<HomePage />} />
<Route path={routes.client.breathResult()} element={<UserCallbacksPage />} /> <Route path={routes.client.breathResult()} element={<UserCallbacksPage />} />
<Route path={routes.client.subscription()} element={<SubscriptionPage />} />
<Route path={routes.client.paymentMethod()} element={<PaymentPage />} />
<Route element={<PrivateOutlet />}> <Route element={<PrivateOutlet />}>
<Route path={routes.client.subscription()} element={<SubscriptionPage />} />
<Route path={routes.client.paymentMethod()} element={<PaymentPage />} />
<Route path={routes.client.wallpaper()} element={<ProtectWallpaperPage />} /> <Route path={routes.client.wallpaper()} element={<ProtectWallpaperPage />} />
</Route> </Route>
<Route path="*" element={<NotFoundPage />} /> <Route path="*" element={<NotFoundPage />} />

View File

@ -1,48 +1,53 @@
import styles from "./styles.module.css"; import styles from "./styles.module.css";
import BreathCircle from "../BreathCircle";
import { useCallback, useEffect, useState } from "react"; import { useCallback, useEffect, useState } from "react";
import { UserCallbacks, useApi, useApiCall } from "@/api"; import { UserCallbacks, useApi, useApiCall } from "@/api";
import { Asset } from "@/api/resources/Assets"; import { Asset } from "@/api/resources/Assets";
import { useDispatch, useSelector } from "react-redux"; import { useDispatch } from "react-redux";
import { actions, selectors } from "@/store"; import { actions } from "@/store";
import {
getCategoryIdByZodiacSign,
getZodiacSignByDate,
} from "@/services/zodiac-sign";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { getRandomArbitrary } from "@/services/random-value"; import { getRandomArbitrary } from "@/services/random-value";
import FullScreenModal from "../FullScreenModal"; import FullScreenModal from "../FullScreenModal";
import StartBreathModalChild from "../StartBreathModalChild"; import StartBreathModalChild from "../StartBreathModalChild";
import Title from "../Title";
import { useNavigate } from "react-router-dom";
import routes from "@/routes";
function BreathPage(): JSX.Element { function BreathPage(): JSX.Element {
const { i18n } = useTranslation(); const { t } = useTranslation();
const locale = i18n.language;
const birthdate = useSelector(selectors.selectBirthdate);
const zodiacSign = getZodiacSignByDate(birthdate);
const [asset, setAsset] = useState<Asset>(); const [asset, setAsset] = useState<Asset>();
const [isOpenModal, setIsOpenModal] = useState<boolean>(true); const [isOpenModal, setIsOpenModal] = useState<boolean>(true);
const api = useApi(); const api = useApi();
const dispatch = useDispatch(); const dispatch = useDispatch();
const navigate = useNavigate();
const assetsData = useCallback(async () => { const assetsData = useCallback(async () => {
const { asset_categories } = await api.getAssetCategories({ locale });
const categoryId = getCategoryIdByZodiacSign(zodiacSign, asset_categories);
const { assets } = await api.getAssets({ const { assets } = await api.getAssets({
category: String(categoryId || "1"), category: String("au"),
}); });
return assets; return assets;
}, [api, locale, zodiacSign]); }, [api]);
const { const {
data, data,
// isPending
} = useApiCall<Asset[]>(assetsData); } = useApiCall<Asset[]>(assetsData);
useEffect(() => {
if (isOpenModal) return;
const timeOut = setTimeout(() => {
navigate(routes.client.breathResult());
}, 50_000);
return () => {
clearTimeout(timeOut);
};
}, [navigate, isOpenModal]);
useEffect(() => { useEffect(() => {
if (data) { if (data) {
setAsset(data[getRandomArbitrary(0, data?.length || 0)]); setAsset(data[getRandomArbitrary(0, data?.length || 0)]);
} }
}, [data]); }, [data, isOpenModal]);
const beginBreath = () => { const beginBreath = () => {
setIsOpenModal(false); setIsOpenModal(false);
@ -89,12 +94,29 @@ function BreathPage(): JSX.Element {
<> <>
<section <section
className={`${styles.page} page`} className={`${styles.page} page`}
style={{ backgroundImage: `url(${asset?.url})` }} style={{
backgroundImage: !isOpenModal ? `url(${asset?.url})` : "none",
}}
> >
<FullScreenModal isOpen={isOpenModal}> <FullScreenModal isOpen={isOpenModal}>
<StartBreathModalChild handleBegin={beginBreath} /> <StartBreathModalChild handleBegin={beginBreath} />
</FullScreenModal> </FullScreenModal>
{!isOpenModal && <BreathCircle />} {!isOpenModal && (
<div className={styles["text-container"]}>
<Title
variant="h2"
className={`${styles.text} ${styles["breath-in"]}`}
>
{t("breathIn")}
</Title>
<Title
variant="h2"
className={`${styles.text} ${styles["breath-out"]}`}
>
{t("breathOut")}
</Title>
</div>
)}
</section> </section>
</> </>
); );

View File

@ -1,12 +1,80 @@
.page { .page {
position: relative; position: relative;
height: calc(100vh - 50px); height: calc(100vh - 50px);
flex: auto; display: flex;
justify-content: center; justify-content: center;
display: grid; align-items: center;
grid-template-rows: 1fr 96px; background-color: #01010b;
justify-items: center; background-size: cover;
background-color: #01010b; background-repeat: no-repeat;
background-size: cover; background-position: center;
background-repeat: no-repeat; flex: auto;
} }
.text-container {
position: relative;
width: 138px;
height: 200px;
}
.text {
position: absolute;
bottom: 0;
color: #fff;
will-change: scale, opacity;
}
.breath-in {
animation-name: breath-in;
animation-duration: 10s;
animation-iteration-count: infinite;
animation-timing-function: linear;
}
.breath-out {
animation-name: breath-out;
animation-duration: 10s;
animation-iteration-count: infinite;
animation-timing-function: linear;
}
@keyframes breath-in {
0% {
opacity: 0;
scale: 1;
}
10% {
opacity: 1;
}
40% {
opacity: 1;
}
50% {
opacity: 0;
scale: 2;
}
100% {
opacity: 0;
}
}
@keyframes breath-out {
0% {
opacity: 0;
scale: 1;
}
50% {
opacity: 0;
scale: 2;
}
60% {
opacity: 1;
}
90% {
opacity: 1;
}
100% {
opacity: 0;
scale: 1;
}
}

View File

@ -21,7 +21,7 @@ function CompatibilityPage(): JSX.Element {
const [isDisabledDate, setIsDisabledDate] = useState(true); const [isDisabledDate, setIsDisabledDate] = useState(true);
const [name, setName] = useState<string>(''); const [name, setName] = useState<string>('');
const [date, setDate] = useState<string | IDate>(''); const [date, setDate] = useState<string | IDate>('');
const [compatCategory, setCompatCategory] = useState(2); const [compatCategory, setCompatCategory] = useState(1);
const handleNext = () => { const handleNext = () => {
dispatch(actions.compatibility.update({ dispatch(actions.compatibility.update({
rightUser: { rightUser: {

View File

@ -11,10 +11,11 @@ type HeaderProps = {
openMenu?: () => void openMenu?: () => void
showBack?: boolean showBack?: boolean
showCross?: boolean showCross?: boolean
classCross?: CSSModuleClasses | string
clickCross?: () => void clickCross?: () => void
} }
function Header({ openMenu, showBack, showCross, clickCross = () => {undefined}, ...props }: HeaderProps & React.HTMLAttributes<HTMLDivElement>): JSX.Element { function Header({ openMenu, showBack, showCross, classCross, clickCross = () => {undefined}, ...props }: HeaderProps & React.HTMLAttributes<HTMLDivElement>): JSX.Element {
const { t } = useTranslation() const { t } = useTranslation()
const navigate = useNavigate() const navigate = useNavigate()
const location = useLocation() const location = useLocation()
@ -44,9 +45,11 @@ function Header({ openMenu, showBack, showCross, clickCross = () => {undefined},
return ( return (
<header className={styles.header} {...props}> <header className={styles.header} {...props}>
{ (showBackButton || showBack) ? <BackButton className='pa' onClick={goBack} /> : null } { (showBackButton || showBack) ? <BackButton className='pa' onClick={goBack} /> : null }
<img src={iconUrl} alt="logo" width="40" height="40" /> <div className={styles['header__logo-container']}>
<span className={styles["header__title"]}>{t('app_name')}</span> <img src={iconUrl} alt="logo" width="40" height="40" />
{(showCrossButton || showCross) ? <img className={styles.cross} src="/cross.png" alt="Cross" onClick={clickCross} /> : null} <span className={styles["header__title"]}>{t('app_name')}</span>
</div>
{(showCrossButton || showCross) ? <img className={`${styles.cross} ${classCross || ''}`} src="/cross.png" alt="Cross" onClick={clickCross} /> : null}
{showMenuButton ? <div className={styles["header__menu-btn"]} onClick={openMenu}> {showMenuButton ? <div className={styles["header__menu-btn"]} onClick={openMenu}>
<img src={menuUrl} alt="menu" width="40" height="40" /> <img src={menuUrl} alt="menu" width="40" height="40" />
</div> : null} </div> : null}

View File

@ -36,3 +36,9 @@
height: 33%; height: 33%;
width: auto; width: auto;
} }
.header__logo-container {
display: flex;
flex-direction: row;
align-items: center;
}

View File

@ -10,7 +10,7 @@
background-size: calc(100% + 186px); background-size: calc(100% + 186px);
background-position: center; background-position: center;
background-repeat: no-repeat; background-repeat: no-repeat;
padding: 16px 12px; padding: 16px 12px 0;
overflow-y: scroll; overflow-y: scroll;
} }
@ -62,11 +62,11 @@
background-repeat: no-repeat; background-repeat: no-repeat;
background-position: center center; background-position: center center;
-webkit-backdrop-filter: blur(14px); -webkit-backdrop-filter: blur(14px);
background-color: #ffffff69; background-color: #696969;
backdrop-filter: blur(14px); backdrop-filter: blur(14px);
border-radius: 100%; border-radius: 100%;
cursor: pointer; cursor: pointer;
background-blend-mode: exclusion; /* background-blend-mode: exclusion; */
} }
.header__save > a { .header__save > a {
@ -108,11 +108,28 @@
} }
.content__daily-forecast-body { .content__daily-forecast-body {
color: #a4a4a4; color: #cbcbcb;
line-height: 130%; line-height: 130%;
font-weight: 500; font-weight: 500;
} }
.content__daily-forecast {
display: flex;
flex-direction: column;
gap: 12px;
}
.content__daily-forecast-item {
-webkit-backdrop-filter: blur(14px);
backdrop-filter: blur(14px);
padding: 12px;
box-shadow: inset 0px 0px 25px rgba(0,0,0,0.5);
background-color: #00000094;
border-radius: 18px;
width: 100vw;
/* margin-left: 13px; */
}
@keyframes pulse { @keyframes pulse {
0% { 0% {
transform: scale(0.9); transform: scale(0.9);

View File

@ -16,7 +16,7 @@ function PriceListPage(): JSX.Element {
const email = useSelector(selectors.selectEmail) const email = useSelector(selectors.selectEmail)
const handleNext = () => { const handleNext = () => {
navigate(routes.client.home()) navigate(routes.client.subscription())
} }
return ( return (

View File

@ -1,49 +1,60 @@
import { useSelector } from 'react-redux' import { useSelector } from "react-redux";
import { useTranslation } from 'react-i18next' import { useTranslation } from "react-i18next";
import { useNavigate } from 'react-router-dom' import { useNavigate } from "react-router-dom";
import { selectors } from '@/store' import { selectors } from "@/store";
import MainButton from '../MainButton' import MainButton from "../MainButton";
import Policy from '../Policy' import Policy from "../Policy";
import Countdown from '../Countdown' import Countdown from "../Countdown";
import PaymentTable, { Currency, Locale } from '../PaymentTable' import PaymentTable, { Currency, Locale } from "../PaymentTable";
import UserHeader from '../UserHeader' import UserHeader from "../UserHeader";
import CallToAction from '../CallToAction' import CallToAction from "../CallToAction";
import routes from '@/routes' import routes from "@/routes";
import './styles.css' import styles from "./styles.module.css";
import Header from "../Header";
const currency = Currency.USD const currency = Currency.USD;
const locale = Locale.EN const locale = Locale.EN;
const itemPriceId = 'aura-membership-2-week-USD' const itemPriceId = "aura-membership-2-week-USD";
const paymentItems = [ const paymentItems = [
{ {
title: 'Per 7-Day Trial For', title: "Per 7-Day Trial For",
price: 1.00, price: 1.0,
description: '2-Week Plan', description: "2-Week Plan",
}, },
] ];
function SubscriptionPage(): JSX.Element { function SubscriptionPage(): JSX.Element {
const { t } = useTranslation() const { t } = useTranslation();
const navigate = useNavigate() const navigate = useNavigate();
const email = useSelector(selectors.selectEmail) const email = useSelector(selectors.selectEmail);
const itemPrice = useSelector(selectors.selectPlanById(itemPriceId)) const itemPrice = useSelector(selectors.selectPlanById(itemPriceId));
const handleClick = () => navigate(routes.client.paymentMethod()) const handleClick = () => navigate(routes.client.paymentMethod());
const policyLink = <a href='https://aura.wit.life/' target='_blank' rel='noopener noreferrer'>{t('subscription_policy')}</a> const handleCross = () => navigate(routes.client.home());
console.log({ itemPrice }) const policyLink = (
<a href="https://aura.wit.life/" target="_blank" rel="noopener noreferrer">
{t("subscription_policy")}
</a>
);
console.log({ itemPrice });
return ( return (
<> <>
<Header classCross={styles.cross} clickCross={handleCross} />
<UserHeader email={email} /> <UserHeader email={email} />
<section className='page'> <section className="page">
<CallToAction /> <CallToAction />
<Countdown start={10}/> <Countdown start={10} />
<PaymentTable items={paymentItems} currency={currency} locale={locale}/> <PaymentTable
<div className='subscription-action'> items={paymentItems}
<MainButton onClick={handleClick}>{t('get_access')}</MainButton> currency={currency}
locale={locale}
/>
<div className={styles["subscription-action"]}>
<MainButton onClick={handleClick}>{t("get_access")}</MainButton>
</div> </div>
<Policy>{t('subscription_text', { policyLink })}</Policy> <Policy>{t("subscription_text", { policyLink })}</Policy>
</section> </section>
</> </>
) );
} }
export default SubscriptionPage export default SubscriptionPage;

View File

@ -7,4 +7,8 @@
justify-content: center; justify-content: center;
background-color: #fff; background-color: #fff;
padding: 15px; padding: 15px;
}
.cross {
left: 28px;
} }

View File

@ -75,7 +75,7 @@ div[class^=divider] {
} }
::-webkit-scrollbar { ::-webkit-scrollbar {
width: 13px; /* width: 13px; */
} }
::-webkit-scrollbar-track { ::-webkit-scrollbar-track {

View File

@ -1,54 +1,74 @@
import type { UserStatus } from "./types" import type { UserStatus } from "./types";
const host = '' const host = "";
const apiHost = 'https://aura.wit.life' const apiHost = "https://aura.wit.life";
const prefix = 'api/v1' const prefix = "api/v1";
const routes = { const routes = {
client: { client: {
root: () => [host, ''].join('/'), root: () => [host, ""].join("/"),
birthday: () => [host, 'birthday'].join('/'), birthday: () => [host, "birthday"].join("/"),
didYouKnow: () => [host, 'did-you-know'].join('/'), didYouKnow: () => [host, "did-you-know"].join("/"),
freePeriodInfo: () => [host, 'free-period'].join('/'), freePeriodInfo: () => [host, "free-period"].join("/"),
birthtime: () => [host, 'birthtime'].join('/'), birthtime: () => [host, "birthtime"].join("/"),
emailEnter: () => [host, 'email'].join('/'), emailEnter: () => [host, "email"].join("/"),
subscription: () => [host, 'subscription'].join('/'), subscription: () => [host, "subscription"].join("/"),
createProfile: () => [host, 'profile', 'create'].join('/'), createProfile: () => [host, "profile", "create"].join("/"),
attention: () => [host, 'attention'].join('/'), attention: () => [host, "attention"].join("/"),
feedback: () => [host, 'feedback'].join('/'), feedback: () => [host, "feedback"].join("/"),
paymentMethod: () => [host, 'payment', 'method'].join('/'), paymentMethod: () => [host, "payment", "method"].join("/"),
wallpaper: () => [host, 'wallpaper'].join('/'), wallpaper: () => [host, "wallpaper"].join("/"),
static: () => [host, 'static', ':typeId'].join('/'), static: () => [host, "static", ":typeId"].join("/"),
legal: (type: string) => [host, 'static', type].join('/'), legal: (type: string) => [host, "static", type].join("/"),
compatibility: () => [host, 'compatibility'].join('/'), compatibility: () => [host, "compatibility"].join("/"),
compatibilityResult: () => [host, 'compatibility', 'result'].join('/'), compatibilityResult: () => [host, "compatibility", "result"].join("/"),
breath: () => [host, 'breath'].join('/'), breath: () => [host, "breath"].join("/"),
priceList: () => [host, 'price-list'].join('/'), priceList: () => [host, "price-list"].join("/"),
home: () => [host, 'home'].join('/'), home: () => [host, "home"].join("/"),
breathResult: () => [host, 'breath', 'result'].join('/'), breathResult: () => [host, "breath", "result"].join("/"),
}, },
server: { server: {
user: () => [apiHost, prefix, 'user.json'].join('/'), user: () => [apiHost, prefix, "user.json"].join("/"),
token: () => [apiHost, prefix, 'auth', 'token.json'].join('/'), token: () => [apiHost, prefix, "auth", "token.json"].join("/"),
elements: () => [apiHost, prefix, 'elements.json'].join('/'), elements: () => [apiHost, prefix, "elements.json"].join("/"),
element: (type: string) => [apiHost, prefix, 'elements', `${type}.json`].join('/'), element: (type: string) =>
apps: (bundleId: string) => [apiHost, prefix, 'apps', `${bundleId}.json`].join('/'), [apiHost, prefix, "elements", `${type}.json`].join("/"),
assets: (category: string) => [apiHost, prefix, 'assets', 'categories', `${category}.json`].join('/'), apps: (bundleId: string) =>
assetCategories: () => [apiHost, prefix, 'assets', 'categories.json'].join('/'), [apiHost, prefix, "apps", `${bundleId}.json`].join("/"),
dailyForecasts: () => [apiHost, prefix, 'user', 'daily_forecast.json'].join('/'), assets: (category: string) =>
auras: () => [apiHost, prefix, 'user', 'aura.json'].join('/'), [apiHost, prefix, "assets", "categories", `${category}.json`].join("/"),
paymentIntents: () => [apiHost, prefix, 'user', 'payment_intents.json'].join('/'), assetCategories: () =>
subscriptionItems: () => [apiHost, prefix, 'user', 'subscription', 'item_prices.json'].join('/'), [apiHost, prefix, "assets", "categories.json"].join("/"),
subscriptionCheckout: () => [apiHost, prefix, 'user', 'subscription', 'checkout', 'new.json'].join('/'), dailyForecasts: () =>
subscriptionStatus: () => [apiHost, prefix, 'user', 'subscription_receipts', 'status.json'].join('/'), [apiHost, prefix, "user", "daily_forecast.json"].join("/"),
subscriptionReceipts: () => [apiHost, prefix, 'user', 'subscription_receipts.json'].join('/'), auras: () => [apiHost, prefix, "user", "aura.json"].join("/"),
subscriptionReceipt: (id: string) => [apiHost, prefix, 'user', 'subscription_receipts', `${id}.json`].join('/'), paymentIntents: () =>
compatCategories: () => [apiHost, prefix, 'ai', 'compat_categories.json'].join('/'), [apiHost, prefix, "user", "payment_intents.json"].join("/"),
compat: () => [apiHost, prefix, 'ai', 'compats.json'].join('/'), subscriptionItems: () =>
createUserCallbacks: () => [apiHost, prefix, 'user', 'callbacks.json'].join('/'), [apiHost, prefix, "user", "subscription", "item_prices.json"].join("/"),
getUserCallbacks: (id: string) => [apiHost, prefix, 'user', 'callbacks', `${id}.json`].join('/'), subscriptionCheckout: () =>
[apiHost, prefix, "user", "subscription", "checkout", "new.json"].join(
"/"
),
subscriptionStatus: () =>
[apiHost, prefix, "user", "subscription_receipts", "status.json"].join(
"/"
),
subscriptionReceipts: () =>
[apiHost, prefix, "user", "subscription_receipts.json"].join("/"),
subscriptionReceipt: (id: string) =>
[apiHost, prefix, "user", "subscription_receipts", `${id}.json`].join(
"/"
),
compatCategories: () =>
[apiHost, prefix, "ai", "compat_categories.json"].join("/"),
compat: () => [apiHost, prefix, "ai", "compats.json"].join("/"),
createUserCallbacks: () =>
[apiHost, prefix, "user", "callbacks.json"].join("/"),
getUserCallbacks: (id: string) =>
[apiHost, prefix, "user", "callbacks", `${id}.json`].join("/"),
}, },
} };
export const entrypoints = [ export const entrypoints = [
routes.client.root(), routes.client.root(),
@ -62,15 +82,20 @@ export const entrypoints = [
routes.client.compatibilityResult(), routes.client.compatibilityResult(),
routes.client.home(), routes.client.home(),
routes.client.breathResult(), routes.client.breathResult(),
] ];
export const isEntrypoint = (path: string) => entrypoints.includes(path) export const isEntrypoint = (path: string) => entrypoints.includes(path);
export const isNotEntrypoint = (path: string) => !isEntrypoint(path) export const isNotEntrypoint = (path: string) => !isEntrypoint(path);
export const withNavigationRoutes = [routes.client.wallpaper()] export const withNavigationRoutes = [routes.client.wallpaper()];
export const hasNavigation = (path: string) => withNavigationRoutes.includes(path) export const hasNavigation = (path: string) =>
export const hasNoNavigation = (path: string) => !hasNavigation(path) withNavigationRoutes.includes(path);
export const hasNoNavigation = (path: string) => !hasNavigation(path);
export const withCrossButtonRoutes = [routes.client.attention()] export const withCrossButtonRoutes = [
export const hasCrossButton = (path: string) => withCrossButtonRoutes.includes(path) routes.client.attention(),
routes.client.subscription(),
];
export const hasCrossButton = (path: string) =>
withCrossButtonRoutes.includes(path);
export const withoutFooterRoutes = [ export const withoutFooterRoutes = [
routes.client.didYouKnow(), routes.client.didYouKnow(),
@ -84,26 +109,29 @@ export const withoutFooterRoutes = [
routes.client.compatibilityResult(), routes.client.compatibilityResult(),
routes.client.home(), routes.client.home(),
routes.client.breathResult(), routes.client.breathResult(),
] ];
export const hasNoFooter = (path: string) => !withoutFooterRoutes.includes(path) export const hasNoFooter = (path: string) =>
!withoutFooterRoutes.includes(path);
export const withoutHeaderRoutes = [ export const withoutHeaderRoutes = [
routes.client.compatibility(), routes.client.compatibility(),
] routes.client.subscription(),
export const hasNoHeader = (path: string) => !withoutHeaderRoutes.includes(path) ];
export const hasNoHeader = (path: string) =>
!withoutHeaderRoutes.includes(path);
export const getRouteBy = (status: UserStatus): string => { export const getRouteBy = (status: UserStatus): string => {
switch (status) { switch (status) {
case 'lead': case "lead":
return routes.client.birthday() return routes.client.birthday();
case 'registred': case "registred":
case 'unsubscribed': case "unsubscribed":
return routes.client.subscription() return routes.client.subscription();
case 'subscribed': case "subscribed":
return routes.client.wallpaper() return routes.client.wallpaper();
default: default:
throw new Error(`Unknown user status, received status is "${status}"`) throw new Error(`Unknown user status, received status is "${status}"`);
} }
} };
export default routes export default routes;