feat(reels): async upload + status indicators

This commit is contained in:
Michael committed 2026-02-10 12:28:36 +01:00
1 parent 4b5ab1ca16
commit 11fac558c3
3 files changed
+258 -173

No files matched your search

+32 -23
View File
@@ -1,6 +1,6 @@
import { useState } from "react";
import { useQuery } from "@tanstack/react-query";
import { Clock, CheckCircle, XCircle, Calendar, History as HistoryIcon, Eye, Image as ImageIcon, Smartphone } from "lucide-react";
import { Clock, CheckCircle, XCircle, Calendar, History as HistoryIcon, Eye, Image as ImageIcon, Smartphone, Loader2 } from "lucide-react";
import Sidebar from "@/components/sidebar";
import TopBar from "@/components/topbar";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
@@ -25,6 +25,7 @@ export default function History() {
const { data: scheduledPosts = [], isLoading } = useQuery<ScheduledPostWithRelations[]>({
queryKey: ['/api/scheduled-posts'],
refetchInterval: 3000,
});
// Filter attempted posts (scheduled in the past)
@@ -38,21 +39,21 @@ export default function History() {
const response = await fetch(`/api/posts/${scheduledPost.postId}`, {
credentials: "include",
});
if (!response.ok) {
throw new Error("Failed to fetch post");
}
const postWithMedia = await response.json();
const mediaIds = postWithMedia.media?.map((m: Media) => m.id) || [];
const allMedia = postWithMedia.media || [];
setPreviewData({
postText: postWithMedia.post.content || '',
mediaIds: mediaIds,
allMedia: allMedia,
});
setPreviewModalOpen(true);
} catch (error) {
toast({
@@ -82,12 +83,12 @@ export default function History() {
return (
<div className="flex h-screen overflow-hidden bg-background">
{sidebarOpen && (
<div
<div
className="fixed inset-0 bg-black/50 z-40 lg:hidden"
onClick={() => setSidebarOpen(false)}
/>
)}
<div className={`
fixed lg:static inset-y-0 left-0 z-50 transform transition-transform duration-300 ease-in-out
${sidebarOpen ? 'translate-x-0' : '-translate-x-full lg:translate-x-0'}
@@ -97,7 +98,7 @@ export default function History() {
<main className="flex-1 overflow-y-auto">
<TopBar onMenuClick={() => setSidebarOpen(!sidebarOpen)} />
<div className="p-4 sm:p-6 lg:p-8 max-w-5xl mx-auto">
<div className="mb-8">
<h1 className="text-3xl font-bold text-foreground">Historique</h1>
@@ -129,19 +130,21 @@ export default function History() {
{publishedPosts.map((scheduledPost) => {
const isPending = !scheduledPost.publishedAt;
const hasError = !!scheduledPost.error;
return (
<Card key={scheduledPost.id} className="rounded-2xl border-border/50 shadow-lg overflow-hidden" data-testid={`card-post-${scheduledPost.id}`}>
<CardHeader className="border-b border-border/50 bg-gradient-to-r from-muted/20 to-muted/10 p-6">
<div className="flex items-start justify-between">
<div className="flex-1">
<div className="flex items-center gap-3 mb-3">
<div className={`w-10 h-10 rounded-xl flex items-center justify-center shadow-md ${
hasError ? 'bg-gradient-to-br from-destructive to-destructive/80' :
'bg-gradient-to-br from-green-500 to-green-600'
}`}>
<div className={`w-10 h-10 rounded-xl flex items-center justify-center shadow-md ${hasError ? 'bg-gradient-to-br from-destructive to-destructive/80' :
isPending ? 'bg-gradient-to-br from-blue-500 to-blue-600' :
'bg-gradient-to-br from-green-500 to-green-600'
}`}>
{hasError ? (
<XCircle className="w-5 h-5 text-white" />
) : isPending ? (
<Loader2 className="w-5 h-5 text-white animate-spin" />
) : (
<CheckCircle className="w-5 h-5 text-white fill-white" />
)}
@@ -165,18 +168,24 @@ export default function History() {
>
<Eye className="w-4 h-4" />
</Button>
<Badge
className={`${
hasError
? 'bg-destructive/20 text-destructive hover:bg-destructive/30'
: 'bg-success/20 text-success hover:bg-success/30'
}`}
<Badge
className={`${hasError
? 'bg-destructive/20 text-destructive hover:bg-destructive/30'
: isPending
? 'bg-blue-500/20 text-blue-500 hover:bg-blue-500/30'
: 'bg-success/20 text-success hover:bg-success/30'
}`}
>
{hasError ? (
<>
<XCircle className="w-3 h-3 mr-1" />
Échoué
</>
) : isPending ? (
<>
<Loader2 className="w-3 h-3 mr-1 animate-spin" />
Traitement...
</>
) : (
<>
<CheckCircle className="w-3 h-3 mr-1" />
@@ -192,14 +201,14 @@ export default function History() {
<div className="flex items-center gap-2">
{getPostTypeIcon(scheduledPost.postType)}
<span className="font-medium capitalize">
{scheduledPost.postType === 'feed' ? 'Feed' :
scheduledPost.postType === 'story' ? 'Story' : 'Feed & Story'}
{scheduledPost.postType === 'feed' ? 'Feed' :
scheduledPost.postType === 'story' ? 'Story' : 'Feed & Story'}
</span>
</div>
<div className="flex items-center gap-2">
<Calendar className="w-4 h-4" />
<span className="font-medium">
{scheduledPost.scheduledAt
{scheduledPost.scheduledAt
? format(new Date(scheduledPost.scheduledAt), "d MMM yyyy 'à' HH:mm", { locale: fr })
: 'Date inconnue'
}
@@ -226,7 +235,7 @@ export default function History() {
postText={previewData.postText}
selectedMedia={previewData.mediaIds}
mediaList={previewData.allMedia}
onPublish={() => {}}
onPublish={() => { }}
isPublishing={false}
readOnly={true}
/>
+7 -3
View File
@@ -3,7 +3,7 @@ import { format } from "date-fns";
import { fr } from "date-fns/locale";
import {
Calendar, CheckCircle, XCircle, Clock,
MoreHorizontal, Filter, Search
MoreHorizontal, Filter, Search, Loader2
} from "lucide-react";
import { useState } from "react";
import Sidebar from "@/components/sidebar";
@@ -22,9 +22,10 @@ import {
export default function MobileHistory() {
const [searchTerm, setSearchTerm] = useState("");
// Fetch posts history
// Fetch posts history with polling to see status updates
const { data: posts } = useQuery<any[]>({
queryKey: ['/api/posts'],
refetchInterval: 3000, // Poll every 3 seconds
});
const getStatusIcon = (status: string) => {
@@ -32,6 +33,7 @@ export default function MobileHistory() {
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" />;
case 'draft': return <Loader2 className="w-5 h-5 text-blue-500 animate-spin" />;
default: return <Clock className="w-5 h-5 text-muted-foreground" />;
}
};
@@ -41,6 +43,7 @@ export default function MobileHistory() {
case 'published': return "Publié";
case 'failed': return "Échec";
case 'scheduled': return "Programmé";
case 'draft': return "Traitement...";
default: return "Brouillon";
}
};
@@ -96,7 +99,8 @@ export default function MobileHistory() {
<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'
post.status === 'failed' ? 'bg-red-500' :
post.status === 'draft' ? 'bg-blue-500' : 'bg-amber-500'
}`} />
<div className="flex-1 p-3">