feat: MARKET - Award-Winning Polish (Tooltips & UX)
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:
- 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 0762d1b23b
commit 15148083c5

View File

@ -25,7 +25,9 @@ import {
LayoutGrid, LayoutGrid,
List, List,
SlidersHorizontal, SlidersHorizontal,
MoreHorizontal MoreHorizontal,
Eye,
Info
} from 'lucide-react' } from 'lucide-react'
import clsx from 'clsx' import clsx from 'clsx'
@ -136,6 +138,20 @@ function parseTimeToSeconds(timeStr?: string): number {
// COMPONENTS // 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 // Stat Card
function StatCard({ function StatCard({
label, label,
@ -196,29 +212,27 @@ function ScoreDisplay({ score, mobile = false }: { score: number; mobile?: boole
const offset = circumference - (score / 100) * circumference const offset = circumference - (score / 100) * circumference
return ( return (
<div className="relative flex items-center justify-center group" style={{ width: size, height: size }}> <Tooltip content={`Pounce Score: ${score}/100`}>
<svg className="absolute w-full h-full -rotate-90"> <div className="relative flex items-center justify-center cursor-help" style={{ width: size, height: size }}>
<circle cx={size/2} cy={size/2} r={radius} className="stroke-zinc-800" strokeWidth={strokeWidth} fill="none" /> <svg className="absolute w-full h-full -rotate-90">
<circle <circle cx={size/2} cy={size/2} r={radius} className="stroke-zinc-800" strokeWidth={strokeWidth} fill="none" />
cx={size/2} <circle
cy={size/2} cx={size/2}
r={radius} cy={size/2}
className={clsx("transition-all duration-700 ease-out", color)} r={radius}
strokeWidth={strokeWidth} className={clsx("transition-all duration-700 ease-out", color)}
strokeDasharray={circumference} strokeWidth={strokeWidth}
strokeDashoffset={offset} strokeDasharray={circumference}
strokeLinecap="round" strokeDashoffset={offset}
fill="none" strokeLinecap="round"
/> fill="none"
</svg> />
<span className={clsx("text-[11px] font-bold font-mono", score >= 80 ? 'text-emerald-400' : 'text-zinc-400')}> </svg>
{score} <span className={clsx("text-[11px] font-bold font-mono", score >= 80 ? 'text-emerald-400' : 'text-zinc-400')}>
</span> {score}
{/* Tooltip */} </span>
<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
</div> </div>
</div> </Tooltip>
) )
} }
@ -241,27 +255,36 @@ function FilterToggle({ active, onClick, label }: { active: boolean; onClick: ()
// Sort Header // Sort Header
function SortableHeader({ 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 const isActive = currentSort === field
return ( return (
<button <div className={clsx(
onClick={() => onSort(field)} "flex items-center gap-1",
className={clsx( align === 'right' && "justify-end ml-auto",
"flex items-center gap-1.5 text-[10px] font-bold uppercase tracking-widest transition-all group select-none py-2", align === 'center' && "justify-center mx-auto"
align === 'right' && "justify-end ml-auto", )}>
align === 'center' && "justify-center mx-auto", <button
isActive ? "text-white" : "text-zinc-500 hover:text-zinc-300" 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>
)} )}
> </div>
{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>
) )
} }
@ -446,10 +469,10 @@ export default function MarketPage() {
{/* DESKTOP TABLE */} {/* 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="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="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-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" /></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" /></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" /></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">
@ -461,10 +484,16 @@ export default function MarketPage() {
{/* Domain */} {/* Domain */}
<div className="col-span-4"> <div className="col-span-4">
<div className="flex items-center gap-3"> <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>
<div className="font-medium text-white text-[15px] tracking-tight">{item.domain}</div> <div className="font-medium text-white text-[15px] tracking-tight cursor-default">{item.domain}</div>
<div className="text-[11px] text-zinc-500 mt-0.5">{item.source}</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> </div>
</div> </div>
@ -472,25 +501,47 @@ export default function MarketPage() {
<div className="col-span-2 flex justify-center"><ScoreDisplay score={item.pounceScore} /></div> <div className="col-span-2 flex justify-center"><ScoreDisplay score={item.pounceScore} /></div>
{/* Price */} {/* Price */}
<div className="col-span-2 text-right"> <div className="col-span-2 text-right">
<div className="font-mono text-white font-medium">{formatPrice(item.price)}</div> <Tooltip content={`${item.numBids || 0} bids placed`}>
{item.numBids !== undefined && item.numBids > 0 && <div className="text-[10px] text-zinc-500 mt-0.5">{item.numBids} bids</div>} <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> </div>
{/* Time */} {/* Time */}
<div className="col-span-2 flex justify-center"> <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")}> <Tooltip content="Auction ends soon">
<Clock className="w-3 h-3" /> <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")}>
{item.timeLeft} <Clock className="w-3 h-3" />
</div> {item.timeLeft}
</div>
</Tooltip>
</div> </div>
{/* Actions */} {/* Actions */}
<div className="col-span-2 flex items-center justify-end gap-2 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">
<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")}> {/* Monitor Button - Distinct Style & Spacing */}
{trackedDomains.has(item.domain) ? <Check className="w-4 h-4" /> : <Plus className="w-4 h-4" />} <Tooltip content={trackedDomains.has(item.domain) ? "Already tracking" : "Add to Watchlist"}>
</button> <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"> onClick={() => handleTrack(item.domain)}
{item.isPounce ? 'Buy' : 'Bid'} disabled={trackedDomains.has(item.domain)}
<ExternalLink className="w-3 h-3" /> className={clsx(
</a> "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>
</div> </div>
) )
@ -527,28 +578,28 @@ export default function MarketPage() {
</div> </div>
</div> </div>
<div className="grid grid-cols-2 gap-2"> <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-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) 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"
: "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) ? ( {trackedDomains.has(item.domain) ? (
<><Check className="w-4 h-4" /> Tracked</> <><Check className="w-4 h-4" /> Tracked</>
) : ( ) : (
<><Plus 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-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'} {item.isPounce ? 'Buy Now' : 'Place Bid'}
<ExternalLink className="w-3 h-3 opacity-50" /> <ExternalLink className="w-3 h-3 opacity-50" />