feat(01-04): add Google button to login and register pages
- Add auth layout with centered card design - Add Input component for form fields - Add LoginForm component with email/password and validation - Add RegisterForm component with password requirements - Add login page with Google button + 'oppure' divider + email form - Add register page with Google button + 'oppure' divider + email form - Italian text throughout (Accedi, Registrati, oppure) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
13
src/app/(auth)/layout.tsx
Normal file
13
src/app/(auth)/layout.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
export default function AuthLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-gray-50 py-12 px-4 sm:px-6 lg:px-8">
|
||||
<div className="max-w-md w-full space-y-8">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
30
src/app/(auth)/login/page.tsx
Normal file
30
src/app/(auth)/login/page.tsx
Normal file
@@ -0,0 +1,30 @@
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { LoginForm } from '@/components/auth/login-form'
|
||||
import { GoogleSignInButton } from '@/components/auth/google-button'
|
||||
|
||||
export default function LoginPage() {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader className="text-center">
|
||||
<CardTitle>Accedi a Leopost</CardTitle>
|
||||
<CardDescription>
|
||||
Inserisci le tue credenziali per continuare
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<GoogleSignInButton />
|
||||
|
||||
<div className="relative">
|
||||
<div className="absolute inset-0 flex items-center">
|
||||
<span className="w-full border-t border-gray-300" />
|
||||
</div>
|
||||
<div className="relative flex justify-center text-xs uppercase">
|
||||
<span className="bg-white px-2 text-gray-500">oppure</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<LoginForm />
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
30
src/app/(auth)/register/page.tsx
Normal file
30
src/app/(auth)/register/page.tsx
Normal file
@@ -0,0 +1,30 @@
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { RegisterForm } from '@/components/auth/register-form'
|
||||
import { GoogleSignInButton } from '@/components/auth/google-button'
|
||||
|
||||
export default function RegisterPage() {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader className="text-center">
|
||||
<CardTitle>Crea il tuo account</CardTitle>
|
||||
<CardDescription>
|
||||
Inizia a usare Leopost gratuitamente
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<GoogleSignInButton />
|
||||
|
||||
<div className="relative">
|
||||
<div className="absolute inset-0 flex items-center">
|
||||
<span className="w-full border-t border-gray-300" />
|
||||
</div>
|
||||
<div className="relative flex justify-center text-xs uppercase">
|
||||
<span className="bg-white px-2 text-gray-500">oppure</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<RegisterForm />
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
103
src/components/auth/login-form.tsx
Normal file
103
src/components/auth/login-form.tsx
Normal file
@@ -0,0 +1,103 @@
|
||||
'use client'
|
||||
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import Link from 'next/link'
|
||||
import { useState } from 'react'
|
||||
import { createClient } from '@/lib/supabase/client'
|
||||
import { useRouter } from 'next/navigation'
|
||||
|
||||
export function LoginForm() {
|
||||
const [email, setEmail] = useState('')
|
||||
const [password, setPassword] = useState('')
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const router = useRouter()
|
||||
const supabase = createClient()
|
||||
|
||||
async function handleSubmit(e: React.FormEvent) {
|
||||
e.preventDefault()
|
||||
setError(null)
|
||||
setLoading(true)
|
||||
|
||||
const { error: signInError } = await supabase.auth.signInWithPassword({
|
||||
email,
|
||||
password,
|
||||
})
|
||||
|
||||
if (signInError) {
|
||||
// SPECIFIC error messages per user requirement
|
||||
if (signInError.message.includes('Invalid login credentials')) {
|
||||
setError('Email o password errata')
|
||||
} else if (signInError.message.includes('Email not confirmed')) {
|
||||
setError('Devi confermare la tua email prima di accedere')
|
||||
} else {
|
||||
setError(signInError.message)
|
||||
}
|
||||
setLoading(false)
|
||||
return
|
||||
}
|
||||
|
||||
router.push('/dashboard')
|
||||
router.refresh()
|
||||
}
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
{error && (
|
||||
<div className="p-3 bg-red-50 border border-red-200 rounded-md">
|
||||
<p className="text-red-800 text-sm">{error}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<label htmlFor="email" className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Email
|
||||
</label>
|
||||
<Input
|
||||
id="email"
|
||||
name="email"
|
||||
type="email"
|
||||
autoComplete="email"
|
||||
required
|
||||
placeholder="tu@esempio.com"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="password" className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Password
|
||||
</label>
|
||||
<Input
|
||||
id="password"
|
||||
name="password"
|
||||
type="password"
|
||||
autoComplete="current-password"
|
||||
required
|
||||
placeholder="La tua password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-end">
|
||||
<Link href="/reset-password" className="text-sm text-blue-600 hover:underline">
|
||||
Password dimenticata?
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<Button type="submit" className="w-full" disabled={loading}>
|
||||
{loading ? 'Accesso...' : 'Accedi'}
|
||||
</Button>
|
||||
|
||||
<p className="text-center text-sm text-gray-600">
|
||||
Non hai un account?{' '}
|
||||
<Link href="/register" className="text-blue-600 hover:underline">
|
||||
Registrati
|
||||
</Link>
|
||||
</p>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
172
src/components/auth/register-form.tsx
Normal file
172
src/components/auth/register-form.tsx
Normal file
@@ -0,0 +1,172 @@
|
||||
'use client'
|
||||
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import Link from 'next/link'
|
||||
import { useState } from 'react'
|
||||
import { createClient } from '@/lib/supabase/client'
|
||||
|
||||
export function RegisterForm() {
|
||||
const [email, setEmail] = useState('')
|
||||
const [password, setPassword] = useState('')
|
||||
const [confirmPassword, setConfirmPassword] = useState('')
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [fieldErrors, setFieldErrors] = useState<Record<string, string>>({})
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [success, setSuccess] = useState(false)
|
||||
const supabase = createClient()
|
||||
|
||||
function validatePassword(pwd: string): string | null {
|
||||
if (pwd.length < 8) {
|
||||
return 'La password deve contenere almeno 8 caratteri'
|
||||
}
|
||||
if (!/[0-9]/.test(pwd)) {
|
||||
return 'La password deve contenere almeno un numero'
|
||||
}
|
||||
if (!/[A-Z]/.test(pwd)) {
|
||||
return 'La password deve contenere almeno una lettera maiuscola'
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
async function handleSubmit(e: React.FormEvent) {
|
||||
e.preventDefault()
|
||||
setError(null)
|
||||
setFieldErrors({})
|
||||
setLoading(true)
|
||||
|
||||
// Validate password
|
||||
const passwordError = validatePassword(password)
|
||||
if (passwordError) {
|
||||
setFieldErrors({ password: passwordError })
|
||||
setLoading(false)
|
||||
return
|
||||
}
|
||||
|
||||
// Validate confirm password
|
||||
if (password !== confirmPassword) {
|
||||
setFieldErrors({ confirmPassword: 'Le password non coincidono' })
|
||||
setLoading(false)
|
||||
return
|
||||
}
|
||||
|
||||
const { error: signUpError } = await supabase.auth.signUp({
|
||||
email,
|
||||
password,
|
||||
options: {
|
||||
emailRedirectTo: `${window.location.origin}/auth/callback`,
|
||||
}
|
||||
})
|
||||
|
||||
if (signUpError) {
|
||||
// SPECIFIC error messages per user requirement
|
||||
if (signUpError.message.includes('already registered')) {
|
||||
setError('Questa email e gia registrata')
|
||||
} else if (signUpError.message.includes('invalid')) {
|
||||
setError('Email non valida')
|
||||
} else {
|
||||
setError(signUpError.message)
|
||||
}
|
||||
setLoading(false)
|
||||
return
|
||||
}
|
||||
|
||||
setSuccess(true)
|
||||
setLoading(false)
|
||||
}
|
||||
|
||||
if (success) {
|
||||
return (
|
||||
<div className="text-center">
|
||||
<div className="mb-4 p-4 bg-green-50 border border-green-200 rounded-md">
|
||||
<p className="text-green-800">
|
||||
Registrazione completata! Controlla la tua email per confermare l'account.
|
||||
</p>
|
||||
</div>
|
||||
<Link href="/login" className="text-blue-600 hover:underline">
|
||||
Torna al login
|
||||
</Link>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
{error && (
|
||||
<div className="p-3 bg-red-50 border border-red-200 rounded-md">
|
||||
<p className="text-red-800 text-sm">{error}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<label htmlFor="email" className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Email
|
||||
</label>
|
||||
<Input
|
||||
id="email"
|
||||
name="email"
|
||||
type="email"
|
||||
autoComplete="email"
|
||||
required
|
||||
placeholder="tu@esempio.com"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="password" className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Password
|
||||
</label>
|
||||
<Input
|
||||
id="password"
|
||||
name="password"
|
||||
type="password"
|
||||
autoComplete="new-password"
|
||||
required
|
||||
placeholder="Minimo 8 caratteri"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
error={!!fieldErrors.password}
|
||||
/>
|
||||
{fieldErrors.password && (
|
||||
<p className="mt-1 text-sm text-red-600">{fieldErrors.password}</p>
|
||||
)}
|
||||
<p className="mt-1 text-xs text-gray-500">
|
||||
Almeno 8 caratteri, 1 numero, 1 maiuscola
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="confirmPassword" className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Conferma Password
|
||||
</label>
|
||||
<Input
|
||||
id="confirmPassword"
|
||||
name="confirmPassword"
|
||||
type="password"
|
||||
autoComplete="new-password"
|
||||
required
|
||||
placeholder="Ripeti la password"
|
||||
value={confirmPassword}
|
||||
onChange={(e) => setConfirmPassword(e.target.value)}
|
||||
error={!!fieldErrors.confirmPassword}
|
||||
/>
|
||||
{fieldErrors.confirmPassword && (
|
||||
<p className="mt-1 text-sm text-red-600">{fieldErrors.confirmPassword}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Button type="submit" className="w-full" disabled={loading}>
|
||||
{loading ? 'Registrazione...' : 'Registrati'}
|
||||
</Button>
|
||||
|
||||
<p className="text-center text-sm text-gray-600">
|
||||
Hai gia un account?{' '}
|
||||
<Link href="/login" className="text-blue-600 hover:underline">
|
||||
Accedi
|
||||
</Link>
|
||||
</p>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
28
src/components/ui/input.tsx
Normal file
28
src/components/ui/input.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
import { forwardRef, InputHTMLAttributes } from 'react'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
export interface InputProps extends InputHTMLAttributes<HTMLInputElement> {
|
||||
error?: boolean
|
||||
}
|
||||
|
||||
const Input = forwardRef<HTMLInputElement, InputProps>(
|
||||
({ className, error, ...props }, ref) => {
|
||||
return (
|
||||
<input
|
||||
className={cn(
|
||||
'flex h-10 w-full rounded-md border bg-white px-3 py-2 text-sm',
|
||||
'placeholder:text-gray-400',
|
||||
'focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-1',
|
||||
'disabled:cursor-not-allowed disabled:opacity-50',
|
||||
error ? 'border-red-500' : 'border-gray-300',
|
||||
className
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
)
|
||||
Input.displayName = 'Input'
|
||||
|
||||
export { Input }
|
||||
Reference in New Issue
Block a user