diff --git a/src/components/profile/profile-picture-upload.tsx b/src/components/profile/profile-picture-upload.tsx index 07b3ee80..7cfc15df 100644 --- a/src/components/profile/profile-picture-upload.tsx +++ b/src/components/profile/profile-picture-upload.tsx @@ -11,6 +11,7 @@ interface ProfilePictureUploadProps { name?: string; disabled?: boolean; userId?: string; + onUploaded?: (objectKey: string) => void; } export function ProfilePictureUpload({ @@ -18,6 +19,7 @@ export function ProfilePictureUpload({ name, disabled = false, userId, + onUploaded, }: ProfilePictureUploadProps) { const { user } = useAuth(); @@ -32,6 +34,7 @@ export function ProfilePictureUpload({ id={userId ?? user?.id ?? ""} disabled={disabled} targetSize={120} + onUploaded={onUploaded} // onUploaded={(objectKey) => { // const base = process.env.NEXT_PUBLIC_MINIO_PUBLIC_URL!; // const bucket = process.env.NEXT_PUBLIC_MINIO_BUCKET!; diff --git a/src/components/settings/pages/profile-settings-content.tsx b/src/components/settings/pages/profile-settings-content.tsx index 7454fc06..be27ab18 100644 --- a/src/components/settings/pages/profile-settings-content.tsx +++ b/src/components/settings/pages/profile-settings-content.tsx @@ -1,3 +1,229 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { z } from "zod"; +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; + +import { Button } from "@/components/primitives/button"; +import { Field, FieldLabel, FieldError } from "@/components/primitives/field"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/primitives/card"; +import { Input } from "@/components/primitives/input"; +import { Textarea } from "@/components/primitives/textarea"; +import { Alert, AlertTitle, AlertDescription } from "@/components/primitives/alert"; +import { CheckCircle2, AlertCircle } from "lucide-react"; + +import { Spinner } from "@/components/primitives/spinner"; + +import { WithPermission } from "@/components/utils/with-permission"; +import { ProfilePictureUpload } from "@/components/profile/profile-picture-upload"; +import { useAuth } from "@/providers/client-auth-provider"; +import { clientApi } from "@/trpc/client"; +import { authClient } from "@/lib/auth/client"; + +const ProfileSchema = z.object({ + firstName: z.string().nonempty("Please enter your first name."), + lastName: z.string().nonempty("Please enter your last name."), + email: z + .string() + .email("Please enter a valid email address.") + .nonempty("Email is required."), + preferredName: z.string().optional(), + pronouns: z.string().optional(), + bio: z.string().optional(), + city: z.string().optional(), + province: z.string().optional(), + profilePictureUrl: z.string().optional(), +}); + +type ProfileSchemaType = z.infer; + export function ProfileSettingsContent() { - return <>Profile coming soon...; + const { user } = useAuth(); + const { refetch: refetchSession } = authClient.useSession(); + + + const { data: volunteer } = clientApi.volunteer.byId.useQuery( + { volunteerUserId: user!.id }, + { enabled: !!user } + ); + const updateProfile = clientApi.volunteer.updateVolunteerProfile.useMutation({ + onSuccess: async () => { + refetchSession(); // so we can keep user-level changes up to date + }, +}); + const [successMessage, setSuccessMessage] = useState(null); + + const { + register, + handleSubmit, + reset, + setValue, + setError, + formState: { errors, isSubmitting }, + } = useForm({ + resolver: zodResolver(ProfileSchema), + mode: "onSubmit", + reValidateMode: "onChange", + defaultValues: { + firstName: user?.name ?? "", + lastName: user?.lastName ?? "", + email: user?.email ?? "", + preferredName: "", + pronouns: "", + bio: "", + city: "", + province: "", + profilePictureUrl: undefined, + }, + }); + + useEffect(() => { + if (!volunteer) return; + + reset({ + firstName: user?.name ?? "", + lastName: user?.lastName ?? "", + email: user?.email ?? "", + preferredName: volunteer.preferredName ?? "", + pronouns: volunteer.pronouns ?? "", + bio: volunteer.bio ?? "", + city: volunteer.city ?? "", + province: volunteer.province ?? "", + profilePictureUrl: undefined, + }); + }, [volunteer, user, reset]); + + const onSubmit = async (data: ProfileSchemaType) => { + if (!user?.id) return; + + const result = await updateProfile.mutateAsync({ + volunteerUserId: user.id, + ...data, + }); + + if (!result.ok) { + setError("root", { + type: "custom", + message: "Something went wrong.", + }); + return; + } + + setSuccessMessage("Your profile has been successfully updated!"); + }; + + return ( + + {errors.root?.message && ( + + + Couldn’t update profile + {errors.root.message} + + )} + + {successMessage && ( + + + Success + {successMessage} + + )} + +
+ + + Profile Information + + + +
+
+ Profile Picture +
+ { + const base = process.env.NEXT_PUBLIC_MINIO_PUBLIC_URL!; + const bucket = process.env.NEXT_PUBLIC_MINIO_BUCKET!; + const imageUrl = `${base}/${bucket}/${objectKey}`; + setValue("profilePictureUrl", imageUrl); + }} + /> +
+
+ +
+ + First Name + + + + + + Last Name + + + +
+
+ + + Email + + + + + + Preferred Name + + + + + + Pronouns + + + + +
+ + City + + + + + + Province + + + +
+ + + Bio +