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 onRegenerated: (updated: PostResult) => void
/** Callback quando le slide vengono modificate inline */ /** Callback quando le slide vengono modificate inline */
onEdit: (updated: PostResult) => void onEdit: (updated: PostResult) => void
/** True se questo post è stato rigenerato (non originale) */
isRegenerated?: boolean
} }
export function PostCard({ export function PostCard({
@@ -35,13 +37,40 @@ export function PostCard({
tono, tono,
onRegenerated, onRegenerated,
onEdit, onEdit,
isRegenerated,
}: PostCardProps) { }: PostCardProps) {
const [expanded, setExpanded] = useState(false) const [expanded, setExpanded] = useState(false)
const [showRegenForm, setShowRegenForm] = useState(false)
const [regenTopic, setRegenTopic] = useState('')
const [regenNotes, setRegenNotes] = useState('')
const generateSingle = useGenerateSingle() const generateSingle = useGenerateSingle()
const slot = result.slot const slot = result.slot
const post = result.post 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() { async function handleRetry() {
if (!slot) return if (!slot) return
const req: GenerateRequest = { const req: GenerateRequest = {
@@ -123,17 +152,22 @@ export function PostCard({
].join(' ')} ].join(' ')}
> >
{/* Header card — sempre visibile, click per espandere */} {/* Header card — sempre visibile, click per espandere */}
<button <div className="w-full px-4 py-4 text-left">
onClick={() => setExpanded((v) => !v)}
className="w-full px-4 py-4 text-left"
>
<div className="flex items-start justify-between gap-2"> <div className="flex items-start justify-between gap-2">
<div className="flex-1 min-w-0"> <div
{/* Numero slot + badge */} 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"> <div className="flex flex-wrap items-center gap-1.5 mb-2">
<span className="text-xs font-mono text-stone-500"> <span className="text-xs font-mono text-stone-500">
#{result.slot_index + 1} #{result.slot_index + 1}
</span> </span>
{isRegenerated && (
<span className="text-amber-400" title="Post rigenerato">
<RefreshCw size={12} />
</span>
)}
{slot && ( {slot && (
<> <>
<BadgePN tipo={slot.tipo_contenuto} /> <BadgePN tipo={slot.tipo_contenuto} />
@@ -159,12 +193,84 @@ export function PostCard({
)} )}
</div> </div>
{/* Expand/collapse icon */} {/* Rigenera button + Expand/collapse icon */}
<span className="text-stone-500 flex-shrink-0 mt-0.5"> <div className="flex items-center gap-1.5 flex-shrink-0 mt-0.5">
{expanded ? <ChevronUp size={16} /> : <ChevronDown size={16} />} <button
</span> 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> </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: SlideViewer */}
{expanded && ( {expanded && (