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:
@@ -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">
|
||||
{/* 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>
|
||||
</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 && (
|
||||
|
||||
Reference in New Issue
Block a user