feat: clickable character cards, default character, rename Archivio → Libreria
- CharacterList: entire card is clickable to enter edit mode - CharacterList: uses ConfirmModal for delete (replaces browser confirm) - CharacterList: action buttons stop propagation to avoid double-nav - ContentPage: auto-selects first active character as default - Rename "Archivio Contenuti" → "Libreria Contenuti" everywhere - Mobile-safe grid for character cards Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,10 +1,13 @@
|
|||||||
import { useState, useEffect } from 'react'
|
import { useState, useEffect } from 'react'
|
||||||
import { Link } from 'react-router-dom'
|
import { Link, useNavigate } from 'react-router-dom'
|
||||||
import { api } from '../api'
|
import { api } from '../api'
|
||||||
|
import ConfirmModal from './ConfirmModal'
|
||||||
|
|
||||||
export default function CharacterList() {
|
export default function CharacterList() {
|
||||||
const [characters, setCharacters] = useState([])
|
const [characters, setCharacters] = useState([])
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
|
const [deleteTarget, setDeleteTarget] = useState(null)
|
||||||
|
const navigate = useNavigate()
|
||||||
|
|
||||||
useEffect(() => { loadCharacters() }, [])
|
useEffect(() => { loadCharacters() }, [])
|
||||||
|
|
||||||
@@ -13,9 +16,10 @@ export default function CharacterList() {
|
|||||||
api.get('/characters/').then(setCharacters).catch(() => {}).finally(() => setLoading(false))
|
api.get('/characters/').then(setCharacters).catch(() => {}).finally(() => setLoading(false))
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleDelete = async (id, name) => {
|
const handleDelete = async () => {
|
||||||
if (!confirm(`Eliminare "${name}"?`)) return
|
if (!deleteTarget) return
|
||||||
await api.delete(`/characters/${id}`)
|
await api.delete(`/characters/${deleteTarget.id}`)
|
||||||
|
setDeleteTarget(null)
|
||||||
loadCharacters()
|
loadCharacters()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -52,20 +56,30 @@ export default function CharacterList() {
|
|||||||
to="/characters/new"
|
to="/characters/new"
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(280px, 1fr))', gap: '1rem' }}>
|
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(min(100%, 280px), 1fr))', gap: '1rem' }}>
|
||||||
{characters.map((c) => (
|
{characters.map((c) => (
|
||||||
<CharacterCard key={c.id} character={c} onDelete={handleDelete} onToggle={handleToggle} />
|
<CharacterCard key={c.id} character={c} onDelete={(id, name) => setDeleteTarget({ id, name })} onToggle={handleToggle} onNavigate={() => navigate(`/characters/${c.id}/edit`)} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
<ConfirmModal
|
||||||
|
open={deleteTarget !== null}
|
||||||
|
title="Elimina personaggio"
|
||||||
|
message={`Sei sicuro di voler eliminare "${deleteTarget?.name}"? Tutti i contenuti associati resteranno ma non potrai più generare con questo personaggio.`}
|
||||||
|
confirmLabel="Elimina"
|
||||||
|
confirmStyle="danger"
|
||||||
|
onConfirm={handleDelete}
|
||||||
|
onCancel={() => setDeleteTarget(null)}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function CharacterCard({ character: c, onDelete, onToggle }) {
|
function CharacterCard({ character: c, onDelete, onToggle, onNavigate }) {
|
||||||
const color = c.visual_style?.primary_color || 'var(--accent)'
|
const color = c.visual_style?.primary_color || 'var(--accent)'
|
||||||
return (
|
return (
|
||||||
<div style={{ backgroundColor: 'var(--surface)', border: '1px solid var(--border)', overflow: 'hidden', transition: 'border-color 0.15s' }}
|
<div style={{ backgroundColor: 'var(--surface)', border: '1px solid var(--border)', overflow: 'hidden', transition: 'border-color 0.15s', cursor: 'pointer' }}
|
||||||
|
onClick={onNavigate}
|
||||||
onMouseEnter={e => e.currentTarget.style.borderColor = 'var(--accent)'}
|
onMouseEnter={e => e.currentTarget.style.borderColor = 'var(--accent)'}
|
||||||
onMouseLeave={e => e.currentTarget.style.borderColor = 'var(--border)'}
|
onMouseLeave={e => e.currentTarget.style.borderColor = 'var(--border)'}
|
||||||
>
|
>
|
||||||
@@ -99,8 +113,7 @@ function CharacterCard({ character: c, onDelete, onToggle }) {
|
|||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div style={{ display: 'flex', gap: '0.5rem', paddingTop: '0.875rem', borderTop: '1px solid var(--border)' }}>
|
<div style={{ display: 'flex', gap: '0.5rem', paddingTop: '0.875rem', borderTop: '1px solid var(--border)' }} onClick={e => e.stopPropagation()}>
|
||||||
<Link to={`/characters/${c.id}/edit`} style={btnSmall}>Modifica</Link>
|
|
||||||
<button onClick={() => onToggle(c)} style={btnSmall}>{c.is_active ? 'Disattiva' : 'Attiva'}</button>
|
<button onClick={() => onToggle(c)} style={btnSmall}>{c.is_active ? 'Disattiva' : 'Attiva'}</button>
|
||||||
<button onClick={() => onDelete(c.id, c.name)} style={{ ...btnSmall, marginLeft: 'auto', color: 'var(--error)' }}>Elimina</button>
|
<button onClick={() => onDelete(c.id, c.name)} style={{ ...btnSmall, marginLeft: 'auto', color: 'var(--error)' }}>Elimina</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -87,12 +87,12 @@ export default function ContentArchive() {
|
|||||||
<div style={{ animation: 'fade-up 0.5s ease-out both' }}>
|
<div style={{ animation: 'fade-up 0.5s ease-out both' }}>
|
||||||
<div style={{ marginBottom: '2rem', display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
<div style={{ marginBottom: '2rem', display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||||
<div>
|
<div>
|
||||||
<span className="editorial-tag">Archivio</span>
|
<span className="editorial-tag">Libreria</span>
|
||||||
<div className="editorial-line" />
|
<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' }}>
|
<h2 style={{ fontFamily: "'Fraunces', serif", fontSize: '1.75rem', fontWeight: 600, letterSpacing: '-0.02em', color: 'var(--ink)', margin: '0.5rem 0 0.3rem' }}>
|
||||||
Archivio Contenuti
|
Libreria Contenuti
|
||||||
</h2>
|
</h2>
|
||||||
<p style={{ fontSize: '0.875rem', color: 'var(--ink-muted)', margin: 0 }}>Tutti i contenuti generati</p>
|
<p style={{ fontSize: '0.875rem', color: 'var(--ink-muted)', margin: 0 }}>I tuoi contenuti generati</p>
|
||||||
</div>
|
</div>
|
||||||
<Link to="/content" style={{ fontSize: '0.8rem', color: 'var(--accent)', textDecoration: 'none', fontWeight: 600 }}>
|
<Link to="/content" style={{ fontSize: '0.8rem', color: 'var(--accent)', textDecoration: 'none', fontWeight: 600 }}>
|
||||||
← Genera nuovo
|
← Genera nuovo
|
||||||
|
|||||||
@@ -69,14 +69,22 @@ export default function ContentPage() {
|
|||||||
api.get('/characters/').then(d => {
|
api.get('/characters/').then(d => {
|
||||||
setCharacters(d)
|
setCharacters(d)
|
||||||
setCharsLoading(false)
|
setCharsLoading(false)
|
||||||
|
|
||||||
|
// Auto-select default character (first active one)
|
||||||
|
const activeChars = d.filter(c => c.is_active)
|
||||||
|
const defaultChar = activeChars.length > 0 ? String(activeChars[0].id) : ''
|
||||||
|
|
||||||
// One-click flow: pre-fill from URL params
|
// One-click flow: pre-fill from URL params
|
||||||
const urlTopic = searchParams.get('topic')
|
const urlTopic = searchParams.get('topic')
|
||||||
const urlCharacter = searchParams.get('character')
|
const urlCharacter = searchParams.get('character')
|
||||||
if (urlTopic && d.length > 0) {
|
if (urlTopic && d.length > 0) {
|
||||||
const charId = urlCharacter || String(d[0].id)
|
const charId = urlCharacter || defaultChar
|
||||||
setForm(prev => ({ ...prev, character_id: charId, topic_hint: urlTopic }))
|
setForm(prev => ({ ...prev, character_id: charId, topic_hint: urlTopic }))
|
||||||
autoGenerateRef.current = true
|
autoGenerateRef.current = true
|
||||||
setSearchParams({}, { replace: true }) // clean URL
|
setSearchParams({}, { replace: true })
|
||||||
|
} else if (defaultChar) {
|
||||||
|
// Pre-select default character
|
||||||
|
setForm(prev => ({ ...prev, character_id: prev.character_id || defaultChar }))
|
||||||
}
|
}
|
||||||
}).catch(() => setCharsLoading(false))
|
}).catch(() => setCharsLoading(false))
|
||||||
}, [])
|
}, [])
|
||||||
@@ -205,7 +213,7 @@ export default function ContentPage() {
|
|||||||
Definisci un brief editoriale, scegli piattaforma e tipo, poi genera. L'AI terrà conto del tono e dei topic del personaggio selezionato.
|
Definisci un brief editoriale, scegli piattaforma e tipo, poi genera. L'AI terrà conto del tono e dei topic del personaggio selezionato.
|
||||||
</p>
|
</p>
|
||||||
<Link to="/content/archive" style={{ fontSize: '0.8rem', color: 'var(--accent)', whiteSpace: 'nowrap', textDecoration: 'none', fontWeight: 600 }}>
|
<Link to="/content/archive" style={{ fontSize: '0.8rem', color: 'var(--accent)', whiteSpace: 'nowrap', textDecoration: 'none', fontWeight: 600 }}>
|
||||||
Archivio →
|
Libreria →
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user