diff --git a/src/api/api.ts b/src/api/api.ts index ca306a3..3143f8b 100644 --- a/src/api/api.ts +++ b/src/api/api.ts @@ -28,6 +28,7 @@ import { OpenAI, SinglePayment, Products, + Palmistry, } from './resources' const api = { @@ -72,6 +73,7 @@ const api = { getSinglePaymentProducts: createMethod(SinglePayment.createRequestGet), createSinglePayment: createMethod(SinglePayment.createRequestPost), checkProductPurchased: createMethod(Products.createRequest), + getPalmistryLines: createMethod(Palmistry.createRequest), } export type ApiContextValue = typeof api diff --git a/src/api/resources/Palmistry.ts b/src/api/resources/Palmistry.ts new file mode 100644 index 0000000..1e63920 --- /dev/null +++ b/src/api/resources/Palmistry.ts @@ -0,0 +1,26 @@ +import routes from "@/routes"; + +export interface Payload { + formData: FormData; +} + +export type Response = IPalmistryLine[]; + +export interface IPalmistryLine { + line: string; + points: IPalmistryPoint[]; +} + +export interface IPalmistryPoint { + x: number; + y: number; +} + +export const createRequest = ({ formData }: Payload) => { + const url = new URL(routes.server.getPalmistryLines()); + const body = formData; + return new Request(url, { + method: "POST", + body, + }); +}; diff --git a/src/api/resources/index.ts b/src/api/resources/index.ts index e90c5b2..2f721e5 100644 --- a/src/api/resources/index.ts +++ b/src/api/resources/index.ts @@ -26,3 +26,4 @@ export * as Assistants from "./Assistants"; export * as OpenAI from "./OpenAI"; export * as SinglePayment from "./SinglePayment"; export * as Products from "./Products"; +export * as Palmistry from "./Palmistry"; diff --git a/src/components/palmistry/palmistry-container/palmistry-container.css b/src/components/palmistry/palmistry-container/palmistry-container.css index cabe470..8d3d5ea 100644 --- a/src/components/palmistry/palmistry-container/palmistry-container.css +++ b/src/components/palmistry/palmistry-container/palmistry-container.css @@ -72,6 +72,7 @@ font-size: 24px; font-weight: 600; line-height: 36px; + min-height: 24px; } .palmistry-container__bottom-content { @@ -206,7 +207,8 @@ text-align: center; } -.palmistry-container__correct-title, .palmistry-container__wrong-title { +.palmistry-container__correct-title, +.palmistry-container__wrong-title { font-size: 18px; font-weight: 700; line-height: 24px; @@ -359,13 +361,12 @@ margin-bottom: 34px; text-align: center; animation: title-opacity 1.5s cubic-bezier(0.37, 0, 0.63, 1); - animation-delay: 13s; animation-fill-mode: forwards; } .palmistry-container_type_scan-photo .palmistry-container__waiting-title { animation: waiting-title 0.5s cubic-bezier(0.37, 0, 0.63, 1); - animation-delay: 14.5s; + /* animation-delay: 14.5s; */ animation-fill-mode: forwards; font-size: 18px; font-weight: 500; @@ -377,7 +378,7 @@ .palmistry-container_type_scan-photo .palmistry-container__waiting-description { animation: waiting-description 8s cubic-bezier(0.37, 0, 0.63, 1); - animation-delay: 15s; + /* animation-delay: 15s; */ animation-fill-mode: forwards; font-size: 18px; font-weight: 400; @@ -428,7 +429,10 @@ width: 100%; } -.palmistry-container_type_email .palmistry-container__input input:not(:placeholder-shown) + .input__placeholder { +.palmistry-container_type_email + .palmistry-container__input + input:not(:placeholder-shown) + + .input__placeholder { font-size: 12px; top: 12px; width: auto; @@ -519,16 +523,19 @@ line-height: 20px; } -.palmistry-container_type_subscription-plan .palmistry-container__plan:not(:last-child) { +.palmistry-container_type_subscription-plan + .palmistry-container__plan:not(:last-child) { margin-right: 12px; } -.palmistry-container_type_subscription-plan .palmistry-container__plan:last-child { +.palmistry-container_type_subscription-plan + .palmistry-container__plan:last-child { position: relative; } -.palmistry-container_type_subscription-plan .palmistry-container__plan:last-child::after { - content: ''; +.palmistry-container_type_subscription-plan + .palmistry-container__plan:last-child::after { + content: ""; background: var(--pale-gray); bottom: -24px; height: 15px; @@ -542,11 +549,13 @@ color: var(--black-color-text); } -.palmistry-container_type_subscription-plan .palmistry-container__plan_active::after { +.palmistry-container_type_subscription-plan + .palmistry-container__plan_active::after { background: var(--strong-blue) !important; } -.palmistry-container_type_subscription-plan .palmistry-container__subscription-text { +.palmistry-container_type_subscription-plan + .palmistry-container__subscription-text { font-size: 14px; font-weight: 600; line-height: 120%; @@ -554,7 +563,8 @@ text-align: center; } -.palmistry-container_type_subscription-plan .palmistry-container__subscription-text_active { +.palmistry-container_type_subscription-plan + .palmistry-container__subscription-text_active { color: var(--blue-color-text); } diff --git a/src/components/palmistry/scanned-photo/scanned-photo.css b/src/components/palmistry/scanned-photo/scanned-photo.css index de39dbb..a15dc14 100644 --- a/src/components/palmistry/scanned-photo/scanned-photo.css +++ b/src/components/palmistry/scanned-photo/scanned-photo.css @@ -1,35 +1,38 @@ .scanned-photo { display: flex; - height: 477px; + /* height: 477px; */ justify-content: center; position: relative; - width: 291px; + width: 100%; + align-items: center; } .scanned-photo__container { - height: 477px; + /* background-color: red; */ + height: 100%; position: relative; - width: 291px; + width: 100%; z-index: 2; } .scanned-photo__stick { animation: scanned-photo-stick-move 5.5s cubic-bezier(0.37, 0, 0.63, 1); - animation-delay: 14.5s; + /* animation-delay: 14.5s; */ animation-fill-mode: forwards; animation-iteration-count: infinite; background-color: var(--strong-blue-text); height: 2px; - left: -2px; + /* left: -2px; */ opacity: 0; position: absolute; - width: 96px; + width: 100%; z-index: 5; } .scanned-photo__image { - height: 100%; - object-fit: cover; + /* height: 100%; */ + /* object-fit: cover; */ + object-fit: contain; width: 100%; } @@ -71,7 +74,7 @@ .scanned-photo__line { stroke-linecap: round; stroke-linejoin: round; - stroke-width: 3px; + stroke-width: 2px; fill-rule: evenodd; clip-rule: evenodd; stroke-miterlimit: 1.5; @@ -84,7 +87,7 @@ .scanned-photo__line_heart { stroke: #f8d90f; - animation-delay: 4.5s; + /* animation-delay: 4.5s; */ } .scanned-photo__line_life { @@ -93,12 +96,12 @@ .scanned-photo__line_head { stroke: #00d114; - animation-delay: 1.5s; + /* animation-delay: 1.5s; */ } .scanned-photo__line_fate { stroke: #05ced8; - animation-delay: 3s; + /* animation-delay: 3s; */ } .scanned-photo__decoration { @@ -107,19 +110,35 @@ justify-content: center; opacity: 0; animation: scanned-photo-opacity 1.5s cubic-bezier(0.37, 0, 0.63, 1); - animation-delay: 12s; + /* animation-delay: 12s; */ animation-fill-mode: forwards; height: 220px; - margin-top: -10px; position: absolute; width: 220px; } .scanned-photo__decoration__corners { animation: scanned-photo-corners-opacity 1.5s cubic-bezier(0.37, 0, 0.63, 1); - animation-delay: 13.5s; + /* animation-delay: 13.5s; */ animation-fill-mode: forwards; - background: linear-gradient(to right, var(--strong-blue-text) 2px, transparent 2px) 0 0, linear-gradient(to right, var(--strong-blue-text) 2px, transparent 2px) 0 100%, linear-gradient(to left, var(--strong-blue-text) 2px, transparent 2px) 100% 0, linear-gradient(to left, var(--strong-blue-text) 2px, transparent 2px) 100% 100%, linear-gradient(to bottom, var(--strong-blue-text) 2px, transparent 2px) 0 0, linear-gradient(to bottom, var(--strong-blue-text) 2px, transparent 2px) 100% 0, linear-gradient(to top, var(--strong-blue-text) 2px, transparent 2px) 0 100%, linear-gradient(to top, var(--strong-blue-text) 2px, transparent 2px) 100% 100%; + background: linear-gradient( + to right, + var(--strong-blue-text) 2px, + transparent 2px + ) + 0 0, + linear-gradient(to right, var(--strong-blue-text) 2px, transparent 2px) 0 + 100%, + linear-gradient(to left, var(--strong-blue-text) 2px, transparent 2px) 100% + 0, + linear-gradient(to left, var(--strong-blue-text) 2px, transparent 2px) 100% + 100%, + linear-gradient(to bottom, var(--strong-blue-text) 2px, transparent 2px) 0 0, + linear-gradient(to bottom, var(--strong-blue-text) 2px, transparent 2px) + 100% 0, + linear-gradient(to top, var(--strong-blue-text) 2px, transparent 2px) 0 100%, + linear-gradient(to top, var(--strong-blue-text) 2px, transparent 2px) 100% + 100%; background-repeat: no-repeat; background-size: 15px 15px; height: 100%; @@ -156,68 +175,74 @@ animation-fill-mode: forwards; } +.scanned-photo_small .scanned-photo__image { + height: 100%; +} + @keyframes scanned-photo-resize { 100% { height: 207px; - } + /* align-items: center; */ + } } @keyframes scanned-photo-container-resize { 100% { - border: 3px solid #fff; - height: 159px; - margin-top: 20px; - width: 97px; - } + /* border: 3px solid #fff; */ + height: 70.7%; + /* margin-top: 20px; */ + width: auto; + /* aspect-ratio: 1 / 1; */ + } } @keyframes scanned-photo-opacity { 100% { opacity: 1; - } + } } @keyframes scanned-photo-corners-opacity { 10% { opacity: 0; - } + } 20% { opacity: 0.7; - } + } 40% { opacity: 0.3; - } + } 50% { opacity: 0.6; - } + } 100% { opacity: 1; - } + } } @keyframes scanned-photo-stick-move { 0% { opacity: 1; top: 0; - } + } 50% { opacity: 1; top: 100%; - } + } 100% { opacity: 1; top: 0; - } + } } @keyframes finger-show { 100% { transform: scale(1); - } + } } @keyframes line-show { 100% { stroke-dashoffset: 0; - } + } } diff --git a/src/components/palmistry/scanned-photo/scanned-photo.tsx b/src/components/palmistry/scanned-photo/scanned-photo.tsx index 08b90ed..2b0a1e1 100644 --- a/src/components/palmistry/scanned-photo/scanned-photo.tsx +++ b/src/components/palmistry/scanned-photo/scanned-photo.tsx @@ -1,27 +1,90 @@ -import './scanned-photo.css'; +import { IPalmistryLine, IPalmistryPoint } from "@/api/resources/Palmistry"; +import "./scanned-photo.css"; +import { useCallback, useEffect, useRef, useState } from "react"; type Props = { photo: string; small: boolean; displayLines: boolean; + lines: IPalmistryLine[]; + lineChangeDelay: number; + startDelay: number; }; export default function StepScanPhoto(props: Props) { - const className = ['scanned-photo']; + const className = ["scanned-photo"]; + const { lines, lineChangeDelay } = props; + const imageRef = useRef(null); + const linesRef = useRef([]); + const [isImageLoaded, setIsImageLoaded] = useState(false); + const [imageWidth, setImageWidth] = useState(0); + const [imageHeight, setImageHeight] = useState(0); if (props.small) { - className.push('scanned-photo_small'); + className.push("scanned-photo_small"); } + useEffect(() => { + if (isImageLoaded && imageRef.current) { + setImageWidth(imageRef.current.width || 0); + setImageHeight(imageRef.current.height || 0); + } + }, [isImageLoaded]); + + const getCoordinatesString = useCallback( + (points: IPalmistryPoint[]) => { + const coordinatesString = `M ${points[0]?.x * imageWidth} ${ + points[0]?.y * imageHeight + }`; + return points.reduce( + (acc, point) => + `${acc} L ${point?.x * imageWidth} ${point?.y * imageHeight}`, + coordinatesString + ); + }, + // eslint-disable-next-line react-hooks/exhaustive-deps + [lines, isImageLoaded, imageWidth, imageHeight] + ); + + const getLineLength = (line: SVGPathElement) => { + return line?.getTotalLength(); + }; + + // const getAnimationDelayOfLine = (index: number) => { + // return `${lineChangeDelay * index + startDelay}ms`; + // }; + return ( -
+
-
+
- PalmIcon - - - + PalmIcon setIsImageLoaded(true)} + // width={imageWidth} + // height={imageHeight} + /> + {!!imageHeight && !!imageWidth && ( + + {/* - + */} - {props.displayLines && ( - <> - + {lines.map((line, index) => ( + + (linesRef.current[index] = el as SVGPathElement) + } + style={{ + strokeDasharray: + getLineLength(linesRef.current[index]) || 500, + strokeDashoffset: + getLineLength(linesRef.current[index]) || 500, + animationDelay: `${lineChangeDelay * (index + 1)}ms`, + }} + /> + ))} + {/* - */} + {/* - - )} - + /> */} + + )} + + )}
-
-
+
+
- - + + (null); + const [currentElementIndex, setCurrentElementIndex] = useState(0); + const [smallPhotoState, setSmallPhotoState] = useState(false); + const [title, setTitle] = useState(""); + const [shouldDisplayPalmLines, setShouldDisplayPalmLines] = useState(false); + + const prevElementIndex = useRef(null); const goNextElement = (delay: number) => { setTimeout(() => { + setTitle(lines[currentElementIndex]?.line); setCurrentElementIndex((prevState) => prevState + 1); }, delay); }; - React.useEffect(() => { - if (storedPhoto) { - setPhoto(storedPhoto); + useEffect(() => { + if (!lines.length) { + return navigate(routes.client.palmistryUpload()); } - }, [storedPhoto]); + }, [lines, navigate]); - React.useEffect(() => { - if (curentElementIndex < palmElements.length && curentElementIndex !== prevElementIndex.current) { - prevElementIndex.current = curentElementIndex; - setTitle(palmElements[curentElementIndex]); - goNextElement(fingers.includes(palmElements[curentElementIndex]) ? fingerChangeDelay : lineChangeDelay); - setSholdDisplayPalmLines(lines.includes(palmElements[curentElementIndex])); + useEffect(() => { + // if (currentElementIndex === 0) { + // new Promise((resolve) => setTimeout(resolve, startDelay)); + // } + if ( + currentElementIndex < lines?.length && + currentElementIndex !== prevElementIndex.current + ) { + prevElementIndex.current = currentElementIndex; + goNextElement(lineChangeDelay); + setShouldDisplayPalmLines(lines?.includes(lines[currentElementIndex])); } - if (curentElementIndex >= palmElements.length) { - setSmallPhotoState(true); - - setTimeout(steps.goNext, goNextDelay); + if (currentElementIndex >= lines?.length) { + setTimeout(() => { + setSmallPhotoState(true); + }, lineChangeDelay); + setTimeout(steps.goNext, lineChangeDelay * lines.length + 8000); } - }, [curentElementIndex]); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [currentElementIndex]); - if (!photo) return null; + // if (!storedPhoto) { + // return Upload your photo; + // } + + // console.log(shouldDisplayPalmLines); + // return Upload your photo; return ( <> -

{title}

+

+ {title} +

+ {/*
{JSON.stringify(lines, null, 2)}
*/} + - +

+ We are putting together a comprehensive Palmistry Reading just for you! +

-

We are putting together a comprehensive Palmistry Reading just for you!

- -

Wow, looks like there is a lot we can tell about your ambitious and strong self-confident future.

+

+ Wow, looks like there is a lot we can tell about your ambitious and + strong self-confident future. +

); } diff --git a/src/components/palmistry/step-upload/step-upload.tsx b/src/components/palmistry/step-upload/step-upload.tsx index 55674ef..fbb719c 100644 --- a/src/components/palmistry/step-upload/step-upload.tsx +++ b/src/components/palmistry/step-upload/step-upload.tsx @@ -1,12 +1,15 @@ -import React from 'react'; +import { useEffect, useState } from "react"; -import useSteps from '../../../hooks/palmistry/use-steps'; -import Button from '../button/button'; -import BiometricData from '../biometric-data/biometric-data'; -import UploadModal from '../upload-modal/upload-modal'; -import ModalOverlay from '../modal-overlay/modal-overlay'; -import PalmRecognitionErrorModal from '../palm-recognition-error-modal/palm-recognition-error-modal'; -import PalmCameraModal from '../palm-camera-modal/palm-camera-modal'; +import useSteps from "../../../hooks/palmistry/use-steps"; +import Button from "../button/button"; +import BiometricData from "../biometric-data/biometric-data"; +import UploadModal from "../upload-modal/upload-modal"; +import ModalOverlay from "../modal-overlay/modal-overlay"; +import PalmRecognitionErrorModal from "../palm-recognition-error-modal/palm-recognition-error-modal"; +import PalmCameraModal from "../palm-camera-modal/palm-camera-modal"; +import { useApi } from "@/api"; +import { useDispatch } from "react-redux"; +import { actions } from "@/store"; type Props = { onOpenModal: (isOpen: boolean) => void; @@ -14,12 +17,15 @@ type Props = { export default function StepUpload(props: Props) { const steps = useSteps(); + const api = useApi(); + const dispatch = useDispatch(); - const [uploadMenuModalIsOpen, setUploadMenuModalIsOpen] = React.useState(false); - const [isUpladProcessing, setIsUpladProcessing] = React.useState(false); - const [recognitionErrorModalIsOpen, setRecognitionErrorModalIsOpen] = React.useState(false); - const [palmCameraModalIsOpen, setPalmCameraModalIsOpen] = React.useState(false); - const [palmPhoto, setPalmPhoto] = React.useState(); + const [uploadMenuModalIsOpen, setUploadMenuModalIsOpen] = useState(false); + const [isUpladProcessing, setIsUpladProcessing] = useState(false); + const [recognitionErrorModalIsOpen, setRecognitionErrorModalIsOpen] = + useState(false); + const [palmCameraModalIsOpen, setPalmCameraModalIsOpen] = useState(false); + const [palmPhoto, setPalmPhoto] = useState(); // const imitateRequestError = () => { // setTimeout(() => { @@ -28,11 +34,21 @@ export default function StepUpload(props: Props) { // }, 2000); // }; - const onSelectFile = (event: React.ChangeEvent) => { + const getLines = async (file: File | Blob) => { + const formData = new FormData(); + formData.append("file", file); + const result = await api.getPalmistryLines({ formData }); + + dispatch(actions.palmistry.update({ lines: result })); + }; + + const onSelectFile = async (event: React.ChangeEvent) => { setUploadMenuModalIsOpen(false); if (!event.target.files || event.target.files.length === 0) return; + await getLines(event.target.files[0]); + setIsUpladProcessing(true); const reader = new FileReader(); @@ -44,46 +60,70 @@ export default function StepUpload(props: Props) { reader.readAsDataURL(event.target.files[0]); }; - const onTakePhoto = (photo: string) => { + const DataURIToBlob = (dataURI: string) => { + const splitDataURI = dataURI.split(","); + const byteString = + splitDataURI[0].indexOf("base64") >= 0 + ? atob(splitDataURI[1]) + : decodeURI(splitDataURI[1]); + const mimeString = splitDataURI[0].split(":")[1].split(";")[0]; + + const ia = new Uint8Array(byteString.length); + for (let i = 0; i < byteString.length; i++) + ia[i] = byteString.charCodeAt(i); + + return new Blob([ia], { type: mimeString }); + }; + + const onTakePhoto = async (photo: string) => { + const file = DataURIToBlob(photo); + await getLines(file); setPalmPhoto(photo as string); setUploadMenuModalIsOpen(false); setPalmCameraModalIsOpen(false); }; - React.useEffect(() => { + useEffect(() => { if (palmPhoto) { - fetch( - 'https://palmistry.hint.app/api/processing', - { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ image: palmPhoto }), - }, - ); + fetch("https://palmistry.hint.app/api/processing", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ image: palmPhoto }), + }); setIsUpladProcessing(false); steps.saveCurrent(palmPhoto); steps.goNext(); } + // eslint-disable-next-line react-hooks/exhaustive-deps }, [palmPhoto]); - React.useEffect(() => { + useEffect(() => { if (recognitionErrorModalIsOpen || palmCameraModalIsOpen) { props.onOpenModal(true); } else { props.onOpenModal(false); } + // eslint-disable-next-line react-hooks/exhaustive-deps }, [recognitionErrorModalIsOpen, palmCameraModalIsOpen]); return ( <> -

Take your palm picture as instructed

+

+ Take your palm picture as instructed +

Correct

- + - + - + @@ -184,7 +235,12 @@ export default function StepUpload(props: Props) {

Wrong

- + - + - - + + - - - + + + - + - - + + - + - + - - + + - + - + - - + +
@@ -473,10 +614,10 @@ export default function StepUpload(props: Props) { onClick={() => setUploadMenuModalIsOpen(true)} isProcessing={isUpladProcessing} > - {isUpladProcessing && 'Loading photo' || 'Take a picture now'} + {(isUpladProcessing && "Loading photo") || "Take a picture now"} - + {uploadMenuModalIsOpen && ( )} - {isUpladProcessing && } + {isUpladProcessing && } {recognitionErrorModalIsOpen && ( - setRecognitionErrorModalIsOpen(false)}/> + setRecognitionErrorModalIsOpen(false)} + /> )} {palmCameraModalIsOpen && ( diff --git a/src/routes.ts b/src/routes.ts index be6867f..de38b8e 100755 --- a/src/routes.ts +++ b/src/routes.ts @@ -220,6 +220,9 @@ const routes = { assistants: () => [apiHost, prefix, "ai", "assistants.json"].join("/"), setExternalChatIdAssistants: (chatId: string) => [apiHost, prefix, "ai", "assistants", chatId, "chats.json"].join("/"), + // Palmistry + getPalmistryLines: () => + ["https://api.aura.witapps.us", "palmistry", "lines"].join("/"), }, openAi: { createThread: () => [openAIHost, openAiPrefix, "threads"].join("/"), diff --git a/src/store/index.ts b/src/store/index.ts index 92d7533..fe822d4 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -63,6 +63,10 @@ import { selectUserCallbacksDescription, selectUserCallbacksPrevStat, } from "./userCallbacks"; +import palmistry, { + actions as palmistryActions, + selectPalmistryLines, +} from "./palmistry"; const preloadedState = loadStore(); export const actions = { @@ -80,6 +84,7 @@ export const actions = { onboardingConfig: onboardingConfigActions, questionnaire: questionnaireActions, userConfig: userConfigActions, + palmistry: palmistryActions, reset: createAction("reset"), }; export const selectors = { @@ -109,6 +114,7 @@ export const selectors = { selectIsShowTryApp, selectIsForceShortPath, selectOpenAiToken, + selectPalmistryLines, ...formSelectors, }; @@ -127,6 +133,7 @@ export const reducer = combineReducers({ onboardingConfig, questionnaire, userConfig, + palmistry, }); export type RootState = ReturnType; diff --git a/src/store/palmistry.ts b/src/store/palmistry.ts new file mode 100644 index 0000000..2a2bf7c --- /dev/null +++ b/src/store/palmistry.ts @@ -0,0 +1,29 @@ +import { IPalmistryLine } from "@/api/resources/Palmistry"; +import { createSlice, createSelector } from "@reduxjs/toolkit"; +import type { PayloadAction } from "@reduxjs/toolkit"; + +interface IPalmistry { + lines: IPalmistryLine[]; +} + +const initialState: IPalmistry = { + lines: [], +}; + +const palmistrySlice = createSlice({ + name: "palmistry", + initialState, + reducers: { + update(state, action: PayloadAction>) { + return { ...state, ...action.payload }; + }, + }, + extraReducers: (builder) => builder.addCase("reset", () => initialState), +}); + +export const { actions } = palmistrySlice; +export const selectPalmistryLines = createSelector( + (state: { palmistry: IPalmistry }) => state.palmistry.lines, + (palmistry) => palmistry +); +export default palmistrySlice.reducer;