From fde66af049136fc6dc48df6d95e2e76079104cea Mon Sep 17 00:00:00 2001 From: Yves Gugger Date: Sat, 13 Dec 2025 15:39:51 +0100 Subject: [PATCH] Watchlist layout fix + TLD detail + Sniper/Yield/Listing redesign + deploy script --- deploy.sh | 207 ++--- frontend/src/app/login/page.tsx | 9 +- frontend/src/app/terminal/listing/page.tsx | 984 +++++++-------------- frontend/src/app/terminal/sniper/page.tsx | 763 +++++++--------- frontend/src/app/terminal/yield/page.tsx | 755 ++++++---------- frontend/src/lib/store.ts | 5 +- 6 files changed, 949 insertions(+), 1774 deletions(-) diff --git a/deploy.sh b/deploy.sh index cff5d9e..477aad4 100755 --- a/deploy.sh +++ b/deploy.sh @@ -1,156 +1,79 @@ #!/bin/bash -# -# POUNCE Deployment Script -# Usage: ./deploy.sh [dev|prod] -# + +# ============================================================================ +# POUNCE DEPLOY SCRIPT +# Commits all changes and deploys to server +# ============================================================================ set -e -MODE=${1:-dev} -echo "================================================" -echo " POUNCE Deployment - Mode: $MODE" -echo "================================================" -echo "" - # Colors -RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' +BLUE='\033[0;34m' +RED='\033[0;31m' NC='\033[0m' # No Color -# Functions -log_info() { echo -e "${GREEN}[INFO]${NC} $1"; } -log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; } -log_error() { echo -e "${RED}[ERROR]${NC} $1"; } +# Server config +SERVER_USER="user" +SERVER_HOST="10.42.0.73" +SERVER_PATH="/home/user/pounce" +SERVER_PASS="user" -# ============================================ -# 1. Check prerequisites -# ============================================ -log_info "Checking prerequisites..." +echo -e "${BLUE}═══════════════════════════════════════════════════════════════${NC}" +echo -e "${BLUE} POUNCE DEPLOY SCRIPT ${NC}" +echo -e "${BLUE}═══════════════════════════════════════════════════════════════${NC}" -if ! command -v python3 &> /dev/null; then - log_error "Python3 not found. Please install Python 3.10+" - exit 1 -fi - -if ! command -v node &> /dev/null; then - log_error "Node.js not found. Please install Node.js 18+" - exit 1 -fi - -if ! command -v npm &> /dev/null; then - log_error "npm not found. Please install npm" - exit 1 -fi - -log_info "Prerequisites OK!" -echo "" - -# ============================================ -# 2. Setup Backend -# ============================================ -log_info "Setting up Backend..." - -cd backend - -# Create virtual environment if not exists -if [ ! -d "venv" ]; then - log_info "Creating Python virtual environment..." - python3 -m venv venv -fi - -# Activate venv -source venv/bin/activate - -# Install dependencies -log_info "Installing Python dependencies..." -pip install -q --upgrade pip -pip install -q -r requirements.txt - -# Create .env if not exists -if [ ! -f ".env" ]; then - log_warn ".env file not found, copying from env.example..." - if [ -f "env.example" ]; then - cp env.example .env - log_warn "Please edit backend/.env with your settings!" - else - log_error "env.example not found!" - fi -fi - -# Initialize database -log_info "Initializing database..." -python scripts/init_db.py - -cd .. -log_info "Backend setup complete!" -echo "" - -# ============================================ -# 3. Setup Frontend -# ============================================ -log_info "Setting up Frontend..." - -cd frontend - -# Install dependencies -log_info "Installing npm dependencies..." -npm install --silent - -# Create .env.local if not exists -if [ ! -f ".env.local" ]; then - log_warn ".env.local not found, creating..." - echo "NEXT_PUBLIC_API_URL=http://localhost:8000" > .env.local -fi - -# Build for production -if [ "$MODE" == "prod" ]; then - log_info "Building for production..." - npm run build -fi - -cd .. -log_info "Frontend setup complete!" -echo "" - -# ============================================ -# 4. Start Services -# ============================================ -if [ "$MODE" == "dev" ]; then - echo "" - echo "================================================" - echo " Development Setup Complete!" - echo "================================================" - echo "" - echo "To start the services:" - echo "" - echo " Backend (Terminal 1):" - echo " cd backend && source venv/bin/activate" - echo " uvicorn app.main:app --reload --host 0.0.0.0 --port 8000" - echo "" - echo " Frontend (Terminal 2):" - echo " cd frontend && npm run dev" - echo "" - echo "Then open: http://localhost:3000" - echo "" +# Get commit message +if [ -z "$1" ]; then + COMMIT_MSG="Deploy: $(date '+%Y-%m-%d %H:%M')" else - echo "" - echo "================================================" - echo " Production Setup Complete!" - echo "================================================" - echo "" - echo "To start with PM2:" - echo "" - echo " # Backend" - echo " cd backend && source venv/bin/activate" - echo " pm2 start 'uvicorn app.main:app --host 0.0.0.0 --port 8000' --name pounce-backend" - echo "" - echo " # Frontend" - echo " cd frontend" - echo " pm2 start 'npm start' --name pounce-frontend" - echo "" - echo "Or use Docker:" - echo " docker-compose up -d" - echo "" + COMMIT_MSG="$1" fi +echo -e "\n${YELLOW}[1/5] Staging changes...${NC}" +git add -A + +echo -e "\n${YELLOW}[2/5] Committing: ${COMMIT_MSG}${NC}" +git commit -m "$COMMIT_MSG" || echo "Nothing to commit" + +echo -e "\n${YELLOW}[3/5] Pushing to git.6bit.ch...${NC}" +git push origin main + +echo -e "\n${YELLOW}[4/5] Syncing files to server...${NC}" +# Sync frontend +sshpass -p "$SERVER_PASS" rsync -avz --delete \ + --exclude 'node_modules' \ + --exclude '.next' \ + --exclude '.git' \ + frontend/ $SERVER_USER@$SERVER_HOST:$SERVER_PATH/frontend/ + +# Sync backend +sshpass -p "$SERVER_PASS" rsync -avz --delete \ + --exclude '__pycache__' \ + --exclude '.pytest_cache' \ + --exclude 'venv' \ + --exclude '.git' \ + --exclude '*.pyc' \ + backend/ $SERVER_USER@$SERVER_HOST:$SERVER_PATH/backend/ + +echo -e "\n${YELLOW}[5/5] Building and restarting on server...${NC}" +sshpass -p "$SERVER_PASS" ssh $SERVER_USER@$SERVER_HOST << 'EOF' + cd ~/pounce + + # Build frontend + echo "Building frontend..." + cd frontend + npm run build 2>&1 | tail -10 + cd .. + + # Restart services + echo "Restarting services..." + ./start.sh 2>&1 | tail -5 +EOF + +echo -e "\n${GREEN}═══════════════════════════════════════════════════════════════${NC}" +echo -e "${GREEN} ✅ DEPLOY COMPLETE! ${NC}" +echo -e "${GREEN}═══════════════════════════════════════════════════════════════${NC}" +echo -e "\nFrontend: ${BLUE}http://$SERVER_HOST:3000${NC}" +echo -e "Backend: ${BLUE}http://$SERVER_HOST:8000${NC}" diff --git a/frontend/src/app/login/page.tsx b/frontend/src/app/login/page.tsx index 4c700c0..1105ed3 100644 --- a/frontend/src/app/login/page.tsx +++ b/frontend/src/app/login/page.tsx @@ -102,18 +102,11 @@ function LoginForm() { try { await login(email, password) - // Check if email is verified - const user = await api.getMe() - if (!user.is_verified) { - // Redirect to verify-email page if not verified - router.push(`/verify-email?email=${encodeURIComponent(email)}`) - return - } - // Clear stored redirect (was set during registration) localStorage.removeItem('pounce_redirect_after_login') // Redirect to intended destination or dashboard + // Note: Email verification is enforced by the backend if REQUIRE_EMAIL_VERIFICATION=true router.push(sanitizeRedirect(redirectTo)) } catch (err: unknown) { console.error('Login error:', err) diff --git a/frontend/src/app/terminal/listing/page.tsx b/frontend/src/app/terminal/listing/page.tsx index e195184..d1bd1b7 100755 --- a/frontend/src/app/terminal/listing/page.tsx +++ b/frontend/src/app/terminal/listing/page.tsx @@ -4,29 +4,14 @@ import { useEffect, useState, useCallback } from 'react' import { useSearchParams } from 'next/navigation' import { useStore } from '@/lib/store' import { api } from '@/lib/api' -import { CommandCenterLayout } from '@/components/CommandCenterLayout' +import { Sidebar } from '@/components/Sidebar' import { - Plus, - Shield, - Eye, - MessageSquare, - ExternalLink, - Loader2, - Trash2, - CheckCircle, - AlertCircle, - Copy, - DollarSign, - X, - Tag, - Sparkles, - ArrowRight, - TrendingUp, - Globe, - Crown, - Check + Plus, Shield, Eye, MessageSquare, ExternalLink, Loader2, Trash2, + CheckCircle, AlertCircle, Copy, DollarSign, X, Tag, Sparkles, + TrendingUp, Gavel, Target, Menu, Settings, LogOut, Crown, Zap, Coins, Check } from 'lucide-react' import Link from 'next/link' +import Image from 'next/image' import clsx from 'clsx' // ============================================================================ @@ -44,39 +29,13 @@ interface Listing { currency: string price_type: string pounce_score: number | null - estimated_value: number | null verification_status: string is_verified: boolean status: string - show_valuation: boolean - allow_offers: boolean view_count: number inquiry_count: number public_url: string created_at: string - published_at: string | null -} - -interface VerificationInfo { - verification_code: string - dns_record_type: string - dns_record_name: string - dns_record_value: string - instructions: string - status: string -} - -interface Inquiry { - id: number - name: string - email: string - phone: string | null - company: string | null - message: string - offer_amount: number | null - status: string - created_at: string - read_at: string | null } // ============================================================================ @@ -84,683 +43,370 @@ interface Inquiry { // ============================================================================ export default function MyListingsPage() { - const { subscription } = useStore() + const { subscription, user, logout, checkAuth } = useStore() const searchParams = useSearchParams() const prefillDomain = searchParams.get('domain') const [listings, setListings] = useState([]) const [loading, setLoading] = useState(true) - const [showCreateModal, setShowCreateModal] = useState(false) - const [showVerifyModal, setShowVerifyModal] = useState(false) - const [showInquiriesModal, setShowInquiriesModal] = useState(false) - const [selectedListing, setSelectedListing] = useState(null) - const [verificationInfo, setVerificationInfo] = useState(null) - const [inquiries, setInquiries] = useState([]) - const [loadingInquiries, setLoadingInquiries] = useState(false) - const [verifying, setVerifying] = useState(false) - const [creating, setCreating] = useState(false) - const [error, setError] = useState(null) - const [success, setSuccess] = useState(null) - const [copied, setCopied] = useState(false) + const [deletingId, setDeletingId] = useState(null) + const [menuOpen, setMenuOpen] = useState(false) - const [newListing, setNewListing] = useState({ - domain: '', - title: '', - description: '', - asking_price: '', - price_type: 'negotiable', - allow_offers: true, - }) - const tier = subscription?.tier || 'scout' - const limits: Record = { scout: 0, trader: 5, tycoon: 50 } - const maxListings = limits[tier] || 0 - const canList = tier !== 'scout' + const listingLimits: Record = { scout: 3, trader: 25, tycoon: 100 } + const maxListings = listingLimits[tier] || 3 + const canAddMore = listings.length < maxListings const isTycoon = tier === 'tycoon' - const activeCount = listings.filter(l => l.status === 'active').length + const activeListings = listings.filter(l => l.status === 'active').length const totalViews = listings.reduce((sum, l) => sum + l.view_count, 0) const totalInquiries = listings.reduce((sum, l) => sum + l.inquiry_count, 0) + useEffect(() => { checkAuth() }, [checkAuth]) + useEffect(() => { if (prefillDomain) setShowCreateModal(true) }, [prefillDomain]) + const loadListings = useCallback(async () => { setLoading(true) try { - const data = await api.request('/listings/my') + const data = await api.getMyListings() setListings(data) - } catch (err: any) { - console.error('Failed to load listings:', err) - } finally { - setLoading(false) - } + } catch (err) { console.error(err) } + finally { setLoading(false) } }, []) - useEffect(() => { - loadListings() - }, [loadListings]) + useEffect(() => { loadListings() }, [loadListings]) - useEffect(() => { - if (prefillDomain) { - setNewListing(prev => ({ ...prev, domain: prefillDomain })) - setShowCreateModal(true) - } - }, [prefillDomain]) - - const handleCreate = async (e: React.FormEvent) => { - e.preventDefault() - setCreating(true) - setError(null) - + const handleDelete = async (id: number, domain: string) => { + if (!confirm(`Delete listing for ${domain}?`)) return + setDeletingId(id) try { - await api.request('/listings', { - method: 'POST', - body: JSON.stringify({ - domain: newListing.domain, - title: newListing.title || null, - description: newListing.description || null, - asking_price: newListing.asking_price ? parseFloat(newListing.asking_price) : null, - price_type: newListing.price_type, - allow_offers: newListing.allow_offers, - }), - }) - setSuccess('Listing created! Verify ownership to publish.') - setShowCreateModal(false) - setNewListing({ domain: '', title: '', description: '', asking_price: '', price_type: 'negotiable', allow_offers: true }) - loadListings() - } catch (err: any) { - setError(err.message) - } finally { - setCreating(false) - } + await api.deleteListing(id) + await loadListings() + } catch (err: any) { alert(err.message || 'Failed') } + finally { setDeletingId(null) } } - const handleStartVerification = async (listing: Listing) => { - setSelectedListing(listing) - setVerifying(true) - - try { - const info = await api.request(`/listings/${listing.id}/verify-dns`, { - method: 'POST', - }) - setVerificationInfo(info) - setShowVerifyModal(true) - } catch (err: any) { - setError(err.message) - } finally { - setVerifying(false) - } - } - - const handleViewInquiries = async (listing: Listing) => { - setSelectedListing(listing) - setLoadingInquiries(true) - setShowInquiriesModal(true) - - try { - const data = await api.request(`/listings/${listing.id}/inquiries`) - setInquiries(data) - } catch (err: any) { - setError(err.message) - setShowInquiriesModal(false) - } finally { - setLoadingInquiries(false) - } - } - - const handleCheckVerification = async () => { - if (!selectedListing) return - setVerifying(true) - - try { - const result = await api.request<{ verified: boolean; message: string }>( - `/listings/${selectedListing.id}/verify-dns/check` - ) - - if (result.verified) { - setSuccess('Domain verified! You can now publish.') - setShowVerifyModal(false) - loadListings() - } else { - setError(result.message) - } - } catch (err: any) { - setError(err.message) - } finally { - setVerifying(false) - } - } - - const handlePublish = async (listing: Listing) => { - try { - await api.request(`/listings/${listing.id}`, { - method: 'PUT', - body: JSON.stringify({ status: 'active' }), - }) - setSuccess('Listing published!') - loadListings() - } catch (err: any) { - setError(err.message) - } - } - - const handleDelete = async (listing: Listing) => { - if (!confirm(`Delete listing for ${listing.domain}?`)) return - - try { - await api.request(`/listings/${listing.id}`, { method: 'DELETE' }) - setSuccess('Listing deleted') - loadListings() - } catch (err: any) { - setError(err.message) - } - } - - const copyToClipboard = (text: string) => { - navigator.clipboard.writeText(text) - setCopied(true) - setTimeout(() => setCopied(false), 2000) - } - - const formatPrice = (price: number | null, currency: string) => { - if (!price) return 'Make Offer' - return new Intl.NumberFormat('en-US', { - style: 'currency', - currency, - minimumFractionDigits: 0, - }).format(price) - } + const mobileNavItems = [ + { 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 }, + ] + + const tierName = subscription?.tier_name || subscription?.tier || 'Scout' + const TierIcon = tierName === 'Tycoon' ? Crown : tierName === 'Trader' ? TrendingUp : Zap + + const drawerNavSections = [ + { title: 'Discover', items: [ + { href: '/terminal/radar', label: 'Radar', icon: Target }, + { href: '/terminal/market', label: 'Market', icon: Gavel }, + { href: '/terminal/intel', label: 'Intel', icon: TrendingUp }, + ]}, + { title: 'Manage', items: [ + { href: '/terminal/watchlist', label: 'Watchlist', icon: Eye }, + { href: '/terminal/sniper', label: 'Sniper', icon: Target }, + ]}, + { title: 'Monetize', items: [ + { href: '/terminal/yield', label: 'Yield', icon: Coins }, + { href: '/terminal/listing', label: 'For Sale', icon: Tag, active: true }, + ]} + ] return ( - - {/* ═══════════════════════════════════════════════════════════════════════ */} - {/* HEADER */} - {/* ═══════════════════════════════════════════════════════════════════════ */} -
-
-
-
- - Marketplace -
- -

