From 828dc80ab299851629e2621d1e5d38f75366a2a5 Mon Sep 17 00:00:00 2001 From: invpt <57822954+invpt@users.noreply.github.com> Date: Mon, 11 Mar 2024 03:16:00 -0400 Subject: [PATCH] Improved error handling --- package-lock.json | 9 +++ package.json | 1 + src/components/Asset.tsx | 52 ++++------------- src/components/Gallery.tsx | 15 +++-- src/components/LatLngEditor.tsx | 18 +++--- src/hooks/AssetUrl.tsx | 46 +++++++++++++++ src/hooks/RouteCalculator.tsx | 19 +++--- src/hooks/Tour.tsx | 4 +- src/index.tsx | 24 ++++---- src/pages/login/Login.module.css | 7 --- src/pages/login/Login.tsx | 49 ---------------- src/pages/project/ProjectAssetsEditor.tsx | 28 ++++----- src/route.ts | 71 ++++++++++++++++------- 13 files changed, 173 insertions(+), 170 deletions(-) create mode 100644 src/hooks/AssetUrl.tsx delete mode 100644 src/pages/login/Login.module.css delete mode 100644 src/pages/login/Login.tsx diff --git a/package-lock.json b/package-lock.json index b05fb0b..82e435c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,6 +17,7 @@ "maplibre-gl": "^4.1.0", "solid-icons": "^1.1.0", "solid-js": "^1.8.15", + "solid-toast": "^0.5.0", "uuid": "^9.0.1" }, "devDependencies": { @@ -2283,6 +2284,14 @@ "solid-js": "^1.3" } }, + "node_modules/solid-toast": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/solid-toast/-/solid-toast-0.5.0.tgz", + "integrity": "sha512-t770JakjyS2P9b8Qa1zMLOD51KYKWXbTAyJePVUoYex5c5FH5S/HtUBUbZAWFcqRCKmAE8KhyIiCvDZA8bOnxQ==", + "peerDependencies": { + "solid-js": "^1.5.4" + } + }, "node_modules/sort-asc": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/sort-asc/-/sort-asc-0.2.0.tgz", diff --git a/package.json b/package.json index fca06a0..9f4d36f 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "maplibre-gl": "^4.1.0", "solid-icons": "^1.1.0", "solid-js": "^1.8.15", + "solid-toast": "^0.5.0", "uuid": "^9.0.1" } } diff --git a/src/components/Asset.tsx b/src/components/Asset.tsx index 37efad3..432955e 100644 --- a/src/components/Asset.tsx +++ b/src/components/Asset.tsx @@ -1,9 +1,10 @@ import { FiArrowDown, FiArrowUp, FiImage, FiMusic, FiTrash, FiUpload } from "solid-icons/fi"; -import { Component, createEffect, createResource, createSignal, createUniqueId, For, JSX, Show } from "solid-js"; +import { Component, createSignal, createUniqueId, For, JSX, Show } from "solid-js"; import styles from "./Asset.module.css"; import { useDB } from "../db"; import { useProject } from "../hooks/Project"; +import { useAssetUrl } from "../hooks/AssetUrl"; export const Asset: Component<{ id?: string, @@ -16,46 +17,21 @@ export const Asset: Component<{ }> = (props) => { const datalistId = createUniqueId(); const fileInputId = createUniqueId(); - const db = useDB(); - const [project, setProject] = useProject(); + const [imageLoaded, setImageLoaded] = createSignal(false); const [query, setQuery] = createSignal(props.asset); - const asset = () => props.asset && project() && props.asset in project()!.assets ? project()?.assets[props.asset] : undefined; - const [assetBlob] = createResource(() => props.asset, asset => { - if (asset === undefined) { - return undefined; - } - const currentProject = project(); - if (currentProject === undefined) { - return undefined; - } - if (!currentProject.assets[asset]) { - return undefined; - } - return db.loadAsset(currentProject.assets[asset].hash); - }); - const assetUrl = () => { - const blob = assetBlob(); - if (blob === undefined) { - return undefined; - } - return URL.createObjectURL(blob); - }; - const assets = () => { - const currentProject = project(); - if (currentProject === undefined) { - return undefined; - } - return Object.keys(currentProject.assets); - } - const resolved = () => !!asset(); + const db = useDB(); + const [project, setProject] = useProject(); + const assetUrl = useAssetUrl(project, () => props.asset); + + const assets = () => Object.keys(project()?.assets ?? {}); const handleQueryInput: JSX.EventHandlerUnion = async (event) => { const newQuery = event.currentTarget.value; setQuery(newQuery); - const match = Object.keys(project()?.assets ?? {}).find(asset => asset === newQuery); + const match = assets().find(asset => asset === newQuery); if (match) { props.onIdChange(match); } @@ -77,13 +53,7 @@ export const Asset: Component<{ const file = files[0]; let assetHash: string; - try { - assetHash = await db.storeAsset(file); - } catch (e) { - alert(`Failed to add asset. The storage alloted to this site by your web browser may have become full.`) - console.error("Failed to add asset", e); - return; - } + assetHash = await db.storeAsset(file); setProject(project => ({ ...project, @@ -141,7 +111,7 @@ export const Asset: Component<{ - + diff --git a/src/components/Gallery.tsx b/src/components/Gallery.tsx index 32bc565..0d44952 100644 --- a/src/components/Gallery.tsx +++ b/src/components/Gallery.tsx @@ -1,10 +1,9 @@ -import { Component, createEffect, createResource, JSX, For, createUniqueId, createSignal, Index, Show } from "solid-js"; -import { FiArrowDown, FiArrowUp, FiImage, FiTrash, FiUpload } from "solid-icons/fi"; +import { Component, Index } from "solid-js"; import { GalleryModel } from "../data"; +import { Asset } from "./Asset"; import styles from "./Gallery.module.css"; -import { Asset } from "./Asset"; export const Gallery: Component<{ id?: string | undefined, @@ -24,11 +23,17 @@ export const Gallery: Component<{ }; const handleUpClick = (i: number) => () => { - + if (i <= 0 || i >= props.value.length) { + return; + } + props.onChange([...props.value.slice(0, i - 1), props.value[i], props.value[i - 1], ...props.value.slice(i + 1)]) }; const handleDownClick = (i: number) => () => { - + if (i < 0 || i >= props.value.length - 1) { + return; + } + props.onChange([...props.value.slice(0, i), props.value[i + 1], props.value[i], ...props.value.slice(i + 2)]); }; return ( diff --git a/src/components/LatLngEditor.tsx b/src/components/LatLngEditor.tsx index 84cb552..16fca96 100644 --- a/src/components/LatLngEditor.tsx +++ b/src/components/LatLngEditor.tsx @@ -10,10 +10,12 @@ export const LatLngEditor: Component<{ lng: number, onChange: (newLat: number, newLng: number) => void, }> = (props) => { + const maxDigitsAfterDot = 6; + const [latVal, setLatVal] = createSignal(props.lat ?? 0); const [lngVal, setLngVal] = createSignal(props.lng ?? 0); - const [latTxt, setLatTxt] = createSignal(truncateDecimal(latVal().toString(), 6)); - const [lngTxt, setLngTxt] = createSignal(truncateDecimal(lngVal().toString(), 6)); + const [latTxt, setLatTxt] = createSignal(truncateDecimal(latVal().toString(), maxDigitsAfterDot)); + const [lngTxt, setLngTxt] = createSignal(truncateDecimal(lngVal().toString(), maxDigitsAfterDot)); const handleLatChange: JSX.EventHandlerUnion = (ev) => { const trimmed = ev.currentTarget.value.trim(); @@ -25,7 +27,7 @@ export const LatLngEditor: Component<{ const group = /-?0*(\d*(\.\d*)?)/.exec(trimmed)?.[1]; if (!group) return; - let newLatTxt = truncateDecimal(trimmed.startsWith("-") ? "-" + group : group, 6); + let newLatTxt = truncateDecimal(trimmed.startsWith("-") ? "-" + group : group, maxDigitsAfterDot); setLatVal(newLat = Number.parseFloat(newLatTxt)); setLatTxt(newLatTxt); @@ -43,7 +45,7 @@ export const LatLngEditor: Component<{ const group = /-?0*(\d*(\.\d*)?)/.exec(trimmed)?.[1]; if (!group) return; - let newLngTxt = truncateDecimal(trimmed.startsWith("-") ? "-" + group : group, 6); + let newLngTxt = truncateDecimal(trimmed.startsWith("-") ? "-" + group : group, maxDigitsAfterDot); setLngVal(newLng = Number.parseFloat(newLngTxt)); setLngTxt(newLngTxt); @@ -54,11 +56,11 @@ export const LatLngEditor: Component<{ createEffect(() => { if (props.lat && latVal() !== props.lat) { setLatVal(props.lat); - setLatTxt(truncateDecimal(latVal().toString(), 6)); + setLatTxt(truncateDecimal(latVal().toString(), maxDigitsAfterDot)); } if (props.lng && lngVal() !== props.lng) { setLngVal(props.lng); - setLngTxt(truncateDecimal(lngVal().toString(), 6)); + setLngTxt(truncateDecimal(lngVal().toString(), maxDigitsAfterDot)); } }); @@ -81,7 +83,9 @@ export const LatLngEditor: Component<{ function truncateDecimal(s: string, maxDigitsAfterDot: number): string { let parts = s.split("."); - if (parts.length === 1) { + if (parts.length === 0) { + return ""; + } else if (parts.length === 1) { return s; } else { return `${parts[0]}.${parts[1].substring(0, maxDigitsAfterDot)}`; diff --git a/src/hooks/AssetUrl.tsx b/src/hooks/AssetUrl.tsx new file mode 100644 index 0000000..3033f3f --- /dev/null +++ b/src/hooks/AssetUrl.tsx @@ -0,0 +1,46 @@ +import { createEffect, createSignal, onCleanup, untrack } from "solid-js"; + +import { DbProject, useDB } from "../db"; + +export const useAssetUrl = (project: () => DbProject | undefined, asset: () => string | undefined) => { + const db = useDB(); + const [assetUrl, setAssetUrl] = createSignal(); + + createEffect(async () => { + const oldAssetUrl = untrack(assetUrl); + if (oldAssetUrl !== undefined) { + URL.revokeObjectURL(oldAssetUrl); + } + + const currentProject = project(); + if (currentProject === undefined) { + return; + } + + const currentAsset = asset(); + if (currentAsset === undefined) { + return; + } + + const assetInfo = currentProject.assets[currentAsset]; + if (assetInfo === undefined) { + return; + } + + const blob = await db.loadAsset(assetInfo.hash); + if (blob === undefined) { + return; + } + + setAssetUrl(URL.createObjectURL(blob)); + }); + + onCleanup(() => { + const finalAssetUrl = assetUrl(); + if (finalAssetUrl !== undefined) { + URL.revokeObjectURL(finalAssetUrl); + } + }); + + return assetUrl; +}; \ No newline at end of file diff --git a/src/hooks/RouteCalculator.tsx b/src/hooks/RouteCalculator.tsx index 30e91b0..64533fb 100644 --- a/src/hooks/RouteCalculator.tsx +++ b/src/hooks/RouteCalculator.tsx @@ -10,7 +10,7 @@ export function useRouteCalculator() { const [prevLatLongs, setPrevLatLongs] = createSignal<(LatLng & { control: "path" | "route" })[]>([]); - createEffect(() => { + createEffect(async () => { if (!tour()) return; const latLongs = tour()!.route @@ -31,13 +31,14 @@ export function useRouteCalculator() { setPrevLatLongs(latLongs); - route(latLongs) - .then(route => setTour(({ - ...tour()!, - path: polyline.encode(route), - }))) - .catch(err => { - console.error(err); - }); + const routePoints = await route(latLongs); + if (!routePoints) { + return; + } + + setTour(tour => ({ + ...tour, + path: polyline.encode(routePoints), + })); }); } \ No newline at end of file diff --git a/src/hooks/Tour.tsx b/src/hooks/Tour.tsx index 55e40c2..5d150e8 100644 --- a/src/hooks/Tour.tsx +++ b/src/hooks/Tour.tsx @@ -31,8 +31,8 @@ export const TourProvider: Component<{ id: string, children: JSX.Element }> = (p if (deleted()) return; const currentProject = project(); - const currentTourIndex = project()?.tours.findIndex(tour => tour.id === props.id); - if (currentProject === undefined || currentTourIndex === undefined || currentTourIndex === -1) { + const currentTourIndex = currentProject?.tours.findIndex(tour => tour.id === props.id); + if (currentProject === undefined || currentTourIndex === undefined || currentTourIndex < 0) { console.warn("Ignoring update with undefined currentTour"); return; } diff --git a/src/index.tsx b/src/index.tsx index 37d6198..4a6749a 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -2,6 +2,7 @@ import { render } from "solid-js/web"; import { HashRouter, Route } from "@solidjs/router"; import { Component } from "solid-js"; +import { Toaster } from "solid-toast"; import "./index.css"; import "maplibre-gl/dist/maplibre-gl.css"; @@ -24,17 +25,20 @@ const Blank: Component = () => <>; render( () => ( - - - - - - - - + <> + + + + + + + + + - - + + + ), root! ); diff --git a/src/pages/login/Login.module.css b/src/pages/login/Login.module.css deleted file mode 100644 index 616ba58..0000000 --- a/src/pages/login/Login.module.css +++ /dev/null @@ -1,7 +0,0 @@ -.Login { - width: 300px; - display: flex; - flex-direction: column; - align-items: stretch; - margin: auto; -} diff --git a/src/pages/login/Login.tsx b/src/pages/login/Login.tsx deleted file mode 100644 index 34e7910..0000000 --- a/src/pages/login/Login.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import { createUniqueId, type Component, createSignal, JSX } from "solid-js"; -import { useNavigate, useRouteData } from "@solidjs/router"; - - -import styles from "./Login.module.css"; -import { Field } from "../../components/Field"; - -export const Login: Component = () => { - const api = useApiClient(); - const navigate = useNavigate(); - const [username, setUsername] = createSignal(""); - const [password, setPassword] = createSignal(""); - - const handleUsernameInput: JSX.EventHandlerUnion = (event) => { - setUsername(event.currentTarget.value); - }; - - const handlePasswordInput: JSX.EventHandlerUnion = (event) => { - setPassword(event.currentTarget.value); - }; - - const handleSubmit: JSX.EventHandlerUnion = async (ev) => { - ev.preventDefault(); - - try { - await api.login(username(), password()); - const redirect = new URLSearchParams(window.location.search).get("redirect"); - if (redirect) { - navigate(redirect); - } else { - navigate("/"); - } - } catch (e) { - alert("Failed to login: " + e); - } - }; - - return ( -
- - {(id) => } - - - {(id) => } - - -
- ); -}; \ No newline at end of file diff --git a/src/pages/project/ProjectAssetsEditor.tsx b/src/pages/project/ProjectAssetsEditor.tsx index f607b59..d7b04d0 100644 --- a/src/pages/project/ProjectAssetsEditor.tsx +++ b/src/pages/project/ProjectAssetsEditor.tsx @@ -1,10 +1,11 @@ import { createResource, type Component, For, createUniqueId, JSX, Show } from "solid-js"; import { useParams } from "@solidjs/router"; -import { FiTrash, FiUpload, FiDownload, FiMusic } from "solid-icons/fi"; +import { FiTrash, FiUpload, FiDownload, FiMusic, FiFile } from "solid-icons/fi"; import styles from "./ProjectAssetsEditor.module.css"; import { useProject } from "../../hooks/Project"; import { useDB } from "../../db"; +import { useAssetUrl } from "../../hooks/AssetUrl"; export const ProjectAssetsEditor: Component = () => { const params = useParams(); @@ -31,6 +32,7 @@ const AssetCard: Component<{ asset: string }> = (props) => { const fileInputId = createUniqueId(); const db = useDB(); const [project, setProject] = useProject(); + const assetUrl = useAssetUrl(project, () => props.asset); const [assetBlob] = createResource(() => props.asset, asset => { if (asset === undefined) { return undefined; @@ -42,32 +44,17 @@ const AssetCard: Component<{ asset: string }> = (props) => { if (!currentProject.assets[asset]) { return undefined; } - return db.loadAsset(currentProject.assets[asset].hash).then(r => { - console.log(r); - return r; - }) + return db.loadAsset(currentProject.assets[asset].hash); }); const assetIsImage = () => assetBlob() ? ["image/jpeg", "image/png"].includes(assetBlob()!.type) : false; const assetIsAudio = () => assetBlob() ? ["audio/mpeg"].includes(assetBlob()!.type) : false; - const assetUrl = () => assetBlob() ? URL.createObjectURL(assetBlob()!) : undefined; const handleFileInput: JSX.EventHandlerUnion = async (event) => { - if (project() === undefined) { - return; - } - const files = event.currentTarget.files; if (!files || files.length < 1) return; const file = files[0]; - let assetHash: string; - try { - assetHash = await db.storeAsset(file); - } catch (e) { - alert(`Failed to update asset. The storage alloted to this site by your web browser may have become full.`) - console.error("Failed to add asset", e); - return; - } + const assetHash = await db.storeAsset(file); setProject(project => ({ ...project, @@ -119,6 +106,11 @@ const AssetCard: Component<{ asset: string }> = (props) => { + +
window.open(assetUrl(), "_blank")}> + +
+
{props.asset}
diff --git a/src/route.ts b/src/route.ts index b1b8006..8869c0f 100644 --- a/src/route.ts +++ b/src/route.ts @@ -1,3 +1,5 @@ +import toast from "solid-toast"; + import { LatLng } from "./data"; import * as polyline from "./polyline"; @@ -18,30 +20,55 @@ export const route = async (points: (LatLng & { control: "path" | "route" })[]) "units": "miles", }; - const resp = await fetch("https://valhalla1.openstreetmap.de/route", { - method: "POST", - body: JSON.stringify(reqJson), - headers: { - "Content-Type": "application/json", - }, - }); - - const respBody: { - trip: { - legs: { - shape: string - }[] - } - } = await resp.json(); + let resp: Response; + let respBody: unknown; + try { + resp = await fetch("https://valhalla1.openstreetmap.de/route", { + method: "POST", + body: JSON.stringify(reqJson), + headers: { + "Content-Type": "application/json", + }, + }); + + respBody = await resp.json(); + } catch (err) { + console.error("Error while calculating route:", err); + toast.error("Unable to generate the path connecting the tour stops along roads. This is likely due to a service connectivity issue. Please try again later and report the issue if it persists."); + return; + } - const outPoints = [points[0] as LatLng]; - let i = 1; - for (const leg of respBody.trip.legs) { - if (points[i].control === "route") { - outPoints.push(...polyline.decode(leg.shape).map(decoded => ({ lat: decoded.lat / 10.0, lng: decoded.lng / 10.0 }))); + if (!resp.ok) { + console.error("Non-OK Valhalla response:", resp); + if (typeof respBody === "object" && respBody && "error" in respBody && typeof respBody.error === "string" && respBody.error) { + toast.error("Unable to generate the path connecting the tour stops along roads. The following error message was received from the external service used to perform this task: " + respBody.error); + } else { + toast.error("Unable to generate the path connecting the tour stops along roads. This is likely due to a bug or a temporary service outage. Please try again later and report the issue if it persists."); } - i++; + return; } - return outPoints; + if (respBody && typeof respBody === "object" + && "trip" in respBody && respBody.trip && typeof respBody.trip === "object" + && "legs" in respBody.trip && respBody.trip.legs && typeof respBody.trip.legs === "object" && Array.isArray(respBody.trip.legs)) { + const outPoints = [{ lat: points[0].lat, lng: points[0].lng }]; + let i = 1; + for (const leg of respBody.trip.legs) { + if (!("shape" in leg) || typeof leg.shape !== "string" || !leg.shape) { + continue; + } + + if (points[i].control === "route") { + outPoints.push(...polyline.decode(leg.shape).map(decoded => ({ lat: decoded.lat / 10.0, lng: decoded.lng / 10.0 }))); + } else { + outPoints.push({ lat: points[i].lat, lng: points[i].lng }); + } + i++; + } + + return outPoints; + } else { + console.error("Malformed Valhalla response:", respBody); + toast.error("Unable to generate the path connecting the tour stops along roads. An invalid response was received from the external service used to perform this task. Please try again later and report the issue if it persists."); + } } \ No newline at end of file