import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' import { useState } from 'react' import { useBreadcrumb } from '../../hooks/useBreadcrumb' import { breadcrumbConfig } from '../../config/breadcrumbConfig' import { FilterBar } from '../../components/FilterPopover' import { Users, User, Plus, RefreshCw, Trash2, ExternalLink, CheckCircle, XCircle, MoreVertical, Clock, Loader2, Download, Info, X, MapPin, Calendar, Link, FolderOpen, Pencil, Search, ChevronDown, ChevronUp, Settings, Filter, Shield, Database, } from 'lucide-react' import { api, getErrorMessage, PaidContentCreator, PaidContentActiveTask, PaidContentCreatorGroup, PaidContentCreatorGroupMember, PaidContentTag } from '../../lib/api' import { formatBytes, formatRelativeTime, formatPlatformName } from '../../lib/utils' import { notificationManager } from '../../lib/notificationManager' // Service base URLs - these match the database defaults const SERVICE_URLS: Record = { coomer: 'https://coomer.party', kemono: 'https://kemono.party', youtube: 'https://www.youtube.com', pornhub: 'https://www.pornhub.com', xhamster: 'https://xhamster.com', tiktok: 'https://www.tiktok.com', instagram: 'https://www.instagram.com', snapchat: 'https://www.snapchat.com', hqcelebcorner: 'https://www.hqcelebcorner.net', picturepub: 'https://picturepub.net', } function getServiceUrl(serviceId: string, platform: string, creatorId: string, username?: string): string { if (serviceId === 'youtube') { // YouTube channel URLs use @ format for handles or /channel/ for IDs if (creatorId.startsWith('UC')) { return `https://www.youtube.com/channel/${creatorId}` } return `https://www.youtube.com/@${creatorId}` } if (serviceId === 'fansly_direct') { // Direct Fansly profile URL return `https://fansly.com/${username || creatorId}` } if (serviceId === 'onlyfans_direct') { return `https://onlyfans.com/${username || creatorId}` } if (serviceId === 'pornhub') { return `https://www.pornhub.com/${creatorId}` } if (serviceId === 'xhamster') { return `https://xhamster.com/${creatorId}` } if (serviceId === 'tiktok') { return `https://www.tiktok.com/@${creatorId}` } if (serviceId === 'instagram') { return `https://www.instagram.com/${creatorId}` } if (serviceId === 'snapchat') { return `https://www.snapchat.com/@${creatorId}` } if (serviceId === 'hqcelebcorner') { return `https://www.hqcelebcorner.net/index.php?search/&q=${encodeURIComponent(creatorId)}&c[title_only]=1&o=date` } if (serviceId === 'picturepub') { return `https://picturepub.net/index.php?search/&q=${encodeURIComponent(creatorId)}&c[title_only]=1&o=date` } if (serviceId === 'coppermine') { return `https://${creatorId}` } const baseUrl = SERVICE_URLS[serviceId] || `https://${serviceId}.su` return `${baseUrl}/${platform}/user/${creatorId}` } // Use centralized platform name formatting function getServiceDisplayName(serviceId: string, platform: string): string { return formatPlatformName(serviceId) || formatPlatformName(platform) } // Domain suffixes that need to be proxied due to CORS/hotlink restrictions const PROXY_DOMAIN_SUFFIXES = [ '.coomer.st', '.coomer.party', '.coomer.su', '.kemono.st', '.kemono.party', '.kemono.su', '.onlyfans.com', '.fansly.com', '.tiktokcdn-us.com', '.tiktokcdn.com', '.xhcdn.com', '.imginn.com', '.cdninstagram.com', ] // Get image URL, proxying if needed for CORS-restricted domains function getProxiedImageUrl(url: string | null | undefined): string | null { if (!url) return null try { const urlObj = new URL(url) const hostname = urlObj.hostname.toLowerCase() if (PROXY_DOMAIN_SUFFIXES.some(suffix => hostname.endsWith(suffix))) { return `/api/paid-content/proxy/image?url=${encodeURIComponent(url)}` } return url } catch { return url } } interface AddCreatorModalProps { isOpen: boolean onClose: () => void onSuccess: () => void } function AddCreatorModal({ isOpen, onClose, onSuccess }: AddCreatorModalProps) { const [url, setUrl] = useState('') const [autoDownload, setAutoDownload] = useState(true) const [downloadEmbeds, setDownloadEmbeds] = useState(true) const [error, setError] = useState('') const addMutation = useMutation({ mutationFn: (data: { url: string; auto_download: boolean; download_embeds: boolean }) => api.paidContent.addCreatorByUrl(data.url, data.auto_download, data.download_embeds), onSuccess: () => { notificationManager.success('Creator Added', 'Creator has been added successfully') onSuccess() handleClose() }, onError: (error: unknown) => { setError(getErrorMessage(error)) }, }) const handleClose = () => { setUrl('') setError('') onClose() } const handleSubmit = (e: React.FormEvent) => { e.preventDefault() setError('') addMutation.mutate({ url, auto_download: autoDownload, download_embeds: downloadEmbeds }) } if (!isOpen) return null return (

