diff --git a/messages/en.json b/messages/en.json index d9f7ffb..bcdcc8b 100644 --- a/messages/en.json +++ b/messages/en.json @@ -295,6 +295,56 @@ "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 ", + "emoji": "star_struck.webp" + }, + "main_numerology_analysis": { + "title": "Relationship plan", + "subtitle": "Discover the future without losing yourself", + "discount": "{discount}% OFF", + "price": "Now ", + "emoji": "ring.webp" + }, + "main_tarot_reading": { + "title": "Healthy compatibility", + "subtitle": "Balance between closeness and freedom", + "discount": "{discount}% OFF", + "price": "Now ", + "emoji": "rose.webp" + }, + "main_palmistry_guide": { + "title": "How to talk about feelings", + "subtitle": "Express your emotions and be understood", + "discount": "{discount}% OFF", + "price": "Now ", + "emoji": "heart_from_hands.webp" + } + } + }, + "Progress": { + "items": { + "1": "Your Soulmate Portrait", + "2": "Video Guides", + "3": "Add Consultation", + "4": "Access Your Results" + } } }, "Chat": { @@ -641,4 +691,4 @@ "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}}" } -} \ No newline at end of file +} diff --git a/public/emoji/heart_from_hands.webp b/public/emoji/heart_from_hands.webp new file mode 100644 index 0000000..be923a0 Binary files /dev/null and b/public/emoji/heart_from_hands.webp differ diff --git a/public/emoji/ring.webp b/public/emoji/ring.webp new file mode 100644 index 0000000..98f9fb8 Binary files /dev/null and b/public/emoji/ring.webp differ diff --git a/public/emoji/rose.webp b/public/emoji/rose.webp new file mode 100644 index 0000000..01d5b96 Binary files /dev/null and b/public/emoji/rose.webp differ diff --git a/src/app/[locale]/(additional-purchases)/ap/[pageType]/page.tsx b/src/app/[locale]/(additional-purchases)/ap/[pageType]/page.tsx index 9e7d979..a74b02a 100644 --- a/src/app/[locale]/(additional-purchases)/ap/[pageType]/page.tsx +++ b/src/app/[locale]/(additional-purchases)/ap/[pageType]/page.tsx @@ -3,6 +3,7 @@ import { redirect } from "next/navigation"; import { AddConsultantPage, AddGuidesPage, + VideoGuidesPage, } from "@/components/domains/additional-purchases"; import { ROUTES } from "@/shared/constants/client-routes"; @@ -20,6 +21,8 @@ export default async function AdditionalProductPage({ return ; case "add_guides": return ; + case "video_guides": + return ; default: return redirect(ROUTES.home()); } diff --git a/src/components/domains/additional-purchases/Progress/Progress.module.scss b/src/components/domains/additional-purchases/Progress/Progress.module.scss new file mode 100644 index 0000000..00f1217 --- /dev/null +++ b/src/components/domains/additional-purchases/Progress/Progress.module.scss @@ -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; + } +} diff --git a/src/components/domains/additional-purchases/Progress/Progress.tsx b/src/components/domains/additional-purchases/Progress/Progress.tsx new file mode 100644 index 0000000..3679b54 --- /dev/null +++ b/src/components/domains/additional-purchases/Progress/Progress.tsx @@ -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({ + defaultWidth: 327, + }); + + const items = Object.values(t.raw("items") as Record); + + 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 ( +
+ {items.map((item, index) => ( +
index && styles.done + )} + > +
+ {activeItemIndex > index && styles.done && ( + + )} + + {index + 1} + +
+ + {item} + +
+ ))} +
+
+ ); +} diff --git a/src/components/domains/additional-purchases/VideoGuidesBanner/HeartIcon.svg b/src/components/domains/additional-purchases/VideoGuidesBanner/HeartIcon.svg new file mode 100644 index 0000000..78e1716 --- /dev/null +++ b/src/components/domains/additional-purchases/VideoGuidesBanner/HeartIcon.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/components/domains/additional-purchases/VideoGuidesBanner/VideoGuidesBanner.module.scss b/src/components/domains/additional-purchases/VideoGuidesBanner/VideoGuidesBanner.module.scss new file mode 100644 index 0000000..5ecfd76 --- /dev/null +++ b/src/components/domains/additional-purchases/VideoGuidesBanner/VideoGuidesBanner.module.scss @@ -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; + } + } +} diff --git a/src/components/domains/additional-purchases/VideoGuidesBanner/VideoGuidesBanner.tsx b/src/components/domains/additional-purchases/VideoGuidesBanner/VideoGuidesBanner.tsx new file mode 100644 index 0000000..06e8d3a --- /dev/null +++ b/src/components/domains/additional-purchases/VideoGuidesBanner/VideoGuidesBanner.tsx @@ -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 ( +
+
+ +
+
+ + {t("title")} + + + {t("description")} + +
+
+ ); +} diff --git a/src/components/domains/additional-purchases/VideoGuidesButton/VideoGuidesButton.module.scss b/src/components/domains/additional-purchases/VideoGuidesButton/VideoGuidesButton.module.scss new file mode 100644 index 0000000..e2127ad --- /dev/null +++ b/src/components/domains/additional-purchases/VideoGuidesButton/VideoGuidesButton.module.scss @@ -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; + } +} diff --git a/src/components/domains/additional-purchases/VideoGuidesButton/VideoGuidesButton.tsx b/src/components/domains/additional-purchases/VideoGuidesButton/VideoGuidesButton.tsx new file mode 100644 index 0000000..9227b80 --- /dev/null +++ b/src/components/domains/additional-purchases/VideoGuidesButton/VideoGuidesButton.tsx @@ -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 ( + + + + + {t("copyright")} + + + ); +} diff --git a/src/components/domains/additional-purchases/VideoGuidesOffer/VideoGuidesOffer.module.scss b/src/components/domains/additional-purchases/VideoGuidesOffer/VideoGuidesOffer.module.scss new file mode 100644 index 0000000..67878d4 --- /dev/null +++ b/src/components/domains/additional-purchases/VideoGuidesOffer/VideoGuidesOffer.module.scss @@ -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; + } + } + } +} diff --git a/src/components/domains/additional-purchases/VideoGuidesOffer/VideoGuidesOffer.tsx b/src/components/domains/additional-purchases/VideoGuidesOffer/VideoGuidesOffer.tsx new file mode 100644 index 0000000..7974e46 --- /dev/null +++ b/src/components/domains/additional-purchases/VideoGuidesOffer/VideoGuidesOffer.tsx @@ -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 ( + +
+
+ +
+
+ + {t("title")} + + {subtitle && ( + + {subtitle} + + )} +
+
+ +
+
+
+ + {t.rich("price", { + price: () => ( + + {getFormattedPrice(price, currency)} + + ), + oldPrice: () => ( + + {getFormattedPrice(oldPrice || 0, currency)} + + ), + })} + + + {t("discount", { + discount: discount || 0, + })} + +
+
+ ); +} diff --git a/src/components/domains/additional-purchases/VideoGuidesOffers/VideoGuidesOffers.module.scss b/src/components/domains/additional-purchases/VideoGuidesOffers/VideoGuidesOffers.module.scss new file mode 100644 index 0000000..f1812c0 --- /dev/null +++ b/src/components/domains/additional-purchases/VideoGuidesOffers/VideoGuidesOffers.module.scss @@ -0,0 +1,7 @@ +.container { + width: 100%; + display: flex; + flex-direction: column; + gap: 12px; + margin-top: 37px; +} diff --git a/src/components/domains/additional-purchases/VideoGuidesOffers/VideoGuidesOffers.tsx b/src/components/domains/additional-purchases/VideoGuidesOffers/VideoGuidesOffers.tsx new file mode 100644 index 0000000..867e083 --- /dev/null +++ b/src/components/domains/additional-purchases/VideoGuidesOffers/VideoGuidesOffers.tsx @@ -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(""); + 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 ( +
+ {offers.map(offer => ( + handleOfferClick(offer)} + /> + ))} +
+ ); +} + +export function VideoGuidesOffersSkeleton() { + return ; +} diff --git a/src/components/domains/additional-purchases/VideoGuidesPage/VideoGuidesPage.module.scss b/src/components/domains/additional-purchases/VideoGuidesPage/VideoGuidesPage.module.scss new file mode 100644 index 0000000..e8a36d9 --- /dev/null +++ b/src/components/domains/additional-purchases/VideoGuidesPage/VideoGuidesPage.module.scss @@ -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; +} diff --git a/src/components/domains/additional-purchases/VideoGuidesPage/VideoGuidesPage.tsx b/src/components/domains/additional-purchases/VideoGuidesPage/VideoGuidesPage.tsx new file mode 100644 index 0000000..b1db972 --- /dev/null +++ b/src/components/domains/additional-purchases/VideoGuidesPage/VideoGuidesPage.tsx @@ -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 ( + + + + + {t("title")} + + + {t("subtitle")} + + }> + + + + {t("description")} + + + + ); +} diff --git a/src/components/domains/additional-purchases/index.ts b/src/components/domains/additional-purchases/index.ts index 360f109..d71789b 100644 --- a/src/components/domains/additional-purchases/index.ts +++ b/src/components/domains/additional-purchases/index.ts @@ -14,3 +14,12 @@ export { ProductSelectionProvider, useProductSelection, } 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";