feat: add release notes page with MDX content system

Implemented comprehensive release notes feature with:

- MDX-based content system for easy release note authoring
- Thematic sidebar navigation (Foundation, Features, Infrastructure)
- Auto-generated pages from MDX frontmatter
- Overview page with status filtering (completed/in_progress/planned)
- Detail pages with custom MDX components
- Mobile-responsive navigation (horizontal scroll tabs)
- Integration with timeline (link to detailed release notes)
- Added "Releases" link to header navigation

Technical implementation:
- next-mdx-remote for MDX parsing and rendering
- gray-matter for frontmatter extraction
- Custom MDX components for images, code blocks, typography
- Static site generation (SSG) for performance
- Template system for consistent release note structure

Files added:
- app/(marketing)/releases/ - Route structure and components
- content/nl/releases/ - MDX content files and index
- lib/mdx/releases.ts - MDX utility functions
- docs/specs/releasepage/ - Build plan documentation
- docs/templates/release-note-template.mdx - Content template

First release note: authentication.mdx (login, signup, password reset)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
colinislit
2025-11-19 20:59:54 +01:00
parent 501d8de22b
commit 2ae7b37e5f
14 changed files with 2759 additions and 8 deletions

View File

@@ -226,7 +226,7 @@ export const BuildTimeline = ({ data }: BuildTimelineProps) => {
{/* Achievements */} {/* Achievements */}
{week.achievements.length > 0 && ( {week.achievements.length > 0 && (
<div className="bg-teal-50 border border-teal-100 rounded-lg p-4"> <div className="bg-teal-50 border border-teal-100 rounded-lg p-4 mb-4">
<h4 className="text-sm font-semibold text-teal-700 uppercase tracking-wide mb-2"> <h4 className="text-sm font-semibold text-teal-700 uppercase tracking-wide mb-2">
Achievements Achievements
</h4> </h4>
@@ -239,6 +239,17 @@ export const BuildTimeline = ({ data }: BuildTimelineProps) => {
</ul> </ul>
</div> </div>
)} )}
{/* Link to detailed release notes */}
<a
href="/releases"
className="inline-flex items-center gap-2 text-teal-600 hover:text-teal-700 font-medium text-sm transition-colors"
>
<span>Bekijk gedetailleerde release notes</span>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
</svg>
</a>
</div> </div>
</div> </div>
))} ))}

View File

@@ -0,0 +1,130 @@
/**
* Release Detail Page
*
* Renders individual release note from MDX file
*/
import { notFound } from 'next/navigation'
import { MDXRemote } from 'next-mdx-remote/rsc'
import { getRelease, getAllReleases } from '@/lib/mdx/releases'
import { mdxComponents } from '../components/mdx-components'
interface ReleasePageProps {
params: Promise<{
category: string
}>
}
// Generate static params for all releases
export async function generateStaticParams() {
const releases = await getAllReleases()
return releases.map((release) => ({
category: release.slug,
}))
}
// Generate metadata for SEO
export async function generateMetadata({ params }: ReleasePageProps) {
const { category } = await params
const release = await getRelease(category)
if (!release) {
return {
title: 'Release Not Found',
}
}
return {
title: `${release.frontmatter.title} - Release Notes`,
description: release.frontmatter.description,
}
}
export default async function ReleasePage({ params }: ReleasePageProps) {
const { category } = await params
const release = await getRelease(category)
if (!release) {
notFound()
}
const { frontmatter, content } = release
return (
<div className="min-h-screen bg-white pt-24 pb-16">
<article className="max-w-4xl mx-auto px-4 md:px-8">
{/* Header */}
<header className="mb-8 pb-8 border-b border-slate-200">
<div className="flex items-center gap-3 mb-4">
<StatusBadge status={frontmatter.status} />
<span className="text-sm text-slate-500">v{frontmatter.version}</span>
<span className="text-sm text-slate-500"></span>
<time className="text-sm text-slate-500">
{new Date(frontmatter.releaseDate).toLocaleDateString('nl-NL', {
year: 'numeric',
month: 'long',
day: 'numeric'
})}
</time>
</div>
<h1 className="text-4xl md:text-5xl font-bold text-slate-900 mb-4">
{frontmatter.title}
</h1>
<p className="text-xl text-slate-600">
{frontmatter.description}
</p>
</header>
{/* MDX Content */}
<div className="prose prose-slate max-w-none">
<MDXRemote source={content} components={mdxComponents} />
</div>
{/* Footer Navigation */}
<footer className="mt-12 pt-8 border-t border-slate-200">
<div className="flex justify-between items-center">
<a
href="/releases"
className="text-teal-600 hover:text-teal-700 font-medium flex items-center gap-2"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
</svg>
Terug naar overzicht
</a>
<a
href="/#timeline"
className="text-slate-600 hover:text-slate-700 font-medium"
>
Bekijk timeline
</a>
</div>
</footer>
</article>
</div>
)
}
function StatusBadge({ status }: { status: 'completed' | 'in_progress' | 'planned' }) {
const styles = {
completed: 'bg-teal-100 text-teal-700 border-teal-200',
in_progress: 'bg-amber-100 text-amber-700 border-amber-200',
planned: 'bg-slate-100 text-slate-600 border-slate-200',
}
const labels = {
completed: 'Voltooid',
in_progress: 'Bezig',
planned: 'Gepland',
}
return (
<span className={`inline-flex items-center px-3 py-1 rounded-full text-xs font-medium border ${styles[status]}`}>
{labels[status]}
</span>
)
}

View File

