16 Commits

Author SHA1 Message Date
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
d9b60daa8b Add PIS code search components 2026-03-18 20:03:02 +00:00
2f0a6b9646 Add PIS Page 2026-03-18 19:20:14 +00:00
1165c02e26 Add OwlBoard npm repo 2026-03-18 19:07:31 +00:00
45dd5a1cf5 ALL_CAPS the locId in the error message for location_not_found error 2026-03-17 20:27:34 +00:00
e47bebe7d4 Add dynamic title sizing for better display on smaller screens 2026-03-17 20:26:04 +00:00
b7007d2fb3 run npm run format 2026-03-17 20:05:44 +00:00
64bc5b979d Add initial logic for 'boards' 2026-03-17 20:04:38 +00:00
3240560a0b Refactor location object into separate module so that it can be used in multiple locations 2026-03-17 19:28:02 +00:00
21 changed files with 505 additions and 48 deletions

1
.npmrc
View File

@@ -1 +1,2 @@
engine-strict=true engine-strict=true
@owlboard:registry=https://git.fjla.uk/api/packages/OwlBoard/npm/

25
package-lock.json generated
View File

@@ -13,6 +13,8 @@
"devDependencies": { "devDependencies": {
"@eslint/compat": "^2.0.2", "@eslint/compat": "^2.0.2",
"@eslint/js": "^9.39.2", "@eslint/js": "^9.39.2",
"@owlboard/api-schema-types": "^3.0.2-alpha1",
"@owlboard/owlboard-ts": "^3.0.0-dev.20260324T1240",
"@playwright/test": "^1.58.1", "@playwright/test": "^1.58.1",
"@sveltejs/adapter-static": "^3.0.10", "@sveltejs/adapter-static": "^3.0.10",
"@sveltejs/kit": "^2.50.2", "@sveltejs/kit": "^2.50.2",
@@ -778,6 +780,23 @@
"@jridgewell/sourcemap-codec": "^1.4.14" "@jridgewell/sourcemap-codec": "^1.4.14"
} }
}, },
"node_modules/@owlboard/api-schema-types": {
"version": "3.0.2-alpha1",
"resolved": "https://git.fjla.uk/api/packages/OwlBoard/npm/%40owlboard%2Fapi-schema-types/-/3.0.2-alpha1/api-schema-types-3.0.2-alpha1.tgz",
"integrity": "sha512-3yqWw28y2DZQmNXgAz8emCN5avX/upBXrTOXj9XLuay3gdVcdELd7BiYODBWfgtwZnSbT0fCgVXgKeTzbhHoSQ==",
"dev": true,
"license": "MIT"
},
"node_modules/@owlboard/owlboard-ts": {
"version": "3.0.0-dev.20260324T1240",
"resolved": "https://git.fjla.uk/api/packages/OwlBoard/npm/%40owlboard%2Fowlboard-ts/-/3.0.0-dev.20260324T1240/owlboard-ts-3.0.0-dev.20260324t1240.tgz",
"integrity": "sha512-s528RtkKLZmx6jZPdj159eKOBEmDHAjKDV0dSEU8/55JMt+7cSXYEqdXC3Cqs6t39wDxsOaPe8P0Q2z6P+d0jg==",
"dev": true,
"license": "GPL-3.0",
"dependencies": {
"@owlboard/api-schema-types": "^3.0.2-alpha1"
}
},
"node_modules/@playwright/test": { "node_modules/@playwright/test": {
"version": "1.58.2", "version": "1.58.2",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.58.2.tgz", "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.58.2.tgz",
@@ -2452,9 +2471,9 @@
} }
}, },
"node_modules/flatted": { "node_modules/flatted": {
"version": "3.4.1", "version": "3.4.2",
"resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.1.tgz", "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz",
"integrity": "sha512-IxfVbRFVlV8V/yRaGzk0UVIcsKKHMSfYw66T/u4nTwlWteQePsxe//LjudR1AMX4tZW3WFCh3Zqa/sjlqpbURQ==", "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==",
"dev": true, "dev": true,
"license": "ISC" "license": "ISC"
}, },

View File

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

View File

@@ -4,7 +4,7 @@
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="manifest" href="/manifest.webmanifest" /> <link rel="manifest" href="/manifest.webmanifest" />
<meta name="title" content="OwlBoard | Your fasted route to live and reference data" /> <meta name="title" content="OwlBoard | Your fastest route to live and reference data" />
<meta <meta
name="description" name="description"
content="Live station departures, Live train tracking, PIS Codes & more" content="Live station departures, Live train tracking, PIS Codes & more"

