Merge branch 'develop' into 'main'

hint-palm

See merge request witapp/aura-webapp!673
This commit is contained in:
Daniil Chemerkin 2025-03-07 16:32:01 +00:00
commit 51bfc8a029
12 changed files with 439 additions and 157 deletions

View File

@ -1,9 +1,7 @@
import styles from "./styles.module.scss";
import Title from "@/components/Title";
import { useDispatch, useSelector } from "react-redux";
import { actions, selectors } from "@/store";
import { Gender } from "@/data";
import PrivacyPolicy from "@/components/pages/ABDesign/v1/components/PrivacyPolicy";
// import Toast from "@/components/pages/ABDesign/v1/components/Toast";
import { useTranslations } from "@/hooks/translations";
import { ELocalesPlacement } from "@/locales";
@ -17,10 +15,11 @@ import { usePreloadImages } from "@/hooks/preload/images";
import { ELottieKeys, useLottie } from "@/hooks/lottie/useLottie";
import { useSession } from "@/hooks/session/useSession";
import { EGender, ESourceAuthorization } from "@/api/resources/User";
import AlreadyHaveAccount from "@/components/ui/AlreadyHaveAccount";
import Answer from "../../components/Answer";
import Loader, { LoaderColor } from "@/components/Loader";
import { useUnleash } from "@/hooks/ab/unleash/useUnleash";
import { EUnleashFlags, useUnleash } from "@/hooks/ab/unleash/useUnleash";
import GenderV0 from "./variants/GenderV0";
import GenderV1 from "./variants/GenderV1";
import GenderV2 from "./variants/GenderV2";
function GenderPage() {
const { translate } = useTranslations(ELocalesPlacement.CompatibilityV2);
@ -41,7 +40,7 @@ function GenderPage() {
const { flags, ready } = useMetricABFlags();
const { isReady, variant: genderPageType } = useUnleash({
flag: "genderPageType"
flag: EUnleashFlags.genderPageType
});
const pageType = flags?.genderPageType?.[0] || genderPageType || "v2";
@ -115,142 +114,19 @@ function GenderPage() {
switch (pageType) {
case "v0":
return (
<>
<Title variant="h2" className={styles.title}>
{translate("/gender.title")}
</Title>
<p className={styles.description}>{translate("/gender.description", {
br: <br />,
})}</p>
{/* <ChooseGender onSelectGender={selectGender} /> */}
<PrivacyPolicy containerClassName={styles["privacy-policy"]} haveCheckbox={false} />
<div className={styles["genders-container"]}>
{localGenders.map((_gender, index) => (
<Answer
key={index}
answer={_gender}
isSelected={gender === _gender.id}
onClick={() => selectGender(genders.find((g) => g.id === _gender.id) ?? null)}
/>
))}
</div>
<AlreadyHaveAccount text={translate("/gender.already_have_account")} />
{/* {gender && !privacyPolicyChecked && (
<Toast classNameContainer={styles["toast-container"]} variant="error">
{translate("/gender.toast", undefined, ELocalesPlacement.V1)}
</Toast>
)} */}
</>
<GenderV0 localGenders={localGenders} gender={gender} selectGender={selectGender} />
)
case "v1":
return (
<>
<Title variant="h2" className={styles.title}>
{translate("/gender.v1.title", {
br: <br />,
})}
</Title>
<p className={styles.subtitle}>{translate("/gender.v1.subtitle", {
br: <br />,
})}</p>
<ul className={styles.points}>
{Array.from({ length: 4 }).map((_, index) => (
<li key={index}>
{translate(`/gender.v1.points.point${index + 1}`)}
</li>
))}
</ul>
{/* <ChooseGender onSelectGender={selectGender} /> */}
<div className={styles["genders-container"]}>
{localGenders.map((_gender, index) => (
<Answer
key={index}
answer={_gender}
isSelected={gender === _gender.id}
onClick={() => selectGender(genders.find((g) => g.id === _gender.id) ?? null)}
/>
))}
</div>
<PrivacyPolicy containerClassName={styles["privacy-policy"]} haveCheckbox={false} />
<AlreadyHaveAccount text={translate("/gender.already_have_account")} />
{/* {gender && !privacyPolicyChecked && (
<Toast classNameContainer={styles["toast-container"]} variant="error">
{translate("/gender.toast", undefined, ELocalesPlacement.V1)}
</Toast>
)} */}
</>
<GenderV1 localGenders={localGenders} gender={gender} selectGender={selectGender} />
)
case "v2":
return (
<>
<Title variant="h2" className={styles.title}>
{translate("/gender.v2.title", {
br: <br />,
})}
</Title>
<ul className={styles.points}>
{Array.from({ length: 5 }).map((_, index) => (
<li key={index}>
{translate(`/gender.v2.points.point${index + 1}`)}
</li>
))}
</ul>
<p
className={styles.subtitle}
style={{
marginTop: "28px",
}}
>{translate("/gender.v2.subtitle", {
br: <br />,
})}</p>
{/* <ChooseGender onSelectGender={selectGender} /> */}
<div className={styles["genders-container"]}>
{localGenders.map((_gender, index) => (
<Answer
key={index}
answer={_gender}
isSelected={gender === _gender.id}
onClick={() => selectGender(genders.find((g) => g.id === _gender.id) ?? null)}
/>
))}
</div>
<PrivacyPolicy containerClassName={styles["privacy-policy"]} haveCheckbox={false} />
<AlreadyHaveAccount text={translate("/gender.already_have_account")} />
{/* {gender && !privacyPolicyChecked && (
<Toast classNameContainer={styles["toast-container"]} variant="error">
{translate("/gender.toast", undefined, ELocalesPlacement.V1)}
</Toast>
)} */}
</>
<GenderV2 localGenders={localGenders} gender={gender} selectGender={selectGender} />
)
default:
return (
<>
<Title variant="h2" className={styles.title}>
{translate("/gender.title")}
</Title>
<p className={styles.description}>{translate("/gender.description", {
br: <br />,
})}</p>
{/* <ChooseGender onSelectGender={selectGender} /> */}
<PrivacyPolicy containerClassName={styles["privacy-policy"]} haveCheckbox={false} />
<div className={styles["genders-container"]}>
{localGenders.map((_gender, index) => (
<Answer
key={index}
answer={_gender}
isSelected={gender === _gender.id}
onClick={() => selectGender(genders.find((g) => g.id === _gender.id) ?? null)}
/>
))}
</div>
<AlreadyHaveAccount text={translate("/gender.already_have_account")} />
{/* {gender && !privacyPolicyChecked && (
<Toast classNameContainer={styles["toast-container"]} variant="error">
{translate("/gender.toast", undefined, ELocalesPlacement.V1)}
</Toast>
)} */}
</>
<GenderV0 localGenders={localGenders} gender={gender} selectGender={selectGender} />
);
}

View File

@ -0,0 +1,53 @@
import Title from "@/components/Title"
import styles from "../../styles.module.scss"
import { useTranslations } from "@/hooks/translations";
import { ELocalesPlacement } from "@/locales";
import PrivacyPolicy from "@/components/pages/ABDesign/v1/components/PrivacyPolicy";
import { Gender } from "@/data";
import { genders } from "@/components/pages/ABDesign/v1/data/genders";
import Answer from "@/components/CompatibilityV2/components/Answer";
import AlreadyHaveAccount from "@/components/ui/AlreadyHaveAccount";
interface IGenderV0Props {
localGenders: Array<{
id: string;
title: React.ReactNode | string;
}>;
gender: string;
selectGender: (gender: Gender | null) => void;
}
function GenderV0({ localGenders, gender, selectGender }: IGenderV0Props) {
const { translate } = useTranslations(ELocalesPlacement.CompatibilityV2);
return (
<>
<Title variant="h2" className={styles.title}>
{translate("/gender.title")}
</Title>
<p className={styles.description}>{translate("/gender.description", {
br: <br />,
})}</p>
{/* <ChooseGender onSelectGender={selectGender} /> */}
<PrivacyPolicy containerClassName={styles["privacy-policy"]} haveCheckbox={false} />
<div className={styles["genders-container"]}>
{localGenders.map((_gender, index) => (
<Answer
key={index}
answer={_gender}
isSelected={gender === _gender.id}
onClick={() => selectGender(genders.find((g) => g.id === _gender.id) ?? null)}
/>
))}
</div>
<AlreadyHaveAccount text={translate("/gender.already_have_account")} />
{/* {gender && !privacyPolicyChecked && (
<Toast classNameContainer={styles["toast-container"]} variant="error">
{translate("/gender.toast", undefined, ELocalesPlacement.V1)}
</Toast>
)} */}
</>
)
}
export default GenderV0

View File

@ -0,0 +1,62 @@
import Title from "@/components/Title"
import styles from "../../styles.module.scss"
import { useTranslations } from "@/hooks/translations";
import { ELocalesPlacement } from "@/locales";
import PrivacyPolicy from "@/components/pages/ABDesign/v1/components/PrivacyPolicy";
import { Gender } from "@/data";
import { genders } from "@/components/pages/ABDesign/v1/data/genders";
import Answer from "@/components/CompatibilityV2/components/Answer";
import AlreadyHaveAccount from "@/components/ui/AlreadyHaveAccount";
interface IGenderV1Props {
localGenders: Array<{
id: string;
title: React.ReactNode | string;
}>;
gender: string;
selectGender: (gender: Gender | null) => void;
}
function GenderV1({ localGenders, gender, selectGender }: IGenderV1Props) {
const { translate } = useTranslations(ELocalesPlacement.CompatibilityV2);
return (
<>
<Title variant="h2" className={styles.title}>
{translate("/gender.v1.title", {
br: <br />,
})}
</Title>
<p className={styles.subtitle}>{translate("/gender.v1.subtitle", {
br: <br />,
})}</p>
<ul className={styles.points}>
{Array.from({ length: 4 }).map((_, index) => (
<li key={index}>
{translate(`/gender.v1.points.point${index + 1}`)}
</li>
))}
</ul>
{/* <ChooseGender onSelectGender={selectGender} /> */}
<div className={styles["genders-container"]}>
{localGenders.map((_gender, index) => (
<Answer
key={index}
answer={_gender}
isSelected={gender === _gender.id}
onClick={() => selectGender(genders.find((g) => g.id === _gender.id) ?? null)}
/>
))}
</div>
<PrivacyPolicy containerClassName={styles["privacy-policy"]} haveCheckbox={false} />
<AlreadyHaveAccount text={translate("/gender.already_have_account")} />
{/* {gender && !privacyPolicyChecked && (
<Toast classNameContainer={styles["toast-container"]} variant="error">
{translate("/gender.toast", undefined, ELocalesPlacement.V1)}
</Toast>
)} */}
</>
)
}
export default GenderV1

View File

@ -0,0 +1,67 @@
import Title from "@/components/Title"
import styles from "../../styles.module.scss"
import { useTranslations } from "@/hooks/translations";
import { ELocalesPlacement } from "@/locales";
import PrivacyPolicy from "@/components/pages/ABDesign/v1/components/PrivacyPolicy";
import { Gender } from "@/data";
import { genders } from "@/components/pages/ABDesign/v1/data/genders";
import Answer from "@/components/CompatibilityV2/components/Answer";
import AlreadyHaveAccount from "@/components/ui/AlreadyHaveAccount";
interface IGenderV2Props {
localGenders: Array<{
id: string;
title: React.ReactNode | string;
}>;
gender: string;
selectGender: (gender: Gender | null) => void;
}
function GenderV2({ localGenders, gender, selectGender }: IGenderV2Props) {
const { translate } = useTranslations(ELocalesPlacement.CompatibilityV2);
return (
<>
<Title variant="h2" className={styles.title}>
{translate("/gender.v2.title", {
br: <br />,
})}
</Title>
<ul className={styles.points}>
{Array.from({ length: 5 }).map((_, index) => (
<li key={index}>
{translate(`/gender.v2.points.point${index + 1}`)}
</li>
))}
</ul>
<p
className={styles.subtitle}
style={{
marginTop: "28px",
}}
>{translate("/gender.v2.subtitle", {
br: <br />,
})}</p>
{/* <ChooseGender onSelectGender={selectGender} /> */}
<div className={styles["genders-container"]}>
{localGenders.map((_gender, index) => (
<Answer
key={index}
answer={_gender}
isSelected={gender === _gender.id}
onClick={() => selectGender(genders.find((g) => g.id === _gender.id) ?? null)}
/>
))}
</div>
<PrivacyPolicy containerClassName={styles["privacy-policy"]} haveCheckbox={false} />
<AlreadyHaveAccount text={translate("/gender.already_have_account")} />
{/* {gender && !privacyPolicyChecked && (
<Toast classNameContainer={styles["toast-container"]} variant="error">
{translate("/gender.toast", undefined, ELocalesPlacement.V1)}
</Toast>
)} */}
</>
)
}
export default GenderV2

View File

@ -12,6 +12,8 @@ import { selectors } from "@/store";
import { useSelector } from "react-redux";
import { images } from "../../data";
import { getZodiacSignByDate } from "@/services/zodiac-sign";
import { EUnleashFlags, useUnleash } from "@/hooks/ab/unleash/useUnleash";
import Loader, { LoaderColor } from "@/components/Loader";
function PalmsInformation() {
const { translate } = useTranslations(ELocalesPlacement.CompatibilityV2);
@ -24,10 +26,18 @@ function PalmsInformation() {
preloadKey: ELottieKeys.scalesHeadPalmistry,
});
const { isReady, variant: zodiacImages } = useUnleash({
flag: EUnleashFlags.zodiacImages
});
const handleNext = () => {
navigate(routes.client.compatibilityV2RelationshipStatus());
};
if (!isReady) {
return <Loader color={LoaderColor.Black} />;
}
return (
<div className={styles["page-container"]}>
{/* {animationData && (
@ -39,13 +49,25 @@ function PalmsInformation() {
width={1920}
/>
)} */}
<div className={styles.zodiac}>
<img
className={styles.image}
src={images(`zodiacs/${gender}/${zodiacSign.toUpperCase()}.webp`)}
alt="Zodiac sign"
/>
</div>
{zodiacImages !== "new" && (
<div className={styles.zodiac}>
<img
className={styles.image}
src={images(`zodiacs/${gender}/${zodiacSign.toUpperCase()}.webp`)}
alt="Zodiac sign"
/>
</div>
)}
{zodiacImages === "new" && (
<div className={styles.zodiacNew}>
<img
className={styles.image}
// src={images(`zodiacs/${gender}/${zodiacSign.toUpperCase()}.webp`)}
src={`/zodiac-signs/${gender?.toLowerCase()}/${zodiacSign.toLowerCase()}.svg`}
alt="Zodiac sign"
/>
</div>
)}
<Title variant="h2" className={styles.title}>
{translate(`/palms-information.${zodiacSign.toLowerCase()}.title`)}
</Title>

View File

@ -56,4 +56,19 @@
.description {
white-space: pre-line;
margin-bottom: 24px;
}
.zodiacNew {
position: relative;
width: 100%;
display: flex;
justify-content: center;
align-items: center;
& > img {
height: 100%;
position: relative;
max-width: 260px;
z-index: -2;
}
}

View File

@ -11,11 +11,13 @@ import { selectors } from "@/store";
import { useSelector } from "react-redux";
import { images } from "../../data";
import { getZodiacSignByDate } from "@/services/zodiac-sign";
import { EUnleashFlags, useUnleash } from "@/hooks/ab/unleash/useUnleash";
import Loader, { LoaderColor } from "@/components/Loader";
function PalmsInformationPartner() {
const { translate } = useTranslations(ELocalesPlacement.CompatibilityV2);
const { partnerGender, partnerBirthdate } = useSelector(selectors.selectQuestionnaire);
const zodiacSign = getZodiacSignByDate(partnerBirthdate);
const navigate = useNavigate();
// const { animationData } =
@ -24,10 +26,18 @@ function PalmsInformationPartner() {
preloadKey: ELottieKeys.scalesHeadPalmistry,
});
const { isReady, variant: zodiacImages } = useUnleash({
flag: EUnleashFlags.zodiacImages
});
const handleNext = () => {
navigate(routes.client.compatibilityV2DateEvent());
};
if (!isReady) {
return <Loader color={LoaderColor.Black} />;
}
return (
<div className={styles["page-container"]}>
{/* {animationData && (
@ -39,13 +49,25 @@ function PalmsInformationPartner() {
width={1920}
/>
)} */}
<div className={styles.zodiac}>
<img
className={styles.image}
src={images(`zodiacs/${partnerGender}/${zodiacSign?.toUpperCase()}.webp`)}
alt="Zodiac sign"
/>
</div>
{zodiacImages !== "new" && (
<div className={styles.zodiac}>
<img
className={styles.image}
src={images(`zodiacs/${partnerGender}/${zodiacSign.toUpperCase()}.webp`)}
alt="Zodiac sign"
/>
</div>
)}
{zodiacImages === "new" && (
<div className={styles.zodiacNew}>
<img
className={styles.image}
// src={images(`zodiacs/${gender}/${zodiacSign.toUpperCase()}.webp`)}
src={`/zodiac-signs/${partnerGender?.toLowerCase()}/${zodiacSign.toLowerCase()}.svg`}
alt="Zodiac sign"
/>
</div>
)}
<Title variant="h2" className={styles.title}>
{translate(`/palms-information-partner.${zodiacSign?.toLowerCase()}.title`)}
</Title>

View File

@ -56,4 +56,19 @@
.description {
white-space: pre-line;
margin-bottom: 24px;
}
.zodiacNew {
position: relative;
width: 100%;
display: flex;
justify-content: center;
align-items: center;
& > img {
height: 100%;
position: relative;
max-width: 260px;
z-index: -2;
}
}

View File

@ -0,0 +1,39 @@
import styles from "./styles.module.scss"
interface ZodiacImagesProps {
gender: string;
partnerGender?: string;
zodiacSign: string;
partnerZodiacSign?: string;
relationshipStatus: string;
classNameContainer?: string;
}
function ZodiacImages({
gender,
partnerGender,
zodiacSign,
partnerZodiacSign,
relationshipStatus,
classNameContainer = ""
}: ZodiacImagesProps) {
const getZodiacImagesWithPartnerClassName = () => {
if (relationshipStatus === "single") {
return "";
}
return `${styles["with-partner"]} ${styles[`with-partner-${gender}-${partnerGender}`]}`;
}
return (
<div
className={`${styles["zodiac-images"]} ${getZodiacImagesWithPartnerClassName()} ${classNameContainer}`}
// style={{ marginBottom: `${-height / 2}px` }}
>
<img src={`/zodiac-signs/${gender?.toLowerCase()}/${zodiacSign?.toLowerCase()}.svg`} alt="Profile zodiac sign" />
{relationshipStatus !== "single" && <img src={`/zodiac-signs/${partnerGender?.toLowerCase()}/${partnerZodiacSign?.toLowerCase()}.svg`} alt="Partner zodiac sign" />}
</div>
)
}
export default ZodiacImages

View File

@ -0,0 +1,66 @@
.zodiac-images {
position: relative;
width: 100dvw;
max-width: 560px;
display: flex;
justify-content: space-between;
align-items: center;
gap: 0px;
margin-top: 8px;
&.with-partner {
// &>img:first-child {
// margin-right: -30%;
// }
&>img:last-child {
// margin-left: -10px;
z-index: 2;
}
&>img {
width: 50%;
}
}
img {
position: relative;
width: 70%;
object-fit: cover;
z-index: 3;
}
&.with-partner-male-female {
flex-direction: row-reverse;
}
&.with-partner-male-male {
&>img:first-child {
transform: scaleX(-1);
}
}
&.with-partner-female-female {
&>img:last-child {
transform: scaleX(-1);
}
}
// &::after {
// content: "";
// position: absolute;
// bottom: 0;
// left: 0;
// width: 100%;
// height: 60%;
// background: linear-gradient(to bottom,
// rgba(255, 255, 255, 0) 0%,
// rgba(255, 255, 255, .7) 10%,
// rgba(255, 255, 255, 1) 15%,
// rgba(255, 255, 255, 1) 30%,
// rgba(255, 255, 255, 1) 40%,
// rgba(255, 255, 255, 1) 100%);
// pointer-events: none;
// z-index: -1;
// }
}

View File

@ -24,6 +24,9 @@ import { formatDateToLocale } from "@/locales/localFormats";
import { useEffect } from "react";
import metricService, { EGoals, EMetrics } from "@/services/metric/metricService";
import MoneyBackGuarantee from "../../components/MoneyBackGuarantee";
import ZodiacImages from "./components/ZodiacImages";
import { EUnleashFlags, useUnleash } from "@/hooks/ab/unleash/useUnleash";
import Loader, { LoaderColor } from "@/components/Loader";
function TrialPayment() {
const { height, elementRef } = useDynamicSize<HTMLDivElement>({});
@ -41,6 +44,10 @@ function TrialPayment() {
"/v1/palmistry/ticket.svg",
])
const { isReady, variant: zodiacImages } = useUnleash({
flag: EUnleashFlags.zodiacImages
});
const handleNext = () => {
navigate(routes.client.compatibilityV2Payment());
};
@ -50,19 +57,31 @@ function TrialPayment() {
metricService.reachGoal(EGoals.AURA_TRIAL_PAYMENT_PAGE_VISIT, [EMetrics.KLAVIYO]);
}, []);
if (!isReady) {
return <Loader color={LoaderColor.Black} />;
}
return (
<>
<Title className={styles["information-title"]}>
{translate("/trial-payment.information-title")}
</Title>
<div
{zodiacImages === "new" && <ZodiacImages
gender={gender}
zodiacSign={zodiacSign}
relationshipStatus={relationshipStatus}
partnerGender={partnerGender}
partnerZodiacSign={partnerZodiacSign}
/>}
{zodiacImages !== "new" && <div
className={`${styles["zodiac-images"]} ${relationshipStatus !== "single" ? styles["with-partner"] : ""}`}
ref={elementRef}
style={{ marginBottom: `${-height / 2}px` }}
>
<img src={images(`zodiacs/${gender}/${zodiacSign?.toUpperCase()}.webp`)} alt="Profile zodiac sign" />
{relationshipStatus !== "single" && <img src={images(`zodiacs/${partnerGender}/${partnerZodiacSign?.toUpperCase()}.webp`)} alt="Partner zodiac sign" />}
</div>
</div>}
{(relationshipStatus === "single" || !partnerBirthdate) &&
<p className={styles["information-description"]}>
{translate("/trial-payment.information-description-single", {

View File

@ -1,25 +1,51 @@
import { useFlagsStatus, useUnleashClient, useVariant } from "@unleash/proxy-client-react";
import { useEffect, useMemo } from "react";
import { useSearchParams } from "react-router-dom";
interface IUseUnleashProps {
flag: string;
export enum EUnleashFlags {
"genderPageType" = "genderPageType",
"zodiacImages" = "zodiacImages",
}
export const useUnleash = ({
/**
* Интерфейс для входных параметров хука useUnleash
* Использует дженерик T для типизации флага
*/
interface IUseUnleashProps<T extends EUnleashFlags> {
flag: T;
}
/**
* Интерфейс для возможных вариантов значений флагов
* Каждый ключ соответствует флагу из EUnleashFlags
*/
interface IVariants {
[EUnleashFlags.genderPageType]: "v0" | "v1" | "v2";
[EUnleashFlags.zodiacImages]: "new" | "old";
}
/**
* Хук для получения значения A/B теста по флагу
* @template T - Тип флага из EUnleashFlags
* @returns Объект с информацией о готовности флага и его значении, типизированным в зависимости от переданного флага
*/
export const useUnleash = <T extends EUnleashFlags>({
flag
}: IUseUnleashProps) => {
}: IUseUnleashProps<T>) => {
const { flagsReady } = useFlagsStatus();
const unleashClient = useUnleashClient();
const abVariant = useVariant(flag);
// const isEnabled = useFlag(flag);
const [searchParams] = useSearchParams();
const variantFromParams = searchParams.get(flag);
const isReady = useMemo(() => {
return flagsReady ?? true;
}, [flagsReady]);
const variant = useMemo(() => {
return abVariant?.payload?.value;
}, [abVariant]);
return variantFromParams || abVariant?.payload?.value;
}, [abVariant, variantFromParams]) as IVariants[T];
useEffect(() => {
unleashClient.on("impression", (impressionEvent: any) => {
@ -43,5 +69,5 @@ export const useUnleash = ({
}), [
isReady,
variant
])
]) as { isReady: boolean; variant: IVariants[T] };
};