From 3ae2ef45d9ae827a01fdc4c1fa498842b51d2a5a Mon Sep 17 00:00:00 2001 From: "yves.gugger" Date: Mon, 8 Dec 2025 15:07:12 +0100 Subject: [PATCH] feat: Complete all missing functional features MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Dashboard Improvements: - Portfolio: Sell Domain Modal mit Profit-Vorschau - Portfolio: Edit Domain Modal für alle Felder - Watchlist: Notification Toggle Button (Bell Icon) - Neue Handler-Funktionen für alle Aktionen New Pages: - /settings - Profile, Notifications, Billing, Security - /blog/[slug] - Blog Detail Page mit Share-Buttons - /unsubscribe - Newsletter Unsubscribe Seite Navigation Updates: - Settings Icon im Header für eingeloggte User - Unsubscribe Link im Footer (Legal Section) API Additions: - updateDomainNotify() für Watchlist-Benachrichtigungen --- .github/workflows/deploy.yml | 0 frontend/src/app/blog/[slug]/page.tsx | 449 +++++++++++++++++++++++ frontend/src/app/dashboard/page.tsx | 372 +++++++++++++++++++ frontend/src/app/settings/page.tsx | 500 ++++++++++++++++++++++++++ frontend/src/app/unsubscribe/page.tsx | 141 ++++++++ frontend/src/components/Footer.tsx | 5 + frontend/src/components/Header.tsx | 10 +- frontend/src/lib/api.ts | 11 + 8 files changed, 1487 insertions(+), 1 deletion(-) mode change 100644 => 100755 .github/workflows/deploy.yml create mode 100644 frontend/src/app/blog/[slug]/page.tsx create mode 100644 frontend/src/app/settings/page.tsx create mode 100644 frontend/src/app/unsubscribe/page.tsx diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml old mode 100644 new mode 100755 diff --git a/frontend/src/app/blog/[slug]/page.tsx b/frontend/src/app/blog/[slug]/page.tsx new file mode 100644 index 0000000..58d4104 --- /dev/null +++ b/frontend/src/app/blog/[slug]/page.tsx @@ -0,0 +1,449 @@ +'use client' + +import { useParams } from 'next/navigation' +import { Header } from '@/components/Header' +import { Footer } from '@/components/Footer' +import { ArrowLeft, Calendar, Clock, User, Share2, Twitter, Linkedin, Link as LinkIcon, BookOpen, ChevronRight } from 'lucide-react' +import Link from 'next/link' +import { useState } from 'react' + +// Sample blog content - in production this would come from a CMS or API +const blogPosts: Record = { + 'complete-guide-domain-investing-2025': { + title: 'The Complete Guide to Domain Investing in 2025', + excerpt: 'Everything you need to know about finding, evaluating, and acquiring valuable domains in today\'s market.', + category: 'Guide', + date: 'Dec 5, 2025', + readTime: '12 min read', + author: 'pounce Team', + content: ` +# The Complete Guide to Domain Investing in 2025 + +Domain investing has evolved significantly over the past decade. What was once a niche hobby has become a sophisticated market with professional investors, data-driven strategies, and substantial returns. + +## Understanding the Domain Market + +The domain market operates on fundamental principles of supply and demand. Premium domains - those that are short, memorable, and keyword-rich - command higher prices because they're scarce and valuable for branding. + +### Key Factors That Determine Domain Value + +1. **Length** - Shorter domains (2-4 characters) are exponentially more valuable +2. **TLD** - .com remains king, but .io, .ai, and country codes have grown in value +3. **Keywords** - Domains containing valuable keywords command premiums +4. **Brandability** - Easy to pronounce, spell, and remember +5. **History** - Clean history with no spam associations + +## Finding Undervalued Domains + +The key to successful domain investing is finding domains that are undervalued relative to their potential. Here are strategies: + +### Expired Domain Hunting +Monitor domain expiration lists for previously registered domains. Tools like pounce make this easy by alerting you when domains on your watchlist become available. + +### Trend Spotting +Stay ahead of industry trends. Domains related to emerging technologies often appreciate rapidly. + +### Geographic Opportunities +Country-code TLDs (ccTLDs) can be undervalued in their home markets but valuable internationally. + +## Building a Portfolio + +Diversification is key. Don't put all your capital into a single premium domain. Instead: + +- Mix price points (some high-value, some speculative) +- Diversify across TLDs +- Balance keyword domains with brandable domains +- Track your portfolio's performance with tools like pounce + +## Selling Strategies + +When it's time to sell: + +1. **Marketplace Listings** - Sedo, Afternic, Dan.com +2. **Direct Outreach** - Contact companies that might benefit +3. **Auction Platforms** - GoDaddy Auctions, NameJet +4. **Broker Services** - For premium domains over $50k + +## Conclusion + +Domain investing in 2025 requires data, patience, and strategy. Use tools like pounce to monitor opportunities, track your portfolio, and make informed decisions. + +Happy investing! + `, + }, + 'understanding-tld-pricing-trends': { + title: 'Understanding TLD Pricing Trends', + excerpt: 'How domain extension prices fluctuate and what it means for your portfolio.', + category: 'Market Analysis', + date: 'Dec 3, 2025', + readTime: '5 min read', + author: 'pounce Team', + content: ` +# Understanding TLD Pricing Trends + +TLD (Top-Level Domain) pricing isn't static. Registry operators regularly adjust prices based on market conditions, and these changes can significantly impact your domain investment strategy. + +## Why TLD Prices Change + +### Registry Price Increases +Registries like Verisign (.com, .net) have contractual rights to increase prices. In 2024, .com prices increased by approximately 7%. + +### Market Demand +New TLDs may start cheap to encourage adoption, then increase as demand grows. .io and .ai are prime examples. + +### Promotional Pricing +Registrars often offer first-year discounts, but renewal prices can be significantly higher. + +## How to Track Pricing + +Use pounce's TLD pricing intelligence to: +- Compare prices across registrars +- Track historical price trends +- Set alerts for price drops +- Identify the cheapest renewal options + +## Strategic Implications + +Understanding pricing trends helps you: +1. Time your purchases for maximum savings +2. Budget accurately for renewals +3. Identify undervalued TLDs before price increases +4. Avoid TLDs with volatile pricing + +Stay informed, and your portfolio will thank you. + `, + }, + 'whois-privacy:-what-you-need-to-know': { + title: 'WHOIS Privacy: What You Need to Know', + excerpt: 'A deep dive into domain privacy protection and why it matters.', + category: 'Security', + date: 'Nov 28, 2025', + readTime: '4 min read', + author: 'pounce Team', + content: ` +# WHOIS Privacy: What You Need to Know + +When you register a domain, your personal information becomes part of the public WHOIS database. Here's what you need to understand about privacy protection. + +## What is WHOIS? + +WHOIS is a public database containing registration information for every domain. It includes: +- Registrant name and address +- Email and phone number +- Registration and expiration dates +- Nameservers + +## Why Privacy Matters + +### Spam Prevention +Public WHOIS data is harvested by spammers for email lists. + +### Identity Protection +Your personal address shouldn't be publicly searchable. + +### Business Confidentiality +Competitors can see what domains you're acquiring. + +## WHOIS Privacy Solutions + +Most registrars offer WHOIS privacy (also called "ID Protection") that replaces your information with the registrar's proxy service. + +### Costs +- Some registrars include it free (Cloudflare, Porkbun) +- Others charge $5-15/year +- Factor this into your domain costs + +## GDPR Impact + +Since GDPR, European registrant data is often redacted by default. However, this varies by TLD and registrar. + +## Our Recommendation + +Always enable WHOIS privacy unless you have a specific business reason not to. The small cost is worth the protection. + `, + }, + 'quick-wins:-domain-flipping-strategies': { + title: 'Quick Wins: Domain Flipping Strategies', + excerpt: 'Proven tactics for finding and selling domains at a profit.', + category: 'Strategy', + date: 'Nov 22, 2025', + readTime: '7 min read', + author: 'pounce Team', + content: ` +# Quick Wins: Domain Flipping Strategies + +Domain flipping - buying domains at low prices and selling them for a profit - can be lucrative when done right. Here are proven strategies. + +## The Basics + +Successful flipping requires: +1. Finding undervalued domains +2. Holding costs management +3. Effective sales channels +4. Patience and persistence + +## Strategy 1: Expired Domain Hunting + +Set up alerts on pounce for: +- Brandable names in popular niches +- Keyword domains with search volume +- Short domains (2-4 letters) +- Premium TLDs (.com, .io, .ai) + +## Strategy 2: Trend Riding + +Monitor: +- Tech news for emerging terms +- Trademark filings for new brands +- Industry reports for growing sectors + +Register relevant domains before they become valuable. + +## Strategy 3: Typo Domains + +(Proceed with caution - trademark issues exist) +Minor misspellings of popular brands can receive traffic. + +## Strategy 4: Geographic Plays + +Register city + service combinations: +- austinplumbers.com +- denverrealestate.com + +## Selling Tips + +1. Price reasonably (10-20x registration cost is realistic) +2. Use multiple marketplaces +3. Respond quickly to inquiries +4. Consider installment payments for higher prices + +## Risk Management + +- Don't overextend on speculative registrations +- Set a renewal budget and stick to it +- Track ROI on every domain +- Drop non-performers after 1-2 years + +Happy flipping! + `, + }, +} + +export default function BlogPostPage() { + const params = useParams() + const slug = params.slug as string + const [copied, setCopied] = useState(false) + + const post = blogPosts[slug] + + if (!post) { + return ( +
+
+
+
+

