Skip to main content

Project management frontend (Next.js)

Type: ReferenceCreated: Team: Platform
draft

About this prompt

This is the exact review prompt the AI reviewer runs for the project management frontend pull requests, applied on top of the shared review rules. Skim it before you raise a PR — most of what it flags is something you can catch and fix yourself first.

# PM Frontend Review Guidelines

> Next.js 15 (App Router) + React 19 + TypeScript (strict) + Apollo Client + GraphQL + Zustand 5 + Formik/Yup + Tailwind 4 + @rootcodelabs/skapp-ui + dnd-kit + i18next + date-fns + Recharts + Sentry

## Architecture

Project management SPA served under the `/pm` basePath. Data flows primarily through **GraphQL** (Apollo Client for queries/mutations/subscriptions) with **Axios** reserved for REST auth endpoints and a custom `fetch` wrapper for AI chat streaming. Real-time updates arrive over **WebSocket** subscriptions via `graphql-ws`.

### Module structure
```
src/
ai/ → AI chat feature module (own components/hooks/store/types/interceptors)
app/ → Next.js App Router pages and layouts
auth/ → Authentication (providers, utils, types, enums)
assets/ → Static images + i18n translation JSON files
components/ → Shared UI (atomic design: atoms / molecules / organisms / skeletons / templates)
constants/ → App-wide constants (shared config values, lookup tables, magic strings)
enums/ → Domain enums (string literal unions used across components and API layer)
graphql/ → GraphQL queries, mutations, subscriptions (gql template literals)
hooks/ → Custom React hooks (wrappers around GraphQL operations, UI behaviors, and shared logic)
icons/ → Project-specific SVG icon components (icons not available in @rootcodelabs/skapp-ui)
lib/ → Core Yup validation schemas for primary entities
providers/ → React Context providers for app-wide features
store/ → Zustand store with slice-based composition
types/ → Shared TypeScript interfaces and type definitions
utils/ → Pure helper functions and shared logic
validations/ → Feature-specific Yup validation schemas
```

### Data layer
```
GraphQL (primary):
Component → Custom Hook (useGet*/useCreate*/useUpdate*/useDelete*)
→ Apollo useQuery / useMutation / useLazyQuery / useSubscription
→ errorLink → authContextLink → splitLink
↳ queries/mutations → httpLink → GraphQL API (NEXT_PUBLIC_PM_API_URL)
↳ subscriptions → wsLink (graphql-ws) → GraphQL API (NEXT_PUBLIC_PM_API_URL)

REST (auth):
authAxios (authInterceptor.ts) — has request/response interceptors
→ adds X-Tenant-ID header, handles token errors
→ Core API (NEXT_PUBLIC_CORE_API_URL)

REST (general):
authFetch (axiosInterceptor.ts) — no interceptors, manual headers via getAuthHeaders()
→ Core API (NEXT_PUBLIC_CORE_API_URL)

Streaming (AI chat):
useChatApi → useChatFetchStream → chatFetch (streamingInterceptor.ts)
→ native fetch with SSE parsing
→ Chat API (NEXT_PUBLIC_CHAT_API_URL)
```

### State management
Single Zustand store composed via the slice pattern with devtools middleware (non-production only). Slices are categorized by responsibility:
- **Server-synced** — current project's reference data (labels, statuses, users, types, fields, priorities, sprints, releases, etc.) populated by `ProjectDataProvider`
- **Form state** — transient form inputs and drafts
- **UI state** — panels, modals, and confirmation dialogs
- **View state** — table configuration and cached view data
- **Feature state** — feature-specific workflows

### Provider hierarchy
```tsx
<AuthProvider> → Authentication state and token management
<ApolloClientProvider> → GraphQL client with auth headers and WebSocket subscriptions
<I18nProvider> → Internationalization
<ProjectDataProvider> → Fetches and syncs server data to Zustand store
<ItemSubscriptionProvider> → Real-time item update subscriptions
<ToastProvider> → Global toast notifications
{children}
```

### Key conventions
- **Path alias**: `@/*` maps to `./src/*` — never use deep relative imports across modules
- **Component files**: PascalCase, directory per component with same-name `.tsx` file
- **No barrel exports**: Direct imports from component files, not `index.ts` re-exports
- **`'use client'` directive**: All interactive components and pages (App Router requirement)
- **UI library**: `@rootcodelabs/skapp-ui` is the shared component library — prefer its components over building custom equivalents
- **CSS variable theming**: Colors via `var(--color-*)` tokens — not hardcoded hex values
- **Typography**: Semantic CSS classes — not arbitrary Tailwind text sizes

