- 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 (
-
{
- if (isSelected) {
- // Remove from selection
- setSelectedMedia(prev => prev.filter(id => id !== media.id));
- } else if (selectedMedia.length < 10) {
- // Add to selection (max 10)
- setSelectedMedia(prev => [...prev, media.id]);
- } else {
- toast({
- title: "Limite atteinte",
- description: "Maximum 10 photos par publication",
- variant: "destructive",
- });
- }
- }}
- className={`relative aspect-square rounded-lg overflow-hidden border-2 transition-all ${isSelected
- ? 'border-primary ring-2 ring-primary opacity-50'
- : 'border-transparent hover:border-muted-foreground'
- }`}
- data-testid={`button-select-media-${media.id}`}
- >
-
- {isSelected && (
-
- )}
-
- );
- })}
-
+ return (
+
toggleMedia(media.id)}
+ className={`relative aspect-square overflow-hidden rounded-lg border-2 transition-all ${isSelected
+ ? 'border-primary'
+ : 'border-transparent hover:border-primary/40'
+ }`}
+ aria-pressed={isSelected}
+ data-testid={`button-select-media-${media.id}`}
+ >
+
+ {isSelected && (
+
+
+ {order + 1}
+
+
+ )}
+
+ );
+ })}
>
)}
+ )}
-
-
- Contenu
- Informations du produit ou message
-
-
-
- Informations du produit
-
-
-
- {generateTextMutation.isPending ? 'Génération...' : 'Générer le texte IA'}
-
-
-
-
- {generatedVariants.length > 0 && (
-
-
- Variations générées par l'IA
- Cliquez sur "Utiliser" pour remplir le texte ci-dessous
+ {/* ÉTAPE 2 : Texte */}
+ {step === 1 && (
+
+
+
+ Rédigez votre texte
+ Écrivez-le vous-même ou laissez l'IA vous proposer des versions.
-
- {generatedVariants.map((variant, index) => (
-
-
-
-
- {variant.variant || `Version ${index + 1}`}
-
-
- {variant.text}
-
+
+
+
+
+
+
+
+ Générer avec l'IA
+ Décrivez votre produit, l'IA rédige 3 propositions
+
+
+
+
+
+ )}
+
+
+
+
+
+ Texte de la publication
+ {postText.length} caractères
- ))}
+
- )}
+
+ )}
-
-
- Texte de la publication
- Écrivez ou générez le texte de votre publication
-
-
-
-
-
+ {/* ÉTAPE 3 : Diffusion */}
+ {step === 2 && (
+
+
+
+ Où publier ?
+ Sélectionnez une ou plusieurs pages.
+
+
+ {pages.length === 0 ? (
+ navigate('/pages')} data-testid="link-add-pages">
+ Connecter un compte
+
+ }
+ />
+ ) : (
+
+ {pages.map((page) => {
+ const checked = selectedPages.includes(page.id);
+ return (
+
+
+
+ {page.pageName}
+ {platformLabel(page.platform)}
+
+ {
+ if (value) {
+ setSelectedPages([...selectedPages, page.id]);
+ } else {
+ setSelectedPages(selectedPages.filter(id => id !== page.id));
+ }
+ }}
+ data-testid={`checkbox-page-${page.id}`}
+ />
+
+ );
+ })}
+
+ )}
+
+
-
-
-
- Pages cibles
- Sélectionnez les pages où publier
-
-
- {pages.length === 0 ? (
-
-
Aucune page connectée
-
navigate('/pages')}
- data-testid="link-add-pages"
- >
- Ajouter des pages
-
-
- ) : (
-
- {pages.map((page) => (
-
- {
- if (checked) {
- setSelectedPages([...selectedPages, page.id]);
- } else {
- setSelectedPages(selectedPages.filter(id => id !== page.id));
- }
- }}
- data-testid={`checkbox-page-${page.id}`}
- />
-
- {page.pageName} ({page.platform})
-
-
+
+
+ Format
+ Comment la publication apparaîtra.
+
+
+ setPostType(value as 'feed' | 'story' | 'both')}
+ className="grid grid-cols-1 gap-2 sm:grid-cols-3"
+ data-testid="select-post-type"
+ >
+ {FORMATS.map(({ value, label, description, icon: Icon }) => (
+
+
+
+
+
+
+
{label}
+
{description}
+
+
))}
+
+ {needsMedia && selectedMedia.length === 0 && (
+
+
+ Une story nécessite au moins une image ou vidéo : revenez à l'étape Médias.
+
+ )}
+
+
+
+ )}
+
+ {/* ÉTAPE 4 : Planification */}
+ {step === 3 && (
+
+
+ Quand publier ?
+ Publiez tout de suite ou choisissez une date.
+
+
+ {
+ setScheduleMode(value as 'now' | 'later');
+ if (value === 'now') setScheduledDate(undefined);
+ }}
+ className="grid grid-cols-1 gap-2 sm:grid-cols-2"
+ >
+ {([
+ { value: 'now', label: 'Publier maintenant', description: 'Envoi immédiat après validation', icon: Zap },
+ { value: 'later', label: 'Programmer', description: 'Choisir une date et une heure', icon: CalendarClock },
+ ] as const).map(({ value, label, description, icon: Icon }) => (
+
+
+
+ {label}
+ {description}
+
+
+
+ ))}
+
+
+ {scheduleMode === 'later' && (
+
+
Date et heure
+
post.scheduledAt)
+ .map(post => new Date(post.scheduledAt!))}
+ placeholder="Choisir une date"
+ />
+ Les créneaux déjà occupés sont signalés dans le calendrier.
)}
+ )}
-
-
- Type de publication
- Choisissez où publier
-
-
-
-
Format
-
setPostType(value)}>
-
-
-
-
- Feed uniquement
- Story uniquement
- Feed + Story
-
-
-
- {postType === 'feed' && 'Publication classique dans le fil d\'actualité'}
- {postType === 'story' && 'Story éphémère (24h) - Nécessite un média'}
- {postType === 'both' && 'Publie à la fois dans le feed et en story - Nécessite un média'}
-
-
-
-
-
-
-
- Planification
- Programmez la publication (optionnel)
-
-
-
-
Date et heure
-
post.scheduledAt)
- .map(post => new Date(post.scheduledAt!))}
- placeholder="Publier immédiatement"
- />
-
- Sélectionnez une date pour planifier, sinon publication immédiate
-
-
-
-
-
- setPreviewModalOpen(true)}
- disabled={createPostMutation.isPending}
- className="w-full bg-gradient-to-r from-primary to-secondary hover:opacity-90 shadow-lg"
- size="lg"
- data-testid="button-preview-post"
- >
-
- Prévisualiser et publier
-
-
+
goTo(step - 1)}
+ backDisabled={step === 0}
+ onNext={() => goTo(step + 1)}
+ nextLabel={step === 0 && selectedMedia.length === 0 ? "Continuer sans média" : "Continuer"}
+ next={
+ step === STEPS.length - 1 ? (
+ {
+ if (scheduleMode === 'later' && !scheduledDate) {
+ toast({ title: "Date manquante", description: "Choisissez une date ou publiez maintenant.", variant: "destructive" });
+ return;
+ }
+ setPreviewModalOpen(true);
+ }}
+ disabled={createPostMutation.isPending}
+ variant="brand"
+ className="w-full sm:w-auto"
+ data-testid="button-preview-post"
+ >
+
+ Prévisualiser et {scheduleMode === 'later' ? 'programmer' : 'publier'}
+
+ ) : undefined
+ }
+ />
-
+
+ {/* Récapitulatif */}
+
+
+
+ Récapitulatif
+
+
+ {selectedMedia.length > 0 ? (
+
+ {selectedMedia.slice(0, 8).map((id) => {
+ const media = mediaList.find(m => m.id === id);
+ if (!media) return null;
+ return (
+
+
+
+ );
+ })}
+
+ ) : (
+
+
+
+ )}
+ {postText.trim() && {postText}
}
+
+ 0} />
+
+ 0} />
+
+
+
+
+
+
+
+
@@ -757,13 +938,11 @@ export default function NewPost() {
-
- Upload en cours...
+
+ Import en cours…
- Votre image est en cours de téléchargement et de traitement.
-
- Veuillez patienter.
+ Votre fichier est en cours d'envoi et de traitement.
diff --git a/client/src/pages/new-reel.tsx b/client/src/pages/new-reel.tsx
index a03f499..dc840d7 100644
--- a/client/src/pages/new-reel.tsx
+++ b/client/src/pages/new-reel.tsx
@@ -536,7 +536,7 @@ export default function NewReel() {
disabled={uploadMutation.isPending}
variant="outline"
>
-
+
{uploadMutation.isPending ? 'Upload...' : 'Uploader'}
@@ -717,7 +717,7 @@ export default function NewReel() {
disabled={generateTextMutation.isPending}
className="w-full"
>
-
+
{generateTextMutation.isPending ? 'Génération...' : 'Générer 3 variations'}
@@ -976,12 +976,12 @@ export default function NewReel() {
>
{createReelMutation.isPending ? (
<>
-
+
Création...
>
) : (
<>
-
+
Publier le Reel
>
)}
diff --git a/client/src/pages/pages.tsx b/client/src/pages/pages.tsx
index 52107f2..5aa7919 100644
--- a/client/src/pages/pages.tsx
+++ b/client/src/pages/pages.tsx
@@ -183,15 +183,15 @@ export default function PagesManagement() {
{page.platform === "tiktok" ? (
{ window.location.href = "/api/tiktok/connect"; }} data-testid={`button-reconnect-page-${page.id}`}>
- Reconnecter
+ Reconnecter
) : (
<>
setEditingPage(page)} data-testid={`button-edit-page-${page.id}`}>
- Remplacer le jeton
+ Remplacer le jeton
window.open(`https://developers.facebook.com/tools/debug/accesstoken/`, "_blank")} data-testid="button-debug-token">
- Déboguer le jeton
+ Déboguer le jeton
>
)}
@@ -201,7 +201,7 @@ export default function PagesManagement() {
className="text-destructive focus:text-destructive"
data-testid={`button-delete-page-${page.id}`}
>
- Déconnecter
+ Déconnecter
diff --git a/client/src/pages/schedule-reel.tsx b/client/src/pages/schedule-reel.tsx
index 4de9961..c9d909f 100644
--- a/client/src/pages/schedule-reel.tsx
+++ b/client/src/pages/schedule-reel.tsx
@@ -269,9 +269,9 @@ export default function ScheduleReel() {
data-testid="button-schedule-reel"
>
{scheduleMutation.isPending ? (
-
+
) : (
-
+
)}
Programmer le Reel
diff --git a/client/src/pages/settings.tsx b/client/src/pages/settings.tsx
index 0b85654..b9f357b 100644
--- a/client/src/pages/settings.tsx
+++ b/client/src/pages/settings.tsx
@@ -1,7 +1,8 @@
import { useState, useEffect } from "react";
import { Page } from "@/components/layout/app-shell";
import { PageHeader } from "@/components/layout/page-header";
-import { Settings as SettingsIcon, Bell, Key, Shield, Cloud, Brain, Image, Upload, X, Video, Plug, Mic, Facebook } from "lucide-react";
+import { Settings as SettingsIcon, Bell, Key, Shield, Cloud, Brain, Image, Upload, X, Video, Plug, Mic, Facebook, Share2, SlidersHorizontal } from "lucide-react";
+import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { useQuery, useMutation } from "@tanstack/react-query";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Label } from "@/components/ui/label";
@@ -15,7 +16,27 @@ import { ModelCombobox } from "@/components/model-combobox";
import { QwenSettingsCard } from "@/components/reels/qwen-settings-card";
import { SiTiktok } from "react-icons/si";
+const SETTINGS_TABS = [
+ { value: "general", label: "Général", icon: SlidersHorizontal },
+ { value: "ai", label: "Intelligence artificielle", icon: Brain },
+ { value: "media", label: "Stockage & vidéo", icon: Cloud },
+ { value: "social", label: "Réseaux sociaux", icon: Share2 },
+ { value: "api", label: "API externe", icon: Plug },
+] as const;
+
+type SettingsTab = (typeof SETTINGS_TABS)[number]["value"];
+
+function initialTab(): SettingsTab {
+ const hash = window.location.hash.replace("#", "");
+ return (SETTINGS_TABS.find((t) => t.value === hash)?.value ?? "general") as SettingsTab;
+}
+
export default function Settings() {
+ const [tab, setTab] = useState
(initialTab);
+ const changeTab = (value: string) => {
+ setTab(value as SettingsTab);
+ window.history.replaceState(null, "", `#${value}`);
+ };
const [notifications, setNotifications] = useState(true);
const [autoPublish, setAutoPublish] = useState(true);
const [cloudName, setCloudName] = useState("");
@@ -418,16 +439,31 @@ export default function Settings() {
return (
-
-
Paramètres
-
- Configurez votre application
-
-
+
-
-
-
+
+
+ {SETTINGS_TABS.map(({ value, label, icon: Icon }) => (
+
+
+ {label}
+
+ ))}
+
+
+
+
+
+
Notifications
@@ -468,8 +504,109 @@ export default function Settings() {
-
-
+ {isAdmin && (
+
+
+
+
+ Logo de l'entreprise
+
+
+ Uploadez votre logo pour l'ajouter aux images éditées
+
+
+
+ {logoPreview ? (
+
+
+
+
deleteLogoMutation.mutate()}
+ disabled={deleteLogoMutation.isPending}
+ data-testid="button-delete-logo"
+ >
+
+
+
+
+
+ document.getElementById('logo-upload-replace')?.click()}
+ >
+
+ {uploadLogoMutation.isPending ? "Upload en cours..." : "Remplacer le logo"}
+
+
+ {
+ handleLogoChange(e);
+ if (e.target.files?.[0]) {
+ uploadLogoMutation.mutate(e.target.files[0]);
+ }
+ }}
+ />
+
+
+ ) : (
+
+
+
+
+ Aucun logo uploadé. Choisissez une image PNG ou JPG.
+
+
+ document.getElementById('logo-upload')?.click()}
+ >
+
+ {uploadLogoMutation.isPending ? "Upload en cours..." : "Uploader un logo"}
+
+
+
{
+ handleLogoChange(e);
+ if (e.target.files?.[0]) {
+ uploadLogoMutation.mutate(e.target.files[0]);
+ }
+ }}
+ />
+
+ {!cloudinaryConfig && (
+
+ Veuillez d'abord configurer MinIO ci-dessus
+
+ )}
+
+ )}
+
+
+ )}
+
+
+
+
Configuration OpenRouter (IA)
@@ -492,7 +629,7 @@ export default function Settings() {
{hasExistingConfig ? (
<>
- ✓ Clé API configurée - Laissez vide pour garder la clé actuelle
+ ✓ Clé API configurée - Laissez vide pour garder la clé actuelle
>
) : (
<>
@@ -549,9 +686,59 @@ export default function Settings() {
+ {/* Google Gemini TTS */}
{isAdmin && (
-
-
+
+
+
+
+ Google Gemini TTS
+
+
+ Configurez votre clé API Google pour utiliser Gemini TTS dans les Reels
+
+
+
+
+
Clé API Google Gemini
+
setGeminiApiKey(e.target.value)}
+ placeholder={hasExistingGeminiConfig ? "••••••••••••••••" : "AIza..."}
+ data-testid="input-gemini-api-key"
+ />
+
+ {hasExistingGeminiConfig ? (
+ ✓ Clé API configurée
+ ) : (
+ "Obtenez une clé API sur console.cloud.google.com → APIs & Services → Credentials"
+ )}
+
+
+ saveGeminiMutation.mutate()}
+ disabled={
+ saveGeminiMutation.isPending ||
+ (!hasExistingGeminiConfig && !geminiApiKey)
+ }
+ data-testid="button-save-gemini"
+ className="w-full"
+ >
+ {saveGeminiMutation.isPending ? "Enregistrement..." : "Enregistrer Gemini TTS"}
+
+
+
+ )}
+
+ {/* Qwen3-TTS (serveur local ou GPU distant) */}
+ {isAdmin && }
+
+
+ {isAdmin && (
+
+
Stockage des médias
@@ -571,8 +758,8 @@ export default function Settings() {
)}
-
-
+
+
Configuration FFmpeg API (Reels)
@@ -609,7 +796,7 @@ export default function Settings() {
{hasExistingFfmpegConfig ? (
<>
- ✓ Clé API configurée - Laissez vide pour garder la clé actuelle
+ ✓ Clé API configurée - Laissez vide pour garder la clé actuelle
>
) : (
"Clé de sécurité X-API-Key pour accéder à l'API FFmpeg"
@@ -630,214 +817,11 @@ export default function Settings() {
-
- {/* Google Gemini TTS */}
+
+
{isAdmin && (
-
-
-
-
- Google Gemini TTS
-
-
- Configurez votre clé API Google pour utiliser Gemini TTS dans les Reels
-
-
-
-
-
Clé API Google Gemini
-
setGeminiApiKey(e.target.value)}
- placeholder={hasExistingGeminiConfig ? "••••••••••••••••" : "AIza..."}
- data-testid="input-gemini-api-key"
- />
-
- {hasExistingGeminiConfig ? (
- ✓ Clé API configurée
- ) : (
- "Obtenez une clé API sur console.cloud.google.com → APIs & Services → Credentials"
- )}
-
-
- saveGeminiMutation.mutate()}
- disabled={
- saveGeminiMutation.isPending ||
- (!hasExistingGeminiConfig && !geminiApiKey)
- }
- data-testid="button-save-gemini"
- className="w-full"
- >
- {saveGeminiMutation.isPending ? "Enregistrement..." : "Enregistrer Gemini TTS"}
-
-
-
- )}
-
- {/* Qwen3-TTS (serveur local ou GPU distant) */}
- {isAdmin && }
-
-
- {isAdmin && (
-
-
-
-
- Logo de l'entreprise
-
-
- Uploadez votre logo pour l'ajouter aux images éditées
-
-
-
- {logoPreview ? (
-
-
-
-
deleteLogoMutation.mutate()}
- disabled={deleteLogoMutation.isPending}
- data-testid="button-delete-logo"
- >
-
-
-
-
-
- document.getElementById('logo-upload-replace')?.click()}
- >
-
- {uploadLogoMutation.isPending ? "Upload en cours..." : "Remplacer le logo"}
-
-
- {
- handleLogoChange(e);
- if (e.target.files?.[0]) {
- uploadLogoMutation.mutate(e.target.files[0]);
- }
- }}
- />
-
-
- ) : (
-
-
-
-
- Aucun logo uploadé. Choisissez une image PNG ou JPG.
-
-
- document.getElementById('logo-upload')?.click()}
- >
-
- {uploadLogoMutation.isPending ? "Upload en cours..." : "Uploader un logo"}
-
-
-
{
- handleLogoChange(e);
- if (e.target.files?.[0]) {
- uploadLogoMutation.mutate(e.target.files[0]);
- }
- }}
- />
-
- {!cloudinaryConfig && (
-
- Veuillez d'abord configurer MinIO ci-dessus
-
- )}
-
- )}
-
-
- )}
-
- {isAdmin && (
-
-
-
-
- API externe
-
-
- Clé d'accès pour créer et programmer des publications via l'API externe (POST /api/v1/publish)
-
-
-
- {hasExistingExternalApiConfig && (
-
-
-
- Clé API configurée et active
-
-
deleteExternalApiMutation.mutate()}
- disabled={deleteExternalApiMutation.isPending}
- >
- {deleteExternalApiMutation.isPending ? "Suppression..." : "Révoquer"}
-
-
- )}
-
-
- {hasExistingExternalApiConfig ? "Nouvelle clé API (remplace l'actuelle)" : "Clé API"}
-
-
setExternalApiKey(e.target.value)}
- placeholder={hasExistingExternalApiConfig ? "Nouvelle clé pour remplacer l'actuelle" : "Générez une clé sécurisée"}
- />
-
- Utilisez cette clé dans le header X-API-Key de vos requêtes
-
-
- saveExternalApiMutation.mutate()}
- disabled={saveExternalApiMutation.isPending || !externalApiKey.trim()}
- className="w-full"
- >
- {saveExternalApiMutation.isPending ? "Enregistrement..." : hasExistingExternalApiConfig ? "Remplacer la clé" : "Enregistrer la clé"}
-
-
-
- )}
-
- {isAdmin && (
-
-
+
+
Facebook
@@ -850,7 +834,7 @@ export default function Settings() {
{facebookConfig?.configured && (
-
+
Application Facebook configurée
@@ -910,20 +894,20 @@ export default function Settings() {
)}
{isAdmin && (
-
-
+
+
TikTok
Identifiants de votre application TikTok (developers.tiktok.com). Ils servent à
- connecter les comptes TikTok de chaque magasin depuis la page « Pages gérées ».
+ connecter les comptes TikTok de chaque magasin depuis la page « Comptes connectés ».
{tiktokConfig?.configured && (
-
+
Application TikTok configurée
@@ -973,24 +957,64 @@ export default function Settings() {
)}
-
-
-
-
-
- Sécurité
-
-
- Paramètres de sécurité et confidentialité
-
-
-
-
- Les paramètres de sécurité avancés seront disponibles prochainement.
-
-
-
-
-
+
+
+ {isAdmin && (
+
+
+
+
+ API externe
+
+
+ Clé d'accès pour créer et programmer des publications via l'API externe (POST /api/v1/publish)
+
+
+
+ {hasExistingExternalApiConfig && (
+
+
+
+ Clé API configurée et active
+
+
deleteExternalApiMutation.mutate()}
+ disabled={deleteExternalApiMutation.isPending}
+ >
+ {deleteExternalApiMutation.isPending ? "Suppression..." : "Révoquer"}
+
+
+ )}
+
+
+ {hasExistingExternalApiConfig ? "Nouvelle clé API (remplace l'actuelle)" : "Clé API"}
+
+
setExternalApiKey(e.target.value)}
+ placeholder={hasExistingExternalApiConfig ? "Nouvelle clé pour remplacer l'actuelle" : "Générez une clé sécurisée"}
+ />
+
+ Utilisez cette clé dans le header X-API-Key de vos requêtes
+
+
+ saveExternalApiMutation.mutate()}
+ disabled={saveExternalApiMutation.isPending || !externalApiKey.trim()}
+ className="w-full"
+ >
+ {saveExternalApiMutation.isPending ? "Enregistrement..." : hasExistingExternalApiConfig ? "Remplacer la clé" : "Enregistrer la clé"}
+
+
+
+ )}
+
+
+
+
);
}