From c7eb934462a640e593fa3fd58454457f87fb20d2 Mon Sep 17 00:00:00 2001 From: Mathias Date: Mon, 23 Mar 2026 16:46:36 +0100 Subject: [PATCH] feat: add exercise modal premium teaser with unlock insights and volume features to localization files and exercise video modal, and update related UI components for premium content promotion --- locales/en.ts | 8 ++ locales/es.ts | 7 ++ locales/fr.ts | 7 ++ locales/pt.ts | 7 ++ locales/ru.ts | 7 ++ locales/zh-CN.ts | 7 ++ src/components/ads/custom/sponsor-config.ts | 2 +- src/components/ui/premium-gate.tsx | 2 +- .../premium/ui/feature-comparison-table.tsx | 7 +- .../premium/ui/premium-upgrade-card.tsx | 33 ++---- src/features/premium/ui/pricing-faq.tsx | 2 +- .../components/ExerciseStatisticsTab.tsx | 13 ++- .../workout-builder/ui/exercise-list-item.tsx | 9 +- .../ui/exercise-video-modal.tsx | 101 +++++++++++++++--- .../ui/exercises-selection.tsx | 12 ++- 15 files changed, 164 insertions(+), 60 deletions(-) diff --git a/locales/en.ts b/locales/en.ts index 139f1c6..0e86d0e 100644 --- a/locales/en.ts +++ b/locales/en.ts @@ -1483,6 +1483,14 @@ export default { or: "or", }, + // Exercise modal premium teaser + exercise_modal: { + pr_title: "Personal Record Trend", + unlock_insights: "Unlock Advanced Insights", + feature_pr: "Personal records", + feature_volume: "Volume tracking", + }, + // Contact Support contact_support: "Contact Support", contact_support_subtitle: "Describe your issue and we'll help you as soon as possible. You can also write to us directly at", diff --git a/locales/es.ts b/locales/es.ts index 4438dcc..77681f8 100644 --- a/locales/es.ts +++ b/locales/es.ts @@ -616,6 +616,13 @@ export default { }, // Contact Support + exercise_modal: { + pr_title: "Tendencia de récords personales", + unlock_insights: "Desbloquear análisis avanzados", + feature_pr: "Récords personales", + feature_volume: "Seguimiento de volumen", + }, + contact_support: "Contactar soporte", contact_support_subtitle: "Describe tu problema y te ayudaremos lo antes posible. También puedes escribirnos directamente a", diff --git a/locales/fr.ts b/locales/fr.ts index 1899a54..81403f1 100644 --- a/locales/fr.ts +++ b/locales/fr.ts @@ -1509,6 +1509,13 @@ export default { or: "ou", }, + exercise_modal: { + pr_title: "Tendance des records personnels", + unlock_insights: "Débloquer les analyses avancées", + feature_pr: "Records personnels", + feature_volume: "Suivi du volume", + }, + // Contact Support contact_support: "Contacter le support", contact_support_subtitle: "Décrivez votre problème et nous vous aiderons dès que possible. Vous pouvez aussi nous écrire directement à", diff --git a/locales/pt.ts b/locales/pt.ts index 3b30d7c..377d565 100644 --- a/locales/pt.ts +++ b/locales/pt.ts @@ -1490,6 +1490,13 @@ export default { }, // Contact Support + exercise_modal: { + pr_title: "Tendência de recordes pessoais", + unlock_insights: "Desbloquear análises avançadas", + feature_pr: "Recordes pessoais", + feature_volume: "Rastreamento de volume", + }, + contact_support: "Contactar Suporte", contact_support_subtitle: "Descreva o seu problema e iremos ajudá-lo o mais rápido possível. Também pode escrever-nos diretamente para", diff --git a/locales/ru.ts b/locales/ru.ts index 83df62b..b417f93 100644 --- a/locales/ru.ts +++ b/locales/ru.ts @@ -1476,6 +1476,13 @@ export default { }, // Contact Support + exercise_modal: { + pr_title: "Тренд личных рекордов", + unlock_insights: "Разблокировать продвинутую аналитику", + feature_pr: "Личные рекорды", + feature_volume: "Отслеживание объёма", + }, + contact_support: "Связаться с поддержкой", contact_support_subtitle: "Опишите вашу проблему, и мы поможем вам как можно скорее. Вы также можете написать нам напрямую на", diff --git a/locales/zh-CN.ts b/locales/zh-CN.ts index faa5d93..efdc1fe 100644 --- a/locales/zh-CN.ts +++ b/locales/zh-CN.ts @@ -612,6 +612,13 @@ export default { }, // Contact Support + exercise_modal: { + pr_title: "个人记录趋势", + unlock_insights: "解锁高级分析", + feature_pr: "个人记录", + feature_volume: "训练量追踪", + }, + contact_support: "联系支持", contact_support_subtitle: "描述您的问题,我们将尽快帮助您。您也可以直接写信给我们:", diff --git a/src/components/ads/custom/sponsor-config.ts b/src/components/ads/custom/sponsor-config.ts index acd2ac5..f47f583 100644 --- a/src/components/ads/custom/sponsor-config.ts +++ b/src/components/ads/custom/sponsor-config.ts @@ -36,7 +36,7 @@ const sponsors: Record = { name: "Nutri&Co", descriptionKey: "ads.sponsor_nutri_and_co", logoUrl: "/images/sponsorship/nutri-and-co.png", - url: "https://www.nutri-and-co.com", + url: "https://nutriandco.com/", brandColor: "#1A2F3B", }, }; diff --git a/src/components/ui/premium-gate.tsx b/src/components/ui/premium-gate.tsx index 8139c5a..c623486 100644 --- a/src/components/ui/premium-gate.tsx +++ b/src/components/ui/premium-gate.tsx @@ -3,8 +3,8 @@ import React from "react"; import Link from "next/link"; import { Crown, Sparkles } from "lucide-react"; - import { useI18n } from "locales/client"; + import { cn } from "@/shared/lib/utils"; import { useUserSubscription } from "@/features/ads/hooks/useUserSubscription"; import { Skeleton } from "@/components/ui/skeleton"; diff --git a/src/features/premium/ui/feature-comparison-table.tsx b/src/features/premium/ui/feature-comparison-table.tsx index 83bb63d..5c7e343 100644 --- a/src/features/premium/ui/feature-comparison-table.tsx +++ b/src/features/premium/ui/feature-comparison-table.tsx @@ -1,7 +1,6 @@ "use client"; import { Check, X, Star, Target } from "lucide-react"; - import { useI18n } from "locales/client"; interface Feature { @@ -172,7 +171,7 @@ export function FeatureComparisonTable() {
{t("premium.plans.free.name")}
{t("premium.plans.free.price_label")}
-
+
{t("premium.plans.premium.name")}
{t("premium.plans.premium.price_label")}
@@ -190,7 +189,9 @@ export function FeatureComparisonTable() {
{feature.name}
{renderFeatureValue(feature.free)}
-
{renderFeatureValue(feature.premium)}
+
+ {renderFeatureValue(feature.premium)} +
))}
diff --git a/src/features/premium/ui/premium-upgrade-card.tsx b/src/features/premium/ui/premium-upgrade-card.tsx index e25c952..39262cb 100644 --- a/src/features/premium/ui/premium-upgrade-card.tsx +++ b/src/features/premium/ui/premium-upgrade-card.tsx @@ -4,15 +4,8 @@ import { useState, useEffect } from "react"; import { useRouter } from "next/navigation"; import Image from "next/image"; import { Crown, Zap, Heart, Check, ArrowRight, LogIn, Github, Users, RefreshCw, Lock, ShieldCheck, GiftIcon } from "lucide-react"; -import { useMutation } from "@tanstack/react-query"; - import { useI18n, useCurrentLocale } from "locales/client"; -import { usePremiumRedirect } from "@/shared/lib/premium/use-premium-redirect"; -import { useIsPremium } from "@/shared/lib/premium/use-premium"; -import { usePendingCheckout } from "@/shared/lib/premium/use-pending-checkout"; -import { usePremiumPlans } from "@/shared/hooks/use-premium-plans"; -import { useSession } from "@/features/auth/lib/auth-client"; -import { Button } from "@/components/ui/button"; +import { useMutation } from "@tanstack/react-query"; import { PricingHeroSection } from "./pricing-hero-section"; import { PricingFAQ } from "./pricing-faq"; @@ -21,6 +14,13 @@ import { ConversionFlowNotification } from "./conversion-flow-notification"; import type { CheckoutResult } from "@/shared/types/premium.types"; +import { usePremiumRedirect } from "@/shared/lib/premium/use-premium-redirect"; +import { useIsPremium } from "@/shared/lib/premium/use-premium"; +import { usePendingCheckout } from "@/shared/lib/premium/use-pending-checkout"; +import { usePremiumPlans } from "@/shared/hooks/use-premium-plans"; +import { useSession } from "@/features/auth/lib/auth-client"; +import { Button } from "@/components/ui/button"; + export function PremiumUpgradeCard() { const t = useI18n(); const locale = useCurrentLocale(); @@ -35,7 +35,6 @@ export function PremiumUpgradeCard() { // Fetch dynamic pricing const { data: plansData, isLoading: plansLoading } = usePremiumPlans(); - console.log("plansData:", plansData); // Handle premium redirects after successful upgrade usePremiumRedirect(); @@ -80,8 +79,6 @@ export function PremiumUpgradeCard() { }; const handleUpgrade = (planId: string) => { - console.log("planId:", planId); - // Check if user is authenticated if (!isAuthenticated) { // Store the selected plan for after authentication @@ -102,11 +99,8 @@ export function PremiumUpgradeCard() { const yearlyPlan = plansData?.plans.find((p) => p.internalId.startsWith("premium-yearly")); const monthlyPrice = monthlyPlan?.priceMonthly || 7.9; - console.log("monthlyPrice:", monthlyPrice); const yearlyPrice = yearlyPlan?.priceYearly || 49.0; - console.log("yearlyPrice:", yearlyPrice); const currency = monthlyPlan?.currency || "EUR"; - console.log("currency:", currency); const currentPrice = isYearly ? yearlyPrice : monthlyPrice; const currentPeriod = isYearly ? t("premium.pricing.year") : t("premium.pricing.month"); @@ -122,17 +116,6 @@ export function PremiumUpgradeCard() { }).format(price); }; - // Log debug info in development - useEffect(() => { - if (plansData && process.env.NODE_ENV === "development") { - console.log("📊 Plans data:", plansData); - console.log("🌍 Detected region:", plansData.detectedRegion); - if (plansData.debug) { - console.log("🔍 Debug headers:", plansData.debug.headers); - } - } - }, [plansData]); - if (isPremium) { return (
diff --git a/src/features/premium/ui/pricing-faq.tsx b/src/features/premium/ui/pricing-faq.tsx index a9cd693..b508793 100644 --- a/src/features/premium/ui/pricing-faq.tsx +++ b/src/features/premium/ui/pricing-faq.tsx @@ -72,7 +72,7 @@ export function PricingFAQ() { key={index} > @@ -192,7 +193,7 @@ export const ExerciseListItem = React.memo(function ExerciseListItem({ - {exercise.fullVideoUrl && } + {exercise.fullVideoUrl && } )}
diff --git a/src/features/workout-builder/ui/exercise-video-modal.tsx b/src/features/workout-builder/ui/exercise-video-modal.tsx index f2b2966..383c82f 100644 --- a/src/features/workout-builder/ui/exercise-video-modal.tsx +++ b/src/features/workout-builder/ui/exercise-video-modal.tsx @@ -1,11 +1,13 @@ -import { useState } from "react"; -import { BarChart3, Play } from "lucide-react"; +import { useEffect, useState } from "react"; +import Link from "next/link"; +import { BarChart3, Crown, Lock, Play, TrendingUp, Zap } from "lucide-react"; import { useCurrentLocale, useI18n } from "locales/client"; import { ExerciseAttributeNameEnum, ExerciseAttributeValueEnum } from "@prisma/client"; import type { ExerciseWithAttributes } from "@/entities/exercise/types/exercise.types"; import { getYouTubeEmbedUrl } from "@/shared/lib/youtube"; +import { useIsPremium } from "@/shared/lib/premium/use-premium"; import { getAttributeValueLabel } from "@/shared/lib/attribute-value-translation"; import { StatisticsTimeframe } from "@/shared/constants/statistics"; import { ExerciseCharts } from "@/features/statistics/components/ExerciseStatisticsTab"; @@ -18,13 +20,19 @@ interface ExerciseVideoModalProps { open: boolean; onOpenChange: (open: boolean) => void; exercise: ExerciseWithAttributes; + defaultTab?: "video" | "statistics"; } -export function ExerciseVideoModal({ open, onOpenChange, exercise }: ExerciseVideoModalProps) { +export function ExerciseVideoModal({ open, onOpenChange, exercise, defaultTab = "video" }: ExerciseVideoModalProps) { const t = useI18n(); const locale = useCurrentLocale(); - const [activeTab, setActiveTab] = useState("video"); + const [activeTab, setActiveTab] = useState(defaultTab); const [selectedTimeframe, setSelectedTimeframe] = useState("8weeks"); + const isPremium = useIsPremium(); + + useEffect(() => { + if (open) setActiveTab(defaultTab); + }, [open, defaultTab]); const title = locale === "fr" ? exercise.name : exercise.nameEn || exercise.name; const introduction = locale === "fr" ? exercise.introduction : exercise.introductionEn || exercise.introduction; @@ -70,7 +78,7 @@ export function ExerciseVideoModal({ open, onOpenChange, exercise }: ExerciseVid - + {/* @ts-expect-error Tabs shadcn */} @@ -121,16 +129,83 @@ export function ExerciseVideoModal({ open, onOpenChange, exercise }: ExerciseVid )} - -
- {/* Timeframe selector */} -
-

{t("statistics.performance_over_time")}

- + +
+ {/* Header — title + timeframe aligned */} +
+
+
+ +
+

+ {t("statistics.performance_over_time")} +

+
+
- {/* Charts */} - + {/* Charts in card */} +
+ +
+ + {/* Premium teaser — Blurred advanced insights (loss aversion + Zeigarnik effect) */} + {!isPremium && ( +
+ {/* Blurred fake insights */} +
+
+
+ + {t("exercise_modal.pr_title")} +
+ +12% +
+
+
+
+
+
+

1RM Est.

+

85kg

+
+
+

Volume

+

2,400kg

+
+
+

Sessions

+

24

+
+
+
+ + {/* Overlay CTA */} +
+ +
+
+ +
+ {t("exercise_modal.unlock_insights")} +
+
+ + + {t("exercise_modal.feature_pr")} + + + + {t("exercise_modal.feature_volume")} + +
+ +
+
+ )}
diff --git a/src/features/workout-builder/ui/exercises-selection.tsx b/src/features/workout-builder/ui/exercises-selection.tsx index f8d71e4..ca98e13 100644 --- a/src/features/workout-builder/ui/exercises-selection.tsx +++ b/src/features/workout-builder/ui/exercises-selection.tsx @@ -1,7 +1,8 @@ -import { useState, useEffect, useCallback, useMemo } from "react"; +import { useState, useEffect, useCallback, useMemo, useRef } from "react"; import { Loader2, Plus } from "lucide-react"; import { useI18n } from "locales/client"; -import { arrayMove, SortableContext, verticalListSortingStrategy } from "@dnd-kit/sortable"; +import { arrayMove, SortableContext } from "@dnd-kit/sortable"; +import { restrictToVerticalAxis, restrictToParentElement } from "@dnd-kit/modifiers"; import { DndContext, DragOverlay, @@ -14,7 +15,6 @@ import { DragOverEvent, MouseSensor, } from "@dnd-kit/core"; -import { restrictToVerticalAxis, restrictToParentElement } from "@dnd-kit/modifiers"; import { useWorkoutStepper } from "../hooks/use-workout-stepper"; import { ExerciseListItem, ExerciseListItemOverlay } from "./exercise-list-item"; @@ -51,6 +51,8 @@ export const ExercisesSelection = ({ const { setExercisesOrder, exercisesOrder } = useWorkoutStepper(); const feedback = useDragFeedback(); const [activeId, setActiveId] = useState(null); + const isMobile = useRef(typeof window !== "undefined" && window.innerWidth < 640); + const modifiers = isMobile.current ? [restrictToVerticalAxis] : [restrictToVerticalAxis, restrictToParentElement]; const sensors = useSensors( useSensor(PointerSensor, { @@ -149,14 +151,14 @@ export const ExercisesSelection = ({ {/* Liste des exercices drag and drop */} - +
{flatExercises.map((item) => (