|
1 | 1 | "use client"; |
2 | 2 |
|
3 | 3 | import type { Booking } from "@sr24/types/interface"; |
4 | | -import { useEffect } from "react"; |
| 4 | +import { useEffect, useMemo, useState } from "react"; |
5 | 5 | import useSWR from "swr"; |
6 | 6 | import Spinner from "@/components/Spinner/Spinner"; |
7 | 7 | import { fetchApi } from "@/utils/api"; |
8 | | -import { init } from "../lib"; |
9 | | -import BookingsControls from "./BookingsControls"; |
| 8 | +import { init, setFeaturesByTime } from "../lib"; |
| 9 | +import { BookingsControl } from "./BookingsControls"; |
10 | 10 | import BookingsMap from "./BookingsMap"; |
11 | 11 |
|
| 12 | +export const REPLAY_SPEEDS = [1, 2, 4, 8, 16]; |
| 13 | + |
12 | 14 | export default function Bookings() { |
13 | 15 | const { data, isLoading } = useSWR<Booking[]>("/data/bookings", fetchApi, { |
14 | 16 | refreshInterval: 10 * 60 * 1000, |
15 | 17 | revalidateOnFocus: false, |
16 | 18 | }); |
17 | 19 |
|
| 20 | + const [progress, setProgress] = useState(0); |
| 21 | + const [playing, setPlaying] = useState(false); |
| 22 | + const [speedIndex, setSpeedIndex] = useState(3); |
| 23 | + |
| 24 | + const timeline = useMemo(() => buildTimeline(data || []), [data]); |
| 25 | + |
| 26 | + useEffect(() => { |
| 27 | + if (!timeline.length) return; |
| 28 | + setProgress(getCurrentTimelineStep(timeline)); |
| 29 | + }, [timeline]); |
| 30 | + |
18 | 31 | useEffect(() => { |
19 | 32 | if (data) { |
20 | 33 | init(data); |
21 | 34 | } |
22 | 35 | }, [data]); |
23 | 36 |
|
| 37 | + useEffect(() => { |
| 38 | + setFeaturesByTime(timeline[progress]); |
| 39 | + }, [progress, timeline]); |
| 40 | + |
| 41 | + useEffect(() => { |
| 42 | + if (!playing) return; |
| 43 | + if (!timeline.length) return; |
| 44 | + |
| 45 | + const intervalMs = (STEP_MINUTES * 60 * 1000) / REPLAY_SPEEDS[speedIndex]; |
| 46 | + const maxIndex = timeline.length - 1; |
| 47 | + |
| 48 | + const interval = setInterval(() => { |
| 49 | + setProgress((prev) => { |
| 50 | + if (prev >= maxIndex) { |
| 51 | + setPlaying(false); |
| 52 | + return prev; |
| 53 | + } |
| 54 | + return prev + 1; |
| 55 | + }); |
| 56 | + }, intervalMs); |
| 57 | + |
| 58 | + return () => clearInterval(interval); |
| 59 | + }, [playing, speedIndex, timeline.length]); |
| 60 | + |
24 | 61 | if (!data || isLoading) { |
25 | 62 | return <Spinner />; |
26 | 63 | } |
27 | 64 |
|
28 | 65 | return ( |
29 | | - <> |
30 | | - <BookingsControls /> |
| 66 | + <div id="map-wrapper"> |
31 | 67 | <BookingsMap /> |
32 | | - </> |
| 68 | + <BookingsControl |
| 69 | + progress={progress} |
| 70 | + setProgress={setProgress} |
| 71 | + setNow={() => setProgress(getCurrentTimelineStep(timeline))} |
| 72 | + setSpeedIndex={setSpeedIndex} |
| 73 | + speedIndex={speedIndex} |
| 74 | + setPlaying={setPlaying} |
| 75 | + playing={playing} |
| 76 | + currentTime={timeline[progress]} |
| 77 | + max={timeline.length - 1} |
| 78 | + /> |
| 79 | + </div> |
33 | 80 | ); |
34 | 81 | } |
| 82 | + |
| 83 | +const STEP_MINUTES = 30; |
| 84 | + |
| 85 | +function buildTimeline(bookings: Booking[]) { |
| 86 | + if (!bookings.length) return []; |
| 87 | + |
| 88 | + const start = new Date(bookings[0].start).getTime(); |
| 89 | + const end = new Date(bookings[bookings.length - 1].end).getTime(); |
| 90 | + |
| 91 | + const stepMs = STEP_MINUTES * 60 * 1000; |
| 92 | + const timeline = []; |
| 93 | + |
| 94 | + for (let t = start; t <= end; t += stepMs) { |
| 95 | + timeline.push(t); |
| 96 | + } |
| 97 | + |
| 98 | + return timeline; |
| 99 | +} |
| 100 | + |
| 101 | +function getCurrentTimelineStep(timeline: number[]) { |
| 102 | + if (!timeline.length) return 0; |
| 103 | + |
| 104 | + const now = Date.now(); |
| 105 | + |
| 106 | + const idx = timeline.findIndex((t) => t > now); |
| 107 | + return idx === -1 ? timeline.length - 1 : Math.max(0, idx - 1); |
| 108 | +} |
0 commit comments