add video guide to dashboard

This commit is contained in:
dev.daminik00 2025-10-28 02:06:45 +01:00
parent 042bb25057
commit 95e05cbabb
22 changed files with 510 additions and 172 deletions

View File

@ -305,43 +305,7 @@
"button": "Continue",
"skip_button": "Skip this offer and proceed further",
"copyright": "© 2025, Wit Lab LLC, California, US",
"products": {
"main_ultra_pack": {
"title": "Ultra Pack",
"subtitle": "3 in 1+2 secret bonus readings",
"discount": "{discount}% OFF",
"price": "Now <price></price> <oldPrice></oldPrice>",
"emoji": "star_struck.webp"
},
"main_numerology_analysis": {
"title": "Relationship plan",
"subtitle": "Discover the future without losing yourself",
"discount": "{discount}% OFF",
"price": "Now <price></price> <oldPrice></oldPrice>",
"emoji": "ring.webp"
},
"main_tarot_reading": {
"title": "Healthy compatibility",
"subtitle": "Balance between closeness and freedom",
"discount": "{discount}% OFF",
"price": "Now <price></price> <oldPrice></oldPrice>",
"emoji": "rose.webp"
},
"main_palmistry_guide": {
"title": "How to talk about feelings",
"subtitle": "Express your emotions and be understood",
"discount": "{discount}% OFF",
"price": "Now <price></price> <oldPrice></oldPrice>",
"emoji": "heart_from_hands.webp"
},
"main_money_reading": {
"title": "How to talk about feelings",
"subtitle": "Express your emotions and be understood",
"discount": "{discount}% OFF",
"price": "Now <price></price> <oldPrice></oldPrice>",
"emoji": "heart_from_hands.webp"
}
},
"now": "Now",
"payment_error": "Something went wrong. Please try again later.",
"select_product_error": "Please select a product"
}

View File

@ -312,43 +312,16 @@
"button": "Continue",
"skip_button": "Skip this offer and proceed further",
"copyright": "© 2025, Wit Lab LLC, California, US",
"products": {
"main_ultra_pack": {
"title": "Ultra Pack",
"subtitle": "3 in 1+2 secret bonus readings",
"discount": "{discount}% OFF",
"price": "Now <price></price> <oldPrice></oldPrice>",
"emoji": "star_struck.webp"
},
"main_numerology_analysis": {
"title": "Relationship plan",
"subtitle": "Discover the future without losing yourself",
"discount": "{discount}% OFF",
"price": "Now <price></price> <oldPrice></oldPrice>",
"emoji": "ring.webp"
},
"main_tarot_reading": {
"title": "Healthy compatibility",
"subtitle": "Balance between closeness and freedom",
"discount": "{discount}% OFF",
"price": "Now <price></price> <oldPrice></oldPrice>",
"emoji": "rose.webp"
},
"main_palmistry_guide": {
"title": "How to talk about feelings",
"subtitle": "Express your emotions and be understood",
"discount": "{discount}% OFF",
"price": "Now <price></price> <oldPrice></oldPrice>",
"emoji": "heart_from_hands.webp"
},
"main_money_reading": {
"title": "How to talk about feelings",
"subtitle": "Express your emotions and be understood",
"discount": "{discount}% OFF",
"price": "Now <price></price> <oldPrice></oldPrice>",
"emoji": "heart_from_hands.webp"
}
}
"now": "Now"
}
},
"Dashboard": {
"adviser": {
"title": "Talk to an Astrologer"
},
"videoGuides": {
"now": "Now",
"purchaseFor": "Buy for {price}"
}
},
"Chat": {

View File

@ -305,43 +305,7 @@
"button": "Continue",
"skip_button": "Skip this offer and proceed further",
"copyright": "© 2025, Wit Lab LLC, California, US",
"products": {
"main_ultra_pack": {
"title": "Ultra Pack",
"subtitle": "3 in 1+2 secret bonus readings",
"discount": "{discount}% OFF",
"price": "Now <price></price> <oldPrice></oldPrice>",
"emoji": "star_struck.webp"
},
"main_numerology_analysis": {
"title": "Relationship plan",
"subtitle": "Discover the future without losing yourself",
"discount": "{discount}% OFF",
"price": "Now <price></price> <oldPrice></oldPrice>",
"emoji": "ring.webp"
},
"main_tarot_reading": {
"title": "Healthy compatibility",
"subtitle": "Balance between closeness and freedom",
"discount": "{discount}% OFF",
"price": "Now <price></price> <oldPrice></oldPrice>",
"emoji": "rose.webp"
},
"main_palmistry_guide": {
"title": "How to talk about feelings",
"subtitle": "Express your emotions and be understood",
"discount": "{discount}% OFF",
"price": "Now <price></price> <oldPrice></oldPrice>",
"emoji": "heart_from_hands.webp"
},
"main_money_reading": {
"title": "How to talk about feelings",
"subtitle": "Express your emotions and be understood",
"discount": "{discount}% OFF",
"price": "Now <price></price> <oldPrice></oldPrice>",
"emoji": "heart_from_hands.webp"
}
},
"now": "Now",
"payment_error": "Something went wrong. Please try again later.",
"select_product_error": "Please select a product"
}

