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
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:
193
deploy.sh
193
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!"
|
||||
# Get commit message
|
||||
if [ -z "$1" ]; then
|
||||
COMMIT_MSG="Deploy: $(date '+%Y-%m-%d %H:%M')"
|
||||
else
|
||||
log_error "env.example not found!"
|
||||
fi
|
||||
COMMIT_MSG="$1"
|
||||
fi
|
||||
|
||||
# Initialize database
|
||||
log_info "Initializing database..."
|
||||
python scripts/init_db.py
|
||||
echo -e "\n${YELLOW}[1/5] Staging changes...${NC}"
|
||||
git add -A
|
||||
|
||||
cd ..
|
||||
log_info "Backend setup complete!"
|
||||
echo ""
|
||||
echo -e "\n${YELLOW}[2/5] Committing: ${COMMIT_MSG}${NC}"
|
||||
git commit -m "$COMMIT_MSG" || echo "Nothing to commit"
|
||||
|
||||
# ============================================
|
||||
# 3. Setup Frontend
|
||||
# ============================================
|
||||
log_info "Setting up Frontend..."
|
||||
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
|
||||
|
||||
# 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
|
||||
|
||||
npm run build 2>&1 | tail -10
|
||||
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
|
||||
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 ""
|
||||
fi
|
||||
# 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}"
|
||||
|
||||
@ -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)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -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,7 +72,7 @@ interface SniperAlert {
|
||||
// ============================================================================
|
||||
|
||||
export default function SniperAlertsPage() {
|
||||
const { subscription } = useStore()
|
||||
const { subscription, user, logout, checkAuth } = useStore()
|
||||
const [alerts, setAlerts] = useState<SniperAlert[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [showCreateModal, setShowCreateModal] = useState(false)
|
||||
@ -70,6 +80,9 @@ export default function SniperAlertsPage() {
|
||||
const [deletingId, setDeletingId] = useState<number | null>(null)
|
||||
const [togglingId, setTogglingId] = useState<number | null>(null)
|
||||
|
||||
// Mobile Menu
|
||||
const [menuOpen, setMenuOpen] = useState(false)
|
||||
|
||||
const tier = subscription?.tier || 'scout'
|
||||
const alertLimits: Record<string, number> = { scout: 2, trader: 10, tycoon: 50 }
|
||||
const maxAlerts = alertLimits[tier] || 2
|
||||
@ -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,42 +142,89 @@ export default function SniperAlertsPage() {
|
||||
}
|
||||
}
|
||||
|
||||
// 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 },
|
||||
]}
|
||||
]
|
||||
|
||||
return (
|
||||
<CommandCenterLayout minimal>
|
||||
{/* ═══════════════════════════════════════════════════════════════════════ */}
|
||||
{/* HEADER */}
|
||||
{/* ═══════════════════════════════════════════════════════════════════════ */}
|
||||
<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="min-h-screen bg-[#020202]">
|
||||
<div className="hidden lg:block"><Sidebar /></div>
|
||||
|
||||
<main className="lg:pl-[240px]">
|
||||
{/* MOBILE HEADER */}
|
||||
<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 className="px-4 py-3">
|
||||
<div className="flex items-center justify-between mb-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">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>
|
||||
</header>
|
||||
|
||||
{/* DESKTOP HEADER */}
|
||||
<section className="hidden lg:block px-10 pt-10 pb-6">
|
||||
<div className="flex items-end justify-between gap-6">
|
||||
<div className="space-y-3">
|
||||
<div className="inline-flex items-center gap-2">
|
||||
<Target className="w-4 h-4 text-accent" />
|
||||
<span className="text-[10px] font-mono tracking-wide text-accent">Automated Alerts</span>
|
||||
<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-[2rem] lg:text-[2.5rem] leading-[1] tracking-[-0.02em]">
|
||||
<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">{alerts.length}/{maxAlerts}</span>
|
||||
<span className="text-white/30 ml-3 font-mono text-[2rem]">{alerts.length}/{maxAlerts}</span>
|
||||
</h1>
|
||||
|
||||
<p className="text-white/40 text-sm max-w-md">
|
||||
Get notified when domains matching your criteria hit the market
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="flex gap-6">
|
||||
<div className="flex items-center gap-6">
|
||||
<div className="flex gap-8">
|
||||
<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 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-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 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>
|
||||
|
||||
@ -168,10 +232,8 @@ export default function SniperAlertsPage() {
|
||||
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"
|
||||
"flex items-center gap-2 px-4 py-2.5 text-xs font-bold uppercase tracking-wider 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" />
|
||||
@ -181,221 +243,208 @@ export default function SniperAlertsPage() {
|
||||
</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="py-6 border-t border-white/[0.08]">
|
||||
<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">
|
||||
<div className="w-14 h-14 mx-auto bg-white/[0.02] border border-white/10 flex items-center justify-center mb-4">
|
||||
<Target className="w-6 h-6 text-white/20" />
|
||||
</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 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-3">
|
||||
<div className="space-y-px bg-white/[0.04] border border-white/[0.08]">
|
||||
{alerts.map((alert) => (
|
||||
<div
|
||||
key={alert.id}
|
||||
className={clsx(
|
||||
"group border transition-all",
|
||||
alert.is_active
|
||||
? "bg-white/[0.02] border-white/[0.08] hover:border-accent/30"
|
||||
: "bg-white/[0.01] border-white/[0.04]"
|
||||
)}
|
||||
>
|
||||
<div className="p-5">
|
||||
<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-1 min-w-0">
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<h3 className="text-base font-medium text-white truncate">{alert.name}</h3>
|
||||
<div className="flex items-center gap-2 mb-2 flex-wrap">
|
||||
<h3 className="text-sm font-bold text-white font-mono">{alert.name}</h3>
|
||||
{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">
|
||||
<div className="w-1.5 h-1.5 bg-accent animate-pulse" />
|
||||
<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 h-1 bg-accent animate-pulse" />
|
||||
Active
|
||||
</span>
|
||||
) : (
|
||||
<span className="px-2 py-0.5 text-[9px] font-mono bg-white/5 text-white/40 border border-white/10">
|
||||
Paused
|
||||
</span>
|
||||
<span className="px-1.5 py-0.5 text-[9px] font-mono bg-white/5 text-white/40 border border-white/10">Paused</span>
|
||||
)}
|
||||
{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">
|
||||
<Crown className="w-3 h-3" />
|
||||
SMS
|
||||
<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" />SMS
|
||||
</span>
|
||||
)}
|
||||
</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">
|
||||
{alert.tlds && (
|
||||
<span className="px-2 py-0.5 text-[10px] font-mono bg-blue-400/10 text-blue-400 border border-blue-400/20">
|
||||
{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>
|
||||
)}
|
||||
<div className="flex flex-wrap gap-1 mb-2">
|
||||
{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>}
|
||||
{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.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" />
|
||||
{alert.min_price ? `$${alert.min_price}` : ''}{alert.max_price ? ` - $${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
|
||||
{alert.min_price || 0}-{alert.max_price || '∞'}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-4 text-[10px] text-white/30 font-mono">
|
||||
<span className="flex items-center gap-1">
|
||||
<Zap className="w-3 h-3" />
|
||||
{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 className="flex items-center gap-3 text-[10px] text-white/30 font-mono">
|
||||
<span className="flex items-center gap-1"><Zap className="w-3 h-3" />{alert.matches_count}</span>
|
||||
<span className="flex items-center gap-1"><Bell className="w-3 h-3" />{alert.notifications_sent}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex items-center gap-1 shrink-0">
|
||||
<button
|
||||
onClick={() => handleToggle(alert.id, alert.is_active)}
|
||||
disabled={togglingId === alert.id}
|
||||
className={clsx(
|
||||
"w-8 h-8 flex items-center justify-center border transition-colors",
|
||||
alert.is_active
|
||||
? "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"
|
||||
alert.is_active ? "bg-accent/10 border-accent/20 text-accent" : "bg-white/5 border-white/10 text-white/40"
|
||||
)}
|
||||
>
|
||||
{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" />
|
||||
)}
|
||||
{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" />}
|
||||
</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 hover:bg-white/10 transition-colors"
|
||||
>
|
||||
<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">
|
||||
<Edit2 className="w-4 h-4" />
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => handleDelete(alert.id, alert.name)}
|
||||
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 ? (
|
||||
<Loader2 className="w-4 h-4 animate-spin" />
|
||||
) : (
|
||||
<Trash2 className="w-4 h-4" />
|
||||
)}
|
||||
{deletingId === alert.id ? <Loader2 className="w-4 h-4 animate-spin" /> : <Trash2 className="w-4 h-4" />}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
|
||||
{/* ═══════════════════════════════════════════════════════════════════════ */}
|
||||
{/* UPGRADE CTA */}
|
||||
{/* ═══════════════════════════════════════════════════════════════════════ */}
|
||||
{!canAddMore && (
|
||||
<section className="py-8 border-t border-white/[0.06]">
|
||||
<div className="text-center max-w-md mx-auto">
|
||||
<Crown className="w-8 h-8 text-amber-400 mx-auto mb-4" />
|
||||
<h3 className="font-display text-xl text-white mb-2">Alert Limit Reached</h3>
|
||||
<p className="text-sm text-white/40 mb-4">
|
||||
You've created {maxAlerts} alerts. Upgrade for more.
|
||||
</p>
|
||||
<div className="flex gap-4 justify-center text-xs mb-6">
|
||||
<div className="px-3 py-2 bg-white/5 border border-white/10">
|
||||
<span className="text-white/40">Trader:</span> <span className="text-white font-medium">10 alerts</span>
|
||||
</div>
|
||||
<div className="px-3 py-2 bg-amber-400/10 border border-amber-400/20">
|
||||
<span className="text-white/40">Tycoon:</span> <span className="text-amber-400 font-medium">50 + SMS</span>
|
||||
</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
|
||||
<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" />
|
||||
<h3 className="text-sm font-bold text-white mb-1">Alert Limit Reached</h3>
|
||||
<p className="text-xs font-mono text-white/40 mb-4">Upgrade for more alerts</p>
|
||||
<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">
|
||||
<Sparkles className="w-3 h-3" />Upgrade
|
||||
</Link>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
</section>
|
||||
|
||||
{/* Create/Edit Modal */}
|
||||
{/* MOBILE BOTTOM NAV */}
|
||||
<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>
|
||||
{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>
|
||||
)}
|
||||
</main>
|
||||
|
||||
{/* MODALS */}
|
||||
{(showCreateModal || editingAlert) && (
|
||||
<CreateEditModal
|
||||
alert={editingAlert}
|
||||
onClose={() => {
|
||||
setShowCreateModal(false)
|
||||
setEditingAlert(null)
|
||||
}}
|
||||
onSuccess={() => {
|
||||
loadAlerts()
|
||||
setShowCreateModal(false)
|
||||
setEditingAlert(null)
|
||||
}}
|
||||
onClose={() => { setShowCreateModal(false); setEditingAlert(null) }}
|
||||
onSuccess={() => { loadAlerts(); setShowCreateModal(false); setEditingAlert(null) }}
|
||||
isTycoon={isTycoon}
|
||||
/>
|
||||
)}
|
||||
</CommandCenterLayout>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@ -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 (
|
||||
<div
|
||||
className="fixed inset-0 z-[100] bg-black/80 backdrop-blur-sm flex items-center justify-center p-4 overflow-y-auto"
|
||||
onClick={onClose}
|
||||
>
|
||||
<div
|
||||
className="w-full max-w-2xl bg-[#050505] border border-white/10 my-8"
|
||||
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 className="fixed inset-0 z-[110] bg-black/80 flex items-center justify-center p-4 overflow-y-auto" onClick={onClose}>
|
||||
<div className="w-full max-w-lg bg-[#0A0A0A] border border-white/[0.08] my-8" onClick={(e) => e.stopPropagation()}>
|
||||
<div className="flex items-center justify-between p-4 border-b border-white/[0.08]">
|
||||
<div className="flex items-center gap-2">
|
||||
<Target className="w-4 h-4 text-accent" />
|
||||
<span className="text-xs font-mono text-accent uppercase tracking-wider">{isEditing ? 'Edit' : 'Create'} Alert</span>
|
||||
</div>
|
||||
</div>
|
||||
<button onClick={onClose} className="p-2 hover:bg-white/10 text-white/40 hover:text-white transition-colors">
|
||||
<X className="w-5 h-5" />
|
||||
<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>
|
||||
|
||||
<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 && (
|
||||
<div className="p-4 bg-rose-500/10 border border-rose-500/20 flex items-center gap-3 text-rose-400">
|
||||
<AlertCircle className="w-5 h-5" />
|
||||
<p className="text-sm flex-1">{error}</p>
|
||||
<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-4 h-4" />{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Basic Info */}
|
||||
<div className="space-y-4">
|
||||
<h4 className="text-xs font-mono text-white/40 tracking-wide">Basic Info</h4>
|
||||
|
||||
<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"
|
||||
/>
|
||||
<label className="block text-[9px] font-mono text-white/40 uppercase mb-1.5">Alert Name *</label>
|
||||
<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 className="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<label className="block text-xs text-white/50 mb-2">Description</label>
|
||||
<textarea
|
||||
value={form.description}
|
||||
onChange={(e) => setForm({ ...form, description: e.target.value })}
|
||||
placeholder="Optional description"
|
||||
rows={2}
|
||||
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"
|
||||
/>
|
||||
<label className="block text-[9px] font-mono text-white/40 uppercase mb-1.5">TLDs</label>
|
||||
<input type="text" value={form.tlds} onChange={(e) => setForm({ ...form, tlds: 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="com,io,ai" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Filters */}
|
||||
<div className="space-y-4">
|
||||
<h4 className="text-xs font-mono text-white/40 tracking-wide">Criteria</h4>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<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"
|
||||
/>
|
||||
<label className="block text-[9px] font-mono text-white/40 uppercase mb-1.5">Platforms</label>
|
||||
<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>
|
||||
<label className="block text-xs text-white/50 mb-2">Must Contain</label>
|
||||
<input
|
||||
type="text"
|
||||
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"
|
||||
/>
|
||||
<label className="block text-[9px] font-mono text-white/40 uppercase mb-1.5">Must Contain</label>
|
||||
<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>
|
||||
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<label className="block text-xs text-white/50 mb-2">Must Not Contain</label>
|
||||
<input
|
||||
type="text"
|
||||
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"
|
||||
/>
|
||||
<label className="block text-[9px] font-mono text-white/40 uppercase mb-1.5">Min Price</label>
|
||||
<input type="number" value={form.min_price} onChange={(e) => setForm({ ...form, min_price: 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="0" />
|
||||
</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"
|
||||
/>
|
||||
<label className="block text-[9px] font-mono text-white/40 uppercase mb-1.5">Max Price</label>
|
||||
<input type="number" value={form.max_price} onChange={(e) => setForm({ ...form, max_price: 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="10000" />
|
||||
</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>
|
||||
|
||||
{/* Notifications */}
|
||||
<div className="space-y-4">
|
||||
<h4 className="text-xs font-mono text-white/40 tracking-wide">Notifications</h4>
|
||||
|
||||
<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"
|
||||
/>
|
||||
<label className="flex items-center gap-3 p-2.5 border border-white/[0.06] cursor-pointer hover:bg-white/[0.02]">
|
||||
<input type="checkbox" checked={form.notify_email} onChange={(e) => setForm({ ...form, notify_email: e.target.checked })} className="w-4 h-4" />
|
||||
<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 className={clsx(
|
||||
"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"
|
||||
/>
|
||||
<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")}>
|
||||
<input type="checkbox" checked={form.notify_sms} onChange={(e) => isTycoon && setForm({ ...form, notify_sms: e.target.checked })} disabled={!isTycoon} className="w-4 h-4" />
|
||||
<MessageSquare className="w-4 h-4 text-amber-400" />
|
||||
<span className="text-sm text-white/60 flex-1">SMS notifications</span>
|
||||
{!isTycoon && <Crown className="w-4 h-4 text-amber-400" />}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{/* Actions */}
|
||||
<div className="flex gap-3 pt-4">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
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" />
|
||||
<div className="flex gap-3 pt-2">
|
||||
<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 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">
|
||||
{loading ? <Loader2 className="w-4 h-4 animate-spin" /> : <CheckCircle className="w-4 h-4" />}
|
||||
{isEditing ? 'Update' : 'Create'}
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
@ -731,3 +605,4 @@ function CreateEditModal({ alert, onClose, onSuccess, isTycoon }: {
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -2,28 +2,17 @@
|
||||
|
||||
import { useState, useEffect, useCallback } from 'react'
|
||||
import {
|
||||
TrendingUp,
|
||||
DollarSign,
|
||||
Zap,
|
||||
Plus,
|
||||
CheckCircle2,
|
||||
Clock,
|
||||
AlertCircle,
|
||||
MousePointer,
|
||||
Target,
|
||||
Wallet,
|
||||
RefreshCw,
|
||||
ChevronRight,
|
||||
Copy,
|
||||
Check,
|
||||
XCircle,
|
||||
Sparkles,
|
||||
Loader2
|
||||
TrendingUp, DollarSign, Zap, Plus, CheckCircle2, Clock, AlertCircle,
|
||||
MousePointer, Target, Wallet, RefreshCw, ChevronRight, Copy, Check,
|
||||
XCircle, Sparkles, Loader2, Eye, Gavel, Menu, Settings, Shield, LogOut,
|
||||
Crown, Coins, Tag, X
|
||||
} from 'lucide-react'
|
||||
import { api, YieldDomain, YieldTransaction } from '@/lib/api'
|
||||
import { useStore } from '@/lib/store'
|
||||
import { CommandCenterLayout } from '@/components/CommandCenterLayout'
|
||||
import { Sidebar } from '@/components/Sidebar'
|
||||
import clsx from 'clsx'
|
||||
import Link from 'next/link'
|
||||
import Image from 'next/image'
|
||||
|
||||
// ============================================================================
|
||||
// STATUS BADGE
|
||||
@ -37,297 +26,65 @@ function StatusBadge({ status }: { status: string }) {
|
||||
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 },
|
||||
}
|
||||
|
||||
const { color, icon: Icon } = config[status] || config.pending
|
||||
|
||||
return (
|
||||
<span className={`inline-flex items-center gap-1.5 px-2 py-0.5 text-[10px] font-mono border ${color}`}>
|
||||
<Icon className="w-3 h-3" />
|
||||
{status}
|
||||
<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" />{status}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// ACTIVATE MODAL
|
||||
// ACTIVATE MODAL (simplified)
|
||||
// ============================================================================
|
||||
|
||||
function ActivateModal({
|
||||
isOpen,
|
||||
onClose,
|
||||
onSuccess
|
||||
}: {
|
||||
isOpen: boolean
|
||||
onClose: () => void
|
||||
onSuccess: () => void
|
||||
}) {
|
||||
const [step, setStep] = useState<'input' | 'analyze' | 'dns' | 'done'>('input')
|
||||
function ActivateModal({ isOpen, onClose, onSuccess }: { isOpen: boolean; onClose: () => void; onSuccess: () => void }) {
|
||||
const [domain, setDomain] = useState('')
|
||||
const [loading, setLoading] = useState(false)
|
||||
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
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
|
||||
try {
|
||||
const result = await api.analyzeYieldDomain(domain.trim())
|
||||
setAnalysis(result)
|
||||
setStep('analyze')
|
||||
} catch (err: any) {
|
||||
setError(err.message || 'Failed to analyze domain')
|
||||
} finally {
|
||||
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 = () => {
|
||||
await api.activateYieldDomain(domain.trim(), true)
|
||||
onSuccess()
|
||||
onClose()
|
||||
setStep('input')
|
||||
setDomain('')
|
||||
setAnalysis(null)
|
||||
setDnsInstructions(null)
|
||||
} catch (err: any) {
|
||||
setError(err.message || 'Failed')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
if (!isOpen) return null
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-[100] flex items-center justify-center">
|
||||
<div className="absolute inset-0 bg-black/80 backdrop-blur-sm" onClick={onClose} />
|
||||
|
||||
<div className="relative bg-[#050505] border border-white/10 w-full max-w-lg mx-4">
|
||||
{/* Header */}
|
||||
<div className="p-6 border-b border-white/[0.08]">
|
||||
<div className="flex items-center gap-3">
|
||||
<Sparkles className="w-5 h-5 text-accent" />
|
||||
<div className="fixed inset-0 z-[110] bg-black/80 flex items-center justify-center p-4" 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="flex items-center gap-2">
|
||||
<Sparkles className="w-4 h-4 text-accent" />
|
||||
<span className="text-xs font-mono text-accent uppercase tracking-wider">Add Domain</span>
|
||||
</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 className="p-4 space-y-4">
|
||||
<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>
|
||||
<label className="block text-[9px] font-mono text-white/40 uppercase mb-1.5">Domain</label>
|
||||
<input type="text" value={domain} onChange={(e) => setDomain(e.target.value)} placeholder="example.com"
|
||||
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>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="p-6">
|
||||
{step === 'input' && (
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-xs text-white/50 mb-2">Domain Name</label>
|
||||
<input
|
||||
type="text"
|
||||
value={domain}
|
||||
onChange={(e) => setDomain(e.target.value)}
|
||||
placeholder="e.g. zahnarzt-zuerich.ch"
|
||||
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" />
|
||||
{error && <div className="p-2 bg-rose-500/10 border border-rose-500/20 text-rose-400 text-xs">{error}</div>}
|
||||
<button onClick={handleActivate} disabled={loading || !domain.trim()}
|
||||
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">
|
||||
{loading ? <Loader2 className="w-4 h-4 animate-spin" /> : <Plus 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>
|
||||
)
|
||||
}
|
||||
@ -337,271 +94,251 @@ function ActivateModal({
|
||||
// ============================================================================
|
||||
|
||||
export default function YieldPage() {
|
||||
const { subscription } = useStore()
|
||||
const { subscription, user, logout, checkAuth } = useStore()
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [dashboard, setDashboard] = useState<any>(null)
|
||||
const [showActivateModal, setShowActivateModal] = useState(false)
|
||||
const [refreshing, setRefreshing] = useState(false)
|
||||
const [menuOpen, setMenuOpen] = useState(false)
|
||||
|
||||
useEffect(() => { checkAuth() }, [checkAuth])
|
||||
|
||||
const fetchDashboard = useCallback(async () => {
|
||||
try {
|
||||
const data = await api.getYieldDashboard()
|
||||
setDashboard(data)
|
||||
} catch (err) {
|
||||
console.error('Failed to load yield dashboard:', err)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
setRefreshing(false)
|
||||
}
|
||||
} catch (err) { console.error(err) }
|
||||
finally { setLoading(false); setRefreshing(false) }
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
fetchDashboard()
|
||||
}, [fetchDashboard])
|
||||
|
||||
const handleRefresh = () => {
|
||||
setRefreshing(true)
|
||||
fetchDashboard()
|
||||
}
|
||||
useEffect(() => { fetchDashboard() }, [fetchDashboard])
|
||||
|
||||
const stats = dashboard?.stats
|
||||
|
||||
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, active: true },
|
||||
{ href: '/terminal/listing', label: 'For Sale', icon: Tag },
|
||||
]}
|
||||
]
|
||||
|
||||
return (
|
||||
<CommandCenterLayout minimal>
|
||||
{/* ═══════════════════════════════════════════════════════════════════════ */}
|
||||
{/* HEADER */}
|
||||
{/* ═══════════════════════════════════════════════════════════════════════ */}
|
||||
<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="min-h-screen bg-[#020202]">
|
||||
<div className="hidden lg:block"><Sidebar /></div>
|
||||
|
||||
<main className="lg:pl-[240px]">
|
||||
{/* MOBILE HEADER */}
|
||||
<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 className="px-4 py-3">
|
||||
<div className="flex items-center justify-between mb-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">Yield</span>
|
||||
</div>
|
||||
<span className="text-[10px] font-mono text-white/40">{stats?.active_domains || 0} active</span>
|
||||
</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 font-mono">${stats?.monthly_revenue || 0}</div>
|
||||
<div className="text-[9px] font-mono text-accent/60 uppercase tracking-wider">Monthly</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?.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>
|
||||
</header>
|
||||
|
||||
{/* DESKTOP HEADER */}
|
||||
<section className="hidden lg:block px-10 pt-10 pb-6">
|
||||
<div className="flex items-end justify-between gap-6">
|
||||
<div className="space-y-3">
|
||||
<div className="inline-flex items-center gap-2">
|
||||
<TrendingUp className="w-4 h-4 text-accent" />
|
||||
<span className="text-[10px] font-mono tracking-wide text-accent">Passive Income</span>
|
||||
<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-[2rem] lg:text-[2.5rem] leading-[1] tracking-[-0.02em]">
|
||||
<span className="text-white">Yield</span>
|
||||
</h1>
|
||||
|
||||
<p className="text-white/40 text-sm max-w-md">
|
||||
Turn parked domains into passive income with intent-based monetization
|
||||
</p>
|
||||
<h1 className="font-display text-[2.5rem] leading-[1] tracking-[-0.02em] text-white">Yield</h1>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="flex items-center gap-6">
|
||||
{stats && (
|
||||
<div className="flex gap-6">
|
||||
<div className="flex gap-8">
|
||||
<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 className="text-2xl font-bold text-accent font-mono">${stats.monthly_revenue}</div>
|
||||
<div className="text-[9px] font-mono text-white/30 uppercase">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 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 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"
|
||||
>
|
||||
<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 bg-accent text-black text-sm font-semibold hover:bg-white transition-colors"
|
||||
>
|
||||
<Plus className="w-4 h-4" />
|
||||
Add Domain
|
||||
<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>
|
||||
</section>
|
||||
|
||||
{/* ═══════════════════════════════════════════════════════════════════════ */}
|
||||
{/* STATS */}
|
||||
{/* ═══════════════════════════════════════════════════════════════════════ */}
|
||||
{stats && (
|
||||
<section className="pb-6 border-b border-white/[0.08]">
|
||||
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
<div className="p-4 bg-accent/5 border border-accent/20">
|
||||
<DollarSign className="w-5 h-5 text-accent mb-2" />
|
||||
<div className="text-xl font-display text-white">{stats.currency} {stats.monthly_revenue.toLocaleString()}</div>
|
||||
<div className="text-[10px] text-white/40 font-mono">Monthly Revenue</div>
|
||||
<div className="text-[10px] text-white/30 mt-1">Lifetime: {stats.currency} {stats.lifetime_revenue.toLocaleString()}</div>
|
||||
</div>
|
||||
|
||||
<div className="p-4 bg-white/[0.02] border border-white/[0.08]">
|
||||
<Zap className="w-5 h-5 text-blue-400 mb-2" />
|
||||
<div className="text-xl font-display text-white">{stats.active_domains}</div>
|
||||
<div className="text-[10px] text-white/40 font-mono">Active Domains</div>
|
||||
<div className="text-[10px] text-white/30 mt-1">{stats.pending_domains} pending</div>
|
||||
</div>
|
||||
|
||||
<div className="p-4 bg-white/[0.02] border border-white/[0.08]">
|
||||
<MousePointer className="w-5 h-5 text-amber-400 mb-2" />
|
||||
<div className="text-xl font-display text-white">{stats.monthly_clicks.toLocaleString()}</div>
|
||||
<div className="text-[10px] text-white/40 font-mono">Monthly Clicks</div>
|
||||
<div className="text-[10px] text-white/30 mt-1">{stats.monthly_conversions} conversions</div>
|
||||
</div>
|
||||
|
||||
<div className="p-4 bg-white/[0.02] border border-white/[0.08]">
|
||||
<Wallet className="w-5 h-5 text-purple-400 mb-2" />
|
||||
<div className="text-xl font-display text-white">{stats.currency} {stats.pending_payout.toLocaleString()}</div>
|
||||
<div className="text-[10px] text-white/40 font-mono">Pending Payout</div>
|
||||
{stats.next_payout_date && (
|
||||
<div className="text-[10px] text-white/30 mt-1">Next: {new Date(stats.next_payout_date).toLocaleDateString()}</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{/* ADD BUTTON MOBILE */}
|
||||
<section className="lg:hidden px-4 py-3 border-b border-white/[0.08]">
|
||||
<button onClick={() => setShowActivateModal(true)}
|
||||
className="w-full flex items-center justify-center gap-2 py-3 bg-accent text-black text-xs font-bold uppercase tracking-wider">
|
||||
<Plus className="w-4 h-4" />Add Domain
|
||||
</button>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* ═══════════════════════════════════════════════════════════════════════ */}
|
||||
{/* DOMAINS TABLE */}
|
||||
{/* ═══════════════════════════════════════════════════════════════════════ */}
|
||||
<section className="py-6">
|
||||
{/* CONTENT */}
|
||||
<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>
|
||||
) : dashboard?.domains?.length === 0 ? (
|
||||
<div className="text-center py-16">
|
||||
<div className="w-14 h-14 mx-auto bg-white/[0.02] border border-white/10 flex items-center justify-center mb-4">
|
||||
<TrendingUp className="w-6 h-6 text-white/20" />
|
||||
</div>
|
||||
<p className="text-white/40 text-sm mb-2">No yield domains yet</p>
|
||||
<p className="text-white/25 text-xs mb-6 max-w-sm mx-auto">
|
||||
Activate your first domain to start generating passive income
|
||||
</p>
|
||||
<button
|
||||
onClick={() => setShowActivateModal(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" />
|
||||
Add First Domain
|
||||
</button>
|
||||
) : !dashboard?.domains?.length ? (
|
||||
<div className="text-center py-16 border border-dashed border-white/[0.08]">
|
||||
<TrendingUp className="w-8 h-8 text-white/10 mx-auto mb-3" />
|
||||
<p className="text-white/40 text-sm font-mono mb-2">No yield domains yet</p>
|
||||
<p className="text-white/25 text-xs font-mono">Activate domains to earn passive income</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full min-w-[800px]">
|
||||
<thead>
|
||||
<tr className="text-xs text-white/40 border-b border-white/[0.06]">
|
||||
<th className="text-left py-3 px-4 font-medium">Domain</th>
|
||||
<th className="text-left py-3 px-4 font-medium">Status</th>
|
||||
<th className="text-left py-3 px-4 font-medium">Intent</th>
|
||||
<th className="text-right py-3 px-4 font-medium">Clicks</th>
|
||||
<th className="text-right py-3 px-4 font-medium">Conversions</th>
|
||||
<th className="text-right py-3 px-4 font-medium">Revenue</th>
|
||||
<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="space-y-px bg-white/[0.04] border border-white/[0.08]">
|
||||
{/* Header */}
|
||||
<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]">
|
||||
<div>Domain</div>
|
||||
<div className="text-center">Status</div>
|
||||
<div>Intent</div>
|
||||
<div className="text-right">Clicks</div>
|
||||
<div className="text-right">Conv.</div>
|
||||
<div className="text-right">Revenue</div>
|
||||
</div>
|
||||
|
||||
{dashboard.domains.map((domain: YieldDomain) => (
|
||||
<div key={domain.id} className="bg-[#020202] hover:bg-white/[0.02] transition-all">
|
||||
{/* Mobile */}
|
||||
<div className="lg:hidden p-3">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<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()}
|
||||
</div>
|
||||
<span className="font-medium text-white">{domain.domain}</span>
|
||||
<span className="text-sm font-bold text-white font-mono">{domain.domain}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td className="py-3 px-4">
|
||||
<StatusBadge status={domain.status} />
|
||||
</td>
|
||||
<td className="py-3 px-4">
|
||||
<span className="text-sm text-white/60 capitalize">
|
||||
{domain.detected_intent?.replace('_', ' ') || '-'}
|
||||
</span>
|
||||
{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>
|
||||
<div className="flex justify-between text-[10px] font-mono text-white/40">
|
||||
<span>{domain.total_clicks} clicks</span>
|
||||
<span className="text-accent font-bold">${domain.total_revenue}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ═══════════════════════════════════════════════════════════════════════ */}
|
||||
{/* 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">
|
||||
{/* Desktop */}
|
||||
<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={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 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>
|
||||
<div>
|
||||
<p className="text-sm font-medium text-white capitalize">{tx.event_type}</p>
|
||||
<p className="text-[10px] text-white/30 font-mono">{tx.partner_slug}</p>
|
||||
<span className="text-sm font-bold text-white font-mono">{domain.domain}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<p className="text-sm font-medium text-accent font-mono">
|
||||
+{tx.currency} {tx.net_amount.toFixed(2)}
|
||||
</p>
|
||||
<p className="text-[10px] text-white/30 font-mono">
|
||||
{new Date(tx.created_at).toLocaleDateString()}
|
||||
</p>
|
||||
<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>
|
||||
</section>
|
||||
)}
|
||||
</section>
|
||||
|
||||
{/* Activate Modal */}
|
||||
<ActivateModal
|
||||
isOpen={showActivateModal}
|
||||
onClose={() => setShowActivateModal(false)}
|
||||
onSuccess={fetchDashboard}
|
||||
/>
|
||||
</CommandCenterLayout>
|
||||
{/* MOBILE BOTTOM NAV */}
|
||||
<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 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>
|
||||
)}
|
||||
</main>
|
||||
|
||||
<ActivateModal isOpen={showActivateModal} onClose={() => setShowActivateModal(false)} onSuccess={fetchDashboard} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@ -100,8 +100,9 @@ export const useStore = create<AppState>((set, get) => ({
|
||||
|
||||
register: async (email, password, name) => {
|
||||
await api.register(email, password, name)
|
||||
// Auto-login after registration
|
||||
await get().login(email, password)
|
||||
// Note: No auto-login after registration
|
||||
// User should verify email first (verification email is sent)
|
||||
// They can then log in manually via the login page
|
||||
},
|
||||
|
||||
logout: () => {
|
||||
|
||||
Reference in New Issue
Block a user