@@ -0,0 +1,153 @@
/**
* Custom MDX Components
*
* Styled components for rendering MDX content
*/
import Image from 'next/image'
import Link from 'next/link'
type MDXComponents = {
[key: string]: React.ComponentType<any>
}
export const mdxComponents: MDXComponents = {
// Headings with anchor links
h1: ({ children, ...props }) => (
<h1 className="text-4xl font-bold text-slate-900 mt-8 mb-4" {...props}>
{children}
</h1>
),
h2: ({ children, ...props }) => (
<h2 className="text-3xl font-bold text-slate-900 mt-8 mb-4 border-b border-slate-200 pb-2" {...props}>
{children}
</h2>
),
h3: ({ children, ...props }) => (
<h3 className="text-2xl font-semibold text-slate-900 mt-6 mb-3" {...props}>
{children}
</h3>
),
h4: ({ children, ...props }) => (
<h4 className="text-xl font-semibold text-slate-900 mt-4 mb-2" {...props}>
{children}
</h4>
),
// Paragraphs
p: ({ children, ...props }) => (
<p className="text-slate-700 leading-relaxed mb-4" {...props}>
{children}
</p>
),
// Lists
ul: ({ children, ...props }) => (
<ul className="list-disc list-inside space-y-2 mb-4 text-slate-700" {...props}>
{children}
</ul>
),
ol: ({ children, ...props }) => (
<ol className="list-decimal list-inside space-y-2 mb-4 text-slate-700" {...props}>
{children}
</ol>
),
li: ({ children, ...props }) => (
<li className="ml-4" {...props}>
{children}
</li>
),
// Links
a: ({ href, children, ...props }) => (
<Link
href={href || '#'}
className="text-teal-600 hover:text-teal-700 underline underline-offset-2"
{...props}
>
{children}
</Link>
),
// Images
img: ({ src, alt, ...props }) => {
if (!src) return null
return (
<figure className="my-8">
<div className="relative rounded-lg overflow-hidden border border-slate-200 bg-slate-50">
<Image
src={src}
alt={alt || ''}
width={1200}
height={675}
className="w-full h-auto"
{...props}
/>
</div>
{alt && (
<figcaption className="text-sm text-slate-500 italic mt-2 text-center">
{alt}
</figcaption>
)}
</figure>
)
},
// Code blocks
pre: ({ children, ...props }) => (
<pre className="bg-slate-900 text-slate-50 rounded-lg p-4 overflow-x-auto mb-4 text-sm" {...props}>
{children}
</pre>
),
code: ({ children, ...props }) => (
<code className="bg-slate-100 text-slate-800 px-1.5 py-0.5 rounded text-sm font-mono" {...props}>
{children}
</code>
),
// Blockquote
blockquote: ({ children, ...props }) => (
<blockquote className="border-l-4 border-teal-500 pl-4 py-2 my-4 bg-teal-50 text-slate-700 italic" {...props}>
{children}
</blockquote>
),
// Horizontal rule
hr: (props) => (
<hr className="my-8 border-slate-200" {...props} />
),
// Table
table: ({ children, ...props }) => (
<div className="overflow-x-auto my-6">
<table className="min-w-full divide-y divide-slate-200" {...props}>
{children}
</table>
</div>
),
th: ({ children, ...props }) => (
<th className="px-4 py-3 bg-slate-50 text-left text-xs font-semibold text-slate-600 uppercase tracking-wider" {...props}>
{children}
</th>
),
td: ({ children, ...props }) => (
<td className="px-4 py-3 text-sm text-slate-700 border-t border-slate-200" {...props}>
{children}
</td>
),
// Strong/Bold
strong: ({ children, ...props }) => (
<strong className="font-semibold text-slate-900" {...props}>
{children}
</strong>
),
// Emphasis/Italic
em: ({ children, ...props }) => (
<em className="italic" {...props}>
{children}
</em>
),
}

View File

