252 lines
7.5 KiB
Markdown
252 lines
7.5 KiB
Markdown
# Phase 3 — Full Premium Site
|
|
|
|
Build all sections using Next.js App Router, shadcn/ui, and brand tokens from Phase 1.
|
|
|
|
## Project Setup
|
|
|
|
```bash
|
|
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`:
|
|
```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:
|
|
```css
|
|
: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'`:
|
|
```tsx
|
|
// 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.
|
|
|
|
```tsx
|
|
// 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:
|
|
```css
|
|
.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):
|
|
```ts
|
|
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.
|
|
|
|
```tsx
|
|
// CSS-only infinite marquee (no JS animation loop — better perf)
|
|
// Use transform: translateX animation
|
|
// Pause on hover via animation-play-state: paused
|
|
```
|
|
|
|
CSS:
|
|
```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`):
|
|
```tsx
|
|
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`:
|
|
```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`
|