From dab3d81afb8758867788b6f90f1aee54c875c915 Mon Sep 17 00:00:00 2001 From: Chenxin Yan Date: Wed, 15 Oct 2025 02:32:34 -0400 Subject: [PATCH 01/39] fix(server): update userCourses Schema --- packages/server/convex/schemas/courses.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/server/convex/schemas/courses.ts b/packages/server/convex/schemas/courses.ts index 33b739f7..1df38627 100644 --- a/packages/server/convex/schemas/courses.ts +++ b/packages/server/convex/schemas/courses.ts @@ -31,7 +31,7 @@ const prerequisites = v.union( const userCourses = { userId: v.string(), - courseId: v.id("courses"), + courseCode: v.string(), // CSCI-UA 101 title: v.string(), year: v.number(), // 2025 term: v.union( From 31a427e3e9f1ef5d7acf8c949c91c94b64a5c809 Mon Sep 17 00:00:00 2001 From: Emily Silkina Date: Mon, 3 Nov 2025 11:45:07 -0500 Subject: [PATCH 02/39] Created account page --- apps/web/src/app/dashboard/account/page.tsx | 49 +++++++++++++++++++ .../components/event-calendar/agenda-view.tsx | 0 2 files changed, 49 insertions(+) create mode 100644 apps/web/src/app/dashboard/account/page.tsx create mode 100644 apps/web/src/components/event-calendar/agenda-view.tsx diff --git a/apps/web/src/app/dashboard/account/page.tsx b/apps/web/src/app/dashboard/account/page.tsx new file mode 100644 index 00000000..629c89cf --- /dev/null +++ b/apps/web/src/app/dashboard/account/page.tsx @@ -0,0 +1,49 @@ +"use client"; + +import { useUser } from "@clerk/nextjs"; +import { useQuery, useMutation } from "convex/react"; +import { api } from "@albert-plus/server/convex/_generated/api"; + +export default function ProfilePage() { + const { user, isLoaded } = useUser(); + const student = useQuery(api.students.getCurrentStudent); + const upsert = useMutation(api.students.upsertCurrentStudent); + + if (!isLoaded) return
Loading...
; + + return ( +
+

Profile

+ +

+ Name: {user?.fullName || "Unknown User"} +

+

+ Email: {user?.primaryEmailAddress?.emailAddress || ""} +

+ + {student && ( + <> +

+ School: {student.school} +

+

+ Programs:{" "} + {student.programs?.length > 0 + ? student.programs.join(", ") + : "None"} +

+ + )} + + {!student && ( + <> +

+ HIIII +

+ + + )} +
+ ); +} diff --git a/apps/web/src/components/event-calendar/agenda-view.tsx b/apps/web/src/components/event-calendar/agenda-view.tsx new file mode 100644 index 00000000..e69de29b From 2cca915a6d4d90b74c53e1025dfbd741fce091e0 Mon Sep 17 00:00:00 2001 From: Chenxin Yan Date: Wed, 5 Nov 2025 16:31:35 -0500 Subject: [PATCH 03/39] add account header --- apps/web/src/app/dashboard/@header/account/page.tsx | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 apps/web/src/app/dashboard/@header/account/page.tsx diff --git a/apps/web/src/app/dashboard/@header/account/page.tsx b/apps/web/src/app/dashboard/@header/account/page.tsx new file mode 100644 index 00000000..e3c55fa5 --- /dev/null +++ b/apps/web/src/app/dashboard/@header/account/page.tsx @@ -0,0 +1,5 @@ +import { AppHeader } from "@/app/dashboard/components/app-header"; + +export default function AdminHeader() { + return ; +} From eaff8d23994e934a78f0a31bea5447e8c81a9bc6 Mon Sep 17 00:00:00 2001 From: Chenxin Yan Date: Wed, 5 Nov 2025 16:43:08 -0500 Subject: [PATCH 04/39] add table joint for get current student --- packages/server/convex/students.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/packages/server/convex/students.ts b/packages/server/convex/students.ts index 2077cc88..1a2e0b7e 100644 --- a/packages/server/convex/students.ts +++ b/packages/server/convex/students.ts @@ -1,14 +1,23 @@ import { omit } from "convex-helpers"; +import { getAll } from "convex-helpers/server/relationships"; import { protectedMutation, protectedQuery } from "./helpers/auth"; import { students } from "./schemas/students"; export const getCurrentStudent = protectedQuery({ args: {}, handler: async (ctx) => { - return await ctx.db + const student = await ctx.db .query("students") .withIndex("by_user_id", (q) => q.eq("userId", ctx.user.subject)) .unique(); + if (!student) return null; + + const programs = await getAll(ctx.db, student.programs); + + return { + ...student, + programs, + }; }, }); From 659d64c925384194afb06f6826edd23b8a0f2a46 Mon Sep 17 00:00:00 2001 From: Chenxin Yan Date: Wed, 5 Nov 2025 17:16:46 -0500 Subject: [PATCH 05/39] update query --- apps/web/src/app/dashboard/account/page.tsx | 25 +++++++++------------ 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/apps/web/src/app/dashboard/account/page.tsx b/apps/web/src/app/dashboard/account/page.tsx index 629c89cf..02f87f4b 100644 --- a/apps/web/src/app/dashboard/account/page.tsx +++ b/apps/web/src/app/dashboard/account/page.tsx @@ -1,15 +1,19 @@ "use client"; -import { useUser } from "@clerk/nextjs"; -import { useQuery, useMutation } from "convex/react"; import { api } from "@albert-plus/server/convex/_generated/api"; +import { useUser } from "@clerk/nextjs"; +import { useConvexAuth, useMutation, useQuery } from "convex/react"; export default function ProfilePage() { - const { user, isLoaded } = useUser(); - const student = useQuery(api.students.getCurrentStudent); - const upsert = useMutation(api.students.upsertCurrentStudent); + const { isAuthenticated } = useConvexAuth(); + const { user } = useUser(); - if (!isLoaded) return
Loading...
; + const student = useQuery( + api.students.getCurrentStudent, + isAuthenticated ? {} : "skip", + ); + + const upsert = useMutation(api.students.upsertCurrentStudent); return (
@@ -36,14 +40,7 @@ export default function ProfilePage() { )} - {!student && ( - <> -

- HIIII -

- - - )} + {!student &&

HIIII

}
); } From 6c5ee7b8ce92bf0d64df88542d61980598543765 Mon Sep 17 00:00:00 2001 From: Emily Silkina Date: Tue, 11 Nov 2025 20:31:58 -0500 Subject: [PATCH 06/39] Fixed program name --- apps/web/src/app/dashboard/account/page.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/web/src/app/dashboard/account/page.tsx b/apps/web/src/app/dashboard/account/page.tsx index 02f87f4b..edadabe4 100644 --- a/apps/web/src/app/dashboard/account/page.tsx +++ b/apps/web/src/app/dashboard/account/page.tsx @@ -34,7 +34,7 @@ export default function ProfilePage() {

Programs:{" "} {student.programs?.length > 0 - ? student.programs.join(", ") + ? student.programs.map(p => p.name).join(", ") : "None"}

From 6064be0c1929dbe81ad3651e22ef7963c517118c Mon Sep 17 00:00:00 2001 From: Chenxin Yan Date: Wed, 12 Nov 2025 22:01:45 -0500 Subject: [PATCH 07/39] lint --- apps/web/src/app/dashboard/account/page.tsx | 2 +- packages/server/convex/students.ts | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/apps/web/src/app/dashboard/account/page.tsx b/apps/web/src/app/dashboard/account/page.tsx index edadabe4..6fda13a3 100644 --- a/apps/web/src/app/dashboard/account/page.tsx +++ b/apps/web/src/app/dashboard/account/page.tsx @@ -34,7 +34,7 @@ export default function ProfilePage() {

Programs:{" "} {student.programs?.length > 0 - ? student.programs.map(p => p.name).join(", ") + ? student.programs.map((p) => p.name).join(", ") : "None"}

diff --git a/packages/server/convex/students.ts b/packages/server/convex/students.ts index 74bcaf2f..cc9003c6 100644 --- a/packages/server/convex/students.ts +++ b/packages/server/convex/students.ts @@ -1,7 +1,6 @@ import { ConvexError } from "convex/values"; import { omit } from "convex-helpers"; -import { getAll } from "convex-helpers/server/relationships"; import { getOneFrom } from "convex-helpers/server/relationships"; import { partial } from "convex-helpers/validators"; import { protectedMutation, protectedQuery } from "./helpers/auth"; From 8bccc6337c3423d43214d1e1ad3ff7db202243bc Mon Sep 17 00:00:00 2001 From: Chenxin Yan Date: Wed, 12 Nov 2025 22:31:40 -0500 Subject: [PATCH 08/39] adadd program table joints to get student --- packages/server/convex/students.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/server/convex/students.ts b/packages/server/convex/students.ts index cc9003c6..3bee6842 100644 --- a/packages/server/convex/students.ts +++ b/packages/server/convex/students.ts @@ -1,5 +1,5 @@ import { ConvexError } from "convex/values"; -import { omit } from "convex-helpers"; +import { asyncMap, omit } from "convex-helpers"; import { getOneFrom } from "convex-helpers/server/relationships"; import { partial } from "convex-helpers/validators"; @@ -26,9 +26,16 @@ export const getCurrentStudent = protectedQuery({ "_id", ); + const programs = await asyncMap(student.programs, async (programId) => { + return await ctx.db.get(programId); + }); + + const validPrograms = programs.filter((p) => p !== null); + return { ...student, school, + programs: validPrograms, }; }, }); From 4ba8f578bd0cf1ce7a9c140c421ce757af89207e Mon Sep 17 00:00:00 2001 From: Emily Silkina Date: Thu, 13 Nov 2025 11:21:06 -0500 Subject: [PATCH 09/39] Added edit profile component --- apps/web/src/app/dashboard/account/page.tsx | 78 ++++++++++++++++++--- apps/web/tsconfig.json | 14 +++- 2 files changed, 80 insertions(+), 12 deletions(-) diff --git a/apps/web/src/app/dashboard/account/page.tsx b/apps/web/src/app/dashboard/account/page.tsx index 6fda13a3..b5c59c43 100644 --- a/apps/web/src/app/dashboard/account/page.tsx +++ b/apps/web/src/app/dashboard/account/page.tsx @@ -3,6 +3,19 @@ import { api } from "@albert-plus/server/convex/_generated/api"; import { useUser } from "@clerk/nextjs"; import { useConvexAuth, useMutation, useQuery } from "convex/react"; +import { + Dialog, + DialogClose, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog" +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; export default function ProfilePage() { const { isAuthenticated } = useConvexAuth(); @@ -15,6 +28,15 @@ export default function ProfilePage() { const upsert = useMutation(api.students.upsertCurrentStudent); + if (student) { + console.log("STUDENTTT"); + console.log(student.programs); + } else{ + console.log("STUDENT NULL"); + } + + + return (

Profile

@@ -29,18 +51,56 @@ export default function ProfilePage() { {student && ( <>

- School: {student.school} -

-

- Programs:{" "} - {student.programs?.length > 0 - ? student.programs.map((p) => p.name).join(", ") - : "None"} -

+ School:{" "} + {student.school + ? `${student.school.name} (${ + student.school.level + ? student.school.level.charAt(0).toUpperCase() + student.school.level.slice(1).toLowerCase() + : "" + })` + : "N/A"} +

+

+ Programs:{" "} + {student.programs?.length > 0 + ? student.programs.map((p) => p.name).join(", ") + : "None"} +

)} - {!student &&

HIIII

} + +
+ + + + + + Edit profile + + Make changes to your profile here. Click save when you're + done. + + +
+
+ + +
+
+ + +
+
+ + + + + + +
+
+
); } diff --git a/apps/web/tsconfig.json b/apps/web/tsconfig.json index 19c51c83..b575f7da 100644 --- a/apps/web/tsconfig.json +++ b/apps/web/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": { - "@/*": ["./src/*"] + "@/*": [ + "./src/*" + ] } }, "include": [ @@ -29,5 +35,7 @@ ".next/types/**/*.ts", ".next/dev/types/**/*.ts" ], - "exclude": ["node_modules"] + "exclude": [ + "node_modules" + ] } From b0503f5b9e2d15e2fd644646455e2cdab6a06907 Mon Sep 17 00:00:00 2001 From: Emily Silkina Date: Thu, 13 Nov 2025 12:59:27 -0500 Subject: [PATCH 10/39] Added form to popup --- .../account/components/editProfile.tsx | 554 ++++++++++++++++++ apps/web/src/app/dashboard/account/page.tsx | 53 +- 2 files changed, 556 insertions(+), 51 deletions(-) create mode 100644 apps/web/src/app/dashboard/account/components/editProfile.tsx diff --git a/apps/web/src/app/dashboard/account/components/editProfile.tsx b/apps/web/src/app/dashboard/account/components/editProfile.tsx new file mode 100644 index 00000000..b8273edf --- /dev/null +++ b/apps/web/src/app/dashboard/account/components/editProfile.tsx @@ -0,0 +1,554 @@ +"use client"; + +import { api } from "@albert-plus/server/convex/_generated/api"; +import type { Doc, Id } from "@albert-plus/server/convex/_generated/dataModel"; +import { useForm } from "@tanstack/react-form"; +import { + useConvexAuth, + useMutation, + usePaginatedQuery, + useQuery, +} from "convex/react"; +import type { FunctionArgs } from "convex/server"; +import { useRouter } from "next/navigation"; +import React, { Activity } from "react"; +import { toast } from "sonner"; +import z from "zod"; +import MultipleSelector from "@/app/onboarding/component/multiselect"; +import { SchoolCombobox } from "@/app/onboarding/component/school-combobox"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { + FieldContent, + FieldError, + FieldGroup, + FieldLabel, + Field as UIField, +} from "@/components/ui/field"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { + Tooltip, + TooltipContent, + TooltipTrigger, +} from "@/components/ui/tooltip"; +import DegreeProgreeUpload from "@/modules/report-parsing/components/degree-progress-upload"; +import type { UserCourse } from "@/modules/report-parsing/types"; +import type { StartingTerm } from "@/modules/report-parsing/utils/parse-starting-term"; +import { getTermAfterSemesters, type Term } from "@/utils/term"; +import { + Dialog, + DialogClose, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog" + +const dateSchema = z.object({ + year: z.number().int().min(2000).max(2100), + term: z.union([ + z.literal("spring"), + z.literal("fall"), + z.literal("j-term"), + z.literal("summer"), + ]), +}); + +const onboardingFormSchema = z + .object({ + school: z.string({ + error: (issue) => + issue.input === undefined ? "Please select a school" : "Invalid input", + }), + programs: z.array(z.string()).min(1, "At least one program is required"), + startingDate: dateSchema, + expectedGraduationDate: dateSchema, + // User courses + userCourses: z.array(z.object()).optional(), + }) + .refine( + (data) => { + const startYear = data.startingDate.year; + const startTerm = data.startingDate.term; + const endYear = data.expectedGraduationDate.year; + const endTerm = data.expectedGraduationDate.term; + + // Convert to comparable numbers (spring=0, fall=1) + const startValue = startYear * 2 + (startTerm === "fall" ? 1 : 0); + const endValue = endYear * 2 + (endTerm === "fall" ? 1 : 0); + + return endValue > startValue; + }, + { + message: "Expected graduation date must be after starting date", + path: ["expectedGraduationDate"], + }, + ); + +export function EditProfilePopup() { + const router = useRouter(); + const { isAuthenticated } = useConvexAuth(); + const [isFileLoaded, setIsFileLoaded] = React.useState(false); + const [currentStep, setCurrentStep] = React.useState<1 | 2>(1); + + // actions + const upsertStudent = useMutation(api.students.upsertCurrentStudent); + const importUserCourses = useMutation(api.userCourses.importUserCourses); + + // schools + const schools = useQuery( + api.schools.getSchools, + isAuthenticated ? {} : ("skip" as const), + ); + + // Programs + const [programsQuery, setProgramsQuery] = React.useState( + undefined, + ); + const { + results: programs, + status: programsStatus, + loadMore: programsLoadMore, + } = usePaginatedQuery( + api.programs.getPrograms, + isAuthenticated ? { query: programsQuery } : ("skip" as const), + { initialNumItems: 20 }, + ); + + const programOptions = React.useMemo( + () => + (programs ?? []).map((program) => ({ + value: program._id, + label: program.name, + })), + [programs], + ); + + const handleSearchPrograms = React.useCallback( + async (value: string) => { + const trimmed = value.trim(); + setProgramsQuery(trimmed.length === 0 ? undefined : trimmed); + return programOptions; + }, + [programOptions], + ); + + const handleLoadMorePrograms = React.useCallback(() => { + if (programsStatus === "CanLoadMore") { + void programsLoadMore(10); + } + }, [programsStatus, programsLoadMore]); + + const currentYear = React.useMemo(() => new Date().getFullYear(), []); + const defaultTerm = React.useMemo(() => { + const month = new Date().getMonth(); + return month >= 6 ? "fall" : "spring"; + }, []); + const defaultStartingDate = { + year: currentYear, + term: defaultTerm, + }; + const defaultExpectedGraduation = getTermAfterSemesters( + { + term: defaultTerm, + year: currentYear, + }, + 14, + ); + + // Generate year options: currentYear ± 4 years + const yearOptions = React.useMemo(() => { + const years: number[] = []; + for (let i = currentYear - 5; i <= currentYear + 5; i++) { + years.push(i); + } + return years; + }, [currentYear]); + + const form = useForm({ + defaultValues: { + // student data + school: undefined as Id<"schools"> | undefined, + programs: [] as Id<"programs">[], + startingDate: defaultStartingDate as Doc<"students">["startingDate"], + expectedGraduationDate: + defaultExpectedGraduation as Doc<"students">["expectedGraduationDate"], + // user courses + userCourses: undefined as + | FunctionArgs["courses"] + | undefined, + }, + validators: { + onSubmit: ({ value }) => { + const result = onboardingFormSchema.safeParse(value); + if (!result.success) { + const fieldErrors: Record = {}; + for (const issue of result.error.issues) { + const path = issue.path.join("."); + fieldErrors[path] = [{ message: issue.message }]; + } + return { + fields: fieldErrors, + }; + } + return undefined; + }, + }, + onSubmit: async ({ value }) => { + try { + toast.success("Onboarding completed."); + router.push("/dashboard"); + + await upsertStudent({ + school: value.school as Id<"schools">, + programs: value.programs, + startingDate: value.startingDate, + expectedGraduationDate: value.expectedGraduationDate, + }); + + if (value.userCourses) { + await importUserCourses({ courses: value.userCourses }); + } + } catch (error) { + console.error("Error completing onboarding:", error); + toast.error("Could not complete onboarding. Please try again."); + } + }, + }); + + function handleConfirmImport( + coursesToImport: UserCourse[], + startingTerm: StartingTerm | null, + ) { + if (coursesToImport.length === 0) { + return; + } + + form.setFieldValue("userCourses", coursesToImport); + + if (startingTerm) { + form.setFieldValue("startingDate", startingTerm); + form.setFieldValue( + "expectedGraduationDate", + getTermAfterSemesters(startingTerm, 14), + ); + } + + setIsFileLoaded(true); + } + + function Form() { + return ( +
{ + e.preventDefault(); + e.stopPropagation(); + form.handleSubmit(); + }} + className="space-y-6" + > + + + + + Degree Progress Report + + + + ? + + + +

+ We do not store your degree progress report. Need help + finding it?{" "} + + View NYU's guide + +

+
+
+
+ + Upload your degree progress report (PDF) so we can help you track + your academic progress and suggest courses. + +
+ + { + form.setFieldValue("userCourses", undefined); + form.setFieldValue("startingDate", defaultStartingDate); + form.setFieldValue( + "expectedGraduationDate", + defaultExpectedGraduation, + ); + setIsFileLoaded(false); + }} + /> + + +
+ + +
+
+
+
+ + + + {/* + Academic Information + + Tell us about your academic background so we can personalize your + experience. + + */} + + + {/* school */} + + {(field) => { + return ( + + + What school or college of NYU do you go to? + + + field.handleChange(value)} + /> + + + + ); + }} + + + {/* programs (multi-select) */} + + {(field) => { + const selected = (field.state.value ?? []).map((p) => ({ + value: p, + label: programOptions.find((val) => val.value === p) + ?.label as string, + })); + return ( + + + What's your major(s) and minor(s)? + + + + field.handleChange( + opts.map((o) => o.value as Id<"programs">), + ) + } + defaultOptions={programOptions} + options={programOptions} + delay={300} + onSearch={handleSearchPrograms} + triggerSearchOnFocus + placeholder="Select your programs" + commandProps={{ label: "Select programs" }} + onListReachEnd={handleLoadMorePrograms} + emptyIndicator={ +

+ No programs found +

+ } + /> +
+ +
+ ); + }} +
+ + {/* Program timeline - start and end dates in one row */} + + When does your program end? +
+
+ + {/* Expected graduation date section */} +
+
+ Expected graduation +
+
+ {/* expectedGraduationDate.term */} + + {(field) => ( + + )} + + {/* expectedGraduationDate.year */} + + {(field) => ( + + )} + +
+
+
+
+ + {/* Aggregate object-level errors (from Zod refine, etc.) */} + + {(field) => } + +
+
+
+ +
+ + +
+
+
+
+
+ ) + } + + return ( +
+ +
+ + + + + + Edit profile + + Make changes to your profile here. Click save when you're + done. + + + + {/*
+
+ + +
+
+ + +
+
*/} + + + + + + +
+
+
+ + ); +} diff --git a/apps/web/src/app/dashboard/account/page.tsx b/apps/web/src/app/dashboard/account/page.tsx index b5c59c43..a1e306e1 100644 --- a/apps/web/src/app/dashboard/account/page.tsx +++ b/apps/web/src/app/dashboard/account/page.tsx @@ -3,19 +3,10 @@ import { api } from "@albert-plus/server/convex/_generated/api"; import { useUser } from "@clerk/nextjs"; import { useConvexAuth, useMutation, useQuery } from "convex/react"; -import { - Dialog, - DialogClose, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, - DialogTrigger, -} from "@/components/ui/dialog" import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; +import {EditProfilePopup} from "./components/editProfile"; export default function ProfilePage() { const { isAuthenticated } = useConvexAuth(); @@ -27,14 +18,6 @@ export default function ProfilePage() { ); const upsert = useMutation(api.students.upsertCurrentStudent); - - if (student) { - console.log("STUDENTTT"); - console.log(student.programs); - } else{ - console.log("STUDENT NULL"); - } - return ( @@ -68,39 +51,7 @@ export default function ProfilePage() {

)} - - -
- - - - - - Edit profile - - Make changes to your profile here. Click save when you're - done. - - -
-
- - -
-
- - -
-
- - - - - - -
-
-
+ ); } From 2af87c7d79a141023683b1991f3db3a4fdd46bf4 Mon Sep 17 00:00:00 2001 From: Emily Silkina Date: Thu, 13 Nov 2025 13:23:49 -0500 Subject: [PATCH 11/39] Populated edit profile with existing user data --- .../account/components/editProfile.tsx | 64 +++++++++++++------ .../onboarding/component/onboarding-form.tsx | 2 +- 2 files changed, 44 insertions(+), 22 deletions(-) diff --git a/apps/web/src/app/dashboard/account/components/editProfile.tsx b/apps/web/src/app/dashboard/account/components/editProfile.tsx index b8273edf..8d63f75d 100644 --- a/apps/web/src/app/dashboard/account/components/editProfile.tsx +++ b/apps/web/src/app/dashboard/account/components/editProfile.tsx @@ -59,6 +59,7 @@ import { DialogTitle, DialogTrigger, } from "@/components/ui/dialog" +import { useUser } from "@clerk/nextjs"; const dateSchema = z.object({ year: z.number().int().min(2000).max(2100), @@ -107,6 +108,13 @@ export function EditProfilePopup() { const [isFileLoaded, setIsFileLoaded] = React.useState(false); const [currentStep, setCurrentStep] = React.useState<1 | 2>(1); + const { user } = useUser(); + + const student = useQuery( + api.students.getCurrentStudent, + isAuthenticated ? {} : "skip", + ); + // actions const upsertStudent = useMutation(api.students.upsertCurrentStudent); const importUserCourses = useMutation(api.userCourses.importUserCourses); @@ -232,6 +240,29 @@ export function EditProfilePopup() { }, }); + React.useEffect(() => { + if (!student) return; + + // school: student.school can be an object or null + form.setFieldValue("school", student.school?._id ?? undefined); + + // programs: student.programs might be an array of objects or array of ids + const programIds: Id<"programs">[] = + (student.programs ?? []).map((p: any) => + typeof p === "string" ? (p as Id<"programs">) : (p?._id as Id<"programs">), + ); + + form.setFieldValue("programs", programIds); + + // dates — assume these are already shaped correctly + if (student.startingDate) { + form.setFieldValue("startingDate", student.startingDate); + } + if (student.expectedGraduationDate) { + form.setFieldValue("expectedGraduationDate", student.expectedGraduationDate); + } + }, [student]); + function handleConfirmImport( coursesToImport: UserCourse[], startingTerm: StartingTerm | null, @@ -263,7 +294,7 @@ export function EditProfilePopup() { }} className="space-y-6" > - + {/* @@ -343,9 +374,9 @@ export function EditProfilePopup() { - + */} - + {/* Academic Information @@ -362,7 +393,7 @@ export function EditProfilePopup() { return ( - What school or college of NYU do you go to? + What school or college of NYU do you attend? - What's your major(s) and minor(s)? + What are your major(s) and minor(s)? - -
- - -
-
+ + +
@@ -540,12 +562,12 @@ export function EditProfilePopup() { */} - + {/* - + */} diff --git a/apps/web/src/app/onboarding/component/onboarding-form.tsx b/apps/web/src/app/onboarding/component/onboarding-form.tsx index 33df742b..b0f95e74 100644 --- a/apps/web/src/app/onboarding/component/onboarding-form.tsx +++ b/apps/web/src/app/onboarding/component/onboarding-form.tsx @@ -351,7 +351,7 @@ export function OnboardingForm() { return ( - What school or college of NYU do you go to? + What school or college of NYU do you attend? Date: Thu, 13 Nov 2025 16:00:06 -0500 Subject: [PATCH 12/39] Added degree progress upload --- .../account/components/editProfile.tsx | 109 +++++++++++++++++- 1 file changed, 108 insertions(+), 1 deletion(-) diff --git a/apps/web/src/app/dashboard/account/components/editProfile.tsx b/apps/web/src/app/dashboard/account/components/editProfile.tsx index 8d63f75d..c3eda511 100644 --- a/apps/web/src/app/dashboard/account/components/editProfile.tsx +++ b/apps/web/src/app/dashboard/account/components/editProfile.tsx @@ -284,6 +284,78 @@ export function EditProfilePopup() { setIsFileLoaded(true); } + function DegreeProgressUpload() { + return ( +
{ + e.preventDefault(); + e.stopPropagation(); + form.handleSubmit(); + }} + className="space-y-6" + > + + + + + Degree Progress Report + + + + ? + + + +

+ We do not store your degree progress report. Need help + finding it?{" "} + + View NYU's guide + +

+
+
+
+ + Upload your degree progress report (PDF) so we can help you track + your academic progress and suggest courses. + +
+ + { + form.setFieldValue("userCourses", undefined); + form.setFieldValue("startingDate", defaultStartingDate); + form.setFieldValue( + "expectedGraduationDate", + defaultExpectedGraduation, + ); + setIsFileLoaded(false); + }} + /> + + + + +
+
+ +
+ ) + } + function Form() { return (
*/}
- + + + +
+ + + + + + Reupload Degree Progress Report + {/* + Reupload your degree progress report here. Click save when you're + done. + */} + + + {/*
+
+ + +
+
+ + +
+
*/} + {/* + + + + + */} +
+
+
+ ); } From 163a99eb716a2156792c40563c6b2909604c6e80 Mon Sep 17 00:00:00 2001 From: Emily Silkina Date: Thu, 13 Nov 2025 18:40:19 -0500 Subject: [PATCH 13/39] Updated toast --- apps/web/src/app/dashboard/account/components/editProfile.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/web/src/app/dashboard/account/components/editProfile.tsx b/apps/web/src/app/dashboard/account/components/editProfile.tsx index c3eda511..a6a9d613 100644 --- a/apps/web/src/app/dashboard/account/components/editProfile.tsx +++ b/apps/web/src/app/dashboard/account/components/editProfile.tsx @@ -220,8 +220,8 @@ export function EditProfilePopup() { }, onSubmit: async ({ value }) => { try { - toast.success("Onboarding completed."); - router.push("/dashboard"); + toast.success("Successfully updated profile."); + // router.push("/dashboard"); await upsertStudent({ school: value.school as Id<"schools">, From 11cfe85838c237d733c93d70bc2d8d7f6f748fad Mon Sep 17 00:00:00 2001 From: Chenxin Yan Date: Thu, 13 Nov 2025 20:40:04 -0500 Subject: [PATCH 14/39] lint --- .../account/components/editProfile.tsx | 540 +++++++++--------- 1 file changed, 273 insertions(+), 267 deletions(-) diff --git a/apps/web/src/app/dashboard/account/components/editProfile.tsx b/apps/web/src/app/dashboard/account/components/editProfile.tsx index a6a9d613..aaa76fc8 100644 --- a/apps/web/src/app/dashboard/account/components/editProfile.tsx +++ b/apps/web/src/app/dashboard/account/components/editProfile.tsx @@ -58,7 +58,7 @@ import { DialogHeader, DialogTitle, DialogTrigger, -} from "@/components/ui/dialog" +} from "@/components/ui/dialog"; import { useUser } from "@clerk/nextjs"; const dateSchema = z.object({ @@ -108,12 +108,12 @@ export function EditProfilePopup() { const [isFileLoaded, setIsFileLoaded] = React.useState(false); const [currentStep, setCurrentStep] = React.useState<1 | 2>(1); - const { user } = useUser(); + const { user } = useUser(); - const student = useQuery( - api.students.getCurrentStudent, - isAuthenticated ? {} : "skip", - ); + const student = useQuery( + api.students.getCurrentStudent, + isAuthenticated ? {} : "skip", + ); // actions const upsertStudent = useMutation(api.students.upsertCurrentStudent); @@ -247,21 +247,26 @@ export function EditProfilePopup() { form.setFieldValue("school", student.school?._id ?? undefined); // programs: student.programs might be an array of objects or array of ids - const programIds: Id<"programs">[] = - (student.programs ?? []).map((p: any) => - typeof p === "string" ? (p as Id<"programs">) : (p?._id as Id<"programs">), - ); + const programIds: Id<"programs">[] = (student.programs ?? []).map( + (p: any) => + typeof p === "string" + ? (p as Id<"programs">) + : (p?._id as Id<"programs">), + ); form.setFieldValue("programs", programIds); // dates — assume these are already shaped correctly if (student.startingDate) { - form.setFieldValue("startingDate", student.startingDate); + form.setFieldValue("startingDate", student.startingDate); } if (student.expectedGraduationDate) { - form.setFieldValue("expectedGraduationDate", student.expectedGraduationDate); + form.setFieldValue( + "expectedGraduationDate", + student.expectedGraduationDate, + ); } - }, [student]); + }, [student]); function handleConfirmImport( coursesToImport: UserCourse[], @@ -286,87 +291,86 @@ export function EditProfilePopup() { function DegreeProgressUpload() { return ( -
{ - e.preventDefault(); - e.stopPropagation(); - form.handleSubmit(); - }} - className="space-y-6" - > - - - - - Degree Progress Report - - - - ? - - - -

- We do not store your degree progress report. Need help - finding it?{" "} - { + e.preventDefault(); + e.stopPropagation(); + form.handleSubmit(); + }} + className="space-y-6" + > + + + + + Degree Progress Report + + + - View NYU's guide - -

-
-
-
- - Upload your degree progress report (PDF) so we can help you track - your academic progress and suggest courses. - -
- - { - form.setFieldValue("userCourses", undefined); - form.setFieldValue("startingDate", defaultStartingDate); - form.setFieldValue( - "expectedGraduationDate", - defaultExpectedGraduation, - ); - setIsFileLoaded(false); - }} - /> - - - - -
-
- -
- ) + + + +
+ + ); } function Form() { return ( -
{ - e.preventDefault(); - e.stopPropagation(); - form.handleSubmit(); - }} - className="space-y-6" - > - {/* + { + e.preventDefault(); + e.stopPropagation(); + form.handleSubmit(); + }} + className="space-y-6" + > + {/* @@ -448,183 +452,186 @@ export function EditProfilePopup() { */} - - - {/* + + + {/* Academic Information Tell us about your academic background so we can personalize your experience. */} - - - {/* school */} - - {(field) => { - return ( - - - What school or college of NYU do you attend? - - - field.handleChange(value)} - /> - - - - ); - }} - - - {/* programs (multi-select) */} - - {(field) => { - const selected = (field.state.value ?? []).map((p) => ({ - value: p, - label: programOptions.find((val) => val.value === p) - ?.label as string, - })); - return ( - - - What are your major(s) and minor(s)? - - - - field.handleChange( - opts.map((o) => o.value as Id<"programs">), - ) - } - defaultOptions={programOptions} - options={programOptions} - delay={300} - onSearch={handleSearchPrograms} - triggerSearchOnFocus - placeholder="Select your programs" - commandProps={{ label: "Select programs" }} - onListReachEnd={handleLoadMorePrograms} - emptyIndicator={ -

- No programs found -

- } - /> -
- -
- ); - }} -
- - {/* Program timeline - start and end dates in one row */} + - When does your program end? -
-
- - {/* Expected graduation date section */} -
-
- Expected graduation -
-
- {/* expectedGraduationDate.term */} - - {(field) => ( - + field.handleChange(val as Term) + } > - - - - Spring - Summer - Fall - Winter - - - )} - - {/* expectedGraduationDate.year */} - - {(field) => ( - + )} + + {/* expectedGraduationDate.year */} + + {(field) => ( + - )} - + + + + + {yearOptions.map((year) => ( + + {year} + + ))} + + + )} + +
- - {/* Aggregate object-level errors (from Zod refine, etc.) */} - - {(field) => } - + {/* Aggregate object-level errors (from Zod refine, etc.) */} + + {(field) => } + +
- -
- - - -
-
- - ) + + +
+
+ + ); } return (
-
- - - - - - Edit profile - - Make changes to your profile here. Click save when you're - done. - - - - {/*
+ + + + + + + Edit profile + + Make changes to your profile here. Click save when you're + done. + + + + {/*
@@ -634,31 +641,31 @@ export function EditProfilePopup() {
*/} - {/* + {/* */} -
- -
- - -
- - - - - - Reupload Degree Progress Report - {/* + +
+
+ + +
+ + + + + + Reupload Degree Progress Report + {/* Reupload your degree progress report here. Click save when you're done. */} - - - {/*
+ + + {/*
@@ -668,16 +675,15 @@ export function EditProfilePopup() {
*/} - {/* + {/* */} - - -
+ + +
- ); } From 3d02fe05185aea2838759b4b3ebe4a21932abd35 Mon Sep 17 00:00:00 2001 From: Emily Silkina Date: Sat, 15 Nov 2025 15:28:53 -0500 Subject: [PATCH 15/39] Added start date to edit profile --- .../account/components/editProfile.tsx | 71 ++++++++++++++++++- 1 file changed, 69 insertions(+), 2 deletions(-) diff --git a/apps/web/src/app/dashboard/account/components/editProfile.tsx b/apps/web/src/app/dashboard/account/components/editProfile.tsx index a6a9d613..001a9c64 100644 --- a/apps/web/src/app/dashboard/account/components/editProfile.tsx +++ b/apps/web/src/app/dashboard/account/components/editProfile.tsx @@ -525,16 +525,76 @@ export function EditProfilePopup() { {/* Program timeline - start and end dates in one row */} - When does your program end? + When does your program start and end?
+ {/* Starting Date section */} +
+
+ Start date +
+
+ {/* startingDate.term */} + + {(field) => ( + + )} + + {/* startingDate.year */} + + {(field) => ( + + )} + +
+
+ {/* Expected graduation date section */}
-
+
Expected graduation
+ {/* expectedGraduationDate.term */} {(field) => ( @@ -588,13 +648,20 @@ export function EditProfilePopup() {
+
+ + +
+ {/* Aggregate object-level errors (from Zod refine, etc.) */} {(field) => } + + From a8857dc9ce788b2bc432b99b3816b84c65e7c156 Mon Sep 17 00:00:00 2001 From: Emily Silkina Date: Sat, 15 Nov 2025 15:37:11 -0500 Subject: [PATCH 16/39] Fixing UI --- .../account/components/editProfile.tsx | 67 +++++-------------- apps/web/src/app/dashboard/account/page.tsx | 14 ++++ 2 files changed, 31 insertions(+), 50 deletions(-) diff --git a/apps/web/src/app/dashboard/account/components/editProfile.tsx b/apps/web/src/app/dashboard/account/components/editProfile.tsx index 001a9c64..f27cad0b 100644 --- a/apps/web/src/app/dashboard/account/components/editProfile.tsx +++ b/apps/web/src/app/dashboard/account/components/editProfile.tsx @@ -676,7 +676,21 @@ export function EditProfilePopup() { } return ( -
+
+ + +
+ + + + + + Reupload Degree Progress Report + + + +
+
@@ -691,59 +705,12 @@ export function EditProfilePopup() { - {/*
-
- - -
-
- - -
-
*/} - {/* - - - - - */} +
- -
- - - - - - Reupload Degree Progress Report - {/* - Reupload your degree progress report here. Click save when you're - done. - */} - - - {/*
-
- - -
-
- - -
-
*/} - {/* - - - - - */} -
-
-
+
); diff --git a/apps/web/src/app/dashboard/account/page.tsx b/apps/web/src/app/dashboard/account/page.tsx index a1e306e1..c8bf871f 100644 --- a/apps/web/src/app/dashboard/account/page.tsx +++ b/apps/web/src/app/dashboard/account/page.tsx @@ -49,6 +49,20 @@ export default function ProfilePage() { ? student.programs.map((p) => p.name).join(", ") : "None"}

+ +

+ Start Date:{" "} + {student.startingDate + ? `${student.startingDate.term.charAt(0).toUpperCase()}${student.startingDate.term.slice(1)} ${student.startingDate.year}` + : "N/A"} +

+ +

+ Expected Graduation:{" "} + {student.expectedGraduationDate + ? `${student.expectedGraduationDate.term.charAt(0).toUpperCase()}${student.expectedGraduationDate.term.slice(1)} ${student.expectedGraduationDate.year}` + : "N/A"} +

)} From 5355226f9ee400194d441792ef70ad8edf6da7b7 Mon Sep 17 00:00:00 2001 From: Emily Silkina Date: Sat, 15 Nov 2025 16:16:00 -0500 Subject: [PATCH 17/39] Fixing Profile Page --- apps/web/package.json | 2 + .../account/components/editProfile.tsx | 2 +- apps/web/src/app/dashboard/account/page.tsx | 104 ++++++- ...ile-page\\components\\profile-content.tsx" | 268 ++++++++++++++++++ ...file-page\\components\\profile-header.tsx" | 50 ++++ .../examples\\profile-page\\page.tsx" | 11 + .../components/profile-content.tsx | 268 ++++++++++++++++++ .../components/profile-header.tsx | 50 ++++ apps/web/src/components/profile-page/page.tsx | 11 + apps/web/src/components/ui/avatar.tsx | 91 +++--- .../components/ui/components\\ui\\avatar.tsx" | 53 ++++ .../components/ui/components\\ui\\badge.tsx" | 46 +++ .../components/ui/components\\ui\\button.tsx" | 59 ++++ .../components/ui/components\\ui\\card.tsx" | 92 ++++++ .../components/ui/components\\ui\\input.tsx" | 21 ++ .../components/ui/components\\ui\\label.tsx" | 24 ++ .../ui/components\\ui\\separator.tsx" | 28 ++ .../components/ui/components\\ui\\switch.tsx" | 31 ++ .../components/ui/components\\ui\\tabs.tsx" | 66 +++++ .../ui/components\\ui\\textarea.tsx" | 18 ++ apps/web/src/components/ui/switch.tsx | 31 ++ apps/web/src/components/ui/tabs.tsx | 8 +- apps/web/src/components/ui/textarea.tsx | 3 +- "apps/web/src/lib/lib\\utils.ts" | 6 + bun.lock | 2 + 25 files changed, 1293 insertions(+), 52 deletions(-) create mode 100644 "apps/web/src/components/examples\\profile-page\\components\\profile-content.tsx" create mode 100644 "apps/web/src/components/examples\\profile-page\\components\\profile-header.tsx" create mode 100644 "apps/web/src/components/examples\\profile-page\\page.tsx" create mode 100644 apps/web/src/components/profile-page/components/profile-content.tsx create mode 100644 apps/web/src/components/profile-page/components/profile-header.tsx create mode 100644 apps/web/src/components/profile-page/page.tsx create mode 100644 "apps/web/src/components/ui/components\\ui\\avatar.tsx" create mode 100644 "apps/web/src/components/ui/components\\ui\\badge.tsx" create mode 100644 "apps/web/src/components/ui/components\\ui\\button.tsx" create mode 100644 "apps/web/src/components/ui/components\\ui\\card.tsx" create mode 100644 "apps/web/src/components/ui/components\\ui\\input.tsx" create mode 100644 "apps/web/src/components/ui/components\\ui\\label.tsx" create mode 100644 "apps/web/src/components/ui/components\\ui\\separator.tsx" create mode 100644 "apps/web/src/components/ui/components\\ui\\switch.tsx" create mode 100644 "apps/web/src/components/ui/components\\ui\\tabs.tsx" create mode 100644 "apps/web/src/components/ui/components\\ui\\textarea.tsx" create mode 100644 apps/web/src/components/ui/switch.tsx create mode 100644 "apps/web/src/lib/lib\\utils.ts" diff --git a/apps/web/package.json b/apps/web/package.json index 93e5369b..a538138d 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -23,6 +23,8 @@ "@radix-ui/react-select": "^2.2.6", "@radix-ui/react-separator": "^1.1.8", "@radix-ui/react-slot": "^1.2.4", + "@radix-ui/react-switch": "^1.2.6", + "@radix-ui/react-tabs": "^1.1.13", "@radix-ui/react-tooltip": "^1.2.8", "@remixicon/react": "^4.7.0", "@t3-oss/env-nextjs": "^0.13.8", diff --git a/apps/web/src/app/dashboard/account/components/editProfile.tsx b/apps/web/src/app/dashboard/account/components/editProfile.tsx index c4efc7b4..af7dd5b3 100644 --- a/apps/web/src/app/dashboard/account/components/editProfile.tsx +++ b/apps/web/src/app/dashboard/account/components/editProfile.tsx @@ -634,7 +634,7 @@ export function EditProfilePopup() {
- + diff --git a/apps/web/src/app/dashboard/account/page.tsx b/apps/web/src/app/dashboard/account/page.tsx index c8bf871f..ab0fe254 100644 --- a/apps/web/src/app/dashboard/account/page.tsx +++ b/apps/web/src/app/dashboard/account/page.tsx @@ -7,6 +7,14 @@ import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import {EditProfilePopup} from "./components/editProfile"; +import { Shield, Key, Trash2, Mail, MapPin } from "lucide-react"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { Separator } from "@/components/ui/separator"; +import { Switch } from "@/components/ui/switch"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { Textarea } from "@/components/ui/textarea"; +import { Badge } from "@/components/ui/badge"; +import { Avatar, AvatarFallback, AvatarImage } from "@radix-ui/react-avatar"; export default function ProfilePage() { const { isAuthenticated } = useConvexAuth(); @@ -21,6 +29,99 @@ export default function ProfilePage() { return ( +
+ + {/* + Personal + Account + Security + Notifications + */} + + +
+
+ + + JD + + +
+
+
+

John Doe

+ +
+

Senior Product Designer

+
+
+ + john.doe@example.com +
+
+ + New York University +
+ {/*
+ + Joined March 2023 +
*/} +
+
+ {/* */} +
+
+
+ + {student && ( + + + Academic Information + View and update your academic information here. + + +
+
+ +

{student.school + ? `${student.school.name} (${ + student.school.level + ? student.school.level.charAt(0).toUpperCase() + student.school.level.slice(1).toLowerCase() + : "" + })` + : "N/A"}

+ {/* */} +
+
+ +

{student.programs?.length > 0 + ? student.programs.map((p) => p.name).join(", ") + : "None"}

+ {/* */} +
+
+ +

{student.startingDate + ? `${student.startingDate.term.charAt(0).toUpperCase()}${student.startingDate.term.slice(1)} ${student.startingDate.year}` + : "N/A"}

+ {/* */} +
+
+ +

{student.expectedGraduationDate + ? `${student.expectedGraduationDate.term.charAt(0).toUpperCase()}${student.expectedGraduationDate.term.slice(1)} ${student.expectedGraduationDate.year}` + : "N/A"}

+ {/* */} +
+ +
+
+
+
)}; + +
+ +

Profile

@@ -65,7 +166,8 @@ export default function ProfilePage() {

)} - + +
); } diff --git "a/apps/web/src/components/examples\\profile-page\\components\\profile-content.tsx" "b/apps/web/src/components/examples\\profile-page\\components\\profile-content.tsx" new file mode 100644 index 00000000..8a4e856c --- /dev/null +++ "b/apps/web/src/components/examples\\profile-page\\components\\profile-content.tsx" @@ -0,0 +1,268 @@ +import { Shield, Key, Trash2 } from "lucide-react"; + +import { Button } from "@/components/ui/button"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Separator } from "@/components/ui/separator"; +import { Switch } from "@/components/ui/switch"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { Textarea } from "@/components/ui/textarea"; +import { Badge } from "@/components/ui/badge"; + +export default function ProfileContent() { + return ( + + + Personal + Account + Security + Notifications + + + {/* Personal Information */} + + + + Personal Information + Update your personal details and profile information. + + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ +