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:
@@ -2,6 +2,7 @@ import { useState, useEffect, useRef } from 'react'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { api } from '../api'
|
||||
import { useAuth } from '../AuthContext'
|
||||
import ConfirmModal from './ConfirmModal'
|
||||
|
||||
const PLATFORMS = [
|
||||
{ value: 'instagram', label: 'Instagram' },
|
||||
@@ -42,9 +43,11 @@ export default function ContentPage() {
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [charsLoading, setCharsLoading] = useState(true)
|
||||
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 [editText, setEditText] = useState('')
|
||||
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false)
|
||||
|
||||
const [form, setForm] = useState({
|
||||
character_id: '',
|
||||
@@ -72,7 +75,8 @@ export default function ContentPage() {
|
||||
if (!form.character_id) { setError('Seleziona un personaggio'); return }
|
||||
setError('')
|
||||
setLoading(true)
|
||||
setGenerated(null)
|
||||
setGeneratedPosts([])
|
||||
setActivePlatformIdx(0)
|
||||
try {
|
||||
const data = await api.post('/content/generate', {
|
||||
character_id: parseInt(form.character_id),
|
||||
@@ -85,8 +89,11 @@ export default function ContentPage() {
|
||||
].filter(Boolean).join('. ') || null,
|
||||
include_affiliates: form.include_affiliates,
|
||||
})
|
||||
setGenerated(data)
|
||||
setEditText(data.text_content || '')
|
||||
// Backend returns array of posts (one per platform)
|
||||
const posts = Array.isArray(data) ? data : [data]
|
||||
setGeneratedPosts(posts)
|
||||
setActivePlatformIdx(0)
|
||||
setEditText(posts[0]?.text_content || '')
|
||||
} catch (err) {
|
||||
if (err.data?.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 () => {
|
||||
if (!generated) return
|
||||
try {
|
||||
await api.post(`/content/posts/${generated.id}/approve`)
|
||||
setGenerated(prev => ({ ...prev, status: 'approved' }))
|
||||
updateActivePost({ 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 }))
|
||||
updateActivePost({ text_content: editText })
|
||||
setEditing(false)
|
||||
} catch (err) { setError(err.message || 'Errore salvataggio') }
|
||||
}
|
||||
|
||||
const handleDelete = async () => {
|
||||
if (!confirm('Eliminare questo contenuto?')) return
|
||||
if (!generated) return
|
||||
try {
|
||||
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') }
|
||||
}
|
||||
|
||||
@@ -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' }}>
|
||||
<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 ? (
|
||||
<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' }} />
|
||||
@@ -356,7 +393,7 @@ export default function ContentPage() {
|
||||
{generated.hashtags?.length > 0 && (
|
||||
<HashtagEditor
|
||||
hashtags={generated.hashtags}
|
||||
onChange={newTags => setGenerated(prev => ({ ...prev, hashtags: newTags }))}
|
||||
onChange={newTags => updateActivePost({ hashtags: newTags })}
|
||||
postId={generated.id}
|
||||
/>
|
||||
)}
|
||||
@@ -366,7 +403,7 @@ export default function ContentPage() {
|
||||
<button onClick={handleApprove} style={{ ...btnPrimary, backgroundColor: 'var(--success)' }}>Approva</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>
|
||||
) : (
|
||||
@@ -380,6 +417,16 @@ export default function ContentPage() {
|
||||
)}
|
||||
</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>
|
||||
</div>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user