diff --git a/public/locales/compatibility-v2/en/male_en.json b/public/locales/compatibility-v2/en/male_en.json index 1757def..5336a6b 100644 --- a/public/locales/compatibility-v2/en/male_en.json +++ b/public/locales/compatibility-v2/en/male_en.json @@ -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": "Приложите ладонь к экрану телефона" } } \ No newline at end of file diff --git a/public/locales/palmistry-v1/en/male_en.json b/public/locales/palmistry-v1/en/male_en.json index 2b44d2f..b155fd0 100644 --- a/public/locales/palmistry-v1/en/male_en.json +++ b/public/locales/palmistry-v1/en/male_en.json @@ -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 " }, "/camera": { + "upload": "Upload", "bad_photo": "Bad Photo!", "try_again": "Try Again", "do_better": "You Can Do Better", diff --git a/public/v2/compatibility/scan-hand/Palm-tach-A.gif b/public/v2/compatibility/scan-hand/Palm-tach-A.gif new file mode 100644 index 0000000..949a8cb Binary files /dev/null and b/public/v2/compatibility/scan-hand/Palm-tach-A.gif differ diff --git a/public/v2/compatibility/scan-hand/female-Palm-A.png b/public/v2/compatibility/scan-hand/female-Palm-A.png new file mode 100644 index 0000000..c660a54 Binary files /dev/null and b/public/v2/compatibility/scan-hand/female-Palm-A.png differ diff --git a/public/v2/compatibility/scan-hand/male-Palm-A.png b/public/v2/compatibility/scan-hand/male-Palm-A.png new file mode 100644 index 0000000..3c06d88 Binary files /dev/null and b/public/v2/compatibility/scan-hand/male-Palm-A.png differ diff --git a/src/api/resources/Palmistry.ts b/src/api/resources/Palmistry.ts index bba6244..ffaf8b6 100644 --- a/src/api/resources/Palmistry.ts +++ b/src/api/resources/Palmistry.ts @@ -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", diff --git a/src/components/CompatibilityV2/pages/Camera/index.tsx b/src/components/CompatibilityV2/pages/Camera/index.tsx index 839a92c..533496b 100644 --- a/src/components/CompatibilityV2/pages/Camera/index.tsx +++ b/src/components/CompatibilityV2/pages/Camera/index.tsx @@ -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) => { + 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) + } } }}>

@@ -290,9 +307,27 @@ function Camera() {

{translate("/camera.bad_photo")} - +
+ + +
)} @@ -300,16 +335,35 @@ function Camera() {
{translate("/camera.no_access_camera")} - +
+ + +
)} diff --git a/src/components/CompatibilityV2/pages/Camera/styles.module.scss b/src/components/CompatibilityV2/pages/Camera/styles.module.scss index 85a7d5f..11d3085 100644 --- a/src/components/CompatibilityV2/pages/Camera/styles.module.scss +++ b/src/components/CompatibilityV2/pages/Camera/styles.module.scss @@ -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 { diff --git a/src/components/CompatibilityV2/pages/ScanHand/index.tsx b/src/components/CompatibilityV2/pages/ScanHand/index.tsx new file mode 100644 index 0000000..15532aa --- /dev/null +++ b/src/components/CompatibilityV2/pages/ScanHand/index.tsx @@ -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(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 => { + 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 ( +
+
+
{ + e.preventDefault(); + handleHandOn(); + }} + > + {!isLoading && <> +
+
+
+
+
+ Palm-tach-A + + {translate("/scan-hand.title")} + + } + hand + {isLoading && + + } +
+
+ ) +} + +export default ScanHand \ No newline at end of file diff --git a/src/components/CompatibilityV2/pages/ScanHand/styles.module.scss b/src/components/CompatibilityV2/pages/ScanHand/styles.module.scss new file mode 100644 index 0000000..4ce2df0 --- /dev/null +++ b/src/components/CompatibilityV2/pages/ScanHand/styles.module.scss @@ -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; +} \ No newline at end of file diff --git a/src/components/CompatibilityV2/pages/ScanInstruction/index.tsx b/src/components/CompatibilityV2/pages/ScanInstruction/index.tsx index 18cd259..5c54054 100644 --- a/src/components/CompatibilityV2/pages/ScanInstruction/index.tsx +++ b/src/components/CompatibilityV2/pages/ScanInstruction/index.tsx @@ -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() { + + + {isError && +

{translate("/scan-instruction.error")}

+ }
); diff --git a/src/components/CompatibilityV2/pages/ScanInstruction/styles.module.scss b/src/components/CompatibilityV2/pages/ScanInstruction/styles.module.scss index b5e2f5f..fd3e7d8 100644 --- a/src/components/CompatibilityV2/pages/ScanInstruction/styles.module.scss +++ b/src/components/CompatibilityV2/pages/ScanInstruction/styles.module.scss @@ -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; +} \ No newline at end of file diff --git a/src/components/CompatibilityV2/pages/ScannedPhoto/index.tsx b/src/components/CompatibilityV2/pages/ScannedPhoto/index.tsx index bdcc053..90c2318 100644 --- a/src/components/CompatibilityV2/pages/ScannedPhoto/index.tsx +++ b/src/components/CompatibilityV2/pages/ScannedPhoto/index.tsx @@ -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]); diff --git a/src/components/CompatibilityV2/pages/ScannedPhoto/styles.module.scss b/src/components/CompatibilityV2/pages/ScannedPhoto/styles.module.scss index 5eb2cbd..a79ed58 100644 --- a/src/components/CompatibilityV2/pages/ScannedPhoto/styles.module.scss +++ b/src/components/CompatibilityV2/pages/ScannedPhoto/styles.module.scss @@ -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; diff --git a/src/components/PalmistryV1/pages/Camera/index.tsx b/src/components/PalmistryV1/pages/Camera/index.tsx index d4d4e14..c6bf2bf 100644 --- a/src/components/PalmistryV1/pages/Camera/index.tsx +++ b/src/components/PalmistryV1/pages/Camera/index.tsx @@ -164,6 +164,7 @@ function Camera() { }; const onSelectFile = async (event: React.ChangeEvent) => { + setToastVisible(null); setUploadMenuModalIsOpen(false); if (!event.target.files || event.target.files.length === 0) return; @@ -294,9 +295,23 @@ function Camera() {
{translate("/camera.bad_photo")} - +
+ + +
)} @@ -304,16 +319,30 @@ function Camera() {
{translate("/camera.no_access_camera")} - +
+ + +
)} diff --git a/src/components/PalmistryV1/pages/Camera/styles.module.scss b/src/components/PalmistryV1/pages/Camera/styles.module.scss index 3112b74..256a0e6 100644 --- a/src/components/PalmistryV1/pages/Camera/styles.module.scss +++ b/src/components/PalmistryV1/pages/Camera/styles.module.scss @@ -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; -} +} \ No newline at end of file diff --git a/src/components/PalmistryV1/pages/ScanInstruction/index.tsx b/src/components/PalmistryV1/pages/ScanInstruction/index.tsx index a50738a..d1ab18a 100644 --- a/src/components/PalmistryV1/pages/ScanInstruction/index.tsx +++ b/src/components/PalmistryV1/pages/ScanInstruction/index.tsx @@ -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() { + + + {isError && +

{translate("/scan-instruction.error")}

+ } ); diff --git a/src/components/PalmistryV1/pages/ScanInstruction/styles.module.scss b/src/components/PalmistryV1/pages/ScanInstruction/styles.module.scss index b5e2f5f..1d70ba8 100644 --- a/src/components/PalmistryV1/pages/ScanInstruction/styles.module.scss +++ b/src/components/PalmistryV1/pages/ScanInstruction/styles.module.scss @@ -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; +} \ No newline at end of file diff --git a/src/hooks/ab/unleash/useUnleash.ts b/src/hooks/ab/unleash/useUnleash.ts index b3b655d..b423966 100644 --- a/src/hooks/ab/unleash/useUnleash.ts +++ b/src/hooks/ab/unleash/useUnleash.ts @@ -23,6 +23,7 @@ export enum EUnleashFlags { "headOrHeartResultPageCompatibilityV3" = "headOrHeartResultPageCompatibilityV3", "headOrHeartResultPageCompatibilityV4" = "headOrHeartResultPageCompatibilityV4", "compatibilityV1EmailEnter" = "compatibilityV1EmailEnter", + "compatibilityV2ScanHand" = "compatibilityV2ScanHand", } interface IUseUnleashProps { @@ -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 = ({ }, [flagsReady]); const variant = useMemo(() => { - return variantFromParams || abVariant?.payload?.value; + return variantFromParams || abVariant?.payload?.value || abVariant?.name; }, [abVariant, variantFromParams]) as IVariants[T]; useEffect(() => { diff --git a/src/hooks/palmistry/useUploadImage.ts b/src/hooks/palmistry/useUploadImage.ts new file mode 100644 index 0000000..6591ad3 --- /dev/null +++ b/src/hooks/palmistry/useUploadImage.ts @@ -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; + 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 => { + 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) => { + 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, + ]) +} \ No newline at end of file diff --git a/src/routerComponents/Compatibility/v2/Layout/index.tsx b/src/routerComponents/Compatibility/v2/Layout/index.tsx index 9a28ddf..0e6ad3b 100644 --- a/src/routerComponents/Compatibility/v2/Layout/index.tsx +++ b/src/routerComponents/Compatibility/v2/Layout/index.tsx @@ -25,6 +25,7 @@ const isBackButtonVisibleRoutes = [ routes.client.compatibilityV2TrialPayment(), routes.client.compatibilityV2TryApp(), routes.client.compatibilityV2Payment(), + routes.client.compatibilityV2ScanHand(), ]; function Layout() { diff --git a/src/routerComponents/Compatibility/v2/index.tsx b/src/routerComponents/Compatibility/v2/index.tsx index 5fb4adf..3d3d940 100644 --- a/src/routerComponents/Compatibility/v2/index.tsx +++ b/src/routerComponents/Compatibility/v2/index.tsx @@ -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={} /> + } + /> }> [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("/"),