← back to karthikjanagiraman__WritingCoach

Function bodies 269 total

All specs Real LLM only Function bodies
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 mu
GET 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
    // (gu
GET 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.pa
Repobility · 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 (!chi
POST 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),
        exis
POST 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 className
handleSubmit 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("credent
BadgeCard 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(unlockedA
BadgeCollectionContent 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/jso
BadgesPage 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: st
fetchCurriculum 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 Set
toggleWeek 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 === selectedOp
Same 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 c
CurriculumSetupPage 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(chil
handleSave 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 (err
LessonDetailPage 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 (
    <TierProv
capitalize 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 null
computeTierLabel 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() || unde
handleSubmit 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/${child
handleSelectChild 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>
  );
}
‹ prevpage 2 / 6next ›