From cdbc4aee39224348944a3e723245053d9ad09cf7 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 6 Oct 2026 12:50:46 +0000 Subject: [PATCH] =?UTF-8?q?Refonte=20UI=20:=20param=C3=A8tres=20en=20ongle?= =?UTF-8?q?ts,=20nouvelle=20publication=20en=204=20=C3=A9tapes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_017ADkrVcVFTT14hbB148HYS --- .../analytics/AnalyticsDashboard.tsx | 2 +- client/src/components/calendar-view.tsx | 2 +- client/src/components/datetime-picker.tsx | 4 +- client/src/components/media-upload.tsx | 2 +- client/src/components/preview-modal.tsx | 2 +- .../components/reels/qwen-settings-card.tsx | 10 +- client/src/components/reels/srt-upload.tsx | 2 +- client/src/components/reels/voice-picker.tsx | 6 +- client/src/components/sidebar.tsx | 2 +- client/src/components/stepper.tsx | 150 ++++ client/src/pages/image-editor.tsx | 2 +- client/src/pages/new-post.tsx | 815 +++++++++++------- client/src/pages/new-reel.tsx | 8 +- client/src/pages/pages.tsx | 8 +- client/src/pages/schedule-reel.tsx | 4 +- client/src/pages/settings.tsx | 522 +++++------ 16 files changed, 947 insertions(+), 594 deletions(-) create mode 100644 client/src/components/stepper.tsx diff --git a/client/src/components/analytics/AnalyticsDashboard.tsx b/client/src/components/analytics/AnalyticsDashboard.tsx index 002caac..cc3893b 100644 --- a/client/src/components/analytics/AnalyticsDashboard.tsx +++ b/client/src/components/analytics/AnalyticsDashboard.tsx @@ -130,7 +130,7 @@ export function AnalyticsDashboard() { onClick={() => refreshMutation.mutate()} disabled={refreshMutation.isPending || !selectedPageId} > - + Actualiser les données diff --git a/client/src/components/calendar-view.tsx b/client/src/components/calendar-view.tsx index 2e5c72f..163ff58 100644 --- a/client/src/components/calendar-view.tsx +++ b/client/src/components/calendar-view.tsx @@ -211,7 +211,7 @@ export default function CalendarView() { className="rounded-xl" data-testid="button-prev-month" > - + Précédent diff --git a/client/src/components/datetime-picker.tsx b/client/src/components/datetime-picker.tsx index 4e5d66a..7113d6a 100644 --- a/client/src/components/datetime-picker.tsx +++ b/client/src/components/datetime-picker.tsx @@ -86,7 +86,7 @@ export function DateTimePicker({ value, onChange, occupiedDates = [], placeholde )} data-testid="button-date-picker" > - + {value ? format(value, "PPP", { locale: fr }) : {placeholder}} @@ -131,7 +131,7 @@ export function DateTimePicker({ value, onChange, occupiedDates = [], placeholde className="w-full justify-start text-left font-normal" data-testid="button-time-picker" > - + {String(selectedHour).padStart(2, '0')}:{String(selectedMinute).padStart(2, '0')} diff --git a/client/src/components/media-upload.tsx b/client/src/components/media-upload.tsx index 8df5022..967b4ac 100644 --- a/client/src/components/media-upload.tsx +++ b/client/src/components/media-upload.tsx @@ -241,7 +241,7 @@ export default function MediaUpload() { className="bg-gradient-to-r from-primary to-secondary hover:opacity-90 rounded-xl" data-testid="button-browse" > - + Parcourir diff --git a/client/src/components/preview-modal.tsx b/client/src/components/preview-modal.tsx index bf92b3c..ef7cdca 100644 --- a/client/src/components/preview-modal.tsx +++ b/client/src/components/preview-modal.tsx @@ -569,7 +569,7 @@ export function PreviewModal({ className="bg-gradient-to-r from-primary to-secondary hover:opacity-90" data-testid="button-publish-from-preview" > - + {isPublishing ? 'Publication...' : 'Publier'} diff --git a/client/src/components/reels/qwen-settings-card.tsx b/client/src/components/reels/qwen-settings-card.tsx index ee78e08..9e8b6b4 100644 --- a/client/src/components/reels/qwen-settings-card.tsx +++ b/client/src/components/reels/qwen-settings-card.tsx @@ -80,8 +80,8 @@ export function QwenSettingsCard() { }); return ( - - + + Qwen TTS (voix locale) @@ -113,12 +113,12 @@ export function QwenSettingsCard() { data-testid="input-qwen-api-key" /> {config?.hasApiKey && ( -

✓ Clé enregistrée (laisser vide pour la garder)

+

✓ Clé enregistrée (laisser vide pour la garder)

)} {test && ( -

+

{test.ok ? `✓ Connecté : ${test.device ?? "?"} — ${test.model ?? ""} (${test.voices ?? 0} voix)` : `✗ ${test.error}`} @@ -133,7 +133,7 @@ export function QwenSettingsCard() { disabled={runTest.isPending || (!url && !config?.url)} data-testid="button-test-qwen" > - {runTest.isPending && } + {runTest.isPending && } Tester la connexion )} diff --git a/client/src/components/reels/voice-picker.tsx b/client/src/components/reels/voice-picker.tsx index ea33649..d039a32 100644 --- a/client/src/components/reels/voice-picker.tsx +++ b/client/src/components/reels/voice-picker.tsx @@ -223,11 +223,11 @@ export function VoicePicker({ value, onChange, sampleText, compact = false, onPr }} > {loading ? ( - + ) : playing ? ( - + ) : ( - + )} {loading ? "Génération de la voix…" : playing ? "Arrêter" : "Tester la voix"} diff --git a/client/src/components/sidebar.tsx b/client/src/components/sidebar.tsx index 572be9f..038c5e1 100644 --- a/client/src/components/sidebar.tsx +++ b/client/src/components/sidebar.tsx @@ -157,7 +157,7 @@ export default function Sidebar() { className="text-destructive focus:text-destructive" data-testid="button-logout" > - + {logout.isPending ? "Déconnexion…" : "Se déconnecter"} diff --git a/client/src/components/stepper.tsx b/client/src/components/stepper.tsx new file mode 100644 index 0000000..f6c34c5 --- /dev/null +++ b/client/src/components/stepper.tsx @@ -0,0 +1,150 @@ +import type { LucideIcon } from "lucide-react"; +import { Check, Lock } from "lucide-react"; +import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; +import { cn } from "@/lib/utils"; + +export interface StepDef { + id: string; + label: string; + icon: LucideIcon; +} + +interface StepperProps { + steps: StepDef[]; + current: number; + /** Renvoie null si l'étape est accessible, sinon la raison du blocage. */ + lockedReason?: (index: number) => string | null; + onStepClick?: (index: number) => void; + className?: string; +} + +/** Indicateur d'étapes cliquable, utilisé par les parcours de création. */ +export function Stepper({ steps, current, lockedReason, onStepClick, className }: StepperProps) { + return ( +

+ ); +} + +/** Barre Précédent / Suivant, collante en bas sur mobile (au-dessus de la navigation). */ +export function StepNavigation({ + onBack, + onNext, + nextLabel = "Continuer", + nextDisabled, + backDisabled, + next, + hint, +}: { + onBack?: () => void; + onNext?: () => void; + nextLabel?: string; + nextDisabled?: boolean; + backDisabled?: boolean; + /** Remplace le bouton « Continuer » (ex. bouton de publication). */ + next?: React.ReactNode; + hint?: React.ReactNode; +}) { + return ( +
+
+ + {hint &&

{hint}

} +
+ {next ?? ( + + )} +
+
+
+ ); +} diff --git a/client/src/pages/image-editor.tsx b/client/src/pages/image-editor.tsx index 51dcb73..8f60ad7 100644 --- a/client/src/pages/image-editor.tsx +++ b/client/src/pages/image-editor.tsx @@ -510,7 +510,7 @@ export default function ImageEditor() { onClick={() => saveImageMutation.mutate()} disabled={saveImageMutation.isPending} > - + {saveImageMutation.isPending ? "Enregistrement..." : "Enregistrer"} diff --git a/client/src/pages/new-post.tsx b/client/src/pages/new-post.tsx index 4ee4b2c..7f0da06 100644 --- a/client/src/pages/new-post.tsx +++ b/client/src/pages/new-post.tsx @@ -3,7 +3,14 @@ import { Page } from "@/components/layout/app-shell"; import { PageHeader } from "@/components/layout/page-header"; import { useQuery, useMutation } from "@tanstack/react-query"; import { useLocation } from "wouter"; -import { Send, Sparkles, Image as ImageIcon, Calendar, Upload, Camera, GripVertical, Loader2 } from "lucide-react"; +import { Send, Sparkles, Image as ImageIcon, Calendar, CalendarClock, Upload, Camera, GripVertical, Loader2, PenSquare, Type, Users, ChevronDown, Check, Link2, LayoutGrid, Smartphone, Layers, AlertTriangle, Zap } from "lucide-react"; +import { format } from "date-fns"; +import { fr } from "date-fns/locale"; +import { Stepper, StepNavigation, type StepDef } from "@/components/stepper"; +import { EmptyState } from "@/components/empty-state"; +import { PlatformIcon, platformLabel } 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 { DndContext, @@ -25,10 +32,8 @@ import { CSS } from "@dnd-kit/utilities"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Textarea } from "@/components/ui/textarea"; -import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Checkbox } from "@/components/ui/checkbox"; -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Dialog, DialogContent } from "@/components/ui/dialog"; import { useToast } from "@/hooks/use-toast"; import { apiRequest, queryClient, handleUnauthorized, getErrorMessage } from "@/lib/queryClient"; @@ -68,7 +73,7 @@ function SortableMediaItem({ ref={setNodeRef} style={style} className={`relative aspect-square rounded-lg overflow-hidden border-2 transition-all ${isSelected - ? 'border-primary ring-2 ring-primary' + ? 'border-primary' : 'border-transparent hover:border-muted-foreground' }`} > @@ -86,15 +91,15 @@ function SortableMediaItem({ {isSelected && ( <> -
+
{index + 1}
- +
)} @@ -112,6 +117,28 @@ function startOfToday(): Date { return date; } +const STEPS: StepDef[] = [ + { id: "media", label: "Médias", icon: ImageIcon }, + { id: "text", label: "Texte", icon: Type }, + { id: "targets", label: "Diffusion", icon: Users }, + { id: "schedule", label: "Planification", icon: Calendar }, +]; + +const FORMATS = [ + { value: 'feed', label: "Fil d'actualité", description: "Publication classique", icon: LayoutGrid }, + { value: 'story', label: "Story", description: "Visible 24 h, média requis", icon: Smartphone }, + { value: 'both', label: "Fil + Story", description: "Les deux à la fois", icon: Layers }, +] as const; + +function SummaryRow({ label, value, ok }: { label: string; value: string; ok: boolean }) { + return ( +
+
{label}
+
{value}
+
+ ); +} + export default function NewPost() { const [, navigate] = useLocation(); const { toast } = useToast(); @@ -125,6 +152,9 @@ export default function NewPost() { const [postText, setPostText] = useState(''); const [postType, setPostType] = useState<'feed' | 'story' | 'both'>('feed'); const [previewModalOpen, setPreviewModalOpen] = useState(false); + const [step, setStep] = useState(0); + const [aiOpen, setAiOpen] = useState(false); + const [scheduleMode, setScheduleMode] = useState<'now' | 'later'>('now'); // Ce composeur gère les publications feed/story : TikTok n'accepte que des // vidéos et se pilote depuis la création de reel. @@ -321,10 +351,6 @@ export default function NewPost() { const handleUseVariant = (text: string) => { setPostText(text); - toast({ - title: "Texte sélectionné", - description: "Le texte a été ajouté à votre publication", - }); }; const handleCameraCapture = (e: React.ChangeEvent) => { @@ -383,95 +409,141 @@ export default function NewPost() { } }; + const contentReady = postText.trim().length > 0 || selectedMedia.length > 0; + const needsMedia = postType === 'story' || postType === 'both'; + const targetsReady = selectedPages.length > 0 && (!needsMedia || selectedMedia.length > 0); + + const lockedReason = (index: number): string | null => { + if (index >= 2 && !contentReady) return "Ajoutez un média ou un texte"; + if (index >= 3 && !targetsReady) return selectedPages.length === 0 ? "Choisissez au moins une page" : "Les stories nécessitent un média"; + return null; + }; + + const goTo = (index: number) => { + const reason = lockedReason(index); + if (reason) { + toast({ title: "Étape incomplète", description: reason, variant: "destructive" }); + return; + } + setStep(index); + window.scrollTo({ top: 0, behavior: "smooth" }); + }; + + const formatLabel = postType === 'feed' ? "Fil d'actualité" : postType === 'story' ? "Story" : "Fil + Story"; + const selectedPageNames = pages.filter(p => selectedPages.includes(p.id)).map(p => p.pageName); + + const toggleMedia = (mediaId: string) => { + if (selectedMedia.includes(mediaId)) { + setSelectedMedia(prev => prev.filter(id => id !== mediaId)); + } else if (selectedMedia.length < 10) { + setSelectedMedia(prev => [...prev, mediaId]); + } else { + toast({ + title: "Limite atteinte", + description: "Maximum 10 médias par publication", + variant: "destructive", + }); + } + }; + return ( <> -
-

Nouvelle publication

-

- Créez et planifiez une nouvelle publication -

-
+ -
-
- - -
-
- Média - Sélectionnez ou uploadez une image/vidéo -
-
- - -
+ + +
+
+ + + {/* ÉTAPE 1 : Médias */} + {step === 0 && ( + + +
+ Choisissez vos médias + Jusqu'à 10 photos ou vidéos. Facultatif pour une publication texte. +
+
+ +
- -
+
{mediaList.length === 0 ? ( -
- -

- {isDragActive ? "Déposez votre photo ici" : "Aucun média disponible"} -

-
- - -
-
+ + + +
+ } + /> ) : ( <> {selectedMedia.length > 0 && ( -
-
- Photos sélectionnées ({selectedMedia.length}/10) +
+
+

Sélection ({selectedMedia.length}/10)

+

Glissez pour réordonner

-
+
{selectedMedia.map((mediaId, index) => { const media = mediaList.find(m => m.id === mediaId); if (!media) return null; @@ -504,252 +576,361 @@ export default function NewPost() {
)} -
- Toutes les photos ({mediaList.length}) -
-
-
- {mediaList.map((media) => { - const isSelected = selectedMedia.includes(media.id); - const isVideo = media.type === 'video'; +

Médias récents

+
+ {mediaList.map((media) => { + const isSelected = selectedMedia.includes(media.id); + const isVideo = media.type === 'video'; + const order = selectedMedia.indexOf(media.id); - return ( - - ); - })} -
+ return ( + + ); + })}
)}
+ )} - - - Contenu - Informations du produit ou message - - -
- -