Watchlist layout fix + TLD detail + Sniper/Yield/Listing redesign + deploy script
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

This commit is contained in:
2025-12-13 15:39:51 +01:00
parent 6a56360f56
commit fde66af049
6 changed files with 949 additions and 1774 deletions

207
deploy.sh
View File

@ -1,156 +1,79 @@
#!/bin/bash #!/bin/bash
#
# POUNCE Deployment Script # ============================================================================
# Usage: ./deploy.sh [dev|prod] # POUNCE DEPLOY SCRIPT
# # Commits all changes and deploys to server
# ============================================================================
set -e set -e
MODE=${1:-dev}
echo "================================================"
echo " POUNCE Deployment - Mode: $MODE"
echo "================================================"
echo ""
# Colors # Colors
RED='\033[0;31m'
GREEN='\033[0;32m' GREEN='\033[0;32m'
YELLOW='\033[1;33m' YELLOW='\033[1;33m'
BLUE='\033[0;34m'
RED='\033[0;31m'
NC='\033[0m' # No Color NC='\033[0m' # No Color
# Functions # Server config
log_info() { echo -e "${GREEN}[INFO]${NC} $1"; } SERVER_USER="user"
log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; } SERVER_HOST="10.42.0.73"
log_error() { echo -e "${RED}[ERROR]${NC} $1"; } SERVER_PATH="/home/user/pounce"
SERVER_PASS="user"
# ============================================ echo -e "${BLUE}═══════════════════════════════════════════════════════════════${NC}"
# 1. Check prerequisites echo -e "${BLUE} POUNCE DEPLOY SCRIPT ${NC}"
# ============================================ echo -e "${BLUE}═══════════════════════════════════════════════════════════════${NC}"
log_info "Checking prerequisites..."
if ! command -v python3 &> /dev/null; then # Get commit message
log_error "Python3 not found. Please install Python 3.10+" if [ -z "$1" ]; then
exit 1 COMMIT_MSG="Deploy: $(date '+%Y-%m-%d %H:%M')"
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 ""
else else
echo "" COMMIT_MSG="$1"
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 ""
fi 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}"

View File

