Skip to content

Commit ee44b79

Browse files
committed
chore: add test CI, doctor script, and documentation
Phase 3 - DX & Testing: - Add GitHub Actions workflow for running tests on PRs - Add `doctor` script to diagnose common setup issues - Checks Node.js version, Bun, dependencies, env files, configs - Run with: bun run doctor Phase 4 - Documentation: - Add components/effects/README.md (GSAP, SplitText, AnimatedGradient) - Add lib/features/README.md (OptionalFeatures pattern) - Add app/api/README.md (draft-mode, revalidate endpoints)
1 parent a17e6c0 commit ee44b79

File tree

6 files changed

+385
-0
lines changed

6 files changed

+385
-0
lines changed

.github/workflows/test.yml

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
name: Test
2+
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
branches: [main]
8+
9+
jobs:
10+
test:
11+
runs-on: ubuntu-latest
12+
13+
steps:
14+
- uses: actions/checkout@v4
15+
16+
- uses: oven-sh/setup-bun@v2
17+
with:
18+
bun-version: latest
19+
20+
- name: Install dependencies
21+
run: bun install --frozen-lockfile
22+
23+
- name: Run checks
24+
run: bun run check

app/api/README.md

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
# API Routes
2+
3+
Server-side API endpoints for integrations and webhooks.
4+
5+
## Endpoints
6+
7+
| Route | Method | Purpose |
8+
|-------|--------|---------|
9+
| `/api/draft-mode/enable` | GET | Enable Sanity draft mode |
10+
| `/api/draft-mode/disable` | GET | Disable Sanity draft mode |
11+
| `/api/revalidate` | POST | Webhook for content revalidation |
12+
13+
## Draft Mode
14+
15+
Used by Sanity Visual Editing to preview unpublished content.
16+
17+
### Enable Draft Mode
18+
19+
```
20+
GET /api/draft-mode/enable?slug=/page-slug
21+
```
22+
23+
Redirects to the page with draft mode cookies set.
24+
25+
### Disable Draft Mode
26+
27+
```
28+
GET /api/draft-mode/disable
29+
```
30+
31+
Clears draft mode cookies and redirects to homepage.
32+
33+
## Revalidation Webhook
34+
35+
Receives webhooks from Sanity/Shopify to revalidate cached content.
36+
37+
```
38+
POST /api/revalidate
39+
```
40+
41+
### Sanity Webhook Setup
42+
43+
1. Go to Sanity project settings → API → Webhooks
44+
2. Create webhook with URL: `https://your-domain.com/api/revalidate`
45+
3. Set secret in environment:
46+
47+
```bash
48+
# .env.local
49+
SANITY_REVALIDATE_SECRET=your-secret-here
50+
```
51+
52+
### Shopify Webhook Setup
53+
54+
1. Shopify Admin → Settings → Notifications → Webhooks
55+
2. Add webhook for product/collection updates
56+
3. URL: `https://your-domain.com/api/revalidate?secret=YOUR_SECRET`
57+
58+
```bash
59+
# .env.local
60+
SHOPIFY_REVALIDATION_SECRET=your-secret-here
61+
```
62+
63+
## Security
64+
65+
- Webhooks require a secret token for authentication
66+
- Rate limiting is applied to prevent abuse
67+
- Invalid requests return 200 to prevent retry loops (standard webhook practice)
68+
69+
## Adding New Endpoints
70+
71+
Create a new route file:
72+
73+
```tsx
74+
// app/api/my-endpoint/route.ts
75+
import { NextResponse } from 'next/server'
76+
77+
export async function POST(request: Request) {
78+
// Handle request
79+
return NextResponse.json({ success: true })
80+
}
81+
```

