From 628b5add3966fbd93721a30ff1fe279c6033439a Mon Sep 17 00:00:00 2001 From: Alex Meng Date: Sat, 16 May 2026 02:07:54 -0700 Subject: [PATCH] Connect profile lab flow to database --- app/profile/labs/new/page.tsx | 410 ++++++++++++++++++++++++++-------- app/profile/page.tsx | 57 ++--- app/profile/profile-data.ts | 107 +++++++++ 3 files changed, 440 insertions(+), 134 deletions(-) create mode 100644 app/profile/profile-data.ts diff --git a/app/profile/labs/new/page.tsx b/app/profile/labs/new/page.tsx index 2d0ad76..9a27431 100644 --- a/app/profile/labs/new/page.tsx +++ b/app/profile/labs/new/page.tsx @@ -1,5 +1,13 @@ import Link from "next/link"; -import { ChevronDown } from "lucide-react"; +import { redirect } from "next/navigation"; +import { revalidatePath } from "next/cache"; +import { ChevronDown, Database, PlusSquare } from "lucide-react"; + +import { connectToDatabase } from "@/lib/mongoose"; +import LabModel from "@/models/Lab"; +import UserLab from "@/models/UserLab"; +import { User } from "@/models/User"; +import { getOrCreateDemoProfileUser } from "@/app/profile/profile-data"; const roleOptions = [ { value: "PI", label: "PI" }, @@ -19,109 +27,321 @@ const fallbackLabs: LabOption[] = [ { id: "bioengineering-shared-facility", name: "Bioengineering Shared Facility" }, ]; -export default function AddLabAffiliationPage() { - const labOptions = fallbackLabs; +async function loadLabOptions(): Promise { + if (!process.env.DATABASE_URL) { + return fallbackLabs; + } + + try { + await connectToDatabase(); + const labs = await LabModel.find() + .sort({ createdAt: -1 }) + .limit(50) + .lean() + .exec(); + + if (labs.length === 0) { + return fallbackLabs; + } + + return labs.map((lab) => ({ + id: String(lab._id), + name: lab.name, + })); + } catch { + return fallbackLabs; + } +} + +async function createLabAction(formData: FormData) { + "use server"; + + const name = String(formData.get("newLabName") ?? "").trim(); + const department = String(formData.get("department") ?? "").trim(); + + if (!name || !department) { + return; + } + + if (!process.env.DATABASE_URL) { + return; + } + + await connectToDatabase(); + await LabModel.create({ + name, + department, + createdAt: new Date(), + }); + + revalidatePath("/profile/labs/new"); +} + +async function addExistingLabAction(formData: FormData) { + "use server"; + + const labId = String(formData.get("labName") ?? "").trim(); + const role = String(formData.get("labRole") ?? "").trim(); + const dateJoinedInput = String(formData.get("dateJoined") ?? "").trim(); + + if (!labId || !role) { + return; + } + + if (!process.env.DATABASE_URL) { + redirect("/profile"); + } + + await connectToDatabase(); + + const lab = await LabModel.findById(labId).exec(); + if (!lab) { + return; + } + + const user = await getOrCreateDemoProfileUser(); + const joinedAt = dateJoinedInput ? new Date(dateJoinedInput) : new Date(); + + await UserLab.findOneAndUpdate( + { user: user._id, lab: lab._id }, + { + role, + joinedAt, + }, + { + upsert: true, + new: true, + runValidators: true, + setDefaultsOnInsert: true, + } + ).exec(); + + await User.findByIdAndUpdate(user._id, { + $pull: { labs: { labId: lab._id } }, + }).exec(); + + await User.findByIdAndUpdate(user._id, { + $push: { + labs: { + labId: lab._id, + role, + }, + }, + }).exec(); + + revalidatePath("/profile"); + revalidatePath("/profile/labs/new"); + redirect("/profile"); +} + +function SectionCard({ + icon, + title, + description, + children, +}: Readonly<{ + icon: React.ReactNode; + title: string; + description: string; + children: React.ReactNode; +}>) { + return ( +
+
+
{icon}
+
+

+ {title} +

+

+ {description} +

+
+
+ {children} +
+ ); +} + +export default async function AddLabAffiliationPage() { + const labOptions = await loadLabOptions(); return (
-
-
-
+
+
+

Add Lab Affiliation

-

- Choose a lab, add your role, and record when you joined so it can - appear on your profile page. +

+ Choose an existing lab from the database or create a new lab record + for testing. The affiliation form stays frontend-only for now, while + the create-lab form can write to MongoDB when `DATABASE_URL` is set.

-
-
- -
- - -
-
- -
- -
- - -
-
- -
- - -
- -
- - Cancel - - -
-
-
+
+ } + title="Add Existing Lab" + description="Pick a lab from the database, choose your role, and record your join date." + > +
+
+ +
+ + +
+
+ +
+ +
+ + +
+
+ +
+ + +
+ +
+ When `DATABASE_URL` is configured, this saves a `UserLab` + affiliation for the demo profile user and redirects back to + the profile page. +
+ +
+ + Cancel + + +
+
+
+ + } + title="Create New Lab" + description="If the lab is not in the database yet, create a new lab record for testing and then select it from the list." + > +
+
+ + +
+ +
+ + +
+ +
+ This form uses the repo's `DATABASE_URL` connection when + available. It creates a lab in the `Lab` collection so it can + appear in the existing-lab dropdown. +
+ +
+ + Cancel + + +
+
+
+
+
); diff --git a/app/profile/page.tsx b/app/profile/page.tsx index 65199ec..3ad0acf 100644 --- a/app/profile/page.tsx +++ b/app/profile/page.tsx @@ -8,36 +8,12 @@ import { UserRound, } from "lucide-react"; -type Affiliation = { - id: string; - labName: string; - role: string; - joined: string; -}; - -const profile = { - name: "Dr. Xu", - pronouns: "He/him", - bio: "Experienced principal investigator at a lab specializing in computational neuroscience. Our lab focuses on developing innovative techniques for analysis in computational neuroscience.", - email: "Xu@ucsd.edu", - phone: "(xxx) xxx-xxxx", - status: "Active", -}; - -const sampleAffiliations: Affiliation[] = [ - { - id: "xu-comp-neuro", - labName: "Xu Computational Neuroscience Lab", - role: "Principal Investigator (PI)", - joined: "March 2026", - }, - { - id: "cognitive-systems-core", - labName: "Cognitive Systems Core", - role: "Lab Advisor", - joined: "January 2025", - }, -]; +import { + Affiliation, + loadProfileAffiliations, + profileSeed, + sampleAffiliations, +} from "./profile-data"; function InfoCard({ icon, @@ -143,15 +119,18 @@ export default async function ProfilePage({ searchParams?: Promise<{ labs?: string }>; }>) { const resolvedParams = searchParams ? await searchParams : undefined; - const affiliations = - resolvedParams?.labs === "empty" ? [] : sampleAffiliations; + const affiliations = resolvedParams?.labs === "empty" + ? [] + : process.env.DATABASE_URL + ? await loadProfileAffiliations() + : sampleAffiliations; return (
@@ -176,11 +155,11 @@ export default async function ProfilePage({

- {profile.name} + {profileSeed.name}

-

{profile.pronouns}

+

{profileSeed.pronouns}

- {profile.bio} + {profileSeed.bio}

@@ -193,8 +172,8 @@ export default async function ProfilePage({ title="Contact Information" >
- - + +
@@ -203,7 +182,7 @@ export default async function ProfilePage({
- {profile.status} + {profileSeed.status}
diff --git a/app/profile/profile-data.ts b/app/profile/profile-data.ts new file mode 100644 index 0000000..73884fb --- /dev/null +++ b/app/profile/profile-data.ts @@ -0,0 +1,107 @@ +import { connectToDatabase } from "@/lib/mongoose"; +import UserLab from "@/models/UserLab"; +import { User } from "@/models/User"; + +export type Affiliation = { + id: string; + labName: string; + role: string; + joined: string; +}; + +export const profileSeed = { + name: "Dr. Xu", + pronouns: "He/him", + bio: "Experienced principal investigator at a lab specializing in computational neuroscience. Our lab focuses on developing innovative techniques for analysis in computational neuroscience.", + email: "xu@ucsd.edu", + phone: "(xxx) xxx-xxxx", + status: "Active", +}; + +export const sampleAffiliations: Affiliation[] = [ + { + id: "xu-comp-neuro", + labName: "Xu Computational Neuroscience Lab", + role: "Principal Investigator (PI)", + joined: "March 2026", + }, + { + id: "cognitive-systems-core", + labName: "Cognitive Systems Core", + role: "Lab Advisor", + joined: "January 2025", + }, +]; + +const roleLabels: Record = { + PI: "PI", + LAB_MANAGER: "Lab Manager", + RESEARCHER: "Researcher", + VIEWER: "Viewer", +}; + +function formatJoined(date: Date | string | undefined) { + if (!date) { + return "Unknown"; + } + + const parsed = date instanceof Date ? date : new Date(date); + if (Number.isNaN(parsed.getTime())) { + return "Unknown"; + } + + return parsed.toLocaleString("en-US", { + month: "long", + year: "numeric", + }); +} + +export async function getOrCreateDemoProfileUser() { + await connectToDatabase(); + + const existing = await User.findOne({ email: profileSeed.email }).exec(); + if (existing) { + return existing; + } + + return User.create({ + ucsdId: "A12345678", + email: profileSeed.email, + name: { first: "Dr.", last: "Xu" }, + role: "PI", + labs: [], + profile: { + title: "Professor", + department: "Computational Neuroscience", + phone: profileSeed.phone, + }, + }); +} + +export async function loadProfileAffiliations() { + if (!process.env.DATABASE_URL) { + return sampleAffiliations; + } + + try { + const user = await getOrCreateDemoProfileUser(); + const rows = await UserLab.find({ user: user._id }) + .populate("lab") + .sort({ joinedAt: -1 }) + .lean() + .exec(); + + if (rows.length === 0) { + return [] as Affiliation[]; + } + + return rows.map((row: any) => ({ + id: String(row._id), + labName: row.lab?.name ?? "Unknown Lab", + role: roleLabels[row.role] ?? row.role, + joined: formatJoined(row.joinedAt), + })); + } catch { + return sampleAffiliations; + } +}