feat: group posts by batch in archive + fullscreen confirm modal
Backend: - Add batch_id column to Post model (UUID, groups posts from same generation) - Set batch_id in /generate endpoint for all posts in same request Frontend: - ContentArchive: group posts by batch_id into single cards with platform tabs - Character name at top, platform tabs below, status badge, text preview - Click platform tab to switch between variants of same content - ConfirmModal: render via React portal to document.body for true fullscreen overlay - Add box-shadow and higher z-index for better visual separation Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -64,6 +64,7 @@ class Post(Base):
|
|||||||
__tablename__ = "posts"
|
__tablename__ = "posts"
|
||||||
|
|
||||||
id = Column(Integer, primary_key=True, index=True)
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
|
batch_id = Column(String(36), nullable=True, index=True) # groups posts generated together
|
||||||
character_id = Column(Integer, ForeignKey("characters.id"), nullable=False)
|
character_id = Column(Integer, ForeignKey("characters.id"), nullable=False)
|
||||||
content_type = Column(String(20), default="text") # text, image, video, carousel
|
content_type = Column(String(20), default="text") # text, image, video, carousel
|
||||||
text_content = Column(Text)
|
text_content = Column(Text)
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
Handles post generation via LLM, image generation, and CRUD operations on posts.
|
Handles post generation via LLM, image generation, and CRUD operations on posts.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import uuid
|
||||||
from datetime import date, datetime
|
from datetime import date, datetime
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||||
@@ -132,7 +133,8 @@ def generate_content(
|
|||||||
for link in links
|
for link in links
|
||||||
]
|
]
|
||||||
|
|
||||||
# Generate one post per platform
|
# Generate one post per platform, all sharing the same batch_id
|
||||||
|
batch_id = str(uuid.uuid4())
|
||||||
posts_created: list[Post] = []
|
posts_created: list[Post] = []
|
||||||
for platform in platforms:
|
for platform in platforms:
|
||||||
text = generate_post_text(
|
text = generate_post_text(
|
||||||
@@ -152,6 +154,7 @@ def generate_content(
|
|||||||
)
|
)
|
||||||
|
|
||||||
post = Post(
|
post = Post(
|
||||||
|
batch_id=batch_id,
|
||||||
character_id=character.id,
|
character_id=character.id,
|
||||||
user_id=current_user.id,
|
user_id=current_user.id,
|
||||||
content_type=request.content_type,
|
content_type=request.content_type,
|
||||||
|
|||||||
@@ -82,6 +82,7 @@ class PostUpdate(BaseModel):
|
|||||||
|
|
||||||
class PostResponse(BaseModel):
|
class PostResponse(BaseModel):
|
||||||
id: int
|
id: int
|
||||||
|
batch_id: Optional[str] = None
|
||||||
character_id: int
|
character_id: int
|
||||||
content_type: str
|
content_type: str
|
||||||
text_content: Optional[str] = None
|
text_content: Optional[str] = None
|
||||||
|
|||||||
@@ -1,11 +1,16 @@
|
|||||||
import { useEffect } from 'react'
|
import { useEffect } from 'react'
|
||||||
|
import { createPortal } from 'react-dom'
|
||||||
|
|
||||||
export default function ConfirmModal({ open, title, message, confirmLabel = 'Conferma', cancelLabel = 'Annulla', confirmStyle = 'danger', onConfirm, onCancel }) {
|
export default function ConfirmModal({ open, title, message, confirmLabel = 'Conferma', cancelLabel = 'Annulla', confirmStyle = 'danger', onConfirm, onCancel }) {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!open) return
|
if (!open) return
|
||||||
const onKey = (e) => { if (e.key === 'Escape') onCancel() }
|
const onKey = (e) => { if (e.key === 'Escape') onCancel() }
|
||||||
document.addEventListener('keydown', onKey)
|
document.addEventListener('keydown', onKey)
|
||||||
return () => document.removeEventListener('keydown', onKey)
|
document.body.style.overflow = 'hidden'
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('keydown', onKey)
|
||||||
|
document.body.style.overflow = ''
|
||||||
|
}
|
||||||
}, [open, onCancel])
|
}, [open, onCancel])
|
||||||
|
|
||||||
if (!open) return null
|
if (!open) return null
|
||||||
@@ -17,24 +22,25 @@ export default function ConfirmModal({ open, title, message, confirmLabel = 'Con
|
|||||||
}
|
}
|
||||||
const cc = confirmColors[confirmStyle] || confirmColors.danger
|
const cc = confirmColors[confirmStyle] || confirmColors.danger
|
||||||
|
|
||||||
return (
|
return createPortal(
|
||||||
<div style={{ position: 'fixed', inset: 0, zIndex: 9999, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
<div style={{ position: 'fixed', top: 0, left: 0, right: 0, bottom: 0, zIndex: 99999, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||||
<div onClick={onCancel} style={{ position: 'absolute', inset: 0, backgroundColor: 'rgba(26,26,26,0.4)', backdropFilter: 'blur(2px)' }} />
|
<div onClick={onCancel} style={{ position: 'fixed', top: 0, left: 0, right: 0, bottom: 0, backgroundColor: 'rgba(26,26,26,0.5)', backdropFilter: 'blur(3px)' }} />
|
||||||
<div style={{
|
<div style={{
|
||||||
position: 'relative', backgroundColor: 'var(--surface)', border: '1px solid var(--border)',
|
position: 'relative', backgroundColor: 'var(--surface, #fff)', border: '1px solid var(--border, #E5E0D8)',
|
||||||
borderTop: '4px solid var(--accent)', padding: '2rem', maxWidth: 420, width: '90%',
|
borderTop: '4px solid var(--accent, #E85A4F)', padding: '2rem', maxWidth: 420, width: '90%',
|
||||||
|
boxShadow: '0 20px 60px rgba(0,0,0,0.15)',
|
||||||
animation: 'fade-up 0.2s ease-out both',
|
animation: 'fade-up 0.2s ease-out both',
|
||||||
}}>
|
}}>
|
||||||
<h3 style={{ fontFamily: "'Fraunces', serif", fontSize: '1.15rem', fontWeight: 600, color: 'var(--ink)', margin: '0 0 0.5rem' }}>
|
<h3 style={{ fontFamily: "'Fraunces', serif", fontSize: '1.15rem', fontWeight: 600, color: 'var(--ink, #1A1A1A)', margin: '0 0 0.5rem' }}>
|
||||||
{title}
|
{title}
|
||||||
</h3>
|
</h3>
|
||||||
<p style={{ fontSize: '0.875rem', color: 'var(--ink-light)', lineHeight: 1.6, margin: '0 0 1.5rem' }}>
|
<p style={{ fontSize: '0.875rem', color: 'var(--ink-light, #4A4A4A)', lineHeight: 1.6, margin: '0 0 1.5rem' }}>
|
||||||
{message}
|
{message}
|
||||||
</p>
|
</p>
|
||||||
<div style={{ display: 'flex', gap: '0.5rem', justifyContent: 'flex-end' }}>
|
<div style={{ display: 'flex', gap: '0.5rem', justifyContent: 'flex-end' }}>
|
||||||
<button onClick={onCancel} style={{
|
<button onClick={onCancel} style={{
|
||||||
padding: '0.5rem 1rem', fontSize: '0.85rem', fontWeight: 600, fontFamily: "'DM Sans', sans-serif",
|
padding: '0.5rem 1rem', fontSize: '0.85rem', fontWeight: 600, fontFamily: "'DM Sans', sans-serif",
|
||||||
backgroundColor: 'var(--cream-dark)', color: 'var(--ink-light)', border: 'none', cursor: 'pointer',
|
backgroundColor: 'var(--cream-dark, #F5F0E8)', color: 'var(--ink-light, #4A4A4A)', border: 'none', cursor: 'pointer',
|
||||||
}}>
|
}}>
|
||||||
{cancelLabel}
|
{cancelLabel}
|
||||||
</button>
|
</button>
|
||||||
@@ -46,6 +52,7 @@ export default function ConfirmModal({ open, title, message, confirmLabel = 'Con
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>,
|
||||||
|
document.body
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,29 +11,43 @@ const statusColors = {
|
|||||||
published: { bg: '#F5F3FF', color: '#6D28D9' },
|
published: { bg: '#F5F3FF', color: '#6D28D9' },
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function groupByBatch(posts) {
|
||||||
|
const groups = []
|
||||||
|
const batchMap = {}
|
||||||
|
for (const post of posts) {
|
||||||
|
const key = post.batch_id || `solo_${post.id}`
|
||||||
|
if (batchMap[key]) {
|
||||||
|
batchMap[key].posts.push(post)
|
||||||
|
} else {
|
||||||
|
const group = { key, posts: [post] }
|
||||||
|
batchMap[key] = group
|
||||||
|
groups.push(group)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return groups
|
||||||
|
}
|
||||||
|
|
||||||
export default function ContentArchive() {
|
export default function ContentArchive() {
|
||||||
const [posts, setPosts] = useState([])
|
const [posts, setPosts] = useState([])
|
||||||
const [characters, setCharacters] = useState([])
|
const [characters, setCharacters] = useState([])
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
const [expandedId, setExpandedId] = useState(null)
|
const [expandedKey, setExpandedKey] = useState(null)
|
||||||
|
const [activePlatform, setActivePlatform] = useState({}) // key → index
|
||||||
const [editingId, setEditingId] = useState(null)
|
const [editingId, setEditingId] = useState(null)
|
||||||
const [editText, setEditText] = useState('')
|
const [editText, setEditText] = useState('')
|
||||||
const [filterCharacter, setFilterCharacter] = useState('')
|
const [filterCharacter, setFilterCharacter] = useState('')
|
||||||
const [filterStatus, setFilterStatus] = useState('')
|
const [filterStatus, setFilterStatus] = useState('')
|
||||||
const [deleteTarget, setDeleteTarget] = useState(null)
|
const [deleteTarget, setDeleteTarget] = useState(null) // { id, batchKey }
|
||||||
|
|
||||||
useEffect(() => { loadData() }, [])
|
useEffect(() => { loadData() }, [])
|
||||||
|
|
||||||
const loadData = async () => {
|
const loadData = async () => {
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
try {
|
try {
|
||||||
const [postsData, charsData] = await Promise.all([
|
const [postsData, charsData] = await Promise.all([api.get('/content/posts'), api.get('/characters/')])
|
||||||
api.get('/content/posts'),
|
|
||||||
api.get('/characters/'),
|
|
||||||
])
|
|
||||||
setPosts(postsData)
|
setPosts(postsData)
|
||||||
setCharacters(charsData)
|
setCharacters(charsData)
|
||||||
} catch { /* silent */ } finally { setLoading(false) }
|
} catch {} finally { setLoading(false) }
|
||||||
}
|
}
|
||||||
|
|
||||||
const getCharacterName = (id) => characters.find(c => c.id === id)?.name || '—'
|
const getCharacterName = (id) => characters.find(c => c.id === id)?.name || '—'
|
||||||
@@ -41,9 +55,16 @@ export default function ContentArchive() {
|
|||||||
const handleApprove = async (postId) => {
|
const handleApprove = async (postId) => {
|
||||||
try { await api.post(`/content/posts/${postId}/approve`); loadData() } catch {}
|
try { await api.post(`/content/posts/${postId}/approve`); loadData() } catch {}
|
||||||
}
|
}
|
||||||
const handleDelete = async (postId) => {
|
|
||||||
try { await api.delete(`/content/posts/${postId}`); setDeleteTarget(null); loadData() } catch {}
|
const handleDelete = async () => {
|
||||||
|
if (!deleteTarget) return
|
||||||
|
try {
|
||||||
|
await api.delete(`/content/posts/${deleteTarget.id}`)
|
||||||
|
setDeleteTarget(null)
|
||||||
|
loadData()
|
||||||
|
} catch {}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleSaveEdit = async (postId) => {
|
const handleSaveEdit = async (postId) => {
|
||||||
try {
|
try {
|
||||||
await api.put(`/content/posts/${postId}`, { text_content: editText })
|
await api.put(`/content/posts/${postId}`, { text_content: editText })
|
||||||
@@ -58,6 +79,8 @@ export default function ContentArchive() {
|
|||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const groups = groupByBatch(filtered)
|
||||||
|
|
||||||
const formatDate = (d) => d ? new Date(d).toLocaleDateString('it-IT', { day: '2-digit', month: 'short', year: 'numeric' }) : '—'
|
const formatDate = (d) => d ? new Date(d).toLocaleDateString('it-IT', { day: '2-digit', month: 'short', year: 'numeric' }) : '—'
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -87,7 +110,7 @@ export default function ContentArchive() {
|
|||||||
{Object.entries(statusLabels).map(([val, label]) => <option key={val} value={val}>{label}</option>)}
|
{Object.entries(statusLabels).map(([val, label]) => <option key={val} value={val}>{label}</option>)}
|
||||||
</select>
|
</select>
|
||||||
<span style={{ fontSize: '0.78rem', color: 'var(--ink-muted)', marginLeft: 'auto' }}>
|
<span style={{ fontSize: '0.78rem', color: 'var(--ink-muted)', marginLeft: 'auto' }}>
|
||||||
{filtered.length} contenut{filtered.length === 1 ? 'o' : 'i'}
|
{groups.length} contenut{groups.length === 1 ? 'o' : 'i'}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -95,7 +118,7 @@ export default function ContentArchive() {
|
|||||||
<div style={{ display: 'flex', justifyContent: 'center', padding: '3rem 0' }}>
|
<div style={{ display: 'flex', justifyContent: 'center', padding: '3rem 0' }}>
|
||||||
<div style={{ width: 32, height: 32, border: '2px solid var(--border)', borderTopColor: 'var(--accent)', borderRadius: '50%', animation: 'spin 0.8s linear infinite' }} />
|
<div style={{ width: 32, height: 32, border: '2px solid var(--border)', borderTopColor: 'var(--accent)', borderRadius: '50%', animation: 'spin 0.8s linear infinite' }} />
|
||||||
</div>
|
</div>
|
||||||
) : filtered.length === 0 ? (
|
) : groups.length === 0 ? (
|
||||||
<div style={{ textAlign: 'center', padding: '4rem 1rem', backgroundColor: 'var(--surface)', border: '1px solid var(--border)' }}>
|
<div style={{ textAlign: 'center', padding: '4rem 1rem', backgroundColor: 'var(--surface)', border: '1px solid var(--border)' }}>
|
||||||
<div style={{ fontSize: '2.5rem', color: 'var(--border-strong)', marginBottom: '1rem' }}>✦</div>
|
<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 trovato</p>
|
<p style={{ fontFamily: "'Fraunces', serif", fontSize: '1rem', color: 'var(--ink)', margin: '0 0 0.5rem' }}>Nessun contenuto trovato</p>
|
||||||
@@ -104,30 +127,48 @@ export default function ContentArchive() {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(340px, 1fr))', gap: '1rem' }}>
|
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(380px, 1fr))', gap: '1rem' }}>
|
||||||
{filtered.map(post => {
|
{groups.map(group => {
|
||||||
const sc = statusColors[post.status] || statusColors.draft
|
const activeIdx = activePlatform[group.key] || 0
|
||||||
const isExpanded = expandedId === post.id
|
const activePost = group.posts[activeIdx] || group.posts[0]
|
||||||
const isEditing = editingId === post.id
|
const sc = statusColors[activePost.status] || statusColors.draft
|
||||||
|
const isExpanded = expandedKey === group.key
|
||||||
|
const isEditing = editingId === activePost.id
|
||||||
|
const hasMultiple = group.posts.length > 1
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={post.id} style={{ backgroundColor: 'var(--surface)', border: '1px solid var(--border)', borderTop: `3px solid ${sc.color}`, cursor: 'pointer', transition: 'border-color 0.15s' }}
|
<div key={group.key} style={{ backgroundColor: 'var(--surface)', border: '1px solid var(--border)', borderTop: `3px solid ${sc.color}`, cursor: 'pointer', transition: 'border-color 0.15s' }}
|
||||||
onClick={() => { if (!isEditing) setExpandedId(isExpanded ? null : post.id) }}>
|
onClick={() => { if (!isEditing) setExpandedKey(isExpanded ? null : group.key) }}>
|
||||||
<div style={{ padding: '1.25rem' }}>
|
<div style={{ padding: '1.25rem' }}>
|
||||||
{/* Header */}
|
|
||||||
<div style={{ display: 'flex', gap: '0.4rem', flexWrap: 'wrap', marginBottom: '0.75rem' }}>
|
{/* Character name */}
|
||||||
<span style={{ fontSize: '0.72rem', fontWeight: 700, padding: '0.15rem 0.5rem', backgroundColor: sc.bg, color: sc.color }}>
|
<p style={{ fontSize: '0.82rem', fontWeight: 700, color: 'var(--ink)', margin: '0 0 0.5rem' }}>
|
||||||
{statusLabels[post.status] || post.status}
|
{getCharacterName(activePost.character_id)}
|
||||||
</span>
|
</p>
|
||||||
{post.platform_hint && (
|
|
||||||
<span style={{ fontSize: '0.72rem', fontWeight: 500, padding: '0.15rem 0.5rem', backgroundColor: 'var(--cream-dark)', color: 'var(--ink-muted)' }}>
|
{/* Platform tabs */}
|
||||||
{post.platform_hint}
|
<div style={{ display: 'flex', gap: '0', marginBottom: '0.5rem', borderBottom: hasMultiple ? '2px solid var(--border)' : 'none' }}>
|
||||||
</span>
|
{group.posts.map((p, i) => (
|
||||||
)}
|
<button key={p.id} onClick={e => { e.stopPropagation(); setActivePlatform(prev => ({ ...prev, [group.key]: i })); setEditing && setEditingId(null) }}
|
||||||
|
style={{
|
||||||
|
padding: '0.35rem 0.75rem', fontSize: '0.75rem', fontWeight: activeIdx === i ? 700 : 400,
|
||||||
|
fontFamily: "'DM Sans', sans-serif", border: 'none', cursor: 'pointer',
|
||||||
|
backgroundColor: activeIdx === i ? 'var(--surface)' : 'transparent',
|
||||||
|
color: activeIdx === i ? 'var(--ink)' : 'var(--ink-muted)',
|
||||||
|
borderBottom: hasMultiple ? (activeIdx === i ? '2px solid var(--accent)' : '2px solid transparent') : 'none',
|
||||||
|
marginBottom: hasMultiple ? '-2px' : 0, transition: 'all 0.15s',
|
||||||
|
}}>
|
||||||
|
{(p.platform_hint || 'post').charAt(0).toUpperCase() + (p.platform_hint || 'post').slice(1)}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p style={{ fontSize: '0.78rem', fontWeight: 600, color: 'var(--ink-light)', margin: '0 0 0.5rem' }}>
|
{/* Status badge */}
|
||||||
{getCharacterName(post.character_id)}
|
<div style={{ marginBottom: '0.75rem' }}>
|
||||||
</p>
|
<span style={{ fontSize: '0.7rem', fontWeight: 700, padding: '0.15rem 0.5rem', backgroundColor: sc.bg, color: sc.color }}>
|
||||||
|
{statusLabels[activePost.status] || activePost.status}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Text content */}
|
{/* Text content */}
|
||||||
{isEditing ? (
|
{isEditing ? (
|
||||||
@@ -135,20 +176,20 @@ export default function ContentArchive() {
|
|||||||
<textarea value={editText} onChange={e => setEditText(e.target.value)} rows={6}
|
<textarea value={editText} onChange={e => setEditText(e.target.value)} rows={6}
|
||||||
style={{ ...inputStyle, resize: 'vertical', lineHeight: 1.6, fontSize: '0.85rem' }} />
|
style={{ ...inputStyle, resize: 'vertical', lineHeight: 1.6, fontSize: '0.85rem' }} />
|
||||||
<div style={{ display: 'flex', gap: '0.5rem', marginTop: '0.5rem' }}>
|
<div style={{ display: 'flex', gap: '0.5rem', marginTop: '0.5rem' }}>
|
||||||
<button onClick={() => handleSaveEdit(post.id)} style={btnPrimary}>Salva</button>
|
<button onClick={() => handleSaveEdit(activePost.id)} style={btnPrimary}>Salva</button>
|
||||||
<button onClick={() => setEditingId(null)} style={btnSecondary}>Annulla</button>
|
<button onClick={() => setEditingId(null)} style={btnSecondary}>Annulla</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<p style={{ fontSize: '0.85rem', color: 'var(--ink)', lineHeight: 1.6, margin: 0, whiteSpace: isExpanded ? 'pre-wrap' : 'normal', overflow: isExpanded ? 'visible' : 'hidden', display: isExpanded ? 'block' : '-webkit-box', WebkitLineClamp: isExpanded ? 'unset' : 3, WebkitBoxOrient: 'vertical' }}>
|
<p style={{ fontSize: '0.85rem', color: 'var(--ink)', lineHeight: 1.6, margin: 0, whiteSpace: isExpanded ? 'pre-wrap' : 'normal', overflow: isExpanded ? 'visible' : 'hidden', display: isExpanded ? 'block' : '-webkit-box', WebkitLineClamp: isExpanded ? 'unset' : 3, WebkitBoxOrient: 'vertical' }}>
|
||||||
{post.text_content || '(nessun testo)'}
|
{activePost.text_content || '(nessun testo)'}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Hashtags when expanded */}
|
{/* Hashtags when expanded */}
|
||||||
{isExpanded && !isEditing && post.hashtags?.length > 0 && (
|
{isExpanded && !isEditing && activePost.hashtags?.length > 0 && (
|
||||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '0.3rem', marginTop: '0.75rem' }}>
|
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '0.3rem', marginTop: '0.75rem' }}>
|
||||||
{post.hashtags.map((tag, i) => (
|
{activePost.hashtags.map((tag, i) => (
|
||||||
<span key={i} style={{ fontSize: '0.72rem', padding: '0.1rem 0.4rem', backgroundColor: 'var(--accent-light)', color: 'var(--accent)' }}>
|
<span key={i} style={{ fontSize: '0.72rem', padding: '0.1rem 0.4rem', backgroundColor: 'var(--accent-light)', color: 'var(--accent)' }}>
|
||||||
{tag}
|
{tag}
|
||||||
</span>
|
</span>
|
||||||
@@ -158,21 +199,21 @@ export default function ContentArchive() {
|
|||||||
|
|
||||||
{/* Footer */}
|
{/* Footer */}
|
||||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginTop: '0.75rem', paddingTop: '0.75rem', borderTop: '1px solid var(--border)' }}>
|
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginTop: '0.75rem', paddingTop: '0.75rem', borderTop: '1px solid var(--border)' }}>
|
||||||
<span style={{ fontSize: '0.72rem', color: 'var(--ink-muted)' }}>{formatDate(post.created_at)}</span>
|
<span style={{ fontSize: '0.72rem', color: 'var(--ink-muted)' }}>{formatDate(activePost.created_at)}</span>
|
||||||
{post.hashtags?.length > 0 && (
|
{activePost.hashtags?.length > 0 && (
|
||||||
<span style={{ fontSize: '0.72rem', color: 'var(--ink-muted)' }}>{post.hashtags.length} hashtag</span>
|
<span style={{ fontSize: '0.72rem', color: 'var(--ink-muted)' }}>{activePost.hashtags.length} hashtag</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Actions */}
|
{/* Actions */}
|
||||||
<div style={{ display: 'flex', gap: '0.5rem', marginTop: '0.75rem', paddingTop: '0.75rem', borderTop: '1px solid var(--border)' }}
|
<div style={{ display: 'flex', gap: '0.5rem', marginTop: '0.75rem', paddingTop: '0.75rem', borderTop: '1px solid var(--border)' }}
|
||||||
onClick={e => e.stopPropagation()}>
|
onClick={e => e.stopPropagation()}>
|
||||||
{post.status === 'draft' && (
|
{activePost.status === 'draft' && (
|
||||||
<button onClick={() => handleApprove(post.id)} style={{ ...btnSmall, backgroundColor: 'var(--success-light)', color: 'var(--success)' }}>Approva</button>
|
<button onClick={() => handleApprove(activePost.id)} style={{ ...btnSmall, backgroundColor: 'var(--success-light)', color: 'var(--success)' }}>Approva</button>
|
||||||
)}
|
)}
|
||||||
<button onClick={() => { setEditingId(post.id); setEditText(post.text_content || ''); setExpandedId(post.id) }}
|
<button onClick={() => { setEditingId(activePost.id); setEditText(activePost.text_content || ''); setExpandedKey(group.key) }}
|
||||||
style={{ ...btnSmall, backgroundColor: 'var(--cream-dark)', color: 'var(--ink-light)' }}>Modifica</button>
|
style={{ ...btnSmall, backgroundColor: 'var(--cream-dark)', color: 'var(--ink-light)' }}>Modifica</button>
|
||||||
<button onClick={() => setDeleteTarget(post.id)}
|
<button onClick={() => setDeleteTarget({ id: activePost.id, batchKey: group.key })}
|
||||||
style={{ ...btnSmall, color: 'var(--error)', backgroundColor: 'transparent', marginLeft: 'auto' }}>Elimina</button>
|
style={{ ...btnSmall, color: 'var(--error)', backgroundColor: 'transparent', marginLeft: 'auto' }}>Elimina</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -181,6 +222,7 @@ export default function ContentArchive() {
|
|||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<ConfirmModal
|
<ConfirmModal
|
||||||
open={deleteTarget !== null}
|
open={deleteTarget !== null}
|
||||||
title="Elimina contenuto"
|
title="Elimina contenuto"
|
||||||
@@ -188,7 +230,7 @@ export default function ContentArchive() {
|
|||||||
confirmLabel="Elimina"
|
confirmLabel="Elimina"
|
||||||
cancelLabel="Annulla"
|
cancelLabel="Annulla"
|
||||||
confirmStyle="danger"
|
confirmStyle="danger"
|
||||||
onConfirm={() => handleDelete(deleteTarget)}
|
onConfirm={handleDelete}
|
||||||
onCancel={() => setDeleteTarget(null)}
|
onCancel={() => setDeleteTarget(null)}
|
||||||
/>
|
/>
|
||||||
<style>{`@keyframes spin { to { transform: rotate(360deg) } }`}</style>
|
<style>{`@keyframes spin { to { transform: rotate(360deg) } }`}</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user