Skip to content

Commit 9d922d5

Browse files
authored
Merge pull request #52 from bartstc/feat/local-server
feat(server): replace Fake Store API with custom local server
2 parents a15412d + c48aad9 commit 9d922d5

102 files changed

Lines changed: 4262 additions & 243 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.agents/skills/building-blocks/SKILL.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
---
22
name: building-blocks
3-
description: Frontend building block catalog — typed patterns for components, hooks, queries, mutations, stores, and models used across the project. Use this skill whenever implementing, reviewing, or planning feature code, API integrations, state management, component patterns, or data modeling within src/features/ or src/lib/. Also triggers for architectural decisions about where code should live or which pattern to apply.
3+
description: Frontend building block catalog — typed patterns for components, hooks, queries, mutations, stores, and models. ALWAYS read the relevant rule file before creating or modifying any building block in src/features/ or src/lib/. This includes spec implementation (read rule files for every block in the Building Blocks Diff), new feature work, refactoring, and architectural decisions.
44
---
55

66
# Frontend Building Blocks

.env

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
VITE_HTTP_LOGGER_DEBUG_MODE=false
2-
VITE_FAKE_STORE_API_HOST=https://fakestoreapi.com
2+
VITE_API=http://localhost:3001/api
33
STORYBOOK_DISABLE_TELEMETRY=1
44
VERSION=v0.0.0
55
BASE_URL=http://localhost:5173

CLAUDE.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ Package manager: PNPM only. Path alias: `@/*` → `src/`.
6161
**6. Spec-Driven Development**
6262

6363
- For features spanning 5+ files or 5+ unconstrained decisions, write a spec first using writing-spec skill
64+
- When implementing a spec, read the building-blocks skill rule files for each block listed in the Building Blocks Diff before writing code
6465
- Skip when the developer opts out
6566

6667
## Conventions

README.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,12 +33,18 @@ An opinionated, production-ready starter for **Single Page Application** develop
3333

3434
### Architecture
3535

