fix: add lesson 6

This commit is contained in:
2026-03-17 17:24:10 +01:00
parent 9ffdecf2c4
commit 8df4087cfd
91 changed files with 10392 additions and 0 deletions

View File

@@ -0,0 +1,6 @@
You are a Next.js 15 expert using App Router with TypeScript.
Use server components by default.
Use "use client" only when needed for interactivity.
Always define TypeScript interfaces for props, params, and API bodies.
Use Tailwind CSS for styling.
Use the @/ import alias for all local imports.

View File

@@ -0,0 +1,41 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/versions
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# env files (can opt-in for committing if needed)
.env*
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts

View File

@@ -0,0 +1,36 @@
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
## Getting Started
First, run the development server:
```bash
npm run dev
# or
yarn dev
# or
pnpm dev
# or
bun dev
```
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
## Learn More
To learn more about Next.js, take a look at the following resources:
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
## Deploy on Vercel
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.

View File

@@ -0,0 +1,7 @@
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
/* config options here */
};
export default nextConfig;

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,23 @@
{
"name": "quickpoll",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start"
},
"dependencies": {
"next": "16.1.6",
"react": "19.2.3",
"react-dom": "19.2.3"
},
"devDependencies": {
"@tailwindcss/postcss": "^4",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"tailwindcss": "^4",
"typescript": "^5"
}
}

View File

@@ -0,0 +1,7 @@
const config = {
plugins: {
"@tailwindcss/postcss": {},
},
};
export default config;

View File

@@ -0,0 +1 @@
<svg fill="none" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M14.5 13.5V5.41a1 1 0 0 0-.3-.7L9.8.29A1 1 0 0 0 9.08 0H1.5v13.5A2.5 2.5 0 0 0 4 16h8a2.5 2.5 0 0 0 2.5-2.5m-1.5 0v-7H8v-5H3v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1M9.5 5V2.12L12.38 5zM5.13 5h-.62v1.25h2.12V5zm-.62 3h7.12v1.25H4.5zm.62 3h-.62v1.25h7.12V11z" clip-rule="evenodd" fill="#666" fill-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 391 B

View File

@@ -0,0 +1 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g clip-path="url(#a)"><path fill-rule="evenodd" clip-rule="evenodd" d="M10.27 14.1a6.5 6.5 0 0 0 3.67-3.45q-1.24.21-2.7.34-.31 1.83-.97 3.1M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16m.48-1.52a7 7 0 0 1-.96 0H7.5a4 4 0 0 1-.84-1.32q-.38-.89-.63-2.08a40 40 0 0 0 3.92 0q-.25 1.2-.63 2.08a4 4 0 0 1-.84 1.31zm2.94-4.76q1.66-.15 2.95-.43a7 7 0 0 0 0-2.58q-1.3-.27-2.95-.43a18 18 0 0 1 0 3.44m-1.27-3.54a17 17 0 0 1 0 3.64 39 39 0 0 1-4.3 0 17 17 0 0 1 0-3.64 39 39 0 0 1 4.3 0m1.1-1.17q1.45.13 2.69.34a6.5 6.5 0 0 0-3.67-3.44q.65 1.26.98 3.1M8.48 1.5l.01.02q.41.37.84 1.31.38.89.63 2.08a40 40 0 0 0-3.92 0q.25-1.2.63-2.08a4 4 0 0 1 .85-1.32 7 7 0 0 1 .96 0m-2.75.4a6.5 6.5 0 0 0-3.67 3.44 29 29 0 0 1 2.7-.34q.31-1.83.97-3.1M4.58 6.28q-1.66.16-2.95.43a7 7 0 0 0 0 2.58q1.3.27 2.95.43a18 18 0 0 1 0-3.44m.17 4.71q-1.45-.12-2.69-.34a6.5 6.5 0 0 0 3.67 3.44q-.65-1.27-.98-3.1" fill="#666"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -0,0 +1 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1155 1000"><path d="m577.3 0 577.4 1000H0z" fill="#fff"/></svg>

After

Width:  |  Height:  |  Size: 128 B

View File