---

## What to flag

### Apollo Client / GraphQL misuse
- Direct `fetch` or `axios` calls for data that should go through GraphQL — all project data uses Apollo Client
- Missing `refetchQueries` after mutations that affect cached data (e.g., creating an item without refetching the board query)
- Using `cache-first` fetch policy for data that must be fresh — real-time views should use `cache-and-network` or `network-only`
- `useLazyQuery` without memoizing the execute function via `useCallback`
- Missing error handling on mutations — every mutation must show user feedback (toast) on failure via `showToast({ toastType: ToastType.ERROR, ... })`
- GraphQL queries that fetch fields never used by the component (over-fetching)
- Not using the dynamic query builder pattern (`createProjectItemsByStatusQuery`) when field inclusion should depend on user config (UserItemCardViewConfig)
- Subscription handlers that don't differentiate create/update/delete events (check `beforeUpdate`/`afterUpdate` null patterns)

### Zustand store issues
- Server state duplicated in Zustand that should live in Apollo cache — `projectDataSlice` is the intended bridge, don't add parallel caches
- Mutating state outside `set()` — Zustand requires immutable updates via the setter function
- Adding global state for data that should be local component state or form state (Formik)
- Cross-slice access without proper typing — slice `StateCreator` generics must include all accessed slice types
- Missing devtools middleware on new stores (production guard: `enabled: process.env.NODE_ENV !== 'production'`)
- New slices not registered in the root `store.ts` composition

### Type safety
- `any` types — strict mode is enabled, every type must be explicit
- Missing or incomplete interface definitions for component props
- Using type assertions (`as`) to silence errors instead of fixing the underlying type
- Enum values that don't match GraphQL schema contracts (check against `enums/` definitions)
- GraphQL response types that don't match the actual query shape — variables and response interfaces must align with the gql template

### API layer
- REST calls for operations that have GraphQL equivalents — only auth (`authenticationEndpoints`), PDF generation (`downloadPdfEndpoint`), chat (`chatEndpoints`), and resource availability (`resourceAvailabilityEndpoints`) use REST
- Missing `X-Tenant-ID` header on REST requests — `authInterceptor` adds it automatically, but custom fetch calls must include it
- Missing `x-project-key` header on project-scoped requests — Apollo auth link adds it from context
- Hardcoded API URLs instead of using `NEXT_PUBLIC_*` environment variables
- Missing `withCredentials: true` on auth requests that need cookie transmission
- Not handling token expiry — `getAccessToken()` must be used (auto-refreshes expired tokens)

### Component patterns
- Class components — this codebase is 100% functional components with hooks
- Props drilling through more than 2 levels when a Zustand store, Context provider, or custom hook exists for that data
- Missing `key` prop on list items or using array index as key on reorderable/drag-and-drop lists
- Components doing data fetching AND rendering — separate into container hooks and presentation components
- Large components (>200 lines) that should be decomposed following atomic design
- Components placed at the wrong atomic level (e.g., a complex multi-section component in `atoms/` instead of `organisms/`)
- Missing `'use client'` directive on components that use hooks, state, or browser APIs
- Using DOM APIs without `typeof window !== 'undefined'` guard (SSR safety)

### Drag-and-drop (dnd-kit)
- Missing sensor configuration — all DndContext should use `useSensors` with appropriate sensors (PointerSensor, KeyboardSensor, MouseSensor, TouchSensor depending on context)
- Missing `DragOverlay` for visual feedback during drag operations
- Not using `SortableContext` with the correct sorting strategy (`verticalListSortingStrategy`, `horizontalListSortingStrategy`)
- Custom collision detection without handling the cursor snap offset bug (see `fixCursorSnapOffset` in `dragUtils.ts`)
- Drag handlers that don't disable during edit mode (check `useSortable` `disabled` prop)
- Missing keyboard accessibility in drag-and-drop — `sortableKeyboardCoordinates` should be configured

### Styling
- Hardcoded color values instead of CSS variable tokens (`var(--color-*)`)
- Using Tailwind arbitrary color values (`text-[#ff0000]`) instead of semantic CSS variables
- Missing the predefined typography classes (`h1`, `h3`, `subtitle1`, `body2`, etc.) — don't use arbitrary `text-xl font-bold` combinations
- Inline styles that could be Tailwind classes
- Not using `@rootcodelabs/skapp-ui` components when they exist for the pattern (e.g., rolling a custom modal instead of `SmallModal`, custom input instead of `InputField`)
- Missing responsive handling — check Tailwind responsive prefixes (`sm:`, `md:`, `lg:`)

