CHANGED font-display → font-semibold: Command Center Pages: - alerts/page.tsx: Matches count, notifications, modal title - marketplace/page.tsx: Listing price - portfolio/page.tsx: Valuation price - listings/page.tsx: Price display, modal titles (2) - seo/page.tsx: Feature title, SEO score, backlinks count Components (used in Command Center): - CommandCenterLayout.tsx: Page title - AdminLayout.tsx: Page title - PremiumTable.tsx: StatCard value KEPT serif font (as requested): - Sidebar.tsx: POUNCE logo only - Header.tsx: POUNCE logo (public pages) - Footer.tsx: POUNCE logo Now the Command Center uses only sans-serif fonts, with the exception of the POUNCE logo in the navigation.
599 lines
25 KiB
TypeScript
599 lines
25 KiB
TypeScript
'use client'
|
|
|
|
import { useEffect, useState } from 'react'
|
|
import { useStore } from '@/lib/store'
|
|
import { api } from '@/lib/api'
|
|
import { CommandCenterLayout } from '@/components/CommandCenterLayout'
|
|
import { PageContainer, StatCard, Badge } from '@/components/PremiumTable'
|
|
import {
|
|
Plus,
|
|
Bell,
|
|
Target,
|
|
Zap,
|
|
Loader2,
|
|
Trash2,
|
|
Edit2,
|
|
CheckCircle,
|
|
AlertCircle,
|
|
X,
|
|
Play,
|
|
Pause,
|
|
Mail,
|
|
Smartphone,
|
|
Settings,
|
|
TestTube,
|
|
ChevronDown,
|
|
ChevronUp,
|
|
} from 'lucide-react'
|
|
import clsx from 'clsx'
|
|
|
|
interface SniperAlert {
|
|
id: number
|
|
name: string
|
|
description: string | null
|
|
tlds: string | null
|
|
keywords: string | null
|
|
exclude_keywords: string | null
|
|
max_length: number | null
|
|
min_length: number | null
|
|
max_price: number | null
|
|
min_price: number | null
|
|
max_bids: number | null
|
|
ending_within_hours: number | null
|
|
platforms: string | null
|
|
no_numbers: boolean
|
|
no_hyphens: boolean
|
|
exclude_chars: string | null
|
|
notify_email: boolean
|
|
notify_sms: boolean
|
|
is_active: boolean
|
|
matches_count: number
|
|
notifications_sent: number
|
|
last_matched_at: string | null
|
|
created_at: string
|
|
}
|
|
|
|
interface TestResult {
|
|
alert_name: string
|
|
auctions_checked: number
|
|
matches_found: number
|
|
matches: Array<{
|
|
domain: string
|
|
platform: string
|
|
current_bid: number
|
|
num_bids: number
|
|
end_time: string
|
|
}>
|
|
message: string
|
|
}
|
|
|
|
export default function SniperAlertsPage() {
|
|
const { subscription } = useStore()
|
|
|
|
const [alerts, setAlerts] = useState<SniperAlert[]>([])
|
|
const [loading, setLoading] = useState(true)
|
|
const [showCreateModal, setShowCreateModal] = useState(false)
|
|
const [creating, setCreating] = useState(false)
|
|
const [testing, setTesting] = useState<number | null>(null)
|
|
const [testResult, setTestResult] = useState<TestResult | null>(null)
|
|
const [expandedAlert, setExpandedAlert] = useState<number | null>(null)
|
|
const [error, setError] = useState<string | null>(null)
|
|
const [success, setSuccess] = useState<string | null>(null)
|
|
|
|
// Create form
|
|
const [newAlert, setNewAlert] = useState({
|
|
name: '',
|
|
description: '',
|
|
tlds: '',
|
|
keywords: '',
|
|
exclude_keywords: '',
|
|
max_length: '',
|
|
min_length: '',
|
|
max_price: '',
|
|
min_price: '',
|
|
max_bids: '',
|
|
no_numbers: false,
|
|
no_hyphens: false,
|
|
exclude_chars: '',
|
|
notify_email: true,
|
|
})
|
|
|
|
useEffect(() => {
|
|
loadAlerts()
|
|
}, [])
|
|
|
|
const loadAlerts = async () => {
|
|
setLoading(true)
|
|
try {
|
|
const data = await api.request<SniperAlert[]>('/sniper-alerts')
|
|
setAlerts(data)
|
|
} catch (err: any) {
|
|
setError(err.message)
|
|
} finally {
|
|
setLoading(false)
|
|
}
|
|
}
|
|
|
|
const handleCreate = async (e: React.FormEvent) => {
|
|
e.preventDefault()
|
|
setCreating(true)
|
|
setError(null)
|
|
|
|
try {
|
|
await api.request('/sniper-alerts', {
|
|
method: 'POST',
|
|
body: JSON.stringify({
|
|
name: newAlert.name,
|
|
description: newAlert.description || null,
|
|
tlds: newAlert.tlds || null,
|
|
keywords: newAlert.keywords || null,
|
|
exclude_keywords: newAlert.exclude_keywords || null,
|
|
max_length: newAlert.max_length ? parseInt(newAlert.max_length) : null,
|
|
min_length: newAlert.min_length ? parseInt(newAlert.min_length) : null,
|
|
max_price: newAlert.max_price ? parseFloat(newAlert.max_price) : null,
|
|
min_price: newAlert.min_price ? parseFloat(newAlert.min_price) : null,
|
|
max_bids: newAlert.max_bids ? parseInt(newAlert.max_bids) : null,
|
|
no_numbers: newAlert.no_numbers,
|
|
no_hyphens: newAlert.no_hyphens,
|
|
exclude_chars: newAlert.exclude_chars || null,
|
|
notify_email: newAlert.notify_email,
|
|
}),
|
|
})
|
|
setSuccess('Sniper Alert created!')
|
|
setShowCreateModal(false)
|
|
setNewAlert({
|
|
name: '', description: '', tlds: '', keywords: '', exclude_keywords: '',
|
|
max_length: '', min_length: '', max_price: '', min_price: '', max_bids: '',
|
|
no_numbers: false, no_hyphens: false, exclude_chars: '', notify_email: true,
|
|
})
|
|
loadAlerts()
|
|
} catch (err: any) {
|
|
setError(err.message)
|
|
} finally {
|
|
setCreating(false)
|
|
}
|
|
}
|
|
|
|
const handleToggle = async (alert: SniperAlert) => {
|
|
try {
|
|
await api.request(`/sniper-alerts/${alert.id}`, {
|
|
method: 'PUT',
|
|
body: JSON.stringify({ is_active: !alert.is_active }),
|
|
})
|
|
loadAlerts()
|
|
} catch (err: any) {
|
|
setError(err.message)
|
|
}
|
|
}
|
|
|
|
const handleDelete = async (alert: SniperAlert) => {
|
|
if (!confirm(`Delete alert "${alert.name}"?`)) return
|
|
|
|
try {
|
|
await api.request(`/sniper-alerts/${alert.id}`, { method: 'DELETE' })
|
|
setSuccess('Alert deleted')
|
|
loadAlerts()
|
|
} catch (err: any) {
|
|
setError(err.message)
|
|
}
|
|
}
|
|
|
|
const handleTest = async (alert: SniperAlert) => {
|
|
setTesting(alert.id)
|
|
setTestResult(null)
|
|
|
|
try {
|
|
const result = await api.request<TestResult>(`/sniper-alerts/${alert.id}/test`, {
|
|
method: 'POST',
|
|
})
|
|
setTestResult(result)
|
|
setExpandedAlert(alert.id)
|
|
} catch (err: any) {
|
|
setError(err.message)
|
|
} finally {
|
|
setTesting(null)
|
|
}
|
|
}
|
|
|
|
const tier = subscription?.tier || 'scout'
|
|
const limits = { scout: 2, trader: 10, tycoon: 50 }
|
|
const maxAlerts = limits[tier as keyof typeof limits] || 2
|
|
|
|
return (
|
|
<CommandCenterLayout
|
|
title="Sniper Alerts"
|
|
subtitle={`Hyper-personalized auction notifications (${alerts.length}/${maxAlerts})`}
|
|
actions={
|
|
<button
|
|
onClick={() => setShowCreateModal(true)}
|
|
disabled={alerts.length >= maxAlerts}
|
|
className="flex items-center gap-2 px-4 py-2 bg-accent text-background text-sm font-medium rounded-lg
|
|
hover:bg-accent-hover transition-all disabled:opacity-50"
|
|
>
|
|
<Plus className="w-4 h-4" />
|
|
New Alert
|
|
</button>
|
|
}
|
|
>
|
|
<PageContainer>
|
|
{/* Messages */}
|
|
{error && (
|
|
<div className="p-4 bg-red-500/10 border border-red-500/20 rounded-xl flex items-center gap-3">
|
|
<AlertCircle className="w-5 h-5 text-red-400" />
|
|
<p className="text-sm text-red-400 flex-1">{error}</p>
|
|
<button onClick={() => setError(null)}><X className="w-4 h-4 text-red-400" /></button>
|
|
</div>
|
|
)}
|
|
|
|
{success && (
|
|
<div className="p-4 bg-accent/10 border border-accent/20 rounded-xl flex items-center gap-3">
|
|
<CheckCircle className="w-5 h-5 text-accent" />
|
|
<p className="text-sm text-accent flex-1">{success}</p>
|
|
<button onClick={() => setSuccess(null)}><X className="w-4 h-4 text-accent" /></button>
|
|
</div>
|
|
)}
|
|
|
|
{/* Stats */}
|
|
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
|
|
<StatCard title="Active Alerts" value={alerts.filter(a => a.is_active).length} icon={Bell} />
|
|
<StatCard title="Total Matches" value={alerts.reduce((sum, a) => sum + a.matches_count, 0)} icon={Target} />
|
|
<StatCard title="Notifications Sent" value={alerts.reduce((sum, a) => sum + a.notifications_sent, 0)} icon={Zap} />
|
|
<StatCard title="Alert Slots" value={`${alerts.length}/${maxAlerts}`} icon={Settings} />
|
|
</div>
|
|
|
|
{/* Alerts List */}
|
|
{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 bg-background-secondary/30 border border-border rounded-2xl">
|
|
<Target className="w-16 h-16 text-foreground-subtle mx-auto mb-6" />
|
|
<h2 className="text-xl font-medium text-foreground mb-2">No Sniper Alerts</h2>
|
|
<p className="text-foreground-muted mb-6 max-w-md mx-auto">
|
|
Create alerts to get notified when domains matching your criteria appear in auctions.
|
|
</p>
|
|
<button
|
|
onClick={() => setShowCreateModal(true)}
|
|
className="inline-flex items-center gap-2 px-6 py-3 bg-accent text-background font-medium rounded-xl hover:bg-accent-hover transition-all"
|
|
>
|
|
<Plus className="w-5 h-5" />
|
|
Create Alert
|
|
</button>
|
|
</div>
|
|
) : (
|
|
<div className="space-y-4">
|
|
{alerts.map((alert) => (
|
|
<div
|
|
key={alert.id}
|
|
className="bg-background-secondary/30 border border-border rounded-2xl overflow-hidden transition-all hover:border-border-hover"
|
|
>
|
|
{/* Header */}
|
|
<div className="p-5">
|
|
<div className="flex flex-wrap items-start gap-4">
|
|
<div className="flex-1 min-w-[200px]">
|
|
<div className="flex items-center gap-3 mb-2">
|
|
<h3 className="text-lg font-medium text-foreground">{alert.name}</h3>
|
|
<Badge variant={alert.is_active ? 'success' : 'default'}>
|
|
{alert.is_active ? 'Active' : 'Paused'}
|
|
</Badge>
|
|
</div>
|
|
{alert.description && (
|
|
<p className="text-sm text-foreground-muted">{alert.description}</p>
|
|
)}
|
|
</div>
|
|
|
|
{/* Stats */}
|
|
<div className="flex items-center gap-6 text-sm">
|
|
<div className="text-center">
|
|
<p className="text-lg font-semibold text-foreground">{alert.matches_count}</p>
|
|
<p className="text-xs text-foreground-muted">Matches</p>
|
|
</div>
|
|
<div className="text-center">
|
|
<p className="text-lg font-semibold text-foreground">{alert.notifications_sent}</p>
|
|
<p className="text-xs text-foreground-muted">Notified</p>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Actions */}
|
|
<div className="flex items-center gap-2">
|
|
<button
|
|
onClick={() => handleTest(alert)}
|
|
disabled={testing === alert.id}
|
|
className="flex items-center gap-1.5 px-3 py-2 bg-foreground/5 text-foreground-muted text-sm font-medium rounded-lg hover:bg-foreground/10 transition-all"
|
|
>
|
|
{testing === alert.id ? (
|
|
<Loader2 className="w-4 h-4 animate-spin" />
|
|
) : (
|
|
<TestTube className="w-4 h-4" />
|
|
)}
|
|
Test
|
|
</button>
|
|
|
|
<button
|
|
onClick={() => handleToggle(alert)}
|
|
className={clsx(
|
|
"flex items-center gap-1.5 px-3 py-2 text-sm font-medium rounded-lg transition-all",
|
|
alert.is_active
|
|
? "bg-amber-500/10 text-amber-400 hover:bg-amber-500/20"
|
|
: "bg-accent/10 text-accent hover:bg-accent/20"
|
|
)}
|
|
>
|
|
{alert.is_active ? <Pause className="w-4 h-4" /> : <Play className="w-4 h-4" />}
|
|
{alert.is_active ? 'Pause' : 'Activate'}
|
|
</button>
|
|
|
|
<button
|
|
onClick={() => handleDelete(alert)}
|
|
className="p-2 text-foreground-subtle hover:text-red-400 transition-colors"
|
|
>
|
|
<Trash2 className="w-4 h-4" />
|
|
</button>
|
|
|
|
<button
|
|
onClick={() => setExpandedAlert(expandedAlert === alert.id ? null : alert.id)}
|
|
className="p-2 text-foreground-subtle hover:text-foreground transition-colors"
|
|
>
|
|
{expandedAlert === alert.id ? (
|
|
<ChevronUp className="w-4 h-4" />
|
|
) : (
|
|
<ChevronDown className="w-4 h-4" />
|
|
)}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Filter Summary */}
|
|
<div className="mt-4 flex flex-wrap gap-2">
|
|
{alert.tlds && (
|
|
<span className="px-2 py-1 bg-foreground/5 text-foreground-muted text-xs rounded">
|
|
TLDs: {alert.tlds}
|
|
</span>
|
|
)}
|
|
{alert.max_length && (
|
|
<span className="px-2 py-1 bg-foreground/5 text-foreground-muted text-xs rounded">
|
|
Max {alert.max_length} chars
|
|
</span>
|
|
)}
|
|
{alert.max_price && (
|
|
<span className="px-2 py-1 bg-foreground/5 text-foreground-muted text-xs rounded">
|
|
Max ${alert.max_price}
|
|
</span>
|
|
)}
|
|
{alert.no_numbers && (
|
|
<span className="px-2 py-1 bg-foreground/5 text-foreground-muted text-xs rounded">
|
|
No numbers
|
|
</span>
|
|
)}
|
|
{alert.no_hyphens && (
|
|
<span className="px-2 py-1 bg-foreground/5 text-foreground-muted text-xs rounded">
|
|
No hyphens
|
|
</span>
|
|
)}
|
|
{alert.notify_email && (
|
|
<span className="px-2 py-1 bg-accent/10 text-accent text-xs rounded flex items-center gap-1">
|
|
<Mail className="w-3 h-3" /> Email
|
|
</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Test Results */}
|
|
{expandedAlert === alert.id && testResult && testResult.alert_name === alert.name && (
|
|
<div className="px-5 pb-5">
|
|
<div className="p-4 bg-background rounded-xl border border-border">
|
|
<div className="flex items-center justify-between mb-3">
|
|
<p className="text-sm font-medium text-foreground">Test Results</p>
|
|
<p className="text-xs text-foreground-muted">
|
|
Checked {testResult.auctions_checked} auctions
|
|
</p>
|
|
</div>
|
|
|
|
{testResult.matches_found === 0 ? (
|
|
<p className="text-sm text-foreground-muted">{testResult.message}</p>
|
|
) : (
|
|
<div className="space-y-2">
|
|
<p className="text-sm text-accent">
|
|
Found {testResult.matches_found} matching domains!
|
|
</p>
|
|
<div className="max-h-48 overflow-y-auto space-y-1">
|
|
{testResult.matches.map((match, idx) => (
|
|
<div key={idx} className="flex items-center justify-between text-sm py-1">
|
|
<span className="font-mono text-foreground">{match.domain}</span>
|
|
<span className="text-foreground-muted">${match.current_bid}</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</PageContainer>
|
|
|
|
{/* Create Modal */}
|
|
{showCreateModal && (
|
|
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-background/80 backdrop-blur-sm overflow-y-auto">
|
|
<div className="w-full max-w-lg bg-background-secondary border border-border rounded-2xl p-6 my-8">
|
|
<h2 className="text-xl font-semibold text-foreground mb-2">Create Sniper Alert</h2>
|
|
<p className="text-sm text-foreground-muted mb-6">
|
|
Get notified when domains matching your criteria appear in auctions.
|
|
</p>
|
|
|
|
<form onSubmit={handleCreate} className="space-y-4 max-h-[60vh] overflow-y-auto pr-2">
|
|
<div>
|
|
<label className="block text-sm text-foreground-muted mb-1">Alert Name *</label>
|
|
<input
|
|
type="text"
|
|
required
|
|
value={newAlert.name}
|
|
onChange={(e) => setNewAlert({ ...newAlert, name: e.target.value })}
|
|
placeholder="4-letter .com without numbers"
|
|
className="w-full px-4 py-3 bg-background border border-border rounded-xl text-foreground placeholder:text-foreground-subtle focus:outline-none focus:border-accent"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm text-foreground-muted mb-1">Description</label>
|
|
<input
|
|
type="text"
|
|
value={newAlert.description}
|
|
onChange={(e) => setNewAlert({ ...newAlert, description: e.target.value })}
|
|
placeholder="Optional description"
|
|
className="w-full px-4 py-3 bg-background border border-border rounded-xl text-foreground placeholder:text-foreground-subtle focus:outline-none focus:border-accent"
|
|
/>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div>
|
|
<label className="block text-sm text-foreground-muted mb-1">TLDs (comma-separated)</label>
|
|
<input
|
|
type="text"
|
|
value={newAlert.tlds}
|
|
onChange={(e) => setNewAlert({ ...newAlert, tlds: e.target.value })}
|
|
placeholder="com,io,ai"
|
|
className="w-full px-4 py-3 bg-background border border-border rounded-xl text-foreground placeholder:text-foreground-subtle focus:outline-none focus:border-accent"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm text-foreground-muted mb-1">Keywords (must contain)</label>
|
|
<input
|
|
type="text"
|
|
value={newAlert.keywords}
|
|
onChange={(e) => setNewAlert({ ...newAlert, keywords: e.target.value })}
|
|
placeholder="ai,tech,crypto"
|
|
className="w-full px-4 py-3 bg-background border border-border rounded-xl text-foreground placeholder:text-foreground-subtle focus:outline-none focus:border-accent"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div>
|
|
<label className="block text-sm text-foreground-muted mb-1">Min Length</label>
|
|
<input
|
|
type="number"
|
|
min="1"
|
|
max="63"
|
|
value={newAlert.min_length}
|
|
onChange={(e) => setNewAlert({ ...newAlert, min_length: e.target.value })}
|
|
placeholder="3"
|
|
className="w-full px-4 py-3 bg-background border border-border rounded-xl text-foreground placeholder:text-foreground-subtle focus:outline-none focus:border-accent"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm text-foreground-muted mb-1">Max Length</label>
|
|
<input
|
|
type="number"
|
|
min="1"
|
|
max="63"
|
|
value={newAlert.max_length}
|
|
onChange={(e) => setNewAlert({ ...newAlert, max_length: e.target.value })}
|
|
placeholder="6"
|
|
className="w-full px-4 py-3 bg-background border border-border rounded-xl text-foreground placeholder:text-foreground-subtle focus:outline-none focus:border-accent"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div>
|
|
<label className="block text-sm text-foreground-muted mb-1">Max Price ($)</label>
|
|
<input
|
|
type="number"
|
|
min="0"
|
|
value={newAlert.max_price}
|
|
onChange={(e) => setNewAlert({ ...newAlert, max_price: e.target.value })}
|
|
placeholder="500"
|
|
className="w-full px-4 py-3 bg-background border border-border rounded-xl text-foreground placeholder:text-foreground-subtle focus:outline-none focus:border-accent"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm text-foreground-muted mb-1">Max Bids (low competition)</label>
|
|
<input
|
|
type="number"
|
|
min="0"
|
|
value={newAlert.max_bids}
|
|
onChange={(e) => setNewAlert({ ...newAlert, max_bids: e.target.value })}
|
|
placeholder="5"
|
|
className="w-full px-4 py-3 bg-background border border-border rounded-xl text-foreground placeholder:text-foreground-subtle focus:outline-none focus:border-accent"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm text-foreground-muted mb-1">Exclude Characters</label>
|
|
<input
|
|
type="text"
|
|
value={newAlert.exclude_chars}
|
|
onChange={(e) => setNewAlert({ ...newAlert, exclude_chars: e.target.value })}
|
|
placeholder="q,x,z"
|
|
className="w-full px-4 py-3 bg-background border border-border rounded-xl text-foreground placeholder:text-foreground-subtle focus:outline-none focus:border-accent"
|
|
/>
|
|
</div>
|
|
|
|
<div className="flex flex-wrap gap-4">
|
|
<label className="flex items-center gap-2 cursor-pointer">
|
|
<input
|
|
type="checkbox"
|
|
checked={newAlert.no_numbers}
|
|
onChange={(e) => setNewAlert({ ...newAlert, no_numbers: e.target.checked })}
|
|
className="w-5 h-5 rounded border-border text-accent focus:ring-accent"
|
|
/>
|
|
<span className="text-sm text-foreground">No numbers</span>
|
|
</label>
|
|
|
|
<label className="flex items-center gap-2 cursor-pointer">
|
|
<input
|
|
type="checkbox"
|
|
checked={newAlert.no_hyphens}
|
|
onChange={(e) => setNewAlert({ ...newAlert, no_hyphens: e.target.checked })}
|
|
className="w-5 h-5 rounded border-border text-accent focus:ring-accent"
|
|
/>
|
|
<span className="text-sm text-foreground">No hyphens</span>
|
|
</label>
|
|
|
|
<label className="flex items-center gap-2 cursor-pointer">
|
|
<input
|
|
type="checkbox"
|
|
checked={newAlert.notify_email}
|
|
onChange={(e) => setNewAlert({ ...newAlert, notify_email: e.target.checked })}
|
|
className="w-5 h-5 rounded border-border text-accent focus:ring-accent"
|
|
/>
|
|
<span className="text-sm text-foreground flex items-center gap-1">
|
|
<Mail className="w-4 h-4" /> Email alerts
|
|
</span>
|
|
</label>
|
|
</div>
|
|
</form>
|
|
|
|
<div className="flex gap-3 mt-6">
|
|
<button
|
|
type="button"
|
|
onClick={() => setShowCreateModal(false)}
|
|
className="flex-1 px-4 py-3 border border-border text-foreground-muted rounded-xl hover:bg-foreground/5 transition-all"
|
|
>
|
|
Cancel
|
|
</button>
|
|
<button
|
|
onClick={handleCreate}
|
|
disabled={creating || !newAlert.name}
|
|
className="flex-1 flex items-center justify-center gap-2 px-4 py-3 bg-accent text-background font-medium rounded-xl hover:bg-accent-hover transition-all disabled:opacity-50"
|
|
>
|
|
{creating ? <Loader2 className="w-5 h-5 animate-spin" /> : <Target className="w-5 h-5" />}
|
|
{creating ? 'Creating...' : 'Create Alert'}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</CommandCenterLayout>
|
|
)
|
|
}
|
|
|