From 1adac2836bc4c0d3e409ea728bf16f0d724a7dcd Mon Sep 17 00:00:00 2001 From: "dev.daminik00" Date: Tue, 28 Oct 2025 05:58:51 +0100 Subject: [PATCH] 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 */} +
+
+