### Forms (Formik + Yup)
- Form state managed with `useState` instead of Formik — all forms use `useFormik` hook pattern
- Missing Yup validation schemas or incomplete validation rules
- Not using `VALIDATION_CONSTANTS` from `constants/validationConstants.ts` for field length limits
- Custom validation logic that duplicates what Yup already provides
- Missing `enableReinitialize: true` on forms that need to reset when initial values change (e.g., edit forms)
- Form submission without `setSubmitting(false)` in finally block
- Yup schemas that don't use the translation function `t()` for error messages when the pattern calls for it (see `addFieldModalSchema`)
- Validation schemas with uniqueness checks that don't account for the edit case (must exclude current item by ID)

### i18n
- Hardcoded user-facing strings — all text must go through `useTranslation` from `react-i18next`
- Translation keys that don't follow the namespace convention (each feature has its own namespace file)
- New namespace JSON file not registered in the `i18n.ts` resources object
- Missing translations for ARIA labels — accessibility strings go in `assets/languages/en/aria/` files
- Using `t()` from wrong namespace — each `useTranslation('namespace')` call must match the correct JSON file
- Missing Swedish translations when adding new keys (check `sv/` directory)
- Interpolation values not passed to `t()` when the translation key uses `{{variable}}` syntax

### Security & auth
- Route access not protected — all `/projects/*` routes must be behind middleware cookie check
- Admin-only pages (dashboard, project settings) missing `useIsAdmin()` guard with redirect
- Guest employee role (`ROLE_PM_GUEST_EMPLOYEE`) not excluded from features they shouldn't access
- Token or user data logged to console in production — remove `console.log` statements
- User-generated HTML rendered without `DOMPurify.sanitize()` — all HTML from API/user input must go through `sanitizeContent()` from `htmlUtils.ts`
- Missing `DOMPurify` sanitization on rich text editor output before rendering
- Sensitive data stored in localStorage — only project filters and UI state belong there (use `localStorageUtils.ts` patterns)

### Performance
- Missing `useMemo` / `useCallback` on expensive computations or callbacks passed to child components (note: this codebase uses minimal memoization — only flag for genuinely expensive operations)
- Large bundle imports (e.g., importing entire icon sets instead of individual icons from `@rootcodelabs/skapp-ui`)
- Missing loading/skeleton states during async operations — use components from `components/skeletons/`
- Infinite scroll without `useInfiniteScroll` hook (IntersectionObserver pattern)
- Apollo queries fetching data that isn't rendered on the current view

### File uploads
- Direct S3 uploads without going through the signed URL flow (`useGetSignedUrlForUpload` hook)
- Missing file type validation before upload — use `isImageFile()`, `isVideoFile()`, `isFileTypeAllowed()` from `fileUtils.ts`
- Missing file size validation — images max 5MB, videos max 50MB, files max 1000MB (from `uploadUtils.ts`)
- S3 key not following the tenant/project path convention: `{tenantId}/{projectKey}/{type}/{subPath}/{timestamp}_{filename}`
- Returning signed URL instead of CDN URL (`NEXT_PUBLIC_PM_CDN_URL`) to the component

### Real-time subscriptions
- New subscription handlers not registered through `ItemSubscriptionProvider` context
- Subscription connection params missing auth headers (`authorization`, `x-tenant-id`, `x-project-key`)
- Not handling reconnection failures — max reconnect attempts should be bounded
- Subscription event handlers that don't check `beforeUpdate`/`afterUpdate` null patterns for create/update/delete discrimination

### Testing
- New utility functions without corresponding test files in `__tests__/` directories
- Tests that don't use `@testing-library/react` patterns (prefer `screen.getByRole` over `getByTestId`)
- Missing module alias mapping in test files — `@/` alias must resolve via `jest.config.ts` `moduleNameMapper`
- Tests that mock implementation details instead of testing behavior

### Naming & conventions
- Components not in PascalCase
- Hooks not prefixed with `use`
- Enum members not matching string literal pattern (`VALUE = 'VALUE'`)
- New constants not placed in the appropriate `constants/*.ts` file
- GraphQL query/mutation files not in `graphql/queries/` or `graphql/mutations/`
- New hooks not placed in `hooks/` (or `ai/hooks/` for AI-specific hooks)
- Validation schemas not in `lib/validations/` or `validations/`

---

## What NOT to flag (false positives)

