skills/website/phases/03-sections.md

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.

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)
  • MagneticButton lives in src/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/dynamic in 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 alt text
  • 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-label and aria-expanded