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 */}
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 && (
{media.duration ? `${Math.floor(media.duration / 60)}:${(media.duration % 60).toString().padStart(2, '0')}` : 'Video'}
)}
{/* 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([])
}}
/>
)}
)
}