feat: sync all BRAIN mobile changes - onboarding, cookies, legal, mobile UX, settings

- Add OnboardingWizard, BetaBanner, CookieBanner components
- Add legal pages (Privacy, Terms, Cookies)
- Update Layout with mobile topbar, sidebar drawer, plan banner
- Update SettingsPage with profile, API config, security
- Update CharacterForm with topic suggestions, niche chips
- Update EditorialCalendar with shared strategy card
- Update ContentPage with narrative technique + brief
- Update SocialAccounts with 4 platforms and token guides
- Fix CSS button color inheritance, mobile responsive
- Add backup script
- Update .gitignore for pgdata and backups

Co-Authored-By: Claude (BRAIN/StackOS) <noreply@anthropic.com>
This commit is contained in:
Michele Borraccia
2026-04-03 14:59:14 +00:00
parent 8b77f1b86b
commit 2ca8b957e9
29 changed files with 4074 additions and 2803 deletions

View File

@@ -1,27 +1,43 @@
import { useState, useEffect } from 'react'
import { Link } from 'react-router-dom'
import { api } from '../api'
const cardStyle = {
backgroundColor: 'var(--surface)',
border: '1px solid var(--border)',
borderRadius: '0.75rem',
padding: '1.5rem',
}
const PLATFORMS = [
{ value: 'instagram', label: 'Instagram' },
{ value: 'facebook', label: 'Facebook' },
{ value: 'youtube', label: 'YouTube' },
{ value: 'tiktok', label: 'TikTok' },
]
const inputStyle = {
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',
const CONTENT_TYPES = [
{ value: 'text', label: 'Testo' },
{ value: 'image', label: 'Immagine' },
{ value: 'video', label: 'Video' },
{ value: 'reel', label: 'Reel/Short' },
{ value: 'story', label: 'Story' },
]
const TECNICHE_NARRATIVE = [
{ value: 'PAS', label: 'PAS', desc: 'Problema → Agitazione → Soluzione' },
{ value: 'AIDA', label: 'AIDA', desc: 'Attenzione → Interesse → Desiderio → Azione' },
{ value: 'Storytelling', label: 'Storytelling', desc: 'Narrazione con arco emotivo' },
{ value: 'Tutorial', label: 'Tutorial', desc: 'Step-by-step educativo' },
{ value: 'Listicle', label: 'Listicle', desc: 'Lista di consigli/esempi' },
{ value: 'Social_proof', label: 'Social Proof', desc: 'Risultati, testimonianze, numeri' },
{ value: 'Hook_domanda', label: 'Hook + Domanda', desc: 'Apertura con domanda provocatoria' },
]
const STATUS_LABELS = { approved: 'Approvato', published: 'Pubblicato', draft: 'Bozza' }
const STATUS_COLORS = {
approved: { bg: 'var(--success-light)', color: 'var(--success)' },
published: { bg: '#EFF6FF', color: '#1D4ED8' },
draft: { bg: '#FFFBEB', color: '#B45309' },
}
export default function ContentPage() {
const [characters, setCharacters] = useState([])
const [loading, setLoading] = useState(false)
const [charsLoading, setCharsLoading] = useState(true)
const [error, setError] = useState('')
const [generated, setGenerated] = useState(null)
const [editing, setEditing] = useState(false)
@@ -29,35 +45,41 @@ export default function ContentPage() {
const [form, setForm] = useState({
character_id: '',
platform: 'instagram',
content_type: 'text',
brief: '',
platforms: ['instagram'],
content_types: ['text'],
topic_hint: '',
tecnica: '',
include_affiliates: false,
})
useEffect(() => {
api.get('/characters/').then(setCharacters).catch(() => {})
api.get('/characters/').then(d => { setCharacters(d); setCharsLoading(false) }).catch(() => setCharsLoading(false))
}, [])
const handleChange = (field, value) => {
setForm((prev) => ({ ...prev, [field]: value }))
const toggleChip = (field, value) => {
setForm(prev => {
const arr = prev[field]
return { ...prev, [field]: arr.includes(value) ? (arr.length > 1 ? arr.filter(v => v !== value) : arr) : [...arr, value] }
})
}
const handleGenerate = async (e) => {
e.preventDefault()
if (!form.character_id) {
setError('Seleziona un personaggio')
return
}
if (!form.character_id) { setError('Seleziona un personaggio'); return }
setError('')
setLoading(true)
setGenerated(null)
try {
const data = await api.post('/content/generate', {
character_id: parseInt(form.character_id),
platform: form.platform,
content_type: form.content_type,
platforms: form.platforms,
content_types: form.content_types,
topic_hint: form.topic_hint || null,
brief: [
form.tecnica ? `Tecnica narrativa: ${form.tecnica}` : '',
form.brief,
].filter(Boolean).join('. ') || null,
include_affiliates: form.include_affiliates,
})
setGenerated(data)
@@ -70,222 +92,247 @@ export default function ContentPage() {
}
const handleApprove = async () => {
if (!generated) return
try {
await api.post(`/content/posts/${generated.id}/approve`)
setGenerated((prev) => ({ ...prev, status: 'approved' }))
} catch (err) {
setError(err.message || 'Errore approvazione')
}
setGenerated(prev => ({ ...prev, status: 'approved' }))
} catch (err) { setError(err.message || 'Errore approvazione') }
}
const handleSaveEdit = async () => {
if (!generated) return
try {
await api.put(`/content/posts/${generated.id}`, { text_content: editText })
setGenerated((prev) => ({ ...prev, text_content: editText }))
setGenerated(prev => ({ ...prev, text_content: editText }))
setEditing(false)
} catch (err) {
setError(err.message || 'Errore salvataggio')
}
} catch (err) { setError(err.message || 'Errore salvataggio') }
}
const handleDelete = async () => {
if (!generated) return
if (!confirm('Eliminare questo contenuto?')) return
try {
await api.delete(`/content/posts/${generated.id}`)
setGenerated(null)
} catch (err) {
setError(err.message || 'Errore eliminazione')
}
}
const platformLabels = {
instagram: 'Instagram',
facebook: 'Facebook',
youtube: 'YouTube',
tiktok: 'TikTok',
}
const contentTypeLabels = {
text: 'Testo',
image: 'Immagine',
video: 'Video',
} catch (err) { setError(err.message || 'Errore eliminazione') }
}
return (
<div>
<div className="mb-6">
<h2 className="text-2xl font-bold" style={{ color: 'var(--ink)' }}>Contenuti</h2>
<p className="mt-1 text-sm" style={{ color: 'var(--muted)' }}>
Genera e gestisci contenuti per i tuoi personaggi
<div style={{ animation: 'fade-up 0.5s ease-out both' }}>
{/* Header */}
<div style={{ marginBottom: '2rem' }}>
<span className="editorial-tag">Contenuti</span>
<div className="editorial-line" />
<h2 style={{ fontFamily: "'Fraunces', serif", fontSize: '1.75rem', fontWeight: 600, letterSpacing: '-0.02em', color: 'var(--ink)', margin: '0.5rem 0 0.3rem' }}>
Genera Contenuti
</h2>
<p style={{ fontSize: '0.875rem', color: 'var(--ink-muted)', margin: 0 }}>
Definisci un brief editoriale, scegli piattaforma e tipo, poi genera. L'AI terrà conto del tono e dei topic del personaggio selezionato.
</p>
</div>
{/* No characters → gate */}
{!charsLoading && characters.length === 0 && (
<div style={{ padding: '3rem 2rem', textAlign: 'center', backgroundColor: 'var(--surface)', border: '1px solid var(--border)', borderTop: '4px solid var(--accent)', marginBottom: '1.5rem' }}>
<div style={{ fontSize: '2rem', color: 'var(--accent)', marginBottom: '1rem' }}>◎</div>
<h3 style={{ fontFamily: "'Fraunces', serif", fontSize: '1.1rem', fontWeight: 600, color: 'var(--ink)', margin: '0 0 0.5rem' }}>Prima crea un Personaggio</h3>
<p style={{ fontSize: '0.875rem', color: 'var(--ink-muted)', maxWidth: 360, margin: '0 auto 1.25rem', lineHeight: 1.6 }}>
Per generare contenuti serve almeno un personaggio che definisca la voce editoriale (tono, nicchia, pubblico).
</p>
<Link to="/characters/new" style={btnPrimary}>Crea il tuo primo Personaggio →</Link>
</div>
)}
{error && (
<div className="mb-4 p-3 bg-red-50 border border-red-200 rounded-lg text-red-600 text-sm">
<div style={{ padding: '0.75rem 1rem', backgroundColor: 'var(--error-light)', border: '1px solid #FED7D7', color: 'var(--error)', fontSize: '0.875rem', marginBottom: '1rem' }}>
{error}
</div>
)}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(300px, 1fr))', gap: '1.25rem' }}>
{/* Generation form */}
<div style={cardStyle}>
<h3 className="font-semibold text-sm uppercase tracking-wider mb-4" style={{ color: 'var(--muted)' }}>
Genera Contenuto
</h3>
<div style={{ backgroundColor: 'var(--surface)', border: '1px solid var(--border)', borderTop: '4px solid var(--accent)', padding: '1.5rem' }}>
<div style={{ marginBottom: '1.25rem' }}>
<span style={labelStyle}>Genera Contenuto</span>
</div>
<form onSubmit={handleGenerate} className="space-y-4">
<form onSubmit={handleGenerate} style={{ display: 'flex', flexDirection: 'column', gap: '1.25rem' }}>
{/* Character select */}
<div>
<label className="block text-sm font-medium mb-1" style={{ color: 'var(--ink)' }}>Personaggio</label>
<select
value={form.character_id}
onChange={(e) => handleChange('character_id', e.target.value)}
style={inputStyle}
required
>
<option value="">Seleziona personaggio...</option>
{characters.map((c) => (
<option key={c.id} value={c.id}>{c.name}</option>
))}
<label style={labelStyle}>Personaggio</label>
<select value={form.character_id} onChange={e => setForm(p => ({ ...p, character_id: e.target.value }))} style={inputStyle} required>
<option value="">Seleziona personaggio…</option>
{characters.map(c => <option key={c.id} value={c.id}>{c.name}</option>)}
</select>
</div>
{/* Prompt / Brief strategico */}
<div>
<label className="block text-sm font-medium mb-1" style={{ color: 'var(--ink)' }}>Piattaforma</label>
<select
value={form.platform}
onChange={(e) => handleChange('platform', e.target.value)}
style={inputStyle}
>
{Object.entries(platformLabels).map(([val, label]) => (
<option key={val} value={val}>{label}</option>
))}
</select>
</div>
<div>
<label className="block text-sm font-medium mb-1" style={{ color: 'var(--ink)' }}>Tipo contenuto</label>
<select
value={form.content_type}
onChange={(e) => handleChange('content_type', e.target.value)}
style={inputStyle}
>
{Object.entries(contentTypeLabels).map(([val, label]) => (
<option key={val} value={val}>{label}</option>
))}
</select>
</div>
<div>
<label className="block text-sm font-medium mb-1" style={{ color: 'var(--ink)' }}>
Suggerimento tema <span className="font-normal" style={{ color: 'var(--muted)' }}>(opzionale)</span>
<label style={{ ...labelStyle, marginBottom: '0.5rem' }}>
Prompt & Strategia del Post
</label>
<input
type="text"
value={form.topic_hint}
onChange={(e) => handleChange('topic_hint', e.target.value)}
placeholder="Es. ultimi trend, tutorial..."
style={inputStyle}
/>
{/* Tecnica narrativa chips */}
<div style={{ marginBottom: '0.75rem' }}>
<p style={{ fontSize: '0.7rem', fontWeight: 600, letterSpacing: '0.06em', textTransform: 'uppercase', color: 'var(--ink-muted)', margin: '0 0 0.4rem' }}>Tecnica narrativa</p>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '0.35rem' }}>
{TECNICHE_NARRATIVE.map(t => (
<button key={t.value} type="button" onClick={() => setForm(p => ({ ...p, tecnica: p.tecnica === t.value ? '' : t.value }))} title={t.desc} style={{
padding: '0.3rem 0.75rem', fontSize: '0.78rem', fontFamily: "'DM Sans', sans-serif",
border: 'none', cursor: 'pointer',
backgroundColor: form.tecnica === t.value ? '#1A1A1A' : 'var(--cream-dark)',
color: form.tecnica === t.value ? 'white' : 'var(--ink-light)',
fontWeight: form.tecnica === t.value ? 600 : 400, transition: 'background-color 0.15s',
}}>
{t.label}
</button>
))}
</div>
{form.tecnica && (
<p style={{ fontSize: '0.7rem', color: 'var(--ink-muted)', margin: '0.3rem 0 0' }}>
{TECNICHE_NARRATIVE.find(t => t.value === form.tecnica)?.desc}
</p>
)}
</div>
{/* Brief textarea */}
<textarea value={form.brief} onChange={e => setForm(p => ({ ...p, brief: e.target.value }))}
placeholder="Descrivi in dettaglio cosa vuoi comunicare: obiettivo del post, a chi ti rivolgi, angolo narrativo, call to action, cosa deve provare chi legge.&#10;&#10;Es: 'Tutorial per principianti su sourdough tono incoraggiante, mostra che è più facile del previsto, CTA: salva la ricetta e taggami nel risultato'"
rows={4} style={{ ...inputStyle, resize: 'vertical', lineHeight: 1.6 }}
onFocus={e => e.target.style.borderColor = 'var(--ink)'} onBlur={e => e.target.style.borderColor = 'var(--border)'} />
<p style={{ fontSize: '0.72rem', color: 'var(--ink-muted)', margin: '0.3rem 0 0', lineHeight: 1.5 }}>
Più sei specifico, più il contenuto sarà preciso e pubblicabile senza revisioni.
</p>
</div>
<div className="flex items-center gap-3">
<label className="relative inline-flex items-center cursor-pointer">
<input
type="checkbox"
checked={form.include_affiliates}
onChange={(e) => handleChange('include_affiliates', e.target.checked)}
className="sr-only peer"
/>
<div className="w-9 h-5 bg-slate-200 rounded-full peer peer-checked:after:translate-x-full after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:rounded-full after:h-4 after:w-4 after:transition-all peer-checked:bg-coral"></div>
</label>
<span className="text-sm" style={{ color: 'var(--ink)' }}>Includi link affiliati</span>
{/* Platform chips */}
<div>
<label style={labelStyle}>Piattaforme <span style={{ fontWeight: 400, textTransform: 'none', letterSpacing: 0, color: 'var(--ink-muted)', fontSize: '0.75rem' }}>(seleziona una o più)</span></label>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '0.4rem', marginTop: '0.4rem' }}>
{PLATFORMS.map(p => {
const active = form.platforms.includes(p.value)
return (
<button key={p.value} type="button" onClick={() => toggleChip('platforms', p.value)} style={{
padding: '0.35rem 0.875rem', fontSize: '0.82rem', fontWeight: active ? 600 : 400,
fontFamily: "'DM Sans', sans-serif", border: 'none', cursor: 'pointer',
backgroundColor: active ? 'var(--ink)' : 'var(--cream-dark)',
color: active ? 'white' : 'var(--ink-light)',
transition: 'background-color 0.15s, color 0.15s',
}}>
{p.label}
</button>
)
})}
</div>
</div>
<button
type="submit"
disabled={loading}
className="w-full py-2.5 text-white font-medium rounded-lg transition-opacity text-sm"
style={{ backgroundColor: 'var(--coral)', opacity: loading ? 0.7 : 1 }}
>
{/* Content type chips */}
<div>
<label style={labelStyle}>Tipo di contenuto <span style={{ fontWeight: 400, textTransform: 'none', letterSpacing: 0, color: 'var(--ink-muted)', fontSize: '0.75rem' }}>(seleziona uno o più)</span></label>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '0.4rem', marginTop: '0.4rem' }}>
{CONTENT_TYPES.map(t => {
const active = form.content_types.includes(t.value)
return (
<button key={t.value} type="button" onClick={() => toggleChip('content_types', t.value)} style={{
padding: '0.35rem 0.875rem', fontSize: '0.82rem', fontWeight: active ? 600 : 400,
fontFamily: "'DM Sans', sans-serif", border: 'none', cursor: 'pointer',
backgroundColor: active ? 'var(--accent)' : 'var(--cream-dark)',
color: active ? 'white' : 'var(--ink-light)',
transition: 'background-color 0.15s, color 0.15s',
}}>
{t.label}
</button>
)
})}
</div>
</div>
{/* Topic hint */}
<div>
<label style={labelStyle}>Parola chiave / Topic <span style={{ fontWeight: 400, textTransform: 'none', letterSpacing: 0, color: 'var(--ink-muted)', fontSize: '0.75rem' }}>(opzionale)</span></label>
<input type="text" value={form.topic_hint} onChange={e => setForm(p => ({ ...p, topic_hint: e.target.value }))}
placeholder="Es. ricetta pasta, trend primavera, lancio prodotto…" style={inputStyle}
onFocus={e => e.target.style.borderColor = 'var(--ink)'} onBlur={e => e.target.style.borderColor = 'var(--border)'} />
</div>
{/* Affiliates toggle */}
<div style={{ display: 'flex', alignItems: 'center', gap: '0.75rem' }}>
<button type="button" onClick={() => setForm(p => ({ ...p, include_affiliates: !p.include_affiliates }))} style={{
width: 44, height: 24, borderRadius: 12, border: 'none', cursor: 'pointer',
backgroundColor: form.include_affiliates ? 'var(--accent)' : 'var(--border-strong)',
position: 'relative', transition: 'background-color 0.2s',
flexShrink: 0,
}}>
<span style={{
position: 'absolute', top: 2, left: form.include_affiliates ? 22 : 2,
width: 20, height: 20, borderRadius: '50%', backgroundColor: 'white',
transition: 'left 0.2s',
}} />
</button>
<span style={{ fontSize: '0.875rem', color: 'var(--ink)' }}>Includi link affiliati</span>
</div>
<button type="submit" disabled={loading || characters.length === 0} style={{
...btnPrimary, width: '100%', justifyContent: 'center', display: 'flex', alignItems: 'center', gap: '0.5rem',
opacity: (loading || characters.length === 0) ? 0.6 : 1,
padding: '0.75rem',
}}>
{loading ? (
<span className="flex items-center justify-center gap-2">
<span className="animate-spin rounded-full h-4 w-4 border-2 border-white border-t-transparent" />
Generazione in corso...
</span>
) : 'Genera'}
<>
<span style={{ width: 16, height: 16, border: '2px solid rgba(255,255,255,0.3)', borderTopColor: 'white', borderRadius: '50%', animation: 'spin 0.8s linear infinite', display: 'inline-block' }} />
Generazione in corso…
</>
) : ' Genera'}
</button>
</form>
</div>
{/* Preview */}
<div style={cardStyle}>
<h3 className="font-semibold text-sm uppercase tracking-wider mb-4" style={{ color: 'var(--muted)' }}>
Ultimo Contenuto Generato
</h3>
{/* Preview panel */}
<div style={{ backgroundColor: 'var(--surface)', border: '1px solid var(--border)', borderTop: '4px solid var(--border-strong)', padding: '1.5rem' }}>
<span style={labelStyle}>Contenuto Generato</span>
{loading ? (
<div className="flex flex-col items-center justify-center py-16">
<div className="animate-spin rounded-full h-8 w-8 border-2 border-t-transparent" style={{ borderColor: 'var(--coral)' }} />
<p className="text-sm mt-3" style={{ color: 'var(--muted)' }}>Generazione in corso...</p>
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', padding: '4rem 0' }}>
<div style={{ width: 32, height: 32, border: '2px solid var(--border)', borderTopColor: 'var(--accent)', borderRadius: '50%', animation: 'spin 0.8s linear infinite', marginBottom: '1rem' }} />
<p style={{ fontSize: '0.875rem', color: 'var(--ink-muted)' }}>Generazione in corso…</p>
</div>
) : generated ? (
<div className="space-y-4">
<div className="flex items-center gap-2">
<span className={`text-xs px-2 py-0.5 rounded-full ${
generated.status === 'approved' ? 'bg-emerald-50 text-emerald-600' :
generated.status === 'published' ? 'bg-blue-50 text-blue-600' :
'bg-amber-50 text-amber-600'
}`}>
{generated.status === 'approved' ? 'Approvato' :
generated.status === 'published' ? 'Pubblicato' : 'Bozza'}
</span>
<span className="text-xs px-2 py-0.5 rounded-full bg-slate-100 text-slate-600">
{platformLabels[generated.platform_hint] || generated.platform_hint}
</span>
<div style={{ marginTop: '1rem' }}>
{/* Status + platform badges */}
<div style={{ display: 'flex', gap: '0.4rem', flexWrap: 'wrap', marginBottom: '1rem' }}>
{(() => { const sc = STATUS_COLORS[generated.status] || STATUS_COLORS.draft; return (
<span style={{ fontSize: '0.72rem', fontWeight: 700, padding: '0.2rem 0.5rem', backgroundColor: sc.bg, color: sc.color }}>
{STATUS_LABELS[generated.status] || generated.status}
</span>
)})()}
{generated.platform_hint && (
<span style={{ fontSize: '0.72rem', fontWeight: 600, padding: '0.2rem 0.5rem', backgroundColor: 'var(--cream-dark)', color: 'var(--ink-muted)' }}>
{generated.platform_hint}
</span>
)}
</div>
{editing ? (
<div className="space-y-2">
<textarea
value={editText}
onChange={(e) => setEditText(e.target.value)}
rows={8}
className="w-full px-4 py-2.5 rounded-lg text-sm resize-none focus:outline-none"
style={{ border: '1px solid var(--border)', color: 'var(--ink)' }}
/>
<div className="flex gap-2">
<button
onClick={handleSaveEdit}
className="px-3 py-1.5 text-white text-xs rounded-lg"
style={{ backgroundColor: 'var(--coral)' }}
>
Salva
</button>
<button
onClick={() => { setEditing(false); setEditText(generated.text_content || '') }}
className="px-3 py-1.5 bg-slate-100 text-slate-600 text-xs rounded-lg"
>
Annulla
</button>
<div>
<textarea value={editText} onChange={e => setEditText(e.target.value)} rows={8}
style={{ ...inputStyle, resize: 'vertical', lineHeight: 1.6 }}
onFocus={e => e.target.style.borderColor = 'var(--ink)'} onBlur={e => e.target.style.borderColor = 'var(--border)'} />
<div style={{ display: 'flex', gap: '0.5rem', marginTop: '0.5rem' }}>
<button onClick={handleSaveEdit} style={btnPrimary}>Salva</button>
<button onClick={() => { setEditing(false); setEditText(generated.text_content || '') }} style={btnSecondary}>Annulla</button>
</div>
</div>
) : (
<div className="p-4 rounded-lg" style={{ backgroundColor: 'var(--cream)' }}>
<p className="text-sm whitespace-pre-wrap leading-relaxed" style={{ color: 'var(--ink)' }}>
<div style={{ padding: '1rem', backgroundColor: 'var(--cream)', marginBottom: '1rem' }}>
<p style={{ fontSize: '0.875rem', whiteSpace: 'pre-wrap', lineHeight: 1.7, color: 'var(--ink)', margin: 0 }}>
{generated.text_content}
</p>
</div>
)}
{generated.hashtags && generated.hashtags.length > 0 && (
<div>
<p className="text-xs font-medium mb-1.5" style={{ color: 'var(--muted)' }}>Hashtag</p>
<div className="flex flex-wrap gap-1.5">
{generated.hashtags?.length > 0 && (
<div style={{ marginBottom: '1rem' }}>
<span style={{ ...labelStyle, display: 'block', marginBottom: '0.4rem' }}>Hashtag</span>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '0.35rem' }}>
{generated.hashtags.map((tag, i) => (
<span key={i} className="text-xs px-2 py-0.5 rounded" style={{ backgroundColor: '#FFF0EC', color: 'var(--coral)' }}>
<span key={i} style={{ fontSize: '0.78rem', padding: '0.15rem 0.5rem', backgroundColor: 'var(--accent-light)', color: 'var(--accent)' }}>
{tag}
</span>
))}
@@ -293,42 +340,49 @@ export default function ContentPage() {
</div>
)}
<div className="flex items-center gap-2 pt-3 border-t" style={{ borderColor: 'var(--border)' }}>
<div style={{ display: 'flex', gap: '0.5rem', paddingTop: '1rem', borderTop: '1px solid var(--border)', flexWrap: 'wrap' }}>
{generated.status !== 'approved' && (
<button
onClick={handleApprove}
className="text-xs px-3 py-1.5 bg-emerald-50 hover:bg-emerald-100 text-emerald-600 rounded-lg transition-colors font-medium"
>
Approva
</button>
<button onClick={handleApprove} style={{ ...btnPrimary, backgroundColor: 'var(--success)' }}>Approva</button>
)}
{!editing && (
<button
onClick={() => setEditing(true)}
className="text-xs px-3 py-1.5 bg-slate-100 hover:bg-slate-200 text-slate-600 rounded-lg transition-colors"
>
Modifica
</button>
)}
<button
onClick={handleDelete}
className="text-xs px-3 py-1.5 text-red-500 hover:bg-red-50 rounded-lg transition-colors ml-auto"
>
Elimina
</button>
{!editing && <button onClick={() => setEditing(true)} style={btnSecondary}>Modifica</button>}
<button onClick={handleDelete} style={{ ...btnSecondary, marginLeft: 'auto', color: 'var(--error)' }}>Elimina</button>
</div>
</div>
) : (
<div className="flex flex-col items-center justify-center py-16 text-center">
<p className="text-4xl mb-3"></p>
<p className="font-medium" style={{ color: 'var(--ink)' }}>Nessun contenuto generato</p>
<p className="text-sm mt-1" style={{ color: 'var(--muted)' }}>
Compila il form e clicca "Genera"
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', padding: '4rem 1rem', textAlign: 'center' }}>
<div style={{ fontSize: '2.5rem', color: 'var(--border-strong)', marginBottom: '1rem' }}>✦</div>
<p style={{ fontFamily: "'Fraunces', serif", fontSize: '1rem', color: 'var(--ink)', margin: '0 0 0.5rem' }}>Nessun contenuto ancora</p>
<p style={{ fontSize: '0.82rem', color: 'var(--ink-muted)', margin: 0 }}>
Scrivi un brief, seleziona personaggio e piattaforma, poi clicca "Genera"
</p>
</div>
)}
</div>
</div>
<style>{`@keyframes spin { to { transform: rotate(360deg) } }`}</style>
</div>
)
}
const labelStyle = {
fontSize: '0.72rem', fontWeight: 700, letterSpacing: '0.1em',
textTransform: 'uppercase', color: 'var(--ink)',
}
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 btnPrimary = {
display: 'inline-block', padding: '0.55rem 1.1rem',
backgroundColor: 'var(--ink)', color: 'white',
fontFamily: "'DM Sans', sans-serif", fontWeight: 600,
fontSize: '0.875rem', border: 'none', cursor: 'pointer',
}
const btnSecondary = {
...btnPrimary,
backgroundColor: 'var(--cream-dark)', color: 'var(--ink-light)',
}