Yves Gugger 4a1ebf0024
Some checks failed
CI / Frontend Lint & Type Check (push) Has been cancelled
CI / Frontend Build (push) Has been cancelled
CI / Backend Lint (push) Has been cancelled
CI / Backend Tests (push) Has been cancelled
CI / Docker Build (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
Deploy / Build & Push Images (push) Has been cancelled
Deploy / Deploy to Server (push) Has been cancelled
Deploy / Notify (push) Has been cancelled
feat: Mobile-optimized landing page + Header drawer + Footer improvements
2025-12-13 17:33:08 +01:00

1532 lines
41 KiB
TypeScript

/**
* API client for pounce backend
*
* API URL is determined dynamically based on the current hostname:
* - localhost/127.0.0.1 → http://localhost:8000/api/v1
* - Local network IPs (10.x, 192.168.x) → http://{hostname}:8000/api/v1
* - Production (any other domain) → https://{hostname}/api/v1 (requires reverse proxy)
*/
const getApiBase = (): string => {
// Server-side rendering: use environment variable or localhost
if (typeof window === 'undefined') {
// Prefer internal backend URL (server-only) when running in Docker/SSR
const backendUrl = process.env.BACKEND_URL?.replace(/\/$/, '')
const configured = (backendUrl ? `${backendUrl}/api/v1` : process.env.NEXT_PUBLIC_API_URL) || 'http://localhost:8000/api/v1'
const normalized = configured.replace(/\/$/, '')
return normalized.endsWith('/api/v1') ? normalized : `${normalized}/api/v1`
}
const { protocol, hostname, port } = window.location
// Localhost development
if (hostname === 'localhost' || hostname === '127.0.0.1') {
return 'http://localhost:8000/api/v1'
}
// Local network (10.x.x.x, 192.168.x.x, 172.16-31.x.x)
if (/^(10\.|192\.168\.|172\.(1[6-9]|2[0-9]|3[01])\.)/.test(hostname)) {
return `http://${hostname}:8000/api/v1`
}
// Production: use same protocol and domain with /api/v1 path
// This requires a reverse proxy (nginx/caddy) to route /api/v1 to the backend
return `${protocol}//${hostname}/api/v1`
}
// Lazy-evaluated to ensure window is available on client
let _apiBase: string | null = null
const getApiBaseUrl = (): string => {
if (_apiBase === null) {
_apiBase = getApiBase()
}
return _apiBase
}
interface ApiError {
detail: string
}
class ApiClient {
get baseUrl(): string {
return getApiBaseUrl().replace('/api/v1', '')
}
async request<T>(
endpoint: string,
options: RequestInit = {}
): Promise<T> {
const url = `${getApiBaseUrl()}${endpoint}`
const headers: Record<string, string> = {
'Content-Type': 'application/json',
...options.headers as Record<string, string>,
}
const response = await fetch(url, {
...options,
headers,
credentials: 'include', // send HttpOnly auth cookie
})
if (!response.ok) {
const errorData = await response.json().catch(() => ({ detail: 'An error occurred' }))
const errorMessage = typeof errorData.detail === 'string'
? errorData.detail
: (errorData.message || JSON.stringify(errorData.detail) || 'An error occurred')
throw new Error(errorMessage)
}
if (response.status === 204) {
return {} as T
}
return response.json()
}
// Auth
async register(email: string, password: string, name?: string) {
return this.request<{ id: number; email: string }>('/auth/register', {
method: 'POST',
body: JSON.stringify({ email, password, name }),
})
}
async login(email: string, password: string) {
const response = await this.request<{ expires_in: number }>(
'/auth/login',
{
method: 'POST',
body: JSON.stringify({ email, password }),
}
)
return response
}
async logout() {
// Clears auth cookie on the backend
return this.request<{ message: string; success: boolean }>('/auth/logout', {
method: 'POST',
})
}
async getMe() {
return this.request<{
id: number
email: string
name: string | null
is_active: boolean
is_admin: boolean
is_verified: boolean
created_at: string
}>('/auth/me')
}
// Dashboard (Terminal Radar) - single call payload
async getDashboardSummary() {
return this.request<{
market: {
total_auctions: number
ending_soon: number
ending_soon_preview: Array<{
domain: string
current_bid: number
time_remaining: string
platform: string
affiliate_url?: string
}>
}
listings: { active: number; sold: number; draft: number; total: number }
tlds: { trending: Array<{ tld: string; reason: string; price_change: number; current_price: number }> }
timestamp: string
}>('/dashboard/summary')
}
async updateMe(data: { name?: string }) {
return this.request<{
id: number
email: string
name: string | null
is_active: boolean
created_at: string
}>('/auth/me', {
method: 'PUT',
body: JSON.stringify(data),
})
}
// Password Reset
async forgotPassword(email: string) {
return this.request<{ message: string; success: boolean }>('/auth/forgot-password', {
method: 'POST',
body: JSON.stringify({ email }),
})
}
async resetPassword(token: string, newPassword: string) {
return this.request<{ message: string; success: boolean }>('/auth/reset-password', {
method: 'POST',
body: JSON.stringify({ token, new_password: newPassword }),
})
}
// Email Verification
async verifyEmail(token: string) {
return this.request<{ message: string; success: boolean }>('/auth/verify-email', {
method: 'POST',
body: JSON.stringify({ token }),
})
}
async resendVerification(email: string) {
return this.request<{ message: string; success: boolean }>('/auth/resend-verification', {
method: 'POST',
body: JSON.stringify({ email }),
})
}
// OAuth
async getOAuthProviders() {
return this.request<{ google_enabled: boolean; github_enabled: boolean }>('/oauth/providers')
}
getGoogleLoginUrl(redirect?: string) {
const params = redirect ? `?redirect=${encodeURIComponent(redirect)}` : ''
return `${getApiBaseUrl()}/oauth/google/login${params}`
}
getGitHubLoginUrl(redirect?: string) {
const params = redirect ? `?redirect=${encodeURIComponent(redirect)}` : ''
return `${getApiBaseUrl()}/oauth/github/login${params}`
}
// Contact Form
async submitContact(name: string, email: string, subject: string, message: string) {
return this.request<{ message: string; success: boolean }>('/contact', {
method: 'POST',
body: JSON.stringify({ name, email, subject, message }),
})
}
// Newsletter
async subscribeNewsletter(email: string) {
return this.request<{ message: string; success: boolean }>('/contact/newsletter/subscribe', {
method: 'POST',
body: JSON.stringify({ email }),
})
}
async unsubscribeNewsletter(email: string, token?: string) {
return this.request<{ message: string; success: boolean }>('/contact/newsletter/unsubscribe', {
method: 'POST',
body: JSON.stringify({ email, token }),
})
}
// Subscription - Stripe Integration
async createCheckoutSession(plan: string, successUrl?: string, cancelUrl?: string) {
return this.request<{ checkout_url: string; session_id: string }>('/subscription/checkout', {
method: 'POST',
body: JSON.stringify({
plan,
success_url: successUrl,
cancel_url: cancelUrl,
}),
})
}
async createPortalSession() {
return this.request<{ portal_url: string }>('/subscription/portal', {
method: 'POST',
})
}
async cancelSubscription() {
return this.request<{ status: string; message: string; new_tier: string }>('/subscription/cancel', {
method: 'POST',
})
}
// Domain Check (public)
async checkDomain(domain: string, quick = false) {
return this.request<{
domain: string
status: string
is_available: boolean
registrar: string | null
expiration_date: string | null
creation_date: string | null
name_servers: string[] | null
error_message: string | null
checked_at: string
}>('/check', {
method: 'POST',
body: JSON.stringify({ domain, quick }),
})
}
// Domains (authenticated)
async getDomains(page = 1, perPage = 20) {
return this.request<{
domains: Array<{
id: number
name: string
status: string
is_available: boolean
registrar: string | null
expiration_date: string | null
notify_on_available: boolean
created_at: string
last_checked: string | null
}>
total: number
page: number
per_page: number
pages: number
}>(`/domains?page=${page}&per_page=${perPage}`)
}
async addDomain(name: string, notify = true) {
return this.request<{
id: number
name: string
status: string
is_available: boolean
}>('/domains', {
method: 'POST',
body: JSON.stringify({ name, notify_on_available: notify }),
})
}
async deleteDomain(id: number) {
return this.request<void>(`/domains/${id}`, {
method: 'DELETE',
})
}
async refreshDomain(id: number) {
return this.request<{
id: number
name: string
status: string
is_available: boolean
}>(`/domains/${id}/refresh`, {
method: 'POST',
})
}
async updateDomainNotify(id: number, notify: boolean) {
return this.request<{
id: number
name: string
notify: boolean
}>(`/domains/${id}/notify`, {
method: 'PATCH',
body: JSON.stringify({ notify }),
})
}
async updateDomainExpiry(id: number, expirationDate: string | null) {
return this.request<{
id: number
name: string
expiration_date: string | null
}>(`/domains/${id}/expiry`, {
method: 'PATCH',
body: JSON.stringify({ expiration_date: expirationDate }),
})
}
// Marketplace Listings (Pounce Direct)
async getMarketplaceListings() {
return this.request<any[]>('/listings')
}
// My Listings
async getMyListings() {
return this.request<any[]>('/listings/my')
}
async createListing(data: { domain: string; asking_price: number | null; currency: string; price_type: string }) {
return this.request<any>('/listings', { method: 'POST', body: JSON.stringify(data) })
}
async deleteListing(id: number) {
return this.request<void>(`/listings/${id}`, { method: 'DELETE' })
}
async updateListing(id: number, data: { status?: string; title?: string; description?: string; asking_price?: number }) {
return this.request<any>(`/listings/${id}`, { method: 'PUT', body: JSON.stringify(data) })
}
async startDnsVerification(id: number) {
return this.request<{
verification_code: string
dns_record_type: string
dns_record_name: string
dns_record_value: string
instructions: string
status: string
}>(`/listings/${id}/verify-dns`, { method: 'POST' })
}
async checkDnsVerification(id: number) {
return this.request<{
verified: boolean
status: string
message: string
}>(`/listings/${id}/verify-dns/check`)
}
async getListingInquiries(id: number) {
return this.request<any[]>(`/listings/${id}/inquiries`)
}
// Subscription
async getSubscription() {
return this.request<{
id: number
tier: string
tier_name: string
status: string
domain_limit: number
domains_used: number
portfolio_limit: number
check_frequency: string
history_days: number
features: {
email_alerts: boolean
priority_alerts: boolean
full_whois: boolean
expiration_tracking: boolean
domain_valuation: boolean
market_insights: boolean
api_access: boolean
webhooks: boolean
bulk_tools: boolean
seo_metrics: boolean
}
started_at: string
expires_at: string | null
}>('/subscription')
}
async getTiers() {
return this.request<{
tiers: Array<{
id: string
name: string
domain_limit: number
price: number
features: string[]
}>
}>('/subscription/tiers')
}
// Domain History (Professional/Enterprise)
async getDomainHistory(domainId: number, limit = 30) {
return this.request<{
domain: string
total_checks: number
history: Array<{
id: number
status: string
is_available: boolean
checked_at: string
}>
}>(`/domains/${domainId}/history?limit=${limit}`)
}
// Domain Health Check - 4-layer analysis (DNS, HTTP, SSL, WHOIS)
async getDomainHealth(domainId: number, options?: { refresh?: boolean }) {
const refreshParam = options?.refresh ? '?refresh=true' : ''
return this.request<DomainHealthReport>(`/domains/${domainId}/health${refreshParam}`)
}
// Quick health check for any domain (premium)
async quickHealthCheck(domain: string) {
return this.request<DomainHealthReport>(`/domains/health-check?domain=${encodeURIComponent(domain)}`, {
method: 'POST',
})
}
// Bulk cached health reports for watchlist UI (fast)
async getDomainsHealthCache() {
return this.request<{
reports: Record<string, DomainHealthReport>
total_domains: number
cached_domains: number
timestamp: string
}>('/domains/health-cache')
}
// TLD Pricing
async getTldOverview(
limit = 25,
offset = 0,
sortBy: 'popularity' | 'price_asc' | 'price_desc' | 'name' = 'popularity',
search?: string
) {
const params = new URLSearchParams({
limit: limit.toString(),
offset: offset.toString(),
sort_by: sortBy,
})
if (search) {
params.append('search', search)
}
return this.request<{
tlds: Array<{
tld: string
type: string
description: string
avg_registration_price: number
min_registration_price: number
max_registration_price: number
min_renewal_price: number
avg_renewal_price: number
registrar_count: number
trend: string
price_change_7d: number
price_change_1y: number
price_change_3y: number
risk_level: 'low' | 'medium' | 'high'
risk_reason: string
popularity_rank?: number
}>
total: number
limit: number
offset: number
has_more: boolean
source: string
}>(`/tld-prices/overview?${params.toString()}`)
}
async getTldHistory(tld: string, days = 90) {
return this.request<{
tld: string
type: string
description: string
registry: string
current_price: number
price_change_7d: number
price_change_30d: number
price_change_90d: number
trend: string
trend_reason: string
history: Array<{
date: string
price: number
}>
source: string
}>(`/tld-prices/${tld}/history?days=${days}`)
}
async getTldCompare(tld: string) {
return this.request<{
tld: string
type: string
description: string
registry: string
introduced: number | null
registrars: Array<{
name: string
registration_price: number
renewal_price: number
transfer_price: number
}>
cheapest_registrar: string
cheapest_price: number
price_range: {
min: number
max: number
avg: number
}
}>(`/tld-prices/${tld}/compare`)
}
async getTrendingTlds() {
return this.request<{
trending: Array<{
tld: string
reason: string
price_change: number
current_price: number
}>
}>('/tld-prices/trending')
}
// ============== Portfolio ==============
async getPortfolio(
status?: string,
sortBy = 'created_at',
sortOrder = 'desc',
limit = 100,
offset = 0
) {
const params = new URLSearchParams({
sort_by: sortBy,
sort_order: sortOrder,
limit: limit.toString(),
offset: offset.toString(),
})
if (status) {
params.append('status', status)
}
return this.request<PortfolioDomain[]>(`/portfolio?${params.toString()}`)
}
async getPortfolioSummary() {
return this.request<PortfolioSummary>('/portfolio/summary')
}
async addPortfolioDomain(data: PortfolioDomainCreate) {
return this.request<PortfolioDomain>('/portfolio', {
method: 'POST',
body: JSON.stringify(data),
})
}
async getPortfolioDomain(id: number) {
return this.request<PortfolioDomain>(`/portfolio/${id}`)
}
async updatePortfolioDomain(id: number, data: Partial<PortfolioDomainCreate>) {
return this.request<PortfolioDomain>(`/portfolio/${id}`, {
method: 'PUT',
body: JSON.stringify(data),
})
}
async deletePortfolioDomain(id: number) {
return this.request<void>(`/portfolio/${id}`, {
method: 'DELETE',
})
}
async markDomainSold(id: number, saleDate: string, salePrice: number) {
return this.request<PortfolioDomain>(`/portfolio/${id}/sell`, {
method: 'POST',
body: JSON.stringify({
sale_date: saleDate,
sale_price: salePrice,
}),
})
}
async refreshDomainValue(id: number) {
return this.request<PortfolioDomain>(`/portfolio/${id}/refresh-value`, {
method: 'POST',
})
}
async getDomainValuation(domain: string) {
return this.request<DomainValuation>(`/portfolio/valuation/${domain}`)
}
// ============== Market Feed (Unified) ==============
/**
* Get unified market feed combining Pounce Direct listings + external auctions.
* This is the main feed for the Market page.
*/
async getMarketFeed(options: {
source?: 'all' | 'pounce' | 'external'
keyword?: string
tld?: string
minPrice?: number
maxPrice?: number
minScore?: number
endingWithin?: number
verifiedOnly?: boolean
sortBy?: 'score' | 'price_asc' | 'price_desc' | 'time' | 'newest'
limit?: number
offset?: number
} = {}) {
const params = new URLSearchParams()
if (options.source) params.append('source', options.source)
if (options.keyword) params.append('keyword', options.keyword)
if (options.tld) params.append('tld', options.tld)
if (options.minPrice !== undefined) params.append('min_price', options.minPrice.toString())
if (options.maxPrice !== undefined) params.append('max_price', options.maxPrice.toString())
if (options.minScore !== undefined) params.append('min_score', options.minScore.toString())
if (options.endingWithin !== undefined) params.append('ending_within', options.endingWithin.toString())
if (options.verifiedOnly) params.append('verified_only', 'true')
if (options.sortBy) params.append('sort_by', options.sortBy)
if (options.limit !== undefined) params.append('limit', options.limit.toString())
if (options.offset !== undefined) params.append('offset', options.offset.toString())
return this.request<{
items: Array<{
id: string
domain: string
tld: string
price: number
currency: string
price_type: 'bid' | 'fixed' | 'negotiable'
status: 'auction' | 'instant'
source: string
is_pounce: boolean
verified: boolean
time_remaining?: string
end_time?: string
num_bids?: number
slug?: string
seller_verified: boolean
url: string
is_external: boolean
pounce_score: number
}>
total: number
pounce_direct_count: number
auction_count: number
sources: string[]
last_updated: string
filters_applied: Record<string, any>
}>(`/auctions/feed?${params.toString()}`)
}
// ============== Auctions (Smart Pounce) ==============
async getAuctions(
keyword?: string,
tld?: string,
platform?: string,
minBid?: number,
maxBid?: number,
endingSoon = false,
sortBy = 'ending',
limit = 100,
offset = 0
) {
const params = new URLSearchParams({
sort_by: sortBy,
limit: limit.toString(),
offset: offset.toString(),
ending_soon: endingSoon.toString(),
})
if (keyword) params.append('keyword', keyword)
if (tld) params.append('tld', tld)
if (platform) params.append('platform', platform)
if (minBid !== undefined) params.append('min_bid', minBid.toString())
if (maxBid !== undefined) params.append('max_bid', maxBid.toString())
return this.request<{
auctions: Array<{
domain: string
platform: string
platform_url: string
current_bid: number
currency: string
num_bids: number
end_time: string
time_remaining: string
buy_now_price: number | null
reserve_met: boolean | null
traffic: number | null
age_years: number | null
tld: string
affiliate_url: string
}>
total: number
platforms_searched: string[]
last_updated: string
}>(`/auctions?${params.toString()}`)
}
async getEndingSoonAuctions(hours = 1, limit = 10) {
return this.request<Array<{
domain: string
platform: string
platform_url: string
current_bid: number
currency: string
num_bids: number
end_time: string
time_remaining: string
buy_now_price: number | null
reserve_met: boolean | null
traffic: number | null
age_years: number | null
tld: string
affiliate_url: string
}>>(`/auctions/ending-soon?hours=${hours}&limit=${limit}`)
}
async getHotAuctions(limit = 10) {
return this.request<Array<{
domain: string
platform: string
platform_url: string
current_bid: number
currency: string
num_bids: number
end_time: string
time_remaining: string
buy_now_price: number | null
reserve_met: boolean | null
traffic: number | null
age_years: number | null
tld: string
affiliate_url: string
}>>(`/auctions/hot?limit=${limit}`)
}
async getAuctionOpportunities() {
return this.request<{
opportunities: Array<{
auction: {
domain: string
platform: string
platform_url: string
current_bid: number
currency: string
num_bids: number
end_time: string
time_remaining: string
buy_now_price: number | null
reserve_met: boolean | null
traffic: number | null
age_years: number | null
tld: string
affiliate_url: string
}
analysis: {
estimated_value: number
current_bid: number
value_ratio: number
potential_profit: number
opportunity_score: number
recommendation: string
}
}>
strategy_tips: string[]
generated_at: string
}>('/auctions/opportunities')
}
async getAuctionPlatformStats() {
return this.request<Array<{
platform: string
active_auctions: number
avg_bid: number
ending_soon: number
}>>('/auctions/stats')
}
// ============== Price Alerts ==============
async getPriceAlerts(activeOnly = false) {
const params = activeOnly ? '?active_only=true' : ''
return this.request<PriceAlert[]>(`/price-alerts${params}`)
}
async createPriceAlert(tld: string, targetPrice?: number, thresholdPercent = 5) {
return this.request<PriceAlert>('/price-alerts', {
method: 'POST',
body: JSON.stringify({
tld: tld.replace(/^\./, ''),
target_price: targetPrice,
threshold_percent: thresholdPercent,
}),
})
}
async getPriceAlertStatus(tld: string) {
return this.request<{ tld: string; has_alert: boolean; is_active: boolean }>(
`/price-alerts/status/${tld.replace(/^\./, '')}`
)
}
async getPriceAlert(tld: string) {
return this.request<PriceAlert>(`/price-alerts/${tld.replace(/^\./, '')}`)
}
async updatePriceAlert(tld: string, data: { is_active?: boolean; target_price?: number; threshold_percent?: number }) {
return this.request<PriceAlert>(`/price-alerts/${tld.replace(/^\./, '')}`, {
method: 'PUT',
body: JSON.stringify(data),
})
}
async deletePriceAlert(tld: string) {
return this.request<void>(`/price-alerts/${tld.replace(/^\./, '')}`, {
method: 'DELETE',
})
}
async togglePriceAlert(tld: string) {
return this.request<PriceAlert>(`/price-alerts/${tld.replace(/^\./, '')}/toggle`, {
method: 'POST',
})
}
}
// ============== Types ==============
export interface PortfolioDomain {
id: number
domain: string
purchase_date: string | null
purchase_price: number | null
purchase_registrar: string | null
registrar: string | null
renewal_date: string | null
renewal_cost: number | null
auto_renew: boolean
estimated_value: number | null
value_updated_at: string | null
is_sold: boolean
sale_date: string | null
sale_price: number | null
status: string
notes: string | null
tags: string | null
roi: number | null
created_at: string
updated_at: string
}
export interface PortfolioSummary {
total_domains: number
active_domains: number
sold_domains: number
total_invested: number
total_value: number
total_sold_value: number
unrealized_profit: number
realized_profit: number
overall_roi: number
}
export interface PortfolioDomainCreate {
domain: string
purchase_date?: string
purchase_price?: number
purchase_registrar?: string
registrar?: string
renewal_date?: string
renewal_cost?: number
auto_renew?: boolean
notes?: string
tags?: string
}
export interface DomainValuation {
domain: string
estimated_value: number
currency: string
scores: {
length: number
tld: number
keyword: number
brandability: number
overall: number
}
factors: {
length: number
tld: string
has_numbers: boolean
has_hyphens: boolean
is_dictionary_word: boolean
}
confidence: string
source: string
calculated_at: string
valuation_formula?: string
}
export interface PriceAlert {
id: number
tld: string
is_active: boolean
target_price: number | null
threshold_percent: number
last_notified_at: string | null
last_notified_price: number | null
created_at: string
}
// Domain Health Check Types
export type HealthStatus = 'healthy' | 'weakening' | 'parked' | 'critical' | 'unknown'
export interface DomainHealthReport {
domain: string
status: HealthStatus
score: number // 0-100
checked_at: string
signals: string[]
recommendations: string[]
dns: {
has_ns: boolean
has_a: boolean
has_mx: boolean
nameservers: string[]
is_parked: boolean
parking_provider?: string
error?: string
}
http: {
is_reachable: boolean
status_code?: number
is_parked: boolean
parking_keywords?: string[]
content_length?: number
error?: string
}
ssl: {
has_certificate: boolean
is_valid: boolean
expires_at?: string
days_until_expiry?: number
issuer?: string
error?: string
}
}
// ============== Admin API Extension ==============
class AdminApiClient extends ApiClient {
// Admin Stats
async getAdminStats() {
return this.request<any>('/admin/stats')
}
// Admin Users
async getAdminUsers(limit: number = 50, offset: number = 0, search?: string) {
const params = new URLSearchParams({ limit: String(limit), offset: String(offset) })
if (search) params.append('search', search)
return this.request<any>(`/admin/users?${params}`)
}
async getAdminUser(userId: number) {
return this.request<any>(`/admin/users/${userId}`)
}
async updateAdminUser(userId: number, data: { name?: string; is_active?: boolean; is_verified?: boolean; is_admin?: boolean }) {
return this.request<any>(`/admin/users/${userId}`, {
method: 'PATCH',
body: JSON.stringify(data),
})
}
async deleteAdminUser(userId: number) {
return this.request<any>(`/admin/users/${userId}`, { method: 'DELETE' })
}
async upgradeUser(userId: number, tier: string) {
return this.request<any>(`/admin/users/${userId}/upgrade?tier=${tier}`, { method: 'POST' })
}
// Newsletter
async getAdminNewsletter(limit: number = 100, offset: number = 0, activeOnly: boolean = true) {
const params = new URLSearchParams({
limit: String(limit),
offset: String(offset),
active_only: String(activeOnly),
})
return this.request<any>(`/admin/newsletter?${params}`)
}
async exportNewsletterEmails() {
return this.request<{ emails: string[]; count: number }>('/admin/newsletter/export')
}
// TLD Scraping
async triggerTldScrape() {
return this.request<any>('/admin/scrape-tld-prices', { method: 'POST' })
}
async getTldPriceStats() {
return this.request<any>('/admin/tld-prices/stats')
}
// Auction Scraping
async triggerAuctionScrape() {
return this.request<any>('/auctions/admin/scrape', { method: 'POST' })
}
async getAuctionScrapeStatus() {
return this.request<any>('/auctions/admin/scrape-status')
}
// System
async getSystemHealth() {
return this.request<any>('/admin/system/health')
}
async makeUserAdmin(email: string) {
return this.request<any>(`/admin/system/make-admin?email=${encodeURIComponent(email)}`, { method: 'POST' })
}
// Price Alerts
async getAdminPriceAlerts(limit = 100, offset = 0) {
return this.request<{
alerts: Array<{
id: number
tld: string
target_price: number | null
alert_type: string
created_at: string
user: { id: number; email: string; name: string | null }
}>
total: number
}>(`/admin/price-alerts?limit=${limit}&offset=${offset}`)
}
// Domain Health Check
async triggerDomainChecks() {
return this.request<{
message: string
domains_queued: number
started_at: string
}>('/admin/domains/check-all', { method: 'POST' })
}
// Email Test
async sendTestEmail() {
return this.request<{
message: string
sent_to: string
timestamp: string
}>('/admin/system/test-email', { method: 'POST' })
}
// Scheduler Status
async getSchedulerStatus() {
return this.request<{
scheduler_running: boolean
jobs: Array<{
id: string
name: string
next_run: string | null
trigger: string
}>
last_runs: {
tld_scrape: string | null
auction_scrape: string | null
domain_check: string | null
}
timestamp: string
}>('/admin/system/scheduler')
}
// User Export
async exportUsersCSV() {
return this.request<{
csv: string
count: number
exported_at: string
}>('/admin/users/export')
}
// Bulk Upgrade
async bulkUpgradeUsers(userIds: number[], tier: string) {
return this.request<{
message: string
tier: string
upgraded: Array<{ user_id: number; email: string }>
failed: Array<{ user_id: number; reason: string }>
total_upgraded: number
total_failed: number
}>('/admin/users/bulk-upgrade', {
method: 'POST',
body: JSON.stringify({ user_ids: userIds, tier }),
})
}
// Activity Log
async getActivityLog(limit = 50, offset = 0) {
return this.request<{
logs: Array<{
id: number
action: string
details: string
created_at: string
admin: { id: number; email: string; name: string | null }
}>
total: number
}>(`/admin/activity-log?limit=${limit}&offset=${offset}`)
}
// ============== Blog ==============
async getBlogPosts(limit = 10, offset = 0, category?: string, tag?: string) {
let url = `/blog/posts?limit=${limit}&offset=${offset}`
if (category) url += `&category=${encodeURIComponent(category)}`
if (tag) url += `&tag=${encodeURIComponent(tag)}`
return this.request<{
posts: Array<{
id: number
title: string
slug: string
excerpt: string | null
cover_image: string | null
category: string | null
tags: string[]
is_published: boolean
published_at: string | null
created_at: string
view_count: number
author: { id: number; name: string | null }
}>
total: number
limit: number
offset: number
}>(url)
}
async getBlogPost(slug: string) {
return this.request<{
id: number
title: string
slug: string
excerpt: string | null
content: string
cover_image: string | null
category: string | null
tags: string[]
meta_title: string | null
meta_description: string | null
is_published: boolean
published_at: string | null
created_at: string
updated_at: string
view_count: number
author: { id: number; name: string | null }
}>(`/blog/posts/${slug}`)
}
async getBlogCategories() {
return this.request<{
categories: Array<{ name: string; count: number }>
}>('/blog/posts/categories')
}
async getFeaturedPosts(limit = 3) {
return this.request<{
posts: Array<{
id: number
title: string
slug: string
excerpt: string | null
cover_image: string | null
category: string | null
published_at: string | null
}>
}>(`/blog/posts/featured?limit=${limit}`)
}
// Admin Blog
async getAdminBlogPosts(limit = 50, offset = 0, status?: 'published' | 'draft') {
let url = `/blog/admin/posts?limit=${limit}&offset=${offset}`
if (status) url += `&status_filter=${status}`
return this.request<{
posts: Array<{
id: number
title: string
slug: string
excerpt: string | null
cover_image: string | null
category: string | null
tags: string[]
is_published: boolean
published_at: string | null
created_at: string
view_count: number
author: { id: number; name: string | null }
}>
total: number
}>(url)
}
async createBlogPost(data: {
title: string
content: string
excerpt?: string
cover_image?: string
category?: string
tags?: string[]
meta_title?: string
meta_description?: string
is_published?: boolean
}) {
return this.request<any>('/blog/admin/posts', {
method: 'POST',
body: JSON.stringify(data),
})
}
async updateBlogPost(postId: number, data: {
title?: string
content?: string
excerpt?: string
cover_image?: string
category?: string
tags?: string[]
meta_title?: string
meta_description?: string
is_published?: boolean
}) {
return this.request<any>(`/blog/admin/posts/${postId}`, {
method: 'PATCH',
body: JSON.stringify(data),
})
}
async deleteBlogPost(postId: number) {
return this.request<any>(`/blog/admin/posts/${postId}`, {
method: 'DELETE',
})
}
async publishBlogPost(postId: number) {
return this.request<any>(`/blog/admin/posts/${postId}/publish`, {
method: 'POST',
})
}
async unpublishBlogPost(postId: number) {
return this.request<any>(`/blog/admin/posts/${postId}/unpublish`, {
method: 'POST',
})
}
// =========================================================================
// YIELD / Intent Routing API
// =========================================================================
async analyzeYieldDomain(domain: string) {
return this.request<{
domain: string
intent: {
category: string
subcategory: string | null
confidence: number
keywords_matched: string[]
suggested_partners: string[]
monetization_potential: 'high' | 'medium' | 'low'
}
value: {
estimated_monthly_min: number
estimated_monthly_max: number
currency: string
potential: string
confidence: number
geo: string | null
}
partners: string[]
monetization_potential: string
}>(`/yield/analyze?domain=${encodeURIComponent(domain)}`, {
method: 'POST',
})
}
async getYieldDashboard() {
return this.request<{
stats: {
total_domains: number
active_domains: number
pending_domains: number
monthly_revenue: number
monthly_clicks: number
monthly_conversions: number
lifetime_revenue: number
lifetime_clicks: number
lifetime_conversions: number
pending_payout: number
next_payout_date: string | null
currency: string
}
domains: YieldDomain[]
recent_transactions: YieldTransaction[]
top_domains: YieldDomain[]
}>('/yield/dashboard')
}
async getYieldDomains(params?: { status?: string; limit?: number; offset?: number }) {
const queryParams = new URLSearchParams()
if (params?.status) queryParams.set('status', params.status)
if (params?.limit) queryParams.set('limit', params.limit.toString())
if (params?.offset) queryParams.set('offset', params.offset.toString())
return this.request<{
domains: YieldDomain[]
total: number
total_active: number
total_revenue: number
total_clicks: number
}>(`/yield/domains?${queryParams}`)
}
async getYieldDomain(domainId: number) {
return this.request<YieldDomain>(`/yield/domains/${domainId}`)
}
async activateYieldDomain(domain: string, acceptTerms: boolean = true) {
return this.request<{
domain_id: number
domain: string
status: string
intent: {
category: string
subcategory: string | null
confidence: number
keywords_matched: string[]
suggested_partners: string[]
monetization_potential: string
}
value_estimate: {
estimated_monthly_min: number
estimated_monthly_max: number
currency: string
potential: string
confidence: number
geo: string | null
}
dns_instructions: {
domain: string
nameservers: string[]
cname_host: string
cname_target: string
verification_url: string
}
message: string
}>('/yield/activate', {
method: 'POST',
body: JSON.stringify({ domain, accept_terms: acceptTerms }),
})
}
async verifyYieldDomainDNS(domainId: number) {
return this.request<{
domain: string
verified: boolean
expected_ns: string[]
actual_ns: string[]
cname_ok: boolean
error: string | null
checked_at: string
}>(`/yield/domains/${domainId}/verify`, {
method: 'POST',
})
}
async updateYieldDomain(domainId: number, data: {
active_route?: string
landing_page_url?: string
status?: string
}) {
return this.request<YieldDomain>(`/yield/domains/${domainId}`, {
method: 'PATCH',
body: JSON.stringify(data),
})
}
async deleteYieldDomain(domainId: number) {
return this.request<{ message: string }>(`/yield/domains/${domainId}`, {
method: 'DELETE',
})
}
async getYieldTransactions(params?: {
domain_id?: number
status?: string
limit?: number
offset?: number
}) {
const queryParams = new URLSearchParams()
if (params?.domain_id) queryParams.set('domain_id', params.domain_id.toString())
if (params?.status) queryParams.set('status', params.status)
if (params?.limit) queryParams.set('limit', params.limit.toString())
if (params?.offset) queryParams.set('offset', params.offset.toString())
return this.request<{
transactions: YieldTransaction[]
total: number
total_gross: number
total_net: number
}>(`/yield/transactions?${queryParams}`)
}
async getYieldPayouts(params?: { status?: string; limit?: number; offset?: number }) {
const queryParams = new URLSearchParams()
if (params?.status) queryParams.set('status', params.status)
if (params?.limit) queryParams.set('limit', params.limit.toString())
if (params?.offset) queryParams.set('offset', params.offset.toString())
return this.request<{
payouts: YieldPayout[]
total: number
total_paid: number
total_pending: number
}>(`/yield/payouts?${queryParams}`)
}
async getYieldPartners(category?: string) {
const params = category ? `?category=${encodeURIComponent(category)}` : ''
return this.request<YieldPartner[]>(`/yield/partners${params}`)
}
}
// Yield Types
export interface YieldDomain {
id: number
domain: string
status: 'pending' | 'verifying' | 'active' | 'paused' | 'inactive' | 'error'
detected_intent: string | null
intent_confidence: number
active_route: string | null
partner_name: string | null
dns_verified: boolean
dns_verified_at: string | null
total_clicks: number
total_conversions: number
total_revenue: number
currency: string
activated_at: string | null
created_at: string
}
export interface YieldTransaction {
id: number
event_type: 'click' | 'lead' | 'sale'
partner_slug: string
gross_amount: number
net_amount: number
currency: string
status: 'pending' | 'confirmed' | 'paid' | 'rejected'
geo_country: string | null
created_at: string
confirmed_at: string | null
}
export interface YieldPayout {
id: number
amount: number
currency: string
period_start: string
period_end: string
transaction_count: number
status: 'pending' | 'processing' | 'completed' | 'failed'
payment_method: string | null
payment_reference: string | null
created_at: string
completed_at: string | null
}
export interface YieldPartner {
slug: string
name: string
network: string
intent_categories: string[]
geo_countries: string[]
payout_type: string
description: string | null
logo_url: string | null
}
export const api = new AdminApiClient()