From 0a9def36bf440c584c9de47200b124f3bf3be251 Mon Sep 17 00:00:00 2001 From: Mannu Date: Thu, 28 May 2026 00:52:23 +0530 Subject: [PATCH] feat(quota): enforce 1-baby limit on free plan - quota.ts: checkChildLimit() mirrors checkMemberLimit() using families.max_children - POST /api/children: returns 403 child_limit_reached when free family is at limit - ChildLimitBanner: new banner component for the family page - /family page: shows banner + hides Add Baby button when at limit Co-Authored-By: Claude Sonnet 4.6 --- src/app/(app)/family/page.tsx | 30 ++++++++++++++++++-- src/app/api/children/route.ts | 9 ++++++ src/components/StorageMeter.tsx | 26 +++++++++++++++++ src/lib/quota.ts | 49 +++++++++++++++++++++++++++++++++ 4 files changed, 112 insertions(+), 2 deletions(-) diff --git a/src/app/(app)/family/page.tsx b/src/app/(app)/family/page.tsx index eade0ce..009220c 100644 --- a/src/app/(app)/family/page.tsx +++ b/src/app/(app)/family/page.tsx @@ -3,6 +3,7 @@ import { useState, useEffect } from "react"; import { useRouter } from "next/navigation"; import { useFamily } from "@/app/FamilyProvider"; +import { ChildLimitBanner } from "@/components/StorageMeter"; interface Child { id: string; @@ -24,6 +25,7 @@ export default function FamilyPage() { const [newName, setNewName] = useState(""); const [newDob, setNewDob] = useState(""); const [newSex, setNewSex] = useState("male"); + const [childLimit, setChildLimit] = useState<{ currentCount: number; limit: number } | null>(null); // Load children from FamilyProvider or fetch useEffect(() => { @@ -35,6 +37,18 @@ export default function FamilyPage() { } }, [childrenFromProvider, familyId]); + // Sync limit state whenever children list changes + useEffect(() => { + if (!familyId) return; + fetch("/api/family") + .then(r => r.json()) + .then(d => { + const maxChildren = d.family?.max_children ?? 1; + setChildLimit({ currentCount: children.length, limit: maxChildren }); + }) + .catch(() => {}); + }, [familyId, children.length]); + const fetchOwnChildren = async () => { if (!familyId) return; try { @@ -85,12 +99,19 @@ export default function FamilyPage() { setShowAdd(false); setNewName(""); setNewDob(""); + } else if (data.reason === "child_limit_reached") { + setShowAdd(false); + setChildLimit({ currentCount: data.currentCount, limit: data.limit }); + } else { + alert(data.error); } } catch (err) { console.error("Failed to add:", err); } }; + const atChildLimit = childLimit !== null && childLimit.currentCount >= childLimit.limit; + return (
@@ -99,8 +120,13 @@ export default function FamilyPage() {
+ {/* Child limit banner */} + {atChildLimit && childLimit && ( + + )} + {/* Add Child Form */} - {showAdd && ( + {showAdd && !atChildLimit && (
0 && ( + {!showAdd && children.length > 0 && !atChildLimit && (