diff --git a/app/(marketing)/components/comparison-table.tsx b/app/(marketing)/components/comparison-table.tsx new file mode 100644 index 0000000..3ca2278 --- /dev/null +++ b/app/(marketing)/components/comparison-table.tsx @@ -0,0 +1,104 @@ +/** + * Comparison Table Component + * + * Visual comparison between Traditional and AI Speedrun approaches. + * Responsive: table on desktop, stacked cards on mobile. + */ + +import type { ComparisonContent } from '@/content/schemas/manifesto' + +interface ComparisonTableProps { + content: ComparisonContent +} + +export function ComparisonTable({ content }: ComparisonTableProps) { + return ( +
+

+ {content.heading} +

+ + {/* Desktop: Table view */} +
+ + + + + + + + + + {content.items.map((item, index) => ( + + + + + + ))} + +
+ Aspect + + Traditioneel + + AI Speedrun +
+ {item.label} + + {item.traditional} + + {item.aiSpeedrun} +
+
+ + {/* Mobile: Stacked cards */} +
+ {content.items.map((item) => ( +
+

+ {item.label} +

+
+
+
Traditioneel:
+
+ {item.traditional} +
+
+
+
+ AI Speedrun: +
+
+ {item.aiSpeedrun} +
+
+
+
+ ))} +
+
+ ) +} + diff --git a/app/(marketing)/components/experiment-cta.tsx b/app/(marketing)/components/experiment-cta.tsx new file mode 100644 index 0000000..e86ffd6 --- /dev/null +++ b/app/(marketing)/components/experiment-cta.tsx @@ -0,0 +1,56 @@ +/** + * Experiment CTA Component + * + * Call-to-action section at the end of the manifesto. + * Subtle buttons, no aggressive colors or fake urgency. + * Follows design specs: large typography, centered, minimal styling. + */ + +import Link from 'next/link' +import type { CTAContent } from '@/content/schemas/manifesto' + +interface ExperimentCTAProps { + content: CTAContent +} + +export function ExperimentCTA({ content }: ExperimentCTAProps) { + return ( +
+
+

+ {content.heading} +

+ + {content.subheading && ( +

+ {content.subheading} +

+ )} + +
+ {/* Primary Button - Subtle dark background */} + + {content.primaryButton.text} + + + {/* Secondary Button - Subtle outline */} + + {content.secondaryButton.text} + +
+
+
+ ) +} + diff --git a/app/(marketing)/components/hero-quote.tsx b/app/(marketing)/components/hero-quote.tsx new file mode 100644 index 0000000..45a913c --- /dev/null +++ b/app/(marketing)/components/hero-quote.tsx @@ -0,0 +1,59 @@ +/** + * Hero Quote Component + * + * Full-viewport hero section with Jensen Huang quote. + * Displays quote, attribution, and subtitle from content. + */ + +import type { HeroContent } from '@/content/schemas/manifesto' +import { MarketingShader } from './marketing-shader' + +interface HeroQuoteProps { + content: HeroContent +} + +export function HeroQuote({ content }: HeroQuoteProps) { + return ( +
+ {/* Shader Background - zeer subtiel (opacity 0.02) */} + {/* Op mobile: donkere fallback, op desktop: subtiele shader */} + + + {/* Content Overlay */} +
+
+ "{content.quote}" +
+ +
+ + {content.attribution} + + {content.attributionContext && ( + + )} +
+ +

+ {content.subtitle} +

+
+
+ ) +} + diff --git a/app/(marketing)/components/insight-box.tsx b/app/(marketing)/components/insight-box.tsx new file mode 100644 index 0000000..4a58ceb --- /dev/null +++ b/app/(marketing)/components/insight-box.tsx @@ -0,0 +1,32 @@ +/** + * Insight Box Component + * + * Visual anchor for key takeaways with yellow left border. + * Used to highlight important insights in the manifesto content. + */ + +interface InsightBoxProps { + children: React.ReactNode + variant?: 'default' | 'highlight' +} + +export function InsightBox({ children, variant = 'default' }: InsightBoxProps) { + const borderColor = variant === 'highlight' ? '#D97706' : '#F59E0B' + + return ( +
+

+ {children} +

+
+ ) +} + diff --git a/app/(marketing)/components/manifesto-content.tsx b/app/(marketing)/components/manifesto-content.tsx new file mode 100644 index 0000000..46fe9af --- /dev/null +++ b/app/(marketing)/components/manifesto-content.tsx @@ -0,0 +1,95 @@ +/** + * Manifesto Content Component + * + * Long-form reading experience with manifesto text. + * Renders paragraphs with proper typography for optimal reading. + */ + +import type { ManifestoSection } from '@/content/schemas/manifesto' +import { InsightBox } from './insight-box' +import { StatementSection } from './statement-section' + +interface ManifestoContentProps { + sections: ManifestoSection[] +} + +export function ManifestoContent({ sections }: ManifestoContentProps) { + // Group consecutive non-statement sections into article blocks + const blocks: Array<{ start: number; end: number }> = [] + let blockStart = 0 + + sections.forEach((section, index) => { + if (section.type === 'statement') { + if (blockStart < index) { + blocks.push({ start: blockStart, end: index - 1 }) + } + blockStart = index + 1 + } + }) + + // Add final block if needed + if (blockStart < sections.length) { + blocks.push({ start: blockStart, end: sections.length - 1 }) + } + + let blockIndex = 0 + + return ( + <> + {sections.map((section, index) => { + // Statement sections are full-width + if (section.type === 'statement') { + return + } + + // Check if this is the start of a new article block + const currentBlock = blocks[blockIndex] + const isBlockStart = currentBlock && index === currentBlock.start + + if (isBlockStart) { + const blockSections = sections.slice(currentBlock.start, currentBlock.end + 1) + blockIndex++ + + return ( +
+
+ {blockSections.map((blockSection) => { + if (blockSection.type === 'paragraph') { + return ( +

+ {blockSection.content} +

+ ) + } + + if (blockSection.type === 'insight') { + return ( + + {blockSection.content} + + ) + } + + return null + })} +
+
+ ) + } + + return null + })} + + ) +} + diff --git a/app/(marketing)/components/marketing-shader.tsx b/app/(marketing)/components/marketing-shader.tsx new file mode 100644 index 0000000..66c74fa --- /dev/null +++ b/app/(marketing)/components/marketing-shader.tsx @@ -0,0 +1,83 @@ +'use client' + +/** + * Marketing Shader Component + * + * Wrapper around DotScreenShader for marketing pages. + * Very subtle opacity (0.02) for hero sections. + * Includes mobile fallback and reduced motion support. + */ + +import dynamic from 'next/dynamic' +import { useEffect, useState } from 'react' + +// Lazy load the shader component (heavy, only load when needed) +const DotScreenShader = dynamic( + () => import('@/components/ui/dot-shader-background').then((mod) => ({ default: mod.DotScreenShader })), + { + ssr: false, + loading: () => null + } +) + +interface MarketingShaderProps { + variant?: 'hero' | 'section' + className?: string +} + +export function MarketingShader({ variant = 'hero', className = '' }: MarketingShaderProps) { + const [mounted, setMounted] = useState(false) + const [reducedMotion, setReducedMotion] = useState(false) + const [isMobile, setIsMobile] = useState(false) + + useEffect(() => { + setMounted(true) + + // Check for reduced motion preference + const mediaQuery = window.matchMedia('(prefers-reduced-motion: reduce)') + setReducedMotion(mediaQuery.matches) + + const handleChange = (e: MediaQueryListEvent) => setReducedMotion(e.matches) + mediaQuery.addEventListener('change', handleChange) + + // Check for mobile (simple check) + const checkMobile = () => { + setIsMobile(window.innerWidth < 768) + } + checkMobile() + window.addEventListener('resize', checkMobile) + + return () => { + mediaQuery.removeEventListener('change', handleChange) + window.removeEventListener('resize', checkMobile) + } + }, []) + + // Fallback voor mobile of reduced motion + // Voor hero variant: donkere achtergrond, voor section variant: lichte achtergrond + if (!mounted || reducedMotion || isMobile) { + const fallbackBg = variant === 'hero' + ? 'bg-slate-900' // Donkere achtergrond voor hero + : 'bg-gradient-to-br from-slate-50 to-slate-100' // Lichte achtergrond voor sections + + return ( +