From 7c18dc1e1690a5b1b81198401b7e46c3735256ef Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 6 Oct 2026 12:47:49 +0000 Subject: [PATCH] =?UTF-8?q?Refonte=20UI=20:=20SQL,=20musiques,=20historiqu?= =?UTF-8?q?e,=20comptes=20connect=C3=A9s,=20utilisateurs?= 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 --- .../src/components/facebook-token-status.tsx | 32 +- client/src/pages/audio-admin.tsx | 257 ++++++----- client/src/pages/history.tsx | 314 +++++++------- client/src/pages/image-editor.tsx | 121 +----- client/src/pages/new-post.tsx | 31 +- client/src/pages/new-reel.tsx | 33 +- client/src/pages/pages.tsx | 407 +++++++++--------- client/src/pages/schedule-reel.tsx | 26 +- client/src/pages/settings.tsx | 29 +- client/src/pages/sql.tsx | 128 +++--- client/src/pages/users-admin.tsx | 340 +++++++-------- 11 files changed, 745 insertions(+), 973 deletions(-) diff --git a/client/src/components/facebook-token-status.tsx b/client/src/components/facebook-token-status.tsx index 8e4adeb..de58384 100644 --- a/client/src/components/facebook-token-status.tsx +++ b/client/src/components/facebook-token-status.tsx @@ -19,10 +19,10 @@ import { fr } from "date-fns/locale"; type TokenStatus = 'valid' | 'expiring' | 'expired' | 'error'; const STATUS_STYLES: Record = { - valid: { label: 'Jeton valide', color: 'text-green-600', bgColor: 'bg-green-100' }, - expiring: { label: 'Jeton bientôt expiré', color: 'text-orange-600', bgColor: 'bg-orange-100' }, - expired: { label: 'Jeton expiré', color: 'text-red-600', bgColor: 'bg-red-100' }, - error: { label: 'Contrôle en échec', color: 'text-red-600', bgColor: 'bg-red-100' }, + valid: { label: 'Connexion active', color: 'text-success', bgColor: 'bg-success/10' }, + expiring: { label: 'Expire bientôt', color: 'text-warning-foreground dark:text-warning', bgColor: 'bg-warning/15' }, + expired: { label: 'Connexion expirée', color: 'text-destructive', bgColor: 'bg-destructive/10' }, + error: { label: 'Contrôle en échec', color: 'text-destructive', bgColor: 'bg-destructive/10' }, }; function statusOf(page: ClientSocialPage): TokenStatus { @@ -33,14 +33,14 @@ function statusOf(page: ClientSocialPage): TokenStatus { * Lance l'autorisation Facebook. La connexion se fait par navigation complète * du navigateur (et non en fetch) puisqu'elle passe par le site de Facebook. */ -export function ConnectFacebookButton({ className }: { className?: string }) { +export function useConnectFacebook() { const { toast } = useToast(); const { data: config } = useQuery<{ configured: boolean }>({ queryKey: ['/api/facebook/config'], }); - const handleClick = () => { + return () => { if (config && !config.configured) { toast({ title: "Facebook n'est pas configuré", @@ -52,6 +52,10 @@ export function ConnectFacebookButton({ className }: { className?: string }) { } window.location.href = '/api/facebook/connect'; }; +} + +export function ConnectFacebookButton({ className }: { className?: string }) { + const handleClick = useConnectFacebook(); return ( ); @@ -108,8 +112,8 @@ export function RefreshTokenButton({ page }: { page: ClientSocialPage }) { disabled={refreshMutation.isPending} data-testid={`button-refresh-token-${page.id}`} > - - {refreshMutation.isPending ? 'Contrôle…' : 'Vérifier le jeton'} + + {refreshMutation.isPending ? 'Vérification…' : 'Vérifier'} ); } @@ -130,9 +134,9 @@ export function TokenHealthPanel({ page }: { page: ClientSocialPage }) {
{status === 'valid' ? ( - + ) : ( - + )} {style.label}
@@ -183,10 +187,10 @@ export function TokenAlertBanner({ pages }: { pages: ClientSocialPage[] }) { if (failing.length === 0) return null; return ( -
- +
+
-

+

{failing.length === 1 ? 'Une page nécessite votre attention' : `${failing.length} pages nécessitent votre attention`} diff --git a/client/src/pages/audio-admin.tsx b/client/src/pages/audio-admin.tsx index 8b8bcb0..0d13d7c 100644 --- a/client/src/pages/audio-admin.tsx +++ b/client/src/pages/audio-admin.tsx @@ -1,8 +1,18 @@ import { useState } from "react"; +import { Page } from "@/components/layout/app-shell"; +import { PageHeader } from "@/components/layout/page-header"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; -import Sidebar from "@/components/sidebar"; -import TopBar from "@/components/topbar"; -import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"; +import { EmptyState } from "@/components/empty-state"; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from "@/components/ui/alert-dialog"; import { Button } from "@/components/ui/button"; import { useToast } from "@/hooks/use-toast"; import { Trash2, Music, Upload, Loader2, Play, Pause } from "lucide-react"; @@ -20,10 +30,10 @@ interface AudioTrack { } export default function AudioAdmin() { - const [sidebarOpen, setSidebarOpen] = useState(false); const [isUploading, setIsUploading] = useState(false); const [playingTrackId, setPlayingTrackId] = useState(null); const [audioElement, setAudioElement] = useState(null); + const [trackToDelete, setTrackToDelete] = useState(null); const { toast } = useToast(); const queryClient = useQueryClient(); @@ -149,140 +159,115 @@ export default function AudioAdmin() { }; return ( -

- {sidebarOpen && ( -
setSidebarOpen(false)} + + + + + + } + /> + + {isLoading ? ( +
+ {[1, 2, 3].map((i) => ( +
+ ))} +
+ ) : tracks.length === 0 ? ( + + + + } /> + ) : ( + <> +

{tracks.length} piste(s)

+
+ {tracks.map((track) => { + const playing = playingTrackId === track.id; + return ( +
+ +
+

{track.title}

+

+ Ajoutée le {format(new Date(track.createdAt), "d MMM yyyy", { locale: fr })} +

+
+ +
+ ); + })} +
+ )} -
- setSidebarOpen(false)} /> -
- -
- setSidebarOpen(!sidebarOpen)} /> - -
-
-
-

- - Bibliothèque Audio -

-

- Gérez les musiques disponibles pour la création de Reels -

-
- -
- - -
-
- - - - Musiques ({tracks.length}) - - Ces pistes pourront être sélectionnées par les utilisateurs lors de la création d'un nouveau Reel. - - - - {isLoading ? ( -
- -
- ) : tracks.length === 0 ? ( -
- -

Aucune musique

-

- Vous n'avez pas encore ajouté de piste audio. Les musiques ajoutées ici seront utilisables pour les Reels. -

-
- ) : ( -
- {tracks.map((track) => ( -
-
-
- -
-
-

{track.title}

-

- {format(new Date(track.createdAt), "dd MMM yyyy", { locale: fr })} -

-
-
- -
- - - -
-
- ))} -
- )} -
-
-
-
-
+ !open && setTrackToDelete(null)}> + + + Supprimer cette musique ? + + « {trackToDelete?.title} » ne sera plus proposée dans les nouveaux Reels. + + + + Annuler + { + if (trackToDelete) deleteMutation.mutate(trackToDelete.id); + setTrackToDelete(null); + }} + > + Supprimer + + + + +
); } diff --git a/client/src/pages/history.tsx b/client/src/pages/history.tsx index 1f393e8..4d7e720 100644 --- a/client/src/pages/history.tsx +++ b/client/src/pages/history.tsx @@ -1,9 +1,14 @@ import { useState } from "react"; +import { Page } from "@/components/layout/app-shell"; +import { PageHeader } from "@/components/layout/page-header"; import { useQuery } from "@tanstack/react-query"; -import { Clock, CheckCircle, XCircle, Calendar, History as HistoryIcon, Eye, Image as ImageIcon, Smartphone, Loader2, Clapperboard } from "lucide-react"; -import Sidebar from "@/components/sidebar"; -import TopBar from "@/components/topbar"; -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { CheckCircle, XCircle, Calendar, History as HistoryIcon, Eye, Image as ImageIcon, Smartphone, Loader2, Clapperboard, Search, Sparkles } from "lucide-react"; +import { Link } from "wouter"; +import { Card } from "@/components/ui/card"; +import { Input } from "@/components/ui/input"; +import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { EmptyState } from "@/components/empty-state"; +import { PlatformIcon } from "@/components/platform-icon"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import type { ScheduledPost, SocialPage, Post, Media } from "@shared/schema"; @@ -26,8 +31,21 @@ function isAwaitingPublication(scheduledPost: ScheduledPostWithRelations): boole ); } +type StatusFilter = "all" | "published" | "pending" | "failed"; + +const STATUS_TABS: { value: StatusFilter; label: string }[] = [ + { value: "all", label: "Toutes" }, + { value: "published", label: "Publiées" }, + { value: "pending", label: "En cours" }, + { value: "failed", label: "Échecs" }, +]; + +const PAGE_SIZE = 20; + export default function History() { - const [sidebarOpen, setSidebarOpen] = useState(false); + const [searchTerm, setSearchTerm] = useState(""); + const [statusFilter, setStatusFilter] = useState("all"); + const [visibleCount, setVisibleCount] = useState(PAGE_SIZE); const [previewModalOpen, setPreviewModalOpen] = useState(false); const [previewData, setPreviewData] = useState<{ postText: string; mediaIds: string[]; allMedia: Media[]; generationStatus?: string; generationProgress?: number }>({ postText: '', mediaIds: [], allMedia: [] }); const { toast } = useToast(); @@ -81,11 +99,11 @@ export default function History() { const getPostTypeIcon = (postType: string) => { if (postType === 'feed') { - return ; + return ; } else if (postType === 'story') { - return ; + return ; } else if (postType === 'reel') { - return ; + return ; } else if (postType === 'both') { return (
@@ -97,155 +115,150 @@ export default function History() { return null; }; + const statusOf = (sp: ScheduledPostWithRelations): StatusFilter => + sp.error ? "failed" : !sp.publishedAt ? "pending" : "published"; + + const counts = publishedPosts.reduce( + (acc, sp) => { + acc[statusOf(sp)] += 1; + return acc; + }, + { all: publishedPosts.length, published: 0, pending: 0, failed: 0 } as Record, + ); + + const term = searchTerm.trim().toLowerCase(); + const filteredPosts = publishedPosts.filter( + (sp) => + (statusFilter === "all" || statusOf(sp) === statusFilter) && + (!term || + sp.post?.content?.toLowerCase().includes(term) || + sp.page?.pageName?.toLowerCase().includes(term)), + ); + const visiblePosts = filteredPosts.slice(0, visibleCount); + return ( -
- {sidebarOpen && ( -
setSidebarOpen(false)} + <> + + - )} -
- setSidebarOpen(false)} /> -
- -
- setSidebarOpen(!sidebarOpen)} /> - -
-
-

Historique

-

- Consultez l'historique de vos publications -

-
- - {isLoading ? ( -
- {[1, 2, 3].map((i) => ( -
+
+ { setStatusFilter(v as StatusFilter); setVisibleCount(PAGE_SIZE); }}> + + {STATUS_TABS.map((t) => ( + + {t.label} + {counts[t.value]} + ))} -
- ) : publishedPosts.length === 0 ? ( - - -
- -
-

Aucune publication

-

- Vos publications apparaîtront ici une fois publiées -

-
-
- ) : ( -
- {publishedPosts.map((scheduledPost) => { - const isPending = !scheduledPost.publishedAt; - const hasError = !!scheduledPost.error; + + +
+ + { setSearchTerm(e.target.value); setVisibleCount(PAGE_SIZE); }} + className="pl-9" + data-testid="input-history-search" + /> +
+
- return ( - - -
-
-
-
- {hasError ? ( - - ) : isPending ? ( - - ) : ( - - )} -
- - {scheduledPost.page?.pageName || 'Page inconnue'} - -
-

- {scheduledPost.post?.content || 'Aucun texte'} -

-
-
- - - {hasError ? ( - <> - - Échoué - - ) : isPending ? ( - <> - - Traitement... - - ) : ( - <> - - Publié - - )} - -
-
-
- -
-
- {getPostTypeIcon(scheduledPost.postType)} - - {scheduledPost.postType === 'feed' ? 'Feed' : - scheduledPost.postType === 'story' ? 'Story' : - scheduledPost.postType === 'reel' ? 'Reel' : 'Feed & Story'} - -
-
- - - {scheduledPost.scheduledAt - ? format(new Date(scheduledPost.scheduledAt), "d MMM yyyy 'à' HH:mm", { locale: fr }) - : 'Date inconnue' - } - -
+ {isLoading ? ( +
+ {[1, 2, 3, 4].map((i) => ( +
+ ))} +
+ ) : publishedPosts.length === 0 ? ( + + Créer une publication + + } + /> + ) : filteredPosts.length === 0 ? ( + + ) : ( +
+ {visiblePosts.map((scheduledPost) => { + const status = statusOf(scheduledPost); + return ( + +
+ +
+
+

{scheduledPost.page?.pageName || "Page inconnue"}

+ {status === "failed" ? ( + Échec + ) : status === "pending" ? ( + En cours + ) : ( + Publié + )} {scheduledPost.post?.aiGenerated && ( - - - Généré par IA - + IA )}
- - - ); - })} -
- )} -
+

+ {scheduledPost.post?.content || Aucun texte} +

+
+ + {getPostTypeIcon(scheduledPost.postType)} + {scheduledPost.postType === "feed" + ? "Feed" + : scheduledPost.postType === "story" + ? "Story" + : scheduledPost.postType === "reel" + ? "Reel" + : "Feed & Story"} + + + + {scheduledPost.scheduledAt + ? format(new Date(scheduledPost.scheduledAt), "d MMM yyyy 'à' HH:mm", { locale: fr }) + : "Date inconnue"} + +
+ {scheduledPost.error && ( +

{scheduledPost.error}

+ )} +
+ +
+ + ); + })} + {filteredPosts.length > visibleCount && ( +
+ +
+ )} +
+ )} + -
-
+ ); } diff --git a/client/src/pages/image-editor.tsx b/client/src/pages/image-editor.tsx index d9fbce0..51dcb73 100644 --- a/client/src/pages/image-editor.tsx +++ b/client/src/pages/image-editor.tsx @@ -1,9 +1,9 @@ import { useState, useEffect, useRef } from "react"; +import { Page } from "@/components/layout/app-shell"; +import { PageHeader } from "@/components/layout/page-header"; import { useQuery, useMutation } from "@tanstack/react-query"; import { apiRequest, queryClient } from "@/lib/queryClient"; import { toPng } from 'html-to-image'; -import Sidebar from "@/components/sidebar"; -import TopBar from "@/components/topbar"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; @@ -39,7 +39,6 @@ interface Logo { } export default function ImageEditor() { - const [sidebarOpen, setSidebarOpen] = useState(false); const [selectedMedia, setSelectedMedia] = useState(null); const [ribbon, setRibbon] = useState({ @@ -119,117 +118,7 @@ export default function ImageEditor() { }, [selectedMedia, ribbon, priceBadge, logo]); return ( -
- - - {sidebarOpen && ( -
setSidebarOpen(false)} - /> - )} - -
- setSidebarOpen(false)} /> -
- -
- setSidebarOpen(!sidebarOpen)} /> - -
+

@@ -634,8 +523,6 @@ export default function ImageEditor() {

-
- -
+ ); } diff --git a/client/src/pages/new-post.tsx b/client/src/pages/new-post.tsx index 0dd2c0d..4ee4b2c 100644 --- a/client/src/pages/new-post.tsx +++ b/client/src/pages/new-post.tsx @@ -1,4 +1,6 @@ import { useState, useCallback, useRef, useMemo } from "react"; +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"; @@ -20,8 +22,6 @@ import { useSortable, } from "@dnd-kit/sortable"; import { CSS } from "@dnd-kit/utilities"; -import Sidebar from "@/components/sidebar"; -import TopBar from "@/components/topbar"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Textarea } from "@/components/ui/textarea"; @@ -113,7 +113,6 @@ function startOfToday(): Date { } export default function NewPost() { - const [sidebarOpen, setSidebarOpen] = useState(false); const [, navigate] = useLocation(); const { toast } = useToast(); const cameraInputRef = useRef(null); @@ -385,25 +384,8 @@ export default function NewPost() { }; return ( -
- {sidebarOpen && ( -
setSidebarOpen(false)} - /> - )} - -
- setSidebarOpen(false)} /> -
- -
- setSidebarOpen(!sidebarOpen)} /> - -
+ <> +

