diff --git a/.gitignore b/.gitignore index 5ef6a5207..925a88793 100644 --- a/.gitignore +++ b/.gitignore @@ -32,6 +32,9 @@ yarn-error.log* # env files (can opt-in for committing if needed) .env* +!.env.example + + # vercel .vercel diff --git a/.husky/commit-msg b/.husky/commit-msg index f2000d95a..056d42163 100755 --- a/.husky/commit-msg +++ b/.husky/commit-msg @@ -2,14 +2,23 @@ # Conventional commit message format # Format: (): -# Types: feat, fix, docs, style, refactor, test, chore, perf, ci, build, revert +# Types: feat, fix, docs, style, refactor, test, chore, perf, ci, build, revert, merge +# Also allows Git's standard merge commit format: "Merge branch '...' of ... into ..." -commit_regex='^(feat|fix|docs|style|refactor|test|chore|perf|ci|build|revert)(\(.+\))?: .{1,50}' +# Regex for conventional commits +conventional_regex='^(feat|fix|docs|style|refactor|test|chore|perf|ci|build|revert|merge)(\(.+\))?: .{1,50}' + +# Regex for Git merge commits +merge_regex='^Merge branch .* of .* into .*' + +# Combined regex - either conventional or merge format +commit_regex="($conventional_regex|$merge_regex)" if ! grep -qE "$commit_regex" "$1"; then echo "❌ Invalid commit message format." - echo "✅ Please use conventional commit format:" - echo " (): " + echo "✅ Please use either:" + echo " 1. Conventional commit format: (): " + echo " 2. Git's standard merge format: Merge branch '...' of ... into ..." echo "" echo "📝 Examples:" echo " feat: add new wallet connection feature" @@ -17,8 +26,9 @@ if ! grep -qE "$commit_regex" "$1"; then echo " docs: update README with setup instructions" echo " style: format code with prettier" echo " refactor(components): improve wallet button" + echo " Merge branch 'main' of https://github.com/user/repo into feature/branch" echo "" - echo "🔧 Types: feat, fix, docs, style, refactor, test, chore, perf, ci, build, revert" + echo "🔧 Types: feat, fix, docs, style, refactor, test, chore, perf, ci, build, revert, merge" exit 1 fi diff --git a/app/api/auth/register/route.ts b/app/api/auth/register/route.ts index 0e52e35ee..f0ddba04b 100644 --- a/app/api/auth/register/route.ts +++ b/app/api/auth/register/route.ts @@ -3,7 +3,8 @@ import { NextResponse } from 'next/server'; import { z } from 'zod'; const userSchema = z.object({ - name: z.string().min(2, 'Name must be at least 2 characters'), + firstName: z.string().min(2, 'First name must be at least 2 characters'), + lastName: z.string().min(2, 'Last name must be at least 2 characters'), email: z.string().email('Invalid email address'), password: z.string().min(8, 'Password must be at least 8 characters'), }); @@ -11,12 +12,12 @@ const userSchema = z.object({ export async function POST(req: Request) { try { const body = await req.json(); - const { name, email, password } = userSchema.parse(body); + const { firstName, lastName, email, password } = userSchema.parse(body); const registerData = { email, password, - firstName: name, - lastName: name, + firstName, + lastName, username: email.split('@')[0], }; const response = await register(registerData); @@ -25,6 +26,25 @@ export async function POST(req: Request) { if (error instanceof z.ZodError) { return NextResponse.json({ message: error.message }, { status: 400 }); } + + if ( + typeof error === 'object' && + error !== null && + 'message' in error && + 'status' in error + ) { + const serverError = error; + const errorMessage = + serverError?.message || 'An unexpected error occurred'; + const statusCode = + typeof serverError?.status === 'number' ? serverError.status : 400; + + return NextResponse.json( + { message: errorMessage }, + { status: statusCode } + ); + } + const errorMessage = error instanceof Error ? error.message : 'An unexpected error occurred'; return NextResponse.json({ message: errorMessage }, { status: 500 }); diff --git a/app/auth/forgot-password/page.tsx b/app/auth/forgot-password/page.tsx index 7b368cd0d..3658bf71a 100644 --- a/app/auth/forgot-password/page.tsx +++ b/app/auth/forgot-password/page.tsx @@ -2,30 +2,46 @@ import { useState } from 'react'; import Link from 'next/link'; -import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; -import { Label } from '@/components/ui/label'; +import { Mail, ArrowLeft } from 'lucide-react'; +import Image from 'next/image'; import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, -} from '@/components/ui/card'; -import { Alert, AlertDescription } from '@/components/ui/alert'; -import { Loader2, Mail, ArrowLeft } from 'lucide-react'; + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from '@/components/ui/form'; +import { useForm } from 'react-hook-form'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { z } from 'zod'; +import { BoundlessButton } from '@/components/buttons'; + +const forgotPasswordSchema = z.object({ + email: z.string().email({ + message: 'Please enter a valid email address', + }), +}); + +type ForgotPasswordFormData = z.infer; export default function ForgotPasswordPage() { - const [email, setEmail] = useState(''); const [isLoading, setIsLoading] = useState(false); - const [error, setError] = useState(''); - const [success, setSuccess] = useState(''); + const [successMessage, setSuccessMessage] = useState(''); + const [errorMessage, setErrorMessage] = useState(''); + + const form = useForm({ + resolver: zodResolver(forgotPasswordSchema), + defaultValues: { + email: '', + }, + }); - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); + const handleSubmit = async (data: ForgotPasswordFormData) => { setIsLoading(true); - setError(''); - setSuccess(''); + setSuccessMessage(''); + setErrorMessage(''); try { const response = await fetch('/api/auth/forgot-password', { @@ -33,95 +49,106 @@ export default function ForgotPasswordPage() { headers: { 'Content-Type': 'application/json', }, - body: JSON.stringify({ email }), + body: JSON.stringify({ email: data.email }), }); - const data = await response.json(); + const res = await response.json(); if (response.ok) { - setSuccess( + setSuccessMessage( 'Password reset instructions have been sent to your email address.' ); - setEmail(''); + form.reset(); } else { - setError(data.message || 'Failed to send reset email'); + setErrorMessage(res.message || 'Failed to send reset email'); } } catch { - setError('An unexpected error occurred'); + setErrorMessage('An unexpected error occurred'); } finally { setIsLoading(false); } }; return ( -
+
+
+ logo +
-

