From 6dd37bb28490008c161db5fe0c6ef110c9b367eb Mon Sep 17 00:00:00 2001 From: Daniil Chemerkin Date: Tue, 6 May 2025 22:13:47 +0000 Subject: [PATCH] develop --- index.html | 2 +- package-lock.json | 41 ++ package.json | 2 + .../locales/compatibility-v4/en/male_en.json | 24 +- public/locales/profile/en/male_en.json | 52 ++ public/locales/v1/en/male_en.json | 20 +- src/api/resources/Payment.ts | 13 +- src/components/App/index.tsx | 445 +++++++++--------- .../components/CameraModal/index.tsx | 288 ++++++++++-- .../components/CameraModal/styles.module.scss | 6 + .../images/SVG/ScanInstruction/index.tsx | 6 +- .../pages/Camera/android/index.tsx | 36 +- .../pages/Camera/iphone/index.tsx | 34 +- .../CompatibilityV2/pages/Gender/index.tsx | 16 +- .../pages/PalmsInformation/index.tsx | 7 + .../pages/RelationshipStatus/index.tsx | 14 + .../CompatibilityV2/pages/TryApp/index.tsx | 1 + .../pages/CodeInstruction/index.tsx | 1 + .../CompatibilityV3/pages/TryApp/index.tsx | 1 + .../components/Address/index.tsx | 17 +- .../components/Footer/index.tsx | 16 +- .../pages/CodeInstruction/index.tsx | 1 + .../pages/TrialPayment/index.tsx | 62 +-- .../TryApp/components/YourReading/index.tsx | 36 +- .../CompatibilityV4/pages/TryApp/index.tsx | 3 + .../pages/TryApp/styles.module.scss | 208 ++++++++ .../CompatibilityV4/pages/TryApp/v1/index.tsx | 187 ++++++++ .../pages/WhatAddToAnalysis/index.tsx | 3 + src/components/EmailEnterPage/EmailInput.tsx | 4 +- .../PalmistryV1/pages/TryApp/index.tsx | 1 + .../nmi/CheckoutForm/AddressFields/index.tsx | 60 +++ .../AddressFields/styles.module.scss | 82 ++++ .../Payment/nmi/CheckoutForm/index.tsx | 9 + .../Profile/components/Billing/index.tsx | 46 ++ .../components/Billing/styles.module.scss | 56 +++ .../Profile/components/LogOut/index.tsx | 22 + .../components/LogOut/styles.module.scss | 12 + .../Profile/components/ProfileBlock/index.tsx | 24 + .../ProfileBlock/styles.module.scss | 31 ++ .../components/ProfileInformation/index.tsx | 39 ++ .../ProfileInformation/styles.module.scss | 16 + .../Profile/components/Table/index.tsx | 23 + .../components/Table/styles.module.scss | 33 ++ .../Profile/pages/Profile/index.tsx | 100 ++++ .../Profile/pages/Profile/styles.module.scss | 76 +++ .../Profile/pages/Subscriptions/index.tsx | 71 +++ .../pages/Subscriptions/styles.module.scss | 74 +++ .../pages/EmailEnterPage/BirthplaceInput.tsx | 2 +- .../v1/pages/EmailEnterPage/EmailInput.tsx | 3 + .../v1/pages/EmailEnterPage/NameInput.tsx | 7 +- .../pages/ABDesign/v1/pages/TryApp/index.tsx | 1 + src/components/pages/TryApp/index.tsx | 1 + src/hooks/ab/unleash/useUnleash.ts | 7 +- src/hooks/payment/nmi/useAddressFields.ts | 155 ++++++ src/hooks/payment/nmi/usePayment.ts | 19 +- src/hooks/paywall/defaultPaywalls.ts | 6 +- src/initialization/index.tsx | 6 + src/locales/index.ts | 1 + .../Compatibility/v2/Layout/index.tsx | 16 + .../v2/LayoutPersonalVideo/index.tsx | 39 +- .../v3/LayoutPersonalVideo/index.tsx | 39 +- .../v4/LayoutPersonalVideo/index.tsx | 39 +- .../v1/LayoutPersonalVideo/index.tsx | 39 +- src/routerComponents/Profile/Layout/index.tsx | 43 ++ .../Profile/Layout/styles.module.scss | 33 ++ src/routerComponents/Profile/index.tsx | 31 ++ src/routes.ts | 5 + src/services/metric/metricService.ts | 1 + src/utils/handLandmarkerSingleton.ts | 59 +++ 69 files changed, 2465 insertions(+), 408 deletions(-) create mode 100644 public/locales/profile/en/male_en.json create mode 100644 src/components/CompatibilityV4/pages/TryApp/v1/index.tsx create mode 100644 src/components/Payment/nmi/CheckoutForm/AddressFields/index.tsx create mode 100644 src/components/Payment/nmi/CheckoutForm/AddressFields/styles.module.scss create mode 100644 src/components/Profile/components/Billing/index.tsx create mode 100644 src/components/Profile/components/Billing/styles.module.scss create mode 100644 src/components/Profile/components/LogOut/index.tsx create mode 100644 src/components/Profile/components/LogOut/styles.module.scss create mode 100644 src/components/Profile/components/ProfileBlock/index.tsx create mode 100644 src/components/Profile/components/ProfileBlock/styles.module.scss create mode 100644 src/components/Profile/components/ProfileInformation/index.tsx create mode 100644 src/components/Profile/components/ProfileInformation/styles.module.scss create mode 100644 src/components/Profile/components/Table/index.tsx create mode 100644 src/components/Profile/components/Table/styles.module.scss create mode 100644 src/components/Profile/pages/Profile/index.tsx create mode 100644 src/components/Profile/pages/Profile/styles.module.scss create mode 100644 src/components/Profile/pages/Subscriptions/index.tsx create mode 100644 src/components/Profile/pages/Subscriptions/styles.module.scss create mode 100644 src/hooks/payment/nmi/useAddressFields.ts create mode 100644 src/routerComponents/Profile/Layout/index.tsx create mode 100644 src/routerComponents/Profile/Layout/styles.module.scss create mode 100644 src/routerComponents/Profile/index.tsx create mode 100644 src/utils/handLandmarkerSingleton.ts diff --git a/index.html b/index.html index 295e4ff..ad877fa 100755 --- a/index.html +++ b/index.html @@ -48,7 +48,7 @@ - + diff --git a/package-lock.json b/package-lock.json index 495294f..37004b7 100755 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "@emotion/react": "^11.11.4", "@emotion/styled": "^11.11.5", "@lottiefiles/dotlottie-react": "^0.6.4", + "@mediapipe/tasks-vision": "^0.10.22-rc.20250304", "@microsoft/clarity": "^1.0.0", "@mui/material": "^5.15.21", "@reduxjs/toolkit": "^1.9.5", @@ -20,6 +21,7 @@ "core-js": "^3.37.1", "framer-motion": "^11.0.8", "html-react-parser": "^3.0.16", + "i18n-iso-countries": "^7.14.0", "i18next": "^22.5.0", "i18next-react-postprocessor": "^3.1.0", "idb": "^8.0.0", @@ -1183,6 +1185,11 @@ "node-pre-gyp": "bin/node-pre-gyp" } }, + "node_modules/@mediapipe/tasks-vision": { + "version": "0.10.22-rc.20250304", + "resolved": "https://registry.npmjs.org/@mediapipe/tasks-vision/-/tasks-vision-0.10.22-rc.20250304.tgz", + "integrity": "sha512-dElxVXMFGthshfIj+qAVm8KE2jmNo2p8oXFib8WzEjb7GNaX/ClWBc8UJfoSZwjEMVrdHJ4YUfa7P3ifl6MIWw==" + }, "node_modules/@microsoft/clarity": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@microsoft/clarity/-/clarity-1.0.0.tgz", @@ -2614,6 +2621,11 @@ "node": ">=8" } }, + "node_modules/diacritics": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/diacritics/-/diacritics-1.3.0.tgz", + "integrity": "sha512-wlwEkqcsaxvPJML+rDh/2iS824jbREk6DUMUKkEaSlxdYHeS43cClJtsWglvw2RfeXGm6ohKDqsXteJ5sP5enA==" + }, "node_modules/dir-glob": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", @@ -3540,6 +3552,17 @@ "node": ">= 6" } }, + "node_modules/i18n-iso-countries": { + "version": "7.14.0", + "resolved": "https://registry.npmjs.org/i18n-iso-countries/-/i18n-iso-countries-7.14.0.tgz", + "integrity": "sha512-nXHJZYtNrfsi1UQbyRqm3Gou431elgLjKl//CYlnBGt5aTWdRPH1PiS2T/p/n8Q8LnqYqzQJik3Q7mkwvLokeg==", + "dependencies": { + "diacritics": "1.3.0" + }, + "engines": { + "node": ">= 12" + } + }, "node_modules/i18next": { "version": "22.5.0", "resolved": "https://registry.npmjs.org/i18next/-/i18next-22.5.0.tgz", @@ -6290,6 +6313,11 @@ "tar": "^6.1.11" } }, + "@mediapipe/tasks-vision": { + "version": "0.10.22-rc.20250304", + "resolved": "https://registry.npmjs.org/@mediapipe/tasks-vision/-/tasks-vision-0.10.22-rc.20250304.tgz", + "integrity": "sha512-dElxVXMFGthshfIj+qAVm8KE2jmNo2p8oXFib8WzEjb7GNaX/ClWBc8UJfoSZwjEMVrdHJ4YUfa7P3ifl6MIWw==" + }, "@microsoft/clarity": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@microsoft/clarity/-/clarity-1.0.0.tgz", @@ -7169,6 +7197,11 @@ "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==", "optional": true }, + "diacritics": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/diacritics/-/diacritics-1.3.0.tgz", + "integrity": "sha512-wlwEkqcsaxvPJML+rDh/2iS824jbREk6DUMUKkEaSlxdYHeS43cClJtsWglvw2RfeXGm6ohKDqsXteJ5sP5enA==" + }, "dir-glob": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", @@ -7876,6 +7909,14 @@ "debug": "4" } }, + "i18n-iso-countries": { + "version": "7.14.0", + "resolved": "https://registry.npmjs.org/i18n-iso-countries/-/i18n-iso-countries-7.14.0.tgz", + "integrity": "sha512-nXHJZYtNrfsi1UQbyRqm3Gou431elgLjKl//CYlnBGt5aTWdRPH1PiS2T/p/n8Q8LnqYqzQJik3Q7mkwvLokeg==", + "requires": { + "diacritics": "1.3.0" + } + }, "i18next": { "version": "22.5.0", "resolved": "https://registry.npmjs.org/i18next/-/i18next-22.5.0.tgz", diff --git a/package.json b/package.json index e6fbf65..44f5a1f 100755 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "@emotion/react": "^11.11.4", "@emotion/styled": "^11.11.5", "@lottiefiles/dotlottie-react": "^0.6.4", + "@mediapipe/tasks-vision": "^0.10.22-rc.20250304", "@microsoft/clarity": "^1.0.0", "@mui/material": "^5.15.21", "@reduxjs/toolkit": "^1.9.5", @@ -27,6 +28,7 @@ "core-js": "^3.37.1", "framer-motion": "^11.0.8", "html-react-parser": "^3.0.16", + "i18n-iso-countries": "^7.14.0", "i18next": "^22.5.0", "i18next-react-postprocessor": "^3.1.0", "idb": "^8.0.0", diff --git a/public/locales/compatibility-v4/en/male_en.json b/public/locales/compatibility-v4/en/male_en.json index db2fa5f..8e71ce4 100644 --- a/public/locales/compatibility-v4/en/male_en.json +++ b/public/locales/compatibility-v4/en/male_en.json @@ -43,7 +43,7 @@ "life": "Life line ✅" }, "reading_ready": { - "title": "Your Compatibility Reading is READY and available in the app for your iPhone!" + "title": "Your Compatibility Reading is READY" }, "your_access_code": "Your Access Code", "copy": "COPY", @@ -88,10 +88,30 @@ "point6": "Индивидуальный прогноз на развитие отношений: что ждет вас впереди.", "point7": "Вопросы и персональные рекомендации от эксперта.", "point8": "Уникальная информация, которую нельзя найти в стандартных гороскопах." + }, + "v3": { + "point1": "Индивидуальный прогноз на развитие отношений: что ждет вас впереди.", + "point2": "Уникальная информация, которую нельзя найти в стандартных гороскопах.", + "point3": "Подробный астрологический разбор: что сближает и что вызывает напряжение.", + "point4": "Полная картина вашей совместимости: уровни в процентах — без иллюзий и догадок.", + "point5": "Вопросы и персональные рекомендации от эксперта.", + "point6": "Подробный астрологический разбор: что сближает и что вызывает напряжение.", + "point7": "Глубокое понимание партнера: как он любит и что для него важно." } }, "description": "To read the full reading, you need to get access through the app for your iPhone" - } + }, + "offer_reserved": { + "title": "Offer reserved", + "button": "Get my Reading" + }, + "information-title": "Мы готовы дать тебе все ответы, не трать годы на сомнения!", + "information-description-single": "Ты когда-нибудь задумывался, почему одни отношения развиваются легко, а другие будто натянуты, как струна? Совпадение или знак судьбы? Руки рассказывают больше, чем вы думаете. Линии на вашей ладони — это карта ваших отношений. в вашей жизни есть скрытые знаки, которые вы ещё не заметили.
Получите детальный анализ совместимости по хиромантии и откройте ответы, которые уже написаны в вашей судьбе.", + "information-description-single-color": " ()", + "information-description-single-event-description": "Ваша дата могла стать поворотной точкой или скрытым сигналом.", + "information-description-with-partner": "Ты когда-нибудь задумывался, почему одни отношения развиваются легко, а другие будто натянуты, как струна? Совпадение или знак судьбы? — два знака, созданные для глубины, но какие тайны скрывает ваш союз? Получите детальный анализ совместимости и откройте ответы, которые уже написаны в вашей судьбе.", + "information-description-with-partner-color": " () + ()", + "information-description-with-partner-event-description": "Ваша дата могла стать поворотной точкой или скрытым сигналом." }, "/find-your-happiness": { "title": "Gain Clarity and Confidence in Life", diff --git a/public/locales/profile/en/male_en.json b/public/locales/profile/en/male_en.json new file mode 100644 index 0000000..31bde5c --- /dev/null +++ b/public/locales/profile/en/male_en.json @@ -0,0 +1,52 @@ +{ + "/profile": { + "profile_information": { + "title": "Profile Information", + "description": "To update your email address please contact support.", + "email_placeholder": "Email", + "name_placeholder": "Name" + }, + "billing": { + "title": "Billing", + "description": "To access your subscription information, please log into your billing account.", + "subscription_type": "Subscription Type:", + "billing_button": "Billing", + "credits": { + "title": "You have credits left", + "description": "You can use them to chat with any Expert on the platform." + }, + "any_questions": "Any questions? ", + "any_questions_link": "Contact us", + "subscription_update": "
If you've just purchased or changed plan, your subscription status will change in a few hours.", + "subscription_update_bold": "Subscription information is updated every few hours." + }, + "log_out": { + "title": "Log out", + "log_out_button": "Log out", + "modal": { + "title": "Are you sure you want to log out?", + "description": "Are you sure you want to log out?", + "stay_button": "Stay", + "log_out_button": "Log out" + } + } + }, + "/subscriptions": { + "title": "Manage my subscriptions", + "modal": { + "title": "Are you sure you want to cancel your subscription?", + "description": "Are you sure you want to cancel your subscription?", + "cancel_button": "Cancel subscription", + "stay_button": "Stay" + }, + "table": { + "subscription_type": "Subscription Type", + "subscription_status": "Subscription Status", + "billing_period": "Billing Period", + "last_payment_on": "Last Payment On", + "renewal_date": "Renewal Date", + "renewal_amount": "Renewal Amount", + "cancel_subscription": "Cancel Subscription" + } + } +} \ No newline at end of file diff --git a/public/locales/v1/en/male_en.json b/public/locales/v1/en/male_en.json index 445a62b..77c1d80 100644 --- a/public/locales/v1/en/male_en.json +++ b/public/locales/v1/en/male_en.json @@ -126,7 +126,25 @@ "description": "You will be charged only . \n before your trial period ends. \nCancel anytime. The charge will appear on your bill as witapps.", "address": "2108 N ST STE 5446 SACRAMENTO, CA 95816", "form_error": "Проверьте правильность заполнения формы", - "price_information": "A charge" + "price_information": "A charge", + "address_form": { + "labels": { + "country": "Country", + "address": "Address", + "zip": "Zip" + }, + "placeholders": { + "country": "Select country", + "address": "City, street, house", + "zip": "Postal code" + }, + "errors": { + "country": "Please select a country", + "address": "Please enter an address", + "zip": "Please enter a zip code", + "zip_invalid": "Invalid zip code for selected country" + } + } }, "add_report": "Add Report", "unlimited_readings": "Unlimited Readings", diff --git a/src/api/resources/Payment.ts b/src/api/resources/Payment.ts index 279018e..4ce17a4 100644 --- a/src/api/resources/Payment.ts +++ b/src/api/resources/Payment.ts @@ -1,6 +1,7 @@ import routes from "@/routes"; import { getAuthHeaders, getBaseHeaders } from "../utils"; import { ICreateAuthorizeResponse } from "./User"; +import { AddressFields } from "@/hooks/payment/nmi/useAddressFields"; export interface Payload { token: string; @@ -11,6 +12,7 @@ export interface PayloadPost extends Payload { placementId: string; paywallId: string; paymentToken?: string; + address?: AddressFields; } export interface PayloadPostAnonymous { @@ -19,6 +21,7 @@ export interface PayloadPostAnonymous { paywallId: string; paymentToken: string; sessionId: string; + address?: AddressFields; } interface ResponsePostSuccess { @@ -62,25 +65,27 @@ export interface ResponsePostAnonymousSuccess { invoiceId: string; } -export const createRequestPost = ({ token, productId, placementId, paywallId, paymentToken }: PayloadPost): Request => { +export const createRequestPost = ({ token, productId, placementId, paywallId, paymentToken, address }: PayloadPost): Request => { const url = new URL(routes.server.makePayment()); const body = JSON.stringify({ productId, placementId, paywallId, - paymentToken + paymentToken, + address }); return new Request(url, { method: "POST", headers: getAuthHeaders(token), body }); }; -export const createRequestPostAnonymous = ({ productId, placementId, paywallId, paymentToken, sessionId }: PayloadPostAnonymous): Request => { +export const createRequestPostAnonymous = ({ productId, placementId, paywallId, paymentToken, sessionId, address }: PayloadPostAnonymous): Request => { const url = new URL(routes.server.makeAnonymousPayment()); const body = JSON.stringify({ productId, placementId, paywallId, paymentToken, - sessionId + sessionId, + address }); return new Request(url, { method: "POST", headers: getBaseHeaders(), body }); }; diff --git a/src/components/App/index.tsx b/src/components/App/index.tsx index 8f326ca..bfa643e 100755 --- a/src/components/App/index.tsx +++ b/src/components/App/index.tsx @@ -1,7 +1,9 @@ import { - // useCallback, + useCallback, useEffect, useLayoutEffect, + useMemo, + useRef, // useMemo, // useRef, useState, @@ -14,6 +16,7 @@ import { useLocation, // useNavigate, useSearchParams, + useNavigate, } from "react-router-dom"; import { useAuth } from "@/auth"; import { useDispatch, useSelector } from "react-redux"; @@ -23,7 +26,7 @@ import routes, { getRouteBy, // hasNoFooter, // hasNoHeader, - // hasNavbarFooter, + hasNavbarFooter, // hasFullDataModal, palmistryV1Prefix, // chatsPrefix, @@ -33,6 +36,11 @@ import routes, { anonymousPrefix, compatibilityV3Prefix, compatibilityV4Prefix, + hasFullDataModal, + hasNoHeader, + hasNoFooter, + hasNavigation, + profilePrefix, } from "@/routes"; import BirthdayPage from "../BirthdayPage"; import BirthtimePage from "../BirthtimePage"; @@ -41,9 +49,9 @@ import EmailEnterPage from "../EmailEnterPage"; import PaymentPage from "../PaymentPage"; import WallpaperPage from "../WallpaperPage"; // import NotFoundPage from "../NotFoundPage"; -// import Header from "../Header"; -// import Navbar from "../Navbar"; -// import Footer from "../Footer"; +import Header from "../Header"; +import Navbar from "../Navbar"; +import Footer from "../Footer"; import "./styles.css"; import DidYouKnowPage from "../DidYouKnowPage"; import FreePeriodInfoPage from "../FreePeriodInfoPage"; @@ -54,11 +62,11 @@ import PriceListPage from "../PriceListPage"; import CompatResultPage from "../CompatResultPage"; import HomePage from "../HomePage"; import UserCallbacksPage from "../UserCallbacksPage"; -// import NavbarFooter, { INavbarHomeItems } from "../NavbarFooter"; -// import { EPathsFromHome } from "@/store/siteConfig"; -import { APNG } from "apng-js"; -// import { useApi, useApiCall } from "@/api"; -// import { Asset } from "@/api/resources/Assets"; +import NavbarFooter, { INavbarHomeItems } from "../NavbarFooter"; +import { EPathsFromHome } from "@/store/siteConfig"; +import parseAPNG, { APNG } from "apng-js"; +import { useApi, useApiCall } from "@/api"; +import { Asset } from "@/api/resources/Assets"; import PaymentResultPage from "../PaymentPage/results"; import PaymentSuccessPage from "../PaymentPage/results/SuccessPage"; import PaymentFailPage from "../PaymentPage/results/ErrorPage"; @@ -80,8 +88,8 @@ import NameHoroscopeResult from "../pages/NameHoroscopeResult"; // import LoadingInRelationshipPage from "../pages/LoadingInRelationship"; // import QuestionnaireIntermediatePage from "../pages/QuestionnaireIntermediate"; // import RelationshipAlmostTherePage from "../pages/RelationshipAlmostThere"; -// import Modal from "../Modal"; -// import FullDataModal from "../FullDataModal"; +import Modal from "../Modal"; +import FullDataModal from "../FullDataModal"; // import SingleZodiacInfoPage from "../pages/SingleZodiacInfo"; // import ProblemsPage from "../pages/Problems"; // import WorksRouterPage from "../pages/WorksRouter"; @@ -118,7 +126,7 @@ import Advisors from "../pages/Advisors"; import AdvisorChatPage from "../pages/AdvisorChat"; // import SuccessPaymentPage from "../pages/SinglePaymentPage/ResultPayment/SuccessPaymentPage"; // import FailPaymentPage from "../pages/SinglePaymentPage/ResultPayment/FailPaymentPage"; -// import { useSchemeColorByElement } from "@/hooks/useSchemeColorByElement"; +import { useSchemeColorByElement } from "@/hooks/useSchemeColorByElement"; import GetInformationPartnerPage from "../pages/GetInformationPartner"; // import BirthPlacePage from "../pages/BirthPlacePage"; // import LoadingPage from "../pages/LoadingPage"; @@ -148,6 +156,7 @@ import { useSession } from "@/hooks/session/useSession"; import { getSourceByPathname } from "@/utils/source.utils"; import "../palmistry/palmistry-container/palmistry-container.css" +import ProfileRoutes from "@/routerComponents/Profile"; const isProduction = import.meta.env.MODE === "production"; const gaMeasurementId = import.meta.env.AURA_GA_MEASUREMENT_ID; @@ -161,13 +170,12 @@ ReactGA.initialize(gaMeasurementId); function App(): JSX.Element { const location = useLocation(); const [leoApng, setLeoApng] = useState(Error); - setLeoApng useScrollToTop({ scrollBehavior: "auto" }); // const [ // padLockApng, // setPadLockApng, // ] = useState(Error); - // const api = useApi(); + const api = useApi(); const dispatch = useDispatch(); const { user } = useAuth(); const { session } = useSession(); @@ -193,7 +201,7 @@ function App(): JSX.Element { unleashClient.updateContext({ sessionId: session?.[source] || undefined, properties: { - source + source } }); } @@ -267,27 +275,27 @@ function App(): JSX.Element { }); }, []); - // const assetsData = useCallback(async () => { - // const { assets } = await api.getAssets({ - // category: String("au"), - // }); - // return assets; - // }, [api]); + const assetsData = useCallback(async () => { + const { assets } = await api.getAssets({ + category: String("au"), + }); + return assets; + }, [api]); - // const { data } = useApiCall(assetsData); + const { data } = useApiCall(assetsData); // data - // useEffect(() => { - // async function getApng() { - // if (!data) return; - // const response = await fetch( - // data.find((item) => item.key === "au.apng.leo")?.url || "" - // ); - // const arrayBuffer = await response.arrayBuffer(); - // setLeoApng(parseAPNG(arrayBuffer)); - // } - // getApng(); - // }, [data]); + useEffect(() => { + async function getApng() { + if (!data) return; + const response = await fetch( + data.find((item) => item.key === "au.apng.leo")?.url || "" + ); + const arrayBuffer = await response.arrayBuffer(); + setLeoApng(parseAPNG(arrayBuffer)); + } + getApng(); + }, [data]); // useEffect(() => { // (async () => { @@ -319,6 +327,10 @@ function App(): JSX.Element { } > + } + /> } @@ -437,66 +449,70 @@ function App(): JSX.Element { /> }> - } /> } - /> - } - /> - } - /> - } - /> - } - /> - } /> - - } /> + element={} + > + } /> + } + /> + } + /> + } + /> + } + /> + } + /> + } /> + + } /> + + } + /> + } + /> + } + /> + } + /> + } + /> + } + /> + } + /> + } + /> - } - /> - } - /> - } - /> - } - /> - } - /> - } - /> - } - /> - } - /> - + {/* } /> */} } /> @@ -975,126 +991,127 @@ function App(): JSX.Element { ); } -// function Layout(): JSX.Element { -// const location = useLocation(); -// const navigate = useNavigate(); -// const dispatch = useDispatch(); -// const showNavbar = hasNavigation(location.pathname); -// const showFooter = hasNoFooter(location.pathname); -// const showHeader = hasNoHeader(location.pathname); -// const isRouteFullDataModal = hasFullDataModal(location.pathname); -// const [isMenuOpen, setIsMenuOpen] = useState(false); -// const homeConfig = useSelector(selectors.selectHome); -// const showNavbarFooter = homeConfig.isShowNavbar; -// const mainRef = useRef(null); -// useSchemeColorByElement(mainRef.current, "section.page, .page, section", [ -// location, -// ]); +function Layout(): JSX.Element { + const location = useLocation(); + const navigate = useNavigate(); + const dispatch = useDispatch(); + const showNavbar = hasNavigation(location.pathname); + const showFooter = hasNoFooter(location.pathname); + const showHeader = hasNoHeader(location.pathname); + const isRouteFullDataModal = hasFullDataModal(location.pathname); + const [isMenuOpen, setIsMenuOpen] = useState(false); + // const homeConfig = useSelector(selectors.selectHome); + // const showNavbarFooter = homeConfig.isShowNavbar; + const showNavbarFooter = true; + const mainRef = useRef(null); + useSchemeColorByElement(mainRef.current, "section.page, .page, section", [ + location, + ]); -// const birthdate = useSelector(selectors.selectBirthdate); -// const dataItems = useMemo(() => [birthdate], [birthdate]); -// const [isShowFullDataModal, setIsShowFullDataModal] = -// useState(false); + const birthdate = useSelector(selectors.selectBirthdate); + const dataItems = useMemo(() => [birthdate], [birthdate]); + const [isShowFullDataModal, setIsShowFullDataModal] = + useState(false); -// useEffect(() => { -// setIsShowFullDataModal(getIsShowFullDataModal(dataItems)); -// }, [dataItems]); + useEffect(() => { + setIsShowFullDataModal(getIsShowFullDataModal(dataItems)); + }, [dataItems]); -// const onCloseFullDataModal = (_birthDate: string) => { -// dispatch(actions.form.addDate(_birthDate)); -// setIsShowFullDataModal(getIsShowFullDataModal(dataItems)); -// }; + const onCloseFullDataModal = (_birthDate: string) => { + dispatch(actions.form.addDate(_birthDate)); + setIsShowFullDataModal(getIsShowFullDataModal(dataItems)); + }; -// const handleCompatibility = () => { -// dispatch( -// actions.siteConfig.update({ -// home: { -// pathFromHome: EPathsFromHome.navbar, -// isShowNavbar: showNavbarFooter, -// }, -// }) -// ); -// navigate(routes.client.compatibility()); -// }; -// const handleBreath = () => { -// dispatch( -// actions.siteConfig.update({ -// home: { -// pathFromHome: EPathsFromHome.navbar, -// isShowNavbar: showNavbarFooter, -// }, -// }) -// ); -// navigate(routes.client.breath()); -// }; + const handleCompatibility = () => { + dispatch( + actions.siteConfig.update({ + home: { + pathFromHome: EPathsFromHome.navbar, + isShowNavbar: showNavbarFooter, + }, + }) + ); + navigate(routes.client.compatibility()); + }; + const handleBreath = () => { + dispatch( + actions.siteConfig.update({ + home: { + pathFromHome: EPathsFromHome.navbar, + isShowNavbar: showNavbarFooter, + }, + }) + ); + navigate(routes.client.breath()); + }; -// const navbarItems: INavbarHomeItems[] = [ -// { -// title: "Breathing", -// path: routes.client.breath(), -// paths: [routes.client.breath(), routes.client.breathResult()], -// image: "Breath.svg", -// onClick: handleBreath, -// }, -// { -// title: "Aura", -// path: routes.client.home(), -// paths: [routes.client.home()], -// image: "Aura.svg", -// active: true, -// onClick: () => null, -// }, -// { -// title: "Compatibility", -// path: routes.client.compatibility(), -// paths: [ -// routes.client.compatibility(), -// routes.client.compatibilityResult(), -// ], -// image: "Compatibility.svg", -// onClick: handleCompatibility, -// }, -// { -// title: "Advisors", -// path: routes.client.advisors(), -// paths: [routes.client.advisors()], -// image: "moon.svg", -// onClick: () => null, -// }, -// { -// title: "My Moon", -// path: routes.client.wallpaper(), -// paths: [routes.client.wallpaper()], -// image: "moon.svg", -// onClick: () => null, -// }, -// ]; + const navbarItems: INavbarHomeItems[] = [ + { + title: "Breathing", + path: routes.client.breath(), + paths: [routes.client.breath(), routes.client.breathResult()], + image: "Breath.svg", + onClick: handleBreath, + }, + { + title: "Aura", + path: routes.client.home(), + paths: [routes.client.home()], + image: "Aura.svg", + active: true, + onClick: () => null, + }, + { + title: "Compatibility", + path: routes.client.compatibility(), + paths: [ + routes.client.compatibility(), + routes.client.compatibilityResult(), + ], + image: "Compatibility.svg", + onClick: handleCompatibility, + }, + { + title: "Advisors", + path: routes.client.advisors(), + paths: [routes.client.advisors()], + image: "moon.svg", + onClick: () => null, + }, + { + title: "My Moon", + path: routes.client.wallpaper(), + paths: [routes.client.wallpaper()], + image: "moon.svg", + onClick: () => null, + }, + ]; -// return ( -//
-// {showHeader ? ( -//
setIsMenuOpen(true)} -// /> -// ) : null} -// {isRouteFullDataModal && ( -// { }}> -// -// -// )} -//
-// -//
-// {showFooter ?
: null} -// {showNavbar ? ( -// setIsMenuOpen(false)} /> -// ) : null} -// {showNavbarFooter && hasNavbarFooter(location.pathname) ? ( -// -// ) : null} -//
-// ); -// } + return ( +
+ {showHeader ? ( +
setIsMenuOpen(true)} + /> + ) : null} + {isRouteFullDataModal && ( + { }}> + + + )} +
+ +
+ {showFooter ?
: null} + {showNavbar ? ( + setIsMenuOpen(false)} /> + ) : null} + {showNavbarFooter && hasNavbarFooter(location.pathname) ? ( + + ) : null} +
+ ); +} // enum EIsAuthPageType { // private, @@ -1275,18 +1292,18 @@ function PrivateSubscriptionOutlet(): JSX.Element { ); } -// function getIsShowFullDataModal(dataItems: Array = []): boolean { -// let hasNoDataItem = false; +function getIsShowFullDataModal(dataItems: Array = []): boolean { + let hasNoDataItem = false; -// for (const item of dataItems) { -// if (!item) { -// hasNoDataItem = true; -// break; -// } -// } + for (const item of dataItems) { + if (!item) { + hasNoDataItem = true; + break; + } + } -// return hasNoDataItem; -// } + return hasNoDataItem; +} function SkipStep(): JSX.Element { const { user } = useAuth(); diff --git a/src/components/CompatibilityV2/components/CameraModal/index.tsx b/src/components/CompatibilityV2/components/CameraModal/index.tsx index a75f449..406d088 100644 --- a/src/components/CompatibilityV2/components/CameraModal/index.tsx +++ b/src/components/CompatibilityV2/components/CameraModal/index.tsx @@ -2,8 +2,9 @@ import Webcam from "react-webcam"; import styles from "./styles.module.scss"; import ModalOverlay, { ModalOverlayType } from "@/components/palmistry/modal-overlay/modal-overlay"; import Modal from "@/components/palmistry/modal/modal"; -import { useRef, useState } from "react"; -// import { useDynamicSize } from "@/hooks/useDynamicSize"; +import { useEffect, useRef, useState } from "react"; +import { HandLandmarker, DrawingUtils, HandLandmarkerResult } from "@mediapipe/tasks-vision"; +import { handLandmarkerSingleton } from "@/utils/handLandmarkerSingleton"; interface ExtendedMediaTrackCapabilities extends MediaTrackCapabilities { torch?: boolean; @@ -22,23 +23,60 @@ interface CameraModalProps { onTakePhoto: (photo: string) => void; onError: (error: string | DOMException) => void; onVideoReady?: () => void; + onMediaPipeInitChange?: (status: boolean) => void; + onIsFacingCameraChange?: (isFacingCamera: boolean) => void; reinitializeKey?: number; // for reinitializing the camera (change the key to reinitialize the camera) isCameraVisible?: boolean; className?: string; + isShowHand?: boolean; + isMediaPipe?: boolean; + mediaPipeRenderingTemplate?: "v2" | "v3" | "v4"; } +function getIsPalmFacingCamera(landmarks: { x: number, y: number, z: number }[], handedness: "Left" | "Right"): boolean { + const v1 = { + x: landmarks[5].x - landmarks[0].x, + y: landmarks[5].y - landmarks[0].y, + z: landmarks[5].z - landmarks[0].z, + }; + const v2 = { + x: landmarks[17].x - landmarks[0].x, + y: landmarks[17].y - landmarks[0].y, + z: landmarks[17].z - landmarks[0].z, + }; + const normal = { + x: v1.y * v2.z - v1.z * v2.y, + y: v1.z * v2.x - v1.x * v2.z, + z: v1.x * v2.y - v1.y * v2.x, + }; + if (handedness === "Right") { + return normal.z < 0; + } else { + return normal.z > 0; + } +} + + function CameraModal({ onClose, onTakePhoto, onError, onVideoReady, + onMediaPipeInitChange, + onIsFacingCameraChange, reinitializeKey = 0, isCameraVisible = true, - className = "" + className = "", + isShowHand = true, + isMediaPipe = false, + mediaPipeRenderingTemplate = "v2" }: CameraModalProps) { const [isVideoReady, setIsVideoReady] = useState(false); const [isTorchOn, setIsTorchOn] = useState(false); const [isTorchAvailable, setIsTorchAvailable] = useState(false); + const [isHandDetected, setIsHandDetected] = useState(false); + const [isPalmFacingCamera, setIsPalmFacingCamera] = useState(false); + const [handName, setHandName] = useState<"Left" | "Right">("Left"); // const { // width: _width, height: _height, // elementRef: containerRef } = useDynamicSize({}); @@ -49,7 +87,9 @@ function CameraModal({ // const isLandscape = height <= width; // const ratio = isLandscape ? width / height : height / width; const cameraRef = useRef(null); - + const canvasRef = useRef(null); + const containerRef = useRef(null); + const handLandmarkerRef = useRef(null); // useEffect(() => { // setIsVideoReady(false); // }, [reinitializeKey]); @@ -115,6 +155,166 @@ function CameraModal({ } }; + useEffect(() => { + const unsubscribe = handLandmarkerSingleton.subscribe((isLoaded) => { + onMediaPipeInitChange?.(isLoaded); + if (isLoaded) { + handLandmarkerRef.current = handLandmarkerSingleton.getHandLandmarker(); + } + }); + + handLandmarkerSingleton.preload(); + + return () => unsubscribe(); + }, []); + + useEffect(() => { + if (!isCameraVisible || !isVideoReady || !isMediaPipe || !handLandmarkerRef.current) return; + + let running = true; + let animationFrameId: number; + let lastVideoTime = -1; + let results: HandLandmarkerResult | undefined = undefined; + + const predictWebcam = () => { + const handLandmarker = handLandmarkerRef.current; + if (!handLandmarker || !cameraRef.current?.video || !canvasRef.current) { + animationFrameId = requestAnimationFrame(predictWebcam); + return; + } + const video = cameraRef.current.video; + const canvas = canvasRef.current; + // if ( + // canvas.width !== video.videoWidth || + // canvas.height !== video.videoHeight + // ) { + // canvas.width = video.videoWidth; + // canvas.height = video.videoHeight; + // } + + if ( + video.videoWidth === 0 || + video.videoHeight === 0 + ) { + animationFrameId = requestAnimationFrame(predictWebcam); + return; + } + + const ctx = canvas.getContext("2d"); + if (!ctx) return; + + const now = performance.now(); + if (lastVideoTime !== video.currentTime) { + lastVideoTime = video.currentTime; + results = handLandmarker.detectForVideo(video, now); + } + + const _isHandDetected = !!(results?.landmarks && results?.landmarks.length > 0); + setIsHandDetected(_isHandDetected); + + ctx.save(); + ctx.clearRect(0, 0, canvas.width, canvas.height); + + if (results?.landmarks) { + const drawingUtils = new DrawingUtils(ctx); + results.landmarks.forEach((landmarks, i) => { + const handedness = results?.handedness[i][0].categoryName as "Left" | "Right"; + const _isPalmFacingCamera = getIsPalmFacingCamera(landmarks, handedness); + setIsPalmFacingCamera(_isPalmFacingCamera); + if (_isHandDetected) { + onIsFacingCameraChange?.(_isPalmFacingCamera); + } + if (_isHandDetected && _isPalmFacingCamera) { + setHandName(handedness); + + let fingertipIndexes = [2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]; + + if (mediaPipeRenderingTemplate === "v2") { + fingertipIndexes = [4, 8, 12, 16, 20]; + } + const fingertipPoints = fingertipIndexes.map(idx => landmarks[idx]); + const fingertipConnections = [ + { start: 0, end: 1 }, + { start: 1, end: 2 }, + { start: 3, end: 4 }, + { start: 4, end: 5 }, + { start: 5, end: 6 }, + { start: 7, end: 8 }, + { start: 8, end: 9 }, + { start: 9, end: 10 }, + { start: 11, end: 12 }, + { start: 12, end: 13 }, + { start: 13, end: 14 }, + { start: 15, end: 16 }, + { start: 16, end: 17 }, + { start: 17, end: 18 }, + ]; + fingertipPoints.forEach(point => { + if (mediaPipeRenderingTemplate === "v2") { + drawingUtils.drawLandmarks([point], { color: "#ffffff4d", lineWidth: 4, fillColor: "#0b60f1" }); + } + if (mediaPipeRenderingTemplate === "v3") { + drawingUtils.drawLandmarks([point], { color: "#0b60f1", lineWidth: 1 }); + } + if (mediaPipeRenderingTemplate === "v4") { + drawingUtils.drawLandmarks([point], { color: "#0b60f1", lineWidth: 1 }); + drawingUtils.drawConnectors(fingertipPoints, fingertipConnections, { color: "#a3a3a303", lineWidth: 2 }); + } + }); + } + }) + } + ctx.restore(); + + if (running) { + animationFrameId = requestAnimationFrame(predictWebcam); + } + }; + + predictWebcam(); + + return () => { + running = false; + if (animationFrameId) cancelAnimationFrame(animationFrameId); + }; + }, [isCameraVisible, reinitializeKey, isVideoReady, isMediaPipe, handLandmarkerRef.current]); + + const getStyleCanvas = () => { + const containerWidth = containerRef?.current?.clientWidth || 0; + const containerHeight = containerRef?.current?.clientHeight || 0; + const videoWidth = cameraRef?.current?.video?.videoWidth || 0; + const videoHeight = cameraRef?.current?.video?.videoHeight || 0; + const videoAspect = videoWidth / videoHeight; + const containerAspect = containerWidth / containerHeight; + + let style = {}; + + if (containerAspect > videoAspect) { + const displayWidth = containerWidth; + style = { + position: "absolute", + width: `${videoWidth}px`, + height: `${videoHeight}px`, + left: "50%", + top: "50%", + transform: `translate(-50%, -50%) scale(${displayWidth / videoWidth})`, + pointerEvents: "none", + }; + } else { + const displayHeight = containerHeight; + style = { + position: "absolute", + width: `${videoWidth}px`, + height: `${videoHeight}px`, + left: "50%", + top: "50%", + transform: `translate(-50%, -50%) scale(${displayHeight / videoHeight})`, + pointerEvents: "none", + }; + } + return style; + } + return
- {/* {width} {height} */} - - - - - {isCameraVisible && { - setIsVideoReady(false); - setIsTorchAvailable(false); - console.error(error); - onError(error); - }} - />} + {isShowHand && (!isHandDetected || !isPalmFacingCamera || !isMediaPipe) && ( + + + + )} + {isCameraVisible && ( + <> + { + setIsVideoReady(false); + setIsTorchAvailable(false); + console.error(error); + onError(error); + }} + /> + {isMediaPipe && } + + )}
- + - + @@ -118,7 +118,7 @@ function ScanInstructionSVG(props: {variant: string}) { - + diff --git a/src/components/CompatibilityV2/pages/Camera/android/index.tsx b/src/components/CompatibilityV2/pages/Camera/android/index.tsx index 0825f8a..f008b1c 100644 --- a/src/components/CompatibilityV2/pages/Camera/android/index.tsx +++ b/src/components/CompatibilityV2/pages/Camera/android/index.tsx @@ -27,6 +27,7 @@ enum EToastVisible { "no_access_camera" = "no_access_camera", "reload_page" = "reload_page", "upload_photo" = "upload_photo", + "turn_hand" = "turn_hand", } const isWebViewAndroid = isWebView() && isAndroid; @@ -53,6 +54,10 @@ function AndroidCamera() { flag: EUnleashFlags.compatibilityV2TimeForCameraInit }); + const { variant: v2CompatibilityCameraTemplate } = useUnleash({ + flag: EUnleashFlags.v2CompatibilityCameraTemplate + }); + const timeForCameraInit = Number(compatibilityV2TimeForCameraInit) || 6000; const [isCameraModalOpen, setIsCameraModalOpen] = useState(false); @@ -60,6 +65,7 @@ function AndroidCamera() { const [isRequestCameraModalOpen, setIsRequestCameraModalOpen] = useState(isShowCameraRequestModal); const [toastVisible, setToastVisible] = useState(null); const [isVideoReady, setIsVideoReady] = useState(false); + const [isMediaPipeInit, setIsMediaPipeInit] = useState(true); const handleToScanHand = () => { metricService.reachGoal(EGoals.SCAN_ARTIFICIAL_PHOTO, [EMetrics.YANDEX, EMetrics.KLAVIYO]); @@ -294,18 +300,30 @@ function AndroidCamera() { )} */} {/* Модальное окно камеры */} - {!isLoading && console.log("close")} onTakePhoto={handleCameraSuccess} onError={handleCameraError} + isShowHand={v2CompatibilityCameraTemplate !== "v1"} + isMediaPipe={["v2", "v3", "v4"].includes(v2CompatibilityCameraTemplate)} + mediaPipeRenderingTemplate={v2CompatibilityCameraTemplate as "v2" | "v3" | "v4"} + onMediaPipeInitChange={(status) => setIsMediaPipeInit(status)} isCameraVisible={isCameraModalOpen} onVideoReady={() => setIsVideoReady(true)} + className={(isLoading || !isMediaPipeInit) ? styles.hideCameraModal : ""} + onIsFacingCameraChange={(isFacingCamera) => { + if (!isFacingCamera) { + setToastVisible(EToastVisible.turn_hand) + } else { + setToastVisible(null) + } + }} // reinitializeKey={reinitializeCameraCount} - />} + /> {/* Лоадер */} - {isLoading && ( + {(isLoading || !isMediaPipeInit) && ( )} @@ -409,6 +427,18 @@ function AndroidCamera() {
)} + + {/* Тост если нужно повернуть руку */} + {toastVisible === EToastVisible.turn_hand && ( + +
+ {translate("/camera.turn_hand")} +
+
+ )} ) } diff --git a/src/components/CompatibilityV2/pages/Camera/iphone/index.tsx b/src/components/CompatibilityV2/pages/Camera/iphone/index.tsx index 6bbe1fc..0a1c3b1 100644 --- a/src/components/CompatibilityV2/pages/Camera/iphone/index.tsx +++ b/src/components/CompatibilityV2/pages/Camera/iphone/index.tsx @@ -24,6 +24,7 @@ enum EToastVisible { "try_again_or_next" = "try_again_or_next", "no_access_camera" = "no_access_camera", "reload_page" = "reload_page", + "turn_hand" = "turn_hand", } function IphoneCamera() { @@ -42,6 +43,11 @@ function IphoneCamera() { flag: EUnleashFlags.compatibilityV2ScanHand }); + const { variant: v2CompatibilityCameraTemplate = "v0" } = useUnleash({ + flag: EUnleashFlags.v2CompatibilityCameraTemplate + }); + console.log("v2CompatibilityCameraTemplate: ", v2CompatibilityCameraTemplate) + const isShowScanHand = compatibilityV2ScanHand !== "hide"; const [isLoading, setIsLoading] = useState(false); @@ -49,6 +55,7 @@ function IphoneCamera() { const [isCameraModalOpen, setIsCameraModalOpen] = useState(false); const [reinitializeCameraCount, setReinitializeCameraCount] = useState(0); const [toastVisible, setToastVisible] = useState(null); + const [isMediaPipeInit, setIsMediaPipeInit] = useState(true); const handleToScanHand = () => { metricService.reachGoal(EGoals.SCAN_ARTIFICIAL_PHOTO, [EMetrics.YANDEX, EMetrics.KLAVIYO]); @@ -250,14 +257,25 @@ function IphoneCamera() { onClose={() => console.log("close")} onTakePhoto={handleCameraSuccess} onError={handleCameraError} + isShowHand={v2CompatibilityCameraTemplate !== "v1"} + isMediaPipe={["v2", "v3", "v4"].includes(v2CompatibilityCameraTemplate)} + mediaPipeRenderingTemplate={v2CompatibilityCameraTemplate as "v2" | "v3" | "v4"} + onMediaPipeInitChange={(status) => setIsMediaPipeInit(status)} isCameraVisible={isCameraModalOpen} reinitializeKey={reinitializeCameraCount} - className={isLoading ? styles.hideCameraModal : ""} + className={(isLoading || !isMediaPipeInit) ? styles.hideCameraModal : ""} + onIsFacingCameraChange={(isFacingCamera) => { + if (!isFacingCamera) { + setToastVisible(EToastVisible.turn_hand) + } else { + setToastVisible(null) + } + }} /> {/* )} */} {/* Лоадер */} - {isLoading && ( + {(isLoading || !isMediaPipeInit) && ( )} @@ -347,6 +365,18 @@ function IphoneCamera() {
)} + + {/* Тост если нужно повернуть руку */} + {toastVisible === EToastVisible.turn_hand && ( + +
+ {translate("/camera.turn_hand")} +
+
+ )} ) } diff --git a/src/components/CompatibilityV2/pages/Gender/index.tsx b/src/components/CompatibilityV2/pages/Gender/index.tsx index 8dc6e12..9b2676f 100644 --- a/src/components/CompatibilityV2/pages/Gender/index.tsx +++ b/src/components/CompatibilityV2/pages/Gender/index.tsx @@ -9,7 +9,7 @@ import { useCallback, useEffect, useState } from "react"; import { sleep } from "@/services/date"; import metricService, { useMetricABFlags } from "@/services/metric/metricService"; import { genders } from "@/components/pages/ABDesign/v1/data/genders"; -import { useNavigate } from "react-router-dom"; +import { Navigate, useNavigate, useSearchParams } from "react-router-dom"; import routes, { compatibilityV2Prefix } from "@/routes"; import { usePreloadImages } from "@/hooks/preload/images"; import { useSession } from "@/hooks/session/useSession"; @@ -30,6 +30,9 @@ function GenderPage() { selectors.selectPrivacyPolicy ); + const [searchParams] = useSearchParams(); + const noRedirectAB = searchParams.get("noRedirectAB") === "true"; + const { gender } = useSelector(selectors.selectQuestionnaire); const [isSelected, setIsSelected] = useState(false); @@ -41,6 +44,10 @@ function GenderPage() { flag: EUnleashFlags.genderPageType }); + const { variant: relationshipStatusPagePlacement = "v0" } = useUnleash({ + flag: EUnleashFlags.v2CompatibilityRelationshipStatusPagePlacement + }); + const pageType = flags?.genderPageType?.[0] || genderPageType || "v2"; const genderButtonIcon = flags?.genderButtonIcon?.[0] || "hide"; @@ -97,6 +104,9 @@ function GenderPage() { sessionId: session.sessionId, }); } + if (relationshipStatusPagePlacement === "v2") { + return navigate(routes.client.compatibilityV2RelationshipStatus()); + } return navigate(routes.client.compatibilityV2Birthdate()); // eslint-disable-next-line react-hooks/exhaustive-deps }, [gender, navigate]); @@ -110,6 +120,10 @@ function GenderPage() { if (!ready || !isReady) return ; + if (relationshipStatusPagePlacement === "v1" && !noRedirectAB) { + return + } + switch (pageType) { case "v0": return ( diff --git a/src/components/CompatibilityV2/pages/PalmsInformation/index.tsx b/src/components/CompatibilityV2/pages/PalmsInformation/index.tsx index fcaa595..bcab491 100644 --- a/src/components/CompatibilityV2/pages/PalmsInformation/index.tsx +++ b/src/components/CompatibilityV2/pages/PalmsInformation/index.tsx @@ -29,7 +29,14 @@ function PalmsInformation() { flag: EUnleashFlags.zodiacImages }); + const { variant: relationshipStatusPagePlacement = "v0" } = useUnleash({ + flag: EUnleashFlags.v2CompatibilityRelationshipStatusPagePlacement + }); + const handleNext = () => { + if (relationshipStatusPagePlacement === "v1" || relationshipStatusPagePlacement === "v2") { + return navigate(`${routes.client.compatibilityV2RelateFollowing()}/1`); + } navigate(routes.client.compatibilityV2RelationshipStatus()); }; diff --git a/src/components/CompatibilityV2/pages/RelationshipStatus/index.tsx b/src/components/CompatibilityV2/pages/RelationshipStatus/index.tsx index 757dfa6..0f4397e 100644 --- a/src/components/CompatibilityV2/pages/RelationshipStatus/index.tsx +++ b/src/components/CompatibilityV2/pages/RelationshipStatus/index.tsx @@ -13,6 +13,7 @@ import { useMemo } from "react"; import { useSession } from "@/hooks/session/useSession"; import { IAnswersSessionCompatibilityV2 } from "@/api/resources/Session"; import { ESourceAuthorization } from "@/api/resources/User"; +import { EUnleashFlags, useUnleash } from "@/hooks/ab/unleash/useUnleash"; function RelationshipStatus() { const { translate } = useTranslations(ELocalesPlacement.CompatibilityV2); @@ -23,6 +24,10 @@ function RelationshipStatus() { selectors.selectCompatibilityV2Answers ); + const { variant: relationshipStatusPagePlacement = "v0" } = useUnleash({ + flag: EUnleashFlags.v2CompatibilityRelationshipStatusPagePlacement + }); + const answers: { id: IAnswersSessionCompatibilityV2["relationship_status"]; title: string; @@ -60,6 +65,15 @@ function RelationshipStatus() { }, }, ESourceAuthorization["aura.compatibility.v2"]); if (id !== relationshipStatus) await sleep(answerTimeOut); + + if (relationshipStatusPagePlacement === "v1") { + return navigate(`${routes.client.compatibilityV2Gender()}?noRedirectAB=true`); + } + + if (relationshipStatusPagePlacement === "v2") { + return navigate(routes.client.compatibilityV2Birthdate()); + } + navigate(`${routes.client.compatibilityV2RelateFollowing()}/1`); }; diff --git a/src/components/CompatibilityV2/pages/TryApp/index.tsx b/src/components/CompatibilityV2/pages/TryApp/index.tsx index 1d84fa6..1047998 100644 --- a/src/components/CompatibilityV2/pages/TryApp/index.tsx +++ b/src/components/CompatibilityV2/pages/TryApp/index.tsx @@ -35,6 +35,7 @@ function TryApp() { const downloadApp = async () => { metricService.reachGoal(EGoals.DOWNLOAD_APP, [EMetrics.YANDEX]); + metricService.reachGoal(EGoals.BLACK_BUTTON, [EMetrics.FACEBOOK]); await copyToClipboard(code); // TODO window.location.href = diff --git a/src/components/CompatibilityV3/pages/CodeInstruction/index.tsx b/src/components/CompatibilityV3/pages/CodeInstruction/index.tsx index 1b04624..ff3d5c5 100644 --- a/src/components/CompatibilityV3/pages/CodeInstruction/index.tsx +++ b/src/components/CompatibilityV3/pages/CodeInstruction/index.tsx @@ -14,6 +14,7 @@ function CodeInstruction() { const downloadApp = async () => { metricService.reachGoal(EGoals.DOWNLOAD_APP, [EMetrics.YANDEX]); + metricService.reachGoal(EGoals.BLACK_BUTTON, [EMetrics.FACEBOOK]); await copyToClipboard(code); // TODO window.location.href = diff --git a/src/components/CompatibilityV3/pages/TryApp/index.tsx b/src/components/CompatibilityV3/pages/TryApp/index.tsx index 4460b82..146a9df 100644 --- a/src/components/CompatibilityV3/pages/TryApp/index.tsx +++ b/src/components/CompatibilityV3/pages/TryApp/index.tsx @@ -44,6 +44,7 @@ function TryApp() { const downloadApp = async () => { metricService.reachGoal(EGoals.DOWNLOAD_APP, [EMetrics.YANDEX]); + metricService.reachGoal(EGoals.BLACK_BUTTON, [EMetrics.FACEBOOK]); await copyToClipboard(code); // TODO window.location.href = diff --git a/src/components/CompatibilityV4/components/Address/index.tsx b/src/components/CompatibilityV4/components/Address/index.tsx index 4e16e5b..1b52445 100644 --- a/src/components/CompatibilityV4/components/Address/index.tsx +++ b/src/components/CompatibilityV4/components/Address/index.tsx @@ -1,10 +1,21 @@ import styles from "./styles.module.scss"; -function Address() { +interface IAddressProps { + version?: "v1" | "v2" +} + +function Address({ + version = "v1" +}: IAddressProps) { return (