Add Creator

Paste a creator URL from any supported platform

setUrl(e.target.value)} placeholder="https://onlyfans.com/username" className="input w-full" required />

Supported platforms:

{[ 'OnlyFans', 'Fansly', 'YouTube', 'Twitch', 'Instagram', 'TikTok', 'Pornhub', 'XHamster', 'Snapchat', 'Reddit', 'Soundgasm', 'Bellazon', 'HQCelebCorner', 'PicturePub', 'BestEyeCandy', 'Coppermine', 'Coomer.party', 'Kemono.party', ].map(name => ( {name} ))}
{error && (
{error}
)}
) } // Use shared types from api.ts type ActiveTask = PaidContentActiveTask function ActiveTaskCard({ task }: { task: ActiveTask }) { const getPhaseIcon = () => { switch (task.phase) { case 'fetching': return case 'processing': return case 'downloading': return case 'backfilling': return default: return } } const getPhaseLabel = () => { switch (task.phase) { case 'fetching': return 'Fetching posts' case 'processing': return 'Processing' case 'downloading': return 'Downloading' case 'backfilling': return 'Backfilling' default: return 'Working' } } const progressValue = task.downloaded || task.progress || 0 const progressPercent = task.total_files ? Math.round((progressValue / task.total_files) * 100) : 0 return (
{getPhaseIcon()}

{task.username}

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

{getPhaseLabel()}

{task.status}

