'use client' import { useState, useEffect, useCallback, useMemo } from 'react' import { api } from '@/lib/api' import { useAnalyzePanelStore } from '@/lib/analyze-store' import { Globe, Loader2, Search, ChevronRight, ChevronLeft, ChevronUp, ChevronDown, RefreshCw, X, Eye, Shield, ExternalLink, Zap, Filter, Ban, Hash, CheckCircle2, AlertCircle, Clock, } from 'lucide-react' import clsx from 'clsx' // ============================================================================ // TYPES // ============================================================================ type AvailabilityStatus = 'available' | 'dropping_soon' | 'taken' | 'unknown' interface DroppedDomain { id: number domain: string tld: string dropped_date: string length: number is_numeric: boolean has_hyphen: boolean availability_status: AvailabilityStatus last_status_check: string | null deletion_date: string | null } interface ZoneStats { ch: { domain_count: number; last_sync: string | null } li: { domain_count: number; last_sync: string | null } daily_drops: number } type SupportedTld = 'ch' | 'li' | 'xyz' | 'org' | 'online' | 'info' | 'dev' | 'app' const ALL_TLDS: { tld: SupportedTld; flag: string }[] = [ { tld: 'ch', flag: '🇨🇭' }, { tld: 'li', flag: '🇱🇮' }, { tld: 'xyz', flag: '🌐' }, { tld: 'org', flag: 'đŸ›ī¸' }, { tld: 'online', flag: 'đŸ’ģ' }, { tld: 'info', flag: 'â„šī¸' }, { tld: 'dev', flag: '👨‍đŸ’ģ' }, { tld: 'app', flag: '📱' }, ] // ============================================================================ // COMPONENT // ============================================================================ interface DropsTabProps { showToast: (message: string, type?: 'success' | 'error' | 'info') => void } export function DropsTab({ showToast }: DropsTabProps) { const openAnalyzePanel = useAnalyzePanelStore((s) => s.open) // Wrapper to open analyze panel with drop status const openAnalyze = useCallback((domain: string, item?: DroppedDomain) => { if (item) { openAnalyzePanel(domain, { status: item.availability_status || 'unknown', deletion_date: item.deletion_date, is_drop: true, }) } else { openAnalyzePanel(domain) } }, [openAnalyzePanel]) // Data State const [items, setItems] = useState([]) const [stats, setStats] = useState(null) const [loading, setLoading] = useState(true) const [refreshing, setRefreshing] = useState(false) const [total, setTotal] = useState(0) // Filter State const [selectedTld, setSelectedTld] = useState(null) const [searchQuery, setSearchQuery] = useState('') const [searchFocused, setSearchFocused] = useState(false) const [minLength, setMinLength] = useState(undefined) const [maxLength, setMaxLength] = useState(undefined) const [excludeNumeric, setExcludeNumeric] = useState(true) const [excludeHyphen, setExcludeHyphen] = useState(true) const [showOnlyAvailable, setShowOnlyAvailable] = useState(false) const [filtersOpen, setFiltersOpen] = useState(false) // Pagination const [page, setPage] = useState(1) const ITEMS_PER_PAGE = 50 // Sorting const [sortField, setSortField] = useState<'domain' | 'length' | 'date'>('length') const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('asc') // Status Checking const [checkingStatus, setCheckingStatus] = useState(null) const [trackingDrop, setTrackingDrop] = useState(null) const [trackedDrops, setTrackedDrops] = useState>(new Set()) // Load Stats const loadStats = useCallback(async () => { try { const result = await api.getDropsStats() setStats(result) } catch (error) { console.error('Failed to load zone stats:', error) } }, []) // Load Drops const loadDrops = useCallback(async (currentPage = 1, isRefresh = false) => { if (isRefresh) setRefreshing(true) else setLoading(true) try { const result = await api.getDrops({ tld: selectedTld || undefined, hours: 24, min_length: minLength, max_length: maxLength, exclude_numeric: excludeNumeric, exclude_hyphen: excludeHyphen, keyword: searchQuery || undefined, limit: ITEMS_PER_PAGE, offset: (currentPage - 1) * ITEMS_PER_PAGE, }) setItems(result.items) setTotal(result.total) } catch (error: any) { console.error('Failed to load drops:', error) if (error.message?.includes('401') || error.message?.includes('auth')) { showToast('Login required to view drops', 'info') } setItems([]) setTotal(0) } finally { setLoading(false) setRefreshing(false) } }, [selectedTld, minLength, maxLength, excludeNumeric, excludeHyphen, searchQuery, showToast]) useEffect(() => { loadStats() }, [loadStats]) useEffect(() => { setPage(1) loadDrops(1) }, [loadDrops]) const handlePageChange = useCallback((newPage: number) => { setPage(newPage) loadDrops(newPage) }, [loadDrops]) const handleRefresh = useCallback(async () => { await loadDrops(page, true) await loadStats() }, [loadDrops, loadStats, page]) // Check real-time status of a drop const checkStatus = useCallback(async (dropId: number, domain: string) => { if (checkingStatus) return setCheckingStatus(dropId) try { const result = await api.checkDropStatus(dropId) // Update the item in our list setItems(prev => prev.map(item => item.id === dropId ? { ...item, availability_status: result.status, last_status_check: new Date().toISOString(), deletion_date: result.deletion_date, } : item )) showToast(result.message, result.can_register_now ? 'success' : 'info') } catch (e) { showToast(e instanceof Error ? e.message : 'Status check failed', 'error') } finally { setCheckingStatus(null) } }, [checkingStatus, showToast]) // Format countdown from deletion date const formatCountdown = useCallback((deletionDate: string | null): string | null => { if (!deletionDate) return null const del = new Date(deletionDate) const now = new Date() const diff = del.getTime() - now.getTime() if (diff <= 0) return 'Now' const days = Math.floor(diff / (1000 * 60 * 60 * 24)) const hours = Math.floor((diff % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60)) const mins = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60)) if (days > 0) return `${days}d ${hours}h` if (hours > 0) return `${hours}h ${mins}m` return `${mins}m` }, []) // Track a drop (add to watchlist) const trackDrop = useCallback(async (dropId: number, domain: string) => { if (trackingDrop) return if (trackedDrops.has(dropId)) { showToast(`${domain} is already in your Watchlist`, 'info') return } setTrackingDrop(dropId) try { const result = await api.trackDrop(dropId) // Mark as tracked regardless of status setTrackedDrops(prev => new Set(prev).add(dropId)) if (result.status === 'already_tracking') { showToast(`${domain} is already in your Watchlist`, 'info') } else { showToast(result.message || `Added ${domain} to Watchlist!`, 'success') } } catch (e) { showToast(e instanceof Error ? e.message : 'Failed to track', 'error') } finally { setTrackingDrop(null) } }, [trackingDrop, trackedDrops, showToast]) // Check if a drop is already tracked const isTracked = useCallback((dropId: number) => trackedDrops.has(dropId), [trackedDrops]) // Filtered and Sorted Items const sortedItems = useMemo(() => { // Filter first if "show only available" is enabled let filtered = items if (showOnlyAvailable) { filtered = items.filter(item => item.availability_status === 'available') } // Then sort const mult = sortDirection === 'asc' ? 1 : -1 return [...filtered].sort((a, b) => { switch (sortField) { case 'domain': return mult * a.domain.localeCompare(b.domain) case 'length': return mult * (a.length - b.length) case 'date': return mult * (new Date(a.dropped_date).getTime() - new Date(b.dropped_date).getTime()) default: return 0 } }) }, [items, sortField, sortDirection, showOnlyAvailable]) // Count available domains const availableCount = useMemo(() => items.filter(item => item.availability_status === 'available').length , [items]) const handleSort = useCallback((field: typeof sortField) => { if (sortField === field) { setSortDirection((d) => (d === 'asc' ? 'desc' : 'asc')) } else { setSortField(field) setSortDirection(field === 'length' ? 'asc' : 'desc') } }, [sortField]) const totalPages = Math.ceil(total / ITEMS_PER_PAGE) const activeFiltersCount = [ selectedTld !== null, minLength !== undefined, maxLength !== undefined, excludeNumeric, excludeHyphen, showOnlyAvailable, ].filter(Boolean).length const formatTime = (iso: string) => { const d = new Date(iso) const now = new Date() const diffH = Math.floor((now.getTime() - d.getTime()) / (1000 * 60 * 60)) if (diffH < 1) return 'Just now' if (diffH === 1) return '1h ago' return `${diffH}h ago` } // Loading State if (loading && items.length === 0) { return (
Loading zone file drops...
) } return (
{/* Header Stats */}
{stats?.daily_drops?.toLocaleString() || total.toLocaleString()}
Fresh drops in last 24h
{/* Search */}
setSearchQuery(e.target.value)} onFocus={() => setSearchFocused(true)} onBlur={() => setSearchFocused(false)} placeholder="Search dropped domains..." className="flex-1 bg-transparent px-4 py-4 text-sm text-white placeholder:text-white/20 outline-none font-mono" /> {searchQuery && ( )}
{/* TLD Quick Filter */}
{ALL_TLDS.map(({ tld }) => ( ))}
{/* Advanced Filters */} {filtersOpen && (
{/* Length Filter */}
Domain Length
setMinLength(e.target.value ? Number(e.target.value) : undefined)} placeholder="Min" className="w-24 bg-white/[0.02] border border-white/10 px-4 py-3 text-sm text-white placeholder:text-white/20 outline-none font-mono focus:border-accent/30 transition-colors" min={1} max={63} /> to setMaxLength(e.target.value ? Number(e.target.value) : undefined)} placeholder="Max" className="w-24 bg-white/[0.02] border border-white/10 px-4 py-3 text-sm text-white placeholder:text-white/20 outline-none font-mono focus:border-accent/30 transition-colors" min={1} max={63} />
{/* Quality Filters */}
{/* Show Only Available Filter */}
)} {/* Results Info */}
{showOnlyAvailable ? sortedItems.length : total.toLocaleString()} domains
{availableCount > 0 && !showOnlyAvailable && (
{availableCount} available now
)}
{totalPages > 1 && !showOnlyAvailable && ( Page {page} of {totalPages} )}
{/* Results Table */} {sortedItems.length === 0 ? (

No drops found

Zone file comparison runs daily. Try adjusting your filters.

) : ( <>
{/* Table Header */}
Status
Actions
{/* Table Body */}
{sortedItems.map((item) => { const fullDomain = `${item.domain}.${item.tld}` const isChecking = checkingStatus === item.id const isTrackingThis = trackingDrop === item.id const alreadyTracked = isTracked(item.id) const status = item.availability_status || 'unknown' // Status display config with better labels const countdown = item.deletion_date ? formatCountdown(item.deletion_date) : null const statusConfig = { available: { label: 'Available Now', color: 'text-accent', bg: 'bg-accent/10', border: 'border-accent/30', icon: CheckCircle2, showBuy: true, }, dropping_soon: { label: countdown ? `In Transition â€ĸ ${countdown}` : 'In Transition', color: 'text-amber-400', bg: 'bg-amber-400/10', border: 'border-amber-400/30', icon: Clock, showBuy: false, }, taken: { label: 'Re-registered', color: 'text-rose-400/60', bg: 'bg-rose-400/5', border: 'border-rose-400/20', icon: Ban, showBuy: false, }, unknown: { label: 'Check Status', color: 'text-white/50', bg: 'bg-white/5', border: 'border-white/20', icon: Search, showBuy: false, }, }[status] const StatusIcon = statusConfig.icon return (
{/* Mobile Row */}
{item.length} chars
{/* Track Button - shows "Tracked" if already in watchlist */} {/* Action Button based on status */} {status === 'available' ? ( Buy Now ) : status === 'dropping_soon' ? (
In Transition {countdown && ( {countdown} until drop )}
) : status === 'taken' ? ( Re-registered ) : ( )}
{/* Desktop Row */}
{/* Domain */}
{/* Length */}
{item.length}
{/* Status - clickable to refresh */}
{/* Time */}
{formatTime(item.dropped_date)}
{/* Actions */}
{/* Track Button - shows checkmark if tracked */} {/* Dynamic Action Button based on status */} {status === 'available' ? ( Buy ) : status === 'dropping_soon' ? ( alreadyTracked ? ( Tracked ) : ( ) ) : status === 'taken' ? ( Taken ) : ( )}
) })}
{/* Pagination */} {totalPages > 1 && (
Page {page} / {totalPages}
)} )}
) }