This commit is contained in:
phaichayon
2026-06-11 12:46:57 +07:00
parent 1993d88306
commit 7a390cf0df
720 changed files with 100919 additions and 0 deletions

View File

@@ -0,0 +1,51 @@
import { queryOptions } from '@tanstack/react-query';
import { apiClient } from '@/lib/api-client';
import type { ProductFilters, ProductsResponse } from '@/features/products/api/types';
import type { UserFilters, UsersResponse } from '@/features/users/api/types';
async function getExampleProducts(filters: ProductFilters): Promise<ProductsResponse> {
const searchParams = new URLSearchParams();
if (filters.page) searchParams.set('page', String(filters.page));
if (filters.limit) searchParams.set('limit', String(filters.limit));
if (filters.categories) searchParams.set('categories', filters.categories);
if (filters.search) searchParams.set('search', filters.search);
if (filters.sort) searchParams.set('sort', filters.sort);
return apiClient<ProductsResponse>(`/example/products?${searchParams.toString()}`);
}
async function getExampleUsers(filters: UserFilters): Promise<UsersResponse> {
const searchParams = new URLSearchParams();
if (filters.page) searchParams.set('page', String(filters.page));
if (filters.limit) searchParams.set('limit', String(filters.limit));
if (filters.roles) searchParams.set('roles', filters.roles);
if (filters.search) searchParams.set('search', filters.search);
if (filters.sort) searchParams.set('sort', filters.sort);
const query = searchParams.toString();
return apiClient<UsersResponse>(`/example/users${query ? `?${query}` : ''}`);
}
export const exampleProductKeys = {
all: ['example-products'] as const,
list: (filters: ProductFilters) => [...exampleProductKeys.all, 'list', filters] as const
};
export const exampleUserKeys = {
all: ['example-users'] as const,
list: (filters: UserFilters) => [...exampleUserKeys.all, 'list', filters] as const
};
export const exampleProductsQueryOptions = (filters: ProductFilters) =>
queryOptions({
queryKey: exampleProductKeys.list(filters),
queryFn: () => getExampleProducts(filters)
});
export const exampleUsersQueryOptions = (filters: UserFilters) =>
queryOptions({
queryKey: exampleUserKeys.list(filters),
queryFn: () => getExampleUsers(filters)
});

View File

@@ -0,0 +1,105 @@
'use client';
import { Badge } from '@/components/ui/badge';
import { DataTable } from '@/components/ui/table/data-table';
import { DataTableColumnHeader } from '@/components/ui/table/data-table-column-header';
import { DataTableToolbar } from '@/components/ui/table/data-table-toolbar';
import { useDataTable } from '@/hooks/use-data-table';
import { getSortingStateParser } from '@/lib/parsers';
import { parseAsInteger, parseAsString, useQueryStates } from 'nuqs';
import { useSuspenseQuery } from '@tanstack/react-query';
import type { Column, ColumnDef } from '@tanstack/react-table';
import { ProductImagePreview } from '@/features/products/components/product-tables/product-image-preview';
import type { Product } from '@/features/products/api/types';
import { CATEGORY_OPTIONS } from '@/features/products/components/product-tables/options';
import { Icons } from '@/components/icons';
import { exampleProductsQueryOptions } from '@/features/example-dashboard/api';
const columns: ColumnDef<Product>[] = [
{
accessorKey: 'photo_url',
header: 'IMAGE',
cell: ({ row }) => (
<ProductImagePreview src={row.getValue('photo_url')} alt={row.getValue('name')} />
)
},
{
id: 'name',
accessorKey: 'name',
header: ({ column }: { column: Column<Product, unknown> }) => (
<DataTableColumnHeader column={column} title='Name' />
),
cell: ({ cell }) => <div className='font-medium'>{cell.getValue<Product['name']>()}</div>,
meta: {
label: 'Name',
placeholder: 'Search products...',
variant: 'text',
icon: Icons.text
},
enableColumnFilter: true
},
{
id: 'category',
accessorKey: 'category',
enableSorting: false,
header: ({ column }: { column: Column<Product, unknown> }) => (
<DataTableColumnHeader column={column} title='Category' />
),
cell: ({ cell }) => (
<Badge variant='outline' className='capitalize'>
{cell.getValue<Product['category']>()}
</Badge>
),
enableColumnFilter: true,
meta: {
label: 'categories',
variant: 'multiSelect',
options: CATEGORY_OPTIONS
}
},
{
accessorKey: 'price',
header: 'PRICE',
cell: ({ row }) => `$${row.original.price.toLocaleString()}`
},
{
accessorKey: 'description',
header: 'DESCRIPTION'
}
];
const columnIds = columns.map((c) => c.id).filter(Boolean) as string[];
export function ExampleProductTable() {
const [params] = useQueryStates({
page: parseAsInteger.withDefault(1),
perPage: parseAsInteger.withDefault(10),
name: parseAsString,
category: parseAsString,
sort: getSortingStateParser(columnIds).withDefault([])
});
const filters = {
page: params.page,
limit: params.perPage,
...(params.name && { search: params.name }),
...(params.category && { categories: params.category }),
...(params.sort.length > 0 && { sort: JSON.stringify(params.sort) })
};
const { data } = useSuspenseQuery(exampleProductsQueryOptions(filters));
const pageCount = Math.ceil(data.total_products / params.perPage);
const { table } = useDataTable({
data: data.products,
columns,
pageCount,
shallow: true
});
return (
<DataTable table={table}>
<DataTableToolbar table={table} />
</DataTable>
);
}