@@ -0,0 +1,171 @@
'use client'
/**
* Release Sidebar Navigation
*
* Fixed sidebar with grouped list of all releases
* Auto-generated from MDX files
*/
import Link from 'next/link'
import { usePathname } from 'next/navigation'
import { ChevronRight, ChevronDown } from 'lucide-react'
import { useState } from 'react'
import type { ReleaseNote, GroupMetadata, CategoryMetadata } from '@/lib/mdx/releases'
interface ReleaseSidebarProps {
releases: ReleaseNote[]
metadata: {
groups: GroupMetadata[]
categories: CategoryMetadata[]
}
}
export function ReleaseSidebar({ releases, metadata }: ReleaseSidebarProps) {
const pathname = usePathname()
const [expandedGroups, setExpandedGroups] = useState<Record<string, boolean>>({
foundation: true,
features: true,
infrastructure: true,
})
const toggleGroup = (groupId: string) => {
setExpandedGroups(prev => ({
...prev,
[groupId]: !prev[groupId],
}))
}
// Group releases by their group
const releasesByGroup = releases.reduce((acc, release) => {
const group = release.frontmatter.group
if (!acc[group]) acc[group] = []
acc[group].push(release)
return acc
}, {} as Record<string, ReleaseNote[]>)
// Check if we're on a specific release page
const isReleaseDetail = pathname.startsWith('/releases/') && pathname !== '/releases'
return (
<>
{/* Mobile: Horizontal scroll menu */}
<div className="lg:hidden bg-white border-b border-slate-200 sticky top-16 z-40">
<div className="flex gap-2 p-4 overflow-x-auto">
<Link
href="/releases"
className={`flex-shrink-0 px-4 py-2 rounded-lg text-sm font-medium transition-colors ${
pathname === '/releases'
? 'bg-teal-100 text-teal-700'
: 'bg-slate-100 text-slate-600 hover:bg-slate-200'
}`}
>
Overzicht
</Link>
{releases.map((release) => (
<Link
key={release.slug}
href={`/releases/${release.slug}`}
className={`flex-shrink-0 px-4 py-2 rounded-lg text-sm font-medium transition-colors ${
pathname === `/releases/${release.slug}`
? 'bg-teal-100 text-teal-700'
: 'bg-slate-100 text-slate-600 hover:bg-slate-200'
}`}
>
{release.frontmatter.title}
</Link>
))}
</div>
</div>
{/* Desktop: Fixed sidebar */}
<aside className="hidden lg:block w-64 fixed top-16 bottom-0 bg-white border-r border-slate-200 overflow-y-auto">
<div className="p-6">
<h2 className="text-xs font-semibold text-slate-500 uppercase tracking-wide mb-4">
Release Notes
</h2>
<nav className="space-y-1">
{/* Overview link */}
<Link
href="/releases"
className={`flex items-center justify-between px-3 py-2 rounded-lg text-sm font-medium transition-colors ${
pathname === '/releases' && !isReleaseDetail
? 'bg-teal-50 text-teal-700'
: 'text-slate-700 hover:bg-slate-50'
}`}
>
<span>Overzicht</span>
</Link>
{/* Grouped releases */}
{metadata.groups
.sort((a, b) => a.order - b.order)
.map((group) => {
const groupReleases = releasesByGroup[group.id] || []
const isExpanded = expandedGroups[group.id]
return (
<div key={group.id} className="space-y-1">
{/* Group header */}
<button
onClick={() => toggleGroup(group.id)}
className="w-full flex items-center justify-between px-3 py-2 text-xs font-semibold text-slate-500 uppercase tracking-wide hover:text-slate-700 transition-colors"
>
<span>{group.title}</span>
{isExpanded ? (
<ChevronDown className="w-4 h-4" />
) : (
<ChevronRight className="w-4 h-4" />
)}
</button>
{/* Group items */}
{isExpanded && (
<div className="space-y-1 ml-2">
{groupReleases.map((release) => {
const isActive = pathname === `/releases/${release.slug}`
return (
<Link
key={release.slug}
href={`/releases/${release.slug}`}
className={`flex items-center justify-between px-3 py-2 rounded-lg text-sm font-medium transition-colors group ${
isActive
? 'bg-teal-50 text-teal-700'
: 'text-slate-700 hover:bg-slate-50'
}`}
>
<div className="flex items-center gap-2">
<StatusDot status={release.frontmatter.status} />
<span className="truncate">{release.frontmatter.title}</span>
</div>
<ChevronRight className={`w-4 h-4 flex-shrink-0 transition-opacity ${
isActive ? 'opacity-100' : 'opacity-0 group-hover:opacity-50'
}`} />
</Link>
)
})}
</div>
)}
</div>
)
})}
</nav>
</div>
</aside>
</>
)
}
function StatusDot({ status }: { status: 'completed' | 'in_progress' | 'planned' }) {
const colors = {
completed: 'bg-teal-500',
in_progress: 'bg-amber-500',
planned: 'bg-slate-300',
}
return (
<div className={`w-2 h-2 rounded-full flex-shrink-0 ${colors[status]}`} />
)
}

View File

@@ -0,0 +1,34 @@
/**
* Releases Layout
*
* Layout for release notes pages with sidebar navigation
*/
import type { ReactNode } from 'react'
import { getAllReleases, getCategoryMetadata } from '@/lib/mdx/releases'
import { ReleaseSidebar } from './components/release-sidebar'
interface ReleasesLayoutProps {
children: ReactNode
}
export default async function ReleasesLayout({ children }: ReleasesLayoutProps) {
const releases = await getAllReleases()
const metadata = await getCategoryMetadata()
return (
<div className="min-h-screen bg-slate-50">
<div className="max-w-[1400px] mx-auto">
<div className="flex">
{/* Sidebar - hidden on mobile, fixed on desktop */}
<ReleaseSidebar releases={releases} metadata={metadata} />
{/* Main Content */}
<div className="flex-1 lg:ml-64">
{children}
</div>
</div>
</div>
</div>
)
}

View File