@@ -0,0 +1 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 2.5h13v10a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1zM0 1h16v11.5a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 0 12.5zm3.75 4.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5M7 4.75a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0m1.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5" fill="#666"/></svg>

After

Width:  |  Height:  |  Size: 385 B

View File

@@ -0,0 +1,27 @@
import { NextResponse } from "next/server";
import { getPollById } from "@/lib/data";
import type { Poll } from "@/types";
interface RouteParams {
params: Promise<{ id: string }>;
}
// STAP 3: GET /api/polls/[id] — enkele poll ophalen
// (Dit bestand is COMPLEET van Les 5. Dit is stap 3 van Les 5.)
export async function GET(
request: Request,
{ params }: RouteParams
): Promise<NextResponse> {
const { id } = await params;
const poll = getPollById(id);
if (!poll) {
return NextResponse.json(
{ error: "Poll niet gevonden" },
{ status: 404 }
);
}
return NextResponse.json<Poll>(poll);
}

View File

@@ -0,0 +1,28 @@
import { NextResponse } from "next/server";
import { votePoll } from "@/lib/data";
interface RouteParams {
params: Promise<{ id: string }>;
}
interface VoteBody {
optionIndex: number;
}
// STAP 4: POST /api/polls/[id]/vote — stem uitbrengen
//
// Wat moet je doen?
// 1. Haal het id op uit params (await!)
// 2. Lees de request body en cast naar VoteBody
// 3. Valideer: is optionIndex een number?
// 4. Roep votePoll(id, body.optionIndex) aan
// 5. Als het resultaat undefined is: return 404
// 6. Anders: return de geüpdatete poll als JSON
export async function POST(
request: Request,
{ params }: RouteParams
): Promise<NextResponse> {
// Implementeer je POST handler hier
return NextResponse.json({ error: "Nog niet geimplementeerd" }, { status: 501 });
}

View File

@@ -0,0 +1,24 @@
import { NextResponse } from "next/server";
import { getPolls, createPoll } from "@/lib/data";
import type { Poll, CreatePollBody } from "@/types";
// GET /api/polls — alle polls ophalen
export async function GET(): Promise<NextResponse<Poll[]>> {
const polls = getPolls();
return NextResponse.json(polls);
}
// POST /api/polls — nieuwe poll aanmaken
export async function POST(request: Request): Promise<NextResponse> {
const body: CreatePollBody = await request.json();
if (!body.question || !body.options || body.options.length < 2) {
return NextResponse.json(
{ error: "Vraag en minstens 2 opties zijn verplicht" },
{ status: 400 }
);
}
const newPoll = createPoll(body.question, body.options);
return NextResponse.json(newPoll, { status: 201 });
}

View File

@@ -0,0 +1,32 @@
"use client";
import { useState } from "react";
import { useRouter } from "next/navigation";
// BONUS: Maak een formulier om een nieuwe poll aan te maken
//
// Benodigde state:
// - question: string
// - options: string[] (start met ["", ""])
// - isSubmitting: boolean
// - error: string | null
//
// Wat moet je bouwen?
// 1. Een input voor de vraag
// 2. Inputs voor de opties (minimaal 2, maximaal 6)
// 3. Knoppen om opties toe te voegen/verwijderen
// 4. Een submit knop die POST naar /api/polls
// 5. Na success: redirect naar / met router.push("/")
export default function CreatePollPage() {
return (
<div className="max-w-2xl mx-auto">
<h1 className="text-2xl font-bold text-gray-900 mb-6">
Nieuwe Poll Aanmaken
</h1>
<p className="text-gray-400 italic">
Bonus: bouw hier het create formulier
</p>
</div>
);
}

View File

