From 0f92f7eb7f45e77cac8dfa3452caaa0119aaaef3 Mon Sep 17 00:00:00 2001 From: Gib Date: Tue, 20 May 2025 16:38:40 -0500 Subject: [PATCH] Not even sure but I'm sure it's better --- src/app/(auth-pages)/profile/page.tsx | 35 ++- src/app/(auth-pages)/profile/page.tsx.bak | 286 ------------------ src/app/layout.tsx | 2 +- src/app/page.tsx | 16 +- .../navigation/auth/AvatarDropdown.tsx | 16 +- .../default/profile/AvatarUpload.tsx | 81 +++-- .../default/profile/ProfileForm.tsx | 25 +- src/components/ui/avatar.tsx | 32 +- src/components/ui/card.tsx | 68 ++--- src/components/ui/dropdown-menu.tsx | 118 ++++---- src/components/ui/form.tsx | 105 +++---- src/components/ui/separator.tsx | 20 +- src/components/ui/sonner.tsx | 24 +- src/lib/actions/auth.ts | 18 +- src/lib/actions/public.ts | 15 +- src/lib/hooks/useAvatar.ts | 17 +- src/lib/hooks/useFileUpload.ts | 16 +- src/lib/hooks/useProfile.ts | 10 +- src/utils/supabase/middleware.ts | 2 +- 19 files changed, 331 insertions(+), 575 deletions(-) delete mode 100644 src/app/(auth-pages)/profile/page.tsx.bak diff --git a/src/app/(auth-pages)/profile/page.tsx b/src/app/(auth-pages)/profile/page.tsx index 5f022b3..80313c0 100644 --- a/src/app/(auth-pages)/profile/page.tsx +++ b/src/app/(auth-pages)/profile/page.tsx @@ -18,7 +18,10 @@ const ProfilePage = () => { await updateProfile({ avatar_url: path }); }; - const handleProfileSubmit = async (values: { full_name: string; email: string }) => { + const handleProfileSubmit = async (values: { + full_name: string; + email: string; + }) => { await updateProfile({ full_name: values.full_name, email: values.email, @@ -31,34 +34,34 @@ const ProfilePage = () => {

Unauthorized

); - + return ( -
- - - Your Profile +
+ + + Your Profile Manage your personal information and how it appears to others {isLoading && !profile ? ( -
- +
+
) : ( -
- + -
)} diff --git a/src/app/(auth-pages)/profile/page.tsx.bak b/src/app/(auth-pages)/profile/page.tsx.bak deleted file mode 100644 index c6426af..0000000 --- a/src/app/(auth-pages)/profile/page.tsx.bak +++ /dev/null @@ -1,286 +0,0 @@ -'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 001fb81..b24f4c7 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -5,7 +5,7 @@ import { cn } from '@/lib/utils'; import { ThemeProvider } from '@/components/context/theme'; import Navigation from '@/components/default/navigation'; import Footer from '@/components/default/footer'; -import { Toaster } from '@/components/ui' +import { Toaster } from '@/components/ui'; export const metadata: Metadata = { title: 'T3 Template with Supabase', diff --git a/src/app/page.tsx b/src/app/page.tsx index 994832a..6e32f89 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -20,17 +20,21 @@ const HomePage = async () => { return (
-
+
- This is a protected component that you can only see as an authenticated - user + This is a protected component that you can only see as an + authenticated user

Your user details

-
+        
           {JSON.stringify(user, null, 2)}
         
diff --git a/src/components/default/navigation/auth/AvatarDropdown.tsx b/src/components/default/navigation/auth/AvatarDropdown.tsx index d55c3c2..4671a33 100644 --- a/src/components/default/navigation/auth/AvatarDropdown.tsx +++ b/src/components/default/navigation/auth/AvatarDropdown.tsx @@ -13,7 +13,7 @@ import { DropdownMenuSeparator, DropdownMenuTrigger, } from '@/components/ui'; -import { useProfile, useAvatar } from '@/lib/hooks' +import { useProfile, useAvatar } from '@/lib/hooks'; import { signOut } from '@/lib/actions'; import { User } from 'lucide-react'; @@ -42,10 +42,16 @@ const AvatarDropdown = () => { {avatarUrl ? ( ) : ( - - {profile?.full_name - ? profile.full_name.split(' ').map(n => n[0]).join('').toUpperCase() - : } + + {profile?.full_name ? ( + profile.full_name + .split(' ') + .map((n) => n[0]) + .join('') + .toUpperCase() + ) : ( + + )} )} diff --git a/src/components/default/profile/AvatarUpload.tsx b/src/components/default/profile/AvatarUpload.tsx index 60e8a5c..a1cbab5 100644 --- a/src/components/default/profile/AvatarUpload.tsx +++ b/src/components/default/profile/AvatarUpload.tsx @@ -9,7 +9,10 @@ type AvatarUploadProps = { onAvatarUploaded: (path: string) => Promise; }; -export const AvatarUpload = ({ profile, onAvatarUploaded }: AvatarUploadProps) => { +export const AvatarUpload = ({ + profile, + onAvatarUploaded, +}: AvatarUploadProps) => { const { avatarUrl, isLoading } = useAvatar(profile); const { isUploading, fileInputRef, uploadToStorage } = useFileUpload(); @@ -20,7 +23,7 @@ export const AvatarUpload = ({ profile, onAvatarUploaded }: AvatarUploadProps) = 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); @@ -29,59 +32,77 @@ export const AvatarUpload = ({ profile, onAvatarUploaded }: AvatarUploadProps) = if (isLoading) { return ( -
-
- - - {profile?.full_name - ? profile.full_name.split(' ').map(n => n[0]).join('').toUpperCase() - : } - - +
+
+ + + {profile?.full_name ? ( + profile.full_name + .split(' ') + .map((n) => n[0]) + .join('') + .toUpperCase() + ) : ( + + )} + + +
-
); } return ( -
-
- +
+
+ {avatarUrl ? ( ) : ( - - {profile?.full_name - ? profile.full_name.split(' ').map(n => n[0]).join('').toUpperCase() - : } + + {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 index 73fb641..0541dfd 100644 --- a/src/components/default/profile/ProfileForm.tsx +++ b/src/components/default/profile/ProfileForm.tsx @@ -18,7 +18,7 @@ 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.' + message: 'Full name is required & must be at least 5 characters.', }), email: z.string().email(), }); @@ -29,7 +29,11 @@ type ProfileFormProps = { onSubmit: (values: z.infer) => Promise; }; -export function ProfileForm({ profile, isLoading, onSubmit }: ProfileFormProps) { +export function ProfileForm({ + profile, + isLoading, + onSubmit, +}: ProfileFormProps) { const form = useForm>({ resolver: zodResolver(formSchema), defaultValues: { @@ -54,10 +58,7 @@ export function ProfileForm({ profile, isLoading, onSubmit }: ProfileFormProps) return (
- + - - Your public display name. - + Your public display name. )} /> - + )} /> - -
+ +