- {t("title")}
+ {name}
- {subtitle && (
+ {description && (
- {subtitle}
+ {description}
)}
@@ -76,24 +76,20 @@ export default function VideoGuidesOffer(props: VideoGuidesOfferProps) {
- {t.rich("price", {
- price: () => (
-
- {getFormattedPrice(price, currency)}
-
- ),
- oldPrice: () => (
-
- {getFormattedPrice(oldPrice || 0, currency)}
-
- ),
- })}
-
-
- {t("discount", {
- discount: discount || 0,
- })}
+ {t("now")}{" "}
+
+ {getFormattedPrice(price, currency)}
+
+ {" "}
+
+ {getFormattedPrice(oldPrice || 0, currency)}
+
+ {!isFirstOffer && (
+
+ {discount}% OFF
+
+ )}
);
diff --git a/src/components/domains/additional-purchases/VideoGuidesOffers/VideoGuidesOffers.tsx b/src/components/domains/additional-purchases/VideoGuidesOffers/VideoGuidesOffers.tsx
index 3b972d0..7bc3d3c 100644
--- a/src/components/domains/additional-purchases/VideoGuidesOffers/VideoGuidesOffers.tsx
+++ b/src/components/domains/additional-purchases/VideoGuidesOffers/VideoGuidesOffers.tsx
@@ -49,11 +49,12 @@ export default function VideoGuidesOffers() {
return (
- {offers.map(offer => (
+ {offers.map((offer, index) => (
handleOfferClick(offer)}
/>
))}
diff --git a/src/components/domains/dashboard/cards/VideoGuideCard/VideoGuideCard.module.scss b/src/components/domains/dashboard/cards/VideoGuideCard/VideoGuideCard.module.scss
new file mode 100644
index 0000000..96a3e87
--- /dev/null
+++ b/src/components/domains/dashboard/cards/VideoGuideCard/VideoGuideCard.module.scss
@@ -0,0 +1,236 @@
+.container.container {
+ display: flex;
+ min-width: 260px;
+ min-height: 280px;
+ flex-direction: column;
+ align-items: flex-start;
+ border-radius: 24px;
+ border: 0 solid #E5E7EB;
+ background: rgba(0, 0, 0, 0);
+ box-shadow: 0 4px 6px 0 rgba(0, 0, 0, 0.10), 0 10px 15px 0 rgba(0, 0, 0, 0.10);
+ cursor: pointer;
+ overflow: hidden;
+ padding: 0;
+ transition: transform 0.2s ease, box-shadow 0.2s ease;
+
+ &:hover {
+ transform: translateY(-4px);
+ box-shadow: 0 6px 10px 0 rgba(0, 0, 0, 0.12), 0 12px 18px 0 rgba(0, 0, 0, 0.12);
+ }
+}
+
+// Image section
+.image {
+ display: flex;
+ min-height: 160px;
+ padding: 16px;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+ gap: 10px;
+ align-self: stretch;
+ border: 0 solid #E5E7EB;
+ position: relative;
+ overflow: hidden;
+
+ &::before {
+ content: '';
+ position: absolute;
+ inset: 0;
+ background: lightgray 50% / cover no-repeat;
+ z-index: 0;
+ }
+
+ .imageContent {
+ position: absolute;
+ inset: 0;
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ object-position: center;
+ pointer-events: none;
+ z-index: 0;
+ }
+
+ .playIcon {
+ position: relative;
+ z-index: 1;
+ width: 64px;
+ height: 65px;
+
+ svg {
+ width: 64px;
+ height: 65px;
+ filter: drop-shadow(0 2px 12px rgba(0, 0, 0, 0.25));
+ }
+ }
+}
+
+// Content section
+.content {
+ display: flex;
+ padding: 16px;
+ flex-direction: column;
+ align-items: flex-start;
+ justify-content: space-between;
+ gap: 24px;
+ align-self: stretch;
+ background: #FFF;
+ flex: 1;
+
+ .purchased & {
+ gap: 6px;
+ }
+}
+
+// Top section
+.top {
+ display: flex;
+ align-items: flex-start;
+ gap: 14px;
+ align-self: stretch;
+
+ .text {
+ display: flex;
+ flex-direction: column;
+ align-items: flex-start;
+ gap: 3px;
+ flex: 1 0 0;
+ }
+
+ .arrowButton {
+ display: flex;
+ width: 40px;
+ height: 40px;
+ padding: 12px 0;
+ justify-content: center;
+ align-items: center;
+ border-radius: 9999px;
+ border: 0 solid #E5E7EB;
+ background: #F5F5F7;
+ cursor: pointer;
+ transition: opacity 0.2s ease;
+
+ svg {
+ width: 8px;
+ height: 14px;
+ flex-shrink: 0;
+ }
+
+ &:hover {
+ opacity: 0.8;
+ }
+ }
+}
+
+.title {
+ align-self: stretch;
+ color: #1D1D1F;
+ font-family: Inter, sans-serif;
+ font-size: 20px;
+ font-style: normal;
+ font-weight: 500;
+ line-height: 28px;
+ text-align: left;
+}
+
+.subtitle {
+ align-self: stretch;
+ color: #6B7280;
+ font-family: Inter, sans-serif;
+ font-size: 16px;
+ font-style: normal;
+ font-weight: 400;
+ line-height: 20px;
+ text-align: left;
+}
+
+// Bottom section
+.bottom {
+ display: flex;
+ flex-direction: column;
+ justify-content: flex-end;
+ align-items: flex-end;
+ gap: 8px;
+ align-self: stretch;
+}
+
+.bottomText {
+ display: flex;
+ height: 24px;
+ justify-content: space-between;
+ align-items: center;
+ align-self: stretch;
+}
+
+.duration {
+ display: flex;
+ width: 49px;
+ flex-direction: column;
+ justify-content: center;
+ align-self: stretch;
+ color: #6B7280;
+ font-family: Inter, sans-serif;
+ font-size: 16px;
+ font-style: normal;
+ font-weight: 400;
+ line-height: 20px;
+}
+
+.discountBadge {
+ display: flex;
+ padding: 6px 10px;
+ justify-content: center;
+ align-items: center;
+ gap: 10px;
+ align-self: stretch;
+ border-radius: 9999px;
+ border: 0 solid #E5E7EB;
+ background: rgba(255, 107, 107, 0.10);
+}
+
+.discountText {
+ color: #FF6B6B;
+ text-align: center;
+ font-family: Inter, sans-serif;
+ font-size: 12px;
+ font-style: normal;
+ font-weight: 700;
+ line-height: normal;
+
+ .oldPrice {
+ color: #8B8B8B;
+ font-family: Inter, sans-serif;
+ font-size: 12px;
+ font-style: normal;
+ font-weight: 500;
+ line-height: normal;
+ text-decoration-line: line-through;
+ }
+}
+
+.buyButton.buyButton {
+ display: flex;
+ padding: 8px 10px;
+ justify-content: center;
+ align-items: center;
+ gap: 10px;
+ border-radius: 12px;
+ border: 0 solid #E5E7EB;
+ background: #2563EB;
+ cursor: pointer;
+ transition: opacity 0.2s ease;
+ width: auto;
+
+ color: #FFF;
+ text-align: center;
+ font-family: Inter, sans-serif;
+ font-size: 14px;
+ font-style: normal;
+ font-weight: 500;
+ line-height: normal;
+
+ &:hover {
+ opacity: 0.9;
+ }
+}
diff --git a/src/components/domains/dashboard/cards/VideoGuideCard/VideoGuideCard.tsx b/src/components/domains/dashboard/cards/VideoGuideCard/VideoGuideCard.tsx
new file mode 100644
index 0000000..2361c9a
--- /dev/null
+++ b/src/components/domains/dashboard/cards/VideoGuideCard/VideoGuideCard.tsx
@@ -0,0 +1,102 @@
+"use client";
+
+import { useTranslations } from "next-intl";
+import clsx from "clsx";
+
+import { Button, Card, Typography } from "@/components/ui";
+import { getFormattedPrice } from "@/shared/utils/price";
+import { Currency } from "@/types";
+
+import styles from "./VideoGuideCard.module.scss";
+
+interface VideoGuideCardProps {
+ name: string;
+ description: string;
+ imageUrl: string;
+ duration: string;
+ price: number;
+ oldPrice: number;
+ discount: number;
+ isPurchased: boolean;
+ className?: string;
+}
+
+export default function VideoGuideCard(props: VideoGuideCardProps) {
+ const { name, description, imageUrl, duration, price, oldPrice, discount, isPurchased, className } = props;
+
+ const tCommon = useTranslations("Dashboard.videoGuides");
+
+ const currency = Currency.USD;
+
+ const handleClick = () => {
+ // TODO: Implement navigation or purchase logic
+ console.log("Video guide clicked", name);
+ };
+
+ return (
+
+ {/* Image with Play Icon */}
+
+

+
+
+
+
+
+ {/* Content */}
+
+ {/* Top Section */}
+
+
+
+ {name}
+
+
+ {description}
+
+
+
+
+
+ {/* Bottom Section */}
+
+
+
{duration}
+ {!isPurchased && (
+
+
+ {discount}% OFF {getFormattedPrice(oldPrice, currency)}
+
+
+ )}
+
+ {!isPurchased && (
+
+ )}
+
+
+
+ );
+}
diff --git a/src/components/domains/dashboard/cards/VideoGuideCard/index.ts b/src/components/domains/dashboard/cards/VideoGuideCard/index.ts
new file mode 100644
index 0000000..e1d5d96
--- /dev/null
+++ b/src/components/domains/dashboard/cards/VideoGuideCard/index.ts
@@ -0,0 +1 @@
+export { default as VideoGuideCard } from "./VideoGuideCard";
diff --git a/src/components/domains/dashboard/cards/index.ts b/src/components/domains/dashboard/cards/index.ts
index 65540f0..1a27805 100644
--- a/src/components/domains/dashboard/cards/index.ts
+++ b/src/components/domains/dashboard/cards/index.ts
@@ -3,3 +3,4 @@ export { default as CompatibilityCard } from "./CompatibilityCard/CompatibilityC
export { default as MeditationCard } from "./MeditationCard/MeditationCard";
export { default as PalmCard } from "./PalmCard/PalmCard";
export { default as PortraitCard } from "./PortraitCard/PortraitCard";
+export { default as VideoGuideCard } from "./VideoGuideCard/VideoGuideCard";
diff --git a/src/components/domains/dashboard/sections/AdvisersSection/AdvisersSection.tsx b/src/components/domains/dashboard/sections/AdvisersSection/AdvisersSection.tsx
index addacd3..b083fa3 100644
--- a/src/components/domains/dashboard/sections/AdvisersSection/AdvisersSection.tsx
+++ b/src/components/domains/dashboard/sections/AdvisersSection/AdvisersSection.tsx
@@ -31,7 +31,12 @@ export default function AdvisersSection({
}: AdvisersSectionProps) {
const assistants = use(promiseAssistants);
const chats = use(promiseChats);
- const columns = getOptimalColumns(assistants?.length || 0);
+
+ if (!assistants || assistants.length === 0) {
+ return null;
+ }
+
+ const columns = getOptimalColumns(assistants.length);
return (
diff --git a/src/components/domains/dashboard/sections/CompatibilitySection/CompatibilitySection.tsx b/src/components/domains/dashboard/sections/CompatibilitySection/CompatibilitySection.tsx
index b0455c8..5289ff1 100644
--- a/src/components/domains/dashboard/sections/CompatibilitySection/CompatibilitySection.tsx
+++ b/src/components/domains/dashboard/sections/CompatibilitySection/CompatibilitySection.tsx
@@ -22,7 +22,12 @@ export default function CompatibilitySection({
gridDisplayMode = "horizontal",
}: CompatibilitySectionProps) {
const compatibilities = use(promise);
- const columns = Math.ceil(compatibilities?.length / 2);
+
+ if (!compatibilities || compatibilities.length === 0) {
+ return null;
+ }
+
+ const columns = Math.ceil(compatibilities.length / 2);
return (
diff --git a/src/components/domains/dashboard/sections/MeditationSection/MeditationSection.tsx b/src/components/domains/dashboard/sections/MeditationSection/MeditationSection.tsx
index 45d9638..8e3de49 100644
--- a/src/components/domains/dashboard/sections/MeditationSection/MeditationSection.tsx
+++ b/src/components/domains/dashboard/sections/MeditationSection/MeditationSection.tsx
@@ -20,7 +20,12 @@ export default function MeditationSection({
gridDisplayMode = "horizontal",
}: MeditationSectionProps) {
const meditations = use(promise);
- const columns = meditations?.length;
+
+ if (!meditations || meditations.length === 0) {
+ return null;
+ }
+
+ const columns = meditations.length;
return (
diff --git a/src/components/domains/dashboard/sections/PalmSection/PalmSection.tsx b/src/components/domains/dashboard/sections/PalmSection/PalmSection.tsx
index bffcc40..d6fa71a 100644
--- a/src/components/domains/dashboard/sections/PalmSection/PalmSection.tsx
+++ b/src/components/domains/dashboard/sections/PalmSection/PalmSection.tsx
@@ -15,7 +15,12 @@ export default function PalmSection({
promise: Promise;
}) {
const palms = use(promise);
- const columns = palms?.length;
+
+ if (!palms || palms.length === 0) {
+ return null;
+ }
+
+ const columns = palms.length;
return (
diff --git a/src/components/domains/dashboard/sections/VideoGuidesSection/VideoGuidesSection.module.scss b/src/components/domains/dashboard/sections/VideoGuidesSection/VideoGuidesSection.module.scss
new file mode 100644
index 0000000..d00ff19
--- /dev/null
+++ b/src/components/domains/dashboard/sections/VideoGuidesSection/VideoGuidesSection.module.scss
@@ -0,0 +1,12 @@
+.sectionContent.sectionContent {
+ overflow-x: scroll;
+ -webkit-overflow-scrolling: touch;
+ width: calc(100% + 32px);
+ padding: 20px 16px 24px 16px;
+ padding-right: 0;
+ margin: -20px -16px -24px -16px;
+}
+
+.grid {
+ padding-right: 16px;
+}
diff --git a/src/components/domains/dashboard/sections/VideoGuidesSection/VideoGuidesSection.tsx b/src/components/domains/dashboard/sections/VideoGuidesSection/VideoGuidesSection.tsx
new file mode 100644
index 0000000..a4426b1
--- /dev/null
+++ b/src/components/domains/dashboard/sections/VideoGuidesSection/VideoGuidesSection.tsx
@@ -0,0 +1,38 @@
+import { Grid, Section } from "@/components/ui";
+import { VideoGuide } from "@/entities/dashboard/types";
+
+import { VideoGuideCard } from "../../cards";
+
+import styles from "./VideoGuidesSection.module.scss";
+
+interface VideoGuidesSectionProps {
+ videoGuides: VideoGuide[];
+}
+
+export default function VideoGuidesSection({ videoGuides }: VideoGuidesSectionProps) {
+ if (!videoGuides || videoGuides.length === 0) {
+ return null;
+ }
+
+ const columns = videoGuides.length;
+
+ return (
+
+
+ {videoGuides.map(videoGuide => (
+
+ ))}
+
+
+ );
+}
diff --git a/src/components/domains/dashboard/sections/VideoGuidesSection/index.ts b/src/components/domains/dashboard/sections/VideoGuidesSection/index.ts
new file mode 100644
index 0000000..444247a
--- /dev/null
+++ b/src/components/domains/dashboard/sections/VideoGuidesSection/index.ts
@@ -0,0 +1 @@
+export { default as VideoGuidesSection } from "./VideoGuidesSection";
diff --git a/src/components/domains/dashboard/sections/index.ts b/src/components/domains/dashboard/sections/index.ts
index 989e957..bc600d1 100644
--- a/src/components/domains/dashboard/sections/index.ts
+++ b/src/components/domains/dashboard/sections/index.ts
@@ -20,3 +20,4 @@ export {
PalmSectionSkeleton,
} from "./PalmSection/PalmSection";
export { default as PortraitsSection } from "./PortraitsSection/PortraitsSection";
+export { default as VideoGuidesSection } from "./VideoGuidesSection/VideoGuidesSection";
diff --git a/src/entities/dashboard/loaders.ts b/src/entities/dashboard/loaders.ts
index c1d5fa0..af0a7e2 100644
--- a/src/entities/dashboard/loaders.ts
+++ b/src/entities/dashboard/loaders.ts
@@ -5,15 +5,18 @@ import { getDashboard } from "./api";
export const loadDashboard = cache(getDashboard);
export const loadAssistants = cache(() =>
- loadDashboard().then(d => d.assistants)
+ loadDashboard().then(d => d.assistants || [])
);
export const loadCompatibility = cache(() =>
- loadDashboard().then(d => d.compatibilityActions)
+ loadDashboard().then(d => d.compatibilityActions || [])
);
export const loadMeditations = cache(() =>
- loadDashboard().then(d => d.meditations)
+ loadDashboard().then(d => d.meditations || [])
);
-export const loadPalms = cache(() => loadDashboard().then(d => d.palmActions));
+export const loadPalms = cache(() => loadDashboard().then(d => d.palmActions || []));
export const loadPortraits = cache(() =>
loadDashboard().then(d => d.partnerPortraits || [])
);
+export const loadVideoGuides = cache(() =>
+ loadDashboard().then(d => d.videoGuides || [])
+);
diff --git a/src/entities/dashboard/types.ts b/src/entities/dashboard/types.ts
index 29a784e..ddb413e 100644
--- a/src/entities/dashboard/types.ts
+++ b/src/entities/dashboard/types.ts
@@ -61,12 +61,29 @@ export const PartnerPortraitSchema = z.object({
});
export type PartnerPortrait = z.infer;
+/* ---------- Video Guide ---------- */
+export const VideoGuideSchema = z.object({
+ id: z.string(),
+ key: z.string(),
+ type: z.string(),
+ name: z.string(),
+ description: z.string(),
+ imageUrl: z.string(),
+ duration: z.string(),
+ price: z.number(),
+ oldPrice: z.number(),
+ discount: z.number(),
+ isPurchased: z.boolean(),
+});
+export type VideoGuide = z.infer;
+
/* ---------- Итоговый ответ /dashboard ---------- */
export const DashboardSchema = z.object({
- assistants: z.array(AssistantSchema),
- compatibilityActions: z.array(ActionSchema),
- palmActions: z.array(ActionSchema),
- meditations: z.array(ActionSchema),
+ assistants: z.array(AssistantSchema).optional(),
+ compatibilityActions: z.array(ActionSchema).optional(),
+ palmActions: z.array(ActionSchema).optional(),
+ meditations: z.array(ActionSchema).optional(),
partnerPortraits: z.array(PartnerPortraitSchema).optional(),
+ videoGuides: z.array(VideoGuideSchema).optional(),
});
export type DashboardData = z.infer;
diff --git a/src/entities/session/funnel/types.ts b/src/entities/session/funnel/types.ts
index 3735dda..8bc3cf6 100644
--- a/src/entities/session/funnel/types.ts
+++ b/src/entities/session/funnel/types.ts
@@ -17,6 +17,9 @@ export const FunnelPaymentVariantSchema = z.object({
id: z.string(),
key: z.string(),
type: z.string(),
+ name: z.string().optional(),
+ description: z.string().optional(),
+ emoji: z.string().optional(),
price: z.number(),
oldPrice: z.number().optional(),
trialPrice: z.number().optional(),