feat: MARKET - Award-Winning Polish (Tooltips & UX)

Changes:
- Added custom animated Tooltip component for Low Noise / High Density
- Tooltips added to: Score, Price, Time, Source, Actions, Headers
- Redesigned 'Monitor' button:
  - Changed icon to 'Eye' (Watch)
  - Made it round and distinct from primary action
  - Added significant spacing (mr-4) from 'Buy' button
- Enhanced Mobile UX:
  - Larger touch targets (h-9 to h-10 equivalents)
  - Better button active states (scale-95)
- General Polish:
  - Refined hover states and shadows
  - Improved accessibility with cursor hints
This commit is contained in:
2025-12-11 06:56:12 +01:00
parent e85a5a65a4
commit 8e2adfac0a

View File

@ -25,7 +25,9 @@ import {
LayoutGrid,
List,
SlidersHorizontal,
MoreHorizontal
MoreHorizontal,
Eye,
Info
} from 'lucide-react'
import clsx from 'clsx'
@ -136,6 +138,20 @@ function parseTimeToSeconds(timeStr?: string): number {
// COMPONENTS
// ============================================================================
// Tooltip Component
function Tooltip({ children, content }: { children: React.ReactNode; content: string }) {
return (
<div className="relative flex items-center group">
{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">
{content}
{/* Arrow */}
<div className="absolute top-full left-1/2 -translate-x-1/2 -mt-px border-4 border-transparent border-t-zinc-800" />
</div>
</div>
)
}
// Stat Card
function StatCard({
label,
@ -196,29 +212,27 @@ function ScoreDisplay({ score, mobile = false }: { score: number; mobile?: boole
const offset = circumference - (score / 100) * circumference
return (
<div className="relative flex items-center justify-center group" style={{ width: size, height: size }}>
<svg className="absolute w-full h-full -rotate-90">
<circle cx={size/2} cy={size/2} r={radius} className="stroke-zinc-800" strokeWidth={strokeWidth} fill="none" />
<circle
cx={size/2}
cy={size/2}
r={radius}
className={clsx("transition-all duration-700 ease-out", color)}
strokeWidth={strokeWidth}
strokeDasharray={circumference}
strokeDashoffset={offset}
strokeLinecap="round"
fill="none"
/>
</svg>
<span className={clsx("text-[11px] font-bold font-mono", score >= 80 ? 'text-emerald-400' : 'text-zinc-400')}>
{score}
</span>
{/* Tooltip */}
<div className="absolute top-full mt-2 left-1/2 -translate-x-1/2 bg-zinc-900 border border-zinc-800 px-2 py-1 rounded text-[10px] text-zinc-400 opacity-0 group-hover:opacity-100 transition-opacity whitespace-nowrap z-20 pointer-events-none">
Pounce Score
<Tooltip content={`Pounce Score: ${score}/100`}>
<div className="relative flex items-center justify-center cursor-help" style={{ width: size, height: size }}>
<svg className="absolute w-full h-full -rotate-90">
<circle cx={size/2} cy={size/2} r={radius} className="stroke-zinc-800" strokeWidth={strokeWidth} fill="none" />
<circle
cx={size/2}
cy={size/2}
r={radius}
className={clsx("transition-all duration-700 ease-out", color)}
strokeWidth={strokeWidth}
strokeDasharray={circumference}
strokeDashoffset={offset}
strokeLinecap="round"
fill="none"
/>
</svg>
<span className={clsx("text-[11px] font-bold font-mono", score >= 80 ? 'text-emerald-400' : 'text-zinc-400')}>
{score}
</span>
</div>
</div>
</Tooltip>
)
}
@ -241,27 +255,36 @@ function FilterToggle({ active, onClick, label }: { active: boolean; onClick: ()
// Sort Header
function SortableHeader({
label, field, currentSort, currentDirection, onSort, align = 'left'
label, field, currentSort, currentDirection, onSort, align = 'left', tooltip
}: {
label: string; field: SortField; currentSort: SortField; currentDirection: SortDirection; onSort: (field: SortField) => void; align?: 'left'|'center'|'right'
label: string; field: SortField; currentSort: SortField; currentDirection: SortDirection; onSort: (field: SortField) => void; align?: 'left'|'center'|'right'; tooltip?: string
}) {
const isActive = currentSort === field
return (
<button
onClick={() => onSort(field)}
className={clsx(
"flex items-center gap-1.5 text-[10px] font-bold uppercase tracking-widest transition-all group select-none py-2",
align === 'right' && "justify-end ml-auto",
align === 'center' && "justify-center mx-auto",
isActive ? "text-white" : "text-zinc-500 hover:text-zinc-300"
<div className={clsx(
"flex items-center gap-1",
align === 'right' && "justify-end ml-auto",
align === 'center' && "justify-center mx-auto"
)}>
<button
onClick={() => onSort(field)}
className={clsx(
"flex items-center gap-1.5 text-[10px] font-bold uppercase tracking-widest transition-all group select-none py-2",
isActive ? "text-white" : "text-zinc-500 hover:text-zinc-300"
)}
>
{label}
<div className={clsx("flex flex-col -space-y-1 transition-opacity", isActive ? "opacity-100" : "opacity-0 group-hover:opacity-30")}>
<ChevronUp className={clsx("w-2 h-2", isActive && currentDirection === 'asc' ? "text-white" : "text-zinc-600")} />
<ChevronDown className={clsx("w-2 h-2", isActive && currentDirection === 'desc' ? "text-white" : "text-zinc-600")} />
</div>
</button>
{tooltip && (
<Tooltip content={tooltip}>
<Info className="w-3 h-3 text-zinc-700 hover:text-zinc-500 transition-colors cursor-help" />
</Tooltip>
)}
>
{label}
<div className={clsx("flex flex-col -space-y-1 transition-opacity", isActive ? "opacity-100" : "opacity-0 group-hover:opacity-30")}>
<ChevronUp className={clsx("w-2 h-2", isActive && currentDirection === 'asc' ? "text-white" : "text-zinc-600")} />
<ChevronDown className={clsx("w-2 h-2", isActive && currentDirection === 'desc' ? "text-white" : "text-zinc-600")} />
</div>
</button>
</div>
)
}
@ -446,10 +469,10 @@ export default function MarketPage() {
{/* DESKTOP TABLE */}
<div className="hidden md:block border border-white/5 rounded-xl overflow-hidden bg-zinc-900/40 backdrop-blur-sm shadow-xl">
<div className="grid grid-cols-12 gap-4 px-6 py-3 border-b border-white/5 bg-white/[0.02]">
<div className="col-span-4"><SortableHeader label="Domain Asset" field="domain" currentSort={sortField} currentDirection={sortDirection} onSort={handleSort} /></div>
<div className="col-span-2 text-center"><SortableHeader label="Score" field="score" currentSort={sortField} currentDirection={sortDirection} onSort={handleSort} align="center" /></div>
<div className="col-span-2 text-right"><SortableHeader label="Price" field="price" currentSort={sortField} currentDirection={sortDirection} onSort={handleSort} align="right" /></div>
<div className="col-span-2 text-center"><SortableHeader label="Time" field="time" currentSort={sortField} currentDirection={sortDirection} onSort={handleSort} align="center" /></div>
<div className="col-span-4"><SortableHeader label="Domain Asset" field="domain" currentSort={sortField} currentDirection={sortDirection} onSort={handleSort} tooltip="The domain name being auctioned" /></div>
<div className="col-span-2 text-center"><SortableHeader label="Score" field="score" currentSort={sortField} currentDirection={sortDirection} onSort={handleSort} align="center" tooltip="Pounce Score: AI-calculated value based on length, TLD, and demand" /></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-right"><span className="text-[10px] font-bold uppercase tracking-widest text-zinc-600 py-2 block">Action</span></div>
</div>
<div className="divide-y divide-white/5">
@ -461,10 +484,16 @@ export default function MarketPage() {
{/* Domain */}
<div className="col-span-4">
<div className="flex items-center gap-3">
{item.isPounce && <Diamond className="w-4 h-4 text-emerald-400 fill-emerald-400/20" />}
{item.isPounce && (
<Tooltip content="Pounce Exclusive Inventory">
<Diamond className="w-4 h-4 text-emerald-400 fill-emerald-400/20" />
</Tooltip>
)}
<div>
<div className="font-medium text-white text-[15px] tracking-tight">{item.domain}</div>
<div className="text-[11px] text-zinc-500 mt-0.5">{item.source}</div>
<div className="font-medium text-white text-[15px] tracking-tight cursor-default">{item.domain}</div>
<Tooltip content={`Source: ${item.source}`}>
<div className="text-[11px] text-zinc-500 mt-0.5 w-fit hover:text-zinc-300 cursor-help transition-colors">{item.source}</div>
</Tooltip>
</div>
</div>
</div>
@ -472,25 +501,47 @@ export default function MarketPage() {
<div className="col-span-2 flex justify-center"><ScoreDisplay score={item.pounceScore} /></div>
{/* Price */}
<div className="col-span-2 text-right">
<div className="font-mono text-white font-medium">{formatPrice(item.price)}</div>
{item.numBids !== undefined && item.numBids > 0 && <div className="text-[10px] text-zinc-500 mt-0.5">{item.numBids} bids</div>}
<Tooltip content={`${item.numBids || 0} bids placed`}>
<div className="cursor-help">
<div className="font-mono text-white font-medium">{formatPrice(item.price)}</div>
{item.numBids !== undefined && item.numBids > 0 && <div className="text-[10px] text-zinc-500 mt-0.5">{item.numBids} bids</div>}
</div>
</Tooltip>
</div>
{/* Time */}
<div className="col-span-2 flex justify-center">
<div className={clsx("flex items-center gap-1.5 px-2.5 py-1 rounded-full text-xs font-medium", isUrgent ? "text-red-400 bg-red-500/10" : "text-zinc-400 bg-zinc-800/50")}>
<Clock className="w-3 h-3" />
{item.timeLeft}
</div>
<Tooltip content="Auction ends soon">
<div className={clsx("flex items-center gap-1.5 px-2.5 py-1 rounded-full text-xs font-medium cursor-help", isUrgent ? "text-red-400 bg-red-500/10" : "text-zinc-400 bg-zinc-800/50")}>
<Clock className="w-3 h-3" />
{item.timeLeft}
</div>
</Tooltip>
</div>
{/* Actions */}
<div className="col-span-2 flex items-center justify-end gap-2 opacity-0 group-hover:opacity-100 transition-opacity">
<button onClick={() => handleTrack(item.domain)} disabled={trackedDomains.has(item.domain)} className={clsx("w-8 h-8 flex items-center justify-center rounded-lg border transition-colors", trackedDomains.has(item.domain) ? "bg-emerald-500 text-white border-emerald-500" : "border-zinc-700 text-zinc-400 hover:text-white hover:border-zinc-500 hover:bg-zinc-800")}>
{trackedDomains.has(item.domain) ? <Check className="w-4 h-4" /> : <Plus className="w-4 h-4" />}
</button>
<a href={item.affiliateUrl || '#'} target="_blank" rel="noopener noreferrer" className="h-8 px-3 flex items-center gap-2 bg-white text-zinc-950 rounded-lg text-xs font-semibold hover:bg-zinc-200 transition-colors">
{item.isPounce ? 'Buy' : 'Bid'}
<ExternalLink className="w-3 h-3" />
</a>
<div className="col-span-2 flex items-center justify-end opacity-0 group-hover:opacity-100 transition-opacity">
{/* Monitor Button - Distinct Style & Spacing */}
<Tooltip content={trackedDomains.has(item.domain) ? "Already tracking" : "Add to Watchlist"}>
<button
onClick={() => handleTrack(item.domain)}
disabled={trackedDomains.has(item.domain)}
className={clsx(
"w-8 h-8 flex items-center justify-center rounded-full border transition-all mr-4", // Added margin-right for separation
trackedDomains.has(item.domain)
? "bg-emerald-500/10 text-emerald-400 border-emerald-500/20 cursor-default"
: "border-zinc-700 bg-zinc-900 text-zinc-400 hover:text-white hover:border-zinc-500 hover:scale-105 active:scale-95"
)}
>
{trackedDomains.has(item.domain) ? <Check className="w-4 h-4" /> : <Eye className="w-4 h-4" />}
</button>
</Tooltip>
{/* Buy Button */}
<Tooltip content={item.isPounce ? "Buy Instantly" : "Place Bid on External Site"}>
<a href={item.affiliateUrl || '#'} target="_blank" rel="noopener noreferrer" className="h-9 px-4 flex items-center gap-2 bg-white text-zinc-950 rounded-lg text-xs font-bold hover:bg-zinc-200 transition-all hover:scale-105 active:scale-95 shadow-lg shadow-white/5">
{item.isPounce ? 'Buy Now' : 'Place Bid'}
<ExternalLink className="w-3 h-3" />
</a>
</Tooltip>
</div>
</div>
)
@ -527,28 +578,28 @@ export default function MarketPage() {
</div>
</div>
<div className="grid grid-cols-2 gap-2">
<div className="grid grid-cols-2 gap-3">
<button
onClick={() => handleTrack(item.domain)}
disabled={trackedDomains.has(item.domain)}
className={clsx(
"flex items-center justify-center gap-2 py-2.5 rounded-lg 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)
? "bg-emerald-500/10 text-emerald-400 border-emerald-500/20"
: "bg-zinc-800/50 text-zinc-400 border-zinc-700/50"
: "bg-zinc-800/30 text-zinc-400 border-zinc-700/50 active:scale-95"
)}
>
{trackedDomains.has(item.domain) ? (
<><Check className="w-4 h-4" /> Tracked</>
) : (
<><Plus className="w-4 h-4" /> Watch</>
<><Eye className="w-4 h-4" /> Watch</>
)}
</button>
<a
href={item.affiliateUrl || '#'}
target="_blank"
rel="noopener noreferrer"
className="flex items-center justify-center gap-2 py-2.5 rounded-lg text-sm font-semibold bg-white text-black hover:bg-zinc-200 transition-colors"
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'}
<ExternalLink className="w-3 h-3 opacity-50" />