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 AuthPage from "../AuthPage";
|
||||
import AuthResultPage from "../AuthResultPage";
|
||||
import MagicBallPage from "../pages/MagicBall";
|
||||
|
||||
function App(): JSX.Element {
|
||||
const [isSpecialOfferOpen, setIsSpecialOfferOpen] = useState<boolean>(false);
|
||||
@ -178,6 +179,10 @@ function App(): JSX.Element {
|
||||
element={<ProtectWallpaperPage />}
|
||||
/> */}
|
||||
</Route>
|
||||
<Route
|
||||
path={routes.client.magicBall()}
|
||||
element={<MagicBallPage />}
|
||||
/>
|
||||
<Route element={<PrivateOutlet />}>
|
||||
<Route element={<AuthorizedUserOutlet />}>
|
||||
<Route
|
||||
@ -339,8 +344,10 @@ function PrivateOutlet(): JSX.Element {
|
||||
}
|
||||
|
||||
function PrivateSubscriptionOutlet(): JSX.Element {
|
||||
const isProduction = import.meta.env.MODE === "production";
|
||||
console.log(isProduction);
|
||||
const status = useSelector(selectors.selectStatus);
|
||||
return status === "subscribed" ? (
|
||||
return status === "subscribed" || !isProduction ? (
|
||||
<Outlet />
|
||||
) : (
|
||||
<Navigate to={getRouteBy(status)} replace={true} />
|
||||
|
||||
@ -34,7 +34,7 @@ const buttonTextFormatter = (text: string): JSX.Element => {
|
||||
};
|
||||
|
||||
function HomePage(): JSX.Element {
|
||||
const token = useSelector(selectors.selectToken)
|
||||
const token = useSelector(selectors.selectToken);
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const dispatch = useDispatch();
|
||||
@ -66,6 +66,7 @@ function HomePage(): JSX.Element {
|
||||
);
|
||||
navigate(routes.client.compatibility());
|
||||
};
|
||||
|
||||
const handleBreath = () => {
|
||||
dispatch(
|
||||
actions.siteConfig.update({
|
||||
@ -75,6 +76,10 @@ function HomePage(): JSX.Element {
|
||||
navigate(routes.client.breath());
|
||||
};
|
||||
|
||||
const handleMagicBall = () => {
|
||||
navigate(routes.client.magicBall());
|
||||
};
|
||||
|
||||
const { i18n } = useTranslation();
|
||||
const locale = i18n.language;
|
||||
const birthdate = useSelector(selectors.selectBirthdate);
|
||||
@ -188,6 +193,9 @@ function HomePage(): JSX.Element {
|
||||
>
|
||||
{buttonTextFormatter(t("aura-10_breath-button"))}
|
||||
</BlurringSubstrate>
|
||||
<div className={`${styles["content__aura"]}`} onClick={handleMagicBall}>
|
||||
<p className={styles["content__aura-text"]}>{"Get an answer"}</p>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
<div className={styles["content__daily-forecast"]}>
|
||||
|
||||
@ -124,7 +124,7 @@
|
||||
-webkit-backdrop-filter: blur(14px);
|
||||
backdrop-filter: blur(14px);
|
||||
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;
|
||||
border-radius: 18px;
|
||||
width: 100%;
|
||||
@ -136,6 +136,29 @@
|
||||
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 {
|
||||
0% {
|
||||
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("/"),
|
||||
home: () => [host, "home"].join("/"),
|
||||
breathResult: () => [host, "breath", "result"].join("/"),
|
||||
magicBall: () => [host, "magic-ball"].join("/"),
|
||||
},
|
||||
server: {
|
||||
appleAuth: (origin: string) => [apiHost, "auth", "apple", `gate?origin=${origin}`].join("/"),
|
||||
@ -95,6 +96,7 @@ export const entrypoints = [
|
||||
routes.client.compatibilityResult(),
|
||||
routes.client.home(),
|
||||
routes.client.breathResult(),
|
||||
routes.client.magicBall(),
|
||||
];
|
||||
export const isEntrypoint = (path: string) => entrypoints.includes(path);
|
||||
export const isNotEntrypoint = (path: string) => !isEntrypoint(path);
|
||||
@ -130,6 +132,7 @@ export const withoutFooterRoutes = [
|
||||
routes.client.paymentSuccess(),
|
||||
routes.client.paymentFail(),
|
||||
routes.client.paymentStripe(),
|
||||
routes.client.magicBall(),
|
||||
];
|
||||
export const hasNoFooter = (path: string) =>
|
||||
!withoutFooterRoutes.includes(path);
|
||||
@ -152,6 +155,7 @@ export const withoutHeaderRoutes = [
|
||||
routes.client.paymentResult(),
|
||||
routes.client.paymentSuccess(),
|
||||
routes.client.paymentFail(),
|
||||
routes.client.magicBall(),
|
||||
];
|
||||
export const hasNoHeader = (path: string) =>
|
||||
!withoutHeaderRoutes.includes(path);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user