View File

@@ -0,0 +1,118 @@
'use client';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
import { Icons } from '@/components/icons';
import { InfoSidebar } from '@/components/layout/info-sidebar';
import { ThemeModeToggle } from '@/components/themes/theme-mode-toggle';
import { ThemeSelector } from '@/components/themes/theme-selector';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { InfobarProvider } from '@/components/ui/infobar';
import { Separator } from '@/components/ui/separator';
import { cn } from '@/lib/utils';
import { exampleDashboardNavItems } from '@/features/example-dashboard/config';
export function ExampleDashboardShell({ children }: { children: React.ReactNode }) {
const pathname = usePathname();
return (
<div className='min-h-screen bg-[radial-gradient(circle_at_top,_hsl(var(--primary)/0.12),_transparent_38%),linear-gradient(180deg,_hsl(var(--background)),_hsl(var(--muted)/0.28))]'>
<div className='mx-auto flex min-h-screen max-w-[1600px] flex-col lg:flex-row'>
<aside className='border-border/60 bg-background/75 supports-[backdrop-filter]:bg-background/70 w-full border-b backdrop-blur-xl lg:w-72 lg:border-r lg:border-b-0'>
<div className='flex items-center justify-between px-5 py-5 lg:block'>
<div>
<div className='flex items-center gap-3'>
<div className='from-primary to-primary/70 text-primary-foreground flex h-11 w-11 items-center justify-center rounded-2xl bg-gradient-to-br shadow-lg'>
<Icons.sparkles className='h-5 w-5' />
</div>
<div>
<p className='text-sm font-medium uppercase tracking-[0.28em] text-muted-foreground'>
Template
</p>
<h1 className='font-semibold'>Example Dashboard</h1>
</div>
</div>
<p className='mt-3 hidden text-sm text-muted-foreground lg:block'>
Public showcase routes for tables, charts, icons, and React Query patterns.
</p>
</div>
<Badge variant='outline' className='hidden sm:inline-flex lg:mt-4'>
Public demo
</Badge>
</div>
<div className='px-3 pb-4'>
<nav className='grid gap-1'>
{exampleDashboardNavItems.map((item) => {
const Icon = item.icon ? Icons[item.icon] : Icons.arrowRight;
const isActive = pathname === item.url;
return (
<Link
key={item.url}
href={item.url}
className={cn(
'group flex items-center gap-3 rounded-2xl px-3 py-3 text-sm transition-colors',
isActive
? 'bg-primary text-primary-foreground shadow-sm'
: 'hover:bg-muted/80 text-muted-foreground hover:text-foreground'
)}
>
<Icon className='h-4 w-4' />
<span className='font-medium'>{item.title}</span>
</Link>
);
})}
</nav>
<Separator className='my-4' />
<div className='grid gap-2'>
<Button asChild variant='outline' className='justify-start rounded-2xl'>
<Link href='/setup'>
<Icons.settings className='mr-2 h-4 w-4' />
Setup wizard
</Link>
</Button>
<p className='px-1 text-xs leading-5 text-muted-foreground'>
Start real project setup at <code>/setup</code>, then browse these pages to see the
template patterns in motion.
</p>
</div>
</div>
</aside>
<InfobarProvider defaultOpen={false}>
<div className='flex min-h-screen flex-1 flex-col'>
<header className='border-border/60 bg-background/75 supports-[backdrop-filter]:bg-background/70 sticky top-0 z-20 border-b px-4 backdrop-blur-xl md:px-6'>
<div className='flex min-h-16 items-center justify-between gap-4'>
<div>
<p className='text-xs uppercase tracking-[0.24em] text-muted-foreground'>
Example Routes
</p>
<div className='flex items-center gap-2'>
<h2 className='text-lg font-semibold'>Interactive template preview</h2>
<Badge variant='secondary' className='hidden sm:inline-flex'>
No auth required
</Badge>
</div>
</div>
<div className='flex items-center gap-2'>
<ThemeModeToggle />
<div className='hidden sm:block'>
<ThemeSelector />
</div>
</div>
</div>
</header>
<div className='flex flex-1 overflow-hidden'>
<main className='flex min-w-0 flex-1 flex-col'>{children}</main>
<InfoSidebar side='right' />
</div>
</div>
</InfobarProvider>
</div>
</div>
);
}

