develop
This commit is contained in:
parent
a29d8e6fec
commit
8083d7ac9c
@ -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<HTMLDivElement>({});
|
||||
@ -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 <ModalOverlay
|
||||
type={ModalOverlayType.Dark}
|
||||
className={styles.overlay}
|
||||
@ -89,9 +144,10 @@ function CameraModal({
|
||||
// height,
|
||||
// aspectRatio: ratio,
|
||||
}}
|
||||
onUserMedia={() => 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 && (
|
||||
<button
|
||||
className={`${styles.torchButton} ${isTorchOn ? styles.torchButtonActive : ""}`}
|
||||
onClick={toggleTorch}
|
||||
style={{
|
||||
opacity: isVideoReady ? 1 : 0.5,
|
||||
}}
|
||||
>
|
||||
<div className={styles.torchIconContainer}>
|
||||
<svg
|
||||
className={styles.torchIcon}
|
||||
width="38"
|
||||
height="38"
|
||||
viewBox="0 0 1024 1024"
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path fill="#fff" d="M634 64H390c-35.2 0-48 28.8-48 64h340c0-35.2-12.8-64-48-64zM392.2 295c15.2 17.6 23.8 40 23.8 63.4v531.8c0 43.8 35.8 69.8 79.8 69.8h32.6c43.8 0 79.8-25.8 79.8-69.8V358.4c0-23.4 8.6-45.6 23.8-63.4 30.8-35.8 50-69 50-135H342c0 70 19.2 99.2 50.2 135z m63.8 181.6c0-31.2 25.2-56.6 56-56.6s56 25.4 56 56.6v70.8c0 31.2-25.2 56.6-56 56.6s-56-25.4-56-56.6v-70.8z" />
|
||||
<path fill="#fff" d="M512 546m-40 0a40 40 0 1 0 80 0 40 40 0 1 0-80 0Z" />
|
||||
</svg>
|
||||
</div>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</Modal>
|
||||
</ModalOverlay>;
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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<IPaywallProduct & { weight: number }>
|
||||
}
|
||||
|
||||
function EmailsList({ products }: IEmailsListProps) {
|
||||
const { translate } = useTranslations(ELocalesPlacement.CompatibilityV2);
|
||||
|
||||
const {
|
||||
displayEmails,
|
||||
countBoughtEmails
|
||||
} = useEmailsGeneration(products);
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.header}>
|
||||
<Title className={styles.title} variant="h2">
|
||||
{translate("/trial-choice.v1.emails_list.title", {
|
||||
count: countBoughtEmails
|
||||
})}
|
||||
</Title>
|
||||
</div>
|
||||
<p className={styles.description}>
|
||||
{translate("/trial-choice.v1.emails_list.description", {
|
||||
count: displayEmails?.length
|
||||
})}
|
||||
</p>
|
||||
<div className={styles.emails}>
|
||||
{displayEmails.map((item) => (
|
||||
<div key={item.id} className={`${styles.emailContainer} ${item.willBeRemoved ? styles.removed : ""}`}>
|
||||
<div className={styles.priceContainer}>
|
||||
{item.willBeRemoved && (
|
||||
<svg width="17" height="17" viewBox="0 0 17 17" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect
|
||||
width="17"
|
||||
height="17"
|
||||
rx="8.5"
|
||||
fill="#62DFA1"
|
||||
/>
|
||||
<path d="M7.2586 12C7.08625 12 6.9139 11.9285 6.78205 11.785L4.19724 8.96395C4.13481 8.89628 4.0852 8.8154 4.05132 8.7261C4.01745 8.63679 4 8.54086 4 8.44395C4 8.34704 4.01745 8.2511 4.05132 8.1618C4.0852 8.07249 4.13481 7.99162 4.19724 7.92394C4.32473 7.78583 4.49574 7.70853 4.67379 7.70853C4.85184 7.70853 5.02285 7.78583 5.15034 7.92394L7.2586 10.2245L11.8491 5.21541C11.9766 5.0773 12.1476 5 12.3256 5C12.5037 5 12.6747 5.0773 12.8022 5.21541C12.8648 5.28298 12.9145 5.36382 12.9485 5.45314C12.9825 5.54246 13 5.63844 13 5.73541C13 5.83239 12.9825 5.92837 12.9485 6.01769C12.9145 6.10701 12.8648 6.18785 12.8022 6.25542L7.73515 11.7845C7.60814 11.9234 7.43683 12.0009 7.2586 12Z" fill="#F4F4F4" />
|
||||
</svg>
|
||||
)}
|
||||
<div className={styles.price}>
|
||||
${item.price.toFixed(2)}
|
||||
</div>
|
||||
</div>
|
||||
<p className={styles.email}>
|
||||
{item.email}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default EmailsList
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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 <Loader color={LoaderColor.Black} />;
|
||||
|
||||
switch (pageType) {
|
||||
case "v0":
|
||||
return (
|
||||
<>
|
||||
<Title variant="h2" className={styles.title}>
|
||||
{translate("/gender.title")}
|
||||
</Title>
|
||||
<p className={styles.description}>{translate("/gender.description", {
|
||||
br: <br />,
|
||||
})}</p>
|
||||
{/* <ChooseGender onSelectGender={selectGender} /> */}
|
||||
<PrivacyPolicy containerClassName={styles["privacy-policy"]} haveCheckbox={false} />
|
||||
<div className={styles["genders-container"]}>
|
||||
{localGenders.map((_gender, index) => (
|
||||
<Answer
|
||||
key={index}
|
||||
answer={_gender}
|
||||
isSelected={gender === _gender.id}
|
||||
onClick={() => selectGender(genders.find((g) => g.id === _gender.id) ?? null)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<AlreadyHaveAccount text={translate("/gender.already_have_account")} />
|
||||
{/* {gender && !privacyPolicyChecked && (
|
||||
<Toast classNameContainer={styles["toast-container"]} variant="error">
|
||||
{translate("/gender.toast", undefined, ELocalesPlacement.V1)}
|
||||
</Toast>
|
||||
)} */}
|
||||
</>
|
||||
)
|
||||
case "v1":
|
||||
return (
|
||||
<>
|
||||
|
||||
@ -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 (
|
||||
<div className={styles.container}>
|
||||
<Loader className={styles.loader} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (trialChoicePageType === "v1") {
|
||||
return <TrialChoiceV1 />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
{!isLoading && (
|
||||
|
||||
141
src/components/CompatibilityV2/pages/TrialChoice/v1/index.tsx
Normal file
141
src/components/CompatibilityV2/pages/TrialChoice/v1/index.tsx
Normal file
@ -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<number, number> = {
|
||||
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 (
|
||||
<div className={styles.container}>
|
||||
{!isLoading && (
|
||||
<>
|
||||
<EmailSubstrate className={styles["email-substrate"]} email={email} />
|
||||
{/* {!isLongText && (
|
||||
<Title className={styles.title} variant="h2">
|
||||
{getText("text.0")}
|
||||
</Title>
|
||||
)} */}
|
||||
{/* <p className={styles.text}>{getLongText(locale)}</p> */}
|
||||
<p className={styles.text}>
|
||||
{translate("/trial-choice.v1.paragraph1", {
|
||||
br: <br />
|
||||
})}
|
||||
</p>
|
||||
<ul className={styles.list}>
|
||||
{Array.from({ length: 4 }).map((_, index) => (
|
||||
<li key={index} className={styles.listItem}>
|
||||
{translate(`/trial-choice.v1.points.point${index + 1}`)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<p
|
||||
className={styles.text}
|
||||
style={{
|
||||
marginTop: "16px"
|
||||
}}
|
||||
>
|
||||
{translate("/trial-choice.v1.paragraph2", {
|
||||
price: addCurrency((Math.max(...products.map(product => product.trialPrice || 0)) / 100).toFixed(2), currency)
|
||||
})}
|
||||
</p>
|
||||
<div className={styles["price-container"]}>
|
||||
<p className={styles["auxiliary-text"]}>{getText("text.1")}</p>
|
||||
<PriceList
|
||||
products={products}
|
||||
activeItem={selectedPrice}
|
||||
classNameItem={styles["price-item"]}
|
||||
classNameItemActive={`${styles["price-item-active"]}`}
|
||||
classNamePricesContainer={styles["prices-container"]}
|
||||
currency={currency}
|
||||
click={handlePriceItem}
|
||||
preActiveItems={[popularProduct?._id]}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{!!products.length && <EmailsList
|
||||
products={products.map(product => ({ ...product, weight: productWeights[product.trialPrice || 100] }))}
|
||||
/>}
|
||||
|
||||
<Button
|
||||
className={styles.button}
|
||||
disabled={isDisabled}
|
||||
onClick={handleNext}
|
||||
>
|
||||
{translate("next")}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
{isLoading && <Loader className={styles.loader} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default TrialChoiceV1
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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<HTMLDivElement>({});
|
||||
@ -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 <ModalOverlay
|
||||
type={ModalOverlayType.Dark}
|
||||
className={styles.overlay}
|
||||
@ -89,9 +144,10 @@ function CameraModal({
|
||||
// height,
|
||||
// aspectRatio: ratio,
|
||||
}}
|
||||
onUserMedia={() => 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 && (
|
||||
<button
|
||||
className={`${styles.torchButton} ${isTorchOn ? styles.torchButtonActive : ""}`}
|
||||
onClick={toggleTorch}
|
||||
style={{
|
||||
opacity: isVideoReady ? 1 : 0.5,
|
||||
}}
|
||||
>
|
||||
<div className={styles.torchIconContainer}>
|
||||
<svg
|
||||
className={styles.torchIcon}
|
||||
width="38"
|
||||
height="38"
|
||||
viewBox="0 0 1024 1024"
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path fill="#fff" d="M634 64H390c-35.2 0-48 28.8-48 64h340c0-35.2-12.8-64-48-64zM392.2 295c15.2 17.6 23.8 40 23.8 63.4v531.8c0 43.8 35.8 69.8 79.8 69.8h32.6c43.8 0 79.8-25.8 79.8-69.8V358.4c0-23.4 8.6-45.6 23.8-63.4 30.8-35.8 50-69 50-135H342c0 70 19.2 99.2 50.2 135z m63.8 181.6c0-31.2 25.2-56.6 56-56.6s56 25.4 56 56.6v70.8c0 31.2-25.2 56.6-56 56.6s-56-25.4-56-56.6v-70.8z" />
|
||||
<path fill="#fff" d="M512 546m-40 0a40 40 0 1 0 80 0 40 40 0 1 0-80 0Z" />
|
||||
</svg>
|
||||
</div>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</Modal>
|
||||
</ModalOverlay>;
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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 <Loader color={LoaderColor.Black} />;
|
||||
|
||||
switch (pageType) {
|
||||
case "v0":
|
||||
return (
|
||||
<>
|
||||
<Title variant="h2" className={styles.title}>
|
||||
{translate("/gender.title")}
|
||||
</Title>
|
||||
<p className={styles.description}>{translate("/gender.description", {
|
||||
br: <br />,
|
||||
})}</p>
|
||||
{/* <ChooseGender onSelectGender={selectGender} /> */}
|
||||
<PrivacyPolicy containerClassName={styles["privacy-policy"]} haveCheckbox={false} />
|
||||
<div className={styles["genders-container"]}>
|
||||
{localGenders.map((_gender, index) => (
|
||||
<Answer
|
||||
key={index}
|
||||
answer={_gender}
|
||||
isSelected={gender === _gender.id}
|
||||
onClick={() => selectGender(genders.find((g) => g.id === _gender.id) ?? null)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<AlreadyHaveAccount text={translate("/gender.already_have_account")} />
|
||||
{/* {gender && !privacyPolicyChecked && (
|
||||
<Toast classNameContainer={styles["toast-container"]} variant="error">
|
||||
{translate("/gender.toast", undefined, ELocalesPlacement.V1)}
|
||||
</Toast>
|
||||
)} */}
|
||||
</>
|
||||
)
|
||||
case "v1":
|
||||
return (
|
||||
<>
|
||||
|
||||
@ -37,6 +37,9 @@ function HeadOrHeart() {
|
||||
useLottie({
|
||||
preloadKey: ELottieKeys.letScan,
|
||||
});
|
||||
useLottie({
|
||||
preloadKey: ELottieKeys.scannedPhoto,
|
||||
});
|
||||
|
||||
const answers: { id: IAnswersSessionCompatibilityV3["head_or_heart"]; title: string }[] =
|
||||
useMemo(
|
||||
|
||||
@ -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<NodeJS.Timeout>();
|
||||
@ -287,6 +293,16 @@ function ScannedPhoto() {
|
||||
</div>
|
||||
</Modal>
|
||||
)}
|
||||
<div className={styles["lottie-animation-container"]}>
|
||||
{animationData &&
|
||||
<DotLottieReact
|
||||
className={`${styles["lottie-animation"]} ym-hide-content`}
|
||||
data={animationData}
|
||||
autoplay
|
||||
loop={true}
|
||||
width={323}
|
||||
/>}
|
||||
</div>
|
||||
{!isDecorationShown && <div className={styles["points-container"]}>
|
||||
{loadingProfilePoints.map(({ title1, title2 }, index) => (
|
||||
<div
|
||||
@ -299,19 +315,19 @@ function ScannedPhoto() {
|
||||
<Title variant="h2" className={styles["point__title"]}>
|
||||
{translate(getProgressValue(index) > 50 ? title2 : title1)}
|
||||
</Title>
|
||||
<ProgressBarLine
|
||||
containerClassName={styles["progress-bar__container"]}
|
||||
lineClassName={styles["progress-bar__line"]}
|
||||
lineColor={"#275DA7"}
|
||||
value={getProgressValue(index)}
|
||||
delay={50}
|
||||
/>
|
||||
<p
|
||||
className={styles["point__percentage"]}
|
||||
>
|
||||
{getProgressValue(index)}%
|
||||
</p>
|
||||
</div>
|
||||
<p
|
||||
className={styles["point__percentage"]}
|
||||
>
|
||||
{getProgressValue(index)}%
|
||||
</p>
|
||||
<ProgressBarLine
|
||||
containerClassName={styles["progress-bar__container"]}
|
||||
lineClassName={styles["progress-bar__line"]}
|
||||
lineColor={"#275DA7"}
|
||||
value={getProgressValue(index)}
|
||||
delay={50}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
@ -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<HTMLDivElement>({});
|
||||
@ -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 <ModalOverlay
|
||||
type={ModalOverlayType.Dark}
|
||||
className={styles.overlay}
|
||||
@ -89,9 +145,10 @@ function CameraModal({
|
||||
// height,
|
||||
// aspectRatio: ratio,
|
||||
}}
|
||||
onUserMedia={() => 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 && (
|
||||
<button
|
||||
className={`${styles.torchButton} ${isTorchOn ? styles.torchButtonActive : ""}`}
|
||||
onClick={toggleTorch}
|
||||
style={{
|
||||
opacity: isVideoReady ? 1 : 0.5,
|
||||
}}
|
||||
>
|
||||
<div className={styles.torchIconContainer}>
|
||||
<svg
|
||||
className={styles.torchIcon}
|
||||
width="38"
|
||||
height="38"
|
||||
viewBox="0 0 1024 1024"
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path fill="#fff" d="M634 64H390c-35.2 0-48 28.8-48 64h340c0-35.2-12.8-64-48-64zM392.2 295c15.2 17.6 23.8 40 23.8 63.4v531.8c0 43.8 35.8 69.8 79.8 69.8h32.6c43.8 0 79.8-25.8 79.8-69.8V358.4c0-23.4 8.6-45.6 23.8-63.4 30.8-35.8 50-69 50-135H342c0 70 19.2 99.2 50.2 135z m63.8 181.6c0-31.2 25.2-56.6 56-56.6s56 25.4 56 56.6v70.8c0 31.2-25.2 56.6-56 56.6s-56-25.4-56-56.6v-70.8z" />
|
||||
<path fill="#fff" d="M512 546m-40 0a40 40 0 1 0 80 0 40 40 0 1 0-80 0Z" />
|
||||
</svg>
|
||||
</div>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</Modal>
|
||||
</ModalOverlay>;
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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<EToastVisible | null>(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 (
|
||||
<>
|
||||
<Modal
|
||||
@ -269,7 +274,7 @@ function Camera() {
|
||||
onClose={() => console.log("close")}
|
||||
onTakePhoto={onTakePhoto}
|
||||
onError={cameraError}
|
||||
isCameraVisible={isIphoneSafari ? true : isCameraModalOpen}
|
||||
isCameraVisible={(isIphoneSafari || isCameraRequestModal) ? true : isCameraModalOpen}
|
||||
reinitializeKey={cameraKey}
|
||||
/>
|
||||
)}
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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 = () => {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user