diff --git a/client/src/components/media-upload.tsx b/client/src/components/media-upload.tsx index e8fa4b3..7838be1 100644 --- a/client/src/components/media-upload.tsx +++ b/client/src/components/media-upload.tsx @@ -4,6 +4,7 @@ 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 { getVideoThumbnailUrl } from "@/lib/media-utils"; import { CloudUpload, Image as ImageIcon, Video, X, Upload, Loader2, ZoomIn, Camera } from "lucide-react"; import { useToast } from "@/hooks/use-toast"; import { SiFacebook, SiInstagram } from "react-icons/si"; @@ -41,7 +42,7 @@ export default function MediaUpload() { const onDrop = useCallback(async (acceptedFiles: File[]) => { if (acceptedFiles.length === 0) return; - + setUploadingCount(acceptedFiles.length); let successCount = 0; let errorCount = 0; @@ -67,7 +68,7 @@ export default function MediaUpload() { try { await Promise.all(uploadPromises); queryClient.invalidateQueries({ queryKey: ["/api/media"] }); - + toast({ title: "Succès", description: `${successCount} média(s) téléchargé(s) avec succès`, @@ -186,7 +187,7 @@ export default function MediaUpload() {
- -
- + {/* Élément sentinelle pour le scroll infini */} {(mediaList && visibleCount < (mediaList as any[]).length) ? (
@@ -324,7 +340,7 @@ export default function MediaUpload() {

Aperçu et recadrage automatique

- +
diff --git a/client/src/lib/media-utils.ts b/client/src/lib/media-utils.ts new file mode 100644 index 0000000..74a4848 --- /dev/null +++ b/client/src/lib/media-utils.ts @@ -0,0 +1,20 @@ +/** + * Derives a Cloudinary video thumbnail URL from the original video URL. + * + * Cloudinary automatically generates image frames for uploaded videos. + * By inserting transformation parameters into the URL, we can extract + * a static JPEG thumbnail without any backend change or DB migration. + * + * @param originalUrl - The Cloudinary video URL (e.g. .../video/upload/v123/file.mp4) + * @returns A JPEG thumbnail URL with fill-crop at 400x400 + */ +export function getVideoThumbnailUrl(originalUrl: string): string { + // Only transform Cloudinary video URLs + if (!originalUrl.includes('/video/upload/')) { + return originalUrl; + } + + return originalUrl + .replace('/video/upload/', '/video/upload/so_0,w_400,h_400,c_fill/') + .replace(/\.[^.]+$/, '.jpg'); +} diff --git a/client/src/pages/new-post.tsx b/client/src/pages/new-post.tsx index 32c1c73..665efdc 100644 --- a/client/src/pages/new-post.tsx +++ b/client/src/pages/new-post.tsx @@ -32,19 +32,20 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@ import { Dialog, DialogContent } from "@/components/ui/dialog"; import { useToast } from "@/hooks/use-toast"; import { apiRequest, queryClient } from "@/lib/queryClient"; +import { getVideoThumbnailUrl } from "@/lib/media-utils"; import type { SocialPage, Media, ScheduledPost } from "@shared/schema"; import { PreviewModal } from "@/components/preview-modal"; import { DateTimePicker } from "@/components/datetime-picker"; -function SortableMediaItem({ - media, - index, - isSelected, - onToggle -}: { - media: Media; - index: number; - isSelected: boolean; +function SortableMediaItem({ + media, + index, + isSelected, + onToggle +}: { + media: Media; + index: number; + isSelected: boolean; onToggle: () => void; }) { const { @@ -66,11 +67,10 @@ function SortableMediaItem({
-
(null); - + const [productInfo, setProductInfo] = useState(''); const [selectedMedia, setSelectedMedia] = useState([]); const [selectedPages, setSelectedPages] = useState([]); @@ -211,9 +210,9 @@ export default function NewPost() { const generateTextMutation = useMutation({ mutationFn: async (productInfoText: string) => { const productInfo = { - name: productInfoText.match(/Produit:\s*(.+?)(?:\n|$)/i)?.[1] || - productInfoText.match(/Nom:\s*(.+?)(?:\n|$)/i)?.[1] || - productInfoText, + name: productInfoText.match(/Produit:\s*(.+?)(?:\n|$)/i)?.[1] || + productInfoText.match(/Nom:\s*(.+?)(?:\n|$)/i)?.[1] || + productInfoText, price: productInfoText.match(/Prix:\s*(.+?)(?:\n|$)/i)?.[1] || "", description: productInfoText.match(/Description:\s*(.+?)(?:\n|$)/i)?.[1] || "", features: productInfoText.match(/Caractéristiques:\s*(.+?)(?:\n|$)/i)?.[1]?.split(",") || [], @@ -240,7 +239,7 @@ export default function NewPost() { }); const createPostMutation = useMutation({ - mutationFn: (data: any) => + mutationFn: (data: any) => apiRequest('POST', '/api/posts', data), onSuccess: async () => { await queryClient.invalidateQueries({ queryKey: ['/api/scheduled-posts'], refetchType: 'all' }); @@ -375,12 +374,12 @@ export default function NewPost() { return (
{sidebarOpen && ( -
setSidebarOpen(false)} /> )} - +
setSidebarOpen(!sidebarOpen)} /> - +

Nouvelle publication

@@ -409,7 +408,7 @@ export default function NewPost() { Sélectionnez ou uploadez une image/vidéo
- - -
- - @@ -492,7 +491,7 @@ export default function NewPost() { {selectedMedia.map((mediaId, index) => { const media = mediaList.find(m => m.id === mediaId); if (!media) return null; - + return ( { const isSelected = selectedMedia.includes(media.id); const isVideo = media.type === 'video'; - + return (
-
- +
diff --git a/client/src/pages/new-reel.tsx b/client/src/pages/new-reel.tsx index d29991f..5540f7b 100644 --- a/client/src/pages/new-reel.tsx +++ b/client/src/pages/new-reel.tsx @@ -27,6 +27,7 @@ import { import { useToast } from "@/hooks/use-toast"; import { apiRequest, queryClient } from "@/lib/queryClient"; import type { SocialPage, Media } from "@shared/schema"; +import { getVideoThumbnailUrl } from "@/lib/media-utils"; import { DateTimePicker } from "@/components/datetime-picker"; interface MusicTrack { @@ -485,12 +486,11 @@ export default function NewReel() { : 'border-transparent hover:border-muted-foreground' }`} > -