Skip to content
Merged
62 changes: 62 additions & 0 deletions app/components/Map.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import React, { useRef, useEffect, useState } from "react";
import type { Map } from "maplibre-gl";
import maplibregl from "maplibre-gl";

interface MapProps extends React.InputHTMLAttributes<HTMLInputElement> {
latitude?: number;
longitude?: number;
}

export type MapContextValue = {
map: Map | null;
};

export const MapContext = React.createContext<MapContextValue>({
map: null,
});

export default function MyMap({ latitude = 7, longitude = 52 }: MapProps) {
const mapContainer = useRef<HTMLDivElement>(null);
const [mapInstance, setMapInstance] = useState<Map | null>(null);

const { current: contextValue } = useRef<MapContextValue>({ map: null });

useEffect(() => {
if (contextValue.map) return; //stops map from intializing more than once

let map: Map;

const initialState = {
lng: longitude,
lat: latitude,
zoom: 2,
};

if (mapContainer.current) {
map = new maplibregl.Map({
container: mapContainer.current,
style: `https://api.maptiler.com/maps/streets/style.json?key=${ENV.MAPTILER_KEY}`,
center: [initialState.lng, initialState.lat],
zoom: initialState.zoom,
});

contextValue.map = map;

setMapInstance(map);
}

return () => {
if (mapInstance) {
map.remove();
}
};
}, [contextValue, longitude, latitude]);

return (
<div className="h-full min-h-full w-full">
<div ref={mapContainer} className="h-full w-full">
<MapContext.Provider value={contextValue}></MapContext.Provider>
</div>
</div>
);
}
80 changes: 80 additions & 0 deletions app/components/bottomBar/BottomBar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { useState } from "react";
import SingleValue from "./SingleValue";
import { ChevronDoubleUpIcon } from "@heroicons/react/24/solid";
import { XCircleIcon } from "@heroicons/react/24/solid";

interface BottomBarProps {
id: string;
name: string;
sensors: Array<SensorProps>;
lastUpdate: string;
}

interface SensorProps {
_id: string;
icon: string;
lastMeasurement: LastMeasurementProps;
sensorType: string;
title: string;
unit: string;
}

interface LastMeasurementProps {
createdAt: string;
value: string;
}

export default function BottomBar(device: BottomBarProps) {
const [isOpen, setIsOpen] = useState<Boolean>(true);
return (
<div>
<div className={"bg-white " + (isOpen ? "animate-fade-in-up" : "hidden")}>
<div className="flex">
<div className="text-l basis-1/4 bg-green-300 pt-6 pb-6 text-center font-bold text-green-900 lg:text-3xl">
<p>{device.name}</p>
</div>
<div className="grid basis-3/4 content-center bg-green-900 pr-2 text-right text-sm text-white">
<div>
<p className="lg:inline text-xs lg:text-sm">Letzte Messung:</p>
<p className="lg:inline text-xs lg:text-sm"> {device.lastUpdate}</p>
</div>
</div>
<div className="flex items-center bg-green-900 pr-2">
<XCircleIcon
Comment thread
freds-dev marked this conversation as resolved.
Outdated
onClick={() => {
setIsOpen(!isOpen);
}}
className="h-6 w-6 lg:h-8 lg:w-8 cursor-pointer text-white"
/>
</div>
</div>
<div className="flex justify-center overflow-auto">
{device.sensors.map((sensor: SensorProps) => {
return (
<SingleValue
key={sensor._id}
_id={sensor._id}
icon={sensor.icon}
sensorType={sensor.sensorType}
title={sensor.title}
unit={sensor.unit}
lastMeasurement={sensor.lastMeasurement}
/>
);
})}
</div>
</div>
<div
onClick={() => {
setIsOpen(!isOpen);
}}
className={
"absolute bottom-5 left-1/2 cursor-pointer rounded-full bg-white p-2 hover:animate-bounce " +
(!isOpen ? "visible" : "hidden")
}
>
<ChevronDoubleUpIcon className="h-6 w-6 text-green-900" />
</div>
</div>
);
}
25 changes: 25 additions & 0 deletions app/components/bottomBar/SingleValue.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
interface SingleValueProps {
_id: string;
icon: string;
lastMeasurement: LastMeasurementProps;
sensorType: string;
title: string;
unit: string;
}