Nouvelle publication

@@ -767,8 +749,7 @@ export default function NewPost() {

-
- + @@ -797,6 +778,6 @@ export default function NewPost() { onPublish={handleCreatePost} isPublishing={createPostMutation.isPending} /> -
+ ); } diff --git a/client/src/pages/new-reel.tsx b/client/src/pages/new-reel.tsx index db1c737..a03f499 100644 --- a/client/src/pages/new-reel.tsx +++ b/client/src/pages/new-reel.tsx @@ -1,4 +1,6 @@ import { useState, useCallback, useRef, useEffect } from "react"; +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 { @@ -7,8 +9,6 @@ import { ChevronRight, Loader2, Check, RefreshCw, Mic } from "lucide-react"; import { useDropzone } from "react-dropzone"; -import Sidebar from "@/components/sidebar"; -import TopBar from "@/components/topbar"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Textarea } from "@/components/ui/textarea"; @@ -55,7 +55,6 @@ interface MusicTrack { type Step = 'video' | 'music' | 'text' | 'publish'; export default function NewReel() { - const [sidebarOpen, setSidebarOpen] = useState(false); const [, navigate] = useLocation(); const { toast } = useToast(); const audioRef = useRef(null); @@ -443,25 +442,8 @@ export default function NewReel() { }; return ( -
- {sidebarOpen && ( -
setSidebarOpen(false)} - /> - )} - -
- setSidebarOpen(false)} /> -
- -
- setSidebarOpen(!sidebarOpen)} /> - -
+ <> +

