Changes: - Rebuilt Dashboard/Radar page from scratch to match Market style - Features: - New 'Ticker' component with clean, borderless design - High-end 'StatCard' grid - 'Universal Search' command center with emerald glow - Split view: Market Pulse vs Watchlist Activity - Visuals: - Dark zinc-950/40 backgrounds - Ultra-thin borders (white/5) - Consistent tooltips and hover effects - Mobile optimized layout
163 lines
4.9 KiB
TypeScript
163 lines
4.9 KiB
TypeScript
'use client'
|
|
|
|
import { useEffect, useState, useRef } from 'react'
|
|
import { TrendingUp, TrendingDown, AlertCircle, Sparkles, Gavel, Clock } from 'lucide-react'
|
|
import clsx from 'clsx'
|
|
|
|
export interface TickerItem {
|
|
id: string
|
|
type: 'tld_change' | 'domain_available' | 'auction_ending' | 'alert'
|
|
message: string
|
|
value?: string
|
|
change?: number
|
|
urgent?: boolean
|
|
}
|
|
|
|
interface TickerProps {
|
|
items: TickerItem[]
|
|
speed?: number // pixels per second
|
|
}
|
|
|
|
export function Ticker({ items, speed = 40 }: TickerProps) {
|
|
const containerRef = useRef<HTMLDivElement>(null)
|
|
const contentRef = useRef<HTMLDivElement>(null)
|
|
const [animationDuration, setAnimationDuration] = useState(0)
|
|
|
|
useEffect(() => {
|
|
if (contentRef.current && containerRef.current) {
|
|
const contentWidth = contentRef.current.scrollWidth
|
|
const duration = contentWidth / speed
|
|
setAnimationDuration(duration)
|
|
}
|
|
}, [items, speed])
|
|
|
|
if (items.length === 0) return null
|
|
|
|
const getIcon = (type: TickerItem['type'], change?: number) => {
|
|
switch (type) {
|
|
case 'tld_change':
|
|
return change && change > 0
|
|
? <TrendingUp className="w-3.5 h-3.5 text-emerald-400" />
|
|
: <TrendingDown className="w-3.5 h-3.5 text-red-400" />
|
|
case 'domain_available':
|
|
return <Sparkles className="w-3.5 h-3.5 text-emerald-400" />
|
|
case 'auction_ending':
|
|
return <Clock className="w-3.5 h-3.5 text-amber-400" />
|
|
case 'alert':
|
|
return <AlertCircle className="w-3.5 h-3.5 text-red-400" />
|
|
default:
|
|
return null
|
|
}
|
|
}
|
|
|
|
const getValueColor = (type: TickerItem['type'], change?: number) => {
|
|
if (type === 'tld_change') {
|
|
return change && change > 0 ? 'text-emerald-400' : 'text-red-400'
|
|
}
|
|
return 'text-white'
|
|
}
|
|
|
|
// Duplicate items for seamless loop
|
|
const tickerItems = [...items, ...items, ...items]
|
|
|
|
return (
|
|
<div
|
|
ref={containerRef}
|
|
className="relative w-full overflow-hidden bg-zinc-900/30 border-y border-white/5 backdrop-blur-sm"
|
|
>
|
|
{/* Fade edges */}
|
|
<div className="absolute left-0 top-0 bottom-0 w-24 bg-gradient-to-r from-zinc-950 to-transparent z-10 pointer-events-none" />
|
|
<div className="absolute right-0 top-0 bottom-0 w-24 bg-gradient-to-l from-zinc-950 to-transparent z-10 pointer-events-none" />
|
|
|
|
<div
|
|
ref={contentRef}
|
|
className="flex items-center gap-12 py-3 px-4 whitespace-nowrap animate-ticker"
|
|
style={{
|
|
animationDuration: `${animationDuration}s`,
|
|
}}
|
|
>
|
|
{tickerItems.map((item, idx) => (
|
|
<div
|
|
key={`${item.id}-${idx}`}
|
|
className={clsx(
|
|
"flex items-center gap-2.5 text-xs font-medium tracking-wide",
|
|
item.urgent && "text-white"
|
|
)}
|
|
>
|
|
{getIcon(item.type, item.change)}
|
|
<span className="text-zinc-400">{item.message}</span>
|
|
{item.value && (
|
|
<span className={clsx("font-mono", getValueColor(item.type, item.change))}>
|
|
{item.value}
|
|
</span>
|
|
)}
|
|
{item.change !== undefined && (
|
|
<span className={clsx(
|
|
"px-1.5 py-0.5 rounded text-[10px] font-bold",
|
|
item.change > 0 ? "bg-emerald-500/10 text-emerald-400" : "bg-red-500/10 text-red-400"
|
|
)}>
|
|
{item.change > 0 ? '+' : ''}{item.change.toFixed(1)}%
|
|
</span>
|
|
)}
|
|
</div>
|
|
))}
|
|
</div>
|
|
|
|
<style jsx>{`
|
|
@keyframes ticker {
|
|
0% { transform: translateX(0); }
|
|
100% { transform: translateX(-33.33%); }
|
|
}
|
|
.animate-ticker {
|
|
animation: ticker linear infinite;
|
|
}
|
|
.animate-ticker:hover {
|
|
animation-play-state: paused;
|
|
}
|
|
`}</style>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
// Hook to generate ticker items from various data sources
|
|
export function useTickerItems(
|
|
trendingTlds: Array<{ tld: string; price_change: number; current_price: number }>,
|
|
availableDomains: Array<{ name: string }>,
|
|
hotAuctions: Array<{ domain: string; time_remaining: string }>
|
|
): TickerItem[] {
|
|
const items: TickerItem[] = []
|
|
|
|
// Add TLD changes
|
|
trendingTlds.forEach((tld) => {
|
|
items.push({
|
|
id: `tld-${tld.tld}`,
|
|
type: 'tld_change',
|
|
message: `.${tld.tld}`,
|
|
value: `$${tld.current_price.toFixed(2)}`,
|
|
change: tld.price_change,
|
|
})
|
|
})
|
|
|
|
// Add available domains
|
|
availableDomains.slice(0, 3).forEach((domain) => {
|
|
items.push({
|
|
id: `available-${domain.name}`,
|
|
type: 'domain_available',
|
|
message: `${domain.name} available!`,
|
|
urgent: true,
|
|
})
|
|
})
|
|
|
|
// Add ending auctions
|
|
hotAuctions.slice(0, 3).forEach((auction) => {
|
|
items.push({
|
|
id: `auction-${auction.domain}`,
|
|
type: 'auction_ending',
|
|
message: `${auction.domain}`,
|
|
value: auction.time_remaining,
|
|
})
|
|
})
|
|
|
|
return items
|
|
}
|