From 79d70ef1da42ea09f1f0d749d26f3b155951e433 Mon Sep 17 00:00:00 2001 From: sahil-singh1107 Date: Sat, 18 Apr 2026 23:33:30 +0530 Subject: [PATCH 1/6] fix profile api --- app/api/profile/route.ts | 139 +++++++++---------- app/dashboard/profile/ProfileClient.tsx | 174 ++++++++++++++---------- lib/schemas.ts | 18 +++ package-lock.json | 5 +- tsconfig.json | 17 ++- 5 files changed, 199 insertions(+), 154 deletions(-) create mode 100644 lib/schemas.ts diff --git a/app/api/profile/route.ts b/app/api/profile/route.ts index a3d98bf..4f74bcd 100644 --- a/app/api/profile/route.ts +++ b/app/api/profile/route.ts @@ -1,114 +1,103 @@ -import { auth, currentUser } from '@clerk/nextjs/server' -import { NextRequest, NextResponse } from 'next/server' -import { connectDB } from '@/lib/mongodb' -import { Teacher } from '@/models/Teacher' +import { auth, currentUser } from "@clerk/nextjs/server"; +import { NextRequest, NextResponse } from "next/server"; +import { connectDB } from "@/lib/mongodb"; +import { Teacher } from "@/models/Teacher"; +import { updateSchema } from "@/lib/schemas"; export async function GET(req: NextRequest) { - const { searchParams } = new URL(req.url) - const queryUserId = searchParams.get('userId') + const { searchParams } = new URL(req.url); + const queryUserId = searchParams.get("userId"); - let userId: string | null = queryUserId + let userId: string | null = queryUserId; if (!userId) { - const session = await auth() - userId = session.userId + const session = await auth(); + userId = session.userId; } - if (!userId) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + if (!userId) + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); try { - await connectDB() - let teacher = await Teacher.findOne({ clerkId: userId }).lean() + await connectDB(); + let teacher = await Teacher.findOne({ clerkId: userId }).lean(); if (!teacher) { - const clerkUser = await currentUser() + const clerkUser = await currentUser(); const created = await Teacher.create({ clerkId: userId, - name: clerkUser?.fullName ?? '', - email: clerkUser?.emailAddresses[0]?.emailAddress ?? '', - department: '', + name: clerkUser?.fullName ?? "", + email: clerkUser?.emailAddresses[0]?.emailAddress ?? "", + department: "", subjects: [], - }) - teacher = created.toObject() + }); + teacher = created.toObject(); } - return NextResponse.json(teacher) + return NextResponse.json(teacher); } catch (error) { - console.error('GET /api/profile error:', error instanceof Error ? error.message : error) - return NextResponse.json({ error: 'Internal server error' }, { status: 500 }) + console.error( + "GET /api/profile error:", + error instanceof Error ? error.message : error, + ); + return NextResponse.json( + { error: "Internal server error" }, + { status: 500 }, + ); } } export async function PUT(req: NextRequest) { - const { userId } = await auth() - if (!userId) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + const { userId } = await auth(); + if (!userId) + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); try { - await connectDB() - - let body + await connectDB(); + + let body; try { - body = await req.json() + body = await req.json(); } catch { - return NextResponse.json({ error: 'Invalid JSON in request body' }, { status: 400 }) + return NextResponse.json( + { error: "Invalid JSON in request body" }, + { status: 400 }, + ); } - - const { name, department, subjects, phone, bio, academicHistory } = body - // Validate input - if (typeof name !== 'string' || !name.trim()) { - return NextResponse.json({ error: 'name must be a non-empty string' }, { status: 400 }) - } - if (department !== undefined && typeof department !== 'string') { - return NextResponse.json({ error: 'department must be a string' }, { status: 400 }) - } - if (!Array.isArray(subjects) || !subjects.every((s) => typeof s === 'string')) { - return NextResponse.json({ error: 'subjects must be an array of strings' }, { status: 400 }) - } - if (phone !== undefined && typeof phone !== 'string') { - return NextResponse.json({ error: 'phone must be a string' }, { status: 400 }) - } - if (bio !== undefined && typeof bio !== 'string') { - return NextResponse.json({ error: 'bio must be a string' }, { status: 400 }) - } - if (academicHistory !== undefined) { - if ( - !Array.isArray(academicHistory) || - academicHistory.length > 20 || - !academicHistory.every( - (entry: unknown) => - entry !== null && - typeof entry === 'object' && - typeof (entry as Record).year === 'string' && - typeof (entry as Record).title === 'string', - ) - ) { - return NextResponse.json( - { error: 'academicHistory must be an array of objects with string year and title (max 20 items)' }, - { status: 400 }, - ) - } + const { name, department, subjects, phone, bio, academicHistory } = body; + + const parsed = updateSchema.safeParse(body); + + if (!parsed.success) { + return NextResponse.json( + { error: parsed.error.flatten() }, + { status: 400 }, + ); } - const updatePayload: Record = { name, subjects } - if (department !== undefined) updatePayload.department = department - if (phone !== undefined) updatePayload.phone = phone - if (bio !== undefined) updatePayload.bio = bio - if (academicHistory !== undefined) updatePayload.academicHistory = academicHistory + const data = parsed.data; + + const updatePayload = Object.fromEntries( + Object.entries(data).filter(([_, v]) => v !== undefined), + ); const teacher = await Teacher.findOneAndUpdate( { clerkId: userId }, { $set: updatePayload }, - { new: true } - ) - + { new: true }, + ); + if (!teacher) { - return NextResponse.json({ error: 'Teacher not found' }, { status: 404 }) + return NextResponse.json({ error: "Teacher not found" }, { status: 404 }); } - return NextResponse.json(teacher) + return NextResponse.json(teacher); } catch (error) { if (error instanceof Error) { - console.error('PUT /api/profile error:', error.message) + console.error("PUT /api/profile error:", error.message); } - return NextResponse.json({ error: 'Internal server error' }, { status: 500 }) + return NextResponse.json( + { error: "Internal server error" }, + { status: 500 }, + ); } } diff --git a/app/dashboard/profile/ProfileClient.tsx b/app/dashboard/profile/ProfileClient.tsx index 71e235e..722b319 100644 --- a/app/dashboard/profile/ProfileClient.tsx +++ b/app/dashboard/profile/ProfileClient.tsx @@ -1,107 +1,127 @@ -'use client' +"use client"; -import { useEffect, useRef, useState } from 'react' +import { useEffect, useRef, useState } from "react"; import Image from "next/image"; -import { useForm } from 'react-hook-form' -import { useUser } from '@clerk/nextjs' -import { Button } from '@/components/ui/Button' -import { useToast } from '@/components/ui/Toast' -import { CardSkeleton } from '@/components/ui/Skeleton' +import { useForm } from "react-hook-form"; +import { useUser } from "@clerk/nextjs"; +import { Button } from "@/components/ui/Button"; +import { useToast } from "@/components/ui/Toast"; +import { CardSkeleton } from "@/components/ui/Skeleton"; import { ConfirmModal } from "@/components/ui/ConfirmModal"; interface AcademicHistoryEntry { - year: string - title: string - description?: string + year: string; + title: string; + description?: string; } interface TeacherProfile { - name: string - email: string - department: string - subjects: string[] - phone: string - bio: string - academicHistory: AcademicHistoryEntry[] - createdAt?: string + name: string; + email: string; + department: string; + subjects: string[]; + phone: string; + bio: string; + academicHistory: AcademicHistoryEntry[]; + createdAt?: string; } interface FormData { - name: string - department: string - subjectsRaw: string - phone: string - bio: string + name: string; + department: string; + subjectsRaw: string; + phone: string; + bio: string; } interface TimelineFormData { - year: string - title: string - description: string + year: string; + title: string; + description: string; } export function ProfileClient() { - const { user } = useUser() - const { toast } = useToast() - const [loading, setLoading] = useState(true) - const [profile, setProfile] = useState(null) - const [editing, setEditing] = useState(false) - const [avatarUploading, setAvatarUploading] = useState(false) - const [timelineModalOpen, setTimelineModalOpen] = useState(false) - const [editingEntry, setEditingEntry] = useState<{ index: number; entry: AcademicHistoryEntry } | null>(null) + const { user } = useUser(); + const { toast } = useToast(); + const [loading, setLoading] = useState(true); + const [profile, setProfile] = useState(null); + const [editing, setEditing] = useState(false); + const [avatarUploading, setAvatarUploading] = useState(false); + const [timelineModalOpen, setTimelineModalOpen] = useState(false); + const [editingEntry, setEditingEntry] = useState<{ + index: number; + entry: AcademicHistoryEntry; + } | null>(null); const [deleteEntryIndex, setDeleteEntryIndex] = useState(null); - const avatarInputRef = useRef(null) + const avatarInputRef = useRef(null); - const { register, handleSubmit, reset, formState: { isSubmitting } } = useForm() + const { + register, + handleSubmit, + reset, + formState: { isSubmitting }, + } = useForm(); const { register: tlRegister, handleSubmit: tlHandleSubmit, reset: tlReset, formState: { isSubmitting: tlSubmitting, errors: tlErrors }, - } = useForm() + } = useForm(); useEffect(() => { async function load() { - setLoading(true) + setLoading(true); try { - const res = await fetch('/api/profile') - if (!res.ok) throw new Error(`Failed to load profile: ${res.status}`) - const data = await res.json() - setProfile(data) + const res = await fetch("/api/profile"); + if (!res.ok) throw new Error(`Failed to load profile: ${res.status}`); + const data = await res.json(); + setProfile(data); reset({ name: data.name, department: data.department, - subjectsRaw: (data.subjects ?? []).join(', '), + subjectsRaw: (data.subjects ?? []).join(", "), phone: data.phone, bio: data.bio, - }) + }); } catch (err) { - toast(err instanceof Error ? err.message : 'Failed to load profile', 'error') + toast( + err instanceof Error ? err.message : "Failed to load profile", + "error", + ); } finally { - setLoading(false) + setLoading(false); } } - load() - }, [reset, toast]) + load(); + }, [reset, toast]); // ── Avatar upload via Clerk ── const handleAvatarChange = async (e: React.ChangeEvent) => { - const file = e.target.files?.[0] - if (!file || !user) return - if (!file.type.startsWith('image/')) { toast('Please select an image file', 'error'); return } - if (file.size > 5 * 1024 * 1024) { toast('Image must be under 5 MB', 'error'); return } + const file = e.target.files?.[0]; + if (!file || !user) return; + if (!file.type.startsWith("image/")) { + toast("Please select an image file", "error"); + return; + } + if (file.size > 5 * 1024 * 1024) { + toast("Image must be under 5 MB", "error"); + return; + } - setAvatarUploading(true) + setAvatarUploading(true); try { - await user.setProfileImage({ file }) - toast('Avatar updated!', 'success') + await user.setProfileImage({ file }); + toast("Avatar updated!", "success"); } catch (err) { - toast(err instanceof Error ? err.message : 'Failed to upload avatar', 'error') + toast( + err instanceof Error ? err.message : "Failed to upload avatar", + "error", + ); } finally { - setAvatarUploading(false) - e.target.value = '' + setAvatarUploading(false); + e.target.value = ""; } - } + }; // ── Profile form submit ── const onSubmit = async (data: FormData) => { @@ -173,32 +193,40 @@ export function ProfileClient() { }; const openAddEntry = () => { - setEditingEntry(null) - tlReset({ year: String(new Date().getFullYear()), title: '', description: '' }) - setTimelineModalOpen(true) - } + setEditingEntry(null); + tlReset({ + year: String(new Date().getFullYear()), + title: "", + description: "", + }); + setTimelineModalOpen(true); + }; const openEditEntry = (index: number, entry: AcademicHistoryEntry) => { - setEditingEntry({ index, entry }) - tlReset({ year: entry.year, title: entry.title, description: entry.description ?? '' }) - setTimelineModalOpen(true) - } + setEditingEntry({ index, entry }); + tlReset({ + year: entry.year, + title: entry.title, + description: entry.description ?? "", + }); + setTimelineModalOpen(true); + }; const onTimelineSubmit = async (data: TimelineFormData) => { - const current = [...(profile?.academicHistory ?? [])] + const current = [...(profile?.academicHistory ?? [])]; if (editingEntry !== null) { - current[editingEntry.index] = data + current[editingEntry.index] = data; } else { - current.push(data) + current.push(data); } // sort descending by year - current.sort((a, b) => b.year.localeCompare(a.year)) + current.sort((a, b) => b.year.localeCompare(a.year)); const success = await saveHistory(current); if (success) { toast("Academic history saved!", "success"); setTimelineModalOpen(false); } - } + }; const executeDeleteEntry = async () => { if (deleteEntryIndex === null) return; @@ -216,7 +244,7 @@ export function ProfileClient() { - ) + ); } return ( diff --git a/lib/schemas.ts b/lib/schemas.ts new file mode 100644 index 0000000..d516488 --- /dev/null +++ b/lib/schemas.ts @@ -0,0 +1,18 @@ +import { z } from "zod"; + +export const updateSchema = z.object({ + name: z.string().min(1).optional(), + department: z.string().optional(), + subjects: z.array(z.string()).optional(), + phone: z.string().optional(), + bio: z.string().optional(), + academicHistory: z + .array( + z.object({ + year: z.string(), + title: z.string(), + }), + ) + .max(20) + .optional(), +}); diff --git a/package-lock.json b/package-lock.json index 54cfc6a..b57d7f8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1782,7 +1782,7 @@ "version": "19.2.14", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "csstype": "^3.2.2" @@ -2948,7 +2948,7 @@ "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/d3-array": { @@ -6104,6 +6104,7 @@ "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "dev": true, "license": "MIT" }, "node_modules/react-redux": { diff --git a/tsconfig.json b/tsconfig.json index 3a13f90..4e424f9 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,7 +1,11 @@ { "compilerOptions": { "target": "ES2017", - "lib": ["dom", "dom.iterable", "esnext"], + "lib": [ + "dom", + "dom.iterable", + "esnext" + ], "allowJs": true, "skipLibCheck": true, "strict": true, @@ -19,7 +23,9 @@ } ], "paths": { - "@/*": ["./*"] + "@/*": [ + "./*" + ] } }, "include": [ @@ -28,7 +34,10 @@ "**/*.tsx", ".next/types/**/*.ts", ".next/dev/types/**/*.ts", - "**/*.mts" + "**/*.mts", + ".next/dev/dev/types/**/*.ts" ], - "exclude": ["node_modules"] + "exclude": [ + "node_modules" + ] } From 70a35dc482da1a6726b43b4367c80a099b0ac0a4 Mon Sep 17 00:00:00 2001 From: sahil-singh1107 Date: Sat, 18 Apr 2026 23:52:38 +0530 Subject: [PATCH 2/6] fix profileclient --- app/dashboard/profile/ProfileClient.tsx | 70 ++++++++++++++++++------- 1 file changed, 51 insertions(+), 19 deletions(-) diff --git a/app/dashboard/profile/ProfileClient.tsx b/app/dashboard/profile/ProfileClient.tsx index 722b319..e04ef96 100644 --- a/app/dashboard/profile/ProfileClient.tsx +++ b/app/dashboard/profile/ProfileClient.tsx @@ -9,6 +9,27 @@ import { useToast } from "@/components/ui/Toast"; import { CardSkeleton } from "@/components/ui/Skeleton"; import { ConfirmModal } from "@/components/ui/ConfirmModal"; +function formatZodError(error: any): string { + if (!error) return "Unknown error"; + + const messages: string[] = []; + + if (error.formErrors?.length) { + messages.push(...error.formErrors); + } + + if (error.fieldErrors) { + for (const key in error.fieldErrors) { + const errs = error.fieldErrors[key]; + if (errs) { + messages.push(...errs.map((e: string) => `${key}: ${e}`)); + } + } + } + + return messages.join(", "); +} + interface AcademicHistoryEntry { year: string; title: string; @@ -46,6 +67,7 @@ export function ProfileClient() { const [loading, setLoading] = useState(true); const [profile, setProfile] = useState(null); const [editing, setEditing] = useState(false); + const [savingHistory, setSavingHistory] = useState(false); const [avatarUploading, setAvatarUploading] = useState(false); const [timelineModalOpen, setTimelineModalOpen] = useState(false); const [editingEntry, setEditingEntry] = useState<{ @@ -131,27 +153,29 @@ export function ProfileClient() { .map((s) => s.trim()) .filter(Boolean); const subjects = Array.from(new Set(subjectsArray)); + const payload: any = {}; + + if (data.name.trim()) payload.name = data.name.trim(); + if (data.department.trim()) payload.department = data.department.trim(); + if (subjects.length) payload.subjects = subjects; + if (data.phone.trim()) payload.phone = data.phone.trim(); + if (data.bio.trim()) payload.bio = data.bio.trim(); + try { const res = await fetch("/api/profile", { method: "PUT", headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - name: data.name, - department: data.department, - subjects, - phone: data.phone, - bio: data.bio, - academicHistory: profile?.academicHistory ?? [], - }), + body: JSON.stringify(payload), }); - if (res.ok) { - const updated = await res.json(); - setProfile(updated); - toast("Profile updated!", "success"); - setEditing(false); - } else { - toast("Failed to update profile", "error"); + const result = await res.json(); + if (!res.ok) { + toast(formatZodError(result.error), "error"); + return; } + + setProfile(result); + toast("Profile updated!", "success"); + setEditing(false); } catch (error) { toast( `Network error: ${error instanceof Error ? error.message : "Failed to update profile"}`, @@ -170,7 +194,6 @@ export function ProfileClient() { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ - ...profile, academicHistory: history, }), }); @@ -220,8 +243,10 @@ export function ProfileClient() { current.push(data); } // sort descending by year - current.sort((a, b) => b.year.localeCompare(a.year)); + current.sort((a, b) => Number(b.year) - Number(a.year)); + setSavingHistory(true); const success = await saveHistory(current); + setSavingHistory(false); if (success) { toast("Academic history saved!", "success"); setTimelineModalOpen(false); @@ -423,7 +448,9 @@ export function ProfileClient() { Subjects (comma-separated) val.trim().length > 0 || "Required", + })} className="input w-full" placeholder="Mathematics, Physics, Programming" /> @@ -497,7 +524,12 @@ export function ProfileClient() {

Academic History

-