-
-
- -
+
+ + ); } diff --git a/client/src/pages/pages.tsx b/client/src/pages/pages.tsx index d63d8e5..52107f2 100644 --- a/client/src/pages/pages.tsx +++ b/client/src/pages/pages.tsx @@ -1,12 +1,31 @@ import { useEffect, useState } from "react"; +import { Page } from "@/components/layout/app-shell"; +import { PageHeader } from "@/components/layout/page-header"; import { useQuery, useMutation } from "@tanstack/react-query"; -import { Plus, Facebook, Instagram, Trash2, RefreshCw, Edit, Bug, Code, AlertTriangle } from "lucide-react"; -import { SiTiktok } from "react-icons/si"; -import Sidebar from "@/components/sidebar"; -import TopBar from "@/components/topbar"; +import { Plus, Trash2, RefreshCw, Bug, Code, ChevronDown, HelpCircle, KeyRound, Link2, MoreVertical } from "lucide-react"; +import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from "@/components/ui/alert-dialog"; +import { EmptyState } from "@/components/empty-state"; +import { PlatformIcon, platformLabel } from "@/components/platform-icon"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; -import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog"; +import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "@/components/ui/dialog"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; @@ -14,16 +33,16 @@ import { useToast } from "@/hooks/use-toast"; import { apiRequest, queryClient } from "@/lib/queryClient"; import type { ClientSocialPage } from "@shared/schema"; import { - ConnectFacebookButton, + useConnectFacebook, RefreshTokenButton, TokenAlertBanner, TokenHealthPanel, } from "@/components/facebook-token-status"; export default function PagesManagement() { - const [sidebarOpen, setSidebarOpen] = useState(false); const [dialogOpen, setDialogOpen] = useState(false); const [editingPage, setEditingPage] = useState(null); + const [pageToDelete, setPageToDelete] = useState(null); const { toast } = useToast(); // Retour des flux d'autorisation (/api/tiktok/callback et /api/facebook/callback @@ -56,8 +75,8 @@ export default function PagesManagement() { onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['/api/pages'] }); toast({ - title: "Page supprimée", - description: "La page a été retirée avec succès", + title: "Compte déconnecté", + description: "Le compte a été retiré de Social Flow", }); }, onError: () => { @@ -69,180 +88,189 @@ export default function PagesManagement() { }, }); + const connectFacebook = useConnectFacebook(); + const connectTiktok = useConnectTiktok(); + return ( -
- {sidebarOpen && ( -
setSidebarOpen(false)} - /> - )} - -
- setSidebarOpen(false)} /> -
- -
- setSidebarOpen(!sidebarOpen)} /> - -
-
-
-