View File

@@ -0,0 +1,113 @@
'use client';
import { Badge } from '@/components/ui/badge';
import { DataTable } from '@/components/ui/table/data-table';
import { DataTableColumnHeader } from '@/components/ui/table/data-table-column-header';
import { DataTableToolbar } from '@/components/ui/table/data-table-toolbar';
import { useDataTable } from '@/hooks/use-data-table';
import { getSortingStateParser } from '@/lib/parsers';
import { parseAsInteger, parseAsString, useQueryStates } from 'nuqs';
import { useSuspenseQuery } from '@tanstack/react-query';
import type { Column, ColumnDef } from '@tanstack/react-table';
import type { User } from '@/features/users/api/types';
import { ROLE_OPTIONS } from '@/features/users/components/users-table/options';
import { Icons } from '@/components/icons';
import { exampleUsersQueryOptions } from '@/features/example-dashboard/api';
const columns: ColumnDef<User>[] = [
{
accessorKey: 'name',
header: ({ column }: { column: Column<User, unknown> }) => (
<DataTableColumnHeader column={column} title='Name' />
),
cell: ({ row }) => (
<div className='flex flex-col'>
<span className='font-medium'>{row.original.name}</span>
<span className='text-muted-foreground text-xs'>{row.original.email}</span>
</div>
),
meta: {
label: 'Name',
placeholder: 'Search users...',
variant: 'text' as const,
icon: Icons.text
},
enableColumnFilter: true
},
{
id: 'role',
accessorFn: (row) =>
row.systemRole === 'super_admin' ? 'super_admin' : row.activeMembershipRole ?? 'user',
header: ({ column }: { column: Column<User, unknown> }) => (
<DataTableColumnHeader column={column} title='Role' />
),
cell: ({ row }) => {
const role =
row.original.systemRole === 'super_admin' ? 'super_admin' : row.original.activeMembershipRole;
return (
<Badge variant={role === 'admin' ? 'default' : 'outline'} className='capitalize'>
{role?.replace('_', ' ') ?? 'user'}
</Badge>
);
},
enableColumnFilter: true,
meta: {
label: 'roles',
variant: 'multiSelect' as const,
options: ROLE_OPTIONS
}
},
{
id: 'organizations',
accessorFn: (row) => row.organizations.map((organization) => organization.name).join(', '),
header: ({ column }: { column: Column<User, unknown> }) => (
<DataTableColumnHeader column={column} title='Organizations' />
),
cell: ({ row }) => (
<div className='flex flex-wrap gap-1'>
{row.original.memberships.map((membership) => (
<Badge key={membership.organizationId} variant='secondary'>
{membership.organizationName} ({membership.membershipRole}, {membership.businessRole.replace('_', ' ')})
</Badge>
))}
</div>
)
}
];
const columnIds = columns.map((c) => c.id).filter(Boolean) as string[];
export function ExampleUserTable() {
const [params] = useQueryStates({
page: parseAsInteger.withDefault(1),
perPage: parseAsInteger.withDefault(10),
name: parseAsString,
role: parseAsString,
sort: getSortingStateParser(columnIds).withDefault([])
});
const filters = {
page: params.page,
limit: params.perPage,
...(params.name && { search: params.name }),
...(params.role && { roles: params.role }),
...(params.sort.length > 0 && { sort: JSON.stringify(params.sort) })
};
const { data } = useSuspenseQuery(exampleUsersQueryOptions(filters));
const pageCount = Math.ceil(data.total_users / params.perPage);
const { table } = useDataTable({
data: data.users,
columns,
pageCount,
shallow: true
});
return (
<DataTable table={table}>
<DataTableToolbar table={table} />
</DataTable>
);
}

