Deploy: 2025-12-19 09:11
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:
318
UX_TERMINAL_UX_REPORT.md
Normal file
318
UX_TERMINAL_UX_REPORT.md
Normal file
@ -0,0 +1,318 @@
|
||||
# UX-Review: Pounce Terminal (Next.js User App)
|
||||
|
||||
Stand: 2025-12-19
|
||||
Scope: **alle Seiten unter** `frontend/src/app/terminal/*` (inkl. Subroute `intel/[tld]`) + globale Terminal-UX (Navigation, Feedback, Loading/Error, Mobile, Accessibility)
|
||||
|
||||
---
|
||||
|
||||
## Executive Summary (was am meisten “zählt”)
|
||||
|
||||
Das Terminal wirkt visuell stark (klare Terminal-Ästhetik, konsistente Farbwelt, guter Einsatz von Stats/Badges). Die größten UX-Hebel liegen aktuell aber weniger im „Look“, sondern in **Konsistenz, Feedback-Loops und Bedienbarkeit**:
|
||||
|
||||
- **P0: Navigation/Chrome ist pro Seite dupliziert** (Mobile Header/Bottom Nav/Drawer) → Inkonsistenzen + Bugs (z.B. aktive Tabs/Routes falsch) + hoher Maintenance-Overhead.
|
||||
- **P0: Keine globalen `loading.tsx` / `error.tsx`** im App Router → schlechte UX bei langsamen API-Calls und Edge-Fehlern.
|
||||
- **P1: Mixed Feedback** (`alert/confirm`, Toast, inline) → mentaler Kontext-Switch + „unbranded“ Dialoge.
|
||||
- **P1: Accessibility & Keyboard UX** (Focus Trap, ESC, ARIA-Rollen, Kontrast/Typografie bei 9–10px) → wirkt “pro”, aber ist für längere Sessions anstrengend.
|
||||
|
||||
---
|
||||
|
||||
## Quick Wins (1–2 Tage, hoher Impact)
|
||||
|
||||
- **Unify Terminal Shell**: Ein einziges Layout für Terminal (Sidebar + Topbar + Mobile Drawer + Bottom Nav) statt Copy/Paste pro Seite.
|
||||
- **App Router Loading/Error**: `frontend/src/app/terminal/loading.tsx` + `frontend/src/app/terminal/error.tsx` (+ ggf. root-level) einführen.
|
||||
- **Mobile Nav Active State fixen**: Aktive Route automatisch über `usePathname()` bestimmen (nicht hardcoded `active: true/false` Arrays).
|
||||
- **Replace `alert/confirm`**: überall ein konsistentes Modal/Toast Pattern.
|
||||
- **Modals**: ESC schließen, Fokus in Modal halten, `role="dialog"`, `aria-modal="true"`, initial focus auf primäres Feld/CTA.
|
||||
|
||||
---
|
||||
|
||||
## P0 / P1 / P2 – Priorisierte UX-Baustellen
|
||||
|
||||
### P0 – Blocker / deutliche UX-Schäden
|
||||
- **Terminal Navigation/Chrome dupliziert**
|
||||
Viele Seiten implementieren eigene Mobile Header/Bottom Nav/Drawer. Das führt zu:
|
||||
- inkonsistenten Labels (Radar/Hunt), unterschiedlichen Menüs, unterschiedlichen Drawer-Sections
|
||||
- Bugs: aktive Navigation ist teils falsch oder gar nicht gesetzt (z.B. `/terminal/listing` hat mobileNavItems alle `active: false`)
|
||||
- fehlende Features in manchen Layouts (z.B. Notifications/Quick Search aus `components/TerminalLayout.tsx` werden in den meisten Terminal-Seiten nie genutzt)
|
||||
|
||||
- **Keine globalen Loading/Error States**
|
||||
Es existieren keine `loading.tsx` / `error.tsx` im `app/` Tree. Bei langsamen Calls (Market Feed, TLD-Loads, WHOIS/Health) entsteht:
|
||||
- „Blank“/Springen zwischen States
|
||||
- inkonsistente Loader-Stile pro Seite
|
||||
- keine zentrale Recovery (Retry, Logging, Support CTA)
|
||||
|
||||
### P1 – hoher Impact, aber kein Blocker
|
||||
- **Mixed Feedback & Confirmation UX**
|
||||
Unterschiedliche Patterns je Seite: Toast (`Toast`), inline error blocks, aber auch `alert()` / `confirm()` (unbranded, nicht mobil-optimiert, blockierend).
|
||||
|
||||
- **Mobile IA/Navigation ist zu “kurz”**
|
||||
Bottom Nav zeigt meist nur 4 Punkte (Hunt/Watch/Portfolio/Intel) → wichtige Module (Yield, Sniper, Inbox, For Sale, Settings) hängen hinter „Menu“, ohne klare Priorisierung oder Badges.
|
||||
|
||||
- **Intel lädt sehr viel Daten “am Client”**
|
||||
`IntelPage` lädt bis zu 500 TLDs in einem Loop (100er chunks). UX-Probleme:
|
||||
- lange initiale Ladezeit
|
||||
- kein echtes Pagination/Search UX (Suche filtert lokal)
|
||||
- mobile wird schnell unübersichtlich
|
||||
|
||||
### P2 – polish / langfristige Qualität
|
||||
- **Typografie & Kontrast**
|
||||
Viele essenzielle Infos sind `text-white/30` oder `text-[9px]` → funktioniert “stylish”, aber wird bei langen Sessions anstrengend.
|
||||
- **Keyboard-first**
|
||||
Das Terminal-Feeling schreit nach Power-User Shortcuts (Cmd+K existiert, aber Search hat noch keine Results UX).
|
||||
|
||||
---
|
||||
|
||||
## Globale Empfehlungen (Terminal-weit)
|
||||
|
||||
### 1) Ein „Terminal Shell“ Layout (Single Source of Truth)
|
||||
Ziel: **Sidebar + Topbar + Mobile Drawer + Mobile Bottom Nav** einmal bauen und Seiten nur noch Content rendern lassen.
|
||||
|
||||
Konkret:
|
||||
- `frontend/src/app/terminal/layout.tsx` (auth guard) ist ok, aber **UI-Chrome sollte zusätzlich** in einem Layout/Wrapper passieren (z.B. `TerminalShellLayout`).
|
||||
- `components/TerminalLayout.tsx` existiert bereits, wird aber primär nur auf `/terminal/welcome` genutzt.
|
||||
Empfehlung: Entweder
|
||||
- **Option A**: `TerminalLayout` so erweitern, dass alle Terminal-Seiten ihn nutzen, oder
|
||||
- **Option B**: neue `TerminalShell` Komponente, die die wiederkehrenden Teile kapselt (Desktop+Mobile).
|
||||
|
||||
**Wichtig**: Mobile Bottom Nav muss aktive Route automatisch bestimmen (z.B. über `usePathname()`), sonst bleibt es fehleranfällig.
|
||||
|
||||
### 2) Konsistentes Feedback-System
|
||||
Standardisieren:
|
||||
- **Errors**: inline „ErrorCard“ mit Retry + „Details“ (expand), und optional „Contact support“ Link.
|
||||
- **Confirms**: eigenes Confirm-Modal (nicht `confirm()`).
|
||||
- **Success**: Toast.
|
||||
- **Long-running**: progress + disable states + ggf. optimistic updates (macht ihr teils schon gut).
|
||||
|
||||
### 3) Modal Quality Bar (Accessibility + UX)
|
||||
Alle Modals (Add/Verify/Thread/Wizards) sollten:
|
||||
- ESC schließen
|
||||
- Fokus im Modal halten (Focus Trap)
|
||||
- `role="dialog"`, `aria-modal="true"`, `aria-labelledby`
|
||||
- initial focus (erstes Input-Feld oder Primary CTA)
|
||||
- „Close“ Button immer sichtbar
|
||||
- bei kritischen Aktionen: klare irreversible Warnung + „Undo“ wo möglich
|
||||
|
||||
### 4) URL-State für Filter & Deep Links
|
||||
Viele Seiten haben Filter/Sort/Search, aber Zustand ist oft nur in React State:
|
||||
- Market: source/tld/price/hideSpam/search/page sollten in Query Params spiegeln (shareable links, back button korrekt).
|
||||
- Intel: Filter und Search ebenfalls in URL.
|
||||
- Inbox: ihr habt schon Query Param `inquiry` + `tab` → gutes Pattern, ausbauen.
|
||||
|
||||
### 5) “System Status” & Datenfrische
|
||||
Für alle datengetriebenen Seiten ein einheitliches Muster:
|
||||
- „Last updated“ + „Refresh“ (nicht überall gleich)
|
||||
- bei Auto-refresh (Watchlist Tycoon): UI-Indikator „Auto-refresh active“ + letzte Hintergrund-Aktualisierung (sonst wirkt es „random“).
|
||||
|
||||
---
|
||||
|
||||
## Seitenreport (Terminal)
|
||||
|
||||
### `/terminal` (Redirect)
|
||||
Ist aktuell ein Spinner + Redirect nach `/terminal/hunt`. UX ok, aber:
|
||||
- **Verbesserung**: kurze Textzeile „Opening Terminal…“ (Barrierefreiheit, Kontext), plus “fallback link” falls Router hängt.
|
||||
|
||||
### `/terminal/welcome`
|
||||
Starkes Upgrade-Confirmation Pattern (Feature Cards, Next Steps).
|
||||
UX-Fixes:
|
||||
- Confetti nutzt `Math.random()` in render → kann bei Re-Renders „flackern“ / unruhig wirken.
|
||||
- CTA Label “Go to Radar” führt nach `/terminal/hunt` (Terminologie konsistent halten: Hunt vs Radar).
|
||||
|
||||
### `/terminal/hunt`
|
||||
Stärken:
|
||||
- Tab UX (Search/Drops/Auctions/Trends/Forge) ist klar und fühlt sich wie ein “Workbench” an.
|
||||
|
||||
Probleme:
|
||||
- Seite rendert eigene Sidebar + Mobile Drawer + Bottom Nav → **duplizierter Shell**.
|
||||
- Mobile Bottom Nav hat „active“ hardcoded; ist hier ok, aber strukturell fragil.
|
||||
|
||||
Empfehlungen:
|
||||
- Tabs als URL-State: `?tab=search|drops|...` (Deep link + Back button).
|
||||
- Top-of-page: “What’s new / last updated” pro Tab (Drops/Auctions/Trends) um Datenfrische sichtbar zu machen.
|
||||
|
||||
### `/terminal/market`
|
||||
Stärken:
|
||||
- Gute Filter, Spam-Hide, Score Badges, Track/Analyze Actions.
|
||||
|
||||
Probleme:
|
||||
- `searchQuery` wird sowohl server-seitig (API `keyword`) als auch client-seitig gefiltert → Doppel-Filter kann zu “WTF”-Momenten führen.
|
||||
- Fehlerfall: bei API error wird `items=[]` gesetzt, aber **kein** Error UI (nur „No domains found“ → falsche Diagnose).
|
||||
- Mobile Drawer/Bottom Nav duplicated.
|
||||
|
||||
Empfehlungen:
|
||||
- Error UI unterscheiden: „0 Results“ vs „Load failed“.
|
||||
- Filter in URL spiegeln.
|
||||
- “Track” UX: wenn Domain schon getrackt ist und Remove passiert, muss UI klar „Removed from Watchlist“ anzeigen (Toast habt ihr), und optional Undo.
|
||||
|
||||
### `/terminal/intel`
|
||||
Stärken:
|
||||
- Tier Gating ist klar visualisiert (Locks, Upgrade CTA).
|
||||
- Sort/Filter/Search UI ist solide.
|
||||
|
||||
Probleme:
|
||||
- Große Client-Loads (bis 500 TLDs in 100er Schritten) → mobile/slow networks leiden.
|
||||
- Duplicate nav/drawer.
|
||||
|
||||
Empfehlungen:
|
||||
- Server-driven pagination: „Top 100 by popularity“ + search server-seitig, infinite scroll optional.
|
||||
- “Renewal trap” als eigenes Filter/Badge prominent (weil es echte Entscheidung beeinflusst).
|
||||
|
||||
### `/terminal/intel/[tld]`
|
||||
Stärken:
|
||||
- Gute „Detail“-Informationsarchitektur (Chart + Domain Check + Registrar Tabelle).
|
||||
- Lock Overlay für Scout ist gut.
|
||||
|
||||
Probleme:
|
||||
- Domain Check Fehler wird nur `console.error`, kein UI-Feedback.
|
||||
- Registrar-Link Fallback `'#'` (führt zu dead click) → besser: Button disabled + Tooltip „No registrar link available“.
|
||||
- Duplicate nav/drawer.
|
||||
|
||||
Empfehlungen:
|
||||
- Domain Check: inline error state + retry.
|
||||
- Chart Tooltip: auf mobile fehlt Hover; optional Tap-to-inspect.
|
||||
|
||||
### `/terminal/watchlist`
|
||||
Stärken:
|
||||
- Sehr gutes Domain Operations UX (Alert toggle, Refresh, Health modal, Expiry tags).
|
||||
|
||||
Probleme:
|
||||
- Mixed confirmations: `confirm('Drop target…')` ist unbranded und blockierend.
|
||||
- Health-Autoload kann bei großen Listen viele Requests erzeugen; UX kann „unruhig“ werden (Loader überall).
|
||||
- Duplicate nav/drawer.
|
||||
|
||||
Empfehlungen:
|
||||
- Confirm Modal + optional Undo „Removed“.
|
||||
- Health Cache: klarer Indikator „Health: cached at …“ vs „live check“.
|
||||
- „Add domain“ Modal: Domain Normalisierung/Validation vor Submit (z.B. whitespace/protocol) + Inline errors.
|
||||
|
||||
### `/terminal/portfolio`
|
||||
Stärken:
|
||||
- Sehr umfangreich, aber erstaunlich gut strukturiert (Assets/Financials Tabs, CFO-Karten, KillList/BurnRate).
|
||||
- DNS Verify Flow ist verständlich (2 Schritte, Copy Button).
|
||||
|
||||
Probleme:
|
||||
- Auto Health checks (bis 20 Domains, delay 300ms) können initial lange dauern; ohne globalen “health loading” Kontext wirkt es wie “random spinners”.
|
||||
- Viele Modals/Overlays, aber Fokus/ESC/ARIA nicht standardisiert.
|
||||
- Duplicate nav/drawer (hier sogar unterschiedliche Drawer-Implementierungen).
|
||||
|
||||
Empfehlungen:
|
||||
- Health prefetch optional machen (Toggle „Preload health on open“).
|
||||
- “Financials” braucht klaren “data computed at” Stempel + “Refresh CFO” prominent.
|
||||
- Vereinheitlichte Drawer/Bottom nav in Shell.
|
||||
|
||||
### `/terminal/listing` (For Sale)
|
||||
Stärken:
|
||||
- Wizard (Create Listing) ist ein gutes Pattern (Step 1–3).
|
||||
- Leads Modal mit Thread UI ist sehr wertvoll (reale Workflows).
|
||||
|
||||
Probleme:
|
||||
- Mobile Bottom Nav: `active` ist überall false → **Navigation Feedback kaputt**.
|
||||
- Mixed feedback: `alert()` bei errors (publish/delete) → unbranded.
|
||||
- Wizard Step 2: “DNS can take 1–5 minutes” ist ok, aber es fehlen “Where exactly to set TXT record?” Verweise/Links pro Registrar.
|
||||
|
||||
Empfehlungen:
|
||||
- Active Nav fixen (global).
|
||||
- Alerts/confirm ersetzen (Toast + Modal).
|
||||
- Wizard: “Copy all fields” + “Open registrar docs” optional.
|
||||
- Leads/Thread: auto-scroll to latest message + “unread” marker.
|
||||
|
||||
### `/terminal/inbox`
|
||||
Stärken:
|
||||
- Buying vs Selling Tabs; query-params unterstütztes Deep-Linking (gut!).
|
||||
- Thread Pane Layout auf Desktop ist sinnvoll.
|
||||
|
||||
Probleme:
|
||||
- Polling (15s messages, 30s threads) ohne sichtbaren „sync“ Indikator; kann „messages jumpen“ oder user verunsichern.
|
||||
- Mobile Drawer ist sehr reduziert (Hunt/Watchlist/For Sale) → Missing core modules.
|
||||
|
||||
Empfehlungen:
|
||||
- „Last synced“ + kleiner Spinner beim Poll refresh.
|
||||
- Thread: “Sending…” state im Message Bubble optimistisch anzeigen.
|
||||
- Mobile: Navigation vereinheitlichen (Shell).
|
||||
|
||||
### `/terminal/sniper`
|
||||
Stärken:
|
||||
- Übersicht mit Active/Matches/Sent Stats.
|
||||
- Create/Edit Modal deckt viele Filter ab.
|
||||
|
||||
Probleme:
|
||||
- Mixed feedback: `alert()` bei toggle/delete errors.
|
||||
- Limit messaging: Scout maxAlerts=0, aber Settings-Plan Copy sagt “2 Sniper Alerts” → **Widerspruch** (führt zu Trust-Bruch).
|
||||
|
||||
Empfehlungen:
|
||||
- Limits und Copy überall konsistent (Settings, Pricing, Backend).
|
||||
- “Preview matches” wäre stark: pro Alert ein „Show matches“ (wenn Backend vorhanden).
|
||||
|
||||
### `/terminal/yield`
|
||||
Stärken:
|
||||
- Aktivierungsflow (Select Domain → DNS Setup → Verify) ist klar.
|
||||
- Dashboard Tabelle liefert „operative“ KPIs.
|
||||
|
||||
Probleme:
|
||||
- **Hardcoded IP** `46.235.147.194` im Frontend: UX/Trust & Ops-Risiko (kann sich ändern, Multi-env schwierig).
|
||||
- Sehr viel `alert()/confirm()` für Verify/Delete → unbranded, keine Recovery UI.
|
||||
- Tier-Gating Messaging: Modal Titel „Preview Yield Landing“ auch für Trader/Tycoon; das ist ok, aber sollte klarer sein.
|
||||
|
||||
Empfehlungen:
|
||||
- IP & DNS instructions server/ENV-driven (z.B. `NEXT_PUBLIC_YIELD_SERVER_IP` + Backend liefert aktuelle DNS Targets).
|
||||
- Verify/Delete: styled modal + inline results (verified/expected vs actual).
|
||||
- “Landing Ready/Missing”: wenn missing, direkt CTA „Regenerate (Tycoon)“ mit Erklärung.
|
||||
|
||||
### `/terminal/settings`
|
||||
Stärken:
|
||||
- Gute Tabs (Profile/Alerts/Plans/Security), Plan Cards ordentlich.
|
||||
- Referral/Invite ist sauber umgesetzt.
|
||||
|
||||
Probleme:
|
||||
- Notification prefs werden nur in `localStorage` gespeichert → nicht cross-device, nicht auditierbar. (UX: „Saved“ wirkt, aber auf anderem Device weg.)
|
||||
- “Email Verified” wird als immer verified angezeigt (UI Copy), unabhängig vom echten `user.is_verified` → UX/Trust Risiko.
|
||||
- “Delete Account” Button ohne Flow (gefährlich).
|
||||
|
||||
Empfehlungen:
|
||||
- Notification prefs in Backend persistieren (Endpoint z.B. `PUT /auth/notification-prefs` oder `PUT /users/me/preferences`).
|
||||
- Security Tab: echten Status aus `user.is_verified` anzeigen + „Resend verification“ CTA falls false.
|
||||
- Delete Account: confirm flow + export data + cooldown.
|
||||
|
||||
---
|
||||
|
||||
## Konsistenz-/Trust-Issues (wichtig für Conversion)
|
||||
|
||||
- **Plan Limits widersprüchlich**:
|
||||
Beispiel: Sniper Limits in `/terminal/sniper` (scout: 0) vs Settings Plan Copy (Scout: “2 Sniper Alerts”). Das ist ein direkter Trust-Bruch.
|
||||
|
||||
- **Terminologie**: Radar vs Hunt; Terminal v1.0 in Drawer; “Go to Radar” aber Route `/terminal/hunt`.
|
||||
Empfehlung: eine Terminologie festlegen (z.B. „Hunt“ überall) und konsequent.
|
||||
|
||||
---
|
||||
|
||||
## Messbare UX-KPIs (damit Improvements nachvollziehbar sind)
|
||||
|
||||
- **Time-to-first-useful-data** pro Seite (Market, Intel, Watchlist, Portfolio CFO)
|
||||
- **Error rate** (API errors) pro Seite + Retry success rate
|
||||
- **Completion rates**:
|
||||
- Listing Wizard Step 1→2→3
|
||||
- Yield Activation Step 1→2→3
|
||||
- DNS verification success time distribution
|
||||
- **Engagement**:
|
||||
- Track/untrack from Market
|
||||
- Analyze opens per session
|
||||
- Inbox reply median time
|
||||
|
||||
---
|
||||
|
||||
## Konkrete nächste Schritte (Engineering Backlog Vorschlag)
|
||||
|
||||
### Phase 1 (P0): Shell + States
|
||||
- Terminal Shell Layout bauen, alle Terminal-Seiten migrieren.
|
||||
- `terminal/loading.tsx` + `terminal/error.tsx` einführen.
|
||||
- Einheitliches Toast/Confirm/Modal Pattern.
|
||||
|
||||
### Phase 2 (P1): Data UX
|
||||
- Market: echte Error UI + URL-state für Filter + klare Trennung API vs client filtering.
|
||||
- Intel: server-driven pagination/search.
|
||||
- Settings: Preferences persistieren + Security Status korrekt.
|
||||
|
||||
### Phase 3 (P2): Polish & Power-User
|
||||
- Keyboard-first: Cmd+K Search mit echten Results + navigation.
|
||||
- Accessibility sweep: Kontrast, Focus states, ARIA, reduced motion.
|
||||
|
||||
|
||||
@ -8,18 +8,23 @@ API endpoints for accessing freshly dropped domains from:
|
||||
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
import logging
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, BackgroundTasks
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select, update
|
||||
|
||||
from app.database import get_db
|
||||
from app.api.deps import get_current_user
|
||||
from app.models.zone_file import DroppedDomain
|
||||
from app.services.zone_file import (
|
||||
ZoneFileService,
|
||||
get_dropped_domains,
|
||||
get_zone_stats,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
router = APIRouter(prefix="/drops", tags=["drops"])
|
||||
|
||||
# All supported TLDs
|
||||
@ -175,3 +180,154 @@ async def api_get_supported_tlds():
|
||||
{"tld": "biz", "name": "Business", "flag": "💼", "registry": "GoDaddy", "source": "czds"},
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@router.post("/check-status/{drop_id}")
|
||||
async def api_check_drop_status(
|
||||
drop_id: int,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Check the real-time availability status of a dropped domain.
|
||||
|
||||
Returns:
|
||||
- available: Domain can be registered NOW
|
||||
- pending_delete: Domain is in deletion phase (monitor it!)
|
||||
- redemption: Domain is in redemption period (owner can recover)
|
||||
- taken: Domain was re-registered
|
||||
- unknown: Could not determine status
|
||||
"""
|
||||
from app.services.domain_checker import domain_checker
|
||||
|
||||
# Get the drop from DB
|
||||
result = await db.execute(
|
||||
select(DroppedDomain).where(DroppedDomain.id == drop_id)
|
||||
)
|
||||
drop = result.scalar_one_or_none()
|
||||
|
||||
if not drop:
|
||||
raise HTTPException(status_code=404, detail="Drop not found")
|
||||
|
||||
full_domain = f"{drop.domain}.{drop.tld}"
|
||||
|
||||
try:
|
||||
# Check with RDAP (not quick mode)
|
||||
check_result = await domain_checker.check_domain(full_domain, quick=False)
|
||||
|
||||
# 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
|
||||
await db.execute(
|
||||
update(DroppedDomain)
|
||||
.where(DroppedDomain.id == drop_id)
|
||||
.values(
|
||||
availability_status=availability_status,
|
||||
rdap_status=str(rdap_status) if rdap_status else None,
|
||||
last_status_check=datetime.utcnow()
|
||||
)
|
||||
)
|
||||
await db.commit()
|
||||
|
||||
return {
|
||||
"id": drop_id,
|
||||
"domain": full_domain,
|
||||
"availability_status": availability_status,
|
||||
"rdap_status": rdap_status,
|
||||
"is_available": check_result.is_available,
|
||||
"can_register_now": availability_status == "available",
|
||||
"can_monitor": availability_status in ("pending_delete", "redemption"),
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Status check failed for {full_domain}: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.post("/monitor/{drop_id}")
|
||||
async def api_monitor_drop(
|
||||
drop_id: int,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Add a dropped domain to the Sniper monitoring list.
|
||||
Will send notification when domain becomes available.
|
||||
"""
|
||||
from app.models.sniper_alert import SniperAlert
|
||||
|
||||
# Get the drop
|
||||
result = await db.execute(
|
||||
select(DroppedDomain).where(DroppedDomain.id == drop_id)
|
||||
)
|
||||
drop = result.scalar_one_or_none()
|
||||
|
||||
if not drop:
|
||||
raise HTTPException(status_code=404, detail="Drop not found")
|
||||
|
||||
full_domain = f"{drop.domain}.{drop.tld}"
|
||||
|
||||
# Check if already monitoring
|
||||
existing = await db.execute(
|
||||
select(SniperAlert).where(
|
||||
SniperAlert.user_id == current_user.id,
|
||||
SniperAlert.domain == full_domain,
|
||||
SniperAlert.is_active == True
|
||||
)
|
||||
)
|
||||
if existing.scalar_one_or_none():
|
||||
return {"status": "already_monitoring", "domain": full_domain}
|
||||
|
||||
# Check user limits
|
||||
from app.api.sniper_alerts import get_user_alert_limit
|
||||
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,
|
||||
domain=full_domain,
|
||||
alert_type="drop",
|
||||
is_active=True,
|
||||
notify_email=True,
|
||||
notify_push=True,
|
||||
)
|
||||
db.add(alert)
|
||||
await db.commit()
|
||||
|
||||
return {
|
||||
"status": "monitoring",
|
||||
"domain": full_domain,
|
||||
"message": f"You'll be notified when {full_domain} becomes available!"
|
||||
}
|
||||
|
||||
@ -37,7 +37,14 @@ class DroppedDomain(Base):
|
||||
has_hyphen = Column(Boolean, default=False)
|
||||
created_at = Column(DateTime, default=datetime.utcnow)
|
||||
|
||||
# Real-time availability status (checked via RDAP)
|
||||
# Possible values: 'available', 'pending_delete', 'redemption', 'taken', 'unknown'
|
||||
availability_status = Column(String(20), default='unknown', index=True)
|
||||
rdap_status = Column(String(255), nullable=True) # Raw RDAP status string
|
||||
last_status_check = Column(DateTime, nullable=True)
|
||||
|
||||
__table_args__ = (
|
||||
Index('ix_dropped_domains_tld_date', 'tld', 'dropped_date'),
|
||||
Index('ix_dropped_domains_length', 'length'),
|
||||
Index('ix_dropped_domains_availability', 'availability_status'),
|
||||
)
|
||||
|
||||
@ -176,9 +176,36 @@ class DomainChecker:
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
# Domain exists = taken
|
||||
# Domain exists in registry - but check status for pending delete
|
||||
data = response.json()
|
||||
|
||||
# Check if domain is pending deletion (dropped but not yet purged)
|
||||
# These domains are effectively available for registration
|
||||
domain_status = data.get('status', [])
|
||||
pending_delete_statuses = [
|
||||
'pending delete',
|
||||
'pendingdelete',
|
||||
'redemption period',
|
||||
'redemptionperiod',
|
||||
'pending purge',
|
||||
'pendingpurge',
|
||||
]
|
||||
|
||||
is_pending_delete = any(
|
||||
any(pds in str(s).lower() for pds in pending_delete_statuses)
|
||||
for s in domain_status
|
||||
)
|
||||
|
||||
if is_pending_delete:
|
||||
logger.info(f"{domain} is pending delete (status: {domain_status})")
|
||||
return DomainCheckResult(
|
||||
domain=domain,
|
||||
status=DomainStatus.AVAILABLE,
|
||||
is_available=True,
|
||||
check_method="rdap_custom",
|
||||
raw_data={"rdap_status": domain_status, "note": "pending_delete"},
|
||||
)
|
||||
|
||||
# Extract dates from events
|
||||
expiration_date = None
|
||||
creation_date = None
|
||||
|
||||
@ -365,12 +365,15 @@ async def get_dropped_domains(
|
||||
"total": total,
|
||||
"items": [
|
||||
{
|
||||
"id": item.id,
|
||||
"domain": item.domain,
|
||||
"tld": item.tld,
|
||||
"dropped_date": item.dropped_date.isoformat(),
|
||||
"length": item.length,
|
||||
"is_numeric": item.is_numeric,
|
||||
"has_hyphen": item.has_hyphen
|
||||
"has_hyphen": item.has_hyphen,
|
||||
"availability_status": getattr(item, 'availability_status', 'unknown') or 'unknown',
|
||||
"last_status_check": item.last_status_check.isoformat() if getattr(item, 'last_status_check', None) else None,
|
||||
}
|
||||
for item in items
|
||||
]
|
||||
|
||||
@ -31,13 +31,18 @@ import clsx from 'clsx'
|
||||
// TYPES
|
||||
// ============================================================================
|
||||
|
||||
type AvailabilityStatus = 'available' | 'pending_delete' | 'redemption' | '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
|
||||
}
|
||||
|
||||
interface ZoneStats {
|
||||
@ -99,6 +104,10 @@ export function DropsTab({ showToast }: DropsTabProps) {
|
||||
// Tracking
|
||||
const [tracking, setTracking] = useState<string | null>(null)
|
||||
|
||||
// Status Checking
|
||||
const [checkingStatus, setCheckingStatus] = useState<number | null>(null)
|
||||
const [monitoringDrop, setMonitoringDrop] = useState<number | null>(null)
|
||||
|
||||
// Load Stats
|
||||
const loadStats = useCallback(async () => {
|
||||
try {
|
||||
@ -173,6 +182,51 @@ export function DropsTab({ showToast }: DropsTabProps) {
|
||||
}
|
||||
}, [addDomain, showToast, tracking])
|
||||
|
||||
// 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.availability_status, last_status_check: new Date().toISOString() }
|
||||
: item
|
||||
))
|
||||
|
||||
if (result.can_register_now) {
|
||||
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) {
|
||||
showToast(e instanceof Error ? e.message : 'Status check failed', 'error')
|
||||
} finally {
|
||||
setCheckingStatus(null)
|
||||
}
|
||||
}, [checkingStatus, showToast])
|
||||
|
||||
// Monitor a pending drop
|
||||
const monitorDrop = useCallback(async (dropId: number, domain: string) => {
|
||||
if (monitoringDrop) return
|
||||
setMonitoringDrop(dropId)
|
||||
try {
|
||||
const result = await api.monitorDrop(dropId)
|
||||
if (result.status === 'already_monitoring') {
|
||||
showToast(`Already monitoring ${domain}`, 'info')
|
||||
} else {
|
||||
showToast(`🎯 Monitoring ${domain} - you'll be notified!`, 'success')
|
||||
}
|
||||
} catch (e) {
|
||||
showToast(e instanceof Error ? e.message : 'Failed to monitor', 'error')
|
||||
} finally {
|
||||
setMonitoringDrop(null)
|
||||
}
|
||||
}, [monitoringDrop, showToast])
|
||||
|
||||
// Sorted Items
|
||||
const sortedItems = useMemo(() => {
|
||||
const mult = sortDirection === 'asc' ? 1 : -1
|
||||
@ -426,7 +480,7 @@ export function DropsTab({ showToast }: DropsTabProps) {
|
||||
<>
|
||||
<div className="border border-white/[0.08] bg-[#020202] overflow-hidden">
|
||||
{/* Table Header */}
|
||||
<div className="hidden lg:grid grid-cols-[1fr_100px_120px_180px] gap-6 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_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]">
|
||||
<button
|
||||
onClick={() => handleSort('domain')}
|
||||
className="flex items-center gap-2 hover:text-white transition-colors text-left"
|
||||
@ -441,6 +495,7 @@ export function DropsTab({ showToast }: DropsTabProps) {
|
||||
<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"
|
||||
@ -456,9 +511,23 @@ export function DropsTab({ showToast }: DropsTabProps) {
|
||||
{sortedItems.map((item) => {
|
||||
const fullDomain = `${item.domain}.${item.tld}`
|
||||
const isTracking = tracking === fullDomain
|
||||
const isChecking = checkingStatus === item.id
|
||||
const isMonitoring = monitoringDrop === item.id
|
||||
const status = item.availability_status || 'unknown'
|
||||
|
||||
// Status display config
|
||||
const statusConfig = {
|
||||
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 },
|
||||
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 },
|
||||
unknown: { label: 'Check', color: 'text-white/40', bg: 'bg-white/5', border: 'border-white/10', icon: Search },
|
||||
}[status]
|
||||
|
||||
const StatusIcon = statusConfig.icon
|
||||
|
||||
return (
|
||||
<div key={fullDomain} className="group hover:bg-white/[0.02] transition-all">
|
||||
<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">
|
||||
@ -478,42 +547,71 @@ export function DropsTab({ showToast }: DropsTabProps) {
|
||||
)}>
|
||||
{item.length} chars
|
||||
</span>
|
||||
<span className="text-[10px] font-mono text-white/30 uppercase flex items-center gap-1.5">
|
||||
<Clock className="w-3 h-3" />
|
||||
{formatTime(item.dropped_date)}
|
||||
</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",
|
||||
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">
|
||||
<button
|
||||
onClick={() => track(fullDomain)}
|
||||
disabled={isTracking}
|
||||
className="flex-1 h-12 text-xs font-bold uppercase tracking-widest border border-white/10 text-white/50 flex items-center justify-center gap-2 hover:bg-white/5 active:scale-[0.98] transition-all"
|
||||
>
|
||||
{isTracking ? <Loader2 className="w-4 h-4 animate-spin" /> : <Eye className="w-4 h-4" />}
|
||||
Track
|
||||
</button>
|
||||
{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 === 'pending_delete' || status === 'redemption' ? (
|
||||
<button
|
||||
onClick={() => monitorDrop(item.id, fullDomain)}
|
||||
disabled={isMonitoring}
|
||||
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" />}
|
||||
Monitor
|
||||
</button>
|
||||
) : status === 'taken' ? (
|
||||
<button
|
||||
onClick={() => checkStatus(item.id, fullDomain)}
|
||||
disabled={isChecking}
|
||||
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"
|
||||
>
|
||||
{isChecking ? <Loader2 className="w-4 h-4 animate-spin" /> : <RefreshCw className="w-4 h-4" />}
|
||||
Recheck
|
||||
</button>
|
||||
) : (
|
||||
<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)}
|
||||
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>
|
||||
<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"
|
||||
>
|
||||
Check & Buy
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Desktop Row */}
|
||||
<div className="hidden lg:grid grid-cols-[1fr_100px_120px_180px] gap-6 items-center px-6 py-4">
|
||||
<div className="hidden lg:grid grid-cols-[1fr_80px_110px_100px_200px] gap-4 items-center px-6 py-3">
|
||||
{/* Domain */}
|
||||
<div className="min-w-0">
|
||||
<button
|
||||
@ -536,6 +634,22 @@ export function DropsTab({ showToast }: DropsTabProps) {
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Status */}
|
||||
<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 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" />}
|
||||
{statusConfig.label}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Time */}
|
||||
<div className="text-center">
|
||||
<span className="text-xs font-mono text-white/40 uppercase tracking-wider">
|
||||
@ -544,31 +658,61 @@ export function DropsTab({ showToast }: DropsTabProps) {
|
||||
</div>
|
||||
|
||||
{/* Actions */}
|
||||
<div className="flex items-center justify-end gap-2 opacity-40 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-10 h-10 flex items-center justify-center border border-white/10 text-white/50 hover:text-white hover:bg-white/5 transition-all"
|
||||
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-4 h-4 animate-spin" /> : <Eye className="w-4 h-4" />}
|
||||
{isTracking ? <Loader2 className="w-3.5 h-3.5 animate-spin" /> : <Eye className="w-3.5 h-3.5" />}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => openAnalyze(fullDomain)}
|
||||
className="w-10 h-10 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"
|
||||
title="Analyze Domain"
|
||||
>
|
||||
<Shield className="w-4 h-4" />
|
||||
<Shield className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
<a
|
||||
href={`https://www.namecheap.com/domains/registration/results/?domain=${fullDomain}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="h-10 px-5 bg-accent text-black text-[10px] font-black uppercase tracking-widest flex items-center gap-2 hover:bg-white transition-all"
|
||||
>
|
||||
Check & Buy
|
||||
<ExternalLink className="w-3.5 h-3.5" />
|
||||
</a>
|
||||
|
||||
{/* 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 Now
|
||||
</a>
|
||||
) : status === 'pending_delete' || status === 'redemption' ? (
|
||||
<button
|
||||
onClick={() => monitorDrop(item.id, fullDomain)}
|
||||
disabled={isMonitoring}
|
||||
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!"
|
||||
>
|
||||
{isMonitoring ? <Loader2 className="w-3 h-3 animate-spin" /> : <Eye className="w-3 h-3" />}
|
||||
Monitor
|
||||
</button>
|
||||
) : 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">
|
||||
<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>
|
||||
|
||||
@ -2043,12 +2043,15 @@ class AdminApiClient extends ApiClient {
|
||||
return this.request<{
|
||||
total: number
|
||||
items: Array<{
|
||||
id: number
|
||||
domain: string
|
||||
tld: string
|
||||
dropped_date: string
|
||||
length: number
|
||||
is_numeric: boolean
|
||||
has_hyphen: boolean
|
||||
availability_status: 'available' | 'pending_delete' | 'redemption' | 'taken' | 'unknown'
|
||||
last_status_check: string | null
|
||||
}>
|
||||
}>(`/drops?${query}`)
|
||||
}
|
||||
@ -2063,6 +2066,26 @@ class AdminApiClient extends ApiClient {
|
||||
}>
|
||||
}>('/drops/tlds')
|
||||
}
|
||||
|
||||
async checkDropStatus(dropId: number) {
|
||||
return this.request<{
|
||||
id: number
|
||||
domain: string
|
||||
availability_status: 'available' | 'pending_delete' | 'redemption' | 'taken' | 'unknown'
|
||||
rdap_status: string[]
|
||||
is_available: boolean
|
||||
can_register_now: boolean
|
||||
can_monitor: boolean
|
||||
}>(`/drops/check-status/${dropId}`, { method: 'POST' })
|
||||
}
|
||||
|
||||
async monitorDrop(dropId: number) {
|
||||
return this.request<{
|
||||
status: string
|
||||
domain: string
|
||||
message?: string
|
||||
}>(`/drops/monitor/${dropId}`, { method: 'POST' })
|
||||
}
|
||||
}
|
||||
|
||||
// Yield Types
|
||||
|
||||
Reference in New Issue
Block a user