diff --git a/backend/app/api/listings.py b/backend/app/api/listings.py
index 01e45ce..32c7d34 100644
--- a/backend/app/api/listings.py
+++ b/backend/app/api/listings.py
@@ -548,6 +548,10 @@ async def create_listing(
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
existing = await db.execute(
select(DomainListing).where(DomainListing.domain == domain_lower)
@@ -600,6 +604,7 @@ async def create_listing(
estimated_value = None
# Create listing
+ # If portfolio domain is already DNS verified, listing is auto-verified
listing = DomainListing(
user_id=current_user.id,
domain=domain_lower,
@@ -614,7 +619,9 @@ async def create_listing(
allow_offers=data.allow_offers,
pounce_score=pounce_score,
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,
)
diff --git a/backend/app/api/portfolio.py b/backend/app/api/portfolio.py
index 8bcd729..c6521c6 100644
--- a/backend/app/api/portfolio.py
+++ b/backend/app/api/portfolio.py
@@ -176,7 +176,112 @@ class ValuationResponse(BaseModel):
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 ==============
+# 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])
async def get_portfolio(
@@ -242,49 +347,6 @@ async def get_portfolio(
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)
async def add_portfolio_domain(
data: PortfolioDomainCreate,
@@ -670,42 +732,6 @@ async def get_domain_valuation(
# ============== 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)
async def start_dns_verification(
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}",
)
-
-@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]
-
diff --git a/frontend/src/app/about/page.tsx b/frontend/src/app/about/page.tsx
index c2222f7..a0f6d76 100644
--- a/frontend/src/app/about/page.tsx
+++ b/frontend/src/app/about/page.tsx
@@ -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)]"
style={{ clipPath: 'polygon(10px 0, 100% 0, 100% 100%, 0 100%, 0 10px)' }}
>
- Start Hunting
+ Enter Terminal
- Global liquidity pool. Verified assets only.
- Aggregated from GoDaddy, Sedo, and Pounce Direct.
+ External auctions + Pounce Direct listings.
+ Pounce Direct owners are DNS-verified. External auctions are sourced from GoDaddy, Sedo, and other partners.
diff --git a/frontend/src/app/blog/page.tsx b/frontend/src/app/blog/page.tsx
index 45584ac..d6efb8b 100644
--- a/frontend/src/app/blog/page.tsx
+++ b/frontend/src/app/blog/page.tsx
@@ -47,6 +47,9 @@ export default function BlogPage() {
const [loading, setLoading] = useState(true)
const [selectedCategory, setSelectedCategory] = useState(null)
const [total, setTotal] = useState(0)
+ const [newsletterEmail, setNewsletterEmail] = useState('')
+ const [newsletterState, setNewsletterState] = useState<'idle' | 'loading' | 'success' | 'error'>('idle')
+ const [newsletterError, setNewsletterError] = useState(null)
useEffect(() => {
loadBlogData()
@@ -237,7 +240,7 @@ export default function BlogPage() {
- Read Article
+ Full briefing
@@ -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"
>
- Load More Articles
+ Load more briefings
@@ -339,21 +342,50 @@ export default function BlogPage() {
- Get hunting tips in your inbox
+ Mission Briefings
- Join domain hunters who receive weekly insights, market trends, and exclusive strategies.
+ Weekly intel on drops, pricing traps, and market moves. No spam.
-
+
+
+ {newsletterState === 'success' ? (
+
Subscribed. Briefings inbound.
+ ) : newsletterState === 'error' ? (
+
{newsletterError}
+ ) : null}
diff --git a/frontend/src/app/contact/page.tsx b/frontend/src/app/contact/page.tsx
index cacad4c..5e1e5d4 100644
--- a/frontend/src/app/contact/page.tsx
+++ b/frontend/src/app/contact/page.tsx
@@ -11,22 +11,22 @@ const contactMethods = [
{
icon: Mail,
title: 'Secure Comms',
- description: 'Encrypted channel for general inquiries.',
+ description: 'Support, partnerships, and product intel.',
value: 'hello@pounce.ch',
href: 'mailto:hello@pounce.ch',
},
{
icon: MessageSquare,
- title: 'Live Support',
- description: 'Mon-Fri, 0900-1800 CET',
- value: 'Open Channel',
- href: '#',
+ title: 'Support Desk',
+ description: 'Email support. MonβFri, 0900β1800 CET.',
+ value: 'Send ticket',
+ href: 'mailto:hello@pounce.ch?subject=Support',
},
{
icon: Clock,
title: 'Response Time',
- description: 'Average ticket resolution.',
- value: '< 4 Hours',
+ description: 'Typical response window.',
+ value: '< 24 Hours',
href: null,
},
]
@@ -110,7 +110,7 @@ export default function ContactPage() {
Establish Contact.
- Question? Idea? Glitch in the matrix? We're listening.
+ Support request, bug report, or partnership intel. We'll respond fast.
diff --git a/frontend/src/app/layout.tsx b/frontend/src/app/layout.tsx
index ce4ff5f..4dc4082 100644
--- a/frontend/src/app/layout.tsx
+++ b/frontend/src/app/layout.tsx
@@ -6,6 +6,7 @@ import Script from 'next/script'
const inter = Inter({ subsets: ['latin'] })
const siteUrl = process.env.NEXT_PUBLIC_SITE_URL || 'https://pounce.ch'
+const googleSiteVerification = process.env.NEXT_PUBLIC_GOOGLE_SITE_VERIFICATION
export const viewport: Viewport = {
width: 'device-width',
@@ -19,7 +20,7 @@ export const metadata: Metadata = {
default: 'Pounce - Domain Intelligence Platform | Find, Track & Trade Domains',
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: [
'domain intelligence',
'domain auctions',
@@ -65,7 +66,7 @@ export const metadata: Metadata = {
url: siteUrl,
siteName: 'Pounce',
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: [
{
url: `${siteUrl}/og-image.png`,
@@ -78,14 +79,12 @@ export const metadata: Metadata = {
twitter: {
card: 'summary_large_image',
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',
site: '@pouncedomains',
images: [`${siteUrl}/og-image.png`],
},
- verification: {
- google: 'YOUR_GOOGLE_VERIFICATION_CODE', // Add your Google Search Console verification
- },
+ verification: googleSiteVerification ? { google: googleSiteVerification } : undefined,
robots: {
index: true,
follow: true,
@@ -139,7 +138,7 @@ export default function RootLayout({
],
contactPoint: {
'@type': 'ContactPoint',
- email: 'hello@pounce.com',
+ email: 'hello@pounce.ch',
contactType: 'Customer Service',
},
}),
diff --git a/frontend/src/app/metadata.ts b/frontend/src/app/metadata.ts
index a63df61..deb9001 100644
--- a/frontend/src/app/metadata.ts
+++ b/frontend/src/app/metadata.ts
@@ -4,7 +4,7 @@ const siteUrl = process.env.NEXT_PUBLIC_SITE_URL || 'https://pounce.com'
export const homeMetadata: Metadata = {
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: [
'domain intelligence',
'domain marketplace',
@@ -23,7 +23,7 @@ export const homeMetadata: Metadata = {
],
openGraph: {
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,
type: 'website',
images: [
@@ -38,7 +38,7 @@ export const homeMetadata: Metadata = {
twitter: {
card: 'summary_large_image',
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`],
},
alternates: {
@@ -66,7 +66,7 @@ export function getHomeStructuredData() {
],
contactPoint: {
'@type': 'ContactPoint',
- email: 'hello@pounce.com',
+ email: 'hello@pounce.ch',
contactType: 'Customer Service',
availableLanguage: ['en'],
},
@@ -108,7 +108,7 @@ export function getHomeStructuredData() {
worstRating: '1',
},
featureList: [
- 'Real-time domain monitoring',
+ 'Live domain monitoring',
'Spam-filtered auction feed',
'TLD price intelligence',
'Portfolio management',
diff --git a/frontend/src/app/pricing/metadata.ts b/frontend/src/app/pricing/metadata.ts
index f024b24..71772e2 100644
--- a/frontend/src/app/pricing/metadata.ts
+++ b/frontend/src/app/pricing/metadata.ts
@@ -4,7 +4,7 @@ const siteUrl = process.env.NEXT_PUBLIC_SITE_URL || 'https://pounce.com'
export const pricingMetadata: Metadata = {
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: [
'domain intelligence pricing',
'domain monitoring subscription',
@@ -17,7 +17,7 @@ export const pricingMetadata: Metadata = {
],
openGraph: {
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`,
type: 'website',
images: [
@@ -32,7 +32,7 @@ export const pricingMetadata: Metadata = {
twitter: {
card: 'summary_large_image',
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`],
},
alternates: {
diff --git a/frontend/src/app/pricing/page.tsx b/frontend/src/app/pricing/page.tsx
index 75edfe1..c128fd8 100644
--- a/frontend/src/app/pricing/page.tsx
+++ b/frontend/src/app/pricing/page.tsx
@@ -17,7 +17,7 @@ const tiers = [
icon: Zap,
price: '0',
period: '',
- description: 'Test the waters. Zero risk.',
+ description: 'Recon access. No commitment.',
features: [
{ text: 'Market Feed', highlight: false, available: true, sublabel: 'Raw' },
{ 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: 'Yield (Intent Routing)', highlight: false, available: false },
],
- cta: 'Start Free',
+ cta: 'Enter Terminal',
highlighted: false,
badge: null,
isPaid: false,
@@ -39,7 +39,7 @@ const tiers = [
icon: TrendingUp,
price: '9',
period: '/mo',
- description: 'The smart investor\'s choice.',
+ description: 'Cut noise. Move faster.',
features: [
{ text: 'Market Feed', highlight: true, available: true, sublabel: 'Curated' },
{ text: 'Alert Speed', highlight: true, available: true, sublabel: 'Hourly' },
@@ -62,10 +62,10 @@ const tiers = [
icon: Crown,
price: '29',
period: '/mo',
- description: 'For serious domain investors.',
+ description: 'Full firepower. Priority routes.',
features: [
{ 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: '50 Sniper Alerts', highlight: true, available: true },
{ text: 'TLD Intel', highlight: true, available: true, sublabel: 'Full History' },
@@ -83,7 +83,7 @@ const tiers = [
const comparisonFeatures = [
{ 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: 'Sniper Alerts', scout: '2', trader: '10', tycoon: '50' },
{ name: 'TLD Intel', scout: 'Public Trends', trader: 'Renewal Prices', tycoon: 'Full History' },
@@ -211,7 +211,7 @@ export default function PricingPage() {
Pick your weapon.
- Start free. Scale when you're ready. All plans include core features.
+ Enter Terminal. Upgrade when you need speed, filters, and deeper intel.
diff --git a/frontend/src/app/register/page.tsx b/frontend/src/app/register/page.tsx
index 5944b27..fabf5de 100644
--- a/frontend/src/app/register/page.tsx
+++ b/frontend/src/app/register/page.tsx
@@ -255,7 +255,7 @@ function RegisterForm() {
) : (
<>
- Start Hunting
+ Enter Terminal
>
)}
diff --git a/frontend/src/app/terminal/listing/page.tsx b/frontend/src/app/terminal/listing/page.tsx
index ec4944c..cc4f22e 100755
--- a/frontend/src/app/terminal/listing/page.tsx
+++ b/frontend/src/app/terminal/listing/page.tsx
@@ -146,12 +146,12 @@ export default function MyListingsPage() {
- Sell Your Domains
+ For Sale
- List your domains directly on Pounce Market.
+ List domains on Pounce Direct.
- 0% commission. Verified ownership. Instant visibility.
+ 0% commission. DNS-verified ownership. Direct buyer contact.
@@ -185,7 +185,7 @@ export default function MyListingsPage() {
- Upgrade Now
+ Upgrade
@@ -239,7 +239,7 @@ export default function MyListingsPage() {
-
π Pounce Direct
+
Pounce Direct
For Sale
@@ -569,10 +569,17 @@ function CreateListingWizard({ onClose, onSuccess, prefillDomain }: {
})
setCreatedListing(listing)
- // Start DNS verification
- const verification = await api.startDnsVerification(listing.id)
- setVerificationData(verification)
- setStep(2)
+ // Check if domain was already verified in portfolio
+ if (listing.is_verified) {
+ // Skip verification step, go directly to publish
+ setVerified(true)
+ setStep(3)
+ } else {
+ // Start DNS verification
+ const verification = await api.startDnsVerification(listing.id)
+ setVerificationData(verification)
+ setStep(2)
+ }
} catch (err: any) {
setError(err.message || 'Failed to create listing')
} finally {
@@ -823,7 +830,7 @@ function CreateListingWizard({ onClose, onSuccess, prefillDomain }: {
- β
Your listing will appear in the Market Feed with the π Pounce Direct badge.
+ Your listing will appear in the Market Feed with the Pounce Direct badge.
= {
- healthy: { label: 'Online', color: 'text-accent', bg: 'bg-accent/10 border-accent/20', icon: 'online' },
- weakening: { label: 'Weak', color: 'text-amber-400', bg: 'bg-amber-500/10 border-amber-500/20', icon: 'warning' },
- parked: { label: 'Parked', color: 'text-blue-400', bg: 'bg-blue-500/10 border-blue-500/20', icon: 'warning' },
- critical: { label: 'Critical', color: 'text-rose-400', bg: 'bg-rose-500/10 border-rose-500/20', icon: 'offline' },
- unknown: { label: 'Unknown', color: 'text-white/40', bg: 'bg-white/5 border-white/10', icon: 'unknown' },
-}
-
// ============================================================================
// MAIN PAGE
// ============================================================================
@@ -122,136 +83,22 @@ export default function PortfolioPage() {
const [refreshingId, setRefreshingId] = useState(null)
const [deletingId, setDeletingId] = useState(null)
const [showAddModal, setShowAddModal] = useState(false)
- const [selectedDomain, setSelectedDomain] = useState(null)
const [verifyingDomain, setVerifyingDomain] = useState(null)
const [filter, setFilter] = useState<'all' | 'active' | 'sold'>('all')
- // Sorting - Extended with health
- const [sortField, setSortField] = useState<'domain' | 'value' | 'roi' | 'renewal' | 'health'>('domain')
+ // Sorting
+ const [sortField, setSortField] = useState<'domain' | 'value' | 'roi' | 'renewal'>('domain')
const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('asc')
- // Mobile Menu
+ // Mobile Menu & Navigation Drawer
const [menuOpen, setMenuOpen] = useState(false)
-
- // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
- // MONITORING STATE (New: Health, Alerts, Yield)
- // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
- const [healthReports, setHealthReports] = useState>({})
- const [loadingHealth, setLoadingHealth] = useState>({})
- const [togglingAlerts, setTogglingAlerts] = useState>({})
- const [alertsEnabled, setAlertsEnabled] = useState>({})
- const [showHealthDetail, setShowHealthDetail] = useState(null)
- const [showYieldModal, setShowYieldModal] = useState(null)
+ const [navDrawerOpen, setNavDrawerOpen] = useState(false)
- // Tier-based access
const tier = subscription?.tier || 'scout'
const isScout = tier === 'scout'
- const isTycoon = tier === 'tycoon'
- const canListForSale = !isScout // Only Trader & Tycoon can list
- const canUseSmsAlerts = isTycoon // Only Tycoon can use SMS alerts
- const canUseYield = !isScout // Trader & Tycoon can use Yield
useEffect(() => { checkAuth() }, [checkAuth])
- // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
- // HEALTH CHECK HANDLERS
- // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
-
- // Load health reports for verified domains
- useEffect(() => {
- if (!domains?.length) return
-
- const verifiedDomains = domains.filter(d => d.is_dns_verified && !d.is_sold)
- verifiedDomains.forEach(domain => {
- if (!healthReports[domain.id] && !loadingHealth[domain.id]) {
- setLoadingHealth(prev => ({ ...prev, [domain.id]: true }))
- // Note: This would need a portfolio-specific health endpoint
- // For now, we'll use the domain name to fetch health
- api.checkDomain(domain.domain)
- .then(() => {
- // Simulate health report - in production this would come from backend
- const simulatedReport: DomainHealthReport = {
- domain: domain.domain,
- checked_at: new Date().toISOString(),
- score: Math.floor(Math.random() * 40) + 60, // Simulated score 60-100
- status: 'healthy' as HealthStatus,
- signals: [],
- recommendations: [],
- dns: { has_a: true, has_ns: true, has_mx: false, nameservers: [], is_parked: false },
- http: { is_reachable: true, status_code: 200, is_parked: false },
- ssl: { has_certificate: true, is_valid: true },
- }
- setHealthReports(prev => ({ ...prev, [domain.id]: simulatedReport }))
- })
- .catch(() => {})
- .finally(() => {
- setLoadingHealth(prev => ({ ...prev, [domain.id]: false }))
- })
- }
- })
- }, [domains, healthReports, loadingHealth])
-
- const handleRefreshHealth = useCallback(async (domainId: number, domainName: string) => {
- setLoadingHealth(prev => ({ ...prev, [domainId]: true }))
- try {
- await api.checkDomain(domainName)
- // Simulated - in production, this would return real health data
- const simulatedReport: DomainHealthReport = {
- domain: domainName,
- checked_at: new Date().toISOString(),
- score: Math.floor(Math.random() * 40) + 60,
- status: 'healthy' as HealthStatus,
- signals: [],
- recommendations: [],
- dns: { has_a: true, has_ns: true, has_mx: false, nameservers: [], is_parked: false },
- http: { is_reachable: true, status_code: 200, is_parked: false },
- ssl: { has_certificate: true, is_valid: true },
- }
- setHealthReports(prev => ({ ...prev, [domainId]: simulatedReport }))
- showToast('Health check complete', 'success')
- } catch {
- showToast('Health check failed', 'error')
- } finally {
- setLoadingHealth(prev => ({ ...prev, [domainId]: false }))
- }
- }, [showToast])
-
- // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
- // ALERT HANDLERS
- // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
-
- const handleToggleEmailAlert = useCallback(async (domainId: number, _currentValue: boolean) => {
- const currentEnabled = alertsEnabled[domainId] || false
- setTogglingAlerts(prev => ({ ...prev, [domainId]: true }))
- try {
- // This would call a backend endpoint to toggle email alerts
- // await api.updatePortfolioDomainAlerts(domainId, { email_alerts: !currentEnabled })
- setAlertsEnabled(prev => ({ ...prev, [domainId]: !currentEnabled }))
- showToast(!currentEnabled ? 'Alerts enabled' : 'Alerts disabled', 'success')
- } catch {
- showToast('Failed to update alert settings', 'error')
- } finally {
- setTogglingAlerts(prev => ({ ...prev, [domainId]: false }))
- }
- }, [showToast, alertsEnabled])
-
- const handleToggleSmsAlert = useCallback(async (domainId: number, currentValue: boolean) => {
- if (!canUseSmsAlerts) {
- showToast('SMS alerts require Tycoon plan', 'error')
- return
- }
- setTogglingAlerts(prev => ({ ...prev, [domainId]: true }))
- try {
- // This would call a backend endpoint to toggle SMS alerts
- // await api.updatePortfolioDomainAlerts(domainId, { sms_alerts: !currentValue })
- showToast(!currentValue ? 'SMS alerts enabled' : 'SMS alerts disabled', 'success')
- } catch {
- showToast('Failed to update alert settings', 'error')
- } finally {
- setTogglingAlerts(prev => ({ ...prev, [domainId]: false }))
- }
- }, [canUseSmsAlerts, showToast])
-
const loadData = useCallback(async () => {
setLoading(true)
try {
@@ -270,7 +117,7 @@ export default function PortfolioPage() {
useEffect(() => { loadData() }, [loadData])
- // Stats - Extended with health metrics
+ // Stats
const stats = useMemo(() => {
const active = domains.filter(d => !d.is_sold).length
const sold = domains.filter(d => d.is_sold).length
@@ -280,13 +127,8 @@ export default function PortfolioPage() {
const days = getDaysUntilRenewal(d.renewal_date)
return days !== null && days <= 30 && days > 0
}).length
-
- // Health stats
- const healthyCount = Object.values(healthReports).filter(h => h.status === 'healthy').length
- const criticalCount = Object.values(healthReports).filter(h => h.status === 'critical').length
-
- return { total: domains.length, active, sold, verified, renewingSoon, healthy: healthyCount, critical: criticalCount }
- }, [domains, healthReports])
+ return { total: domains.length, active, sold, verified, renewingSoon }
+ }, [domains])
// Filtered & Sorted
const filteredDomains = useMemo(() => {
@@ -306,16 +148,12 @@ export default function PortfolioPage() {
const aDate = a.renewal_date ? new Date(a.renewal_date).getTime() : Infinity
const bDate = b.renewal_date ? new Date(b.renewal_date).getTime() : Infinity
return mult * (aDate - bDate)
- case 'health':
- const aHealth = healthReports[a.id]?.score || 0
- const bHealth = healthReports[b.id]?.score || 0
- return mult * (aHealth - bHealth)
default: return 0
}
})
return filtered
- }, [domains, filter, sortField, sortDirection, healthReports])
+ }, [domains, filter, sortField, sortDirection])
const handleSort = useCallback((field: typeof sortField) => {
if (sortField === field) {
@@ -343,7 +181,7 @@ export default function PortfolioPage() {
await api.deletePortfolioDomain(id)
setDomains(prev => prev.filter(d => d.id !== id))
showToast('Domain removed', 'success')
- loadData() // Refresh summary
+ loadData()
} catch { showToast('Failed', 'error') }
finally { setDeletingId(null) }
}, [showToast, loadData])
@@ -355,7 +193,7 @@ export default function PortfolioPage() {
{ href: '/terminal/radar', label: 'Radar', icon: Target, active: false },
{ href: '/terminal/market', label: 'Market', icon: Gavel, active: false },
{ href: '/terminal/watchlist', label: 'Watch', icon: Eye, active: false },
- { href: '/terminal/intel', label: 'Intel', icon: TrendingUp, active: false },
+ { href: '/terminal/portfolio', label: 'Portfolio', icon: Briefcase, active: true },
]
const drawerNavSections = [
@@ -380,25 +218,27 @@ export default function PortfolioPage() {
+ {/* βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */}
{/* MOBILE HEADER */}
+ {/* βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */}
-
My Portfolio
+
Portfolio
{stats.total} domains
-
+
- {/* Stats Grid - Extended with Health */}
-
+ {/* Stats Grid */}
+
{formatCurrency(summary?.total_value || 0).replace('$', '').slice(0, 6)}
Value
@@ -410,18 +250,16 @@ export default function PortfolioPage() {
ROI
-
{stats.healthy}
-
Healthy
-
-
-
{stats.renewingSoon}
-
Renew
+
{stats.verified}
+
Verified
+ {/* βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */}
{/* DESKTOP HEADER */}
+ {/* βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */}
@@ -434,41 +272,39 @@ export default function PortfolioPage() {
{stats.total}
- Manage your domain assets. Track value, monitor health, and list for sale.
+ Manage your domain assets. Track value, verify ownership, and list for sale.
-
+
-
{formatCurrency(summary?.total_invested || 0)}
+
{formatCurrency(summary?.total_invested || 0)}
Invested
-
{formatCurrency(summary?.total_value || 0)}
+
{formatCurrency(summary?.total_value || 0)}
Value
-
= 0 ? "text-accent" : "text-rose-400")}>
+
= 0 ? "text-accent" : "text-rose-400")}>
{formatROI(summary?.overall_roi || 0)}
ROI
-
-
{stats.healthy}
-
Healthy
-
-
-
{stats.renewingSoon}
-
Renewing
+
+
{stats.verified}
+
Verified
- {/* ADD DOMAIN + FILTERS */}
+ {/* βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */}
+ {/* FILTERS + ADD BUTTON */}
+ {/* βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */}
- {/* Filters - LEFT */}
+ {/* Filters */}
{[
{ value: 'all', label: 'All', count: stats.total },
@@ -488,19 +324,22 @@ export default function PortfolioPage() {
{item.label} ({item.count})
))}
-
+
- {/* Add Domain Button - RIGHT */}
+ {/* Add Domain Button */}
setShowAddModal(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 transition-colors"
>
- Add Domain
+
+ Add Domain
+ {/* βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */}
{/* DOMAIN LIST */}
+ {/* βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */}
{loading ? (
@@ -519,26 +358,26 @@ export default function PortfolioPage() {
) : (
-
- {/* Desktop Table Header - Matches Watchlist Style */}
-
+
+ {/* Desktop Table Header */}
+
handleSort('domain')} className="flex items-center gap-1 hover:text-white/60 text-left">
Domain
{sortField === 'domain' && (sortDirection === 'asc' ? : )}
-
handleSort('health')} className="flex items-center gap-1 justify-center hover:text-white/60">
- Health
- {sortField === 'health' && (sortDirection === 'asc' ? : )}
-
handleSort('value')} className="flex items-center gap-1 justify-end hover:text-white/60">
Value
{sortField === 'value' && (sortDirection === 'asc' ? : )}
+
handleSort('roi')} className="flex items-center gap-1 justify-end hover:text-white/60">
+ ROI
+ {sortField === 'roi' && (sortDirection === 'asc' ? : )}
+
handleSort('renewal')} className="flex items-center gap-1 justify-center hover:text-white/60">
- Expiry
+ Expires
{sortField === 'renewal' && (sortDirection === 'asc' ? : )}
-
Alert
+
Status
Actions
@@ -549,35 +388,27 @@ export default function PortfolioPage() {
return (
- {/* Mobile Row - Extended with Health & Alerts */}
+ {/* Mobile Row */}
-
+
{domain.is_sold ? : }
{domain.domain}
-
-
{domain.registrar || 'Unknown'}
- {/* Health Badge - Mobile */}
- {!domain.is_sold && domain.is_dns_verified && (() => {
- const health = healthReports[domain.id]
- if (!health) return null
- const config = healthConfig[health.status]
- return (
-
setShowHealthDetail(domain.id)}
- className={clsx("flex items-center gap-1 px-1 py-0.5 text-[9px] font-mono border", config.bg, config.color)}
- >
- {health.status === 'healthy' ? : }
- {health.score}
-
- )
- })()}
+
+ {domain.registrar && (
+ {domain.registrar}
+ )}
+ {domain.is_dns_verified && (
+
+ Verified
+
+ )}
@@ -590,237 +421,134 @@ export default function PortfolioPage() {
- {/* Info Row - Renewal & Alerts */}
+ {/* Mobile Actions */}
{!domain.is_sold && (
-
-
- {/* Renewal Info */}
- {daysUntilRenewal && (
-
-
-
- {isRenewingSoon ? `${daysUntilRenewal}d` : formatDate(domain.renewal_date)}
-
-
- )}
-
-
- {/* Alert Toggles - Mobile */}
-
-
handleToggleEmailAlert(domain.id, false)}
- className="w-7 h-7 flex items-center justify-center text-white/30 hover:text-accent border border-white/[0.06]"
- title="Email alerts"
+
+
+
+ {daysUntilRenewal !== null ? (
+
+ {daysUntilRenewal}d
+
+ ) : 'β'}
+
+
+ {!domain.is_dns_verified && (
+ setVerifyingDomain(domain)}
+ className="px-2 py-1.5 text-[9px] font-mono uppercase border border-amber-400/30 text-amber-400 bg-amber-400/5"
+ >
+ Verify
+
+ )}
+ handleRefreshValue(domain.id)}
+ disabled={refreshingId === domain.id}
+ className="p-1.5 text-white/30 hover:text-white disabled:animate-spin"
>
-
+
handleToggleSmsAlert(domain.id, false)}
- disabled={!canUseSmsAlerts}
- className={clsx(
- "w-7 h-7 flex items-center justify-center border border-white/[0.06]",
- canUseSmsAlerts ? "text-white/30 hover:text-accent" : "text-white/10"
- )}
- title={canUseSmsAlerts ? "SMS alerts" : "SMS requires Tycoon"}
+ onClick={() => handleDelete(domain.id, domain.domain)}
+ disabled={deletingId === domain.id}
+ className="p-1.5 text-white/30 hover:text-rose-400"
>
- {canUseSmsAlerts ? : }
+
)}
-
- {/* Action Buttons */}
-
- {!domain.is_sold && (
- domain.is_dns_verified ? (
- <>
- {canListForSale && (
-
- Sell
-
- )}
- {canUseYield && (
- setShowYieldModal(domain)}
- className="flex-1 py-2 bg-accent/10 border border-accent/20 text-accent text-[10px] font-bold uppercase flex items-center justify-center gap-1"
- >
- Yield
-
- )}
- >
- ) : (
- setVerifyingDomain(domain)}
- className="flex-1 py-2 bg-blue-400/10 border border-blue-400/20 text-blue-400 text-[10px] font-bold uppercase flex items-center justify-center gap-1"
- >
- Verify
-
- )
- )}
- setSelectedDomain(domain)}
- className="flex-1 py-2 border border-white/[0.08] text-[10px] font-mono text-white/40 flex items-center justify-center gap-1"
- >
- Details
-
- handleDelete(domain.id, domain.domain)}
- disabled={deletingId === domain.id}
- className="px-4 py-2 border border-white/[0.08] text-white/40 hover:text-rose-400"
- >
- {deletingId === domain.id ? : }
-
-
- {/* Desktop Row - Matches Watchlist Style */}
-
- {/* Domain Info */}
-
+ {/* Desktop Row */}
+
+ {/* Domain */}
+
- {domain.is_sold ? :
- domain.is_dns_verified ? : }
+ {domain.is_sold ? : }
-
{domain.domain}
-
- {domain.registrar || 'Unknown'}
- {domain.is_sold && β’ Sold }
- {!domain.is_sold && domain.is_dns_verified && β’ Verified }
-
-
-
-
-
-
-
- {/* Health - Like Watchlist */}
-
{
- if (domain.is_dns_verified && !domain.is_sold) {
- const health = healthReports[domain.id]
- if (health) setShowHealthDetail(domain.id)
- else handleRefreshHealth(domain.id, domain.domain)
- }
- }}
- disabled={domain.is_sold || !domain.is_dns_verified}
- className="w-20 flex items-center gap-1.5 hover:opacity-80 transition-opacity shrink-0"
- >
- {domain.is_sold ? (
- β
- ) : !domain.is_dns_verified ? (
- Verify
- ) : loadingHealth[domain.id] ? (
-
- ) : (() => {
- const health = healthReports[domain.id]
- if (!health) return
- const config = healthConfig[health.status]
- return (
- <>
-
- {config.label}
- >
- )
- })()}
-
-
- {/* Value + ROI combined */}
-
-
{formatCurrency(domain.estimated_value)}
-
- {roiPositive ? '+' : ''}{formatROI(domain.roi)}
+
{domain.domain}
+
{domain.registrar || 'Unknown'}
- {/* Expiry - Like Watchlist */}
-
- {domain.is_sold ? (
-
β
- ) : isRenewingSoon ? (
-
{daysUntilRenewal}d
- ) : daysUntilRenewal ? (
-
{daysUntilRenewal}d
- ) : (
- formatDate(domain.renewal_date)
+ {/* Value */}
+
+
{formatCurrency(domain.estimated_value)}
+ {domain.purchase_price && (
+
Cost: {formatCurrency(domain.purchase_price)}
)}
- {/* Alert Toggle - Like Watchlist Bell */}
-
handleToggleEmailAlert(domain.id, false)}
- disabled={togglingAlerts[domain.id] || domain.is_sold}
- className={clsx(
- "w-8 h-8 flex items-center justify-center border transition-colors shrink-0",
- domain.is_sold
- ? "text-white/10 border-white/5"
- : alertsEnabled[domain.id]
- ? "text-accent border-accent/20 bg-accent/10"
- : "text-white/20 border-white/10 hover:text-white/40"
- )}
- >
- {togglingAlerts[domain.id] ? (
-
- ) : alertsEnabled[domain.id] ? (
-
- ) : (
-
- )}
-
+ {/* ROI */}
+
+
+ {formatROI(domain.roi)}
+
+
- {/* Actions - Like Watchlist */}
-
- {/* Primary Action - Verify or Sell */}
- {!domain.is_sold && (
- domain.is_dns_verified ? (
- canListForSale && (
-
- Sell
-
-
- )
- ) : (
-
setVerifyingDomain(domain)}
- className="h-7 px-3 bg-blue-400/10 text-blue-400 text-xs font-bold flex items-center gap-1.5 border border-blue-400/20 hover:bg-blue-400/20 transition-colors"
- >
- Verify
-
- )
+ {/* Expires */}
+
+ {daysUntilRenewal !== null ? (
+
+ {daysUntilRenewal}d
+
+ ) : (
+ β
+ )}
+
+
+ {/* Status */}
+
+ {domain.is_sold ? (
+ Sold
+ ) : domain.is_dns_verified ? (
+
+ Verified
+
+ ) : (
+ setVerifyingDomain(domain)}
+ className="px-2 py-1 text-[9px] font-mono uppercase border border-amber-400/30 text-amber-400 bg-amber-400/5 hover:bg-amber-400/10 transition-colors"
+ >
+ Verify
+
+ )}
+
+
+ {/* Actions */}
+
+ {!domain.is_sold && domain.is_dns_verified && (
+
+ Sell
+
)}
- setSelectedDomain(domain)}
- className="w-7 h-7 flex items-center justify-center text-white/20 hover:text-white border border-white/10 hover:bg-white/5 transition-all"
- >
-
-
handleRefreshValue(domain.id)}
disabled={refreshingId === domain.id}
- className="w-7 h-7 flex items-center justify-center text-white/20 hover:text-white border border-white/10 hover:bg-white/5 transition-all"
+ className={clsx(
+ "p-1.5 text-white/30 hover:text-white transition-colors",
+ refreshingId === domain.id && "animate-spin"
+ )}
>
-
+
handleDelete(domain.id, domain.domain)}
disabled={deletingId === domain.id}
- className="w-7 h-7 flex items-center justify-center text-white/20 hover:text-rose-400 border border-white/10 hover:border-rose-400/20 hover:bg-rose-500/10 transition-all"
+ className="p-1.5 text-white/30 hover:text-rose-400 transition-colors"
>
- {deletingId === domain.id ? (
-
- ) : (
-
- )}
+
@@ -831,192 +559,125 @@ export default function PortfolioPage() {
)}
- {/* MOBILE BOTTOM NAV */}
-
-
+ {/* βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */}
+ {/* MOBILE BOTTOM NAVIGATION */}
+ {/* βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */}
+
+
{mobileNavItems.map((item) => (
-
- {item.active &&
}
+
{item.label}
))}
-
setMenuOpen(true)} className="flex-1 flex flex-col items-center justify-center gap-0.5 text-white/40">
- Menu
+ setNavDrawerOpen(true)}
+ className="flex flex-col items-center gap-0.5 py-2 text-white/40"
+ >
+
+ Menu
-
+
- {/* MOBILE DRAWER */}
- {menuOpen &&
setMenuOpen(false)} onLogout={() => { logout(); setMenuOpen(false) }} />}
-
-
- {/* ADD DOMAIN MODAL */}
- {showAddModal && setShowAddModal(false)} onSuccess={() => { loadData(); setShowAddModal(false) }} />}
-
- {/* DOMAIN DETAIL MODAL */}
- {selectedDomain && setSelectedDomain(null)} onUpdate={loadData} canListForSale={canListForSale} />}
-
- {/* DNS VERIFICATION MODAL */}
- {verifyingDomain && setVerifyingDomain(null)} onSuccess={() => { loadData(); setVerifyingDomain(null) }} />}
-
- {/* HEALTH DETAIL MODAL - NEW */}
- {showHealthDetail && (() => {
- const domain = domains.find(d => d.id === showHealthDetail)
- const health = healthReports[showHealthDetail]
- if (!domain || !health) return null
- const config = healthConfig[health.status]
-
- return (
- setShowHealthDetail(null)}>
-
e.stopPropagation()}>
-
-
-
setShowHealthDetail(null)} className="w-8 h-8 flex items-center justify-center border border-white/10 text-white/40 hover:text-white">
-
-
-
-
-
- {/* Domain & Score */}
+ {/* Navigation Drawer */}
+ {navDrawerOpen && (
+
+
setNavDrawerOpen(false)} />
+
+
-
{domain.domain}
-
- {health.score}
- {config.label}
-
-
-
- {/* Health Checks */}
-
-
System Checks
-
-
-
-
- DNS Resolution
-
- {health.dns?.has_a || health.dns?.has_ns ? (
-
OK
- ) : (
-
Failed
- )}
-
-
-
-
-
- HTTP Reachable
-
- {health.http?.is_reachable ? (
-
OK ({health.http.status_code})
- ) : (
-
Failed
- )}
-
-
-
-
-
- SSL Certificate
-
- {health.ssl?.has_certificate ? (
-
Valid
- ) : (
-
Missing
- )}
-
-
-
-
- {!health.dns?.is_parked && !health.http?.is_parked ? (
-
Not Parked
- ) : (
-
Parked
- )}
+
+
+ Terminal
+
setNavDrawerOpen(false)} className="p-1 text-white/40 hover:text-white">
+
+
+
+
+
+ {drawerNavSections.map((section) => (
+
+
{section.title}
+
+ {section.items.map((item) => (
+ setNavDrawerOpen(false)}
+ className={clsx(
+ "flex items-center gap-3 px-3 py-2.5 transition-colors",
+ item.active ? "bg-accent/10 text-accent" : "text-white/60 hover:text-white hover:bg-white/[0.02]"
+ )}
+ >
+
+ {item.label}
+ {item.isNew && (
+ New
+ )}
+
+ ))}
+
+
+ ))}
- {/* Last Check */}
-
-
Last checked: {formatTimeAgo(health.checked_at)}
-
{ handleRefreshHealth(domain.id, domain.domain); setShowHealthDetail(null) }}
- className="text-accent hover:underline"
- >
- Refresh
-
+
+ setNavDrawerOpen(false)}
+ className="flex items-center gap-3 px-3 py-2.5 text-white/60 hover:text-white transition-colors"
+ >
+
+ Settings
+
+ { logout(); setNavDrawerOpen(false) }}
+ className="w-full flex items-center gap-3 px-3 py-2.5 text-rose-400/60 hover:text-rose-400 transition-colors"
+ >
+
+ Logout
+
- )
- })()}
-
- {/* YIELD ACTIVATION MODAL - Phase 2 Preview - NEW */}
- {showYieldModal && (
-
setShowYieldModal(null)}>
-
e.stopPropagation()}>
-
-
-
- Yield Activation
-
-
setShowYieldModal(null)} className="w-8 h-8 flex items-center justify-center border border-white/10 text-white/40 hover:text-white">
-
-
-
-
-
-
-
-
-
-
-
Yield Coming Soon
-
- Turn your parked domains into revenue generators with AI-powered intent routing.
-
-
-
-
-
How it works
-
-
1
-
Point your nameservers to ns.pounce.ch
-
-
-
2
-
We analyze visitor intent and route traffic
-
-
-
3
-
Earn up to 70% of affiliate revenue
-
-
-
-
- setShowYieldModal(null)}
- className="w-full py-3 bg-white/5 border border-white/10 text-white/40 text-sm font-mono"
- >
- Notify me when available
-
-
-
-
-
+ )}
+
+
+ {/* βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */}
+ {/* ADD DOMAIN MODAL */}
+ {/* βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */}
+ {showAddModal && (
+
setShowAddModal(false)}
+ onSuccess={() => { setShowAddModal(false); loadData() }}
+ showToast={showToast}
+ />
)}
-
+
+ {/* βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */}
+ {/* DNS VERIFICATION MODAL */}
+ {/* βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */}
+ {verifyingDomain && (
+ setVerifyingDomain(null)}
+ onVerified={() => { setVerifyingDomain(null); loadData() }}
+ showToast={showToast}
+ />
+ )}
+
{toast && }
-
+
)
}
@@ -1024,460 +685,228 @@ export default function PortfolioPage() {
// ADD DOMAIN MODAL
// ============================================================================
-function AddDomainModal({ onClose, onSuccess }: { onClose: () => void; onSuccess: () => void }) {
+function AddDomainModal({ onClose, onSuccess, showToast }: {
+ onClose: () => void
+ onSuccess: () => void
+ showToast: (msg: string, type: 'success' | 'error') => void
+}) {
const [domain, setDomain] = useState('')
const [purchasePrice, setPurchasePrice] = useState('')
- const [purchaseDate, setPurchaseDate] = useState('')
const [registrar, setRegistrar] = useState('')
- const [renewalDate, setRenewalDate] = useState('')
- const [renewalCost, setRenewalCost] = useState('')
const [loading, setLoading] = useState(false)
- const [error, setError] = useState
(null)
-
+
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
if (!domain.trim()) return
+
setLoading(true)
- setError(null)
try {
await api.addPortfolioDomain({
- domain: domain.trim(),
+ domain: domain.trim().toLowerCase(),
purchase_price: purchasePrice ? parseFloat(purchasePrice) : undefined,
- purchase_date: purchaseDate || undefined,
registrar: registrar || undefined,
- renewal_date: renewalDate || undefined,
- renewal_cost: renewalCost ? parseFloat(renewalCost) : undefined,
})
+ showToast('Domain added successfully', 'success')
onSuccess()
} catch (err: any) {
- setError(err.message || 'Failed to add domain')
+ showToast(err.message || 'Failed to add domain', 'error')
} finally {
setLoading(false)
}
}
-
+
return (
-
-
e.stopPropagation()}>
-
-
-
-
- )
-}
-
-// ============================================================================
-// DOMAIN DETAIL MODAL
-// ============================================================================
-
-function DomainDetailModal({ domain, onClose, onUpdate, canListForSale }: { domain: PortfolioDomain; onClose: () => void; onUpdate: () => void; canListForSale: boolean }) {
- const [notes, setNotes] = useState(domain.notes || '')
- const [tags, setTags] = useState(domain.tags || '')
- const [showSellModal, setShowSellModal] = useState(false)
- const [saving, setSaving] = useState(false)
-
- const handleSave = async () => {
- setSaving(true)
- try {
- await api.updatePortfolioDomain(domain.id, { notes, tags })
- onUpdate()
- onClose()
- } catch {}
- finally { setSaving(false) }
- }
-
- const handleMarkSold = async (saleDate: string, salePrice: number) => {
- try {
- await api.markDomainSold(domain.id, saleDate, salePrice)
- onUpdate()
- onClose()
- } catch {}
- }
-
- return (
-
-
e.stopPropagation()}>
-
-
-
- {/* Domain Header */}
-
-
{domain.domain}
-
{domain.registrar || 'Unknown registrar'}
-
-
- {/* Stats Grid */}
-
-
-
{formatCurrency(domain.purchase_price)}
-
Purchased
-
-
-
{formatCurrency(domain.estimated_value)}
-
Est. Value
-
-
= 0 ? "bg-accent/[0.05] border-accent/20" : "bg-rose-500/[0.05] border-rose-500/20")}>
-
= 0 ? "text-accent" : "text-rose-400")}>{formatROI(domain.roi)}
-
ROI
-
-
-
- {/* Dates */}
-
-
-
Purchase Date
-
{formatDate(domain.purchase_date)}
-
-
-
Renewal Date
-
{formatDate(domain.renewal_date)}
-
-
-
- {/* Notes */}
-
- Notes
-
-
- {/* Tags */}
-
- Tags (comma-separated)
- setTags(e.target.value)}
- className="w-full px-3 py-2.5 bg-white/5 border border-white/10 text-white text-sm font-mono placeholder:text-white/20 outline-none focus:border-accent/50" placeholder="premium, 3-letter, .com" />
-
-
- {/* Actions */}
-
- {!domain.is_sold && (
-
- {canListForSale ? (
-
- List for Sale
-
- ) : (
-
- Upgrade to Sell
-
- )}
- setShowSellModal(true)} className="flex-1 py-2.5 border border-white/10 text-white/60 text-xs font-mono uppercase hover:bg-white/5 transition-colors flex items-center justify-center gap-2">
- Mark as Sold
-
-
- )}
-
- {saving ? : }Save
+
+
+
+
- {/* Sell Modal */}
- {showSellModal &&
setShowSellModal(false)} onConfirm={handleMarkSold} />}
-
-
- )
-}
-
-// ============================================================================
-// SELL MODAL
-// ============================================================================
-
-function SellModal({ onClose, onConfirm }: { onClose: () => void; onConfirm: (date: string, price: number) => void }) {
- const [saleDate, setSaleDate] = useState(new Date().toISOString().split('T')[0])
- const [salePrice, setSalePrice] = useState('')
-
- return (
-
-
e.stopPropagation()}>
-
Mark as Sold
-
-
- Cancel
- onConfirm(saleDate, parseFloat(salePrice) || 0)} disabled={!salePrice} className="flex-1 py-2.5 bg-accent text-black text-xs font-bold uppercase disabled:opacity-50">Confirm
-
+
+
+ Purchase Price (USD)
+ setPurchasePrice(e.target.value)}
+ placeholder="100"
+ className="w-full px-4 py-3 bg-white/[0.02] border border-white/[0.08] text-white text-sm font-mono placeholder:text-white/20 focus:border-accent focus:outline-none"
+ />
+
+
+
+ Registrar
+ setRegistrar(e.target.value)}
+ placeholder="Namecheap, GoDaddy, etc."
+ className="w-full px-4 py-3 bg-white/[0.02] border border-white/[0.08] text-white text-sm font-mono placeholder:text-white/20 focus:border-accent focus:outline-none"
+ />
+
+
+
+ {loading ? : }
+ Add to Portfolio
+
+
)
}
-// ============================================================================
-// MOBILE DRAWER
-// ============================================================================
-
-function MobileDrawer({ user, tierName, TierIcon, sections, onClose, onLogout }: any) {
- return (
-
-
-
-
-
- {sections.map((section: any) => (
-
-
- {section.items.map((item: any) => (
-
-
{item.label}
- {item.isNew &&
NEW }
-
- ))}
-
- ))}
-
- Settings
- {user?.is_admin && Admin }
-
-
-
-
-
-
{user?.name || user?.email?.split('@')[0] || 'User'}
{tierName}
-
- {tierName === 'Scout' &&
Upgrade}
-
Sign out
-
-
-
- )
-}
-
// ============================================================================
// DNS VERIFICATION MODAL
// ============================================================================
-function DnsVerificationModal({ domain, onClose, onSuccess }: { domain: PortfolioDomain; onClose: () => void; onSuccess: () => void }) {
- const [step, setStep] = useState<'loading' | 'instructions' | 'checking'>('loading')
+function DNSVerificationModal({ domain, onClose, onVerified, showToast }: {
+ domain: PortfolioDomain
+ onClose: () => void
+ onVerified: () => void
+ showToast: (msg: string, type: 'success' | 'error') => void
+}) {
+ const [loading, setLoading] = useState(true)
+ const [checking, setChecking] = useState(false)
const [verificationData, setVerificationData] = useState<{
verification_code: string
dns_record_name: string
dns_record_value: string
instructions: string
} | null>(null)
- const [error, setError] = useState(null)
- const [checkResult, setCheckResult] = useState(null)
- const [copied, setCopied] = useState(false)
-
+ const [copied, setCopied] = useState(null)
+
useEffect(() => {
- const startVerification = async () => {
- try {
- const data = await api.startPortfolioDnsVerification(domain.id)
- setVerificationData({
- verification_code: data.verification_code,
- dns_record_name: data.dns_record_name,
- dns_record_value: data.dns_record_value,
- instructions: data.instructions,
- })
- setStep('instructions')
- } catch (err: any) {
- setError(err.message || 'Failed to start verification')
- setStep('instructions')
- }
- }
- startVerification()
+ loadVerificationData()
}, [domain.id])
-
+
+ const loadVerificationData = async () => {
+ try {
+ const data = await api.startPortfolioDnsVerification(domain.id)
+ setVerificationData(data)
+ } catch (err: any) {
+ showToast(err.message || 'Failed to start verification', 'error')
+ onClose()
+ } finally {
+ setLoading(false)
+ }
+ }
+
const handleCheck = async () => {
- setStep('checking')
- setCheckResult(null)
- setError(null)
+ setChecking(true)
try {
const result = await api.checkPortfolioDnsVerification(domain.id)
if (result.verified) {
- onSuccess()
+ showToast('Domain verified successfully!', 'success')
+ onVerified()
} else {
- setCheckResult(result.message)
- setStep('instructions')
+ showToast(result.message || 'Verification pending', 'error')
}
} catch (err: any) {
- setError(err.message || 'Verification check failed')
- setStep('instructions')
+ showToast(err.message || 'Verification failed', 'error')
+ } finally {
+ setChecking(false)
}
}
-
- const handleCopy = (text: string) => {
- navigator.clipboard.writeText(text)
- setCopied(true)
- setTimeout(() => setCopied(false), 2000)
+
+ const copyToClipboard = async (text: string, field: string) => {
+ try {
+ await navigator.clipboard.writeText(text)
+ setCopied(field)
+ setTimeout(() => setCopied(null), 2000)
+ } catch {}
}
-
+
return (
-
-
e.stopPropagation()}>
-
-
-
- Verify Domain Ownership
-
-
-
-
-
-
-
- {/* Domain Header */}
-
-
{domain.domain}
-
DNS Verification Required
-
-
- {step === 'loading' && (
-
-
-
- )}
-
- {step === 'instructions' && verificationData && (
- <>
- {/* Instructions */}
-
-
Add this TXT record to your DNS:
-
-
-
Host / Name
-
- _pounce
- handleCopy('_pounce')} className="w-8 h-8 flex items-center justify-center border border-white/10 text-white/40 hover:text-white">
- {copied ? : }
-
-
-
-
-
-
Value
-
- {verificationData.verification_code}
- handleCopy(verificationData.verification_code)} className="w-8 h-8 flex items-center justify-center border border-white/10 text-white/40 hover:text-white">
- {copied ? : }
-
-
-
-
+
+
+
+
+
+
Verify Ownership
+
+
+
+
+
Add a DNS TXT record to prove you own {domain.domain}
-
- {/* Info */}
-
-
DNS changes can take up to 48 hours to propagate, but usually complete within minutes.
-
-
- {/* Error/Check Result */}
- {error && (
-
- {error}
-
- )}
- {checkResult && (
-
- {checkResult}
-
- )}
-
- {/* Actions */}
-
-
- Cancel
+
+ {loading ? (
+
+
+
+ ) : verificationData ? (
+
+
+
Host / Name
+
+
+ {verificationData.dns_record_name}
+
+
copyToClipboard(verificationData.dns_record_name, 'name')}
+ className="p-3 border border-white/[0.08] text-white/40 hover:text-white"
+ >
+ {copied === 'name' ? : }
-
- Check Verification
-
-
- >
- )}
-
- {step === 'checking' && (
-
-
-
Checking DNS records...
-
- )}
-
- {step === 'instructions' && !verificationData && error && (
-
- )}
-
+
+
+
TXT Value
+
+
+ {verificationData.verification_code}
+
+
copyToClipboard(verificationData.verification_code, 'value')}
+ className="p-3 border border-white/[0.08] text-white/40 hover:text-white"
+ >
+ {copied === 'value' ? : }
+
+
+
+
+
+ π‘ DNS changes can take 1-5 minutes to propagate. If verification fails, wait a moment and try again.
+
+
+
+ {checking ? : }
+ Check Verification
+
+
+ ) : (
+
+ Failed to load verification data
+
+ )}
)
diff --git a/frontend/src/app/terminal/radar/page.tsx b/frontend/src/app/terminal/radar/page.tsx
index f03b66a..5541697 100644
--- a/frontend/src/app/terminal/radar/page.tsx
+++ b/frontend/src/app/terminal/radar/page.tsx
@@ -47,11 +47,30 @@ import Image from 'next/image'
interface HotAuction {
domain: string
current_bid: number
- time_remaining: string
+ end_time?: string
platform: 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 {
domain: string
status: string
@@ -74,6 +93,7 @@ export default function RadarPage() {
const [hotAuctions, setHotAuctions] = useState
([])
const [marketStats, setMarketStats] = useState({ totalAuctions: 0, endingSoon: 0 })
const [loadingData, setLoadingData] = useState(true)
+ const [tick, setTick] = useState(0)
const [searchQuery, setSearchQuery] = useState('')
const [searchResult, setSearchResult] = useState(null)
@@ -96,33 +116,27 @@ export default function RadarPage() {
// Load Data - Using same API as Market page for consistency
const loadDashboardData = useCallback(async () => {
try {
- // Use getMarketFeed for consistency with Market & Acquire pages
- const result = await api.getMarketFeed({
- source: 'all',
- sortBy: 'time',
- limit: 10, // Show top 10 ending soon
- })
-
- // Convert to HotAuction format
- const auctions = (result.items || [])
+ // External auctions only (Pounce Direct has no end_time)
+ const [feed, ending24h] = await Promise.all([
+ api.getMarketFeed({ source: 'external', sortBy: 'time', limit: 10 }),
+ api.getMarketFeed({ source: 'external', endingWithin: 24, sortBy: 'time', limit: 1 }),
+ ])
+
+ const auctions: HotAuction[] = (feed.items || [])
.filter((item: any) => item.status === 'auction' && item.end_time)
.slice(0, 6)
.map((item: any) => ({
domain: item.domain,
current_bid: item.price || 0,
- time_remaining: item.time_remaining || '',
+ end_time: item.end_time || undefined,
platform: item.source || 'Unknown',
affiliate_url: item.url || '',
}))
-
+
setHotAuctions(auctions)
setMarketStats({
- totalAuctions: result.auction_count || result.total || 0,
- endingSoon: result.items?.filter((i: any) => {
- 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,
+ totalAuctions: feed.total || feed.auction_count || 0,
+ endingSoon: ending24h.total || 0,
})
} catch (error) {
console.error('Failed to load data:', error)
@@ -135,6 +149,8 @@ export default function RadarPage() {
if (!authLoading) {
if (isAuthenticated) {
loadDashboardData()
+ const interval = setInterval(() => setTick(t => t + 1), 30000)
+ return () => clearInterval(interval)
} else {
setLoadingData(false)
}
@@ -149,18 +165,13 @@ export default function RadarPage() {
case 'domain': return mult * a.domain.localeCompare(b.domain)
case 'bid': return mult * (a.current_bid - b.current_bid)
case 'time':
- // Parse time_remaining like "2h 30m" or "5d 12h"
- const parseTime = (t: string) => {
- const d = t.match(/(\d+)d/)?.[1] || 0
- 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))
+ const aTime = a.end_time ? new Date(a.end_time).getTime() : Infinity
+ const bTime = b.end_time ? new Date(b.end_time).getTime() : Infinity
+ return mult * (aTime - bTime)
default: return 0
}
})
- }, [hotAuctions, auctionSort, auctionSortDir])
+ }, [hotAuctions, auctionSort, auctionSortDir, tick])
const handleAuctionSort = useCallback((field: typeof auctionSort) => {
if (auctionSort === field) {
@@ -289,7 +300,7 @@ export default function RadarPage() {
{marketStats.totalAuctions.toLocaleString()} auctions
- {marketStats.endingSoon} ending
+ {marketStats.endingSoon} ending 24h
@@ -323,7 +334,7 @@ export default function RadarPage() {
-
Intelligence Hub
+
Domain Radar
Domain Radar
@@ -602,7 +613,7 @@ export default function RadarPage() {
- {auction.time_remaining}
+ {calcTimeRemaining(auction.end_time)}
@@ -657,7 +668,7 @@ export default function RadarPage() {
- {auction.time_remaining}
+ {calcTimeRemaining(auction.end_time)}
diff --git a/frontend/src/app/terminal/settings/page.tsx b/frontend/src/app/terminal/settings/page.tsx
index 00687ed..ae749ac 100644
--- a/frontend/src/app/terminal/settings/page.tsx
+++ b/frontend/src/app/terminal/settings/page.tsx
@@ -143,7 +143,7 @@ export default function SettingsPage() {
const upgraded = params.get('upgraded')
const cancelled = params.get('cancelled')
if (upgraded) {
- setSuccess(`π Welcome to ${upgraded.charAt(0).toUpperCase() + upgraded.slice(1)}!`)
+ setSuccess(`Upgrade complete: ${upgraded.charAt(0).toUpperCase() + upgraded.slice(1)}`)
setActiveTab('billing')
window.history.replaceState({}, '', '/terminal/settings')
checkAuth()
diff --git a/frontend/src/app/terminal/welcome/page.tsx b/frontend/src/app/terminal/welcome/page.tsx
index 07414af..fb3456e 100644
--- a/frontend/src/app/terminal/welcome/page.tsx
+++ b/frontend/src/app/terminal/welcome/page.tsx
@@ -162,7 +162,7 @@ export default function WelcomePage() {
{/* Next Steps */}
- Get Started
+ Next Steps
{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
hover:bg-accent-hover transition-all shadow-lg shadow-accent/20"
>
- Go to Dashboard
+ Go to Radar
- Need help? Check out our documentation or{' '}
- contact support.
+ Need help? Contact support.
diff --git a/frontend/src/app/terminal/yield/page.tsx b/frontend/src/app/terminal/yield/page.tsx
index 6fef469..ed21c84 100644
--- a/frontend/src/app/terminal/yield/page.tsx
+++ b/frontend/src/app/terminal/yield/page.tsx
@@ -255,7 +255,7 @@ export default function YieldPage() {
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">
- Add Domain
+ Activate Domain
@@ -266,7 +266,7 @@ export default function YieldPage() {
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">
- Add Domain
+ Activate Domain
diff --git a/frontend/src/app/tld/[tld]/TldDetailClient.tsx b/frontend/src/app/tld/[tld]/TldDetailClient.tsx
index 7ab3083..e43f18e 100644
--- a/frontend/src/app/tld/[tld]/TldDetailClient.tsx
+++ b/frontend/src/app/tld/[tld]/TldDetailClient.tsx
@@ -417,7 +417,7 @@ export default function TldDetailClient({ tld, initialData }: Props) {
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"
>
- Start Free
+ Enter Terminal
diff --git a/frontend/src/components/Footer.tsx b/frontend/src/components/Footer.tsx
index 3a709be..4e2cea7 100644
--- a/frontend/src/components/Footer.tsx
+++ b/frontend/src/components/Footer.tsx
@@ -4,9 +4,34 @@ import Link from 'next/link'
import Image from 'next/image'
import { Twitter, Mail, Linkedin, ArrowRight } from 'lucide-react'
import { useStore } from '@/lib/store'
+import { api } from '@/lib/api'
+import { useCallback, useMemo, useState } from 'react'
export function Footer() {
const { isAuthenticated } = useStore()
+ const [newsletterEmail, setNewsletterEmail] = useState('')
+ const [newsletterState, setNewsletterState] = useState<'idle' | 'loading' | 'success' | 'error'>('idle')
+ const [newsletterError, setNewsletterError] = useState
(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 (
@@ -40,16 +65,31 @@ export function Footer() {
{/* Newsletter - Hidden on Mobile */}
Mission Briefings
-
+
+ {newsletterState === 'success' ? (
+
Subscribed. Briefings inbound.
+ ) : newsletterState === 'error' ? (
+
{newsletterError}
+ ) : (
+
Weekly intel. No spam.
+ )}
diff --git a/frontend/src/components/Header.tsx b/frontend/src/components/Header.tsx
index c8e174c..7550a69 100644
--- a/frontend/src/components/Header.tsx
+++ b/frontend/src/components/Header.tsx
@@ -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"
>
- Terminal
+ Enter Terminal
) : (
<>
@@ -104,7 +104,7 @@ export function Header() {
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"
>
- Start Free
+ Enter Terminal
>
)}
@@ -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"
>
- Open Terminal
+ Enter Terminal
- Start Free
+ Enter Terminal