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
-
-
-
{generatedVariants.map((variant, index) => (
-
@@ -667,8 +664,8 @@ export default function NewPost() {
{pages.length === 0 ? (
Aucune page connectée
-
navigate('/pages')}
data-testid="link-add-pages"
>
@@ -754,7 +751,7 @@ export default function NewPost() {
- setPreviewModalOpen(true)}
disabled={createPostMutation.isPending}
className="w-full bg-gradient-to-r from-primary to-secondary hover:opacity-90 shadow-lg"
@@ -768,7 +765,7 @@ export default function NewPost() {
-
+