36+
```
37+
src/ React SPA (features, pages, lib)
38+
server/ Local Fastify API server (dev/demo)
39+
e2e/ Playwright end-to-end tests
40+
```
41+
3642
- Feature slice architecture with clean architecture principles — each feature has four layers (`components/`, `application/`, `providers/`, `models/`) with strict dependency rules
3743
- Centralized API layer with endpoint-based organization and type consolidation
3844
- Spec-driven development for AI-assisted workflows — four-phase gated spec process, typed building block patterns, and a self-improvement loop
3945
- Formatting utilities for numbers, monetary values, and dates
4046
- File naming: PascalCase for React components/stories/page objects, kebab-case for everything else
41-
- A demo app with authentication showcasing the project structure and tooling in action (powered by [Fake Store API](https://fakestoreapi.com/docs))
47+
- A demo app with authentication showcasing the project structure and tooling in action (powered by a local Fastify server in `server/`)
4248
- Read more: [`docs/architecture.md`](docs/architecture.md)
4349

4450
### AI-assisted development
@@ -70,6 +76,8 @@ It's recommended to run the dev server inside a container for consistent Node/PN
7076
| Command | Description |
7177
| ---------------------- | ------------------------------------------ |
7278
| `pnpm dev` | Dev server with HMR on port `5173` |
79+
| `pnpm dev:server` | Local API server only on port `3001` |
80+
| `pnpm dev:all` | Frontend + API server together |
7381
| `pnpm lint` | Check for lint errors |
7482
| `pnpm build` | Production build |
7583
| `pnpm test` | Run all tests (unit + storybook) |

docs/architecture.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ Each feature follows feature slice architecture patterns with four layers:
5555
- **providers/** - Hook composition and data access gateway for the feature slice. Exposes query hooks, mutations, loaders, domain errors, and DTOs sourced from `src/lib/api/`. Library-specific code (React Query, etc.) must not leak beyond this layer.
5656
- files are named after their primary export or reexport: `useCartProductsQuery``use-cart-products-query.ts`, `useAddToCartMutation``use-add-to-cart-mutation.ts`
5757
- **models/** - Domain type definitions, utilities, and type mapping functions.
58+
- exposes frontend models for the feature. Components, application, and pages import types from `models/`, never directly from `src/lib/api/`. When the DTO shape is identical, a simple re-export with a domain name suffices (`export type { ProductDto as Product }`). When it diverges, map to a dedicated frontend model.
5859

5960
**Dependency rule:** `components/` and `application/` import from `models/` and `providers/`. `providers/` and `models/` have no internal feature dependencies.
6061

@@ -95,6 +96,14 @@ Never use `-command.ts`, `-service.ts`, or other suffixes.
9596

9697
XState is preferred for business processes where states must be explicit and transitions constrained.
9798

99+
## Internationalization
100+
101+
- Translation files live in `public/locales/{lang}/translation.json`
102+
- Keys mirror the feature path e.g. `features.products.<key>`
103+
- All user-facing text goes through i18next — no hardcoded strings in components
104+
- When adding any value that renders as a user-facing label, add the corresponding translation key in the same task
105+
- When removing a value, remove its stale translation key
106+
98107
## Routing
99108

100109
- **File-based routing** - Pages in `src/pages/` with corresponding loaders

e2e/fixtures/credentials-fixture.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,14 @@
11
import type { User } from "@/features/auth/models/user";
22
import { UserFixture } from "@/test-lib/fixtures/user-fixture";
33

4-
// AIDEV-NOTE: Reuses UserFixture from @/test-lib for domain data, adds E2E-specific password field
54
export interface TestCredentials {
65
user: User;
76
password: string;
87
}
98

109
export const ValidUserCredentials: TestCredentials = {
11-
user: UserFixture.createPermutation({ username: "mor_2314" }),
12-
password: "83r5^_",
10+
user: UserFixture.createPermutation({ username: "bob" }),
11+
password: "Pa$$w0rd",
1312
};
1413

1514
export const InvalidUserCredentials: TestCredentials = {

e2e/pages/components/ProductCardComponent.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,11 @@ export class ProductCardComponent {
77
_page: Page,
88
private root: Locator
99
) {
10-
// AIDEV-NOTE: Using semantic locators - no test IDs needed
1110
this.addToCartButton = root.getByRole("button", { name: /add to cart/i });
1211
}
1312

1413
async click(): Promise<void> {
15-
// AIDEV-NOTE: Click the product card image area to navigate to product details
16-
// The Box with background image has onClick handler for navigation
17-
await this.root.locator("div").first().click();
14+
await this.root.getByRole("img").click();
1815
}
1916

2017
async addToCart(): Promise<void> {

e2e/pages/index.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,24 @@ import { ProductDetailsPage } from "@e2e/pages/products/ProductDetailsPage";
77
import { ProductListPage } from "@e2e/pages/products/ProductListPage";
88

99
interface PageFixtures {
10+
resetDb: void;
1011
signInPage: SignInPage;
1112
productListPage: ProductListPage;
1213
productDetailsPage: ProductDetailsPage;
1314
cartPage: CartPage;
1415
}
1516

17+
const API_BASE = process.env.VITE_API ?? "http://localhost:3001/api";
18+
1619
export const test = base.extend<PageFixtures>({
20+
// AIDEV-NOTE: Auto fixture — resets DB to seed state before every test for isolation
21+
resetDb: [
22+
async ({ request }, use) => {
23+
await request.post(`${API_BASE}/test/reset`);
24+
await use();
25+
},
26+
{ auto: true },
27+
],
1728
signInPage: async ({ page }, use) => {
1829
await use(new SignInPage(page));
1930
},

e2e/pages/products/ProductDetailsPage.ts

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -26,16 +26,12 @@ export class ProductDetailsPage extends BasePage {
2626
});
2727
}
2828

29-
private async goto(productId: string): Promise<this> {
30-
await this.page.goto(`/products/${productId}`, {
31-
waitUntil: "networkidle",
32-
});
33-
return this;
34-
}
35-
3629
async gotoFirstProduct(): Promise<this> {
37-
// AIDEV-NOTE: Navigate to first product (ID = 1) as E2E uses real API data
38-
return this.goto("1");
30+
// AIDEV-NOTE: Navigate via products list to avoid hardcoding ID format
31+
await this.page.goto("/products", { waitUntil: "networkidle" });
32+
await this.page.getByRole("article").first().getByRole("img").click();
33+
await this.page.waitForURL(/\/products\/.+/, { waitUntil: "networkidle" });
34+
return this;
3935
}
4036

4137
async addToCart(): Promise<this> {

e2e/pages/products/ProductListPage.ts

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,24 +21,20 @@ export class ProductListPage extends BasePage {
2121

2222
async waitForProductsToLoad(): Promise<void> {
2323
// AIDEV-NOTE: Wait for at least one "Add to cart" button to appear
24-
// This indicates products have loaded, without needing a test ID on the grid
24+
// This indicates products have loaded
2525
await this.page
2626
.getByRole("button", { name: /add to cart/i })
2727
.first()
2828
.waitFor({ state: "visible" });
2929
}
3030

3131
async getProductCount(): Promise<number> {
32-
// AIDEV-NOTE: Count product cards by counting <article> elements
33-
// Each ProductCard renders as a semantic <article> element
34-
const cards = await this.page.locator("article").all();
32+
const cards = await this.page.getByRole("article").all();
3533
return cards.length;
3634
}
3735

3836
private getProductCard(index = 0): ProductCardComponent {
39-
// AIDEV-NOTE: Get product card by index (default: first product)
40-
// E2E tests use real API data, so we can't rely on specific product names
41-
const cardRoot = this.page.locator("article").nth(index);
37+
const cardRoot = this.page.getByRole("article").nth(index);
4238

4339
return new ProductCardComponent(this.page, cardRoot);
4440
}

0 commit comments

Comments
 (0)