Merge branch 'develop' into 'main'

AW-483-484-485-fix-bugs

See merge request witapp/aura-webapp!826
This commit is contained in:
Daniil Chemerkin 2025-09-10 21:24:26 +00:00
commit 4b7332f4d5
16 changed files with 959 additions and 24 deletions

View File

@ -462,7 +462,7 @@
"personalized_offer": "Personalized offer reserved",
"title": "Start your <trialPeriod> trial",
"total_today": "Total today",
"code_applied_bold": "WITLAB24",
"code_applied_bold": "HAIR50",
"code_applied": "Code <bold> applied!"
},
"guarantees": {
@ -685,8 +685,42 @@
"text": "\"Я не ожидала такого результата - приложение дало точные подсказки, которые помогли мне разобраться в чувствах и принять важное решение. Это действительно работает.\"",
"likes": "1.2K"
},
"v3": {
"quote": "“A tool that becomes your ally on the path to a harmonious relationship.”"
},
"v4": {
"title": "Where did you first hear about us?",
"description": "Many people say they first learned about us through leading publications and platforms.",
"answers": {
"nyt": "The New York Times",
"forbes": "Forbes",
"cosmopolitan": "Cosmopolitan",
"oprah": "Oprah Daily",
"social": "Social media",
"other": "Other sources"
}
},
"button": "Continue"
},
"/wheel-of-fortune": {
"title": "Spin the wheel for your discount!",
"description": "Wheel of Fortune",
"button_stop": "STOP",
"button_last_chance": "Try your last chance",
"skip_button": "Skip for now"
},
"/special-offer": {
"title": "Тебе повезло!",
"description": "Ты получил специальную эксклюзивную скидку на 94%",
"button": "Continue",
"offer": {
"title": "Special Offer",
"discount": "94% OFF",
"discount_description": "Одноразовая эксклюзивная скидка",
"promo_code": "HAIR50",
"description": "Скопируйте или нажмите <button> чтобы применить скидку"
}
},
"period": {
"day_one": "{{count}} day",
"day_other": "{{count}} days",

Binary file not shown.

After

Width:  |  Height:  |  Size: 172 KiB

View File

@ -0,0 +1,86 @@
import Title from "@/components/Title";
import styles from "./styles.module.scss";
import { useTranslations } from "@/hooks/translations";
import { ELocalesPlacement } from "@/locales";
interface SpecialOfferBannerProps {
handleCopyCode: () => void;
}
function SpecialOfferBanner({ handleCopyCode }: SpecialOfferBannerProps) {
const { translate } = useTranslations(ELocalesPlacement.CompatibilityV2);
return (
<div className={styles.container}>
<svg
width="30"
height="31"
viewBox="0 0 30 31"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M11.1621 4.78125L13.2012 8.25H13.125H8.90625C7.61133 8.25 6.5625 7.20117 6.5625 5.90625C6.5625 4.61133 7.61133 3.5625 8.90625 3.5625H9.03516C9.9082 3.5625 10.7227 4.02539 11.1621 4.78125ZM3.75 5.90625C3.75 6.75 3.95508 7.54688 4.3125 8.25H1.875C0.837891 8.25 0 9.08789 0 10.125V13.875C0 14.9121 0.837891 15.75 1.875 15.75H28.125C29.1621 15.75 30 14.9121 30 13.875V10.125C30 9.08789 29.1621 8.25 28.125 8.25H25.6875C26.0449 7.54688 26.25 6.75 26.25 5.90625C26.25 3.05859 23.9414 0.75 21.0938 0.75H20.9648C19.0957 0.75 17.3613 1.74023 16.4121 3.35156L15 5.75977L13.5879 3.35742C12.6387 1.74023 10.9043 0.75 9.03516 0.75H8.90625C6.05859 0.75 3.75 3.05859 3.75 5.90625ZM23.4375 5.90625C23.4375 7.20117 22.3887 8.25 21.0938 8.25H16.875H16.7988L18.8379 4.78125C19.2832 4.02539 20.0918 3.5625 20.9648 3.5625H21.0938C22.3887 3.5625 23.4375 4.61133 23.4375 5.90625ZM1.875 17.625V27.9375C1.875 29.4902 3.13477 30.75 4.6875 30.75H13.125V17.625H1.875ZM16.875 30.75H25.3125C26.8652 30.75 28.125 29.4902 28.125 27.9375V17.625H16.875V30.75Z"
fill="#FCD34D"
/>
</svg>
<Title variant="h2" className={styles.title}>
{translate("/special-offer.offer.title")}
</Title>
<div className={styles.offer}>
<Title variant="h3" className={styles.offerTitle}>
{translate("/special-offer.offer.discount")}
</Title>
<p className={styles.offerDescription}>
{translate("/special-offer.offer.discount_description")}
</p>
</div>
<div className={styles.hrContainer}>
<hr className={styles.hr} />
</div>
<div className={styles.promoCodeContainer} onClick={handleCopyCode}>
<Title variant="h3" className={styles.promoCodeTitle}>
{translate("/special-offer.offer.promo_code")}
</Title>
<svg
width="25"
height="25"
viewBox="0 0 25 25"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M8.3335 4.16683V16.6668C8.3335 17.8175 9.26624 18.7502 10.4168 18.7502H18.7502C19.9008 18.7502 20.8335 17.8175 20.8335 16.6668V7.54352C20.8335 6.98287 20.6076 6.44588 20.2066 6.05392L16.7532 2.67724C16.3639 2.29662 15.8412 2.0835 15.2967 2.0835H10.4168C9.26624 2.0835 8.3335 3.01624 8.3335 4.16683Z"
stroke="#FCD34D"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M16.6665 18.7498V20.8332C16.6665 21.9838 15.7338 22.9165 14.5832 22.9165H6.24984C5.09924 22.9165 4.1665 21.9838 4.1665 20.8332V9.37484C4.1665 8.22424 5.09924 7.2915 6.24984 7.2915H8.33317"
stroke="#FCD34D"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</div>
<p className={styles.promoCodeDescription}>
{translate("/special-offer.offer.description", {
button: <b>{translate("/special-offer.button")}</b>,
})}
</p>
<div className={styles.blob1} />
<div className={styles.blob2} />
</div>
);
}
export default SpecialOfferBanner;

View File

@ -0,0 +1,138 @@
.container {
background: linear-gradient(90deg, #1f2937 0%, #374151 100%);
border-radius: 24px;
padding: 34px 32px 12px;
box-shadow: 0px 20px 40px 0px #0000004d, 0px 8px 16px 0px #00000033;
display: flex;
flex-direction: column;
align-items: center;
margin-top: 44px;
position: relative;
overflow: hidden;
& * {
z-index: 1;
}
}
.title {
font-size: 20px;
line-height: 28px;
font-weight: 700;
margin: 0;
color: #ffffff;
}
.offer {
background: linear-gradient(90deg, #fcd34d 0%, #f59e0b 100%);
border-radius: 16px;
padding: 16px 19px;
margin-top: 14px;
& > .offerTitle {
font-size: 36px;
font-weight: 900;
line-height: 1;
margin: 0;
color: #1f2937;
}
& > .offerDescription {
font-size: 17px;
font-weight: 600;
line-height: 1;
margin: 0;
color: #1f2937;
text-align: center;
margin-top: 8px;
}
}
.hrContainer {
width: calc(100% + 32px * 2);
padding: 10px 32px;
margin-top: 34px;
position: relative;
overflow: hidden;
height: 20px;
& > .hr {
border: none;
border-top: 1px solid #4b5563;
margin: 0;
}
::before {
content: "";
position: absolute;
top: 50%;
left: 0;
width: 20px;
height: 20px;
background-color: #f8fafc;
border-radius: 50%;
transform: translate(-50%, -50%);
}
::after {
content: "";
position: absolute;
top: 50%;
right: 0;
width: 20px;
height: 20px;
background-color: #f8fafc;
border-radius: 50%;
transform: translate(50%, -50%);
}
}
.promoCodeContainer {
width: 100%;
display: flex;
align-items: center;
justify-content: center;
gap: 16px;
margin-top: 8px;
cursor: pointer;
& > .promoCodeTitle {
font-size: 18px;
font-weight: 600;
line-height: 1;
letter-spacing: 0.9px;
margin: 0;
color: #fcd34d;
margin-left: 41px;
}
}
.promoCodeDescription {
font-size: 14px;
text-align: center;
max-width: 249px;
margin-top: 8px;
color: #9ca3af;
}
.blob1 {
position: absolute;
right: -16px;
top: -16px;
width: 64px;
height: 64px;
border-radius: 50%;
background: #5c594a;
z-index: 0;
}
.blob2 {
position: absolute;
left: -24px;
bottom: -26px;
width: 80px;
height: 80px;
border-radius: 50%;
background: #24344c;
z-index: 0;
}

View File

@ -0,0 +1,30 @@
import styles from "./styles.module.scss";
import { useNavigate } from "react-router-dom";
function WheelPathHeader() {
const navigate = useNavigate();
const handleBack = () => {
navigate(-1);
};
return (
<div className={styles.header}>
<svg
width="10"
height="18"
viewBox="0 0 10 18"
fill="none"
xmlns="http://www.w3.org/2000/svg"
onClick={handleBack}
>
<path
d="M0.366139 8.09227C-0.122046 8.59433 -0.122046 9.40968 0.366139 9.91175L7.86468 17.6235C8.35286 18.1255 9.14568 18.1255 9.63386 17.6235C10.122 17.1214 10.122 16.306 9.63386 15.804L3.01797 9L9.62996 2.19603C10.1181 1.69396 10.1181 0.878612 9.62996 0.376548C9.14177 -0.125516 8.34896 -0.125516 7.86077 0.376548L0.362234 8.08825L0.366139 8.09227Z"
fill="black"
/>
</svg>
</div>
);
}
export default WheelPathHeader;

View File

@ -0,0 +1,21 @@
.header {
display: flex;
flex-direction: row;
align-items: center;
justify-content: flex-start;
width: 100%;
& > svg {
cursor: pointer;
}
}
:global(body.dark-theme) {
.header {
& > svg {
path {
fill: #fff;
}
}
}
}

View File

@ -13,11 +13,13 @@ import Loader, { LoaderColor } from "@/components/Loader";
import Policy from "@/components/Policy";
import { useTranslations } from "@/hooks/translations";
import { ELocalesPlacement } from "@/locales";
import {
useMetricABFlags,
} from "@/services/metric/metricService";
import { useMetricABFlags } from "@/services/metric/metricService";
import NameInput from "@/components/pages/ABDesign/v1/pages/EmailEnterPage/NameInput";
import { useSession } from "@/hooks/session/useSession";
import { EUnleashFlags, useUnleash } from "@/hooks/ab/unleash/useUnleash";
import { usePreloadImages } from "@/hooks/preload/images";
import { images } from "../../data";
import { ELottieKeys, useLottie } from "@/hooks/lottie/useLottie";
function Email() {
const { updateSession } = useSession();
@ -34,6 +36,15 @@ function Email() {
const [isAuth, setIsAuth] = useState(false);
const authCode = useSelector(selectors.selectAuthCode);
const { isReady, variant: v2CompatibilityTrialChoicePath } = useUnleash({
flag: EUnleashFlags.v2CompatibilityTrialChoicePath,
});
usePreloadImages([images("wheel-of-fortune/wheel.png")]);
useLottie({
preloadKey: ELottieKeys.confetti,
});
const { flags } = useMetricABFlags();
const auraVideoTrial = flags?.auraVideoTrial?.[0];
@ -92,14 +103,20 @@ function Email() {
};
const handleNext = useCallback(() => {
if (!!authCode?.length) {
if (authCode?.length) {
return navigate(routes.client.compatibilityV2TryApp());
}
if (auraVideoTrial === "on") {
return navigate(routes.client.compatibilityV2TrialChoiceVideo());
}
if (["v1", "v3"].includes(v2CompatibilityTrialChoicePath)) {
return navigate(routes.client.compatibilityV2WheelOfFortune());
}
if (v2CompatibilityTrialChoicePath === "v2") {
return navigate(routes.client.compatibilityV2SpecialOffer());
}
return navigate(routes.client.compatibilityV2TrialChoice());
}, [auraVideoTrial, authCode, navigate]);
}, [auraVideoTrial, authCode, navigate, v2CompatibilityTrialChoicePath]);
useEffect(() => {
if (user && token?.length && !isLoading && !error) {
@ -114,6 +131,10 @@ function Email() {
}
}, [dispatch, error, handleNext, isLoading, token?.length, user]);
if (!isReady) {
return <Loader className={styles.loader} />;
}
return (
<>
<Title variant="h2" className={styles.title}>
@ -186,7 +207,11 @@ function Email() {
</Policy>
{!!error?.length && (
<Title variant="h3" style={{ color: "red", margin: 0 }}>
{translate("went_wrong", undefined, ELocalesPlacement.CompatibilityV2)}
{translate(
"went_wrong",
undefined,
ELocalesPlacement.CompatibilityV2
)}
</Title>
)}
</>

View File

@ -11,9 +11,6 @@ interface IReviewV3Props {
function ReviewV3({ handleNext }: IReviewV3Props) {
const { translate } = useTranslations(ELocalesPlacement.CompatibilityV2);
const quote =
"“A tool that becomes your ally on the path to a harmonious relationship.”";
return (
<div className={styles.container}>
<img
@ -21,13 +18,16 @@ function ReviewV3({ handleNext }: IReviewV3Props) {
src={images("Cosmopolitan-Logo.png")}
alt="COSMOPOLITAN"
/>
<p className={styles.quote}>{quote}</p>
<p className={styles.quote}>{translate("/review.v3.quote")}</p>
<img
className={styles.partnersV3}
src={images("review/partners.png")}
alt="partners"
/>
<Button className={`${styles.buttonV2} ${styles.buttonV3}`} onClick={handleNext}>
<Button
className={`${styles.buttonV2} ${styles.buttonV3}`}
onClick={handleNext}
>
{translate("/review.button")}
</Button>
</div>

View File

@ -9,24 +9,27 @@ import layoutCss from "@/routerComponents/Compatibility/v2/Layout/styles.module.
import stepperCss from "@/routerComponents/Compatibility/v2/StepperLayout/styles.module.scss";
import { selectors } from "@/store";
import { answerTimeOut } from "@/components/CompatibilityV2/data";
import { useTranslations } from "@/hooks/translations";
import { ELocalesPlacement } from "@/locales";
interface IReviewV4Props {
handleNext: () => void;
}
const OPTIONS: { id: string; title: string }[] = [
{ id: "nyt", title: "The New York Times" },
{ id: "forbes", title: "Forbes" },
{ id: "cosmopolitan", title: "Cosmopolitan" },
{ id: "oprah", title: "Oprah Daily" },
{ id: "social", title: "Social media" },
{ id: "other", title: "Other sources" },
];
function ReviewV4({ handleNext }: IReviewV4Props) {
const { translate } = useTranslations(ELocalesPlacement.CompatibilityV2);
const darkTheme = useSelector(selectors.selectDarkTheme);
const [selected, setSelected] = useState<string | null>(null);
const OPTIONS: { id: string; title: string }[] = [
{ id: "nyt", title: translate("/review.v4.answers.nyt") },
{ id: "forbes", title: translate("/review.v4.answers.forbes") },
{ id: "cosmopolitan", title: translate("/review.v4.answers.cosmopolitan") },
{ id: "oprah", title: translate("/review.v4.answers.oprah") },
{ id: "social", title: translate("/review.v4.answers.social") },
{ id: "other", title: translate("/review.v4.answers.other") },
];
const handleSelect = (id: string) => {
setSelected(id);
setTimeout(() => {
@ -49,11 +52,10 @@ function ReviewV4({ handleNext }: IReviewV4Props) {
<section className={layoutCss.page}>
<div className={styles.container}>
<Title variant="h2" className={styles.title}>
Where did you first hear about us?
{translate("/review.v4.title")}
</Title>
<p className={styles.description}>
Many people say they first learned about us through leading publications
and platforms.
{translate("/review.v4.description")}
</p>
{OPTIONS.map((option) => (
<Answer

View File

@ -0,0 +1,59 @@
import Title from "@/components/Title";
import styles from "./styles.module.scss";
import { useTranslations } from "@/hooks/translations";
import { ELocalesPlacement } from "@/locales";
import WheelPathHeader from "../../components/WheelPathHeader";
import Button from "../../components/Button";
import SpecialOfferBanner from "../../components/SpecialOfferBanner";
import { copyToClipboard } from "@/services/data";
import { useEffect } from "react";
import { useDispatch } from "react-redux";
import { actions } from "@/store";
import { useFunnel } from "@/hooks/funnel/useFunnel";
import routes from "@/routes";
import { useNavigate } from "react-router-dom";
function SpecialOffer() {
const { translate } = useTranslations(ELocalesPlacement.CompatibilityV2);
const dispatch = useDispatch();
const navigate = useNavigate();
const { products } = useFunnel({
funnel: ELocalesPlacement.CompatibilityV2,
paymentPlacement: "main",
});
const handleNext = () => {
copyToClipboard("HAIR50");
navigate(routes.client.compatibilityV2TrialPayment());
};
useEffect(() => {
dispatch(
actions.payment.update({
activeProduct: products[0],
})
);
}, [dispatch, products]);
return (
<div className={styles.container}>
<WheelPathHeader />
<Title variant="h1" className={styles.title}>
{translate("/special-offer.title")}
</Title>
<p className={styles.description}>
{translate("/special-offer.description")}
</p>
<SpecialOfferBanner handleCopyCode={handleNext} />
<Button className={styles.button} onClick={handleNext}>
{translate("/special-offer.button")}
</Button>
</div>
);
}
export default SpecialOffer;

View File

@ -0,0 +1,32 @@
.container {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
padding: 36px 24px 18px;
max-width: 560px;
margin: 0 auto;
min-height: 100dvh;
}
.title {
font-size: 25px;
line-height: 38px;
font-weight: 700;
margin: 0;
margin-top: 29px;
}
.description {
font-size: 17px;
line-height: 26px;
margin-top: 16px;
font-weight: 500;
text-align: center;
max-width: 318px;
}
.button {
position: sticky;
margin-top: auto;
}

View File

@ -0,0 +1,362 @@
import { useState, useEffect, useRef } from "react";
import Title from "@/components/Title";
import styles from "./styles.module.scss";
import { useTranslations } from "@/hooks/translations";
import { ELocalesPlacement } from "@/locales";
import Button from "../../components/Button";
import { images } from "../../data";
import WheelPathHeader from "../../components/WheelPathHeader";
import routes from "@/routes";
import { useNavigate } from "react-router-dom";
import { DotLottieReact } from "@lottiefiles/dotlottie-react";
import { useLottie } from "@/hooks/lottie/useLottie";
import { ELottieKeys } from "@/hooks/lottie/useLottie";
import { EUnleashFlags, useUnleash } from "@/hooks/ab/unleash/useUnleash";
import Loader, { LoaderColor } from "@/components/Loader";
type WheelState =
| "spinning"
| "stopping"
| "stopped"
| "accelerating"
| "ready-for-second-spin";
function WheelOfFortune() {
const { translate } = useTranslations(ELocalesPlacement.CompatibilityV2);
const navigate = useNavigate();
const { animationData: animationDataConfetti } = useLottie({
loadKey: ELottieKeys.confetti,
});
const [wheelState, setWheelState] = useState<WheelState>("spinning");
const wheelStateRef = useRef<WheelState>("spinning");
const [showSkipButton, setShowSkipButton] = useState(true);
const [showMainButton, setShowMainButton] = useState(true);
const [isConfettiVisible, setIsConfettiVisible] = useState(false);
const { isReady, variant: v2CompatibilityTrialChoicePath } = useUnleash({
flag: EUnleashFlags.v2CompatibilityTrialChoicePath,
});
const updateWheelState = (newState: WheelState) => {
wheelStateRef.current = newState;
setWheelState(newState);
};
const [currentRotation, setCurrentRotation] = useState(0);
const [isButtonDisabled, setIsButtonDisabled] = useState(false);
const wheelRef = useRef<HTMLImageElement>(null);
const animationRef = useRef<number>();
const WHEEL_SPEED = 760;
const ACCELERATION_DURATION = 2000;
const STOP_DURATION = 3000;
const FIRST_STOP_ANGLE = 240;
const SECOND_STOP_ANGLE = 274;
useEffect(() => {
if (wheelState === "spinning" && currentRotation === 0) {
startSpinning();
}
return () => {
if (animationRef.current) {
cancelAnimationFrame(animationRef.current);
}
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const startSpinning = () => {
setIsButtonDisabled(false);
accelerateWheel();
};
const accelerateWheel = () => {
let startTime: number;
let lastTime: number;
const animate = (timestamp: number) => {
if (!startTime) {
startTime = timestamp;
lastTime = timestamp;
}
const elapsed = timestamp - startTime;
const deltaTime = timestamp - lastTime;
lastTime = timestamp;
const progress = Math.min(elapsed / ACCELERATION_DURATION, 1);
const easedProgress = 1 - Math.pow(1 - progress, 3);
const currentSpeed = WHEEL_SPEED * easedProgress;
const rotationIncrement = (currentSpeed * deltaTime) / 1000;
setCurrentRotation((prev) => prev + rotationIncrement);
if (progress < 1) {
animationRef.current = requestAnimationFrame(animate);
} else {
startContinuousSpinning();
}
};
animationRef.current = requestAnimationFrame(animate);
};
const startContinuousSpinning = () => {
let lastTime: number;
const animate = (timestamp: number) => {
if (!lastTime) lastTime = timestamp;
const deltaTime = timestamp - lastTime;
lastTime = timestamp;
const rotationIncrement = (WHEEL_SPEED * deltaTime) / 1000;
setCurrentRotation((prev) => prev + rotationIncrement);
if (
wheelStateRef.current === "spinning" ||
wheelStateRef.current === "ready-for-second-spin"
) {
animationRef.current = requestAnimationFrame(animate);
}
};
animationRef.current = requestAnimationFrame(animate);
};
const stopWheel = () => {
if (isButtonDisabled || wheelState === "stopping") return;
setIsButtonDisabled(true);
updateWheelState("stopping");
if (animationRef.current) {
cancelAnimationFrame(animationRef.current);
}
const originalState = wheelState;
const targetAngle =
originalState === "spinning" ? FIRST_STOP_ANGLE : SECOND_STOP_ANGLE;
if (originalState === "ready-for-second-spin") {
setShowMainButton(false);
}
const currentAngle = currentRotation % 360;
const additionalRotations =
Math.ceil((currentAngle + 360 - targetAngle) / 360) * 360;
const finalRotation =
currentRotation + additionalRotations - (currentAngle - targetAngle);
let startTime: number;
const startRotation = currentRotation;
const animate = (timestamp: number) => {
if (!startTime) startTime = timestamp;
const elapsed = timestamp - startTime;
const progress = Math.min(elapsed / STOP_DURATION, 1);
const easedProgress = 1 - Math.pow(1 - progress, 4);
const currentRotation =
startRotation + (finalRotation - startRotation) * easedProgress;
setCurrentRotation(currentRotation);
if (progress < 1) {
animationRef.current = requestAnimationFrame(animate);
} else {
updateWheelState("stopped");
setIsButtonDisabled(false);
if (originalState === "ready-for-second-spin") {
setIsConfettiVisible(true);
setTimeout(() => {
handleNext();
}, 5000);
}
}
};
animationRef.current = requestAnimationFrame(animate);
};
const handleButtonClick = () => {
if (wheelState === "spinning" || wheelState === "ready-for-second-spin") {
stopWheel();
} else if (wheelState === "stopped") {
updateWheelState("ready-for-second-spin");
setShowSkipButton(false);
setIsButtonDisabled(false);
accelerateWheel();
}
};
const getButtonText = () => {
switch (wheelState) {
case "spinning":
case "ready-for-second-spin":
return translate("/wheel-of-fortune.button_stop");
case "stopped":
return translate("/wheel-of-fortune.button_last_chance");
case "accelerating":
return translate("/wheel-of-fortune.button_stop");
default:
return translate("/wheel-of-fortune.button_stop");
}
};
const getWheelClassName = () => {
const baseClass = styles.wheelImage;
if (
wheelState === "spinning" ||
wheelState === "accelerating" ||
wheelState === "ready-for-second-spin"
) {
return `${baseClass} ${styles.spinning} ${v2CompatibilityTrialChoicePath !== "v3" && styles.blurSpinning}`;
}
if (wheelState === "stopping") {
return `${baseClass} ${styles.stopping} ${v2CompatibilityTrialChoicePath !== "v3" && styles.blurStopping}`;
}
return baseClass;
};
const handleNext = () => {
navigate(routes.client.compatibilityV2SpecialOffer());
};
if (!isReady) {
return <Loader color={LoaderColor.Black} />;
}
return (
<div className={styles.container}>
<WheelPathHeader />
<Title variant="h1" className={styles.title}>
{translate("/wheel-of-fortune.title")}
</Title>
<p className={styles.description}>
{translate("/wheel-of-fortune.description")}
</p>
<div className={styles.wheelContainer}>
<img
ref={wheelRef}
className={getWheelClassName()}
src={images("wheel-of-fortune/wheel.png")}
alt="Wheel of Fortune"
style={{
transform: `rotate(${currentRotation}deg)`,
}}
/>
<svg
width="30"
height="30"
viewBox="0 0 30 30"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className={styles.giftImage}
>
<path
d="M11.1621 4.03125L13.2012 7.5H13.125H8.90625C7.61133 7.5 6.5625 6.45117 6.5625 5.15625C6.5625 3.86133 7.61133 2.8125 8.90625 2.8125H9.03516C9.9082 2.8125 10.7227 3.27539 11.1621 4.03125ZM3.75 5.15625C3.75 6 3.95508 6.79688 4.3125 7.5H1.875C0.837891 7.5 0 8.33789 0 9.375V13.125C0 14.1621 0.837891 15 1.875 15H28.125C29.1621 15 30 14.1621 30 13.125V9.375C30 8.33789 29.1621 7.5 28.125 7.5H25.6875C26.0449 6.79688 26.25 6 26.25 5.15625C26.25 2.30859 23.9414 0 21.0938 0H20.9648C19.0957 0 17.3613 0.990234 16.4121 2.60156L15 5.00977L13.5879 2.60742C12.6387 0.990234 10.9043 0 9.03516 0H8.90625C6.05859 0 3.75 2.30859 3.75 5.15625ZM23.4375 5.15625C23.4375 6.45117 22.3887 7.5 21.0938 7.5H16.875H16.7988L18.8379 4.03125C19.2832 3.27539 20.0918 2.8125 20.9648 2.8125H21.0938C22.3887 2.8125 23.4375 3.86133 23.4375 5.15625ZM1.875 16.875V27.1875C1.875 28.7402 3.13477 30 4.6875 30H13.125V16.875H1.875ZM16.875 30H25.3125C26.8652 30 28.125 28.7402 28.125 27.1875V16.875H16.875V30Z"
fill="#FF8004"
/>
</svg>
<svg
width="81"
height="76"
viewBox="0 0 81 76"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className={styles.cursor}
>
<g filter="url(#filter0_d_698_4287)">
<path
d="M43.0981 57.25C41.9434 59.25 39.0566 59.25 37.9019 57.25L17.5503 22C16.3956 20 17.839 17.5 20.1484 17.5L60.8516 17.5C63.161 17.5 64.6044 20 63.4497 22L43.0981 57.25Z"
fill="#FF0D11"
/>
<path
d="M43.9639 57.75C42.4242 60.4165 38.5758 60.4165 37.0361 57.75L16.6846 22.5C15.145 19.8333 17.0692 16.5 20.1484 16.5L60.8516 16.5C63.9308 16.5 65.855 19.8333 64.3154 22.5L43.9639 57.75Z"
stroke="white"
strokeWidth="2"
/>
</g>
<defs>
<filter
id="filter0_d_698_4287"
x="0.141113"
y="0.5"
width="80.7178"
height="75.25"
filterUnits="userSpaceOnUse"
colorInterpolationFilters="sRGB"
>
<feFlood floodOpacity="0" result="BackgroundImageFix" />
<feColorMatrix
in="SourceAlpha"
type="matrix"
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
result="hardAlpha"
/>
<feOffset />
<feGaussianBlur stdDeviation="7.5" />
<feComposite in2="hardAlpha" operator="out" />
<feColorMatrix
type="matrix"
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.4 0"
/>
<feBlend
mode="normal"
in2="BackgroundImageFix"
result="effect1_dropShadow_698_4287"
/>
<feBlend
mode="normal"
in="SourceGraphic"
in2="effect1_dropShadow_698_4287"
result="shape"
/>
</filter>
</defs>
</svg>
</div>
{showMainButton && (
<Button
className={styles.button}
onClick={handleButtonClick}
disabled={isButtonDisabled}
>
{getButtonText()}
</Button>
)}
{showSkipButton && (
<Button className={styles.skipButton} onClick={handleNext}>
{translate("/wheel-of-fortune.skip_button")}
</Button>
)}
{isConfettiVisible && (
<div className={styles["lottie-animation-container-confetti"]}>
{animationDataConfetti && (
<DotLottieReact
className={`${styles["lottie-animation-confetti"]} ym-hide-content`}
data={animationDataConfetti}
autoplay
width={80}
/>
)}
</div>
)}
</div>
);
}
export default WheelOfFortune;

View File

@ -0,0 +1,132 @@
.container {
display: flex;
flex-direction: column;
align-items: center;
padding: 36px 24px 80px;
max-width: 560px;
margin: 0 auto;
overflow-x: hidden;
}
.title {
font-size: 25px;
line-height: 38px;
margin-top: 29px;
margin-bottom: 0;
}
.description {
font-size: 17px;
line-height: 26px;
margin-top: 16px;
font-weight: 500;
}
.button {
margin-top: 36px;
border-radius: 21px;
max-width: 272px;
min-height: 0px;
padding: 25px;
line-height: 1;
&:disabled {
opacity: 0.6;
cursor: not-allowed;
}
}
.skipButton.skipButton {
background: none;
min-height: 0;
min-width: 0;
border: none;
font-size: 16px;
font-weight: 500;
text-decoration: underline;
color: #64748b;
width: fit-content;
padding: 0;
box-shadow: none;
margin-top: 36px;
}
.wheelContainer {
width: calc(100% + 48px);
max-width: 350px;
margin-top: 38px;
position: relative;
// overflow: hidden;
padding: 24px;
& > .wheelImage {
width: 100%;
height: 100%;
object-fit: contain;
object-position: center;
user-select: none;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
-webkit-user-drag: none;
-webkit-tap-highlight-color: transparent;
-webkit-touch-callout: none;
transform-origin: center;
transition: filter 0.5s ease-in-out;
// &.spinning {
// filter: blur(6px);
// }
// &.stopping {
// filter: blur(3px);
// }
&.blurSpinning {
filter: blur(6px);
}
&.blurStopping {
filter: blur(3px);
}
}
& > .giftImage {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
& > .cursor {
position: absolute;
top: -15px;
left: 50%;
transform: translateX(-50%);
}
}
.lottie-animation-container-confetti {
position: fixed;
bottom: 0;
left: 0;
width: 100dvw;
// aspect-ratio: 1 / 1;
// min-height: 100px;
height: 100dvh;
z-index: 9999;
pointer-events: none;
}
.lottie-animation-confetti {
// aspect-ratio: 1 / 1;
width: 100dvw;
height: 100dvh;
}
:global(body.dark-theme) {
.skipButton {
color: #889ebd;
}
}

View File

@ -38,6 +38,7 @@ export enum EUnleashFlags {
"v2CompatibilityRelationshipStatusPagePlacement" = "v2-compatibility-relationship-status-page-placement",
"v2CompatibilityReviewPage" = "v2-compatibility-review-page",
"v2CompatibilityPathToEnteringBirthdate" = "v2-compatibility-path-to-entering-birthdate",
"v2CompatibilityTrialChoicePath" = "v2-compatibility-trial-choice-path",
}
interface IUseUnleashProps<T extends EUnleashFlags> {
@ -80,6 +81,7 @@ interface IVariants {
[EUnleashFlags.v2CompatibilityRelationshipStatusPagePlacement]: 'v0' | 'v1' | 'v2';
[EUnleashFlags.v2CompatibilityReviewPage]: 'v0' | 'v1' | 'v2' | 'v3' | 'v4';
[EUnleashFlags.v2CompatibilityPathToEnteringBirthdate]: 'hide' | 'show';
[EUnleashFlags.v2CompatibilityTrialChoicePath]: 'v0' | 'v1' | 'v2' | 'v3';
}
/**

View File

@ -60,6 +60,8 @@ import ImportantStep from "@/components/CompatibilityV2/pages/ImportantStep";
import WhoMatter from "@/components/CompatibilityV2/pages/WhoMatter";
import YourPriority from "@/components/CompatibilityV2/pages/YourPriority";
import PersonalizedRelationshipAnalysis from "@/components/CompatibilityV2/pages/PersonalizedRelationshipAnalysis";
import WheelOfFortune from "@/components/CompatibilityV2/pages/WheelOfFortune";
import SpecialOffer from "@/components/CompatibilityV2/pages/SpecialOffer";
const removePrefix = (path: string) => path.replace(compatibilityV2Prefix, "");
@ -240,6 +242,14 @@ function CompatibilityV2Routes() {
path={removePrefix(routes.client.compatibilityV2Review())}
element={<Review />}
/>
<Route
path={removePrefix(routes.client.compatibilityV2WheelOfFortune())}
element={<WheelOfFortune />}
/>
<Route
path={removePrefix(routes.client.compatibilityV2SpecialOffer())}
element={<SpecialOffer />}
/>
<Route element={<Layout />}>
<Route
element={

View File

@ -264,6 +264,8 @@ const routes = {
compatibilityV2SecretDiscount: () => [compatibilityV2Prefix, "secret-discount"].join("/"),
compatibilityV2Onboarding: () => [compatibilityV2Prefix, "onboarding"].join("/"),
compatibilityV2Review: () => [compatibilityV2Prefix, "review"].join("/"),
compatibilityV2WheelOfFortune: () => [compatibilityV2Prefix, "wheel-of-fortune"].join("/"),
compatibilityV2SpecialOffer: () => [compatibilityV2Prefix, "special-offer"].join("/"),
// CompatibilityV3
compatibilityV3Welcome: () => [compatibilityV3Prefix, "welcome"].join("/"),
compatibilityV3Gender: () => [compatibilityV3Prefix, "gender"].join("/"),