diff --git a/src/components/Directory/DirectoryProfileTabs.tsx b/src/components/Directory/DirectoryProfileTabs.tsx index 73629a6..936807d 100644 --- a/src/components/Directory/DirectoryProfileTabs.tsx +++ b/src/components/Directory/DirectoryProfileTabs.tsx @@ -1,4 +1,3 @@ -import { AcademicInfo } from "../Profile/AcademicInfo"; import { InternshipList } from "../Profile/Internship/InternshipList"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "../ui/tabs"; import { ResearchList } from "../Profile/Research/ResearchList"; @@ -48,7 +47,6 @@ export function DirectoryProfileTabs({ officerId, archived = false, editable = f - {editable && } diff --git a/src/components/Profile/AcademicInfo.tsx b/src/components/Profile/AcademicInfo.tsx index d10aaea..b7d1ee1 100644 --- a/src/components/Profile/AcademicInfo.tsx +++ b/src/components/Profile/AcademicInfo.tsx @@ -1,15 +1,53 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { useQuery } from "@tanstack/react-query"; -import UpdateAcademics from "./UpdateAcademics"; +import UpdateAcademics, { type UpdateAcademicsHandle } from "./UpdateAcademics"; import { getOfficerByIdQuery, getOfficerQuery } from "@/queries/officer"; +import type { Officer } from "@/schemas/officer"; +import { CalendarDays, GraduationCap } from "lucide-react"; type Props = { officerId?: string; archived?: boolean; editable?: boolean; + variant?: "card" | "inline"; + hideSubmitButton?: boolean; + academicFormRef?: React.Ref; }; -export function AcademicInfo({ officerId, archived = false, editable = false }: Props) { +function AcademicContent({ officer }: { officer: Officer }) { + return ( +
+
+
+ + Year Standing +
+
+ {officer.yearStanding} +
+
+ +
+
+ + Expected Graduation +
+
+ {officer.expectedGrad.term} {officer.expectedGrad.year} +
+
+
+ ); +} + +export function AcademicInfo({ + officerId, + archived = false, + editable = false, + variant = "card", + hideSubmitButton = false, + academicFormRef, +}: Props) { const { data: officer } = useQuery( officerId ? getOfficerByIdQuery(officerId, archived) : getOfficerQuery ); @@ -18,6 +56,27 @@ export function AcademicInfo({ officerId, archived = false, editable = false }: return null; } + const content = editable ? ( + + ) : ( + + ); + + if (variant === "inline") { + return ( +
+ + Academic Information + + {content} +
+ ); + } + return ( @@ -25,50 +84,7 @@ export function AcademicInfo({ officerId, archived = false, editable = false }: Academic Information - - {editable ? ( - - ) : ( -
-
-
- Net ID -
-
- {officer.netId} -
-
- -
-
-
- Year Standing -
-
- {officer.yearStanding} -
-
-
-
- Credit Standing -
-
- {officer.creditStanding} -
-
-
- -
-
- Expected Graduation -
-
- {officer.expectedGrad.term} {officer.expectedGrad.year} -
-
-
- )} -
+ {content}
); } diff --git a/src/components/Profile/ImageUpdate.tsx b/src/components/Profile/ImageUpdate.tsx index 9d4f8be..e6f752b 100644 --- a/src/components/Profile/ImageUpdate.tsx +++ b/src/components/Profile/ImageUpdate.tsx @@ -15,11 +15,18 @@ type Props = { officerId: string; firstName: string; lastName: string; + editable?: boolean; }; -export function ImageUpdate({ photo, officerId, firstName, lastName }: Props) { +export function ImageUpdate({ + photo, + officerId, + firstName, + lastName, + editable = false, +}: Props) { const avatarOutputSize = 720; const defaultAdjustments: ImageAdjustments = { @@ -243,19 +250,21 @@ export function ImageUpdate({ photo, officerId, firstName, lastName }: Props) { accept="image/*" className="hidden" /> - + {editable && ( + + )} ); diff --git a/src/components/Profile/ProfileView.tsx b/src/components/Profile/ProfileView.tsx index 5f49d8a..3773d22 100644 --- a/src/components/Profile/ProfileView.tsx +++ b/src/components/Profile/ProfileView.tsx @@ -1,9 +1,11 @@ import { cn } from "@/lib/utils"; +import { useRef, useState } from "react"; import { RoleList } from "./RoleList"; import { ExternalLinks } from "../Socials/ExternalLinks"; import { ImageUpdate } from "./ImageUpdate"; import { UserAvatar } from "./UserAvatar"; import { UpdateName } from "./UpdateName"; +import { AcademicInfo } from "./AcademicInfo"; import { getOfficerQuery, getOfficerByIdQuery, @@ -17,7 +19,15 @@ import { isExecutive } from "@/lib/admin"; import { Button } from "../ui/button"; import { Separator } from "@/components/ui/separator"; import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; -import { EllipsisVertical, Loader2 } from "lucide-react"; +import { + ArchiveRestore, + CalendarDays, + CheckCircle2, + EllipsisVertical, + GraduationCap, + Loader2, +} from "lucide-react"; +import type { UpdateAcademicsHandle } from "./UpdateAcademics"; type Props = { officerId?: string; @@ -29,6 +39,8 @@ export function ProfileView({ officerId, archived = false, editable = false }: P const { data: officer, isLoading } = useQuery( officerId ? getOfficerByIdQuery(officerId, archived) : getOfficerQuery ); + const [isEditing, setIsEditing] = useState(false); + const academicFormRef = useRef(null); const { data: viewer } = useQuery(getOfficerQuery); const isViewerExecutive = viewer ? isExecutive(viewer) : false; @@ -46,8 +58,8 @@ export function ProfileView({ officerId, archived = false, editable = false }: P } return (
-
-
+
+
{isViewerExecutive && officerId && ( @@ -72,7 +84,7 @@ export function ProfileView({ officerId, archived = false, editable = false }: P
@@ -110,35 +132,38 @@ export function ProfileView({ officerId, archived = false, editable = false }: P )}
-
-
+
+
- {officer.isActive ? "Active" : "Inactive"} + > +
+ {officer.isActive ? "Active" : "Inactive"} +
-
+
{editable ? ( ) : ( )} + + {editable ? ( + + ) : ( +

+ {officer.firstName} {officer.lastName} +

+ )} + +
+ +
- {editable ? ( - + + + {isEditing ? ( +
+ + + + +
+ + Socials + + setIsEditing(true)} + isEditing + onCancelEdit={() => setIsEditing(false)} + onFinishEdit={() => setIsEditing(false)} + onBeforeSave={() => academicFormRef.current?.submit()} + /> +
+
) : ( -

- {officer.firstName} {officer.lastName} -

- )} +
+
+
+ +
+
+ Year Standing +
+ {officer.yearStanding} +
+
+
+ +
+
+ Expected Graduation +
+ + {officer.expectedGrad.term} {officer.expectedGrad.year} + +
+
+
-
- -
-
+
- - -
- - Socials - - + setIsEditing(true)} + onCancelEdit={() => setIsEditing(false)} + onFinishEdit={() => setIsEditing(false)} + /> +
+ )}
); diff --git a/src/components/Profile/Socials/EditSocials.tsx b/src/components/Profile/Socials/EditSocials.tsx index b0487c4..fab6d37 100644 --- a/src/components/Profile/Socials/EditSocials.tsx +++ b/src/components/Profile/Socials/EditSocials.tsx @@ -23,6 +23,7 @@ type EditSocialsProps = { links: SocialLinks; onCancel?: () => void; onSuccess?: () => void; + onBeforeSave?: () => Promise | void; }; const emailValidator = z.email(); @@ -74,7 +75,13 @@ const inputClassName = type SocialLinksFormValues = z.infer; -export function EditSocials({ officerId, links, onCancel, onSuccess }: EditSocialsProps) { +export function EditSocials({ + officerId, + links, + onCancel, + onSuccess, + onBeforeSave, +}: EditSocialsProps) { const { register, handleSubmit, @@ -104,6 +111,10 @@ export function EditSocials({ officerId, links, onCancel, onSuccess }: EditSocia }, [links.github, links.instagram, links.linkedin, links.personalEmail, reset]); const onSubmit = async (values: SocialLinksFormValues) => { + if (onBeforeSave) { + await onBeforeSave(); + } + const updated: SocialLinks = { ...links }; const applyField = ( diff --git a/src/components/Profile/UpdateAcademics.tsx b/src/components/Profile/UpdateAcademics.tsx index 26058ca..083c131 100644 --- a/src/components/Profile/UpdateAcademics.tsx +++ b/src/components/Profile/UpdateAcademics.tsx @@ -1,7 +1,6 @@ import { type Officer, StandingSchema, TermSchema } from "@/schemas/officer"; import { useMutation } from "@tanstack/react-query"; import { Button } from "../ui/button"; -import { Input } from "../ui/input"; import { updateAcademicInfoMutationOptions } from "@/queries/officer"; import { Field, @@ -21,30 +20,38 @@ import { useForm, Controller } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; import { z } from "zod"; import { toast } from "sonner"; -import { useEffect } from "react"; +import { forwardRef, useEffect, useImperativeHandle } from "react"; +import { CalendarDays, GraduationCap } from "lucide-react"; const UpdateAcademicsSchema = z.object({ - netId: z.string().min(1, "Net ID is required"), - creditStanding: StandingSchema, yearStanding: StandingSchema, expectedGrad: TermSchema, }); type UpdateAcademicsFormData = z.infer; -export default function UpdateAcademics({ officer }: { officer: Officer }) { +export type UpdateAcademicsHandle = { + submit: () => Promise; +}; + +type Props = { + officer: Officer; + showSubmitButton?: boolean; +}; + +const UpdateAcademics = forwardRef(function UpdateAcademics( + { officer, showSubmitButton = true }, + ref +) { const currentYear = new Date().getFullYear(); const startYear = 2020; // matches TermSchema minimum const years = Array.from({ length: currentYear + 6 - startYear + 1 }, (_, i) => startYear + i); const initialValues = { - netId: officer.netId, - creditStanding: officer.creditStanding, yearStanding: officer.yearStanding, expectedGrad: officer.expectedGrad, }; const { - register, handleSubmit, formState: { errors, isDirty }, control, @@ -61,8 +68,6 @@ export default function UpdateAcademics({ officer }: { officer: Officer }) { useEffect(() => { reset(initialValues); }, [ - officer.netId, - officer.creditStanding, officer.yearStanding, officer.expectedGrad.term, officer.expectedGrad.year, @@ -71,36 +76,36 @@ export default function UpdateAcademics({ officer }: { officer: Officer }) { const onSubmit = async (data: UpdateAcademicsFormData) => { try { - await updateAcademicInfo({ officerId: officer.id, ...data }); + await updateAcademicInfo({ + officerId: officer.id, + netId: officer.netId, + creditStanding: officer.creditStanding, + ...data, + }); reset(data); toast.success("Academic info updated successfully"); } catch (error) { toast.error("Failed to update academic info"); + throw error; } }; + + useImperativeHandle(ref, () => ({ + submit: async () => { + await handleSubmit(onSubmit)(); + }, + })); return (
- - - - Net ID - - - - - - -
+
- Standing (by year) + + + Standing (by year) + - - Standing (by credit) + + + + Expected Graduation + - ( - - )} +
+
+ ( + + )} + /> +
+
+ ( + + )} + /> +
+
+ -
- - - - - Expected Graduation - -
-
- ( - - )} - /> -
-
- ( - - )} - /> -
-
- -
-
-
- -
+ {showSubmitButton && ( +
+ +
+ )} ); -} +}); + +export default UpdateAcademics; diff --git a/src/components/Profile/UpdateName.tsx b/src/components/Profile/UpdateName.tsx index ea65357..12f91bd 100644 --- a/src/components/Profile/UpdateName.tsx +++ b/src/components/Profile/UpdateName.tsx @@ -1,4 +1,4 @@ -import { useEffect, useState } from "react"; +import { useEffect } from "react"; import { useMutation } from "@tanstack/react-query"; import { Button } from "../ui/button"; import { Input } from "../ui/input"; @@ -19,10 +19,10 @@ type Props = { officerId?: string; firstName: string; lastName: string; + editable?: boolean; }; -export function UpdateName({ officerId, firstName, lastName }: Props) { - const [isEditing, setIsEditing] = useState(false); +export function UpdateName({ officerId, firstName, lastName, editable = false }: Props) { const initialValues = { firstName, lastName, @@ -51,34 +51,17 @@ export function UpdateName({ officerId, firstName, lastName }: Props) { await updateName({ officerId, ...data }); reset(data); toast.success("Name updated successfully"); - setIsEditing(false); } catch (error) { toast.error("Failed to update name"); } }; - const handleCancel = () => { - reset(initialValues); - setIsEditing(false); - }; - - if (!isEditing) { + if (!editable) { return (

{firstName} {lastName}

-
); } @@ -86,11 +69,11 @@ export function UpdateName({ officerId, firstName, lastName }: Props) { return (
-