ServerLightGrid.tsx105 lines · main
1import { memo, useEffect, useMemo, useRef, useState } from 'react'
2import { cn } from 'ui'
3
4const ROWS = 4
5const COLS = 6
6const TOTAL = ROWS * COLS
7
8const ServerLightCell = memo(function ServerLightCell({
9 index,
10 isActive,
11}: {
12 index: number
13 isActive: boolean
14}) {
15 const row = Math.floor(index / COLS)
16 const col = index % COLS
17
18 return (
19 <div
20 className={cn(
21 'flex items-center justify-center',
22 col < COLS - 1 && 'border-r border-dotted border-foreground/10',
23 row < ROWS - 1 && 'border-b border-dotted border-foreground/10'
24 )}
25 >
26 <span
27 className={cn(
28 'block h-1 w-1 rounded-full motion-safe:transition-all motion-safe:duration-150',
29 isActive ? 'bg-brand-500 shadow-[0_0_6px_1px] shadow-brand-500/50' : 'bg-foreground/15'
30 )}
31 />
32 </div>
33 )
34})
35
36const GRID_STYLE = {
37 gridTemplateColumns: `repeat(${COLS}, 1fr)`,
38 gridTemplateRows: `repeat(${ROWS}, 1fr)`,
39} as const
40
41const CELL_INDICES = Array.from({ length: TOTAL }, (_, i) => i)
42
43function randomDelay() {
44 return 400 + Math.random() * 1400
45}
46
47function randomOnDuration() {
48 return 200 + Math.random() * 800
49}
50
51export function ServerLightGrid() {
52 const [active, setActive] = useState<Set<number>>(() => new Set())
53 const timers = useRef<Map<number, ReturnType<typeof setTimeout>>>(new Map())
54
55 useEffect(() => {
56 if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return
57
58 const timerMap = timers.current
59
60 function scheduleBlink(index: number) {
61 const offDelay = randomDelay()
62 const timer = setTimeout(() => {
63 setActive((prev) => {
64 const next = new Set(prev)
65 next.add(index)
66 return next
67 })
68
69 const onDuration = randomOnDuration()
70 const offTimer = setTimeout(() => {
71 setActive((prev) => {
72 const next = new Set(prev)
73 next.delete(index)
74 return next
75 })
76 scheduleBlink(index)
77 }, onDuration)
78
79 timerMap.set(index, offTimer)
80 }, offDelay)
81
82 timerMap.set(index, timer)
83 }
84
85 for (let i = 0; i < TOTAL; i++) {
86 scheduleBlink(i)
87 }
88
89 return () => {
90 timerMap.forEach(clearTimeout)
91 timerMap.clear()
92 }
93 }, [])
94
95 const cells = useMemo(
96 () => CELL_INDICES.map((i) => <ServerLightCell key={i} index={i} isActive={active.has(i)} />),
97 [active]
98 )
99
100 return (
101 <div className="grid h-full w-full" style={GRID_STYLE}>
102 {cells}
103 </div>
104 )
105}