From a7d5ccff43cf9415d5bb745fab95620df8c48572 Mon Sep 17 00:00:00 2001 From: gofnnp Date: Wed, 22 Nov 2023 02:16:45 +0400 Subject: [PATCH] feat: add magic ball page --- src/components/App/index.tsx | 9 +- src/components/HomePage/index.tsx | 10 +- src/components/HomePage/styles.module.css | 25 +++- src/components/pages/MagicBall/index.tsx | 109 ++++++++++++++++++ .../pages/MagicBall/styles.module.css | 86 ++++++++++++++ src/hooks/useInterval.tsx | 22 ++++ src/routes.ts | 4 + 7 files changed, 262 insertions(+), 3 deletions(-) create mode 100644 src/components/pages/MagicBall/index.tsx create mode 100644 src/components/pages/MagicBall/styles.module.css create mode 100644 src/hooks/useInterval.tsx diff --git a/src/components/App/index.tsx b/src/components/App/index.tsx index fa03aa3..d4ff9ca 100644 --- a/src/components/App/index.tsx +++ b/src/components/App/index.tsx @@ -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(false); @@ -178,6 +179,10 @@ function App(): JSX.Element { element={} /> */} + } + /> }> }> ) : ( diff --git a/src/components/HomePage/index.tsx b/src/components/HomePage/index.tsx index ea935ab..67b3a45 100644 --- a/src/components/HomePage/index.tsx +++ b/src/components/HomePage/index.tsx @@ -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"))} +
+

{"Get an answer"}

+
}
diff --git a/src/components/HomePage/styles.module.css b/src/components/HomePage/styles.module.css index 7fed6b2..0de2ed4 100644 --- a/src/components/HomePage/styles.module.css +++ b/src/components/HomePage/styles.module.css @@ -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); diff --git a/src/components/pages/MagicBall/index.tsx b/src/components/pages/MagicBall/index.tsx new file mode 100644 index 0000000..1cdeb44 --- /dev/null +++ b/src/components/pages/MagicBall/index.tsx @@ -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 ( +
+
+ Cross +
+
+ + {t("au.magic.title")} + +

{t("au.magic.text1")}

+
+ + {auraText[processState]} + +
+ {processState === auraText.length - 1 && ( + + {"Repeat"} + + )} +
+
+ ); +} + +export default MagicBallPage; diff --git a/src/components/pages/MagicBall/styles.module.css b/src/components/pages/MagicBall/styles.module.css new file mode 100644 index 0000000..c892001 --- /dev/null +++ b/src/components/pages/MagicBall/styles.module.css @@ -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; +} diff --git a/src/hooks/useInterval.tsx b/src/hooks/useInterval.tsx new file mode 100644 index 0000000..005e95d --- /dev/null +++ b/src/hooks/useInterval.tsx @@ -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]); +} diff --git a/src/routes.ts b/src/routes.ts index 7ca5f05..ebd2eae 100644 --- a/src/routes.ts +++ b/src/routes.ts @@ -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);