yves.gugger 9acc40b658
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
feat: Complete Watchlist monitoring, Portfolio tracking & Listings marketplace
## Watchlist & Monitoring
-  Automatic domain monitoring based on subscription tier
-  Email alerts when domains become available
-  Health checks (DNS/HTTP/SSL) with caching
-  Expiry warnings for domains <30 days
-  Weekly digest emails
-  Instant alert toggle (optimistic UI updates)
-  Redesigned health check overlays with full details
- 🔒 'Not public' display for .ch/.de domains without public expiry

## Portfolio Management (NEW)
-  Track owned domains with purchase price & date
-  ROI calculation (unrealized & realized)
-  Domain valuation with auto-refresh
-  Renewal date tracking
-  Sale recording with profit calculation
-  List domains for sale directly from portfolio
-  Full portfolio summary dashboard

## Listings / For Sale
-  Renamed from 'Portfolio' to 'For Sale'
-  Fixed listing limits: Scout=0, Trader=5, Tycoon=50
-  Featured badge for Tycoon listings
-  Inquiries modal for sellers
-  Email notifications when buyer inquires
-  Inquiries column in listings table

## Scrapers & Data
-  Added 4 new registrar scrapers (Namecheap, Cloudflare, GoDaddy, Dynadot)
-  Increased scraping frequency to 2x daily (03:00 & 15:00 UTC)
-  Real historical data from database
-  Fixed RDAP/WHOIS for .ch/.de domains
-  Enhanced SSL certificate parsing

## Scheduler Jobs
-  Tiered domain checks (Scout=daily, Trader=hourly, Tycoon=10min)
-  Daily health checks (06:00 UTC)
-  Weekly expiry warnings (Mon 08:00 UTC)
-  Weekly digest emails (Sun 10:00 UTC)
-  Auction cleanup every 15 minutes

## UI/UX Improvements
-  Removed 'Back' buttons from Intel pages
-  Redesigned Radar page to match Market/Intel design
-  Less prominent check frequency footer
-  Consistent StatCard components across all pages
-  Ambient background glows
-  Better error handling

## Documentation
-  Updated README with monitoring section
-  Added env.example with all required variables
-  Updated Memory Bank (activeContext.md)
-  SMTP configuration requirements documented
2025-12-11 16:57:28 +01:00

431 lines
20 KiB
TypeScript

