Add the ability to upload new photos directly from the new post page

Integrates react-dropzone for image/video uploads on the new post page, including upload mutation and UI updates to the media card.

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/TXBOLMj
This commit is contained in:
michaelschal committed 2025-10-08 13:56:21 +00:00
1 parent e69472d4b0
commit fa3e286dce
2 files changed
+108 -37

No files matched your search

+4
View File
@@ -18,6 +18,10 @@ externalPort = 80
localPort = 35357 localPort = 35357
externalPort = 3002 externalPort = 3002
[[ports]]
localPort = 36673
externalPort = 3003
[[ports]] [[ports]]
localPort = 37659 localPort = 37659
externalPort = 4200 externalPort = 4200
+104 -37
View File
@@ -1,7 +1,8 @@
import { useState } from "react"; import { useState, useCallback } from "react";
import { useQuery, useMutation } from "@tanstack/react-query"; import { useQuery, useMutation } from "@tanstack/react-query";
import { useLocation } from "wouter"; import { useLocation } from "wouter";
import { Send, Sparkles, Image as ImageIcon, Calendar } from "lucide-react"; import { Send, Sparkles, Image as ImageIcon, Calendar, Upload } from "lucide-react";
import { useDropzone } from "react-dropzone";
import Sidebar from "@/components/sidebar"; import Sidebar from "@/components/sidebar";
import TopBar from "@/components/topbar"; import TopBar from "@/components/topbar";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
@@ -36,6 +37,51 @@ export default function NewPost() {
queryKey: ['/api/media'], 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"] });
setSelectedMedia(data.id);
toast({
title: "Succès",
description: "Photo téléchargée avec succès",
});
},
onError: () => {
toast({
title: "Erreur",
description: "Impossible de télécharger la photo",
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,
noClick: true,
noKeyboard: true,
});
const generateTextMutation = useMutation({ const generateTextMutation = useMutation({
mutationFn: async (productInfoText: string) => { mutationFn: async (productInfoText: string) => {
const productInfo = { const productInfo = {
@@ -254,44 +300,65 @@ export default function NewPost() {
<Card className="rounded-2xl border-border/50 shadow-lg"> <Card className="rounded-2xl border-border/50 shadow-lg">
<CardHeader className="p-6"> <CardHeader className="p-6">
<CardTitle>Média</CardTitle> <div className="flex items-center justify-between">
<CardDescription>Sélectionnez une image ou vidéo</CardDescription> <div>
<CardTitle>Média</CardTitle>
<CardDescription>Sélectionnez ou uploadez une image/vidéo</CardDescription>
</div>
<Button
onClick={open}
disabled={uploadMutation.isPending}
size="sm"
variant="outline"
data-testid="button-upload-new-media"
>
<Upload className="w-4 h-4 mr-2" />
{uploadMutation.isPending ? 'Upload...' : 'Uploader'}
</Button>
</div>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
{mediaList.length === 0 ? ( <div {...getRootProps()} className={`${isDragActive ? 'bg-primary/5 border-primary' : ''}`}>
<div className="text-center py-8"> <input {...getInputProps()} />
<ImageIcon className="w-12 h-12 mx-auto text-muted-foreground mb-2" /> {mediaList.length === 0 ? (
<p className="text-muted-foreground">Aucun média disponible</p> <div className="text-center py-8 border-2 border-dashed rounded-lg">
<Button <ImageIcon className="w-12 h-12 mx-auto text-muted-foreground mb-2" />
variant="link" <p className="text-muted-foreground mb-2">
onClick={() => navigate('/media')} {isDragActive ? "Déposez votre photo ici" : "Aucun média disponible"}
data-testid="link-upload-media" </p>
> <Button
Télécharger des médias onClick={open}
</Button> disabled={uploadMutation.isPending}
</div> size="sm"
) : ( data-testid="button-upload-first-media"
<div className="grid grid-cols-3 gap-2">
{mediaList.map((media) => (
<button
key={media.id}
onClick={() => setSelectedMedia(media.id)}
className={`relative aspect-square rounded-lg overflow-hidden border-2 transition-all ${
selectedMedia === media.id
? 'border-primary ring-2 ring-primary'
: 'border-transparent hover:border-muted-foreground'
}`}
data-testid={`button-select-media-${media.id}`}
> >
<img <Upload className="w-4 h-4 mr-2" />
src={media.originalUrl} Uploader une photo
alt={media.fileName} </Button>
className="w-full h-full object-cover" </div>
/> ) : (
</button> <div className="grid grid-cols-3 gap-2">
))} {mediaList.map((media) => (
</div> <button
)} key={media.id}
onClick={() => setSelectedMedia(media.id)}
className={`relative aspect-square rounded-lg overflow-hidden border-2 transition-all ${
selectedMedia === media.id
? 'border-primary ring-2 ring-primary'
: 'border-transparent hover:border-muted-foreground'
}`}
data-testid={`button-select-media-${media.id}`}
>
<img
src={media.originalUrl}
alt={media.fileName}
className="w-full h-full object-cover"
/>
</button>
))}
</div>
)}
</div>
</CardContent> </CardContent>
</Card> </Card>
</div> </div>