Merge branch 'develop' into 'main'

hint-palm

See merge request witapp/aura-webapp!675
This commit is contained in:
Daniil Chemerkin 2025-03-10 21:31:33 +00:00
commit 98228a7387
24 changed files with 5300 additions and 16 deletions

View File

@ -268,7 +268,10 @@
},
"/let-scan": {
"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": {
"title": "Photograph Your Palm as Shown",

View File

@ -137,7 +137,10 @@
},
"/let-scan": {
"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.",
"/scan-instruction": {

BIN
public/users-hands/1.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 284 KiB

BIN
public/users-hands/10.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 264 KiB

BIN
public/users-hands/11.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

BIN
public/users-hands/12.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

BIN
public/users-hands/13.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

BIN
public/users-hands/2.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 256 KiB

BIN
public/users-hands/3.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 270 KiB

BIN
public/users-hands/4.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

BIN
public/users-hands/5.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 268 KiB

BIN
public/users-hands/6.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 332 KiB

BIN
public/users-hands/7.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 292 KiB

BIN
public/users-hands/8.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 276 KiB

BIN
public/users-hands/9.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 299 KiB

View File

@ -8,6 +8,10 @@ import { useTranslations } from "@/hooks/translations";
import { ELocalesPlacement } from "@/locales";
import { ELottieKeys, useLottie } from "@/hooks/lottie/useLottie";
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() {
const { translate } = useTranslations(ELocalesPlacement.CompatibilityV2);
@ -16,10 +20,20 @@ function LetScan() {
loadKey: ELottieKeys.letScan,
});
const {
displayHands
} = useHandsGeneration();
const { isReady, variant: dynamicHandsCompV2 } = useUnleash({
flag: EUnleashFlags.dynamicHandsCompV2
});
const handleNext = () => {
navigate(routes.client.compatibilityV2ScanInstruction());
};
if (!isReady) return <Loader color={LoaderColor.Black} />;
return (
<div className={styles.page}>
<div className={styles["lottie-animation-container"]}>
@ -28,7 +42,7 @@ function LetScan() {
className={`${styles["lottie-animation"]} ym-hide-content`}
data={animationData}
autoplay
loop={false}
loop={true}
width={323}
/>}
</div>
@ -36,6 +50,14 @@ function LetScan() {
{translate("/let-scan.title")}
</Title>
<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}>
{translate("next")}
</Button>

View File

@ -63,16 +63,16 @@ function PalmsInformation() {
<img
className={styles.image}
// 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"
/>
</div>
)}
<Title variant="h2" className={styles.title}>
{translate(`/palms-information.${zodiacSign.toLowerCase()}.title`)}
{translate(`/palms-information.${zodiacSign?.toLowerCase()}.title`)}
</Title>
<p className={styles.description}>
{translate(`/palms-information.${zodiacSign.toLowerCase()}.description`)}
{translate(`/palms-information.${zodiacSign?.toLowerCase()}.description`)}
</p>
<Button onClick={handleNext}>
{translate("next")}

View File

@ -63,7 +63,7 @@ function PalmsInformationPartner() {
<img
className={styles.image}
// 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"
/>
</div>

View File

@ -8,6 +8,10 @@ import { useTranslations } from "@/hooks/translations";
import { ELocalesPlacement } from "@/locales";
import { ELottieKeys, useLottie } from "@/hooks/lottie/useLottie";
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() {
const { translate } = useTranslations(ELocalesPlacement.PalmistryV1);
@ -16,10 +20,20 @@ function LetScan() {
loadKey: ELottieKeys.letScan,
});
const {
displayHands
} = useHandsGeneration();
const { isReady, variant: dynamicHandsPalmistryV1 } = useUnleash({
flag: EUnleashFlags.dynamicHandsPalmistryV1
});
const handleNext = () => {
navigate(routes.client.palmistryV1ScanInstruction());
};
if (!isReady) return <Loader color={LoaderColor.Black} />;
return (
<div className={styles.page}>
<div className={styles["lottie-animation-container"]}>
@ -28,7 +42,7 @@ function LetScan() {
className={`${styles["lottie-animation"]} ym-hide-content`}
data={animationData}
autoplay
loop={false}
loop={true}
width={323}
/>}
</div>
@ -36,6 +50,14 @@ function LetScan() {
{translate("/let-scan.title")}
</Title>
<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}>
{translate("next")}
</Button>

View 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

View 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;
}
}

View File

@ -5,23 +5,19 @@ import { useSearchParams } from "react-router-dom";
export enum EUnleashFlags {
"genderPageType" = "genderPageType",
"zodiacImages" = "zodiacImages",
"dynamicHandsCompV2" = "dynamicHandsCompV2",
"dynamicHandsPalmistryV1" = "dynamicHandsPalmistryV1",
}
/**
* Интерфейс для входных параметров хука useUnleash
* Использует дженерик T для типизации флага
*/
interface IUseUnleashProps<T extends EUnleashFlags> {
flag: T;
}
/**
* Интерфейс для возможных вариантов значений флагов
* Каждый ключ соответствует флагу из EUnleashFlags
*/
interface IVariants {
[EUnleashFlags.genderPageType]: "v0" | "v1" | "v2";
[EUnleashFlags.zodiacImages]: "new" | "old";
[EUnleashFlags.dynamicHandsCompV2]: "show" | "hide";
[EUnleashFlags.dynamicHandsPalmistryV1]: "show" | "hide";
}
/**

File diff suppressed because it is too large Load Diff

View 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 };
};