Skip to content

Commit

Permalink
integrate leaflet correctly with fresh
Browse files Browse the repository at this point in the history
  • Loading branch information
sigmaSd committed May 5, 2024
1 parent 9322b40 commit 1de8dba
Show file tree
Hide file tree
Showing 10 changed files with 160 additions and 197 deletions.
2 changes: 1 addition & 1 deletion components/NavigationBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export default function NavigationBar() {
<nav>
<ul class="list-none m-0 p-0 overflow-hidden bg-gray-800">
{items.map((item) => (
<li class="float-left">
<li key={item.name} class="float-left">
<a
class="block text-white text-center py-4 px-6 no-underline"
href={item.href}
Expand Down
1 change: 0 additions & 1 deletion deno.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
"tasks": {
"start": "deno run -A --watch=static/,routes/ dev.ts",
"start:check": "deno run -A --check --watch=static/,routes/ dev.ts",
"compile-map": "deno run -A scripts/compileMap.ts",
"create-radio-db": "deno run -A scripts/createRadioDb.ts; deno fmt",
"create-radio-db:check-update": "deno run -A scripts/createRadioDb.ts check-update",
"build": "deno run -A dev.ts build",
Expand Down
4 changes: 3 additions & 1 deletion fresh.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,12 @@ import * as $map from "./routes/map.tsx";
import * as $search from "./routes/search.tsx";
import * as $top from "./routes/top.tsx";
import * as $FavStations from "./islands/FavStations.tsx";
import * as $Map from "./islands/Map.tsx";
import * as $StatioMain from "./islands/StatioMain.tsx";
import * as $StationSearch from "./islands/StationSearch.tsx";
import * as $Stations from "./islands/Stations.tsx";
import * as $TopStations from "./islands/TopStations.tsx";
import type { Manifest } from "$fresh/server.ts";
import { type Manifest } from "$fresh/server.ts";

const manifest = {
routes: {
Expand All @@ -38,6 +39,7 @@ const manifest = {
},
islands: {
"./islands/FavStations.tsx": $FavStations,
"./islands/Map.tsx": $Map,
"./islands/StatioMain.tsx": $StatioMain,
"./islands/StationSearch.tsx": $StationSearch,
"./islands/Stations.tsx": $Stations,
Expand Down
141 changes: 141 additions & 0 deletions islands/Map.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
import type * as Leaflet from "https://esm.sh/v135/@types/[email protected]/index.d.ts";
import { IS_BROWSER } from "$fresh/runtime.ts";
import {
type StateUpdater,
useContext,
useEffect,
useState,
} from "preact/hooks";
import { type ComponentChildren, createContext } from "preact";

// Create a context to hold Leaflet data/functions
const LeafletContext = createContext<typeof Leaflet | null>(null);

// LeafletProvider component manages Leaflet loading and context
function LeafletProvider(props: { children: ComponentChildren }) {
if (!IS_BROWSER) {
// NOTE: what is the point of returning this component
// return <p>Leaflet must be loaded on the client. No children will render</p>;
return;
}
const [value, setValue] = useState<typeof Leaflet | null>(null);
return (
<>
{/* Load Leaflet CSS */}
<link
rel="stylesheet"
href="https://unpkg.com/[email protected]/dist/leaflet.css"
integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY="
crossorigin=""
/>
{/* Load Leaflet JS */}
<script
// deno-lint-ignore no-window
onLoad={() => setValue(window.L)}
src="https://unpkg.com/[email protected]/dist/leaflet.js"
integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo="
crossorigin=""
/>
{/* Provide Leaflet context to children */}
<LeafletContext.Provider value={value}>
{props.children}
</LeafletContext.Provider>
</>
);
}

// MapComponent utilizes Leaflet context for rendering the map
function MapComponent(
{ activeCountry, setActiveCountry }: {
activeCountry: string;
setActiveCountry: StateUpdater<string>;
},
) {
const leaf = useContext(LeafletContext);
if (!leaf) return <div>Loading Map...</div>;

//@ts-ignore seems ok
useEffect(async () => {
const latlng = await getLatLng(activeCountry);

const map = leaf.map("map");
leaf.tileLayer("https://tile.openstreetmap.org/{z}/{x}/{y}.png", {
attribution:
'&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
}).addTo(map);

let activeMark = leaf.marker(latlng).addTo(map)
.bindPopup(`Selected '${activeCountry}'`)
.openPopup();
map.setView(latlng, 4);
map.on(
"click",
async (ev) => {
activeMark.remove();
const result = await (async () => {
const map = ev.target;

const activeCountryNew = await countryFromLatLng(ev.latlng);
if (!activeCountry) {
return;
}
const latlon = await getLatLng(activeCountryNew);
const activeMark = leaf.marker(latlon).addTo(map)
.bindPopup(`Selected '${activeCountryNew}'`)
.openPopup();
return [activeMark, activeCountryNew] as const;
})();
if (result) {
activeMark = result[0];
setActiveCountry(result[1]);
}
},
);
}, []);
return <div id="map" class="relative w-[80vw] h-[50vh]" />;
}

// MapIsland is the parent component integrating LeafletProvider and MapComponent
export default function MapIsland() {
const [activeCountry, setActiveCountry] = useState(
Intl.DateTimeFormat()
.resolvedOptions()
.timeZone.split("/")[1],
);
return (
<>
<LeafletProvider>
<MapComponent
activeCountry={activeCountry}
setActiveCountry={setActiveCountry}
/>
</LeafletProvider>
<a
class="bg-green-600 border border-gray-400 rounded-md shadow-sm py-2 px-4 text-white font-bold cursor-pointer inline-block mt-5 w-150 h-30"
href={`/byCountry/${activeCountry}`}
>
Go
</a>
</>
);
}

async function getLatLng(cn: string): Promise<[number, number]> {
const resp = await fetch(
`https://nominatim.openstreetmap.org/search.php?country=${cn}&format=json`,
).then((r) => r.json());
const lat = resp[0].lat;
const lon = resp[0].lon;
return [Number.parseFloat(lat), Number.parseFloat(lon)];
}

async function countryFromLatLng(
{ lat, lng }: { lat: number; lng: number },
): Promise<string> {
const url =
// make sure we get english country names because that's what the database expects.
`https://nominatim.openstreetmap.org/reverse?format=json&lat=${lat}&lon=${lng}&accept-language=en`;
return await fetch(url).then((r) => r.json()).then((r) =>
r.address?.country || ""
);
}
5 changes: 4 additions & 1 deletion islands/Stations.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -163,8 +163,11 @@ export default function Stations(
gridTemplateColumns: "repeat(auto-fill,minmax(160px, 1fr))",
}}
>
{displayStations.value.map((station) => (
{displayStations.value.map((station: StationType) => (
<Station
// The database already remove duplicate stations
// so its probably fine to use the name as key
key={station.name}
station={station}
activeStaion={activeStaion}
/>
Expand Down
16 changes: 9 additions & 7 deletions routes/byCountry/[country].tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
import type { PageProps } from "$fresh/src/server/types.ts";
import StationMain from "@/islands/StatioMain.tsx";
import { Partial } from "$fresh/runtime.ts";

export default function Page(props: PageProps) {
let country = decodeURIComponent(props.params.country); // handle things like "United%20Kingdom"
if (country.toLowerCase() === "is" + "rael") country = "palestine";
if (country === "Palestinian Territories") country = "palestine";

// We add partial here, because this page is reched from map.js
// it changes the windows location directly so fresh is not aware of it
// and so it won't be wrapped with _app.tsx
return (
<Partial name="country">
<StationMain title={country} country={country} />
</Partial>
// the div is important!
// if we just return <StationMain...> it will be the same as index.tsx route
// this will make fresh partial not rerender the stations when going country -> index
// the div breaks that similarity
<>
<div>
<StationMain title={country} country={country} />;
</div>
</>
);
}
11 changes: 2 additions & 9 deletions routes/map.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Head } from "$fresh/runtime.ts";
import MapIsland from "@/islands/Map.tsx";

export default function RadioMap() {
return (
Expand All @@ -12,15 +13,7 @@ export default function RadioMap() {
/>
<link rel="stylesheet" href="/map/map.css" />
</Head>
<div id="map" />
<button
type="button"
class="bg-green-600 border border-gray-400 rounded-md shadow-sm py-2 px-4 text-white font-bold cursor-pointer inline-block mt-5 w-150 h-30"
id="goBtn"
>
Go!
</button>
<script type="module" src="/map/map.js" />
<MapIsland />
</>
);
}
30 changes: 0 additions & 30 deletions scripts/compileMap.ts

This file was deleted.

67 changes: 0 additions & 67 deletions static/map/map.js

This file was deleted.

Loading

0 comments on commit 1de8dba

Please sign in to comment.