feat(02-02): PostCard regen button with inline form, topic override, and regenerated badge

- Add handleRegen function with topic/notes override support
- Add inline popover form with optional topic and notes fields
- Add isRegenerated prop with amber RefreshCw badge for regenerated posts
- Regen button positioned in card header next to expand/collapse chevron
- Form appears below header with Rigenera/Annulla actions

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Michele
2026-03-08 21:01:47 +01:00
parent 36fa50173f
commit 3488023142

View File

@@ -26,6 +26,8 @@ interface PostCardProps {
onRegenerated: (updated: PostResult) => void
/** Callback quando le slide vengono modificate inline */
onEdit: (updated: PostResult) => void
/** True se questo post è stato rigenerato (non originale) */
isRegenerated?: boolean
}
export function PostCard({
@@ -35,13 +37,40 @@ export function PostCard({
tono,
onRegenerated,
onEdit,
isRegenerated,
}: PostCardProps) {
const [expanded, setExpanded] = useState(false)
const [showRegenForm, setShowRegenForm] = useState(false)
const [regenTopic, setRegenTopic] = useState('')
const [regenNotes, setRegenNotes] = useState('')
const generateSingle = useGenerateSingle()
const slot = result.slot
const post = result.post
async function handleRegen() {
if (!slot) return
// Se l'utente ha specificato un topic, usalo come override
const overriddenSlot = regenTopic.trim()
? { ...slot, topic: regenTopic.trim() }
: slot
const req: GenerateRequest = {
slot: overriddenSlot,
obiettivo_campagna: obiettivoCampagna,
brand_name: brandName,
tono: regenNotes.trim() || tono, // Se note fornite, usale come tono override
}
try {
const newResult = await generateSingle.mutateAsync(req)
onRegenerated({ ...newResult, slot })
setShowRegenForm(false)
setRegenTopic('')
setRegenNotes('')
} catch {
// Errore gestito da generateSingle.error
}
}
async function handleRetry() {
if (!slot) return
const req: GenerateRequest = {
@@ -123,17 +152,22 @@ export function PostCard({
].join(' ')}
>
{/* Header card — sempre visibile, click per espandere */}
<button
onClick={() => setExpanded((v) => !v)}
className="w-full px-4 py-4 text-left"
>
<div className="w-full px-4 py-4 text-left">
<div className="flex items-start justify-between gap-2">
<div className="flex-1 min-w-0">
{/* Numero slot + badge */}
<div
className="flex-1 min-w-0 cursor-pointer"
onClick={() => setExpanded((v) => !v)}
>
{/* Numero slot + badge rigenerato + badge PN/Schwartz */}
<div className="flex flex-wrap items-center gap-1.5 mb-2">
<span className="text-xs font-mono text-stone-500">
#{result.slot_index + 1}
</span>
{isRegenerated && (
<span className="text-amber-400" title="Post rigenerato">
<RefreshCw size={12} />
</span>
)}
{slot && (
<>
<BadgePN tipo={slot.tipo_contenuto} />
@@ -159,12 +193,84 @@ export function PostCard({
)}
</div>
{/* Expand/collapse icon */}
<span className="text-stone-500 flex-shrink-0 mt-0.5">
{expanded ? <ChevronUp size={16} /> : <ChevronDown size={16} />}
</span>
{/* Rigenera button + Expand/collapse icon */}
<div className="flex items-center gap-1.5 flex-shrink-0 mt-0.5">
<button
onClick={(e) => {
e.stopPropagation()
setShowRegenForm((v) => !v)
}}
className="text-stone-500 hover:text-amber-400 transition-colors"
title="Rigenera questo post"
>
<RefreshCw size={14} />
</button>
<span
className="text-stone-500 cursor-pointer"
onClick={() => setExpanded((v) => !v)}
>
{expanded ? <ChevronUp size={16} /> : <ChevronDown size={16} />}
</span>
</div>
</div>
</button>
</div>
{/* Regen inline form */}
{showRegenForm && (
<div className="px-4 pb-3 border-t border-stone-700/50 pt-3 space-y-3">
<div>
<label className="text-xs text-stone-400 block mb-1">
Topic alternativo <span className="text-stone-600">(opzionale)</span>
</label>
<input
type="text"
value={regenTopic}
onChange={(e) => setRegenTopic(e.target.value)}
placeholder="Es: 3 errori comuni nel marketing digitale"
className="w-full px-3 py-2 rounded-lg bg-stone-900 border border-stone-700 text-sm text-stone-200 placeholder:text-stone-600 focus:border-amber-500/50 focus:outline-none"
/>
</div>
<div>
<label className="text-xs text-stone-400 block mb-1">
Note aggiuntive <span className="text-stone-600">(opzionale)</span>
</label>
<input
type="text"
value={regenNotes}
onChange={(e) => setRegenNotes(e.target.value)}
placeholder="Es: tono piu' provocatorio, focus su ROI"
className="w-full px-3 py-2 rounded-lg bg-stone-900 border border-stone-700 text-sm text-stone-200 placeholder:text-stone-600 focus:border-amber-500/50 focus:outline-none"
/>
</div>
<div className="flex items-center gap-2">
<button
onClick={handleRegen}
disabled={generateSingle.isPending}
className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg bg-amber-500 text-stone-950 text-xs font-semibold hover:bg-amber-400 disabled:opacity-50 transition-colors"
>
{generateSingle.isPending ? (
<Loader2 size={12} className="animate-spin" />
) : (
<RefreshCw size={12} />
)}
{generateSingle.isPending ? 'Rigenerazione...' : 'Rigenera'}
</button>
<button
onClick={() => {
setShowRegenForm(false)
setRegenTopic('')
setRegenNotes('')
}}
className="px-3 py-1.5 rounded-lg text-xs text-stone-400 hover:text-stone-200 transition-colors"
>
Annulla
</button>
</div>
{generateSingle.error && (
<p className="text-xs text-red-400">{generateSingle.error.message}</p>
)}
</div>
)}
{/* Expanded: SlideViewer */}
{expanded && (