7.5 KiB
Phase 3 — Full Premium Site
Build all sections using Next.js App Router, shadcn/ui, and brand tokens from Phase 1.
Project Setup
npx create-next-app@latest <brand.name>-site --typescript --tailwind --app --src-dir
cd <brand.name>-site
npm install gsap @gsap/react framer-motion lenis
npx shadcn@latest init -d
npx shadcn@latest add button card input textarea badge separator
Global Layout
src/app/layout.tsx:
import { Geist, Geist_Mono } from 'next/font/google'
import { GSAPProvider } from '@/components/gsap-provider'
import { brand } from '@/lib/brand'
import './globals.css'
const geist = Geist({ subsets: ['latin'], variable: '--font-geist' })
const geistMono = Geist_Mono({ subsets: ['latin'], variable: '--font-geist-mono' })
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en" className={`${geist.variable} ${geistMono.variable}`}>
<body style={{
'--color-primary': brand.colors.primary,
'--color-secondary': brand.colors.secondary,
'--color-accent': brand.colors.accent,
'--color-bg': brand.colors.bg,
} as React.CSSProperties}>
<GSAPProvider>{children}</GSAPProvider>
</body>
</html>
)
}
src/app/globals.css — add after Tailwind directives:
:root {
--color-primary: #18181B;
--color-secondary: #3F3F46;
--color-accent: #6366F1;
--color-bg: #FAFAFA;
}
body {
background-color: var(--color-bg);
color: var(--color-secondary);
font-family: var(--font-geist), system-ui, sans-serif;
}
Navigation — Floating Glass Pill
src/components/Nav.tsx — 'use client':
// Floating pill navbar detached from top
// Closed: glass pill with logo + links + CTA
// Mobile: hamburger morphs to X, full-screen overlay with staggered link reveals
// Uses backdrop-blur ONLY because it is position:fixed (sticky element rule)
Key CSS: position: fixed, top: 1.5rem, left: 50%, transform: translateX(-50%),
backdrop-blur-xl, bg-white/80 dark:bg-black/80, rounded-full, px-6 py-3
Hamburger morph: two <span> bars, on open:
- Bar 1:
rotate-45 translate-y-[7px] - Bar 2:
-rotate-45 -translate-y-[7px]Transition:duration-300 ease-[cubic-bezier(0.32,0.72,0,1)]
Services Section — 2-Col Zig-Zag Bento
src/components/ServicesSection.tsx:
Never use 3 equal columns. Use alternating 2-col layout where content and visual swap sides.
// Each service = one "row" that alternates layout
// Even rows: [content left (60%) | visual right (40%)]
// Odd rows: [visual left (40%) | content right (60%)]
// On mobile: always stack, content first
// scroll reveal with IntersectionObserver (never window.scroll)
useEffect(() => {
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
entry.target.classList.add('revealed')
observer.unobserve(entry.target)
}
})
},
{ threshold: 0.15 }
)
document.querySelectorAll('.service-row').forEach((el) => observer.observe(el))
return () => observer.disconnect()
}, [])
CSS for reveal:
.service-row {
opacity: 0;
transform: translateY(40px);
transition: opacity 0.7s cubic-bezier(0.32,0.72,0,1), transform 0.7s cubic-bezier(0.32,0.72,0,1);
}
.service-row.revealed {
opacity: 1;
transform: translateY(0);
}
Stagger via CSS transition-delay: calc(var(--index) * 0.1s)
About Section — Editorial Split with Parallax
src/components/AboutSection.tsx:
Layout: massive typography on left, parallax image stack on right.
- Left: large serif-style stat numbers, short paragraph about the business
- Right: 2-3 overlapping images with slight rotation (
-2deg,1deg,3deg) that straighten on hover - On mobile: remove rotations and overlaps, stack vertically
Image rotation on mount (GSAP):
gsap.set('.about-img', { rotation: (i) => [-2, 1, 3][i] || 0 })
// On hover: straighten
// On leave: restore rotation
Testimonials Section — Infinite Marquee
src/components/TestimonialsSection.tsx — 'use client':
Duplicate the testimonial cards array to create a seamless loop.
// CSS-only infinite marquee (no JS animation loop — better perf)
// Use transform: translateX animation
// Pause on hover via animation-play-state: paused
CSS:
.marquee-track {
display: flex;
width: max-content;
animation: marquee 30s linear infinite;
}
.marquee-track:hover {
animation-play-state: paused;
}
@keyframes marquee {
from { transform: translateX(0) }
to { transform: translateX(-50%) }
}
Testimonial card: white bg, whisper border (border border-zinc-200/50), rounded-3xl, p-8.
Avatar: squircle (not circle) — rounded-2xl on w-10 h-10.
Use organic-looking names and messy numbers (e.g., "47% faster", "+$2.3k/mo").
Contact Section — Form with Validation
src/components/ContactSection.tsx — 'use client':
Fields: Name, Email, Message (textarea), Submit button.
Validation (inline, no window.alert):
const [errors, setErrors] = useState<Record<string, string>>({})
const validate = () => {
const errs: Record<string, string> = {}
if (!name.trim()) errs.name = 'Name is required'
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) errs.email = 'Enter a valid email'
if (message.trim().length < 20) errs.message = 'Message must be at least 20 characters'
return errs
}
Error display: red text below each input, text-sm text-red-500.
Success state: replace form with a composed "Thank you" panel (not a toast or alert).
Submit: useTransition for pending state — button shows "Sending..." during submission.
Footer Section
src/components/FooterSection.tsx:
Clean, minimal. No "link farm" with 4 columns.
Structure:
[Logo + tagline] [Nav links: 4 max] [Legal: Privacy · Terms]
Copyright © {year} {brand.name}
Page Assembly
src/app/page.tsx:
import Nav from '@/components/Nav'
import HeroSection from '@/components/HeroSection'
import ServicesSection from '@/components/ServicesSection'
import AboutSection from '@/components/AboutSection'
import TestimonialsSection from '@/components/TestimonialsSection'
import ContactSection from '@/components/ContactSection'
import FooterSection from '@/components/FooterSection'
export default function Home() {
return (
<main>
<Nav />
<HeroSection />
<ServicesSection />
<AboutSection />
<TestimonialsSection />
<ContactSection />
<FooterSection />
</main>
)
}
Component File Rules
- Each section = its own file in
src/components/ - Never define components inside other components (
rerender-no-inline-components) MagneticButtonlives insrc/components/ui/magnetic-button.tsx- GSAP provider lives in
src/components/gsap-provider.tsx - Brand tokens live in
src/lib/brand.ts - Heavy libraries (GSAP): dynamically import via
next/dynamicin page-level components where possible (bundle-dynamic-imports) - All interactive components have
'use client'at top - Server Components are default — only add
'use client'for interactivity or browser APIs
Accessibility Checklist
- All images have meaningful
alttext - Skip-to-content link in Nav
- Focus rings visible on all interactive elements
- Form fields have associated
<label>elements - Marquee pauses on hover AND
prefers-reduced-motion: reduce - Color contrast ≥ 4.5:1 for body text
- Hamburger button has
aria-labelandaria-expanded