- Forgot your password? +

+ Reset Password

-

- Enter your email address and we'll send you a link to reset your - password. +

+ Enter the email address you used when you joined, and we'll send you + instructions to reset your password.

- - - Reset Password - - We'll send you an email with instructions to reset your password - - - - {error && ( - - - {typeof error === 'string' ? error : 'An error occurred'} - - - )} + {successMessage && ( +
{successMessage}
+ )} - {success && ( - - {success} - - )} + {errorMessage && ( +
{errorMessage}
+ )} -
-
- -
- - setEmail(e.target.value)} - placeholder='Enter your email' - className='pl-10' - required - /> -
-
+ + + ( + + + Email + + +
+ + +
+
+ +
+ )} + /> - - + + Send Reset Link + + + -
- - - Back to sign in - -
-
-
+
+ + + Back to sign in + +
); diff --git a/app/auth/reset-password/page.tsx b/app/auth/reset-password/page.tsx index 68aa276e9..64837a9eb 100644 --- a/app/auth/reset-password/page.tsx +++ b/app/auth/reset-password/page.tsx @@ -3,62 +3,71 @@ import { useState, useEffect } from 'react'; import { useRouter, useSearchParams } from 'next/navigation'; import Link from 'next/link'; -import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; -import { Label } from '@/components/ui/label'; -import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, -} from '@/components/ui/card'; +import { Card, CardContent } from '@/components/ui/card'; import { Alert, AlertDescription } from '@/components/ui/alert'; -import { Loader2, Lock, Eye, EyeOff } from 'lucide-react'; +import { Lock, Eye, EyeOff } from 'lucide-react'; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from '@/components/ui/form'; +import { useForm } from 'react-hook-form'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { z } from 'zod'; +import { BoundlessButton } from '@/components/buttons'; + +const resetPasswordSchema = z + .object({ + password: z.string().min(8, { + message: 'Password must be at least 8 characters long', + }), + confirmPassword: z.string(), + }) + .refine(data => data.password === data.confirmPassword, { + message: "Passwords don't match", + path: ['confirmPassword'], + }); + +type ResetPasswordFormData = z.infer; export default function ResetPasswordPage() { - const [password, setPassword] = useState(''); - const [confirmPassword, setConfirmPassword] = useState(''); const [showPassword, setShowPassword] = useState(false); const [showConfirmPassword, setShowConfirmPassword] = useState(false); const [isLoading, setIsLoading] = useState(false); - const [error, setError] = useState(''); - const [success, setSuccess] = useState(''); + const [successMessage, setSuccessMessage] = useState(''); + const [errorMessage, setErrorMessage] = useState(''); + const [token, setToken] = useState(''); const router = useRouter(); const searchParams = useSearchParams(); + const form = useForm({ + resolver: zodResolver(resetPasswordSchema), + defaultValues: { + password: '', + confirmPassword: '', + }, + }); + useEffect(() => { const tokenParam = searchParams.get('token'); if (tokenParam) { setToken(tokenParam); } else { - setError('Invalid reset link. Please request a new password reset.'); + setErrorMessage( + 'Invalid reset link. Please request a new password reset.' + ); } }, [searchParams]); - const validateForm = () => { - if (password !== confirmPassword) { - setError('Passwords do not match'); - return false; - } - if (password.length < 8) { - setError('Password must be at least 8 characters long'); - return false; - } - return true; - }; - - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); + const handleSubmit = async (data: ResetPasswordFormData) => { setIsLoading(true); - setError(''); - setSuccess(''); - - if (!validateForm()) { - setIsLoading(false); - return; - } + setSuccessMessage(''); + setErrorMessage(''); try { const response = await fetch('/api/auth/reset-password', { @@ -68,26 +77,25 @@ export default function ResetPasswordPage() { }, body: JSON.stringify({ token, - newPassword: password, + newPassword: data.password, }), }); - const data = await response.json(); + const responseData = await response.json(); if (response.ok) { - setSuccess( + setSuccessMessage( 'Password reset successfully! You can now sign in with your new password.' ); - setPassword(''); - setConfirmPassword(''); + form.reset(); setTimeout(() => { router.push('/auth/signin'); }, 2000); } else { - setError(data.message || 'Failed to reset password'); + setErrorMessage(responseData.message || 'Failed to reset password'); } } catch { - setError('An unexpected error occurred'); + setErrorMessage('An unexpected error occurred'); } finally { setIsLoading(false); } @@ -120,110 +128,120 @@ export default function ResetPasswordPage() { } return ( -
+
-

+

Reset your password

-

- Enter your new password below -

+

Enter your new password below

- - - New Password - - Choose a strong password for your account - - - - {error && ( - - - {typeof error === 'string' ? error : 'An error occurred'} - - - )} + {successMessage && ( +
{successMessage}
+ )} + + {errorMessage && ( +
{errorMessage}
+ )} + +
+ + ( + + + New Password + + +
+ + + +
+
+ +
+ )} + /> + + ( + + + Confirm New Password + + +
+ + + +
+
+ +
+ )} + /> + + + Reset Password + + + - {success && ( - - {success} - - )} - -
-
- -
- - setPassword(e.target.value)} - placeholder='Enter new password' - className='pl-10 pr-10' - required - /> - -
-
- -
- -
- - setConfirmPassword(e.target.value)} - placeholder='Confirm new password' - className='pl-10 pr-10' - required - /> - -
-
- - -
- -
- - Back to sign in - -
-
-
+
+ + Back to sign in + +
); diff --git a/app/auth/signin copy/page.tsx b/app/auth/signin copy/page.tsx new file mode 100644 index 000000000..3b4451a28 --- /dev/null +++ b/app/auth/signin copy/page.tsx @@ -0,0 +1,218 @@ +'use client'; + +import { useState } from 'react'; +import { signIn, getSession } from 'next-auth/react'; +import { useRouter, useSearchParams } from 'next/navigation'; +import Link from 'next/link'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from '@/components/ui/card'; +import { Alert, AlertDescription } from '@/components/ui/alert'; +import { Separator } from '@/components/ui/separator'; +import { Loader2, Mail, Lock, Eye, EyeOff } from 'lucide-react'; +import Cookies from 'js-cookie'; + +export default function SignInPage() { + const [email, setEmail] = useState(''); + const [password, setPassword] = useState(''); + const [showPassword, setShowPassword] = useState(false); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(''); + const router = useRouter(); + const searchParams = useSearchParams(); + const callbackUrl = searchParams.get('callbackUrl') || '/user'; + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setIsLoading(true); + setError(''); + + try { + const result = await signIn('credentials', { + email, + password, + redirect: false, + }); + + if (result?.error) { + if (result.error === 'UNVERIFIED_EMAIL') { + setError( + 'Please verify your email before signing in. Check your inbox for a verification link.' + ); + setTimeout(() => { + router.push( + `/auth/verify-email?email=${encodeURIComponent(email)}` + ); + }, 3000); + } else { + setError('Invalid email or password'); + } + } else { + const session = await getSession(); + if (session) { + if (result?.ok) { + Cookies.set('accessToken', session?.user.accessToken ?? ''); + Cookies.set('refreshToken', session?.user.refreshToken ?? ''); + } + router.push(callbackUrl); + } + } + } catch { + setError('An unexpected error occurred'); + } finally { + setIsLoading(false); + } + }; + + const handleGoogleSignIn = async () => { + setIsLoading(true); + try { + await signIn('google', { callbackUrl: '/user' }); + } catch { + setError('Failed to sign in with Google'); + setIsLoading(false); + } + }; + + return ( +
+
+
+

+ Sign in to your account +

+

+ Or{' '} + + create a new account + +

+
+ + + + Welcome back + + Enter your credentials to access your account + + + + {error && ( + + + {typeof error === 'string' ? error : 'An error occurred'} + + + )} + +
+
+ +
+ + setEmail(e.target.value)} + placeholder='Enter your email' + className='pl-10' + required + /> +
+
+ +
+ +
+ + setPassword(e.target.value)} + placeholder='Enter your password' + className='pl-10 pr-10' + required + /> + +
+
+ + +
+ +
+
+ +
+
+ + Or continue with + +
+
+ + + +
+ + Forgot your password? + +
+
+
+
+
+ ); +} diff --git a/app/auth/signin/page.tsx b/app/auth/signin/page.tsx index 3b4451a28..62d338d8a 100644 --- a/app/auth/signin/page.tsx +++ b/app/auth/signin/page.tsx @@ -1,218 +1,11 @@ 'use client'; - -import { useState } from 'react'; -import { signIn, getSession } from 'next-auth/react'; -import { useRouter, useSearchParams } from 'next/navigation'; -import Link from 'next/link'; -import { Button } from '@/components/ui/button'; -import { Input } from '@/components/ui/input'; -import { Label } from '@/components/ui/label'; -import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, -} from '@/components/ui/card'; -import { Alert, AlertDescription } from '@/components/ui/alert'; -import { Separator } from '@/components/ui/separator'; -import { Loader2, Mail, Lock, Eye, EyeOff } from 'lucide-react'; -import Cookies from 'js-cookie'; +import AuthLayout from '@/components/auth/AuthLayout'; +import LoginForm from '@/components/auth/LoginForm'; export default function SignInPage() { - const [email, setEmail] = useState(''); - const [password, setPassword] = useState(''); - const [showPassword, setShowPassword] = useState(false); - const [isLoading, setIsLoading] = useState(false); - const [error, setError] = useState(''); - const router = useRouter(); - const searchParams = useSearchParams(); - const callbackUrl = searchParams.get('callbackUrl') || '/user'; - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); - setIsLoading(true); - setError(''); - - try { - const result = await signIn('credentials', { - email, - password, - redirect: false, - }); - - if (result?.error) { - if (result.error === 'UNVERIFIED_EMAIL') { - setError( - 'Please verify your email before signing in. Check your inbox for a verification link.' - ); - setTimeout(() => { - router.push( - `/auth/verify-email?email=${encodeURIComponent(email)}` - ); - }, 3000); - } else { - setError('Invalid email or password'); - } - } else { - const session = await getSession(); - if (session) { - if (result?.ok) { - Cookies.set('accessToken', session?.user.accessToken ?? ''); - Cookies.set('refreshToken', session?.user.refreshToken ?? ''); - } - router.push(callbackUrl); - } - } - } catch { - setError('An unexpected error occurred'); - } finally { - setIsLoading(false); - } - }; - - const handleGoogleSignIn = async () => { - setIsLoading(true); - try { - await signIn('google', { callbackUrl: '/user' }); - } catch { - setError('Failed to sign in with Google'); - setIsLoading(false); - } - }; - return ( -
-
-
-

- Sign in to your account -

-

- Or{' '} - - create a new account - -

-
- - - - Welcome back - - Enter your credentials to access your account - - - - {error && ( - - - {typeof error === 'string' ? error : 'An error occurred'} - - - )} - -
-
- -
- - setEmail(e.target.value)} - placeholder='Enter your email' - className='pl-10' - required - /> -
-
- -
- -
- - setPassword(e.target.value)} - placeholder='Enter your password' - className='pl-10 pr-10' - required - /> - -
-
- - -
- -
-
- -
-
- - Or continue with - -
-
- - - -
- - Forgot your password? - -
-
-
-
-
+ + + ); } diff --git a/app/auth/signup copy/page.tsx b/app/auth/signup copy/page.tsx new file mode 100644 index 000000000..b2bbdd00e --- /dev/null +++ b/app/auth/signup copy/page.tsx @@ -0,0 +1,308 @@ +'use client'; + +import { useState } from 'react'; +import { useRouter } from 'next/navigation'; +import Link from 'next/link'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from '@/components/ui/card'; +import { Alert, AlertDescription } from '@/components/ui/alert'; +import { Separator } from '@/components/ui/separator'; +import { Loader2, Mail, Lock, User, Eye, EyeOff } from 'lucide-react'; + +export default function SignUpPage() { + const [formData, setFormData] = useState({ + firstName: '', + lastName: '', + email: '', + password: '', + confirmPassword: '', + }); + const [showPassword, setShowPassword] = useState(false); + const [showConfirmPassword, setShowConfirmPassword] = useState(false); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(''); + const [success, setSuccess] = useState(''); + const router = useRouter(); + + const handleInputChange = (e: React.ChangeEvent) => { + const { name, value } = e.target; + setFormData(prev => ({ ...prev, [name]: value })); + }; + + const validateForm = () => { + if (formData.password !== formData.confirmPassword) { + setError('Passwords do not match'); + return false; + } + if (formData.password.length < 8) { + setError('Password must be at least 8 characters long'); + return false; + } + if (!formData.firstName.trim() || !formData.lastName.trim()) { + setError('First name and last name are required'); + return false; + } + return true; + }; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setIsLoading(true); + setError(''); + setSuccess(''); + + if (!validateForm()) { + setIsLoading(false); + return; + } + + try { + const response = await fetch('/api/auth/register', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + name: `${formData.firstName} ${formData.lastName}`, + email: formData.email, + password: formData.password, + }), + }); + + const data = await response.json(); + + if (response.ok) { + setSuccess( + 'Account created successfully! Please check your email to verify your account.' + ); + setFormData({ + firstName: '', + lastName: '', + email: '', + password: '', + confirmPassword: '', + }); + setTimeout(() => { + router.push( + `/auth/verify-email?email=${encodeURIComponent(formData.email)}` + ); + }, 2000); + } else { + setError(data.message || 'Failed to create account'); + } + } catch { + setError('An unexpected error occurred'); + } finally { + setIsLoading(false); + } + }; + + return ( +
+
+
+

+ Create your account +

+

+ Or{' '} + + sign in to your existing account + +

+
+ + + + Join us + + Create your account to get started + + + + {error && ( + + + {typeof error === 'string' ? error : 'An error occurred'} + + + )} + + {success && ( + + {success} + + )} + +
+
+
+ +
+ + +
+
+ +
+ +
+ + +
+
+
+ +
+ +
+ + +
+
+ +
+ +
+ + + +
+
+ +
+ +
+ + + +
+
+ + +
+ +
+
+ +
+
+ + Or continue with + +
+
+ + +
+
+
+
+ ); +} diff --git a/app/auth/signup/page.tsx b/app/auth/signup/page.tsx index b2bbdd00e..eee134030 100644 --- a/app/auth/signup/page.tsx +++ b/app/auth/signup/page.tsx @@ -1,308 +1,14 @@ 'use client'; +import AuthLayout from '@/components/auth/AuthLayout'; +import SignupForm from '@/components/auth/SignupForm'; +import React from 'react'; -import { useState } from 'react'; -import { useRouter } from 'next/navigation'; -import Link from 'next/link'; -import { Button } from '@/components/ui/button'; -import { Input } from '@/components/ui/input'; -import { Label } from '@/components/ui/label'; -import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, -} from '@/components/ui/card'; -import { Alert, AlertDescription } from '@/components/ui/alert'; -import { Separator } from '@/components/ui/separator'; -import { Loader2, Mail, Lock, User, Eye, EyeOff } from 'lucide-react'; - -export default function SignUpPage() { - const [formData, setFormData] = useState({ - firstName: '', - lastName: '', - email: '', - password: '', - confirmPassword: '', - }); - const [showPassword, setShowPassword] = useState(false); - const [showConfirmPassword, setShowConfirmPassword] = useState(false); - const [isLoading, setIsLoading] = useState(false); - const [error, setError] = useState(''); - const [success, setSuccess] = useState(''); - const router = useRouter(); - - const handleInputChange = (e: React.ChangeEvent) => { - const { name, value } = e.target; - setFormData(prev => ({ ...prev, [name]: value })); - }; - - const validateForm = () => { - if (formData.password !== formData.confirmPassword) { - setError('Passwords do not match'); - return false; - } - if (formData.password.length < 8) { - setError('Password must be at least 8 characters long'); - return false; - } - if (!formData.firstName.trim() || !formData.lastName.trim()) { - setError('First name and last name are required'); - return false; - } - return true; - }; - - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); - setIsLoading(true); - setError(''); - setSuccess(''); - - if (!validateForm()) { - setIsLoading(false); - return; - } - - try { - const response = await fetch('/api/auth/register', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - name: `${formData.firstName} ${formData.lastName}`, - email: formData.email, - password: formData.password, - }), - }); - - const data = await response.json(); - - if (response.ok) { - setSuccess( - 'Account created successfully! Please check your email to verify your account.' - ); - setFormData({ - firstName: '', - lastName: '', - email: '', - password: '', - confirmPassword: '', - }); - setTimeout(() => { - router.push( - `/auth/verify-email?email=${encodeURIComponent(formData.email)}` - ); - }, 2000); - } else { - setError(data.message || 'Failed to create account'); - } - } catch { - setError('An unexpected error occurred'); - } finally { - setIsLoading(false); - } - }; - +const page = () => { return ( -
-
-
-

- Create your account -

-

- Or{' '} - - sign in to your existing account - -

-
- - - - Join us - - Create your account to get started - - - - {error && ( - - - {typeof error === 'string' ? error : 'An error occurred'} - - - )} - - {success && ( - - {success} - - )} - -
-
-
- -
- - -
-
- -
- -
- - -
-
-
- -
- -
- - -
-
- -
- -
- - - -
-
- -
- -
- - - -
-
- - -
- -
-
- -
-
- - Or continue with - -
-
- - -
-
-
-
+ + + ); -} +}; + +export default page; diff --git a/app/page.tsx b/app/page.tsx index c1d7498a2..6e2d67c69 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,22 +1,16 @@ 'use client'; -import { PriceDisplay } from '@/components/PriceDisplay'; -import EmptyState from '@/components/EmptyState'; import { BoundlessButton } from '@/components/buttons'; -import { Coins, History, Plus } from 'lucide-react'; -import Card from '@/components/card'; -import RecentProjects from '@/components/overview/RecentProjects'; -import RecentContributions from '@/components/overview/ReecntContributions'; -import GrantHistory from '@/components/overview/GrantHistory'; import { AuthNav } from '@/components/auth/AuthNav'; +// eslint-disable-next-line @typescript-eslint/no-unused-vars import CommentModal from '@/components/comment/modal'; import { motion } from 'framer-motion'; import { - fadeInUp, staggerContainer, slideInFromLeft, slideInFromRight, } from '@/lib/motion'; import PageTransition from '@/components/PageTransition'; +import Link from 'next/link'; export default function Home() { return ( @@ -35,101 +29,12 @@ export default function Home() { - - - - console.log('Comment submitted:', comment) - } - > - } - iconPosition='right' - > - Add comment - - - } - /> - - - - - - -
- } - /> - - - - - No recent submissions -
- } - /> - - - - 0 - Approved Submissions - - } - /> - - - - } - bottomText={ -
- 6 grants available -
- } - /> -
- - - - - - - - - - - - + + Go to Dashboard + diff --git a/auth.ts b/auth.ts index 6def1e8a3..561def06f 100644 --- a/auth.ts +++ b/auth.ts @@ -74,6 +74,7 @@ declare module 'next-auth' { const getMe = (token?: string) => getMeBase(token); export const { handlers, signIn, signOut, auth } = NextAuth({ + debug: process.env.NODE_ENV === 'development', providers: [ Google({ clientId: process.env.GOOGLE_CLIENT_ID!, @@ -122,8 +123,16 @@ export const { handlers, signIn, signOut, auth } = NextAuth({ ? credentials.password : ''; + console.log('Attempting login with:', { email, password: '***' }); + const response = await login({ email, password }); + console.log('Login response:', { + success: !!response, + hasAccessToken: !!response?.accessToken, + hasRefreshToken: !!response?.refreshToken, + }); + if (response && response.accessToken) { const user = await getMe(response.accessToken); if (user) { @@ -132,8 +141,7 @@ export const { handlers, signIn, signOut, auth } = NextAuth({ } const userInfo = extractUserInfo(user); - // Cookies.set('accessToken', response.accessToken); - // Cookies.set('refreshToken', response.refreshToken || ''); + console.log('User info extracted:', userInfo); return { ...userInfo, @@ -142,11 +150,14 @@ export const { handlers, signIn, signOut, auth } = NextAuth({ }; } } + console.log('Login failed: No valid response or user data'); return null; } catch (err) { + console.error('Login error in NextAuth:', err); if (err instanceof Error && err.message === 'UNVERIFIED_EMAIL') { throw err; } + // Return null for any other error to indicate login failure return null; } }, diff --git a/components/auth/AuthLayout.tsx b/components/auth/AuthLayout.tsx new file mode 100644 index 000000000..8a3cd1e0b --- /dev/null +++ b/components/auth/AuthLayout.tsx @@ -0,0 +1,211 @@ +'use client'; +import Image from 'next/image'; +import React from 'react'; +import { Badge } from '../ui/badge'; +import { useState, useEffect } from 'react'; +import { + Carousel, + CarouselContent, + CarouselItem, + CarouselNext, + CarouselPrevious, + type CarouselApi, +} from '../ui/carousel'; +import Autoplay from 'embla-carousel-autoplay'; + +interface AuthLayoutProps { + children: React.ReactNode; + showCarousel?: boolean; +} + +const AuthLayout = ({ children, showCarousel = true }: AuthLayoutProps) => { + // Slide data + const slides = [ + { + id: 1, + title: 'Threadify', + description: + 'Threadify is a platform that lets creators and brands design, customize, and sell products online without upfront costs or inventory.', + badge: 'TRENDING PROJECTS', + logo: '/wallets/albedo.svg', + }, + { + id: 2, + title: 'Boundless', + description: + 'Boundless is a decentralized funding platform that connects creators with supporters through transparent and secure blockchain technology.', + badge: 'FEATURED PROJECTS', + logo: '/wallets/freighter.svg', + }, + { + id: 3, + title: 'CreatorHub', + description: + 'CreatorHub empowers content creators with tools to monetize their audience and build sustainable income streams.', + badge: 'POPULAR PROJECTS', + logo: '/next.svg', + }, + { + id: 4, + title: 'InnovateLab', + description: + 'InnovateLab provides resources and funding for innovative startups to bring their ideas to life and scale globally.', + badge: 'INNOVATION PROJECTS', + logo: '/auth/logo.svg', + }, + { + id: 5, + title: 'EcoFund', + description: + 'EcoFund supports environmental initiatives and sustainable projects that make a positive impact on our planet.', + badge: 'SUSTAINABLE PROJECTS', + logo: '/auth/logo.svg', + }, + ]; + + const [currentSlide, setCurrentSlide] = useState(0); + const [api, setApi] = useState(); + + useEffect(() => { + if (!api) { + return; + } + + api.on('select', () => { + setCurrentSlide(api.selectedScrollSnap()); + }); + }, [api]); + + return ( +
+
+ {/* Left Panel - Auth Form */} +
+ auth +
+ auth +
{children}
+
+
+ auth +
+
+ + {/* Right Panel - Carousel */} + {showCarousel && ( +
+
+
+ + + {slides.map(slide => ( + +
+ {/* Main card with glassmorphism effects */} +
+ Glassmorphism card +
+ logo +
+
+ + {/* Project information section */} +
+ + {slide.badge} + + +
+

+ {slide.title} +

+

+ {slide.description} +

+
+
+
+
+ ))} +
+
+ +
+ {slides.map((_, dotIndex) => ( +
+ +
+
+
+
+
+ )} +
+
+ ); +}; + +export default AuthLayout; diff --git a/components/auth/CreatePasswordForm.tsx b/components/auth/CreatePasswordForm.tsx new file mode 100644 index 000000000..31c389344 --- /dev/null +++ b/components/auth/CreatePasswordForm.tsx @@ -0,0 +1,197 @@ +'use client'; +import React, { useState } from 'react'; +import { BoundlessButton } from '../buttons'; +import { + Form, + FormControl, + FormField, + FormItem, + FormMessage, +} from '../ui/form'; +import { FormLabel } from '../ui/form'; +import z from 'zod'; +import { useForm } from 'react-hook-form'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { Input } from '../ui/input'; +import { LockIcon, EyeIcon, EyeOffIcon } from 'lucide-react'; +import { toast } from 'sonner'; +import { useRouter } from 'next/navigation'; +import Cookies from 'js-cookie'; + +const formSchema = z + .object({ + password: z.string().min(8, { + message: 'Password must be at least 8 characters', + }), + confirmPassword: z.string().min(8, { + message: 'Password must be at least 8 characters', + }), + }) + .refine(data => data.password === data.confirmPassword, { + message: "Passwords don't match", + path: ['confirmPassword'], + }); + +interface CreatePasswordFormProps { + email: string; + name: string; +} + +const CreatePasswordForm = ({ email, name }: CreatePasswordFormProps) => { + const router = useRouter(); + const [showPassword, setShowPassword] = useState(false); + const [showConfirmPassword, setShowConfirmPassword] = useState(false); + + const form = useForm>({ + resolver: zodResolver(formSchema), + defaultValues: { + password: '', + confirmPassword: '', + }, + }); + + const onSubmit = async (values: z.infer) => { + try { + // Here you would typically create the user account with password + const response = await fetch('/api/auth/create-account', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + email, + name, + password: values.password, + }), + }); + + if (response.ok) { + const data = await response.json(); + + // Set cookies if tokens are returned + if (data.accessToken) { + Cookies.set('accessToken', data.accessToken); + } + if (data.refreshToken) { + Cookies.set('refreshToken', data.refreshToken); + } + + toast.success('Account created successfully!'); + router.push('/user'); + } else { + const error = await response.json(); + toast.error(error.message || 'Failed to create account'); + } + } catch { + toast.error('Failed to create account. Please try again.'); + } + }; + + return ( + <> +
+
+

+ Create password +

+

+ Enter the OTP that was sent to {email} +

+

+ Please keep this code private. +

+
+ +
+ + ( + + + Password + + +
+ + + +
+
+ +
+ )} + /> + + ( + + + Confirm password + + +
+ + + +
+
+ +
+ )} + /> + + + Proceed to Dashboard + + + +
+ + ); +}; + +export default CreatePasswordForm; diff --git a/components/auth/LoginForm.tsx b/components/auth/LoginForm.tsx new file mode 100644 index 000000000..d155dd094 --- /dev/null +++ b/components/auth/LoginForm.tsx @@ -0,0 +1,246 @@ +'use client'; +import React, { useState } from 'react'; +import { BoundlessButton } from '../buttons'; +import { + Form, + FormControl, + FormField, + FormItem, + FormMessage, +} from '../ui/form'; +import { FormLabel } from '../ui/form'; +import z from 'zod'; +import { useForm } from 'react-hook-form'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { Input } from '../ui/input'; +import { Eye, EyeOff, LockIcon, MailIcon } from 'lucide-react'; +import { Checkbox } from '../ui/checkbox'; +import { Label } from '../ui/label'; +import Link from 'next/link'; +import { getSession, signIn } from 'next-auth/react'; +import { toast } from 'sonner'; +import { useRouter, useSearchParams } from 'next/navigation'; +import Cookies from 'js-cookie'; +import Image from 'next/image'; + +const formSchema = z.object({ + email: z.string().email({ + message: 'Invalid email address', + }), + password: z.string().min(8, { + message: 'Password must be at least 8 characters', + }), +}); + +const LoginForm = () => { + const router = useRouter(); + const searchParams = useSearchParams(); + const callbackUrl = searchParams.get('callbackUrl') || '/user'; + const [showPassword, setShowPassword] = useState(false); + + const form = useForm>({ + resolver: zodResolver(formSchema), + defaultValues: { + email: '', + password: '', + }, + }); + + const onSubmit = async (values: z.infer) => { + try { + const result = await signIn('credentials', { + email: values.email, + password: values.password, + redirect: false, + }); + + if (result?.error) { + if (result.error === 'UNVERIFIED_EMAIL') { + toast.error( + 'Please verify your email before signing in. Check your inbox for a verification link.' + ); + setTimeout(() => { + router.push( + `/auth/verify-email?email=${encodeURIComponent(values.email)}` + ); + }, 3000); + } else { + form.setError('email', { + type: 'manual', + message: 'Invalid email or password', + }); + form.setError('password', { + type: 'manual', + message: 'Invalid email or password', + }); + } + } else if (result?.ok) { + const session = await getSession(); + + if (session) { + if (session.user.accessToken) { + Cookies.set('accessToken', session.user.accessToken); + } + if (session.user.refreshToken) { + Cookies.set('refreshToken', session.user.refreshToken); + } + router.push(callbackUrl); + } else { + form.setError('root', { + type: 'manual', + message: + 'Login successful but session not found. Please try again.', + }); + } + } else { + form.setError('root', { + type: 'manual', + message: 'An unexpected error occurred. Please try again.', + }); + } + } catch { + form.setError('root', { + type: 'manual', + message: 'An unexpected error occurred. Please try again.', + }); + } + }; + + return ( + <> +
+

+ Sign in +

+

+ Sign in to manage campaigns, apply for grants, and track your funding + progress — all in one dashboard. +

+
+
+ + google + Continue with Google + + +
+
+

Or

+
+
+ +
+ + ( + + + Email + + +
+ + +
+
+ +
+ )} + /> + + ( + + + Password + + +
+ + + +
+
+ +
+ )} + /> + +
+
+ + +
+ + Forgot password? + +
+ + + Sign in + + + + +

+ Don't have an account?{' '} + + Sign up + +

+
+ + ); +}; + +export default LoginForm; diff --git a/components/auth/OtpForm.tsx b/components/auth/OtpForm.tsx new file mode 100644 index 000000000..31dbb2ccf --- /dev/null +++ b/components/auth/OtpForm.tsx @@ -0,0 +1,180 @@ +'use client'; +import React from 'react'; +import { BoundlessButton } from '../buttons'; +import { + Form, + FormControl, + FormField, + FormItem, + FormMessage, +} from '../ui/form'; +import z from 'zod'; +import { useForm } from 'react-hook-form'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { InputOTP, InputOTPSlot } from '../ui/input-otp'; +import { toast } from 'sonner'; +import { maskEmail } from '@/lib/utils'; +import Cookies from 'js-cookie'; + +const formSchema = z.object({ + otp: z.string().length(6, { + message: 'OTP must be 6 digits', + }), +}); + +interface OtpFormProps { + email: string; + onOtpSuccess: () => void; + onResendOtp: () => void; +} + +const OtpForm = ({ email, onOtpSuccess, onResendOtp }: OtpFormProps) => { + const form = useForm>({ + resolver: zodResolver(formSchema), + defaultValues: { + otp: '', + }, + }); + + const onSubmit = async (values: z.infer) => { + try { + const response = await fetch('/api/auth/verify-otp', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + email, + otp: values.otp, + }), + }); + + if (response.ok) { + const data = await response.json(); + + if (data.accessToken) { + Cookies.set('accessToken', data.accessToken); + } + if (data.refreshToken) { + Cookies.set('refreshToken', data.refreshToken); + } + + toast.success('Account created successfully!'); + onOtpSuccess(); + } else { + const error = await response.json(); + + form.setError('otp', { + type: 'manual', + message: error.message || 'Invalid OTP', + }); + } + } catch { + form.setError('otp', { + type: 'manual', + message: 'Failed to verify OTP. Please try again.', + }); + } + }; + + const handleResendOtp = async () => { + try { + onResendOtp(); + toast.success('OTP resent successfully!'); + } catch { + toast.error('Failed to resend OTP. Please try again.'); + } + }; + + return ( + <> +
+
+

+ Enter OTP +

+

+ Enter the OTP that was sent to {maskEmail(email)} +

+

+ Please keep this code private. +

+
+ +
+ + ( + + + + + + + + + + + + + + )} + /> + + + Continue + + + + +
+ +
+
+ + ); +}; + +export default OtpForm; diff --git a/components/auth/SignupForm.tsx b/components/auth/SignupForm.tsx new file mode 100644 index 000000000..5e42fe321 --- /dev/null +++ b/components/auth/SignupForm.tsx @@ -0,0 +1,308 @@ +'use client'; +import React from 'react'; +import { BoundlessButton } from '../buttons'; +import { + Form, + FormControl, + FormField, + FormItem, + FormMessage, +} from '../ui/form'; +import { FormLabel } from '../ui/form'; +import z from 'zod'; +import { useForm } from 'react-hook-form'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { Input } from '../ui/input'; +import { LockIcon, MailIcon, User } from 'lucide-react'; +import Link from 'next/link'; +import { toast } from 'sonner'; +import { useRouter } from 'next/navigation'; +import Image from 'next/image'; +import OtpForm from './OtpForm'; +import { useState } from 'react'; + +const formSchema = z.object({ + email: z.string().email({ + message: 'Invalid email address', + }), + firstName: z.string().min(2, { + message: 'First name must be at least 2 characters', + }), + lastName: z.string().min(2, { + message: 'Last name must be at least 2 characters', + }), + password: z.string().min(8, { + message: 'Password must be at least 8 characters', + }), +}); + +const SignupForm = () => { + const router = useRouter(); + const [step, setStep] = useState<'signup' | 'otp'>('signup'); + const [userData, setUserData] = useState<{ email: string } | null>(null); + + const form = useForm>({ + resolver: zodResolver(formSchema), + defaultValues: { + email: '', + firstName: '', + lastName: '', + password: '', + }, + }); + + const onSubmit = async (values: z.infer) => { + try { + const response = await fetch('/api/auth/register', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + email: values.email, + firstName: values.firstName, + lastName: values.lastName, + password: values.password, + }), + }); + + if (response.ok) { + setUserData({ email: values.email }); + setStep('otp'); + toast.success('OTP sent to your email!'); + } else { + const error = await response.json(); + + if (error.field) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + form.setError(error.field as any, { + type: 'manual', + message: error.message, + }); + } else if (error.message) { + form.setError('root', { + type: 'manual', + message: error.message, + }); + } else { + form.setError('root', { + type: 'manual', + message: 'Failed to create account', + }); + } + } + } catch { + form.setError('root', { + type: 'manual', + message: 'Failed to create account. Please try again.', + }); + } + }; + + const handleOtpSuccess = () => { + router.push('/user'); + }; + + const handleResendOtp = async () => { + if (!userData) return; + + try { + const response = await fetch('/api/auth/resend-otp', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + email: userData.email, + }), + }); + + if (response.ok) { + toast.success('OTP resent successfully!'); + } else { + toast.error('Failed to resend OTP'); + } + } catch { + toast.error('Failed to resend OTP'); + } + }; + + if (step === 'otp' && userData) { + return ( + + ); + } + + return ( + <> +
+

+ Create account +

+

+ Create an account to manage campaigns, apply for grants, and track + your funding progress — all in one dashboard. +

+
+
+ + google + Continue with Google + + +
+
+

Or

+
+
+ +
+ + {form.formState.errors.root && ( +
+ {form.formState.errors.root.message} +
+ )} +
+ ( + + + First Name + + +
+ + +
+
+ +
+ )} + /> + ( + + + Last Name + + +
+ + +
+
+ +
+ )} + /> +
+ + ( + + + Email + + +
+ + +
+
+ +
+ )} + /> + + ( + + + Password + + +
+ + +
+
+ +
+ )} + /> + + + Continue + + + + +

