refactor: Terminal Module Restructure (Sprint 2)
- RADAR: dashboard → /terminal/radar - MARKET: auctions + marketplace → /terminal/market - INTEL: pricing → /terminal/intel - WATCHLIST: watchlist + portfolio → /terminal/watchlist - LISTING: listings → /terminal/listing All redirects configured for backwards compatibility. Updated sidebar navigation with new module names.
This commit is contained in:
@ -8,19 +8,16 @@
|
||||
|
||||
## 📊 IST vs. SOLL Analyse
|
||||
|
||||
### Aktuelle Struktur (Command Center)
|
||||
### Aktuelle Struktur (Terminal) ✅ IMPLEMENTIERT
|
||||
```
|
||||
/command/
|
||||
├── dashboard/ → Allgemeines Dashboard
|
||||
├── watchlist/ → Domain-Überwachung
|
||||
├── portfolio/ → Eigene Domains
|
||||
├── listings/ → Verkaufsangebote
|
||||
├── auctions/ → Externe Auktionen
|
||||
├── marketplace/ → Pounce-Marktplatz
|
||||
├── pricing/ → TLD Preise
|
||||
├── alerts/ → Sniper Alerts
|
||||
├── seo/ → SEO Juice (Tycoon)
|
||||
├── settings/ → Einstellungen
|
||||
/terminal/
|
||||
├── radar/ → RADAR (Startseite/Dashboard)
|
||||
├── market/ → MARKET (Auktionen + Listings)
|
||||
├── intel/ → INTEL (TLD Pricing)
|
||||
│ └── [tld]/ → Detail-Seite pro TLD
|
||||
├── watchlist/ → WATCHLIST (Watching + Portfolio)
|
||||
├── listing/ → LISTING (Verkaufs-Wizard)
|
||||
├── settings/ → SETTINGS (Einstellungen)
|
||||
└── welcome/ → Onboarding
|
||||
```
|
||||
|
||||
@ -47,13 +44,13 @@
|
||||
- [x] 1.4 Redirect von `/command/*` → `/terminal/*` einrichten
|
||||
- [x] 1.5 Sidebar-Navigation aktualisieren
|
||||
|
||||
### Phase 2: Module neu strukturieren
|
||||
- [ ] 2.1 **RADAR** Module (Dashboard)
|
||||
- [ ] 2.2 **MARKET** Module (Auktionen + Listings)
|
||||
- [ ] 2.3 **INTEL** Module (TLD Pricing)
|
||||
- [ ] 2.4 **WATCHLIST** Module (Watching + Portfolio)
|
||||
- [ ] 2.5 **LISTING** Module (Verkaufs-Wizard)
|
||||
- [ ] 2.6 **SETTINGS** Module (Admin)
|
||||
### Phase 2: Module neu strukturieren ✅ ABGESCHLOSSEN
|
||||
- [x] 2.1 **RADAR** Module (Dashboard → /terminal/radar)
|
||||
- [x] 2.2 **MARKET** Module (Auktionen + Listings → /terminal/market)
|
||||
- [x] 2.3 **INTEL** Module (TLD Pricing → /terminal/intel)
|
||||
- [x] 2.4 **WATCHLIST** Module (Watching + Portfolio → /terminal/watchlist)
|
||||
- [x] 2.5 **LISTING** Module (Verkaufs-Wizard → /terminal/listing)
|
||||
- [x] 2.6 **SETTINGS** Module (Admin → /terminal/settings)
|
||||
|
||||
### Phase 3: UI/UX Verbesserungen
|
||||
- [ ] 3.1 Global Search (CMD+K) verbessern
|
||||
|
||||
@ -3,12 +3,13 @@ const nextConfig = {
|
||||
reactStrictMode: true,
|
||||
// output: 'standalone', // Only needed for Docker deployment
|
||||
|
||||
// Redirects from old /command/* to new /terminal/*
|
||||
// Redirects from old routes to new Terminal routes
|
||||
async redirects() {
|
||||
return [
|
||||
// Old Command Center routes
|
||||
{
|
||||
source: '/command',
|
||||
destination: '/terminal/dashboard',
|
||||
destination: '/terminal/radar',
|
||||
permanent: true,
|
||||
},
|
||||
{
|
||||
@ -16,6 +17,58 @@ const nextConfig = {
|
||||
destination: '/terminal/:path*',
|
||||
permanent: true,
|
||||
},
|
||||
// Dashboard → RADAR
|
||||
{
|
||||
source: '/terminal/dashboard',
|
||||
destination: '/terminal/radar',
|
||||
permanent: true,
|
||||
},
|
||||
// Pricing → INTEL
|
||||
{
|
||||
source: '/terminal/pricing',
|
||||
destination: '/terminal/intel',
|
||||
permanent: true,
|
||||
},
|
||||
{
|
||||
source: '/terminal/pricing/:tld*',
|
||||
destination: '/terminal/intel/:tld*',
|
||||
permanent: true,
|
||||
},
|
||||
// Listings → LISTING
|
||||
{
|
||||
source: '/terminal/listings',
|
||||
destination: '/terminal/listing',
|
||||
permanent: true,
|
||||
},
|
||||
// Auctions & Marketplace → MARKET
|
||||
{
|
||||
source: '/terminal/auctions',
|
||||
destination: '/terminal/market',
|
||||
permanent: true,
|
||||
},
|
||||
{
|
||||
source: '/terminal/marketplace',
|
||||
destination: '/terminal/market',
|
||||
permanent: true,
|
||||
},
|
||||
// Portfolio → WATCHLIST (combined)
|
||||
{
|
||||
source: '/terminal/portfolio',
|
||||
destination: '/terminal/watchlist',
|
||||
permanent: true,
|
||||
},
|
||||
// Alerts → RADAR (will be integrated)
|
||||
{
|
||||
source: '/terminal/alerts',
|
||||
destination: '/terminal/radar',
|
||||
permanent: true,
|
||||
},
|
||||
// SEO → RADAR (premium feature, hidden for now)
|
||||
{
|
||||
source: '/terminal/seo',
|
||||
destination: '/terminal/radar',
|
||||
permanent: true,
|
||||
},
|
||||
]
|
||||
},
|
||||
|
||||
|
||||
@ -216,7 +216,7 @@ export default function BrowseListingsPage() {
|
||||
: 'Be the first to list your domain!'}
|
||||
</p>
|
||||
<Link
|
||||
href="/terminal/listings"
|
||||
href="/terminal/listing"
|
||||
className="inline-flex items-center gap-2 px-6 py-3 bg-accent text-background font-medium rounded-xl hover:bg-accent-hover transition-all"
|
||||
>
|
||||
<Sparkles className="w-5 h-5" />
|
||||
@ -288,7 +288,7 @@ export default function BrowseListingsPage() {
|
||||
DNS verification ensures only real owners can list.
|
||||
</p>
|
||||
<Link
|
||||
href="/terminal/listings"
|
||||
href="/terminal/listing"
|
||||
className="inline-flex items-center gap-2 px-6 py-3 bg-accent text-background font-medium rounded-xl hover:bg-accent-hover transition-all"
|
||||
>
|
||||
List Your Domain
|
||||
|
||||
@ -56,7 +56,7 @@ function LoginForm() {
|
||||
|
||||
// Get redirect URL from query params or localStorage (set during registration)
|
||||
const paramRedirect = searchParams.get('redirect')
|
||||
const [redirectTo, setRedirectTo] = useState(paramRedirect || '/terminal/dashboard')
|
||||
const [redirectTo, setRedirectTo] = useState(paramRedirect || '/terminal/radar')
|
||||
|
||||
// Check localStorage for redirect (set during registration before email verification)
|
||||
useEffect(() => {
|
||||
@ -125,7 +125,7 @@ function LoginForm() {
|
||||
}
|
||||
|
||||
// Generate register link with redirect preserved
|
||||
const registerLink = redirectTo !== '/terminal/dashboard'
|
||||
const registerLink = redirectTo !== '/terminal/radar'
|
||||
? `/register?redirect=${encodeURIComponent(redirectTo)}`
|
||||
: '/register'
|
||||
|
||||
|
||||
@ -12,7 +12,7 @@ function OAuthCallbackContent() {
|
||||
|
||||
useEffect(() => {
|
||||
const token = searchParams.get('token')
|
||||
const redirect = searchParams.get('redirect') || '/terminal/dashboard'
|
||||
const redirect = searchParams.get('redirect') || '/terminal/radar'
|
||||
const isNew = searchParams.get('new') === 'true'
|
||||
const error = searchParams.get('error')
|
||||
|
||||
|
||||
@ -772,7 +772,7 @@ export default function HomePage() {
|
||||
<ArrowRight className="w-4 h-4" />
|
||||
</Link>
|
||||
<Link
|
||||
href={isAuthenticated ? "/terminal/dashboard" : "/register"}
|
||||
href={isAuthenticated ? "/terminal/radar" : "/register"}
|
||||
className="inline-flex items-center gap-2 px-8 py-4 text-foreground-muted hover:text-foreground transition-colors"
|
||||
>
|
||||
{isAuthenticated ? "Go to Dashboard" : "Start Free"}
|
||||
@ -793,7 +793,7 @@ export default function HomePage() {
|
||||
Track your first domain in under a minute. Free forever, no credit card.
|
||||
</p>
|
||||
<Link
|
||||
href={isAuthenticated ? "/terminal/dashboard" : "/register"}
|
||||
href={isAuthenticated ? "/terminal/radar" : "/register"}
|
||||
className="group inline-flex items-center gap-3 px-10 py-5 bg-accent text-background rounded-2xl
|
||||
text-lg font-semibold hover:bg-accent-hover transition-all duration-300
|
||||
shadow-[0_0_40px_rgba(16,185,129,0.2)] hover:shadow-[0_0_60px_rgba(16,185,129,0.3)]"
|
||||
|
||||
@ -142,7 +142,7 @@ export default function PricingPage() {
|
||||
}
|
||||
|
||||
if (!isPaid) {
|
||||
router.push('/terminal/dashboard')
|
||||
router.push('/terminal/radar')
|
||||
return
|
||||
}
|
||||
|
||||
@ -395,7 +395,7 @@ export default function PricingPage() {
|
||||
Start with Scout. It's free forever. Upgrade when you need more.
|
||||
</p>
|
||||
<Link
|
||||
href={isAuthenticated ? "/terminal/dashboard" : "/register"}
|
||||
href={isAuthenticated ? "/terminal/radar" : "/register"}
|
||||
className="btn-primary inline-flex items-center gap-2 px-6 py-3"
|
||||
>
|
||||
{isAuthenticated ? "Command Center" : "Join the Hunt"}
|
||||
|
||||
@ -62,7 +62,7 @@ function RegisterForm() {
|
||||
const [registered, setRegistered] = useState(false)
|
||||
|
||||
// Get redirect URL from query params
|
||||
const redirectTo = searchParams.get('redirect') || '/terminal/dashboard'
|
||||
const redirectTo = searchParams.get('redirect') || '/terminal/radar'
|
||||
|
||||
// Load OAuth providers
|
||||
useEffect(() => {
|
||||
@ -79,7 +79,7 @@ function RegisterForm() {
|
||||
|
||||
// Store redirect URL for after email verification
|
||||
// This will be picked up by the login page after verification
|
||||
if (redirectTo !== '/terminal/dashboard') {
|
||||
if (redirectTo !== '/terminal/radar') {
|
||||
localStorage.setItem('pounce_redirect_after_login', redirectTo)
|
||||
}
|
||||
|
||||
@ -93,7 +93,7 @@ function RegisterForm() {
|
||||
}
|
||||
|
||||
// Generate login link with redirect preserved
|
||||
const loginLink = redirectTo !== '/terminal/dashboard'
|
||||
const loginLink = redirectTo !== '/terminal/radar'
|
||||
? `/login?redirect=${encodeURIComponent(redirectTo)}`
|
||||
: '/login'
|
||||
|
||||
|
||||
@ -377,7 +377,7 @@ export default function CommandTldDetailPage() {
|
||||
<h1 className="text-xl font-medium text-foreground mb-2">TLD Not Found</h1>
|
||||
<p className="text-foreground-muted mb-8">{error || `The TLD .${tld} could not be found.`}</p>
|
||||
<Link
|
||||
href="/terminal/pricing"
|
||||
href="/terminal/intel"
|
||||
className="inline-flex items-center gap-2 px-6 py-3 bg-accent text-background rounded-xl font-medium hover:bg-accent-hover transition-all"
|
||||
>
|
||||
<ArrowLeft className="w-4 h-4" />
|
||||
@ -397,7 +397,7 @@ export default function CommandTldDetailPage() {
|
||||
<PageContainer>
|
||||
{/* Breadcrumb */}
|
||||
<nav className="flex items-center gap-2 text-sm mb-6">
|
||||
<Link href="/terminal/pricing" className="text-foreground-subtle hover:text-foreground transition-colors">
|
||||
<Link href="/terminal/intel" className="text-foreground-subtle hover:text-foreground transition-colors">
|
||||
TLD Pricing
|
||||
</Link>
|
||||
<ChevronRight className="w-3.5 h-3.5 text-foreground-subtle" />
|
||||
@ -352,7 +352,7 @@ export default function TLDPricingPage() {
|
||||
data={sortedData}
|
||||
keyExtractor={(tld) => tld.tld}
|
||||
loading={loading}
|
||||
onRowClick={(tld) => window.location.href = `/terminal/pricing/${tld.tld}`}
|
||||
onRowClick={(tld) => window.location.href = `/terminal/intel/${tld.tld}`}
|
||||
emptyIcon={<Globe className="w-12 h-12 text-foreground-subtle" />}
|
||||
emptyTitle="No TLDs found"
|
||||
emptyDescription={searchQuery ? `No TLDs matching "${searchQuery}"` : "Check back later for TLD data"}
|
||||
578
frontend/src/app/terminal/market/page.tsx
Normal file
578
frontend/src/app/terminal/market/page.tsx
Normal file
@ -0,0 +1,578 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect, useState, useMemo, useCallback, memo } from 'react'
|
||||
import { useStore } from '@/lib/store'
|
||||
import { api } from '@/lib/api'
|
||||
import { TerminalLayout } from '@/components/TerminalLayout'
|
||||
import {
|
||||
PremiumTable,
|
||||
Badge,
|
||||
PlatformBadge,
|
||||
StatCard,
|
||||
PageContainer,
|
||||
SearchInput,
|
||||
TabBar,
|
||||
FilterBar,
|
||||
SelectDropdown,
|
||||
ActionButton,
|
||||
} from '@/components/PremiumTable'
|
||||
import {
|
||||
Clock,
|
||||
ExternalLink,
|
||||
Flame,
|
||||
Timer,
|
||||
Gavel,
|
||||
DollarSign,
|
||||
RefreshCw,
|
||||
Target,
|
||||
Loader2,
|
||||
Sparkles,
|
||||
Eye,
|
||||
Zap,
|
||||
Crown,
|
||||
Plus,
|
||||
Check,
|
||||
} from 'lucide-react'
|
||||
import Link from 'next/link'
|
||||
import clsx from 'clsx'
|
||||
|
||||
interface Auction {
|
||||
domain: string
|
||||
platform: string
|
||||
platform_url: string
|
||||
current_bid: number
|
||||
currency: string
|
||||
num_bids: number
|
||||
end_time: string
|
||||
time_remaining: string
|
||||
buy_now_price: number | null
|
||||
reserve_met: boolean | null
|
||||
traffic: number | null
|
||||
age_years: number | null
|
||||
tld: string
|
||||
affiliate_url: string
|
||||
}
|
||||
|
||||
interface Opportunity {
|
||||
auction: Auction
|
||||
analysis: {
|
||||
opportunity_score: number
|
||||
urgency?: string
|
||||
competition?: string
|
||||
price_range?: string
|
||||
recommendation: string
|
||||
reasoning?: string
|
||||
}
|
||||
}
|
||||
|
||||
type TabType = 'all' | 'ending' | 'hot' | 'opportunities'
|
||||
type SortField = 'ending' | 'bid_asc' | 'bid_desc' | 'bids' | 'score'
|
||||
type FilterPreset = 'all' | 'no-trash' | 'short' | 'high-value' | 'low-competition'
|
||||
|
||||
const PLATFORMS = [
|
||||
{ value: 'All', label: 'All Sources' },
|
||||
{ value: 'GoDaddy', label: 'GoDaddy' },
|
||||
{ value: 'Sedo', label: 'Sedo' },
|
||||
{ value: 'NameJet', label: 'NameJet' },
|
||||
{ value: 'DropCatch', label: 'DropCatch' },
|
||||
]
|
||||
|
||||
const FILTER_PRESETS: { id: FilterPreset, label: string, icon: typeof Gavel, description: string, proOnly?: boolean }[] = [
|
||||
{ id: 'all', label: 'All', icon: Gavel, description: 'Show all auctions' },
|
||||
{ id: 'no-trash', label: 'No Trash', icon: Sparkles, description: 'Clean domains only (no spam)', proOnly: true },
|
||||
{ id: 'short', label: 'Short', icon: Zap, description: '4-letter domains or less' },
|
||||
{ id: 'high-value', label: 'High Value', icon: Crown, description: 'Premium TLDs with high activity', proOnly: true },
|
||||
{ id: 'low-competition', label: 'Low Competition', icon: Target, description: 'Under 5 bids' },
|
||||
]
|
||||
|
||||
const PREMIUM_TLDS = ['com', 'io', 'ai', 'co', 'net', 'org', 'app', 'dev']
|
||||
|
||||
// Pure functions (no hooks needed)
|
||||
function isCleanDomain(auction: Auction): boolean {
|
||||
const name = auction.domain.split('.')[0]
|
||||
if (name.includes('-')) return false
|
||||
if (name.length > 4 && /\d/.test(name)) return false
|
||||
if (name.length > 12) return false
|
||||
if (!PREMIUM_TLDS.includes(auction.tld)) return false
|
||||
return true
|
||||
}
|
||||
|
||||
function calculateDealScore(auction: Auction): number {
|
||||
let score = 50
|
||||
const name = auction.domain.split('.')[0]
|
||||
if (name.length <= 4) score += 25
|
||||
else if (name.length <= 6) score += 15
|
||||
else if (name.length <= 8) score += 5
|
||||
if (['com', 'io', 'ai'].includes(auction.tld)) score += 15
|
||||
else if (['co', 'net', 'org'].includes(auction.tld)) score += 5
|
||||
if (auction.age_years && auction.age_years > 10) score += 15
|
||||
else if (auction.age_years && auction.age_years > 5) score += 10
|
||||
if (auction.num_bids >= 20) score += 10
|
||||
else if (auction.num_bids >= 10) score += 5
|
||||
if (isCleanDomain(auction)) score += 10
|
||||
return Math.min(score, 100)
|
||||
}
|
||||
|
||||
function getTimeColor(timeRemaining: string): string {
|
||||
if (timeRemaining.includes('m') && !timeRemaining.includes('h') && !timeRemaining.includes('d')) return 'text-red-400'
|
||||
if (timeRemaining.includes('h') && parseInt(timeRemaining) < 2) return 'text-amber-400'
|
||||
return 'text-foreground-muted'
|
||||
}
|
||||
|
||||
const formatCurrency = (value: number) => {
|
||||
return new Intl.NumberFormat('en-US', {
|
||||
style: 'currency',
|
||||
currency: 'USD',
|
||||
minimumFractionDigits: 0,
|
||||
maximumFractionDigits: 0,
|
||||
}).format(value)
|
||||
}
|
||||
|
||||
export default function AuctionsPage() {
|
||||
const { isAuthenticated, subscription } = useStore()
|
||||
|
||||
const [allAuctions, setAllAuctions] = useState<Auction[]>([])
|
||||
const [endingSoon, setEndingSoon] = useState<Auction[]>([])
|
||||
const [hotAuctions, setHotAuctions] = useState<Auction[]>([])
|
||||
const [opportunities, setOpportunities] = useState<Opportunity[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [refreshing, setRefreshing] = useState(false)
|
||||
const [activeTab, setActiveTab] = useState<TabType>('all')
|
||||
const [sortBy, setSortBy] = useState<SortField>('ending')
|
||||
const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('asc')
|
||||
|
||||
// Filters
|
||||
const [searchQuery, setSearchQuery] = useState('')
|
||||
const [selectedPlatform, setSelectedPlatform] = useState('All')
|
||||
const [maxBid, setMaxBid] = useState('')
|
||||
const [filterPreset, setFilterPreset] = useState<FilterPreset>('all')
|
||||
const [trackedDomains, setTrackedDomains] = useState<Set<string>>(new Set())
|
||||
const [trackingInProgress, setTrackingInProgress] = useState<string | null>(null)
|
||||
|
||||
const isPaidUser = subscription?.tier === 'trader' || subscription?.tier === 'tycoon'
|
||||
|
||||
// Data loading
|
||||
const loadData = useCallback(async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const [auctionsData, hotData, endingData] = await Promise.all([
|
||||
api.getAuctions(),
|
||||
api.getHotAuctions(50),
|
||||
api.getEndingSoonAuctions(24, 50),
|
||||
])
|
||||
|
||||
setAllAuctions(auctionsData.auctions || [])
|
||||
setHotAuctions(hotData || [])
|
||||
setEndingSoon(endingData || [])
|
||||
} catch (error) {
|
||||
console.error('Failed to load auction data:', error)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const loadOpportunities = useCallback(async () => {
|
||||
try {
|
||||
const oppData = await api.getAuctionOpportunities()
|
||||
setOpportunities(oppData.opportunities || [])
|
||||
} catch (e) {
|
||||
console.error('Failed to load opportunities:', e)
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
loadData()
|
||||
}, [loadData])
|
||||
|
||||
useEffect(() => {
|
||||
if (isAuthenticated && opportunities.length === 0) {
|
||||
loadOpportunities()
|
||||
}
|
||||
}, [isAuthenticated, opportunities.length, loadOpportunities])
|
||||
|
||||
const handleRefresh = useCallback(async () => {
|
||||
setRefreshing(true)
|
||||
await loadData()
|
||||
if (isAuthenticated) await loadOpportunities()
|
||||
setRefreshing(false)
|
||||
}, [loadData, loadOpportunities, isAuthenticated])
|
||||
|
||||
const handleTrackDomain = useCallback(async (domain: string) => {
|
||||
if (trackedDomains.has(domain)) return
|
||||
|
||||
setTrackingInProgress(domain)
|
||||
try {
|
||||
await api.addDomain(domain)
|
||||
setTrackedDomains(prev => new Set([...Array.from(prev), domain]))
|
||||
} catch (error) {
|
||||
console.error('Failed to track domain:', error)
|
||||
} finally {
|
||||
setTrackingInProgress(null)
|
||||
}
|
||||
}, [trackedDomains])
|
||||
|
||||
const handleSort = useCallback((field: string) => {
|
||||
const f = field as SortField
|
||||
if (sortBy === f) {
|
||||
setSortDirection(d => d === 'asc' ? 'desc' : 'asc')
|
||||
} else {
|
||||
setSortBy(f)
|
||||
setSortDirection('asc')
|
||||
}
|
||||
}, [sortBy])
|
||||
|
||||
// Memoized tabs
|
||||
const tabs = useMemo(() => [
|
||||
{ id: 'all', label: 'All', icon: Gavel, count: allAuctions.length },
|
||||
{ id: 'ending', label: 'Ending Soon', icon: Timer, count: endingSoon.length, color: 'warning' as const },
|
||||
{ id: 'hot', label: 'Hot', icon: Flame, count: hotAuctions.length },
|
||||
{ id: 'opportunities', label: 'Opportunities', icon: Target, count: opportunities.length },
|
||||
], [allAuctions.length, endingSoon.length, hotAuctions.length, opportunities.length])
|
||||
|
||||
// Filter and sort auctions
|
||||
const sortedAuctions = useMemo(() => {
|
||||
// Get base auctions for current tab
|
||||
let auctions: Auction[] = []
|
||||
switch (activeTab) {
|
||||
case 'ending': auctions = [...endingSoon]; break
|
||||
case 'hot': auctions = [...hotAuctions]; break
|
||||
case 'opportunities': auctions = opportunities.map(o => o.auction); break
|
||||
default: auctions = [...allAuctions]
|
||||
}
|
||||
|
||||
// Apply preset filter
|
||||
const baseFilter = filterPreset === 'all' && isPaidUser ? 'no-trash' : filterPreset
|
||||
switch (baseFilter) {
|
||||
case 'no-trash': auctions = auctions.filter(isCleanDomain); break
|
||||
case 'short': auctions = auctions.filter(a => a.domain.split('.')[0].length <= 4); break
|
||||
case 'high-value': auctions = auctions.filter(a =>
|
||||
PREMIUM_TLDS.slice(0, 3).includes(a.tld) && a.num_bids >= 5 && calculateDealScore(a) >= 70
|
||||
); break
|
||||
case 'low-competition': auctions = auctions.filter(a => a.num_bids < 5); break
|
||||
}
|
||||
|
||||
// Apply search
|
||||
if (searchQuery) {
|
||||
const q = searchQuery.toLowerCase()
|
||||
auctions = auctions.filter(a => a.domain.toLowerCase().includes(q))
|
||||
}
|
||||
|
||||
// Apply platform filter
|
||||
if (selectedPlatform !== 'All') {
|
||||
auctions = auctions.filter(a => a.platform === selectedPlatform)
|
||||
}
|
||||
|
||||
// Apply max bid
|
||||
if (maxBid) {
|
||||
const max = parseFloat(maxBid)
|
||||
auctions = auctions.filter(a => a.current_bid <= max)
|
||||
}
|
||||
|
||||
// Sort (skip for opportunities - already sorted by score)
|
||||
if (activeTab !== 'opportunities') {
|
||||
const mult = sortDirection === 'asc' ? 1 : -1
|
||||
auctions.sort((a, b) => {
|
||||
switch (sortBy) {
|
||||
case 'ending': return mult * (new Date(a.end_time).getTime() - new Date(b.end_time).getTime())
|
||||
case 'bid_asc':
|
||||
case 'bid_desc': return mult * (a.current_bid - b.current_bid)
|
||||
case 'bids': return mult * (b.num_bids - a.num_bids)
|
||||
case 'score': return mult * (calculateDealScore(b) - calculateDealScore(a))
|
||||
default: return 0
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return auctions
|
||||
}, [activeTab, allAuctions, endingSoon, hotAuctions, opportunities, filterPreset, isPaidUser, searchQuery, selectedPlatform, maxBid, sortBy, sortDirection])
|
||||
|
||||
// Subtitle
|
||||
const subtitle = useMemo(() => {
|
||||
if (loading) return 'Loading live auctions...'
|
||||
const total = allAuctions.length
|
||||
if (total === 0) return 'No active auctions found'
|
||||
return `${sortedAuctions.length.toLocaleString()} auctions ${sortedAuctions.length < total ? `(filtered from ${total.toLocaleString()})` : 'across 4 platforms'}`
|
||||
}, [loading, allAuctions.length, sortedAuctions.length])
|
||||
|
||||
// Get opportunity data helper
|
||||
const getOpportunityData = useCallback((domain: string) => {
|
||||
if (activeTab !== 'opportunities') return null
|
||||
return opportunities.find(o => o.auction.domain === domain)?.analysis
|
||||
}, [activeTab, opportunities])
|
||||
|
||||
// Table columns - memoized
|
||||
const columns = useMemo(() => [
|
||||
{
|
||||
key: 'domain',
|
||||
header: 'Domain',
|
||||
sortable: true,
|
||||
render: (a: Auction) => (
|
||||
<div>
|
||||
<a
|
||||
href={a.affiliate_url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="font-mono font-medium text-foreground hover:text-accent transition-colors"
|
||||
>
|
||||
{a.domain}
|
||||
</a>
|
||||
<div className="flex items-center gap-2 mt-1 lg:hidden">
|
||||
<PlatformBadge platform={a.platform} />
|
||||
{a.age_years && <span className="text-xs text-foreground-subtle">{a.age_years}y</span>}
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'platform',
|
||||
header: 'Platform',
|
||||
hideOnMobile: true,
|
||||
render: (a: Auction) => (
|
||||
<div className="space-y-1">
|
||||
<PlatformBadge platform={a.platform} />
|
||||
{a.age_years && (
|
||||
<span className="text-xs text-foreground-subtle flex items-center gap-1">
|
||||
<Clock className="w-3 h-3" /> {a.age_years}y
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'bid_asc',
|
||||
header: 'Bid',
|
||||
sortable: true,
|
||||
align: 'right' as const,
|
||||
render: (a: Auction) => (
|
||||
<div>
|
||||
<span className="font-medium text-foreground">{formatCurrency(a.current_bid)}</span>
|
||||
{a.buy_now_price && (
|
||||
<p className="text-xs text-accent">Buy: {formatCurrency(a.buy_now_price)}</p>
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'score',
|
||||
header: 'Deal Score',
|
||||
sortable: true,
|
||||
align: 'center' as const,
|
||||
hideOnMobile: true,
|
||||
render: (a: Auction) => {
|
||||
if (activeTab === 'opportunities') {
|
||||
const oppData = getOpportunityData(a.domain)
|
||||
if (oppData) {
|
||||
return (
|
||||
<span className="inline-flex items-center justify-center w-9 h-9 bg-accent/10 text-accent font-bold rounded-lg">
|
||||
{oppData.opportunity_score}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (!isPaidUser) {
|
||||
return (
|
||||
<Link
|
||||
href="/pricing"
|
||||
className="inline-flex items-center justify-center w-9 h-9 bg-foreground/5 text-foreground-subtle rounded-lg hover:bg-accent/10 hover:text-accent transition-all"
|
||||
title="Upgrade to see Deal Score"
|
||||
>
|
||||
<Crown className="w-4 h-4" />
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
const score = calculateDealScore(a)
|
||||
return (
|
||||
<div className="inline-flex flex-col items-center">
|
||||
<span className={clsx(
|
||||
"inline-flex items-center justify-center w-9 h-9 rounded-lg font-bold text-sm",
|
||||
score >= 75 ? "bg-accent/20 text-accent" :
|
||||
score >= 50 ? "bg-amber-500/20 text-amber-400" :
|
||||
"bg-foreground/10 text-foreground-muted"
|
||||
)}>
|
||||
{score}
|
||||
</span>
|
||||
{score >= 75 && <span className="text-[10px] text-accent mt-0.5 font-medium">Undervalued</span>}
|
||||
</div>
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'bids',
|
||||
header: 'Bids',
|
||||
sortable: true,
|
||||
align: 'right' as const,
|
||||
hideOnMobile: true,
|
||||
render: (a: Auction) => (
|
||||
<span className={clsx(
|
||||
"font-medium flex items-center justify-end gap-1",
|
||||
a.num_bids >= 20 ? "text-accent" : a.num_bids >= 10 ? "text-amber-400" : "text-foreground-muted"
|
||||
)}>
|
||||
{a.num_bids}
|
||||
{a.num_bids >= 20 && <Flame className="w-3 h-3" />}
|
||||
</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'ending',
|
||||
header: 'Time Left',
|
||||
sortable: true,
|
||||
align: 'right' as const,
|
||||
hideOnMobile: true,
|
||||
render: (a: Auction) => (
|
||||
<span className={clsx("font-medium", getTimeColor(a.time_remaining))}>
|
||||
{a.time_remaining}
|
||||
</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'actions',
|
||||
header: '',
|
||||
align: 'right' as const,
|
||||
render: (a: Auction) => (
|
||||
<div className="flex items-center gap-2 justify-end">
|
||||
<button
|
||||
onClick={(e) => { e.preventDefault(); handleTrackDomain(a.domain) }}
|
||||
disabled={trackedDomains.has(a.domain) || trackingInProgress === a.domain}
|
||||
className={clsx(
|
||||
"inline-flex items-center justify-center w-8 h-8 rounded-lg transition-all",
|
||||
trackedDomains.has(a.domain)
|
||||
? "bg-accent/20 text-accent cursor-default"
|
||||
: "bg-foreground/5 text-foreground-subtle hover:bg-accent/10 hover:text-accent"
|
||||
)}
|
||||
title={trackedDomains.has(a.domain) ? 'Already tracked' : 'Add to Watchlist'}
|
||||
>
|
||||
{trackingInProgress === a.domain ? (
|
||||
<Loader2 className="w-4 h-4 animate-spin" />
|
||||
) : trackedDomains.has(a.domain) ? (
|
||||
<Check className="w-4 h-4" />
|
||||
) : (
|
||||
<Plus className="w-4 h-4" />
|
||||
)}
|
||||
</button>
|
||||
<a
|
||||
href={a.affiliate_url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center gap-1.5 px-4 py-2 bg-foreground text-background text-xs font-medium rounded-lg hover:bg-foreground/90 transition-all"
|
||||
>
|
||||
Bid <ExternalLink className="w-3 h-3" />
|
||||
</a>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
], [activeTab, isPaidUser, trackedDomains, trackingInProgress, handleTrackDomain, getOpportunityData])
|
||||
|
||||
return (
|
||||
<TerminalLayout
|
||||
title="Auctions"
|
||||
subtitle={subtitle}
|
||||
actions={
|
||||
<ActionButton onClick={handleRefresh} disabled={refreshing} variant="ghost" icon={refreshing ? Loader2 : RefreshCw}>
|
||||
{refreshing ? '' : 'Refresh'}
|
||||
</ActionButton>
|
||||
}
|
||||
>
|
||||
<PageContainer>
|
||||
{/* Stats */}
|
||||
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
<StatCard title="All Auctions" value={allAuctions.length} icon={Gavel} />
|
||||
<StatCard title="Ending Soon" value={endingSoon.length} icon={Timer} />
|
||||
<StatCard title="Hot Auctions" value={hotAuctions.length} subtitle="20+ bids" icon={Flame} />
|
||||
<StatCard title="Opportunities" value={opportunities.length} icon={Target} />
|
||||
</div>
|
||||
|
||||
{/* Tabs */}
|
||||
<TabBar tabs={tabs} activeTab={activeTab} onChange={(id) => setActiveTab(id as TabType)} />
|
||||
|
||||
{/* Smart Filter Presets */}
|
||||
<div className="flex flex-wrap gap-2 p-1.5 bg-background-secondary/30 border border-border/30 rounded-xl">
|
||||
{FILTER_PRESETS.map((preset) => {
|
||||
const isDisabled = preset.proOnly && !isPaidUser
|
||||
const isActive = filterPreset === preset.id
|
||||
const Icon = preset.icon
|
||||
return (
|
||||
<button
|
||||
key={preset.id}
|
||||
onClick={() => !isDisabled && setFilterPreset(preset.id)}
|
||||
disabled={isDisabled}
|
||||
title={isDisabled ? 'Upgrade to Trader to use this filter' : preset.description}
|
||||
className={clsx(
|
||||
"flex items-center gap-2 px-3 py-2 text-sm font-medium rounded-lg transition-all",
|
||||
isActive
|
||||
? "bg-accent text-background shadow-md"
|
||||
: isDisabled
|
||||
? "text-foreground-subtle opacity-50 cursor-not-allowed"
|
||||
: "text-foreground-muted hover:text-foreground hover:bg-foreground/5"
|
||||
)}
|
||||
>
|
||||
<Icon className="w-4 h-4" />
|
||||
<span className="hidden sm:inline">{preset.label}</span>
|
||||
{preset.proOnly && !isPaidUser && <Crown className="w-3 h-3 text-amber-400" />}
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Tier notification for Scout users */}
|
||||
{!isPaidUser && (
|
||||
<div className="p-4 bg-amber-500/10 border border-amber-500/20 rounded-xl flex items-center gap-3">
|
||||
<div className="w-10 h-10 bg-amber-500/20 rounded-xl flex items-center justify-center shrink-0">
|
||||
<Eye className="w-5 h-5 text-amber-400" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<p className="text-sm font-medium text-foreground">You're seeing the raw auction feed</p>
|
||||
<p className="text-xs text-foreground-muted">
|
||||
Upgrade to Trader for spam-free listings, Deal Scores, and Smart Filters.
|
||||
</p>
|
||||
</div>
|
||||
<Link
|
||||
href="/pricing"
|
||||
className="shrink-0 px-4 py-2 bg-accent text-background text-sm font-medium rounded-lg hover:bg-accent-hover transition-all"
|
||||
>
|
||||
Upgrade
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Filters */}
|
||||
<FilterBar>
|
||||
<SearchInput
|
||||
value={searchQuery}
|
||||
onChange={setSearchQuery}
|
||||
placeholder="Search domains..."
|
||||
className="flex-1 min-w-[200px] max-w-md"
|
||||
/>
|
||||
<SelectDropdown value={selectedPlatform} onChange={setSelectedPlatform} options={PLATFORMS} />
|
||||
<div className="relative">
|
||||
<DollarSign className="absolute left-3.5 top-1/2 -translate-y-1/2 w-4 h-4 text-foreground-muted" />
|
||||
<input
|
||||
type="number"
|
||||
placeholder="Max bid"
|
||||
value={maxBid}
|
||||
onChange={(e) => setMaxBid(e.target.value)}
|
||||
className="w-28 h-10 pl-9 pr-3 bg-background-secondary/50 border border-border/40 rounded-xl
|
||||
text-sm text-foreground placeholder:text-foreground-subtle
|
||||
focus:outline-none focus:border-accent/50 transition-all"
|
||||
/>
|
||||
</div>
|
||||
</FilterBar>
|
||||
|
||||
{/* Table */}
|
||||
<PremiumTable
|
||||
data={sortedAuctions}
|
||||
keyExtractor={(a) => `${a.domain}-${a.platform}`}
|
||||
loading={loading}
|
||||
sortBy={sortBy}
|
||||
sortDirection={sortDirection}
|
||||
onSort={handleSort}
|
||||
emptyIcon={<Gavel className="w-12 h-12 text-foreground-subtle" />}
|
||||
emptyTitle={searchQuery ? `No auctions matching "${searchQuery}"` : "No auctions found"}
|
||||
emptyDescription="Try adjusting your filters or check back later"
|
||||
columns={columns}
|
||||
/>
|
||||
</PageContainer>
|
||||
</TerminalLayout>
|
||||
)
|
||||
}
|
||||
@ -118,7 +118,7 @@ export default function CommandMarketplacePage() {
|
||||
title="Marketplace"
|
||||
subtitle={`${listings.length} premium domains for sale`}
|
||||
actions={
|
||||
<Link href="/terminal/listings">
|
||||
<Link href="/terminal/listing">
|
||||
<ActionButton icon={Tag} variant="secondary">My Listings</ActionButton>
|
||||
</Link>
|
||||
}
|
||||
@ -233,7 +233,7 @@ export default function CommandMarketplacePage() {
|
||||
: 'No domains are currently listed for sale'}
|
||||
</p>
|
||||
<Link
|
||||
href="/terminal/listings"
|
||||
href="/terminal/listing"
|
||||
className="inline-flex items-center gap-2 px-6 py-3 bg-accent text-background font-medium rounded-xl hover:bg-accent-hover transition-all"
|
||||
>
|
||||
<Tag className="w-5 h-5" />
|
||||
|
||||
@ -7,7 +7,7 @@ export default function CommandPage() {
|
||||
const router = useRouter()
|
||||
|
||||
useEffect(() => {
|
||||
router.replace('/terminal/dashboard')
|
||||
router.replace('/terminal/radar')
|
||||
}, [router])
|
||||
|
||||
return (
|
||||
|
||||
@ -464,7 +464,7 @@ export default function PortfolioPage() {
|
||||
</button>
|
||||
<div className="my-1 border-t border-border/30" />
|
||||
<Link
|
||||
href={`/terminal/listings?domain=${encodeURIComponent(domain.domain)}`}
|
||||
href={`/terminal/listing?domain=${encodeURIComponent(domain.domain)}`}
|
||||
onClick={() => setOpenMenuId(null)}
|
||||
className="w-full flex items-center gap-3 px-4 py-2.5 text-sm text-accent hover:bg-accent/5 transition-colors"
|
||||
>
|
||||
|
||||
@ -64,7 +64,7 @@ export default function DashboardPage() {
|
||||
useEffect(() => {
|
||||
if (searchParams.get('upgraded') === 'true') {
|
||||
showToast('Welcome to your upgraded plan! 🎉', 'success')
|
||||
window.history.replaceState({}, '', '/terminal/dashboard')
|
||||
window.history.replaceState({}, '', '/terminal/radar')
|
||||
}
|
||||
}, [searchParams])
|
||||
|
||||
@ -349,7 +349,7 @@ export default function DashboardPage() {
|
||||
icon={TrendingUp}
|
||||
compact
|
||||
action={
|
||||
<Link href="/terminal/pricing" className="text-sm text-accent hover:text-accent/80 transition-colors">
|
||||
<Link href="/terminal/intel" className="text-sm text-accent hover:text-accent/80 transition-colors">
|
||||
View all →
|
||||
</Link>
|
||||
}
|
||||
@ -360,7 +360,7 @@ export default function SettingsPage() {
|
||||
<div className="py-12 text-center border border-dashed border-border/50 rounded-xl bg-foreground/5">
|
||||
<Bell className="w-10 h-10 text-foreground-subtle mx-auto mb-4" />
|
||||
<p className="text-foreground-muted mb-3">No price alerts set</p>
|
||||
<Link href="/terminal/pricing" className="text-accent hover:text-accent/80 text-sm font-medium">
|
||||
<Link href="/terminal/intel" className="text-accent hover:text-accent/80 text-sm font-medium">
|
||||
Browse TLD prices →
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
@ -188,7 +188,7 @@ export default function WelcomePage() {
|
||||
{/* Go to Dashboard */}
|
||||
<div className="text-center">
|
||||
<Link
|
||||
href="/terminal/dashboard"
|
||||
href="/terminal/radar"
|
||||
className="inline-flex items-center gap-2 px-8 py-4 bg-accent text-background font-medium rounded-xl
|
||||
hover:bg-accent-hover transition-all shadow-lg shadow-accent/20"
|
||||
>
|
||||
|
||||
@ -91,7 +91,7 @@ export function AdminLayout({
|
||||
<h1 className="text-xl font-semibold text-foreground mb-2">Access Denied</h1>
|
||||
<p className="text-foreground-muted mb-4">Admin privileges required</p>
|
||||
<button
|
||||
onClick={() => router.push('/terminal/dashboard')}
|
||||
onClick={() => router.push('/terminal/radar')}
|
||||
className="px-4 py-2 bg-accent text-background rounded-lg font-medium"
|
||||
>
|
||||
Go to Dashboard
|
||||
@ -286,7 +286,7 @@ function AdminSidebar({
|
||||
<div className="border-t border-border/30 py-4 px-3 space-y-2">
|
||||
{/* Back to User Dashboard */}
|
||||
<Link
|
||||
href="/terminal/dashboard"
|
||||
href="/terminal/radar"
|
||||
className={clsx(
|
||||
"flex items-center gap-3 px-3 py-3 rounded-xl transition-all duration-300",
|
||||
"text-accent hover:bg-accent/10 border border-transparent hover:border-accent/20"
|
||||
|
||||
@ -152,7 +152,7 @@ export function DomainChecker() {
|
||||
Grab it now or track it in your watchlist.
|
||||
</p>
|
||||
<Link
|
||||
href={isAuthenticated ? '/terminal/dashboard' : '/register'}
|
||||
href={isAuthenticated ? '/terminal/radar' : '/register'}
|
||||
className="shrink-0 flex items-center justify-center sm:justify-start gap-2 px-5 py-2.5
|
||||
bg-accent text-background text-ui font-medium rounded-lg
|
||||
hover:bg-accent-hover transition-all duration-300"
|
||||
@ -268,7 +268,7 @@ export function DomainChecker() {
|
||||
<span className="text-left">We'll alert you the moment it drops.</span>
|
||||
</div>
|
||||
<Link
|
||||
href={isAuthenticated ? '/terminal/dashboard' : '/register'}
|
||||
href={isAuthenticated ? '/terminal/radar' : '/register'}
|
||||
className="shrink-0 flex items-center justify-center sm:justify-start gap-2 px-4 py-2.5
|
||||
bg-background-tertiary text-foreground text-ui font-medium rounded-lg
|
||||
border border-border hover:border-border-hover transition-all duration-300"
|
||||
|
||||
@ -79,7 +79,7 @@ export function Footer() {
|
||||
</li>
|
||||
{isAuthenticated ? (
|
||||
<li>
|
||||
<Link href="/terminal/dashboard" className="text-body-sm text-accent hover:text-accent-hover transition-colors">
|
||||
<Link href="/terminal/radar" className="text-body-sm text-accent hover:text-accent-hover transition-colors">
|
||||
Command Center
|
||||
</Link>
|
||||
</li>
|
||||
|
||||
@ -101,7 +101,7 @@ export function Header() {
|
||||
<>
|
||||
{/* Go to Command Center */}
|
||||
<Link
|
||||
href="/terminal/dashboard"
|
||||
href="/terminal/radar"
|
||||
className="flex items-center gap-2 h-9 px-4 text-[0.8125rem] bg-accent text-background
|
||||
rounded-lg font-medium hover:bg-accent-hover transition-all duration-200"
|
||||
>
|
||||
@ -164,7 +164,7 @@ export function Header() {
|
||||
{isAuthenticated ? (
|
||||
<>
|
||||
<Link
|
||||
href="/terminal/dashboard"
|
||||
href="/terminal/radar"
|
||||
className="flex items-center gap-3 px-4 py-3 text-body-sm text-center bg-accent text-background
|
||||
rounded-xl font-medium hover:bg-accent-hover transition-all duration-200"
|
||||
>
|
||||
|
||||
@ -77,20 +77,14 @@ export function Sidebar({ collapsed: controlledCollapsed, onCollapsedChange }: S
|
||||
// SECTION 1: Discover - External market data
|
||||
const discoverItems = [
|
||||
{
|
||||
href: '/terminal/auctions',
|
||||
label: 'Auctions',
|
||||
href: '/terminal/market',
|
||||
label: 'MARKET',
|
||||
icon: Gavel,
|
||||
badge: null,
|
||||
},
|
||||
{
|
||||
href: '/terminal/marketplace',
|
||||
label: 'Marketplace',
|
||||
icon: Tag,
|
||||
badge: null,
|
||||
},
|
||||
{
|
||||
href: '/terminal/pricing',
|
||||
label: 'TLD Pricing',
|
||||
href: '/terminal/intel',
|
||||
label: 'INTEL',
|
||||
icon: TrendingUp,
|
||||
badge: null,
|
||||
},
|
||||
@ -105,42 +99,23 @@ export function Sidebar({ collapsed: controlledCollapsed, onCollapsedChange }: S
|
||||
tycoonOnly?: boolean
|
||||
}> = [
|
||||
{
|
||||
href: '/terminal/dashboard',
|
||||
label: 'Dashboard',
|
||||
href: '/terminal/radar',
|
||||
label: 'RADAR',
|
||||
icon: LayoutDashboard,
|
||||
badge: null,
|
||||
},
|
||||
{
|
||||
href: '/terminal/watchlist',
|
||||
label: 'Watchlist',
|
||||
label: 'WATCHLIST',
|
||||
icon: Eye,
|
||||
badge: availableCount || null,
|
||||
},
|
||||
{
|
||||
href: '/terminal/portfolio',
|
||||
label: 'Portfolio',
|
||||
icon: Briefcase,
|
||||
badge: null,
|
||||
},
|
||||
{
|
||||
href: '/terminal/listings',
|
||||
label: 'My Listings',
|
||||
href: '/terminal/listing',
|
||||
label: 'LISTING',
|
||||
icon: Tag,
|
||||
badge: null,
|
||||
},
|
||||
{
|
||||
href: '/terminal/alerts',
|
||||
label: 'Sniper Alerts',
|
||||
icon: Target,
|
||||
badge: null,
|
||||
},
|
||||
{
|
||||
href: '/terminal/seo',
|
||||
label: 'SEO Juice',
|
||||
icon: Link2,
|
||||
badge: null,
|
||||
tycoonOnly: true,
|
||||
},
|
||||
]
|
||||
|
||||
const bottomItems = [
|
||||
@ -148,7 +123,7 @@ export function Sidebar({ collapsed: controlledCollapsed, onCollapsedChange }: S
|
||||
]
|
||||
|
||||
const isActive = (href: string) => {
|
||||
if (href === '/terminal/dashboard') return pathname === '/terminal/dashboard' || pathname === '/terminal'
|
||||
if (href === '/terminal/radar') return pathname === '/terminal/radar' || pathname === '/terminal' || pathname === '/terminal/dashboard'
|
||||
return pathname.startsWith(href)
|
||||
}
|
||||
|
||||
|
||||
@ -239,11 +239,11 @@ export function useUserShortcuts() {
|
||||
useEffect(() => {
|
||||
const userShortcuts: Shortcut[] = [
|
||||
// Navigation
|
||||
{ key: 'g', label: 'Go to Dashboard', description: 'Navigate to dashboard', action: () => router.push('/terminal/dashboard'), category: 'navigation' },
|
||||
{ key: 'g', label: 'Go to Dashboard', description: 'Navigate to dashboard', action: () => router.push('/terminal/radar'), category: 'navigation' },
|
||||
{ key: 'w', label: 'Go to Watchlist', description: 'Navigate to watchlist', action: () => router.push('/terminal/watchlist'), category: 'navigation' },
|
||||
{ key: 'p', label: 'Go to Portfolio', description: 'Navigate to portfolio', action: () => router.push('/terminal/portfolio'), category: 'navigation' },
|
||||
{ key: 'a', label: 'Go to Auctions', description: 'Navigate to auctions', action: () => router.push('/terminal/auctions'), category: 'navigation' },
|
||||
{ key: 't', label: 'Go to TLD Pricing', description: 'Navigate to TLD pricing', action: () => router.push('/terminal/pricing'), category: 'navigation' },
|
||||
{ key: 't', label: 'Go to TLD Pricing', description: 'Navigate to TLD pricing', action: () => router.push('/terminal/intel'), category: 'navigation' },
|
||||
{ key: 's', label: 'Go to Settings', description: 'Navigate to settings', action: () => router.push('/terminal/settings'), category: 'navigation' },
|
||||
// Actions
|
||||
{ key: 'n', label: 'Add Domain', description: 'Quick add a new domain', action: () => document.querySelector<HTMLInputElement>('input[placeholder*="domain"]')?.focus(), category: 'actions' },
|
||||
@ -281,7 +281,7 @@ export function useAdminShortcuts() {
|
||||
{ key: 'e', label: 'Export', description: 'Export current data', action: () => {}, category: 'actions' },
|
||||
// Global
|
||||
{ key: '?', label: 'Show Shortcuts', description: 'Display this help', action: () => setShowHelp(true), category: 'global' },
|
||||
{ key: 'd', label: 'Back to Dashboard', description: 'Return to user dashboard', action: () => router.push('/terminal/dashboard'), category: 'global' },
|
||||
{ key: 'd', label: 'Back to Dashboard', description: 'Return to user dashboard', action: () => router.push('/terminal/radar'), category: 'global' },
|
||||
]
|
||||
|
||||
adminShortcuts.forEach(registerShortcut)
|
||||
|
||||
Reference in New Issue
Block a user