From 7f9f1af39e840e8690f31b88bfcb8ca564b4f1ba Mon Sep 17 00:00:00 2001 From: gofnnp Date: Tue, 28 Oct 2025 21:29:34 +0400 Subject: [PATCH] video-progress-edits ap edits --- messages/de.json | 2 +- messages/es.json | 2 +- .../ap/[pageType]/layout.tsx | 7 +- .../em/(soulmate)/s/v1/landing/page.tsx | 5 +- .../AddConsultantButton.tsx | 13 +- .../AddConsultantPage/AddConsultantPage.tsx | 20 +-- .../AddGuidesButton/AddGuidesButton.tsx | 11 +- .../AddGuidesPage/AddGuidesPage.tsx | 20 +-- .../Progress/Progress.module.scss | 74 ++------ .../Progress/Progress.tsx | 164 ++++++------------ .../ProgressLayout/ProgressLayout.tsx | 27 +++ .../VideoGuidesButton/VideoGuidesButton.tsx | 11 +- .../VideoGuidesOffer/VideoGuidesOffer.tsx | 4 +- .../VideoGuidesOffers/VideoGuidesOffers.tsx | 3 +- .../VideoGuidesPage/VideoGuidesPage.tsx | 20 +-- .../domains/additional-purchases/index.ts | 1 + .../chat/MessageInput/MessageInput.tsx | 5 +- .../PortraitCard/PortraitCard.module.scss | 14 +- .../cards/PortraitCard/PortraitCard.tsx | 79 +++++++-- .../VideoGuideCard/VideoGuideCard.module.scss | 46 ++--- .../cards/VideoGuideCard/VideoGuideCard.tsx | 121 ++++++++++--- .../VideoGuidesSection.module.scss | 5 +- .../VideoGuidesSection/VideoGuidesSection.tsx | 27 +-- .../DetailedPortraitCard.tsx | 4 +- .../GuaranteedSecurityPayments.tsx | 4 +- .../IndividualAdviceCard.tsx | 4 +- .../LandingButtonWrapper.tsx | 4 +- .../soulmate/v1/Payments/Payments.tsx | 4 +- .../TrialIntervalOffer/TrialIntervalOffer.tsx | 4 +- .../portraits/PortraitView/PortraitView.tsx | 110 +++++++++--- .../VideoGuideView/VideoGuideView.tsx | 15 +- .../NavigationBar/NavigationBar.module.scss | 6 +- src/components/ui/Icon/icons/AlertCircle.tsx | 22 +-- .../ui/MarkdownText/MarkdownText.tsx | 8 +- src/components/ui/index.ts | 5 +- src/entities/dashboard/loaders.ts | 3 +- src/hooks/chats/useChatSocket.ts | 4 +- src/hooks/timer/useTimer.ts | 6 +- .../video-guides/useVideoGuidePurchase.ts | 63 +++---- 39 files changed, 516 insertions(+), 431 deletions(-) create mode 100644 src/components/domains/additional-purchases/ProgressLayout/ProgressLayout.tsx diff --git a/messages/de.json b/messages/de.json index 49c3b33..755bf5d 100644 --- a/messages/de.json +++ b/messages/de.json @@ -418,4 +418,4 @@ }, "Soulmate": {} } -} \ No newline at end of file +} diff --git a/messages/es.json b/messages/es.json index 614f564..f57b0cc 100644 --- a/messages/es.json +++ b/messages/es.json @@ -418,4 +418,4 @@ }, "Soulmate": {} } -} \ No newline at end of file +} diff --git a/src/app/[locale]/(additional-purchases)/ap/[pageType]/layout.tsx b/src/app/[locale]/(additional-purchases)/ap/[pageType]/layout.tsx index 8a311ed..04d878c 100644 --- a/src/app/[locale]/(additional-purchases)/ap/[pageType]/layout.tsx +++ b/src/app/[locale]/(additional-purchases)/ap/[pageType]/layout.tsx @@ -1,6 +1,9 @@ import { redirect } from "next/navigation"; -import { MultiPageNavigationProvider } from "@/components/domains/additional-purchases"; +import { + MultiPageNavigationProvider, + ProgressLayout, +} from "@/components/domains/additional-purchases"; import { loadFunnelPaymentById } from "@/entities/session/funnel/loaders"; import { ROUTES } from "@/shared/constants/client-routes"; import { ELocalesPlacement } from "@/types"; @@ -30,7 +33,7 @@ export default async function MultiPageLayout({ return ( - {children} + {children} ); } diff --git a/src/app/[locale]/(email-marketing)/em/(soulmate)/s/v1/landing/page.tsx b/src/app/[locale]/(email-marketing)/em/(soulmate)/s/v1/landing/page.tsx index 09385af..83efe4c 100644 --- a/src/app/[locale]/(email-marketing)/em/(soulmate)/s/v1/landing/page.tsx +++ b/src/app/[locale]/(email-marketing)/em/(soulmate)/s/v1/landing/page.tsx @@ -34,7 +34,10 @@ function getUsernameFromEmail(email: string): string { export default async function EmailMarketingSoulmateV1Landing() { const [payment, user] = await Promise.all([ - loadFunnelPaymentById(payload, "main") as Promise, + loadFunnelPaymentById( + payload, + "main" + ) as Promise, loadUser(), ]); diff --git a/src/components/domains/additional-purchases/AddConsultantButton/AddConsultantButton.tsx b/src/components/domains/additional-purchases/AddConsultantButton/AddConsultantButton.tsx index 9f799db..be206bd 100644 --- a/src/components/domains/additional-purchases/AddConsultantButton/AddConsultantButton.tsx +++ b/src/components/domains/additional-purchases/AddConsultantButton/AddConsultantButton.tsx @@ -24,16 +24,8 @@ export default function AddConsultantButton() { const { handleSingleCheckout, isLoading } = useSingleCheckout({ onSuccess: async () => { - // Устанавливаем флаг навигации чтобы заблокировать повторные нажатия setIsNavigating(true); - - // Переходим на следующую страницу или на главную - if (navigation.hasNext) { - await navigation.goToNext(); - } else { - // Если это последний экран - переходим на дашборд - window.location.href = ROUTES.home(); - } + await navigation.goToNext(); }, onError: _error => { addToast({ @@ -68,7 +60,6 @@ export default function AddConsultantButton() { navigation.goToNext(); }; - // Блокируем кнопку во время загрузки ИЛИ навигации const isButtonDisabled = isLoading || isNavigating || !product; return ( @@ -78,7 +69,7 @@ export default function AddConsultantButton() { onClick={handleGetConsultation} disabled={isButtonDisabled} > - {(isLoading || isNavigating) ? ( + {isLoading || isNavigating ? ( ) : ( diff --git a/src/components/domains/additional-purchases/AddConsultantPage/AddConsultantPage.tsx b/src/components/domains/additional-purchases/AddConsultantPage/AddConsultantPage.tsx index bb4b65c..aa259d0 100644 --- a/src/components/domains/additional-purchases/AddConsultantPage/AddConsultantPage.tsx +++ b/src/components/domains/additional-purchases/AddConsultantPage/AddConsultantPage.tsx @@ -1,32 +1,18 @@ -"use client"; - -import { useTranslations } from "next-intl"; +import { getTranslations } from "next-intl/server"; import { AddConsultantButton, ConsultationTable, - Progress, - useMultiPageNavigationContext, } from "@/components/domains/additional-purchases"; import { Card, Typography } from "@/components/ui"; 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 || ""; - }); +export default async function AddConsultantPage() { + const t = await getTranslations("AdditionalPurchases.add-consultant"); return ( <> - {t("title")} diff --git a/src/components/domains/additional-purchases/AddGuidesButton/AddGuidesButton.tsx b/src/components/domains/additional-purchases/AddGuidesButton/AddGuidesButton.tsx index ac0693f..2213171 100644 --- a/src/components/domains/additional-purchases/AddGuidesButton/AddGuidesButton.tsx +++ b/src/components/domains/additional-purchases/AddGuidesButton/AddGuidesButton.tsx @@ -23,16 +23,8 @@ export default function AddGuidesButton() { const { handleSingleCheckout, isLoading } = useSingleCheckout({ onSuccess: async () => { - // Устанавливаем флаг навигации чтобы заблокировать повторные нажатия setIsNavigating(true); - - // Переходим на следующую страницу или на главную - if (navigation.hasNext) { - await navigation.goToNext(); - } else { - // Если это последний экран - переходим на дашборд - window.location.href = ROUTES.home(); - } + await navigation.goToNext(); }, onError: _error => { addToast({ @@ -69,7 +61,6 @@ export default function AddGuidesButton() { const isSkipOffer = selectedProduct?.id === "main_skip_offer"; - // Блокируем кнопку во время загрузки ИЛИ навигации const isButtonDisabled = isLoading || isNavigating; return ( diff --git a/src/components/domains/additional-purchases/AddGuidesPage/AddGuidesPage.tsx b/src/components/domains/additional-purchases/AddGuidesPage/AddGuidesPage.tsx index cec2d27..1e8e90a 100644 --- a/src/components/domains/additional-purchases/AddGuidesPage/AddGuidesPage.tsx +++ b/src/components/domains/additional-purchases/AddGuidesPage/AddGuidesPage.tsx @@ -1,35 +1,21 @@ -"use client"; - import { Suspense } from "react"; -import { useTranslations } from "next-intl"; +import { getTranslations } from "next-intl/server"; import { AddGuidesButton, Offers, OffersSkeleton, ProductSelectionProvider, - Progress, - useMultiPageNavigationContext, } from "@/components/domains/additional-purchases"; import { Typography } from "@/components/ui"; 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 || ""; - }); +export default async function AddGuidesPage() { + const t = await getTranslations("AdditionalPurchases.add-guides"); return ( - {t("title")} diff --git a/src/components/domains/additional-purchases/Progress/Progress.module.scss b/src/components/domains/additional-purchases/Progress/Progress.module.scss index 9f9026e..b2ee372 100644 --- a/src/components/domains/additional-purchases/Progress/Progress.module.scss +++ b/src/components/domains/additional-purchases/Progress/Progress.module.scss @@ -1,70 +1,35 @@ -.stickyWrapper { - position: sticky; - top: 0; - margin-top: -24px; - margin-left: -24px; - margin-right: -24px; - margin-bottom: 6px; - padding-left: 24px; - padding-right: 24px; - padding-top: 24px; - padding-bottom: 20px; - z-index: 100; - transition: background 0.2s ease, box-shadow 0.2s ease; - - &.scrolled { - background: rgba(255, 255, 255, 0.85); - backdrop-filter: blur(12px); - -webkit-backdrop-filter: blur(12px); - box-shadow: 0px 2px 8px 0px rgba(0, 0, 0, 0.04); - - &::after { - opacity: 1; - } - } - - &::after { - content: ""; - position: absolute; - bottom: 0; - left: 0; - right: 0; - height: 1px; - background: linear-gradient( - 90deg, - rgba(226, 232, 240, 0) 0%, - rgba(226, 232, 240, 0.8) 20%, - rgba(226, 232, 240, 0.8) 80%, - rgba(226, 232, 240, 0) 100% - ); - opacity: 0; - transition: opacity 0.2s ease; - } -} - .container { - position: relative; width: 100%; display: flex; flex-direction: row; + align-items: flex-start; + justify-content: space-between; + position: sticky; + top: 24px; + z-index: 7777; + + & > .blurGradient { + background: rgb(247 247 247 / 42%); + rotate: 180deg; + left: -24px; + right: -24px; + } & > * { - z-index: 1; + z-index: 8888; } & > .item { - position: absolute; - top: 0; display: flex; flex-direction: column; align-items: center; gap: 12px; - width: 80px; + max-width: 80px; & > .marker { width: 32px; height: 32px; - border: 2px solid #e2e8f0; + border: 1px solid #e2e8f0; border-radius: 50%; background: f8fafc; display: flex; @@ -85,9 +50,6 @@ font-weight: 500; line-height: 16px; color: #9ca3af; - word-wrap: break-word; - overflow-wrap: break-word; - hyphens: auto; } &.active { @@ -124,7 +86,7 @@ & > .marker { box-shadow: 0px 0px 0px 0px #3b82f626; background-color: #2866ed; - border: 2px solid #2866ed; + border: none; & > .number { display: none; @@ -139,8 +101,8 @@ .connector { position: absolute; - top: 16px; + top: 15px; height: 2px; - z-index: 0; + z-index: 7777; } } diff --git a/src/components/domains/additional-purchases/Progress/Progress.tsx b/src/components/domains/additional-purchases/Progress/Progress.tsx index 2ac81e1..eecb8d1 100644 --- a/src/components/domains/additional-purchases/Progress/Progress.tsx +++ b/src/components/domains/additional-purchases/Progress/Progress.tsx @@ -1,10 +1,10 @@ "use client"; -import { useEffect, useRef, useState } from "react"; import { useTranslations } from "next-intl"; import clsx from "clsx"; import { Icon, IconName, Typography } from "@/components/ui"; +import { BlurComponent } from "@/components/widgets"; import { useDynamicSize } from "@/hooks/DOM/useDynamicSize"; import styles from "./Progress.module.scss"; @@ -16,133 +16,79 @@ interface IProgressProps { export default function Progress({ items, activeItemIndex }: IProgressProps) { const t = useTranslations("AdditionalPurchases.Progress"); - const { width: containerWidth, elementRef } = useDynamicSize({ - defaultWidth: 327, - }); - const wrapperRef = useRef(null); - const [isScrolled, setIsScrolled] = useState(false); - const [containerHeight, setContainerHeight] = useState(0); - // Всегда добавляем финальный пункт в конец const finalStep = t("final_step"); const allItems = [...items, finalStep]; - // Фиксированные отступы от краев (центр кружочка = 50px от края) - const edgeOffset = 50; // 50px от края до центра кружочка - const totalItems = allItems.length; + const { width: containerWidth, elementRef } = useDynamicSize({ + defaultWidth: 327, + }); - // Рассчитываем позицию каждого элемента с равными отступами - const calculateItemPosition = (index: number) => { - if (totalItems === 1) return 50; // Центрируем если один элемент + const firstChild = elementRef.current?.childNodes[0] as HTMLElement; - // Распределяем элементы равномерно между краями - const availableWidth = containerWidth - (edgeOffset * 2); - const spacing = availableWidth / (totalItems - 1); - return edgeOffset + (spacing * index); - }; + const lastChild = elementRef.current?.childNodes[ + allItems.length - 1 + ] as HTMLElement; - // Динамическое определение высоты контейнера - useEffect(() => { - if (elementRef.current) { - // Находим все элементы прогресс бара - const items = elementRef.current.querySelectorAll(`.${styles.item}`); - let maxHeight = 0; + const leftIndent = + ((firstChild?.getBoundingClientRect().width || 100) - 32) / 2; - items.forEach((item) => { - const height = (item as HTMLElement).offsetHeight; - if (height > maxHeight) { - maxHeight = height; - } - }); - - setContainerHeight(maxHeight); - } - }, [allItems, containerWidth]); - - // Отслеживаем скролл для появления фона - useEffect(() => { - const handleScroll = () => { - if (wrapperRef.current) { - const rect = wrapperRef.current.getBoundingClientRect(); - // Показываем фон только когда элемент прокручен вверх (скроллится) - // При начальной загрузке top будет около 0, но мы проверяем window.scrollY - const hasScrolled = window.scrollY > 0 && rect.top <= 1; - setIsScrolled(hasScrolled); - } - }; - - window.addEventListener("scroll", handleScroll, { passive: true }); - // При монтировании проверяем - если страница уже прокручена - handleScroll(); - - return () => window.removeEventListener("scroll", handleScroll); - }, []); + const rightIndent = + ((lastChild?.getBoundingClientRect().width || 76) - 32) / 2; return ( -
-
0 ? `${containerHeight}px` : undefined, - }} - > - {allItems.map((item, index) => { - const itemPosition = calculateItemPosition(index); - return ( -
index && styles.done + {allItems.map((item, index) => ( +
index && styles.done + )} + > +
+ {activeItemIndex > index && styles.done && ( + )} - style={{ - left: `${itemPosition}px`, - transform: 'translateX(-50%)', // Центрируем относительно позиции - }} - > -
- {activeItemIndex > index && styles.done && ( - - )} - - {index + 1} - -
- - {item} + + {index + 1}
- ); - })} + + {item} + +
+ ))}
0 - ? `linear-gradient( - 90deg, - #2866ed 0%, - #2866ed ${((activeItemIndex) / (totalItems - 1)) * 100}%, - #E2E8F0 ${((activeItemIndex) / (totalItems - 1)) * 100}%, - #E2E8F0 100% - )` + width: containerWidth - leftIndent - rightIndent, + left: leftIndent, + background: activeItemIndex + ? ` + linear-gradient( + 90deg, + #2866ed 0%, + #2866ed ${((activeItemIndex - 1) / items.length) * 100}%, + #c4d9fc ${(activeItemIndex / items.length) * containerWidth + 16}px, + #E2E8F0 100%) + ` : "#E2E8F0", }} /> -
-
+ ); } diff --git a/src/components/domains/additional-purchases/ProgressLayout/ProgressLayout.tsx b/src/components/domains/additional-purchases/ProgressLayout/ProgressLayout.tsx new file mode 100644 index 0000000..70021e8 --- /dev/null +++ b/src/components/domains/additional-purchases/ProgressLayout/ProgressLayout.tsx @@ -0,0 +1,27 @@ +"use client"; + +import { IFunnelPaymentPlacement } from "@/entities/session/funnel/types"; + +import { Progress, useMultiPageNavigationContext } from ".."; + +interface IProgressLayoutProps { + children: React.ReactNode; +} + +export default function ProgressLayout({ children }: IProgressLayoutProps) { + const { navigation } = useMultiPageNavigationContext(); + + const progressItems = navigation.data.map((item: IFunnelPaymentPlacement) => { + return item.title || item.type || ""; + }); + + return ( + <> + + {children} + + ); +} diff --git a/src/components/domains/additional-purchases/VideoGuidesButton/VideoGuidesButton.tsx b/src/components/domains/additional-purchases/VideoGuidesButton/VideoGuidesButton.tsx index d6b9a5a..febc548 100644 --- a/src/components/domains/additional-purchases/VideoGuidesButton/VideoGuidesButton.tsx +++ b/src/components/domains/additional-purchases/VideoGuidesButton/VideoGuidesButton.tsx @@ -23,16 +23,8 @@ export default function VideoGuidesButton() { const { handleSingleCheckout, isLoading } = useSingleCheckout({ onSuccess: async () => { - // Устанавливаем флаг навигации чтобы заблокировать повторные нажатия setIsNavigating(true); - - // Переходим на следующую страницу или на главную - if (navigation.hasNext) { - await navigation.goToNext(); - } else { - // Если это последний экран - переходим на дашборд - window.location.href = ROUTES.home(); - } + await navigation.goToNext(); }, onError: _error => { addToast({ @@ -67,7 +59,6 @@ export default function VideoGuidesButton() { navigation.goToNext(); }; - // Блокируем кнопку во время загрузки ИЛИ навигации const isButtonDisabled = isLoading || isNavigating; return ( diff --git a/src/components/domains/additional-purchases/VideoGuidesOffer/VideoGuidesOffer.tsx b/src/components/domains/additional-purchases/VideoGuidesOffer/VideoGuidesOffer.tsx index 9ff7d27..57481c7 100644 --- a/src/components/domains/additional-purchases/VideoGuidesOffer/VideoGuidesOffer.tsx +++ b/src/components/domains/additional-purchases/VideoGuidesOffer/VideoGuidesOffer.tsx @@ -86,9 +86,7 @@ export default function VideoGuidesOffer(props: VideoGuidesOfferProps) { {!isFirstOffer && ( - - {discount}% OFF - + {discount}% OFF )}
diff --git a/src/components/domains/additional-purchases/VideoGuidesOffers/VideoGuidesOffers.tsx b/src/components/domains/additional-purchases/VideoGuidesOffers/VideoGuidesOffers.tsx index 7bc3d3c..8a25b33 100644 --- a/src/components/domains/additional-purchases/VideoGuidesOffers/VideoGuidesOffers.tsx +++ b/src/components/domains/additional-purchases/VideoGuidesOffers/VideoGuidesOffers.tsx @@ -23,7 +23,8 @@ export default function VideoGuidesOffers() { const serverOffers = data?.variants ?? []; return serverOffers.map((offer: IFunnelPaymentVariant, index: number) => { // Первый товар имеет скидку 50%, остальные - 45% - const discountPercent = index === 0 ? FIRST_PRODUCT_DISCOUNT : OTHER_PRODUCTS_DISCOUNT; + 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 { diff --git a/src/components/domains/additional-purchases/VideoGuidesPage/VideoGuidesPage.tsx b/src/components/domains/additional-purchases/VideoGuidesPage/VideoGuidesPage.tsx index 14201e2..b3b9046 100644 --- a/src/components/domains/additional-purchases/VideoGuidesPage/VideoGuidesPage.tsx +++ b/src/components/domains/additional-purchases/VideoGuidesPage/VideoGuidesPage.tsx @@ -1,13 +1,9 @@ -"use client"; - import { Suspense } from "react"; -import { useTranslations } from "next-intl"; +import { getTranslations } from "next-intl/server"; import { AdditionalPurchaseBanner, ProductSelectionProvider, - Progress, - useMultiPageNavigationContext, VideoGuidesButton, VideoGuidesOffers, VideoGuidesOffersSkeleton, @@ -16,21 +12,11 @@ import { Typography } from "@/components/ui"; 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 || ""; - }); +export default async function VideoGuidesPage() { + const t = await getTranslations("AdditionalPurchases.video-guides"); return ( - {t("title")} diff --git a/src/components/domains/additional-purchases/index.ts b/src/components/domains/additional-purchases/index.ts index 2bd3916..5b0b597 100644 --- a/src/components/domains/additional-purchases/index.ts +++ b/src/components/domains/additional-purchases/index.ts @@ -16,6 +16,7 @@ export { useProductSelection, } from "./ProductSelectionProvider"; export { default as Progress } from "./Progress/Progress"; +export { default as ProgressLayout } from "./ProgressLayout/ProgressLayout"; export { default as VideoGuidesBanner } from "./VideoGuidesBanner/VideoGuidesBanner"; export { default as VideoGuidesButton } from "./VideoGuidesButton/VideoGuidesButton"; export { default as VideoGuidesOffer } from "./VideoGuidesOffer/VideoGuidesOffer"; diff --git a/src/components/domains/chat/MessageInput/MessageInput.tsx b/src/components/domains/chat/MessageInput/MessageInput.tsx index 621742f..435e101 100644 --- a/src/components/domains/chat/MessageInput/MessageInput.tsx +++ b/src/components/domains/chat/MessageInput/MessageInput.tsx @@ -82,7 +82,10 @@ export default function MessageInput({ aria-label="Send" className={styles.sendButton} > - +
diff --git a/src/components/domains/dashboard/cards/PortraitCard/PortraitCard.module.scss b/src/components/domains/dashboard/cards/PortraitCard/PortraitCard.module.scss index 79be070..9b37602 100644 --- a/src/components/domains/dashboard/cards/PortraitCard/PortraitCard.module.scss +++ b/src/components/domains/dashboard/cards/PortraitCard/PortraitCard.module.scss @@ -8,7 +8,9 @@ display: flex; flex-direction: column; cursor: pointer; - transition: transform 0.2s ease, box-shadow 0.2s ease; + transition: + transform 0.2s ease, + box-shadow 0.2s ease; &:hover { transform: translateY(-4px); @@ -102,14 +104,14 @@ } .statusDone { - color: #16A34A; + color: #16a34a; .statusText { - color: #16A34A; + color: #16a34a; } .checkmark { - color: #16A34A; + color: #16a34a; } } @@ -158,7 +160,9 @@ align-items: center; justify-content: center; cursor: pointer; - transition: background 0.2s ease, transform 0.1s ease; + transition: + background 0.2s ease, + transform 0.1s ease; flex-shrink: 0; svg { diff --git a/src/components/domains/dashboard/cards/PortraitCard/PortraitCard.tsx b/src/components/domains/dashboard/cards/PortraitCard/PortraitCard.tsx index cd7c3cc..ef6a83d 100644 --- a/src/components/domains/dashboard/cards/PortraitCard/PortraitCard.tsx +++ b/src/components/domains/dashboard/cards/PortraitCard/PortraitCard.tsx @@ -12,13 +12,22 @@ import styles from "./PortraitCard.module.scss"; type PortraitCardProps = PartnerPortrait; const HeartCheckIcon = () => ( - + - + - + @@ -35,21 +44,39 @@ const getStatusConfig = (status: PartnerPortrait["status"]) => { }; case "processing": return { - icon: , + icon: ( + + ), text: "Processing...", showCheckmark: false, className: styles.statusProcessing, }; case "queued": return { - icon: , + icon: ( + + ), text: "In Queue", showCheckmark: false, className: styles.statusQueued, }; case "error": return { - icon: , + icon: ( + + ), text: "Error", showCheckmark: false, className: styles.statusError, @@ -66,7 +93,10 @@ export default function PortraitCard({ const router = useRouter(); // Use polling hook to update status in real-time - const { status, imageUrl: polledImageUrl } = useGenerationStatus(_id, initialStatus); + const { status, imageUrl: polledImageUrl } = useGenerationStatus( + _id, + initialStatus + ); // Use polled imageUrl if available, otherwise use initial const imageUrl = polledImageUrl || initialImageUrl; @@ -84,7 +114,7 @@ export default function PortraitCard({
{imageUrl ? ( @@ -98,17 +128,32 @@ export default function PortraitCard({ /> ) : (
- +
)}
- + {title} - + Finding the One Guide
@@ -120,13 +165,21 @@ export default function PortraitCard({ {statusConfig.text} {statusConfig.showCheckmark && ( - + )}
{status === "done" && ( )} diff --git a/src/components/domains/dashboard/cards/VideoGuideCard/VideoGuideCard.module.scss b/src/components/domains/dashboard/cards/VideoGuideCard/VideoGuideCard.module.scss index ea30e5b..e5ee6e0 100644 --- a/src/components/domains/dashboard/cards/VideoGuideCard/VideoGuideCard.module.scss +++ b/src/components/domains/dashboard/cards/VideoGuideCard/VideoGuideCard.module.scss @@ -6,18 +6,24 @@ flex-direction: column; align-items: flex-start; border-radius: 24px; - border: 0 solid #E5E7EB; + 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); + box-shadow: + 0 4px 6px 0 rgba(0, 0, 0, 0.1), + 0 10px 15px 0 rgba(0, 0, 0, 0.1); overflow: hidden; padding: 0; - transition: transform 0.2s ease, box-shadow 0.2s ease; + transition: + transform 0.2s ease, + box-shadow 0.2s ease; position: relative; // Hover effect only for purchased cards (wrapped in Link) :global(a):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); + box-shadow: + 0 6px 10px 0 rgba(0, 0, 0, 0.12), + 0 12px 18px 0 rgba(0, 0, 0, 0.12); } &.processing { @@ -48,12 +54,12 @@ align-items: center; gap: 10px; align-self: stretch; - border: 0 solid #E5E7EB; + border: 0 solid #e5e7eb; position: relative; overflow: hidden; &::before { - content: ''; + content: ""; position: absolute; inset: 0; background: lightgray 50% / cover no-repeat; @@ -94,7 +100,7 @@ justify-content: space-between; gap: 24px; align-self: stretch; - background: #FFF; + background: #fff; flex: 1; .purchased & { @@ -125,8 +131,8 @@ justify-content: center; align-items: center; border-radius: 9999px; - border: 0 solid #E5E7EB; - background: #F5F5F7; + border: 0 solid #e5e7eb; + background: #f5f5f7; cursor: pointer; transition: opacity 0.2s ease; @@ -144,7 +150,7 @@ .title { align-self: stretch; - color: #1D1D1F; + color: #1d1d1f; font-family: Inter, sans-serif; font-size: 20px; font-style: normal; @@ -155,7 +161,7 @@ .subtitle { align-self: stretch; - color: #6B7280; + color: #6b7280; font-family: Inter, sans-serif; font-size: 16px; font-style: normal; @@ -188,7 +194,7 @@ flex-direction: column; justify-content: center; align-self: stretch; - color: #6B7280; + color: #6b7280; font-family: Inter, sans-serif; font-size: 16px; font-style: normal; @@ -200,7 +206,7 @@ display: flex; justify-content: flex-end; align-self: stretch; - color: #6B7280; + color: #6b7280; font-family: Inter, sans-serif; font-size: 16px; font-style: normal; @@ -216,12 +222,12 @@ gap: 10px; align-self: stretch; border-radius: 9999px; - border: 0 solid #E5E7EB; - background: rgba(255, 107, 107, 0.10); + border: 0 solid #e5e7eb; + background: rgba(255, 107, 107, 0.1); } .discountText { - color: #FF6B6B; + color: #ff6b6b; text-align: center; font-family: Inter, sans-serif; font-size: 12px; @@ -230,7 +236,7 @@ line-height: normal; .oldPrice { - color: #8B8B8B; + color: #8b8b8b; font-family: Inter, sans-serif; font-size: 12px; font-style: normal; @@ -247,13 +253,13 @@ align-items: center; gap: 10px; border-radius: 12px; - border: 0 solid #E5E7EB; - background: #2563EB; + border: 0 solid #e5e7eb; + background: #2563eb; cursor: pointer; transition: opacity 0.2s ease; width: auto; - color: #FFF; + color: #fff; text-align: center; font-family: Inter, sans-serif; font-size: 14px; diff --git a/src/components/domains/dashboard/cards/VideoGuideCard/VideoGuideCard.tsx b/src/components/domains/dashboard/cards/VideoGuideCard/VideoGuideCard.tsx index 1fe2fdb..766a4ee 100644 --- a/src/components/domains/dashboard/cards/VideoGuideCard/VideoGuideCard.tsx +++ b/src/components/domains/dashboard/cards/VideoGuideCard/VideoGuideCard.tsx @@ -1,5 +1,6 @@ "use client"; +import Image from "next/image"; import { useTranslations } from "next-intl"; import clsx from "clsx"; @@ -25,7 +26,20 @@ interface VideoGuideCardProps { } export default function VideoGuideCard(props: VideoGuideCardProps) { - const { name, description, imageUrl, duration, price, oldPrice, discount, isPurchased, isCheckoutLoading, isProcessingPurchase, onPurchaseClick, className } = props; + const { + name, + description, + imageUrl, + duration, + price, + oldPrice, + discount, + isPurchased, + isCheckoutLoading, + isProcessingPurchase, + onPurchaseClick, + className, + } = props; const tCommon = useTranslations("Dashboard.videoGuides"); @@ -43,25 +57,72 @@ export default function VideoGuideCard(props: VideoGuideCardProps) { } return ( - + {/* Image with Play Icon */}
- {name} + {name}
- + - + - - - - - - - - - + + + + + + + + + @@ -82,8 +143,17 @@ export default function VideoGuideCard(props: VideoGuideCardProps) {
{isPurchased && ( )} @@ -94,16 +164,21 @@ export default function VideoGuideCard(props: VideoGuideCardProps) { {!isPurchased ? ( <>
- {duration} + + {duration} +
- {discount}% OFF {getFormattedPrice(oldPrice, currency)} + {discount}% OFF{" "} + + {getFormattedPrice(oldPrice, currency)} +
) : ( - {duration} + + {duration} + )}
diff --git a/src/components/domains/dashboard/sections/VideoGuidesSection/VideoGuidesSection.module.scss b/src/components/domains/dashboard/sections/VideoGuidesSection/VideoGuidesSection.module.scss index 1cd8a02..4bcbd0d 100644 --- a/src/components/domains/dashboard/sections/VideoGuidesSection/VideoGuidesSection.module.scss +++ b/src/components/domains/dashboard/sections/VideoGuidesSection/VideoGuidesSection.module.scss @@ -10,8 +10,9 @@ .grid { padding-right: 16px; grid-auto-rows: 1fr; - - a, > div { + + a, + > div { text-decoration: none; color: inherit; display: block; diff --git a/src/components/domains/dashboard/sections/VideoGuidesSection/VideoGuidesSection.tsx b/src/components/domains/dashboard/sections/VideoGuidesSection/VideoGuidesSection.tsx index d441783..13de83d 100644 --- a/src/components/domains/dashboard/sections/VideoGuidesSection/VideoGuidesSection.tsx +++ b/src/components/domains/dashboard/sections/VideoGuidesSection/VideoGuidesSection.tsx @@ -15,16 +15,18 @@ interface VideoGuidesSectionProps { } function VideoGuideCardWrapper({ videoGuide }: { videoGuide: VideoGuide }) { - const { handlePurchase, isCheckoutLoading, isProcessingPurchase } = useVideoGuidePurchase({ - videoGuideId: videoGuide.id, - productId: videoGuide.id, - productKey: videoGuide.key, - }); + 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 href = + videoGuide.isPurchased && videoGuide.videoLink + ? `/video-guides/${videoGuide.id}` + : "#"; const isClickable = videoGuide.isPurchased && videoGuide.videoLink; @@ -46,10 +48,7 @@ function VideoGuideCardWrapper({ videoGuide }: { videoGuide: VideoGuide }) { if (isClickable) { return ( - + {cardElement} ); @@ -58,7 +57,9 @@ function VideoGuideCardWrapper({ videoGuide }: { videoGuide: VideoGuide }) { return
{cardElement}
; } -export default function VideoGuidesSection({ videoGuides }: VideoGuidesSectionProps) { +export default function VideoGuidesSection({ + videoGuides, +}: VideoGuidesSectionProps) { if (!videoGuides || videoGuides.length === 0) { return null; } diff --git a/src/components/domains/email-marketing/soulmate/v1/DetailedPortraitCard/DetailedPortraitCard.tsx b/src/components/domains/email-marketing/soulmate/v1/DetailedPortraitCard/DetailedPortraitCard.tsx index 1fc6854..325ff38 100644 --- a/src/components/domains/email-marketing/soulmate/v1/DetailedPortraitCard/DetailedPortraitCard.tsx +++ b/src/components/domains/email-marketing/soulmate/v1/DetailedPortraitCard/DetailedPortraitCard.tsx @@ -12,9 +12,7 @@ import styles from "./DetailedPortraitCard.module.scss"; export default function DetailedPortraitCard() { const t = useTranslations( - translatePathEmailMarketingSoulmateV1( - "Landing.what-get.detailed-portrait" - ) + translatePathEmailMarketingSoulmateV1("Landing.what-get.detailed-portrait") ); const { user } = useUser(); const gender = user?.profile?.gender; diff --git a/src/components/domains/email-marketing/soulmate/v1/GuaranteedSecurityPayments/GuaranteedSecurityPayments.tsx b/src/components/domains/email-marketing/soulmate/v1/GuaranteedSecurityPayments/GuaranteedSecurityPayments.tsx index 5c084e2..d1c117b 100644 --- a/src/components/domains/email-marketing/soulmate/v1/GuaranteedSecurityPayments/GuaranteedSecurityPayments.tsx +++ b/src/components/domains/email-marketing/soulmate/v1/GuaranteedSecurityPayments/GuaranteedSecurityPayments.tsx @@ -8,9 +8,7 @@ import { translatePathEmailMarketingSoulmateV1 } from "@/shared/constants/transl import styles from "./GuaranteedSecurityPayments.module.scss"; export default function GuaranteedSecurityPayments() { - const t = useTranslations( - translatePathEmailMarketingSoulmateV1("Landing") - ); + const t = useTranslations(translatePathEmailMarketingSoulmateV1("Landing")); return (
; diff --git a/src/components/domains/email-marketing/soulmate/v1/LandingButtonWrapper/LandingButtonWrapper.tsx b/src/components/domains/email-marketing/soulmate/v1/LandingButtonWrapper/LandingButtonWrapper.tsx index 33ef6ef..d9353a2 100644 --- a/src/components/domains/email-marketing/soulmate/v1/LandingButtonWrapper/LandingButtonWrapper.tsx +++ b/src/components/domains/email-marketing/soulmate/v1/LandingButtonWrapper/LandingButtonWrapper.tsx @@ -12,9 +12,7 @@ import styles from "./LandingButtonWrapper.module.scss"; export default function LandingButtonWrapper() { const router = useRouter(); - const t = useTranslations( - translatePathEmailMarketingSoulmateV1("Landing") - ); + const t = useTranslations(translatePathEmailMarketingSoulmateV1("Landing")); const handleContinue = () => { router.push(ROUTES.emailMarketingSoulmateV1SpecialOffer()); diff --git a/src/components/domains/email-marketing/soulmate/v1/Payments/Payments.tsx b/src/components/domains/email-marketing/soulmate/v1/Payments/Payments.tsx index 1737f94..8b7384a 100644 --- a/src/components/domains/email-marketing/soulmate/v1/Payments/Payments.tsx +++ b/src/components/domains/email-marketing/soulmate/v1/Payments/Payments.tsx @@ -1,8 +1,6 @@ import Image from "next/image"; -import { - emailMarketingCompV2Images, -} from "@/shared/constants/images"; +import { emailMarketingCompV2Images } from "@/shared/constants/images"; import styles from "./Payments.module.scss"; diff --git a/src/components/domains/email-marketing/soulmate/v1/TrialIntervalOffer/TrialIntervalOffer.tsx b/src/components/domains/email-marketing/soulmate/v1/TrialIntervalOffer/TrialIntervalOffer.tsx index f07aa6f..f533e28 100644 --- a/src/components/domains/email-marketing/soulmate/v1/TrialIntervalOffer/TrialIntervalOffer.tsx +++ b/src/components/domains/email-marketing/soulmate/v1/TrialIntervalOffer/TrialIntervalOffer.tsx @@ -19,9 +19,7 @@ export default async function TrialIntervalOffer({ newTrialInterval, }: ITrialIntervalOfferProps) { const t = await getTranslations( - translatePathEmailMarketingSoulmateV1( - "Landing.special-offer.trial-offer" - ) + translatePathEmailMarketingSoulmateV1("Landing.special-offer.trial-offer") ); return ( diff --git a/src/components/domains/portraits/PortraitView/PortraitView.tsx b/src/components/domains/portraits/PortraitView/PortraitView.tsx index 0d1e671..c69d401 100644 --- a/src/components/domains/portraits/PortraitView/PortraitView.tsx +++ b/src/components/domains/portraits/PortraitView/PortraitView.tsx @@ -14,7 +14,11 @@ interface PortraitViewProps { result?: string | null; } -export default function PortraitView({ title, imageUrl, result }: PortraitViewProps) { +export default function PortraitView({ + title, + imageUrl, + result, +}: PortraitViewProps) { const router = useRouter(); const handleDownload = async () => { @@ -42,7 +46,12 @@ export default function PortraitView({ title, imageUrl, result }: PortraitViewPr - + {title}
@@ -67,28 +76,87 @@ export default function PortraitView({ title, imageUrl, result }: PortraitViewPr onClick={handleDownload} aria-label="Download portrait" > - + - - - - - + + + + + - - - - - - - - - - - - - + + + + + + + + + + + + + diff --git a/src/components/domains/video-guides/VideoGuideView/VideoGuideView.tsx b/src/components/domains/video-guides/VideoGuideView/VideoGuideView.tsx index dd343c4..57319a8 100644 --- a/src/components/domains/video-guides/VideoGuideView/VideoGuideView.tsx +++ b/src/components/domains/video-guides/VideoGuideView/VideoGuideView.tsx @@ -13,14 +13,18 @@ interface VideoGuideViewProps { videoLink: string; } -export default function VideoGuideView({ name, description, videoLink }: VideoGuideViewProps) { +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 + /^([a-zA-Z0-9_-]{11})$/, // Direct video ID ]; for (const pattern of patterns) { @@ -42,7 +46,12 @@ export default function VideoGuideView({ name, description, videoLink }: VideoGu - + {name} diff --git a/src/components/layout/NavigationBar/NavigationBar.module.scss b/src/components/layout/NavigationBar/NavigationBar.module.scss index 2ac3fc2..6841564 100644 --- a/src/components/layout/NavigationBar/NavigationBar.module.scss +++ b/src/components/layout/NavigationBar/NavigationBar.module.scss @@ -6,13 +6,15 @@ right: 0; width: 100vw; // Height: tab bar height + moderate overlap above tab bar - height: calc(14px + 60px + 20px); // bottom offset + tab bar height + overlap above + height: calc( + 14px + 60px + 20px + ); // bottom offset + tab bar height + overlap above z-index: 9994; // Just below the tab bar backdrop-filter: blur(4px); -webkit-backdrop-filter: blur(4px); background: rgba(255, 255, 255, 0.7); pointer-events: none; // Don't block interactions - + // Fallback for browsers that don't support backdrop-filter @supports not (backdrop-filter: blur(1px)) { background: rgba(255, 255, 255, 0.85); diff --git a/src/components/ui/Icon/icons/AlertCircle.tsx b/src/components/ui/Icon/icons/AlertCircle.tsx index e96fa1b..eccda6b 100644 --- a/src/components/ui/Icon/icons/AlertCircle.tsx +++ b/src/components/ui/Icon/icons/AlertCircle.tsx @@ -11,25 +11,9 @@ export default function AlertCircleIcon(props: SVGProps) { xmlns="http://www.w3.org/2000/svg" {...props} > - - - + + + ); } diff --git a/src/components/ui/MarkdownText/MarkdownText.tsx b/src/components/ui/MarkdownText/MarkdownText.tsx index c893a2e..6ba5721 100644 --- a/src/components/ui/MarkdownText/MarkdownText.tsx +++ b/src/components/ui/MarkdownText/MarkdownText.tsx @@ -33,7 +33,13 @@ export default function MarkdownText({ if (headerMatch) { const level = headerMatch[1].length; const text = headerMatch[2]; - const HeaderTag = `h${level}` as "h1" | "h2" | "h3" | "h4" | "h5" | "h6"; + const HeaderTag = `h${level}` as + | "h1" + | "h2" + | "h3" + | "h4" + | "h5" + | "h6"; elements.push( React.createElement( HeaderTag, diff --git a/src/components/ui/index.ts b/src/components/ui/index.ts index 498ac49..b5a0ff1 100644 --- a/src/components/ui/index.ts +++ b/src/components/ui/index.ts @@ -8,7 +8,10 @@ export { default as FullScreenBlurModal } from "./FullScreenBlurModal/FullScreen export { default as GPTAnimationText } from "./GPTAnimationText/GPTAnimationText"; export { default as Grid } from "./Grid/Grid"; export { default as Icon, IconName, type IconProps } from "./Icon/Icon"; -export { default as IconLabel, type IconLabelProps } from "./IconLabel/IconLabel"; +export { + default as IconLabel, + type IconLabelProps, +} from "./IconLabel/IconLabel"; export { default as MarkdownText } from "./MarkdownText/MarkdownText"; export { default as MetaLabel } from "./MetaLabel/MetaLabel"; export { default as Modal, type ModalProps } from "./Modal/Modal"; diff --git a/src/entities/dashboard/loaders.ts b/src/entities/dashboard/loaders.ts index 2dcc202..1533d2b 100644 --- a/src/entities/dashboard/loaders.ts +++ b/src/entities/dashboard/loaders.ts @@ -12,8 +12,7 @@ export const loadCompatibility = () => export const loadMeditations = () => loadDashboard().then(d => d.meditations || []); -export const loadPalms = () => - loadDashboard().then(d => d.palmActions || []); +export const loadPalms = () => loadDashboard().then(d => d.palmActions || []); export const loadPortraits = () => loadDashboard().then(d => d.partnerPortraits || []); diff --git a/src/hooks/chats/useChatSocket.ts b/src/hooks/chats/useChatSocket.ts index 47b85c4..db23e8b 100644 --- a/src/hooks/chats/useChatSocket.ts +++ b/src/hooks/chats/useChatSocket.ts @@ -144,7 +144,9 @@ export const useChatSocket = ( autoTopUp: false, }); // eslint-disable-next-line no-console - console.info("Auto top-up disabled successfully after payment failure"); + console.info( + "Auto top-up disabled successfully after payment failure" + ); } } catch (error) { // eslint-disable-next-line no-console diff --git a/src/hooks/timer/useTimer.ts b/src/hooks/timer/useTimer.ts index 644e613..aa50582 100644 --- a/src/hooks/timer/useTimer.ts +++ b/src/hooks/timer/useTimer.ts @@ -22,7 +22,7 @@ export function useTimer({ // Load from localStorage after mount (client-only) useEffect(() => { - if (persist && storageKey && typeof window !== 'undefined') { + if (persist && storageKey && typeof window !== "undefined") { const saved = localStorage.getItem(storageKey); if (saved !== null) { const parsed = parseInt(saved, 10); @@ -36,7 +36,7 @@ export function useTimer({ // Save to localStorage when seconds change useEffect(() => { - if (persist && storageKey && typeof window !== 'undefined') { + if (persist && storageKey && typeof window !== "undefined") { localStorage.setItem(storageKey, seconds.toString()); } }, [seconds, persist, storageKey]); @@ -61,7 +61,7 @@ export function useTimer({ const reset = useCallback(() => { setSeconds(initialSeconds); - if (persist && storageKey && typeof window !== 'undefined') { + if (persist && storageKey && typeof window !== "undefined") { localStorage.setItem(storageKey, initialSeconds.toString()); } }, [initialSeconds, persist, storageKey]); diff --git a/src/hooks/video-guides/useVideoGuidePurchase.ts b/src/hooks/video-guides/useVideoGuidePurchase.ts index 0a0d1ba..0adf6ad 100644 --- a/src/hooks/video-guides/useVideoGuidePurchase.ts +++ b/src/hooks/video-guides/useVideoGuidePurchase.ts @@ -22,39 +22,43 @@ export function useVideoGuidePurchase(options: UseVideoGuidePurchaseOptions) { const [isCheckingPurchase, setIsCheckingPurchase] = useState(false); const [isPending, startTransition] = useTransition(); - const { handleSingleCheckout, isLoading: isCheckoutLoading } = useSingleCheckout({ - onSuccess: async () => { - // Показываем toast о успешной покупке - addToast({ - variant: "success", - message: "Video guide purchased successfully!", - duration: 3000, - }); + const { handleSingleCheckout, isLoading: isCheckoutLoading } = + useSingleCheckout({ + onSuccess: async () => { + // Показываем toast о успешной покупке + addToast({ + variant: "success", + message: "Video guide purchased successfully!", + duration: 3000, + }); - // Включаем лоадер на всей карточке - setIsProcessingPurchase(true); + // Включаем лоадер на всей карточке + setIsProcessingPurchase(true); - // Ждем 4 секунды - await new Promise(resolve => setTimeout(resolve, 4000)); + // Ждем 4 секунды + await new Promise(resolve => setTimeout(resolve, 4000)); - // Обновляем данные dashboard в transition - // isPending будет true пока данные загружаются - startTransition(() => { - router.refresh(); - }); + // Обновляем данные dashboard в transition + // isPending будет true пока данные загружаются + startTransition(() => { + router.refresh(); + }); - // Убираем наш флаг, но isPending продолжит показывать loader - setIsProcessingPurchase(false); - }, - onError: error => { - addToast({ - variant: "error", - message: error || "Purchase failed. Please try again.", - duration: 5000, - }); - }, - returnUrl: new URL(ROUTES.home(), process.env.NEXT_PUBLIC_APP_URL || "").toString(), - }); + // Убираем наш флаг, но isPending продолжит показывать loader + setIsProcessingPurchase(false); + }, + onError: error => { + addToast({ + variant: "error", + message: error || "Purchase failed. Please try again.", + duration: 5000, + }); + }, + returnUrl: new URL( + ROUTES.home(), + process.env.NEXT_PUBLIC_APP_URL || "" + ).toString(), + }); const handlePurchase = useCallback(async () => { // Сначала проверяем, не куплен ли уже продукт @@ -97,6 +101,7 @@ export function useVideoGuidePurchase(options: UseVideoGuidePurchaseOptions) { key: productKey, }); } catch (error) { + // eslint-disable-next-line no-console console.error("Error checking purchase status:", error); setIsCheckingPurchase(false);