View File

@@ -4,62 +4,48 @@
import { fade } from 'svelte/transition'; import { fade } from 'svelte/transition';
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
interface LocationRecord { import { LOCATIONS } from '$lib/locations-object.svelte';
n: string; // name import type { ApiLocationFilter } from '@owlboard/api-schema-types';
t: string; // tiploc
c?: string; // crs
s: string; // search string
}
let { value = $bindable() } = $props(); let { value = $bindable() } = $props();
let results = $state<LocationRecord[]>([]);
let locations: LocationRecord[] = [];
let showResults = $state(false); let showResults = $state(false);
let selectedIndex = $state(-1); let selectedIndex = $state(-1);
const MAX_RESULTS = 5; const MAX_RESULTS = 5;
async function loadLocations() {
const res = await fetch('/api/tiplocs');
locations = await res.json();
}
onMount(loadLocations);
function tokenize(query: string) { function tokenize(query: string) {
return query.toLowerCase().trim().split(/\s+/).filter(Boolean); return query.toLowerCase().trim().split(/\s+/).filter(Boolean);
} }
function search(query: string) { let results = $derived.by(() => {
if (query.length < 3) { if (value.length < 3) return [];
results = [];
return;
}
const tokens = tokenize(query); const tokens = tokenize(value);
const lowerQuery = query.toLowerCase().trim(); const lowerQuery = value.toLowerCase().trim();
results = locations return LOCATIONS.data
.filter((r) => tokens.every((t) => r.s.includes(t))) .filter((r) => tokens.every((t) => r.s.includes(t)))
.sort((a, b) => { .sort((a, b) => {
// Check if query matches CRS
const aIsCrs = a.c?.toLowerCase() === lowerQuery;
const bIsCrs = b.c?.toLowerCase() === lowerQuery;
// Sort matching CRS first // Priority One - Exact CRS Match
if (aIsCrs && !bIsCrs) return -1; const aExactCrs = a.c?.toLowerCase() === lowerQuery;
if (!aIsCrs && bIsCrs) return 1; 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 // Alphabetical Sort
return a.n.localeCompare(b.n); return a.n.localeCompare(b.n);
}) })
.slice(0, MAX_RESULTS); .slice(0, MAX_RESULTS);
} });
$effect(() => { $effect(() => {
search(value); if (results) selectedIndex = -1;
}); });
// Hide results when click outside of container // Hide results when click outside of container
@@ -77,12 +63,12 @@
} }
}); });
function choose(loc: LocationRecord) { function choose(loc: ApiLocationFilter.LocationFilterObject) {
showResults = false; showResults = false;
selectedIndex = -1; selectedIndex = -1;
value = ''; value = '';
console.log('Selected Location: ', JSON.stringify(loc)); console.log('Selected Location: ', JSON.stringify(loc));
const queryString = loc.c || loc.t const queryString = loc.c || loc.t;
goto(`/board?loc=${queryString.toLowerCase()}`); goto(`/board?loc=${queryString.toLowerCase()}`);
} }
@@ -213,6 +199,7 @@
font-size: 1.1rem; font-size: 1.1rem;
font-weight: 700; font-weight: 700;
text-align: right; text-align: right;
text-transform: capitalize;
} }
.tiploc { .tiploc {

View File

@@ -72,6 +72,7 @@
transition: all 0.2s ease-in-out; transition: all 0.2s ease-in-out;
outline: none; outline: none;
text-align: center; text-align: center;
box-shadow: var(--shadow-std);
} }
.all-caps { .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; font-family: 'URW Gothic', sans-serif;
color: var(--color-brand); color: var(--color-brand);
padding: 10px 0; padding: 10px 0;
box-shadow: var(--shadow-std);
} }
.header-content { .header-content {

View File

@@ -0,0 +1,54 @@
<script lang="ts">
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 {
codeValue = '';
}
</script>
<BaseCard header={'Find by Code'}>
<div class="card-content">
<div class="textbox-container">
<div class="textbox-item-wrapper">
<Textbox placeholder={"Code"} uppercase={true} type={'number'} max={9999} bind:value={codeValue} />
</div>
</div>
<div class="button-wrapper">
<Button onclick={() => onsearch(codeValue.toString())}>Search</Button>
<Button onclick={resetValues}>Reset</Button>
</div>
</div>
</BaseCard>
<style>
.card-content {
text-align: center;
width: 90%;
margin: auto;
padding: 10px 0 10px 0;
}
.textbox-container {
display: flex;
width: 100%;
justify-content: center;
gap: 4rem;
}
.textbox-item-wrapper {
width: 30%;
}
.button-wrapper {
display: flex;
justify-content: center;
gap: 1rem;
margin-top: 15px;
}
</style>

View File

@@ -0,0 +1,60 @@
<script lang="ts">
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('');
function resetValues(): void {
startValue = '';
endValue = '';
}
</script>
<BaseCard header={'Find by Start/End CRS'}>
<div class="card-content">
<div class="textbox-container">
<div class="textbox-item-wrapper">
<Textbox placeholder={"Start"} uppercase={true} maxLength={3} bind:value={startValue} />
</div>
<div class="textbox-item-wrapper">
<Textbox placeholder={"End"} uppercase={true} maxLength={3} bind:value={endValue} />
</div>
</div>
<div class="button-wrapper">
<Button onclick={() => onsearch(startValue, endValue)}>Search</Button>
<Button onclick={resetValues}>Reset</Button>
</div>
</div>
</BaseCard>
<style>
.card-content {
text-align: center;
width: 90%;
margin: auto;
padding: 10px 0 10px 0;
}
.textbox-container {
display: flex;
width: 100%;
justify-content: center;
gap: 4rem;
}
.textbox-item-wrapper {
width: 30%;
}
.button-wrapper {
display: flex;
justify-content: center;
gap: 1rem;
margin-top: 15px;
}
</style>

View File

@@ -0,0 +1,33 @@
import { OwlClient } from "./owlClient";
import type { ApiLocationFilter } from '@owlboard/owlboard-ts'
class LocationStore {
data = $state<ApiLocationFilter.LocationFilterObject[]>([]);
loaded = $state(false);
async init() {
if (this.loaded) return;
try {
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): ApiLocationFilter.LocationFilterObject | undefined {
if (!id) return undefined;
const query = id.toUpperCase().trim();
console.log(query);
return this.data.find((loc) => {
return loc.t === query || loc.c === query;
});
}
}
export const LOCATIONS = new LocationStore();

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

@@ -59,12 +59,14 @@
color: var(--color-title); color: var(--color-title);
max-width: 300px; max-width: 300px;
margin-top: 5px; margin-top: 5px;
margin-bottom: 30px; margin-bottom: 20px;
} }
.debug-info { .debug-info {
background: rgba(255, 255, 255, 0.05); background: rgba(255, 255, 255, 0.05);
padding: 5px 12px; padding: 5px 15px;
margin-top: 0;
margin-bottom: 20px;
border-radius: 4px; border-radius: 4px;
font-size: 0.8rem; font-size: 0.8rem;
opacity: 0.6; opacity: 0.6;

View File

@@ -1,6 +1,9 @@
<script lang="ts"> <script lang="ts">
import { page } from '$app/state'; import { page } from '$app/state';
import { slide, fade } from 'svelte/transition'; import { slide, fade } from 'svelte/transition';
import { onMount } from 'svelte';
import { LOCATIONS } from '$lib/locations-object.svelte';
import '$lib/global.css'; import '$lib/global.css';
@@ -10,6 +13,8 @@
import { IconHome, IconDialpad, IconSettings, IconHelp, IconDots } from '@tabler/icons-svelte'; import { IconHome, IconDialpad, IconSettings, IconHelp, IconDots } from '@tabler/icons-svelte';
onMount(() => LOCATIONS.init());
let { children } = $props(); let { children } = $props();
// Navigation State // Navigation State
@@ -18,9 +23,9 @@
const navItems = [ const navItems = [
{ label: 'Home', path: '/', icon: IconHome }, { label: 'Home', path: '/', icon: IconHome },
{ label: 'PIS', path: '/pis', icon: IconDialpad }, { label: 'PIS', path: '/pis/', icon: IconDialpad },
{ label: 'Options', path: '/preferences', icon: IconSettings }, { label: 'Options', path: '/preferences/', icon: IconSettings },
{ label: 'About', path: '/about', icon: IconHelp } { label: 'About', path: '/about/', icon: IconHelp }
]; ];
let navWidth = $state(0); let navWidth = $state(0);
@@ -78,7 +83,7 @@
<nav bind:clientWidth={navWidth}> <nav bind:clientWidth={navWidth}>
<!-- Dynamic Nav Elements Here! --> <!-- Dynamic Nav Elements Here! -->
{#each visibleItems as item} {#each visibleItems as item}
{@const isActive = activePath === item.path} {@const isActive = activePath.replace(/\/$/, '') === item.path.replace(/\/$/, '')}
<a <a
href={item.path} href={item.path}
class="nav-item" class="nav-item"
@@ -110,7 +115,7 @@
></div> ></div>
<div class="menu-popover" transition:slide={{ axis: 'y', duration: 250 }}> <div class="menu-popover" transition:slide={{ axis: 'y', duration: 250 }}>
{#each hiddenItems as item} {#each hiddenItems as item}
{@const isActive = activePath === item.path} {@const isActive = activePath.replace(/\/$/, '') === item.path.replace(/\/$/, '')}
<a <a
href={item.path} href={item.path}
class="menu-popover-item" class="menu-popover-item"
@@ -157,10 +162,12 @@
.page-title { .page-title {
font-family: 'URW Gothic', sans-serif; font-family: 'URW Gothic', sans-serif;
font-weight: 600; font-weight: 600;
font-size: clamp(0.9rem, 2.5vw + 0.8rem, 2rem);
font-style: normal; font-style: normal;
margin-left: 5px; margin-left: 5px;
padding-bottom: 2px; padding-bottom: 2px;
color: var(--color-title); color: var(--color-title);
text-transform: capitalize;
} }
header, header,
nav { nav {

View File

@@ -1,3 +1,4 @@
export const prerender = true; export const prerender = true;
export const trailingSlash = 'always'; export const trailingSlash = 'always';
export const csr = true; export const csr = true;
export const ssr = false;

View File

@@ -30,8 +30,8 @@
</p> </p>
<p class="amble"> <p class="amble">
Why OwlBoard? The name was chosen as an evolution of its predecessor, 'Athena'; owls are 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 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, representing the 'Minerva Owl' sculpture trail in the city, with 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. many of the sculptures still in the area.
</p> </p>
<p class="opensource"> <p class="opensource">

View File

@@ -0,0 +1,13 @@
<section>Live board are not yet implemented on the server</section>
<style>
section {
font-family: 'URW Gothic', sans-serif;
text-align: center;
font-size: 2rem;
width: 90%;
margin: auto;
padding-top: 25px;
max-width: 500px;
}
</style>

37
src/routes/board/+page.ts Normal file
View File

@@ -0,0 +1,37 @@
import { LOCATIONS } from '$lib/locations-object.svelte';
import type { PageLoad } from './$types';
import { error } from '@sveltejs/kit';
export const load: PageLoad = async ({ url }) => {
const locId = url.searchParams.get('loc');
if (!LOCATIONS.loaded) {
await LOCATIONS.init();
}
let title: string = '';
if (!locId) {
error(400, {
message: 'Location not provided',
owlCode: 'NO_LOCATION_IN_PATH'
});
}
if (locId) {
const location = LOCATIONS.find(locId);
if (location) {
title = location.n || location.t;
} else {
error(404, {
message: `Location (${locId.toUpperCase()}) not found`,
owlCode: 'INVALID_LOCATION_CODE'
});
}
}
return {
title,
location
};
};

158
src/routes/pis/+page.svelte Normal file
View File

@@ -0,0 +1,158 @@
<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>
{#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 {
display: flex;
align-items: center;
flex-direction: column;
gap: 20px;
justify-content: center;
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;
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>

5
src/routes/pis/+page.ts Normal file
View File

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

View File

@@ -99,5 +99,8 @@
{"n":"Luton Airport Sidings","t":"LUTSID","c":"","s":"luton airport sidings lutsid"}, {"n":"Luton Airport Sidings","t":"LUTSID","c":"","s":"luton airport sidings lutsid"},
{"n":"Stevenage Hitchin Junction","t":"STHJC","c":"","s":"stevenage hitchin junction sthjc"}, {"n":"Stevenage Hitchin Junction","t":"STHJC","c":"","s":"stevenage hitchin junction sthjc"},
{"n":"Chelmsford New Hall Junction","t":"CHNJCT","c":"","s":"chelmsford new hall junction chnjct"}, {"n":"Chelmsford New Hall Junction","t":"CHNJCT","c":"","s":"chelmsford new hall junction chnjct"},
{"n":"Ipswich Derby Road Depot","t":"IPDRDP","c":"","s":"ipswich derby road depot ipdrdp"} {"n":"","t":"BPWY532","c":"","s":"bpwy532"},
{"n":"Ipswich Derby Road Depot","t":"IPDRDP","c":"","s":"ipswich derby road depot ipdrdp"},
{"n":"Rhoose Cardiff International Airport","c":"RIA","t":"RHOOSE","s":"rhoose cardiff international airport ria"},
{"n":"Southampton Airport Parkway","c":"SOA","t":"SOTAPT","s":"southampton airport parkway soa sotapt"}
] ]