Pārlūkot izejas kodu

Feature/exercise video modal (#9)

* feature: exercise video modal

* feature: youtube embed autoplay

* feature: exercise video modal review
Lucas Neves Pereira 1 mēnesi atpakaļ
vecāks
revīzija
83223c70da

+ 0 - 1
docker-compose.yml

@@ -1,7 +1,6 @@
 services:
   postgres:
     image: postgres:15
-    restart: unless-stopped
     ports:
       - "5432:5432"
     environment:

+ 1 - 0
locales/en.ts

@@ -82,6 +82,7 @@ export default {
       shuffle: "Shuffle",
       pick: "Pick",
       remove: "Remove",
+      no_video_available: "No video available.",
     },
     loading: {
       exercises: "Loading exercises...",

+ 1 - 0
locales/fr.ts

@@ -82,6 +82,7 @@ export default {
       shuffle: "Mélanger",
       pick: "Choisir",
       remove: "Supprimer",
+      no_video_available: "Aucune vidéo disponible.",
     },
     loading: {
       exercises: "Chargement des exercices...",

+ 28 - 3
src/features/workout-builder/ui/exercise-card.tsx

@@ -4,13 +4,15 @@ import { useState } from "react";
 import Image from "next/image";
 import { Play, Shuffle, MoreVertical, Trash2, Info, Target } from "lucide-react";
 
-import { useI18n } from "locales/client";
+import { useCurrentLocale, useI18n } from "locales/client";
 import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
 import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu";
 import { Card, CardContent, CardHeader } from "@/components/ui/card";
 import { Button } from "@/components/ui/button";
 import { Badge } from "@/components/ui/badge";
 
+import { ExerciseVideoModal } from "./exercise-video-modal";
+
 import type { ExerciseWithAttributes } from "../types";
 
 interface ExerciseCardProps {
@@ -23,7 +25,9 @@ interface ExerciseCardProps {
 
 export function ExerciseCard({ exercise, muscle, onShuffle, onPick, onDelete }: ExerciseCardProps) {
   const t = useI18n();
+  const locale = useCurrentLocale();
   const [imageError, setImageError] = useState(false);
+  const [showVideo, setShowVideo] = useState(false);
 
   // Extraire les attributs utiles
   const equipmentAttributes =
@@ -34,6 +38,13 @@ export function ExerciseCard({ exercise, muscle, onShuffle, onPick, onDelete }:
 
   const mechanicsType = exercise.attributes?.find((attr) => attr.attributeName.name === "MECHANICS_TYPE")?.attributeValue.value;
 
+  const exerciseName = locale === "fr" ? exercise.name : exercise.nameEn
+
+  const handlePlayVideo = () => {
+    setShowVideo(true);
+  };
+
+
   return (
     <TooltipProvider>
       <Card className="group relative overflow-hidden bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 hover:shadow-lg transition-all duration-200 hover:border-blue-200 dark:hover:border-blue-800">
@@ -51,7 +62,12 @@ export function ExerciseCard({ exercise, muscle, onShuffle, onPick, onDelete }:
                   src={exercise.fullVideoImageUrl}
                 />
                 <div className="absolute inset-0 bg-black/20 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center">
-                  <Button className="bg-white/90 text-slate-900" size="small" variant="secondary">
+                  <Button
+                    className="bg-white/90 text-slate-900"
+                    onClick={handlePlayVideo}
+                    size="small"
+                    variant="secondary"
+                  >
                     <Play className="h-4 w-4 mr-2" />
                     {t("workout_builder.exercise.watch_video")}
                   </Button>
@@ -166,6 +182,15 @@ export function ExerciseCard({ exercise, muscle, onShuffle, onPick, onDelete }:
           </div>
         </CardContent>
       </Card>
-    </TooltipProvider>
+      {/* Video Modal */}
+      {exercise.fullVideoUrl && (
+        <ExerciseVideoModal
+          onOpenChange={setShowVideo}
+          open={showVideo}
+          title={exerciseName || exercise.name}
+          videoUrl={exercise.fullVideoUrl}
+        />
+      )}
+    </TooltipProvider >
   );
 }

+ 19 - 1
src/features/workout-builder/ui/exercise-list-item.tsx

@@ -8,6 +8,8 @@ import { useCurrentLocale, useI18n } from "locales/client";
 import { InlineTooltip } from "@/components/ui/tooltip";
 import { Button } from "@/components/ui/button";
 
+import { ExerciseVideoModal } from "./exercise-video-modal";
+
 import type { ExerciseWithAttributes } from "../types";
 
 interface ExerciseListItemProps {
@@ -24,6 +26,12 @@ export function ExerciseListItem({ exercise, muscle, onShuffle, onPick, onDelete
   const [isHovered, setIsHovered] = useState(false);
   const locale = useCurrentLocale();
   const exerciseName = locale === "fr" ? exercise.name : exercise.nameEn;
+  const [showVideo, setShowVideo] = useState(false);
+
+
+  const handleOpenVideo = () => {
+    setShowVideo(true);
+  };
 
   // Déterminer la couleur du muscle
   const getMuscleConfig = (muscle: string) => {
@@ -73,7 +81,7 @@ export function ExerciseListItem({ exercise, muscle, onShuffle, onPick, onDelete
               />
               {/* Overlay play icon */}
               <div className="absolute inset-0 bg-black/20 flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity duration-200">
-                <Play className="h-3 w-3 text-white fill-current" />
+                <Play className="h-3 w-3 text-white fill-current" onClick={handleOpenVideo} />
               </div>
             </div>
           )}
@@ -131,6 +139,16 @@ export function ExerciseListItem({ exercise, muscle, onShuffle, onPick, onDelete
           </Button>
         </div>
       </div>
+
+      {/* Video Modal */}
+      {exercise.fullVideoUrl && (
+        <ExerciseVideoModal
+          onOpenChange={setShowVideo}
+          open={showVideo}
+          title={exerciseName || exercise.name}
+          videoUrl={exercise.fullVideoUrl}
+        />
+      )}
     </div>
   );
 }

+ 53 - 0
src/features/workout-builder/ui/exercise-video-modal.tsx

@@ -0,0 +1,53 @@
+"use client";
+
+import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
+import { getYouTubeEmbedUrl } from "@/shared/lib/youtube";
+import { useI18n } from "locales/client";
+
+interface ExerciseVideoModalProps {
+  open: boolean;
+  onOpenChange: (open: boolean) => void;
+  videoUrl: string;
+  title: string;
+}
+
+
+export function ExerciseVideoModal({ open, onOpenChange, videoUrl, title }: ExerciseVideoModalProps) {
+  const youTubeEmbedUrl = getYouTubeEmbedUrl(videoUrl);
+  const t = useI18n();
+
+  return (
+    <Dialog onOpenChange={onOpenChange} open={open}>
+      <DialogContent className="max-w-xl p-0 overflow-hidden">
+        <DialogHeader className="flex flex-row items-center justify-between px-4 pt-4 pb-2">
+          <DialogTitle className="text-base">{title}</DialogTitle>
+        </DialogHeader>
+        <div className="w-full aspect-video bg-black flex items-center justify-center">
+          {videoUrl ? (
+            youTubeEmbedUrl ? (
+              <iframe
+                allow="autoplay; encrypted-media"
+                allowFullScreen
+                className="w-full h-full border-0"
+                src={youTubeEmbedUrl}
+                title={title}
+              />
+            ) : (
+              <video
+                autoPlay
+                className="w-full h-full object-contain bg-black"
+                controls
+                poster=""
+                src={videoUrl}
+              />
+            )
+          ) : (
+            <div className="text-white text-center p-8">
+              {t("workout_builder.exercise.no_video_available")}
+            </div>
+          )}
+        </div>
+      </DialogContent>
+    </Dialog>
+  );
+}