155 lines
5.9 KiB
TypeScript
155 lines
5.9 KiB
TypeScript
'use client'
|
|
import { useEffect, useState } from 'react'
|
|
import { motion } from 'framer-motion'
|
|
import { useEngineStore } from '@/store/engineStore'
|
|
import { formatUptime } from '@/lib/utils'
|
|
import StatsGrid from '@/components/dashboard/StatsGrid'
|
|
import RecentListings from '@/components/dashboard/RecentListings'
|
|
import EngineConsole from '@/components/dashboard/EngineConsole'
|
|
import ActivityLog from '@/components/dashboard/ActivityLog'
|
|
|
|
const BASE = 'http://localhost:8000'
|
|
|
|
/* ── Live clock ──────────────────────────────────────────────── */
|
|
function LiveClock() {
|
|
const [tick, setTick] = useState('')
|
|
|
|
useEffect(() => {
|
|
const fmt = () =>
|
|
new Date().toLocaleTimeString([], {
|
|
hour: '2-digit',
|
|
minute: '2-digit',
|
|
second: '2-digit',
|
|
})
|
|
setTick(fmt())
|
|
const id = setInterval(() => setTick(fmt()), 1000)
|
|
return () => clearInterval(id)
|
|
}, [])
|
|
|
|
return (
|
|
<span className="text-[12px] font-mono text-g-faint/60 tabular-nums tracking-widest select-none">
|
|
{tick}
|
|
</span>
|
|
)
|
|
}
|
|
|
|
/* ── Connection badge ────────────────────────────────────────── */
|
|
function ConnectionBadge({ offline }: { offline: boolean }) {
|
|
return (
|
|
<div
|
|
className={`inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full border text-[10px] font-bold tracking-wide transition-all duration-500 ${
|
|
offline
|
|
? 'bg-g-red/8 border-g-red/20 text-g-red'
|
|
: 'bg-g-green/8 border-g-green/15 text-g-green'
|
|
}`}
|
|
>
|
|
<span
|
|
className="w-1.5 h-1.5 rounded-full"
|
|
style={{
|
|
background: offline ? '#f43f5e' : '#00e87b',
|
|
boxShadow: offline ? '0 0 6px #f43f5e80' : '0 0 6px #00e87b80',
|
|
animation: offline ? undefined : 'pulse-ring 2s ease-out infinite',
|
|
}}
|
|
/>
|
|
{offline ? 'OFFLINE' : 'LIVE'}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
/* ── Divider strip ───────────────────────────────────────────── */
|
|
function SectionLabel({ children }: { children: React.ReactNode }) {
|
|
return (
|
|
<div className="flex items-center gap-3">
|
|
<span className="text-[10px] font-bold uppercase tracking-[0.16em] text-g-faint/70 whitespace-nowrap">
|
|
{children}
|
|
</span>
|
|
<div className="flex-1 h-px bg-gradient-to-r from-g-border/60 to-transparent" />
|
|
</div>
|
|
)
|
|
}
|
|
|
|
/* ── Main ────────────────────────────────────────────────────── */
|
|
export default function DashboardPage() {
|
|
const { status, uptime_seconds, total_scanned, total_alerts, isOffline } = useEngineStore()
|
|
|
|
const [keywordCount, setKeywordCount] = useState(0)
|
|
|
|
useEffect(() => {
|
|
fetch(`${BASE}/api/keywords`)
|
|
.then(r => r.json())
|
|
.then(d => { if (Array.isArray(d)) setKeywordCount(d.length) })
|
|
.catch(() => {})
|
|
}, [])
|
|
|
|
return (
|
|
<div className="space-y-8">
|
|
|
|
{/* ── Page header ──────────────────────────────────────── */}
|
|
<motion.div
|
|
initial={{ opacity: 1, y: -10 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
transition={{ duration: 0.45, ease: [0.22, 1, 0.36, 1] }}
|
|
className="flex items-end justify-between"
|
|
>
|
|
<div className="space-y-1">
|
|
<div className="flex items-center gap-3">
|
|
<h1 className="text-[22px] font-extrabold tracking-[-0.04em] leading-none bg-gradient-to-r from-g-text to-g-muted bg-clip-text text-transparent">
|
|
Mission Control
|
|
</h1>
|
|
<ConnectionBadge offline={isOffline} />
|
|
</div>
|
|
<p className="text-[12px] text-g-faint font-medium">
|
|
Auction intelligence engine · Ghost Node v2.7
|
|
</p>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-4">
|
|
<LiveClock />
|
|
{/* Quick export shortcut */}
|
|
<a
|
|
href={`${BASE}/api/export/csv`}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className="g-btn h-8 px-3 text-[11px] gap-1.5"
|
|
title="Export all listings as CSV"
|
|
>
|
|
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
|
|
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/>
|
|
</svg>
|
|
Export
|
|
</a>
|
|
</div>
|
|
</motion.div>
|
|
|
|
{/* ── Glow divider ─────────────────────────────────────── */}
|
|
<div className="glow-line" />
|
|
|
|
{/* ── Stats strip ──────────────────────────────────────── */}
|
|
<section>
|
|
<StatsGrid
|
|
scanned={total_scanned}
|
|
alerts={total_alerts}
|
|
keywords={keywordCount}
|
|
uptime={formatUptime(uptime_seconds)}
|
|
/>
|
|
</section>
|
|
|
|
{/* ── Main grid ────────────────────────────────────────── */}
|
|
<section className="space-y-3">
|
|
<SectionLabel>Live Feed</SectionLabel>
|
|
<div className="grid grid-cols-1 xl:grid-cols-[1fr_360px] gap-5">
|
|
<RecentListings />
|
|
<EngineConsole />
|
|
</div>
|
|
</section>
|
|
|
|
{/* ── Activity log ─────────────────────────────────────── */}
|
|
<section className="space-y-3">
|
|
<SectionLabel>Activity Log</SectionLabel>
|
|
<ActivityLog />
|
|
</section>
|
|
|
|
</div>
|
|
)
|
|
}
|