Deploy: 2025-12-19 09:35
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:
@ -193,12 +193,11 @@ async def api_check_drop_status(
|
|||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
- available: Domain can be registered NOW
|
- available: Domain can be registered NOW
|
||||||
- pending_delete: Domain is in deletion phase (monitor it!)
|
- dropping_soon: Domain is in deletion phase (track it!)
|
||||||
- redemption: Domain is in redemption period (owner can recover)
|
|
||||||
- taken: Domain was re-registered
|
- taken: Domain was re-registered
|
||||||
- unknown: Could not determine status
|
- unknown: Could not determine status
|
||||||
"""
|
"""
|
||||||
from app.services.domain_checker import domain_checker
|
from app.services.drop_status_checker import check_drop_status
|
||||||
|
|
||||||
# Get the drop from DB
|
# Get the drop from DB
|
||||||
result = await db.execute(
|
result = await db.execute(
|
||||||
@ -212,36 +211,16 @@ async def api_check_drop_status(
|
|||||||
full_domain = f"{drop.domain}.{drop.tld}"
|
full_domain = f"{drop.domain}.{drop.tld}"
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Check with RDAP (not quick mode)
|
# Check with dedicated drop status checker
|
||||||
check_result = await domain_checker.check_domain(full_domain, quick=False)
|
status_result = await check_drop_status(full_domain)
|
||||||
|
|
||||||
# Determine availability status
|
|
||||||
if check_result.is_available:
|
|
||||||
availability_status = "available"
|
|
||||||
rdap_status = check_result.raw_data.get("rdap_status", []) if check_result.raw_data else []
|
|
||||||
|
|
||||||
# Check if it's pending delete (available but not immediately)
|
|
||||||
if rdap_status and any("pending" in str(s).lower() for s in rdap_status):
|
|
||||||
availability_status = "pending_delete"
|
|
||||||
else:
|
|
||||||
# Domain exists - check specific status
|
|
||||||
rdap_status = check_result.raw_data.get("rdap_status", []) if check_result.raw_data else []
|
|
||||||
rdap_str = str(rdap_status).lower()
|
|
||||||
|
|
||||||
if "pending delete" in rdap_str or "pendingdelete" in rdap_str:
|
|
||||||
availability_status = "pending_delete"
|
|
||||||
elif "redemption" in rdap_str:
|
|
||||||
availability_status = "redemption"
|
|
||||||
else:
|
|
||||||
availability_status = "taken"
|
|
||||||
|
|
||||||
# Update the drop in DB
|
# Update the drop in DB
|
||||||
await db.execute(
|
await db.execute(
|
||||||
update(DroppedDomain)
|
update(DroppedDomain)
|
||||||
.where(DroppedDomain.id == drop_id)
|
.where(DroppedDomain.id == drop_id)
|
||||||
.values(
|
.values(
|
||||||
availability_status=availability_status,
|
availability_status=status_result.status,
|
||||||
rdap_status=str(rdap_status) if rdap_status else None,
|
rdap_status=str(status_result.rdap_status) if status_result.rdap_status else None,
|
||||||
last_status_check=datetime.utcnow()
|
last_status_check=datetime.utcnow()
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@ -250,11 +229,11 @@ async def api_check_drop_status(
|
|||||||
return {
|
return {
|
||||||
"id": drop_id,
|
"id": drop_id,
|
||||||
"domain": full_domain,
|
"domain": full_domain,
|
||||||
"availability_status": availability_status,
|
"status": status_result.status,
|
||||||
"rdap_status": rdap_status,
|
"rdap_status": status_result.rdap_status,
|
||||||
"is_available": check_result.is_available,
|
"can_register_now": status_result.can_register_now,
|
||||||
"can_register_now": availability_status == "available",
|
"should_track": status_result.should_monitor,
|
||||||
"can_monitor": availability_status in ("pending_delete", "redemption"),
|
"message": status_result.message,
|
||||||
}
|
}
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@ -262,17 +241,19 @@ async def api_check_drop_status(
|
|||||||
raise HTTPException(status_code=500, detail=str(e))
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
|
||||||
@router.post("/monitor/{drop_id}")
|
@router.post("/track/{drop_id}")
|
||||||
async def api_monitor_drop(
|
async def api_track_drop(
|
||||||
drop_id: int,
|
drop_id: int,
|
||||||
db: AsyncSession = Depends(get_db),
|
db: AsyncSession = Depends(get_db),
|
||||||
current_user = Depends(get_current_user)
|
current_user = Depends(get_current_user)
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Add a dropped domain to the Sniper monitoring list.
|
Add a dropped domain to the user's Watchlist.
|
||||||
Will send notification when domain becomes available.
|
Will send notification when domain becomes available.
|
||||||
|
|
||||||
|
This is the same as adding to watchlist, but optimized for drops.
|
||||||
"""
|
"""
|
||||||
from app.models.sniper_alert import SniperAlert
|
from app.models.domain import Domain, DomainStatus
|
||||||
|
|
||||||
# Get the drop
|
# Get the drop
|
||||||
result = await db.execute(
|
result = await db.execute(
|
||||||
@ -285,49 +266,29 @@ async def api_monitor_drop(
|
|||||||
|
|
||||||
full_domain = f"{drop.domain}.{drop.tld}"
|
full_domain = f"{drop.domain}.{drop.tld}"
|
||||||
|
|
||||||
# Check if already monitoring
|
# Check if already in watchlist
|
||||||
existing = await db.execute(
|
existing = await db.execute(
|
||||||
select(SniperAlert).where(
|
select(Domain).where(
|
||||||
SniperAlert.user_id == current_user.id,
|
Domain.user_id == current_user.id,
|
||||||
SniperAlert.domain == full_domain,
|
Domain.name == full_domain
|
||||||
SniperAlert.is_active == True
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
if existing.scalar_one_or_none():
|
if existing.scalar_one_or_none():
|
||||||
return {"status": "already_monitoring", "domain": full_domain}
|
return {"status": "already_tracking", "domain": full_domain}
|
||||||
|
|
||||||
# Check user limits
|
# Add to watchlist with notification enabled
|
||||||
from app.api.sniper_alerts import get_user_alert_limit
|
domain = Domain(
|
||||||
limit = get_user_alert_limit(current_user)
|
|
||||||
|
|
||||||
count_result = await db.execute(
|
|
||||||
select(SniperAlert).where(
|
|
||||||
SniperAlert.user_id == current_user.id,
|
|
||||||
SniperAlert.is_active == True
|
|
||||||
)
|
|
||||||
)
|
|
||||||
current_count = len(count_result.scalars().all())
|
|
||||||
|
|
||||||
if current_count >= limit:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=400,
|
|
||||||
detail=f"Monitor limit reached ({limit}). Upgrade to add more."
|
|
||||||
)
|
|
||||||
|
|
||||||
# Create sniper alert for this drop
|
|
||||||
alert = SniperAlert(
|
|
||||||
user_id=current_user.id,
|
user_id=current_user.id,
|
||||||
domain=full_domain,
|
name=full_domain,
|
||||||
alert_type="drop",
|
status=DomainStatus.AVAILABLE if drop.availability_status == 'available' else DomainStatus.UNKNOWN,
|
||||||
is_active=True,
|
is_available=drop.availability_status == 'available',
|
||||||
notify_email=True,
|
notify_on_available=True, # Enable notification!
|
||||||
notify_push=True,
|
|
||||||
)
|
)
|
||||||
db.add(alert)
|
db.add(domain)
|
||||||
await db.commit()
|
await db.commit()
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"status": "monitoring",
|
"status": "tracking",
|
||||||
"domain": full_domain,
|
"domain": full_domain,
|
||||||
"message": f"You'll be notified when {full_domain} becomes available!"
|
"message": f"Added {full_domain} to your Watchlist. You'll be notified when available!"
|
||||||
}
|
}
|
||||||
|
|||||||
153
backend/app/services/drop_status_checker.py
Normal file
153
backend/app/services/drop_status_checker.py
Normal file
@ -0,0 +1,153 @@
|
|||||||
|
"""
|
||||||
|
Drop Status Checker
|
||||||
|
====================
|
||||||
|
Dedicated RDAP checker for dropped domains.
|
||||||
|
Correctly identifies pending_delete, redemption, and available status.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import httpx
|
||||||
|
import logging
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# RDAP endpoints for different TLDs
|
||||||
|
RDAP_ENDPOINTS = {
|
||||||
|
# ccTLDs
|
||||||
|
'ch': 'https://rdap.nic.ch/domain/',
|
||||||
|
'li': 'https://rdap.nic.ch/domain/',
|
||||||
|
'de': 'https://rdap.denic.de/domain/',
|
||||||
|
# gTLDs via CentralNic
|
||||||
|
'online': 'https://rdap.centralnic.com/online/domain/',
|
||||||
|
'xyz': 'https://rdap.centralnic.com/xyz/domain/',
|
||||||
|
'club': 'https://rdap.nic.club/domain/',
|
||||||
|
# gTLDs via Afilias/Donuts
|
||||||
|
'info': 'https://rdap.afilias.net/rdap/info/domain/',
|
||||||
|
'biz': 'https://rdap.afilias.net/rdap/biz/domain/',
|
||||||
|
'org': 'https://rdap.publicinterestregistry.org/rdap/org/domain/',
|
||||||
|
# Google TLDs
|
||||||
|
'dev': 'https://rdap.nic.google/domain/',
|
||||||
|
'app': 'https://rdap.nic.google/domain/',
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class DropStatus:
|
||||||
|
"""Status of a dropped domain."""
|
||||||
|
domain: str
|
||||||
|
status: str # 'available', 'dropping_soon', 'taken', 'unknown'
|
||||||
|
rdap_status: list[str]
|
||||||
|
can_register_now: bool
|
||||||
|
should_monitor: bool
|
||||||
|
message: str
|
||||||
|
|
||||||
|
|
||||||
|
async def check_drop_status(domain: str) -> DropStatus:
|
||||||
|
"""
|
||||||
|
Check the real status of a dropped domain via RDAP.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
DropStatus with one of:
|
||||||
|
- 'available': Domain can be registered NOW
|
||||||
|
- 'dropping_soon': Domain is in pending delete/redemption (monitor it!)
|
||||||
|
- 'taken': Domain was re-registered
|
||||||
|
- 'unknown': Could not determine status
|
||||||
|
"""
|
||||||
|
tld = domain.split('.')[-1].lower()
|
||||||
|
|
||||||
|
endpoint = RDAP_ENDPOINTS.get(tld)
|
||||||
|
if not endpoint:
|
||||||
|
# Try generic lookup
|
||||||
|
logger.warning(f"No RDAP endpoint for .{tld}, returning unknown")
|
||||||
|
return DropStatus(
|
||||||
|
domain=domain,
|
||||||
|
status='unknown',
|
||||||
|
rdap_status=[],
|
||||||
|
can_register_now=False,
|
||||||
|
should_monitor=False,
|
||||||
|
message=f"No RDAP endpoint for .{tld}"
|
||||||
|
)
|
||||||
|
|
||||||
|
url = f"{endpoint}{domain}"
|
||||||
|
|
||||||
|
try:
|
||||||
|
async with httpx.AsyncClient(timeout=10) as client:
|
||||||
|
resp = await client.get(url)
|
||||||
|
|
||||||
|
# 404 = Domain not found = AVAILABLE!
|
||||||
|
if resp.status_code == 404:
|
||||||
|
return DropStatus(
|
||||||
|
domain=domain,
|
||||||
|
status='available',
|
||||||
|
rdap_status=[],
|
||||||
|
can_register_now=True,
|
||||||
|
should_monitor=False,
|
||||||
|
message="Domain is available for registration!"
|
||||||
|
)
|
||||||
|
|
||||||
|
# 200 = Domain exists in registry
|
||||||
|
if resp.status_code == 200:
|
||||||
|
data = resp.json()
|
||||||
|
rdap_status = data.get('status', [])
|
||||||
|
status_lower = ' '.join(str(s).lower() for s in rdap_status)
|
||||||
|
|
||||||
|
# Check for pending delete / redemption status
|
||||||
|
is_pending = any(x in status_lower for x in [
|
||||||
|
'pending delete', 'pendingdelete',
|
||||||
|
'pending purge', 'pendingpurge',
|
||||||
|
'redemption period', 'redemptionperiod',
|
||||||
|
'pending restore', 'pendingrestore',
|
||||||
|
])
|
||||||
|
|
||||||
|
if is_pending:
|
||||||
|
return DropStatus(
|
||||||
|
domain=domain,
|
||||||
|
status='dropping_soon',
|
||||||
|
rdap_status=rdap_status,
|
||||||
|
can_register_now=False,
|
||||||
|
should_monitor=True,
|
||||||
|
message="Domain is being deleted. Track it to get notified when available!"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Domain is actively registered
|
||||||
|
return DropStatus(
|
||||||
|
domain=domain,
|
||||||
|
status='taken',
|
||||||
|
rdap_status=rdap_status,
|
||||||
|
can_register_now=False,
|
||||||
|
should_monitor=False,
|
||||||
|
message="Domain was re-registered"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Other status code
|
||||||
|
logger.warning(f"RDAP returned {resp.status_code} for {domain}")
|
||||||
|
return DropStatus(
|
||||||
|
domain=domain,
|
||||||
|
status='unknown',
|
||||||
|
rdap_status=[],
|
||||||
|
can_register_now=False,
|
||||||
|
should_monitor=False,
|
||||||
|
message=f"RDAP returned HTTP {resp.status_code}"
|
||||||
|
)
|
||||||
|
|
||||||
|
except httpx.TimeoutException:
|
||||||
|
logger.warning(f"RDAP timeout for {domain}")
|
||||||
|
return DropStatus(
|
||||||
|
domain=domain,
|
||||||
|
status='unknown',
|
||||||
|
rdap_status=[],
|
||||||
|
can_register_now=False,
|
||||||
|
should_monitor=False,
|
||||||
|
message="RDAP timeout"
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"RDAP error for {domain}: {e}")
|
||||||
|
return DropStatus(
|
||||||
|
domain=domain,
|
||||||
|
status='unknown',
|
||||||
|
rdap_status=[],
|
||||||
|
can_register_now=False,
|
||||||
|
should_monitor=False,
|
||||||
|
message=str(e)
|
||||||
|
)
|
||||||
@ -3,7 +3,6 @@
|
|||||||
import { useState, useEffect, useCallback, useMemo } from 'react'
|
import { useState, useEffect, useCallback, useMemo } from 'react'
|
||||||
import { api } from '@/lib/api'
|
import { api } from '@/lib/api'
|
||||||
import { useAnalyzePanelStore } from '@/lib/analyze-store'
|
import { useAnalyzePanelStore } from '@/lib/analyze-store'
|
||||||
import { useStore } from '@/lib/store'
|
|
||||||
import {
|
import {
|
||||||
Globe,
|
Globe,
|
||||||
Loader2,
|
Loader2,
|
||||||
@ -31,7 +30,7 @@ import clsx from 'clsx'
|
|||||||
// TYPES
|
// TYPES
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
type AvailabilityStatus = 'available' | 'pending_delete' | 'redemption' | 'taken' | 'unknown'
|
type AvailabilityStatus = 'available' | 'dropping_soon' | 'taken' | 'unknown'
|
||||||
|
|
||||||
interface DroppedDomain {
|
interface DroppedDomain {
|
||||||
id: number
|
id: number
|
||||||
@ -74,7 +73,6 @@ interface DropsTabProps {
|
|||||||
|
|
||||||
export function DropsTab({ showToast }: DropsTabProps) {
|
export function DropsTab({ showToast }: DropsTabProps) {
|
||||||
const openAnalyze = useAnalyzePanelStore((s) => s.open)
|
const openAnalyze = useAnalyzePanelStore((s) => s.open)
|
||||||
const addDomain = useStore((s) => s.addDomain)
|
|
||||||
|
|
||||||
// Data State
|
// Data State
|
||||||
const [items, setItems] = useState<DroppedDomain[]>([])
|
const [items, setItems] = useState<DroppedDomain[]>([])
|
||||||
@ -101,12 +99,9 @@ export function DropsTab({ showToast }: DropsTabProps) {
|
|||||||
const [sortField, setSortField] = useState<'domain' | 'length' | 'date'>('length')
|
const [sortField, setSortField] = useState<'domain' | 'length' | 'date'>('length')
|
||||||
const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('asc')
|
const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('asc')
|
||||||
|
|
||||||
// Tracking
|
|
||||||
const [tracking, setTracking] = useState<string | null>(null)
|
|
||||||
|
|
||||||
// Status Checking
|
// Status Checking
|
||||||
const [checkingStatus, setCheckingStatus] = useState<number | null>(null)
|
const [checkingStatus, setCheckingStatus] = useState<number | null>(null)
|
||||||
const [monitoringDrop, setMonitoringDrop] = useState<number | null>(null)
|
const [trackingDrop, setTrackingDrop] = useState<number | null>(null)
|
||||||
|
|
||||||
// Load Stats
|
// Load Stats
|
||||||
const loadStats = useCallback(async () => {
|
const loadStats = useCallback(async () => {
|
||||||
@ -169,19 +164,6 @@ export function DropsTab({ showToast }: DropsTabProps) {
|
|||||||
await loadStats()
|
await loadStats()
|
||||||
}, [loadDrops, loadStats, page])
|
}, [loadDrops, loadStats, page])
|
||||||
|
|
||||||
const track = useCallback(async (domain: string) => {
|
|
||||||
if (tracking) return
|
|
||||||
setTracking(domain)
|
|
||||||
try {
|
|
||||||
await addDomain(domain)
|
|
||||||
showToast(`Added: ${domain}`, 'success')
|
|
||||||
} catch (e) {
|
|
||||||
showToast(e instanceof Error ? e.message : 'Failed', 'error')
|
|
||||||
} finally {
|
|
||||||
setTracking(null)
|
|
||||||
}
|
|
||||||
}, [addDomain, showToast, tracking])
|
|
||||||
|
|
||||||
// Check real-time status of a drop
|
// Check real-time status of a drop
|
||||||
const checkStatus = useCallback(async (dropId: number, domain: string) => {
|
const checkStatus = useCallback(async (dropId: number, domain: string) => {
|
||||||
if (checkingStatus) return
|
if (checkingStatus) return
|
||||||
@ -191,17 +173,11 @@ export function DropsTab({ showToast }: DropsTabProps) {
|
|||||||
// Update the item in our list
|
// Update the item in our list
|
||||||
setItems(prev => prev.map(item =>
|
setItems(prev => prev.map(item =>
|
||||||
item.id === dropId
|
item.id === dropId
|
||||||
? { ...item, availability_status: result.availability_status, last_status_check: new Date().toISOString() }
|
? { ...item, availability_status: result.status, last_status_check: new Date().toISOString() }
|
||||||
: item
|
: item
|
||||||
))
|
))
|
||||||
|
|
||||||
if (result.can_register_now) {
|
showToast(result.message, result.can_register_now ? 'success' : 'info')
|
||||||
showToast(`✅ ${domain} is available NOW!`, 'success')
|
|
||||||
} else if (result.can_monitor) {
|
|
||||||
showToast(`⏳ ${domain} is pending deletion. Add to monitor!`, 'info')
|
|
||||||
} else {
|
|
||||||
showToast(`❌ ${domain} is taken`, 'error')
|
|
||||||
}
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
showToast(e instanceof Error ? e.message : 'Status check failed', 'error')
|
showToast(e instanceof Error ? e.message : 'Status check failed', 'error')
|
||||||
} finally {
|
} finally {
|
||||||
@ -209,23 +185,23 @@ export function DropsTab({ showToast }: DropsTabProps) {
|
|||||||
}
|
}
|
||||||
}, [checkingStatus, showToast])
|
}, [checkingStatus, showToast])
|
||||||
|
|
||||||
// Monitor a pending drop
|
// Track a drop (add to watchlist)
|
||||||
const monitorDrop = useCallback(async (dropId: number, domain: string) => {
|
const trackDrop = useCallback(async (dropId: number, domain: string) => {
|
||||||
if (monitoringDrop) return
|
if (trackingDrop) return
|
||||||
setMonitoringDrop(dropId)
|
setTrackingDrop(dropId)
|
||||||
try {
|
try {
|
||||||
const result = await api.monitorDrop(dropId)
|
const result = await api.trackDrop(dropId)
|
||||||
if (result.status === 'already_monitoring') {
|
if (result.status === 'already_tracking') {
|
||||||
showToast(`Already monitoring ${domain}`, 'info')
|
showToast(`${domain} is already in your Watchlist`, 'info')
|
||||||
} else {
|
} else {
|
||||||
showToast(`🎯 Monitoring ${domain} - you'll be notified!`, 'success')
|
showToast(result.message, 'success')
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
showToast(e instanceof Error ? e.message : 'Failed to monitor', 'error')
|
showToast(e instanceof Error ? e.message : 'Failed to track', 'error')
|
||||||
} finally {
|
} finally {
|
||||||
setMonitoringDrop(null)
|
setTrackingDrop(null)
|
||||||
}
|
}
|
||||||
}, [monitoringDrop, showToast])
|
}, [trackingDrop, showToast])
|
||||||
|
|
||||||
// Sorted Items
|
// Sorted Items
|
||||||
const sortedItems = useMemo(() => {
|
const sortedItems = useMemo(() => {
|
||||||
@ -480,7 +456,7 @@ export function DropsTab({ showToast }: DropsTabProps) {
|
|||||||
<>
|
<>
|
||||||
<div className="border border-white/[0.08] bg-[#020202] overflow-hidden">
|
<div className="border border-white/[0.08] bg-[#020202] overflow-hidden">
|
||||||
{/* Table Header */}
|
{/* Table Header */}
|
||||||
<div className="hidden lg:grid grid-cols-[1fr_80px_110px_100px_200px] 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]">
|
<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
|
<button
|
||||||
onClick={() => handleSort('domain')}
|
onClick={() => handleSort('domain')}
|
||||||
className="flex items-center gap-2 hover:text-white transition-colors text-left"
|
className="flex items-center gap-2 hover:text-white transition-colors text-left"
|
||||||
@ -510,18 +486,16 @@ export function DropsTab({ showToast }: DropsTabProps) {
|
|||||||
<div className="divide-y divide-white/[0.04]">
|
<div className="divide-y divide-white/[0.04]">
|
||||||
{sortedItems.map((item) => {
|
{sortedItems.map((item) => {
|
||||||
const fullDomain = `${item.domain}.${item.tld}`
|
const fullDomain = `${item.domain}.${item.tld}`
|
||||||
const isTracking = tracking === fullDomain
|
|
||||||
const isChecking = checkingStatus === item.id
|
const isChecking = checkingStatus === item.id
|
||||||
const isMonitoring = monitoringDrop === item.id
|
const isTrackingThis = trackingDrop === item.id
|
||||||
const status = item.availability_status || 'unknown'
|
const status = item.availability_status || 'unknown'
|
||||||
|
|
||||||
// Status display config
|
// Simplified status display config
|
||||||
const statusConfig = {
|
const statusConfig = {
|
||||||
available: { label: 'Available', color: 'text-accent', bg: 'bg-accent/10', border: 'border-accent/30', icon: CheckCircle2 },
|
available: { label: 'Available', color: 'text-accent', bg: 'bg-accent/10', border: 'border-accent/30', icon: CheckCircle2 },
|
||||||
pending_delete: { label: 'Pending', color: 'text-amber-400', bg: 'bg-amber-400/10', border: 'border-amber-400/30', icon: Clock },
|
dropping_soon: { label: 'Dropping Soon', color: 'text-amber-400', bg: 'bg-amber-400/10', border: 'border-amber-400/30', icon: Clock },
|
||||||
redemption: { label: 'Redemption', color: 'text-orange-400', bg: 'bg-orange-400/10', border: 'border-orange-400/30', icon: AlertCircle },
|
|
||||||
taken: { label: 'Taken', color: 'text-rose-400', bg: 'bg-rose-400/10', border: 'border-rose-400/30', icon: Ban },
|
taken: { label: 'Taken', color: 'text-rose-400', bg: 'bg-rose-400/10', border: 'border-rose-400/30', icon: Ban },
|
||||||
unknown: { label: 'Check', color: 'text-white/40', bg: 'bg-white/5', border: 'border-white/10', icon: Search },
|
unknown: { label: 'Check', color: 'text-white/50', bg: 'bg-white/5', border: 'border-white/20', icon: Search },
|
||||||
}[status]
|
}[status]
|
||||||
|
|
||||||
const StatusIcon = statusConfig.icon
|
const StatusIcon = statusConfig.icon
|
||||||
@ -573,24 +547,20 @@ export function DropsTab({ showToast }: DropsTabProps) {
|
|||||||
<Zap className="w-4 h-4" />
|
<Zap className="w-4 h-4" />
|
||||||
Buy Now
|
Buy Now
|
||||||
</a>
|
</a>
|
||||||
) : status === 'pending_delete' || status === 'redemption' ? (
|
) : status === 'dropping_soon' ? (
|
||||||
<button
|
<button
|
||||||
onClick={() => monitorDrop(item.id, fullDomain)}
|
onClick={() => trackDrop(item.id, fullDomain)}
|
||||||
disabled={isMonitoring}
|
disabled={isTrackingThis}
|
||||||
className="flex-1 h-12 bg-amber-500 text-black text-xs font-black uppercase tracking-widest flex items-center justify-center gap-2 hover:bg-amber-400 active:scale-[0.98] transition-all"
|
className="flex-1 h-12 bg-amber-500 text-black text-xs font-black uppercase tracking-widest flex items-center justify-center gap-2 hover:bg-amber-400 active:scale-[0.98] transition-all"
|
||||||
>
|
>
|
||||||
{isMonitoring ? <Loader2 className="w-4 h-4 animate-spin" /> : <Eye className="w-4 h-4" />}
|
{isTrackingThis ? <Loader2 className="w-4 h-4 animate-spin" /> : <Eye className="w-4 h-4" />}
|
||||||
Monitor
|
Track & Notify
|
||||||
</button>
|
</button>
|
||||||
) : status === 'taken' ? (
|
) : status === 'taken' ? (
|
||||||
<button
|
<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">
|
||||||
onClick={() => checkStatus(item.id, fullDomain)}
|
<Ban className="w-4 h-4" />
|
||||||
disabled={isChecking}
|
Re-registered
|
||||||
className="flex-1 h-12 border border-white/10 text-white/40 text-xs font-bold uppercase tracking-widest flex items-center justify-center gap-2"
|
</span>
|
||||||
>
|
|
||||||
{isChecking ? <Loader2 className="w-4 h-4 animate-spin" /> : <RefreshCw className="w-4 h-4" />}
|
|
||||||
Recheck
|
|
||||||
</button>
|
|
||||||
) : (
|
) : (
|
||||||
<button
|
<button
|
||||||
onClick={() => checkStatus(item.id, fullDomain)}
|
onClick={() => checkStatus(item.id, fullDomain)}
|
||||||
@ -611,7 +581,7 @@ export function DropsTab({ showToast }: DropsTabProps) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Desktop Row */}
|
{/* Desktop Row */}
|
||||||
<div className="hidden lg:grid grid-cols-[1fr_80px_110px_100px_200px] gap-4 items-center px-6 py-3">
|
<div className="hidden lg:grid grid-cols-[1fr_80px_130px_100px_180px] gap-4 items-center px-6 py-3">
|
||||||
{/* Domain */}
|
{/* Domain */}
|
||||||
<div className="min-w-0">
|
<div className="min-w-0">
|
||||||
<button
|
<button
|
||||||
@ -634,7 +604,7 @@ export function DropsTab({ showToast }: DropsTabProps) {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Status */}
|
{/* Status - clickable to refresh */}
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<button
|
<button
|
||||||
onClick={() => checkStatus(item.id, fullDomain)}
|
onClick={() => checkStatus(item.id, fullDomain)}
|
||||||
@ -657,16 +627,8 @@ export function DropsTab({ showToast }: DropsTabProps) {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Actions */}
|
{/* Actions - simplified */}
|
||||||
<div className="flex items-center justify-end gap-2 opacity-60 group-hover:opacity-100 transition-all">
|
<div className="flex items-center justify-end gap-2 opacity-60 group-hover:opacity-100 transition-all">
|
||||||
<button
|
|
||||||
onClick={() => track(fullDomain)}
|
|
||||||
disabled={isTracking}
|
|
||||||
className="w-9 h-9 flex items-center justify-center border border-white/10 text-white/50 hover:text-white hover:bg-white/5 transition-all"
|
|
||||||
title="Add to Watchlist"
|
|
||||||
>
|
|
||||||
{isTracking ? <Loader2 className="w-3.5 h-3.5 animate-spin" /> : <Eye className="w-3.5 h-3.5" />}
|
|
||||||
</button>
|
|
||||||
<button
|
<button
|
||||||
onClick={() => openAnalyze(fullDomain)}
|
onClick={() => openAnalyze(fullDomain)}
|
||||||
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"
|
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"
|
||||||
@ -687,18 +649,18 @@ export function DropsTab({ showToast }: DropsTabProps) {
|
|||||||
<Zap className="w-3 h-3" />
|
<Zap className="w-3 h-3" />
|
||||||
Buy Now
|
Buy Now
|
||||||
</a>
|
</a>
|
||||||
) : status === 'pending_delete' || status === 'redemption' ? (
|
) : status === 'dropping_soon' ? (
|
||||||
<button
|
<button
|
||||||
onClick={() => monitorDrop(item.id, fullDomain)}
|
onClick={() => trackDrop(item.id, fullDomain)}
|
||||||
disabled={isMonitoring}
|
disabled={isTrackingThis}
|
||||||
className="h-9 px-4 bg-amber-500 text-black text-[10px] font-black uppercase tracking-widest flex items-center gap-1.5 hover:bg-amber-400 transition-all"
|
className="h-9 px-4 bg-amber-500 text-black text-[10px] font-black uppercase tracking-widest flex items-center gap-1.5 hover:bg-amber-400 transition-all"
|
||||||
title="Get notified when available!"
|
title="Add to Watchlist & get notified!"
|
||||||
>
|
>
|
||||||
{isMonitoring ? <Loader2 className="w-3 h-3 animate-spin" /> : <Eye className="w-3 h-3" />}
|
{isTrackingThis ? <Loader2 className="w-3 h-3 animate-spin" /> : <Eye className="w-3 h-3" />}
|
||||||
Monitor
|
Track
|
||||||
</button>
|
</button>
|
||||||
) : status === 'taken' ? (
|
) : status === 'taken' ? (
|
||||||
<span className="h-9 px-4 text-rose-400/60 text-[10px] font-bold uppercase tracking-widest flex items-center gap-1.5 border border-rose-400/20 bg-rose-400/5">
|
<span className="h-9 px-4 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" />
|
<Ban className="w-3 h-3" />
|
||||||
Taken
|
Taken
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@ -2050,7 +2050,7 @@ class AdminApiClient extends ApiClient {
|
|||||||
length: number
|
length: number
|
||||||
is_numeric: boolean
|
is_numeric: boolean
|
||||||
has_hyphen: boolean
|
has_hyphen: boolean
|
||||||
availability_status: 'available' | 'pending_delete' | 'redemption' | 'taken' | 'unknown'
|
availability_status: 'available' | 'dropping_soon' | 'taken' | 'unknown'
|
||||||
last_status_check: string | null
|
last_status_check: string | null
|
||||||
}>
|
}>
|
||||||
}>(`/drops?${query}`)
|
}>(`/drops?${query}`)
|
||||||
@ -2071,20 +2071,20 @@ class AdminApiClient extends ApiClient {
|
|||||||
return this.request<{
|
return this.request<{
|
||||||
id: number
|
id: number
|
||||||
domain: string
|
domain: string
|
||||||
availability_status: 'available' | 'pending_delete' | 'redemption' | 'taken' | 'unknown'
|
status: 'available' | 'dropping_soon' | 'taken' | 'unknown'
|
||||||
rdap_status: string[]
|
rdap_status: string[]
|
||||||
is_available: boolean
|
|
||||||
can_register_now: boolean
|
can_register_now: boolean
|
||||||
can_monitor: boolean
|
should_track: boolean
|
||||||
|
message: string
|
||||||
}>(`/drops/check-status/${dropId}`, { method: 'POST' })
|
}>(`/drops/check-status/${dropId}`, { method: 'POST' })
|
||||||
}
|
}
|
||||||
|
|
||||||
async monitorDrop(dropId: number) {
|
async trackDrop(dropId: number) {
|
||||||
return this.request<{
|
return this.request<{
|
||||||
status: string
|
status: string
|
||||||
domain: string
|
domain: string
|
||||||
message?: string
|
message: string
|
||||||
}>(`/drops/monitor/${dropId}`, { method: 'POST' })
|
}>(`/drops/track/${dropId}`, { method: 'POST' })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user