mirror of
https://github.com/R0m1k3/Socialflow.git
synced 2026-10-11 17:26:45 +02:00
feat: Add mobile adaptation for Reels/History and fix FFmpeg service architecture
This commit is contained in:
1 parent
1050f351c2
commit
366fa2f15f
3 files changed
+563
-2
No files matched your search
+6
-2
@@ -7,9 +7,13 @@ import { TooltipProvider } from "@/components/ui/tooltip";
|
||||
import { createResponsiveRoute } from "@/components/responsive-route";
|
||||
import NotFound from "@/pages/not-found";
|
||||
import Login from "@/pages/login";
|
||||
import History from "@/pages/history";
|
||||
|
||||
|
||||
// Create responsive routes with lazy loading for optimal performance
|
||||
const History = createResponsiveRoute(
|
||||
() => import("@/pages/history"),
|
||||
() => import("@/pages/mobile/history")
|
||||
);
|
||||
const Dashboard = createResponsiveRoute(
|
||||
() => import("@/pages/dashboard"),
|
||||
() => import("@/pages/mobile/dashboard")
|
||||
@@ -68,7 +72,7 @@ const Analytics = createResponsiveRoute(
|
||||
// NewReel uses desktop version for now (mobile can be added later)
|
||||
const NewReel = createResponsiveRoute(
|
||||
() => import("@/pages/new-reel"),
|
||||
() => import("@/pages/new-reel") // Uses same component for now
|
||||
() => import("@/pages/mobile/new-reel") // Uses same component for now
|
||||
);
|
||||
|
||||
function ProtectedRoute({ component: Component, adminOnly = false }: { component: React.ComponentType; adminOnly?: boolean }) {
|
||||
|
||||
@@ -0,0 +1,144 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { format } from "date-fns";
|
||||
import { fr } from "date-fns/locale";
|
||||
import {
|
||||
Calendar, CheckCircle, XCircle, Clock,
|
||||
MoreHorizontal, Filter, Search
|
||||
} from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import Sidebar from "@/components/sidebar";
|
||||
import { Sheet, SheetContent, SheetTrigger } from "@/components/ui/sheet";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
|
||||
export default function MobileHistory() {
|
||||
const [searchTerm, setSearchTerm] = useState("");
|
||||
|
||||
// Fetch posts history
|
||||
const { data: posts } = useQuery<any[]>({
|
||||
queryKey: ['/api/posts'],
|
||||
});
|
||||
|
||||
const getStatusIcon = (status: string) => {
|
||||
switch (status) {
|
||||
case 'published': return <CheckCircle className="w-5 h-5 text-green-500" />;
|
||||
case 'failed': return <XCircle className="w-5 h-5 text-red-500" />;
|
||||
case 'scheduled': return <Clock className="w-5 h-5 text-amber-500" />;
|
||||
default: return <Clock className="w-5 h-5 text-muted-foreground" />;
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusLabel = (status: string) => {
|
||||
switch (status) {
|
||||
case 'published': return "Publié";
|
||||
case 'failed': return "Échec";
|
||||
case 'scheduled': return "Programmé";
|
||||
default: return "Brouillon";
|
||||
}
|
||||
};
|
||||
|
||||
const filteredPosts = posts?.filter(post =>
|
||||
post.content?.toLowerCase().includes(searchTerm.toLowerCase())
|
||||
) || [];
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
{/* Mobile Header */}
|
||||
<div className="sticky top-0 z-50 bg-background/95 backdrop-blur border-b p-4 flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<Sheet>
|
||||
<SheetTrigger asChild>
|
||||
<Button variant="ghost" size="icon" className="-ml-2">
|
||||
<MoreHorizontal className="h-6 w-6" />
|
||||
</Button>
|
||||
</SheetTrigger>
|
||||
<SheetContent side="left" className="p-0 w-[80%]">
|
||||
<Sidebar />
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
<h1 className="font-semibold text-lg">Historique</h1>
|
||||
</div>
|
||||
<Button variant="ghost" size="icon">
|
||||
<Filter className="h-5 w-5" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="p-4 space-y-4">
|
||||
{/* Search */}
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-3 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
placeholder="Rechercher une publication..."
|
||||
className="pl-9 bg-secondary/50"
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Timeline List */}
|
||||
<div className="space-y-3">
|
||||
{filteredPosts.length === 0 ? (
|
||||
<div className="text-center py-12 text-muted-foreground">
|
||||
<Clock className="w-12 h-12 mx-auto mb-3 opacity-20" />
|
||||
<p>Aucune publication trouvée</p>
|
||||
</div>
|
||||
) : (
|
||||
filteredPosts.map((post) => (
|
||||
<Card key={post.id} className="overflow-hidden border-none shadow-sm bg-card">
|
||||
<CardContent className="p-0">
|
||||
<div className="flex items-stretch">
|
||||
<div className={`w-1.5 ${post.status === 'published' ? 'bg-green-500' :
|
||||
post.status === 'failed' ? 'bg-red-500' : 'bg-amber-500'
|
||||
}`} />
|
||||
|
||||
<div className="flex-1 p-3">
|
||||
<div className="flex justify-between items-start mb-2">
|
||||
<Badge variant="outline" className="flex items-center gap-1.5 px-2 py-0.5 h-6 text-xs font-normal">
|
||||
{getStatusIcon(post.status)}
|
||||
{getStatusLabel(post.status)}
|
||||
</Badge>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{post.scheduledFor ? format(new Date(post.scheduledFor), "d MMM HH:mm", { locale: fr }) : '-'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<p className="text-sm line-clamp-2 text-foreground mb-2">
|
||||
{post.content || "Sans contenu"}
|
||||
</p>
|
||||
|
||||
<div className="flex items-center justify-between mt-2">
|
||||
<div className="flex -space-x-2">
|
||||
{/* Placeholder for page icons if available in data */}
|
||||
<div className="w-6 h-6 rounded-full bg-blue-100 border border-background flex items-center justify-center text-[10px]">fb</div>
|
||||
</div>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" size="icon" className="h-8 w-8">
|
||||
<MoreHorizontal className="h-4 w-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem>Voir détails</DropdownMenuItem>
|
||||
<DropdownMenuItem className="text-red-500">Supprimer</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,413 @@
|
||||
import { useState, useCallback, useRef } from "react";
|
||||
import { useQuery, useMutation } from "@tanstack/react-query";
|
||||
import { useLocation } from "wouter";
|
||||
import {
|
||||
Send, Sparkles, Video, Music, Type, Calendar,
|
||||
Upload, Camera, Play, Pause, Volume2,
|
||||
ChevronRight, Loader2, Check, RefreshCw, ChevronLeft,
|
||||
Menu
|
||||
} from "lucide-react";
|
||||
import { useDropzone } from "react-dropzone";
|
||||
import Sidebar from "@/components/sidebar";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { Slider } from "@/components/ui/slider";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
import { apiRequest, queryClient } from "@/lib/queryClient";
|
||||
import type { SocialPage, Media } from "@shared/schema";
|
||||
import { DateTimePicker } from "@/components/datetime-picker";
|
||||
import { Sheet, SheetContent, SheetTrigger } from "@/components/ui/sheet";
|
||||
|
||||
interface MusicTrack {
|
||||
id: string;
|
||||
title: string;
|
||||
artist: string;
|
||||
albumName: string;
|
||||
duration: number;
|
||||
previewUrl: string;
|
||||
downloadUrl: string;
|
||||
imageUrl: string;
|
||||
license: string;
|
||||
}
|
||||
|
||||
type Step = 'video' | 'music' | 'text' | 'publish';
|
||||
|
||||
export default function MobileNewReel() {
|
||||
const [, navigate] = useLocation();
|
||||
const { toast } = useToast();
|
||||
const cameraInputRef = useRef<HTMLInputElement>(null);
|
||||
const audioRef = useRef<HTMLAudioElement>(null);
|
||||
|
||||
// État du workflow
|
||||
const [currentStep, setCurrentStep] = useState<Step>('video');
|
||||
|
||||
// État des données
|
||||
const [selectedVideoId, setSelectedVideoId] = useState<string | null>(null);
|
||||
const [selectedVideo, setSelectedVideo] = useState<Media | null>(null);
|
||||
const [selectedTrack, setSelectedTrack] = useState<MusicTrack | null>(null);
|
||||
const [overlayText, setOverlayText] = useState('');
|
||||
const [productInfo, setProductInfo] = useState('');
|
||||
const [generatedVariants, setGeneratedVariants] = useState<any[]>([]);
|
||||
const [selectedPages, setSelectedPages] = useState<string[]>([]);
|
||||
const [scheduledDate, setScheduledDate] = useState<Date | undefined>(undefined);
|
||||
const [musicVolume, setMusicVolume] = useState([25]);
|
||||
|
||||
// État audio preview
|
||||
const [isPlaying, setIsPlaying] = useState<string | null>(null);
|
||||
const [musicOffset, setMusicOffset] = useState(0);
|
||||
|
||||
const { data: pages = [] } = useQuery<SocialPage[]>({
|
||||
queryKey: ['/api/pages'],
|
||||
});
|
||||
|
||||
const { data: allMedia = [] } = useQuery<Media[]>({
|
||||
queryKey: ['/api/media'],
|
||||
});
|
||||
|
||||
const videoList = allMedia.filter(m => m.type === 'video').slice(0, 12);
|
||||
const [loadedTracks, setLoadedTracks] = useState<MusicTrack[]>([]);
|
||||
|
||||
const { data: musicData, isLoading: musicLoading, refetch: refetchMusic } = useQuery<{ tracks: MusicTrack[] }>({
|
||||
queryKey: ['/api/music/search', selectedVideo?.id, musicOffset],
|
||||
queryFn: async () => {
|
||||
const minDuration = 10;
|
||||
const maxDuration = 120;
|
||||
const response = await fetch(
|
||||
`/api/music/search?minDuration=${minDuration}&maxDuration=${maxDuration}&limit=10&offset=${musicOffset}`
|
||||
);
|
||||
const data = await response.json();
|
||||
setLoadedTracks(data.tracks || []);
|
||||
return data;
|
||||
},
|
||||
enabled: currentStep === 'music' && !!selectedVideo,
|
||||
});
|
||||
|
||||
const handleLoadNewMusic = () => {
|
||||
setMusicOffset(prev => prev + 10);
|
||||
refetchMusic();
|
||||
};
|
||||
|
||||
const uploadMutation = useMutation({
|
||||
mutationFn: async (file: File) => {
|
||||
const formData = new FormData();
|
||||
formData.append("file", file);
|
||||
const response = await fetch("/api/media/upload", {
|
||||
method: "POST",
|
||||
body: formData,
|
||||
});
|
||||
if (!response.ok) throw new Error("Upload failed");
|
||||
return response.json();
|
||||
},
|
||||
onSuccess: (data) => {
|
||||
queryClient.invalidateQueries({ queryKey: ["/api/media"] });
|
||||
setSelectedVideoId(data.id);
|
||||
setSelectedVideo(data);
|
||||
toast({ title: "Succès", description: "Vidéo téléchargée" });
|
||||
},
|
||||
onError: () => {
|
||||
toast({ title: "Erreur", description: "Échec upload", variant: "destructive" });
|
||||
},
|
||||
});
|
||||
|
||||
const generateTextMutation = useMutation({
|
||||
mutationFn: async (productInfoText: string) => {
|
||||
const response = await apiRequest('POST', '/api/reels/generate-text', {
|
||||
productInfo: productInfoText,
|
||||
});
|
||||
return response.json();
|
||||
},
|
||||
onSuccess: (data: any) => {
|
||||
setGeneratedVariants(data.variants || []);
|
||||
toast({ title: "Généré", description: "Variations créées" });
|
||||
},
|
||||
});
|
||||
|
||||
const createReelMutation = useMutation({
|
||||
mutationFn: async (data: any) => {
|
||||
const response = await apiRequest('POST', '/api/reels', data);
|
||||
return response.json();
|
||||
},
|
||||
onSuccess: async (data) => {
|
||||
await queryClient.invalidateQueries({ queryKey: ['/api/scheduled-posts'], refetchType: 'all' });
|
||||
toast({
|
||||
title: data.success ? "Reel créé !" : "Attention",
|
||||
description: data.success ? "Publication réussie" : "Certaines erreurs",
|
||||
});
|
||||
navigate('/');
|
||||
},
|
||||
onError: (error: any) => {
|
||||
toast({ title: "Erreur", description: error.message, variant: "destructive" });
|
||||
},
|
||||
});
|
||||
|
||||
const onDrop = useCallback((acceptedFiles: File[]) => {
|
||||
if (acceptedFiles.length > 0 && acceptedFiles[0].type.startsWith('video/')) {
|
||||
uploadMutation.mutate(acceptedFiles[0]);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const { getRootProps, getInputProps, isDragActive, open } = useDropzone({
|
||||
onDrop,
|
||||
accept: { "video/*": [".mp4", ".mov", ".webm"] },
|
||||
noClick: true,
|
||||
noKeyboard: true,
|
||||
});
|
||||
|
||||
const handleCameraCapture = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (file) uploadMutation.mutate(file);
|
||||
};
|
||||
|
||||
const togglePlayPreview = async (track: MusicTrack) => {
|
||||
if (isPlaying === track.id) {
|
||||
audioRef.current?.pause();
|
||||
setIsPlaying(null);
|
||||
} else {
|
||||
if (audioRef.current) {
|
||||
audioRef.current.src = track.previewUrl;
|
||||
try {
|
||||
await audioRef.current.play();
|
||||
setIsPlaying(track.id);
|
||||
} catch {
|
||||
setIsPlaying(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleCreateReel = () => {
|
||||
if (!selectedVideoId || selectedPages.length === 0) {
|
||||
toast({ title: "Erreur", description: "Vidéo et page requises", variant: "destructive" });
|
||||
return;
|
||||
}
|
||||
createReelMutation.mutate({
|
||||
videoMediaId: selectedVideoId,
|
||||
musicTrackId: selectedTrack?.id,
|
||||
overlayText: overlayText,
|
||||
description: overlayText,
|
||||
pageIds: selectedPages,
|
||||
scheduledFor: scheduledDate?.toISOString(),
|
||||
musicVolume: musicVolume[0] / 100,
|
||||
});
|
||||
};
|
||||
|
||||
const steps: Step[] = ['video', 'music', 'text', 'publish'];
|
||||
const currentStepIndex = steps.indexOf(currentStep);
|
||||
|
||||
const goNext = () => {
|
||||
if (currentStepIndex < steps.length - 1) setCurrentStep(steps[currentStepIndex + 1]);
|
||||
};
|
||||
|
||||
const goBack = () => {
|
||||
if (currentStepIndex > 0) setCurrentStep(steps[currentStepIndex - 1]);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background pb-20">
|
||||
{/* Header Mobile */}
|
||||
<div className="sticky top-0 z-50 bg-background/95 backdrop-blur border-b p-4 flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<Sheet>
|
||||
<SheetTrigger asChild>
|
||||
<Button variant="ghost" size="icon" className="-ml-2">
|
||||
<Menu className="h-6 w-6" />
|
||||
</Button>
|
||||
</SheetTrigger>
|
||||
<SheetContent side="left" className="p-0 w-[80%]">
|
||||
<Sidebar />
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
<h1 className="font-semibold text-lg">Nouveau Reel</h1>
|
||||
</div>
|
||||
<div className="text-sm text-muted-foreground font-medium">
|
||||
{currentStepIndex + 1}/{steps.length}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<main className="p-4 space-y-6">
|
||||
{/* VIDEO STEP */}
|
||||
{currentStep === 'video' && (
|
||||
<div className="space-y-4">
|
||||
<div {...getRootProps()} className={`border-2 border-dashed rounded-xl p-6 text-center ${isDragActive ? 'border-primary bg-primary/5' : 'border-muted'}`}>
|
||||
<input {...getInputProps()} />
|
||||
<input ref={cameraInputRef} type="file" accept="video/*" capture="environment" onChange={handleCameraCapture} className="hidden" />
|
||||
|
||||
<div className="flex gap-2 justify-center mb-6">
|
||||
<Button onClick={() => cameraInputRef.current?.click()} variant="outline" size="lg" className="h-12">
|
||||
<Camera className="mr-2 h-5 w-5" /> Capturer
|
||||
</Button>
|
||||
<Button onClick={open} variant="outline" size="lg" className="h-12">
|
||||
<Upload className="mr-2 h-5 w-5" /> Galerie
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{selectedVideo ? (
|
||||
<div className="relative rounded-lg overflow-hidden aspect-[9/16] bg-black max-h-[50vh] mx-auto shadow-lg">
|
||||
<video src={selectedVideo.originalUrl} className="w-full h-full object-contain" controls />
|
||||
<div className="absolute top-2 right-2 bg-primary text-white text-xs px-2 py-1 rounded-full flex items-center shadow-sm">
|
||||
<Check className="w-3 h-3 mr-1" /> Sélectionné
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-2 gap-3 mt-4">
|
||||
{videoList.map((media) => (
|
||||
<div key={media.id} onClick={() => setSelectedVideoId(media.id) || setSelectedVideo(media)} className={`relative aspect-[9/16] bg-black rounded-lg overflow-hidden border-2 transition-all ${selectedVideoId === media.id ? 'border-primary' : 'border-transparent'}`}>
|
||||
<video src={media.originalUrl} className="w-full h-full object-cover" muted />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{selectedVideo && (
|
||||
<Button className="w-full h-12 text-lg sticky bottom-4 shadow-xl" onClick={goNext}>
|
||||
Suivant <ChevronRight className="ml-2 w-5 h-5" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* MUSIC STEP */}
|
||||
{currentStep === 'music' && (
|
||||
<div className="space-y-4">
|
||||
<audio ref={audioRef} onEnded={() => setIsPlaying(null)} />
|
||||
|
||||
{selectedVideo && (
|
||||
<div className="aspect-video bg-black rounded-lg overflow-hidden h-32 mx-auto">
|
||||
<video src={selectedVideo.originalUrl} className="w-full h-full object-cover opacity-50" muted />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-2">
|
||||
{musicData?.tracks?.map((track) => (
|
||||
<div key={track.id} onClick={() => setSelectedTrack(track)} className={`flex items-center gap-3 p-3 rounded-xl border ${selectedTrack?.id === track.id ? 'border-primary bg-primary/5' : 'border-border bg-card'}`}>
|
||||
<Button size="icon" variant="ghost" className="h-10 w-10 rounded-full bg-primary/10" onClick={(e) => { e.stopPropagation(); togglePlayPreview(track); }}>
|
||||
{isPlaying === track.id ? <Pause className="h-5 w-5" /> : <Play className="h-5 w-5 ml-0.5" />}
|
||||
</Button>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="font-medium truncate">{track.title}</p>
|
||||
<p className="text-xs text-muted-foreground truncate">{track.artist}</p>
|
||||
</div>
|
||||
{selectedTrack?.id === track.id && <Check className="h-5 w-5 text-primary" />}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{selectedTrack && (
|
||||
<div className="bg-card p-4 rounded-xl border mt-4">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<Volume2 className="w-4 h-4 text-primary" />
|
||||
<span className="text-sm font-medium">Volume: {musicVolume[0]}%</span>
|
||||
</div>
|
||||
<Slider value={musicVolume} onValueChange={setMusicVolume} max={100} step={5} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex gap-3 sticky bottom-4">
|
||||
<Button variant="outline" onClick={goBack} className="flex-1 h-12">Retour</Button>
|
||||
<Button onClick={goNext} className="flex-1 h-12">Suivant</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* TEXT STEP */}
|
||||
{currentStep === 'text' && (
|
||||
<div className="space-y-6">
|
||||
<Card>
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-base flex items-center"><Sparkles className="w-4 h-4 mr-2 text-primary" /> Assistant IA</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Textarea
|
||||
placeholder="Décrivez le produit..."
|
||||
value={productInfo}
|
||||
onChange={(e) => setProductInfo(e.target.value)}
|
||||
className="mb-3"
|
||||
/>
|
||||
<Button onClick={() => generateTextMutation.mutate(productInfo)} disabled={generateTextMutation.isPending} className="w-full" variant="secondary">
|
||||
{generateTextMutation.isPending ? <Loader2 className="animate-spin mr-2" /> : <Sparkles className="mr-2 w-4 h-4" />}
|
||||
Générer
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{generatedVariants.length > 0 && (
|
||||
<div className="space-y-3">
|
||||
{generatedVariants.map((v, i) => (
|
||||
<div key={i} className="bg-card p-3 rounded-lg border text-sm" onClick={() => { setOverlayText(v.text); toast({ title: "Texte appliqué" }); }}>
|
||||
{v.text}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">Texte Overlay</label>
|
||||
<Textarea
|
||||
value={overlayText}
|
||||
onChange={(e) => setOverlayText(e.target.value)}
|
||||
placeholder="Texte sur la vidéo..."
|
||||
className="text-lg"
|
||||
rows={3}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-3 sticky bottom-4">
|
||||
<Button variant="outline" onClick={goBack} className="flex-1 h-12">Retour</Button>
|
||||
<Button onClick={goNext} className="flex-1 h-12">Suivant</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* PUBLISH STEP */}
|
||||
{currentStep === 'publish' && (
|
||||
<div className="space-y-6">
|
||||
<Card>
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-base">Diffusion</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
{pages.filter(p => p.platform === 'facebook').map((page) => (
|
||||
<div key={page.id} className="flex items-center space-x-3 bg-secondary/20 p-3 rounded-lg">
|
||||
<Checkbox
|
||||
id={page.id}
|
||||
checked={selectedPages.includes(page.id)}
|
||||
onCheckedChange={(checked) => {
|
||||
if (checked) setSelectedPages([...selectedPages, page.id]);
|
||||
else setSelectedPages(selectedPages.filter(id => id !== page.id));
|
||||
}}
|
||||
/>
|
||||
<label htmlFor={page.id} className="font-medium flex-1">{page.pageName}</label>
|
||||
</div>
|
||||
))}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-base flex items-center"><Calendar className="mr-2 w-4 h-4" /> Planification</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<DateTimePicker value={scheduledDate} onChange={setScheduledDate} placeholder="Maintenant" />
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<div className="flex gap-3 sticky bottom-4 pb-4 bg-background">
|
||||
<Button variant="outline" onClick={goBack} className="flex-1 h-12">Retour</Button>
|
||||
<Button
|
||||
onClick={handleCreateReel}
|
||||
disabled={createReelMutation.isPending}
|
||||
className="flex-[2] h-12 bg-primary text-primary-foreground font-bold shadow-lg"
|
||||
>
|
||||
{createReelMutation.isPending ? <Loader2 className="animate-spin mr-2" /> : <Send className="mr-2 w-5 h-5" />}
|
||||
{scheduledDate ? 'Programmer' : 'Publier'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user