import { createResource, createSignal, Show, onCleanup, type JSX } from "solid-js";
import { A, Route, Router, useNavigate } from "@solidjs/router";
import { api, ApiError, decodePolyline, UNAUTHORIZED, type Me } from "./api";
import { createLiveStore } from "./live";
import { SessionContext } from "./session";
import MapPage from "./MapPage";
import Settings from "./Settings";
const TRAIL_SECONDS = 24 * 3600;
const TRAIL_REFRESH_MS = 60_000;
export default function App() {
return (
);
}
function NotFound() {
return (
no such page
back to the map
);
}
/**
* The router root: session, navigation, and the one polling loop.
*
* Signing out is not a redirect. The signed-out shell renders the login form in
* place, so a deep link to /settings survives it — you land where you were
* heading rather than back at the map.
*/
function Shell(props: { children?: JSX.Element }) {
// undefined while the initial /api/me is in flight, null when signed out.
const [me, setMe] = createSignal(undefined);
api.me()
.then(setMe)
.catch(() => setMe(null));
return (
loading…}>
} keyed>
{(user) => (
setMe(null)}>
{props.children}
)}
);
}
function SignedIn(props: { me: Me; onSignedOut: () => void; children?: JSX.Element }) {
const live = createLiveStore(props.onSignedOut);
const [selected, setSelected] = createSignal(props.me.id);
// The trail for whoever is selected. A 403 means the share does not grant
// trail visibility, which is a normal answer, not an error to surface.
const [trail, { refetch }] = createResource(selected, async (id) => {
const to = live.serverTime();
try {
const res = await api.track(id, to - TRAIL_SECONDS, to);
return decodePolyline(res.polyline);
} catch (e) {
if (e instanceof ApiError && (e.status === 403 || e.status === UNAUTHORIZED)) return null;
throw e;
}
});
const timer = setInterval(() => void refetch(), TRAIL_REFRESH_MS);
onCleanup(() => clearInterval(timer));
async function signOut() {
await api.logout().catch(() => {});
props.onSignedOut();
}
return (
trail() ?? null,
}}
>
opentracker{props.me.display_name}{(msg) =>
{msg()}
}
{props.children}
);
}
function Login(props: { onSignedIn: (me: Me) => void }) {
const [username, setUsername] = createSignal("");
const [password, setPassword] = createSignal("");
const [error, setError] = createSignal(null);
const [busy, setBusy] = createSignal(false);
const navigate = useNavigate();
async function submit(e: Event) {
e.preventDefault();
setBusy(true);
setError(null);
try {
const { user } = await api.login(username(), password());
// A stale bookmark to a route that no longer exists would otherwise sign
// in and land on the 404 page.
if (window.location.pathname === "/login") navigate("/", { replace: true });
props.onSignedIn(user);
} catch (e) {
setError(e instanceof Error ? e.message : String(e));
} finally {
setBusy(false);
}
}
return (
);
}