summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorYuval Adam <_@yuv.al>2022-11-29 09:19:07 +0200
committerGitHub <noreply@github.com>2022-11-29 09:19:07 +0200
commit50d9d173d1c2cf22efb960fd122ca64201f34bf5 (patch)
tree8e54dc74a491c564575a47f85f79ae1d41c03059 /src
parent3c0fce7319f4e4ae076527eb98e6164210bdb34c (diff)
parent8507df99e5a3a77fa2a2f1c716e2eefeee92f11b (diff)
Merge pull request #30 from yuvadm/v6v6.0.0
Major v6 upgrade
Diffstat (limited to 'src')
-rw-r--r--src/Datetime.tsx (renamed from src/Datetime.js)12
-rw-r--r--src/Geshem.js94
-rw-r--r--src/Geshem.test.tsx (renamed from src/Geshem.test.js)2
-rw-r--r--src/Geshem.tsx80
-rw-r--r--src/Map.js65
-rw-r--r--src/Map.tsx99
-rw-r--r--src/Slider.js37
-rw-r--r--src/Slider.tsx45
-rw-r--r--src/config.tsx (renamed from src/config.js)10
-rw-r--r--src/index.js11
-rw-r--r--src/index.tsx13
-rw-r--r--src/react-app-env.d.ts1
-rw-r--r--src/serviceWorker.js135
-rw-r--r--src/setupTests.ts5
14 files changed, 257 insertions, 352 deletions
diff --git a/src/Datetime.js b/src/Datetime.tsx
index 9807211..0a8fbcf 100644
--- a/src/Datetime.js
+++ b/src/Datetime.tsx
@@ -1,12 +1,16 @@
import React from "react";
import { DateTime as LuxonDateTime } from "luxon";
-function DateTime(props) {
- let { images, slider } = props;
+interface DateTimeProps {
+ images: string[],
+ slider: number
+}
+
+export function DateTime({ images, slider }: DateTimeProps) {
let datetime = null;
if (images.length > 0) {
- const ds = images[slider].substr(5, 13);
+ const ds = images[slider].substring(5, 18);
datetime = LuxonDateTime.fromFormat(ds, "yyyyMMdd/HHmm", {
zone: "utc",
}).setZone("Asia/Jerusalem");
@@ -22,5 +26,3 @@ function DateTime(props) {
</div>
);
}
-
-export default DateTime;
diff --git a/src/Geshem.js b/src/Geshem.js
deleted file mode 100644
index 1eecdd0..0000000
--- a/src/Geshem.js
+++ /dev/null
@@ -1,94 +0,0 @@
-import React, { useState, useEffect } from "react";
-import { BrowserRouter as Router, Route } from "react-router-dom";
-
-import Map from "./Map";
-import Slider from "./Slider";
-import DateTime from "./Datetime";
-
-import { IMAGES_BASE_URL } from "./config";
-
-import "./Geshem.css";
-import "rc-slider/assets/index.css";
-
-function App() {
- return (
- <Router>
- <Route exact path="/" component={Geshem} />
- <Route path="/history/:date" component={Geshem} />
- </Router>
- );
-}
-
-function Geshem(props) {
- const [images, setImages] = useState([]);
- const [playback] = useState(
- props.match.params.date ||
- new URL(window.location).searchParams.get("history") ||
- false
- );
- const [slider, setSlider] = useState(playback ? 143 : 9);
-
- useEffect(() => {
- const fetchImages = async () =>
- fetch(`${IMAGES_BASE_URL}/imgs.json`)
- .then(res => res.json())
- .then(imgs => imgs["280"])
- .then(setImages);
-
- const buildPlayback = async () => {
- const date = playback;
- const hours = [...Array(24).keys()].map(
- h => `${String(h).padStart(2, "0")}`
- );
- const minutes = [...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;
- if (playback) buildPlayback();
- else {
- fetchImages();
- timer = setInterval(fetchImages, 60 * 1000);
- }
-
- return () => {
- if (timer !== undefined) clearInterval(timer);
- };
- }, [playback]);
-
- return (
- <>
- <Map images={images} slider={slider} />
- <DateTime images={images} slider={slider} />
- <Slider slider={slider} playback={playback} setSlider={setSlider} />
- </>
- );
-}
-
-function getGeolocation() {
- if (navigator.geolocation) {
- navigator.geolocation.getCurrentPosition(pos => {
- const { longitude, latitude } = pos.coords;
- this.setState({
- lng: longitude,
- lat: latitude,
- zoom: 11
- });
- if (this.state.mapLoaded) {
- this.map.jumpTo({
- center: [longitude, latitude],
- zoom: 11
- });
- }
- });
- }
-}
-
-export default App;
diff --git a/src/Geshem.test.js b/src/Geshem.test.tsx
index 185b1f4..8e4652d 100644
--- a/src/Geshem.test.js
+++ b/src/Geshem.test.tsx
@@ -1,6 +1,6 @@
import React from 'react';
import ReactDOM from 'react-dom';
-import Geshem from './Geshem';
+import { Geshem } from './Geshem';
it('renders without crashing', () => {
const div = document.createElement('div');
diff --git a/src/Geshem.tsx b/src/Geshem.tsx
new file mode 100644
index 0000000..6e88f19
--- /dev/null
+++ b/src/Geshem.tsx
@@ -0,0 +1,80 @@
+import React, { useState, useEffect } from "react";
+import { BrowserRouter, Route, Routes } from "react-router-dom";
+
+import { Map } from "./Map";
+import { Slider } from "./Slider";
+import { DateTime } from "./Datetime";
+
+import { IMAGES_BASE_URL, PLAYBACK_HOURS, PLAYBACK_SLOTS } from "./config";
+
+import "./Geshem.css";
+import "rc-slider/assets/index.css";
+
+export function App() {
+ return (
+ <BrowserRouter>
+ <Routes>
+ <Route path="/" element={<Geshem />} />
+ <Route path="/history/:date" element={<Geshem />} />
+ </Routes>
+ </BrowserRouter>
+ );
+}
+
+interface GeshemProps {
+ date?: string
+}
+
+
+export function Geshem({ date }: GeshemProps) {
+ const [images, setImages] = useState<string[]>([]);
+ const [playback] = useState(
+ date ||
+ new URL(window.location.toString()).searchParams.get("history") ||
+ 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["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<string[]>(
+ (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 (
+ <>
+ <Map images={images} slider={slider} />
+ <DateTime images={images} slider={slider} />
+ <Slider slider={slider} playback={playback} setSlider={setSlider} />
+ </>
+ );
+}
diff --git a/src/Map.js b/src/Map.js
deleted file mode 100644
index c39e1ac..0000000
--- a/src/Map.js
+++ /dev/null
@@ -1,65 +0,0 @@
-import React, { Fragment, useState } from "react";
-import ReactMapboxGl, { Layer, Source } from "react-mapbox-gl";
-
-import {
- MAPBOX_ACCESS_TOKEN,
- IMAGE_COORDINATES,
- IMAGES_BASE_URL
-} from "./config";
-
-import "mapbox-gl/dist/mapbox-gl.css";
-
-const Mapbox = ReactMapboxGl({
- accessToken: MAPBOX_ACCESS_TOKEN,
- minZoom: 5,
- maxZoom: 10,
- hash: false
-});
-
-function Map(props) {
- const [center] = useState([35, 31.9]);
- const [zoom] = useState([6.3]);
-
- return (
- <Mapbox
- style="mapbox://styles/mapbox/dark-v9"
- center={center}
- zoom={zoom}
- containerStyle={{
- height: "100vh",
- width: "100vw"
- }}
- >
- {props.images.map((img, i) => {
- const id = `radar-280-${i}`;
- return (
- <Fragment key={`image-${id}`}>
- <Source
- id={id}
- key={`source-${id}`}
- tileJsonSource={{
- type: "image",
- url: `${IMAGES_BASE_URL}/${img}`,
- coordinates: IMAGE_COORDINATES
- }}
- />
- <Layer
- id={id}
- key={`layer-${id}`}
- sourceId={id}
- type="raster"
- paint={{
- "raster-opacity": i === props.slider ? 0.85 : 0,
- "raster-opacity-transition": {
- duration: 0
- }
- }}
- />
- </Fragment>
- );
- })}
- </Mapbox>
- );
-}
-
-export default Map;
diff --git a/src/Map.tsx b/src/Map.tsx
new file mode 100644
index 0000000..39f28fa
--- /dev/null
+++ b/src/Map.tsx
@@ -0,0 +1,99 @@
+import React, { useState, useEffect, useRef } from "react";
+// @ts-ignore
+import mapboxgl from '!mapbox-gl'; // eslint-disable-line import/no-webpack-loader-syntax
+
+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<mapboxgl.Map>(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) 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);
+ });
+ });
+
+ useEffect(() => {
+ if (!loaded) return;
+
+ // remove old layers
+ prevImages.forEach((img) => {
+ if (!images.includes(img)) {
+ map.current.removeLayer(`layer-${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) return;
+ images[slider] && map.current.setPaintProperty(`layer-${images[slider]}`, "raster-opacity", 0.85);
+ return () => {
+ // callback will hide the previous layer with the previous slider value
+ map.current.setPaintProperty(`layer-${images[slider]}`, "raster-opacity", 0);
+ }
+ }, [loaded, slider, images]);
+
+ return (
+ <div>
+ <div ref={mapContainer} className="map-container" style={{
+ height: "100vh",
+ width: "100vw"
+ }} />
+ </div>
+ );
+}
diff --git a/src/Slider.js b/src/Slider.js
deleted file mode 100644
index 9e86f66..0000000
--- a/src/Slider.js
+++ /dev/null
@@ -1,37 +0,0 @@
-import React from "react";
-import RcSlider from "rc-slider";
-
-function Slider(props) {
- const handleStyle = {
- height: 40,
- width: 40,
- border: 0,
- marginTop: -10,
- boxShadow: ".5px .5px 2px 1px rgba(0,0,0,.32)"
- };
-
- const railStyle = {
- height: 20,
- backgroundColor: "#3498db"
- };
-
- const trackStyle = {
- display: "none"
- };
-
- return (
- <div id="slider">
- <RcSlider
- mix={0}
- max={props.playback ? 143 : 9}
- defaultValue={props.slider}
- handleStyle={handleStyle}
- railStyle={railStyle}
- trackStyle={trackStyle}
- onChange={val => props.setSlider(val)}
- />
- </div>
- );
-}
-
-export default Slider;
diff --git a/src/Slider.tsx b/src/Slider.tsx
new file mode 100644
index 0000000..50223fc
--- /dev/null
+++ b/src/Slider.tsx
@@ -0,0 +1,45 @@
+import React from "react";
+import RcSlider, { SliderProps, SliderRef } from 'rc-slider/lib/Slider';
+import { PLAYBACK_SLOTS } from "./config";
+
+// use workaround from: https://github.com/react-component/slider/issues/835#issuecomment-1201805736
+const CustomSlider = RcSlider as React.ForwardRefExoticComponent<SliderProps<number> & React.RefAttributes<SliderRef>>;
+
+interface GeshemSliderProps {
+ playback?: string,
+ slider: number,
+ setSlider: React.Dispatch<React.SetStateAction<number>>
+}
+
+export function Slider({ playback, slider, setSlider }: GeshemSliderProps) {
+ const handleStyle = {
+ height: 40,
+ width: 40,
+ border: 0,
+ marginTop: -10,
+ boxShadow: ".5px .5px 2px 1px rgba(0,0,0,.32)"
+ };
+
+ const railStyle = {
+ height: 20,
+ backgroundColor: "#3498db"
+ };
+
+ const trackStyle = {
+ display: "none"
+ };
+
+ return (
+ <div id="slider">
+ <CustomSlider
+ min={0}
+ max={playback ? PLAYBACK_SLOTS : 9}
+ defaultValue={slider}
+ handleStyle={handleStyle}
+ railStyle={railStyle}
+ trackStyle={trackStyle}
+ onChange={val => setSlider(val)}
+ />
+ </div>
+ );
+}
diff --git a/src/config.js b/src/config.tsx
index f74382c..4ca4578 100644
--- a/src/config.js
+++ b/src/config.tsx
@@ -2,16 +2,18 @@ const hostname = window && window.location && window.location.hostname;
const PRODUCTION = hostname.includes("geshem");
-const IMAGES_BASE_URL = PRODUCTION ? "https://imgs.geshem.space" : "";
+export const IMAGES_BASE_URL = PRODUCTION ? "https://imgs.geshem.space" : "";
-const MAPBOX_ACCESS_TOKEN =
+export const MAPBOX_ACCESS_TOKEN =
"pk.eyJ1IjoieXV2YWRtIiwiYSI6ImNpcnMxbzBuaTAwZWdoa25oczlzZmkwbHcifQ.UHtLngbKm9O8945pJm23Nw";
-const IMAGE_COORDINATES = [
+export const IMAGE_COORDINATES = [
[31.7503896894, 34.4878044232],
[37.8574239563, 34.5078463729],
[37.7157066403, 29.4538271687],
[31.9347389087, 29.4373462909]
];
-module.exports = { IMAGES_BASE_URL, MAPBOX_ACCESS_TOKEN, IMAGE_COORDINATES };
+export const PLAYBACK_HOURS = 2;
+
+export const PLAYBACK_SLOTS = (PLAYBACK_HOURS * 6) - 1; \ No newline at end of file
diff --git a/src/index.js b/src/index.js
deleted file mode 100644
index dbc6663..0000000
--- a/src/index.js
+++ /dev/null
@@ -1,11 +0,0 @@
-import React from 'react';
-import ReactDOM from 'react-dom';
-import './index.css';
-import App from './Geshem';
-import * as serviceWorker from './serviceWorker';
-
-console.log()
-
-ReactDOM.render(<App />, document.getElementById('root'));
-
-serviceWorker.unregister();
diff --git a/src/index.tsx b/src/index.tsx
new file mode 100644
index 0000000..353cebb
--- /dev/null
+++ b/src/index.tsx
@@ -0,0 +1,13 @@
+import React from 'react';
+import ReactDOM from 'react-dom/client';
+import './index.css';
+import { App } from './Geshem';
+
+console.log()
+
+const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement);
+root.render(
+ <React.StrictMode>
+ <App />
+ </React.StrictMode >
+); \ No newline at end of file
diff --git a/src/react-app-env.d.ts b/src/react-app-env.d.ts
new file mode 100644
index 0000000..6431bc5
--- /dev/null
+++ b/src/react-app-env.d.ts
@@ -0,0 +1 @@
+/// <reference types="react-scripts" />
diff --git a/src/serviceWorker.js b/src/serviceWorker.js
deleted file mode 100644
index 2283ff9..0000000
--- a/src/serviceWorker.js
+++ /dev/null
@@ -1,135 +0,0 @@
-// This optional code is used to register a service worker.
-// register() is not called by default.
-
-// This lets the app load faster on subsequent visits in production, and gives
-// it offline capabilities. However, it also means that developers (and users)
-// will only see deployed updates on subsequent visits to a page, after all the
-// existing tabs open on the page have been closed, since previously cached
-// resources are updated in the background.
-
-// To learn more about the benefits of this model and instructions on how to
-// opt-in, read http://bit.ly/CRA-PWA
-
-const isLocalhost = Boolean(
- window.location.hostname === 'localhost' ||
- // [::1] is the IPv6 localhost address.
- window.location.hostname === '[::1]' ||
- // 127.0.0.1/8 is considered localhost for IPv4.
- window.location.hostname.match(
- /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
- )
-);
-
-export function register(config) {
- if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
- // The URL constructor is available in all browsers that support SW.
- const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href);
- if (publicUrl.origin !== window.location.origin) {
- // Our service worker won't work if PUBLIC_URL is on a different origin
- // from what our page is served on. This might happen if a CDN is used to
- // serve assets; see https://github.com/facebook/create-react-app/issues/2374
- return;
- }
-
- window.addEventListener('load', () => {
- const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
-
- if (isLocalhost) {
- // This is running on localhost. Let's check if a service worker still exists or not.
- checkValidServiceWorker(swUrl, config);
-
- // Add some additional logging to localhost, pointing developers to the
- // service worker/PWA documentation.
- navigator.serviceWorker.ready.then(() => {
- console.log(
- 'This web app is being served cache-first by a service ' +
- 'worker. To learn more, visit http://bit.ly/CRA-PWA'
- );
- });
- } else {
- // Is not localhost. Just register service worker
- registerValidSW(swUrl, config);
- }
- });
- }
-}
-
-function registerValidSW(swUrl, config) {
- navigator.serviceWorker
- .register(swUrl)
- .then(registration => {
- registration.onupdatefound = () => {
- const installingWorker = registration.installing;
- if (installingWorker == null) {
- return;
- }
- installingWorker.onstatechange = () => {
- if (installingWorker.state === 'installed') {
- if (navigator.serviceWorker.controller) {
- // At this point, the updated precached content has been fetched,
- // but the previous service worker will still serve the older
- // content until all client tabs are closed.
- console.log(
- 'New content is available and will be used when all ' +
- 'tabs for this page are closed. See http://bit.ly/CRA-PWA.'
- );
-
- // Execute callback
- if (config && config.onUpdate) {
- config.onUpdate(registration);
- }
- } else {
- // At this point, everything has been precached.
- // It's the perfect time to display a
- // "Content is cached for offline use." message.
- console.log('Content is cached for offline use.');
-
- // Execute callback
- if (config && config.onSuccess) {
- config.onSuccess(registration);
- }
- }
- }
- };
- };
- })
- .catch(error => {
- console.error('Error during service worker registration:', error);
- });
-}
-
-function checkValidServiceWorker(swUrl, config) {
- // Check if the service worker can be found. If it can't reload the page.
- fetch(swUrl)
- .then(response => {
- // Ensure service worker exists, and that we really are getting a JS file.
- const contentType = response.headers.get('content-type');
- if (
- response.status === 404 ||
- (contentType != null && contentType.indexOf('javascript') === -1)
- ) {
- // No service worker found. Probably a different app. Reload the page.
- navigator.serviceWorker.ready.then(registration => {
- registration.unregister().then(() => {
- window.location.reload();
- });
- });
- } else {
- // Service worker found. Proceed as normal.
- registerValidSW(swUrl, config);
- }
- })
- .catch(() => {
- console.log(
- 'No internet connection found. App is running in offline mode.'
- );
- });
-}
-
-export function unregister() {
- if ('serviceWorker' in navigator) {
- navigator.serviceWorker.ready.then(registration => {
- registration.unregister();
- });
- }
-}
diff --git a/src/setupTests.ts b/src/setupTests.ts
new file mode 100644
index 0000000..8f2609b
--- /dev/null
+++ b/src/setupTests.ts
@@ -0,0 +1,5 @@
+// jest-dom adds custom jest matchers for asserting on DOM nodes.
+// allows you to do things like:
+// expect(element).toHaveTextContent(/react/i)
+// learn more: https://github.com/testing-library/jest-dom
+import '@testing-library/jest-dom';