Function bodies 269 total
POST function · typescript · L6-L92 (87 LOC)src/app/api/curriculum/generate/route.ts
export async function POST(request: NextRequest) {
try {
const session = await auth();
if (!session?.user?.userId) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
const userId = session.user.userId;
const body = await request.json();
const { childId, lessonsPerWeek, weekCount, focusAreas } = body;
if (!childId || typeof childId !== "string") {
return NextResponse.json(
{ error: "childId is required" },
{ status: 400 }
);
}
// Verify ownership
const child = await prisma.childProfile.findFirst({
where: { id: childId, parentId: userId },
});
if (!child) {
return NextResponse.json({ error: "Child not found" }, { status: 404 });
}
// Check child has completed placement
const placement = await prisma.placementResult.findUnique({
where: { childId },
});
if (!placement) {
return NextResponse.json(
{ error: "Placement assessment muGET function · typescript · L6-L64 (59 LOC)src/app/api/lessons/[id]/route.ts
export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
try {
const session = await auth();
if (!session?.user) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
const { id: lessonId } = await params;
const lesson = getLessonById(lessonId);
if (!lesson) {
return NextResponse.json(
{ error: "Lesson not found" },
{ status: 404 }
);
}
// Include rubric summary if the lesson has one
let rubricSummary = null;
if (lesson.rubricId) {
const rubric = getRubricById(lesson.rubricId);
if (rubric) {
rubricSummary = {
id: rubric.id,
description: rubric.description,
wordRange: rubric.word_range,
criteriaCount: rubric.criteria.length,
criteria: rubric.criteria.map((c) => ({
name: c.name,
displayName: c.display_name,
weight: c.weight,
})),POST function · typescript · L14-L346 (333 LOC)src/app/api/lessons/message/route.ts
export async function POST(request: NextRequest) {
try {
const authSession = await auth();
if (!authSession?.user) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
const body = await request.json();
const { sessionId, message } = body;
if (!sessionId || !message) {
return NextResponse.json(
{ error: "sessionId and message are required" },
{ status: 400 }
);
}
if (typeof message !== "string" || message.trim().length === 0) {
return NextResponse.json(
{ error: "message must be a non-empty string" },
{ status: 400 }
);
}
// Load session
const session = await prisma.session.findUnique({
where: { id: sessionId },
include: { child: true },
});
if (!session) {
return NextResponse.json(
{ error: "Session not found" },
{ status: 404 }
);
}
// Load lesson
const lesson = getLessonById(session.lessonId)POST function · typescript · L14-L272 (259 LOC)src/app/api/lessons/revise/route.ts
export async function POST(request: NextRequest) {
try {
const authSession = await auth();
if (!authSession?.user) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
const body = await request.json();
const { sessionId, text, timeSpentSec } = body;
if (!sessionId || !text) {
return NextResponse.json(
{ error: "sessionId and text are required" },
{ status: 400 }
);
}
if (typeof text !== "string" || text.trim().length === 0) {
return NextResponse.json(
{ error: "text must be a non-empty string" },
{ status: 400 }
);
}
// Load session
const session = await prisma.session.findUnique({
where: { id: sessionId },
include: { child: true },
});
if (!session) {
return NextResponse.json(
{ error: "Session not found" },
{ status: 404 }
);
}
// Only allow revisions in the feedback phase
if (session.phase !POST function · typescript · L14-L242 (229 LOC)src/app/api/lessons/start/route.ts
export async function POST(request: NextRequest) {
try {
const session = await auth();
if (!session?.user) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
const body = await request.json();
const { childId, lessonId, forceNew } = body;
if (!childId || !lessonId) {
return NextResponse.json(
{ error: "childId and lessonId are required" },
{ status: 400 }
);
}
// Verify child exists and belongs to this parent
const child = await prisma.childProfile.findFirst({
where: { id: childId, parentId: session.user.userId },
});
if (!child) {
return NextResponse.json(
{ error: "Child not found or access denied" },
{ status: 403 }
);
}
// Verify lesson exists in curriculum
const lesson = getLessonById(lessonId);
if (!lesson) {
return NextResponse.json(
{ error: "Lesson not found" },
{ status: 404 }
);
}
// POST function · typescript · L17-L328 (312 LOC)src/app/api/lessons/submit/route.ts
export async function POST(request: NextRequest) {
try {
const authSession = await auth();
if (!authSession?.user) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
const body = await request.json();
const { sessionId, text, timeSpentSec } = body;
if (!sessionId || !text) {
return NextResponse.json(
{ error: "sessionId and text are required" },
{ status: 400 }
);
}
if (typeof text !== "string" || text.trim().length === 0) {
return NextResponse.json(
{ error: "text must be a non-empty string" },
{ status: 400 }
);
}
// Load session
const session = await prisma.session.findUnique({
where: { id: sessionId },
include: { child: true },
});
if (!session) {
return NextResponse.json(
{ error: "Session not found" },
{ status: 404 }
);
}
// Only allow submission during assessment or guided phase
// (guGET function · typescript · L5-L61 (57 LOC)src/app/api/placement/[childId]/route.ts
export async function GET(
_request: NextRequest,
{ params }: { params: Promise<{ childId: string }> }
) {
try {
const session = await auth();
if (!session?.user?.userId) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
const { childId } = await params;
// Verify parent owns the child
const child = await prisma.childProfile.findFirst({
where: { id: childId, parentId: session.user.userId },
});
if (!child) {
return NextResponse.json(
{ error: "Child not found" },
{ status: 404 }
);
}
const placementResult = await prisma.placementResult.findUnique({
where: { childId },
});
if (!placementResult) {
return NextResponse.json(
{ error: "No placement result found" },
{ status: 404 }
);
}
return NextResponse.json({
placement: {
id: placementResult.id,
childId: placementResult.childId,
prompts: JSON.paRepobility · MCP-ready · https://repobility.com
PATCH function · typescript · L63-L145 (83 LOC)src/app/api/placement/[childId]/route.ts
export async function PATCH(
request: NextRequest,
{ params }: { params: Promise<{ childId: string }> }
) {
try {
const session = await auth();
if (!session?.user?.userId) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
const { childId } = await params;
// Verify parent owns the child
const child = await prisma.childProfile.findFirst({
where: { id: childId, parentId: session.user.userId },
});
if (!child) {
return NextResponse.json(
{ error: "Child not found" },
{ status: 404 }
);
}
const existingResult = await prisma.placementResult.findUnique({
where: { childId },
});
if (!existingResult) {
return NextResponse.json(
{ error: "No placement result found" },
{ status: 404 }
);
}
const body = await request.json();
const { assignedTier } = body;
if (
typeof assignedTier !== "number" ||
assignedTier < 1 POST function · typescript · L5-L74 (70 LOC)src/app/api/placement/save-draft/route.ts
export async function POST(request: Request) {
try {
const session = await auth();
if (!session?.user?.userId) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
const body = await request.json();
const { childId, responses, step } = body;
if (!childId || typeof childId !== "string") {
return NextResponse.json(
{ error: "childId is required" },
{ status: 400 }
);
}
if (!Array.isArray(responses) || responses.length !== 3) {
return NextResponse.json(
{ error: "Exactly 3 responses are required" },
{ status: 400 }
);
}
if (typeof step !== "number" || step < 0 || step > 2) {
return NextResponse.json(
{ error: "step must be 0, 1, or 2" },
{ status: 400 }
);
}
// Verify parent owns the child
const child = await prisma.childProfile.findFirst({
where: { id: childId, parentId: session.user.userId },
});
if (!chiPOST function · typescript · L7-L121 (115 LOC)src/app/api/placement/start/route.ts
export async function POST(request: Request) {
try {
const session = await auth();
if (!session?.user?.userId) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
const body = await request.json();
const { childId } = body;
if (!childId || typeof childId !== "string") {
return NextResponse.json(
{ error: "childId is required" },
{ status: 400 }
);
}
// Verify parent owns the child
const child = await prisma.childProfile.findFirst({
where: { id: childId, parentId: session.user.userId },
});
if (!child) {
return NextResponse.json(
{ error: "Child not found" },
{ status: 404 }
);
}
// Check if placement already exists
const existingResult = await prisma.placementResult.findUnique({
where: { childId },
});
if (existingResult) {
return NextResponse.json({
prompts: JSON.parse(existingResult.prompts),
exisPOST function · typescript · L7-L170 (164 LOC)src/app/api/placement/submit/route.ts
export async function POST(request: Request) {
try {
const session = await auth();
if (!session?.user?.userId) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
const body = await request.json();
const { childId, prompts, responses } = body;
if (!childId || typeof childId !== "string") {
return NextResponse.json(
{ error: "childId is required" },
{ status: 400 }
);
}
if (
!Array.isArray(prompts) ||
prompts.length !== 3 ||
!Array.isArray(responses) ||
responses.length !== 3
) {
return NextResponse.json(
{ error: "Exactly 3 prompts and 3 responses are required" },
{ status: 400 }
);
}
// Verify parent owns the child
const child = await prisma.childProfile.findFirst({
where: { id: childId, parentId: session.user.userId },
});
if (!child) {
return NextResponse.json(
{ error: "Child not found" },
GET function · typescript · L5-L38 (34 LOC)src/app/api/rubrics/[id]/route.ts
export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
try {
const session = await auth();
if (!session?.user) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
const { id: rubricId } = await params;
const rubric = getRubricById(rubricId);
if (!rubric) {
return NextResponse.json(
{ error: "Rubric not found" },
{ status: 404 }
);
}
const metadata = getRubricMetadata();
return NextResponse.json({
rubric,
scoringScale: metadata.scoring_scale,
});
} catch (error) {
console.error("GET /api/rubrics/[id] error:", error);
return NextResponse.json(
{ error: "Internal server error" },
{ status: 500 }
);
}
}LoginPage function · typescript · L8-L141 (134 LOC)src/app/auth/login/page.tsx
export default function LoginPage() {
const router = useRouter();
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [error, setError] = useState("");
const [loading, setLoading] = useState(false);
async function handleSubmit(e: React.FormEvent) {
e.preventDefault();
setError("");
setLoading(true);
try {
const result = await signIn("credentials", {
email,
password,
redirect: false,
});
if (result?.error) {
setError("Invalid email or password. Please try again.");
} else {
router.push("/dashboard");
}
} catch {
setError("Something went wrong. Please try again.");
} finally {
setLoading(false);
}
}
return (
<div className="min-h-screen bg-tier1-bg flex items-center justify-center px-4">
<div className="w-full max-w-md">
{/* Branding */}
<div className="text-center mb-8">
<h1 classNamehandleSubmit function · typescript · L15-L37 (23 LOC)src/app/auth/login/page.tsx
async function handleSubmit(e: React.FormEvent) {
e.preventDefault();
setError("");
setLoading(true);
try {
const result = await signIn("credentials", {
email,
password,
redirect: false,
});
if (result?.error) {
setError("Invalid email or password. Please try again.");
} else {
router.push("/dashboard");
}
} catch {
setError("Something went wrong. Please try again.");
} finally {
setLoading(false);
}
}SignupPage function · typescript · L8-L214 (207 LOC)src/app/auth/signup/page.tsx
export default function SignupPage() {
const router = useRouter();
const [name, setName] = useState("");
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [confirmPassword, setConfirmPassword] = useState("");
const [error, setError] = useState("");
const [loading, setLoading] = useState(false);
async function handleSubmit(e: React.FormEvent) {
e.preventDefault();
setError("");
// Client-side validation
if (password.length < 8) {
setError("Password must be at least 8 characters.");
return;
}
if (password !== confirmPassword) {
setError("Passwords do not match.");
return;
}
setLoading(true);
try {
const res = await fetch("/api/auth/signup", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name, email, password }),
});
const data = await res.json();
if (res.status === 409) {
Repobility analyzer · published findings · https://repobility.com
handleSubmit function · typescript · L17-L73 (57 LOC)src/app/auth/signup/page.tsx
async function handleSubmit(e: React.FormEvent) {
e.preventDefault();
setError("");
// Client-side validation
if (password.length < 8) {
setError("Password must be at least 8 characters.");
return;
}
if (password !== confirmPassword) {
setError("Passwords do not match.");
return;
}
setLoading(true);
try {
const res = await fetch("/api/auth/signup", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name, email, password }),
});
const data = await res.json();
if (res.status === 409) {
setError("Email already registered. Please log in instead.");
setLoading(false);
return;
}
if (!res.ok) {
setError(data.error || "Something went wrong. Please try again.");
setLoading(false);
return;
}
// Auto-login after successful signup
const result = await signIn("credentBadgeCard function · typescript · L38-L79 (42 LOC)src/app/badges/[childId]/page.tsx
function BadgeCard({
badge,
earned,
unlockedAt,
}: {
badge: BadgeDefinition;
earned: boolean;
unlockedAt?: string;
}) {
return (
<div
className={`rounded-2xl p-4 border text-center transition-all duration-200 ${
earned
? "bg-white border-active-accent/30 shadow-sm hover:shadow-md"
: "bg-gray-50 border-gray-200 opacity-60"
}`}
>
<div className={`text-3xl mb-2 ${earned ? "" : "grayscale"}`}>
{earned ? badge.emoji : "\uD83D\uDD12"}
</div>
<h4
className={`font-bold text-sm ${
earned ? "text-active-text" : "text-gray-400"
}`}
>
{badge.name}
</h4>
<p
className={`text-xs mt-1 leading-relaxed ${
earned ? "text-active-text/60" : "text-gray-400"
}`}
>
{badge.description}
</p>
{earned && unlockedAt && (
<p className="text-[11px] text-active-secondary font-semibold mt-2">
{new Date(unlockedABadgeCollectionContent function · typescript · L81-L236 (156 LOC)src/app/badges/[childId]/page.tsx
function BadgeCollectionContent({
childId,
}: {
childId: string;
}) {
const [badgesData, setBadgesData] = useState<BadgesResponse | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
fetch(`/api/children/${encodeURIComponent(childId)}/badges`)
.then((res) => {
if (!res.ok) throw new Error("Failed to load badges");
return res.json();
})
.then((data) => setBadgesData(data))
.catch((err) => setError(err.message))
.finally(() => setLoading(false));
}, [childId]);
// Mark unseen badges as seen
useEffect(() => {
if (!badgesData || badgesData.unseen === 0) return;
const unseenIds = badgesData.badges
.filter((b) => !b.seen)
.map((b) => b.id);
if (unseenIds.length === 0) return;
fetch(`/api/children/${encodeURIComponent(childId)}/badges/seen`, {
method: "POST",
headers: { "Content-Type": "application/jsoBadgesPage function · typescript · L238-L268 (31 LOC)src/app/badges/[childId]/page.tsx
export default function BadgesPage() {
const { data: session, status } = useSession();
const { activeChild } = useActiveChild();
const router = useRouter();
const params = useParams();
const childId = params.childId as string;
useEffect(() => {
if (status === "unauthenticated") {
router.push("/auth/login");
}
}, [status, router]);
if (status === "loading") {
return (
<div className="min-h-screen bg-active-bg flex items-center justify-center">
<p className="text-active-text/60 font-semibold">Loading...</p>
</div>
);
}
if (!session) return null;
const tier = (activeChild?.tier ?? 1) as Tier;
return (
<TierProvider tier={tier}>
<BadgeCollectionContent childId={childId} />
</TierProvider>
);
}CurriculumPage function · typescript · L37-L346 (310 LOC)src/app/curriculum/[childId]/page.tsx
export default function CurriculumPage() {
const router = useRouter();
const { childId } = useParams<{ childId: string }>();
const [curriculum, setCurriculum] = useState<CurriculumData | null>(null);
const [weeks, setWeeks] = useState<Week[]>([]);
const [expandedWeeks, setExpandedWeeks] = useState<Set<number>>(new Set());
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [noCurriculum, setNoCurriculum] = useState(false);
useEffect(() => {
async function fetchCurriculum() {
try {
const res = await fetch(`/api/curriculum/${childId}`);
if (res.status === 404) {
setNoCurriculum(true);
return;
}
if (!res.ok) throw new Error("Failed to load curriculum");
const data = await res.json();
setCurriculum(data.curriculum);
const fetchedWeeks: Week[] = data.weeks.map(
(w: {
weekNumber: number;
theme: stfetchCurriculum function · typescript · L49-L86 (38 LOC)src/app/curriculum/[childId]/page.tsx
async function fetchCurriculum() {
try {
const res = await fetch(`/api/curriculum/${childId}`);
if (res.status === 404) {
setNoCurriculum(true);
return;
}
if (!res.ok) throw new Error("Failed to load curriculum");
const data = await res.json();
setCurriculum(data.curriculum);
const fetchedWeeks: Week[] = data.weeks.map(
(w: {
weekNumber: number;
theme: string;
status: string;
lessons?: Lesson[];
lessonIds?: string | string[];
}) => ({
weekNumber: w.weekNumber,
theme: w.theme,
status: w.status,
lessons: w.lessons || [],
})
);
setWeeks(fetchedWeeks);
// Auto-expand the current week (first non-completed)
const currentWeek = fetchedWeeks.find((w) => w.status !== "completed");
if (currentWeek) {
setExpandedWeeks(new SettoggleWeek function · typescript · L90-L100 (11 LOC)src/app/curriculum/[childId]/page.tsx
function toggleWeek(weekNumber: number) {
setExpandedWeeks((prev) => {
const next = new Set(prev);
if (next.has(weekNumber)) {
next.delete(weekNumber);
} else {
next.add(weekNumber);
}
return next;
});
}CurriculumRevisePage function · typescript · L41-L305 (265 LOC)src/app/curriculum/[childId]/revise/page.tsx
export default function CurriculumRevisePage() {
const { data: session, status } = useSession();
const router = useRouter();
const { childId } = useParams<{ childId: string }>();
const [selectedOption, setSelectedOption] = useState<string | null>(null);
const [customText, setCustomText] = useState("");
const [submitting, setSubmitting] = useState(false);
const [error, setError] = useState<string | null>(null);
const [success, setSuccess] = useState(false);
useEffect(() => {
if (status === "unauthenticated") {
router.push("/auth/login");
}
}, [status, router]);
async function handleSubmit(e: React.FormEvent) {
e.preventDefault();
setError(null);
if (!selectedOption && !customText.trim()) {
setError("Please select an option or describe the change you'd like.");
return;
}
setSubmitting(true);
// Build description from selected option + custom text
const preset = PRESET_OPTIONS.find((o) => o.id === selectedOpSame scanner, your repo: https://repobility.com — Repobility
handleSubmit function · typescript · L58-L100 (43 LOC)src/app/curriculum/[childId]/revise/page.tsx
async function handleSubmit(e: React.FormEvent) {
e.preventDefault();
setError(null);
if (!selectedOption && !customText.trim()) {
setError("Please select an option or describe the change you'd like.");
return;
}
setSubmitting(true);
// Build description from selected option + custom text
const preset = PRESET_OPTIONS.find((o) => o.id === selectedOption);
const parts: string[] = [];
if (preset) {
parts.push(`${preset.label}: ${preset.description}`);
}
if (customText.trim()) {
parts.push(customText.trim());
}
try {
const res = await fetch(`/api/curriculum/${childId}/revise`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
reason: "parent_request",
description: parts.join(". "),
}),
});
if (!res.ok) {
const data = await res.json();
throw new Error(data.error || "Failed to revise cCurriculumSetupPage function · typescript · L19-L234 (216 LOC)src/app/curriculum/[childId]/setup/page.tsx
export default function CurriculumSetupPage() {
const router = useRouter();
const { childId } = useParams<{ childId: string }>();
const [childName, setChildName] = useState<string | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [lessonsPerWeek, setLessonsPerWeek] = useState(3);
const [weekCount, setWeekCount] = useState(8);
const [focusAreas, setFocusAreas] = useState<string[]>([]);
const [generating, setGenerating] = useState(false);
useEffect(() => {
async function fetchChild() {
try {
const res = await fetch(`/api/children/${childId}`);
if (!res.ok) throw new Error("Failed to load child profile");
const data = await res.json();
setChildName(data.child?.name || data.name || "Your Child");
} catch {
setError("Could not load child profile.");
} finally {
setLoading(false);
}
}
fetchChild();
}, [childId])fetchChild function · typescript · L33-L44 (12 LOC)src/app/curriculum/[childId]/setup/page.tsx
async function fetchChild() {
try {
const res = await fetch(`/api/children/${childId}`);
if (!res.ok) throw new Error("Failed to load child profile");
const data = await res.json();
setChildName(data.child?.name || data.name || "Your Child");
} catch {
setError("Could not load child profile.");
} finally {
setLoading(false);
}
}toggleFocusArea function · typescript · L48-L52 (5 LOC)src/app/curriculum/[childId]/setup/page.tsx
function toggleFocusArea(area: string) {
setFocusAreas((prev) =>
prev.includes(area) ? prev.filter((a) => a !== area) : [...prev, area]
);
}handleGenerate function · typescript · L54-L81 (28 LOC)src/app/curriculum/[childId]/setup/page.tsx
async function handleGenerate(e: React.FormEvent) {
e.preventDefault();
setError(null);
setGenerating(true);
try {
const res = await fetch("/api/curriculum/generate", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
childId,
lessonsPerWeek,
weekCount,
focusAreas: focusAreas.length > 0 ? focusAreas : undefined,
}),
});
if (!res.ok) {
const data = await res.json();
throw new Error(data.error || "Failed to generate curriculum");
}
router.push(`/curriculum/${childId}`);
} catch (err) {
setError(err instanceof Error ? err.message : "Something went wrong");
setGenerating(false);
}
}computeTierLabel function · typescript · L18-L22 (5 LOC)src/app/dashboard/children/[id]/page.tsx
function computeTierLabel(age: number): string {
if (age <= 9) return "Tier 1: Foundational";
if (age <= 12) return "Tier 2: Developing";
return "Tier 3: Advanced";
}EditChildPage function · typescript · L24-L317 (294 LOC)src/app/dashboard/children/[id]/page.tsx
export default function EditChildPage({
params,
}: {
params: Promise<{ id: string }>;
}) {
const { id } = use(params);
const router = useRouter();
const [name, setName] = useState("");
const [age, setAge] = useState(8);
const [gradeLevel, setGradeLevel] = useState("");
const [interests, setInterests] = useState("");
const [avatarEmoji, setAvatarEmoji] = useState(AVATAR_OPTIONS[0]);
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
const [deleting, setDeleting] = useState(false);
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
fetch(`/api/children/${id}`)
.then((res) => {
if (!res.ok) throw new Error("Failed to load child profile");
return res.json();
})
.then((data) => {
const child = data.child;
setName(child.name);
setAge(child.age);
setGradeLevel(chilhandleSave function · typescript · L60-L100 (41 LOC)src/app/dashboard/children/[id]/page.tsx
async function handleSave(e: React.FormEvent) {
e.preventDefault();
setError(null);
if (!name.trim()) {
setError("Name is required");
return;
}
if (age < 7 || age > 15) {
setError("Age must be between 7 and 15");
return;
}
setSaving(true);
try {
const res = await fetch(`/api/children/${id}`, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
name: name.trim(),
age,
gradeLevel: gradeLevel.trim() || null,
interests: interests.trim() || null,
avatarEmoji,
}),
});
if (!res.ok) {
const data = await res.json();
throw new Error(data.error || "Failed to update profile");
}
router.push("/dashboard");
} catch (err) {
setError(err instanceof Error ? err.message : "Something went wrong");
} finally {
setSaving(false);
}
}All rows scored by the Repobility analyzer (https://repobility.com)
handleDelete function · typescript · L102-L121 (20 LOC)src/app/dashboard/children/[id]/page.tsx
async function handleDelete() {
setDeleting(true);
try {
const res = await fetch(`/api/children/${id}`, {
method: "DELETE",
});
if (!res.ok) {
const data = await res.json();
throw new Error(data.error || "Failed to delete profile");
}
router.push("/dashboard");
} catch (err) {
setError(err instanceof Error ? err.message : "Something went wrong");
setDeleting(false);
setShowDeleteConfirm(false);
}
}capitalize function · typescript · L17-L19 (3 LOC)src/app/dashboard/children/[id]/report/[lessonId]/page.tsx
function capitalize(s: string): string {
return s.charAt(0).toUpperCase() + s.slice(1);
}StarRating function · typescript · L21-L36 (16 LOC)src/app/dashboard/children/[id]/report/[lessonId]/page.tsx
function StarRating({ score, maxScore = 4 }: { score: number; maxScore?: number }) {
const rounded = Math.round(score * 2) / 2;
return (
<span className="inline-flex items-center gap-0.5">
{Array.from({ length: maxScore }, (_, i) => {
const isFull = i < Math.floor(rounded);
const isHalf = !isFull && i < rounded;
return (
<span key={i} className="text-lg">
{isFull ? "\u2B50" : isHalf ? "\u2B50" : "\u2606"}
</span>
);
})}
</span>
);
}LessonDetailContent function · typescript · L38-L278 (241 LOC)src/app/dashboard/children/[id]/report/[lessonId]/page.tsx
function LessonDetailContent({ childId, lessonId }: { childId: string; lessonId: string }) {
const router = useRouter();
const [data, setData] = useState<LessonReportResponse | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [showFullText, setShowFullText] = useState<Record<string, boolean>>({});
useEffect(() => {
getLessonReport(childId, lessonId)
.then(setData)
.catch((err) => setError(err.message))
.finally(() => setLoading(false));
}, [childId, lessonId]);
if (loading) {
return (
<div className="min-h-screen bg-active-bg flex items-center justify-center">
<div className="text-center">
<div className="w-8 h-8 border-4 border-active-primary/30 border-t-active-primary rounded-full animate-spin mx-auto" />
<p className="mt-4 text-active-text/60 font-semibold">Loading lesson detail...</p>
</div>
</div>
);
}
if (errLessonDetailPage function · typescript · L280-L320 (41 LOC)src/app/dashboard/children/[id]/report/[lessonId]/page.tsx
export default function LessonDetailPage({
params,
}: {
params: Promise<{ id: string; lessonId: string }>;
}) {
const { id: childId, lessonId } = use(params);
const { data: session, status } = useSession();
const router = useRouter();
const [tier, setTier] = useState<Tier>(1);
useEffect(() => {
if (status === "unauthenticated") {
router.push("/auth/login");
}
}, [status, router]);
useEffect(() => {
fetch(`/api/children/${encodeURIComponent(childId)}`)
.then((res) => (res.ok ? res.json() : null))
.then((data) => {
if (data?.child?.tier) setTier(data.child.tier as Tier);
})
.catch(() => {});
}, [childId]);
if (status === "loading") {
return (
<div className="min-h-screen bg-[#FFF9F0] flex items-center justify-center">
<div className="w-8 h-8 border-4 border-[#FF6B6B]/30 border-t-[#FF6B6B] rounded-full animate-spin" />
</div>
);
}
if (!session) return null;
return (
<TierProvcapitalize function · typescript · L98-L100 (3 LOC)src/app/dashboard/children/[id]/report/page.tsx
function capitalize(s: string): string {
return s.charAt(0).toUpperCase() + s.slice(1);
}StarRating function · typescript · L102-L117 (16 LOC)src/app/dashboard/children/[id]/report/page.tsx
function StarRating({ score, maxScore = 4 }: { score: number; maxScore?: number }) {
const rounded = Math.round(score * 2) / 2;
return (
<span className="inline-flex items-center gap-0.5">
{Array.from({ length: maxScore }, (_, i) => {
const isFull = i < Math.floor(rounded);
const isHalf = !isFull && i < rounded;
return (
<span key={i} className="text-base">
{isFull ? "\u2B50" : isHalf ? "\u2B50" : "\u2606"}
</span>
);
})}
</span>
);
}handleExport function · typescript · L140-L163 (24 LOC)src/app/dashboard/children/[id]/report/page.tsx
async function handleExport() {
setExporting(true);
try {
const res = await fetch(
`/api/children/${encodeURIComponent(childId)}/report/export`
);
if (!res.ok) throw new Error("Export failed");
const blob = await res.blob();
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download =
res.headers.get("Content-Disposition")?.match(/filename="(.+)"/)?.[1] ||
"report.csv";
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
} catch {
alert("Failed to export report. Please try again.");
} finally {
setExporting(false);
}
}Repobility · MCP-ready · https://repobility.com
handleGenerateSummary function · typescript · L165-L179 (15 LOC)src/app/dashboard/children/[id]/report/page.tsx
async function handleGenerateSummary() {
setGeneratingSummary(true);
try {
const res = await fetch(
`/api/children/${encodeURIComponent(childId)}/report?generateSummary=true`
);
if (!res.ok) throw new Error("Failed to generate summary");
const data = await res.json();
setAiSummary(data.aiSummary ?? null);
} catch {
setAiSummary(null);
} finally {
setGeneratingSummary(false);
}
}toggleAssessment function · typescript · L181-L188 (8 LOC)src/app/dashboard/children/[id]/report/page.tsx
function toggleAssessment(lessonId: string) {
setExpandedAssessments((prev) => {
const next = new Set(prev);
if (next.has(lessonId)) next.delete(lessonId);
else next.add(lessonId);
return next;
});
}ReportPage function · typescript · L630-L673 (44 LOC)src/app/dashboard/children/[id]/report/page.tsx
export default function ReportPage({
params,
}: {
params: Promise<{ id: string }>;
}) {
const { id: childId } = use(params);
const { data: session, status } = useSession();
const router = useRouter();
const [tier, setTier] = useState<Tier>(1);
useEffect(() => {
if (status === "unauthenticated") {
router.push("/auth/login");
}
}, [status, router]);
// Fetch child tier for TierProvider
useEffect(() => {
fetch(`/api/children/${encodeURIComponent(childId)}`)
.then((res) => (res.ok ? res.json() : null))
.then((data) => {
if (data?.child?.tier) {
setTier(data.child.tier as Tier);
}
})
.catch(() => {});
}, [childId]);
if (status === "loading") {
return (
<div className="min-h-screen bg-[#FFF9F0] flex items-center justify-center">
<div className="w-8 h-8 border-4 border-[#FF6B6B]/30 border-t-[#FF6B6B] rounded-full animate-spin" />
</div>
);
}
if (!session) return nullcomputeTierLabel function · typescript · L18-L22 (5 LOC)src/app/dashboard/children/new/page.tsx
function computeTierLabel(age: number): string {
if (age <= 9) return "Tier 1: Foundational";
if (age <= 12) return "Tier 2: Developing";
return "Tier 3: Advanced";
}NewChildPage function · typescript · L24-L221 (198 LOC)src/app/dashboard/children/new/page.tsx
export default function NewChildPage() {
const router = useRouter();
const [name, setName] = useState("");
const [age, setAge] = useState(8);
const [gradeLevel, setGradeLevel] = useState("");
const [interests, setInterests] = useState("");
const [avatarEmoji, setAvatarEmoji] = useState(AVATAR_OPTIONS[0]);
const [submitting, setSubmitting] = useState(false);
const [error, setError] = useState<string | null>(null);
async function handleSubmit(e: React.FormEvent) {
e.preventDefault();
setError(null);
if (!name.trim()) {
setError("Name is required");
return;
}
if (age < 7 || age > 15) {
setError("Age must be between 7 and 15");
return;
}
setSubmitting(true);
try {
const res = await fetch("/api/children", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
name: name.trim(),
age,
gradeLevel: gradeLevel.trim() || undehandleSubmit function · typescript · L34-L75 (42 LOC)src/app/dashboard/children/new/page.tsx
async function handleSubmit(e: React.FormEvent) {
e.preventDefault();
setError(null);
if (!name.trim()) {
setError("Name is required");
return;
}
if (age < 7 || age > 15) {
setError("Age must be between 7 and 15");
return;
}
setSubmitting(true);
try {
const res = await fetch("/api/children", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
name: name.trim(),
age,
gradeLevel: gradeLevel.trim() || undefined,
interests: interests.trim() || undefined,
avatarEmoji,
}),
});
if (!res.ok) {
const errData = await res.json();
throw new Error(errData.error || "Failed to create child profile");
}
const data = await res.json();
router.push(`/placement/${data.child.id}`);
} catch (err) {
setError(err instanceof Error ? err.message : "Something went wrong"ParentDashboard function · typescript · L36-L288 (253 LOC)src/app/dashboard/page.tsx
export default function ParentDashboard() {
const { data: session } = useSession();
const router = useRouter();
const { setActiveChild } = useActiveChild();
const [children, setChildren] = useState<ChildData[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [childStatuses, setChildStatuses] = useState<Record<string, ChildStatus>>({});
const [childExtraStats, setChildExtraStats] = useState<Record<string, ChildExtraStats>>({});
useEffect(() => {
fetch("/api/children")
.then((res) => {
if (!res.ok) throw new Error("Failed to load children");
return res.json();
})
.then((data) => {
setChildren(data.children);
// Fetch placement/curriculum status for each child
for (const child of data.children) {
Promise.all([
fetch(`/api/placement/${child.id}`).then(r => r.ok).catch(() => false),
fetch(`/api/curriculum/${childhandleSelectChild function · typescript · L101-L110 (10 LOC)src/app/dashboard/page.tsx
function handleSelectChild(child: ChildData) {
setActiveChild({
id: child.id,
name: child.name,
age: child.age,
tier: child.tier as 1 | 2 | 3,
avatarEmoji: child.avatarEmoji,
});
router.push("/home");
}Repobility analyzer · published findings · https://repobility.com
ProgressBar function · typescript · L30-L39 (10 LOC)src/app/home/page.tsx
function ProgressBar({ value, color, height = "h-3" }: { value: number; color: string; height?: string }) {
return (
<div className={`w-full ${height} bg-gray-100 rounded-full overflow-hidden`}>
<div
className={`h-full ${color} rounded-full transition-all duration-700 ease-out`}
style={{ width: `${value}%` }}
/>
</div>
);
}Dashboard function · typescript · L501-L608 (108 LOC)src/app/home/page.tsx
export default function Dashboard() {
const { activeChild } = useActiveChild();
const router = useRouter();
const [data, setData] = useState<StudentProgressResponse | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [curriculum, setCurriculum] = useState<any>(null);
const [curriculumLoading, setCurriculumLoading] = useState(true);
const [hasPlacement, setHasPlacement] = useState<boolean | null>(null);
const [skills, setSkills] = useState<SkillCategory[] | null>(null);
const [streakData, setStreakData] = useState<StreakData | null>(null);
const [recentBadges, setRecentBadges] = useState<RecentBadge[] | null>(null);
useEffect(() => {
if (!activeChild) {
router.push("/dashboard");
return;
}
getProgress(activeChild.id)
.then(setData)
.catch((err) => setError(err.message))
.finally(() => setLoading(false));
}, [activeChild, router]);
useEffect(() =RootLayout function · typescript · L11-L33 (23 LOC)src/app/layout.tsx
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<head>
<link
href="https://fonts.googleapis.com/css2?family=Nunito:wght@400;600;700;800&family=DM+Sans:wght@400;500;700&family=Sora:wght@400;500;600;700&family=Literata:ital,wght@0,400;0,700;1,400&display=swap"
rel="stylesheet"
/>
</head>
<body className="antialiased">
<SessionProvider>
<ActiveChildProvider>
{children}
</ActiveChildProvider>
</SessionProvider>
</body>
</html>
);
}