@@ -0,0 +1,155 @@
/**
* Releases Overview Page
*
* Landing page for all release notes grouped by category
*/
import Link from 'next/link'
import { getAllReleases } from '@/lib/mdx/releases'
export const metadata = {
title: 'Release Notes - AI Speedrun',
description: 'Volledige changelog van het Mini-ECD prototype. Transparant build-in-public overzicht van features, fixes en improvements.',
}
export default async function ReleasesPage() {
const releases = await getAllReleases()
// Group by status for better overview
const completed = releases.filter(r => r.frontmatter.status === 'completed')
const inProgress = releases.filter(r => r.frontmatter.status === 'in_progress')
const planned = releases.filter(r => r.frontmatter.status === 'planned')
return (
<div className="min-h-screen bg-slate-50 pt-24 pb-16">
<div className="max-w-4xl mx-auto px-4 md:px-8">
{/* Header */}
<div className="mb-12">
<h1 className="text-4xl md:text-5xl font-bold text-slate-900 mb-4">
Release Notes
</h1>
<p className="text-xl text-slate-600 max-w-3xl">
Volledige changelog van het Mini-ECD prototype. Build in public met transparantie over features, tijd en kosten.
</p>
</div>
{/* Stats */}
<div className="grid grid-cols-3 gap-4 mb-12">
<div className="bg-white rounded-lg border border-slate-200 p-4 text-center">
<div className="text-3xl font-bold text-teal-600">{completed.length}</div>
<div className="text-sm text-slate-500 mt-1">Voltooid</div>
</div>
<div className="bg-white rounded-lg border border-slate-200 p-4 text-center">
<div className="text-3xl font-bold text-amber-600">{inProgress.length}</div>
<div className="text-sm text-slate-500 mt-1">In Progress</div>
</div>
<div className="bg-white rounded-lg border border-slate-200 p-4 text-center">
<div className="text-3xl font-bold text-slate-400">{planned.length}</div>
<div className="text-sm text-slate-500 mt-1">Gepland</div>
</div>
</div>
{/* Completed Releases */}
{completed.length > 0 && (
<section className="mb-12">
<h2 className="text-2xl font-bold text-slate-900 mb-4">
Voltooid
</h2>
<div className="space-y-4">
{completed.map((release) => (
<ReleaseCard key={release.slug} release={release} />
))}
</div>
</section>
)}
{/* In Progress Releases */}
{inProgress.length > 0 && (
<section className="mb-12">
<h2 className="text-2xl font-bold text-slate-900 mb-4">
🔄 In Progress
</h2>
<div className="space-y-4">
{inProgress.map((release) => (
<ReleaseCard key={release.slug} release={release} />
))}
</div>
</section>
)}
{/* Planned Releases */}
{planned.length > 0 && (
<section className="mb-12">
<h2 className="text-2xl font-bold text-slate-900 mb-4">
Gepland
</h2>
<div className="space-y-4">
{planned.map((release) => (
<ReleaseCard key={release.slug} release={release} />
))}
</div>
</section>
)}
</div>
</div>
)
}
function ReleaseCard({ release }: { release: any }) {
return (
<Link
href={`/releases/${release.slug}`}
className="block bg-white rounded-lg border border-slate-200 p-6 hover:border-teal-300 hover:shadow-md transition-all"
>
<div className="flex items-start justify-between gap-4">
<div className="flex-1">
<div className="flex items-center gap-3 mb-2">
<h3 className="text-xl font-bold text-slate-900">
{release.frontmatter.title}
</h3>
<StatusBadge status={release.frontmatter.status} />
</div>
<p className="text-slate-600 mb-3">
{release.frontmatter.description}
</p>
<div className="flex items-center gap-4 text-sm text-slate-500">
<span className="capitalize">{release.frontmatter.group.replace('-', ' ')}</span>
<span></span>
<span>v{release.frontmatter.version}</span>
<span></span>
<span>{new Date(release.frontmatter.releaseDate).toLocaleDateString('nl-NL', {
year: 'numeric',
month: 'long',
day: 'numeric'
})}</span>
</div>
</div>
<div className="text-slate-400">
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
</svg>
</div>
</div>
</Link>
)
}
function StatusBadge({ status }: { status: 'completed' | 'in_progress' | 'planned' }) {
const styles = {
completed: 'bg-teal-100 text-teal-700 border-teal-200',
in_progress: 'bg-amber-100 text-amber-700 border-amber-200',
planned: 'bg-slate-100 text-slate-600 border-slate-200',
}
const labels = {
completed: 'Voltooid',
in_progress: 'Bezig',
planned: 'Gepland',
}
return (
<span className={`inline-flex items-center px-3 py-1 rounded-full text-xs font-medium border ${styles[status]}`}>
{labels[status]}
</span>
)
}

View File

