feat: multi-platform generation + custom confirm modal + per-platform tabs
Backend: - /generate now returns array of posts (one per platform selected) - Each post generated with platform-specific LLM prompt and char limits - Monthly counter incremented by number of platforms Frontend: - ConfirmModal: reusable Editorial Fresh modal replaces ugly browser confirm() - ContentPage: platform tabs when multiple posts, switch between variants - ContentPage: generatedPosts array state replaces single generated - ContentArchive: uses ConfirmModal for delete confirmation - Platform chips filtered by plan (Freemium: IG/FB only) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -45,14 +45,13 @@ def _get_setting(db: Session, key: str, user_id: int = None) -> str | None:
|
|||||||
return setting.value
|
return setting.value
|
||||||
|
|
||||||
|
|
||||||
@router.post("/generate", response_model=PostResponse)
|
@router.post("/generate", response_model=list[PostResponse])
|
||||||
def generate_content(
|
def generate_content(
|
||||||
request: GenerateContentRequest,
|
request: GenerateContentRequest,
|
||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
current_user: User = Depends(get_current_user),
|
current_user: User = Depends(get_current_user),
|
||||||
):
|
):
|
||||||
"""Generate content for a character using LLM."""
|
"""Generate content for a character using LLM. One post per platform."""
|
||||||
# Validate character belongs to user
|
|
||||||
character = (
|
character = (
|
||||||
db.query(Character)
|
db.query(Character)
|
||||||
.filter(Character.id == request.character_id, Character.user_id == current_user.id)
|
.filter(Character.id == request.character_id, Character.user_id == current_user.id)
|
||||||
@@ -61,6 +60,9 @@ def generate_content(
|
|||||||
if not character:
|
if not character:
|
||||||
raise HTTPException(status_code=404, detail="Character not found")
|
raise HTTPException(status_code=404, detail="Character not found")
|
||||||
|
|
||||||
|
# Determine platforms to generate for
|
||||||
|
platforms = request.platforms if request.platforms else [request.platform]
|
||||||
|
|
||||||
# Check monthly post limit
|
# Check monthly post limit
|
||||||
first_of_month = date.today().replace(day=1)
|
first_of_month = date.today().replace(day=1)
|
||||||
if current_user.posts_reset_date != first_of_month:
|
if current_user.posts_reset_date != first_of_month:
|
||||||
@@ -68,11 +70,20 @@ def generate_content(
|
|||||||
current_user.posts_reset_date = first_of_month
|
current_user.posts_reset_date = first_of_month
|
||||||
db.commit()
|
db.commit()
|
||||||
|
|
||||||
allowed, msg = check_limit(current_user, "posts_per_month", current_user.posts_generated_this_month or 0)
|
current_count = current_user.posts_generated_this_month or 0
|
||||||
|
allowed, msg = check_limit(current_user, "posts_per_month", current_count)
|
||||||
if not allowed:
|
if not allowed:
|
||||||
raise HTTPException(status_code=403, detail={"message": msg, "upgrade_required": True})
|
raise HTTPException(status_code=403, detail={"message": msg, "upgrade_required": True})
|
||||||
|
|
||||||
# Get LLM settings (user-specific first, then global)
|
# Also check if we have room for all platforms
|
||||||
|
allowed_after, msg_after = check_limit(current_user, "posts_per_month", current_count + len(platforms) - 1)
|
||||||
|
if not allowed_after:
|
||||||
|
raise HTTPException(status_code=403, detail={
|
||||||
|
"message": f"Non hai abbastanza post rimanenti per generare su {len(platforms)} piattaforme. {msg_after}",
|
||||||
|
"upgrade_required": True,
|
||||||
|
})
|
||||||
|
|
||||||
|
# Get LLM settings
|
||||||
provider_name = request.provider or _get_setting(db, "llm_provider", current_user.id)
|
provider_name = request.provider or _get_setting(db, "llm_provider", current_user.id)
|
||||||
api_key = _get_setting(db, "llm_api_key", current_user.id)
|
api_key = _get_setting(db, "llm_api_key", current_user.id)
|
||||||
model = request.model or _get_setting(db, "llm_model", current_user.id)
|
model = request.model or _get_setting(db, "llm_model", current_user.id)
|
||||||
@@ -94,7 +105,6 @@ def generate_content(
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
# Build character dict for content service
|
|
||||||
char_dict = {
|
char_dict = {
|
||||||
"name": character.name,
|
"name": character.name,
|
||||||
"niche": character.niche,
|
"niche": character.niche,
|
||||||
@@ -102,22 +112,11 @@ def generate_content(
|
|||||||
"tone": character.tone or "professional",
|
"tone": character.tone or "professional",
|
||||||
}
|
}
|
||||||
|
|
||||||
# Create LLM provider and generate text
|
|
||||||
base_url = _get_setting(db, "llm_base_url", current_user.id)
|
base_url = _get_setting(db, "llm_base_url", current_user.id)
|
||||||
llm = get_llm_provider(provider_name, api_key, model, base_url=base_url)
|
llm = get_llm_provider(provider_name, api_key, model, base_url=base_url)
|
||||||
text = generate_post_text(
|
|
||||||
character=char_dict,
|
|
||||||
llm_provider=llm,
|
|
||||||
platform=request.effective_platform,
|
|
||||||
topic_hint=request.topic_hint,
|
|
||||||
brief=request.brief,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Generate hashtags
|
# Preload affiliate links once
|
||||||
hashtags = generate_hashtags(text, llm, request.effective_platform)
|
affiliate_link_dicts: list[dict] = []
|
||||||
|
|
||||||
# Handle affiliate links
|
|
||||||
affiliate_links_used: list[dict] = []
|
|
||||||
if request.include_affiliates:
|
if request.include_affiliates:
|
||||||
links = (
|
links = (
|
||||||
db.query(AffiliateLink)
|
db.query(AffiliateLink)
|
||||||
@@ -128,39 +127,51 @@ def generate_content(
|
|||||||
)
|
)
|
||||||
.all()
|
.all()
|
||||||
)
|
)
|
||||||
if links:
|
affiliate_link_dicts = [
|
||||||
link_dicts = [
|
{"url": link.url, "label": link.name, "keywords": link.topics or []}
|
||||||
{
|
for link in links
|
||||||
"url": link.url,
|
]
|
||||||
"label": link.name,
|
|
||||||
"keywords": link.topics or [],
|
# Generate one post per platform
|
||||||
}
|
posts_created: list[Post] = []
|
||||||
for link in links
|
for platform in platforms:
|
||||||
]
|
text = generate_post_text(
|
||||||
|
character=char_dict,
|
||||||
|
llm_provider=llm,
|
||||||
|
platform=platform,
|
||||||
|
topic_hint=request.topic_hint,
|
||||||
|
brief=request.brief,
|
||||||
|
)
|
||||||
|
|
||||||
|
hashtags = generate_hashtags(text, llm, platform)
|
||||||
|
|
||||||
|
affiliate_links_used: list[dict] = []
|
||||||
|
if affiliate_link_dicts:
|
||||||
text, affiliate_links_used = inject_affiliate_links(
|
text, affiliate_links_used = inject_affiliate_links(
|
||||||
text, link_dicts, character.topics or []
|
text, affiliate_link_dicts, character.topics or []
|
||||||
)
|
)
|
||||||
|
|
||||||
# Create post record
|
post = Post(
|
||||||
post = Post(
|
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,
|
text_content=text,
|
||||||
text_content=text,
|
hashtags=hashtags,
|
||||||
hashtags=hashtags,
|
affiliate_links_used=affiliate_links_used,
|
||||||
affiliate_links_used=affiliate_links_used,
|
llm_provider=provider_name,
|
||||||
llm_provider=provider_name,
|
llm_model=model,
|
||||||
llm_model=model,
|
platform_hint=platform,
|
||||||
platform_hint=request.platform,
|
status="draft",
|
||||||
status="draft",
|
)
|
||||||
)
|
db.add(post)
|
||||||
db.add(post)
|
posts_created.append(post)
|
||||||
|
|
||||||
# Increment monthly counter
|
# Increment monthly counter by number of posts generated
|
||||||
current_user.posts_generated_this_month = (current_user.posts_generated_this_month or 0) + 1
|
current_user.posts_generated_this_month = current_count + len(platforms)
|
||||||
db.commit()
|
db.commit()
|
||||||
db.refresh(post)
|
for post in posts_created:
|
||||||
return post
|
db.refresh(post)
|
||||||
|
return posts_created
|
||||||
|
|
||||||
|
|
||||||
@router.post("/generate-image", response_model=PostResponse)
|
@router.post("/generate-image", response_model=PostResponse)
|
||||||
|
|||||||
51
frontend/src/components/ConfirmModal.jsx
Normal file
51
frontend/src/components/ConfirmModal.jsx
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
import { useEffect } from 'react'
|
||||||
|
|
||||||
|
export default function ConfirmModal({ open, title, message, confirmLabel = 'Conferma', cancelLabel = 'Annulla', confirmStyle = 'danger', onConfirm, onCancel }) {
|
||||||
|
useEffect(() => {
|
||||||
|
if (!open) return
|
||||||
|
const onKey = (e) => { if (e.key === 'Escape') onCancel() }
|
||||||
|
document.addEventListener('keydown', onKey)
|
||||||
|
return () => document.removeEventListener('keydown', onKey)
|
||||||
|
}, [open, onCancel])
|
||||||
|
|
||||||
|
if (!open) return null
|
||||||
|
|
||||||
|
const confirmColors = {
|
||||||
|
danger: { bg: 'var(--error)', color: 'white' },
|
||||||
|
primary: { bg: 'var(--ink)', color: 'white' },
|
||||||
|
success: { bg: 'var(--success)', color: 'white' },
|
||||||
|
}
|
||||||
|
const cc = confirmColors[confirmStyle] || confirmColors.danger
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ position: 'fixed', inset: 0, zIndex: 9999, 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 style={{
|
||||||
|
position: 'relative', backgroundColor: 'var(--surface)', border: '1px solid var(--border)',
|
||||||
|
borderTop: '4px solid var(--accent)', padding: '2rem', maxWidth: 420, width: '90%',
|
||||||
|
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' }}>
|
||||||
|
{title}
|
||||||
|
</h3>
|
||||||
|
<p style={{ fontSize: '0.875rem', color: 'var(--ink-light)', lineHeight: 1.6, margin: '0 0 1.5rem' }}>
|
||||||
|
{message}
|
||||||
|
</p>
|
||||||
|
<div style={{ display: 'flex', gap: '0.5rem', justifyContent: 'flex-end' }}>
|
||||||
|
<button onClick={onCancel} style={{
|
||||||
|
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',
|
||||||
|
}}>
|
||||||
|
{cancelLabel}
|
||||||
|
</button>
|
||||||
|
<button onClick={onConfirm} style={{
|
||||||
|
padding: '0.5rem 1rem', fontSize: '0.85rem', fontWeight: 600, fontFamily: "'DM Sans', sans-serif",
|
||||||
|
backgroundColor: cc.bg, color: cc.color, border: 'none', cursor: 'pointer',
|
||||||
|
}}>
|
||||||
|
{confirmLabel}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useState, useEffect } from 'react'
|
import { useState, useEffect } from 'react'
|
||||||
import { Link } from 'react-router-dom'
|
import { Link } from 'react-router-dom'
|
||||||
import { api } from '../api'
|
import { api } from '../api'
|
||||||
|
import ConfirmModal from './ConfirmModal'
|
||||||
|
|
||||||
const statusLabels = { draft: 'Bozza', approved: 'Approvato', scheduled: 'Schedulato', published: 'Pubblicato' }
|
const statusLabels = { draft: 'Bozza', approved: 'Approvato', scheduled: 'Schedulato', published: 'Pubblicato' }
|
||||||
const statusColors = {
|
const statusColors = {
|
||||||
@@ -19,6 +20,7 @@ export default function ContentArchive() {
|
|||||||
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)
|
||||||
|
|
||||||
useEffect(() => { loadData() }, [])
|
useEffect(() => { loadData() }, [])
|
||||||
|
|
||||||
@@ -40,8 +42,7 @@ export default function ContentArchive() {
|
|||||||
try { await api.post(`/content/posts/${postId}/approve`); loadData() } catch {}
|
try { await api.post(`/content/posts/${postId}/approve`); loadData() } catch {}
|
||||||
}
|
}
|
||||||
const handleDelete = async (postId) => {
|
const handleDelete = async (postId) => {
|
||||||
if (!confirm('Eliminare questo contenuto?')) return
|
try { await api.delete(`/content/posts/${postId}`); setDeleteTarget(null); loadData() } catch {}
|
||||||
try { await api.delete(`/content/posts/${postId}`); loadData() } catch {}
|
|
||||||
}
|
}
|
||||||
const handleSaveEdit = async (postId) => {
|
const handleSaveEdit = async (postId) => {
|
||||||
try {
|
try {
|
||||||
@@ -171,7 +172,7 @@ export default function ContentArchive() {
|
|||||||
)}
|
)}
|
||||||
<button onClick={() => { setEditingId(post.id); setEditText(post.text_content || ''); setExpandedId(post.id) }}
|
<button onClick={() => { setEditingId(post.id); setEditText(post.text_content || ''); setExpandedId(post.id) }}
|
||||||
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={() => handleDelete(post.id)}
|
<button onClick={() => setDeleteTarget(post.id)}
|
||||||
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>
|
||||||
@@ -180,6 +181,16 @@ export default function ContentArchive() {
|
|||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
<ConfirmModal
|
||||||
|
open={deleteTarget !== null}
|
||||||
|
title="Elimina contenuto"
|
||||||
|
message="Sei sicuro di voler eliminare questo contenuto? L'operazione non è reversibile."
|
||||||
|
confirmLabel="Elimina"
|
||||||
|
cancelLabel="Annulla"
|
||||||
|
confirmStyle="danger"
|
||||||
|
onConfirm={() => handleDelete(deleteTarget)}
|
||||||
|
onCancel={() => setDeleteTarget(null)}
|
||||||
|
/>
|
||||||
<style>{`@keyframes spin { to { transform: rotate(360deg) } }`}</style>
|
<style>{`@keyframes spin { to { transform: rotate(360deg) } }`}</style>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { useState, useEffect, useRef } from 'react'
|
|||||||
import { Link } from 'react-router-dom'
|
import { Link } from 'react-router-dom'
|
||||||
import { api } from '../api'
|
import { api } from '../api'
|
||||||
import { useAuth } from '../AuthContext'
|
import { useAuth } from '../AuthContext'
|
||||||
|
import ConfirmModal from './ConfirmModal'
|
||||||
|
|
||||||
const PLATFORMS = [
|
const PLATFORMS = [
|
||||||
{ value: 'instagram', label: 'Instagram' },
|
{ value: 'instagram', label: 'Instagram' },
|
||||||
@@ -42,9 +43,11 @@ export default function ContentPage() {
|
|||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
const [charsLoading, setCharsLoading] = useState(true)
|
const [charsLoading, setCharsLoading] = useState(true)
|
||||||
const [error, setError] = useState('')
|
const [error, setError] = useState('')
|
||||||
const [generated, setGenerated] = useState(null)
|
const [generatedPosts, setGeneratedPosts] = useState([]) // array of posts, one per platform
|
||||||
|
const [activePlatformIdx, setActivePlatformIdx] = useState(0)
|
||||||
const [editing, setEditing] = useState(false)
|
const [editing, setEditing] = useState(false)
|
||||||
const [editText, setEditText] = useState('')
|
const [editText, setEditText] = useState('')
|
||||||
|
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false)
|
||||||
|
|
||||||
const [form, setForm] = useState({
|
const [form, setForm] = useState({
|
||||||
character_id: '',
|
character_id: '',
|
||||||
@@ -72,7 +75,8 @@ export default function ContentPage() {
|
|||||||
if (!form.character_id) { setError('Seleziona un personaggio'); return }
|
if (!form.character_id) { setError('Seleziona un personaggio'); return }
|
||||||
setError('')
|
setError('')
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
setGenerated(null)
|
setGeneratedPosts([])
|
||||||
|
setActivePlatformIdx(0)
|
||||||
try {
|
try {
|
||||||
const data = await api.post('/content/generate', {
|
const data = await api.post('/content/generate', {
|
||||||
character_id: parseInt(form.character_id),
|
character_id: parseInt(form.character_id),
|
||||||
@@ -85,8 +89,11 @@ export default function ContentPage() {
|
|||||||
].filter(Boolean).join('. ') || null,
|
].filter(Boolean).join('. ') || null,
|
||||||
include_affiliates: form.include_affiliates,
|
include_affiliates: form.include_affiliates,
|
||||||
})
|
})
|
||||||
setGenerated(data)
|
// Backend returns array of posts (one per platform)
|
||||||
setEditText(data.text_content || '')
|
const posts = Array.isArray(data) ? data : [data]
|
||||||
|
setGeneratedPosts(posts)
|
||||||
|
setActivePlatformIdx(0)
|
||||||
|
setEditText(posts[0]?.text_content || '')
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err.data?.missing_settings) {
|
if (err.data?.missing_settings) {
|
||||||
setError('__MISSING_SETTINGS__')
|
setError('__MISSING_SETTINGS__')
|
||||||
@@ -100,26 +107,37 @@ export default function ContentPage() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const generated = generatedPosts[activePlatformIdx] || null
|
||||||
|
|
||||||
|
const updateActivePost = (updates) => {
|
||||||
|
setGeneratedPosts(prev => prev.map((p, i) => i === activePlatformIdx ? { ...p, ...updates } : p))
|
||||||
|
}
|
||||||
|
|
||||||
const handleApprove = async () => {
|
const handleApprove = async () => {
|
||||||
|
if (!generated) return
|
||||||
try {
|
try {
|
||||||
await api.post(`/content/posts/${generated.id}/approve`)
|
await api.post(`/content/posts/${generated.id}/approve`)
|
||||||
setGenerated(prev => ({ ...prev, status: 'approved' }))
|
updateActivePost({ status: 'approved' })
|
||||||
} catch (err) { setError(err.message || 'Errore approvazione') }
|
} catch (err) { setError(err.message || 'Errore approvazione') }
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleSaveEdit = async () => {
|
const handleSaveEdit = async () => {
|
||||||
|
if (!generated) return
|
||||||
try {
|
try {
|
||||||
await api.put(`/content/posts/${generated.id}`, { text_content: editText })
|
await api.put(`/content/posts/${generated.id}`, { text_content: editText })
|
||||||
setGenerated(prev => ({ ...prev, text_content: editText }))
|
updateActivePost({ text_content: editText })
|
||||||
setEditing(false)
|
setEditing(false)
|
||||||
} catch (err) { setError(err.message || 'Errore salvataggio') }
|
} catch (err) { setError(err.message || 'Errore salvataggio') }
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleDelete = async () => {
|
const handleDelete = async () => {
|
||||||
if (!confirm('Eliminare questo contenuto?')) return
|
if (!generated) return
|
||||||
try {
|
try {
|
||||||
await api.delete(`/content/posts/${generated.id}`)
|
await api.delete(`/content/posts/${generated.id}`)
|
||||||
setGenerated(null)
|
const remaining = generatedPosts.filter((_, i) => i !== activePlatformIdx)
|
||||||
|
setGeneratedPosts(remaining)
|
||||||
|
setActivePlatformIdx(Math.min(activePlatformIdx, Math.max(0, remaining.length - 1)))
|
||||||
|
setShowDeleteConfirm(false)
|
||||||
} catch (err) { setError(err.message || 'Errore eliminazione') }
|
} catch (err) { setError(err.message || 'Errore eliminazione') }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -314,6 +332,25 @@ export default function ContentPage() {
|
|||||||
<div style={{ backgroundColor: 'var(--surface)', border: '1px solid var(--border)', borderTop: '4px solid var(--border-strong)', padding: '1.5rem' }}>
|
<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>
|
<span style={labelStyle}>Contenuto Generato</span>
|
||||||
|
|
||||||
|
{/* Platform tabs when multiple posts */}
|
||||||
|
{generatedPosts.length > 1 && (
|
||||||
|
<div style={{ display: 'flex', gap: '0', marginTop: '0.75rem', borderBottom: '2px solid var(--border)' }}>
|
||||||
|
{generatedPosts.map((p, i) => (
|
||||||
|
<button key={i} onClick={() => { setActivePlatformIdx(i); setEditText(p.text_content || ''); setEditing(false) }}
|
||||||
|
style={{
|
||||||
|
padding: '0.5rem 1rem', fontSize: '0.8rem', fontWeight: activePlatformIdx === i ? 700 : 400,
|
||||||
|
fontFamily: "'DM Sans', sans-serif", border: 'none', cursor: 'pointer',
|
||||||
|
backgroundColor: activePlatformIdx === i ? 'var(--surface)' : 'transparent',
|
||||||
|
color: activePlatformIdx === i ? 'var(--accent)' : 'var(--ink-muted)',
|
||||||
|
borderBottom: activePlatformIdx === i ? '2px solid var(--accent)' : '2px solid transparent',
|
||||||
|
marginBottom: '-2px', transition: 'all 0.15s',
|
||||||
|
}}>
|
||||||
|
{(p.platform_hint || '').charAt(0).toUpperCase() + (p.platform_hint || '').slice(1)}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', padding: '4rem 0' }}>
|
<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' }} />
|
<div style={{ width: 32, height: 32, border: '2px solid var(--border)', borderTopColor: 'var(--accent)', borderRadius: '50%', animation: 'spin 0.8s linear infinite', marginBottom: '1rem' }} />
|
||||||
@@ -356,7 +393,7 @@ export default function ContentPage() {
|
|||||||
{generated.hashtags?.length > 0 && (
|
{generated.hashtags?.length > 0 && (
|
||||||
<HashtagEditor
|
<HashtagEditor
|
||||||
hashtags={generated.hashtags}
|
hashtags={generated.hashtags}
|
||||||
onChange={newTags => setGenerated(prev => ({ ...prev, hashtags: newTags }))}
|
onChange={newTags => updateActivePost({ hashtags: newTags })}
|
||||||
postId={generated.id}
|
postId={generated.id}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@@ -366,7 +403,7 @@ export default function ContentPage() {
|
|||||||
<button onClick={handleApprove} style={{ ...btnPrimary, backgroundColor: 'var(--success)' }}>Approva</button>
|
<button onClick={handleApprove} style={{ ...btnPrimary, backgroundColor: 'var(--success)' }}>Approva</button>
|
||||||
)}
|
)}
|
||||||
{!editing && <button onClick={() => setEditing(true)} style={btnSecondary}>Modifica</button>}
|
{!editing && <button onClick={() => setEditing(true)} style={btnSecondary}>Modifica</button>}
|
||||||
<button onClick={handleDelete} style={{ ...btnSecondary, marginLeft: 'auto', color: 'var(--error)' }}>Elimina</button>
|
<button onClick={() => setShowDeleteConfirm(true)} style={{ ...btnSecondary, marginLeft: 'auto', color: 'var(--error)' }}>Elimina</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
@@ -380,6 +417,16 @@ export default function ContentPage() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<ConfirmModal
|
||||||
|
open={showDeleteConfirm}
|
||||||
|
title="Elimina contenuto"
|
||||||
|
message="Sei sicuro di voler eliminare questo contenuto? L'operazione non è reversibile."
|
||||||
|
confirmLabel="Elimina"
|
||||||
|
cancelLabel="Annulla"
|
||||||
|
confirmStyle="danger"
|
||||||
|
onConfirm={handleDelete}
|
||||||
|
onCancel={() => setShowDeleteConfirm(false)}
|
||||||
|
/>
|
||||||
<style>{`@keyframes spin { to { transform: rotate(360deg) } }`}</style>
|
<style>{`@keyframes spin { to { transform: rotate(360deg) } }`}</style>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user