hint-palm
@ -268,7 +268,10 @@
|
|||||||
},
|
},
|
||||||
"/let-scan": {
|
"/let-scan": {
|
||||||
"title": "We Are Scanning Your Palm",
|
"title": "We Are Scanning Your Palm",
|
||||||
"text": "Follow the on-screen instructions so we can analyze the lines of your palm, revealing the future and the secrets of your destiny!"
|
"text": "Follow the on-screen instructions so we can analyze the lines of your palm, revealing the future and the secrets of your destiny!",
|
||||||
|
"hands": {
|
||||||
|
"title": "Сейчас сканируют ладонь <count> человек:"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"/scan-instruction": {
|
"/scan-instruction": {
|
||||||
"title": "Photograph Your Palm as Shown",
|
"title": "Photograph Your Palm as Shown",
|
||||||
|
|||||||
@ -137,7 +137,10 @@
|
|||||||
},
|
},
|
||||||
"/let-scan": {
|
"/let-scan": {
|
||||||
"title": "We are scanning your palm",
|
"title": "We are scanning your palm",
|
||||||
"text": "Follow the on-screen instructions so we can analyze the lines of your palm, revealing the future and the secrets of your destiny!"
|
"text": "Follow the on-screen instructions so we can analyze the lines of your palm, revealing the future and the secrets of your destiny!",
|
||||||
|
"hands": {
|
||||||
|
"title": "Сейчас сканируют ладонь <count> человек:"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"biometric_data": "We do not collect biometric data. The entire recognition process happens on your device.",
|
"biometric_data": "We do not collect biometric data. The entire recognition process happens on your device.",
|
||||||
"/scan-instruction": {
|
"/scan-instruction": {
|
||||||
|
|||||||
BIN
public/users-hands/1.webp
Normal file
|
After Width: | Height: | Size: 284 KiB |
BIN
public/users-hands/10.png
Normal file
|
After Width: | Height: | Size: 264 KiB |
BIN
public/users-hands/11.webp
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
public/users-hands/12.webp
Normal file
|
After Width: | Height: | Size: 19 KiB |
BIN
public/users-hands/13.webp
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
public/users-hands/2.webp
Normal file
|
After Width: | Height: | Size: 256 KiB |
BIN
public/users-hands/3.webp
Normal file
|
After Width: | Height: | Size: 270 KiB |
BIN
public/users-hands/4.webp
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
public/users-hands/5.webp
Normal file
|
After Width: | Height: | Size: 268 KiB |
BIN
public/users-hands/6.webp
Normal file
|
After Width: | Height: | Size: 332 KiB |
BIN
public/users-hands/7.webp
Normal file
|
After Width: | Height: | Size: 292 KiB |
BIN
public/users-hands/8.webp
Normal file
|
After Width: | Height: | Size: 276 KiB |
BIN
public/users-hands/9.png
Normal file
|
After Width: | Height: | Size: 299 KiB |
@ -8,6 +8,10 @@ import { useTranslations } from "@/hooks/translations";
|
|||||||
import { ELocalesPlacement } from "@/locales";
|
import { ELocalesPlacement } from "@/locales";
|
||||||
import { ELottieKeys, useLottie } from "@/hooks/lottie/useLottie";
|
import { ELottieKeys, useLottie } from "@/hooks/lottie/useLottie";
|
||||||
import { DotLottieReact } from "@lottiefiles/dotlottie-react";
|
import { DotLottieReact } from "@lottiefiles/dotlottie-react";
|
||||||
|
import UsersHands from "@/components/UsersHands";
|
||||||
|
import { useHandsGeneration } from "@/hooks/handsGeneration/useHandsGeneration";
|
||||||
|
import { EUnleashFlags, useUnleash } from "@/hooks/ab/unleash/useUnleash";
|
||||||
|
import Loader, { LoaderColor } from "@/components/Loader";
|
||||||
|
|
||||||
function LetScan() {
|
function LetScan() {
|
||||||
const { translate } = useTranslations(ELocalesPlacement.CompatibilityV2);
|
const { translate } = useTranslations(ELocalesPlacement.CompatibilityV2);
|
||||||
@ -16,10 +20,20 @@ function LetScan() {
|
|||||||
loadKey: ELottieKeys.letScan,
|
loadKey: ELottieKeys.letScan,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const {
|
||||||
|
displayHands
|
||||||
|
} = useHandsGeneration();
|
||||||
|
|
||||||
|
const { isReady, variant: dynamicHandsCompV2 } = useUnleash({
|
||||||
|
flag: EUnleashFlags.dynamicHandsCompV2
|
||||||
|
});
|
||||||
|
|
||||||
const handleNext = () => {
|
const handleNext = () => {
|
||||||
navigate(routes.client.compatibilityV2ScanInstruction());
|
navigate(routes.client.compatibilityV2ScanInstruction());
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (!isReady) return <Loader color={LoaderColor.Black} />;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.page}>
|
<div className={styles.page}>
|
||||||
<div className={styles["lottie-animation-container"]}>
|
<div className={styles["lottie-animation-container"]}>
|
||||||
@ -28,7 +42,7 @@ function LetScan() {
|
|||||||
className={`${styles["lottie-animation"]} ym-hide-content`}
|
className={`${styles["lottie-animation"]} ym-hide-content`}
|
||||||
data={animationData}
|
data={animationData}
|
||||||
autoplay
|
autoplay
|
||||||
loop={false}
|
loop={true}
|
||||||
width={323}
|
width={323}
|
||||||
/>}
|
/>}
|
||||||
</div>
|
</div>
|
||||||
@ -36,6 +50,14 @@ function LetScan() {
|
|||||||
{translate("/let-scan.title")}
|
{translate("/let-scan.title")}
|
||||||
</Title>
|
</Title>
|
||||||
<p className={styles.text}>{translate("/let-scan.text")}</p>
|
<p className={styles.text}>{translate("/let-scan.text")}</p>
|
||||||
|
{dynamicHandsCompV2 === "show" && (
|
||||||
|
<UsersHands
|
||||||
|
title={translate("/let-scan.hands.title", {
|
||||||
|
count: String(displayHands.length),
|
||||||
|
})}
|
||||||
|
hands={displayHands}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<Button className={styles.button} onClick={handleNext}>
|
<Button className={styles.button} onClick={handleNext}>
|
||||||
{translate("next")}
|
{translate("next")}
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@ -63,16 +63,16 @@ function PalmsInformation() {
|
|||||||
<img
|
<img
|
||||||
className={styles.image}
|
className={styles.image}
|
||||||
// src={images(`zodiacs/${gender}/${zodiacSign.toUpperCase()}.webp`)}
|
// src={images(`zodiacs/${gender}/${zodiacSign.toUpperCase()}.webp`)}
|
||||||
src={`/zodiac-signs/${gender?.toLowerCase()}/${zodiacSign.toLowerCase()}.svg`}
|
src={`/zodiac-signs/${gender?.toLowerCase()}/${zodiacSign?.toLowerCase()}.svg`}
|
||||||
alt="Zodiac sign"
|
alt="Zodiac sign"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<Title variant="h2" className={styles.title}>
|
<Title variant="h2" className={styles.title}>
|
||||||
{translate(`/palms-information.${zodiacSign.toLowerCase()}.title`)}
|
{translate(`/palms-information.${zodiacSign?.toLowerCase()}.title`)}
|
||||||
</Title>
|
</Title>
|
||||||
<p className={styles.description}>
|
<p className={styles.description}>
|
||||||
{translate(`/palms-information.${zodiacSign.toLowerCase()}.description`)}
|
{translate(`/palms-information.${zodiacSign?.toLowerCase()}.description`)}
|
||||||
</p>
|
</p>
|
||||||
<Button onClick={handleNext}>
|
<Button onClick={handleNext}>
|
||||||
{translate("next")}
|
{translate("next")}
|
||||||
|
|||||||
@ -63,7 +63,7 @@ function PalmsInformationPartner() {
|
|||||||
<img
|
<img
|
||||||
className={styles.image}
|
className={styles.image}
|
||||||
// src={images(`zodiacs/${gender}/${zodiacSign.toUpperCase()}.webp`)}
|
// src={images(`zodiacs/${gender}/${zodiacSign.toUpperCase()}.webp`)}
|
||||||
src={`/zodiac-signs/${partnerGender?.toLowerCase()}/${zodiacSign.toLowerCase()}.svg`}
|
src={`/zodiac-signs/${partnerGender?.toLowerCase()}/${zodiacSign?.toLowerCase()}.svg`}
|
||||||
alt="Zodiac sign"
|
alt="Zodiac sign"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -8,6 +8,10 @@ import { useTranslations } from "@/hooks/translations";
|
|||||||
import { ELocalesPlacement } from "@/locales";
|
import { ELocalesPlacement } from "@/locales";
|
||||||
import { ELottieKeys, useLottie } from "@/hooks/lottie/useLottie";
|
import { ELottieKeys, useLottie } from "@/hooks/lottie/useLottie";
|
||||||
import { DotLottieReact } from "@lottiefiles/dotlottie-react";
|
import { DotLottieReact } from "@lottiefiles/dotlottie-react";
|
||||||
|
import UsersHands from "@/components/UsersHands";
|
||||||
|
import { useHandsGeneration } from "@/hooks/handsGeneration/useHandsGeneration";
|
||||||
|
import { EUnleashFlags, useUnleash } from "@/hooks/ab/unleash/useUnleash";
|
||||||
|
import Loader, { LoaderColor } from "@/components/Loader";
|
||||||
|
|
||||||
function LetScan() {
|
function LetScan() {
|
||||||
const { translate } = useTranslations(ELocalesPlacement.PalmistryV1);
|
const { translate } = useTranslations(ELocalesPlacement.PalmistryV1);
|
||||||
@ -16,10 +20,20 @@ function LetScan() {
|
|||||||
loadKey: ELottieKeys.letScan,
|
loadKey: ELottieKeys.letScan,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const {
|
||||||
|
displayHands
|
||||||
|
} = useHandsGeneration();
|
||||||
|
|
||||||
|
const { isReady, variant: dynamicHandsPalmistryV1 } = useUnleash({
|
||||||
|
flag: EUnleashFlags.dynamicHandsPalmistryV1
|
||||||
|
});
|
||||||
|
|
||||||
const handleNext = () => {
|
const handleNext = () => {
|
||||||
navigate(routes.client.palmistryV1ScanInstruction());
|
navigate(routes.client.palmistryV1ScanInstruction());
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (!isReady) return <Loader color={LoaderColor.Black} />;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.page}>
|
<div className={styles.page}>
|
||||||
<div className={styles["lottie-animation-container"]}>
|
<div className={styles["lottie-animation-container"]}>
|
||||||
@ -28,7 +42,7 @@ function LetScan() {
|
|||||||
className={`${styles["lottie-animation"]} ym-hide-content`}
|
className={`${styles["lottie-animation"]} ym-hide-content`}
|
||||||
data={animationData}
|
data={animationData}
|
||||||
autoplay
|
autoplay
|
||||||
loop={false}
|
loop={true}
|
||||||
width={323}
|
width={323}
|
||||||
/>}
|
/>}
|
||||||
</div>
|
</div>
|
||||||
@ -36,6 +50,14 @@ function LetScan() {
|
|||||||
{translate("/let-scan.title")}
|
{translate("/let-scan.title")}
|
||||||
</Title>
|
</Title>
|
||||||
<p className={styles.text}>{translate("/let-scan.text")}</p>
|
<p className={styles.text}>{translate("/let-scan.text")}</p>
|
||||||
|
{dynamicHandsPalmistryV1 === "show" && (
|
||||||
|
<UsersHands
|
||||||
|
title={translate("/let-scan.hands.title", {
|
||||||
|
count: String(displayHands.length),
|
||||||
|
})}
|
||||||
|
hands={displayHands}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<Button className={styles.button} onClick={handleNext}>
|
<Button className={styles.button} onClick={handleNext}>
|
||||||
{translate("next")}
|
{translate("next")}
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
104
src/components/UsersHands/index.tsx
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
import { IPalmistryPoint } from "@/api/resources/Palmistry";
|
||||||
|
import Title from "../Title";
|
||||||
|
import styles from "./styles.module.scss";
|
||||||
|
import { DisplayHand } from "@/hooks/handsGeneration/useHandsGeneration";
|
||||||
|
import { useRef } from "react";
|
||||||
|
|
||||||
|
interface IUsersHandsProps {
|
||||||
|
title?: string;
|
||||||
|
hands: DisplayHand[];
|
||||||
|
}
|
||||||
|
|
||||||
|
function UsersHands({
|
||||||
|
title,
|
||||||
|
hands
|
||||||
|
}: IUsersHandsProps) {
|
||||||
|
const linesRef = useRef<SVGPathElement[]>([]);
|
||||||
|
|
||||||
|
const getCoordinatesString = (points: IPalmistryPoint[]) => {
|
||||||
|
const coordinatesString = `M ${points[0]?.x * 56} ${points[0]?.y * 75
|
||||||
|
}`;
|
||||||
|
return points.reduce(
|
||||||
|
(acc, point) =>
|
||||||
|
`${acc} L ${point?.x * 56} ${point?.y * 75}`,
|
||||||
|
coordinatesString
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const getLineLength = (line: SVGPathElement) => {
|
||||||
|
return line?.getTotalLength();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.container}>
|
||||||
|
{!!title?.length && <Title variant="h3" className={styles.title}>
|
||||||
|
{title}
|
||||||
|
</Title>}
|
||||||
|
<div className={styles.hands}>
|
||||||
|
{hands.map(({ image, lines, willBeRemoved }, index) => (
|
||||||
|
<div className={`${styles.hand} ${willBeRemoved ? styles.willBeRemoved : ""}`} key={index}>
|
||||||
|
<img
|
||||||
|
src={image}
|
||||||
|
alt={`hand-${index}`}
|
||||||
|
/>
|
||||||
|
<svg
|
||||||
|
viewBox={`0 0 ${56} ${75}`}
|
||||||
|
className={`scanned-photo__svg-objects ${styles.svgObjects}`}
|
||||||
|
>
|
||||||
|
{/* {!!fingers.length &&
|
||||||
|
fingers?.map((finger, index) => {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
x={finger.point.x * 56 - 12}
|
||||||
|
y={finger.point.y * 75 - 12}
|
||||||
|
height="24px"
|
||||||
|
width="24px"
|
||||||
|
key={index}
|
||||||
|
>
|
||||||
|
<circle
|
||||||
|
cx="50%"
|
||||||
|
cy="50%"
|
||||||
|
r="11"
|
||||||
|
fill="white"
|
||||||
|
opacity="0.3"
|
||||||
|
className="scanned-photo__finger-point"
|
||||||
|
/>
|
||||||
|
<circle
|
||||||
|
cx="50%"
|
||||||
|
cy="50%"
|
||||||
|
r="5"
|
||||||
|
fill="#066FDE"
|
||||||
|
stroke="white"
|
||||||
|
strokeWidth="0.3"
|
||||||
|
className="scanned-photo__finger-point"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
})} */}
|
||||||
|
|
||||||
|
{lines.map((line, index) => (
|
||||||
|
<g key={`line-${index}`}>
|
||||||
|
<path
|
||||||
|
className={`scanned-photo__line scanned-photo__line_${line?.name} ${styles.line}`}
|
||||||
|
d={getCoordinatesString(line?.points)}
|
||||||
|
ref={(el) =>
|
||||||
|
(linesRef.current[index] = el as SVGPathElement)
|
||||||
|
}
|
||||||
|
style={{
|
||||||
|
strokeDasharray:
|
||||||
|
getLineLength(linesRef.current[index]) || 500,
|
||||||
|
strokeDashoffset:
|
||||||
|
getLineLength(linesRef.current[index]) || 500,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
))}
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default UsersHands
|
||||||
86
src/components/UsersHands/styles.module.scss
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
.container {
|
||||||
|
width: 100%;
|
||||||
|
background: linear-gradient(0.63deg, #E1EBFF 0.53%, rgba(75, 136, 255, 0.31) 0.54%);
|
||||||
|
border-radius: 18px;
|
||||||
|
padding: 32px 14px 6px;
|
||||||
|
margin-top: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-family: SF Pro Text;
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 125%;
|
||||||
|
text-align: center;
|
||||||
|
vertical-align: middle;
|
||||||
|
margin-bottom: 21px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hands {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-items: center;
|
||||||
|
overflow-x: auto;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
scrollbar-width: thin;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
|
||||||
|
&>.hand {
|
||||||
|
position: relative;
|
||||||
|
width: 56px;
|
||||||
|
height: 75px;
|
||||||
|
border-radius: 10px;
|
||||||
|
overflow: hidden;
|
||||||
|
flex-shrink: 0;
|
||||||
|
opacity: 1;
|
||||||
|
margin-right: 10px;
|
||||||
|
|
||||||
|
img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.willBeRemoved {
|
||||||
|
animation: fadeOut 1s ease-in-out forwards;
|
||||||
|
animation-delay: 2s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.svgObjects {
|
||||||
|
animation: fadeIn 1.5s ease-in-out forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
.line {
|
||||||
|
animation-delay: 1.5s;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeIn {
|
||||||
|
0% {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
99% {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeOut {
|
||||||
|
from {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
opacity: 0;
|
||||||
|
width: 0;
|
||||||
|
margin-right: 0;
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -5,23 +5,19 @@ import { useSearchParams } from "react-router-dom";
|
|||||||
export enum EUnleashFlags {
|
export enum EUnleashFlags {
|
||||||
"genderPageType" = "genderPageType",
|
"genderPageType" = "genderPageType",
|
||||||
"zodiacImages" = "zodiacImages",
|
"zodiacImages" = "zodiacImages",
|
||||||
|
"dynamicHandsCompV2" = "dynamicHandsCompV2",
|
||||||
|
"dynamicHandsPalmistryV1" = "dynamicHandsPalmistryV1",
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Интерфейс для входных параметров хука useUnleash
|
|
||||||
* Использует дженерик T для типизации флага
|
|
||||||
*/
|
|
||||||
interface IUseUnleashProps<T extends EUnleashFlags> {
|
interface IUseUnleashProps<T extends EUnleashFlags> {
|
||||||
flag: T;
|
flag: T;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Интерфейс для возможных вариантов значений флагов
|
|
||||||
* Каждый ключ соответствует флагу из EUnleashFlags
|
|
||||||
*/
|
|
||||||
interface IVariants {
|
interface IVariants {
|
||||||
[EUnleashFlags.genderPageType]: "v0" | "v1" | "v2";
|
[EUnleashFlags.genderPageType]: "v0" | "v1" | "v2";
|
||||||
[EUnleashFlags.zodiacImages]: "new" | "old";
|
[EUnleashFlags.zodiacImages]: "new" | "old";
|
||||||
|
[EUnleashFlags.dynamicHandsCompV2]: "show" | "hide";
|
||||||
|
[EUnleashFlags.dynamicHandsPalmistryV1]: "show" | "hide";
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
4910
src/hooks/handsGeneration/hands.ts
Normal file
138
src/hooks/handsGeneration/useHandsGeneration.ts
Normal file
@ -0,0 +1,138 @@
|
|||||||
|
import { IPalmistryFinger, IPalmistryLine } from '@/api/resources/Palmistry';
|
||||||
|
import { useState, useEffect, useCallback, useRef } from 'react';
|
||||||
|
import { getShuffledHands } from './hands';
|
||||||
|
|
||||||
|
const hands = getShuffledHands();
|
||||||
|
|
||||||
|
export interface DisplayHand {
|
||||||
|
image: string;
|
||||||
|
willBeRemoved: boolean;
|
||||||
|
id: number;
|
||||||
|
lines: IPalmistryLine[];
|
||||||
|
fingers: IPalmistryFinger[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface HandsToDelete {
|
||||||
|
hands: DisplayHand[];
|
||||||
|
deleteAt: number;
|
||||||
|
markAt: number;
|
||||||
|
marked: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useHandsGeneration = () => {
|
||||||
|
const [displayHands, setDisplayHands] = useState<DisplayHand[]>([]);
|
||||||
|
const [handsToDeleteQueue, setHandsToDeleteQueue] = useState<HandsToDelete[]>([]);
|
||||||
|
const currentHandIndexRef = useRef(1);
|
||||||
|
|
||||||
|
const minHands = 1;
|
||||||
|
const maxHands = 5;
|
||||||
|
|
||||||
|
const getHand = useCallback(() => {
|
||||||
|
const currentIndex = currentHandIndexRef.current;
|
||||||
|
|
||||||
|
currentHandIndexRef.current = currentIndex >= hands.length ? 1 : currentIndex + 1;
|
||||||
|
|
||||||
|
const currentHand = hands[currentIndex - 1];
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: Date.now() + Math.random(),
|
||||||
|
image: currentHand.image,
|
||||||
|
willBeRemoved: false,
|
||||||
|
lines: currentHand.lines,
|
||||||
|
fingers: currentHand.fingers
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const createHand = useCallback(() => {
|
||||||
|
return getHand();
|
||||||
|
}, [getHand]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
addHands(3);
|
||||||
|
return () => setDisplayHands([]);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const addHands = useCallback((countHands?: number) => {
|
||||||
|
const addHandsTimeout = 6000 + Math.random() * 4000;
|
||||||
|
|
||||||
|
const count = !!countHands ? countHands : Math.random() < 0.6 ? 1 : 2;
|
||||||
|
const _newHands = Array(count).fill(null).map(createHand);
|
||||||
|
|
||||||
|
setDisplayHands(prev => {
|
||||||
|
const updatedHands = [...prev, ..._newHands].slice(-maxHands);
|
||||||
|
|
||||||
|
if (updatedHands.length < minHands) {
|
||||||
|
const additionalCount = minHands - updatedHands.length;
|
||||||
|
const additionalHands = Array(additionalCount).fill(null).map(createHand);
|
||||||
|
return [...updatedHands, ...additionalHands];
|
||||||
|
}
|
||||||
|
|
||||||
|
return updatedHands;
|
||||||
|
});
|
||||||
|
|
||||||
|
const now = Date.now();
|
||||||
|
setHandsToDeleteQueue(prev => [...prev, ..._newHands.map((value) => {
|
||||||
|
const deleteTime = 6000 + Math.random() * 20000;
|
||||||
|
return {
|
||||||
|
hands: [value],
|
||||||
|
deleteAt: now + deleteTime,
|
||||||
|
markAt: now + deleteTime - 3000,
|
||||||
|
marked: false
|
||||||
|
}
|
||||||
|
})]);
|
||||||
|
|
||||||
|
const timeoutAddHands = setTimeout(() => {
|
||||||
|
addHands();
|
||||||
|
clearTimeout(timeoutAddHands);
|
||||||
|
}, addHandsTimeout);
|
||||||
|
}, [createHand]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (handsToDeleteQueue.length === 0) return;
|
||||||
|
|
||||||
|
const checkQueue = () => {
|
||||||
|
const now = Date.now();
|
||||||
|
|
||||||
|
setHandsToDeleteQueue(prev => {
|
||||||
|
const updatedQueue = [...prev];
|
||||||
|
let hasChanges = false;
|
||||||
|
|
||||||
|
updatedQueue.forEach(item => {
|
||||||
|
if (displayHands.length <= minHands) return;
|
||||||
|
if (!item.marked && now >= item.markAt) {
|
||||||
|
markHands(item.hands);
|
||||||
|
item.marked = true;
|
||||||
|
hasChanges = true;
|
||||||
|
}
|
||||||
|
if (now >= item.deleteAt) {
|
||||||
|
deleteHands(item.hands);
|
||||||
|
hasChanges = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return hasChanges ? updatedQueue.filter(item => now < item.deleteAt) : updatedQueue;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const interval = setInterval(checkQueue, 100);
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
}, [handsToDeleteQueue]);
|
||||||
|
|
||||||
|
const deleteHands = (handsToDelete: DisplayHand[]) => {
|
||||||
|
setDisplayHands(prev =>
|
||||||
|
prev.filter(hand => !handsToDelete.some(h => h.id === hand.id))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const markHands = (handsToMark: DisplayHand[]) => {
|
||||||
|
setDisplayHands(prev =>
|
||||||
|
prev.map(hand =>
|
||||||
|
handsToMark.some(h => h.id === hand.id)
|
||||||
|
? { ...hand, willBeRemoved: true }
|
||||||
|
: hand
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { displayHands };
|
||||||
|
};
|
||||||