Premium overhaul based on review feedback
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
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
- Fix Command Center loading on mobile (add mobile sidebar menu) - Rename 'Market' to 'Auctions' in navigation (clearer naming) - Add Vanity Filter for public auctions (hide spam domains) - Premium TLDs only for public (.com, .io, .ai, etc.) - Max 15 chars, max 1 hyphen, max 2 digits - No random consonant strings - Improve pricing page differentiation - Highlight 'Smart spam filter' for Trader - Show 'Curated list' vs 'Raw feed' - Add sublabels for key features - Add background effects to Command Center - Improve responsive design
This commit is contained in:
@ -224,6 +224,15 @@ async def search_auctions(
|
|||||||
# Build query
|
# Build query
|
||||||
query = select(DomainAuction).where(DomainAuction.is_active == True)
|
query = select(DomainAuction).where(DomainAuction.is_active == True)
|
||||||
|
|
||||||
|
# VANITY FILTER: For public (non-logged-in) users, only show premium-looking domains
|
||||||
|
# This ensures the first impression is high-quality, not spam domains
|
||||||
|
if current_user is None:
|
||||||
|
# Premium TLDs only (no .cc, .website, .info spam clusters)
|
||||||
|
premium_tlds = ['com', 'io', 'ai', 'co', 'de', 'ch', 'net', 'org', 'app', 'dev', 'xyz']
|
||||||
|
query = query.where(DomainAuction.tld.in_(premium_tlds))
|
||||||
|
# No domains with more than 15 characters (excluding TLD)
|
||||||
|
# Note: We filter further in Python for complex rules
|
||||||
|
|
||||||
if keyword:
|
if keyword:
|
||||||
query = query.where(DomainAuction.domain.ilike(f"%{keyword}%"))
|
query = query.where(DomainAuction.domain.ilike(f"%{keyword}%"))
|
||||||
|
|
||||||
@ -266,6 +275,49 @@ async def search_auctions(
|
|||||||
result = await db.execute(query)
|
result = await db.execute(query)
|
||||||
auctions = list(result.scalars().all())
|
auctions = list(result.scalars().all())
|
||||||
|
|
||||||
|
# VANITY FILTER PART 2: Apply Python-side filtering for public users
|
||||||
|
# This ensures only premium-looking domains are shown to non-logged-in users
|
||||||
|
if current_user is None:
|
||||||
|
def is_premium_domain(domain_name: str) -> bool:
|
||||||
|
"""Check if a domain looks premium/professional"""
|
||||||
|
# Extract just the domain part (without TLD)
|
||||||
|
parts = domain_name.rsplit('.', 1)
|
||||||
|
name = parts[0] if parts else domain_name
|
||||||
|
|
||||||
|
# Rule 1: No more than 15 characters
|
||||||
|
if len(name) > 15:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Rule 2: No more than 1 hyphen
|
||||||
|
if name.count('-') > 1:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Rule 3: No more than 2 digits total
|
||||||
|
digit_count = sum(1 for c in name if c.isdigit())
|
||||||
|
if digit_count > 2:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Rule 4: Must be at least 3 characters
|
||||||
|
if len(name) < 3:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Rule 5: No random-looking strings (too many consonants in a row)
|
||||||
|
consonants = 'bcdfghjklmnpqrstvwxyz'
|
||||||
|
consonant_streak = 0
|
||||||
|
max_streak = 0
|
||||||
|
for c in name.lower():
|
||||||
|
if c in consonants:
|
||||||
|
consonant_streak += 1
|
||||||
|
max_streak = max(max_streak, consonant_streak)
|
||||||
|
else:
|
||||||
|
consonant_streak = 0
|
||||||
|
if max_streak > 4:
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
auctions = [a for a in auctions if is_premium_domain(a.domain)]
|
||||||
|
|
||||||
# Convert to response with valuations
|
# Convert to response with valuations
|
||||||
listings = []
|
listings = []
|
||||||
for auction in auctions:
|
for auction in auctions:
|
||||||
|
|||||||
0
frontend/src/app/intelligence/page.tsx
Normal file → Executable file
0
frontend/src/app/intelligence/page.tsx
Normal file → Executable file
@ -22,9 +22,11 @@ const tiers = [
|
|||||||
{ text: '5 domains to track', highlight: false, available: true },
|
{ text: '5 domains to track', highlight: false, available: true },
|
||||||
{ text: 'Daily availability scans', highlight: false, available: true },
|
{ text: 'Daily availability scans', highlight: false, available: true },
|
||||||
{ text: 'Email alerts', highlight: false, available: true },
|
{ text: 'Email alerts', highlight: false, available: true },
|
||||||
{ text: 'TLD price overview', highlight: false, available: true },
|
{ text: 'Raw auction feed', highlight: false, available: true, sublabel: 'Unfiltered' },
|
||||||
|
{ text: 'Curated auction list', highlight: false, available: false },
|
||||||
|
{ text: 'Deal scores & valuations', highlight: false, available: false },
|
||||||
],
|
],
|
||||||
cta: 'Hunt Free',
|
cta: 'Start Free',
|
||||||
highlighted: false,
|
highlighted: false,
|
||||||
badge: null,
|
badge: null,
|
||||||
isPaid: false,
|
isPaid: false,
|
||||||
@ -35,20 +37,19 @@ const tiers = [
|
|||||||
icon: TrendingUp,
|
icon: TrendingUp,
|
||||||
price: '9',
|
price: '9',
|
||||||
period: '/mo',
|
period: '/mo',
|
||||||
description: 'Hunt with precision. Daily intel.',
|
description: 'The smart investor\'s choice.',
|
||||||
features: [
|
features: [
|
||||||
{ text: '50 domains to track', highlight: true, available: true },
|
{ text: '50 domains to track', highlight: true, available: true },
|
||||||
{ text: 'Hourly scans', highlight: true, available: true },
|
{ text: 'Hourly scans', highlight: true, available: true, sublabel: '24x faster' },
|
||||||
{ text: 'Email alerts', highlight: false, available: true },
|
{ text: 'Smart spam filter', highlight: true, available: true, sublabel: 'Curated list' },
|
||||||
{ text: 'Full TLD market data', highlight: false, available: true },
|
{ text: 'Deal scores & valuations', highlight: true, available: true },
|
||||||
{ text: 'Domain valuation', highlight: true, available: true },
|
{ text: 'Portfolio tracking (25)', highlight: true, available: true },
|
||||||
{ text: 'Portfolio (25 domains)', highlight: true, available: true },
|
|
||||||
{ text: '90-day price history', highlight: false, available: true },
|
{ text: '90-day price history', highlight: false, available: true },
|
||||||
{ text: 'Expiry tracking', highlight: true, available: true },
|
{ text: 'Expiry date tracking', highlight: true, available: true },
|
||||||
],
|
],
|
||||||
cta: 'Start Trading',
|
cta: 'Upgrade to Trader',
|
||||||
highlighted: true,
|
highlighted: true,
|
||||||
badge: 'Most Popular',
|
badge: 'Best Value',
|
||||||
isPaid: true,
|
isPaid: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -57,14 +58,15 @@ const tiers = [
|
|||||||
icon: Crown,
|
icon: Crown,
|
||||||
price: '29',
|
price: '29',
|
||||||
period: '/mo',
|
period: '/mo',
|
||||||
description: 'Dominate the market. No limits.',
|
description: 'For serious domain investors.',
|
||||||
features: [
|
features: [
|
||||||
{ text: '500 domains to track', highlight: true, available: true },
|
{ text: '500 domains to track', highlight: true, available: true },
|
||||||
{ text: 'Real-time scans (10 min)', highlight: true, available: true },
|
{ text: 'Real-time scans', highlight: true, available: true, sublabel: 'Every 10 min' },
|
||||||
{ text: 'Priority email alerts', highlight: false, available: true },
|
{ text: 'Priority alerts', highlight: true, available: true },
|
||||||
{ text: 'Unlimited portfolio', highlight: true, available: true },
|
{ text: 'Unlimited portfolio', highlight: true, available: true },
|
||||||
{ text: 'Full price history', highlight: true, available: true },
|
{ text: 'Full price history', highlight: true, available: true },
|
||||||
{ text: 'Advanced valuation', highlight: true, available: true },
|
{ text: 'Advanced valuation', highlight: true, available: true },
|
||||||
|
{ text: 'API access', highlight: true, available: true, sublabel: 'Coming soon' },
|
||||||
],
|
],
|
||||||
cta: 'Go Tycoon',
|
cta: 'Go Tycoon',
|
||||||
highlighted: false,
|
highlighted: false,
|
||||||
@ -76,8 +78,10 @@ const tiers = [
|
|||||||
const comparisonFeatures = [
|
const comparisonFeatures = [
|
||||||
{ name: 'Watchlist Domains', scout: '5', trader: '50', tycoon: '500' },
|
{ name: 'Watchlist Domains', scout: '5', trader: '50', tycoon: '500' },
|
||||||
{ name: 'Check Frequency', scout: 'Daily', trader: 'Hourly', tycoon: '10 min' },
|
{ name: 'Check Frequency', scout: 'Daily', trader: 'Hourly', tycoon: '10 min' },
|
||||||
|
{ name: 'Auction Feed', scout: 'Raw (unfiltered)', trader: 'Curated (spam-free)', tycoon: 'Curated (spam-free)' },
|
||||||
|
{ name: 'Deal Scores', scout: '—', trader: 'check', tycoon: 'check' },
|
||||||
{ name: 'Portfolio Domains', scout: '—', trader: '25', tycoon: 'Unlimited' },
|
{ name: 'Portfolio Domains', scout: '—', trader: '25', tycoon: 'Unlimited' },
|
||||||
{ name: 'Domain Valuation', scout: '—', trader: 'check', tycoon: 'check' },
|
{ name: 'Domain Valuation', scout: '—', trader: 'check', tycoon: 'Advanced' },
|
||||||
{ name: 'Price History', scout: '—', trader: '90 days', tycoon: 'Unlimited' },
|
{ name: 'Price History', scout: '—', trader: '90 days', tycoon: 'Unlimited' },
|
||||||
{ name: 'Expiry Tracking', scout: '—', trader: 'check', tycoon: 'check' },
|
{ name: 'Expiry Tracking', scout: '—', trader: 'check', tycoon: 'check' },
|
||||||
]
|
]
|
||||||
@ -230,9 +234,24 @@ export default function PricingPage() {
|
|||||||
<ul className="space-y-3 mb-8 flex-1">
|
<ul className="space-y-3 mb-8 flex-1">
|
||||||
{tier.features.map((feature) => (
|
{tier.features.map((feature) => (
|
||||||
<li key={feature.text} className="flex items-start gap-3">
|
<li key={feature.text} className="flex items-start gap-3">
|
||||||
<Check className="w-4 h-4 mt-0.5 shrink-0 text-accent" strokeWidth={2.5} />
|
{feature.available ? (
|
||||||
<span className="text-body-sm text-foreground">
|
<Check className={clsx(
|
||||||
|
"w-4 h-4 mt-0.5 shrink-0",
|
||||||
|
feature.highlight ? "text-accent" : "text-foreground-muted"
|
||||||
|
)} strokeWidth={2.5} />
|
||||||
|
) : (
|
||||||
|
<X className="w-4 h-4 mt-0.5 shrink-0 text-foreground-subtle" strokeWidth={2} />
|
||||||
|
)}
|
||||||
|
<span className={clsx(
|
||||||
|
"text-body-sm",
|
||||||
|
feature.available ? "text-foreground" : "text-foreground-subtle line-through"
|
||||||
|
)}>
|
||||||
{feature.text}
|
{feature.text}
|
||||||
|
{feature.sublabel && (
|
||||||
|
<span className="ml-1.5 text-xs text-accent font-medium">
|
||||||
|
{feature.sublabel}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</span>
|
</span>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
|
|||||||
79
frontend/src/components/CommandCenterLayout.tsx
Normal file → Executable file
79
frontend/src/components/CommandCenterLayout.tsx
Normal file → Executable file
@ -27,14 +27,22 @@ export function CommandCenterLayout({
|
|||||||
const [notificationsOpen, setNotificationsOpen] = useState(false)
|
const [notificationsOpen, setNotificationsOpen] = useState(false)
|
||||||
const [searchOpen, setSearchOpen] = useState(false)
|
const [searchOpen, setSearchOpen] = useState(false)
|
||||||
const [searchQuery, setSearchQuery] = useState('')
|
const [searchQuery, setSearchQuery] = useState('')
|
||||||
|
const [mounted, setMounted] = useState(false)
|
||||||
|
|
||||||
|
// Ensure component is mounted before rendering
|
||||||
|
useEffect(() => {
|
||||||
|
setMounted(true)
|
||||||
|
}, [])
|
||||||
|
|
||||||
// Load sidebar state from localStorage
|
// Load sidebar state from localStorage
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const saved = localStorage.getItem('sidebar-collapsed')
|
if (mounted) {
|
||||||
if (saved) {
|
const saved = localStorage.getItem('sidebar-collapsed')
|
||||||
setSidebarCollapsed(saved === 'true')
|
if (saved) {
|
||||||
|
setSidebarCollapsed(saved === 'true')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, [])
|
}, [mounted])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
checkAuth()
|
checkAuth()
|
||||||
@ -50,10 +58,14 @@ export function CommandCenterLayout({
|
|||||||
const availableDomains = domains?.filter(d => d.is_available) || []
|
const availableDomains = domains?.filter(d => d.is_available) || []
|
||||||
const hasNotifications = availableDomains.length > 0
|
const hasNotifications = availableDomains.length > 0
|
||||||
|
|
||||||
if (isLoading) {
|
// Show loading only if we're still checking auth
|
||||||
|
if (!mounted || isLoading) {
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen flex items-center justify-center bg-background">
|
<div className="min-h-screen flex items-center justify-center bg-background">
|
||||||
<div className="w-6 h-6 border-2 border-accent border-t-transparent rounded-full animate-spin" />
|
<div className="flex flex-col items-center gap-4">
|
||||||
|
<div className="w-8 h-8 border-2 border-accent border-t-transparent rounded-full animate-spin" />
|
||||||
|
<p className="text-sm text-foreground-muted">Loading Command Center...</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -64,6 +76,12 @@ export function CommandCenterLayout({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-background">
|
<div className="min-h-screen bg-background">
|
||||||
|
{/* Background Effects */}
|
||||||
|
<div className="fixed inset-0 pointer-events-none">
|
||||||
|
<div className="absolute top-[-20%] left-1/2 -translate-x-1/2 w-[1200px] h-[800px] bg-accent/[0.02] rounded-full blur-[120px]" />
|
||||||
|
<div className="absolute bottom-[-10%] right-[-10%] w-[600px] h-[600px] bg-accent/[0.015] rounded-full blur-[100px]" />
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Sidebar */}
|
{/* Sidebar */}
|
||||||
<Sidebar
|
<Sidebar
|
||||||
collapsed={sidebarCollapsed}
|
collapsed={sidebarCollapsed}
|
||||||
@ -73,38 +91,51 @@ export function CommandCenterLayout({
|
|||||||
{/* Main Content Area */}
|
{/* Main Content Area */}
|
||||||
<div
|
<div
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"min-h-screen transition-all duration-300",
|
"relative min-h-screen transition-all duration-300",
|
||||||
sidebarCollapsed ? "ml-[72px]" : "ml-[240px]"
|
// Desktop: adjust for sidebar
|
||||||
|
"lg:ml-[240px]",
|
||||||
|
sidebarCollapsed && "lg:ml-[72px]",
|
||||||
|
// Mobile: no margin, just padding for menu button
|
||||||
|
"ml-0 pt-16 lg:pt-0"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{/* Top Bar */}
|
{/* Top Bar */}
|
||||||
<header className="sticky top-0 z-30 h-16 sm:h-20 bg-background/80 backdrop-blur-xl border-b border-border/50">
|
<header className="sticky top-0 z-30 h-16 sm:h-20 bg-background/80 backdrop-blur-xl border-b border-border/50">
|
||||||
<div className="h-full px-6 flex items-center justify-between">
|
<div className="h-full px-4 sm:px-6 flex items-center justify-between">
|
||||||
{/* Left: Title */}
|
{/* Left: Title */}
|
||||||
<div>
|
<div className="ml-10 lg:ml-0">
|
||||||
{title && (
|
{title && (
|
||||||
<h1 className="text-xl sm:text-2xl font-display text-foreground">{title}</h1>
|
<h1 className="text-lg sm:text-xl lg:text-2xl font-display text-foreground">{title}</h1>
|
||||||
)}
|
)}
|
||||||
{subtitle && (
|
{subtitle && (
|
||||||
<p className="text-sm text-foreground-muted mt-0.5">{subtitle}</p>
|
<p className="text-xs sm:text-sm text-foreground-muted mt-0.5 hidden sm:block">{subtitle}</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Right: Actions */}
|
{/* Right: Actions */}
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-2 sm:gap-3">
|
||||||
{/* Quick Search */}
|
{/* Quick Search */}
|
||||||
<button
|
<button
|
||||||
onClick={() => setSearchOpen(true)}
|
onClick={() => setSearchOpen(true)}
|
||||||
className="hidden sm:flex items-center gap-2 h-9 px-4 bg-foreground/5 hover:bg-foreground/10
|
className="hidden md:flex items-center gap-2 h-9 px-4 bg-foreground/5 hover:bg-foreground/10
|
||||||
border border-border/50 rounded-lg text-sm text-foreground-muted
|
border border-border/50 rounded-lg text-sm text-foreground-muted
|
||||||
hover:text-foreground transition-all duration-200"
|
hover:text-foreground transition-all duration-200"
|
||||||
>
|
>
|
||||||
<Search className="w-4 h-4" />
|
<Search className="w-4 h-4" />
|
||||||
<span>Search domains...</span>
|
<span>Search...</span>
|
||||||
<kbd className="hidden md:inline-flex items-center h-5 px-1.5 bg-background border border-border
|
<kbd className="hidden lg:inline-flex items-center h-5 px-1.5 bg-background border border-border
|
||||||
rounded text-[10px] text-foreground-subtle font-mono">⌘K</kbd>
|
rounded text-[10px] text-foreground-subtle font-mono">⌘K</kbd>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
{/* Mobile Search */}
|
||||||
|
<button
|
||||||
|
onClick={() => setSearchOpen(true)}
|
||||||
|
className="md:hidden flex items-center justify-center w-9 h-9 text-foreground-muted
|
||||||
|
hover:text-foreground hover:bg-foreground/5 rounded-lg transition-colors"
|
||||||
|
>
|
||||||
|
<Search className="w-4.5 h-4.5" />
|
||||||
|
</button>
|
||||||
|
|
||||||
{/* Notifications */}
|
{/* Notifications */}
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<button
|
<button
|
||||||
@ -178,7 +209,7 @@ export function CommandCenterLayout({
|
|||||||
</header>
|
</header>
|
||||||
|
|
||||||
{/* Page Content */}
|
{/* Page Content */}
|
||||||
<main className="p-6 lg:p-8">
|
<main className="relative p-4 sm:p-6 lg:p-8">
|
||||||
{children}
|
{children}
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
@ -186,7 +217,7 @@ export function CommandCenterLayout({
|
|||||||
{/* Quick Search Modal */}
|
{/* Quick Search Modal */}
|
||||||
{searchOpen && (
|
{searchOpen && (
|
||||||
<div
|
<div
|
||||||
className="fixed inset-0 z-50 bg-background/80 backdrop-blur-sm flex items-start justify-center pt-[20vh]"
|
className="fixed inset-0 z-[60] bg-background/80 backdrop-blur-sm flex items-start justify-center pt-[15vh] sm:pt-[20vh] px-4"
|
||||||
onClick={() => setSearchOpen(false)}
|
onClick={() => setSearchOpen(false)}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
@ -204,10 +235,15 @@ export function CommandCenterLayout({
|
|||||||
outline-none text-lg"
|
outline-none text-lg"
|
||||||
autoFocus
|
autoFocus
|
||||||
/>
|
/>
|
||||||
<kbd className="hidden sm:flex items-center h-6 px-2 bg-background border border-border
|
<button
|
||||||
rounded text-xs text-foreground-subtle font-mono">ESC</kbd>
|
onClick={() => setSearchOpen(false)}
|
||||||
|
className="flex items-center h-6 px-2 bg-background border border-border
|
||||||
|
rounded text-xs text-foreground-subtle font-mono hover:text-foreground transition-colors"
|
||||||
|
>
|
||||||
|
ESC
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="p-4 text-center text-foreground-muted text-sm">
|
<div className="p-6 text-center text-foreground-muted text-sm">
|
||||||
Start typing to search...
|
Start typing to search...
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -235,4 +271,3 @@ function KeyboardShortcut({ onTrigger, keys }: { onTrigger: () => void, keys: st
|
|||||||
|
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -38,7 +38,7 @@ export function Header() {
|
|||||||
|
|
||||||
// Public navigation - same for all visitors
|
// Public navigation - same for all visitors
|
||||||
const publicNavItems = [
|
const publicNavItems = [
|
||||||
{ href: '/auctions', label: 'Market', icon: Gavel },
|
{ href: '/auctions', label: 'Auctions', icon: Gavel },
|
||||||
{ href: '/tld-pricing', label: 'TLD Intel', icon: TrendingUp },
|
{ href: '/tld-pricing', label: 'TLD Intel', icon: TrendingUp },
|
||||||
{ href: '/pricing', label: 'Pricing', icon: CreditCard },
|
{ href: '/pricing', label: 'Pricing', icon: CreditCard },
|
||||||
]
|
]
|
||||||
|
|||||||
@ -17,6 +17,8 @@ import {
|
|||||||
Zap,
|
Zap,
|
||||||
Shield,
|
Shield,
|
||||||
CreditCard,
|
CreditCard,
|
||||||
|
Menu,
|
||||||
|
X,
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
import { useState, useEffect } from 'react'
|
import { useState, useEffect } from 'react'
|
||||||
import clsx from 'clsx'
|
import clsx from 'clsx'
|
||||||
@ -32,6 +34,7 @@ export function Sidebar({ collapsed: controlledCollapsed, onCollapsedChange }: S
|
|||||||
|
|
||||||
// Internal state for uncontrolled mode
|
// Internal state for uncontrolled mode
|
||||||
const [internalCollapsed, setInternalCollapsed] = useState(false)
|
const [internalCollapsed, setInternalCollapsed] = useState(false)
|
||||||
|
const [mobileOpen, setMobileOpen] = useState(false)
|
||||||
|
|
||||||
// Use controlled or uncontrolled state
|
// Use controlled or uncontrolled state
|
||||||
const collapsed = controlledCollapsed ?? internalCollapsed
|
const collapsed = controlledCollapsed ?? internalCollapsed
|
||||||
@ -44,6 +47,11 @@ export function Sidebar({ collapsed: controlledCollapsed, onCollapsedChange }: S
|
|||||||
setCollapsed(saved === 'true')
|
setCollapsed(saved === 'true')
|
||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
// Close mobile menu on route change
|
||||||
|
useEffect(() => {
|
||||||
|
setMobileOpen(false)
|
||||||
|
}, [pathname])
|
||||||
|
|
||||||
// Save collapsed state
|
// Save collapsed state
|
||||||
const toggleCollapsed = () => {
|
const toggleCollapsed = () => {
|
||||||
@ -56,7 +64,10 @@ export function Sidebar({ collapsed: controlledCollapsed, onCollapsedChange }: S
|
|||||||
const tierIcon = tierName === 'Tycoon' ? Crown : tierName === 'Trader' ? TrendingUp : Zap
|
const tierIcon = tierName === 'Tycoon' ? Crown : tierName === 'Trader' ? TrendingUp : Zap
|
||||||
const TierIcon = tierIcon
|
const TierIcon = tierIcon
|
||||||
|
|
||||||
// Navigation items
|
// Count available domains for notification badge
|
||||||
|
const availableCount = domains?.filter(d => d.is_available).length || 0
|
||||||
|
|
||||||
|
// Navigation items - renamed "Market" to "Auctions" per review
|
||||||
const navItems = [
|
const navItems = [
|
||||||
{
|
{
|
||||||
href: '/dashboard',
|
href: '/dashboard',
|
||||||
@ -68,7 +79,7 @@ export function Sidebar({ collapsed: controlledCollapsed, onCollapsedChange }: S
|
|||||||
href: '/watchlist',
|
href: '/watchlist',
|
||||||
label: 'Watchlist',
|
label: 'Watchlist',
|
||||||
icon: Eye,
|
icon: Eye,
|
||||||
badge: domains?.filter(d => d.is_available).length || null,
|
badge: availableCount || null,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
href: '/portfolio',
|
href: '/portfolio',
|
||||||
@ -77,8 +88,8 @@ export function Sidebar({ collapsed: controlledCollapsed, onCollapsedChange }: S
|
|||||||
badge: null,
|
badge: null,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
href: '/market',
|
href: '/auctions',
|
||||||
label: 'Market',
|
label: 'Auctions',
|
||||||
icon: Gavel,
|
icon: Gavel,
|
||||||
badge: null,
|
badge: null,
|
||||||
},
|
},
|
||||||
@ -99,15 +110,8 @@ export function Sidebar({ collapsed: controlledCollapsed, onCollapsedChange }: S
|
|||||||
return pathname.startsWith(href)
|
return pathname.startsWith(href)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
const SidebarContent = () => (
|
||||||
<aside
|
<>
|
||||||
className={clsx(
|
|
||||||
"fixed left-0 top-0 bottom-0 z-40 flex flex-col",
|
|
||||||
"bg-background-secondary/50 backdrop-blur-xl border-r border-border",
|
|
||||||
"transition-all duration-300 ease-in-out",
|
|
||||||
collapsed ? "w-[72px]" : "w-[240px]"
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{/* Logo */}
|
{/* Logo */}
|
||||||
<div className={clsx(
|
<div className={clsx(
|
||||||
"h-16 sm:h-20 flex items-center border-b border-border/50",
|
"h-16 sm:h-20 flex items-center border-b border-border/50",
|
||||||
@ -135,6 +139,7 @@ export function Sidebar({ collapsed: controlledCollapsed, onCollapsedChange }: S
|
|||||||
<Link
|
<Link
|
||||||
key={item.href}
|
key={item.href}
|
||||||
href={item.href}
|
href={item.href}
|
||||||
|
onClick={() => setMobileOpen(false)}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"group flex items-center gap-3 px-3 py-2.5 rounded-xl transition-all duration-200",
|
"group flex items-center gap-3 px-3 py-2.5 rounded-xl transition-all duration-200",
|
||||||
isActive(item.href)
|
isActive(item.href)
|
||||||
@ -174,6 +179,7 @@ export function Sidebar({ collapsed: controlledCollapsed, onCollapsedChange }: S
|
|||||||
{user?.is_admin && (
|
{user?.is_admin && (
|
||||||
<Link
|
<Link
|
||||||
href="/admin"
|
href="/admin"
|
||||||
|
onClick={() => setMobileOpen(false)}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"group flex items-center gap-3 px-3 py-2.5 rounded-xl transition-all duration-200",
|
"group flex items-center gap-3 px-3 py-2.5 rounded-xl transition-all duration-200",
|
||||||
pathname.startsWith('/admin')
|
pathname.startsWith('/admin')
|
||||||
@ -192,6 +198,7 @@ export function Sidebar({ collapsed: controlledCollapsed, onCollapsedChange }: S
|
|||||||
<Link
|
<Link
|
||||||
key={item.href}
|
key={item.href}
|
||||||
href={item.href}
|
href={item.href}
|
||||||
|
onClick={() => setMobileOpen(false)}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"group flex items-center gap-3 px-3 py-2.5 rounded-xl transition-all duration-200",
|
"group flex items-center gap-3 px-3 py-2.5 rounded-xl transition-all duration-200",
|
||||||
isActive(item.href)
|
isActive(item.href)
|
||||||
@ -247,7 +254,10 @@ export function Sidebar({ collapsed: controlledCollapsed, onCollapsedChange }: S
|
|||||||
|
|
||||||
{/* Logout */}
|
{/* Logout */}
|
||||||
<button
|
<button
|
||||||
onClick={logout}
|
onClick={() => {
|
||||||
|
logout()
|
||||||
|
setMobileOpen(false)
|
||||||
|
}}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"w-full flex items-center gap-3 px-3 py-2.5 rounded-xl transition-all duration-200",
|
"w-full flex items-center gap-3 px-3 py-2.5 rounded-xl transition-all duration-200",
|
||||||
"text-foreground-muted hover:text-foreground hover:bg-foreground/5"
|
"text-foreground-muted hover:text-foreground hover:bg-foreground/5"
|
||||||
@ -259,12 +269,12 @@ export function Sidebar({ collapsed: controlledCollapsed, onCollapsedChange }: S
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Collapse Toggle */}
|
{/* Collapse Toggle - Desktop only */}
|
||||||
<button
|
<button
|
||||||
onClick={toggleCollapsed}
|
onClick={toggleCollapsed}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"absolute -right-3 top-24 w-6 h-6 bg-background-secondary border border-border rounded-full",
|
"hidden lg:flex absolute -right-3 top-24 w-6 h-6 bg-background-secondary border border-border rounded-full",
|
||||||
"flex items-center justify-center text-foreground-muted hover:text-foreground",
|
"items-center justify-center text-foreground-muted hover:text-foreground",
|
||||||
"hover:bg-foreground/5 transition-all duration-200 shadow-sm"
|
"hover:bg-foreground/5 transition-all duration-200 shadow-sm"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
@ -274,7 +284,60 @@ export function Sidebar({ collapsed: controlledCollapsed, onCollapsedChange }: S
|
|||||||
<ChevronLeft className="w-3.5 h-3.5" />
|
<ChevronLeft className="w-3.5 h-3.5" />
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
</aside>
|
</>
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{/* Mobile Menu Button */}
|
||||||
|
<button
|
||||||
|
onClick={() => setMobileOpen(true)}
|
||||||
|
className="lg:hidden fixed top-4 left-4 z-50 w-10 h-10 bg-background-secondary border border-border
|
||||||
|
rounded-xl flex items-center justify-center text-foreground-muted hover:text-foreground
|
||||||
|
transition-colors shadow-lg"
|
||||||
|
>
|
||||||
|
<Menu className="w-5 h-5" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* Mobile Overlay */}
|
||||||
|
{mobileOpen && (
|
||||||
|
<div
|
||||||
|
className="lg:hidden fixed inset-0 z-40 bg-background/80 backdrop-blur-sm"
|
||||||
|
onClick={() => setMobileOpen(false)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Mobile Sidebar */}
|
||||||
|
<aside
|
||||||
|
className={clsx(
|
||||||
|
"lg:hidden fixed left-0 top-0 bottom-0 z-50 w-[280px] flex flex-col",
|
||||||
|
"bg-background-secondary border-r border-border",
|
||||||
|
"transition-transform duration-300 ease-in-out",
|
||||||
|
mobileOpen ? "translate-x-0" : "-translate-x-full"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{/* Close button */}
|
||||||
|
<button
|
||||||
|
onClick={() => setMobileOpen(false)}
|
||||||
|
className="absolute top-4 right-4 w-8 h-8 flex items-center justify-center
|
||||||
|
text-foreground-muted hover:text-foreground transition-colors"
|
||||||
|
>
|
||||||
|
<X className="w-5 h-5" />
|
||||||
|
</button>
|
||||||
|
<SidebarContent />
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
{/* Desktop Sidebar */}
|
||||||
|
<aside
|
||||||
|
className={clsx(
|
||||||
|
"hidden lg:flex fixed left-0 top-0 bottom-0 z-40 flex-col",
|
||||||
|
"bg-background-secondary/50 backdrop-blur-xl border-r border-border",
|
||||||
|
"transition-all duration-300 ease-in-out",
|
||||||
|
collapsed ? "w-[72px]" : "w-[240px]"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<SidebarContent />
|
||||||
|
</aside>
|
||||||
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
87
report.md
Normal file
87
report.md
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
Das ist ein gewaltiger Schritt nach vorne! 🚀
|
||||||
|
|
||||||
|
Die Seiten wirken jetzt kohärent, professionell und haben eine klare psychologische Führung (Hook -> Value -> Gate -> Sign Up). Besonders der Wechsel auf **$9 für den Einstieg** (Trader) ist smart – das ist ein "No-Brainer"-Preis für Impulse-Käufe.
|
||||||
|
|
||||||
|
Hier ist mein Feedback zu den einzelnen Seiten mit Fokus auf Conversion und UX:
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 1. Navigation & Globales Layout
|
||||||
|
Die Navigation ist **perfekt minimalistisch**.
|
||||||
|
* `Market | TLD Intel | Pricing` – Das sind genau die drei Säulen.
|
||||||
|
* **Vorschlag:** Ich würde "Market" eventuell in **"Auctions"** oder **"Live Market"** umbenennen. "Market" ist etwas vage. "Auctions" triggert eher das Gefühl "Hier gibt es Schnäppchen".
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. Landing Page
|
||||||
|
**Das Starke:**
|
||||||
|
* Die Headline *"The market never sleeps. You should."* ist Weltklasse.
|
||||||
|
* Der Ticker mit den Live-Preisen erzeugt sofort FOMO (Fear Of Missing Out).
|
||||||
|
* Die Sektion "TLD Intelligence" mit den "Sign in to view"-Overlays bei den Daten ist ein **exzellenter Conversion-Treiber**. Der User sieht, dass da Daten *sind*, aber er muss sich anmelden (kostenlos), um sie zu sehen. Das ist der perfekte "Account-Erstellungs-Köder".
|
||||||
|
|
||||||
|
**Kritikpunkt / To-Do:**
|
||||||
|
* **Der "Search"-Fokus:** Du schreibst *"Try dream.com..."*, aber visuell muss dort ein **riesiges Input-Feld** sein. Das muss das dominante Element sein.
|
||||||
|
* **Der Ticker:** Achte darauf, dass der Ticker technisch sauber läuft (marquee/scrolling). Im Text oben wiederholt sich die Liste statisch – auf der echten Seite muss das fließen.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. Market / Auctions Page (WICHTIG!)
|
||||||
|
Hier sehe ich das **größte Risiko**.
|
||||||
|
Dein Konzept ("Unlock Smart Opportunities") ist super. Aber die **Beispiel-Daten**, die du auf der Public-Seite zeigst, sind gefährlich.
|
||||||
|
|
||||||
|
**Das Problem:**
|
||||||
|
In deiner Liste stehen Dinge wie:
|
||||||
|
* `fgagtqjisqxyoyjrjfizxshtw.xyz`
|
||||||
|
* `52gao1588.cc`
|
||||||
|
* `professional-packing-services...website`
|
||||||
|
|
||||||
|
Wenn ein neuer User das sieht, denkt er: **"Das ist eine Spam-Seite voll mit Schrott."** Er wird sich nicht anmelden.
|
||||||
|
|
||||||
|
**Die Lösung (Der "Vanity-Filter"):**
|
||||||
|
Du musst für die **öffentliche Seite (ausgeloggt)** einen harten Filter in den Code bauen. Zeige ausgeloggten Usern **NUR** Domains an, die schön aussehen.
|
||||||
|
* Regel 1: Keine Zahlen (außer bei kurzen Domains).
|
||||||
|
* Regel 2: Keine Bindestriche (Hyphens).
|
||||||
|
* Regel 3: Länge < 12 Zeichen.
|
||||||
|
* Regel 4: Nur .com, .io, .ai, .co, .de, .ch (Keine .cc, .website Spam-Cluster).
|
||||||
|
|
||||||
|
**Warum?**
|
||||||
|
Der User soll denken: "Wow, hier gibt es Premium-Domains wie `nexus.dev`". Er darf den Müll nicht sehen, bevor er eingeloggt ist (und selbst dann solltest du den Müll filtern, wie wir besprochen haben).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4. TLD Pricing Page
|
||||||
|
**Sehr gut gelöst.**
|
||||||
|
* Die "Moving Now"-Karten oben (.ai +35%) sind der Haken.
|
||||||
|
* Die Tabelle darunter mit "Sign in" zu sperren (Blur-Effekt oder Schloss-Icon), ist genau richtig.
|
||||||
|
* Der User bekommt genug Info ("Aha, .com ist beliebt"), aber für die Details ("Ist der Trend steigend?") muss er 'Scout' werden.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5. Pricing Page
|
||||||
|
Die neue Struktur mit **Scout (Free) / Trader ($9) / Tycoon ($29)** ist viel besser als das alte $19-Modell.
|
||||||
|
|
||||||
|
**Optimierung der Tabelle:**
|
||||||
|
Du musst den Unterschied zwischen **Scout** und **Trader** noch schärfer machen, damit die Leute die $9 bezahlen.
|
||||||
|
|
||||||
|
| Feature | Scout (Free) | Trader ($9) | Warum Upgrade? |
|
||||||
|
| :--- | :--- | :--- | :--- |
|
||||||
|
| **Auctions** | Raw Feed (Ungefiltert) | **Smart Clean Feed** | *"Ich will den Spam nicht sehen."* |
|
||||||
|
| **Data** | Nur Preise | **Valuation & Deal Score** | *"Ich will wissen, ob es ein Schnäppchen ist."* |
|
||||||
|
| **Updates** | Täglich | **Stündlich** | *"Ich will schneller sein als andere."* |
|
||||||
|
|
||||||
|
**Wichtig:** Füge in der "Trader"-Spalte explizit **"Spam Filters"** oder **"Curated List"** hinzu. Das ist Zeitersparnis, und dafür zahlen Leute.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Zusammenfassung & Tone of Voice
|
||||||
|
|
||||||
|
Der Tone of Voice ist jetzt konsistent: **Analytisch, Knapp, Strategisch.**
|
||||||
|
|
||||||
|
* *Alt:* "Jage Domains." (Bisschen spielerisch)
|
||||||
|
* *Neu:* "Don't guess. Know." (Professionell, B2B-tauglich)
|
||||||
|
|
||||||
|
**Letzter Check vor dem Launch:**
|
||||||
|
1. **Mobile View:** Prüfe die riesige Tabelle auf dem Handy. Wahrscheinlich musst du auf Mobile Spalten ausblenden (z.B. nur Domain + Preis + Button zeigen).
|
||||||
|
2. **Der Filter:** Bitte, bitte filtere die `fgagtqjis...xyz` Domains auf der Startseite raus. Das ist der wichtigste Punkt für den ersten Eindruck.
|
||||||
|
|
||||||
|
Das sieht nach einem Produkt aus, für das ich meine Kreditkarte zücken würde. Gute Arbeit!
|
||||||
Reference in New Issue
Block a user