313 lines
9.3 KiB
Markdown

# Component Patterns
## Hydration Strategy
| Directive | When to Use | Components |
|-----------|-------------|------------|
| `client:load` | Above-fold, needs immediate interaction | Navigation, Hero, Loader, CustomCursor |
| `client:visible` | Below-fold, can wait until scrolled into view | About, Contact, Footer, GamesList, Marquee |
| `client:idle` | Low priority, background loading | BlogSearch |
## GSAP Animation Patterns
### Required Setup in Every Animated Component
```typescript
import { useEffect, useRef } from 'react';
import gsap from 'gsap';
import { ScrollTrigger } from 'gsap/ScrollTrigger';
gsap.registerPlugin(ScrollTrigger);
export default function AnimatedComponent() {
const sectionRef = useRef<HTMLElement>(null);
useEffect(() => {
// Always check reduced motion
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return;
// Always wrap in context for cleanup
const ctx = gsap.context(() => {
// animations here
});
return () => ctx.revert();
}, []);
}
```
### SessionStorage One-Time Entrance
For Hero and Loader — skip expensive animations on repeat visits:
```typescript
const [isVisible, setIsVisible] = useState(() => {
// Synchronous check before first render
if (typeof window === 'undefined') return true;
return !sessionStorage.getItem('{{STORAGE_KEY}}');
});
// In animation completion callback:
sessionStorage.setItem('{{STORAGE_KEY}}', 'true');
setIsVisible(false);
```
### ScrollTrigger Entrance Pattern
For below-fold sections (About, Contact, GamesList):
```typescript
gsap.context(() => {
const tl = gsap.timeline({
scrollTrigger: {
trigger: sectionRef.current,
start: 'top 80%',
toggleActions: 'play none none none',
},
});
tl.from(leftRef.current, {
x: -50, opacity: 0, duration: 0.8, ease: 'power2.out',
})
.from(rightRef.current, {
x: 50, opacity: 0, duration: 0.8, ease: 'power2.out',
}, '-=0.6'); // Overlap by 0.6s for flowing feel
});
```
### Parallax Pattern
```typescript
gsap.to(bgRef.current, {
yPercent: 20,
ease: 'none',
scrollTrigger: {
trigger: sectionRef.current,
start: 'top bottom',
end: 'bottom top',
scrub: true,
},
});
```
### Idle Float Animation
```typescript
gsap.to(elementRef.current, {
y: 12,
rotation: 1.5,
duration: 3,
ease: 'sine.inOut',
yoyo: true,
repeat: -1,
});
```
### Animation Rules
- **Transform-only**: Use x, y, scale, rotation, opacity. Never animate width, height, top, left, margin, padding.
- **Stagger**: Use negative relative positioning (`'-=0.6'`) in timelines for overlapping reveals.
- **Ease functions**: `power2.out` for entrances, `power3.inOut` for exits, `sine.inOut` for idle, `back.out(1.7)` for bouncy CTAs.
## Navigation Component
Fixed header + fullscreen overlay menu:
**Header Bar:**
- Fixed position, z-50
- Transparent when at top, semi-opaque after scroll (useEffect with scroll listener)
- Logo (left), optional center ticker, controls (right: audio toggle, menu button)
- Top accent line: `h-[2px]` gradient with brand colors
**Fullscreen Menu:**
- Hidden by default (`display: none`), shown via GSAP
- Background: layered gradients, noise texture, scanlines
- Two-column: nav links (left) + info (right)
- Nav links: indexed (01-06), description on hover (hidden mobile)
- GSAP open sequence: scanline wipe → bg fade → links stagger → info slide
- GSAP close: all fade 0.15s → bg fade 0.2s → `display: none`
**Interactions:**
- ESC to close (KeyboardEvent listener)
- Body overflow hidden when open
- Hash links scroll with GSAP ScrollToPlugin
- Audio SFX via Web Audio API (optional — synthesized, no files needed)
- 44px minimum touch targets on all links/buttons
## Hero Component
Full viewport section:
**Background Layers (bottom to top):**
1. Video/image (opacity 50%, object-cover)
2. Gradient overlays (bottom-to-top dark, radial center-fade)
3. Noise texture (mix-blend-overlay, opacity 3-5%)
4. Scanlines (optional)
**Content:**
- Centered: logo, tagline, 2 CTA buttons, optional stats ticker
- Floating decorative elements (characters, icons) with idle float animations
- Diagonal accent slash (optional, brand colored)
**Entrance Animation (sessionStorage skip on repeat):**
```
0.0s: Start
0.3s: Slash scaleX 0→1 (0.6s)
0.5s: Logo blur(20px) x:-120 → clear x:0 (1.0s)
0.7s: Character blur → clear (0.9s)
1.0s: Tagline clipPath reveal (0.7s)
1.2s: CTAs scale+y stagger (0.6s each, 0.12s gap)
1.5s: Ticker fade up (0.6s)
```
**Parallax (always active):**
- Video: yPercent +20 on scroll
- Character: yPercent -30 on scroll
## Loader Component (optional)
One-time splash screen:
```typescript
const [isVisible, setIsVisible] = useState(() => {
if (typeof window === 'undefined') return true;
return !sessionStorage.getItem('{{KEY}}-loader-seen');
});
if (!isVisible) return null;
```
- Progress bar: width 0→100% (1.5s)
- Logo: opacity 0, scale 1.1, blur 10px → visible (0.5s)
- Container: yPercent 0→-100 (0.8s, power3.inOut)
- Safety timeout: 5s max
- Click anywhere to skip
- On complete: `sessionStorage.setItem`, `setIsVisible(false)`
## CustomCursor Component (optional)
```typescript
export default function CustomCursor() {
const cursorRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (window.matchMedia('(pointer: coarse)').matches) return;
const onMove = (e: MouseEvent) => {
gsap.to(cursorRef.current, {
x: e.clientX, y: e.clientY,
duration: 0.1, ease: 'power2.out',
});
};
window.addEventListener('mousemove', onMove);
return () => window.removeEventListener('mousemove', onMove);
}, []);
return <div ref={cursorRef} id="custom-cursor" />;
}
```
Styled via global.css `#custom-cursor` rules.
## Contact Component
Two-column layout:
**Left Column:**
- Section label (accent color, pixel font)
- Heading with gradient text on key word
- Contact email link
- Social icons row with hover color transitions
- Optional location tagline
**Right Column (Form):**
- Fields: name, email, subject, message
- Honeypot: `_honey` (hidden, aria-hidden, tabIndex -1)
- Submit button: brand bg, white text, pixel shadow, hover translate 2px
- Status states: idle → sending (disabled) → success (cyan border box) → error (primary border box)
**GSAP entrance**: ScrollTrigger, left x:-50, right x:+50, staggered.
## Footer Component
Multi-column grid:
```
┌─────────────────────────────────────────────────────────┐
│ Brand │ SITEMAP │ LEGAL │ CTA │
│ Logo │ Home │ Privacy │ Steam │
│ Name │ Product │ Terms │ Button │
│ Tagline │ About │ Press │ │
│ Socials │ Blog │ │ │
│ │ Contact │ │ │
├─────────────────────────────────────────────────────────┤
│ © 2024 Company. Est. 20XX. │
└─────────────────────────────────────────────────────────┘
```
- Background: `--color-darker` (#050505)
- Text: white/35 base, white on hover, 200ms transition
- Grid: 1 col mobile → 2 cols tablet → 4 cols desktop
## External API Integration Pattern
Build-time data fetching (e.g., Steam API):
```typescript
// src/lib/steam.ts
interface SteamData {
app: SteamAppDetails | null;
reviews: SteamReviewSummary | null;
players: number | null;
news: SteamNewsItem[];
version: string | null;
}
async function fetchJson<T>(url: string): Promise<T | null> {
try {
const res = await fetch(url);
if (!res.ok) return null;
return await res.json() as T;
} catch {
return null;
}
}
export async function fetchAllData(id: number): Promise<SteamData> {
const [app, reviews, players, news] = await Promise.all([
fetchAppDetails(id),
fetchReviewSummary(id),
fetchCurrentPlayers(id),
fetchNews(id),
]);
return { app, reviews, players, news, version: parseVersionFromNews(news) };
}
```
Key principles:
- Every fetch returns `T | null` — never throws
- Use `Promise.all` for parallel fetching
- Aggregate into single data object with nullable fields
- Builds never break if external API is down
- Wire into pages via frontmatter `await`
## Accessibility Checklist
Every component should have:
- `aria-label` on icon-only buttons
- 44px minimum touch targets (w-11 h-11, or py-3 px-3 -mx-3)
- Keyboard navigation (ESC to close overlays, Tab order)
- `focus-visible` outline styles
- Semantic HTML (`<nav>`, `<article>`, `<footer>`, `<section id="...">`)
- Color contrast meeting WCAG AA on dark backgrounds
## Mobile Patterns
- Touch feedback: `:active` scale 0.985, 80ms transition
- Image bleed to edges on mobile (`-mx-5`, `max-width: calc(100% + 2.5rem)`)
- Horizontal scroll with `overflow-x-auto scrollbar-none` for category strips
- Safe area: `padding-bottom: calc(5rem + env(safe-area-inset-bottom))`
- `pointer: coarse` media query to hide desktop-only features (custom cursor, hover descriptions)