10 Commits

Author SHA1 Message Date
4a969e626c Ensure dependencies are listed properly 2026-03-25 10:54:21 +00:00
3e1b7ea5d5 Disable source-maps for output code 2026-03-25 10:51:37 +00:00
fd213d6340 Refine filtering logic
Remove api-types package as the types are re-exported by the TS Client package
2026-03-25 10:50:26 +00:00
3eceddf20a Bump OwlBoard-TS 2026-03-25 10:24:58 +00:00
1d461780ab Add proper capitalization to page titles and location names. 2026-03-24 15:44:50 +00:00
ec4dd5dd3b Move 'LocationFilter' fetching to API Lib package 2026-03-24 12:47:39 +00:00
a7c244171c Add display options to PIS 2026-03-20 19:41:36 +00:00
3467f97889 Add PIS fetch logic 2026-03-19 23:41:12 +00:00
b1d8eea518 Add PIS Logic 2026-03-19 20:46:12 +00:00
deb151075a Add OwlBoard API Library
Add styling to UI Components
2026-03-19 10:59:25 +00:00
15 changed files with 265 additions and 38 deletions

24
package-lock.json generated
View File

@@ -13,6 +13,7 @@
"devDependencies": {
"@eslint/compat": "^2.0.2",
"@eslint/js": "^9.39.2",
"@owlboard/owlboard-ts": "^3.0.0-dev.20260325T1023",
"@playwright/test": "^1.58.1",
"@sveltejs/adapter-static": "^3.0.10",
"@sveltejs/kit": "^2.50.2",
@@ -778,6 +779,23 @@
"@jridgewell/sourcemap-codec": "^1.4.14"
}
},
"node_modules/@owlboard/api-schema-types": {
"version": "3.0.2-alpha2",
"resolved": "https://git.fjla.uk/api/packages/OwlBoard/npm/%40owlboard%2Fapi-schema-types/-/3.0.2-alpha2/api-schema-types-3.0.2-alpha2.tgz",
"integrity": "sha512-KyX4QcOCzVqYpiXY+WfhM1soXduMt2ldG6JSBK2WBxXWokS+keZshOHWHGTZvPLoZEWsuPznMAdzytI03/D3Ag==",
"dev": true,
"license": "MIT"
},
"node_modules/@owlboard/owlboard-ts": {
"version": "3.0.0-dev.20260325T1023",
"resolved": "https://git.fjla.uk/api/packages/OwlBoard/npm/%40owlboard%2Fowlboard-ts/-/3.0.0-dev.20260325T1023/owlboard-ts-3.0.0-dev.20260325t1023.tgz",
"integrity": "sha512-h5jAO9MYmYUToXvw3gCohUbG3oVl7h+PKrZ94I1NahXOLEd+CaQzXbXk5+KCsnojgkqf0I1FavaBbADgb2ZKkQ==",
"dev": true,
"license": "GPL-3.0",
"dependencies": {
"@owlboard/api-schema-types": "^3.0.2-alpha2"
}
},
"node_modules/@playwright/test": {
"version": "1.58.2",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.58.2.tgz",
@@ -2452,9 +2470,9 @@
}
},
"node_modules/flatted": {
"version": "3.4.1",
"resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.1.tgz",
"integrity": "sha512-IxfVbRFVlV8V/yRaGzk0UVIcsKKHMSfYw66T/u4nTwlWteQePsxe//LjudR1AMX4tZW3WFCh3Zqa/sjlqpbURQ==",
"version": "3.4.2",
"resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz",
"integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==",
"dev": true,
"license": "ISC"
},

View File