@@ -1,6 +1,10 @@
{ {
"logo": "AI SPEEDRUN", "logo": "AI SPEEDRUN",
"links": [ "links": [
{
"label": "Releases",
"href": "/releases"
},
{ {
"label": "Contact", "label": "Contact",
"href": "/contact" "href": "/contact"

View File

@@ -0,0 +1,87 @@
{
"groups": [
{
"id": "foundation",
"title": "Foundation",
"description": "Basis setup en infrastructuur",
"order": 1
},
{
"id": "features",
"title": "Core Features",
"description": "EPD functionaliteit",
"order": 2
},
{
"id": "infrastructure",
"title": "Infrastructure",
"description": "Ondersteunende systemen",
"order": 3
}
],
"categories": [
{
"slug": "authentication",
"title": "Authentication",
"group": "foundation",
"description": "Login, signup en password reset",
"order": 1
},
{
"slug": "database",
"title": "Database & Schema",
"group": "foundation",
"description": "PostgreSQL schema en RLS policies",
"order": 2
},
{
"slug": "environment",
"title": "Environment Setup",
"group": "foundation",
"description": "Development en deployment configuratie",
"order": 3
},
{
"slug": "dashboard",
"title": "Dashboard & Navigation",
"group": "features",
"description": "EPD layout en navigatie",
"order": 4
},
{
"slug": "client-management",
"title": "Client Management",
"group": "features",
"description": "CRUD operations voor cliënten",
"order": 5
},
{
"slug": "ai-features",
"title": "AI Integrations",
"group": "features",
"description": "AI-gestuurde workflows",
"order": 6
},
{
"slug": "hosting",
"title": "Hosting & Deployment",
"group": "infrastructure",
"description": "Vercel deployment en CI/CD",
"order": 7
},
{
"slug": "design-system",
"title": "Design System",
"group": "infrastructure",
"description": "UI components en styling",
"order": 8
},
{
"slug": "performance",
"title": "Performance",
"group": "infrastructure",
"description": "Optimalisaties en monitoring",
"order": 9
}
]
}

View File

@@ -0,0 +1,198 @@
---
title: "Authentication & User Management"
category: "authentication"
group: "foundation"
version: "0.1.0"
releaseDate: "2024-11-15"
status: "completed"
description: "Login, signup en password reset functionaliteit via Supabase Auth"
---
## Overview
De authentication flow vormt de basis van het Mini-ECD systeem. Gebruikers kunnen nu veilig inloggen, accounts aanmaken en wachtwoorden resetten via Supabase Auth integratie.
**Key features:**
- Email/password authentication
- Demo account voor quick testing
- Password reset flow
- Session management met JWT tokens
- Security via RLS policies
---
## Features
### Login Flow
![Login screen](/releases/authentication/login-screen.png)
*Login formulier met email/password fields en demo account optie*
De login pagina biedt twee opties:
- **Handmatige login:** Email en wachtwoord
- **Demo account:** One-click toegang met `demo@mini-ecd.demo`
**Functionaliteit:**
- Client-side validatie (email format, min 8 characters)
- Server-side authenticatie via Supabase
- Error handling met user-friendly messages
- Redirect naar `/epd/clients` na succesvolle login
**Demo:**
1. Ga naar `/login`
2. Klik op "Demo Account Proberen"
3. Automatisch ingelogd en doorgestuurd
### Signup Flow
Nieuwe gebruikers kunnen zich registreren met:
- Email adres (moet uniek zijn)
- Wachtwoord (min 8 karakters)
- Wachtwoord confirmatie
**Auto-login functionaliteit:**
- Email confirmation is uitgeschakeld voor development
- Direct ingelogd na signup
- Redirect naar EPD applicatie
**Duplicate email handling:**
- Auth hook detecteert bestaande emails
- Automatische login als email al bestaat
- User-friendly message: "Dit emailadres bestaat al. Je bent nu ingelogd!"
### Password Reset
*Coming soon - gepland voor volgende release*
---
## Technical Notes
### Architecture
**Stack:**
- Supabase Auth voor user management
- Next.js 15 Server Components
- Client-side auth helpers in `/lib/auth/client.ts`
**Security:**
- PKCE flow voor OAuth (toekomst)
- JWT tokens in httpOnly cookies
- RLS policies voor data isolatie
- No API keys in frontend code
### File Structure
```
app/
├── login/
│ └── page.tsx # Login/signup form
└── auth/
└── callback/
└── route.ts # OAuth callback (future)
lib/
├── auth/
│ ├── client.ts # loginWithPassword, signUpWithPassword
│ └── server.ts # createClient for server components
supabase/
└── functions/
└── auth-hook/
└── index.ts # Duplicate email detection
```
### Key Functions
```tsx
// Login function
export async function loginWithPassword(email: string, password: string) {
const supabase = createClient()
const { data, error } = await supabase.auth.signInWithPassword({
email,
password,
})
if (error) throw error
return data
}
// Signup with auto-login
export async function signUpWithPassword(email: string, password: string) {
const supabase = createClient()
const { data, error } = await supabase.auth.signUp({
email,
password,
options: {
emailRedirectTo: `${origin}/auth/callback`,
},
})
if (error) throw error
return data
}
```
### Auth Hook
Edge Function voor duplicate email detection:
```typescript
// Prevents duplicate signups by auto-logging in existing users
if (event === 'signup' && existingUser) {
return new Response(
JSON.stringify({
error: {
message: 'Dit emailadres is al geregistreerd. Je wordt automatisch ingelogd.',
code: 'user_already_registered',
data: { shouldAutoLogin: true }
}
}),
{ status: 400 }
)
}
```
---
## Metrics
**Development:**
- ⏱️ Build tijd: 6 uur
- 📝 Lines of code: ~380
- 🧪 Tests: Manual testing (geautomatiseerde tests komen later)
**Performance:**
- 🚀 Login response: < 500ms
- 📦 Bundle size: +15kb (auth client)
- 💾 Database queries: 1 per login
**Cost:**
- 💰 Supabase: €0 (free tier)
- 💰 Edge Functions: €0 (within limits)
---
## What's Next
Geplande verbeteringen voor volgende releases:
- 🔜 Password reset flow
- 🔜 Email verification (production)
- 🔜 OAuth providers (Google, GitHub)
- 🔜 Multi-factor authentication
- 🔜 Session management dashboard
---
## Related Links
**Timeline:**
- [Week 1 - Foundation & Marketing](#timeline)
**Code:**
- [Authentication client library](https://github.com/yourusername/mini-ecd/blob/main/lib/auth/client.ts)
- [Auth hook function](https://github.com/yourusername/mini-ecd/blob/main/supabase/functions/auth-hook)
**Documentation:**
- [Supabase Auth Docs](https://supabase.com/docs/guides/auth)

View File

@@ -0,0 +1,329 @@
# 🚀 Bouwplan: Release Notes Feature
**Projectnaam:** Release Notes Pagina
**Versie:** v1.0
**Datum:** 19-11-2024
**Auteur:** Colin + AI
---
## 1. Doel en Context
Toevoegen van een dedicated release notes pagina aan de AI Speedrun website om transparantie te bieden over ontwikkelvoortgang. De pagina past in de "build in public" filosofie en biedt:
- **Overzichtspagina** met lijst van alle releases
- **Detail paginas** per week met uitgebreide release notes
- **Sidebar navigatie** (standard software docs stijl)
- **Links vanuit timeline** naar release details
---
## 2. Uitgangspunten
### 2.1 Technische Stack
- **Frontend:** Next.js 15 (bestaand) + TailwindCSS
- **Content:** MDX bestanden in `/content/nl/releases/` met frontmatter
- **Content Parser:** `next-mdx-remote` of `@next/mdx` voor MDX rendering
- **Routing:** `/releases` (overview) + `/releases/[category]` (detail per functionaliteit)
- **Components:** ReleaseSidebar, MDXContent wrapper
- **Layout:** Marketing layout met sidebar
- **Navigatie:** Thematisch op functionaliteit (niet chronologisch op weken)
- **Images:** `/public/releases/[category]/` voor screenshots en visuals
### 2.2 Projectkaders
- **Tijd:** 2-3 uur implementatie
- **Budget:** €0 (bestaande stack)
- **Scope:** MVP - basis release notes zonder fancy features
- **Design:** Standard docs-stijl (links sidebar + content)
### 2.3 Programmeer Uitgangspunten
- **DRY:** Hergebruik bestaande timeline data waar mogelijk
- **KISS:** Simpele layout, geen onnodige animaties
- **SOC:** Content schemas apart, components herbruikbaar
- **Mobile-first:** Horizontal scroll nav op mobile, sidebar op desktop
---
## 3. Epics & Stories Overzicht
| Epic ID | Titel | Doel | Status | Stories | Geschatte tijd |
|---------|-------|------|--------|---------|----------------|
| E1 | Content Schema | Release data structuur | ⏳ | 2 | 30 min |
| E2 | Route Structure | Pages en layouts | ⏳ | 3 | 45 min |
| E3 | Components | Sidebar + content | ⏳ | 2 | 45 min |
| E4 | Integration | Timeline links + nav | ⏳ | 2 | 30 min |
**Totaal geschat:** 2.5 uur
---
## 4. Epics & Stories (Uitwerking)
### Epic 1 — Content Schema
| Story | Beschrijving | Acceptatie | Status |
|-------|--------------|------------|--------|
| E1.S1 | MDX template maken | Release note template met frontmatter + content secties | ⏳ |
| E1.S2 | Content categories definiëren | Logische indeling functionaliteiten (Auth, Dashboard, AI, etc.) | ⏳ |
| E1.S3 | MDX parser setup | next-mdx-remote configureren voor content rendering | ⏳ |
**MDX Template:**
```mdx
---
title: "Authentication & User Management"
category: "authentication"
group: "foundation"
version: "0.1.0"
releaseDate: "2024-11-15"
status: "completed"
description: "Login, signup en password reset functionaliteit"
---
## Overview
Korte beschrijving van wat er is gebouwd in deze release...
## Features
### Login Flow
![Login screenshot](/releases/authentication/login-screen.png)
Beschrijving van de login functionaliteit:
- Email/password login
- Demo account optie
- Error handling
Code voorbeeld (optioneel):
```tsx
const handleLogin = async (email, password) => {
// implementation
}
```
### Signup Flow
[Details over signup...]
## Technical Notes
- Supabase Auth integratie
- RLS policies voor security
- Client-side validation
## Related Links
- [Timeline Week 1](/timeline#week-1)
- [Database Schema](/docs/schema)
```
**Voorbeeld categorieën:**
- `authentication` - Login, signup, password reset
- `client-management` - CRUD operations voor cliënten
- `dashboard` - EPD dashboard en navigatie
- `ai-features` - AI integraties (samenvatting, classificatie, plannen)
- `infrastructure` - Database, hosting, performance
- `design-system` - UI components en styling
---
### Epic 2 — Route Structure
| Story | Beschrijving | Acceptatie | Status |
|-------|--------------|------------|--------|
| E2.S1 | Layout maken | `app/(marketing)/releases/layout.tsx` met sidebar | ⏳ |
| E2.S2 | Overview page | `app/(marketing)/releases/page.tsx` met lijst (read MDX frontmatter) | ⏳ |
| E2.S3 | Detail pages | `app/(marketing)/releases/[category]/page.tsx` met MDX rendering | ⏳ |
**Structuur:**
```
# Code structuur
app/(marketing)/releases/
├── layout.tsx # Sidebar wrapper
├── page.tsx # Overview (lijst van alle MDX files)
├── [category]/
│ └── page.tsx # MDX content renderer
└── components/
├── release-sidebar.tsx
└── mdx-components.tsx # Custom components voor MDX
# Content structuur
content/nl/releases/
├── authentication.mdx
├── client-management.mdx
├── dashboard.mdx
├── ai-features.mdx
├── infrastructure.mdx
└── design-system.mdx
# Assets structuur
public/releases/
├── authentication/
│ ├── login-screen.png
│ └── signup-flow.png
├── ai-features/
│ ├── summary-demo.gif
│ └── classification.png
└── ...
```
**URL voorbeelden:**
- `/releases` - Overview alle functionaliteiten (parsed MDX frontmatter)
- `/releases/authentication` - Renders `authentication.mdx`
- `/releases/ai-features` - Renders `ai-features.mdx`
- `/releases/dashboard` - Renders `dashboard.mdx`
---
### Epic 3 — Components
| Story | Beschrijving | Acceptatie | Status |
|-------|--------------|------------|--------|
| E3.S1 | ReleaseSidebar | Fixed sidebar desktop, horizontal scroll mobile, reads MDX files | ⏳ |
| E3.S2 | MDX Components | Custom components voor images, code blocks, callouts | ⏳ |
| E3.S3 | Typography styling | Prose styling voor MDX content (tailwindcss/typography) | ⏳ |
**Features:**
- Sidebar auto-generated from MDX files in `/content/nl/releases/`
- Active state in sidebar
- Mobile-responsive
- Status indicators (completed/in progress/planned) from frontmatter
- Custom MDX components:
- Images met caption
- Code blocks met syntax highlighting
- Callout boxes (info, warning, success)
- YouTube/video embeds (optional)
---
### Epic 4 — Integration
| Story | Beschrijving | Acceptatie | Status |
|-------|--------------|------------|--------|
| E4.S1 | Timeline links | "Release notes →" link per week in timeline | ⏳ |
| E4.S2 | Nav menu | "Releases" link in header nav | ⏳ |
**Changes:**
- `timeline.tsx`: Add link per week
- `navigation.json`: Add releases link
- Test navigation flow
---
## 5. Design Specs (Compact)
**Layout:**
```
┌────────────────────────────────────────┐
│ Header (existing) │
├──────────────┬─────────────────────────┤
│ Sidebar │ Main Content │
│ │ │
│ Overview │ # Release Notes │
│ ────── │ │
│ Foundation │ [Category cards...] │
│ ├─ Auth │ │
│ ├─ Database │ │
│ Features │ │
│ ├─ Dashboard │ │
│ ├─ Clients │ │
│ ├─ AI │ │
│ Infrastructure │
│ ├─ Hosting │ │
│ ├─ Design │ │
└──────────────┴─────────────────────────┘
```
**Sidebar structuur (thematisch):**
- **Foundation** (basis setup)
- Authentication
- Database & Schema
- Environment Setup
- **Core Features** (EPD functionaliteit)
- Dashboard & Navigation
- Client Management
- AI Integrations
- **Infrastructure** (ondersteunend)
- Hosting & Deployment
- Design System
- Performance
**Mobile:** Horizontal scroll tabs boven content
**Content styling:**
- `@tailwindcss/typography` plugin voor prose content
- Custom MDX components styled met Tailwind
- Images responsive met captions
- Code blocks met syntax highlighting (shiki/prism)
- Heading anchors voor deep linking
**Colors:**
- Teal accents (consistent met brand)
- Slate backgrounds for code blocks
- White background for main content
- Prose styles voor text readability
---
## 6. Acceptatiecriteria
### Must Have:
- [ ] `/releases` toont overzicht gegroepeerd per categorie
- [ ] `/releases/[category]` toont detail van specifieke functionaliteit
- [ ] Sidebar heeft thematische grouping (Foundation, Features, Infrastructure)
- [ ] Sidebar werkt op desktop (fixed) met collapsible sections
- [ ] Mobile heeft horizontal scroll nav met categorieën
- [ ] Timeline linkt naar relevante release categories
- [ ] "Releases" link in header nav
- [ ] Active states in navigatie
### Nice to Have (later):
- RSS feed
- Search functie
- Filters (features/fixes/improvements)
- Changelog syntax highlighting
---
## 7. Risico's & Mitigatie
| Risico | Kans | Impact | Mitigatie |
|--------|------|--------|-----------|
| Content duplicatie met timeline | Middel | Laag | Hergebruik timeline data, extend met extra fields |
| Mobile nav niet intuïtief | Middel | Middel | Test met gebruiker, voeg tooltips toe indien nodig |
| SEO voor individuele releases | Laag | Laag | Metadata per release page toevoegen |
---
## 8. Implementatie Volgorde
1. **E1.S1:** MDX template maken + voorbeeld content
2. **E1.S2:** Categorieën definiëren (authentication, dashboard, ai-features, etc.)
3. **E1.S3:** MDX parser setup (next-mdx-remote of @next/mdx)
4. **E2.S1:** Layout component met sidebar (thematische grouping)
5. **E3.S1:** ReleaseSidebar component (auto-generated from MDX files)
6. **E3.S2:** Custom MDX components (images, code, callouts)
7. **E3.S3:** Typography styling (@tailwindcss/typography)
8. **E2.S2:** Overview page (list van MDX frontmatter)
9. **E2.S3:** Detail page (MDX renderer)
10. **E4.S1:** Timeline integration (link naar relevante categories)
11. **E4.S2:** Navigation update (releases link)
---
## 9. Referenties
**Bestaande Componenten:**
- `app/(marketing)/components/build-timeline.tsx` - Timeline component
- `content/nl/timeline.json` - Timeline data
- `app/(marketing)/layout.tsx` - Marketing layout
**Design Inspiratie:**
- Next.js docs (https://nextjs.org/docs)
- Vercel changelog (https://vercel.com/changelog)
- Linear releases (https://linear.app/releases)
---
## 10. Versiehistorie
| Versie | Datum | Auteur | Wijziging |
|--------|-------|--------|-----------|
| v1.0 | 19-11-2024 | Colin + AI | Initiële versie - compact bouwplan |
| v1.1 | 19-11-2024 | Colin + AI | Update: thematische indeling ipv chronologisch |
| v1.2 | 19-11-2024 | Colin + AI | Update: MDX/Markdown content ipv JSON/cards |

231
docs/templates/release-note-template.mdx vendored Normal file
View File

@@ -0,0 +1,231 @@
---
title: "[Feature Name] - Descriptive Title"
category: "category-slug"
group: "foundation | features | infrastructure"
version: "0.1.0"
releaseDate: "2024-11-15"
status: "completed | in_progress | planned"
description: "Korte beschrijving (1-2 zinnen) voor overview pagina"
---
## Overview
Korte introductie van wat er is gebouwd. Waarom is dit belangrijk? Welk probleem lost het op?
Bijvoorbeeld:
> "De authentication flow is de basis van het EPD systeem. Gebruikers kunnen nu veilig inloggen, accounts aanmaken en wachtwoorden resetten via Supabase Auth."
---
## Features
### Feature 1: [Naam]
![Screenshot beschrijving](/releases/[category]/[image].png)
*Caption: Beschrijf wat je in de screenshot ziet*
Beschrijving van de feature:
- Belangrijkste functionaliteit
- User benefits
- Edge cases afgehandeld
**Demo:**
- Stap 1: Open `/login`
- Stap 2: Vul credentials in
- Stap 3: Klik op login knop
- Resultaat: Redirect naar `/epd/clients`
**Code voorbeeld** (optioneel):
```tsx
// Relevante code snippet
const handleLogin = async (email: string, password: string) => {
const { data, error } = await supabase.auth.signInWithPassword({
email,
password
})
if (error) throw error
return data
}
```
### Feature 2: [Naam]
[Herhaal bovenstaande structuur...]
---
## Improvements
Verbeteringen aan bestaande functionaliteit:
- **Performance:** Login response tijd van 2s naar 500ms
- **UX:** Error messages nu user-friendly
- **Accessibility:** Keyboard navigation toegevoegd
---
## Bug Fixes
Opgeloste bugs (indien van toepassing):
- ✅ Fixed: Wachtwoord reset email kwam niet aan
- ✅ Fixed: Session expiry handling
- ✅ Fixed: Redirect loop bij logout
---
## Technical Notes
### Architecture
Technische details voor developers:
- **Stack:** Supabase Auth + Next.js 15 + Server Actions
- **Database:** RLS policies voor user isolation
- **Security:** PKCE flow voor OAuth
- **Session:** JWT tokens in httpOnly cookies
### File Structure
```
app/
├── login/
│ └── page.tsx # Login form
├── signup/
│ └── page.tsx # Signup form
└── auth/
└── callback/
└── route.ts # OAuth callback handler
lib/
├── auth/
│ ├── client.ts # Client-side auth functions
│ └── server.ts # Server-side auth helpers
```
### Database Schema
```sql
-- Relevant tables/policies
CREATE POLICY "Users can only read own data"
ON profiles FOR SELECT
USING (auth.uid() = id);
```
### Configuration
Environment variables vereist:
```bash
NEXT_PUBLIC_SUPABASE_URL=your_url
NEXT_PUBLIC_SUPABASE_ANON_KEY=your_key
```
---
## Breaking Changes
⚠️ **Let op:** Breaking changes voor developers (indien van toepassing):
- `loginUser()` functie hernoemd naar `signInWithPassword()`
- `UserSession` type nu `Session` (import van @supabase/supabase-js)
**Migration guide:**
```tsx
// ❌ Oud
import { loginUser } from '@/lib/auth'
const session = await loginUser(email, password)
// ✅ Nieuw
import { signInWithPassword } from '@/lib/auth/client'
const { data } = await signInWithPassword(email, password)
```
---
## Testing
### Manual Test Checklist
- [ ] Login met valid credentials werkt
- [ ] Login met invalid credentials toont error
- [ ] Signup flow compleet doorlopen
- [ ] Password reset email ontvangen
- [ ] Session persists na page reload
- [ ] Logout werkt correct
### Automated Tests
```bash
# Run tests
npm run test:auth
# Coverage
npm run test:coverage
```
---
## Metrics
**Development:**
- ⏱️ Build tijd: 8 uur
- 📝 Lines of code: ~450
- 🧪 Test coverage: 85%
**Performance:**
- 🚀 Login response: < 500ms (was 2s)
- 📦 Bundle size: +12kb (auth client)
- 💾 Database queries: 2 per login
**Cost:**
- 💰 Supabase: €0 (free tier)
- 💰 Vercel: €0 (hobby plan)
---
## Screenshots
### Login Screen
![Login screen](/releases/authentication/login-screen.png)
*Login formulier met email/password en demo account optie*
### Signup Flow
![Signup screen](/releases/authentication/signup-flow.png)
*Signup formulier met password confirmation*
### Error States
![Error handling](/releases/authentication/error-states.png)
*User-friendly error messages voor verschillende scenarios*
---
## Related Links
**Timeline:**
- [Week 1 - Foundation](/timeline#week-1)
**Documentation:**
- [Database Schema](/docs/database-schema)
- [API Documentation](/docs/api)
**External:**
- [Supabase Auth Docs](https://supabase.com/docs/guides/auth)
- [Next.js Auth Guide](https://nextjs.org/docs/authentication)
---
## What's Next
Geplande verbeteringen voor volgende releases:
- 🔜 OAuth providers (Google, GitHub)
- 🔜 Multi-factor authentication (MFA)
- 🔜 Magic link login
- 🔜 Session management dashboard
---
## Feedback
Vragen of feedback? [Open een issue](https://github.com/[org]/[repo]/issues) of [stuur een email](mailto:contact@example.com).

122
lib/mdx/releases.ts Normal file
View File

@@ -0,0 +1,122 @@
/**
* Release Notes MDX Utilities
*
* Functions for loading and parsing release note MDX files
*/
import fs from 'fs'
import path from 'path'
import matter from 'gray-matter'
import { MDXRemote } from 'next-mdx-remote/rsc'
const RELEASES_DIR = path.join(process.cwd(), 'content/nl/releases')
export interface ReleaseFrontmatter {
title: string
category: string
group: 'foundation' | 'features' | 'infrastructure'
version: string
releaseDate: string
status: 'completed' | 'in_progress' | 'planned'
description: string
}
export interface ReleaseNote {
slug: string
frontmatter: ReleaseFrontmatter
content: string
}
/**
* Get all release note files
*/
export async function getAllReleases(): Promise<ReleaseNote[]> {
const files = fs.readdirSync(RELEASES_DIR)
const releases = files
.filter(file => file.endsWith('.mdx') && !file.startsWith('_'))
.map(file => {
const slug = file.replace('.mdx', '')
const filePath = path.join(RELEASES_DIR, file)
const fileContent = fs.readFileSync(filePath, 'utf-8')
const { data, content } = matter(fileContent)
return {
slug,
frontmatter: data as ReleaseFrontmatter,
content,
}
})
// Sort by group order and then by category
return releases.sort((a, b) => {
const groupOrder = { foundation: 1, features: 2, infrastructure: 3 }
const aOrder = groupOrder[a.frontmatter.group]
const bOrder = groupOrder[b.frontmatter.group]
if (aOrder !== bOrder) return aOrder - bOrder
return a.frontmatter.category.localeCompare(b.frontmatter.category)
})
}
/**
* Get a single release by slug
*/
export async function getRelease(slug: string): Promise<ReleaseNote | null> {
const filePath = path.join(RELEASES_DIR, `${slug}.mdx`)
if (!fs.existsSync(filePath)) {
return null
}
const fileContent = fs.readFileSync(filePath, 'utf-8')
const { data, content } = matter(fileContent)
return {
slug,
frontmatter: data as ReleaseFrontmatter,
content,
}
}
/**
* Get releases grouped by their group (foundation, features, infrastructure)
*/
export async function getReleasesGrouped() {
const releases = await getAllReleases()
return {
foundation: releases.filter(r => r.frontmatter.group === 'foundation'),
features: releases.filter(r => r.frontmatter.group === 'features'),
infrastructure: releases.filter(r => r.frontmatter.group === 'infrastructure'),
}
}
/**
* Get category metadata from index file
*/
export interface CategoryMetadata {
slug: string
title: string
group: string
description: string
order: number
}
export interface GroupMetadata {
id: string
title: string
description: string
order: number
}
interface IndexData {
groups: GroupMetadata[]
categories: CategoryMetadata[]
}
export async function getCategoryMetadata(): Promise<IndexData> {
const indexPath = path.join(RELEASES_DIR, '_index.json')
const indexContent = fs.readFileSync(indexPath, 'utf-8')
return JSON.parse(indexContent)
}

View File

@@ -20,8 +20,10 @@
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"framer-motion": "^12.23.24", "framer-motion": "^12.23.24",
"gray-matter": "^4.0.3",
"lucide-react": "^0.553.0", "lucide-react": "^0.553.0",
"next": "16.0.1", "next": "16.0.1",
"next-mdx-remote": "^5.0.0",
"next-themes": "^0.4.6", "next-themes": "^0.4.6",
"react": "19.2.0", "react": "19.2.0",
"react-dom": "19.2.0", "react-dom": "19.2.0",

1138
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff