Add NearestStations Card & Location monitor

This commit is contained in:
2026-03-30 23:34:12 +01:00
parent 4a969e626c
commit 777519ff5d
16 changed files with 465 additions and 390 deletions

View File

@@ -4,6 +4,7 @@
import { onMount } from 'svelte';
import { LOCATIONS } from '$lib/locations-object.svelte';
import { nearestStationsState } from '$lib/geohash.svelte';
import '$lib/global.css';

View File

@@ -1,9 +1,11 @@
<script lang="ts">
import LocationBoardCard from '$lib/components/ui/cards/LocationBoardCard.svelte';
import NearbyStationsCard from '$lib/components/ui/cards/NearbyStationsCard.svelte';
</script>
<div class="card-container">
<LocationBoardCard />
<NearbyStationsCard />
</div>
<style>

View File

@@ -30,9 +30,9 @@
</p>
<p class="amble">
Why OwlBoard? The name was chosen as an evolution of its predecessor, 'Athena'; owls are
associated with the equivalent Roman Goddess - Minerva - as well as with wisdom. This also links to Bath, where the
app has been built and is run, relating to the 'Minerva Owl' sculpture trail in the city, with
many of the sculptures still in the area.
associated with the equivalent Roman Goddess - Minerva - as well as with wisdom. This also links
to Bath, where the app has been built and is run, relating to the 'Minerva Owl' sculpture trail
in the city, with many of the sculptures still in the area.
</p>
<p class="opensource">
Some components that combine to form OwlBoard are open-source, see the <a

View File

