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:
@@ -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>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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',
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)} />}
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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',
|
||||||
|
}
|
||||||
|
|||||||
@@ -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',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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); }
|
||||||
|
|||||||
@@ -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',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user