diff --git a/client/src/pages/image-editor.tsx b/client/src/pages/image-editor.tsx index 8f60ad7..5330cb2 100644 --- a/client/src/pages/image-editor.tsx +++ b/client/src/pages/image-editor.tsx @@ -13,7 +13,9 @@ import { Checkbox } from "@/components/ui/checkbox"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Slider } from "@/components/ui/slider"; import { Badge } from "@/components/ui/badge"; -import { Wand2, Save, ArrowRight, Image as ImageIcon, X } from "lucide-react"; +import { Wand2, Save, Image as ImageIcon, X } from "lucide-react"; +import { Link } from "wouter"; +import { EmptyState } from "@/components/empty-state"; import { useToast } from "@/hooks/use-toast"; interface Ribbon { @@ -119,26 +121,111 @@ export default function ImageEditor() { return ( -
-

- - Éditeur d'images -

-

- Ajoutez des rubans promotionnels, badges prix et filtres à vos images -

-
+ -
+ + +
{/* Left Column - Controls */}
{/* Image Selection */} - - - Sélectionner une image - + 1. Choisissez une image {mediaList && (mediaList as any[]).filter((m: any) => m.type === 'image').length > 0 ? ( @@ -149,7 +236,7 @@ export default function ImageEditor() {
setSelectedMedia(media)} data-testid={`select-image-${media.id}`} @@ -163,9 +250,17 @@ export default function ImageEditor() { ))}
) : ( -

- Aucune image disponible. Uploadez des images dans la médiathèque. -

+ + Ouvrir la médiathèque + + } + /> )}
@@ -173,7 +268,7 @@ export default function ImageEditor() { {/* Ribbon Controls */} - Ruban promotionnel + 2. Ruban promotionnel
@@ -254,7 +349,7 @@ export default function ImageEditor() { {/* Price Badge */} - Badge prix + 3. Prix
@@ -339,7 +434,7 @@ export default function ImageEditor() { {/* Logo */} - Logo + 4. Logo
@@ -415,7 +510,7 @@ export default function ImageEditor() { {selectedMedia ? (
-
+
setSelectedMedia(null)} - className="absolute top-2 right-2 p-1.5 bg-black/50 hover:bg-black/70 rounded-full text-white transition-colors z-20" + className="absolute top-2 right-2 p-1.5 bg-background/80 backdrop-blur hover:bg-background rounded-full text-foreground shadow transition-colors z-20" data-testid="button-clear-selection" title="Désélectionner l'image" > @@ -505,18 +600,19 @@ export default function ImageEditor() {
) : ( -
-

Sélectionnez une image

+
+ + Choisissez une image pour voir l'aperçu
)} diff --git a/client/src/pages/new-reel.tsx b/client/src/pages/new-reel.tsx index dc840d7..b922d34 100644 --- a/client/src/pages/new-reel.tsx +++ b/client/src/pages/new-reel.tsx @@ -4,10 +4,15 @@ import { PageHeader } from "@/components/layout/page-header"; import { useQuery, useMutation } from "@tanstack/react-query"; import { useLocation } from "wouter"; import { - Send, Sparkles, Video, Music, Type, Calendar, + Send, Sparkles, Video, Music, Type, Film, Upload, Play, Pause, Volume2, VolumeX, - ChevronRight, Loader2, Check, RefreshCw, Mic + ChevronDown, Loader2, Check, Mic, CheckCircle2, AlertTriangle, Zap, CalendarClock, Link2 } from "lucide-react"; +import { Stepper, StepNavigation, type StepDef } from "@/components/stepper"; +import { EmptyState } from "@/components/empty-state"; +import { PlatformIcon } from "@/components/platform-icon"; +import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; +import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible"; import { useDropzone } from "react-dropzone"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; @@ -35,7 +40,6 @@ import { DEFAULT_CAPTION_STYLE, type CaptionStyle } from "@shared/captions"; import { DEFAULT_TTS_STYLE, DEFAULT_VOICE } from "@shared/voices"; import { apiRequest, queryClient, handleUnauthorized, getErrorMessage } from "@/lib/queryClient"; import type { SocialPage, Media } from "@shared/schema"; -import { SiFacebook, SiTiktok } from "react-icons/si"; import { MediaThumbnail } from "@/components/media-thumbnail"; import { DateTimePicker } from "@/components/datetime-picker"; @@ -54,6 +58,15 @@ interface MusicTrack { // Étapes du workflow type Step = 'video' | 'music' | 'text' | 'publish'; +const STEP_ORDER: Step[] = ['video', 'music', 'text', 'publish']; + +const REEL_STEPS: StepDef[] = [ + { id: 'video', label: 'Vidéo', icon: Video }, + { id: 'music', label: 'Musique', icon: Music }, + { id: 'text', label: 'Texte & voix', icon: Type }, + { id: 'publish', label: 'Publication', icon: Send }, +]; + export default function NewReel() { const [, navigate] = useLocation(); const { toast } = useToast(); @@ -61,6 +74,8 @@ export default function NewReel() { // État du workflow const [currentStep, setCurrentStep] = useState('video'); + const [aiOpen, setAiOpen] = useState(false); + const [scheduleMode, setScheduleMode] = useState<'now' | 'later'>('now'); // État des données const [selectedVideoId, setSelectedVideoId] = useState(null); @@ -144,27 +159,30 @@ export default function NewReel() { const facebookPages = pages.filter(p => p.platform === 'facebook'); const tiktokAccounts = pages.filter(p => p.platform === 'tiktok'); - const renderTargetCheckbox = (page: SocialPage) => ( -
- { - if (checked) { - setSelectedPages([...selectedPages, page.id]); - } else { - setSelectedPages(selectedPages.filter(id => id !== page.id)); - } - }} - /> + const renderTargetCheckbox = (page: SocialPage) => { + const checked = selectedPages.includes(page.id); + return ( -
- ); + ); + }; // Récupérer les vidéos disponibles const { data: allMedia = [] } = useQuery({ @@ -383,10 +401,6 @@ export default function NewReel() { const handleUseVariant = (text: string) => { setOverlayText(text); - toast({ - title: "Texte sélectionné", - description: "Le texte a été ajouté à votre Reel", - }); }; const handleCreateReel = () => { @@ -441,618 +455,524 @@ export default function NewReel() { return `${mins}:${secs.toString().padStart(2, '0')}`; }; + const stepIndex = STEP_ORDER.indexOf(currentStep); + const lockedReason = (index: number): string | null => + index > 0 && !selectedVideo ? "Sélectionnez d'abord une vidéo" : null; + const goTo = (index: number) => { + if (index < 0 || index >= STEP_ORDER.length) return; + const reason = lockedReason(index); + if (reason) { + toast({ title: "Étape incomplète", description: reason, variant: "destructive" }); + return; + } + setCurrentStep(STEP_ORDER[index]); + window.scrollTo({ top: 0, behavior: "smooth" }); + }; + + const optionRow = (id: string, label: string, description: string, checked: boolean, onChange: (v: boolean) => void) => ( +
+ + +
+ ); + return ( <> - -
-

-

-

- Créez un Reel Facebook avec musique et texte overlay -

-
+ + - {/* Stepper */} -
- {(['video', 'music', 'text', 'publish'] as Step[]).map((step, index) => ( -
- - {index < 3 && } -
- ))} -
+ - {/* Contenu par étape */} -
- {/* Colonne principale */} -
+
+ {/* Colonne principale */} +
- - {/* ÉTAPE 1: Vidéo */} - {currentStep === 'video' && ( - - - - - - Choisissez une vidéo existante ou téléchargez-en une nouvelle - - - -
-
-
- -

- Recommandé pour un rendu professionnel sur Facebook Reels -

-
- - Note iPhone : Activez la stabilisation dans Réglages > Appareil Photo -
-
- -
-
- -
- - -
- -
- - {videoList.length === 0 ? ( -
-
- ) : ( -
- {videoList.map((media) => ( - - ))} -
- )} -
- - {selectedVideo && ( -
- -
- )} -
-
- )} - - {/* ÉTAPE 2: Musique */} - {currentStep === 'music' && ( - - - - - - Choisissez une Musique - - - Sélectionnez une musique libre de droits (optionnel) - - - - - - )} - - {/* ÉTAPE 3: Texte */} - {currentStep === 'text' && ( - <> - {!srtFile && ( - - - - - Générer avec l'IA - - - Décrivez votre produit pour générer des textes - - - -