Not even sure but I'm sure it's better

This commit is contained in:
Gabriel Brown 2025-05-20 16:38:40 -05:00
parent 259aa46ef8
commit 0f92f7eb7f
19 changed files with 331 additions and 575 deletions

View File

@ -18,7 +18,10 @@ const ProfilePage = () => {
await updateProfile({ avatar_url: path }); 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({ await updateProfile({
full_name: values.full_name, full_name: values.full_name,
email: values.email, email: values.email,
@ -33,21 +36,21 @@ const ProfilePage = () => {
); );
return ( return (
<div className="max-w-3xl mx-auto p-4"> <div className='max-w-3xl min-w-sm mx-auto p-4'>
<Card className="mb-8"> <Card className='mb-8'>
<CardHeader className="pb-2"> <CardHeader className='pb-2'>
<CardTitle className="text-2xl">Your Profile</CardTitle> <CardTitle className='text-2xl'>Your Profile</CardTitle>
<CardDescription> <CardDescription>
Manage your personal information and how it appears to others Manage your personal information and how it appears to others
</CardDescription> </CardDescription>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
{isLoading && !profile ? ( {isLoading && !profile ? (
<div className="flex justify-center py-8"> <div className='flex justify-center py-8'>
<Loader2 className="h-8 w-8 animate-spin text-gray-500" /> <Loader2 className='h-8 w-8 animate-spin text-gray-500' />
</div> </div>
) : ( ) : (
<div className="space-y-8"> <div className='space-y-8'>
<AvatarUpload <AvatarUpload
profile={profile} profile={profile}
onAvatarUploaded={handleAvatarUploaded} onAvatarUploaded={handleAvatarUploaded}

View File

@ -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<Profile | undefined>(undefined);
const [avatarUrl, setAvatarUrl] = useState<string | undefined>(undefined);
const [isLoading, setIsLoading] = useState(true);
const [isUploading, setIsUploading] = useState(false);
const fileInputRef = useRef<HTMLInputElement>(null);
const form = useForm<z.infer<typeof formSchema>>({
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<HTMLInputElement>) => {
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<typeof formSchema>) => {
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 (
<div className='flex p-5 items-center justify-center'>
<h1>Unauthorized</h1>
</div>
);
return (
<Card className='p-8'>
<CardHeader className='pb-2'>
<CardTitle className='text-2xl'>
{profile?.full_name ?? 'Profile'}
</CardTitle>
<CardDescription>
Manage your personal information & how it appears to others.
</CardDescription>
</CardHeader>
<CardContent>
{isLoading && !profile ? (
<div className="flex justify-center py-8">
<div className="animate-pulse text-center">
<div className="h-24 w-24 rounded-full bg-gray-200 mx-auto mb-4"></div>
<div className="h-4 w-48 bg-gray-200 mx-auto"></div>
</div>
</div>
) : (
<div className="space-y-8">
<div className="flex flex-col items-center">
<div className="relative group cursor-pointer mb-4" onClick={handleAvatarClick}>
<Avatar className="h-32 w-32">
{avatarUrl ? (
<AvatarImage src={avatarUrl} alt={profile.full_name ?? 'User'} />
) : (
<AvatarFallback className="text-2xl">
{profile?.full_name
? profile.full_name.split(' ').map(n => n[0]).join('').toUpperCase()
: <User size={32} />}
</AvatarFallback>
)}
</Avatar>
<div className="absolute inset-0 rounded-full bg-black/0 group-hover:bg-black/50 transition-all flex items-center justify-center">
<Pencil className="text-white opacity-0 group-hover:opacity-100 transition-opacity" size={24} />
</div>
<input
ref={fileInputRef}
type="file"
accept="image/*"
className="hidden"
onChange={handleFileChange}
disabled={isUploading}
/>
</div>
{isUploading && (
<div className="text-sm text-gray-500">Uploading...</div>
)}
<p className="text-sm text-gray-500 mt-2">
Click on the avatar to upload a new image
</p>
</div>
<Separator />
<Form { ...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
className='space-y-6'
>
<FormField
control={form.control}
name='full_name'
render={({ field }) => (
<FormItem>
<FormLabel>Full Name</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormDescription>
Your public display name.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='email'
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormDescription>
Your email address associated with your account.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<div className="flex justify-center">
<Button type='submit' disabled={isLoading}>
{isLoading ? 'Saving...' : 'Save Changes'}
</Button>
</div>
</form>
</Form>
</div>
)}
</CardContent>
</Card>
);
};
export default ProfilePage;

View File

@ -5,7 +5,7 @@ import { cn } from '@/lib/utils';
import { ThemeProvider } from '@/components/context/theme'; import { ThemeProvider } from '@/components/context/theme';
import Navigation from '@/components/default/navigation'; import Navigation from '@/components/default/navigation';
import Footer from '@/components/default/footer'; import Footer from '@/components/default/footer';
import { Toaster } from '@/components/ui' import { Toaster } from '@/components/ui';
export const metadata: Metadata = { export const metadata: Metadata = {
title: 'T3 Template with Supabase', title: 'T3 Template with Supabase',

View File

@ -20,17 +20,21 @@ const HomePage = async () => {
return ( return (
<div className='flex-1 w-full flex flex-col gap-12'> <div className='flex-1 w-full flex flex-col gap-12'>
<div className='w-full'> <div className='w-full'>
<div className='bg-accent text-sm p-3 px-5 <div
rounded-md text-foreground flex gap-3 items-center'> className='bg-accent text-sm p-3 px-5
rounded-md text-foreground flex gap-3 items-center'
>
<InfoIcon size='16' strokeWidth={2} /> <InfoIcon size='16' strokeWidth={2} />
This is a protected component that you can only see as an authenticated This is a protected component that you can only see as an
user authenticated user
</div> </div>
</div> </div>
<div className='flex flex-col gap-2 items-start'> <div className='flex flex-col gap-2 items-start'>
<h2 className='font-bold text-2xl mb-4'>Your user details</h2> <h2 className='font-bold text-2xl mb-4'>Your user details</h2>
<pre className='text-xs font-mono p-3 rounded <pre
border max-h-32 overflow-auto'> className='text-xs font-mono p-3 rounded
border max-h-32 overflow-auto'
>
{JSON.stringify(user, null, 2)} {JSON.stringify(user, null, 2)}
</pre> </pre>
</div> </div>

View File

@ -13,7 +13,7 @@ import {
DropdownMenuSeparator, DropdownMenuSeparator,
DropdownMenuTrigger, DropdownMenuTrigger,
} from '@/components/ui'; } from '@/components/ui';
import { useProfile, useAvatar } from '@/lib/hooks' import { useProfile, useAvatar } from '@/lib/hooks';
import { signOut } from '@/lib/actions'; import { signOut } from '@/lib/actions';
import { User } from 'lucide-react'; import { User } from 'lucide-react';
@ -42,10 +42,16 @@ const AvatarDropdown = () => {
{avatarUrl ? ( {avatarUrl ? (
<AvatarImage src={avatarUrl} /> <AvatarImage src={avatarUrl} />
) : ( ) : (
<AvatarFallback className="text-2xl"> <AvatarFallback className='text-2xl'>
{profile?.full_name {profile?.full_name ? (
? profile.full_name.split(' ').map(n => n[0]).join('').toUpperCase() profile.full_name
: <User size={32} />} .split(' ')
.map((n) => n[0])
.join('')
.toUpperCase()
) : (
<User size={32} />
)}
</AvatarFallback> </AvatarFallback>
)} )}
</Avatar> </Avatar>

View File

@ -9,7 +9,10 @@ type AvatarUploadProps = {
onAvatarUploaded: (path: string) => Promise<void>; onAvatarUploaded: (path: string) => Promise<void>;
}; };
export const AvatarUpload = ({ profile, onAvatarUploaded }: AvatarUploadProps) => { export const AvatarUpload = ({
profile,
onAvatarUploaded,
}: AvatarUploadProps) => {
const { avatarUrl, isLoading } = useAvatar(profile); const { avatarUrl, isLoading } = useAvatar(profile);
const { isUploading, fileInputRef, uploadToStorage } = useFileUpload(); const { isUploading, fileInputRef, uploadToStorage } = useFileUpload();
@ -29,59 +32,77 @@ export const AvatarUpload = ({ profile, onAvatarUploaded }: AvatarUploadProps) =
if (isLoading) { if (isLoading) {
return ( return (
<div className="flex flex-col items-center"> <div className='flex flex-col items-center'>
<div className="relative group cursor-pointer mb-4"> <div className='mb-4'>
<Avatar> <Avatar className='h-32 w-32'>
<AvatarFallback className="text-2xl"> <AvatarFallback className='text-2xl'>
{profile?.full_name {profile?.full_name ? (
? profile.full_name.split(' ').map(n => n[0]).join('').toUpperCase() profile.full_name
: <User size={32} />} .split(' ')
</AvatarFallback> .map((n) => n[0])
</Avatar> .join('')
.toUpperCase()
) : (
<User size={32} />
)}
</AvatarFallback>
</Avatar>
</div>
</div> </div>
</div>
); );
} }
return ( return (
<div className="flex flex-col items-center"> <div className='flex flex-col items-center'>
<div className="relative group cursor-pointer mb-4" onClick={handleAvatarClick}> <div
<Avatar className="h-32 w-32"> className='relative group cursor-pointer mb-4'
onClick={handleAvatarClick}
>
<Avatar className='h-32 w-32'>
{avatarUrl ? ( {avatarUrl ? (
<AvatarImage src={avatarUrl} alt={profile?.full_name ?? 'User'} /> <AvatarImage src={avatarUrl} alt={profile?.full_name ?? 'User'} />
) : ( ) : (
<AvatarFallback className="text-2xl"> <AvatarFallback className='text-2xl'>
{profile?.full_name {profile?.full_name ? (
? profile.full_name.split(' ').map(n => n[0]).join('').toUpperCase() profile.full_name
: <User size={32} />} .split(' ')
.map((n) => n[0])
.join('')
.toUpperCase()
) : (
<User size={32} />
)}
</AvatarFallback> </AvatarFallback>
)} )}
</Avatar> </Avatar>
<div className="absolute inset-0 rounded-full bg-black/0 group-hover:bg-black/50 <div
transition-all flex items-center justify-center" className='absolute inset-0 rounded-full bg-black/0 group-hover:bg-black/50
transition-all flex items-center justify-center'
> >
<Pencil className="text-white opacity-0 group-hover:opacity-100 <Pencil
transition-opacity" size={24} className='text-white opacity-0 group-hover:opacity-100
transition-opacity'
size={24}
/> />
</div> </div>
<input <input
ref={fileInputRef} ref={fileInputRef}
type="file" type='file'
accept="image/*" accept='image/*'
className="hidden" className='hidden'
onChange={handleFileChange} onChange={handleFileChange}
disabled={isUploading} disabled={isUploading}
/> />
</div> </div>
{isUploading && ( {isUploading && (
<div className="flex items-center text-sm text-gray-500 mt-2"> <div className='flex items-center text-sm text-gray-500 mt-2'>
<Loader2 className="h-4 w-4 mr-2 animate-spin" /> <Loader2 className='h-4 w-4 mr-2 animate-spin' />
Uploading... Uploading...
</div> </div>
)} )}
<p className="text-sm text-gray-500 mt-2"> <p className='text-sm text-gray-500 mt-2'>
Click on the avatar to upload a new image Click on the avatar to upload a new image
</p> </p>
</div> </div>
); );
} };

View File

@ -18,7 +18,7 @@ import { useEffect } from 'react';
const formSchema = z.object({ const formSchema = z.object({
full_name: z.string().min(5, { 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(), email: z.string().email(),
}); });
@ -29,7 +29,11 @@ type ProfileFormProps = {
onSubmit: (values: z.infer<typeof formSchema>) => Promise<void>; onSubmit: (values: z.infer<typeof formSchema>) => Promise<void>;
}; };
export function ProfileForm({ profile, isLoading, onSubmit }: ProfileFormProps) { export function ProfileForm({
profile,
isLoading,
onSubmit,
}: ProfileFormProps) {
const form = useForm<z.infer<typeof formSchema>>({ const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema), resolver: zodResolver(formSchema),
defaultValues: { defaultValues: {
@ -54,10 +58,7 @@ export function ProfileForm({ profile, isLoading, onSubmit }: ProfileFormProps)
return ( return (
<Form {...form}> <Form {...form}>
<form <form onSubmit={form.handleSubmit(handleSubmit)} className='space-y-6'>
onSubmit={form.handleSubmit(handleSubmit)}
className='space-y-6'
>
<FormField <FormField
control={form.control} control={form.control}
name='full_name' name='full_name'
@ -67,9 +68,7 @@ export function ProfileForm({ profile, isLoading, onSubmit }: ProfileFormProps)
<FormControl> <FormControl>
<Input {...field} /> <Input {...field} />
</FormControl> </FormControl>
<FormDescription> <FormDescription>Your public display name.</FormDescription>
Your public display name.
</FormDescription>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
)} )}
@ -92,11 +91,11 @@ export function ProfileForm({ profile, isLoading, onSubmit }: ProfileFormProps)
)} )}
/> />
<div className="flex justify-center"> <div className='flex justify-center'>
<Button type='submit' disabled={isLoading}> <Button type='submit' disabled={isLoading}>
{isLoading ? ( {isLoading ? (
<> <>
<Loader2 className="mr-2 h-4 w-4 animate-spin" /> <Loader2 className='mr-2 h-4 w-4 animate-spin' />
Saving... Saving...
</> </>
) : ( ) : (

View File

@ -1,9 +1,9 @@
"use client" 'use client';
import * as React from "react" import * as React from 'react';
import * as AvatarPrimitive from "@radix-ui/react-avatar" import * as AvatarPrimitive from '@radix-ui/react-avatar';
import { cn } from "@/lib/utils" import { cn } from '@/lib/utils';
function Avatar({ function Avatar({
className, className,
@ -11,14 +11,14 @@ function Avatar({
}: React.ComponentProps<typeof AvatarPrimitive.Root>) { }: React.ComponentProps<typeof AvatarPrimitive.Root>) {
return ( return (
<AvatarPrimitive.Root <AvatarPrimitive.Root
data-slot="avatar" data-slot='avatar'
className={cn( className={cn(
"relative flex size-8 shrink-0 overflow-hidden rounded-full", 'relative flex size-8 shrink-0 overflow-hidden rounded-full',
className className,
)} )}
{...props} {...props}
/> />
) );
} }
function AvatarImage({ function AvatarImage({
@ -27,11 +27,11 @@ function AvatarImage({
}: React.ComponentProps<typeof AvatarPrimitive.Image>) { }: React.ComponentProps<typeof AvatarPrimitive.Image>) {
return ( return (
<AvatarPrimitive.Image <AvatarPrimitive.Image
data-slot="avatar-image" data-slot='avatar-image'
className={cn("aspect-square size-full", className)} className={cn('aspect-square size-full', className)}
{...props} {...props}
/> />
) );
} }
function AvatarFallback({ function AvatarFallback({
@ -40,14 +40,14 @@ function AvatarFallback({
}: React.ComponentProps<typeof AvatarPrimitive.Fallback>) { }: React.ComponentProps<typeof AvatarPrimitive.Fallback>) {
return ( return (
<AvatarPrimitive.Fallback <AvatarPrimitive.Fallback
data-slot="avatar-fallback" data-slot='avatar-fallback'
className={cn( className={cn(
"bg-muted flex size-full items-center justify-center rounded-full", 'bg-muted flex size-full items-center justify-center rounded-full',
className className,
)} )}
{...props} {...props}
/> />
) );
} }
export { Avatar, AvatarImage, AvatarFallback } export { Avatar, AvatarImage, AvatarFallback };

View File

@ -1,84 +1,84 @@
import * as React from "react" import * as React from 'react';
import { cn } from "@/lib/utils" import { cn } from '@/lib/utils';
function Card({ className, ...props }: React.ComponentProps<"div">) { function Card({ className, ...props }: React.ComponentProps<'div'>) {
return ( return (
<div <div
data-slot="card" data-slot='card'
className={cn( className={cn(
"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm", 'bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm',
className className,
)} )}
{...props} {...props}
/> />
) );
} }
function CardHeader({ className, ...props }: React.ComponentProps<"div">) { function CardHeader({ className, ...props }: React.ComponentProps<'div'>) {
return ( return (
<div <div
data-slot="card-header" data-slot='card-header'
className={cn( className={cn(
"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6", '@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6',
className className,
)} )}
{...props} {...props}
/> />
) );
} }
function CardTitle({ className, ...props }: React.ComponentProps<"div">) { function CardTitle({ className, ...props }: React.ComponentProps<'div'>) {
return ( return (
<div <div
data-slot="card-title" data-slot='card-title'
className={cn("leading-none font-semibold", className)} className={cn('leading-none font-semibold', className)}
{...props} {...props}
/> />
) );
} }
function CardDescription({ className, ...props }: React.ComponentProps<"div">) { function CardDescription({ className, ...props }: React.ComponentProps<'div'>) {
return ( return (
<div <div
data-slot="card-description" data-slot='card-description'
className={cn("text-muted-foreground text-sm", className)} className={cn('text-muted-foreground text-sm', className)}
{...props} {...props}
/> />
) );
} }
function CardAction({ className, ...props }: React.ComponentProps<"div">) { function CardAction({ className, ...props }: React.ComponentProps<'div'>) {
return ( return (
<div <div
data-slot="card-action" data-slot='card-action'
className={cn( className={cn(
"col-start-2 row-span-2 row-start-1 self-start justify-self-end", 'col-start-2 row-span-2 row-start-1 self-start justify-self-end',
className className,
)} )}
{...props} {...props}
/> />
) );
} }
function CardContent({ className, ...props }: React.ComponentProps<"div">) { function CardContent({ className, ...props }: React.ComponentProps<'div'>) {
return ( return (
<div <div
data-slot="card-content" data-slot='card-content'
className={cn("px-6", className)} className={cn('px-6', className)}
{...props} {...props}
/> />
) );
} }
function CardFooter({ className, ...props }: React.ComponentProps<"div">) { function CardFooter({ className, ...props }: React.ComponentProps<'div'>) {
return ( return (
<div <div
data-slot="card-footer" data-slot='card-footer'
className={cn("flex items-center px-6 [.border-t]:pt-6", className)} className={cn('flex items-center px-6 [.border-t]:pt-6', className)}
{...props} {...props}
/> />
) );
} }
export { export {
@ -89,4 +89,4 @@ export {
CardAction, CardAction,
CardDescription, CardDescription,
CardContent, CardContent,
} };

View File

@ -1,23 +1,23 @@
"use client" 'use client';
import * as React from "react" import * as React from 'react';
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu" import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu';
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react" import { CheckIcon, ChevronRightIcon, CircleIcon } from 'lucide-react';
import { cn } from "@/lib/utils" import { cn } from '@/lib/utils';
function DropdownMenu({ function DropdownMenu({
...props ...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) { }: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) {
return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} /> return <DropdownMenuPrimitive.Root data-slot='dropdown-menu' {...props} />;
} }
function DropdownMenuPortal({ function DropdownMenuPortal({
...props ...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) { }: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) {
return ( return (
<DropdownMenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} /> <DropdownMenuPrimitive.Portal data-slot='dropdown-menu-portal' {...props} />
) );
} }
function DropdownMenuTrigger({ function DropdownMenuTrigger({
@ -25,10 +25,10 @@ function DropdownMenuTrigger({
}: React.ComponentProps<typeof DropdownMenuPrimitive.Trigger>) { }: React.ComponentProps<typeof DropdownMenuPrimitive.Trigger>) {
return ( return (
<DropdownMenuPrimitive.Trigger <DropdownMenuPrimitive.Trigger
data-slot="dropdown-menu-trigger" data-slot='dropdown-menu-trigger'
{...props} {...props}
/> />
) );
} }
function DropdownMenuContent({ function DropdownMenuContent({
@ -39,47 +39,47 @@ function DropdownMenuContent({
return ( return (
<DropdownMenuPrimitive.Portal> <DropdownMenuPrimitive.Portal>
<DropdownMenuPrimitive.Content <DropdownMenuPrimitive.Content
data-slot="dropdown-menu-content" data-slot='dropdown-menu-content'
sideOffset={sideOffset} sideOffset={sideOffset}
className={cn( className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md", 'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md',
className className,
)} )}
{...props} {...props}
/> />
</DropdownMenuPrimitive.Portal> </DropdownMenuPrimitive.Portal>
) );
} }
function DropdownMenuGroup({ function DropdownMenuGroup({
...props ...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) { }: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) {
return ( return (
<DropdownMenuPrimitive.Group data-slot="dropdown-menu-group" {...props} /> <DropdownMenuPrimitive.Group data-slot='dropdown-menu-group' {...props} />
) );
} }
function DropdownMenuItem({ function DropdownMenuItem({
className, className,
inset, inset,
variant = "default", variant = 'default',
...props ...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & { }: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & {
inset?: boolean inset?: boolean;
variant?: "default" | "destructive" variant?: 'default' | 'destructive';
}) { }) {
return ( return (
<DropdownMenuPrimitive.Item <DropdownMenuPrimitive.Item
data-slot="dropdown-menu-item" data-slot='dropdown-menu-item'
data-inset={inset} data-inset={inset}
data-variant={variant} data-variant={variant}
className={cn( className={cn(
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", "focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className className,
)} )}
{...props} {...props}
/> />
) );
} }
function DropdownMenuCheckboxItem({ function DropdownMenuCheckboxItem({
@ -90,22 +90,22 @@ function DropdownMenuCheckboxItem({
}: React.ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem>) { }: React.ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem>) {
return ( return (
<DropdownMenuPrimitive.CheckboxItem <DropdownMenuPrimitive.CheckboxItem
data-slot="dropdown-menu-checkbox-item" data-slot='dropdown-menu-checkbox-item'
className={cn( className={cn(
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", "focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className className,
)} )}
checked={checked} checked={checked}
{...props} {...props}
> >
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center"> <span className='pointer-events-none absolute left-2 flex size-3.5 items-center justify-center'>
<DropdownMenuPrimitive.ItemIndicator> <DropdownMenuPrimitive.ItemIndicator>
<CheckIcon className="size-4" /> <CheckIcon className='size-4' />
</DropdownMenuPrimitive.ItemIndicator> </DropdownMenuPrimitive.ItemIndicator>
</span> </span>
{children} {children}
</DropdownMenuPrimitive.CheckboxItem> </DropdownMenuPrimitive.CheckboxItem>
) );
} }
function DropdownMenuRadioGroup({ function DropdownMenuRadioGroup({
@ -113,10 +113,10 @@ function DropdownMenuRadioGroup({
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioGroup>) { }: React.ComponentProps<typeof DropdownMenuPrimitive.RadioGroup>) {
return ( return (
<DropdownMenuPrimitive.RadioGroup <DropdownMenuPrimitive.RadioGroup
data-slot="dropdown-menu-radio-group" data-slot='dropdown-menu-radio-group'
{...props} {...props}
/> />
) );
} }
function DropdownMenuRadioItem({ function DropdownMenuRadioItem({
@ -126,21 +126,21 @@ function DropdownMenuRadioItem({
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioItem>) { }: React.ComponentProps<typeof DropdownMenuPrimitive.RadioItem>) {
return ( return (
<DropdownMenuPrimitive.RadioItem <DropdownMenuPrimitive.RadioItem
data-slot="dropdown-menu-radio-item" data-slot='dropdown-menu-radio-item'
className={cn( className={cn(
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", "focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className className,
)} )}
{...props} {...props}
> >
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center"> <span className='pointer-events-none absolute left-2 flex size-3.5 items-center justify-center'>
<DropdownMenuPrimitive.ItemIndicator> <DropdownMenuPrimitive.ItemIndicator>
<CircleIcon className="size-2 fill-current" /> <CircleIcon className='size-2 fill-current' />
</DropdownMenuPrimitive.ItemIndicator> </DropdownMenuPrimitive.ItemIndicator>
</span> </span>
{children} {children}
</DropdownMenuPrimitive.RadioItem> </DropdownMenuPrimitive.RadioItem>
) );
} }
function DropdownMenuLabel({ function DropdownMenuLabel({
@ -148,19 +148,19 @@ function DropdownMenuLabel({
inset, inset,
...props ...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & { }: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & {
inset?: boolean inset?: boolean;
}) { }) {
return ( return (
<DropdownMenuPrimitive.Label <DropdownMenuPrimitive.Label
data-slot="dropdown-menu-label" data-slot='dropdown-menu-label'
data-inset={inset} data-inset={inset}
className={cn( className={cn(
"px-2 py-1.5 text-sm font-medium data-[inset]:pl-8", 'px-2 py-1.5 text-sm font-medium data-[inset]:pl-8',
className className,
)} )}
{...props} {...props}
/> />
) );
} }
function DropdownMenuSeparator({ function DropdownMenuSeparator({
@ -169,33 +169,33 @@ function DropdownMenuSeparator({
}: React.ComponentProps<typeof DropdownMenuPrimitive.Separator>) { }: React.ComponentProps<typeof DropdownMenuPrimitive.Separator>) {
return ( return (
<DropdownMenuPrimitive.Separator <DropdownMenuPrimitive.Separator
data-slot="dropdown-menu-separator" data-slot='dropdown-menu-separator'
className={cn("bg-border -mx-1 my-1 h-px", className)} className={cn('bg-border -mx-1 my-1 h-px', className)}
{...props} {...props}
/> />
) );
} }
function DropdownMenuShortcut({ function DropdownMenuShortcut({
className, className,
...props ...props
}: React.ComponentProps<"span">) { }: React.ComponentProps<'span'>) {
return ( return (
<span <span
data-slot="dropdown-menu-shortcut" data-slot='dropdown-menu-shortcut'
className={cn( className={cn(
"text-muted-foreground ml-auto text-xs tracking-widest", 'text-muted-foreground ml-auto text-xs tracking-widest',
className className,
)} )}
{...props} {...props}
/> />
) );
} }
function DropdownMenuSub({ function DropdownMenuSub({
...props ...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) { }: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) {
return <DropdownMenuPrimitive.Sub data-slot="dropdown-menu-sub" {...props} /> return <DropdownMenuPrimitive.Sub data-slot='dropdown-menu-sub' {...props} />;
} }
function DropdownMenuSubTrigger({ function DropdownMenuSubTrigger({
@ -204,22 +204,22 @@ function DropdownMenuSubTrigger({
children, children,
...props ...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & { }: React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & {
inset?: boolean inset?: boolean;
}) { }) {
return ( return (
<DropdownMenuPrimitive.SubTrigger <DropdownMenuPrimitive.SubTrigger
data-slot="dropdown-menu-sub-trigger" data-slot='dropdown-menu-sub-trigger'
data-inset={inset} data-inset={inset}
className={cn( className={cn(
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8", 'focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8',
className className,
)} )}
{...props} {...props}
> >
{children} {children}
<ChevronRightIcon className="ml-auto size-4" /> <ChevronRightIcon className='ml-auto size-4' />
</DropdownMenuPrimitive.SubTrigger> </DropdownMenuPrimitive.SubTrigger>
) );
} }
function DropdownMenuSubContent({ function DropdownMenuSubContent({
@ -228,14 +228,14 @@ function DropdownMenuSubContent({
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubContent>) { }: React.ComponentProps<typeof DropdownMenuPrimitive.SubContent>) {
return ( return (
<DropdownMenuPrimitive.SubContent <DropdownMenuPrimitive.SubContent
data-slot="dropdown-menu-sub-content" data-slot='dropdown-menu-sub-content'
className={cn( className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg", 'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg',
className className,
)} )}
{...props} {...props}
/> />
) );
} }
export { export {
@ -254,4 +254,4 @@ export {
DropdownMenuSub, DropdownMenuSub,
DropdownMenuSubTrigger, DropdownMenuSubTrigger,
DropdownMenuSubContent, DropdownMenuSubContent,
} };

View File

@ -1,8 +1,8 @@
"use client" 'use client';
import * as React from "react" import * as React from 'react';
import * as LabelPrimitive from "@radix-ui/react-label" import * as LabelPrimitive from '@radix-ui/react-label';
import { Slot } from "@radix-ui/react-slot" import { Slot } from '@radix-ui/react-slot';
import { import {
Controller, Controller,
FormProvider, FormProvider,
@ -11,23 +11,23 @@ import {
type ControllerProps, type ControllerProps,
type FieldPath, type FieldPath,
type FieldValues, type FieldValues,
} from "react-hook-form" } from 'react-hook-form';
import { cn } from "@/lib/utils" import { cn } from '@/lib/utils';
import { Label } from "@/components/ui/label" import { Label } from '@/components/ui/label';
const Form = FormProvider const Form = FormProvider;
type FormFieldContextValue< type FormFieldContextValue<
TFieldValues extends FieldValues = FieldValues, TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>, TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
> = { > = {
name: TName name: TName;
} };
const FormFieldContext = React.createContext<FormFieldContextValue>( const FormFieldContext = React.createContext<FormFieldContextValue>(
{} as FormFieldContextValue {} as FormFieldContextValue,
) );
const FormField = < const FormField = <
TFieldValues extends FieldValues = FieldValues, TFieldValues extends FieldValues = FieldValues,
@ -39,21 +39,21 @@ const FormField = <
<FormFieldContext.Provider value={{ name: props.name }}> <FormFieldContext.Provider value={{ name: props.name }}>
<Controller {...props} /> <Controller {...props} />
</FormFieldContext.Provider> </FormFieldContext.Provider>
) );
} };
const useFormField = () => { const useFormField = () => {
const fieldContext = React.useContext(FormFieldContext) const fieldContext = React.useContext(FormFieldContext);
const itemContext = React.useContext(FormItemContext) const itemContext = React.useContext(FormItemContext);
const { getFieldState } = useFormContext() const { getFieldState } = useFormContext();
const formState = useFormState({ name: fieldContext.name }) const formState = useFormState({ name: fieldContext.name });
const fieldState = getFieldState(fieldContext.name, formState) const fieldState = getFieldState(fieldContext.name, formState);
if (!fieldContext) { if (!fieldContext) {
throw new Error("useFormField should be used within <FormField>") throw new Error('useFormField should be used within <FormField>');
} }
const { id } = itemContext const { id } = itemContext;
return { return {
id, id,
@ -62,54 +62,55 @@ const useFormField = () => {
formDescriptionId: `${id}-form-item-description`, formDescriptionId: `${id}-form-item-description`,
formMessageId: `${id}-form-item-message`, formMessageId: `${id}-form-item-message`,
...fieldState, ...fieldState,
} };
} };
type FormItemContextValue = { type FormItemContextValue = {
id: string id: string;
} };
const FormItemContext = React.createContext<FormItemContextValue>( const FormItemContext = React.createContext<FormItemContextValue>(
{} as FormItemContextValue {} as FormItemContextValue,
) );
function FormItem({ className, ...props }: React.ComponentProps<"div">) { function FormItem({ className, ...props }: React.ComponentProps<'div'>) {
const id = React.useId() const id = React.useId();
return ( return (
<FormItemContext.Provider value={{ id }}> <FormItemContext.Provider value={{ id }}>
<div <div
data-slot="form-item" data-slot='form-item'
className={cn("grid gap-2", className)} className={cn('grid gap-2', className)}
{...props} {...props}
/> />
</FormItemContext.Provider> </FormItemContext.Provider>
) );
} }
function FormLabel({ function FormLabel({
className, className,
...props ...props
}: React.ComponentProps<typeof LabelPrimitive.Root>) { }: React.ComponentProps<typeof LabelPrimitive.Root>) {
const { error, formItemId } = useFormField() const { error, formItemId } = useFormField();
return ( return (
<Label <Label
data-slot="form-label" data-slot='form-label'
data-error={!!error} data-error={!!error}
className={cn("data-[error=true]:text-destructive", className)} className={cn('data-[error=true]:text-destructive', className)}
htmlFor={formItemId} htmlFor={formItemId}
{...props} {...props}
/> />
) );
} }
function FormControl({ ...props }: React.ComponentProps<typeof Slot>) { function FormControl({ ...props }: React.ComponentProps<typeof Slot>) {
const { error, formItemId, formDescriptionId, formMessageId } = useFormField() const { error, formItemId, formDescriptionId, formMessageId } =
useFormField();
return ( return (
<Slot <Slot
data-slot="form-control" data-slot='form-control'
id={formItemId} id={formItemId}
aria-describedby={ aria-describedby={
!error !error
@ -119,40 +120,40 @@ function FormControl({ ...props }: React.ComponentProps<typeof Slot>) {
aria-invalid={!!error} aria-invalid={!!error}
{...props} {...props}
/> />
) );
} }
function FormDescription({ className, ...props }: React.ComponentProps<"p">) { function FormDescription({ className, ...props }: React.ComponentProps<'p'>) {
const { formDescriptionId } = useFormField() const { formDescriptionId } = useFormField();
return ( return (
<p <p
data-slot="form-description" data-slot='form-description'
id={formDescriptionId} id={formDescriptionId}
className={cn("text-muted-foreground text-sm", className)} className={cn('text-muted-foreground text-sm', className)}
{...props} {...props}
/> />
) );
} }
function FormMessage({ className, ...props }: React.ComponentProps<"p">) { function FormMessage({ className, ...props }: React.ComponentProps<'p'>) {
const { error, formMessageId } = useFormField() const { error, formMessageId } = useFormField();
const body = error ? String(error?.message ?? "") : props.children const body = error ? String(error?.message ?? '') : props.children;
if (!body) { if (!body) {
return null return null;
} }
return ( return (
<p <p
data-slot="form-message" data-slot='form-message'
id={formMessageId} id={formMessageId}
className={cn("text-destructive text-sm", className)} className={cn('text-destructive text-sm', className)}
{...props} {...props}
> >
{body} {body}
</p> </p>
) );
} }
export { export {
@ -164,4 +165,4 @@ export {
FormDescription, FormDescription,
FormMessage, FormMessage,
FormField, FormField,
} };

View File

@ -1,28 +1,28 @@
"use client" 'use client';
import * as React from "react" import * as React from 'react';
import * as SeparatorPrimitive from "@radix-ui/react-separator" import * as SeparatorPrimitive from '@radix-ui/react-separator';
import { cn } from "@/lib/utils" import { cn } from '@/lib/utils';
function Separator({ function Separator({
className, className,
orientation = "horizontal", orientation = 'horizontal',
decorative = true, decorative = true,
...props ...props
}: React.ComponentProps<typeof SeparatorPrimitive.Root>) { }: React.ComponentProps<typeof SeparatorPrimitive.Root>) {
return ( return (
<SeparatorPrimitive.Root <SeparatorPrimitive.Root
data-slot="separator-root" data-slot='separator-root'
decorative={decorative} decorative={decorative}
orientation={orientation} orientation={orientation}
className={cn( className={cn(
"bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px", 'bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px',
className className,
)} )}
{...props} {...props}
/> />
) );
} }
export { Separator } export { Separator };

View File

@ -1,25 +1,25 @@
"use client" 'use client';
import { useTheme } from "next-themes" import { useTheme } from 'next-themes';
import { Toaster as Sonner, ToasterProps } from "sonner" import { Toaster as Sonner, ToasterProps } from 'sonner';
const Toaster = ({ ...props }: ToasterProps) => { const Toaster = ({ ...props }: ToasterProps) => {
const { theme = "system" } = useTheme() const { theme = 'system' } = useTheme();
return ( return (
<Sonner <Sonner
theme={theme as ToasterProps["theme"]} theme={theme as ToasterProps['theme']}
className="toaster group" className='toaster group'
style={ style={
{ {
"--normal-bg": "var(--popover)", '--normal-bg': 'var(--popover)',
"--normal-text": "var(--popover-foreground)", '--normal-text': 'var(--popover-foreground)',
"--normal-border": "var(--border)", '--normal-border': 'var(--border)',
} as React.CSSProperties } as React.CSSProperties
} }
{...props} {...props}
/> />
) );
} };
export { Toaster } export { Toaster };

View File

@ -3,7 +3,7 @@
import 'server-only'; import 'server-only';
import { encodedRedirect } from '@/utils/utils'; import { encodedRedirect } from '@/utils/utils';
import { createServerClient } from '@/utils/supabase'; import { createServerClient } from '@/utils/supabase';
import type { User } from '@supabase/supabase-js' import type { User } from '@supabase/supabase-js';
import { headers } from 'next/headers'; import { headers } from 'next/headers';
import { redirect } from 'next/navigation'; import { redirect } from 'next/navigation';
import type { Result } from './index'; import type { Result } from './index';
@ -119,11 +119,7 @@ export const resetPassword = async (formData: FormData) => {
} }
if (password !== confirmPassword) { if (password !== confirmPassword) {
encodedRedirect( encodedRedirect('error', '/reset-password', 'Passwords do not match');
'error',
'/reset-password',
'Passwords do not match',
);
} }
const { error } = await supabase.auth.updateUser({ const { error } = await supabase.auth.updateUser({
@ -131,11 +127,7 @@ export const resetPassword = async (formData: FormData) => {
}); });
if (error) { if (error) {
encodedRedirect( encodedRedirect('error', '/reset-password', 'Password update failed');
'error',
'/reset-password',
'Password update failed',
);
} }
encodedRedirect('success', '/reset-password', 'Password updated'); encodedRedirect('success', '/reset-password', 'Password updated');
@ -152,8 +144,8 @@ export const getUser = async (): Promise<Result<User>> => {
const supabase = await createServerClient(); const supabase = await createServerClient();
const { data, error } = await supabase.auth.getUser(); const { data, error } = await supabase.auth.getUser();
if (error) throw error; if (error) throw error;
return {success: true, data: data.user}; return { success: true, data: data.user };
} catch (error) { } catch (error) {
return {success: false, error: 'Could not get user!'}; return { success: false, error: 'Could not get user!' };
} }
}; };

View File

@ -8,7 +8,8 @@ import type { Result } from './index';
export const getProfile = async (): Promise<Result<Profile>> => { export const getProfile = async (): Promise<Result<Profile>> => {
try { try {
const user = await getUser(); const user = await getUser();
if (!user.success || user.data === undefined) throw new Error('User not found'); if (!user.success || user.data === undefined)
throw new Error('User not found');
const supabase = await createServerClient(); const supabase = await createServerClient();
const { data, error } = await supabase const { data, error } = await supabase
.from('profiles') .from('profiles')
@ -40,7 +41,11 @@ export const updateProfile = async ({
avatar_url, avatar_url,
}: updateProfileProps): Promise<Result<Profile>> => { }: updateProfileProps): Promise<Result<Profile>> => {
try { try {
if (full_name === undefined && email === undefined && avatar_url === undefined) if (
full_name === undefined &&
email === undefined &&
avatar_url === undefined
)
throw new Error('No profile data provided'); throw new Error('No profile data provided');
const userResponse = await getUser(); const userResponse = await getUser();
@ -51,9 +56,9 @@ export const updateProfile = async ({
const { data, error } = await supabase const { data, error } = await supabase
.from('profiles') .from('profiles')
.update({ .update({
...(full_name !== undefined && {full_name}), ...(full_name !== undefined && { full_name }),
...(email !== undefined && {email}), ...(email !== undefined && { email }),
...(avatar_url !== undefined && {avatar_url}), ...(avatar_url !== undefined && { avatar_url }),
}) })
.eq('id', userResponse.data.id) .eq('id', userResponse.data.id)
.select() .select()

View File

@ -17,7 +17,7 @@ export const useAvatar = (profile?: Profile) => {
url: profile.avatar_url, url: profile.avatar_url,
transform: { transform: {
quality: 20, quality: 20,
} },
}); });
if (response.success) { if (response.success) {
@ -34,13 +34,16 @@ export const useAvatar = (profile?: Profile) => {
}; };
getAvatarUrl().catch((error) => { getAvatarUrl().catch((error) => {
toast.error(error instanceof Error ? error.message : 'Failed to get signed avatar url.'); toast.error(
error instanceof Error
? error.message
: 'Failed to get signed avatar url.',
);
}); });
}, [profile]); }, [profile]);
return { return {
avatarUrl, avatarUrl,
isLoading, isLoading,
}; };
} };

View File

@ -32,7 +32,11 @@ export const useFileUpload = () => {
return { success: true, path: uploadResult.data }; return { success: true, path: uploadResult.data };
} catch (error) { } catch (error) {
console.error(`Error uploading to ${bucket}:`, error); console.error(`Error uploading to ${bucket}:`, error);
toast.error(error instanceof Error ? error.message : `Failed to upload to ${bucket}`); toast.error(
error instanceof Error
? error.message
: `Failed to upload to ${bucket}`,
);
return { success: false, error }; return { success: false, error };
} finally { } finally {
setIsUploading(false); setIsUploading(false);
@ -46,4 +50,4 @@ export const useFileUpload = () => {
fileInputRef, fileInputRef,
uploadToStorage, uploadToStorage,
}; };
} };

View File

@ -22,7 +22,9 @@ export const useProfile = () => {
} }
}; };
fetchProfile().catch((error) => { fetchProfile().catch((error) => {
toast.error(error instanceof Error ? error.message : 'Failed to get profile'); toast.error(
error instanceof Error ? error.message : 'Failed to get profile',
);
}); });
}, []); }, []);
@ -42,7 +44,9 @@ export const useProfile = () => {
return { success: true, data: result.data }; return { success: true, data: result.data };
} catch (error) { } catch (error) {
console.error('Error updating profile: ', error); console.error('Error updating profile: ', error);
toast.error(error instanceof Error ? error.message : 'Failed to update profile'); toast.error(
error instanceof Error ? error.message : 'Failed to update profile',
);
return { success: false, error }; return { success: false, error };
} finally { } finally {
setIsLoading(false); setIsLoading(false);
@ -54,4 +58,4 @@ export const useProfile = () => {
isLoading, isLoading,
updateProfile: updateUserProfile, updateProfile: updateUserProfile,
}; };
} };

View File

@ -46,7 +46,7 @@ export const updateSession = async (request: NextRequest) => {
} }
//if (request.nextUrl.pathname === '/' && !user.error) { //if (request.nextUrl.pathname === '/' && !user.error) {
//return NextResponse.redirect(new URL('/protected', request.url)); //return NextResponse.redirect(new URL('/protected', request.url));
//} //}
return response; return response;