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:
Michele
2026-01-31 05:07:24 +01:00
parent 1d454d2fcb
commit dcbd7e8b46
6 changed files with 376 additions and 0 deletions

13
src/app/(auth)/layout.tsx Normal file
View 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>
)
}

View 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>
)
}

View 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>
)
}

View 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>
)
}

View 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&apos;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>
)
}

View 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 }