summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/Datetime.tsx (renamed from src/components/Datetime.tsx)16
-rw-r--r--src/Geshem.css30
-rw-r--r--src/Geshem.test.tsx9
-rw-r--r--src/Geshem.tsx (renamed from src/components/Geshem.tsx)31
-rw-r--r--src/Map.tsx (renamed from src/components/Map.tsx)54
-rw-r--r--src/Slider.tsx45
-rw-r--r--src/components/Slider.tsx86
-rw-r--r--src/config.tsx (renamed from src/config.ts)6
-rw-r--r--src/functions/scheduled.ts178
-rw-r--r--src/index.css (renamed from src/styles/global.css)4
-rw-r--r--src/index.tsx13
-rw-r--r--src/layouts/Layout.astro28
-rw-r--r--src/pages/404.astro67
-rw-r--r--src/pages/api/update.ts200
-rw-r--r--src/pages/history/[date].astro13
-rw-r--r--src/pages/index.astro11
-rw-r--r--src/pages/privacy.astro40
-rw-r--r--src/react-app-env.d.ts1
-rw-r--r--src/setupTests.ts5
19 files changed, 162 insertions, 675 deletions
diff --git a/src/components/Datetime.tsx b/src/Datetime.tsx
index 8b66a4f..0a8fbcf 100644
--- a/src/components/Datetime.tsx
+++ b/src/Datetime.tsx
@@ -2,14 +2,14 @@ import React from "react";
import { DateTime as LuxonDateTime } from "luxon";
interface DateTimeProps {
- images: string[];
- slider: number;
+ images: string[],
+ slider: number
}
export function DateTime({ images, slider }: DateTimeProps) {
- let datetime: LuxonDateTime | null = null;
+ let datetime = null;
- if (images.length > 0 && images[slider]) {
+ if (images.length > 0) {
const ds = images[slider].substring(5, 18);
datetime = LuxonDateTime.fromFormat(ds, "yyyyMMdd/HHmm", {
zone: "utc",
@@ -20,9 +20,9 @@ export function DateTime({ images, slider }: DateTimeProps) {
const time = datetime ? datetime.toFormat("HH:mm") : "";
return (
- <div id="datetime" className="absolute top-5 left-5 text-white font-mono">
- <div id="date" className="text-xl">{date}</div>
- <div id="time" className="text-4xl">{time}</div>
+ <div id="datetime">
+ <div id="date">{date}</div>
+ <div id="time">{time}</div>
</div>
);
-} \ No newline at end of file
+}
diff --git a/src/Geshem.css b/src/Geshem.css
new file mode 100644
index 0000000..2e0f58e
--- /dev/null
+++ b/src/Geshem.css
@@ -0,0 +1,30 @@
+#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;
+ }
+}
diff --git a/src/Geshem.test.tsx b/src/Geshem.test.tsx
new file mode 100644
index 0000000..8e4652d
--- /dev/null
+++ b/src/Geshem.test.tsx
@@ -0,0 +1,9 @@
+import React from 'react';
+import ReactDOM from 'react-dom';
+import { Geshem } from './Geshem';
+
+it('renders without crashing', () => {
+ const div = document.createElement('div');
+ ReactDOM.render(<Geshem />, div);
+ ReactDOM.unmountComponentAtNode(div);
+});
diff --git a/src/components/Geshem.tsx b/src/Geshem.tsx
index 982d841..6e88f19 100644
--- a/src/components/Geshem.tsx
+++ b/src/Geshem.tsx
@@ -1,20 +1,36 @@
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 { 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;
+ date?: string
}
+
export function Geshem({ date }: GeshemProps) {
const [images, setImages] = useState<string[]>([]);
const [playback] = useState(
date ||
- (typeof window !== 'undefined' ? new URL(window.location.toString()).searchParams.get("history") : null) ||
+ new URL(window.location.toString()).searchParams.get("history") ||
undefined
);
const [slider, setSlider] = useState(playback ? PLAYBACK_SLOTS : 9);
@@ -23,7 +39,7 @@ export function Geshem({ date }: GeshemProps) {
const fetchImages = async () =>
fetch(`${IMAGES_BASE_URL}/imgs.json`)
.then(res => res.json())
- .then(imgs => (imgs as any)["280"])
+ .then(imgs => imgs["280"])
.then(setImages);
const buildPlayback = async () => {
@@ -43,9 +59,8 @@ export function Geshem({ date }: GeshemProps) {
};
let timer: number;
- if (playback) {
- buildPlayback();
- } else {
+ if (playback) buildPlayback();
+ else {
fetchImages();
timer = window.setInterval(fetchImages, 60 * 1000);
}
@@ -62,4 +77,4 @@ export function Geshem({ date }: GeshemProps) {
<Slider slider={slider} playback={playback} setSlider={setSlider} />
</>
);
-} \ No newline at end of file
+}
diff --git a/src/components/Map.tsx b/src/Map.tsx
index 070dc5c..39f28fa 100644
--- a/src/components/Map.tsx
+++ b/src/Map.tsx
@@ -1,22 +1,23 @@
import React, { useState, useEffect, useRef } from "react";
-import mapboxgl from 'mapbox-gl';
+// @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";
+} from "./config";
import "mapbox-gl/dist/mapbox-gl.css";
interface MapProps {
- slider: number;
- images: string[];
+ slider: number,
+ images: string[],
}
export function Map({ slider, images }: MapProps) {
- const mapContainer = useRef<HTMLDivElement>(null);
- const map = useRef<mapboxgl.Map | null>(null);
+ const mapContainer = useRef(null);
+ const map = useRef<mapboxgl.Map>(null);
const [lng] = useState(35);
const [lat] = useState(31.9);
@@ -26,7 +27,7 @@ export function Map({ slider, images }: MapProps) {
const prevImages = useRef(images).current;
useEffect(() => {
- if (map.current || !mapContainer.current) return;
+ if (map.current) return;
map.current = new mapboxgl.Map({
accessToken: MAPBOX_ACCESS_TOKEN,
@@ -42,32 +43,28 @@ export function Map({ slider, images }: MapProps) {
map.current.on("style.load", () => {
setLoaded(true);
});
- }, [lng, lat, zoom]);
+ });
useEffect(() => {
- if (!loaded || !map.current) return;
+ if (!loaded) 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}`);
- }
+ 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}`, {
+ 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({
+ map.current.addLayer({
id: `layer-${img}`,
source: `source-${img}`,
type: "raster",
@@ -83,23 +80,20 @@ export function Map({ slider, images }: MapProps) {
}, [loaded, prevImages, images]);
useEffect(() => {
- if (!loaded || !images.length || !map.current) return;
-
- if (images[slider]) {
- map.current.setPaintProperty(`layer-${images[slider]}`, "raster-opacity", 0.85);
- }
-
+ 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
- if (images[slider] && map.current?.getLayer(`layer-${images[slider]}`)) {
- map.current.setPaintProperty(`layer-${images[slider]}`, "raster-opacity", 0);
- }
- };
+ map.current.setPaintProperty(`layer-${images[slider]}`, "raster-opacity", 0);
+ }
}, [loaded, slider, images]);
return (
<div>
- <div ref={mapContainer} className="map-container h-screen w-screen" />
+ <div ref={mapContainer} className="map-container" style={{
+ height: "100vh",
+ width: "100vw"
+ }} />
</div>
);
-} \ No newline at end of file
+}
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/components/Slider.tsx b/src/components/Slider.tsx
deleted file mode 100644
index 22a8f8f..0000000
--- a/src/components/Slider.tsx
+++ /dev/null
@@ -1,86 +0,0 @@
-import React, { useCallback, useRef, useState } from "react";
-import { PLAYBACK_SLOTS } from "../config";
-
-interface SliderProps {
- playback?: string;
- slider: number;
- setSlider: React.Dispatch<React.SetStateAction<number>>;
-}
-
-export function Slider({ playback, slider, setSlider }: SliderProps) {
- const sliderRef = useRef<HTMLDivElement>(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 (
- <div id="slider" className="py-2.5 fixed bottom-[8vh] w-[30vw] ml-[35vw] max-[812px]:w-[80vw] max-[812px]:ml-[8vw] max-[812px]:z-10">
- <div
- ref={sliderRef}
- className="relative h-5 bg-blue-500 rounded-full cursor-pointer touch-none"
- onMouseDown={handleStart}
- onTouchStart={handleStart}
- >
- <div
- className={`absolute -top-2.5 w-10 h-10 bg-white rounded-full shadow-md z-[1] ${
- isDragging ? 'cursor-grabbing' : 'cursor-grab'
- }`}
- style={{
- left: `calc(${percentage}% - 20px)`
- }}
- />
- </div>
- </div>
- );
-} \ No newline at end of file
diff --git a/src/config.ts b/src/config.tsx
index 615e6d4..4ca4578 100644
--- a/src/config.ts
+++ b/src/config.tsx
@@ -1,13 +1,13 @@
-const hostname = typeof window !== 'undefined' && window.location && window.location.hostname;
+const hostname = window && window.location && window.location.hostname;
-const PRODUCTION = true;
+const PRODUCTION = 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: [number, number][] = [
+export const IMAGE_COORDINATES = [
[31.7503896894, 34.4878044232],
[37.8574239563, 34.5078463729],
[37.7157066403, 29.4538271687],
diff --git a/src/functions/scheduled.ts b/src/functions/scheduled.ts
deleted file mode 100644
index 5aa3fa0..0000000
--- a/src/functions/scheduled.ts
+++ /dev/null
@@ -1,178 +0,0 @@
-// Cloudflare Workers scheduled handler for cron jobs
-import type { ScheduledController, ScheduledEvent } from '@cloudflare/workers-types';
-
-interface Env {
- BASE_URL: string;
- AUTH_USER: string;
- AUTH_PASS: string;
- IMGS_BUCKET: R2Bucket;
-}
-
-interface ImageData {
- [key: string]: string[];
-}
-
-class GeshemUpdate {
- private bucket: R2Bucket;
- private env: Env;
-
- constructor(bucket: R2Bucket, env: Env) {
- this.bucket = bucket;
- this.env = env;
- }
-
- private async authenticate(request: Request): Promise<Request> {
- const headers = new Headers(request.headers);
- const credentials = btoa(`${this.env.AUTH_USER}:${this.env.AUTH_PASS}`);
- headers.set('Authorization', `Basic ${credentials}`);
-
- return new Request(request.url, {
- method: request.method,
- headers,
- body: request.body
- });
- }
-
- private async getLatestImages(): Promise<string[]> {
- const authenticatedRequest = await this.authenticate(new Request(this.env.BASE_URL));
- const response = await fetch(authenticatedRequest);
- const html = await response.text();
-
- const regex = /radar280comp_\d+\.png/g;
- const matches = html.match(regex) || [];
- const uniqueImages = [...new Set(matches)];
-
- return uniqueImages.sort().slice(-10);
- }
-
- private async getLatestBucketKeys(): Promise<string[]> {
- const yesterday = new Date();
- yesterday.setDate(yesterday.getDate() - 1);
- const yesterdayStr = yesterday.toISOString().slice(0, 10).replace(/-/g, '');
-
- try {
- const objects = await this.bucket.list({
- prefix: 'imgs/',
- startAfter: `imgs/${yesterdayStr}`
- });
-
- return objects.objects.map(obj => obj.key);
- } catch (error) {
- console.error('Error listing bucket objects:', error);
- return [];
- }
- }
-
- private async fetchMissingImages(): Promise<boolean> {
- const images = await this.getLatestImages();
- const s3Images = await this.getLatestBucketKeys();
- let updated = false;
-
- for (const img of images) {
- const key = this.keyFromFilename(img);
- if (!s3Images.includes(key)) {
- await this.fetchImage(img, key);
- updated = true;
- }
- }
-
- return updated;
- }
-
- private async fetchImage(imgName: string, key: string): Promise<void> {
- console.log(`Downloading ${imgName} from web server`);
-
- const imageUrl = `${this.env.BASE_URL}/${imgName}`;
- const authenticatedRequest = await this.authenticate(new Request(imageUrl));
- const response = await fetch(authenticatedRequest);
-
- if (!response.ok) {
- throw new Error(`Failed to fetch image: ${response.statusText}`);
- }
-
- const imageData = await response.arrayBuffer();
-
- console.log(`Uploading to ${key}`);
- await this.bucket.put(key, imageData, {
- httpMetadata: {
- contentType: 'image/png',
- cacheControl: 'public, max-age=31536000'
- }
- });
- }
-
- private keyFromFilename(filename: string): string {
- const [name, dateStr] = filename.split('.')[0].split('_');
- const res = 280;
-
- // Parse date: YYYYMMDDHHMM
- const year = dateStr.slice(0, 4);
- const month = dateStr.slice(4, 6);
- const day = dateStr.slice(6, 8);
- const hour = dateStr.slice(8, 10);
- const minute = dateStr.slice(10, 12);
-
- const d = `${year}${month}${day}`;
- const t = `${hour}${minute}`;
-
- return `imgs/${d}/${t}/${res}.png`;
- }
-
- private async generateJson(): Promise<void> {
- const latestKeys = await this.getLatestBucketKeys();
- const keys = latestKeys
- .filter(key => key.endsWith('280.png'))
- .sort()
- .slice(-10);
-
- const index: ImageData = { '280': keys };
-
- await this.bucket.put('imgs.json', JSON.stringify(index), {
- httpMetadata: {
- contentType: 'application/json',
- cacheControl: 'public, max-age=60'
- }
- });
- }
-
- async run(): Promise<string> {
- try {
- const updated = await this.fetchMissingImages();
- if (updated) {
- await this.generateJson();
- }
- return `Updated: ${updated}`;
- } catch (error) {
- console.error('Error in GeshemUpdate.run():', error);
- throw error;
- }
- }
-}
-
-export default {
- async scheduled(
- controller: ScheduledController,
- env: Env,
- ctx: ExecutionContext
- ): Promise<void> {
- console.log('Cron job triggered at', new Date().toISOString());
-
- try {
- if (!env?.BASE_URL || !env?.AUTH_USER || !env?.AUTH_PASS) {
- throw new Error('Missing environment variables');
- }
-
- if (!env.IMGS_BUCKET) {
- throw new Error('R2 bucket not configured');
- }
-
- const updater = new GeshemUpdate(env.IMGS_BUCKET, env);
- const result = await updater.run();
-
- console.log(`Cron job completed: ${result}`);
- } catch (error) {
- console.error('Error in scheduled function:', error);
- throw error;
- }
- }
-}; \ No newline at end of file
diff --git a/src/styles/global.css b/src/index.css
index c81f9a9..822eb36 100644
--- a/src/styles/global.css
+++ b/src/index.css
@@ -1,5 +1,3 @@
-@import "tailwindcss";
-
body {
margin: 0;
padding: 0;
@@ -13,4 +11,4 @@ body {
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
-} \ No newline at end of file
+}
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/layouts/Layout.astro b/src/layouts/Layout.astro
deleted file mode 100644
index e983a6d..0000000
--- a/src/layouts/Layout.astro
+++ /dev/null
@@ -1,28 +0,0 @@
----
-export interface Props {
- title?: string;
- description?: string;
-}
-
-const { title = "geshem.space", description = "geshem.space - Israel Rain Radar" } = Astro.props;
----
-
-<!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="/favicon.ico">
- <link rel="apple-touch-icon" href="/apple-touch-icon.png">
- <link rel="manifest" href="/manifest.json">
- <title>{title}</title>
- <meta name="description" content={description}>
- <script src="https://cdn.usefathom.com/script.js" data-site="MBXPLVRM" defer></script>
- </head>
- <body>
- <slot />
- </body>
-</html> \ No newline at end of file
diff --git a/src/pages/404.astro b/src/pages/404.astro
deleted file mode 100644
index 4db6121..0000000
--- a/src/pages/404.astro
+++ /dev/null
@@ -1,67 +0,0 @@
----
----
-
-<html lang="en">
- <head>
- <meta charset="utf-8">
- <meta name="viewport" content="width=device-width, initial-scale=1">
- <link rel="shortcut icon" href="/favicon.ico">
- <title>404 - Page Not Found | geshem.space</title>
- <style>
- body {
- margin: 0;
- padding: 0;
- background-color: #1a1a1a;
- color: white;
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
- "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif;
- display: flex;
- justify-content: center;
- align-items: center;
- height: 100vh;
- text-align: center;
- }
-
- .container {
- max-width: 500px;
- }
-
- h1 {
- font-size: 6rem;
- margin: 0;
- font-weight: bold;
- opacity: 0.8;
- }
-
- h2 {
- font-size: 1.5rem;
- margin: 1rem 0;
- font-weight: normal;
- }
-
- p {
- margin: 1rem 0;
- opacity: 0.7;
- line-height: 1.5;
- }
-
- a {
- color: #3498db;
- text-decoration: none;
- font-weight: 500;
- }
-
- a:hover {
- text-decoration: underline;
- }
- </style>
- </head>
- <body>
- <div class="container">
- <h1>404</h1>
- <h2>Page Not Found</h2>
- <p>The page you're looking for doesn't exist.</p>
- <p><a href="/">← Back to Rain Radar</a></p>
- </div>
- </body>
-</html> \ No newline at end of file
diff --git a/src/pages/api/update.ts b/src/pages/api/update.ts
deleted file mode 100644
index 88dfa3c..0000000
--- a/src/pages/api/update.ts
+++ /dev/null
@@ -1,200 +0,0 @@
-import type { APIRoute } from 'astro';
-
-export const prerender = false;
-
-interface ImageData {
- [key: string]: string[];
-}
-
-interface CloudflareEnv {
- BASE_URL: string;
- AUTH_USER: string;
- AUTH_PASS: string;
- BUCKET_NAME: string;
- SCHEDULED?: boolean;
-}
-
-class GeshemUpdate {
- private bucket: R2Bucket;
- private env: CloudflareEnv;
-
- constructor(bucket: R2Bucket, env: CloudflareEnv) {
- this.bucket = bucket;
- this.env = env;
- }
-
- private async authenticate(request: Request): Promise<Request> {
- const headers = new Headers(request.headers);
- const credentials = btoa(`${this.env.AUTH_USER}:${this.env.AUTH_PASS}`);
- headers.set('Authorization', `Basic ${credentials}`);
-
- return new Request(request.url, {
- method: request.method,
- headers,
- body: request.body
- });
- }
-
- private async getLatestImages(): Promise<string[]> {
- const authenticatedRequest = await this.authenticate(new Request(this.env.BASE_URL));
- const response = await fetch(authenticatedRequest);
- const html = await response.text();
-
- const regex = /radar280comp_\d+\.png/g;
- const matches = html.match(regex) || [];
- const uniqueImages = [...new Set(matches)];
-
- return uniqueImages.sort().slice(-10);
- }
-
- private async getLatestBucketKeys(): Promise<string[]> {
- const yesterday = new Date();
- yesterday.setDate(yesterday.getDate() - 1);
- const yesterdayStr = yesterday.toISOString().slice(0, 10).replace(/-/g, '');
-
- try {
- const objects = await this.bucket.list({
- prefix: 'imgs/',
- startAfter: `imgs/${yesterdayStr}`
- });
-
- return objects.objects.map(obj => obj.key);
- } catch (error) {
- console.error('Error listing bucket objects:', error);
- return [];
- }
- }
-
- private async fetchMissingImages(): Promise<boolean> {
- const images = await this.getLatestImages();
- const s3Images = await this.getLatestBucketKeys();
- let updated = false;
-
- for (const img of images) {
- const key = this.keyFromFilename(img);
- if (!s3Images.includes(key)) {
- await this.fetchImage(img, key);
- updated = true;
- }
- }
-
- return updated;
- }
-
- private async fetchImage(imgName: string, key: string): Promise<void> {
- console.log(`Downloading ${imgName} from web server`);
-
- const imageUrl = `${this.env.BASE_URL}/${imgName}`;
- const authenticatedRequest = await this.authenticate(new Request(imageUrl));
- const response = await fetch(authenticatedRequest);
-
- if (!response.ok) {
- throw new Error(`Failed to fetch image: ${response.statusText}`);
- }
-
- const imageData = await response.arrayBuffer();
-
- console.log(`Uploading to ${key}`);
- await this.bucket.put(key, imageData, {
- httpMetadata: {
- contentType: 'image/png',
- cacheControl: 'public, max-age=31536000'
- }
- });
- }
-
- private keyFromFilename(filename: string): string {
- const [name, dateStr] = filename.split('.')[0].split('_');
- const res = 280;
-
- // Parse date: YYYYMMDDHHMM
- const year = dateStr.slice(0, 4);
- const month = dateStr.slice(4, 6);
- const day = dateStr.slice(6, 8);
- const hour = dateStr.slice(8, 10);
- const minute = dateStr.slice(10, 12);
-
- const d = `${year}${month}${day}`;
- const t = `${hour}${minute}`;
-
- return `imgs/${d}/${t}/${res}.png`;
- }
-
- private async generateJson(): Promise<void> {
- const latestKeys = await this.getLatestBucketKeys();
- const keys = latestKeys
- .filter(key => key.endsWith('280.png'))
- .sort()
- .slice(-10);
-
- const index: ImageData = { '280': keys };
-
- await this.bucket.put('imgs.json', JSON.stringify(index), {
- httpMetadata: {
- contentType: 'application/json',
- cacheControl: 'public, max-age=60'
- }
- });
- }
-
- async run(): Promise<string> {
- try {
- const updated = await this.fetchMissingImages();
- if (updated) {
- await this.generateJson();
- }
- return `Updated: ${updated}`;
- } catch (error) {
- console.error('Error in GeshemUpdate.run():', error);
- throw error;
- }
- }
-}
-
-export const GET: APIRoute = async ({ platform, request }) => {
- // This endpoint should only be accessible via Cloudflare Cron
- const cron = request.headers.get('CF-Cron');
- if (!cron) {
- return new Response('Unauthorized', { status: 401 });
- }
-
- try {
- const env = platform?.env as unknown as CloudflareEnv;
-
- if (!env?.BASE_URL || !env?.AUTH_USER || !env?.AUTH_PASS) {
- return new Response('Missing environment variables', { status: 500 });
- }
-
- // Get R2 bucket (this would be configured in your Cloudflare Workers environment)
- const bucket = env.BUCKET_NAME ? platform?.env[env.BUCKET_NAME] as R2Bucket : null;
-
- if (!bucket) {
- return new Response('R2 bucket not configured', { status: 500 });
- }
-
- const updater = new GeshemUpdate(bucket, env);
- const result = await updater.run();
-
- return new Response(JSON.stringify({
- message: `SUCCESS: ${result}`,
- timestamp: new Date().toISOString()
- }), {
- status: 200,
- headers: {
- 'Content-Type': 'application/json'
- }
- });
-
- } catch (error) {
- console.error('Error in update handler:', error);
- return new Response(JSON.stringify({
- message: 'ERROR',
- error: error instanceof Error ? error.message : 'Unknown error'
- }), {
- status: 500,
- headers: {
- 'Content-Type': 'application/json'
- }
- });
- }
-}; \ No newline at end of file
diff --git a/src/pages/history/[date].astro b/src/pages/history/[date].astro
deleted file mode 100644
index 0a78f9f..0000000
--- a/src/pages/history/[date].astro
+++ /dev/null
@@ -1,13 +0,0 @@
----
-import Layout from '../../layouts/Layout.astro';
-import { Geshem } from '../../components/Geshem';
-import '../../styles/global.css';
-
-export const prerender = false;
-
-const { date } = Astro.params;
----
-
-<Layout title="Geshem - Rain Radar History" description="Israel Rain Radar History">
- <Geshem client:load date={date} />
-</Layout> \ No newline at end of file
diff --git a/src/pages/index.astro b/src/pages/index.astro
deleted file mode 100644
index 5b08f18..0000000
--- a/src/pages/index.astro
+++ /dev/null
@@ -1,11 +0,0 @@
----
-import Layout from "../layouts/Layout.astro";
-import { Geshem } from "../components/Geshem";
-import "../styles/global.css";
-
-const { date } = Astro.params;
----
-
-<Layout>
- <Geshem client:load date={date} />
-</Layout>
diff --git a/src/pages/privacy.astro b/src/pages/privacy.astro
deleted file mode 100644
index d01a26b..0000000
--- a/src/pages/privacy.astro
+++ /dev/null
@@ -1,40 +0,0 @@
----
-
----
-
-<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="/favicon.ico" />
- <link rel="apple-touch-icon" href="/apple-touch-icon.png" />
- <link rel="manifest" href="/manifest.json" />
- <title>Privacy Policy | geshem.space</title>
- <script src="https://cdn.usefathom.com/script.js" data-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;
- background-color: #1a1a1a;
- color: white;
- margin: 0;
- }
- </style>
- </head>
- <body>
- <h1>We do not collect any data.</h1>
- </body>
-</html>
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/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';