Pages gérées

-

- Connectez et gérez vos pages Facebook, Instagram et vos comptes TikTok -

-
-
- - - -
- !open && setEditingPage(null)} /> -
- - - - {/* Permissions required notice */} -
- -
-

Connectez vos pages plutôt que de coller un jeton

-

- « Connecter des pages Facebook » récupère les jetons de page via l'autorisation Facebook : - ils n'expirent pas et SocialFlow les régénère seul en cas de révocation. Un jeton collé à la - main expire au bout de 60 jours et devra être remplacé manuellement. -

-

- Le jeton doit couvrir : pages_manage_posts{" "} - pages_read_engagement{" "} - publish_video -

-
-
- -
- - -
- - {isLoading ? ( -
- {[1, 2, 3].map((i) => ( -
- ))} -
- ) : pages.length === 0 ? ( - - -
- + + + + + + + + +
+

Facebook & Instagram

+

Recommandé · connexion permanente

-

Aucune page connectée

-

- Commencez par connecter vos pages Facebook et Instagram -

- +
+ + +
+

TikTok

+

Autorisation via TikTok

+
+
+ + setDialogOpen(true)} className="items-start gap-3 py-2.5" data-testid="button-add-page"> + + + +
+

Ajout manuel

+

Avec un jeton d'accès (expire après 60 jours)

+
+
+
+ + } + /> + + !open && setEditingPage(null)} /> + + + + {isLoading ? ( +
+ {[1, 2].map((i) => ( +
+ ))} +
+ ) : pages.length === 0 ? ( + + + +
+ } + /> + ) : ( +
+ {pages.map((page) => ( + + + +
+ {page.pageName} + + {platformLabel(page.platform)} · {page.pageId} + +
+ + + + + + {page.platform === "tiktok" ? ( + { window.location.href = "/api/tiktok/connect"; }} data-testid={`button-reconnect-page-${page.id}`}> + Reconnecter + + ) : ( + <> + setEditingPage(page)} data-testid={`button-edit-page-${page.id}`}> + Remplacer le jeton + + window.open(`https://developers.facebook.com/tools/debug/accesstoken/`, "_blank")} data-testid="button-debug-token"> + Déboguer le jeton + + + )} + + setPageToDelete(page)} + className="text-destructive focus:text-destructive" + data-testid={`button-delete-page-${page.id}`} + > + Déconnecter + + + +
+ + {/* État du jeton, tel que constaté par le dernier contrôle serveur */} + + {page.platform !== "tiktok" && ( +
+ +
+ )}
- ) : ( -
- {pages.map((page) => ( - - -
-
- {page.platform === 'facebook' ? ( -
- -
- ) : page.platform === 'tiktok' ? ( -
- -
- ) : ( -
- -
- )} -
- {page.pageName} - - {page.platform} - -
-
-
-
- -
- ID: {page.pageId} -
- - {/* État du jeton, tel que constaté par le dernier contrôle serveur */} - - -
- {page.platform !== 'tiktok' && } - {page.platform === 'tiktok' ? ( - // Un token TikTok ne se saisit pas : on repasse par l'autorisation - - ) : ( - - )} - -
-
-
- ))} -
- )} + ))}
-
-
+ )} + + + + + Besoin d'aide pour connecter un compte ? + + + +

