From 042bb250575c90e7912d56d981ef21b6e102d3e7 Mon Sep 17 00:00:00 2001 From: "dev.daminik00" Date: Mon, 27 Oct 2025 23:13:45 +0100 Subject: [PATCH 1/5] video guide --- messages/de.json | 58 +++++++++++++++++- messages/en.json | 30 ++++++---- messages/es.json | 58 +++++++++++++++++- .../AddConsultantButton.module.scss | 59 ++++++++++++------- .../AddConsultantButton.tsx | 8 ++- .../AddConsultantPage/AddConsultantPage.tsx | 16 ++++- .../AddGuidesButton.module.scss | 24 +++++++- .../AddGuidesButton/AddGuidesButton.tsx | 5 +- .../AddGuidesPage/AddGuidesPage.tsx | 16 ++++- .../AdditionalPurchaseBanner.module.scss | 45 ++++++++++++++ .../AdditionalPurchaseBanner.tsx | 29 +++++++++ .../AdditionalPurchaseBanner/HeartIcon.svg | 3 + .../Progress/Progress.module.scss | 6 +- .../Progress/Progress.tsx | 15 +++-- .../VideoGuidesOffer/VideoGuidesOffer.tsx | 2 +- .../VideoGuidesOffers/VideoGuidesOffers.tsx | 47 +++++---------- .../VideoGuidesPage/VideoGuidesPage.tsx | 18 +++++- .../domains/additional-purchases/index.ts | 1 + src/entities/session/funnel/types.ts | 1 + .../multiPages/useMultiPageNavigation.ts | 3 + 20 files changed, 355 insertions(+), 89 deletions(-) create mode 100644 src/components/domains/additional-purchases/AdditionalPurchaseBanner/AdditionalPurchaseBanner.module.scss create mode 100644 src/components/domains/additional-purchases/AdditionalPurchaseBanner/AdditionalPurchaseBanner.tsx create mode 100644 src/components/domains/additional-purchases/AdditionalPurchaseBanner/HeartIcon.svg diff --git a/messages/de.json b/messages/de.json index b55f3f2..4c19c40 100644 --- a/messages/de.json +++ b/messages/de.json @@ -222,6 +222,13 @@ "required_field": "This field is required" }, "AdditionalPurchases": { + "banner": { + "title": "Amazing!", + "description": "Your journey begins now" + }, + "Progress": { + "final_step": "Access Your Results" + }, "caution": { "title": "Caution!", "description": "To prevent double charges please don`t close the page and don`t go back." @@ -238,7 +245,8 @@ "save": "Save {discount}%", "get_my_consultation": "Get my consultation", "skip_this_offer": "Skip this offer", - "payment_error": "Something went wrong. Please try again later." + "payment_error": "Something went wrong. Please try again later.", + "copyright": "© 2025, Wit Lab LLC, California, US" }, "add-guides": { "title": "Choose your sign-up offer 🔥", @@ -248,6 +256,7 @@ "payment_error": "Something went wrong. Please try again later.", "select_product_error": "Please select a product", "skip_offer": "Skip offer", + "copyright": "© 2025, Wit Lab LLC, California, US", "products": { "main_ultra_pack": { "title": "ULTRA PACK", @@ -288,6 +297,53 @@ "emoji": "rised_hand.webp" } } + }, + "video-guides": { + "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" + }, + "main_money_reading": { + "title": "How to talk about feelings", + "subtitle": "Express your emotions and be understood", + "discount": "{discount}% OFF", + "price": "Now ", + "emoji": "heart_from_hands.webp" + } + }, + "payment_error": "Something went wrong. Please try again later.", + "select_product_error": "Please select a product" } }, "Chat": { diff --git a/messages/en.json b/messages/en.json index bcdcc8b..94d7dd2 100644 --- a/messages/en.json +++ b/messages/en.json @@ -229,6 +229,13 @@ "required_field": "This field is required" }, "AdditionalPurchases": { + "banner": { + "title": "Amazing!", + "description": "Your journey begins now" + }, + "Progress": { + "final_step": "Access Your Results" + }, "caution": { "title": "Caution!", "description": "To prevent double charges please don`t close the page and don`t go back." @@ -245,7 +252,8 @@ "save": "Save {discount}%", "get_my_consultation": "Get my consultation", "skip_this_offer": "Skip this offer", - "payment_error": "Something went wrong. Please try again later." + "payment_error": "Something went wrong. Please try again later.", + "copyright": "© 2025, Wit Lab LLC, California, US" }, "add-guides": { "title": "Choose your sign-up offer 🔥", @@ -255,6 +263,7 @@ "payment_error": "Something went wrong. Please try again later.", "select_product_error": "Please select a product", "skip_offer": "Skip offer", + "copyright": "© 2025, Wit Lab LLC, California, US", "products": { "main_ultra_pack": { "title": "ULTRA PACK", @@ -297,10 +306,6 @@ } }, "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.", @@ -335,16 +340,15 @@ "discount": "{discount}% OFF", "price": "Now ", "emoji": "heart_from_hands.webp" + }, + "main_money_reading": { + "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": { diff --git a/messages/es.json b/messages/es.json index 4dccde1..fefa091 100644 --- a/messages/es.json +++ b/messages/es.json @@ -222,6 +222,13 @@ "required_field": "This field is required" }, "AdditionalPurchases": { + "banner": { + "title": "Amazing!", + "description": "Your journey begins now" + }, + "Progress": { + "final_step": "Access Your Results" + }, "caution": { "title": "Caution!", "description": "To prevent double charges please don`t close the page and don`t go back." @@ -238,7 +245,8 @@ "save": "Save {discount}%", "get_my_consultation": "Get my consultation", "skip_this_offer": "Skip this offer", - "payment_error": "Something went wrong. Please try again later." + "payment_error": "Something went wrong. Please try again later.", + "copyright": "© 2025, Wit Lab LLC, California, US" }, "add-guides": { "title": "Choose your sign-up offer 🔥", @@ -248,6 +256,7 @@ "payment_error": "Something went wrong. Please try again later.", "select_product_error": "Please select a product", "skip_offer": "Skip offer", + "copyright": "© 2025, Wit Lab LLC, California, US", "products": { "main_ultra_pack": { "title": "ULTRA PACK", @@ -288,6 +297,53 @@ "emoji": "rised_hand.webp" } } + }, + "video-guides": { + "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" + }, + "main_money_reading": { + "title": "How to talk about feelings", + "subtitle": "Express your emotions and be understood", + "discount": "{discount}% OFF", + "price": "Now ", + "emoji": "heart_from_hands.webp" + } + }, + "payment_error": "Something went wrong. Please try again later.", + "select_product_error": "Please select a product" } }, "Chat": { diff --git a/src/components/domains/additional-purchases/AddConsultantButton/AddConsultantButton.module.scss b/src/components/domains/additional-purchases/AddConsultantButton/AddConsultantButton.module.scss index cf8be48..e2127ad 100644 --- a/src/components/domains/additional-purchases/AddConsultantButton/AddConsultantButton.module.scss +++ b/src/components/domains/additional-purchases/AddConsultantButton/AddConsultantButton.module.scss @@ -1,26 +1,45 @@ -.container.container { - display: flex; - flex-direction: column; - -webkit-box-align: center; - align-items: center; - width: 100%; - height: fit-content; - max-width: 560px; +.container { position: fixed; - bottom: 0dvh; + bottom: calc(0dvh + 16px); left: 50%; transform: translateX(-50%); - margin-top: 0px; - padding-bottom: 20px; - padding-inline: 15px; - z-index: 5; -} + width: 100%; + padding-inline: 24px; + max-width: 560px; + height: fit-content; -.button.button { - padding-block: 16px; -} + & > .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; -.skipButton.skipButton { - background-color: transparent; - text-decoration: underline; + & > .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/AddConsultantButton/AddConsultantButton.tsx b/src/components/domains/additional-purchases/AddConsultantButton/AddConsultantButton.tsx index 94d3f24..c73c6f5 100644 --- a/src/components/domains/additional-purchases/AddConsultantButton/AddConsultantButton.tsx +++ b/src/components/domains/additional-purchases/AddConsultantButton/AddConsultantButton.tsx @@ -67,20 +67,24 @@ export default function AddConsultantButton() { {isLoading ? ( ) : ( - + {t("get_my_consultation")} )} + + {t("copyright")} + ); } diff --git a/src/components/domains/additional-purchases/AddConsultantPage/AddConsultantPage.tsx b/src/components/domains/additional-purchases/AddConsultantPage/AddConsultantPage.tsx index 1b7eeb8..80b5f61 100644 --- a/src/components/domains/additional-purchases/AddConsultantPage/AddConsultantPage.tsx +++ b/src/components/domains/additional-purchases/AddConsultantPage/AddConsultantPage.tsx @@ -1,9 +1,12 @@ +"use client"; + import { useTranslations } from "next-intl"; import { AddConsultantButton, - Caution, ConsultationTable, + Progress, + useMultiPageNavigationContext, } from "@/components/domains/additional-purchases"; import { Card, Typography } from "@/components/ui"; @@ -11,10 +14,19 @@ import styles from "./AddConsultantPage.module.scss"; export default function AddConsultantPage() { const t = useTranslations("AdditionalPurchases.add-consultant"); + const { navigation } = useMultiPageNavigationContext(); + + // Получаем названия всех страниц для прогресса + const progressItems = navigation.data.map((item: any) => { + return item.title || item.type || ""; + }); return ( <> - + {t("title")} diff --git a/src/components/domains/additional-purchases/AddGuidesButton/AddGuidesButton.module.scss b/src/components/domains/additional-purchases/AddGuidesButton/AddGuidesButton.module.scss index 28a4f02..465d567 100644 --- a/src/components/domains/additional-purchases/AddGuidesButton/AddGuidesButton.module.scss +++ b/src/components/domains/additional-purchases/AddGuidesButton/AddGuidesButton.module.scss @@ -7,8 +7,26 @@ padding-inline: 24px; max-width: 560px; height: fit-content; -} -.button { - padding-block: 16px; + & > .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%; + } + } + + & > .copyright { + font-size: 12px; + line-height: 16px; + color: #9ca3af; + margin-top: 20px; + } } diff --git a/src/components/domains/additional-purchases/AddGuidesButton/AddGuidesButton.tsx b/src/components/domains/additional-purchases/AddGuidesButton/AddGuidesButton.tsx index 3e0a305..2cd60c1 100644 --- a/src/components/domains/additional-purchases/AddGuidesButton/AddGuidesButton.tsx +++ b/src/components/domains/additional-purchases/AddGuidesButton/AddGuidesButton.tsx @@ -68,11 +68,14 @@ export default function AddGuidesButton() { {isLoading ? ( ) : ( - + {isSkipOffer ? t("skip_offer") : t("button")} )} + + {t("copyright")} + ); } diff --git a/src/components/domains/additional-purchases/AddGuidesPage/AddGuidesPage.tsx b/src/components/domains/additional-purchases/AddGuidesPage/AddGuidesPage.tsx index c44f400..ee4e5df 100644 --- a/src/components/domains/additional-purchases/AddGuidesPage/AddGuidesPage.tsx +++ b/src/components/domains/additional-purchases/AddGuidesPage/AddGuidesPage.tsx @@ -1,12 +1,15 @@ +"use client"; + import { Suspense } from "react"; import { useTranslations } from "next-intl"; import { AddGuidesButton, - Caution, Offers, OffersSkeleton, ProductSelectionProvider, + Progress, + useMultiPageNavigationContext, } from "@/components/domains/additional-purchases"; import { Typography } from "@/components/ui"; @@ -14,10 +17,19 @@ import styles from "./AddGuidesPage.module.scss"; export default function AddGuidesPage() { const t = useTranslations("AdditionalPurchases.add-guides"); + const { navigation } = useMultiPageNavigationContext(); + + // Получаем названия всех страниц для прогресса + const progressItems = navigation.data.map((item: any) => { + return item.title || item.type || ""; + }); return ( - + {t("title")} diff --git a/src/components/domains/additional-purchases/AdditionalPurchaseBanner/AdditionalPurchaseBanner.module.scss b/src/components/domains/additional-purchases/AdditionalPurchaseBanner/AdditionalPurchaseBanner.module.scss new file mode 100644 index 0000000..5ecfd76 --- /dev/null +++ b/src/components/domains/additional-purchases/AdditionalPurchaseBanner/AdditionalPurchaseBanner.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/AdditionalPurchaseBanner/AdditionalPurchaseBanner.tsx b/src/components/domains/additional-purchases/AdditionalPurchaseBanner/AdditionalPurchaseBanner.tsx new file mode 100644 index 0000000..11b98d3 --- /dev/null +++ b/src/components/domains/additional-purchases/AdditionalPurchaseBanner/AdditionalPurchaseBanner.tsx @@ -0,0 +1,29 @@ +"use client"; + +import { useTranslations } from "next-intl"; + +import { Typography } from "@/components/ui"; + +import HeartIcon from "./HeartIcon.svg"; + +import styles from "./AdditionalPurchaseBanner.module.scss"; + +export default function AdditionalPurchaseBanner() { + const t = useTranslations("AdditionalPurchases.banner"); + + return ( +
+
+ +
+
+ + {t("title")} + + + {t("description")} + +
+
+ ); +} diff --git a/src/components/domains/additional-purchases/AdditionalPurchaseBanner/HeartIcon.svg b/src/components/domains/additional-purchases/AdditionalPurchaseBanner/HeartIcon.svg new file mode 100644 index 0000000..78e1716 --- /dev/null +++ b/src/components/domains/additional-purchases/AdditionalPurchaseBanner/HeartIcon.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/components/domains/additional-purchases/Progress/Progress.module.scss b/src/components/domains/additional-purchases/Progress/Progress.module.scss index 00f1217..a57cad1 100644 --- a/src/components/domains/additional-purchases/Progress/Progress.module.scss +++ b/src/components/domains/additional-purchases/Progress/Progress.module.scss @@ -19,7 +19,7 @@ & > .marker { width: 32px; height: 32px; - border: 1px solid #e2e8f0; + border: 2px solid #e2e8f0; border-radius: 50%; background: f8fafc; display: flex; @@ -76,7 +76,7 @@ & > .marker { box-shadow: 0px 0px 0px 0px #3b82f626; background-color: #2866ed; - border: none; + border: 2px solid #2866ed; & > .number { display: none; @@ -91,7 +91,7 @@ .connector { position: absolute; - top: 15px; + top: 16px; 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 index 3679b54..9aa5b18 100644 --- a/src/components/domains/additional-purchases/Progress/Progress.tsx +++ b/src/components/domains/additional-purchases/Progress/Progress.tsx @@ -9,20 +9,23 @@ import { useDynamicSize } from "@/hooks/DOM/useDynamicSize"; import styles from "./Progress.module.scss"; interface IProgressProps { + items: string[]; activeItemIndex: number; } -export default function Progress({ activeItemIndex }: IProgressProps) { +export default function Progress({ items, activeItemIndex }: IProgressProps) { const t = useTranslations("AdditionalPurchases.Progress"); const { width: containerWidth, elementRef } = useDynamicSize({ defaultWidth: 327, }); - const items = Object.values(t.raw("items") as Record); + // Всегда добавляем финальный пункт в конец + const finalStep = t("final_step"); + const allItems = [...items, finalStep]; const firstChild = elementRef.current?.childNodes[0] as HTMLElement; const lastChild = elementRef.current?.childNodes[ - items.length - 1 + allItems.length - 1 ] as HTMLElement; const leftIndent = ((firstChild?.getBoundingClientRect().width || 100) - 32) / 2; @@ -31,7 +34,7 @@ export default function Progress({ activeItemIndex }: IProgressProps) { return (
- {items.map((item, index) => ( + {allItems.map((item, index) => (
{ - 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 ?? []; + // Используем данные с сервера и добавляем oldPrice на основе зашитого процента скидки + const serverOffers = data?.variants ?? []; + return serverOffers.map((offer: IFunnelPaymentVariant, index: number) => { + // Первый товар имеет скидку 50%, остальные - 45% + const discountPercent = index === 0 ? FIRST_PRODUCT_DISCOUNT : OTHER_PRODUCTS_DISCOUNT; + // Рассчитываем oldPrice: если price это цена со скидкой X%, то oldPrice = price / (1 - X/100) + const oldPrice = Math.round(offer.price / (1 - discountPercent / 100)); + return { + ...offer, + oldPrice, + }; + }); }, [data]); const [activeOffer, setActiveOffer] = useState(""); const { setSelectedProduct } = useProductSelection(); diff --git a/src/components/domains/additional-purchases/VideoGuidesPage/VideoGuidesPage.tsx b/src/components/domains/additional-purchases/VideoGuidesPage/VideoGuidesPage.tsx index b1db972..338358f 100644 --- a/src/components/domains/additional-purchases/VideoGuidesPage/VideoGuidesPage.tsx +++ b/src/components/domains/additional-purchases/VideoGuidesPage/VideoGuidesPage.tsx @@ -1,13 +1,16 @@ +"use client"; + import { Suspense } from "react"; import { useTranslations } from "next-intl"; import { + AdditionalPurchaseBanner, ProductSelectionProvider, Progress, - VideoGuidesBanner, VideoGuidesButton, VideoGuidesOffers, VideoGuidesOffersSkeleton, + useMultiPageNavigationContext, } from "@/components/domains/additional-purchases"; import { Typography } from "@/components/ui"; @@ -15,11 +18,20 @@ import styles from "./VideoGuidesPage.module.scss"; export default function VideoGuidesPage() { const t = useTranslations("AdditionalPurchases.video-guides"); + const { navigation } = useMultiPageNavigationContext(); + + // Получаем названия всех страниц для прогресса + const progressItems = navigation.data.map((item: any) => { + return item.title || item.type || ""; + }); return ( - - + + {t("title")} diff --git a/src/components/domains/additional-purchases/index.ts b/src/components/domains/additional-purchases/index.ts index d71789b..2bd3916 100644 --- a/src/components/domains/additional-purchases/index.ts +++ b/src/components/domains/additional-purchases/index.ts @@ -2,6 +2,7 @@ export { default as AddConsultantButton } from "./AddConsultantButton/AddConsult export { default as AddConsultantPage } from "./AddConsultantPage/AddConsultantPage"; export { default as AddGuidesButton } from "./AddGuidesButton/AddGuidesButton"; export { default as AddGuidesPage } from "./AddGuidesPage/AddGuidesPage"; +export { default as AdditionalPurchaseBanner } from "./AdditionalPurchaseBanner/AdditionalPurchaseBanner"; export { default as Caution } from "./Caution/Caution"; export { default as ConsultationTable } from "./ConsultationTable/ConsultationTable"; export { diff --git a/src/entities/session/funnel/types.ts b/src/entities/session/funnel/types.ts index 9231dcd..3735dda 100644 --- a/src/entities/session/funnel/types.ts +++ b/src/entities/session/funnel/types.ts @@ -35,6 +35,7 @@ export const FunnelPaymentPlacementSchema = z.object({ variants: z.array(FunnelPaymentVariantSchema).optional(), paymentUrl: z.string().optional(), type: z.string().optional(), + title: z.string().optional(), }); export const FunnelSchema = z.object({ diff --git a/src/hooks/multiPages/useMultiPageNavigation.ts b/src/hooks/multiPages/useMultiPageNavigation.ts index c1680b8..fcb5919 100644 --- a/src/hooks/multiPages/useMultiPageNavigation.ts +++ b/src/hooks/multiPages/useMultiPageNavigation.ts @@ -16,6 +16,7 @@ interface PageNavigationOptions { } interface PageNavigationReturn { + data: T[]; currentItem: T | undefined; currentIndex: number; isFirst: boolean; @@ -124,6 +125,7 @@ export function useMultiPageNavigation({ return useMemo( () => ({ + data, currentItem, nextItem, currentIndex, @@ -141,6 +143,7 @@ export function useMultiPageNavigation({ totalPages, }), [ + data, currentItem, nextItem, currentIndex, From 95e05cbabb74a2791fa533e8761325d11d8ea4f7 Mon Sep 17 00:00:00 2001 From: "dev.daminik00" Date: Tue, 28 Oct 2025 02:06:45 +0100 Subject: [PATCH 2/5] add video guide to dashboard --- messages/de.json | 38 +-- messages/en.json | 47 +--- messages/es.json | 38 +-- src/app/[locale]/(core)/page.tsx | 5 + .../VideoGuidesOffer.module.scss | 38 +-- .../VideoGuidesOffer/VideoGuidesOffer.tsx | 54 ++-- .../VideoGuidesOffers/VideoGuidesOffers.tsx | 3 +- .../VideoGuideCard/VideoGuideCard.module.scss | 236 ++++++++++++++++++ .../cards/VideoGuideCard/VideoGuideCard.tsx | 102 ++++++++ .../dashboard/cards/VideoGuideCard/index.ts | 1 + .../domains/dashboard/cards/index.ts | 1 + .../AdvisersSection/AdvisersSection.tsx | 7 +- .../CompatibilitySection.tsx | 7 +- .../MeditationSection/MeditationSection.tsx | 7 +- .../sections/PalmSection/PalmSection.tsx | 7 +- .../VideoGuidesSection.module.scss | 12 + .../VideoGuidesSection/VideoGuidesSection.tsx | 38 +++ .../sections/VideoGuidesSection/index.ts | 1 + .../domains/dashboard/sections/index.ts | 1 + src/entities/dashboard/loaders.ts | 11 +- src/entities/dashboard/types.ts | 25 +- src/entities/session/funnel/types.ts | 3 + 22 files changed, 510 insertions(+), 172 deletions(-) create mode 100644 src/components/domains/dashboard/cards/VideoGuideCard/VideoGuideCard.module.scss create mode 100644 src/components/domains/dashboard/cards/VideoGuideCard/VideoGuideCard.tsx create mode 100644 src/components/domains/dashboard/cards/VideoGuideCard/index.ts create mode 100644 src/components/domains/dashboard/sections/VideoGuidesSection/VideoGuidesSection.module.scss create mode 100644 src/components/domains/dashboard/sections/VideoGuidesSection/VideoGuidesSection.tsx create mode 100644 src/components/domains/dashboard/sections/VideoGuidesSection/index.ts diff --git a/messages/de.json b/messages/de.json index 4c19c40..49c3b33 100644 --- a/messages/de.json +++ b/messages/de.json @@ -305,43 +305,7 @@ "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" - }, - "main_money_reading": { - "title": "How to talk about feelings", - "subtitle": "Express your emotions and be understood", - "discount": "{discount}% OFF", - "price": "Now ", - "emoji": "heart_from_hands.webp" - } - }, + "now": "Now", "payment_error": "Something went wrong. Please try again later.", "select_product_error": "Please select a product" } diff --git a/messages/en.json b/messages/en.json index 94d7dd2..4ad28d3 100644 --- a/messages/en.json +++ b/messages/en.json @@ -312,43 +312,16 @@ "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" - }, - "main_money_reading": { - "title": "How to talk about feelings", - "subtitle": "Express your emotions and be understood", - "discount": "{discount}% OFF", - "price": "Now ", - "emoji": "heart_from_hands.webp" - } - } + "now": "Now" + } + }, + "Dashboard": { + "adviser": { + "title": "Talk to an Astrologer" + }, + "videoGuides": { + "now": "Now", + "purchaseFor": "Buy for {price}" } }, "Chat": { diff --git a/messages/es.json b/messages/es.json index fefa091..614f564 100644 --- a/messages/es.json +++ b/messages/es.json @@ -305,43 +305,7 @@ "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" - }, - "main_money_reading": { - "title": "How to talk about feelings", - "subtitle": "Express your emotions and be understood", - "discount": "{discount}% OFF", - "price": "Now ", - "emoji": "heart_from_hands.webp" - } - }, + "now": "Now", "payment_error": "Something went wrong. Please try again later.", "select_product_error": "Please select a product" } diff --git a/src/app/[locale]/(core)/page.tsx b/src/app/[locale]/(core)/page.tsx index 4f4e81f..898b6e8 100644 --- a/src/app/[locale]/(core)/page.tsx +++ b/src/app/[locale]/(core)/page.tsx @@ -11,6 +11,7 @@ import { PalmSection, PalmSectionSkeleton, PortraitsSection, + VideoGuidesSection, } from "@/components/domains/dashboard"; import { loadChatsList } from "@/entities/chats/loaders"; import { @@ -19,6 +20,7 @@ import { loadMeditations, loadPalms, loadPortraits, + loadVideoGuides, } from "@/entities/dashboard/loaders"; import styles from "./page.module.scss"; @@ -26,11 +28,14 @@ import styles from "./page.module.scss"; export default async function Home() { const chatsPromise = loadChatsList(); const portraits = await loadPortraits(); + const videoGuides = await loadVideoGuides(); return (
+ + }> diff --git a/src/components/domains/additional-purchases/VideoGuidesOffer/VideoGuidesOffer.module.scss b/src/components/domains/additional-purchases/VideoGuidesOffer/VideoGuidesOffer.module.scss index 67878d4..aa1012a 100644 --- a/src/components/domains/additional-purchases/VideoGuidesOffer/VideoGuidesOffer.module.scss +++ b/src/components/domains/additional-purchases/VideoGuidesOffer/VideoGuidesOffer.module.scss @@ -7,6 +7,25 @@ box-shadow: 0px 0px 30px 0px #0000001f; gap: 4px; + & > .topBadge { + position: absolute; + top: -15px; + right: 49px; + background: #ff3737; + box-shadow: 0px 1px 11.98px 0px #ff44448c; + border: 2px solid #ffffff4d; + padding: 8px 12px; + border-radius: 9999px; + z-index: 10; + + & > .topBadgeText { + color: #fff; + font-size: 14px; + font-weight: 800; + letter-spacing: 0.5px; + } + } + & > .content { display: grid; grid-template-columns: 40px 1fr 20px; @@ -109,23 +128,4 @@ } } } - - &.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 index 2ec300c..9ff7d27 100644 --- a/src/components/domains/additional-purchases/VideoGuidesOffer/VideoGuidesOffer.tsx +++ b/src/components/domains/additional-purchases/VideoGuidesOffer/VideoGuidesOffer.tsx @@ -11,30 +11,25 @@ import styles from "./VideoGuidesOffer.module.scss"; interface VideoGuidesOfferProps { offer: IFunnelPaymentVariant; isActive: boolean; + isFirstOffer?: boolean; className?: string; onClick: () => void; } export default function VideoGuidesOffer(props: VideoGuidesOfferProps) { - const { offer, isActive, className, onClick } = props; + const { offer, isActive, isFirstOffer, className, onClick } = props; - const { key, price, oldPrice } = offer; + const { key, name, description, emoji, 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.round( (((oldPrice || 0) - price) / (oldPrice || 0)) * 100 ); - const emoji = t.has("emoji") ? t("emoji") : undefined; + const t = useTranslations("AdditionalPurchases.video-guides"); return ( + {isFirstOffer && ( +
+ {discount}% OFF +
+ )}
- {t("title")} + {name} - {subtitle && ( + {description && ( - {subtitle} + {description} )}
@@ -76,24 +76,20 @@ export default function VideoGuidesOffer(props: VideoGuidesOfferProps) {
- {t.rich("price", { - price: () => ( - - {getFormattedPrice(price, currency)} - - ), - oldPrice: () => ( - - {getFormattedPrice(oldPrice || 0, currency)} - - ), - })} - - - {t("discount", { - discount: discount || 0, - })} + {t("now")}{" "} + + {getFormattedPrice(price, currency)} + + {" "} + + {getFormattedPrice(oldPrice || 0, currency)} + + {!isFirstOffer && ( + + {discount}% OFF + + )}
); diff --git a/src/components/domains/additional-purchases/VideoGuidesOffers/VideoGuidesOffers.tsx b/src/components/domains/additional-purchases/VideoGuidesOffers/VideoGuidesOffers.tsx index 3b972d0..7bc3d3c 100644 --- a/src/components/domains/additional-purchases/VideoGuidesOffers/VideoGuidesOffers.tsx +++ b/src/components/domains/additional-purchases/VideoGuidesOffers/VideoGuidesOffers.tsx @@ -49,11 +49,12 @@ export default function VideoGuidesOffers() { return (
- {offers.map(offer => ( + {offers.map((offer, index) => ( handleOfferClick(offer)} /> ))} diff --git a/src/components/domains/dashboard/cards/VideoGuideCard/VideoGuideCard.module.scss b/src/components/domains/dashboard/cards/VideoGuideCard/VideoGuideCard.module.scss new file mode 100644 index 0000000..96a3e87 --- /dev/null +++ b/src/components/domains/dashboard/cards/VideoGuideCard/VideoGuideCard.module.scss @@ -0,0 +1,236 @@ +.container.container { + display: flex; + min-width: 260px; + min-height: 280px; + flex-direction: column; + align-items: flex-start; + border-radius: 24px; + border: 0 solid #E5E7EB; + background: rgba(0, 0, 0, 0); + box-shadow: 0 4px 6px 0 rgba(0, 0, 0, 0.10), 0 10px 15px 0 rgba(0, 0, 0, 0.10); + cursor: pointer; + overflow: hidden; + padding: 0; + transition: transform 0.2s ease, box-shadow 0.2s ease; + + &:hover { + transform: translateY(-4px); + box-shadow: 0 6px 10px 0 rgba(0, 0, 0, 0.12), 0 12px 18px 0 rgba(0, 0, 0, 0.12); + } +} + +// Image section +.image { + display: flex; + min-height: 160px; + padding: 16px; + flex-direction: column; + justify-content: center; + align-items: center; + gap: 10px; + align-self: stretch; + border: 0 solid #E5E7EB; + position: relative; + overflow: hidden; + + &::before { + content: ''; + position: absolute; + inset: 0; + background: lightgray 50% / cover no-repeat; + z-index: 0; + } + + .imageContent { + position: absolute; + inset: 0; + width: 100%; + height: 100%; + object-fit: cover; + object-position: center; + pointer-events: none; + z-index: 0; + } + + .playIcon { + position: relative; + z-index: 1; + width: 64px; + height: 65px; + + svg { + width: 64px; + height: 65px; + filter: drop-shadow(0 2px 12px rgba(0, 0, 0, 0.25)); + } + } +} + +// Content section +.content { + display: flex; + padding: 16px; + flex-direction: column; + align-items: flex-start; + justify-content: space-between; + gap: 24px; + align-self: stretch; + background: #FFF; + flex: 1; + + .purchased & { + gap: 6px; + } +} + +// Top section +.top { + display: flex; + align-items: flex-start; + gap: 14px; + align-self: stretch; + + .text { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 3px; + flex: 1 0 0; + } + + .arrowButton { + display: flex; + width: 40px; + height: 40px; + padding: 12px 0; + justify-content: center; + align-items: center; + border-radius: 9999px; + border: 0 solid #E5E7EB; + background: #F5F5F7; + cursor: pointer; + transition: opacity 0.2s ease; + + svg { + width: 8px; + height: 14px; + flex-shrink: 0; + } + + &:hover { + opacity: 0.8; + } + } +} + +.title { + align-self: stretch; + color: #1D1D1F; + font-family: Inter, sans-serif; + font-size: 20px; + font-style: normal; + font-weight: 500; + line-height: 28px; + text-align: left; +} + +.subtitle { + align-self: stretch; + color: #6B7280; + font-family: Inter, sans-serif; + font-size: 16px; + font-style: normal; + font-weight: 400; + line-height: 20px; + text-align: left; +} + +// Bottom section +.bottom { + display: flex; + flex-direction: column; + justify-content: flex-end; + align-items: flex-end; + gap: 8px; + align-self: stretch; +} + +.bottomText { + display: flex; + height: 24px; + justify-content: space-between; + align-items: center; + align-self: stretch; +} + +.duration { + display: flex; + width: 49px; + flex-direction: column; + justify-content: center; + align-self: stretch; + color: #6B7280; + font-family: Inter, sans-serif; + font-size: 16px; + font-style: normal; + font-weight: 400; + line-height: 20px; +} + +.discountBadge { + display: flex; + padding: 6px 10px; + justify-content: center; + align-items: center; + gap: 10px; + align-self: stretch; + border-radius: 9999px; + border: 0 solid #E5E7EB; + background: rgba(255, 107, 107, 0.10); +} + +.discountText { + color: #FF6B6B; + text-align: center; + font-family: Inter, sans-serif; + font-size: 12px; + font-style: normal; + font-weight: 700; + line-height: normal; + + .oldPrice { + color: #8B8B8B; + font-family: Inter, sans-serif; + font-size: 12px; + font-style: normal; + font-weight: 500; + line-height: normal; + text-decoration-line: line-through; + } +} + +.buyButton.buyButton { + display: flex; + padding: 8px 10px; + justify-content: center; + align-items: center; + gap: 10px; + border-radius: 12px; + border: 0 solid #E5E7EB; + background: #2563EB; + cursor: pointer; + transition: opacity 0.2s ease; + width: auto; + + color: #FFF; + text-align: center; + font-family: Inter, sans-serif; + font-size: 14px; + font-style: normal; + font-weight: 500; + line-height: normal; + + &:hover { + opacity: 0.9; + } +} diff --git a/src/components/domains/dashboard/cards/VideoGuideCard/VideoGuideCard.tsx b/src/components/domains/dashboard/cards/VideoGuideCard/VideoGuideCard.tsx new file mode 100644 index 0000000..2361c9a --- /dev/null +++ b/src/components/domains/dashboard/cards/VideoGuideCard/VideoGuideCard.tsx @@ -0,0 +1,102 @@ +"use client"; + +import { useTranslations } from "next-intl"; +import clsx from "clsx"; + +import { Button, Card, Typography } from "@/components/ui"; +import { getFormattedPrice } from "@/shared/utils/price"; +import { Currency } from "@/types"; + +import styles from "./VideoGuideCard.module.scss"; + +interface VideoGuideCardProps { + name: string; + description: string; + imageUrl: string; + duration: string; + price: number; + oldPrice: number; + discount: number; + isPurchased: boolean; + className?: string; +} + +export default function VideoGuideCard(props: VideoGuideCardProps) { + const { name, description, imageUrl, duration, price, oldPrice, discount, isPurchased, className } = props; + + const tCommon = useTranslations("Dashboard.videoGuides"); + + const currency = Currency.USD; + + const handleClick = () => { + // TODO: Implement navigation or purchase logic + console.log("Video guide clicked", name); + }; + + return ( + + {/* Image with Play Icon */} +
+ {name} +
+ + + + + + + + + + + + + + + + + +
+
+ + {/* Content */} +
+ {/* Top Section */} +
+
+ + {name} + + + {description} + +
+ +
+ + {/* Bottom Section */} +
+
+ {duration} + {!isPurchased && ( +
+ + {discount}% OFF {getFormattedPrice(oldPrice, currency)} + +
+ )} +
+ {!isPurchased && ( + + )} +
+
+
+ ); +} diff --git a/src/components/domains/dashboard/cards/VideoGuideCard/index.ts b/src/components/domains/dashboard/cards/VideoGuideCard/index.ts new file mode 100644 index 0000000..e1d5d96 --- /dev/null +++ b/src/components/domains/dashboard/cards/VideoGuideCard/index.ts @@ -0,0 +1 @@ +export { default as VideoGuideCard } from "./VideoGuideCard"; diff --git a/src/components/domains/dashboard/cards/index.ts b/src/components/domains/dashboard/cards/index.ts index 65540f0..1a27805 100644 --- a/src/components/domains/dashboard/cards/index.ts +++ b/src/components/domains/dashboard/cards/index.ts @@ -3,3 +3,4 @@ export { default as CompatibilityCard } from "./CompatibilityCard/CompatibilityC export { default as MeditationCard } from "./MeditationCard/MeditationCard"; export { default as PalmCard } from "./PalmCard/PalmCard"; export { default as PortraitCard } from "./PortraitCard/PortraitCard"; +export { default as VideoGuideCard } from "./VideoGuideCard/VideoGuideCard"; diff --git a/src/components/domains/dashboard/sections/AdvisersSection/AdvisersSection.tsx b/src/components/domains/dashboard/sections/AdvisersSection/AdvisersSection.tsx index addacd3..b083fa3 100644 --- a/src/components/domains/dashboard/sections/AdvisersSection/AdvisersSection.tsx +++ b/src/components/domains/dashboard/sections/AdvisersSection/AdvisersSection.tsx @@ -31,7 +31,12 @@ export default function AdvisersSection({ }: AdvisersSectionProps) { const assistants = use(promiseAssistants); const chats = use(promiseChats); - const columns = getOptimalColumns(assistants?.length || 0); + + if (!assistants || assistants.length === 0) { + return null; + } + + const columns = getOptimalColumns(assistants.length); return (
diff --git a/src/components/domains/dashboard/sections/CompatibilitySection/CompatibilitySection.tsx b/src/components/domains/dashboard/sections/CompatibilitySection/CompatibilitySection.tsx index b0455c8..5289ff1 100644 --- a/src/components/domains/dashboard/sections/CompatibilitySection/CompatibilitySection.tsx +++ b/src/components/domains/dashboard/sections/CompatibilitySection/CompatibilitySection.tsx @@ -22,7 +22,12 @@ export default function CompatibilitySection({ gridDisplayMode = "horizontal", }: CompatibilitySectionProps) { const compatibilities = use(promise); - const columns = Math.ceil(compatibilities?.length / 2); + + if (!compatibilities || compatibilities.length === 0) { + return null; + } + + const columns = Math.ceil(compatibilities.length / 2); return (
diff --git a/src/components/domains/dashboard/sections/MeditationSection/MeditationSection.tsx b/src/components/domains/dashboard/sections/MeditationSection/MeditationSection.tsx index 45d9638..8e3de49 100644 --- a/src/components/domains/dashboard/sections/MeditationSection/MeditationSection.tsx +++ b/src/components/domains/dashboard/sections/MeditationSection/MeditationSection.tsx @@ -20,7 +20,12 @@ export default function MeditationSection({ gridDisplayMode = "horizontal", }: MeditationSectionProps) { const meditations = use(promise); - const columns = meditations?.length; + + if (!meditations || meditations.length === 0) { + return null; + } + + const columns = meditations.length; return (
diff --git a/src/components/domains/dashboard/sections/PalmSection/PalmSection.tsx b/src/components/domains/dashboard/sections/PalmSection/PalmSection.tsx index bffcc40..d6fa71a 100644 --- a/src/components/domains/dashboard/sections/PalmSection/PalmSection.tsx +++ b/src/components/domains/dashboard/sections/PalmSection/PalmSection.tsx @@ -15,7 +15,12 @@ export default function PalmSection({ promise: Promise; }) { const palms = use(promise); - const columns = palms?.length; + + if (!palms || palms.length === 0) { + return null; + } + + const columns = palms.length; return (
diff --git a/src/components/domains/dashboard/sections/VideoGuidesSection/VideoGuidesSection.module.scss b/src/components/domains/dashboard/sections/VideoGuidesSection/VideoGuidesSection.module.scss new file mode 100644 index 0000000..d00ff19 --- /dev/null +++ b/src/components/domains/dashboard/sections/VideoGuidesSection/VideoGuidesSection.module.scss @@ -0,0 +1,12 @@ +.sectionContent.sectionContent { + overflow-x: scroll; + -webkit-overflow-scrolling: touch; + width: calc(100% + 32px); + padding: 20px 16px 24px 16px; + padding-right: 0; + margin: -20px -16px -24px -16px; +} + +.grid { + padding-right: 16px; +} diff --git a/src/components/domains/dashboard/sections/VideoGuidesSection/VideoGuidesSection.tsx b/src/components/domains/dashboard/sections/VideoGuidesSection/VideoGuidesSection.tsx new file mode 100644 index 0000000..a4426b1 --- /dev/null +++ b/src/components/domains/dashboard/sections/VideoGuidesSection/VideoGuidesSection.tsx @@ -0,0 +1,38 @@ +import { Grid, Section } from "@/components/ui"; +import { VideoGuide } from "@/entities/dashboard/types"; + +import { VideoGuideCard } from "../../cards"; + +import styles from "./VideoGuidesSection.module.scss"; + +interface VideoGuidesSectionProps { + videoGuides: VideoGuide[]; +} + +export default function VideoGuidesSection({ videoGuides }: VideoGuidesSectionProps) { + if (!videoGuides || videoGuides.length === 0) { + return null; + } + + const columns = videoGuides.length; + + return ( +
+ + {videoGuides.map(videoGuide => ( + + ))} + +
+ ); +} diff --git a/src/components/domains/dashboard/sections/VideoGuidesSection/index.ts b/src/components/domains/dashboard/sections/VideoGuidesSection/index.ts new file mode 100644 index 0000000..444247a --- /dev/null +++ b/src/components/domains/dashboard/sections/VideoGuidesSection/index.ts @@ -0,0 +1 @@ +export { default as VideoGuidesSection } from "./VideoGuidesSection"; diff --git a/src/components/domains/dashboard/sections/index.ts b/src/components/domains/dashboard/sections/index.ts index 989e957..bc600d1 100644 --- a/src/components/domains/dashboard/sections/index.ts +++ b/src/components/domains/dashboard/sections/index.ts @@ -20,3 +20,4 @@ export { PalmSectionSkeleton, } from "./PalmSection/PalmSection"; export { default as PortraitsSection } from "./PortraitsSection/PortraitsSection"; +export { default as VideoGuidesSection } from "./VideoGuidesSection/VideoGuidesSection"; diff --git a/src/entities/dashboard/loaders.ts b/src/entities/dashboard/loaders.ts index c1d5fa0..af0a7e2 100644 --- a/src/entities/dashboard/loaders.ts +++ b/src/entities/dashboard/loaders.ts @@ -5,15 +5,18 @@ import { getDashboard } from "./api"; export const loadDashboard = cache(getDashboard); export const loadAssistants = cache(() => - loadDashboard().then(d => d.assistants) + loadDashboard().then(d => d.assistants || []) ); export const loadCompatibility = cache(() => - loadDashboard().then(d => d.compatibilityActions) + loadDashboard().then(d => d.compatibilityActions || []) ); export const loadMeditations = cache(() => - loadDashboard().then(d => d.meditations) + loadDashboard().then(d => d.meditations || []) ); -export const loadPalms = cache(() => loadDashboard().then(d => d.palmActions)); +export const loadPalms = cache(() => loadDashboard().then(d => d.palmActions || [])); export const loadPortraits = cache(() => loadDashboard().then(d => d.partnerPortraits || []) ); +export const loadVideoGuides = cache(() => + loadDashboard().then(d => d.videoGuides || []) +); diff --git a/src/entities/dashboard/types.ts b/src/entities/dashboard/types.ts index 29a784e..ddb413e 100644 --- a/src/entities/dashboard/types.ts +++ b/src/entities/dashboard/types.ts @@ -61,12 +61,29 @@ export const PartnerPortraitSchema = z.object({ }); export type PartnerPortrait = z.infer; +/* ---------- Video Guide ---------- */ +export const VideoGuideSchema = z.object({ + id: z.string(), + key: z.string(), + type: z.string(), + name: z.string(), + description: z.string(), + imageUrl: z.string(), + duration: z.string(), + price: z.number(), + oldPrice: z.number(), + discount: z.number(), + isPurchased: z.boolean(), +}); +export type VideoGuide = z.infer; + /* ---------- Итоговый ответ /dashboard ---------- */ export const DashboardSchema = z.object({ - assistants: z.array(AssistantSchema), - compatibilityActions: z.array(ActionSchema), - palmActions: z.array(ActionSchema), - meditations: z.array(ActionSchema), + assistants: z.array(AssistantSchema).optional(), + compatibilityActions: z.array(ActionSchema).optional(), + palmActions: z.array(ActionSchema).optional(), + meditations: z.array(ActionSchema).optional(), partnerPortraits: z.array(PartnerPortraitSchema).optional(), + videoGuides: z.array(VideoGuideSchema).optional(), }); export type DashboardData = z.infer; diff --git a/src/entities/session/funnel/types.ts b/src/entities/session/funnel/types.ts index 3735dda..8bc3cf6 100644 --- a/src/entities/session/funnel/types.ts +++ b/src/entities/session/funnel/types.ts @@ -17,6 +17,9 @@ export const FunnelPaymentVariantSchema = z.object({ id: z.string(), key: z.string(), type: z.string(), + name: z.string().optional(), + description: z.string().optional(), + emoji: z.string().optional(), price: z.number(), oldPrice: z.number().optional(), trialPrice: z.number().optional(), From 1adac2836bc4c0d3e409ea728bf16f0d724a7dcd Mon Sep 17 00:00:00 2001 From: "dev.daminik00" Date: Tue, 28 Oct 2025 05:58:51 +0100 Subject: [PATCH 3/5] add video --- src/app/[locale]/(core)/page.tsx | 3 + .../(core)/video-guides/[id]/layout.tsx | 7 + .../(core)/video-guides/[id]/loading.tsx | 22 +++ .../(core)/video-guides/[id]/page.tsx | 38 +++++ .../AddConsultantButton.tsx | 24 ++- .../AddGuidesButton/AddGuidesButton.tsx | 22 ++- .../Progress/Progress.module.scss | 52 +++++- .../Progress/Progress.tsx | 154 +++++++++++++----- .../VideoGuidesButton/VideoGuidesButton.tsx | 22 ++- .../GlobalNewMessagesBanner.tsx | 5 +- .../VideoGuideCard/VideoGuideCard.module.scss | 35 +++- .../cards/VideoGuideCard/VideoGuideCard.tsx | 73 ++++++--- .../VideoGuidesSection.module.scss | 8 + .../VideoGuidesSection/VideoGuidesSection.tsx | 60 ++++++- .../PortraitView/PortraitView.module.scss | 2 + .../VideoGuideView/VideoGuideView.module.scss | 96 +++++++++++ .../VideoGuideView/VideoGuideView.tsx | 76 +++++++++ src/components/domains/video-guides/index.ts | 1 + src/components/layout/Header/Header.tsx | 5 +- .../layout/NavigationBar/NavigationBar.tsx | 5 +- src/entities/dashboard/actions.ts | 38 +++++ src/entities/dashboard/api.ts | 2 +- src/entities/dashboard/loaders.ts | 38 ++--- src/entities/dashboard/types.ts | 1 + src/hooks/payment/useSingleCheckout.ts | 17 +- src/hooks/video-guides/index.ts | 1 + .../video-guides/useVideoGuidePurchase.ts | 117 +++++++++++++ src/shared/constants/api-routes.ts | 2 + 28 files changed, 802 insertions(+), 124 deletions(-) create mode 100644 src/app/[locale]/(core)/video-guides/[id]/layout.tsx create mode 100644 src/app/[locale]/(core)/video-guides/[id]/loading.tsx create mode 100644 src/app/[locale]/(core)/video-guides/[id]/page.tsx create mode 100644 src/components/domains/video-guides/VideoGuideView/VideoGuideView.module.scss create mode 100644 src/components/domains/video-guides/VideoGuideView/VideoGuideView.tsx create mode 100644 src/components/domains/video-guides/index.ts create mode 100644 src/entities/dashboard/actions.ts create mode 100644 src/hooks/video-guides/index.ts create mode 100644 src/hooks/video-guides/useVideoGuidePurchase.ts diff --git a/src/app/[locale]/(core)/page.tsx b/src/app/[locale]/(core)/page.tsx index 898b6e8..b701641 100644 --- a/src/app/[locale]/(core)/page.tsx +++ b/src/app/[locale]/(core)/page.tsx @@ -25,6 +25,9 @@ import { import styles from "./page.module.scss"; +// Force dynamic to always get fresh data +export const dynamic = "force-dynamic"; + export default async function Home() { const chatsPromise = loadChatsList(); const portraits = await loadPortraits(); diff --git a/src/app/[locale]/(core)/video-guides/[id]/layout.tsx b/src/app/[locale]/(core)/video-guides/[id]/layout.tsx new file mode 100644 index 0000000..ef8c04f --- /dev/null +++ b/src/app/[locale]/(core)/video-guides/[id]/layout.tsx @@ -0,0 +1,7 @@ +export default function VideoGuideLayout({ + children, +}: { + children: React.ReactNode; +}) { + return <>{children}; +} diff --git a/src/app/[locale]/(core)/video-guides/[id]/loading.tsx b/src/app/[locale]/(core)/video-guides/[id]/loading.tsx new file mode 100644 index 0000000..ef6ee0e --- /dev/null +++ b/src/app/[locale]/(core)/video-guides/[id]/loading.tsx @@ -0,0 +1,22 @@ +import { Spinner } from "@/components/ui"; + +export default function Loading() { + return ( +
+ +
+ ); +} diff --git a/src/app/[locale]/(core)/video-guides/[id]/page.tsx b/src/app/[locale]/(core)/video-guides/[id]/page.tsx new file mode 100644 index 0000000..dfc43ea --- /dev/null +++ b/src/app/[locale]/(core)/video-guides/[id]/page.tsx @@ -0,0 +1,38 @@ +import { notFound } from "next/navigation"; + +import { VideoGuideView } from "@/components/domains/video-guides"; +import { DashboardData, DashboardSchema } from "@/entities/dashboard/types"; +import { http } from "@/shared/api/httpClient"; +import { API_ROUTES } from "@/shared/constants/api-routes"; + +// Force dynamic to always get fresh data +export const dynamic = "force-dynamic"; + +export default async function VideoGuidePage({ + params, +}: { + params: Promise<{ id: string }>; +}) { + const { id } = await params; + + // Get fresh dashboard data without cache + const dashboard = await http.get(API_ROUTES.dashboard(), { + cache: "no-store", + schema: DashboardSchema, + }); + + const videoGuide = dashboard.videoGuides?.find(v => v.id === id); + + if (!videoGuide || !videoGuide.isPurchased || !videoGuide.videoLink) { + notFound(); + } + + return ( + + ); +} diff --git a/src/components/domains/additional-purchases/AddConsultantButton/AddConsultantButton.tsx b/src/components/domains/additional-purchases/AddConsultantButton/AddConsultantButton.tsx index c73c6f5..88b1459 100644 --- a/src/components/domains/additional-purchases/AddConsultantButton/AddConsultantButton.tsx +++ b/src/components/domains/additional-purchases/AddConsultantButton/AddConsultantButton.tsx @@ -1,5 +1,6 @@ "use client"; +import { useState } from "react"; import { useTranslations } from "next-intl"; import { Button, Spinner, Typography } from "@/components/ui"; @@ -17,12 +18,22 @@ export default function AddConsultantButton() { const { addToast } = useToast(); const { navigation } = useMultiPageNavigationContext(); const data = navigation.currentItem; + const [isNavigating, setIsNavigating] = useState(false); const product = data?.variants?.[0]; const { handleSingleCheckout, isLoading } = useSingleCheckout({ - onSuccess: () => { - navigation.goToNext(); + onSuccess: async () => { + // Устанавливаем флаг навигации чтобы заблокировать повторные нажатия + setIsNavigating(true); + + // Переходим на следующую страницу или на главную + if (navigation.hasNext) { + await navigation.goToNext(); + } else { + // Если это последний экран - переходим на дашборд + window.location.href = ROUTES.home(); + } }, onError: _error => { addToast({ @@ -57,14 +68,17 @@ export default function AddConsultantButton() { navigation.goToNext(); }; + // Блокируем кнопку во время загрузки ИЛИ навигации + const isButtonDisabled = isLoading || isNavigating || !product; + return (
- + {isPurchased && ( + + )}
{/* Bottom Section */}
-
- {duration} - {!isPurchased && ( -
- - {discount}% OFF {getFormattedPrice(oldPrice, currency)} - + {!isPurchased ? ( + <> +
+ {duration} +
+ + {discount}% OFF {getFormattedPrice(oldPrice, currency)} + +
- )} -
- {!isPurchased && ( - + + + ) : ( + {duration} )}
diff --git a/src/components/domains/dashboard/sections/VideoGuidesSection/VideoGuidesSection.module.scss b/src/components/domains/dashboard/sections/VideoGuidesSection/VideoGuidesSection.module.scss index d00ff19..1cd8a02 100644 --- a/src/components/domains/dashboard/sections/VideoGuidesSection/VideoGuidesSection.module.scss +++ b/src/components/domains/dashboard/sections/VideoGuidesSection/VideoGuidesSection.module.scss @@ -9,4 +9,12 @@ .grid { padding-right: 16px; + grid-auto-rows: 1fr; + + a, > div { + text-decoration: none; + color: inherit; + display: block; + height: 100%; + } } diff --git a/src/components/domains/dashboard/sections/VideoGuidesSection/VideoGuidesSection.tsx b/src/components/domains/dashboard/sections/VideoGuidesSection/VideoGuidesSection.tsx index a4426b1..5fad724 100644 --- a/src/components/domains/dashboard/sections/VideoGuidesSection/VideoGuidesSection.tsx +++ b/src/components/domains/dashboard/sections/VideoGuidesSection/VideoGuidesSection.tsx @@ -1,5 +1,10 @@ +"use client"; + +import Link from "next/link"; + import { Grid, Section } from "@/components/ui"; import { VideoGuide } from "@/entities/dashboard/types"; +import { useVideoGuidePurchase } from "@/hooks/video-guides/useVideoGuidePurchase"; import { VideoGuideCard } from "../../cards"; @@ -9,6 +14,50 @@ interface VideoGuidesSectionProps { videoGuides: VideoGuide[]; } +function VideoGuideCardWrapper({ videoGuide }: { videoGuide: VideoGuide }) { + const { handlePurchase, isCheckoutLoading, isProcessingPurchase } = useVideoGuidePurchase({ + videoGuideId: videoGuide.id, + productId: videoGuide.id, + productKey: videoGuide.key, + }); + + // Для купленных видео - ссылка на страницу просмотра + const href = videoGuide.isPurchased && videoGuide.videoLink + ? `/video-guides/${videoGuide.id}` + : '#'; + + const isClickable = videoGuide.isPurchased && videoGuide.videoLink; + + const cardElement = ( + + ); + + if (isClickable) { + return ( + + {cardElement} + + ); + } + + return
{cardElement}
; +} + export default function VideoGuidesSection({ videoGuides }: VideoGuidesSectionProps) { if (!videoGuides || videoGuides.length === 0) { return null; @@ -20,16 +69,9 @@ export default function VideoGuidesSection({ videoGuides }: VideoGuidesSectionPr
{videoGuides.map(videoGuide => ( - ))} diff --git a/src/components/domains/portraits/PortraitView/PortraitView.module.scss b/src/components/domains/portraits/PortraitView/PortraitView.module.scss index 677f344..e36f756 100644 --- a/src/components/domains/portraits/PortraitView/PortraitView.module.scss +++ b/src/components/domains/portraits/PortraitView/PortraitView.module.scss @@ -7,6 +7,8 @@ display: flex; flex-direction: column; overflow: hidden; + z-index: 1000; + background: var(--background); } .header { diff --git a/src/components/domains/video-guides/VideoGuideView/VideoGuideView.module.scss b/src/components/domains/video-guides/VideoGuideView/VideoGuideView.module.scss new file mode 100644 index 0000000..943df36 --- /dev/null +++ b/src/components/domains/video-guides/VideoGuideView/VideoGuideView.module.scss @@ -0,0 +1,96 @@ +.container { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + display: flex; + flex-direction: column; + overflow: hidden; + z-index: 1000; + background: var(--background); +} + +.header { + display: flex; + align-items: center; + gap: 12px; + padding: 16px; + background: var(--background); + position: relative; + flex-shrink: 0; +} + +.backButton { + width: 40px; + height: 40px; + border-radius: 50%; + background: #f5f5f5; + display: flex; + align-items: center; + justify-content: center; + border: none; + cursor: pointer; + flex-shrink: 0; + transition: background 0.2s ease; + + &:hover { + background: #e0e0e0; + } + + &:active { + background: #d0d0d0; + } +} + +.title { + flex: 1; + text-align: center; + padding-right: 40px; // Compensate for back button width +} + +.contentWrapper { + flex: 1; + display: flex; + flex-direction: column; + align-items: center; + padding: 24px; + overflow-y: auto; + gap: 32px; +} + +.videoContainer { + position: relative; + width: 100%; + max-width: 800px; + aspect-ratio: 16 / 9; + border-radius: 24px; + overflow: hidden; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08); +} + +.videoInner { + position: relative; + width: 100%; + height: 100%; +} + +.video { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + border: none; +} + +.descriptionWrapper { + width: 100%; + max-width: 800px; + padding: 0; +} + +.description { + color: #646464; + line-height: 1.6; +} diff --git a/src/components/domains/video-guides/VideoGuideView/VideoGuideView.tsx b/src/components/domains/video-guides/VideoGuideView/VideoGuideView.tsx new file mode 100644 index 0000000..b858029 --- /dev/null +++ b/src/components/domains/video-guides/VideoGuideView/VideoGuideView.tsx @@ -0,0 +1,76 @@ +"use client"; + +import { useRouter } from "next/navigation"; + +import { Icon, IconName, Typography } from "@/components/ui"; + +import styles from "./VideoGuideView.module.scss"; + +interface VideoGuideViewProps { + id: string; + name: string; + description: string; + videoLink: string; +} + +export default function VideoGuideView({ name, description, videoLink }: VideoGuideViewProps) { + const router = useRouter(); + + // Extract video ID from various YouTube URL formats + const getYouTubeVideoId = (url: string): string | null => { + const patterns = [ + /(?:youtube\.com\/watch\?v=|youtu\.be\/|youtube\.com\/embed\/)([^&\n?#]+)/, + /^([a-zA-Z0-9_-]{11})$/ // Direct video ID + ]; + + for (const pattern of patterns) { + const match = url.match(pattern); + if (match) return match[1]; + } + return null; + }; + + const videoId = getYouTubeVideoId(videoLink); + const embedUrl = videoId + ? `https://www.youtube.com/embed/${videoId}?rel=0&modestbranding=1` + : videoLink; + + return ( +
+ {/* Header with back button and title */} +
+ + + {name} + +
+ + {/* Video and Description */} +
+ {/* Video Player */} +
+
+