diff --git a/analysis_3.md b/analysis_3.md new file mode 100644 index 0000000..0a4e51b --- /dev/null +++ b/analysis_3.md @@ -0,0 +1,166 @@ +Um die Churn Rate (Absprungrate) zu senken und den Umsatz pro Kunde (LTV - Lifetime Value) zu steigern, musst du das Mindset des Nutzers ändern: + +**Von:** *"Ich nutze Pounce, um eine Domain zu **finden**."* (Einmaliges Projekt) +**Zu:** *"Ich nutze Pounce, um mein Domain-Business zu **betreiben**."* (Laufender Prozess) + +Wenn Pounce nur ein "Such-Tool" ist, kündigen die Leute, sobald sie fündig wurden. Wenn Pounce aber ihr "Betriebssystem" wird, bleiben sie für immer. + +Hier sind 4 Strategien, um Pounce unverzichtbar zu machen: + +--- + +### 1. Strategie: Vom "Jäger" zum "Wächter" (Portfolio Monitoring) +*Ziel: Den Nutzer binden, auch wenn er gerade nichts kaufen will.* + +Viele Domainer und Agenturen besitzen bereits 50-500 Domains. Sie haben Angst, eine Verlängerung zu verpassen oder technische Fehler nicht zu bemerken. + +* **Das Feature:** **"My Portfolio Health"** + Der Nutzer importiert seine *eigenen* Domains in Pounce (nicht um sie zu kaufen, sondern zu verwalten). + * **Uptime Monitor:** Ist meine Seite noch online? + * **SSL Monitor:** Läuft mein Zertifikat ab? + * **Expiration Alert:** Erinnere mich 30 Tage vor Ablauf (besser als die Spam-Mails der Registrare). + * **Blacklist Check:** Landet meine Domain auf einer Spam-Liste? + +* **Der Lock-in Effekt:** + Niemand kündigt das Tool, das seine Assets überwacht ("Versicherungs-Psychologie"). Wenn du ihre 50 Domains überwachst, bist du unverzichtbar. + +### 2. Strategie: Der "Micro-Marktplatz" (Liquidity) +*Ziel: Mehr Umsatz durch Transaktionen.* + +Wenn ein "Hunter" eine Domain über Pounce findet, will er sie oft später wieder verkaufen (Flipping). Aktuell schickst du ihn dafür weg zu Sedo. Warum nicht im Haus behalten? + +* **Das Feature:** **"Pounce 'For Sale' Landing Pages"** + Ein User (Trader/Tycoon) kann für seine Domains mit einem Klick eine schicke Verkaufsseite erstellen. + * *Domain:* `super-startup.ai` + * *Pounce generiert:* `pounce.ch/buy/super-startup-ai` + * *Design:* Hochwertig, zeigt deine "Valuation Daten" (Pounce Score) an, um den Preis zu rechtfertigen. + * *Kontakt:* Ein einfaches Kontaktformular, das die Anfrage direkt an den User leitet. + +* **Das Geld:** + * Entweder Teil des Abo-Preises ("Erstelle 5 Verkaufsseiten kostenlos"). + * Oder: Du nimmst keine Provision, aber der Käufer muss sich bei Pounce registrieren, um den Verkäufer zu kontaktieren (Lead Gen). + +### 3. Strategie: SEO-Daten & Backlinks (Neue Zielgruppe) +*Ziel: Kunden mit hohem Budget gewinnen (Agenturen).* + +SEO-Agenturen kündigen fast nie, weil sie monatliche Budgets für Tools haben. Sie suchen Domains nicht wegen dem Namen, sondern wegen der **Power** (Backlinks). + +* **Das Feature:** **"SEO Juice Detector"** + Wenn eine Domain droppt, prüfst du nicht nur den Namen, sondern (über günstige APIs wie Moz oder durch Scraping öffentlicher Daten), ob Backlinks existieren. + * *Anzeige:* "Domain `alte-bäckerei-münchen.de` ist frei. Hat Links von `sueddeutsche.de` und `wikipedia.org`." +* **Der Wert:** Solche Domains sind für SEOs 100€ - 500€ wert, auch wenn der Name hässlich ist. +* **Monetarisierung:** Das ist ein reines **Tycoon-Feature ($29 oder sogar $49/Monat)**. + +### 4. Strategie: Alerts "nach Maß" (Hyper-Personalisierung) +*Ziel: Den Nutzer täglich zurückholen.* + +Wenn ich nur eine Mail bekomme "Hier sind 100 neue Domains", ist das oft Spam für mich. Ich will nur *genau das*, was ich suche. + +* **Das Feature:** **"Sniper Alerts"** + Der User kann extrem spezifische Filter speichern: + * *"Informiere mich NUR, wenn eine 4-Letter .com Domain droppt, die kein 'q' oder 'x' enthält."* + * *"Informiere mich, wenn eine .ch Domain droppt, die das Wort 'Immo' enthält."* +* **Der Effekt:** Wenn die SMS/Mail kommt, weiß der User: "Das ist relevant". Er klickt, loggt sich ein, bleibt aktiv. + +--- + +### Zusammenfassung des erweiterten Business-Modells + +So sieht deine Umsatz-Maschine dann aus: + +| Stufe | Was der User tut | Warum er bleibt (Retention) | Dein Umsatz | +| :--- | :--- | :--- | :--- | +| **Phase 1: Finding** | Sucht freie/droppende Domains. | Findet bessere Deals durch Spam-Filter. | $9 / Monat | +| **Phase 2: Monitoring** | Überwacht Wettbewerber & eigene Domains. | Angst, Status-Änderungen zu verpassen (Versicherung). | Churn sinkt drastisch. | +| **Phase 3: Selling** | Erstellt Verkaufs-Landings via Pounce. | Nutzt Pounce als Schaufenster für sein Business. | User ist "locked in". | +| **Phase 4: SEO** | Sucht Backlink-Monster. | Verdient Geld mit deinen Daten (ROI). | $29 - $49 / Monat | + +### Mein Tipp für den Start: +Konzentriere dich auf **Strategie 1 (Portfolio Monitoring)** als erstes Zusatz-Feature nach dem Launch. + +Warum? +Es ist technisch einfach (du hast die Ping-Skripte ja schon für die Analyse gebaut). Du erlaubst dem User einfach, Domains *manuell* hinzuzufügen. +Sobald ein User mal 50 seiner eigenen Domains eingetragen hat, wird er sein Abo **niemals kündigen**, weil er sonst seine Überwachung verliert. Das ist der ultimative "Golden Handcuff". + +Vertrauen ist im Domain-Business tatsächlich die **härteste Währung**. Die Branche ist leider voll von Betrügern (Domain-Diebstahl, Phishing, Fake-Auktionen). + +Wenn `pounce.ch` als "Command Center" wahrgenommen werden soll, muss die Plattform **sauberer sein als der Rest**. + +Hier ist ein **4-Säulen-Sicherheitskonzept**, mit dem du Missbrauch verhinderst und gleichzeitig massives Vertrauen bei deinen echten Nutzern aufbaust. + +--- + +### Säule 1: Identity Verification (Wer bist du?) +*Hürde: Betrüger hassen Identifikation.* + +Du darfst "Tycoon"-Features (und vor allem Verkaufs-Features) nicht einfach jedem geben, der eine E-Mail-Adresse hat. + +1. **Stripe Identity / Radar:** + Nutze für die Zahlungsabwicklung Stripe. Stripe hat eingebaute Betrugserkennung ("Radar"). Wenn jemand eine gestohlene Kreditkarte nutzt, blockiert Stripe ihn meist sofort. Das ist deine erste Firewall. +2. **SMS-Verifizierung (2FA):** + Jeder Account, der Domains verkaufen oder überwachen will, muss eine **Handynummer verifizieren**. Wegwerf-Nummern (VoIP) werden blockiert. Das erhöht die Hürde für Spammer massiv. +3. **LinkedIn-Login (Optional für Trust):** + Biete an: "Verbinde dein LinkedIn für den 'Verified Professional' Status". Ein Profil mit 500+ Kontakten und Historie ist selten ein Fake. + +--- + +### Säule 2: Asset Verification (Gehört dir das wirklich?) +*Hürde: Verhindern, dass Leute fremde Domains als ihre eigenen ausgeben.* + +Das ist der wichtigste Punkt, wenn du Features wie "Portfolio Monitoring" oder "For Sale Pages" anbietest. + +**Die technische Lösung: DNS Ownership Verify** +Bevor ein Nutzer eine Domain in sein Portfolio aufnehmen kann, um sie zu verkaufen oder tief zu analysieren, muss er beweisen, dass er der Admin ist. +* **Wie es funktioniert:** + 1. User fügt `mein-startup.ch` hinzu. + 2. Pounce sagt: "Bitte erstelle einen TXT-Record in deinen DNS-Einstellungen mit dem Inhalt: `pounce-verification=847392`." + 3. Dein System prüft den Record. + 4. Nur wenn er da ist -> **Domain Verified ✅**. + +*Das ist der Industriestandard (macht Google auch). Wer keinen Zugriff auf die DNS hat, kann die Domain nicht claimen.* + +--- + +### Säule 3: Content Monitoring (Was machst du damit?) +*Hürde: Verhindern, dass deine "For Sale"-Seiten für Phishing genutzt werden.* + +Wenn User über Pounce Verkaufsseiten ("Landers") erstellen können, könnten sie dort versuchen, Bankdaten abzugreifen. + +1. **Automatischer Blacklist-Scan:** + Jede Domain, die ins System kommt, wird sofort gegen **Google Safe Browsing** und **Spamhaus** geprüft. Ist die Domain dort als "Malware" gelistet? -> **Sofortiger Ban.** +2. **Keyword-Blocking:** + Erlaube keine Titel oder Texte auf Verkaufsseiten, die Wörter enthalten wie: "Login", "Bank", "Verify", "Paypal", "Password". +3. **No Custom HTML:** + Erlaube Usern auf ihren Verkaufsseiten *kein* eigenes HTML/JavaScript. Nur Text und vordefinierte Buttons. So können sie keine Schadsoftware einschleusen. + +--- + +### Säule 4: The "Safe Harbor" Badge (Marketing) +*Nutzen: Du machst die Sicherheit zu deinem Verkaufsargument.* + +Du kommunizierst diese Strenge nicht als "Nervigkeit", sondern als **Qualitätsmerkmal**. + +* **Das "Pounce Verified" Siegel:** + Auf jeder Verkaufsseite oder in jedem Profil zeigst du an: + * ✅ **ID Verified** (Handy/Zahlung geprüft) + * ✅ **Owner Verified** (DNS geprüft) + * ✅ **Clean History** (Keine Spam-Reports) + +--- + +### Prozess bei Verstößen ("Zero Tolerance") + +Du brauchst klare AGBs ("Terms of Service"): +1. **One Strike Policy:** Wer versucht, Phishing zu betreiben oder gestohlene Domains anzubieten, wird sofort permanent gesperrt. Keine Diskussion. +2. **Reporting Button:** Gib der Community Macht. Ein "Report Abuse"-Button auf jeder Seite. Wenn 2-3 unabhängige User etwas melden, wird das Asset automatisch offline genommen, bis du es geprüft hast. + +### Zusammenfassung: Der "Trust Stack" + +| Ebene | Maßnahme | Effekt | +| :--- | :--- | :--- | +| **Login** | SMS / 2FA + Stripe Radar | Hält Bots und Kreditkartenbetrüger fern. | +| **Portfolio** | **DNS TXT Record (Zwingend)** | Nur der echte Besitzer kann Domains verwalten. | +| **Marktplatz** | Google Safe Browsing Check | Verhindert Malware/Phishing auf deiner Plattform. | +| **Frontend** | "Verified Owner" Badge | Käufer wissen: Das hier ist sicher. | + +**Damit positionierst du Pounce als den "Safe Space" im wilden Westen des Domain-Handels.** Das ist für seriöse Investoren oft wichtiger als der Preis. \ No newline at end of file diff --git a/frontend/src/app/auctions/page.tsx b/frontend/src/app/auctions/page.tsx index 68e82ba..12165cb 100644 --- a/frontend/src/app/auctions/page.tsx +++ b/frontend/src/app/auctions/page.tsx @@ -3,7 +3,8 @@ import { useEffect, useState } from 'react' import { useStore } from '@/lib/store' import { api } from '@/lib/api' -import { CommandCenterLayout } from '@/components/CommandCenterLayout' +import { Header } from '@/components/Header' +import { Footer } from '@/components/Footer' import { PremiumTable, Badge, PlatformBadge, StatCard, PageContainer } from '@/components/PremiumTable' import { Clock, @@ -12,15 +13,13 @@ import { Flame, Timer, Gavel, - ChevronUp, - ChevronDown, - ChevronsUpDown, DollarSign, RefreshCw, Target, X, - TrendingUp, - Loader2, + Lock, + Sparkles, + ArrowRight, } from 'lucide-react' import Link from 'next/link' import clsx from 'clsx' @@ -66,7 +65,7 @@ const PLATFORMS = [ ] export default function AuctionsPage() { - const { isAuthenticated, subscription } = useStore() + const { isAuthenticated } = useStore() const [allAuctions, setAllAuctions] = useState([]) const [endingSoon, setEndingSoon] = useState([]) @@ -202,224 +201,276 @@ export default function AuctionsPage() { } return ( - - - Refresh - - } - > - - {/* Stats */} -
- - - - +
+
+ + {/* Hero Section */} +
+ {/* Background Effects */} +
+
- {/* Tabs */} -
- {[ - { id: 'all' as const, label: 'All', icon: Gavel, count: allAuctions.length }, - { id: 'ending' as const, label: 'Ending Soon', icon: Timer, count: endingSoon.length, color: 'warning' }, - { id: 'hot' as const, label: 'Hot', icon: Flame, count: hotAuctions.length }, - { id: 'opportunities' as const, label: 'Opportunities', icon: Target, count: opportunities.length }, - ].map((tab) => ( +
+
+
+

+ Domain Auctions +

+

{getSubtitle()}

+
- ))} -
+
- {/* Filters */} -
-
- - setSearchQuery(e.target.value)} - className="w-full pl-11 pr-10 py-3 bg-background-secondary/50 border border-border/50 rounded-xl - text-sm text-foreground placeholder:text-foreground-subtle - focus:outline-none focus:border-accent/50 transition-all" + {/* Stats */} +
+ + + + - {searchQuery && ( -
+ + {/* Login Banner for Opportunities */} + {!isAuthenticated && ( +
+
+
+
+ +
+
+

Unlock Smart Opportunities

+

Get AI-powered auction analysis and personalized recommendations

+
+
+ + Sign In + +
+
+ )} + + {/* Tabs */} +
+ {[ + { id: 'all' as const, label: 'All', icon: Gavel, count: allAuctions.length }, + { id: 'ending' as const, label: 'Ending Soon', icon: Timer, count: endingSoon.length, color: 'warning' }, + { id: 'hot' as const, label: 'Hot', icon: Flame, count: hotAuctions.length }, + { id: 'opportunities' as const, label: 'Opportunities', icon: Target, count: opportunities.length, locked: !isAuthenticated }, + ].map((tab) => ( + - )} + ))}
- -
- - setMaxBid(e.target.value)} - className="w-32 pl-10 pr-4 py-3 bg-background-secondary/50 border border-border/50 rounded-xl - text-sm text-foreground placeholder:text-foreground-subtle - focus:outline-none focus:border-accent/50" - /> -
-
- {/* Table */} - `${a.domain}-${a.platform}`} - loading={loading} - sortBy={sortBy} - sortDirection={sortDirection} - onSort={(key) => handleSort(key as SortField)} - emptyIcon={} - emptyTitle={searchQuery ? `No auctions matching "${searchQuery}"` : "No auctions found"} - emptyDescription="Try adjusting your filters or check back later" - columns={[ - { - key: 'domain', - header: 'Domain', - sortable: true, - render: (a) => ( -
- +
+ + setSearchQuery(e.target.value)} + className="w-full pl-11 pr-10 py-3 bg-background-secondary/50 border border-border/50 rounded-xl + text-sm text-foreground placeholder:text-foreground-subtle + focus:outline-none focus:border-accent/50 transition-all" + /> + {searchQuery && ( + + )} +
+ +
+ + setMaxBid(e.target.value)} + className="w-32 pl-10 pr-4 py-3 bg-background-secondary/50 border border-border/50 rounded-xl + text-sm text-foreground placeholder:text-foreground-subtle + focus:outline-none focus:border-accent/50" + /> +
+
+ + {/* Table */} + `${a.domain}-${a.platform}`} + loading={loading} + sortBy={sortBy} + sortDirection={sortDirection} + onSort={(key) => handleSort(key as SortField)} + emptyIcon={} + emptyTitle={searchQuery ? `No auctions matching "${searchQuery}"` : "No auctions found"} + emptyDescription="Try adjusting your filters or check back later" + columns={[ + { + key: 'domain', + header: 'Domain', + sortable: true, + render: (a) => ( +
+ + {a.domain} + +
+ + {a.age_years && {a.age_years}y} +
+
+ ), + }, + { + key: 'platform', + header: 'Platform', + hideOnMobile: true, + render: (a) => ( +
+ + {a.age_years && ( + + {a.age_years}y + + )} +
+ ), + }, + { + key: 'bid_asc', + header: 'Bid', + sortable: true, + align: 'right', + render: (a) => ( +
+ {formatCurrency(a.current_bid)} + {a.buy_now_price && ( +

Buy: {formatCurrency(a.buy_now_price)}

+ )} +
+ ), + }, + { + key: 'bids', + header: 'Bids', + sortable: true, + align: 'right', + hideOnMobile: true, + render: (a) => ( + = 20 ? "text-accent" : a.num_bids >= 10 ? "text-amber-400" : "text-foreground-muted" + )}> + {a.num_bids} + {a.num_bids >= 20 && } + + ), + }, + { + key: 'ending', + header: 'Time Left', + sortable: true, + align: 'right', + hideOnMobile: true, + render: (a) => ( + + {a.time_remaining} + + ), + }, + ...(activeTab === 'opportunities' ? [{ + key: 'score', + header: 'Score', + align: 'center' as const, + render: (a: Auction) => { + const oppData = getOpportunityData(a.domain) + if (!oppData) return null + return ( + + {oppData.opportunity_score} + + ) + }, + }] : []), + { + key: 'action', + header: '', + align: 'right', + render: (a) => ( + - {a.domain} + Bid -
- - {a.age_years && {a.age_years}y} -
-
- ), - }, - { - key: 'platform', - header: 'Platform', - hideOnMobile: true, - render: (a) => ( -
- - {a.age_years && ( - - {a.age_years}y - - )} -
- ), - }, - { - key: 'bid_asc', - header: 'Bid', - sortable: true, - align: 'right', - render: (a) => ( -
- {formatCurrency(a.current_bid)} - {a.buy_now_price && ( -

Buy: {formatCurrency(a.buy_now_price)}

- )} -
- ), - }, - { - key: 'bids', - header: 'Bids', - sortable: true, - align: 'right', - hideOnMobile: true, - render: (a) => ( - = 20 ? "text-accent" : a.num_bids >= 10 ? "text-amber-400" : "text-foreground-muted" - )}> - {a.num_bids} - {a.num_bids >= 20 && } - - ), - }, - { - key: 'ending', - header: 'Time Left', - sortable: true, - align: 'right', - hideOnMobile: true, - render: (a) => ( - - {a.time_remaining} - - ), - }, - ...(activeTab === 'opportunities' ? [{ - key: 'score', - header: 'Score', - align: 'center' as const, - render: (a: Auction) => { - const oppData = getOpportunityData(a.domain) - if (!oppData) return null - return ( - - {oppData.opportunity_score} - - ) + ), }, - }] : []), - { - key: 'action', - header: '', - align: 'right', - render: (a) => ( - - Bid - - ), - }, - ]} - /> - - + ]} + /> +
+
+ +
+
+
) } diff --git a/frontend/src/app/intelligence/page.tsx b/frontend/src/app/intelligence/page.tsx index e1201a5..22d623e 100755 --- a/frontend/src/app/intelligence/page.tsx +++ b/frontend/src/app/intelligence/page.tsx @@ -3,7 +3,8 @@ import { useEffect, useState } from 'react' import { useStore } from '@/lib/store' import { api } from '@/lib/api' -import { CommandCenterLayout } from '@/components/CommandCenterLayout' +import { Header } from '@/components/Header' +import { Footer } from '@/components/Footer' import { PremiumTable, Badge, StatCard, PageContainer } from '@/components/PremiumTable' import { Search, @@ -18,6 +19,8 @@ import { RefreshCw, Bell, X, + Sparkles, + ArrowRight, } from 'lucide-react' import clsx from 'clsx' import Link from 'next/link' @@ -34,7 +37,7 @@ interface TLDData { } export default function IntelligencePage() { - const { subscription } = useStore() + const { isAuthenticated } = useStore() const [tldData, setTldData] = useState([]) const [loading, setLoading] = useState(true) @@ -96,203 +99,245 @@ export default function IntelligencePage() { } return ( - - - Refresh - - } - > - - {/* Stats Overview */} -
- 0 ? total.toLocaleString() : '—'} - subtitle="updated daily" - icon={Globe} - accent - /> - 0 ? `$${lowestPrice.toFixed(2)}` : '—'} - icon={DollarSign} - /> - 0 ? `.${hottestTld}` : '—'} - subtitle="rising prices" - icon={TrendingUp} - /> - +
+
+ + {/* Hero Section */} +
+ {/* Background Effects */} +
+
- {/* Filters */} -
-
- - setSearchQuery(e.target.value)} - placeholder="Search TLDs (e.g. com, io, dev)..." - className="w-full h-11 pl-11 pr-10 bg-background-secondary/50 border border-border/50 rounded-xl - text-sm text-foreground placeholder:text-foreground-subtle - focus:outline-none focus:border-accent/50 transition-all" +
+
+
+