@@ -0,0 +1,30 @@
"use client";
// STAP 7: Error boundary
//
// Dit bestand vangt fouten op in de route.
// MOET een client component zijn ("use client" staat al bovenaan).
//
// Props die je krijgt:
// - error: Error — het error object met .message
// - reset: () => void — functie om de pagina opnieuw te proberen
//
// Bouw een nette error pagina met:
// - Een titel "Er ging iets mis!"
// - De error message
// - Een "Probeer opnieuw" knop die reset() aanroept
export default function Error({
error,
reset,
}: {
error: Error;
reset: () => void;
}) {
return (
<div className="text-center py-16">
<p>Er ging iets mis: {error.message}</p>
<button onClick={() => reset()}>Probeer opnieuw</button>
</div>
);
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

View File

@@ -0,0 +1 @@
@import "tailwindcss";

View File

@@ -0,0 +1,36 @@
import type { Metadata } from "next";
import Link from "next/link";
import "./globals.css";
export const metadata: Metadata = {
title: "QuickPoll — Stem op alles",
description: "Maak en deel polls met je vrienden",
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="nl">
<body className="min-h-screen bg-gray-50 text-gray-900">
{/*
STAP 1: Bouw hier een navigatiebalk met:
- Logo/titel "QuickPoll" (links) die linkt naar /
- Een link naar / ("Polls")
- Een link naar /create ("Nieuwe Poll")
Tip: gebruik <Link> van "next/link", niet <a>
Tip: gebruik Tailwind classes voor styling
*/}
<main className="max-w-4xl mx-auto px-4 py-8">{children}</main>
<footer className="text-center text-gray-400 text-sm py-8">
© 2025 QuickPoll NOVI Hogeschool Les 5
</footer>
</body>
</html>
);
}

View File

@@ -0,0 +1,17 @@
// STAP 7: Loading state
//
// Dit bestand wordt automatisch getoond terwijl een pagina laadt.
// Bouw een skeleton loader met Tailwind's animate-pulse class.
//
// Voorbeeld:
// <div className="animate-pulse">
// <div className="h-8 bg-gray-200 rounded w-1/3 mb-4" />
// </div>
export default function Loading() {
return (
<div>
<p>Laden...</p>
</div>
);
}

View File

@@ -0,0 +1,18 @@
import Link from "next/link";
// STAP 7: Not found pagina
//
// Wordt getoond als een pagina niet bestaat.
// Bouw een nette 404 pagina met een link terug naar home.
export default function NotFound() {
return (
<div className="text-center py-16">
<h2 className="text-4xl font-bold text-gray-900 mb-4">404</h2>
<p className="text-gray-600 mb-6">Deze pagina bestaat niet.</p>
<Link href="/" className="text-purple-600 hover:underline">
Terug naar home
</Link>
</div>
);
}

View File

@@ -0,0 +1,31 @@
import Link from "next/link";
import { getPolls } from "@/lib/data";
import type { Poll } from "@/types";
export const dynamic = "force-dynamic";
export default function HomePage() {
// STAP 2: Haal alle polls op met getPolls()
// Dit is een Server Component — je kunt gewoon functies aanroepen!
return (
<div>
<h1 className="text-3xl font-bold text-gray-900 mb-2">Actieve Polls</h1>
<p className="text-gray-500 mb-8">Klik op een poll om te stemmen</p>
<div className="grid gap-4">
{/*
STAP 2: Map over de polls en toon voor elke poll:
- De vraag (poll.question)
- Het aantal opties en stemmen
- De opties als tags/badges
- Wrap het in een <Link> naar /poll/{poll.id}
Tip: maak een helper functie voor het totaal aantal stemmen:
const totalVotes = (poll: Poll): number =>
poll.votes.reduce((sum, v) => sum + v, 0);
*/}
</div>
</div>
);
}

View File