- For Sale - {listings.length}/{maxListings} -

- -

- List domains on Pounce Marketplace. 0% commission. -

-
- -
- {canList && ( -
-
-
{activeCount}
-
Active
-
-
-
{totalViews}
-
Views
-
-
-
{totalInquiries}
-
Inquiries
-
-
- )} - - -
-
-
- - {/* Messages */} - {error && ( -
- -

{error}

- -
- )} +
+
- {success && ( -
- -

{success}

- -
- )} - - {/* ═══════════════════════════════════════════════════════════════════════ */} - {/* PAYWALL */} - {/* ═══════════════════════════════════════════════════════════════════════ */} - {!canList && ( -
-
- -

Unlock Domain Selling

-

- List your domains with 0% commission on Pounce Marketplace. -

- -
-
- -
Trader
-
$9/mo
-
5 Listings
+
+ {/* MOBILE HEADER */} +
+
+
+
+
+ For Sale
-
- -
Tycoon
-
$29/mo
-
50 Listings
-
+ Featured Badge
+ {listings.length}/{maxListings} +
+
+
+
{activeListings}
+
Active
+
+
+
{totalViews}
+
Views
+
+
+
{totalInquiries}
+
Leads
+
+
- - Upgrade Now - + {/* DESKTOP HEADER */} +
+
+
+
+
+ Domain Marketplace +
+

+ For Sale + {listings.length}/{maxListings} +

+
+
+
+
+
{activeListings}
+
Active
+
+
+
{totalViews}
+
Views
+
+
+
{totalInquiries}
+
Leads
+
+
+ +
- )} - {/* ═══════════════════════════════════════════════════════════════════════ */} - {/* LISTINGS TABLE */} - {/* ═══════════════════════════════════════════════════════════════════════ */} - {canList && ( -
+ {/* ADD BUTTON MOBILE */} +
+ +
+ + {/* CONTENT */} +
{loading ? (
) : listings.length === 0 ? ( -
-
- -
-

No listings yet

-

Create your first listing to start selling

- +
+ +

No listings yet

+

Create your first listing

) : ( -
+
+ {/* Header */} +
+
Domain
+
Price
+
Status
+
Views
+
Leads
+
Actions
+
+ {listings.map((listing) => ( -
-
-
-
- {listing.domain.charAt(0).toUpperCase()} - {isTycoon && listing.status === 'active' && ( -
- -
- )} -
- -
-
- {listing.domain} - {listing.is_verified && } - - {listing.status} - +
+ {/* Mobile */} +
+
+
+
+ {listing.is_verified ? : }
-
{listing.title || 'No headline'}
+ {listing.domain} +
+ {listing.status} +
+
+ ${listing.asking_price?.toLocaleString() || 'Negotiable'} + {listing.view_count} views · {listing.inquiry_count} leads +
+
+ + View + + +
+
+ + {/* Desktop */} +
+
+
+ {listing.is_verified ? : } +
+
+ {listing.domain} + {isTycoon && Featured}
- -
-
-
{formatPrice(listing.asking_price, listing.currency)}
- {listing.pounce_score &&
Score: {listing.pounce_score}
} -
- -
- - - {listing.view_count} - - -
- -
- {!listing.is_verified ? ( - - ) : listing.status === 'draft' ? ( - - ) : ( - - - - )} - - -
+
${listing.asking_price?.toLocaleString() || '—'}
+
+ {listing.status} +
+
{listing.view_count}
+
{listing.inquiry_count}
+
+ + + +
))}
)} + + {!canAddMore && ( +
+ +

Listing Limit Reached

+

Upgrade for more listings

+ + Upgrade + +
+ )}
- )} - {/* ═══════════════════════════════════════════════════════════════════════ */} + {/* MOBILE BOTTOM NAV */} + + + {/* DRAWER */} + {menuOpen && ( +
+
setMenuOpen(false)} /> +
+
+
+ Pounce +

POUNCE

Terminal v1.0

+
+ +
+
+ {drawerNavSections.map((section) => ( +
+
{section.title}
+ {section.items.map((item: any) => ( + setMenuOpen(false)} className={clsx("flex items-center gap-3 px-4 py-2.5 border-l-2 border-transparent", item.active ? "text-accent border-accent bg-white/[0.02]" : "text-white/60")}> + {item.label} + + ))} +
+ ))} +
+ setMenuOpen(false)} className="flex items-center gap-3 py-2.5 text-white/50">Settings + {user?.is_admin && setMenuOpen(false)} className="flex items-center gap-3 py-2.5 text-amber-500/70">Admin} +
+
+
+
+
+

