| name | senior-workflow-frontend |
| description | Use when starting ANY non-trivial issue or feature on the Next.js frontend — covers the full Senior Engineer workflow: requirements clarification → technical design → task breakdown → implement → self-QA → PR. ALWAYS trigger this skill when the user says "implement", "add feature", "build page", "add component", "create hook", "fix", "fix bug", "find root cause", "plan and implement", pastes a GitHub issue/ticket number or URL, or asks Codex to take a selected issue all the way from intake to PR. Do NOT skip phases — Phase 5 (Self-QA) is the gate before PR and the phase most commonly skipped; skipping it is the #1 source of rework.
|
Senior Engineer Workflow — Next.js Frontend
Run these 6 phases in order. Mark each one done before moving to the next.
Phase 5 is mandatory — do not open a PR without completing it.
Issue-driven automation rule
When the user provides a concrete issue/ticket/URL or this skill is reached from the next-task automation flow, treat that issue as the single source of truth and run Phases 1 → 6 continuously. Do not stop at implementation if the user expectation is end-to-end delivery; continue into commit/PR preparation after self-QA unless blocked.
Phase 1 — Requirements Clarification
Before writing a single line of code, understand the why.
Questions to answer (ask the user if unclear):
- Who uses this screen?
- Worker on phone →
(kiosk) route group, Vietnamese labels, touch targets ≥ 48px
- Manager on desktop →
(dashboard) route group, responsive grid
- What is the exact Definition of Done?
- Screen renders? Or also: loading state, error state, empty state, responsive at 375/768/1280px?
- Adversarial edge cases to surface:
- "What if the API returns an empty array?"
- "What if the user taps the button twice before the mutation resolves?"
- "What if
data is undefined on first render?" (TanStack Query always starts undefined)
- "Is the response a
PagedResult<T> ({ items, total_items }) or a plain T[]?"
- "Are any date/optional fields missing in some API responses?"
- Is there a Sprint issue number? (for commit/PR tagging)
Output of this phase: a short bullet list — Who / DoD / Edge cases identified
Phase 2 — Technical Design (Next.js Frontend)
Design before coding. Answer these questions before opening any file.
Route & component tree
Worker on phone? → (kiosk)/page-name/page.tsx
Manager on desktop? → (dashboard)/page-name/page.tsx
Component tree sketch:
page.tsx (can be server component — exports metadata)
└── PageContent ('use client' — owns state + data fetching)
├── loading.tsx (Skeleton that mirrors real layout)
├── error.tsx ('use client' — required directive)
└── domain components (pure, props only, no fetching)
API alignment check — do this before writing any TypeScript
Read the backend iface.go for the relevant module. Confirm:
- Does the endpoint return
PagedResult<T> or T[]?
PagedResult<T> → { items: T[], total_items: number, ... } — always unwrap .items
- Skipping this causes
TypeError: cannot read property 'filter' of undefined
- What are the exact JSON field names? Go uses
snake_case — must match exactly in src/types/api.ts
- Which fields are pointers in Go (
*string, *time.Time)? These can be null — type them as field?: string or field: string | null
- Are date fields always present or sometimes omitted? → type as
created_at?: string and guard before formatting
State & data flow
- Server state → TanStack Query
useQuery / useMutation
- Local UI state →
useState in the component
- Cross-component shared state (rare) → Zustand in
src/lib/hooks/
- Query key strategy:
[domain] → [domain, filter] → [domain, id]
- Which queries need
invalidateQueries after a mutation? List them now.
Impact analysis
- Does this touch existing cache keys? A mutation may need to invalidate more than one key.
- New route? → needs
loading.tsx + error.tsx siblings
- New navigation entry? → update
side-nav.tsx or bottom-nav.tsx AND authorization.ts
- New API domain file? → follow 4-file pattern
Output of this phase: component tree sketch, confirmed API shape + optional fields, query keys, list of mutations that invalidate
Phase 3 — Task Breakdown
Split into discrete tasks. Use TaskCreate to track them.
Typical order for a new feature:
- Add/update types in
src/types/api.ts (match backend iface.go exactly)
- Create
src/lib/api/<domain>.ts (thin wrappers over apiClient)
- Create
src/lib/hooks/use-<domain>.ts (TanStack Query hooks)
- Create
page.tsx + loading.tsx + error.tsx
- Build presentational components (no fetch logic inside them)
- Wire data into page via hooks
- Handle all three states: loading / error / empty
- TypeScript check → ESLint → build
Rule: Types must be correct before writing hooks. Hooks must be correct before writing UI. Never combine steps that should be sequential.
Phase 4 — Implement
4-file pattern for every new API domain
src/types/api.ts ← Step 1: DTOs (never declare types locally in a page)
src/lib/api/<domain>.ts ← Step 2: thin API wrappers over apiClient
src/lib/hooks/use-<domain>.ts ← Step 3: TanStack Query hooks
src/app/(group)/page-name/page.tsx ← Step 4: page + inline components
Type safety rules
const items = data?.items ?? []
plan.deadline ? formatDate(plan.deadline) : '—'
const d = new Date(entity.created_at)
supplier_code: string | null
Hook patterns
const { data, isLoading, error } = useMyEntities(filter)
const items = data?.items ?? []
useMutation({
mutationFn: myApi.create,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: [MY_KEY] })
toast.success('Đã lưu')
},
onError: (err: ApiClientError) => toast.error(err.message),
})
Side effect rules — the #1 source of infinite loops
Never call setState or setX directly in the render body.
Any logic of the form "when X changes, update Y state" belongs in useEffect:
const prevRef = { current: '' }
if (someValue && prevRef.current !== someValue) {
prevRef.current = someValue
setOtherState(...)
}
useEffect(() => {
if (!someValue) return
setOtherState(derivedFrom(someValue))
}, [someValue])
Use useRef for values that must persist across renders without triggering a re-render (e.g., scanner ref, previous value tracker). Declare refs with useRef(initialValue) — never as a plain object { current: '' }.
Required states — every data-driven page must have all three
if (isLoading) return <Skeleton ... />
if (error) return <ErrorMessage ... />
if (!items.length) return <EmptyState />
Kiosk-specific rules (if in (kiosk) route group)
- All interactive elements:
min-h-[48px] — no exceptions
- Primary actions:
BigButton from @/components/kiosk/big-button
- All user-visible labels: Vietnamese
- Font size: ≥ 16px for anything the user reads or taps
- Action feedback:
toast.success() / toast.error() within 300ms of mutation settling
Phase 5 — Self-QA ⛔ DO NOT SKIP
This is the gate before PR. Work through every section below in order — not just the ones that seem relevant. Most rework in this project comes from skipping this phase.
Step 5.1 — Run automated checks first
These must all pass before anything else. Fix every error; do not move to 5.2 with failures.
npx tsc --noEmit
npm run build
npm run build is the true gate. Turbopack and the production compiler catch different errors — a passing tsc --noEmit does not mean the build will pass.
If lint reports suggestCanonicalClasses warnings (Tailwind v4), replace arbitrary values with canonical equivalents:
max-w-[375px] → max-w-93.75
h-[48px] → h-12
Step 5.2 — Read every changed file top to bottom
Do not skim. For each changed file, ask yourself the questions below.
Render body side effects
Import hygiene
TypeScript strictness
Date handling
TanStack Query correctness
Component structure
Storybook (if stories were added or modified)
Navigation & routing
Step 5.3 — Spot-check in the browser
Before marking the task done, open the page and verify:
Phase 6 — PR
Commit message format
[area] verb: brief description
- bullet detail 1
- bullet detail 2
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
area is one of: kiosk, dashboard, api, hooks, types, or the feature name.
Example:
[kiosk] fix: classify camera errors with user-facing guidance
- Detect NotAllowedError, NotFoundError, insecure context separately
- Show Vietnamese error banner with actionable instructions per error type
- Move lineItems → rows sync into useEffect to prevent infinite re-render
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Branch rules
- If this started from an issue-driven automation flow and you are still on
dev, create the feature/chore branch from dev before the final commit/PR step.
- Feature branch from
dev: git checkout -b feat/area-brief-description dev
- Never push directly to
main or dev
- PR: feature →
dev (approval optional)
dev → main requires 1 approval
PR body template
Include the issue number/link, touched BR-* rules, and any contract-sync notes when relevant.
## Summary
- What was changed and why
- Route group affected: (kiosk) / (dashboard) / shared
## Technical notes
- New API types added: yes/no — list them
- New query keys: list them
- Breaking changes: yes/no
## Test plan
- [ ] `npx tsc --noEmit` — 0 errors
- [ ] `npm run lint` — clean
- [ ] `npm run build` — succeeds
- [ ] Renders at 375px / 768px / 1280px without layout issues
- [ ] Loading / error / empty states all render correctly
- [ ] All mutations show success/error toast
- [ ] Tested manually: describe scenario