diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/Datetime.tsx (renamed from src/Datetime.js) | 12 | ||||
| -rw-r--r-- | src/Geshem.js | 94 | ||||
| -rw-r--r-- | src/Geshem.test.tsx (renamed from src/Geshem.test.js) | 2 | ||||
| -rw-r--r-- | src/Geshem.tsx | 80 | ||||
| -rw-r--r-- | src/Map.js | 65 | ||||
| -rw-r--r-- | src/Map.tsx | 99 | ||||
| -rw-r--r-- | src/Slider.js | 37 | ||||
| -rw-r--r-- | src/Slider.tsx | 45 | ||||
| -rw-r--r-- | src/config.tsx (renamed from src/config.js) | 10 | ||||
| -rw-r--r-- | src/index.js | 11 | ||||
| -rw-r--r-- | src/index.tsx | 13 | ||||
| -rw-r--r-- | src/react-app-env.d.ts | 1 | ||||
| -rw-r--r-- | src/serviceWorker.js | 135 | ||||
| -rw-r--r-- | src/setupTests.ts | 5 |
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'; |
