import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' import { useState } from 'react' import { useBreadcrumb } from '../../hooks/useBreadcrumb' import { breadcrumbConfig } from '../../config/breadcrumbConfig' import { Users, User, FileText, HardDrive, CheckCircle, RefreshCw, ArrowUpRight, XCircle, Download, Gem, AlertCircle, Loader2, Play, Image as ImageIcon, Clock, MessageSquare, Eye, EyeOff, Cookie, Settings, } from 'lucide-react' import { api, getErrorMessage, PaidContentService, PaidContentActiveTask, PaidContentPost, PlatformCredential } from '../../lib/api' import { formatBytes, formatRelativeTime, formatPlatformName, cleanCaption, THUMB_CACHE_V } from '../../lib/utils' import { notificationManager } from '../../lib/notificationManager' import { Link, useNavigate } from 'react-router-dom' import BundleLightbox from '../../components/paid-content/BundleLightbox' // Use shared types from api.ts type ActiveTask = PaidContentActiveTask function StatCard({ title, value, icon: Icon, color = 'blue', subtitle, link, }: { title: string value: string | number icon: React.ElementType color?: 'blue' | 'green' | 'purple' | 'orange' | 'red' subtitle?: string link?: string }) { const colorStyles: Record = { blue: { card: 'stat-card-blue shadow-blue-glow', icon: 'bg-blue-500/20 text-blue-600 dark:text-blue-400', }, green: { card: 'stat-card-green shadow-green-glow', icon: 'bg-emerald-500/20 text-emerald-600 dark:text-emerald-400', }, purple: { card: 'stat-card-purple shadow-purple-glow', icon: 'bg-violet-500/20 text-violet-600 dark:text-violet-400', }, orange: { card: 'stat-card-orange shadow-orange-glow', icon: 'bg-amber-500/20 text-amber-600 dark:text-amber-400', }, red: { card: 'bg-card border border-red-200 dark:border-red-800', icon: 'bg-red-500/20 text-red-600 dark:text-red-400', }, } const styles = colorStyles[color] const content = (

{title}

{value}

