From 4072f8954f75a2dae5b6eb448083f2c24570d7e0 Mon Sep 17 00:00:00 2001 From: Michael SCHAL Date: Tue, 24 Mar 2026 19:36:53 +0100 Subject: [PATCH] feat: add publication scheduling to Remotion video generator - New POST /api/remotion/publish endpoint: uploads MP4 to Cloudinary, creates post/media records, publishes or schedules per-page as a Reel - Desktop and mobile pages show page selector + DateTimePicker + description after video generation, matching the Reels publication workflow Co-Authored-By: Claude Sonnet 4.6 --- client/src/pages/mobile/remotion-video.tsx | 80 ++++++++++++++- client/src/pages/remotion-video.tsx | 111 +++++++++++++++++++-- server/routes/remotion.ts | 103 +++++++++++++++++++ 3 files changed, 282 insertions(+), 12 deletions(-) diff --git a/client/src/pages/mobile/remotion-video.tsx b/client/src/pages/mobile/remotion-video.tsx index b52ed23..8b4fb9e 100644 --- a/client/src/pages/mobile/remotion-video.tsx +++ b/client/src/pages/mobile/remotion-video.tsx @@ -8,12 +8,14 @@ import { Textarea } from "@/components/ui/textarea"; import { Label } from "@/components/ui/label"; import { Switch } from "@/components/ui/switch"; import { Slider } from "@/components/ui/slider"; -import { UploadCloud, Video, Loader2, Menu, Check, Sparkles, Mic, Volume2, Music, Play, Pause } from "lucide-react"; +import { UploadCloud, Video, Loader2, Menu, Check, Sparkles, Mic, Volume2, Music, Play, Pause, Send } from "lucide-react"; import { Sheet, SheetContent, SheetTrigger } from "@/components/ui/sheet"; import Sidebar from "@/components/sidebar"; +import { Checkbox } from "@/components/ui/checkbox"; +import { DateTimePicker } from "@/components/datetime-picker"; import { useQuery, useMutation } from "@tanstack/react-query"; import { apiRequest } from "@/lib/queryClient"; -import type { Media } from "@shared/schema"; +import type { Media, SocialPage } from "@shared/schema"; interface AudioTrack { id: string; title: string; fileName: string; url: string; duration: number; } @@ -56,18 +58,23 @@ export default function MobileRemotionVideoPage() { const [isPlaying, setIsPlaying] = useState(null); const [isRendering, setIsRendering] = useState(false); const [videoUrl, setVideoUrl] = useState(null); + const [selectedPageIds, setSelectedPageIds] = useState([]); + const [scheduledDate, setScheduledDate] = useState(undefined); + const [publishDescription, setPublishDescription] = useState(""); + const [isPublishing, setIsPublishing] = useState(false); const audioRef = useRef(null); const { toast } = useToast(); const { data: allMedia = [] } = useQuery({ queryKey: ['/api/media'] }); const { data: audioTracks = [], isLoading: tracksLoading } = useQuery({ queryKey: ['/api/audio-tracks'] }); const { data: cloudinaryConfig } = useQuery<{ cloudName?: string; logoPublicId?: string } | null>({ queryKey: ['/api/cloudinary/config'] }); - const { data: socialPages = [] } = useQuery<{ pageName: string }[]>({ queryKey: ['/api/pages'] }); + const { data: socialPages = [] } = useQuery({ queryKey: ['/api/pages'] }); const previewLogoUrl = cloudinaryConfig?.cloudName && cloudinaryConfig?.logoPublicId ? `https://res.cloudinary.com/${cloudinaryConfig.cloudName}/image/upload/${cloudinaryConfig.logoPublicId}` : undefined; - const previewStoreName = (socialPages[0] as any)?.pageName as string | undefined; + const previewStoreName = socialPages[0]?.pageName as string | undefined; + const facebookPages = socialPages.filter(p => p.platform === 'facebook'); const imageMediaList = allMedia.filter(m => m.type === 'image').slice(0, 20); const generateTextMutation = useMutation({ @@ -143,6 +150,27 @@ export default function MobileRemotionVideoPage() { } finally { setIsRendering(false); } }; + const handlePublish = async () => { + if (!videoUrl) return; + if (selectedPageIds.length === 0) { toast({ title: "Sélectionnez au moins une page.", variant: "destructive" }); return; } + setIsPublishing(true); + try { + const response = await apiRequest('POST', '/api/remotion/publish', { + videoUrl, pageIds: selectedPageIds, + scheduledFor: scheduledDate?.toISOString(), + description: publishDescription || undefined, + }); + const data = await response.json(); + if (!data.success) throw new Error(data.results?.find((r: any) => r.error)?.error || "Erreur"); + toast({ + title: scheduledDate ? "Publication planifiée !" : "Vidéo publiée !", + description: scheduledDate ? `Sera publiée le ${scheduledDate.toLocaleString('fr-FR')}` : "Publiée sur Facebook.", + }); + } catch (err: any) { + toast({ title: "Erreur de publication", description: err.message, variant: "destructive" }); + } finally { setIsPublishing(false); } + }; + return (