Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0b3d00f219 | ||
|
|
c70cc79ff4 |
61
.github/copilot-instructions.md
vendored
Normal file
61
.github/copilot-instructions.md
vendored
Normal file
@@ -0,0 +1,61 @@
|
||||
## Copilot / AI Agent Instructions for this repository
|
||||
|
||||
Purpose: quick, practical guidance so an AI coding agent can be productive immediately.
|
||||
|
||||
- **Big picture**: This is a single-page Next.js (App Router) app that tracks bicycle wear parts. The UI lives under `app/` (React Server/Client components) and server logic is implemented as Next API Route handlers under `app/api/*` (files named `route.ts`). Database access is via Prisma + SQLite; the Prisma client is in `lib/prisma.ts` and schema in `prisma/schema.prisma`.
|
||||
|
||||
- **Key directories / files**:
|
||||
- `app/` — Next App Router. Routes and React components live here.
|
||||
- `app/api/` — Route handlers (e.g. `app/api/bikes/route.ts`, `app/api/bikes/[id]/route.ts`). These are the server-side endpoints used by the frontend.
|
||||
- `app/components/` — small client components. Note many components include `'use client'` at the top (e.g. `BikeDetail.tsx`).
|
||||
- `lib/prisma.ts` — exports Prisma client; import this in route handlers.
|
||||
- `lib/validations.ts` — Zod schemas used both server-side and in tests.
|
||||
- `prisma/schema.prisma` — database schema (SQLite). Use Prisma commands after edits.
|
||||
- `types/` — TypeScript types used across UI and APIs (e.g. `BikeWithParts`).
|
||||
- `__tests__/` — Vitest tests; split into `api/` and `lib/` tests.
|
||||
|
||||
- **Runtime & stack**:
|
||||
- Uses Bun as the development runtime (`bun run dev` in README) but apps run as Next.js (v14). Keep using `bun` commands as the project expects.
|
||||
- Tailwind CSS for styling, Zod for validation, Prisma for DB.
|
||||
|
||||
- **Project-specific conventions and patterns**:
|
||||
- Next App Router pattern: server code under `app/api/*` uses `route.ts` exports. Edit these when changing backend behavior.
|
||||
- Client components explicitly use `'use client'`. If a component needs browser APIs or state/hooks, it must be a client component.
|
||||
- The repo uses an import alias `@` mapped to the project root (see `vitest.config.ts`). Tests and code reference `@/` paths — preserve that style.
|
||||
- Database changes: update `prisma/schema.prisma`, then run Prisma generate/push. The README suggests `bunx prisma generate` and `bunx prisma db push`.
|
||||
|
||||
- **Developer workflows (commands)** — copyable examples:
|
||||
- Install deps: `bun install`
|
||||
- Dev server: `bun run dev` (runs `next dev`)
|
||||
- Build: `bun run build`
|
||||
- Prisma: `bunx prisma generate` then `bunx prisma db push` (or `bun run db:push` from package.json)
|
||||
- Open DB UI: `bun run db:studio`
|
||||
|
||||
- **Testing**:
|
||||
- Tests run with Vitest. Root scripts in `package.json`:
|
||||
- `bun run test` — runs `vitest run --exclude '**/validations.test.ts' && bun test __tests__/lib/validations.test.ts` (note: validations tests are run separately in this project).
|
||||
- `bun run test:all` — runs `vitest run`.
|
||||
- `bun run test:ui` — runs `vitest --ui`.
|
||||
- `vitest.config.ts` sets `environment: 'node'`, `globals: true`, and the alias `@` to the project root — tests assume these settings.
|
||||
- To run a single test file use: `vitest run __tests__/api/bikes.test.ts` or run the npm script with `bun run`.
|
||||
|
||||
- **Editing APIs & DB changes**:
|
||||
- When changing a route under `app/api/*`, update or add corresponding tests in `__tests__/api/*`.
|
||||
- When changing `prisma/schema.prisma`, run `bunx prisma generate` and `bunx prisma db push`. Update any affected `lib/prisma.ts` usages if the client API changes.
|
||||
|
||||
- **What to avoid / gotchas**:
|
||||
- Do not assume a separate backend service — API routes are in-repo Next route handlers.
|
||||
- Pay attention to client vs server components (`'use client'`). Moving code between them requires changing data fetching approaches.
|
||||
- Tests rely on the `@` alias — preserve import paths like `@/lib/validations` rather than relative deep paths when modifying code.
|
||||
|
||||
- **Where to look for examples**:
|
||||
- API patterns: `app/api/bikes/route.ts` and `app/api/bikes/[id]/route.ts`.
|
||||
- Prisma usage: `lib/prisma.ts` and `prisma/schema.prisma`.
|
||||
- Validations: `lib/validations.ts` and `__tests__/lib/validations.test.ts`.
|
||||
- Client component example: `app/components/BikeDetail.tsx` uses local state, child forms (`WearPartForm`) and lists (`WearPartList`).
|
||||
|
||||
- **If something is unclear**: ask the maintainer these quick questions before making changes:
|
||||
1. Do you want to keep using `bun` for tests and dev, or prefer `npm`/`pnpm` wrappers? (README uses `bun`.)
|
||||
2. Are there deployment targets or environment variables not in the repo? (DB URL / secrets)
|
||||
|
||||
Be concise in edits: when changing API behavior, update `app/api/*` and the corresponding tests in `__tests__/api/` in the same PR.
|
||||
@@ -162,6 +162,42 @@ describe('Bikes API', () => {
|
||||
|
||||
expect(bikes).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('should include wearParts with maintenanceHistory when fetching a bike', async () => {
|
||||
// Create bike, part and maintenance
|
||||
const bike = await prisma.bike.create({ data: { name: `Include Test ${Date.now()}` } })
|
||||
const part = await prisma.wearPart.create({
|
||||
data: {
|
||||
bikeId: bike.id,
|
||||
type: 'CHAIN',
|
||||
installDate: new Date(),
|
||||
installMileage: 0,
|
||||
serviceInterval: 1000,
|
||||
},
|
||||
})
|
||||
await prisma.maintenanceHistory.create({
|
||||
data: {
|
||||
wearPartId: part.id,
|
||||
date: new Date(),
|
||||
mileage: 0,
|
||||
action: 'INSTALL',
|
||||
},
|
||||
})
|
||||
|
||||
const found = await prisma.bike.findUnique({
|
||||
where: { id: bike.id },
|
||||
include: {
|
||||
wearParts: {
|
||||
include: { maintenanceHistory: true },
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(found).toBeDefined()
|
||||
expect(found?.wearParts).toBeDefined()
|
||||
expect(found?.wearParts[0].maintenanceHistory).toBeDefined()
|
||||
expect(found?.wearParts[0].maintenanceHistory.length).toBeGreaterThanOrEqual(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe('GET /api/bikes/[id]', () => {
|
||||
|
||||
@@ -103,6 +103,30 @@ describe('Utility Functions', () => {
|
||||
expect(status.status).toBe('CRITICAL')
|
||||
expect(status.percentageUsed).toBeGreaterThanOrEqual(90)
|
||||
})
|
||||
|
||||
it('should mark as OVERDUE when current mileage exceeds interval', () => {
|
||||
const part = {
|
||||
id: '1',
|
||||
bikeId: 'bike1',
|
||||
type: 'CHAIN',
|
||||
installDate: new Date(),
|
||||
installMileage: 0,
|
||||
serviceInterval: 1000,
|
||||
status: 'ACTIVE',
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
brand: null,
|
||||
model: null,
|
||||
cost: null,
|
||||
notes: null,
|
||||
maintenanceHistory: [],
|
||||
}
|
||||
|
||||
const status = calculateServiceStatus(part, 1500)
|
||||
expect(status.status).toBe('OVERDUE')
|
||||
expect(status.isOverdue).toBe(true)
|
||||
expect(status.remainingKm).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe('calculateTotalCosts', () => {
|
||||
@@ -295,6 +319,45 @@ describe('Utility Functions', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('should ignore replaced parts without full install/replace history when calculating average lifespan', () => {
|
||||
const parts = [
|
||||
{
|
||||
id: '1',
|
||||
bikeId: 'bike1',
|
||||
type: 'CHAIN',
|
||||
installDate: new Date(),
|
||||
installMileage: 0,
|
||||
serviceInterval: 1000,
|
||||
status: 'REPLACED',
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
brand: null,
|
||||
model: null,
|
||||
cost: null,
|
||||
notes: null,
|
||||
maintenanceHistory: [
|
||||
{
|
||||
id: '1',
|
||||
wearPartId: '1',
|
||||
date: new Date('2024-01-01'),
|
||||
mileage: 0,
|
||||
action: 'INSTALL',
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
notes: null,
|
||||
cost: null,
|
||||
},
|
||||
// Missing REPLACE entry, should be ignored
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
const avg = calculateAverageLifespan(parts)
|
||||
// Implementation counts replaced parts with maintenanceHistory present
|
||||
// even if REPLACE entry is missing; totalKm becomes 0 and average is 0
|
||||
expect(avg).toBe(0)
|
||||
})
|
||||
|
||||
describe('formatDate', () => {
|
||||
it('should format date correctly', () => {
|
||||
const date = new Date('2024-01-15')
|
||||
|
||||
@@ -235,7 +235,8 @@ describe('Validation Schemas', () => {
|
||||
expect(result.data.purchaseDate).toBeInstanceOf(Date)
|
||||
} else {
|
||||
// If it's still a string, it should be parseable
|
||||
expect(new Date(result.data.purchaseDate as string)).toBeInstanceOf(Date)
|
||||
const parsed = new Date(String(result.data.purchaseDate))
|
||||
expect(parsed).toBeInstanceOf(Date)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -45,6 +45,6 @@ export default function BikeDetailPage() {
|
||||
return null
|
||||
}
|
||||
|
||||
return <BikeDetail bike={bike} onUpdate={fetchBike} />
|
||||
return <BikeDetail bike={bike} onUpdate={() => fetchBike(params.id as string)} />
|
||||
}
|
||||
|
||||
|
||||
@@ -91,7 +91,12 @@ export default function BikeDetail({ bike, onUpdate }: BikeDetailProps) {
|
||||
|
||||
<WearPartList
|
||||
bikeId={bike.id}
|
||||
parts={bike.wearParts}
|
||||
parts={bike.wearParts.map((p) => ({
|
||||
// ensure each part has a `maintenanceHistory` array to satisfy WearPartList prop types
|
||||
...p,
|
||||
// some server queries may not include maintenanceHistory; default to empty array
|
||||
maintenanceHistory: (p as any).maintenanceHistory ?? [],
|
||||
}))}
|
||||
bikeCurrentMileage={bike.currentMileage}
|
||||
onUpdate={onUpdate}
|
||||
/>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"dev": "next dev --port 3001",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint",
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import { Bike, WearPart, MaintenanceHistory } from '@prisma/client'
|
||||
|
||||
export type BikeWithParts = Bike & {
|
||||
wearParts: WearPart[]
|
||||
}
|
||||
|
||||
export type WearPartWithHistory = WearPart & {
|
||||
maintenanceHistory: MaintenanceHistory[]
|
||||
bike: Bike
|
||||
}
|
||||
|
||||
export type BikeWithParts = Bike & {
|
||||
wearParts: WearPartWithHistory[]
|
||||
}
|
||||
|
||||
export type MaintenanceHistoryWithPart = MaintenanceHistory & {
|
||||
wearPart: WearPart
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user