{user?.name || user?.email?.split('@')[0] || 'User'}

{tierName}

+
+ {tierName === 'Scout' && setMenuOpen(false)} className="flex items-center justify-center gap-2 w-full py-2.5 bg-accent text-black text-xs font-bold uppercase mb-2">Upgrade} + +
+
+
+ )} +
+ {/* CREATE MODAL */} - {/* ═══════════════════════════════════════════════════════════════════════ */} {showCreateModal && ( -
setShowCreateModal(false)}> -
e.stopPropagation()}> -
-

Create Listing

-

List your domain for sale

-
- -
-
- - setNewListing({ ...newListing, domain: e.target.value })} - placeholder="example.com" - className="w-full px-4 py-3 bg-white/5 border border-white/10 text-white placeholder:text-white/25 outline-none focus:border-accent/50 font-mono" - /> -
- -
- - setNewListing({ ...newListing, title: e.target.value })} - placeholder="Short, catchy title" - className="w-full px-4 py-3 bg-white/5 border border-white/10 text-white placeholder:text-white/25 outline-none focus:border-accent/50" - /> -
- -
-
- -
- - setNewListing({ ...newListing, asking_price: e.target.value })} - placeholder="Make Offer" - className="w-full pl-9 pr-4 py-3 bg-white/5 border border-white/10 text-white placeholder:text-white/25 outline-none focus:border-accent/50 font-mono" - /> -
-
- -
- - -
-
- - - -
- - -
-
-
-
+ setShowCreateModal(false)} + onSuccess={() => { loadListings(); setShowCreateModal(false) }} + prefillDomain={prefillDomain || ''} + /> )} - - {/* ═══════════════════════════════════════════════════════════════════════ */} - {/* VERIFY MODAL */} - {/* ═══════════════════════════════════════════════════════════════════════ */} - {showVerifyModal && verificationInfo && selectedListing && ( -
setShowVerifyModal(false)}> -
e.stopPropagation()}> -
-