@ -102,18 +102,11 @@ function LoginForm() {
try { try {
await login(email, password) 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) // Clear stored redirect (was set during registration)
localStorage.removeItem('pounce_redirect_after_login') localStorage.removeItem('pounce_redirect_after_login')
// Redirect to intended destination or dashboard // Redirect to intended destination or dashboard
// Note: Email verification is enforced by the backend if REQUIRE_EMAIL_VERIFICATION=true
router.push(sanitizeRedirect(redirectTo)) router.push(sanitizeRedirect(redirectTo))
} catch (err: unknown) { } catch (err: unknown) {
console.error('Login error:', err) console.error('Login error:', err)

File diff suppressed because it is too large Load Diff

View File

@ -3,7 +3,7 @@
import { useEffect, useState, useCallback } from 'react' import { useEffect, useState, useCallback } from 'react'
import { useStore } from '@/lib/store' import { useStore } from '@/lib/store'
import { api } from '@/lib/api' import { api } from '@/lib/api'
import { CommandCenterLayout } from '@/components/CommandCenterLayout' import { Sidebar } from '@/components/Sidebar'
import { import {
Plus, Plus,
Target, Target,
@ -22,10 +22,20 @@ import {
DollarSign, DollarSign,
Hash, Hash,
Crown, Crown,
Activity Eye,
Gavel,
TrendingUp,
Menu,
Settings,
Shield,
LogOut,
Sparkles,
Coins,
Tag
} from 'lucide-react' } from 'lucide-react'
import clsx from 'clsx' import clsx from 'clsx'
import Link from 'next/link' import Link from 'next/link'
import Image from 'next/image'
// ============================================================================ // ============================================================================
// INTERFACES // INTERFACES
@ -62,13 +72,16 @@ interface SniperAlert {
// ============================================================================ // ============================================================================
export default function SniperAlertsPage() { export default function SniperAlertsPage() {
const { subscription } = useStore() const { subscription, user, logout, checkAuth } = useStore()
const [alerts, setAlerts] = useState<SniperAlert[]>([]) const [alerts, setAlerts] = useState<SniperAlert[]>([])
const [loading, setLoading] = useState(true) const [loading, setLoading] = useState(true)
const [showCreateModal, setShowCreateModal] = useState(false) const [showCreateModal, setShowCreateModal] = useState(false)
const [editingAlert, setEditingAlert] = useState<SniperAlert | null>(null) const [editingAlert, setEditingAlert] = useState<SniperAlert | null>(null)
const [deletingId, setDeletingId] = useState<number | null>(null) const [deletingId, setDeletingId] = useState<number | null>(null)
const [togglingId, setTogglingId] = useState<number | null>(null) const [togglingId, setTogglingId] = useState<number | null>(null)
// Mobile Menu
const [menuOpen, setMenuOpen] = useState(false)
const tier = subscription?.tier || 'scout' const tier = subscription?.tier || 'scout'
const alertLimits: Record<string, number> = { scout: 2, trader: 10, tycoon: 50 } const alertLimits: Record<string, number> = { 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 totalMatches = alerts.reduce((sum, a) => sum + a.matches_count, 0)
const totalNotifications = alerts.reduce((sum, a) => sum + a.notifications_sent, 0) const totalNotifications = alerts.reduce((sum, a) => sum + a.notifications_sent, 0)
useEffect(() => {
checkAuth()
}, [checkAuth])
const loadAlerts = useCallback(async () => { const loadAlerts = useCallback(async () => {
setLoading(true) setLoading(true)
try { try {
@ -125,277 +142,309 @@ export default function SniperAlertsPage() {
} }
} }
return ( // Mobile Nav
<CommandCenterLayout minimal> const mobileNavItems = [
{/* ═══════════════════════════════════════════════════════════════════════ */} { href: '/terminal/radar', label: 'Radar', icon: Target, active: false },
{/* HEADER */} { href: '/terminal/market', label: 'Market', icon: Gavel, active: false },
{/* ═══════════════════════════════════════════════════════════════════════ */} { href: '/terminal/watchlist', label: 'Watch', icon: Eye, active: false },
<section className="pt-6 lg:pt-8 pb-6"> { href: '/terminal/intel', label: 'Intel', icon: TrendingUp, active: false },
<div className="flex flex-col lg:flex-row lg:items-end lg:justify-between gap-6"> ]
<div className="space-y-3">
<div className="inline-flex items-center gap-2"> const tierName = subscription?.tier_name || subscription?.tier || 'Scout'
<Target className="w-4 h-4 text-accent" /> const TierIcon = tierName === 'Tycoon' ? Crown : tierName === 'Trader' ? TrendingUp : Zap
<span className="text-[10px] font-mono tracking-wide text-accent">Automated Alerts</span>
</div> const drawerNavSections = [
{ title: 'Discover', items: [
<h1 className="font-display text-[2rem] lg:text-[2.5rem] leading-[1] tracking-[-0.02em]"> { href: '/terminal/radar', label: 'Radar', icon: Target },
<span className="text-white">Sniper</span> { href: '/terminal/market', label: 'Market', icon: Gavel },
<span className="text-white/30 ml-3">{alerts.length}/{maxAlerts}</span> { href: '/terminal/intel', label: 'Intel', icon: TrendingUp },
</h1> ]},
{ title: 'Manage', items: [
<p className="text-white/40 text-sm max-w-md"> { href: '/terminal/watchlist', label: 'Watchlist', icon: Eye },
Get notified when domains matching your criteria hit the market { href: '/terminal/sniper', label: 'Sniper', icon: Target, active: true },
</p> ]},
</div> { title: 'Monetize', items: [
{ href: '/terminal/yield', label: 'Yield', icon: Coins, isNew: true },
<div className="flex items-center gap-4"> { href: '/terminal/listing', label: 'For Sale', icon: Tag },
<div className="flex gap-6"> ]}
<div className="text-right"> ]
<div className="text-xl font-display text-accent">{activeAlerts}</div>
<div className="text-[9px] tracking-wide text-white/30 font-mono">Active</div>
</div>
<div className="text-right">
<div className="text-xl font-display text-white">{totalMatches}</div>
<div className="text-[9px] tracking-wide text-white/30 font-mono">Matches</div>
</div>
<div className="text-right">
<div className="text-xl font-display text-white">{totalNotifications}</div>
<div className="text-[9px] tracking-wide text-white/30 font-mono">Sent</div>
</div>
</div>
<button
onClick={() => setShowCreateModal(true)}
disabled={!canAddMore}
className={clsx(
"flex items-center gap-2 px-4 py-2 text-sm font-semibold transition-colors",
canAddMore
? "bg-accent text-black hover:bg-white"
: "bg-white/10 text-white/40 cursor-not-allowed"
)}
>
<Plus className="w-4 h-4" />
New Alert
</button>
</div>
</div>
</section>
{/* ═══════════════════════════════════════════════════════════════════════ */} return (
{/* ALERTS LIST */} <div className="min-h-screen bg-[#020202]">
{/* ═══════════════════════════════════════════════════════════════════════ */} <div className="hidden lg:block"><Sidebar /></div>
<section className="py-6 border-t border-white/[0.08]">
{loading ? ( <main className="lg:pl-[240px]">
<div className="flex items-center justify-center py-20"> {/* MOBILE HEADER */}
<Loader2 className="w-6 h-6 text-accent animate-spin" /> <header className="lg:hidden sticky top-0 z-40 bg-[#020202]/95 backdrop-blur-md border-b border-white/[0.08]" style={{ paddingTop: 'env(safe-area-inset-top)' }}>
</div> <div className="px-4 py-3">
) : alerts.length === 0 ? ( <div className="flex items-center justify-between mb-3">
<div className="text-center py-16"> <div className="flex items-center gap-2">
<div className="w-14 h-14 mx-auto bg-white/[0.02] border border-white/10 flex items-center justify-center mb-4"> <div className="w-1.5 h-1.5 bg-accent animate-pulse" />
<Target className="w-6 h-6 text-white/20" /> <span className="text-[10px] font-mono tracking-[0.2em] text-accent uppercase">Sniper Alerts</span>
</div>
<div className="text-[10px] font-mono text-white/40">{alerts.length}/{maxAlerts}</div>
</div>
<div className="grid grid-cols-3 gap-2">
<div className="bg-accent/[0.05] border border-accent/20 p-2">
<div className="text-lg font-bold text-accent tabular-nums">{activeAlerts}</div>
<div className="text-[9px] font-mono text-accent/60 uppercase tracking-wider">Active</div>
</div>
<div className="bg-white/[0.02] border border-white/[0.08] p-2">
<div className="text-lg font-bold text-white tabular-nums">{totalMatches}</div>
<div className="text-[9px] font-mono text-white/30 uppercase tracking-wider">Matches</div>
</div>
<div className="bg-white/[0.02] border border-white/[0.08] p-2">
<div className="text-lg font-bold text-white tabular-nums">{totalNotifications}</div>
<div className="text-[9px] font-mono text-white/30 uppercase tracking-wider">Sent</div>
</div>
</div> </div>
<p className="text-white/40 text-sm mb-2">No alerts yet</p>
<p className="text-white/25 text-xs mb-6 max-w-sm mx-auto">
Create your first sniper alert to get notified when matching domains appear
</p>
<button
onClick={() => setShowCreateModal(true)}
className="inline-flex items-center gap-2 px-4 py-2 bg-accent text-black text-sm font-semibold hover:bg-white transition-colors"
>
<Plus className="w-4 h-4" />
Create First Alert
</button>
</div> </div>
) : ( </header>
<div className="space-y-3">
{alerts.map((alert) => ( {/* DESKTOP HEADER */}
<div <section className="hidden lg:block px-10 pt-10 pb-6">
key={alert.id} <div className="flex items-end justify-between gap-6">
<div className="space-y-3">
<div className="flex items-center gap-2">
<div className="w-1.5 h-1.5 bg-accent animate-pulse" />
<span className="text-[10px] font-mono tracking-[0.2em] text-accent uppercase">Automated Alerts</span>
</div>
<h1 className="font-display text-[2.5rem] leading-[1] tracking-[-0.02em]">
<span className="text-white">Sniper</span>
<span className="text-white/30 ml-3 font-mono text-[2rem]">{alerts.length}/{maxAlerts}</span>
</h1>
</div>
<div className="flex items-center gap-6">
<div className="flex gap-8">
<div className="text-right">
<div className="text-2xl font-bold text-accent font-mono">{activeAlerts}</div>
<div className="text-[9px] font-mono text-white/30 uppercase tracking-wider">Active</div>
</div>
<div className="text-right">
<div className="text-2xl font-bold text-white font-mono">{totalMatches}</div>
<div className="text-[9px] font-mono text-white/30 uppercase tracking-wider">Matches</div>
</div>
</div>
<button
onClick={() => setShowCreateModal(true)}
disabled={!canAddMore}
className={clsx( className={clsx(
"group border transition-all", "flex items-center gap-2 px-4 py-2.5 text-xs font-bold uppercase tracking-wider transition-colors",
alert.is_active canAddMore ? "bg-accent text-black hover:bg-white" : "bg-white/10 text-white/40 cursor-not-allowed"
? "bg-white/[0.02] border-white/[0.08] hover:border-accent/30"
: "bg-white/[0.01] border-white/[0.04]"
)} )}
> >
<div className="p-5"> <Plus className="w-4 h-4" />
New Alert
</button>
</div>
</div>
</section>
{/* ADD BUTTON MOBILE */}
<section className="lg:hidden px-4 py-3 border-b border-white/[0.08]">
<button
onClick={() => setShowCreateModal(true)}
disabled={!canAddMore}
className={clsx(
"w-full flex items-center justify-center gap-2 py-3 text-xs font-bold uppercase tracking-wider transition-colors",
canAddMore ? "bg-accent text-black" : "bg-white/10 text-white/40"
)}
>
<Plus className="w-4 h-4" />
New Alert
</button>
</section>
{/* ALERTS LIST */}
<section className="px-4 lg:px-10 py-4 pb-28 lg:pb-10">
{loading ? (
<div className="flex items-center justify-center py-20">
<Loader2 className="w-6 h-6 text-accent animate-spin" />
</div>
) : alerts.length === 0 ? (
<div className="text-center py-16 border border-dashed border-white/[0.08]">
<Target className="w-8 h-8 text-white/10 mx-auto mb-3" />
<p className="text-white/40 text-sm font-mono mb-2">No alerts yet</p>
<p className="text-white/25 text-xs font-mono">Create your first sniper alert</p>
</div>
) : (
<div className="space-y-px bg-white/[0.04] border border-white/[0.08]">
{alerts.map((alert) => (
<div key={alert.id} className="bg-[#020202] hover:bg-white/[0.02] transition-all p-4">
<div className="flex items-start justify-between gap-4"> <div className="flex items-start justify-between gap-4">
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<div className="flex items-center gap-3 mb-2"> <div className="flex items-center gap-2 mb-2 flex-wrap">
<h3 className="text-base font-medium text-white truncate">{alert.name}</h3> <h3 className="text-sm font-bold text-white font-mono">{alert.name}</h3>
{alert.is_active ? ( {alert.is_active ? (
<span className="px-2 py-0.5 text-[9px] font-mono bg-accent/10 text-accent border border-accent/20 flex items-center gap-1"> <span className="px-1.5 py-0.5 text-[9px] font-mono bg-accent/10 text-accent border border-accent/20 flex items-center gap-1">
<div className="w-1.5 h-1.5 bg-accent animate-pulse" /> <div className="w-1 h-1 bg-accent animate-pulse" />
Active Active
</span> </span>
) : ( ) : (
<span className="px-2 py-0.5 text-[9px] font-mono bg-white/5 text-white/40 border border-white/10"> <span className="px-1.5 py-0.5 text-[9px] font-mono bg-white/5 text-white/40 border border-white/10">Paused</span>
Paused
</span>
)} )}
{isTycoon && alert.notify_sms && ( {isTycoon && alert.notify_sms && (
<span className="px-2 py-0.5 text-[9px] font-mono bg-amber-400/10 text-amber-400 border border-amber-400/20 flex items-center gap-1"> <span className="px-1.5 py-0.5 text-[9px] font-mono bg-amber-400/10 text-amber-400 border border-amber-400/20 flex items-center gap-1">
<Crown className="w-3 h-3" /> <Crown className="w-3 h-3" />SMS
SMS
</span> </span>
)} )}
</div> </div>
{alert.description && (
<p className="text-sm text-white/40 mb-3">{alert.description}</p>
)}
<div className="flex flex-wrap gap-1.5 mb-3"> <div className="flex flex-wrap gap-1 mb-2">
{alert.tlds && ( {alert.tlds && <span className="px-1.5 py-0.5 text-[9px] font-mono bg-blue-400/10 text-blue-400 border border-blue-400/20">{alert.tlds}</span>}
<span className="px-2 py-0.5 text-[10px] font-mono bg-blue-400/10 text-blue-400 border border-blue-400/20"> {alert.keywords && <span className="px-1.5 py-0.5 text-[9px] font-mono bg-accent/10 text-accent border border-accent/20">+{alert.keywords}</span>}
{alert.tlds}
</span>
)}
{alert.keywords && (
<span className="px-2 py-0.5 text-[10px] font-mono bg-accent/10 text-accent border border-accent/20">
+{alert.keywords}
</span>
)}
{alert.exclude_keywords && (
<span className="px-2 py-0.5 text-[10px] font-mono bg-rose-400/10 text-rose-400 border border-rose-400/20">
-{alert.exclude_keywords}
</span>
)}
{(alert.min_length || alert.max_length) && (
<span className="px-2 py-0.5 text-[10px] font-mono bg-white/5 text-white/50 border border-white/10 flex items-center gap-1">
<Hash className="w-3 h-3" />
{alert.min_length || 1}-{alert.max_length || 63}
</span>
)}
{(alert.min_price || alert.max_price) && ( {(alert.min_price || alert.max_price) && (
<span className="px-2 py-0.5 text-[10px] font-mono bg-white/5 text-white/50 border border-white/10 flex items-center gap-1"> <span className="px-1.5 py-0.5 text-[9px] font-mono bg-white/5 text-white/50 border border-white/10 flex items-center gap-0.5">
<DollarSign className="w-3 h-3" /> <DollarSign className="w-3 h-3" />
{alert.min_price ? `$${alert.min_price}` : ''}{alert.max_price ? ` - $${alert.max_price}` : '+'} {alert.min_price || 0}-{alert.max_price || ''}
</span>
)}
{alert.no_numbers && (
<span className="px-2 py-0.5 text-[10px] font-mono bg-white/5 text-white/50 border border-white/10">
No digits
</span>
)}
{alert.no_hyphens && (
<span className="px-2 py-0.5 text-[10px] font-mono bg-white/5 text-white/50 border border-white/10">
No hyphens
</span> </span>
)} )}
</div> </div>
<div className="flex items-center gap-4 text-[10px] text-white/30 font-mono"> <div className="flex items-center gap-3 text-[10px] text-white/30 font-mono">
<span className="flex items-center gap-1"> <span className="flex items-center gap-1"><Zap className="w-3 h-3" />{alert.matches_count}</span>
<Zap className="w-3 h-3" /> <span className="flex items-center gap-1"><Bell className="w-3 h-3" />{alert.notifications_sent}</span>
{alert.matches_count} matches
</span>
<span className="flex items-center gap-1">
<Bell className="w-3 h-3" />
{alert.notifications_sent} sent
</span>
{alert.last_matched_at && (
<span className="flex items-center gap-1">
<Clock className="w-3 h-3" />
{new Date(alert.last_matched_at).toLocaleDateString()}
</span>
)}
</div> </div>
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-1 shrink-0">
<button <button
onClick={() => handleToggle(alert.id, alert.is_active)} onClick={() => handleToggle(alert.id, alert.is_active)}
disabled={togglingId === alert.id} disabled={togglingId === alert.id}
className={clsx( className={clsx(
"w-8 h-8 flex items-center justify-center border transition-colors", "w-8 h-8 flex items-center justify-center border transition-colors",
alert.is_active alert.is_active ? "bg-accent/10 border-accent/20 text-accent" : "bg-white/5 border-white/10 text-white/40"
? "bg-accent/10 border-accent/20 text-accent hover:bg-accent/20"
: "bg-white/5 border-white/10 text-white/40 hover:bg-white/10"
)} )}
> >
{togglingId === alert.id ? ( {togglingId === alert.id ? <Loader2 className="w-4 h-4 animate-spin" /> : alert.is_active ? <Power className="w-4 h-4" /> : <PowerOff className="w-4 h-4" />}
<Loader2 className="w-4 h-4 animate-spin" />
) : alert.is_active ? (
<Power className="w-4 h-4" />
) : (
<PowerOff className="w-4 h-4" />
)}
</button> </button>
<button onClick={() => setEditingAlert(alert)} className="w-8 h-8 flex items-center justify-center border bg-white/5 border-white/10 text-white/40 hover:text-white">
<button
onClick={() => setEditingAlert(alert)}
className="w-8 h-8 flex items-center justify-center border bg-white/5 border-white/10 text-white/40 hover:text-white hover:bg-white/10 transition-colors"
>
<Edit2 className="w-4 h-4" /> <Edit2 className="w-4 h-4" />
</button> </button>
<button <button
onClick={() => handleDelete(alert.id, alert.name)} onClick={() => handleDelete(alert.id, alert.name)}
disabled={deletingId === alert.id} disabled={deletingId === alert.id}
className="w-8 h-8 flex items-center justify-center border bg-white/5 border-white/10 text-white/40 hover:text-rose-400 hover:border-rose-400/30 transition-colors" className="w-8 h-8 flex items-center justify-center border bg-white/5 border-white/10 text-white/40 hover:text-rose-400"
> >
{deletingId === alert.id ? ( {deletingId === alert.id ? <Loader2 className="w-4 h-4 animate-spin" /> : <Trash2 className="w-4 h-4" />}
<Loader2 className="w-4 h-4 animate-spin" />
) : (
<Trash2 className="w-4 h-4" />
)}
</button> </button>
</div> </div>
</div> </div>
</div> </div>
</div> ))}
))} </div>
</div> )}
)}
</section>
{/* ═══════════════════════════════════════════════════════════════════════ */} {!canAddMore && (
{/* UPGRADE CTA */} <div className="mt-6 p-6 border border-white/[0.08] bg-white/[0.02] text-center">
{/* ═══════════════════════════════════════════════════════════════════════ */} <Crown className="w-6 h-6 text-amber-400 mx-auto mb-3" />
{!canAddMore && ( <h3 className="text-sm font-bold text-white mb-1">Alert Limit Reached</h3>
<section className="py-8 border-t border-white/[0.06]"> <p className="text-xs font-mono text-white/40 mb-4">Upgrade for more alerts</p>
<div className="text-center max-w-md mx-auto"> <Link href="/pricing" className="inline-flex items-center gap-2 px-4 py-2 bg-amber-400 text-black text-xs font-bold uppercase tracking-wider">
<Crown className="w-8 h-8 text-amber-400 mx-auto mb-4" /> <Sparkles className="w-3 h-3" />Upgrade
<h3 className="font-display text-xl text-white mb-2">Alert Limit Reached</h3> </Link>
<p className="text-sm text-white/40 mb-4"> </div>
You've created {maxAlerts} alerts. Upgrade for more. )}
</p> </section>
<div className="flex gap-4 justify-center text-xs mb-6">
<div className="px-3 py-2 bg-white/5 border border-white/10"> {/* MOBILE BOTTOM NAV */}
<span className="text-white/40">Trader:</span> <span className="text-white font-medium">10 alerts</span> <nav className="lg:hidden fixed bottom-0 left-0 right-0 z-50 bg-[#020202] border-t border-white/[0.08]" style={{ paddingBottom: 'env(safe-area-inset-bottom)' }}>
<div className="flex items-stretch h-14">
{mobileNavItems.map((item) => (
<Link key={item.href} href={item.href} className={clsx("flex-1 flex flex-col items-center justify-center gap-0.5 relative", item.active ? "text-accent" : "text-white/40")}>
{item.active && <div className="absolute top-0 left-1/2 -translate-x-1/2 w-10 h-0.5 bg-accent" />}
<item.icon className="w-5 h-5" />
<span className="text-[9px] font-mono uppercase tracking-wider">{item.label}</span>
</Link>
))}
<button onClick={() => setMenuOpen(true)} className="flex-1 flex flex-col items-center justify-center gap-0.5 text-white/40">
<Menu className="w-5 h-5" />
<span className="text-[9px] font-mono uppercase tracking-wider">Menu</span>
</button>
</div>
</nav>
{/* DRAWER */}
{menuOpen && (
<div className="lg:hidden fixed inset-0 z-[100]">
<div className="absolute inset-0 bg-black/80" onClick={() => setMenuOpen(false)} />
<div className="absolute top-0 right-0 bottom-0 w-[80%] max-w-[300px] bg-[#0A0A0A] border-l border-white/[0.08] flex flex-col" style={{ paddingTop: 'env(safe-area-inset-top)', paddingBottom: 'env(safe-area-inset-bottom)' }}>
<div className="flex items-center justify-between p-4 border-b border-white/[0.08]">
<div className="flex items-center gap-3">
<Image src="/pounce-puma.png" alt="Pounce" width={28} height={28} className="object-contain" />
<div>
<h2 className="text-sm font-bold text-white">POUNCE</h2>
<p className="text-[9px] text-white/40 font-mono uppercase">Terminal v1.0</p>
</div>
</div>
<button onClick={() => setMenuOpen(false)} className="w-8 h-8 flex items-center justify-center border border-white/10 text-white/60">
<X className="w-4 h-4" />
</button>
</div> </div>
<div className="px-3 py-2 bg-amber-400/10 border border-amber-400/20"> <div className="flex-1 overflow-y-auto py-4">
<span className="text-white/40">Tycoon:</span> <span className="text-amber-400 font-medium">50 + SMS</span> {drawerNavSections.map((section) => (
<div key={section.title} className="mb-4">
<div className="flex items-center gap-2 px-4 mb-2">
<div className="w-1 h-3 bg-accent" />
<span className="text-[9px] font-bold text-white/30 uppercase tracking-[0.2em]">{section.title}</span>
</div>
{section.items.map((item: any) => (
<Link key={item.href} href={item.href} onClick={() => 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.icon className="w-4 h-4 text-white/30" />
<span className="text-sm font-medium flex-1">{item.label}</span>
{item.isNew && <span className="px-1.5 py-0.5 text-[8px] font-bold bg-accent text-black">NEW</span>}
</Link>
))}
</div>
))}
<div className="pt-3 border-t border-white/[0.08] mx-4">
<Link href="/terminal/settings" onClick={() => setMenuOpen(false)} className="flex items-center gap-3 py-2.5 text-white/50">
<Settings className="w-4 h-4" /><span className="text-sm">Settings</span>
</Link>
{user?.is_admin && (
<Link href="/admin" onClick={() => setMenuOpen(false)} className="flex items-center gap-3 py-2.5 text-amber-500/70">
<Shield className="w-4 h-4" /><span className="text-sm">Admin</span>
</Link>
)}
</div>
</div>
<div className="p-4 bg-white/[0.02] border-t border-white/[0.08]">
<div className="flex items-center gap-3 mb-3">
<div className="w-8 h-8 bg-accent/10 border border-accent/20 flex items-center justify-center">
<TierIcon className="w-4 h-4 text-accent" />
</div>
<div className="flex-1 min-w-0">
<p className="text-sm font-bold text-white truncate">{user?.name || user?.email?.split('@')[0] || 'User'}</p>
<p className="text-[9px] font-mono text-white/40 uppercase">{tierName}</p>
</div>
</div>
{tierName === 'Scout' && (
<Link href="/pricing" onClick={() => 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">
<Sparkles className="w-3 h-3" />Upgrade
</Link>
)}
<button onClick={() => { logout(); setMenuOpen(false) }} className="flex items-center justify-center gap-2 w-full py-2 border border-white/10 text-white/40 text-[10px] font-mono uppercase">
<LogOut className="w-3 h-3" />Sign out
</button>
</div> </div>
</div> </div>
<Link
href="/pricing"
className="inline-flex items-center gap-2 px-6 py-3 bg-amber-400 text-black font-semibold hover:bg-amber-300 transition-colors"
>
Upgrade Now
</Link>
</div> </div>
</section> )}
)} </main>
{/* Create/Edit Modal */} {/* MODALS */}
{(showCreateModal || editingAlert) && ( {(showCreateModal || editingAlert) && (
<CreateEditModal <CreateEditModal
alert={editingAlert} alert={editingAlert}
onClose={() => { onClose={() => { setShowCreateModal(false); setEditingAlert(null) }}
setShowCreateModal(false) onSuccess={() => { loadAlerts(); setShowCreateModal(false); setEditingAlert(null) }}
setEditingAlert(null)
}}
onSuccess={() => {
loadAlerts()
setShowCreateModal(false)
setEditingAlert(null)
}}
isTycoon={isTycoon} isTycoon={isTycoon}
/> />
)} )}
</CommandCenterLayout> </div>
) )
} }
@ -459,271 +508,96 @@ function CreateEditModal({ alert, onClose, onSuccess, isTycoon }: {
notify_sms: form.notify_sms && isTycoon, notify_sms: form.notify_sms && isTycoon,
} }
if (isEditing) { if (isEditing && alert) {
await api.request(`/sniper-alerts/${alert.id}`, { await api.request(`/sniper-alerts/${alert.id}`, { method: 'PUT', body: JSON.stringify(payload) })
method: 'PUT',
body: JSON.stringify(payload),
})
} else { } else {
await api.request('/sniper-alerts', { await api.request('/sniper-alerts', { method: 'POST', body: JSON.stringify(payload) })
method: 'POST',
body: JSON.stringify(payload),
})
} }
onSuccess() onSuccess()
} catch (err: any) { } catch (err: any) {
setError(err.message || 'Failed to save alert') setError(err.message || 'Failed to save')
} finally { } finally {
setLoading(false) setLoading(false)
} }
} }
return ( return (
<div <div className="fixed inset-0 z-[110] bg-black/80 flex items-center justify-center p-4 overflow-y-auto" onClick={onClose}>
className="fixed inset-0 z-[100] bg-black/80 backdrop-blur-sm flex items-center justify-center p-4 overflow-y-auto" <div className="w-full max-w-lg bg-[#0A0A0A] border border-white/[0.08] my-8" onClick={(e) => e.stopPropagation()}>
onClick={onClose} <div className="flex items-center justify-between p-4 border-b border-white/[0.08]">
> <div className="flex items-center gap-2">
<div <Target className="w-4 h-4 text-accent" />
className="w-full max-w-2xl bg-[#050505] border border-white/10 my-8" <span className="text-xs font-mono text-accent uppercase tracking-wider">{isEditing ? 'Edit' : 'Create'} Alert</span>
onClick={(e) => e.stopPropagation()}
>
{/* Header */}
<div className="flex items-center justify-between p-6 border-b border-white/[0.08]">
<div className="flex items-center gap-3">
<Target className="w-5 h-5 text-accent" />
<div>
<h3 className="font-medium text-white">{isEditing ? 'Edit Alert' : 'Create Sniper Alert'}</h3>
<p className="text-xs text-white/40">Set criteria for domain matching</p>
</div>
</div> </div>
<button onClick={onClose} className="p-2 hover:bg-white/10 text-white/40 hover:text-white transition-colors"> <button onClick={onClose} className="w-8 h-8 flex items-center justify-center border border-white/10 text-white/40">
<X className="w-5 h-5" /> <X className="w-4 h-4" />
</button> </button>
</div> </div>
<form onSubmit={handleSubmit} className="p-6 space-y-6 max-h-[70vh] overflow-y-auto"> <form onSubmit={handleSubmit} className="p-4 space-y-4 max-h-[70vh] overflow-y-auto">
{error && ( {error && (
<div className="p-4 bg-rose-500/10 border border-rose-500/20 flex items-center gap-3 text-rose-400"> <div className="p-3 bg-rose-500/10 border border-rose-500/20 flex items-center gap-2 text-rose-400 text-xs">
<AlertCircle className="w-5 h-5" /> <AlertCircle className="w-4 h-4" />{error}
<p className="text-sm flex-1">{error}</p>
</div> </div>
)} )}
{/* Basic Info */} <div>
<div className="space-y-4"> <label className="block text-[9px] font-mono text-white/40 uppercase mb-1.5">Alert Name *</label>
<h4 className="text-xs font-mono text-white/40 tracking-wide">Basic Info</h4> <input type="text" value={form.name} onChange={(e) => 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" />
<div> </div>
<label className="block text-xs text-white/50 mb-2">Alert Name *</label>
<input
type="text"
value={form.name}
onChange={(e) => 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"
/>
</div>
<div className="grid grid-cols-2 gap-3">
<div> <div>
<label className="block text-xs text-white/50 mb-2">Description</label> <label className="block text-[9px] font-mono text-white/40 uppercase mb-1.5">TLDs</label>
<textarea <input type="text" value={form.tlds} onChange={(e) => setForm({ ...form, tlds: e.target.value })}
value={form.description} 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="com,io,ai" />
onChange={(e) => setForm({ ...form, description: e.target.value })} </div>
placeholder="Optional description" <div>
rows={2} <label className="block text-[9px] font-mono text-white/40 uppercase mb-1.5">Platforms</label>
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 resize-none" <input type="text" value={form.platforms} onChange={(e) => setForm({ ...form, platforms: e.target.value })}
/> className="w-full px-3 py-2.5 bg-white/5 border border-white/10 text-white text-sm font-mono placeholder:text-white/20 outline-none focus:border-accent/50" placeholder="godaddy,sedo" />
</div> </div>
</div> </div>
{/* Filters */} <div>
<div className="space-y-4"> <label className="block text-[9px] font-mono text-white/40 uppercase mb-1.5">Must Contain</label>
<h4 className="text-xs font-mono text-white/40 tracking-wide">Criteria</h4> <input type="text" value={form.keywords} onChange={(e) => setForm({ ...form, keywords: e.target.value })}
className="w-full px-3 py-2.5 bg-white/5 border border-white/10 text-white text-sm font-mono placeholder:text-white/20 outline-none focus:border-accent/50" placeholder="crypto,web3,ai" />
<div className="grid grid-cols-2 gap-4"> </div>
<div>
<label className="block text-xs text-white/50 mb-2">TLDs</label>
<input
type="text"
value={form.tlds}
onChange={(e) => setForm({ ...form, tlds: e.target.value })}
placeholder="com,io,ai"
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 text-sm"
/>
</div>
<div>
<label className="block text-xs text-white/50 mb-2">Platforms</label>
<input
type="text"
value={form.platforms}
onChange={(e) => setForm({ ...form, platforms: e.target.value })}
placeholder="godaddy,sedo"
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 text-sm"
/>
</div>
</div>
<div className="grid grid-cols-2 gap-3">
<div> <div>
<label className="block text-xs text-white/50 mb-2">Must Contain</label> <label className="block text-[9px] font-mono text-white/40 uppercase mb-1.5">Min Price</label>
<input <input type="number" value={form.min_price} onChange={(e) => setForm({ ...form, min_price: e.target.value })} min="0"
type="text" 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="0" />
value={form.keywords}
onChange={(e) => setForm({ ...form, keywords: e.target.value })}
placeholder="crypto,web3,ai"
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 text-sm"
/>
</div> </div>
<div> <div>
<label className="block text-xs text-white/50 mb-2">Must Not Contain</label> <label className="block text-[9px] font-mono text-white/40 uppercase mb-1.5">Max Price</label>
<input <input type="number" value={form.max_price} onChange={(e) => setForm({ ...form, max_price: e.target.value })} min="0"
type="text" 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="10000" />
value={form.exclude_keywords}
onChange={(e) => setForm({ ...form, exclude_keywords: e.target.value })}
placeholder="xxx,adult"
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 text-sm"
/>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-xs text-white/50 mb-2">Min Length</label>
<input
type="number"
value={form.min_length}
onChange={(e) => setForm({ ...form, min_length: e.target.value })}
placeholder="1"
min="1"
max="63"
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"
/>
</div>
<div>
<label className="block text-xs text-white/50 mb-2">Max Length</label>
<input
type="number"
value={form.max_length}
onChange={(e) => setForm({ ...form, max_length: e.target.value })}
placeholder="63"
min="1"
max="63"
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"
/>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-xs text-white/50 mb-2">Min Price (USD)</label>
<input
type="number"
value={form.min_price}
onChange={(e) => setForm({ ...form, min_price: e.target.value })}
placeholder="0"
min="0"
step="0.01"
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"
/>
</div>
<div>
<label className="block text-xs text-white/50 mb-2">Max Price (USD)</label>
<input
type="number"
value={form.max_price}
onChange={(e) => setForm({ ...form, max_price: e.target.value })}
placeholder="10000"
min="0"
step="0.01"
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"
/>
</div>
</div>
<div className="space-y-2">
<label className="flex items-center gap-3 cursor-pointer p-3 border border-white/[0.06] hover:bg-white/[0.02] transition-colors">
<input
type="checkbox"
checked={form.no_numbers}
onChange={(e) => setForm({ ...form, no_numbers: e.target.checked })}
className="w-4 h-4 border-white/20 bg-black text-accent focus:ring-accent focus:ring-offset-0"
/>
<span className="text-sm text-white/60">No numbers in domain</span>
</label>
<label className="flex items-center gap-3 cursor-pointer p-3 border border-white/[0.06] hover:bg-white/[0.02] transition-colors">
<input
type="checkbox"
checked={form.no_hyphens}
onChange={(e) => setForm({ ...form, no_hyphens: e.target.checked })}
className="w-4 h-4 border-white/20 bg-black text-accent focus:ring-accent focus:ring-offset-0"
/>
<span className="text-sm text-white/60">No hyphens in domain</span>
</label>
</div> </div>
</div> </div>
{/* Notifications */} <div className="space-y-2">
<div className="space-y-4"> <label className="flex items-center gap-3 p-2.5 border border-white/[0.06] cursor-pointer hover:bg-white/[0.02]">
<h4 className="text-xs font-mono text-white/40 tracking-wide">Notifications</h4> <input type="checkbox" checked={form.notify_email} onChange={(e) => setForm({ ...form, notify_email: e.target.checked })} className="w-4 h-4" />
<label className="flex items-center gap-3 cursor-pointer p-3 border border-white/[0.06] hover:bg-white/[0.02] transition-colors">
<input
type="checkbox"
checked={form.notify_email}
onChange={(e) => setForm({ ...form, notify_email: e.target.checked })}
className="w-4 h-4 border-white/20 bg-black text-accent focus:ring-accent focus:ring-offset-0"
/>
<Bell className="w-4 h-4 text-accent" /> <Bell className="w-4 h-4 text-accent" />
<span className="text-sm text-white/60 flex-1">Email notifications</span> <span className="text-sm text-white/60">Email notifications</span>
</label> </label>
<label className={clsx("flex items-center gap-3 p-2.5 border cursor-pointer", isTycoon ? "border-amber-400/20 hover:bg-amber-400/[0.02]" : "border-white/[0.06] opacity-50")}>
<label className={clsx( <input type="checkbox" checked={form.notify_sms} onChange={(e) => isTycoon && setForm({ ...form, notify_sms: e.target.checked })} disabled={!isTycoon} className="w-4 h-4" />
"flex items-center gap-3 cursor-pointer p-3 border transition-colors",
isTycoon ? "border-amber-400/20 hover:bg-amber-400/[0.02]" : "border-white/[0.06] opacity-50 cursor-not-allowed"
)}>
<input
type="checkbox"
checked={form.notify_sms}
onChange={(e) => isTycoon && setForm({ ...form, notify_sms: e.target.checked })}
disabled={!isTycoon}
className="w-4 h-4 border-white/20 bg-black text-amber-400 focus:ring-amber-400 focus:ring-offset-0 disabled:opacity-50"
/>
<MessageSquare className="w-4 h-4 text-amber-400" /> <MessageSquare className="w-4 h-4 text-amber-400" />
<span className="text-sm text-white/60 flex-1">SMS notifications</span> <span className="text-sm text-white/60 flex-1">SMS notifications</span>
{!isTycoon && <Crown className="w-4 h-4 text-amber-400" />} {!isTycoon && <Crown className="w-4 h-4 text-amber-400" />}
</label> </label>
</div> </div>
{/* Actions */} <div className="flex gap-3 pt-2">
<div className="flex gap-3 pt-4"> <button type="button" onClick={onClose} className="flex-1 py-2.5 border border-white/10 text-white/60 text-xs font-mono uppercase">Cancel</button>
<button <button type="submit" disabled={loading || !form.name} className="flex-1 flex items-center justify-center gap-2 py-2.5 bg-accent text-black text-xs font-bold uppercase disabled:opacity-50">
type="button" {loading ? <Loader2 className="w-4 h-4 animate-spin" /> : <CheckCircle className="w-4 h-4" />}
onClick={onClose} {isEditing ? 'Update' : 'Create'}
className="flex-1 px-4 py-3 border border-white/10 text-white/60 hover:bg-white/5 hover:text-white transition-all font-medium"
>
Cancel
</button>
<button
type="submit"
disabled={loading || !form.name}
className="flex-1 flex items-center justify-center gap-2 px-4 py-3 bg-accent text-black font-semibold hover:bg-white transition-all disabled:opacity-50"
>
{loading ? (
<>
<Loader2 className="w-5 h-5 animate-spin" />
Saving...
</>
) : (
<>
<CheckCircle className="w-5 h-5" />
{isEditing ? 'Update' : 'Create'}
</>
)}
</button> </button>
</div> </div>
</form> </form>
@ -731,3 +605,4 @@ function CreateEditModal({ alert, onClose, onSuccess, isTycoon }: {
</div> </div>
) )
} }

View File

@ -2,28 +2,17 @@
import { useState, useEffect, useCallback } from 'react' import { useState, useEffect, useCallback } from 'react'
import { import {
TrendingUp, TrendingUp, DollarSign, Zap, Plus, CheckCircle2, Clock, AlertCircle,
DollarSign, MousePointer, Target, Wallet, RefreshCw, ChevronRight, Copy, Check,
Zap, XCircle, Sparkles, Loader2, Eye, Gavel, Menu, Settings, Shield, LogOut,
Plus, Crown, Coins, Tag, X
CheckCircle2,
Clock,
AlertCircle,
MousePointer,
Target,
Wallet,
RefreshCw,
ChevronRight,
Copy,
Check,
XCircle,
Sparkles,
Loader2
} from 'lucide-react' } from 'lucide-react'
import { api, YieldDomain, YieldTransaction } from '@/lib/api' import { api, YieldDomain, YieldTransaction } from '@/lib/api'
import { useStore } from '@/lib/store' import { useStore } from '@/lib/store'
import { CommandCenterLayout } from '@/components/CommandCenterLayout' import { Sidebar } from '@/components/Sidebar'
import clsx from 'clsx' import clsx from 'clsx'
import Link from 'next/link'
import Image from 'next/image'
// ============================================================================ // ============================================================================
// STATUS BADGE // STATUS BADGE
@ -37,295 +26,63 @@ function StatusBadge({ status }: { status: string }) {
paused: { color: 'bg-white/5 text-white/40 border-white/10', icon: AlertCircle }, paused: { color: 'bg-white/5 text-white/40 border-white/10', icon: AlertCircle },
error: { color: 'bg-red-400/10 text-red-400 border-red-400/20', icon: XCircle }, error: { color: 'bg-red-400/10 text-red-400 border-red-400/20', icon: XCircle },
} }
const { color, icon: Icon } = config[status] || config.pending const { color, icon: Icon } = config[status] || config.pending
return ( return (
<span className={`inline-flex items-center gap-1.5 px-2 py-0.5 text-[10px] font-mono border ${color}`}> <span className={`inline-flex items-center gap-1 px-1.5 py-0.5 text-[9px] font-mono border ${color}`}>
<Icon className="w-3 h-3" /> <Icon className="w-3 h-3" />{status}
{status}
</span> </span>
) )
} }
// ============================================================================ // ============================================================================
// ACTIVATE MODAL // ACTIVATE MODAL (simplified)
// ============================================================================ // ============================================================================
function ActivateModal({ function ActivateModal({ isOpen, onClose, onSuccess }: { isOpen: boolean; onClose: () => void; onSuccess: () => void }) {
isOpen,
onClose,
onSuccess
}: {
isOpen: boolean
onClose: () => void
onSuccess: () => void
}) {
const [step, setStep] = useState<'input' | 'analyze' | 'dns' | 'done'>('input')
const [domain, setDomain] = useState('') const [domain, setDomain] = useState('')
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)
const [error, setError] = useState<string | null>(null) const [error, setError] = useState<string | null>(null)
const [analysis, setAnalysis] = useState<any>(null)
const [dnsInstructions, setDnsInstructions] = useState<any>(null)
const [copied, setCopied] = useState<string | null>(null)
const handleAnalyze = async () => { const handleActivate = async () => {
if (!domain.trim()) return if (!domain.trim()) return
setLoading(true) setLoading(true)
setError(null) setError(null)
try { try {
const result = await api.analyzeYieldDomain(domain.trim()) await api.activateYieldDomain(domain.trim(), true)
setAnalysis(result) onSuccess()
setStep('analyze') onClose()
setDomain('')
} catch (err: any) { } catch (err: any) {
setError(err.message || 'Failed to analyze domain') setError(err.message || 'Failed')
} finally { } finally {
setLoading(false) setLoading(false)
} }
} }
const handleActivate = async () => {
setLoading(true)
setError(null)
try {
const result = await api.activateYieldDomain(domain.trim(), true)
setDnsInstructions(result.dns_instructions)
setStep('dns')
} catch (err: any) {
setError(err.message || 'Failed to activate domain')
} finally {
setLoading(false)
}
}
const copyToClipboard = (text: string, key: string) => {
navigator.clipboard.writeText(text)
setCopied(key)
setTimeout(() => setCopied(null), 2000)
}
const handleDone = () => {
onSuccess()
onClose()
setStep('input')
setDomain('')
setAnalysis(null)
setDnsInstructions(null)
}
if (!isOpen) return null if (!isOpen) return null
return ( return (
<div className="fixed inset-0 z-[100] flex items-center justify-center"> <div className="fixed inset-0 z-[110] bg-black/80 flex items-center justify-center p-4" onClick={onClose}>
<div className="absolute inset-0 bg-black/80 backdrop-blur-sm" onClick={onClose} /> <div className="w-full max-w-md bg-[#0A0A0A] border border-white/[0.08]" onClick={(e) => e.stopPropagation()}>
<div className="flex items-center justify-between p-4 border-b border-white/[0.08]">
<div className="relative bg-[#050505] border border-white/10 w-full max-w-lg mx-4"> <div className="flex items-center gap-2">
{/* Header */} <Sparkles className="w-4 h-4 text-accent" />
<div className="p-6 border-b border-white/[0.08]"> <span className="text-xs font-mono text-accent uppercase tracking-wider">Add Domain</span>
<div className="flex items-center gap-3">
<Sparkles className="w-5 h-5 text-accent" />
<div>
<h2 className="font-medium text-white">Activate Domain for Yield</h2>
<p className="text-xs text-white/40">Turn parked domains into passive income</p>
</div>
</div> </div>
<button onClick={onClose} className="w-8 h-8 flex items-center justify-center border border-white/10 text-white/40"><X className="w-4 h-4" /></button>
</div> </div>
<div className="p-4 space-y-4">
{/* Content */} <div>
<div className="p-6"> <label className="block text-[9px] font-mono text-white/40 uppercase mb-1.5">Domain</label>
{step === 'input' && ( <input type="text" value={domain} onChange={(e) => setDomain(e.target.value)} placeholder="example.com"
<div className="space-y-4"> 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" />
<div> </div>
<label className="block text-xs text-white/50 mb-2">Domain Name</label> {error && <div className="p-2 bg-rose-500/10 border border-rose-500/20 text-rose-400 text-xs">{error}</div>}
<input <button onClick={handleActivate} disabled={loading || !domain.trim()}
type="text" className="w-full py-2.5 bg-accent text-black text-xs font-bold uppercase flex items-center justify-center gap-2 disabled:opacity-50">
value={domain} {loading ? <Loader2 className="w-4 h-4 animate-spin" /> : <Plus className="w-4 h-4" />}
onChange={(e) => setDomain(e.target.value)} Activate
placeholder="e.g. zahnarzt-zuerich.ch" </button>
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"
onKeyDown={(e) => e.key === 'Enter' && handleAnalyze()}
/>
</div>
{error && (
<div className="p-3 bg-red-400/10 border border-red-400/20 text-red-400 text-sm">
{error}
</div>
)}
<button
onClick={handleAnalyze}
disabled={loading || !domain.trim()}
className="w-full py-3 px-4 bg-accent text-black font-semibold hover:bg-white disabled:bg-white/10 disabled:text-white/40 transition-colors flex items-center justify-center gap-2"
>
{loading ? (
<>
<Loader2 className="w-4 h-4 animate-spin" />
Analyzing...
</>
) : (
<>
<Zap className="w-4 h-4" />
Analyze Intent
</>
)}
</button>
</div>
)}
{step === 'analyze' && analysis && (
<div className="space-y-5">
<div className="p-4 bg-white/[0.02] border border-white/[0.08]">
<div className="flex items-center justify-between mb-3">
<span className="text-xs text-white/40">Detected Intent</span>
<span className={clsx(
"px-2 py-0.5 text-[10px] font-mono",
analysis.intent.confidence > 0.7 ? 'bg-accent/10 text-accent' :
analysis.intent.confidence > 0.4 ? 'bg-amber-400/10 text-amber-400' :
'bg-white/5 text-white/40'
)}>
{Math.round(analysis.intent.confidence * 100)}% confidence
</span>
</div>
<p className="text-base font-medium text-white capitalize">
{analysis.intent.category.replace('_', ' ')}
{analysis.intent.subcategory && (
<span className="text-white/40"> / {analysis.intent.subcategory.replace('_', ' ')}</span>
)}
</p>
{analysis.intent.keywords_matched.length > 0 && (
<div className="mt-2 flex flex-wrap gap-1">
{analysis.intent.keywords_matched.map((kw: string, i: number) => (
<span key={i} className="px-2 py-0.5 bg-white/5 text-[10px] font-mono text-white/60">
{kw.split('~')[0]}
</span>
))}
</div>
)}
</div>
<div className="p-4 bg-accent/5 border border-accent/20">
<span className="text-xs text-white/40">Estimated Monthly Revenue</span>
<div className="flex items-baseline gap-2 mt-1">
<span className="text-2xl font-display text-accent">
{analysis.value.currency} {analysis.value.estimated_monthly_min}
</span>
<span className="text-white/40">-</span>
<span className="text-2xl font-display text-accent">
{analysis.value.estimated_monthly_max}
</span>
</div>
</div>
<div className="flex items-center justify-between p-3 bg-white/[0.02] border border-white/[0.06]">
<span className="text-xs text-white/40">Monetization Potential</span>
<span className={clsx(
"font-medium text-sm",
analysis.monetization_potential === 'high' ? 'text-accent' :
analysis.monetization_potential === 'medium' ? 'text-amber-400' :
'text-white/40'
)}>
{analysis.monetization_potential.toUpperCase()}
</span>
</div>
{error && (
<div className="p-3 bg-red-400/10 border border-red-400/20 text-red-400 text-sm">
{error}
</div>
)}
<div className="flex gap-3">
<button
onClick={() => setStep('input')}
className="flex-1 py-3 px-4 bg-white/5 border border-white/10 text-white/60 hover:bg-white/10 hover:text-white transition-colors"
>
Back
</button>
<button
onClick={handleActivate}
disabled={loading}
className="flex-1 py-3 px-4 bg-accent text-black font-semibold hover:bg-white disabled:bg-white/10 disabled:text-white/40 transition-colors flex items-center justify-center gap-2"
>
{loading ? (
<>
<Loader2 className="w-4 h-4 animate-spin" />
Activating...
</>
) : (
<>
<CheckCircle2 className="w-4 h-4" />
Activate
</>
)}
</button>
</div>
</div>
)}
{step === 'dns' && dnsInstructions && (
<div className="space-y-5">
<div className="text-center mb-4">
<div className="w-12 h-12 bg-accent/10 flex items-center justify-center mx-auto mb-3">
<CheckCircle2 className="w-6 h-6 text-accent" />
</div>
<h3 className="font-medium text-white">Domain Registered!</h3>
<p className="text-xs text-white/40 mt-1">Complete DNS setup to start earning</p>
</div>
<div className="p-4 bg-white/[0.02] border border-white/[0.08]">
<h4 className="text-xs font-medium text-white mb-3">Option 1: Nameservers</h4>
<div className="space-y-2">
{dnsInstructions.nameservers.map((ns: string, i: number) => (
<div key={i} className="flex items-center justify-between p-2 bg-black/30">
<code className="text-sm text-accent font-mono">{ns}</code>
<button
onClick={() => copyToClipboard(ns, `ns-${i}`)}
className="p-1 hover:bg-white/10"
>
{copied === `ns-${i}` ? (
<Check className="w-4 h-4 text-accent" />
) : (
<Copy className="w-4 h-4 text-white/40" />
)}
</button>
</div>
))}
</div>
</div>
<div className="p-4 bg-white/[0.02] border border-white/[0.08]">
<h4 className="text-xs font-medium text-white mb-3">Option 2: CNAME Record</h4>
<div className="flex items-center justify-between p-2 bg-black/30">
<div className="text-sm">
<span className="text-white/40">Host: </span>
<code className="text-white font-mono">{dnsInstructions.cname_host}</code>
<span className="text-white/40 mx-2"></span>
<code className="text-accent font-mono">{dnsInstructions.cname_target}</code>
</div>
<button
onClick={() => copyToClipboard(dnsInstructions.cname_target, 'cname')}
className="p-1 hover:bg-white/10"
>
{copied === 'cname' ? (
<Check className="w-4 h-4 text-accent" />
) : (
<Copy className="w-4 h-4 text-white/40" />
)}
</button>
</div>
</div>
<button
onClick={handleDone}
className="w-full py-3 px-4 bg-accent text-black font-semibold hover:bg-white transition-colors"
>
Done
</button>
</div>
)}
</div> </div>
</div> </div>
</div> </div>
@ -337,271 +94,251 @@ function ActivateModal({
// ============================================================================ // ============================================================================
export default function YieldPage() { export default function YieldPage() {
const { subscription } = useStore() const { subscription, user, logout, checkAuth } = useStore()
const [loading, setLoading] = useState(true) const [loading, setLoading] = useState(true)
const [dashboard, setDashboard] = useState<any>(null) const [dashboard, setDashboard] = useState<any>(null)
const [showActivateModal, setShowActivateModal] = useState(false) const [showActivateModal, setShowActivateModal] = useState(false)
const [refreshing, setRefreshing] = useState(false) const [refreshing, setRefreshing] = useState(false)
const [menuOpen, setMenuOpen] = useState(false)
useEffect(() => { checkAuth() }, [checkAuth])
const fetchDashboard = useCallback(async () => { const fetchDashboard = useCallback(async () => {
try { try {
const data = await api.getYieldDashboard() const data = await api.getYieldDashboard()
setDashboard(data) setDashboard(data)
} catch (err) { } catch (err) { console.error(err) }
console.error('Failed to load yield dashboard:', err) finally { setLoading(false); setRefreshing(false) }
} finally {
setLoading(false)
setRefreshing(false)
}
}, []) }, [])
useEffect(() => { useEffect(() => { fetchDashboard() }, [fetchDashboard])
fetchDashboard()
}, [fetchDashboard])
const handleRefresh = () => {
setRefreshing(true)
fetchDashboard()
}
const stats = dashboard?.stats const stats = dashboard?.stats
return ( const mobileNavItems = [
<CommandCenterLayout minimal> { href: '/terminal/radar', label: 'Radar', icon: Target, active: false },
{/* ═══════════════════════════════════════════════════════════════════════ */} { href: '/terminal/market', label: 'Market', icon: Gavel, active: false },
{/* HEADER */} { href: '/terminal/watchlist', label: 'Watch', icon: Eye, active: false },
{/* ═══════════════════════════════════════════════════════════════════════ */} { href: '/terminal/intel', label: 'Intel', icon: TrendingUp, active: false },
<section className="pt-6 lg:pt-8 pb-6"> ]
<div className="flex flex-col lg:flex-row lg:items-end lg:justify-between gap-6">
<div className="space-y-3"> const tierName = subscription?.tier_name || subscription?.tier || 'Scout'
<div className="inline-flex items-center gap-2"> const TierIcon = tierName === 'Tycoon' ? Crown : tierName === 'Trader' ? TrendingUp : Zap
<TrendingUp className="w-4 h-4 text-accent" />
<span className="text-[10px] font-mono tracking-wide text-accent">Passive Income</span> const drawerNavSections = [
</div> { title: 'Discover', items: [
{ href: '/terminal/radar', label: 'Radar', icon: Target },
<h1 className="font-display text-[2rem] lg:text-[2.5rem] leading-[1] tracking-[-0.02em]"> { href: '/terminal/market', label: 'Market', icon: Gavel },
<span className="text-white">Yield</span> { href: '/terminal/intel', label: 'Intel', icon: TrendingUp },
</h1> ]},
{ title: 'Manage', items: [
<p className="text-white/40 text-sm max-w-md"> { href: '/terminal/watchlist', label: 'Watchlist', icon: Eye },
Turn parked domains into passive income with intent-based monetization { href: '/terminal/sniper', label: 'Sniper', icon: Target },
</p> ]},
</div> { title: 'Monetize', items: [
{ href: '/terminal/yield', label: 'Yield', icon: Coins, active: true },
<div className="flex items-center gap-4"> { href: '/terminal/listing', label: 'For Sale', icon: Tag },
{stats && ( ]}
<div className="flex gap-6"> ]
<div className="text-right">
<div className="text-xl font-display text-accent">{stats.currency} {stats.monthly_revenue.toLocaleString()}</div>
<div className="text-[9px] tracking-wide text-white/30 font-mono">Monthly</div>
</div>
<div className="text-right">
<div className="text-xl font-display text-white">{stats.active_domains}</div>
<div className="text-[9px] tracking-wide text-white/30 font-mono">Active</div>
</div>
<div className="text-right">
<div className="text-xl font-display text-white">{stats.monthly_clicks.toLocaleString()}</div>
<div className="text-[9px] tracking-wide text-white/30 font-mono">Clicks</div>
</div>
</div>
)}
<div className="flex items-center gap-2">
<button
onClick={handleRefresh}
disabled={refreshing}
className="w-9 h-9 flex items-center justify-center border border-white/10 text-white/40 hover:text-white hover:bg-white/5 transition-colors"
>
<RefreshCw className={clsx("w-4 h-4", refreshing && "animate-spin")} />
</button>
<button
onClick={() => setShowActivateModal(true)}
className="flex items-center gap-2 px-4 py-2 bg-accent text-black text-sm font-semibold hover:bg-white transition-colors"
>
<Plus className="w-4 h-4" />
Add Domain
</button>
</div>
</div>
</div>
</section>
{/* ═══════════════════════════════════════════════════════════════════════ */} return (
{/* STATS */} <div className="min-h-screen bg-[#020202]">
{/* ═══════════════════════════════════════════════════════════════════════ */} <div className="hidden lg:block"><Sidebar /></div>
{stats && (
<section className="pb-6 border-b border-white/[0.08]"> <main className="lg:pl-[240px]">
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4"> {/* MOBILE HEADER */}
<div className="p-4 bg-accent/5 border border-accent/20"> <header className="lg:hidden sticky top-0 z-40 bg-[#020202]/95 backdrop-blur-md border-b border-white/[0.08]" style={{ paddingTop: 'env(safe-area-inset-top)' }}>
<DollarSign className="w-5 h-5 text-accent mb-2" /> <div className="px-4 py-3">
<div className="text-xl font-display text-white">{stats.currency} {stats.monthly_revenue.toLocaleString()}</div> <div className="flex items-center justify-between mb-3">
<div className="text-[10px] text-white/40 font-mono">Monthly Revenue</div> <div className="flex items-center gap-2">
<div className="text-[10px] text-white/30 mt-1">Lifetime: {stats.currency} {stats.lifetime_revenue.toLocaleString()}</div> <div className="w-1.5 h-1.5 bg-accent animate-pulse" />
<span className="text-[10px] font-mono tracking-[0.2em] text-accent uppercase">Yield</span>
</div>
<span className="text-[10px] font-mono text-white/40">{stats?.active_domains || 0} active</span>
</div> </div>
<div className="grid grid-cols-3 gap-2">
<div className="p-4 bg-white/[0.02] border border-white/[0.08]"> <div className="bg-accent/[0.05] border border-accent/20 p-2">
<Zap className="w-5 h-5 text-blue-400 mb-2" /> <div className="text-lg font-bold text-accent tabular-nums font-mono">${stats?.monthly_revenue || 0}</div>
<div className="text-xl font-display text-white">{stats.active_domains}</div> <div className="text-[9px] font-mono text-accent/60 uppercase tracking-wider">Monthly</div>
<div className="text-[10px] text-white/40 font-mono">Active Domains</div> </div>
<div className="text-[10px] text-white/30 mt-1">{stats.pending_domains} pending</div> <div className="bg-white/[0.02] border border-white/[0.08] p-2">
<div className="text-lg font-bold text-white tabular-nums">{stats?.monthly_clicks || 0}</div>
<div className="text-[9px] font-mono text-white/30 uppercase tracking-wider">Clicks</div>
</div>
<div className="bg-white/[0.02] border border-white/[0.08] p-2">
<div className="text-lg font-bold text-white tabular-nums">${stats?.pending_payout || 0}</div>
<div className="text-[9px] font-mono text-white/30 uppercase tracking-wider">Pending</div>
</div>
</div> </div>
</div>
<div className="p-4 bg-white/[0.02] border border-white/[0.08]"> </header>
<MousePointer className="w-5 h-5 text-amber-400 mb-2" />
<div className="text-xl font-display text-white">{stats.monthly_clicks.toLocaleString()}</div> {/* DESKTOP HEADER */}
<div className="text-[10px] text-white/40 font-mono">Monthly Clicks</div> <section className="hidden lg:block px-10 pt-10 pb-6">
<div className="text-[10px] text-white/30 mt-1">{stats.monthly_conversions} conversions</div> <div className="flex items-end justify-between gap-6">
<div className="space-y-3">
<div className="flex items-center gap-2">
<div className="w-1.5 h-1.5 bg-accent animate-pulse" />
<span className="text-[10px] font-mono tracking-[0.2em] text-accent uppercase">Passive Income</span>
</div>
<h1 className="font-display text-[2.5rem] leading-[1] tracking-[-0.02em] text-white">Yield</h1>
</div> </div>
<div className="flex items-center gap-6">
<div className="p-4 bg-white/[0.02] border border-white/[0.08]"> {stats && (
<Wallet className="w-5 h-5 text-purple-400 mb-2" /> <div className="flex gap-8">
<div className="text-xl font-display text-white">{stats.currency} {stats.pending_payout.toLocaleString()}</div> <div className="text-right">
<div className="text-[10px] text-white/40 font-mono">Pending Payout</div> <div className="text-2xl font-bold text-accent font-mono">${stats.monthly_revenue}</div>
{stats.next_payout_date && ( <div className="text-[9px] font-mono text-white/30 uppercase">Monthly</div>
<div className="text-[10px] text-white/30 mt-1">Next: {new Date(stats.next_payout_date).toLocaleDateString()}</div> </div>
<div className="text-right">
<div className="text-2xl font-bold text-white font-mono">{stats.active_domains}</div>
<div className="text-[9px] font-mono text-white/30 uppercase">Active</div>
</div>
</div>
)} )}
<div className="flex gap-2">
<button onClick={() => { setRefreshing(true); fetchDashboard() }} disabled={refreshing}
className="w-9 h-9 flex items-center justify-center border border-white/10 text-white/40 hover:text-white">
<RefreshCw className={clsx("w-4 h-4", refreshing && "animate-spin")} />
</button>
<button onClick={() => setShowActivateModal(true)}
className="flex items-center gap-2 px-4 py-2.5 bg-accent text-black text-xs font-bold uppercase tracking-wider hover:bg-white">
<Plus className="w-4 h-4" />Add Domain
</button>
</div>
</div> </div>
</div> </div>
</section> </section>
)}
{/* ═══════════════════════════════════════════════════════════════════════ */} {/* ADD BUTTON MOBILE */}
{/* DOMAINS TABLE */} <section className="lg:hidden px-4 py-3 border-b border-white/[0.08]">
{/* ═══════════════════════════════════════════════════════════════════════ */} <button onClick={() => setShowActivateModal(true)}
<section className="py-6"> className="w-full flex items-center justify-center gap-2 py-3 bg-accent text-black text-xs font-bold uppercase tracking-wider">
{loading ? ( <Plus className="w-4 h-4" />Add Domain
<div className="flex items-center justify-center py-20"> </button>
<Loader2 className="w-6 h-6 text-accent animate-spin" /> </section>
</div>
) : dashboard?.domains?.length === 0 ? ( {/* CONTENT */}
<div className="text-center py-16"> <section className="px-4 lg:px-10 py-4 pb-28 lg:pb-10">
<div className="w-14 h-14 mx-auto bg-white/[0.02] border border-white/10 flex items-center justify-center mb-4"> {loading ? (
<TrendingUp className="w-6 h-6 text-white/20" /> <div className="flex items-center justify-center py-20">
<Loader2 className="w-6 h-6 text-accent animate-spin" />
</div> </div>
<p className="text-white/40 text-sm mb-2">No yield domains yet</p> ) : !dashboard?.domains?.length ? (
<p className="text-white/25 text-xs mb-6 max-w-sm mx-auto"> <div className="text-center py-16 border border-dashed border-white/[0.08]">
Activate your first domain to start generating passive income <TrendingUp className="w-8 h-8 text-white/10 mx-auto mb-3" />
</p> <p className="text-white/40 text-sm font-mono mb-2">No yield domains yet</p>
<button <p className="text-white/25 text-xs font-mono">Activate domains to earn passive income</p>
onClick={() => setShowActivateModal(true)} </div>
className="inline-flex items-center gap-2 px-4 py-2 bg-accent text-black text-sm font-semibold hover:bg-white transition-colors" ) : (
> <div className="space-y-px bg-white/[0.04] border border-white/[0.08]">
<Plus className="w-4 h-4" /> {/* Header */}
Add First Domain <div className="hidden lg:grid grid-cols-[1fr_80px_120px_80px_80px_80px] gap-4 px-3 py-2 text-[10px] font-mono text-white/40 uppercase tracking-wider border-b border-white/[0.08]">
</button> <div>Domain</div>
</div> <div className="text-center">Status</div>
) : ( <div>Intent</div>
<div className="overflow-x-auto"> <div className="text-right">Clicks</div>
<table className="w-full min-w-[800px]"> <div className="text-right">Conv.</div>
<thead> <div className="text-right">Revenue</div>
<tr className="text-xs text-white/40 border-b border-white/[0.06]"> </div>
<th className="text-left py-3 px-4 font-medium">Domain</th>
<th className="text-left py-3 px-4 font-medium">Status</th> {dashboard.domains.map((domain: YieldDomain) => (
<th className="text-left py-3 px-4 font-medium">Intent</th> <div key={domain.id} className="bg-[#020202] hover:bg-white/[0.02] transition-all">
<th className="text-right py-3 px-4 font-medium">Clicks</th> {/* Mobile */}
<th className="text-right py-3 px-4 font-medium">Conversions</th> <div className="lg:hidden p-3">
<th className="text-right py-3 px-4 font-medium">Revenue</th> <div className="flex items-center justify-between mb-2">
<th className="text-right py-3 px-4 font-medium"></th>
</tr>
</thead>
<tbody>
{dashboard?.domains?.map((domain: YieldDomain) => (
<tr key={domain.id} className="group border-b border-white/[0.04] hover:bg-white/[0.02]">
<td className="py-3 px-4">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="w-8 h-8 bg-white/5 border border-white/10 flex items-center justify-center text-accent text-xs font-bold"> <div className="w-8 h-8 bg-white/[0.02] border border-white/[0.06] flex items-center justify-center text-accent text-xs font-bold font-mono">
{domain.domain.charAt(0).toUpperCase()} {domain.domain.charAt(0).toUpperCase()}
</div> </div>
<span className="font-medium text-white">{domain.domain}</span> <span className="text-sm font-bold text-white font-mono">{domain.domain}</span>
</div> </div>
</td>
<td className="py-3 px-4">
<StatusBadge status={domain.status} /> <StatusBadge status={domain.status} />
</td> </div>
<td className="py-3 px-4"> <div className="flex justify-between text-[10px] font-mono text-white/40">
<span className="text-sm text-white/60 capitalize"> <span>{domain.total_clicks} clicks</span>
{domain.detected_intent?.replace('_', ' ') || '-'} <span className="text-accent font-bold">${domain.total_revenue}</span>
</span> </div>
{domain.intent_confidence > 0 && (
<span className="text-[10px] text-white/30 ml-2 font-mono">
({Math.round(domain.intent_confidence * 100)}%)
</span>
)}
</td>
<td className="py-3 px-4 text-right text-white/60 font-mono">
{domain.total_clicks.toLocaleString()}
</td>
<td className="py-3 px-4 text-right text-white/60 font-mono">
{domain.total_conversions.toLocaleString()}
</td>
<td className="py-3 px-4 text-right">
<span className="font-medium text-accent font-mono">
{domain.currency} {domain.total_revenue.toLocaleString()}
</span>
</td>
<td className="py-3 px-4 text-right">
<button className="p-1.5 hover:bg-white/10 transition-colors opacity-0 group-hover:opacity-100">
<ChevronRight className="w-4 h-4 text-white/40" />
</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</section>
{/* ═══════════════════════════════════════════════════════════════════════ */}
{/* RECENT TRANSACTIONS */}
{/* ═══════════════════════════════════════════════════════════════════════ */}
{dashboard?.recent_transactions?.length > 0 && (
<section className="py-6 border-t border-white/[0.06]">
<h2 className="text-xs font-mono text-white/40 tracking-wide mb-4">Recent Activity</h2>
<div className="space-y-px">
{dashboard.recent_transactions.slice(0, 5).map((tx: YieldTransaction) => (
<div key={tx.id} className="flex items-center justify-between p-4 bg-white/[0.01] border border-white/[0.04] hover:bg-white/[0.02] transition-colors">
<div className="flex items-center gap-3">
<div className={clsx(
"w-8 h-8 flex items-center justify-center",
tx.event_type === 'sale' ? 'bg-accent/10' :
tx.event_type === 'lead' ? 'bg-blue-400/10' :
'bg-white/5'
)}>
{tx.event_type === 'sale' ? (
<DollarSign className="w-4 h-4 text-accent" />
) : tx.event_type === 'lead' ? (
<Target className="w-4 h-4 text-blue-400" />
) : (
<MousePointer className="w-4 h-4 text-white/40" />
)}
</div> </div>
<div>
<p className="text-sm font-medium text-white capitalize">{tx.event_type}</p> {/* Desktop */}
<p className="text-[10px] text-white/30 font-mono">{tx.partner_slug}</p> <div className="hidden lg:grid grid-cols-[1fr_80px_120px_80px_80px_80px] gap-4 items-center px-3 py-3">
<div className="flex items-center gap-3">
<div className="w-8 h-8 bg-white/[0.02] border border-white/[0.06] flex items-center justify-center text-accent text-xs font-bold font-mono">
{domain.domain.charAt(0).toUpperCase()}
</div>
<span className="text-sm font-bold text-white font-mono">{domain.domain}</span>
</div>
<div className="flex justify-center"><StatusBadge status={domain.status} /></div>
<span className="text-xs text-white/60 capitalize font-mono">{domain.detected_intent?.replace('_', ' ') || '—'}</span>
<div className="text-right text-xs font-mono text-white/60">{domain.total_clicks}</div>
<div className="text-right text-xs font-mono text-white/60">{domain.total_conversions}</div>
<div className="text-right text-sm font-bold font-mono text-accent">${domain.total_revenue}</div>
</div> </div>
</div> </div>
<div className="text-right"> ))}
<p className="text-sm font-medium text-accent font-mono"> </div>
+{tx.currency} {tx.net_amount.toFixed(2)} )}
</p> </section>
<p className="text-[10px] text-white/30 font-mono">
{new Date(tx.created_at).toLocaleDateString()} {/* MOBILE BOTTOM NAV */}
</p> <nav className="lg:hidden fixed bottom-0 left-0 right-0 z-50 bg-[#020202] border-t border-white/[0.08]" style={{ paddingBottom: 'env(safe-area-inset-bottom)' }}>
<div className="flex items-stretch h-14">
{mobileNavItems.map((item) => (
<Link key={item.href} href={item.href} className={clsx("flex-1 flex flex-col items-center justify-center gap-0.5 relative", item.active ? "text-accent" : "text-white/40")}>
{item.active && <div className="absolute top-0 left-1/2 -translate-x-1/2 w-10 h-0.5 bg-accent" />}
<item.icon className="w-5 h-5" />
<span className="text-[9px] font-mono uppercase tracking-wider">{item.label}</span>
</Link>
))}
<button onClick={() => setMenuOpen(true)} className="flex-1 flex flex-col items-center justify-center gap-0.5 text-white/40">
<Menu className="w-5 h-5" /><span className="text-[9px] font-mono uppercase tracking-wider">Menu</span>
</button>
</div>
</nav>
{/* DRAWER */}
{menuOpen && (
<div className="lg:hidden fixed inset-0 z-[100]">
<div className="absolute inset-0 bg-black/80" onClick={() => setMenuOpen(false)} />
<div className="absolute top-0 right-0 bottom-0 w-[80%] max-w-[300px] bg-[#0A0A0A] border-l border-white/[0.08] flex flex-col" style={{ paddingTop: 'env(safe-area-inset-top)', paddingBottom: 'env(safe-area-inset-bottom)' }}>
<div className="flex items-center justify-between p-4 border-b border-white/[0.08]">
<div className="flex items-center gap-3">
<Image src="/pounce-puma.png" alt="Pounce" width={28} height={28} className="object-contain" />
<div><h2 className="text-sm font-bold text-white">POUNCE</h2><p className="text-[9px] text-white/40 font-mono uppercase">Terminal v1.0</p></div>
</div>
<button onClick={() => setMenuOpen(false)} className="w-8 h-8 flex items-center justify-center border border-white/10 text-white/60"><X className="w-4 h-4" /></button>
</div>
<div className="flex-1 overflow-y-auto py-4">
{drawerNavSections.map((section) => (
<div key={section.title} className="mb-4">
<div className="flex items-center gap-2 px-4 mb-2"><div className="w-1 h-3 bg-accent" /><span className="text-[9px] font-bold text-white/30 uppercase tracking-[0.2em]">{section.title}</span></div>
{section.items.map((item: any) => (
<Link key={item.href} href={item.href} onClick={() => 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.icon className="w-4 h-4 text-white/30" /><span className="text-sm font-medium flex-1">{item.label}</span>
</Link>
))}
</div>
))}
<div className="pt-3 border-t border-white/[0.08] mx-4">
<Link href="/terminal/settings" onClick={() => setMenuOpen(false)} className="flex items-center gap-3 py-2.5 text-white/50"><Settings className="w-4 h-4" /><span className="text-sm">Settings</span></Link>
{user?.is_admin && <Link href="/admin" onClick={() => setMenuOpen(false)} className="flex items-center gap-3 py-2.5 text-amber-500/70"><Shield className="w-4 h-4" /><span className="text-sm">Admin</span></Link>}
</div> </div>
</div> </div>
))} <div className="p-4 bg-white/[0.02] border-t border-white/[0.08]">
<div className="flex items-center gap-3 mb-3">
<div className="w-8 h-8 bg-accent/10 border border-accent/20 flex items-center justify-center"><TierIcon className="w-4 h-4 text-accent" /></div>
<div className="flex-1 min-w-0"><p className="text-sm font-bold text-white truncate">{user?.name || user?.email?.split('@')[0] || 'User'}</p><p className="text-[9px] font-mono text-white/40 uppercase">{tierName}</p></div>
</div>
{tierName === 'Scout' && <Link href="/pricing" onClick={() => 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"><Sparkles className="w-3 h-3" />Upgrade</Link>}
<button onClick={() => { logout(); setMenuOpen(false) }} className="flex items-center justify-center gap-2 w-full py-2 border border-white/10 text-white/40 text-[10px] font-mono uppercase"><LogOut className="w-3 h-3" />Sign out</button>
</div>
</div>
</div> </div>
</section> )}
)} </main>
{/* Activate Modal */} <ActivateModal isOpen={showActivateModal} onClose={() => setShowActivateModal(false)} onSuccess={fetchDashboard} />
<ActivateModal </div>
isOpen={showActivateModal}
onClose={() => setShowActivateModal(false)}
onSuccess={fetchDashboard}
/>
</CommandCenterLayout>
) )
} }

View File

@ -100,8 +100,9 @@ export const useStore = create<AppState>((set, get) => ({
register: async (email, password, name) => { register: async (email, password, name) => {
await api.register(email, password, name) await api.register(email, password, name)
// Auto-login after registration // Note: No auto-login after registration
await get().login(email, password) // User should verify email first (verification email is sent)
// They can then log in manually via the login page
}, },
logout: () => { logout: () => {