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

@@ -3,9 +3,9 @@ import { useAuth } from '../AuthContext'
const nav = [
{ to: '/', label: 'Dashboard', icon: '◉' },
{ to: '/characters', label: 'Personaggi', icon: '◎' },
{ to: '/characters',label: 'Personaggi', 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: '/schedule', label: 'Schedulazione', icon: '◈' },
{ to: '/social', label: 'Social', icon: '◇' },
@@ -15,53 +15,137 @@ const nav = [
]
export default function Layout() {
const { user, logout } = useAuth()
const { user, logout, isPro, isAdmin } = useAuth()
return (
<div className="min-h-screen flex" style={{ backgroundColor: 'var(--cream)' }}>
{/* Sidebar */}
<aside className="w-60 flex flex-col shrink-0" style={{ backgroundColor: 'var(--ink)' }}>
<div className="p-5 border-b" style={{ borderColor: 'rgba(255,255,255,0.08)' }}>
<h1 className="text-lg font-bold tracking-tight text-white font-serif">
Leopost <span style={{ color: 'var(--coral)' }}>Full</span>
<div style={{ minHeight: '100vh', display: 'flex', backgroundColor: 'var(--cream)' }}>
{/* ── Sidebar ─────────────────────────────────────────────── */}
<aside style={{
width: 240,
flexShrink: 0,
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>
<p className="text-[10px] mt-0.5" style={{ color: 'var(--muted)' }}>
Content Automation
<div style={{ width: 40, height: 3, backgroundColor: 'var(--accent)', marginTop: '0.4rem' }} />
<p style={{ fontSize: '0.7rem', color: 'var(--ink-muted)', marginTop: '0.4rem', fontWeight: 500, letterSpacing: '0.08em', textTransform: 'uppercase' }}>
Content Studio
</p>
</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 }) => (
<NavLink
key={to}
to={to}
end={to === '/'}
className={({ isActive }) =>
`flex items-center gap-2.5 px-3 py-2 rounded text-[13px] font-medium transition-colors ${
isActive
? 'text-white'
: 'text-slate-400 hover:text-white'
}`
style={({ isActive }) => ({
display: 'flex',
alignItems: 'center',
gap: '0.625rem',
padding: '0.625rem 0.875rem',
fontSize: '0.84rem',
fontWeight: isActive ? 600 : 400,
color: isActive ? 'var(--accent)' : 'var(--ink-light)',
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)'
}
style={({ isActive }) =>
isActive ? { backgroundColor: 'var(--coral)' } : {}
}}
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}
</NavLink>
))}
</nav>
<div className="p-3 border-t" style={{ borderColor: 'rgba(255,255,255,0.08)' }}>
<div className="flex items-center justify-between px-2">
<span className="text-xs" style={{ color: 'var(--muted)' }}>
{user?.username}
{/* User footer */}
<div style={{
padding: '1rem 1.25rem',
borderTop: '1px solid var(--border)',
}}>
<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
onClick={logout}
className="text-[11px] transition-colors hover:text-white"
style={{ color: 'var(--muted)' }}
style={{
marginLeft: 'auto',
background: 'none',
border: 'none',
fontSize: '0.75rem',
color: 'var(--ink-muted)',
cursor: 'pointer',
padding: 0,
fontFamily: "'DM Sans', sans-serif",
}}
>
Logout
</button>
@@ -69,9 +153,9 @@ export default function Layout() {
</div>
</aside>
{/* Main content */}
<main className="flex-1 overflow-auto">
<div className="max-w-6xl mx-auto p-6">
{/* ── Main content ──────────────────────────────────────── */}
<main style={{ flex: 1, overflowY: 'auto' }}>
<div style={{ maxWidth: 1100, margin: '0 auto', padding: '2rem 2.5rem' }}>
<Outlet />
</div>
</main>

View File

@@ -3,11 +3,12 @@ import { useNavigate } from 'react-router-dom'
import { useAuth } from '../AuthContext'
import { BASE_URL } from '../api'
const CORAL = '#FF6B4A'
const CREAM = '#FAF8F3'
const INK = '#1A1A2E'
const MUTED = '#888'
const BORDER = '#E8E4DC'
const ACCENT = '#E85A4F'
const ACCENT_HOVER= '#D14940'
const CREAM = '#FFFBF5'
const INK = '#1A1A1A'
const INK_MUTED = '#7A7A7A'
const BORDER = '#E5E0D8'
export default function LoginPage() {
const [mode, setMode] = useState('login') // 'login' | 'register'
@@ -43,68 +44,105 @@ export default function LoginPage() {
window.location.href = `${BASE_URL}/auth/oauth/google`
}
const comingSoon = (e) => {
e.preventDefault()
// tooltip handled via title attr
}
return (
<div style={{ display: 'flex', height: '100vh', fontFamily: 'Inter, sans-serif' }}>
{/* LEFT SIDE */}
<div style={{
width: '40%',
backgroundColor: CORAL,
display: 'flex',
height: '100vh',
fontFamily: "'DM Sans', sans-serif",
overflow: 'hidden',
}}>
{/* ── LEFT PANEL ─────────────────────────────────────────── */}
<div style={{
width: '45%',
backgroundColor: ACCENT,
padding: '3rem',
display: 'flex',
flexDirection: 'column',
justifyContent: 'space-between',
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={{
fontFamily: 'Georgia, serif',
fontSize: '2.8rem',
fontFamily: "'Fraunces', serif",
fontSize: '2.6rem',
fontWeight: 700,
margin: 0,
letterSpacing: '-1px',
lineHeight: 1.15,
letterSpacing: '-0.02em',
}}>
Leopost
Il tuo studio<br />editoriale AI
</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>
<ul style={{ marginTop: '2.5rem', listStyle: 'none', padding: 0, lineHeight: 2 }}>
<li style={{ display: 'flex', alignItems: 'center', gap: '0.6rem' }}>
<span style={{ fontSize: '1.2rem' }}></span> Genera contenuti AI per ogni piattaforma
</li>
<li style={{ display: 'flex', alignItems: 'center', gap: '0.6rem' }}>
<span style={{ fontSize: '1.2rem' }}></span> Pubblica su Facebook, Instagram, YouTube, TikTok
</li>
<li style={{ display: 'flex', alignItems: 'center', gap: '0.6rem' }}>
<span style={{ fontSize: '1.2rem' }}></span> Schedula in automatico con piani editoriali
</li>
<li style={{ display: 'flex', alignItems: 'center', gap: '0.6rem' }}>
<span style={{ fontSize: '1.2rem' }}></span> Gestisci commenti con risposte AI
</li>
<li style={{ display: 'flex', alignItems: 'center', gap: '0.6rem' }}>
<span style={{ fontSize: '1.2rem' }}></span> Link affiliati integrati nei post
{/* 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={{
backgroundColor: 'rgba(255,255,255,0.15)',
borderRadius: '12px',
padding: '1rem 1.5rem',
fontSize: '0.85rem',
backdropFilter: 'blur(4px)',
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',
}}>
<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>
{/* RIGHT SIDE */}
{/* ── RIGHT PANEL ────────────────────────────────────────── */}
<div style={{
flex: 1,
backgroundColor: CREAM,
@@ -112,55 +150,52 @@ export default function LoginPage() {
alignItems: 'center',
justifyContent: 'center',
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={{
display: 'flex',
backgroundColor: 'white',
borderRadius: '10px',
border: `1px solid ${BORDER}`,
padding: '4px',
marginBottom: '1.5rem',
borderBottom: `2px solid ${BORDER}`,
marginBottom: '2rem',
}}>
{['login', 'register'].map((m) => (
{[
{ key: 'login', label: 'Accedi' },
{ key: 'register', label: 'Registrati' },
].map(({ key, label }) => (
<button
key={m}
onClick={() => { setMode(m); setError('') }}
key={key}
onClick={() => { setMode(key); setError('') }}
style={{
flex: 1,
padding: '0.5rem',
padding: '0.75rem',
background: 'none',
border: 'none',
borderRadius: '7px',
borderBottom: mode === key ? `2px solid ${ACCENT}` : '2px solid transparent',
marginBottom: '-2px',
cursor: 'pointer',
fontFamily: "'DM Sans', sans-serif",
fontWeight: 600,
fontSize: '0.875rem',
transition: 'all 0.2s',
backgroundColor: mode === m ? CORAL : 'transparent',
color: mode === m ? 'white' : MUTED,
fontSize: '0.9rem',
color: mode === key ? ACCENT : INK_MUTED,
transition: 'color 0.2s, border-color 0.2s',
}}
>
{m === 'login' ? 'Accedi' : 'Registrati'}
{label}
</button>
))}
</div>
<form onSubmit={handleSubmit} style={{
backgroundColor: 'white',
borderRadius: '16px',
padding: '2rem',
border: `1px solid ${BORDER}`,
boxShadow: '0 4px 24px rgba(0,0,0,0.06)',
}}>
{/* ── Form ── */}
<form onSubmit={handleSubmit} style={{ marginBottom: '1.5rem' }}>
{error && (
<div style={{
marginBottom: '1rem',
marginBottom: '1.25rem',
padding: '0.75rem 1rem',
backgroundColor: '#FEE2E2',
border: '1px solid #FECACA',
borderRadius: '8px',
color: '#DC2626',
backgroundColor: '#FFF5F5',
border: '1px solid #FED7D7',
color: '#C53030',
fontSize: '0.875rem',
}}>
{error}
@@ -169,9 +204,7 @@ export default function LoginPage() {
{mode === 'register' && (
<div style={{ marginBottom: '1rem' }}>
<label style={{ display: 'block', fontSize: '0.8rem', fontWeight: 600, color: INK, marginBottom: '0.4rem' }}>
Nome visualizzato
</label>
<label style={labelStyle}>Nome visualizzato</label>
<input
type="text"
value={displayName}
@@ -183,9 +216,7 @@ export default function LoginPage() {
)}
<div style={{ marginBottom: '1rem' }}>
<label style={{ display: 'block', fontSize: '0.8rem', fontWeight: 600, color: INK, marginBottom: '0.4rem' }}>
Email
</label>
<label style={labelStyle}>Email</label>
<input
type="email"
value={email}
@@ -197,9 +228,7 @@ export default function LoginPage() {
</div>
<div style={{ marginBottom: '1.5rem' }}>
<label style={{ display: 'block', fontSize: '0.8rem', fontWeight: 600, color: INK, marginBottom: '0.4rem' }}>
Password
</label>
<label style={labelStyle}>Password</label>
<input
type="password"
value={password}
@@ -215,32 +244,32 @@ export default function LoginPage() {
disabled={loading}
style={{
width: '100%',
padding: '0.75rem',
backgroundColor: CORAL,
padding: '0.8rem',
backgroundColor: loading ? '#888' : INK,
color: 'white',
border: 'none',
borderRadius: '8px',
borderRadius: 0,
fontFamily: "'DM Sans', sans-serif",
fontWeight: 600,
fontSize: '0.9rem',
cursor: loading ? 'not-allowed' : 'pointer',
opacity: loading ? 0.7 : 1,
transition: 'opacity 0.2s',
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', margin: '1.25rem 0', gap: '1rem' }}>
{/* ── Divider ── */}
<div style={{ display: 'flex', alignItems: 'center', gap: '1rem', marginBottom: '1.25rem' }}>
<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>
{/* Social login buttons */}
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.6rem' }}>
{/* Google */}
{/* ── Google ── */}
<button
onClick={handleGoogleLogin}
style={{
@@ -249,88 +278,88 @@ export default function LoginPage() {
justifyContent: 'center',
gap: '0.75rem',
width: '100%',
padding: '0.7rem',
padding: '0.75rem',
backgroundColor: 'white',
border: `1px solid ${BORDER}`,
borderRadius: '8px',
borderRadius: 0,
cursor: 'pointer',
fontFamily: "'DM Sans', sans-serif",
fontWeight: 500,
fontSize: '0.875rem',
color: INK,
transition: 'box-shadow 0.2s',
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 row */}
<div style={{ display: 'flex', gap: '0.5rem', justifyContent: 'center' }}>
{/* ── Coming soon ── */}
<div style={{ display: 'flex', gap: '0.5rem', justifyContent: 'center', marginBottom: '0.5rem' }}>
{[
{ name: 'Facebook', icon: '📘' },
{ name: 'Microsoft', icon: '🪟' },
{ name: 'Apple', icon: '🍎' },
{ name: 'Instagram', icon: '📸' },
{ name: 'TikTok', icon: '🎵' },
].map(({ name, icon }) => (
<button
key={name}
onClick={comingSoon}
title={`${name} — Disponibile a breve`}
style={{
padding: '0.5rem 0.75rem',
padding: '0.5rem 0.85rem',
backgroundColor: '#F5F5F5',
border: `1px solid ${BORDER}`,
borderRadius: '8px',
borderRadius: 0,
cursor: 'not-allowed',
fontSize: '1rem',
opacity: 0.5,
position: 'relative',
opacity: 0.45,
}}
>
{icon}
</button>
))}
</div>
<p style={{ textAlign: 'center', fontSize: '0.75rem', color: MUTED, margin: '0.25rem 0 0' }}>
<p style={{ textAlign: 'center', fontSize: '0.73rem', color: INK_MUTED, marginBottom: '1.5rem' }}>
Altri provider disponibili a breve
</p>
</div>
{/* Redeem code link */}
<p style={{ textAlign: 'center', marginTop: '1.5rem', fontSize: '0.85rem', color: MUTED }}>
{/* ── 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: CORAL,
color: ACCENT,
cursor: 'pointer',
fontWeight: 600,
fontSize: '0.85rem',
padding: 0,
textDecoration: 'underline',
textUnderlineOffset: '3px',
}}
>
Riscattalo
Riscattalo
</button>
</p>
</div>
</div>
{showRedeemModal && (
<RedeemModal onClose={() => setShowRedeemModal(false)} />
)}
</div>
</div>
{showRedeemModal && <RedeemModal onClose={() => setShowRedeemModal(false)} />}
</div>
)
}
// ── Redeem modal ───────────────────────────────────────────────────────────────
function RedeemModal({ onClose }) {
const [code, setCode] = useState('')
const [loading, setLoading] = useState(false)
const [message, setMessage] = useState('')
const [error, setError] = useState('')
const { login } = useAuth()
const handleRedeem = async (e) => {
e.preventDefault()
@@ -351,31 +380,34 @@ function RedeemModal({ onClose }) {
return (
<div style={{
position: 'fixed', inset: 0,
backgroundColor: 'rgba(0,0,0,0.4)',
backgroundColor: 'rgba(0,0,0,0.45)',
display: 'flex', alignItems: 'center', justifyContent: 'center',
zIndex: 1000,
}}>
<div style={{
position: 'relative',
backgroundColor: 'white',
borderRadius: '16px',
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.5rem', fontSize: '1.1rem', color: INK }}>Riscatta Codice Pro</h3>
<p style={{ margin: '0 0 1.25rem', fontSize: '0.85rem', color: MUTED }}>
<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', 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}
</div>
) : (
<form onSubmit={handleRedeem}>
{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}
</div>
)}
@@ -393,13 +425,13 @@ function RedeemModal({ onClose }) {
style={{
width: '100%',
padding: '0.7rem',
backgroundColor: '#FF6B4A',
backgroundColor: loading ? '#888' : INK,
color: 'white',
border: 'none',
borderRadius: '8px',
borderRadius: 0,
fontFamily: "'DM Sans', sans-serif",
fontWeight: 600,
cursor: loading ? 'not-allowed' : 'pointer',
opacity: loading ? 0.7 : 1,
}}
>
{loading ? 'Verifica...' : 'Riscatta'}
@@ -415,9 +447,10 @@ function RedeemModal({ onClose }) {
padding: '0.6rem',
backgroundColor: 'transparent',
border: `1px solid ${BORDER}`,
borderRadius: '8px',
borderRadius: 0,
cursor: 'pointer',
color: MUTED,
color: INK_MUTED,
fontFamily: "'DM Sans', sans-serif",
fontSize: '0.875rem',
}}
>
@@ -428,6 +461,8 @@ function RedeemModal({ onClose }) {
)
}
// ── Google Icon SVG ────────────────────────────────────────────────────────────
function GoogleIcon() {
return (
<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 = {
width: '100%',
padding: '0.65rem 0.9rem',
border: `1px solid #E8E4DC`,
borderRadius: '8px',
padding: '0.65rem 0.875rem',
border: `1px solid ${BORDER}`,
borderRadius: 0,
fontSize: '0.875rem',
outline: 'none',
boxSizing: 'border-box',
color: '#1A1A2E',
backgroundColor: '#FAF8F3',
color: INK,
backgroundColor: '#FFFFFF',
fontFamily: "'DM Sans', sans-serif",
transition: 'border-color 0.15s',
}

View File

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

View File

@@ -31,27 +31,6 @@ const VOICE_PROVIDERS = [
{ 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 ────────────────────────────────────────────────────────
function ProviderSection({ title, icon, description, providers, settingKeys, values, onChange, onSave, saving, success, error }) {
@@ -61,35 +40,53 @@ function ProviderSection({ title, icon, description, providers, settingKeys, val
const showBaseUrl = currentProvider.needsBaseUrl && baseUrlKey
return (
<div style={card} className="space-y-4">
<div className="flex items-start gap-3">
<span className="text-2xl">{icon}</span>
<div style={{
position: 'relative',
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>
<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}
</h3>
<p className="text-xs mt-0.5" style={{ color: 'var(--muted)' }}>{description}</p>
</span>
<p style={{ fontSize: '0.82rem', color: 'var(--ink-muted)', margin: 0 }}>{description}</p>
</div>
</div>
{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}
</div>
)}
{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
</div>
)}
{/* Provider dropdown */}
<div>
<label className="block text-sm font-medium mb-1" style={{ color: 'var(--ink)' }}>Provider</label>
<div style={{ marginBottom: '1rem' }}>
<label style={labelStyle}>Provider</label>
<select
value={values[providerKey] || providers[0].value}
onChange={e => onChange(providerKey, e.target.value)}
style={input}
style={selectStyle}
>
{providers.map(p => (
<option key={p.value} value={p.value}>{p.label}</option>
@@ -97,41 +94,45 @@ function ProviderSection({ title, icon, description, providers, settingKeys, val
</select>
</div>
{/* Custom base URL (shown only for 'custom' provider) */}
{/* Custom base URL */}
{showBaseUrl && (
<div>
<label className="block text-sm font-medium mb-1" style={{ color: 'var(--ink)' }}>
Base URL <span className="font-normal ml-1" style={{ color: 'var(--muted)' }}>(es. https://mio-provider.com/v1)</span>
<div style={{ marginBottom: '1rem' }}>
<label style={labelStyle}>
Base URL <span style={{ fontWeight: 400, color: 'var(--ink-muted)', textTransform: 'none', letterSpacing: 0 }}>(es. https://mio-provider.com/v1)</span>
</label>
<input
type="text"
value={values[baseUrlKey] || ''}
onChange={e => onChange(baseUrlKey, e.target.value)}
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>
)}
{/* API Key */}
<div>
<label className="block text-sm font-medium mb-1" style={{ color: 'var(--ink)' }}>API Key</label>
<div style={{ marginBottom: '1rem' }}>
<label style={labelStyle}>API Key</label>
<input
type="password"
value={values[apiKeyKey] || ''}
onChange={e => onChange(apiKeyKey, e.target.value)}
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>
{/* Model (optional, only for text/video providers) */}
{/* Model */}
{showModel && (
<div>
<label className="block text-sm font-medium mb-1" style={{ color: 'var(--ink)' }}>
<div style={{ marginBottom: '1rem' }}>
<label style={labelStyle}>
Modello
{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})
</span>
)}
@@ -141,23 +142,27 @@ function ProviderSection({ title, icon, description, providers, settingKeys, val
value={values[modelKey] || ''}
onChange={e => onChange(modelKey, e.target.value)}
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>
)}
{/* Extra field (es. Voice ID per ElevenLabs) */}
{/* Extra field */}
{extraKey && (
<div>
<label className="block text-sm font-medium mb-1" style={{ color: 'var(--ink)' }}>
{extraLabel} <span className="font-normal ml-1" style={{ color: 'var(--muted)' }}>(opzionale)</span>
<div style={{ marginBottom: '1rem' }}>
<label style={labelStyle}>
{extraLabel} <span style={{ fontWeight: 400, color: 'var(--ink-muted)', textTransform: 'none', letterSpacing: 0 }}>(opzionale)</span>
</label>
<input
type="text"
value={values[extraKey] || ''}
onChange={e => onChange(extraKey, e.target.value)}
placeholder={extraPlaceholder}
style={input}
style={inputStyle}
onFocus={(e) => e.target.style.borderColor = 'var(--ink)'}
onBlur={(e) => e.target.style.borderColor = 'var(--border)'}
/>
</div>
)}
@@ -165,8 +170,20 @@ function ProviderSection({ title, icon, description, providers, settingKeys, val
<button
onClick={onSave}
disabled={saving}
className="px-4 py-2 text-white text-sm font-medium rounded-lg transition-opacity"
style={{ backgroundColor: 'var(--coral)', opacity: saving ? 0.7 : 1 }}
style={{
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'}
</button>
@@ -178,27 +195,11 @@ function ProviderSection({ title, icon, description, providers, settingKeys, val
export default function SettingsPage() {
const [values, setValues] = useState({
// Text
llm_provider: 'claude',
llm_api_key: '',
llm_model: '',
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: '',
llm_provider: 'claude', llm_api_key: '', llm_model: '', llm_base_url: '',
image_provider: 'dalle', image_api_key: '', image_base_url: '',
video_provider: 'wavespeed', video_api_key: '', video_model: '', video_base_url: '',
voice_provider: 'elevenlabs', voice_api_key: '', voice_base_url: '', elevenlabs_voice_id: '',
})
const [loading, setLoading] = useState(true)
const [saving, setSaving] = useState({})
const [success, setSuccess] = useState({})
@@ -245,112 +246,117 @@ export default function SettingsPage() {
if (loading) {
return (
<div>
<h2 className="text-2xl font-bold mb-1" style={{ fontFamily: 'Fraunces, serif', color: 'var(--ink)' }}>
Impostazioni
<div style={{ animation: 'fade-in 0.4s ease-out both' }}>
<span className="editorial-tag">Impostazioni</span>
<div className="editorial-line" />
<h2 style={{ fontFamily: "'Fraunces', serif", fontSize: '1.75rem', color: 'var(--ink)', margin: '0.5rem 0 2rem' }}>
Provider AI
</h2>
<div className="flex justify-center py-12">
<div className="animate-spin rounded-full h-8 w-8 border-2 border-t-transparent" style={{ borderColor: 'var(--coral)' }} />
<div style={{ display: 'flex', justifyContent: 'center', padding: '3rem 0' }}>
<div style={{
width: 32, height: 32,
border: '2px solid var(--border)',
borderTopColor: 'var(--accent)',
borderRadius: '50%',
animation: 'spin 0.8s linear infinite',
}} />
</div>
<style>{`@keyframes spin { to { transform: rotate(360deg) } }`}</style>
</div>
)
}
return (
<div>
<div className="mb-8">
<h2 className="text-2xl font-bold" style={{ fontFamily: 'Fraunces, serif', color: 'var(--ink)' }}>
Impostazioni
<div style={{ animation: 'fade-up 0.5s ease-out both' }}>
<div style={{ marginBottom: '2rem' }}>
<span className="editorial-tag">Impostazioni</span>
<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>
<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.
</p>
</div>
<div className="max-w-2xl space-y-6">
<div style={{ maxWidth: 640, display: 'flex', flexDirection: 'column', gap: '1.25rem' }}>
{/* TEXT */}
<ProviderSection
title="Testi & Script"
icon="✍️"
title="Testi & Script" icon="✍️"
description="Provider LLM per generare post, caption, script e contenuti testuali."
providers={TEXT_PROVIDERS}
settingKeys={{
providerKey: 'llm_provider',
apiKeyKey: 'llm_api_key',
modelKey: 'llm_model',
baseUrlKey: 'llm_base_url',
}}
values={values}
onChange={handleChange}
settingKeys={{ providerKey: 'llm_provider', 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'])}
saving={saving.text}
success={success.text}
error={errors.text}
saving={saving.text} success={success.text} error={errors.text}
/>
{/* IMAGE */}
<ProviderSection
title="Immagini"
icon="🖼️"
title="Immagini" icon="🖼️"
description="Provider per la generazione di immagini AI."
providers={IMAGE_PROVIDERS}
settingKeys={{
providerKey: 'image_provider',
apiKeyKey: 'image_api_key',
baseUrlKey: 'image_base_url',
}}
values={values}
onChange={handleChange}
settingKeys={{ providerKey: 'image_provider', apiKeyKey: 'image_api_key', baseUrlKey: 'image_base_url' }}
values={values} onChange={handleChange}
onSave={() => saveSection('image', ['image_provider', 'image_api_key', 'image_base_url'])}
saving={saving.image}
success={success.image}
error={errors.image}
saving={saving.image} success={success.image} error={errors.image}
/>
{/* VIDEO */}
<ProviderSection
title="Video"
icon="🎬"
title="Video" icon="🎬"
description="Provider per la generazione di video AI (testo → video, immagine → video)."
providers={VIDEO_PROVIDERS}
settingKeys={{
providerKey: 'video_provider',
apiKeyKey: 'video_api_key',
modelKey: 'video_model',
baseUrlKey: 'video_base_url',
}}
values={values}
onChange={handleChange}
settingKeys={{ providerKey: 'video_provider', 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'])}
saving={saving.video}
success={success.video}
error={errors.video}
saving={saving.video} success={success.video} error={errors.video}
/>
{/* VOICE */}
<ProviderSection
title="Voiceover"
icon="🎙️"
title="Voiceover" icon="🎙️"
description="Provider text-to-speech per generare voiceover dai tuoi contenuti."
providers={VOICE_PROVIDERS}
settingKeys={{
providerKey: 'voice_provider',
apiKeyKey: 'voice_api_key',
baseUrlKey: 'voice_base_url',
extraKey: 'elevenlabs_voice_id',
extraLabel: 'Voice ID',
extraPlaceholder: 'ID della voce (solo ElevenLabs)',
providerKey: 'voice_provider', apiKeyKey: 'voice_api_key', baseUrlKey: 'voice_base_url',
extraKey: 'elevenlabs_voice_id', extraLabel: 'Voice ID', extraPlaceholder: 'ID della voce (solo ElevenLabs)',
}}
values={values}
onChange={handleChange}
values={values} onChange={handleChange}
onSave={() => saveSection('voice', ['voice_provider', 'voice_api_key', 'voice_base_url', 'elevenlabs_voice_id'])}
saving={saving.voice}
success={success.voice}
error={errors.voice}
saving={saving.voice} success={success.voice} error={errors.voice}
/>
</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,10 +2,11 @@ import { useState } from 'react'
import { api } from '../api'
import { useAuth } from '../AuthContext'
const CORAL = '#FF6B4A'
const INK = '#1A1A2E'
const MUTED = '#888'
const BORDER = '#E8E4DC'
const ACCENT = '#E85A4F'
const INK = '#1A1A1A'
const MUTED = '#7A7A7A'
const BORDER = '#E5E0D8'
const SUCCESS = '#2D7A4F'
const PLANS = [
{ months: 1, label: '1 mese', price: '€14.95', pricePerMonth: '€14.95/mese' },
@@ -56,34 +57,43 @@ export default function UpgradeModal({ onClose }) {
zIndex: 1000,
padding: '1rem',
overflowY: 'auto',
animation: 'fade-in 0.2s ease-out both',
}}>
<div style={{
position: 'relative',
backgroundColor: 'white',
borderRadius: '20px',
borderTop: `4px solid ${ACCENT}`,
padding: '2.5rem',
width: '100%',
maxWidth: '680px',
maxHeight: '90vh',
overflowY: 'auto',
boxShadow: '0 25px 80px rgba(0,0,0,0.25)',
position: 'relative',
animation: 'fade-up 0.3s ease-out both',
}}>
{/* Header */}
<div style={{ textAlign: 'center', marginBottom: '1.5rem' }}>
<div style={{ textAlign: 'center', marginBottom: '2rem' }}>
<span style={{
display: 'inline-block',
padding: '0.3rem 0.8rem',
backgroundColor: '#FFF3E0',
color: '#E65100',
borderRadius: '20px',
fontSize: '0.75rem',
padding: '0.25rem 0.75rem',
backgroundColor: '#FFF0EE',
color: ACCENT,
fontSize: '0.7rem',
fontWeight: 700,
letterSpacing: '0.05em',
letterSpacing: '0.12em',
textTransform: 'uppercase',
marginBottom: '0.75rem',
}}>
EARLY ADOPTER BETA
</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
</h2>
<p style={{ margin: '0.5rem 0 0', color: MUTED, fontSize: '0.9rem' }}>
@@ -92,16 +102,11 @@ export default function UpgradeModal({ onClose }) {
</div>
{/* Comparison table */}
<div style={{
border: `1px solid ${BORDER}`,
borderRadius: '12px',
overflow: 'hidden',
marginBottom: '1.5rem',
}}>
<div style={{ border: `1px solid ${BORDER}`, overflow: 'hidden', marginBottom: '1.5rem' }}>
<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={{ padding: '0.75rem 1rem', backgroundColor: '#F9F9F9', fontWeight: 700, fontSize: '0.8rem', color: MUTED, textTransform: 'uppercase', 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 })}>Feature</div>
<div style={{ ...thCell({ header: true }), textAlign: 'center' }}>Freemium</div>
<div style={{ ...thCell({ header: true }), textAlign: 'center', color: ACCENT, backgroundColor: '#FFF0EE' }}>Pro </div>
</div>
{COMPARISON.map((row, i) => (
<div key={row.feature} style={{
@@ -110,42 +115,42 @@ export default function UpgradeModal({ onClose }) {
borderTop: `1px solid ${BORDER}`,
backgroundColor: i % 2 === 0 ? 'white' : '#FAFAFA',
}}>
<div style={{ padding: '0.6rem 1rem', fontSize: '0.85rem', color: INK }}>{row.feature}</div>
<div style={{ padding: '0.6rem 1rem', fontSize: '0.85rem', color: MUTED, textAlign: 'center' }}>{row.free}</div>
<div style={{ padding: '0.6rem 1rem', fontSize: '0.85rem', color: '#16A34A', fontWeight: 600, textAlign: 'center' }}>{row.pro}</div>
<div style={tdCell()}>{row.feature}</div>
<div style={{ ...tdCell(), textAlign: 'center', color: MUTED }}>{row.free}</div>
<div style={{ ...tdCell(), textAlign: 'center', color: SUCCESS, fontWeight: 600 }}>{row.pro}</div>
</div>
))}
</div>
{/* Pricing */}
{/* Pricing cards */}
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(2, 1fr)', gap: '0.75rem', marginBottom: '1.5rem' }}>
{PLANS.map((plan) => (
<div key={plan.months} style={{
border: `1px solid ${BORDER}`,
borderRadius: '10px',
border: `1px solid ${plan.months === 12 ? ACCENT : BORDER}`,
padding: '1rem',
position: 'relative',
backgroundColor: plan.months === 12 ? '#FFF5F3' : 'white',
borderColor: plan.months === 12 ? CORAL : BORDER,
backgroundColor: plan.months === 12 ? '#FFF0EE' : 'white',
}}>
{plan.badge && (
<span style={{
position: 'absolute',
top: '-10px',
right: '10px',
backgroundColor: CORAL,
backgroundColor: ACCENT,
color: 'white',
padding: '0.15rem 0.5rem',
borderRadius: '10px',
fontSize: '0.7rem',
fontSize: '0.68rem',
fontWeight: 700,
letterSpacing: '0.04em',
}}>
{plan.badge}
</span>
)}
<div style={{ fontWeight: 700, color: INK, marginBottom: '0.25rem' }}>{plan.label}</div>
<div style={{ fontSize: '1.4rem', fontWeight: 800, color: plan.months === 12 ? CORAL : INK }}>{plan.price}</div>
<div style={{ fontSize: '0.75rem', color: MUTED }}>{plan.pricePerMonth}</div>
<div style={{ fontWeight: 600, color: INK, marginBottom: '0.25rem', fontSize: '0.9rem' }}>{plan.label}</div>
<div style={{ fontSize: '1.5rem', fontWeight: 700, color: plan.months === 12 ? ACCENT : INK, fontFamily: "'Fraunces', serif" }}>
{plan.price}
</div>
<div style={{ fontSize: '0.73rem', color: MUTED }}>{plan.pricePerMonth}</div>
</div>
))}
</div>
@@ -157,29 +162,38 @@ export default function UpgradeModal({ onClose }) {
display: 'block',
textAlign: 'center',
padding: '0.9rem',
backgroundColor: CORAL,
backgroundColor: INK,
color: 'white',
borderRadius: '10px',
fontFamily: "'DM Sans', sans-serif",
fontWeight: 700,
fontSize: '0.95rem',
textDecoration: 'none',
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
</a>
{/* Redeem code */}
{/* Redeem code section */}
<div style={{
border: `1px solid ${BORDER}`,
borderRadius: '10px',
padding: '1.25rem',
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 ? (
<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}
</div>
) : (
@@ -193,26 +207,31 @@ export default function UpgradeModal({ onClose }) {
flex: 1,
padding: '0.6rem 0.75rem',
border: `1px solid ${BORDER}`,
borderRadius: '7px',
borderRadius: 0,
fontFamily: 'monospace',
fontSize: '0.875rem',
outline: 'none',
backgroundColor: 'white',
color: INK,
}}
onFocus={(e) => e.target.style.borderColor = INK}
onBlur={(e) => e.target.style.borderColor = BORDER}
/>
<button
type="submit"
disabled={redeemLoading || !redeemCode.trim()}
style={{
padding: '0.6rem 1rem',
backgroundColor: INK,
backgroundColor: redeemLoading ? '#888' : INK,
color: 'white',
border: 'none',
borderRadius: '7px',
borderRadius: 0,
cursor: redeemLoading ? 'not-allowed' : 'pointer',
fontFamily: "'DM Sans', sans-serif",
fontWeight: 600,
fontSize: '0.85rem',
opacity: redeemLoading ? 0.7 : 1,
whiteSpace: 'nowrap',
opacity: (!redeemCode.trim() && !redeemLoading) ? 0.5 : 1,
}}
>
{redeemLoading ? '...' : 'Riscatta'}
@@ -220,7 +239,7 @@ export default function UpgradeModal({ onClose }) {
</form>
)}
{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>
@@ -233,10 +252,11 @@ export default function UpgradeModal({ onClose }) {
right: '1rem',
background: 'none',
border: 'none',
fontSize: '1.4rem',
fontSize: '1.5rem',
cursor: 'pointer',
color: MUTED,
lineHeight: 1,
padding: '0.25rem',
}}
>
×
@@ -245,3 +265,25 @@ export default function UpgradeModal({ onClose }) {
</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 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 {
--coral: #FF6B4A;
--cream: #FAF8F3;
--ink: #1A1A2E;
--muted: #8B8B9A;
/* Core palette */
--cream: #FFFBF5;
--cream-dark: #F5F0E8;
--ink: #1A1A1A;
--ink-light: #4A4A4A;
--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;
--border: #E8E4DE;
/* Legacy aliases used in components */
--coral: var(--accent);
--muted: var(--ink-muted);
}
* {
box-sizing: border-box;
}
body {
@@ -18,8 +36,142 @@ body {
color: var(--ink);
font-family: 'DM Sans', sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
h1, h2, h3 {
h1, h2, h3, h4 {
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,29 +4,39 @@ export default {
theme: {
extend: {
colors: {
coral: '#FF6B4A',
cream: '#FAF8F3',
ink: '#1A1A2E',
muted: '#8B8B9A',
border: '#E8E4DE',
// Brand alias per compatibilità con componenti esistenti
brand: {
50: '#fff4f1',
100: '#ffe4dd',
200: '#ffc4b5',
300: '#ff9d85',
400: '#ff7a5c',
500: '#FF6B4A',
600: '#e8522f',
700: '#c43f22',
800: '#9e3219',
900: '#7c2912',
},
cream: '#FFFBF5',
'cream-dark': '#F5F0E8',
ink: '#1A1A1A',
'ink-light': '#4A4A4A',
'ink-muted': '#7A7A7A',
accent: '#E85A4F',
'accent-hover':'#D14940',
'accent-light':'#FFF0EE',
border: '#E5E0D8',
'border-strong':'#D0C9BD',
success: '#2D7A4F',
// Legacy aliases
coral: '#E85A4F',
muted: '#7A7A7A',
},
fontFamily: {
serif: ['Fraunces', 'serif'],
serif: ['Fraunces', 'Georgia', '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',
},
},
},
plugins: [],