This commit is contained in:
Daniil Chemerkin 2024-06-18 22:10:03 +00:00
parent 498ccdddea
commit 9d99d6f90e
22 changed files with 235 additions and 458 deletions

View File

@ -4,10 +4,19 @@ export interface Payload {
formData: FormData; formData: FormData;
} }
export type Response = IPalmistryLine[]; export type Response = {
fingers: IPalmistryFinger[];
lines: IPalmistryLine[];
};
export interface IPalmistryFinger {
name: string;
point: IPalmistryPoint;
}
export interface IPalmistryLine { export interface IPalmistryLine {
line: string; name: string;
line: number;
points: IPalmistryPoint[]; points: IPalmistryPoint[];
} }

View File

@ -7,6 +7,7 @@ interface ModalProps {
isCloseButtonVisible?: boolean; isCloseButtonVisible?: boolean;
className?: string; className?: string;
containerClassName?: string; containerClassName?: string;
type?: "hidden" | "normal";
onClose?: () => void; onClose?: () => void;
} }
@ -16,6 +17,7 @@ function Modal({
isCloseButtonVisible = true, isCloseButtonVisible = true,
className = "", className = "",
containerClassName = "", containerClassName = "",
type = "normal",
onClose, onClose,
}: ModalProps): JSX.Element { }: ModalProps): JSX.Element {
const handleClose = (event: React.MouseEvent) => { const handleClose = (event: React.MouseEvent) => {
@ -34,9 +36,14 @@ function Modal({
}; };
}, [open]); }, [open]);
if (!open) return <></>; if (!open && type === "normal") return <></>;
return ( return (
<div className={`${styles.modal} ${className}`} onClick={handleClose}> <div
className={`${styles.modal} ${className} ${
type === "hidden" && !open ? styles.hidden : ""
}`}
onClick={handleClose}
>
<div className={`${styles["modal-content"]} ${containerClassName}`}> <div className={`${styles["modal-content"]} ${containerClassName}`}>
{isCloseButtonVisible && ( {isCloseButtonVisible && (
<button className={styles["modal-close-btn"]} onClick={handleClose} /> <button className={styles["modal-close-btn"]} onClick={handleClose} />

View File

@ -12,6 +12,11 @@
z-index: 2000; z-index: 2000;
} }
.hidden {
height: 0;
visibility: hidden;
}
.modal-content { .modal-content {
position: absolute; position: absolute;
background: #fff; background: #fff;

View File

@ -1,6 +1,6 @@
import Title from "@/components/Title"; import Title from "@/components/Title";
import styles from "./styles.module.css"; import styles from "./styles.module.css";
import PaymentMethodsChoice from "../PaymentMethodsChoice"; import PaymentMethodsChoice from "../pages/TrialPayment/components/PaymentMethodsChoice";
import { useEffect, useMemo, useState } from "react"; import { useEffect, useMemo, useState } from "react";
import { EPaymentMethod, paymentMethods } from "@/data/paymentMethods"; import { EPaymentMethod, paymentMethods } from "@/data/paymentMethods";
import { Elements } from "@stripe/react-stripe-js"; import { Elements } from "@stripe/react-stripe-js";
@ -9,7 +9,7 @@ import { AvailablePaymentMethods, Stripe, loadStripe } from "@stripe/stripe-js";
import { useSelector } from "react-redux"; import { useSelector } from "react-redux";
import { selectors } from "@/store"; import { selectors } from "@/store";
import Loader from "@/components/Loader"; import Loader from "@/components/Loader";
import SecurityPayments from "../SecurityPayments"; import SecurityPayments from "../pages/TrialPayment/components/SecurityPayments";
import { EPlacementKeys, IPaywallProduct } from "@/api/resources/Paywall"; import { EPlacementKeys, IPaywallProduct } from "@/api/resources/Paywall";
import { usePaywall } from "@/hooks/paywall/usePaywall"; import { usePaywall } from "@/hooks/paywall/usePaywall";
import { useMakePayment } from "@/hooks/payment/useMakePayment"; import { useMakePayment } from "@/hooks/payment/useMakePayment";
@ -21,7 +21,7 @@ interface IPaymentModalProps {
activeProduct?: IPaywallProduct; activeProduct?: IPaywallProduct;
noTrial?: boolean; noTrial?: boolean;
returnUrl?: string; returnUrl?: string;
placementKey?: EPlacementKeys; placementKey: EPlacementKeys;
} }
const getPrice = (product: IPaywallProduct | null) => { const getPrice = (product: IPaywallProduct | null) => {
@ -35,7 +35,7 @@ function PaymentModal({
activeProduct, activeProduct,
noTrial, noTrial,
returnUrl, returnUrl,
placementKey = EPlacementKeys["aura.placement.main"], placementKey,
}: IPaymentModalProps) { }: IPaymentModalProps) {
const navigate = useNavigate(); const navigate = useNavigate();
const [stripePromise, setStripePromise] = const [stripePromise, setStripePromise] =

View File

@ -40,7 +40,7 @@ function EmailEnterPage({
const [isAuth, setIsAuth] = useState(false); const [isAuth, setIsAuth] = useState(false);
const { subPlan } = useParams(); const { subPlan } = useParams();
const { width: pageWidth, elementRef: pageRef } = useDynamicSize({}); const { width: pageWidth, elementRef: pageRef } = useDynamicSize({});
const { error, isLoading, authorization } = useAuthentication(); const { error, isLoading, token, user, authorization } = useAuthentication();
const { gender } = useSelector(selectors.selectQuestionnaire); const { gender } = useSelector(selectors.selectQuestionnaire);
const activeProductFromStore = useSelector(selectors.selectActiveProduct); const activeProductFromStore = useSelector(selectors.selectActiveProduct);
const { products } = usePaywall({ const { products } = usePaywall({
@ -106,17 +106,34 @@ function EmailEnterPage({
source = ESourceAuthorization["aura.moons"]; source = ESourceAuthorization["aura.moons"];
} }
await authorization(email, source); await authorization(email, source);
dispatch(
actions.payment.update({
activeProduct,
})
);
setIsAuth(true);
setTimeout(() => {
navigate(redirectUrl);
}, 1000);
}; };
useEffect(() => {
if (user && token?.length && !isLoading && !error) {
dispatch(
actions.payment.update({
activeProduct,
})
);
setIsAuth(true);
const timeout = setTimeout(() => {
navigate(redirectUrl);
}, 1000);
return () => {
clearTimeout(timeout);
};
}
}, [
activeProduct,
dispatch,
error,
isLoading,
navigate,
redirectUrl,
token?.length,
user,
]);
return ( return (
<section <section
className={`${styles.page} page`} className={`${styles.page} page`}
@ -134,11 +151,11 @@ function EmailEnterPage({
</Title> </Title>
<p className={styles["not-share"]}>{t("we_dont_share")}</p> <p className={styles["not-share"]}>{t("we_dont_share")}</p>
<EmailInput <EmailInput
name="email" name="email"
value={email} value={email}
placeholder={t("your_email")} placeholder={t("your_email")}
onValid={handleValidEmail} onValid={handleValidEmail}
onInvalid={() => setIsValidEmail(false)} onInvalid={() => setIsValidEmail(false)}
/> />
<NameInput <NameInput
value={name} value={name}

View File

@ -1,197 +0,0 @@
import Title from "@/components/Title";
import styles from "./styles.module.css";
import PaymentMethodsChoice from "../PaymentMethodsChoice";
import { useEffect, useMemo, useState } from "react";
import { EPaymentMethod, paymentMethods } from "@/data/paymentMethods";
import { Elements } from "@stripe/react-stripe-js";
import CheckoutForm from "@/components/PaymentPage/methods/CheckoutForm";
import { AvailablePaymentMethods, Stripe, loadStripe } from "@stripe/stripe-js";
import { useSelector } from "react-redux";
import { selectors } from "@/store";
import { useNavigate } from "react-router-dom";
import routes from "@/routes";
import Loader from "@/components/Loader";
import SecurityPayments from "../SecurityPayments";
import { EPlacementKeys, IPaywallProduct } from "@/api/resources/Paywall";
import { usePaywall } from "@/hooks/paywall/usePaywall";
import { useMakePayment } from "@/hooks/payment/useMakePayment";
import ExpressCheckoutStripe from "@/components/PaymentPage/methods/ExpressCheckoutStripe";
interface IPaymentModalProps {
activeProduct?: IPaywallProduct;
noTrial?: boolean;
returnUrl?: string;
placementKey?: EPlacementKeys;
}
const getPrice = (product: IPaywallProduct) => {
return (product.trialPrice || 0) / 100;
};
function PaymentModal({
activeProduct,
noTrial,
returnUrl,
placementKey = EPlacementKeys["aura.placement.redesign.main"],
}: IPaymentModalProps) {
const navigate = useNavigate();
const [stripePromise, setStripePromise] =
useState<Promise<Stripe | null> | null>(null);
const activeProductFromStore = useSelector(selectors.selectActiveProduct);
const _activeProduct = activeProduct ? activeProduct : activeProductFromStore;
const { products, paywallId, placementId } = usePaywall({ placementKey });
const {
paymentIntentId,
clientSecret,
returnUrl: checkoutUrl,
paymentType,
publicKey,
isLoading: isLoadingPayment,
error,
} = useMakePayment({
productId: _activeProduct?._id || "",
paywallId,
placementId,
returnPaidUrl: returnUrl,
});
const [availableMethods, setAvailableMethods] = useState<
AvailablePaymentMethods | undefined
>();
const [isLoadingExpressCheckout, setIsLoadingExpressCheckout] =
useState(true);
const isLoading = useMemo(() => {
return isLoadingPayment || isLoadingExpressCheckout;
}, [isLoadingPayment, isLoadingExpressCheckout]);
if (checkoutUrl?.length) {
window.location.href = checkoutUrl;
}
const paymentMethodsButtons = useMemo(() => {
return paymentMethods(availableMethods || null);
}, [availableMethods]);
const [selectedPaymentMethod, setSelectedPaymentMethod] = useState(
paymentMethodsButtons[0].id
);
const onSelectPaymentMethod = (method: EPaymentMethod) => {
setSelectedPaymentMethod(method);
};
useEffect(() => {
(async () => {
if (!products?.length || !publicKey) return;
setStripePromise(loadStripe(publicKey));
const isActiveProduct = products.find(
(product) => product._id === _activeProduct?._id
);
if (!_activeProduct || !isActiveProduct) {
navigate(routes.client.trialChoiceV1());
}
})();
}, [_activeProduct, navigate, products, publicKey]);
const onAvailableExpressCheckout = (
isAvailable: boolean,
availableMethods: AvailablePaymentMethods | undefined
) => {
if (isAvailable && availableMethods) {
setAvailableMethods(availableMethods);
return setSelectedPaymentMethod(EPaymentMethod.PAYMENT_BUTTONS);
}
return setAvailableMethods(undefined);
};
if (error?.length) {
return (
<div className={styles["payment-modal"]}>
<Title variant="h3" className={styles.title}>
Something went wrong
</Title>
</div>
);
}
return (
<>
{isLoading && (
<div className={styles["payment-modal"]}>
<div className={styles["payment-loader"]}>
<Loader />
</div>
</div>
)}
<div
className={`${styles["payment-modal"]} ${isLoading ? styles.hide : ""}`}
>
<Title variant="h3" className={styles.title}>
Choose payment method
</Title>
<PaymentMethodsChoice
paymentMethods={paymentMethodsButtons}
selectedPaymentMethod={selectedPaymentMethod}
onSelectPaymentMethod={onSelectPaymentMethod}
/>
{_activeProduct && (
<div>
{!noTrial && (
<>
<p className={styles["sub-plan-description"]}>
You will be charged only{" "}
<b>${getPrice(_activeProduct)} for your 3-day trial.</b>
</p>
<p className={styles["sub-plan-description"]}>
We`ll <b>email you a reminder</b> before your trial period
ends.
</p>
</>
)}
<p className={styles["sub-plan-description"]}>
Cancel anytime. The charge will appear on your bill as witapps.
</p>
</div>
)}
<div className={styles["payment-method-container"]}>
{stripePromise && clientSecret && (
<>
<Elements stripe={stripePromise} options={{ clientSecret }}>
<ExpressCheckoutStripe
clientSecret={clientSecret}
returnUrl={returnUrl}
isHide={
selectedPaymentMethod !== EPaymentMethod.PAYMENT_BUTTONS
}
onAvailable={(_isAvailable, _availableMethods) =>
onAvailableExpressCheckout(_isAvailable, _availableMethods)
}
onChangeLoading={(isLoading) =>
setIsLoadingExpressCheckout(isLoading)
}
/>
</Elements>
<Elements stripe={stripePromise} options={{ clientSecret }}>
<CheckoutForm
confirmType={paymentType}
subscriptionReceiptId={paymentIntentId}
returnUrl={returnUrl}
isHide={selectedPaymentMethod !== EPaymentMethod.CREDIT_CARD}
/>
</Elements>
</>
)}
</div>
<SecurityPayments />
<p className={styles.address}>1123 Rimer Dr Moraga, California 94556</p>
</div>
</>
);
}
export default PaymentModal;

View File

@ -15,7 +15,6 @@ import OftenAsk from "./components/OftenAsk";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import WithPartnerInformation from "./components/WithPartnerInformation"; import WithPartnerInformation from "./components/WithPartnerInformation";
import Modal from "@/components/Modal"; import Modal from "@/components/Modal";
import PaymentModal from "./components/PaymentModal";
import { trialPaymentPointsList } from "@/data/pointsLists"; import { trialPaymentPointsList } from "@/data/pointsLists";
import { trialPaymentReviews } from "@/data/reviews"; import { trialPaymentReviews } from "@/data/reviews";
import TrialPaymentHeader from "./components/Header"; import TrialPaymentHeader from "./components/Header";
@ -24,6 +23,7 @@ import BackgroundTopBlob from "../../ui/BackgroundTopBlob";
import { useDynamicSize } from "@/hooks/useDynamicSize"; import { useDynamicSize } from "@/hooks/useDynamicSize";
import { EPlacementKeys, IPaywallProduct } from "@/api/resources/Paywall"; import { EPlacementKeys, IPaywallProduct } from "@/api/resources/Paywall";
import { usePaywall } from "@/hooks/paywall/usePaywall"; import { usePaywall } from "@/hooks/paywall/usePaywall";
import PaymentModal from "@/components/PaymentModal";
function TrialPaymentPage() { function TrialPaymentPage() {
const dispatch = useDispatch(); const dispatch = useDispatch();
@ -118,8 +118,11 @@ function TrialPaymentPage() {
containerClassName={styles.modal} containerClassName={styles.modal}
open={isOpenPaymentModal} open={isOpenPaymentModal}
onClose={handleDiscount} onClose={handleDiscount}
type="hidden"
> >
<PaymentModal /> <PaymentModal
placementKey={EPlacementKeys["aura.placement.redesign.main"]}
/>
</Modal> </Modal>
<BackgroundTopBlob <BackgroundTopBlob
width={pageWidth} width={pageWidth}

View File

@ -3,12 +3,12 @@ import styles from "./styles.module.css";
import MainButton from "@/components/MainButton"; import MainButton from "@/components/MainButton";
import PaymentDiscountTable from "./PaymentDiscountTable"; import PaymentDiscountTable from "./PaymentDiscountTable";
import Modal from "@/components/Modal"; import Modal from "@/components/Modal";
import PaymentModal from "../TrialPayment/components/PaymentModal";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { actions, selectors } from "@/store"; import { actions, selectors } from "@/store";
import { useDispatch, useSelector } from "react-redux"; import { useDispatch, useSelector } from "react-redux";
import { usePaywall } from "@/hooks/paywall/usePaywall"; import { usePaywall } from "@/hooks/paywall/usePaywall";
import { EPlacementKeys } from "@/api/resources/Paywall"; import { EPlacementKeys } from "@/api/resources/Paywall";
import PaymentModal from "@/components/PaymentModal";
function TrialPaymentWithDiscount() { function TrialPaymentWithDiscount() {
const dispatch = useDispatch(); const dispatch = useDispatch();
@ -38,7 +38,7 @@ function TrialPaymentWithDiscount() {
return ( return (
<section className={`${styles.page} page`}> <section className={`${styles.page} page`}>
<Modal open={isOpenPaymentModal} onClose={handleClose}> <Modal open={isOpenPaymentModal} onClose={handleClose} type="hidden">
<PaymentModal <PaymentModal
placementKey={EPlacementKeys["aura.placement.secret.discount"]} placementKey={EPlacementKeys["aura.placement.secret.discount"]}
/> />

View File

@ -3,7 +3,7 @@ import styles from "./styles.module.css";
import ReservedTimer from "./components/ReservedTimer"; import ReservedTimer from "./components/ReservedTimer";
import MainButton from "@/components/MainButton"; import MainButton from "@/components/MainButton";
import Modal from "@/components/Modal"; import Modal from "@/components/Modal";
import PaymentModal from "../../TrialPayment/components/PaymentModal"; import PaymentModal from "@/components/PaymentModal";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { usePaywall } from "@/hooks/paywall/usePaywall"; import { usePaywall } from "@/hooks/paywall/usePaywall";
import { EPlacementKeys } from "@/api/resources/Paywall"; import { EPlacementKeys } from "@/api/resources/Paywall";
@ -37,6 +37,7 @@ function MarketingTrialPayment() {
containerClassName={styles.modal} containerClassName={styles.modal}
open={isOpenPaymentModal} open={isOpenPaymentModal}
onClose={handleCloseModal} onClose={handleCloseModal}
type="hidden"
> >
<PaymentModal <PaymentModal
placementKey={EPlacementKeys["aura.placement.email.marketing"]} placementKey={EPlacementKeys["aura.placement.email.marketing"]}
@ -52,7 +53,9 @@ function MarketingTrialPayment() {
<p className={styles.description}>No pressure. Cancel anytime</p> <p className={styles.description}>No pressure. Cancel anytime</p>
<div className={styles["total-today"]}> <div className={styles["total-today"]}>
<p className={styles.description}>Total today:</p> <p className={styles.description}>Total today:</p>
<p className={styles.value}>${(products[0]?.trialPrice / 100).toFixed(2) || 0}</p> <p className={styles.value}>
${(products[0]?.trialPrice / 100).toFixed(2) || 0}
</p>
</div> </div>
<div className={styles.line} /> <div className={styles.line} />
<div className={styles["code-container"]}> <div className={styles["code-container"]}>
@ -71,7 +74,11 @@ function MarketingTrialPayment() {
<p className={styles["sale-description"]}>Save $10 every period</p> <p className={styles["sale-description"]}>Save $10 every period</p>
<div className={styles.line} /> <div className={styles.line} />
<p className={styles["text-description"]}> <p className={styles["text-description"]}>
You will be charged only <b>${(products[0]?.trialPrice / 100).toFixed(2) || 0} for your 7-day trial.</b>{" "} You will be charged only{" "}
<b>
${(products[0]?.trialPrice / 100).toFixed(2) || 0} for your 7-day
trial.
</b>{" "}
Subscription <b>renews automatically</b> until cancelled. You{" "} Subscription <b>renews automatically</b> until cancelled. You{" "}
<b>can cancel at any time</b> before the end of the trial. <b>can cancel at any time</b> before the end of the trial.
</p> </p>

View File

@ -1,54 +0,0 @@
.payment-modal {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 250px;
gap: 25px;
color: #2f2e37;
}
.payment-modal.hide {
min-height: 0;
height: 0;
opacity: 0;
}
.title {
font-weight: 700;
font-size: 20px;
line-height: 20px;
text-align: center;
margin: 0;
}
.sub-plan-description {
font-size: 12px;
text-align: center;
line-height: 150%;
}
.payment-method-container {
width: 100%;
display: flex;
flex-direction: column;
gap: 24px;
}
.address {
margin-bottom: 24px;
text-transform: uppercase;
}
.payment-method {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 16px;
}
.address {
color: gray;
font-size: 10px;
}

View File

@ -16,7 +16,7 @@ import OftenAsk from "./components/OftenAsk";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import WithPartnerInformation from "./components/WithPartnerInformation"; import WithPartnerInformation from "./components/WithPartnerInformation";
import Modal from "@/components/Modal"; import Modal from "@/components/Modal";
import PaymentModal from "./components/PaymentModal"; import PaymentModal from "@/components/PaymentModal";
import { trialPaymentPointsList } from "@/data/pointsLists"; import { trialPaymentPointsList } from "@/data/pointsLists";
import { trialPaymentReviews } from "@/data/reviews"; import { trialPaymentReviews } from "@/data/reviews";
import { usePaywall } from "@/hooks/paywall/usePaywall"; import { usePaywall } from "@/hooks/paywall/usePaywall";
@ -111,8 +111,9 @@ function TrialPaymentPage() {
containerClassName={styles.modal} containerClassName={styles.modal}
open={isOpenPaymentModal} open={isOpenPaymentModal}
onClose={handleDiscount} onClose={handleDiscount}
type="hidden"
> >
<PaymentModal /> <PaymentModal placementKey={EPlacementKeys["aura.placement.main"]} />
</Modal> </Modal>
<Header buttonClick={openStripeModal} /> <Header buttonClick={openStripeModal} />
{singleOrWithPartner === "partner" && ( {singleOrWithPartner === "partner" && (

View File

@ -3,7 +3,7 @@ import styles from "./styles.module.css";
import MainButton from "@/components/MainButton"; import MainButton from "@/components/MainButton";
import PaymentDiscountTable from "./PaymentDiscountTable"; import PaymentDiscountTable from "./PaymentDiscountTable";
import Modal from "@/components/Modal"; import Modal from "@/components/Modal";
import PaymentModal from "../TrialPayment/components/PaymentModal"; import PaymentModal from "@/components/PaymentModal";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { usePaywall } from "@/hooks/paywall/usePaywall"; import { usePaywall } from "@/hooks/paywall/usePaywall";
import { EPlacementKeys } from "@/api/resources/Paywall"; import { EPlacementKeys } from "@/api/resources/Paywall";
@ -39,7 +39,7 @@ function TrialPaymentWithDiscount() {
return ( return (
<section className={`${styles.page} page`}> <section className={`${styles.page} page`}>
<Modal open={isOpenPaymentModal} onClose={handleClose}> <Modal open={isOpenPaymentModal} onClose={handleClose} type="hidden">
<PaymentModal <PaymentModal
placementKey={EPlacementKeys["aura.placement.secret.discount"]} placementKey={EPlacementKeys["aura.placement.secret.discount"]}
/> />

View File

@ -7,7 +7,7 @@ import "./payment-screen.css";
import useSteps, { Step } from "@/hooks/palmistry/use-steps"; import useSteps, { Step } from "@/hooks/palmistry/use-steps";
import useTimer from "@/hooks/palmistry/use-timer"; import useTimer from "@/hooks/palmistry/use-timer";
import HeaderLogo from "@/components/palmistry/header-logo/header-logo"; import HeaderLogo from "@/components/palmistry/header-logo/header-logo";
import PaymentModal from "@/components/pages/TrialPayment/components/PaymentModal"; import PaymentModal from "@/components/PaymentModal";
import { selectors } from "@/store"; import { selectors } from "@/store";
import { EPlacementKeys } from "@/api/resources/Paywall"; import { EPlacementKeys } from "@/api/resources/Paywall";
import { useSearchParams } from "react-router-dom"; import { useSearchParams } from "react-router-dom";

View File

@ -51,26 +51,6 @@
transform-origin: center center; transform-origin: center center;
} }
.scanned-photo__finger-point_thumb {
animation-delay: 0s;
}
.scanned-photo__finger-point_index {
animation-delay: 1s;
}
.scanned-photo__finger-point_middle {
animation-delay: 2s;
}
.scanned-photo__finger-point_ring {
animation-delay: 3s;
}
.scanned-photo__finger-point_little {
animation-delay: 4s;
}
.scanned-photo__line { .scanned-photo__line {
stroke-linecap: round; stroke-linecap: round;
stroke-linejoin: round; stroke-linejoin: round;

View File

@ -1,4 +1,8 @@
import { IPalmistryLine, IPalmistryPoint } from "@/api/resources/Palmistry"; import {
IPalmistryFinger,
IPalmistryLine,
IPalmistryPoint,
} from "@/api/resources/Palmistry";
import "./scanned-photo.css"; import "./scanned-photo.css";
import { useCallback, useEffect, useRef, useState } from "react"; import { useCallback, useEffect, useRef, useState } from "react";
@ -7,13 +11,15 @@ type Props = {
small: boolean; small: boolean;
displayLines: boolean; displayLines: boolean;
lines: IPalmistryLine[]; lines: IPalmistryLine[];
lineChangeDelay: number; fingers: IPalmistryFinger[];
drawElementChangeDelay: number;
startDelay: number; startDelay: number;
drawElements: Array<IPalmistryLine | IPalmistryFinger>;
}; };
export default function StepScanPhoto(props: Props) { export default function StepScanPhoto(props: Props) {
const className = ["scanned-photo"]; const className = ["scanned-photo"];
const { lines, lineChangeDelay } = props; const { lines, drawElementChangeDelay, fingers, drawElements } = props;
const imageRef = useRef<HTMLImageElement>(null); const imageRef = useRef<HTMLImageElement>(null);
const linesRef = useRef<SVGPathElement[]>([]); const linesRef = useRef<SVGPathElement[]>([]);
const [isImageLoaded, setIsImageLoaded] = useState(false); const [isImageLoaded, setIsImageLoaded] = useState(false);
@ -51,7 +57,7 @@ export default function StepScanPhoto(props: Props) {
}; };
// const getAnimationDelayOfLine = (index: number) => { // const getAnimationDelayOfLine = (index: number) => {
// return `${lineChangeDelay * index + startDelay}ms`; // return `${drawElementChangeDelay * index + startDelay}ms`;
// }; // };
return ( return (
@ -65,7 +71,9 @@ export default function StepScanPhoto(props: Props) {
<div <div
className="scanned-photo__stick" className="scanned-photo__stick"
style={{ style={{
animationDelay: `${lineChangeDelay * lines.length + 2500}ms`, animationDelay: `${
drawElementChangeDelay * drawElements?.length + 2500
}ms`,
maxWidth: `${imageWidth}px`, maxWidth: `${imageWidth}px`,
}} }}
/> />
@ -84,112 +92,52 @@ export default function StepScanPhoto(props: Props) {
viewBox={`0 0 ${imageWidth} ${imageHeight}`} viewBox={`0 0 ${imageWidth} ${imageHeight}`}
className="scanned-photo__svg-objects" className="scanned-photo__svg-objects"
> >
{/* <svg x="235" y="211" height="24px" width="24px"> {fingers.map((finger, index) => {
<circle return (
cx="50%" <svg
cy="50%" x={finger.point.x * imageWidth - 12}
r="11" y={finger.point.y * imageHeight - 12}
fill="white" height="24px"
opacity="0.3" width="24px"
className="scanned-photo__finger-point scanned-photo__finger-point_thumb" key={index}
/> >
<circle <circle
cx="50%" cx="50%"
cy="50%" cy="50%"
r="5" r="11"
fill="#066FDE" fill="white"
stroke="white" opacity="0.3"
strokeWidth="0.3" className="scanned-photo__finger-point"
className="scanned-photo__finger-point scanned-photo__finger-point_thumb" style={{
/> animationDelay: `${
</svg> drawElementChangeDelay * (index + 1)
}ms`,
<svg x="172" y="38" height="24px" width="24px"> }}
<circle />
cx="50%" <circle
cy="50%" cx="50%"
r="11" cy="50%"
fill="white" r="5"
opacity="0.3" fill="#066FDE"
className="scanned-photo__finger-point scanned-photo__finger-point_index" stroke="white"
/> strokeWidth="0.3"
<circle className="scanned-photo__finger-point"
cx="50%" style={{
cy="50%" animationDelay: `${
r="5" drawElementChangeDelay * (index + 1)
fill="#066FDE" }ms`,
stroke="white" }}
strokeWidth="0.3" />
className="scanned-photo__finger-point scanned-photo__finger-point_index" </svg>
/> );
</svg> })}
<svg x="125" y="10" height="24px" width="24px">
<circle
cx="50%"
cy="50%"
r="11"
fill="white"
opacity="0.3"
className="scanned-photo__finger-point scanned-photo__finger-point_middle"
/>
<circle
cx="50%"
cy="50%"
r="5"
fill="#066FDE"
stroke="white"
strokeWidth="0.3"
className="scanned-photo__finger-point scanned-photo__finger-point_middle"
/>
</svg>
<svg x="81" y="41" height="24px" width="24px">
<circle
cx="50%"
cy="50%"
r="11"
fill="white"
opacity="0.3"
className="scanned-photo__finger-point scanned-photo__finger-point_ring"
/>
<circle
cx="50%"
cy="50%"
r="5"
fill="#066FDE"
stroke="white"
strokeWidth="0.3"
className="scanned-photo__finger-point scanned-photo__finger-point_ring"
/>
</svg>
<svg x="32" y="113" height="24px" width="24px">
<circle
cx="50%"
cy="50%"
r="11"
fill="white"
opacity="0.3"
className="scanned-photo__finger-point scanned-photo__finger-point_little"
/>
<circle
cx="50%"
cy="50%"
r="5"
fill="#066FDE"
stroke="white"
strokeWidth="0.3"
className="scanned-photo__finger-point scanned-photo__finger-point_little"
/>
</svg> */}
{props.displayLines && ( {props.displayLines && (
<> <>
{lines.map((line, index) => ( {lines.map((line, index) => (
<path <path
key={index} key={index}
className={`scanned-photo__line scanned-photo__line_${line?.line}`} className={`scanned-photo__line scanned-photo__line_${line?.name}`}
d={getCoordinatesString(line?.points)} d={getCoordinatesString(line?.points)}
ref={(el) => ref={(el) =>
(linesRef.current[index] = el as SVGPathElement) (linesRef.current[index] = el as SVGPathElement)
@ -199,30 +147,12 @@ export default function StepScanPhoto(props: Props) {
getLineLength(linesRef.current[index]) || 500, getLineLength(linesRef.current[index]) || 500,
strokeDashoffset: strokeDashoffset:
getLineLength(linesRef.current[index]) || 500, getLineLength(linesRef.current[index]) || 500,
animationDelay: `${lineChangeDelay * (index + 1)}ms`, animationDelay: `${
drawElementChangeDelay * (index + 1)
}ms`,
}} }}
/> />
))} ))}
{/* <path
className={`scanned-photo__line scanned-photo__line_heart`}
d="M 95 334 L 99 330 L 104 327 L 109 323 L 113 319 L 118 315 L 123 311 L 128 308 L 132 304 L 137 301 L 142 298 L 146 296 L 151 293 L 156 291 L 160 289 L 165 287 L 170 286 L 174 284 L 179 283 L 184 283 L 189 282 L 193 282 L 198 283 L 203 284 L 207 285"
style={{ strokeDasharray: 128.14, strokeDashoffset: 128.14 }}
/> */}
{/* <path
className="scanned-photo__line scanned-photo__line_life"
d="M 205 283 L 193 291 L 181 299 L 170 306 L 160 314 L 153 322 L 147 329 L 143 337 L 139 345 L 136 352 L 133 360 L 130 368 L 128 376 L 126 383 L 125 391 L 125 399 L 126 406 L 128 414 L 132 422 L 137 429 L 143 437 L 149 445 L 156 452"
style={{ strokeDasharray: 211.483, strokeDashoffset: 211.483 }}
/>
<path
className="scanned-photo__line scanned-photo__line_head"
d="M 24 316 L 29 316 L 34 315 L 38 315 L 43 314 L 48 313 L 52 312 L 57 312 L 62 311 L 67 310 L 71 309 L 76 307 L 81 305 L 85 303 L 90 301 L 95 298 L 99 296 L 104 294 L 109 292 L 113 289 L 118 287 L 123 284 L 128 280 L 132 276 L 137 271 L 142 265"
style={{ strokeDasharray: 132.6, strokeDashoffset: 132.6 }}
/>
<path
className="scanned-photo__line scanned-photo__line_fate"
d="M 134 260 L 129 299 L 125 306 L 122 314 L 120 322 L 118 329 L 116 337 L 115 345 L 114 352"
style={{ strokeDasharray: 94.8313, strokeDashoffset: 94.8313 }}
/> */}
</> </>
)} )}
</svg> </svg>
@ -232,13 +162,15 @@ export default function StepScanPhoto(props: Props) {
<div <div
className="scanned-photo__decoration" className="scanned-photo__decoration"
style={{ style={{
animationDelay: `${lineChangeDelay * lines?.length}ms`, animationDelay: `${drawElementChangeDelay * drawElements?.length}ms`,
}} }}
> >
<div <div
className="scanned-photo__decoration__corners" className="scanned-photo__decoration__corners"
style={{ style={{
animationDelay: `${lineChangeDelay * lines?.length + 1500}ms`, animationDelay: `${
drawElementChangeDelay * drawElements?.length + 1500
}ms`,
}} }}
> >
<div className="scanned-photo__decoration__light-blue-circle" /> <div className="scanned-photo__decoration__light-blue-circle" />
@ -270,7 +202,9 @@ export default function StepScanPhoto(props: Props) {
> >
<animateTransform <animateTransform
attributeName="transform" attributeName="transform"
dur={`${lineChangeDelay * lines?.length + 3000}ms`} dur={`${
drawElementChangeDelay * drawElements?.length + 3000
}ms`}
type="rotate" type="rotate"
from="0 110 110" from="0 110 110"
to="360 110 110" to="360 110 110"

View File

@ -1,4 +1,4 @@
import { useEffect, useRef, useState } from "react"; import { useEffect, useMemo, useRef, useState } from "react";
import { Step } from "@/hooks/palmistry/use-steps"; import { Step } from "@/hooks/palmistry/use-steps";
import useSteps from "@/hooks/palmistry/use-steps"; import useSteps from "@/hooks/palmistry/use-steps";
@ -7,8 +7,10 @@ import { useSelector } from "react-redux";
import { selectors } from "@/store"; import { selectors } from "@/store";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import routes from "@/routes"; import routes from "@/routes";
import { IPalmistryLine } from "@/api/resources/Palmistry";
import { IPalmistryFingerLocal } from "@/store/palmistry";
const lineChangeDelay = 1500; const drawElementChangeDelay = 1500;
const startDelay = 500; const startDelay = 500;
// const goNextDelay = 12000; // const goNextDelay = 12000;
@ -19,46 +21,64 @@ export default function StepScanPhoto() {
const storedPhoto = steps.getStoredValue(Step.Upload); const storedPhoto = steps.getStoredValue(Step.Upload);
const lines = useSelector(selectors.selectPalmistryLines); const lines = useSelector(selectors.selectPalmistryLines);
const fingers = useSelector(selectors.selectPalmistryFingers);
const drawElements = useMemo(() => [...fingers, ...lines], [fingers, lines]);
const [currentElementIndex, setCurrentElementIndex] = useState(0); const [currentElementIndex, setCurrentElementIndex] = useState(0);
const [smallPhotoState, setSmallPhotoState] = useState(false); const [smallPhotoState, setSmallPhotoState] = useState(false);
const [title, setTitle] = useState(""); const [title, setTitle] = useState("");
const [shouldDisplayPalmLines, setShouldDisplayPalmLines] = useState(false); const [shouldDisplayPalmLines, setShouldDisplayPalmLines] = useState(false);
const changeTitleTimeOut = useRef<NodeJS.Timeout>();
const prevElementIndex = useRef<number | null>(null); const prevElementIndex = useRef<number | null>(null);
const goNextElement = (delay: number) => { const goNextElement = (delay: number) => {
setTimeout(() => { changeTitleTimeOut.current = setTimeout(() => {
setTitle(lines[currentElementIndex]?.line); const title =
(drawElements[currentElementIndex] as IPalmistryFingerLocal)
.fingerName || drawElements[currentElementIndex].name;
setTitle(title);
setCurrentElementIndex((prevState) => prevState + 1); setCurrentElementIndex((prevState) => prevState + 1);
}, delay); }, delay);
}; };
useEffect(() => { useEffect(() => {
if (!lines.length) { if (!drawElements.length) {
return navigate(routes.client.palmistryUpload()); return navigate(routes.client.palmistryUpload());
} }
}, [lines, navigate]); }, [drawElements, navigate]);
useEffect(() => { useEffect(() => {
// if (currentElementIndex === 0) { // if (currentElementIndex === 0) {
// new Promise((resolve) => setTimeout(resolve, startDelay)); // new Promise((resolve) => setTimeout(resolve, startDelay));
// } // }
if ( if (
currentElementIndex < lines?.length && currentElementIndex < drawElements?.length &&
currentElementIndex !== prevElementIndex.current currentElementIndex !== prevElementIndex.current
) { ) {
prevElementIndex.current = currentElementIndex; prevElementIndex.current = currentElementIndex;
goNextElement(lineChangeDelay); goNextElement(drawElementChangeDelay);
setShouldDisplayPalmLines(lines?.includes(lines[currentElementIndex])); setShouldDisplayPalmLines(
lines?.includes(drawElements[currentElementIndex] as IPalmistryLine)
);
} }
if (currentElementIndex >= lines?.length) { if (currentElementIndex >= drawElements?.length) {
setTimeout(() => { const toSmallTimeOut = setTimeout(() => {
setSmallPhotoState(true); setSmallPhotoState(true);
}, lineChangeDelay); }, drawElementChangeDelay);
setTimeout(steps.goNext, lineChangeDelay * lines.length + 8000); const goNextTimeOut = setTimeout(
steps.goNext,
drawElementChangeDelay * drawElements.length + 8000
);
return () => {
clearTimeout(toSmallTimeOut);
clearTimeout(goNextTimeOut);
};
} }
return () => {
if (changeTitleTimeOut.current) clearTimeout(changeTitleTimeOut.current);
};
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [currentElementIndex]); }, [currentElementIndex]);
@ -74,25 +94,29 @@ export default function StepScanPhoto() {
<h2 <h2
className="palmistry-container__title" className="palmistry-container__title"
style={{ style={{
animationDelay: `${lineChangeDelay * lines.length}ms`, animationDelay: `${drawElementChangeDelay * drawElements.length}ms`,
animationDuration: `${drawElementChangeDelay}ms`,
}} }}
> >
{title} {title}
</h2> </h2>
{/* <pre>{JSON.stringify(lines, null, 2)}</pre> */}
<ScannedPhoto <ScannedPhoto
photo={storedPhoto} photo={storedPhoto}
small={smallPhotoState} small={smallPhotoState}
lineChangeDelay={lineChangeDelay} drawElementChangeDelay={drawElementChangeDelay}
startDelay={startDelay} startDelay={startDelay}
displayLines={shouldDisplayPalmLines} displayLines={shouldDisplayPalmLines}
lines={lines} lines={lines}
fingers={fingers}
drawElements={drawElements}
/> />
<h2 <h2
className="palmistry-container__waiting-title" className="palmistry-container__waiting-title"
style={{ style={{
animationDelay: `${lineChangeDelay * lines.length + 2500}ms`, animationDelay: `${
drawElementChangeDelay * drawElements.length + 2500
}ms`,
}} }}
> >
We are putting together a comprehensive Palmistry Reading just for you! We are putting together a comprehensive Palmistry Reading just for you!
@ -101,7 +125,9 @@ export default function StepScanPhoto() {
<h3 <h3
className="palmistry-container__waiting-description" className="palmistry-container__waiting-description"
style={{ style={{
animationDelay: `${lineChangeDelay * lines.length + 3000}ms`, animationDelay: `${
drawElementChangeDelay * drawElements.length + 3000
}ms`,
}} }}
> >
Wow, looks like there is a lot we can tell about your ambitious and Wow, looks like there is a lot we can tell about your ambitious and

View File

@ -10,6 +10,28 @@ import PalmCameraModal from "../palm-camera-modal/palm-camera-modal";
import { useApi } from "@/api"; import { useApi } from "@/api";
import { useDispatch } from "react-redux"; import { useDispatch } from "react-redux";
import { actions } from "@/store"; import { actions } from "@/store";
import { IPalmistryFinger } from "@/api/resources/Palmistry";
import { IPalmistryFingerLocal } from "@/store/palmistry";
const fingersNames = {
thumb: "Thumb finger",
index_finger: "Index finger",
middle_finger: "Middle finger",
ring_finger: "Ring finger",
pinky: "Little finger",
};
const setFingersNames = (
fingers: IPalmistryFinger[]
): IPalmistryFingerLocal[] => {
if (!fingers) return [];
return fingers.map((finger) => {
return {
...finger,
fingerName: fingersNames[finger.name as keyof typeof fingersNames],
};
});
};
type Props = { type Props = {
onOpenModal: (isOpen: boolean) => void; onOpenModal: (isOpen: boolean) => void;
@ -40,8 +62,14 @@ export default function StepUpload(props: Props) {
const formData = new FormData(); const formData = new FormData();
formData.append("file", file); formData.append("file", file);
const result = await api.getPalmistryLines({ formData }); const result = await api.getPalmistryLines({ formData });
const fingers = setFingersNames(result?.fingers);
dispatch(actions.palmistry.update({ lines: result })); dispatch(
actions.palmistry.update({
lines: result?.lines,
fingers,
})
);
}; };
const onSelectFile = async (event: React.ChangeEvent<HTMLInputElement>) => { const onSelectFile = async (event: React.ChangeEvent<HTMLInputElement>) => {
@ -49,10 +77,10 @@ export default function StepUpload(props: Props) {
if (!event.target.files || event.target.files.length === 0) return; if (!event.target.files || event.target.files.length === 0) return;
await getLines(event.target.files[0]);
setIsUpladProcessing(true); setIsUpladProcessing(true);
await getLines(event.target.files[0]);
const reader = new FileReader(); const reader = new FileReader();
reader.onloadend = () => { reader.onloadend = () => {

View File

@ -118,6 +118,7 @@ export const useAuthentication = () => {
const authorization = useCallback(async (email: string, source: ESourceAuthorization) => { const authorization = useCallback(async (email: string, source: ESourceAuthorization) => {
try { try {
setIsLoading(true); setIsLoading(true);
setError(null)
const payload = getAuthorizationPayload(email, source); const payload = getAuthorizationPayload(email, source);
const { token, userId } = await api.authorization(payload); const { token, userId } = await api.authorization(payload);
const { user } = await api.getUser({ token }); const { user } = await api.getUser({ token });

View File

@ -264,7 +264,7 @@ const routes = {
[apiHost, prefix, "ai", "assistants", chatId, "chats.json"].join("/"), [apiHost, prefix, "ai", "assistants", chatId, "chats.json"].join("/"),
// Palmistry // Palmistry
getPalmistryLines: () => getPalmistryLines: () =>
["https://api.aura.witapps.us", "palmistry", "lines"].join("/"), [dApiHost, dApiPrefix, "palmistry", "lines"].join("/"),
// Paywall // Paywall
getPaywallByPlacementKey: (placementKey: EPlacementKeys) => getPaywallByPlacementKey: (placementKey: EPlacementKeys) =>

View File

@ -67,6 +67,7 @@ import {
} from "./userCallbacks"; } from "./userCallbacks";
import palmistry, { import palmistry, {
actions as palmistryActions, actions as palmistryActions,
selectPalmistryFingers,
selectPalmistryLines, selectPalmistryLines,
} from "./palmistry"; } from "./palmistry";
import { selectPaywallsIsMustUpdate, selectPaywalls } from "./paywalls"; import { selectPaywallsIsMustUpdate, selectPaywalls } from "./paywalls";
@ -121,6 +122,7 @@ export const selectors = {
selectIsForceShortPath, selectIsForceShortPath,
selectOpenAiToken, selectOpenAiToken,
selectPalmistryLines, selectPalmistryLines,
selectPalmistryFingers,
selectPaywalls, selectPaywalls,
selectPaywallsIsMustUpdate, selectPaywallsIsMustUpdate,
selectPrivacyPolicy, selectPrivacyPolicy,

View File

@ -1,13 +1,17 @@
import { IPalmistryLine } from "@/api/resources/Palmistry"; import { IPalmistryFinger, IPalmistryLine } from "@/api/resources/Palmistry";
import { createSlice, createSelector } from "@reduxjs/toolkit"; import { createSlice, createSelector } from "@reduxjs/toolkit";
import type { PayloadAction } from "@reduxjs/toolkit"; import type { PayloadAction } from "@reduxjs/toolkit";
export type IPalmistryFingerLocal = IPalmistryFinger & { fingerName?: string }
interface IPalmistry { interface IPalmistry {
lines: IPalmistryLine[]; lines: IPalmistryLine[];
fingers: IPalmistryFingerLocal[];
} }
const initialState: IPalmistry = { const initialState: IPalmistry = {
lines: [], lines: [],
fingers: []
}; };
const palmistrySlice = createSlice({ const palmistrySlice = createSlice({
@ -26,4 +30,8 @@ export const selectPalmistryLines = createSelector(
(state: { palmistry: IPalmistry }) => state.palmistry.lines, (state: { palmistry: IPalmistry }) => state.palmistry.lines,
(palmistry) => palmistry (palmistry) => palmistry
); );
export const selectPalmistryFingers = createSelector(
(state: { palmistry: IPalmistry }) => state.palmistry.fingers,
(palmistry) => palmistry
);
export default palmistrySlice.reducer; export default palmistrySlice.reducer;