Fix: Intel TLD pagination, Market page pagination + toggle tracking
Some checks failed
CI / Frontend Lint & Type Check (push) Has been cancelled
CI / Frontend Build (push) Has been cancelled
CI / Backend Lint (push) Has been cancelled
CI / Backend Tests (push) Has been cancelled
CI / Docker Build (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
Deploy / Build & Push Images (push) Has been cancelled
Deploy / Deploy to Server (push) Has been cancelled
Deploy / Notify (push) Has been cancelled
Some checks failed
CI / Frontend Lint & Type Check (push) Has been cancelled
CI / Frontend Build (push) Has been cancelled
CI / Backend Lint (push) Has been cancelled
CI / Backend Tests (push) Has been cancelled
CI / Docker Build (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
Deploy / Build & Push Images (push) Has been cancelled
Deploy / Deploy to Server (push) Has been cancelled
Deploy / Notify (push) Has been cancelled
This commit is contained in:
@ -92,33 +92,50 @@ export default function IntelPage() {
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
try {
|
||||
const response = await api.getTldOverview(500, 0, 'popularity')
|
||||
console.log('TLD API Response:', response)
|
||||
// Fetch multiple pages to get more TLDs (API limit is 100 per request)
|
||||
const allTlds: TLDData[] = []
|
||||
let totalRecords = 0
|
||||
|
||||
if (!response || !response.tlds) {
|
||||
for (let offset = 0; offset < 500; offset += 100) {
|
||||
const response = await api.getTldOverview(100, offset, 'popularity')
|
||||
|
||||
if (!response || !response.tlds || response.tlds.length === 0) {
|
||||
break
|
||||
}
|
||||
|
||||
totalRecords = response.total || 0
|
||||
|
||||
const mapped: TLDData[] = response.tlds.map((tld: any) => ({
|
||||
tld: tld.tld || '',
|
||||
min_price: tld.min_registration_price || 0,
|
||||
avg_price: tld.avg_registration_price || 0,
|
||||
max_price: tld.max_registration_price || 0,
|
||||
min_renewal_price: tld.min_renewal_price || 0,
|
||||
avg_renewal_price: tld.avg_renewal_price || 0,
|
||||
price_change_7d: tld.price_change_7d || 0,
|
||||
price_change_1y: tld.price_change_1y || 0,
|
||||
price_change_3y: tld.price_change_3y || 0,
|
||||
risk_level: tld.risk_level || 'low',
|
||||
risk_reason: tld.risk_reason || '',
|
||||
popularity_rank: tld.popularity_rank,
|
||||
type: tld.type,
|
||||
}))
|
||||
|
||||
allTlds.push(...mapped)
|
||||
|
||||
// Stop if we got less than requested (no more data)
|
||||
if (response.tlds.length < 100) break
|
||||
}
|
||||
|
||||
if (allTlds.length === 0) {
|
||||
setError('No TLD data available')
|
||||
setTldData([])
|
||||
setTotal(0)
|
||||
return
|
||||
}
|
||||
|
||||
const mapped: TLDData[] = (response.tlds || []).map((tld: any) => ({
|
||||
tld: tld.tld || '',
|
||||
min_price: tld.min_registration_price || 0,
|
||||
avg_price: tld.avg_registration_price || 0,
|
||||
max_price: tld.max_registration_price || 0,
|
||||
min_renewal_price: tld.min_renewal_price || 0,
|
||||
avg_renewal_price: tld.avg_renewal_price || 0,
|
||||
price_change_7d: tld.price_change_7d || 0,
|
||||
price_change_1y: tld.price_change_1y || 0,
|
||||
price_change_3y: tld.price_change_3y || 0,
|
||||
risk_level: tld.risk_level || 'low',
|
||||
risk_reason: tld.risk_reason || '',
|
||||
popularity_rank: tld.popularity_rank,
|
||||
type: tld.type,
|
||||
}))
|
||||
setTldData(mapped)
|
||||
setTotal(response.total || mapped.length)
|
||||
setTldData(allTlds)
|
||||
setTotal(totalRecords || allTlds.length)
|
||||
} catch (err: any) {
|
||||
console.error('Failed to load TLD data:', err)
|
||||
setError(err.message || 'Failed to load TLD data')
|
||||
|
||||
@ -10,6 +10,8 @@ import {
|
||||
Diamond,
|
||||
Zap,
|
||||
ChevronDown,
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
Plus,
|
||||
Check,
|
||||
TrendingUp,
|
||||
@ -17,6 +19,7 @@ import {
|
||||
Clock,
|
||||
Search,
|
||||
Eye,
|
||||
EyeOff,
|
||||
ShieldCheck,
|
||||
Store,
|
||||
Gavel,
|
||||
@ -123,8 +126,13 @@ export default function MarketPage() {
|
||||
|
||||
const [trackedDomains, setTrackedDomains] = useState<Set<string>>(new Set())
|
||||
const [trackingInProgress, setTrackingInProgress] = useState<string | null>(null)
|
||||
|
||||
// Pagination
|
||||
const [page, setPage] = useState(1)
|
||||
const [totalPages, setTotalPages] = useState(1)
|
||||
const ITEMS_PER_PAGE = 50
|
||||
|
||||
const loadData = useCallback(async () => {
|
||||
const loadData = useCallback(async (currentPage = 1) => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const result = await api.getMarketFeed({
|
||||
@ -136,7 +144,8 @@ export default function MarketPage() {
|
||||
sortBy: sortField === 'score' ? 'score' :
|
||||
sortField === 'price' ? (sortDirection === 'asc' ? 'price_asc' : 'price_desc') :
|
||||
sortField === 'time' ? 'time' : 'newest',
|
||||
limit: 100
|
||||
limit: ITEMS_PER_PAGE,
|
||||
offset: (currentPage - 1) * ITEMS_PER_PAGE
|
||||
})
|
||||
|
||||
setItems(result.items || [])
|
||||
@ -146,6 +155,7 @@ export default function MarketPage() {
|
||||
auctionCount: result.auction_count,
|
||||
highScore: (result.items || []).filter(i => i.pounce_score >= 80).length
|
||||
})
|
||||
setTotalPages(Math.ceil((result.total || 0) / ITEMS_PER_PAGE))
|
||||
} catch (error) {
|
||||
console.error('Failed to load market data:', error)
|
||||
setItems([])
|
||||
@ -154,7 +164,15 @@ export default function MarketPage() {
|
||||
}
|
||||
}, [sourceFilter, searchQuery, priceRange, sortField, sortDirection, tldFilter])
|
||||
|
||||
useEffect(() => { loadData() }, [loadData])
|
||||
useEffect(() => {
|
||||
setPage(1)
|
||||
loadData(1)
|
||||
}, [loadData])
|
||||
|
||||
const handlePageChange = useCallback((newPage: number) => {
|
||||
setPage(newPage)
|
||||
loadData(newPage)
|
||||
}, [loadData])
|
||||
|
||||
const handleRefresh = useCallback(async () => {
|
||||
setRefreshing(true)
|
||||
@ -171,12 +189,42 @@ export default function MarketPage() {
|
||||
}
|
||||
}, [sortField])
|
||||
|
||||
// Load user's tracked domains on mount
|
||||
useEffect(() => {
|
||||
const loadTrackedDomains = async () => {
|
||||
try {
|
||||
const result = await api.getDomains(1, 100)
|
||||
const domainSet = new Set(result.items.map(d => d.domain))
|
||||
setTrackedDomains(domainSet)
|
||||
} catch (error) {
|
||||
console.error('Failed to load tracked domains:', error)
|
||||
}
|
||||
}
|
||||
loadTrackedDomains()
|
||||
}, [])
|
||||
|
||||
const handleTrack = useCallback(async (domain: string) => {
|
||||
if (trackedDomains.has(domain) || trackingInProgress) return
|
||||
if (trackingInProgress) return
|
||||
setTrackingInProgress(domain)
|
||||
|
||||
try {
|
||||
await api.addDomain(domain)
|
||||
setTrackedDomains(prev => new Set([...Array.from(prev), domain]))
|
||||
if (trackedDomains.has(domain)) {
|
||||
// Find and delete the domain
|
||||
const result = await api.getDomains(1, 100)
|
||||
const domainToDelete = result.items.find(d => d.domain === domain)
|
||||
if (domainToDelete) {
|
||||
await api.deleteDomain(domainToDelete.id)
|
||||
setTrackedDomains(prev => {
|
||||
const next = new Set(Array.from(prev))
|
||||
next.delete(domain)
|
||||
return next
|
||||
})
|
||||
}
|
||||
} else {
|
||||
// Add the domain
|
||||
await api.addDomain(domain)
|
||||
setTrackedDomains(prev => new Set([...Array.from(prev), domain]))
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
} finally {
|
||||
@ -425,6 +473,34 @@ export default function MarketPage() {
|
||||
<span>Score: {item.pounce_score}</span>
|
||||
<span>{displayTime || 'Instant'}</span>
|
||||
</div>
|
||||
{/* Mobile Actions */}
|
||||
<div className="flex items-center gap-2 mt-3 pt-3 border-t border-white/[0.05]">
|
||||
<button
|
||||
onClick={() => handleTrack(item.domain)}
|
||||
disabled={isTracking}
|
||||
className={clsx(
|
||||
"flex-1 h-8 flex items-center justify-center gap-2 text-xs font-medium border transition-colors",
|
||||
isTracked
|
||||
? "bg-accent/10 text-accent border-accent/20"
|
||||
: "text-white/50 border-white/10 hover:text-white"
|
||||
)}
|
||||
>
|
||||
{isTracking ? <Loader2 className="w-3 h-3 animate-spin" /> : isTracked ? <Eye className="w-3 h-3" /> : <EyeOff className="w-3 h-3" />}
|
||||
{isTracked ? 'Tracking' : 'Track'}
|
||||
</button>
|
||||
<a
|
||||
href={item.url}
|
||||
target={isPounce ? "_self" : "_blank"}
|
||||
rel={isPounce ? undefined : "noopener noreferrer"}
|
||||
className={clsx(
|
||||
"flex-1 h-8 flex items-center justify-center gap-1.5 text-xs font-semibold",
|
||||
isPounce ? "bg-accent text-black" : "bg-white/10 text-white"
|
||||
)}
|
||||
>
|
||||
{isPounce ? 'Buy' : 'Bid'}
|
||||
{!isPounce && <ExternalLink className="w-3 h-3" />}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Desktop */}
|
||||
@ -504,20 +580,21 @@ export default function MarketPage() {
|
||||
<div className="flex items-center justify-end gap-2 opacity-50 group-hover:opacity-100 transition-opacity">
|
||||
<button
|
||||
onClick={() => handleTrack(item.domain)}
|
||||
disabled={isTracked || isTracking}
|
||||
disabled={isTracking}
|
||||
title={isTracked ? "Stop tracking" : "Start tracking"}
|
||||
className={clsx(
|
||||
"w-7 h-7 flex items-center justify-center border transition-colors",
|
||||
isTracked
|
||||
? "bg-accent/10 text-accent border-accent/20"
|
||||
: "text-white/30 border-white/10 hover:text-white hover:bg-white/5"
|
||||
? "bg-accent/10 text-accent border-accent/20 hover:bg-red-500/10 hover:text-red-400 hover:border-red-500/20"
|
||||
: "text-white/30 border-white/10 hover:text-accent hover:bg-accent/10 hover:border-accent/20"
|
||||
)}
|
||||
>
|
||||
{isTracking ? (
|
||||
<Loader2 className="w-3.5 h-3.5 animate-spin" />
|
||||
) : isTracked ? (
|
||||
<Check className="w-3.5 h-3.5" />
|
||||
) : (
|
||||
<Eye className="w-3.5 h-3.5" />
|
||||
) : (
|
||||
<EyeOff className="w-3.5 h-3.5" />
|
||||
)}
|
||||
</button>
|
||||
|
||||
@ -542,6 +619,63 @@ export default function MarketPage() {
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Pagination */}
|
||||
{!loading && totalPages > 1 && (
|
||||
<div className="flex items-center justify-between pt-6">
|
||||
<div className="text-xs text-white/40 font-mono">
|
||||
Page {page} of {totalPages} · {stats.total} domains
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={() => handlePageChange(page - 1)}
|
||||
disabled={page === 1}
|
||||
className="w-8 h-8 flex items-center justify-center border border-white/10 text-white/50 hover:text-white hover:bg-white/5 disabled:opacity-30 disabled:cursor-not-allowed transition-colors"
|
||||
>
|
||||
<ChevronLeft className="w-4 h-4" />
|
||||
</button>
|
||||
|
||||
{/* Page Numbers */}
|
||||
<div className="flex items-center gap-1">
|
||||
{Array.from({ length: Math.min(5, totalPages) }, (_, i) => {
|
||||
let pageNum: number
|
||||
if (totalPages <= 5) {
|
||||
pageNum = i + 1
|
||||
} else if (page <= 3) {
|
||||
pageNum = i + 1
|
||||
} else if (page >= totalPages - 2) {
|
||||
pageNum = totalPages - 4 + i
|
||||
} else {
|
||||
pageNum = page - 2 + i
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
key={pageNum}
|
||||
onClick={() => handlePageChange(pageNum)}
|
||||
className={clsx(
|
||||
"w-8 h-8 flex items-center justify-center text-xs font-mono border transition-colors",
|
||||
page === pageNum
|
||||
? "bg-accent text-black border-accent"
|
||||
: "border-white/10 text-white/50 hover:text-white hover:bg-white/5"
|
||||
)}
|
||||
>
|
||||
{pageNum}
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={() => handlePageChange(page + 1)}
|
||||
disabled={page === totalPages}
|
||||
className="w-8 h-8 flex items-center justify-center border border-white/10 text-white/50 hover:text-white hover:bg-white/5 disabled:opacity-30 disabled:cursor-not-allowed transition-colors"
|
||||
>
|
||||
<ChevronRight className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
</CommandCenterLayout>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user