Post Not Found

+

+ The blog post you're looking for doesn't exist or has been moved. +

+ + + Back to Blog + +
+
+
+
+ ) + } + + const handleCopyLink = () => { + navigator.clipboard.writeText(window.location.href) + setCopied(true) + setTimeout(() => setCopied(false), 2000) + } + + const handleShare = (platform: 'twitter' | 'linkedin') => { + const url = encodeURIComponent(window.location.href) + const title = encodeURIComponent(post.title) + + const shareUrls = { + twitter: `https://twitter.com/intent/tweet?text=${title}&url=${url}`, + linkedin: `https://www.linkedin.com/sharing/share-offsite/?url=${url}`, + } + + window.open(shareUrls[platform], '_blank', 'width=600,height=400') + } + + return ( +
+ {/* Ambient glow */} +
+
+
+ +
+ +
+
+ {/* Back link */} + + + Back to Blog + + + {/* Header */} +
+
+ + {post.category} + +
+ +

+ {post.title} +

+ +

+ {post.excerpt} +

+ +
+ + + {post.author} + + + + {post.date} + + + + {post.readTime} + +
+
+ + {/* Content */} +
+ {post.content.split('\n').map((line, i) => { + if (line.startsWith('# ')) { + return

{line.slice(2)}

+ } + if (line.startsWith('## ')) { + return

{line.slice(3)}

+ } + if (line.startsWith('### ')) { + return

{line.slice(4)}

+ } + if (line.startsWith('- ')) { + return
  • {line.slice(2)}
  • + } + if (line.match(/^\d+\. /)) { + return
  • {line.replace(/^\d+\. /, '')}
  • + } + if (line.startsWith('**') && line.endsWith('**')) { + return

    {line.slice(2, -2)}

    + } + if (line.trim() === '') { + return
    + } + return

    {line}

    + })} +
    + + {/* Share */} +
    +
    +

    Share this article

    +
    + + + + {copied && ( + Copied! + )} +
    +
    +
    + + {/* Related posts */} +
    +

    Continue Reading

    +
    + {Object.entries(blogPosts) + .filter(([s]) => s !== slug) + .slice(0, 2) + .map(([postSlug, relatedPost]) => ( + + {relatedPost.category} +

    + {relatedPost.title} +

    +

    {relatedPost.excerpt}

    + + ))} +
    +
    + + {/* CTA */} +
    + +

    + Ready to start investing? +

    +

    + Track domains, monitor prices, and build your portfolio with pounce. +

    + + Get Started Free + + +
    +
    +
    + +
    +
    + ) +} + diff --git a/frontend/src/app/dashboard/page.tsx b/frontend/src/app/dashboard/page.tsx index 771cd5d..e1e3ec5 100644 --- a/frontend/src/app/dashboard/page.tsx +++ b/frontend/src/app/dashboard/page.tsx @@ -95,6 +95,31 @@ export default function DashboardPage() { }) const [addingPortfolio, setAddingPortfolio] = useState(false) + // Edit Portfolio Modal state + const [showEditPortfolioModal, setShowEditPortfolioModal] = useState(false) + const [editingPortfolioDomain, setEditingPortfolioDomain] = useState(null) + const [editPortfolioForm, setEditPortfolioForm] = useState({ + purchase_price: '', + purchase_date: '', + registrar: '', + renewal_date: '', + renewal_cost: '', + notes: '', + }) + const [savingEdit, setSavingEdit] = useState(false) + + // Sell Domain Modal state + const [showSellModal, setShowSellModal] = useState(false) + const [sellingDomain, setSellingDomain] = useState(null) + const [sellForm, setSellForm] = useState({ + sale_date: new Date().toISOString().split('T')[0], + sale_price: '', + }) + const [processingSale, setProcessingSale] = useState(false) + + // Notification toggle state + const [togglingNotifyId, setTogglingNotifyId] = useState(null) + useEffect(() => { checkAuth() }, [checkAuth]) @@ -255,6 +280,88 @@ export default function DashboardPage() { } } + // Toggle domain notification + const handleToggleNotify = async (domainId: number, currentNotify: boolean) => { + setTogglingNotifyId(domainId) + try { + await api.updateDomainNotify(domainId, !currentNotify) + // Refresh domains list + const { fetchDomains } = useStore.getState() + await fetchDomains() + } catch (err) { + setError(err instanceof Error ? err.message : 'Failed to update notification setting') + } finally { + setTogglingNotifyId(null) + } + } + + // Open Edit Portfolio Modal + const handleOpenEditPortfolio = (domain: PortfolioDomain) => { + setEditingPortfolioDomain(domain) + setEditPortfolioForm({ + purchase_price: domain.purchase_price?.toString() || '', + purchase_date: domain.purchase_date || '', + registrar: domain.registrar || '', + renewal_date: domain.renewal_date || '', + renewal_cost: domain.renewal_cost?.toString() || '', + notes: domain.notes || '', + }) + setShowEditPortfolioModal(true) + } + + // Save Edit Portfolio + const handleSaveEditPortfolio = async (e: React.FormEvent) => { + e.preventDefault() + if (!editingPortfolioDomain) return + + setSavingEdit(true) + try { + await api.updatePortfolioDomain(editingPortfolioDomain.id, { + purchase_price: editPortfolioForm.purchase_price ? parseFloat(editPortfolioForm.purchase_price) : undefined, + purchase_date: editPortfolioForm.purchase_date || undefined, + registrar: editPortfolioForm.registrar || undefined, + renewal_date: editPortfolioForm.renewal_date || undefined, + renewal_cost: editPortfolioForm.renewal_cost ? parseFloat(editPortfolioForm.renewal_cost) : undefined, + notes: editPortfolioForm.notes || undefined, + }) + setShowEditPortfolioModal(false) + setEditingPortfolioDomain(null) + loadPortfolio() + } catch (err) { + setError(err instanceof Error ? err.message : 'Failed to update domain') + } finally { + setSavingEdit(false) + } + } + + // Open Sell Modal + const handleOpenSellModal = (domain: PortfolioDomain) => { + setSellingDomain(domain) + setSellForm({ + sale_date: new Date().toISOString().split('T')[0], + sale_price: '', + }) + setShowSellModal(true) + } + + // Process Sale + const handleSellDomain = async (e: React.FormEvent) => { + e.preventDefault() + if (!sellingDomain || !sellForm.sale_price) return + + setProcessingSale(true) + try { + await api.markDomainSold(sellingDomain.id, sellForm.sale_date, parseFloat(sellForm.sale_price)) + setShowSellModal(false) + setSellingDomain(null) + loadPortfolio() + } catch (err) { + setError(err instanceof Error ? err.message : 'Failed to mark domain as sold') + } finally { + setProcessingSale(false) + } + } + const formatDate = (dateStr: string | null) => { if (!dateStr) return 'Not checked yet' const date = new Date(dateStr) @@ -574,6 +681,19 @@ export default function DashboardPage() { > +
    )}
    + {domain.status !== 'sold' && ( + + )} +
    )} + + {/* Edit Portfolio Domain Modal */} + {showEditPortfolioModal && editingPortfolioDomain && ( +
    +
    +
    +
    +
    +

    Edit Domain

    +

    {editingPortfolioDomain.domain}

    +
    + +
    + +
    +
    +
    + + setEditPortfolioForm({ ...editPortfolioForm, purchase_price: e.target.value })} + placeholder="$0.00" + className="w-full px-4 py-3 bg-background border border-border rounded-xl text-body text-foreground + placeholder:text-foreground-subtle focus:outline-none focus:border-border-hover transition-all" + /> +
    +
    + + setEditPortfolioForm({ ...editPortfolioForm, purchase_date: e.target.value })} + className="w-full px-4 py-3 bg-background border border-border rounded-xl text-body text-foreground + focus:outline-none focus:border-border-hover transition-all" + /> +
    +
    + +
    + + setEditPortfolioForm({ ...editPortfolioForm, registrar: e.target.value })} + placeholder="e.g., Namecheap, GoDaddy" + className="w-full px-4 py-3 bg-background border border-border rounded-xl text-body text-foreground + placeholder:text-foreground-subtle focus:outline-none focus:border-border-hover transition-all" + /> +
    + +
    +
    + + setEditPortfolioForm({ ...editPortfolioForm, renewal_date: e.target.value })} + className="w-full px-4 py-3 bg-background border border-border rounded-xl text-body text-foreground + focus:outline-none focus:border-border-hover transition-all" + /> +
    +
    + + setEditPortfolioForm({ ...editPortfolioForm, renewal_cost: e.target.value })} + placeholder="$0.00" + className="w-full px-4 py-3 bg-background border border-border rounded-xl text-body text-foreground + placeholder:text-foreground-subtle focus:outline-none focus:border-border-hover transition-all" + /> +
    +
    + +
    + +