From 7f94bb6359e30f63558fdd2f43fc4dfe077ea3e7 Mon Sep 17 00:00:00 2001 From: janpeter visser Date: Wed, 22 Apr 2026 21:04:48 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20ST-001=E2=80=93ST-005=20foundation=20?= =?UTF-8?q?=E2=80=94=20scaffolding,=20Prisma,=20schema,=20seed,=20env?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ST-001: Next.js 16 + React 19 + TypeScript strict + Tailwind + shadcn/ui + all deps - ST-002: Prisma v7 setup with better-sqlite3 adapter (local) and pg adapter (cloud) - ST-003: Full schema migration (users, pbis, stories, sprints, tasks, todos, api_tokens) - ST-004: Seed with 9 PBIs, ~40 stories, demo user (demo/demo1234), lars user - ST-005: Zod-validated env vars, .env.example, lib/session, lib/auth, lib/api-auth Co-Authored-By: Claude Sonnet 4.6 --- .gitignore | 8 + CLAUDE.md | 589 +- actions/auth.ts | 68 + app/globals.css | 136 +- app/page.tsx | 68 +- components.json | 25 + components/ui/alert-dialog.tsx | 187 + components/ui/badge.tsx | 52 + components/ui/button.tsx | 58 + components/ui/dialog.tsx | 160 + components/ui/dropdown-menu.tsx | 268 + components/ui/input.tsx | 20 + components/ui/select.tsx | 201 + components/ui/separator.tsx | 25 + components/ui/sheet.tsx | 138 + components/ui/skeleton.tsx | 13 + components/ui/sonner.tsx | 49 + components/ui/textarea.tsx | 18 + components/ui/tooltip.tsx | 66 + lib/api-auth.ts | 23 + lib/auth.ts | 34 + lib/env.ts | 18 + lib/prisma.ts | 32 + lib/session.ts | 16 + lib/utils.ts | 6 + package-lock.json | 5892 ++++++++++++++++- package.json | 34 +- prisma.config.ts | 17 + .../20260422184304_init/migration.sql | 171 + prisma/migrations/migration_lock.toml | 3 + prisma/schema.prisma | 203 + prisma/seed.ts | 238 + 32 files changed, 8653 insertions(+), 183 deletions(-) create mode 100644 actions/auth.ts create mode 100644 components.json create mode 100644 components/ui/alert-dialog.tsx create mode 100644 components/ui/badge.tsx create mode 100644 components/ui/button.tsx create mode 100644 components/ui/dialog.tsx create mode 100644 components/ui/dropdown-menu.tsx create mode 100644 components/ui/input.tsx create mode 100644 components/ui/select.tsx create mode 100644 components/ui/separator.tsx create mode 100644 components/ui/sheet.tsx create mode 100644 components/ui/skeleton.tsx create mode 100644 components/ui/sonner.tsx create mode 100644 components/ui/textarea.tsx create mode 100644 components/ui/tooltip.tsx create mode 100644 lib/api-auth.ts create mode 100644 lib/auth.ts create mode 100644 lib/env.ts create mode 100644 lib/prisma.ts create mode 100644 lib/session.ts create mode 100644 lib/utils.ts create mode 100644 prisma.config.ts create mode 100644 prisma/migrations/20260422184304_init/migration.sql create mode 100644 prisma/migrations/migration_lock.toml create mode 100644 prisma/schema.prisma create mode 100644 prisma/seed.ts diff --git a/.gitignore b/.gitignore index 5ef6a52..d03db3f 100644 --- a/.gitignore +++ b/.gitignore @@ -39,3 +39,11 @@ yarn-error.log* # typescript *.tsbuildinfo next-env.d.ts + +# Documentation +scrum4me*.md + +# SQLite local database +*.db +*.db-shm +*.db-wal \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md index 43c994c..899c39a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1 +1,588 @@ -@AGENTS.md +# CLAUDE.md — Scrum4Me + +Dit is het centrale instructiedocument voor Claude Code. Lees dit volledig voordat je iets bouwt. + +--- + +## Wat is Scrum4Me? + +Een desktop-first fullstack webapplicatie voor solo developers en kleine Scrum Teams die meerdere softwareprojecten parallel beheren. De app organiseert werk hiërarchisch (product → PBI → story → taak), biedt gesplitste planningsschermen met drag-and-drop, en integreert met Claude Code via een REST API zodat implementatieplannen, testresultaten en commits automatisch vastgelegd worden in stories. + +--- + +## Specificatiedocumenten + +Lees het relevante document voordat je aan een feature begint. Nooit gokken over requirements. + +| Document | Gebruik voor | +|---|---| +| `scrum4me-functional-spec.md` | Acceptatiecriteria, randgevallen, user flows per feature | +| `scrum4me-architecture.md` | Stack, datamodel, Prisma schema, Zustand stores, projectstructuur | +| `scrum4me-backlog.md` | Welke task bouwen, in welke volgorde, "done when"-criteria | +| `scrum4me-personas.md` | Lars (primaire gebruiker), Dina, Remi — gebruik bij UI-beslissingen | +| `scrum4me-product-backlog.md` | Testdata voor de seed — PBI's en stories van Scrum4Me zelf | + +--- + +## Waar te beginnen + +Volg de backlog strikt op volgorde. Start bij **ST-001**. Sla geen milestone over. + +``` +M0 (ST-001–008) → M1 (ST-101–110) → M2 (ST-201–210) +→ M3 (ST-301–312) → M4 (ST-401–410) → M5 (ST-501–506) +→ M6 (ST-601–612) +``` + +Per task: +1. Lees de task in `scrum4me-backlog.md` +2. Zoek de bijbehorende feature-spec op in `scrum4me-functional-spec.md` +3. Bouw — test — verifieer de "Done when"-criteria +4. Commit met de task-ID in het commit-bericht: `feat: ST-001 project scaffolding` + +--- + +## Tech stack (samenvatting) + +``` +Next.js 15 (App Router) + React 19 +TypeScript strict +Tailwind CSS + shadcn/ui +Zustand (client state) +dnd-kit (drag-and-drop) +Prisma v7 (ORM) +PostgreSQL via Neon (cloud) | SQLite (lokaal) +iron-session (auth cookies) +bcrypt (wachtwoord hashing) +Zod (validatie) +Sonner (toasts) +``` + +--- + +## Exacte dependencies (package.json) + +```json +{ + "dependencies": { + "next": "^15.0.0", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "typescript": "^5.0.0", + "tailwindcss": "^3.4.0", + "zustand": "^5.0.0", + "@dnd-kit/core": "^6.3.0", + "@dnd-kit/sortable": "^8.0.0", + "@dnd-kit/utilities": "^3.2.0", + "prisma": "^7.0.0", + "@prisma/client": "^7.0.0", + "@prisma/adapter-pg": "^7.0.0", + "iron-session": "^8.0.0", + "bcryptjs": "^2.4.3", + "zod": "^3.22.0", + "sonner": "^1.5.0", + "pg": "^8.11.0" + }, + "devDependencies": { + "@types/bcryptjs": "^2.4.6", + "@types/pg": "^8.11.0", + "@types/node": "^20.0.0", + "@types/react": "^19.0.0", + "@types/react-dom": "^19.0.0", + "eslint": "^8.0.0", + "eslint-config-next": "^15.0.0" + } +} +``` + +--- + +## shadcn/ui componenten om te installeren + +Voer deze uit na `npx shadcn@latest init`: + +```bash +npx shadcn@latest add button +npx shadcn@latest add input +npx shadcn@latest add textarea +npx shadcn@latest add dialog +npx shadcn@latest add dropdown-menu +npx shadcn@latest add badge +npx shadcn@latest add tooltip +npx shadcn@latest add separator +npx shadcn@latest add sheet # voor story slide-over +npx shadcn@latest add select +npx shadcn@latest add alert-dialog # voor bevestigingsdialogen +npx shadcn@latest add skeleton +npx shadcn@latest add toast +``` + +--- + +## Projectstructuur + +``` +scrum4me/ +├── app/ +│ ├── (auth)/ +│ │ ├── login/page.tsx +│ │ └── register/page.tsx +│ ├── (app)/ +│ │ ├── layout.tsx # Auth-check + navigatie +│ │ ├── dashboard/page.tsx +│ │ ├── products/ +│ │ │ ├── new/page.tsx +│ │ │ └── [id]/ +│ │ │ ├── page.tsx # Product Backlog +│ │ │ └── sprint/ +│ │ │ ├── page.tsx # Sprint Backlog +│ │ │ └── planning/page.tsx +│ │ ├── todos/page.tsx +│ │ └── settings/ +│ │ ├── page.tsx +│ │ └── tokens/page.tsx +│ └── api/ +│ ├── products/[id]/next-story/route.ts +│ ├── sprints/[id]/tasks/route.ts +│ ├── stories/[id]/ +│ │ ├── log/route.ts +│ │ └── tasks/reorder/route.ts +│ ├── tasks/[id]/route.ts +│ └── todos/route.ts +├── components/ +│ ├── ui/ # shadcn/ui (auto-gegenereerd) +│ ├── split-pane/ +│ │ └── split-pane.tsx +│ ├── backlog/ +│ │ ├── pbi-list.tsx +│ │ ├── pbi-item.tsx +│ │ ├── story-grid.tsx +│ │ └── story-block.tsx +│ ├── sprint/ +│ │ ├── sprint-backlog.tsx +│ │ └── sprint-story-item.tsx +│ ├── planning/ +│ │ ├── task-list.tsx +│ │ └── task-item.tsx +│ └── shared/ +│ ├── panel-nav-bar.tsx +│ ├── confirm-dialog.tsx +│ └── story-log.tsx +├── stores/ +│ ├── planner-store.ts +│ ├── selection-store.ts +│ └── sprint-store.ts +├── lib/ +│ ├── prisma.ts +│ ├── session.ts +│ ├── auth.ts +│ ├── api-auth.ts +│ └── env.ts +├── actions/ +│ ├── products.ts +│ ├── pbis.ts +│ ├── stories.ts +│ ├── sprints.ts +│ ├── tasks.ts +│ └── todos.ts +├── prisma/ +│ ├── schema.prisma +│ ├── migrations/ +│ └── seed.ts +├── middleware.ts +├── prisma.config.ts +└── .env.example +``` + +--- + +## Kritieke implementatiepatronen + +### 1. iron-session configuratie + +```ts +// lib/session.ts +import { SessionOptions } from 'iron-session' + +export interface SessionData { + userId: string + isDemo: boolean +} + +export const sessionOptions: SessionOptions = { + password: process.env.SESSION_SECRET!, + cookieName: 'scrum4me-session', + cookieOptions: { + secure: process.env.NODE_ENV === 'production', + httpOnly: true, + sameSite: 'lax', + }, +} +``` + +```ts +// Gebruik in Server Action of Route Handler: +import { getIronSession } from 'iron-session' +import { cookies } from 'next/headers' +import { SessionData, sessionOptions } from '@/lib/session' + +const session = await getIronSession(await cookies(), sessionOptions) +if (!session.userId) redirect('/login') +``` + +### 2. Prisma Client singleton + +```ts +// lib/prisma.ts +import { PrismaClient } from '@prisma/client' + +const globalForPrisma = globalThis as unknown as { prisma: PrismaClient | undefined } + +export const prisma = globalForPrisma.prisma ?? new PrismaClient({ + log: process.env.NODE_ENV === 'development' ? ['query', 'error', 'warn'] : ['error'], +}) + +if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma +``` + +### 3. prisma.config.ts (Prisma v7 vereiste) + +```ts +// prisma.config.ts +import 'dotenv/config' +import { defineConfig } from 'prisma/config' + +export default defineConfig({ + schema: 'prisma/schema.prisma', + migrations: { path: 'prisma/migrations' }, +}) +``` + +### 4. API Bearer token authenticatie + +```ts +// lib/api-auth.ts +import { createHash } from 'crypto' +import { prisma } from '@/lib/prisma' + +export async function authenticateApiRequest(request: Request) { + const authHeader = request.headers.get('Authorization') + if (!authHeader?.startsWith('Bearer ')) { + return { error: 'Unauthorized', status: 401 } + } + + const token = authHeader.slice(7) + const tokenHash = createHash('sha256').update(token).digest('hex') + + const apiToken = await prisma.apiToken.findUnique({ + where: { token_hash: tokenHash }, + include: { user: true }, + }) + + if (!apiToken || apiToken.revoked_at) { + return { error: 'Unauthorized', status: 401 } + } + + return { userId: apiToken.user_id, isDemo: apiToken.user.is_demo } +} + +// Gebruik in Route Handler: +export async function GET(request: Request) { + const auth = await authenticateApiRequest(request) + if ('error' in auth) { + return Response.json({ error: auth.error }, { status: auth.status }) + } + // auth.userId beschikbaar +} +``` + +### 5. Float sort_order — drag-and-drop volgorde + +```ts +// Bereken nieuwe sort_order bij tussenvoeging: +function getSortOrder(before: number | null, after: number | null): number { + if (before === null && after === null) return 1.0 + if (before === null) return after! / 2 + if (after === null) return before + 1.0 + return (before + after) / 2 +} + +// Herindexeer als precisie opraakt (< 0.001 verschil): +async function reindexIfNeeded(items: { id: string; sort_order: number }[]) { + const minGap = Math.min(...items.slice(1).map((item, i) => + item.sort_order - items[i].sort_order + )) + if (minGap < 0.001) { + // Herindexeer: 1.0, 2.0, 3.0, ... + await Promise.all(items.map((item, i) => + prisma.pbi.update({ where: { id: item.id }, data: { sort_order: i + 1.0 } }) + )) + } +} +``` + +### 6. Zustand store patroon (optimistische update + rollback) + +```ts +// Gebruik in dnd-kit onDragEnd: +const { pbiOrder, reorderPbis, rollbackPbis } = usePlannerStore() + +async function handleDragEnd(event: DragEndEvent) { + const { active, over } = event + if (!over || active.id === over.id) return + + const prevOrder = [...pbiOrder[productId]] + const newOrder = arrayMove(prevOrder, oldIndex, newIndex) + + // 1. Optimistisch updaten + reorderPbis(productId, newOrder) + + // 2. Server Action aanroepen + const result = await reorderPbisAction(productId, newOrder) + + // 3. Rollback bij fout + if (!result.success) { + rollbackPbis(productId, prevOrder) + toast.error('Volgorde opslaan mislukt') + } +} +``` + +### 7. Server Action patroon + +```ts +// actions/pbis.ts +'use server' + +import { revalidatePath } from 'next/cache' +import { getIronSession } from 'iron-session' +import { cookies } from 'next/headers' +import { z } from 'zod' +import { prisma } from '@/lib/prisma' +import { SessionData, sessionOptions } from '@/lib/session' + +const createPbiSchema = z.object({ + productId: z.string().cuid(), + title: z.string().min(1).max(200), + priority: z.number().int().min(1).max(4), +}) + +export async function createPbi(formData: FormData) { + const session = await getIronSession(await cookies(), sessionOptions) + if (!session.userId) return { error: 'Niet ingelogd' } + if (session.isDemo) return { error: 'Niet beschikbaar in demo-modus' } + + const parsed = createPbiSchema.safeParse({ + productId: formData.get('productId'), + title: formData.get('title'), + priority: Number(formData.get('priority')), + }) + if (!parsed.success) return { error: parsed.error.flatten().fieldErrors } + + // Valideer eigenaarschap + const product = await prisma.product.findFirst({ + where: { id: parsed.data.productId, user_id: session.userId } + }) + if (!product) return { error: 'Product niet gevonden' } + + // Bepaal sort_order (onderaan de prioriteitsgroep) + const last = await prisma.pbi.findFirst({ + where: { product_id: parsed.data.productId, priority: parsed.data.priority }, + orderBy: { sort_order: 'desc' }, + }) + const sort_order = (last?.sort_order ?? 0) + 1.0 + + const pbi = await prisma.pbi.create({ + data: { ...parsed.data, product_id: parsed.data.productId, sort_order }, + }) + + revalidatePath(`/products/${parsed.data.productId}`) + return { success: true, pbi } +} +``` + +### 8. Route Handler patroon (REST API) + +```ts +// app/api/products/[id]/next-story/route.ts +import { authenticateApiRequest } from '@/lib/api-auth' +import { prisma } from '@/lib/prisma' + +export async function GET( + request: Request, + { params }: { params: Promise<{ id: string }> } +) { + const auth = await authenticateApiRequest(request) + if ('error' in auth) { + return Response.json({ error: auth.error }, { status: auth.status }) + } + + const { id } = await params + + // Valideer eigenaarschap + const sprint = await prisma.sprint.findFirst({ + where: { product_id: id, status: 'ACTIVE', product: { user_id: auth.userId } }, + }) + if (!sprint) { + return Response.json({ error: 'Geen actieve Sprint gevonden' }, { status: 404 }) + } + + const story = await prisma.story.findFirst({ + where: { sprint_id: sprint.id, status: 'IN_SPRINT' }, + orderBy: [{ priority: 'asc' }, { sort_order: 'asc' }], + include: { tasks: { orderBy: [{ priority: 'asc' }, { sort_order: 'asc' }] } }, + }) + + if (!story) { + return Response.json({ error: 'Geen open stories in de Sprint' }, { status: 404 }) + } + + return Response.json(story) +} +``` + +--- + +## Middleware patroon + +```ts +// middleware.ts +import { NextResponse } from 'next/server' +import type { NextRequest } from 'next/server' +import { getIronSession } from 'iron-session' +import { SessionData, sessionOptions } from '@/lib/session' + +const protectedRoutes = ['/dashboard', '/products', '/todos', '/settings'] +const authRoutes = ['/login', '/register'] + +export async function middleware(request: NextRequest) { + const response = NextResponse.next() + const session = await getIronSession(request.cookies, sessionOptions) + + const isProtected = protectedRoutes.some(r => request.nextUrl.pathname.startsWith(r)) + const isAuthRoute = authRoutes.some(r => request.nextUrl.pathname.startsWith(r)) + + if (isProtected && !session.userId) { + return NextResponse.redirect(new URL('/login', request.url)) + } + if (isAuthRoute && session.userId) { + return NextResponse.redirect(new URL('/dashboard', request.url)) + } + + return response +} + +export const config = { + matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'], +} +``` + +--- + +## Scrum-terminologie (gebruik consistent) + +| Correct | Niet gebruiken | +|---|---| +| Product Backlog Item (PBI) | Feature, Epic, Issue | +| Story | User Story, Ticket | +| Sprint Goal | Sprint Objective | +| Sprint Planning | Sprint Meeting | +| Scrum Team | Team | +| Definition of Done | DoD criteria | + +--- + +## Env vars + +```bash +# .env.local +DATABASE_URL="postgresql://user:password@host/dbname?sslmode=require" +DIRECT_URL="postgresql://user:password@host/dbname?sslmode=require" +SESSION_SECRET="genereer-met-openssl-rand-base64-32" + +# Lokaal (SQLite): +# DATABASE_URL="file:./dev.db" +# DIRECT_URL niet nodig bij SQLite +``` + +--- + +## Lokale setup (quickstart) + +```bash +git clone +cd scrum4me +npm install +cp .env.example .env.local +# Vul SESSION_SECRET in .env.local + +# SQLite lokaal: +npx prisma db push +npx prisma db seed +npm run dev +``` + +--- + +## Demo-gebruiker credentials + +Na seeding: +- **Gebruikersnaam:** `demo` +- **Wachtwoord:** `demo1234` + +De demo-gebruiker heeft read-only rechten. Alle schrijfacties geven een 403 of zijn uitgeschakeld in de UI. + +--- + +## REST API — alle endpoints + +| Methode | Endpoint | Doel | +|---|---|---| +| GET | `/api/products` | Actieve producten ophalen | +| GET | `/api/products/:id/next-story` | Hoogst geprioriteerde open story | +| GET | `/api/sprints/:id/tasks?limit=10` | Eerste N taken van de Sprint | +| PATCH | `/api/stories/:id/tasks/reorder` | Taakvolgorde aanpassen | +| POST | `/api/stories/:id/log` | Plan / testresultaat / commit vastleggen | +| PATCH | `/api/tasks/:id` | Taakstatus bijwerken | +| POST | `/api/todos` | Todo aanmaken | + +Alle endpoints vereisen: `Authorization: Bearer ` + +### POST /api/stories/:id/log — body schema + +```json +// Implementatieplan: +{ "type": "IMPLEMENTATION_PLAN", "content": "string" } + +// Testresultaat: +{ "type": "TEST_RESULT", "content": "string", "status": "PASSED" | "FAILED" } + +// Commit: +{ "type": "COMMIT", "content": "string", "commit_hash": "string", "commit_message": "string" } +``` + +--- + +## Conventies + +- **Commit-berichten:** `feat: ST-XXX beschrijving` / `fix: ST-XXX beschrijving` +- **Branch-namen:** `feat/ST-001-scaffolding` +- **Server Actions:** altijd in `actions/[domein].ts`, nooit inline in page.tsx +- **Validatie:** altijd Zod, nooit handmatige checks +- **Eigenaarschap:** elke Server Action en Route Handler controleert dat de resource bij de geverifieerde gebruiker hoort +- **Demo-check:** elke Server Action controleert `session.isDemo` vóór schrijven +- **Foutberichten:** altijd in het Nederlands voor eindgebruikers +- **Comments in code:** Engels + +--- + +## Definitie of Done (project) + +De MVP is klaar wanneer: +- [ ] Alle 62 tasks (ST-001 t/m ST-612) zijn afgerond +- [ ] Volledige Lars-flow doorlopen zonder fouten (ST-612) +- [ ] Alle 7 API-endpoints werken via curl +- [ ] Demo-gebruiker heeft geen schrijfrechten +- [ ] App lokaal opzetbaar via README zonder extra hulp +- [ ] CI/CD actief — falende build blokkeert merge +- [ ] Beveiligingsreview API geslaagd (cross-user toegang onmogelijk) diff --git a/actions/auth.ts b/actions/auth.ts new file mode 100644 index 0000000..5b88c99 --- /dev/null +++ b/actions/auth.ts @@ -0,0 +1,68 @@ +'use server' + +import { redirect } from 'next/navigation' +import { cookies } from 'next/headers' +import { getIronSession } from 'iron-session' +import { z } from 'zod' +import { registerUser, verifyUser } from '@/lib/auth' +import { SessionData, sessionOptions } from '@/lib/session' + +const registerSchema = z.object({ + username: z.string().min(3, 'Gebruikersnaam moet minimaal 3 tekens bevatten').max(50), + password: z.string().min(8, 'Wachtwoord moet minimaal 8 tekens bevatten'), +}) + +const loginSchema = z.object({ + username: z.string().min(1), + password: z.string().min(1), +}) + +export async function registerAction(formData: FormData) { + const parsed = registerSchema.safeParse({ + username: formData.get('username'), + password: formData.get('password'), + }) + + if (!parsed.success) { + return { error: parsed.error.flatten().fieldErrors } + } + + const result = await registerUser(parsed.data.username, parsed.data.password) + if (result.error) return { error: result.error } + + const session = await getIronSession(await cookies(), sessionOptions) + session.userId = result.user!.id + session.isDemo = false + await session.save() + + redirect('/dashboard') +} + +export async function loginAction(formData: FormData) { + const parsed = loginSchema.safeParse({ + username: formData.get('username'), + password: formData.get('password'), + }) + + if (!parsed.success) { + return { error: 'Ongeldige inloggegevens' } + } + + const user = await verifyUser(parsed.data.username, parsed.data.password) + if (!user) { + return { error: 'Onjuiste gebruikersnaam of wachtwoord' } + } + + const session = await getIronSession(await cookies(), sessionOptions) + session.userId = user.id + session.isDemo = user.is_demo + await session.save() + + redirect('/dashboard') +} + +export async function logoutAction() { + const session = await getIronSession(await cookies(), sessionOptions) + session.destroy() + redirect('/login') +} diff --git a/app/globals.css b/app/globals.css index a2dc41e..c56032b 100644 --- a/app/globals.css +++ b/app/globals.css @@ -1,26 +1,130 @@ @import "tailwindcss"; +@import "tw-animate-css"; +@import "shadcn/tailwind.css"; -:root { - --background: #ffffff; - --foreground: #171717; -} +@custom-variant dark (&:is(.dark *)); @theme inline { --color-background: var(--background); --color-foreground: var(--foreground); - --font-sans: var(--font-geist-sans); + --font-sans: var(--font-sans); --font-mono: var(--font-geist-mono); + --font-heading: var(--font-sans); + --color-sidebar-ring: var(--sidebar-ring); + --color-sidebar-border: var(--sidebar-border); + --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); + --color-sidebar-accent: var(--sidebar-accent); + --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); + --color-sidebar-primary: var(--sidebar-primary); + --color-sidebar-foreground: var(--sidebar-foreground); + --color-sidebar: var(--sidebar); + --color-chart-5: var(--chart-5); + --color-chart-4: var(--chart-4); + --color-chart-3: var(--chart-3); + --color-chart-2: var(--chart-2); + --color-chart-1: var(--chart-1); + --color-ring: var(--ring); + --color-input: var(--input); + --color-border: var(--border); + --color-destructive: var(--destructive); + --color-accent-foreground: var(--accent-foreground); + --color-accent: var(--accent); + --color-muted-foreground: var(--muted-foreground); + --color-muted: var(--muted); + --color-secondary-foreground: var(--secondary-foreground); + --color-secondary: var(--secondary); + --color-primary-foreground: var(--primary-foreground); + --color-primary: var(--primary); + --color-popover-foreground: var(--popover-foreground); + --color-popover: var(--popover); + --color-card-foreground: var(--card-foreground); + --color-card: var(--card); + --radius-sm: calc(var(--radius) * 0.6); + --radius-md: calc(var(--radius) * 0.8); + --radius-lg: var(--radius); + --radius-xl: calc(var(--radius) * 1.4); + --radius-2xl: calc(var(--radius) * 1.8); + --radius-3xl: calc(var(--radius) * 2.2); + --radius-4xl: calc(var(--radius) * 2.6); } -@media (prefers-color-scheme: dark) { - :root { - --background: #0a0a0a; - --foreground: #ededed; +:root { + --background: oklch(1 0 0); + --foreground: oklch(0.145 0 0); + --card: oklch(1 0 0); + --card-foreground: oklch(0.145 0 0); + --popover: oklch(1 0 0); + --popover-foreground: oklch(0.145 0 0); + --primary: oklch(0.205 0 0); + --primary-foreground: oklch(0.985 0 0); + --secondary: oklch(0.97 0 0); + --secondary-foreground: oklch(0.205 0 0); + --muted: oklch(0.97 0 0); + --muted-foreground: oklch(0.556 0 0); + --accent: oklch(0.97 0 0); + --accent-foreground: oklch(0.205 0 0); + --destructive: oklch(0.577 0.245 27.325); + --border: oklch(0.922 0 0); + --input: oklch(0.922 0 0); + --ring: oklch(0.708 0 0); + --chart-1: oklch(0.87 0 0); + --chart-2: oklch(0.556 0 0); + --chart-3: oklch(0.439 0 0); + --chart-4: oklch(0.371 0 0); + --chart-5: oklch(0.269 0 0); + --radius: 0.625rem; + --sidebar: oklch(0.985 0 0); + --sidebar-foreground: oklch(0.145 0 0); + --sidebar-primary: oklch(0.205 0 0); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.97 0 0); + --sidebar-accent-foreground: oklch(0.205 0 0); + --sidebar-border: oklch(0.922 0 0); + --sidebar-ring: oklch(0.708 0 0); +} + +.dark { + --background: oklch(0.145 0 0); + --foreground: oklch(0.985 0 0); + --card: oklch(0.205 0 0); + --card-foreground: oklch(0.985 0 0); + --popover: oklch(0.205 0 0); + --popover-foreground: oklch(0.985 0 0); + --primary: oklch(0.922 0 0); + --primary-foreground: oklch(0.205 0 0); + --secondary: oklch(0.269 0 0); + --secondary-foreground: oklch(0.985 0 0); + --muted: oklch(0.269 0 0); + --muted-foreground: oklch(0.708 0 0); + --accent: oklch(0.269 0 0); + --accent-foreground: oklch(0.985 0 0); + --destructive: oklch(0.704 0.191 22.216); + --border: oklch(1 0 0 / 10%); + --input: oklch(1 0 0 / 15%); + --ring: oklch(0.556 0 0); + --chart-1: oklch(0.87 0 0); + --chart-2: oklch(0.556 0 0); + --chart-3: oklch(0.439 0 0); + --chart-4: oklch(0.371 0 0); + --chart-5: oklch(0.269 0 0); + --sidebar: oklch(0.205 0 0); + --sidebar-foreground: oklch(0.985 0 0); + --sidebar-primary: oklch(0.488 0.243 264.376); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.269 0 0); + --sidebar-accent-foreground: oklch(0.985 0 0); + --sidebar-border: oklch(1 0 0 / 10%); + --sidebar-ring: oklch(0.556 0 0); +} + +@layer base { + * { + @apply border-border outline-ring/50; } -} - -body { - background: var(--background); - color: var(--foreground); - font-family: Arial, Helvetica, sans-serif; -} + body { + @apply bg-background text-foreground; + } + html { + @apply font-sans; + } +} \ No newline at end of file diff --git a/app/page.tsx b/app/page.tsx index 3f36f7c..4c3e167 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,65 +1,13 @@ -import Image from "next/image"; +import { Button } from "@/components/ui/button" export default function Home() { return ( -
-
- Next.js logo -
-

