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