2026-03-27 16:03:58 +03:00

7.1 KiB

Page Reference

File Location

apps/dashboard/app/(authenticated)/<section>/<feature>/page.tsx

Where <section> is the navigation section (e.g. sales, inventory, hr) and <feature> is the resource in kebab-case plural.

Complete Template (ResourcePage Pattern)

"use client"

import { ResourcePage } from '@/shared/data-view/resource-page'
import { ColumnHeader } from '@/shared/data-view/table-view'
import { <Feature>Form } from '@/modules/<feature>/<feature>-form'
import { <RESOURCE>_ROUTES } from '@repo/api'
import type { <Resource>Client } from '@repo/api'

export default function <Features>Page() {
    return (
        <ResourcePage<<Resource>Client>
            pageTitle="<Features>"
            title="<Feature>"
            routeKey={<RESOURCE>_ROUTES.INDEX}
            getClient={(api) => api.<camelResource>}
            columns={({ actionsColumn }) => [
                {
                    accessorKey: "<primary_field>",
                    header: ({ column }) => <ColumnHeader column={column} title="<Primary Field>" />,
                },
                {
                    accessorKey: "<field_2>",
                    header: ({ column }) => <ColumnHeader column={column} title="<Field 2>" />,
                },
                // Add more columns as needed...
                actionsColumn(),
            ]}
            renderForm={({ resourceId, initialData, onSuccess }) => (
                <<Feature>Form
                    resourceId={resourceId}
                    initialData={initialData}
                    onSuccess={onSuccess}
                />
            )}
        />
    )
}

ResourcePage Props

Prop Required Description
pageTitle No Page heading text (e.g. "Customers")
title Yes Singular noun for button/dialog (e.g. "Customer" → "Add Customer")
routeKey Yes React Query cache key, use ROUTES.INDEX
getClient Yes Selects the domain client from the authenticated API
columns Yes Column definitions — use callback form to get actionsColumn helper
renderForm Yes Renders the form component inside the dialog
queryOptions No React Query overrides (staleTime, etc.)

Column Patterns

Simple text column (sortable)

{
    accessorKey: "name",
    header: ({ column }) => <ColumnHeader column={column} title="Name" />,
},

Custom cell renderer

{
    accessorKey: "status",
    header: ({ column }) => <ColumnHeader column={column} title="Status" />,
    cell: ({ row }) => (
        <span className={row.original.status === "active" ? "text-green-600" : "text-red-600"}>
            {row.original.status}
        </span>
    ),
},

Column with icon

{
    accessorKey: "name",
    header: ({ column }) => <ColumnHeader column={column} title="Name" />,
    cell: ({ row }) => (
        <div className="flex items-center gap-2">
            <UserIcon className="text-muted-foreground" />
            <span>{row.original.name}</span>
        </div>
    ),
},

Non-sortable column

{
    accessorKey: "notes",
    header: () => <span>Notes</span>,
    enableSorting: false,
},

Actions column (always last)

actionsColumn(),

Real Example: Customers Page

"use client"

import { ResourcePage } from '@/shared/data-view/resource-page'
import { ColumnHeader } from '@/shared/data-view/table-view'
import { CustomerForm } from '@/modules/customers/customer-form'
import { CUSTOMER_ROUTES } from '@repo/api'
import type { CustomersClient } from '@repo/api'
import { Building2Icon, UserIcon } from 'lucide-react'

export default function CustomersPage() {
    return (
        <ResourcePage<CustomersClient>
            pageTitle='Customers'
            title="Customer"
            routeKey={CUSTOMER_ROUTES.INDEX}
            getClient={(api) => api.customers}
            columns={({ actionsColumn }) => [
                {
                    accessorKey: "first_name",
                    header: ({ column }) => <ColumnHeader column={column} title="Customer" />,
                    cell: ({ row }) => {
                        const customerName = row.original.first_name
                        const isCompany = row.original.customer_type?.name?.toLocaleLowerCase() === "company";
                        const companyName = row.original.company_name
                        const name = isCompany && companyName
                            ? `${customerName} (${row.original.last_name})`
                            : customerName
                        return (
                            <div className="flex items-center gap-2">
                                {isCompany
                                    ? <Building2Icon className="text-muted-foreground" />
                                    : <UserIcon className="text-muted-foreground" />}
                                <span>{name}</span>
                            </div>
                        )
                    },
                },
                {
                    accessorKey: "email",
                    header: ({ column }) => <ColumnHeader column={column} title="Email" />,
                },
                {
                    accessorKey: "phone",
                    header: ({ column }) => <ColumnHeader column={column} title="Phone" />,
                },
                actionsColumn(),
            ]}
            renderForm={({ resourceId, initialData, onSuccess }) => (
                <CustomerForm
                    resourceId={resourceId}
                    initialData={initialData}
                    onSuccess={onSuccess}
                />
            )}
        />
    )
}

Alternative: Manual DataTable Pattern (Read-Only or Custom Layout)

Use only when you don't need create/edit/delete in a dialog:

"use client"

import { DashboardHeader } from '@/base/components/layout/dashboard'
import DashboardPage from '@/base/components/layout/dashboard/dashboard-page'
import { ColumnHeader, DataTable, useDataTableQuery } from '@/shared/data-view/table-view'
import { useAuthApi } from '@/shared/useApi'
import { <RESOURCE>_ROUTES } from '@repo/api'
import type { ColumnDef } from '@tanstack/react-table'

const columns: ColumnDef<any>[] = [
    {
        accessorKey: "name",
        header: ({ column }) => <ColumnHeader column={column} title="Name" />,
    },
    // ... more columns
]

export default function <Features>Page() {
    const api = useAuthApi()

    const { data, isLoading, pagination, sorting, handleChange } = useDataTableQuery({
        queryKey: [<RESOURCE>_ROUTES.INDEX],
        client: api.<camelResource>,
    })

    const response = data as any

    return (
        <DashboardPage header={<DashboardHeader />}>
            <DataTable
                columns={columns}
                data={response?.data ?? []}
                pagination={{
                    ...pagination,
                    pageCount: response?.meta?.last_page ?? 1,
                    total: response?.meta?.total ?? 0,
                }}
                sorting={sorting}
                onChange={handleChange}
                isLoading={isLoading}
            />
        </DashboardPage>
    )
}