From c5a9bd83d57c81da970f444715fb441d827a409c Mon Sep 17 00:00:00 2001 From: Yves Gugger Date: Sat, 20 Dec 2025 23:29:31 +0100 Subject: [PATCH] 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 --- UX_TERMINAL_UX_REPORT.md | 2 + backend/app/api/drops.py | 63 ++++++++---- .../src/components/analyze/AnalyzePanel.tsx | 58 ++++++++++- frontend/src/components/hunt/DropsTab.tsx | 97 ++++++++++++++----- frontend/src/lib/analyze-store.ts | 16 ++- 5 files changed, 188 insertions(+), 48 deletions(-) diff --git a/UX_TERMINAL_UX_REPORT.md b/UX_TERMINAL_UX_REPORT.md index 34914ad..54b176c 100644 --- a/UX_TERMINAL_UX_REPORT.md +++ b/UX_TERMINAL_UX_REPORT.md @@ -324,3 +324,5 @@ Empfehlungen: + + diff --git a/backend/app/api/drops.py b/backend/app/api/drops.py index 05f1e02..2d48bdd 100644 --- a/backend/app/api/drops.py +++ b/backend/app/api/drops.py @@ -274,22 +274,49 @@ async def api_track_drop( Domain.name == full_domain ) ) - if existing.scalar_one_or_none(): - return {"status": "already_tracking", "domain": full_domain} + existing_domain = existing.scalar_one_or_none() + if existing_domain: + return { + "status": "already_tracking", + "domain": full_domain, + "message": f"{full_domain} is already in your Watchlist", + "domain_id": existing_domain.id + } - # Add to watchlist with notification enabled - domain = Domain( - user_id=current_user.id, - name=full_domain, - status=DomainStatus.AVAILABLE if drop.availability_status == 'available' else DomainStatus.UNKNOWN, - is_available=drop.availability_status == 'available', - notify_on_available=True, # Enable notification! - ) - db.add(domain) - await db.commit() - - return { - "status": "tracking", - "domain": full_domain, - "message": f"Added {full_domain} to your Watchlist. You'll be notified when available!" - } + try: + # Add to watchlist with notification enabled + domain = Domain( + user_id=current_user.id, + name=full_domain, + status=DomainStatus.AVAILABLE if drop.availability_status == 'available' else DomainStatus.UNKNOWN, + is_available=drop.availability_status == 'available', + notify_on_available=True, # Enable notification! + ) + db.add(domain) + await db.commit() + await db.refresh(domain) + + return { + "status": "tracking", + "domain": full_domain, + "message": f"Added {full_domain} to your Watchlist. You'll be notified when available!", + "domain_id": domain.id + } + except Exception as e: + await db.rollback() + # If duplicate key error, try to find existing + existing = await db.execute( + select(Domain).where( + Domain.user_id == current_user.id, + Domain.name == full_domain + ) + ) + existing_domain = existing.scalar_one_or_none() + if existing_domain: + return { + "status": "already_tracking", + "domain": full_domain, + "message": f"{full_domain} is already in your Watchlist", + "domain_id": existing_domain.id + } + raise HTTPException(status_code=500, detail=str(e)) diff --git a/frontend/src/components/analyze/AnalyzePanel.tsx b/frontend/src/components/analyze/AnalyzePanel.tsx index 54d8b53..962eab5 100644 --- a/frontend/src/components/analyze/AnalyzePanel.tsx +++ b/frontend/src/components/analyze/AnalyzePanel.tsx @@ -178,7 +178,8 @@ export function AnalyzePanel() { fastMode, setFastMode, sectionVisibility, - setSectionVisibility + setSectionVisibility, + dropStatus, } = useAnalyzePanelStore() const [loading, setLoading] = useState(false) @@ -374,6 +375,61 @@ export function AnalyzePanel() { )} + {/* Drop Status Banner */} + {dropStatus?.is_drop && ( +
+
+
+ {dropStatus.status === 'available' ? ( + + ) : dropStatus.status === 'dropping_soon' ? ( + + ) : dropStatus.status === 'taken' ? ( + + ) : ( + + )} +
+
+ {dropStatus.status === 'available' ? 'Available Now' : + dropStatus.status === 'dropping_soon' ? 'In Transition' : + dropStatus.status === 'taken' ? 'Re-registered' : + 'Status Unknown'} +
+ {dropStatus.status === 'dropping_soon' && dropStatus.deletion_date && ( +
+ Drops: {new Date(dropStatus.deletion_date).toLocaleDateString()} +
+ )} +
+
+ {dropStatus.status === 'available' && domain && ( + + + Buy Now + + )} +
+
+ )} + {/* Controls */}
- {/* Track Button - always visible */} + {/* Track Button - shows "Tracked" if already in watchlist */} {/* Action Button based on status */} @@ -682,7 +714,7 @@ export function DropsTab({ showToast }: DropsTabProps) { )} + alreadyTracked ? ( + + + Tracked + + ) : ( + + ) ) : status === 'taken' ? ( diff --git a/frontend/src/lib/analyze-store.ts b/frontend/src/lib/analyze-store.ts index 3018936..d6323e2 100644 --- a/frontend/src/lib/analyze-store.ts +++ b/frontend/src/lib/analyze-store.ts @@ -2,17 +2,25 @@ import { create } from 'zustand' export type AnalyzeSectionVisibility = Record +export type DropStatusInfo = { + status: 'available' | 'dropping_soon' | 'taken' | 'unknown' + deletion_date?: string | null + is_drop?: boolean +} + export type AnalyzePanelState = { isOpen: boolean domain: string | null fastMode: boolean filterText: string sectionVisibility: AnalyzeSectionVisibility - open: (domain: string) => void + dropStatus: DropStatusInfo | null + open: (domain: string, dropStatus?: DropStatusInfo) => void close: () => void setFastMode: (fast: boolean) => void setFilterText: (value: string) => void setSectionVisibility: (next: AnalyzeSectionVisibility) => void + setDropStatus: (status: DropStatusInfo | null) => void } const DEFAULT_VISIBILITY: AnalyzeSectionVisibility = { @@ -28,11 +36,13 @@ export const useAnalyzePanelStore = create((set) => ({ fastMode: false, filterText: '', sectionVisibility: DEFAULT_VISIBILITY, - open: (domain) => set({ isOpen: true, domain, filterText: '' }), - close: () => set({ isOpen: false }), + dropStatus: null, + open: (domain, dropStatus) => set({ isOpen: true, domain, filterText: '', dropStatus: dropStatus || null }), + close: () => set({ isOpen: false, dropStatus: null }), setFastMode: (fastMode) => set({ fastMode }), setFilterText: (filterText) => set({ filterText }), setSectionVisibility: (sectionVisibility) => set({ sectionVisibility }), + setDropStatus: (dropStatus) => set({ dropStatus }), })) export const ANALYZE_PREFS_KEY = 'pounce_analyze_prefs_v1'