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 { Link } from 'react-router-dom'
|
||||
import { Link, useNavigate } from 'react-router-dom'
|
||||
import { api } from '../api'
|
||||
import ConfirmModal from './ConfirmModal'
|
||||
|
||||
export default function CharacterList() {
|
||||
const [characters, setCharacters] = useState([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [deleteTarget, setDeleteTarget] = useState(null)
|
||||
const navigate = useNavigate()
|
||||
|
||||
useEffect(() => { loadCharacters() }, [])
|
||||
|
||||
@@ -13,9 +16,10 @@ export default function CharacterList() {
|
||||
api.get('/characters/').then(setCharacters).catch(() => {}).finally(() => setLoading(false))
|
||||
}
|
||||
|
||||
const handleDelete = async (id, name) => {
|
||||
if (!confirm(`Eliminare "${name}"?`)) return
|
||||
await api.delete(`/characters/${id}`)
|
||||
const handleDelete = async () => {
|
||||
if (!deleteTarget) return
|
||||
await api.delete(`/characters/${deleteTarget.id}`)
|
||||
setDeleteTarget(null)
|
||||
loadCharacters()
|
||||
}
|
||||
|
||||
@@ -52,20 +56,30 @@ export default function CharacterList() {
|
||||
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) => (
|
||||
<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>
|
||||
)}
|
||||
<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>
|
||||
)
|
||||
}
|
||||
|
||||
function CharacterCard({ character: c, onDelete, onToggle }) {
|
||||
function CharacterCard({ character: c, onDelete, onToggle, onNavigate }) {
|
||||
const color = c.visual_style?.primary_color || 'var(--accent)'
|
||||
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)'}
|
||||
onMouseLeave={e => e.currentTarget.style.borderColor = 'var(--border)'}
|
||||
>
|
||||
@@ -99,8 +113,7 @@ function CharacterCard({ character: c, onDelete, onToggle }) {
|
||||
</p>
|
||||
)}
|
||||
|
||||
<div style={{ display: 'flex', gap: '0.5rem', paddingTop: '0.875rem', borderTop: '1px solid var(--border)' }}>
|
||||
<Link to={`/characters/${c.id}/edit`} style={btnSmall}>Modifica</Link>
|
||||
<div style={{ display: 'flex', gap: '0.5rem', paddingTop: '0.875rem', borderTop: '1px solid var(--border)' }} onClick={e => e.stopPropagation()}>
|
||||
<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>
|
||||
</div>
|
||||
|
||||
@@ -87,12 +87,12 @@ export default function ContentArchive() {
|
||||
<div style={{ animation: 'fade-up 0.5s ease-out both' }}>
|
||||
<div style={{ marginBottom: '2rem', display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
<div>
|
||||
<span className="editorial-tag">Archivio</span>
|
||||
<span className="editorial-tag">Libreria</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' }}>
|
||||
Archivio Contenuti
|
||||
Libreria Contenuti
|
||||
</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>
|
||||
<Link to="/content" style={{ fontSize: '0.8rem', color: 'var(--accent)', textDecoration: 'none', fontWeight: 600 }}>
|
||||
← Genera nuovo
|
||||
|
||||
@@ -69,14 +69,22 @@ export default function ContentPage() {
|
||||
api.get('/characters/').then(d => {
|
||||
setCharacters(d)
|
||||
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
|
||||
const urlTopic = searchParams.get('topic')
|
||||
const urlCharacter = searchParams.get('character')
|
||||
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 }))
|
||||
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))
|
||||
}, [])
|
||||
@@ -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.
|
||||
</p>
|
||||
<Link to="/content/archive" style={{ fontSize: '0.8rem', color: 'var(--accent)', whiteSpace: 'nowrap', textDecoration: 'none', fontWeight: 600 }}>
|
||||
Archivio →
|
||||
Libreria →
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user