mirror of
https://github.com/R0m1k3/Socialflow.git
synced 2026-10-23 15:08:13 +02:00
- Suppression de la page « Générateur Vidéo » (/remotion) : routes /api/remotion, pipeline de rendu d'images, composition ImageVideo et aperçu associé. Le générateur de Reels est conservé. - Plus de badge « New » à côté des menus. - Tableau de bord : bouton pour supprimer un Reel en échec et sa notification. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017tFkVXQDATYfw5aFtjQahi
162 lines
8.0 KiB
TypeScript
162 lines
8.0 KiB
TypeScript
import { useMutation, useQuery } from "@tanstack/react-query";
|
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|
import { Progress } from "@/components/ui/progress";
|
|
import { Clapperboard, Loader2, CheckCircle2, XCircle, X } from "lucide-react";
|
|
import { Button } from "@/components/ui/button";
|
|
import { useToast } from "@/hooks/use-toast";
|
|
import { apiRequest, queryClient, getErrorMessage } from "@/lib/queryClient";
|
|
import type { Post } from "@shared/schema";
|
|
|
|
type OngoingReel = Post & { generationStep?: string | null };
|
|
|
|
/** Libellé de l'étape en cours, telle que rapportée par la file de rendu. */
|
|
const STEP_LABELS: Record<string, string> = {
|
|
prepare: "Préparation…",
|
|
voice: "Voix et préparation de la vidéo…",
|
|
render: "Montage : sous-titres, logo, effets…",
|
|
store: "Enregistrement dans la médiathèque…",
|
|
publish: "Publication…",
|
|
};
|
|
|
|
function getStageLabel(post: OngoingReel): string {
|
|
if (post.generationStatus === "pending") return "En attente : un autre Reel est en cours…";
|
|
return (post.generationStep && STEP_LABELS[post.generationStep]) || "Démarrage du traitement…";
|
|
}
|
|
|
|
interface OngoingReelsProps {
|
|
/** Compact layout for mobile */
|
|
compact?: boolean;
|
|
}
|
|
|
|
export default function OngoingReels({ compact = false }: OngoingReelsProps) {
|
|
const { data: ongoingPosts = [] } = useQuery<OngoingReel[]>({
|
|
queryKey: ["/api/reels/ongoing"],
|
|
// Cette route ne renvoie que les générations en cours : tant qu'elle est
|
|
// vide il n'y a pas de barre de progression à animer. On garde une veille
|
|
// lente, qui suffit à détecter une génération lancée depuis un autre écran.
|
|
refetchInterval: (query) =>
|
|
(query.state.data ?? []).some((p) => p.generationStatus !== "failed") ? 3000 : 30000,
|
|
});
|
|
|
|
const { toast } = useToast();
|
|
|
|
// Un Reel en échec n'a rien à publier : le supprimer retire aussi la notification
|
|
const dismissMutation = useMutation({
|
|
mutationFn: async (postId: string) => (await apiRequest("DELETE", `/api/reels/${postId}`)).json(),
|
|
onSuccess: (_data, postId) => {
|
|
queryClient.setQueryData<OngoingReel[]>(["/api/reels/ongoing"], (posts) =>
|
|
posts?.filter((p) => p.id !== postId),
|
|
);
|
|
queryClient.invalidateQueries({ queryKey: ["/api/reels/ongoing"] });
|
|
queryClient.invalidateQueries({ queryKey: ["/api/scheduled-posts"] });
|
|
},
|
|
onError: (error: unknown) => {
|
|
toast({
|
|
title: "Erreur",
|
|
description: getErrorMessage(error, "Impossible de supprimer la notification"),
|
|
variant: "destructive",
|
|
});
|
|
},
|
|
});
|
|
|
|
// Nothing to show — render nothing (not even a card)
|
|
if (ongoingPosts.length === 0) {
|
|
return null;
|
|
}
|
|
|
|
return (
|
|
<Card
|
|
className={`rounded-2xl border-border/50 shadow-lg ${compact ? "" : ""
|
|
}`}
|
|
data-testid="card-ongoing-reels"
|
|
>
|
|
<CardHeader className={compact ? "pb-2 px-4 pt-4" : ""}>
|
|
<CardTitle className={`flex items-center gap-2 ${compact ? "text-base" : ""}`}>
|
|
{ongoingPosts.some((p) => p.generationStatus !== "failed") ? (
|
|
<Loader2 className="w-5 h-5 animate-spin text-primary" />
|
|
) : (
|
|
<XCircle className="w-5 h-5 text-destructive" />
|
|
)}
|
|
Reels en cours
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent className={compact ? "px-4 pb-4 pt-0" : ""}>
|
|
<div className={`space-y-${compact ? "3" : "4"}`}>
|
|
{ongoingPosts.map((post) => {
|
|
const progress = post.generationProgress ?? 0;
|
|
const isFailed = post.generationStatus === "failed";
|
|
|
|
return (
|
|
<div
|
|
key={post.id}
|
|
className={`flex items-start gap-3 p-${compact ? "3" : "4"} rounded-lg border border-border/50 bg-muted/30`}
|
|
data-testid={`ongoing-reel-${post.id}`}
|
|
>
|
|
{/* Icon */}
|
|
<div className="w-9 h-9 rounded-lg bg-gradient-to-br from-violet-500 to-fuchsia-500 flex items-center justify-center flex-shrink-0">
|
|
<Clapperboard className="w-4 h-4 text-white" />
|
|
</div>
|
|
|
|
{/* Content */}
|
|
<div className="flex-1 min-w-0">
|
|
<p className={`font-medium truncate ${compact ? "text-sm" : "text-sm"}`}>
|
|
{post.content || "Reel sans texte"}
|
|
</p>
|
|
|
|
{isFailed ? (
|
|
<div className="flex items-center gap-1.5 mt-2 text-destructive">
|
|
<XCircle className="w-4 h-4" />
|
|
<span className="text-xs break-words">
|
|
Échec : {post.generationError || "erreur lors du traitement"}
|
|
</span>
|
|
</div>
|
|
) : (
|
|
<>
|
|
<div className="mt-2">
|
|
<Progress
|
|
value={progress}
|
|
className="h-2"
|
|
/>
|
|
</div>
|
|
<div className="flex items-center justify-between mt-1.5">
|
|
<span className="text-xs text-muted-foreground">
|
|
{getStageLabel(post)}
|
|
</span>
|
|
<span className="text-xs font-medium text-primary">
|
|
{progress}%
|
|
</span>
|
|
</div>
|
|
</>
|
|
)}
|
|
|
|
{progress >= 100 && !isFailed && (
|
|
<div className="flex items-center gap-1.5 mt-2 text-green-500">
|
|
<CheckCircle2 className="w-4 h-4" />
|
|
<span className="text-xs font-medium">Terminé</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{isFailed && (
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
className="h-7 w-7 flex-shrink-0 text-muted-foreground hover:text-destructive"
|
|
onClick={() => dismissMutation.mutate(post.id)}
|
|
disabled={dismissMutation.isPending && dismissMutation.variables === post.id}
|
|
title="Supprimer"
|
|
aria-label="Supprimer ce Reel en échec"
|
|
data-testid={`button-dismiss-reel-${post.id}`}
|
|
>
|
|
<X className="w-4 h-4" />
|
|
</Button>
|
|
)}
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
}
|