- To get started, edit the page.tsx file. -

-

- Looking for a starting point or more instructions? Head over to{" "} - - Templates - {" "} - or the{" "} - - Learning - {" "} - center. -

-
- -
+
+
+

Scrum4Me

+

Scaffolding complete — shadcn/ui Button works.

+ +
- ); + ) } diff --git a/components.json b/components.json new file mode 100644 index 0000000..f382eb7 --- /dev/null +++ b/components.json @@ -0,0 +1,25 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "base-nova", + "rsc": true, + "tsx": true, + "tailwind": { + "config": "", + "css": "app/globals.css", + "baseColor": "neutral", + "cssVariables": true, + "prefix": "" + }, + "iconLibrary": "lucide", + "rtl": false, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + }, + "menuColor": "default", + "menuAccent": "subtle", + "registries": {} +} diff --git a/components/ui/alert-dialog.tsx b/components/ui/alert-dialog.tsx new file mode 100644 index 0000000..0ee2c5f --- /dev/null +++ b/components/ui/alert-dialog.tsx @@ -0,0 +1,187 @@ +"use client" + +import * as React from "react" +import { AlertDialog as AlertDialogPrimitive } from "@base-ui/react/alert-dialog" + +import { cn } from "@/lib/utils" +import { Button } from "@/components/ui/button" + +function AlertDialog({ ...props }: AlertDialogPrimitive.Root.Props) { + return +} + +function AlertDialogTrigger({ ...props }: AlertDialogPrimitive.Trigger.Props) { + return ( + + ) +} + +function AlertDialogPortal({ ...props }: AlertDialogPrimitive.Portal.Props) { + return ( + + ) +} + +function AlertDialogOverlay({ + className, + ...props +}: AlertDialogPrimitive.Backdrop.Props) { + return ( + + ) +} + +function AlertDialogContent({ + className, + size = "default", + ...props +}: AlertDialogPrimitive.Popup.Props & { + size?: "default" | "sm" +}) { + return ( + + + + + ) +} + +function AlertDialogHeader({ + className, + ...props +}: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function AlertDialogFooter({ + className, + ...props +}: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function AlertDialogMedia({ + className, + ...props +}: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function AlertDialogTitle({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AlertDialogDescription({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AlertDialogAction({ + className, + ...props +}: React.ComponentProps) { + return ( +