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
|
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 && (
|
||||||
|
|||||||
Reference in New Issue
Block a user