components/effects/README.md

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
# Effect Components
2+
3+
Animation and visual effect components built with GSAP.
4+
5+
## Components
6+
7+
| Component | Purpose |
8+
|-----------|---------|
9+
| `gsap.tsx` | GSAP Runtime - syncs GSAP ticker with Tempus RAF |
10+
| `animated-gradient/` | WebGL animated gradient background |
11+
| `split-text/` | Text splitting for character/word/line animations |
12+
| `progress-text/` | Scroll-based text reveal animation |
13+
14+
## GSAP Runtime
15+
16+
Automatically included via `OptionalFeatures` in the root layout. Ensures GSAP animations sync with Tempus for consistent frame timing across all animations.
17+
18+
```tsx
19+
// Already loaded - no manual import needed
20+
// GSAP will use Tempus RAF automatically
21+
```
22+
23+
## Split Text
24+
25+
Split text into characters, words, or lines for staggered animations.
26+
27+
```tsx
28+
import { SplitText } from '@/components/effects/split-text'
29+
30+
<SplitText type="chars" className="text-4xl">
31+
Animate each character
32+
</SplitText>
33+
34+
<SplitText type="words" className="text-2xl">
35+
Animate each word separately
36+
</SplitText>
37+
38+
<SplitText type="lines" className="text-xl">
39+
Animate line by line
40+
for multiline text
41+
</SplitText>
42+
```
43+
44+
## Animated Gradient
45+
46+
WebGL-based animated gradient background. Requires WebGL to be enabled.
47+
48+
```tsx
49+
import { AnimatedGradient } from '@/components/effects/animated-gradient'
50+
51+
<AnimatedGradient
52+
colors={['#ff0000', '#00ff00', '#0000ff']}
53+
speed={0.5}
54+
/>
55+
```
56+
57+
## Progress Text
58+
59+
Reveal text based on scroll progress.
60+
61+
```tsx
62+
import { ProgressText } from '@/components/effects/progress-text'
63+
64+
<ProgressText>
65+
This text reveals as you scroll through the section
66+
</ProgressText>
67+
```
68+
69+
## Dependencies
70+
71+
- **GSAP** - Animation library (gsap)
72+
- **Tempus** - RAF management (tempus)
73+
- **SplitText Plugin** - GSAP plugin for text splitting

lib/features/README.md

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
# Optional Features
2+
3+
Conditionally loaded features for the root layout.
4+
5+
## Overview
6+
7+
`OptionalFeatures` is mounted in `app/layout.tsx` and conditionally loads heavy dependencies based on environment configuration. This prevents unused features from bloating the client bundle.
8+
9+
## Features
10+
11+
| Feature | Env Variable | Default | Description |
12+
|---------|--------------|---------|-------------|
13+
| GSAP Runtime | Always loaded | - | Syncs GSAP with Tempus RAF |
14+
| WebGL Canvas | `NEXT_PUBLIC_ENABLE_WEBGL` | `false` | Global Three.js canvas |
15+
| Dev Tools | `NODE_ENV` | dev only | Orchestra debug panel |
16+
17+
## Configuration
18+
19+
### Enable WebGL
20+
21+
```bash
22+
# .env.local
23+
NEXT_PUBLIC_ENABLE_WEBGL=true
24+
```
25+
26+
### Dev Tools
27+
28+
Automatically enabled in development. Access with `Cmd/Ctrl + O`.
29+
30+
## How It Works
31+
32+
```tsx
33+
// app/layout.tsx - already configured
34+
<OptionalFeatures />
35+
```
36+
37+
The component:
38+
1. Waits for client-side hydration
39+
2. Checks environment variables
40+
3. Dynamically imports only needed features
41+
4. Renders them with `ssr: false` to avoid hydration issues
42+
43+
## Adding Custom Features
44+
45+
```tsx
46+
// lib/features/index.tsx
47+
48+
const MyFeature = dynamic(
49+
() => import('@/components/my-feature').then((mod) => mod.MyFeature),
50+
{ ssr: false }
51+
)
52+
53+
// Then conditionally render based on env var
54+
{process.env.NEXT_PUBLIC_MY_FEATURE === 'true' && <MyFeature />}
55+
```
56+
57+
## Architecture Note
58+
59+
This pattern keeps the root layout clean while allowing opt-in features. Features are code-split and only downloaded when enabled.

