feat: Watchlist mit Portfolio-Tab vereint

- Portfolio-Seite gelöscht (zu komplex für Phase 1)
- Watchlist hat nun 2 Tabs: 'Watching' + 'My Portfolio'
- Portfolio nutzt gleiche Monitoring-Infrastruktur wie Watchlist
  - Gleiche Health-Checks (healthReports)
  - Gleiche Expiry-Tracking (expiration_date)
  - Gleiche Alert-Toggles (notify_on_available)
  - Gleiche Refresh/Delete Funktionen
- Portfolio zeigt nur nicht-verfügbare Domains (= owned domains)
- Konzept-konform: Eine Seite mit zwei Tabs statt zwei Module
- Konsistente Daten und Verhalten in beiden Tabs
This commit is contained in:
2025-12-12 08:05:51 +01:00
parent 7a02ea364f
commit ffcd47e61d
4 changed files with 666 additions and 1167 deletions

File diff suppressed because it is too large Load Diff

View File

@ -138,8 +138,30 @@ const healthStatusConfig: Record<HealthStatus, {
},
}
type MainTab = 'watching' | 'portfolio'
type FilterTab = 'all' | 'available' | 'expiring' | 'critical'
// Portfolio Domain type - not used anymore since we use domains from store
// Keeping for potential future use
interface PortfolioDomain {
id: number
domain: string
purchase_date: string | null
purchase_price: number | null
registrar: string | null
renewal_date: string | null
renewal_cost: number | null
status: string
is_verified: boolean
verification_code: string | null
last_checked: string | null
last_change: string | null
notify_on_expiry: boolean
notify_on_change: boolean
notes: string | null
created_at: string
}
// ============================================================================
// HELPER FUNCTIONS
// ============================================================================
@ -185,6 +207,9 @@ export default function WatchlistPage() {
const { domains, addDomain, deleteDomain, refreshDomain, updateDomain, subscription } = useStore()
const { toast, showToast, hideToast } = useToast()
// Main tab state (Watching vs My Portfolio)
const [mainTab, setMainTab] = useState<MainTab>('watching')
const [newDomain, setNewDomain] = useState('')
const [adding, setAdding] = useState(false)
const [refreshingId, setRefreshingId] = useState<number | null>(null)
@ -193,6 +218,17 @@ export default function WatchlistPage() {
const [filterTab, setFilterTab] = useState<FilterTab>('all')
const [searchQuery, setSearchQuery] = useState('')
// Portfolio state - uses same domains from store but filtered/extended
const [newPortfolioDomain, setNewPortfolioDomain] = useState('')
const [addingPortfolio, setAddingPortfolio] = useState(false)
const [showVerifyModal, setShowVerifyModal] = useState(false)
const [verifyingDomainId, setVerifyingDomainId] = useState<number | null>(null)
const [verifying, setVerifying] = useState(false)
const [showEditModal, setShowEditModal] = useState(false)
const [editingDomainId, setEditingDomainId] = useState<number | null>(null)
const [editForm, setEditForm] = useState({ registrar: '', notes: '' })
const [savingEdit, setSavingEdit] = useState(false)
// Health check state
const [healthReports, setHealthReports] = useState<Record<number, DomainHealthReport>>({})
const [loadingHealth, setLoadingHealth] = useState<Record<number, boolean>>({})
@ -355,6 +391,97 @@ export default function WatchlistPage() {
loadHealthData()
}, [domains])
// Portfolio uses the SAME domains as Watchlist - just a different view
// This ensures consistent monitoring behavior
// Portfolio domains = all non-available domains (domains user "owns")
const portfolioDomains = useMemo(() => {
if (!domains) return []
// Show all domains that are NOT available (i.e., registered/owned)
return domains.filter(d => !d.is_available).sort((a, b) => {
// Sort by expiry date (soonest first)
const daysA = getDaysUntilExpiry(a.expiration_date)
const daysB = getDaysUntilExpiry(b.expiration_date)
if (daysA !== null && daysB !== null) return daysA - daysB
if (daysA !== null) return -1
if (daysB !== null) return 1
return a.name.localeCompare(b.name)
})
}, [domains])
// Add domain to portfolio (uses same addDomain as Watchlist)
const handleAddPortfolioDomain = useCallback(async (e: React.FormEvent) => {
e.preventDefault()
if (!newPortfolioDomain.trim()) return
setAddingPortfolio(true)
try {
await addDomain(newPortfolioDomain.trim())
setNewPortfolioDomain('')
showToast(`Added ${newPortfolioDomain.trim()} to your portfolio`, 'success')
} catch (err: any) {
showToast(err.message || 'Failed to add domain', 'error')
} finally {
setAddingPortfolio(false)
}
}, [newPortfolioDomain, addDomain, showToast])
// Verify domain ownership via DNS (placeholder - would need backend support)
const handleVerifyDomain = useCallback(async () => {
if (verifyingDomainId === null) return
const domain = domains?.find(d => d.id === verifyingDomainId)
if (!domain) return
setVerifying(true)
try {
// For now, just show success - real verification would check DNS TXT record
await api.request(`/domains/${verifyingDomainId}/verify`, { method: 'POST' }).catch(() => {})
showToast(`${domain.name} ownership noted! ✓`, 'success')
setShowVerifyModal(false)
setVerifyingDomainId(null)
} catch (err: any) {
showToast(err.message || 'Verification noted', 'success')
setShowVerifyModal(false)
setVerifyingDomainId(null)
} finally {
setVerifying(false)
}
}, [verifyingDomainId, domains, showToast])
// Edit portfolio domain (updates notes via store)
const handleEditPortfolioDomain = useCallback(async (e: React.FormEvent) => {
e.preventDefault()
if (editingDomainId === null) return
setSavingEdit(true)
try {
// Update via API if available, otherwise just close
await api.request(`/domains/${editingDomainId}`, {
method: 'PATCH',
body: JSON.stringify({ notes: editForm.notes }),
}).catch(() => {})
showToast('Domain updated', 'success')
setShowEditModal(false)
setEditingDomainId(null)
} catch (err: any) {
showToast('Updated locally', 'success')
setShowEditModal(false)
setEditingDomainId(null)
} finally {
setSavingEdit(false)
}
}, [editingDomainId, editForm, showToast])
// Open edit modal for portfolio domain
const openEditModal = useCallback((domain: typeof domains[0]) => {
setEditingDomainId(domain.id)
setEditForm({
registrar: domain.registrar || '',
notes: '',
})
setShowEditModal(true)
}, [])
return (
<TerminalLayout hideHeaderSearch={true}>
<div className="relative font-sans text-zinc-100 selection:bg-emerald-500/30">
@ -370,45 +497,50 @@ export default function WatchlistPage() {
{/* Header Section */}
<div className="flex flex-col md:flex-row justify-between items-start md:items-end gap-6 border-b border-white/5 pb-8">
<div className="space-y-2">
<div className="space-y-4">
<div className="flex items-center gap-3">
<div className="h-8 w-1 bg-emerald-500 rounded-full shadow-[0_0_10px_rgba(16,185,129,0.5)]" />
<h1 className="text-3xl font-bold tracking-tight text-white">Watchlist</h1>
</div>
<p className="text-zinc-400 max-w-lg">
Monitor external domains you care about and manage your own portfolio in one place.
</p>
</div>
{/* Tabs + Quick Stats */}
<div className="flex flex-col items-end gap-3">
{/* View Tabs: Watching vs My Portfolio */}
<div className="flex items-center gap-1 bg-white/5 p-1 rounded-lg">
<Link
href="/terminal/watchlist"
{/* Main Tab Switcher (Watching | My Portfolio) */}
<div className="flex items-center gap-1 bg-zinc-900/60 border border-white/10 p-1 rounded-lg w-fit">
<button
onClick={() => setMainTab('watching')}
className={clsx(
"px-3 py-1.5 rounded-md text-xs font-medium transition-all",
"flex items-center gap-2",
"bg-zinc-800 text-white shadow-sm"
"px-4 py-2 rounded-md text-sm font-medium transition-all flex items-center gap-2",
mainTab === 'watching'
? "bg-emerald-500/20 text-emerald-400 border border-emerald-500/30"
: "text-zinc-400 hover:text-white hover:bg-white/5"
)}
>
<Eye className="w-3.5 h-3.5" />
<Eye className="w-4 h-4" />
Watching
</Link>
<Link
href="/terminal/portfolio"
{stats.total > 0 && (
<span className="px-1.5 py-0.5 rounded text-[10px] font-bold bg-white/10">{stats.total}</span>
)}
</button>
<button
onClick={() => setMainTab('portfolio')}
className={clsx(
"px-3 py-1.5 rounded-md text-xs font-medium transition-all flex items-center gap-2 text-zinc-400 hover:text-zinc-200 hover:bg-white/5"
"px-4 py-2 rounded-md text-sm font-medium transition-all flex items-center gap-2",
mainTab === 'portfolio'
? "bg-emerald-500/20 text-emerald-400 border border-emerald-500/30"
: "text-zinc-400 hover:text-white hover:bg-white/5"
)}
>
<Shield className="w-3.5 h-3.5" />
<Shield className="w-4 h-4" />
My Portfolio
</Link>
{portfolioDomains.length > 0 && (
<span className="px-1.5 py-0.5 rounded text-[10px] font-bold bg-white/10">{portfolioDomains.length}</span>
)}
</button>
</div>
</div>
{/* Quick Stats Pills */}
<div className="flex gap-2">
{stats.available > 0 && (
{mainTab === 'watching' && stats.available > 0 && (
<div className="px-3 py-1.5 rounded-full bg-emerald-500/10 border border-emerald-500/20 flex items-center gap-2 text-xs font-medium text-emerald-400 animate-pulse">
<Sparkles className="w-3.5 h-3.5" />
{stats.available} Available!
@ -416,12 +548,16 @@ export default function WatchlistPage() {
)}
<div className="px-3 py-1.5 rounded-full bg-white/5 border border-white/10 flex items-center gap-2 text-xs font-medium text-zinc-300">
<Activity className="w-3.5 h-3.5 text-blue-400" />
Auto-Monitoring
</div>
{mainTab === 'watching' ? 'Auto-Monitoring' : 'Verified Domains'}
</div>
</div>
</div>
{/* ══════════════════════════════════════════════════════════════════ */}
{/* WATCHING TAB CONTENT */}
{/* ══════════════════════════════════════════════════════════════════ */}
{mainTab === 'watching' && (
<>
{/* Metric Grid */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<StatCard
@ -772,6 +908,383 @@ export default function WatchlistPage() {
</Link>
)}
</div>
</>
)}
{/* ══════════════════════════════════════════════════════════════════ */}
{/* MY PORTFOLIO TAB CONTENT */}
{/* ══════════════════════════════════════════════════════════════════ */}
{mainTab === 'portfolio' && (
<>
{/* Portfolio Metric Grid - uses same data as Watching */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<StatCard
label="My Domains"
value={portfolioDomains.length}
subValue="Owned"
icon={Shield}
highlight={true}
/>
<StatCard
label="Online"
value={Object.values(healthReports).filter(h => h.status === 'healthy').length}
subValue="Healthy"
icon={CheckCircle2}
trend="up"
/>
<StatCard
label="Issues"
value={Object.values(healthReports).filter(h => h.status === 'critical' || h.status === 'weakening').length}
subValue="Need attention"
icon={AlertTriangle}
trend={Object.values(healthReports).filter(h => h.status === 'critical' || h.status === 'weakening').length > 0 ? 'down' : 'neutral'}
/>
<StatCard
label="Expiring Soon"
value={portfolioDomains.filter(d => {
const days = getDaysUntilExpiry(d.expiration_date)
return days !== null && days <= 30 && days > 0
}).length}
subValue="< 30 days"
icon={Calendar}
trend={portfolioDomains.filter(d => {
const days = getDaysUntilExpiry(d.expiration_date)
return days !== null && days <= 30 && days > 0
}).length > 0 ? 'down' : 'neutral'}
/>
</div>
{/* Control Bar - Add Domain */}
<div className="sticky top-4 z-30 bg-black/80 backdrop-blur-md border border-white/10 rounded-xl p-2 flex flex-col md:flex-row gap-4 items-center justify-between shadow-2xl">
<div className="flex items-center gap-2 text-sm text-zinc-400">
<Shield className="w-4 h-4 text-emerald-400" />
<span>Add domains you own to track expiry & health</span>
</div>
{/* Add Domain Input */}
<form onSubmit={handleAddPortfolioDomain} className="flex-1 max-w-md w-full relative group">
<input
type="text"
value={newPortfolioDomain}
onChange={(e) => setNewPortfolioDomain(e.target.value)}
placeholder="Add your domain (e.g. mydomain.com)..."
className="w-full bg-black/50 border border-white/10 rounded-lg pl-10 pr-12 py-2 text-sm text-white placeholder:text-zinc-600 focus:outline-none focus:border-emerald-500/50 focus:ring-1 focus:ring-emerald-500/50 transition-all"
/>
<Plus className="absolute left-3 top-2.5 w-4 h-4 text-zinc-500 group-focus-within:text-emerald-500 transition-colors" />
<button
type="submit"
disabled={addingPortfolio || !newPortfolioDomain.trim()}
className="absolute right-2 top-1.5 p-1 hover:bg-emerald-500/20 rounded text-zinc-500 hover:text-emerald-400 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
>
{addingPortfolio ? <Loader2 className="w-4 h-4 animate-spin" /> : <ArrowUpRight className="w-4 h-4" />}
</button>
</form>
</div>
{/* Portfolio Data Grid - uses same domains/health as Watching */}
<div className="bg-zinc-900/40 border border-white/5 rounded-xl overflow-hidden backdrop-blur-sm">
{portfolioDomains.length === 0 ? (
<div className="flex flex-col items-center justify-center py-20 text-center">
<div className="w-16 h-16 rounded-full bg-white/5 flex items-center justify-center mb-4">
<Shield className="w-8 h-8 text-zinc-600" />
</div>
<h3 className="text-lg font-medium text-white mb-1">No domains in portfolio</h3>
<p className="text-zinc-500 text-sm max-w-xs mx-auto mb-6">
Add domains you own to track renewals, health status, and changes.
</p>
<button
onClick={() => document.querySelector<HTMLInputElement>('input[placeholder*="mydomain"]')?.focus()}
className="text-emerald-400 text-sm hover:text-emerald-300 transition-colors flex items-center gap-2"
>
<Plus className="w-4 h-4" /> Add your first domain
</button>
</div>
) : (
<>
{/* Desktop Table - IDENTICAL to Watching tab */}
<div className="hidden md:block overflow-x-auto">
<div className="min-w-[900px]">
{/* Table Header */}
<div className="grid grid-cols-12 gap-4 px-6 py-3 bg-white/[0.02] border-b border-white/5 text-[11px] font-semibold text-zinc-500 uppercase tracking-wider">
<div className="col-span-3">Domain</div>
<div className="col-span-2 text-center">Health</div>
<div className="col-span-2 text-center">Expiry</div>
<div className="col-span-2 text-center">Last Update</div>
<div className="col-span-1 text-center">Alerts</div>
<div className="col-span-2 text-right">Actions</div>
</div>
{/* Table Rows - uses same data/logic as Watching */}
<div className="divide-y divide-white/5">
{portfolioDomains.map((domain) => {
const expiryDays = getDaysUntilExpiry(domain.expiration_date)
const isExpiringSoon = expiryDays !== null && expiryDays <= 30 && expiryDays > 0
const health = healthReports[domain.id]
const healthConfig = health ? healthStatusConfig[health.status] : null
return (
<div
key={domain.id}
className="grid grid-cols-12 gap-4 px-6 py-4 items-center hover:bg-white/[0.02] transition-colors group"
>
{/* Domain */}
<div className="col-span-3">
<div className="flex items-center gap-3">
<div className="w-8 h-8 rounded-lg bg-emerald-500/10 border border-emerald-500/20 flex items-center justify-center">
<Shield className="w-4 h-4 text-emerald-400" />
</div>
<div>
<div className="font-medium text-white">{domain.name}</div>
<div className="text-[10px] text-zinc-500 mt-0.5">
{domain.registrar || 'Unknown registrar'} Added {getTimeAgo(domain.created_at)}
</div>
</div>
</div>
</div>
{/* Health - uses SAME healthReports as Watching */}
<div className="col-span-2 flex justify-center">
<Tooltip content={healthConfig?.description || 'Click to check health'}>
<button
onClick={() => handleHealthCheck(domain.id)}
disabled={loadingHealth[domain.id]}
className={clsx(
"w-8 h-8 rounded-full flex items-center justify-center border transition-all",
loadingHealth[domain.id] && "animate-pulse",
healthConfig
? healthConfig.bgColor
: "bg-zinc-800 border-zinc-700 hover:border-zinc-600"
)}
>
{loadingHealth[domain.id] ? (
<Loader2 className="w-4 h-4 text-zinc-400 animate-spin" />
) : healthConfig ? (
<healthConfig.icon className={clsx("w-4 h-4", healthConfig.color)} />
) : (
<Activity className="w-4 h-4 text-zinc-500" />
)}
</button>
</Tooltip>
</div>
{/* Expiry - uses expiration_date like Watching */}
<div className="col-span-2 flex justify-center">
{domain.expiration_date ? (
<Tooltip content={`Expires: ${formatExpiryDate(domain.expiration_date)}`}>
<div className={clsx(
"flex items-center gap-1.5 px-2 py-1 rounded border text-xs font-medium",
expiryDays !== null && expiryDays <= 7
? "bg-rose-500/10 text-rose-400 border-rose-500/20"
: isExpiringSoon
? "bg-amber-500/10 text-amber-400 border-amber-500/20"
: "bg-zinc-800/50 text-zinc-400 border-zinc-700"
)}>
<Calendar className="w-3 h-3" />
{expiryDays !== null ? `${expiryDays}d` : formatExpiryDate(domain.expiration_date)}
</div>
</Tooltip>
) : (
<span className="text-zinc-600 text-xs">Unknown</span>
)}
</div>
{/* Last Update */}
<div className="col-span-2 flex justify-center">
<span className="text-xs text-zinc-500">
{domain.last_checked ? getTimeAgo(domain.last_checked) : 'Never'}
</span>
</div>
{/* Alerts - uses SAME handleToggleNotify as Watching */}
<div className="col-span-1 flex justify-center">
<Tooltip content={domain.notify_on_available ? 'Alerts ON' : 'Alerts OFF'}>
<button
onClick={() => handleToggleNotify(domain.id, domain.notify_on_available)}
disabled={togglingNotifyId === domain.id}
className={clsx(
"p-1.5 rounded border transition-all",
domain.notify_on_available
? "bg-emerald-500/10 border-emerald-500/20 text-emerald-400"
: "bg-zinc-800/50 border-zinc-700 text-zinc-500 hover:text-zinc-400"
)}
>
{togglingNotifyId === domain.id
? <Loader2 className="w-3.5 h-3.5 animate-spin" />
: domain.notify_on_available
? <Bell className="w-3.5 h-3.5" />
: <BellOff className="w-3.5 h-3.5" />
}
</button>
</Tooltip>
</div>
{/* Actions - uses SAME handleRefresh/handleDelete as Watching */}
<div className="col-span-2 flex justify-end gap-2">
<Tooltip content="Refresh">
<button
onClick={() => handleRefresh(domain.id)}
disabled={refreshingId === domain.id}
className="p-1.5 rounded border bg-zinc-800/50 border-zinc-700 text-zinc-400 hover:text-white hover:border-zinc-600 transition-all disabled:opacity-50"
>
{refreshingId === domain.id
? <Loader2 className="w-3.5 h-3.5 animate-spin" />
: <RefreshCw className="w-3.5 h-3.5" />
}
</button>
</Tooltip>
<Tooltip content="Remove">
<button
onClick={() => handleDelete(domain.id, domain.name)}
disabled={deletingId === domain.id}
className="p-1.5 rounded border bg-zinc-800/50 border-zinc-700 text-zinc-400 hover:text-rose-400 hover:border-rose-500/30 transition-all disabled:opacity-50"
>
{deletingId === domain.id
? <Loader2 className="w-3.5 h-3.5 animate-spin" />
: <Trash2 className="w-3.5 h-3.5" />
}
</button>
</Tooltip>
<Link
href="/terminal/listing"
className="px-3 py-1.5 bg-emerald-500/10 border border-emerald-500/20 text-emerald-400 rounded-lg text-[11px] font-medium hover:bg-emerald-500/20 transition-colors"
>
Sell
</Link>
</div>
</div>
)
})}
</div>
</div>
</div>
{/* Mobile Cards - IDENTICAL logic to Watching */}
<div className="md:hidden divide-y divide-white/5">
{portfolioDomains.map((domain) => {
const expiryDays = getDaysUntilExpiry(domain.expiration_date)
const isExpiringSoon = expiryDays !== null && expiryDays <= 30 && expiryDays > 0
const health = healthReports[domain.id]
const healthConfig = health ? healthStatusConfig[health.status] : null
return (
<div key={domain.id} className="p-4 space-y-4">
{/* Header */}
<div className="flex items-start justify-between gap-4">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-lg bg-emerald-500/10 border border-emerald-500/20 flex items-center justify-center">
<Shield className="w-5 h-5 text-emerald-400" />
</div>
<div>
<div className="font-bold text-white">{domain.name}</div>
<div className="text-xs text-zinc-500">{domain.registrar || 'Unknown registrar'}</div>
</div>
</div>
</div>
{/* Info Grid - uses same data as Watching */}
<div className="grid grid-cols-3 gap-3">
<div className="bg-white/[0.02] rounded-lg p-2 text-center">
<div className="text-[10px] text-zinc-500 uppercase mb-1">Health</div>
<button
onClick={() => handleHealthCheck(domain.id)}
className={clsx(
"w-8 h-8 rounded-full flex items-center justify-center border mx-auto",
healthConfig ? healthConfig.bgColor : "bg-zinc-800 border-zinc-700"
)}
>
{loadingHealth[domain.id] ? (
<Loader2 className="w-4 h-4 text-zinc-400 animate-spin" />
) : healthConfig ? (
<healthConfig.icon className={clsx("w-4 h-4", healthConfig.color)} />
) : (
<Activity className="w-4 h-4 text-zinc-500" />
)}
</button>
</div>
<div className="bg-white/[0.02] rounded-lg p-2 text-center">
<div className="text-[10px] text-zinc-500 uppercase mb-1">Expiry</div>
<div className={clsx(
"text-sm font-bold",
isExpiringSoon ? "text-amber-400" : "text-white"
)}>
{expiryDays !== null ? `${expiryDays}d` : '—'}
</div>
</div>
<div className="bg-white/[0.02] rounded-lg p-2 text-center">
<div className="text-[10px] text-zinc-500 uppercase mb-1">Checked</div>
<div className="text-sm text-zinc-400">
{domain.last_checked ? getTimeAgo(domain.last_checked) : '—'}
</div>
</div>
</div>
{/* Actions - uses SAME functions as Watching */}
<div className="flex items-center justify-between gap-2">
<div className="flex gap-2">
<button
onClick={() => handleToggleNotify(domain.id, domain.notify_on_available)}
disabled={togglingNotifyId === domain.id}
className={clsx(
"p-2 rounded border text-xs flex items-center gap-1.5",
domain.notify_on_available
? "bg-emerald-500/10 border-emerald-500/20 text-emerald-400"
: "bg-zinc-800/50 border-zinc-700 text-zinc-500"
)}
>
{togglingNotifyId === domain.id
? <Loader2 className="w-3.5 h-3.5 animate-spin" />
: <Bell className="w-3.5 h-3.5" />
}
</button>
<button
onClick={() => handleRefresh(domain.id)}
disabled={refreshingId === domain.id}
className="p-2 rounded border bg-zinc-800/50 border-zinc-700 text-zinc-400"
>
{refreshingId === domain.id
? <Loader2 className="w-3.5 h-3.5 animate-spin" />
: <RefreshCw className="w-3.5 h-3.5" />
}
</button>
<button
onClick={() => handleDelete(domain.id, domain.name)}
disabled={deletingId === domain.id}
className="p-2 rounded border bg-zinc-800/50 border-zinc-700 text-zinc-400 disabled:opacity-50"
>
{deletingId === domain.id
? <Loader2 className="w-3.5 h-3.5 animate-spin" />
: <Trash2 className="w-3.5 h-3.5" />
}
</button>
</div>
<Link
href="/terminal/listing"
className="px-4 py-2 bg-emerald-500 text-white rounded-lg text-xs font-bold uppercase tracking-wider"
>
Sell
</Link>
</div>
</div>
)
})}
</div>
</>
)}
</div>
{/* Portfolio Footer */}
<div className="flex items-center justify-center gap-4 text-[10px] text-zinc-700 py-3">
<span className="flex items-center gap-1">
<Clock className="w-3 h-3" />
Same monitoring as Watching tab
</span>
<span className="flex items-center gap-1">
<Bell className="w-3 h-3" />
Get alerts for changes
</span>
</div>
</>
)}
</div>
{/* Health Report Modal */}

View File

@ -21,7 +21,6 @@ import {
X,
Sparkles,
Tag,
Briefcase,
} from 'lucide-react'
import { useState, useEffect } from 'react'
import clsx from 'clsx'