ServerLightGrid.tsx 2.5 KB

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