- 2025, Wit Apps LLC,
- 2108 N ST STE 5446 SACRAMENTO, CA 95816, US + {version === "v1" && <> + 2025, Wit Apps LLC,
+ 2108 N ST STE 5446 SACRAMENTO, CA 95816, US + } + {version === "v2" && <> + 2025, Wit Apps LLC, US + }

); } diff --git a/src/components/CompatibilityV4/components/Footer/index.tsx b/src/components/CompatibilityV4/components/Footer/index.tsx index 16452a1..4083e23 100644 --- a/src/components/CompatibilityV4/components/Footer/index.tsx +++ b/src/components/CompatibilityV4/components/Footer/index.tsx @@ -4,7 +4,15 @@ import Address from "../Address"; import { useTranslations } from "@/hooks/translations"; import { ELocalesPlacement } from "@/locales"; -function Footer() { +interface IFooterProps { + version?: "v1" | "v2" + addressVersion?: "v1" | "v2" +} + +function Footer({ + version = "v1", + addressVersion = "v1" +}: IFooterProps) { const { translate } = useTranslations(ELocalesPlacement.CompatibilityV4); return ( @@ -13,15 +21,15 @@ function Footer() {

{translate("/trial-payment.footer.text1")}

-
+
); } diff --git a/src/components/CompatibilityV4/pages/CodeInstruction/index.tsx b/src/components/CompatibilityV4/pages/CodeInstruction/index.tsx index 5c601bd..e3304af 100644 --- a/src/components/CompatibilityV4/pages/CodeInstruction/index.tsx +++ b/src/components/CompatibilityV4/pages/CodeInstruction/index.tsx @@ -14,6 +14,7 @@ function CodeInstruction() { const downloadApp = async () => { metricService.reachGoal(EGoals.DOWNLOAD_APP, [EMetrics.YANDEX]); + metricService.reachGoal(EGoals.BLACK_BUTTON, [EMetrics.FACEBOOK]); await copyToClipboard(code); // TODO window.location.href = diff --git a/src/components/CompatibilityV4/pages/TrialPayment/index.tsx b/src/components/CompatibilityV4/pages/TrialPayment/index.tsx index 57f1a6e..53ed681 100644 --- a/src/components/CompatibilityV4/pages/TrialPayment/index.tsx +++ b/src/components/CompatibilityV4/pages/TrialPayment/index.tsx @@ -83,43 +83,43 @@ function TrialPayment() { - {(relationshipStatus === "single" || !partnerBirthdate) && -

- {translate("/trial-payment.information-description-single", { - color: + {(relationshipStatus === "single" || !partnerBirthdate) && +

+ {translate("/trial-payment.information-description-single", { + color: {translate("/trial-payment.information-description-single-color", { - zodiacSign: zodiacSign?.toLowerCase().charAt(0).toUpperCase() + zodiacSign?.toLowerCase().slice(1), - birthdate: formatDateToLocale(birthdate, language), + zodiacSign: zodiacSign?.toLowerCase().charAt(0).toUpperCase() + zodiacSign?.toLowerCase().slice(1), + birthdate: formatDateToLocale(birthdate, language), })} , - eventDescription: dateEvent ? - {translate("/trial-payment.information-description-single-event-description", { - dateEvent: formatDateToLocale(dateEvent, language), - })} - : "", - br:
- })} -

- } - {relationshipStatus !== "single" && partnerBirthdate && -

- {translate("/trial-payment.information-description-with-partner", { - color: + eventDescription: dateEvent ? + {translate("/trial-payment.information-description-single-event-description", { + dateEvent: formatDateToLocale(dateEvent, language), + })} + : "", + br:
+ })} +

+ } + {relationshipStatus !== "single" && partnerBirthdate && +

+ {translate("/trial-payment.information-description-with-partner", { + color: {translate("/trial-payment.information-description-with-partner-color", { - zodiacSign: zodiacSign?.toLowerCase().charAt(0).toUpperCase() + zodiacSign?.toLowerCase().slice(1), - partnerZodiacSign: partnerZodiacSign?.toLowerCase().charAt(0).toUpperCase() + partnerZodiacSign?.toLowerCase().slice(1), - birthdate: formatDateToLocale(birthdate, language), - partnerBirthdate: formatDateToLocale(partnerBirthdate, language), + zodiacSign: zodiacSign?.toLowerCase().charAt(0).toUpperCase() + zodiacSign?.toLowerCase().slice(1), + partnerZodiacSign: partnerZodiacSign?.toLowerCase().charAt(0).toUpperCase() + partnerZodiacSign?.toLowerCase().slice(1), + birthdate: formatDateToLocale(birthdate, language), + partnerBirthdate: formatDateToLocale(partnerBirthdate, language), })} , - eventDescription: dateEvent ? - {translate("/trial-payment.information-description-with-partner-event-description", { - dateEvent: formatDateToLocale(dateEvent, language), - })} - : "", - })} -

- } + eventDescription: dateEvent ? + {translate("/trial-payment.information-description-with-partner-event-description", { + dateEvent: formatDateToLocale(dateEvent, language), + })} + : "", + })} +

+ } {/*{relationshipStatus !== "single" && partnerBirthdate &&*/} diff --git a/src/components/CompatibilityV4/pages/TryApp/components/YourReading/index.tsx b/src/components/CompatibilityV4/pages/TryApp/components/YourReading/index.tsx index 1ac2d5b..7ea2e7b 100644 --- a/src/components/CompatibilityV4/pages/TryApp/components/YourReading/index.tsx +++ b/src/components/CompatibilityV4/pages/TryApp/components/YourReading/index.tsx @@ -11,6 +11,13 @@ interface YourReadingProps { relationshipStatus: string; partnerGender?: string; partnerZodiacSign?: string; + isDescriptionVisible?: boolean; + pointsClassName?: string; + pointsLength?: number; + titleClassName?: string; + subtitleClassName?: string; + blurPointsIndex?: number; + pointsTranslateVersion?: string; } function YourReading({ @@ -18,16 +25,27 @@ function YourReading({ zodiacSign, relationshipStatus, partnerGender, - partnerZodiacSign + partnerZodiacSign, + isDescriptionVisible = true, + pointsClassName = "", + pointsLength = 8, + titleClassName = "", + subtitleClassName = "", + blurPointsIndex = 3, + pointsTranslateVersion = "v1" }: YourReadingProps) { const { translate } = useTranslations(ELocalesPlacement.CompatibilityV4); const { flags, ready } = useMetricABFlags(); - const version = flags?.yourReading?.[0] ?? "v1"; + let version = flags?.yourReading?.[0] ?? "v1"; + if (pointsTranslateVersion) { + version = pointsTranslateVersion; + } + return (
{translate("/try-app.your-reading.title")} @@ -41,21 +59,21 @@ function YourReading({ classNameContainer={styles.zodiacImages} /> <Title - className={styles.subtitle} + className={`${styles.subtitle} ${subtitleClassName}`} variant="h4" > {translate("/try-app.your-reading.subtitle")} -
    - {ready && Array.from({ length: 8 }).map((_, index) => ( -
  • 3 ? styles.point_blur : ""}`}> +
      + {ready && Array.from({ length: pointsLength }).map((_, index) => ( +
    • blurPointsIndex ? styles.point_blur : ""}`}> {translate(`/try-app.your-reading.points.${version}.point${index + 1}`)}
    • ))}
    -

    + {isDescriptionVisible &&

    {translate("/try-app.your-reading.description")} -

    +

    }
) } diff --git a/src/components/CompatibilityV4/pages/TryApp/index.tsx b/src/components/CompatibilityV4/pages/TryApp/index.tsx index 8808fbc..a044165 100644 --- a/src/components/CompatibilityV4/pages/TryApp/index.tsx +++ b/src/components/CompatibilityV4/pages/TryApp/index.tsx @@ -15,6 +15,7 @@ import { getFormattedPrice } from "@/utils/price.utils"; import { useEffect } from "react"; import { useDispatch } from "react-redux"; import { actions } from "@/store"; +import TryAppV1 from "./v1"; function TryApp() { const dispatch = useDispatch(); @@ -52,6 +53,8 @@ function TryApp() { return } + return + return ( <> diff --git a/src/components/CompatibilityV4/pages/TryApp/styles.module.scss b/src/components/CompatibilityV4/pages/TryApp/styles.module.scss index 3627033..b0adf6f 100644 --- a/src/components/CompatibilityV4/pages/TryApp/styles.module.scss +++ b/src/components/CompatibilityV4/pages/TryApp/styles.module.scss @@ -31,4 +31,212 @@ background: #F1F1F1; color: #121620; border-radius: 21px; +} + +.paywall__get-prediction { + position: sticky; + top: 0dvh; + left: 0; + background: #eff2fd; + box-shadow: 0 -3px 11px rgba(0, 0, 0, .15); + padding: 12px 24px; + transition: all 0.5s; + width: 100dvw; + max-width: 560px; + z-index: 10; + color: #000; + font-size: 15px; + font-weight: 600; + line-height: 23px; + display: flex; + align-items: center; + justify-content: center; + gap: 24px; + text-align: center; +} + +.paywall__get-prediction-timer { + border-radius: 4px; + background: initial; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + margin: 5px; + min-width: 62px; + padding: 0; +} + +.paywall__get-prediction-timer>span { + color: #000; + font-size: 23px; + font-weight: 600; + line-height: 23px; +} + +.paywall__get-prediction-button { + font-weight: 700; + font-size: 17px; + line-height: 125%; + min-height: 52px; + min-width: auto; + padding: 6px 8px; + white-space: normal; + width: 100%; + max-width: 220px; +} + +.information-title { + font-size: 16px; + font-weight: 700; + margin-bottom: 12px; + margin-top: 20px; + line-height: 130%; +} + +.information-description { + text-align: center; + font-size: 16px; + line-height: 125%; + font-weight: 400; + margin-top: 8px; + margin-bottom: 8px; + + &>span { + font-weight: 600; + // color: #146DA5; + } +} + +.reading-ready { + margin-top: 32px; +} + +.palm-reading-ready { + margin-top: 64px; +} + +.instructionPoint { + width: 100%; + font-weight: 600; + font-size: 19px; + line-height: 22.99px; + text-align: center; + margin-top: 32px; +} + +.downloadApp { + width: 100%; + max-width: 236px; + margin-top: 16px; + cursor: pointer; +} + +.notShareDescription { + font-weight: 500; + font-size: 15px; + line-height: 18.15px; + text-align: center; + max-width: 300px; + margin-top: 24px; +} + +.how-work { + margin-top: 40px; +} + +.instructionPoint2 { + font-weight: 600; + font-size: 22px; + line-height: 26.63px; + text-align: center; +} + +.pulse-button { + animation: pulseButton 1.2s infinite ease-in-out; + will-change: transform; +} + +.getPredictionInApp { + width: 100%; + max-width: 342px; + padding: 12px; + padding-left: 19px; + background-color: #000; + border-radius: 8px; + box-shadow: 2px 5px 2.5px -1px rgba(0, 0, 0, 0.2); + font-family: SF Pro Text; + font-weight: 500; + font-size: 20px; + line-height: 30px; + text-align: left; + display: flex; + align-items: center; + justify-content: flex-start; + gap: 22px; + margin-top: 36px; + margin-bottom: 62px; + + &>img { + width: 38px; + } +} + +.your-reading-subtitle { + font-size: 27px !important; + line-height: 125% !important; + margin-top: 32px !important; +} + +.your-reading-points { + margin-top: 16px !important; + + &>li { + margin-top: 16px; + } +} + +.get-my-reading-in-app { + font-weight: 700; + font-size: 17px; + line-height: 20.57px; + text-align: center; + margin-top: 16px; +} + +.why-love { + font-size: 28px; + margin: 40px 18px 20px; + font-weight: 700; + + &>span { + color: #224e90; + } +} + +.as-seen-in { + font-size: 32px; + margin-top: 50px; + + &>span { + color: #224e90; + } +} + +.partners { + width: 100%; +} + +@keyframes pulseButton { + 0% { + transform: scale(1); + } + + 50% { + transform: scale(1.05); + } + + 100% { + transform: scale(1); + } } \ No newline at end of file diff --git a/src/components/CompatibilityV4/pages/TryApp/v1/index.tsx b/src/components/CompatibilityV4/pages/TryApp/v1/index.tsx new file mode 100644 index 0000000..90816d8 --- /dev/null +++ b/src/components/CompatibilityV4/pages/TryApp/v1/index.tsx @@ -0,0 +1,187 @@ +import { useTranslations } from "@/hooks/translations"; +import styles from "../styles.module.scss"; +import Button from "@/components/CompatibilityV4/components/Button"; +import useTimer from "@/hooks/palmistry/use-timer"; +import metricService, { EGoals, EMetrics } from "@/services/metric/metricService"; +import { copyToClipboard } from "@/services/data"; +import { useSelector } from "react-redux"; +import { selectors } from "@/store"; +import { ELocalesPlacement, language } from "@/locales"; +import AppNumberOne from "@/components/CompatibilityV4/components/AppNumberOne"; +import Title from "@/components/Title"; +import ZodiacImagesWithBook from "../../TrialPayment/components/ZodiacImagesWithBook"; +import { getZodiacSignByDate } from "@/services/zodiac-sign"; +import { formatDateToLocale } from "@/locales/localFormats"; +import YourAccessCode from "../components/YourAccessCode"; +import { images } from "@/components/CompatibilityV4/data"; +import HowWork from "@/components/CompatibilityV4/components/HowWork"; +import MoneyBackGuarantee from "@/components/CompatibilityV4/components/MoneyBackGuarantee"; +import CopyCode from "../components/CopyCode"; +import YourReading from "../components/YourReading"; +import WhatIncluded from "@/components/CompatibilityV4/components/WhatIncluded"; +import Reviews from "@/components/CompatibilityV4/components/Reviews"; +import EnterCode from "../components/EnterCode"; +import { compatibilityV4Prefix } from "@/routes"; +import Footer from "@/components/CompatibilityV4/components/Footer"; + +function TryAppV1() { + const { translate } = useTranslations(ELocalesPlacement.CompatibilityV4); + const time = useTimer(); + const code = useSelector(selectors.selectAuthCode); + const { gender, birthdate, partnerGender, partnerBirthdate } = useSelector(selectors.selectQuestionnaire); + const { dateEvent } = useSelector(selectors.selectCompatibilityV4Answers); + const { relationshipStatus } = useSelector(selectors.selectCompatibilityV4Answers); + const zodiacSign = getZodiacSignByDate(birthdate); + const partnerZodiacSign = getZodiacSignByDate(partnerBirthdate); + + const downloadApp = async () => { + metricService.reachGoal(EGoals.DOWNLOAD_APP, [EMetrics.YANDEX]); + metricService.reachGoal(EGoals.BLACK_BUTTON, [EMetrics.FACEBOOK]); + await copyToClipboard(code); + // TODO + window.location.href = + "https://apps.apple.com/us/app/aura-astrology-horoscope/id1601978549"; + }; + + return ( + <> + <div className={styles["paywall__get-prediction"]}> + <div> + {translate("/try-app.offer_reserved.title")} + <span className={styles["paywall__get-prediction-timer"]}> + <span>{time}</span> + </span> + </div> + + <Button + type="button" + className={`${styles["paywall__get-prediction-button"]} ${styles["pulse-button"]}`} + onClick={downloadApp} + > + {translate("/try-app.offer_reserved.button")} + </Button> + </div> + <AppNumberOne /> + <Title className={styles["information-title"]}> + {translate("/try-app.information-title")} + + + {(relationshipStatus === "single" || !partnerBirthdate) && +

+ {translate("/try-app.information-description-single", { + color: + {translate("/try-app.information-description-single-color", { + zodiacSign: zodiacSign?.toLowerCase().charAt(0).toUpperCase() + zodiacSign?.toLowerCase().slice(1), + birthdate: formatDateToLocale(birthdate, language), + })} + , + eventDescription: dateEvent ? + {translate("/try-app.information-description-single-event-description", { + dateEvent: formatDateToLocale(dateEvent, language), + })} + : "", + br:
+ })} +

+ } + {relationshipStatus !== "single" && partnerBirthdate && +

+ {translate("/try-app.information-description-with-partner", { + color: + {translate("/try-app.information-description-with-partner-color", { + zodiacSign: zodiacSign?.toLowerCase().charAt(0).toUpperCase() + zodiacSign?.toLowerCase().slice(1), + partnerZodiacSign: partnerZodiacSign?.toLowerCase().charAt(0).toUpperCase() + partnerZodiacSign?.toLowerCase().slice(1), + birthdate: formatDateToLocale(birthdate, language), + partnerBirthdate: formatDateToLocale(partnerBirthdate, language), + })} + , + eventDescription: dateEvent ? + {translate("/try-app.information-description-with-partner-event-description", { + dateEvent: formatDateToLocale(dateEvent, language), + })} + : "", + })} +

+ } + + {translate("/try-app.reading_ready.title")} + +

{translate("/try-app.instruction_point_1")}

+ +

{translate("/try-app.instruction_point_2")}

+ Download app +

{translate("/try-app.instruction_point_3")}

+

+ {translate("/try-app.not_share_description")} +

+ + {translate("/try-app.how_work.title")} + + + + + {translate("/try-app.your_palm_reading_is_ready")} + +

{translate("/try-app.instruction_point_4")}

+ +

{translate("/try-app.instruction_point_5")}

+ + + + + {translate("/try-app.your_palm_reading_is_ready")} + +

{translate("/try-app.instruction_point_4")}

+ +

{translate("/try-app.instruction_point_5")}

+ + + {translate("/try-app.why_love", { + color: <span>{translate("/try-app.why_love_color")}</span>, + })} + + + + + + {translate("/trial-payment.as_seen_in", { + color: ( + <span> + {translate("app_name", undefined, ELocalesPlacement.V1)} + </span> + ), + })} + + Partners +