Instruction file imported from metalice/cnv-weekly-report (
.cursor/rules/client-guide.mdc). Copyright stays with the author.
Client Development Guide
Comprehensive guide for React, PatternFly 6, and the weekly report dashboard UI.
1. Component Architecture
Component Types
| Type | Location | Purpose |
|---|---|---|
| Pages | pages/ |
Top-level route components. Fetch data via TanStack Query, compose layout. One per route. |
| Report components | components/report/ |
Report-specific: PersonCard, WorkItemRow, ReportPreview, EditableNotes. |
| Common components | components/common/ |
Reusable UI elements: StatusBadge, TimeAgo, PersonAvatar, StatCard. |
| Layout components | components/layout/ |
App shell: Masthead, Sidebar, PageSection wrappers. |
| Modals | components/modals/ |
Dialogs: SendReportModal, AddNoteModal, TeamMemberModal. |
File Structure
packages/client/src/
├── api/ # API client functions (one file per resource)
│ ├── client.ts # apiFetch, apiPost, apiPut, ApiError
│ ├── reports.ts
│ ├── team.ts
│ ├── activity.ts
│ └── subscription.ts
├── components/
│ ├── common/ # StatusBadge, TimeAgo, PersonAvatar, StatCard
│ ├── report/ # PersonCard, WorkItemRow, ReportPreview, EditableNotes
│ ├── layout/ # AppLayout, AppMasthead, AppSidebar
│ └── modals/ # SendReportModal, AddNoteModal, TeamMemberModal
├── pages/ # DashboardPage, ReportEditorPage, TeamPage, HistoryPage, SettingsPage
├── styles/
│ └── app.css # Custom CSS using PF tokens
└── utils/ # Pure utility functions
Naming Conventions
- Component files: PascalCase (
DashboardPage.tsx,PersonCard.tsx) - API files: camelCase matching resource (
reports.ts,team.ts) - Hook files: camelCase with
useprefix (useWebSocket.ts) - Utility files: camelCase (
formatting.ts)
2. React Patterns
Component Internal Order
- Router hooks (
useParams,useNavigate,useSearchParams) - Query hooks (
useQuery,useMutation) - Local state (
useState) - Derived values (
useMemo) - Callbacks (
useCallback) - Effects (
useEffect) - Early returns (loading, error, empty)
- Main JSX return
TanStack Query Conventions
const { data, isLoading, error } = useQuery({
queryKey: ['report', weekId],
queryFn: () => apiFetch<WeeklyReport>(`/report/${weekId}`),
enabled: Boolean(weekId),
});
- Query keys: resource name first, params second
- Invalidate by prefix:
queryClient.invalidateQueries({ queryKey: ['report'] }) - Mutations must invalidate related queries in
onSuccess - Use
enabledto conditionally fetch
State Management Rules
- Server state: TanStack Query only. Never mirror API data in
useState. - UI state:
useStatefor component-local (modals, toggles, form fields). - URL state:
useSearchParamsfor filterable/shareable state. - No Redux, no Zustand. TanStack Query + local state covers all needs.
3. PatternFly 6 Component Guide
Page Layout
<Page>
<Masthead>...</Masthead>
<Sidebar><Nav>...</Nav></Sidebar>
<PageSection variant="light">
<Title headingLevel="h1">Page Title</Title>
</PageSection>
<PageSection>
{/* Main content */}
</PageSection>
</Page>
Component Selection
| Need | Component | Import from |
|---|---|---|
| Content container | Card + CardHeader + CardBody |
@patternfly/react-core |
| Data table | Table, Thead, Tbody, Tr, Th, Td |
@patternfly/react-table |
| Key-value pairs | DescriptionList |
@patternfly/react-core |
| Form dialog | Modal + ModalBody |
@patternfly/react-core |
| Toolbar actions | Toolbar + ToolbarContent + ToolbarItem |
@patternfly/react-core |
| Status label | Label with variant (success, danger, warning) |
@patternfly/react-core |
| Empty state | EmptyState + icon + EmptyStateBody |
@patternfly/react-core |
| Loading | Spinner (full) or Skeleton (inline) |
@patternfly/react-core |
| Tabs | Tabs + Tab |
@patternfly/react-core |
| Inline editing | TextInput / TextArea with edit toggle |
@patternfly/react-core |
| Person info | Flex with avatar + name + stats |
@patternfly/react-core |
Layout Components
| Need | Component | Usage |
|---|---|---|
| Horizontal layout | Flex + FlexItem |
<Flex gap={{ default: 'gapMd' }}> |
| Responsive card grid | Gallery |
<Gallery minWidths={{ default: '300px' }}> |
| Fixed columns | Grid + GridItem |
<Grid hasGutter><GridItem span={6}> |
| Vertical stack | Stack + StackItem |
<Stack hasGutter> |
| Split (left/right) | Split + SplitItem |
<Split hasGutter><SplitItem isFilled> |
4. Report Editor UI Patterns
PersonCard
Each team member gets a card in the report editor:
<Card>
<CardHeader>
<Flex>
<PersonAvatar member={member} />
<Title headingLevel="h3">{member.displayName}</Title>
<StatCard label="PRs Merged" value={stats.prsMerged} />
<StatCard label="Tickets Closed" value={stats.ticketsClosed} />
</Flex>
</CardHeader>
<CardBody>
<Tabs>
<Tab title="PRs">{/* PR list */}</Tab>
<Tab title="Jira">{/* Ticket list */}</Tab>
<Tab title="Commits">{/* Commit summary */}</Tab>
</Tabs>
<EditableNotes value={notes} onChange={onNotesChange} />
</CardBody>
</Card>
Inline Editing
- Click-to-edit text areas for manager notes
- Checkbox to exclude items from the report
- Drag-and-drop reorder for person sections (stretch goal)
- All edits auto-save via mutation with debounce
Report Preview
- Toggle between edit mode and preview mode
- Preview shows the HTML email format
- Finalize button changes report state from REVIEW to FINALIZED
5. Styling Rules
CSS Token System
All custom styling in packages/client/src/styles/app.css using PF tokens:
.app-person-card {
padding: var(--pf-t--global--spacer--md);
border-left: 3px solid var(--pf-t--global--color--status--info--default);
background: var(--pf-t--global--background--color--secondary--default);
border-radius: var(--pf-t--global--border--radius--small);
}
Rules
- NO hardcoded
pxfor spacing -- use--pf-t--global--spacer--* - NO hex/rgb colors -- use
--pf-t--global--color--*tokens - NO inline
styleprops in JSX - Prefix custom classes with
app-to avoid PF collisions - NO targeting PF internal class names
- NO
!important
Dark Mode
- All custom CSS automatically works in dark mode when using PF tokens
- Toggle dark mode:
document.documentElement.classList.toggle('pf-v6-theme-dark') - Test both themes during development
6. Accessibility
Required
aria-labelon icon-only buttons- Form inputs inside
<FormGroup label="Field name"> - Keyboard navigation on all interactive elements
- Focus visible: never hide focus outlines
- Color is not the only indicator: add text or icons alongside status colors
- Touch targets: minimum 44x44px
7. API Client Layer
import { apiFetch } from './client';
import type { WeeklyReport } from '@cnv-report/shared';
export const fetchReport = (weekId: string): Promise<WeeklyReport> =>
apiFetch(`/report/${weekId}`);
export const updateReport = (weekId: string, body: UpdateReportRequest): Promise<void> =>
apiPut(`/report/${weekId}`, body);
- One file per resource in
api/ - Functions wrap
apiFetch/apiPost/apiPutonly - Return typed promises using generics
- Don't handle errors here -- TanStack Query handles them
8. Page Conventions
Standard Page Structure
export const MyPage = () => {
useEffect(() => { document.title = 'My Page | CNV Weekly Report'; }, []);
const { data, isLoading, error } = useQuery({ ... });
if (isLoading) {
return <PageSection isFilled><Spinner /></PageSection>;
}
if (error) {
return <PageSection><EmptyState icon={ExclamationCircleIcon}>Error</EmptyState></PageSection>;
}
if (!data) {
return <PageSection><EmptyState>No data</EmptyState></PageSection>;
}
return (
<>
<PageSection variant="light"><Title headingLevel="h1">My Page</Title></PageSection>
<PageSection>{/* content */}</PageSection>
</>
);
};
Lazy Loading
All pages use React.lazy in App.tsx:
const MyPage = React.lazy(() =>
import('./pages/MyPage').then(m => ({ default: m.MyPage })),
);
Adding a New Page
- Create
pages/MyPage.tsxwith the standard structure - Add lazy import in
App.tsx - Add
<Route path="/my-page" element={<MyPage />} /> - Add navigation item in
components/layout/AppSidebar.tsx