feat: replace DashboardShell and SignedOut/SignedIn components with DashboardPageLayout for improved structure and state handling

This commit is contained in:
Abhimanyu Saharan
2026-02-09 00:10:35 +05:30
parent 746b909ed6
commit 9a8fd3558d
2 changed files with 187 additions and 237 deletions

View File

@@ -5,7 +5,7 @@ export const dynamic = "force-dynamic";
import { useMemo, useState } from "react"; import { useMemo, useState } from "react";
import Link from "next/link"; import Link from "next/link";
import { SignedIn, SignedOut, useAuth } from "@/auth/clerk"; import { useAuth } from "@/auth/clerk";
import { import {
type ColumnDef, type ColumnDef,
flexRender, flexRender,
@@ -22,8 +22,7 @@ import {
useListBoardGroupsApiV1BoardGroupsGet, useListBoardGroupsApiV1BoardGroupsGet,
} from "@/api/generated/board-groups/board-groups"; } from "@/api/generated/board-groups/board-groups";
import type { BoardGroupRead } from "@/api/generated/model"; import type { BoardGroupRead } from "@/api/generated/model";
import { DashboardSidebar } from "@/components/organisms/DashboardSidebar"; import { DashboardPageLayout } from "@/components/templates/DashboardPageLayout";
import { DashboardShell } from "@/components/templates/DashboardShell";
import { Button, buttonVariants } from "@/components/ui/button"; import { Button, buttonVariants } from "@/components/ui/button";
import { import {
Dialog, Dialog,
@@ -33,7 +32,6 @@ import {
DialogHeader, DialogHeader,
DialogTitle, DialogTitle,
} from "@/components/ui/dialog"; } from "@/components/ui/dialog";
import { SignedOutPanel } from "@/components/auth/SignedOutPanel";
import { formatTimestamp } from "@/lib/formatters"; import { formatTimestamp } from "@/lib/formatters";
import { TableEmptyStateRow, TableLoadingRow } from "@/components/ui/table-state"; import { TableEmptyStateRow, TableLoadingRow } from "@/components/ui/table-state";
@@ -179,120 +177,97 @@ export default function BoardGroupsPage() {
}); });
return ( return (
<DashboardShell> <>
<SignedOut> <DashboardPageLayout
<SignedOutPanel signedOut={{
message="Sign in to view board groups." message: "Sign in to view board groups.",
forceRedirectUrl="/board-groups" forceRedirectUrl: "/board-groups",
/> }}
</SignedOut> title="Board groups"
<SignedIn> description={`Group boards so agents can see related work. ${groups.length} group${groups.length === 1 ? "" : "s"} total.`}
<DashboardSidebar /> headerActions={
<main className="flex-1 overflow-y-auto bg-slate-50"> <Link
<div className="sticky top-0 z-30 border-b border-slate-200 bg-white"> href="/board-groups/new"
<div className="px-8 py-6"> className={buttonVariants({ size: "md", variant: "primary" })}
<div className="flex flex-wrap items-center justify-between gap-4"> >
<div> Create group
<h1 className="text-2xl font-semibold tracking-tight text-slate-900"> </Link>
Board groups }
</h1> stickyHeader
<p className="mt-1 text-sm text-slate-500"> >
Group boards so agents can see related work. {groups.length}{" "} <div className="overflow-hidden rounded-xl border border-slate-200 bg-white shadow-sm">
group{groups.length === 1 ? "" : "s"} total. <div className="overflow-x-auto">
</p> <table className="w-full text-left text-sm">
</div> <thead className="sticky top-0 z-10 bg-slate-50 text-xs uppercase tracking-wide text-slate-500">
<Link {table.getHeaderGroups().map((headerGroup) => (
href="/board-groups/new" <tr key={headerGroup.id}>
className={buttonVariants({ size: "md", variant: "primary" })} {headerGroup.headers.map((header) => (
> <th
Create group key={header.id}
</Link> className="px-6 py-3 text-left font-semibold"
</div> >
</div> {header.isPlaceholder
</div> ? null
: flexRender(
<div className="p-8"> header.column.columnDef.header,
<div className="overflow-hidden rounded-xl border border-slate-200 bg-white shadow-sm"> header.getContext(),
<div className="overflow-x-auto"> )}
<table className="w-full text-left text-sm"> </th>
<thead className="sticky top-0 z-10 bg-slate-50 text-xs uppercase tracking-wide text-slate-500">
{table.getHeaderGroups().map((headerGroup) => (
<tr key={headerGroup.id}>
{headerGroup.headers.map((header) => (
<th
key={header.id}
className="px-6 py-3 text-left font-semibold"
>
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext(),
)}
</th>
))}
</tr>
))} ))}
</thead> </tr>
<tbody className="divide-y divide-slate-100"> ))}
{groupsQuery.isLoading ? ( </thead>
<TableLoadingRow colSpan={columns.length} /> <tbody className="divide-y divide-slate-100">
) : table.getRowModel().rows.length ? ( {groupsQuery.isLoading ? (
table.getRowModel().rows.map((row) => ( <TableLoadingRow colSpan={columns.length} />
<tr ) : table.getRowModel().rows.length ? (
key={row.id} table.getRowModel().rows.map((row) => (
className="transition hover:bg-slate-50" <tr key={row.id} className="transition hover:bg-slate-50">
> {row.getVisibleCells().map((cell) => (
{row.getVisibleCells().map((cell) => ( <td key={cell.id} className="px-6 py-4 align-top">
<td key={cell.id} className="px-6 py-4 align-top"> {flexRender(
{flexRender( cell.column.columnDef.cell,
cell.column.columnDef.cell, cell.getContext(),
cell.getContext(), )}
)} </td>
</td> ))}
))} </tr>
</tr> ))
)) ) : (
) : ( <TableEmptyStateRow
<TableEmptyStateRow colSpan={columns.length}
colSpan={columns.length} icon={
icon={ <svg
<svg className="h-16 w-16 text-slate-300"
className="h-16 w-16 text-slate-300" viewBox="0 0 24 24"
viewBox="0 0 24 24" fill="none"
fill="none" stroke="currentColor"
stroke="currentColor" strokeWidth="1.5"
strokeWidth="1.5" strokeLinecap="round"
strokeLinecap="round" strokeLinejoin="round"
strokeLinejoin="round" >
> <path d="M3 7h8" />
<path d="M3 7h8" /> <path d="M3 17h8" />
<path d="M3 17h8" /> <path d="M13 7h8" />
<path d="M13 7h8" /> <path d="M13 17h8" />
<path d="M13 17h8" /> <path d="M3 12h18" />
<path d="M3 12h18" /> </svg>
</svg> }
} title="No groups yet"
title="No groups yet" description="Create a board group to increase cross-board visibility for agents."
description="Create a board group to increase cross-board visibility for agents." actionHref="/board-groups/new"
actionHref="/board-groups/new" actionLabel="Create your first group"
actionLabel="Create your first group" />
/> )}
)} </tbody>
</tbody> </table>
</table>
</div>
</div>
{groupsQuery.error ? (
<p className="mt-4 text-sm text-red-500">
{groupsQuery.error.message}
</p>
) : null}
</div> </div>
</main> </div>
</SignedIn>
{groupsQuery.error ? (
<p className="mt-4 text-sm text-red-500">{groupsQuery.error.message}</p>
) : null}
</DashboardPageLayout>
<Dialog <Dialog
open={!!deleteTarget} open={!!deleteTarget}
onOpenChange={(nextOpen) => { onOpenChange={(nextOpen) => {
@@ -324,6 +299,6 @@ export default function BoardGroupsPage() {
</DialogFooter> </DialogFooter>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
</DashboardShell> </>
); );
} }

View File

@@ -5,7 +5,7 @@ export const dynamic = "force-dynamic";
import { useMemo, useState } from "react"; import { useMemo, useState } from "react";
import Link from "next/link"; import Link from "next/link";
import { SignedIn, SignedOut, useAuth } from "@/auth/clerk"; import { useAuth } from "@/auth/clerk";
import { import {
type ColumnDef, type ColumnDef,
flexRender, flexRender,
@@ -28,8 +28,7 @@ import {
import { formatTimestamp } from "@/lib/formatters"; import { formatTimestamp } from "@/lib/formatters";
import { useOrganizationMembership } from "@/lib/use-organization-membership"; import { useOrganizationMembership } from "@/lib/use-organization-membership";
import type { BoardGroupRead, BoardRead } from "@/api/generated/model"; import type { BoardGroupRead, BoardRead } from "@/api/generated/model";
import { DashboardSidebar } from "@/components/organisms/DashboardSidebar"; import { DashboardPageLayout } from "@/components/templates/DashboardPageLayout";
import { DashboardShell } from "@/components/templates/DashboardShell";
import { Button, buttonVariants } from "@/components/ui/button"; import { Button, buttonVariants } from "@/components/ui/button";
import { import {
Dialog, Dialog,
@@ -39,7 +38,6 @@ import {
DialogHeader, DialogHeader,
DialogTitle, DialogTitle,
} from "@/components/ui/dialog"; } from "@/components/ui/dialog";
import { SignedOutPanel } from "@/components/auth/SignedOutPanel";
import { TableEmptyStateRow, TableLoadingRow } from "@/components/ui/table-state"; import { TableEmptyStateRow, TableLoadingRow } from "@/components/ui/table-state";
const compactId = (value: string) => const compactId = (value: string) =>
@@ -228,125 +226,102 @@ export default function BoardsPage() {
}); });
return ( return (
<DashboardShell> <>
<SignedOut> <DashboardPageLayout
<SignedOutPanel signedOut={{
message="Sign in to view boards." message: "Sign in to view boards.",
forceRedirectUrl="/boards" forceRedirectUrl: "/boards",
signUpForceRedirectUrl="/boards" signUpForceRedirectUrl: "/boards",
/> }}
</SignedOut> title="Boards"
<SignedIn> description={`Manage boards and task workflows. ${boards.length} board${boards.length === 1 ? "" : "s"} total.`}
<DashboardSidebar /> headerActions={
<main className="flex-1 overflow-y-auto bg-slate-50"> boards.length > 0 && isAdmin ? (
<div className="sticky top-0 z-30 border-b border-slate-200 bg-white"> <Link
<div className="px-8 py-6"> href="/boards/new"
<div className="flex flex-wrap items-center justify-between gap-4"> className={buttonVariants({
<div> size: "md",
<h1 className="text-2xl font-semibold tracking-tight text-slate-900"> variant: "primary",
Boards })}
</h1> >
<p className="mt-1 text-sm text-slate-500"> Create board
Manage boards and task workflows. {boards.length} board </Link>
{boards.length === 1 ? "" : "s"} total. ) : null
</p> }
</div> stickyHeader
{boards.length > 0 && isAdmin ? ( >
<Link <div className="overflow-hidden rounded-xl border border-slate-200 bg-white shadow-sm">
href="/boards/new" <div className="overflow-x-auto">
className={buttonVariants({ <table className="w-full text-left text-sm">
size: "md", <thead className="sticky top-0 z-10 bg-slate-50 text-xs uppercase tracking-wide text-slate-500">
variant: "primary", {table.getHeaderGroups().map((headerGroup) => (
})} <tr key={headerGroup.id}>
> {headerGroup.headers.map((header) => (
Create board <th
</Link> key={header.id}
) : null} className="px-6 py-3 text-left font-semibold"
</div> >
</div> {header.isPlaceholder
</div> ? null
: flexRender(
<div className="p-8"> header.column.columnDef.header,
<div className="overflow-hidden rounded-xl border border-slate-200 bg-white shadow-sm"> header.getContext(),
<div className="overflow-x-auto"> )}
<table className="w-full text-left text-sm"> </th>
<thead className="sticky top-0 z-10 bg-slate-50 text-xs uppercase tracking-wide text-slate-500">
{table.getHeaderGroups().map((headerGroup) => (
<tr key={headerGroup.id}>
{headerGroup.headers.map((header) => (
<th
key={header.id}
className="px-6 py-3 text-left font-semibold"
>
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext(),
)}
</th>
))}
</tr>
))} ))}
</thead> </tr>
<tbody className="divide-y divide-slate-100"> ))}
{boardsQuery.isLoading ? ( </thead>
<TableLoadingRow colSpan={columns.length} /> <tbody className="divide-y divide-slate-100">
) : table.getRowModel().rows.length ? ( {boardsQuery.isLoading ? (
table.getRowModel().rows.map((row) => ( <TableLoadingRow colSpan={columns.length} />
<tr ) : table.getRowModel().rows.length ? (
key={row.id} table.getRowModel().rows.map((row) => (
className="transition hover:bg-slate-50" <tr key={row.id} className="transition hover:bg-slate-50">
> {row.getVisibleCells().map((cell) => (
{row.getVisibleCells().map((cell) => ( <td key={cell.id} className="px-6 py-4 align-top">
<td key={cell.id} className="px-6 py-4 align-top"> {flexRender(
{flexRender( cell.column.columnDef.cell,
cell.column.columnDef.cell, cell.getContext(),
cell.getContext(), )}
)} </td>
</td> ))}
))} </tr>
</tr> ))
)) ) : (
) : ( <TableEmptyStateRow
<TableEmptyStateRow colSpan={columns.length}
colSpan={columns.length} icon={
icon={ <svg
<svg className="h-16 w-16 text-slate-300"
className="h-16 w-16 text-slate-300" viewBox="0 0 24 24"
viewBox="0 0 24 24" fill="none"
fill="none" stroke="currentColor"
stroke="currentColor" strokeWidth="1.5"
strokeWidth="1.5" strokeLinecap="round"
strokeLinecap="round" strokeLinejoin="round"
strokeLinejoin="round" >
> <rect x="3" y="3" width="7" height="7" />
<rect x="3" y="3" width="7" height="7" /> <rect x="14" y="3" width="7" height="7" />
<rect x="14" y="3" width="7" height="7" /> <rect x="14" y="14" width="7" height="7" />
<rect x="14" y="14" width="7" height="7" /> <rect x="3" y="14" width="7" height="7" />
<rect x="3" y="14" width="7" height="7" /> </svg>
</svg> }
} title="No boards yet"
title="No boards yet" description="Create your first board to start routing tasks and monitoring work across agents."
description="Create your first board to start routing tasks and monitoring work across agents." actionHref="/boards/new"
actionHref="/boards/new" actionLabel="Create your first board"
actionLabel="Create your first board" />
/> )}
)} </tbody>
</tbody> </table>
</table>
</div>
</div>
{boardsQuery.error ? (
<p className="mt-4 text-sm text-red-500">
{boardsQuery.error.message}
</p>
) : null}
</div> </div>
</main> </div>
</SignedIn>
{boardsQuery.error ? (
<p className="mt-4 text-sm text-red-500">{boardsQuery.error.message}</p>
) : null}
</DashboardPageLayout>
<Dialog <Dialog
open={!!deleteTarget} open={!!deleteTarget}
onOpenChange={(nextOpen) => { onOpenChange={(nextOpen) => {
@@ -378,6 +353,6 @@ export default function BoardsPage() {
</DialogFooter> </DialogFooter>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
</DashboardShell> </>
); );
} }