AW-429-comp-v1-ios-ab

This commit is contained in:
Daniil Chemerkin 2025-04-01 13:07:55 +00:00
parent 757524c295
commit e66d724572
23 changed files with 715 additions and 38 deletions

View File

@ -275,7 +275,9 @@
},
"/scan-instruction": {
"title": "Photograph Your Palm as Shown",
"button": "Take Photo Now"
"button": "Take Photo Now",
"upload_photo": "Upload palm photo",
"error": "Something went wrong. Please try again."
},
"/email": {
"title": "Enter Your Email to Receive a Detailed Palmistry Compatibility Analysis!",
@ -490,6 +492,7 @@
"choose_from": "Choose from over 80 expert astrologers."
},
"/camera": {
"upload": "Upload",
"bad_photo": "Bad Photo!",
"try_again": "Try Again",
"do_better": "You Can Do Better",
@ -561,5 +564,8 @@
"capricorn": "Capricorn",
"aquarius": "Aquarius",
"pisces": "Pisces"
},
"/scan-hand": {
"title": "Приложите ладонь к экрану телефона"
}
}

View File

@ -145,7 +145,9 @@
"biometric_data": "We do not collect biometric data. The entire recognition process happens on your device.",
"/scan-instruction": {
"title": "Photograph your palm as shown",
"button": "Take a photo now"
"button": "Take a photo now",
"upload_photo": "Upload palm photo",
"error": "Something went wrong. Please try again."
},
"/email": {
"title": "Enter your email to receive an extended palmistry analysis with AURA",
@ -277,6 +279,7 @@
"app_number_one": "The #1 Astrology app trusted by over <color>"
},
"/camera": {
"upload": "Upload",
"bad_photo": "Bad Photo!",
"try_again": "Try Again",
"do_better": "You Can Do Better",

Binary file not shown.

After

Width:  |  Height:  |  Size: 129 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 531 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 667 KiB

View File

@ -2,6 +2,7 @@ import routes from "@/routes";
export interface Payload {
formData: FormData;
gender?: "male" | "female";
}
export type Response = {
@ -25,8 +26,9 @@ export interface IPalmistryPoint {
y: number;
}
export const createRequest = ({ formData }: Payload) => {
const url = new URL(routes.server.getPalmistryLines());
export const createRequest = ({ formData, gender }: Payload) => {
const urlString = !!gender?.length ? `${routes.server.getPalmistryLines()}/${gender}` : routes.server.getPalmistryLines();
const url = new URL(urlString);
const body = formData;
return new Request(url, {
method: "POST",

View File

@ -54,7 +54,12 @@ function Camera() {
const { isReady, variant: cameraRequestModalCompatibilityV2 } = useUnleash({
flag: EUnleashFlags.cameraRequestModalCompatibilityV2
});
const { variant: compatibilityV2ScanHand } = useUnleash({
flag: EUnleashFlags.compatibilityV2ScanHand
});
const isScanHand = compatibilityV2ScanHand === "show";
const isCameraRequestModal = cameraRequestModalCompatibilityV2 !== "hide";
const [isRequestCameraModalOpen, setIsRequestCameraModalOpen] = useState((isIphoneSafari || !isCameraRequestModal) ? false : true);
@ -160,6 +165,7 @@ function Camera() {
};
const onSelectFile = async (event: React.ChangeEvent<HTMLInputElement>) => {
setToastVisible(null);
setUploadMenuModalIsOpen(false);
if (!event.target.files || event.target.files.length === 0) return;
@ -193,6 +199,14 @@ function Camera() {
const cameraError = (error: string | DOMException) => {
console.error("Camera error", error)
if (isScanHand) {
if (!isIphoneSafari) {
return navigate(routes.client.compatibilityV2ScanHand())
}
if (isIphoneSafari && cameraKey > 1) {
return navigate(routes.client.compatibilityV2ScanHand())
}
}
if (!isIphoneSafari || !isCameraRequestModal) return;
if (error === "Video is not ready") {
return setToastVisible(EToastVisible.no_access_camera)
@ -247,6 +261,9 @@ function Camera() {
requestCameraPermission()
} else {
setIsCameraModalOpen(true)
if (window.navigator.userAgent.includes("Instagram")) {
setToastVisible(EToastVisible.no_access_camera)
}
}
}}>
<p className={styles["modal-answer-text"]}>
@ -290,9 +307,27 @@ function Camera() {
<Toast classNameContainer={styles["toast-container"]} variant="error">
<div className={styles["toast-content"]}>
<span>{translate("/camera.bad_photo")}</span>
<button onClick={() => setToastVisible(null)}>
{translate("/camera.try_again")}
</button>
<div className={styles["toast-buttons-container"]}>
<button onClick={() => setToastVisible(null)}>
{translate("/camera.try_again")}
</button>
<button className={styles.buttonUpload}>
<input
id="upload-input"
type="file"
accept="image/*"
style={{ display: 'none' }}
onChange={onSelectFile}
/>
<label htmlFor="upload-input" className={styles.labelUpload} onClick={() => {
if (!isIphoneSafari) {
return navigate(routes.client.compatibilityV2ScanHand());
}
}}>
{translate("/camera.upload")}
</label>
</button>
</div>
</div>
</Toast>
)}
@ -300,16 +335,35 @@ function Camera() {
<Toast classNameContainer={styles["toast-container"]} variant="error">
<div className={styles["toast-content"]}>
<span>{translate("/camera.no_access_camera")}</span>
<button onClick={() => {
setToastVisible(null)
if (isIphoneSafari) {
requestCameraPermission()
} else {
setIsCameraModalOpen(true)
}
}}>
{translate("/camera.give_access")}
</button>
<div className={styles["toast-buttons-container"]}>
<button onClick={() => {
setToastVisible(null)
if (isIphoneSafari) {
requestCameraPermission()
} else {
setIsCameraModalOpen(true)
navigate(routes.client.compatibilityV2ScanHand())
}
}}>
{translate("/camera.give_access")}
</button>
<button className={styles.buttonUpload}>
<input
id="upload-input"
type="file"
accept="image/*"
style={{ display: 'none' }}
onChange={onSelectFile}
/>
<label htmlFor="upload-input" className={styles.labelUpload} onClick={() => {
if (!isIphoneSafari) {
return navigate(routes.client.compatibilityV2ScanHand());
}
}}>
{translate("/camera.upload")}
</label>
</button>
</div>
</div>
</Toast>
)}

View File

@ -28,7 +28,8 @@
justify-content: space-between;
&>button,
&>.buttons-container>button {
&>.buttons-container>button,
&>.toast-buttons-container>button {
padding: 6px 18px;
border: none;
background-color: #fff;
@ -47,6 +48,24 @@
grid-template-columns: 1fr 1fr;
gap: 8px;
}
&>.toast-buttons-container {
display: flex;
flex-direction: column;
gap: 8px;
}
}
.buttonUpload {
padding: 0 !important;
}
.labelUpload {
display: block;
cursor: pointer;
padding: 6px 18px;
width: 100%;
height: 100%;
}
.modal {

View File

@ -0,0 +1,175 @@
import Title from "@/components/Title";
import Header from "../../components/Header";
import { images } from "../../data";
import styles from "./styles.module.scss";
import { useTranslations } from "@/hooks/translations";
import { ELocalesPlacement } from "@/locales";
import { useEffect, useState, useRef } from "react";
import { useDispatch, useSelector } from "react-redux";
import { actions, selectors } from "@/store";
import Loader, { LoaderColor } from "@/components/Loader";
import { useNavigate } from "react-router-dom";
import routes from "@/routes";
import { IPalmistryFinger } from "@/api/resources/Palmistry";
import { ICompatibilityV2FingerLocal } from "@/store/compatibilityV2";
import { useApi } from "@/api";
import { DataURIToBlob } from "@/services/data";
function ScanHand() {
const api = useApi();
const dispatch = useDispatch();
const navigate = useNavigate();
const { translate } = useTranslations(ELocalesPlacement.CompatibilityV2);
const [isLoading, setIsLoading] = useState(false);
const imageRef = useRef<HTMLImageElement>(null);
const gender = (useSelector(selectors.selectQuestionnaire)?.gender || "female");
const handleNext = () => {
navigate(`${routes.client.compatibilityV2ScannedPhoto()}?fromScanHand=true`)
}
useEffect(() => {
const timer = setTimeout(() => {
handleHandOn()
}, 6000);
return () => clearTimeout(timer);
}, []);
const convertImageToBase64 = (img: HTMLImageElement): Promise<string> => {
return new Promise((resolve, reject) => {
const canvas = document.createElement('canvas');
const padding = 100;
canvas.width = img.width * 1.4 + padding * 2;
canvas.height = img.height * 1.4;
const ctx = canvas.getContext('2d');
if (!ctx) {
reject(new Error('Не удалось получить контекст canvas'));
return;
}
ctx.fillStyle = '#272727';
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.drawImage(img, padding, 0, img.width * 1.4, canvas.height);
const base64String = canvas.toDataURL('image/png');
resolve(base64String);
});
};
const fingersNames = {
thumb: translate("thumb"),
index_finger: translate("index_finger"),
middle_finger: translate("middle_finger"),
ring_finger: translate("ring_finger"),
pinky: translate("pinky"),
};
const setFingersNames = (
fingers: IPalmistryFinger[]
): ICompatibilityV2FingerLocal[] => {
if (!fingers) return [];
return fingers.map((finger) => {
return {
...finger,
fingerName: fingersNames[finger.name as keyof typeof fingersNames],
};
});
};
const getLines = async (file: File | Blob) => {
const formData = new FormData();
formData.append("file", file);
try {
const result = await api.getPalmistryLines({ formData, gender: gender as "male" | "female" });
const fingers = setFingersNames(result?.fingers);
dispatch(
actions.compatibilityV2.update({
lines: result?.lines,
fingers,
})
);
return result;
} catch (error) {
dispatch(
actions.compatibilityV2.update({
lines: [],
fingers: [],
})
);
return null;
}
};
const handleHandOn = async () => {
if (isLoading || !imageRef.current) return;
setIsLoading(true);
try {
const photo = await convertImageToBase64(imageRef.current);
const file = DataURIToBlob(photo);
await getLines(file);
URL.revokeObjectURL(URL.createObjectURL(file));
dispatch(
actions.compatibilityV2.update({
photo,
})
);
handleNext()
} catch (error) {
console.error('Ошибка при конвертации изображения:', error);
} finally {
setIsLoading(false);
}
}
return (
<div
className={styles.container}
>
<Header
className={styles.header}
classNameTitle={styles["header-title"]}
isBackButtonVisible={true}
/>
<div
className={styles.content}
onClick={handleHandOn}
onTouchStart={(e) => {
e.preventDefault();
handleHandOn();
}}
>
{!isLoading && <>
<div className={styles.gradientContainer}>
<div className={styles.animationContainer}>
<div className={styles.line} />
</div>
</div>
<img className={styles.imageGif} src={images("scan-hand/Palm-tach-A.gif")} alt="Palm-tach-A" />
<Title variant="h2" className={styles.title}>
{translate("/scan-hand.title")}
</Title>
</>}
<img
ref={imageRef}
className={styles.imageHand}
src={images(`scan-hand/${gender}-Palm-A.png`)}
alt="hand"
draggable={false}
/>
{isLoading &&
<Loader color={LoaderColor.White} />
}
</div>
</div>
)
}
export default ScanHand

View File

@ -0,0 +1,108 @@
.container {
width: 100%;
max-width: 560px;
height: fit-content;
min-height: 100dvh;
padding: 0px;
margin: 0 auto;
background-color: #272727;
overflow: hidden;
& * {
position: relative;
z-index: 9;
}
}
.header {
padding: 20px 26px;
background-color: #fff;
}
.content {
position: relative;
width: 100%;
padding-inline: 20px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
min-height: calc(100dvh - 66px);
&>.imageGif {
max-width: 150px;
}
&>.title {
margin-top: 56px;
color: #FFFFFF;
font-size: 28px;
line-height: 125%;
font-weight: 500;
max-width: 350px;
}
& * {
pointer-events: none;
}
}
.gradientContainer {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: linear-gradient(360deg, rgba(3, 20, 109, 0.9) 0%, rgba(129, 138, 182, 0.666) 0.01%, rgba(67, 68, 70, 0.504) 49.65%, rgba(41, 41, 41, 0.549) 80.57%);
display: flex;
flex-direction: column;
justify-content: flex-end;
align-items: center;
z-index: 1;
& * {
position: relative;
z-index: 1;
}
}
.animationContainer {
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: flex-start;
background-color: #272727;
animation: moveLine 6s ease-in-out infinite;
&>.line {
width: 100%;
height: 7px;
background-color: #03146DE5;
}
}
@keyframes moveLine {
0% {
height: 100%;
}
50% {
height: 0;
}
100% {
height: 100%;
}
}
.imageHand {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%) scale(1.4);
width: 100%;
object-fit: cover;
opacity: 0;
transition: opacity 0.2s ease-in-out;
}

View File

@ -8,6 +8,8 @@ import { useTranslations } from "@/hooks/translations";
import { ELocalesPlacement } from "@/locales";
import ScanInstructionSVG from "../../images/SVG/ScanInstruction";
import { usePreloadImages } from "@/hooks/preload/images";
import { useUploadImage } from "@/hooks/palmistry/useUploadImage";
import Loader, { LoaderColor } from "@/components/Loader";
function ScanInstruction() {
const { translate } = useTranslations(ELocalesPlacement.CompatibilityV2);
@ -20,6 +22,22 @@ function ScanInstruction() {
"/v1/palmistry/hand-little-finger.svg",
]);
const handleToScannedPhoto = () => {
navigate(routes.client.compatibilityV2ScannedPhoto());
};
const { isLoading, isError, onSelectFile } = useUploadImage({
fingersNames: {
thumb: translate("thumb"),
index_finger: translate("index_finger"),
middle_finger: translate("middle_finger"),
ring_finger: translate("ring_finger"),
pinky: translate("pinky"),
},
keyOfStore: "compatibilityV2",
onImageUploaded: handleToScannedPhoto
});
const handleClick = () => {
navigate(routes.client.compatibilityV2Camera());
};
@ -33,6 +51,22 @@ function ScanInstruction() {
<Button className={styles.button} onClick={handleClick}>
{translate("/scan-instruction.button")}
</Button>
<input
id="upload-input"
type="file"
accept="image/*"
style={{ display: 'none' }}
onChange={onSelectFile}
/>
<label htmlFor="upload-input" className={styles.labelUpload}>
{translate("/scan-instruction.upload_photo")}
{isLoading && <div className={styles.loaderContainer}>
<Loader color={LoaderColor.Black} />
</div>}
</label>
{isError &&
<p className={styles.error}>{translate("/scan-instruction.error")}</p>
}
<BiometricData className={styles.biometric} />
</div>
);

View File

@ -25,3 +25,34 @@
display: none;
}
}
.labelUpload {
position: relative;
margin-top: 6px;
font-size: 21px;
line-height: 125%;
text-align: center;
color: #17699E;
font-weight: 500;
font-family: SF Pro Text;
cursor: pointer;
padding: 16px;
}
.loaderContainer {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
background-color: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(2px);
}
.error {
color: #FF0000;
font-size: 14px;
}

View File

@ -5,7 +5,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { IPalmistryLine } from "@/api/resources/Palmistry";
import Title from "@/components/Title";
import { ICompatibilityV2FingerLocal } from "@/store/compatibilityV2";
import { useNavigate } from "react-router-dom";
import { useNavigate, useSearchParams } from "react-router-dom";
import routes from "@/routes";
import ScannedPhotoElement from "@/components/palmistry/scanned-photo/scanned-photo";
import { useTranslations } from "@/hooks/translations";
@ -44,7 +44,10 @@ function ScannedPhoto() {
const [shouldDisplayPalmLines, setShouldDisplayPalmLines] = useState(false);
const [smallPhotoState, setSmallPhotoState] = useState(false);
const [isDecorationShown, setIsDecorationShown] = useState(true);
const [classNameScannedPhoto, setClassNameScannedPhoto] = useState("");
const [searchParams] = useSearchParams();
const isFromScanHand = searchParams.get("fromScanHand") === "true";
const [classNameScannedPhoto, setClassNameScannedPhoto] = useState(isFromScanHand ? styles.scannedPhotoFromScanHand : "");
const feature = useSelector(selectors.selectFeature);
const isIOSPath = useMemo(() => feature?.toLowerCase()?.includes("ios"), [feature]);

View File

@ -81,6 +81,26 @@
--loader-background: rgba(16, 32, 77, 0.35);
}
.scannedPhotoFromScanHand {
// position: fixed;
// top: 50%;
// left: 50%;
// transform: translate(-50%, -50%) scale(1.4);
// width: 100%;
// object-fit: cover;
// transform: scale();
animation: scaleScannedPhotoFromScanHand 1s ease-in-out forwards;
}
@keyframes scaleScannedPhotoFromScanHand {
0% {
transform: scale(3);
}
100% {
transform: scale(1);
}
}
.title {
min-height: 36px;
margin: 0;

View File

@ -164,6 +164,7 @@ function Camera() {
};
const onSelectFile = async (event: React.ChangeEvent<HTMLInputElement>) => {
setToastVisible(null);
setUploadMenuModalIsOpen(false);
if (!event.target.files || event.target.files.length === 0) return;
@ -294,9 +295,23 @@ function Camera() {
<Toast classNameContainer={styles["toast-container"]} variant="error">
<div className={styles["toast-content"]}>
<span>{translate("/camera.bad_photo")}</span>
<button onClick={() => setToastVisible(null)}>
{translate("/camera.try_again")}
</button>
<div className={styles["toast-buttons-container"]}>
<button onClick={() => setToastVisible(null)}>
{translate("/camera.try_again")}
</button>
<button className={styles.buttonUpload}>
<input
id="upload-input"
type="file"
accept="image/*"
style={{ display: 'none' }}
onChange={onSelectFile}
/>
<label htmlFor="upload-input" className={styles.labelUpload}>
{translate("/camera.upload")}
</label>
</button>
</div>
</div>
</Toast>
)}
@ -304,16 +319,30 @@ function Camera() {
<Toast classNameContainer={styles["toast-container"]} variant="error">
<div className={styles["toast-content"]}>
<span>{translate("/camera.no_access_camera")}</span>
<button onClick={() => {
setToastVisible(null)
if (isIphoneSafari) {
requestCameraPermission()
} else {
setIsCameraModalOpen(true)
}
}}>
{translate("/camera.give_access")}
</button>
<div className={styles["toast-buttons-container"]}>
<button onClick={() => {
setToastVisible(null)
if (isIphoneSafari) {
requestCameraPermission()
} else {
setIsCameraModalOpen(true)
}
}}>
{translate("/camera.give_access")}
</button>
<button className={styles.buttonUpload}>
<input
id="upload-input"
type="file"
accept="image/*"
style={{ display: 'none' }}
onChange={onSelectFile}
/>
<label htmlFor="upload-input" className={styles.labelUpload}>
{translate("/camera.upload")}
</label>
</button>
</div>
</div>
</Toast>
)}

View File

@ -28,7 +28,8 @@
justify-content: space-between;
&>button,
&>.buttons-container>button {
&>.buttons-container>button,
&>.toast-buttons-container>button {
padding: 6px 18px;
border: none;
background-color: #fff;
@ -47,6 +48,23 @@
grid-template-columns: 1fr 1fr;
gap: 8px;
}
&>.toast-buttons-container {
display: flex;
flex-direction: column;
gap: 8px;
}
}
.buttonUpload {
padding: 0 !important;
}
.labelUpload {
display: block;
padding: 6px 18px;
width: 100%;
height: 100%;
}
.modal {
@ -90,4 +108,4 @@
:global(.dark-theme) .modal-container {
background-color: #343639;
}
}

View File

@ -8,6 +8,8 @@ import { useTranslations } from "@/hooks/translations";
import { ELocalesPlacement } from "@/locales";
import ScanInstructionSVG from "../../images/SVG/ScanInstruction";
import { usePreloadImages } from "@/hooks/preload/images";
import Loader, { LoaderColor } from "@/components/Loader";
import { useUploadImage } from "@/hooks/palmistry/useUploadImage";
function ScanInstruction() {
const { translate } = useTranslations(ELocalesPlacement.PalmistryV1);
@ -20,6 +22,22 @@ function ScanInstruction() {
"/v1/palmistry/hand-little-finger.svg",
]);
const handleToScannedPhoto = () => {
navigate(routes.client.palmistryV1ScannedPhoto());
};
const { isLoading, isError, onSelectFile } = useUploadImage({
fingersNames: {
thumb: translate("thumb"),
index_finger: translate("index_finger"),
middle_finger: translate("middle_finger"),
ring_finger: translate("ring_finger"),
pinky: translate("pinky"),
},
keyOfStore: "palmistry",
onImageUploaded: handleToScannedPhoto
});
const handleClick = () => {
navigate(routes.client.palmistryV1Camera());
};
@ -33,6 +51,22 @@ function ScanInstruction() {
<Button className={styles.button} onClick={handleClick}>
{translate("/scan-instruction.button")}
</Button>
<input
id="upload-input"
type="file"
accept="image/*"
style={{ display: 'none' }}
onChange={onSelectFile}
/>
<label htmlFor="upload-input" className={styles.labelUpload}>
{translate("/scan-instruction.upload_photo")}
{isLoading && <div className={styles.loaderContainer}>
<Loader color={LoaderColor.Black} />
</div>}
</label>
{isError &&
<p className={styles.error}>{translate("/scan-instruction.error")}</p>
}
<BiometricData className={styles.biometric} />
</div>
);

View File

@ -21,7 +21,38 @@
line-height: 125%;
margin-top: 20px;
& > svg {
&>svg {
display: none;
}
}
.labelUpload {
position: relative;
margin-top: 6px;
font-size: 21px;
line-height: 125%;
text-align: center;
color: #17699E;
font-weight: 500;
font-family: SF Pro Text;
cursor: pointer;
padding: 16px;
}
.loaderContainer {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
background-color: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(2px);
}
.error {
color: #FF0000;
font-size: 14px;
}

View File

@ -23,6 +23,7 @@ export enum EUnleashFlags {
"headOrHeartResultPageCompatibilityV3" = "headOrHeartResultPageCompatibilityV3",
"headOrHeartResultPageCompatibilityV4" = "headOrHeartResultPageCompatibilityV4",
"compatibilityV1EmailEnter" = "compatibilityV1EmailEnter",
"compatibilityV2ScanHand" = "compatibilityV2ScanHand",
}
interface IUseUnleashProps<T extends EUnleashFlags> {
@ -50,6 +51,7 @@ interface IVariants {
[EUnleashFlags.headOrHeartResultPageCompatibilityV3]: "v0" | "v1";
[EUnleashFlags.headOrHeartResultPageCompatibilityV4]: "v0" | "v1";
[EUnleashFlags.compatibilityV1EmailEnter]: "show" | "hide";
[EUnleashFlags.compatibilityV2ScanHand]: "show" | "hide";
}
/**
@ -72,7 +74,7 @@ export const useUnleash = <T extends EUnleashFlags>({
}, [flagsReady]);
const variant = useMemo(() => {
return variantFromParams || abVariant?.payload?.value;
return variantFromParams || abVariant?.payload?.value || abVariant?.name;
}, [abVariant, variantFromParams]) as IVariants[T];
useEffect(() => {

View File

@ -0,0 +1,101 @@
import { useApi } from "@/api";
import { IPalmistryFinger } from "@/api/resources/Palmistry";
import { actions } from "@/store";
import { useCallback, useMemo, useState } from "react"
import { useDispatch } from "react-redux";
interface IUseUploadImageProps {
fingersNames: Record<string, string>;
keyOfStore: "compatibilityV2" | "palmistry";
onImageUploaded: () => void;
}
export const useUploadImage = ({
fingersNames,
keyOfStore,
onImageUploaded
}: IUseUploadImageProps) => {
const dispatch = useDispatch();
const api = useApi();
const [isLoading, setIsLoading] = useState(false);
const [isError, setIsError] = useState(false);
const setFingersNames = useCallback((
fingers: IPalmistryFinger[]
): Array<IPalmistryFinger & { fingerName?: string }> => {
if (!fingers) return [];
return fingers.map((finger) => {
return {
...finger,
fingerName: fingersNames[finger.name as keyof typeof fingersNames],
};
});
}, [fingersNames]);
const getLines = useCallback(async (file: File | Blob) => {
setIsLoading(true);
const formData = new FormData();
formData.append("file", file);
try {
const result = await api.getPalmistryLines({ formData });
const fingers = setFingersNames(result?.fingers);
dispatch(
actions[keyOfStore].update({
lines: result?.lines,
fingers,
})
);
return result;
} catch (error) {
dispatch(
actions[keyOfStore].update({
lines: [],
fingers: [],
})
);
setIsError(true);
return null;
} finally {
setIsLoading(false);
}
}, [api, dispatch]);
const onSelectFile = useCallback(async (event: React.ChangeEvent<HTMLInputElement>) => {
setIsError(false);
if (!event.target.files || event.target.files.length === 0) {
event.target.value = '';
return setIsError(true)
};
const result = await getLines(event.target.files[0]);
const lines = result?.lines;
const reader = new FileReader();
reader.onloadend = () => {
dispatch(
actions[keyOfStore].update({
photo: reader.result as string,
})
);
if (!lines?.length || lines?.length < 2) {
return setIsError(true);
}
onImageUploaded?.();
};
reader.readAsDataURL(event.target.files[0]);
}, []);
return useMemo(() => ({
isLoading,
isError,
onSelectFile,
}), [
isLoading,
isError,
onSelectFile,
])
}

View File

@ -25,6 +25,7 @@ const isBackButtonVisibleRoutes = [
routes.client.compatibilityV2TrialPayment(),
routes.client.compatibilityV2TryApp(),
routes.client.compatibilityV2Payment(),
routes.client.compatibilityV2ScanHand(),
];
function Layout() {

View File

@ -48,6 +48,7 @@ import TryApp from "@/components/CompatibilityV2/pages/TryApp";
import { ESiteTheme, useTheme } from "@/hooks/theme/useTheme";
import { EUnleashFlags, useUnleash } from "@/hooks/ab/unleash/useUnleash";
import Loader, { LoaderColor } from "@/components/Loader";
import ScanHand from "@/components/CompatibilityV2/pages/ScanHand";
const removePrefix = (path: string) => path.replace(compatibilityV2Prefix, "");
@ -191,6 +192,10 @@ function CompatibilityV2Routes() {
path={removePrefix(routes.client.compatibilityV2SecretDiscount())}
element={<SecretDiscount />}
/>
<Route
path={removePrefix(routes.client.compatibilityV2ScanHand())}
element={<ScanHand />}
/>
<Route element={<Layout />}>
<Route
element={

View File

@ -239,6 +239,7 @@ const routes = {
compatibilityV2LetScan: () => [compatibilityV2Prefix, "let-scan"].join("/"),
compatibilityV2ScanInstruction: () => [compatibilityV2Prefix, "scan-instruction"].join("/"),
compatibilityV2Camera: () => [compatibilityV2Prefix, "camera"].join("/"),
compatibilityV2ScanHand: () => [compatibilityV2Prefix, "scan-hand"].join("/"),
compatibilityV2ScannedPhoto: () => [compatibilityV2Prefix, "scanned-photo"].join("/"),
compatibilityV2Email: () => [compatibilityV2Prefix, "email"].join("/"),
compatibilityV2TrialChoice: () => [compatibilityV2Prefix, "trial-choice"].join("/"),