feat: add besties horoscope result page

This commit is contained in:
gofnnp 2023-11-24 05:25:42 +04:00
parent 0f6f8f7e72
commit 16d79d3f3a
13 changed files with 518 additions and 10 deletions

View File

@ -22,7 +22,8 @@ import {
Zodiacs,
GoogleAuth,
SubscriptionPlans,
AppleAuth
AppleAuth,
AIRequestsV2
} from './resources'
const api = {
@ -52,6 +53,8 @@ const api = {
getUserCallbacks: createMethod<UserCallbacks.PayloadGet, UserCallbacks.Response>(UserCallbacks.createRequestGet),
getTranslations: createMethod<Translations.Payload, Translations.Response>(Translations.createRequest),
getZodiacs: createMethod<Zodiacs.Payload, Zodiacs.Response>(Zodiacs.createRequest),
AIRequestsV2: createMethod<AIRequestsV2.Payload, AIRequestsV2.Response>(AIRequestsV2.createRequest),
getAIRequestsV2: createMethod<AIRequestsV2.PayloadGet, AIRequestsV2.IAiResponseGet>(AIRequestsV2.createRequestGet),
}
export type ApiContextValue = typeof api

View File

@ -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),
});
};

View File

@ -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";

View File

@ -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<boolean>(false);
@ -179,10 +180,6 @@ function App(): JSX.Element {
element={<ProtectWallpaperPage />}
/> */}
</Route>
<Route
path={routes.client.magicBall()}
element={<MagicBallPage />}
/>
<Route element={<PrivateOutlet />}>
<Route element={<AuthorizedUserOutlet />}>
<Route
@ -220,6 +217,14 @@ function App(): JSX.Element {
path={routes.client.wallpaper()}
element={<WallpaperPage />}
/>
<Route
path={routes.client.magicBall()}
element={<MagicBallPage />}
/>
<Route
path={routes.client.horoscopeBestiesResult()}
element={<BestiesHoroscopeResult />}
/>
</Route>
</Route>
<Route path="*" element={<NotFoundPage />} />

View File

@ -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}
>
<p className={styles.text}>
{t("au.besties.result")}

View File

@ -11,6 +11,7 @@
background-size: cover !important;
background-position: center !important;
background-color: #000 !important;
cursor: pointer;
}
.text {

View File

@ -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 (
<section
className={`${styles.page} page`}
@ -143,6 +150,12 @@ function HomePage(): JSX.Element {
backgroundImage: `url(${asset?.url.replace("http://", "https://")})`,
}}
>
{/* <div
className={styles.background}
style={{
backgroundImage: `url(${asset?.url.replace("http://", "https://")})`,
}}
> */}
<div className={styles.header}>
<BlurringSubstrate>
<div className={styles["header__energies"]}>
@ -214,7 +227,13 @@ function HomePage(): JSX.Element {
</Title>
<Slider>
{bestiesHoroscopes.map((item, index) => (
<BestiesHoroscopeSlider data={item} key={index} />
<BestiesHoroscopeSlider
data={item}
key={index}
onClick={() => {
handleBestiesHoroscope(item);
}}
/>
))}
</Slider>
</div>
@ -242,6 +261,7 @@ function HomePage(): JSX.Element {
))}
</div>
</div>
{/* </div> */}
</section>
);
}

View File

@ -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%;

View File

@ -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<IProgressBarsModalProps>): 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 (
<section
className={`${styles.container} ${className || ""}`}
style={{ paddingBottom: showNavbarFooter ? "48px" : "16px" }}
>
<div className={styles.title}>{children[0]}</div>
{progressBars.map((progressBar, index) => (
<div className={styles["progress-bar"]} key={index}>
<Title variant="h4" className={styles["progress-bar__label"]}>
{progressBar.label}
</Title>
<div className={styles["progress-bar__container"]}>
<ProgressBarLine
containerClassName={styles["progress-bar__line"]}
value={getProgressValue(index, progress)}
delay={50}
/>
<span className={styles["progress-bar__percentage"]}>
{getProgressValue(index, progress)}%
</span>
</div>
</div>
))}
<Spinner width="40px" strokeWidth={3} />
</section>
);
}
export default ProgressBarsModal;

View File

@ -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);
}

View File

@ -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<AIRequestsV2.IAIRequest>(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 (
<section
className={`${styles.page} page`}
style={{ paddingBottom: getPaddingBottomPage() }}
>
<FullScreenModal isOpen={isOpenModal}>
<ProgressBarsModal
progressBars={progressBars}
onEndLoading={() => {
setIsVisualLoading(false);
checkLoading();
}}
>
<Title variant="h2">
{t("au.besties.title")}{" "}
<span className={styles["loading-name"]}>{name}</span>
</Title>
<></>
</ProgressBarsModal>
</FullScreenModal>
<div className={styles["cross-container"]}>
<div className={styles.cross} onClick={handleNext}></div>
</div>
<div
className={styles["sign-image"]}
style={{ backgroundImage: `url(${backgroundUrl})` }}
>
<Title variant="h2">
{t("au.besties.result")}{" "}
<span className={styles["loading-name"]}>{name}</span>
</Title>
</div>
<p className={styles.text}>{text}</p>
</section>
);
}
export default BestiesHoroscopeResult;

View File

@ -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;
}

View File

@ -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);