change UI

This commit is contained in:
Arthur Barre 2025-03-14 00:58:18 +01:00
parent b311e76bf3
commit 81f3900ac8
6 changed files with 550 additions and 126 deletions

View File

@ -20,6 +20,7 @@
"@radix-ui/react-slot": "^1.1.2",
"@radix-ui/react-tabs": "^1.1.3",
"@radix-ui/react-toast": "^1.2.6",
"@radix-ui/react-tooltip": "^1.1.8",
"@react-oauth/google": "^0.12.1",
"@tailwindcss/vite": "^4.0.12",
"axios": "^1.8.2",

View File

@ -35,6 +35,9 @@ importers:
'@radix-ui/react-toast':
specifier: ^1.2.6
version: 1.2.6(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
'@radix-ui/react-tooltip':
specifier: ^1.1.8
version: 1.1.8(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
'@react-oauth/google':
specifier: ^0.12.1
version: 0.12.1(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
@ -1163,6 +1166,19 @@ packages:
'@types/react-dom':
optional: true
'@radix-ui/react-tooltip@1.1.8':
resolution: {integrity: sha512-YAA2cu48EkJZdAMHC0dqo9kialOcRStbtiY4nJPaht7Ptrhcvpo+eDChaM6BIs8kL6a8Z5l5poiqLnXcNduOkA==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
'@radix-ui/react-use-callback-ref@1.1.0':
resolution: {integrity: sha512-CasTfvsy+frcFkbXtSJ2Zu9JHpN8TYKxkgJGWbjiZhFivxaeW7rMeZt7QELGVLaYVfFMsKHjb7Ak0nMEe+2Vfw==}
peerDependencies:
@ -4200,6 +4216,26 @@ snapshots:
'@types/react': 19.0.10
'@types/react-dom': 19.0.4(@types/react@19.0.10)
'@radix-ui/react-tooltip@1.1.8(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)':
dependencies:
'@radix-ui/primitive': 1.1.1
'@radix-ui/react-compose-refs': 1.1.1(@types/react@19.0.10)(react@19.0.0)
'@radix-ui/react-context': 1.1.1(@types/react@19.0.10)(react@19.0.0)
'@radix-ui/react-dismissable-layer': 1.1.5(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
'@radix-ui/react-id': 1.1.0(@types/react@19.0.10)(react@19.0.0)
'@radix-ui/react-popper': 1.2.2(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
'@radix-ui/react-portal': 1.1.4(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
'@radix-ui/react-presence': 1.1.2(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
'@radix-ui/react-primitive': 2.0.2(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
'@radix-ui/react-slot': 1.1.2(@types/react@19.0.10)(react@19.0.0)
'@radix-ui/react-use-controllable-state': 1.1.0(@types/react@19.0.10)(react@19.0.0)
'@radix-ui/react-visually-hidden': 1.1.2(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
react: 19.0.0
react-dom: 19.0.0(react@19.0.0)
optionalDependencies:
'@types/react': 19.0.10
'@types/react-dom': 19.0.4(@types/react@19.0.10)
'@radix-ui/react-use-callback-ref@1.1.0(@types/react@19.0.10)(react@19.0.0)':
dependencies:
react: 19.0.0

View File

@ -0,0 +1,137 @@
import * as React from "react"
import * as SheetPrimitive from "@radix-ui/react-dialog"
import { XIcon } from "lucide-react"
import { cn } from "@/lib/utils"
function Sheet({ ...props }: React.ComponentProps<typeof SheetPrimitive.Root>) {
return <SheetPrimitive.Root data-slot="sheet" {...props} />
}
function SheetTrigger({
...props
}: React.ComponentProps<typeof SheetPrimitive.Trigger>) {
return <SheetPrimitive.Trigger data-slot="sheet-trigger" {...props} />
}
function SheetClose({
...props
}: React.ComponentProps<typeof SheetPrimitive.Close>) {
return <SheetPrimitive.Close data-slot="sheet-close" {...props} />
}
function SheetPortal({
...props
}: React.ComponentProps<typeof SheetPrimitive.Portal>) {
return <SheetPrimitive.Portal data-slot="sheet-portal" {...props} />
}
function SheetOverlay({
className,
...props
}: React.ComponentProps<typeof SheetPrimitive.Overlay>) {
return (
<SheetPrimitive.Overlay
data-slot="sheet-overlay"
className={cn(
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
className
)}
{...props}
/>
)
}
function SheetContent({
className,
children,
side = "right",
...props
}: React.ComponentProps<typeof SheetPrimitive.Content> & {
side?: "top" | "right" | "bottom" | "left"
}) {
return (
<SheetPortal>
<SheetOverlay />
<SheetPrimitive.Content
data-slot="sheet-content"
className={cn(
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out fixed z-50 flex flex-col gap-4 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500",
side === "right" &&
"data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right inset-y-0 right-0 h-full w-3/4 border-l sm:max-w-sm",
side === "left" &&
"data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left inset-y-0 left-0 h-full w-3/4 border-r sm:max-w-sm",
side === "top" &&
"data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top inset-x-0 top-0 h-auto border-b",
side === "bottom" &&
"data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom inset-x-0 bottom-0 h-auto border-t",
className
)}
{...props}
>
{children}
<SheetPrimitive.Close className="ring-offset-background focus:ring-ring data-[state=open]:bg-secondary absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none">
<XIcon className="size-4" />
<span className="sr-only">Close</span>
</SheetPrimitive.Close>
</SheetPrimitive.Content>
</SheetPortal>
)
}
function SheetHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="sheet-header"
className={cn("flex flex-col gap-1.5 p-4", className)}
{...props}
/>
)
}
function SheetFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="sheet-footer"
className={cn("mt-auto flex flex-col gap-2 p-4", className)}
{...props}
/>
)
}
function SheetTitle({
className,
...props
}: React.ComponentProps<typeof SheetPrimitive.Title>) {
return (
<SheetPrimitive.Title
data-slot="sheet-title"
className={cn("text-foreground font-semibold", className)}
{...props}
/>
)
}
function SheetDescription({
className,
...props
}: React.ComponentProps<typeof SheetPrimitive.Description>) {
return (
<SheetPrimitive.Description
data-slot="sheet-description"
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
)
}
export {
Sheet,
SheetTrigger,
SheetClose,
SheetContent,
SheetHeader,
SheetFooter,
SheetTitle,
SheetDescription,
}

View File

@ -0,0 +1,59 @@
import * as React from "react"
import * as TooltipPrimitive from "@radix-ui/react-tooltip"
import { cn } from "@/lib/utils"
function TooltipProvider({
delayDuration = 0,
...props
}: React.ComponentProps<typeof TooltipPrimitive.Provider>) {
return (
<TooltipPrimitive.Provider
data-slot="tooltip-provider"
delayDuration={delayDuration}
{...props}
/>
)
}
function Tooltip({
...props
}: React.ComponentProps<typeof TooltipPrimitive.Root>) {
return (
<TooltipProvider>
<TooltipPrimitive.Root data-slot="tooltip" {...props} />
</TooltipProvider>
)
}
function TooltipTrigger({
...props
}: React.ComponentProps<typeof TooltipPrimitive.Trigger>) {
return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />
}
function TooltipContent({
className,
sideOffset = 0,
children,
...props
}: React.ComponentProps<typeof TooltipPrimitive.Content>) {
return (
<TooltipPrimitive.Portal>
<TooltipPrimitive.Content
data-slot="tooltip-content"
sideOffset={sideOffset}
className={cn(
"bg-primary text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-fit rounded-md px-3 py-1.5 text-xs text-balance",
className
)}
{...props}
>
{children}
<TooltipPrimitive.Arrow className="bg-primary fill-primary z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px]" />
</TooltipPrimitive.Content>
</TooltipPrimitive.Portal>
)
}
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }

View File

@ -0,0 +1,33 @@
import { useState, useEffect } from 'react';
/**
* Hook qui détecte si l'appareil est un mobile en fonction de la largeur de l'écran
* @param breakpoint Largeur en pixels en dessous de laquelle on considère qu'il s'agit d'un mobile (défaut: 768px)
* @returns boolean indiquant si l'appareil est considéré comme mobile
*/
export function useMobile(breakpoint: number = 768): boolean {
// État initial basé sur la largeur de la fenêtre (si disponible côté client)
const [isMobile, setIsMobile] = useState<boolean>(
typeof window !== 'undefined' ? window.innerWidth < breakpoint : false
);
useEffect(() => {
// Fonction pour mettre à jour l'état en fonction de la largeur de la fenêtre
const checkMobile = () => {
setIsMobile(window.innerWidth < breakpoint);
};
// Vérifier immédiatement au montage du composant
checkMobile();
// Ajouter un écouteur d'événement pour le redimensionnement de la fenêtre
window.addEventListener('resize', checkMobile);
// Nettoyer l'écouteur d'événement lors du démontage du composant
return () => {
window.removeEventListener('resize', checkMobile);
};
}, [breakpoint]); // Recalculer si le breakpoint change
return isMobile;
}

View File

@ -6,35 +6,41 @@ import { useState, useEffect } from "react"
import { useNavigate } from "react-router-dom"
import { recipeService } from "@/api/recipe"
import { Button } from "@/components/ui/button"
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"
import { Mic, Upload, ArrowLeft, Loader2 } from "lucide-react"
import { motion } from "framer-motion"
import { Card } from "@/components/ui/card"
import { Mic, ArrowLeft, Trash2, ChefHat, Info, X, Check } from "lucide-react"
import { motion, AnimatePresence } from "framer-motion"
import { KitchenIllustration } from "@/components/illustrations/KitchenIllustration"
import { CookingLoader } from "@/components/illustrations/CookingLoader"
import { useAudioRecorder } from "@/hooks/useAudioRecorder"
import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle, SheetTrigger } from "@/components/ui/sheet"
import { cn } from "@/lib/utils"
import { useMobile } from "@/hooks/useMobile"
export default function RecipeForm() {
const navigate = useNavigate()
const isMobile = useMobile()
const {
isLoading: isRecorderLoading,
isRecording,
currentRecording,
startRecording,
stopRecording
stopRecording,
} = useAudioRecorder()
const [audioFile, setAudioFile] = useState<File | null>(null)
const [recordingStatus, setRecordingStatus] = useState<"idle" | "recording" | "processing">("idle")
const [loading, setLoading] = useState(false)
const [error, setError] = useState("")
const [recordingTime, setRecordingTime] = useState(0)
const [showTips, setShowTips] = useState(false)
// Update audioFile when recording is available
useEffect(() => {
if (currentRecording) {
fetch(currentRecording)
.then(res => res.blob())
.then(blob => {
.then((res) => res.blob())
.then((blob) => {
const file = new File([blob], "recording.mp3", { type: "audio/mp3" })
setAudioFile(file)
setRecordingStatus("idle")
@ -43,6 +49,21 @@ export default function RecipeForm() {
}
}, [currentRecording])
// Recording timer
useEffect(() => {
let interval: NodeJS.Timeout | null = null
if (isRecording) {
interval = setInterval(() => {
setRecordingTime((prev) => prev + 1)
}, 1000)
} else {
setRecordingTime(0)
}
return () => {
if (interval) clearInterval(interval)
}
}, [isRecording])
// Handle recording start
const handleStartRecording = async () => {
try {
@ -63,6 +84,13 @@ export default function RecipeForm() {
}
}
// Fonction pour réinitialiser l'enregistrement
const handleResetRecording = () => {
setAudioFile(null)
setRecordingStatus("idle")
setError("")
}
// Soumettre le formulaire
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
@ -88,143 +116,273 @@ export default function RecipeForm() {
}
}
// Format recording time
const formatTime = (seconds: number) => {
const mins = Math.floor(seconds / 60)
const secs = seconds % 60
return `${mins.toString().padStart(2, "0")}:${secs.toString().padStart(2, "0")}`
}
return (
<div className="h-full flex flex-col bg-gradient-to-b from-amber-50 to-orange-50 dark:from-slate-950 dark:to-slate-900">
<div className="p-2 sm:p-4">
<Button
variant="ghost"
className="cursor-pointer flex items-center gap-2"
onClick={() => navigate("/recipes")}
>
<ArrowLeft className="h-4 w-4" />
Retour aux recettes
<div className="min-h-screen flex flex-col bg-gradient-to-b from-amber-50 to-orange-50 dark:from-slate-950 dark:to-slate-900">
{/* Header */}
<header className="sticky top-0 z-10 backdrop-blur-sm bg-amber-50/80 dark:bg-slate-950/80 border-b border-amber-100 dark:border-slate-800 p-4 flex justify-between items-center">
<Button variant="ghost" size="icon" className="rounded-full" onClick={() => navigate("/recipes")}>
<ArrowLeft className="h-5 w-5" />
<span className="sr-only">Retour</span>
</Button>
</div>
<h1 className="text-lg font-medium">Nouvelle Recette</h1>
<Sheet>
<SheetTrigger asChild>
<Button variant="ghost" size="icon" className="rounded-full">
<Info className="h-5 w-5" />
<span className="sr-only">Aide</span>
</Button>
</SheetTrigger>
<SheetContent side="bottom" className="rounded-t-xl">
<SheetHeader>
<SheetTitle>Comment ça marche ?</SheetTitle>
<SheetDescription>
<ol className="mt-4 space-y-3 list-decimal list-inside text-sm">
<li>Appuyez sur le bouton d'enregistrement</li>
<li>Listez clairement les ingrédients que vous avez à disposition</li>
<li>Terminez l'enregistrement</li>
<li>Notre IA créera une recette adaptée à vos ingrédients</li>
</ol>
</SheetDescription>
</SheetHeader>
</SheetContent>
</Sheet>
</header>
<div className="w-full space-y-4 sm:space-y-6 mt-2 sm:mt-4 px-2 sm:px-4 md:px-6 lg:px-8 max-w-3xl mx-auto">
{/* Illustrations */}
<div className="flex flex-col items-center text-center mb-4 sm:mb-8">
<KitchenIllustration
height={150}
/>
<p className="max-w-[900px] text-muted-foreground text-sm md:text-xl/relaxed mt-2 sm:mt-4">
Notre intelligence artificielle transforme votre façon de cuisiner
</p>
</div>
<main className="flex-1 flex flex-col">
{/* Main content */}
<div className="flex-1 flex flex-col p-4 max-w-md mx-auto w-full">
{/* Illustration */}
<AnimatePresence>
{!isRecording && !loading && (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
className="flex justify-center mb-6"
>
<KitchenIllustration height={120} />
</motion.div>
)}
</AnimatePresence>
<Card className="border-none shadow-lg">
<CardHeader className="p-2 sm:p-4">
<CardTitle className="text-xl sm:text-2xl">Créer une nouvelle recette</CardTitle>
<CardDescription className="text-sm">
Enregistrez ou téléchargez un fichier audio listant les ingrédients disponibles, et nous générerons une
recette pour vous.
</CardDescription>
</CardHeader>
<CardContent className="p-2 sm:p-4">
{/* Error message */}
<AnimatePresence>
{error && (
<div className="rounded-md bg-red-50 p-4 text-sm text-red-700 dark:bg-red-900/50 dark:text-red-200 mb-4">
{error}
</div>
<motion.div
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: "auto" }}
exit={{ opacity: 0, height: 0 }}
className="mb-4"
>
<div className="rounded-lg bg-red-50 p-3 text-sm text-red-700 dark:bg-red-900/30 dark:text-red-200 flex items-start">
<div className="flex-1">{error}</div>
<Button variant="ghost" size="sm" className="h-6 w-6 p-0 rounded-full" onClick={() => setError("")}>
<X className="h-4 w-4" />
</Button>
</div>
</motion.div>
)}
</AnimatePresence>
{recordingStatus !== "processing" && !loading && (
<div className="space-y-4">
{audioFile && (
<div className="rounded-lg bg-green-50 dark:bg-green-900/20 p-4">
<p className="font-medium text-green-800 dark:text-green-300">Fichier audio prêt !</p>
<p className="text-sm text-green-700 dark:text-green-400 mt-1">
{audioFile.name} ({(audioFile.size / 1024).toFixed(2)} KB)
</p>
<div className="mt-3">
<audio controls className="w-full">
<source src={URL.createObjectURL(audioFile)} type={audioFile.type} />
Votre navigateur ne supporte pas la lecture audio.
</audio>
</div>
</div>
)}
<p className="text-sm text-muted-foreground">
Enregistrez-vous en listant les ingrédients que vous avez à disposition. Notre IA générera une
recette adaptée à ces ingrédients.
</p>
</div>
)}
{(recordingStatus === "processing" || loading) && (
<div className="py-8 flex flex-col items-center justify-center">
{/* Recording UI */}
<AnimatePresence mode="wait">
{recordingStatus === "processing" || loading ? (
<motion.div
key="processing"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="flex-1 flex flex-col items-center justify-center"
>
<CookingLoader />
<p className="mt-4 text-center font-medium">Préparation de votre recette...</p>
<p className="text-sm text-muted-foreground text-center mt-2">
<h2 className="mt-6 text-xl font-medium text-center">Préparation en cours...</h2>
<p className="text-sm text-muted-foreground text-center mt-2 max-w-xs">
Notre chef IA mijote quelque chose de délicieux avec vos ingrédients
</p>
</div>
)}
</CardContent>
<CardFooter className="p-2 sm:p-4 flex justify-between">
<Button
variant="outline"
className="cursor-pointer"
onClick={() => navigate("/recipes")}
disabled={loading || recordingStatus === "processing"}
>
Annuler
</Button>
{!audioFile && !isRecording && recordingStatus !== "processing" && !loading && (
<Button
variant="default"
className="cursor-pointer"
onClick={handleStartRecording}
</motion.div>
) : isRecording ? (
<motion.div
key="recording"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="flex-1 flex flex-col"
>
<Mic className="mr-2 h-4 w-4" />
Commencer l'enregistrement
</Button>
)}
<Card className="flex-1 flex flex-col items-center justify-center p-6 border-2 border-red-400 dark:border-red-700 bg-gradient-to-b from-red-50 to-red-100 dark:from-red-950/30 dark:to-red-900/20">
<motion.div
animate={{ scale: [1, 1.1, 1] }}
transition={{ repeat: Number.POSITIVE_INFINITY, duration: 1.5 }}
className="relative"
>
<div className="absolute inset-0 bg-red-400/20 dark:bg-red-700/30 rounded-full animate-ping" />
<div className="relative bg-red-500 dark:bg-red-600 text-white p-6 rounded-full">
<Mic className="h-8 w-8" />
</div>
</motion.div>
{isRecording && (
<Button
variant="destructive"
className="cursor-pointer"
onClick={handleStopRecording}
<div className="mt-8 text-center">
<div className="text-3xl font-mono font-bold text-red-600 dark:text-red-400">
{formatTime(recordingTime)}
</div>
<p className="mt-2 text-sm text-red-700 dark:text-red-300 animate-pulse">
Enregistrement en cours...
</p>
</div>
<div className="mt-8 w-full max-w-xs">
<Button variant="destructive" className="w-full" size="lg" onClick={handleStopRecording}>
Arrêter l'enregistrement
</Button>
</div>
</Card>
<div className="mt-4">
<Button variant="outline" className="w-full text-sm" onClick={() => setShowTips(!showTips)}>
{showTips ? "Masquer les conseils" : "Afficher les conseils"}
</Button>
<AnimatePresence>
{showTips && (
<motion.div
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: "auto" }}
exit={{ opacity: 0, height: 0 }}
className="overflow-hidden"
>
<div className="mt-3 p-3 bg-amber-100/50 dark:bg-amber-900/20 rounded-lg text-sm">
<h3 className="font-medium mb-2">Conseils pour un bon enregistrement :</h3>
<ul className="space-y-2 list-disc list-inside text-muted-foreground">
<li>Parlez clairement et lentement</li>
<li>Listez un ingrédient à la fois</li>
<li>Précisez les quantités si possible</li>
<li>Mentionnez vos préférences culinaires</li>
</ul>
</div>
</motion.div>
)}
</AnimatePresence>
</div>
</motion.div>
) : audioFile ? (
<motion.div
key="review"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="flex-1 flex flex-col"
>
<motion.div
className="flex items-center"
animate={{ scale: [1, 1.05, 1] }}
transition={{ repeat: Number.POSITIVE_INFINITY, duration: 1.5 }}
<Card className="flex-1 p-5 border-2 border-green-200 dark:border-green-800 bg-gradient-to-b from-green-50 to-green-100/50 dark:from-green-950/30 dark:to-green-900/20">
<div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-2">
<div className="bg-green-100 dark:bg-green-800 p-2 rounded-full">
<Check className="h-5 w-5 text-green-600 dark:text-green-300" />
</div>
<h3 className="font-medium text-green-700 dark:text-green-300">Enregistrement prêt !</h3>
</div>
<Button
variant="ghost"
size="icon"
className="h-8 w-8 text-red-500 hover:text-red-700 hover:bg-red-50 dark:hover:bg-red-900/20 rounded-full"
onClick={handleResetRecording}
>
<Trash2 className="h-4 w-4" />
<span className="sr-only">Supprimer</span>
</Button>
</div>
<div className="bg-white/80 dark:bg-slate-800/50 rounded-lg p-3 mb-4">
<audio controls className="w-full h-10">
<source src={URL.createObjectURL(audioFile)} type={audioFile.type} />
Votre navigateur ne supporte pas la lecture audio.
</audio>
<p className="text-xs text-muted-foreground mt-2">
{audioFile.name} ({(audioFile.size / 1024).toFixed(1)} KB)
</p>
</div>
<div className="mt-auto">
<Button className="w-full" size="lg" onClick={handleSubmit}>
<ChefHat className="h-5 w-5 mr-2" />
Créer ma recette
</Button>
</div>
</Card>
<div className="mt-4 text-center">
<Button variant="outline" onClick={handleResetRecording}>
Nouvel enregistrement
</Button>
</div>
</motion.div>
) : (
<motion.div
key="start"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="flex-1 flex flex-col"
>
<div className="text-center mb-6">
<h2 className="text-xl font-medium">Qu'avez-vous dans votre frigo ?</h2>
<p className="text-sm text-muted-foreground mt-2">
Enregistrez les ingrédients que vous avez à disposition
</p>
</div>
<Card
className={cn(
"flex-1 flex flex-col items-center justify-center p-6",
"bg-gradient-to-b from-amber-100/50 to-orange-100/30",
"dark:from-amber-900/20 dark:to-orange-900/10",
"border-2 border-dashed border-amber-200 dark:border-amber-800",
)}
>
<Mic className="mr-2 h-4 w-4" />
Arrêter l'enregistrement
</motion.div>
</Button>
)}
<motion.div
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
className="cursor-pointer"
onClick={handleStartRecording}
>
<div className="bg-gradient-to-br from-amber-400 to-orange-500 dark:from-amber-500 dark:to-orange-600 p-6 rounded-full text-white shadow-lg">
<Mic className="h-10 w-10" />
</div>
</motion.div>
{audioFile && !isRecording && recordingStatus !== "processing" && !loading && (
<Button
className="cursor-pointer"
onClick={handleSubmit}
>
<Upload className="mr-2 h-4 w-4" />
Créer la recette
</Button>
)}
<h3 className="mt-6 text-lg font-medium">Appuyez pour commencer</h3>
<p className="text-sm text-muted-foreground text-center mt-2 max-w-xs">
Listez les ingrédients que vous avez à disposition et notre IA créera une recette adaptée
</p>
{loading && (
<Button
className="cursor-pointer"
disabled
>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Création en cours...
</Button>
<Button className="mt-8" size="lg" onClick={handleStartRecording}>
Commencer l'enregistrement
</Button>
</Card>
<div className="mt-6 bg-amber-100/50 dark:bg-amber-900/20 rounded-lg p-4">
<h3 className="font-medium mb-2 flex items-center gap-2">
<Info className="h-4 w-4" />
Comment ça marche ?
</h3>
<ol className="space-y-2 list-decimal list-inside text-sm text-muted-foreground">
<li>Enregistrez les ingrédients que vous avez</li>
<li>Notre IA analyse votre enregistrement</li>
<li>Recevez une recette personnalisée</li>
<li>Cuisinez et régalez-vous !</li>
</ol>
</div>
</motion.div>
)}
</CardFooter>
</Card>
</div>
</div >
</AnimatePresence>
</div>
</main>
</div>
)
}