'use client'
import { useEffect, useState, memo } from 'react'
import { useParams } from 'next/navigation'
import { api } from '@/lib/api'
import { Header } from '@/components/Header'
import { Footer } from '@/components/Footer'
import {
Shield,
CheckCircle,
Clock,
DollarSign,
Mail,
User,
Building,
Phone,
MessageSquare,
Send,
Loader2,
AlertCircle,
Sparkles,
TrendingUp,
Globe,
Calendar,
ExternalLink,
ShieldCheck,
Lock,
ArrowRight,
Check,
Info
} 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: 'bid' | 'fixed' | 'negotiable'
pounce_score: number | null
estimated_value: number | null
is_verified: boolean
allow_offers: boolean
public_url: string
seller_verified: boolean
seller_member_since: string | null
status: string
}
// Tooltip Component
const Tooltip = memo(({ children, content }: { children: React.ReactNode; content: string }) => (
<div className="relative flex items-center group/tooltip w-fit">
{children}
<div className="absolute bottom-full left-1/2 -translate-x-1/2 mb-2 px-2 py-1 bg-zinc-900 border border-zinc-800 rounded text-[10px] text-zinc-300 whitespace-nowrap opacity-0 group-hover/tooltip:opacity-100 transition-opacity pointer-events-none z-50 shadow-xl">
{content}
<div className="absolute top-full left-1/2 -translate-x-1/2 -mt-px border-4 border-transparent border-t-zinc-800" />
</div>
</div>
))
Tooltip.displayName = 'Tooltip'
export default function BuyDomainPage() {
const params = useParams()
const slug = params.slug as string
const [listing, setListing] = useState<Listing | null>(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
// Inquiry form state
const [submitting, setSubmitting] = useState(false)
const [submitted, setSubmitted] = useState(false)
const [formData, setFormData] = useState({
name: '',
email: '',
phone: '',
company: '',
message: '',
offer_amount: '',
})
useEffect(() => {
loadListing()
}, [slug])
const loadListing = async () => {
setLoading(true)
setError(null)
try {
const data = await api.request<Listing>(`/listings/${slug}`)
setListing(data)
} catch (err: any) {
setError(err.message || 'Listing not found')
} finally {
setLoading(false)
}
}
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
setSubmitting(true)
try {
await api.request(`/listings/${slug}/inquire`, {
method: 'POST',
body: JSON.stringify({
...formData,
offer_amount: formData.offer_amount ? parseFloat(formData.offer_amount) : null,
}),
})
setSubmitted(true)
} catch (err: any) {
setError(err.message || 'Failed to submit inquiry')
} finally {
setSubmitting(false)
}
}
const formatPrice = (price: number, currency: string) => {
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency,
minimumFractionDigits: 0,
maximumFractionDigits: 0,
}).format(price)
}
const getScoreColor = (score: number) => {
if (score >= 80) return 'text-emerald-400'
if (score >= 60) return 'text-amber-400'
return 'text-zinc-500'
}
if (loading) {
return (
<div className="min-h-screen bg-black flex flex-col items-center justify-center relative overflow-hidden">
<div className="absolute inset-0 bg-[radial-gradient(circle_at_center,_var(--tw-gradient-stops))] from-zinc-900/50 via-black to-black" />
<Loader2 className="w-8 h-8 text-emerald-500 animate-spin relative z-10" />
</div>
)
}
if (error || !listing) {
return (
<div className="min-h-screen bg-black text-white font-sans selection:bg-emerald-500/30">
<Header />
<main className="min-h-[70vh] flex items-center justify-center relative px-4">
{/* Background Grid */}
<div className="absolute inset-0 bg-[linear-gradient(to_right,#80808012_1px,transparent_1px),linear-gradient(to_bottom,#80808012_1px,transparent_1px)] bg-[size:24px_24px] pointer-events-none" />
<div className="max-w-md w-full text-center relative z-10">
<div className="w-20 h-20 bg-zinc-900 rounded-2xl flex items-center justify-center mx-auto mb-6 border border-zinc-800 shadow-2xl rotate-3">
<AlertCircle className="w-10 h-10 text-zinc-500" />
</div>
<h1 className="text-3xl font-bold text-white mb-3 tracking-tight">Domain Unavailable</h1>
<p className="text-zinc-500 mb-8 leading-relaxed">
The domain you are looking for has been sold, removed, or is temporarily unavailable.
</p>
<Link
href="/auctions"
className="inline-flex items-center gap-2 px-8 py-4 bg-white text-black font-bold rounded-full hover:bg-zinc-200 transition-all hover:scale-105"
>
Browse Marketplace
<ArrowRight className="w-4 h-4" />
</Link>
</div>
</main>
<Footer />
</div>
)
}
return (
<div className="min-h-screen bg-black text-white font-sans selection:bg-emerald-500/30">
<Header />
{/* Hero Section */}
<main className="relative pt-32 pb-20 px-4 sm:px-6 lg:px-8">
{/* Cinematic Background */}
<div className="fixed inset-0 pointer-events-none">
<div className="absolute top-0 left-1/2 -translate-x-1/2 w-full h-[1000px] bg-[radial-gradient(ellipse_at_top,_var(--tw-gradient-stops))] from-zinc-900/50 via-black to-black" />
<div className="absolute top-0 inset-x-0 h-px bg-gradient-to-r from-transparent via-emerald-500/30 to-transparent" />
<div className="absolute inset-0 bg-[linear-gradient(to_right,#80808008_1px,transparent_1px),linear-gradient(to_bottom,#80808008_1px,transparent_1px)] bg-[size:48px_48px] mask-image-gradient-to-b" />
</div>
<div className="max-w-7xl mx-auto relative z-10">
{/* Top Label */}
<div className="flex justify-center mb-8 sm:mb-10">
<div className="inline-flex items-center gap-2 px-4 py-2 rounded-full border border-emerald-500/20 bg-emerald-500/5 text-emerald-400 text-sm font-bold uppercase tracking-widest shadow-[0_0_20px_rgba(16,185,129,0.2)]">
<ShieldCheck className="w-4 h-4" />
Verified Listing
</div>
</div>
{/* Domain Name */}
<div className="text-center mb-16 sm:mb-24 relative max-w-5xl mx-auto">
<h1 className="font-display text-[2.5rem] sm:text-[4rem] md:text-[5rem] lg:text-[7rem] leading-[0.9] tracking-[-0.03em] text-white drop-shadow-2xl break-words">
{listing.domain}
</h1>
{listing.title && (
<p className="mt-6 sm:mt-8 text-xl sm:text-2xl md:text-3xl text-zinc-400 max-w-3xl mx-auto font-light leading-relaxed">
{listing.title}
</p>
)}
</div>
<div className="grid lg:grid-cols-12 gap-12 lg:gap-24 items-start">
{/* Left Column: Details & Stats */}
<div className="lg:col-span-7 space-y-12">
{/* Description */}
{listing.description && (
<div className="prose prose-invert prose-lg max-w-none">
<h3 className="text-2xl font-bold text-white mb-4">About this Asset</h3>
<p className="text-zinc-400 leading-relaxed text-lg whitespace-pre-line">
{listing.description}
</p>
</div>
)}
{/* Stats Grid */}
<div className="grid sm:grid-cols-2 gap-4">
<div className="p-6 rounded-2xl bg-zinc-900/30 border border-white/5 backdrop-blur-sm relative overflow-hidden group">
<div className="absolute inset-0 bg-gradient-to-br from-emerald-500/5 to-transparent opacity-0 group-hover:opacity-100 transition-opacity" />
<div className="relative z-10">
<div className="flex items-center gap-3 mb-4">
<div className="w-10 h-10 rounded-full bg-emerald-500/10 flex items-center justify-center text-emerald-400">
<Sparkles className="w-5 h-5" />
</div>
<span className="text-sm font-bold text-zinc-500 uppercase tracking-wider">Pounce Score</span>
</div>
<div className="flex items-baseline gap-2">
<span className={clsx("text-4xl font-bold", getScoreColor(listing.pounce_score || 0))}>
{listing.pounce_score || 'N/A'}
</span>
<span className="text-lg text-zinc-600">/100</span>
</div>
<p className="mt-2 text-xs text-zinc-500">Based on length, TLD, and market demand.</p>
</div>
</div>
<div className="p-6 rounded-2xl bg-zinc-900/30 border border-white/5 backdrop-blur-sm flex flex-col justify-center relative overflow-hidden group">
<div className="absolute inset-0 bg-gradient-to-br from-blue-500/5 to-transparent opacity-0 group-hover:opacity-100 transition-opacity" />
<div className="relative z-10">
<div className="flex items-center gap-3 mb-4">
<div className="w-10 h-10 rounded-full bg-blue-500/10 flex items-center justify-center text-blue-400">
<TrendingUp className="w-5 h-5" />
</div>
<span className="text-sm font-bold text-zinc-500 uppercase tracking-wider">Est. Value</span>
</div>
<div className="flex items-baseline gap-2">
<span className="text-4xl font-bold text-white">
{listing.estimated_value ? formatPrice(listing.estimated_value, listing.currency) : '—'}
</span>
</div>
<p className="mt-2 text-xs text-zinc-500">Automated AI valuation estimate.</p>
</div>
</div>
</div>
{/* Trust Section */}
<div className="pt-8 border-t border-white/5">
<h3 className="text-lg font-bold text-white mb-6">Secure Transfer Guarantee</h3>
<div className="grid sm:grid-cols-3 gap-6">
<div className="flex flex-col gap-3">
<div className="w-8 h-8 rounded-lg bg-zinc-900 flex items-center justify-center text-zinc-400 border border-white/5">
<Lock className="w-4 h-4" />
</div>
<div>
<h4 className="font-bold text-white text-sm">Escrow Service</h4>
<p className="text-xs text-zinc-500 mt-1">Funds held securely until transfer is complete.</p>
</div>
</div>
<div className="flex flex-col gap-3">
<div className="w-8 h-8 rounded-lg bg-zinc-900 flex items-center justify-center text-zinc-400 border border-white/5">
<Shield className="w-4 h-4" />
</div>
<div>
<h4 className="font-bold text-white text-sm">Verified Owner</h4>
<p className="text-xs text-zinc-500 mt-1">Ownership verified via DNS validation.</p>
</div>
</div>
<div className="flex flex-col gap-3">
<div className="w-8 h-8 rounded-lg bg-zinc-900 flex items-center justify-center text-zinc-400 border border-white/5">
<Clock className="w-4 h-4" />
</div>
<div>
<h4 className="font-bold text-white text-sm">Fast Transfer</h4>
<p className="text-xs text-zinc-500 mt-1">Most transfers completed within 24 hours.</p>
</div>
</div>
</div>
</div>
</div>
{/* Right Column: Action Card */}
<div className="lg:col-span-5 relative">
<div className="sticky top-32">
<div className="absolute -inset-1 bg-gradient-to-b from-emerald-500/20 to-blue-500/20 rounded-3xl blur-2xl opacity-50" />
<div className="relative bg-black border border-white/10 rounded-2xl p-8 shadow-2xl overflow-hidden">
{/* Card Shine */}
<div className="absolute top-0 right-0 w-64 h-64 bg-white/5 rounded-full blur-3xl -translate-y-1/2 translate-x-1/2" />
{!submitted ? (
<>
<div className="mb-8">
<p className="text-sm font-medium text-zinc-400 uppercase tracking-widest mb-2">
{listing.price_type === 'fixed' ? 'Buy Now Price' : 'Asking Price'}
</p>
<div className="flex items-baseline gap-2">
{listing.asking_price ? (
<span className="text-5xl font-bold text-white tracking-tight">
{formatPrice(listing.asking_price, listing.currency)}
</span>
) : (
<span className="text-4xl font-bold text-white tracking-tight">Make Offer</span>
)}
{listing.price_type === 'negotiable' && listing.asking_price && (
<span className="px-2 py-1 bg-white/10 rounded text-[10px] font-bold uppercase tracking-wider text-white">
Negotiable
</span>
)}
</div>
</div>
{/* Always Visible Form */}
<form onSubmit={handleSubmit} className="space-y-4 animate-fade-in">
<div className="flex items-center justify-between mb-4">
<h3 className="font-bold text-white text-lg">
{listing.asking_price ? 'Purchase Inquiry' : 'Contact Seller'}
</h3>
</div>
<div className="space-y-3">
<div className="grid grid-cols-2 gap-3">
<input
type="text"
placeholder="Name"
required
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
className="w-full bg-zinc-900/50 border border-white/10 rounded-lg px-4 py-3 text-sm text-white placeholder:text-zinc-600 focus:outline-none focus:border-emerald-500/50 transition-all"
/>
<input
type="email"
placeholder="Email"
required
value={formData.email}
onChange={(e) => setFormData({ ...formData, email: e.target.value })}
className="w-full bg-zinc-900/50 border border-white/10 rounded-lg px-4 py-3 text-sm text-white placeholder:text-zinc-600 focus:outline-none focus:border-emerald-500/50 transition-all"
/>
</div>
<input
type="text"
placeholder="Phone (Optional)"
value={formData.phone}
onChange={(e) => setFormData({ ...formData, phone: e.target.value })}
className="w-full bg-zinc-900/50 border border-white/10 rounded-lg px-4 py-3 text-sm text-white placeholder:text-zinc-600 focus:outline-none focus:border-emerald-500/50 transition-all"
/>
{listing.allow_offers && (
<div className="relative">
<span className="absolute left-4 top-1/2 -translate-y-1/2 text-zinc-500">$</span>
<input
type="number"
placeholder="Your Offer Amount"
value={formData.offer_amount}
onChange={(e) => setFormData({ ...formData, offer_amount: e.target.value })}
className="w-full bg-zinc-900/50 border border-white/10 rounded-lg pl-8 pr-4 py-3 text-sm text-white placeholder:text-zinc-600 focus:outline-none focus:border-emerald-500/50 transition-all"
/>
</div>
)}
<textarea
placeholder="I'm interested in this domain..."
rows={3}
required
value={formData.message}
onChange={(e) => setFormData({ ...formData, message: e.target.value })}
className="w-full bg-zinc-900/50 border border-white/10 rounded-lg px-4 py-3 text-sm text-white placeholder:text-zinc-600 focus:outline-none focus:border-emerald-500/50 transition-all resize-none"
/>
</div>
<button
type="submit"
disabled={submitting}
className="w-full py-4 bg-white text-black font-bold text-lg rounded-xl hover:bg-zinc-200 transition-all disabled:opacity-50 flex items-center justify-center gap-2 mt-6 shadow-lg shadow-white/10"
>
{submitting ? <Loader2 className="w-5 h-5 animate-spin" /> : <Send className="w-5 h-5" />}
{listing.asking_price ? 'Send Purchase Request' : 'Send Offer'}
</button>
<p className="text-center text-xs text-zinc-600 mt-3">
Secure escrow transfer available via Escrow.com
</p>
</form>
</>
) : (
<div className="text-center py-12 animate-fade-in">
<div className="w-16 h-16 bg-emerald-500/10 rounded-full flex items-center justify-center mx-auto mb-4 text-emerald-400">
<Check className="w-8 h-8" />
</div>
<h3 className="text-2xl font-bold text-white mb-2">Inquiry Sent</h3>
<p className="text-zinc-400">The seller has been notified and will contact you shortly.</p>
<button
onClick={() => setSubmitted(false)}
className="mt-6 text-sm text-zinc-500 hover:text-white"
>
Send another message
</button>
</div>
)}
</div>
</div>
</div>
</div>
</div>
</main>
<Footer />
</div>
)
}