Files
Socialflow/client/src/components/media-upload.tsx
T
michaelschal d7f8472798 Add a waiting modal for image uploads and display image thumbnails in the media library
Implement a loading dialog for ongoing image uploads and display actual image previews within the media library component.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: ae4037a0-2a6f-4530-9bac-79b543286bda
Replit-Commit-Checkpoint-Type: intermediate_checkpoint
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/397bca8c-984f-43ff-841a-10897aeb8140/ae4037a0-2a6f-4530-9bac-79b543286bda/AfZNdRv
2025-10-09 11:54:15 +00:00

248 lines
11 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useState, useCallback } from "react";
import { useDropzone } from "react-dropzone";
import { Button } from "@/components/ui/button";
import { Dialog, DialogContent } from "@/components/ui/dialog";
import { useMutation, useQuery } from "@tanstack/react-query";
import { apiRequest, queryClient } from "@/lib/queryClient";
import { CloudUpload, Image as ImageIcon, Video, X, Upload, Loader2 } from "lucide-react";
import { useToast } from "@/hooks/use-toast";
import { SiFacebook, SiInstagram } from "react-icons/si";
export default function MediaUpload() {
const { toast } = useToast();
const [selectedFile, setSelectedFile] = useState<any>(null);
const { data: mediaList } = useQuery({
queryKey: ["/api/media"],
});
const uploadMutation = useMutation({
mutationFn: async (file: File) => {
const formData = new FormData();
formData.append("file", file);
const response = await fetch("/api/media/upload", {
method: "POST",
body: formData,
});
if (!response.ok) throw new Error("Upload failed");
return response.json();
},
onSuccess: (data) => {
queryClient.invalidateQueries({ queryKey: ["/api/media"] });
setSelectedFile(data);
toast({
title: "Succès",
description: "Média téléchargé et traité avec succès",
});
},
onError: () => {
toast({
title: "Erreur",
description: "Impossible de télécharger le média",
variant: "destructive",
});
},
});
const onDrop = useCallback((acceptedFiles: File[]) => {
if (acceptedFiles.length > 0) {
uploadMutation.mutate(acceptedFiles[0]);
}
}, []);
const { getRootProps, getInputProps, isDragActive, open } = useDropzone({
onDrop,
accept: {
"image/*": [".png", ".jpg", ".jpeg", ".gif"],
"video/*": [".mp4", ".mov"],
},
maxSize: 52428800,
});
const deleteMutation = useMutation({
mutationFn: async (id: string) => {
await apiRequest("DELETE", `/api/media/${id}`);
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["/api/media"] });
setSelectedFile(null);
},
});
return (
<div className="bg-card rounded-2xl border border-border/50 overflow-hidden shadow-lg">
<div className="border-b border-border/50 p-6 bg-gradient-to-r from-primary/5 to-secondary/5">
<div className="flex items-center justify-between">
<div className="flex items-center gap-4">
<div className="w-12 h-12 rounded-xl bg-gradient-to-br from-primary to-secondary flex items-center justify-center shadow-lg">
<Upload className="text-white w-6 h-6" />
</div>
<div>
<h3 className="text-lg font-semibold text-foreground">Médiathèque</h3>
<p className="text-sm text-muted-foreground">Téléchargement et recadrage automatique</p>
</div>
</div>
<Button
onClick={open}
className="bg-gradient-to-r from-primary to-secondary hover:opacity-90 rounded-xl"
data-testid="button-browse"
>
<CloudUpload className="w-4 h-4 mr-2" />
Parcourir
</Button>
</div>
</div>
<div className="p-8">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
<div className="space-y-6">
<div
{...getRootProps()}
className={`
border-2 border-dashed rounded-2xl p-12 text-center cursor-pointer transition-all
${isDragActive ? "border-primary bg-primary/5 shadow-lg" : "border-border/50 hover:border-primary hover:bg-accent/30"}
`}
data-testid="dropzone-upload"
>
<input {...getInputProps()} />
<div className="w-16 h-16 rounded-2xl bg-gradient-to-br from-primary/10 to-secondary/10 flex items-center justify-center mx-auto mb-6">
<CloudUpload className="w-8 h-8 text-primary" />
</div>
<p className="text-foreground font-semibold text-lg mb-2">
{isDragActive ? "Déposez vos fichiers ici" : "Glissez-déposez vos fichiers"}
</p>
<p className="text-sm text-muted-foreground mb-4">ou cliquez pour parcourir</p>
<p className="text-xs text-muted-foreground font-medium">PNG, JPG, MP4 jusqu'à 50MB</p>
</div>
<div className="space-y-4">
<h4 className="text-sm font-semibold text-foreground flex items-center justify-between">
<span>Fichiers téléchargés</span>
<span className="text-xs font-medium text-muted-foreground bg-muted/30 px-3 py-1 rounded-full">
{(mediaList as any[])?.length || 0} média(s)
</span>
</h4>
<div className="space-y-3">
{(mediaList as any[])?.slice(0, 5).map((media: any) => (
<div
key={media.id}
onClick={() => setSelectedFile(media)}
className="flex items-center gap-4 p-4 rounded-xl bg-muted/30 border border-border/50 cursor-pointer hover:bg-muted hover:shadow-md transition-all"
data-testid={`media-item-${media.id}`}
>
<div className="w-14 h-14 rounded-lg bg-gradient-to-br from-muted to-muted/50 flex items-center justify-center flex-shrink-0 overflow-hidden">
{media.type === "video" ? (
<Video className="w-7 h-7 text-secondary" />
) : (
<img
src={media.originalUrl}
alt={media.fileName}
className="w-full h-full object-cover"
/>
)}
</div>
<div className="flex-1 min-w-0">
<p className="text-sm font-semibold text-foreground truncate">{media.fileName}</p>
<p className="text-xs text-muted-foreground font-medium mt-1">
{(media.fileSize / 1024 / 1024).toFixed(1)} MB
</p>
</div>
<div className="flex items-center gap-3">
<span className="inline-flex items-center px-3 py-1 rounded-full text-xs font-semibold bg-success/20 text-success">
Prêt
</span>
<button
onClick={(e) => {
e.stopPropagation();
deleteMutation.mutate(media.id);
}}
className="text-muted-foreground hover:text-destructive transition-all p-1 rounded-lg hover:bg-destructive/10"
data-testid={`button-delete-${media.id}`}
>
<X className="w-5 h-5" />
</button>
</div>
</div>
))}
</div>
</div>
</div>
<div className="space-y-6">
<h4 className="text-sm font-semibold text-foreground">Aperçu et recadrage automatique</h4>
<div className="space-y-5">
<div className="border border-border/50 rounded-xl p-5 bg-card shadow-sm">
<div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-3">
<SiFacebook className="text-[#1877F2] text-lg" />
<SiInstagram className="text-[#E4405F] text-lg" />
<span className="text-sm font-semibold text-foreground">Feed</span>
</div>
<span className="text-xs text-muted-foreground bg-muted/30 px-2 py-1 rounded-md font-medium">1080×1080</span>
</div>
<div className="aspect-square bg-muted/30 rounded-lg overflow-hidden border border-border/50" style={{ width: '250px' }}>
{selectedFile?.facebookFeedUrl || selectedFile?.instagramFeedUrl ? (
<img
src={selectedFile.facebookFeedUrl || selectedFile.instagramFeedUrl}
alt="Feed preview"
className="w-full h-full object-cover"
/>
) : (
<div className="w-full h-full flex items-center justify-center">
<ImageIcon className="w-12 h-12 text-muted-foreground opacity-50" />
</div>
)}
</div>
</div>
<div className="border border-border/50 rounded-xl p-5 bg-card shadow-sm">
<div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-3">
<SiFacebook className="text-[#1877F2] text-lg" />
<SiInstagram className="text-[#E4405F] text-lg" />
<span className="text-sm font-semibold text-foreground">Story</span>
</div>
<span className="text-xs text-muted-foreground bg-muted/30 px-2 py-1 rounded-md font-medium">1080×1920</span>
</div>
<div className="aspect-[9/16] bg-muted/30 rounded-lg overflow-hidden border border-border/50" style={{ width: '200px' }}>
{selectedFile?.instagramStoryUrl ? (
<img
src={selectedFile.instagramStoryUrl}
alt="Story preview"
className="w-full h-full object-cover"
/>
) : (
<div className="w-full h-full flex items-center justify-center">
<ImageIcon className="w-10 h-10 text-muted-foreground opacity-50" />
</div>
)}
</div>
</div>
</div>
</div>
</div>
</div>
<Dialog open={uploadMutation.isPending}>
<DialogContent className="sm:max-w-md [&>button]:hidden">
<div className="flex flex-col items-center justify-center py-8">
<div className="w-20 h-20 rounded-full bg-primary/10 flex items-center justify-center mb-6">
<Loader2 className="w-10 h-10 text-primary animate-spin" />
</div>
<h3 className="text-xl font-semibold text-foreground mb-2">
Upload en cours...
</h3>
<p className="text-sm text-muted-foreground text-center">
Votre image est en cours de téléchargement et de traitement.
<br />
Veuillez patienter.
</p>
</div>
</DialogContent>
</Dialog>
</div>
);
}