/** * BoostMedia AI Content Generator Admin - Review Step * * @package BoostMedia_AI * @license GPL-2.0-or-later */ import { useState } from 'react' import { ChevronRight, ChevronDown, ChevronUp, Check, Clock, Send, RotateCcw, CheckCheck, ExternalLink, Eye, Trash2, RefreshCw, ImageIcon, Maximize2, } from 'lucide-react' import type { GeneratedPost } from '../../types' import { Button, Card, Badge, CopyscapeStatusBadge } from '../common' import { GeneratedPostPreview } from './GeneratedPostPreview' import { RegenerateImageDialog } from './RegenerateImageDialog' import { ImageLightbox } from './ImageLightbox' import { endpoints } from '../../api/client' import { t, tf, isRtl } from '../../lib/i18n' type PostStatus = GeneratedPost['status'] function getStatusConfig(): Record { return { published: { label: t('Published'), variant: 'success' }, draft: { label: t('Draft'), variant: 'default' }, pending: { label: t('Pending approval'), variant: 'warning' }, scheduled: { label: t('Scheduled'), variant: 'info' }, failed: { label: t('Failed'), variant: 'error' }, } } interface ReviewStepProps { posts: GeneratedPost[] onPublish: (postIds: number[]) => void onSchedule: (postIds: number[], scheduleAt: string) => void onBack: () => void onStartOver: () => void } export function ReviewStep({ posts, onPublish, onSchedule, onBack, onStartOver, }: ReviewStepProps) { const safePosts = Array.isArray(posts) ? posts : [] const [selectedPosts, setSelectedPosts] = useState>( new Set(safePosts.map((p) => p.id)) ) const [expandedPost, setExpandedPost] = useState(null) const [lightboxTarget, setLightboxTarget] = useState(null) const [regenerateTarget, setRegenerateTarget] = useState(null) const [localPosts, setLocalPosts] = useState(safePosts) const [isPublishing, setIsPublishing] = useState(false) const [actionLoading, setActionLoading] = useState(null) const [publishResult, setPublishResult] = useState<{ success: number failed: number } | null>(null) const togglePostSelection = (postId: number) => { const newSelected = new Set(selectedPosts) if (newSelected.has(postId)) { newSelected.delete(postId) } else { newSelected.add(postId) } setSelectedPosts(newSelected) } const selectAll = () => { setSelectedPosts(new Set(localPosts.map((p) => p.id))) } const deselectAll = () => { setSelectedPosts(new Set()) } const handlePublishSingle = async (postId: number) => { setActionLoading(postId) try { await endpoints.publishGenerated(postId) setLocalPosts((prev) => prev.map((p) => p.id === postId ? { ...p, status: 'published' as const } : p ) ) } catch (err) { console.error(`Failed to publish post ${postId}:`, err) } setActionLoading(null) } const handleDeleteSingle = async (postId: number) => { if (!confirm(t('Delete this content?'))) return setActionLoading(postId) try { await endpoints.deleteGenerated(postId) setLocalPosts((prev) => prev.filter((p) => p.id !== postId)) setSelectedPosts((prev) => { const next = new Set(prev) next.delete(postId) return next }) } catch (err) { console.error(`Failed to delete post ${postId}:`, err) } setActionLoading(null) } const handlePublishBulk = async () => { setIsPublishing(true) setPublishResult(null) let success = 0 let failed = 0 for (const postId of selectedPosts) { try { await endpoints.publishGenerated(postId) success++ setLocalPosts((prev) => prev.map((p) => p.id === postId ? { ...p, status: 'published' as const } : p ) ) } catch (err) { console.error(`Failed to publish post ${postId}:`, err) failed++ } } setPublishResult({ success, failed }) setIsPublishing(false) setSelectedPosts(new Set()) if (success > 0) { onPublish(Array.from(selectedPosts)) } } const handleSchedule = async () => { const tomorrow = new Date() tomorrow.setDate(tomorrow.getDate() + 1) tomorrow.setHours(9, 0, 0, 0) setIsPublishing(true) for (const postId of selectedPosts) { try { await endpoints.scheduleGenerated(postId, tomorrow.toISOString()) setLocalPosts((prev) => prev.map((p) => p.id === postId ? { ...p, status: 'scheduled' as const, scheduled_at: tomorrow.toISOString() } : p ) ) } catch (err) { console.error(`Failed to schedule post ${postId}:`, err) } } setIsPublishing(false) onSchedule(Array.from(selectedPosts), tomorrow.toISOString()) } const statusConfig = getStatusConfig() return (

{t('Content Review')}

{t('Review the created content, edit if needed, and approve for publishing')}

{/* Success/Error Message */} {publishResult && ( 0 ? 'bg-yellow-50' : 'bg-green-50' }`} >
0 ? 'text-yellow-600' : 'text-bc-success' }`} />

{publishResult.success > 0 && tf('%d posts published successfully', publishResult.success)} {publishResult.failed > 0 && publishResult.success > 0 && ', '} {publishResult.failed > 0 && tf('%d posts failed', publishResult.failed)}

)} {/* Selection Controls */}
{tf('%d of %d selected', selectedPosts.size, localPosts.length)}
|
{/* Posts List */}
{localPosts.map((post) => { const config = statusConfig[post.status] || statusConfig.draft const isActioning = actionLoading === post.id const isExpanded = expandedPost === post.id return ( {/* Post header row */}
{/* Checkbox */}
{/* Image thumbnail — click opens lightbox or generate dialog */} {post.wp_post_id ? ( ) : (
)} {/* Post info */}

setExpandedPost(isExpanded ? null : post.id)} > {extractTitle(post) || tf('Content #%d', post.id)}

{config.label}
{t('Type:')} {post.post_type} {t('created')} {new Date(post.created_at).toLocaleDateString()} {post.error_message && ( {post.error_message} )}
{/* Action buttons */}
{/* Edit in WordPress */} {post.wp_post_id && ( )} {/* Preview on site */} {post.wp_post_id && ( )} {/* Publish single post */} {(post.status === 'draft' || post.status === 'pending') && ( )} {/* Delete */} {/* Expand/collapse toggle — chevron instead of eye */}
{/* Expandable preview content */} {isExpanded && (
setExpandedPost(null)} hideHeader />
)}
) })}
{/* Navigation */}
{/* Image Lightbox */} {lightboxTarget?.featured_image_url && ( setLightboxTarget(null)} onRegenerate={() => { setLightboxTarget(null) setRegenerateTarget(lightboxTarget) }} /> )} {/* Regenerate Image Dialog */} {regenerateTarget?.wp_post_id && ( setRegenerateTarget(null)} onSuccess={(newUrl, thumbUrl) => { setLocalPosts(prev => prev.map(p => p.id === regenerateTarget.id ? { ...p, featured_image_url: newUrl, featured_image_thumbnail: thumbUrl || newUrl } : p ) ) setRegenerateTarget(null) }} /> )}
) } function extractTitle(post: GeneratedPost): string { if (post.title) return post.title const raw = post.generated_content ?? post.content if (typeof raw === 'object' && raw !== null) { if ((raw as Record).title) return String((raw as Record).title) const content = (raw as Record).content as string | undefined if (content) return extractTitleFromHtml(content) return '' } if (typeof raw === 'string') return extractTitleFromHtml(raw) return '' } function extractTitleFromHtml(html: string): string { const match = html.match(/]*>(.*?)<\/h[12]>/i) if (match) return match[1].replace(/<[^>]+>/g, '') const text = html.replace(/<[^>]+>/g, ' ').trim() return text.slice(0, 100) }