From 8083d7ac9c2bc4c211c51fe455efba45644002ab Mon Sep 17 00:00:00 2001 From: Daniil Chemerkin Date: Sat, 1 Mar 2025 20:20:19 +0000 Subject: [PATCH] develop --- .../components/CameraModal/index.tsx | 81 ++++++++- .../components/CameraModal/styles.module.scss | 39 +++++ .../components/EmailsList/index.tsx | 63 +++++++ .../components/EmailsList/styles.module.scss | 91 ++++++++++ .../CompatibilityV2/pages/Gender/index.tsx | 31 +++- .../pages/TrialChoice/index.tsx | 19 ++- .../pages/TrialChoice/v1/index.tsx | 141 +++++++++++++++ .../pages/TrialChoice/v1/styles.module.scss | 160 ++++++++++++++++++ .../components/CameraModal/index.tsx | 81 ++++++++- .../components/CameraModal/styles.module.scss | 39 +++++ .../CompatibilityV3/pages/Gender/index.tsx | 31 +++- .../pages/HeadOrHeart/index.tsx | 3 + .../pages/ScannedPhoto/index.tsx | 40 +++-- .../pages/ScannedPhoto/styles.module.scss | 42 +++-- .../components/CameraModal/index.tsx | 82 ++++++++- .../components/CameraModal/styles.module.scss | 39 +++++ .../PalmistryV1/pages/Camera/index.tsx | 13 +- src/hooks/lottie/useLottie.ts | 2 + src/services/metric/metricService.ts | 3 +- 19 files changed, 961 insertions(+), 39 deletions(-) create mode 100644 src/components/CompatibilityV2/components/EmailsList/index.tsx create mode 100644 src/components/CompatibilityV2/components/EmailsList/styles.module.scss create mode 100644 src/components/CompatibilityV2/pages/TrialChoice/v1/index.tsx create mode 100644 src/components/CompatibilityV2/pages/TrialChoice/v1/styles.module.scss diff --git a/src/components/CompatibilityV2/components/CameraModal/index.tsx b/src/components/CompatibilityV2/components/CameraModal/index.tsx index ffd2ce5..b514dc2 100644 --- a/src/components/CompatibilityV2/components/CameraModal/index.tsx +++ b/src/components/CompatibilityV2/components/CameraModal/index.tsx @@ -5,6 +5,18 @@ import Modal from "@/components/palmistry/modal/modal"; import { useRef, useState } from "react"; // import { useDynamicSize } from "@/hooks/useDynamicSize"; +interface ExtendedMediaTrackCapabilities extends MediaTrackCapabilities { + torch?: boolean; +} + +interface TorchConstraints extends MediaTrackConstraintSet { + torch?: boolean; +} + +interface ExtendedMediaTrackConstraints extends MediaTrackConstraints { + advanced?: TorchConstraints[]; +} + interface CameraModalProps { onClose: () => void; onTakePhoto: (photo: string) => void; @@ -21,6 +33,8 @@ function CameraModal({ isCameraVisible = true }: CameraModalProps) { const [isVideoReady, setIsVideoReady] = useState(false); + const [isTorchOn, setIsTorchOn] = useState(false); + const [isTorchAvailable, setIsTorchAvailable] = useState(false); // const { // width: _width, height: _height, // elementRef: containerRef } = useDynamicSize({}); @@ -55,6 +69,47 @@ function CameraModal({ } }; + const checkTorchAvailability = async (track: MediaStreamTrack) => { + try { + const capabilities = track.getCapabilities() as ExtendedMediaTrackCapabilities; + setIsTorchAvailable(!!capabilities.torch); + } catch (error) { + setIsTorchAvailable(false); + console.error('Ошибка при проверке поддержки фонарика:', error); + } + }; + + const onUserMedia = (stream: MediaStream) => { + setIsVideoReady(true); + const track = stream.getVideoTracks()[0]; + if (track) { + checkTorchAvailability(track); + } + }; + + const toggleTorch = async () => { + try { + const track = cameraRef.current?.video?.srcObject instanceof MediaStream + ? (cameraRef.current.video.srcObject as MediaStream).getVideoTracks()[0] + : null; + + if (track) { + const capabilities = track.getCapabilities() as ExtendedMediaTrackCapabilities; + if (capabilities.torch) { + const constraints: ExtendedMediaTrackConstraints = { + advanced: [{ torch: !isTorchOn }] + }; + await track.applyConstraints(constraints); + setIsTorchOn(!isTorchOn); + } else { + onError("Вспышка не поддерживается на этом устройстве"); + } + } + } catch (error) { + onError(error instanceof Error ? error.message : "Ошибка при управлении вспышкой"); + } + }; + return setIsVideoReady(true)} + onUserMedia={onUserMedia} onUserMediaError={(error) => { setIsVideoReady(false); + setIsTorchAvailable(false); console.error(error); onError(error); }} @@ -103,6 +159,29 @@ function CameraModal({ opacity: isVideoReady ? 1 : 0.5, }} /> + {isTorchAvailable && ( + + )} ; diff --git a/src/components/CompatibilityV2/components/CameraModal/styles.module.scss b/src/components/CompatibilityV2/components/CameraModal/styles.module.scss index 7cf7f91..31585dd 100644 --- a/src/components/CompatibilityV2/components/CameraModal/styles.module.scss +++ b/src/components/CompatibilityV2/components/CameraModal/styles.module.scss @@ -63,4 +63,43 @@ transform: translate(-50%); width: auto; z-index: 9; +} + +.torchButton { + position: fixed; + bottom: calc(0dvh + 22px); + right: 20px; + width: 50px; + height: 50px; + border-radius: 25px; + background: rgba(0, 0, 0, 0.5); + border: none; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + transition: all 0.3s ease; + + &:active { + transform: scale(0.95); + } + + &>.torchIconContainer { + display: block; + z-index: 10; + width: 38px; + height: 38px; + } + + &.torchButtonActive { + background-color: #fff; + + &.torchIcon>path { + fill: #000; + + &:last-child { + transform: translate(0, -64px); + } + } + } } \ No newline at end of file diff --git a/src/components/CompatibilityV2/components/EmailsList/index.tsx b/src/components/CompatibilityV2/components/EmailsList/index.tsx new file mode 100644 index 0000000..745fdd9 --- /dev/null +++ b/src/components/CompatibilityV2/components/EmailsList/index.tsx @@ -0,0 +1,63 @@ +import Title from "@/components/Title" +import styles from "./styles.module.scss" +import { IPaywallProduct } from "@/api/resources/Paywall" +import { useTranslations } from "@/hooks/translations" +import { ELocalesPlacement } from "@/locales" +import { useEmailsGeneration } from "@/hooks/emailsGeneration/useEmailsGeneration" + +interface IEmailsListProps { + products: Array +} + +function EmailsList({ products }: IEmailsListProps) { + const { translate } = useTranslations(ELocalesPlacement.CompatibilityV2); + + const { + displayEmails, + countBoughtEmails + } = useEmailsGeneration(products); + + return ( +
+
+ + {translate("/trial-choice.v1.emails_list.title", { + count: countBoughtEmails + })} + +
+