View File

@@ -0,0 +1,29 @@
import type { NavItem } from '@/types';
export const exampleDashboardNavItems: NavItem[] = [
{
title: 'Overview',
url: '/example/dashboard/overview',
icon: 'dashboard'
},
{
title: 'Products',
url: '/example/dashboard/product',
icon: 'product'
},
{
title: 'Users',
url: '/example/dashboard/users',
icon: 'teams'
},
{
title: 'React Query',
url: '/example/dashboard/react-query',
icon: 'code'
},
{
title: 'Icons',
url: '/example/dashboard/elements/icons',
icon: 'palette'
}
];

View File

@@ -0,0 +1,300 @@
import type { Product } from '@/features/products/api/types';
import type { User } from '@/features/users/api/types';
function imageFor(name: string, accent: string) {
return `https://placehold.co/120x120/${accent}/ffffff/png?text=${encodeURIComponent(name.slice(0, 12))}`;
}
const now = '2026-06-07T00:00:00.000Z';
export const exampleProducts: Product[] = [
{
id: 1,
organization_id: 'example-org',
photo_url: imageFor('Asset Portal', '1b4332'),
name: 'Asset Portal',
description: 'Starter module showing a route-handler-backed listing with filters and pagination.',
created_at: now,
price: 1290,
category: 'Electronics',
updated_at: now
},
{
id: 2,
organization_id: 'example-org',
photo_url: imageFor('Ops Console', '2d6a4f'),
name: 'Ops Console',
description: 'Admin surface for approvals, task queues, and role-sensitive workflows.',
created_at: now,
price: 980,
category: 'Books',
updated_at: now
},
{
id: 3,
organization_id: 'example-org',
photo_url: imageFor('Workspace Kit', '40916c'),
name: 'Workspace Kit',
description: 'Multi-tenant UI pattern package for organization switching and scoped data.',
created_at: now,
price: 1420,
category: 'Furniture',
updated_at: now
},
{
id: 4,
organization_id: 'example-org',
photo_url: imageFor('Sales Pulse', '52796f'),
name: 'Sales Pulse',
description: 'Overview widgets and charts demonstrating KPI storytelling in a dashboard shell.',
created_at: now,
price: 760,
category: 'Beauty Products',
updated_at: now
},
{
id: 5,
organization_id: 'example-org',
photo_url: imageFor('Field Notes', '606c38'),
name: 'Field Notes',
description: 'Lightweight operational recordkeeping page used to showcase table composition.',
created_at: now,
price: 240,
category: 'Books',
updated_at: now
},
{
id: 6,
organization_id: 'example-org',
photo_url: imageFor('Checkout Sync', '283618'),
name: 'Checkout Sync',
description: 'Feature example for queue-based updates and cache invalidation patterns.',
created_at: now,
price: 1120,
category: 'Electronics',
updated_at: now
},
{
id: 7,
organization_id: 'example-org',
photo_url: imageFor('Catalog Lens', 'bc6c25'),
name: 'Catalog Lens',
description: 'Search-oriented admin view ideal for practicing server-prefetch and filtering.',
created_at: now,
price: 640,
category: 'Clothing',
updated_at: now
},
{
id: 8,
organization_id: 'example-org',
photo_url: imageFor('Support Desk', 'dda15e'),
name: 'Support Desk',
description: 'Internal tooling example for queues, ownership, and operational auditability.',
created_at: now,
price: 880,
category: 'Toys',
updated_at: now
},
{
id: 9,
organization_id: 'example-org',
photo_url: imageFor('Client Ledger', '9c6644'),
name: 'Client Ledger',
description: 'Demonstrates data-heavy pages where columns, sorting, and filters matter.',
created_at: now,
price: 1540,
category: 'Jewelry',
updated_at: now
},
{
id: 10,
organization_id: 'example-org',
photo_url: imageFor('Daily Brief', '582f0e'),
name: 'Daily Brief',
description: 'Small, opinionated module for daily metrics and recent changes.',
created_at: now,
price: 430,
category: 'Groceries',
updated_at: now
},
{
id: 11,
organization_id: 'example-org',
photo_url: imageFor('Warehouse Pad', '7f4f24'),
name: 'Warehouse Pad',
description: 'Inventory-oriented sample used to show category filtering in the template.',
created_at: now,
price: 1180,
category: 'Furniture',
updated_at: now
},
{
id: 12,
organization_id: 'example-org',
photo_url: imageFor('Pulse Board', '6f1d1b'),
name: 'Pulse Board',
description: 'A concise management view for experimenting with table state and URL sync.',
created_at: now,
price: 990,
category: 'Electronics',
updated_at: now
}
];
export const exampleUsers: User[] = [
{
id: 'user-1',
name: 'Mina Parker',
email: 'mina@example.com',
systemRole: 'super_admin',
activeMembershipRole: 'admin',
activeOrganizationId: 'org-atlas',
organizations: [
{ id: 'org-atlas', name: 'Atlas Labs', role: 'admin' },
{ id: 'org-nova', name: 'Nova Retail', role: 'admin' }
],
memberships: [
{
organizationId: 'org-atlas',
organizationName: 'Atlas Labs',
membershipRole: 'admin',
businessRole: 'it_admin'
},
{
organizationId: 'org-nova',
organizationName: 'Nova Retail',
membershipRole: 'admin',
businessRole: 'auditor'
}
]
},
{
id: 'user-2',
name: 'Noah Bennett',
email: 'noah@example.com',
systemRole: 'user',
activeMembershipRole: 'admin',
activeOrganizationId: 'org-atlas',
organizations: [{ id: 'org-atlas', name: 'Atlas Labs', role: 'admin' }],
memberships: [
{
organizationId: 'org-atlas',
organizationName: 'Atlas Labs',
membershipRole: 'admin',
businessRole: 'helpdesk'
}
]
},
{
id: 'user-3',
name: 'Lina Torres',
email: 'lina@example.com',
systemRole: 'user',
activeMembershipRole: 'user',
activeOrganizationId: 'org-atlas',
organizations: [{ id: 'org-atlas', name: 'Atlas Labs', role: 'user' }],
memberships: [
{
organizationId: 'org-atlas',
organizationName: 'Atlas Labs',
membershipRole: 'user',
businessRole: 'application'
}
]
},
{
id: 'user-4',
name: 'Kai Morgan',
email: 'kai@example.com',
systemRole: 'user',
activeMembershipRole: 'admin',
activeOrganizationId: 'org-nova',
organizations: [{ id: 'org-nova', name: 'Nova Retail', role: 'admin' }],
memberships: [
{
organizationId: 'org-nova',
organizationName: 'Nova Retail',
membershipRole: 'admin',
businessRole: 'infrastructure'
}
]
},
{
id: 'user-5',
name: 'Pim Srisuk',
email: 'pim@example.com',
systemRole: 'user',
activeMembershipRole: 'user',
activeOrganizationId: 'org-nova',
organizations: [{ id: 'org-nova', name: 'Nova Retail', role: 'user' }],
memberships: [
{
organizationId: 'org-nova',
organizationName: 'Nova Retail',
membershipRole: 'user',
businessRole: 'viewer'
}
]
},
{
id: 'user-6',
name: 'Sara Kim',
email: 'sara@example.com',
systemRole: 'user',
activeMembershipRole: 'user',
activeOrganizationId: 'org-orbit',
organizations: [{ id: 'org-orbit', name: 'Orbit Health', role: 'user' }],
memberships: [
{
organizationId: 'org-orbit',
organizationName: 'Orbit Health',
membershipRole: 'user',
businessRole: 'auditor'
}
]
},
{
id: 'user-7',
name: 'Theo Walsh',
email: 'theo@example.com',
systemRole: 'user',
activeMembershipRole: 'admin',
activeOrganizationId: 'org-orbit',
organizations: [{ id: 'org-orbit', name: 'Orbit Health', role: 'admin' }],
memberships: [
{
organizationId: 'org-orbit',
organizationName: 'Orbit Health',
membershipRole: 'admin',
businessRole: 'it_admin'
}
]
},
{
id: 'user-8',
name: 'Mew Lert',
email: 'mew@example.com',
systemRole: 'user',
activeMembershipRole: 'user',
activeOrganizationId: 'org-atlas',
organizations: [
{ id: 'org-atlas', name: 'Atlas Labs', role: 'user' },
{ id: 'org-orbit', name: 'Orbit Health', role: 'user' }
],
memberships: [
{
organizationId: 'org-atlas',
organizationName: 'Atlas Labs',
membershipRole: 'user',
businessRole: 'helpdesk'
},
{
organizationId: 'org-orbit',
organizationName: 'Orbit Health',
membershipRole: 'user',
businessRole: 'viewer'
}
]
}
];