fix: auto-save hashtags, plan-based platforms, archive with text preview

- Hashtag auto-save: debounced 500ms save on add/remove/edit, no manual button
- Platform chips: Freemium sees only Instagram/Facebook, Pro sees all 4
- Platform badge: changed from tab-like to informative "per instagram" label
- Add "Archivio →" link in content page header
- Rewrite ContentArchive: show text_content preview (was showing only hashtags),
  add edit button, use Editorial Fresh design system, fix post.text → post.text_content

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Michele
2026-04-03 18:43:46 +02:00
parent a6270c2e3f
commit 5620a71f1b
2 changed files with 165 additions and 178 deletions

View File

@@ -1,6 +1,7 @@
import { useState, useEffect } from 'react'
import { useState, useEffect, useRef } from 'react'
import { Link } from 'react-router-dom'
import { api } from '../api'
import { useAuth } from '../AuthContext'
const PLATFORMS = [
{ value: 'instagram', label: 'Instagram' },
@@ -35,6 +36,8 @@ const STATUS_COLORS = {
}
export default function ContentPage() {
const { isPro } = useAuth()
const availablePlatforms = isPro ? PLATFORMS : PLATFORMS.filter(p => ['instagram', 'facebook'].includes(p.value))
const [characters, setCharacters] = useState([])
const [loading, setLoading] = useState(false)
const [charsLoading, setCharsLoading] = useState(true)
@@ -129,9 +132,14 @@ export default function ContentPage() {
<h2 style={{ fontFamily: "'Fraunces', serif", fontSize: '1.75rem', fontWeight: 600, letterSpacing: '-0.02em', color: 'var(--ink)', margin: '0.5rem 0 0.3rem' }}>
Genera Contenuti
</h2>
<p style={{ fontSize: '0.875rem', color: 'var(--ink-muted)', margin: 0 }}>
Definisci un brief editoriale, scegli piattaforma e tipo, poi genera. L'AI terrà conto del tono e dei topic del personaggio selezionato.
</p>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: '1rem' }}>
<p style={{ fontSize: '0.875rem', color: 'var(--ink-muted)', margin: 0 }}>
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 →
</Link>
</div>
</div>
{/* No characters → gate */}
@@ -224,7 +232,7 @@ export default function ContentPage() {
<div>
<label style={labelStyle}>Piattaforme <span style={{ fontWeight: 400, textTransform: 'none', letterSpacing: 0, color: 'var(--ink-muted)', fontSize: '0.75rem' }}>(seleziona una o più)</span></label>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '0.4rem', marginTop: '0.4rem' }}>
{PLATFORMS.map(p => {
{availablePlatforms.map(p => {
const active = form.platforms.includes(p.value)
return (
<button key={p.value} type="button" onClick={() => toggleChip('platforms', p.value)} style={{
@@ -321,8 +329,8 @@ export default function ContentPage() {
</span>
)})()}
{generated.platform_hint && (
<span style={{ fontSize: '0.72rem', fontWeight: 600, padding: '0.2rem 0.5rem', backgroundColor: 'var(--cream-dark)', color: 'var(--ink-muted)' }}>
{generated.platform_hint}
<span style={{ fontSize: '0.72rem', fontWeight: 500, padding: '0.2rem 0.5rem', backgroundColor: 'var(--cream-dark)', color: 'var(--ink-muted)', borderLeft: '2px solid var(--border)' }}>
per {generated.platform_hint}
</span>
)}
</div>
@@ -381,11 +389,22 @@ function HashtagEditor({ hashtags, onChange, postId }) {
const [newTag, setNewTag] = useState('')
const [editIdx, setEditIdx] = useState(null)
const [editValue, setEditValue] = useState('')
const [saving, setSaving] = useState(false)
const saveTimer = useRef(null)
const persistHashtags = (tags) => {
clearTimeout(saveTimer.current)
saveTimer.current = setTimeout(() => {
api.put(`/content/posts/${postId}`, { hashtags: tags }).catch(() => {})
}, 500)
}
const updateAndSave = (newTags) => {
onChange(newTags)
persistHashtags(newTags)
}
const removeTag = (idx) => {
const updated = hashtags.filter((_, i) => i !== idx)
onChange(updated)
updateAndSave(hashtags.filter((_, i) => i !== idx))
}
const addTag = () => {
@@ -393,7 +412,7 @@ function HashtagEditor({ hashtags, onChange, postId }) {
if (!tag) return
if (!tag.startsWith('#')) tag = `#${tag}`
if (!hashtags.includes(tag)) {
onChange([...hashtags, tag])
updateAndSave([...hashtags, tag])
}
setNewTag('')
}
@@ -410,18 +429,10 @@ function HashtagEditor({ hashtags, onChange, postId }) {
if (!tag.startsWith('#')) tag = `#${tag}`
const updated = [...hashtags]
updated[editIdx] = tag
onChange(updated)
updateAndSave(updated)
setEditIdx(null)
}
const saveHashtags = async () => {
setSaving(true)
try {
await api.put(`/content/posts/${postId}`, { hashtags })
} catch (e) { /* silent */ }
setSaving(false)
}
return (
<div style={{ marginBottom: '1rem' }}>
<span style={{ ...labelStyle, display: 'block', marginBottom: '0.4rem' }}>Hashtag</span>
@@ -447,10 +458,6 @@ function HashtagEditor({ hashtags, onChange, postId }) {
onFocus={e => e.target.style.borderColor = 'var(--ink)'} onBlur={e => e.target.style.borderColor = 'var(--border)'} />
<button type="button" onClick={addTag} style={{ fontSize: '0.78rem', padding: '0.3rem 0.6rem', backgroundColor: 'var(--cream-dark)', border: 'none', cursor: 'pointer', color: 'var(--ink-light)' }}>+</button>
</div>
<button type="button" onClick={saveHashtags} disabled={saving}
style={{ fontSize: '0.72rem', marginTop: '0.35rem', padding: '0.2rem 0.5rem', backgroundColor: 'transparent', border: '1px solid var(--border)', cursor: 'pointer', color: 'var(--ink-muted)' }}>
{saving ? 'Salvataggio' : 'Salva hashtag'}
</button>
</div>
)
}