@@ -1,98 +1,102 @@
<script lang="ts">
import PisStartEndCard from '$lib/components/ui/cards/pis/PisStartEndCard.svelte';
import PisCode from '$lib/components/ui/cards/pis/PisCode.svelte';
import Button from '$lib/components/ui/Button.svelte';
import type { ApiPisObject } from '@owlboard/owlboard-ts';
import { OwlClient, ApiError, ValidationError } from '$lib/owlClient';
import TocStyle from '$lib/components/ui/TocStyle.svelte';
import PisStartEndCard from '$lib/components/ui/cards/pis/PisStartEndCard.svelte';
import PisCode from '$lib/components/ui/cards/pis/PisCode.svelte';
import Button from '$lib/components/ui/Button.svelte';
import type { ApiPisObject } from '@owlboard/owlboard-ts';
import { OwlClient, ApiError, ValidationError } from '$lib/owlClient';
import TocStyle from '$lib/components/ui/TocStyle.svelte';
let results = $state<ApiPisObject.PisObjects[]>([]);
let resultsLoaded = $state<boolean>(false);
let errorState = $state<{status: number, message: string} | null>(null);
let results = $state<ApiPisObject.PisObjects[]>([]);
let resultsLoaded = $state<boolean>(false);
let errorState = $state<{ status: number; message: string } | null>(null);
async function handleStartEndSearch(start: string, end: string): Promise<void> {
console.log(`PIS Search: ${start}-${end}`);
errorState = null;
async function handleStartEndSearch(start: string, end: string): Promise<void> {
console.log(`PIS Search: ${start}-${end}`);
errorState = null;
try {
const response = await OwlClient.pis.getByStartEndCrs(start, end);
results = response.data || [];
} catch (e) {
if (e instanceof ValidationError) {
errorState = { status: 400, message: e.message };
} else if (e instanceof ApiError) {
console.log(e)
errorState = { status: 20, message: e.message };
} else {
errorState = { status: 0, message: `Unknown Error: ${e.message}` };
}
} finally {
resultsLoaded = true;
}
}
try {
const response = await OwlClient.pis.getByStartEndCrs(start, end);
results = response.data || [];
} catch (e) {
if (e instanceof ValidationError) {
errorState = { status: 400, message: e.message };
} else if (e instanceof ApiError) {
console.log(e);
errorState = { status: 20, message: e.message };
} else {
errorState = { status: 0, message: `Unknown Error: ${e.message}` };
}
} finally {
resultsLoaded = true;
}
}
async function handleCodeSearch(code: string) {
console.log(`PIS Search: ${code}`);
errorState = null;
try {
const response = await OwlClient.pis.getByCode(code);
results = response.data || []
} catch (e) {
if (e instanceof ValidationError) {
errorState = { status: 400, message: e.message };
} else if (e instanceof ApiError) {
console.log(e)
errorState = { status: 20, message: e.message };
} else {
errorState = { status: 0, message: `Unknown Error: ${e.message}` };
}
} finally {
resultsLoaded = true;
}
}
async function handleCodeSearch(code: string) {
console.log(`PIS Search: ${code}`);
errorState = null;
try {
const response = await OwlClient.pis.getByCode(code);
results = response.data || [];
} catch (e) {
if (e instanceof ValidationError) {
errorState = { status: 400, message: e.message };
} else if (e instanceof ApiError) {
console.log(e);
errorState = { status: 20, message: e.message };
} else {
errorState = { status: 0, message: `Unknown Error: ${e.message}` };
}
} finally {
resultsLoaded = true;
}
}
function clearResults() {
console.log('Clearing Results');
resultsLoaded = false;
results = [];
}
function clearResults() {
console.log('Clearing Results');
resultsLoaded = false;
results = [];
}
</script>
{#if !resultsLoaded}
<div class="card-container">
<PisStartEndCard onsearch={handleStartEndSearch} />
<PisCode onsearch={handleCodeSearch} />
</div>
<div class="card-container">
<PisStartEndCard onsearch={handleStartEndSearch} />
<PisCode onsearch={handleCodeSearch} />
</div>
{:else}
<div class="result-container">
{#if errorState}
<span class="errCode">Error: {errorState.status}</span>
<span class="errMsg">{errorState.message}</span>
{:else}
{#if results.length}
<h2 class="result-title">{results.length} Result{#if results.length > 1}s{/if} found</h2>
<table class="result-table">
<thead>
<tr>
<th style="width:16%">TOC</th>
<th style="width:14%">Code</th>
<th style="width:70%">Locations</th>
</tr></thead>
{#each results as result}
<tbody><tr>
<td><TocStyle toc={result.toc || ""} /></td>
<td>{result.code}</td>
<td class="locations-row">{result.crsStops?.join(' ') || ''}</td>
</tr></tbody>
{/each}
</table>
{:else}
<p class="no-results">No matching results</p>
{/if}
{/if}
<div class="reset-button-container">
<Button onclick={clearResults}>Reset</Button>
</div> </div>
<div class="result-container">
{#if errorState}
<span class="errCode">Error: {errorState.status}</span>
<span class="errMsg">{errorState.message}</span>
{:else if results.length}
<h2 class="result-title">
{results.length} Result{#if results.length > 1}s{/if} found
</h2>
<table class="result-table">
<thead>
<tr>
<th style="width:16%">TOC</th>
<th style="width:14%">Code</th>
<th style="width:70%">Locations</th>
</tr></thead
>
{#each results as result}
<tbody
><tr>
<td><TocStyle toc={result.toc || ''} /></td>
<td>{result.code}</td>
<td class="locations-row">{result.crsStops?.join(' ') || ''}</td>
</tr></tbody
>
{/each}
</table>
{:else}
<p class="no-results">No matching results</p>
{/if}
<div class="reset-button-container">
<Button onclick={clearResults}>Reset</Button>
</div>
</div>
{/if}
<style>
@@ -105,54 +109,54 @@
padding: 20px 10px;
}
.result-container {
font-family: 'URW Gothic', sans-serif;
font-size: 1.2rem;
font-weight: 600;
display: flex;
align-items: center;
flex-direction: column;
gap: 5px;
justify-content: center;
background: var(--color-accent);
border-radius: 15px;
padding: 20px 0 20px 0;
margin: auto;
margin-top: 25px;
margin-bottom: 25px;
width: 90%;
max-width: 500px;
.result-container {
font-family: 'URW Gothic', sans-serif;
font-size: 1.2rem;
font-weight: 600;
display: flex;
align-items: center;
flex-direction: column;
gap: 5px;
justify-content: center;
background: var(--color-accent);
border-radius: 15px;
padding: 20px 0 20px 0;
margin: auto;
margin-top: 25px;
margin-bottom: 25px;
width: 90%;
max-width: 500px;
box-shadow: var(--shadow-std);
}
}
.result-title {
color: var(--color-brand);
}
.result-title {
color: var(--color-brand);
}
.result-table {
width: 90%;
max-width: 350px;
margin: auto;
text-align: center;
table-layout: fixed;
border-collapse: separate;
border-spacing: 0 20px;
font-weight: 400;
}
.result-table {
width: 90%;
max-width: 350px;
margin: auto;
text-align: center;
table-layout: fixed;
border-collapse: separate;
border-spacing: 0 20px;
font-weight: 400;
}
.locations-row {
font-family:'Courier New', Courier, monospace;
text-align: left;
padding-left: 20px;
}
.locations-row {
font-family: 'Courier New', Courier, monospace;
text-align: left;
padding-left: 20px;
}
.errCode {
color: rgb(255, 54, 54);
font-weight: 600;
font-size: 2rem;
}
.errCode {
color: rgb(255, 54, 54);
font-weight: 600;
font-size: 2rem;
}
.reset-button-container {
padding: 20px 0 3px 0;
}
</style>
.reset-button-container {
padding: 20px 0 3px 0;
}
</style>

View File

@@ -2,4 +2,4 @@ export const load = () => {
return {
title: 'PIS Codes'
};
};
};