G5 — Age-Aware UX:
- useStageCheck hook: maps birth date → BabyStage (newborn/infant/sitter/crawler/toddler/walker)
- Time-of-day fast-log suggestion chip on home page (time × stage matrix)
- Milestones page: 25 WHO/AAP milestones, category filter, progress bar, inline date picker
- Milestones API: GET (merged definitions + achievements), POST (upsert), DELETE (un-mark)
- DB: milestone_achievements table with unique(child_id, milestone_key)
- Milestones 🌟 added to menu
G6 — Mama Affiliate Page:
- member_profiles, recommended_products, product_clicks tables
- /api/profile CRUD (GET/PUT), /api/profile/products (GET/POST/PATCH/DELETE)
- Public routes: /api/profile/[slug] and /api/profile/[slug]/click (IP hashed)
- /settings/profile: slug + bio editor, product list with ↑↓ reorder + click counts
- /m/[slug]: beautiful public page (gradient bg, product grid, Shop → click tracking)
- Settings page link to profile setup
DB migrations: 0014_milestones, 0015_affiliate.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
54 lines
1.9 KiB
TypeScript
54 lines
1.9 KiB
TypeScript
import { NextResponse } from "next/server";
|
|
import { sql } from "@/db";
|
|
import { requireFamily } from "@/lib/auth";
|
|
import { MILESTONES } from "@/lib/milestones";
|
|
|
|
export async function GET(request: Request) {
|
|
const auth = await requireFamily();
|
|
if (!auth.success) return NextResponse.json({ error: auth.error }, { status: auth.status });
|
|
|
|
const { searchParams } = new URL(request.url);
|
|
const childId = searchParams.get("childId");
|
|
if (!childId) return NextResponse.json({ error: "childId required" }, { status: 400 });
|
|
|
|
const familyId = auth.session!.familyId!;
|
|
|
|
const rows = await sql`
|
|
SELECT milestone_key, achieved_at, notes
|
|
FROM milestone_achievements
|
|
WHERE child_id = ${childId} AND family_id = ${familyId}
|
|
`;
|
|
|
|
const achievedMap = new Map(rows.map(r => [r.milestone_key, r]));
|
|
|
|
const items = MILESTONES.map(m => ({
|
|
...m,
|
|
achieved: achievedMap.has(m.key),
|
|
achievedAt: achievedMap.get(m.key)?.achieved_at ?? null,
|
|
notes: achievedMap.get(m.key)?.notes ?? null,
|
|
}));
|
|
|
|
return NextResponse.json({ items });
|
|
}
|
|
|
|
export async function POST(request: Request) {
|
|
const auth = await requireFamily();
|
|
if (!auth.success) return NextResponse.json({ error: auth.error }, { status: auth.status });
|
|
|
|
const familyId = auth.session!.familyId!;
|
|
const { childId, milestoneKey, achievedAt, notes } = await request.json();
|
|
|
|
if (!childId || !milestoneKey || !achievedAt) {
|
|
return NextResponse.json({ error: "childId, milestoneKey, achievedAt required" }, { status: 400 });
|
|
}
|
|
|
|
await sql`
|
|
INSERT INTO milestone_achievements (child_id, family_id, milestone_key, achieved_at, notes)
|
|
VALUES (${childId}, ${familyId}, ${milestoneKey}, ${achievedAt}, ${notes ?? null})
|
|
ON CONFLICT (child_id, milestone_key) DO UPDATE SET
|
|
achieved_at = EXCLUDED.achieved_at,
|
|
notes = EXCLUDED.notes
|
|
`;
|
|
|
|
return NextResponse.json({ success: true });
|
|
}
|