From 67fb3ede36051579a1a8efd53f03c052ed5a9a7f Mon Sep 17 00:00:00 2001 From: BigBen-7 Date: Sun, 31 Aug 2025 20:47:06 +0100 Subject: [PATCH 1/2] feat(profile)/ implement profile management ui --- app/profile/page.tsx | 43 ++++++++++++ components/profile/ProfileForm.tsx | 109 +++++++++++++++++++++++++++++ lib/api-schema.ts | 4 ++ lib/msw/handlers.ts | 25 ++++++- lib/validators.ts | 10 +++ package-lock.json | 46 ++++++++++++ package.json | 3 + public/mockServiceWorker.js | 1 + 8 files changed, 239 insertions(+), 2 deletions(-) create mode 100644 app/profile/page.tsx create mode 100644 components/profile/ProfileForm.tsx create mode 100644 lib/validators.ts diff --git a/app/profile/page.tsx b/app/profile/page.tsx new file mode 100644 index 0000000..8fe24f1 --- /dev/null +++ b/app/profile/page.tsx @@ -0,0 +1,43 @@ +'use client' + +import { useQuery } from '@tanstack/react-query' +import { useSession } from 'next-auth/react' +import { redirect } from 'next/navigation' +import type { UserProfile } from '@/lib/api-schema' +import ProfileForm from '@/components/profile/ProfileForm' + +const fetchProfile = async (): Promise => { + const res = await fetch('/api/profile') + if (!res.ok) throw new Error('Failed to fetch profile') + return res.json() +} + +export default function ProfilePage() { + // Protect the route + const { status } = useSession({ + required: true, + onUnauthenticated() { + redirect('/') + }, + }) + + const { data: profile, isLoading } = useQuery({ + queryKey: ['profile'], + queryFn: fetchProfile, + }) + + if (status === 'loading' || isLoading) { + return ( +
+ Loading Profile... +
+ ) + } + + return ( +
+

Edit Your Profile

+ {profile && } +
+ ) +} diff --git a/components/profile/ProfileForm.tsx b/components/profile/ProfileForm.tsx new file mode 100644 index 0000000..309fbfb --- /dev/null +++ b/components/profile/ProfileForm.tsx @@ -0,0 +1,109 @@ +'use client' + +import type { UserProfile } from '@/lib/api-schema' +import { profileFormSchema, type ProfileFormValues } from '@/lib/validators' +import { zodResolver } from '@hookform/resolvers/zod' +import { useMutation, useQueryClient } from '@tanstack/react-query' +import { useForm } from 'react-hook-form' + +// This is our API function for the mutation +const updateProfile = async (data: ProfileFormValues): Promise => { + const res = await fetch('/api/profile', { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(data), + }) + if (!res.ok) throw new Error('Server error: Failed to update profile.') + return res.json() +} + +interface ProfileFormProps { + profile: UserProfile +} + +export default function ProfileForm({ profile }: ProfileFormProps) { + const queryClient = useQueryClient() + + const { + register, + handleSubmit, + formState: { errors, isSubmitting }, + } = useForm({ + resolver: zodResolver(profileFormSchema), + defaultValues: { + bio: profile.bio || '', + }, + }) + + const mutation = useMutation({ + mutationFn: updateProfile, + // --- The Optimistic Update Logic --- + onMutate: async (newProfileData) => { + // 1. Cancel any outgoing refetches (so they don't overwrite our optimistic update) + await queryClient.cancelQueries({ queryKey: ['profile'] }) + + // 2. Snapshot the previous value + const previousProfile = queryClient.getQueryData(['profile']) + + // 3. Optimistically update to the new value + queryClient.setQueryData(['profile'], (old) => + old ? { ...old, ...newProfileData } : undefined + ) + + // 4. Return a context object with the snapshotted value + return { previousProfile } + }, + // If the mutation fails, use the context returned from onMutate to roll back + onError: (err, newProfile, context) => { + if (context?.previousProfile) { + queryClient.setQueryData(['profile'], context.previousProfile) + // You can also show a toast notification here + alert('Update failed! Your changes have been reverted.') + } + }, + // Always refetch after error or success to ensure server state + onSettled: () => { + queryClient.invalidateQueries({ queryKey: ['profile'] }) + }, + }) + + const onSubmit = (data: ProfileFormValues) => { + mutation.mutate(data) + } + + // We get the latest profile data directly from the query cache + const currentProfile = queryClient.getQueryData(['profile']) + + return ( +
+
+ +