From c7b4b2c65350c1a82b99c4ba5da6ba476ca046c6 Mon Sep 17 00:00:00 2001 From: michaelschal <35957947-michaelschal@users.noreply.replit.com> Date: Tue, 28 Oct 2025 12:53:38 +0000 Subject: [PATCH 1/4] 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/VJoXNjs From 5681814957ff5a76d00f412cd836c4be7d302cc6 Mon Sep 17 00:00:00 2001 From: michaelschal <35957947-michaelschal@users.noreply.replit.com> Date: Tue, 28 Oct 2025 12:59:19 +0000 Subject: [PATCH 2/4] Add a date and time picker for scheduling posts Integrates a new DateTimePicker component into the new post page, replacing the old datetime-local input. This component allows for selecting specific dates and times, and also visually indicates dates with pre-existing scheduled posts. The internal state and API request payload have been updated to correctly handle Date objects instead of strings. 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/aGnsIb5 --- client/src/components/datetime-picker.tsx | 210 ++++++++++++++++++++++ client/src/pages/new-post.tsx | 94 ++-------- 2 files changed, 221 insertions(+), 83 deletions(-) create mode 100644 client/src/components/datetime-picker.tsx diff --git a/client/src/components/datetime-picker.tsx b/client/src/components/datetime-picker.tsx new file mode 100644 index 0000000..9f1f009 --- /dev/null +++ b/client/src/components/datetime-picker.tsx @@ -0,0 +1,210 @@ +import { useState, useEffect } from "react"; +import { Calendar } from "@/components/ui/calendar"; +import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; +import { Button } from "@/components/ui/button"; +import { Calendar as CalendarIcon, Clock, X } from "lucide-react"; +import { format } from "date-fns"; +import { fr } from "date-fns/locale"; +import { cn } from "@/lib/utils"; +import { ScrollArea } from "@/components/ui/scroll-area"; + +interface DateTimePickerProps { + value: Date | undefined; + onChange: (date: Date | undefined) => void; + occupiedDates?: Date[]; + placeholder?: string; +} + +const OPTIMAL_HOURS = [8, 12, 13, 18, 19, 20]; + +export function DateTimePicker({ value, onChange, occupiedDates = [], placeholder = "Sélectionner une date" }: DateTimePickerProps) { + const [selectedDate, setSelectedDate] = useState(value); + const [selectedHour, setSelectedHour] = useState(value?.getHours() || 8); + const [selectedMinute, setSelectedMinute] = useState(value?.getMinutes() || 0); + + // Sync internal state with parent value prop + useEffect(() => { + if (value) { + setSelectedDate(value); + setSelectedHour(value.getHours()); + setSelectedMinute(value.getMinutes()); + } else { + setSelectedDate(undefined); + setSelectedHour(8); + setSelectedMinute(0); + } + }, [value]); + + const hours = Array.from({ length: 24 }, (_, i) => i); + const minutes = [0, 15, 30, 45]; + + const isOptimalHour = (hour: number) => OPTIMAL_HOURS.includes(hour); + + const handleDateSelect = (date: Date | undefined) => { + setSelectedDate(date); + if (date) { + const newDate = new Date(date); + newDate.setHours(selectedHour); + newDate.setMinutes(selectedMinute); + onChange(newDate); + } else { + onChange(undefined); + } + }; + + const handleTimeChange = (hour: number, minute: number) => { + setSelectedHour(hour); + setSelectedMinute(minute); + if (selectedDate) { + const newDate = new Date(selectedDate); + newDate.setHours(hour); + newDate.setMinutes(minute); + onChange(newDate); + } + }; + + // Convert occupied dates to a Set of date strings for efficient lookup + const occupiedDateStrings = new Set( + occupiedDates.map(date => format(date, 'yyyy-MM-dd')) + ); + + // Check if a date is occupied + const isDateOccupied = (date: Date) => { + return occupiedDateStrings.has(format(date, 'yyyy-MM-dd')); + }; + + return ( +
+
+ + + + + + isDateOccupied(date) + }} + modifiersClassNames={{ + occupied: "bg-red-500/20 text-red-700 dark:text-red-300 font-bold hover:bg-red-500/30" + }} + disabled={(date) => date < new Date(new Date().setHours(0, 0, 0, 0))} + data-testid="calendar-picker" + /> + + + {value && ( + + )} +
+ + {selectedDate && ( + + + + + +
+ +
+
Heures
+ {hours.map((hour) => ( + + ))} +
+
+ +
+
Minutes
+ {minutes.map((minute) => ( + + ))} +
+
+
+
+
+
+ + Heures optimales (8h, 12h-13h, 18h-20h) + +
+

+ Ces créneaux maximisent l'engagement +

+
+
+
+ )} + + {occupiedDates.length > 0 && ( +
+
+
+ + Les dates en fond rouge ont déjà des publications programmées + +
+
+ )} +
+ ); +} diff --git a/client/src/pages/new-post.tsx b/client/src/pages/new-post.tsx index 7a1cdb2..d817c7c 100644 --- a/client/src/pages/new-post.tsx +++ b/client/src/pages/new-post.tsx @@ -34,6 +34,7 @@ import { useToast } from "@/hooks/use-toast"; import { apiRequest, queryClient } from "@/lib/queryClient"; import type { SocialPage, Media, ScheduledPost } from "@shared/schema"; import { PreviewModal } from "@/components/preview-modal"; +import { DateTimePicker } from "@/components/datetime-picker"; function SortableMediaItem({ media, @@ -108,7 +109,7 @@ export default function NewPost() { const [productInfo, setProductInfo] = useState(''); const [selectedMedia, setSelectedMedia] = useState([]); const [selectedPages, setSelectedPages] = useState([]); - const [scheduledDate, setScheduledDate] = useState(''); + const [scheduledDate, setScheduledDate] = useState(undefined); const [generatedVariants, setGeneratedVariants] = useState([]); const [postText, setPostText] = useState(''); const [postType, setPostType] = useState<'feed' | 'story' | 'both'>('feed'); @@ -280,7 +281,7 @@ export default function NewPost() { createPostMutation.mutate({ content: postText, - scheduledFor: scheduledDate ? new Date(scheduledDate).toISOString() : undefined, + scheduledFor: scheduledDate ? scheduledDate.toISOString() : undefined, mediaIds: selectedMedia.length > 0 ? selectedMedia : undefined, pageIds: selectedPages, postType, @@ -712,91 +713,18 @@ export default function NewPost() {
- - Date et heure + setScheduledDate(e.target.value)} - data-testid="input-scheduled-date" + onChange={setScheduledDate} + occupiedDates={scheduledPosts + .filter(post => post.scheduledAt) + .map(post => new Date(post.scheduledAt!))} + placeholder="Publier immédiatement" />

- Laissez vide pour publier immédiatement + Sélectionnez une date pour planifier, sinon publication immédiate

- - {(() => { - // Get dates with scheduled posts - const datesWithPosts = scheduledPosts - .filter(post => post.scheduledAt) - .map(post => { - const date = new Date(post.scheduledAt!); - return date.toISOString().split('T')[0]; - }) - .filter((date, index, self) => self.indexOf(date) === index) - .sort(); - - if (datesWithPosts.length > 0) { - return ( -
-
-
- - Dates avec publications programmées - -
-
- {datesWithPosts.slice(0, 5).map((date) => { - const dateObj = new Date(date + 'T00:00:00'); - const formatted = dateObj.toLocaleDateString('fr-FR', { - day: '2-digit', - month: 'short' - }); - return ( - - {formatted} - - ); - })} - {datesWithPosts.length > 5 && ( - - +{datesWithPosts.length - 5} autre{datesWithPosts.length - 5 > 1 ? 's' : ''} - - )} -
-

- Ces dates ont déjà des publications planifiées -

-
- ); - } - return null; - })()} - -
-
-
- - Heures optimales pour l'audience - -
-
- - 8h - 9h - - - 12h - 14h - - - 18h - 21h - -
-

- Ces créneaux maximisent la portée de vos publications -

-
From 5398d3f3abd8230f77feb01d5dc252b4ec68b96c Mon Sep 17 00:00:00 2001 From: michaelschal <35957947-michaelschal@users.noreply.replit.com> Date: Tue, 28 Oct 2025 13:04:24 +0000 Subject: [PATCH 3/4] Improve interactive calendar with visual posting aids Update calendar component with custom DateTimePicker, visual indicators for occupied dates and optimal posting times, and French locale support. 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/aGnsIb5 --- replit.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/replit.md b/replit.md index 213f6a1..53ec9c8 100644 --- a/replit.md +++ b/replit.md @@ -11,7 +11,7 @@ Preferred communication style: Simple, everyday language. ## Recent Changes ### October 28, 2025 -- **Calendar Mobile UX Improvements**: Enhanced mobile calendar and publication planning interface with three visual improvements: (1) Mobile calendar list now displays today's date at the top, before chronological ordering, for quick access to current day's publications; (2) Added green visual indicators in new-post page showing optimal posting times (8h-9h, 12h-14h, 18h-21h) to maximize audience reach; (3) Added red visual indicators showing dates with existing scheduled posts to prevent scheduling conflicts. Both indicators appear dynamically below the datetime selector, displaying up to 5 dates with option to view more. +- **Interactive Calendar with Visual Indicators**: Replaced native datetime-local input with custom DateTimePicker component featuring visual planning aids. Calendar displays occupied dates with red background (bg-red-500/20) to prevent scheduling conflicts. Time selector highlights optimal posting hours (8h, 12h-13h, 18h-20h) with green background (bg-green-500/20) and sparkle emoji to maximize audience engagement. Component includes clear button (X icon) to quickly return to immediate publishing, useEffect synchronization for external state updates, and French locale support via date-fns. Mobile calendar list still displays today's date first for quick access. - **Facebook Video Story Publishing Fix**: Fixed critical bug preventing video story publication. Facebook Graph API requires a 3-phase upload process for video stories: (1) START phase - initialize upload session and get `video_id` + `upload_url`, (2) UPLOAD phase - POST video to rupload.facebook.com endpoint with `file_url` header and OAuth authorization, (3) FINISH phase - finalize and publish story with `video_id`. Previous implementation incorrectly attempted direct upload to `/video_stories` endpoint with `file_url` parameter, causing "#100 The parameter upload_phase is required" error. Fixed by implementing proper 3-phase workflow using remote URL upload method (file_url header), avoiding need for binary chunked uploads. ### October 17, 2025 From fe006c12fadbd32d8412b119ac84c74b6ca13c24 Mon Sep 17 00:00:00 2001 From: michaelschal <35957947-michaelschal@users.noreply.replit.com> Date: Tue, 28 Oct 2025 13:04:40 +0000 Subject: [PATCH 4/4] Add initial project files and setup for a social media posting application Initialize project with basic file structure and dependencies. 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/aGnsIb5