Files
roboco/panel/src/components/dashboard/social-summary-card.tsx
T
f0b6390189 feat: Social page — aggregated post queues + X/video history (#345)
* feat(api): x/video post history endpoints

Approved or rejected drafts vanished from both queues permanently --
the listers exclude terminal statuses and no history surface existed,
so a posted tweet or video was only findable in the raw task list.
GET /x/posts/history and GET /video/posts/history (CEO-gated, bounded)
return acted-on drafts newest-first with the posted platform ids and
reject reasons from the draft markers. Route tests assert by identity,
not emptiness: approve/reject commits the whole session, so prior
tests' rows legitimately persist in the shared test DB.

* feat(panel): Social page aggregating post queues and history

New dashboard page composing the X and video post queues with one
unified history section beneath them -- both platforms interleaved
newest-first, kind and outcome badges, posted X ids linking to the
live tweet, reject reasons shown. The command center's two full queue
cards become a compact pending-counts card linking to the page, so the
queues have one home instead of duplicated surfaces.

---------

Co-authored-by: Renn F <rennf93@users.noreply.github.com>
2026-07-09 00:44:51 +02:00

68 lines
2.1 KiB
TypeScript

"use client";
import Link from "next/link";
import { useQuery } from "@tanstack/react-query";
import { xApi, videoApi } from "@/lib/api";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { Share2 } from "lucide-react";
// Compact stand-in for the full X/video queues on the command center — the
// full queues (plus the unified history) moved to /social to avoid a
// duplicated surface. Reuses the same queries the queues themselves run
// (["x","posts"] / ["video","posts"]), so react-query dedups the fetch once
// /social's own queues are also mounted.
export function SocialSummaryCard({ className }: { className?: string }) {
const { data: xPosts } = useQuery({
queryKey: ["x", "posts"],
queryFn: () => xApi.listPosts(),
refetchInterval: 30000,
});
const { data: videoPosts } = useQuery({
queryKey: ["video", "posts"],
queryFn: () => videoApi.listPosts(),
refetchInterval: 30000,
});
const xCount = xPosts?.length ?? 0;
const videoCount = videoPosts?.length ?? 0;
const total = xCount + videoCount;
return (
<Card className={className}>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Share2 className="h-5 w-5" />
Social
{total > 0 && <Badge variant="secondary">{total}</Badge>}
</CardTitle>
<CardDescription>
Held X and video drafts awaiting your approval.
</CardDescription>
</CardHeader>
<CardContent className="flex flex-wrap items-center justify-between gap-3">
<div className="flex gap-4 text-sm text-muted-foreground">
<span>
{xCount} X draft{xCount === 1 ? "" : "s"}
</span>
<span>
{videoCount} video draft{videoCount === 1 ? "" : "s"}
</span>
</div>
<Link href="/social" prefetch={false}>
<Button variant="outline" size="sm">
Open Social
</Button>
</Link>
</CardContent>
</Card>
);
}