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:
@ -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" />
|
||||
|
||||
Reference in New Issue
Block a user