313 lines
9.3 KiB
Markdown
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)
|