diff --git a/bun.lockb b/bun.lockb index ab5f3b7..331110f 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/package.json b/package.json index 74d8edd..be95821 100644 --- a/package.json +++ b/package.json @@ -51,7 +51,7 @@ "@sentry/nextjs": "^9.34.0", "@supabase-cache-helpers/postgrest-react-query": "^1.13.4", "@supabase/ssr": "^0.6.1", - "@supabase/supabase-js": "^2.50.2", + "@supabase/supabase-js": "^2.50.3", "@t3-oss/env-nextjs": "^0.12.0", "@tanstack/react-query": "^5.81.5", "@tanstack/react-table": "^8.21.3", @@ -75,10 +75,10 @@ "react-icons": "^5.5.0", "react-resizable-panels": "^3.0.3", "recharts": "^3.0.2", - "sonner": "^2.0.5", + "sonner": "^2.0.6", "tailwind-merge": "^3.3.1", "vaul": "^1.1.2", - "zod": "^3.25.67" + "zod": "^3.25.71" }, "devDependencies": { "@eslint/eslintrc": "^3.3.1", @@ -98,7 +98,7 @@ "prettier": "^3.6.2", "prettier-plugin-tailwindcss": "^0.6.13", "tailwindcss": "^4.1.11", - "tw-animate-css": "^1.3.4", + "tw-animate-css": "^1.3.5", "typescript": "^5.8.3", "typescript-eslint": "^8.35.1" }, diff --git a/src/app/page.tsx b/src/app/page.tsx index 6ec30b1..a7dc76a 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,4 +1,5 @@ import { SignInCard } from '@/components/default/auth/cards/client/sign-in'; +import { ForgotPasswordCard } from '@/components/default/auth/cards/client/forgot-password'; import { ThemeToggle } from '@/lib/hooks/context'; export default function HomePage() { @@ -9,6 +10,7 @@ export default function HomePage() { Create T3 App + diff --git a/src/components/default/auth/buttons/client/sign-out.tsx b/src/components/default/auth/buttons/client/sign-out.tsx new file mode 100644 index 0000000..2ee273f --- /dev/null +++ b/src/components/default/auth/buttons/client/sign-out.tsx @@ -0,0 +1,48 @@ +'use client'; +import { SubmitButton, type SubmitButtonProps } from '@/components/default/forms'; +import { useRouter } from 'next/navigation'; +import { useAuth } from '@/lib/hooks/context'; +import { signOut } from '@/lib/queries'; +import { useSupabaseClient } from '@/utils/supabase'; +import { cn } from '@/lib/utils'; + +type SignOutProps = Omit + +export const SignOut = ({ + className, + pendingText = 'Signing out...', + ...props +}: SignOutProps) => { + + const supabase = useSupabaseClient(); + const { loading, refreshUser } = useAuth(); + const router = useRouter(); + + const handleSignOut = async () => { + try { + if (!supabase) throw new Error('Supabase client not found'); + const result = await signOut(supabase); + if (result.error) throw new Error(result.error.message); + await refreshUser(); + router.push('/sign-in'); + } catch (error) { + console.error(error); + } + }; + + return ( + + Sign Out + + ); +}; diff --git a/src/components/default/auth/buttons/server/sign-out.tsx b/src/components/default/auth/buttons/server/sign-out.tsx new file mode 100644 index 0000000..e69de29 diff --git a/src/components/default/auth/cards/client/forgot-password.tsx b/src/components/default/auth/cards/client/forgot-password.tsx index e69de29..5775411 100644 --- a/src/components/default/auth/cards/client/forgot-password.tsx +++ b/src/components/default/auth/cards/client/forgot-password.tsx @@ -0,0 +1,177 @@ +'use client'; +import { z } from 'zod'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { useForm } from 'react-hook-form'; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, + Input, +} from '@/components/ui'; +import Link from 'next/link'; +import { forgotPassword } from '@/lib/queries'; +import { useAuth } from '@/lib/hooks/context'; +import { useEffect, useState, type ComponentProps } from 'react'; +import { useRouter } from 'next/navigation'; +import { useSupabaseClient } from '@/utils/supabase'; +import { StatusMessage, SubmitButton } from '@/components/default/forms'; +import { cn } from '@/lib/utils'; + +const forgotPasswordFormSchema = z.object({ + email: z.string().email({ + message: 'Please enter a valid email address.' + }), +}); + +type ForgotPasswordCardProps = { + cardClassName?: ComponentProps['className']; + cardProps?: Omit, 'className'>; + cardTitleClassName?: ComponentProps['className']; + cardTitleProps?: Omit, 'className'>; + cardDescriptionClassName?: ComponentProps['className']; + cardDescriptionProps?: Omit, 'className'>; + signUpLinkClassName?: ComponentProps['className']; + signUpLinkProps?: Omit, 'className' | 'href'>; + formClassName?: ComponentProps<'form'>['className']; + formProps?: Omit, 'className' | 'onSubmit'>; + formLabelClassName?: ComponentProps['className']; + formLabelProps?: Omit, 'className'>; + buttonProps?: ComponentProps; +}; + +export const ForgotPasswordCard = ({ + cardClassName, + cardProps, + cardTitleClassName, + cardTitleProps, + cardDescriptionClassName, + cardDescriptionProps, + signUpLinkClassName, + signUpLinkProps, + formClassName, + formProps, + formLabelClassName, + formLabelProps, + buttonProps = { + pendingText: 'Sending Reset Link...', + }, +}: ForgotPasswordCardProps) => { + const router = useRouter(); + const { isAuthenticated, loading, refreshUser } = useAuth(); + const [statusMessage, setStatusMessage] = useState(''); + const supabase = useSupabaseClient(); + + const form = useForm>({ + resolver: zodResolver(forgotPasswordFormSchema), + defaultValues: { + email: '', + }, + }); + + useEffect(() => { + if (isAuthenticated) router.push('/') + }, [isAuthenticated, router]); + + const handleForgotPassword = async (values: z.infer) => { + try { + setStatusMessage(''); + const formData = new FormData(); + formData.append('email', values.email); + if (!supabase) throw new Error('Supabase client not found'); + const result = await forgotPassword(supabase, formData); + if (result.error) throw new Error(result.error.message); + await refreshUser(); + setStatusMessage('Check your email for a link to reset your password.'); + form.reset(); + router.push(''); + } catch (error) { + setStatusMessage( + `Error: ${error instanceof Error ? error.message : 'Could not sign in!'}`, + ); + } + }; + + return ( + + + + Forgot Password + + + Don't have an account?{' '} + + Sign up! + + + + +
+ + ( + + + Email + + + + + + + )} + /> + + Reset Password + + {statusMessage && + (statusMessage.includes('Error') || + statusMessage.includes('error') || + statusMessage.includes('failed') || + statusMessage.includes('invalid') ? ( + + ) : ( + + ))} + + +
+
+ ); +}; diff --git a/src/components/default/auth/cards/client/profile.tsx b/src/components/default/auth/cards/client/profile.tsx index e69de29..c9495b3 100644 --- a/src/components/default/auth/cards/client/profile.tsx +++ b/src/components/default/auth/cards/client/profile.tsx @@ -0,0 +1,4 @@ +'use client'; +import { useAuth } from '@/lib/hooks/context'; +import { useEffect } from 'react'; +import { useRouter } from 'next/navigation'; diff --git a/src/components/default/auth/cards/client/sign-in.tsx b/src/components/default/auth/cards/client/sign-in.tsx index 9569c39..57e0208 100755 --- a/src/components/default/auth/cards/client/sign-in.tsx +++ b/src/components/default/auth/cards/client/sign-in.tsx @@ -1,5 +1,4 @@ 'use client'; - import { z } from 'zod'; import { zodResolver } from '@hookform/resolvers/zod'; import { useForm } from 'react-hook-form'; diff --git a/src/components/default/auth/forms/profile/avatar-upload.tsx b/src/components/default/auth/forms/profile/avatar-upload.tsx new file mode 100644 index 0000000..e69de29 diff --git a/src/components/default/auth/forms/profile/profile-form.tsx b/src/components/default/auth/forms/profile/profile-form.tsx new file mode 100644 index 0000000..e69de29 diff --git a/src/components/default/auth/forms/profile/reset-password-form.tsx b/src/components/default/auth/forms/profile/reset-password-form.tsx new file mode 100644 index 0000000..e69de29 diff --git a/src/components/default/forms/submit-button.tsx b/src/components/default/forms/submit-button.tsx index 94e3d90..8cdd63f 100644 --- a/src/components/default/forms/submit-button.tsx +++ b/src/components/default/forms/submit-button.tsx @@ -7,9 +7,8 @@ import { cn } from '@/lib/utils'; export type SubmitButtonProps = Omit< ComponentProps, - 'type' | 'aria-disabled' | 'className', + 'type' | 'aria-disabled' > & { - className?: ComponentProps['className']; pendingText?: string; pendingTextClassName?: ComponentProps<'p'>['className']; pendingTextProps?: Omit, 'className'>;