ubiquitous-invention/apps/web/app/(app)/[workspaceSlug]/page.tsx

314 lines
10 KiB
TypeScript
Raw Normal View History

"use client";
feat(web): wire workspace-home dashboard to real tRPC queries Path-A task 2/5. Replaces the hardcoded `stats` (24/8/12) and hardcoded `recent` list on the workspace-home page with real workspace-scoped data. * server/routers/objects.ts: add two new procedures. - `objects.stats` returns { openTasks, containers }. Open-task count treats null status as open; only `done` and `closed` (per packages/shared object-statuses) are terminal. Container count aggregates project + space + group rows. - `objects.listRecent({ limit })` returns the N most-recently-updated rows, descending by updated_at. Excludes archived and excludes `workspace`/`group` from the activity feed (containers clutter "what did I just touch" recency). Both go through workspaceProcedure, so the workspace_id filter comes from the middleware-resolved ctx.workspace.id rather than any user input. * app/(app)/[workspaceSlug]/page.tsx: rewrite to consume the new procedures via @trpc/react-query. Adds: - Skeleton loading state (no flash of zeros). - Empty state with a "New task" CTA on workspaces with no objects. - Real "X ago" labels on the recent feed. - Click-through links from recent rows to /{slug}/{id}. - A locally-mounted CreateObjectDialog instance independent of the global one in AppShell so the empty-state CTA can pre-seed defaultType="task" without coordinating shared state. * components/ui/skeleton.tsx: new (standard shadcn pulse skeleton). Used by the dashboard but reusable across the app. The scaffolded "Due this week" stat is dropped: `objects` has no due_at column and the task explicitly preferred dropping a card to schema-creep. `pnpm lint && pnpm type-check` clean. Closes plans/Plan-daily-driver-finish/Epic-shipping-the-shell/ Task-wire-workspace-home-dashboard.md. Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-02 01:17:29 -04:00
import { useState } from "react";
import Link from "next/link";
import { useParams } from "next/navigation";
import {
ArrowRight,
CheckCircle2,
CircleDashed,
feat(web): wire workspace-home dashboard to real tRPC queries Path-A task 2/5. Replaces the hardcoded `stats` (24/8/12) and hardcoded `recent` list on the workspace-home page with real workspace-scoped data. * server/routers/objects.ts: add two new procedures. - `objects.stats` returns { openTasks, containers }. Open-task count treats null status as open; only `done` and `closed` (per packages/shared object-statuses) are terminal. Container count aggregates project + space + group rows. - `objects.listRecent({ limit })` returns the N most-recently-updated rows, descending by updated_at. Excludes archived and excludes `workspace`/`group` from the activity feed (containers clutter "what did I just touch" recency). Both go through workspaceProcedure, so the workspace_id filter comes from the middleware-resolved ctx.workspace.id rather than any user input. * app/(app)/[workspaceSlug]/page.tsx: rewrite to consume the new procedures via @trpc/react-query. Adds: - Skeleton loading state (no flash of zeros). - Empty state with a "New task" CTA on workspaces with no objects. - Real "X ago" labels on the recent feed. - Click-through links from recent rows to /{slug}/{id}. - A locally-mounted CreateObjectDialog instance independent of the global one in AppShell so the empty-state CTA can pre-seed defaultType="task" without coordinating shared state. * components/ui/skeleton.tsx: new (standard shadcn pulse skeleton). Used by the dashboard but reusable across the app. The scaffolded "Due this week" stat is dropped: `objects` has no due_at column and the task explicitly preferred dropping a card to schema-creep. `pnpm lint && pnpm type-check` clean. Closes plans/Plan-daily-driver-finish/Epic-shipping-the-shell/ Task-wire-workspace-home-dashboard.md. Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-02 01:17:29 -04:00
ClipboardList,
FileText,
FolderKanban,
LayoutDashboard,
feat(web): wire workspace-home dashboard to real tRPC queries Path-A task 2/5. Replaces the hardcoded `stats` (24/8/12) and hardcoded `recent` list on the workspace-home page with real workspace-scoped data. * server/routers/objects.ts: add two new procedures. - `objects.stats` returns { openTasks, containers }. Open-task count treats null status as open; only `done` and `closed` (per packages/shared object-statuses) are terminal. Container count aggregates project + space + group rows. - `objects.listRecent({ limit })` returns the N most-recently-updated rows, descending by updated_at. Excludes archived and excludes `workspace`/`group` from the activity feed (containers clutter "what did I just touch" recency). Both go through workspaceProcedure, so the workspace_id filter comes from the middleware-resolved ctx.workspace.id rather than any user input. * app/(app)/[workspaceSlug]/page.tsx: rewrite to consume the new procedures via @trpc/react-query. Adds: - Skeleton loading state (no flash of zeros). - Empty state with a "New task" CTA on workspaces with no objects. - Real "X ago" labels on the recent feed. - Click-through links from recent rows to /{slug}/{id}. - A locally-mounted CreateObjectDialog instance independent of the global one in AppShell so the empty-state CTA can pre-seed defaultType="task" without coordinating shared state. * components/ui/skeleton.tsx: new (standard shadcn pulse skeleton). Used by the dashboard but reusable across the app. The scaffolded "Due this week" stat is dropped: `objects` has no due_at column and the task explicitly preferred dropping a card to schema-creep. `pnpm lint && pnpm type-check` clean. Closes plans/Plan-daily-driver-finish/Epic-shipping-the-shell/ Task-wire-workspace-home-dashboard.md. Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-02 01:17:29 -04:00
LayoutGrid,
Plus,
Presentation,
Sparkles,
feat(web): wire workspace-home dashboard to real tRPC queries Path-A task 2/5. Replaces the hardcoded `stats` (24/8/12) and hardcoded `recent` list on the workspace-home page with real workspace-scoped data. * server/routers/objects.ts: add two new procedures. - `objects.stats` returns { openTasks, containers }. Open-task count treats null status as open; only `done` and `closed` (per packages/shared object-statuses) are terminal. Container count aggregates project + space + group rows. - `objects.listRecent({ limit })` returns the N most-recently-updated rows, descending by updated_at. Excludes archived and excludes `workspace`/`group` from the activity feed (containers clutter "what did I just touch" recency). Both go through workspaceProcedure, so the workspace_id filter comes from the middleware-resolved ctx.workspace.id rather than any user input. * app/(app)/[workspaceSlug]/page.tsx: rewrite to consume the new procedures via @trpc/react-query. Adds: - Skeleton loading state (no flash of zeros). - Empty state with a "New task" CTA on workspaces with no objects. - Real "X ago" labels on the recent feed. - Click-through links from recent rows to /{slug}/{id}. - A locally-mounted CreateObjectDialog instance independent of the global one in AppShell so the empty-state CTA can pre-seed defaultType="task" without coordinating shared state. * components/ui/skeleton.tsx: new (standard shadcn pulse skeleton). Used by the dashboard but reusable across the app. The scaffolded "Due this week" stat is dropped: `objects` has no due_at column and the task explicitly preferred dropping a card to schema-creep. `pnpm lint && pnpm type-check` clean. Closes plans/Plan-daily-driver-finish/Epic-shipping-the-shell/ Task-wire-workspace-home-dashboard.md. Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-02 01:17:29 -04:00
type LucideIcon,
} from "lucide-react";
feat(web): wire workspace-home dashboard to real tRPC queries Path-A task 2/5. Replaces the hardcoded `stats` (24/8/12) and hardcoded `recent` list on the workspace-home page with real workspace-scoped data. * server/routers/objects.ts: add two new procedures. - `objects.stats` returns { openTasks, containers }. Open-task count treats null status as open; only `done` and `closed` (per packages/shared object-statuses) are terminal. Container count aggregates project + space + group rows. - `objects.listRecent({ limit })` returns the N most-recently-updated rows, descending by updated_at. Excludes archived and excludes `workspace`/`group` from the activity feed (containers clutter "what did I just touch" recency). Both go through workspaceProcedure, so the workspace_id filter comes from the middleware-resolved ctx.workspace.id rather than any user input. * app/(app)/[workspaceSlug]/page.tsx: rewrite to consume the new procedures via @trpc/react-query. Adds: - Skeleton loading state (no flash of zeros). - Empty state with a "New task" CTA on workspaces with no objects. - Real "X ago" labels on the recent feed. - Click-through links from recent rows to /{slug}/{id}. - A locally-mounted CreateObjectDialog instance independent of the global one in AppShell so the empty-state CTA can pre-seed defaultType="task" without coordinating shared state. * components/ui/skeleton.tsx: new (standard shadcn pulse skeleton). Used by the dashboard but reusable across the app. The scaffolded "Due this week" stat is dropped: `objects` has no due_at column and the task explicitly preferred dropping a card to schema-creep. `pnpm lint && pnpm type-check` clean. Closes plans/Plan-daily-driver-finish/Epic-shipping-the-shell/ Task-wire-workspace-home-dashboard.md. Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-02 01:17:29 -04:00
import { CreateObjectDialog } from "@/components/objects";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Separator } from "@/components/ui/separator";
feat(web): wire workspace-home dashboard to real tRPC queries Path-A task 2/5. Replaces the hardcoded `stats` (24/8/12) and hardcoded `recent` list on the workspace-home page with real workspace-scoped data. * server/routers/objects.ts: add two new procedures. - `objects.stats` returns { openTasks, containers }. Open-task count treats null status as open; only `done` and `closed` (per packages/shared object-statuses) are terminal. Container count aggregates project + space + group rows. - `objects.listRecent({ limit })` returns the N most-recently-updated rows, descending by updated_at. Excludes archived and excludes `workspace`/`group` from the activity feed (containers clutter "what did I just touch" recency). Both go through workspaceProcedure, so the workspace_id filter comes from the middleware-resolved ctx.workspace.id rather than any user input. * app/(app)/[workspaceSlug]/page.tsx: rewrite to consume the new procedures via @trpc/react-query. Adds: - Skeleton loading state (no flash of zeros). - Empty state with a "New task" CTA on workspaces with no objects. - Real "X ago" labels on the recent feed. - Click-through links from recent rows to /{slug}/{id}. - A locally-mounted CreateObjectDialog instance independent of the global one in AppShell so the empty-state CTA can pre-seed defaultType="task" without coordinating shared state. * components/ui/skeleton.tsx: new (standard shadcn pulse skeleton). Used by the dashboard but reusable across the app. The scaffolded "Due this week" stat is dropped: `objects` has no due_at column and the task explicitly preferred dropping a card to schema-creep. `pnpm lint && pnpm type-check` clean. Closes plans/Plan-daily-driver-finish/Epic-shipping-the-shell/ Task-wire-workspace-home-dashboard.md. Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-02 01:17:29 -04:00
import { Skeleton } from "@/components/ui/skeleton";
import { api } from "@/lib/trpc";
import { usePanelStore } from "@/lib/stores/panel-store";
import { useWorkspaceStore } from "@/lib/stores/workspace-store";
import { cn } from "@/lib/utils";
feat(web): wire workspace-home dashboard to real tRPC queries Path-A task 2/5. Replaces the hardcoded `stats` (24/8/12) and hardcoded `recent` list on the workspace-home page with real workspace-scoped data. * server/routers/objects.ts: add two new procedures. - `objects.stats` returns { openTasks, containers }. Open-task count treats null status as open; only `done` and `closed` (per packages/shared object-statuses) are terminal. Container count aggregates project + space + group rows. - `objects.listRecent({ limit })` returns the N most-recently-updated rows, descending by updated_at. Excludes archived and excludes `workspace`/`group` from the activity feed (containers clutter "what did I just touch" recency). Both go through workspaceProcedure, so the workspace_id filter comes from the middleware-resolved ctx.workspace.id rather than any user input. * app/(app)/[workspaceSlug]/page.tsx: rewrite to consume the new procedures via @trpc/react-query. Adds: - Skeleton loading state (no flash of zeros). - Empty state with a "New task" CTA on workspaces with no objects. - Real "X ago" labels on the recent feed. - Click-through links from recent rows to /{slug}/{id}. - A locally-mounted CreateObjectDialog instance independent of the global one in AppShell so the empty-state CTA can pre-seed defaultType="task" without coordinating shared state. * components/ui/skeleton.tsx: new (standard shadcn pulse skeleton). Used by the dashboard but reusable across the app. The scaffolded "Due this week" stat is dropped: `objects` has no due_at column and the task explicitly preferred dropping a card to schema-creep. `pnpm lint && pnpm type-check` clean. Closes plans/Plan-daily-driver-finish/Epic-shipping-the-shell/ Task-wire-workspace-home-dashboard.md. Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-02 01:17:29 -04:00
/**
* Minimal "X ago" formatter. We avoid pulling in date-fns or a Yjs-aware
* relative-time helper for a single use site; the home page just needs
* coarse-grained recency labels.
*/
function relativeTime(value: Date | string): string {
const date = value instanceof Date ? value : new Date(value);
const ms = Date.now() - date.getTime();
if (Number.isNaN(ms)) return String(value);
const secs = Math.max(1, Math.round(ms / 1000));
if (secs < 60) return `${secs}s ago`;
const mins = Math.round(secs / 60);
if (mins < 60) return `${mins}m ago`;
const hours = Math.round(mins / 60);
if (hours < 24) return `${hours}h ago`;
const days = Math.round(hours / 24);
if (days < 7) return `${days}d ago`;
return date.toLocaleDateString();
}
const TYPE_LABELS: Record<string, string> = {
task: "Task",
document: "Document",
whiteboard: "Whiteboard",
space: "Space",
project: "Project",
group: "Group",
form: "Form",
};
const TYPE_ICONS: Record<string, LucideIcon> = {
task: ClipboardList,
document: FileText,
whiteboard: Presentation,
space: LayoutGrid,
project: FolderKanban,
group: FolderKanban,
form: ClipboardList,
};
feat(web): wire workspace-home dashboard to real tRPC queries Path-A task 2/5. Replaces the hardcoded `stats` (24/8/12) and hardcoded `recent` list on the workspace-home page with real workspace-scoped data. * server/routers/objects.ts: add two new procedures. - `objects.stats` returns { openTasks, containers }. Open-task count treats null status as open; only `done` and `closed` (per packages/shared object-statuses) are terminal. Container count aggregates project + space + group rows. - `objects.listRecent({ limit })` returns the N most-recently-updated rows, descending by updated_at. Excludes archived and excludes `workspace`/`group` from the activity feed (containers clutter "what did I just touch" recency). Both go through workspaceProcedure, so the workspace_id filter comes from the middleware-resolved ctx.workspace.id rather than any user input. * app/(app)/[workspaceSlug]/page.tsx: rewrite to consume the new procedures via @trpc/react-query. Adds: - Skeleton loading state (no flash of zeros). - Empty state with a "New task" CTA on workspaces with no objects. - Real "X ago" labels on the recent feed. - Click-through links from recent rows to /{slug}/{id}. - A locally-mounted CreateObjectDialog instance independent of the global one in AppShell so the empty-state CTA can pre-seed defaultType="task" without coordinating shared state. * components/ui/skeleton.tsx: new (standard shadcn pulse skeleton). Used by the dashboard but reusable across the app. The scaffolded "Due this week" stat is dropped: `objects` has no due_at column and the task explicitly preferred dropping a card to schema-creep. `pnpm lint && pnpm type-check` clean. Closes plans/Plan-daily-driver-finish/Epic-shipping-the-shell/ Task-wire-workspace-home-dashboard.md. Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-02 01:17:29 -04:00
function isDoneStatus(status: string | null): boolean {
return status === "done" || status === "closed";
}
export default function WorkspaceHomePage() {
feat(web): wire workspace-home dashboard to real tRPC queries Path-A task 2/5. Replaces the hardcoded `stats` (24/8/12) and hardcoded `recent` list on the workspace-home page with real workspace-scoped data. * server/routers/objects.ts: add two new procedures. - `objects.stats` returns { openTasks, containers }. Open-task count treats null status as open; only `done` and `closed` (per packages/shared object-statuses) are terminal. Container count aggregates project + space + group rows. - `objects.listRecent({ limit })` returns the N most-recently-updated rows, descending by updated_at. Excludes archived and excludes `workspace`/`group` from the activity feed (containers clutter "what did I just touch" recency). Both go through workspaceProcedure, so the workspace_id filter comes from the middleware-resolved ctx.workspace.id rather than any user input. * app/(app)/[workspaceSlug]/page.tsx: rewrite to consume the new procedures via @trpc/react-query. Adds: - Skeleton loading state (no flash of zeros). - Empty state with a "New task" CTA on workspaces with no objects. - Real "X ago" labels on the recent feed. - Click-through links from recent rows to /{slug}/{id}. - A locally-mounted CreateObjectDialog instance independent of the global one in AppShell so the empty-state CTA can pre-seed defaultType="task" without coordinating shared state. * components/ui/skeleton.tsx: new (standard shadcn pulse skeleton). Used by the dashboard but reusable across the app. The scaffolded "Due this week" stat is dropped: `objects` has no due_at column and the task explicitly preferred dropping a card to schema-creep. `pnpm lint && pnpm type-check` clean. Closes plans/Plan-daily-driver-finish/Epic-shipping-the-shell/ Task-wire-workspace-home-dashboard.md. Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-02 01:17:29 -04:00
const params = useParams();
const workspaceSlug =
typeof params?.workspaceSlug === "string" ? params.workspaceSlug : undefined;
const workspace = useWorkspaceStore((s) => s.currentWorkspace);
const openPanel = usePanelStore((s) => s.open);
feat(web): wire workspace-home dashboard to real tRPC queries Path-A task 2/5. Replaces the hardcoded `stats` (24/8/12) and hardcoded `recent` list on the workspace-home page with real workspace-scoped data. * server/routers/objects.ts: add two new procedures. - `objects.stats` returns { openTasks, containers }. Open-task count treats null status as open; only `done` and `closed` (per packages/shared object-statuses) are terminal. Container count aggregates project + space + group rows. - `objects.listRecent({ limit })` returns the N most-recently-updated rows, descending by updated_at. Excludes archived and excludes `workspace`/`group` from the activity feed (containers clutter "what did I just touch" recency). Both go through workspaceProcedure, so the workspace_id filter comes from the middleware-resolved ctx.workspace.id rather than any user input. * app/(app)/[workspaceSlug]/page.tsx: rewrite to consume the new procedures via @trpc/react-query. Adds: - Skeleton loading state (no flash of zeros). - Empty state with a "New task" CTA on workspaces with no objects. - Real "X ago" labels on the recent feed. - Click-through links from recent rows to /{slug}/{id}. - A locally-mounted CreateObjectDialog instance independent of the global one in AppShell so the empty-state CTA can pre-seed defaultType="task" without coordinating shared state. * components/ui/skeleton.tsx: new (standard shadcn pulse skeleton). Used by the dashboard but reusable across the app. The scaffolded "Due this week" stat is dropped: `objects` has no due_at column and the task explicitly preferred dropping a card to schema-creep. `pnpm lint && pnpm type-check` clean. Closes plans/Plan-daily-driver-finish/Epic-shipping-the-shell/ Task-wire-workspace-home-dashboard.md. Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-02 01:17:29 -04:00
const [createOpen, setCreateOpen] = useState(false);
const [createType, setCreateType] = useState<string | undefined>(undefined);
const statsQuery = api.objects.stats.useQuery(
{ workspace: workspaceSlug! },
{ enabled: Boolean(workspaceSlug) },
);
const recentQuery = api.objects.listRecent.useQuery(
{ workspace: workspaceSlug!, limit: 5 },
{ enabled: Boolean(workspaceSlug) },
);
const name = workspace?.name ?? "Workspace";
feat(web): wire workspace-home dashboard to real tRPC queries Path-A task 2/5. Replaces the hardcoded `stats` (24/8/12) and hardcoded `recent` list on the workspace-home page with real workspace-scoped data. * server/routers/objects.ts: add two new procedures. - `objects.stats` returns { openTasks, containers }. Open-task count treats null status as open; only `done` and `closed` (per packages/shared object-statuses) are terminal. Container count aggregates project + space + group rows. - `objects.listRecent({ limit })` returns the N most-recently-updated rows, descending by updated_at. Excludes archived and excludes `workspace`/`group` from the activity feed (containers clutter "what did I just touch" recency). Both go through workspaceProcedure, so the workspace_id filter comes from the middleware-resolved ctx.workspace.id rather than any user input. * app/(app)/[workspaceSlug]/page.tsx: rewrite to consume the new procedures via @trpc/react-query. Adds: - Skeleton loading state (no flash of zeros). - Empty state with a "New task" CTA on workspaces with no objects. - Real "X ago" labels on the recent feed. - Click-through links from recent rows to /{slug}/{id}. - A locally-mounted CreateObjectDialog instance independent of the global one in AppShell so the empty-state CTA can pre-seed defaultType="task" without coordinating shared state. * components/ui/skeleton.tsx: new (standard shadcn pulse skeleton). Used by the dashboard but reusable across the app. The scaffolded "Due this week" stat is dropped: `objects` has no due_at column and the task explicitly preferred dropping a card to schema-creep. `pnpm lint && pnpm type-check` clean. Closes plans/Plan-daily-driver-finish/Epic-shipping-the-shell/ Task-wire-workspace-home-dashboard.md. Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-02 01:17:29 -04:00
const recent = recentQuery.data?.objects ?? [];
return (
<div className="flex min-h-full flex-col bg-background">
<header className="relative overflow-hidden border-b border-border px-6 py-10 sm:px-10">
<div
className={cn(
"absolute inset-0 bg-gradient-primary opacity-90",
"dark:opacity-100",
)}
aria-hidden
/>
<div className="absolute inset-0 bg-[radial-gradient(ellipse_at_top_right,_rgba(255,255,255,0.12),_transparent_55%)]" />
<div className="relative flex flex-col gap-3">
<div className="flex flex-wrap items-center gap-2 text-primary-foreground/90">
<LayoutDashboard className="size-5" />
<span className="text-xs font-semibold uppercase tracking-widest">
Home
</span>
</div>
<h1 className="text-3xl font-bold tracking-tight text-primary-foreground sm:text-4xl">
{name}
</h1>
<p className="max-w-2xl text-sm text-primary-foreground/85 sm:text-base">
Your command center for tasks, docs, and boards. Pick up where you
left off or open the assistant to plan the day.
</p>
<div className="flex flex-wrap gap-2 pt-2">
<Button
type="button"
size="sm"
variant="secondary"
className="gap-2 bg-primary-foreground/15 text-primary-foreground hover:bg-primary-foreground/25"
onClick={() => openPanel("ai-chat")}
>
<Sparkles className="size-4" />
Open AI assistant
</Button>
<Button
type="button"
size="sm"
variant="secondary"
className="gap-2 bg-primary-foreground/15 text-primary-foreground hover:bg-primary-foreground/25"
feat(web): wire workspace-home dashboard to real tRPC queries Path-A task 2/5. Replaces the hardcoded `stats` (24/8/12) and hardcoded `recent` list on the workspace-home page with real workspace-scoped data. * server/routers/objects.ts: add two new procedures. - `objects.stats` returns { openTasks, containers }. Open-task count treats null status as open; only `done` and `closed` (per packages/shared object-statuses) are terminal. Container count aggregates project + space + group rows. - `objects.listRecent({ limit })` returns the N most-recently-updated rows, descending by updated_at. Excludes archived and excludes `workspace`/`group` from the activity feed (containers clutter "what did I just touch" recency). Both go through workspaceProcedure, so the workspace_id filter comes from the middleware-resolved ctx.workspace.id rather than any user input. * app/(app)/[workspaceSlug]/page.tsx: rewrite to consume the new procedures via @trpc/react-query. Adds: - Skeleton loading state (no flash of zeros). - Empty state with a "New task" CTA on workspaces with no objects. - Real "X ago" labels on the recent feed. - Click-through links from recent rows to /{slug}/{id}. - A locally-mounted CreateObjectDialog instance independent of the global one in AppShell so the empty-state CTA can pre-seed defaultType="task" without coordinating shared state. * components/ui/skeleton.tsx: new (standard shadcn pulse skeleton). Used by the dashboard but reusable across the app. The scaffolded "Due this week" stat is dropped: `objects` has no due_at column and the task explicitly preferred dropping a card to schema-creep. `pnpm lint && pnpm type-check` clean. Closes plans/Plan-daily-driver-finish/Epic-shipping-the-shell/ Task-wire-workspace-home-dashboard.md. Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-02 01:17:29 -04:00
onClick={() => {
setCreateType("task");
setCreateOpen(true);
}}
>
feat(web): wire workspace-home dashboard to real tRPC queries Path-A task 2/5. Replaces the hardcoded `stats` (24/8/12) and hardcoded `recent` list on the workspace-home page with real workspace-scoped data. * server/routers/objects.ts: add two new procedures. - `objects.stats` returns { openTasks, containers }. Open-task count treats null status as open; only `done` and `closed` (per packages/shared object-statuses) are terminal. Container count aggregates project + space + group rows. - `objects.listRecent({ limit })` returns the N most-recently-updated rows, descending by updated_at. Excludes archived and excludes `workspace`/`group` from the activity feed (containers clutter "what did I just touch" recency). Both go through workspaceProcedure, so the workspace_id filter comes from the middleware-resolved ctx.workspace.id rather than any user input. * app/(app)/[workspaceSlug]/page.tsx: rewrite to consume the new procedures via @trpc/react-query. Adds: - Skeleton loading state (no flash of zeros). - Empty state with a "New task" CTA on workspaces with no objects. - Real "X ago" labels on the recent feed. - Click-through links from recent rows to /{slug}/{id}. - A locally-mounted CreateObjectDialog instance independent of the global one in AppShell so the empty-state CTA can pre-seed defaultType="task" without coordinating shared state. * components/ui/skeleton.tsx: new (standard shadcn pulse skeleton). Used by the dashboard but reusable across the app. The scaffolded "Due this week" stat is dropped: `objects` has no due_at column and the task explicitly preferred dropping a card to schema-creep. `pnpm lint && pnpm type-check` clean. Closes plans/Plan-daily-driver-finish/Epic-shipping-the-shell/ Task-wire-workspace-home-dashboard.md. Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-02 01:17:29 -04:00
<Plus className="size-4" />
New task
</Button>
</div>
</div>
</header>
<div className="mx-auto flex w-full max-w-5xl flex-1 flex-col gap-8 px-6 py-8 sm:px-10">
<section>
<h2 className="text-sm font-semibold text-foreground">Quick stats</h2>
<p className="mt-1 text-sm text-muted-foreground">
feat(web): wire workspace-home dashboard to real tRPC queries Path-A task 2/5. Replaces the hardcoded `stats` (24/8/12) and hardcoded `recent` list on the workspace-home page with real workspace-scoped data. * server/routers/objects.ts: add two new procedures. - `objects.stats` returns { openTasks, containers }. Open-task count treats null status as open; only `done` and `closed` (per packages/shared object-statuses) are terminal. Container count aggregates project + space + group rows. - `objects.listRecent({ limit })` returns the N most-recently-updated rows, descending by updated_at. Excludes archived and excludes `workspace`/`group` from the activity feed (containers clutter "what did I just touch" recency). Both go through workspaceProcedure, so the workspace_id filter comes from the middleware-resolved ctx.workspace.id rather than any user input. * app/(app)/[workspaceSlug]/page.tsx: rewrite to consume the new procedures via @trpc/react-query. Adds: - Skeleton loading state (no flash of zeros). - Empty state with a "New task" CTA on workspaces with no objects. - Real "X ago" labels on the recent feed. - Click-through links from recent rows to /{slug}/{id}. - A locally-mounted CreateObjectDialog instance independent of the global one in AppShell so the empty-state CTA can pre-seed defaultType="task" without coordinating shared state. * components/ui/skeleton.tsx: new (standard shadcn pulse skeleton). Used by the dashboard but reusable across the app. The scaffolded "Due this week" stat is dropped: `objects` has no due_at column and the task explicitly preferred dropping a card to schema-creep. `pnpm lint && pnpm type-check` clean. Closes plans/Plan-daily-driver-finish/Epic-shipping-the-shell/ Task-wire-workspace-home-dashboard.md. Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-02 01:17:29 -04:00
A snapshot of what&apos;s live in this workspace right now.
</p>
feat(web): wire workspace-home dashboard to real tRPC queries Path-A task 2/5. Replaces the hardcoded `stats` (24/8/12) and hardcoded `recent` list on the workspace-home page with real workspace-scoped data. * server/routers/objects.ts: add two new procedures. - `objects.stats` returns { openTasks, containers }. Open-task count treats null status as open; only `done` and `closed` (per packages/shared object-statuses) are terminal. Container count aggregates project + space + group rows. - `objects.listRecent({ limit })` returns the N most-recently-updated rows, descending by updated_at. Excludes archived and excludes `workspace`/`group` from the activity feed (containers clutter "what did I just touch" recency). Both go through workspaceProcedure, so the workspace_id filter comes from the middleware-resolved ctx.workspace.id rather than any user input. * app/(app)/[workspaceSlug]/page.tsx: rewrite to consume the new procedures via @trpc/react-query. Adds: - Skeleton loading state (no flash of zeros). - Empty state with a "New task" CTA on workspaces with no objects. - Real "X ago" labels on the recent feed. - Click-through links from recent rows to /{slug}/{id}. - A locally-mounted CreateObjectDialog instance independent of the global one in AppShell so the empty-state CTA can pre-seed defaultType="task" without coordinating shared state. * components/ui/skeleton.tsx: new (standard shadcn pulse skeleton). Used by the dashboard but reusable across the app. The scaffolded "Due this week" stat is dropped: `objects` has no due_at column and the task explicitly preferred dropping a card to schema-creep. `pnpm lint && pnpm type-check` clean. Closes plans/Plan-daily-driver-finish/Epic-shipping-the-shell/ Task-wire-workspace-home-dashboard.md. Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-02 01:17:29 -04:00
<div className="mt-4 grid gap-3 sm:grid-cols-2">
<StatCard
label="Open tasks"
value={statsQuery.data?.openTasks}
hint="Excludes done and closed"
isLoading={statsQuery.isLoading}
/>
<StatCard
label="Projects, spaces & groups"
value={statsQuery.data?.containers}
hint="All container objects"
isLoading={statsQuery.isLoading}
/>
</div>
</section>
<Separator />
<section>
<div className="flex items-center justify-between gap-2">
<div>
<h2 className="text-sm font-semibold text-foreground">
Recent activity
</h2>
<p className="mt-1 text-sm text-muted-foreground">
feat(web): wire workspace-home dashboard to real tRPC queries Path-A task 2/5. Replaces the hardcoded `stats` (24/8/12) and hardcoded `recent` list on the workspace-home page with real workspace-scoped data. * server/routers/objects.ts: add two new procedures. - `objects.stats` returns { openTasks, containers }. Open-task count treats null status as open; only `done` and `closed` (per packages/shared object-statuses) are terminal. Container count aggregates project + space + group rows. - `objects.listRecent({ limit })` returns the N most-recently-updated rows, descending by updated_at. Excludes archived and excludes `workspace`/`group` from the activity feed (containers clutter "what did I just touch" recency). Both go through workspaceProcedure, so the workspace_id filter comes from the middleware-resolved ctx.workspace.id rather than any user input. * app/(app)/[workspaceSlug]/page.tsx: rewrite to consume the new procedures via @trpc/react-query. Adds: - Skeleton loading state (no flash of zeros). - Empty state with a "New task" CTA on workspaces with no objects. - Real "X ago" labels on the recent feed. - Click-through links from recent rows to /{slug}/{id}. - A locally-mounted CreateObjectDialog instance independent of the global one in AppShell so the empty-state CTA can pre-seed defaultType="task" without coordinating shared state. * components/ui/skeleton.tsx: new (standard shadcn pulse skeleton). Used by the dashboard but reusable across the app. The scaffolded "Due this week" stat is dropped: `objects` has no due_at column and the task explicitly preferred dropping a card to schema-creep. `pnpm lint && pnpm type-check` clean. Closes plans/Plan-daily-driver-finish/Epic-shipping-the-shell/ Task-wire-workspace-home-dashboard.md. Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-02 01:17:29 -04:00
Latest updates across this workspace.
</p>
</div>
<Badge variant="secondary" className="shrink-0">
feat(web): wire workspace-home dashboard to real tRPC queries Path-A task 2/5. Replaces the hardcoded `stats` (24/8/12) and hardcoded `recent` list on the workspace-home page with real workspace-scoped data. * server/routers/objects.ts: add two new procedures. - `objects.stats` returns { openTasks, containers }. Open-task count treats null status as open; only `done` and `closed` (per packages/shared object-statuses) are terminal. Container count aggregates project + space + group rows. - `objects.listRecent({ limit })` returns the N most-recently-updated rows, descending by updated_at. Excludes archived and excludes `workspace`/`group` from the activity feed (containers clutter "what did I just touch" recency). Both go through workspaceProcedure, so the workspace_id filter comes from the middleware-resolved ctx.workspace.id rather than any user input. * app/(app)/[workspaceSlug]/page.tsx: rewrite to consume the new procedures via @trpc/react-query. Adds: - Skeleton loading state (no flash of zeros). - Empty state with a "New task" CTA on workspaces with no objects. - Real "X ago" labels on the recent feed. - Click-through links from recent rows to /{slug}/{id}. - A locally-mounted CreateObjectDialog instance independent of the global one in AppShell so the empty-state CTA can pre-seed defaultType="task" without coordinating shared state. * components/ui/skeleton.tsx: new (standard shadcn pulse skeleton). Used by the dashboard but reusable across the app. The scaffolded "Due this week" stat is dropped: `objects` has no due_at column and the task explicitly preferred dropping a card to schema-creep. `pnpm lint && pnpm type-check` clean. Closes plans/Plan-daily-driver-finish/Epic-shipping-the-shell/ Task-wire-workspace-home-dashboard.md. Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-02 01:17:29 -04:00
Live
</Badge>
</div>
feat(web): wire workspace-home dashboard to real tRPC queries Path-A task 2/5. Replaces the hardcoded `stats` (24/8/12) and hardcoded `recent` list on the workspace-home page with real workspace-scoped data. * server/routers/objects.ts: add two new procedures. - `objects.stats` returns { openTasks, containers }. Open-task count treats null status as open; only `done` and `closed` (per packages/shared object-statuses) are terminal. Container count aggregates project + space + group rows. - `objects.listRecent({ limit })` returns the N most-recently-updated rows, descending by updated_at. Excludes archived and excludes `workspace`/`group` from the activity feed (containers clutter "what did I just touch" recency). Both go through workspaceProcedure, so the workspace_id filter comes from the middleware-resolved ctx.workspace.id rather than any user input. * app/(app)/[workspaceSlug]/page.tsx: rewrite to consume the new procedures via @trpc/react-query. Adds: - Skeleton loading state (no flash of zeros). - Empty state with a "New task" CTA on workspaces with no objects. - Real "X ago" labels on the recent feed. - Click-through links from recent rows to /{slug}/{id}. - A locally-mounted CreateObjectDialog instance independent of the global one in AppShell so the empty-state CTA can pre-seed defaultType="task" without coordinating shared state. * components/ui/skeleton.tsx: new (standard shadcn pulse skeleton). Used by the dashboard but reusable across the app. The scaffolded "Due this week" stat is dropped: `objects` has no due_at column and the task explicitly preferred dropping a card to schema-creep. `pnpm lint && pnpm type-check` clean. Closes plans/Plan-daily-driver-finish/Epic-shipping-the-shell/ Task-wire-workspace-home-dashboard.md. Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-02 01:17:29 -04:00
{recentQuery.isLoading ? (
<ul className="mt-4 divide-y divide-border rounded-lg border border-border bg-card">
{[0, 1, 2].map((i) => (
<li key={i} className="flex items-start gap-3 px-4 py-3">
<Skeleton className="mt-0.5 size-4 rounded-full" />
<div className="min-w-0 flex-1 space-y-2">
<Skeleton className="h-4 w-2/3" />
<Skeleton className="h-3 w-1/3" />
</div>
<Skeleton className="h-5 w-16" />
</li>
))}
</ul>
) : recent.length === 0 ? (
<EmptyRecent
onCreate={() => {
setCreateType("task");
setCreateOpen(true);
}}
/>
) : (
<ul className="mt-4 divide-y divide-border rounded-lg border border-border bg-card">
{recent.map((item) => {
if (!workspaceSlug) return null;
const Icon = TYPE_ICONS[item.type] ?? ClipboardList;
const typeLabel = TYPE_LABELS[item.type] ?? item.type;
const done = isDoneStatus(item.status);
return (
<li
key={item.id}
className="first:rounded-t-lg last:rounded-b-lg"
>
<Link
href={`/${workspaceSlug}/${item.id}`}
className="flex items-start gap-3 px-4 py-3 transition-colors hover:bg-muted/40"
>
<span className="mt-0.5 text-muted-foreground">
{done ? (
<CheckCircle2 className="size-4 text-teal" />
) : (
<Icon className="size-4" />
)}
</span>
<div className="min-w-0 flex-1">
<p className="truncate font-medium text-foreground">
{item.title || "Untitled"}
</p>
<p className="text-xs text-muted-foreground">
{typeLabel} · Updated {relativeTime(item.updatedAt)}
</p>
</div>
<Badge
variant={done ? "secondary" : "outline"}
className="shrink-0 capitalize"
>
{done
? "Done"
: item.status?.replace("_", " ") ?? "Active"}
</Badge>
<ArrowRight className="mt-0.5 size-4 shrink-0 text-muted-foreground/60" />
</Link>
</li>
);
})}
</ul>
)}
</section>
</div>
feat(web): wire workspace-home dashboard to real tRPC queries Path-A task 2/5. Replaces the hardcoded `stats` (24/8/12) and hardcoded `recent` list on the workspace-home page with real workspace-scoped data. * server/routers/objects.ts: add two new procedures. - `objects.stats` returns { openTasks, containers }. Open-task count treats null status as open; only `done` and `closed` (per packages/shared object-statuses) are terminal. Container count aggregates project + space + group rows. - `objects.listRecent({ limit })` returns the N most-recently-updated rows, descending by updated_at. Excludes archived and excludes `workspace`/`group` from the activity feed (containers clutter "what did I just touch" recency). Both go through workspaceProcedure, so the workspace_id filter comes from the middleware-resolved ctx.workspace.id rather than any user input. * app/(app)/[workspaceSlug]/page.tsx: rewrite to consume the new procedures via @trpc/react-query. Adds: - Skeleton loading state (no flash of zeros). - Empty state with a "New task" CTA on workspaces with no objects. - Real "X ago" labels on the recent feed. - Click-through links from recent rows to /{slug}/{id}. - A locally-mounted CreateObjectDialog instance independent of the global one in AppShell so the empty-state CTA can pre-seed defaultType="task" without coordinating shared state. * components/ui/skeleton.tsx: new (standard shadcn pulse skeleton). Used by the dashboard but reusable across the app. The scaffolded "Due this week" stat is dropped: `objects` has no due_at column and the task explicitly preferred dropping a card to schema-creep. `pnpm lint && pnpm type-check` clean. Closes plans/Plan-daily-driver-finish/Epic-shipping-the-shell/ Task-wire-workspace-home-dashboard.md. Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-02 01:17:29 -04:00
<CreateObjectDialog
open={createOpen}
onOpenChange={setCreateOpen}
defaultType={createType}
/>
</div>
);
}
function StatCard({
label,
value,
hint,
isLoading,
}: {
label: string;
value: number | undefined;
hint: string;
isLoading: boolean;
}) {
return (
<div className="rounded-lg border border-border bg-card p-4 shadow-sm">
<p className="text-xs font-medium text-muted-foreground">{label}</p>
{isLoading ? (
<Skeleton className="mt-2 h-8 w-16" />
) : (
<p className="mt-2 text-2xl font-semibold tabular-nums text-foreground">
{value ?? 0}
</p>
)}
<p className="mt-1 text-xs text-muted-foreground">{hint}</p>
</div>
);
}
function EmptyRecent({ onCreate }: { onCreate: () => void }) {
return (
<div className="mt-4 flex flex-col items-center gap-3 rounded-lg border border-dashed border-border bg-card px-6 py-10 text-center">
<CircleDashed className="size-6 text-muted-foreground" />
<div>
<p className="text-sm font-medium text-foreground">
Nothing here yet
</p>
<p className="mt-1 text-xs text-muted-foreground">
Create your first task to see recent activity here.
</p>
</div>
<Button size="sm" onClick={onCreate} className="gap-1">
<Plus className="size-4" />
New task
</Button>
</div>
);
}