View File

@ -11,6 +11,7 @@ import {
PalmSection,
PalmSectionSkeleton,
PortraitsSection,
VideoGuidesSection,
} from "@/components/domains/dashboard";
import { loadChatsList } from "@/entities/chats/loaders";
import {
@ -19,6 +20,7 @@ import {
loadMeditations,
loadPalms,
loadPortraits,
loadVideoGuides,
} from "@/entities/dashboard/loaders";
import styles from "./page.module.scss";
@ -26,11 +28,14 @@ import styles from "./page.module.scss";
export default async function Home() {
const chatsPromise = loadChatsList();
const portraits = await loadPortraits();
const videoGuides = await loadVideoGuides();
return (
<section className={styles.page}>
<PortraitsSection portraits={portraits} />
<VideoGuidesSection videoGuides={videoGuides} />
<HoroscopeSection />
<Suspense fallback={<AdvisersSectionSkeleton />}>

View File

@ -7,6 +7,25 @@
box-shadow: 0px 0px 30px 0px #0000001f;
gap: 4px;
& > .topBadge {
position: absolute;
top: -15px;
right: 49px;
background: #ff3737;
box-shadow: 0px 1px 11.98px 0px #ff44448c;
border: 2px solid #ffffff4d;
padding: 8px 12px;
border-radius: 9999px;
z-index: 10;
& > .topBadgeText {
color: #fff;
font-size: 14px;
font-weight: 800;
letter-spacing: 0.5px;
}
}
& > .content {
display: grid;
grid-template-columns: 40px 1fr 20px;
@ -109,23 +128,4 @@
}
}
}
&.main_ultra_pack {
& > .footer {
& > .discount {
position: absolute;
top: -15px;
right: 49px;
background: #ff3737;
box-shadow: 0px 1px 11.98px 0px #ff44448c;
border: 2px solid #ffffff4d;
padding: 8px 12px;
color: #fff;
font-size: 14px;
font-weight: 800;
letter-spacing: 0.5px;
margin-right: 0;
}
}
}
}

View File

