video-progress-edits
ap edits
This commit is contained in:
parent
83d1faaa38
commit
7f9f1af39e
@ -418,4 +418,4 @@
|
||||
},
|
||||
"Soulmate": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -418,4 +418,4 @@
|
||||
},
|
||||
"Soulmate": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 (
|
||||
<MultiPageNavigationProvider data={allProducts} currentType={pageType}>
|
||||
{children}
|
||||
<ProgressLayout>{children}</ProgressLayout>
|
||||
</MultiPageNavigationProvider>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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<IFunnelPaymentPlacement | null>,
|
||||
loadFunnelPaymentById(
|
||||
payload,
|
||||
"main"
|
||||
) as Promise<IFunnelPaymentPlacement | null>,
|
||||
loadUser(),
|
||||
]);
|
||||
|
||||
|
||||
@ -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 ? (
|
||||
<Spinner />
|
||||
) : (
|
||||
<Typography color="white" className={styles.text}>
|
||||
|
||||
@ -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 (
|
||||
<>
|
||||
<Progress
|
||||
items={progressItems}
|
||||
activeItemIndex={navigation.currentIndex}
|
||||
/>
|
||||
<Typography as="h2" size="xl" weight="semiBold" className={styles.title}>
|
||||
{t("title")}
|
||||
</Typography>
|
||||
|
||||
@ -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 (
|
||||
|
||||
@ -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 (
|
||||
<ProductSelectionProvider>
|
||||
<Progress
|
||||
items={progressItems}
|
||||
activeItemIndex={navigation.currentIndex}
|
||||
/>
|
||||
<Typography as="h2" size="xl" weight="semiBold" className={styles.title}>
|
||||
{t("title")}
|
||||
</Typography>
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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<HTMLDivElement>({
|
||||
defaultWidth: 327,
|
||||
});
|
||||
const wrapperRef = useRef<HTMLDivElement>(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<HTMLDivElement>({
|
||||
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 (
|
||||
<div
|
||||
className={clsx(styles.stickyWrapper, isScrolled && styles.scrolled)}
|
||||
ref={wrapperRef}
|
||||
<BlurComponent
|
||||
isActiveBlur={true}
|
||||
className={styles.container}
|
||||
gradientClassName={styles.blurGradient}
|
||||
ref={elementRef}
|
||||
>
|
||||
<div
|
||||
className={styles.container}
|
||||
ref={elementRef}
|
||||
style={{
|
||||
minHeight: containerHeight > 0 ? `${containerHeight}px` : undefined,
|
||||
}}
|
||||
>
|
||||
{allItems.map((item, index) => {
|
||||
const itemPosition = calculateItemPosition(index);
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
className={clsx(
|
||||
styles.item,
|
||||
activeItemIndex === index && styles.active,
|
||||
activeItemIndex > index && styles.done
|
||||
{allItems.map((item, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={clsx(
|
||||
styles.item,
|
||||
activeItemIndex === index && styles.active,
|
||||
activeItemIndex > index && styles.done
|
||||
)}
|
||||
>
|
||||
<div className={styles.marker}>
|
||||
{activeItemIndex > index && styles.done && (
|
||||
<Icon
|
||||
name={IconName.Check}
|
||||
color="#fff"
|
||||
size={{
|
||||
width: 12,
|
||||
height: 12,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
style={{
|
||||
left: `${itemPosition}px`,
|
||||
transform: 'translateX(-50%)', // Центрируем относительно позиции
|
||||
}}
|
||||
>
|
||||
<div className={styles.marker}>
|
||||
{activeItemIndex > index && styles.done && (
|
||||
<Icon
|
||||
name={IconName.Check}
|
||||
color="#fff"
|
||||
size={{
|
||||
width: 12,
|
||||
height: 12,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<Typography as="span" className={styles.number}>
|
||||
{index + 1}
|
||||
</Typography>
|
||||
</div>
|
||||
<Typography as="p" align="center" className={styles.text}>
|
||||
{item}
|
||||
<Typography as="span" className={styles.number}>
|
||||
{index + 1}
|
||||
</Typography>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<Typography as="p" align="center" className={styles.text}>
|
||||
{item}
|
||||
</Typography>
|
||||
</div>
|
||||
))}
|
||||
<div
|
||||
className={styles.connector}
|
||||
style={{
|
||||
width: containerWidth - (edgeOffset * 2),
|
||||
left: edgeOffset,
|
||||
background: activeItemIndex > 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",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</BlurComponent>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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 (
|
||||
<>
|
||||
<Progress
|
||||
items={progressItems}
|
||||
activeItemIndex={navigation.currentIndex}
|
||||
/>
|
||||
{children}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@ -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 (
|
||||
|
||||
@ -86,9 +86,7 @@ export default function VideoGuidesOffer(props: VideoGuidesOfferProps) {
|
||||
</Typography>
|
||||
</Typography>
|
||||
{!isFirstOffer && (
|
||||
<Typography className={styles.discount}>
|
||||
{discount}% OFF
|
||||
</Typography>
|
||||
<Typography className={styles.discount}>{discount}% OFF</Typography>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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 (
|
||||
<ProductSelectionProvider>
|
||||
<Progress
|
||||
items={progressItems}
|
||||
activeItemIndex={navigation.currentIndex}
|
||||
/>
|
||||
<AdditionalPurchaseBanner />
|
||||
<Typography as="h1" className={styles.title}>
|
||||
{t("title")}
|
||||
|
||||
@ -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";
|
||||
|
||||
@ -82,7 +82,10 @@ export default function MessageInput({
|
||||
aria-label="Send"
|
||||
className={styles.sendButton}
|
||||
>
|
||||
<Icon name={IconName.PaperAirplane} size={{ height: 14, width: 14 }} />
|
||||
<Icon
|
||||
name={IconName.PaperAirplane}
|
||||
size={{ height: 14, width: 14 }}
|
||||
/>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -12,13 +12,22 @@ import styles from "./PortraitCard.module.scss";
|
||||
type PortraitCardProps = PartnerPortrait;
|
||||
|
||||
const HeartCheckIcon = () => (
|
||||
<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<svg
|
||||
width="15"
|
||||
height="15"
|
||||
viewBox="0 0 15 15"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<g clipPath="url(#clip0_2443_1886)">
|
||||
<path d="M1.78594 8.96384L6.72695 13.5767C6.93203 13.7681 7.20273 13.8748 7.48438 13.8748C7.76602 13.8748 8.03672 13.7681 8.2418 13.5767L13.1828 8.96384C14.0141 8.19001 14.4844 7.10447 14.4844 5.9697V5.81111C14.4844 3.89978 13.1035 2.27009 11.2195 1.95564C9.97266 1.74783 8.70391 2.15525 7.8125 3.04666L7.48438 3.37478L7.15625 3.04666C6.26484 2.15525 4.99609 1.74783 3.74922 1.95564C1.86523 2.27009 0.484375 3.89978 0.484375 5.81111V5.9697C0.484375 7.10447 0.954687 8.19001 1.78594 8.96384Z" fill="#16A34A"/>
|
||||
<path
|
||||
d="M1.78594 8.96384L6.72695 13.5767C6.93203 13.7681 7.20273 13.8748 7.48438 13.8748C7.76602 13.8748 8.03672 13.7681 8.2418 13.5767L13.1828 8.96384C14.0141 8.19001 14.4844 7.10447 14.4844 5.9697V5.81111C14.4844 3.89978 13.1035 2.27009 11.2195 1.95564C9.97266 1.74783 8.70391 2.15525 7.8125 3.04666L7.48438 3.37478L7.15625 3.04666C6.26484 2.15525 4.99609 1.74783 3.74922 1.95564C1.86523 2.27009 0.484375 3.89978 0.484375 5.81111V5.9697C0.484375 7.10447 0.954687 8.19001 1.78594 8.96384Z"
|
||||
fill="#16A34A"
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_2443_1886">
|
||||
<path d="M0.484375 0.75H14.4844V14.75H0.484375V0.75Z" fill="white"/>
|
||||
<path d="M0.484375 0.75H14.4844V14.75H0.484375V0.75Z" fill="white" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
@ -35,21 +44,39 @@ const getStatusConfig = (status: PartnerPortrait["status"]) => {
|
||||
};
|
||||
case "processing":
|
||||
return {
|
||||
icon: <Icon name={IconName.Loader} className={styles.iconProcessing} size={{ width: 16, height: 16 }} />,
|
||||
icon: (
|
||||
<Icon
|
||||
name={IconName.Loader}
|
||||
className={styles.iconProcessing}
|
||||
size={{ width: 16, height: 16 }}
|
||||
/>
|
||||
),
|
||||
text: "Processing...",
|
||||
showCheckmark: false,
|
||||
className: styles.statusProcessing,
|
||||
};
|
||||
case "queued":
|
||||
return {
|
||||
icon: <Icon name={IconName.Clock} className={styles.iconQueued} size={{ width: 16, height: 16 }} />,
|
||||
icon: (
|
||||
<Icon
|
||||
name={IconName.Clock}
|
||||
className={styles.iconQueued}
|
||||
size={{ width: 16, height: 16 }}
|
||||
/>
|
||||
),
|
||||
text: "In Queue",
|
||||
showCheckmark: false,
|
||||
className: styles.statusQueued,
|
||||
};
|
||||
case "error":
|
||||
return {
|
||||
icon: <Icon name={IconName.AlertCircle} className={styles.iconError} size={{ width: 16, height: 16 }} />,
|
||||
icon: (
|
||||
<Icon
|
||||
name={IconName.AlertCircle}
|
||||
className={styles.iconError}
|
||||
size={{ width: 16, height: 16 }}
|
||||
/>
|
||||
),
|
||||
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({
|
||||
<Card
|
||||
className={`${styles.card} ${!isReady ? styles.disabled : ""}`}
|
||||
onClick={handleClick}
|
||||
style={{ cursor: isReady ? 'pointer' : 'default' }}
|
||||
style={{ cursor: isReady ? "pointer" : "default" }}
|
||||
>
|
||||
<div className={styles.imageContainer}>
|
||||
{imageUrl ? (
|
||||
@ -98,17 +128,32 @@ export default function PortraitCard({
|
||||
/>
|
||||
) : (
|
||||
<div className={styles.placeholderImage}>
|
||||
<Icon name={IconName.Loader} className={styles.placeholderLoader} size={{ width: 48, height: 48 }} />
|
||||
<Icon
|
||||
name={IconName.Loader}
|
||||
className={styles.placeholderLoader}
|
||||
size={{ width: 48, height: 48 }}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className={styles.content}>
|
||||
<div className={styles.textContent}>
|
||||
<Typography as="h3" size="lg" weight="semiBold" align="left" className={styles.title}>
|
||||
<Typography
|
||||
as="h3"
|
||||
size="lg"
|
||||
weight="semiBold"
|
||||
align="left"
|
||||
className={styles.title}
|
||||
>
|
||||
{title}
|
||||
</Typography>
|
||||
<Typography size="sm" color="secondary" align="left" className={styles.subtitle}>
|
||||
<Typography
|
||||
size="sm"
|
||||
color="secondary"
|
||||
align="left"
|
||||
className={styles.subtitle}
|
||||
>
|
||||
Finding the One Guide
|
||||
</Typography>
|
||||
</div>
|
||||
@ -120,13 +165,21 @@ export default function PortraitCard({
|
||||
{statusConfig.text}
|
||||
</Typography>
|
||||
{statusConfig.showCheckmark && (
|
||||
<Icon name={IconName.Check} className={styles.checkmark} size={{ width: 14, height: 14 }} />
|
||||
<Icon
|
||||
name={IconName.Check}
|
||||
className={styles.checkmark}
|
||||
size={{ width: 14, height: 14 }}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{status === "done" && (
|
||||
<button className={styles.actionButton} aria-label="View portrait">
|
||||
<Icon name={IconName.ChevronRight} size={{ width: 20, height: 20 }} color="white" />
|
||||
<Icon
|
||||
name={IconName.ChevronRight}
|
||||
size={{ width: 20, height: 20 }}
|
||||
color="white"
|
||||
/>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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 (
|
||||
<Card className={clsx(styles.container, className, isPurchased && styles.purchased)}>
|
||||
<Card
|
||||
className={clsx(
|
||||
styles.container,
|
||||
className,
|
||||
isPurchased && styles.purchased
|
||||
)}
|
||||
>
|
||||
{/* Image with Play Icon */}
|
||||
<div className={styles.image}>
|
||||
<img src={imageUrl} alt={name} className={styles.imageContent} />
|
||||
<Image
|
||||
src={imageUrl}
|
||||
alt={name}
|
||||
width={260}
|
||||
height={160}
|
||||
unoptimized
|
||||
className={styles.imageContent}
|
||||
/>
|
||||
<div className={styles.playIcon}>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="65" viewBox="0 0 64 65" fill="none">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="64"
|
||||
height="65"
|
||||
viewBox="0 0 64 65"
|
||||
fill="none"
|
||||
>
|
||||
<g filter="url(#filter0_d_2540_2312)">
|
||||
<path d="M48.25 31C48.25 26.7247 46.538 22.6246 43.4905 19.6015C40.443 16.5784 36.3098 14.88 32 14.88C27.6902 14.88 23.557 16.5784 20.5095 19.6015C17.462 22.6246 15.75 26.7247 15.75 31C15.75 35.2753 17.462 39.3755 20.5095 42.3986C23.557 45.4217 27.6902 47.12 32 47.12C36.3098 47.12 40.443 45.4217 43.4905 42.3986C46.538 39.3755 48.25 35.2753 48.25 31ZM12 31C12 25.7381 14.1071 20.6918 17.8579 16.971C21.6086 13.2503 26.6957 11.16 32 11.16C37.3043 11.16 42.3914 13.2503 46.1421 16.971C49.8929 20.6918 52 25.7381 52 31C52 36.2619 49.8929 41.3083 46.1421 45.029C42.3914 48.7498 37.3043 50.84 32 50.84C26.6957 50.84 21.6086 48.7498 17.8579 45.029C14.1071 41.3083 12 36.2619 12 31ZM26.7109 22.5603C27.3047 22.2348 28.0234 22.2425 28.6094 22.599L39.8594 29.419C40.4141 29.76 40.7578 30.3568 40.7578 31.0078C40.7578 31.6588 40.4141 32.2555 39.8594 32.5965L28.6094 39.4165C28.0313 39.7653 27.3047 39.7808 26.7109 39.4553C26.1172 39.1298 25.75 38.5098 25.75 37.8355V24.18C25.75 23.5058 26.1172 22.8858 26.7109 22.5603Z" fill="white"/>
|
||||
<path
|
||||
d="M48.25 31C48.25 26.7247 46.538 22.6246 43.4905 19.6015C40.443 16.5784 36.3098 14.88 32 14.88C27.6902 14.88 23.557 16.5784 20.5095 19.6015C17.462 22.6246 15.75 26.7247 15.75 31C15.75 35.2753 17.462 39.3755 20.5095 42.3986C23.557 45.4217 27.6902 47.12 32 47.12C36.3098 47.12 40.443 45.4217 43.4905 42.3986C46.538 39.3755 48.25 35.2753 48.25 31ZM12 31C12 25.7381 14.1071 20.6918 17.8579 16.971C21.6086 13.2503 26.6957 11.16 32 11.16C37.3043 11.16 42.3914 13.2503 46.1421 16.971C49.8929 20.6918 52 25.7381 52 31C52 36.2619 49.8929 41.3083 46.1421 45.029C42.3914 48.7498 37.3043 50.84 32 50.84C26.6957 50.84 21.6086 48.7498 17.8579 45.029C14.1071 41.3083 12 36.2619 12 31ZM26.7109 22.5603C27.3047 22.2348 28.0234 22.2425 28.6094 22.599L39.8594 29.419C40.4141 29.76 40.7578 30.3568 40.7578 31.0078C40.7578 31.6588 40.4141 32.2555 39.8594 32.5965L28.6094 39.4165C28.0313 39.7653 27.3047 39.7808 26.7109 39.4553C26.1172 39.1298 25.75 38.5098 25.75 37.8355V24.18C25.75 23.5058 26.1172 22.8858 26.7109 22.5603Z"
|
||||
fill="white"
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
<filter id="filter0_d_2540_2312" x="-8.75" y="-10" width="84" height="86" filterUnits="userSpaceOnUse" colorInterpolationFilters="sRGB">
|
||||
<feFlood floodOpacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="2"/>
|
||||
<feGaussianBlur stdDeviation="6"/>
|
||||
<feComposite in2="hardAlpha" operator="out"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0"/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_2540_2312"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_2540_2312" result="shape"/>
|
||||
<filter
|
||||
id="filter0_d_2540_2312"
|
||||
x="-8.75"
|
||||
y="-10"
|
||||
width="84"
|
||||
height="86"
|
||||
filterUnits="userSpaceOnUse"
|
||||
colorInterpolationFilters="sRGB"
|
||||
>
|
||||
<feFlood floodOpacity="0" result="BackgroundImageFix" />
|
||||
<feColorMatrix
|
||||
in="SourceAlpha"
|
||||
type="matrix"
|
||||
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
|
||||
result="hardAlpha"
|
||||
/>
|
||||
<feOffset dy="2" />
|
||||
<feGaussianBlur stdDeviation="6" />
|
||||
<feComposite in2="hardAlpha" operator="out" />
|
||||
<feColorMatrix
|
||||
type="matrix"
|
||||
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0"
|
||||
/>
|
||||
<feBlend
|
||||
mode="normal"
|
||||
in2="BackgroundImageFix"
|
||||
result="effect1_dropShadow_2540_2312"
|
||||
/>
|
||||
<feBlend
|
||||
mode="normal"
|
||||
in="SourceGraphic"
|
||||
in2="effect1_dropShadow_2540_2312"
|
||||
result="shape"
|
||||
/>
|
||||
</filter>
|
||||
</defs>
|
||||
</svg>
|
||||
@ -82,8 +143,17 @@ export default function VideoGuideCard(props: VideoGuideCardProps) {
|
||||
</div>
|
||||
{isPurchased && (
|
||||
<button className={styles.arrowButton}>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="8" height="14" viewBox="0 0 8 14" fill="none">
|
||||
<path d="M7.70859 6.29609C8.09922 6.68672 8.09922 7.32109 7.70859 7.71172L1.70859 13.7117C1.31797 14.1023 0.683594 14.1023 0.292969 13.7117C-0.0976562 13.3211 -0.0976562 12.6867 0.292969 12.2961L5.58672 7.00234L0.296094 1.70859C-0.0945313 1.31797 -0.0945313 0.683594 0.296094 0.292969C0.686719 -0.0976562 1.32109 -0.0976562 1.71172 0.292969L7.71172 6.29297L7.70859 6.29609Z" fill="#A0A7B5"/>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="8"
|
||||
height="14"
|
||||
viewBox="0 0 8 14"
|
||||
fill="none"
|
||||
>
|
||||
<path
|
||||
d="M7.70859 6.29609C8.09922 6.68672 8.09922 7.32109 7.70859 7.71172L1.70859 13.7117C1.31797 14.1023 0.683594 14.1023 0.292969 13.7117C-0.0976562 13.3211 -0.0976562 12.6867 0.292969 12.2961L5.58672 7.00234L0.296094 1.70859C-0.0945313 1.31797 -0.0945313 0.683594 0.296094 0.292969C0.686719 -0.0976562 1.32109 -0.0976562 1.71172 0.292969L7.71172 6.29297L7.70859 6.29609Z"
|
||||
fill="#A0A7B5"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
)}
|
||||
@ -94,16 +164,21 @@ export default function VideoGuideCard(props: VideoGuideCardProps) {
|
||||
{!isPurchased ? (
|
||||
<>
|
||||
<div className={styles.bottomText}>
|
||||
<Typography className={styles.duration} align="left">{duration}</Typography>
|
||||
<Typography className={styles.duration} align="left">
|
||||
{duration}
|
||||
</Typography>
|
||||
<div className={styles.discountBadge}>
|
||||
<Typography className={styles.discountText}>
|
||||
{discount}% OFF <span className={styles.oldPrice}>{getFormattedPrice(oldPrice, currency)}</span>
|
||||
{discount}% OFF{" "}
|
||||
<span className={styles.oldPrice}>
|
||||
{getFormattedPrice(oldPrice, currency)}
|
||||
</span>
|
||||
</Typography>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
className={styles.buyButton}
|
||||
onClick={(e) => {
|
||||
onClick={e => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
onPurchaseClick?.();
|
||||
@ -113,12 +188,16 @@ export default function VideoGuideCard(props: VideoGuideCardProps) {
|
||||
{isCheckoutLoading ? (
|
||||
<Spinner size={20} />
|
||||
) : (
|
||||
tCommon("purchaseFor", { price: getFormattedPrice(price, currency) })
|
||||
tCommon("purchaseFor", {
|
||||
price: getFormattedPrice(price, currency),
|
||||
})
|
||||
)}
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<Typography className={styles.durationPurchased} align="right">{duration}</Typography>
|
||||
<Typography className={styles.durationPurchased} align="right">
|
||||
{duration}
|
||||
</Typography>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -10,8 +10,9 @@
|
||||
.grid {
|
||||
padding-right: 16px;
|
||||
grid-auto-rows: 1fr;
|
||||
|
||||
a, > div {
|
||||
|
||||
a,
|
||||
> div {
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
display: block;
|
||||
|
||||
@ -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 (
|
||||
<Link
|
||||
href={href}
|
||||
key={`video-guide-${videoGuide.id}`}
|
||||
>
|
||||
<Link href={href} key={`video-guide-${videoGuide.id}`}>
|
||||
{cardElement}
|
||||
</Link>
|
||||
);
|
||||
@ -58,7 +57,9 @@ function VideoGuideCardWrapper({ videoGuide }: { videoGuide: VideoGuide }) {
|
||||
return <div key={`video-guide-${videoGuide.id}`}>{cardElement}</div>;
|
||||
}
|
||||
|
||||
export default function VideoGuidesSection({ videoGuides }: VideoGuidesSectionProps) {
|
||||
export default function VideoGuidesSection({
|
||||
videoGuides,
|
||||
}: VideoGuidesSectionProps) {
|
||||
if (!videoGuides || videoGuides.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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 (
|
||||
<div className={styles.container}>
|
||||
<Image
|
||||
|
||||
@ -22,9 +22,7 @@ type TMessagesKey = "me" | "advisor";
|
||||
|
||||
export default async function IndividualAdviceCard() {
|
||||
const t = await getTranslations(
|
||||
translatePathEmailMarketingSoulmateV1(
|
||||
"Landing.what-get.individual-advice"
|
||||
)
|
||||
translatePathEmailMarketingSoulmateV1("Landing.what-get.individual-advice")
|
||||
);
|
||||
|
||||
const messages = t.raw("messages") as Record<TMessagesKey, IMessage>;
|
||||
|
||||
@ -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());
|
||||
|
||||
@ -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";
|
||||
|
||||
|
||||
@ -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 (
|
||||
|
||||
@ -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
|
||||
<button className={styles.backButton} onClick={() => router.back()}>
|
||||
<Icon name={IconName.ChevronLeft} size={{ width: 24, height: 24 }} />
|
||||
</button>
|
||||
<Typography as="h1" size="xl" weight="semiBold" className={styles.title}>
|
||||
<Typography
|
||||
as="h1"
|
||||
size="xl"
|
||||
weight="semiBold"
|
||||
className={styles.title}
|
||||
>
|
||||
{title}
|
||||
</Typography>
|
||||
</div>
|
||||
@ -67,28 +76,87 @@ export default function PortraitView({ title, imageUrl, result }: PortraitViewPr
|
||||
onClick={handleDownload}
|
||||
aria-label="Download portrait"
|
||||
>
|
||||
<svg width="52" height="52" viewBox="0 0 52 52" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<svg
|
||||
width="52"
|
||||
height="52"
|
||||
viewBox="0 0 52 52"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<g filter="url(#filter0_dd_2449_3797)">
|
||||
<path d="M6 22C6 10.9543 14.9543 2 26 2C37.0457 2 46 10.9543 46 22C46 33.0457 37.0457 42 26 42C14.9543 42 6 33.0457 6 22Z" fill="white" fillOpacity="0.75"/>
|
||||
<path d="M25 13.0124C24.9983 12.4601 25.4446 12.011 25.9969 12.0093C26.5492 12.0076 26.9983 12.4539 27 13.0061L25 13.0124Z" fill="#646464"/>
|
||||
<path d="M28.3158 20.2952L27.0269 21.5921L27 13.0063L25 13.0126L25.0269 21.5984L23.7301 20.3096C23.3383 19.9203 22.7051 19.9222 22.3158 20.314C21.9265 20.7057 21.9285 21.3389 22.3203 21.7282L22.3228 21.7307L22.3238 21.7317L26.039 25.4237L29.7206 21.7188L29.7262 21.7132L29.727 21.7124L29.7278 21.7116L29.7337 21.7057L28.3158 20.2952Z" fill="#646464"/>
|
||||
<path d="M29.7345 21.7049C30.1238 21.3131 30.1218 20.6799 29.7301 20.2906C29.3383 19.9014 28.7051 19.9034 28.3159 20.2951L29.7345 21.7049Z" fill="#646464"/>
|
||||
<path d="M18 22C18 20.8954 18.8954 20 20 20C20.5523 20 21 19.5523 21 19C21 18.4477 20.5523 18 20 18C17.7909 18 16 19.7909 16 22V28C16 30.2091 17.7909 32 20 32H31C33.7614 32 36 29.7614 36 27V22C36 19.7909 34.2091 18 32 18C31.4477 18 31 18.4477 31 19C31 19.5523 31.4477 20 32 20C33.1046 20 34 20.8954 34 22V27C34 28.6569 32.6569 30 31 30H20C18.8954 30 18 29.1046 18 28V22Z" fill="#646464"/>
|
||||
<path
|
||||
d="M6 22C6 10.9543 14.9543 2 26 2C37.0457 2 46 10.9543 46 22C46 33.0457 37.0457 42 26 42C14.9543 42 6 33.0457 6 22Z"
|
||||
fill="white"
|
||||
fillOpacity="0.75"
|
||||
/>
|
||||
<path
|
||||
d="M25 13.0124C24.9983 12.4601 25.4446 12.011 25.9969 12.0093C26.5492 12.0076 26.9983 12.4539 27 13.0061L25 13.0124Z"
|
||||
fill="#646464"
|
||||
/>
|
||||
<path
|
||||
d="M28.3158 20.2952L27.0269 21.5921L27 13.0063L25 13.0126L25.0269 21.5984L23.7301 20.3096C23.3383 19.9203 22.7051 19.9222 22.3158 20.314C21.9265 20.7057 21.9285 21.3389 22.3203 21.7282L22.3228 21.7307L22.3238 21.7317L26.039 25.4237L29.7206 21.7188L29.7262 21.7132L29.727 21.7124L29.7278 21.7116L29.7337 21.7057L28.3158 20.2952Z"
|
||||
fill="#646464"
|
||||
/>
|
||||
<path
|
||||
d="M29.7345 21.7049C30.1238 21.3131 30.1218 20.6799 29.7301 20.2906C29.3383 19.9014 28.7051 19.9034 28.3159 20.2951L29.7345 21.7049Z"
|
||||
fill="#646464"
|
||||
/>
|
||||
<path
|
||||
d="M18 22C18 20.8954 18.8954 20 20 20C20.5523 20 21 19.5523 21 19C21 18.4477 20.5523 18 20 18C17.7909 18 16 19.7909 16 22V28C16 30.2091 17.7909 32 20 32H31C33.7614 32 36 29.7614 36 27V22C36 19.7909 34.2091 18 32 18C31.4477 18 31 18.4477 31 19C31 19.5523 31.4477 20 32 20C33.1046 20 34 20.8954 34 22V27C34 28.6569 32.6569 30 31 30H20C18.8954 30 18 29.1046 18 28V22Z"
|
||||
fill="#646464"
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
<filter id="filter0_dd_2449_3797" x="0" y="0" width="52" height="52" filterUnits="userSpaceOnUse" colorInterpolationFilters="sRGB">
|
||||
<feFlood floodOpacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="4"/>
|
||||
<feGaussianBlur stdDeviation="3"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.1 0"/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_2449_3797"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="2"/>
|
||||
<feGaussianBlur stdDeviation="2"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.1 0"/>
|
||||
<feBlend mode="normal" in2="effect1_dropShadow_2449_3797" result="effect2_dropShadow_2449_3797"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect2_dropShadow_2449_3797" result="shape"/>
|
||||
<filter
|
||||
id="filter0_dd_2449_3797"
|
||||
x="0"
|
||||
y="0"
|
||||
width="52"
|
||||
height="52"
|
||||
filterUnits="userSpaceOnUse"
|
||||
colorInterpolationFilters="sRGB"
|
||||
>
|
||||
<feFlood floodOpacity="0" result="BackgroundImageFix" />
|
||||
<feColorMatrix
|
||||
in="SourceAlpha"
|
||||
type="matrix"
|
||||
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
|
||||
result="hardAlpha"
|
||||
/>
|
||||
<feOffset dy="4" />
|
||||
<feGaussianBlur stdDeviation="3" />
|
||||
<feColorMatrix
|
||||
type="matrix"
|
||||
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.1 0"
|
||||
/>
|
||||
<feBlend
|
||||
mode="normal"
|
||||
in2="BackgroundImageFix"
|
||||
result="effect1_dropShadow_2449_3797"
|
||||
/>
|
||||
<feColorMatrix
|
||||
in="SourceAlpha"
|
||||
type="matrix"
|
||||
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
|
||||
result="hardAlpha"
|
||||
/>
|
||||
<feOffset dy="2" />
|
||||
<feGaussianBlur stdDeviation="2" />
|
||||
<feColorMatrix
|
||||
type="matrix"
|
||||
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.1 0"
|
||||
/>
|
||||
<feBlend
|
||||
mode="normal"
|
||||
in2="effect1_dropShadow_2449_3797"
|
||||
result="effect2_dropShadow_2449_3797"
|
||||
/>
|
||||
<feBlend
|
||||
mode="normal"
|
||||
in="SourceGraphic"
|
||||
in2="effect2_dropShadow_2449_3797"
|
||||
result="shape"
|
||||
/>
|
||||
</filter>
|
||||
</defs>
|
||||
</svg>
|
||||
|
||||
@ -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
|
||||
<button className={styles.backButton} onClick={() => router.back()}>
|
||||
<Icon name={IconName.ChevronLeft} size={{ width: 24, height: 24 }} />
|
||||
</button>
|
||||
<Typography as="h1" size="xl" weight="semiBold" className={styles.title}>
|
||||
<Typography
|
||||
as="h1"
|
||||
size="xl"
|
||||
weight="semiBold"
|
||||
className={styles.title}
|
||||
>
|
||||
{name}
|
||||
</Typography>
|
||||
</div>
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -11,25 +11,9 @@ export default function AlertCircleIcon(props: SVGProps<SVGSVGElement>) {
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...props}
|
||||
>
|
||||
<circle
|
||||
cx="12"
|
||||
cy="12"
|
||||
r="9"
|
||||
stroke={color}
|
||||
strokeWidth="2"
|
||||
/>
|
||||
<path
|
||||
d="M12 8V12"
|
||||
stroke={color}
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
<circle
|
||||
cx="12"
|
||||
cy="16"
|
||||
r="1"
|
||||
fill={color}
|
||||
/>
|
||||
<circle cx="12" cy="12" r="9" stroke={color} strokeWidth="2" />
|
||||
<path d="M12 8V12" stroke={color} strokeWidth="2" strokeLinecap="round" />
|
||||
<circle cx="12" cy="16" r="1" fill={color} />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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";
|
||||
|
||||
@ -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 || []);
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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]);
|
||||
|
||||
@ -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);
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user