summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
authorYuval Adam <_@yuv.al>2024-09-23 10:29:53 +0200
committerYuval Adam <_@yuv.al>2024-09-23 10:29:53 +0200
commit8a6ff5a2c15422badf0bc9c8166787dc23a71072 (patch)
treeda45cc6fc598520102260da87c3733107ef4e980 /app
parent51370d591d4f2ac4eddd8b2ea2f9c5118fbf9ab1 (diff)
Move all geshem/ files to root
Diffstat (limited to 'app')
-rw-r--r--app/components/Datetime.tsx28
-rw-r--r--app/components/Geshem.tsx80
-rw-r--r--app/components/Map.tsx99
-rw-r--r--app/components/Slider.tsx45
-rw-r--r--app/components/config.tsx19
-rw-r--r--app/favicon.icobin0 -> 32038 bytes
-rw-r--r--app/fonts/GeistMonoVF.woffbin0 -> 67864 bytes
-rw-r--r--app/fonts/GeistVF.woffbin0 -> 66268 bytes
-rw-r--r--app/globals.css58
-rw-r--r--app/layout.tsx35
-rw-r--r--app/page.tsx6
-rw-r--r--app/public/apple-touch-icon.pngbin0 -> 1059 bytes
-rw-r--r--app/public/manifest.json15
-rw-r--r--app/public/privacy.html30
-rw-r--r--app/public/screenshot.pngbin0 -> 382294 bytes
15 files changed, 415 insertions, 0 deletions
diff --git a/app/components/Datetime.tsx b/app/components/Datetime.tsx
new file mode 100644
index 0000000..0a8fbcf
--- /dev/null
+++ b/app/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 = null;
+
+ if (images.length > 0) {
+ 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 (
+ <div id="datetime">
+ <div id="date">{date}</div>
+ <div id="time">{time}</div>
+ </div>
+ );
+}
diff --git a/app/components/Geshem.tsx b/app/components/Geshem.tsx
new file mode 100644
index 0000000..50659be
--- /dev/null
+++ b/app/components/Geshem.tsx
@@ -0,0 +1,80 @@
+"use client"
+
+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/app/components/Map.tsx b/app/components/Map.tsx
new file mode 100644
index 0000000..39f28fa
--- /dev/null
+++ b/app/components/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/app/components/Slider.tsx b/app/components/Slider.tsx
new file mode 100644
index 0000000..50223fc
--- /dev/null
+++ b/app/components/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/app/components/config.tsx b/app/components/config.tsx
new file mode 100644
index 0000000..4dc3d6a
--- /dev/null
+++ b/app/components/config.tsx
@@ -0,0 +1,19 @@
+const hostname = window !== undefined && window.location && window.location.hostname;
+
+const PRODUCTION = hostname && hostname.includes("geshem");
+
+export const IMAGES_BASE_URL = PRODUCTION ? "https://imgs.geshem.space" : "";
+
+export const MAPBOX_ACCESS_TOKEN =
+ "pk.eyJ1IjoieXV2YWRtIiwiYSI6ImNpcnMxbzBuaTAwZWdoa25oczlzZmkwbHcifQ.UHtLngbKm9O8945pJm23Nw";
+
+export const IMAGE_COORDINATES = [
+ [31.7503896894, 34.4878044232],
+ [37.8574239563, 34.5078463729],
+ [37.7157066403, 29.4538271687],
+ [31.9347389087, 29.4373462909]
+];
+
+export const PLAYBACK_HOURS = 2;
+
+export const PLAYBACK_SLOTS = (PLAYBACK_HOURS * 6) - 1; \ No newline at end of file
diff --git a/app/favicon.ico b/app/favicon.ico
new file mode 100644
index 0000000..a97277e
--- /dev/null
+++ b/app/favicon.ico
Binary files differ
diff --git a/app/fonts/GeistMonoVF.woff b/app/fonts/GeistMonoVF.woff
new file mode 100644
index 0000000..f2ae185
--- /dev/null
+++ b/app/fonts/GeistMonoVF.woff
Binary files differ
diff --git a/app/fonts/GeistVF.woff b/app/fonts/GeistVF.woff
new file mode 100644
index 0000000..1b62daa
--- /dev/null
+++ b/app/fonts/GeistVF.woff
Binary files differ
diff --git a/app/globals.css b/app/globals.css
new file mode 100644
index 0000000..62b9a76
--- /dev/null
+++ b/app/globals.css
@@ -0,0 +1,58 @@
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
+
+:root {
+ --background: #ffffff;
+ --foreground: #171717;
+}
+
+@media (prefers-color-scheme: dark) {
+ :root {
+ --background: #0a0a0a;
+ --foreground: #ededed;
+ }
+}
+
+body {
+ color: var(--foreground);
+ background: var(--background);
+ font-family: Arial, Helvetica, sans-serif;
+}
+
+@layer utilities {
+ .text-balance {
+ text-wrap: balance;
+ }
+}
+
+#datetime {
+ position: absolute;
+ top: 20px;
+ left: 20px;
+ color: white;
+ font-family: monospace;
+}
+
+#date {
+ font-size: 1.4em;
+}
+
+#time {
+ font-size: 2.8em;
+}
+
+#slider {
+ position: fixed;
+ bottom: 8vh;
+ width: 30vw;
+ margin-left: 35vw;
+}
+
+@media only screen and (max-device-width: 812px) {
+ #slider {
+ width: 80vw;
+ margin-left: 8vw;
+ z-index: 10;
+ }
+} \ No newline at end of file
diff --git a/app/layout.tsx b/app/layout.tsx
new file mode 100644
index 0000000..14a4e0c
--- /dev/null
+++ b/app/layout.tsx
@@ -0,0 +1,35 @@
+import type { Metadata } from "next";
+import localFont from "next/font/local";
+import "./globals.css";
+
+const geistSans = localFont({
+ src: "./fonts/GeistVF.woff",
+ variable: "--font-geist-sans",
+ weight: "100 900",
+});
+const geistMono = localFont({
+ src: "./fonts/GeistMonoVF.woff",
+ variable: "--font-geist-mono",
+ weight: "100 900",
+});
+
+export const metadata: Metadata = {
+ title: "Geshem.space | Real-Time Israel Rain Radar Visualization",
+ description: "Track Israel's rainfall in real-time with Geshem.space's advanced radar visualizer. Get accurate, up-to-date precipitation data and forecasts for precise weather monitoring across Israel.",
+};
+
+export default function RootLayout({
+ children,
+}: Readonly<{
+ children: React.ReactNode;
+}>) {
+ return (
+ <html lang="en">
+ <body
+ className={`${geistSans.variable} ${geistMono.variable} antialiased`}
+ >
+ {children}
+ </body>
+ </html>
+ );
+}
diff --git a/app/page.tsx b/app/page.tsx
new file mode 100644
index 0000000..a44da7b
--- /dev/null
+++ b/app/page.tsx
@@ -0,0 +1,6 @@
+import { Geshem } from "@/app/components/Geshem";
+export default function Home() {
+ return (
+ <Geshem />
+ );
+}
diff --git a/app/public/apple-touch-icon.png b/app/public/apple-touch-icon.png
new file mode 100644
index 0000000..7c93f47
--- /dev/null
+++ b/app/public/apple-touch-icon.png
Binary files differ
diff --git a/app/public/manifest.json b/app/public/manifest.json
new file mode 100644
index 0000000..20d2b0d
--- /dev/null
+++ b/app/public/manifest.json
@@ -0,0 +1,15 @@
+{
+ "short_name": "Geshem",
+ "name": "Geshem.space Rain Radar",
+ "icons": [
+ {
+ "src": "favicon.ico",
+ "sizes": "64x64 32x32 24x24 16x16",
+ "type": "image/x-icon"
+ }
+ ],
+ "start_url": ".",
+ "display": "standalone",
+ "theme_color": "#000000",
+ "background_color": "#ffffff"
+}
diff --git a/app/public/privacy.html b/app/public/privacy.html
new file mode 100644
index 0000000..3976c2c
--- /dev/null
+++ b/app/public/privacy.html
@@ -0,0 +1,30 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
+ <meta name="apple-mobile-web-app-title" content="Geshem">
+ <meta name="apple-mobile-web-app-capable" content="yes">
+ <meta name="theme-color" content="#000000">
+ <link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico">
+ <link rel="apple-touch-icon" href="%PUBLIC_URL%/apple-touch-icon.png">
+ <link rel="manifest" href="%PUBLIC_URL%/manifest.json">
+ <title>Privacy Policy | geshem.space</title>
+ <script src="https://turtle.geshem.space/script.js" site="MBXPLVRM" defer></script>
+ <style>
+ body{
+ overflow: hidden;
+ position: absolute;
+ width: 100%;
+ height: 100%;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ font-family: sans-serif;
+ }
+ </style>
+ </head>
+ <body>
+ <h1>We do not collect any data.</h1>
+ </body>
+</html>
diff --git a/app/public/screenshot.png b/app/public/screenshot.png
new file mode 100644
index 0000000..7fd104c
--- /dev/null
+++ b/app/public/screenshot.png
Binary files differ