fix: Tooltip behavior & Links (INTEL, MARKET, RADAR)
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
Changes: - Fixed hover/tooltip issue: Tooltips now only trigger on the specific element, not the entire row. - Added explicit 'Details' link to TLD table in INTEL view. - Added clickable TLD badges in INTEL view. - Standardized Tooltip implementation across all views. - Ensured consistent 'Award-Winning' style for all interactive elements.
This commit is contained in:
@ -31,9 +31,9 @@ import Link from 'next/link'
|
|||||||
|
|
||||||
function Tooltip({ children, content }: { children: React.ReactNode; content: string }) {
|
function Tooltip({ children, content }: { children: React.ReactNode; content: string }) {
|
||||||
return (
|
return (
|
||||||
<div className="relative flex items-center group">
|
<div className="relative flex items-center group/tooltip w-fit">
|
||||||
{children}
|
{children}
|
||||||
<div className="absolute bottom-full left-1/2 -translate-x-1/2 mb-2 px-2 py-1 bg-zinc-900 border border-zinc-800 rounded text-[10px] text-zinc-300 whitespace-nowrap opacity-0 group-hover:opacity-100 transition-opacity pointer-events-none z-50 shadow-xl">
|
<div className="absolute bottom-full left-1/2 -translate-x-1/2 mb-2 px-2 py-1 bg-zinc-900 border border-zinc-800 rounded text-[10px] text-zinc-300 whitespace-nowrap opacity-0 group-hover/tooltip:opacity-100 transition-opacity pointer-events-none z-50 shadow-xl">
|
||||||
{content}
|
{content}
|
||||||
<div className="absolute top-full left-1/2 -translate-x-1/2 -mt-px border-4 border-transparent border-t-zinc-800" />
|
<div className="absolute top-full left-1/2 -translate-x-1/2 -mt-px border-4 border-transparent border-t-zinc-800" />
|
||||||
</div>
|
</div>
|
||||||
@ -340,7 +340,7 @@ export default function IntelPage() {
|
|||||||
<div className="col-span-2 text-right"><SortableHeader label="Renewal" field="price" currentSort={sortField} currentDirection={sortDirection} onSort={handleSort} align="right" tooltip="Estimated annual renewal cost" /></div>
|
<div className="col-span-2 text-right"><SortableHeader label="Renewal" field="price" currentSort={sortField} currentDirection={sortDirection} onSort={handleSort} align="right" tooltip="Estimated annual renewal cost" /></div>
|
||||||
<div className="col-span-2 text-center"><SortableHeader label="Trend (1y)" field="change" currentSort={sortField} currentDirection={sortDirection} onSort={handleSort} align="center" /></div>
|
<div className="col-span-2 text-center"><SortableHeader label="Trend (1y)" field="change" currentSort={sortField} currentDirection={sortDirection} onSort={handleSort} align="center" /></div>
|
||||||
<div className="col-span-2 text-center"><SortableHeader label="Risk Level" field="risk" currentSort={sortField} currentDirection={sortDirection} onSort={handleSort} align="center" tooltip="Risk of price hikes or restrictions" /></div>
|
<div className="col-span-2 text-center"><SortableHeader label="Risk Level" field="risk" currentSort={sortField} currentDirection={sortDirection} onSort={handleSort} align="center" tooltip="Risk of price hikes or restrictions" /></div>
|
||||||
<div className="col-span-2 text-right"><span className="text-[10px] font-bold uppercase tracking-widest text-zinc-600 py-2 block">Provider</span></div>
|
<div className="col-span-2 text-right"><span className="text-[10px] font-bold uppercase tracking-widest text-zinc-600 py-2 block">Action</span></div>
|
||||||
</div>
|
</div>
|
||||||
<div className="divide-y divide-white/5">
|
<div className="divide-y divide-white/5">
|
||||||
{filteredData.map((tld) => {
|
{filteredData.map((tld) => {
|
||||||
@ -351,7 +351,9 @@ export default function IntelPage() {
|
|||||||
<div key={tld.tld} className="grid grid-cols-12 gap-4 px-6 py-4 items-center hover:bg-white/[0.04] transition-all group relative">
|
<div key={tld.tld} className="grid grid-cols-12 gap-4 px-6 py-4 items-center hover:bg-white/[0.04] transition-all group relative">
|
||||||
{/* TLD */}
|
{/* TLD */}
|
||||||
<div className="col-span-2">
|
<div className="col-span-2">
|
||||||
<span className="font-mono font-bold text-white text-lg">.{tld.tld}</span>
|
<Link href={`/terminal/intel/${tld.tld}`} className="font-mono font-bold text-white text-lg hover:text-emerald-400 transition-colors">
|
||||||
|
.{tld.tld}
|
||||||
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Price */}
|
{/* Price */}
|
||||||
@ -396,15 +398,21 @@ export default function IntelPage() {
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Provider */}
|
{/* Action / Provider */}
|
||||||
<div className="col-span-2 text-right">
|
<div className="col-span-2 flex justify-end items-center gap-3">
|
||||||
{tld.cheapest_registrar ? (
|
{tld.cheapest_registrar && (
|
||||||
<a href={tld.cheapest_registrar_url || '#'} target="_blank" className="text-xs text-zinc-500 hover:text-white transition-colors">
|
<Tooltip content={`Best price at ${tld.cheapest_registrar}`}>
|
||||||
{tld.cheapest_registrar} <ExternalLink className="w-3 h-3 inline ml-0.5" />
|
<a href={tld.cheapest_registrar_url || '#'} target="_blank" className="text-xs text-zinc-500 hover:text-white transition-colors truncate max-w-[80px]">
|
||||||
</a>
|
{tld.cheapest_registrar}
|
||||||
) : (
|
</a>
|
||||||
<span className="text-xs text-zinc-600">-</span>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
|
<Link
|
||||||
|
href={`/terminal/intel/${tld.tld}`}
|
||||||
|
className="w-8 h-8 flex items-center justify-center rounded-lg border border-zinc-800 text-zinc-400 hover:text-white hover:border-zinc-600 hover:bg-white/5 transition-all"
|
||||||
|
>
|
||||||
|
<ArrowRight className="w-4 h-4" />
|
||||||
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
@ -417,40 +425,45 @@ export default function IntelPage() {
|
|||||||
{filteredData.map((tld) => {
|
{filteredData.map((tld) => {
|
||||||
const isTrap = tld.min_renewal_price > tld.min_price * 1.5
|
const isTrap = tld.min_renewal_price > tld.min_price * 1.5
|
||||||
return (
|
return (
|
||||||
<div key={tld.tld} className="bg-zinc-900/40 border border-white/5 rounded-xl p-4 active:bg-zinc-900/60 transition-colors">
|
<Link href={`/terminal/intel/${tld.tld}`} key={tld.tld}>
|
||||||
<div className="flex justify-between items-start mb-3">
|
<div className="bg-zinc-900/40 border border-white/5 rounded-xl p-4 active:bg-zinc-900/60 transition-colors">
|
||||||
<span className="font-mono font-bold text-white text-xl">.{tld.tld}</span>
|
<div className="flex justify-between items-start mb-3">
|
||||||
<div className={clsx("px-2 py-1 rounded text-[10px] uppercase font-bold",
|
<span className="font-mono font-bold text-white text-xl">.{tld.tld}</span>
|
||||||
tld.risk_level === 'low' ? "bg-emerald-500/10 text-emerald-400" :
|
<div className={clsx("px-2 py-1 rounded text-[10px] uppercase font-bold",
|
||||||
tld.risk_level === 'medium' ? "bg-amber-500/10 text-amber-400" :
|
tld.risk_level === 'low' ? "bg-emerald-500/10 text-emerald-400" :
|
||||||
"bg-red-500/10 text-red-400"
|
tld.risk_level === 'medium' ? "bg-amber-500/10 text-amber-400" :
|
||||||
)}>
|
"bg-red-500/10 text-red-400"
|
||||||
{tld.risk_level} Risk
|
)}>
|
||||||
|
{tld.risk_level} Risk
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
<div className="grid grid-cols-2 gap-4 mb-3">
|
||||||
<div className="grid grid-cols-2 gap-4 mb-3">
|
<div>
|
||||||
<div>
|
<div className="text-[10px] text-zinc-500 uppercase tracking-wider mb-0.5">Register</div>
|
||||||
<div className="text-[10px] text-zinc-500 uppercase tracking-wider mb-0.5">Register</div>
|
<div className="font-mono text-lg font-medium text-white">{formatPrice(tld.min_price)}</div>
|
||||||
<div className="font-mono text-lg font-medium text-white">{formatPrice(tld.min_price)}</div>
|
</div>
|
||||||
|
<div className="text-right">
|
||||||
|
<div className="text-[10px] text-zinc-500 uppercase tracking-wider mb-0.5">Renew</div>
|
||||||
|
<div className={clsx("font-mono text-lg font-medium", isTrap ? "text-amber-400" : "text-zinc-400")}>
|
||||||
|
{formatPrice(tld.min_renewal_price)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-right">
|
|
||||||
<div className="text-[10px] text-zinc-500 uppercase tracking-wider mb-0.5">Renew</div>
|
<div className="pt-3 border-t border-white/5 flex items-center justify-between">
|
||||||
<div className={clsx("font-mono text-lg font-medium", isTrap ? "text-amber-400" : "text-zinc-400")}>
|
<div className="flex items-center gap-1 text-xs text-zinc-500">
|
||||||
{formatPrice(tld.min_renewal_price)}
|
<span>Provider:</span>
|
||||||
|
<span className="text-white font-medium truncate max-w-[100px]">
|
||||||
|
{tld.cheapest_registrar || '-'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-1 text-emerald-400 text-xs font-bold">
|
||||||
|
Details <ArrowRight className="w-3 h-3" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</Link>
|
||||||
{tld.cheapest_registrar && (
|
|
||||||
<div className="pt-3 border-t border-white/5 text-center">
|
|
||||||
<span className="text-xs text-zinc-500">Best price at </span>
|
|
||||||
<a href={tld.cheapest_registrar_url || '#'} className="text-xs text-white font-medium hover:underline">
|
|
||||||
{tld.cheapest_registrar}
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -141,9 +141,9 @@ function parseTimeToSeconds(timeStr?: string): number {
|
|||||||
// Tooltip Component
|
// Tooltip Component
|
||||||
function Tooltip({ children, content }: { children: React.ReactNode; content: string }) {
|
function Tooltip({ children, content }: { children: React.ReactNode; content: string }) {
|
||||||
return (
|
return (
|
||||||
<div className="relative flex items-center group">
|
<div className="relative flex items-center group/tooltip w-fit">
|
||||||
{children}
|
{children}
|
||||||
<div className="absolute bottom-full left-1/2 -translate-x-1/2 mb-2 px-2 py-1 bg-zinc-900 border border-zinc-800 rounded text-[10px] text-zinc-300 whitespace-nowrap opacity-0 group-hover:opacity-100 transition-opacity pointer-events-none z-50 shadow-xl">
|
<div className="absolute bottom-full left-1/2 -translate-x-1/2 mb-2 px-2 py-1 bg-zinc-900 border border-zinc-800 rounded text-[10px] text-zinc-300 whitespace-nowrap opacity-0 group-hover/tooltip:opacity-100 transition-opacity pointer-events-none z-50 shadow-xl">
|
||||||
{content}
|
{content}
|
||||||
{/* Arrow */}
|
{/* Arrow */}
|
||||||
<div className="absolute top-full left-1/2 -translate-x-1/2 -mt-px border-4 border-transparent border-t-zinc-800" />
|
<div className="absolute top-full left-1/2 -translate-x-1/2 -mt-px border-4 border-transparent border-t-zinc-800" />
|
||||||
@ -313,7 +313,7 @@ export default function MarketPage() {
|
|||||||
// Watchlist
|
// Watchlist
|
||||||
const [trackedDomains, setTrackedDomains] = useState<Set<string>>(new Set())
|
const [trackedDomains, setTrackedDomains] = useState<Set<string>>(new Set())
|
||||||
const [trackingInProgress, setTrackingInProgress] = useState<string | null>(null)
|
const [trackingInProgress, setTrackingInProgress] = useState<string | null>(null)
|
||||||
|
|
||||||
// Load
|
// Load
|
||||||
const loadData = useCallback(async () => {
|
const loadData = useCallback(async () => {
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
@ -378,7 +378,7 @@ export default function MarketPage() {
|
|||||||
if (priceRange === 'high') filtered = filtered.filter(item => item.price >= 1000)
|
if (priceRange === 'high') filtered = filtered.filter(item => item.price >= 1000)
|
||||||
if (searchQuery) filtered = filtered.filter(item => item.domain.toLowerCase().includes(searchQuery.toLowerCase()))
|
if (searchQuery) filtered = filtered.filter(item => item.domain.toLowerCase().includes(searchQuery.toLowerCase()))
|
||||||
|
|
||||||
const mult = sortDirection === 'asc' ? 1 : -1
|
const mult = sortDirection === 'asc' ? 1 : -1
|
||||||
filtered.sort((a, b) => {
|
filtered.sort((a, b) => {
|
||||||
switch (sortField) {
|
switch (sortField) {
|
||||||
case 'domain': return mult * a.domain.localeCompare(b.domain)
|
case 'domain': return mult * a.domain.localeCompare(b.domain)
|
||||||
@ -386,9 +386,9 @@ export default function MarketPage() {
|
|||||||
case 'price': return mult * (a.price - b.price)
|
case 'price': return mult * (a.price - b.price)
|
||||||
case 'time': return mult * (parseTimeToSeconds(a.timeLeft) - parseTimeToSeconds(b.timeLeft))
|
case 'time': return mult * (parseTimeToSeconds(a.timeLeft) - parseTimeToSeconds(b.timeLeft))
|
||||||
case 'source': return mult * a.source.localeCompare(b.source)
|
case 'source': return mult * a.source.localeCompare(b.source)
|
||||||
default: return 0
|
default: return 0
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
return filtered
|
return filtered
|
||||||
}, [auctions, hideSpam, pounceOnly, priceRange, searchQuery, sortField, sortDirection])
|
}, [auctions, hideSpam, pounceOnly, priceRange, searchQuery, sortField, sortDirection])
|
||||||
|
|
||||||
@ -486,7 +486,7 @@ export default function MarketPage() {
|
|||||||
<div className="col-span-2 text-right"><SortableHeader label="Price" field="price" currentSort={sortField} currentDirection={sortDirection} onSort={handleSort} align="right" tooltip="Current highest bid or buy-now price" /></div>
|
<div className="col-span-2 text-right"><SortableHeader label="Price" field="price" currentSort={sortField} currentDirection={sortDirection} onSort={handleSort} align="right" tooltip="Current highest bid or buy-now price" /></div>
|
||||||
<div className="col-span-2 text-center"><SortableHeader label="Time" field="time" currentSort={sortField} currentDirection={sortDirection} onSort={handleSort} align="center" tooltip="Time remaining until auction ends" /></div>
|
<div className="col-span-2 text-center"><SortableHeader label="Time" field="time" currentSort={sortField} currentDirection={sortDirection} onSort={handleSort} align="center" tooltip="Time remaining until auction ends" /></div>
|
||||||
<div className="col-span-2 text-right"><span className="text-[10px] font-bold uppercase tracking-widest text-zinc-600 py-2 block">Action</span></div>
|
<div className="col-span-2 text-right"><span className="text-[10px] font-bold uppercase tracking-widest text-zinc-600 py-2 block">Action</span></div>
|
||||||
</div>
|
</div>
|
||||||
<div className="divide-y divide-white/5">
|
<div className="divide-y divide-white/5">
|
||||||
{marketItems.map((item) => {
|
{marketItems.map((item) => {
|
||||||
const timeLeftSec = parseTimeToSeconds(item.timeLeft)
|
const timeLeftSec = parseTimeToSeconds(item.timeLeft)
|
||||||
@ -528,7 +528,7 @@ export default function MarketPage() {
|
|||||||
{item.timeLeft}
|
{item.timeLeft}
|
||||||
</div>
|
</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
{/* Actions */}
|
{/* Actions */}
|
||||||
<div className="col-span-2 flex items-center justify-end opacity-0 group-hover:opacity-100 transition-opacity">
|
<div className="col-span-2 flex items-center justify-end opacity-0 group-hover:opacity-100 transition-opacity">
|
||||||
{/* Monitor Button - Distinct Style & Spacing */}
|
{/* Monitor Button - Distinct Style & Spacing */}
|
||||||
@ -555,7 +555,7 @@ export default function MarketPage() {
|
|||||||
</a>
|
</a>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
@ -566,7 +566,7 @@ export default function MarketPage() {
|
|||||||
{marketItems.map((item) => {
|
{marketItems.map((item) => {
|
||||||
const timeLeftSec = parseTimeToSeconds(item.timeLeft)
|
const timeLeftSec = parseTimeToSeconds(item.timeLeft)
|
||||||
const isUrgent = timeLeftSec < 3600
|
const isUrgent = timeLeftSec < 3600
|
||||||
return (
|
return (
|
||||||
<div key={item.id} className="bg-zinc-900/40 border border-white/5 rounded-xl p-4 active:bg-zinc-900/60 transition-colors">
|
<div key={item.id} className="bg-zinc-900/40 border border-white/5 rounded-xl p-4 active:bg-zinc-900/60 transition-colors">
|
||||||
<div className="flex justify-between items-start mb-3">
|
<div className="flex justify-between items-start mb-3">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
@ -588,13 +588,13 @@ export default function MarketPage() {
|
|||||||
{item.timeLeft}
|
{item.timeLeft}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-3">
|
<div className="grid grid-cols-2 gap-3">
|
||||||
<button
|
<button
|
||||||
onClick={() => handleTrack(item.domain)}
|
onClick={() => handleTrack(item.domain)}
|
||||||
disabled={trackedDomains.has(item.domain)}
|
disabled={trackedDomains.has(item.domain)}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"flex items-center justify-center gap-2 py-3 rounded-xl text-sm font-medium border transition-all",
|
"flex items-center justify-center gap-2 py-3 rounded-xl text-sm font-medium border transition-all",
|
||||||
trackedDomains.has(item.domain)
|
trackedDomains.has(item.domain)
|
||||||
? "bg-emerald-500/10 text-emerald-400 border-emerald-500/20"
|
? "bg-emerald-500/10 text-emerald-400 border-emerald-500/20"
|
||||||
@ -605,27 +605,27 @@ export default function MarketPage() {
|
|||||||
<><Check className="w-4 h-4" /> Tracked</>
|
<><Check className="w-4 h-4" /> Tracked</>
|
||||||
) : (
|
) : (
|
||||||
<><Eye className="w-4 h-4" /> Watch</>
|
<><Eye className="w-4 h-4" /> Watch</>
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
<a
|
<a
|
||||||
href={item.affiliateUrl || '#'}
|
href={item.affiliateUrl || '#'}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="flex items-center justify-center gap-2 py-3 rounded-xl text-sm font-bold bg-white text-black hover:bg-zinc-200 active:scale-95 transition-all shadow-lg shadow-white/5"
|
className="flex items-center justify-center gap-2 py-3 rounded-xl text-sm font-bold bg-white text-black hover:bg-zinc-200 active:scale-95 transition-all shadow-lg shadow-white/5"
|
||||||
>
|
>
|
||||||
{item.isPounce ? 'Buy Now' : 'Place Bid'}
|
{item.isPounce ? 'Buy Now' : 'Place Bid'}
|
||||||
<ExternalLink className="w-3 h-3 opacity-50" />
|
<ExternalLink className="w-3 h-3 opacity-50" />
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</TerminalLayout>
|
</TerminalLayout>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -40,9 +40,9 @@ import Link from 'next/link'
|
|||||||
|
|
||||||
function Tooltip({ children, content }: { children: React.ReactNode; content: string }) {
|
function Tooltip({ children, content }: { children: React.ReactNode; content: string }) {
|
||||||
return (
|
return (
|
||||||
<div className="relative flex items-center group">
|
<div className="relative flex items-center group/tooltip w-fit">
|
||||||
{children}
|
{children}
|
||||||
<div className="absolute bottom-full left-1/2 -translate-x-1/2 mb-2 px-2 py-1 bg-zinc-900 border border-zinc-800 rounded text-[10px] text-zinc-300 whitespace-nowrap opacity-0 group-hover:opacity-100 transition-opacity pointer-events-none z-50 shadow-xl">
|
<div className="absolute bottom-full left-1/2 -translate-x-1/2 mb-2 px-2 py-1 bg-zinc-900 border border-zinc-800 rounded text-[10px] text-zinc-300 whitespace-nowrap opacity-0 group-hover/tooltip:opacity-100 transition-opacity pointer-events-none z-50 shadow-xl">
|
||||||
{content}
|
{content}
|
||||||
<div className="absolute top-full left-1/2 -translate-x-1/2 -mt-px border-4 border-transparent border-t-zinc-800" />
|
<div className="absolute top-full left-1/2 -translate-x-1/2 -mt-px border-4 border-transparent border-t-zinc-800" />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user