import { useState } from 'react' import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' import { useBreadcrumb } from '../../hooks/useBreadcrumb' import { breadcrumbConfig } from '../../config/breadcrumbConfig' import { Bell, BellRing, Loader2, AlertCircle, Video, Download, RefreshCw, CheckCircle, XCircle, Info, Clock, Trash2, } from 'lucide-react' import { api, getErrorMessage, PaidContentNotification } from '../../lib/api' import { formatRelativeTime, formatBytes, THUMB_CACHE_V } from '../../lib/utils' import { notificationManager } from '../../lib/notificationManager' import BundleLightbox from '../../components/paid-content/BundleLightbox' import { PaidContentPost, PaidContentAttachment } from '../../lib/api' interface MediaFile { file_path: string filename: string source?: string content_type?: string file_size?: number width?: number height?: number file_type?: string platform?: string post_id?: number | null post_content?: string post_date?: string duration?: number attachment_id?: number downloaded_at?: string } // Create a minimal post object from notification data for BundleLightbox function createMinimalPost(notification: PaidContentNotification, mediaFiles: MediaFile[]): PaidContentPost { // Use post_content from the first media file if available (backend includes this) const firstMedia = mediaFiles[0] const postContent = firstMedia?.post_content || null const postDate = firstMedia?.post_date || notification.created_at return { id: notification.post_id || 0, creator_id: notification.creator_id || 0, post_id: String(notification.post_id || ''), title: null, content: postContent, published_at: postDate, added_at: notification.created_at, has_attachments: 1, attachment_count: mediaFiles.length, downloaded: 1, download_date: notification.created_at, is_favorited: 0, is_viewed: 0, is_pinned: 0, pinned_at: null, username: notification.username || firstMedia?.source || '', platform: notification.platform || firstMedia?.platform || '', service_id: '', display_name: notification.username || firstMedia?.source || null, profile_image_url: null, identity_id: null, attachments: [], } } // Convert notification media files to attachment format function mediaFilesToAttachments(mediaFiles: MediaFile[]): PaidContentAttachment[] { return mediaFiles.map((mf, index) => ({ id: mf.attachment_id || index, post_id: mf.post_id || 0, attachment_index: index, name: mf.filename, file_type: mf.file_type || (isVideoFile(mf.filename) ? 'video' : 'image'), extension: mf.filename.split('.').pop() || null, server_path: null, download_url: null, file_size: mf.file_size || null, width: mf.width || null, height: mf.height || null, duration: mf.duration || null, status: 'completed', local_path: mf.file_path, local_filename: mf.filename, file_hash: null, error_message: null, download_attempts: 0, downloaded_at: mf.downloaded_at || mf.post_date || null, created_at: mf.post_date || null, })) } function isVideoFile(filename: string): boolean { const ext = filename.split('.').pop()?.toLowerCase() return ['mp4', 'mov', 'avi', 'mkv', 'webm', 'm4v'].includes(ext || '') } import LazyThumbnail from '../../components/LazyThumbnail' function NotificationIcon({ type }: { type: string }) { switch (type) { case 'new_content': return (
) case 'download_complete': return (
) case 'sync_complete': return (
) case 'error': return (
) default: return (
) } } export default function PaidContentNotifications() { useBreadcrumb(breadcrumbConfig['/paid-content/notifications']) const queryClient = useQueryClient() const [lightboxNotification, setLightboxNotification] = useState(null) const [lightboxMediaFiles, setLightboxMediaFiles] = useState([]) const [lightboxIndex, setLightboxIndex] = useState(0) const { data: notifications, isLoading } = useQuery({ queryKey: ['paid-content-notifications'], queryFn: () => api.paidContent.getNotifications({}), refetchInterval: 30000, }) const deleteNotificationMutation = useMutation({ mutationFn: (id: number) => api.paidContent.deleteNotification(id), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['paid-content-notifications'] }) notificationManager.success('Deleted', 'Notification removed') }, onError: (error: unknown) => { notificationManager.error('Error', getErrorMessage(error)) }, }) const totalCount = notifications?.length || 0 // Calculate stats const stats = { total: totalCount, byType: notifications?.reduce((acc, n) => { acc[n.notification_type] = (acc[n.notification_type] || 0) + 1 return acc }, {} as Record) || {}, } const getThumbnailUrl = (media: MediaFile) => { // Use ID-based endpoint if available (matches Feed), otherwise fall back to file path if (media.attachment_id) { return `/api/paid-content/files/thumbnail/${media.attachment_id}?size=large&${THUMB_CACHE_V}` } return `/api/paid-content/thumbnail?file_path=${encodeURIComponent(media.file_path)}` } const openLightbox = (notification: PaidContentNotification, mediaFiles: MediaFile[], index: number) => { setLightboxNotification(notification) setLightboxMediaFiles(mediaFiles) setLightboxIndex(index) } return (
{/* Header */}

Notification History

{totalCount} notification{totalCount !== 1 ? 's' : ''}

{/* Stats Cards */}

Total

{stats.total}

Sync Complete

{stats.byType['sync_complete'] || 0}

Errors

{stats.byType['error'] || 0}

{/* Notifications List */}
{isLoading ? (
) : !notifications || notifications.length === 0 ? (

No notifications

No notifications yet

) : ( notifications.map((notification) => { // Enrich media files with post_id from notification const mediaFiles = (notification.metadata?.media_files || []).map(mf => ({ ...mf, post_id: notification.post_id, platform: (mf as any).platform || notification.platform, source: mf.source || notification.username, })) const hasMedia = mediaFiles.length > 0 return (

{notification.title}

{notification.message}

{formatRelativeTime(notification.created_at)} {notification.username && ( @{notification.username} )} {notification.download_count !== undefined && notification.download_count > 0 && ( {notification.download_count} posts )} {notification.file_count !== undefined && notification.file_count > 0 && ( {notification.file_count} files )}
{/* Media Thumbnails Grid */} {hasMedia && (
{mediaFiles.slice(0, 10).map((media: MediaFile, idx: number) => { const isMediaVideo = isVideoFile(media.filename) return (
openLightbox(notification, mediaFiles, idx)} > {/* Video badge - top left with icon and duration */} {isMediaVideo && (
)} {/* Hover overlay with file info */}
{media.filename}
{media.file_size && {formatBytes(media.file_size)}} {media.width && media.height && {media.width}×{media.height}}
) })} {mediaFiles.length > 10 && (
openLightbox(notification, mediaFiles, 10)} > +{mediaFiles.length - 10}
)}
)}
) }) )}
{/* Notification Types Legend */}

Notification Types

New Content
Download Complete
Sync Complete
Error
{/* BundleLightbox */} {lightboxNotification && lightboxMediaFiles.length > 0 && ( { setLightboxNotification(null) setLightboxMediaFiles([]) }} /> )}
) }