Yves Gugger c5a9bd83d5 fix: Track endpoint error handling, improve drops UI with tracked state
Backend:
- Fixed track endpoint duplicate key error with proper rollback
- Returns domain_id for already tracked domains

Frontend DropsTab:
- Added trackedDrops state to show "Tracked" status
- Track button shows checkmark when already in watchlist
- Status button shows "In Transition" with countdown

AnalyzePanel:
- Added dropStatus to store for passing drop info
- Shows Drop Status banner with availability
- "Buy Now" button for available domains in panel
2025-12-20 23:29:31 +01:00

879 lines
39 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'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<DroppedDomain[]>([])
const [stats, setStats] = useState<ZoneStats | null>(null)
const [loading, setLoading] = useState(true)
const [refreshing, setRefreshing] = useState(false)
const [total, setTotal] = useState(0)
// Filter State
const [selectedTld, setSelectedTld] = useState<SupportedTld | null>(null)
const [searchQuery, setSearchQuery] = useState('')
const [searchFocused, setSearchFocused] = useState(false)
const [minLength, setMinLength] = useState<number | undefined>(undefined)
const [maxLength, setMaxLength] = useState<number | undefined>(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<number | null>(null)
const [trackingDrop, setTrackingDrop] = useState<number | null>(null)
const [trackedDrops, setTrackedDrops] = useState<Set<number>>(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 (
<div className="flex flex-col items-center justify-center py-24">
<Loader2 className="w-8 h-8 text-accent animate-spin mb-4" />
<span className="text-xs font-mono text-white/30 uppercase tracking-widest">Loading zone file drops...</span>
</div>
)
}
return (
<div className="space-y-6">
{/* Header Stats */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-4">
<div className="w-12 h-12 bg-gradient-to-br from-accent/20 to-accent/5 border border-accent/30 flex items-center justify-center">
<Zap className="w-6 h-6 text-accent" />
</div>
<div>
<div className="text-2xl font-black text-white font-mono tracking-tight">
{stats?.daily_drops?.toLocaleString() || total.toLocaleString()}
</div>
<div className="text-[10px] font-mono text-white/40 uppercase tracking-widest">Fresh drops in last 24h</div>
</div>
</div>
<button
onClick={handleRefresh}
disabled={refreshing}
className="p-3 border border-white/10 text-white/40 hover:text-accent hover:border-accent/30 hover:bg-accent/5 transition-all"
title="Refresh drops"
>
<RefreshCw className={clsx("w-5 h-5", refreshing && "animate-spin")} />
</button>
</div>
{/* Search */}
<div className={clsx(
"relative border-2 transition-all duration-200",
searchFocused ? "border-accent/50 bg-accent/[0.02]" : "border-white/[0.08] bg-white/[0.02]"
)}>
<div className="flex items-center">
<Search className={clsx("w-5 h-5 ml-4 transition-colors", searchFocused ? "text-accent" : "text-white/30")} />
<input
type="text"
value={searchQuery}
onChange={(e) => 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 && (
<button onClick={() => setSearchQuery('')} className="p-4 text-white/30 hover:text-white transition-colors">
<X className="w-5 h-5" />
</button>
)}
</div>
</div>
{/* TLD Quick Filter */}
<div className="flex gap-2 flex-wrap">
<button
onClick={() => setSelectedTld(null)}
className={clsx(
"px-4 py-2.5 text-xs font-mono uppercase tracking-wider border transition-all",
selectedTld === null
? "border-accent bg-accent/10 text-accent font-bold"
: "border-white/[0.08] text-white/40 hover:border-white/20 hover:text-white/60"
)}
>
All TLDs
</button>
{ALL_TLDS.map(({ tld }) => (
<button
key={tld}
onClick={() => setSelectedTld(tld)}
className={clsx(
"px-4 py-2.5 text-xs font-mono uppercase tracking-wider border transition-all",
selectedTld === tld
? "border-accent bg-accent/10 text-accent font-bold"
: "border-white/[0.08] text-white/40 hover:border-white/20 hover:text-white/60"
)}
>
.{tld}
</button>
))}
</div>
{/* Advanced Filters */}
<button
onClick={() => setFiltersOpen(!filtersOpen)}
className={clsx(
"flex items-center justify-between w-full py-3 px-5 border transition-all",
filtersOpen ? "border-accent/30 bg-accent/[0.03]" : "border-white/[0.08] bg-white/[0.02] hover:border-white/20"
)}
>
<div className="flex items-center gap-3">
<Filter className={clsx("w-4 h-4", filtersOpen ? "text-accent" : "text-white/40")} />
<span className={clsx("text-xs font-mono uppercase tracking-widest", filtersOpen ? "text-accent" : "text-white/50")}>
Advanced Filters
</span>
{activeFiltersCount > 0 && (
<span className="px-2 py-0.5 text-[9px] font-black bg-accent text-black">{activeFiltersCount}</span>
)}
</div>
<ChevronRight className={clsx("w-4 h-4 transition-transform", filtersOpen ? "rotate-90 text-accent" : "text-white/30")} />
</button>
{filtersOpen && (
<div className="p-5 border border-white/[0.08] bg-white/[0.01] space-y-5 animate-in fade-in slide-in-from-top-2 duration-200">
{/* Length Filter */}
<div>
<div className="text-[10px] font-mono text-white/40 uppercase tracking-widest mb-3">Domain Length</div>
<div className="flex gap-3 items-center">
<input
type="number"
value={minLength || ''}
onChange={(e) => 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}
/>
<span className="text-white/20 font-mono text-xs">to</span>
<input
type="number"
value={maxLength || ''}
onChange={(e) => 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}
/>
</div>
</div>
{/* Quality Filters */}
<div className="grid grid-cols-2 gap-3">
<button
onClick={() => setExcludeNumeric(!excludeNumeric)}
className={clsx(
"flex items-center justify-between py-3 px-4 border transition-all",
excludeNumeric ? "border-accent/30 bg-accent/5" : "border-white/[0.08] hover:border-white/20"
)}
>
<div className="flex items-center gap-3">
<Hash className={clsx("w-4 h-4", excludeNumeric ? "text-accent" : "text-white/30")} />
<span className={clsx("text-xs font-mono uppercase tracking-wider", excludeNumeric ? "text-accent" : "text-white/50")}>
No Numbers
</span>
</div>
<div className={clsx(
"w-5 h-5 border flex items-center justify-center transition-all",
excludeNumeric ? "border-accent bg-accent" : "border-white/20"
)}>
{excludeNumeric && <CheckCircle2 className="w-3 h-3 text-black" />}
</div>
</button>
<button
onClick={() => setExcludeHyphen(!excludeHyphen)}
className={clsx(
"flex items-center justify-between py-3 px-4 border transition-all",
excludeHyphen ? "border-accent/30 bg-accent/5" : "border-white/[0.08] hover:border-white/20"
)}
>
<div className="flex items-center gap-3">
<Ban className={clsx("w-4 h-4", excludeHyphen ? "text-accent" : "text-white/30")} />
<span className={clsx("text-xs font-mono uppercase tracking-wider", excludeHyphen ? "text-accent" : "text-white/50")}>
No Hyphens
</span>
</div>
<div className={clsx(
"w-5 h-5 border flex items-center justify-center transition-all",
excludeHyphen ? "border-accent bg-accent" : "border-white/20"
)}>
{excludeHyphen && <CheckCircle2 className="w-3 h-3 text-black" />}
</div>
</button>
{/* Show Only Available Filter */}
<button
onClick={() => setShowOnlyAvailable(!showOnlyAvailable)}
className={clsx(
"flex items-center justify-between py-3 px-4 border transition-all",
showOnlyAvailable ? "border-accent/30 bg-accent/5" : "border-white/[0.08] hover:border-white/20"
)}
>
<div className="flex items-center gap-3">
<Zap className={clsx("w-4 h-4", showOnlyAvailable ? "text-accent" : "text-white/30")} />
<span className={clsx("text-xs font-mono uppercase tracking-wider", showOnlyAvailable ? "text-accent" : "text-white/50")}>
Only Available
</span>
</div>
<div className={clsx(
"w-5 h-5 border flex items-center justify-center transition-all",
showOnlyAvailable ? "border-accent bg-accent" : "border-white/20"
)}>
{showOnlyAvailable && <CheckCircle2 className="w-3 h-3 text-black" />}
</div>
</button>
</div>
</div>
)}
{/* Results Info */}
<div className="flex items-center justify-between px-1">
<div className="flex items-center gap-4 text-[11px] font-mono uppercase tracking-widest">
<div className="flex items-center gap-2 text-white/40">
<div className="w-1.5 h-1.5 bg-accent rounded-full animate-pulse" />
<span>{showOnlyAvailable ? sortedItems.length : total.toLocaleString()} domains</span>
</div>
{availableCount > 0 && !showOnlyAvailable && (
<div className="flex items-center gap-2 text-accent">
<Zap className="w-3 h-3" />
<span>{availableCount} available now</span>
</div>
)}
</div>
{totalPages > 1 && !showOnlyAvailable && (
<span className="text-[11px] font-mono text-white/30 uppercase tracking-widest">
Page {page} of {totalPages}
</span>
)}
</div>
{/* Results Table */}
{sortedItems.length === 0 ? (
<div className="text-center py-24 border border-dashed border-white/[0.08] bg-white/[0.01]">
<Globe className="w-16 h-16 text-white/5 mx-auto mb-6" />
<p className="text-white/50 text-sm font-mono uppercase tracking-widest font-bold">No drops found</p>
<p className="text-white/20 text-xs font-mono mt-3 uppercase tracking-wider max-w-sm mx-auto leading-relaxed">
Zone file comparison runs daily. Try adjusting your filters.
</p>
</div>
) : (
<>
<div className="border border-white/[0.08] bg-[#020202] overflow-hidden">
{/* Table Header */}
<div className="hidden lg:grid grid-cols-[1fr_80px_130px_100px_180px] gap-4 px-6 py-4 text-[10px] font-mono text-white/40 uppercase tracking-[0.15em] border-b border-white/[0.08] bg-white/[0.02]">
<button
onClick={() => handleSort('domain')}
className="flex items-center gap-2 hover:text-white transition-colors text-left"
>
<span className={clsx(sortField === 'domain' && "text-accent font-bold")}>Domain</span>
{sortField === 'domain' && (sortDirection === 'asc' ? <ChevronUp className="w-3 h-3 text-accent" /> : <ChevronDown className="w-3 h-3 text-accent" />)}
</button>
<button
onClick={() => handleSort('length')}
className="flex items-center gap-2 justify-center hover:text-white transition-colors"
>
<span className={clsx(sortField === 'length' && "text-accent font-bold")}>Length</span>
{sortField === 'length' && (sortDirection === 'asc' ? <ChevronUp className="w-3 h-3 text-accent" /> : <ChevronDown className="w-3 h-3 text-accent" />)}
</button>
<div className="text-center">Status</div>
<button
onClick={() => handleSort('date')}
className="flex items-center gap-2 justify-center hover:text-white transition-colors"
>
<span className={clsx(sortField === 'date' && "text-accent font-bold")}>Detected</span>
{sortField === 'date' && (sortDirection === 'asc' ? <ChevronUp className="w-3 h-3 text-accent" /> : <ChevronDown className="w-3 h-3 text-accent" />)}
</button>
<div className="text-right">Actions</div>
</div>
{/* Table Body */}
<div className="divide-y divide-white/[0.04]">
{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 (
<div key={`${item.id}-${fullDomain}`} className="group hover:bg-white/[0.02] transition-all">
{/* Mobile Row */}
<div className="lg:hidden p-5">
<div className="flex items-start justify-between gap-4 mb-4">
<div className="min-w-0">
<button
onClick={() => openAnalyze(fullDomain, item)}
className="text-lg font-bold text-white font-mono truncate block text-left hover:text-accent transition-colors"
>
{item.domain}<span className="text-white/30">.{item.tld}</span>
</button>
<div className="flex items-center gap-3 mt-2">
<span className={clsx(
"text-[10px] font-mono font-bold px-2.5 py-1 border",
item.length <= 4 ? "text-accent border-accent/20 bg-accent/5" :
item.length <= 6 ? "text-amber-400 border-amber-400/20 bg-amber-400/5" :
"text-white/40 border-white/10 bg-white/5"
)}>
{item.length} chars
</span>
<button
onClick={() => checkStatus(item.id, fullDomain)}
disabled={isChecking}
className={clsx(
"text-[10px] font-mono font-bold px-2.5 py-1 border flex items-center gap-1.5",
statusConfig.color, statusConfig.bg, statusConfig.border
)}
>
{isChecking ? <Loader2 className="w-3 h-3 animate-spin" /> : <StatusIcon className="w-3 h-3" />}
{statusConfig.label}
</button>
</div>
</div>
</div>
<div className="flex gap-2">
{/* Track Button - shows "Tracked" if already in watchlist */}
<button
onClick={() => trackDrop(item.id, fullDomain)}
disabled={isTrackingThis || alreadyTracked}
className={clsx(
"h-12 px-4 border text-xs font-bold uppercase tracking-widest flex items-center justify-center gap-2 transition-all",
alreadyTracked
? "border-accent/30 text-accent bg-accent/5 cursor-default"
: "border-white/10 text-white/60 hover:bg-white/5 active:scale-[0.98]"
)}
>
{isTrackingThis ? <Loader2 className="w-4 h-4 animate-spin" /> :
alreadyTracked ? <CheckCircle2 className="w-4 h-4" /> : <Eye className="w-4 h-4" />}
{alreadyTracked ? 'Tracked' : 'Track'}
</button>
{/* Action Button based on status */}
{status === 'available' ? (
<a
href={`https://www.namecheap.com/domains/registration/results/?domain=${fullDomain}`}
target="_blank"
rel="noopener noreferrer"
className="flex-1 h-12 bg-accent text-black text-xs font-black uppercase tracking-widest flex items-center justify-center gap-2 hover:bg-white active:scale-[0.98] transition-all"
>
<Zap className="w-4 h-4" />
Buy Now
</a>
) : status === 'dropping_soon' ? (
<div className="flex-1 h-12 border border-amber-400/30 text-amber-400 bg-amber-400/5 text-xs font-bold uppercase tracking-widest flex flex-col items-center justify-center">
<span className="flex items-center gap-1.5">
<Clock className="w-3 h-3" />
In Transition
</span>
{countdown && (
<span className="text-[9px] text-amber-400/70 font-mono">{countdown} until drop</span>
)}
</div>
) : status === 'taken' ? (
<span className="flex-1 h-12 border border-rose-400/20 text-rose-400/60 text-xs font-bold uppercase tracking-widest flex items-center justify-center gap-2 bg-rose-400/5">
<Ban className="w-4 h-4" />
Re-registered
</span>
) : (
<button
onClick={() => checkStatus(item.id, fullDomain)}
disabled={isChecking}
className="flex-1 h-12 border border-accent/30 text-accent text-xs font-bold uppercase tracking-widest flex items-center justify-center gap-2 hover:bg-accent/10 active:scale-[0.98] transition-all"
>
{isChecking ? <Loader2 className="w-4 h-4 animate-spin" /> : <Search className="w-4 h-4" />}
Check Status
</button>
)}
<button
onClick={() => openAnalyze(fullDomain, item)}
className="w-14 h-12 border border-white/10 text-white/50 flex items-center justify-center hover:text-accent hover:border-accent/30 hover:bg-accent/5 transition-all"
>
<Shield className="w-5 h-5" />
</button>
</div>
</div>
{/* Desktop Row */}
<div className="hidden lg:grid grid-cols-[1fr_80px_130px_100px_180px] gap-4 items-center px-6 py-3">
{/* Domain */}
<div className="min-w-0">
<button
onClick={() => openAnalyze(fullDomain, item)}
className="text-sm font-bold text-white font-mono truncate group-hover:text-accent transition-colors text-left block"
>
{item.domain}<span className="text-white/30 group-hover:text-accent/40">.{item.tld}</span>
</button>
</div>
{/* Length */}
<div className="text-center">
<span className={clsx(
"text-xs font-mono font-bold px-3 py-1 border inline-block",
item.length <= 4 ? "text-accent border-accent/20 bg-accent/5" :
item.length <= 6 ? "text-amber-400 border-amber-400/20 bg-amber-400/5" :
"text-white/40 border-white/10 bg-white/5"
)}>
{item.length}
</span>
</div>
{/* Status - clickable to refresh */}
<div className="text-center">
<button
onClick={() => checkStatus(item.id, fullDomain)}
disabled={isChecking}
className={clsx(
"text-[10px] font-mono font-bold px-2.5 py-1.5 border inline-flex items-center gap-1.5 transition-all hover:opacity-80",
statusConfig.color, statusConfig.bg, statusConfig.border
)}
title="Click to check real-time status"
>
{isChecking ? <Loader2 className="w-3 h-3 animate-spin" /> : <StatusIcon className="w-3 h-3" />}
<span className="max-w-[100px] truncate">{statusConfig.label}</span>
</button>
</div>
{/* Time */}
<div className="text-center">
<span className="text-xs font-mono text-white/40 uppercase tracking-wider">
{formatTime(item.dropped_date)}
</span>
</div>
{/* Actions */}
<div className="flex items-center justify-end gap-2 opacity-60 group-hover:opacity-100 transition-all">
{/* Track Button - shows checkmark if tracked */}
<button
onClick={() => trackDrop(item.id, fullDomain)}
disabled={isTrackingThis || alreadyTracked}
className={clsx(
"w-9 h-9 flex items-center justify-center border transition-all",
alreadyTracked
? "border-accent/30 text-accent bg-accent/5 cursor-default"
: "border-white/10 text-white/50 hover:text-white hover:bg-white/5"
)}
title={alreadyTracked ? "Already in Watchlist" : "Add to Watchlist"}
>
{isTrackingThis ? <Loader2 className="w-3.5 h-3.5 animate-spin" /> :
alreadyTracked ? <CheckCircle2 className="w-3.5 h-3.5" /> : <Eye className="w-3.5 h-3.5" />}
</button>
<button
onClick={() => openAnalyze(fullDomain, item)}
className="w-9 h-9 flex items-center justify-center border border-white/10 text-white/50 hover:text-accent hover:border-accent/30 hover:bg-accent/5 transition-all"
title="Analyze Domain"
>
<Shield className="w-3.5 h-3.5" />
</button>
{/* Dynamic Action Button based on status */}
{status === 'available' ? (
<a
href={`https://www.namecheap.com/domains/registration/results/?domain=${fullDomain}`}
target="_blank"
rel="noopener noreferrer"
className="h-9 px-4 bg-accent text-black text-[10px] font-black uppercase tracking-widest flex items-center gap-1.5 hover:bg-white transition-all"
title="Register this domain now!"
>
<Zap className="w-3 h-3" />
Buy
</a>
) : status === 'dropping_soon' ? (
alreadyTracked ? (
<span className="h-9 px-3 text-accent text-[10px] font-bold uppercase tracking-widest flex items-center gap-1.5 border border-accent/30 bg-accent/5">
<CheckCircle2 className="w-3 h-3" />
Tracked
</span>
) : (
<button
onClick={() => trackDrop(item.id, fullDomain)}
disabled={isTrackingThis}
className="h-9 px-3 text-amber-400 text-[10px] font-bold uppercase tracking-widest flex items-center gap-1.5 border border-amber-400/30 bg-amber-400/5 hover:bg-amber-400/10 transition-all"
title={countdown ? `Drops in ${countdown} - Track to get notified!` : 'Track to get notified when available'}
>
{isTrackingThis ? <Loader2 className="w-3 h-3 animate-spin" /> : <Eye className="w-3 h-3" />}
Track
</button>
)
) : status === 'taken' ? (
<span className="h-9 px-3 text-rose-400/50 text-[10px] font-bold uppercase tracking-widest flex items-center gap-1.5 border border-rose-400/20 bg-rose-400/5">
<Ban className="w-3 h-3" />
Taken
</span>
) : (
<button
onClick={() => checkStatus(item.id, fullDomain)}
disabled={isChecking}
className="h-9 px-4 border border-accent/40 text-accent text-[10px] font-bold uppercase tracking-widest flex items-center gap-1.5 hover:bg-accent/10 transition-all"
title="Check availability status"
>
{isChecking ? <Loader2 className="w-3 h-3 animate-spin" /> : <Search className="w-3 h-3" />}
Check
</button>
)}
</div>
</div>
</div>
)
})}
</div>
</div>
{/* Pagination */}
{totalPages > 1 && (
<div className="flex items-center justify-center gap-2 pt-4">
<button
onClick={() => handlePageChange(page - 1)}
disabled={page === 1}
className="w-12 h-12 flex items-center justify-center border border-white/10 text-white/50 hover:text-white hover:bg-white/5 disabled:opacity-20 disabled:cursor-not-allowed transition-all"
>
<ChevronLeft className="w-5 h-5" />
</button>
<div className="flex items-center bg-white/[0.02] border border-white/[0.08] px-6 h-12">
<span className="text-xs text-white/50 font-mono uppercase tracking-widest">
Page <span className="text-white font-bold mx-1">{page}</span> / {totalPages}
</span>
</div>
<button
onClick={() => handlePageChange(page + 1)}
disabled={page === totalPages}
className="w-12 h-12 flex items-center justify-center border border-white/10 text-white/50 hover:text-white hover:bg-white/5 disabled:opacity-20 disabled:cursor-not-allowed transition-all"
>
<ChevronRight className="w-5 h-5" />
</button>
</div>
)}
</>
)}
</div>
)
}