├── .gitignore
├── app
├── backend
│ ├── .python-version
│ ├── app
│ │ ├── __init__.py
│ │ ├── data
│ │ │ ├── __init__.py
│ │ │ └── seed_products.py
│ │ ├── api
│ │ │ ├── __init__.py
│ │ │ └── products.py
│ │ ├── core
│ │ │ ├── __init__.py
│ │ │ ├── config.py
│ │ │ └── logging_config.py
│ │ ├── models
│ │ │ ├── __init__.py
│ │ │ ├── error.py
│ │ │ └── product.py
│ │ ├── services
│ │ │ ├── __init__.py
│ │ │ └── product_service.py
│ │ └── main.py
│ ├── tests
│ │ ├── __init__.py
│ │ ├── conftest.py
│ │ ├── test_products_basic.py
│ │ └── test_products_filtering.py
│ ├── run_api.py
│ ├── .gitignore
│ └── pyproject.toml
└── frontend
│ ├── bunfig.toml
│ ├── src
│ ├── lib
│ │ ├── utils.ts
│ │ ├── logger.ts
│ │ └── api-client.ts
│ ├── react.svg
│ ├── index.html
│ ├── components
│ │ ├── ui
│ │ │ ├── label.tsx
│ │ │ ├── input.tsx
│ │ │ ├── card.tsx
│ │ │ ├── button.tsx
│ │ │ ├── form.tsx
│ │ │ └── select.tsx
│ │ ├── ProductGrid.tsx
│ │ └── ProductCard.tsx
│ ├── frontend.tsx
│ ├── index.css
│ ├── index.tsx
│ ├── types
│ │ ├── error.ts
│ │ └── product.ts
│ ├── APITester.tsx
│ ├── logo.svg
│ └── App.tsx
│ ├── bun-env.d.ts
│ ├── tsconfig.json
│ ├── components.json
│ ├── .gitignore
│ ├── package.json
│ ├── biome.json
│ ├── styles
│ └── globals.css
│ ├── build.ts
│ └── bun.lock
├── rules-and-commands
├── commands
│ ├── execute.md
│ ├── prime.md
│ ├── start-server.md
│ ├── commit.md
│ ├── prp-review.md
│ ├── noqa.md
│ └── planning.md
└── rules
│ └── CLAUDE.md
├── tasks
├── TASK1.md
└── TASK2.md
├── SETUP_GUIDE.md
├── exercises
├── exercise_1.md
├── exercise_2.md
└── exercise_3.md
└── README.md
/.gitignore:
--------------------------------------------------------------------------------
1 | logs
2 |
--------------------------------------------------------------------------------
/app/backend/.python-version:
--------------------------------------------------------------------------------
1 | 3.12
2 |
--------------------------------------------------------------------------------
/app/backend/app/__init__.py:
--------------------------------------------------------------------------------
1 | """Product catalog API application package."""
2 |
--------------------------------------------------------------------------------
/app/backend/tests/__init__.py:
--------------------------------------------------------------------------------
1 | """Tests for the product catalog API."""
2 |
--------------------------------------------------------------------------------
/app/backend/app/data/__init__.py:
--------------------------------------------------------------------------------
1 | """Sample data for the product catalog."""
2 |
--------------------------------------------------------------------------------
/app/backend/app/api/__init__.py:
--------------------------------------------------------------------------------
1 | """API route handlers for the product catalog."""
2 |
--------------------------------------------------------------------------------
/app/backend/app/core/__init__.py:
--------------------------------------------------------------------------------
1 | """Core application configuration and utilities."""
2 |
--------------------------------------------------------------------------------
/app/backend/app/models/__init__.py:
--------------------------------------------------------------------------------
1 | """Data models for the product catalog API."""
2 |
--------------------------------------------------------------------------------
/app/backend/app/services/__init__.py:
--------------------------------------------------------------------------------
1 | """Business logic services for the product catalog API."""
2 |
--------------------------------------------------------------------------------
/app/frontend/bunfig.toml:
--------------------------------------------------------------------------------
1 | [serve.static]
2 | plugins = ["bun-plugin-tailwind"]
3 | env = "BUN_PUBLIC_*"
4 |
--------------------------------------------------------------------------------
/app/frontend/src/lib/utils.ts:
--------------------------------------------------------------------------------
1 | import { type ClassValue, clsx } from "clsx";
2 | import { twMerge } from "tailwind-merge";
3 |
4 | export function cn(...inputs: ClassValue[]) {
5 | return twMerge(clsx(inputs));
6 | }
7 |
--------------------------------------------------------------------------------
/app/frontend/src/react.svg:
--------------------------------------------------------------------------------
1 |
9 |
--------------------------------------------------------------------------------
/app/backend/run_api.py:
--------------------------------------------------------------------------------
1 | """
2 | Development server runner for the product catalog API.
3 |
4 | Run this script to start the development server:
5 | python run_api.py
6 | """
7 |
8 | import uvicorn
9 |
10 | if __name__ == "__main__":
11 | uvicorn.run(
12 | "app.main:app",
13 | host="0.0.0.0",
14 | port=8000,
15 | reload=True,
16 | log_level="info"
17 | )
18 |
--------------------------------------------------------------------------------
/app/frontend/bun-env.d.ts:
--------------------------------------------------------------------------------
1 | // Generated by `bun init`
2 |
3 | declare module "*.svg" {
4 | /**
5 | * A path to the SVG file
6 | */
7 | const path: `${string}.svg`;
8 | export = path;
9 | }
10 |
11 | declare module "*.module.css" {
12 | /**
13 | * A record of class names to their corresponding CSS module classes
14 | */
15 | const classes: { readonly [key: string]: string };
16 | export = classes;
17 | }
18 |
--------------------------------------------------------------------------------
/app/backend/.gitignore:
--------------------------------------------------------------------------------
1 | # Python
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 | *.so
6 | .Python
7 |
8 | # Virtual environments
9 | .venv/
10 | venv/
11 | ENV/
12 | env/
13 |
14 | # UV
15 | uv.lock
16 |
17 | # Testing
18 | .pytest_cache/
19 | .coverage
20 | htmlcov/
21 |
22 | # IDEs
23 | .vscode/
24 | .idea/
25 | *.swp
26 | *.swo
27 | *~
28 |
29 | # OS
30 | .DS_Store
31 | Thumbs.db
32 |
33 | .ruff
34 | .ruff_cache
35 | .ruff_cache/
36 |
--------------------------------------------------------------------------------
/app/frontend/src/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Bun + React
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/app/frontend/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "jsx": "react-jsx",
4 | "allowJs": true,
5 |
6 | // Bundler mode
7 | "moduleResolution": "bundler",
8 | "module": "Preserve",
9 | "allowImportingTsExtensions": true,
10 | "verbatimModuleSyntax": true,
11 | "noEmit": true,
12 |
13 | "baseUrl": ".",
14 | "paths": {
15 | "@/*": ["./src/*"]
16 | }
17 | },
18 | "include": ["**/*.ts", "**/*.tsx"],
19 | "exclude": ["dist", "node_modules"]
20 | }
21 |
--------------------------------------------------------------------------------
/app/frontend/components.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://ui.shadcn.com/schema.json",
3 | "style": "new-york",
4 | "rsc": false,
5 | "tsx": true,
6 | "tailwind": {
7 | "config": "",
8 | "css": "styles/globals.css",
9 | "baseColor": "zinc",
10 | "cssVariables": true,
11 | "prefix": ""
12 | },
13 | "aliases": {
14 | "components": "@/components",
15 | "utils": "@/lib/utils",
16 | "ui": "@/components/ui",
17 | "lib": "@/lib",
18 | "hooks": "@/hooks"
19 | },
20 | "iconLibrary": "lucide"
21 | }
22 |
--------------------------------------------------------------------------------
/app/frontend/.gitignore:
--------------------------------------------------------------------------------
1 | # dependencies (bun install)
2 | node_modules
3 |
4 | # output
5 | out
6 | dist
7 | *.tgz
8 |
9 | # code coverage
10 | coverage
11 | *.lcov
12 |
13 | # logs
14 | logs
15 | _.log
16 | report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
17 |
18 | # dotenv environment variable files
19 | .env
20 | .env.development.local
21 | .env.test.local
22 | .env.production.local
23 | .env.local
24 |
25 | # caches
26 | .eslintcache
27 | .cache
28 | *.tsbuildinfo
29 |
30 | # IntelliJ based IDEs
31 | .idea
32 |
33 | # Finder (MacOS) folder config
34 | .DS_Store
35 |
--------------------------------------------------------------------------------
/app/frontend/src/components/ui/label.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as LabelPrimitive from "@radix-ui/react-label";
4 | import type * as React from "react";
5 |
6 | import { cn } from "@/lib/utils";
7 |
8 | function Label({ className, ...props }: React.ComponentProps) {
9 | return (
10 |
18 | );
19 | }
20 |
21 | export { Label };
22 |
--------------------------------------------------------------------------------
/app/backend/tests/conftest.py:
--------------------------------------------------------------------------------
1 | """
2 | Pytest configuration and shared test fixtures.
3 |
4 | This module provides reusable test fixtures for all test files.
5 | """
6 |
7 | import pytest
8 | from fastapi.testclient import TestClient
9 | from app.main import app
10 |
11 |
12 | @pytest.fixture
13 | def test_client() -> TestClient:
14 | """
15 | Provide a FastAPI TestClient for making HTTP requests in tests.
16 |
17 | Returns:
18 | TestClient instance configured with the FastAPI app
19 |
20 | Example:
21 | def test_get_products(test_client):
22 | response = test_client.get("/api/products")
23 | assert response.status_code == 200
24 | """
25 | return TestClient(app)
26 |
--------------------------------------------------------------------------------
/app/frontend/src/frontend.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * This file is the entry point for the React app, it sets up the root
3 | * element and renders the App component to the DOM.
4 | *
5 | * It is included in `src/index.html`.
6 | */
7 |
8 | import { StrictMode } from "react";
9 | import { createRoot } from "react-dom/client";
10 | import { App } from "./App";
11 |
12 | const elem = document.getElementById("root")!;
13 | const app = (
14 |
15 |
16 |
17 | );
18 |
19 | if (import.meta.hot) {
20 | // With hot module reloading, `import.meta.hot.data` is persisted.
21 | const root = (import.meta.hot.data.root ??= createRoot(elem));
22 | root.render(app);
23 | } else {
24 | // The hot module reloading API is not available in production.
25 | createRoot(elem).render(app);
26 | }
27 |
--------------------------------------------------------------------------------
/app/backend/app/core/config.py:
--------------------------------------------------------------------------------
1 | """Application configuration settings."""
2 |
3 | from pydantic_settings import BaseSettings
4 |
5 |
6 | class ApplicationSettings(BaseSettings):
7 | """
8 | Application-wide configuration settings.
9 |
10 | These settings can be overridden via environment variables.
11 | For example, LOG_LEVEL=DEBUG will set the logging level to DEBUG.
12 |
13 | Attributes:
14 | application_name: Display name of the application
15 | application_version: Semantic version number
16 | log_level: Logging level (DEBUG, INFO, WARNING, ERROR, CRITICAL)
17 | enable_cors: Whether to enable CORS (Cross-Origin Resource Sharing)
18 | """
19 |
20 | application_name: str = "Product Catalog API"
21 | application_version: str = "0.1.0"
22 | log_level: str = "INFO"
23 | enable_cors: bool = True
24 |
25 |
26 | # Global settings instance
27 | settings = ApplicationSettings()
28 |
--------------------------------------------------------------------------------
/rules-and-commands/commands/execute.md:
--------------------------------------------------------------------------------
1 | # Execute PRP Plan
2 |
3 | Implement a feature plan from the PRPs directory by following its Step by Step Tasks section.
4 |
5 | ## Variables
6 |
7 | Plan file: $ARGUMENTS
8 |
9 | ## Instructions
10 |
11 | - Read the entire plan file carefully
12 | - Execute **every step** in the "Step by Step Tasks" section in order, top to bottom
13 | - Follow the "Testing Strategy" to create proper unit and integration tests
14 | - Complete all "Validation Commands" at the end
15 | - Ensure all linters pass and all tests pass before finishing
16 | - Follow CLAUDE.md guidelines for type safety, logging, and docstrings
17 |
18 | ## When done
19 |
20 | - Move the PRP file to the completed directory in PRPs/features/completed
21 |
22 | ## Report
23 |
24 | - Summarize completed work in a concise bullet point list
25 | - Show files and lines changed: `git diff --stat`
26 | - Confirm all validation commands passed
27 | - Note any deviations from the plan (if any)
28 |
--------------------------------------------------------------------------------
/app/frontend/src/index.css:
--------------------------------------------------------------------------------
1 | @import "../styles/globals.css";
2 |
3 | @layer base {
4 | :root {
5 | @apply font-sans;
6 | }
7 |
8 | body {
9 | @apply grid place-items-center min-w-[320px] min-h-screen relative m-0 bg-background text-foreground;
10 | }
11 | }
12 |
13 | /* cool Bun background animation 😎 */
14 | body::before {
15 | content: "";
16 | position: fixed;
17 | inset: 0;
18 | z-index: -1;
19 | opacity: 0.05;
20 | background: url("./logo.svg");
21 | background-size: 256px;
22 | transform: rotate(-12deg) scale(1.35);
23 | animation: slide 30s linear infinite;
24 | pointer-events: none;
25 | }
26 |
27 | @keyframes slide {
28 | from {
29 | background-position: 0 0;
30 | }
31 | to {
32 | background-position: 256px 224px;
33 | }
34 | }
35 |
36 | @keyframes spin {
37 | from {
38 | transform: rotate(0);
39 | }
40 | to {
41 | transform: rotate(360deg);
42 | }
43 | }
44 |
45 | @media (prefers-reduced-motion) {
46 | *,
47 | ::before,
48 | ::after {
49 | animation: none !important;
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/app/frontend/src/index.tsx:
--------------------------------------------------------------------------------
1 | import { serve } from "bun";
2 | import index from "./index.html";
3 |
4 | const server = serve({
5 | routes: {
6 | // Serve index.html for all unmatched routes.
7 | "/*": index,
8 |
9 | "/api/hello": {
10 | async GET(req) {
11 | return Response.json({
12 | message: "Hello, world!",
13 | method: "GET",
14 | });
15 | },
16 | async PUT(req) {
17 | return Response.json({
18 | message: "Hello, world!",
19 | method: "PUT",
20 | });
21 | },
22 | },
23 |
24 | "/api/hello/:name": async (req) => {
25 | const name = req.params.name;
26 | return Response.json({
27 | message: `Hello, ${name}!`,
28 | });
29 | },
30 | },
31 |
32 | development: process.env.NODE_ENV !== "production" && {
33 | // Enable browser hot reloading in development
34 | hmr: true,
35 |
36 | // Echo console logs from the browser to the server
37 | console: true,
38 | },
39 | });
40 |
41 | console.log(`🚀 Server running at ${server.url}`);
42 |
--------------------------------------------------------------------------------
/tasks/TASK1.md:
--------------------------------------------------------------------------------
1 | # [FEAT-1234] Add Product Filtering to Catalog API
2 |
3 | ## Description
4 |
5 | Users need to filter and search products in the catalog API. Currently `GET /api/products` returns all 30 products without filtering capabilities.
6 |
7 | ## Requirements
8 |
9 | Add filtering and search capabilities to `GET /api/products`:
10 |
11 | - **Price filtering**: Support minimum and maximum price filters
12 | - **Category filtering**: Allow filtering by product category
13 | - **Keyword search**: Search product names and descriptions
14 | - **Sorting**: Enable sorting by price or name (both directions)
15 |
16 | All filters should be optional and work together when combined.
17 |
18 | ## Acceptance Criteria
19 |
20 | - [ ] All filtering tests pass
21 | - [ ] Invalid inputs return appropriate HTTP 400 errors
22 | - [ ] Backwards compatible (no filters = all products)
23 | - [ ] Follows existing code patterns and conventions
24 |
25 | ## Technical Notes
26 |
27 | - Validate query parameters
28 | - Use appropriate types for monetary values
29 | - Log filter operations
30 |
31 | ## Definition of Done
32 |
33 | - All acceptance criteria met
34 | - All Tests passing
35 | `uv run pytest`
36 |
--------------------------------------------------------------------------------
/rules-and-commands/commands/prime.md:
--------------------------------------------------------------------------------
1 | # Prime
2 |
3 | Execute the following sections to understand the codebase before starting new work, then summarize your understanding.
4 |
5 | ## Run
6 |
7 | - List all tracked files: `git ls-files`
8 | - Show project structure: `tree -I '.venv|__pycache__|*.pyc|.pytest_cache|.mypy_cache|.ruff_cache' -L 3`
9 |
10 | ## Read
11 |
12 | - `CLAUDE.md` - Core project instructions, principles, logging rules, testing requirements
13 | - `app/backend/README.md` - Project overview and setup (if exists)
14 | - `app/frontend/README.md` - Project overview and setup (if exists)
15 |
16 | - Identify core files in both backend and frontend and read them
17 |
18 | ## Report
19 |
20 | Provide a concise summary of:
21 |
22 | 1. **Project Purpose**: What this application does
23 | 2. **Architecture**: Key patterns (vertical slice, FastAPI + Pydantic AI)
24 | 3. **Core Principles**: TYPE SAFETY, KISS, YAGNI
25 | 4. **Tech Stack**: Main dependencies and tools
26 | 5. **Key Requirements**: Logging, testing, type annotations
27 | 6. **Current State**: What's implemented
28 |
29 | Keep the summary brief (5-10 bullet points) and focused on what you need to know to contribute effectively.
30 |
--------------------------------------------------------------------------------
/rules-and-commands/commands/start-server.md:
--------------------------------------------------------------------------------
1 | # Start Servers
2 |
3 | Start both the FastAPI backend and React frontend development servers with hot reload.
4 |
5 | ## Run
6 |
7 | ### Run in the background with bash tool
8 |
9 | - Ensure you are in the right PWD
10 | - Use the Bash tool to run the servers in the background so you can read the shell outputs
11 | - IMPORTANT: run `git ls-files` first so you know where directories are located before you start
12 |
13 | ### Backend Server (FastAPI)
14 |
15 | - Navigate to backend: `cd app/backend`
16 | - Start server in background: `uv sync && uv run python run_api.py`
17 | - Wait 2-3 seconds for startup
18 | - Test health endpoint: `curl http://localhost:8000/health`
19 | - Test products endpoint: `curl http://localhost:8000/api/products`
20 |
21 | ### Frontend Server (Bun + React)
22 |
23 | - Navigate to frontend: `cd ../app/frontend`
24 | - Start server in background: `bun install && bun dev`
25 | - Wait 2-3 seconds for startup
26 | - Frontend should be accessible at `http://localhost:3000`
27 |
28 | ## Report
29 |
30 | - Confirm backend is running on `http://localhost:8000`
31 | - Confirm frontend is running on `http://localhost:3000`
32 | - Show the health check response from backend
33 | - Mention: "Backend logs will show structured JSON logging for all requests"
34 |
--------------------------------------------------------------------------------
/app/frontend/src/components/ui/input.tsx:
--------------------------------------------------------------------------------
1 | import type * as React from "react";
2 |
3 | import { cn } from "@/lib/utils";
4 |
5 | function Input({ className, type, ...props }: React.ComponentProps<"input">) {
6 | return (
7 |
16 | );
17 | }
18 |
19 | export { Input };
20 |
--------------------------------------------------------------------------------
/app/frontend/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "product-catalog-frontend",
3 | "version": "0.1.0",
4 | "description": "E-commerce product catalog frontend - Module 1 Exercise",
5 | "private": true,
6 | "type": "module",
7 | "main": "src/index.tsx",
8 | "module": "src/index.tsx",
9 | "scripts": {
10 | "dev": "bun --hot src/index.tsx",
11 | "start": "NODE_ENV=production bun src/index.tsx",
12 | "build": "bun run build.ts",
13 | "lint": "bunx biome lint src/",
14 | "lint:fix": "bunx biome lint --write src/",
15 | "format": "bunx biome format src/",
16 | "format:fix": "bunx biome format --write src/",
17 | "check": "bunx biome check src/",
18 | "check:fix": "bunx biome check --write src/",
19 | "ci": "bunx biome ci src/"
20 | },
21 | "dependencies": {
22 | "@hookform/resolvers": "^4.1.0",
23 | "@radix-ui/react-label": "^2.1.2",
24 | "@radix-ui/react-select": "^2.1.6",
25 | "@radix-ui/react-slot": "^1.1.2",
26 | "bun-plugin-tailwind": "^0.0.14",
27 | "class-variance-authority": "^0.7.1",
28 | "clsx": "^2.1.1",
29 | "lucide-react": "^0.475.0",
30 | "react": "^19",
31 | "react-dom": "^19",
32 | "react-hook-form": "^7.54.2",
33 | "tailwind-merge": "^3.0.1",
34 | "tailwindcss": "^4.0.6",
35 | "tailwindcss-animate": "^1.0.7",
36 | "zod": "^3.24.2"
37 | },
38 | "devDependencies": {
39 | "@biomejs/biome": "2.2.6",
40 | "@types/bun": "latest",
41 | "@types/react": "^19",
42 | "@types/react-dom": "^19"
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/app/frontend/src/components/ui/card.tsx:
--------------------------------------------------------------------------------
1 | import type * as React from "react";
2 |
3 | import { cn } from "@/lib/utils";
4 |
5 | function Card({ className, ...props }: React.ComponentProps<"div">) {
6 | return (
7 |
12 | );
13 | }
14 |
15 | function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
16 | return ;
17 | }
18 |
19 | function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
20 | return (
21 |
22 | );
23 | }
24 |
25 | function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
26 | return ;
27 | }
28 |
29 | function CardContent({ className, ...props }: React.ComponentProps<"div">) {
30 | return ;
31 | }
32 |
33 | function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
34 | return ;
35 | }
36 |
37 | export { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle };
38 |
--------------------------------------------------------------------------------
/app/backend/app/services/product_service.py:
--------------------------------------------------------------------------------
1 | """
2 | Product service containing business logic for product operations.
3 |
4 | This service layer separates business logic from API routing logic,
5 | making the code more testable and maintainable.
6 | """
7 |
8 | from app.core.logging_config import StructuredLogger
9 | from app.data.seed_products import get_seed_products
10 | from app.models.product import Product
11 |
12 | # Initialize structured logger for this module
13 | logger = StructuredLogger(__name__)
14 |
15 | # In-memory product storage (in a real app, this would be a database)
16 | _PRODUCTS_DATABASE: list[Product] = get_seed_products()
17 |
18 |
19 | def get_all_products() -> list[Product]:
20 | """
21 | Retrieve all products from the catalog.
22 |
23 | This function returns all available products without any filtering.
24 | It logs the operation for debugging and monitoring purposes.
25 |
26 | Returns:
27 | List of all Product objects in the catalog
28 |
29 | Example:
30 | >>> products = get_all_products()
31 | >>> len(products)
32 | 30
33 | >>> products[0].product_name
34 | 'Wireless Bluetooth Mouse'
35 | """
36 | logger.info(
37 | "retrieving_all_products", total_products_in_database=len(_PRODUCTS_DATABASE), operation="get_all_products"
38 | )
39 |
40 | logger.info(
41 | "products_retrieved_successfully", products_returned=len(_PRODUCTS_DATABASE), operation="get_all_products"
42 | )
43 |
44 | return _PRODUCTS_DATABASE
45 |
--------------------------------------------------------------------------------
/rules-and-commands/commands/commit.md:
--------------------------------------------------------------------------------
1 | # Create Git Commit
2 |
3 | Create an atomic git commit with a properly formatted commit message following best practices for the uncommited changes or these specific files if specified.
4 |
5 | Specific files (skip if not specified):
6 |
7 | - File 1: $1
8 | - File 2: $2
9 | - File 3: $3
10 | - File 4: $4
11 | - File 5: $5
12 |
13 | ## Instructions
14 |
15 | **Commit Message Format:**
16 |
17 | - Use conventional commits: `: `
18 | - Types: `feat`, `fix`, `docs`, `style`, `refactor`, `test`, `chore`
19 | - Present tense (e.g., "add", "fix", "update", not "added", "fixed", "updated")
20 | - 50 characters or less for the subject line
21 | - Lowercase subject line
22 | - No period at the end
23 | - Be specific and descriptive
24 |
25 | **Examples:**
26 |
27 | - `feat: add web search tool with structured logging`
28 | - `fix: resolve type errors in middleware`
29 | - `test: add unit tests for config module`
30 | - `docs: update CLAUDE.md with testing guidelines`
31 | - `refactor: simplify logging configuration`
32 | - `chore: update dependencies`
33 |
34 | **Atomic Commits:**
35 |
36 | - One logical change per commit
37 | - If you've made multiple unrelated changes, consider splitting into separate commits
38 | - Commit should be self-contained and not break the build
39 |
40 | **IMPORTANT**
41 |
42 | - NEVER mention claude code, anthropic, co authored by or anything similar in the commit messages
43 |
44 | ## Run
45 |
46 | 1. Review changes: `git diff HEAD`
47 | 2. Check status: `git status`
48 | 3. Stage changes: `git add -A`
49 | 4. Create commit: `git commit -m ": "`
50 |
51 | ## Report
52 |
53 | - Output the commit message used
54 | - Confirm commit was successful with commit hash
55 | - List files that were committed
56 |
--------------------------------------------------------------------------------
/app/backend/app/models/error.py:
--------------------------------------------------------------------------------
1 | """Error response models for consistent API error handling."""
2 |
3 | from datetime import UTC, datetime
4 | from typing import Any
5 |
6 | from pydantic import BaseModel, Field
7 |
8 |
9 | class ErrorResponse(BaseModel):
10 | """
11 | Standard error response format for all API errors.
12 |
13 | This model ensures consistent error messaging across the entire API,
14 | making it easier for both humans and AI to understand what went wrong.
15 |
16 | Attributes:
17 | error_code: Machine-readable error identifier (e.g., 'invalid_price_range')
18 | error_message: Human-readable error message for end users
19 | error_details: Optional dictionary with additional debugging context
20 | timestamp_utc: ISO 8601 formatted timestamp when the error occurred
21 |
22 | Examples:
23 | >>> ErrorResponse(
24 | ... error_code="invalid_price_range",
25 | ... error_message="Minimum price cannot exceed maximum price",
26 | ... error_details={"min_price": "100.00", "max_price": "50.00"}
27 | ... )
28 | """
29 |
30 | error_code: str = Field(
31 | ...,
32 | description="Machine-readable error code for programmatic handling",
33 | examples=["invalid_price_range", "product_not_found", "validation_error"],
34 | min_length=1,
35 | max_length=100,
36 | )
37 |
38 | error_message: str = Field(
39 | ...,
40 | description="Human-readable error message suitable for displaying to end users",
41 | min_length=1,
42 | max_length=500,
43 | )
44 |
45 | error_details: dict[str, Any] | None = Field(
46 | default=None, description="Additional context about the error for debugging purposes"
47 | )
48 |
49 | timestamp_utc: str = Field(
50 | default_factory=lambda: datetime.now(UTC).isoformat(),
51 | description="ISO 8601 timestamp when the error occurred (UTC timezone)",
52 | )
53 |
--------------------------------------------------------------------------------
/tasks/TASK2.md:
--------------------------------------------------------------------------------
1 | # [FEAT-1235] Add Product Filtering UI to Frontend
2 |
3 | ## Description
4 |
5 | Backend filtering capabilities are now available on `GET /api/products` with query parameters. Users need a frontend interface to interact with these filters and see filtered results.
6 |
7 | ## Requirements
8 |
9 | Build a product filtering interface that connects to the backend filtering API:
10 |
11 | - **Price range controls**: Allow users to set minimum and maximum price filters
12 | - **Category selector**: Enable filtering by product category
13 | - **Search input**: Provide keyword search for product names and descriptions
14 | - **Sort controls**: Allow users to sort results by price or name
15 | - **Filter management**: Provide ability to apply and clear filters
16 |
17 | The interface should handle loading states, empty results, and validation errors appropriately.
18 |
19 | ## Acceptance Criteria
20 |
21 | - [ ] Price filtering interface works and sends correct query parameters
22 | - [ ] Category selection filters products correctly
23 | - [ ] Search input filters products by keyword
24 | - [ ] Sort options reorder products as expected
25 | - [ ] Multiple filters work together (combined filtering)
26 | - [ ] Clear filters returns to showing all products
27 | - [ ] Validation errors are displayed to users
28 | - [ ] Empty state shown when no products match filters
29 | - [ ] Loading state shown during filter operations
30 | - [ ] All filter interactions logged to console (structured JSON)
31 |
32 | ## Technical Notes
33 |
34 | - Update API client to accept filter parameters
35 | - Build query string from filter values
36 | - Use React Hook Form and Zod for form validation
37 | - Follow existing logging patterns (structured JSON)
38 | - Use existing shadcn components where possible
39 |
40 | ## Definition of Done
41 |
42 | - All acceptance criteria met
43 | - Backend API called with correct query parameters
44 | - Follows existing code patterns and conventions
45 | - Manual testing checklist completed
46 |
--------------------------------------------------------------------------------
/app/backend/pyproject.toml:
--------------------------------------------------------------------------------
1 | [project]
2 | name = "product-catalog-api"
3 | version = "0.1.0"
4 | description = "E-commerce product catalog API - Module 1 Exercise"
5 | requires-python = ">=3.12"
6 | dependencies = [
7 | "fastapi>=0.118.0",
8 | "httpx>=0.28.1",
9 | "pydantic>=2.11.10",
10 | "pydantic-settings>=2.11.0",
11 | "pytest>=8.4.2",
12 | "ruff>=0.8.4",
13 | "uvicorn>=0.37.0",
14 | ]
15 |
16 | [build-system]
17 | requires = ["hatchling"]
18 | build-backend = "hatchling.build"
19 |
20 | [tool.hatch.build.targets.wheel]
21 | packages = ["app"]
22 |
23 | # Ruff configuration for linting and formatting
24 | [tool.ruff]
25 | line-length = 120
26 | target-version = "py312"
27 |
28 | # Enable auto-fixing where possible
29 | fix = true
30 |
31 | # Exclude common directories
32 | exclude = [
33 | ".venv",
34 | ".git",
35 | "__pycache__",
36 | ".pytest_cache",
37 | "*.egg-info",
38 | ]
39 |
40 | [tool.ruff.lint]
41 | # Enable essential rule sets for clean, maintainable code
42 | select = [
43 | "E", # pycodestyle errors
44 | "W", # pycodestyle warnings
45 | "F", # pyflakes
46 | "I", # isort (import sorting)
47 | "N", # pep8-naming
48 | "UP", # pyupgrade (modernize Python code)
49 | "B", # flake8-bugbear (find likely bugs)
50 | "C4", # flake8-comprehensions
51 | "SIM", # flake8-simplify
52 | "RUF", # Ruff-specific rules
53 | ]
54 |
55 | # Ignore specific rules that conflict with our patterns
56 | ignore = [
57 | "E501", # Line too long (handled by formatter)
58 | ]
59 |
60 | [tool.ruff.lint.isort]
61 | # Import sorting configuration
62 | known-first-party = ["app"]
63 | section-order = ["future", "standard-library", "third-party", "first-party", "local-folder"]
64 |
65 | [tool.ruff.format]
66 | # Use double quotes for strings
67 | quote-style = "double"
68 | # Indent with spaces
69 | indent-style = "space"
70 | # Respect magic trailing comma
71 | skip-magic-trailing-comma = false
72 | # Automatically detect line ending style
73 | line-ending = "auto"
74 |
--------------------------------------------------------------------------------
/app/frontend/src/components/ui/button.tsx:
--------------------------------------------------------------------------------
1 | import { Slot } from "@radix-ui/react-slot";
2 | import { cva, type VariantProps } from "class-variance-authority";
3 | import type * as React from "react";
4 |
5 | import { cn } from "@/lib/utils";
6 |
7 | const buttonVariants = cva(
8 | "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-[color,box-shadow] disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:shrink-0 ring-ring/10 dark:ring-ring/20 dark:outline-ring/40 outline-ring/50 focus-visible:ring-4 focus-visible:outline-1 aria-invalid:focus-visible:ring-0",
9 | {
10 | variants: {
11 | variant: {
12 | default: "bg-primary text-primary-foreground shadow-sm hover:bg-primary/90",
13 | destructive: "bg-destructive text-destructive-foreground shadow-xs hover:bg-destructive/90",
14 | outline: "border border-input bg-background shadow-xs hover:bg-accent hover:text-accent-foreground",
15 | secondary: "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
16 | ghost: "hover:bg-accent hover:text-accent-foreground",
17 | link: "text-primary underline-offset-4 hover:underline",
18 | },
19 | size: {
20 | default: "h-9 px-4 py-2 has-[>svg]:px-3",
21 | sm: "h-8 rounded-md px-3 has-[>svg]:px-2.5",
22 | lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
23 | icon: "size-9",
24 | },
25 | },
26 | defaultVariants: {
27 | variant: "default",
28 | size: "default",
29 | },
30 | }
31 | );
32 |
33 | function Button({
34 | className,
35 | variant,
36 | size,
37 | asChild = false,
38 | ...props
39 | }: React.ComponentProps<"button"> &
40 | VariantProps & {
41 | asChild?: boolean;
42 | }) {
43 | const Comp = asChild ? Slot : "button";
44 |
45 | return ;
46 | }
47 |
48 | export { Button, buttonVariants };
49 |
--------------------------------------------------------------------------------
/app/backend/app/api/products.py:
--------------------------------------------------------------------------------
1 | """
2 | Product API endpoints.
3 |
4 | This module defines all HTTP endpoints related to product operations.
5 | Each endpoint delegates business logic to the service layer.
6 | """
7 |
8 | from fastapi import APIRouter
9 |
10 | from app.core.logging_config import StructuredLogger
11 | from app.models.product import ProductListResponse
12 | from app.services import product_service
13 |
14 | # Initialize router for product endpoints
15 | router = APIRouter(prefix="/api/products", tags=["products"])
16 |
17 | # Initialize structured logger
18 | logger = StructuredLogger(__name__)
19 |
20 |
21 | @router.get("", response_model=ProductListResponse)
22 | async def get_products() -> ProductListResponse:
23 | """
24 | Get all products from the catalog.
25 |
26 | This endpoint returns all products currently available in the catalog.
27 | In the future, this endpoint will support filtering by price, category,
28 | and keyword search (that's what you'll be adding in the exercise!).
29 |
30 | Returns:
31 | ProductListResponse containing list of products and total count
32 |
33 | Example Response:
34 | {
35 | "products": [
36 | {
37 | "product_id": 1,
38 | "product_name": "Wireless Bluetooth Mouse",
39 | "product_description": "Ergonomic wireless mouse...",
40 | "product_price_usd": "29.99",
41 | "product_category": "electronics",
42 | "product_in_stock": true
43 | },
44 | ...
45 | ],
46 | "total_count": 30
47 | }
48 | """
49 | logger.info("api_request_received", endpoint="/api/products", http_method="GET", operation="get_products")
50 |
51 | # Delegate to service layer for business logic
52 | products = product_service.get_all_products()
53 |
54 | logger.info(
55 | "api_response_prepared", endpoint="/api/products", products_count=len(products), operation="get_products"
56 | )
57 |
58 | return ProductListResponse(products=products, total_count=len(products))
59 |
--------------------------------------------------------------------------------
/app/frontend/src/types/error.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Error types matching backend Pydantic ErrorResponse model EXACTLY.
3 | *
4 | * Backend definition (app/models/error.py):
5 | * ```python
6 | * class ErrorResponse(BaseModel):
7 | * error_code: str = Field(..., min_length=1, max_length=100)
8 | * error_message: str = Field(..., min_length=1, max_length=500)
9 | * error_details: dict[str, Any] | None = Field(default=None)
10 | * timestamp_utc: str = Field(default_factory=lambda: datetime.now(UTC).isoformat())
11 | * ```
12 | */
13 |
14 | /**
15 | * Error response model matching backend ErrorResponse.
16 | *
17 | * All API errors follow this consistent structure.
18 | */
19 | export interface ErrorResponse {
20 | /** Machine-readable error code (e.g., "invalid_price_range", "product_not_found") */
21 | error_code: string;
22 |
23 | /** Human-readable error message for end users */
24 | error_message: string;
25 |
26 | /** Optional additional context for debugging */
27 | error_details?: Record;
28 |
29 | /** ISO 8601 timestamp when error occurred (UTC) */
30 | timestamp_utc: string;
31 | }
32 |
33 | /**
34 | * Custom error class for API errors with structured information.
35 | *
36 | * Wraps ErrorResponse from backend with HTTP status code.
37 | * Use this for proper error handling in try-catch blocks.
38 | *
39 | * Example:
40 | * ```typescript
41 | * try {
42 | * await fetchProducts();
43 | * } catch (error) {
44 | * if (error instanceof ApiError) {
45 | * console.error(error.errorResponse.error_code);
46 | * console.error(error.statusCode);
47 | * }
48 | * }
49 | * ```
50 | */
51 | export class ApiError extends Error {
52 | constructor(
53 | /** HTTP status code (e.g., 400, 404, 500) */
54 | public readonly statusCode: number,
55 | /** Structured error response from backend */
56 | public readonly errorResponse: ErrorResponse
57 | ) {
58 | super(errorResponse.error_message);
59 | this.name = "ApiError";
60 |
61 | // Maintains proper stack trace for where error was thrown (V8 engines only)
62 | if (Error.captureStackTrace) {
63 | Error.captureStackTrace(this, ApiError);
64 | }
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/app/frontend/src/APITester.tsx:
--------------------------------------------------------------------------------
1 | import { type FormEvent, useRef } from "react";
2 | import { Button } from "@/components/ui/button";
3 | import { Input } from "@/components/ui/input";
4 | import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
5 | import { cn } from "@/lib/utils";
6 |
7 | export function APITester() {
8 | const responseInputRef = useRef(null);
9 |
10 | const testEndpoint = async (e: FormEvent) => {
11 | e.preventDefault();
12 |
13 | try {
14 | const form = e.currentTarget;
15 | const formData = new FormData(form);
16 | const endpoint = formData.get("endpoint") as string;
17 | const url = new URL(endpoint, location.href);
18 | const method = formData.get("method") as string;
19 | const res = await fetch(url, { method });
20 |
21 | const data = await res.json();
22 | responseInputRef.current!.value = JSON.stringify(data, null, 2);
23 | } catch (error) {
24 | responseInputRef.current!.value = String(error);
25 | }
26 | };
27 |
28 | return (
29 |
30 |
60 |
61 |
72 |
73 | );
74 | }
75 |
--------------------------------------------------------------------------------
/app/frontend/src/components/ProductGrid.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * ProductGrid component for displaying a responsive grid of products.
3 | *
4 | * Handles three states:
5 | * 1. Loading - Shows spinner and loading message
6 | * 2. Empty - Shows message when no products match criteria
7 | * 3. Success - Shows grid of ProductCard components
8 | *
9 | * Layout:
10 | * - 1 column on mobile
11 | * - 2 columns on tablets (md breakpoint)
12 | * - 3 columns on desktop (lg breakpoint)
13 | */
14 |
15 | import type { Product } from "@/types/product";
16 | import { ProductCard } from "./ProductCard";
17 |
18 | interface ProductGridProps {
19 | /** Array of products to display */
20 | products: Product[];
21 |
22 | /** Whether products are currently being loaded from API */
23 | loading: boolean;
24 | }
25 |
26 | /**
27 | * Display products in a responsive grid layout.
28 | *
29 | * @param products - Products array from ProductListResponse
30 | * @param loading - Loading state from parent component
31 | */
32 | export function ProductGrid({ products, loading }: ProductGridProps) {
33 | // Loading state - show spinner
34 | if (loading) {
35 | return (
36 |
37 |
38 | {/* Animated spinner */}
39 |
40 |
Loading products...
41 |
42 |
43 | );
44 | }
45 |
46 | // Empty state - no products found
47 | if (products.length === 0) {
48 | return (
49 |
50 |
51 | {/* Empty state illustration */}
52 |
66 |
No products found
67 |
Try adjusting your filters or check back later for new products.
68 |
69 |
70 | );
71 | }
72 |
73 | // Success state - show product grid
74 | return (
75 |
76 | {products.map((product) => (
77 |
78 | ))}
79 |
80 | );
81 | }
82 |
--------------------------------------------------------------------------------
/app/backend/app/main.py:
--------------------------------------------------------------------------------
1 | """
2 | Main FastAPI application entry point.
3 |
4 | This module initializes the FastAPI app, configures middleware,
5 | sets up logging, and registers all API routers.
6 | """
7 |
8 | from collections.abc import AsyncIterator
9 | from contextlib import asynccontextmanager
10 |
11 | from fastapi import FastAPI
12 | from fastapi.middleware.cors import CORSMiddleware
13 |
14 | from app.api import products
15 | from app.core.config import settings
16 | from app.core.logging_config import StructuredLogger, setup_logging
17 |
18 | # Configure structured JSON logging
19 | setup_logging(log_level=settings.log_level)
20 |
21 | # Initialize logger for this module
22 | logger = StructuredLogger(__name__)
23 |
24 |
25 | @asynccontextmanager
26 | async def application_lifespan(app: FastAPI) -> AsyncIterator[None]:
27 | """
28 | Manage application lifespan events (startup and shutdown).
29 |
30 | This context manager handles initialization on startup and cleanup on shutdown,
31 | logging both events for monitoring and debugging purposes.
32 |
33 | Args:
34 | app: The FastAPI application instance
35 |
36 | Yields:
37 | None: Control flow during application runtime
38 | """
39 | # Startup: Log application initialization
40 | logger.info(
41 | "application_startup",
42 | application_name=settings.application_name,
43 | application_version=settings.application_version,
44 | log_level=settings.log_level,
45 | cors_enabled=settings.enable_cors,
46 | )
47 |
48 | yield
49 |
50 | # Shutdown: Log application termination
51 | logger.info("application_shutdown", application_name=settings.application_name)
52 |
53 |
54 | # Create FastAPI application instance with lifespan handler
55 | app = FastAPI(
56 | title=settings.application_name,
57 | version=settings.application_version,
58 | description="E-commerce product catalog API with filtering and search capabilities",
59 | lifespan=application_lifespan,
60 | )
61 |
62 | # Configure CORS (Cross-Origin Resource Sharing) if enabled
63 | if settings.enable_cors:
64 | app.add_middleware(
65 | CORSMiddleware,
66 | allow_origins=["*"], # In production, specify exact origins
67 | allow_credentials=True,
68 | allow_methods=["*"],
69 | allow_headers=["*"],
70 | )
71 | logger.info("cors_middleware_enabled", allow_origins="*", allow_methods="*")
72 |
73 | # Register API routers
74 | app.include_router(products.router)
75 | logger.info("api_router_registered", router_prefix="/api/products", router_tag="products")
76 |
77 |
78 | @app.get("/health")
79 | async def health_check() -> dict[str, str]:
80 | """
81 | Health check endpoint for monitoring and load balancers.
82 |
83 | Returns:
84 | Dictionary with status indicating the application is running
85 |
86 | Example Response:
87 | {"status": "healthy"}
88 | """
89 | logger.info("health_check_request", endpoint="/health")
90 |
91 | return {"status": "healthy"}
92 |
--------------------------------------------------------------------------------
/app/frontend/biome.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://biomejs.dev/schemas/2.2.6/schema.json",
3 | "vcs": {
4 | "enabled": true,
5 | "clientKind": "git",
6 | "useIgnoreFile": true
7 | },
8 | "files": {
9 | "ignoreUnknown": false,
10 | "includes": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.js", "src/**/*.jsx"]
11 | },
12 | "overrides": [
13 | {
14 | "includes": ["src/APITester.tsx", "src/frontend.tsx", "src/index.tsx"],
15 | "linter": {
16 | "rules": {
17 | "style": {
18 | "noNonNullAssertion": "off"
19 | },
20 | "suspicious": {
21 | "noAssignInExpressions": "off"
22 | },
23 | "correctness": {
24 | "noUnusedFunctionParameters": "off"
25 | }
26 | }
27 | }
28 | }
29 | ],
30 | "formatter": {
31 | "enabled": true,
32 | "indentStyle": "space",
33 | "indentWidth": 2,
34 | "lineWidth": 120
35 | },
36 | "javascript": {
37 | "formatter": {
38 | "quoteStyle": "double",
39 | "trailingCommas": "es5",
40 | "semicolons": "always",
41 | "arrowParentheses": "always",
42 | "bracketSpacing": true,
43 | "jsxQuoteStyle": "double"
44 | }
45 | },
46 | "linter": {
47 | "enabled": true,
48 | "rules": {
49 | "recommended": true,
50 | "a11y": {
51 | "recommended": true
52 | },
53 | "complexity": {
54 | "recommended": true,
55 | "noExtraBooleanCast": "error",
56 | "noUselessCatch": "error",
57 | "noUselessConstructor": "error",
58 | "noUselessLoneBlockStatements": "error",
59 | "noUselessRename": "error",
60 | "noVoid": "error",
61 | "useLiteralKeys": "error",
62 | "useOptionalChain": "error"
63 | },
64 | "correctness": {
65 | "recommended": true,
66 | "noUnusedVariables": "error",
67 | "useExhaustiveDependencies": "warn",
68 | "useHookAtTopLevel": "error"
69 | },
70 | "style": {
71 | "recommended": true,
72 | "noNegationElse": "error",
73 | "noParameterAssign": "warn",
74 | "useBlockStatements": "error",
75 | "useCollapsedElseIf": "error",
76 | "useConsistentArrayType": "error",
77 | "useExponentiationOperator": "error",
78 | "useFragmentSyntax": "error",
79 | "useShorthandAssign": "error",
80 | "useSingleVarDeclarator": "error",
81 | "useTemplate": "error"
82 | },
83 | "suspicious": {
84 | "recommended": true,
85 | "noArrayIndexKey": "warn",
86 | "noConfusingVoidType": "error",
87 | "noConsole": "off",
88 | "noDuplicateObjectKeys": "error",
89 | "noExplicitAny": "warn"
90 | }
91 | }
92 | },
93 | "assist": {
94 | "enabled": true,
95 | "actions": {
96 | "source": {
97 | "organizeImports": "on"
98 | }
99 | }
100 | }
101 | }
102 |
--------------------------------------------------------------------------------
/app/frontend/src/types/product.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Product types matching backend Pydantic models EXACTLY.
3 | *
4 | * Backend definitions (app/models/product.py):
5 | * - ProductCategory = Literal["electronics", "clothing", "home", "sports", "books"]
6 | * - Product model with strict field types
7 | * - ProductListResponse wrapper
8 | *
9 | * IMPORTANT: These types must stay in sync with backend models.
10 | * Any changes to backend models require updating these types.
11 | */
12 |
13 | /**
14 | * Product category enum matching backend Literal type.
15 | *
16 | * Backend: ProductCategory = Literal["electronics", "clothing", "home", "sports", "books"]
17 | */
18 | export type ProductCategory = "electronics" | "clothing" | "home" | "sports" | "books";
19 |
20 | /**
21 | * Product model matching backend Pydantic Product model.
22 | *
23 | * Backend definition:
24 | * ```python
25 | * class Product(BaseModel):
26 | * product_id: int = Field(..., gt=0)
27 | * product_name: str = Field(..., min_length=1, max_length=200)
28 | * product_description: str = Field(..., min_length=1, max_length=1000)
29 | * product_price_usd: Decimal = Field(..., gt=0, decimal_places=2)
30 | * product_category: ProductCategory
31 | * product_in_stock: bool = Field(default=True)
32 | * ```
33 | *
34 | * Note: Decimal from backend is serialized as string in JSON
35 | */
36 | export interface Product {
37 | /** Unique product identifier (always positive integer) */
38 | product_id: number;
39 |
40 | /** Display name of the product (1-200 characters) */
41 | product_name: string;
42 |
43 | /** Detailed product description (1-1000 characters) */
44 | product_description: string;
45 |
46 | /** Price in USD as string (Decimal serialized from backend with 2 decimal places) */
47 | product_price_usd: string;
48 |
49 | /** Product category (one of 5 valid categories) */
50 | product_category: ProductCategory;
51 |
52 | /** Whether product is currently available for purchase */
53 | product_in_stock: boolean;
54 | }
55 |
56 | /**
57 | * Product list response matching backend ProductListResponse model.
58 | *
59 | * Backend definition:
60 | * ```python
61 | * class ProductListResponse(BaseModel):
62 | * products: list[Product] = Field(...)
63 | * total_count: int = Field(..., ge=0)
64 | * ```
65 | */
66 | export interface ProductListResponse {
67 | /** Array of product objects */
68 | products: Product[];
69 |
70 | /** Total number of products in response (always >= 0) */
71 | total_count: number;
72 | }
73 |
74 | /**
75 | * Product filter parameters (for future implementation by students).
76 | *
77 | * These match the backend ProductFilterParameters that students will create.
78 | * Currently not implemented - will be added during the exercise.
79 | */
80 | export interface ProductFilterParams {
81 | /** Filter products with price >= this amount */
82 | minimum_price_usd?: number;
83 |
84 | /** Filter products with price <= this amount */
85 | maximum_price_usd?: number;
86 |
87 | /** Filter by specific category */
88 | category?: ProductCategory;
89 |
90 | /** Search keyword in product name/description (case-insensitive) */
91 | search_keyword?: string;
92 |
93 | /** Sort order for results */
94 | sort_by?: "price_asc" | "price_desc" | "name_asc" | "name_desc";
95 | }
96 |
--------------------------------------------------------------------------------
/app/backend/tests/test_products_basic.py:
--------------------------------------------------------------------------------
1 | """
2 | Basic tests for the products API (before filtering is added).
3 |
4 | These tests verify the basic GET /api/products endpoint works correctly.
5 | They demonstrate the testing pattern you should follow when implementing
6 | the filtering functionality.
7 | """
8 |
9 | from fastapi.testclient import TestClient
10 |
11 |
12 | def test_get_all_products_returns_200(test_client: TestClient) -> None:
13 | """
14 | Test that GET /api/products returns HTTP 200 status code.
15 |
16 | This is the most basic test - just checking the endpoint is reachable
17 | and returns a successful response.
18 | """
19 | response = test_client.get("/api/products")
20 |
21 | assert response.status_code == 200
22 |
23 |
24 | def test_get_all_products_returns_correct_structure(test_client: TestClient) -> None:
25 | """
26 | Test that GET /api/products returns the expected JSON structure.
27 |
28 | The response should have:
29 | - "products": list of product objects
30 | - "total_count": integer count of products
31 | """
32 | response = test_client.get("/api/products")
33 | data = response.json()
34 |
35 | # Verify response structure
36 | assert "products" in data
37 | assert "total_count" in data
38 |
39 | # Verify types
40 | assert isinstance(data["products"], list)
41 | assert isinstance(data["total_count"], int)
42 |
43 |
44 | def test_get_all_products_returns_30_products(test_client: TestClient) -> None:
45 | """
46 | Test that GET /api/products returns all 30 seed products.
47 |
48 | Since we have 30 products in our seed data, we should get all of them
49 | when no filters are applied.
50 | """
51 | response = test_client.get("/api/products")
52 | data = response.json()
53 |
54 | assert data["total_count"] == 30
55 | assert len(data["products"]) == 30
56 |
57 |
58 | def test_product_objects_have_required_fields(test_client: TestClient) -> None:
59 | """
60 | Test that each product object contains all required fields.
61 |
62 | Every product should have:
63 | - product_id
64 | - product_name
65 | - product_description
66 | - product_price_usd
67 | - product_category
68 | - product_in_stock
69 | """
70 | response = test_client.get("/api/products")
71 | data = response.json()
72 |
73 | products = data["products"]
74 | assert len(products) > 0 # Make sure we have products to test
75 |
76 | # Check first product has all fields (spot check)
77 | first_product = products[0]
78 | required_fields = [
79 | "product_id",
80 | "product_name",
81 | "product_description",
82 | "product_price_usd",
83 | "product_category",
84 | "product_in_stock"
85 | ]
86 |
87 | for field in required_fields:
88 | assert field in first_product
89 |
90 |
91 | def test_health_check_endpoint(test_client: TestClient) -> None:
92 | """
93 | Test that the /health endpoint works correctly.
94 |
95 | This is a simple sanity check to ensure the app is running.
96 | """
97 | response = test_client.get("/health")
98 |
99 | assert response.status_code == 200
100 | assert response.json() == {"status": "healthy"}
101 |
--------------------------------------------------------------------------------
/rules-and-commands/commands/prp-review.md:
--------------------------------------------------------------------------------
1 | # Code Review
2 |
3 | Review implemented work against a PRP specification to ensure code quality, correctness, and adherence to project standards.
4 |
5 | ## Variables
6 |
7 | Plan file: $ARGUMENTS (e.g., `PRPs/features/add-web-search.md`)
8 |
9 | ## Instructions
10 |
11 | **Understand the Changes:**
12 |
13 | - Check current branch: `git branch`
14 | - Review changes: `git diff origin/main` (or `git diff HEAD` if not on a branch)
15 | - Read the PRP plan file to understand requirements
16 |
17 | **Code Quality Review:**
18 |
19 | - **Type Safety**: Verify all functions have type annotations, mypy passes
20 | - **Logging**: Check structured logging is used correctly (event names, context, exception handling)
21 | - **Docstrings**: Ensure Google-style docstrings on all functions/classes
22 | - **Testing**: Verify unit tests exist for all new files, integration tests if needed
23 | - **Architecture**: Confirm vertical slice structure is followed
24 | - **CLAUDE.md Compliance**: Check adherence to core principles (KISS, YAGNI, TYPE SAFETY)
25 |
26 | **Validation Ruff for BE and Biome for FE:**
27 |
28 | - Run linters: `uv run ruff check src/ && uv run mypy src/`
29 | - Run tests: `uv run pytest tests/ -v`
30 | - Start server and test endpoints with curl (if applicable)
31 | - Verify structured logs show proper correlation IDs and context
32 |
33 | **Issue Severity:**
34 |
35 | - `blocker` - Must fix before merge (breaks build, missing tests, type errors, security issues)
36 | - `major` - Should fix (missing logging, incomplete docstrings, poor patterns)
37 | - `minor` - Nice to have (style improvements, optimization opportunities)
38 |
39 | ## Report
40 |
41 | Return ONLY valid JSON (no markdown, no explanations) save to [report-#.json] in prps/reports directory create the directory if it doesn't exist. Output will be parsed with JSON.parse().
42 |
43 | ### Output Structure
44 |
45 | ```json
46 | {
47 | "success": "boolean - true if NO BLOCKER issues, false if BLOCKER issues exist",
48 | "review_summary": "string - 2-4 sentences: what was built, does it match spec, quality assessment",
49 | "review_issues": [
50 | {
51 | "issue_number": "number - issue index",
52 | "file_path": "string - file with the issue (if applicable)",
53 | "issue_description": "string - what's wrong",
54 | "issue_resolution": "string - how to fix it",
55 | "severity": "string - blocker|major|minor"
56 | }
57 | ],
58 | "validation_results": {
59 | "linting_passed": "boolean",
60 | "type_checking_passed": "boolean",
61 | "tests_passed": "boolean",
62 | "api_endpoints_tested": "boolean - true if endpoints were tested with curl"
63 | }
64 | }
65 | ```
66 |
67 | ## Example Success Review
68 |
69 | ```json
70 | {
71 | "success": true,
72 | "review_summary": "The web search tool has been implemented with proper type annotations, structured logging, and comprehensive tests. The implementation follows the vertical slice architecture and matches all spec requirements. Code quality is high with proper error handling and documentation.",
73 | "review_issues": [
74 | {
75 | "issue_number": 1,
76 | "file_path": "src/tools/web_search/tool.py",
77 | "issue_description": "Missing debug log for API response",
78 | "issue_resolution": "Add logger.debug with response metadata",
79 | "severity": "minor"
80 | }
81 | ],
82 | "validation_results": {
83 | "linting_passed": true,
84 | "type_checking_passed": true,
85 | "tests_passed": true,
86 | "api_endpoints_tested": true
87 | }
88 | }
89 | ```
90 |
--------------------------------------------------------------------------------
/app/backend/app/models/product.py:
--------------------------------------------------------------------------------
1 | """Product data models for the e-commerce catalog API."""
2 |
3 | from decimal import Decimal
4 | from typing import Literal
5 |
6 | from pydantic import BaseModel, Field
7 |
8 | # Define valid product categories as a type alias for reusability
9 | ProductCategory = Literal["electronics", "clothing", "home", "sports", "books"]
10 |
11 |
12 | class Product(BaseModel):
13 | """
14 | Represents a single product in the e-commerce catalog.
15 |
16 | This model uses verbose, intention-revealing names to make it easy for AI
17 | and humans to understand. All fields use the `product_` prefix for clarity.
18 |
19 | Attributes:
20 | product_id: Unique identifier for the product
21 | product_name: Display name of the product
22 | product_description: Detailed description of the product
23 | product_price_usd: Price in US dollars (uses Decimal for precision)
24 | product_category: One of the predefined product categories
25 | product_in_stock: Whether the product is currently available
26 |
27 | Examples:
28 | >>> Product(
29 | ... product_id=1,
30 | ... product_name="Wireless Mouse",
31 | ... product_description="Ergonomic wireless mouse with USB receiver",
32 | ... product_price_usd=Decimal("29.99"),
33 | ... product_category="electronics",
34 | ... product_in_stock=True
35 | ... )
36 | """
37 |
38 | product_id: int = Field(..., description="Unique product identifier", gt=0, examples=[1, 42, 1337])
39 |
40 | product_name: str = Field(
41 | ...,
42 | description="Display name of the product",
43 | min_length=1,
44 | max_length=200,
45 | examples=["Wireless Mouse", "Cotton T-Shirt", "Coffee Maker"],
46 | )
47 |
48 | product_description: str = Field(
49 | ...,
50 | description="Detailed description of the product features and specifications",
51 | min_length=1,
52 | max_length=1000,
53 | examples=["Ergonomic wireless mouse with USB receiver and long battery life"],
54 | )
55 |
56 | product_price_usd: Decimal = Field(
57 | ...,
58 | description="Product price in US dollars (uses Decimal for monetary precision)",
59 | gt=0,
60 | decimal_places=2,
61 | examples=["29.99", "199.99", "9.99"],
62 | )
63 |
64 | product_category: ProductCategory = Field(
65 | ...,
66 | description="Product category, must be one of the predefined categories",
67 | examples=["electronics", "clothing", "home", "sports", "books"],
68 | )
69 |
70 | product_in_stock: bool = Field(default=True, description="Whether the product is currently available for purchase")
71 |
72 |
73 | class ProductListResponse(BaseModel):
74 | """
75 | Response model for endpoints that return a list of products.
76 |
77 | Using a wrapper object (instead of returning a raw list) makes the API
78 | more extensible - we can easily add metadata like total_count, pagination,
79 | etc. in the future without breaking changes.
80 |
81 | Attributes:
82 | products: List of product objects
83 | total_count: Number of products in the response
84 |
85 | Examples:
86 | >>> ProductListResponse(
87 | ... products=[product1, product2, product3],
88 | ... total_count=3
89 | ... )
90 | """
91 |
92 | products: list[Product] = Field(..., description="List of products matching the request criteria")
93 |
94 | total_count: int = Field(..., description="Total number of products in this response", ge=0)
95 |
--------------------------------------------------------------------------------
/app/frontend/src/logo.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/frontend/src/lib/logger.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Structured logging utility matching backend logging patterns.
3 | *
4 | * This mirrors the backend's StructuredLogger (app/core/logging_config.py).
5 | * Logs are output as JSON to console for AI readability and debugging.
6 | *
7 | * Backend pattern:
8 | * ```python
9 | * logger = StructuredLogger(__name__)
10 | * logger.info("filtering_products", filter_category="electronics", total_products=30)
11 | * ```
12 | *
13 | * Frontend usage:
14 | * ```typescript
15 | * import { logger } from "@/lib/logger";
16 | * logger.info("fetching_products", { endpoint: "/api/products", operation: "initial_load" });
17 | * ```
18 | *
19 | * Key principles for AI-friendly logging (matching backend):
20 | * 1. Use JSON format for machine readability
21 | * 2. Include contextual fields (operation, parameters, results)
22 | * 3. Log to console (AI can read it directly)
23 | * 4. Use descriptive event names (fetching_products, validation_failed, etc.)
24 | * 5. Include fix_suggestion fields in error logs where appropriate
25 | */
26 |
27 | /** Log data structure for additional context fields */
28 | interface LogData {
29 | [key: string]: unknown;
30 | }
31 |
32 | /**
33 | * StructuredLogger class for consistent JSON logging.
34 | *
35 | * Mirrors backend StructuredLogger pattern but adapted for frontend/console.
36 | *
37 | * Output format matches backend:
38 | * {
39 | * "timestamp": "2025-10-13T13:14:03.123Z",
40 | * "level": "INFO",
41 | * "logger_name": "frontend",
42 | * "message": "fetching_products",
43 | * ...additional_fields
44 | * }
45 | */
46 | class StructuredLogger {
47 | /**
48 | * Format log entry as JSON string.
49 | *
50 | * @param level - Log level (INFO, ERROR, WARNING, DEBUG)
51 | * @param message - Event name or message (use snake_case like backend)
52 | * @param data - Additional structured fields
53 | */
54 | private formatLog(level: string, message: string, data?: LogData): string {
55 | return JSON.stringify({
56 | timestamp: new Date().toISOString(),
57 | level,
58 | logger_name: "frontend",
59 | message,
60 | ...data,
61 | });
62 | }
63 |
64 | /**
65 | * Log informational message.
66 | *
67 | * Use for normal operations like API calls, state changes, etc.
68 | *
69 | * Example:
70 | * ```typescript
71 | * logger.info("fetching_products", {
72 | * endpoint: "/api/products",
73 | * operation: "initial_load"
74 | * });
75 | * ```
76 | */
77 | info(message: string, data?: LogData): void {
78 | console.log(this.formatLog("INFO", message, data));
79 | }
80 |
81 | /**
82 | * Log error message.
83 | *
84 | * Use for errors, failures, exceptions.
85 | * Include fix_suggestion field when possible (matching backend pattern).
86 | *
87 | * Example:
88 | * ```typescript
89 | * logger.error("fetch_products_failed", {
90 | * error_message: err.message,
91 | * status_code: 500,
92 | * fix_suggestion: "Check backend server is running"
93 | * });
94 | * ```
95 | */
96 | error(message: string, data?: LogData): void {
97 | console.error(this.formatLog("ERROR", message, data));
98 | }
99 |
100 | /**
101 | * Log warning message.
102 | *
103 | * Use for recoverable issues, deprecations, etc.
104 | *
105 | * Example:
106 | * ```typescript
107 | * logger.warning("api_slow_response", {
108 | * endpoint: "/api/products",
109 | * response_time_ms: 5000
110 | * });
111 | * ```
112 | */
113 | warning(message: string, data?: LogData): void {
114 | console.warn(this.formatLog("WARNING", message, data));
115 | }
116 |
117 | /**
118 | * Log debug message.
119 | *
120 | * Use for development/debugging information.
121 | *
122 | * Example:
123 | * ```typescript
124 | * logger.debug("state_updated", {
125 | * previous_count: 0,
126 | * new_count: 30
127 | * });
128 | * ```
129 | */
130 | debug(message: string, data?: LogData): void {
131 | console.debug(this.formatLog("DEBUG", message, data));
132 | }
133 | }
134 |
135 | /** Singleton logger instance for use throughout the application */
136 | export const logger = new StructuredLogger();
137 |
--------------------------------------------------------------------------------
/app/frontend/src/components/ui/form.tsx:
--------------------------------------------------------------------------------
1 | import type * as LabelPrimitive from "@radix-ui/react-label";
2 | import { Slot } from "@radix-ui/react-slot";
3 | import * as React from "react";
4 | import {
5 | Controller,
6 | type ControllerProps,
7 | type FieldPath,
8 | type FieldValues,
9 | FormProvider,
10 | useFormContext,
11 | useFormState,
12 | } from "react-hook-form";
13 |
14 | import { Label } from "@/components/ui/label";
15 | import { cn } from "@/lib/utils";
16 |
17 | const Form = FormProvider;
18 |
19 | type FormFieldContextValue<
20 | TFieldValues extends FieldValues = FieldValues,
21 | TName extends FieldPath = FieldPath,
22 | > = {
23 | name: TName;
24 | };
25 |
26 | const FormFieldContext = React.createContext({} as FormFieldContextValue);
27 |
28 | const FormField = <
29 | TFieldValues extends FieldValues = FieldValues,
30 | TName extends FieldPath = FieldPath,
31 | >({
32 | ...props
33 | }: ControllerProps) => {
34 | return (
35 |
36 |
37 |
38 | );
39 | };
40 |
41 | const useFormField = () => {
42 | const fieldContext = React.useContext(FormFieldContext);
43 | const itemContext = React.useContext(FormItemContext);
44 | const { getFieldState } = useFormContext();
45 | const formState = useFormState({ name: fieldContext.name });
46 | const fieldState = getFieldState(fieldContext.name, formState);
47 |
48 | if (!fieldContext) {
49 | throw new Error("useFormField should be used within ");
50 | }
51 |
52 | const { id } = itemContext;
53 |
54 | return {
55 | id,
56 | name: fieldContext.name,
57 | formItemId: `${id}-form-item`,
58 | formDescriptionId: `${id}-form-item-description`,
59 | formMessageId: `${id}-form-item-message`,
60 | ...fieldState,
61 | };
62 | };
63 |
64 | type FormItemContextValue = {
65 | id: string;
66 | };
67 |
68 | const FormItemContext = React.createContext({} as FormItemContextValue);
69 |
70 | function FormItem({ className, ...props }: React.ComponentProps<"div">) {
71 | const id = React.useId();
72 |
73 | return (
74 |
75 |
76 |
77 | );
78 | }
79 |
80 | function FormLabel({ className, ...props }: React.ComponentProps) {
81 | const { error, formItemId } = useFormField();
82 |
83 | return (
84 |
91 | );
92 | }
93 |
94 | function FormControl({ ...props }: React.ComponentProps) {
95 | const { error, formItemId, formDescriptionId, formMessageId } = useFormField();
96 |
97 | return (
98 |
105 | );
106 | }
107 |
108 | function FormDescription({ className, ...props }: React.ComponentProps<"p">) {
109 | const { formDescriptionId } = useFormField();
110 |
111 | return (
112 |
118 | );
119 | }
120 |
121 | function FormMessage({ className, ...props }: React.ComponentProps<"p">) {
122 | const { error, formMessageId } = useFormField();
123 | const body = error ? String(error?.message) : props.children;
124 |
125 | if (!body) {
126 | return null;
127 | }
128 |
129 | return (
130 |
136 | {body}
137 |
138 | );
139 | }
140 |
141 | export { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage, useFormField };
142 |
--------------------------------------------------------------------------------
/rules-and-commands/rules/CLAUDE.md:
--------------------------------------------------------------------------------
1 | # Product Catalog - AI Development Instructions
2 |
3 | ## Project Overview
4 |
5 | Fullstack e-commerce product catalog: FastAPI backend + React 19 frontend with TypeScript. Service layer (backend), component-based (frontend).
6 |
7 | ## Core Principles
8 |
9 | **TYPE SAFETY IS NON-NEGOTIABLE** - All functions/variables/components MUST have type annotations
10 | **KISS** - Keep It Simple, Stupid
11 | **YAGNI** - You Aren't Gonna Need It
12 |
13 | ## Architecture
14 |
15 | Backend: `api/` (routes) → `services/` (logic) → `models/` (validation) → `data/`
16 | Frontend: `components/` (UI) + `lib/` (api-client, logger) + `types/` (must match backend EXACTLY)
17 |
18 | ## Naming Conventions
19 |
20 | **Backend:**
21 | - Product fields: `product_id`, `product_name`, `product_price_usd` (use `product_` prefix)
22 | - Money: `_usd` suffix + `Decimal` type (NEVER `float`)
23 | - Functions: `filter_products_by_category_and_price_range()` (descriptive)
24 | - Use `snake_case`
25 |
26 | **Frontend:**
27 | - Types match backend: `Product`, `ProductListResponse`
28 | - `PascalCase` for components, `camelCase` for variables
29 | - Money is `string`: `product_price_usd: string` (Decimal serialized from backend)
30 |
31 | **DO NOT:** Short names (`price`, `min`, `prod`), generic names (`data`, `items`), `float`/`number` for money
32 |
33 | ## Type Safety
34 |
35 | **Backend:**
36 | ```python
37 | from decimal import Decimal
38 | from pydantic import BaseModel, Field
39 |
40 | class ProductFilterParameters(BaseModel):
41 | minimum_price_usd: Decimal | None = Field(default=None, ge=0, decimal_places=2)
42 |
43 | def filter_products(minimum_price_usd: Decimal | None = None) -> list[Product]:
44 | """Complete type hints required."""
45 | ```
46 |
47 | **Frontend:**
48 | ```typescript
49 | interface Product {
50 | product_id: number;
51 | product_price_usd: string; // Decimal from backend
52 | }
53 | ```
54 |
55 | ## Logging (Structured JSON)
56 |
57 | **Backend:**
58 | ```python
59 | from app.core.logging_config import StructuredLogger
60 | logger = StructuredLogger(__name__)
61 |
62 | logger.info("filtering_products", filter_category="electronics", total_products=30)
63 | logger.error("validation_failed", error_type="invalid_price_range",
64 | fix_suggestion="Ensure min_price_usd <= max_price_usd")
65 | ```
66 |
67 | **Frontend:**
68 | ```typescript
69 | import { logger } from "@/lib/logger";
70 |
71 | logger.info("fetching_products", { endpoint: "/api/products" });
72 | logger.error("api_call_failed", { error_message: err.message, fix_suggestion: "Check backend" });
73 | ```
74 |
75 | **Always log:** Operation start (with params), completion (with count), errors (with `fix_suggestion`)
76 | **Never log:** Sensitive data, use string formatting, spam in loops
77 |
78 | ## Error Handling
79 |
80 | **Backend:**
81 | ```python
82 | from fastapi import HTTPException
83 | from app.models.error import ErrorResponse
84 |
85 | raise HTTPException(
86 | status_code=400,
87 | detail=ErrorResponse(
88 | error_code="invalid_price_range",
89 | error_message="Minimum price cannot exceed maximum price"
90 | ).model_dump()
91 | )
92 | ```
93 |
94 | **Frontend:**
95 | ```typescript
96 | import { ApiError } from "@/types/error";
97 |
98 | try {
99 | await fetchProducts();
100 | } catch (err) {
101 | if (err instanceof ApiError) console.error(err.errorResponse.error_code);
102 | }
103 | ```
104 |
105 | ## Backend Patterns
106 |
107 | 1. **Service Layer:** Business logic in `services/`, NOT in `api/` routes
108 | 2. **Pydantic Everything:** `BaseModel` + `Field()` validation
109 | 3. **Query Params:** `filters: ProductFilterParameters = Depends()`
110 | 4. **Type Hints:** Every function
111 | 5. **Docstrings:** Google-style (Args/Returns/Raises)
112 |
113 | ## Frontend Patterns (React 19)
114 |
115 | 1. **React 19:** `use` hook, actions, automatic context selectors
116 | 2. **Components:** shadcn/ui (Button, Card, Input, Select, Form), controlled components
117 | 3. **State:** `useState` locally, lift when shared
118 | 4. **Forms:** React Hook Form + Zod validation, loading/error states
119 | 5. **API:** Type-safe client, match backend types EXACTLY, structured logging
120 |
121 | ## Development
122 |
123 | **Start:** Backend: `cd app/backend && uv run python run_api.py` (port 8000)
124 | Frontend: `cd app/frontend && bun dev` (port 3000)
125 |
126 | **Backend:** `uv run ruff check app/`, `uv run pytest tests/ -v`
127 | **Frontend:** `bun run check:fix`, `bun run lint:fix`
128 |
129 | ## Testing
130 |
131 | Backend: Tests mirror source. `uv run pytest tests/ -v`
132 | Frontend: Manual browser testing, check console JSON logs
133 |
--------------------------------------------------------------------------------
/app/frontend/src/components/ProductCard.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * ProductCard component for displaying a single product.
3 | *
4 | * Uses shadcn Card component for consistent styling and layout.
5 | * Displays all product information with proper formatting and badges.
6 | *
7 | * Backend model (app/models/product.py):
8 | * ```python
9 | * class Product(BaseModel):
10 | * product_id: int
11 | * product_name: str
12 | * product_description: str
13 | * product_price_usd: Decimal
14 | * product_category: ProductCategory
15 | * product_in_stock: bool
16 | * ```
17 | */
18 |
19 | import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
20 | import type { Product } from "@/types/product";
21 |
22 | interface ProductCardProps {
23 | /** Product object to display (matches backend Product model) */
24 | product: Product;
25 | }
26 |
27 | /**
28 | * Display a single product in a card layout.
29 | *
30 | * Features:
31 | * - Product name and description
32 | * - Formatted price in USD
33 | * - Category badge with color coding
34 | * - Stock status indicator
35 | * - Responsive layout
36 | *
37 | * @param product - Product data from API
38 | */
39 | export function ProductCard({ product }: ProductCardProps) {
40 | // Format price as USD currency
41 | // Backend sends Decimal as string, parse to number for formatting
42 | const formattedPrice = new Intl.NumberFormat("en-US", {
43 | style: "currency",
44 | currency: "USD",
45 | minimumFractionDigits: 2,
46 | maximumFractionDigits: 2,
47 | }).format(parseFloat(product.product_price_usd));
48 |
49 | // Category badge color mapping
50 | // Each category gets a distinct color for visual distinction
51 | const categoryColors: Record = {
52 | electronics: "bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200",
53 | clothing: "bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-200",
54 | home: "bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200",
55 | sports: "bg-orange-100 text-orange-800 dark:bg-orange-900 dark:text-orange-200",
56 | books: "bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200",
57 | };
58 |
59 | const categoryColor = categoryColors[product.product_category] || "bg-gray-100 text-gray-800";
60 |
61 | return (
62 |
63 |
64 |
65 | {product.product_name}
66 |
67 | {product.product_category}
68 |
69 |
70 |
71 |
72 | {/* Product description with line clamping */}
73 | {product.product_description}
74 |
75 | {/* Price and stock status - pushed to bottom */}
76 |
77 |
{formattedPrice}
78 | {product.product_in_stock ? (
79 |
80 |
87 | In Stock
88 |
89 | ) : (
90 |
91 |
98 | Out of Stock
99 |
100 | )}
101 |
102 |
103 |
104 | );
105 | }
106 |
--------------------------------------------------------------------------------
/SETUP_GUIDE.md:
--------------------------------------------------------------------------------
1 | # Setup Guide: Prerequisites Installation
2 |
3 | Quick installation guide for all required tools. Follow in order.
4 |
5 | ---
6 |
7 | ## 1. Python 3.10+
8 |
9 | **macOS:**
10 |
11 | ```bash
12 | # Download and run the installer
13 | # Visit: https://www.python.org/downloads/
14 | ```
15 |
16 | **Windows:**
17 |
18 | ```bash
19 | # Download and run the installer
20 | # Visit: https://www.python.org/downloads/
21 | ```
22 |
23 | **Linux:**
24 |
25 | ```bash
26 | # Usually pre-installed. Check version first:
27 | python3 --version
28 | ```
29 |
30 | **Verify installation:**
31 |
32 | ```bash
33 | python3 --version # Should show 3.10 or higher
34 | ```
35 |
36 | 📚 [Official Python Downloads](https://www.python.org/downloads/)
37 |
38 | ---
39 |
40 | ## 2. UV (Python Package Manager)
41 |
42 | **macOS/Linux:**
43 |
44 | ```bash
45 | curl -LsSf https://astral.sh/uv/install.sh | sh
46 | ```
47 |
48 | **Windows (PowerShell):**
49 |
50 | ```powershell
51 | powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"
52 | ```
53 |
54 | **Alternative (Homebrew):**
55 |
56 | ```bash
57 | brew install uv
58 | ```
59 |
60 | **Verify installation:**
61 |
62 | ```bash
63 | uv --version
64 | ```
65 |
66 | 📚 [Official UV Installation Guide](https://docs.astral.sh/uv/getting-started/installation/)
67 |
68 | ---
69 |
70 | ## 3. Node.js & npm
71 |
72 | **Recommended: Official Installer**
73 |
74 | - Download from: https://nodejs.org/
75 | - Choose the LTS (Long Term Support) version
76 | - npm is included automatically
77 |
78 | **Alternative (macOS - Homebrew):**
79 |
80 | ```bash
81 | brew install node
82 | ```
83 |
84 | **Alternative (Windows - Chocolatey):**
85 |
86 | ```bash
87 | choco install nodejs
88 | ```
89 |
90 | **Verify installation:**
91 |
92 | ```bash
93 | node --version
94 | npm --version
95 | ```
96 |
97 | 📚 [Official Node.js Downloads](https://nodejs.org/en/download/)
98 |
99 | ---
100 |
101 | ## 4. Claude Code
102 |
103 | **NPM Install (Recommended if you have Node.js 18+):**
104 |
105 | ```bash
106 | npm install -g @anthropic-ai/claude-code
107 | ```
108 |
109 | **Native Install - macOS/Linux (Homebrew):**
110 |
111 | ```bash
112 | brew install --cask claude-code
113 | ```
114 |
115 | **Native Install - macOS/Linux/WSL:**
116 |
117 | ```bash
118 | curl -fsSL https://claude.ai/install.sh | bash
119 | ```
120 |
121 | **Native Install - Windows (PowerShell):**
122 |
123 | ```powershell
124 | irm https://claude.ai/install.ps1 | iex
125 | ```
126 |
127 | **Verify installation:**
128 |
129 | ```bash
130 | claude --version
131 | ```
132 |
133 | 📚 [Official Claude Code Quickstart](https://docs.claude.com/en/docs/claude-code/quickstart)
134 |
135 | ---
136 |
137 | ## 5. VS Code (Recommended)
138 |
139 | **All Platforms:**
140 |
141 | - Download installer for your OS: https://code.visualstudio.com/download
142 | - Run the installer
143 | - Follow the installation wizard
144 |
145 | **Verify installation:**
146 |
147 | ```bash
148 | code --version
149 | ```
150 |
151 | 📚 [Official VS Code Downloads](https://code.visualstudio.com/download)
152 |
153 | ---
154 |
155 | ## 6. Git
156 |
157 | **macOS:**
158 |
159 | ```bash
160 | # Trigger automatic installation
161 | git --version
162 |
163 | # Or via Homebrew
164 | brew install git
165 | ```
166 |
167 | **Windows:**
168 |
169 | - Download from: https://git-scm.com/download/win
170 | - Run the installer
171 |
172 | **Linux (Ubuntu/Debian):**
173 |
174 | ```bash
175 | sudo apt install git-all
176 | ```
177 |
178 | **Linux (Fedora/RHEL):**
179 |
180 | ```bash
181 | sudo dnf install git-all
182 | ```
183 |
184 | **Verify installation:**
185 |
186 | ```bash
187 | git --version
188 | ```
189 |
190 | 📚 [Official Git Installation Guide](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git)
191 |
192 | ---
193 |
194 | ## Final Verification
195 |
196 | Run all commands to verify everything is installed:
197 |
198 | ```bash
199 | python3 --version # Should show 3.10+
200 | uv --version # Should show version number
201 | node --version # Should show version number
202 | npm --version # Should show version number
203 | claude --version # Should show version number
204 | code --version # Should show version number
205 | git --version # Should show version number
206 | ```
207 |
208 | ✅ **All commands show version numbers? You're ready to start!**
209 |
210 | ❌ **Getting "command not found"?** Restart your terminal and try again. If the issue persists, revisit the installation steps for that specific tool.
211 |
212 | ---
213 |
214 | ## Alternative AI Coding Assistants
215 |
216 | This course teaches frameworks that work with **any** AI coding assistant:
217 |
218 | - **Cursor**: https://cursor.sh/
219 | - **GitHub Copilot**: https://github.com/features/copilot
220 | - **Aider**: https://aider.chat/
221 |
222 | Use what you're comfortable with.
223 |
224 | ---
225 |
226 | ## Need Help?
227 |
228 | - Check the official documentation linked above
229 | - Join the community # channel
230 | - Post your error message with the tool name
231 |
232 | **Installation Time:** 20-30 minutes from scratch is normal.
233 |
234 | ---
235 |
236 | _Last updated: October 2025_
237 |
--------------------------------------------------------------------------------
/app/frontend/styles/globals.css:
--------------------------------------------------------------------------------
1 | @import "tailwindcss";
2 |
3 | @plugin "tailwindcss-animate";
4 |
5 | @custom-variant dark (&:is(.dark *));
6 |
7 | :root {
8 | --background: hsl(0 0% 100%);
9 | --foreground: hsl(240 10% 3.9%);
10 | --card: hsl(0 0% 100%);
11 | --card-foreground: hsl(240 10% 3.9%);
12 | --popover: hsl(0 0% 100%);
13 | --popover-foreground: hsl(240 10% 3.9%);
14 | --primary: hsl(240 5.9% 10%);
15 | --primary-foreground: hsl(0 0% 98%);
16 | --secondary: hsl(240 4.8% 95.9%);
17 | --secondary-foreground: hsl(240 5.9% 10%);
18 | --muted: hsl(240 4.8% 95.9%);
19 | --muted-foreground: hsl(240 3.8% 46.1%);
20 | --accent: hsl(240 4.8% 95.9%);
21 | --accent-foreground: hsl(240 5.9% 10%);
22 | --destructive: hsl(0 84.2% 60.2%);
23 | --destructive-foreground: hsl(0 0% 98%);
24 | --border: hsl(240 5.9% 90%);
25 | --input: hsl(240 5.9% 90%);
26 | --ring: hsl(240 10% 3.9%);
27 | --chart-1: hsl(12 76% 61%);
28 | --chart-2: hsl(173 58% 39%);
29 | --chart-3: hsl(197 37% 24%);
30 | --chart-4: hsl(43 74% 66%);
31 | --chart-5: hsl(27 87% 67%);
32 | --radius: 0.6rem;
33 | --sidebar-background: hsl(0 0% 98%);
34 | --sidebar-foreground: hsl(240 5.3% 26.1%);
35 | --sidebar-primary: hsl(240 5.9% 10%);
36 | --sidebar-primary-foreground: hsl(0 0% 98%);
37 | --sidebar-accent: hsl(240 4.8% 95.9%);
38 | --sidebar-accent-foreground: hsl(240 5.9% 10%);
39 | --sidebar-border: hsl(220 13% 91%);
40 | --sidebar-ring: hsl(217.2 91.2% 59.8%);
41 | }
42 |
43 | .dark {
44 | --background: hsl(240 10% 3.9%);
45 | --foreground: hsl(0 0% 98%);
46 | --card: hsl(240 10% 3.9%);
47 | --card-foreground: hsl(0 0% 98%);
48 | --popover: hsl(240 10% 3.9%);
49 | --popover-foreground: hsl(0 0% 98%);
50 | --primary: hsl(0 0% 98%);
51 | --primary-foreground: hsl(240 5.9% 10%);
52 | --secondary: hsl(240 3.7% 15.9%);
53 | --secondary-foreground: hsl(0 0% 98%);
54 | --muted: hsl(240 3.7% 15.9%);
55 | --muted-foreground: hsl(240 5% 64.9%);
56 | --accent: hsl(240 3.7% 15.9%);
57 | --accent-foreground: hsl(0 0% 98%);
58 | --destructive: hsl(0 62.8% 30.6%);
59 | --destructive-foreground: hsl(0 0% 98%);
60 | --border: hsl(240 3.7% 15.9%);
61 | --input: hsl(240 3.7% 15.9%);
62 | --ring: hsl(240 4.9% 83.9%);
63 | --chart-1: hsl(220 70% 50%);
64 | --chart-2: hsl(160 60% 45%);
65 | --chart-3: hsl(30 80% 55%);
66 | --chart-4: hsl(280 65% 60%);
67 | --chart-5: hsl(340 75% 55%);
68 | --sidebar-background: hsl(240 5.9% 10%);
69 | --sidebar-foreground: hsl(240 4.8% 95.9%);
70 | --sidebar-primary: hsl(224.3 76.3% 48%);
71 | --sidebar-primary-foreground: hsl(0 0% 100%);
72 | --sidebar-accent: hsl(240 3.7% 15.9%);
73 | --sidebar-accent-foreground: hsl(240 4.8% 95.9%);
74 | --sidebar-border: hsl(240 3.7% 15.9%);
75 | --sidebar-ring: hsl(217.2 91.2% 59.8%);
76 | }
77 |
78 | @theme inline {
79 | --color-background: var(--background);
80 | --color-foreground: var(--foreground);
81 | --color-card: var(--card);
82 | --color-card-foreground: var(--card-foreground);
83 | --color-popover: var(--popover);
84 | --color-popover-foreground: var(--popover-foreground);
85 | --color-primary: var(--primary);
86 | --color-primary-foreground: var(--primary-foreground);
87 | --color-secondary: var(--secondary);
88 | --color-secondary-foreground: var(--secondary-foreground);
89 | --color-muted: var(--muted);
90 | --color-muted-foreground: var(--muted-foreground);
91 | --color-accent: var(--accent);
92 | --color-accent-foreground: var(--accent-foreground);
93 | --color-destructive: var(--destructive);
94 | --color-destructive-foreground: var(--destructive-foreground);
95 | --color-border: var(--border);
96 | --color-input: var(--input);
97 | --color-ring: var(--ring);
98 | --color-chart-1: var(--chart-1);
99 | --color-chart-2: var(--chart-2);
100 | --color-chart-3: var(--chart-3);
101 | --color-chart-4: var(--chart-4);
102 | --color-chart-5: var(--chart-5);
103 | --radius-sm: calc(var(--radius) - 4px);
104 | --radius-md: calc(var(--radius) - 2px);
105 | --radius-lg: var(--radius);
106 | --radius-xl: calc(var(--radius) + 4px);
107 | --color-sidebar-ring: var(--sidebar-ring);
108 | --color-sidebar-border: var(--sidebar-border);
109 | --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
110 | --color-sidebar-accent: var(--sidebar-accent);
111 | --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
112 | --color-sidebar-primary: var(--sidebar-primary);
113 | --color-sidebar-foreground: var(--sidebar-foreground);
114 | --color-sidebar: var(--sidebar-background);
115 | --animate-accordion-down: accordion-down 0.2s ease-out;
116 | --animate-accordion-up: accordion-up 0.2s ease-out;
117 |
118 | @keyframes accordion-down {
119 | from {
120 | height: 0;
121 | }
122 | to {
123 | height: var(--radix-accordion-content-height);
124 | }
125 | }
126 |
127 | @keyframes accordion-up {
128 | from {
129 | height: var(--radix-accordion-content-height);
130 | }
131 | to {
132 | height: 0;
133 | }
134 | }
135 | }
136 |
137 | @layer base {
138 | * {
139 | @apply border-border outline-ring/50;
140 | }
141 | body {
142 | @apply bg-background text-foreground;
143 | }
144 | }
145 |
--------------------------------------------------------------------------------
/app/frontend/src/lib/api-client.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Type-safe API client for communicating with FastAPI backend.
3 | *
4 | * Backend endpoints (app/api/products.py, app/main.py):
5 | * - GET /api/products - Get all products
6 | * - GET /health - Health check
7 | *
8 | * Configuration:
9 | * - API_BASE_URL: Backend URL from environment or default
10 | *
11 | * Error handling:
12 | * - Network errors: Throws standard Error
13 | * - API errors: Throws ApiError with ErrorResponse
14 | */
15 |
16 | import { ApiError, type ErrorResponse } from "@/types/error";
17 | import type { ProductListResponse } from "@/types/product";
18 | import { logger } from "./logger";
19 |
20 | /**
21 | * Backend API base URL.
22 | *
23 | * Default: http://localhost:8000 (matching backend run_api.py port)
24 | * TODO: Make this configurable via build-time environment variable
25 | */
26 | const API_BASE_URL = "http://localhost:8000";
27 |
28 | /**
29 | * Fetch all products from the catalog API.
30 | *
31 | * Backend endpoint: GET /api/products
32 | * Response model: ProductListResponse
33 | *
34 | * @returns ProductListResponse with products array and total count
35 | * @throws ApiError if backend returns error response (4xx/5xx)
36 | * @throws Error if network failure or unable to reach backend
37 | *
38 | * Example usage:
39 | * ```typescript
40 | * try {
41 | * const response = await fetchProducts();
42 | * console.log(`Loaded ${response.total_count} products`);
43 | * } catch (error) {
44 | * if (error instanceof ApiError) {
45 | * console.error(`API Error: ${error.errorResponse.error_code}`);
46 | * } else {
47 | * console.error('Network error');
48 | * }
49 | * }
50 | * ```
51 | */
52 | export async function fetchProducts(): Promise {
53 | const endpoint = "/api/products";
54 | const url = `${API_BASE_URL}${endpoint}`;
55 |
56 | logger.info("fetching_products", {
57 | endpoint,
58 | url,
59 | operation: "fetchProducts",
60 | });
61 |
62 | try {
63 | const response = await fetch(url, {
64 | method: "GET",
65 | headers: {
66 | "Content-Type": "application/json",
67 | },
68 | });
69 |
70 | // Handle non-OK responses (4xx, 5xx)
71 | if (!response.ok) {
72 | let errorData: ErrorResponse;
73 |
74 | try {
75 | // Try to parse error response from backend
76 | errorData = await response.json();
77 | } catch {
78 | // Fallback if response isn't JSON
79 | errorData = {
80 | error_code: "unknown_error",
81 | error_message: `HTTP ${response.status}: ${response.statusText}`,
82 | timestamp_utc: new Date().toISOString(),
83 | };
84 | }
85 |
86 | logger.error("fetch_products_failed", {
87 | endpoint,
88 | status_code: response.status,
89 | error_code: errorData.error_code,
90 | error_message: errorData.error_message,
91 | operation: "fetchProducts",
92 | });
93 |
94 | throw new ApiError(response.status, errorData);
95 | }
96 |
97 | // Parse successful response
98 | const data: ProductListResponse = await response.json();
99 |
100 | logger.info("products_fetched_successfully", {
101 | endpoint,
102 | products_count: data.total_count,
103 | operation: "fetchProducts",
104 | });
105 |
106 | return data;
107 | } catch (error) {
108 | // Re-throw ApiError as-is
109 | if (error instanceof ApiError) {
110 | throw error;
111 | }
112 |
113 | // Handle network errors (fetch failed completely)
114 | const errorMessage = error instanceof Error ? error.message : String(error);
115 |
116 | logger.error("network_error", {
117 | endpoint,
118 | error_message: errorMessage,
119 | error_type: "network_failure",
120 | fix_suggestion: `Check that backend server is running at ${API_BASE_URL}`,
121 | operation: "fetchProducts",
122 | });
123 |
124 | throw new Error(`Network error while fetching products: ${errorMessage}`);
125 | }
126 | }
127 |
128 | /**
129 | * Health check for the backend API.
130 | *
131 | * Backend endpoint: GET /health
132 | * Expected response: { "status": "healthy" }
133 | *
134 | * @returns true if backend is healthy and reachable
135 | * @returns false if backend is down or unhealthy
136 | *
137 | * Example usage:
138 | * ```typescript
139 | * const isHealthy = await checkHealth();
140 | * if (!isHealthy) {
141 | * console.warn('Backend is not responding');
142 | * }
143 | * ```
144 | */
145 | export async function checkHealth(): Promise {
146 | const endpoint = "/health";
147 | const url = `${API_BASE_URL}${endpoint}`;
148 |
149 | try {
150 | const response = await fetch(url, {
151 | method: "GET",
152 | headers: {
153 | "Content-Type": "application/json",
154 | },
155 | });
156 |
157 | if (!response.ok) {
158 | return false;
159 | }
160 |
161 | const data = await response.json();
162 | const isHealthy = data.status === "healthy";
163 |
164 | logger.info("health_check_completed", {
165 | endpoint,
166 | is_healthy: isHealthy,
167 | operation: "checkHealth",
168 | });
169 |
170 | return isHealthy;
171 | } catch (error) {
172 | logger.warning("health_check_failed", {
173 | endpoint,
174 | error_message: error instanceof Error ? error.message : String(error),
175 | operation: "checkHealth",
176 | });
177 |
178 | return false;
179 | }
180 | }
181 |
--------------------------------------------------------------------------------
/rules-and-commands/commands/noqa.md:
--------------------------------------------------------------------------------
1 | # NOQA Analysis and Resolution
2 |
3 | Find all noqa/type:ignore comments in the codebase, investigate why they exist, and provide recommendations for resolution or justification.
4 |
5 | ## Instructions
6 |
7 | **Step 1: Find all NOQA comments**
8 |
9 | - Use Grep tool to find all noqa comments: pattern `noqa|type:\s*ignore`
10 | - Use output_mode "content" with line numbers (-n flag)
11 | - Search across all Python files (type: "py")
12 | - Document total count of noqa comments found
13 |
14 | **Step 2: For EACH noqa comment (repeat this process):**
15 |
16 | - Read the file containing the noqa comment with sufficient context (at least 10 lines before and after)
17 | - Identify the specific linting rule or type error being suppressed
18 | - Understand the code's purpose and why the suppression was added
19 | - Investigate if the suppression is still necessary or can be resolved
20 |
21 | **Step 3: Investigation checklist for each noqa:**
22 |
23 | - What specific error/warning is being suppressed? (e.g., `type: ignore[arg-type]`, `noqa: F401`)
24 | - Why was the suppression necessary? (legacy code, false positive, legitimate limitation, technical debt)
25 | - Can the underlying issue be fixed? (refactor code, update types, improve imports)
26 | - What would it take to remove the suppression? (effort estimate, breaking changes, architectural changes)
27 | - Is the suppression justified long-term? (external library limitation, Python limitation, intentional design)
28 |
29 | **Step 4: Research solutions:**
30 |
31 | - Check if newer versions of tools (mypy, ruff) handle the case better
32 | - Look for alternative code patterns that avoid the suppression
33 | - Consider if type stubs or Protocol definitions could help
34 | - Evaluate if refactoring would be worthwhile
35 |
36 | ## Report Format
37 |
38 | Create a markdown report file (create the reports directory if not created yet): `PRPs/reports/noqa-analysis-{YYYY-MM-DD}.md`
39 |
40 | Use this structure for the report:
41 |
42 | ````markdown
43 | # NOQA Analysis Report
44 |
45 | **Generated:** {date}
46 | **Total NOQA comments found:** {count}
47 |
48 | ---
49 |
50 | ## Summary
51 |
52 | - Total suppressions: {count}
53 | - Can be removed: {count}
54 | - Should remain: {count}
55 | - Requires investigation: {count}
56 |
57 | ---
58 |
59 | ## Detailed Analysis
60 |
61 | ### 1. {File path}:{line number}
62 |
63 | **Location:** `{file_path}:{line_number}`
64 |
65 | **Suppression:** `{noqa comment or type: ignore}`
66 |
67 | **Code context:**
68 |
69 | ```python
70 | {relevant code snippet}
71 | ```
72 | ````
73 |
74 | **Why it exists:**
75 | {explanation of why the suppression was added}
76 |
77 | **Options to resolve:**
78 |
79 | 1. {Option 1: description}
80 | - Effort: {Low/Medium/High}
81 | - Breaking: {Yes/No}
82 | - Impact: {description}
83 |
84 | 2. {Option 2: description}
85 | - Effort: {Low/Medium/High}
86 | - Breaking: {Yes/No}
87 | - Impact: {description}
88 |
89 | **Tradeoffs:**
90 |
91 | - {Tradeoff 1}
92 | - {Tradeoff 2}
93 |
94 | **Recommendation:** {Remove | Keep | Refactor}
95 | {Justification for recommendation}
96 |
97 | ---
98 |
99 | {Repeat for each noqa comment}
100 |
101 | ````
102 |
103 | ## Example Analysis Entry
104 |
105 | ```markdown
106 | ### 1. src/shared/config.py:45
107 |
108 | **Location:** `src/shared/config.py:45`
109 |
110 | **Suppression:** `# type: ignore[assignment]`
111 |
112 | **Code context:**
113 | ```python
114 | @property
115 | def openai_api_key(self) -> str:
116 | key = os.getenv("OPENAI_API_KEY")
117 | if not key:
118 | raise ValueError("OPENAI_API_KEY not set")
119 | return key # type: ignore[assignment]
120 | ````
121 |
122 | **Why it exists:**
123 | MyPy cannot infer that the ValueError prevents None from being returned, so it thinks the return type could be `str | None`.
124 |
125 | **Options to resolve:**
126 |
127 | 1. Use assert to help mypy narrow the type
128 | - Effort: Low
129 | - Breaking: No
130 | - Impact: Cleaner code, removes suppression
131 |
132 | 2. Add explicit cast with typing.cast()
133 | - Effort: Low
134 | - Breaking: No
135 | - Impact: More verbose but type-safe
136 |
137 | 3. Refactor to use separate validation method
138 | - Effort: Medium
139 | - Breaking: No
140 | - Impact: Better separation of concerns
141 |
142 | **Tradeoffs:**
143 |
144 | - Option 1 (assert) is cleanest but asserts can be disabled with -O flag
145 | - Option 2 (cast) is most explicit but adds import and verbosity
146 | - Option 3 is most robust but requires more refactoring
147 |
148 | **Recommendation:** Remove (use Option 1)
149 | Replace the type:ignore with an assert statement after the if check. This helps mypy understand the control flow while maintaining runtime safety. The assert will never fail in practice since the ValueError is raised first.
150 |
151 | **Implementation:**
152 |
153 | ```python
154 | @property
155 | def openai_api_key(self) -> str:
156 | key = os.getenv("OPENAI_API_KEY")
157 | if not key:
158 | raise ValueError("OPENAI_API_KEY not set")
159 | assert key is not None # Help mypy understand control flow
160 | return key
161 | ```
162 |
163 | ```
164 |
165 | ## Report
166 |
167 | After completing the analysis:
168 |
169 | - Output the path to the generated report file
170 | - Summarize findings:
171 | - Total suppressions found
172 | - How many can be removed immediately (low effort)
173 | - How many should remain (justified)
174 | - How many need deeper investigation or refactoring
175 | - Highlight any quick wins (suppressions that can be removed with minimal effort)
176 | ```
177 |
--------------------------------------------------------------------------------
/app/frontend/build.ts:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bun
2 | import { build, type BuildConfig } from "bun";
3 | import plugin from "bun-plugin-tailwind";
4 | import { existsSync } from "fs";
5 | import { rm } from "fs/promises";
6 | import path from "path";
7 |
8 | // Print help text if requested
9 | if (process.argv.includes("--help") || process.argv.includes("-h")) {
10 | console.log(`
11 | 🏗️ Bun Build Script
12 |
13 | Usage: bun run build.ts [options]
14 |
15 | Common Options:
16 | --outdir Output directory (default: "dist")
17 | --minify Enable minification (or --minify.whitespace, --minify.syntax, etc)
18 | --source-map Sourcemap type: none|linked|inline|external
19 | --target Build target: browser|bun|node
20 | --format Output format: esm|cjs|iife
21 | --splitting Enable code splitting
22 | --packages Package handling: bundle|external
23 | --public-path Public path for assets
24 | --env Environment handling: inline|disable|prefix*
25 | --conditions Package.json export conditions (comma separated)
26 | --external External packages (comma separated)
27 | --banner Add banner text to output
28 | --footer Add footer text to output
29 | --define Define global constants (e.g. --define.VERSION=1.0.0)
30 | --help, -h Show this help message
31 |
32 | Example:
33 | bun run build.ts --outdir=dist --minify --source-map=linked --external=react,react-dom
34 | `);
35 | process.exit(0);
36 | }
37 |
38 | // Helper function to convert kebab-case to camelCase
39 | const toCamelCase = (str: string): string => {
40 | return str.replace(/-([a-z])/g, g => g[1].toUpperCase());
41 | };
42 |
43 | // Helper function to parse a value into appropriate type
44 | const parseValue = (value: string): any => {
45 | // Handle true/false strings
46 | if (value === "true") return true;
47 | if (value === "false") return false;
48 |
49 | // Handle numbers
50 | if (/^\d+$/.test(value)) return parseInt(value, 10);
51 | if (/^\d*\.\d+$/.test(value)) return parseFloat(value);
52 |
53 | // Handle arrays (comma-separated)
54 | if (value.includes(",")) return value.split(",").map(v => v.trim());
55 |
56 | // Default to string
57 | return value;
58 | };
59 |
60 | // Magical argument parser that converts CLI args to BuildConfig
61 | function parseArgs(): Partial {
62 | const config: Record = {};
63 | const args = process.argv.slice(2);
64 |
65 | for (let i = 0; i < args.length; i++) {
66 | const arg = args[i];
67 | if (!arg.startsWith("--")) continue;
68 |
69 | // Handle --no-* flags
70 | if (arg.startsWith("--no-")) {
71 | const key = toCamelCase(arg.slice(5));
72 | config[key] = false;
73 | continue;
74 | }
75 |
76 | // Handle --flag (boolean true)
77 | if (!arg.includes("=") && (i === args.length - 1 || args[i + 1].startsWith("--"))) {
78 | const key = toCamelCase(arg.slice(2));
79 | config[key] = true;
80 | continue;
81 | }
82 |
83 | // Handle --key=value or --key value
84 | let key: string;
85 | let value: string;
86 |
87 | if (arg.includes("=")) {
88 | [key, value] = arg.slice(2).split("=", 2);
89 | } else {
90 | key = arg.slice(2);
91 | value = args[++i];
92 | }
93 |
94 | // Convert kebab-case key to camelCase
95 | key = toCamelCase(key);
96 |
97 | // Handle nested properties (e.g. --minify.whitespace)
98 | if (key.includes(".")) {
99 | const [parentKey, childKey] = key.split(".");
100 | config[parentKey] = config[parentKey] || {};
101 | config[parentKey][childKey] = parseValue(value);
102 | } else {
103 | config[key] = parseValue(value);
104 | }
105 | }
106 |
107 | return config as Partial;
108 | }
109 |
110 | // Helper function to format file sizes
111 | const formatFileSize = (bytes: number): string => {
112 | const units = ["B", "KB", "MB", "GB"];
113 | let size = bytes;
114 | let unitIndex = 0;
115 |
116 | while (size >= 1024 && unitIndex < units.length - 1) {
117 | size /= 1024;
118 | unitIndex++;
119 | }
120 |
121 | return `${size.toFixed(2)} ${units[unitIndex]}`;
122 | };
123 |
124 | console.log("\n🚀 Starting build process...\n");
125 |
126 | // Parse CLI arguments with our magical parser
127 | const cliConfig = parseArgs();
128 | const outdir = cliConfig.outdir || path.join(process.cwd(), "dist");
129 |
130 | if (existsSync(outdir)) {
131 | console.log(`🗑️ Cleaning previous build at ${outdir}`);
132 | await rm(outdir, { recursive: true, force: true });
133 | }
134 |
135 | const start = performance.now();
136 |
137 | // Scan for all HTML files in the project
138 | const entrypoints = [...new Bun.Glob("**.html").scanSync("src")]
139 | .map(a => path.resolve("src", a))
140 | .filter(dir => !dir.includes("node_modules"));
141 | console.log(`📄 Found ${entrypoints.length} HTML ${entrypoints.length === 1 ? "file" : "files"} to process\n`);
142 |
143 | // Build all the HTML files
144 | const result = await build({
145 | entrypoints,
146 | outdir,
147 | plugins: [plugin],
148 | minify: true,
149 | target: "browser",
150 | sourcemap: "linked",
151 | define: {
152 | "process.env.NODE_ENV": JSON.stringify("production"),
153 | },
154 | ...cliConfig, // Merge in any CLI-provided options
155 | });
156 |
157 | // Print the results
158 | const end = performance.now();
159 |
160 | const outputTable = result.outputs.map(output => ({
161 | "File": path.relative(process.cwd(), output.path),
162 | "Type": output.kind,
163 | "Size": formatFileSize(output.size),
164 | }));
165 |
166 | console.table(outputTable);
167 | const buildTime = (end - start).toFixed(2);
168 |
169 | console.log(`\n✅ Build completed in ${buildTime}ms\n`);
170 |
--------------------------------------------------------------------------------
/app/frontend/src/components/ui/select.tsx:
--------------------------------------------------------------------------------
1 | import * as SelectPrimitive from "@radix-ui/react-select";
2 | import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react";
3 | import type * as React from "react";
4 |
5 | import { cn } from "@/lib/utils";
6 |
7 | function Select({ ...props }: React.ComponentProps) {
8 | return ;
9 | }
10 |
11 | function SelectGroup({ ...props }: React.ComponentProps) {
12 | return ;
13 | }
14 |
15 | function SelectValue({ ...props }: React.ComponentProps) {
16 | return ;
17 | }
18 |
19 | function SelectTrigger({ className, children, ...props }: React.ComponentProps) {
20 | return (
21 | span]:line-clamp-1",
25 | className
26 | )}
27 | {...props}
28 | >
29 | {children}
30 |
31 |
32 |
33 |
34 | );
35 | }
36 |
37 | function SelectContent({
38 | className,
39 | children,
40 | position = "popper",
41 | ...props
42 | }: React.ComponentProps) {
43 | return (
44 |
45 |
56 |
57 |
64 | {children}
65 |
66 |
67 |
68 |
69 | );
70 | }
71 |
72 | function SelectLabel({ className, ...props }: React.ComponentProps) {
73 | return (
74 |
79 | );
80 | }
81 |
82 | function SelectItem({ className, children, ...props }: React.ComponentProps) {
83 | return (
84 |
92 |
93 |
94 |
95 |
96 |
97 | {children}
98 |
99 | );
100 | }
101 |
102 | function SelectSeparator({ className, ...props }: React.ComponentProps) {
103 | return (
104 |
109 | );
110 | }
111 |
112 | function SelectScrollUpButton({ className, ...props }: React.ComponentProps) {
113 | return (
114 |
119 |
120 |
121 | );
122 | }
123 |
124 | function SelectScrollDownButton({
125 | className,
126 | ...props
127 | }: React.ComponentProps) {
128 | return (
129 |
134 |
135 |
136 | );
137 | }
138 |
139 | export {
140 | Select,
141 | SelectContent,
142 | SelectGroup,
143 | SelectItem,
144 | SelectLabel,
145 | SelectScrollDownButton,
146 | SelectScrollUpButton,
147 | SelectSeparator,
148 | SelectTrigger,
149 | SelectValue,
150 | };
151 |
--------------------------------------------------------------------------------
/app/frontend/src/App.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * Main application component for the Product Catalog.
3 | *
4 | * Responsibilities:
5 | * - Fetch products from backend API on mount
6 | * - Manage loading, error, and success states
7 | * - Display product grid with proper error handling
8 | * - Provide user feedback for all states
9 | *
10 | * State management:
11 | * - products: Array of Product objects from API
12 | * - loading: Boolean indicating API call in progress
13 | * - error: String with error message (null if no error)
14 | *
15 | * Backend integration:
16 | * - Calls GET /api/products via fetchProducts()
17 | * - Handles ApiError and network errors
18 | * - Logs all operations for debugging
19 | */
20 |
21 | import { useCallback, useEffect, useState } from "react";
22 | import { ProductGrid } from "@/components/ProductGrid";
23 | import { fetchProducts } from "@/lib/api-client";
24 | import { logger } from "@/lib/logger";
25 | import { ApiError } from "@/types/error";
26 | import type { Product } from "@/types/product";
27 | import "./index.css";
28 |
29 | /**
30 | * Main App component.
31 | *
32 | * Lifecycle:
33 | * 1. Mount: Start loading products
34 | * 2. Loading: Show loading spinner in ProductGrid
35 | * 3. Success: Display products in grid
36 | * 4. Error: Show error message with retry button
37 | */
38 | export function App() {
39 | // State for products data
40 | const [products, setProducts] = useState([]);
41 |
42 | // State for loading indicator
43 | const [loading, setLoading] = useState(true);
44 |
45 | // State for error message (null = no error)
46 | const [error, setError] = useState(null);
47 |
48 | /**
49 | * Fetch products from backend API.
50 | *
51 | * Handles both ApiError (from backend) and network errors.
52 | * Updates state based on result.
53 | */
54 | const loadProducts = useCallback(async () => {
55 | logger.info("app_loading_products", {
56 | operation: "initial_load",
57 | component: "App",
58 | });
59 |
60 | try {
61 | setLoading(true);
62 | setError(null);
63 |
64 | // Call backend API
65 | const response = await fetchProducts();
66 |
67 | // Update state with fetched products
68 | setProducts(response.products);
69 |
70 | logger.info("app_products_loaded", {
71 | products_count: response.total_count,
72 | operation: "initial_load",
73 | component: "App",
74 | });
75 | } catch (err) {
76 | // Extract error message based on error type
77 | const errorMessage =
78 | err instanceof ApiError
79 | ? err.errorResponse.error_message
80 | : err instanceof Error
81 | ? err.message
82 | : "An unknown error occurred while loading products";
83 |
84 | setError(errorMessage);
85 |
86 | logger.error("app_load_products_failed", {
87 | error_message: errorMessage,
88 | error_type: err instanceof ApiError ? "api_error" : "network_error",
89 | error_code: err instanceof ApiError ? err.errorResponse.error_code : undefined,
90 | operation: "initial_load",
91 | component: "App",
92 | fix_suggestion:
93 | err instanceof ApiError
94 | ? "Check backend logs for error details"
95 | : "Verify backend server is running at http://localhost:8000",
96 | });
97 | } finally {
98 | setLoading(false);
99 | }
100 | }, []);
101 |
102 | // Load products on component mount
103 | useEffect(() => {
104 | loadProducts();
105 | }, [loadProducts]);
106 |
107 | return (
108 |
109 | {/* Header section */}
110 |
122 |
123 | {/* Main content area */}
124 |
125 | {/* Error state - show error message with retry button */}
126 | {error ? (
127 |
128 |
129 |
130 | {/* Error icon */}
131 |
144 |
145 |
Error loading products
146 |
{error}
147 |
154 |
155 |
156 |
157 |
158 | {/* Helpful debug info */}
159 |
160 |
Make sure the backend server is running:
161 |
162 | cd app/backend && uv run python run_api.py
163 |
164 |
165 |
166 | ) : (
167 | // Success/Loading state - show product grid
168 |
169 | )}
170 |
171 |
172 | {/* Footer */}
173 |
178 |
179 | );
180 | }
181 |
182 | export default App;
183 |
--------------------------------------------------------------------------------
/exercises/exercise_1.md:
--------------------------------------------------------------------------------
1 | # Exercise 1: Baseline Implementation
2 |
3 | ## Overview
4 |
5 | This is the first of three exercises in the AI Coding Summit workshop. Exercise 1 is designed to establish your personal baseline for AI-assisted coding **before** learning systematic techniques.
6 |
7 | You'll implement product filtering capabilities in both the backend API and frontend interface of an e-commerce product catalog, working exactly as you would in a real project today.
8 |
9 | **Important**: This is a baseline assessment, not a test. Use AI coding assistants however you're most comfortable. There are no rules, restrictions, or requirements on how you use AI tools - just work naturally.
10 |
11 | ## Goals
12 |
13 | 1. **Implement a complete fullstack feature** from backend to frontend
14 | 2. **Establish your baseline** for time, effort, and confidence when using AI assistants
15 | 3. **Experience your current workflow** before learning optimization techniques in Exercises 2 and 3
16 |
17 | ## The Task
18 |
19 | You'll implement product filtering in two parts:
20 |
21 | ### Use AI However You Want
22 |
23 | There are **no rules or restrictions** on AI usage:
24 |
25 | - **Work exactly as you normally would if you got this task in a real project today**
26 | - Use whatever prompting style feels natural to you
27 | - Ask for help, code generation, debugging - whatever you need
28 |
29 | ### Part 1: Backend API Filtering
30 |
31 | Add filtering capabilities to the FastAPI backend:
32 |
33 | - **Price filtering**: Support minimum and maximum price filters
34 | - **Category filtering**: Allow filtering by product category
35 | - **Keyword search**: Search product names and descriptions
36 | - **Sorting**: Enable sorting by price or name (both directions)
37 |
38 | All filters should be optional and work together when combined.
39 |
40 | ### Part 2: Frontend Filtering UI
41 |
42 | Build a React frontend interface that connects to your backend filtering API:
43 |
44 | - **Price range controls**: Allow users to set min/max price filters
45 | - **Category selector**: Enable filtering by product category
46 | - **Search input**: Provide keyword search functionality
47 | - **Sort controls**: Allow users to sort results
48 | - **Filter management**: Ability to apply and clear filters
49 |
50 | The interface should handle loading states, empty results, and validation errors appropriately.
51 |
52 | ## Project Structure
53 |
54 | ```
55 | ai-coding-summit-workshop/
56 | ├── app/
57 | │ ├── backend/ # FastAPI backend application
58 | │ │ ├── app/
59 | │ │ │ ├── api/
60 | │ │ │ ├── models/
61 | │ │ │ ├── services/
62 | │ │ │ └── ...
63 | │ │ └── tests/ # Backend tests (make these pass!)
64 | │ └── frontend/ # React frontend application
65 | │ └── src/
66 | │ ├── components/
67 | │ ├── lib/
68 | │ └── types/
69 | ```
70 |
71 | ## Getting Started
72 |
73 | ### 1. Review the Codebase
74 |
75 | Familiarize yourself with:
76 |
77 | - **Backend**: `app/backend/app/` - Note the existing patterns for models, services, and logging
78 | - **Frontend**: `app/frontend/src/` - Review the existing components and API client
79 | - **Tests**: `app/backend/tests/` - Examine `test_products_filtering.py` to see what needs to pass
80 |
81 | ### 2. Start Backend Development
82 |
83 | ```bash
84 | cd app/backend
85 |
86 | # Install dependencies
87 | uv venv --python 3.12
88 | uv sync
89 |
90 | # Start development server
91 | uv run python run_api.py
92 | ```
93 |
94 | Implement the backend filtering feature described above. Use AI assistants in whatever way feels natural to you.
95 |
96 | ### 3. Start Frontend Development
97 |
98 | Once your backend filtering is working:
99 |
100 | ```bash
101 | cd app/frontend
102 |
103 | # Install dependencies
104 | bun install
105 |
106 | # Start development server
107 | bun dev
108 | ```
109 |
110 | Implement the frontend filtering UI described above. Again, use AI however you normally would.
111 |
112 | ## What to Track
113 |
114 | As you work, please track the following metrics:
115 |
116 | ### ⏱️ Time Tracking
117 |
118 | - **Backend time**: How long did TASK1 take?
119 | - **Frontend time**: How long did TASK2 take?
120 | - **Total time**: Overall exercise duration
121 |
122 | ### 💬 AI Interaction
123 |
124 | - **Number of prompts**: How many times did you prompt your AI assistant?
125 | - **Types of requests**: Were you asking for code generation? Debugging? Explanations?
126 | - **Iteration cycles**: How many back-and-forth exchanges did it take?
127 |
128 | ### 😊 Confidence Level
129 |
130 | After completing both tasks, rate your confidence (1-10):
131 |
132 | - How confident are you that the code is **correct**?
133 | - How confident are you that the code follows **best practices**?
134 | - How well do you **understand** the generated code?
135 | - How **maintainable** is the resulting code?
136 |
137 | ### 🐛 Issues Encountered
138 |
139 | - Did the AI make mistakes? What kinds?
140 | - Did you need to debug generated code?
141 | - Were there type errors or test failures?
142 | - How much manual fixing was required?
143 |
144 | ## Success Criteria
145 |
146 | You've completed the exercise when:
147 |
148 | - ✅ All backend tests pass (`uv run pytest`)
149 | - ✅ Backend API accepts and processes filter parameters correctly
150 | - ✅ Frontend displays a working filter interface
151 | - ✅ Filters can be applied and results update accordingly
152 | - ✅ You can manually test the full filtering flow in the browser
153 |
154 | ## Important Notes
155 |
156 | ### This is Not a Competition
157 |
158 | The goal is to establish **your personal baseline**, not to compete with others. Be honest about:
159 |
160 | - Time taken (don't rush!)
161 | - Prompts used (track them all)
162 | - Confidence levels (be realistic)
163 | - Issues encountered (document everything)
164 |
165 | ### Documentation is Key
166 |
167 | After completing the exercise, you'll reflect on:
168 |
169 | - What worked well in your AI workflow?
170 | - What was frustrating or slow?
171 | - Where did you get stuck?
172 | - What would you want to improve?
173 |
174 | This reflection will inform the learning modules that follow.
175 |
176 | ## After Completion
177 |
178 | When you're done, you'll have:
179 |
180 | 1. A working fullstack filtering feature
181 | 2. Baseline metrics for your AI-assisted coding workflow
182 | 3. Awareness of your current strengths and pain points
183 | 4. Context for the optimization techniques you'll learn next
184 |
185 | ## Questions?
186 |
187 | - Check the main [README.md](./README.md) for project architecture and setup details
188 | - Review existing code patterns in `app/backend/app/` and `app/frontend/src/`
189 | - Run `/health` endpoint to verify backend is running: http://localhost:8000/health
190 | - Check API docs when backend is running: http://localhost:8000/docs
191 |
192 | ## Ready?
193 |
194 | Start implementing and use AI coding assistants however you normally would. Track your time, prompts, and confidence as you go.
195 |
196 | Good luck!
197 |
--------------------------------------------------------------------------------
/app/backend/tests/test_products_filtering.py:
--------------------------------------------------------------------------------
1 | """
2 | Tests for product filtering functionality.
3 |
4 | These tests are currently STUBS - they will fail until you implement
5 | the filtering functionality. Your goal is to make all these tests pass!
6 |
7 | Run these tests with:
8 | pytest tests/test_products_filtering.py -v
9 | """
10 |
11 | import pytest
12 | from fastapi.testclient import TestClient
13 |
14 |
15 | @pytest.mark.skip(reason="Not implemented yet - this is your exercise!")
16 | def test_filter_products_by_minimum_price(test_client: TestClient) -> None:
17 | """
18 | Test filtering products by minimum price.
19 |
20 | When min_price_usd=100 is provided, only products costing $100 or more
21 | should be returned.
22 |
23 | Expected: Should return products like "Wireless Earbuds Pro" ($149.99),
24 | "Smart Robot Vacuum" ($299.99), "Adjustable Dumbbell Set" ($499.99), etc.
25 | """
26 | response = test_client.get("/api/products?min_price_usd=100")
27 | data = response.json()
28 |
29 | assert response.status_code == 200
30 | assert all(
31 | float(product["product_price_usd"]) >= 100
32 | for product in data["products"]
33 | )
34 |
35 |
36 | @pytest.mark.skip(reason="Not implemented yet - this is your exercise!")
37 | def test_filter_products_by_maximum_price(test_client: TestClient) -> None:
38 | """
39 | Test filtering products by maximum price.
40 |
41 | When max_price_usd=30 is provided, only products costing $30 or less
42 | should be returned.
43 |
44 | Expected: Should return products like "Smart LED Light Bulb" ($19.99),
45 | "Classic Cotton T-Shirt" ($24.99), "Merino Wool Beanie" ($29.99), etc.
46 | """
47 | response = test_client.get("/api/products?max_price_usd=30")
48 | data = response.json()
49 |
50 | assert response.status_code == 200
51 | assert all(
52 | float(product["product_price_usd"]) <= 30
53 | for product in data["products"]
54 | )
55 |
56 |
57 | @pytest.mark.skip(reason="Not implemented yet - this is your exercise!")
58 | def test_filter_products_by_price_range(test_client: TestClient) -> None:
59 | """
60 | Test filtering products by both minimum and maximum price.
61 |
62 | When min_price_usd=25 and max_price_usd=50 are provided, only products
63 | in that price range should be returned.
64 |
65 | Expected: Should return products like "Wireless Bluetooth Mouse" ($29.99),
66 | "USB-C Hub 7-in-1" ($45.99), "Ceramic Non-Stick Frying Pan" ($49.99), etc.
67 | """
68 | response = test_client.get("/api/products?min_price_usd=25&max_price_usd=50")
69 | data = response.json()
70 |
71 | assert response.status_code == 200
72 | assert all(
73 | 25 <= float(product["product_price_usd"]) <= 50
74 | for product in data["products"]
75 | )
76 |
77 |
78 | @pytest.mark.skip(reason="Not implemented yet - this is your exercise!")
79 | def test_filter_products_by_category(test_client: TestClient) -> None:
80 | """
81 | Test filtering products by category.
82 |
83 | When category=electronics is provided, only electronics products
84 | should be returned.
85 |
86 | Expected: Should return 8 electronics products (IDs 1-8).
87 | """
88 | response = test_client.get("/api/products?category=electronics")
89 | data = response.json()
90 |
91 | assert response.status_code == 200
92 | assert all(
93 | product["product_category"] == "electronics"
94 | for product in data["products"]
95 | )
96 | # We have 8 electronics products in seed data
97 | assert len(data["products"]) == 8
98 |
99 |
100 | @pytest.mark.skip(reason="Not implemented yet - this is your exercise!")
101 | def test_search_products_by_keyword(test_client: TestClient) -> None:
102 | """
103 | Test searching products by keyword.
104 |
105 | When search_keyword=wireless is provided, only products with "wireless"
106 | in their name or description should be returned (case-insensitive).
107 |
108 | Expected: Should return "Wireless Bluetooth Mouse", "Wireless Earbuds Pro",
109 | "Wireless Charger Stand", etc.
110 | """
111 | response = test_client.get("/api/products?search_keyword=wireless")
112 | data = response.json()
113 |
114 | assert response.status_code == 200
115 | # All returned products should have "wireless" in name or description
116 | for product in data["products"]:
117 | name_lower = product["product_name"].lower()
118 | desc_lower = product["product_description"].lower()
119 | assert "wireless" in name_lower or "wireless" in desc_lower
120 |
121 |
122 | @pytest.mark.skip(reason="Not implemented yet - this is your exercise!")
123 | def test_filter_with_multiple_parameters(test_client: TestClient) -> None:
124 | """
125 | Test filtering with multiple parameters combined.
126 |
127 | When category=electronics AND max_price_usd=50 are provided, only
128 | electronics products costing $50 or less should be returned.
129 |
130 | Expected: Should return products like "Wireless Bluetooth Mouse" ($29.99),
131 | "USB-C Hub 7-in-1" ($45.99), "Smart LED Light Bulb" ($19.99), etc.
132 | """
133 | response = test_client.get("/api/products?category=electronics&max_price_usd=50")
134 | data = response.json()
135 |
136 | assert response.status_code == 200
137 | assert all(
138 | product["product_category"] == "electronics" and
139 | float(product["product_price_usd"]) <= 50
140 | for product in data["products"]
141 | )
142 |
143 |
144 | @pytest.mark.skip(reason="Not implemented yet - this is your exercise!")
145 | def test_invalid_price_range_returns_400(test_client: TestClient) -> None:
146 | """
147 | Test that invalid price range (min > max) returns HTTP 400 error.
148 |
149 | When min_price_usd is greater than max_price_usd, the API should
150 | return a 400 Bad Request error with a clear error message.
151 |
152 | Expected error response:
153 | {
154 | "error_code": "invalid_price_range",
155 | "error_message": "Minimum price cannot exceed maximum price",
156 | "error_details": {...},
157 | "timestamp_utc": "..."
158 | }
159 | """
160 | response = test_client.get("/api/products?min_price_usd=100&max_price_usd=50")
161 | data = response.json()
162 |
163 | assert response.status_code == 400
164 | assert "error_code" in data
165 | assert data["error_code"] == "invalid_price_range"
166 |
167 |
168 | @pytest.mark.skip(reason="Not implemented yet - this is your exercise!")
169 | def test_no_filters_returns_all_products(test_client: TestClient) -> None:
170 | """
171 | Test that when no filters are provided, all products are returned.
172 |
173 | This ensures that filtering is optional and the default behavior
174 | (no parameters) still works correctly.
175 |
176 | Expected: Should return all 30 products.
177 | """
178 | response = test_client.get("/api/products")
179 | data = response.json()
180 |
181 | assert response.status_code == 200
182 | assert data["total_count"] == 30
183 | assert len(data["products"]) == 30
184 |
--------------------------------------------------------------------------------
/app/backend/app/core/logging_config.py:
--------------------------------------------------------------------------------
1 | """
2 | Structured JSON logging configuration for AI-debuggable output.
3 |
4 | This module configures logging to output structured JSON to stdout, making it
5 | easy for AI coding assistants to read and understand errors, debugging context,
6 | and application behavior.
7 |
8 | Key principles for AI-friendly logging:
9 | 1. Use JSON format for machine readability
10 | 2. Include contextual fields (operation, parameters, results)
11 | 3. Log to stdout (AI can read it directly)
12 | 4. Use descriptive event names (filtering_products, validation_failed, etc.)
13 | 5. Include fix_suggestion fields in error logs
14 | """
15 |
16 | import json
17 | import logging
18 | import sys
19 | from datetime import UTC, datetime
20 | from typing import Any
21 |
22 |
23 | class JsonFormatter(logging.Formatter):
24 | """
25 | Formats log records as JSON objects for structured logging.
26 |
27 | Each log record is converted to a JSON object with:
28 | - timestamp: ISO 8601 formatted UTC timestamp
29 | - level: Log level (INFO, ERROR, WARNING, etc.)
30 | - logger_name: Name of the logger that generated the record
31 | - message: The log message
32 | - **extra_fields: Any additional context passed via extra={}
33 |
34 | Example output:
35 | {
36 | "timestamp": "2025-01-15T10:30:45.123456Z",
37 | "level": "INFO",
38 | "logger_name": "app.services.product_service",
39 | "message": "filtering_products",
40 | "filter_category": "electronics",
41 | "total_products": 30
42 | }
43 | """
44 |
45 | def format(self, record: logging.LogRecord) -> str:
46 | """
47 | Format a log record as a JSON string.
48 |
49 | Args:
50 | record: The log record to format
51 |
52 | Returns:
53 | JSON-formatted string representation of the log record
54 | """
55 | log_data: dict[str, Any] = {
56 | "timestamp": datetime.now(UTC).isoformat(),
57 | "level": record.levelname,
58 | "logger_name": record.name,
59 | "message": record.getMessage(),
60 | }
61 |
62 | # Add any extra fields that were passed to the logger
63 | # (e.g., logger.info("event", extra={"field": "value"}))
64 | if hasattr(record, "extra_fields"):
65 | log_data.update(record.extra_fields) # type: ignore[attr-defined]
66 |
67 | # Add exception info if present
68 | if record.exc_info:
69 | log_data["exception"] = self.formatException(record.exc_info)
70 |
71 | return json.dumps(log_data, default=str)
72 |
73 |
74 | def setup_logging(log_level: str = "INFO") -> None:
75 | """
76 | Configure application-wide structured JSON logging to stdout.
77 |
78 | This function sets up all loggers to output JSON-formatted logs to stdout,
79 | making it easy for AI to read and understand application behavior.
80 |
81 | Args:
82 | log_level: The minimum log level to output (DEBUG, INFO, WARNING, ERROR, CRITICAL)
83 |
84 | Example:
85 | >>> setup_logging("INFO")
86 | >>> logger = logging.getLogger(__name__)
87 | >>> logger.info(
88 | ... "Processing products",
89 | ... extra={"total_products": 30, "filter_category": "electronics"}
90 | ... )
91 | """
92 | # Create JSON formatter
93 | json_formatter = JsonFormatter()
94 |
95 | # Configure stdout handler
96 | stdout_handler = logging.StreamHandler(sys.stdout)
97 | stdout_handler.setFormatter(json_formatter)
98 |
99 | # Configure root logger
100 | root_logger = logging.getLogger()
101 | root_logger.setLevel(getattr(logging, log_level.upper()))
102 | root_logger.addHandler(stdout_handler)
103 |
104 | # Prevent duplicate logs from propagating
105 | root_logger.propagate = False
106 |
107 |
108 | def get_logger(logger_name: str) -> logging.Logger:
109 | """
110 | Get a configured logger instance with structured logging support.
111 |
112 | This is a convenience function that returns a logger configured for
113 | JSON output. Use extra={} to add structured fields to your logs.
114 |
115 | Args:
116 | logger_name: Name for the logger (typically __name__)
117 |
118 | Returns:
119 | Configured logger instance
120 |
121 | Example:
122 | >>> logger = get_logger(__name__)
123 | >>> logger.info(
124 | ... "filtering_products",
125 | ... extra={
126 | ... "filter_category": "electronics",
127 | ... "min_price": "10.00",
128 | ... "max_price": "100.00"
129 | ... }
130 | ... )
131 |
132 | >>> logger.error(
133 | ... "validation_failed",
134 | ... extra={
135 | ... "error_type": "invalid_price_range",
136 | ... "error_details": {"min_price": "100", "max_price": "50"},
137 | ... "fix_suggestion": "Ensure min_price_usd <= max_price_usd"
138 | ... }
139 | ... )
140 | """
141 | return logging.getLogger(logger_name)
142 |
143 |
144 | # Custom adapter for cleaner API
145 | class StructuredLogger:
146 | """
147 | Logger wrapper that makes structured logging more ergonomic.
148 |
149 | Instead of passing extra={} every time, this wrapper lets you pass
150 | fields directly as keyword arguments.
151 |
152 | Example:
153 | >>> logger = StructuredLogger(__name__)
154 | >>> logger.info(
155 | ... "filtering_products",
156 | ... filter_category="electronics",
157 | ... total_products=30
158 | ... )
159 | """
160 |
161 | def __init__(self, logger_name: str):
162 | """Initialize with a logger name."""
163 | self._logger = logging.getLogger(logger_name)
164 |
165 | def _log(self, level: int, message: str, **fields: Any) -> None:
166 | """Internal method to log with structured fields."""
167 | # Create a modified record that includes our fields
168 | record = self._logger.makeRecord(
169 | self._logger.name,
170 | level,
171 | "", # pathname
172 | 0, # lineno
173 | message,
174 | (), # args
175 | None, # exc_info
176 | )
177 | record.extra_fields = fields
178 | self._logger.handle(record)
179 |
180 | def debug(self, message: str, **fields: Any) -> None:
181 | """Log a debug message with structured fields."""
182 | self._log(logging.DEBUG, message, **fields)
183 |
184 | def info(self, message: str, **fields: Any) -> None:
185 | """Log an info message with structured fields."""
186 | self._log(logging.INFO, message, **fields)
187 |
188 | def warning(self, message: str, **fields: Any) -> None:
189 | """Log a warning message with structured fields."""
190 | self._log(logging.WARNING, message, **fields)
191 |
192 | def error(self, message: str, **fields: Any) -> None:
193 | """Log an error message with structured fields."""
194 | self._log(logging.ERROR, message, **fields)
195 |
196 | def critical(self, message: str, **fields: Any) -> None:
197 | """Log a critical message with structured fields."""
198 | self._log(logging.CRITICAL, message, **fields)
199 |
--------------------------------------------------------------------------------
/exercises/exercise_2.md:
--------------------------------------------------------------------------------
1 | # Exercise 2: Systematic Implementation (PIV Loop)
2 |
3 | ## Overview
4 |
5 | This is the second exercise in the AI Coding Summit workshop. After experiencing your natural workflow in Exercise 1, you'll now learn and apply the **PIV Loop** - a systematic approach to AI-assisted coding that dramatically improves output quality and gives you more control over the implementation.
6 |
7 | **PIV Loop**: Planning → Implementing → Validating → Iterating
8 |
9 | ## Goals
10 |
11 | 1. **Experience systematic AI-assisted coding** using the PIV Loop methodology
12 | 2. **Compare the difference** in process smoothness and control versus Exercise 1
13 | 3. **Learn to create rich context** that guides AI toward better implementations
14 | 4. **Stay in the driver's seat** while delegating coding work to your AI assistant
15 |
16 | ## The PIV Loop Methodology
17 |
18 | ### 1. Planning
19 |
20 | **Before writing any code**, create a comprehensive implementation plan that includes:
21 |
22 | - **Success Criteria**: What does "done" look like? How will you validate it?
23 | - **Documentation References**: What files, patterns, or conventions should be followed?
24 | - **Task Breakdown**: What are the specific steps needed?
25 | - **Desired Codebase Structure**: What files will be created/modified? What patterns should be used?
26 |
27 | This plan becomes the **context** you provide to your AI assistant for every prompt.
28 |
29 | ### 2. Implementing
30 |
31 | Execute your plan with AI assistance:
32 |
33 | - Provide your full plan as context in your prompts
34 | - Reference specific parts of the plan for each implementation step
35 | - Keep the AI focused on your desired structure and patterns
36 | - Maintain control over what gets implemented and how
37 |
38 | ### 3. Validating
39 |
40 | Verify that the implementation matches your plan:
41 |
42 | - Run automated tests
43 | - Check that code follows the patterns you specified
44 | - Verify that success criteria are met
45 | - Look for deviations from your plan
46 |
47 | ### 4. Iterating
48 |
49 | Refine based on validation results:
50 |
51 | - Fix any issues found during validation
52 | - Adjust the plan if needed based on what you learned
53 | - Keep the AI aligned with your updated understanding
54 | - Maintain the quality bar you set in your plan
55 |
56 | ## The Task
57 |
58 | You'll implement the same product filtering feature from Exercise 1, but this time using the PIV Loop methodology.
59 |
60 | ### Part 1: Create Your Implementation Plan
61 |
62 | Start with the structured task specifications provided:
63 |
64 | - **Backend Task**: `tasks/TASK1.md`
65 | - **Frontend Task**: `tasks/TASK2.md`
66 |
67 | These tasks are written in the format you might receive from a project management tool like Linear, Jira, or Asana. They include:
68 |
69 | - Requirements and acceptance criteria
70 | - Technical notes and constraints
71 | - Definition of done
72 |
73 | Use these task specifications to create a detailed implementation plan that includes:
74 |
75 | 1. **Success Criteria**: How will you know you're done?
76 | - All tests in `app/backend/tests/test_products_filtering.py` pass
77 | - Frontend UI provides all required filtering controls
78 | - Manual testing checklist items pass
79 |
80 | 2. **Documentation to Reference**:
81 | - Existing code patterns in `app/backend/app/`
82 | - Existing frontend patterns in `app/frontend/src/`
83 | - Test files that show expected behavior
84 |
85 | 3. **Task Breakdown**:
86 | - Backend: Parameter model, service function, endpoint updates
87 | - Frontend: Filter component, API client updates, state management
88 |
89 | 4. **Desired Codebase Structure**:
90 | - What naming conventions to use
91 | - What file organization to follow
92 | - What logging patterns to implement
93 | - What type safety to maintain
94 |
95 | ### Part 2: Implement Using Your Plan
96 |
97 | With your plan created, implement the features:
98 |
99 | - Provide your full plan as context when prompting your AI assistant
100 | - Reference specific sections of your plan for each step
101 | - Keep your AI focused on your desired approach
102 | - Notice how much more control you have compared to Exercise 1
103 |
104 | ### Part 3: Validate and Iterate
105 |
106 | After implementation:
107 |
108 | - Run the test suite: `cd app/backend && uv run pytest tests/ -v`
109 | - Test the UI manually
110 | - Check that code follows your planned patterns
111 | - Iterate on any issues found
112 |
113 | ## What to Track
114 |
115 | As you work through Exercise 2, track:
116 |
117 | ### Process Comparison
118 |
119 | - **Planning Time**: How long did it take to create your plan?
120 | - **Implementation Time**: How long to implement with the plan as context?
121 | - **Validation Time**: How long to test and verify?
122 | - **Total Time**: Compare to Exercise 1 total time
123 |
124 | ### Control and Understanding
125 |
126 | - **How much more control** did you feel compared to Exercise 1?
127 | - **How well did the AI follow** your plan and structure?
128 | - **How much did you understand** what was being implemented?
129 | - **How confident are you** in the resulting code quality?
130 |
131 | ### AI Interaction Quality
132 |
133 | - **Number of prompts**: More or fewer than Exercise 1?
134 | - **Prompt effectiveness**: Did the AI understand your intentions better?
135 | - **Rework required**: How much code needed to be redone?
136 |
137 | ## Success Criteria
138 |
139 | You've completed Exercise 2 when:
140 |
141 | - ✅ You created a comprehensive implementation plan before coding
142 | - ✅ All backend tests pass (`uv run pytest`)
143 | - ✅ Frontend filtering UI works as specified
144 | - ✅ Code follows your planned patterns and structure
145 | - ✅ You can articulate how the PIV Loop improved your workflow
146 |
147 | ## Key Differences from Exercise 1
148 |
149 | | Aspect | Exercise 1 | Exercise 2 |
150 | |--------|-----------|-----------|
151 | | **Approach** | Ad-hoc prompting | Planned, systematic |
152 | | **Context** | Minimal | Rich, structured plan |
153 | | **Control** | Following AI suggestions | Directing AI with plan |
154 | | **Understanding** | Learning as you go | Understanding upfront |
155 | | **Validation** | Reactive debugging | Proactive verification |
156 |
157 | ## Tips for Success
158 |
159 | 1. **Don't skip planning**: It feels like extra work, but it saves time overall
160 | 2. **Make your plan detailed**: The more specific, the better the AI can follow it
161 | 3. **Include examples**: Reference existing code patterns you want to emulate
162 | 4. **Use your plan as context**: Copy relevant sections into every prompt
163 | 5. **Validate frequently**: Don't wait until the end to check if things work
164 | 6. **Update your plan**: If you discover something new, adjust the plan
165 |
166 | ## After Exercise 2
167 |
168 | Reflect on your experience:
169 |
170 | - How much smoother was the process compared to Exercise 1?
171 | - How much more control did you feel over the implementation?
172 | - What parts of the planning phase were most valuable?
173 | - How much more confident are you in the resulting code?
174 | - What would you do differently next time?
175 |
176 | ## Ready?
177 |
178 | 1. Read the task specifications in `tasks/TASK1.md` and `tasks/TASK2.md`
179 | 2. Create your comprehensive implementation plan
180 | 3. Implement using the PIV Loop methodology
181 | 4. Compare your experience to Exercise 1
182 |
183 | The difference should be dramatic. Good luck!
184 |
--------------------------------------------------------------------------------
/rules-and-commands/commands/planning.md:
--------------------------------------------------------------------------------
1 | # Feature Planning
2 |
3 | Create a new plan to implement the `PRP` using the exact specified markdown `PRP Format`. Follow the `Instructions` to create the plan use the `Relevant Files` to focus on the right files.
4 |
5 | ## Variables
6 |
7 | FEATURE $1 $2
8 |
9 | ## Instructions
10 |
11 | - IMPORTANT: You're writing a plan to implement a net new feature based on the `Feature` that will add value to the application.
12 | - IMPORTANT: The `Feature` describes the feature that will be implemented but remember we're not implementing a new feature, we're creating the plan that will be used to implement the feature based on the `PRP Format` below.
13 | - Create the plan in the `PRPs/features/` directory with filename: `{descriptive-name}.md`
14 | - Replace `{descriptive-name}` with a short, descriptive name based on the feature (e.g., "add-auth-system", "implement-search", "create-dashboard")
15 | - Use the `PRP Format` below to create the plan.
16 | - Deeply research the codebase to understand existing patterns, architecture, and conventions before planning the feature.
17 | - If no patterns are established or are unclear ask the user for clarifications while providing best recommendations and options
18 | - IMPORTANT: Replace every in the `PRP Format` with the requested value. Add as much detail as needed to implement the feature successfully.
19 | - Use your reasoning model: THINK HARD about the feature requirements, design, and implementation approach.
20 | - Follow existing patterns and conventions in the codebase. Don't reinvent the wheel.
21 | - Design for extensibility and maintainability.
22 | - Deeply do web research to understand the latest trends and technologies in the field.
23 | - Figure out latest best practices and library documentation.
24 | - Include links to relevant resources and documentation with anchor tags for easy navigation.
25 | - If you need a new library, use `uv add ` and report it in the `Notes` section.
26 | - Read `CLAUDE.md` for project principles, logging rules, testing requirements, and docstring style.
27 | - All code MUST have type annotations (strict mypy enforcement).
28 | - Use Google-style docstrings for all functions, classes, and modules.
29 | - Every new file in `src/` MUST have a corresponding test file in `tests/`.
30 | - Respect requested files in the `Relevant Files` section.
31 |
32 | ## Relevant Files
33 |
34 | Focus on the following files and vertical slice structure:
35 |
36 | **Core Files:**
37 |
38 | - `CLAUDE.md` - Project instructions, logging rules, testing requirements, docstring style
39 | app/backend core files
40 | app/frontend core files
41 |
42 | ## PRP Format
43 |
44 | ```md
45 | # Feature:
46 |
47 | ## Feature Description
48 |
49 |
50 |
51 | ## User Story
52 |
53 | As a
54 | I want to
55 | So that
56 |
57 | ## Problem Statement
58 |
59 |
60 |
61 | ## Solution Statement
62 |
63 |
64 |
65 | ## Relevant Files
66 |
67 | Use these files to implement the feature:
68 |
69 |
70 |
71 | ## Relevant research docstring
72 |
73 | Use these documentation files and links to help with understanding the technology to use:
74 |
75 | - [Documentation Link 1](https://example.com/doc1)
76 | - [Anchor tag]
77 | - [Short summary]
78 | - [Documentation Link 2](https://example.com/doc2)
79 | - [Anchor tag]
80 | - [Short summary]
81 |
82 | ## Implementation Plan
83 |
84 | ### Phase 1: Foundation
85 |
86 |
87 |
88 | ### Phase 2: Core Implementation
89 |
90 |
91 |
92 | ### Phase 3: Integration
93 |
94 |
95 |
96 | ## Step by Step Tasks
97 |
98 | IMPORTANT: Execute every step in order, top to bottom.
99 |
100 |
109 |
110 | /test_.py`
116 | - Add integration test in `tests/integration/` if needed>
117 |
118 | ## Testing Strategy
119 |
120 | See `CLAUDE.md` for complete testing requirements. Every file in `src/` must have a corresponding test file in `tests/`.
121 |
122 | ### Unit Tests
123 |
124 |
125 |
126 | ### Integration Tests
127 |
128 |
129 |
130 | ### Edge Cases
131 |
132 |
133 |
134 | ## Acceptance Criteria
135 |
136 |
137 |
138 | ## Validation Commands
139 |
140 | Execute every command to validate the feature works correctly with zero regressions.
141 |
142 |
150 |
151 | **Required validation commands:**
152 |
153 | - `uv run ruff check src/` - Lint check must pass
154 | - `uv run mypy src/` - Type check must pass
155 | - `uv run pytest tests/ -v` - All tests must pass with zero regressions
156 |
157 | **Run server and test core endpoints:**
158 |
159 | - Start server: @.claude/start-server
160 | - Test endpoints with curl (at minimum: health check, main functionality)
161 | - Verify structured logs show proper correlation IDs and context
162 | - Stop server after validation
163 |
164 | ## Notes
165 |
166 |
167 | ```
168 |
169 | ## Feature
170 |
171 | Extract the feature details from the `issue_json` variable (parse the JSON and use the title and body fields).
172 |
173 | ## Report
174 |
175 | - Summarize the work you've just done in a concise bullet point list.
176 | - Include the full path to the plan file you created (e.g., `PRPs/features/add-auth-system.md`)
177 |
--------------------------------------------------------------------------------
/exercises/exercise_3.md:
--------------------------------------------------------------------------------
1 | # Exercise 3: Reusable Prompts Only
2 |
3 | ## Overview
4 |
5 | This is the third and final exercise in the AI Coding Summit workshop. After learning the PIV Loop in Exercise 2, you'll now master the ultimate level of systematic AI-assisted coding: **using only reusable prompts** (also known as slash commands or /commands).
6 |
7 | In this exercise, you're **not allowed** to type directly into your AI assistant's chat except to invoke a command. All interactions must go through pre-defined reusable prompts. This constraint forces you to think systematically and helps you build a library of reliable, repeatable workflows.
8 |
9 | ## Goals
10 |
11 | 1. **Master reusable prompts** for consistent, efficient AI-assisted coding
12 | 2. **Build systematic workflows** that can be reused across projects
13 | 3. **Enforce best practices** through structured prompts
14 | 4. **Experience ultimate control** over AI interactions
15 |
16 | ## What Are Reusable Prompts?
17 |
18 | Reusable prompts (also called slash commands or /commands) are pre-written prompts that:
19 |
20 | - Embed consistent context and instructions
21 | - Enforce best practices and patterns
22 | - Can be invoked with a simple command
23 | - Provide repeatable, reliable results
24 |
25 | **Example**: Instead of typing "please implement a filter function for products", you invoke a command like `/implement` that includes comprehensive context about your codebase patterns, logging requirements, type safety standards, etc.
26 |
27 | ## The Challenge: Commands Only
28 |
29 | For this exercise, you must:
30 |
31 | - ✅ Use ONLY the commands in `rules-and-commands/commands/`
32 | - ✅ Leverage the global rules in `rules-and-commands/rules/`
33 | - ✅ Edit existing commands or create new ones as needed
34 | - ❌ NO typing directly into the AI chat
35 | - ❌ NO ad-hoc prompts
36 |
37 | This constraint might feel limiting at first, but it teaches you to:
38 |
39 | - Think about what reusable patterns you need
40 | - Build a library of effective prompts
41 | - Achieve consistency across all implementations
42 |
43 | ## Available Resources
44 |
45 | ### Global Rules (`rules-and-commands/rules/`)
46 |
47 | Global rules are automatically included in every AI interaction. They set the baseline standards for:
48 |
49 | - Code quality and style
50 | - Naming conventions
51 | - Logging patterns
52 | - Type safety requirements
53 | - Testing practices
54 |
55 | Review these rules to understand the baseline context your AI assistant always has.
56 |
57 | ### Commands (`rules-and-commands/commands/`)
58 |
59 | Commands are reusable prompts you can invoke. Explore the existing commands to see what's available. Common command patterns might include:
60 |
61 | - `/plan` - Create an implementation plan
62 | - `/implement` - Implement a feature following patterns
63 | - `/test` - Run tests and fix issues
64 | - `/review` - Review code quality
65 | - `/debug` - Debug a specific issue
66 |
67 | **Note**: The actual commands available depend on what's in your `rules-and-commands/commands/` folder. Review that folder to see what's provided.
68 |
69 | ## The Task
70 |
71 | Implement a new feature or enhancement to the product catalog application using ONLY reusable prompts.
72 |
73 | ### Part 1: Understand Your Tools
74 |
75 | Before starting:
76 |
77 | 1. **Review Global Rules**: Read all files in `rules-and-commands/rules/`
78 | - What standards are automatically enforced?
79 | - What context is always provided?
80 |
81 | 2. **Review Available Commands**: Read all files in `rules-and-commands/commands/`
82 | - What commands exist?
83 | - What does each command do?
84 | - What parameters do they accept?
85 |
86 | 3. **Identify Gaps**: What commands might you need that don't exist?
87 |
88 | ### Part 2: Plan Your Command Strategy
89 |
90 | Think about your implementation workflow:
91 |
92 | - What command will you use for planning?
93 | - What command will you use for implementation?
94 | - What command will you use for validation?
95 | - Do you need to create or edit any commands?
96 |
97 | ### Part 3: Implement Using Commands Only
98 |
99 | Execute your implementation:
100 |
101 | - Invoke commands for each step of your workflow
102 | - Edit commands if they don't quite fit your needs
103 | - Create new commands if you discover missing patterns
104 | - **Never type directly into the chat** - only use commands
105 |
106 | ### Part 4: Reflect on Command Effectiveness
107 |
108 | After implementation:
109 |
110 | - Which commands were most valuable?
111 | - Which commands needed editing?
112 | - What new commands did you create?
113 | - How reusable are your commands for future projects?
114 |
115 | ## What to Track
116 |
117 | As you work through Exercise 3, track:
118 |
119 | ### Workflow Metrics
120 |
121 | - **Commands Used**: Which commands did you invoke?
122 | - **Commands Created**: What new commands did you add?
123 | - **Commands Edited**: What existing commands did you modify?
124 | - **Time to Completion**: How long compared to Exercises 1 and 2?
125 |
126 | ### Quality and Control
127 |
128 | - **Consistency**: How consistent was the AI output?
129 | - **Control**: How much control did you have over the implementation?
130 | - **Understanding**: How well do you understand the patterns?
131 | - **Reusability**: How reusable are your commands for future work?
132 |
133 | ### Comparison to Previous Exercises
134 |
135 | | Metric | Exercise 1 | Exercise 2 | Exercise 3 |
136 | |--------|-----------|-----------|-----------|
137 | | **Approach** | Ad-hoc | Planned | Systematic |
138 | | **Context** | Minimal | Rich plan | Embedded rules |
139 | | **Consistency** | Variable | Better | Excellent |
140 | | **Reusability** | None | Some | High |
141 | | **Control** | Low | Medium | High |
142 |
143 | ## Success Criteria
144 |
145 | You've completed Exercise 3 when:
146 |
147 | - ✅ Feature implemented using ONLY reusable prompts (no direct chat)
148 | - ✅ All tests pass and feature works as expected
149 | - ✅ Code follows all global rules and patterns
150 | - ✅ You have a library of commands you could reuse on future projects
151 | - ✅ You understand the power of systematic, reusable workflows
152 |
153 | ## Tips for Success
154 |
155 | ### 1. Start with Planning Commands
156 |
157 | Use a command to create your plan, don't try to plan in your head.
158 |
159 | ### 2. Edit Commands Liberally
160 |
161 | If a command doesn't quite fit, edit it! Commands should evolve to match your needs.
162 |
163 | ### 3. Create Commands for Common Patterns
164 |
165 | If you find yourself wanting to do something repeatedly, create a command for it.
166 |
167 | ### 4. Keep Commands Focused
168 |
169 | Each command should do one thing well. Don't try to make a command that does everything.
170 |
171 | ### 5. Include Rich Context in Commands
172 |
173 | Commands should embed all the context the AI needs:
174 | - References to documentation
175 | - Examples of patterns to follow
176 | - Success criteria
177 | - Validation steps
178 |
179 | ### 6. Test Your Commands
180 |
181 | After creating or editing a command, test it to make sure it produces the results you want.
182 |
183 | ## Example Command Structure
184 |
185 | Here's what a good command might look like:
186 |
187 | ```markdown
188 | # /implement
189 |
190 | You are implementing a feature in the product catalog application.
191 |
192 | **Context**:
193 | - Review existing patterns in app/backend/app/ and app/frontend/src/
194 | - Follow the service layer architecture
195 | - Use comprehensive type hints
196 | - Implement structured JSON logging
197 | - Write tests alongside implementation
198 |
199 | **Task**: [User will provide task description]
200 |
201 | **Process**:
202 | 1. Review task requirements
203 | 2. Check existing code patterns
204 | 3. Implement following established patterns
205 | 4. Add structured logging
206 | 5. Update/create tests
207 | 6. Validate implementation
208 |
209 | **Success Criteria**:
210 | - All tests pass
211 | - Code follows existing patterns
212 | - Logging is comprehensive
213 | - Types are complete
214 | ```
215 |
216 | ## After Exercise 3
217 |
218 | Reflect on the three exercises:
219 |
220 | ### Exercise 1: Baseline
221 | - How did it feel to work without structure?
222 | - What challenges did you face?
223 | - How much control did you have?
224 |
225 | ### Exercise 2: PIV Loop
226 | - How did planning change the experience?
227 | - How much more control did structured prompting give you?
228 | - What was the quality difference?
229 |
230 | ### Exercise 3: Reusable Prompts
231 | - How did commands enforce consistency?
232 | - How reusable is your command library?
233 | - What commands will you use in your daily work?
234 |
235 | ## Building Your Command Library
236 |
237 | After completing this workshop, continue building your command library:
238 |
239 | 1. **Create commands for your common tasks**:
240 | - Feature implementation
241 | - Bug fixing
242 | - Code review
243 | - Refactoring
244 | - Documentation
245 |
246 | 2. **Embed your team's standards**:
247 | - Coding conventions
248 | - Architecture patterns
249 | - Testing requirements
250 | - Documentation format
251 |
252 | 3. **Share with your team**:
253 | - Consistent prompts = consistent code
254 | - Team members can use the same commands
255 | - Onboarding becomes easier
256 |
257 | 4. **Iterate and improve**:
258 | - Commands should evolve with your needs
259 | - Update them based on what works
260 | - Remove or consolidate commands that don't add value
261 |
262 | ## Ready?
263 |
264 | 1. Review the global rules in `rules-and-commands/rules/`
265 | 2. Explore available commands in `rules-and-commands/commands/`
266 | 3. Plan which commands you'll use (using a command, of course!)
267 | 4. Implement using ONLY reusable prompts
268 | 5. Reflect on how systematic AI-assisted coding has transformed your workflow
269 |
270 | Remember: You cannot type directly into the chat. All interactions must be through commands. This constraint is the key to building systematic, reusable workflows.
271 |
272 | Good luck with your final exercise!
273 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # AI Coding Summit Workshop
2 |
3 | A hands-on workshop that demonstrates the dramatic difference between fumbling and systematic AI-assisted coding. This project contains a full-stack e-commerce product catalog application designed as a training ground for learning effective AI coding techniques.
4 |
5 | **Prerequisites**: If you need to install any prerequisites (Python, Node, Git, an AI coding assistant), see the [SETUP_GUIDE.md](./SETUP_GUIDE.md).
6 |
7 | ## Quick Start
8 |
9 | Get the application running in two terminals:
10 |
11 | **Terminal 1 - Backend**:
12 | ```bash
13 | cd app/backend
14 | uv venv --python 3.12
15 | uv sync
16 | uv run python run_api.py
17 | # → http://localhost:8000
18 | ```
19 |
20 | **Terminal 2 - Frontend**:
21 | ```bash
22 | cd app/frontend
23 | npm install -g bun
24 | bun install
25 | bun dev
26 | # → http://localhost:3000
27 | ```
28 |
29 | ## Project Overview
30 |
31 | This workshop challenges participants to implement product filtering capabilities in a full-stack application (FastAPI backend + React frontend). The same feature will be implemented multiple times using progressively more sophisticated approaches to AI-assisted coding.
32 |
33 | ## The Three Exercises
34 |
35 | ### Exercise 1: Baseline Implementation
36 |
37 | **Goal**: Establish your personal baseline for AI-assisted coding without structured techniques.
38 |
39 | In this first exercise, you'll implement product filtering features in both the backend and frontend exactly as you would in a normal project today. Use AI coding assistants however you're most comfortable - there are no rules or restrictions.
40 |
41 | **Important Note**: Your implementation will likely work, even with simple prompts - AI coding assistants are powerful enough to handle these features. This project is intentionally straightforward because **the goal isn't to struggle with getting code to work**. Instead, pay attention to:
42 | - How smooth does the process feel?
43 | - How much do you understand what's being implemented?
44 | - How much control do you feel you have over the specific implementation details?
45 | - Are you in the driver's seat, or just along for the ride?
46 |
47 | This workshop is about learning to stay in the driver's seat while delegating the coding work to your AI assistant.
48 |
49 | **What You'll Do**:
50 | - Add filtering capabilities to the FastAPI backend (price, category, keyword search, sorting)
51 | - Build a React filtering interface that connects to your backend API
52 | - Get all tests passing and verify the feature works end-to-end
53 |
54 | **What to Track**:
55 | - Time spent on backend and frontend
56 | - Number of AI prompts and iterations
57 | - How much control you felt over the implementation details
58 | - How well you understand what was implemented
59 | - Issues encountered and debugging time
60 |
61 | **See [exercises/exercise_1.md](./exercises/exercise_1.md) for complete Exercise 1 instructions.**
62 |
63 | ### Exercise 2: Systematic Implementation (PIV Loop)
64 |
65 | **Goal**: Experience the power of systematic planning and structured AI prompting.
66 |
67 | After learning about the PIV Loop (Planning → Implementing → Validating → Iterating), you'll implement a new feature using a structured approach. This exercise demonstrates how upfront planning and rich context dramatically improves AI output quality and gives you more control over the implementation.
68 |
69 | **The PIV Loop Approach**:
70 | 1. **Planning**: Create a detailed implementation plan with:
71 | - Clear success criteria
72 | - Documentation references
73 | - Task breakdown
74 | - Desired codebase structure
75 | 2. **Implementing**: Execute the plan with AI assistance
76 | 3. **Validating**: Run tests and verify functionality
77 | 4. **Iterating**: Refine based on validation results
78 |
79 | **What You'll Do**:
80 | - Start with the structured task specifications in `tasks/TASK1.md` and `tasks/TASK2.md`
81 | - Create a comprehensive implementation plan before writing code
82 | - Use the plan as context for all AI prompts
83 | - Track how much smoother the process feels compared to Exercise 1 and the level of control you have
84 |
85 | **See [exercises/exercise_2.md](./exercises/exercise_2.md) for complete Exercise 2 instructions.**
86 |
87 | ### Exercise 3: Reusable Prompts Only
88 |
89 | **Goal**: Master the use of global rules and reusable prompts for consistent, efficient AI coding.
90 |
91 | After learning about global rules and slash commands (reusable prompts), you'll implement features using ONLY pre-defined reusable prompts. This constraint forces you to leverage systematic patterns so you can learn to build your own reliable and reusable workflows for using AI coding assistants.
92 |
93 | **The Challenge**:
94 | - Implement features using ONLY the commands in `rules-and-commands/commands/`
95 | - Leverage the global rules in `rules-and-commands/rules/`
96 | - You can edit existing commands or create new ones, but all AI interactions must go through reusable prompts
97 | - NO typing directly into the AI chat except to invoke commands
98 |
99 | **What You'll Learn**:
100 | - How reusable prompts enforce best practices
101 | - The power of consistent context across all AI interactions
102 | - How to build a library of effective prompts for your workflow
103 |
104 | **See [exercises/exercise_3.md](./exercises/exercise_3.md) for complete Exercise 3 instructions.**
105 |
106 | ## Project Architecture
107 |
108 | ### Backend (FastAPI + Python 3.12)
109 |
110 | **Location**: `app/backend/`
111 |
112 | **Tech Stack**:
113 | - FastAPI for REST API
114 | - Pydantic for data validation
115 | - Structured JSON logging
116 | - pytest for testing
117 | - Ruff for code quality
118 |
119 | **Key Features**:
120 | - Service layer architecture (clear separation of concerns)
121 | - Comprehensive type hints on all functions
122 | - Verbose, AI-friendly naming (`product_price_usd`, `filter_products_by_category_and_price_range`)
123 | - Structured JSON logging to stdout for AI debugging
124 | - Complete docstrings with examples
125 | - 30 sample products across 5 categories
126 |
127 | **Quick Start**:
128 | ```bash
129 | cd app/backend
130 | uv venv --python 3.12
131 | uv sync
132 | uv run python run_api.py
133 | ```
134 |
135 | **Testing**:
136 | ```bash
137 | uv run pytest tests/ -v
138 | ```
139 |
140 | ### Frontend (React 19 + TypeScript)
141 |
142 | **Location**: `app/frontend/`
143 |
144 | **Tech Stack**:
145 | - Bun runtime
146 | - React 19 with TypeScript
147 | - shadcn/ui components (New York theme)
148 | - Tailwind CSS v4
149 | - Biome for linting and formatting
150 | - React Hook Form + Zod validation
151 |
152 | **Key Features**:
153 | - Types match backend Pydantic models EXACTLY
154 | - Structured JSON logging (mirrors backend pattern)
155 | - Type-safe API client
156 | - Proper loading, error, and empty states
157 | - Accessibility-first component patterns
158 | - Hot module reloading for fast development
159 |
160 | **Quick Start**:
161 | ```bash
162 | cd app/frontend
163 | bun install
164 | bun dev # → http://localhost:3000
165 | ```
166 |
167 | **Code Quality**:
168 | ```bash
169 | bun run check # Lint and format check
170 | bun run check:fix # Auto-fix issues
171 | ```
172 |
173 | ## Development Workflow
174 |
175 | 1. **Start Backend** (Terminal 1):
176 | ```bash
177 | cd app/backend
178 | uv run python run_api.py
179 | # → http://localhost:8000
180 | ```
181 |
182 | 2. **Start Frontend** (Terminal 2):
183 | ```bash
184 | cd app/frontend
185 | bun dev
186 | # → http://localhost:3000
187 | ```
188 |
189 | 3. **Run Tests** (Terminal 3):
190 | ```bash
191 | cd app/backend
192 | uv run pytest tests/ -v
193 | ```
194 |
195 | ## Key Learning Principles
196 |
197 | ### 1. AI-Friendly Code Patterns
198 |
199 | Both backend and frontend demonstrate patterns that work exceptionally well with AI:
200 |
201 | - **Verbose naming**: `product_price_usd` instead of `price`
202 | - **Complete type hints**: Every function fully typed
203 | - **Structured logging**: JSON logs for AI debugging
204 | - **Comprehensive docs**: Docstrings with examples everywhere
205 | - **Clear patterns**: Consistent architecture makes AI suggestions more accurate
206 |
207 | ### 2. Context Engineering
208 |
209 | The exercises progressively demonstrate how rich context improves AI output:
210 |
211 | - **Exercise 1**: Minimal context → lots of iteration and debugging
212 | - **Exercise 2**: Rich planning context → smooth implementation
213 | - **Exercise 3**: Reusable prompts with embedded context → consistent excellence
214 |
215 | ### 3. Validation-First Development
216 |
217 | Tests are provided upfront to validate your implementation:
218 |
219 | - Backend: `app/backend/tests/test_products_filtering.py`
220 | - Clear acceptance criteria in task specifications
221 | - Fast feedback loops with automated tests
222 |
223 | ## Resources
224 |
225 | - **Exercise 1 Instructions**: [exercises/exercise_1.md](./exercises/exercise_1.md)
226 | - **Exercise 2 Instructions**: [exercises/exercise_2.md](./exercises/exercise_2.md)
227 | - **Exercise 3 Instructions**: [exercises/exercise_3.md](./exercises/exercise_3.md)
228 | - **Setup Guide**: [SETUP_GUIDE.md](./SETUP_GUIDE.md)
229 | - **Task Specifications**: `tasks/TASK1.md` and `tasks/TASK2.md`
230 | - **Reusable Prompts**: `rules-and-commands/commands/`
231 | - **Global Rules**: `rules-and-commands/rules/`
232 |
233 | ## Success Metrics
234 |
235 | Track your progress across all three exercises:
236 |
237 | - **Time to completion**: How long does each approach take?
238 | - **AI prompt efficiency**: How many prompts needed?
239 | - **Code confidence**: How confident are you in the result?
240 | - **Debugging effort**: How much manual fixing was required?
241 | - **Learning insights**: What did you discover about effective AI prompting?
242 |
243 | ## Getting Help
244 |
245 | - **API docs**: http://localhost:8000/docs (when backend is running)
246 | - **Health check**: http://localhost:8000/health
247 | - **Type definitions**: `app/backend/app/models/` and `app/frontend/src/types/`
248 | - **Example patterns**: Review existing code in both backend and frontend
249 |
250 | ## Ready to Start?
251 |
252 | Begin with **[exercises/exercise_one.md](./exercises/exercise_one.md)** for Exercise 1. Remember: the goal is to establish your personal baseline first, then experience the dramatic improvement that systematic AI-assisted coding provides.
253 |
254 | Good luck!
255 |
--------------------------------------------------------------------------------
/app/backend/app/data/seed_products.py:
--------------------------------------------------------------------------------
1 | """
2 | Sample product data for the e-commerce catalog.
3 |
4 | This module contains 30 diverse products across all categories to provide
5 | realistic test data for the API. Prices range from $5.99 to $499.99.
6 | """
7 |
8 | from decimal import Decimal
9 |
10 | from app.models.product import Product
11 |
12 |
13 | def get_seed_products() -> list[Product]:
14 | """
15 | Return a list of 30 sample products for the catalog.
16 |
17 | The products are diverse across all five categories (electronics, clothing,
18 | home, sports, books) with a range of prices. Some products are marked as
19 | out of stock to test filtering edge cases.
20 |
21 | Returns:
22 | List of Product objects ready to use in the API
23 |
24 | Example:
25 | >>> products = get_seed_products()
26 | >>> len(products)
27 | 30
28 | >>> products[0].product_category
29 | 'electronics'
30 | """
31 | return [
32 | # Electronics (8 products)
33 | Product(
34 | product_id=1,
35 | product_name="Wireless Bluetooth Mouse",
36 | product_description="Ergonomic wireless mouse with 2.4GHz USB receiver and long-lasting battery life",
37 | product_price_usd=Decimal("29.99"),
38 | product_category="electronics",
39 | product_in_stock=True,
40 | ),
41 | Product(
42 | product_id=2,
43 | product_name="Mechanical Gaming Keyboard",
44 | product_description="RGB backlit mechanical keyboard with blue switches and programmable macros",
45 | product_price_usd=Decimal("89.99"),
46 | product_category="electronics",
47 | product_in_stock=True,
48 | ),
49 | Product(
50 | product_id=3,
51 | product_name="USB-C Hub 7-in-1",
52 | product_description="Multi-port USB-C hub with HDMI, SD card reader, and 100W power delivery",
53 | product_price_usd=Decimal("45.99"),
54 | product_category="electronics",
55 | product_in_stock=True,
56 | ),
57 | Product(
58 | product_id=4,
59 | product_name="Wireless Earbuds Pro",
60 | product_description="Active noise cancelling wireless earbuds with 30-hour battery life and charging case",
61 | product_price_usd=Decimal("149.99"),
62 | product_category="electronics",
63 | product_in_stock=False,
64 | ),
65 | Product(
66 | product_id=5,
67 | product_name="4K Webcam",
68 | product_description="Ultra HD 4K webcam with autofocus, ring light, and dual microphones",
69 | product_price_usd=Decimal("119.99"),
70 | product_category="electronics",
71 | product_in_stock=True,
72 | ),
73 | Product(
74 | product_id=6,
75 | product_name="Portable SSD 1TB",
76 | product_description="Ultra-fast portable solid state drive with USB 3.2 Gen 2 speeds up to 1050MB/s",
77 | product_price_usd=Decimal("129.99"),
78 | product_category="electronics",
79 | product_in_stock=True,
80 | ),
81 | Product(
82 | product_id=7,
83 | product_name="Smart LED Light Bulb",
84 | product_description="WiFi-enabled color-changing LED bulb compatible with Alexa and Google Home",
85 | product_price_usd=Decimal("19.99"),
86 | product_category="electronics",
87 | product_in_stock=True,
88 | ),
89 | Product(
90 | product_id=8,
91 | product_name="Wireless Charger Stand",
92 | product_description="15W fast wireless charging stand with adjustable viewing angle for smartphones",
93 | product_price_usd=Decimal("34.99"),
94 | product_category="electronics",
95 | product_in_stock=True,
96 | ),
97 | # Clothing (7 products)
98 | Product(
99 | product_id=9,
100 | product_name="Classic Cotton T-Shirt",
101 | product_description="100% organic cotton crew neck t-shirt available in multiple colors",
102 | product_price_usd=Decimal("24.99"),
103 | product_category="clothing",
104 | product_in_stock=True,
105 | ),
106 | Product(
107 | product_id=10,
108 | product_name="Slim Fit Denim Jeans",
109 | product_description="Stretch denim jeans with modern slim fit and classic 5-pocket styling",
110 | product_price_usd=Decimal("59.99"),
111 | product_category="clothing",
112 | product_in_stock=True,
113 | ),
114 | Product(
115 | product_id=11,
116 | product_name="Hooded Zip Sweatshirt",
117 | product_description="Comfortable fleece-lined hoodie with full zip and kangaroo pockets",
118 | product_price_usd=Decimal("44.99"),
119 | product_category="clothing",
120 | product_in_stock=True,
121 | ),
122 | Product(
123 | product_id=12,
124 | product_name="Running Jacket Windbreaker",
125 | product_description="Lightweight water-resistant windbreaker with reflective details for running",
126 | product_price_usd=Decimal("69.99"),
127 | product_category="clothing",
128 | product_in_stock=False,
129 | ),
130 | Product(
131 | product_id=13,
132 | product_name="Merino Wool Beanie",
133 | product_description="Soft merino wool winter beanie hat with fold-over cuff design",
134 | product_price_usd=Decimal("29.99"),
135 | product_category="clothing",
136 | product_in_stock=True,
137 | ),
138 | Product(
139 | product_id=14,
140 | product_name="Canvas Sneakers",
141 | product_description="Classic low-top canvas sneakers with rubber sole and cushioned insole",
142 | product_price_usd=Decimal("54.99"),
143 | product_category="clothing",
144 | product_in_stock=True,
145 | ),
146 | Product(
147 | product_id=15,
148 | product_name="Leather Crossbody Bag",
149 | product_description="Genuine leather crossbody bag with adjustable strap and multiple compartments",
150 | product_price_usd=Decimal("89.99"),
151 | product_category="clothing",
152 | product_in_stock=True,
153 | ),
154 | # Home (7 products)
155 | Product(
156 | product_id=16,
157 | product_name="Stainless Steel French Press",
158 | product_description="34oz double-wall insulated French press coffee maker with heat-resistant handle",
159 | product_price_usd=Decimal("39.99"),
160 | product_category="home",
161 | product_in_stock=True,
162 | ),
163 | Product(
164 | product_id=17,
165 | product_name="Ceramic Non-Stick Frying Pan",
166 | product_description="10-inch ceramic-coated frying pan with ergonomic handle and even heat distribution",
167 | product_price_usd=Decimal("49.99"),
168 | product_category="home",
169 | product_in_stock=True,
170 | ),
171 | Product(
172 | product_id=18,
173 | product_name="Memory Foam Pillow Set",
174 | product_description="Set of 2 bamboo-covered memory foam pillows with adjustable fill for custom comfort",
175 | product_price_usd=Decimal("79.99"),
176 | product_category="home",
177 | product_in_stock=True,
178 | ),
179 | Product(
180 | product_id=19,
181 | product_name="Smart Robot Vacuum",
182 | product_description="App-controlled robot vacuum with auto-recharge and scheduled cleaning features",
183 | product_price_usd=Decimal("299.99"),
184 | product_category="home",
185 | product_in_stock=False,
186 | ),
187 | Product(
188 | product_id=20,
189 | product_name="Bamboo Cutting Board Set",
190 | product_description="Set of 3 bamboo cutting boards with juice grooves and non-slip feet",
191 | product_price_usd=Decimal("34.99"),
192 | product_category="home",
193 | product_in_stock=True,
194 | ),
195 | Product(
196 | product_id=21,
197 | product_name="Aromatherapy Essential Oil Diffuser",
198 | product_description="Ultrasonic essential oil diffuser with 7 LED light colors and auto shut-off",
199 | product_price_usd=Decimal("29.99"),
200 | product_category="home",
201 | product_in_stock=True,
202 | ),
203 | Product(
204 | product_id=22,
205 | product_name="Weighted Blanket 15lbs",
206 | product_description="Premium weighted blanket with glass beads and soft breathable cotton cover",
207 | product_price_usd=Decimal("89.99"),
208 | product_category="home",
209 | product_in_stock=True,
210 | ),
211 | # Sports (5 products)
212 | Product(
213 | product_id=23,
214 | product_name="Yoga Mat with Carrying Strap",
215 | product_description="6mm thick non-slip yoga mat with alignment marks and free carrying strap",
216 | product_price_usd=Decimal("39.99"),
217 | product_category="sports",
218 | product_in_stock=True,
219 | ),
220 | Product(
221 | product_id=24,
222 | product_name="Adjustable Dumbbell Set",
223 | product_description="Pair of adjustable dumbbells from 5-52.5 lbs with quick-change dial system",
224 | product_price_usd=Decimal("499.99"),
225 | product_category="sports",
226 | product_in_stock=True,
227 | ),
228 | Product(
229 | product_id=25,
230 | product_name="Resistance Bands Set",
231 | product_description="Set of 5 resistance bands with handles, door anchor, and carrying bag",
232 | product_price_usd=Decimal("24.99"),
233 | product_category="sports",
234 | product_in_stock=True,
235 | ),
236 | Product(
237 | product_id=26,
238 | product_name="Foam Roller for Muscle Recovery",
239 | product_description="High-density foam roller for deep tissue massage and muscle recovery",
240 | product_price_usd=Decimal("29.99"),
241 | product_category="sports",
242 | product_in_stock=True,
243 | ),
244 | Product(
245 | product_id=27,
246 | product_name="Sports Water Bottle 32oz",
247 | product_description="Insulated stainless steel water bottle keeps drinks cold for 24 hours",
248 | product_price_usd=Decimal("34.99"),
249 | product_category="sports",
250 | product_in_stock=False,
251 | ),
252 | # Books (3 products)
253 | Product(
254 | product_id=28,
255 | product_name="The Pragmatic Programmer",
256 | product_description="Classic software development book with timeless programming wisdom and best practices",
257 | product_price_usd=Decimal("44.99"),
258 | product_category="books",
259 | product_in_stock=True,
260 | ),
261 | Product(
262 | product_id=29,
263 | product_name="Atomic Habits",
264 | product_description="Science-backed strategies for building good habits and breaking bad ones",
265 | product_price_usd=Decimal("16.99"),
266 | product_category="books",
267 | product_in_stock=True,
268 | ),
269 | Product(
270 | product_id=30,
271 | product_name="The Design of Everyday Things",
272 | product_description="Foundational book on user-centered design and human-computer interaction",
273 | product_price_usd=Decimal("24.99"),
274 | product_category="books",
275 | product_in_stock=True,
276 | ),
277 | ]
278 |
--------------------------------------------------------------------------------
/app/frontend/bun.lock:
--------------------------------------------------------------------------------
1 | {
2 | "lockfileVersion": 1,
3 | "workspaces": {
4 | "": {
5 | "name": "bun-react-template",
6 | "dependencies": {
7 | "@hookform/resolvers": "^4.1.0",
8 | "@radix-ui/react-label": "^2.1.2",
9 | "@radix-ui/react-select": "^2.1.6",
10 | "@radix-ui/react-slot": "^1.1.2",
11 | "bun-plugin-tailwind": "^0.0.14",
12 | "class-variance-authority": "^0.7.1",
13 | "clsx": "^2.1.1",
14 | "lucide-react": "^0.475.0",
15 | "react": "^19",
16 | "react-dom": "^19",
17 | "react-hook-form": "^7.54.2",
18 | "tailwind-merge": "^3.0.1",
19 | "tailwindcss": "^4.0.6",
20 | "tailwindcss-animate": "^1.0.7",
21 | "zod": "^3.24.2",
22 | },
23 | "devDependencies": {
24 | "@biomejs/biome": "2.2.6",
25 | "@types/bun": "latest",
26 | "@types/react": "^19",
27 | "@types/react-dom": "^19",
28 | },
29 | },
30 | },
31 | "packages": {
32 | "@biomejs/biome": ["@biomejs/biome@2.2.6", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.2.6", "@biomejs/cli-darwin-x64": "2.2.6", "@biomejs/cli-linux-arm64": "2.2.6", "@biomejs/cli-linux-arm64-musl": "2.2.6", "@biomejs/cli-linux-x64": "2.2.6", "@biomejs/cli-linux-x64-musl": "2.2.6", "@biomejs/cli-win32-arm64": "2.2.6", "@biomejs/cli-win32-x64": "2.2.6" }, "bin": { "biome": "bin/biome" } }, "sha512-yKTCNGhek0rL5OEW1jbLeZX8LHaM8yk7+3JRGv08my+gkpmtb5dDE+54r2ZjZx0ediFEn1pYBOJSmOdDP9xtFw=="],
33 |
34 | "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.2.6", "", { "os": "darwin", "cpu": "arm64" }, "sha512-UZPmn3M45CjTYulgcrFJFZv7YmK3pTxTJDrFYlNElT2FNnkkX4fsxjExTSMeWKQYoZjvekpH5cvrYZZlWu3yfA=="],
35 |
36 | "@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.2.6", "", { "os": "darwin", "cpu": "x64" }, "sha512-HOUIquhHVgh/jvxyClpwlpl/oeMqntlteL89YqjuFDiZ091P0vhHccwz+8muu3nTyHWM5FQslt+4Jdcd67+xWQ=="],
37 |
38 | "@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.2.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-BpGtuMJGN+o8pQjvYsUKZ+4JEErxdSmcRD/JG3mXoWc6zrcA7OkuyGFN1mDggO0Q1n7qXxo/PcupHk8gzijt5g=="],
39 |
40 | "@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.2.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-TjCenQq3N6g1C+5UT3jE1bIiJb5MWQvulpUngTIpFsL4StVAUXucWD0SL9MCW89Tm6awWfeXBbZBAhJwjyFbRQ=="],
41 |
42 | "@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.2.6", "", { "os": "linux", "cpu": "x64" }, "sha512-1HaM/dpI/1Z68zp8ZdT6EiBq+/O/z97a2AiHMl+VAdv5/ELckFt9EvRb8hDHpk8hUMoz03gXkC7VPXOVtU7faA=="],
43 |
44 | "@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.2.6", "", { "os": "linux", "cpu": "x64" }, "sha512-1ZcBux8zVM3JhWN2ZCPaYf0+ogxXG316uaoXJdgoPZcdK/rmRcRY7PqHdAos2ExzvjIdvhQp72UcveI98hgOog=="],
45 |
46 | "@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.2.6", "", { "os": "win32", "cpu": "arm64" }, "sha512-h3A88G8PGM1ryTeZyLlSdfC/gz3e95EJw9BZmA6Po412DRqwqPBa2Y9U+4ZSGUAXCsnSQE00jLV8Pyrh0d+jQw=="],
47 |
48 | "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.2.6", "", { "os": "win32", "cpu": "x64" }, "sha512-yx0CqeOhPjYQ5ZXgPfu8QYkgBhVJyvWe36as7jRuPrKPO5ylVDfwVtPQ+K/mooNTADW0IhxOZm3aPu16dP8yNQ=="],
49 |
50 | "@floating-ui/core": ["@floating-ui/core@1.7.3", "", { "dependencies": { "@floating-ui/utils": "^0.2.10" } }, "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w=="],
51 |
52 | "@floating-ui/dom": ["@floating-ui/dom@1.7.4", "", { "dependencies": { "@floating-ui/core": "^1.7.3", "@floating-ui/utils": "^0.2.10" } }, "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA=="],
53 |
54 | "@floating-ui/react-dom": ["@floating-ui/react-dom@2.1.6", "", { "dependencies": { "@floating-ui/dom": "^1.7.4" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-4JX6rEatQEvlmgU80wZyq9RT96HZJa88q8hp0pBd+LrczeDI4o6uA2M+uvxngVHo4Ihr8uibXxH6+70zhAFrVw=="],
55 |
56 | "@floating-ui/utils": ["@floating-ui/utils@0.2.10", "", {}, "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ=="],
57 |
58 | "@hookform/resolvers": ["@hookform/resolvers@4.1.3", "", { "dependencies": { "@standard-schema/utils": "^0.3.0" }, "peerDependencies": { "react-hook-form": "^7.0.0" } }, "sha512-Jsv6UOWYTrEFJ/01ZrnwVXs7KDvP8XIo115i++5PWvNkNvkrsTfGiLS6w+eJ57CYtUtDQalUWovCZDHFJ8u1VQ=="],
59 |
60 | "@radix-ui/number": ["@radix-ui/number@1.1.1", "", {}, "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g=="],
61 |
62 | "@radix-ui/primitive": ["@radix-ui/primitive@1.1.3", "", {}, "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg=="],
63 |
64 | "@radix-ui/react-arrow": ["@radix-ui/react-arrow@1.1.7", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w=="],
65 |
66 | "@radix-ui/react-collection": ["@radix-ui/react-collection@1.1.7", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw=="],
67 |
68 | "@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg=="],
69 |
70 | "@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="],
71 |
72 | "@radix-ui/react-direction": ["@radix-ui/react-direction@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw=="],
73 |
74 | "@radix-ui/react-dismissable-layer": ["@radix-ui/react-dismissable-layer@1.1.11", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-escape-keydown": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg=="],
75 |
76 | "@radix-ui/react-focus-guards": ["@radix-ui/react-focus-guards@1.1.3", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw=="],
77 |
78 | "@radix-ui/react-focus-scope": ["@radix-ui/react-focus-scope@1.1.7", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw=="],
79 |
80 | "@radix-ui/react-id": ["@radix-ui/react-id@1.1.1", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg=="],
81 |
82 | "@radix-ui/react-label": ["@radix-ui/react-label@2.1.7", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-YT1GqPSL8kJn20djelMX7/cTRp/Y9w5IZHvfxQTVHrOqa2yMl7i/UfMqKRU5V7mEyKTrUVgJXhNQPVCG8PBLoQ=="],
83 |
84 | "@radix-ui/react-popper": ["@radix-ui/react-popper@1.2.8", "", { "dependencies": { "@floating-ui/react-dom": "^2.0.0", "@radix-ui/react-arrow": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-rect": "1.1.1", "@radix-ui/react-use-size": "1.1.1", "@radix-ui/rect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw=="],
85 |
86 | "@radix-ui/react-portal": ["@radix-ui/react-portal@1.1.9", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ=="],
87 |
88 | "@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
89 |
90 | "@radix-ui/react-select": ["@radix-ui/react-select@2.2.6", "", { "dependencies": { "@radix-ui/number": "1.1.1", "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-visually-hidden": "1.2.3", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-I30RydO+bnn2PQztvo25tswPH+wFBjehVGtmagkU78yMdwTwVf12wnAOF+AeP8S2N8xD+5UPbGhkUfPyvT+mwQ=="],
91 |
92 | "@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
93 |
94 | "@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg=="],
95 |
96 | "@radix-ui/react-use-controllable-state": ["@radix-ui/react-use-controllable-state@1.2.2", "", { "dependencies": { "@radix-ui/react-use-effect-event": "0.0.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg=="],
97 |
98 | "@radix-ui/react-use-effect-event": ["@radix-ui/react-use-effect-event@0.0.2", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA=="],
99 |
100 | "@radix-ui/react-use-escape-keydown": ["@radix-ui/react-use-escape-keydown@1.1.1", "", { "dependencies": { "@radix-ui/react-use-callback-ref": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g=="],
101 |
102 | "@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ=="],
103 |
104 | "@radix-ui/react-use-previous": ["@radix-ui/react-use-previous@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ=="],
105 |
106 | "@radix-ui/react-use-rect": ["@radix-ui/react-use-rect@1.1.1", "", { "dependencies": { "@radix-ui/rect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w=="],
107 |
108 | "@radix-ui/react-use-size": ["@radix-ui/react-use-size@1.1.1", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ=="],
109 |
110 | "@radix-ui/react-visually-hidden": ["@radix-ui/react-visually-hidden@1.2.3", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug=="],
111 |
112 | "@radix-ui/rect": ["@radix-ui/rect@1.1.1", "", {}, "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw=="],
113 |
114 | "@standard-schema/utils": ["@standard-schema/utils@0.3.0", "", {}, "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g=="],
115 |
116 | "@types/bun": ["@types/bun@1.3.0", "", { "dependencies": { "bun-types": "1.3.0" } }, "sha512-+lAGCYjXjip2qY375xX/scJeVRmZ5cY0wyHYyCYxNcdEXrQ4AOe3gACgd4iQ8ksOslJtW4VNxBJ8llUwc3a6AA=="],
117 |
118 | "@types/node": ["@types/node@24.7.2", "", { "dependencies": { "undici-types": "~7.14.0" } }, "sha512-/NbVmcGTP+lj5oa4yiYxxeBjRivKQ5Ns1eSZeB99ExsEQ6rX5XYU1Zy/gGxY/ilqtD4Etx9mKyrPxZRetiahhA=="],
119 |
120 | "@types/react": ["@types/react@19.2.2", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA=="],
121 |
122 | "@types/react-dom": ["@types/react-dom@19.2.1", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-/EEvYBdT3BflCWvTMO7YkYBHVE9Ci6XdqZciZANQgKpaiDRGOLIlRo91jbTNRQjgPFWVaRxcYc0luVNFitz57A=="],
123 |
124 | "aria-hidden": ["aria-hidden@1.2.6", "", { "dependencies": { "tslib": "^2.0.0" } }, "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA=="],
125 |
126 | "bun-plugin-tailwind": ["bun-plugin-tailwind@0.0.14", "", { "dependencies": { "tailwindcss": "4.0.0-beta.9" }, "peerDependencies": { "typescript": "^5.0.0" } }, "sha512-Ge8M8DQsRDErCzH/uI8pYjx5vZWXxQvnwM/xMQMElxQqHieGbAopfYo/q/kllkPkRbFHiwhnHwTpRMAMJZCjug=="],
127 |
128 | "bun-types": ["bun-types@1.3.0", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-u8X0thhx+yJ0KmkxuEo9HAtdfgCBaM/aI9K90VQcQioAmkVp3SG3FkwWGibUFz3WdXAdcsqOcbU40lK7tbHdkQ=="],
129 |
130 | "class-variance-authority": ["class-variance-authority@0.7.1", "", { "dependencies": { "clsx": "^2.1.1" } }, "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg=="],
131 |
132 | "clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="],
133 |
134 | "csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
135 |
136 | "detect-node-es": ["detect-node-es@1.1.0", "", {}, "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ=="],
137 |
138 | "get-nonce": ["get-nonce@1.0.1", "", {}, "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q=="],
139 |
140 | "lucide-react": ["lucide-react@0.475.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-NJzvVu1HwFVeZ+Gwq2q00KygM1aBhy/ZrhY9FsAgJtpB+E4R7uxRk9M2iKvHa6/vNxZydIB59htha4c2vvwvVg=="],
141 |
142 | "react": ["react@19.2.0", "", {}, "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ=="],
143 |
144 | "react-dom": ["react-dom@19.2.0", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.0" } }, "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ=="],
145 |
146 | "react-hook-form": ["react-hook-form@7.65.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17 || ^18 || ^19" } }, "sha512-xtOzDz063WcXvGWaHgLNrNzlsdFgtUWcb32E6WFaGTd7kPZG3EeDusjdZfUsPwKCKVXy1ZlntifaHZ4l8pAsmw=="],
147 |
148 | "react-remove-scroll": ["react-remove-scroll@2.7.1", "", { "dependencies": { "react-remove-scroll-bar": "^2.3.7", "react-style-singleton": "^2.2.3", "tslib": "^2.1.0", "use-callback-ref": "^1.3.3", "use-sidecar": "^1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-HpMh8+oahmIdOuS5aFKKY6Pyog+FNaZV/XyJOq7b4YFwsFHe5yYfdbIalI4k3vU2nSDql7YskmUseHsRrJqIPA=="],
149 |
150 | "react-remove-scroll-bar": ["react-remove-scroll-bar@2.3.8", "", { "dependencies": { "react-style-singleton": "^2.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react"] }, "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q=="],
151 |
152 | "react-style-singleton": ["react-style-singleton@2.2.3", "", { "dependencies": { "get-nonce": "^1.0.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ=="],
153 |
154 | "scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
155 |
156 | "tailwind-merge": ["tailwind-merge@3.3.1", "", {}, "sha512-gBXpgUm/3rp1lMZZrM/w7D8GKqshif0zAymAhbCyIt8KMe+0v9DQ7cdYLR4FHH/cKpdTXb+A/tKKU3eolfsI+g=="],
157 |
158 | "tailwindcss": ["tailwindcss@4.1.14", "", {}, "sha512-b7pCxjGO98LnxVkKjaZSDeNuljC4ueKUddjENJOADtubtdo8llTaJy7HwBMeLNSSo2N5QIAgklslK1+Ir8r6CA=="],
159 |
160 | "tailwindcss-animate": ["tailwindcss-animate@1.0.7", "", { "peerDependencies": { "tailwindcss": ">=3.0.0 || insiders" } }, "sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA=="],
161 |
162 | "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
163 |
164 | "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
165 |
166 | "undici-types": ["undici-types@7.14.0", "", {}, "sha512-QQiYxHuyZ9gQUIrmPo3IA+hUl4KYk8uSA7cHrcKd/l3p1OTpZcM0Tbp9x7FAtXdAYhlasd60ncPpgu6ihG6TOA=="],
167 |
168 | "use-callback-ref": ["use-callback-ref@1.3.3", "", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg=="],
169 |
170 | "use-sidecar": ["use-sidecar@1.1.3", "", { "dependencies": { "detect-node-es": "^1.1.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ=="],
171 |
172 | "zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
173 |
174 | "bun-plugin-tailwind/tailwindcss": ["tailwindcss@4.0.0-beta.9", "", {}, "sha512-96KpsfQi+/sFIOfyFnGzyy5pobuzf1iMBD9NVtelerPM/lPI2XUS4Kikw9yuKRniXXw77ov1sl7gCSKLsn6CJA=="],
175 | }
176 | }
177 |
--------------------------------------------------------------------------------