fix: Portfolio page redesign, unified DNS verification, fix API route conflicts
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

This commit is contained in:
2025-12-14 21:44:40 +01:00
parent 8051b2ac51
commit 1f72f2664d
23 changed files with 740 additions and 1214 deletions

View File

@ -548,6 +548,10 @@ async def create_listing(
detail="Cannot list a sold domain for sale.", detail="Cannot list a sold domain for sale.",
) )
# Check if domain is DNS verified in portfolio
# If verified in portfolio, listing inherits verification
is_portfolio_verified = getattr(portfolio_domain, 'is_dns_verified', False) or False
# Check if domain is already listed # Check if domain is already listed
existing = await db.execute( existing = await db.execute(
select(DomainListing).where(DomainListing.domain == domain_lower) select(DomainListing).where(DomainListing.domain == domain_lower)
@ -600,6 +604,7 @@ async def create_listing(
estimated_value = None estimated_value = None
# Create listing # Create listing
# If portfolio domain is already DNS verified, listing is auto-verified
listing = DomainListing( listing = DomainListing(
user_id=current_user.id, user_id=current_user.id,
domain=domain_lower, domain=domain_lower,
@ -614,7 +619,9 @@ async def create_listing(
allow_offers=data.allow_offers, allow_offers=data.allow_offers,
pounce_score=pounce_score, pounce_score=pounce_score,
estimated_value=estimated_value, estimated_value=estimated_value,
verification_code=_generate_verification_code(), verification_code=portfolio_domain.verification_code if is_portfolio_verified else _generate_verification_code(),
verification_status=VerificationStatus.VERIFIED.value if is_portfolio_verified else VerificationStatus.PENDING.value,
verified_at=portfolio_domain.verified_at if is_portfolio_verified else None,
status=ListingStatus.DRAFT.value, status=ListingStatus.DRAFT.value,
) )

View File

@ -176,7 +176,112 @@ class ValuationResponse(BaseModel):
disclaimer: str disclaimer: str
# ============== Helper Functions ==============
def _generate_verification_code() -> str:
"""Generate a unique verification code."""
return f"pounce-verify-{secrets.token_hex(8)}"
def _domain_to_response(domain: PortfolioDomain) -> PortfolioDomainResponse:
"""Convert PortfolioDomain to response schema."""
return PortfolioDomainResponse(
id=domain.id,
domain=domain.domain,
purchase_date=domain.purchase_date,
purchase_price=domain.purchase_price,
purchase_registrar=domain.purchase_registrar,
registrar=domain.registrar,
renewal_date=domain.renewal_date,
renewal_cost=domain.renewal_cost,
auto_renew=domain.auto_renew,
estimated_value=domain.estimated_value,
value_updated_at=domain.value_updated_at,
is_sold=domain.is_sold,
sale_date=domain.sale_date,
sale_price=domain.sale_price,
status=domain.status,
notes=domain.notes,
tags=domain.tags,
roi=domain.roi,
is_dns_verified=getattr(domain, 'is_dns_verified', False) or False,
verification_status=getattr(domain, 'verification_status', 'unverified') or 'unverified',
verification_code=getattr(domain, 'verification_code', None),
verified_at=getattr(domain, 'verified_at', None),
created_at=domain.created_at,
updated_at=domain.updated_at,
)
# ============== Portfolio Endpoints ============== # ============== Portfolio Endpoints ==============
# IMPORTANT: Static routes must come BEFORE dynamic routes like /{domain_id}
@router.get("/verified", response_model=List[PortfolioDomainResponse])
async def get_verified_domains(
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
"""
Get only DNS-verified portfolio domains.
These domains can be used for Yield or For Sale listings.
"""
result = await db.execute(
select(PortfolioDomain).where(
and_(
PortfolioDomain.user_id == current_user.id,
PortfolioDomain.is_dns_verified == True,
PortfolioDomain.is_sold == False,
)
).order_by(PortfolioDomain.domain.asc())
)
domains = result.scalars().all()
return [_domain_to_response(d) for d in domains]
@router.get("/summary", response_model=PortfolioSummary)
async def get_portfolio_summary(
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
"""Get portfolio summary statistics."""
result = await db.execute(
select(PortfolioDomain).where(PortfolioDomain.user_id == current_user.id)
)
domains = result.scalars().all()
total_domains = len(domains)
active_domains = sum(1 for d in domains if d.status == "active" and not d.is_sold)
sold_domains = sum(1 for d in domains if d.is_sold)
total_invested = sum(d.purchase_price or 0 for d in domains)
total_value = sum(d.estimated_value or 0 for d in domains if not d.is_sold)
total_sold_value = sum(d.sale_price or 0 for d in domains if d.is_sold)
# Calculate active investment for ROI
active_investment = sum(d.purchase_price or 0 for d in domains if not d.is_sold)
sold_investment = sum(d.purchase_price or 0 for d in domains if d.is_sold)
unrealized_profit = total_value - active_investment
realized_profit = total_sold_value - sold_investment
overall_roi = 0.0
if total_invested > 0:
overall_roi = ((total_value + total_sold_value - total_invested) / total_invested) * 100
return PortfolioSummary(
total_domains=total_domains,
active_domains=active_domains,
sold_domains=sold_domains,
total_invested=round(total_invested, 2),
total_value=round(total_value, 2),
total_sold_value=round(total_sold_value, 2),
unrealized_profit=round(unrealized_profit, 2),
realized_profit=round(realized_profit, 2),
overall_roi=round(overall_roi, 2),
)
@router.get("", response_model=List[PortfolioDomainResponse]) @router.get("", response_model=List[PortfolioDomainResponse])
async def get_portfolio( async def get_portfolio(
@ -242,49 +347,6 @@ async def get_portfolio(
return responses return responses
@router.get("/summary", response_model=PortfolioSummary)
async def get_portfolio_summary(
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
"""Get portfolio summary statistics."""
result = await db.execute(
select(PortfolioDomain).where(PortfolioDomain.user_id == current_user.id)
)
domains = result.scalars().all()
total_domains = len(domains)
active_domains = sum(1 for d in domains if d.status == "active" and not d.is_sold)
sold_domains = sum(1 for d in domains if d.is_sold)
total_invested = sum(d.purchase_price or 0 for d in domains)
total_value = sum(d.estimated_value or 0 for d in domains if not d.is_sold)
total_sold_value = sum(d.sale_price or 0 for d in domains if d.is_sold)
# Calculate active investment for ROI
active_investment = sum(d.purchase_price or 0 for d in domains if not d.is_sold)
sold_investment = sum(d.purchase_price or 0 for d in domains if d.is_sold)
unrealized_profit = total_value - active_investment
realized_profit = total_sold_value - sold_investment
overall_roi = 0.0
if total_invested > 0:
overall_roi = ((total_value + total_sold_value - total_invested) / total_invested) * 100
return PortfolioSummary(
total_domains=total_domains,
active_domains=active_domains,
sold_domains=sold_domains,
total_invested=round(total_invested, 2),
total_value=round(total_value, 2),
total_sold_value=round(total_sold_value, 2),
unrealized_profit=round(unrealized_profit, 2),
realized_profit=round(realized_profit, 2),
overall_roi=round(overall_roi, 2),
)
@router.post("", response_model=PortfolioDomainResponse, status_code=status.HTTP_201_CREATED) @router.post("", response_model=PortfolioDomainResponse, status_code=status.HTTP_201_CREATED)
async def add_portfolio_domain( async def add_portfolio_domain(
data: PortfolioDomainCreate, data: PortfolioDomainCreate,
@ -670,42 +732,6 @@ async def get_domain_valuation(
# ============== DNS Verification Endpoints ============== # ============== DNS Verification Endpoints ==============
def _generate_verification_code() -> str:
"""Generate a unique verification code."""
return f"pounce-verify-{secrets.token_hex(8)}"
def _domain_to_response(domain: PortfolioDomain) -> PortfolioDomainResponse:
"""Convert PortfolioDomain to response schema."""
return PortfolioDomainResponse(
id=domain.id,
domain=domain.domain,
purchase_date=domain.purchase_date,
purchase_price=domain.purchase_price,
purchase_registrar=domain.purchase_registrar,
registrar=domain.registrar,
renewal_date=domain.renewal_date,
renewal_cost=domain.renewal_cost,
auto_renew=domain.auto_renew,
estimated_value=domain.estimated_value,
value_updated_at=domain.value_updated_at,
is_sold=domain.is_sold,
sale_date=domain.sale_date,
sale_price=domain.sale_price,
status=domain.status,
notes=domain.notes,
tags=domain.tags,
roi=domain.roi,
# Use getattr with defaults for new fields that may not exist in DB yet
is_dns_verified=getattr(domain, 'is_dns_verified', False) or False,
verification_status=getattr(domain, 'verification_status', 'unverified') or 'unverified',
verification_code=getattr(domain, 'verification_code', None),
verified_at=getattr(domain, 'verified_at', None),
created_at=domain.created_at,
updated_at=domain.updated_at,
)
@router.post("/{domain_id}/verify-dns", response_model=DNSVerificationStartResponse) @router.post("/{domain_id}/verify-dns", response_model=DNSVerificationStartResponse)
async def start_dns_verification( async def start_dns_verification(
domain_id: int, domain_id: int,
@ -860,27 +886,3 @@ async def check_dns_verification(
message=f"TXT record found but value doesn't match. Expected: {domain.verification_code}", message=f"TXT record found but value doesn't match. Expected: {domain.verification_code}",
) )
@router.get("/verified", response_model=List[PortfolioDomainResponse])
async def get_verified_domains(
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
"""
Get only DNS-verified portfolio domains.
These domains can be used for Yield or For Sale listings.
"""
result = await db.execute(
select(PortfolioDomain).where(
and_(
PortfolioDomain.user_id == current_user.id,
PortfolioDomain.is_dns_verified == True,
PortfolioDomain.is_sold == False,
)
).order_by(PortfolioDomain.domain.asc())
)
domains = result.scalars().all()
return [_domain_to_response(d) for d in domains]

View File

@ -143,7 +143,7 @@ export default function AboutPage() {
className="px-8 py-4 bg-accent text-black text-xs font-bold uppercase tracking-[0.2em] hover:bg-white transition-all shadow-[0_0_20px_rgba(16,185,129,0.2)]" className="px-8 py-4 bg-accent text-black text-xs font-bold uppercase tracking-[0.2em] hover:bg-white transition-all shadow-[0_0_20px_rgba(16,185,129,0.2)]"
style={{ clipPath: 'polygon(10px 0, 100% 0, 100% 100%, 0 100%, 0 10px)' }} style={{ clipPath: 'polygon(10px 0, 100% 0, 100% 100%, 0 100%, 0 10px)' }}
> >
Start Hunting Enter Terminal
</Link> </Link>
<Link <Link
href="/contact" href="/contact"

View File

@ -4,7 +4,7 @@ const siteUrl = process.env.NEXT_PUBLIC_SITE_URL || 'https://pounce.com'
export const marketMetadata: Metadata = { export const marketMetadata: Metadata = {
title: 'Live Domain Market - Auctions, Drops & Premium Domains', title: 'Live Domain Market - Auctions, Drops & Premium Domains',
description: 'Real-time domain marketplace aggregating auctions from GoDaddy, Sedo, Dynadot, and premium domain listings. Spam-filtered feed. Search 100,000+ domains. Buy, sell, or bid now.', description: 'Live domain marketplace aggregating auctions and Pounce Direct listings. Cut noise with filters. Pounce Direct owners are DNS-verified and sell with 0% commission.',
keywords: [ keywords: [
'domain marketplace', 'domain marketplace',
'domain auctions', 'domain auctions',
@ -21,7 +21,7 @@ export const marketMetadata: Metadata = {
], ],
openGraph: { openGraph: {
title: 'Live Domain Market - Pounce', title: 'Live Domain Market - Pounce',
description: 'Real-time domain marketplace. Auctions, drops, and premium listings. Spam-filtered. Search 100,000+ domains.', description: 'Live domain marketplace. Auctions and Pounce Direct listings. Filters, pricing intel, and verified sellers.',
url: `${siteUrl}/market`, url: `${siteUrl}/market`,
type: 'website', type: 'website',
images: [ images: [
@ -36,7 +36,7 @@ export const marketMetadata: Metadata = {
twitter: { twitter: {
card: 'summary_large_image', card: 'summary_large_image',
title: 'Live Domain Market - Pounce', title: 'Live Domain Market - Pounce',
description: 'Real-time domain marketplace. Auctions, drops, and premium listings. Spam-filtered.', description: 'Live domain marketplace. Auctions and Pounce Direct listings. Filters and verified sellers.',
images: [`${siteUrl}/og-market.png`], images: [`${siteUrl}/og-market.png`],
}, },
alternates: { alternates: {
@ -52,7 +52,7 @@ export function getMarketStructuredData() {
'@context': 'https://schema.org', '@context': 'https://schema.org',
'@type': 'CollectionPage', '@type': 'CollectionPage',
name: 'Live Domain Market', name: 'Live Domain Market',
description: 'Real-time domain marketplace aggregating auctions and premium listings', description: 'Live domain marketplace aggregating auctions and verified listings',
url: `${siteUrl}/market`, url: `${siteUrl}/market`,
breadcrumb: { breadcrumb: {
'@type': 'BreadcrumbList', '@type': 'BreadcrumbList',

View File

@ -457,8 +457,8 @@ export default function AcquirePage() {
Acquire Assets. Acquire Assets.
</h1> </h1>
<p className="mt-6 text-lg text-white/50 max-w-2xl font-light leading-relaxed"> <p className="mt-6 text-lg text-white/50 max-w-2xl font-light leading-relaxed">
Global liquidity pool. Verified assets only. External auctions + Pounce Direct listings.
<span className="block mt-2 text-white/80">Aggregated from GoDaddy, Sedo, and Pounce Direct.</span> <span className="block mt-2 text-white/80">Pounce Direct owners are DNS-verified. External auctions are sourced from GoDaddy, Sedo, and other partners.</span>
</p> </p>
</div> </div>
<div className="grid grid-cols-3 gap-8 text-right"> <div className="grid grid-cols-3 gap-8 text-right">

View File

@ -47,6 +47,9 @@ export default function BlogPage() {
const [loading, setLoading] = useState(true) const [loading, setLoading] = useState(true)
const [selectedCategory, setSelectedCategory] = useState<string | null>(null) const [selectedCategory, setSelectedCategory] = useState<string | null>(null)
const [total, setTotal] = useState(0) const [total, setTotal] = useState(0)
const [newsletterEmail, setNewsletterEmail] = useState('')
const [newsletterState, setNewsletterState] = useState<'idle' | 'loading' | 'success' | 'error'>('idle')
const [newsletterError, setNewsletterError] = useState<string | null>(null)
useEffect(() => { useEffect(() => {
loadBlogData() loadBlogData()
@ -237,7 +240,7 @@ export default function BlogPage() {
</div> </div>
<span className="inline-flex items-center gap-2 text-accent font-medium group-hover:gap-4 transition-all duration-300"> <span className="inline-flex items-center gap-2 text-accent font-medium group-hover:gap-4 transition-all duration-300">
Read Article Full briefing
<ArrowRight className="w-5 h-5" /> <ArrowRight className="w-5 h-5" />
</span> </span>
</div> </div>
@ -326,7 +329,7 @@ export default function BlogPage() {
}} }}
className="group inline-flex items-center gap-3 px-8 py-4 bg-background-secondary/50 border border-border rounded-full text-foreground font-medium hover:border-accent/50 hover:bg-accent/5 transition-all duration-300" className="group inline-flex items-center gap-3 px-8 py-4 bg-background-secondary/50 border border-border rounded-full text-foreground font-medium hover:border-accent/50 hover:bg-accent/5 transition-all duration-300"
> >
Load More Articles Load more briefings
<ArrowRight className="w-4 h-4 group-hover:translate-x-1 transition-transform" /> <ArrowRight className="w-4 h-4 group-hover:translate-x-1 transition-transform" />
</button> </button>
</div> </div>
@ -339,21 +342,50 @@ export default function BlogPage() {
<div className="absolute inset-0 bg-gradient-to-r from-accent/10 via-accent/5 to-accent/10 rounded-3xl blur-xl" /> <div className="absolute inset-0 bg-gradient-to-r from-accent/10 via-accent/5 to-accent/10 rounded-3xl blur-xl" />
<div className="relative p-10 sm:p-14 bg-background-secondary/50 backdrop-blur-sm border border-accent/20 rounded-3xl text-center"> <div className="relative p-10 sm:p-14 bg-background-secondary/50 backdrop-blur-sm border border-accent/20 rounded-3xl text-center">
<h3 className="text-2xl sm:text-3xl font-display text-foreground mb-4"> <h3 className="text-2xl sm:text-3xl font-display text-foreground mb-4">
Get hunting tips in your inbox Mission Briefings
</h3> </h3>
<p className="text-foreground-muted mb-8 max-w-xl mx-auto"> <p className="text-foreground-muted mb-8 max-w-xl mx-auto">
Join domain hunters who receive weekly insights, market trends, and exclusive strategies. Weekly intel on drops, pricing traps, and market moves. No spam.
</p> </p>
<div className="flex flex-col sm:flex-row gap-4 justify-center max-w-md mx-auto"> <form
className="flex flex-col sm:flex-row gap-4 justify-center max-w-md mx-auto"
onSubmit={async (e) => {
e.preventDefault()
const email = newsletterEmail.trim()
if (!email || !email.includes('@') || !email.includes('.')) return
if (newsletterState === 'loading') return
setNewsletterState('loading')
setNewsletterError(null)
try {
await api.subscribeNewsletter(email)
setNewsletterState('success')
} catch (err: any) {
setNewsletterState('error')
setNewsletterError(err?.message || 'Subscription failed. Retry.')
}
}}
>
<input <input
type="email" type="email"
placeholder="Enter your email" value={newsletterEmail}
onChange={(e) => setNewsletterEmail(e.target.value)}
placeholder="Email address"
className="flex-1 px-5 py-3.5 bg-background border border-border rounded-xl text-foreground placeholder:text-foreground-subtle focus:outline-none focus:border-accent/50 transition-colors" className="flex-1 px-5 py-3.5 bg-background border border-border rounded-xl text-foreground placeholder:text-foreground-subtle focus:outline-none focus:border-accent/50 transition-colors"
/> />
<button className="px-8 py-3.5 bg-accent text-background rounded-xl font-medium hover:bg-accent-hover transition-colors whitespace-nowrap"> <button
Subscribe type="submit"
disabled={newsletterState === 'loading'}
className="px-8 py-3.5 bg-accent text-background rounded-xl font-medium hover:bg-accent-hover transition-colors whitespace-nowrap disabled:opacity-60 disabled:cursor-not-allowed"
>
{newsletterState === 'loading' ? 'Subscribing…' : 'Subscribe'}
</button> </button>
</div> </form>
{newsletterState === 'success' ? (
<p className="mt-4 text-sm text-accent">Subscribed. Briefings inbound.</p>
) : newsletterState === 'error' ? (
<p className="mt-4 text-sm text-red-500">{newsletterError}</p>
) : null}
</div> </div>
</div> </div>
</div> </div>

View File

@ -11,22 +11,22 @@ const contactMethods = [
{ {
icon: Mail, icon: Mail,
title: 'Secure Comms', title: 'Secure Comms',
description: 'Encrypted channel for general inquiries.', description: 'Support, partnerships, and product intel.',
value: 'hello@pounce.ch', value: 'hello@pounce.ch',
href: 'mailto:hello@pounce.ch', href: 'mailto:hello@pounce.ch',
}, },
{ {
icon: MessageSquare, icon: MessageSquare,
title: 'Live Support', title: 'Support Desk',
description: 'Mon-Fri, 0900-1800 CET', description: 'Email support. MonFri, 09001800 CET.',
value: 'Open Channel', value: 'Send ticket',
href: '#', href: 'mailto:hello@pounce.ch?subject=Support',
}, },
{ {
icon: Clock, icon: Clock,
title: 'Response Time', title: 'Response Time',
description: 'Average ticket resolution.', description: 'Typical response window.',
value: '< 4 Hours', value: '< 24 Hours',
href: null, href: null,
}, },
] ]
@ -110,7 +110,7 @@ export default function ContactPage() {
Establish Contact. Establish Contact.
</h1> </h1>
<p className="text-lg text-white/50 max-w-xl mx-auto font-light"> <p className="text-lg text-white/50 max-w-xl mx-auto font-light">
Question? Idea? Glitch in the matrix? We're listening. Support request, bug report, or partnership intel. We&apos;ll respond fast.
</p> </p>
</div> </div>

View File

@ -6,6 +6,7 @@ import Script from 'next/script'
const inter = Inter({ subsets: ['latin'] }) const inter = Inter({ subsets: ['latin'] })
const siteUrl = process.env.NEXT_PUBLIC_SITE_URL || 'https://pounce.ch' const siteUrl = process.env.NEXT_PUBLIC_SITE_URL || 'https://pounce.ch'
const googleSiteVerification = process.env.NEXT_PUBLIC_GOOGLE_SITE_VERIFICATION
export const viewport: Viewport = { export const viewport: Viewport = {
width: 'device-width', width: 'device-width',
@ -19,7 +20,7 @@ export const metadata: Metadata = {
default: 'Pounce - Domain Intelligence Platform | Find, Track & Trade Domains', default: 'Pounce - Domain Intelligence Platform | Find, Track & Trade Domains',
template: '%s | Pounce', template: '%s | Pounce',
}, },
description: 'The #1 domain intelligence platform. Real-time auction aggregation from GoDaddy, Sedo, DropCatch & more. TLD price tracking, spam-free market feed, and portfolio management. Find undervalued domains before anyone else.', description: 'Domain intelligence for serious investors. Scan live auctions, track drops, compare TLD pricing, and manage portfolios with a clean market feed and verified listings.',
keywords: [ keywords: [
'domain intelligence', 'domain intelligence',
'domain auctions', 'domain auctions',
@ -65,7 +66,7 @@ export const metadata: Metadata = {
url: siteUrl, url: siteUrl,
siteName: 'Pounce', siteName: 'Pounce',
title: 'Pounce - Domain Intelligence Platform | Find, Track & Trade Domains', title: 'Pounce - Domain Intelligence Platform | Find, Track & Trade Domains',
description: 'The #1 domain intelligence platform. Real-time auction aggregation, TLD price tracking, spam-free market feed. Find undervalued domains before anyone else.', description: 'Domain intelligence for serious investors. Live auctions, TLD pricing intel, a clean market feed, and verified listings.',
images: [ images: [
{ {
url: `${siteUrl}/og-image.png`, url: `${siteUrl}/og-image.png`,
@ -78,14 +79,12 @@ export const metadata: Metadata = {
twitter: { twitter: {
card: 'summary_large_image', card: 'summary_large_image',
title: 'Pounce - Domain Intelligence Platform', title: 'Pounce - Domain Intelligence Platform',
description: 'The #1 domain intelligence platform. Real-time auctions, TLD pricing, spam-free feed. Find undervalued domains.', description: 'Domain intelligence for serious investors. Live auctions, TLD pricing intel, a clean market feed, and verified listings.',
creator: '@pouncedomains', creator: '@pouncedomains',
site: '@pouncedomains', site: '@pouncedomains',
images: [`${siteUrl}/og-image.png`], images: [`${siteUrl}/og-image.png`],
}, },
verification: { verification: googleSiteVerification ? { google: googleSiteVerification } : undefined,
google: 'YOUR_GOOGLE_VERIFICATION_CODE', // Add your Google Search Console verification
},
robots: { robots: {
index: true, index: true,
follow: true, follow: true,
@ -139,7 +138,7 @@ export default function RootLayout({
], ],
contactPoint: { contactPoint: {
'@type': 'ContactPoint', '@type': 'ContactPoint',
email: 'hello@pounce.com', email: 'hello@pounce.ch',
contactType: 'Customer Service', contactType: 'Customer Service',
}, },
}), }),

View File

@ -4,7 +4,7 @@ const siteUrl = process.env.NEXT_PUBLIC_SITE_URL || 'https://pounce.com'
export const homeMetadata: Metadata = { export const homeMetadata: Metadata = {
title: 'Pounce - Domain Intelligence for Investors | The Market Never Sleeps', title: 'Pounce - Domain Intelligence for Investors | The Market Never Sleeps',
description: 'Domain intelligence platform for investors. Real-time drops, spam-filtered auctions, TLD price tracking, portfolio monitoring. Scout, track, and trade premium domains. 0% marketplace commission.', description: 'Domain intelligence for investors. Live auctions, drops, TLD price tracking, and portfolio monitoring with a clean market feed and verified listings. 0% marketplace commission.',
keywords: [ keywords: [
'domain intelligence', 'domain intelligence',
'domain marketplace', 'domain marketplace',
@ -23,7 +23,7 @@ export const homeMetadata: Metadata = {
], ],
openGraph: { openGraph: {
title: 'Pounce - Domain Intelligence for Investors', title: 'Pounce - Domain Intelligence for Investors',
description: 'The market never sleeps. You should. Real-time domain intelligence, auctions, and market data.', description: 'The market never sleeps. You should. Live domain intelligence, auctions, and market data.',
url: siteUrl, url: siteUrl,
type: 'website', type: 'website',
images: [ images: [
@ -38,7 +38,7 @@ export const homeMetadata: Metadata = {
twitter: { twitter: {
card: 'summary_large_image', card: 'summary_large_image',
title: 'Pounce - Domain Intelligence for Investors', title: 'Pounce - Domain Intelligence for Investors',
description: 'The market never sleeps. You should. Real-time domain intelligence.', description: 'The market never sleeps. You should. Live domain intelligence.',
images: [`${siteUrl}/og-image.png`], images: [`${siteUrl}/og-image.png`],
}, },
alternates: { alternates: {
@ -66,7 +66,7 @@ export function getHomeStructuredData() {
], ],
contactPoint: { contactPoint: {
'@type': 'ContactPoint', '@type': 'ContactPoint',
email: 'hello@pounce.com', email: 'hello@pounce.ch',
contactType: 'Customer Service', contactType: 'Customer Service',
availableLanguage: ['en'], availableLanguage: ['en'],
}, },
@ -108,7 +108,7 @@ export function getHomeStructuredData() {
worstRating: '1', worstRating: '1',
}, },
featureList: [ featureList: [
'Real-time domain monitoring', 'Live domain monitoring',
'Spam-filtered auction feed', 'Spam-filtered auction feed',
'TLD price intelligence', 'TLD price intelligence',
'Portfolio management', 'Portfolio management',

View File

@ -4,7 +4,7 @@ const siteUrl = process.env.NEXT_PUBLIC_SITE_URL || 'https://pounce.com'
export const pricingMetadata: Metadata = { export const pricingMetadata: Metadata = {
title: 'Pricing Plans - Domain Intelligence & Market Access', title: 'Pricing Plans - Domain Intelligence & Market Access',
description: 'Choose your domain intelligence plan. Scout (Free), Trader ($9/mo), or Tycoon ($29/mo). Real-time market data, spam-filtered auctions, and portfolio monitoring. 0% commission on marketplace sales.', description: 'Choose your domain intelligence plan. Scout (Free), Trader ($9/mo), or Tycoon ($29/mo). Live market data, spam-filtered auctions, and portfolio monitoring. 0% commission on Pounce Direct sales.',
keywords: [ keywords: [
'domain intelligence pricing', 'domain intelligence pricing',
'domain monitoring subscription', 'domain monitoring subscription',
@ -17,7 +17,7 @@ export const pricingMetadata: Metadata = {
], ],
openGraph: { openGraph: {
title: 'Pricing Plans - Pounce Domain Intelligence', title: 'Pricing Plans - Pounce Domain Intelligence',
description: 'Scout (Free), Trader ($9/mo), Tycoon ($29/mo). Real-time market data, spam-filtered auctions, portfolio monitoring.', description: 'Scout (Free), Trader ($9/mo), Tycoon ($29/mo). Live market data, spam-filtered auctions, and portfolio monitoring.',
url: `${siteUrl}/pricing`, url: `${siteUrl}/pricing`,
type: 'website', type: 'website',
images: [ images: [
@ -32,7 +32,7 @@ export const pricingMetadata: Metadata = {
twitter: { twitter: {
card: 'summary_large_image', card: 'summary_large_image',
title: 'Pricing Plans - Pounce Domain Intelligence', title: 'Pricing Plans - Pounce Domain Intelligence',
description: 'Scout (Free), Trader ($9/mo), Tycoon ($29/mo). Start hunting domains today.', description: 'Scout (Free), Trader ($9/mo), Tycoon ($29/mo). Clean market feed, faster monitoring, and verified listings.',
images: [`${siteUrl}/og-pricing.png`], images: [`${siteUrl}/og-pricing.png`],
}, },
alternates: { alternates: {

View File

@ -17,7 +17,7 @@ const tiers = [
icon: Zap, icon: Zap,
price: '0', price: '0',
period: '', period: '',
description: 'Test the waters. Zero risk.', description: 'Recon access. No commitment.',
features: [ features: [
{ text: 'Market Feed', highlight: false, available: true, sublabel: 'Raw' }, { text: 'Market Feed', highlight: false, available: true, sublabel: 'Raw' },
{ text: 'Alert Speed', highlight: false, available: true, sublabel: 'Daily' }, { text: 'Alert Speed', highlight: false, available: true, sublabel: 'Daily' },
@ -28,7 +28,7 @@ const tiers = [
{ text: 'Marketplace', highlight: false, available: true, sublabel: 'Buy Only' }, { text: 'Marketplace', highlight: false, available: true, sublabel: 'Buy Only' },
{ text: 'Yield (Intent Routing)', highlight: false, available: false }, { text: 'Yield (Intent Routing)', highlight: false, available: false },
], ],
cta: 'Start Free', cta: 'Enter Terminal',
highlighted: false, highlighted: false,
badge: null, badge: null,
isPaid: false, isPaid: false,
@ -39,7 +39,7 @@ const tiers = [
icon: TrendingUp, icon: TrendingUp,
price: '9', price: '9',
period: '/mo', period: '/mo',
description: 'The smart investor\'s choice.', description: 'Cut noise. Move faster.',
features: [ features: [
{ text: 'Market Feed', highlight: true, available: true, sublabel: 'Curated' }, { text: 'Market Feed', highlight: true, available: true, sublabel: 'Curated' },
{ text: 'Alert Speed', highlight: true, available: true, sublabel: 'Hourly' }, { text: 'Alert Speed', highlight: true, available: true, sublabel: 'Hourly' },
@ -62,10 +62,10 @@ const tiers = [
icon: Crown, icon: Crown,
price: '29', price: '29',
period: '/mo', period: '/mo',
description: 'For serious domain investors.', description: 'Full firepower. Priority routes.',
features: [ features: [
{ text: 'Market Feed', highlight: true, available: true, sublabel: 'Priority' }, { text: 'Market Feed', highlight: true, available: true, sublabel: 'Priority' },
{ text: 'Alert Speed', highlight: true, available: true, sublabel: 'Real-Time' }, { text: 'Alert Speed', highlight: true, available: true, sublabel: '10 min' },
{ text: '500 Watchlist Domains', highlight: true, available: true }, { text: '500 Watchlist Domains', highlight: true, available: true },
{ text: '50 Sniper Alerts', highlight: true, available: true }, { text: '50 Sniper Alerts', highlight: true, available: true },
{ text: 'TLD Intel', highlight: true, available: true, sublabel: 'Full History' }, { text: 'TLD Intel', highlight: true, available: true, sublabel: 'Full History' },
@ -83,7 +83,7 @@ const tiers = [
const comparisonFeatures = [ const comparisonFeatures = [
{ name: 'Market Feed', scout: 'Raw (Unfiltered)', trader: 'Curated (Spam-Free)', tycoon: 'Curated + Priority' }, { name: 'Market Feed', scout: 'Raw (Unfiltered)', trader: 'Curated (Spam-Free)', tycoon: 'Curated + Priority' },
{ name: 'Alert Speed', scout: 'Daily', trader: 'Hourly', tycoon: 'Real-Time (10 min)' }, { name: 'Alert Speed', scout: 'Daily', trader: 'Hourly', tycoon: 'Every 10 minutes' },
{ name: 'Watchlist', scout: '5 Domains', trader: '50 Domains', tycoon: '500 Domains' }, { name: 'Watchlist', scout: '5 Domains', trader: '50 Domains', tycoon: '500 Domains' },
{ name: 'Sniper Alerts', scout: '2', trader: '10', tycoon: '50' }, { name: 'Sniper Alerts', scout: '2', trader: '10', tycoon: '50' },
{ name: 'TLD Intel', scout: 'Public Trends', trader: 'Renewal Prices', tycoon: 'Full History' }, { name: 'TLD Intel', scout: 'Public Trends', trader: 'Renewal Prices', tycoon: 'Full History' },
@ -211,7 +211,7 @@ export default function PricingPage() {
Pick your weapon. Pick your weapon.
</h1> </h1>
<p className="mt-5 sm:mt-8 text-sm sm:text-lg lg:text-xl text-white/50 max-w-xl lg:mx-auto font-light leading-relaxed"> <p className="mt-5 sm:mt-8 text-sm sm:text-lg lg:text-xl text-white/50 max-w-xl lg:mx-auto font-light leading-relaxed">
Start free. Scale when you&apos;re ready. All plans include core features. Enter Terminal. Upgrade when you need speed, filters, and deeper intel.
</p> </p>
</div> </div>

View File

@ -255,7 +255,7 @@ function RegisterForm() {
<Loader2 className="w-4 h-4 animate-spin" /> <Loader2 className="w-4 h-4 animate-spin" />
) : ( ) : (
<> <>
Start Hunting Enter Terminal
<ArrowRight className="w-4 h-4" /> <ArrowRight className="w-4 h-4" />
</> </>
)} )}

View File

@ -146,12 +146,12 @@ export default function MyListingsPage() {
<div className="w-20 h-20 bg-amber-400/10 border border-amber-400/20 flex items-center justify-center mx-auto mb-6"> <div className="w-20 h-20 bg-amber-400/10 border border-amber-400/20 flex items-center justify-center mx-auto mb-6">
<Lock className="w-10 h-10 text-amber-400" /> <Lock className="w-10 h-10 text-amber-400" />
</div> </div>
<h1 className="font-display text-3xl text-white mb-4">Sell Your Domains</h1> <h1 className="font-display text-3xl text-white mb-4">For Sale</h1>
<p className="text-white/50 text-sm font-mono mb-2"> <p className="text-white/50 text-sm font-mono mb-2">
List your domains directly on Pounce Market. List domains on Pounce Direct.
</p> </p>
<p className="text-white/30 text-xs font-mono mb-8"> <p className="text-white/30 text-xs font-mono mb-8">
0% commission. Verified ownership. Instant visibility. 0% commission. DNS-verified ownership. Direct buyer contact.
</p> </p>
<div className="bg-white/[0.02] border border-white/[0.08] p-6 mb-6"> <div className="bg-white/[0.02] border border-white/[0.08] p-6 mb-6">
@ -185,7 +185,7 @@ export default function MyListingsPage() {
</div> </div>
<Link href="/pricing" className="inline-flex items-center gap-2 px-6 py-3 bg-accent text-black text-xs font-bold uppercase tracking-wider hover:bg-white transition-colors"> <Link href="/pricing" className="inline-flex items-center gap-2 px-6 py-3 bg-accent text-black text-xs font-bold uppercase tracking-wider hover:bg-white transition-colors">
<Sparkles className="w-4 h-4" />Upgrade Now <Sparkles className="w-4 h-4" />Upgrade
</Link> </Link>
</div> </div>
</div> </div>
@ -239,7 +239,7 @@ export default function MyListingsPage() {
<div className="space-y-3"> <div className="space-y-3">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div className="w-1.5 h-1.5 bg-accent animate-pulse" /> <div className="w-1.5 h-1.5 bg-accent animate-pulse" />
<span className="text-[10px] font-mono tracking-[0.2em] text-accent uppercase">💎 Pounce Direct</span> <span className="text-[10px] font-mono tracking-[0.2em] text-accent uppercase">Pounce Direct</span>
</div> </div>
<h1 className="font-display text-[2.5rem] leading-[1] tracking-[-0.02em]"> <h1 className="font-display text-[2.5rem] leading-[1] tracking-[-0.02em]">
<span className="text-white">For Sale</span> <span className="text-white">For Sale</span>
@ -569,10 +569,17 @@ function CreateListingWizard({ onClose, onSuccess, prefillDomain }: {
}) })
setCreatedListing(listing) setCreatedListing(listing)
// Start DNS verification // Check if domain was already verified in portfolio
const verification = await api.startDnsVerification(listing.id) if (listing.is_verified) {
setVerificationData(verification) // Skip verification step, go directly to publish
setStep(2) setVerified(true)
setStep(3)
} else {
// Start DNS verification
const verification = await api.startDnsVerification(listing.id)
setVerificationData(verification)
setStep(2)
}
} catch (err: any) { } catch (err: any) {
setError(err.message || 'Failed to create listing') setError(err.message || 'Failed to create listing')
} finally { } finally {
@ -823,7 +830,7 @@ function CreateListingWizard({ onClose, onSuccess, prefillDomain }: {
</div> </div>
<div className="p-3 bg-accent/5 border border-accent/20 text-[11px] font-mono text-white/60 leading-relaxed"> <div className="p-3 bg-accent/5 border border-accent/20 text-[11px] font-mono text-white/60 leading-relaxed">
Your listing will appear in the <strong>Market Feed</strong> with the <span className="text-accent">💎 Pounce Direct</span> badge. Your listing will appear in the <strong>Market Feed</strong> with the <span className="text-accent">Pounce Direct</span> badge.
</div> </div>
<button <button

File diff suppressed because it is too large Load Diff

View File

@ -47,11 +47,30 @@ import Image from 'next/image'
interface HotAuction { interface HotAuction {
domain: string domain: string
current_bid: number current_bid: number
time_remaining: string end_time?: string
platform: string platform: string
affiliate_url?: string affiliate_url?: string
} }
function calcTimeRemaining(endTimeIso?: string): string {
if (!endTimeIso) return 'N/A'
const end = new Date(endTimeIso).getTime()
const now = Date.now()
const diff = end - now
if (diff <= 0) return 'Ended'
const seconds = Math.floor(diff / 1000)
const days = Math.floor(seconds / 86400)
const hours = Math.floor((seconds % 86400) / 3600)
const mins = Math.floor((seconds % 3600) / 60)
if (days > 0) return `${days}d ${hours}h`
if (hours > 0) return `${hours}h ${mins}m`
if (mins > 0) return `${mins}m`
return '< 1m'
}
interface SearchResult { interface SearchResult {
domain: string domain: string
status: string status: string
@ -74,6 +93,7 @@ export default function RadarPage() {
const [hotAuctions, setHotAuctions] = useState<HotAuction[]>([]) const [hotAuctions, setHotAuctions] = useState<HotAuction[]>([])
const [marketStats, setMarketStats] = useState({ totalAuctions: 0, endingSoon: 0 }) const [marketStats, setMarketStats] = useState({ totalAuctions: 0, endingSoon: 0 })
const [loadingData, setLoadingData] = useState(true) const [loadingData, setLoadingData] = useState(true)
const [tick, setTick] = useState(0)
const [searchQuery, setSearchQuery] = useState('') const [searchQuery, setSearchQuery] = useState('')
const [searchResult, setSearchResult] = useState<SearchResult | null>(null) const [searchResult, setSearchResult] = useState<SearchResult | null>(null)
@ -96,33 +116,27 @@ export default function RadarPage() {
// Load Data - Using same API as Market page for consistency // Load Data - Using same API as Market page for consistency
const loadDashboardData = useCallback(async () => { const loadDashboardData = useCallback(async () => {
try { try {
// Use getMarketFeed for consistency with Market & Acquire pages // External auctions only (Pounce Direct has no end_time)
const result = await api.getMarketFeed({ const [feed, ending24h] = await Promise.all([
source: 'all', api.getMarketFeed({ source: 'external', sortBy: 'time', limit: 10 }),
sortBy: 'time', api.getMarketFeed({ source: 'external', endingWithin: 24, sortBy: 'time', limit: 1 }),
limit: 10, // Show top 10 ending soon ])
})
const auctions: HotAuction[] = (feed.items || [])
// Convert to HotAuction format
const auctions = (result.items || [])
.filter((item: any) => item.status === 'auction' && item.end_time) .filter((item: any) => item.status === 'auction' && item.end_time)
.slice(0, 6) .slice(0, 6)
.map((item: any) => ({ .map((item: any) => ({
domain: item.domain, domain: item.domain,
current_bid: item.price || 0, current_bid: item.price || 0,
time_remaining: item.time_remaining || '', end_time: item.end_time || undefined,
platform: item.source || 'Unknown', platform: item.source || 'Unknown',
affiliate_url: item.url || '', affiliate_url: item.url || '',
})) }))
setHotAuctions(auctions) setHotAuctions(auctions)
setMarketStats({ setMarketStats({
totalAuctions: result.auction_count || result.total || 0, totalAuctions: feed.total || feed.auction_count || 0,
endingSoon: result.items?.filter((i: any) => { endingSoon: ending24h.total || 0,
if (!i.end_time) return false
const hoursLeft = (new Date(i.end_time).getTime() - Date.now()) / (1000 * 60 * 60)
return hoursLeft > 0 && hoursLeft <= 24
}).length || 0,
}) })
} catch (error) { } catch (error) {
console.error('Failed to load data:', error) console.error('Failed to load data:', error)
@ -135,6 +149,8 @@ export default function RadarPage() {
if (!authLoading) { if (!authLoading) {
if (isAuthenticated) { if (isAuthenticated) {
loadDashboardData() loadDashboardData()
const interval = setInterval(() => setTick(t => t + 1), 30000)
return () => clearInterval(interval)
} else { } else {
setLoadingData(false) setLoadingData(false)
} }
@ -149,18 +165,13 @@ export default function RadarPage() {
case 'domain': return mult * a.domain.localeCompare(b.domain) case 'domain': return mult * a.domain.localeCompare(b.domain)
case 'bid': return mult * (a.current_bid - b.current_bid) case 'bid': return mult * (a.current_bid - b.current_bid)
case 'time': case 'time':
// Parse time_remaining like "2h 30m" or "5d 12h" const aTime = a.end_time ? new Date(a.end_time).getTime() : Infinity
const parseTime = (t: string) => { const bTime = b.end_time ? new Date(b.end_time).getTime() : Infinity
const d = t.match(/(\d+)d/)?.[1] || 0 return mult * (aTime - bTime)
const h = t.match(/(\d+)h/)?.[1] || 0
const m = t.match(/(\d+)m/)?.[1] || 0
return Number(d) * 86400 + Number(h) * 3600 + Number(m) * 60
}
return mult * (parseTime(a.time_remaining) - parseTime(b.time_remaining))
default: return 0 default: return 0
} }
}) })
}, [hotAuctions, auctionSort, auctionSortDir]) }, [hotAuctions, auctionSort, auctionSortDir, tick])
const handleAuctionSort = useCallback((field: typeof auctionSort) => { const handleAuctionSort = useCallback((field: typeof auctionSort) => {
if (auctionSort === field) { if (auctionSort === field) {
@ -289,7 +300,7 @@ export default function RadarPage() {
</div> </div>
<div className="flex items-center gap-2 text-[10px] font-mono text-white/40"> <div className="flex items-center gap-2 text-[10px] font-mono text-white/40">
<span>{marketStats.totalAuctions.toLocaleString()} auctions</span> <span>{marketStats.totalAuctions.toLocaleString()} auctions</span>
<span className="text-accent">{marketStats.endingSoon} ending</span> <span className="text-accent">{marketStats.endingSoon} ending 24h</span>
</div> </div>
</div> </div>
@ -323,7 +334,7 @@ export default function RadarPage() {
<div className="hidden lg:block mb-8"> <div className="hidden lg:block mb-8">
<div className="flex items-center gap-3 mb-4"> <div className="flex items-center gap-3 mb-4">
<div className="w-1.5 h-1.5 bg-accent animate-pulse" /> <div className="w-1.5 h-1.5 bg-accent animate-pulse" />
<span className="text-[10px] font-mono tracking-[0.2em] text-accent uppercase">Intelligence Hub</span> <span className="text-[10px] font-mono tracking-[0.2em] text-accent uppercase">Domain Radar</span>
</div> </div>
<h1 className="font-display text-[2.5rem] leading-[1] tracking-[-0.02em] text-white mb-2"> <h1 className="font-display text-[2.5rem] leading-[1] tracking-[-0.02em] text-white mb-2">
Domain Radar Domain Radar
@ -602,7 +613,7 @@ export default function RadarPage() {
</div> </div>
<div className="text-[10px] font-mono text-white/40 flex items-center justify-end gap-1"> <div className="text-[10px] font-mono text-white/40 flex items-center justify-end gap-1">
<Clock className="w-3 h-3" /> <Clock className="w-3 h-3" />
{auction.time_remaining} {calcTimeRemaining(auction.end_time)}
</div> </div>
</div> </div>
</div> </div>
@ -657,7 +668,7 @@ export default function RadarPage() {
<div className="text-center"> <div className="text-center">
<span className="text-xs font-mono text-white/50 flex items-center justify-center gap-1"> <span className="text-xs font-mono text-white/50 flex items-center justify-center gap-1">
<Clock className="w-3 h-3" /> <Clock className="w-3 h-3" />
{auction.time_remaining} {calcTimeRemaining(auction.end_time)}
</span> </span>
</div> </div>

View File

@ -143,7 +143,7 @@ export default function SettingsPage() {
const upgraded = params.get('upgraded') const upgraded = params.get('upgraded')
const cancelled = params.get('cancelled') const cancelled = params.get('cancelled')
if (upgraded) { if (upgraded) {
setSuccess(`🎉 Welcome to ${upgraded.charAt(0).toUpperCase() + upgraded.slice(1)}!`) setSuccess(`Upgrade complete: ${upgraded.charAt(0).toUpperCase() + upgraded.slice(1)}`)
setActiveTab('billing') setActiveTab('billing')
window.history.replaceState({}, '', '/terminal/settings') window.history.replaceState({}, '', '/terminal/settings')
checkAuth() checkAuth()

View File

@ -162,7 +162,7 @@ export default function WelcomePage() {
{/* Next Steps */} {/* Next Steps */}
<div className="mb-12"> <div className="mb-12">
<h2 className="text-lg font-semibold text-foreground mb-6 text-center"> <h2 className="text-lg font-semibold text-foreground mb-6 text-center">
Get Started Next Steps
</h2> </h2>
<div className="max-w-2xl mx-auto space-y-3"> <div className="max-w-2xl mx-auto space-y-3">
{plan.nextSteps.map((step, i) => ( {plan.nextSteps.map((step, i) => (
@ -192,12 +192,11 @@ export default function WelcomePage() {
className="inline-flex items-center gap-2 px-8 py-4 bg-accent text-background font-medium rounded-xl 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" hover:bg-accent-hover transition-all shadow-lg shadow-accent/20"
> >
Go to Dashboard Go to Radar
<ArrowRight className="w-4 h-4" /> <ArrowRight className="w-4 h-4" />
</Link> </Link>
<p className="text-sm text-foreground-muted mt-4"> <p className="text-sm text-foreground-muted mt-4">
Need help? Check out our <Link href="/docs" className="text-accent hover:underline">documentation</Link> or{' '} Need help? <Link href="mailto:hello@pounce.ch" className="text-accent hover:underline">Contact support</Link>.
<Link href="mailto:support@pounce.ch" className="text-accent hover:underline">contact support</Link>.
</p> </p>
</div> </div>
</PageContainer> </PageContainer>

View File

@ -255,7 +255,7 @@ export default function YieldPage() {
</button> </button>
<button onClick={() => setShowActivateModal(true)} <button onClick={() => setShowActivateModal(true)}
className="flex items-center gap-2 px-4 py-2.5 bg-accent text-black text-xs font-bold uppercase tracking-wider hover:bg-white"> className="flex items-center gap-2 px-4 py-2.5 bg-accent text-black text-xs font-bold uppercase tracking-wider hover:bg-white">
<Plus className="w-4 h-4" />Add Domain <Plus className="w-4 h-4" />Activate Domain
</button> </button>
</div> </div>
</div> </div>
@ -266,7 +266,7 @@ export default function YieldPage() {
<section className="lg:hidden px-4 py-3 border-b border-white/[0.08]"> <section className="lg:hidden px-4 py-3 border-b border-white/[0.08]">
<button onClick={() => setShowActivateModal(true)} <button onClick={() => setShowActivateModal(true)}
className="w-full flex items-center justify-center gap-2 py-3 bg-accent text-black text-xs font-bold uppercase tracking-wider"> className="w-full flex items-center justify-center gap-2 py-3 bg-accent text-black text-xs font-bold uppercase tracking-wider">
<Plus className="w-4 h-4" />Add Domain <Plus className="w-4 h-4" />Activate Domain
</button> </button>
</section> </section>

View File

@ -417,7 +417,7 @@ export default function TldDetailClient({ tld, initialData }: Props) {
href="/register" href="/register"
className="inline-flex items-center gap-2 px-8 py-4 bg-accent text-black font-bold hover:bg-accent/90 transition-colors" className="inline-flex items-center gap-2 px-8 py-4 bg-accent text-black font-bold hover:bg-accent/90 transition-colors"
> >
Start Free <ArrowRight className="w-4 h-4" /> Enter Terminal <ArrowRight className="w-4 h-4" />
</Link> </Link>
</div> </div>
</section> </section>

View File

@ -4,9 +4,34 @@ import Link from 'next/link'
import Image from 'next/image' import Image from 'next/image'
import { Twitter, Mail, Linkedin, ArrowRight } from 'lucide-react' import { Twitter, Mail, Linkedin, ArrowRight } from 'lucide-react'
import { useStore } from '@/lib/store' import { useStore } from '@/lib/store'
import { api } from '@/lib/api'
import { useCallback, useMemo, useState } from 'react'
export function Footer() { export function Footer() {
const { isAuthenticated } = useStore() const { isAuthenticated } = useStore()
const [newsletterEmail, setNewsletterEmail] = useState('')
const [newsletterState, setNewsletterState] = useState<'idle' | 'loading' | 'success' | 'error'>('idle')
const [newsletterError, setNewsletterError] = useState<string | null>(null)
const canSubmitNewsletter = useMemo(() => {
const email = newsletterEmail.trim()
return email.length > 3 && email.includes('@') && email.includes('.')
}, [newsletterEmail])
const handleNewsletterSubmit = useCallback(async (e: React.FormEvent) => {
e.preventDefault()
if (!canSubmitNewsletter || newsletterState === 'loading') return
setNewsletterState('loading')
setNewsletterError(null)
try {
await api.subscribeNewsletter(newsletterEmail.trim())
setNewsletterState('success')
} catch (err: any) {
setNewsletterState('error')
setNewsletterError(err?.message || 'Subscription failed. Retry.')
}
}, [canSubmitNewsletter, newsletterEmail, newsletterState])
return ( return (
<footer className="relative border-t border-white/10 bg-[#020202] mt-auto overflow-hidden"> <footer className="relative border-t border-white/10 bg-[#020202] mt-auto overflow-hidden">
@ -40,16 +65,31 @@ export function Footer() {
{/* Newsletter - Hidden on Mobile */} {/* Newsletter - Hidden on Mobile */}
<div className="hidden sm:block mb-8 max-w-sm"> <div className="hidden sm:block mb-8 max-w-sm">
<label className="text-[10px] font-mono uppercase tracking-widest text-white/30 mb-2 block">Mission Briefings</label> <label className="text-[10px] font-mono uppercase tracking-widest text-white/30 mb-2 block">Mission Briefings</label>
<div className="flex"> <form onSubmit={handleNewsletterSubmit} className="flex">
<input <input
type="email" type="email"
placeholder="ENTER_EMAIL" value={newsletterEmail}
onChange={(e) => setNewsletterEmail(e.target.value)}
placeholder="Email address"
className="w-full bg-[#0A0A0A] border border-white/10 px-4 py-3 text-white font-mono text-xs placeholder:text-white/20 focus:outline-none focus:border-accent transition-all" className="w-full bg-[#0A0A0A] border border-white/10 px-4 py-3 text-white font-mono text-xs placeholder:text-white/20 focus:outline-none focus:border-accent transition-all"
/> />
<button className="px-4 bg-white text-black hover:bg-accent transition-colors border-l border-white/10"> <button
type="submit"
disabled={!canSubmitNewsletter || newsletterState === 'loading'}
className="px-4 bg-white text-black hover:bg-accent transition-colors border-l border-white/10 disabled:opacity-50 disabled:cursor-not-allowed"
aria-label="Subscribe to briefings"
title="Subscribe"
>
<ArrowRight className="w-4 h-4" /> <ArrowRight className="w-4 h-4" />
</button> </button>
</div> </form>
{newsletterState === 'success' ? (
<p className="mt-2 text-[10px] font-mono text-accent/80">Subscribed. Briefings inbound.</p>
) : newsletterState === 'error' ? (
<p className="mt-2 text-[10px] font-mono text-rose-300/80">{newsletterError}</p>
) : (
<p className="mt-2 text-[10px] font-mono text-white/20">Weekly intel. No spam.</p>
)}
</div> </div>
<div className="flex items-center gap-3 sm:gap-4"> <div className="flex items-center gap-3 sm:gap-4">

View File

@ -90,7 +90,7 @@ export function Header() {
className="flex items-center gap-2 h-9 px-5 text-xs bg-accent text-black font-bold uppercase tracking-wider hover:bg-white transition-colors" className="flex items-center gap-2 h-9 px-5 text-xs bg-accent text-black font-bold uppercase tracking-wider hover:bg-white transition-colors"
> >
<Target className="w-4 h-4" /> <Target className="w-4 h-4" />
Terminal Enter Terminal
</Link> </Link>
) : ( ) : (
<> <>
@ -104,7 +104,7 @@ export function Header() {
href="/register" href="/register"
className="flex items-center h-9 px-5 text-xs bg-accent text-black font-bold uppercase tracking-wider hover:bg-white transition-colors" className="flex items-center h-9 px-5 text-xs bg-accent text-black font-bold uppercase tracking-wider hover:bg-white transition-colors"
> >
Start Free Enter Terminal
</Link> </Link>
</> </>
)} )}
@ -199,7 +199,7 @@ export function Header() {
className="flex items-center justify-center gap-2 w-full py-3 bg-accent text-black text-xs font-bold uppercase tracking-wider active:scale-[0.98] transition-all mb-2" className="flex items-center justify-center gap-2 w-full py-3 bg-accent text-black text-xs font-bold uppercase tracking-wider active:scale-[0.98] transition-all mb-2"
> >
<Target className="w-3 h-3" /> <Target className="w-3 h-3" />
Open Terminal Enter Terminal
</Link> </Link>
<button <button
@ -218,7 +218,7 @@ export function Header() {
className="flex items-center justify-center gap-2 w-full py-3 bg-accent text-black text-xs font-bold uppercase tracking-wider active:scale-[0.98] transition-all mb-2" className="flex items-center justify-center gap-2 w-full py-3 bg-accent text-black text-xs font-bold uppercase tracking-wider active:scale-[0.98] transition-all mb-2"
> >
<Sparkles className="w-3 h-3" /> <Sparkles className="w-3 h-3" />
Start Free Enter Terminal
</Link> </Link>
<Link <Link

View File

@ -14,7 +14,7 @@ export interface SEOProps {
} }
const defaultTitle = 'Pounce - Domain Intelligence for Investors' const defaultTitle = 'Pounce - Domain Intelligence for Investors'
const defaultDescription = 'The market never sleeps. You should. Scan, track, and trade domains with real-time drops, auctions, and TLD price intelligence. Spam-filtered. 0% commission.' const defaultDescription = 'The market never sleeps. You should. Scan auctions, track drops, and trade domains with clean data, TLD price intel, and verified listings. Spam-filtered. 0% commission.'
const defaultKeywords = [ const defaultKeywords = [
'domain marketplace', 'domain marketplace',
'domain auctions', 'domain auctions',
@ -115,7 +115,7 @@ export function getOrganizationSchema() {
], ],
contactPoint: { contactPoint: {
'@type': 'ContactPoint', '@type': 'ContactPoint',
email: 'hello@pounce.com', email: 'hello@pounce.ch',
contactType: 'Customer Service', contactType: 'Customer Service',
}, },
} }

View File

@ -8,7 +8,7 @@ export const DEFAULT_OG_IMAGE = `${SITE_URL}/og-image.png`
export const SEO_CONFIG = { export const SEO_CONFIG = {
home: { home: {
title: 'Pounce - Domain Intelligence Platform | Find, Track & Trade Domains', title: 'Pounce - Domain Intelligence Platform | Find, Track & Trade Domains',
description: 'The #1 domain intelligence platform. Real-time auction aggregation, TLD price tracking, spam-free market feed, and portfolio management. Find undervalued domains before anyone else.', description: 'Domain intelligence for serious investors. Scan live auctions, track drops, compare TLD pricing, and manage portfolios with a clean market feed and verified listings.',
keywords: [ keywords: [
'domain intelligence', 'domain auctions', 'expired domains', 'domain investing', 'domain intelligence', 'domain auctions', 'expired domains', 'domain investing',
'TLD pricing', 'domain drops', 'domain marketplace', 'domain monitoring', 'TLD pricing', 'domain drops', 'domain marketplace', 'domain monitoring',
@ -27,7 +27,7 @@ export const SEO_CONFIG = {
}, },
acquire: { acquire: {
title: 'Domain Auctions & Marketplace | Live Drops & Deals | Pounce', title: 'Domain Auctions & Marketplace | Live Drops & Deals | Pounce',
description: 'Browse 10,000+ live domain auctions from GoDaddy, Sedo, DropCatch & more. Spam-filtered feed, real-time updates, and exclusive Pounce Direct listings with 0% commission.', description: 'Browse live domain auctions from GoDaddy, Sedo, DropCatch & more. Use filters to cut noise, and discover Pounce Direct listings with DNS-verified owners and 0% commission.',
keywords: [ keywords: [
'domain auctions', 'buy domains', 'expired domain auctions', 'domain marketplace', 'domain auctions', 'buy domains', 'expired domain auctions', 'domain marketplace',
'godaddy auctions', 'sedo domains', 'dropcatch', 'namejet auctions', 'godaddy auctions', 'sedo domains', 'dropcatch', 'namejet auctions',
@ -44,8 +44,8 @@ export const SEO_CONFIG = {
], ],
}, },
pricing: { pricing: {
title: 'Pricing Plans | Start Free, Scale Smart | Pounce', title: 'Pricing Plans | Scout, Trader, Tycoon | Pounce',
description: 'From hobbyist to tycoon. Scout (free), Trader ($9/mo), Tycoon ($29/mo). Real-time alerts, spam-free feeds, domain valuation, and 0% marketplace commission.', description: 'Scout, Trader, or Tycoon. Clean market feed, faster monitoring, valuation intel, portfolio tools, and 0% commission on Pounce Direct listings. Tycoon checks every 10 minutes.',
keywords: [ keywords: [
'domain tool pricing', 'domain software', 'domain investing tools', 'domain tool pricing', 'domain software', 'domain investing tools',
'domain monitoring service', 'domain alert service', 'domain trading platform' 'domain monitoring service', 'domain alert service', 'domain trading platform'
@ -83,7 +83,7 @@ export function generateSoftwareApplicationSchema() {
applicationCategory: 'BusinessApplication', applicationCategory: 'BusinessApplication',
operatingSystem: 'Web', operatingSystem: 'Web',
url: SITE_URL, url: SITE_URL,
description: 'Real-time domain auction aggregation, TLD price tracking, and portfolio management platform', description: 'Live domain auction aggregation, TLD price tracking, and portfolio management platform',
offers: [ offers: [
{ {
'@type': 'Offer', '@type': 'Offer',
@ -106,7 +106,7 @@ export function generateSoftwareApplicationSchema() {
price: '29', price: '29',
priceCurrency: 'USD', priceCurrency: 'USD',
priceValidUntil: '2025-12-31', priceValidUntil: '2025-12-31',
description: 'Enterprise tier with real-time alerts and unlimited portfolio', description: 'Advanced tier with 10-minute monitoring, priority alerts, and unlimited portfolio',
}, },
], ],
aggregateRating: { aggregateRating: {