'use client'
import { useEffect, useState, useCallback } from 'react'
import { useSearchParams } from 'next/navigation'
import { useStore } from '@/lib/store'
import { api } from '@/lib/api'
import { Sidebar } from '@/components/Sidebar'
import {
Plus, Shield, Eye, ExternalLink, Loader2, Trash2,
CheckCircle, AlertCircle, Copy, X, Tag, Sparkles,
TrendingUp, Gavel, Target, Menu, Settings, LogOut, Crown, Zap, Coins,
ArrowRight, RefreshCw, Globe, Lock, Briefcase
} from 'lucide-react'
import Link from 'next/link'
import Image from 'next/image'
import clsx from 'clsx'
// ============================================================================
// TYPES
// ============================================================================
interface Listing {
id: number
domain: string
slug: string
title: string | null
description: string | null
asking_price: number | null
min_offer: number | null
currency: string
price_type: string
pounce_score: number | null
verification_status: string
is_verified: boolean
status: string
view_count: number
inquiry_count: number
public_url: string
created_at: string
}
// ============================================================================
// MAIN PAGE
// ============================================================================
export default function MyListingsPage() {
const { subscription, user, logout, checkAuth } = useStore()
const searchParams = useSearchParams()
const prefillDomain = searchParams.get('domain')
const [listings, setListings] = useState
([])
const [loading, setLoading] = useState(true)
const [showCreateWizard, setShowCreateWizard] = useState(false)
const [selectedListing, setSelectedListing] = useState(null)
const [deletingId, setDeletingId] = useState(null)
const [menuOpen, setMenuOpen] = useState(false)
const tier = subscription?.tier || 'scout'
const isScout = tier === 'scout'
const listingLimits: Record = { scout: 0, trader: 5, tycoon: 50 }
const maxListings = listingLimits[tier] || 0
const canAddMore = listings.length < maxListings && !isScout
const isTycoon = tier === 'tycoon'
const activeListings = listings.filter(l => l.status === 'active').length
const draftListings = listings.filter(l => l.status === 'draft').length
const totalViews = listings.reduce((sum, l) => sum + l.view_count, 0)
const totalInquiries = listings.reduce((sum, l) => sum + l.inquiry_count, 0)
useEffect(() => { checkAuth() }, [checkAuth])
useEffect(() => { if (prefillDomain && !isScout) setShowCreateWizard(true) }, [prefillDomain, isScout])
const loadListings = useCallback(async () => {
if (isScout) {
setLoading(false)
return
}
setLoading(true)
try {
const data = await api.getMyListings()
setListings(data)
} catch (err) { console.error(err) }
finally { setLoading(false) }
}, [isScout])
useEffect(() => { loadListings() }, [loadListings])
const handleDelete = async (id: number, domain: string) => {
if (!confirm(`Delete listing for ${domain}?`)) return
setDeletingId(id)
try {
await api.deleteListing(id)
await loadListings()
} catch (err: any) { alert(err.message || 'Failed') }
finally { setDeletingId(null) }
}
const handlePublish = async (listing: Listing) => {
if (!listing.is_verified) {
setSelectedListing(listing)
return
}
try {
await api.updateListing(listing.id, { status: 'active' })
await loadListings()
} catch (err: any) { alert(err.message || 'Failed to publish') }
}
const mobileNavItems = [
{ href: '/terminal/radar', label: 'Radar', icon: Target, active: false },
{ href: '/terminal/market', label: 'Market', icon: Gavel, active: false },
{ href: '/terminal/watchlist', label: 'Watch', icon: Eye, active: false },
{ href: '/terminal/intel', label: 'Intel', icon: TrendingUp, active: false },
]
const tierName = subscription?.tier_name || subscription?.tier || 'Scout'
const TierIcon = tierName === 'Tycoon' ? Crown : tierName === 'Trader' ? TrendingUp : Zap
const drawerNavSections = [
{ title: 'Discover', items: [
{ href: '/terminal/radar', label: 'Radar', icon: Target },
{ href: '/terminal/market', label: 'Market', icon: Gavel },
{ href: '/terminal/intel', label: 'Intel', icon: TrendingUp },
]},
{ title: 'Manage', items: [
{ href: '/terminal/watchlist', label: 'Watchlist', icon: Eye },
{ href: '/terminal/portfolio', label: 'Portfolio', icon: Briefcase },
{ href: '/terminal/sniper', label: 'Sniper', icon: Target },
]},
{ title: 'Monetize', items: [
{ href: '/terminal/yield', label: 'Yield', icon: Coins },
{ href: '/terminal/listing', label: 'For Sale', icon: Tag, active: true },
]}
]
// ============================================================================
// SCOUT UPGRADE PROMPT (Feature not available for free tier)
// ============================================================================
if (isScout) {
return (
For Sale
List domains on Pounce Direct.
0% commission. DNS-verified ownership. Direct buyer contact.
- 5 Active Listings
- DNS Ownership Verification
- Direct Buyer Contact
- 50 Active Listings
- Featured Placement
- Priority in Market Feed
Upgrade
)
}
// ============================================================================
// MAIN LISTING VIEW (For Trader & Tycoon)
// ============================================================================
return (
{/* MOBILE HEADER */}
{listings.length}/{maxListings}
{/* DESKTOP HEADER */}
For Sale
{listings.length}/{maxListings}
List your domains for sale. 0% commission, verified ownership, direct buyer contact.
{/* ADD BUTTON MOBILE */}
{/* CONTENT */}
{loading ? (
) : listings.length === 0 ? (
No listings yet
Create your first listing to start selling
) : (
{/* Header */}
Domain
Price
Status
Views
Leads
Actions
{listings.map((listing) => (
handleDelete(listing.id, listing.domain)}
onVerify={() => setSelectedListing(listing)}
onPublish={() => handlePublish(listing)}
isDeleting={deletingId === listing.id}
/>
))}
)}
{!canAddMore && listings.length >= maxListings && (
Listing Limit Reached
{tier === 'trader' ? 'Upgrade to Tycoon for 50 listings' : 'Contact us for enterprise plans'}
{tier === 'trader' && (
Upgrade to Tycoon
)}
)}
{/* MOBILE BOTTOM NAV */}
{/* DRAWER */}
{menuOpen && (
setMenuOpen(false)}
onLogout={() => { logout(); setMenuOpen(false) }}
/>
)}
{/* CREATE WIZARD (3-Step) */}
{showCreateWizard && (
setShowCreateWizard(false)}
onSuccess={() => { loadListings(); setShowCreateWizard(false) }}
prefillDomain={prefillDomain || ''}
/>
)}
{/* DNS VERIFICATION MODAL */}
{selectedListing && (
setSelectedListing(null)}
onVerified={() => { loadListings(); setSelectedListing(null) }}
/>
)}
)
}
// ============================================================================
// LISTING ROW COMPONENT
// ============================================================================
function ListingRow({
listing,
isTycoon,
onDelete,
onVerify,
onPublish,
isDeleting
}: {
listing: Listing
isTycoon: boolean
onDelete: () => void
onVerify: () => void
onPublish: () => void
isDeleting: boolean
}) {
const isDraft = listing.status === 'draft'
const isActive = listing.status === 'active'
const needsVerification = !listing.is_verified
return (
{/* Mobile */}
{listing.is_verified ?
:
}
{listing.domain}
{isTycoon && Featured}
{listing.status}
${listing.asking_price?.toLocaleString() || 'Make Offer'}
{listing.view_count} views ยท {listing.inquiry_count} leads
{isDraft && needsVerification && (
)}
{isDraft && !needsVerification && (
)}
{isActive && (
View
)}
{/* Desktop */}
{listing.is_verified ?
:
}
{listing.domain}
{isTycoon && Featured}
{!listing.is_verified && Unverified}
${listing.asking_price?.toLocaleString() || 'โ'}
{listing.status}
{listing.view_count}
{listing.inquiry_count}
{isDraft && needsVerification && (
)}
{isDraft && !needsVerification && (
)}
{isActive && (
)}
)
}
// ============================================================================
// CREATE LISTING WIZARD (3-Step Process)
// ============================================================================
function CreateListingWizard({ onClose, onSuccess, prefillDomain }: {
onClose: () => void
onSuccess: () => void
prefillDomain: string
}) {
const [step, setStep] = useState(1)
const [domain, setDomain] = useState(prefillDomain)
const [price, setPrice] = useState('')
const [priceType, setPriceType] = useState<'fixed' | 'negotiable'>('negotiable')
const [loading, setLoading] = useState(false)
const [error, setError] = useState(null)
const [createdListing, setCreatedListing] = useState(null)
const [verificationData, setVerificationData] = useState(null)
const [verifying, setVerifying] = useState(false)
const [verified, setVerified] = useState(false)
// Portfolio domains (for dropdown selection)
const [portfolioDomains, setPortfolioDomains] = useState<{ id: number; domain: string }[]>([])
const [loadingDomains, setLoadingDomains] = useState(true)
useEffect(() => {
const fetchPortfolioDomains = async () => {
setLoadingDomains(true)
try {
const domains = await api.getPortfolio()
// Filter out sold domains
setPortfolioDomains(domains.filter(d => !d.is_sold).map(d => ({ id: d.id, domain: d.domain })))
} catch (err) {
console.error('Failed to load portfolio domains:', err)
} finally {
setLoadingDomains(false)
}
}
fetchPortfolioDomains()
}, [])
// Step 1: Create listing
const handleCreateListing = async () => {
if (!domain.trim()) return
setLoading(true)
setError(null)
try {
const listing = await api.createListing({
domain: domain.trim(),
asking_price: price ? parseFloat(price) : null,
currency: 'USD',
price_type: priceType
})
setCreatedListing(listing)
// Check if domain was already verified in portfolio
if (listing.is_verified) {
// Skip verification step, go directly to publish
setVerified(true)
setStep(3)
} else {
// Start DNS verification
const verification = await api.startDnsVerification(listing.id)
setVerificationData(verification)
setStep(2)
}
} catch (err: any) {
setError(err.message || 'Failed to create listing')
} finally {
setLoading(false)
}
}
// Step 2: Check DNS verification
const handleCheckVerification = async () => {
if (!createdListing) return
setVerifying(true)
try {
const result = await api.checkDnsVerification(createdListing.id)
if (result.verified) {
setVerified(true)
setStep(3)
} else {
setError(result.message || 'DNS record not found yet. Please wait for propagation.')
}
} catch (err: any) {
setError(err.message || 'Verification check failed')
} finally {
setVerifying(false)
}
}
// Step 3: Publish
const handlePublish = async () => {
if (!createdListing) return
setLoading(true)
try {
await api.updateListing(createdListing.id, { status: 'active' })
onSuccess()
} catch (err: any) {
setError(err.message || 'Failed to publish')
} finally {
setLoading(false)
}
}
const copyToClipboard = (text: string) => {
navigator.clipboard.writeText(text)
}
return (
e.stopPropagation()}>
{/* Header */}
New Listing
{/* Step Indicators */}
{[1, 2, 3].map((s) => (
s ? "bg-accent/20 text-accent border-accent/40" :
"bg-white/5 text-white/30 border-white/10"
)}>
{step > s ? : s}
))}
{/* Step Content */}
{error && (
{error}
)}
{/* STEP 1: Domain & Price */}
{step === 1 && (
Enter Domain Details
Step 1 of 3: Set your domain and price
{loadingDomains ? (
) : portfolioDomains.length === 0 ? (
) : (
)}
Only domains from your portfolio can be listed. DNS verification required after listing.
{priceType === 'fixed' && (
setPrice(e.target.value)}
min="0"
className="w-full px-4 py-3 bg-white/5 border border-white/10 text-white text-sm font-mono placeholder:text-white/20 outline-none focus:border-accent/50"
placeholder="Enter price"
/>
)}
)}
{/* STEP 2: DNS Verification */}
{step === 2 && verificationData && (
Verify Ownership
Step 2 of 3: Add a DNS TXT record to prove you own this domain
Add this TXT record to your domain's DNS settings. This proves you control the domain.
{verificationData.dns_record_name}
{verificationData.verification_code}
๐ก DNS changes can take 1-5 minutes to propagate. If verification fails, wait a moment and try again.
)}
{/* STEP 3: Publish */}
{step === 3 && (
Ownership Verified!
Step 3 of 3: Publish your listing to the Pounce Market
{domain}
{price ? `$${parseFloat(price).toLocaleString()}` : 'Make Offer'}
Your listing will appear in the Market Feed with the Pounce Direct badge.
)}
)
}
// ============================================================================
// DNS VERIFICATION MODAL (For existing drafts)
// ============================================================================
function DnsVerificationModal({ listing, onClose, onVerified }: {
listing: Listing
onClose: () => void
onVerified: () => void
}) {
const [verificationData, setVerificationData] = useState(null)
const [loading, setLoading] = useState(true)
const [verifying, setVerifying] = useState(false)
const [error, setError] = useState(null)
useEffect(() => {
loadVerificationData()
}, [listing.id])
const loadVerificationData = async () => {
try {
const data = await api.startDnsVerification(listing.id)
setVerificationData(data)
} catch (err: any) {
setError(err.message || 'Failed to load verification data')
} finally {
setLoading(false)
}
}
const handleCheck = async () => {
setVerifying(true)
setError(null)
try {
const result = await api.checkDnsVerification(listing.id)
if (result.verified) {
onVerified()
} else {
setError(result.message || 'DNS record not found yet')
}
} catch (err: any) {
setError(err.message || 'Check failed')
} finally {
setVerifying(false)
}
}
const copyToClipboard = (text: string) => {
navigator.clipboard.writeText(text)
}
return (
e.stopPropagation()}>
{loading ? (
) : verificationData ? (
{error && (
{error}
)}
{listing.domain}
Add this TXT record to verify ownership
{verificationData.dns_record_name}
{verificationData.verification_code}
) : (
Failed to load verification data
)}
)
}
// ============================================================================
// MOBILE DRAWER
// ============================================================================
function MobileDrawer({ user, tierName, TierIcon, drawerNavSections, onClose, onLogout }: any) {
return (
{drawerNavSections.map((section: any) => (
{section.items.map((item: any) => (
{item.label}
))}
))}
Settings
{user?.is_admin && Admin}
{user?.name || user?.email?.split('@')[0] || 'User'}
{tierName}
{tierName === 'Scout' &&
Upgrade}
)
}