+ TLD Intelligence +

+

{getSubtitle()}

+
+ +
+ + {/* Stats Overview */} +
+ 0 ? total.toLocaleString() : '—'} + subtitle="updated daily" + icon={Globe} + accent /> - {searchQuery && ( -
+ + {/* CTA Banner for non-authenticated users */} + {!isAuthenticated && ( +
+
+
+
+ +
+
+

Set Price Alerts

+

Get notified when TLD prices drop to your target

+
+
+ + Sign In + +
+
+ )} + + {/* Filters */} +
+
+ + setSearchQuery(e.target.value)} + placeholder="Search TLDs (e.g. com, io, dev)..." + className="w-full h-11 pl-11 pr-10 bg-background-secondary/50 border border-border/50 rounded-xl + text-sm text-foreground placeholder:text-foreground-subtle + focus:outline-none focus:border-accent/50 transition-all" + /> + {searchQuery && ( + + )} +
+
+ + +
-
- - -
-
- {/* TLD Table */} - tld.tld} - loading={loading} - onRowClick={(tld) => window.location.href = `/tld-pricing/${tld.tld}`} - emptyIcon={} - emptyTitle="No TLDs found" - emptyDescription={searchQuery ? `No TLDs matching "${searchQuery}"` : "Check back later for TLD data"} - columns={[ - { - key: 'tld', - header: 'TLD', - width: '120px', - render: (tld) => ( - - .{tld.tld} - - ), - }, - { - key: 'min_price', - header: 'Min Price', - align: 'right', - width: '100px', - render: (tld) => ( - ${tld.min_price.toFixed(2)} - ), - }, - { - key: 'avg_price', - header: 'Avg Price', - align: 'right', - width: '100px', - hideOnMobile: true, - render: (tld) => ( - ${tld.avg_price.toFixed(2)} - ), - }, - { - key: 'change', - header: '7d Change', - align: 'right', - width: '120px', - render: (tld) => ( -
- {getTrendIcon(tld.price_change_7d)} - 0 ? "text-orange-400" : - (tld.price_change_7d || 0) < 0 ? "text-accent" : "text-foreground-muted" - )}> - {(tld.price_change_7d || 0) > 0 ? '+' : ''}{(tld.price_change_7d || 0).toFixed(1)}% + {/* TLD Table */} + tld.tld} + loading={loading} + onRowClick={(tld) => window.location.href = `/tld-pricing/${tld.tld}`} + emptyIcon={} + emptyTitle="No TLDs found" + emptyDescription={searchQuery ? `No TLDs matching "${searchQuery}"` : "Check back later for TLD data"} + columns={[ + { + key: 'tld', + header: 'TLD', + width: '120px', + render: (tld) => ( + + .{tld.tld} -
- ), - }, - { - key: 'registrar', - header: 'Cheapest At', - hideOnMobile: true, - render: (tld) => ( - {tld.cheapest_registrar} - ), - }, - { - key: 'actions', - header: '', - align: 'right', - width: '80px', - render: (tld) => ( -
- e.stopPropagation()} - title="Set price alert" - > - - - -
- ), - }, - ]} - /> + ), + }, + { + key: 'min_price', + header: 'Min Price', + align: 'right', + width: '100px', + render: (tld) => ( + ${tld.min_price.toFixed(2)} + ), + }, + { + key: 'avg_price', + header: 'Avg Price', + align: 'right', + width: '100px', + hideOnMobile: true, + render: (tld) => ( + ${tld.avg_price.toFixed(2)} + ), + }, + { + key: 'change', + header: '7d Change', + align: 'right', + width: '120px', + render: (tld) => ( +
+ {getTrendIcon(tld.price_change_7d)} + 0 ? "text-orange-400" : + (tld.price_change_7d || 0) < 0 ? "text-accent" : "text-foreground-muted" + )}> + {(tld.price_change_7d || 0) > 0 ? '+' : ''}{(tld.price_change_7d || 0).toFixed(1)}% + +
+ ), + }, + { + key: 'registrar', + header: 'Cheapest At', + hideOnMobile: true, + render: (tld) => ( + {tld.cheapest_registrar} + ), + }, + { + key: 'actions', + header: '', + align: 'right', + width: '80px', + render: (tld) => ( +
+ e.stopPropagation()} + title="View details" + > + + + +
+ ), + }, + ]} + /> - {/* Pagination */} - {total > 50 && ( -
- - - Page {page + 1} of {Math.ceil(total / 50)} - - -
- )} - - + {/* Pagination */} + {total > 50 && ( +
+ + + Page {page + 1} of {Math.ceil(total / 50)} + + +
+ )} +
+
+ +
+
) } diff --git a/frontend/src/app/watchlist/page.tsx b/frontend/src/app/watchlist/page.tsx old mode 100644 new mode 100755