diff --git a/src/app/(auth-pages)/profile/page.tsx b/src/app/(auth-pages)/profile/page.tsx index c00c0fb..ae2b419 100644 --- a/src/app/(auth-pages)/profile/page.tsx +++ b/src/app/(auth-pages)/profile/page.tsx @@ -1,282 +1,71 @@ 'use client'; -import { z } from 'zod'; -import { zodResolver } from '@hookform/resolvers/zod'; -import { useForm } from 'react-hook-form'; -import { getProfile, getSignedUrl, updateProfile, uploadFile } from '@/lib/actions'; -import { useState, useEffect, useRef } from 'react'; -import type { Profile } from '@/utils/supabase'; +import { useProfile } from '@/lib/hooks/useProfile'; +import { AvatarUpload, ProfileForm } from '@/components/default/profile'; import { - Avatar, - AvatarFallback, - AvatarImage, - Button, Card, CardContent, - CardDescription, CardHeader, CardTitle, - Form, - FormControl, - FormDescription, - FormField, - FormItem, - FormLabel, - FormMessage, - Input, + CardDescription, Separator, } from '@/components/ui'; -import { toast } from 'sonner'; -import { Pencil, User } from 'lucide-react' - -const formSchema = z.object({ - full_name: z.string().min(5, { - message: 'Full name is required & must be at least 5 characters.' - }), - email: z.string().email(), -}); +import { Loader2 } from 'lucide-react'; const ProfilePage = () => { - const [profile, setProfile] = useState(undefined); - const [avatarUrl, setAvatarUrl] = useState(undefined); - const [isLoading, setIsLoading] = useState(true); - const [isUploading, setIsUploading] = useState(false); - const fileInputRef = useRef(null); + const { profile, isLoading, updateProfile } = useProfile(); + const handleAvatarUploaded = async (path: string) => { + await updateProfile({ avatar_url: path }); + }; - const form = useForm>({ - resolver: zodResolver(formSchema), - defaultValues: { - full_name: '', - email: '', - }, - }); - - useEffect(() => { - const fetchProfile = async () => { - try { - setIsLoading(true); - const profileResponse = await getProfile(); - if (!profileResponse.success) - throw new Error('Profile response unsuccessful'); - setProfile(profileResponse.data); - form.reset({ - full_name: profileResponse.data.full_name ?? '', - email: profileResponse.data.email ?? '', - }); - } catch (error) { - setProfile(undefined); - } finally { - setIsLoading(false); - } - }; - fetchProfile().catch((error) => { - console.error('Error getting profile:', error); + const handleProfileSubmit = async (values: { full_name: string; email: string }) => { + await updateProfile({ + full_name: values.full_name, + email: values.email, }); - }, [form]); - - useEffect(() => { - const getAvatarUrl = async () => { - if (profile?.avatar_url) { - try { - const response = await getSignedUrl({ - bucket: 'avatars', - url: profile.avatar_url, - }); - - if (response.success) { - setAvatarUrl(response.data); - } - } catch (error) { - console.error('Error getting signed URL:', error); - } - } - }; - getAvatarUrl().catch((error) => { - toast.error(error instanceof Error ? error.message : 'Failed to get signed avatar url.'); - }); - }, [profile]); - - const handleAvatarClick = () => { - fileInputRef.current?.click(); }; - const handleFileChange = async (e: React.ChangeEvent) => { - const file = e.target.files?.[0]; - if (!file) - throw new Error('No file selected'); - - try { - setIsUploading(true); - - const fileExt = file.name.split('.').pop(); - const fileName = `${Date.now()}-${profile?.id ?? Math.random().toString(36).substring(2,15)}.${fileExt}`; - - const uploadResult = await uploadFile({ - bucket: 'avatars', - path: fileName, - file, - options: { - upsert: true, - contentType: file.type, - }, - }); - if (!uploadResult.success) - throw new Error(uploadResult.error ?? 'Failed to upload avatar'); - - const updateResult = await updateProfile({ - avatar_url: uploadResult.data, - }); - - if (!updateResult.success) - throw new Error(updateResult.error ?? 'Failed to update profile'); - - setProfile(updateResult.data); - toast.success('Avatar updated successfully.') - - } catch (error) { - toast.error(error instanceof Error ? error.message : 'Failed to uploaad avatar.'); - } finally { - setIsUploading(false); - if (fileInputRef.current) fileInputRef.current.value = ''; - } - }; - - const getInitials = (name: string) => { - return name - .split(' ') - .map((n) => n[0]) - .join('') - .toUpperCase(); - } - - const onSubmit = async (values: z.infer) => { - try { - setIsLoading(true); - const result = await updateProfile({ - full_name: values.full_name, - email: values.email, - }); - - if (!result.success) { - throw new Error(result.error ?? 'Failed to update profile'); - } - setProfile(result.data); - toast.success('Profile updated successfully!'); - } catch (error) { - toast.error(error instanceof Error ? error.message : 'Failed to update profile.'); - } finally { - setIsLoading(false); - } - }; - - if (profile === undefined) + if (profile === undefined && !isLoading) return (

Unauthorized

- ); - + ); + return ( - - - - {profile?.full_name ?? 'Profile'} - - - Manage your personal information & how it appears to others. - - - - {isLoading && !profile ? ( -
-
-
-
+
+ + + Your Profile + + Manage your personal information and how it appears to others + + + + {isLoading && !profile ? ( +
+
-
- ) : ( -
-
-
- - {avatarUrl ? ( - - ) : ( - - {profile?.full_name - ? profile.full_name.split(' ').map(n => n[0]).join('').toUpperCase() - : } - - )} - -
- -
- -
- {isUploading && ( -
Uploading...
- )} -

- Click on the avatar to upload a new image -

+ ) : ( +
+ + + + +
- -
- - ( - - Full Name - - - - - Your public display name. - - - - )} - /> - ( - - Email - - - - - Your email address associated with your account. - - - - )} - /> -
- -
- - -
- )} - - + )} + + +
); }; + export default ProfilePage; diff --git a/src/app/(auth-pages)/profile/page.tsx.bak b/src/app/(auth-pages)/profile/page.tsx.bak new file mode 100644 index 0000000..c6426af --- /dev/null +++ b/src/app/(auth-pages)/profile/page.tsx.bak @@ -0,0 +1,286 @@ +'use client'; +import { z } from 'zod'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { useForm } from 'react-hook-form'; +import { getProfile, getSignedUrl, updateProfile, uploadFile } from '@/lib/actions'; +import { useState, useEffect, useRef } from 'react'; +import type { Profile } from '@/utils/supabase'; +import { + Avatar, + AvatarFallback, + AvatarImage, + Button, + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage, + Input, + Separator, +} from '@/components/ui'; +import { toast } from 'sonner'; +import { Pencil, User } from 'lucide-react' + +const formSchema = z.object({ + full_name: z.string().min(5, { + message: 'Full name is required & must be at least 5 characters.' + }), + email: z.string().email(), +}); + +const ProfilePage = () => { + const [profile, setProfile] = useState(undefined); + const [avatarUrl, setAvatarUrl] = useState(undefined); + const [isLoading, setIsLoading] = useState(true); + const [isUploading, setIsUploading] = useState(false); + const fileInputRef = useRef(null); + + + const form = useForm>({ + resolver: zodResolver(formSchema), + defaultValues: { + full_name: '', + email: '', + }, + }); + + useEffect(() => { + const fetchProfile = async () => { + try { + setIsLoading(true); + const profileResponse = await getProfile(); + if (!profileResponse.success) + throw new Error('Profile response unsuccessful'); + setProfile(profileResponse.data); + form.reset({ + full_name: profileResponse.data.full_name ?? '', + email: profileResponse.data.email ?? '', + }); + } catch (error) { + setProfile(undefined); + } finally { + setIsLoading(false); + } + }; + fetchProfile().catch((error) => { + console.error('Error getting profile:', error); + }); + }, [form]); + + useEffect(() => { + const getAvatarUrl = async () => { + if (profile?.avatar_url) { + try { + const response = await getSignedUrl({ + bucket: 'avatars', + url: profile.avatar_url, + transform: { + quality: 40, + resize: 'fill', + } + }); + + if (response.success) { + setAvatarUrl(response.data); + } + } catch (error) { + console.error('Error getting signed URL:', error); + } + } + }; + getAvatarUrl().catch((error) => { + toast.error(error instanceof Error ? error.message : 'Failed to get signed avatar url.'); + }); + }, [profile]); + + const handleAvatarClick = () => { + fileInputRef.current?.click(); + }; + + const handleFileChange = async (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (!file) + throw new Error('No file selected'); + + try { + setIsUploading(true); + + const fileExt = file.name.split('.').pop(); + const fileName = `${Date.now()}-${profile?.id ?? Math.random().toString(36).substring(2,15)}.${fileExt}`; + + const uploadResult = await uploadFile({ + bucket: 'avatars', + path: fileName, + file, + options: { + upsert: true, + contentType: file.type, + }, + }); + if (!uploadResult.success) + throw new Error(uploadResult.error ?? 'Failed to upload avatar'); + + const updateResult = await updateProfile({ + avatar_url: uploadResult.data, + }); + + if (!updateResult.success) + throw new Error(updateResult.error ?? 'Failed to update profile'); + + setProfile(updateResult.data); + toast.success('Avatar updated successfully.') + + } catch (error) { + toast.error(error instanceof Error ? error.message : 'Failed to uploaad avatar.'); + } finally { + setIsUploading(false); + if (fileInputRef.current) fileInputRef.current.value = ''; + } + }; + + const getInitials = (name: string) => { + return name + .split(' ') + .map((n) => n[0]) + .join('') + .toUpperCase(); + } + + const onSubmit = async (values: z.infer) => { + try { + setIsLoading(true); + const result = await updateProfile({ + full_name: values.full_name, + email: values.email, + }); + + if (!result.success) { + throw new Error(result.error ?? 'Failed to update profile'); + } + setProfile(result.data); + toast.success('Profile updated successfully!'); + } catch (error) { + toast.error(error instanceof Error ? error.message : 'Failed to update profile.'); + } finally { + setIsLoading(false); + } + }; + + if (profile === undefined) + return ( +
+

Unauthorized

+
+ ); + + return ( + + + + {profile?.full_name ?? 'Profile'} + + + Manage your personal information & how it appears to others. + + + + {isLoading && !profile ? ( +
+
+
+
+
+
+ ) : ( +
+
+
+ + {avatarUrl ? ( + + ) : ( + + {profile?.full_name + ? profile.full_name.split(' ').map(n => n[0]).join('').toUpperCase() + : } + + )} + +
+ +
+ +
+ {isUploading && ( +
Uploading...
+ )} +

+ Click on the avatar to upload a new image +

+
+ +
+ + ( + + Full Name + + + + + Your public display name. + + + + )} + /> + ( + + Email + + + + + Your email address associated with your account. + + + + )} + /> +
+ +
+ + +
+ )} +
+
+ ); +}; +export default ProfilePage; diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 1ca6f2a..001fb81 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -3,8 +3,8 @@ import '@/styles/globals.css'; import { Geist } from 'next/font/google'; import { cn } from '@/lib/utils'; import { ThemeProvider } from '@/components/context/theme'; -import Navigation from '@/components/navigation'; -import Footer from '@/components/footer'; +import Navigation from '@/components/default/navigation'; +import Footer from '@/components/default/footer'; import { Toaster } from '@/components/ui' export const metadata: Metadata = { diff --git a/src/app/page.tsx b/src/app/page.tsx index 7b10b97..994832a 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,6 +1,6 @@ 'use server'; -import { FetchDataSteps } from '@/components/tutorial'; +import { FetchDataSteps } from '@/components/default/tutorial'; import { InfoIcon } from 'lucide-react'; import { getUser } from '@/lib/actions'; import type { User } from '@/utils/supabase'; diff --git a/src/components/footer/index.tsx b/src/components/default/footer/index.tsx similarity index 100% rename from src/components/footer/index.tsx rename to src/components/default/footer/index.tsx diff --git a/src/components/navigation/auth/avatar.tsx b/src/components/default/navigation/auth/AvatarDropdown.tsx similarity index 98% rename from src/components/navigation/auth/avatar.tsx rename to src/components/default/navigation/auth/AvatarDropdown.tsx index 43b693b..5efcc41 100644 --- a/src/components/navigation/auth/avatar.tsx +++ b/src/components/default/navigation/auth/AvatarDropdown.tsx @@ -1,7 +1,6 @@ 'use client'; import { useState, useEffect } from 'react'; -import { useRouter } from 'next/navigation'; import { Avatar, AvatarFallback, diff --git a/src/components/navigation/auth/index.tsx b/src/components/default/navigation/auth/index.tsx similarity index 94% rename from src/components/navigation/auth/index.tsx rename to src/components/default/navigation/auth/index.tsx index d12ffe1..13fd2cc 100644 --- a/src/components/navigation/auth/index.tsx +++ b/src/components/default/navigation/auth/index.tsx @@ -3,7 +3,7 @@ import Link from 'next/link'; import { Button } from '@/components/ui'; import { getProfile } from '@/lib/actions'; -import AvatarDropdown from './avatar'; +import AvatarDropdown from './AvatarDropdown'; const NavigationAuth = async () => { try { diff --git a/src/components/navigation/index.tsx b/src/components/default/navigation/index.tsx similarity index 94% rename from src/components/navigation/index.tsx rename to src/components/default/navigation/index.tsx index 9941c11..c3206fd 100644 --- a/src/components/navigation/index.tsx +++ b/src/components/default/navigation/index.tsx @@ -2,7 +2,7 @@ import Link from 'next/link'; import { Button } from '@/components/ui'; -import NavigationAuth from '@/components/navigation/auth'; +import NavigationAuth from './auth'; import { ThemeToggle } from '@/components/context/theme'; const Navigation = () => { diff --git a/src/components/default/profile/AvatarUpload.tsx b/src/components/default/profile/AvatarUpload.tsx new file mode 100644 index 0000000..fc4d9d1 --- /dev/null +++ b/src/components/default/profile/AvatarUpload.tsx @@ -0,0 +1,71 @@ +import { useFileUpload } from '@/lib/hooks/useFileUpload'; +import { useAvatar } from '@/lib/hooks/useAvatar'; +import type { Profile } from '@/utils/supabase'; +import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui'; +import { Pencil, User, Loader2 } from 'lucide-react'; + +type AvatarUploadProps = { + profile?: Profile; + onAvatarUploaded: (path: string) => Promise; +}; + +export const AvatarUpload = ({ profile, onAvatarUploaded }: AvatarUploadProps) => { + const { avatarUrl } = useAvatar(profile); + const { isUploading, fileInputRef, uploadToStorage } = useFileUpload(); + + const handleAvatarClick = () => { + fileInputRef.current?.click(); + }; + + const handleFileChange = async (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (!file) return; + + const result = await uploadToStorage(file, 'avatars'); + if (result.success && result.path) { + await onAvatarUploaded(result.path); + } + }; + + return ( +
+
+ + {avatarUrl ? ( + + ) : ( + + {profile?.full_name + ? profile.full_name.split(' ').map(n => n[0]).join('').toUpperCase() + : } + + )} + +
+ +
+ +
+ {isUploading && ( +
+ + Uploading... +
+ )} +

+ Click on the avatar to upload a new image +

+
+ ); +} diff --git a/src/components/default/profile/ProfileForm.tsx b/src/components/default/profile/ProfileForm.tsx new file mode 100644 index 0000000..ea387b9 --- /dev/null +++ b/src/components/default/profile/ProfileForm.tsx @@ -0,0 +1,110 @@ +import { z } from 'zod'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { useForm } from 'react-hook-form'; +import type { Profile } from '@/utils/supabase'; +import { + Button, + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage, + Input, +} from '@/components/ui'; +import { Loader2 } from 'lucide-react'; +import { useEffect } from 'react'; + +const formSchema = z.object({ + full_name: z.string().min(5, { + message: 'Full name is required & must be at least 5 characters.' + }), + email: z.string().email(), +}); + +type ProfileFormProps = { + profile?: Profile; + isLoading: boolean; + onSubmit: (values: z.infer) => Promise; +}; + +export function ProfileForm({ profile, isLoading, onSubmit }: ProfileFormProps) { + const form = useForm>({ + resolver: zodResolver(formSchema), + defaultValues: { + full_name: profile?.full_name ?? '', + email: profile?.email ?? '', + }, + }); + + // Update form values when profile changes + useEffect(() => { + if (profile) { + form.reset({ + full_name: profile.full_name ?? '', + email: profile.email ?? '', + }); + } + }, [profile, form]); + + const handleSubmit = async (values: z.infer) => { + await onSubmit(values); + }; + + return ( +
+ + ( + + Full Name + + + + + Your public display name. + + + + )} + /> + + ( + + Email + + + + + Your email address associated with your account. + + + + )} + /> + +
+ +
+ + + ); +} diff --git a/src/components/default/profile/index.tsx b/src/components/default/profile/index.tsx new file mode 100644 index 0000000..d054aae --- /dev/null +++ b/src/components/default/profile/index.tsx @@ -0,0 +1,2 @@ +export * from './AvatarUpload'; +export * from './ProfileForm'; diff --git a/src/components/default/submit-button.tsx b/src/components/default/submit-button.tsx index b486071..319f28c 100644 --- a/src/components/default/submit-button.tsx +++ b/src/components/default/submit-button.tsx @@ -1,6 +1,6 @@ 'use client'; -import { Button } from '@/components/ui/button'; +import { Button } from '@/components/ui'; import { type ComponentProps } from 'react'; import { useFormStatus } from 'react-dom'; diff --git a/src/components/tutorial/code-block.tsx b/src/components/default/tutorial/code-block.tsx similarity index 100% rename from src/components/tutorial/code-block.tsx rename to src/components/default/tutorial/code-block.tsx diff --git a/src/components/tutorial/fetch-data-steps.tsx b/src/components/default/tutorial/fetch-data-steps.tsx similarity index 97% rename from src/components/tutorial/fetch-data-steps.tsx rename to src/components/default/tutorial/fetch-data-steps.tsx index d62ad76..1a5059c 100644 --- a/src/components/tutorial/fetch-data-steps.tsx +++ b/src/components/default/tutorial/fetch-data-steps.tsx @@ -1,4 +1,4 @@ -import { TutorialStep, CodeBlock } from '@/components/tutorial'; +import { TutorialStep, CodeBlock } from '@/components/default/tutorial'; const create = `create table notes ( id bigserial primary key, diff --git a/src/components/default/tutorial/index.tsx b/src/components/default/tutorial/index.tsx new file mode 100644 index 0000000..024de97 --- /dev/null +++ b/src/components/default/tutorial/index.tsx @@ -0,0 +1,3 @@ +export { CodeBlock } from './code-block'; +export { FetchDataSteps } from './fetch-data-steps'; +export { TutorialStep } from './tutorial-step'; diff --git a/src/components/tutorial/tutorial-step.tsx b/src/components/default/tutorial/tutorial-step.tsx similarity index 100% rename from src/components/tutorial/tutorial-step.tsx rename to src/components/default/tutorial/tutorial-step.tsx diff --git a/src/components/tutorial/index.tsx b/src/components/tutorial/index.tsx deleted file mode 100644 index b208baf..0000000 --- a/src/components/tutorial/index.tsx +++ /dev/null @@ -1,3 +0,0 @@ -export { CodeBlock } from '@/components/tutorial/code-block'; -export { FetchDataSteps } from '@/components/tutorial/fetch-data-steps'; -export { TutorialStep } from '@/components/tutorial/tutorial-step'; diff --git a/src/lib/actions/storage.ts b/src/lib/actions/storage.ts index 6c30a5b..16c6c70 100644 --- a/src/lib/actions/storage.ts +++ b/src/lib/actions/storage.ts @@ -157,7 +157,7 @@ export async function listFiles({ offset?: number; sortBy?: { column: string; order: 'asc' | 'desc' }; }; -}): Promise>> { +}): Promise>> { try { const supabase = await createServerClient(); const { data, error } = await supabase.storage @@ -169,6 +169,7 @@ export async function listFiles({ return { success: true, data }; } catch (error) { + console.error('Could not list files!', error); return { success: false, error: diff --git a/src/lib/hooks/useAvatar.ts b/src/lib/hooks/useAvatar.ts new file mode 100644 index 0000000..13e8fd6 --- /dev/null +++ b/src/lib/hooks/useAvatar.ts @@ -0,0 +1,46 @@ +import { useState, useEffect } from 'react'; +import { getSignedUrl } from '@/lib/actions'; +import type { Profile } from '@/utils/supabase'; +import { toast } from 'sonner'; + +export const useAvatar = (profile?: Profile) => { + const [avatarUrl, setAvatarUrl] = useState(undefined); + const [isLoading, setIsLoading] = useState(false); + + useEffect(() => { + const getAvatarUrl = async () => { + if (profile?.avatar_url) { + try { + setIsLoading(true); + const response = await getSignedUrl({ + bucket: 'avatars', + url: profile.avatar_url, + transform: { + quality: 20, + } + }); + + if (response.success) { + setAvatarUrl(response.data); + } + } catch (error) { + console.error('Error getting signed URL:', error); + } finally { + setIsLoading(false); + } + } else { + setAvatarUrl(undefined); + } + }; + + getAvatarUrl().catch((error) => { + toast.error(error instanceof Error ? error.message : 'Failed to get signed avatar url.'); + }); + + }, [profile]); + + return { + avatarUrl, + isLoading, + }; +} diff --git a/src/lib/hooks/useFileUpload.ts b/src/lib/hooks/useFileUpload.ts new file mode 100644 index 0000000..5a198e7 --- /dev/null +++ b/src/lib/hooks/useFileUpload.ts @@ -0,0 +1,49 @@ +import { useState, useRef } from 'react'; +import { uploadFile } from '@/lib/actions'; +import { toast } from 'sonner'; + +export const useFileUpload = () => { + const [isUploading, setIsUploading] = useState(false); + const fileInputRef = useRef(null); + + const uploadToStorage = async (file: File, bucket: string) => { + try { + setIsUploading(true); + + // Generate a unique filename to avoid collisions + const fileExt = file.name.split('.').pop(); + const fileName = `${Date.now()}-${Math.random().toString(36).substring(2, 15)}.${fileExt}`; + + // Upload the file to Supabase storage + const uploadResult = await uploadFile({ + bucket, + path: fileName, + file, + options: { + upsert: true, + contentType: file.type, + }, + }); + + if (!uploadResult.success) { + throw new Error(uploadResult.error || `Failed to upload to ${bucket}`); + } + + return { success: true, path: uploadResult.data }; + } catch (error) { + console.error(`Error uploading to ${bucket}:`, error); + toast.error(error instanceof Error ? error.message : `Failed to upload to ${bucket}`); + return { success: false, error }; + } finally { + setIsUploading(false); + // Clear the input value so the same file can be selected again + if (fileInputRef.current) fileInputRef.current.value = ''; + } + }; + + return { + isUploading, + fileInputRef, + uploadToStorage, + }; +} diff --git a/src/lib/hooks/useProfile.ts b/src/lib/hooks/useProfile.ts new file mode 100644 index 0000000..6bed57d --- /dev/null +++ b/src/lib/hooks/useProfile.ts @@ -0,0 +1,57 @@ +import { useState, useEffect } from 'react'; +import { getProfile, updateProfile } from '@/lib/actions'; +import type { Profile } from '@/utils/supabase'; +import { toast } from 'sonner'; + +export function useProfile() { + const [profile, setProfile] = useState(undefined); + const [isLoading, setIsLoading] = useState(true); + + useEffect(() => { + const fetchProfile = async () => { + try { + setIsLoading(true); + const profileResponse = await getProfile(); + if (!profileResponse.success) + throw new Error('Profile response unsuccessful'); + setProfile(profileResponse.data); + } catch (error) { + setProfile(undefined); + } finally { + setIsLoading(false); + } + }; + fetchProfile().catch((error) => { + toast.error(error instanceof Error ? error.message : 'Failed to get profile'); + }); + }, []); + + const updateUserProfile = async (data: { + full_name?: string; + email?: string; + avatar_url?: string; + }) => { + try { + setIsLoading(true); + const result = await updateProfile(data); + if (!result.success) { + throw new Error(result.error ?? 'Failed to update profile'); + } + setProfile(result.data); + toast.success('Profile updated successfully!'); + return { success: true, data: result.data }; + } catch (error) { + console.error('Error updating profile: ', error); + toast.error(error instanceof Error ? error.message : 'Failed to update profile'); + return { success: false, error }; + } finally { + setIsLoading(false); + } + }; + + return { + profile, + isLoading, + updateProfile: updateUserProfile, + }; +}