feat: add magic ball page
This commit is contained in:
parent
997427ad54
commit
a7d5ccff43
@ -51,6 +51,7 @@ import PaymentFailPage from "../PaymentPage/results/ErrorPage";
|
|||||||
import { StripePage } from "../StripePage";
|
import { StripePage } from "../StripePage";
|
||||||
import AuthPage from "../AuthPage";
|
import AuthPage from "../AuthPage";
|
||||||
import AuthResultPage from "../AuthResultPage";
|
import AuthResultPage from "../AuthResultPage";
|
||||||
|
import MagicBallPage from "../pages/MagicBall";
|
||||||
|
|
||||||
function App(): JSX.Element {
|
function App(): JSX.Element {
|
||||||
const [isSpecialOfferOpen, setIsSpecialOfferOpen] = useState<boolean>(false);
|
const [isSpecialOfferOpen, setIsSpecialOfferOpen] = useState<boolean>(false);
|
||||||
@ -178,6 +179,10 @@ function App(): JSX.Element {
|
|||||||
element={<ProtectWallpaperPage />}
|
element={<ProtectWallpaperPage />}
|
||||||
/> */}
|
/> */}
|
||||||
</Route>
|
</Route>
|
||||||
|
<Route
|
||||||
|
path={routes.client.magicBall()}
|
||||||
|
element={<MagicBallPage />}
|
||||||
|
/>
|
||||||
<Route element={<PrivateOutlet />}>
|
<Route element={<PrivateOutlet />}>
|
||||||
<Route element={<AuthorizedUserOutlet />}>
|
<Route element={<AuthorizedUserOutlet />}>
|
||||||
<Route
|
<Route
|
||||||
@ -339,8 +344,10 @@ function PrivateOutlet(): JSX.Element {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function PrivateSubscriptionOutlet(): JSX.Element {
|
function PrivateSubscriptionOutlet(): JSX.Element {
|
||||||
|
const isProduction = import.meta.env.MODE === "production";
|
||||||
|
console.log(isProduction);
|
||||||
const status = useSelector(selectors.selectStatus);
|
const status = useSelector(selectors.selectStatus);
|
||||||
return status === "subscribed" ? (
|
return status === "subscribed" || !isProduction ? (
|
||||||
<Outlet />
|
<Outlet />
|
||||||
) : (
|
) : (
|
||||||
<Navigate to={getRouteBy(status)} replace={true} />
|
<Navigate to={getRouteBy(status)} replace={true} />
|
||||||
|
|||||||
@ -34,7 +34,7 @@ const buttonTextFormatter = (text: string): JSX.Element => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
function HomePage(): JSX.Element {
|
function HomePage(): JSX.Element {
|
||||||
const token = useSelector(selectors.selectToken)
|
const token = useSelector(selectors.selectToken);
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
@ -66,6 +66,7 @@ function HomePage(): JSX.Element {
|
|||||||
);
|
);
|
||||||
navigate(routes.client.compatibility());
|
navigate(routes.client.compatibility());
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleBreath = () => {
|
const handleBreath = () => {
|
||||||
dispatch(
|
dispatch(
|
||||||
actions.siteConfig.update({
|
actions.siteConfig.update({
|
||||||
@ -75,6 +76,10 @@ function HomePage(): JSX.Element {
|
|||||||
navigate(routes.client.breath());
|
navigate(routes.client.breath());
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleMagicBall = () => {
|
||||||
|
navigate(routes.client.magicBall());
|
||||||
|
};
|
||||||
|
|
||||||
const { i18n } = useTranslation();
|
const { i18n } = useTranslation();
|
||||||
const locale = i18n.language;
|
const locale = i18n.language;
|
||||||
const birthdate = useSelector(selectors.selectBirthdate);
|
const birthdate = useSelector(selectors.selectBirthdate);
|
||||||
@ -188,6 +193,9 @@ function HomePage(): JSX.Element {
|
|||||||
>
|
>
|
||||||
{buttonTextFormatter(t("aura-10_breath-button"))}
|
{buttonTextFormatter(t("aura-10_breath-button"))}
|
||||||
</BlurringSubstrate>
|
</BlurringSubstrate>
|
||||||
|
<div className={`${styles["content__aura"]}`} onClick={handleMagicBall}>
|
||||||
|
<p className={styles["content__aura-text"]}>{"Get an answer"}</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
<div className={styles["content__daily-forecast"]}>
|
<div className={styles["content__daily-forecast"]}>
|
||||||
|
|||||||
@ -124,7 +124,7 @@
|
|||||||
-webkit-backdrop-filter: blur(14px);
|
-webkit-backdrop-filter: blur(14px);
|
||||||
backdrop-filter: blur(14px);
|
backdrop-filter: blur(14px);
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
box-shadow: inset 0px 0px 25px rgba(0,0,0,0.5);
|
box-shadow: inset 0px 0px 25px rgba(0, 0, 0, 0.5);
|
||||||
background-color: #00000094;
|
background-color: #00000094;
|
||||||
border-radius: 18px;
|
border-radius: 18px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@ -136,6 +136,29 @@
|
|||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.content__aura {
|
||||||
|
margin: 0 auto;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100px;
|
||||||
|
height: 100px;
|
||||||
|
transition: top 3s, left 3s;
|
||||||
|
background-image: url("/goosebumps-aura.png");
|
||||||
|
background-size: 150px;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: center center;
|
||||||
|
animation: pulse 1s alternate infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content__aura-text {
|
||||||
|
width: 100%;
|
||||||
|
text-align: center;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 16px;
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
|
||||||
@keyframes pulse {
|
@keyframes pulse {
|
||||||
0% {
|
0% {
|
||||||
transform: scale(0.9);
|
transform: scale(0.9);
|
||||||
|
|||||||
109
src/components/pages/MagicBall/index.tsx
Normal file
109
src/components/pages/MagicBall/index.tsx
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import styles from "./styles.module.css";
|
||||||
|
import routes from "@/routes";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import Title from "@/components/Title";
|
||||||
|
import MainButton from "@/components/MainButton";
|
||||||
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
|
import { getRandomArbitrary } from "@/services/random-value";
|
||||||
|
import { UseInterval } from "@/hooks/useInterval";
|
||||||
|
|
||||||
|
const answers = [
|
||||||
|
"Undoubtedly.",
|
||||||
|
"Predetermined.",
|
||||||
|
"No doubt about it.",
|
||||||
|
"Definitely yes.",
|
||||||
|
"You can be sure of it.",
|
||||||
|
"It seems like yes.",
|
||||||
|
"Most likely.",
|
||||||
|
"Looks promising.",
|
||||||
|
"Signs point to yes.",
|
||||||
|
"Yes.",
|
||||||
|
"Unclear now, try again.",
|
||||||
|
"Ask later.",
|
||||||
|
"Better not tell you now.",
|
||||||
|
"Cannot predict now.",
|
||||||
|
"Concentrate and ask again.",
|
||||||
|
"Don’t even think about it.",
|
||||||
|
"My answer is no.",
|
||||||
|
"According to my sources, no.",
|
||||||
|
"Prospects aren't very good.",
|
||||||
|
"Quite doubtful.",
|
||||||
|
];
|
||||||
|
|
||||||
|
function MagicBallPage(): JSX.Element {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const [processState, setProcessState] = useState(0);
|
||||||
|
const [isRunning, setIsRunning] = useState(false);
|
||||||
|
const [isRepeat, setIsRepeat] = useState(false);
|
||||||
|
const auraText = useMemo(
|
||||||
|
() => [
|
||||||
|
"Start",
|
||||||
|
"3",
|
||||||
|
"2",
|
||||||
|
"1",
|
||||||
|
answers[getRandomArbitrary(0, answers.length - 1)],
|
||||||
|
],
|
||||||
|
[isRepeat]
|
||||||
|
);
|
||||||
|
|
||||||
|
const clickCross = () => {
|
||||||
|
navigate(routes.client.home());
|
||||||
|
};
|
||||||
|
|
||||||
|
const start = () => {
|
||||||
|
if (processState) return;
|
||||||
|
setIsRunning(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
UseInterval(
|
||||||
|
() => {
|
||||||
|
if (processState >= auraText.length - 1) {
|
||||||
|
setIsRunning(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const canVibrate = !!window.navigator.vibrate;
|
||||||
|
if (canVibrate) window.navigator.vibrate(100);
|
||||||
|
setProcessState((prev) => prev + 1);
|
||||||
|
},
|
||||||
|
isRunning ? 1000 : null
|
||||||
|
);
|
||||||
|
|
||||||
|
const repeat = () => {
|
||||||
|
setProcessState(0);
|
||||||
|
setIsRunning(false);
|
||||||
|
setIsRepeat((prev) => !prev);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className={`${styles.page} page`}>
|
||||||
|
<div className={styles.header}>
|
||||||
|
<img
|
||||||
|
className={`${styles.cross}`}
|
||||||
|
src="/cross.png"
|
||||||
|
alt="Cross"
|
||||||
|
onClick={clickCross}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className={styles.content}>
|
||||||
|
<Title variant="h3" className={styles.title}>
|
||||||
|
{t("au.magic.title")}
|
||||||
|
</Title>
|
||||||
|
<p className={styles.text}>{t("au.magic.text1")}</p>
|
||||||
|
<div className={styles.aura} onClick={start}>
|
||||||
|
<Title variant="h2" className={styles["aura__text"]}>
|
||||||
|
{auraText[processState]}
|
||||||
|
</Title>
|
||||||
|
</div>
|
||||||
|
{processState === auraText.length - 1 && (
|
||||||
|
<MainButton className={styles.repeat} onClick={repeat}>
|
||||||
|
{"Repeat"}
|
||||||
|
</MainButton>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MagicBallPage;
|
||||||
86
src/components/pages/MagicBall/styles.module.css
Normal file
86
src/components/pages/MagicBall/styles.module.css
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
.page {
|
||||||
|
height: fit-content;
|
||||||
|
min-height: 100vh;
|
||||||
|
/* max-height: -webkit-fill-available; */
|
||||||
|
padding-bottom: 32px;
|
||||||
|
flex: auto !important;
|
||||||
|
background-color: #000;
|
||||||
|
color: #fff;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
-khtml-user-select: none;
|
||||||
|
-moz-user-select: none;
|
||||||
|
-ms-user-select: none;
|
||||||
|
-o-user-select: none;
|
||||||
|
user-select: none;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: flex-end;
|
||||||
|
padding: 16px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cross {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
padding-top: 64px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 22px;
|
||||||
|
max-width: 280px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text {
|
||||||
|
font-size: 17px;
|
||||||
|
font-weight: 400;
|
||||||
|
text-align: center;
|
||||||
|
max-width: 290px;
|
||||||
|
line-height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.aura {
|
||||||
|
margin: 0 auto;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 280px;
|
||||||
|
aspect-ratio: 1 / 1;
|
||||||
|
background-image: url("/goosebumps-aura.png");
|
||||||
|
background-size: 160%;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: center center;
|
||||||
|
transition: top 3s, left 3s;
|
||||||
|
animation: pulse 1s alternate infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.aura__text {
|
||||||
|
margin: 0;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 28px;
|
||||||
|
line-height: 34px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.repeat {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 250px;
|
||||||
|
background-color: #fff;
|
||||||
|
color: #000;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 16px;
|
||||||
|
margin-top: 60px;
|
||||||
|
}
|
||||||
22
src/hooks/useInterval.tsx
Normal file
22
src/hooks/useInterval.tsx
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import { useEffect, useRef } from "react";
|
||||||
|
|
||||||
|
export function UseInterval(callback: () => void, delay: number | null) {
|
||||||
|
const savedCallback = useRef<() => void>();
|
||||||
|
|
||||||
|
// Remember the latest callback.
|
||||||
|
useEffect(() => {
|
||||||
|
savedCallback.current = callback;
|
||||||
|
}, [callback]);
|
||||||
|
|
||||||
|
// Set up the interval.
|
||||||
|
useEffect(() => {
|
||||||
|
function tick() {
|
||||||
|
if (!savedCallback.current) return;
|
||||||
|
savedCallback.current();
|
||||||
|
}
|
||||||
|
if (delay !== null) {
|
||||||
|
const id = setInterval(tick, delay);
|
||||||
|
return () => clearInterval(id);
|
||||||
|
}
|
||||||
|
}, [delay]);
|
||||||
|
}
|
||||||
@ -33,6 +33,7 @@ const routes = {
|
|||||||
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("/"),
|
||||||
|
magicBall: () => [host, "magic-ball"].join("/"),
|
||||||
},
|
},
|
||||||
server: {
|
server: {
|
||||||
appleAuth: (origin: string) => [apiHost, "auth", "apple", `gate?origin=${origin}`].join("/"),
|
appleAuth: (origin: string) => [apiHost, "auth", "apple", `gate?origin=${origin}`].join("/"),
|
||||||
@ -95,6 +96,7 @@ export const entrypoints = [
|
|||||||
routes.client.compatibilityResult(),
|
routes.client.compatibilityResult(),
|
||||||
routes.client.home(),
|
routes.client.home(),
|
||||||
routes.client.breathResult(),
|
routes.client.breathResult(),
|
||||||
|
routes.client.magicBall(),
|
||||||
];
|
];
|
||||||
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);
|
||||||
@ -130,6 +132,7 @@ export const withoutFooterRoutes = [
|
|||||||
routes.client.paymentSuccess(),
|
routes.client.paymentSuccess(),
|
||||||
routes.client.paymentFail(),
|
routes.client.paymentFail(),
|
||||||
routes.client.paymentStripe(),
|
routes.client.paymentStripe(),
|
||||||
|
routes.client.magicBall(),
|
||||||
];
|
];
|
||||||
export const hasNoFooter = (path: string) =>
|
export const hasNoFooter = (path: string) =>
|
||||||
!withoutFooterRoutes.includes(path);
|
!withoutFooterRoutes.includes(path);
|
||||||
@ -152,6 +155,7 @@ export const withoutHeaderRoutes = [
|
|||||||
routes.client.paymentResult(),
|
routes.client.paymentResult(),
|
||||||
routes.client.paymentSuccess(),
|
routes.client.paymentSuccess(),
|
||||||
routes.client.paymentFail(),
|
routes.client.paymentFail(),
|
||||||
|
routes.client.magicBall(),
|
||||||
];
|
];
|
||||||
export const hasNoHeader = (path: string) =>
|
export const hasNoHeader = (path: string) =>
|
||||||
!withoutHeaderRoutes.includes(path);
|
!withoutHeaderRoutes.includes(path);
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user