fix(login): replicate original Leopost auth layout exactly
- Left panel: dark ink (#1A1A1A) bg with decorative blobs, testimonial quote, avatar, copyright — NOT coral - Right panel: cream bg, no card/shadow, form directly on page - editorial-tag (red uppercase label) + Fraunces heading per page - Google button: outline style (white bg + border) - Inputs: full border h-11, white bg, focus:border-ink, no border-radius - Submit CTA: black full-width h-12 → hover accent - Login/Register as separate form components (not tab toggle) - Responsive: left panel hidden on mobile Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,25 +1,149 @@
|
|||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { useNavigate } from 'react-router-dom'
|
import { useNavigate, Link } from 'react-router-dom'
|
||||||
import { useAuth } from '../AuthContext'
|
import { useAuth } from '../AuthContext'
|
||||||
import { BASE_URL } from '../api'
|
import { BASE_URL } from '../api'
|
||||||
|
|
||||||
const ACCENT = '#E85A4F'
|
|
||||||
const ACCENT_HOVER= '#D14940'
|
|
||||||
const CREAM = '#FFFBF5'
|
|
||||||
const INK = '#1A1A1A'
|
|
||||||
const INK_MUTED = '#7A7A7A'
|
|
||||||
const BORDER = '#E5E0D8'
|
|
||||||
|
|
||||||
export default function LoginPage() {
|
export default function LoginPage() {
|
||||||
const [mode, setMode] = useState('login') // 'login' | 'register'
|
const [mode, setMode] = useState('login') // 'login' | 'register'
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ minHeight: '100vh', display: 'flex', fontFamily: "'DM Sans', sans-serif" }}>
|
||||||
|
|
||||||
|
{/* ── LEFT — dark ink branding panel ─────────────────────── */}
|
||||||
|
<div style={{
|
||||||
|
display: 'none',
|
||||||
|
width: '50%',
|
||||||
|
backgroundColor: '#1A1A1A',
|
||||||
|
padding: '3rem',
|
||||||
|
flexDirection: 'column',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
position: 'relative',
|
||||||
|
overflow: 'hidden',
|
||||||
|
// show on large screens via media query (inlined below)
|
||||||
|
}}
|
||||||
|
className="auth-left-panel"
|
||||||
|
>
|
||||||
|
{/* Decorative blobs */}
|
||||||
|
<div style={{
|
||||||
|
position: 'absolute', top: 0, right: 0,
|
||||||
|
width: 256, height: 256,
|
||||||
|
borderRadius: '50%',
|
||||||
|
backgroundColor: 'rgba(232,90,79,0.10)',
|
||||||
|
transform: 'translate(50%, -50%)',
|
||||||
|
}} />
|
||||||
|
<div style={{
|
||||||
|
position: 'absolute', bottom: 0, left: 0,
|
||||||
|
width: 384, height: 384,
|
||||||
|
borderRadius: '50%',
|
||||||
|
backgroundColor: 'rgba(232,90,79,0.05)',
|
||||||
|
transform: 'translate(-50%, 50%)',
|
||||||
|
}} />
|
||||||
|
|
||||||
|
{/* Logo */}
|
||||||
|
<span style={{
|
||||||
|
fontFamily: "'Fraunces', serif",
|
||||||
|
fontSize: '1.75rem',
|
||||||
|
fontWeight: 600,
|
||||||
|
color: '#FFFBF5',
|
||||||
|
position: 'relative',
|
||||||
|
zIndex: 1,
|
||||||
|
}}>
|
||||||
|
Leopost
|
||||||
|
</span>
|
||||||
|
|
||||||
|
{/* Testimonial */}
|
||||||
|
<div style={{ position: 'relative', zIndex: 1, maxWidth: 400 }}>
|
||||||
|
<div style={{ width: 48, height: 4, backgroundColor: '#E85A4F', marginBottom: '2rem' }} />
|
||||||
|
<blockquote style={{
|
||||||
|
fontFamily: "'Fraunces', serif",
|
||||||
|
fontSize: '1.75rem',
|
||||||
|
color: 'rgba(255,251,245,0.9)',
|
||||||
|
lineHeight: 1.35,
|
||||||
|
fontWeight: 500,
|
||||||
|
fontStyle: 'italic',
|
||||||
|
margin: 0,
|
||||||
|
}}>
|
||||||
|
"Finalmente un assistente che capisce il mio brand e mi fa risparmiare ore ogni settimana."
|
||||||
|
</blockquote>
|
||||||
|
<div style={{ marginTop: '2rem', display: 'flex', alignItems: 'center', gap: '1rem' }}>
|
||||||
|
<div style={{
|
||||||
|
width: 48, height: 48,
|
||||||
|
borderRadius: '50%',
|
||||||
|
backgroundColor: 'rgba(255,251,245,0.2)',
|
||||||
|
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||||
|
}}>
|
||||||
|
<span style={{ fontFamily: "'Fraunces', serif", color: '#FFFBF5', fontWeight: 600 }}>MR</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p style={{ color: '#FFFBF5', fontWeight: 500, margin: 0 }}>Marco Rossi</p>
|
||||||
|
<p style={{ color: 'rgba(255,251,245,0.6)', fontSize: '0.875rem', margin: 0 }}>Social Media Manager</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Copyright */}
|
||||||
|
<p style={{ color: 'rgba(255,251,245,0.4)', fontSize: '0.875rem', position: 'relative', zIndex: 1 }}>
|
||||||
|
© 2026 Leopost
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* ── RIGHT — form panel ──────────────────────────────────── */}
|
||||||
|
<div style={{
|
||||||
|
flex: 1,
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
backgroundColor: '#FFFBF5',
|
||||||
|
}}>
|
||||||
|
{/* Mobile header */}
|
||||||
|
<div style={{
|
||||||
|
padding: '1.5rem',
|
||||||
|
borderBottom: '1px solid #E5E0D8',
|
||||||
|
}}
|
||||||
|
className="auth-mobile-header"
|
||||||
|
>
|
||||||
|
<span style={{ fontFamily: "'Fraunces', serif", fontSize: '1.5rem', fontWeight: 600, color: '#1A1A1A' }}>
|
||||||
|
Leopost
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Form area */}
|
||||||
|
<div style={{
|
||||||
|
flex: 1,
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
padding: '2rem 1.5rem',
|
||||||
|
}}>
|
||||||
|
<div style={{ width: '100%', maxWidth: 448, animation: 'fade-up 0.5s ease-out both' }}>
|
||||||
|
{mode === 'login' ? (
|
||||||
|
<LoginForm onSwitchMode={() => setMode('register')} />
|
||||||
|
) : (
|
||||||
|
<RegisterForm onSwitchMode={() => setMode('login')} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* CSS for responsive left panel */}
|
||||||
|
<style>{`
|
||||||
|
@media (min-width: 1024px) {
|
||||||
|
.auth-left-panel { display: flex !important; }
|
||||||
|
.auth-mobile-header { display: none !important; }
|
||||||
|
}
|
||||||
|
`}</style>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Login form ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
function LoginForm({ onSwitchMode }) {
|
||||||
const [email, setEmail] = useState('')
|
const [email, setEmail] = useState('')
|
||||||
const [displayName, setDisplayName] = useState('')
|
|
||||||
const [password, setPassword] = useState('')
|
const [password, setPassword] = useState('')
|
||||||
const [error, setError] = useState('')
|
const [error, setError] = useState('')
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
const [showRedeemModal, setShowRedeemModal] = useState(false)
|
|
||||||
|
|
||||||
const { login, register } = useAuth()
|
const { login } = useAuth()
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
|
|
||||||
const handleSubmit = async (e) => {
|
const handleSubmit = async (e) => {
|
||||||
@@ -27,475 +151,364 @@ export default function LoginPage() {
|
|||||||
setError('')
|
setError('')
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
try {
|
try {
|
||||||
if (mode === 'login') {
|
await login(email, password)
|
||||||
await login(email, password)
|
|
||||||
} else {
|
|
||||||
await register(email, password, displayName)
|
|
||||||
}
|
|
||||||
navigate('/')
|
navigate('/')
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err.message || (mode === 'login' ? 'Credenziali non valide' : 'Errore durante la registrazione'))
|
setError(err.message || 'Email o password errata')
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleGoogleLogin = () => {
|
return (
|
||||||
window.location.href = `${BASE_URL}/auth/oauth/google`
|
<div>
|
||||||
|
{/* Header */}
|
||||||
|
<div style={{ marginBottom: '2rem' }}>
|
||||||
|
<span style={{
|
||||||
|
fontSize: '0.72rem',
|
||||||
|
fontWeight: 700,
|
||||||
|
letterSpacing: '0.12em',
|
||||||
|
textTransform: 'uppercase',
|
||||||
|
color: '#E85A4F',
|
||||||
|
}}>
|
||||||
|
Bentornato
|
||||||
|
</span>
|
||||||
|
<h1 style={{
|
||||||
|
fontFamily: "'Fraunces', serif",
|
||||||
|
fontSize: '1.875rem',
|
||||||
|
fontWeight: 600,
|
||||||
|
color: '#1A1A1A',
|
||||||
|
letterSpacing: '-0.02em',
|
||||||
|
margin: '1rem 0 0.5rem',
|
||||||
|
}}>
|
||||||
|
Accedi a Leopost
|
||||||
|
</h1>
|
||||||
|
<p style={{ color: '#4A4A4A', fontSize: '0.9rem', margin: 0 }}>
|
||||||
|
Inserisci le tue credenziali per continuare
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Google button */}
|
||||||
|
<GoogleButton onClick={() => { window.location.href = `${BASE_URL}/auth/oauth/google` }} />
|
||||||
|
|
||||||
|
{/* Divider */}
|
||||||
|
<Divider />
|
||||||
|
|
||||||
|
{/* Form */}
|
||||||
|
<form onSubmit={handleSubmit} style={{ display: 'flex', flexDirection: 'column', gap: '1.25rem' }}>
|
||||||
|
{error && <ErrorBox message={error} />}
|
||||||
|
|
||||||
|
<Field label="Email">
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
value={email}
|
||||||
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
|
placeholder="tu@esempio.it"
|
||||||
|
required
|
||||||
|
style={inputStyle}
|
||||||
|
onFocus={(e) => e.target.style.borderColor = '#1A1A1A'}
|
||||||
|
onBlur={(e) => e.target.style.borderColor = '#E5E0D8'}
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
<Field label="Password">
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
value={password}
|
||||||
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
|
placeholder="La tua password"
|
||||||
|
required
|
||||||
|
style={inputStyle}
|
||||||
|
onFocus={(e) => e.target.style.borderColor = '#1A1A1A'}
|
||||||
|
onBlur={(e) => e.target.style.borderColor = '#E5E0D8'}
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
<div style={{ textAlign: 'right', marginTop: '-0.5rem' }}>
|
||||||
|
<span style={{ fontSize: '0.875rem', color: '#E85A4F', cursor: 'pointer' }}>
|
||||||
|
Password dimenticata?
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<SubmitButton loading={loading}>
|
||||||
|
{loading ? 'Accesso in corso...' : 'Accedi'}
|
||||||
|
</SubmitButton>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{/* Footer */}
|
||||||
|
<div style={{ marginTop: '2rem', paddingTop: '1.5rem', borderTop: '1px solid #E5E0D8', textAlign: 'center' }}>
|
||||||
|
<p style={{ fontSize: '0.875rem', color: '#4A4A4A', margin: 0 }}>
|
||||||
|
Non hai un account?{' '}
|
||||||
|
<button
|
||||||
|
onClick={onSwitchMode}
|
||||||
|
style={{ background: 'none', border: 'none', color: '#E85A4F', cursor: 'pointer', fontWeight: 600, fontSize: '0.875rem', padding: 0, fontFamily: "'DM Sans', sans-serif" }}
|
||||||
|
>
|
||||||
|
Registrati gratis
|
||||||
|
</button>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Register form ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
function RegisterForm({ onSwitchMode }) {
|
||||||
|
const [email, setEmail] = useState('')
|
||||||
|
const [displayName, setDisplayName] = useState('')
|
||||||
|
const [password, setPassword] = useState('')
|
||||||
|
const [error, setError] = useState('')
|
||||||
|
const [loading, setLoading] = useState(false)
|
||||||
|
|
||||||
|
const { register } = useAuth()
|
||||||
|
const navigate = useNavigate()
|
||||||
|
|
||||||
|
const handleSubmit = async (e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
setError('')
|
||||||
|
setLoading(true)
|
||||||
|
try {
|
||||||
|
await register(email, password, displayName)
|
||||||
|
navigate('/')
|
||||||
|
} catch (err) {
|
||||||
|
setError(err.message || 'Errore durante la registrazione')
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{
|
<div>
|
||||||
display: 'flex',
|
{/* Header */}
|
||||||
height: '100vh',
|
<div style={{ marginBottom: '2rem' }}>
|
||||||
fontFamily: "'DM Sans', sans-serif",
|
<span style={{
|
||||||
overflow: 'hidden',
|
fontSize: '0.72rem',
|
||||||
}}>
|
fontWeight: 700,
|
||||||
{/* ── LEFT PANEL ─────────────────────────────────────────── */}
|
letterSpacing: '0.12em',
|
||||||
<div style={{
|
textTransform: 'uppercase',
|
||||||
width: '45%',
|
color: '#E85A4F',
|
||||||
backgroundColor: ACCENT,
|
|
||||||
padding: '3rem',
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'column',
|
|
||||||
justifyContent: 'space-between',
|
|
||||||
color: 'white',
|
|
||||||
flexShrink: 0,
|
|
||||||
}}>
|
|
||||||
<div style={{ animation: 'fade-up 0.6s ease-out both' }}>
|
|
||||||
{/* Tag editoriale */}
|
|
||||||
<span style={{
|
|
||||||
display: 'inline-block',
|
|
||||||
fontSize: '0.7rem',
|
|
||||||
fontWeight: 700,
|
|
||||||
letterSpacing: '0.15em',
|
|
||||||
textTransform: 'uppercase',
|
|
||||||
color: 'rgba(255,255,255,0.7)',
|
|
||||||
marginBottom: '0.5rem',
|
|
||||||
}}>
|
|
||||||
LEOPOST
|
|
||||||
</span>
|
|
||||||
{/* editorial-line bianco */}
|
|
||||||
<div style={{ width: 60, height: 3, backgroundColor: 'rgba(255,255,255,0.6)', marginBottom: '1.5rem' }} />
|
|
||||||
|
|
||||||
<h1 style={{
|
|
||||||
fontFamily: "'Fraunces', serif",
|
|
||||||
fontSize: '2.6rem',
|
|
||||||
fontWeight: 700,
|
|
||||||
margin: 0,
|
|
||||||
lineHeight: 1.15,
|
|
||||||
letterSpacing: '-0.02em',
|
|
||||||
}}>
|
|
||||||
Il tuo studio<br />editoriale AI
|
|
||||||
</h1>
|
|
||||||
|
|
||||||
<p style={{
|
|
||||||
fontFamily: "'DM Sans', sans-serif",
|
|
||||||
fontSize: '1rem',
|
|
||||||
marginTop: '1rem',
|
|
||||||
opacity: 0.85,
|
|
||||||
lineHeight: 1.6,
|
|
||||||
fontWeight: 400,
|
|
||||||
maxWidth: 300,
|
|
||||||
}}>
|
|
||||||
Genera, schedula e pubblica contenuti su tutti i social — con l'AI al tuo fianco.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
{/* Benefit list */}
|
|
||||||
<ul style={{ marginTop: '2.5rem', listStyle: 'none', padding: 0, display: 'flex', flexDirection: 'column', gap: '0.75rem' }}>
|
|
||||||
{[
|
|
||||||
'Personaggi AI con voce e stile unico',
|
|
||||||
'Contenuti su Facebook, Instagram, YouTube, TikTok',
|
|
||||||
'Schedulazione automatica con piani editoriali',
|
|
||||||
].map((txt) => (
|
|
||||||
<li key={txt} style={{
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'flex-start',
|
|
||||||
gap: '0.75rem',
|
|
||||||
fontSize: '0.9rem',
|
|
||||||
lineHeight: 1.5,
|
|
||||||
padding: '0.6rem 0.9rem',
|
|
||||||
backgroundColor: 'rgba(255,255,255,0.12)',
|
|
||||||
backdropFilter: 'blur(4px)',
|
|
||||||
}}>
|
|
||||||
<span style={{ fontSize: '0.8rem', marginTop: '0.15rem', flexShrink: 0 }}>✦</span>
|
|
||||||
{txt}
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Badge in basso */}
|
|
||||||
<div style={{
|
|
||||||
display: 'inline-flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
gap: '0.5rem',
|
|
||||||
padding: '0.6rem 1rem',
|
|
||||||
border: '1px solid rgba(255,255,255,0.4)',
|
|
||||||
fontSize: '0.8rem',
|
|
||||||
fontWeight: 600,
|
|
||||||
letterSpacing: '0.05em',
|
|
||||||
animation: 'fade-up 0.6s ease-out 0.3s both',
|
|
||||||
alignSelf: 'flex-start',
|
|
||||||
}}>
|
}}>
|
||||||
<span style={{ opacity: 0.7 }}>★</span> EARLY ADOPTER BETA
|
Inizia gratis
|
||||||
</div>
|
</span>
|
||||||
|
<h1 style={{
|
||||||
|
fontFamily: "'Fraunces', serif",
|
||||||
|
fontSize: '1.875rem',
|
||||||
|
fontWeight: 600,
|
||||||
|
color: '#1A1A1A',
|
||||||
|
letterSpacing: '-0.02em',
|
||||||
|
margin: '1rem 0 0.5rem',
|
||||||
|
}}>
|
||||||
|
Crea il tuo account
|
||||||
|
</h1>
|
||||||
|
<p style={{ color: '#4A4A4A', fontSize: '0.9rem', margin: 0 }}>
|
||||||
|
Inizia a gestire i tuoi social in modo intelligente
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* ── RIGHT PANEL ────────────────────────────────────────── */}
|
{/* Google button */}
|
||||||
<div style={{
|
<GoogleButton onClick={() => { window.location.href = `${BASE_URL}/auth/oauth/google` }} />
|
||||||
flex: 1,
|
|
||||||
backgroundColor: CREAM,
|
{/* Divider */}
|
||||||
|
<Divider />
|
||||||
|
|
||||||
|
{/* Form */}
|
||||||
|
<form onSubmit={handleSubmit} style={{ display: 'flex', flexDirection: 'column', gap: '1.25rem' }}>
|
||||||
|
{error && <ErrorBox message={error} />}
|
||||||
|
|
||||||
|
<Field label="Nome visualizzato">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={displayName}
|
||||||
|
onChange={(e) => setDisplayName(e.target.value)}
|
||||||
|
placeholder="Il tuo nome o nickname"
|
||||||
|
style={inputStyle}
|
||||||
|
onFocus={(e) => e.target.style.borderColor = '#1A1A1A'}
|
||||||
|
onBlur={(e) => e.target.style.borderColor = '#E5E0D8'}
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
<Field label="Email">
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
value={email}
|
||||||
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
|
placeholder="tu@esempio.it"
|
||||||
|
required
|
||||||
|
style={inputStyle}
|
||||||
|
onFocus={(e) => e.target.style.borderColor = '#1A1A1A'}
|
||||||
|
onBlur={(e) => e.target.style.borderColor = '#E5E0D8'}
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
<Field label="Password">
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
value={password}
|
||||||
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
|
placeholder="Minimo 8 caratteri"
|
||||||
|
required
|
||||||
|
style={inputStyle}
|
||||||
|
onFocus={(e) => e.target.style.borderColor = '#1A1A1A'}
|
||||||
|
onBlur={(e) => e.target.style.borderColor = '#E5E0D8'}
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
<SubmitButton loading={loading}>
|
||||||
|
{loading ? 'Creazione account...' : 'Crea account'}
|
||||||
|
</SubmitButton>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{/* Footer */}
|
||||||
|
<div style={{ marginTop: '2rem', paddingTop: '1.5rem', borderTop: '1px solid #E5E0D8', textAlign: 'center' }}>
|
||||||
|
<p style={{ fontSize: '0.875rem', color: '#4A4A4A', margin: 0 }}>
|
||||||
|
Hai già un account?{' '}
|
||||||
|
<button
|
||||||
|
onClick={onSwitchMode}
|
||||||
|
style={{ background: 'none', border: 'none', color: '#E85A4F', cursor: 'pointer', fontWeight: 600, fontSize: '0.875rem', padding: 0, fontFamily: "'DM Sans', sans-serif" }}
|
||||||
|
>
|
||||||
|
Accedi
|
||||||
|
</button>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Shared sub-components ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
function GoogleButton({ onClick }) {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onClick}
|
||||||
|
style={{
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
padding: '2rem',
|
gap: '0.75rem',
|
||||||
overflowY: 'auto',
|
width: '100%',
|
||||||
}}>
|
height: 48,
|
||||||
<div style={{ width: '100%', maxWidth: '420px', animation: 'fade-up 0.6s ease-out 0.1s both' }}>
|
backgroundColor: 'white',
|
||||||
|
border: '1px solid #E5E0D8',
|
||||||
|
borderRadius: 0,
|
||||||
|
cursor: 'pointer',
|
||||||
|
fontFamily: "'DM Sans', sans-serif",
|
||||||
|
fontWeight: 500,
|
||||||
|
fontSize: '1rem',
|
||||||
|
color: '#1A1A1A',
|
||||||
|
transition: 'border-color 0.15s',
|
||||||
|
}}
|
||||||
|
onMouseEnter={(e) => e.currentTarget.style.borderColor = '#1A1A1A'}
|
||||||
|
onMouseLeave={(e) => e.currentTarget.style.borderColor = '#E5E0D8'}
|
||||||
|
>
|
||||||
|
<GoogleIcon />
|
||||||
|
Continua con Google
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
{/* ── Mode toggle ── */}
|
function Divider() {
|
||||||
<div style={{
|
return (
|
||||||
display: 'flex',
|
<div style={{ position: 'relative', margin: '2rem 0' }}>
|
||||||
borderBottom: `2px solid ${BORDER}`,
|
<div style={{ position: 'absolute', inset: 0, display: 'flex', alignItems: 'center' }}>
|
||||||
marginBottom: '2rem',
|
<div style={{ width: '100%', borderTop: '1px solid #E5E0D8' }} />
|
||||||
}}>
|
</div>
|
||||||
{[
|
<div style={{ position: 'relative', display: 'flex', justifyContent: 'center' }}>
|
||||||
{ key: 'login', label: 'Accedi' },
|
<span style={{
|
||||||
{ key: 'register', label: 'Registrati' },
|
backgroundColor: '#FFFBF5',
|
||||||
].map(({ key, label }) => (
|
padding: '0 1rem',
|
||||||
<button
|
fontSize: '0.78rem',
|
||||||
key={key}
|
color: '#7A7A7A',
|
||||||
onClick={() => { setMode(key); setError('') }}
|
letterSpacing: '0.1em',
|
||||||
style={{
|
textTransform: 'uppercase',
|
||||||
flex: 1,
|
}}>
|
||||||
padding: '0.75rem',
|
oppure
|
||||||
background: 'none',
|
</span>
|
||||||
border: 'none',
|
|
||||||
borderBottom: mode === key ? `2px solid ${ACCENT}` : '2px solid transparent',
|
|
||||||
marginBottom: '-2px',
|
|
||||||
cursor: 'pointer',
|
|
||||||
fontFamily: "'DM Sans', sans-serif",
|
|
||||||
fontWeight: 600,
|
|
||||||
fontSize: '0.9rem',
|
|
||||||
color: mode === key ? ACCENT : INK_MUTED,
|
|
||||||
transition: 'color 0.2s, border-color 0.2s',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{label}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* ── Form ── */}
|
|
||||||
<form onSubmit={handleSubmit} style={{ marginBottom: '1.5rem' }}>
|
|
||||||
{error && (
|
|
||||||
<div style={{
|
|
||||||
marginBottom: '1.25rem',
|
|
||||||
padding: '0.75rem 1rem',
|
|
||||||
backgroundColor: '#FFF5F5',
|
|
||||||
border: '1px solid #FED7D7',
|
|
||||||
color: '#C53030',
|
|
||||||
fontSize: '0.875rem',
|
|
||||||
}}>
|
|
||||||
{error}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{mode === 'register' && (
|
|
||||||
<div style={{ marginBottom: '1rem' }}>
|
|
||||||
<label style={labelStyle}>Nome visualizzato</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={displayName}
|
|
||||||
onChange={(e) => setDisplayName(e.target.value)}
|
|
||||||
placeholder="Il tuo nome o nickname"
|
|
||||||
style={inputStyle}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div style={{ marginBottom: '1rem' }}>
|
|
||||||
<label style={labelStyle}>Email</label>
|
|
||||||
<input
|
|
||||||
type="email"
|
|
||||||
value={email}
|
|
||||||
onChange={(e) => setEmail(e.target.value)}
|
|
||||||
placeholder="tu@esempio.it"
|
|
||||||
required
|
|
||||||
style={inputStyle}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style={{ marginBottom: '1.5rem' }}>
|
|
||||||
<label style={labelStyle}>Password</label>
|
|
||||||
<input
|
|
||||||
type="password"
|
|
||||||
value={password}
|
|
||||||
onChange={(e) => setPassword(e.target.value)}
|
|
||||||
placeholder="••••••••"
|
|
||||||
required
|
|
||||||
style={inputStyle}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
disabled={loading}
|
|
||||||
style={{
|
|
||||||
width: '100%',
|
|
||||||
padding: '0.8rem',
|
|
||||||
backgroundColor: loading ? '#888' : INK,
|
|
||||||
color: 'white',
|
|
||||||
border: 'none',
|
|
||||||
borderRadius: 0,
|
|
||||||
fontFamily: "'DM Sans', sans-serif",
|
|
||||||
fontWeight: 600,
|
|
||||||
fontSize: '0.9rem',
|
|
||||||
cursor: loading ? 'not-allowed' : 'pointer',
|
|
||||||
transition: 'background-color 0.2s, transform 0.15s',
|
|
||||||
}}
|
|
||||||
onMouseEnter={(e) => { if (!loading) { e.target.style.backgroundColor = ACCENT; e.target.style.transform = 'translateY(-1px)' } }}
|
|
||||||
onMouseLeave={(e) => { if (!loading) { e.target.style.backgroundColor = INK; e.target.style.transform = 'translateY(0)' } }}
|
|
||||||
>
|
|
||||||
{loading ? 'Caricamento...' : mode === 'login' ? 'Accedi' : 'Crea account'}
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
{/* ── Divider ── */}
|
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: '1rem', marginBottom: '1.25rem' }}>
|
|
||||||
<div style={{ flex: 1, height: 1, backgroundColor: BORDER }} />
|
|
||||||
<span style={{ color: INK_MUTED, fontSize: '0.8rem' }}>oppure</span>
|
|
||||||
<div style={{ flex: 1, height: 1, backgroundColor: BORDER }} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* ── Google ── */}
|
|
||||||
<button
|
|
||||||
onClick={handleGoogleLogin}
|
|
||||||
style={{
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
gap: '0.75rem',
|
|
||||||
width: '100%',
|
|
||||||
padding: '0.75rem',
|
|
||||||
backgroundColor: 'white',
|
|
||||||
border: `1px solid ${BORDER}`,
|
|
||||||
borderRadius: 0,
|
|
||||||
cursor: 'pointer',
|
|
||||||
fontFamily: "'DM Sans', sans-serif",
|
|
||||||
fontWeight: 500,
|
|
||||||
fontSize: '0.875rem',
|
|
||||||
color: INK,
|
|
||||||
marginBottom: '0.75rem',
|
|
||||||
transition: 'border-color 0.2s',
|
|
||||||
}}
|
|
||||||
onMouseEnter={(e) => e.currentTarget.style.borderColor = INK}
|
|
||||||
onMouseLeave={(e) => e.currentTarget.style.borderColor = BORDER}
|
|
||||||
>
|
|
||||||
<GoogleIcon />
|
|
||||||
Continua con Google
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{/* ── Coming soon ── */}
|
|
||||||
<div style={{ display: 'flex', gap: '0.5rem', justifyContent: 'center', marginBottom: '0.5rem' }}>
|
|
||||||
{[
|
|
||||||
{ name: 'Facebook', icon: '📘' },
|
|
||||||
{ name: 'Microsoft', icon: '🪟' },
|
|
||||||
{ name: 'Apple', icon: '🍎' },
|
|
||||||
].map(({ name, icon }) => (
|
|
||||||
<button
|
|
||||||
key={name}
|
|
||||||
title={`${name} — Disponibile a breve`}
|
|
||||||
style={{
|
|
||||||
padding: '0.5rem 0.85rem',
|
|
||||||
backgroundColor: '#F5F5F5',
|
|
||||||
border: `1px solid ${BORDER}`,
|
|
||||||
borderRadius: 0,
|
|
||||||
cursor: 'not-allowed',
|
|
||||||
fontSize: '1rem',
|
|
||||||
opacity: 0.45,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{icon}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<p style={{ textAlign: 'center', fontSize: '0.73rem', color: INK_MUTED, marginBottom: '1.5rem' }}>
|
|
||||||
Altri provider disponibili a breve
|
|
||||||
</p>
|
|
||||||
|
|
||||||
{/* ── Redeem link ── */}
|
|
||||||
<p style={{ textAlign: 'center', fontSize: '0.85rem', color: INK_MUTED }}>
|
|
||||||
Hai un codice Pro?{' '}
|
|
||||||
<button
|
|
||||||
onClick={() => setShowRedeemModal(true)}
|
|
||||||
style={{
|
|
||||||
background: 'none',
|
|
||||||
border: 'none',
|
|
||||||
color: ACCENT,
|
|
||||||
cursor: 'pointer',
|
|
||||||
fontWeight: 600,
|
|
||||||
fontSize: '0.85rem',
|
|
||||||
padding: 0,
|
|
||||||
textDecoration: 'underline',
|
|
||||||
textUnderlineOffset: '3px',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Riscattalo →
|
|
||||||
</button>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{showRedeemModal && <RedeemModal onClose={() => setShowRedeemModal(false)} />}
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Redeem modal ───────────────────────────────────────────────────────────────
|
function Field({ label, children }) {
|
||||||
|
return (
|
||||||
function RedeemModal({ onClose }) {
|
<div>
|
||||||
const [code, setCode] = useState('')
|
<label style={{
|
||||||
const [loading, setLoading] = useState(false)
|
display: 'block',
|
||||||
const [message, setMessage] = useState('')
|
fontSize: '0.875rem',
|
||||||
const [error, setError] = useState('')
|
fontWeight: 500,
|
||||||
|
color: '#1A1A1A',
|
||||||
const handleRedeem = async (e) => {
|
marginBottom: '0.5rem',
|
||||||
e.preventDefault()
|
}}>
|
||||||
setLoading(true)
|
{label}
|
||||||
setError('')
|
</label>
|
||||||
setMessage('')
|
{children}
|
||||||
try {
|
</div>
|
||||||
const { api } = await import('../api')
|
)
|
||||||
const result = await api.post('/auth/redeem', { code })
|
}
|
||||||
setMessage(`Codice riscattato! Piano Pro attivo fino al ${new Date(result.subscription_expires_at).toLocaleDateString('it-IT')}.`)
|
|
||||||
} catch (err) {
|
|
||||||
setError(err.message || 'Codice non valido o già utilizzato.')
|
|
||||||
} finally {
|
|
||||||
setLoading(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
function ErrorBox({ message }) {
|
||||||
return (
|
return (
|
||||||
<div style={{
|
<div style={{
|
||||||
position: 'fixed', inset: 0,
|
padding: '1rem',
|
||||||
backgroundColor: 'rgba(0,0,0,0.45)',
|
backgroundColor: '#FFF5F5',
|
||||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
borderLeft: '4px solid #E85A4F',
|
||||||
zIndex: 1000,
|
|
||||||
}}>
|
}}>
|
||||||
<div style={{
|
<p style={{ color: '#C53030', fontSize: '0.875rem', margin: 0 }}>{message}</p>
|
||||||
position: 'relative',
|
|
||||||
backgroundColor: 'white',
|
|
||||||
borderTop: `4px solid ${ACCENT}`,
|
|
||||||
padding: '2rem',
|
|
||||||
width: '380px',
|
|
||||||
maxWidth: '90vw',
|
|
||||||
boxShadow: '0 20px 60px rgba(0,0,0,0.2)',
|
|
||||||
}}>
|
|
||||||
<h3 style={{ margin: '0 0 0.4rem', fontSize: '1.1rem', color: INK, fontFamily: "'Fraunces', serif" }}>
|
|
||||||
Riscatta Codice Pro
|
|
||||||
</h3>
|
|
||||||
<p style={{ margin: '0 0 1.25rem', fontSize: '0.85rem', color: INK_MUTED }}>
|
|
||||||
Inserisci il tuo codice di attivazione (es. LP-XXXXXXXX)
|
|
||||||
</p>
|
|
||||||
|
|
||||||
{message ? (
|
|
||||||
<div style={{ padding: '0.75rem', backgroundColor: '#F0FDF4', border: '1px solid #86EFAC', color: '#16A34A', fontSize: '0.875rem', marginBottom: '1rem' }}>
|
|
||||||
{message}
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<form onSubmit={handleRedeem}>
|
|
||||||
{error && (
|
|
||||||
<div style={{ padding: '0.75rem', backgroundColor: '#FFF5F5', border: '1px solid #FED7D7', color: '#C53030', fontSize: '0.875rem', marginBottom: '0.75rem' }}>
|
|
||||||
{error}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={code}
|
|
||||||
onChange={(e) => setCode(e.target.value.toUpperCase())}
|
|
||||||
placeholder="LP-XXXXXXXXXXXXXXXX"
|
|
||||||
required
|
|
||||||
style={{ ...inputStyle, marginBottom: '0.75rem', fontFamily: 'monospace', letterSpacing: '0.05em' }}
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
disabled={loading}
|
|
||||||
style={{
|
|
||||||
width: '100%',
|
|
||||||
padding: '0.7rem',
|
|
||||||
backgroundColor: loading ? '#888' : INK,
|
|
||||||
color: 'white',
|
|
||||||
border: 'none',
|
|
||||||
borderRadius: 0,
|
|
||||||
fontFamily: "'DM Sans', sans-serif",
|
|
||||||
fontWeight: 600,
|
|
||||||
cursor: loading ? 'not-allowed' : 'pointer',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{loading ? 'Verifica...' : 'Riscatta'}
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<button
|
|
||||||
onClick={onClose}
|
|
||||||
style={{
|
|
||||||
marginTop: '1rem',
|
|
||||||
width: '100%',
|
|
||||||
padding: '0.6rem',
|
|
||||||
backgroundColor: 'transparent',
|
|
||||||
border: `1px solid ${BORDER}`,
|
|
||||||
borderRadius: 0,
|
|
||||||
cursor: 'pointer',
|
|
||||||
color: INK_MUTED,
|
|
||||||
fontFamily: "'DM Sans', sans-serif",
|
|
||||||
fontSize: '0.875rem',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Chiudi
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Google Icon SVG ────────────────────────────────────────────────────────────
|
function SubmitButton({ loading, children }) {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={loading}
|
||||||
|
style={{
|
||||||
|
width: '100%',
|
||||||
|
height: 48,
|
||||||
|
backgroundColor: loading ? '#888' : '#1A1A1A',
|
||||||
|
color: 'white',
|
||||||
|
border: 'none',
|
||||||
|
borderRadius: 0,
|
||||||
|
fontFamily: "'DM Sans', sans-serif",
|
||||||
|
fontWeight: 600,
|
||||||
|
fontSize: '1rem',
|
||||||
|
cursor: loading ? 'not-allowed' : 'pointer',
|
||||||
|
transition: 'background-color 0.2s, transform 0.15s',
|
||||||
|
}}
|
||||||
|
onMouseEnter={(e) => { if (!loading) { e.target.style.backgroundColor = '#E85A4F'; e.target.style.transform = 'translateY(-1px)' } }}
|
||||||
|
onMouseLeave={(e) => { if (!loading) { e.target.style.backgroundColor = '#1A1A1A'; e.target.style.transform = 'translateY(0)' } }}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
function GoogleIcon() {
|
function GoogleIcon() {
|
||||||
return (
|
return (
|
||||||
<svg width="18" height="18" viewBox="0 0 48 48">
|
<svg width="20" height="20" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||||
<path fill="#FFC107" d="M43.6 20H24v8h11.3C33.6 33.1 29.3 36 24 36c-6.6 0-12-5.4-12-12s5.4-12 12-12c3.1 0 5.8 1.2 8 3l5.7-5.7C34.2 6.6 29.3 4.5 24 4.5 12.7 4.5 3.5 13.7 3.5 25S12.7 45.5 24 45.5c10.5 0 19.5-7.6 19.5-21 0-1.2-.1-2.4-.4-3.5z" />
|
<path d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z" fill="#4285F4" />
|
||||||
<path fill="#FF3D00" d="M6.3 14.7l6.6 4.8C14.6 15.1 19 12 24 12c3.1 0 5.8 1.2 8 3l5.7-5.7C34.2 6.6 29.3 4.5 24 4.5c-7.7 0-14.4 4.4-17.7 10.2z" />
|
<path d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z" fill="#34A853" />
|
||||||
<path fill="#4CAF50" d="M24 45.5c5.2 0 9.9-1.9 13.5-5l-6.2-5.2C29.3 37 26.8 38 24 38c-5.3 0-9.7-3-11.3-7.4l-6.6 5.1C9.6 41.1 16.3 45.5 24 45.5z" />
|
<path d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z" fill="#FBBC05" />
|
||||||
<path fill="#1976D2" d="M43.6 20H24v8h11.3c-.7 2.1-2 3.9-3.7 5.2l6.2 5.2c3.7-3.4 5.7-8.4 5.7-13.4 0-1.2-.1-2.4-.4-3.5z" />
|
<path d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z" fill="#EA4335" />
|
||||||
</svg>
|
</svg>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Shared inline styles ───────────────────────────────────────────────────────
|
// ── Shared style ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
const labelStyle = {
|
|
||||||
display: 'block',
|
|
||||||
fontSize: '0.78rem',
|
|
||||||
fontWeight: 600,
|
|
||||||
color: INK,
|
|
||||||
marginBottom: '0.4rem',
|
|
||||||
letterSpacing: '0.03em',
|
|
||||||
textTransform: 'uppercase',
|
|
||||||
}
|
|
||||||
|
|
||||||
const inputStyle = {
|
const inputStyle = {
|
||||||
|
display: 'flex',
|
||||||
width: '100%',
|
width: '100%',
|
||||||
padding: '0.65rem 0.875rem',
|
height: 44,
|
||||||
border: `1px solid ${BORDER}`,
|
border: '1px solid #E5E0D8',
|
||||||
borderRadius: 0,
|
borderRadius: 0,
|
||||||
fontSize: '0.875rem',
|
backgroundColor: 'white',
|
||||||
|
padding: '0 1rem',
|
||||||
|
fontSize: '1rem',
|
||||||
|
color: '#1A1A1A',
|
||||||
outline: 'none',
|
outline: 'none',
|
||||||
boxSizing: 'border-box',
|
boxSizing: 'border-box',
|
||||||
color: INK,
|
|
||||||
backgroundColor: '#FFFFFF',
|
|
||||||
fontFamily: "'DM Sans', sans-serif",
|
fontFamily: "'DM Sans', sans-serif",
|
||||||
transition: 'border-color 0.15s',
|
transition: 'border-color 0.15s',
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user