From 307f465ebb887cab76754766f167a679516c9821 Mon Sep 17 00:00:00 2001 From: "yves.gugger" Date: Wed, 10 Dec 2025 12:22:01 +0100 Subject: [PATCH] feat: Marketplace command page + SEO disabled state + Pricing update MARKETPLACE: - Created /command/marketplace for authenticated users - Shows all listings with search, sort, and filters - Grid layout with domain cards, scores, and verification badges - Links to 'My Listings' for quick access to management SIDEBAR SEO JUICE: - Removed 'Tycoon' badge label - Now shows as disabled/grayed out for non-Tycoon users - Crown icon indicates premium feature - Hover tooltip explains feature & upgrade path PRICING PAGE: - Added new features to tier cards: - Scout: 2 domain listings - Trader: 10 listings, 5 Sniper Alerts - Tycoon: 50 listings, unlimited alerts, SEO Juice - Updated comparison table with: - For Sale Listings row - Sniper Alerts row - SEO Juice Detector row --- frontend/src/app/command/listings/page.tsx | 0 frontend/src/app/command/marketplace/page.tsx | 311 ++++++++++++++++++ frontend/src/app/pricing/page.tsx | 15 +- frontend/src/components/Sidebar.tsx | 120 ++++--- 4 files changed, 391 insertions(+), 55 deletions(-) mode change 100644 => 100755 frontend/src/app/command/listings/page.tsx create mode 100644 frontend/src/app/command/marketplace/page.tsx diff --git a/frontend/src/app/command/listings/page.tsx b/frontend/src/app/command/listings/page.tsx old mode 100644 new mode 100755 diff --git a/frontend/src/app/command/marketplace/page.tsx b/frontend/src/app/command/marketplace/page.tsx new file mode 100644 index 0000000..eacc969 --- /dev/null +++ b/frontend/src/app/command/marketplace/page.tsx @@ -0,0 +1,311 @@ +'use client' + +import { useEffect, useState } from 'react' +import { api } from '@/lib/api' +import { CommandCenterLayout } from '@/components/CommandCenterLayout' +import { PageContainer, StatCard, Badge } from '@/components/PremiumTable' +import { + Search, + Shield, + Loader2, + ExternalLink, + Store, + Tag, + DollarSign, + Filter, + SortAsc, + ArrowUpDown, +} from 'lucide-react' +import Link from 'next/link' +import clsx from 'clsx' + +interface Listing { + domain: string + slug: string + title: string | null + description: string | null + asking_price: number | null + currency: string + price_type: string + pounce_score: number | null + estimated_value: number | null + is_verified: boolean + allow_offers: boolean + public_url: string + seller_verified: boolean +} + +type SortOption = 'newest' | 'price_asc' | 'price_desc' | 'score' + +export default function CommandMarketplacePage() { + const [listings, setListings] = useState([]) + const [loading, setLoading] = useState(true) + const [searchQuery, setSearchQuery] = useState('') + const [minPrice, setMinPrice] = useState('') + const [maxPrice, setMaxPrice] = useState('') + const [verifiedOnly, setVerifiedOnly] = useState(false) + const [sortBy, setSortBy] = useState('newest') + const [showFilters, setShowFilters] = useState(false) + + useEffect(() => { + loadListings() + }, [sortBy, verifiedOnly]) + + const loadListings = async () => { + setLoading(true) + try { + const params = new URLSearchParams() + params.set('limit', '100') + if (sortBy === 'price_asc') params.set('sort', 'price_asc') + if (sortBy === 'price_desc') params.set('sort', 'price_desc') + if (verifiedOnly) params.set('verified_only', 'true') + + const data = await api.request(`/listings?${params.toString()}`) + setListings(data) + } catch (err) { + console.error('Failed to load listings:', err) + } finally { + setLoading(false) + } + } + + const formatPrice = (price: number | null, currency: string) => { + if (!price) return 'Make Offer' + return new Intl.NumberFormat('en-US', { + style: 'currency', + currency, + minimumFractionDigits: 0, + }).format(price) + } + + const filteredListings = listings.filter(listing => { + // Search filter + if (searchQuery) { + const query = searchQuery.toLowerCase() + if (!listing.domain.toLowerCase().includes(query)) { + return false + } + } + + // Price filters + if (minPrice && listing.asking_price && listing.asking_price < parseFloat(minPrice)) { + return false + } + if (maxPrice && listing.asking_price && listing.asking_price > parseFloat(maxPrice)) { + return false + } + + return true + }) + + // Sort listings + const sortedListings = [...filteredListings].sort((a, b) => { + switch (sortBy) { + case 'price_asc': + return (a.asking_price || 0) - (b.asking_price || 0) + case 'price_desc': + return (b.asking_price || 0) - (a.asking_price || 0) + case 'score': + return (b.pounce_score || 0) - (a.pounce_score || 0) + default: + return 0 + } + }) + + const verifiedCount = listings.filter(l => l.is_verified).length + const avgPrice = listings.length > 0 + ? listings.filter(l => l.asking_price).reduce((sum, l) => sum + (l.asking_price || 0), 0) / listings.filter(l => l.asking_price).length + : 0 + + return ( + + + My Listings + + } + > + + {/* Stats */} +
+ + + 0 ? `$${Math.round(avgPrice).toLocaleString()}` : '—'} + icon={DollarSign} + /> + +
+ + {/* Search & Filters */} +
+
+ {/* Search */} +
+ + setSearchQuery(e.target.value)} + className="w-full pl-12 pr-4 py-3 bg-background border border-border rounded-xl + text-foreground placeholder:text-foreground-subtle + focus:outline-none focus:ring-2 focus:ring-accent/30 focus:border-accent" + /> +
+ + {/* Sort */} + + + {/* Filter Toggle */} + +
+ + {/* Expanded Filters */} + {showFilters && ( +
+
+ Price: + setMinPrice(e.target.value)} + className="w-24 px-3 py-2 bg-background border border-border rounded-lg text-sm text-foreground + placeholder:text-foreground-subtle focus:outline-none focus:border-accent" + /> + + setMaxPrice(e.target.value)} + className="w-24 px-3 py-2 bg-background border border-border rounded-lg text-sm text-foreground + placeholder:text-foreground-subtle focus:outline-none focus:border-accent" + /> +
+ + +
+ )} +
+ + {/* Listings Grid */} + {loading ? ( +
+ +
+ ) : sortedListings.length === 0 ? ( +
+ +

No Domains Found

+

+ {searchQuery || minPrice || maxPrice + ? 'Try adjusting your filters' + : 'No domains are currently listed for sale'} +

+ + + List Your Domain + +
+ ) : ( +
+ {sortedListings.map((listing) => ( + +
+
+

+ {listing.domain} +

+ {listing.title && ( +

{listing.title}

+ )} +
+ {listing.is_verified && ( +
+ +
+ )} +
+ + {listing.description && ( +

+ {listing.description} +

+ )} + +
+
+ {listing.pounce_score && ( +
+ {listing.pounce_score} +
+ )} + {listing.allow_offers && ( + Offers + )} +
+
+

+ {formatPrice(listing.asking_price, listing.currency)} +

+ {listing.price_type === 'negotiable' && ( +

Negotiable

+ )} +
+
+ + ))} +
+ )} +
+
+ ) +} + diff --git a/frontend/src/app/pricing/page.tsx b/frontend/src/app/pricing/page.tsx index 8d1f5b5..eb413cd 100644 --- a/frontend/src/app/pricing/page.tsx +++ b/frontend/src/app/pricing/page.tsx @@ -23,8 +23,9 @@ const tiers = [ { text: 'Daily availability scans', highlight: false, available: true }, { text: 'Email alerts', highlight: false, available: true }, { text: 'Raw auction feed', highlight: false, available: true, sublabel: 'Unfiltered' }, - { text: 'Curated auction list', highlight: false, available: false }, + { text: '2 domain listings', highlight: false, available: true, sublabel: 'For Sale' }, { text: 'Deal scores & valuations', highlight: false, available: false }, + { text: 'Sniper Alerts', highlight: false, available: false }, ], cta: 'Start Free', highlighted: false, @@ -43,8 +44,9 @@ const tiers = [ { text: 'Hourly scans', highlight: true, available: true, sublabel: '24x faster' }, { text: 'Smart spam filter', highlight: true, available: true, sublabel: 'Curated list' }, { text: 'Deal scores & valuations', highlight: true, available: true }, + { text: '10 domain listings', highlight: true, available: true, sublabel: 'For Sale' }, + { text: '5 Sniper Alerts', highlight: true, available: true }, { text: 'Portfolio tracking (25)', highlight: true, available: true }, - { text: '90-day price history', highlight: false, available: true }, { text: 'Expiry date tracking', highlight: true, available: true }, ], cta: 'Upgrade to Trader', @@ -62,10 +64,11 @@ const tiers = [ features: [ { text: '500 domains to track', highlight: true, available: true }, { text: 'Real-time scans', highlight: true, available: true, sublabel: 'Every 10 min' }, - { text: 'Priority alerts', highlight: true, available: true }, + { text: '50 domain listings', highlight: true, available: true, sublabel: 'For Sale' }, + { text: 'Unlimited Sniper Alerts', highlight: true, available: true }, + { text: 'SEO Juice Detector', highlight: true, available: true, sublabel: 'Backlinks' }, { text: 'Unlimited portfolio', highlight: true, available: true }, { text: 'Full price history', highlight: true, available: true }, - { text: 'Advanced valuation', highlight: true, available: true }, { text: 'API access', highlight: true, available: true, sublabel: 'Coming soon' }, ], cta: 'Go Tycoon', @@ -80,8 +83,10 @@ const comparisonFeatures = [ { 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: 'For Sale Listings', scout: '2', trader: '10', tycoon: '50' }, + { name: 'Sniper Alerts', scout: '—', trader: '5', tycoon: 'Unlimited' }, { name: 'Portfolio Domains', scout: '—', trader: '25', tycoon: 'Unlimited' }, - { name: 'Domain Valuation', scout: '—', trader: 'check', tycoon: 'Advanced' }, + { name: 'SEO Juice Detector', scout: '—', trader: '—', tycoon: 'check' }, { name: 'Price History', scout: '—', trader: '90 days', tycoon: 'Unlimited' }, { name: 'Expiry Tracking', scout: '—', trader: 'check', tycoon: 'check' }, ] diff --git a/frontend/src/components/Sidebar.tsx b/frontend/src/components/Sidebar.tsx index ac6cb7b..72118b6 100755 --- a/frontend/src/components/Sidebar.tsx +++ b/frontend/src/components/Sidebar.tsx @@ -72,6 +72,8 @@ export function Sidebar({ collapsed: controlledCollapsed, onCollapsedChange }: S // Count available domains for notification badge const availableCount = domains?.filter(d => d.is_available).length || 0 + const isTycoon = tierName.toLowerCase() === 'tycoon' + // SECTION 1: Discover - External market data const discoverItems = [ { @@ -81,7 +83,7 @@ export function Sidebar({ collapsed: controlledCollapsed, onCollapsedChange }: S badge: null, }, { - href: '/buy', + href: '/command/marketplace', label: 'Marketplace', icon: Tag, badge: null, @@ -95,7 +97,13 @@ export function Sidebar({ collapsed: controlledCollapsed, onCollapsedChange }: S ] // SECTION 2: Manage - Your own assets and tools - const manageItems = [ + const manageItems: Array<{ + href: string + label: string + icon: any + badge: number | null + tycoonOnly?: boolean + }> = [ { href: '/command/dashboard', label: 'Dashboard', @@ -130,7 +138,8 @@ export function Sidebar({ collapsed: controlledCollapsed, onCollapsedChange }: S href: '/command/seo', label: 'SEO Juice', icon: Link2, - badge: 'Tycoon', + badge: null, + tycoonOnly: true, }, ] @@ -246,56 +255,67 @@ export function Sidebar({ collapsed: controlledCollapsed, onCollapsedChange }: S {collapsed &&
}
- {manageItems.map((item) => ( - setMobileOpen(false)} - className={clsx( - "group relative flex items-center gap-3 px-3 py-3 rounded-xl transition-all duration-300", - isActive(item.href) - ? "bg-gradient-to-r from-accent/20 to-accent/5 text-foreground border border-accent/20 shadow-[0_0_20px_-5px_rgba(16,185,129,0.2)]" - : "text-foreground-muted hover:text-foreground hover:bg-foreground/5 border border-transparent" - )} - title={collapsed ? item.label : undefined} - > - {isActive(item.href) && ( -
- )} -
- - {item.badge && typeof item.badge === 'number' && ( - - {item.badge > 9 ? '9+' : item.badge} + {manageItems.map((item) => { + const isDisabled = item.tycoonOnly && !isTycoon + const ItemWrapper = isDisabled ? 'div' : Link + + return ( + !isDisabled && setMobileOpen(false)} + className={clsx( + "group relative flex items-center gap-3 px-3 py-3 rounded-xl transition-all duration-300", + isDisabled + ? "opacity-50 cursor-not-allowed border border-transparent" + : isActive(item.href) + ? "bg-gradient-to-r from-accent/20 to-accent/5 text-foreground border border-accent/20 shadow-[0_0_20px_-5px_rgba(16,185,129,0.2)]" + : "text-foreground-muted hover:text-foreground hover:bg-foreground/5 border border-transparent" + )} + title={ + isDisabled + ? "SEO Juice Detector: Analyze backlinks, domain authority & find hidden SEO value. Upgrade to Tycoon to unlock." + : collapsed ? item.label : undefined + } + > + {!isDisabled && isActive(item.href) && ( +
+ )} +
+ + {item.badge && typeof item.badge === 'number' && !isDisabled && ( + + {item.badge > 9 ? '9+' : item.badge} + + )} +
+ {!collapsed && ( + + {item.label} )} - {item.badge && typeof item.badge === 'string' && !collapsed && ( - - {item.badge} - + {/* Lock icon for disabled items */} + {isDisabled && !collapsed && ( + )} -
- {!collapsed && ( - - {item.label} - - )} - {!isActive(item.href) && ( -
- )} - - ))} + {!isDisabled && !isActive(item.href) && ( +
+ )} + + ) + })}