Cleaned up components and their props
This commit is contained in:
12
package.json
12
package.json
@@ -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",
|
||||||
|
@@ -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>
|
||||||
|
@@ -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>
|
||||||
|
@@ -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>
|
||||||
|
@@ -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>
|
||||||
|
@@ -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>
|
||||||
|
@@ -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't have an account?{' '}
|
Don'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>
|
||||||
|
@@ -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>
|
||||||
|
@@ -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>
|
||||||
|
);
|
||||||
|
|
||||||
|
};
|
@@ -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>
|
||||||
)}
|
)}
|
||||||
|
@@ -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>
|
||||||
|
@@ -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!')
|
||||||
|
@@ -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,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -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'];
|
||||||
|
Reference in New Issue
Block a user