Verify Ownership

-

- Add this DNS TXT record to {selectedListing.domain} -

-
- -
-
-
-
Type
-
- {verificationInfo.dns_record_type} -
-
-
-
Name / Host
-
copyToClipboard(verificationInfo.dns_record_name)} - > - {verificationInfo.dns_record_name} - {copied ? : } -
-
-
- -
-
Value
-
copyToClipboard(verificationInfo.dns_record_value)} - > - {verificationInfo.dns_record_value} - {copied ? : } -
-
- -
-

- After adding the record, it may take up to 24 hours to propagate. Click verify to check. -

-
-
- -
- - -
-
-
- )} - - {/* ═══════════════════════════════════════════════════════════════════════ */} - {/* INQUIRIES MODAL */} - {/* ═══════════════════════════════════════════════════════════════════════ */} - {showInquiriesModal && selectedListing && ( -
setShowInquiriesModal(false)}> -
e.stopPropagation()}> -
-
-

Inquiries

-

- {inquiries.length} for {selectedListing.domain} -

-
- -
- -
- {loadingInquiries ? ( -
- -
- ) : inquiries.length === 0 ? ( -
- -

No inquiries yet

-
- ) : ( - inquiries.map((inquiry) => ( -
-
-
-
{inquiry.name}
-
{inquiry.email}
- {inquiry.company &&
{inquiry.company}
} -
-
- {inquiry.offer_amount && ( -
- ${inquiry.offer_amount.toLocaleString()} -
- )} -
- {new Date(inquiry.created_at).toLocaleDateString()} -
-
-
-

- {inquiry.message} -

-
- - {inquiry.status} - - - Reply - -
-
- )) - )} -
-
-
- )} - +
+ ) +} + +// ============================================================================ +// CREATE MODAL (simplified) +// ============================================================================ + +function CreateListingModal({ onClose, onSuccess, prefillDomain }: { onClose: () => void; onSuccess: () => void; prefillDomain: string }) { + const [domain, setDomain] = useState(prefillDomain) + const [price, setPrice] = 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.createListing({ domain: domain.trim(), asking_price: price ? parseFloat(price) : null, currency: 'USD', price_type: price ? 'fixed' : 'negotiable' }) + onSuccess() + } catch (err: any) { setError(err.message || 'Failed') } + finally { setLoading(false) } + } + + return ( +
+
e.stopPropagation()}> +
+
New Listing
+ +
+
+ {error &&
{error}
} +
+ + setDomain(e.target.value)} required + 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="example.com" /> +
+
+ + setPrice(e.target.value)} min="0" + 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="Leave empty for negotiable" /> +
+
+ + +
+
+
+
) } diff --git a/frontend/src/app/terminal/sniper/page.tsx b/frontend/src/app/terminal/sniper/page.tsx index e75c725..0d7bd67 100644 --- a/frontend/src/app/terminal/sniper/page.tsx +++ b/frontend/src/app/terminal/sniper/page.tsx @@ -3,7 +3,7 @@ import { useEffect, useState, useCallback } from 'react' import { useStore } from '@/lib/store' import { api } from '@/lib/api' -import { CommandCenterLayout } from '@/components/CommandCenterLayout' +import { Sidebar } from '@/components/Sidebar' import { Plus, Target, @@ -22,10 +22,20 @@ import { DollarSign, Hash, Crown, - Activity + Eye, + Gavel, + TrendingUp, + Menu, + Settings, + Shield, + LogOut, + Sparkles, + Coins, + Tag } from 'lucide-react' import clsx from 'clsx' import Link from 'next/link' +import Image from 'next/image' // ============================================================================ // INTERFACES @@ -62,13 +72,16 @@ interface SniperAlert { // ============================================================================ export default function SniperAlertsPage() { - const { subscription } = useStore() + const { subscription, user, logout, checkAuth } = useStore() const [alerts, setAlerts] = useState([]) const [loading, setLoading] = useState(true) const [showCreateModal, setShowCreateModal] = useState(false) const [editingAlert, setEditingAlert] = useState(null) const [deletingId, setDeletingId] = useState(null) const [togglingId, setTogglingId] = useState(null) + + // Mobile Menu + const [menuOpen, setMenuOpen] = useState(false) const tier = subscription?.tier || 'scout' const alertLimits: Record = { scout: 2, trader: 10, tycoon: 50 } @@ -80,6 +93,10 @@ export default function SniperAlertsPage() { const totalMatches = alerts.reduce((sum, a) => sum + a.matches_count, 0) const totalNotifications = alerts.reduce((sum, a) => sum + a.notifications_sent, 0) + useEffect(() => { + checkAuth() + }, [checkAuth]) + const loadAlerts = useCallback(async () => { setLoading(true) try { @@ -125,277 +142,309 @@ export default function SniperAlertsPage() { } } - return ( - - {/* ═══════════════════════════════════════════════════════════════════════ */} - {/* HEADER */} - {/* ═══════════════════════════════════════════════════════════════════════ */} -
-
-
-
- - Automated Alerts -
- -

- Sniper - {alerts.length}/{maxAlerts} -

- -

- Get notified when domains matching your criteria hit the market -

-
- -
-
-
-
{activeAlerts}
-
Active
-
-
-
{totalMatches}
-
Matches
-
-
-
{totalNotifications}
-
Sent
-
-
- - -
-
-
+ // Mobile Nav + const mobileNavItems = [ + { 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 }, + ] + + const tierName = subscription?.tier_name || subscription?.tier || 'Scout' + const TierIcon = tierName === 'Tycoon' ? Crown : tierName === 'Trader' ? TrendingUp : Zap + + const drawerNavSections = [ + { title: 'Discover', items: [ + { href: '/terminal/radar', label: 'Radar', icon: Target }, + { href: '/terminal/market', label: 'Market', icon: Gavel }, + { href: '/terminal/intel', label: 'Intel', icon: TrendingUp }, + ]}, + { title: 'Manage', items: [ + { href: '/terminal/watchlist', label: 'Watchlist', icon: Eye }, + { href: '/terminal/sniper', label: 'Sniper', icon: Target, active: true }, + ]}, + { title: 'Monetize', items: [ + { href: '/terminal/yield', label: 'Yield', icon: Coins, isNew: true }, + { href: '/terminal/listing', label: 'For Sale', icon: Tag }, + ]} + ] - {/* ═══════════════════════════════════════════════════════════════════════ */} - {/* ALERTS LIST */} - {/* ═══════════════════════════════════════════════════════════════════════ */} -
- {loading ? ( -
- -
- ) : alerts.length === 0 ? ( -
-
- + return ( +
+
+ +
+ {/* MOBILE HEADER */} +
+
+
+
+
+ Sniper Alerts +
+
{alerts.length}/{maxAlerts}
+
+ +
+
+
{activeAlerts}
+
Active
+
+
+
{totalMatches}
+
Matches
+
+
+
{totalNotifications}
+
Sent
+
-

No alerts yet

-

- Create your first sniper alert to get notified when matching domains appear -

-
- ) : ( -
- {alerts.map((alert) => ( -
+ + {/* DESKTOP HEADER */} +
+
+
+
+
+ Automated Alerts +
+

+ Sniper + {alerts.length}/{maxAlerts} +

+
+ +
+
+
+
{activeAlerts}
+
Active
+
+
+
{totalMatches}
+
Matches
+
+
+ + +
+
+
+ + {/* ADD BUTTON MOBILE */} +
+ +
+ + {/* ALERTS LIST */} +
+ {loading ? ( +
+ +
+ ) : alerts.length === 0 ? ( +
+ +

No alerts yet

+

Create your first sniper alert

+
+ ) : ( +
+ {alerts.map((alert) => ( +
-
-

{alert.name}

+
+

{alert.name}

{alert.is_active ? ( - -
+ +
Active ) : ( - - Paused - + Paused )} {isTycoon && alert.notify_sms && ( - - - SMS + + SMS )}
- - {alert.description && ( -

{alert.description}

- )} -
- {alert.tlds && ( - - {alert.tlds} - - )} - {alert.keywords && ( - - +{alert.keywords} - - )} - {alert.exclude_keywords && ( - - -{alert.exclude_keywords} - - )} - {(alert.min_length || alert.max_length) && ( - - - {alert.min_length || 1}-{alert.max_length || 63} - - )} +
+ {alert.tlds && {alert.tlds}} + {alert.keywords && +{alert.keywords}} {(alert.min_price || alert.max_price) && ( - + - {alert.min_price ? `$${alert.min_price}` : ''}{alert.max_price ? ` - $${alert.max_price}` : '+'} - - )} - {alert.no_numbers && ( - - No digits - - )} - {alert.no_hyphens && ( - - No hyphens + {alert.min_price || 0}-{alert.max_price || '∞'} )}
-
- - - {alert.matches_count} matches - - - - {alert.notifications_sent} sent - - {alert.last_matched_at && ( - - - {new Date(alert.last_matched_at).toLocaleDateString()} - - )} +
+ {alert.matches_count} + {alert.notifications_sent}
-
+
- - -
-
- ))} -
- )} -
+ ))} +
+ )} - {/* ═══════════════════════════════════════════════════════════════════════ */} - {/* UPGRADE CTA */} - {/* ═══════════════════════════════════════════════════════════════════════ */} - {!canAddMore && ( -
-
- -

Alert Limit Reached

-

- You've created {maxAlerts} alerts. Upgrade for more. -

-
-
- Trader: 10 alerts + {!canAddMore && ( +
+ +

Alert Limit Reached

+

Upgrade for more alerts

+ + Upgrade + +
+ )} +
+ + {/* MOBILE BOTTOM NAV */} + + + {/* DRAWER */} + {menuOpen && ( +
+
setMenuOpen(false)} /> +
+
+
+ Pounce +
+

POUNCE

+

Terminal v1.0

+
+
+
-
- Tycoon: 50 + SMS +
+ {drawerNavSections.map((section) => ( +
+
+
+ {section.title} +
+ {section.items.map((item: any) => ( + setMenuOpen(false)} className={clsx("flex items-center gap-3 px-4 py-2.5 border-l-2 border-transparent", item.active ? "text-accent border-accent bg-white/[0.02]" : "text-white/60")}> + + {item.label} + {item.isNew && NEW} + + ))} +
+ ))} +
+ setMenuOpen(false)} className="flex items-center gap-3 py-2.5 text-white/50"> + Settings + + {user?.is_admin && ( + setMenuOpen(false)} className="flex items-center gap-3 py-2.5 text-amber-500/70"> + Admin + + )} +
+
+
+
+
+ +
+
+

{user?.name || user?.email?.split('@')[0] || 'User'}

+

{tierName}

+
+
+ {tierName === 'Scout' && ( + setMenuOpen(false)} className="flex items-center justify-center gap-2 w-full py-2.5 bg-accent text-black text-xs font-bold uppercase mb-2"> + Upgrade + + )} +
- - Upgrade Now -
-
- )} + )} + - {/* Create/Edit Modal */} + {/* MODALS */} {(showCreateModal || editingAlert) && ( { - setShowCreateModal(false) - setEditingAlert(null) - }} - onSuccess={() => { - loadAlerts() - setShowCreateModal(false) - setEditingAlert(null) - }} + onClose={() => { setShowCreateModal(false); setEditingAlert(null) }} + onSuccess={() => { loadAlerts(); setShowCreateModal(false); setEditingAlert(null) }} isTycoon={isTycoon} /> )} -
+
) } @@ -459,271 +508,96 @@ function CreateEditModal({ alert, onClose, onSuccess, isTycoon }: { notify_sms: form.notify_sms && isTycoon, } - if (isEditing) { - await api.request(`/sniper-alerts/${alert.id}`, { - method: 'PUT', - body: JSON.stringify(payload), - }) + if (isEditing && alert) { + await api.request(`/sniper-alerts/${alert.id}`, { method: 'PUT', body: JSON.stringify(payload) }) } else { - await api.request('/sniper-alerts', { - method: 'POST', - body: JSON.stringify(payload), - }) + await api.request('/sniper-alerts', { method: 'POST', body: JSON.stringify(payload) }) } - onSuccess() } catch (err: any) { - setError(err.message || 'Failed to save alert') + setError(err.message || 'Failed to save') } finally { setLoading(false) } } return ( -
-
e.stopPropagation()} - > - {/* Header */} -
-
- -
-

{isEditing ? 'Edit Alert' : 'Create Sniper Alert'}

-

Set criteria for domain matching

-
+
+
e.stopPropagation()}> +
+
+ + {isEditing ? 'Edit' : 'Create'} Alert
-
-
+ {error && ( -
- -

{error}

+
+ {error}
)} - {/* Basic Info */} -
-

Basic Info

- -
- - setForm({ ...form, name: e.target.value })} - placeholder="e.g. Premium 4L .com domains" - required - className="w-full px-4 py-3 bg-white/5 border border-white/10 text-white placeholder:text-white/25 outline-none focus:border-accent/50 transition-all" - /> -
+
+ + setForm({ ...form, name: e.target.value })} required + 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="e.g. Premium 4L .com" /> +
+
- -