lib/scripts/doctor.ts

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
#!/usr/bin/env bun
2+
/**
3+
* Doctor Script - Diagnose common setup issues
4+
*
5+
* Run with: bun run doctor
6+
*/
7+
8+
import { existsSync, readFileSync } from 'node:fs'
9+
import { join } from 'node:path'
10+
11+
const ROOT = process.cwd()
12+
13+
interface Check {
14+
name: string
15+
check: () => boolean | Promise<boolean>
16+
fix?: string
17+
}
18+
19+
const colors = {
20+
green: (s: string) => `\x1b[32m${s}\x1b[0m`,
21+
red: (s: string) => `\x1b[31m${s}\x1b[0m`,
22+
yellow: (s: string) => `\x1b[33m${s}\x1b[0m`,
23+
dim: (s: string) => `\x1b[2m${s}\x1b[0m`,
24+
}
25+
26+
const checks: Check[] = [
27+
{
28+
name: 'Node.js version >= 22',
29+
check: () => {
30+
const version = process.versions.node
31+
const major = Number.parseInt(version.split('.')[0] ?? '0', 10)
32+
return major >= 22
33+
},
34+
fix: 'Install Node.js 22+ from https://nodejs.org or use nvm/fnm',
35+
},
36+
{
37+
name: 'Bun installed',
38+
check: () => {
39+
try {
40+
Bun.version
41+
return true
42+
} catch {
43+
return false
44+
}
45+
},
46+
fix: 'Install Bun: curl -fsSL https://bun.sh/install | bash',
47+
},
48+
{
49+
name: 'Dependencies installed',
50+
check: () => existsSync(join(ROOT, 'node_modules')),
51+
fix: 'Run: bun install',
52+
},
53+
{
54+
name: 'Environment file exists',
55+
check: () =>
56+
existsSync(join(ROOT, '.env.local')) || existsSync(join(ROOT, '.env')),
57+
fix: 'Copy .env.example to .env.local and fill in values',
58+
},
59+
{
60+
name: 'TypeScript config exists',
61+
check: () => existsSync(join(ROOT, 'tsconfig.json')),
62+
fix: 'Ensure tsconfig.json exists in project root',
63+
},
64+
{
65+
name: 'Next.js config valid',
66+
check: () =>
67+
existsSync(join(ROOT, 'next.config.ts')) ||
68+
existsSync(join(ROOT, 'next.config.js')),
69+
fix: 'Ensure next.config.ts exists',
70+
},
71+
{
72+
name: 'Biome config valid',
73+
check: () => {
74+
try {
75+
const biome = readFileSync(join(ROOT, 'biome.json'), 'utf-8')
76+
JSON.parse(biome)
77+
return true
78+
} catch {
79+
return false
80+
}
81+
},
82+
fix: 'Check biome.json for syntax errors',
83+
},
84+
{
85+
name: 'Generated styles exist',
86+
check: () => existsSync(join(ROOT, 'lib/styles/css/tailwind.css')),
87+
fix: 'Run: bun run setup:styles',
88+
},
89+
{
90+
name: 'Public fonts directory exists',
91+
check: () => existsSync(join(ROOT, 'public/fonts')),
92+
fix: 'Add fonts to public/fonts/ directory',
93+
},
94+
{
95+
name: 'Git hooks installed (lefthook)',
96+
check: () => existsSync(join(ROOT, '.git/hooks/pre-commit')),
97+
fix: 'Run: bunx lefthook install',
98+
},
99+
]
100+
101+
async function runDoctor() {
102+
console.log('\n🩺 Satus Doctor\n')
103+
console.log(colors.dim('Checking your development environment...\n'))
104+
105+
let passed = 0
106+
let failed = 0
107+
108+
for (const { name, check, fix } of checks) {
109+
try {
110+
const result = await check()
111+
if (result) {
112+
console.log(`${colors.green('✓')} ${name}`)
113+
passed++
114+
} else {
115+
console.log(`${colors.red('✗')} ${name}`)
116+
if (fix) {
117+
console.log(` ${colors.dim(`Fix: ${fix}`)}`)
118+
}
119+
failed++
120+
}
121+
} catch (_error) {
122+
console.log(
123+
`${colors.yellow('?')} ${name} ${colors.dim('(check failed)')}`
124+
)
125+
failed++
126+
}
127+
}
128+
129+
console.log('')
130+
if (failed === 0) {
131+
console.log(
132+
colors.green(`All ${passed} checks passed! Your environment is ready.`)
133+
)
134+
} else {
135+
console.log(
136+
`${colors.green(`${passed} passed`)}, ${colors.red(`${failed} failed`)}`
137+
)
138+
console.log(
139+
colors.dim('\nFix the issues above and run again: bun run doctor')
140+
)
141+
}
142+
console.log('')
143+
144+
process.exit(failed > 0 ? 1 : 0)
145+
}
146+
147+
runDoctor()

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
"setup:project": "bun ./lib/scripts/setup-project.ts",
3232
"test": "bun test",
3333
"test:setup": "bun test lib/scripts/setup-project.test.ts",
34+
"doctor": "bun ./lib/scripts/doctor.ts",
3435
"setup:styles": "bun ./lib/styles/scripts/setup-styles.ts",
3536
"generate": "bun ./lib/scripts/generate.ts",
3637
"handoff": "bun ./lib/scripts/prepare-handoff.ts",

0 commit comments

Comments
 (0)