+ {translate("/trial-choice.v1.emails_list.description", { + count: displayEmails?.length + })} +

+
+ {displayEmails.map((item) => ( +
+
+ {item.willBeRemoved && ( + + + + + )} +
+ ${item.price.toFixed(2)} +
+
+

+ {item.email} +

+
+ ))} +
+
+ ) +} + +export default EmailsList \ No newline at end of file diff --git a/src/components/CompatibilityV2/components/EmailsList/styles.module.scss b/src/components/CompatibilityV2/components/EmailsList/styles.module.scss new file mode 100644 index 0000000..8e38672 --- /dev/null +++ b/src/components/CompatibilityV2/components/EmailsList/styles.module.scss @@ -0,0 +1,91 @@ +.container { + width: 100%; + max-width: 328px; + background-color: #4B88FF4F; + border-radius: 18px; + margin-top: 16px; + + & * { + font-family: SF Pro Text, sans-serif; + } + + &>.description { + font-size: 16px; + line-height: 20px; + font-weight: 500; + color: #191919; + margin-top: 4px; + width: 100%; + padding-inline: 16px; + text-align: center; + } +} + +.header { + width: 100%; + border-bottom: 1px solid #E2ECFF; + padding-inline: 16px; + + &>.title { + font-size: 18px; + margin-bottom: 0; + line-height: 216%; + font-weight: 600; + } +} + +.emails { + width: 100%; + display: flex; + flex-direction: column; + gap: 8px; + margin-top: 13px; + height: 62px; + overflow-y: hidden; + + &>.emailContainer { + // display: flex; + // align-items: center; + // justify-content: center; + display: grid; + grid-template-columns: 100px 1fr; + padding-inline: 16px; + gap: 4px; + width: 100%; + opacity: 1; + transition: opacity 1.5s ease-in-out; + will-change: opacity; + + &.removed { + opacity: 0; + } + + &>.priceContainer { + display: flex; + align-items: center; + justify-content: right; + gap: 4px; + + &>.price { + padding: 1px 12px; + background-color: #FFFFFF; + border-radius: 24px; + font-size: 13px; + font-weight: 600; + line-height: 16px; + text-align: center; + color: #27296E; + width: fit-content; + } + } + + &>.email { + font-size: 13px; + line-height: 16px; + font-weight: 400; + color: #000; + padding-left: 2px; + text-align: left; + } + } +} \ No newline at end of file diff --git a/src/components/CompatibilityV2/pages/Gender/index.tsx b/src/components/CompatibilityV2/pages/Gender/index.tsx index a5008ac..2af7a27 100644 --- a/src/components/CompatibilityV2/pages/Gender/index.tsx +++ b/src/components/CompatibilityV2/pages/Gender/index.tsx @@ -38,7 +38,7 @@ function GenderPage() { }); const { flags, ready } = useMetricABFlags(); - const pageType = flags?.genderPageType?.[0]; + const pageType = flags?.genderPageType?.[0] || "v2"; const localGenders = genders.map((gender) => ({ id: gender.id, title: translate(gender.id, undefined, ELocalesPlacement.V1), @@ -97,6 +97,35 @@ function GenderPage() { if (!ready) return ; switch (pageType) { + case "v0": + return ( + <> + + {translate("/gender.title")} + +

{translate("/gender.description", { + br:
, + })}

+ {/* */} + +
+ {localGenders.map((_gender, index) => ( + selectGender(genders.find((g) => g.id === _gender.id) ?? null)} + /> + ))} +
+ + {/* {gender && !privacyPolicyChecked && ( + + {translate("/gender.toast", undefined, ELocalesPlacement.V1)} + + )} */} + + ) case "v1": return ( <> diff --git a/src/components/CompatibilityV2/pages/TrialChoice/index.tsx b/src/components/CompatibilityV2/pages/TrialChoice/index.tsx index 73fa0fb..f3305e2 100644 --- a/src/components/CompatibilityV2/pages/TrialChoice/index.tsx +++ b/src/components/CompatibilityV2/pages/TrialChoice/index.tsx @@ -19,7 +19,8 @@ import Loader from "@/components/Loader"; import { useTranslations } from "@/hooks/translations"; // import { useMetricABFlags } from "@/services/metric/metricService"; import { getLongText } from "./abText"; -import metricService, { EGoals, EMetrics } from "@/services/metric/metricService"; +import metricService, { EGoals, EMetrics, useMetricABFlags } from "@/services/metric/metricService"; +import TrialChoiceV1 from "./v1"; function TrialChoice() { const { translate } = useTranslations(ELocalesPlacement.CompatibilityV2); @@ -32,6 +33,10 @@ function TrialChoice() { const locale = getDefaultLocaleByLanguage(language); + const { flags, ready } = useMetricABFlags(); + const trialChoicePageType = flags?.trialChoicePageType?.[0]; + // const trialChoicePageType = "v1"; + // const { flags } = useMetricABFlags(); // const isLongText = flags?.text?.[0] === "on"; @@ -64,6 +69,18 @@ function TrialChoice() { metricService.reachGoal(EGoals.AURA_TRIAL_CHOICE_PAGE_VISIT, [EMetrics.KLAVIYO]); }, []); + if (!ready) { + return ( +
+ +
+ ); + } + + if (trialChoicePageType === "v1") { + return ; + } + return (
{!isLoading && ( diff --git a/src/components/CompatibilityV2/pages/TrialChoice/v1/index.tsx b/src/components/CompatibilityV2/pages/TrialChoice/v1/index.tsx new file mode 100644 index 0000000..04cb50b --- /dev/null +++ b/src/components/CompatibilityV2/pages/TrialChoice/v1/index.tsx @@ -0,0 +1,141 @@ +import { useTranslations } from "@/hooks/translations"; +import styles from "./styles.module.scss"; +import { addCurrency, ELocalesPlacement } from "@/locales"; +import { useNavigate } from "react-router-dom"; +import { useDispatch, useSelector } from "react-redux"; +import { usePaywall } from "@/hooks/paywall/usePaywall"; +import { useEffect, useState } from "react"; +import { actions, selectors } from "@/store"; +import metricService, { EGoals, EMetrics } from "@/services/metric/metricService"; +import routes from "@/routes"; +import Loader from "@/components/Loader"; +import PriceList from "@/components/pages/ABDesign/v1/components/PriceList"; +import { EPlacementKeys } from "@/api/resources/Paywall"; +import Button from "@/components/CompatibilityV2/components/Button"; +import EmailSubstrate from "@/components/CompatibilityV2/components/EmailSubstrate"; +import EmailsList from "@/components/CompatibilityV2/components/EmailsList"; + +const productWeights: Record = { + 100: 1, // 10% + 500: 1, // 10% + 900: 3, // 30% + 1376: 5 // 50% +} + +function TrialChoiceV1() { + const { translate } = useTranslations(ELocalesPlacement.CompatibilityV2); + const navigate = useNavigate(); + const dispatch = useDispatch(); + const { products, isLoading, currency, getText } = usePaywall({ + placementKey: EPlacementKeys["aura.placement.compatibility.v2"], + localesPlacement: ELocalesPlacement.CompatibilityV2, + }); + const popularProduct = products[products.length - 1]; + + // const { flags } = useMetricABFlags(); + // const isLongText = flags?.text?.[0] === "on"; + + const [isDisabled, setIsDisabled] = useState(true); + + const selectedPrice = useSelector(selectors.selectSelectedPrice); + const email = useSelector(selectors.selectEmail); + const homeConfig = useSelector(selectors.selectHome); + + const handlePriceItem = () => { + metricService.reachGoal(EGoals.SELECT_TRIAL, [EMetrics.YANDEX, EMetrics.KLAVIYO]); + metricService.reachGoal(EGoals.AURA_SELECT_TRIAL, [EMetrics.KLAVIYO]); + setIsDisabled(false); + }; + + const handleNext = () => { + if (isDisabled) { + return; + } + dispatch( + actions.siteConfig.update({ + home: { pathFromHome: homeConfig.pathFromHome, isShowNavbar: false }, + }) + ); + navigate(routes.client.compatibilityV2TrialPayment()); + }; + + // useEffect(() => { + // metricService.reachGoal(EGoals.TRIAL_CHOICE_PAGE_VISIT, [EMetrics.YANDEX, EMetrics.KLAVIYO]); + // metricService.reachGoal(EGoals.AURA_TRIAL_CHOICE_PAGE_VISIT, [EMetrics.KLAVIYO]); + // }, []); + + useEffect(() => { + if (popularProduct) { + dispatch(actions.payment.update({ + activeProduct: popularProduct + })) + setIsDisabled(false); + } + }, [popularProduct]) + + return ( +
+ {!isLoading && ( + <> + + {/* {!isLongText && ( + + {getText("text.0")} + + )} */} + {/*

{getLongText(locale)}

*/} +

+ {translate("/trial-choice.v1.paragraph1", { + br:
+ })} +

+
    + {Array.from({ length: 4 }).map((_, index) => ( +
  • + {translate(`/trial-choice.v1.points.point${index + 1}`)} +
  • + ))} +
+

+ {translate("/trial-choice.v1.paragraph2", { + price: addCurrency((Math.max(...products.map(product => product.trialPrice || 0)) / 100).toFixed(2), currency) + })} +

+
+

{getText("text.1")}

+ +
+ + {!!products.length && ({ ...product, weight: productWeights[product.trialPrice || 100] }))} + />} + + + + )} + {isLoading && } +
+ ); +} + +export default TrialChoiceV1 \ No newline at end of file diff --git a/src/components/CompatibilityV2/pages/TrialChoice/v1/styles.module.scss b/src/components/CompatibilityV2/pages/TrialChoice/v1/styles.module.scss new file mode 100644 index 0000000..72fd18c --- /dev/null +++ b/src/components/CompatibilityV2/pages/TrialChoice/v1/styles.module.scss @@ -0,0 +1,160 @@ +.container { + position: relative; + display: flex; + flex-direction: column; + align-items: center; +} + +.email-substrate { + display: grid; + grid-template-columns: 1fr 41px !important; + align-content: center; + position: absolute; + max-width: 100%; + top: -18px; + right: -12px; + padding: 0px !important; + padding-left: 16px !important; + border-radius: 20px !important; + height: 41px !important; + background: transparent !important; + outline: 2px solid #81A9F5 !important; + + &>p { + color: #000000 !important; + font-size: 14px !important; + } + + &>div { + height: 45px !important; + width: 45px !important; + background-color: #81A9F5 !important; + } +} + +.title { + font-size: 28px; + line-height: 125%; + margin-top: 44px; + color: #2c2c2c; + font-weight: 500; +} + +.prices-container { + justify-content: center; + gap: 15px; + width: fit-content; + margin: 0 auto; +} + +.price-item { + border: solid #2c2c2c 1px; + border-radius: 8px; + width: 70px; + height: 65px; + font-size: 20px; + font-weight: 500; + + &:last-child::after { + content: ""; + position: absolute; + width: 16px; + height: 20px; + // background-color: #224e90; + background-image: url("/v1/palmistry/trial-choice/arrow.svg"); + background-size: contain; + background-repeat: no-repeat; + background-position: center; + top: -30px; + left: 50%; + transform: translateX(-50%); + } + + &.price-item-active { + border: solid #224e90 3px !important; + background-color: transparent; + color: #224e90; + } +} + +.price-container { + position: relative; + width: 100%; + margin-top: 16px; + display: flex; + flex-direction: column; + gap: 32px; +} + +.auxiliary-text { + font-size: 15px; + line-height: 125%; + font-weight: 600; + color: #0244a5; + width: 100%; + text-align: center; +} + +.cursor { + position: absolute; + width: 2px; + height: 20px; + background-color: #224e90; + top: 71px; + right: 34px; +} + +.loader { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); +} + +.button { + margin-top: 20px; + transition: background 0.2s ease, color 0.2s ease; + // position: sticky; + // bottom: calc(0dvh + 16px); + + &:disabled { + border: solid #224e90 1px; + background: none; + color: #120d0d; + opacity: 1; + } +} + +.text { + width: calc(100% + 24px); + white-space: pre-wrap; + margin-top: 40px; + margin-bottom: 0px; + font-size: 15px; + font-weight: 500; + line-height: 125%; + color: #2C2C2C; + text-align: center; +} + +.list { + width: calc(100% + 24px); + margin-top: 16px; + margin-bottom: 0px; + display: flex; + flex-direction: column; + gap: 16px; + + li { + font-size: 15px; + font-weight: 500; + line-height: 125%; + color: #2C2C2C; + list-style-type: disc; + margin-left: 24px; + + &::marker { + font-size: 10px; + } + } +} \ No newline at end of file diff --git a/src/components/CompatibilityV3/components/CameraModal/index.tsx b/src/components/CompatibilityV3/components/CameraModal/index.tsx index ffd2ce5..c7cc0c7 100644 --- a/src/components/CompatibilityV3/components/CameraModal/index.tsx +++ b/src/components/CompatibilityV3/components/CameraModal/index.tsx @@ -13,6 +13,18 @@ interface CameraModalProps { isCameraVisible?: boolean; } +interface ExtendedMediaTrackCapabilities extends MediaTrackCapabilities { + torch?: boolean; +} + +interface TorchConstraints extends MediaTrackConstraintSet { + torch?: boolean; +} + +interface ExtendedMediaTrackConstraints extends MediaTrackConstraints { + advanced?: TorchConstraints[]; +} + function CameraModal({ onClose, onTakePhoto, @@ -21,6 +33,8 @@ function CameraModal({ isCameraVisible = true }: CameraModalProps) { const [isVideoReady, setIsVideoReady] = useState(false); + const [isTorchOn, setIsTorchOn] = useState(false); + const [isTorchAvailable, setIsTorchAvailable] = useState(false); // const { // width: _width, height: _height, // elementRef: containerRef } = useDynamicSize({}); @@ -55,6 +69,47 @@ function CameraModal({ } }; + const checkTorchAvailability = async (track: MediaStreamTrack) => { + try { + const capabilities = track.getCapabilities() as ExtendedMediaTrackCapabilities; + setIsTorchAvailable(!!capabilities.torch); + } catch (error) { + setIsTorchAvailable(false); + console.error('Ошибка при проверке поддержки фонарика:', error); + } + }; + + const onUserMedia = (stream: MediaStream) => { + setIsVideoReady(true); + const track = stream.getVideoTracks()[0]; + if (track) { + checkTorchAvailability(track); + } + }; + + const toggleTorch = async () => { + try { + const track = cameraRef.current?.video?.srcObject instanceof MediaStream + ? (cameraRef.current.video.srcObject as MediaStream).getVideoTracks()[0] + : null; + + if (track) { + const capabilities = track.getCapabilities() as ExtendedMediaTrackCapabilities; + if (capabilities.torch) { + const constraints: ExtendedMediaTrackConstraints = { + advanced: [{ torch: !isTorchOn }] + }; + await track.applyConstraints(constraints); + setIsTorchOn(!isTorchOn); + } else { + onError("Вспышка не поддерживается на этом устройстве"); + } + } + } catch (error) { + onError(error instanceof Error ? error.message : "Ошибка при управлении вспышкой"); + } + }; + return setIsVideoReady(true)} + onUserMedia={onUserMedia} onUserMediaError={(error) => { setIsVideoReady(false); + setIsTorchAvailable(false); console.error(error); onError(error); }} @@ -103,6 +159,29 @@ function CameraModal({ opacity: isVideoReady ? 1 : 0.5, }} /> + {isTorchAvailable && ( + + )}
; diff --git a/src/components/CompatibilityV3/components/CameraModal/styles.module.scss b/src/components/CompatibilityV3/components/CameraModal/styles.module.scss index 7cf7f91..31585dd 100644 --- a/src/components/CompatibilityV3/components/CameraModal/styles.module.scss +++ b/src/components/CompatibilityV3/components/CameraModal/styles.module.scss @@ -63,4 +63,43 @@ transform: translate(-50%); width: auto; z-index: 9; +} + +.torchButton { + position: fixed; + bottom: calc(0dvh + 22px); + right: 20px; + width: 50px; + height: 50px; + border-radius: 25px; + background: rgba(0, 0, 0, 0.5); + border: none; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + transition: all 0.3s ease; + + &:active { + transform: scale(0.95); + } + + &>.torchIconContainer { + display: block; + z-index: 10; + width: 38px; + height: 38px; + } + + &.torchButtonActive { + background-color: #fff; + + &.torchIcon>path { + fill: #000; + + &:last-child { + transform: translate(0, -64px); + } + } + } } \ No newline at end of file diff --git a/src/components/CompatibilityV3/pages/Gender/index.tsx b/src/components/CompatibilityV3/pages/Gender/index.tsx index c81b05d..1a97be1 100644 --- a/src/components/CompatibilityV3/pages/Gender/index.tsx +++ b/src/components/CompatibilityV3/pages/Gender/index.tsx @@ -38,7 +38,7 @@ function GenderPage() { }); const { flags, ready } = useMetricABFlags(); - const pageType = flags?.genderPageType?.[0]; + const pageType = flags?.genderPageType?.[0] || "v2"; const localGenders = genders.map((gender) => ({ id: gender.id, title: translate(gender.id, undefined, ELocalesPlacement.V1), @@ -97,6 +97,35 @@ function GenderPage() { if (!ready) return ; switch (pageType) { + case "v0": + return ( + <> + + {translate("/gender.title")} + +

{translate("/gender.description", { + br:
, + })}

+ {/* */} + +
+ {localGenders.map((_gender, index) => ( + selectGender(genders.find((g) => g.id === _gender.id) ?? null)} + /> + ))} +
+ + {/* {gender && !privacyPolicyChecked && ( + + {translate("/gender.toast", undefined, ELocalesPlacement.V1)} + + )} */} + + ) case "v1": return ( <> diff --git a/src/components/CompatibilityV3/pages/HeadOrHeart/index.tsx b/src/components/CompatibilityV3/pages/HeadOrHeart/index.tsx index c609929..534dee1 100644 --- a/src/components/CompatibilityV3/pages/HeadOrHeart/index.tsx +++ b/src/components/CompatibilityV3/pages/HeadOrHeart/index.tsx @@ -37,6 +37,9 @@ function HeadOrHeart() { useLottie({ preloadKey: ELottieKeys.letScan, }); + useLottie({ + preloadKey: ELottieKeys.scannedPhoto, + }); const answers: { id: IAnswersSessionCompatibilityV3["head_or_heart"]; title: string }[] = useMemo( diff --git a/src/components/CompatibilityV3/pages/ScannedPhoto/index.tsx b/src/components/CompatibilityV3/pages/ScannedPhoto/index.tsx index 4128360..fbc23e1 100644 --- a/src/components/CompatibilityV3/pages/ScannedPhoto/index.tsx +++ b/src/components/CompatibilityV3/pages/ScannedPhoto/index.tsx @@ -14,6 +14,8 @@ import ProgressBarLine from "@/components/ui/ProgressBarLine"; import Modal from "@/components/Modal"; import { useAuthentication } from "@/hooks/authentication/use-authentication"; import { ESourceAuthorization } from "@/api/resources/User"; +import { ELottieKeys, useLottie } from "@/hooks/lottie/useLottie"; +import { DotLottieReact } from "@lottiefiles/dotlottie-react"; // const drawElementChangeDelay = 1500; // const startDelay = 500; @@ -163,6 +165,10 @@ function ScannedPhoto() { // }, [currentElementIndex, drawElements.length, navigate]); + const { animationData } = useLottie({ + loadKey: ELottieKeys.scannedPhoto, + }); + const [progress, setProgress] = useState(0); const [isPause, setIsPause] = useState(false); const interval = useRef(); @@ -287,6 +293,16 @@ function ScannedPhoto() { )} +
+ {animationData && + } +
{!isDecorationShown &&
{loadingProfilePoints.map(({ title1, title2 }, index) => (
{translate(getProgressValue(index) > 50 ? title2 : title1)} - +

+ {getProgressValue(index)}% +

-

- {getProgressValue(index)}% -

+
))} } diff --git a/src/components/CompatibilityV3/pages/ScannedPhoto/styles.module.scss b/src/components/CompatibilityV3/pages/ScannedPhoto/styles.module.scss index dd7389b..502378e 100644 --- a/src/components/CompatibilityV3/pages/ScannedPhoto/styles.module.scss +++ b/src/components/CompatibilityV3/pages/ScannedPhoto/styles.module.scss @@ -1,6 +1,6 @@ .page { width: 100%; - padding: 0 16px 16px; + padding: 0 0 16px; margin: 0 auto; max-width: 560px; height: fit-content; @@ -228,31 +228,29 @@ width: 100%; display: flex; flex-direction: column; - margin-top: 24px; -} - -.points-container { - padding: 0 17px; - gap: 50px; + margin-top: 50px; + padding: 0; + gap: 40px; } .point { display: flex; - flex-direction: row; - align-items: flex-end; - justify-content: space-between; - gap: 20px; + flex-direction: column; + gap: 8px; } .point__text-container { - width: calc(100% - 60px); + width: calc(100%); display: flex; - flex-direction: column; + justify-content: space-between; + align-items: flex-end; + flex-direction: row; gap: 4px; } .point__title { - font-size: 15px; + font-size: 20px; + font-weight: 600; line-height: 125%; font-weight: normal; margin: 0; @@ -261,7 +259,7 @@ } .point__percentage { - font-size: 16px; + font-size: 22px; font-weight: 600; line-height: 125%; color: #275DA6; @@ -269,11 +267,13 @@ .progress-bar__container { background-color: #e6e6e6; - height: 8px; + height: 38px; + border-radius: 10px; } .progress-bar__line { background-color: #908cf2; + border-radius: 10px; } .modal-container { @@ -312,4 +312,14 @@ &:first-child { border-right: 1px solid #D9D9D9; } +} + +.lottie-animation-container { + width: 160px; + min-height: 160px; +} + +.lottie-animation { + aspect-ratio: 1; + width: 160px; } \ No newline at end of file diff --git a/src/components/PalmistryV1/components/CameraModal/index.tsx b/src/components/PalmistryV1/components/CameraModal/index.tsx index ffd2ce5..686d480 100644 --- a/src/components/PalmistryV1/components/CameraModal/index.tsx +++ b/src/components/PalmistryV1/components/CameraModal/index.tsx @@ -13,6 +13,19 @@ interface CameraModalProps { isCameraVisible?: boolean; } +interface ExtendedMediaTrackCapabilities extends MediaTrackCapabilities { + torch?: boolean; +} + +interface TorchConstraints extends MediaTrackConstraintSet { + torch?: boolean; +} + +interface ExtendedMediaTrackConstraints extends MediaTrackConstraints { + advanced?: TorchConstraints[]; +} + + function CameraModal({ onClose, onTakePhoto, @@ -21,6 +34,8 @@ function CameraModal({ isCameraVisible = true }: CameraModalProps) { const [isVideoReady, setIsVideoReady] = useState(false); + const [isTorchOn, setIsTorchOn] = useState(false); + const [isTorchAvailable, setIsTorchAvailable] = useState(false); // const { // width: _width, height: _height, // elementRef: containerRef } = useDynamicSize({}); @@ -55,6 +70,47 @@ function CameraModal({ } }; + const checkTorchAvailability = async (track: MediaStreamTrack) => { + try { + const capabilities = track.getCapabilities() as ExtendedMediaTrackCapabilities; + setIsTorchAvailable(!!capabilities.torch); + } catch (error) { + setIsTorchAvailable(false); + console.error('Ошибка при проверке поддержки фонарика:', error); + } + }; + + const onUserMedia = (stream: MediaStream) => { + setIsVideoReady(true); + const track = stream.getVideoTracks()[0]; + if (track) { + checkTorchAvailability(track); + } + }; + + const toggleTorch = async () => { + try { + const track = cameraRef.current?.video?.srcObject instanceof MediaStream + ? (cameraRef.current.video.srcObject as MediaStream).getVideoTracks()[0] + : null; + + if (track) { + const capabilities = track.getCapabilities() as ExtendedMediaTrackCapabilities; + if (capabilities.torch) { + const constraints: ExtendedMediaTrackConstraints = { + advanced: [{ torch: !isTorchOn }] + }; + await track.applyConstraints(constraints); + setIsTorchOn(!isTorchOn); + } else { + onError("Вспышка не поддерживается на этом устройстве"); + } + } + } catch (error) { + onError(error instanceof Error ? error.message : "Ошибка при управлении вспышкой"); + } + }; + return setIsVideoReady(true)} + onUserMedia={onUserMedia} onUserMediaError={(error) => { setIsVideoReady(false); + setIsTorchAvailable(false); console.error(error); onError(error); }} @@ -103,6 +160,29 @@ function CameraModal({ opacity: isVideoReady ? 1 : 0.5, }} /> + {isTorchAvailable && ( + + )} ; diff --git a/src/components/PalmistryV1/components/CameraModal/styles.module.scss b/src/components/PalmistryV1/components/CameraModal/styles.module.scss index 7cf7f91..31585dd 100644 --- a/src/components/PalmistryV1/components/CameraModal/styles.module.scss +++ b/src/components/PalmistryV1/components/CameraModal/styles.module.scss @@ -63,4 +63,43 @@ transform: translate(-50%); width: auto; z-index: 9; +} + +.torchButton { + position: fixed; + bottom: calc(0dvh + 22px); + right: 20px; + width: 50px; + height: 50px; + border-radius: 25px; + background: rgba(0, 0, 0, 0.5); + border: none; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + transition: all 0.3s ease; + + &:active { + transform: scale(0.95); + } + + &>.torchIconContainer { + display: block; + z-index: 10; + width: 38px; + height: 38px; + } + + &.torchButtonActive { + background-color: #fff; + + &.torchIcon>path { + fill: #000; + + &:last-child { + transform: translate(0, -64px); + } + } + } } \ No newline at end of file diff --git a/src/components/PalmistryV1/pages/Camera/index.tsx b/src/components/PalmistryV1/pages/Camera/index.tsx index aaf489e..13ac262 100644 --- a/src/components/PalmistryV1/pages/Camera/index.tsx +++ b/src/components/PalmistryV1/pages/Camera/index.tsx @@ -14,7 +14,7 @@ import Toast from "@/components/pages/ABDesign/v1/components/Toast"; import { useTranslations } from "@/hooks/translations"; import { ELocalesPlacement } from "@/locales"; import CameraModal from "../../components/CameraModal"; -import metricService, { EGoals, EMetrics } from "@/services/metric/metricService"; +import metricService, { EGoals, EMetrics, useMetricABFlags } from "@/services/metric/metricService"; import Modal from "@/components/Modal"; import Title from "@/components/Title"; @@ -47,10 +47,13 @@ function Camera() { const [isLoading, setIsLoading] = useState(false); const [uploadMenuModalIsOpen, setUploadMenuModalIsOpen] = useState(false); const [toastVisible, setToastVisible] = useState(null); - const [isRequestCameraModalOpen, setIsRequestCameraModalOpen] = useState(isIphoneSafari ? false : true); const [cameraKey, setCameraKey] = useState(0); const [isCameraModalOpen, setIsCameraModalOpen] = useState(false); + const { flags, ready } = useMetricABFlags(); + const isCameraRequestModal = flags?.cameraRequestModal?.[0] !== "without"; + const [isRequestCameraModalOpen, setIsRequestCameraModalOpen] = useState((isIphoneSafari || !isCameraRequestModal) ? false : true); + const handleNext = () => { metricService.reachGoal(EGoals.CAMERA_SUCCESS, [EMetrics.YANDEX, EMetrics.KLAVIYO]); navigate(routes.client.palmistryV1ScannedPhoto()); @@ -185,7 +188,7 @@ function Camera() { const cameraError = (error: string | DOMException) => { console.error("Camera error", error) - if (!isIphoneSafari) return; + if (!isIphoneSafari || !isCameraRequestModal) return; if (error === "Video is not ready") { return setToastVisible(EToastVisible.no_access_camera) } @@ -210,6 +213,8 @@ function Camera() { } }; + if (!ready) return null; + return ( <> console.log("close")} onTakePhoto={onTakePhoto} onError={cameraError} - isCameraVisible={isIphoneSafari ? true : isCameraModalOpen} + isCameraVisible={(isIphoneSafari || isCameraRequestModal) ? true : isCameraModalOpen} reinitializeKey={cameraKey} /> )} diff --git a/src/hooks/lottie/useLottie.ts b/src/hooks/lottie/useLottie.ts index 677e126..1658992 100644 --- a/src/hooks/lottie/useLottie.ts +++ b/src/hooks/lottie/useLottie.ts @@ -22,6 +22,7 @@ export enum ELottieKeys { scalesHeadPalmistry = "scalesHeadPalmistry", scalesHeartPalmistry = "scalesHeartPalmistry", letScan = "letScan", + scannedPhoto = "scannedPhoto", } export const lottieUrls = { @@ -44,6 +45,7 @@ export const lottieUrls = { [ELottieKeys.scalesHeadPalmistry]: "https://lottie.host/d16336c4-2622-48f8-b361-8d9d50b3c8a6/wWSM7JMCHu.lottie", [ELottieKeys.scalesHeartPalmistry]: "https://lottie.host/fa931c2d-07f5-4c57-a4bb-8302b411ecca/zy9ag3MyMe.lottie", [ELottieKeys.letScan]: "https://lottie.host/f87184ec-aa5e-4cf4-82a5-9ab5e60c22d5/qpgweCSCtn.lottie", + [ELottieKeys.scannedPhoto]: "https://lottie.host/a63a6fa4-420d-42b9-a202-cf6939f554a7/A4ZU7LYWa3.lottie", } interface IUseLottieProps { diff --git a/src/services/metric/metricService.ts b/src/services/metric/metricService.ts index 999de30..ea5dc1f 100644 --- a/src/services/metric/metricService.ts +++ b/src/services/metric/metricService.ts @@ -209,9 +209,10 @@ type TABFlags = { auraPalmistry: "on"; esFlag: "hiCopy" | "standard"; palmOnPayment: "graphical" | "real"; - genderPageType: "v1" | "v2"; + genderPageType: "v0" | "v1" | "v2"; trialChoicePageType: "v1" | "v2"; welcomePageImage: "v1" | "v2"; + cameraRequestModal: "with" | "without"; } export const useMetricABFlags = () => {