- Backend FastAPI con multi-LLM (Claude/OpenAI/Gemini) - Publishing su Facebook, Instagram, YouTube, TikTok - Calendario editoriale con awareness levels (PAS, AIDA, BAB...) - Design system Editorial Fresh (Fraunces + DM Sans) - Scheduler automatico, gestione commenti AI, affiliate links Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
332 lines
13 KiB
JavaScript
332 lines
13 KiB
JavaScript
import { useState, useEffect } from 'react'
|
|
import { api } from '../api'
|
|
|
|
const platformLabels = {
|
|
instagram: 'Instagram',
|
|
facebook: 'Facebook',
|
|
youtube: 'YouTube',
|
|
tiktok: 'TikTok',
|
|
}
|
|
|
|
const platformColors = {
|
|
instagram: 'bg-pink-50 text-pink-600 border-pink-200',
|
|
facebook: 'bg-blue-50 text-blue-600 border-blue-200',
|
|
youtube: 'bg-red-50 text-red-600 border-red-200',
|
|
tiktok: 'bg-slate-900 text-white border-slate-700',
|
|
}
|
|
|
|
const EMPTY_ACCOUNT = {
|
|
platform: 'instagram',
|
|
account_name: '',
|
|
access_token: '',
|
|
page_id: '',
|
|
}
|
|
|
|
export default function SocialAccounts() {
|
|
const [characters, setCharacters] = useState([])
|
|
const [accounts, setAccounts] = useState([])
|
|
const [loading, setLoading] = useState(true)
|
|
const [showForm, setShowForm] = useState(null) // character_id or null
|
|
const [form, setForm] = useState(EMPTY_ACCOUNT)
|
|
const [saving, setSaving] = useState(false)
|
|
const [error, setError] = useState('')
|
|
const [testing, setTesting] = useState(null)
|
|
const [testResult, setTestResult] = useState({})
|
|
|
|
useEffect(() => {
|
|
loadData()
|
|
}, [])
|
|
|
|
const loadData = async () => {
|
|
setLoading(true)
|
|
try {
|
|
const [charsData, accsData] = await Promise.all([
|
|
api.get('/characters/'),
|
|
api.get('/social/accounts'),
|
|
])
|
|
setCharacters(charsData)
|
|
setAccounts(accsData)
|
|
} catch {
|
|
// silent
|
|
} finally {
|
|
setLoading(false)
|
|
}
|
|
}
|
|
|
|
const getAccountsForCharacter = (characterId) => {
|
|
return accounts.filter((a) => a.character_id === characterId)
|
|
}
|
|
|
|
const handleFormChange = (field, value) => {
|
|
setForm((prev) => ({ ...prev, [field]: value }))
|
|
}
|
|
|
|
const handleAddAccount = async (characterId) => {
|
|
setError('')
|
|
setSaving(true)
|
|
try {
|
|
await api.post('/social/accounts', {
|
|
character_id: characterId,
|
|
platform: form.platform,
|
|
account_name: form.account_name,
|
|
access_token: form.access_token,
|
|
page_id: form.page_id || null,
|
|
})
|
|
setShowForm(null)
|
|
setForm(EMPTY_ACCOUNT)
|
|
loadData()
|
|
} catch (err) {
|
|
setError(err.message || 'Errore nel salvataggio')
|
|
} finally {
|
|
setSaving(false)
|
|
}
|
|
}
|
|
|
|
const handleTest = async (accountId) => {
|
|
setTesting(accountId)
|
|
setTestResult((prev) => ({ ...prev, [accountId]: null }))
|
|
try {
|
|
const result = await api.post(`/social/accounts/${accountId}/test`)
|
|
setTestResult((prev) => ({ ...prev, [accountId]: { success: true, message: result.message || 'Connessione OK' } }))
|
|
} catch (err) {
|
|
setTestResult((prev) => ({ ...prev, [accountId]: { success: false, message: err.message || 'Test fallito' } }))
|
|
} finally {
|
|
setTesting(null)
|
|
}
|
|
}
|
|
|
|
const handleToggle = async (account) => {
|
|
try {
|
|
await api.put(`/social/accounts/${account.id}`, { is_active: !account.is_active })
|
|
loadData()
|
|
} catch {
|
|
// silent
|
|
}
|
|
}
|
|
|
|
const handleRemove = async (accountId) => {
|
|
if (!confirm('Rimuovere questo account social?')) return
|
|
try {
|
|
await api.delete(`/social/accounts/${accountId}`)
|
|
loadData()
|
|
} catch {
|
|
// silent
|
|
}
|
|
}
|
|
|
|
if (loading) {
|
|
return (
|
|
<div>
|
|
<h2 className="text-2xl font-bold text-slate-800 mb-1">Account Social</h2>
|
|
<p className="text-slate-500 text-sm mb-6">Gestisci le connessioni ai social network</p>
|
|
<div className="flex justify-center py-12">
|
|
<div className="animate-spin rounded-full h-8 w-8 border-2 border-brand-500 border-t-transparent" />
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<div>
|
|
<div className="mb-6">
|
|
<h2 className="text-2xl font-bold text-slate-800">Account Social</h2>
|
|
<p className="text-slate-500 mt-1 text-sm">
|
|
Gestisci le connessioni ai social network per ogni personaggio
|
|
</p>
|
|
</div>
|
|
|
|
{/* Info box */}
|
|
<div className="mb-6 p-4 bg-blue-50 border border-blue-200 rounded-xl">
|
|
<div className="flex gap-3">
|
|
<span className="text-blue-500 text-lg shrink-0">i</span>
|
|
<div>
|
|
<p className="text-sm text-blue-700 font-medium">Configurazione OAuth</p>
|
|
<p className="text-xs text-blue-600 mt-0.5">
|
|
Per la pubblicazione automatica, ogni piattaforma richiede la configurazione di un'app
|
|
developer con le relative credenziali OAuth. Inserisci access token e page ID ottenuti
|
|
dalla console developer di ciascuna piattaforma.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{error && (
|
|
<div className="mb-4 p-3 bg-red-50 border border-red-200 rounded-lg text-red-600 text-sm">
|
|
{error}
|
|
</div>
|
|
)}
|
|
|
|
{characters.length === 0 ? (
|
|
<div className="text-center py-16 bg-white rounded-xl border border-slate-200">
|
|
<p className="text-4xl mb-3">◇</p>
|
|
<p className="text-slate-500 font-medium">Nessun personaggio</p>
|
|
<p className="text-slate-400 text-sm mt-1">
|
|
Crea un personaggio per poi collegare gli account social
|
|
</p>
|
|
</div>
|
|
) : (
|
|
<div className="space-y-6">
|
|
{characters.map((character) => {
|
|
const charAccounts = getAccountsForCharacter(character.id)
|
|
const isFormOpen = showForm === character.id
|
|
|
|
return (
|
|
<div key={character.id} className="bg-white rounded-xl border border-slate-200 overflow-hidden">
|
|
{/* Character header */}
|
|
<div className="p-5 border-b border-slate-100">
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex items-center gap-3">
|
|
<div
|
|
className="w-10 h-10 rounded-full flex items-center justify-center text-white font-bold text-sm shrink-0"
|
|
style={{ backgroundColor: character.visual_style?.primary_color || '#f97316' }}
|
|
>
|
|
{character.name?.charAt(0).toUpperCase()}
|
|
</div>
|
|
<div>
|
|
<h3 className="font-semibold text-slate-800">{character.name}</h3>
|
|
<p className="text-xs text-slate-400">{character.niche}</p>
|
|
</div>
|
|
</div>
|
|
<button
|
|
onClick={() => {
|
|
setShowForm(isFormOpen ? null : character.id)
|
|
setForm(EMPTY_ACCOUNT)
|
|
setError('')
|
|
}}
|
|
className="text-xs px-3 py-1.5 bg-brand-50 hover:bg-brand-100 text-brand-600 rounded-lg transition-colors font-medium"
|
|
>
|
|
{isFormOpen ? 'Annulla' : '+ Connetti Account'}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Inline form */}
|
|
{isFormOpen && (
|
|
<div className="p-5 bg-slate-50 border-b border-slate-100">
|
|
<div className="max-w-md space-y-3">
|
|
<div>
|
|
<label className="block text-sm font-medium text-slate-700 mb-1">Piattaforma</label>
|
|
<select
|
|
value={form.platform}
|
|
onChange={(e) => handleFormChange('platform', e.target.value)}
|
|
className="w-full px-4 py-2.5 border border-slate-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-brand-500 focus:border-transparent text-sm bg-white"
|
|
>
|
|
{Object.entries(platformLabels).map(([val, label]) => (
|
|
<option key={val} value={val}>{label}</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-slate-700 mb-1">Nome account</label>
|
|
<input
|
|
type="text"
|
|
value={form.account_name}
|
|
onChange={(e) => handleFormChange('account_name', e.target.value)}
|
|
placeholder="Es. @mio_profilo"
|
|
className="w-full px-4 py-2.5 border border-slate-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-brand-500 focus:border-transparent text-sm"
|
|
required
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-slate-700 mb-1">Access Token</label>
|
|
<input
|
|
type="password"
|
|
value={form.access_token}
|
|
onChange={(e) => handleFormChange('access_token', e.target.value)}
|
|
placeholder="Token di accesso dalla piattaforma"
|
|
className="w-full px-4 py-2.5 border border-slate-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-brand-500 focus:border-transparent text-sm font-mono"
|
|
required
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-slate-700 mb-1">
|
|
Page ID
|
|
<span className="text-slate-400 font-normal ml-1">(opzionale)</span>
|
|
</label>
|
|
<input
|
|
type="text"
|
|
value={form.page_id}
|
|
onChange={(e) => handleFormChange('page_id', e.target.value)}
|
|
placeholder="ID pagina (per Facebook/YouTube)"
|
|
className="w-full px-4 py-2.5 border border-slate-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-brand-500 focus:border-transparent text-sm font-mono"
|
|
/>
|
|
</div>
|
|
|
|
<button
|
|
onClick={() => handleAddAccount(character.id)}
|
|
disabled={saving || !form.account_name || !form.access_token}
|
|
className="px-4 py-2 bg-brand-600 hover:bg-brand-700 disabled:bg-brand-300 text-white text-sm font-medium rounded-lg transition-colors"
|
|
>
|
|
{saving ? 'Salvataggio...' : 'Salva Account'}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Accounts list */}
|
|
<div className="divide-y divide-slate-50">
|
|
{charAccounts.length === 0 ? (
|
|
<div className="px-5 py-8 text-center">
|
|
<p className="text-sm text-slate-400">Nessun account collegato</p>
|
|
</div>
|
|
) : (
|
|
charAccounts.map((account) => (
|
|
<div key={account.id} className="px-5 py-3 flex items-center gap-3">
|
|
{/* Platform badge */}
|
|
<span className={`text-xs px-2 py-0.5 rounded-full font-medium border ${platformColors[account.platform] || 'bg-slate-100 text-slate-600 border-slate-200'}`}>
|
|
{platformLabels[account.platform] || account.platform}
|
|
</span>
|
|
|
|
{/* Account name */}
|
|
<span className="text-sm font-medium text-slate-700 flex-1 min-w-0 truncate">
|
|
{account.account_name}
|
|
</span>
|
|
|
|
{/* Status */}
|
|
<span className={`w-2 h-2 rounded-full shrink-0 ${account.is_active ? 'bg-emerald-500' : 'bg-slate-300'}`} />
|
|
|
|
{/* Test result */}
|
|
{testResult[account.id] && (
|
|
<span className={`text-xs ${testResult[account.id].success ? 'text-emerald-600' : 'text-red-500'}`}>
|
|
{testResult[account.id].message}
|
|
</span>
|
|
)}
|
|
|
|
{/* Actions */}
|
|
<div className="flex items-center gap-1 shrink-0">
|
|
<button
|
|
onClick={() => handleTest(account.id)}
|
|
disabled={testing === account.id}
|
|
className="text-xs px-2 py-1 bg-slate-100 hover:bg-slate-200 text-slate-600 rounded transition-colors disabled:opacity-50"
|
|
>
|
|
{testing === account.id ? 'Test...' : 'Test'}
|
|
</button>
|
|
<button
|
|
onClick={() => handleToggle(account)}
|
|
className="text-xs px-2 py-1 bg-slate-100 hover:bg-slate-200 text-slate-600 rounded transition-colors"
|
|
>
|
|
{account.is_active ? 'Disattiva' : 'Attiva'}
|
|
</button>
|
|
<button
|
|
onClick={() => handleRemove(account.id)}
|
|
className="text-xs px-2 py-1 text-red-500 hover:bg-red-50 rounded transition-colors"
|
|
>
|
|
Rimuovi
|
|
</button>
|
|
</div>
|
|
</div>
|
|
))
|
|
)}
|
|
</div>
|
|
</div>
|
|
)
|
|
})}
|
|
</div>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|