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
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:
@ -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,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@ -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]
|
|
||||||
|
|
||||||
|
|||||||
@ -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"
|
||||||
|
|||||||
@ -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',
|
||||||
|
|||||||
@ -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">
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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. Mon–Fri, 0900–1800 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'll respond fast.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -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',
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
|||||||
@ -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',
|
||||||
|
|||||||
@ -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: {
|
||||||
|
|||||||
@ -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're ready. All plans include core features.
|
Enter Terminal. Upgrade when you need speed, filters, and deeper intel.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -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" />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -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
@ -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>
|
||||||
|
|
||||||
|
|||||||
@ -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()
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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">
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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',
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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: {
|
||||||
|
|||||||
Reference in New Issue
Block a user