Merge branch 'develop' into 'main'
AW-429-comp-v1-ios-ab See merge request witapp/aura-webapp!713
This commit is contained in:
commit
0b66116779
@ -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": "Приложите ладонь к экрану телефона"
|
||||
}
|
||||
}
|
||||
@ -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",
|
||||
|
||||
BIN
public/v2/compatibility/scan-hand/Palm-tach-A.gif
Normal file
BIN
public/v2/compatibility/scan-hand/Palm-tach-A.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 129 KiB |
BIN
public/v2/compatibility/scan-hand/female-Palm-A.png
Normal file
BIN
public/v2/compatibility/scan-hand/female-Palm-A.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 531 KiB |
BIN
public/v2/compatibility/scan-hand/male-Palm-A.png
Normal file
BIN
public/v2/compatibility/scan-hand/male-Palm-A.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 667 KiB |
@ -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",
|
||||
|
||||
@ -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>
|
||||
)}
|
||||
|
||||
@ -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 {
|
||||
|
||||
175
src/components/CompatibilityV2/pages/ScanHand/index.tsx
Normal file
175
src/components/CompatibilityV2/pages/ScanHand/index.tsx
Normal 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
|
||||
108
src/components/CompatibilityV2/pages/ScanHand/styles.module.scss
Normal file
108
src/components/CompatibilityV2/pages/ScanHand/styles.module.scss
Normal 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;
|
||||
}
|
||||
@ -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>
|
||||
);
|
||||
|
||||
@ -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;
|
||||
}
|
||||
@ -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]);
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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>
|
||||
)}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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>
|
||||
);
|
||||
|
||||
@ -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;
|
||||
}
|
||||
@ -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(() => {
|
||||
|
||||
101
src/hooks/palmistry/useUploadImage.ts
Normal file
101
src/hooks/palmistry/useUploadImage.ts
Normal 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,
|
||||
])
|
||||
}
|
||||
@ -25,6 +25,7 @@ const isBackButtonVisibleRoutes = [
|
||||
routes.client.compatibilityV2TrialPayment(),
|
||||
routes.client.compatibilityV2TryApp(),
|
||||
routes.client.compatibilityV2Payment(),
|
||||
routes.client.compatibilityV2ScanHand(),
|
||||
];
|
||||
|
||||
function Layout() {
|
||||
|
||||
@ -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={
|
||||
|
||||
@ -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("/"),
|
||||
|
||||
Loading…
Reference in New Issue
Block a user