@@ -0,0 +1,28 @@
"use client";
// STAP 7: Error boundary voor poll detail pagina
//
// Dit bestand vangt fouten op in deze route.
export default function ErrorPoll({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
return (
<div className="max-w-2xl mx-auto text-center py-12">
<h2 className="text-2xl font-bold text-red-600 mb-4">
Oeps! Iets ging fout
</h2>
<p className="text-gray-600 mb-6">{error.message}</p>
<button
onClick={reset}
className="bg-red-600 text-white px-4 py-2 rounded-lg hover:bg-red-700"
>
Probeer opnieuw
</button>
</div>
);
}

View File

@@ -0,0 +1,20 @@
// STAP 7: Loading state voor poll detail pagina
//
// Dit bestand wordt automatisch getoond terwijl de poll detail pagina laadt.
// Bouw een skeleton loader die lijkt op de echte content.
export default function LoadingPoll() {
return (
<div className="max-w-2xl mx-auto animate-pulse">
<div className="h-10 bg-gray-200 rounded w-3/4 mb-8" />
<div className="space-y-4">
{[1, 2, 3].map((i) => (
<div
key={i}
className="bg-white rounded-lg border p-4 h-16"
/>
))}
</div>
</div>
);
}

View File

@@ -0,0 +1,17 @@
import Link from "next/link";
export default function PollNotFound() {
return (
<div className="text-center py-16">
<h2 className="text-2xl font-bold text-gray-900 mb-4">
Poll niet gevonden
</h2>
<p className="text-gray-600 mb-6">
Deze poll bestaat niet of is verwijderd.
</p>
<Link href="/" className="text-purple-600 hover:underline">
Bekijk alle polls
</Link>
</div>
);
}

View File

@@ -0,0 +1,40 @@
import { notFound } from "next/navigation";
import { getPollById } from "@/lib/data";
import { VoteForm } from "@/components/VoteForm";
import type { Metadata } from "next";
interface PageProps {
params: Promise<{ id: string }>;
}
// STAP 5: generateMetadata — dynamische pagina titel
//
// Deze functie genereert de <title> tag voor SEO.
// Haal de poll op en return de vraag als titel.
export async function generateMetadata({ params }: PageProps): Promise<Metadata> {
const { id } = await params;
const poll = getPollById(id);
return {
title: poll ? `${poll.question} — QuickPoll` : "Poll niet gevonden",
};
}
// STAP 5: PollPage — de poll detail pagina
//
// Wat moet je doen?
// 1. Haal het id op uit params (await!)
// 2. Zoek de poll met getPollById(id)
// 3. Als de poll niet bestaat: roep notFound() aan
// 4. Render de poll vraag als <h1>
// 5. Render de <VoteForm poll={poll} /> component
export default async function PollPage({ params }: PageProps) {
// Implementeer je pagina hier
return (
<div className="max-w-2xl mx-auto">
<p>Implementeer deze pagina (zie stap 5 in de opdracht)</p>
</div>
);
}

View File

@@ -0,0 +1,129 @@
"use client";
import { useState } from "react";
import Link from "next/link";
import type { Poll } from "@/types";
interface VoteFormProps {
poll: Poll;
}
// STAP 6: VoteForm — de stem interface
//
// Dit is een CLIENT component ("use client" staat bovenaan).
// Hier mag je wel useState en onClick gebruiken!
//
// Benodigde state:
// - selectedOption: number | null (welke optie is geselecteerd)
// - hasVoted: boolean (heeft de gebruiker al gestemd)
// - isLoading: boolean (wordt het formulier verstuurd)
// - currentPoll: Poll (de huidige poll data, update na stemmen)
//
// Wat moet je bouwen?
// 1. Toon alle opties als klikbare knoppen (voor het stemmen)
// 2. Highlight de geselecteerde optie met purple border
// 3. Een "Stem!" knop die een POST doet naar /api/polls/{id}/vote
// 4. Na het stemmen: toon de resultaten met progress bars en percentages
// 5. Toon een "Terug" link naar de homepage
export function VoteForm({ poll }: VoteFormProps) {
const [selectedOption, setSelectedOption] = useState<number | null>(null);
const [hasVoted, setHasVoted] = useState<boolean>(false);
const [isLoading, setIsLoading] = useState<boolean>(false);
const [currentPoll, setCurrentPoll] = useState<Poll>(poll);
const totalVotes: number = currentPoll.votes.reduce((sum, v) => sum + v, 0);
function getPercentage(votes: number): number {
if (totalVotes === 0) return 0;
return Math.round((votes / totalVotes) * 100);
}
async function handleVote(): Promise<void> {
if (selectedOption === null || isLoading) return;
setIsLoading(true);
try {
const response = await fetch(`/api/polls/${currentPoll.id}/vote`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ optionIndex: selectedOption }),
});
if (response.ok) {
const updatedPoll: Poll = await response.json();
setCurrentPoll(updatedPoll);
setHasVoted(true);
}
} finally {
setIsLoading(false);
}
}
// Nog niet gestemd — toon opties
if (!hasVoted) {
return (
<div className="space-y-4">
{currentPoll.options.map((option, idx) => (
<button
key={idx}
onClick={() => setSelectedOption(idx)}
disabled={isLoading}
className={`w-full text-left p-4 rounded-lg border-2 transition ${
selectedOption === idx
? "border-purple-500 bg-purple-50"
: "border-gray-200 hover:border-purple-300"
}`}
>
<span className="font-medium">{option}</span>
</button>
))}
<button
onClick={handleVote}
disabled={selectedOption === null || isLoading}
className="w-full bg-purple-600 text-white py-3 rounded-lg hover:bg-purple-700 disabled:opacity-50 disabled:cursor-not-allowed transition font-medium"
>
{isLoading ? "Aan het stemmen..." : "Stem!"}
</button>
</div>
);
}
// Wel gestemd — toon resultaten
return (
<div className="space-y-6">
<p className="text-green-600 font-medium text-lg">
Bedankt voor je stem!
</p>
<div className="space-y-4">
{currentPoll.options.map((option, idx) => (
<div key={idx}>
<div className="flex justify-between mb-2">
<span className="font-medium">{option}</span>
<span className="text-sm text-gray-600">
{currentPoll.votes[idx]} stemmen ({getPercentage(currentPoll.votes[idx])}%)
</span>
</div>
<div className="w-full bg-gray-200 rounded-full h-3">
<div
className="bg-purple-600 h-3 rounded-full transition-all"
style={{
width: `${getPercentage(currentPoll.votes[idx])}%`,
}}
/>
</div>
</div>
))}
</div>
<Link
href="/"
className="block text-center text-purple-600 hover:underline mt-6"
>
Terug naar polls
</Link>
</div>
);
}