{(task.phase === 'downloading' || task.phase === 'fetching') && task.total_files && (
Progress {progressValue} / {task.total_files} ({progressPercent}%)
{/* Active Downloads */} {task.active_downloads && task.active_downloads.length > 0 && (
Downloading ({task.active_count || task.active_downloads.length}): {task.active_downloads.slice(0, 2).map((dl, idx) => (
{dl.name.length > 30 ? dl.name.slice(0, 30) + '...' : 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 > 2 && ( +{task.active_downloads.length - 2} more... )}
)}
)} {task.phase === 'fetching' && !!task.posts_fetched && !task.total_files && (

{task.posts_fetched} posts fetched

)}
) } function CreatorCard({ creator, onSync, onDelete, onToggleEnabled, onSettings, isSyncing, }: { creator: PaidContentCreator onSync: () => void onDelete: () => void onToggleEnabled: () => void onSettings?: () => void isSyncing: boolean }) { const [showMenu, setShowMenu] = useState(false) const [showBio, setShowBio] = useState(false) const [showLinks, setShowLinks] = useState(false) // Parse external links if available let externalLinks: Array<{ title: string; url: string }> = [] try { externalLinks = creator.external_links ? JSON.parse(creator.external_links) : [] } catch { externalLinks = [] } return (
{/* Banner - fixed height for uniformity */}
{creator.banner_image_url && ( )}
{creator.profile_image_url ? ( {creator.username} ) : (
)}

{creator.display_name || creator.username}

{creator.bio && ( )} {externalLinks.length > 0 && ( )}

@{creator.username}

{(creator.joined_date || creator.location) && (
{creator.joined_date && ( {creator.joined_date} )} {creator.location && ( {creator.location} )}
)}
{showMenu && ( <>
setShowMenu(false)} />
setShowMenu(false)} > View on {getServiceDisplayName(creator.service_id, creator.platform)} {creator.service_id === 'instagram' && ( )} setShowMenu(false)} > Bulk Delete Posts
)}
{formatPlatformName(creator.platform)} {!!creator.use_authenticated_api && ( Authenticated )} {creator.filter_tagged_users && (() => { try { const u = JSON.parse(creator.filter_tagged_users!); return u.length > 0 } catch { return false } })() && ( { try { return JSON.parse(creator.filter_tagged_users!).map((u: string) => '@' + u).join(', ') } catch { return '' } })()}`}> Filtered )} {creator.enabled ? : } {creator.enabled ? 'Enabled' : 'Disabled'}

{creator.post_count || 0}

Posts

{creator.downloaded_count || 0}

Downloaded

{formatBytes(creator.total_size_bytes || 0)}

Size

Last checked: {creator.last_checked ? formatRelativeTime(creator.last_checked) : 'Never'} {isSyncing && ( Syncing... )}
{/* Bio Popup Modal */} {showBio && creator.bio && (
setShowBio(false)} >
e.stopPropagation()} > {/* Header */}
{creator.profile_image_url ? ( {creator.username} ) : (
)}

{creator.display_name || creator.username}

@{creator.username}

{/* Bio Content */}

{creator.bio}

)} {/* External Links Popup Modal */} {showLinks && externalLinks.length > 0 && (
setShowLinks(false)} >
e.stopPropagation()} > {/* Header */}
{creator.profile_image_url ? ( {creator.username} ) : (
)}

{creator.display_name || creator.username}

External Links

{/* Links Content */}
{externalLinks.map((link: { title: string; url: string }, index: number) => (

{link.title}

{link.url}

))}
)}
) } function CreatorSettingsModal({ creator, onClose, }: { creator: PaidContentCreator onClose: () => void }) { const queryClient = useQueryClient() const [syncPosts, setSyncPosts] = useState(creator.sync_posts !== 0) const [syncStories, setSyncStories] = useState(creator.sync_stories !== 0) const [syncHighlights, setSyncHighlights] = useState(creator.sync_highlights !== 0) const [useAuthenticatedApi, setUseAuthenticatedApi] = useState(creator.use_authenticated_api !== 0) const [selectedUsers, setSelectedUsers] = useState(() => { try { return creator.filter_tagged_users ? JSON.parse(creator.filter_tagged_users) : [] } catch { return [] } }) const { data: taggedUsers = [] } = useQuery({ queryKey: ['paid-content-creator-tagged-users', creator.id], queryFn: () => api.paidContent.getCreatorTaggedUsers(creator.id), }) const saveMutation = useMutation({ mutationFn: () => api.paidContent.updateCreator(creator.id, { sync_posts: syncPosts, sync_stories: syncStories, sync_highlights: syncHighlights, use_authenticated_api: useAuthenticatedApi, filter_tagged_users: selectedUsers, }), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['paid-content-creators'] }) queryClient.invalidateQueries({ queryKey: ['paid-content-creators-all'] }) queryClient.invalidateQueries({ queryKey: ['paid-content-feed'] }) notificationManager.success('Settings Saved', 'Sync settings saved successfully') onClose() }, onError: (error) => { notificationManager.error('Save Failed', 'Failed to save settings: ' + getErrorMessage(error)) }, }) const toggleUser = (username: string) => { setSelectedUsers(prev => prev.includes(username) ? prev.filter(u => u !== username) : [...prev, username] ) } return (

Sync Settings — @{creator.username}

{/* Content Types */}

Content Types

{[ { label: 'Sync Posts', value: syncPosts, setter: setSyncPosts }, { label: 'Sync Stories', value: syncStories, setter: setSyncStories }, { label: 'Sync Highlights', value: syncHighlights, setter: setSyncHighlights }, ].map(({ label, value, setter }) => (
{label}
))}
{/* Authenticated API (Instagram only) */} {creator.platform === 'instagram' && (

Authenticated API

Use Authenticated API

Fetches posts using browser cookies as the primary method. Falls back to unauthenticated if cookies expire.

)} {/* Backfill Missing Posts (Instagram only, when authenticated API is enabled) */} {creator.platform === 'instagram' && useAuthenticatedApi && (

Backfill Missing Posts

Scans the full timeline using browser cookies to find and import any posts missing from the database. Automatically downloads new media afterward.

)} {/* Tagged User Filter */}

Only show posts tagged with

When set, only posts where selected users are tagged will appear in the feed

{taggedUsers.length > 0 ? (
{taggedUsers.map(({ username, post_count }) => ( ))}
) : (

No tagged users found for this creator

)}
) } // Service to platforms mapping const SERVICE_PLATFORMS: Record = { coomer: ['onlyfans', 'fansly', 'candfans'], kemono: ['patreon', 'fanbox', 'gumroad', 'subscribestar', 'discord'], fansly_direct: ['fansly'], onlyfans_direct: ['onlyfans'], youtube: ['youtube'], pornhub: ['pornhub'], snapchat: ['snapchat'], hqcelebcorner: ['hqcelebcorner'], picturepub: ['picturepub'], } // ============================================================================ // Member Filter Modal // ============================================================================ function MemberFilterModal({ member, groupId, onClose, }: { member: PaidContentCreatorGroupMember groupId: number onClose: () => void }) { const queryClient = useQueryClient() const [selectedUsers, setSelectedUsers] = useState(() => { try { return member.filter_tagged_users ? JSON.parse(member.filter_tagged_users) : [] } catch { return [] } }) const [selectedTagIds, setSelectedTagIds] = useState(() => { try { return member.filter_tag_ids ? JSON.parse(member.filter_tag_ids) : [] } catch { return [] } }) const { data: taggedUsers = [] } = useQuery({ queryKey: ['paid-content-creator-tagged-users', member.id], queryFn: () => api.paidContent.getCreatorTaggedUsers(member.id), }) const { data: tags = [] } = useQuery({ queryKey: ['paid-content-tags', member.id], queryFn: () => api.paidContent.getTags([member.id]), }) const saveMutation = useMutation({ mutationFn: () => api.paidContent.updateGroupMemberFilters(groupId, member.id, { filter_tagged_users: selectedUsers.length > 0 ? selectedUsers : null, filter_tag_ids: selectedTagIds.length > 0 ? selectedTagIds : null, }), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['paid-content-creator-group', groupId] }) queryClient.invalidateQueries({ queryKey: ['paid-content-feed'] }) notificationManager.success('Filters Saved', `Filters updated for @${member.username}`) onClose() }, onError: (error) => { notificationManager.error('Save Failed', 'Failed to save filters: ' + getErrorMessage(error)) }, }) const toggleUser = (username: string) => { setSelectedUsers(prev => prev.includes(username) ? prev.filter(u => u !== username) : [...prev, username] ) } const toggleTag = (tagId: number) => { setSelectedTagIds(prev => prev.includes(tagId) ? prev.filter(id => id !== tagId) : [...prev, tagId] ) } const hasFilters = selectedUsers.length > 0 || selectedTagIds.length > 0 return (

Feed Filters — @{member.username}

When filters are set, only posts matching at least one filter will appear in the group feed for this creator.

{/* Tagged Users */}

Only show posts tagged with

{taggedUsers.length > 0 ? (
{taggedUsers.map(({ username, post_count }: { username: string; post_count: number }) => ( ))}
) : (

No tagged users found

)}
{/* Tags */}

Only show posts with tags

{tags.length > 0 ? (
{tags.map((tag) => ( ))}
) : (

No tags found

)}
{hasFilters && ( )}
) } // ============================================================================ // Creator Groups Management Component // ============================================================================ function CreatorGroupsManagement() { const queryClient = useQueryClient() const [showCreateForm, setShowCreateForm] = useState(false) const [newGroupName, setNewGroupName] = useState('') const [newGroupDesc, setNewGroupDesc] = useState('') const [expandedGroupId, setExpandedGroupId] = useState(null) const [editingGroupId, setEditingGroupId] = useState(null) const [editName, setEditName] = useState('') const [editDesc, setEditDesc] = useState('') const [addCreatorSearch, setAddCreatorSearch] = useState('') const [showAddCreatorFor, setShowAddCreatorFor] = useState(null) const [editingMemberFilters, setEditingMemberFilters] = useState(null) const { data: groups = [], isLoading } = useQuery({ queryKey: ['paid-content-creator-groups'], queryFn: () => api.paidContent.getCreatorGroups(), }) const { data: expandedGroup } = useQuery({ queryKey: ['paid-content-creator-group', expandedGroupId], queryFn: () => api.paidContent.getCreatorGroup(expandedGroupId!), enabled: !!expandedGroupId, }) const { data: allCreators = [] } = useQuery({ queryKey: ['paid-content-creators-all'], queryFn: () => api.paidContent.getCreators({}), staleTime: 30000, }) const createMutation = useMutation({ mutationFn: () => api.paidContent.createCreatorGroup(newGroupName, newGroupDesc || undefined), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['paid-content-creator-groups'] }) setNewGroupName('') setNewGroupDesc('') setShowCreateForm(false) notificationManager.success('Group Created', 'Creator group has been created') }, onError: (error: unknown) => notificationManager.error('Error', getErrorMessage(error)), }) const updateMutation = useMutation({ mutationFn: ({ id, data }: { id: number; data: { name?: string; description?: string } }) => api.paidContent.updateCreatorGroup(id, data), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['paid-content-creator-groups'] }) queryClient.invalidateQueries({ queryKey: ['paid-content-creator-group'] }) setEditingGroupId(null) notificationManager.success('Group Updated', 'Creator group has been updated') }, onError: (error: unknown) => notificationManager.error('Error', getErrorMessage(error)), }) const deleteMutation = useMutation({ mutationFn: (id: number) => api.paidContent.deleteCreatorGroup(id), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['paid-content-creator-groups'] }) if (expandedGroupId) setExpandedGroupId(null) notificationManager.success('Group Deleted', 'Creator group has been deleted') }, onError: (error: unknown) => notificationManager.error('Error', getErrorMessage(error)), }) const addMemberMutation = useMutation({ mutationFn: ({ groupId, creatorId }: { groupId: number; creatorId: number }) => api.paidContent.addCreatorToGroup(groupId, creatorId), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['paid-content-creator-groups'] }) queryClient.invalidateQueries({ queryKey: ['paid-content-creator-group'] }) }, onError: (error: unknown) => notificationManager.error('Error', getErrorMessage(error)), }) const removeMemberMutation = useMutation({ mutationFn: ({ groupId, creatorId }: { groupId: number; creatorId: number }) => api.paidContent.removeCreatorFromGroup(groupId, creatorId), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['paid-content-creator-groups'] }) queryClient.invalidateQueries({ queryKey: ['paid-content-creator-group'] }) }, onError: (error: unknown) => notificationManager.error('Error', getErrorMessage(error)), }) const memberIds = new Set(expandedGroup?.members?.map((m: PaidContentCreatorGroupMember) => m.id) || []) const filteredAddCreators = allCreators.filter(c => { if (memberIds.has(c.id)) return false if (!addCreatorSearch) return true const q = addCreatorSearch.toLowerCase() return c.username.toLowerCase().includes(q) || (c.display_name?.toLowerCase().includes(q)) }) return (
{/* Create Group */} {!showCreateForm ? ( ) : (

New Group

setNewGroupName(e.target.value)} placeholder="Group name" className="input w-full" autoFocus /> setNewGroupDesc(e.target.value)} placeholder="Description (optional)" className="input w-full" />
)} {/* Groups List */} {isLoading ? (
) : groups.length === 0 ? (

No groups yet

Create a group to organize your creators

) : (
{groups.map((group: PaidContentCreatorGroup) => { const isExpanded = expandedGroupId === group.id const isEditing = editingGroupId === group.id return (
{/* Group Header */}
{ if (isExpanded) { setExpandedGroupId(null) setShowAddCreatorFor(null) } else { setExpandedGroupId(group.id) } }} >
{isEditing ? (
e.stopPropagation()}> setEditName(e.target.value)} className="input text-sm" autoFocus /> setEditDesc(e.target.value)} placeholder="Description" className="input text-sm" />
) : ( <>

{group.name}

{group.description && (

{group.description}

)} )}
{group.member_count} {group.member_count === 1 ? 'creator' : 'creators'} {!isEditing && ( <> )} {isExpanded ? : }
{/* Expanded Members */} {isExpanded && expandedGroup && (
{/* Members */} {expandedGroup.members && expandedGroup.members.length > 0 ? (
{expandedGroup.members.map((member: PaidContentCreatorGroupMember) => { const hasActiveFilters = (member.filter_tagged_users && member.filter_tagged_users !== '[]') || (member.filter_tag_ids && member.filter_tag_ids !== '[]') return (
{member.profile_image_url ? ( {member.username} { (e.target as HTMLImageElement).style.display = 'none' }} /> ) : (
)}
{member.display_name || member.username}
{formatPlatformName(member.platform)}
) })}
) : (

No members yet. Add creators to this group.

)} {/* Add Creator */} {showAddCreatorFor === group.id ? (
setAddCreatorSearch(e.target.value)} placeholder="Search creators to add..." className="input w-full pl-9" autoFocus />
{filteredAddCreators.length === 0 ? (

No creators found

) : ( filteredAddCreators.slice(0, 50).map(creator => ( )) )}
) : ( )}
)}
) })}
)} {editingMemberFilters && expandedGroupId && ( setEditingMemberFilters(null)} /> )}
) } function serviceSort(a: string, b: string) { return formatPlatformName(a).localeCompare(formatPlatformName(b)) } function ServiceGroupedCreators({ creators, syncingIds, onSync, onDelete, onToggleEnabled, onSettings, onSyncService, syncingServices, }: { creators: PaidContentCreator[] syncingIds: number[] onSync: (id: number) => void onDelete: (id: number, username: string) => void onToggleEnabled: (id: number, enabled: boolean) => void onSettings: (creator: PaidContentCreator) => void onSyncService: (serviceId: string) => void syncingServices: Set }) { const [collapsedServices, setCollapsedServices] = useState>(new Set()) // Group creators by service_id const grouped = creators.reduce>((acc, creator) => { const key = creator.service_id if (!acc[key]) acc[key] = [] acc[key].push(creator) return acc }, {}) // Sort creators within each group by username for (const key of Object.keys(grouped)) { grouped[key].sort((a, b) => (a.username || '').localeCompare(b.username || '')) } const serviceIds = Object.keys(grouped).sort(serviceSort) const toggleService = (serviceId: string) => { setCollapsedServices(prev => { const next = new Set(prev) if (next.has(serviceId)) { next.delete(serviceId) } else { next.add(serviceId) } return next }) } // If only one service, show a simple header with sync button but no collapsible if (serviceIds.length <= 1 && serviceIds.length === 1) { const serviceId = serviceIds[0] const isSyncingService = syncingServices.has(serviceId) || creators.some(c => syncingIds.includes(c.id)) return (

{formatPlatformName(serviceId)}

{creators.length} {creators.length === 1 ? 'creator' : 'creators'}
{creators.map((creator) => ( onSync(creator.id)} onDelete={() => onDelete(creator.id, creator.username)} onToggleEnabled={() => onToggleEnabled(creator.id, !creator.enabled)} onSettings={() => onSettings(creator)} isSyncing={syncingIds.includes(creator.id)} /> ))}
) } // No creators if (serviceIds.length === 0) { return (
{creators.map((creator) => ( onSync(creator.id)} onDelete={() => onDelete(creator.id, creator.username)} onToggleEnabled={() => onToggleEnabled(creator.id, !creator.enabled)} onSettings={() => onSettings(creator)} isSyncing={syncingIds.includes(creator.id)} /> ))}
) } return (
{serviceIds.map(serviceId => { const serviceCreators = grouped[serviceId] const isCollapsed = collapsedServices.has(serviceId) return (
{/* Service Header */} ) })()} {isCollapsed ? : }
{/* Creator Grid */} {!isCollapsed && (
{serviceCreators.map((creator) => ( onSync(creator.id)} onDelete={() => onDelete(creator.id, creator.username)} onToggleEnabled={() => onToggleEnabled(creator.id, !creator.enabled)} onSettings={() => onSettings(creator)} isSyncing={syncingIds.includes(creator.id)} /> ))}
)}
) })}
) } type CreatorsTabType = 'creators' | 'groups' export default function PaidContentCreators() { useBreadcrumb(breadcrumbConfig['/paid-content/creators']) const queryClient = useQueryClient() const [showAddModal, setShowAddModal] = useState(false) const [search, setSearch] = useState('') const [filterPlatform, setFilterPlatform] = useState('') const [filterService, setFilterService] = useState('') const [syncingIds, setSyncingIds] = useState([]) const [activeTab, setActiveTab] = useState('creators') const [settingsCreator, setSettingsCreator] = useState(null) const tabs = [ { id: 'creators' as CreatorsTabType, label: 'Creators', icon: Users }, { id: 'groups' as CreatorsTabType, label: 'Groups', icon: FolderOpen }, ] // Poll for active sync tasks const { data: activeSyncs } = useQuery({ queryKey: ['paid-content-active-syncs'], queryFn: () => api.paidContent.getActiveSyncs(), refetchInterval: 2000, staleTime: 1000, }) // Refetch creators more frequently when syncs are active const hasActiveSyncs = activeSyncs && activeSyncs.length > 0 // Fetch all creators to derive filter options const { data: allCreators = [] } = useQuery({ queryKey: ['paid-content-creators-all'], queryFn: () => api.paidContent.getCreators({}), staleTime: 30000, // Cache for 30s }) // Derive unique services and platforms from actual creators const availableServices = [...new Set(allCreators.map(c => c.service_id))].sort() const allPlatforms = [...new Set(allCreators.map(c => c.platform))].sort() // Filter platforms based on selected service const availablePlatforms = filterService ? allPlatforms.filter(p => SERVICE_PLATFORMS[filterService]?.includes(p)) : allPlatforms // Clear platform if it's not valid for the selected service const handleServiceChange = (service: string) => { setFilterService(service) if (service && filterPlatform) { const validPlatforms = SERVICE_PLATFORMS[service] || [] if (!validPlatforms.includes(filterPlatform)) { setFilterPlatform('') } } } const { data: creators, isLoading } = useQuery({ queryKey: ['paid-content-creators', search, filterPlatform, filterService], queryFn: () => api.paidContent.getCreators({ search: search || undefined, platform: filterPlatform || undefined, service: filterService || undefined, }), refetchInterval: hasActiveSyncs ? 3000 : false, // Poll every 3s during active syncs }) const syncMutation = useMutation({ mutationFn: (id: number) => { setSyncingIds((prev) => [...prev, id]) return api.paidContent.syncCreator(id) }, onSuccess: () => { notificationManager.success('Sync Started', 'Creator sync has been queued') }, onError: (error: unknown) => { notificationManager.error('Sync Failed', getErrorMessage(error)) }, onSettled: (_result, _error, id) => { setSyncingIds((prev) => prev.filter((i) => i !== id)) queryClient.invalidateQueries({ queryKey: ['paid-content-creators'] }) }, }) const [syncingServices, setSyncingServices] = useState>(new Set()) const syncServiceMutation = useMutation({ mutationFn: (serviceId: string) => { setSyncingServices(prev => new Set(prev).add(serviceId)) return api.paidContent.syncService(serviceId) }, onSuccess: (_data, serviceId) => { const count = _data?.count ?? 0 notificationManager.success('Service Sync Started', `Queued sync for ${count} ${formatPlatformName(serviceId)} creators`) }, onError: (error: unknown) => { notificationManager.error('Service Sync Failed', getErrorMessage(error)) }, onSettled: (_result, _error, serviceId) => { setSyncingServices(prev => { const next = new Set(prev) next.delete(serviceId) return next }) queryClient.invalidateQueries({ queryKey: ['paid-content-creators'] }) }, }) const toggleMutation = useMutation({ mutationFn: ({ id, enabled }: { id: number; enabled: boolean }) => api.paidContent.updateCreator(id, { enabled }), onSuccess: (_data, variables) => { notificationManager.success( variables.enabled ? 'Creator Enabled' : 'Creator Disabled', variables.enabled ? 'Creator will be included in syncs' : 'Creator will be skipped during syncs' ) queryClient.invalidateQueries({ queryKey: ['paid-content-creators'] }) }, onError: (error: unknown) => { notificationManager.error('Update Failed', getErrorMessage(error)) }, }) const [deleteTarget, setDeleteTarget] = useState<{ id: number; username: string } | null>(null) const [deleteFiles, setDeleteFiles] = useState(false) const deleteMutation = useMutation({ mutationFn: ({ id, deleteFiles: df }: { id: number; deleteFiles: boolean }) => api.paidContent.deleteCreator(id, df), onSuccess: (_data, variables) => { notificationManager.success( 'Creator Removed', variables.deleteFiles ? 'Creator and downloaded files have been removed' : 'Creator has been removed' ) queryClient.invalidateQueries({ queryKey: ['paid-content-creators'] }) setDeleteTarget(null) setDeleteFiles(false) }, onError: (error: unknown) => { notificationManager.error('Delete Failed', getErrorMessage(error)) }, }) const handleDelete = (id: number, username: string) => { setDeleteTarget({ id, username }) setDeleteFiles(false) } return (
{/* Header */}

Creators

Manage tracked creators and groups

{activeTab === 'creators' && ( )}
{/* Tab Navigation */}
{/* Groups Tab */} {activeTab === 'groups' && } {/* Creators Tab */} {activeTab === 'creators' && <> {/* Filters */} ({ value: service, label: formatPlatformName(service), })), ], value: filterService, onChange: (value) => handleServiceChange(value as string), }, { id: 'platform', label: 'Platform', type: 'select', options: [ { value: '', label: 'All Platforms' }, ...availablePlatforms.map(platform => ({ value: platform, label: formatPlatformName(platform), })), ], value: filterPlatform, onChange: (value) => setFilterPlatform(value as string), }, ]} activeFilters={[ ...(search ? [{ id: 'search', label: 'Search', value: search, displayValue: `"${search}"`, onRemove: () => setSearch(''), }] : []), ...(filterService ? [{ id: 'service', label: 'Service', value: filterService, displayValue: formatPlatformName(filterService), onRemove: () => setFilterService(''), }] : []), ...(filterPlatform ? [{ id: 'platform', label: 'Platform', value: filterPlatform, displayValue: formatPlatformName(filterPlatform), onRemove: () => setFilterPlatform(''), }] : []), ]} onClearAll={() => { setSearch('') setFilterService('') setFilterPlatform('') }} totalCount={creators?.length} countLabel="creators" /> {/* Active Tasks */} {activeSyncs && activeSyncs.length > 0 && (

Active Tasks ({activeSyncs.length})

{activeSyncs.map((task) => ( ))}
)} {/* Creator Grid */} {isLoading ? (
) : !creators || creators.length === 0 ? (

No creators found

{search || filterPlatform || filterService ? 'Try adjusting your filters' : 'Add a creator to get started'}

{!search && !filterPlatform && !filterService && ( )}
) : ( syncMutation.mutate(id)} onDelete={(id, username) => handleDelete(id, username)} onToggleEnabled={(id, enabled) => toggleMutation.mutate({ id, enabled })} onSettings={(creator) => setSettingsCreator(creator)} onSyncService={(serviceId) => syncServiceMutation.mutate(serviceId)} syncingServices={syncingServices} /> )} } {/* Creator Settings Modal (Instagram only) */} {settingsCreator && ( setSettingsCreator(null)} /> )} {/* Add Creator Modal */} setShowAddModal(false)} onSuccess={() => queryClient.invalidateQueries({ queryKey: ['paid-content-creators'] })} /> {/* Delete Creator Confirmation Modal */} {deleteTarget && (

Remove Creator

Are you sure you want to remove {deleteTarget.username}?

)}
) }