From a225842bd0703a5965eb4206c4f6dc8fcf25b683 Mon Sep 17 00:00:00 2001 From: michaelschal <35957947-michaelschal@users.noreply.replit.com> Date: Wed, 8 Oct 2025 19:59:01 +0000 Subject: [PATCH 1/8] Transitioned from Plan to Build mode Replit-Commit-Author: Agent Replit-Commit-Session-Id: ae4037a0-2a6f-4530-9bac-79b543286bda Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/397bca8c-984f-43ff-841a-10897aeb8140/ae4037a0-2a6f-4530-9bac-79b543286bda/561RCgs --- .replit | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.replit b/.replit index b321948..d51ca2b 100644 --- a/.replit +++ b/.replit @@ -42,10 +42,6 @@ externalPort = 6000 localPort = 43245 externalPort = 3001 -[[ports]] -localPort = 45355 -externalPort = 6800 - [[ports]] localPort = 46681 externalPort = 5173 From 0bdc1139d6db409f081772be1e1d2bad4174c148 Mon Sep 17 00:00:00 2001 From: michaelschal <35957947-michaelschal@users.noreply.replit.com> Date: Wed, 8 Oct 2025 20:01:08 +0000 Subject: [PATCH 2/8] Add media ordering to scheduled posts for better content presentation Update schema to include `display_order` for post media and create a corresponding insert schema. 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/561RCgs --- shared/schema.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/shared/schema.ts b/shared/schema.ts index d9f44cd..2cb6af9 100644 --- a/shared/schema.ts +++ b/shared/schema.ts @@ -79,6 +79,7 @@ export const postMedia = pgTable("post_media", { id: varchar("id").primaryKey().default(sql`gen_random_uuid()`), postId: varchar("post_id").notNull().references(() => posts.id, { onDelete: "cascade" }), mediaId: varchar("media_id").notNull().references(() => media.id, { onDelete: "cascade" }), + displayOrder: integer("display_order").notNull().default(0), }); export const scheduledPosts = pgTable("scheduled_posts", { @@ -260,6 +261,10 @@ export const insertUserPagePermissionSchema = createInsertSchema(userPagePermiss createdAt: true, }); +export const insertPostMediaSchema = createInsertSchema(postMedia).omit({ + id: true, +}); + // Types export type User = typeof users.$inferSelect; export type InsertUser = z.infer; @@ -288,6 +293,7 @@ export type InsertOpenrouterConfig = z.infer; export type PostMedia = typeof postMedia.$inferSelect; +export type InsertPostMedia = z.infer; export type UserPagePermission = typeof userPagePermissions.$inferSelect; export type InsertUserPagePermission = z.infer; From b2df4ee031a01c1c41ad99147d691cdcb212cff1 Mon Sep 17 00:00:00 2001 From: michaelschal <35957947-michaelschal@users.noreply.replit.com> Date: Wed, 8 Oct 2025 20:01:43 +0000 Subject: [PATCH 3/8] Add drag and drop functionality for content creation Install @dnd-kit dependencies to enable drag and drop features for reordering content. 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/561RCgs --- package-lock.json | 56 +++++++++++++++++++++++++++++++++++++++++++++++ package.json | 3 +++ 2 files changed, 59 insertions(+) diff --git a/package-lock.json b/package-lock.json index 9961c5f..b32a42f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,9 @@ "version": "1.0.0", "license": "MIT", "dependencies": { + "@dnd-kit/core": "^6.3.1", + "@dnd-kit/sortable": "^10.0.0", + "@dnd-kit/utilities": "^3.2.2", "@hookform/resolvers": "^3.10.0", "@jridgewell/trace-mapping": "^0.3.25", "@neondatabase/serverless": "^0.10.4", @@ -418,6 +421,59 @@ "node": ">=6.9.0" } }, + "node_modules/@dnd-kit/accessibility": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.1.1.tgz", + "integrity": "sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/@dnd-kit/core": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz", + "integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==", + "license": "MIT", + "dependencies": { + "@dnd-kit/accessibility": "^3.1.1", + "@dnd-kit/utilities": "^3.2.2", + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@dnd-kit/sortable": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@dnd-kit/sortable/-/sortable-10.0.0.tgz", + "integrity": "sha512-+xqhmIIzvAYMGfBYYnbKuNicfSsk4RksY2XdmJhT+HAC01nix6fHCztU68jooFiMUB01Ky3F0FyOvhG/BZrWkg==", + "license": "MIT", + "dependencies": { + "@dnd-kit/utilities": "^3.2.2", + "tslib": "^2.0.0" + }, + "peerDependencies": { + "@dnd-kit/core": "^6.3.0", + "react": ">=16.8.0" + } + }, + "node_modules/@dnd-kit/utilities": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@dnd-kit/utilities/-/utilities-3.2.2.tgz", + "integrity": "sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, "node_modules/@drizzle-team/brocli": { "version": "0.10.2", "resolved": "https://registry.npmjs.org/@drizzle-team/brocli/-/brocli-0.10.2.tgz", diff --git a/package.json b/package.json index fe661ff..81dab0c 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,9 @@ "db:push": "drizzle-kit push" }, "dependencies": { + "@dnd-kit/core": "^6.3.1", + "@dnd-kit/sortable": "^10.0.0", + "@dnd-kit/utilities": "^3.2.2", "@hookform/resolvers": "^3.10.0", "@jridgewell/trace-mapping": "^0.3.25", "@neondatabase/serverless": "^0.10.4", From 3d4ceaa5d855164597b204108a56aed4d6be7f2a Mon Sep 17 00:00:00 2001 From: michaelschal <35957947-michaelschal@users.noreply.replit.com> Date: Wed, 8 Oct 2025 20:07:12 +0000 Subject: [PATCH 4/8] Update post creation to support multiple media items and carousels Refactor post creation logic in `server/routes.ts` to handle an array of media items (`mediaIds`) for posts, supporting both single media and carousel formats. Implement validation for a maximum of 10 media items and ensure stories have at least one media. Modify `server/storage.ts` to order media by `displayOrder` when retrieving post media. 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/WzkpF6q --- .replit | 4 ++++ server/routes.ts | 46 ++++++++++++++++++++++++++++++++++++++-------- server/storage.ts | 4 ++-- 3 files changed, 44 insertions(+), 10 deletions(-) diff --git a/.replit b/.replit index d51ca2b..998c030 100644 --- a/.replit +++ b/.replit @@ -22,6 +22,10 @@ externalPort = 3003 localPort = 35357 externalPort = 3002 +[[ports]] +localPort = 35903 +externalPort = 6800 + [[ports]] localPort = 37431 externalPort = 5000 diff --git a/server/routes.ts b/server/routes.ts index f92ba5a..28c51aa 100644 --- a/server/routes.ts +++ b/server/routes.ts @@ -557,11 +557,39 @@ export async function registerRoutes(app: Express): Promise { try { const user = req.user as User; const userId = user.id; - const { pageIds, postType, mediaId, ...postFields } = req.body; + const { pageIds, postType, mediaId, mediaIds, ...postFields } = req.body; + + // Convert mediaIds to standardized format: array of { mediaId, displayOrder } + let finalMediaItems: Array<{ mediaId: string; displayOrder: number }> = []; + + if (mediaIds && Array.isArray(mediaIds)) { + // New format: array of objects or strings + finalMediaItems = mediaIds.map((item: any, index: number) => { + if (typeof item === 'string') { + // Legacy array of strings: use index as displayOrder + return { mediaId: item, displayOrder: index }; + } else if (item.mediaId) { + // New format: object with mediaId and displayOrder + return { + mediaId: item.mediaId, + displayOrder: item.displayOrder ?? index, + }; + } + throw new Error("Invalid media format"); + }); + } else if (mediaId) { + // Legacy single mediaId + finalMediaItems = [{ mediaId, displayOrder: 0 }]; + } + + // Validate max 10 photos + if (finalMediaItems.length > 10) { + return res.status(400).json({ error: "Maximum 10 photos autorisées par publication" }); + } // Validate that stories require media - if ((postType === 'story' || postType === 'both') && !mediaId) { - return res.status(400).json({ error: "Les stories nécessitent un média (image ou vidéo)" }); + if ((postType === 'story' || postType === 'both') && finalMediaItems.length === 0) { + return res.status(400).json({ error: "Les stories nécessitent au moins un média (image ou vidéo)" }); } // Convert scheduledFor string to Date if provided @@ -573,12 +601,14 @@ export async function registerRoutes(app: Express): Promise { const postData = insertPostSchema.parse({ ...postFields, userId }); const post = await storage.createPost(postData); - // Link media to post if provided - if (mediaId) { - await db.insert(postMedia).values({ + // Link media to post if provided (with display order) + if (finalMediaItems.length > 0) { + const postMediaValues = finalMediaItems.map(item => ({ postId: post.id, - mediaId: mediaId, - }); + mediaId: item.mediaId, + displayOrder: item.displayOrder, + })); + await db.insert(postMedia).values(postMediaValues); } // Create scheduled posts for each selected page diff --git a/server/storage.ts b/server/storage.ts index 82a7b95..20571bd 100644 --- a/server/storage.ts +++ b/server/storage.ts @@ -30,7 +30,7 @@ import { type InsertUserPagePermission, } from "@shared/schema"; import { db } from "./db"; -import { eq, and, gte, lte, desc, isNull } from "drizzle-orm"; +import { eq, and, gte, lte, desc, asc, isNull } from "drizzle-orm"; export interface IStorage { // Users @@ -168,7 +168,7 @@ export class DatabaseStorage implements IStorage { } async getPostMedia(postId: string): Promise { - return await db.select().from(postMedia).where(eq(postMedia.postId, postId)); + return await db.select().from(postMedia).where(eq(postMedia.postId, postId)).orderBy(asc(postMedia.displayOrder)); } // Posts From 0041dc090d9e260b2bdf5508304f7456794204d6 Mon Sep 17 00:00:00 2001 From: michaelschal <35957947-michaelschal@users.noreply.replit.com> Date: Wed, 8 Oct 2025 20:10:17 +0000 Subject: [PATCH 5/8] Improve media selection for posts, allowing multiple uploads Update the NewPost page to support selecting up to 10 media items for posts, including carousel and story types, and modify the API call to send an array of media IDs. 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/WzkpF6q --- .replit | 4 -- client/src/pages/new-post.tsx | 81 +++++++++++++++++++++++++---------- 2 files changed, 58 insertions(+), 27 deletions(-) diff --git a/.replit b/.replit index 998c030..d51ca2b 100644 --- a/.replit +++ b/.replit @@ -22,10 +22,6 @@ externalPort = 3003 localPort = 35357 externalPort = 3002 -[[ports]] -localPort = 35903 -externalPort = 6800 - [[ports]] localPort = 37431 externalPort = 5000 diff --git a/client/src/pages/new-post.tsx b/client/src/pages/new-post.tsx index 77f7ca5..92969f2 100644 --- a/client/src/pages/new-post.tsx +++ b/client/src/pages/new-post.tsx @@ -23,7 +23,7 @@ export default function NewPost() { const cameraInputRef = useRef(null); const [productInfo, setProductInfo] = useState(''); - const [selectedMedia, setSelectedMedia] = useState(null); + const [selectedMedia, setSelectedMedia] = useState([]); const [selectedPages, setSelectedPages] = useState([]); const [scheduledDate, setScheduledDate] = useState(''); const [generatedVariants, setGeneratedVariants] = useState([]); @@ -51,7 +51,18 @@ export default function NewPost() { }, onSuccess: (data) => { queryClient.invalidateQueries({ queryKey: ["/api/media"] }); - setSelectedMedia(data.id); + // Add to selected media array if not already at max (10) + setSelectedMedia(prev => { + if (prev.length >= 10) { + toast({ + title: "Limite atteinte", + description: "Maximum 10 photos par publication", + variant: "destructive", + }); + return prev; + } + return [...prev, data.id]; + }); toast({ title: "Succès", description: "Photo téléchargée avec succès", @@ -164,10 +175,10 @@ export default function NewPost() { return; } - if ((postType === 'story' || postType === 'both') && !selectedMedia) { + if ((postType === 'story' || postType === 'both') && selectedMedia.length === 0) { toast({ title: "Média requis", - description: "Les stories nécessitent une image ou vidéo", + description: "Les stories nécessitent au moins une image ou vidéo", variant: "destructive", }); return; @@ -176,7 +187,7 @@ export default function NewPost() { createPostMutation.mutate({ content: postText, scheduledFor: scheduledDate || undefined, - mediaId: selectedMedia || undefined, + mediaIds: selectedMedia.length > 0 ? selectedMedia : undefined, pageIds: selectedPages, postType, }); @@ -299,24 +310,48 @@ export default function NewPost() { ) : (
- {mediaList.map((media) => ( - - ))} + {mediaList.map((media) => { + const isSelected = selectedMedia.includes(media.id); + const selectionIndex = selectedMedia.indexOf(media.id); + + return ( + + ); + })}
)} From 26f549c34044f37e075a3ae9dce76d9ddf6f1fae Mon Sep 17 00:00:00 2001 From: michaelschal <35957947-michaelschal@users.noreply.replit.com> Date: Wed, 8 Oct 2025 20:14:12 +0000 Subject: [PATCH 6/8] Add drag-and-drop functionality to reorder selected media in posts Integrates @dnd-kit for drag-and-drop reordering of media items within the new post creation interface, enabling carousel slide order customization. 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/WzkpF6q --- client/src/pages/new-post.tsx | 225 +++++++++++++++++++++++++++------- 1 file changed, 182 insertions(+), 43 deletions(-) diff --git a/client/src/pages/new-post.tsx b/client/src/pages/new-post.tsx index 92969f2..b165cdf 100644 --- a/client/src/pages/new-post.tsx +++ b/client/src/pages/new-post.tsx @@ -1,8 +1,25 @@ import { useState, useCallback, useRef } from "react"; import { useQuery, useMutation } from "@tanstack/react-query"; import { useLocation } from "wouter"; -import { Send, Sparkles, Image as ImageIcon, Calendar, Upload, Camera } from "lucide-react"; +import { Send, Sparkles, Image as ImageIcon, Calendar, Upload, Camera, GripVertical } from "lucide-react"; import { useDropzone } from "react-dropzone"; +import { + DndContext, + closestCenter, + KeyboardSensor, + PointerSensor, + useSensor, + useSensors, + DragEndEvent, +} from "@dnd-kit/core"; +import { + arrayMove, + SortableContext, + sortableKeyboardCoordinates, + rectSortingStrategy, + useSortable, +} from "@dnd-kit/sortable"; +import { CSS } from "@dnd-kit/utilities"; import Sidebar from "@/components/sidebar"; import TopBar from "@/components/topbar"; import { Button } from "@/components/ui/button"; @@ -16,6 +33,69 @@ import { useToast } from "@/hooks/use-toast"; import { apiRequest, queryClient } from "@/lib/queryClient"; import type { SocialPage, Media } from "@shared/schema"; +function SortableMediaItem({ + media, + index, + isSelected, + onToggle +}: { + media: Media; + index: number; + isSelected: boolean; + onToggle: () => void; +}) { + const { + attributes, + listeners, + setNodeRef, + transform, + transition, + } = useSortable({ id: media.id }); + + const style = { + transform: CSS.Transform.toString(transform), + transition, + }; + + return ( +
+ + {isSelected && ( + <> +
+ {index + 1} +
+
+ +
+ + )} +
+ ); +} + export default function NewPost() { const [sidebarOpen, setSidebarOpen] = useState(false); const [, navigate] = useLocation(); @@ -208,6 +288,25 @@ export default function NewPost() { } }; + const sensors = useSensors( + useSensor(PointerSensor), + useSensor(KeyboardSensor, { + coordinateGetter: sortableKeyboardCoordinates, + }) + ); + + const handleDragEnd = (event: DragEndEvent) => { + const { active, over } = event; + + if (over && active.id !== over.id) { + setSelectedMedia((items) => { + const oldIndex = items.indexOf(active.id as string); + const newIndex = items.indexOf(over.id as string); + return arrayMove(items, oldIndex, newIndex); + }); + } + }; + return (
{sidebarOpen && ( @@ -309,50 +408,90 @@ export default function NewPost() {
) : ( -
- {mediaList.map((media) => { - const isSelected = selectedMedia.includes(media.id); - const selectionIndex = selectedMedia.indexOf(media.id); - - return ( - - ); - })} -
+ + + + )} +
+ Toutes les photos +
+
+ {mediaList.map((media) => { + const isSelected = selectedMedia.includes(media.id); + + return ( + + ); + })} +
+ )} From 304f0bc473e135ee203e3a5a28375c2463035717 Mon Sep 17 00:00:00 2001 From: michaelschal <35957947-michaelschal@users.noreply.replit.com> Date: Wed, 8 Oct 2025 20:47:43 +0000 Subject: [PATCH 7/8] Add a preview modal to review posts before publishing Introduce a new PreviewModal component for post previsualization and integrate it into the new-post page, allowing users to select preview formats and navigate media. 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/LsHUZLR --- client/src/components/preview-modal.tsx | 335 ++++++++++++++++++++++++ client/src/pages/new-post.tsx | 18 +- 2 files changed, 350 insertions(+), 3 deletions(-) create mode 100644 client/src/components/preview-modal.tsx diff --git a/client/src/components/preview-modal.tsx b/client/src/components/preview-modal.tsx new file mode 100644 index 0000000..5e1f55a --- /dev/null +++ b/client/src/components/preview-modal.tsx @@ -0,0 +1,335 @@ +import { useState, useEffect } from 'react'; +import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from '@/components/ui/dialog'; +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 } from 'lucide-react'; +import type { Media } from '@shared/schema'; + +interface PreviewModalProps { + open: boolean; + onOpenChange: (open: boolean) => void; + postText: string; + selectedMedia: string[]; + mediaList: Media[]; + onPublish: () => void; + isPublishing: boolean; +} + +type PreviewFormat = 'facebook-feed' | 'instagram-feed' | 'instagram-story'; + +export function PreviewModal({ + open, + onOpenChange, + postText, + selectedMedia, + mediaList, + onPublish, + isPublishing +}: PreviewModalProps) { + const [previewFormat, setPreviewFormat] = useState('facebook-feed'); + const [currentPhotoIndex, setCurrentPhotoIndex] = useState(0); + + const orderedMedia = selectedMedia + .map(id => mediaList.find(m => m.id === id)) + .filter((m): m is Media => m !== undefined); + + // Reset photo index when modal opens or media changes + useEffect(() => { + if (open) { + setCurrentPhotoIndex(0); + } + }, [open, selectedMedia.length]); + + const nextPhoto = () => { + setCurrentPhotoIndex((prev) => (prev + 1) % orderedMedia.length); + }; + + const prevPhoto = () => { + setCurrentPhotoIndex((prev) => (prev - 1 + orderedMedia.length) % orderedMedia.length); + }; + + const goToPhoto = (index: number) => { + setCurrentPhotoIndex(index); + }; + + const getMediaUrl = (media: Media) => { + if (previewFormat === 'facebook-feed') { + return media.facebookFeedUrl || media.originalUrl; + } else if (previewFormat === 'instagram-feed') { + return media.instagramFeedUrl || media.originalUrl; + } else { + return media.instagramStoryUrl || media.originalUrl; + } + }; + + const renderFacebookFeed = () => ( +
+ {/* Header */} +
+
+ +
+
+
Votre Page
+
À l'instant
+
+
+ + {/* Text */} + {postText && ( +
+ {postText} +
+ )} + + {/* Media Carousel */} + {orderedMedia.length > 0 && ( +
+ {`Photo + {orderedMedia.length > 1 && ( + <> + + +
+ {orderedMedia.map((_, idx) => ( +
+ + )} +
+ )} + + {/* Actions */} +
+ + + +
+
+ ); + + const renderInstagramFeed = () => ( +
+ {/* Header */} +
+
+ +
+
+
votre_page
+
+
+ + {/* Media Carousel */} + {orderedMedia.length > 0 && ( +
+ {`Photo + {orderedMedia.length > 1 && ( + <> + + +
+ {orderedMedia.map((_, idx) => ( +
+ + )} +
+ )} + + {/* Actions */} +
+ + + + +
+ + {/* Text */} + {postText && ( +
+ votre_page + {postText} +
+ )} +
+ ); + + const renderInstagramStory = () => ( +
+ {/* Header */} +
+
+ +
+
+
votre_page
+
+
+ + {/* Media */} + {orderedMedia.length > 0 && ( +
+ {`Story + {orderedMedia.length > 1 && ( + <> +
+ {orderedMedia.map((_, idx) => ( +
+ + + + )} + {/* Text overlay */} + {postText && ( +
+
+

{postText}

+
+
+ )} +
+ )} +
+ ); + + return ( + + + + Prévisualisation de la publication + + Visualisez votre publication avant de la publier + + + +
+ {/* Format selector */} +
+ + +
+ + {/* Preview */} +
+ {previewFormat === 'facebook-feed' && renderFacebookFeed()} + {previewFormat === 'instagram-feed' && renderInstagramFeed()} + {previewFormat === 'instagram-story' && renderInstagramStory()} +
+ + {/* Actions */} +
+ + +
+
+
+
+ ); +} diff --git a/client/src/pages/new-post.tsx b/client/src/pages/new-post.tsx index b165cdf..35f2d7e 100644 --- a/client/src/pages/new-post.tsx +++ b/client/src/pages/new-post.tsx @@ -32,6 +32,7 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@ import { useToast } from "@/hooks/use-toast"; import { apiRequest, queryClient } from "@/lib/queryClient"; import type { SocialPage, Media } from "@shared/schema"; +import { PreviewModal } from "@/components/preview-modal"; function SortableMediaItem({ media, @@ -109,6 +110,7 @@ export default function NewPost() { const [generatedVariants, setGeneratedVariants] = useState([]); const [postText, setPostText] = useState(''); const [postType, setPostType] = useState<'feed' | 'story' | 'both'>('feed'); + const [previewModalOpen, setPreviewModalOpen] = useState(false); const { data: pages = [] } = useQuery({ queryKey: ['/api/pages'], @@ -675,19 +677,29 @@ export default function NewPost() { + + ); } From 56f1ceb84e354c00bcb6a33219e8ae9d3adf5751 Mon Sep 17 00:00:00 2001 From: michaelschal <35957947-michaelschal@users.noreply.replit.com> Date: Wed, 8 Oct 2025 20:49:16 +0000 Subject: [PATCH 8/8] Add multi-photo carousel posts and preview modal functionality Implements multi-photo carousel post creation with drag-and-drop reordering, adds a new `post_media` junction table, and introduces a preview modal for realistic Facebook/Instagram rendering before publishing. Updates `POST /api/posts` to accept `mediaIds` for carousel media. Replit-Commit-Author: Agent Replit-Commit-Session-Id: ae4037a0-2a6f-4530-9bac-79b543286bda Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/397bca8c-984f-43ff-841a-10897aeb8140/ae4037a0-2a6f-4530-9bac-79b543286bda/LsHUZLR --- replit.md | 50 +++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 49 insertions(+), 1 deletion(-) diff --git a/replit.md b/replit.md index 80479af..8c98c5d 100644 --- a/replit.md +++ b/replit.md @@ -10,6 +10,8 @@ Social Flow is a comprehensive social media automation platform designed to stre - Multi-page social media account management (Facebook & Instagram) - AI-powered content generation using OpenRouter API - Automated media processing and format optimization +- **Multi-photo carousel posts** (up to 10 photos per post with drag & drop reordering) +- **Preview modal** with realistic Facebook/Instagram rendering before publishing - Post scheduling with automated publication - Media library management - Dashboard analytics and activity tracking @@ -121,6 +123,7 @@ Core Tables: - `social_pages` - Connected Facebook/Instagram pages with access tokens - `media` - Uploaded media files with Cloudinary public IDs and transformation URLs - `posts` - Post content with AI generation tracking and status +- `post_media` - **NEW**: Junction table linking posts to multiple media with `displayOrder` field for carousel ordering - `scheduled_posts` - Scheduled publication queue with page assignments - `ai_generations` - History of AI-generated content @@ -222,4 +225,49 @@ Core Tables: - Zod: Schema validation for forms and API data - date-fns: Date manipulation and formatting - react-dropzone: Drag-and-drop file uploads -- Lucide React: Icon library \ No newline at end of file +- Lucide React: Icon library +- **@dnd-kit**: Drag and drop library for photo carousel reordering + +## Recent Changes (October 2025) + +### Multi-Photo Carousel Feature + +**Overview**: Support for creating posts with up to 10 photos in a carousel format with drag & drop reordering and preview modal. + +**Database Changes**: +- Added `post_media` table with `displayOrder` integer field for ordered media +- Posts can now have multiple photos via `post_media` junction table +- API accepts array of media IDs with display order + +**Frontend Components**: + +**New Post Page** (`/new-post`): +- **Photo Selection**: "Photos sélectionnées" section shows selected photos with order numbers (1, 2, 3...) +- **Drag & Drop**: Uses @dnd-kit/sortable for reordering selected photos + - Drag handle (GripVertical icon) on each selected photo + - Visual order indicators on each photo + - Separate section for selected vs. all photos +- **Limit**: Maximum 10 photos per post +- **Preview Button**: Opens PreviewModal instead of immediate publish + +**PreviewModal Component** (`client/src/components/preview-modal.tsx`): +- **Realistic Rendering**: Shows post exactly as it will appear on Facebook/Instagram +- **Format Switching**: Toggle between: + - Feed Facebook (1200x630px) - Horizontal carousel + - Feed Instagram (1080x1080px) - Square carousel + - Story Instagram (1080x1920px) - Vertical carousel +- **Carousel Navigation**: + - Next/Previous buttons (ChevronLeft/ChevronRight) + - Clickable photo indicators (dots for Facebook, bars for Instagram) + - Shows current photo index +- **Publish Integration**: Publish button in modal triggers post creation + +**API Changes**: +- `POST /api/posts` accepts `mediaIds` parameter as: + - Simple string array (using index as order): `["id1", "id2", "id3"]` + - Or array of objects with displayOrder: `[{mediaId: "id1", displayOrder: 0}, ...]` +- Backwards compatible with single media posts + +**Storage Layer**: +- `createPost` method creates post_media entries with displayOrder +- Bulk insert of media-post relationships with proper ordering \ No newline at end of file