ubiquitous-invention/apps/web/components/views/overview/overview-view.tsx

126 lines
4.2 KiB
TypeScript
Raw Normal View History

"use client";
import { useMemo } from "react";
import { Badge } from "@/components/ui/badge";
import { api } from "@/lib/trpc";
export interface OverviewViewProps {
workspaceId?: string;
spaceId?: string;
}
function formatUpdatedAt(value: Date | string): string {
const date = value instanceof Date ? value : new Date(value);
if (Number.isNaN(date.getTime())) return String(value);
return date.toLocaleString();
}
export function OverviewView({ workspaceId, spaceId }: OverviewViewProps) {
const spaceQuery = api.objects.getById.useQuery(
{ id: spaceId! },
{ enabled: Boolean(spaceId) },
);
const childrenQuery = api.objects.list.useQuery(
{
workspaceId: workspaceId!,
parentId: spaceId ?? undefined,
limit: 200,
},
{ enabled: Boolean(workspaceId) },
);
const statusCounts = useMemo(() => {
const counts = { open: 0, in_progress: 0, done: 0 };
for (const row of childrenQuery.data?.objects ?? []) {
const s = row.status;
if (s === "in_progress") counts.in_progress += 1;
else if (s === "done") counts.done += 1;
else counts.open += 1;
}
return counts;
}, [childrenQuery.data?.objects]);
const recentChildren = useMemo(() => {
const rows = childrenQuery.data?.objects ?? [];
return [...rows]
.sort((a, b) => {
const ta = new Date(a.updatedAt).getTime();
const tb = new Date(b.updatedAt).getTime();
return tb - ta;
})
.slice(0, 10);
}, [childrenQuery.data?.objects]);
const isLoading = spaceQuery.isLoading || childrenQuery.isLoading;
const children = childrenQuery.data?.objects ?? [];
const hasChildren = children.length > 0;
const space = spaceQuery.data as { title: string } | undefined;
return (
<div className="h-full overflow-auto p-6">
{spaceQuery.isLoading ? (
<div className="text-sm text-muted-foreground">Loading space</div>
) : space ? (
<h1 className="text-2xl font-semibold tracking-tight">{space.title}</h1>
) : null}
<div className="mt-8 space-y-8">
<section>
<h2 className="text-sm font-semibold">By status</h2>
<div className="mt-3 grid grid-cols-3 gap-4">
{(
[
{ key: "open" as const, label: "Open" },
{ key: "in_progress" as const, label: "In progress" },
{ key: "done" as const, label: "Done" },
] as const
).map(({ key, label }) => (
<div
key={key}
className="rounded-lg border border-border bg-card p-4 shadow-sm"
>
<div className="text-sm text-muted-foreground">{label}</div>
<div className="mt-1 text-2xl font-semibold tabular-nums">
{isLoading ? "—" : statusCounts[key]}
</div>
</div>
))}
</div>
</section>
<section>
<h2 className="text-sm font-semibold">Recent activity</h2>
{childrenQuery.isLoading ? (
<p className="mt-3 text-sm text-muted-foreground">Loading</p>
) : !hasChildren ? (
<div className="mt-4 rounded-lg border border-dashed border-border bg-muted/30 p-10 text-center text-sm text-muted-foreground">
No items in this space yet.
</div>
) : (
<ul className="mt-3 divide-y divide-border rounded-lg border border-border bg-card shadow-sm">
{recentChildren.map((obj) => (
<li
key={obj.id}
className="flex items-center justify-between gap-3 px-4 py-3"
>
<span className="min-w-0 truncate font-medium">
{obj.title}
</span>
<div className="flex shrink-0 items-center gap-2">
<Badge variant="outline">{obj.type}</Badge>
<span className="text-xs text-muted-foreground tabular-nums">
{formatUpdatedAt(obj.updatedAt)}
</span>
</div>
</li>
))}
</ul>
)}
</section>
</div>
</div>
);
}