From 7a7188e5089c2c9d5379be879d08f6bef8671a02 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 6 Oct 2026 12:56:22 +0000 Subject: [PATCH] =?UTF-8?q?Refonte=20UI=20:=20m=C3=A9diath=C3=A8que,=20cal?= =?UTF-8?q?endrier,=20aper=C3=A7u=20et=20statistiques=20sur=20les=20nouvea?= =?UTF-8?q?ux=20tokens?= 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 | 70 ++-- .../src/components/analytics/MetricsCard.tsx | 6 +- client/src/components/calendar-list-view.tsx | 12 +- client/src/components/calendar-view.tsx | 119 +++--- client/src/components/datetime-picker.tsx | 14 +- client/src/components/media-upload.tsx | 370 +++++++++--------- client/src/components/preview-modal.tsx | 56 ++- 7 files changed, 325 insertions(+), 322 deletions(-) diff --git a/client/src/components/analytics/AnalyticsDashboard.tsx b/client/src/components/analytics/AnalyticsDashboard.tsx index cc3893b..b5042b2 100644 --- a/client/src/components/analytics/AnalyticsDashboard.tsx +++ b/client/src/components/analytics/AnalyticsDashboard.tsx @@ -2,6 +2,9 @@ import { useState, useEffect, useRef } from 'react'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { MetricsCard } from './MetricsCard'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; +import { Link } from 'wouter'; +import { Badge } from '@/components/ui/badge'; +import { EmptyState } from '@/components/empty-state'; import { Button } from '@/components/ui/button'; import { RefreshCw, Users, BarChart3, Eye, Heart } from 'lucide-react'; import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Legend } from 'recharts'; @@ -98,15 +101,34 @@ export function AnalyticsDashboard() { return Math.round(((current - prev) / prev) * 100); }; - if (isLoadingPages) return
Chargement des pages...
; - if (!pages.length) return
Aucune page connectée. Veuillez connecter une page Facebook dans les Paramètres.
; + if (isLoadingPages) { + return ( +
+ {[1, 2, 3, 4].map((i) =>
)} +
+ ); + } + if (!pages.length) { + return ( + + Connecter un compte + + } + /> + ); + } return ( -
-
-
+
+
+
{selectedPage && ( - - Token : {selectedPage.tokenStatus || 'Inconnu'} - + + {selectedPage.tokenStatus === 'valid' ? 'Connexion active' : 'Connexion à vérifier'} + )}
@@ -131,22 +153,22 @@ export function AnalyticsDashboard() { disabled={refreshMutation.isPending || !selectedPageId} > - Actualiser les données + Actualiser
{selectedPageId && ( <> -
+
} trend={latest && previous ? getTrend(latest.followersCount, previous.followersCount) : undefined} description="depuis la dernière mise à jour" /> } trend={latest && previous ? getTrend(latest.pageReach, previous.pageReach) : undefined} @@ -160,7 +182,7 @@ export function AnalyticsDashboard() { description="interactions totales (jour)" /> } trend={latest && previous ? getTrend(latest.pageViews || 0, previous.pageViews || 0) : undefined} @@ -170,22 +192,22 @@ export function AnalyticsDashboard() { - Historique de Croissance & Portée + Évolution
- - - - - + + + + + - - - - + + + +
diff --git a/client/src/components/analytics/MetricsCard.tsx b/client/src/components/analytics/MetricsCard.tsx index ed8c5e1..c118b28 100644 --- a/client/src/components/analytics/MetricsCard.tsx +++ b/client/src/components/analytics/MetricsCard.tsx @@ -13,17 +13,17 @@ export function MetricsCard({ title, value, description, trend, icon }: MetricsC return ( - + {title} {icon &&
{icon}
}
-
{value}
+
{value}
{(trend !== undefined || description) && (

{trend !== undefined && ( - 0 ? 'text-green-500' : trend < 0 ? 'text-red-500' : ''}`}> + 0 ? 'text-success' : trend < 0 ? 'text-destructive' : ''}`}> {trend > 0 ? : trend < 0 ? : } {Math.abs(trend)}% diff --git a/client/src/components/calendar-list-view.tsx b/client/src/components/calendar-list-view.tsx index 18e450e..1143279 100644 --- a/client/src/components/calendar-list-view.tsx +++ b/client/src/components/calendar-list-view.tsx @@ -165,7 +165,7 @@ export default function CalendarListView({ scheduledPosts, onEditPost, onDeleteP

@@ -176,8 +176,8 @@ export default function CalendarListView({ scheduledPosts, onEditPost, onDeleteP {time} {isPending ? 'Programmé' : 'Publié'} @@ -197,7 +197,7 @@ export default function CalendarListView({ scheduledPosts, onEditPost, onDeleteP data-testid={`button-preview-post-${post.id}`} title="Prévisualiser" > - + {isPending && ( <> @@ -209,7 +209,7 @@ export default function CalendarListView({ scheduledPosts, onEditPost, onDeleteP data-testid={`button-edit-post-${post.id}`} title="Modifier" > - + )} diff --git a/client/src/components/calendar-view.tsx b/client/src/components/calendar-view.tsx index 163ff58..f0616ca 100644 --- a/client/src/components/calendar-view.tsx +++ b/client/src/components/calendar-view.tsx @@ -191,52 +191,47 @@ export default function CalendarView() { }; return ( -
-
-
-
-
- -
-
-

Calendrier

-

Visualisez vos publications planifiées

-
-
-
- - - {monthNames[currentDate.getMonth()]} {currentDate.getFullYear()} - - -
+
+
+

+ {monthNames[currentDate.getMonth()]} {currentDate.getFullYear()} +

+
+ + +
{isDesktop ? ( -
+
{daysOfWeek.map((day) => ( -
- {day} +
+ {day}
))}
@@ -246,8 +241,8 @@ export default function CalendarView() {
{isToday(day.date) && ( - + Aujourd'hui )} @@ -289,8 +284,8 @@ export default function CalendarView() { key={idx} className={` px-3 py-2 rounded-lg text-xs transition-all font-medium shadow-sm group relative - ${isPending ? 'bg-blue-500/20 text-blue-600 hover:bg-blue-500/30' : ''} - ${isPublished ? 'bg-green-500/20 text-green-600 hover:bg-green-500/30' : ''} + ${isPending ? 'bg-primary/10 text-primary hover:bg-primary/20' : ''} + ${isPublished ? 'bg-success/15 text-success hover:bg-success/25' : ''} `} data-testid={`calendar-post-${post.id}`} > @@ -308,29 +303,29 @@ export default function CalendarView() {
{isPending && ( <> )} @@ -368,8 +363,8 @@ export default function CalendarView() { key={idx} className={` px-3 py-2 rounded-lg text-xs transition-all font-medium shadow-sm group relative - ${isPending ? 'bg-blue-500/20 text-blue-600 hover:bg-blue-500/30' : ''} - ${isPublished ? 'bg-green-500/20 text-green-600 hover:bg-green-500/30' : ''} + ${isPending ? 'bg-primary/10 text-primary hover:bg-primary/20' : ''} + ${isPublished ? 'bg-success/15 text-success hover:bg-success/25' : ''} `} data-testid={`popover-post-${post.id}`} > @@ -387,29 +382,29 @@ export default function CalendarView() {
{isPending && ( <> )} @@ -429,13 +424,13 @@ export default function CalendarView() { ))}
-
+
-
+
Programmé
-
+
Publié
diff --git a/client/src/components/datetime-picker.tsx b/client/src/components/datetime-picker.tsx index 7113d6a..b036fb0 100644 --- a/client/src/components/datetime-picker.tsx +++ b/client/src/components/datetime-picker.tsx @@ -100,7 +100,7 @@ export function DateTimePicker({ value, onChange, occupiedDates = [], placeholde occupied: (date) => isDateOccupied(date) }} modifiersClassNames={{ - occupied: "bg-red-500/20 text-red-700 dark:text-red-300 font-bold hover:bg-red-500/30" + occupied: "bg-destructive/15 text-destructive font-bold hover:bg-destructive/25" }} disabled={(date) => date < new Date(new Date().setHours(0, 0, 0, 0))} data-testid="calendar-picker" @@ -154,7 +154,7 @@ export function DateTimePicker({ value, onChange, occupiedDates = [], placeholde className={cn( "w-full justify-center mb-0.5 h-9 text-xs", selectedHour === hour && "bg-primary text-primary-foreground", - isOptimalHour(hour) && selectedHour !== hour && "bg-green-500/20 text-green-700 dark:text-green-300 font-semibold hover:bg-green-500/30" + isOptimalHour(hour) && selectedHour !== hour && "bg-success/15 text-success font-semibold hover:bg-success/25" )} onClick={() => handleTimeChange(hour, selectedMinute)} data-testid={`hour-${hour}`} @@ -189,8 +189,8 @@ export function DateTimePicker({ value, onChange, occupiedDates = [], placeholde
-
- +
+ Heures optimales (8h, 12h-13h, 18h-20h)
@@ -200,10 +200,10 @@ export function DateTimePicker({ value, onChange, occupiedDates = [], placeholde )} {occupiedDates.length > 0 && ( -
+
-
- +
+ Les dates en fond rouge ont déjà des publications programmées
diff --git a/client/src/components/media-upload.tsx b/client/src/components/media-upload.tsx index 967b4ac..c05a0d2 100644 --- a/client/src/components/media-upload.tsx +++ b/client/src/components/media-upload.tsx @@ -5,7 +5,18 @@ import { Dialog, DialogContent } from "@/components/ui/dialog"; import { useInfiniteQuery, useMutation } from "@tanstack/react-query"; import { apiRequest, queryClient, handleUnauthorized } from "@/lib/queryClient"; import { MediaThumbnail } from "@/components/media-thumbnail"; -import { CloudUpload, Image as ImageIcon, Video, X, Upload, Loader2, ZoomIn, Camera } from "lucide-react"; +import { CloudUpload, Image as ImageIcon, X, Upload, Loader2, ZoomIn, Camera, Trash2 } from "lucide-react"; +import { EmptyState } from "@/components/empty-state"; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from "@/components/ui/alert-dialog"; import { useToast } from "@/hooks/use-toast"; import { SiFacebook, SiInstagram } from "react-icons/si"; @@ -16,6 +27,7 @@ export default function MediaUpload() { const { toast } = useToast(); const [selectedFile, setSelectedFile] = useState(null); const [zoomImage, setZoomImage] = useState(null); + const [mediaToDelete, setMediaToDelete] = useState(null); const loadMoreRef = useRef(null); const cameraInputRef = useRef(null); @@ -213,219 +225,203 @@ export default function MediaUpload() { }; }, [hasNextPage, isFetchingNextPage, fetchNextPage]); + const formatPreview = (label: string, size: string, url: string | undefined, aspect: string, width: string) => ( +
+
+
+ + + {label} +
+ {size} +
+
+ {url ? ( + {`Aperçu + ) : ( +
+ +
+ )} +
+
+ ); + return ( -
-
-
-
-
- -
-
-

Médiathèque

-

Téléchargement et recadrage automatique

-
-
-
- - -
+
+ +
0 ? "pointer-events-none opacity-60" : ""} + `} + data-testid="dropzone-upload" + > + +
+ {uploadingCount > 0 ? : } +
+
+ {uploadingCount > 0 ? ( + <> +

Import en cours…

+

{uploadingCount} fichier(s) en cours de traitement

+ + ) : ( + <> +

{isDragActive ? "Déposez vos fichiers ici" : "Glissez-déposez vos images et vidéos"}

+

PNG, JPG ou MP4 jusqu'à 50 Mo — recadrées automatiquement pour le fil et les stories.

+ + )} +
+
e.stopPropagation()}> + +
-
-
-
-
0 ? "pointer-events-none opacity-50" : ""} - `} - data-testid="dropzone-upload" - > - - -
- {uploadingCount > 0 ? ( - - ) : ( - - )} -
- {uploadingCount > 0 ? ( - <> -

- Téléchargement en cours... -

-

- {uploadingCount} fichier(s) en cours de traitement -

- - ) : ( - <> -

- {isDragActive ? "Déposez vos fichiers ici" : "Glissez-déposez vos fichiers"} -

-

ou cliquez pour parcourir

-

PNG, JPG, MP4 jusqu'à 50MB

- - )} -
+
+
+
+

Vos médias

+ {totalMedia} élément(s) +
-
-

- Fichiers téléchargés - - {totalMedia} média(s) - -

- -
-
- {mediaList.map((media: any) => ( -
setSelectedFile(media)} - className="relative aspect-square rounded-lg overflow-hidden border-2 border-border/50 cursor-pointer hover:border-primary hover:shadow-lg transition-all group" - data-testid={`media-item-${media.id}`} - > - -
- {media.type === "image" && ( - - )} + {mediaList.length === 0 ? ( + + ) : ( +
+ {mediaList.map((media: any) => { + const selected = selectedFile?.id === media.id; + return ( +
setSelectedFile(media)} + className={`group relative aspect-square cursor-pointer overflow-hidden rounded-lg border-2 transition-all ${selected ? "border-primary" : "border-transparent hover:border-primary/40"}`} + data-testid={`media-item-${media.id}`} + > + +
+ {media.type === "image" && ( -
+ )} +
- ))} -
- - {/* Élément sentinelle pour le scroll infini */} - {hasNextPage ? ( -
-
- ) : null} -
+ ); + })}
-
+ )} -
-

Aperçu et recadrage automatique

- -
-
-
-
- - - Feed -
- 1080×1080 -
-
- {selectedFile?.facebookFeedUrl || selectedFile?.instagramFeedUrl ? ( - Feed preview - ) : ( -
- -
- )} -
-
- -
-
-
- - - Story -
- 1080×1920 -
-
- {selectedFile?.instagramStoryUrl ? ( - Story preview - ) : ( -
- -
- )} -
-
+ {/* Élément sentinelle pour le scroll infini */} + {hasNextPage ? ( +
+
-
+ ) : null}
+ +
+ !o && setMediaToDelete(null)}> + + + Supprimer ce média ? + + « {mediaToDelete?.fileName} » sera définitivement retiré de la médiathèque. + + + + Annuler + { + if (mediaToDelete) { + deleteMutation.mutate(mediaToDelete.id); + if (selectedFile?.id === mediaToDelete.id) setSelectedFile(null); + } + setMediaToDelete(null); + }} + > + Supprimer + + + + +
-

- 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 recadrage.

diff --git a/client/src/components/preview-modal.tsx b/client/src/components/preview-modal.tsx index ef7cdca..6f1b23f 100644 --- a/client/src/components/preview-modal.tsx +++ b/client/src/components/preview-modal.tsx @@ -1,5 +1,6 @@ import { useState, useEffect, useRef } from 'react'; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from '@/components/ui/dialog'; +import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { Button } from '@/components/ui/button'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; import { ChevronLeft, ChevronRight, Send, Facebook, Instagram, Heart, MessageCircle, Share2, Bookmark, Loader2, Video } from 'lucide-react'; @@ -98,7 +99,7 @@ export function PreviewModal({
{/* Header */}
-
+
@@ -404,7 +405,7 @@ export function PreviewModal({
) : ( -
+
)} @@ -503,42 +504,31 @@ export function PreviewModal({ - Prévisualisation de la publication + Aperçu de la publication - Visualisez votre publication avant de la publier + Vérifiez le rendu sur chaque réseau avant de valider.
{/* Format selector */} -
- - -
+ }} + > + + Facebook · Fil + Facebook · Story + Instagram · Fil + Instagram · Story + + {/* Preview */} -
+
{previewFormat === 'facebook-feed' && renderFacebookFeed()} {previewFormat === 'facebook-story' && renderFacebookStory()} {previewFormat === 'instagram-feed' && renderInstagramFeed()} @@ -546,7 +536,7 @@ export function PreviewModal({
{/* Actions */} -
+
{readOnly ? ( )}