interface LastMeasurementProps {
createdAt: string;
value: string;
}

export default function SingleValue(sensor: SingleValueProps) {
return (
<div className="border-grey-300 lg:mb-3 mt-3 flex-1 border-r border-l border-solid pl-3 pr-3 text-center text-l lg:text-2xl">
<div className="flex justify-center">
{sensor.lastMeasurement ? (<b>{sensor.lastMeasurement.value}</b>) : (<b>xx</b>)}
<p>{sensor.unit}</p>
</div>
<p className="text-sm lg:text-xl">{sensor.title}</p>
</div>
);
}
3 changes: 3 additions & 0 deletions app/entry.server.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,12 @@ import { Response } from "@remix-run/node";
import { RemixServer } from "@remix-run/react";
import isbot from "isbot";
import { renderToPipeableStream } from "react-dom/server";
import { getEnv } from "./env.server";

const ABORT_DELAY = 5000;

global.ENV = getEnv();

export default function handleRequest(
request: Request,
responseStatusCode: number,
Expand Down
14 changes: 14 additions & 0 deletions app/env.server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
export function getEnv() {
return {
MAPTILER_KEY: process.env.MAPTILER_KEY,
};
}

type ENV = ReturnType<typeof getEnv>;

declare global {
var ENV: ENV;
interface Window {
ENV: ENV;
}
}
9 changes: 9 additions & 0 deletions app/root.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ import {
Outlet,
Scripts,
ScrollRestoration,
useLoaderData,
} from "@remix-run/react";
import { getEnv } from "./env.server";

import { getUser } from "./session.server";
import tailwindStylesheetUrl from "./styles/tailwind.css";
Expand Down Expand Up @@ -58,10 +60,12 @@ export const meta: MetaFunction = () => ({
export async function loader({ request }: LoaderArgs) {
return json({
user: await getUser(request),
ENV: getEnv(),
});
}

export default function App() {
const data = useLoaderData<typeof loader>();
return (
<html lang="en" className="h-full">
<head>
Expand All @@ -72,6 +76,11 @@ export default function App() {
<Outlet />
<ScrollRestoration />
<Scripts />
<script
dangerouslySetInnerHTML={{
__html: `window.ENV = ${JSON.stringify(data.ENV)}`,
}}
/>
<LiveReload />
</body>
</html>
Expand Down
24 changes: 24 additions & 0 deletions app/routes/explore.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { LinksFunction } from "@remix-run/node";
import { Outlet } from "@remix-run/react";
import Map from "~/components/Map";
import maplibregl from "maplibre-gl/dist/maplibre-gl.css";

export const links: LinksFunction = () => {
return [
{
rel: "stylesheet",
href: maplibregl,
},
];
};

export default function Explore() {
return (
<div className="h-full w-full">
<Map />
<main className="absolute bottom-0 z-10 w-full">
<Outlet />
</main>
</div>
);
}
41 changes: 41 additions & 0 deletions app/routes/explore/$deviceId.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { json, LoaderArgs } from "@remix-run/node";
import { useCatch, useLoaderData } from "@remix-run/react";
import BottomBar from "~/components/bottomBar/BottomBar";

export async function loader({ params }: LoaderArgs) {
// request to API with deviceID
const response = await fetch(
"https://api.opensensemap.org/boxes/" + params.deviceId
Comment thread
freds-dev marked this conversation as resolved.
Outdated
);
const data = await response.json();
if (data.code === "UnprocessableEntity") {
throw new Response("Device not found", { status: 502 });
}
return json(data);
}

export default function DeviceId() {
const data = useLoaderData<typeof loader>();
return (
<BottomBar
id={data._id}
name={data.name}
sensors={data.sensors}
lastUpdate={data.updatedAt}
/>
);
}

export function CatchBoundary() {
const caught = useCatch();
if (caught.status === 502) {
return (
<div>
<div className="flex animate-fade-in-up items-center justify-center bg-white py-10">
<div className="text-red-500">Oh no, we could not find this Device ID. Are you sure it exists?</div>
</div>
</div>
);
}
throw new Error(`Unsupported thrown response status code: ${caught.status}`);
}
Loading