diff --git a/src/api/api.ts b/src/api/api.ts index f6c0744..abba855 100644 --- a/src/api/api.ts +++ b/src/api/api.ts @@ -22,7 +22,8 @@ import { Zodiacs, GoogleAuth, SubscriptionPlans, - AppleAuth + AppleAuth, + AIRequestsV2 } from './resources' const api = { @@ -52,6 +53,8 @@ const api = { getUserCallbacks: createMethod(UserCallbacks.createRequestGet), getTranslations: createMethod(Translations.createRequest), getZodiacs: createMethod(Zodiacs.createRequest), + AIRequestsV2: createMethod(AIRequestsV2.createRequest), + getAIRequestsV2: createMethod(AIRequestsV2.createRequestGet), } export type ApiContextValue = typeof api diff --git a/src/api/resources/AIRequestsV2.ts b/src/api/resources/AIRequestsV2.ts new file mode 100644 index 0000000..d52398c --- /dev/null +++ b/src/api/resources/AIRequestsV2.ts @@ -0,0 +1,102 @@ +import routes from "@/routes"; +import { getAuthHeaders } from "../utils"; + +const dateFormatter = (date: string): string => { + return date + .split("-") + .map((item) => item.padStart(2, "0")) + .join("-"); +}; + +export interface Payload { + promptKey: string; + aiRequest: IAIRequestPayload; + token: string; +} + +export interface PayloadGet { + id: string; + token: string; +} + +interface IAIRequestPayload { + birthDate: string; + sign: string; +} + +export interface Response { + ai_request: IAiResponse; + meta: IMeta; +} + +interface IMeta { + links: { + self: string; + }; +} + +export interface IAiResponse { + id: string; + prompt_key: string; + created_at: string; + updated_at: string; + state: string; + is_reused: boolean; + finished_at: string; + job_id: null; + response: IAIRequest; +} + +export interface IAiResponseGet { + ai_request: IAIRequest; +} + +export interface IAIRequest { + id: string; + prompt_key: string; + created_at: string; + updated_at: string; + state: string; + is_reused: boolean; + finished_at: string; + job_id: unknown; + response: { + id: string; + inputs: { + sign: string; + birth_date: string; + }; + created_at: string; + updated_at: string; + body: string; + has_vocal: boolean; + speech: unknown; + }; +} + +export const createRequest = ({ + promptKey, + aiRequest, + token, +}: Payload): Request => { + const url = new URL(routes.server.aiRequestsV2(promptKey)); + const body = JSON.stringify({ + ai_request: { + birth_date: dateFormatter(aiRequest.birthDate), + sign: aiRequest.sign, + }, + }); + return new Request(url, { + method: "POST", + headers: getAuthHeaders(token), + body, + }); +}; + +export const createRequestGet = ({ id, token }: PayloadGet): Request => { + const url = new URL(routes.server.getAiRequestsV2(id)); + return new Request(url, { + method: "GET", + headers: getAuthHeaders(token), + }); +}; diff --git a/src/api/resources/index.ts b/src/api/resources/index.ts index a679cc4..bdd0dcf 100644 --- a/src/api/resources/index.ts +++ b/src/api/resources/index.ts @@ -21,3 +21,4 @@ export * as Zodiacs from "./Zodiacs"; export * as GoogleAuth from "./GoogleAuth"; export * as SubscriptionPlans from "./SubscriptionPlans"; export * as AppleAuth from "./AppleAuth"; +export * as AIRequestsV2 from "./AIRequestsV2"; diff --git a/src/components/App/index.tsx b/src/components/App/index.tsx index 42618aa..1bb954c 100644 --- a/src/components/App/index.tsx +++ b/src/components/App/index.tsx @@ -52,6 +52,7 @@ import { StripePage } from "../StripePage"; import AuthPage from "../AuthPage"; import AuthResultPage from "../AuthResultPage"; import MagicBallPage from "../pages/MagicBall"; +import BestiesHoroscopeResult from "../pages/BestiesHoroscopeResult"; function App(): JSX.Element { const [isSpecialOfferOpen, setIsSpecialOfferOpen] = useState(false); @@ -179,10 +180,6 @@ function App(): JSX.Element { element={} /> */} - } - /> }> }> } /> + } + /> + } + /> } /> diff --git a/src/components/BestiesHoroscopeSlider/index.tsx b/src/components/BestiesHoroscopeSlider/index.tsx index 2c1d526..08ea4bc 100644 --- a/src/components/BestiesHoroscopeSlider/index.tsx +++ b/src/components/BestiesHoroscopeSlider/index.tsx @@ -10,6 +10,7 @@ import { getRandomArbitrary } from "@/services/random-value"; interface BestiesHoroscopeSliderProps { data: Horoscope; + onClick: () => void; } export interface Horoscope { @@ -19,6 +20,7 @@ export interface Horoscope { function BestiesHoroscopeSlider({ data, + onClick }: BestiesHoroscopeSliderProps): JSX.Element { const api = useApi(); const { i18n, t } = useTranslation(); @@ -51,9 +53,7 @@ function BestiesHoroscopeSlider({ style={{ background: `url(${backgroundUrl})`, }} - onLoad={() => { - console.log("start"); - }} + onClick={onClick} >

{t("au.besties.result")} diff --git a/src/components/BestiesHoroscopeSlider/styles.module.css b/src/components/BestiesHoroscopeSlider/styles.module.css index 4e86c46..0546a79 100644 --- a/src/components/BestiesHoroscopeSlider/styles.module.css +++ b/src/components/BestiesHoroscopeSlider/styles.module.css @@ -11,6 +11,7 @@ background-size: cover !important; background-position: center !important; background-color: #000 !important; + cursor: pointer; } .text { diff --git a/src/components/HomePage/index.tsx b/src/components/HomePage/index.tsx index 5890fd5..93159d6 100644 --- a/src/components/HomePage/index.tsx +++ b/src/components/HomePage/index.tsx @@ -22,7 +22,7 @@ import { buildFilename, saveFile } from "../WallpaperPage/utils"; import Onboarding from "../Onboarding"; import TextWithFinger from "../TextWithFinger"; import Slider from "../Slider"; -import BestiesHoroscopeSlider from "../BestiesHoroscopeSlider"; +import BestiesHoroscopeSlider, { Horoscope } from "../BestiesHoroscopeSlider"; const buttonTextFormatter = (text: string): JSX.Element => { const sentences = text.split("."); @@ -136,6 +136,13 @@ function HomePage(): JSX.Element { saveFile(asset.url.replace("http://", "https://"), buildFilename("1")); }; + const handleBestiesHoroscope = (item: Horoscope) => { + const { name, birthDate } = item; + navigate( + `${routes.client.horoscopeBestiesResult()}?name=${name}&birthDate=${birthDate}` + ); + }; + return (

+ {/*
*/}
@@ -214,7 +227,13 @@ function HomePage(): JSX.Element { {bestiesHoroscopes.map((item, index) => ( - + { + handleBestiesHoroscope(item); + }} + /> ))}
@@ -242,6 +261,7 @@ function HomePage(): JSX.Element { ))}
+ {/* */}
); } diff --git a/src/components/HomePage/styles.module.css b/src/components/HomePage/styles.module.css index 6d391f4..b87bb9a 100644 --- a/src/components/HomePage/styles.module.css +++ b/src/components/HomePage/styles.module.css @@ -14,6 +14,17 @@ overflow-y: scroll; } +.background { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-size: calc(100% + 186px); + background-position: center; + background-repeat: no-repeat; +} + .header { position: relative; width: 100%; diff --git a/src/components/ProgressBarsModal/index.tsx b/src/components/ProgressBarsModal/index.tsx new file mode 100644 index 0000000..04db887 --- /dev/null +++ b/src/components/ProgressBarsModal/index.tsx @@ -0,0 +1,83 @@ +import { useEffect, useState } from "react"; +import Title from "../Title"; +import ProgressBarLine from "../ui/ProgressBarLine"; +import styles from "./styles.module.css"; +import Spinner from "../ui/Spinner"; +import { useSelector } from "react-redux"; +import { selectors } from "@/store"; + +interface IProgressBarsModalProps { + children: JSX.Element[]; + progressBars: ProgressBar[]; + className?: string; + onEndLoading: () => void; +} + +export interface ProgressBar { + label: string; +} + +const getProgressValue = (index: number, value: number) => { + const integerDivision = Math.floor(value / 100); + if (integerDivision > index) { + return 100; + } + if (integerDivision === index) { + return value % 100; + } + return 0; +}; + +function ProgressBarsModal({ + progressBars, + className, + onEndLoading, + children, +}: React.PropsWithChildren): JSX.Element { + const [progress, setProgress] = useState(0); + const homeConfig = useSelector(selectors.selectHome); + const showNavbarFooter = homeConfig.isShowNavbar; + + useEffect(() => { + if (progress >= progressBars.length * 100) { + return onEndLoading(); + } + const interval = setTimeout(() => { + setProgress((prevProgress) => { + return prevProgress + 1; + }); + }, 50); + return () => { + clearTimeout(interval); + }; + }, [progress, onEndLoading]); + + return ( +
+
{children[0]}
+ {progressBars.map((progressBar, index) => ( +
+ + {progressBar.label} + +
+ + + {getProgressValue(index, progress)}% + +
+
+ ))} + +
+ ); +} + +export default ProgressBarsModal; diff --git a/src/components/ProgressBarsModal/styles.module.css b/src/components/ProgressBarsModal/styles.module.css new file mode 100644 index 0000000..3cf3abc --- /dev/null +++ b/src/components/ProgressBarsModal/styles.module.css @@ -0,0 +1,39 @@ +.container { + padding: 16px 32px; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; +} + +.title { + color: #fd3761; + font-weight: 600; + width: 100%; + text-align: left; +} + +.progress-bar { + display: flex; + flex-direction: column; + width: 100%; + margin-bottom: 16px; +} + +.progress-bar__label { + font-size: 16px; + margin-bottom: 8px; + width: 100%; + text-align: left; +} + +.progress-bar__container { + display: flex; + gap: 16px; + flex-direction: row; + align-items: center; +} + +.progress-bar__line { + width: calc(100% - 56px); +} diff --git a/src/components/pages/BestiesHoroscopeResult/index.tsx b/src/components/pages/BestiesHoroscopeResult/index.tsx new file mode 100644 index 0000000..abcd620 --- /dev/null +++ b/src/components/pages/BestiesHoroscopeResult/index.tsx @@ -0,0 +1,159 @@ +import { useTranslation } from "react-i18next"; +import Title from "@/components/Title"; +import styles from "./styles.module.css"; +import { useDispatch, useSelector } from "react-redux"; +import { selectors } from "@/store"; +import { useCallback, useEffect, useState } from "react"; +import { AICompats, AIRequestsV2, useApi, useApiCall } from "@/api"; +import { useNavigate, useSearchParams } from "react-router-dom"; +import routes from "@/routes"; +import FullScreenModal from "@/components/FullScreenModal"; +import ProgressBarsModal, { ProgressBar } from "@/components/ProgressBarsModal"; +import { + getCategoryIdByZodiacSign, + getZodiacSignByDate, +} from "@/services/zodiac-sign"; +import { getRandomArbitrary } from "@/services/random-value"; + +function BestiesHoroscopeResult(): JSX.Element { + const token = useSelector(selectors.selectToken); + const { i18n, t } = useTranslation(); + const locale = i18n.language; + const navigate = useNavigate(); + const api = useApi(); + const homeConfig = useSelector(selectors.selectHome); + const showNavbarFooter = homeConfig.isShowNavbar; + const [text, setText] = useState("Loading..."); + const [isOpenModal, setIsOpenModal] = useState(true); + const [isVisualLoading, setIsVisualLoading] = useState(true); + const [isDataLoading, setIsDataLoading] = useState(true); + const [searchParams] = useSearchParams(); + const name = searchParams.get("name"); + const birthDate = searchParams.get("birthDate") || ""; + const zodiacSign = getZodiacSignByDate(birthDate); + const [backgroundUrl, setBackgroundUrl] = useState(""); + + const progressBars: ProgressBar[] = [ + { + label: t("au.besties.loading1"), + }, + { + label: t("au.besties.loading2"), + }, + { + label: t("au.besties.loading3"), + }, + { + label: t("au.besties.loading4"), + }, + ]; + + const handleNext = () => { + return navigate(routes.client.home()); + }; + + const loadData = useCallback(async () => { + const payload: AIRequestsV2.Payload = { + aiRequest: { + birthDate, + sign: getZodiacSignByDate(birthDate).toLowerCase(), + }, + promptKey: "horoscope_besties", + token, + }; + const aIRequest = await api.AIRequestsV2(payload); + if (aIRequest.ai_request.state !== "ready") { + const getAIRequest = async () => { + const aIRequestById = await api.getAIRequestsV2({ + id: aIRequest.ai_request.id, + token, + }); + if (aIRequestById.ai_request.state !== "ready") { + setTimeout(getAIRequest, 3000); + } + setText(aIRequestById?.ai_request?.response?.body || "Loading..."); + setIsDataLoading(false); + checkLoading(); + return aIRequestById.ai_request; + }; + return await getAIRequest(); + } + setIsDataLoading(false); + checkLoading(); + setText(aIRequest?.ai_request?.response?.response?.body || "Loading..."); + + return aIRequest?.ai_request?.response; + }, [api, token, birthDate]); + + useApiCall(loadData); + + useEffect(() => { + (async () => { + try { + const { asset_categories } = await api.getAssetCategories({ locale }); + const categoryId = getCategoryIdByZodiacSign( + zodiacSign, + asset_categories + ); + const assets = ( + await api.getAssets({ category: String(categoryId || "1") }) + ).assets; + const randomAsset = assets[getRandomArbitrary(0, assets.length - 1)]; + setBackgroundUrl(randomAsset.url); + } catch (error) { + console.error("Error: ", error); + } + })(); + }, [api, locale, zodiacSign]); + + const getPaddingBottomPage = () => { + if (showNavbarFooter) return "164px"; + return "108px"; + }; + + function checkLoading() { + if (isVisualLoading || isDataLoading) { + setIsOpenModal(true); + } else { + setIsOpenModal(false); + } + } + + return ( +
+ + { + setIsVisualLoading(false); + checkLoading(); + }} + > + + {t("au.besties.title")}{" "} + <span className={styles["loading-name"]}>{name}</span> + + <> + + +
+
+
+
+ + {t("au.besties.result")}{" "} + <span className={styles["loading-name"]}>{name}</span> + +
+

{text}

+
+ ); +} + +export default BestiesHoroscopeResult; diff --git a/src/components/pages/BestiesHoroscopeResult/styles.module.css b/src/components/pages/BestiesHoroscopeResult/styles.module.css new file mode 100644 index 0000000..b58138b --- /dev/null +++ b/src/components/pages/BestiesHoroscopeResult/styles.module.css @@ -0,0 +1,79 @@ +.page { + position: relative; + height: fit-content; + min-height: 100vh; + flex: auto; + /* max-height: -webkit-fill-available; */ + background-color: #000; + color: #fff; + overflow-y: scroll; + padding-bottom: 180px; + display: flex; + flex-direction: column; + gap: 20px; + align-items: center; +} + +.loading-name { + color: #fff; +} + +.sign-image { + width: 100%; + height: 446px; + color: #fd433f; + border-radius: 17px; + background-color: #000; + background-position: center; + background-repeat: no-repeat; + background-size: cover; + display: flex; + flex-direction: column; + justify-content: flex-end; +} + +.text { + font-size: 18px; + line-height: 22px; + font-weight: 400; + padding: 0 8px; + text-align: left; +} + +.cross-container { + width: 100%; + display: flex; + flex-direction: row; + justify-content: flex-end; +} + +.cross { + width: 24px; + height: 24px; + border: solid 2px #bdbdbd; + border-radius: 100%; + rotate: 45deg; + cursor: pointer; +} + +.cross::before { + content: ""; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + width: 10px; + height: 2px; + background-color: #bdbdbd; +} + +.cross::after { + content: ""; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + width: 2px; + height: 10px; + background-color: #bdbdbd; +} diff --git a/src/routes.ts b/src/routes.ts index ebd2eae..4db4d3d 100644 --- a/src/routes.ts +++ b/src/routes.ts @@ -34,6 +34,7 @@ const routes = { home: () => [host, "home"].join("/"), breathResult: () => [host, "breath", "result"].join("/"), magicBall: () => [host, "magic-ball"].join("/"), + horoscopeBestiesResult: () => [host, "horoscope", "besties"].join("/"), }, server: { appleAuth: (origin: string) => [apiHost, "auth", "apple", `gate?origin=${origin}`].join("/"), @@ -81,6 +82,8 @@ const routes = { getUserCallbacks: (id: string) => [apiHost, prefix, "user", "callbacks", `${id}.json`].join("/"), getTranslations: () => [siteHost, "api/v2", "t.json"].join("/"), + aiRequestsV2: (promptKey: string) => [apiHost, "api/v2", "ai", "prompts", promptKey, "requests.json"].join("/"), + getAiRequestsV2: (id: string) => [apiHost, "api/v2", "ai", "requests", `${id}.json`].join("/"), }, }; @@ -133,6 +136,7 @@ export const withoutFooterRoutes = [ routes.client.paymentFail(), routes.client.paymentStripe(), routes.client.magicBall(), + routes.client.horoscopeBestiesResult(), ]; export const hasNoFooter = (path: string) => !withoutFooterRoutes.includes(path); @@ -156,6 +160,7 @@ export const withoutHeaderRoutes = [ routes.client.paymentSuccess(), routes.client.paymentFail(), routes.client.magicBall(), + routes.client.horoscopeBestiesResult(), ]; export const hasNoHeader = (path: string) => !withoutHeaderRoutes.includes(path);