From 0435a442bd15d3ba058dafda7e5595902e1b4a3f Mon Sep 17 00:00:00 2001 From: michaelschal <35957947-michaelschal@users.noreply.replit.com> Date: Thu, 9 Oct 2025 08:02:49 +0000 Subject: [PATCH 1/9] 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/LsHUZLR From ce8da4dafedfa642c652de665ce67711e7aeffd1 Mon Sep 17 00:00:00 2001 From: michaelschal <35957947-michaelschal@users.noreply.replit.com> Date: Thu, 9 Oct 2025 08:06:17 +0000 Subject: [PATCH 2/9] Add ability to edit scheduled social media posts Implement functionality to edit scheduled posts, including date, time, and target page. Introduces a new dialog component for editing and updates the API to support PATCH requests for scheduled posts. 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/ER8Naji --- .replit | 4 + client/src/components/calendar-view.tsx | 56 +++++-- .../components/edit-scheduled-post-dialog.tsx | 139 ++++++++++++++++++ server/routes.ts | 39 ++++- 4 files changed, 226 insertions(+), 12 deletions(-) create mode 100644 client/src/components/edit-scheduled-post-dialog.tsx diff --git a/.replit b/.replit index d51ca2b..bd88b27 100644 --- a/.replit +++ b/.replit @@ -34,6 +34,10 @@ externalPort = 4200 localPort = 40537 externalPort = 3000 +[[ports]] +localPort = 40905 +externalPort = 6800 + [[ports]] localPort = 42161 externalPort = 6000 diff --git a/client/src/components/calendar-view.tsx b/client/src/components/calendar-view.tsx index 6a40ccc..39b6b97 100644 --- a/client/src/components/calendar-view.tsx +++ b/client/src/components/calendar-view.tsx @@ -1,13 +1,17 @@ import { useState } from "react"; import { Button } from "@/components/ui/button"; -import { ChevronLeft, ChevronRight, Calendar as CalendarIcon, Trash2 } from "lucide-react"; +import { ChevronLeft, ChevronRight, Calendar as CalendarIcon, Trash2, Edit } from "lucide-react"; import { useQuery, useMutation } from "@tanstack/react-query"; import { useToast } from "@/hooks/use-toast"; import { apiRequest, queryClient } from "@/lib/queryClient"; import type { ScheduledPost } from "@shared/schema"; +import { SiFacebook, SiInstagram } from "react-icons/si"; +import EditScheduledPostDialog from "./edit-scheduled-post-dialog"; export default function CalendarView() { const [currentDate, setCurrentDate] = useState(new Date()); + const [editDialogOpen, setEditDialogOpen] = useState(false); + const [selectedPost, setSelectedPost] = useState(null); const { toast } = useToast(); const { data: scheduledPosts = [] } = useQuery({ @@ -40,6 +44,12 @@ export default function CalendarView() { } }; + const handleEditPost = (post: any, e: React.MouseEvent) => { + e.stopPropagation(); + setSelectedPost(post); + setEditDialogOpen(true); + }; + const getPostsForDate = (date: Date) => { return (scheduledPosts || []).filter(post => { if (!post.scheduledAt) return false; @@ -181,10 +191,13 @@ export default function CalendarView() { return (
- {postsForDay.slice(0, 2).map((post, idx) => { + {postsForDay.slice(0, 2).map((post: any, idx) => { const time = post.scheduledAt ? new Date(post.scheduledAt).toLocaleTimeString('fr-FR', { hour: '2-digit', minute: '2-digit' }) : ''; const isPending = !post.publishedAt; const isPublished = !!post.publishedAt; + const pageName = post.page?.pageName || 'Page inconnue'; + const platform = post.page?.platform || 'facebook'; + const PlatformIcon = platform === 'instagram' ? SiInstagram : SiFacebook; return (
{time}
-
{post.postType || 'Post'}
+
+ + {pageName} +
{isPending && ( - +
+ + +
)}
@@ -238,6 +264,14 @@ export default function CalendarView() { + + {selectedPost && ( + + )} ); } diff --git a/client/src/components/edit-scheduled-post-dialog.tsx b/client/src/components/edit-scheduled-post-dialog.tsx new file mode 100644 index 0000000..e93338e --- /dev/null +++ b/client/src/components/edit-scheduled-post-dialog.tsx @@ -0,0 +1,139 @@ +import { useState, useEffect } from "react"; +import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog"; +import { Button } from "@/components/ui/button"; +import { Label } from "@/components/ui/label"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; +import { useQuery, useMutation } from "@tanstack/react-query"; +import { useToast } from "@/hooks/use-toast"; +import { apiRequest, queryClient } from "@/lib/queryClient"; +import type { SocialPage } from "@shared/schema"; +import { format } from "date-fns"; + +interface EditScheduledPostDialogProps { + open: boolean; + onOpenChange: (open: boolean) => void; + scheduledPost: any; +} + +export default function EditScheduledPostDialog({ open, onOpenChange, scheduledPost }: EditScheduledPostDialogProps) { + const { toast } = useToast(); + const [date, setDate] = useState(""); + const [time, setTime] = useState(""); + const [pageId, setPageId] = useState(""); + + const { data: pages = [] } = useQuery({ + queryKey: ["/api/pages"], + }); + + useEffect(() => { + if (scheduledPost && open) { + const scheduledAt = new Date(scheduledPost.scheduledAt); + setDate(format(scheduledAt, "yyyy-MM-dd")); + setTime(format(scheduledAt, "HH:mm")); + setPageId(scheduledPost.pageId); + } + }, [scheduledPost, open]); + + const updateMutation = useMutation({ + mutationFn: (data: { scheduledAt: string; pageId: string }) => + apiRequest('PATCH', `/api/scheduled-posts/${scheduledPost.id}`, data), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['/api/scheduled-posts'] }); + toast({ + title: "Programmation modifiée", + description: "La date et l'heure ont été mises à jour avec succès", + }); + onOpenChange(false); + }, + onError: () => { + toast({ + title: "Erreur", + description: "Impossible de modifier la programmation", + variant: "destructive", + }); + }, + }); + + const handleSave = () => { + if (!date || !time || !pageId) { + toast({ + title: "Erreur", + description: "Veuillez remplir tous les champs", + variant: "destructive", + }); + return; + } + + const scheduledAt = new Date(`${date}T${time}`); + updateMutation.mutate({ scheduledAt: scheduledAt.toISOString(), pageId }); + }; + + return ( + + + + Modifier la programmation + + +
+
+ + setDate(e.target.value)} + className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50" + data-testid="input-scheduled-date" + /> +
+ +
+ + setTime(e.target.value)} + className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50" + data-testid="input-scheduled-time" + /> +
+ +
+ + +
+
+ + + + + +
+
+ ); +} diff --git a/server/routes.ts b/server/routes.ts index 28c51aa..6282a33 100644 --- a/server/routes.ts +++ b/server/routes.ts @@ -9,7 +9,7 @@ import { z } from "zod"; import { openRouterService } from "./services/openrouter"; import { cloudinaryService } from "./services/cloudinary"; import { insertPostSchema, insertScheduledPostSchema, insertSocialPageSchema, insertAiGenerationSchema, insertCloudinaryConfigSchema, updateCloudinaryConfigSchema, insertOpenrouterConfigSchema, updateOpenrouterConfigSchema, insertUserSchema, postMedia, type SocialPage } from "@shared/schema"; -import type { User, InsertUser } from "@shared/schema"; +import type { User, InsertUser, ScheduledPost } from "@shared/schema"; const upload = multer({ storage: multer.memoryStorage() }); @@ -696,6 +696,43 @@ export async function registerRoutes(app: Express): Promise { } }); + app.patch("/api/scheduled-posts/:id", requireAuth, async (req, res) => { + try { + const user = req.user as User; + const userId = user.id; + const { id } = req.params; + + // Verify the scheduled post belongs to the user before updating + const scheduledPost = await storage.getScheduledPost(id); + if (!scheduledPost) { + return res.status(404).json({ error: "Scheduled post not found" }); + } + + const post = await storage.getPost(scheduledPost.postId); + if (!post || post.userId !== userId) { + return res.status(403).json({ error: "Unauthorized" }); + } + + // Only allow updating scheduledAt and pageId + const { scheduledAt, pageId } = req.body; + const updateData: Partial = {}; + + if (scheduledAt) { + updateData.scheduledAt = new Date(scheduledAt); + } + + if (pageId) { + updateData.pageId = pageId; + } + + const updated = await storage.updateScheduledPost(id, updateData); + res.json(updated); + } catch (error) { + console.error("Error updating scheduled post:", error); + res.status(500).json({ error: "Failed to update scheduled post" }); + } + }); + app.post("/api/scheduled-posts", requireAuth, async (req, res) => { try { const scheduledPostData = insertScheduledPostSchema.parse(req.body); From d164f512ccee2da8b44a0a6b95761fd651434b82 Mon Sep 17 00:00:00 2001 From: michaelschal <35957947-michaelschal@users.noreply.replit.com> Date: Thu, 9 Oct 2025 08:12:53 +0000 Subject: [PATCH 3/9] Improve calendar view by adding a responsive list layout Introduce a new component for a responsive calendar list view and update the existing calendar view to conditionally render based on screen size. 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/ER8Naji --- .replit | 4 - client/src/components/calendar-list-view.tsx | 168 +++++++++++++++++++ client/src/components/calendar-view.tsx | 65 +++---- client/src/hooks/use-media-query.ts | 30 ++++ 4 files changed, 235 insertions(+), 32 deletions(-) create mode 100644 client/src/components/calendar-list-view.tsx create mode 100644 client/src/hooks/use-media-query.ts diff --git a/.replit b/.replit index bd88b27..d51ca2b 100644 --- a/.replit +++ b/.replit @@ -34,10 +34,6 @@ externalPort = 4200 localPort = 40537 externalPort = 3000 -[[ports]] -localPort = 40905 -externalPort = 6800 - [[ports]] localPort = 42161 externalPort = 6000 diff --git a/client/src/components/calendar-list-view.tsx b/client/src/components/calendar-list-view.tsx new file mode 100644 index 0000000..edac5de --- /dev/null +++ b/client/src/components/calendar-list-view.tsx @@ -0,0 +1,168 @@ +import { format, isSameDay, parseISO } from "date-fns"; +import { fr } from "date-fns/locale"; +import { Edit, Trash2, ChevronDown, ChevronUp } from "lucide-react"; +import { SiFacebook, SiInstagram } from "react-icons/si"; +import { Button } from "@/components/ui/button"; +import { useState } from "react"; + +interface CalendarListViewProps { + scheduledPosts: any[]; + onEditPost: (post: any) => void; + onDeletePost: (postId: string) => void; +} + +export default function CalendarListView({ scheduledPosts, onEditPost, onDeletePost }: CalendarListViewProps) { + const [expandedDates, setExpandedDates] = useState>(new Set()); + + // Group posts by date + const groupedPosts = scheduledPosts.reduce((acc: { [key: string]: any[] }, post) => { + if (!post.scheduledAt) return acc; + const date = format(parseISO(post.scheduledAt), "yyyy-MM-dd"); + if (!acc[date]) { + acc[date] = []; + } + acc[date].push(post); + return acc; + }, {}); + + // Sort dates + const sortedDates = Object.keys(groupedPosts).sort(); + + const toggleDate = (date: string) => { + const newExpanded = new Set(expandedDates); + if (newExpanded.has(date)) { + newExpanded.delete(date); + } else { + newExpanded.add(date); + } + setExpandedDates(newExpanded); + }; + + const isToday = (dateStr: string) => { + return isSameDay(parseISO(dateStr), new Date()); + }; + + return ( +
+ {sortedDates.length === 0 ? ( +
+ Aucune publication programmée +
+ ) : ( + sortedDates.map((dateStr) => { + const posts = groupedPosts[dateStr]; + const isExpanded = expandedDates.has(dateStr); + const dateObj = parseISO(dateStr); + const isDateToday = isToday(dateStr); + + return ( +
+ + + {isExpanded && ( +
+ {posts.map((post: any) => { + const time = post.scheduledAt + ? new Date(post.scheduledAt).toLocaleTimeString('fr-FR', { + hour: '2-digit', + minute: '2-digit', + }) + : ''; + const isPending = !post.publishedAt; + const isPublished = !!post.publishedAt; + const pageName = post.page?.pageName || 'Page inconnue'; + const platform = post.page?.platform || 'facebook'; + const PlatformIcon = platform === 'instagram' ? SiInstagram : SiFacebook; + + return ( +
+
+
+
+ {time} + + {isPending ? 'Programmé' : 'Publié'} + +
+
+ + {pageName} +
+
+ + {isPending && ( +
+ + +
+ )} +
+
+ ); + })} +
+ )} +
+ ); + }) + )} +
+ ); +} diff --git a/client/src/components/calendar-view.tsx b/client/src/components/calendar-view.tsx index 39b6b97..1292da5 100644 --- a/client/src/components/calendar-view.tsx +++ b/client/src/components/calendar-view.tsx @@ -7,12 +7,15 @@ import { apiRequest, queryClient } from "@/lib/queryClient"; import type { ScheduledPost } from "@shared/schema"; import { SiFacebook, SiInstagram } from "react-icons/si"; import EditScheduledPostDialog from "./edit-scheduled-post-dialog"; +import CalendarListView from "./calendar-list-view"; +import { useMediaQuery } from "@/hooks/use-media-query"; export default function CalendarView() { const [currentDate, setCurrentDate] = useState(new Date()); const [editDialogOpen, setEditDialogOpen] = useState(false); const [selectedPost, setSelectedPost] = useState(null); const { toast } = useToast(); + const isDesktop = useMediaQuery("(min-width: 768px)"); const { data: scheduledPosts = [] } = useQuery({ queryKey: ["/api/scheduled-posts"], @@ -37,15 +40,15 @@ export default function CalendarView() { }, }); - const handleDeletePost = (postId: string, e: React.MouseEvent) => { - e.stopPropagation(); + const handleDeletePost = (postId: string, e?: React.MouseEvent) => { + if (e) e.stopPropagation(); if (confirm("Êtes-vous sûr de vouloir supprimer cette publication programmée ?")) { deletePostMutation.mutate(postId); } }; - const handleEditPost = (post: any, e: React.MouseEvent) => { - e.stopPropagation(); + const handleEditPost = (post: any, e?: React.MouseEvent) => { + if (e) e.stopPropagation(); setSelectedPost(post); setEditDialogOpen(true); }; @@ -147,16 +150,17 @@ export default function CalendarView() { -
-
- {daysOfWeek.map((day) => ( -
- {day} -
- ))} -
+ {isDesktop ? ( +
+
+ {daysOfWeek.map((day) => ( +
+ {day} +
+ ))} +
-
+
{days.map((day, index) => (
-
-
-
- Programmé -
-
-
- Publié +
+
+
+ Programmé +
+
+
+ Publié +
-
- - {selectedPost && ( - )} + +
); } diff --git a/client/src/hooks/use-media-query.ts b/client/src/hooks/use-media-query.ts new file mode 100644 index 0000000..abb69b4 --- /dev/null +++ b/client/src/hooks/use-media-query.ts @@ -0,0 +1,30 @@ +import { useState, useEffect } from "react"; + +export function useMediaQuery(query: string): boolean { + const [matches, setMatches] = useState(false); + + useEffect(() => { + const media = window.matchMedia(query); + + // Set initial value + setMatches(media.matches); + + // Define callback for changes + const listener = (e: MediaQueryListEvent) => { + setMatches(e.matches); + }; + + // Modern browsers + if (media.addEventListener) { + media.addEventListener('change', listener); + return () => media.removeEventListener('change', listener); + } + // Legacy browsers + else { + media.addListener(listener); + return () => media.removeListener(listener); + } + }, [query]); + + return matches; +} From 15d65ebf430ea2775f49ffa8d6d1de74bc5fc31e Mon Sep 17 00:00:00 2001 From: michaelschal <35957947-michaelschal@users.noreply.replit.com> Date: Thu, 9 Oct 2025 08:16:57 +0000 Subject: [PATCH 4/9] Add AI text generation for product descriptions Add AI text generation for product descriptions based on product information. 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/ER8Naji --- .replit | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.replit b/.replit index d51ca2b..d2b4742 100644 --- a/.replit +++ b/.replit @@ -22,6 +22,10 @@ externalPort = 3003 localPort = 35357 externalPort = 3002 +[[ports]] +localPort = 35385 +externalPort = 6800 + [[ports]] localPort = 37431 externalPort = 5000 From 344da0f78efd98ab263bddbc812e74481e423923 Mon Sep 17 00:00:00 2001 From: michaelschal <35957947-michaelschal@users.noreply.replit.com> Date: Thu, 9 Oct 2025 08:18:30 +0000 Subject: [PATCH 5/9] Improve calendar view for mobile and add post editing functionality Enhance calendar page with responsive list view for mobile, including edit functionality for scheduled posts, and display page names and platform icons. 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/ER8Naji --- replit.md | 58 ++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 57 insertions(+), 1 deletion(-) diff --git a/replit.md b/replit.md index 8c98c5d..65ceddf 100644 --- a/replit.md +++ b/replit.md @@ -270,4 +270,60 @@ Core Tables: **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 +- Bulk insert of media-post relationships with proper ordering + +### Calendar Responsive Improvements + +**Overview**: Enhanced calendar page with mobile-responsive list view, edit functionality for scheduled posts, and improved visual display with page names and platform icons. + +**Desktop View (≥768px)**: +- Grid calendar layout with 7 columns (days of week) +- Scheduled posts displayed in day cells with: + - Time (HH:mm format) + - Platform icon (Facebook/Instagram from react-icons/si) + - Page name (from social_pages.page_name) + - Edit button (blue pencil icon, hover to show) + - Delete button (red trash icon, hover to show) +- Color coding: Blue for pending posts, Green for published posts +- Legend showing status colors + +**Mobile View (<768px)**: +- **CalendarListView Component**: Chronological list with date grouping +- **Expandable Sections**: Click to expand/collapse posts by date +- Each date section displays: "Day Date Month Year" with post count +- Highlighted "Aujourd'hui" for current date +- Posts display: + - Time in large font (text-lg) + - Status badge (Programmé/Publié) + - Platform icon + page name + - Edit button (44px x 44px touch target) + - Delete button (44px x 44px touch target) +- Responsive breakpoint detection via useMediaQuery hook + +**Edit Scheduled Post Feature**: +- **EditScheduledPostDialog Component**: Modal dialog for editing scheduled posts +- **Date Picker**: HTML5 date input with YYYY-MM-DD format +- **Time Picker**: HTML5 time input with HH:mm format +- **Page Selector**: Dropdown with platform icons and page names +- Pre-fills current values via useEffect when dialog opens +- **API**: PATCH /api/scheduled-posts/:id endpoint +- Updates: scheduledAt timestamp and pageId +- Cache invalidation after successful update +- Toast notification: "Programmation modifiée" + +**Frontend Components Created**: +- `client/src/components/calendar-list-view.tsx` - Mobile list view with expandable dates +- `client/src/components/edit-scheduled-post-dialog.tsx` - Edit dialog for scheduled posts +- `client/src/hooks/use-media-query.ts` - Responsive breakpoint detection hook + +**Backend Changes**: +- `PATCH /api/scheduled-posts/:id` - Update scheduledAt and pageId for a scheduled post +- Validates date/time and page existence before update +- Returns updated scheduled post with joined page data + +**Mobile Optimizations**: +- 44px minimum touch targets for all interactive elements +- Reduced padding and spacing on mobile devices +- Large, readable text sizes for time and status +- Expandable sections to reduce visual clutter +- Smooth transitions for expand/collapse animations \ No newline at end of file From 5439566577ddcf8a88e0edcc3f23a466a2c9024d Mon Sep 17 00:00:00 2001 From: michaelschal <35957947-michaelschal@users.noreply.replit.com> Date: Thu, 9 Oct 2025 08:20:51 +0000 Subject: [PATCH 6/9] Add ability to update post media and retrieve posts with their associated media Introduce `getPostWithMedia` and `updatePostMedia` methods to `DatabaseStorage` for handling post media associations. 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/ER8Naji --- server/storage.ts | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/server/storage.ts b/server/storage.ts index 20571bd..cb8e701 100644 --- a/server/storage.ts +++ b/server/storage.ts @@ -58,9 +58,11 @@ export interface IStorage { // Posts getPosts(userId: string): Promise; getPost(id: string): Promise; + getPostWithMedia(id: string): Promise<{ post: Post; media: Media[] } | undefined>; createPost(post: InsertPost): Promise; updatePost(id: string, post: Partial): Promise; deletePost(id: string): Promise; + updatePostMedia(postId: string, mediaIds: string[]): Promise; // Scheduled Posts getScheduledPosts(userId: string, startDate?: Date, endDate?: Date): Promise; @@ -181,6 +183,23 @@ export class DatabaseStorage implements IStorage { return post || undefined; } + async getPostWithMedia(id: string): Promise<{ post: Post; media: Media[] } | undefined> { + const post = await this.getPost(id); + if (!post) return undefined; + + const postMediaLinks = await this.getPostMedia(id); + const mediaItems: Media[] = []; + + for (const link of postMediaLinks) { + const mediaItem = await this.getMediaById(link.mediaId); + if (mediaItem) { + mediaItems.push(mediaItem); + } + } + + return { post, media: mediaItems }; + } + async createPost(post: InsertPost): Promise { const [newPost] = await db.insert(posts).values(post).returning(); return newPost; @@ -195,6 +214,19 @@ export class DatabaseStorage implements IStorage { await db.delete(posts).where(eq(posts.id, id)); } + async updatePostMedia(postId: string, mediaIds: string[]): Promise { + await db.delete(postMedia).where(eq(postMedia.postId, postId)); + + if (mediaIds.length > 0) { + const postMediaEntries = mediaIds.map((mediaId, index) => ({ + postId, + mediaId, + displayOrder: index, + })); + await db.insert(postMedia).values(postMediaEntries); + } + } + // Scheduled Posts async getScheduledPosts(userId: string, startDate?: Date, endDate?: Date): Promise { let query = db From db67f4d03952bfdda3561ed83e39edfa658f52c7 Mon Sep 17 00:00:00 2001 From: michaelschal <35957947-michaelschal@users.noreply.replit.com> Date: Thu, 9 Oct 2025 08:21:47 +0000 Subject: [PATCH 7/9] Allow users to edit and preview their posts before publishing Add API endpoints to fetch, update post content, and manage associated media for posts. 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/ER8Naji --- server/routes.ts | 79 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 79 insertions(+) diff --git a/server/routes.ts b/server/routes.ts index 6282a33..6e418ce 100644 --- a/server/routes.ts +++ b/server/routes.ts @@ -653,6 +653,85 @@ export async function registerRoutes(app: Express): Promise { } }); + app.get("/api/posts/:id", requireAuth, async (req, res) => { + try { + const user = req.user as User; + const userId = user.id; + const { id } = req.params; + + const postWithMedia = await storage.getPostWithMedia(id); + if (!postWithMedia) { + return res.status(404).json({ error: "Post not found" }); + } + + if (postWithMedia.post.userId !== userId) { + return res.status(403).json({ error: "Unauthorized" }); + } + + res.json(postWithMedia); + } catch (error) { + console.error("Error fetching post:", error); + res.status(500).json({ error: "Failed to fetch post" }); + } + }); + + app.patch("/api/posts/:id", requireAuth, async (req, res) => { + try { + const user = req.user as User; + const userId = user.id; + const { id } = req.params; + const { content } = req.body; + + const post = await storage.getPost(id); + if (!post) { + return res.status(404).json({ error: "Post not found" }); + } + + if (post.userId !== userId) { + return res.status(403).json({ error: "Unauthorized" }); + } + + const updatedPost = await storage.updatePost(id, { content }); + res.json(updatedPost); + } catch (error) { + console.error("Error updating post:", error); + res.status(500).json({ error: "Failed to update post" }); + } + }); + + app.patch("/api/posts/:id/media", requireAuth, async (req, res) => { + try { + const user = req.user as User; + const userId = user.id; + const { id } = req.params; + const { mediaIds } = req.body; + + const post = await storage.getPost(id); + if (!post) { + return res.status(404).json({ error: "Post not found" }); + } + + if (post.userId !== userId) { + return res.status(403).json({ error: "Unauthorized" }); + } + + if (!Array.isArray(mediaIds)) { + return res.status(400).json({ error: "mediaIds must be an array" }); + } + + if (mediaIds.length > 10) { + return res.status(400).json({ error: "Maximum 10 photos autorisées par publication" }); + } + + await storage.updatePostMedia(id, mediaIds); + const updatedPostWithMedia = await storage.getPostWithMedia(id); + res.json(updatedPostWithMedia); + } catch (error) { + console.error("Error updating post media:", error); + res.status(500).json({ error: "Failed to update post media" }); + } + }); + // Scheduled posts app.get("/api/scheduled-posts", requireAuth, async (req, res) => { try { From d7dbb0bcce9eb35765326efcb1f8ef50f59f08cd Mon Sep 17 00:00:00 2001 From: michaelschal <35957947-michaelschal@users.noreply.replit.com> Date: Thu, 9 Oct 2025 08:25:13 +0000 Subject: [PATCH 8/9] Enable editing of scheduled posts with text and media previews Update the edit scheduled post dialog to allow modification of post content and associated media, incorporating fetched post data and a new text area 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/ER8Naji --- .../components/edit-scheduled-post-dialog.tsx | 218 +++++++++++++++--- 1 file changed, 180 insertions(+), 38 deletions(-) diff --git a/client/src/components/edit-scheduled-post-dialog.tsx b/client/src/components/edit-scheduled-post-dialog.tsx index e93338e..f70732d 100644 --- a/client/src/components/edit-scheduled-post-dialog.tsx +++ b/client/src/components/edit-scheduled-post-dialog.tsx @@ -2,12 +2,14 @@ import { useState, useEffect } from "react"; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog"; import { Button } from "@/components/ui/button"; import { Label } from "@/components/ui/label"; +import { Textarea } from "@/components/ui/textarea"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { useQuery, useMutation } from "@tanstack/react-query"; import { useToast } from "@/hooks/use-toast"; import { apiRequest, queryClient } from "@/lib/queryClient"; -import type { SocialPage } from "@shared/schema"; +import type { SocialPage, Media, Post } from "@shared/schema"; import { format } from "date-fns"; +import { Image as ImageIcon, CheckCircle } from "lucide-react"; interface EditScheduledPostDialogProps { open: boolean; @@ -20,11 +22,23 @@ export default function EditScheduledPostDialog({ open, onOpenChange, scheduledP const [date, setDate] = useState(""); const [time, setTime] = useState(""); const [pageId, setPageId] = useState(""); + const [postText, setPostText] = useState(""); + const [selectedMediaIds, setSelectedMediaIds] = useState([]); const { data: pages = [] } = useQuery({ queryKey: ["/api/pages"], }); + const { data: postData } = useQuery<{ post: Post; media: Media[] }>({ + queryKey: ["/api/posts", scheduledPost?.postId], + enabled: !!scheduledPost?.postId && open, + }); + + const { data: allMedia = [] } = useQuery({ + queryKey: ["/api/media"], + enabled: open, + }); + useEffect(() => { if (scheduledPost && open) { const scheduledAt = new Date(scheduledPost.scheduledAt); @@ -34,27 +48,38 @@ export default function EditScheduledPostDialog({ open, onOpenChange, scheduledP } }, [scheduledPost, open]); - const updateMutation = useMutation({ + useEffect(() => { + if (postData && open) { + setPostText(postData.post.content); + setSelectedMediaIds(postData.media.map(m => m.id)); + } + }, [postData, open]); + + const updateScheduledMutation = useMutation({ mutationFn: (data: { scheduledAt: string; pageId: string }) => apiRequest('PATCH', `/api/scheduled-posts/${scheduledPost.id}`, data), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['/api/scheduled-posts'] }); - toast({ - title: "Programmation modifiée", - description: "La date et l'heure ont été mises à jour avec succès", - }); - onOpenChange(false); - }, - onError: () => { - toast({ - title: "Erreur", - description: "Impossible de modifier la programmation", - variant: "destructive", - }); }, }); - const handleSave = () => { + const updatePostTextMutation = useMutation({ + mutationFn: (data: { content: string }) => + apiRequest('PATCH', `/api/posts/${scheduledPost?.postId}`, data), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['/api/posts', scheduledPost?.postId] }); + }, + }); + + const updatePostMediaMutation = useMutation({ + mutationFn: (data: { mediaIds: string[] }) => + apiRequest('PATCH', `/api/posts/${scheduledPost?.postId}/media`, data), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['/api/posts', scheduledPost?.postId] }); + }, + }); + + const handleSave = async () => { if (!date || !time || !pageId) { toast({ title: "Erreur", @@ -64,40 +89,157 @@ export default function EditScheduledPostDialog({ open, onOpenChange, scheduledP return; } - const scheduledAt = new Date(`${date}T${time}`); - updateMutation.mutate({ scheduledAt: scheduledAt.toISOString(), pageId }); + try { + const scheduledAt = new Date(`${date}T${time}`); + + await updateScheduledMutation.mutateAsync({ scheduledAt: scheduledAt.toISOString(), pageId }); + + if (postData && postText !== postData.post.content) { + await updatePostTextMutation.mutateAsync({ content: postText }); + } + + const currentMediaIds = postData?.media.map(m => m.id) || []; + if (JSON.stringify(selectedMediaIds) !== JSON.stringify(currentMediaIds)) { + await updatePostMediaMutation.mutateAsync({ mediaIds: selectedMediaIds }); + } + + toast({ + title: "Modifications enregistrées", + description: "Le post a été mis à jour avec succès", + }); + onOpenChange(false); + } catch (error) { + toast({ + title: "Erreur", + description: "Impossible de modifier le post", + variant: "destructive", + }); + } }; + const toggleMediaSelection = (mediaId: string) => { + setSelectedMediaIds(prev => { + if (prev.includes(mediaId)) { + return prev.filter(id => id !== mediaId); + } else { + if (prev.length >= 10) { + toast({ + title: "Limite atteinte", + description: "Maximum 10 photos par publication", + variant: "destructive", + }); + return prev; + } + return [...prev, mediaId]; + } + }); + }; + + const isPending = updateScheduledMutation.isPending || updatePostTextMutation.isPending || updatePostMediaMutation.isPending; + return ( - + - Modifier la programmation + Modifier le post
- - setDate(e.target.value)} - className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50" - data-testid="input-scheduled-date" + +