{subtitle &&

{subtitle}

}
{link && (
View details
)}
) return link ? {content} : content } function ServiceStatusCard({ service, credential }: { service: PaidContentService; credential?: PlatformCredential }) { const queryClient = useQueryClient() const healthCheckMutation = useMutation({ mutationFn: () => api.paidContent.checkServiceHealth(service.id), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['paid-content-services'] }) notificationManager.success('Health Check', `${service.name} health check completed`) }, onError: (error: unknown) => { notificationManager.error('Health Check Failed', getErrorMessage(error)) }, }) const monitoringMutation = useMutation({ mutationFn: (enabled: boolean) => api.togglePlatformMonitoring(service.id, enabled), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['platform-credentials'] }) queryClient.invalidateQueries({ queryKey: ['cookie-health'] }) }, }) const statusStyles: Record = { healthy: { bg: 'bg-emerald-100 dark:bg-emerald-500/20 text-emerald-700 dark:text-emerald-400', icon: CheckCircle }, degraded: { bg: 'bg-amber-100 dark:bg-amber-500/20 text-amber-700 dark:text-amber-400', icon: AlertCircle }, down: { bg: 'bg-red-100 dark:bg-red-500/20 text-red-700 dark:text-red-400', icon: XCircle }, unknown: { bg: 'bg-slate-100 dark:bg-slate-500/20 text-slate-700 dark:text-slate-400', icon: AlertCircle }, } const status = statusStyles[service.health_status || 'unknown'] || statusStyles.unknown const StatusIcon = status.icon const monitoringEnabled = credential?.monitoring_enabled ?? false const cookiesCount = credential?.cookies_count ?? 0 return (

{service.name}

{service.health_status || 'unknown'} {cookiesCount > 0 && ( {cookiesCount} )}
{service.last_health_check && (

Last checked: {formatRelativeTime(service.last_health_check)}

)}
) } function ActiveTaskCard({ task }: { task: ActiveTask }) { const getPhaseIcon = () => { switch (task.phase) { case 'fetching': return case 'processing': return case 'downloading': return default: return } } const getPhaseLabel = () => { switch (task.phase) { case 'fetching': return 'Fetching posts' case 'processing': return 'Processing' case 'downloading': return 'Downloading' default: return 'Working' } } const progressPercent = task.total_files ? Math.round(((task.downloaded || 0) / task.total_files) * 100) : 0 return (
{getPhaseIcon()}

{task.username}

{formatPlatformName(task.platform)} • {formatPlatformName(task.service)}

{getPhaseLabel()}

{task.status}

{task.phase === 'downloading' && task.total_files && (
Download Progress {task.downloaded || 0} / {task.total_files} files ({progressPercent}%)
{/* Active Downloads */} {task.active_downloads && task.active_downloads.length > 0 && (
Currently downloading ({task.active_count || task.active_downloads.length}): {task.active_downloads.slice(0, 3).map((dl, idx) => (
{dl.name.length > 35 ? dl.name.slice(0, 35) + '...' : dl.name} {formatBytes(dl.progress)}{dl.size ? ` / ${formatBytes(dl.size)}` : ''}
{dl.size && (
0 ? Math.min(100, (dl.progress / dl.size) * 100) : 0}%` }} />
)}
))} {task.active_downloads.length > 3 && ( +{task.active_downloads.length - 3} more... )}
)}
)} {task.phase === 'fetching' && task.posts_fetched !== undefined && (

{task.posts_fetched} posts fetched so far

)}
) } function RecentPostsCard({ posts, onPostClick, onAttachmentClick }: { posts: PaidContentPost[] onPostClick: (post: PaidContentPost) => void onAttachmentClick: (post: PaidContentPost, attachmentIndex: number) => void }) { if (!posts || posts.length === 0) { return (

Recent Posts

No posts yet

) } // Truncate text to ~2 lines const truncateText = (text: string | null, maxLength: number = 80) => { if (!text) return '' // Decode HTML entities const decoded = text.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>') if (decoded.length <= maxLength) return decoded return decoded.slice(0, maxLength).trim() + '...' } return (

Recent Posts

View all
{posts.map((post) => { const completedAttachments = (post.attachments || []).filter( (a) => a.status === 'completed' && (a.file_type === 'image' || a.file_type === 'video') ) const displayAttachments = completedAttachments.slice(0, 4) const extraCount = completedAttachments.length - 4 return (
{/* Post Header - Clickable */}
onPostClick(post)} >
{post.profile_image_url ? ( { (e.target as HTMLImageElement).style.display = 'none' }} /> ) : (
)}
{post.display_name || post.username || 'Unknown'} @{post.username}

{truncateText(cleanCaption(post.content || post.title || ''), 100) || 'No description'}

{formatRelativeTime(post.published_at || post.added_at || '')}

{/* Attachments Grid - Fixed height row */} {displayAttachments.length > 0 && (
{displayAttachments.map((att, idx) => { const isVideo = att.file_type === 'video' return (
{ e.stopPropagation() onAttachmentClick(post, idx) }} > { (e.target as HTMLImageElement).style.display = 'none' }} /> {isVideo && (
Video
)} {idx === 3 && extraCount > 0 && (
+{extraCount}
)}
) })}
)} {/* No attachments placeholder - only show for posts that should have media */} {displayAttachments.length === 0 && post.has_attachments === 1 && (
)}
) })}
) } export default function PaidContentDashboard() { useBreadcrumb(breadcrumbConfig['/paid-content']) const navigate = useNavigate() const queryClient = useQueryClient() const [retryingIds, setRetryingIds] = useState([]) // Lightbox state for recent posts const [lightboxPost, setLightboxPost] = useState(null) const [lightboxIndex, setLightboxIndex] = useState(0) // Poll for active sync tasks (like main dashboard does) const { data: activeSyncs } = useQuery({ queryKey: ['paid-content-active-syncs'], queryFn: async () => { const syncs = await api.paidContent.getActiveSyncs() if (syncs && syncs.length > 0) { console.log('[PaidContent Dashboard] Active syncs:', syncs) } return syncs }, refetchInterval: 2000, // Poll every 2 seconds for real-time updates staleTime: 1000, }) // Refetch stats more frequently when syncs are active const hasActiveSyncs = activeSyncs && activeSyncs.length > 0 const { data: stats, isLoading: statsLoading } = useQuery({ queryKey: ['paid-content-stats'], queryFn: () => api.paidContent.getDashboardStats(), refetchInterval: hasActiveSyncs ? 3000 : 30000, // Poll every 3s during active syncs }) const { data: services, isLoading: servicesLoading } = useQuery({ queryKey: ['paid-content-services'], queryFn: () => api.paidContent.getServices(), refetchInterval: 60000, // Refresh cached status every minute }) const { data: credentialsData } = useQuery({ queryKey: ['platform-credentials'], queryFn: () => api.getPlatformCredentials(), refetchInterval: 60000, }) // Map platform credentials by service id for quick lookup const credentialsByService = (credentialsData?.platforms ?? []).reduce>( (acc, p) => { acc[p.id] = p; return acc }, {} ) const { data: failedDownloads, isLoading: failedLoading } = useQuery({ queryKey: ['paid-content-failed'], queryFn: () => api.paidContent.getFailedDownloads(), refetchInterval: hasActiveSyncs ? 5000 : 60000, // Poll every 5s during active syncs }) // Fetch recent posts for the Recent Posts card const { data: recentPostsData } = useQuery({ queryKey: ['paid-content-recent-posts'], queryFn: () => api.paidContent.getFeed({ limit: 4, sort_by: 'published_at', sort_order: 'desc', pinned_first: false, // Don't prioritize pinned posts in dashboard skip_pinned: true, // Exclude pinned posts from recent posts }), refetchInterval: hasActiveSyncs ? 10000 : 60000, // Refresh more often during active syncs }) const retryMutation = useMutation({ mutationFn: (ids: number[]) => api.paidContent.retryFailedDownloads(ids), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['paid-content-failed'] }) notificationManager.success('Retry Queued', 'Failed downloads queued for retry') setRetryingIds([]) }, onError: (error: unknown) => { notificationManager.error('Retry Failed', getErrorMessage(error)) }, }) const handleRetry = (ids: number[]) => { setRetryingIds(ids) retryMutation.mutate(ids) } const syncAllMutation = useMutation({ mutationFn: () => api.paidContent.syncAllCreators(), onSuccess: () => { notificationManager.success('Sync Started', 'Syncing all creators in background') }, onError: (error: unknown) => { notificationManager.error('Sync Failed', getErrorMessage(error)) }, }) // Unread messages count const { data: unreadMessagesData } = useQuery({ queryKey: ['paid-content-unread-messages-count'], queryFn: () => api.paidContent.getUnreadMessagesCount(), staleTime: 60000, refetchInterval: 60000, }) const unreadMessagesCount = unreadMessagesData?.count ?? 0 // Mark all messages read mutation const markAllMessagesReadMutation = useMutation({ mutationFn: () => api.paidContent.markAllMessagesRead(), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['paid-content-unread-messages-count'] }) }, }) // Unviewed posts count const { data: unviewedData } = useQuery({ queryKey: ['paid-content-unviewed-count'], queryFn: () => api.paidContent.getUnviewedCount(), staleTime: 60000, refetchInterval: 60000, }) const unviewedCount = unviewedData?.count ?? 0 // Mark all posts viewed mutation const markAllViewedMutation = useMutation({ mutationFn: () => api.paidContent.markAllViewed(), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['paid-content-unviewed-count'] }) queryClient.invalidateQueries({ queryKey: ['paid-content-recent-posts'] }) }, }) const isLoading = statsLoading || servicesLoading || failedLoading // Handlers for Recent Posts card const handlePostClick = (post: PaidContentPost) => { // Navigate to feed and open this specific post navigate(`/paid-content/feed?post=${post.id}`) } const handleAttachmentClick = (post: PaidContentPost, attachmentIndex: number) => { setLightboxPost(post) setLightboxIndex(attachmentIndex) } // Get completed attachments for lightbox const lightboxAttachments = lightboxPost?.attachments?.filter( (a) => a.status === 'completed' && (a.file_type === 'image' || a.file_type === 'video') ) || [] return (
{/* Lightbox for Recent Posts attachments */} {lightboxPost && lightboxAttachments.length > 0 && ( setLightboxPost(null)} onNavigate={setLightboxIndex} onViewPost={() => { setLightboxPost(null) navigate(`/paid-content/feed?post=${lightboxPost.id}`) }} /> )} {/* Header */}

Paid Content

Track and download content from creators

{/* Unread messages banner */} {unreadMessagesCount > 0 && (
{unreadMessagesCount}
{unreadMessagesCount === 1 ? '1 unread message' : `${unreadMessagesCount} unread messages`}
View messages
)} {/* Unviewed posts banner */} {unviewedCount > 0 && (
{unviewedCount}
{unviewedCount === 1 ? '1 unviewed post' : `${unviewedCount} unviewed posts`}
View unviewed
)} {isLoading ? (
) : ( <> {/* Stats Grid */}
{/* Recent Posts */} {/* Active Tasks */} {activeSyncs && activeSyncs.length > 0 && (

Active Tasks ({activeSyncs.length})

{activeSyncs.map((task) => ( ))}
)} {/* Services Status */}

Service Status

{services?.map((service) => ( ))}
{/* Two Column Layout */}
{/* Failed Downloads */}

Failed Downloads

{failedDownloads && failedDownloads.length > 0 && ( )}
{!failedDownloads || failedDownloads.length === 0 ? (

No failed downloads

) : (
{failedDownloads.map((download) => (

{download.name}

{download.extension || 'unknown'} file

{download.error_message}

))}
)}
{/* Storage by Creator */}

Storage by Creator

{!stats?.storage_by_creator || stats.storage_by_creator.length === 0 ? (

No storage data yet

) : (
{stats.storage_by_creator.map((creator, index) => (
{creator.profile_image_url ? ( ) : ( )}

{creator.display_name || creator.username}

@{creator.username}

{formatBytes(creator.total_size)}
))}
)}
)}
) }