develop
This commit is contained in:
parent
37b12a69af
commit
6dd37bb284
@ -48,7 +48,7 @@
|
||||
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
||||
<link rel="preload" as="image" href="/leo.webp" fetchpriority="high" />
|
||||
<!-- <link rel="preload" as="image" href="/leo.webp" fetchpriority="high" /> -->
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" />
|
||||
|
||||
41
package-lock.json
generated
41
package-lock.json
generated
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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": "Ты когда-нибудь задумывался, почему одни отношения развиваются легко, а другие будто натянуты, как струна? Совпадение или знак судьбы? Руки рассказывают больше, чем вы думаете. Линии на вашей ладони — это карта ваших отношений. <color> в вашей жизни есть скрытые знаки, которые вы ещё не заметили. <eventDescription><br>Получите детальный анализ совместимости по хиромантии и откройте ответы, которые уже написаны в вашей судьбе.",
|
||||
"information-description-single-color": "<zodiacSign> (<birthdate>)",
|
||||
"information-description-single-event-description": "Ваша дата <dateEvent> могла стать поворотной точкой или скрытым сигналом.",
|
||||
"information-description-with-partner": "Ты когда-нибудь задумывался, почему одни отношения развиваются легко, а другие будто натянуты, как струна? Совпадение или знак судьбы? <color> — два знака, созданные для глубины, но какие тайны скрывает ваш союз? <eventDescription> Получите детальный анализ совместимости и откройте ответы, которые уже написаны в вашей судьбе.",
|
||||
"information-description-with-partner-color": "<zodiacSign> (<birthdate>) + <partnerZodiacSign> (<partnerBirthdate>)",
|
||||
"information-description-with-partner-event-description": "Ваша дата <dateEvent> могла стать поворотной точкой или скрытым сигналом."
|
||||
},
|
||||
"/find-your-happiness": {
|
||||
"title": "Gain Clarity and Confidence in Life",
|
||||
|
||||
52
public/locales/profile/en/male_en.json
Normal file
52
public/locales/profile/en/male_en.json
Normal file
@ -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> credits left",
|
||||
"description": "You can use them to chat with any Expert on the platform."
|
||||
},
|
||||
"any_questions": "Any questions? <link>",
|
||||
"any_questions_link": "Contact us",
|
||||
"subscription_update": "<bold><br>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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -126,7 +126,25 @@
|
||||
"description": "You will be charged only <priceForDays>. \n<emailReminder> 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 <price> charge"
|
||||
"price_information": "A <price> 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",
|
||||
|
||||
@ -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 });
|
||||
};
|
||||
|
||||
@ -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 | APNG>(Error);
|
||||
setLeoApng
|
||||
useScrollToTop({ scrollBehavior: "auto" });
|
||||
// const [
|
||||
// padLockApng,
|
||||
// setPadLockApng,
|
||||
// ] = useState<Error | APNG>(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<Asset[]>(assetsData);
|
||||
const { data } = useApiCall<Asset[]>(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 {
|
||||
<CookieYesController isDelete={subscriptionStatus === "subscribed"} />
|
||||
}
|
||||
>
|
||||
<Route
|
||||
path={`${profilePrefix}/*`}
|
||||
element={<ProfileRoutes />}
|
||||
/>
|
||||
<Route
|
||||
path={`${anonymousPrefix}/*`}
|
||||
element={<AnonymousRoutes />}
|
||||
@ -437,66 +449,70 @@ function App(): JSX.Element {
|
||||
/>
|
||||
</Route>
|
||||
<Route element={<PrivateSubscriptionOutlet />}>
|
||||
<Route path={routes.client.home()} element={<HomePage />} />
|
||||
<Route
|
||||
path={routes.client.compatibility()}
|
||||
element={<CompatibilityPage />}
|
||||
/>
|
||||
<Route
|
||||
path={routes.client.compatibilityResult()}
|
||||
element={<CompatResultPage />}
|
||||
/>
|
||||
<Route
|
||||
path={routes.client.breath()}
|
||||
element={<BreathPage leoApng={leoApng} />}
|
||||
/>
|
||||
<Route
|
||||
path={routes.client.breathResult()}
|
||||
element={<UserCallbacksPage />}
|
||||
/>
|
||||
<Route
|
||||
path={routes.client.wallpaper()}
|
||||
element={<WallpaperPage />}
|
||||
/>
|
||||
<Route path={routes.client.advisors()} element={<Advisors />} />
|
||||
<Route path={routes.client.advisors()}>
|
||||
<Route path=":id" element={<AdvisorChatPage />} />
|
||||
element={<Layout />}
|
||||
>
|
||||
<Route path={routes.client.home()} element={<HomePage />} />
|
||||
<Route
|
||||
path={routes.client.compatibility()}
|
||||
element={<CompatibilityPage />}
|
||||
/>
|
||||
<Route
|
||||
path={routes.client.compatibilityResult()}
|
||||
element={<CompatResultPage />}
|
||||
/>
|
||||
<Route
|
||||
path={routes.client.breath()}
|
||||
element={<BreathPage leoApng={leoApng} />}
|
||||
/>
|
||||
<Route
|
||||
path={routes.client.breathResult()}
|
||||
element={<UserCallbacksPage />}
|
||||
/>
|
||||
<Route
|
||||
path={routes.client.wallpaper()}
|
||||
element={<WallpaperPage />}
|
||||
/>
|
||||
<Route path={routes.client.advisors()} element={<Advisors />} />
|
||||
<Route path={routes.client.advisors()}>
|
||||
<Route path=":id" element={<AdvisorChatPage />} />
|
||||
</Route>
|
||||
<Route
|
||||
path={routes.client.magicBall()}
|
||||
element={<MagicBallPage />}
|
||||
/>
|
||||
<Route
|
||||
path={routes.client.horoscopeBestiesResult()}
|
||||
element={<BestiesHoroscopeResult />}
|
||||
/>
|
||||
<Route
|
||||
path={routes.client.predictionMoonResult()}
|
||||
element={<PredictionMoonResult />}
|
||||
/>
|
||||
<Route
|
||||
path={routes.client.myHoroscopeResult()}
|
||||
element={<MyHoroscopeResult />}
|
||||
/>
|
||||
<Route
|
||||
path={routes.client.thermalResult()}
|
||||
element={<ThermalResult />}
|
||||
/>
|
||||
<Route
|
||||
path={routes.client.moonPhaseTracker()}
|
||||
element={<MoonPhaseTrackerResult />}
|
||||
/>
|
||||
<Route
|
||||
path={routes.client.energyVampirismResult()}
|
||||
element={<EnergyVampirismResult />}
|
||||
/>
|
||||
<Route
|
||||
path={routes.client.nameHoroscopeResult()}
|
||||
element={<NameHoroscopeResult />}
|
||||
/>
|
||||
</Route>
|
||||
<Route
|
||||
path={routes.client.magicBall()}
|
||||
element={<MagicBallPage />}
|
||||
/>
|
||||
<Route
|
||||
path={routes.client.horoscopeBestiesResult()}
|
||||
element={<BestiesHoroscopeResult />}
|
||||
/>
|
||||
<Route
|
||||
path={routes.client.predictionMoonResult()}
|
||||
element={<PredictionMoonResult />}
|
||||
/>
|
||||
<Route
|
||||
path={routes.client.myHoroscopeResult()}
|
||||
element={<MyHoroscopeResult />}
|
||||
/>
|
||||
<Route
|
||||
path={routes.client.thermalResult()}
|
||||
element={<ThermalResult />}
|
||||
/>
|
||||
<Route
|
||||
path={routes.client.moonPhaseTracker()}
|
||||
element={<MoonPhaseTrackerResult />}
|
||||
/>
|
||||
<Route
|
||||
path={routes.client.energyVampirismResult()}
|
||||
element={<EnergyVampirismResult />}
|
||||
/>
|
||||
<Route
|
||||
path={routes.client.nameHoroscopeResult()}
|
||||
element={<NameHoroscopeResult />}
|
||||
/>
|
||||
</Route>
|
||||
</Route>
|
||||
|
||||
|
||||
{/* <Route path="*" element={<ABDesignV1Routes />} /> */}
|
||||
|
||||
<Route path="*" element={<Navigate to={getRouteBy(subscriptionStatus)} />} />
|
||||
@ -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<boolean>(false);
|
||||
// const homeConfig = useSelector(selectors.selectHome);
|
||||
// const showNavbarFooter = homeConfig.isShowNavbar;
|
||||
// const mainRef = useRef<HTMLDivElement>(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<boolean>(false);
|
||||
// const homeConfig = useSelector(selectors.selectHome);
|
||||
// const showNavbarFooter = homeConfig.isShowNavbar;
|
||||
const showNavbarFooter = true;
|
||||
const mainRef = useRef<HTMLDivElement>(null);
|
||||
useSchemeColorByElement(mainRef.current, "section.page, .page, section", [
|
||||
location,
|
||||
]);
|
||||
|
||||
// const birthdate = useSelector(selectors.selectBirthdate);
|
||||
// const dataItems = useMemo(() => [birthdate], [birthdate]);
|
||||
// const [isShowFullDataModal, setIsShowFullDataModal] =
|
||||
// useState<boolean>(false);
|
||||
const birthdate = useSelector(selectors.selectBirthdate);
|
||||
const dataItems = useMemo(() => [birthdate], [birthdate]);
|
||||
const [isShowFullDataModal, setIsShowFullDataModal] =
|
||||
useState<boolean>(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 (
|
||||
// <div className="container">
|
||||
// {showHeader ? (
|
||||
// <Header
|
||||
// openMenu={() => setIsMenuOpen(true)}
|
||||
// />
|
||||
// ) : null}
|
||||
// {isRouteFullDataModal && (
|
||||
// <Modal open={isShowFullDataModal} isCloseButtonVisible={false} onClose={() => { }}>
|
||||
// <FullDataModal onClose={onCloseFullDataModal} />
|
||||
// </Modal>
|
||||
// )}
|
||||
// <main className="content" ref={mainRef}>
|
||||
// <Outlet />
|
||||
// </main>
|
||||
// {showFooter ? <Footer color={showNavbar ? "black" : "white"} /> : null}
|
||||
// {showNavbar ? (
|
||||
// <Navbar isOpen={isMenuOpen} closeMenu={() => setIsMenuOpen(false)} />
|
||||
// ) : null}
|
||||
// {showNavbarFooter && hasNavbarFooter(location.pathname) ? (
|
||||
// <NavbarFooter items={navbarItems} />
|
||||
// ) : null}
|
||||
// </div>
|
||||
// );
|
||||
// }
|
||||
return (
|
||||
<div className="container">
|
||||
{showHeader ? (
|
||||
<Header
|
||||
openMenu={() => setIsMenuOpen(true)}
|
||||
/>
|
||||
) : null}
|
||||
{isRouteFullDataModal && (
|
||||
<Modal open={isShowFullDataModal} isCloseButtonVisible={false} onClose={() => { }}>
|
||||
<FullDataModal onClose={onCloseFullDataModal} />
|
||||
</Modal>
|
||||
)}
|
||||
<main className="content" ref={mainRef}>
|
||||
<Outlet />
|
||||
</main>
|
||||
{showFooter ? <Footer color={showNavbar ? "black" : "white"} /> : null}
|
||||
{showNavbar ? (
|
||||
<Navbar isOpen={isMenuOpen} closeMenu={() => setIsMenuOpen(false)} />
|
||||
) : null}
|
||||
{showNavbarFooter && hasNavbarFooter(location.pathname) ? (
|
||||
<NavbarFooter items={navbarItems} />
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// enum EIsAuthPageType {
|
||||
// private,
|
||||
@ -1275,18 +1292,18 @@ function PrivateSubscriptionOutlet(): JSX.Element {
|
||||
);
|
||||
}
|
||||
|
||||
// function getIsShowFullDataModal(dataItems: Array<unknown> = []): boolean {
|
||||
// let hasNoDataItem = false;
|
||||
function getIsShowFullDataModal(dataItems: Array<unknown> = []): 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();
|
||||
|
||||
@ -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<HTMLDivElement>({});
|
||||
@ -49,7 +87,9 @@ function CameraModal({
|
||||
// const isLandscape = height <= width;
|
||||
// const ratio = isLandscape ? width / height : height / width;
|
||||
const cameraRef = useRef<Webcam>(null);
|
||||
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const handLandmarkerRef = useRef<HandLandmarker | null>(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 <ModalOverlay
|
||||
type={ModalOverlayType.Dark}
|
||||
className={`${styles.overlay} ${className}`}
|
||||
@ -126,37 +326,57 @@ function CameraModal({
|
||||
modalClassName={styles.modal}
|
||||
>
|
||||
<div
|
||||
// ref={containerRef}
|
||||
ref={containerRef}
|
||||
className={styles.cameraContainer}>
|
||||
{/* {width} {height} */}
|
||||
<svg
|
||||
className={styles.handIcon} width="226" height="435" viewBox="0 0 226 435" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M44.9888 101.991C31.3554 80.777 16.4145 88.555 8.56162 99.382C3.99661 105.676 3.45507 113.764 3.41227 121.539C3.06124 185.302 2.66386 298.933 3.47936 322.748C4.34753 348.102 22.1089 380.082 30.881 392.903C47.521 412.857 95.0172 446.752 151.882 422.699C208.748 398.646 222.91 337.033 223 307.148V181.957V181.957C222.663 170.327 216.966 158.307 205.412 156.937C201.087 156.425 197.042 156.699 193.716 157.409C191.581 157.864 189.641 158.915 187.848 160.159V160.159C181.442 164.605 177.583 171.876 177.493 179.673L176.571 259.523L177.251 200.682L176.705 74.0516C176.618 53.7907 170.942 24.5591 151.038 28.3435C144.116 29.6596 138.353 35.2118 135.333 42.942M44.9888 101.991L46.074 202.212M44.9888 101.991L45.9236 62.9801C46.2026 51.3361 47.4303 38.0466 57.4751 32.1503C67.0778 26.5135 80.2774 27.7506 90.8392 42.942M90.8392 42.942V202.212M90.8392 42.942V38.4912C90.8392 26.0387 91.7293 11.455 102.608 5.39458C108.902 1.88803 116.617 1.86364 124.232 7.80872C130.541 12.7337 133.624 20.5445 134.497 28.5001C134.994 33.0286 135.333 38.1796 135.333 42.942M135.333 42.942V202.212" stroke="white" strokeWidth="6" strokeLinecap="round" />
|
||||
</svg>
|
||||
|
||||
{isCameraVisible && <Webcam
|
||||
key={reinitializeKey}
|
||||
ref={cameraRef}
|
||||
className={styles.camera}
|
||||
muted
|
||||
// width={width}
|
||||
// height={height}
|
||||
// disablePictureInPicture={true}
|
||||
screenshotQuality={2}
|
||||
videoConstraints={{
|
||||
facingMode: { ideal: "environment" },
|
||||
// width,
|
||||
// height,
|
||||
// aspectRatio: ratio,
|
||||
}}
|
||||
onUserMedia={onUserMedia}
|
||||
onUserMediaError={(error) => {
|
||||
setIsVideoReady(false);
|
||||
setIsTorchAvailable(false);
|
||||
console.error(error);
|
||||
onError(error);
|
||||
}}
|
||||
/>}
|
||||
{isShowHand && (!isHandDetected || !isPalmFacingCamera || !isMediaPipe) && (
|
||||
<svg
|
||||
className={styles.handIcon}
|
||||
width="226"
|
||||
height="435"
|
||||
viewBox="0 0 226 435"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
style={{
|
||||
transform: `translate(-50%) scale(${handName === "Right" ? 1 : -1}, 1)`,
|
||||
}}
|
||||
>
|
||||
<path d="M44.9888 101.991C31.3554 80.777 16.4145 88.555 8.56162 99.382C3.99661 105.676 3.45507 113.764 3.41227 121.539C3.06124 185.302 2.66386 298.933 3.47936 322.748C4.34753 348.102 22.1089 380.082 30.881 392.903C47.521 412.857 95.0172 446.752 151.882 422.699C208.748 398.646 222.91 337.033 223 307.148V181.957V181.957C222.663 170.327 216.966 158.307 205.412 156.937C201.087 156.425 197.042 156.699 193.716 157.409C191.581 157.864 189.641 158.915 187.848 160.159V160.159C181.442 164.605 177.583 171.876 177.493 179.673L176.571 259.523L177.251 200.682L176.705 74.0516C176.618 53.7907 170.942 24.5591 151.038 28.3435C144.116 29.6596 138.353 35.2118 135.333 42.942M44.9888 101.991L46.074 202.212M44.9888 101.991L45.9236 62.9801C46.2026 51.3361 47.4303 38.0466 57.4751 32.1503C67.0778 26.5135 80.2774 27.7506 90.8392 42.942M90.8392 42.942V202.212M90.8392 42.942V38.4912C90.8392 26.0387 91.7293 11.455 102.608 5.39458C108.902 1.88803 116.617 1.86364 124.232 7.80872C130.541 12.7337 133.624 20.5445 134.497 28.5001C134.994 33.0286 135.333 38.1796 135.333 42.942M135.333 42.942V202.212" stroke="white" strokeWidth="6" strokeLinecap="round" />
|
||||
</svg>
|
||||
)}
|
||||
{isCameraVisible && (
|
||||
<>
|
||||
<Webcam
|
||||
key={reinitializeKey}
|
||||
ref={cameraRef}
|
||||
className={styles.camera}
|
||||
muted
|
||||
// width={width}
|
||||
// height={height}
|
||||
// disablePictureInPicture={true}
|
||||
screenshotQuality={2}
|
||||
videoConstraints={{
|
||||
facingMode: { ideal: "environment" },
|
||||
// width,
|
||||
// height,
|
||||
// aspectRatio: ratio,
|
||||
}}
|
||||
onUserMedia={onUserMedia}
|
||||
onUserMediaError={(error) => {
|
||||
setIsVideoReady(false);
|
||||
setIsTorchAvailable(false);
|
||||
console.error(error);
|
||||
onError(error);
|
||||
}}
|
||||
/>
|
||||
{isMediaPipe && <canvas
|
||||
ref={canvasRef}
|
||||
className={styles.overlayCanvas}
|
||||
width={cameraRef?.current?.video?.videoWidth || 0}
|
||||
height={cameraRef?.current?.video?.videoHeight || 0}
|
||||
style={getStyleCanvas()}
|
||||
/>}
|
||||
</>
|
||||
)}
|
||||
<div
|
||||
className={styles.shutterButton}
|
||||
onClick={onClickTakePhoto}
|
||||
|
||||
@ -29,6 +29,7 @@
|
||||
width: 100%;
|
||||
height: 100dvh;
|
||||
object-fit: cover;
|
||||
object-position: center;
|
||||
}
|
||||
|
||||
.shutterButton {
|
||||
@ -102,4 +103,9 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.overlayCanvas {
|
||||
position: absolute;
|
||||
pointer-events: none;
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@ -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<EToastVisible | null>(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 && <CameraModal
|
||||
<CameraModal
|
||||
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}
|
||||
onVideoReady={() => setIsVideoReady(true)}
|
||||
className={(isLoading || !isMediaPipeInit) ? styles.hideCameraModal : ""}
|
||||
onIsFacingCameraChange={(isFacingCamera) => {
|
||||
if (!isFacingCamera) {
|
||||
setToastVisible(EToastVisible.turn_hand)
|
||||
} else {
|
||||
setToastVisible(null)
|
||||
}
|
||||
}}
|
||||
// reinitializeKey={reinitializeCameraCount}
|
||||
/>}
|
||||
/>
|
||||
|
||||
|
||||
{/* Лоадер */}
|
||||
{isLoading && (
|
||||
{(isLoading || !isMediaPipeInit) && (
|
||||
<Loader className={styles.loader} color={LoaderColor.Black} />
|
||||
)}
|
||||
|
||||
@ -409,6 +427,18 @@ function AndroidCamera() {
|
||||
</div>
|
||||
</Toast>
|
||||
)}
|
||||
|
||||
{/* Тост если нужно повернуть руку */}
|
||||
{toastVisible === EToastVisible.turn_hand && (
|
||||
<Toast classNameContainer={styles["toast-container"]} variant="error">
|
||||
<div
|
||||
className={styles["toast-content"]}
|
||||
style={{ flexDirection: "column" }}
|
||||
>
|
||||
<span>{translate("/camera.turn_hand")}</span>
|
||||
</div>
|
||||
</Toast>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@ -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<EToastVisible | null>(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) && (
|
||||
<Loader className={styles.loader} color={LoaderColor.Black} />
|
||||
)}
|
||||
|
||||
@ -347,6 +365,18 @@ function IphoneCamera() {
|
||||
</div>
|
||||
</Toast>
|
||||
)}
|
||||
|
||||
{/* Тост если нужно повернуть руку */}
|
||||
{toastVisible === EToastVisible.turn_hand && (
|
||||
<Toast classNameContainer={styles["toast-container"]} variant="error">
|
||||
<div
|
||||
className={styles["toast-content"]}
|
||||
style={{ flexDirection: "column" }}
|
||||
>
|
||||
<span>{translate("/camera.turn_hand")}</span>
|
||||
</div>
|
||||
</Toast>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@ -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 <Loader color={LoaderColor.Black} />;
|
||||
|
||||
if (relationshipStatusPagePlacement === "v1" && !noRedirectAB) {
|
||||
return <Navigate to={routes.client.compatibilityV2RelationshipStatus()} />
|
||||
}
|
||||
|
||||
switch (pageType) {
|
||||
case "v0":
|
||||
return (
|
||||
|
||||
@ -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());
|
||||
};
|
||||
|
||||
|
||||
@ -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`);
|
||||
};
|
||||
|
||||
|
||||
@ -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 =
|
||||
|
||||
@ -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 =
|
||||
|
||||
@ -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 =
|
||||
|
||||
@ -1,10 +1,21 @@
|
||||
import styles from "./styles.module.scss";
|
||||
|
||||
function Address() {
|
||||
interface IAddressProps {
|
||||
version?: "v1" | "v2"
|
||||
}
|
||||
|
||||
function Address({
|
||||
version = "v1"
|
||||
}: IAddressProps) {
|
||||
return (
|
||||
<p className={styles.address}>
|
||||
2025, Wit Apps LLC, <br />
|
||||
2108 N ST STE 5446 SACRAMENTO, CA 95816, US
|
||||
{version === "v1" && <>
|
||||
2025, Wit Apps LLC, <br />
|
||||
2108 N ST STE 5446 SACRAMENTO, CA 95816, US
|
||||
</>}
|
||||
{version === "v2" && <>
|
||||
2025, Wit Apps LLC, US
|
||||
</>}
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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() {
|
||||
<p className={styles.text}>{translate("/trial-payment.footer.text1")}</p>
|
||||
<div className={styles.links}>
|
||||
<a href="https://witapps.us" target="_blank" className={styles.link}>
|
||||
<img src={`${compatibilityV4Prefix}/headphones.svg`} alt="Headphones" />
|
||||
{version === "v1" && <img src={`${compatibilityV4Prefix}/headphones.svg`} alt="Headphones" />}
|
||||
{translate("/trial-payment.footer.text2")}
|
||||
</a>
|
||||
<a href="https://witapps.us" target="_blank" className={styles.link}>
|
||||
<img src={`${compatibilityV4Prefix}/question.svg`} alt="Headphones" />
|
||||
{version === "v1" && <img src={`${compatibilityV4Prefix}/question.svg`} alt="Headphones" />}
|
||||
{translate("/trial-payment.footer.text3")}
|
||||
</a>
|
||||
</div>
|
||||
<Address />
|
||||
<Address version={addressVersion} />
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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 =
|
||||
|
||||
@ -83,43 +83,43 @@ function TrialPayment() {
|
||||
</Title>
|
||||
<ZodiacImagesWithBook />
|
||||
|
||||
{(relationshipStatus === "single" || !partnerBirthdate) &&
|
||||
<p className={styles["information-description"]}>
|
||||
{translate("/trial-payment.information-description-single", {
|
||||
color: <span>
|
||||
{(relationshipStatus === "single" || !partnerBirthdate) &&
|
||||
<p className={styles["information-description"]}>
|
||||
{translate("/trial-payment.information-description-single", {
|
||||
color: <span>
|
||||
{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),
|
||||
})}
|
||||
</span>,
|
||||
eventDescription: dateEvent ? <b>
|
||||
{translate("/trial-payment.information-description-single-event-description", {
|
||||
dateEvent: formatDateToLocale(dateEvent, language),
|
||||
})}
|
||||
</b> : "",
|
||||
br: <br />
|
||||
})}
|
||||
</p>
|
||||
}
|
||||
{relationshipStatus !== "single" && partnerBirthdate &&
|
||||
<p className={styles["information-description"]}>
|
||||
{translate("/trial-payment.information-description-with-partner", {
|
||||
color: <span>
|
||||
eventDescription: dateEvent ? <b>
|
||||
{translate("/trial-payment.information-description-single-event-description", {
|
||||
dateEvent: formatDateToLocale(dateEvent, language),
|
||||
})}
|
||||
</b> : "",
|
||||
br: <br />
|
||||
})}
|
||||
</p>
|
||||
}
|
||||
{relationshipStatus !== "single" && partnerBirthdate &&
|
||||
<p className={styles["information-description"]}>
|
||||
{translate("/trial-payment.information-description-with-partner", {
|
||||
color: <span>
|
||||
{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),
|
||||
})}
|
||||
</span>,
|
||||
eventDescription: dateEvent ? <b>
|
||||
{translate("/trial-payment.information-description-with-partner-event-description", {
|
||||
dateEvent: formatDateToLocale(dateEvent, language),
|
||||
})}
|
||||
</b> : "",
|
||||
})}
|
||||
</p>
|
||||
}
|
||||
eventDescription: dateEvent ? <b>
|
||||
{translate("/trial-payment.information-description-with-partner-event-description", {
|
||||
dateEvent: formatDateToLocale(dateEvent, language),
|
||||
})}
|
||||
</b> : "",
|
||||
})}
|
||||
</p>
|
||||
}
|
||||
|
||||
|
||||
{/*{relationshipStatus !== "single" && partnerBirthdate &&*/}
|
||||
|
||||
@ -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 (
|
||||
<div className={styles.container}>
|
||||
<Title
|
||||
className={styles.title}
|
||||
className={`${styles.title} ${titleClassName}`}
|
||||
variant="h3"
|
||||
>
|
||||
{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")}
|
||||
</Title>
|
||||
<ul className={styles.points}>
|
||||
{ready && Array.from({ length: 8 }).map((_, index) => (
|
||||
<li key={index} className={`${index > 3 ? styles.point_blur : ""}`}>
|
||||
<ul className={`${styles.points} ${pointsClassName}`}>
|
||||
{ready && Array.from({ length: pointsLength }).map((_, index) => (
|
||||
<li key={index} className={`${index > blurPointsIndex ? styles.point_blur : ""}`}>
|
||||
{translate(`/try-app.your-reading.points.${version}.point${index + 1}`)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<p className={styles.description}>
|
||||
{isDescriptionVisible && <p className={styles.description}>
|
||||
{translate("/try-app.your-reading.description")}
|
||||
</p>
|
||||
</p>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@ -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 <Loader color={LoaderColor.Black} />
|
||||
}
|
||||
|
||||
return <TryAppV1 />
|
||||
|
||||
return (
|
||||
<>
|
||||
<Title className={styles.title}>
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
187
src/components/CompatibilityV4/pages/TryApp/v1/index.tsx
Normal file
187
src/components/CompatibilityV4/pages/TryApp/v1/index.tsx
Normal file
@ -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")}
|
||||
</Title>
|
||||
<ZodiacImagesWithBook />
|
||||
{(relationshipStatus === "single" || !partnerBirthdate) &&
|
||||
<p className={styles["information-description"]}>
|
||||
{translate("/try-app.information-description-single", {
|
||||
color: <span>
|
||||
{translate("/try-app.information-description-single-color", {
|
||||
zodiacSign: zodiacSign?.toLowerCase().charAt(0).toUpperCase() + zodiacSign?.toLowerCase().slice(1),
|
||||
birthdate: formatDateToLocale(birthdate, language),
|
||||
})}
|
||||
</span>,
|
||||
eventDescription: dateEvent ? <b>
|
||||
{translate("/try-app.information-description-single-event-description", {
|
||||
dateEvent: formatDateToLocale(dateEvent, language),
|
||||
})}
|
||||
</b> : "",
|
||||
br: <br />
|
||||
})}
|
||||
</p>
|
||||
}
|
||||
{relationshipStatus !== "single" && partnerBirthdate &&
|
||||
<p className={styles["information-description"]}>
|
||||
{translate("/try-app.information-description-with-partner", {
|
||||
color: <span>
|
||||
{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),
|
||||
})}
|
||||
</span>,
|
||||
eventDescription: dateEvent ? <b>
|
||||
{translate("/try-app.information-description-with-partner-event-description", {
|
||||
dateEvent: formatDateToLocale(dateEvent, language),
|
||||
})}
|
||||
</b> : "",
|
||||
})}
|
||||
</p>
|
||||
}
|
||||
<Title className={styles["reading-ready"]}>
|
||||
{translate("/try-app.reading_ready.title")}
|
||||
</Title>
|
||||
<p className={styles.instructionPoint} style={{
|
||||
marginBottom: "16px",
|
||||
marginTop: "16px"
|
||||
}}>{translate("/try-app.instruction_point_1")}</p>
|
||||
<YourAccessCode />
|
||||
<p className={styles.instructionPoint}>{translate("/try-app.instruction_point_2")}</p>
|
||||
<img className={styles.downloadApp} src={images("download-app.png")} alt="Download app" onClick={downloadApp} />
|
||||
<p className={styles.instructionPoint}>{translate("/try-app.instruction_point_3")}</p>
|
||||
<p className={styles.notShareDescription}>
|
||||
{translate("/try-app.not_share_description")}
|
||||
</p>
|
||||
<Title className={styles["how-work"]}>
|
||||
{translate("/try-app.how_work.title")}
|
||||
</Title>
|
||||
<HowWork />
|
||||
<MoneyBackGuarantee />
|
||||
<Title className={styles["palm-reading-ready"]}>
|
||||
{translate("/try-app.your_palm_reading_is_ready")}
|
||||
</Title>
|
||||
<p className={styles.instructionPoint2}>{translate("/try-app.instruction_point_4")}</p>
|
||||
<CopyCode variant="black" />
|
||||
<p className={styles.instructionPoint2} style={{ marginTop: "24px" }}>{translate("/try-app.instruction_point_5")}</p>
|
||||
<Button className={`${styles.getPredictionInApp} ${styles["pulse-button"]}`} onClick={downloadApp}>
|
||||
<img src={images("apple-icon.png")} alt="Apple icon" />
|
||||
{translate("/try-app.get_prediction_in_app")}
|
||||
</Button>
|
||||
<YourReading
|
||||
gender={gender}
|
||||
zodiacSign={zodiacSign}
|
||||
relationshipStatus={relationshipStatus}
|
||||
partnerGender={partnerGender}
|
||||
partnerZodiacSign={partnerZodiacSign}
|
||||
isDescriptionVisible={false}
|
||||
subtitleClassName={styles["your-reading-subtitle"]}
|
||||
pointsClassName={styles["your-reading-points"]}
|
||||
blurPointsIndex={2}
|
||||
pointsLength={7}
|
||||
pointsTranslateVersion="v3"
|
||||
/>
|
||||
<WhatIncluded />
|
||||
<Title className={styles["palm-reading-ready"]}>
|
||||
{translate("/try-app.your_palm_reading_is_ready")}
|
||||
</Title>
|
||||
<p className={styles.instructionPoint2}>{translate("/try-app.instruction_point_4")}</p>
|
||||
<CopyCode variant="black" />
|
||||
<p className={styles.instructionPoint2} style={{ marginTop: "24px" }}>{translate("/try-app.instruction_point_5")}</p>
|
||||
<Button className={`${styles["get-my-reading-in-app"]} ${styles["pulse-button"]}`} onClick={downloadApp}>
|
||||
{translate("/try-app.get-my-reading-in-app")}
|
||||
</Button>
|
||||
<Title className={styles["why-love"]}>
|
||||
{translate("/try-app.why_love", {
|
||||
color: <span>{translate("/try-app.why_love_color")}</span>,
|
||||
})}
|
||||
</Title>
|
||||
<Reviews />
|
||||
<EnterCode style={{ marginTop: "32px" }} />
|
||||
<Button
|
||||
className={`${styles["get-my-reading-in-app"]} ${styles["pulse-button"]}`}
|
||||
style={{ marginTop: "28px" }}
|
||||
onClick={downloadApp}
|
||||
>
|
||||
{translate("/try-app.get-my-reading-in-app")}
|
||||
</Button>
|
||||
<Title className={styles["as-seen-in"]}>
|
||||
{translate("/trial-payment.as_seen_in", {
|
||||
color: (
|
||||
<span>
|
||||
{translate("app_name", undefined, ELocalesPlacement.V1)}
|
||||
</span>
|
||||
),
|
||||
})}
|
||||
</Title>
|
||||
<img className={styles.partners} src={`${compatibilityV4Prefix}/partners.png`} alt="Partners" />
|
||||
<Footer version="v2" addressVersion="v2" />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default TryAppV1;
|
||||
@ -166,6 +166,9 @@ function WhatAddToAnalysis() {
|
||||
if (whatAddToAnalysis.includes("potential_partner")) {
|
||||
return navigate(routes.client.compatibilityV4PotentialPartnerName());
|
||||
}
|
||||
if (whatAddToAnalysis.includes("former_partner")) {
|
||||
return navigate(routes.client.compatibilityV4FormerPartnerName());
|
||||
}
|
||||
navigate(routes.client.compatibilityV4Loading());
|
||||
};
|
||||
|
||||
|
||||
@ -10,10 +10,11 @@ const isValidEmail = (email: string) => {
|
||||
|
||||
type EmailInputProps = FormField<string> & {
|
||||
className?: string;
|
||||
readonly?: boolean;
|
||||
};
|
||||
|
||||
function EmailInput(props: EmailInputProps): JSX.Element {
|
||||
const { name, value, placeholder, onValid, onInvalid, className } = props;
|
||||
const { name, value, placeholder, onValid, onInvalid, className, readonly } = props;
|
||||
const [email, setEmail] = useState(value);
|
||||
|
||||
const handleChangeEmail = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
@ -44,6 +45,7 @@ function EmailInput(props: EmailInputProps): JSX.Element {
|
||||
value={email}
|
||||
onChange={handleChangeEmail}
|
||||
placeholder=" "
|
||||
readOnly={readonly}
|
||||
/>
|
||||
<span className={styles["input__placeholder"]}>{placeholder}</span>
|
||||
</div>
|
||||
|
||||
@ -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 =
|
||||
|
||||
@ -0,0 +1,60 @@
|
||||
import React from "react";
|
||||
import styles from "./styles.module.scss";
|
||||
import { AddressFields as AddressFieldsType } from "@/hooks/payment/nmi/useAddressFields";
|
||||
import { useTranslations } from "@/hooks/translations";
|
||||
import { ELocalesPlacement } from "@/locales";
|
||||
|
||||
interface Props {
|
||||
fields: AddressFieldsType;
|
||||
errors: Partial<Record<keyof AddressFieldsType, string>>;
|
||||
onChange: (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => void;
|
||||
countryList: { code: string; name: string }[];
|
||||
}
|
||||
|
||||
export default function AddressFields({ fields, errors, onChange, countryList }: Props) {
|
||||
const { translate } = useTranslations(ELocalesPlacement.V1);
|
||||
return (
|
||||
<div className={styles.formGroup}>
|
||||
<div className={styles.formRow}>
|
||||
<label htmlFor="country">{translate("payment_modal.address_form.labels.country")}</label>
|
||||
<select
|
||||
id="country"
|
||||
name="country"
|
||||
className={`${styles.input} ${errors.country ? styles.invalid : ""}`}
|
||||
value={fields.country}
|
||||
onChange={onChange}
|
||||
>
|
||||
<option value="">{translate("payment_modal.address_form.placeholders.country")}</option>
|
||||
{countryList.map(c => (
|
||||
<option key={c.code} value={c.code}>{c.name}</option>
|
||||
))}
|
||||
</select>
|
||||
{errors.country && <div className={styles.errorMessage}>{errors.country}</div>}
|
||||
</div>
|
||||
<div className={styles.formRow}>
|
||||
<label htmlFor="address">{translate("payment_modal.address_form.labels.address")}</label>
|
||||
<input
|
||||
id="address"
|
||||
name="address"
|
||||
className={`${styles.input} ${errors.address ? styles.invalid : ""}`}
|
||||
value={fields.address}
|
||||
onChange={onChange}
|
||||
placeholder={translate("payment_modal.address_form.placeholders.address")}
|
||||
/>
|
||||
{errors.address && <div className={styles.errorMessage}>{errors.address}</div>}
|
||||
</div>
|
||||
<div className={styles.formRow}>
|
||||
<label htmlFor="zip">{translate("payment_modal.address_form.labels.zip")}</label>
|
||||
<input
|
||||
id="zip"
|
||||
name="zip"
|
||||
className={`${styles.input} ${errors.zip ? styles.invalid : ""}`}
|
||||
value={fields.zip}
|
||||
onChange={onChange}
|
||||
placeholder={translate("payment_modal.address_form.placeholders.zip")}
|
||||
/>
|
||||
{errors.zip && <div className={styles.errorMessage}>{errors.zip}</div>}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,82 @@
|
||||
.formGroup {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
// gap: 1rem;
|
||||
max-width: 302px;
|
||||
margin-inline: auto;
|
||||
}
|
||||
|
||||
.formRow {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-bottom: 16px;
|
||||
|
||||
label {
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
font-weight: 400;
|
||||
text-align: left;
|
||||
|
||||
padding-left: 7px;
|
||||
font-size: 16px;
|
||||
max-width: 302px;
|
||||
}
|
||||
}
|
||||
|
||||
.input {
|
||||
background-color: #F0F5FF;
|
||||
border: 0.5px solid #CDCDCD;
|
||||
border-radius: 11px;
|
||||
overflow: hidden;
|
||||
transition: border-color 0.2s;
|
||||
padding: 16px;
|
||||
height: 52px;
|
||||
width: 100%;
|
||||
max-width: 302px;
|
||||
|
||||
&:focus-within {
|
||||
border-color: #3b82f6;
|
||||
}
|
||||
|
||||
&.invalid {
|
||||
border-color: #dc3545;
|
||||
}
|
||||
}
|
||||
|
||||
.fieldContainer {
|
||||
background-color: #F0F5FF;
|
||||
border: 0.5px solid #CDCDCD;
|
||||
border-radius: 11px;
|
||||
overflow: hidden;
|
||||
transition: border-color 0.2s;
|
||||
padding: 2px;
|
||||
height: 52px;
|
||||
width: 100%;
|
||||
|
||||
&:focus-within {
|
||||
border-color: #3b82f6;
|
||||
}
|
||||
|
||||
&.invalid {
|
||||
border-color: #dc3545;
|
||||
}
|
||||
}
|
||||
|
||||
.errorMessage {
|
||||
color: #dc3545;
|
||||
font-size: 12px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
@media (max-width: 280px) {
|
||||
.formGroup {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.formRow {
|
||||
&:nth-child(2) {
|
||||
margin-left: 0 !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -42,6 +42,8 @@ export default function CheckoutForm({
|
||||
});
|
||||
const isNewPaymentButton = variant === "new";
|
||||
|
||||
// const address = useAddressFields(language);
|
||||
|
||||
const {
|
||||
isLoading,
|
||||
error,
|
||||
@ -82,6 +84,7 @@ export default function CheckoutForm({
|
||||
e.preventDefault();
|
||||
setPayButtonClicked(true);
|
||||
|
||||
// const isAddressValid = address.validateAll();
|
||||
if (!isFormValid) {
|
||||
return;
|
||||
}
|
||||
@ -145,6 +148,12 @@ export default function CheckoutForm({
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{/* <AddressFields
|
||||
fields={address.fields}
|
||||
errors={address.errors}
|
||||
onChange={address.handleChange}
|
||||
countryList={address.countryList}
|
||||
/> */}
|
||||
</div>
|
||||
{isNewPaymentButton && payButtonClicked && !isFormValid &&
|
||||
<p className={styles.errorMessage} style={{ marginBottom: "16px" }}>
|
||||
|
||||
46
src/components/Profile/components/Billing/index.tsx
Normal file
46
src/components/Profile/components/Billing/index.tsx
Normal file
@ -0,0 +1,46 @@
|
||||
import styles from "./styles.module.scss"
|
||||
import Button from "@/components/CompatibilityV2/components/Button";
|
||||
import { useTranslations } from "@/hooks/translations"
|
||||
import { ELocalesPlacement } from "@/locales"
|
||||
|
||||
interface IBillingProps {
|
||||
onBilling: () => void;
|
||||
}
|
||||
|
||||
function Billing({ onBilling }: IBillingProps) {
|
||||
const { translate } = useTranslations(ELocalesPlacement.Profile);
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<Button
|
||||
className={styles.button}
|
||||
onClick={onBilling}
|
||||
>
|
||||
{translate("/profile.billing.billing_button")}
|
||||
</Button>
|
||||
<div className={styles.credits}>
|
||||
<p className={styles.creditsTitle}>
|
||||
{translate("/profile.billing.credits.title", {
|
||||
credits: String(0)
|
||||
})}
|
||||
</p>
|
||||
<p className={styles.creditsDescription}>{translate("/profile.billing.credits.description")}</p>
|
||||
</div>
|
||||
<p className={styles.anyQuestions}>
|
||||
{translate("/profile.billing.any_questions", {
|
||||
link: <a href="https://witapps.us/en#contact-us" target="_blank" rel="noopener noreferrer">
|
||||
{translate("/profile.billing.any_questions_link")}
|
||||
</a>
|
||||
})}
|
||||
</p>
|
||||
<p className={styles.subscriptionUpdate}>
|
||||
{translate("/profile.billing.subscription_update", {
|
||||
bold: <span>{translate("/profile.billing.subscription_update_bold")}</span>,
|
||||
br: <br />
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Billing
|
||||
56
src/components/Profile/components/Billing/styles.module.scss
Normal file
56
src/components/Profile/components/Billing/styles.module.scss
Normal file
@ -0,0 +1,56 @@
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.button {
|
||||
max-width: 100%;
|
||||
min-height: 60px;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.credits {
|
||||
padding: 16px;
|
||||
background-color: #275ca7;
|
||||
border-radius: 12px;
|
||||
color: #fff;
|
||||
width: 100%;
|
||||
|
||||
.creditsTitle {
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.creditsDescription {
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
margin-top: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.anyQuestions {
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
|
||||
&>a {
|
||||
color: #275ca7;
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
.subscriptionUpdate {
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
font-size: 16px;
|
||||
font-weight: 400;
|
||||
color: #778499;
|
||||
line-height: 1.25;
|
||||
|
||||
&>span {
|
||||
font-weight: 700;
|
||||
}
|
||||
}
|
||||
22
src/components/Profile/components/LogOut/index.tsx
Normal file
22
src/components/Profile/components/LogOut/index.tsx
Normal file
@ -0,0 +1,22 @@
|
||||
import styles from "./styles.module.scss"
|
||||
import Button from "@/components/CompatibilityV2/components/Button";
|
||||
|
||||
interface ILogOutProps {
|
||||
onLogout: () => void;
|
||||
}
|
||||
|
||||
function LogOut({ onLogout }: ILogOutProps) {
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<Button
|
||||
className={styles.button}
|
||||
onClick={onLogout}
|
||||
>
|
||||
Log out
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default LogOut
|
||||
12
src/components/Profile/components/LogOut/styles.module.scss
Normal file
12
src/components/Profile/components/LogOut/styles.module.scss
Normal file
@ -0,0 +1,12 @@
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.button {
|
||||
max-width: 100%;
|
||||
min-height: 60px;
|
||||
font-size: 24px;
|
||||
}
|
||||
24
src/components/Profile/components/ProfileBlock/index.tsx
Normal file
24
src/components/Profile/components/ProfileBlock/index.tsx
Normal file
@ -0,0 +1,24 @@
|
||||
import Title from "@/components/Title"
|
||||
import styles from "./styles.module.scss"
|
||||
|
||||
interface ProfileBlockProps {
|
||||
title: string
|
||||
description?: string
|
||||
children?: React.ReactNode
|
||||
}
|
||||
|
||||
function ProfileBlock({ title, description, children }: ProfileBlockProps) {
|
||||
return (
|
||||
<section className={styles.container}>
|
||||
<header className={styles.header}>
|
||||
<Title variant="h2" className={styles.title}>{title}</Title>
|
||||
{description && <p className={styles.description}>{description}</p>}
|
||||
</header>
|
||||
{!!children && <div className={styles.content}>
|
||||
{children}
|
||||
</div>}
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
export default ProfileBlock
|
||||
@ -0,0 +1,31 @@
|
||||
.container {
|
||||
width: 100%;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
|
||||
&>.title {
|
||||
font-size: 24px;
|
||||
line-height: 32px;
|
||||
text-align: left;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
&>.description {
|
||||
margin: 0;
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
text-align: left;
|
||||
font-weight: 100;
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
width: 100%;
|
||||
margin-top: 16px;
|
||||
}
|
||||
@ -0,0 +1,39 @@
|
||||
import styles from "./styles.module.scss"
|
||||
import { useSelector } from "react-redux"
|
||||
import { selectors } from "@/store"
|
||||
import EmailInput from "@/components/pages/ABDesign/v1/pages/EmailEnterPage/EmailInput"
|
||||
import NameInput from "@/components/pages/ABDesign/v1/pages/EmailEnterPage/NameInput"
|
||||
import { useTranslations } from "@/hooks/translations"
|
||||
import { ELocalesPlacement } from "@/locales"
|
||||
|
||||
function ProfileInformation() {
|
||||
const { translate } = useTranslations(ELocalesPlacement.Profile);
|
||||
const email = useSelector(selectors.selectEmail) || ""
|
||||
const name = useSelector(selectors.selectUser)?.username || ""
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<EmailInput
|
||||
name="email"
|
||||
value={email}
|
||||
placeholder={translate("/profile.profile_information.email_placeholder")}
|
||||
inputContainerClassName={styles.inputContainer}
|
||||
inputClassName={styles.input}
|
||||
onValid={() => { }}
|
||||
onInvalid={() => { }}
|
||||
readonly
|
||||
/>
|
||||
<NameInput
|
||||
value={name}
|
||||
placeholder={translate("/profile.profile_information.name_placeholder")}
|
||||
inputContainerClassName={styles.inputContainer}
|
||||
inputClassName={styles.input}
|
||||
onValid={() => { }}
|
||||
onInvalid={() => { }}
|
||||
readonly
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ProfileInformation
|
||||
@ -0,0 +1,16 @@
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.input {
|
||||
background-color: #f3f3f3;
|
||||
min-height: 60px;
|
||||
}
|
||||
|
||||
.inputContainer {
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
}
|
||||
23
src/components/Profile/components/Table/index.tsx
Normal file
23
src/components/Profile/components/Table/index.tsx
Normal file
@ -0,0 +1,23 @@
|
||||
import styles from "./styles.module.scss"
|
||||
|
||||
interface ITableProps {
|
||||
data: (string | React.ReactNode)[][];
|
||||
}
|
||||
|
||||
function Table({ data }: ITableProps) {
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
{data.map((row, index) => (
|
||||
<div key={index} className={styles.row}>
|
||||
{row.map((cell, cellIndex) => (
|
||||
<div key={cellIndex} className={styles.cell}>
|
||||
{cell}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Table
|
||||
33
src/components/Profile/components/Table/styles.module.scss
Normal file
33
src/components/Profile/components/Table/styles.module.scss
Normal file
@ -0,0 +1,33 @@
|
||||
.container {
|
||||
width: 100%;
|
||||
padding: 16px;
|
||||
border-radius: 12px;
|
||||
background-color: #f0f0f4;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
padding: 16px 8px;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.cell {
|
||||
width: 100%;
|
||||
line-height: 1.5;
|
||||
font-size: 16px;
|
||||
color: #7d8785;
|
||||
|
||||
&:nth-child(2) {
|
||||
color: #090909;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
100
src/components/Profile/pages/Profile/index.tsx
Normal file
100
src/components/Profile/pages/Profile/index.tsx
Normal file
@ -0,0 +1,100 @@
|
||||
import { useMemo, useState } from "react"
|
||||
import styles from "./styles.module.scss"
|
||||
import ProfileBlock from "../../components/ProfileBlock"
|
||||
import ProfileInformation from "../../components/ProfileInformation"
|
||||
import Billing from "../../components/Billing"
|
||||
import LogOut from "../../components/LogOut"
|
||||
import routes from "@/routes"
|
||||
import { useAuth } from "@/auth"
|
||||
import { useNavigate } from "react-router-dom"
|
||||
import Modal from "@/components/Modal"
|
||||
import Title from "@/components/Title"
|
||||
import { useTranslations } from "@/hooks/translations"
|
||||
import { ELocalesPlacement } from "@/locales"
|
||||
|
||||
function ProfilePage() {
|
||||
const { translate } = useTranslations(ELocalesPlacement.Profile);
|
||||
const { logout } = useAuth();
|
||||
const navigate = useNavigate();
|
||||
const [logoutModal, setLogoutModal] = useState(false);
|
||||
|
||||
const handleLogout = () => {
|
||||
localStorage.removeItem("palmistry.firstUnpassedStep");
|
||||
navigate(routes.client.compatibilityV2Gender());
|
||||
logout();
|
||||
};
|
||||
|
||||
const handleLogoutModal = () => {
|
||||
setLogoutModal(true);
|
||||
};
|
||||
|
||||
const handleBilling = () => {
|
||||
navigate(routes.client.profileSubscriptions());
|
||||
};
|
||||
|
||||
const profileBlocks = useMemo(() => [
|
||||
{
|
||||
title: translate("/profile.profile_information.title"),
|
||||
description: translate("/profile.profile_information.description"),
|
||||
children: <ProfileInformation />
|
||||
},
|
||||
// {
|
||||
// title: "Access code",
|
||||
// description: "Your account is protected by securely generated access code."
|
||||
// },
|
||||
{
|
||||
title: translate("/profile.billing.title"),
|
||||
description: translate("/profile.billing.description"),
|
||||
children: <Billing onBilling={handleBilling} />
|
||||
},
|
||||
// {
|
||||
// title: "Delete Account",
|
||||
// description: "Once your account is deleted, its resources and data will be permanently deleted. Your subscription will be active until cancelation in the Billing block above. Before deleting your account, please download any data or information you wish to retain."
|
||||
// },
|
||||
{
|
||||
title: translate("/profile.log_out.title"),
|
||||
children: <LogOut onLogout={handleLogoutModal} />
|
||||
}
|
||||
], [translate])
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
{profileBlocks.map((block, index) => (
|
||||
<div key={block.title} className={styles.block}>
|
||||
<ProfileBlock {...block}>
|
||||
{block.children}
|
||||
</ProfileBlock>
|
||||
{index !== profileBlocks.length - 1 && <hr />}
|
||||
</div>
|
||||
))}
|
||||
{logoutModal && <Modal
|
||||
isCloseButtonVisible={false}
|
||||
open={!!logoutModal}
|
||||
onClose={() => setLogoutModal(false)}
|
||||
className={styles.modal}
|
||||
containerClassName={styles["modal-container"]}
|
||||
>
|
||||
<Title variant="h4" className={styles["modal-title"]}>
|
||||
{translate("/profile.log_out.modal.title")}
|
||||
</Title>
|
||||
<p className={styles["modal-description"]}>
|
||||
{translate("/profile.log_out.modal.description")}
|
||||
</p>
|
||||
<div className={styles["modal-answers"]}>
|
||||
<div className={styles["modal-answer"]} onClick={handleLogout}>
|
||||
<p className={styles["modal-answer-text"]}>
|
||||
{translate("/profile.log_out.modal.log_out_button")}
|
||||
</p>
|
||||
</div>
|
||||
<div className={styles["modal-answer"]} onClick={() => setLogoutModal(false)}>
|
||||
<p className={styles["modal-answer-text"]}>
|
||||
{translate("/profile.log_out.modal.stay_button")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ProfilePage
|
||||
76
src/components/Profile/pages/Profile/styles.module.scss
Normal file
76
src/components/Profile/pages/Profile/styles.module.scss
Normal file
@ -0,0 +1,76 @@
|
||||
.container {
|
||||
width: 100%;
|
||||
height: fit-content;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 0 #0000, 0 0 #0000, 0 1px 3px 0 rgb(0 0 0 / .1), 0 1px 2px -1px rgb(0 0 0 / .1);
|
||||
}
|
||||
|
||||
.block {
|
||||
width: 100%;
|
||||
|
||||
&>hr {
|
||||
width: 100%;
|
||||
height: 1px;
|
||||
background-color: #f0f0f0;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.modal-container {
|
||||
max-width: 290px;
|
||||
padding: 24px 0px 0px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
font-weight: 600;
|
||||
margin-bottom: 16px;
|
||||
padding-inline: 24px;
|
||||
}
|
||||
|
||||
.modal-description {
|
||||
padding-inline: 24px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.modal-answers {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
margin-top: 24px;
|
||||
border-top: 1px solid #D9D9D9;
|
||||
}
|
||||
|
||||
.modal-answer {
|
||||
width: 50%;
|
||||
height: 52px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #275DA7;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
|
||||
&:first-child {
|
||||
border-right: 1px solid #D9D9D9;
|
||||
}
|
||||
}
|
||||
|
||||
:global(.dark-theme) .modal-container {
|
||||
background-color: #343639;
|
||||
|
||||
&>.modal-answers>.modal-answer {
|
||||
color: #1e7dff;
|
||||
}
|
||||
}
|
||||
|
||||
:global(.dark-theme) .modal-title {
|
||||
color: #F7F7F7;
|
||||
}
|
||||
|
||||
:global(.dark-theme) .modal-description {
|
||||
color: #F7F7F7;
|
||||
}
|
||||
71
src/components/Profile/pages/Subscriptions/index.tsx
Normal file
71
src/components/Profile/pages/Subscriptions/index.tsx
Normal file
@ -0,0 +1,71 @@
|
||||
import Title from "@/components/Title"
|
||||
import styles from "./styles.module.scss"
|
||||
import Table from "../../components/Table"
|
||||
import { useMemo, useState } from "react"
|
||||
import Modal from "@/components/Modal"
|
||||
import Button from "@/components/CompatibilityV2/components/Button"
|
||||
import { useTranslations } from "@/hooks/translations"
|
||||
import { ELocalesPlacement } from "@/locales"
|
||||
|
||||
function SubscriptionsPage() {
|
||||
const { translate } = useTranslations(ELocalesPlacement.Profile);
|
||||
const [cancelSubscriptionModal, setCancelSubscriptionModal] = useState(false);
|
||||
|
||||
const handleCancelSubscriptionModal = () => {
|
||||
setCancelSubscriptionModal(true);
|
||||
};
|
||||
|
||||
const handleCancelSubscription = () => {
|
||||
console.log("cancel subscription");
|
||||
setCancelSubscriptionModal(false);
|
||||
};
|
||||
|
||||
const data = useMemo(() => [
|
||||
[translate("/subscriptions.table.subscription_type"), "Weekly Subscription"],
|
||||
[translate("/subscriptions.table.subscription_status"), "Cancels on May 06, 2025"],
|
||||
[translate("/subscriptions.table.billing_period"), "Week"],
|
||||
[translate("/subscriptions.table.last_payment_on"), "Apr 29, 2025"],
|
||||
[translate("/subscriptions.table.renewal_date"), "May 06, 2025"],
|
||||
[translate("/subscriptions.table.renewal_amount"), "$14.50"],
|
||||
[
|
||||
<Button className={styles.button} onClick={handleCancelSubscriptionModal}>
|
||||
{translate("/subscriptions.table.cancel_subscription")}
|
||||
</Button>
|
||||
]
|
||||
], [translate])
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<Title variant="h1" className={styles.title}>{translate("/subscriptions.title")}</Title>
|
||||
<Table data={data} />
|
||||
{cancelSubscriptionModal && <Modal
|
||||
isCloseButtonVisible={false}
|
||||
open={!!cancelSubscriptionModal}
|
||||
onClose={() => setCancelSubscriptionModal(false)}
|
||||
className={styles.modal}
|
||||
containerClassName={styles["modal-container"]}
|
||||
>
|
||||
<Title variant="h4" className={styles["modal-title"]}>
|
||||
{translate("/subscriptions.modal.title")}
|
||||
</Title>
|
||||
<p className={styles["modal-description"]}>
|
||||
{translate("/subscriptions.modal.description")}
|
||||
</p>
|
||||
<div className={styles["modal-answers"]}>
|
||||
<div className={styles["modal-answer"]} onClick={handleCancelSubscription}>
|
||||
<p className={styles["modal-answer-text"]}>
|
||||
{translate("/subscriptions.modal.cancel_button")}
|
||||
</p>
|
||||
</div>
|
||||
<div className={styles["modal-answer"]} onClick={() => setCancelSubscriptionModal(false)}>
|
||||
<p className={styles["modal-answer-text"]}>
|
||||
{translate("/subscriptions.modal.stay_button")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default SubscriptionsPage
|
||||
@ -0,0 +1,74 @@
|
||||
.container {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
padding-inline: 8px;
|
||||
}
|
||||
|
||||
.title {
|
||||
margin: 0;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.button {
|
||||
max-width: 100%;
|
||||
min-height: 60px;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.modal-container {
|
||||
max-width: 290px;
|
||||
padding: 24px 0px 0px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
font-weight: 600;
|
||||
margin-bottom: 16px;
|
||||
padding-inline: 24px;
|
||||
}
|
||||
|
||||
.modal-description {
|
||||
padding-inline: 24px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.modal-answers {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
margin-top: 24px;
|
||||
border-top: 1px solid #D9D9D9;
|
||||
}
|
||||
|
||||
.modal-answer {
|
||||
width: 50%;
|
||||
height: 52px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #275DA7;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
text-align: center;
|
||||
|
||||
&:first-child {
|
||||
border-right: 1px solid #D9D9D9;
|
||||
}
|
||||
}
|
||||
|
||||
:global(.dark-theme) .modal-container {
|
||||
background-color: #343639;
|
||||
|
||||
&>.modal-answers>.modal-answer {
|
||||
color: #1e7dff;
|
||||
}
|
||||
}
|
||||
|
||||
:global(.dark-theme) .modal-title {
|
||||
color: #F7F7F7;
|
||||
}
|
||||
|
||||
:global(.dark-theme) .modal-description {
|
||||
color: #F7F7F7;
|
||||
}
|
||||
@ -16,7 +16,7 @@ const isValidBirthplace = (birthplace: string) => {
|
||||
|
||||
function BirthplaceInput({
|
||||
inputClassName,
|
||||
value,
|
||||
value = "",
|
||||
placeholder,
|
||||
placeholderClassName,
|
||||
onValid,
|
||||
|
||||
@ -11,6 +11,7 @@ const isValidEmail = (email: string) => {
|
||||
interface IEmailInputProps {
|
||||
inputContainerClassName?: string;
|
||||
placeholderClassName?: string;
|
||||
readonly?: boolean;
|
||||
}
|
||||
|
||||
function EmailInput(props: FormField<string> & IEmailInputProps): JSX.Element {
|
||||
@ -23,6 +24,7 @@ function EmailInput(props: FormField<string> & IEmailInputProps): JSX.Element {
|
||||
placeholderClassName = "",
|
||||
onValid,
|
||||
onInvalid,
|
||||
readonly = false,
|
||||
} = props;
|
||||
const [email, setEmail] = useState(value);
|
||||
|
||||
@ -55,6 +57,7 @@ function EmailInput(props: FormField<string> & IEmailInputProps): JSX.Element {
|
||||
onChange={handleChangeEmail}
|
||||
placeholder=" "
|
||||
autoComplete="email"
|
||||
readOnly={readonly}
|
||||
/>
|
||||
<span
|
||||
className={`${styles["input__placeholder"]} ${placeholderClassName}`}
|
||||
|
||||
@ -6,6 +6,8 @@ interface INameInputProps {
|
||||
value: string;
|
||||
placeholder: string;
|
||||
placeholderClassName?: string;
|
||||
inputContainerClassName?: string;
|
||||
readonly?: boolean;
|
||||
onValid: (value: string) => void;
|
||||
onInvalid: () => void;
|
||||
}
|
||||
@ -16,11 +18,13 @@ const isValidName = (name: string) => {
|
||||
|
||||
function NameInput({
|
||||
inputClassName,
|
||||
inputContainerClassName = "",
|
||||
value,
|
||||
placeholder,
|
||||
placeholderClassName,
|
||||
onValid,
|
||||
onInvalid,
|
||||
readonly = false,
|
||||
}: INameInputProps & Partial<FormField<string>>) {
|
||||
const [name, setName] = useState(value);
|
||||
|
||||
@ -35,7 +39,7 @@ function NameInput({
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles["input-container"]}>
|
||||
<div className={`${styles["input-container"]} ${inputContainerClassName}`}>
|
||||
<input
|
||||
className={inputClassName}
|
||||
type="text"
|
||||
@ -45,6 +49,7 @@ function NameInput({
|
||||
onChange={handleChangeName}
|
||||
placeholder=" "
|
||||
autoComplete="name"
|
||||
readOnly={readonly}
|
||||
/>
|
||||
<span className={`${styles["input__placeholder"]} ${placeholderClassName}`}>{placeholder}</span>
|
||||
</div>
|
||||
|
||||
@ -51,6 +51,7 @@ function TryAppPage() {
|
||||
|
||||
const downloadApp = async () => {
|
||||
metricService.reachGoal(EGoals.DOWNLOAD_APP, [EMetrics.YANDEX]);
|
||||
metricService.reachGoal(EGoals.BLACK_BUTTON, [EMetrics.FACEBOOK]);
|
||||
await copyToClipboard(authCode);
|
||||
// TODO
|
||||
window.location.href =
|
||||
|
||||
@ -47,6 +47,7 @@ function TryAppPage() {
|
||||
|
||||
const downloadApp = () => {
|
||||
metricService.reachGoal(EGoals.DOWNLOAD_APP, [EMetrics.YANDEX]);
|
||||
metricService.reachGoal(EGoals.BLACK_BUTTON, [EMetrics.FACEBOOK]);
|
||||
// TODO
|
||||
window.location.href =
|
||||
"https://apps.apple.com/us/app/aura-astrology-horoscope/id1601978549";
|
||||
|
||||
@ -32,7 +32,9 @@ export enum EUnleashFlags {
|
||||
"compatibilityV4TrialTextPrice" = "v4-compatibility-trial-text-price",
|
||||
"palmistryV1TrialTextPrice" = "v1-palmistry-trial-text-price",
|
||||
"v2CompatibilityScanResultNumbers" = "v2-compatibility-scan-result-numbers",
|
||||
"v2CompatibilityScanInstructionImage" = "v2-compatibility-scan-instruction-image"
|
||||
"v2CompatibilityScanInstructionImage" = "v2-compatibility-scan-instruction-image",
|
||||
"v2CompatibilityCameraTemplate" = "v2-compatibility-camera-template",
|
||||
"v2CompatibilityRelationshipStatusPagePlacement" = "v2-compatibility-relationship-status-page-placement",
|
||||
}
|
||||
|
||||
interface IUseUnleashProps<T extends EUnleashFlags> {
|
||||
@ -70,7 +72,8 @@ interface IVariants {
|
||||
[EUnleashFlags.palmistryV1TrialTextPrice]: "v0" | "v1" | "v2" | "v3";
|
||||
[EUnleashFlags.v2CompatibilityScanResultNumbers]: 'off' | 'on';
|
||||
[EUnleashFlags.v2CompatibilityScanInstructionImage]: 'v0' | 'v1';
|
||||
|
||||
[EUnleashFlags.v2CompatibilityCameraTemplate]: 'v0' | 'v1' | 'v2' | 'v3' | 'v4';
|
||||
[EUnleashFlags.v2CompatibilityRelationshipStatusPagePlacement]: 'v0' | 'v1' | 'v2';
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
155
src/hooks/payment/nmi/useAddressFields.ts
Normal file
155
src/hooks/payment/nmi/useAddressFields.ts
Normal file
@ -0,0 +1,155 @@
|
||||
import { useState, useMemo } from "react";
|
||||
import countries from "i18n-iso-countries";
|
||||
import en from "i18n-iso-countries/langs/en.json";
|
||||
import fr from "i18n-iso-countries/langs/fr.json";
|
||||
import es from "i18n-iso-countries/langs/es.json";
|
||||
import de from "i18n-iso-countries/langs/de.json";
|
||||
import pt from "i18n-iso-countries/langs/pt.json";
|
||||
import hi from "i18n-iso-countries/langs/hi.json";
|
||||
import ru from "i18n-iso-countries/langs/ru.json";
|
||||
import { ELocalesPlacement } from "@/locales";
|
||||
import { useTranslations } from "@/hooks/translations";
|
||||
|
||||
const locales = [en, fr, es, de, pt, hi, ru];
|
||||
|
||||
locales.forEach(locale => {
|
||||
countries.registerLocale(locale);
|
||||
});
|
||||
|
||||
export interface AddressFields {
|
||||
country: string;
|
||||
address: string;
|
||||
zip: string;
|
||||
}
|
||||
|
||||
export function useAddressFields(locale: string = "en", initial?: Partial<AddressFields>) {
|
||||
const { translate } = useTranslations(ELocalesPlacement.V1);
|
||||
|
||||
const [fields, setFields] = useState<AddressFields>({
|
||||
country: "",
|
||||
address: "",
|
||||
zip: "",
|
||||
...initial,
|
||||
});
|
||||
const [errors, setErrors] = useState<Partial<Record<keyof AddressFields, string>>>({
|
||||
country: "",
|
||||
address: "",
|
||||
zip: "",
|
||||
});
|
||||
const [touched, setTouched] = useState<Partial<Record<keyof AddressFields, boolean>>>({
|
||||
country: false,
|
||||
address: false,
|
||||
zip: false,
|
||||
});
|
||||
|
||||
const isValid = useMemo(() => {
|
||||
if (Object.values(touched).some(value => !value)) {
|
||||
return false;
|
||||
}
|
||||
return Object.values(errors).every(error => error === "");
|
||||
}, [errors, touched]);
|
||||
|
||||
const countryList = useMemo(() => {
|
||||
let _locale = locale;
|
||||
if (locale === "es-419") {
|
||||
_locale = "es";
|
||||
}
|
||||
if (locale === "pt-BR" || locale === "pt-PT") {
|
||||
_locale = "pt";
|
||||
}
|
||||
if (!Object.keys(countries.getNames(_locale)).length) {
|
||||
_locale = "en";
|
||||
}
|
||||
const names = countries.getNames(_locale, { select: "official" });
|
||||
|
||||
return Object.entries(names).map(([code, name]) => ({ code, name }));
|
||||
}, [locale]);
|
||||
|
||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => {
|
||||
setTouched({ ...touched, [e.target.name]: true });
|
||||
validate(e.target.name as keyof AddressFields, e.target.value);
|
||||
setFields({ ...fields, [e.target.name]: e.target.value });
|
||||
};
|
||||
|
||||
const validate = (name: keyof AddressFields, value: string) => {
|
||||
switch (name) {
|
||||
case "country":
|
||||
if (!value) {
|
||||
setErrors({ ...errors, country: translate("payment_modal.address_form.errors.country") });
|
||||
} else {
|
||||
setErrors({ ...errors, country: "" });
|
||||
}
|
||||
break;
|
||||
case "address":
|
||||
if (!value) {
|
||||
setErrors({ ...errors, address: translate("payment_modal.address_form.errors.address") });
|
||||
} else {
|
||||
setErrors({ ...errors, address: "" });
|
||||
}
|
||||
break;
|
||||
case "zip":
|
||||
const country = fields.country || initial?.country || "";
|
||||
let zipValid = false;
|
||||
let zipError = "";
|
||||
|
||||
const zipRegexes: Record<string, RegExp> = {
|
||||
US: /^\d{5}(-\d{4})?$/, // 12345 или 12345-6789
|
||||
GB: /^[A-Z]{1,2}\d[A-Z\d]? ?\d[A-Z]{2}$/i, // SW1A 1AA
|
||||
AU: /^\d{4}$/, // 4000
|
||||
FR: /^\d{5}$/, // 75008
|
||||
ES: /^\d{5}$/, // 28013
|
||||
DE: /^\d{5}$/, // 10115
|
||||
PT: /^\d{4}-\d{3}$/, // 1000-001
|
||||
BR: /^\d{5}-?\d{3}$/, // 12345-678 или 12345678
|
||||
IN: /^\d{6}$/, // 110001
|
||||
RU: /^\d{6}$/, // 123456
|
||||
};
|
||||
|
||||
if (!value) {
|
||||
zipError = translate("payment_modal.address_form.errors.zip");
|
||||
} else if (country && zipRegexes[country]) {
|
||||
zipValid = zipRegexes[country].test(value);
|
||||
if (!zipValid) {
|
||||
zipError = translate("payment_modal.address_form.errors.zip_invalid");
|
||||
}
|
||||
} else {
|
||||
zipValid = !!value;
|
||||
if (!zipValid) {
|
||||
zipError = translate("payment_modal.address_form.errors.zip");
|
||||
}
|
||||
}
|
||||
|
||||
setErrors({ ...errors, zip: zipError });
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
const validateAll = () => {
|
||||
for (const key in fields) {
|
||||
validate(key as keyof AddressFields, fields[key as keyof AddressFields]);
|
||||
}
|
||||
return Object.values(errors).every(error => error === "");
|
||||
};
|
||||
|
||||
return useMemo(() => ({
|
||||
fields,
|
||||
errors,
|
||||
handleChange,
|
||||
validate,
|
||||
validateAll,
|
||||
countryList,
|
||||
setFields,
|
||||
setErrors,
|
||||
isValid
|
||||
}), [
|
||||
fields,
|
||||
errors,
|
||||
handleChange,
|
||||
validate,
|
||||
validateAll,
|
||||
countryList,
|
||||
setFields,
|
||||
setErrors,
|
||||
isValid
|
||||
]);
|
||||
}
|
||||
@ -5,8 +5,9 @@ import { useAuthentication } from "@/hooks/authentication/use-authentication";
|
||||
import useElementRemovalObserver from "@/hooks/DOM/useElementRemovalObserver";
|
||||
import { usePaywall } from "@/hooks/paywall/usePaywall";
|
||||
import { selectors } from "@/store";
|
||||
import { useEffect, useMemo, useState } from "react"
|
||||
import { useEffect, useMemo, useRef, useState } from "react"
|
||||
import { useSelector } from "react-redux";
|
||||
import { AddressFields } from "./useAddressFields";
|
||||
|
||||
interface IUsePaymentProps {
|
||||
placementKey: EPlacementKeys;
|
||||
@ -52,6 +53,11 @@ export const usePayment = ({
|
||||
cvv: { isValid: false, message: '' }
|
||||
});
|
||||
const [isCollectJSLoaded, setIsCollectJSLoaded] = useState(false);
|
||||
const addressRef = useRef<AddressFields>({
|
||||
country: "",
|
||||
address: "",
|
||||
zip: "",
|
||||
});
|
||||
|
||||
const { anonymousAuthorization } = useAuthentication();
|
||||
|
||||
@ -184,7 +190,8 @@ export const usePayment = ({
|
||||
productId: activeProduct?._id || "",
|
||||
placementId,
|
||||
paywallId,
|
||||
paymentToken: response.token
|
||||
paymentToken: response.token,
|
||||
address: addressRef.current || undefined
|
||||
})
|
||||
:
|
||||
await api.makeAnonymousPayment({
|
||||
@ -192,7 +199,8 @@ export const usePayment = ({
|
||||
placementId,
|
||||
paywallId,
|
||||
paymentToken: response.token,
|
||||
sessionId: sessionId
|
||||
sessionId: sessionId,
|
||||
address: addressRef.current || undefined
|
||||
})
|
||||
|
||||
if (isAnonymous && "user" in res && res?.user) {
|
||||
@ -224,7 +232,10 @@ export const usePayment = ({
|
||||
setIsOpenModal(true)
|
||||
};
|
||||
|
||||
const submitInlineForm = () => {
|
||||
const submitInlineForm = (address?: AddressFields) => {
|
||||
if (address) {
|
||||
addressRef.current = address;
|
||||
}
|
||||
try {
|
||||
setIsSubmitting(true);
|
||||
window.CollectJS.startPaymentRequest();
|
||||
|
||||
@ -53,7 +53,7 @@ export const defaultPaywalls: { [key in EPlacementKeys]: IPaywall } = {
|
||||
},
|
||||
{
|
||||
"key": "text.4",
|
||||
"value": "*Cost of trial as of February 2024",
|
||||
"value": "*Cost of trial as of February 2025",
|
||||
"_id": "664542bbfe0a8eb4ee0b4f2c"
|
||||
},
|
||||
{
|
||||
@ -174,7 +174,7 @@ export const defaultPaywalls: { [key in EPlacementKeys]: IPaywall } = {
|
||||
},
|
||||
{
|
||||
"key": "text.4",
|
||||
"value": "*Cost of trial as of February 2024",
|
||||
"value": "*Cost of trial as of February 2025",
|
||||
"_id": "664542bbfe0a8eb4ee0b4f2c"
|
||||
},
|
||||
{
|
||||
@ -295,7 +295,7 @@ export const defaultPaywalls: { [key in EPlacementKeys]: IPaywall } = {
|
||||
},
|
||||
{
|
||||
"key": "text.4",
|
||||
"value": "*Cost of trial as of February 2024",
|
||||
"value": "*Cost of trial as of February 2025",
|
||||
"_id": "664542bbfe0a8eb4ee0b4f2c"
|
||||
},
|
||||
{
|
||||
|
||||
@ -9,6 +9,7 @@ import { useEffect, useState } from "react";
|
||||
import { useDispatch } from "react-redux";
|
||||
import { useLocation, useSearchParams } from "react-router-dom";
|
||||
import { FlagProvider } from '@unleash/proxy-client-react';
|
||||
import { handLandmarkerSingleton } from "@/utils/handLandmarkerSingleton";
|
||||
|
||||
interface InitializationProviderProps {
|
||||
children: React.ReactNode;
|
||||
@ -37,6 +38,11 @@ export function InitializationProvider({ children }: InitializationProviderProps
|
||||
} = useSession()
|
||||
const source = getSourceByPathname();
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
handLandmarkerSingleton.preload();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const createSessionBySource = async () => {
|
||||
if (!availableSourcesForSession.includes(source)) {
|
||||
|
||||
@ -92,6 +92,7 @@ export enum ELocalesPlacement {
|
||||
CompatibilityV3 = "compatibility-v3",
|
||||
CompatibilityV4 = "compatibility-v4",
|
||||
EmailGenerator = "email-generator",
|
||||
Profile = "profile",
|
||||
}
|
||||
|
||||
interface ITranslationJSON {
|
||||
|
||||
@ -4,6 +4,7 @@ import { useRef } from "react";
|
||||
import { Outlet, useLocation } from "react-router-dom";
|
||||
import routes from "@/routes";
|
||||
import Header from "@/components/CompatibilityV2/components/Header";
|
||||
import { EUnleashFlags, useUnleash } from "@/hooks/ab/unleash/useUnleash";
|
||||
|
||||
const isBackButtonVisibleRoutes = [
|
||||
routes.client.compatibilityV2Birthdate(),
|
||||
@ -32,7 +33,22 @@ function Layout() {
|
||||
location,
|
||||
]);
|
||||
|
||||
const { variant: relationshipStatusPagePlacement = "v0" } = useUnleash({
|
||||
flag: EUnleashFlags.v2CompatibilityRelationshipStatusPagePlacement
|
||||
});
|
||||
|
||||
const getIsBackButtonVisible = () => {
|
||||
if (
|
||||
relationshipStatusPagePlacement === "v1" && location.pathname.includes(routes.client.compatibilityV2RelationshipStatus())
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
relationshipStatusPagePlacement === "v1" && location.pathname.includes(routes.client.compatibilityV2Gender())
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
for (const route of isBackButtonVisibleRoutes) {
|
||||
if (location.pathname.includes(route)) return true;
|
||||
}
|
||||
|
||||
@ -1,11 +1,8 @@
|
||||
import routes from "@/routes";
|
||||
import styles from "./styles.module.scss";
|
||||
import PersonalVideo from "@/components/pages/ABDesign/v1/pages/TrialPayment/components/PersonalVideo";
|
||||
import { actions, selectors } from "@/store";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { actions } from "@/store";
|
||||
import { useDispatch } from "react-redux";
|
||||
import { Outlet, useLocation } from "react-router-dom";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { getDefaultLocaleByLanguage, language } from "@/locales";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
|
||||
export interface IOutletContext {
|
||||
containerVideoRef: React.RefObject<HTMLDivElement>;
|
||||
@ -17,23 +14,23 @@ export interface IOutletContext {
|
||||
function LayoutPersonalVideo() {
|
||||
const dispatch = useDispatch();
|
||||
const location = useLocation();
|
||||
const { gender } = useSelector(selectors.selectQuestionnaire);
|
||||
const [isVisibleElements, setIsVisibleElements] = useState(false);
|
||||
const showElementsTimer = useRef<NodeJS.Timeout>();
|
||||
// const { gender } = useSelector(selectors.selectQuestionnaire);
|
||||
const [isVisibleElements, _setIsVisibleElements] = useState(false);
|
||||
// const showElementsTimer = useRef<NodeJS.Timeout>();
|
||||
const containerVideoRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const locale = getDefaultLocaleByLanguage(language);
|
||||
// const locale = getDefaultLocaleByLanguage(language);
|
||||
|
||||
const videoUrl =
|
||||
locale === "es"
|
||||
? "/trial-choice-palmistry-es.mp4"
|
||||
: "/trial-choice-palmistry.mp4";
|
||||
// const videoUrl =
|
||||
// locale === "es"
|
||||
// ? "/trial-choice-palmistry-es.mp4"
|
||||
// : "/trial-choice-palmistry.mp4";
|
||||
|
||||
const showElements = useCallback(() => {
|
||||
showElementsTimer.current = setTimeout(() => {
|
||||
setIsVisibleElements(true);
|
||||
}, 29_000);
|
||||
}, [setIsVisibleElements]);
|
||||
// const showElements = useCallback(() => {
|
||||
// showElementsTimer.current = setTimeout(() => {
|
||||
// setIsVisibleElements(true);
|
||||
// }, 29_000);
|
||||
// }, [setIsVisibleElements]);
|
||||
|
||||
useEffect(() => {
|
||||
if (location.pathname !== routes.client.compatibilityV2TrialChoiceVideo()) {
|
||||
@ -43,7 +40,7 @@ function LayoutPersonalVideo() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<PersonalVideo
|
||||
{/* <PersonalVideo
|
||||
gender={gender}
|
||||
url={videoUrl}
|
||||
classNameContainer={
|
||||
@ -57,7 +54,7 @@ function LayoutPersonalVideo() {
|
||||
isAutoPlay={
|
||||
location.pathname === routes.client.compatibilityV2TrialChoiceVideo()
|
||||
}
|
||||
/>
|
||||
/> */}
|
||||
<Outlet
|
||||
context={{
|
||||
containerVideoRef,
|
||||
|
||||
@ -1,11 +1,8 @@
|
||||
import routes from "@/routes";
|
||||
import styles from "./styles.module.scss";
|
||||
import PersonalVideo from "@/components/pages/ABDesign/v1/pages/TrialPayment/components/PersonalVideo";
|
||||
import { actions, selectors } from "@/store";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { actions } from "@/store";
|
||||
import { useDispatch } from "react-redux";
|
||||
import { Outlet, useLocation } from "react-router-dom";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { getDefaultLocaleByLanguage, language } from "@/locales";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
|
||||
export interface IOutletContext {
|
||||
containerVideoRef: React.RefObject<HTMLDivElement>;
|
||||
@ -17,23 +14,23 @@ export interface IOutletContext {
|
||||
function LayoutPersonalVideo() {
|
||||
const dispatch = useDispatch();
|
||||
const location = useLocation();
|
||||
const { gender } = useSelector(selectors.selectQuestionnaire);
|
||||
const [isVisibleElements, setIsVisibleElements] = useState(false);
|
||||
const showElementsTimer = useRef<NodeJS.Timeout>();
|
||||
// const { gender } = useSelector(selectors.selectQuestionnaire);
|
||||
const [isVisibleElements, _setIsVisibleElements] = useState(false);
|
||||
// const showElementsTimer = useRef<NodeJS.Timeout>();
|
||||
const containerVideoRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const locale = getDefaultLocaleByLanguage(language);
|
||||
// const locale = getDefaultLocaleByLanguage(language);
|
||||
|
||||
const videoUrl =
|
||||
locale === "es"
|
||||
? "/trial-choice-palmistry-es.mp4"
|
||||
: "/trial-choice-palmistry.mp4";
|
||||
// const videoUrl =
|
||||
// locale === "es"
|
||||
// ? "/trial-choice-palmistry-es.mp4"
|
||||
// : "/trial-choice-palmistry.mp4";
|
||||
|
||||
const showElements = useCallback(() => {
|
||||
showElementsTimer.current = setTimeout(() => {
|
||||
setIsVisibleElements(true);
|
||||
}, 29_000);
|
||||
}, [setIsVisibleElements]);
|
||||
// const showElements = useCallback(() => {
|
||||
// showElementsTimer.current = setTimeout(() => {
|
||||
// setIsVisibleElements(true);
|
||||
// }, 29_000);
|
||||
// }, [setIsVisibleElements]);
|
||||
|
||||
useEffect(() => {
|
||||
if (location.pathname !== routes.client.compatibilityV3TrialChoiceVideo()) {
|
||||
@ -43,7 +40,7 @@ function LayoutPersonalVideo() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<PersonalVideo
|
||||
{/* <PersonalVideo
|
||||
gender={gender}
|
||||
url={videoUrl}
|
||||
classNameContainer={
|
||||
@ -57,7 +54,7 @@ function LayoutPersonalVideo() {
|
||||
isAutoPlay={
|
||||
location.pathname === routes.client.compatibilityV3TrialChoiceVideo()
|
||||
}
|
||||
/>
|
||||
/> */}
|
||||
<Outlet
|
||||
context={{
|
||||
containerVideoRef,
|
||||
|
||||
@ -1,11 +1,8 @@
|
||||
import routes from "@/routes";
|
||||
import styles from "./styles.module.scss";
|
||||
import PersonalVideo from "@/components/pages/ABDesign/v1/pages/TrialPayment/components/PersonalVideo";
|
||||
import { actions, selectors } from "@/store";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { actions } from "@/store";
|
||||
import { useDispatch } from "react-redux";
|
||||
import { Outlet, useLocation } from "react-router-dom";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { getDefaultLocaleByLanguage, language } from "@/locales";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
|
||||
export interface IOutletContext {
|
||||
containerVideoRef: React.RefObject<HTMLDivElement>;
|
||||
@ -17,23 +14,23 @@ export interface IOutletContext {
|
||||
function LayoutPersonalVideo() {
|
||||
const dispatch = useDispatch();
|
||||
const location = useLocation();
|
||||
const { gender } = useSelector(selectors.selectQuestionnaire);
|
||||
const [isVisibleElements, setIsVisibleElements] = useState(false);
|
||||
const showElementsTimer = useRef<NodeJS.Timeout>();
|
||||
// const { gender } = useSelector(selectors.selectQuestionnaire);
|
||||
const [isVisibleElements, _setIsVisibleElements] = useState(false);
|
||||
// const showElementsTimer = useRef<NodeJS.Timeout>();
|
||||
const containerVideoRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const locale = getDefaultLocaleByLanguage(language);
|
||||
// const locale = getDefaultLocaleByLanguage(language);
|
||||
|
||||
const videoUrl =
|
||||
locale === "es"
|
||||
? "/trial-choice-palmistry-es.mp4"
|
||||
: "/trial-choice-palmistry.mp4";
|
||||
// const videoUrl =
|
||||
// locale === "es"
|
||||
// ? "/trial-choice-palmistry-es.mp4"
|
||||
// : "/trial-choice-palmistry.mp4";
|
||||
|
||||
const showElements = useCallback(() => {
|
||||
showElementsTimer.current = setTimeout(() => {
|
||||
setIsVisibleElements(true);
|
||||
}, 29_000);
|
||||
}, [setIsVisibleElements]);
|
||||
// const showElements = useCallback(() => {
|
||||
// showElementsTimer.current = setTimeout(() => {
|
||||
// setIsVisibleElements(true);
|
||||
// }, 29_000);
|
||||
// }, [setIsVisibleElements]);
|
||||
|
||||
useEffect(() => {
|
||||
if (location.pathname !== routes.client.compatibilityV4TrialChoiceVideo()) {
|
||||
@ -43,7 +40,7 @@ function LayoutPersonalVideo() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<PersonalVideo
|
||||
{/* <PersonalVideo
|
||||
gender={gender}
|
||||
url={videoUrl}
|
||||
classNameContainer={
|
||||
@ -57,7 +54,7 @@ function LayoutPersonalVideo() {
|
||||
isAutoPlay={
|
||||
location.pathname === routes.client.compatibilityV4TrialChoiceVideo()
|
||||
}
|
||||
/>
|
||||
/> */}
|
||||
<Outlet
|
||||
context={{
|
||||
containerVideoRef,
|
||||
|
||||
@ -1,11 +1,8 @@
|
||||
import routes from "@/routes";
|
||||
import styles from "./styles.module.scss";
|
||||
import PersonalVideo from "@/components/pages/ABDesign/v1/pages/TrialPayment/components/PersonalVideo";
|
||||
import { actions, selectors } from "@/store";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { actions } from "@/store";
|
||||
import { useDispatch } from "react-redux";
|
||||
import { Outlet, useLocation } from "react-router-dom";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { getDefaultLocaleByLanguage, language } from "@/locales";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
|
||||
export interface IOutletContext {
|
||||
containerVideoRef: React.RefObject<HTMLDivElement>;
|
||||
@ -17,23 +14,23 @@ export interface IOutletContext {
|
||||
function LayoutPersonalVideo() {
|
||||
const dispatch = useDispatch();
|
||||
const location = useLocation();
|
||||
const { gender } = useSelector(selectors.selectQuestionnaire);
|
||||
const [isVisibleElements, setIsVisibleElements] = useState(false);
|
||||
const showElementsTimer = useRef<NodeJS.Timeout>();
|
||||
// const { gender } = useSelector(selectors.selectQuestionnaire);
|
||||
const [isVisibleElements, _setIsVisibleElements] = useState(false);
|
||||
// const showElementsTimer = useRef<NodeJS.Timeout>();
|
||||
const containerVideoRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const locale = getDefaultLocaleByLanguage(language);
|
||||
// const locale = getDefaultLocaleByLanguage(language);
|
||||
|
||||
const videoUrl =
|
||||
locale === "es"
|
||||
? "/trial-choice-palmistry-es.mp4"
|
||||
: "/trial-choice-palmistry.mp4";
|
||||
// const videoUrl =
|
||||
// locale === "es"
|
||||
// ? "/trial-choice-palmistry-es.mp4"
|
||||
// : "/trial-choice-palmistry.mp4";
|
||||
|
||||
const showElements = useCallback(() => {
|
||||
showElementsTimer.current = setTimeout(() => {
|
||||
setIsVisibleElements(true);
|
||||
}, 29_000);
|
||||
}, [setIsVisibleElements]);
|
||||
// const showElements = useCallback(() => {
|
||||
// showElementsTimer.current = setTimeout(() => {
|
||||
// setIsVisibleElements(true);
|
||||
// }, 29_000);
|
||||
// }, [setIsVisibleElements]);
|
||||
|
||||
useEffect(() => {
|
||||
if (location.pathname !== routes.client.palmistryV1TrialChoiceVideo()) {
|
||||
@ -43,7 +40,7 @@ function LayoutPersonalVideo() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<PersonalVideo
|
||||
{/* <PersonalVideo
|
||||
gender={gender}
|
||||
url={videoUrl}
|
||||
classNameContainer={
|
||||
@ -57,7 +54,7 @@ function LayoutPersonalVideo() {
|
||||
isAutoPlay={
|
||||
location.pathname === routes.client.palmistryV1TrialChoiceVideo()
|
||||
}
|
||||
/>
|
||||
/> */}
|
||||
<Outlet
|
||||
context={{
|
||||
containerVideoRef,
|
||||
|
||||
43
src/routerComponents/Profile/Layout/index.tsx
Normal file
43
src/routerComponents/Profile/Layout/index.tsx
Normal file
@ -0,0 +1,43 @@
|
||||
import { useRef } from "react";
|
||||
import styles from "./styles.module.scss"
|
||||
import { useSchemeColorByElement } from "@/hooks/useSchemeColorByElement";
|
||||
import { Outlet } from "react-router-dom";
|
||||
import Header from "@/components/CompatibilityV2/components/Header";
|
||||
import routes from "@/routes";
|
||||
|
||||
const isBackButtonVisibleRoutes = [
|
||||
routes.client.profileSubscriptions(),
|
||||
];
|
||||
|
||||
function Layout() {
|
||||
const mainRef = useRef<HTMLDivElement>(null);
|
||||
useSchemeColorByElement(mainRef.current, "section.page, .page, section", [
|
||||
location,
|
||||
]);
|
||||
|
||||
const getIsBackButtonVisible = () => {
|
||||
for (const route of isBackButtonVisibleRoutes) {
|
||||
if (location.pathname.includes(route)) return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<main className={`${styles.main} content`} ref={mainRef}>
|
||||
<Header
|
||||
className={styles.header}
|
||||
classNameTitle={styles["header-title"]}
|
||||
isBackButtonVisible={getIsBackButtonVisible()}
|
||||
/>
|
||||
{/* <Suspense fallback={<LoadingPage />}> */}
|
||||
<section className={styles.page}>
|
||||
<Outlet />
|
||||
</section>
|
||||
{/* </Suspense> */}
|
||||
</main>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default Layout
|
||||
33
src/routerComponents/Profile/Layout/styles.module.scss
Normal file
33
src/routerComponents/Profile/Layout/styles.module.scss
Normal file
@ -0,0 +1,33 @@
|
||||
.main {
|
||||
align-items: center;
|
||||
padding: 20px 8px;
|
||||
height: fit-content;
|
||||
min-height: 100dvh;
|
||||
max-width: 560px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.header {
|
||||
padding: 8px 0 30px;
|
||||
}
|
||||
|
||||
:global(.dark-theme) .header>button>svg>path {
|
||||
fill: #BAC2EE;
|
||||
}
|
||||
|
||||
.header-title {
|
||||
color: #275CA7;
|
||||
}
|
||||
|
||||
:global(.dark-theme) .header>svg>path {
|
||||
fill: #4F8DE5;
|
||||
}
|
||||
|
||||
.page {
|
||||
width: 100%;
|
||||
height: fit-content;
|
||||
padding-bottom: 32px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
31
src/routerComponents/Profile/index.tsx
Normal file
31
src/routerComponents/Profile/index.tsx
Normal file
@ -0,0 +1,31 @@
|
||||
import ProfilePage from '@/components/Profile/pages/Profile';
|
||||
import routes, { profilePrefix } from '@/routes'
|
||||
import { Route, Routes } from 'react-router-dom'
|
||||
import Layout from './Layout';
|
||||
import SubscriptionsPage from '@/components/Profile/pages/Subscriptions';
|
||||
|
||||
const removePrefix = (path: string) => path.replace(profilePrefix, "");
|
||||
|
||||
function ProfileRoutes() {
|
||||
|
||||
return (
|
||||
<Routes>
|
||||
<Route element={<Layout />}>
|
||||
<Route
|
||||
path={removePrefix(routes.client.profile())}
|
||||
element={
|
||||
<ProfilePage />
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path={removePrefix(routes.client.profileSubscriptions())}
|
||||
element={
|
||||
<SubscriptionsPage />
|
||||
}
|
||||
/>
|
||||
</Route>
|
||||
</Routes>
|
||||
)
|
||||
}
|
||||
|
||||
export default ProfileRoutes
|
||||
@ -20,6 +20,7 @@ export const palmistryV2Prefix = [host, "v2", "palmistry"].join("/")
|
||||
export const palmistryEmailMarketingV2Prefix = [palmistryV2Prefix, "email-marketing"].join("/")
|
||||
export const emailMarketingV1Prefix = [host, "v1", "email-marketing"].join("/")
|
||||
export const anonymousPrefix = [host, "anonymous"].join("/")
|
||||
export const profilePrefix = [host, "profile"].join("/")
|
||||
|
||||
export const chatsPrefix = [host, "chats"].join("/")
|
||||
|
||||
@ -477,6 +478,10 @@ const routes = {
|
||||
chatsThankYou: () => [chatsPrefix, "thankYou"].join("/"),
|
||||
chatsConnecting: () => [chatsPrefix, "connecting"].join("/"),
|
||||
chatsExpert: () => [chatsPrefix, "expert"].join("/"),
|
||||
|
||||
// Profile
|
||||
profile: () => [profilePrefix].join("/"),
|
||||
profileSubscriptions: () => [profilePrefix, "subscriptions"].join("/"),
|
||||
},
|
||||
server: {
|
||||
userLocale: () => ["https://ipapi.co", "json"].join("/"),
|
||||
|
||||
@ -55,6 +55,7 @@ export enum EGoals {
|
||||
ROSE_VIDEO_PLAY_USER_PLAY = "RoseVideoPlayUserPlay",
|
||||
|
||||
DOWNLOAD_APP = "DownloadApp",
|
||||
BLACK_BUTTON = "BlackButton",
|
||||
|
||||
CAMERA_HAND = "CameraHand",
|
||||
SCAN_ARTIFICIAL_PHOTO = "ScanArtificialPhoto",
|
||||
|
||||
59
src/utils/handLandmarkerSingleton.ts
Normal file
59
src/utils/handLandmarkerSingleton.ts
Normal file
@ -0,0 +1,59 @@
|
||||
import { HandLandmarker, FilesetResolver } from "@mediapipe/tasks-vision";
|
||||
|
||||
type Callback = (isLoaded: boolean) => void;
|
||||
|
||||
class HandLandmarkerSingleton {
|
||||
private handLandmarker: HandLandmarker | null = null;
|
||||
private isLoading = false;
|
||||
private isLoaded = false;
|
||||
private callbacks: Callback[] = [];
|
||||
private loadPromise: Promise<void> | null = null;
|
||||
|
||||
preload = () => {
|
||||
if (this.isLoaded || this.isLoading) return this.loadPromise!;
|
||||
this.isLoading = true;
|
||||
this.loadPromise = (async () => {
|
||||
const vision = await FilesetResolver.forVisionTasks("https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision/wasm");
|
||||
this.handLandmarker = await HandLandmarker.createFromOptions(vision, {
|
||||
baseOptions: {
|
||||
modelAssetPath: `https://storage.googleapis.com/mediapipe-models/hand_landmarker/hand_landmarker/float16/1/hand_landmarker.task`,
|
||||
delegate: "GPU"
|
||||
},
|
||||
runningMode: "VIDEO",
|
||||
numHands: 1
|
||||
});
|
||||
|
||||
this.warmUpHandLandmarker(this.handLandmarker);
|
||||
|
||||
this.isLoaded = true;
|
||||
this.isLoading = false;
|
||||
this.callbacks.forEach(cb => cb(true));
|
||||
})();
|
||||
this.callbacks.forEach(cb => cb(false));
|
||||
return this.loadPromise;
|
||||
};
|
||||
|
||||
getHandLandmarker = () => this.handLandmarker;
|
||||
|
||||
subscribe = (cb: Callback) => {
|
||||
this.callbacks.push(cb);
|
||||
cb(this.isLoaded);
|
||||
return () => {
|
||||
this.callbacks = this.callbacks.filter(f => f !== cb);
|
||||
};
|
||||
};
|
||||
|
||||
private warmUpHandLandmarker(handLandmarker: HandLandmarker) {
|
||||
try {
|
||||
const video = document.createElement('video');
|
||||
Object.defineProperty(video, 'videoWidth', { value: 1280 });
|
||||
Object.defineProperty(video, 'videoHeight', { value: 720 });
|
||||
|
||||
handLandmarker.detectForVideo(video, performance.now());
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const handLandmarkerSingleton = new HandLandmarkerSingleton();
|
||||
Loading…
Reference in New Issue
Block a user