This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in node_modules/next/dist/docs/ before writing any code. Heed deprecation notices.
CODESTYLE.md is the highest-priority convention document for this repository.
- If any rule in this file conflicts with
CODESTYLE.md, followCODESTYLE.md. - Keep naming, exports, file boundaries, and function styles compliant with
CODESTYLE.md. - Do not introduce patterns forbidden by
CODESTYLE.md(for example invalid entry files or forbidden barrel usage in app code).
Use this structure as the default for all new product-detail related work.
styles/methods/app/features/components/hooks/interfaces/types/enums/classes/ui/(shadcn/ui)e2e/
Folder names must use camelCase.
Pages are route entry points and should focus on composition.
app/
product/
[id]/
page.tsxpage.tsx responsibilities:
- Layouting
- Component usage/composition
- Initial data fetching if necessary
Each feature owns one vertical slice and may receive data from its parent or fetch data by itself.
features/
homeHero/
header/
homePostPreview/Each feature follows this internal layout:
features/<featureName>/
index.tsx # entry component to the feature
docs/
homeHeroSpec.md
homeHeroTodo.md
components/ # flat list only (no nested component folders)
hooks/ # flat list only
methods/ # flat list only
constants/ # flat list only
actions/ # flat list only
tests/ # flat list only
i18n/Feature folder names and nested folders must use camelCase.
Feature entrypoint export convention:
- Use default export for the feature component
- Consumers must import feature APIs via default import (for example
import HomeHero from '@/features/homeHero').
Features are independent vertical slices. They must not know about each other at the code level. This rule is enforced
to keep feature boundaries clean, to make features independently testable and replaceable, and to keep the dependency
graph a directed acyclic graph with app/* at the top and features/* as leaves.
- Features must not import from other features. A file under
src/features/<featureA>/**must not reference anything fromsrc/features/<featureB>/**. This includes components, hooks, types, models, server actions, i18n keys, and test utilities. - When two features need to interact, do the composition at the page/layout level under
src/app/*. The composing page/layout is the only place that is allowed to import from multiple features and wire them together. - Use slot/prop injection when one feature needs to render UI that conceptually belongs to another. The receiving feature exposes a named slot prop (for example
cartSlot?: ReactNode) on its entrypoint and renders the injected node as-is without introspecting it. The composing page/layout fetches any data the injected node needs and passes a fully-formed node (for example<CartDropdown initial={...} />) into the slot. - Cross-feature end-to-end flows are covered by tests in
src/e2e/, not by one feature importing another feature's components to test them together. - If a piece of code is genuinely needed by more than one feature, it does not belong inside a feature. Lift it to:
src/ui/for shared shadcn UI primitives,src/components/for other shared UI components,src/methods/for pure helper methods,src/hooks/for helper hooks,src/constants/for shared constants,src/interfaces/for shared interfaces,src/types/for shared utility or union types,
How to recognize a violation:
- Any
import ... from '@/features/<otherFeature>'inside a file undersrc/features/<thisFeature>/**. - A feature reading another feature's i18n namespace directly.
- A feature importing a hook, server action, or type that physically lives inside another feature folder.
All of the above require refactoring: either move the shared concern up a layer (components/methods/hooks/types) or move the composition up to the page/layout and inject via props.
src/ui/contains shared shadcn-based UI primitives.- Features should use the shadcn-based components from
src/ui/for consistent styling and behavior. - If a feature needs a component which another feature also needs, the component can be extracted to the global
src/componentsfolder
- Components: one component per file, filename matches component name, default export, and use named
functiondeclarations (no component arrow-function exports). - Component props: define a local
Propsinterface at the top of the file unless shared viasrc/interfaces/. - Methods outside components: one method per file, default export, named
functiondeclaration. - Methods inside components: arrow functions are allowed and preferred.
- Custom hooks: one hook per file, filename matches hook name, default export, named
functiondeclaration, and hook names start withuse. - Interfaces: one interface per file, PascalCase name, default export, and no
Iprefixes. - Enums: define enum first, then
export default <EnumName>.
- Translations are done using the
next-intlpackage with its corresponding hooks and components. - Each feature has its own
i18n/folder for translations specific to that feature. - Add a
[language-key like de].jsonfile for each supported language (currently only de.json) for each feature. - Import the translation file in the
src/methods/i18n/request.tsfile to make it available for the application. - The translations which are written inside the json files need to be inside an object with the key of the feature folder name, for example:
{
"productDescription": {
"label": "Produktbeschreibung",
"loading": "Lädt Produktbeschreibung..."
}
}- Keep feature tests in
features/<featureName>/tests/. - Use vitest for feature tests, focusing on unit and integration tests for feature components, hooks, and services.
- Use Playwright for end-to-end tests in
e2e/. - Keep cross-feature end-to-end coverage in
e2e/.
- Component test files use PascalCase matching the component name:
ProductBuyPanelClient.tsxis tested bytests/ProductBuyPanelClient.test.tsx. - Function / mapper / reducer / hook / server-action test files use camelCase matching the exported symbol:
mapCartToCartPageViewModel.test.ts,cartDropdownReducer.test.ts,addToCart.test.ts,useOptimisticCart.test.tsx. - Accessibility tests live in
tests/accessibility.test.tsx(no prefix, no per-form copy). See the "Accessibility Tests" subsection below. - Shared test utilities live in
tests/testUtils.tsxnext to the tests that consume them. See the "Shared testUtils.tsx" subsection below. - Component-source files also use PascalCase (
ProductBuyPanelClient.tsx, neverproductBuyPanelClient.tsx). Feature entrypoints stayindex.tsx.
Different test categories have different conventions because they exercise different kinds of contracts.
Files like mapXToY.test.ts, *Reducer.test.ts, parseHashError.test.ts, addToCart.test.ts. These MUST follow the table-driven style below. The full cart-side and mapper suites already model this.
- Wrap the unit under test in a top-level
describe('<symbolName>'). - Inside that block, split coverage into
describe('Success Cases', ...)anddescribe('Error Cases', ...). - Use
test.each([...])from vitest for every case (notit.each). - Define cases as an array of objects with
name,input, andexpectedkeys whenever possible. For error cases, keepnameandinput, and useexpectedErroronly when asserting a thrown error message is clearer than wrapping it inexpected. - Keep each case object limited to properties relevant to that scenario.
- In
Success Cases, assert returned values or state changes (for example.toEqual(),.toMatchObject(), or.toBe()). - In
Error Cases, assert thrown errors explicitly (.toThrow(),.rejects.toThrow(), or{ ok: false, errorKey: ... }result shapes). - Keep assertions in
test.eachclean and avoidif/elsebranching in test logic. - Use interpolated variables like
$nameor$expectedintest.eachdescriptions for readable test runner output.
Files like ProductBuyPanelClient.test.tsx, LoginForm.test.tsx, Header.test.tsx. The table-driven style is still preferred wherever the assertions of multiple cases share the same shape, but UI flows with distinct setup/interaction/assertion steps are allowed to use free-form it(...) blocks.
- Prefer
test.eachfor data-driven component scenarios: variant-by-state rendering, input-to-output mapping, routing-by-prop.LoginForm.test.tsxandHeader.test.tsxare the reference examples. - Use free-form
it(...)ortest(...)blocks when the scenario is a distinct user flow (fireEventsequence, async waitFor, pending-state, multi-step interaction). Pending-state,aria-busy, and toast-side-effect checks typically do not fit cleanly intotest.each. - When a component test grows past ~5 cases of the same shape, lift them into a
test.eacheven if thedescribewrapper doesn't strictly match Success/Error. - The Success/Error split is encouraged for component tests too when the boundary is meaningful (server-action success vs failure paths, validated vs invalid form submission), but is not strictly required.
- Accessibility coverage lives in
tests/accessibility.test.tsxinside each feature that has one. - Structure: one top-level
describe('<feature> accessibility smoke', ...)containing one nesteddescribe('<Component>', ...)per component-under-test, each with focusedtest(...)blocks. - NEVER consolidate multiple components into a single monolithic
it(...)— regressions must localise to the specific component that broke. cartDropdown/tests/accessibility.test.tsxandauthentication/tests/accessibility.test.tsxare the reference examples.
Every test that renders a component using next-intl MUST mock next-intl with the namespace-prefix variant:
vi.mock('next-intl', () => ({
useTranslations: (namespace?: string) => (key: string) =>
namespace ? `${namespace}.${key}` : key
}))- Assertions read
getByRole('heading', { name: 'cartDropdown.title' })— the translation key path is self-documenting at the assertion site. - Tests do not assert German copy, so translation-string changes do not cascade into test breakage.
- Components that call
useTranslations()without a namespace (passing a fully-qualified key tot()) work transparently because the factory falls through to the bare key. - Components that do interpolation with
t('key', { values })can use an extended mock that handles the secondvaluesargument (seeheader/tests/Header.test.tsx). This is the one sanctioned deviation. - Do NOT load
de.jsonat test time and interpolate against it. Tests must not couple to translation copy.
Server-action tests (addToCart.test.ts, resetPasswordAction.test.ts, cartDropdown/tests/serverActions.test.ts) follow this pattern:
- Use
vi.hoisted(() => ({...}))for shared mock references that are referenced insidevi.mockfactories. - When the action imports a repository that touches
fetcher/env vars at module load, declare a localFakeXRepositoryError extends Errorclass and register it viavi.mock('@/repositories/xRepository', () => ({ XRepositoryError: FakeXRepositoryError, ... }))so the real repository module never evaluates. - Import the action lazily inside each test via
await import('../serverActions/<action>'). Lazy import ensures the action resolves AFTER allvi.mockcalls have been hoisted and registered. - Mock
next/cache(revalidatePath) andnext/navigation(redirect) where the action calls them. Forredirect, use a mock that throws (throw new Error('REDIRECT:${path}')) and assert via.rejects.toThrow(...)because the realredirect()signals control flow by throwing. - Keep the Success / Error split with
test.eachas for other pure-logic tests.
For route-level 404s triggered from a page, call notFound() from next/navigation instead of rendering fallback UI inline.
- Prefer server-safe data access paths for initial page load.
- Keep page files thin; move logic to feature hooks/services.
- Do not call external APIs using a fetch directly from features, always use the available
api/*services or add new ones as needed. - Use the components provided inside the
ui/folder for consistent styling; avoid ad-hoc styles as much as possible.
Every feature change must reach both the desktop AND the mobile experience before it can be considered done. Mobile is never a follow-up.
- When you add, change, or remove functionality for a feature, explicitly verify the mobile surface (drawer, sheet, responsive variants) and update it in the same change.
- When extending a view-model contract (new data fields, new navigation types, new interactive affordances), update every component that consumes that contract — desktop AND mobile — even if only one of them is mentioned in the user request.
- When adapting data from a new or changed API response, mobile rendering must consume the same normalized/sanitized view model as desktop. Never branch mobile-specific mapping logic away from the shared data source.
- In feature docs, keep
### Desktopand### Mobilebehaviour sections in lockstep. If a behaviour is desktop-only or mobile-only, state that explicitly in both sections with the rationale. - Tests for a changed feature must cover both surfaces when both surfaces are affected (e.g.
<FeatureDesktop />and<FeatureMobileDrawer />tests, or viewport-specifice2ecoverage). - Before marking a task complete, ask yourself: "Does this change also land on the mobile view?" If the answer is no or unclear, the work is not done.
- Use
features/<featureName>/docs/<featureName>Spec.mdfor a concise functional specification only. - Functional specs must focus on user outcomes and behavior (goal, scope, flows, states, acceptance criteria).
- Do not include technical implementation documentation in feature specs (folder structures, DTO/view-model shapes, API/service wiring, test strategy, commands, or code-level guardrails).
- Use
features/<featureName>/docs/<featureName>Todo.mdonly for open work items that are actionable. - Keep technical implementation notes in code, PR descriptions, or TODO items when needed.
- When a TODO is implemented, mark it as checked in
<featureName>Todo.mdand reflect the final behavior in<featureName>Spec.mdin the same change. - Keep completed checklist items in TODO docs as checked entries for traceability; do not delete them.
- Lint passes with the repository standard (
pnpm run lint) (the linting is type aware so never runtscon your own). - Formatting is compliant with the repository standard (
pnpm run fmt). - Unused code is removed using knip (
pnpm run knip). - Relevant tests pass (feature-level tests and/or
e2ecoverage for changed user flows). - Documentation is updated when behavior or architecture contracts change.
- Changes respect the architecture rules in this
AGENTS.md(page scope, feature boundaries, api/utilities ownership).