From 2a050b383bcafefb8fc378531d6f15f352fdb5a5 Mon Sep 17 00:00:00 2001 From: Yuval Adam <_@yuv.al> Date: Wed, 24 Sep 2025 10:52:38 +0200 Subject: Override all files with v8 implementation --- src/components/Datetime.tsx | 28 ++++++++++++ src/components/Geshem.tsx | 65 +++++++++++++++++++++++++++ src/components/Map.tsx | 105 ++++++++++++++++++++++++++++++++++++++++++++ src/components/Slider.tsx | 86 ++++++++++++++++++++++++++++++++++++ 4 files changed, 284 insertions(+) create mode 100644 src/components/Datetime.tsx create mode 100644 src/components/Geshem.tsx create mode 100644 src/components/Map.tsx create mode 100644 src/components/Slider.tsx (limited to 'src/components') diff --git a/src/components/Datetime.tsx b/src/components/Datetime.tsx new file mode 100644 index 0000000..8b66a4f --- /dev/null +++ b/src/components/Datetime.tsx @@ -0,0 +1,28 @@ +import React from "react"; +import { DateTime as LuxonDateTime } from "luxon"; + +interface DateTimeProps { + images: string[]; + slider: number; +} + +export function DateTime({ images, slider }: DateTimeProps) { + let datetime: LuxonDateTime | null = null; + + if (images.length > 0 && images[slider]) { + const ds = images[slider].substring(5, 18); + datetime = LuxonDateTime.fromFormat(ds, "yyyyMMdd/HHmm", { + zone: "utc", + }).setZone("Asia/Jerusalem"); + } + + const date = datetime ? datetime.toFormat("dd/MM/y") : ""; + const time = datetime ? datetime.toFormat("HH:mm") : ""; + + return ( +
+
{date}
+
{time}
+
+ ); +} \ No newline at end of file diff --git a/src/components/Geshem.tsx b/src/components/Geshem.tsx new file mode 100644 index 0000000..982d841 --- /dev/null +++ b/src/components/Geshem.tsx @@ -0,0 +1,65 @@ +import React, { useState, useEffect } from "react"; + +import { Map } from "./Map"; +import { Slider } from "./Slider"; +import { DateTime } from "./Datetime"; + +import { IMAGES_BASE_URL, PLAYBACK_HOURS, PLAYBACK_SLOTS } from "../config"; + +interface GeshemProps { + date?: string; +} + +export function Geshem({ date }: GeshemProps) { + const [images, setImages] = useState([]); + const [playback] = useState( + date || + (typeof window !== 'undefined' ? new URL(window.location.toString()).searchParams.get("history") : null) || + undefined + ); + const [slider, setSlider] = useState(playback ? PLAYBACK_SLOTS : 9); + + useEffect(() => { + const fetchImages = async () => + fetch(`${IMAGES_BASE_URL}/imgs.json`) + .then(res => res.json()) + .then(imgs => (imgs as any)["280"]) + .then(setImages); + + const buildPlayback = async () => { + const date = playback; + const hours = Array.from(Array(PLAYBACK_HOURS).keys()).map( + h => `${String(h).padStart(2, "0")}` + ); + const minutes = Array.from(Array(6).keys()).map( + m => `${String(m * 10).padStart(2, "0")}` + ); + const paths = hours.reduce( + (acc, h) => + acc.concat(minutes.map(m => `imgs/${date}/${h}${m}/280.png`)), + [] + ); + setImages(paths); + }; + + let timer: number; + if (playback) { + buildPlayback(); + } else { + fetchImages(); + timer = window.setInterval(fetchImages, 60 * 1000); + } + + return () => { + if (timer !== undefined) clearInterval(timer); + }; + }, [playback]); + + return ( + <> + + + + + ); +} \ No newline at end of file diff --git a/src/components/Map.tsx b/src/components/Map.tsx new file mode 100644 index 0000000..070dc5c --- /dev/null +++ b/src/components/Map.tsx @@ -0,0 +1,105 @@ +import React, { useState, useEffect, useRef } from "react"; +import mapboxgl from 'mapbox-gl'; + +import { + MAPBOX_ACCESS_TOKEN, + IMAGE_COORDINATES, + IMAGES_BASE_URL +} from "../config"; + +import "mapbox-gl/dist/mapbox-gl.css"; + +interface MapProps { + slider: number; + images: string[]; +} + +export function Map({ slider, images }: MapProps) { + const mapContainer = useRef(null); + const map = useRef(null); + + const [lng] = useState(35); + const [lat] = useState(31.9); + const [zoom] = useState(6.3); + const [loaded, setLoaded] = useState(false); + + const prevImages = useRef(images).current; + + useEffect(() => { + if (map.current || !mapContainer.current) return; + + map.current = new mapboxgl.Map({ + accessToken: MAPBOX_ACCESS_TOKEN, + container: mapContainer.current, + style: "mapbox://styles/mapbox/dark-v9", + center: [lng, lat], + zoom: zoom, + minZoom: 5, + maxZoom: 10, + hash: false, + }); + + map.current.on("style.load", () => { + setLoaded(true); + }); + }, [lng, lat, zoom]); + + useEffect(() => { + if (!loaded || !map.current) return; + + // remove old layers + prevImages.forEach((img) => { + if (!images.includes(img)) { + if (map.current?.getLayer(`layer-${img}`)) { + map.current.removeLayer(`layer-${img}`); + } + if (map.current?.getSource(`source-${img}`)) { + map.current.removeSource(`source-${img}`); + } + } + }); + + // add new layers + images.forEach((img, i) => { + if (!prevImages.includes(img) && !map.current?.getSource(`source-${img}`)) { + map.current?.addSource(`source-${img}`, { + type: "image", + url: `${IMAGES_BASE_URL}/${img}`, + coordinates: IMAGE_COORDINATES + }); + map.current?.addLayer({ + id: `layer-${img}`, + source: `source-${img}`, + type: "raster", + paint: { + "raster-opacity": 0, + "raster-opacity-transition": { + duration: 0 + } + } + }); + } + }); + }, [loaded, prevImages, images]); + + useEffect(() => { + if (!loaded || !images.length || !map.current) return; + + if (images[slider]) { + map.current.setPaintProperty(`layer-${images[slider]}`, "raster-opacity", 0.85); + } + + return () => { + // callback will hide the previous layer with the previous slider value + if (images[slider] && map.current?.getLayer(`layer-${images[slider]}`)) { + map.current.setPaintProperty(`layer-${images[slider]}`, "raster-opacity", 0); + } + }; + }, [loaded, slider, images]); + + return ( +
+
+
+ ); +} \ No newline at end of file diff --git a/src/components/Slider.tsx b/src/components/Slider.tsx new file mode 100644 index 0000000..22a8f8f --- /dev/null +++ b/src/components/Slider.tsx @@ -0,0 +1,86 @@ +import React, { useCallback, useRef, useState } from "react"; +import { PLAYBACK_SLOTS } from "../config"; + +interface SliderProps { + playback?: string; + slider: number; + setSlider: React.Dispatch>; +} + +export function Slider({ playback, slider, setSlider }: SliderProps) { + const sliderRef = useRef(null); + const [isDragging, setIsDragging] = useState(false); + + const max = playback ? PLAYBACK_SLOTS : 9; + + const handleStart = useCallback((e: React.MouseEvent | React.TouchEvent) => { + setIsDragging(true); + updateSliderValue(e); + }, []); + + const updateSliderValue = useCallback((e: MouseEvent | React.MouseEvent | TouchEvent | React.TouchEvent) => { + if (!sliderRef.current) return; + + const rect = sliderRef.current.getBoundingClientRect(); + let clientX: number; + + if ('touches' in e) { + clientX = e.touches?.[0]?.clientX || (e as TouchEvent).changedTouches?.[0]?.clientX || 0; + } else { + clientX = e.clientX; + } + + const percentage = Math.max(0, Math.min(1, (clientX - rect.left) / rect.width)); + const value = Math.round(percentage * max); + setSlider(value); + }, [max, setSlider]); + + const handleMove = useCallback((e: MouseEvent | TouchEvent) => { + if (isDragging) { + e.preventDefault(); + updateSliderValue(e); + } + }, [isDragging, updateSliderValue]); + + const handleEnd = useCallback(() => { + setIsDragging(false); + }, []); + + React.useEffect(() => { + if (isDragging) { + document.addEventListener('mousemove', handleMove); + document.addEventListener('mouseup', handleEnd); + document.addEventListener('touchmove', handleMove, { passive: false }); + document.addEventListener('touchend', handleEnd); + + return () => { + document.removeEventListener('mousemove', handleMove); + document.removeEventListener('mouseup', handleEnd); + document.removeEventListener('touchmove', handleMove); + document.removeEventListener('touchend', handleEnd); + }; + } + }, [isDragging, handleMove, handleEnd]); + + const percentage = (slider / max) * 100; + + return ( +
+
+
+
+
+ ); +} \ No newline at end of file -- cgit v1.3.1