- **Zustand + Apollo coexistence** — Zustand manages client/UI state, Apollo manages server/GraphQL cache, `ProjectDataProvider` bridges them intentionally
- **Multiple HTTP clients**`authAxios` (REST auth), `authFetch` (REST utilities), `chatFetch` (streaming), Apollo Client (GraphQL) each serve different purposes
- **`'use client'` on almost every component** — App Router requires this for any component using hooks, state, or browser APIs
- **Minimal `React.memo` / `useMemo` / `useCallback` usage** — the codebase intentionally avoids premature memoization; only flag when there's a measurable performance issue
- **No barrel `index.ts` exports** — direct imports from component files is the convention
- **CSS variables mixed with Tailwind**`var(--color-*)` tokens from skapp-ui theme alongside Tailwind utilities is the standard approach
- **`console.error` in mutation `onError` handlers** — this is acceptable alongside toast notifications
- **Prettier/ESLint fixable issues** (import order, formatting, trailing commas) — `.prettierrc` enforces `singleQuote`, `trailingComma: 'all'`, `endOfLine: 'crlf'`
- **ESLint currently disabled** (`ignores: ['**/*']` in eslint.config.mjs) — this is a known temporary state for CI/CD
- **Zod installed but unused** — it's in `package.json` but no usage exists; Yup is the validation library
- **`basePath: '/pm'`** — all routes are automatically prefixed by Next.js; route constants don't include `/pm`
- **`spring.liquibase.enabled: false`** equivalent patterns — programmatic control is intentional
- **Locize plugin in development only**`locizePlugin` is conditionally loaded for translation management
- **Different audit column patterns between MySQL and PostgreSQL changelogs** — these are intentionally different

---

## Checklist

Verify each item for every changed file:

1. **Type safety**: Any `any`, `as` assertion, or missing prop interface? Every variable, parameter, and return value must have an explicit type. GraphQL response/variable types must match the actual query shape.
2. **Null/undefined safety**: Optional chaining (`?.`) where needed? Check GraphQL response shapes — can fields be `null` at runtime? Are `undefined` checks in place before accessing nested properties from Apollo data?
3. **Apollo Client correctness**: Correct fetch policy for the use case (`cache-and-network` for live views, `cache-first` for static data)? `refetchQueries` specified after mutations that affect cached data? Error handling with `ToastType.ERROR` toast on every mutation failure?
4. **Zustand store usage**: State placed in the correct slice? No server data duplicated outside `projectDataSlice`? Immutable updates via `set()`? Cross-slice type generics correct?
5. **Error handling**: Every mutation has `onError` with user-visible toast? Loading and error states rendered in the UI? Streaming API calls handle HTTP 429 rate limits?
6. **Behavioral preservation in refactors**: When hooks or components are reorganized — same GraphQL query variables? Same fetch policies? Same subscription handlers? Same toast messages? Same redirect logic?
7. **i18n completeness**: Any new user-facing string missing `useTranslation()` wrapping? Translation keys added to the correct namespace JSON file? ARIA labels translated in `aria/` files? New namespace registered in `i18n.ts` resources?
8. **Security**: Admin-only pages gated by `useIsAdmin()` with redirect? Guest employee role excluded where needed? User-generated HTML sanitized with `DOMPurify`? No sensitive data in localStorage or console logs?
9. **Naming conventions**: Components in PascalCase in correct atomic directory? Hooks prefixed with `use` in `hooks/`? Enums in `enums/`? GraphQL in `graphql/queries|mutations|subscriptions/`? Constants in `constants/`?
10. **Dead code**: Unused imports, unreachable branches, state variables set but never read, hooks called but return values unused, GraphQL fields fetched but not rendered?
11. **Drag-and-drop correctness**: `DndContext` with proper sensors? `SortableContext` with correct strategy? `DragOverlay` for visual feedback? Keyboard accessibility configured? Drag disabled during edit mode?
12. **File uploads**: Type and size validated before upload? Using signed URL flow via `useGetSignedUrlForUpload`? CDN URL returned (not signed URL)? S3 key follows `{tenantId}/{projectKey}/{type}/` convention?
13. **Form validation**: Yup schema present and covers all required fields? Uses `VALIDATION_CONSTANTS` for limits? Edit mode accounts for self-exclusion in uniqueness checks? `enableReinitialize` set when needed?
14. **Accessibility**: Interactive elements have `aria-label`? ARIA translations in `aria/*.json`? Skip-to-content landmarks (`#main-content`, `#sidebar`, `#appbar`) preserved? Keyboard navigation works for custom widgets?
15. **Code duplication**: 2+ components with near-identical markup or logic? Extract shared component or custom hook. 2+ GraphQL queries with overlapping fields? Consider shared fragments or the dynamic query builder.