Merge pull request #71 from pennyteenycat/additional-purchases-v2
additional-purchases-v2
This commit is contained in:
commit
b98096bb7c
@ -295,6 +295,56 @@
|
|||||||
"emoji": "rised_hand.webp"
|
"emoji": "rised_hand.webp"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"video-guides": {
|
||||||
|
"banner": {
|
||||||
|
"title": "Amazing!",
|
||||||
|
"description": "Your journey begins now"
|
||||||
|
},
|
||||||
|
"title": "Choose your sign-up offer 🔥",
|
||||||
|
"subtitle": "Available only now",
|
||||||
|
"description": "* You will be charged for the add-on services or offers selected at the time of purchase. This is a non-recuring payment.",
|
||||||
|
"button": "Continue",
|
||||||
|
"skip_button": "Skip this offer and proceed further",
|
||||||
|
"copyright": "© 2025, Wit Lab LLC, California, US",
|
||||||
|
"products": {
|
||||||
|
"main_ultra_pack": {
|
||||||
|
"title": "Ultra Pack",
|
||||||
|
"subtitle": "3 in 1+2 secret bonus readings",
|
||||||
|
"discount": "{discount}% OFF",
|
||||||
|
"price": "Now <price></price> <oldPrice></oldPrice>",
|
||||||
|
"emoji": "star_struck.webp"
|
||||||
|
},
|
||||||
|
"main_numerology_analysis": {
|
||||||
|
"title": "Relationship plan",
|
||||||
|
"subtitle": "Discover the future without losing yourself",
|
||||||
|
"discount": "{discount}% OFF",
|
||||||
|
"price": "Now <price></price> <oldPrice></oldPrice>",
|
||||||
|
"emoji": "ring.webp"
|
||||||
|
},
|
||||||
|
"main_tarot_reading": {
|
||||||
|
"title": "Healthy compatibility",
|
||||||
|
"subtitle": "Balance between closeness and freedom",
|
||||||
|
"discount": "{discount}% OFF",
|
||||||
|
"price": "Now <price></price> <oldPrice></oldPrice>",
|
||||||
|
"emoji": "rose.webp"
|
||||||
|
},
|
||||||
|
"main_palmistry_guide": {
|
||||||
|
"title": "How to talk about feelings",
|
||||||
|
"subtitle": "Express your emotions and be understood",
|
||||||
|
"discount": "{discount}% OFF",
|
||||||
|
"price": "Now <price></price> <oldPrice></oldPrice>",
|
||||||
|
"emoji": "heart_from_hands.webp"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Progress": {
|
||||||
|
"items": {
|
||||||
|
"1": "Your Soulmate Portrait",
|
||||||
|
"2": "Video Guides",
|
||||||
|
"3": "Add Consultation",
|
||||||
|
"4": "Access Your Results"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Chat": {
|
"Chat": {
|
||||||
@ -641,4 +691,4 @@
|
|||||||
"month": "{count, plural, zero {#-months} one {#-month} two {#-months} few {#-months} many {#-months} other {#-months}}",
|
"month": "{count, plural, zero {#-months} one {#-month} two {#-months} few {#-months} many {#-months} other {#-months}}",
|
||||||
"year": "{count, plural, zero {#-years} one {#-year} two {#-years} few {#-years} many {#-years} other {#-years}}"
|
"year": "{count, plural, zero {#-years} one {#-year} two {#-years} few {#-years} many {#-years} other {#-years}}"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
BIN
public/emoji/heart_from_hands.webp
Normal file
BIN
public/emoji/heart_from_hands.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.2 KiB |
BIN
public/emoji/ring.webp
Normal file
BIN
public/emoji/ring.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.8 KiB |
BIN
public/emoji/rose.webp
Normal file
BIN
public/emoji/rose.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.8 KiB |
@ -3,6 +3,7 @@ import { redirect } from "next/navigation";
|
|||||||
import {
|
import {
|
||||||
AddConsultantPage,
|
AddConsultantPage,
|
||||||
AddGuidesPage,
|
AddGuidesPage,
|
||||||
|
VideoGuidesPage,
|
||||||
} from "@/components/domains/additional-purchases";
|
} from "@/components/domains/additional-purchases";
|
||||||
import { ROUTES } from "@/shared/constants/client-routes";
|
import { ROUTES } from "@/shared/constants/client-routes";
|
||||||
|
|
||||||
@ -20,6 +21,8 @@ export default async function AdditionalProductPage({
|
|||||||
return <AddConsultantPage />;
|
return <AddConsultantPage />;
|
||||||
case "add_guides":
|
case "add_guides":
|
||||||
return <AddGuidesPage />;
|
return <AddGuidesPage />;
|
||||||
|
case "video_guides":
|
||||||
|
return <VideoGuidesPage />;
|
||||||
default:
|
default:
|
||||||
return redirect(ROUTES.home());
|
return redirect(ROUTES.home());
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,98 @@
|
|||||||
|
.container {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
|
||||||
|
& > * {
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
& > .item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
|
||||||
|
& > .marker {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: f8fafc;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
background-color: #f8fafc;
|
||||||
|
|
||||||
|
& > .number {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #9ca3af;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
& > .text {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
line-height: 16px;
|
||||||
|
color: #9ca3af;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
& > .marker {
|
||||||
|
position: relative;
|
||||||
|
box-shadow: 0px 2px 15px 0px #3b82f6f7;
|
||||||
|
background-color: #3b82f6;
|
||||||
|
border: 2px solid #ffffff;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
& > .number {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
& > .text {
|
||||||
|
color: #2866ed;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.done {
|
||||||
|
& > .marker {
|
||||||
|
box-shadow: 0px 0px 0px 0px #3b82f626;
|
||||||
|
background-color: #2866ed;
|
||||||
|
border: none;
|
||||||
|
|
||||||
|
& > .number {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
& > .text {
|
||||||
|
color: #282828;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.connector {
|
||||||
|
position: absolute;
|
||||||
|
top: 15px;
|
||||||
|
height: 2px;
|
||||||
|
z-index: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,81 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
import clsx from "clsx";
|
||||||
|
|
||||||
|
import { Icon, IconName, Typography } from "@/components/ui";
|
||||||
|
import { useDynamicSize } from "@/hooks/DOM/useDynamicSize";
|
||||||
|
|
||||||
|
import styles from "./Progress.module.scss";
|
||||||
|
|
||||||
|
interface IProgressProps {
|
||||||
|
activeItemIndex: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Progress({ activeItemIndex }: IProgressProps) {
|
||||||
|
const t = useTranslations("AdditionalPurchases.Progress");
|
||||||
|
const { width: containerWidth, elementRef } = useDynamicSize<HTMLDivElement>({
|
||||||
|
defaultWidth: 327,
|
||||||
|
});
|
||||||
|
|
||||||
|
const items = Object.values(t.raw("items") as Record<string, string>);
|
||||||
|
|
||||||
|
const firstChild = elementRef.current?.childNodes[0] as HTMLElement;
|
||||||
|
const lastChild = elementRef.current?.childNodes[
|
||||||
|
items.length - 1
|
||||||
|
] as HTMLElement;
|
||||||
|
const leftIndent =
|
||||||
|
((firstChild?.getBoundingClientRect().width || 100) - 32) / 2;
|
||||||
|
const rightIndent =
|
||||||
|
((lastChild?.getBoundingClientRect().width || 76) - 32) / 2;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.container} ref={elementRef}>
|
||||||
|
{items.map((item, index) => (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
className={clsx(
|
||||||
|
styles.item,
|
||||||
|
activeItemIndex === index && styles.active,
|
||||||
|
activeItemIndex > index && styles.done
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className={styles.marker}>
|
||||||
|
{activeItemIndex > index && styles.done && (
|
||||||
|
<Icon
|
||||||
|
name={IconName.Check}
|
||||||
|
color="#fff"
|
||||||
|
size={{
|
||||||
|
width: 12,
|
||||||
|
height: 12,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<Typography as="span" className={styles.number}>
|
||||||
|
{index + 1}
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
<Typography as="p" align="center" className={styles.text}>
|
||||||
|
{item}
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
<div
|
||||||
|
className={styles.connector}
|
||||||
|
style={{
|
||||||
|
width: containerWidth - leftIndent - rightIndent,
|
||||||
|
left: leftIndent,
|
||||||
|
background: activeItemIndex
|
||||||
|
? `
|
||||||
|
linear-gradient(
|
||||||
|
90deg, #2866ed 0%,
|
||||||
|
#2866ed ${((activeItemIndex - 1) / items.length) * 100}%,
|
||||||
|
#c4d9fc ${(activeItemIndex / items.length) * containerWidth + 16}px,
|
||||||
|
#E2E8F0 100%)
|
||||||
|
`
|
||||||
|
: "#E2E8F0",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -0,0 +1,3 @@
|
|||||||
|
<svg width="20" height="18" viewBox="0 0 20 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M1.85938 10.0879L8.91797 16.6777C9.21094 16.9511 9.59766 17.1035 10 17.1035C10.4023 17.1035 10.7891 16.9511 11.082 16.6777L18.1406 10.0879C19.3281 8.98239 20 7.43161 20 5.81051V5.58395C20 2.85348 18.0273 0.525356 15.3359 0.0761369C13.5547 -0.220738 11.7422 0.361293 10.4688 1.63473L10 2.10348L9.53125 1.63473C8.25781 0.361293 6.44531 -0.220738 4.66406 0.0761369C1.97266 0.525356 0 2.85348 0 5.58395V5.81051C0 7.43161 0.671875 8.98239 1.85938 10.0879Z" fill="#4ECDC4"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 581 B |
@ -0,0 +1,45 @@
|
|||||||
|
.container {
|
||||||
|
width: 100%;
|
||||||
|
padding: 18px 20px 25px 25px;
|
||||||
|
background: linear-gradient(
|
||||||
|
90deg,
|
||||||
|
rgba(78, 205, 196, 0.1) 0%,
|
||||||
|
rgba(102, 126, 234, 0.1) 100%
|
||||||
|
);
|
||||||
|
border: 1px solid #4ecdc433;
|
||||||
|
border-radius: 32px;
|
||||||
|
margin-top: 34px;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 48px 1fr;
|
||||||
|
gap: 16px;
|
||||||
|
|
||||||
|
& > .iconContainer {
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: #4ecdc433;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
& > .textContainer {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
|
||||||
|
& > .title {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 28px;
|
||||||
|
color: #262626;
|
||||||
|
}
|
||||||
|
|
||||||
|
& > .description {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 500;
|
||||||
|
line-height: 24px;
|
||||||
|
color: #525252;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,27 @@
|
|||||||
|
import { getTranslations } from "next-intl/server";
|
||||||
|
|
||||||
|
import { Typography } from "@/components/ui";
|
||||||
|
|
||||||
|
import HeartIcon from "./HeartIcon.svg";
|
||||||
|
|
||||||
|
import styles from "./VideoGuidesBanner.module.scss";
|
||||||
|
|
||||||
|
export default async function VideoGuidesBanner() {
|
||||||
|
const t = await getTranslations("AdditionalPurchases.video-guides.banner");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.container}>
|
||||||
|
<div className={styles.iconContainer}>
|
||||||
|
<HeartIcon />
|
||||||
|
</div>
|
||||||
|
<div className={styles.textContainer}>
|
||||||
|
<Typography as="h4" align="left" className={styles.title}>
|
||||||
|
{t("title")}
|
||||||
|
</Typography>
|
||||||
|
<Typography as="p" align="left" className={styles.description}>
|
||||||
|
{t("description")}
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -0,0 +1,45 @@
|
|||||||
|
.container {
|
||||||
|
position: fixed;
|
||||||
|
bottom: calc(0dvh + 16px);
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
width: 100%;
|
||||||
|
padding-inline: 24px;
|
||||||
|
max-width: 560px;
|
||||||
|
height: fit-content;
|
||||||
|
|
||||||
|
& > .button {
|
||||||
|
padding-block: 20px;
|
||||||
|
background: linear-gradient(90deg, #3b82f6 0%, #2563eb 100%);
|
||||||
|
border-radius: 16px;
|
||||||
|
box-shadow:
|
||||||
|
0px 5px 14px 0px #3b82f666,
|
||||||
|
0px 4px 6px 0px #3b82f61a;
|
||||||
|
|
||||||
|
& > .text {
|
||||||
|
font-size: 19px;
|
||||||
|
font-weight: 500;
|
||||||
|
line-height: 125%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
& > .skipButton {
|
||||||
|
padding: 0;
|
||||||
|
min-height: none;
|
||||||
|
margin-top: 13px;
|
||||||
|
|
||||||
|
& > .text {
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 24px;
|
||||||
|
color: #1f2937;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
& > .copyright {
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 16px;
|
||||||
|
color: #9ca3af;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,88 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
|
||||||
|
import { Button, Spinner, Typography } from "@/components/ui";
|
||||||
|
import { BlurComponent } from "@/components/widgets";
|
||||||
|
import { useSingleCheckout } from "@/hooks/payment/useSingleCheckout";
|
||||||
|
import { useToast } from "@/providers/toast-provider";
|
||||||
|
import { ROUTES } from "@/shared/constants/client-routes";
|
||||||
|
|
||||||
|
import { useMultiPageNavigationContext } from "..";
|
||||||
|
import { useProductSelection } from "../ProductSelectionProvider";
|
||||||
|
|
||||||
|
import styles from "./VideoGuidesButton.module.scss";
|
||||||
|
|
||||||
|
export default function VideoGuidesButton() {
|
||||||
|
const t = useTranslations("AdditionalPurchases.video-guides");
|
||||||
|
const { addToast } = useToast();
|
||||||
|
const { selectedProduct } = useProductSelection();
|
||||||
|
const { navigation } = useMultiPageNavigationContext();
|
||||||
|
|
||||||
|
const { handleSingleCheckout, isLoading } = useSingleCheckout({
|
||||||
|
onSuccess: () => {
|
||||||
|
navigation.goToNext();
|
||||||
|
},
|
||||||
|
onError: _error => {
|
||||||
|
addToast({
|
||||||
|
variant: "error",
|
||||||
|
message: t("payment_error"),
|
||||||
|
duration: 5000,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
returnUrl: new URL(
|
||||||
|
navigation.getNextPageUrl() || ROUTES.home(),
|
||||||
|
process.env.NEXT_PUBLIC_APP_URL || ""
|
||||||
|
).toString(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const handlePurchase = () => {
|
||||||
|
if (!selectedProduct) {
|
||||||
|
addToast({
|
||||||
|
variant: "error",
|
||||||
|
message: t("select_product_error"),
|
||||||
|
duration: 5000,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
handleSingleCheckout({
|
||||||
|
productId: selectedProduct.id,
|
||||||
|
key: selectedProduct.key,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSkipOffer = () => {
|
||||||
|
navigation.goToNext();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BlurComponent isActiveBlur={true} className={styles.container}>
|
||||||
|
<Button
|
||||||
|
className={styles.button}
|
||||||
|
onClick={handlePurchase}
|
||||||
|
disabled={isLoading}
|
||||||
|
>
|
||||||
|
{isLoading ? (
|
||||||
|
<Spinner />
|
||||||
|
) : (
|
||||||
|
<Typography color="white" className={styles.text}>
|
||||||
|
{t("button")}
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
className={styles.skipButton}
|
||||||
|
variant="ghost"
|
||||||
|
onClick={handleSkipOffer}
|
||||||
|
>
|
||||||
|
<Typography as="p" className={styles.text}>
|
||||||
|
{t("skip_button")}
|
||||||
|
</Typography>
|
||||||
|
</Button>
|
||||||
|
<Typography as="p" className={styles.copyright}>
|
||||||
|
{t("copyright")}
|
||||||
|
</Typography>
|
||||||
|
</BlurComponent>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -0,0 +1,131 @@
|
|||||||
|
.container.container {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
padding: 11px 18px 12px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
box-shadow: 0px 0px 30px 0px #0000001f;
|
||||||
|
gap: 4px;
|
||||||
|
|
||||||
|
& > .content {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 40px 1fr 20px;
|
||||||
|
gap: 12px;
|
||||||
|
|
||||||
|
& > .emojiContainer {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border: 1px solid #cedfff;
|
||||||
|
background: linear-gradient(0deg, #e2ebff, #e2ebff);
|
||||||
|
border-radius: 16px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
& > .emoji {
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
background-position: center;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-size: contain;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
& > .textContainer {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
|
||||||
|
& > .title {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 24px;
|
||||||
|
color: #262626;
|
||||||
|
}
|
||||||
|
|
||||||
|
& > .subtitle {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
line-height: 16px;
|
||||||
|
color: #737373;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
& > .checmarkContainer {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
border: 2px solid #d4d4d4;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-top: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
& > .footer {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: space-between;
|
||||||
|
|
||||||
|
& > .price {
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 20px;
|
||||||
|
color: #737373;
|
||||||
|
|
||||||
|
& > .currentPrice {
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 20px;
|
||||||
|
color: #737373;
|
||||||
|
}
|
||||||
|
|
||||||
|
& > .oldPrice {
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 20px;
|
||||||
|
color: #9ca3af;
|
||||||
|
text-decoration: line-through;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
& > .discount {
|
||||||
|
display: block;
|
||||||
|
padding: 6px 8px;
|
||||||
|
background: #ff6b6b1a;
|
||||||
|
border-radius: 9999;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #ff6b6b;
|
||||||
|
margin-right: 19px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
outline: 2px solid #9fbdff;
|
||||||
|
|
||||||
|
& > .content {
|
||||||
|
& > .checmarkContainer {
|
||||||
|
background-color: #2866ed;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.main_ultra_pack {
|
||||||
|
& > .footer {
|
||||||
|
& > .discount {
|
||||||
|
position: absolute;
|
||||||
|
top: -15px;
|
||||||
|
right: 49px;
|
||||||
|
background: #ff3737;
|
||||||
|
box-shadow: 0px 1px 11.98px 0px #ff44448c;
|
||||||
|
border: 2px solid #ffffff4d;
|
||||||
|
padding: 8px 12px;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 800;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,100 @@
|
|||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
import clsx from "clsx";
|
||||||
|
|
||||||
|
import { Card, Icon, IconName, Typography } from "@/components/ui";
|
||||||
|
import { IFunnelPaymentVariant } from "@/entities/session/funnel/types";
|
||||||
|
import { getFormattedPrice } from "@/shared/utils/price";
|
||||||
|
import { Currency } from "@/types";
|
||||||
|
|
||||||
|
import styles from "./VideoGuidesOffer.module.scss";
|
||||||
|
|
||||||
|
interface VideoGuidesOfferProps {
|
||||||
|
offer: IFunnelPaymentVariant;
|
||||||
|
isActive: boolean;
|
||||||
|
className?: string;
|
||||||
|
onClick: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function VideoGuidesOffer(props: VideoGuidesOfferProps) {
|
||||||
|
const { offer, isActive, className, onClick } = props;
|
||||||
|
|
||||||
|
const { key, price, oldPrice } = offer;
|
||||||
|
|
||||||
|
const productKey = key.replaceAll(".", "_");
|
||||||
|
|
||||||
|
const t = useTranslations(
|
||||||
|
`AdditionalPurchases.video-guides.products.${productKey}`
|
||||||
|
);
|
||||||
|
|
||||||
|
const currency = Currency.USD;
|
||||||
|
|
||||||
|
const subtitle = t.has("subtitle") ? t("subtitle") : undefined;
|
||||||
|
|
||||||
|
const discount = Math.ceil(
|
||||||
|
(((oldPrice || 0) - price) / (oldPrice || 0)) * 100
|
||||||
|
);
|
||||||
|
|
||||||
|
const emoji = t.has("emoji") ? t("emoji") : undefined;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
className={clsx(
|
||||||
|
styles.container,
|
||||||
|
isActive && styles.active,
|
||||||
|
styles[productKey],
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
onClick={onClick}
|
||||||
|
>
|
||||||
|
<div className={styles.content}>
|
||||||
|
<div className={styles.emojiContainer}>
|
||||||
|
<span
|
||||||
|
className={styles.emoji}
|
||||||
|
style={{ backgroundImage: `url(/emoji/${emoji})` }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className={styles.textContainer}>
|
||||||
|
<Typography as="h5" align="left" className={styles.title}>
|
||||||
|
{t("title")}
|
||||||
|
</Typography>
|
||||||
|
{subtitle && (
|
||||||
|
<Typography as="p" align="left" className={styles.subtitle}>
|
||||||
|
{subtitle}
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className={styles.checmarkContainer}>
|
||||||
|
<Icon
|
||||||
|
name={IconName.Check}
|
||||||
|
color="white"
|
||||||
|
size={{
|
||||||
|
width: 11,
|
||||||
|
height: 11,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={styles.footer}>
|
||||||
|
<Typography className={styles.price}>
|
||||||
|
{t.rich("price", {
|
||||||
|
price: () => (
|
||||||
|
<Typography className={styles.currentPrice}>
|
||||||
|
{getFormattedPrice(price, currency)}
|
||||||
|
</Typography>
|
||||||
|
),
|
||||||
|
oldPrice: () => (
|
||||||
|
<Typography className={styles.oldPrice}>
|
||||||
|
{getFormattedPrice(oldPrice || 0, currency)}
|
||||||
|
</Typography>
|
||||||
|
),
|
||||||
|
})}
|
||||||
|
</Typography>
|
||||||
|
<Typography className={styles.discount}>
|
||||||
|
{t("discount", {
|
||||||
|
discount: discount || 0,
|
||||||
|
})}
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -0,0 +1,7 @@
|
|||||||
|
.container {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
margin-top: 37px;
|
||||||
|
}
|
||||||
@ -0,0 +1,81 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useEffect, useMemo, useState } from "react";
|
||||||
|
|
||||||
|
import { Skeleton } from "@/components/ui";
|
||||||
|
import { IFunnelPaymentVariant } from "@/entities/session/funnel/types";
|
||||||
|
|
||||||
|
import { useMultiPageNavigationContext, VideoGuidesOffer } from "..";
|
||||||
|
import { useProductSelection } from "../ProductSelectionProvider";
|
||||||
|
|
||||||
|
import styles from "./VideoGuidesOffers.module.scss";
|
||||||
|
|
||||||
|
export default function VideoGuidesOffers() {
|
||||||
|
const { navigation } = useMultiPageNavigationContext();
|
||||||
|
const data = navigation.currentItem;
|
||||||
|
|
||||||
|
const offers = useMemo(() => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
id: "1",
|
||||||
|
key: "main_ultra_pack",
|
||||||
|
type: "sdv",
|
||||||
|
price: 1939,
|
||||||
|
oldPrice: 3499,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "2",
|
||||||
|
key: "main_numerology_analysis",
|
||||||
|
type: "sdv",
|
||||||
|
price: 938,
|
||||||
|
oldPrice: 1999,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "3",
|
||||||
|
key: "main_tarot_reading",
|
||||||
|
type: "sdv",
|
||||||
|
price: 937,
|
||||||
|
oldPrice: 1999,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "4",
|
||||||
|
key: "main_palmistry_guide",
|
||||||
|
type: "sdv",
|
||||||
|
price: 936,
|
||||||
|
oldPrice: 1999,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
return data?.variants ?? [];
|
||||||
|
}, [data]);
|
||||||
|
const [activeOffer, setActiveOffer] = useState<string>("");
|
||||||
|
const { setSelectedProduct } = useProductSelection();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (offers[0]) {
|
||||||
|
setActiveOffer(offers[0]?.id);
|
||||||
|
setSelectedProduct(offers[0]);
|
||||||
|
}
|
||||||
|
}, [offers, setSelectedProduct]);
|
||||||
|
|
||||||
|
const handleOfferClick = (offer: IFunnelPaymentVariant) => {
|
||||||
|
setActiveOffer(offer.id);
|
||||||
|
setSelectedProduct(offer);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.container}>
|
||||||
|
{offers.map(offer => (
|
||||||
|
<VideoGuidesOffer
|
||||||
|
offer={offer}
|
||||||
|
key={offer.id}
|
||||||
|
isActive={activeOffer === offer.id}
|
||||||
|
onClick={() => handleOfferClick(offer)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function VideoGuidesOffersSkeleton() {
|
||||||
|
return <Skeleton style={{ height: "400px" }} />;
|
||||||
|
}
|
||||||
@ -0,0 +1,23 @@
|
|||||||
|
.title {
|
||||||
|
font-size: 25px;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 28px;
|
||||||
|
margin-top: 32px;
|
||||||
|
color: #000000;
|
||||||
|
max-width: 281px;
|
||||||
|
margin-inline: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtitle {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 500;
|
||||||
|
line-height: 24px;
|
||||||
|
color: #737373;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description {
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 16px;
|
||||||
|
color: #6b7280;
|
||||||
|
margin-top: 12px;
|
||||||
|
}
|
||||||
@ -0,0 +1,38 @@
|
|||||||
|
import { Suspense } from "react";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
|
||||||
|
import {
|
||||||
|
ProductSelectionProvider,
|
||||||
|
Progress,
|
||||||
|
VideoGuidesBanner,
|
||||||
|
VideoGuidesButton,
|
||||||
|
VideoGuidesOffers,
|
||||||
|
VideoGuidesOffersSkeleton,
|
||||||
|
} from "@/components/domains/additional-purchases";
|
||||||
|
import { Typography } from "@/components/ui";
|
||||||
|
|
||||||
|
import styles from "./VideoGuidesPage.module.scss";
|
||||||
|
|
||||||
|
export default function VideoGuidesPage() {
|
||||||
|
const t = useTranslations("AdditionalPurchases.video-guides");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ProductSelectionProvider>
|
||||||
|
<Progress activeItemIndex={1} />
|
||||||
|
<VideoGuidesBanner />
|
||||||
|
<Typography as="h1" className={styles.title}>
|
||||||
|
{t("title")}
|
||||||
|
</Typography>
|
||||||
|
<Typography as="h2" className={styles.subtitle}>
|
||||||
|
{t("subtitle")}
|
||||||
|
</Typography>
|
||||||
|
<Suspense fallback={<VideoGuidesOffersSkeleton />}>
|
||||||
|
<VideoGuidesOffers />
|
||||||
|
</Suspense>
|
||||||
|
<Typography as="p" align="left" className={styles.description}>
|
||||||
|
{t("description")}
|
||||||
|
</Typography>
|
||||||
|
<VideoGuidesButton />
|
||||||
|
</ProductSelectionProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -14,3 +14,12 @@ export {
|
|||||||
ProductSelectionProvider,
|
ProductSelectionProvider,
|
||||||
useProductSelection,
|
useProductSelection,
|
||||||
} from "./ProductSelectionProvider";
|
} from "./ProductSelectionProvider";
|
||||||
|
export { default as Progress } from "./Progress/Progress";
|
||||||
|
export { default as VideoGuidesBanner } from "./VideoGuidesBanner/VideoGuidesBanner";
|
||||||
|
export { default as VideoGuidesButton } from "./VideoGuidesButton/VideoGuidesButton";
|
||||||
|
export { default as VideoGuidesOffer } from "./VideoGuidesOffer/VideoGuidesOffer";
|
||||||
|
export {
|
||||||
|
default as VideoGuidesOffers,
|
||||||
|
VideoGuidesOffersSkeleton,
|
||||||
|
} from "./VideoGuidesOffers/VideoGuidesOffers";
|
||||||
|
export { default as VideoGuidesPage } from "./VideoGuidesPage/VideoGuidesPage";
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user