+ Préférez « Facebook & Instagram » plutôt que l'ajout manuel : + les jetons obtenus ainsi n'expirent pas et Social Flow les régénère seul en cas de révocation. Un jeton + collé à la main expire au bout de 60 jours et devra être remplacé. +

+

+ Permissions nécessaires : pages_manage_posts{" "} + pages_read_engagement{" "} + publish_video +

+
+ + +
+
+
+ + !open && setPageToDelete(null)}> + + + Déconnecter « {pageToDelete?.pageName} » ? + + Social Flow ne pourra plus publier sur ce compte. Vous pourrez le reconnecter à tout moment. + + + + Annuler + { + if (pageToDelete) deleteMutation.mutate(pageToDelete.id); + setPageToDelete(null); + }} + > + Déconnecter + + + + + ); } @@ -250,7 +278,7 @@ export default function PagesManagement() { * Lance l'autorisation TikTok. La connexion se fait par navigation complète du * navigateur (et non en fetch) puisqu'elle passe par le site de TikTok. */ -function ConnectTiktokButton() { +function useConnectTiktok() { const { toast } = useToast(); const { data: config } = useQuery<{ configured: boolean }>({ @@ -269,12 +297,7 @@ function ConnectTiktokButton() { window.location.href = '/api/tiktok/connect'; }; - return ( - - ); + return handleClick; } function AddPageDialog({ open, onOpenChange }: { open: boolean; onOpenChange: (open: boolean) => void }) { @@ -320,17 +343,11 @@ function AddPageDialog({ open, onOpenChange }: { open: boolean; onOpenChange: (o return ( - - - - Ajouter une page + Ajouter un compte manuellement - Connectez une page Facebook ou Instagram pour commencer à publier + Renseignez l'identifiant de la page et un jeton d'accès. Ce jeton expirera au bout de 60 jours.
@@ -411,7 +428,7 @@ function AddPageDialog({ open, onOpenChange }: { open: boolean; onOpenChange: (o > {addMutation.isPending ? ( <> - + Ajout... ) : ( @@ -510,7 +527,7 @@ function EditPageDialog({ page, onOpenChange }: { page: ClientSocialPage | null; > {editMutation.isPending ? ( <> - + Modification... ) : ( diff --git a/client/src/pages/schedule-reel.tsx b/client/src/pages/schedule-reel.tsx index bb0ae7d..4de9961 100644 --- a/client/src/pages/schedule-reel.tsx +++ b/client/src/pages/schedule-reel.tsx @@ -1,10 +1,10 @@ import { useState, useCallback } from "react"; +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 { CalendarClock, Upload, Loader2, Check, Video } from "lucide-react"; import { useDropzone } from "react-dropzone"; -import Sidebar from "@/components/sidebar"; -import TopBar from "@/components/topbar"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Textarea } from "@/components/ui/textarea"; @@ -23,7 +23,6 @@ import { DateTimePicker } from "@/components/datetime-picker"; * publiée telle quelle par le planificateur à la date choisie. */ export default function ScheduleReel() { - const [sidebarOpen, setSidebarOpen] = useState(false); const [, navigate] = useLocation(); const { toast } = useToast(); @@ -125,22 +124,7 @@ export default function ScheduleReel() { ); return ( -
- {sidebarOpen && ( -
setSidebarOpen(false)} /> - )} - -
- setSidebarOpen(false)} /> -
- -
- setSidebarOpen(!sidebarOpen)} /> - -
+

@@ -293,8 +277,6 @@ export default function ScheduleReel() { -

-
-
+ ); } diff --git a/client/src/pages/settings.tsx b/client/src/pages/settings.tsx index f46ae4d..0b85654 100644 --- a/client/src/pages/settings.tsx +++ b/client/src/pages/settings.tsx @@ -1,8 +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 { useQuery, useMutation } from "@tanstack/react-query"; -import Sidebar from "@/components/sidebar"; -import TopBar from "@/components/topbar"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Label } from "@/components/ui/label"; import { Switch } from "@/components/ui/switch"; @@ -16,7 +16,6 @@ import { QwenSettingsCard } from "@/components/reels/qwen-settings-card"; import { SiTiktok } from "react-icons/si"; export default function Settings() { - const [sidebarOpen, setSidebarOpen] = useState(false); const [notifications, setNotifications] = useState(true); const [autoPublish, setAutoPublish] = useState(true); const [cloudName, setCloudName] = useState(""); @@ -418,25 +417,7 @@ export default function Settings() { }; return ( -
- {sidebarOpen && ( -
setSidebarOpen(false)} - /> - )} - -
- setSidebarOpen(false)} /> -
- -
- setSidebarOpen(!sidebarOpen)} /> - -
+

Paramètres

@@ -1010,8 +991,6 @@ export default function Settings() {

-
-
-
+ ); } diff --git a/client/src/pages/sql.tsx b/client/src/pages/sql.tsx index 11037b2..b4a5ca8 100644 --- a/client/src/pages/sql.tsx +++ b/client/src/pages/sql.tsx @@ -1,17 +1,16 @@ import { useState } from "react"; +import { Page } from "@/components/layout/app-shell"; +import { PageHeader } from "@/components/layout/page-header"; import { useQuery, useMutation } from "@tanstack/react-query"; -import Sidebar from "@/components/sidebar"; -import TopBar from "@/components/topbar"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { Textarea } from "@/components/ui/textarea"; import { useToast } from "@/hooks/use-toast"; import { apiRequest, queryClient } from "@/lib/queryClient"; -import { Database, Download, Play } from "lucide-react"; +import { Database, Download, Play, Table2 } from "lucide-react"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; export default function SqlAdmin() { - const [sidebarOpen, setSidebarOpen] = useState(false); const { toast } = useToast(); const [sqlQuery, setSqlQuery] = useState(""); const [queryResult, setQueryResult] = useState(null); @@ -75,77 +74,60 @@ export default function SqlAdmin() { }; return ( -
- {sidebarOpen && ( -
setSidebarOpen(false)} - /> - )} - -
- setSidebarOpen(false)} /> -
+ + -
- setSidebarOpen(!sidebarOpen)} /> - -
-
-

- - Administration SQL -

-

- Exécutez des requêtes SQL et gérez votre base de données -

-
- -
+
{/* Liste des tables */} - - - Tables disponibles - - Cliquez pour insérer dans la requête - + + + Tables + Cliquez pour pré-remplir la requête -
- {(tablesData as any)?.tables?.map((table: any) => ( - - ))} -
+ {(tablesData as any)?.tables?.length ? ( +
+ {(tablesData as any).tables.map((table: any) => ( + + ))} +
+ ) : ( +

Aucune table trouvée.

+ )}
{/* Éditeur SQL et résultats */} -
- - - Éditeur SQL - - Entrez votre requête SQL ci-dessous - +
+ + + Requête + Ctrl + Entrée pour exécuter