Prompt file imported from vishnumsvp/react-fe-dev-template (
.github/prompts/create-list-page.prompt.md). Fill in{{item_name}}before use. Copyright stays with the author.
Create List / Table Page
Scaffold a list or table page that exactly matches the visual design of src/features/users/pages/UsersPage.tsx and src/features/users/components/UserTable/UserTable.tsx.
Mandatory standards applied to every file generated by this prompt:
- React latest — use only current stable React APIs; no class components or deprecated methods.
- SCSS Modules — every
.tsxmust have a co-located.module.scss; no inlinestyle={{}}props.- WCAG 2.1 AA — all interactive elements need
aria-label,scope="col"on<th>, focus rings via&:focus-visiblein SCSS.
What to ask first
- Feature name (camelCase, e.g.
products,orders) and entity name (PascalCase, e.g.Product,Order) - Route path (e.g.
/products) - Entity fields to display as table columns (id, name, email, status, createdAt, …)
- Which columns need a
<Badge>for status/role/category? What values and which badge variants (success,warning,danger,info)? - Actions per row? Delete only? Edit + Delete? View detail?
- Does a service and hooks file already exist? If not, create them too.
Files to create
1. Page — src/features/<feature>/pages/<Entity>sPage.tsx
import React, { useState } from 'react';
import { DashboardLayout } from '@components/templates/DashboardLayout/DashboardLayout';
import { SearchBar } from '@components/molecules/SearchBar/SearchBar';
import { PageFiltersProvider, usePageFilters } from '@context/PageFiltersContext';
import { useEntities, useDeleteEntity } from '../hooks/useEntities';
import { EntityTable } from '../components/EntityTable/EntityTable';
import { useDebounce } from '@hooks/useDebounce';
import styles from './<Entity>sPage.module.scss';
const EntitiesPageContent: React.FC = () => {
const { filters, setSearch, setPage } = usePageFilters();
const debouncedSearch = useDebounce(filters.search);
void debouncedSearch; // pass to queryFn in real implementation
const { data, isLoading } = useEntities(filters.page, filters.limit);
const deleteEntity = useDeleteEntity();
const [deletingId, setDeletingId] = useState<number | null>(null);
const handleDelete = async (id: number) => {
setDeletingId(id);
await deleteEntity.mutateAsync(id).catch(() => null);
setDeletingId(null);
};
return (
<DashboardLayout>
<div className={styles.page}>
<div className={styles.header}>
<h1 className={styles.title}>Entities</h1>
<SearchBar
value={filters.search}
onChange={setSearch}
placeholder="Search entities..."
/>
</div>
<EntityTable
items={data?.data ?? []}
isLoading={isLoading || !!deletingId}
onDelete={(id) => { void handleDelete(id); }}
/>
{data ? (
<div className={styles.pagination}>
<button
disabled={filters.page <= 1}
onClick={() => setPage(filters.page - 1)}
aria-label="Previous page"
>
← Previous
</button>
<span>
Page {filters.page} of {Math.ceil(data.total / filters.limit)}
</span>
<button
disabled={filters.page * filters.limit >= data.total}
onClick={() => setPage(filters.page + 1)}
aria-label="Next page"
>
Next →
</button>
</div>
) : null}
</div>
</DashboardLayout>
);
};
const EntitiesPage: React.FC = () => (
<PageFiltersProvider>
<EntitiesPageContent />
</PageFiltersProvider>
);
export default EntitiesPage;
2. Page SCSS — <Entity>sPage.module.scss
.page {
padding: 1.6rem 0;
}
.header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 2.4rem;
gap: 1.6rem;
flex-wrap: wrap;
}
.title {
font-size: 3rem;
font-weight: 700;
color: var(--color-text-primary);
margin: 0;
}
.pagination {
display: flex;
align-items: center;
justify-content: center;
gap: 1.6rem;
margin-top: 2.4rem;
button {
padding: 0.8rem 1rem;
background: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: 0.6rem;
cursor: pointer;
color: var(--color-text-primary);
&:disabled { opacity: 0.5; cursor: not-allowed; }
&:hover:not(:disabled) { background: var(--color-primary-ghost); }
&:focus-visible { outline: 2px solid var(--color-focus); outline-offset: 2px; }
}
span { color: var(--color-text-secondary); font-size: 1.4rem; }
}
3. Table organism — src/features/<feature>/components/<Entity>Table/<Entity>Table.tsx
import React from 'react';
import { Badge } from '@components/atoms/Badge/Badge';
import { Button } from '@components/atoms/Button/Button';
import { Spinner } from '@components/atoms/Spinner/Spinner';
import type { Entity } from '@/types/index';
import styles from './<Entity>Table.module.scss';
interface EntityTableProps {
items: Entity[];
isLoading: boolean;
onDelete?: (id: number) => void;
}
// Map entity status/role values to Badge variants
const getStatusVariant = (status: Entity['status']): 'success' | 'warning' | 'danger' | 'info' => {
const map: Record<Entity['status'], 'success' | 'warning' | 'danger' | 'info'> = {
active: 'success',
pending: 'warning',
inactive: 'danger',
archived: 'info',
};
return map[status];
};
export const EntityTable: React.FC<EntityTableProps> = ({ items, isLoading, onDelete }) => {
if (isLoading) {
return (
<div className={styles.loading}>
<Spinner size="large" />
</div>
);
}
return (
<div className={styles.tableWrapper} role="region" aria-label="Entities table">
<table className={styles.table}>
<thead>
<tr>
<th scope="col">ID</th>
<th scope="col">Name</th>
<th scope="col">Status</th>
<th scope="col">Created</th>
{onDelete ? <th scope="col">Actions</th> : null}
</tr>
</thead>
<tbody>
{items.length === 0 ? (
<tr>
<td colSpan={onDelete ? 5 : 4} className={styles.empty}>
No entities found
</td>
</tr>
) : (
items.map((item) => (
<tr key={item.id}>
<td>{item.id}</td>
<td>{item.name}</td>
<td>
<Badge variant={getStatusVariant(item.status)}>{item.status}</Badge>
</td>
<td>{new Date(item.createdAt).toLocaleDateString()}</td>
{onDelete ? (
<td>
<Button
variant="danger"
size="small"
onClick={() => onDelete(item.id)}
aria-label={`Delete {{item_name}}`}
>
Delete
</Button>
</td>
) : null}
</tr>
))
)}
</tbody>
</table>
</div>
);
};
4. Table SCSS — <Entity>Table.module.scss
.tableWrapper {
overflow-x: auto;
border-radius: 0.8rem;
border: 1px solid var(--color-border);
}
.table {
width: 100%;
border-collapse: collapse;
font-size: 1.4rem;
th,
td {
padding: 1.2rem 1rem;
text-align: left;
border-bottom: 1px solid var(--color-border);
}
th {
background-color: var(--color-surface-secondary);
font-weight: 600;
color: var(--color-text-secondary);
font-size: 1.2rem;
text-transform: uppercase;
letter-spacing: 0.05em;
}
tr:last-child td { border-bottom: none; }
tr:hover td { background-color: var(--color-hover); }
}
.loading {
display: flex;
justify-content: center;
padding: 4.8rem;
}
.empty {
text-align: center;
color: var(--color-text-secondary);
padding: 3.2rem !important;
}
Route registration
const EntitiesPage = lazy(() => import('@features/<feature>/pages/<Entity>sPage'));
// Inside <Route element={<PrivateRoute />}>:
<Route path="/<entities>" element={<EntitiesPage />} />
Design Rules (do not deviate)
| Element | Specification |
|---|---|
| Page root | <div className={styles.page}> → padding: 1.6rem 0 |
| Header row | flex, space-between, gap: 1.6rem, flex-wrap: wrap, mb: 2.4rem |
Page h1 |
3rem / 700 / --color-text-primary, margin: 0 |
| Table wrapper | border-radius: 0.8rem; border: 1px solid var(--color-border) |
th |
1.2rem, 600, uppercase, letter-spacing: 0.05em, --color-text-secondary, --color-surface-secondary bg |
td |
1.4rem, 1.2rem 1rem padding |
| Row hover | --color-hover background |
| Loading | <Spinner size="large" /> inside .loading (flex centre, padding 4.8rem) |
| Empty | Centred <td> with --color-text-secondary |
| Pagination | Centred flex, gap: 1.6rem, mt: 2.4rem |
| Colours | Only var(--color-*) |
After creating the files
Remind the user to:
- Create the entity type in
src/types/index.ts. - Create the service with
#create-service. - Create the React Query hooks with
#create-hook. - Add the
getStatusVariantmap to match actual entity status values. - Add tests with
#create-tests. - Run
npm run lint— thejsx-a11yplugin validates all accessibility attributes. - Run
npm outdatedand upgrade React if a newer stable version is available.
Note: This prompt always asks for confirmation before writing files. Present the full file list and wait for the user to say "yes" or "proceed".