+ By continuing, you agree to our{' '} + Terms of Service and{' '} + Privacy Policy +

+ +

+ Already have an account?{' '} + + Sign in + +

+
+ + ); +}; + +export default SignupForm; diff --git a/components/buttons/BoundlessButton.tsx b/components/buttons/BoundlessButton.tsx index e19f9fb05..c47d34312 100644 --- a/components/buttons/BoundlessButton.tsx +++ b/components/buttons/BoundlessButton.tsx @@ -5,6 +5,7 @@ import { cva, type VariantProps } from 'class-variance-authority'; import { cn } from '@/lib/utils'; import { motion } from 'framer-motion'; import { buttonHover } from '@/lib/motion'; +import LoadingSpinner from '../LoadingSpinner'; const boundlessButtonVariants = cva( "inline-flex items-center justify-center gap-2 whitespace-nowrap !rounded-[10px] text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", @@ -64,6 +65,7 @@ export interface BoundlessButtonProps loading?: boolean; icon?: React.ReactNode; iconPosition?: 'left' | 'right'; + fullWidth?: boolean; } const BoundlessButton = React.forwardRef< @@ -81,6 +83,7 @@ const BoundlessButton = React.forwardRef< iconPosition = 'left', children, disabled, + fullWidth, ...props }, ref @@ -90,26 +93,11 @@ const BoundlessButton = React.forwardRef< const buttonContent = ( <> {loading && ( - - - - + )} {!loading && icon && iconPosition === 'left' && icon} @@ -123,7 +111,7 @@ const BoundlessButton = React.forwardRef< whileHover='hover' whileTap='tap' variants={buttonHover} - className='inline-block' + className={cn('inline-block', fullWidth && 'w-full')} >