View File

@@ -0,0 +1,55 @@
import { Poll } from "@/types";
export const polls: Poll[] = [
{
id: "1",
question: "Wat is de beste code editor?",
options: ["VS Code", "Cursor", "Vim", "WebStorm"],
votes: [12, 25, 5, 3],
},
{
id: "2",
question: "Wat is de beste programmeertaal?",
options: ["TypeScript", "Python", "Rust", "Go"],
votes: [18, 15, 8, 4],
},
{
id: "3",
question: "Welk framework heeft de toekomst?",
options: ["Next.js", "Remix", "Astro", "SvelteKit"],
votes: [22, 6, 10, 7],
},
];
let nextId = 4;
export function getPolls(): Poll[] {
return polls;
}
export function getPollById(id: string): Poll | undefined {
return polls.find((poll) => poll.id === id);
}
export function createPoll(question: string, options: string[]): Poll {
const newPoll: Poll = {
id: String(nextId++),
question,
options,
votes: new Array(options.length).fill(0),
};
polls.push(newPoll);
return newPoll;
}
export function votePoll(
pollId: string,
optionIndex: number
): Poll | undefined {
const poll = polls.find((p) => p.id === pollId);
if (!poll || optionIndex < 0 || optionIndex >= poll.options.length) {
return undefined;
}
poll.votes[optionIndex]++;
return poll;
}

View File

@@ -0,0 +1,17 @@
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
export function middleware(request: NextRequest): NextResponse {
const start = Date.now();
console.log(`[${request.method}] ${request.nextUrl.pathname}`);
const response = NextResponse.next();
response.headers.set("x-request-time", String(Date.now() - start));
return response;
}
export const config = {
matcher: ["/api/:path*", "/poll/:path*"],
};

View File

@@ -0,0 +1,11 @@
export interface Poll {
id: string;
question: string;
options: string[];
votes: number[];
}
export interface CreatePollBody {
question: string;
options: string[];
}

View File

@@ -0,0 +1,34 @@
{
"compilerOptions": {
"target": "ES2017",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "react-jsx",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": ["./src/*"]
}
},
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts",
".next/dev/types/**/*.ts",
"**/*.mts"
],
"exclude": ["node_modules"]
}