From 0738f989c62a2be9d1dc7651402b84bac43e01dc Mon Sep 17 00:00:00 2001 From: Daniil Chemerkin Date: Wed, 10 Sep 2025 21:24:25 +0000 Subject: [PATCH] AW-483-484-485-fix-bugs --- .../locales/compatibility-v2/en/male_en.json | 36 +- .../compatibility/wheel-of-fortune/wheel.png | Bin 0 -> 175780 bytes .../components/SpecialOfferBanner/index.tsx | 86 +++++ .../SpecialOfferBanner/styles.module.scss | 138 +++++++ .../components/WheelPathHeader/index.tsx | 30 ++ .../WheelPathHeader/styles.module.scss | 21 + .../CompatibilityV2/pages/Email/index.tsx | 37 +- .../CompatibilityV2/pages/Review/v3/index.tsx | 10 +- .../CompatibilityV2/pages/Review/v4/index.tsx | 26 +- .../pages/SpecialOffer/index.tsx | 59 +++ .../pages/SpecialOffer/styles.module.scss | 32 ++ .../pages/WheelOfFortune/index.tsx | 362 ++++++++++++++++++ .../pages/WheelOfFortune/styles.module.scss | 132 +++++++ src/hooks/ab/unleash/useUnleash.ts | 2 + .../Compatibility/v2/index.tsx | 10 + src/routes.ts | 2 + 16 files changed, 959 insertions(+), 24 deletions(-) create mode 100644 public/v2/compatibility/wheel-of-fortune/wheel.png create mode 100644 src/components/CompatibilityV2/components/SpecialOfferBanner/index.tsx create mode 100644 src/components/CompatibilityV2/components/SpecialOfferBanner/styles.module.scss create mode 100644 src/components/CompatibilityV2/components/WheelPathHeader/index.tsx create mode 100644 src/components/CompatibilityV2/components/WheelPathHeader/styles.module.scss create mode 100644 src/components/CompatibilityV2/pages/SpecialOffer/index.tsx create mode 100644 src/components/CompatibilityV2/pages/SpecialOffer/styles.module.scss create mode 100644 src/components/CompatibilityV2/pages/WheelOfFortune/index.tsx create mode 100644 src/components/CompatibilityV2/pages/WheelOfFortune/styles.module.scss diff --git a/public/locales/compatibility-v2/en/male_en.json b/public/locales/compatibility-v2/en/male_en.json index 5331639..a6a9c53 100644 --- a/public/locales/compatibility-v2/en/male_en.json +++ b/public/locales/compatibility-v2/en/male_en.json @@ -462,7 +462,7 @@ "personalized_offer": "Personalized offer reserved", "title": "Start your trial", "total_today": "Total today", - "code_applied_bold": "WITLAB24", + "code_applied_bold": "HAIR50", "code_applied": "Code 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": "Скопируйте или нажмите diff --git a/src/components/CompatibilityV2/pages/Review/v4/index.tsx b/src/components/CompatibilityV2/pages/Review/v4/index.tsx index 9fa615a..9e81011 100644 --- a/src/components/CompatibilityV2/pages/Review/v4/index.tsx +++ b/src/components/CompatibilityV2/pages/Review/v4/index.tsx @@ -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(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) {
- Where did you first hear about us? + {translate("/review.v4.title")}

- Many people say they first learned about us through leading publications - and platforms. + {translate("/review.v4.description")}

{OPTIONS.map((option) => ( { + copyToClipboard("HAIR50"); + navigate(routes.client.compatibilityV2TrialPayment()); + }; + + useEffect(() => { + dispatch( + actions.payment.update({ + activeProduct: products[0], + }) + ); + }, [dispatch, products]); + + return ( +
+ + + {translate("/special-offer.title")} + + +

+ {translate("/special-offer.description")} +

+ + + + +
+ ); +} + +export default SpecialOffer; diff --git a/src/components/CompatibilityV2/pages/SpecialOffer/styles.module.scss b/src/components/CompatibilityV2/pages/SpecialOffer/styles.module.scss new file mode 100644 index 0000000..55b6071 --- /dev/null +++ b/src/components/CompatibilityV2/pages/SpecialOffer/styles.module.scss @@ -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; +} diff --git a/src/components/CompatibilityV2/pages/WheelOfFortune/index.tsx b/src/components/CompatibilityV2/pages/WheelOfFortune/index.tsx new file mode 100644 index 0000000..97a14e0 --- /dev/null +++ b/src/components/CompatibilityV2/pages/WheelOfFortune/index.tsx @@ -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("spinning"); + const wheelStateRef = useRef("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(null); + const animationRef = useRef(); + + 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 ; + } + + return ( +
+ + + + {translate("/wheel-of-fortune.title")} + + +

+ {translate("/wheel-of-fortune.description")} +

+ +
+ Wheel of Fortune + + + + + + + + + + + + + + + + + + + + + +
+ + {showMainButton && ( + + )} + + {showSkipButton && ( + + )} + + {isConfettiVisible && ( +
+ {animationDataConfetti && ( + + )} +
+ )} +
+ ); +} + +export default WheelOfFortune; diff --git a/src/components/CompatibilityV2/pages/WheelOfFortune/styles.module.scss b/src/components/CompatibilityV2/pages/WheelOfFortune/styles.module.scss new file mode 100644 index 0000000..ac06697 --- /dev/null +++ b/src/components/CompatibilityV2/pages/WheelOfFortune/styles.module.scss @@ -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; + } +} diff --git a/src/hooks/ab/unleash/useUnleash.ts b/src/hooks/ab/unleash/useUnleash.ts index 94f5f6d..32fb8ed 100644 --- a/src/hooks/ab/unleash/useUnleash.ts +++ b/src/hooks/ab/unleash/useUnleash.ts @@ -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 { @@ -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'; } /** diff --git a/src/routerComponents/Compatibility/v2/index.tsx b/src/routerComponents/Compatibility/v2/index.tsx index 52423bf..5bc1c65 100644 --- a/src/routerComponents/Compatibility/v2/index.tsx +++ b/src/routerComponents/Compatibility/v2/index.tsx @@ -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={} /> + } + /> + } + /> }> [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("/"),