redesign: apply correct Editorial Fresh design system

- Fix palette: #E85A4F accent, #FFFBF5 cream, #1A1A1A ink (was wrong values)
- Remove all border-radius (zero radius everywhere per design spec)
- Sidebar: cream-dark #F5F0E8 bg with accent-left active indicator
- card-editorial: white bg, 4px accent top bar via absolute div
- Buttons: ink bg → accent hover + translateY(-1px)
- LoginPage: correct split layout with Editorial Fresh tokens
- Add .btn-primary / .btn-outline / .input-editorial / .editorial-tag classes
- Fraunces + DM Sans correctly applied everywhere

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Michele
2026-03-31 20:38:29 +02:00
parent b837f961e4
commit 3139468c92
8 changed files with 981 additions and 589 deletions

View File

@@ -7,13 +7,8 @@ import PlanBanner from './PlanBanner'
export default function Dashboard() { export default function Dashboard() {
const { user, isAdmin } = useAuth() const { user, isAdmin } = useAuth()
const [stats, setStats] = useState({ const [stats, setStats] = useState({
characters: 0, characters: 0, active: 0, posts: 0, scheduled: 0,
active: 0, pendingComments: 0, affiliates: 0, plans: 0,
posts: 0,
scheduled: 0,
pendingComments: 0,
affiliates: 0,
plans: 0,
}) })
const [recentPosts, setRecentPosts] = useState([]) const [recentPosts, setRecentPosts] = useState([])
const [providerStatus, setProviderStatus] = useState(null) const [providerStatus, setProviderStatus] = useState(null)
@@ -44,49 +39,72 @@ export default function Dashboard() {
}) })
}, []) }, [])
const hour = new Date().getHours()
const greeting = hour < 12 ? 'Buongiorno' : hour < 18 ? 'Buon pomeriggio' : 'Buonasera'
return ( return (
<div> <div style={{ animation: 'fade-up 0.6s ease-out both' }}>
<div className="flex items-center justify-between mb-1"> {/* ── Header ─────────────────────────────────────────────── */}
<h2 className="text-2xl font-bold" style={{ color: 'var(--ink)' }}> <div style={{ display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between', marginBottom: '1.75rem' }}>
Dashboard <div>
</h2> <span className="editorial-tag">Dashboard</span>
<div className="editorial-line" />
<h2 style={{
fontFamily: "'Fraunces', serif",
fontSize: '2rem',
fontWeight: 600,
letterSpacing: '-0.02em',
color: 'var(--ink)',
margin: '0.4rem 0 0.25rem',
}}>
{greeting}{user?.display_name ? `, ${user.display_name}` : ''}
</h2>
<p style={{ fontSize: '0.875rem', color: 'var(--ink-muted)', margin: 0 }}>
Panoramica del tuo studio editoriale AI
</p>
</div>
{isAdmin && ( {isAdmin && (
<Link <Link
to="/admin" to="/admin"
className="px-3 py-1 text-xs font-semibold rounded-lg" style={{
style={{ backgroundColor: '#FEF3C7', color: '#D97706', border: '1px solid #FDE68A' }} padding: '0.4rem 0.85rem',
fontSize: '0.75rem',
fontWeight: 600,
backgroundColor: '#FEF3C7',
color: '#D97706',
border: '1px solid #FDE68A',
textDecoration: 'none',
letterSpacing: '0.03em',
}}
> >
Admin Settings Admin
</Link> </Link>
)} )}
</div> </div>
<p className="text-sm mb-3" style={{ color: 'var(--muted)' }}>
{user?.display_name ? `Ciao, ${user.display_name}` : ''}Panoramica Leopost Full
</p>
<PlanBanner /> <PlanBanner />
{/* Stats grid */} {/* ── Stats grid ──────────────────────────────────────────── */}
<div className="grid grid-cols-2 lg:grid-cols-4 gap-3 mt-5"> <div style={{
<StatCard label="Personaggi" value={loading ? '—' : stats.characters} sub={`${stats.active} attivi`} accentColor="var(--coral)" /> display: 'grid',
<StatCard label="Post generati" value={loading ? '—' : stats.posts} accentColor="#3B82F6" /> gridTemplateColumns: 'repeat(auto-fill, minmax(180px, 1fr))',
<StatCard label="Schedulati" value={loading ? '—' : stats.scheduled} sub="in coda" accentColor="#10B981" /> gap: '1rem',
<StatCard label="Commenti" value={loading ? '—' : stats.pendingComments} sub="in attesa" accentColor="#8B5CF6" /> marginBottom: '2rem',
<StatCard label="Link Affiliati" value={loading ? '—' : stats.affiliates} accentColor="#F59E0B" /> }}>
<StatCard label="Piani Attivi" value={loading ? '—' : stats.plans} accentColor="#14B8A6" /> <StatCard label="Personaggi" value={loading ? '—' : stats.characters} sub={`${stats.active} attivi`} accent="#E85A4F" delay={0} />
<StatCard label="Post generati" value={loading ? '—' : stats.posts} accent="#3B82F6" delay={0.1} />
<StatCard label="Schedulati" value={loading ? '—' : stats.scheduled} sub="in coda" accent="#10B981" delay={0.2} />
<StatCard label="Commenti" value={loading ? '—' : stats.pendingComments} sub="in attesa" accent="#8B5CF6" delay={0.15} />
<StatCard label="Link Affiliati" value={loading ? '—' : stats.affiliates} accent="#F59E0B" delay={0.25} />
<StatCard label="Piani Attivi" value={loading ? '—' : stats.plans} accent="#14B8A6" delay={0.3} />
</div> </div>
{/* Provider status */} {/* ── Provider status ─────────────────────────────────────── */}
{providerStatus && ( {providerStatus && (
<div <div className="card-editorial" style={{ marginBottom: '2rem' }}>
className="mt-6 rounded-xl p-5" <span className="editorial-tag" style={{ marginBottom: '0.75rem', display: 'block' }}>Stato Provider</span>
style={{ backgroundColor: 'var(--surface)', border: '1px solid var(--border)' }} <div style={{ display: 'flex', flexWrap: 'wrap', gap: '0.5rem' }}>
> <ProviderBadge name="LLM" ok={providerStatus.llm?.configured} detail={providerStatus.llm?.provider} />
<h3 className="text-sm font-semibold uppercase tracking-wider mb-3" style={{ color: 'var(--muted)' }}>
Stato Provider
</h3>
<div className="flex flex-wrap gap-3">
<ProviderBadge name="LLM" ok={providerStatus.llm?.configured} detail={providerStatus.llm?.provider} />
<ProviderBadge name="Immagini" ok={providerStatus.image?.configured} detail={providerStatus.image?.provider} /> <ProviderBadge name="Immagini" ok={providerStatus.image?.configured} detail={providerStatus.image?.provider} />
<ProviderBadge name="Voiceover" ok={providerStatus.voice?.configured} /> <ProviderBadge name="Voiceover" ok={providerStatus.voice?.configured} />
{providerStatus.social && Object.entries(providerStatus.social).map(([k, v]) => ( {providerStatus.social && Object.entries(providerStatus.social).map(([k, v]) => (
@@ -94,9 +112,9 @@ export default function Dashboard() {
))} ))}
</div> </div>
{!providerStatus.llm?.configured && ( {!providerStatus.llm?.configured && (
<p className="text-xs mt-2" style={{ color: 'var(--muted)' }}> <p style={{ fontSize: '0.78rem', color: 'var(--ink-muted)', marginTop: '0.75rem' }}>
Configura le API key in{' '} Configura le API key in{' '}
<Link to="/settings" style={{ color: 'var(--coral)' }} className="hover:underline"> <Link to="/settings" style={{ color: 'var(--accent)', textDecoration: 'underline' }}>
Impostazioni Impostazioni
</Link> </Link>
</p> </p>
@@ -104,67 +122,43 @@ export default function Dashboard() {
</div> </div>
)} )}
{/* Quick actions */} {/* ── Quick actions ───────────────────────────────────────── */}
<div className="mt-6"> <div style={{ marginBottom: '2rem' }}>
<h3 className="text-sm font-semibold uppercase tracking-wider mb-3" style={{ color: 'var(--muted)' }}> <span className="editorial-tag" style={{ marginBottom: '0.75rem', display: 'block' }}>Azioni rapide</span>
Azioni rapide <div style={{ display: 'flex', flexWrap: 'wrap', gap: '0.5rem' }}>
</h3> <Link to="/content" className="btn-primary">Genera contenuto</Link>
<div className="flex flex-wrap gap-2"> <Link to="/editorial" className="btn-outline">Calendario AI</Link>
<Link <Link to="/characters/new" className="btn-outline">Nuovo personaggio</Link>
to="/content" <Link to="/plans/new" className="btn-outline">Nuovo piano</Link>
className="px-4 py-2 text-white text-sm font-medium rounded-lg transition-opacity hover:opacity-90"
style={{ backgroundColor: 'var(--coral)' }}
>
Genera contenuto
</Link>
<Link
to="/editorial"
className="px-4 py-2 text-sm font-medium rounded-lg transition-colors"
style={{ backgroundColor: 'var(--surface)', border: '1px solid var(--border)', color: 'var(--ink)' }}
>
Calendario AI
</Link>
<Link
to="/characters/new"
className="px-4 py-2 text-sm font-medium rounded-lg transition-colors"
style={{ backgroundColor: 'var(--surface)', border: '1px solid var(--border)', color: 'var(--ink)' }}
>
Nuovo personaggio
</Link>
<Link
to="/plans/new"
className="px-4 py-2 text-sm font-medium rounded-lg transition-colors"
style={{ backgroundColor: 'var(--surface)', border: '1px solid var(--border)', color: 'var(--ink)' }}
>
Nuovo piano
</Link>
</div> </div>
</div> </div>
{/* Recent posts */} {/* ── Recent posts ────────────────────────────────────────── */}
{recentPosts.length > 0 && ( {recentPosts.length > 0 && (
<div className="mt-6"> <div>
<div className="flex items-center justify-between mb-3"> <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: '0.75rem' }}>
<h3 className="text-sm font-semibold uppercase tracking-wider" style={{ color: 'var(--muted)' }}> <span className="editorial-tag">Post recenti</span>
Post recenti <Link to="/content/archive" style={{ fontSize: '0.78rem', color: 'var(--accent)', textDecoration: 'underline', textUnderlineOffset: '3px' }}>
</h3>
<Link to="/content/archive" style={{ color: 'var(--coral)' }} className="text-xs hover:underline">
Vedi tutti Vedi tutti
</Link> </Link>
</div> </div>
<div className="space-y-2"> <div style={{ display: 'flex', flexDirection: 'column', gap: '0.5rem' }}>
{recentPosts.map((p) => ( {recentPosts.map((p) => (
<div <div
key={p.id} key={p.id}
className="flex items-center gap-3 p-3 rounded-lg" style={{
style={{ backgroundColor: 'var(--surface)', border: '1px solid var(--border)' }} display: 'flex',
alignItems: 'center',
gap: '0.75rem',
padding: '0.75rem 1rem',
backgroundColor: 'var(--surface)',
border: '1px solid var(--border)',
}}
> >
<span className={`text-[10px] px-2 py-0.5 rounded-full font-medium ${statusColor(p.status)}`}> <StatusBadge status={p.status} />
{p.status} <span style={{ fontSize: '0.75rem', color: 'var(--ink-muted)', flexShrink: 0 }}>{p.platform_hint}</span>
</span> <p style={{ fontSize: '0.85rem', color: 'var(--ink)', margin: 0, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', flex: 1 }}>
<span className="text-xs" style={{ color: 'var(--muted)' }}>{p.platform_hint}</span> {p.text_content?.slice(0, 80)}
<p className="text-sm truncate flex-1" style={{ color: 'var(--ink)' }}>
{p.text_content?.slice(0, 80)}...
</p> </p>
</div> </div>
))} ))}
@@ -175,43 +169,80 @@ export default function Dashboard() {
) )
} }
function StatCard({ label, value, sub, accentColor }) { // ── Sub-components ─────────────────────────────────────────────────────────────
function StatCard({ label, value, sub, accent, delay = 0 }) {
return ( return (
<div <div
className="rounded-xl p-4" className="card-editorial animate-fade-up"
style={{ backgroundColor: 'var(--surface)', border: '1px solid var(--border)' }} style={{ animationDelay: `${delay}s` }}
> >
<div className="flex items-center gap-2 mb-1.5"> <div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', marginBottom: '0.75rem' }}>
<div className="w-2 h-2 rounded-full" style={{ backgroundColor: accentColor }} /> <div style={{ width: 8, height: 8, borderRadius: '50%', backgroundColor: accent, flexShrink: 0 }} />
<span className="text-[11px] font-medium uppercase tracking-wider" style={{ color: 'var(--muted)' }}> <span style={{
fontSize: '0.68rem',
fontWeight: 600,
letterSpacing: '0.1em',
textTransform: 'uppercase',
color: 'var(--ink-muted)',
}}>
{label} {label}
</span> </span>
</div> </div>
<p className="text-2xl font-bold" style={{ color: 'var(--ink)' }}>{value}</p> <p style={{ fontSize: '2rem', fontWeight: 700, color: 'var(--ink)', margin: 0, fontFamily: "'Fraunces', serif" }}>
{sub && <p className="text-[11px] mt-0.5" style={{ color: 'var(--muted)' }}>{sub}</p>} {value}
</p>
{sub && <p style={{ fontSize: '0.72rem', color: 'var(--ink-muted)', marginTop: '0.25rem' }}>{sub}</p>}
</div> </div>
) )
} }
function ProviderBadge({ name, ok, detail }) { function ProviderBadge({ name, ok, detail }) {
return ( return (
<div className={`flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-medium ${ <div style={{
ok ? 'bg-emerald-50 text-emerald-700' : 'bg-slate-100 text-slate-400' display: 'flex',
}`}> alignItems: 'center',
<div className={`w-1.5 h-1.5 rounded-full ${ok ? 'bg-emerald-500' : 'bg-slate-300'}`} /> gap: '0.4rem',
padding: '0.35rem 0.75rem',
fontSize: '0.78rem',
fontWeight: 500,
backgroundColor: ok ? 'var(--success-light, #F0F9F4)' : 'var(--cream-dark)',
color: ok ? 'var(--success)' : 'var(--ink-muted)',
border: `1px solid ${ok ? '#A7F3D0' : 'var(--border)'}`,
}}>
<div style={{
width: 6,
height: 6,
borderRadius: '50%',
backgroundColor: ok ? 'var(--success)' : 'var(--border-strong)',
}} />
{name} {name}
{detail && <span className="text-[10px] opacity-60">({detail})</span>} {detail && <span style={{ fontSize: '0.68rem', opacity: 0.6 }}>({detail})</span>}
</div> </div>
) )
} }
function statusColor(s) { function StatusBadge({ status }) {
const map = { const map = {
draft: 'bg-slate-100 text-slate-500', draft: { bg: 'var(--cream-dark)', color: 'var(--ink-muted)' },
approved: 'bg-blue-50 text-blue-600', approved: { bg: '#EFF6FF', color: '#2563EB' },
scheduled: 'bg-amber-50 text-amber-600', scheduled: { bg: '#FFFBEB', color: '#D97706' },
published: 'bg-emerald-50 text-emerald-600', published: { bg: 'var(--success-light, #F0F9F4)', color: 'var(--success)' },
failed: 'bg-red-50 text-red-600', failed: { bg: '#FFF5F5', color: '#C53030' },
} }
return map[s] || 'bg-slate-100 text-slate-500' const s = map[status] || map.draft
return (
<span style={{
fontSize: '0.65rem',
fontWeight: 600,
letterSpacing: '0.06em',
textTransform: 'uppercase',
padding: '0.2rem 0.5rem',
backgroundColor: s.bg,
color: s.color,
flexShrink: 0,
}}>
{status}
</span>
)
} }

View File

@@ -2,66 +2,150 @@ import { NavLink, Outlet } from 'react-router-dom'
import { useAuth } from '../AuthContext' import { useAuth } from '../AuthContext'
const nav = [ const nav = [
{ to: '/', label: 'Dashboard', icon: '◉' }, { to: '/', label: 'Dashboard', icon: '◉' },
{ to: '/characters', label: 'Personaggi', icon: '◎' }, { to: '/characters',label: 'Personaggi', icon: '◎' },
{ to: '/content', label: 'Contenuti', icon: '✦' }, { to: '/content', label: 'Contenuti', icon: '✦' },
{ to: '/affiliates', label: 'Link Affiliati', icon: '⟁' }, { to: '/affiliates',label: 'Link Affiliati', icon: '⟁' },
{ to: '/plans', label: 'Piano Editoriale', icon: '▦' }, { to: '/plans', label: 'Piano Editoriale', icon: '▦' },
{ to: '/schedule', label: 'Schedulazione', icon: '◈' }, { to: '/schedule', label: 'Schedulazione', icon: '◈' },
{ to: '/social', label: 'Social', icon: '◇' }, { to: '/social', label: 'Social', icon: '◇' },
{ to: '/comments', label: 'Commenti', icon: '◌' }, { to: '/comments', label: 'Commenti', icon: '◌' },
{ to: '/editorial', label: 'Calendario AI', icon: '◰' }, { to: '/editorial', label: 'Calendario AI', icon: '◰' },
{ to: '/settings', label: 'Impostazioni', icon: '⚙' }, { to: '/settings', label: 'Impostazioni', icon: '⚙' },
] ]
export default function Layout() { export default function Layout() {
const { user, logout } = useAuth() const { user, logout, isPro, isAdmin } = useAuth()
return ( return (
<div className="min-h-screen flex" style={{ backgroundColor: 'var(--cream)' }}> <div style={{ minHeight: '100vh', display: 'flex', backgroundColor: 'var(--cream)' }}>
{/* Sidebar */} {/* ── Sidebar ─────────────────────────────────────────────── */}
<aside className="w-60 flex flex-col shrink-0" style={{ backgroundColor: 'var(--ink)' }}> <aside style={{
<div className="p-5 border-b" style={{ borderColor: 'rgba(255,255,255,0.08)' }}> width: 240,
<h1 className="text-lg font-bold tracking-tight text-white font-serif"> flexShrink: 0,
Leopost <span style={{ color: 'var(--coral)' }}>Full</span> display: 'flex',
flexDirection: 'column',
backgroundColor: 'var(--cream-dark)',
borderRight: '1px solid var(--border)',
}}>
{/* Logo */}
<div style={{
padding: '1.5rem 1.25rem 1.25rem',
borderBottom: '1px solid var(--border)',
}}>
<h1 style={{
fontFamily: "'Fraunces', serif",
fontWeight: 700,
fontSize: '1.4rem',
letterSpacing: '-0.02em',
color: 'var(--ink)',
margin: 0,
}}>
Leopost
</h1> </h1>
<p className="text-[10px] mt-0.5" style={{ color: 'var(--muted)' }}> <div style={{ width: 40, height: 3, backgroundColor: 'var(--accent)', marginTop: '0.4rem' }} />
Content Automation <p style={{ fontSize: '0.7rem', color: 'var(--ink-muted)', marginTop: '0.4rem', fontWeight: 500, letterSpacing: '0.08em', textTransform: 'uppercase' }}>
Content Studio
</p> </p>
</div> </div>
<nav className="flex-1 p-3 space-y-0.5 overflow-y-auto"> {/* Nav */}
<nav style={{ flex: 1, padding: '0.75rem 0.5rem', overflowY: 'auto' }}>
{nav.map(({ to, label, icon }) => ( {nav.map(({ to, label, icon }) => (
<NavLink <NavLink
key={to} key={to}
to={to} to={to}
end={to === '/'} end={to === '/'}
className={({ isActive }) => style={({ isActive }) => ({
`flex items-center gap-2.5 px-3 py-2 rounded text-[13px] font-medium transition-colors ${ display: 'flex',
isActive alignItems: 'center',
? 'text-white' gap: '0.625rem',
: 'text-slate-400 hover:text-white' padding: '0.625rem 0.875rem',
}` fontSize: '0.84rem',
} fontWeight: isActive ? 600 : 400,
style={({ isActive }) => color: isActive ? 'var(--accent)' : 'var(--ink-light)',
isActive ? { backgroundColor: 'var(--coral)' } : {} borderLeft: isActive ? '3px solid var(--accent)' : '3px solid transparent',
} backgroundColor: isActive ? 'var(--accent-light)' : 'transparent',
textDecoration: 'none',
transition: 'background-color 0.15s, color 0.15s',
marginBottom: '0.1rem',
})}
onMouseEnter={(e) => {
if (!e.currentTarget.getAttribute('aria-current')) {
e.currentTarget.style.backgroundColor = 'var(--border)'
e.currentTarget.style.color = 'var(--ink)'
}
}}
onMouseLeave={(e) => {
if (!e.currentTarget.getAttribute('aria-current')) {
e.currentTarget.style.backgroundColor = 'transparent'
e.currentTarget.style.color = 'var(--ink-light)'
}
}}
> >
<span className="text-base w-5 text-center">{icon}</span> <span style={{ fontSize: '0.95rem', width: 18, textAlign: 'center', flexShrink: 0 }}>{icon}</span>
{label} {label}
</NavLink> </NavLink>
))} ))}
</nav> </nav>
<div className="p-3 border-t" style={{ borderColor: 'rgba(255,255,255,0.08)' }}> {/* User footer */}
<div className="flex items-center justify-between px-2"> <div style={{
<span className="text-xs" style={{ color: 'var(--muted)' }}> padding: '1rem 1.25rem',
{user?.username} borderTop: '1px solid var(--border)',
</span> }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '0.625rem', marginBottom: '0.5rem' }}>
<div style={{
width: 32,
height: 32,
borderRadius: '50%',
backgroundColor: 'var(--accent)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
color: 'white',
fontWeight: 700,
fontSize: '0.85rem',
flexShrink: 0,
}}>
{(user?.display_name || user?.username || '?')[0].toUpperCase()}
</div>
<div style={{ overflow: 'hidden' }}>
<p style={{ fontSize: '0.8rem', fontWeight: 600, color: 'var(--ink)', margin: 0, truncate: true, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
{user?.display_name || user?.username}
</p>
<span style={{
display: 'inline-block',
fontSize: '0.65rem',
fontWeight: 700,
letterSpacing: '0.08em',
textTransform: 'uppercase',
padding: '0.1rem 0.4rem',
backgroundColor: isPro ? 'var(--success)' : 'var(--border-strong)',
color: isPro ? 'white' : 'var(--ink-muted)',
}}>
{isPro ? 'PRO' : 'FREEMIUM'}
</span>
</div>
</div>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
{isAdmin && (
<NavLink to="/admin" style={{ fontSize: '0.72rem', color: '#D97706', fontWeight: 600, textDecoration: 'none' }}>
Admin
</NavLink>
)}
<button <button
onClick={logout} onClick={logout}
className="text-[11px] transition-colors hover:text-white" style={{
style={{ color: 'var(--muted)' }} marginLeft: 'auto',
background: 'none',
border: 'none',
fontSize: '0.75rem',
color: 'var(--ink-muted)',
cursor: 'pointer',
padding: 0,
fontFamily: "'DM Sans', sans-serif",
}}
> >
Logout Logout
</button> </button>
@@ -69,9 +153,9 @@ export default function Layout() {
</div> </div>
</aside> </aside>
{/* Main content */} {/* ── Main content ──────────────────────────────────────── */}
<main className="flex-1 overflow-auto"> <main style={{ flex: 1, overflowY: 'auto' }}>
<div className="max-w-6xl mx-auto p-6"> <div style={{ maxWidth: 1100, margin: '0 auto', padding: '2rem 2.5rem' }}>
<Outlet /> <Outlet />
</div> </div>
</main> </main>

View File

@@ -3,11 +3,12 @@ import { useNavigate } from 'react-router-dom'
import { useAuth } from '../AuthContext' import { useAuth } from '../AuthContext'
import { BASE_URL } from '../api' import { BASE_URL } from '../api'
const CORAL = '#FF6B4A' const ACCENT = '#E85A4F'
const CREAM = '#FAF8F3' const ACCENT_HOVER= '#D14940'
const INK = '#1A1A2E' const CREAM = '#FFFBF5'
const MUTED = '#888' const INK = '#1A1A1A'
const BORDER = '#E8E4DC' 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'
@@ -43,68 +44,105 @@ export default function LoginPage() {
window.location.href = `${BASE_URL}/auth/oauth/google` window.location.href = `${BASE_URL}/auth/oauth/google`
} }
const comingSoon = (e) => {
e.preventDefault()
// tooltip handled via title attr
}
return ( return (
<div style={{ display: 'flex', height: '100vh', fontFamily: 'Inter, sans-serif' }}> <div style={{
{/* LEFT SIDE */} display: 'flex',
height: '100vh',
fontFamily: "'DM Sans', sans-serif",
overflow: 'hidden',
}}>
{/* ── LEFT PANEL ─────────────────────────────────────────── */}
<div style={{ <div style={{
width: '40%', width: '45%',
backgroundColor: CORAL, backgroundColor: ACCENT,
padding: '3rem', padding: '3rem',
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
justifyContent: 'space-between', justifyContent: 'space-between',
color: 'white', color: 'white',
flexShrink: 0,
}}> }}>
<div> <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={{ <h1 style={{
fontFamily: 'Georgia, serif', fontFamily: "'Fraunces', serif",
fontSize: '2.8rem', fontSize: '2.6rem',
fontWeight: 700, fontWeight: 700,
margin: 0, margin: 0,
letterSpacing: '-1px', lineHeight: 1.15,
letterSpacing: '-0.02em',
}}> }}>
Leopost Il tuo studio<br />editoriale AI
</h1> </h1>
<p style={{ fontSize: '1.05rem', marginTop: '0.5rem', opacity: 0.9, lineHeight: 1.4 }}>
Il tuo studio editoriale AI per i social <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> </p>
<ul style={{ marginTop: '2.5rem', listStyle: 'none', padding: 0, lineHeight: 2 }}> {/* Benefit list */}
<li style={{ display: 'flex', alignItems: 'center', gap: '0.6rem' }}> <ul style={{ marginTop: '2.5rem', listStyle: 'none', padding: 0, display: 'flex', flexDirection: 'column', gap: '0.75rem' }}>
<span style={{ fontSize: '1.2rem' }}></span> Genera contenuti AI per ogni piattaforma {[
</li> 'Personaggi AI con voce e stile unico',
<li style={{ display: 'flex', alignItems: 'center', gap: '0.6rem' }}> 'Contenuti su Facebook, Instagram, YouTube, TikTok',
<span style={{ fontSize: '1.2rem' }}></span> Pubblica su Facebook, Instagram, YouTube, TikTok 'Schedulazione automatica con piani editoriali',
</li> ].map((txt) => (
<li style={{ display: 'flex', alignItems: 'center', gap: '0.6rem' }}> <li key={txt} style={{
<span style={{ fontSize: '1.2rem' }}></span> Schedula in automatico con piani editoriali display: 'flex',
</li> alignItems: 'flex-start',
<li style={{ display: 'flex', alignItems: 'center', gap: '0.6rem' }}> gap: '0.75rem',
<span style={{ fontSize: '1.2rem' }}></span> Gestisci commenti con risposte AI fontSize: '0.9rem',
</li> lineHeight: 1.5,
<li style={{ display: 'flex', alignItems: 'center', gap: '0.6rem' }}> padding: '0.6rem 0.9rem',
<span style={{ fontSize: '1.2rem' }}></span> Link affiliati integrati nei post backgroundColor: 'rgba(255,255,255,0.12)',
</li> backdropFilter: 'blur(4px)',
}}>
<span style={{ fontSize: '0.8rem', marginTop: '0.15rem', flexShrink: 0 }}>✦</span>
{txt}
</li>
))}
</ul> </ul>
</div> </div>
{/* Badge in basso */}
<div style={{ <div style={{
backgroundColor: 'rgba(255,255,255,0.15)', display: 'inline-flex',
borderRadius: '12px', alignItems: 'center',
padding: '1rem 1.5rem', gap: '0.5rem',
fontSize: '0.85rem', padding: '0.6rem 1rem',
backdropFilter: 'blur(4px)', 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',
}}> }}>
<strong>Early Adopter Beta</strong> Unisciti ora e ottieni un accesso esclusivo al piano Pro a prezzo speciale. <span style={{ opacity: 0.7 }}>★</span> EARLY ADOPTER BETA
</div> </div>
</div> </div>
{/* RIGHT SIDE */} {/* ── RIGHT PANEL ────────────────────────────────────────── */}
<div style={{ <div style={{
flex: 1, flex: 1,
backgroundColor: CREAM, backgroundColor: CREAM,
@@ -112,55 +150,52 @@ export default function LoginPage() {
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
padding: '2rem', padding: '2rem',
overflowY: 'auto',
}}> }}>
<div style={{ width: '100%', maxWidth: '420px' }}> <div style={{ width: '100%', maxWidth: '420px', animation: 'fade-up 0.6s ease-out 0.1s both' }}>
{/* Toggle */} {/* ── Mode toggle ── */}
<div style={{ <div style={{
display: 'flex', display: 'flex',
backgroundColor: 'white', borderBottom: `2px solid ${BORDER}`,
borderRadius: '10px', marginBottom: '2rem',
border: `1px solid ${BORDER}`,
padding: '4px',
marginBottom: '1.5rem',
}}> }}>
{['login', 'register'].map((m) => ( {[
{ key: 'login', label: 'Accedi' },
{ key: 'register', label: 'Registrati' },
].map(({ key, label }) => (
<button <button
key={m} key={key}
onClick={() => { setMode(m); setError('') }} onClick={() => { setMode(key); setError('') }}
style={{ style={{
flex: 1, flex: 1,
padding: '0.5rem', padding: '0.75rem',
background: 'none',
border: 'none', border: 'none',
borderRadius: '7px', borderBottom: mode === key ? `2px solid ${ACCENT}` : '2px solid transparent',
marginBottom: '-2px',
cursor: 'pointer', cursor: 'pointer',
fontFamily: "'DM Sans', sans-serif",
fontWeight: 600, fontWeight: 600,
fontSize: '0.875rem', fontSize: '0.9rem',
transition: 'all 0.2s', color: mode === key ? ACCENT : INK_MUTED,
backgroundColor: mode === m ? CORAL : 'transparent', transition: 'color 0.2s, border-color 0.2s',
color: mode === m ? 'white' : MUTED,
}} }}
> >
{m === 'login' ? 'Accedi' : 'Registrati'} {label}
</button> </button>
))} ))}
</div> </div>
<form onSubmit={handleSubmit} style={{ {/* ── Form ── */}
backgroundColor: 'white', <form onSubmit={handleSubmit} style={{ marginBottom: '1.5rem' }}>
borderRadius: '16px',
padding: '2rem',
border: `1px solid ${BORDER}`,
boxShadow: '0 4px 24px rgba(0,0,0,0.06)',
}}>
{error && ( {error && (
<div style={{ <div style={{
marginBottom: '1rem', marginBottom: '1.25rem',
padding: '0.75rem 1rem', padding: '0.75rem 1rem',
backgroundColor: '#FEE2E2', backgroundColor: '#FFF5F5',
border: '1px solid #FECACA', border: '1px solid #FED7D7',
borderRadius: '8px', color: '#C53030',
color: '#DC2626',
fontSize: '0.875rem', fontSize: '0.875rem',
}}> }}>
{error} {error}
@@ -169,9 +204,7 @@ export default function LoginPage() {
{mode === 'register' && ( {mode === 'register' && (
<div style={{ marginBottom: '1rem' }}> <div style={{ marginBottom: '1rem' }}>
<label style={{ display: 'block', fontSize: '0.8rem', fontWeight: 600, color: INK, marginBottom: '0.4rem' }}> <label style={labelStyle}>Nome visualizzato</label>
Nome visualizzato
</label>
<input <input
type="text" type="text"
value={displayName} value={displayName}
@@ -183,9 +216,7 @@ export default function LoginPage() {
)} )}
<div style={{ marginBottom: '1rem' }}> <div style={{ marginBottom: '1rem' }}>
<label style={{ display: 'block', fontSize: '0.8rem', fontWeight: 600, color: INK, marginBottom: '0.4rem' }}> <label style={labelStyle}>Email</label>
Email
</label>
<input <input
type="email" type="email"
value={email} value={email}
@@ -197,9 +228,7 @@ export default function LoginPage() {
</div> </div>
<div style={{ marginBottom: '1.5rem' }}> <div style={{ marginBottom: '1.5rem' }}>
<label style={{ display: 'block', fontSize: '0.8rem', fontWeight: 600, color: INK, marginBottom: '0.4rem' }}> <label style={labelStyle}>Password</label>
Password
</label>
<input <input
type="password" type="password"
value={password} value={password}
@@ -215,122 +244,122 @@ export default function LoginPage() {
disabled={loading} disabled={loading}
style={{ style={{
width: '100%', width: '100%',
padding: '0.75rem', padding: '0.8rem',
backgroundColor: CORAL, backgroundColor: loading ? '#888' : INK,
color: 'white', color: 'white',
border: 'none', border: 'none',
borderRadius: '8px', borderRadius: 0,
fontFamily: "'DM Sans', sans-serif",
fontWeight: 600, fontWeight: 600,
fontSize: '0.9rem', fontSize: '0.9rem',
cursor: loading ? 'not-allowed' : 'pointer', cursor: loading ? 'not-allowed' : 'pointer',
opacity: loading ? 0.7 : 1, transition: 'background-color 0.2s, transform 0.15s',
transition: 'opacity 0.2s',
}} }}
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'} {loading ? 'Caricamento...' : mode === 'login' ? 'Accedi' : 'Crea account'}
</button> </button>
</form> </form>
{/* Divider */} {/* ── Divider ── */}
<div style={{ display: 'flex', alignItems: 'center', margin: '1.25rem 0', gap: '1rem' }}> <div style={{ display: 'flex', alignItems: 'center', gap: '1rem', marginBottom: '1.25rem' }}>
<div style={{ flex: 1, height: 1, backgroundColor: BORDER }} /> <div style={{ flex: 1, height: 1, backgroundColor: BORDER }} />
<span style={{ color: MUTED, fontSize: '0.8rem' }}>oppure</span> <span style={{ color: INK_MUTED, fontSize: '0.8rem' }}>oppure</span>
<div style={{ flex: 1, height: 1, backgroundColor: BORDER }} /> <div style={{ flex: 1, height: 1, backgroundColor: BORDER }} />
</div> </div>
{/* Social login buttons */} {/* ── Google ── */}
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.6rem' }}> <button
{/* Google */} onClick={handleGoogleLogin}
<button style={{
onClick={handleGoogleLogin} display: 'flex',
style={{ alignItems: 'center',
display: 'flex', justifyContent: 'center',
alignItems: 'center', gap: '0.75rem',
justifyContent: 'center', width: '100%',
gap: '0.75rem', padding: '0.75rem',
width: '100%', backgroundColor: 'white',
padding: '0.7rem', border: `1px solid ${BORDER}`,
backgroundColor: 'white', borderRadius: 0,
border: `1px solid ${BORDER}`, cursor: 'pointer',
borderRadius: '8px', fontFamily: "'DM Sans', sans-serif",
cursor: 'pointer', fontWeight: 500,
fontWeight: 500, fontSize: '0.875rem',
fontSize: '0.875rem', color: INK,
color: INK, marginBottom: '0.75rem',
transition: 'box-shadow 0.2s', transition: 'border-color 0.2s',
}} }}
> onMouseEnter={(e) => e.currentTarget.style.borderColor = INK}
<GoogleIcon /> onMouseLeave={(e) => e.currentTarget.style.borderColor = BORDER}
Continua con Google >
</button> <GoogleIcon />
Continua con Google
</button>
{/* Coming soon row */} {/* ── Coming soon ── */}
<div style={{ display: 'flex', gap: '0.5rem', justifyContent: 'center' }}> <div style={{ display: 'flex', gap: '0.5rem', justifyContent: 'center', marginBottom: '0.5rem' }}>
{[ {[
{ name: 'Facebook', icon: '📘' }, { name: 'Facebook', icon: '📘' },
{ name: 'Microsoft', icon: '🪟' }, { name: 'Microsoft', icon: '🪟' },
{ name: 'Apple', icon: '🍎' }, { name: 'Apple', icon: '🍎' },
{ name: 'Instagram', icon: '📸' }, ].map(({ name, icon }) => (
{ name: 'TikTok', icon: '🎵' }, <button
].map(({ name, icon }) => ( key={name}
<button title={`${name} — Disponibile a breve`}
key={name} style={{
onClick={comingSoon} padding: '0.5rem 0.85rem',
title={`${name} — Disponibile a breve`} backgroundColor: '#F5F5F5',
style={{ border: `1px solid ${BORDER}`,
padding: '0.5rem 0.75rem', borderRadius: 0,
backgroundColor: '#F5F5F5', cursor: 'not-allowed',
border: `1px solid ${BORDER}`, fontSize: '1rem',
borderRadius: '8px', opacity: 0.45,
cursor: 'not-allowed', }}
fontSize: '1rem', >
opacity: 0.5, {icon}
position: 'relative', </button>
}} ))}
>
{icon}
</button>
))}
</div>
<p style={{ textAlign: 'center', fontSize: '0.75rem', color: MUTED, margin: '0.25rem 0 0' }}>
Altri provider disponibili a breve
</p>
</div> </div>
<p style={{ textAlign: 'center', fontSize: '0.73rem', color: INK_MUTED, marginBottom: '1.5rem' }}>
Altri provider disponibili a breve
</p>
{/* Redeem code link */} {/* ── Redeem link ── */}
<p style={{ textAlign: 'center', marginTop: '1.5rem', fontSize: '0.85rem', color: MUTED }}> <p style={{ textAlign: 'center', fontSize: '0.85rem', color: INK_MUTED }}>
Hai un codice Pro?{' '} Hai un codice Pro?{' '}
<button <button
onClick={() => setShowRedeemModal(true)} onClick={() => setShowRedeemModal(true)}
style={{ style={{
background: 'none', background: 'none',
border: 'none', border: 'none',
color: CORAL, color: ACCENT,
cursor: 'pointer', cursor: 'pointer',
fontWeight: 600, fontWeight: 600,
fontSize: '0.85rem', fontSize: '0.85rem',
padding: 0, padding: 0,
textDecoration: 'underline',
textUnderlineOffset: '3px',
}} }}
> >
Riscattalo Riscattalo
</button> </button>
</p> </p>
{showRedeemModal && (
<RedeemModal onClose={() => setShowRedeemModal(false)} />
)}
</div> </div>
</div> </div>
{showRedeemModal && <RedeemModal onClose={() => setShowRedeemModal(false)} />}
</div> </div>
) )
} }
// ── Redeem modal ───────────────────────────────────────────────────────────────
function RedeemModal({ onClose }) { function RedeemModal({ onClose }) {
const [code, setCode] = useState('') const [code, setCode] = useState('')
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)
const [message, setMessage] = useState('') const [message, setMessage] = useState('')
const [error, setError] = useState('') const [error, setError] = useState('')
const { login } = useAuth()
const handleRedeem = async (e) => { const handleRedeem = async (e) => {
e.preventDefault() e.preventDefault()
@@ -351,31 +380,34 @@ function RedeemModal({ onClose }) {
return ( return (
<div style={{ <div style={{
position: 'fixed', inset: 0, position: 'fixed', inset: 0,
backgroundColor: 'rgba(0,0,0,0.4)', backgroundColor: 'rgba(0,0,0,0.45)',
display: 'flex', alignItems: 'center', justifyContent: 'center', display: 'flex', alignItems: 'center', justifyContent: 'center',
zIndex: 1000, zIndex: 1000,
}}> }}>
<div style={{ <div style={{
position: 'relative',
backgroundColor: 'white', backgroundColor: 'white',
borderRadius: '16px', borderTop: `4px solid ${ACCENT}`,
padding: '2rem', padding: '2rem',
width: '380px', width: '380px',
maxWidth: '90vw', maxWidth: '90vw',
boxShadow: '0 20px 60px rgba(0,0,0,0.2)', boxShadow: '0 20px 60px rgba(0,0,0,0.2)',
}}> }}>
<h3 style={{ margin: '0 0 0.5rem', fontSize: '1.1rem', color: INK }}>Riscatta Codice Pro</h3> <h3 style={{ margin: '0 0 0.4rem', fontSize: '1.1rem', color: INK, fontFamily: "'Fraunces', serif" }}>
<p style={{ margin: '0 0 1.25rem', fontSize: '0.85rem', color: MUTED }}> 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) Inserisci il tuo codice di attivazione (es. LP-XXXXXXXX)
</p> </p>
{message ? ( {message ? (
<div style={{ padding: '0.75rem', backgroundColor: '#F0FDF4', border: '1px solid #86EFAC', borderRadius: '8px', color: '#16A34A', fontSize: '0.875rem', marginBottom: '1rem' }}> <div style={{ padding: '0.75rem', backgroundColor: '#F0FDF4', border: '1px solid #86EFAC', color: '#16A34A', fontSize: '0.875rem', marginBottom: '1rem' }}>
{message} {message}
</div> </div>
) : ( ) : (
<form onSubmit={handleRedeem}> <form onSubmit={handleRedeem}>
{error && ( {error && (
<div style={{ padding: '0.75rem', backgroundColor: '#FEE2E2', border: '1px solid #FECACA', borderRadius: '8px', color: '#DC2626', fontSize: '0.875rem', marginBottom: '0.75rem' }}> <div style={{ padding: '0.75rem', backgroundColor: '#FFF5F5', border: '1px solid #FED7D7', color: '#C53030', fontSize: '0.875rem', marginBottom: '0.75rem' }}>
{error} {error}
</div> </div>
)} )}
@@ -393,13 +425,13 @@ function RedeemModal({ onClose }) {
style={{ style={{
width: '100%', width: '100%',
padding: '0.7rem', padding: '0.7rem',
backgroundColor: '#FF6B4A', backgroundColor: loading ? '#888' : INK,
color: 'white', color: 'white',
border: 'none', border: 'none',
borderRadius: '8px', borderRadius: 0,
fontFamily: "'DM Sans', sans-serif",
fontWeight: 600, fontWeight: 600,
cursor: loading ? 'not-allowed' : 'pointer', cursor: loading ? 'not-allowed' : 'pointer',
opacity: loading ? 0.7 : 1,
}} }}
> >
{loading ? 'Verifica...' : 'Riscatta'} {loading ? 'Verifica...' : 'Riscatta'}
@@ -415,9 +447,10 @@ function RedeemModal({ onClose }) {
padding: '0.6rem', padding: '0.6rem',
backgroundColor: 'transparent', backgroundColor: 'transparent',
border: `1px solid ${BORDER}`, border: `1px solid ${BORDER}`,
borderRadius: '8px', borderRadius: 0,
cursor: 'pointer', cursor: 'pointer',
color: MUTED, color: INK_MUTED,
fontFamily: "'DM Sans', sans-serif",
fontSize: '0.875rem', fontSize: '0.875rem',
}} }}
> >
@@ -428,6 +461,8 @@ function RedeemModal({ onClose }) {
) )
} }
// ── Google Icon SVG ────────────────────────────────────────────────────────────
function GoogleIcon() { function GoogleIcon() {
return ( return (
<svg width="18" height="18" viewBox="0 0 48 48"> <svg width="18" height="18" viewBox="0 0 48 48">
@@ -439,14 +474,28 @@ function GoogleIcon() {
) )
} }
// ── Shared inline styles ───────────────────────────────────────────────────────
const labelStyle = {
display: 'block',
fontSize: '0.78rem',
fontWeight: 600,
color: INK,
marginBottom: '0.4rem',
letterSpacing: '0.03em',
textTransform: 'uppercase',
}
const inputStyle = { const inputStyle = {
width: '100%', width: '100%',
padding: '0.65rem 0.9rem', padding: '0.65rem 0.875rem',
border: `1px solid #E8E4DC`, border: `1px solid ${BORDER}`,
borderRadius: '8px', borderRadius: 0,
fontSize: '0.875rem', fontSize: '0.875rem',
outline: 'none', outline: 'none',
boxSizing: 'border-box', boxSizing: 'border-box',
color: '#1A1A2E', color: INK,
backgroundColor: '#FAF8F3', backgroundColor: '#FFFFFF',
fontFamily: "'DM Sans', sans-serif",
transition: 'border-color 0.15s',
} }

View File

@@ -18,16 +18,23 @@ export default function PlanBanner() {
alignItems: 'center', alignItems: 'center',
gap: '0.75rem', gap: '0.75rem',
padding: '0.75rem 1rem', padding: '0.75rem 1rem',
backgroundColor: '#ECFDF5', backgroundColor: 'var(--success-light, #F0F9F4)',
border: '1px solid #A7F3D0', border: '1px solid #A7F3D0',
borderRadius: '10px', marginBottom: '1.5rem',
marginBottom: '1.25rem',
}}> }}>
<span style={{ fontSize: '1.1rem' }}></span> <div style={{ width: 8, height: 8, borderRadius: '50%', backgroundColor: 'var(--success)', flexShrink: 0 }} />
<div> <div>
<span style={{ fontWeight: 600, fontSize: '0.875rem', color: '#065F46' }}>Piano Pro</span> <span style={{
fontSize: '0.75rem',
fontWeight: 700,
letterSpacing: '0.08em',
textTransform: 'uppercase',
color: 'var(--success)',
}}>
Piano Pro
</span>
{expires && ( {expires && (
<span style={{ fontSize: '0.8rem', color: '#059669', marginLeft: '0.5rem' }}> <span style={{ fontSize: '0.78rem', color: 'var(--success)', marginLeft: '0.5rem', opacity: 0.8 }}>
Attivo fino al {expires} Attivo fino al {expires}
</span> </span>
)} )}
@@ -37,7 +44,8 @@ export default function PlanBanner() {
} }
const postsUsed = user.posts_generated_this_month || 0 const postsUsed = user.posts_generated_this_month || 0
const postsMax = 15 const postsMax = 15
const pct = Math.min(100, (postsUsed / postsMax) * 100)
return ( return (
<> <>
@@ -46,47 +54,57 @@ export default function PlanBanner() {
alignItems: 'center', alignItems: 'center',
justifyContent: 'space-between', justifyContent: 'space-between',
padding: '0.75rem 1rem', padding: '0.75rem 1rem',
backgroundColor: '#FFF7F5', backgroundColor: 'var(--accent-light)',
border: '1px solid #FFCBB8', border: '1px solid #FECCC8',
borderRadius: '10px', marginBottom: '1.5rem',
marginBottom: '1.25rem',
flexWrap: 'wrap', flexWrap: 'wrap',
gap: '0.5rem', gap: '0.5rem',
}}> }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem' }}> <div style={{ display: 'flex', alignItems: 'center', gap: '0.75rem', flexWrap: 'wrap' }}>
<span style={{ fontSize: '1rem' }}>🆓</span> <span style={{
<span style={{ fontWeight: 600, fontSize: '0.875rem', color: '#C2410C' }}> fontSize: '0.72rem',
Piano Freemium fontWeight: 700,
letterSpacing: '0.08em',
textTransform: 'uppercase',
color: 'var(--accent)',
}}>
Freemium
</span> </span>
<span style={{ fontSize: '0.8rem', color: '#9A3412' }}> <span style={{ fontSize: '0.8rem', color: 'var(--ink-light)' }}>
{postsUsed} post su {postsMax} usati questo mese {postsUsed} / {postsMax} post questo mese
</span> </span>
{/* progress bar */} <div style={{ width: 80, height: 4, backgroundColor: '#FECCC8', overflow: 'hidden' }}>
<div style={{ width: 80, height: 6, backgroundColor: '#FED7AA', borderRadius: 3, overflow: 'hidden', marginLeft: 4 }}>
<div style={{ <div style={{
height: '100%', height: '100%',
width: `${Math.min(100, (postsUsed / postsMax) * 100)}%`, width: `${pct}%`,
backgroundColor: '#F97316', backgroundColor: 'var(--accent)',
borderRadius: 3, transition: 'width 0.6s ease',
}} /> }} />
</div> </div>
</div> </div>
<button <button
onClick={() => setShowUpgrade(true)} onClick={() => setShowUpgrade(true)}
style={{ style={{
padding: '0.4rem 0.9rem', padding: '0.4rem 0.9rem',
backgroundColor: '#FF6B4A', backgroundColor: 'var(--ink)',
color: 'white', color: 'white',
border: 'none', border: 'none',
borderRadius: '7px', borderRadius: 0,
cursor: 'pointer', cursor: 'pointer',
fontFamily: "'DM Sans', sans-serif",
fontWeight: 600, fontWeight: 600,
fontSize: '0.8rem', fontSize: '0.78rem',
letterSpacing: '0.03em',
transition: 'background-color 0.2s, transform 0.15s',
}} }}
onMouseEnter={(e) => { e.target.style.backgroundColor = 'var(--accent)'; e.target.style.transform = 'translateY(-1px)' }}
onMouseLeave={(e) => { e.target.style.backgroundColor = 'var(--ink)'; e.target.style.transform = 'translateY(0)' }}
> >
Passa a Pro Passa a Pro
</button> </button>
</div> </div>
{showUpgrade && <UpgradeModal onClose={() => setShowUpgrade(false)} />} {showUpgrade && <UpgradeModal onClose={() => setShowUpgrade(false)} />}
</> </>
) )

View File

@@ -4,92 +4,89 @@ import { api } from '../api'
// ─── Provider catalogs ──────────────────────────────────────────────────────── // ─── Provider catalogs ────────────────────────────────────────────────────────
const TEXT_PROVIDERS = [ const TEXT_PROVIDERS = [
{ value: 'claude', label: 'Claude (Anthropic)', defaultModel: 'claude-sonnet-4-20250514', needsBaseUrl: false }, { value: 'claude', label: 'Claude (Anthropic)', defaultModel: 'claude-sonnet-4-20250514', needsBaseUrl: false },
{ value: 'openai', label: 'OpenAI', defaultModel: 'gpt-4o-mini', needsBaseUrl: false }, { value: 'openai', label: 'OpenAI', defaultModel: 'gpt-4o-mini', needsBaseUrl: false },
{ value: 'gemini', label: 'Gemini (Google)', defaultModel: 'gemini-2.0-flash', needsBaseUrl: false }, { value: 'gemini', label: 'Gemini (Google)', defaultModel: 'gemini-2.0-flash', needsBaseUrl: false },
{ value: 'openrouter', label: 'OpenRouter', defaultModel: 'openai/gpt-4o-mini', needsBaseUrl: false }, { value: 'openrouter', label: 'OpenRouter', defaultModel: 'openai/gpt-4o-mini', needsBaseUrl: false },
{ value: 'custom', label: 'Personalizzato (custom)', defaultModel: '', needsBaseUrl: true }, { value: 'custom', label: 'Personalizzato (custom)', defaultModel: '', needsBaseUrl: true },
] ]
const IMAGE_PROVIDERS = [ const IMAGE_PROVIDERS = [
{ value: 'dalle', label: 'DALL-E (OpenAI)', needsBaseUrl: false }, { value: 'dalle', label: 'DALL-E (OpenAI)', needsBaseUrl: false },
{ value: 'replicate', label: 'Replicate', needsBaseUrl: false }, { value: 'replicate', label: 'Replicate', needsBaseUrl: false },
{ value: 'wavespeed', label: 'WaveSpeed', needsBaseUrl: false }, { value: 'wavespeed', label: 'WaveSpeed', needsBaseUrl: false },
{ value: 'custom', label: 'Personalizzato (custom)', needsBaseUrl: true }, { value: 'custom', label: 'Personalizzato (custom)', needsBaseUrl: true },
] ]
const VIDEO_PROVIDERS = [ const VIDEO_PROVIDERS = [
{ value: 'wavespeed', label: 'WaveSpeed', needsBaseUrl: false }, { value: 'wavespeed', label: 'WaveSpeed', needsBaseUrl: false },
{ value: 'replicate', label: 'Replicate', needsBaseUrl: false }, { value: 'replicate', label: 'Replicate', needsBaseUrl: false },
{ value: 'custom', label: 'Personalizzato (custom)', needsBaseUrl: true }, { value: 'custom', label: 'Personalizzato (custom)', needsBaseUrl: true },
] ]
const VOICE_PROVIDERS = [ const VOICE_PROVIDERS = [
{ value: 'elevenlabs', label: 'ElevenLabs', needsBaseUrl: false }, { value: 'elevenlabs', label: 'ElevenLabs', needsBaseUrl: false },
{ value: 'openai_tts', label: 'OpenAI TTS', needsBaseUrl: false }, { value: 'openai_tts', label: 'OpenAI TTS', needsBaseUrl: false },
{ value: 'wavespeed', label: 'WaveSpeed', needsBaseUrl: false }, { value: 'wavespeed', label: 'WaveSpeed', needsBaseUrl: false },
{ value: 'custom', label: 'Personalizzato (custom)', needsBaseUrl: true }, { value: 'custom', label: 'Personalizzato (custom)', needsBaseUrl: true },
] ]
// ─── Styles ───────────────────────────────────────────────────────────────────
const card = {
backgroundColor: 'var(--surface)',
border: '1px solid var(--border)',
borderRadius: '0.75rem',
padding: '1.5rem',
}
const input = {
width: '100%',
padding: '0.625rem 1rem',
border: '1px solid var(--border)',
borderRadius: '0.5rem',
fontSize: '0.875rem',
color: 'var(--ink)',
backgroundColor: 'var(--cream)',
outline: 'none',
boxSizing: 'border-box',
}
// ─── Section component ──────────────────────────────────────────────────────── // ─── Section component ────────────────────────────────────────────────────────
function ProviderSection({ title, icon, description, providers, settingKeys, values, onChange, onSave, saving, success, error }) { function ProviderSection({ title, icon, description, providers, settingKeys, values, onChange, onSave, saving, success, error }) {
const { providerKey, apiKeyKey, modelKey, baseUrlKey, extraKey, extraLabel, extraPlaceholder } = settingKeys const { providerKey, apiKeyKey, modelKey, baseUrlKey, extraKey, extraLabel, extraPlaceholder } = settingKeys
const currentProvider = providers.find(p => p.value === values[providerKey]) || providers[0] const currentProvider = providers.find(p => p.value === values[providerKey]) || providers[0]
const showModel = modelKey != null const showModel = modelKey != null
const showBaseUrl = currentProvider.needsBaseUrl && baseUrlKey const showBaseUrl = currentProvider.needsBaseUrl && baseUrlKey
return ( return (
<div style={card} className="space-y-4"> <div style={{
<div className="flex items-start gap-3"> position: 'relative',
<span className="text-2xl">{icon}</span> backgroundColor: 'var(--surface)',
border: '1px solid var(--border)',
padding: '1.5rem',
overflow: 'hidden',
}}>
{/* accent top bar */}
<div style={{ position: 'absolute', top: 0, left: 0, right: 0, height: 4, backgroundColor: 'var(--accent)' }} />
{/* Section header */}
<div style={{ display: 'flex', alignItems: 'flex-start', gap: '0.75rem', marginBottom: '1.25rem' }}>
<span style={{ fontSize: '1.4rem', lineHeight: 1 }}>{icon}</span>
<div> <div>
<h3 className="font-semibold text-sm uppercase tracking-wider" style={{ color: 'var(--muted)' }}> <span style={{
display: 'block',
fontSize: '0.7rem',
fontWeight: 700,
letterSpacing: '0.1em',
textTransform: 'uppercase',
color: 'var(--accent)',
marginBottom: '0.2rem',
}}>
{title} {title}
</h3> </span>
<p className="text-xs mt-0.5" style={{ color: 'var(--muted)' }}>{description}</p> <p style={{ fontSize: '0.82rem', color: 'var(--ink-muted)', margin: 0 }}>{description}</p>
</div> </div>
</div> </div>
{error && ( {error && (
<div className="p-3 rounded-lg text-sm" style={{ backgroundColor: '#fef2f2', border: '1px solid #fecaca', color: '#dc2626' }}> <div style={{ padding: '0.75rem', backgroundColor: '#FFF5F5', border: '1px solid #FED7D7', color: '#C53030', fontSize: '0.875rem', marginBottom: '1rem' }}>
{error} {error}
</div> </div>
)} )}
{success && ( {success && (
<div className="p-3 rounded-lg text-sm" style={{ backgroundColor: '#f0fdf4', border: '1px solid #bbf7d0', color: '#16a34a' }}> <div style={{ padding: '0.75rem', backgroundColor: '#F0FDF4', border: '1px solid #86EFAC', color: 'var(--success)', fontSize: '0.875rem', marginBottom: '1rem' }}>
Salvato con successo Salvato con successo
</div> </div>
)} )}
{/* Provider dropdown */} {/* Provider dropdown */}
<div> <div style={{ marginBottom: '1rem' }}>
<label className="block text-sm font-medium mb-1" style={{ color: 'var(--ink)' }}>Provider</label> <label style={labelStyle}>Provider</label>
<select <select
value={values[providerKey] || providers[0].value} value={values[providerKey] || providers[0].value}
onChange={e => onChange(providerKey, e.target.value)} onChange={e => onChange(providerKey, e.target.value)}
style={input} style={selectStyle}
> >
{providers.map(p => ( {providers.map(p => (
<option key={p.value} value={p.value}>{p.label}</option> <option key={p.value} value={p.value}>{p.label}</option>
@@ -97,41 +94,45 @@ function ProviderSection({ title, icon, description, providers, settingKeys, val
</select> </select>
</div> </div>
{/* Custom base URL (shown only for 'custom' provider) */} {/* Custom base URL */}
{showBaseUrl && ( {showBaseUrl && (
<div> <div style={{ marginBottom: '1rem' }}>
<label className="block text-sm font-medium mb-1" style={{ color: 'var(--ink)' }}> <label style={labelStyle}>
Base URL <span className="font-normal ml-1" style={{ color: 'var(--muted)' }}>(es. https://mio-provider.com/v1)</span> Base URL <span style={{ fontWeight: 400, color: 'var(--ink-muted)', textTransform: 'none', letterSpacing: 0 }}>(es. https://mio-provider.com/v1)</span>
</label> </label>
<input <input
type="text" type="text"
value={values[baseUrlKey] || ''} value={values[baseUrlKey] || ''}
onChange={e => onChange(baseUrlKey, e.target.value)} onChange={e => onChange(baseUrlKey, e.target.value)}
placeholder="https://..." placeholder="https://..."
style={{ ...input, fontFamily: 'monospace' }} style={{ ...inputStyle, fontFamily: 'monospace' }}
onFocus={(e) => e.target.style.borderColor = 'var(--ink)'}
onBlur={(e) => e.target.style.borderColor = 'var(--border)'}
/> />
</div> </div>
)} )}
{/* API Key */} {/* API Key */}
<div> <div style={{ marginBottom: '1rem' }}>
<label className="block text-sm font-medium mb-1" style={{ color: 'var(--ink)' }}>API Key</label> <label style={labelStyle}>API Key</label>
<input <input
type="password" type="password"
value={values[apiKeyKey] || ''} value={values[apiKeyKey] || ''}
onChange={e => onChange(apiKeyKey, e.target.value)} onChange={e => onChange(apiKeyKey, e.target.value)}
placeholder="Inserisci la tua API key..." placeholder="Inserisci la tua API key..."
style={{ ...input, fontFamily: 'monospace' }} style={{ ...inputStyle, fontFamily: 'monospace' }}
onFocus={(e) => e.target.style.borderColor = 'var(--ink)'}
onBlur={(e) => e.target.style.borderColor = 'var(--border)'}
/> />
</div> </div>
{/* Model (optional, only for text/video providers) */} {/* Model */}
{showModel && ( {showModel && (
<div> <div style={{ marginBottom: '1rem' }}>
<label className="block text-sm font-medium mb-1" style={{ color: 'var(--ink)' }}> <label style={labelStyle}>
Modello Modello
{currentProvider.defaultModel && ( {currentProvider.defaultModel && (
<span className="font-normal ml-1" style={{ color: 'var(--muted)' }}> <span style={{ fontWeight: 400, color: 'var(--ink-muted)', textTransform: 'none', letterSpacing: 0, marginLeft: '0.4rem' }}>
(default: {currentProvider.defaultModel}) (default: {currentProvider.defaultModel})
</span> </span>
)} )}
@@ -141,23 +142,27 @@ function ProviderSection({ title, icon, description, providers, settingKeys, val
value={values[modelKey] || ''} value={values[modelKey] || ''}
onChange={e => onChange(modelKey, e.target.value)} onChange={e => onChange(modelKey, e.target.value)}
placeholder={currentProvider.defaultModel || 'nome-modello'} placeholder={currentProvider.defaultModel || 'nome-modello'}
style={{ ...input, fontFamily: 'monospace' }} style={{ ...inputStyle, fontFamily: 'monospace' }}
onFocus={(e) => e.target.style.borderColor = 'var(--ink)'}
onBlur={(e) => e.target.style.borderColor = 'var(--border)'}
/> />
</div> </div>
)} )}
{/* Extra field (es. Voice ID per ElevenLabs) */} {/* Extra field */}
{extraKey && ( {extraKey && (
<div> <div style={{ marginBottom: '1rem' }}>
<label className="block text-sm font-medium mb-1" style={{ color: 'var(--ink)' }}> <label style={labelStyle}>
{extraLabel} <span className="font-normal ml-1" style={{ color: 'var(--muted)' }}>(opzionale)</span> {extraLabel} <span style={{ fontWeight: 400, color: 'var(--ink-muted)', textTransform: 'none', letterSpacing: 0 }}>(opzionale)</span>
</label> </label>
<input <input
type="text" type="text"
value={values[extraKey] || ''} value={values[extraKey] || ''}
onChange={e => onChange(extraKey, e.target.value)} onChange={e => onChange(extraKey, e.target.value)}
placeholder={extraPlaceholder} placeholder={extraPlaceholder}
style={input} style={inputStyle}
onFocus={(e) => e.target.style.borderColor = 'var(--ink)'}
onBlur={(e) => e.target.style.borderColor = 'var(--border)'}
/> />
</div> </div>
)} )}
@@ -165,8 +170,20 @@ function ProviderSection({ title, icon, description, providers, settingKeys, val
<button <button
onClick={onSave} onClick={onSave}
disabled={saving} disabled={saving}
className="px-4 py-2 text-white text-sm font-medium rounded-lg transition-opacity" style={{
style={{ backgroundColor: 'var(--coral)', opacity: saving ? 0.7 : 1 }} padding: '0.6rem 1.25rem',
backgroundColor: saving ? '#888' : 'var(--ink)',
color: 'white',
border: 'none',
borderRadius: 0,
fontFamily: "'DM Sans', sans-serif",
fontWeight: 600,
fontSize: '0.875rem',
cursor: saving ? 'not-allowed' : 'pointer',
transition: 'background-color 0.2s, transform 0.15s',
}}
onMouseEnter={(e) => { if (!saving) { e.target.style.backgroundColor = 'var(--accent)'; e.target.style.transform = 'translateY(-1px)' } }}
onMouseLeave={(e) => { if (!saving) { e.target.style.backgroundColor = 'var(--ink)'; e.target.style.transform = 'translateY(0)' } }}
> >
{saving ? 'Salvataggio...' : 'Salva'} {saving ? 'Salvataggio...' : 'Salva'}
</button> </button>
@@ -178,27 +195,11 @@ function ProviderSection({ title, icon, description, providers, settingKeys, val
export default function SettingsPage() { export default function SettingsPage() {
const [values, setValues] = useState({ const [values, setValues] = useState({
// Text llm_provider: 'claude', llm_api_key: '', llm_model: '', llm_base_url: '',
llm_provider: 'claude', image_provider: 'dalle', image_api_key: '', image_base_url: '',
llm_api_key: '', video_provider: 'wavespeed', video_api_key: '', video_model: '', video_base_url: '',
llm_model: '', voice_provider: 'elevenlabs', voice_api_key: '', voice_base_url: '', elevenlabs_voice_id: '',
llm_base_url: '',
// Image
image_provider: 'dalle',
image_api_key: '',
image_base_url: '',
// Video
video_provider: 'wavespeed',
video_api_key: '',
video_model: '',
video_base_url: '',
// Voice
voice_provider: 'elevenlabs',
voice_api_key: '',
voice_base_url: '',
elevenlabs_voice_id: '',
}) })
const [loading, setLoading] = useState(true) const [loading, setLoading] = useState(true)
const [saving, setSaving] = useState({}) const [saving, setSaving] = useState({})
const [success, setSuccess] = useState({}) const [success, setSuccess] = useState({})
@@ -245,112 +246,117 @@ export default function SettingsPage() {
if (loading) { if (loading) {
return ( return (
<div> <div style={{ animation: 'fade-in 0.4s ease-out both' }}>
<h2 className="text-2xl font-bold mb-1" style={{ fontFamily: 'Fraunces, serif', color: 'var(--ink)' }}> <span className="editorial-tag">Impostazioni</span>
Impostazioni <div className="editorial-line" />
<h2 style={{ fontFamily: "'Fraunces', serif", fontSize: '1.75rem', color: 'var(--ink)', margin: '0.5rem 0 2rem' }}>
Provider AI
</h2> </h2>
<div className="flex justify-center py-12"> <div style={{ display: 'flex', justifyContent: 'center', padding: '3rem 0' }}>
<div className="animate-spin rounded-full h-8 w-8 border-2 border-t-transparent" style={{ borderColor: 'var(--coral)' }} /> <div style={{
width: 32, height: 32,
border: '2px solid var(--border)',
borderTopColor: 'var(--accent)',
borderRadius: '50%',
animation: 'spin 0.8s linear infinite',
}} />
</div> </div>
<style>{`@keyframes spin { to { transform: rotate(360deg) } }`}</style>
</div> </div>
) )
} }
return ( return (
<div> <div style={{ animation: 'fade-up 0.5s ease-out both' }}>
<div className="mb-8"> <div style={{ marginBottom: '2rem' }}>
<h2 className="text-2xl font-bold" style={{ fontFamily: 'Fraunces, serif', color: 'var(--ink)' }}> <span className="editorial-tag">Impostazioni</span>
Impostazioni <div className="editorial-line" />
<h2 style={{ fontFamily: "'Fraunces', serif", fontSize: '1.75rem', color: 'var(--ink)', margin: '0.5rem 0 0.4rem', letterSpacing: '-0.02em' }}>
Provider AI
</h2> </h2>
<p className="mt-1 text-sm" style={{ color: 'var(--muted)' }}> <p style={{ fontSize: '0.875rem', color: 'var(--ink-muted)', margin: 0 }}>
Scegli il provider per ogni tipo di output. Usa "Personalizzato" per collegare qualsiasi servizio compatibile. Scegli il provider per ogni tipo di output. Usa "Personalizzato" per collegare qualsiasi servizio compatibile.
</p> </p>
</div> </div>
<div className="max-w-2xl space-y-6"> <div style={{ maxWidth: 640, display: 'flex', flexDirection: 'column', gap: '1.25rem' }}>
{/* TEXT */}
<ProviderSection <ProviderSection
title="Testi & Script" title="Testi & Script" icon="✍️"
icon="✍️"
description="Provider LLM per generare post, caption, script e contenuti testuali." description="Provider LLM per generare post, caption, script e contenuti testuali."
providers={TEXT_PROVIDERS} providers={TEXT_PROVIDERS}
settingKeys={{ settingKeys={{ providerKey: 'llm_provider', apiKeyKey: 'llm_api_key', modelKey: 'llm_model', baseUrlKey: 'llm_base_url' }}
providerKey: 'llm_provider', values={values} onChange={handleChange}
apiKeyKey: 'llm_api_key',
modelKey: 'llm_model',
baseUrlKey: 'llm_base_url',
}}
values={values}
onChange={handleChange}
onSave={() => saveSection('text', ['llm_provider', 'llm_api_key', 'llm_model', 'llm_base_url'])} onSave={() => saveSection('text', ['llm_provider', 'llm_api_key', 'llm_model', 'llm_base_url'])}
saving={saving.text} saving={saving.text} success={success.text} error={errors.text}
success={success.text}
error={errors.text}
/> />
{/* IMAGE */}
<ProviderSection <ProviderSection
title="Immagini" title="Immagini" icon="🖼️"
icon="🖼️"
description="Provider per la generazione di immagini AI." description="Provider per la generazione di immagini AI."
providers={IMAGE_PROVIDERS} providers={IMAGE_PROVIDERS}
settingKeys={{ settingKeys={{ providerKey: 'image_provider', apiKeyKey: 'image_api_key', baseUrlKey: 'image_base_url' }}
providerKey: 'image_provider', values={values} onChange={handleChange}
apiKeyKey: 'image_api_key',
baseUrlKey: 'image_base_url',
}}
values={values}
onChange={handleChange}
onSave={() => saveSection('image', ['image_provider', 'image_api_key', 'image_base_url'])} onSave={() => saveSection('image', ['image_provider', 'image_api_key', 'image_base_url'])}
saving={saving.image} saving={saving.image} success={success.image} error={errors.image}
success={success.image}
error={errors.image}
/> />
{/* VIDEO */}
<ProviderSection <ProviderSection
title="Video" title="Video" icon="🎬"
icon="🎬"
description="Provider per la generazione di video AI (testo → video, immagine → video)." description="Provider per la generazione di video AI (testo → video, immagine → video)."
providers={VIDEO_PROVIDERS} providers={VIDEO_PROVIDERS}
settingKeys={{ settingKeys={{ providerKey: 'video_provider', apiKeyKey: 'video_api_key', modelKey: 'video_model', baseUrlKey: 'video_base_url' }}
providerKey: 'video_provider', values={values} onChange={handleChange}
apiKeyKey: 'video_api_key',
modelKey: 'video_model',
baseUrlKey: 'video_base_url',
}}
values={values}
onChange={handleChange}
onSave={() => saveSection('video', ['video_provider', 'video_api_key', 'video_model', 'video_base_url'])} onSave={() => saveSection('video', ['video_provider', 'video_api_key', 'video_model', 'video_base_url'])}
saving={saving.video} saving={saving.video} success={success.video} error={errors.video}
success={success.video}
error={errors.video}
/> />
{/* VOICE */}
<ProviderSection <ProviderSection
title="Voiceover" title="Voiceover" icon="🎙️"
icon="🎙️"
description="Provider text-to-speech per generare voiceover dai tuoi contenuti." description="Provider text-to-speech per generare voiceover dai tuoi contenuti."
providers={VOICE_PROVIDERS} providers={VOICE_PROVIDERS}
settingKeys={{ settingKeys={{
providerKey: 'voice_provider', providerKey: 'voice_provider', apiKeyKey: 'voice_api_key', baseUrlKey: 'voice_base_url',
apiKeyKey: 'voice_api_key', extraKey: 'elevenlabs_voice_id', extraLabel: 'Voice ID', extraPlaceholder: 'ID della voce (solo ElevenLabs)',
baseUrlKey: 'voice_base_url',
extraKey: 'elevenlabs_voice_id',
extraLabel: 'Voice ID',
extraPlaceholder: 'ID della voce (solo ElevenLabs)',
}} }}
values={values} values={values} onChange={handleChange}
onChange={handleChange}
onSave={() => saveSection('voice', ['voice_provider', 'voice_api_key', 'voice_base_url', 'elevenlabs_voice_id'])} onSave={() => saveSection('voice', ['voice_provider', 'voice_api_key', 'voice_base_url', 'elevenlabs_voice_id'])}
saving={saving.voice} saving={saving.voice} success={success.voice} error={errors.voice}
success={success.voice}
error={errors.voice}
/> />
</div> </div>
</div> </div>
) )
} }
// ─── Shared styles ────────────────────────────────────────────────────────────
const labelStyle = {
display: 'block',
fontSize: '0.72rem',
fontWeight: 700,
letterSpacing: '0.08em',
textTransform: 'uppercase',
color: 'var(--ink)',
marginBottom: '0.4rem',
}
const inputStyle = {
width: '100%',
padding: '0.625rem 0.875rem',
border: '1px solid var(--border)',
borderRadius: 0,
fontSize: '0.875rem',
color: 'var(--ink)',
backgroundColor: 'var(--surface)',
outline: 'none',
boxSizing: 'border-box',
transition: 'border-color 0.15s',
fontFamily: "'DM Sans', sans-serif",
}
const selectStyle = {
...inputStyle,
cursor: 'pointer',
appearance: 'auto',
}

View File

@@ -2,27 +2,28 @@ import { useState } from 'react'
import { api } from '../api' import { api } from '../api'
import { useAuth } from '../AuthContext' import { useAuth } from '../AuthContext'
const CORAL = '#FF6B4A' const ACCENT = '#E85A4F'
const INK = '#1A1A2E' const INK = '#1A1A1A'
const MUTED = '#888' const MUTED = '#7A7A7A'
const BORDER = '#E8E4DC' const BORDER = '#E5E0D8'
const SUCCESS = '#2D7A4F'
const PLANS = [ const PLANS = [
{ months: 1, label: '1 mese', price: '€14.95', pricePerMonth: '€14.95/mese' }, { months: 1, label: '1 mese', price: '€14.95', pricePerMonth: '€14.95/mese' },
{ months: 3, label: '3 mesi', price: '€39.95', pricePerMonth: '€13.32/mese', badge: 'Risparmia 15%' }, { months: 3, label: '3 mesi', price: '€39.95', pricePerMonth: '€13.32/mese', badge: 'Risparmia 15%' },
{ months: 6, label: '6 mesi', price: '€64.95', pricePerMonth: '€10.83/mese', badge: 'Risparmia 28%' }, { months: 6, label: '6 mesi', price: '€64.95', pricePerMonth: '€10.83/mese', badge: 'Risparmia 28%' },
{ months: 12, label: '1 anno', price: '€119.95', pricePerMonth: '€9.99/mese', badge: 'Best Value ✦' }, { months: 12, label: '1 anno', price: '€119.95', pricePerMonth: '€9.99/mese', badge: 'Best Value ✦' },
] ]
const COMPARISON = [ const COMPARISON = [
{ feature: 'Personaggi', free: '1', pro: 'Illimitati' }, { feature: 'Personaggi', free: '1', pro: 'Illimitati' },
{ feature: 'Post al mese', free: '15', pro: 'Illimitati' }, { feature: 'Post al mese', free: '15', pro: 'Illimitati' },
{ feature: 'Piattaforme', free: 'FB + IG', pro: 'FB + IG + YT + TT' }, { feature: 'Piattaforme', free: 'FB + IG', pro: 'FB + IG + YT + TT' },
{ feature: 'Piani automatici', free: '✗', pro: '✓' }, { feature: 'Piani automatici', free: '✗', pro: '✓' },
{ feature: 'Gestione commenti AI', free: '✗', pro: '✓' }, { feature: 'Gestione commenti AI', free: '✗', pro: '✓' },
{ feature: 'Link affiliati', free: '✗', pro: '✓' }, { feature: 'Link affiliati', free: '✗', pro: '✓' },
{ feature: 'Calendario editoriale', free: '5 slot', pro: 'Illimitato' }, { feature: 'Calendario editoriale', free: '5 slot', pro: 'Illimitato' },
{ feature: 'Priorità supporto', free: '✗', pro: '✓' }, { feature: 'Priorità supporto', free: '✗', pro: '✓' },
] ]
export default function UpgradeModal({ onClose }) { export default function UpgradeModal({ onClose }) {
@@ -56,34 +57,43 @@ export default function UpgradeModal({ onClose }) {
zIndex: 1000, zIndex: 1000,
padding: '1rem', padding: '1rem',
overflowY: 'auto', overflowY: 'auto',
animation: 'fade-in 0.2s ease-out both',
}}> }}>
<div style={{ <div style={{
position: 'relative',
backgroundColor: 'white', backgroundColor: 'white',
borderRadius: '20px', borderTop: `4px solid ${ACCENT}`,
padding: '2.5rem', padding: '2.5rem',
width: '100%', width: '100%',
maxWidth: '680px', maxWidth: '680px',
maxHeight: '90vh', maxHeight: '90vh',
overflowY: 'auto', overflowY: 'auto',
boxShadow: '0 25px 80px rgba(0,0,0,0.25)', boxShadow: '0 25px 80px rgba(0,0,0,0.25)',
position: 'relative', animation: 'fade-up 0.3s ease-out both',
}}> }}>
{/* Header */} {/* Header */}
<div style={{ textAlign: 'center', marginBottom: '1.5rem' }}> <div style={{ textAlign: 'center', marginBottom: '2rem' }}>
<span style={{ <span style={{
display: 'inline-block', display: 'inline-block',
padding: '0.3rem 0.8rem', padding: '0.25rem 0.75rem',
backgroundColor: '#FFF3E0', backgroundColor: '#FFF0EE',
color: '#E65100', color: ACCENT,
borderRadius: '20px', fontSize: '0.7rem',
fontSize: '0.75rem',
fontWeight: 700, fontWeight: 700,
letterSpacing: '0.05em', letterSpacing: '0.12em',
textTransform: 'uppercase',
marginBottom: '0.75rem', marginBottom: '0.75rem',
}}> }}>
EARLY ADOPTER BETA EARLY ADOPTER BETA
</span> </span>
<h2 style={{ margin: 0, fontSize: '1.8rem', color: INK, fontWeight: 700 }}> <h2 style={{
fontFamily: "'Fraunces', serif",
margin: 0,
fontSize: '1.8rem',
color: INK,
fontWeight: 600,
letterSpacing: '-0.02em',
}}>
Passa a Leopost Pro Passa a Leopost Pro
</h2> </h2>
<p style={{ margin: '0.5rem 0 0', color: MUTED, fontSize: '0.9rem' }}> <p style={{ margin: '0.5rem 0 0', color: MUTED, fontSize: '0.9rem' }}>
@@ -92,16 +102,11 @@ export default function UpgradeModal({ onClose }) {
</div> </div>
{/* Comparison table */} {/* Comparison table */}
<div style={{ <div style={{ border: `1px solid ${BORDER}`, overflow: 'hidden', marginBottom: '1.5rem' }}>
border: `1px solid ${BORDER}`,
borderRadius: '12px',
overflow: 'hidden',
marginBottom: '1.5rem',
}}>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr' }}> <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr' }}>
<div style={{ padding: '0.75rem 1rem', backgroundColor: '#F9F9F9', fontWeight: 700, fontSize: '0.8rem', color: MUTED, textTransform: 'uppercase', letterSpacing: '0.05em' }}>Feature</div> <div style={thCell({ header: true })}>Feature</div>
<div style={{ padding: '0.75rem 1rem', backgroundColor: '#F9F9F9', fontWeight: 700, fontSize: '0.8rem', color: MUTED, textTransform: 'uppercase', textAlign: 'center' }}>Freemium</div> <div style={{ ...thCell({ header: true }), textAlign: 'center' }}>Freemium</div>
<div style={{ padding: '0.75rem 1rem', backgroundColor: '#FFF5F3', fontWeight: 700, fontSize: '0.8rem', color: CORAL, textTransform: 'uppercase', textAlign: 'center' }}>Pro </div> <div style={{ ...thCell({ header: true }), textAlign: 'center', color: ACCENT, backgroundColor: '#FFF0EE' }}>Pro </div>
</div> </div>
{COMPARISON.map((row, i) => ( {COMPARISON.map((row, i) => (
<div key={row.feature} style={{ <div key={row.feature} style={{
@@ -110,42 +115,42 @@ export default function UpgradeModal({ onClose }) {
borderTop: `1px solid ${BORDER}`, borderTop: `1px solid ${BORDER}`,
backgroundColor: i % 2 === 0 ? 'white' : '#FAFAFA', backgroundColor: i % 2 === 0 ? 'white' : '#FAFAFA',
}}> }}>
<div style={{ padding: '0.6rem 1rem', fontSize: '0.85rem', color: INK }}>{row.feature}</div> <div style={tdCell()}>{row.feature}</div>
<div style={{ padding: '0.6rem 1rem', fontSize: '0.85rem', color: MUTED, textAlign: 'center' }}>{row.free}</div> <div style={{ ...tdCell(), textAlign: 'center', color: MUTED }}>{row.free}</div>
<div style={{ padding: '0.6rem 1rem', fontSize: '0.85rem', color: '#16A34A', fontWeight: 600, textAlign: 'center' }}>{row.pro}</div> <div style={{ ...tdCell(), textAlign: 'center', color: SUCCESS, fontWeight: 600 }}>{row.pro}</div>
</div> </div>
))} ))}
</div> </div>
{/* Pricing */} {/* Pricing cards */}
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(2, 1fr)', gap: '0.75rem', marginBottom: '1.5rem' }}> <div style={{ display: 'grid', gridTemplateColumns: 'repeat(2, 1fr)', gap: '0.75rem', marginBottom: '1.5rem' }}>
{PLANS.map((plan) => ( {PLANS.map((plan) => (
<div key={plan.months} style={{ <div key={plan.months} style={{
border: `1px solid ${BORDER}`, border: `1px solid ${plan.months === 12 ? ACCENT : BORDER}`,
borderRadius: '10px',
padding: '1rem', padding: '1rem',
position: 'relative', position: 'relative',
backgroundColor: plan.months === 12 ? '#FFF5F3' : 'white', backgroundColor: plan.months === 12 ? '#FFF0EE' : 'white',
borderColor: plan.months === 12 ? CORAL : BORDER,
}}> }}>
{plan.badge && ( {plan.badge && (
<span style={{ <span style={{
position: 'absolute', position: 'absolute',
top: '-10px', top: '-10px',
right: '10px', right: '10px',
backgroundColor: CORAL, backgroundColor: ACCENT,
color: 'white', color: 'white',
padding: '0.15rem 0.5rem', padding: '0.15rem 0.5rem',
borderRadius: '10px', fontSize: '0.68rem',
fontSize: '0.7rem',
fontWeight: 700, fontWeight: 700,
letterSpacing: '0.04em',
}}> }}>
{plan.badge} {plan.badge}
</span> </span>
)} )}
<div style={{ fontWeight: 700, color: INK, marginBottom: '0.25rem' }}>{plan.label}</div> <div style={{ fontWeight: 600, color: INK, marginBottom: '0.25rem', fontSize: '0.9rem' }}>{plan.label}</div>
<div style={{ fontSize: '1.4rem', fontWeight: 800, color: plan.months === 12 ? CORAL : INK }}>{plan.price}</div> <div style={{ fontSize: '1.5rem', fontWeight: 700, color: plan.months === 12 ? ACCENT : INK, fontFamily: "'Fraunces', serif" }}>
<div style={{ fontSize: '0.75rem', color: MUTED }}>{plan.pricePerMonth}</div> {plan.price}
</div>
<div style={{ fontSize: '0.73rem', color: MUTED }}>{plan.pricePerMonth}</div>
</div> </div>
))} ))}
</div> </div>
@@ -157,29 +162,38 @@ export default function UpgradeModal({ onClose }) {
display: 'block', display: 'block',
textAlign: 'center', textAlign: 'center',
padding: '0.9rem', padding: '0.9rem',
backgroundColor: CORAL, backgroundColor: INK,
color: 'white', color: 'white',
borderRadius: '10px', fontFamily: "'DM Sans', sans-serif",
fontWeight: 700, fontWeight: 700,
fontSize: '0.95rem', fontSize: '0.95rem',
textDecoration: 'none', textDecoration: 'none',
marginBottom: '1.5rem', marginBottom: '1.5rem',
transition: 'background-color 0.2s, transform 0.15s',
}} }}
onMouseEnter={(e) => { e.currentTarget.style.backgroundColor = ACCENT; e.currentTarget.style.transform = 'translateY(-1px)' }}
onMouseLeave={(e) => { e.currentTarget.style.backgroundColor = INK; e.currentTarget.style.transform = 'translateY(0)' }}
> >
Contattaci per attivare Pro Early Adopter Contattaci per attivare Pro Early Adopter
</a> </a>
{/* Redeem code */} {/* Redeem code section */}
<div style={{ <div style={{
border: `1px solid ${BORDER}`, border: `1px solid ${BORDER}`,
borderRadius: '10px',
padding: '1.25rem', padding: '1.25rem',
backgroundColor: '#FAFAFA', backgroundColor: '#FAFAFA',
}}> }}>
<h4 style={{ margin: '0 0 0.75rem', fontSize: '0.9rem', color: INK }}>Hai già un codice?</h4> <h4 style={{
margin: '0 0 0.75rem',
fontSize: '0.875rem',
color: INK,
fontFamily: "'Fraunces', serif",
}}>
Hai già un codice?
</h4>
{redeemSuccess ? ( {redeemSuccess ? (
<div style={{ padding: '0.75rem', backgroundColor: '#F0FDF4', border: '1px solid #86EFAC', borderRadius: '8px', color: '#16A34A', fontSize: '0.875rem' }}> <div style={{ padding: '0.75rem', backgroundColor: '#F0FDF4', border: '1px solid #86EFAC', color: SUCCESS, fontSize: '0.875rem' }}>
{redeemSuccess} {redeemSuccess}
</div> </div>
) : ( ) : (
@@ -193,26 +207,31 @@ export default function UpgradeModal({ onClose }) {
flex: 1, flex: 1,
padding: '0.6rem 0.75rem', padding: '0.6rem 0.75rem',
border: `1px solid ${BORDER}`, border: `1px solid ${BORDER}`,
borderRadius: '7px', borderRadius: 0,
fontFamily: 'monospace', fontFamily: 'monospace',
fontSize: '0.875rem', fontSize: '0.875rem',
outline: 'none', outline: 'none',
backgroundColor: 'white',
color: INK,
}} }}
onFocus={(e) => e.target.style.borderColor = INK}
onBlur={(e) => e.target.style.borderColor = BORDER}
/> />
<button <button
type="submit" type="submit"
disabled={redeemLoading || !redeemCode.trim()} disabled={redeemLoading || !redeemCode.trim()}
style={{ style={{
padding: '0.6rem 1rem', padding: '0.6rem 1rem',
backgroundColor: INK, backgroundColor: redeemLoading ? '#888' : INK,
color: 'white', color: 'white',
border: 'none', border: 'none',
borderRadius: '7px', borderRadius: 0,
cursor: redeemLoading ? 'not-allowed' : 'pointer', cursor: redeemLoading ? 'not-allowed' : 'pointer',
fontFamily: "'DM Sans', sans-serif",
fontWeight: 600, fontWeight: 600,
fontSize: '0.85rem', fontSize: '0.85rem',
opacity: redeemLoading ? 0.7 : 1,
whiteSpace: 'nowrap', whiteSpace: 'nowrap',
opacity: (!redeemCode.trim() && !redeemLoading) ? 0.5 : 1,
}} }}
> >
{redeemLoading ? '...' : 'Riscatta'} {redeemLoading ? '...' : 'Riscatta'}
@@ -220,7 +239,7 @@ export default function UpgradeModal({ onClose }) {
</form> </form>
)} )}
{redeemError && ( {redeemError && (
<p style={{ margin: '0.5rem 0 0', color: '#DC2626', fontSize: '0.8rem' }}>{redeemError}</p> <p style={{ margin: '0.5rem 0 0', color: '#C53030', fontSize: '0.8rem' }}>{redeemError}</p>
)} )}
</div> </div>
@@ -233,10 +252,11 @@ export default function UpgradeModal({ onClose }) {
right: '1rem', right: '1rem',
background: 'none', background: 'none',
border: 'none', border: 'none',
fontSize: '1.4rem', fontSize: '1.5rem',
cursor: 'pointer', cursor: 'pointer',
color: MUTED, color: MUTED,
lineHeight: 1, lineHeight: 1,
padding: '0.25rem',
}} }}
> >
× ×
@@ -245,3 +265,25 @@ export default function UpgradeModal({ onClose }) {
</div> </div>
) )
} }
// ── Helper style functions ─────────────────────────────────────────────────────
function thCell({ header } = {}) {
return {
padding: '0.75rem 1rem',
backgroundColor: header ? '#F5F0E8' : 'white',
fontWeight: 700,
fontSize: '0.72rem',
color: '#7A7A7A',
textTransform: 'uppercase',
letterSpacing: '0.07em',
}
}
function tdCell() {
return {
padding: '0.6rem 1rem',
fontSize: '0.85rem',
color: '#1A1A1A',
}
}

View File

@@ -2,15 +2,33 @@
@tailwind components; @tailwind components;
@tailwind utilities; @tailwind utilities;
@import url('https://fonts.googleapis.com/css2?family=Fraunces:ital,wght@0,400;0,600;0,700;1,400&family=DM+Sans:wght@400;500;600&display=swap'); @import url('https://fonts.googleapis.com/css2?family=Fraunces:ital,opsz,wght@0,9..144,400;0,9..144,600;0,9..144,700;1,9..144,400&family=DM+Sans:wght@400;500;600&display=swap');
:root { :root {
--coral: #FF6B4A; /* Core palette */
--cream: #FAF8F3; --cream: #FFFBF5;
--ink: #1A1A2E; --cream-dark: #F5F0E8;
--muted: #8B8B9A; --ink: #1A1A1A;
--surface: #FFFFFF; --ink-light: #4A4A4A;
--border: #E8E4DE; --ink-muted: #7A7A7A;
--accent: #E85A4F;
--accent-hover: #D14940;
--accent-light: #FFF0EE;
--border: #E5E0D8;
--border-strong: #D0C9BD;
--success: #2D7A4F;
--success-light: #F0F9F4;
--error: #C53030;
--error-light: #FFF5F5;
--surface: #FFFFFF;
/* Legacy aliases used in components */
--coral: var(--accent);
--muted: var(--ink-muted);
}
* {
box-sizing: border-box;
} }
body { body {
@@ -18,8 +36,142 @@ body {
color: var(--ink); color: var(--ink);
font-family: 'DM Sans', sans-serif; font-family: 'DM Sans', sans-serif;
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
} }
h1, h2, h3 { h1, h2, h3, h4 {
font-family: 'Fraunces', serif; font-family: 'Fraunces', serif;
letter-spacing: -0.02em;
font-weight: 600;
} }
/* ─── Card editorial ────────────────────────────────────────── */
.card-editorial {
position: relative;
background: var(--surface);
border: 1px solid var(--border);
border-radius: 0;
padding: 1.5rem;
overflow: hidden;
}
.card-editorial::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 4px;
background: var(--accent);
}
/* ─── Buttons ───────────────────────────────────────────────── */
.btn-primary {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 0.6rem 1.25rem;
background-color: var(--ink);
color: #FFFFFF;
border: none;
border-radius: 0;
font-family: 'DM Sans', sans-serif;
font-weight: 600;
font-size: 0.875rem;
cursor: pointer;
transition: background-color 0.2s ease, transform 0.15s ease;
text-decoration: none;
}
.btn-primary:hover:not(:disabled) {
background-color: var(--accent);
transform: translateY(-1px);
}
.btn-primary:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.btn-outline {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 0.6rem 1.25rem;
background-color: transparent;
color: var(--ink);
border: 1px solid var(--border-strong);
border-radius: 0;
font-family: 'DM Sans', sans-serif;
font-weight: 500;
font-size: 0.875rem;
cursor: pointer;
transition: border-color 0.2s ease, background-color 0.2s ease;
text-decoration: none;
}
.btn-outline:hover:not(:disabled) {
border-color: var(--ink);
background-color: var(--cream-dark);
}
/* ─── Inputs ────────────────────────────────────────────────── */
.input-editorial {
width: 100%;
padding: 0.625rem 0.875rem;
border: 1px solid var(--border);
border-radius: 0;
font-family: 'DM Sans', sans-serif;
font-size: 0.875rem;
color: var(--ink);
background-color: var(--surface);
outline: none;
transition: border-color 0.15s ease;
}
.input-editorial:focus {
border-color: var(--ink);
}
.input-editorial::placeholder {
color: var(--ink-muted);
}
/* ─── Editorial tags & decorators ──────────────────────────── */
.editorial-tag {
display: inline-block;
font-family: 'DM Sans', sans-serif;
font-size: 0.75rem;
font-weight: 600;
letter-spacing: 0.1em;
text-transform: uppercase;
color: var(--accent);
}
.editorial-line {
width: 60px;
height: 3px;
background-color: var(--accent);
margin: 0.5rem 0;
}
/* ─── Animations ────────────────────────────────────────────── */
@keyframes fade-up {
from { opacity: 0; transform: translateY(12px); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes fade-in {
from { opacity: 0; }
to { opacity: 1; }
}
.animate-fade-up {
animation: fade-up 0.6s ease-out both;
}
.animate-fade-in {
animation: fade-in 0.4s ease-out both;
}
.delay-100 { animation-delay: 0.1s; }
.delay-200 { animation-delay: 0.2s; }
.delay-300 { animation-delay: 0.3s; }
.delay-400 { animation-delay: 0.4s; }
/* ─── Scrollbar ─────────────────────────────────────────────── */
::-webkit-scrollbar { width: 5px; height: 5px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: var(--border-strong); border-radius: 2px; }
::-webkit-scrollbar-thumb:hover { background: var(--ink-muted); }

View File

@@ -4,28 +4,38 @@ export default {
theme: { theme: {
extend: { extend: {
colors: { colors: {
coral: '#FF6B4A', cream: '#FFFBF5',
cream: '#FAF8F3', 'cream-dark': '#F5F0E8',
ink: '#1A1A2E', ink: '#1A1A1A',
muted: '#8B8B9A', 'ink-light': '#4A4A4A',
border: '#E8E4DE', 'ink-muted': '#7A7A7A',
// Brand alias per compatibilità con componenti esistenti accent: '#E85A4F',
brand: { 'accent-hover':'#D14940',
50: '#fff4f1', 'accent-light':'#FFF0EE',
100: '#ffe4dd', border: '#E5E0D8',
200: '#ffc4b5', 'border-strong':'#D0C9BD',
300: '#ff9d85', success: '#2D7A4F',
400: '#ff7a5c', // Legacy aliases
500: '#FF6B4A', coral: '#E85A4F',
600: '#e8522f', muted: '#7A7A7A',
700: '#c43f22',
800: '#9e3219',
900: '#7c2912',
},
}, },
fontFamily: { fontFamily: {
serif: ['Fraunces', 'serif'], serif: ['Fraunces', 'Georgia', 'serif'],
sans: ['DM Sans', 'sans-serif'], sans: ['DM Sans', 'sans-serif'],
},
borderRadius: {
DEFAULT: '0',
none: '0',
sm: '0',
md: '0',
lg: '0',
xl: '0',
'2xl': '0',
full: '9999px', // keep for avatars/circles
},
letterSpacing: {
tight: '-0.02em',
editorial: '0.1em',
}, },
}, },
}, },