mirror of
https://github.com/Snouzy/workout-cool.git
synced 2026-05-19 14:40:35 +00:00
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
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
|
||||
@@ -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 à",
|
||||
|
||||
@@ -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",
|
||||
|
||||
|
||||
@@ -1476,6 +1476,13 @@ export default {
|
||||
},
|
||||
|
||||
// Contact Support
|
||||
exercise_modal: {
|
||||
pr_title: "Тренд личных рекордов",
|
||||
unlock_insights: "Разблокировать продвинутую аналитику",
|
||||
feature_pr: "Личные рекорды",
|
||||
feature_volume: "Отслеживание объёма",
|
||||
},
|
||||
|
||||
contact_support: "Связаться с поддержкой",
|
||||
contact_support_subtitle: "Опишите вашу проблему, и мы поможем вам как можно скорее. Вы также можете написать нам напрямую на",
|
||||
|
||||
|
||||
@@ -612,6 +612,13 @@ export default {
|
||||
},
|
||||
|
||||
// Contact Support
|
||||
exercise_modal: {
|
||||
pr_title: "个人记录趋势",
|
||||
unlock_insights: "解锁高级分析",
|
||||
feature_pr: "个人记录",
|
||||
feature_volume: "训练量追踪",
|
||||
},
|
||||
|
||||
contact_support: "联系支持",
|
||||
contact_support_subtitle: "描述您的问题,我们将尽快帮助您。您也可以直接写信给我们:",
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ const sponsors: Record<string, Sponsor> = {
|
||||
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",
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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() {
|
||||
<div className="font-bold text-gray-900 dark:text-white">{t("premium.plans.free.name")}</div>
|
||||
<div className="text-sm text-gray-500">{t("premium.plans.free.price_label")}</div>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<div className="text-center bg-[#00D4AA]/5 dark:bg-[#00D4AA]/10 rounded-lg p-2 -m-2">
|
||||
<div className="font-bold text-[#00D4AA]">{t("premium.plans.premium.name")}</div>
|
||||
<div className="text-sm text-gray-500">{t("premium.plans.premium.price_label")}</div>
|
||||
</div>
|
||||
@@ -190,7 +189,9 @@ export function FeatureComparisonTable() {
|
||||
<div className="grid grid-cols-3 gap-4 items-center py-2" key={featureIndex}>
|
||||
<div className="text-xs sm:text-sm text-gray-700 dark:text-gray-300">{feature.name}</div>
|
||||
<div className="text-center">{renderFeatureValue(feature.free)}</div>
|
||||
<div className="text-center">{renderFeatureValue(feature.premium)}</div>
|
||||
<div className="text-center bg-[#00D4AA]/5 dark:bg-[#00D4AA]/10 rounded-lg py-1">
|
||||
{renderFeatureValue(feature.premium)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -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 (
|
||||
<div className="m-3 relative overflow-hidden bg-gradient-to-b from-[#FF6B35]/5 to-[#00D4AA]/5 dark:from-[#FF6B35]/10 dark:to-[#00D4AA]/10 rounded-3xl p-8 border border-[#FF6B35]/20 dark:border-[#FF6B35]/30">
|
||||
|
||||
@@ -72,7 +72,7 @@ export function PricingFAQ() {
|
||||
key={index}
|
||||
>
|
||||
<button
|
||||
className="w-full px-6 py-4 text-left flex items-center justify-between hover:bg-gray-400 dark:hover:bg-gray-800 transition-colors duration-200"
|
||||
className="w-full px-6 py-4 text-left flex items-center justify-between hover:bg-slate-200 dark:hover:bg-gray-800 transition-colors duration-200"
|
||||
onClick={() => toggleFAQ(index)}
|
||||
>
|
||||
<span className="text-lg sm:text-xl leading-tight font-semibold text-gray-900 dark:text-white pr-4">{item.question}</span>
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import { AlertCircle } from "lucide-react";
|
||||
|
||||
import { useI18n, useCurrentLocale } from "locales/client";
|
||||
|
||||
import { useWeightProgression, useOneRepMax, useVolumeData } from "../hooks/use-exercise-statistics";
|
||||
import { WeightProgressionChart } from "./WeightProgressionChart";
|
||||
import { VolumeChart } from "./VolumeChart";
|
||||
import { OneRepMaxChart } from "./OneRepMaxChart";
|
||||
|
||||
import { cn } from "@/shared/lib/utils";
|
||||
import { formatDate } from "@/shared/lib/date";
|
||||
import { StatisticsTimeframe } from "@/shared/constants/statistics";
|
||||
@@ -11,11 +15,6 @@ import { PremiumGate } from "@/components/ui/premium-gate";
|
||||
import { Loader } from "@/components/ui/loader";
|
||||
import { Alert, AlertDescription } from "@/components/ui/alert";
|
||||
|
||||
import { useWeightProgression, useOneRepMax, useVolumeData } from "../hooks/use-exercise-statistics";
|
||||
import { WeightProgressionChart } from "./WeightProgressionChart";
|
||||
import { VolumeChart } from "./VolumeChart";
|
||||
import { OneRepMaxChart } from "./OneRepMaxChart";
|
||||
|
||||
interface ExerciseStatisticsTabProps {
|
||||
exerciseId: string;
|
||||
unit?: "kg" | "lbs";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useCallback } from "react";
|
||||
import React, { useCallback, useState } from "react";
|
||||
import Image from "next/image";
|
||||
import { Play, Shuffle, Trash2, GripVertical, Loader2, BarChart3 } from "lucide-react";
|
||||
import { useCurrentLocale, useI18n } from "locales/client";
|
||||
@@ -83,6 +83,7 @@ export const ExerciseListItem = React.memo(function ExerciseListItem({
|
||||
const t = useI18n();
|
||||
const locale = useCurrentLocale();
|
||||
const playVideo = useBoolean();
|
||||
const [modalTab, setModalTab] = useState<"video" | "statistics">("video");
|
||||
|
||||
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
|
||||
id: exercise.id,
|
||||
@@ -143,7 +144,7 @@ export const ExerciseListItem = React.memo(function ExerciseListItem({
|
||||
{exercise.fullVideoImageUrl && (
|
||||
<div
|
||||
className="relative h-8 w-8 sm:h-10 sm:w-10 rounded overflow-hidden shrink-0 bg-slate-200 dark:bg-slate-800 cursor-pointer border border-slate-200 dark:border-slate-700/50"
|
||||
onClick={playVideo.setTrue}
|
||||
onClick={() => { setModalTab("video"); playVideo.setTrue(); }}
|
||||
>
|
||||
<Image
|
||||
alt={exerciseName ?? ""}
|
||||
@@ -183,7 +184,7 @@ export const ExerciseListItem = React.memo(function ExerciseListItem({
|
||||
|
||||
<button
|
||||
className="p-1 sm:p-2 text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300"
|
||||
onClick={playVideo.setTrue}
|
||||
onClick={() => { setModalTab("statistics"); playVideo.setTrue(); }}
|
||||
>
|
||||
<BarChart3 className="h-4 w-4" />
|
||||
</button>
|
||||
@@ -192,7 +193,7 @@ export const ExerciseListItem = React.memo(function ExerciseListItem({
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</button>
|
||||
|
||||
{exercise.fullVideoUrl && <ExerciseVideoModal exercise={exercise} onOpenChange={playVideo.toggle} open={playVideo.value} />}
|
||||
{exercise.fullVideoUrl && <ExerciseVideoModal defaultTab={modalTab} exercise={exercise} onOpenChange={playVideo.toggle} open={playVideo.value} />}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -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<StatisticsTimeframe>("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
|
||||
</div>
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
{/* @ts-expect-error Tabs shadcn */}
|
||||
<Tabs className="flex-1" onValueChange={setActiveTab} value={activeTab}>
|
||||
<TabsList className="grid w-full grid-cols-2 mx-4" style={{ width: "calc(100% - 2rem)" }}>
|
||||
<TabsTrigger className="flex items-center gap-2" value="video">
|
||||
@@ -121,16 +129,83 @@ export function ExerciseVideoModal({ open, onOpenChange, exercise }: ExerciseVid
|
||||
)}
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent className="mt-0 px-2 md:px-6 pt-4 pb-6" value="statistics">
|
||||
<div className="space-y-4">
|
||||
{/* Timeframe selector */}
|
||||
<div className="flex items-center justify-between flex-col sm:flex-row">
|
||||
<h3 className="text-lg font-semibold text-slate-700 dark:text-slate-200">{t("statistics.performance_over_time")}</h3>
|
||||
<TimeframeSelector onSelect={setSelectedTimeframe} selected={selectedTimeframe} />
|
||||
<TabsContent className="mt-0" value="statistics">
|
||||
<div className="space-y-5 px-3 md:px-6 pt-4 pb-6">
|
||||
{/* Header — title + timeframe aligned */}
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div className="flex items-center gap-2 min-w-0">
|
||||
<div className="w-8 h-8 rounded-lg bg-blue-100 dark:bg-blue-900/40 flex items-center justify-center shrink-0">
|
||||
<BarChart3 className="w-4 h-4 text-blue-600 dark:text-blue-400" />
|
||||
</div>
|
||||
<h3 className="text-base md:text-lg font-semibold text-slate-800 dark:text-slate-200 truncate">
|
||||
{t("statistics.performance_over_time")}
|
||||
</h3>
|
||||
</div>
|
||||
<TimeframeSelector className="shrink-0" onSelect={setSelectedTimeframe} selected={selectedTimeframe} />
|
||||
</div>
|
||||
|
||||
{/* Charts */}
|
||||
<ExerciseCharts exerciseId={exercise.id} timeframe={selectedTimeframe} />
|
||||
{/* Charts in card */}
|
||||
<div className="rounded-xl border border-slate-200 dark:border-slate-700/50 bg-white dark:bg-slate-800/30 p-3 md:p-4">
|
||||
<ExerciseCharts exerciseId={exercise.id} timeframe={selectedTimeframe} />
|
||||
</div>
|
||||
|
||||
{/* Premium teaser — Blurred advanced insights (loss aversion + Zeigarnik effect) */}
|
||||
{!isPremium && (
|
||||
<div className="relative rounded-xl border border-dashed border-amber-300 dark:border-amber-700/50 overflow-hidden">
|
||||
{/* Blurred fake insights */}
|
||||
<div className="select-none pointer-events-none blur-[6px] opacity-50 p-4 space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<TrendingUp className="w-4 h-4 text-emerald-500" />
|
||||
<span className="text-sm font-semibold text-slate-700 dark:text-slate-300">{t("exercise_modal.pr_title")}</span>
|
||||
</div>
|
||||
<span className="text-lg font-bold text-emerald-500">+12%</span>
|
||||
</div>
|
||||
<div className="h-2 rounded-full bg-slate-200 dark:bg-slate-700">
|
||||
<div className="h-2 rounded-full bg-gradient-to-r from-emerald-400 to-emerald-600 w-3/4" />
|
||||
</div>
|
||||
<div className="grid grid-cols-3 gap-2">
|
||||
<div className="rounded-lg bg-slate-100 dark:bg-slate-800 p-2.5 text-center">
|
||||
<p className="text-[10px] text-slate-400 uppercase tracking-wide">1RM Est.</p>
|
||||
<p className="font-bold text-slate-700 dark:text-slate-200">85kg</p>
|
||||
</div>
|
||||
<div className="rounded-lg bg-slate-100 dark:bg-slate-800 p-2.5 text-center">
|
||||
<p className="text-[10px] text-slate-400 uppercase tracking-wide">Volume</p>
|
||||
<p className="font-bold text-slate-700 dark:text-slate-200">2,400kg</p>
|
||||
</div>
|
||||
<div className="rounded-lg bg-slate-100 dark:bg-slate-800 p-2.5 text-center">
|
||||
<p className="text-[10px] text-slate-400 uppercase tracking-wide">Sessions</p>
|
||||
<p className="font-bold text-slate-700 dark:text-slate-200">24</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Overlay CTA */}
|
||||
<div className="absolute inset-0 flex items-center justify-center bg-white/30 dark:bg-slate-900/30">
|
||||
<Link
|
||||
className=" flex flex-col items-center gap-2.5 px-6 py-4 rounded-2xl bg-white/95 dark:bg-slate-900/95 backdrop-blur-sm border border-amber-200 dark:border-amber-800 shadow-xl shadow-amber-500/10 hover:shadow-amber-500/25 transition-all duration-200 hover:scale-[1.02]"
|
||||
href="/premium"
|
||||
>
|
||||
<div className="flex items-center gap-2.5">
|
||||
<div className="w-9 h-9 rounded-full bg-gradient-to-br from-amber-400 to-orange-500 flex items-center justify-center shadow-lg shadow-amber-500/30">
|
||||
<Crown className="w-4.5 h-4.5 text-white" />
|
||||
</div>
|
||||
<span className="font-bold text-sm text-slate-800 dark:text-slate-100">{t("exercise_modal.unlock_insights")}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-3 text-xs text-slate-500 dark:text-slate-400">
|
||||
<span className="flex items-center gap-1">
|
||||
<Zap className="w-3 h-3 text-amber-500" />
|
||||
{t("exercise_modal.feature_pr")}
|
||||
</span>
|
||||
<span className="flex items-center gap-1">
|
||||
<Lock className="w-3 h-3 text-amber-500" />
|
||||
{t("exercise_modal.feature_volume")}
|
||||
</span>
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
|
||||
@@ -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<string | null>(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 */}
|
||||
<DndContext
|
||||
collisionDetection={closestCenter}
|
||||
modifiers={[restrictToVerticalAxis, restrictToParentElement]}
|
||||
modifiers={modifiers}
|
||||
onDragCancel={handleDragCancel}
|
||||
onDragEnd={handleDragEnd}
|
||||
onDragOver={handleDragOver}
|
||||
onDragStart={handleDragStart}
|
||||
sensors={sensors}
|
||||
>
|
||||
<SortableContext items={sortableItems} strategy={verticalListSortingStrategy}>
|
||||
<SortableContext items={sortableItems}>
|
||||
<div className="bg-white dark:bg-slate-900 rounded-t-lg border border-b-0 border-slate-200 dark:border-slate-800 overflow-hidden">
|
||||
{flatExercises.map((item) => (
|
||||
<ExerciseListItem
|
||||
|
||||
Reference in New Issue
Block a user