@@ -19,6 +19,7 @@
"devDependencies": {
"@eslint/compat": "^2.0.2",
"@eslint/js": "^9.39.2",
"@owlboard/owlboard-ts": "^3.0.0-dev.20260325T1023",
"@playwright/test": "^1.58.1",
"@sveltejs/adapter-static": "^3.0.10",
"@sveltejs/kit": "^2.50.2",

View File

@@ -4,7 +4,8 @@
import { fade } from 'svelte/transition';
import { goto } from '$app/navigation';
import { LOCATIONS } from '$lib/locations-object.svelte.ts';
import { LOCATIONS } from '$lib/locations-object.svelte';
import type { ApiLocationFilter } from '@owlboard/api-schema-types';
let { value = $bindable() } = $props();
@@ -26,13 +27,16 @@
return LOCATIONS.data
.filter((r) => tokens.every((t) => r.s.includes(t)))
.sort((a, b) => {
// Check if query matches CRS
const aIsCrs = a.c?.toLowerCase() === lowerQuery;
const bIsCrs = b.c?.toLowerCase() === lowerQuery;
// Sort matching CRS first
if (aIsCrs && !bIsCrs) return -1;
if (!aIsCrs && bIsCrs) return 1;
// Priority One - Exact CRS Match
const aExactCrs = a.c?.toLowerCase() === lowerQuery;
const bExactCrs = b.c?.toLowerCase() === lowerQuery;
if (aExactCrs && !bExactCrs) return -1;
if (!aExactCrs && bExactCrs) return 1;
// Priority Two - 'Stations' with CRS
if (!!a.c && !b.c) return -1;
if (!a.c & !! b.c) return 1;
// Alphabetical Sort
return a.n.localeCompare(b.n);
@@ -59,7 +63,7 @@
}
});
function choose(loc: LocationRecord) {
function choose(loc: ApiLocationFilter.LocationFilterObject) {
showResults = false;
selectedIndex = -1;
value = '';
@@ -195,6 +199,7 @@
font-size: 1.1rem;
font-weight: 700;
text-align: right;
text-transform: capitalize;
}
.tiploc {

View File

@@ -72,6 +72,7 @@
transition: all 0.2s ease-in-out;
outline: none;
text-align: center;
box-shadow: var(--shadow-std);
}
.all-caps {

View File

@@ -0,0 +1,53 @@
<script lang="ts">
interface Props {
toc: string;
}
let {
toc
}: Props = $props();
let code = $derived(toc.toUpperCase());
</script>
<div class="toc-container {code}">
{code}
</div>
<style>
.toc-container {
border-radius: 10px;
display: inline-flex;
align-items: center;
justify-content: center;
padding: 2px 8px;
font-weight: 800;
background-color: #333;
color: #fff;
}
.GW { /* Great Western Railway */
background: #004225;
color: #E2E2E2;
}
.GR { /* LNER */
background-color: #C00000;
color: #FFFFFF;
}
.VT { /* Avanti West Coast */
background-color: #004354;
color: #FFFFFF;
}
.SW { /* South Western Railway */
background-color: #2A3389;
color: #FFFFFF;
}
.XC { /* CrossCountry */
background-color: #660000;
color: #E4D5B1;
}
</style>

View File

@@ -60,6 +60,7 @@
font-family: 'URW Gothic', sans-serif;
color: var(--color-brand);
padding: 10px 0;
box-shadow: var(--shadow-std);
}
.header-content {

View File

@@ -3,6 +3,8 @@ import BaseCard from '$lib/components/ui/cards/BaseCard.svelte';
import Textbox from '$lib/components/ui/Textbox.svelte';
import Button from '$lib/components/ui/Button.svelte';
let { onsearch }: { onsearch: (c: string) => void } = $props();
let codeValue = $state('');
function resetValues(): void {
@@ -18,7 +20,7 @@ function resetValues(): void {
</div>
</div>
<div class="button-wrapper">
<Button>Search</Button>
<Button onclick={() => onsearch(codeValue.toString())}>Search</Button>
<Button onclick={resetValues}>Reset</Button>
</div>
</div>

View File

@@ -3,6 +3,9 @@ import BaseCard from '$lib/components/ui/cards/BaseCard.svelte';
import Textbox from '$lib/components/ui/Textbox.svelte';
import Button from '$lib/components/ui/Button.svelte';
let { onsearch }: { onsearch: (s: string, e: string) => void } = $props();
let startValue = $state('');
let endValue = $state('');
@@ -23,7 +26,7 @@ function resetValues(): void {
</div>
</div>
<div class="button-wrapper">
<Button>Search</Button>
<Button onclick={() => onsearch(startValue, endValue)}>Search</Button>
<Button onclick={resetValues}>Reset</Button>
</div>
</div>

View File

@@ -1,27 +1,23 @@
interface LocationRecord {
n: string; // name
t: string; // tiploc
c?: string; // crs
s: string; // search string
}
import { OwlClient } from "./owlClient";
import type { ApiLocationFilter } from '@owlboard/owlboard-ts'
class LocationStore {
data = $state<LocationRecord[]>([]);
data = $state<ApiLocationFilter.LocationFilterObject[]>([]);
loaded = $state(false);
async init(fetcher = fetch) {
async init() {
if (this.loaded) return;
try {
const res = await fetcher('/api/tiplocs');
this.data = await res.json();
const fetch = await OwlClient.locationFilter.getLocationFilterData()
this.data = fetch.data;
this.loaded = true;
} catch (err) {
console.error('Failed to load locations', err);
}
}
find(id: string | null): LocationRecord | undefined {
find(id: string | null): ApiLocationFilter.LocationFilterObject | undefined {
if (!id) return undefined;
const query = id.toUpperCase().trim();

21
src/lib/owlClient.ts Normal file
View File

@@ -0,0 +1,21 @@
import { OwlBoardClient, ValidationError, ApiError } from "@owlboard/owlboard-ts";
import { browser, dev } from "$app/environment";
// Import the runes containing the API Key config Here...
const baseUrl: string = browser ? window.location.origin : '';
const getBaseUrl = () => {
if (!browser) return '';
if (dev) return 'https://test.owlboard.info';
return window.location.origin;
}
export const OwlClient = new OwlBoardClient(
getBaseUrl(),
// API Key Here when ready!!!
)
export { ValidationError, ApiError };

View File

@@ -3,7 +3,7 @@
import { slide, fade } from 'svelte/transition';
import { onMount } from 'svelte';
import { LOCATIONS } from '$lib/locations-object.svelte.ts';
import { LOCATIONS } from '$lib/locations-object.svelte';
import '$lib/global.css';
@@ -13,7 +13,7 @@
import { IconHome, IconDialpad, IconSettings, IconHelp, IconDots } from '@tabler/icons-svelte';
onMount(() => LOCATIONS.init(fetch));
onMount(() => LOCATIONS.init());
let { children } = $props();
@@ -167,6 +167,7 @@
margin-left: 5px;
padding-bottom: 2px;
color: var(--color-title);
text-transform: capitalize;
}
header,
nav {

View File

@@ -30,8 +30,8 @@
</p>
<p class="amble">
Why OwlBoard? The name was chosen as an evolution of its predecessor, 'Athena'; owls are
associated with the Roman Goddess as well as with wisdom. The name also links to Bath, where the
app has been built and is run, representing the 'Minerva Owl' sculpture trail in the city, with
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">

View File

@@ -6,7 +6,7 @@ export const load: PageLoad = async ({ url }) => {
const locId = url.searchParams.get('loc');
if (!LOCATIONS.loaded) {
await LOCATIONS.init(fetch);
await LOCATIONS.init();
}
let title: string = '';

View File

@@ -1,16 +1,99 @@
<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';
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;
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;
}
}
function clearResults() {
console.log('Clearing Results');
resultsLoaded = false;
results = [];
}
</script>
<div class="card-container">
<PisStartEndCard />
<PisCode />
</div>
<div class="result-container">
</div>
{#if !resultsLoaded}
<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>
{/if}
<style>
.card-container {
@@ -23,11 +106,53 @@
}
.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-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;
}
.errCode {
color: rgb(255, 54, 54);
font-weight: 600;
font-size: 2rem;
}
.reset-button-container {
padding: 20px 0 3px 0;
}
</style>

View File

@@ -8,7 +8,7 @@
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"sourceMap": true,
"sourceMap": false,
"strict": true,
"moduleResolution": "bundler"
}