hero-section, start login
This commit is contained in:
@@ -3,20 +3,25 @@
|
|||||||
import { HeroSection } from '@/components/ui/hero-section-2'
|
import { HeroSection } from '@/components/ui/hero-section-2'
|
||||||
|
|
||||||
interface HeroSectionClientProps {
|
interface HeroSectionClientProps {
|
||||||
logo: {
|
logo?: {
|
||||||
url: string
|
url: string
|
||||||
alt: string
|
alt: string
|
||||||
text?: string
|
text?: string
|
||||||
}
|
}
|
||||||
slogan?: string
|
slogan?: string
|
||||||
title: React.ReactNode
|
title: React.ReactNode
|
||||||
subtitle: string
|
subtitle: string | React.ReactNode
|
||||||
|
punchline?: string | React.ReactNode
|
||||||
callToAction: {
|
callToAction: {
|
||||||
text: string
|
text: string
|
||||||
href: string
|
href: string
|
||||||
}
|
}
|
||||||
|
secondaryCallToAction?: {
|
||||||
|
text: string
|
||||||
|
href: string
|
||||||
|
}
|
||||||
backgroundImage: string
|
backgroundImage: string
|
||||||
contactInfo: {
|
contactInfo?: {
|
||||||
website: string
|
website: string
|
||||||
phone: string
|
phone: string
|
||||||
address: string
|
address: string
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import { getContent } from '@/lib/content/loader'
|
|||||||
import type { MetadataContent } from '@/content/schemas/manifesto'
|
import type { MetadataContent } from '@/content/schemas/manifesto'
|
||||||
import { HeroSectionClient } from './components/hero-section-client'
|
import { HeroSectionClient } from './components/hero-section-client'
|
||||||
import { BuildTimeline } from './components/build-timeline'
|
import { BuildTimeline } from './components/build-timeline'
|
||||||
|
import { WhyMe } from '@/components/ui/why-me'
|
||||||
|
|
||||||
// Generate metadata for SEO
|
// Generate metadata for SEO
|
||||||
export async function generateMetadata(): Promise<Metadata> {
|
export async function generateMetadata(): Promise<Metadata> {
|
||||||
@@ -70,13 +71,31 @@ export async function generateMetadata(): Promise<Metadata> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Content interfaces
|
// Content interfaces
|
||||||
interface StatementContent {
|
interface HeroContent {
|
||||||
hero: {
|
title: {
|
||||||
quote: string
|
main: string
|
||||||
attribution: string
|
accent: string
|
||||||
attributionContext: string
|
|
||||||
subtitle: string
|
|
||||||
}
|
}
|
||||||
|
subtitle: string
|
||||||
|
punchline: {
|
||||||
|
question: string
|
||||||
|
answer: string
|
||||||
|
}
|
||||||
|
callToAction: {
|
||||||
|
primary: {
|
||||||
|
text: string
|
||||||
|
href: string
|
||||||
|
}
|
||||||
|
secondary: {
|
||||||
|
text: string
|
||||||
|
href: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
backgroundImage: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface StatementContent {
|
||||||
|
hero: HeroContent
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Feature {
|
interface Feature {
|
||||||
@@ -111,35 +130,32 @@ export default async function HomePage() {
|
|||||||
// Load content
|
// Load content
|
||||||
const manifestoContent = await getContent<StatementContent>('nl', 'manifesto')
|
const manifestoContent = await getContent<StatementContent>('nl', 'manifesto')
|
||||||
const timelineContent = await getContent<TimelineContent>('nl', 'timeline')
|
const timelineContent = await getContent<TimelineContent>('nl', 'timeline')
|
||||||
|
const heroContent = manifestoContent.hero
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* Hero Section */}
|
{/* Hero Section */}
|
||||||
<HeroSectionClient
|
<HeroSectionClient
|
||||||
logo={{
|
|
||||||
url: "/next.svg",
|
|
||||||
alt: "AI Speedrun Logo",
|
|
||||||
text: "AI Speedrun"
|
|
||||||
}}
|
|
||||||
slogan="BUILD IN PUBLIC"
|
|
||||||
title={
|
title={
|
||||||
<>
|
<>
|
||||||
Software on Demand
|
{heroContent.title.main}{' '}
|
||||||
<br />
|
<span className="text-teal-600 block mt-2">
|
||||||
<span className="text-teal-600">Van €100k naar €200</span>
|
{heroContent.title.accent}
|
||||||
|
</span>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
subtitle="Enterprise software hoeft niet meer €100.000+ per jaar te kosten. AI-powered development verkort dit naar 4 weken en €200 totale kosten. Dit EPD prototype is het levende bewijs."
|
subtitle={heroContent.subtitle}
|
||||||
callToAction={{
|
punchline={
|
||||||
text: "PROBEER HET PROTOTYPE →",
|
<>
|
||||||
href: "/login"
|
{heroContent.punchline.question}
|
||||||
}}
|
<br />
|
||||||
backgroundImage="https://images.unsplash.com/photo-1451187580459-43490279c0fa?q=80&w=2072&auto=format&fit=crop"
|
<br />
|
||||||
contactInfo={{
|
<strong>{heroContent.punchline.answer}</strong>
|
||||||
website: "aispeedrun.nl",
|
</>
|
||||||
phone: "Demo Project",
|
}
|
||||||
address: "Build in Public"
|
callToAction={heroContent.callToAction.primary}
|
||||||
}}
|
secondaryCallToAction={heroContent.callToAction.secondary}
|
||||||
|
backgroundImage={heroContent.backgroundImage}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Statement Section - Software on Demand */}
|
{/* Statement Section - Software on Demand */}
|
||||||
@@ -181,6 +197,9 @@ export default async function HomePage() {
|
|||||||
<BuildTimeline data={timelineContent} />
|
<BuildTimeline data={timelineContent} />
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
{/* About Me Section */}
|
||||||
|
<WhyMe />
|
||||||
|
|
||||||
{/* CTA Section */}
|
{/* CTA Section */}
|
||||||
<section className="py-24 px-4 text-center bg-gradient-to-br from-slate-50 to-white">
|
<section className="py-24 px-4 text-center bg-gradient-to-br from-slate-50 to-white">
|
||||||
<div className="max-w-3xl mx-auto">
|
<div className="max-w-3xl mx-auto">
|
||||||
|
|||||||
@@ -211,16 +211,40 @@ body {
|
|||||||
outline-offset: 2px;
|
outline-offset: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Floating animation for expertise badges */
|
||||||
|
@keyframes float {
|
||||||
|
0%, 100% {
|
||||||
|
transform: translateY(0px) rotate(0deg);
|
||||||
|
}
|
||||||
|
25% {
|
||||||
|
transform: translateY(-5px) rotate(0.5deg);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: translateY(-3px) rotate(-0.5deg);
|
||||||
|
}
|
||||||
|
75% {
|
||||||
|
transform: translateY(-7px) rotate(0.3deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-float {
|
||||||
|
animation: float 5s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
/* Reduced motion support */
|
/* Reduced motion support */
|
||||||
@media (prefers-reduced-motion: reduce) {
|
@media (prefers-reduced-motion: reduce) {
|
||||||
*,
|
*,
|
||||||
*::before,
|
*::before,
|
||||||
*::after {
|
*::after {
|
||||||
animation-duration: 0.01ms !important;
|
animation-duration: 0.02ms !important;
|
||||||
animation-iteration-count: 1 !important;
|
animation-iteration-count: 1 !important;
|
||||||
transition-duration: 0.01ms !important;
|
transition-duration: 0.03ms !important;
|
||||||
scroll-behavior: auto !important;
|
scroll-behavior: auto !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.animate-float {
|
||||||
|
animation: none !important;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* AI source highlighting (gebruikt in Intake editor) */
|
/* AI source highlighting (gebruikt in Intake editor) */
|
||||||
|
|||||||
@@ -37,13 +37,18 @@ interface HeroSectionProps {
|
|||||||
};
|
};
|
||||||
slogan?: string;
|
slogan?: string;
|
||||||
title: React.ReactNode;
|
title: React.ReactNode;
|
||||||
subtitle: string;
|
subtitle: string | React.ReactNode;
|
||||||
|
punchline?: string | React.ReactNode;
|
||||||
callToAction: {
|
callToAction: {
|
||||||
text: string;
|
text: string;
|
||||||
href: string;
|
href: string;
|
||||||
};
|
};
|
||||||
|
secondaryCallToAction?: {
|
||||||
|
text: string;
|
||||||
|
href: string;
|
||||||
|
};
|
||||||
backgroundImage: string;
|
backgroundImage: string;
|
||||||
contactInfo: {
|
contactInfo?: {
|
||||||
website: string;
|
website: string;
|
||||||
phone: string;
|
phone: string;
|
||||||
address: string;
|
address: string;
|
||||||
@@ -52,7 +57,7 @@ interface HeroSectionProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const HeroSection = React.forwardRef<HTMLDivElement, HeroSectionProps>(
|
const HeroSection = React.forwardRef<HTMLDivElement, HeroSectionProps>(
|
||||||
({ className, logo, slogan, title, subtitle, callToAction, backgroundImage, contactInfo, ...props }, ref) => {
|
({ className, logo, slogan, title, subtitle, punchline, callToAction, secondaryCallToAction, backgroundImage, contactInfo, ...props }, ref) => {
|
||||||
|
|
||||||
// Animation variants for the container to orchestrate children animations
|
// Animation variants for the container to orchestrate children animations
|
||||||
const containerVariants = {
|
const containerVariants = {
|
||||||
@@ -108,36 +113,64 @@ const HeroSection = React.forwardRef<HTMLDivElement, HeroSectionProps>(
|
|||||||
</motion.header>
|
</motion.header>
|
||||||
|
|
||||||
<motion.main variants={containerVariants}>
|
<motion.main variants={containerVariants}>
|
||||||
<motion.h1 className="text-4xl font-bold leading-tight text-foreground md:text-5xl" variants={itemVariants}>
|
<motion.h1 className="text-4xl font-bold leading-tight text-foreground md:text-5xl lg:text-6xl" variants={itemVariants}>
|
||||||
{title}
|
{title}
|
||||||
</motion.h1>
|
</motion.h1>
|
||||||
<motion.div className="my-6 h-1 w-20 bg-primary" variants={itemVariants}></motion.div>
|
<motion.div className="my-6 h-1 w-20 bg-primary" variants={itemVariants}></motion.div>
|
||||||
<motion.p className="mb-8 max-w-md text-base text-muted-foreground" variants={itemVariants}>
|
<motion.div className="mb-6 max-w-lg text-base md:text-lg leading-relaxed text-slate-600" variants={itemVariants}>
|
||||||
{subtitle}
|
{subtitle}
|
||||||
</motion.p>
|
</motion.div>
|
||||||
<motion.a href={callToAction.href} className="text-lg font-bold tracking-widest text-primary transition-colors hover:text-primary/80" variants={itemVariants}>
|
|
||||||
{callToAction.text}
|
{punchline && (
|
||||||
</motion.a>
|
<motion.aside
|
||||||
|
className="mb-8 max-w-lg md:max-w-2xl border-l-4 border-primary bg-slate-50 p-6 rounded-r-lg"
|
||||||
|
variants={itemVariants}
|
||||||
|
role="note"
|
||||||
|
>
|
||||||
|
<div className="text-base md:text-lg font-medium text-slate-800 leading-relaxed [&_*:not(br)]:whitespace-normal md:[&_*:not(br)]:whitespace-nowrap">
|
||||||
|
{punchline}
|
||||||
|
</div>
|
||||||
|
</motion.aside>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<motion.div className="flex flex-col sm:flex-row gap-4" variants={itemVariants}>
|
||||||
|
<a
|
||||||
|
href={callToAction.href}
|
||||||
|
className="inline-block px-8 py-4 bg-primary hover:bg-primary/90 text-white font-bold text-base tracking-wide rounded-lg transition-colors text-center"
|
||||||
|
>
|
||||||
|
{callToAction.text}
|
||||||
|
</a>
|
||||||
|
{secondaryCallToAction && (
|
||||||
|
<a
|
||||||
|
href={secondaryCallToAction.href}
|
||||||
|
className="inline-block px-8 py-4 border-2 border-slate-300 hover:bg-slate-50 text-slate-700 font-medium text-base rounded-lg transition-colors text-center"
|
||||||
|
>
|
||||||
|
{secondaryCallToAction.text}
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
</motion.div>
|
||||||
</motion.main>
|
</motion.main>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Bottom Section: Footer Info */}
|
{/* Bottom Section: Footer Info */}
|
||||||
<motion.footer className="mt-12 w-full" variants={itemVariants}>
|
{contactInfo && (
|
||||||
<div className="grid grid-cols-1 gap-6 text-xs text-muted-foreground sm:grid-cols-3">
|
<motion.footer className="mt-12 w-full" variants={itemVariants}>
|
||||||
<div className="flex items-center">
|
<div className="grid grid-cols-1 gap-6 text-xs text-muted-foreground sm:grid-cols-3">
|
||||||
<InfoIcon type="website" />
|
<div className="flex items-center">
|
||||||
<span>{contactInfo.website}</span>
|
<InfoIcon type="website" />
|
||||||
|
<span>{contactInfo.website}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<InfoIcon type="phone" />
|
||||||
|
<span>{contactInfo.phone}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<InfoIcon type="address" />
|
||||||
|
<span>{contactInfo.address}</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center">
|
</motion.footer>
|
||||||
<InfoIcon type="phone" />
|
)}
|
||||||
<span>{contactInfo.phone}</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center">
|
|
||||||
<InfoIcon type="address" />
|
|
||||||
<span>{contactInfo.address}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</motion.footer>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Right Side: Image with Clip Path Animation */}
|
{/* Right Side: Image with Clip Path Animation */}
|
||||||
|
|||||||
66
components/ui/scroll-reveal.tsx
Normal file
66
components/ui/scroll-reveal.tsx
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { useEffect, useRef, useState, ReactNode } from 'react'
|
||||||
|
|
||||||
|
interface ScrollRevealProps {
|
||||||
|
children: ReactNode
|
||||||
|
direction?: 'up' | 'down' | 'left' | 'right'
|
||||||
|
delay?: number
|
||||||
|
className?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ScrollReveal({
|
||||||
|
children,
|
||||||
|
direction = 'up',
|
||||||
|
delay = 0,
|
||||||
|
className = '',
|
||||||
|
}: ScrollRevealProps) {
|
||||||
|
const [isVisible, setIsVisible] = useState(false)
|
||||||
|
const ref = useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const observer = new IntersectionObserver(
|
||||||
|
([entry]) => {
|
||||||
|
if (entry.isIntersecting) {
|
||||||
|
setIsVisible(true)
|
||||||
|
observer.disconnect()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
threshold: 0.1,
|
||||||
|
rootMargin: '50px',
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
if (ref.current) {
|
||||||
|
observer.observe(ref.current)
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
observer.disconnect()
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const directionClasses = {
|
||||||
|
up: 'translate-y-8',
|
||||||
|
down: '-translate-y-8',
|
||||||
|
left: 'translate-x-8',
|
||||||
|
right: '-translate-x-8',
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
className={`transition-all duration-700 ease-out ${
|
||||||
|
isVisible
|
||||||
|
? 'opacity-100 translate-x-0 translate-y-0'
|
||||||
|
: `opacity-0 ${directionClasses[direction]}`
|
||||||
|
} ${className}`}
|
||||||
|
style={{
|
||||||
|
transitionDelay: isVisible ? `${delay}ms` : '0ms',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
117
components/ui/why-me.tsx
Normal file
117
components/ui/why-me.tsx
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
import Image from 'next/image'
|
||||||
|
import { ScrollReveal } from '@/components/ui/scroll-reveal'
|
||||||
|
import aboutContent from '@/content/nl/about.json'
|
||||||
|
|
||||||
|
export function WhyMe() {
|
||||||
|
const [mounted, setMounted] = useState(false)
|
||||||
|
const { title, subtitle, image, paragraphs, stats, expertise } = aboutContent
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setMounted(true)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section
|
||||||
|
id="over"
|
||||||
|
className="section-padding bg-gray-50 relative overflow-hidden"
|
||||||
|
>
|
||||||
|
{/* Decorative elements */}
|
||||||
|
<div className="absolute top-0 right-0 w-64 h-64 bg-teal/5 rounded-full blur-3xl translate-x-1/2 -translate-y-1/2" />
|
||||||
|
<div className="absolute bottom-0 left-0 w-96 h-96 bg-teal/5 rounded-full blur-3xl -translate-x-1/2 translate-y-1/2" />
|
||||||
|
|
||||||
|
<div className="container-custom relative z-10">
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-16 lg:gap-24 items-center max-w-6xl mx-auto">
|
||||||
|
{/* Left: Text Content */}
|
||||||
|
<ScrollReveal direction="left">
|
||||||
|
<div className="space-y-8">
|
||||||
|
<h2 className="text-5xl md:text-6xl lg:text-7xl font-heading font-bold text-text-primary leading-tight">
|
||||||
|
{title}
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<p className="text-xl md:text-2xl text-text-secondary font-light leading-relaxed">
|
||||||
|
{subtitle}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="space-y-6 text-text-secondary text-lg leading-relaxed">
|
||||||
|
{paragraphs.map((paragraph, index) => (
|
||||||
|
<p key={index}>{paragraph}</p>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-4 pt-8">
|
||||||
|
{stats.map((stat, index) => (
|
||||||
|
<div key={index}>
|
||||||
|
<div className="text-3xl font-heading font-bold text-text-primary mb-1">{stat.value}</div>
|
||||||
|
<div className="text-text-secondary text-sm">{stat.label}</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ScrollReveal>
|
||||||
|
|
||||||
|
{/* Right: Photo */}
|
||||||
|
<ScrollReveal direction="right" delay={200}>
|
||||||
|
<div className="relative aspect-[3/4] lg:aspect-[4/5] max-w-md mx-auto lg:ml-auto">
|
||||||
|
{/* Optional subtle border/frame effect */}
|
||||||
|
<div className="absolute inset-0 rounded-2xl bg-teal/10" />
|
||||||
|
|
||||||
|
<div className="relative w-full h-full rounded-2xl overflow-hidden">
|
||||||
|
<Image
|
||||||
|
src={image.src}
|
||||||
|
alt={image.alt}
|
||||||
|
fill
|
||||||
|
className="object-cover"
|
||||||
|
priority
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ScrollReveal>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* FASE 2: Expertise Section - Full Width with Floating Animation */}
|
||||||
|
{expertise && (
|
||||||
|
<div className="mt-16 pt-16 border-t border-gray-200">
|
||||||
|
<ScrollReveal>
|
||||||
|
<h3 className="text-2xl md:text-3xl font-heading font-bold text-center mb-3 text-text-primary">
|
||||||
|
{expertise.label}
|
||||||
|
</h3>
|
||||||
|
<p className="text-center text-text-secondary mb-12 max-w-2xl mx-auto">
|
||||||
|
De tools en methoden waarmee ik jouw AI-uitdaging aanpak
|
||||||
|
</p>
|
||||||
|
</ScrollReveal>
|
||||||
|
|
||||||
|
<div className="flex flex-wrap justify-center gap-4 max-w-5xl mx-auto">
|
||||||
|
{expertise.items.map((item, index) => (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
className={`
|
||||||
|
group px-5 py-3 rounded-full font-medium text-sm
|
||||||
|
transition-all duration-300 cursor-default animate-float
|
||||||
|
border shadow-sm
|
||||||
|
${
|
||||||
|
item.highlight
|
||||||
|
? 'bg-teal-700 text-white border-teal-700 hover:bg-teal-600 hover:-translate-y-2 hover:rotate-2 hover:shadow-md'
|
||||||
|
: 'bg-white text-gray-800 border-gray-300 hover:bg-teal-50 hover:border-teal-600 hover:text-teal-800 hover:-translate-y-2 hover:rotate-1 hover:shadow-md'
|
||||||
|
}
|
||||||
|
`}
|
||||||
|
style={{
|
||||||
|
animationDelay: mounted ? `${index * 150}ms` : '0ms',
|
||||||
|
animationDuration: `${5 + (index % 3) * 0.8}s`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span className="font-semibold">{item.name}</span>
|
||||||
|
<span className={`ml-2 text-xs ${item.highlight ? 'opacity-80 group-hover:opacity-100' : 'opacity-70 group-hover:opacity-100'}`}>
|
||||||
|
{item.category}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
95
content/nl/about.json
Normal file
95
content/nl/about.json
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
{
|
||||||
|
"title": "Over mij",
|
||||||
|
"subtitle": "AI-gedreven development in de praktijk",
|
||||||
|
"image": {
|
||||||
|
"src": "/images/colin-office-6.webp",
|
||||||
|
"alt": "Colin van der Heijden - AI Development Specialist"
|
||||||
|
},
|
||||||
|
"paragraphs": [
|
||||||
|
"Na jaren in traditionele software development te hebben gewerkt, zag ik hoe bedrijven vast liepen in eindeloze implementatietrajecten en vendor lock-in. De belofte van AI-powered development klonk te mooi om waar te zijn - totdat ik het zelf ging testen.",
|
||||||
|
"Dit EPD prototype is geen marketingpraatje, maar een hands-on bewijs van wat er mogelijk is. Elke regel code, elk design decision, elke feature - allemaal gedocumenteerd en transparant. Want de beste manier om te laten zien wat AI-development kan, is het gewoon doen."
|
||||||
|
],
|
||||||
|
"stats": [
|
||||||
|
{
|
||||||
|
"value": "4 weken",
|
||||||
|
"label": "Van concept tot werkend prototype"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "€200",
|
||||||
|
"label": "Totale build cost"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "10+ jaar",
|
||||||
|
"label": "Ervaring in software development"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "100%",
|
||||||
|
"label": "Transparant build in public"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"expertise": {
|
||||||
|
"label": "Tech Stack & Expertise",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"name": "Gezond Verstand",
|
||||||
|
"category": "Mindset",
|
||||||
|
"highlight": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Supabase",
|
||||||
|
"category": "Backend",
|
||||||
|
"highlight": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Goeie werkethos",
|
||||||
|
"category": "Mindset",
|
||||||
|
"highlight": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Gemini AI",
|
||||||
|
"category": "AI Tool",
|
||||||
|
"highlight": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Claude AI",
|
||||||
|
"category": "AI Tool",
|
||||||
|
"highlight": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Cursor IDE",
|
||||||
|
"category": "Development",
|
||||||
|
"highlight": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "No Bullshit",
|
||||||
|
"category": "Mindset",
|
||||||
|
"highlight": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Vercel",
|
||||||
|
"category": "Hosting",
|
||||||
|
"highlight": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Supabase",
|
||||||
|
"category": "Database",
|
||||||
|
"highlight": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Bak ervaring",
|
||||||
|
"category": "Integration",
|
||||||
|
"highlight": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Handson",
|
||||||
|
"category": "Mindset",
|
||||||
|
"highlight": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Next.js",
|
||||||
|
"category": "Frontend",
|
||||||
|
"highlight": false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +1,25 @@
|
|||||||
{
|
{
|
||||||
"hero": {
|
"hero": {
|
||||||
"quote": "Software is eating the world, but AI is going to eat software",
|
"title": {
|
||||||
"attribution": "Jensen Huang, CEO van Nvidia",
|
"main": "\"Staat op de roadmap voor Q3 2026\"",
|
||||||
"attributionContext": "zei dit tijdens zijn keynote op GTC in maart 2024",
|
"accent": "Dat hoef je nooit meer te horen."
|
||||||
"subtitle": "Nu, in 2025, zien we het gebeuren. En bijna niemand heeft het door."
|
},
|
||||||
|
"subtitle": "Ik bouw live een EPD, stap voor stap, open en transparant. Laten we samen kijken hoe ver we komen.",
|
||||||
|
"punchline": {
|
||||||
|
"question": "De vraag is niet óf AI softwareontwikkeling verandert.",
|
||||||
|
"answer": "De vraag is: accepteer je de beperkingen, of kies je voor mogelijkheden?"
|
||||||
|
},
|
||||||
|
"callToAction": {
|
||||||
|
"primary": {
|
||||||
|
"text": "🔵 VOLG DE AI SPEEDRUN",
|
||||||
|
"href": "#timeline"
|
||||||
|
},
|
||||||
|
"secondary": {
|
||||||
|
"text": "⚪ WAAROM EEN EPD?",
|
||||||
|
"href": "#over"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"backgroundImage": "https://images.unsplash.com/photo-1451187580459-43490279c0fa?q=80&w=2072&auto=format&fit=crop"
|
||||||
},
|
},
|
||||||
"sections": [
|
"sections": [
|
||||||
{
|
{
|
||||||
|
|||||||
438
docs/specs/bouwplan-login-refactor-v1.0.md
Normal file
438
docs/specs/bouwplan-login-refactor-v1.0.md
Normal file
@@ -0,0 +1,438 @@
|
|||||||
|
# 🚀 Bouwplan — Login Refactor: Email + Wachtwoord Primair
|
||||||
|
|
||||||
|
**Projectnaam:** Mini EPD Demo Platform - Login Optimalisatie
|
||||||
|
**Versie:** v1.0
|
||||||
|
**Datum:** 18-01-2025
|
||||||
|
**Auteur:** Colin (met Claude Code)
|
||||||
|
**Scope:** Login page herontwerp (Email+Password primair, Magic Link secundair)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Doel en Context
|
||||||
|
|
||||||
|
🎯 **Doel:** De login flow optimaliseren voor een demo platform door Email + Wachtwoord de primaire methode te maken in plaats van Magic Link.
|
||||||
|
|
||||||
|
📘 **Toelichting:**
|
||||||
|
Het huidige login scherm heeft Magic Link als primaire methode, maar voor een **demo platform** is dit suboptimaal omdat prospects direct willen inloggen zonder email roundtrip. We maken Email + Password de primaire methode en verplaatsen Magic Link naar een secundaire optie.
|
||||||
|
|
||||||
|
**Huidige situatie:**
|
||||||
|
- Magic Link is default (toggle nodig voor demo login)
|
||||||
|
- Demo credentials verborgen achter extra click
|
||||||
|
- 2-3 extra clicks voor demo users
|
||||||
|
- Conditional rendering met `showDemoLogin` state
|
||||||
|
|
||||||
|
**Gewenste situatie:**
|
||||||
|
- Email + Password formulier als primair scherm
|
||||||
|
- One-click demo login prominent zichtbaar
|
||||||
|
- Magic Link als alternatieve optie onderaan
|
||||||
|
- Alle opties altijd zichtbaar (geen toggle)
|
||||||
|
|
||||||
|
**Context:**
|
||||||
|
- Dit is een DEMO platform, geen productie EPD
|
||||||
|
- Primaire use case: Prospects willen snel kijken
|
||||||
|
- Secundaire use case: Serieuze trial users maken eigen account
|
||||||
|
- Magic Link blijft beschikbaar voor zero-password signup
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Uitgangspunten
|
||||||
|
|
||||||
|
### 2.2 Projectkaders
|
||||||
|
|
||||||
|
- **Tijd:** 2-3 uur voor volledige implementatie + testing
|
||||||
|
- **Team:** 1 developer (zelfstandig uit te voeren)
|
||||||
|
- **Demo accounts:** Bestaande demo users blijven ongewijzigd
|
||||||
|
- **Breaking changes:** GEEN - Alle bestaande auth flows blijven werken
|
||||||
|
- **Deployment:** Auto-deploy via Vercel na git push
|
||||||
|
|
||||||
|
### 2.3 Programmeer Uitgangspunten
|
||||||
|
|
||||||
|
**Code Quality Principles:**
|
||||||
|
- ✅ **DRY:** Hergebruik bestaande auth functies (`loginWithPassword`, `loginWithMagicLink`)
|
||||||
|
- ✅ **KISS:** Eenvoudige layout refactor, geen complexe state management
|
||||||
|
- ✅ **SOC:** UI changes in page.tsx, auth logic blijft in lib/auth/client.ts
|
||||||
|
- ✅ **YAGNI:** Alleen login UI optimalisatie, geen extra conversion features
|
||||||
|
|
||||||
|
**Security:**
|
||||||
|
- Demo credentials blijven in info box (niet hardcoded in code)
|
||||||
|
- Bestaande Supabase auth flows blijven ongewijzigd
|
||||||
|
- Geen nieuwe environment variables nodig
|
||||||
|
|
||||||
|
**Bestaande Bestanden (NIET wijzigen):**
|
||||||
|
```
|
||||||
|
lib/auth/client.ts - Auth functies
|
||||||
|
lib/auth/server.ts - Server auth
|
||||||
|
middleware.ts - Route protection
|
||||||
|
app/auth/callback/route.ts - Magic link callback
|
||||||
|
app/auth/logout/route.ts - Logout handler
|
||||||
|
components/ui/button.tsx - UI components
|
||||||
|
```
|
||||||
|
|
||||||
|
**Te Wijzigen Bestanden:**
|
||||||
|
```
|
||||||
|
app/login/page.tsx - Volledige UI refactor
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Epics & Stories Overzicht
|
||||||
|
|
||||||
|
🎯 **Doel:** De bouw opdelen in logische epics (fases) met stories (subfases).
|
||||||
|
|
||||||
|
| Epic ID | Titel | Doel | Status | Stories | Geschatte Tijd |
|
||||||
|
|---------|-------|------|--------|---------|----------------|
|
||||||
|
| E1 | Login UI Refactor | Email+Password primair maken | ⏳ To Do | 3 | 1-2 uur |
|
||||||
|
| E2 | Demo UX Verbetering | One-click demo + betere copy | ⏳ To Do | 2 | 30 min |
|
||||||
|
| E3 | Testing & Verificatie | Alle flows testen | ⏳ To Do | 2 | 30 min |
|
||||||
|
|
||||||
|
**Totale schatting:** 2-3 uur werk
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Epics & Stories (Uitwerking)
|
||||||
|
|
||||||
|
### Epic 1 — Login UI Refactor
|
||||||
|
|
||||||
|
**Epic Doel:** Email + Password formulier wordt de primaire login methode zonder toggle logic.
|
||||||
|
|
||||||
|
| Story ID | Beschrijving | Acceptatiecriteria | Status | Afhankelijkheden | Story Points |
|
||||||
|
|----------|--------------|---------------------|--------|------------------|-----------------|
|
||||||
|
| E1.S1 | Verwijder conditional toggle logic | `showDemoLogin` state verwijderd, geen toggle buttons meer | ⏳ | — | 2 |
|
||||||
|
| E1.S2 | Herstructureer page layout | Email+Password bovenaan, Magic Link onderaan met divider | ⏳ | E1.S1 | 3 |
|
||||||
|
| E1.S3 | Update copy & labels | Primaire form heet "Login", Magic Link "Alternatieve optie" | ⏳ | E1.S2 | 1 |
|
||||||
|
|
||||||
|
**Technical Notes:**
|
||||||
|
|
||||||
|
**E1.S1 - Toggle Logic Verwijderen:**
|
||||||
|
```typescript
|
||||||
|
// VERWIJDER:
|
||||||
|
const [showDemoLogin, setShowDemoLogin] = useState(false)
|
||||||
|
|
||||||
|
// VERWIJDER buttons:
|
||||||
|
<button onClick={() => setShowDemoLogin(true)}>Login met Demo Account</button>
|
||||||
|
<button onClick={() => setShowDemoLogin(false)}>← Terug</button>
|
||||||
|
```
|
||||||
|
|
||||||
|
**E1.S2 - Layout Herstructureren:**
|
||||||
|
```
|
||||||
|
NIEUWE STRUCTUUR:
|
||||||
|
┌─────────────────────────────────────────┐
|
||||||
|
│ Header: "Login" │
|
||||||
|
├─────────────────────────────────────────┤
|
||||||
|
│ [QuickDemoButton - zie E2.S1] │
|
||||||
|
│ │
|
||||||
|
│ ─── of vul handmatig in ─── │
|
||||||
|
│ │
|
||||||
|
│ Email: [________________] │
|
||||||
|
│ Password: [________________] │
|
||||||
|
│ [Login Button] │
|
||||||
|
│ │
|
||||||
|
│ [Demo Credentials Info Box - E2.S2] │
|
||||||
|
│ │
|
||||||
|
│ ─────────── of ─────────── │
|
||||||
|
│ │
|
||||||
|
│ Gebruik Magic Link (geen wachtwoord) │
|
||||||
|
│ Email: [________________] │
|
||||||
|
│ [Stuur Magic Link] │
|
||||||
|
└─────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
**E1.S3 - Copy Updates:**
|
||||||
|
```typescript
|
||||||
|
// OUD → NIEUW
|
||||||
|
"🔑 Demo Account Login" → "Login"
|
||||||
|
"📧 Login met Magic Link" → "Of gebruik Magic Link"
|
||||||
|
"Login met Demo Account" → [VERWIJDERD - QuickDemoButton vervangt dit]
|
||||||
|
"Snelle Demo Login" → "🚀 Start Demo" (in QuickDemoButton)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Epic 2 — Demo UX Verbetering
|
||||||
|
|
||||||
|
**Epic Doel:** Demo gebruikers kunnen met één click inloggen zonder formulier in te vullen.
|
||||||
|
|
||||||
|
| Story ID | Beschrijving | Acceptatiecriteria | Status | Afhankelijkheden | Story Points |
|
||||||
|
|----------|--------------|---------------------|--------|------------------|-----------------|
|
||||||
|
| E2.S1 | Implementeer QuickDemoButton | One-click demo login zonder form invullen | ⏳ | E1.S2 | 3 |
|
||||||
|
| E2.S2 | Voeg credentials info box toe | Demo credentials zichtbaar voor manual login | ⏳ | E1.S2 | 2 |
|
||||||
|
|
||||||
|
**Technical Notes:**
|
||||||
|
|
||||||
|
**E2.S1 - QuickDemoButton Component:**
|
||||||
|
```typescript
|
||||||
|
// Inline component in app/login/page.tsx
|
||||||
|
// (of aparte component indien herbruikbaar elders)
|
||||||
|
|
||||||
|
function QuickDemoButton() {
|
||||||
|
const [loading, setLoading] = useState(false)
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
async function handleQuickDemo() {
|
||||||
|
setLoading(true)
|
||||||
|
try {
|
||||||
|
const result = await loginWithPassword(
|
||||||
|
'demo@mini-ecd.demo',
|
||||||
|
'Demo2024!'
|
||||||
|
)
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
setMessage({
|
||||||
|
type: 'success',
|
||||||
|
text: 'Ingelogd! Redirect naar EPD...'
|
||||||
|
})
|
||||||
|
setTimeout(() => router.push('/epd/clients'), 1000)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
setMessage({
|
||||||
|
type: 'error',
|
||||||
|
text: 'Login mislukt. Probeer opnieuw.'
|
||||||
|
})
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
size="lg"
|
||||||
|
className="w-full bg-teal-600 hover:bg-teal-700"
|
||||||
|
onClick={handleQuickDemo}
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
|
{loading ? (
|
||||||
|
<>
|
||||||
|
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||||
|
Demo laden...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
🚀 Start Demo (geen registratie)
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**E2.S2 - Credentials Info Box:**
|
||||||
|
```typescript
|
||||||
|
// Info card onder password form, boven Magic Link divider
|
||||||
|
|
||||||
|
<div className="rounded-lg bg-slate-50 border border-slate-200 p-4 space-y-2">
|
||||||
|
<p className="text-sm font-medium text-slate-700">
|
||||||
|
Demo Account Credentials:
|
||||||
|
</p>
|
||||||
|
<div className="text-xs text-slate-600 space-y-1">
|
||||||
|
<p>📧 Email: <code className="bg-white px-2 py-1 rounded">demo@mini-ecd.demo</code></p>
|
||||||
|
<p>🔒 Wachtwoord: <code className="bg-white px-2 py-1 rounded">Demo2024!</code></p>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-slate-500 italic">
|
||||||
|
💡 Of gebruik de "Start Demo" knop voor directe toegang
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Epic 3 — Testing & Verificatie
|
||||||
|
|
||||||
|
**Epic Doel:** Alle login flows werken correct na refactor zonder regressies.
|
||||||
|
|
||||||
|
| Story ID | Beschrijving | Acceptatiecriteria | Status | Afhankelijkheden | Story Points |
|
||||||
|
|----------|--------------|---------------------|--------|------------------|-----------------|
|
||||||
|
| E3.S1 | Test alle login flows | Alle 3 methoden werken zonder errors | ⏳ | E2.S2 | 2 |
|
||||||
|
| E3.S2 | Responsive & accessibility check | Werkt op mobile, keyboard navigatie OK | ⏳ | E3.S1 | 1 |
|
||||||
|
|
||||||
|
**Technical Notes:**
|
||||||
|
|
||||||
|
**E3.S1 - Login Flow Test Scenarios:**
|
||||||
|
|
||||||
|
| Test Case | Scenario | Expected Result | Status |
|
||||||
|
|-----------|----------|-----------------|--------|
|
||||||
|
| TC1 | Click "Start Demo" button | Direct inloggen → redirect /epd/clients | ⏳ |
|
||||||
|
| TC2 | Manual login (demo credentials) | Formulier submit → success → redirect | ⏳ |
|
||||||
|
| TC3 | Magic link (geldig email) | "Check je email" message → email ontvangen | ⏳ |
|
||||||
|
| TC4 | Invalid password | Error message: "Ongeldige inloggegevens" | ⏳ |
|
||||||
|
| TC5 | Empty fields | Validation error | ⏳ |
|
||||||
|
| TC6 | Network error | User-friendly error message | ⏳ |
|
||||||
|
|
||||||
|
**E3.S2 - Responsive & Accessibility Checklist:**
|
||||||
|
|
||||||
|
**Responsive:**
|
||||||
|
- [ ] 320px viewport (iPhone SE): Layout niet broken
|
||||||
|
- [ ] 768px viewport (iPad): Twee-kolom layout werkt
|
||||||
|
- [ ] 1920px viewport (Desktop): Maximale breedte begrensd
|
||||||
|
|
||||||
|
**Accessibility:**
|
||||||
|
- [ ] Tab-order logisch: QuickDemo → Email → Password → Login → MagicLink Email → Send
|
||||||
|
- [ ] Focus states zichtbaar (outline/ring)
|
||||||
|
- [ ] Error messages hebben `role="alert"`
|
||||||
|
- [ ] Buttons hebben duidelijke labels
|
||||||
|
- [ ] Contrast ratio > 4.5:1 (WCAG AA)
|
||||||
|
- [ ] Screen reader test: NVDA/VoiceOver leest alles voor
|
||||||
|
|
||||||
|
**Browser Testing:**
|
||||||
|
- [ ] Chrome (latest)
|
||||||
|
- [ ] Firefox (latest)
|
||||||
|
- [ ] Safari (macOS + iOS)
|
||||||
|
- [ ] Edge (latest)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Kwaliteit & Testplan
|
||||||
|
|
||||||
|
🎯 **Doel:** Vastleggen hoe de kwaliteit van de refactor wordt geborgd.
|
||||||
|
|
||||||
|
### Manual Test Checklist (voor deployment)
|
||||||
|
|
||||||
|
**Happy Flows:**
|
||||||
|
- [ ] Quick demo button: Click → Loading state → Success → Redirect /epd/clients
|
||||||
|
- [ ] Manual email+password: Type credentials → Submit → Redirect
|
||||||
|
- [ ] Magic link: Enter email → Submit → "Check email" message → Email ontvangen
|
||||||
|
|
||||||
|
**Error Flows:**
|
||||||
|
- [ ] Wrong password: Error message "Ongeldige inloggegevens"
|
||||||
|
- [ ] Invalid email format: Validation error
|
||||||
|
- [ ] Network timeout: User-friendly error
|
||||||
|
- [ ] Rate limit (4 emails/hour): Supabase error handled
|
||||||
|
|
||||||
|
**UI/UX:**
|
||||||
|
- [ ] Loading states show spinners
|
||||||
|
- [ ] Success messages turn teal-50 background
|
||||||
|
- [ ] Error messages turn red-50 background
|
||||||
|
- [ ] All text readable (contrast check)
|
||||||
|
- [ ] No console errors/warnings
|
||||||
|
- [ ] No layout shift during loading
|
||||||
|
|
||||||
|
**Regression Testing:**
|
||||||
|
- [ ] Existing demo accounts still work
|
||||||
|
- [ ] `/auth/callback` magic link flow unchanged
|
||||||
|
- [ ] Middleware still protects `/epd/*` routes
|
||||||
|
- [ ] Logout still works (`/auth/logout`)
|
||||||
|
- [ ] Session refresh in middleware works
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Risico's & Mitigatie
|
||||||
|
|
||||||
|
🎯 **Doel:** Risico's vroeg signaleren en voorzien van oplossingen.
|
||||||
|
|
||||||
|
| Risico | Kans | Impact | Mitigatie | Owner |
|
||||||
|
|--------|------|--------|-----------|-------|
|
||||||
|
| Breaking change in login flow | Laag | Hoog | - Behoud alle bestaande auth functies<br>- Test beide flows grondig<br>- Rollback plan ready | Developer |
|
||||||
|
| Demo credentials exposure | Middel | Laag | - Info box toont credentials (is OK voor demo)<br>- Geen hardcoded passwords in source<br>- RLS policies beschermen database | Developer |
|
||||||
|
| Magic link users verward | Middel | Laag | - Duidelijke "Of gebruik Magic Link" sectie<br>- Behoud alle bestaande UX voor magic link | Developer |
|
||||||
|
| Mobile layout breaks | Laag | Middel | - Test op 320px viewport<br>- Use responsive Tailwind classes<br>- Max-width container | Developer |
|
||||||
|
| Supabase rate limit tijdens testing | Hoog | Laag | - Use demo account voor testing (geen magic link)<br>- Test magic link max 1x per test run | Developer |
|
||||||
|
| Accessibility regression | Middel | Middel | - Tab-order testing<br>- Screen reader check<br>- WCAG contrast check | Developer |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Definition of Done
|
||||||
|
|
||||||
|
**Epic 1-3 zijn compleet wanneer:**
|
||||||
|
|
||||||
|
✅ **Functional Requirements:**
|
||||||
|
- Email + Password is primair formulier (bovenaan pagina)
|
||||||
|
- Quick demo button werkt (one-click login)
|
||||||
|
- Magic Link optie blijft beschikbaar (onderaan)
|
||||||
|
- Demo credentials info box zichtbaar
|
||||||
|
- Alle 3 login methoden getest en werkend
|
||||||
|
|
||||||
|
✅ **Quality Requirements:**
|
||||||
|
- Geen console errors/warnings
|
||||||
|
- Mobile responsive (320px - 1920px)
|
||||||
|
- Accessible (keyboard nav + screen reader)
|
||||||
|
- Loading states correct
|
||||||
|
- Error messages user-friendly
|
||||||
|
|
||||||
|
✅ **Code Quality:**
|
||||||
|
- Bestaande auth functies ongewijzigd
|
||||||
|
- Clean code (geen commented code)
|
||||||
|
- Consistent Tailwind styling
|
||||||
|
- Type-safe (TypeScript errors = 0)
|
||||||
|
|
||||||
|
✅ **Documentation:**
|
||||||
|
- Git commit message: `feat: Login UI refactor - Email+Password primair`
|
||||||
|
- Code comments voor complexe logica
|
||||||
|
- Dit bouwplan bijgewerkt met "✅ Gereed" status
|
||||||
|
|
||||||
|
✅ **Deployment:**
|
||||||
|
- Lokaal getest (npm run dev)
|
||||||
|
- Git commit + push
|
||||||
|
- Vercel auto-deploy succesvol
|
||||||
|
- Production smoke test uitgevoerd
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Implementatie Volgorde
|
||||||
|
|
||||||
|
**Aanbevolen volgorde:**
|
||||||
|
|
||||||
|
1. **E1.S1** - Verwijder toggle logic (15 min)
|
||||||
|
- Clean up `showDemoLogin` state
|
||||||
|
- Verwijder toggle buttons
|
||||||
|
|
||||||
|
2. **E1.S2** - Herstructureer layout (30 min)
|
||||||
|
- Email+Password form bovenaan
|
||||||
|
- Dividers toevoegen
|
||||||
|
- Magic Link onderaan
|
||||||
|
|
||||||
|
3. **E1.S3** - Update copy (10 min)
|
||||||
|
- Alle labels updaten
|
||||||
|
- Verwarrende tekst verwijderen
|
||||||
|
|
||||||
|
4. **E2.S1** - QuickDemoButton (20 min)
|
||||||
|
- Component implementeren
|
||||||
|
- Loading states
|
||||||
|
- Error handling
|
||||||
|
|
||||||
|
5. **E2.S2** - Credentials info box (10 min)
|
||||||
|
- Styled info card
|
||||||
|
- Demo credentials display
|
||||||
|
|
||||||
|
6. **E3.S1** - Test alle flows (20 min)
|
||||||
|
- Happy flows
|
||||||
|
- Error flows
|
||||||
|
- Regression tests
|
||||||
|
|
||||||
|
7. **E3.S2** - Responsive + A11y (15 min)
|
||||||
|
- Mobile viewport test
|
||||||
|
- Keyboard navigation
|
||||||
|
- Screen reader check
|
||||||
|
|
||||||
|
**Total: ~2 uur**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Referenties
|
||||||
|
|
||||||
|
**Mission Control Documents:**
|
||||||
|
- **PRD:** `docs/specs/prd-mini-ecd-v1.2.md`
|
||||||
|
- **FO:** `docs/specs/fo-mini-ecd-v2.md`
|
||||||
|
- **TO:** `docs/specs/to-mini-ecd-v1_2.md`
|
||||||
|
- **Auth Setup:** `docs/AUTH_SETUP.md`
|
||||||
|
|
||||||
|
**Code References:**
|
||||||
|
- Login page: `app/login/page.tsx` (TE WIJZIGEN)
|
||||||
|
- Auth client: `lib/auth/client.ts` (ONGEWIJZIGD)
|
||||||
|
- Auth server: `lib/auth/server.ts` (ONGEWIJZIGD)
|
||||||
|
- Middleware: `middleware.ts` (ONGEWIJZIGD)
|
||||||
|
|
||||||
|
**External Resources:**
|
||||||
|
- Repository: `https://github.com/[org]/15-mini-epd-prototype`
|
||||||
|
- Deployment: Vercel (auto-deploy on push)
|
||||||
|
- Supabase Project: `dqugbrpwtisgyxscpefg` (EU region)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. Versiehistorie
|
||||||
|
|
||||||
|
| Versie | Datum | Auteur | Wijziging |
|
||||||
|
|--------|-------|--------|-----------|
|
||||||
|
| v1.0 | 18-01-2025 | Colin | Initiële versie - Login refactor bouwplan |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Status:** ⏳ Ready for Implementation
|
||||||
|
**Next Steps:** Start met E1.S1 (toggle logic verwijderen)
|
||||||
BIN
public/images/colin-office-6.webp
Normal file
BIN
public/images/colin-office-6.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 133 KiB |
Reference in New Issue
Block a user