Cleaned up components and their props

This commit is contained in:
2025-07-07 13:44:28 -05:00
parent edd0a9ccba
commit 2fbb259e62
20 changed files with 258 additions and 193 deletions

BIN
bun.lockb

Binary file not shown.

View File

@@ -48,7 +48,7 @@
"@radix-ui/react-toggle": "^1.1.9", "@radix-ui/react-toggle": "^1.1.9",
"@radix-ui/react-toggle-group": "^1.1.10", "@radix-ui/react-toggle-group": "^1.1.10",
"@radix-ui/react-tooltip": "^1.2.7", "@radix-ui/react-tooltip": "^1.2.7",
"@sentry/nextjs": "^9.34.0", "@sentry/nextjs": "^9.35.0",
"@supabase-cache-helpers/postgrest-react-query": "^1.13.4", "@supabase-cache-helpers/postgrest-react-query": "^1.13.4",
"@supabase/ssr": "^0.6.1", "@supabase/ssr": "^0.6.1",
"@supabase/supabase-js": "^2.50.3", "@supabase/supabase-js": "^2.50.3",
@@ -64,21 +64,21 @@
"import-in-the-middle": "^1.14.2", "import-in-the-middle": "^1.14.2",
"input-otp": "^1.4.2", "input-otp": "^1.4.2",
"lucide-react": "^0.522.0", "lucide-react": "^0.522.0",
"next": "^15.3.4", "next": "^15.3.5",
"next-plausible": "^3.12.4", "next-plausible": "^3.12.4",
"next-themes": "^0.4.6", "next-themes": "^0.4.6",
"postgres": "^3.4.7", "postgres": "^3.4.7",
"react": "^19.1.0", "react": "^19.1.0",
"react-day-picker": "^9.7.0", "react-day-picker": "^9.8.0",
"react-dom": "^19.1.0", "react-dom": "^19.1.0",
"react-hook-form": "^7.59.0", "react-hook-form": "^7.60.0",
"react-icons": "^5.5.0", "react-icons": "^5.5.0",
"react-resizable-panels": "^3.0.3", "react-resizable-panels": "^3.0.3",
"recharts": "^3.0.2", "recharts": "^3.0.2",
"sonner": "^2.0.6", "sonner": "^2.0.6",
"tailwind-merge": "^3.3.1", "tailwind-merge": "^3.3.1",
"vaul": "^1.1.2", "vaul": "^1.1.2",
"zod": "^3.25.71" "zod": "^3.25.75"
}, },
"devDependencies": { "devDependencies": {
"@eslint/eslintrc": "^3.3.1", "@eslint/eslintrc": "^3.3.1",
@@ -90,7 +90,7 @@
"@types/react-dom": "^19.1.6", "@types/react-dom": "^19.1.6",
"drizzle-kit": "^0.30.6", "drizzle-kit": "^0.30.6",
"eslint": "^9.30.1", "eslint": "^9.30.1",
"eslint-config-next": "^15.3.4", "eslint-config-next": "^15.3.5",
"eslint-config-prettier": "^10.1.5", "eslint-config-prettier": "^10.1.5",
"eslint-plugin-drizzle": "^0.2.3", "eslint-plugin-drizzle": "^0.2.3",
"eslint-plugin-prettier": "^5.5.1", "eslint-plugin-prettier": "^5.5.1",

View File

@@ -15,21 +15,15 @@ import { cn } from '@/lib/utils';
export type SignInWithAppleProps = { export type SignInWithAppleProps = {
submitButtonProps?: SubmitButtonProps; submitButtonProps?: SubmitButtonProps;
formClassName?: ComponentProps<'form'>['className']; formProps?: ComponentProps<'form'>;
formProps?: Omit<ComponentProps<'form'>, 'className'>; textProps?: ComponentProps<'p'>;
textClassName?: ComponentProps<'p'>['className']; iconProps?: ComponentProps<'svg'>;
textProps?: Omit<ComponentProps<'p'>, 'className'>;
iconClassName?: ComponentProps<'svg'>['className'];
iconProps?: Omit<ComponentProps<'svg'>, 'className'>;
}; };
export const SignInWithApple = ({ export const SignInWithApple = ({
submitButtonProps, submitButtonProps,
formClassName,
formProps, formProps,
textClassName,
textProps, textProps,
iconClassName,
iconProps, iconProps,
} : SignInWithAppleProps) => { } : SignInWithAppleProps) => {
const router = useRouter(); const router = useRouter();
@@ -58,19 +52,19 @@ export const SignInWithApple = ({
return ( return (
<form <form
onSubmit={handleSignInWithApple}
className={cn('my-4', formClassName)}
{...formProps} {...formProps}
onSubmit={handleSignInWithApple}
className={cn('my-4', formProps?.className)}
> >
<SubmitButton <SubmitButton
disabled={isLoading || loading} disabled={isLoading || loading}
pendingText='Signing in...' pendingText='Signing in...'
className={cn('w-full', submitButtonProps?.className)}
{...submitButtonProps} {...submitButtonProps}
className={cn('w-full', submitButtonProps?.className)}
> >
<div className='flex items-center gap-2'> <div className='flex items-center gap-2'>
<FaApple className={cn('size-5', iconClassName)} {...iconProps} /> <FaApple {...iconProps} className={cn('size-5', iconProps?.className)} />
<p className={cn('text-[1.0rem]', textClassName)} {...textProps}> <p {...textProps} className={cn('text-[1.0rem]', textProps?.className)} >
Sign In with Apple Sign In with Apple
</p> </p>
</div> </div>

View File

@@ -15,21 +15,15 @@ import { cn } from '@/lib/utils';
export type SignInWithMicrosoftProps = { export type SignInWithMicrosoftProps = {
submitButtonProps?: SubmitButtonProps; submitButtonProps?: SubmitButtonProps;
formClassName?: ComponentProps<'form'>['className']; formProps?: ComponentProps<'form'>;
formProps?: Omit<ComponentProps<'form'>, 'className'>; textProps?: ComponentProps<'p'>;
textClassName?: ComponentProps<'p'>['className']; iconProps?: ComponentProps<'svg'>;
textProps?: Omit<ComponentProps<'p'>, 'className'>;
iconClassName?: ComponentProps<'svg'>['className'];
iconProps?: Omit<ComponentProps<'svg'>, 'className'>;
}; };
export const SignInWithMicrosoft = ({ export const SignInWithMicrosoft = ({
submitButtonProps, submitButtonProps,
formClassName,
formProps, formProps,
textClassName,
textProps, textProps,
iconClassName,
iconProps, iconProps,
} : SignInWithMicrosoftProps) => { } : SignInWithMicrosoftProps) => {
const router = useRouter(); const router = useRouter();
@@ -59,18 +53,18 @@ export const SignInWithMicrosoft = ({
return ( return (
<form <form
onSubmit={handleSignInWithMicrosoft} onSubmit={handleSignInWithMicrosoft}
className={cn('my-4', formClassName)}
{...formProps} {...formProps}
className={cn('my-4', formProps?.className)}
> >
<SubmitButton <SubmitButton
disabled={isLoading || loading} disabled={isLoading || loading}
pendingText='Signing in...' pendingText='Signing in...'
className={cn('w-full', submitButtonProps?.className)}
{...submitButtonProps} {...submitButtonProps}
className={cn('w-full', submitButtonProps?.className)}
> >
<div className='flex items-center gap-2'> <div className='flex items-center gap-2'>
<FaMicrosoft className={cn('size-5', iconClassName)} {...iconProps} /> <FaMicrosoft {...iconProps} className={cn('size-5', iconProps?.className)} />
<p className={cn('text-[1.0rem]', textClassName)} {...textProps}> <p {...textProps} className={cn('text-[1.0rem]', textProps?.className)}>
Sign In with Microsoft Sign In with Microsoft
</p> </p>
</div> </div>

View File

@@ -34,13 +34,13 @@ export const SignOut = ({
<SubmitButton <SubmitButton
disabled={loading} disabled={loading}
onClick={handleSignOut} onClick={handleSignOut}
{...props}
pendingText={pendingText} pendingText={pendingText}
className={cn( className={cn(
'text-[1.0rem] font-semibold \ 'text-[1.0rem] font-semibold \
hover:bg-red-700/60 dark:hover:bg-red-300/80', hover:bg-red-700/60 dark:hover:bg-red-300/80',
className className
)} )}
{...props}
> >
Sign Out Sign Out
</SubmitButton> </SubmitButton>

View File

@@ -11,21 +11,15 @@ import { cn } from '@/lib/utils';
export type SignInWithAppleProps = { export type SignInWithAppleProps = {
submitButtonProps?: SubmitButtonProps; submitButtonProps?: SubmitButtonProps;
formClassName?: ComponentProps<'form'>['className']; formProps?: ComponentProps<'form'>;
formProps?: Omit<ComponentProps<'form'>, 'className'>; textProps?: ComponentProps<'p'>;
textClassName?: ComponentProps<'p'>['className']; iconProps?: ComponentProps<'svg'>;
textProps?: Omit<ComponentProps<'p'>, 'className'>;
iconClassName?: ComponentProps<'svg'>['className'];
iconProps?: Omit<ComponentProps<'svg'>, 'className'>;
}; };
export const SignInWithApple = async ({ export const SignInWithApple = async ({
submitButtonProps, submitButtonProps,
formClassName,
formProps, formProps,
textClassName,
textProps, textProps,
iconClassName,
iconProps, iconProps,
} : SignInWithAppleProps) => { } : SignInWithAppleProps) => {
const supabase = await SupabaseServer(); const supabase = await SupabaseServer();
@@ -45,17 +39,17 @@ export const SignInWithApple = async ({
return ( return (
<form <form
action={handleSignInWithApple} action={handleSignInWithApple}
className={cn('my-4', formClassName)}
{...formProps} {...formProps}
className={cn('my-4', formProps?.className)}
> >
<SubmitButton <SubmitButton
pendingText='Signing in...' pendingText='Signing in...'
className={cn('w-full', submitButtonProps?.className)}
{...submitButtonProps} {...submitButtonProps}
className={cn('w-full', submitButtonProps?.className)}
> >
<div className='flex items-center gap-2'> <div className='flex items-center gap-2'>
<FaApple className={cn('size-5', iconClassName)} {...iconProps} /> <FaApple {...iconProps} className={cn('size-5', iconProps?.className)} />
<p className={cn('text-[1.0rem]', textClassName)} {...textProps}> <p {...textProps} className={cn('text-[1.0rem]', textProps?.className)}>
Sign In with Apple Sign In with Apple
</p> </p>
</div> </div>

View File

@@ -11,21 +11,15 @@ import { cn } from '@/lib/utils';
export type SignInWithMicrosoftProps = { export type SignInWithMicrosoftProps = {
submitButtonProps?: SubmitButtonProps; submitButtonProps?: SubmitButtonProps;
formClassName?: ComponentProps<'form'>['className']; formProps?: ComponentProps<'form'>;
formProps?: Omit<ComponentProps<'form'>, 'className'>; textProps?: ComponentProps<'p'>;
textClassName?: ComponentProps<'p'>['className']; iconProps?: ComponentProps<'svg'>;
textProps?: Omit<ComponentProps<'p'>, 'className'>;
iconClassName?: ComponentProps<'svg'>['className'];
iconProps?: Omit<ComponentProps<'svg'>, 'className'>;
}; };
export const SignInWithMicrosoft = async ({ export const SignInWithMicrosoft = async ({
submitButtonProps, submitButtonProps,
formClassName,
formProps, formProps,
textClassName,
textProps, textProps,
iconClassName,
iconProps, iconProps,
} : SignInWithMicrosoftProps) => { } : SignInWithMicrosoftProps) => {
const supabase = await SupabaseServer(); const supabase = await SupabaseServer();
@@ -45,17 +39,17 @@ export const SignInWithMicrosoft = async ({
return ( return (
<form <form
action={handleSignInWithMicrosoft} action={handleSignInWithMicrosoft}
className={cn('my-4', formClassName)}
{...formProps} {...formProps}
className={cn('my-4', formProps?.className)}
> >
<SubmitButton <SubmitButton
pendingText='Signing in...' pendingText='Signing in...'
className={cn('w-full', submitButtonProps?.className)}
{...submitButtonProps} {...submitButtonProps}
className={cn('w-full', submitButtonProps?.className)}
> >
<div className='flex items-center gap-2'> <div className='flex items-center gap-2'>
<FaMicrosoft className={cn('size-5', iconClassName)} {...iconProps} /> <FaMicrosoft {...iconProps} className={cn('size-5', iconProps?.className)} />
<p className={cn('text-[1.0rem]', textClassName)} {...textProps}> <p {...textProps} className={cn('text-[1.0rem]', textProps?.className)}>
Sign In with Microsoft Sign In with Microsoft
</p> </p>
</div> </div>

View File

@@ -32,33 +32,21 @@ const forgotPasswordFormSchema = z.object({
}); });
type ForgotPasswordCardProps = { type ForgotPasswordCardProps = {
cardClassName?: ComponentProps<typeof Card>['className']; cardProps?: ComponentProps<typeof Card>;
cardProps?: Omit<ComponentProps<typeof Card>, 'className'>; cardTitleProps?: ComponentProps<typeof CardTitle>;
cardTitleClassName?: ComponentProps<typeof CardTitle>['className']; cardDescriptionProps?: ComponentProps<typeof CardDescription>;
cardTitleProps?: Omit<ComponentProps<typeof CardTitle>, 'className'>; signUpLinkProps?: Omit<ComponentProps<typeof Link>, 'href'>;
cardDescriptionClassName?: ComponentProps<typeof CardDescription>['className']; formProps?: Omit<ComponentProps<'form'>, 'onSubmit'>;
cardDescriptionProps?: Omit<ComponentProps<typeof CardDescription>, 'className'>; formLabelProps?: ComponentProps<typeof FormLabel>;
signUpLinkClassName?: ComponentProps<typeof Link>['className'];
signUpLinkProps?: Omit<ComponentProps<typeof Link>, 'className' | 'href'>;
formClassName?: ComponentProps<'form'>['className'];
formProps?: Omit<ComponentProps<'form'>, 'className' | 'onSubmit'>;
formLabelClassName?: ComponentProps<typeof FormLabel>['className'];
formLabelProps?: Omit<ComponentProps<typeof FormLabel>, 'className'>;
buttonProps?: ComponentProps<typeof SubmitButton>; buttonProps?: ComponentProps<typeof SubmitButton>;
}; };
export const ForgotPasswordCard = ({ export const ForgotPasswordCard = ({
cardClassName,
cardProps, cardProps,
cardTitleClassName,
cardTitleProps, cardTitleProps,
cardDescriptionClassName,
cardDescriptionProps, cardDescriptionProps,
signUpLinkClassName,
signUpLinkProps, signUpLinkProps,
formClassName,
formProps, formProps,
formLabelClassName,
formLabelProps, formLabelProps,
buttonProps = { buttonProps = {
pendingText: 'Sending Reset Link...', pendingText: 'Sending Reset Link...',
@@ -101,25 +89,25 @@ export const ForgotPasswordCard = ({
return ( return (
<Card <Card
className={cn('min-w-xs sm:min-w-sm bg-card/50', cardClassName)}
{...cardProps} {...cardProps}
className={cn('min-w-xs sm:min-w-sm bg-card/50', cardProps?.className)}
> >
<CardHeader> <CardHeader>
<CardTitle <CardTitle
className={cn('text-2xl font-medium', cardTitleClassName)}
{...cardTitleProps} {...cardTitleProps}
className={cn('text-2xl font-medium', cardTitleProps?.className)}
> >
Forgot Password Forgot Password
</CardTitle> </CardTitle>
<CardDescription <CardDescription
className={cn('text-sm text-foreground', cardDescriptionClassName)}
{...cardDescriptionProps} {...cardDescriptionProps}
className={cn('text-sm text-foreground', cardDescriptionProps?.className)}
> >
Don&apos;t have an account?{' '} Don&apos;t have an account?{' '}
<Link <Link
className={cn('font-medium underline', signUpLinkClassName)}
href='/sign-up'
{...signUpLinkProps} {...signUpLinkProps}
href='/sign-up'
className={cn('font-medium underline', signUpLinkProps?.className)}
> >
Sign up! Sign up!
</Link> </Link>
@@ -128,9 +116,9 @@ export const ForgotPasswordCard = ({
<CardContent> <CardContent>
<Form {...form}> <Form {...form}>
<form <form
onSubmit={form.handleSubmit(handleForgotPassword)}
className={cn('flex flex-col min-w-64 space-y-6', formClassName)}
{...formProps} {...formProps}
onSubmit={form.handleSubmit(handleForgotPassword)}
className={cn('flex flex-col min-w-64 space-y-6', formProps?.className)}
> >
<FormField <FormField
control={form.control} control={form.control}
@@ -138,8 +126,8 @@ export const ForgotPasswordCard = ({
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel <FormLabel
className={cn('text-xl', formLabelClassName)}
{...formLabelProps} {...formLabelProps}
className={cn('text-xl', formLabelProps?.className)}
> >
Email Email
</FormLabel> </FormLabel>

View File

@@ -64,20 +64,13 @@ const signUpFormSchema = z
}); });
type SignInCardProps = { type SignInCardProps = {
containerClassName?: ComponentProps<typeof Card>['className']; containerProps?: ComponentProps<typeof Card>;
containerProps?: Omit<ComponentProps<typeof Card>, 'className'>; tabsProps?: ComponentProps<typeof Tabs>;
tabsClassName?: ComponentProps<typeof Tabs>['className']; tabsListProps?: ComponentProps<typeof TabsList>;
tabsProps?: Omit<ComponentProps<typeof Tabs>, 'className'>; tabsTriggerProps?: Omit<ComponentProps<typeof TabsTrigger>, 'value'>;
tabsListClassName?: ComponentProps<typeof TabsList>['className']; cardProps?: ComponentProps<typeof Card>;
tabsListProps?: Omit<ComponentProps<typeof TabsList>, 'className'>; formProps?: Omit<ComponentProps<'form'>, 'onSubmit'>;
tabsTriggerClassName?: ComponentProps<typeof TabsTrigger>['className']; formLabelProps?: ComponentProps<typeof FormLabel>;
tabsTriggerProps?: Omit<ComponentProps<typeof TabsTrigger>, 'className' | 'value'>;
cardClassName?: ComponentProps<typeof Card>['className'];
cardProps?: Omit<ComponentProps<typeof Card>, 'className'>;
formClassName?: ComponentProps<'form'>['className'];
formProps?: Omit<ComponentProps<'form'>, 'className' | 'onSubmit'>;
formLabelClassName?: ComponentProps<typeof FormLabel>['className'];
formLabelProps?: Omit<ComponentProps<typeof FormLabel>, 'className'>;
submitButtonProps?: Omit<ComponentProps<typeof SubmitButton>, submitButtonProps?: Omit<ComponentProps<typeof SubmitButton>,
'pendingText' | 'disabled'>; 'pendingText' | 'disabled'>;
signInWithAppleProps?: SignInWithAppleProps; signInWithAppleProps?: SignInWithAppleProps;
@@ -85,19 +78,12 @@ type SignInCardProps = {
}; };
export const SignInCard = ({ export const SignInCard = ({
containerClassName,
containerProps, containerProps,
tabsClassName,
tabsProps = { defaultValue: 'sign-in' }, tabsProps = { defaultValue: 'sign-in' },
tabsListClassName,
tabsListProps, tabsListProps,
tabsTriggerClassName,
tabsTriggerProps, tabsTriggerProps,
cardClassName,
cardProps, cardProps,
formClassName,
formProps, formProps,
formLabelClassName,
formLabelProps, formLabelProps,
submitButtonProps, submitButtonProps,
signInWithAppleProps, signInWithAppleProps,
@@ -165,52 +151,52 @@ export const SignInCard = ({
return ( return (
<Card <Card
className={cn('p-4 bg-card/25 min-h-[720px]', containerClassName)}
{...containerProps} {...containerProps}
className={cn('p-4 bg-card/25 min-h-[720px]', containerProps?.className)}
> >
<Tabs <Tabs
className={cn('items-center', tabsClassName)}
{...tabsProps} {...tabsProps}
className={cn('items-center', tabsProps?.className)}
> >
<TabsList <TabsList
className={cn('py-6', tabsListClassName)}
{...tabsListProps} {...tabsListProps}
className={cn('py-6', tabsListProps?.className)}
> >
<TabsTrigger <TabsTrigger
value='sign-in' value='sign-in'
{...tabsTriggerProps}
className={cn( className={cn(
'p-6 text-2xl font-bold cursor-pointer', 'p-6 text-2xl font-bold cursor-pointer',
tabsTriggerClassName tabsTriggerProps?.className
)} )}
{...tabsTriggerProps}
> >
Sign In Sign In
</TabsTrigger> </TabsTrigger>
<TabsTrigger <TabsTrigger
value='sign-up' value='sign-up'
{...tabsTriggerProps}
className={cn( className={cn(
'p-6 text-2xl font-bold cursor-pointer', 'p-6 text-2xl font-bold cursor-pointer',
tabsTriggerClassName, tabsTriggerProps?.className,
)} )}
{...tabsTriggerProps}
> >
Sign Up Sign Up
</TabsTrigger> </TabsTrigger>
</TabsList> </TabsList>
<TabsContent value='sign-in'> <TabsContent value='sign-in'>
<Card <Card
{...cardProps}
className={cn( className={cn(
'min-w-xs sm:min-w-sm bg-card/50', 'min-w-xs sm:min-w-sm bg-card/50',
cardClassName, cardProps?.className,
)} )}
{...cardProps}
> >
<CardContent> <CardContent>
<Form {...signInForm}> <Form {...signInForm}>
<form <form
onSubmit={signInForm.handleSubmit(handleSignIn)} onSubmit={signInForm.handleSubmit(handleSignIn)}
className={cn('flex flex-col space-y-6', formClassName)}
{...formProps} {...formProps}
className={cn('flex flex-col space-y-6', formProps?.className)}
> >
<FormField <FormField
control={signInForm.control} control={signInForm.control}
@@ -218,8 +204,9 @@ export const SignInCard = ({
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel <FormLabel
className={cn('text-xl', formLabelClassName)} {...formLabelProps}
{...formLabelProps}> className={cn('text-xl', formLabelProps?.className)}
>
Email Email
</FormLabel> </FormLabel>
<FormControl> <FormControl>
@@ -240,8 +227,8 @@ export const SignInCard = ({
<FormItem> <FormItem>
<div className='flex justify-between'> <div className='flex justify-between'>
<FormLabel <FormLabel
className={cn('text-xl', formLabelClassName)}
{...formLabelProps} {...formLabelProps}
className={cn('text-xl', formLabelProps?.className)}
> >
Password Password
</FormLabel> </FormLabel>
@@ -274,11 +261,11 @@ export const SignInCard = ({
<SubmitButton <SubmitButton
disabled={loading} disabled={loading}
pendingText='Signing In...' pendingText='Signing In...'
{...submitButtonProps}
className={cn( className={cn(
'text-lg font-semibold w-2/3 mx-auto', 'text-lg font-semibold w-2/3 mx-auto',
submitButtonProps?.className submitButtonProps?.className
)} )}
{...submitButtonProps}
> >
Sign In Sign In
</SubmitButton> </SubmitButton>
@@ -290,6 +277,7 @@ export const SignInCard = ({
<Separator className='flex-1 bg-muted-foreground/50 py-0.5' /> <Separator className='flex-1 bg-muted-foreground/50 py-0.5' />
</div> </div>
<SignInWithMicrosoft <SignInWithMicrosoft
{...signInWithMicrosoftProps}
submitButtonProps = {{ submitButtonProps = {{
className: cn( className: cn(
'flex w-5/6 m-auto', 'flex w-5/6 m-auto',
@@ -303,9 +291,9 @@ export const SignInCard = ({
'size-6', 'size-6',
signInWithMicrosoftProps?.iconClassName, signInWithMicrosoftProps?.iconClassName,
)} )}
{...signInWithMicrosoftProps}
/> />
<SignInWithApple <SignInWithApple
{...signInWithAppleProps}
submitButtonProps = {{ submitButtonProps = {{
className: cn( className: cn(
'flex w-5/6 m-auto', 'flex w-5/6 m-auto',
@@ -319,23 +307,22 @@ export const SignInCard = ({
'size-6', 'size-6',
signInWithAppleProps?.iconClassName, signInWithAppleProps?.iconClassName,
)} )}
{...signInWithAppleProps}
/> />
</CardContent> </CardContent>
</Card> </Card>
</TabsContent> </TabsContent>
<TabsContent value='sign-up'> <TabsContent value='sign-up'>
<Card <Card
{...cardProps}
className={cn( className={cn(
'min-w-xs sm:min-w-sm bg-card/50', 'min-w-xs sm:min-w-sm bg-card/50',
cardClassName, cardProps?.className,
)} )}
{...cardProps}
> >
<CardContent> <CardContent>
<Form {...signUpForm}> <Form {...signUpForm}>
<form <form
className={cn('flex flex-col space-y-6', formClassName)} className={cn('flex flex-col space-y-6', formProps?.className)}
onSubmit={signUpForm.handleSubmit(handleSignUp)} onSubmit={signUpForm.handleSubmit(handleSignUp)}
{...formProps} {...formProps}
> >
@@ -345,8 +332,8 @@ export const SignInCard = ({
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel <FormLabel
className={cn('text-xl', formLabelClassName)}
{...formLabelProps} {...formLabelProps}
className={cn('text-xl', formLabelProps?.className)}
> >
Name Name
</FormLabel> </FormLabel>
@@ -366,8 +353,8 @@ export const SignInCard = ({
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel <FormLabel
className={cn('text-xl', formLabelClassName)}
{...formLabelProps} {...formLabelProps}
className={cn('text-xl', formLabelProps?.className)}
> >
Email Email
</FormLabel> </FormLabel>
@@ -387,8 +374,8 @@ export const SignInCard = ({
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel <FormLabel
className={cn('text-xl', formLabelClassName)}
{...formLabelProps} {...formLabelProps}
className={cn('text-xl', formLabelProps?.className)}
> >
Password Password
</FormLabel> </FormLabel>
@@ -408,8 +395,8 @@ export const SignInCard = ({
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel <FormLabel
className={cn('text-xl', formLabelClassName)}
{...formLabelProps} {...formLabelProps}
className={cn('text-xl', formLabelProps?.className)}
> >
Confirm Passsword Confirm Passsword
</FormLabel> </FormLabel>
@@ -435,11 +422,11 @@ export const SignInCard = ({
<SubmitButton <SubmitButton
disabled={loading} disabled={loading}
pendingText='Signing Up...' pendingText='Signing Up...'
{...submitButtonProps}
className={cn( className={cn(
'text-lg font-semibold w-2/3 mx-auto', 'text-lg font-semibold w-2/3 mx-auto',
submitButtonProps?.className submitButtonProps?.className
)} )}
{...submitButtonProps}
> >
Sign Up Sign Up
</SubmitButton> </SubmitButton>
@@ -451,6 +438,7 @@ export const SignInCard = ({
<Separator className='flex-1 bg-accent py-0.5' /> <Separator className='flex-1 bg-accent py-0.5' />
</div> </div>
<SignInWithMicrosoft <SignInWithMicrosoft
{...signInWithMicrosoftProps}
submitButtonProps = {{ submitButtonProps = {{
className: cn( className: cn(
'flex w-5/6 m-auto', 'flex w-5/6 m-auto',
@@ -464,9 +452,9 @@ export const SignInCard = ({
'size-6', 'size-6',
signInWithMicrosoftProps?.iconClassName, signInWithMicrosoftProps?.iconClassName,
)} )}
{...signInWithMicrosoftProps}
/> />
<SignInWithApple <SignInWithApple
{...signInWithAppleProps}
submitButtonProps = {{ submitButtonProps = {{
className: cn( className: cn(
'flex w-5/6 m-auto', 'flex w-5/6 m-auto',
@@ -480,7 +468,6 @@ export const SignInCard = ({
'size-6', 'size-6',
signInWithAppleProps?.iconClassName, signInWithAppleProps?.iconClassName,
)} )}
{...signInWithAppleProps}
/> />
</CardContent> </CardContent>
</Card> </Card>

View File

@@ -0,0 +1,72 @@
'use client';
import { useFileUpload } from '@/lib/hooks';
import { useAuth } from '@/lib/hooks/context';
import { useSupabaseClient } from '@/utils/supabase';
import {
BasedAvatar,
Card,
CardContent,
} from '@/components/ui';
import { Loader2, Pencil, Upload } from 'lucide-react';
import type { ComponentProps, ChangeEvent } from 'react';
import { toast } from 'sonner';
type AvatarUploadProps = {
onAvatarUploaded: (path: string) => Promise<void>;
};
export const AvatarUpload = ({
onAvatarUploaded,
}: AvatarUploadProps) => {
const { profile, isAuthenticated } = useAuth();
const { isUploading, fileInputRef, uploadAvatarMutation } = useFileUpload();
const client = useSupabaseClient();
const handleAvatarClick = () => {
if (!isAuthenticated) {
toast.error('You must be logged in to upload an avatar!');
return;
}
fileInputRef.current?.click();
};
const handleFileChange = async (e: ChangeEvent<HTMLInputElement>) => {
try {
const file = e.target.files?.[0];
if (!file) throw new Error('No file selected!');
if (!client) throw new Error('Supabase client not found!');
if (!isAuthenticated) throw new Error('User is not authenticated!');
if (!file.type.startsWith('image/')) throw new Error('File is not an image!');
if (file.size > 8 * 1024 * 1024) throw new Error('File is too large!');
const fileExt = file.name.split('.').pop();
const avatarPath = profile?.avatar_url ?? profile?.id;
const avatarUrl = await uploadAvatarMutation.mutateAsync({
client,
file,
bucket: 'avatars',
resize: {
maxWidth: 500,
maxHeight: 500,
quality: 0.8,
},
replace: avatarPath,
});
if (avatarUrl) await onAvatarUploaded(avatarUrl);
} catch (error) {
toast.error(`Error: ${error as string}`);
}
};
return (
<Card>
<CardContent>
<div>
</div>
</CardContent>
</Card>
);
};

View File

@@ -8,45 +8,47 @@ type Message =
type StatusMessageProps = { type StatusMessageProps = {
message: Message; message: Message;
containerClassName?: ComponentProps<'div'>['className']; containerProps?: ComponentProps<'div'>;
containerProps?: Omit<ComponentProps<'div'>, 'className'>; textProps?: ComponentProps<'div'>;
textClassName?: ComponentProps<'div'>['className'];
textProps?: Omit<ComponentProps<'div'>, 'className'>;
}; };
export const StatusMessage = ({ export const StatusMessage = ({
message, message,
containerClassName,
containerProps, containerProps,
textClassName,
textProps, textProps,
}: StatusMessageProps) => { }: StatusMessageProps) => {
return ( return (
<div <div
{...containerProps}
className={cn( className={cn(
'flex flex-col gap-2 w-full\ 'flex flex-col gap-2 w-full\
text-sm bg-accent rounded-md p-2 px-4', text-sm bg-accent rounded-md p-2 px-4',
containerClassName, containerProps?.className,
)} )}
{...containerProps}
> >
{'success' in message && ( {'success' in message && (
<div className={cn( <div
'dark:text-green-500 text-green-700',
textClassName
)}
{...textProps} {...textProps}
className={cn(
'dark:text-green-500 text-green-700',
textProps?.className
)}
> >
{message.success} {message.success}
</div> </div>
)} )}
{'error' in message && ( {'error' in message && (
<div className={cn('text-destructive', textClassName)}> <div
{...textProps}
className={cn('text-destructive', textProps?.className)}
>
{message.error} {message.error}
</div> </div>
)} )}
{'message' in message && ( {'message' in message && (
<div className={textClassName}> <div
{...textProps}
>
{message.message} {message.message}
</div> </div>
)} )}

View File

@@ -10,39 +10,35 @@ export type SubmitButtonProps = Omit<
'type' | 'aria-disabled' 'type' | 'aria-disabled'
> & { > & {
pendingText?: string; pendingText?: string;
pendingTextClassName?: ComponentProps<'p'>['className']; pendingTextProps?: ComponentProps<'p'>;
pendingTextProps?: Omit<ComponentProps<'p'>, 'className'>; loaderProps?: ComponentProps<typeof Loader2>;
loaderClassName?: ComponentProps<typeof Loader2>['className'];
loaderProps?: Omit<ComponentProps<typeof Loader2>, 'className'>;
}; };
export const SubmitButton = ({ export const SubmitButton = ({
children, children,
className, className,
pendingText = 'Submitting...', pendingText = 'Submitting...',
pendingTextClassName,
pendingTextProps, pendingTextProps,
loaderClassName,
loaderProps, loaderProps,
...props ...props
}: SubmitButtonProps) => { }: SubmitButtonProps) => {
const { pending } = useFormStatus(); const { pending } = useFormStatus();
return ( return (
<Button <Button
className={cn('cursor-pointer', className)}
type='submit' type='submit'
aria-disabled={pending} aria-disabled={pending}
{...props} {...props}
className={cn('cursor-pointer', className)}
> >
{pending || props.disabled ? ( {pending || props.disabled ? (
<> <>
<Loader2 <Loader2
className={cn('mr-2 h-4 w-4 animate-spin', loaderClassName)}
{...loaderProps} {...loaderProps}
className={cn('mr-2 h-4 w-4 animate-spin', loaderProps?.className)}
/> />
<p <p
className={cn('text-sm font-medium', pendingTextClassName)}
{...pendingTextProps} {...pendingTextProps}
className={cn('text-sm font-medium', pendingTextProps?.className)}
> >
{pendingText} {pendingText}
</p> </p>

View File

@@ -20,10 +20,8 @@ const queryCacheOnError = (error: unknown, query: any) => {
const errorMessage = error instanceof Error ? error.message : error as string; const errorMessage = error instanceof Error ? error.message : error as string;
switch (query.meta?.errCode) { switch (query.meta?.errCode) {
case QueryErrorCodes.FETCH_USER_FAILED: case QueryErrorCodes.FETCH_USER_FAILED:
toast.error('Failed to fetch user!');
break; break;
case QueryErrorCodes.FETCH_PROFILE_FAILED: case QueryErrorCodes.FETCH_PROFILE_FAILED:
toast.error('Failed to fetch profile!');
break; break;
case QueryErrorCodes.FETCH_AVATAR_FAILED: case QueryErrorCodes.FETCH_AVATAR_FAILED:
console.warn('Failed to fetch avatar. User may not have one!') console.warn('Failed to fetch avatar. User may not have one!')

View File

@@ -1,9 +1,10 @@
'use client'; 'use client';
import { useState, useRef } from 'react'; import { useState, useRef } from 'react';
import { uploadFile, resizeImage } from '@/lib/queries'; import { useMutation, useQueryClient } from '@tanstack/react-query';
import { getSignedUrl, resizeImage, uploadFile } from '@/lib/queries';
import { useAuth, QueryErrorCodes } from '@/lib/hooks/context';
import type { SupabaseClient, Result, User, Profile } from '@/utils/supabase';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { useAuth } from '@/lib/hooks/context';
import { type SupabaseClient } from '@/utils/supabase';
type UploadToStorageProps = { type UploadToStorageProps = {
client: SupabaseClient; client: SupabaseClient;
@@ -21,6 +22,7 @@ const useFileUpload = () => {
const [isUploading, setIsUploading] = useState(false); const [isUploading, setIsUploading] = useState(false);
const fileInputRef = useRef<HTMLInputElement | null>(null); const fileInputRef = useRef<HTMLInputElement | null>(null);
const { profile, isAuthenticated } = useAuth(); const { profile, isAuthenticated } = useAuth();
const queryClient = useQueryClient();
const uploadToStorage = async ({ const uploadToStorage = async ({
client, client,
@@ -31,51 +33,85 @@ const useFileUpload = () => {
}: UploadToStorageProps) => { }: UploadToStorageProps) => {
try { try {
if (!isAuthenticated) if (!isAuthenticated)
throw new Error('User is not authenticated!'); throw new Error('Error: User is not authenticated!');
setIsUploading(true); setIsUploading(true);
let fileToUpload = file; let fileToUpload = file;
if (resize && file.type.startsWith('image/')) if (resize && file.type.startsWith('image/'))
fileToUpload = await resizeImage({file, options: resize}); fileToUpload = await resizeImage({file, options: resize});
if (replace) { const path = replace || `${Date.now()}-${profile?.id}.${file.name.split('.').pop()}`;
const { data, error} = await uploadFile({ const { data, error} = await uploadFile({
client, client,
bucket, bucket,
path: replace, path,
file: fileToUpload, file: fileToUpload,
options: { options: {
contentType: file.type, contentType: file.type,
upsert: true, ...(replace && {upsert: true})
}, },
}); });
if (error) throw error; if (error) throw new Error(`Error uploading file: ${error.message}`);
return data const { data: urlData, error: urlError } = await getSignedUrl({
} else {
const fileExt = file.name.split('.').pop();
const fileName = `${Date.now()}-${profile?.id}.${fileExt}`;
const { data, error } = await uploadFile({
client, client,
bucket, bucket,
path: fileName, path: data.path,
file: fileToUpload,
options: {
contentType: file.type,
},
}); });
if (error) throw error; if (urlError) throw new Error(`Error getting signed URL: ${urlError.message}`);
return data; return {urlData, error: null};
}
} catch (error) { } catch (error) {
toast.error(`Error uploading file: ${error as string}`); return { data: null, error };
return error;
} finally { } finally {
setIsUploading(false); setIsUploading(false);
if (fileInputRef.current) fileInputRef.current.value = ''; if (fileInputRef.current) fileInputRef.current.value = '';
} }
}; };
const uploadMutation = useMutation({
mutationFn: uploadToStorage,
onSuccess: (result) => {
if (result.error) {
toast.error(`Upload failed: ${result.error as string}`)
} else {
toast.success(`File uploaded successfully!`);
}
},
onError: (error) => {
toast.error(`Upload failed: ${error instanceof Error ? error.message : error}`);
},
meta: { errCode: QueryErrorCodes.UPLOAD_PHOTO_FAILED },
});
const uploadAvatarMutation = useMutation({
mutationFn: async (props: UploadToStorageProps) => {
const { data, error } = await uploadToStorage(props);
if (error) throw new Error(`Error uploading avatar: ${error as string}`);
return data;
},
onSuccess: (avatarUrl) => {
queryClient.invalidateQueries({ queryKey: ['auth'] });
queryClient.setQueryData(['auth, user'], (oldUser: User) => oldUser);
if (profile?.id) {
queryClient.setQueryData(['profiles', profile.id], (oldProfile: Profile) => ({
...oldProfile,
avatar_url: avatarUrl,
updated_at: new Date().toISOString(),
}));
}
toast.success('Avatar uploaded sucessfully!');
},
onError: (error) => {
toast.error(`Avatar upload failed: ${error instanceof Error ? error.message : error}`);
},
meta: { errCode: QueryErrorCodes.UPLOAD_PHOTO_FAILED },
})
return { return {
isUploading, isUploading: isUploading || uploadMutation.isPending || uploadAvatarMutation.isPending,
fileInputRef, fileInputRef,
uploadToStorage, uploadToStorage,
uploadMutation,
uploadAvatarMutation,
}; };
}; };

View File

@@ -5,6 +5,16 @@ export type SupabaseClient = SBClient<Database>;
export type { User } from '@supabase/supabase-js'; export type { User } from '@supabase/supabase-js';
export type Result<T> = {
data: T | null;
error: { message: string } | null;
};
export type AsyncReturnType<T extends (...args: any) => Promise<any>> =
T extends (...args: any) => Promise<infer R> ? R : never;
export type ExtractResultData<T> = T extends Result<infer U> ? U : never;
// Table row types // Table row types
export type Profile = Database['public']['Tables']['profiles']['Row']; export type Profile = Database['public']['Tables']['profiles']['Row'];
export type Status = Database['public']['Tables']['statuses']['Row']; export type Status = Database['public']['Tables']['statuses']['Row'];