@ -11,30 +11,25 @@ import styles from "./VideoGuidesOffer.module.scss";
interface VideoGuidesOfferProps {
offer: IFunnelPaymentVariant;
isActive: boolean;
isFirstOffer?: boolean;
className?: string;
onClick: () => void;
}
export default function VideoGuidesOffer(props: VideoGuidesOfferProps) {
const { offer, isActive, className, onClick } = props;
const { offer, isActive, isFirstOffer, className, onClick } = props;
const { key, price, oldPrice } = offer;
const { key, name, description, emoji, price, oldPrice } = offer;
const productKey = key.replaceAll(".", "_");
const t = useTranslations(
`AdditionalPurchases.video-guides.products.${productKey}`
);
const currency = Currency.USD;
const subtitle = t.has("subtitle") ? t("subtitle") : undefined;
const discount = Math.round(
(((oldPrice || 0) - price) / (oldPrice || 0)) * 100
);
const emoji = t.has("emoji") ? t("emoji") : undefined;
const t = useTranslations("AdditionalPurchases.video-guides");
return (
<Card
@ -46,6 +41,11 @@ export default function VideoGuidesOffer(props: VideoGuidesOfferProps) {
)}
onClick={onClick}
>
{isFirstOffer && (
<div className={styles.topBadge}>
<span className={styles.topBadgeText}>{discount}% OFF</span>
</div>
)}
<div className={styles.content}>
<div className={styles.emojiContainer}>
<span
@ -55,11 +55,11 @@ export default function VideoGuidesOffer(props: VideoGuidesOfferProps) {
</div>
<div className={styles.textContainer}>
<Typography as="h5" align="left" className={styles.title}>
{t("title")}
{name}
</Typography>
{subtitle && (
{description && (
<Typography as="p" align="left" className={styles.subtitle}>
{subtitle}
{description}
</Typography>
)}
</div>
@ -76,24 +76,20 @@ export default function VideoGuidesOffer(props: VideoGuidesOfferProps) {
</div>
<div className={styles.footer}>
<Typography className={styles.price}>
{t.rich("price", {
price: () => (
<Typography className={styles.currentPrice}>
{getFormattedPrice(price, currency)}
</Typography>
),
oldPrice: () => (
<Typography className={styles.oldPrice}>
{getFormattedPrice(oldPrice || 0, currency)}
</Typography>
),
})}
</Typography>
<Typography className={styles.discount}>
{t("discount", {
discount: discount || 0,
})}
{t("now")}{" "}
<Typography className={styles.currentPrice}>
{getFormattedPrice(price, currency)}
</Typography>
{" "}
<Typography className={styles.oldPrice}>
{getFormattedPrice(oldPrice || 0, currency)}
</Typography>
</Typography>
{!isFirstOffer && (
<Typography className={styles.discount}>
{discount}% OFF
</Typography>
)}
</div>
</Card>
);

View File

@ -49,11 +49,12 @@ export default function VideoGuidesOffers() {
return (
<div className={styles.container}>
{offers.map(offer => (
{offers.map((offer, index) => (
<VideoGuidesOffer
offer={offer}
key={offer.id}
isActive={activeOffer === offer.id}
isFirstOffer={index === 0}
onClick={() => handleOfferClick(offer)}
/>
))}

View File

@ -0,0 +1,236 @@
.container.container {
display: flex;
min-width: 260px;
min-height: 280px;
flex-direction: column;
align-items: flex-start;
border-radius: 24px;
border: 0 solid #E5E7EB;
background: rgba(0, 0, 0, 0);
box-shadow: 0 4px 6px 0 rgba(0, 0, 0, 0.10), 0 10px 15px 0 rgba(0, 0, 0, 0.10);
cursor: pointer;
overflow: hidden;
padding: 0;
transition: transform 0.2s ease, box-shadow 0.2s ease;
&:hover {
transform: translateY(-4px);
box-shadow: 0 6px 10px 0 rgba(0, 0, 0, 0.12), 0 12px 18px 0 rgba(0, 0, 0, 0.12);
}
}
// Image section
.image {
display: flex;
min-height: 160px;
padding: 16px;
flex-direction: column;
justify-content: center;
align-items: center;
gap: 10px;
align-self: stretch;
border: 0 solid #E5E7EB;
position: relative;
overflow: hidden;
&::before {
content: '';
position: absolute;
inset: 0;
background: lightgray 50% / cover no-repeat;
z-index: 0;
}
.imageContent {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
object-fit: cover;
object-position: center;
pointer-events: none;
z-index: 0;
}
.playIcon {
position: relative;
z-index: 1;
width: 64px;
height: 65px;
svg {
width: 64px;
height: 65px;
filter: drop-shadow(0 2px 12px rgba(0, 0, 0, 0.25));
}
}
}
// Content section
.content {
display: flex;
padding: 16px;
flex-direction: column;
align-items: flex-start;
justify-content: space-between;
gap: 24px;
align-self: stretch;
background: #FFF;
flex: 1;
.purchased & {
gap: 6px;
}
}
// Top section
.top {
display: flex;
align-items: flex-start;
gap: 14px;
align-self: stretch;
.text {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 3px;
flex: 1 0 0;
}
.arrowButton {
display: flex;
width: 40px;
height: 40px;
padding: 12px 0;
justify-content: center;
align-items: center;
border-radius: 9999px;
border: 0 solid #E5E7EB;
background: #F5F5F7;
cursor: pointer;
transition: opacity 0.2s ease;
svg {
width: 8px;
height: 14px;
flex-shrink: 0;
}
&:hover {
opacity: 0.8;
}
}
}
.title {
align-self: stretch;
color: #1D1D1F;
font-family: Inter, sans-serif;
font-size: 20px;
font-style: normal;
font-weight: 500;
line-height: 28px;
text-align: left;
}
.subtitle {
align-self: stretch;
color: #6B7280;
font-family: Inter, sans-serif;
font-size: 16px;
font-style: normal;
font-weight: 400;
line-height: 20px;
text-align: left;
}
// Bottom section
.bottom {
display: flex;
flex-direction: column;
justify-content: flex-end;
align-items: flex-end;
gap: 8px;
align-self: stretch;
}
.bottomText {
display: flex;
height: 24px;
justify-content: space-between;
align-items: center;
align-self: stretch;
}
.duration {
display: flex;
width: 49px;
flex-direction: column;
justify-content: center;
align-self: stretch;
color: #6B7280;
font-family: Inter, sans-serif;
font-size: 16px;
font-style: normal;
font-weight: 400;
line-height: 20px;
}
.discountBadge {
display: flex;
padding: 6px 10px;
justify-content: center;
align-items: center;
gap: 10px;
align-self: stretch;
border-radius: 9999px;
border: 0 solid #E5E7EB;
background: rgba(255, 107, 107, 0.10);
}
.discountText {
color: #FF6B6B;
text-align: center;
font-family: Inter, sans-serif;
font-size: 12px;
font-style: normal;
font-weight: 700;
line-height: normal;
.oldPrice {
color: #8B8B8B;
font-family: Inter, sans-serif;
font-size: 12px;
font-style: normal;
font-weight: 500;
line-height: normal;
text-decoration-line: line-through;
}
}
.buyButton.buyButton {
display: flex;
padding: 8px 10px;
justify-content: center;
align-items: center;
gap: 10px;
border-radius: 12px;
border: 0 solid #E5E7EB;
background: #2563EB;
cursor: pointer;
transition: opacity 0.2s ease;
width: auto;
color: #FFF;
text-align: center;
font-family: Inter, sans-serif;
font-size: 14px;
font-style: normal;
font-weight: 500;
line-height: normal;
&:hover {
opacity: 0.9;
}
}

View File

@ -0,0 +1,102 @@
"use client";
import { useTranslations } from "next-intl";
import clsx from "clsx";
import { Button, Card, Typography } from "@/components/ui";
import { getFormattedPrice } from "@/shared/utils/price";
import { Currency } from "@/types";
import styles from "./VideoGuideCard.module.scss";
interface VideoGuideCardProps {
name: string;
description: string;
imageUrl: string;
duration: string;
price: number;
oldPrice: number;
discount: number;
isPurchased: boolean;
className?: string;
}
export default function VideoGuideCard(props: VideoGuideCardProps) {
const { name, description, imageUrl, duration, price, oldPrice, discount, isPurchased, className } = props;
const tCommon = useTranslations("Dashboard.videoGuides");
const currency = Currency.USD;
const handleClick = () => {
// TODO: Implement navigation or purchase logic
console.log("Video guide clicked", name);
};
return (
<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} />
<div className={styles.playIcon}>
<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"/>
</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>
</defs>
</svg>
</div>
</div>
{/* Content */}
<div className={styles.content}>
{/* Top Section */}
<div className={styles.top}>
<div className={styles.text}>
<Typography as="h4" className={styles.title} align="left">
{name}
</Typography>
<Typography as="p" className={styles.subtitle} align="left">
{description}
</Typography>
</div>
<button className={styles.arrowButton} onClick={handleClick}>
<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>
</div>
{/* Bottom Section */}
<div className={styles.bottom}>
<div className={styles.bottomText}>
<Typography className={styles.duration} align="left">{duration}</Typography>
{!isPurchased && (
<div className={styles.discountBadge}>
<Typography className={styles.discountText}>
{discount}% OFF <span className={styles.oldPrice}>{getFormattedPrice(oldPrice, currency)}</span>
</Typography>
</div>
)}
</div>
{!isPurchased && (
<Button className={styles.buyButton} onClick={handleClick}>
{tCommon("purchaseFor", { price: getFormattedPrice(price, currency) })}
</Button>
)}
</div>
</div>
</Card>
);
}

View File

@ -0,0 +1 @@
export { default as VideoGuideCard } from "./VideoGuideCard";

View File

@ -3,3 +3,4 @@ export { default as CompatibilityCard } from "./CompatibilityCard/CompatibilityC
export { default as MeditationCard } from "./MeditationCard/MeditationCard";
export { default as PalmCard } from "./PalmCard/PalmCard";
export { default as PortraitCard } from "./PortraitCard/PortraitCard";
export { default as VideoGuideCard } from "./VideoGuideCard/VideoGuideCard";

View File

@ -31,7 +31,12 @@ export default function AdvisersSection({
}: AdvisersSectionProps) {
const assistants = use(promiseAssistants);
const chats = use(promiseChats);
const columns = getOptimalColumns(assistants?.length || 0);
if (!assistants || assistants.length === 0) {
return null;
}
const columns = getOptimalColumns(assistants.length);
return (
<Section title="Advisers" contentClassName={styles.sectionContent}>

View File

@ -22,7 +22,12 @@ export default function CompatibilitySection({
gridDisplayMode = "horizontal",
}: CompatibilitySectionProps) {
const compatibilities = use(promise);
const columns = Math.ceil(compatibilities?.length / 2);
if (!compatibilities || compatibilities.length === 0) {
return null;
}
const columns = Math.ceil(compatibilities.length / 2);
return (
<Section title="Compatibility" contentClassName={styles.sectionContent}>

View File

@ -20,7 +20,12 @@ export default function MeditationSection({
gridDisplayMode = "horizontal",
}: MeditationSectionProps) {
const meditations = use(promise);
const columns = meditations?.length;
if (!meditations || meditations.length === 0) {
return null;
}
const columns = meditations.length;
return (
<Section title="Meditations" contentClassName={styles.sectionContent}>

View File

@ -15,7 +15,12 @@ export default function PalmSection({
promise: Promise<Action[]>;
}) {
const palms = use(promise);
const columns = palms?.length;
if (!palms || palms.length === 0) {
return null;
}
const columns = palms.length;
return (
<Section title="Palm" contentClassName={styles.sectionContent}>

View File

@ -0,0 +1,12 @@
.sectionContent.sectionContent {
overflow-x: scroll;
-webkit-overflow-scrolling: touch;
width: calc(100% + 32px);
padding: 20px 16px 24px 16px;
padding-right: 0;
margin: -20px -16px -24px -16px;
}
.grid {
padding-right: 16px;
}

View File

@ -0,0 +1,38 @@
import { Grid, Section } from "@/components/ui";
import { VideoGuide } from "@/entities/dashboard/types";
import { VideoGuideCard } from "../../cards";
import styles from "./VideoGuidesSection.module.scss";
interface VideoGuidesSectionProps {
videoGuides: VideoGuide[];
}
export default function VideoGuidesSection({ videoGuides }: VideoGuidesSectionProps) {
if (!videoGuides || videoGuides.length === 0) {
return null;
}
const columns = videoGuides.length;
return (
<Section title="Video Guides" contentClassName={styles.sectionContent}>
<Grid columns={columns} className={styles.grid}>
{videoGuides.map(videoGuide => (
<VideoGuideCard
key={`video-guide-${videoGuide.id}`}
name={videoGuide.name}
description={videoGuide.description}
imageUrl={videoGuide.imageUrl}
duration={videoGuide.duration}
price={videoGuide.price}
oldPrice={videoGuide.oldPrice}
discount={videoGuide.discount}
isPurchased={videoGuide.isPurchased}
/>
))}
</Grid>
</Section>
);
}

View File

@ -0,0 +1 @@
export { default as VideoGuidesSection } from "./VideoGuidesSection";

View File

@ -20,3 +20,4 @@ export {
PalmSectionSkeleton,
} from "./PalmSection/PalmSection";
export { default as PortraitsSection } from "./PortraitsSection/PortraitsSection";
export { default as VideoGuidesSection } from "./VideoGuidesSection/VideoGuidesSection";

View File

@ -5,15 +5,18 @@ import { getDashboard } from "./api";
export const loadDashboard = cache(getDashboard);
export const loadAssistants = cache(() =>
loadDashboard().then(d => d.assistants)
loadDashboard().then(d => d.assistants || [])
);
export const loadCompatibility = cache(() =>
loadDashboard().then(d => d.compatibilityActions)
loadDashboard().then(d => d.compatibilityActions || [])
);
export const loadMeditations = cache(() =>
loadDashboard().then(d => d.meditations)
loadDashboard().then(d => d.meditations || [])
);
export const loadPalms = cache(() => loadDashboard().then(d => d.palmActions));
export const loadPalms = cache(() => loadDashboard().then(d => d.palmActions || []));
export const loadPortraits = cache(() =>
loadDashboard().then(d => d.partnerPortraits || [])
);
export const loadVideoGuides = cache(() =>
loadDashboard().then(d => d.videoGuides || [])
);

View File

@ -61,12 +61,29 @@ export const PartnerPortraitSchema = z.object({
});
export type PartnerPortrait = z.infer<typeof PartnerPortraitSchema>;
/* ---------- Video Guide ---------- */
export const VideoGuideSchema = z.object({
id: z.string(),
key: z.string(),
type: z.string(),
name: z.string(),
description: z.string(),
imageUrl: z.string(),
duration: z.string(),
price: z.number(),
oldPrice: z.number(),
discount: z.number(),
isPurchased: z.boolean(),
});
export type VideoGuide = z.infer<typeof VideoGuideSchema>;
/* ---------- Итоговый ответ /dashboard ---------- */
export const DashboardSchema = z.object({
assistants: z.array(AssistantSchema),
compatibilityActions: z.array(ActionSchema),
palmActions: z.array(ActionSchema),
meditations: z.array(ActionSchema),
assistants: z.array(AssistantSchema).optional(),
compatibilityActions: z.array(ActionSchema).optional(),
palmActions: z.array(ActionSchema).optional(),
meditations: z.array(ActionSchema).optional(),
partnerPortraits: z.array(PartnerPortraitSchema).optional(),
videoGuides: z.array(VideoGuideSchema).optional(),
});
export type DashboardData = z.infer<typeof DashboardSchema>;

View File

@ -17,6 +17,9 @@ export const FunnelPaymentVariantSchema = z.object({
id: z.string(),
key: z.string(),
type: z.string(),
name: z.string().optional(),
description: z.string().optional(),
emoji: z.string().optional(),
price: z.number(),
oldPrice: z.number().optional(),
trialPrice: z.number().optional(),