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

@@ -27,7 +27,6 @@
return LOCATIONS.data
.filter((r) => tokens.every((t) => r.s.includes(t)))
.sort((a, b) => {
// Priority One - Exact CRS Match
const aExactCrs = a.c?.toLowerCase() === lowerQuery;
const bExactCrs = b.c?.toLowerCase() === lowerQuery;
@@ -36,7 +35,7 @@
// Priority Two - 'Stations' with CRS
if (!!a.c && !b.c) return -1;
if (!a.c & !! b.c) return 1;
if (!a.c & !!b.c) return 1;
// Alphabetical Sort
return a.n.localeCompare(b.n);

View File

@@ -1,53 +1,56 @@
<script lang="ts">
interface Props {
toc: string;
}
interface Props {
toc: string;
}
let {
toc
}: Props = $props();
let { toc }: Props = $props();
let code = $derived(toc.toUpperCase());
let code = $derived(toc.toUpperCase());
</script>
<div class="toc-container {code}">
{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;
}
.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;
}
.GW {
/* Great Western Railway */
background: #004225;
color: #e2e2e2;
}
.GR { /* LNER */
background-color: #C00000;
color: #FFFFFF;
}
.GR {
/* LNER */
background-color: #c00000;
color: #ffffff;
}
.VT { /* Avanti West Coast */
background-color: #004354;
color: #FFFFFF;
}
.VT {
/* Avanti West Coast */
background-color: #004354;
color: #ffffff;
}
.SW { /* South Western Railway */
background-color: #2A3389;
color: #FFFFFF;
}
.SW {
/* South Western Railway */
background-color: #2a3389;
color: #ffffff;
}
.XC { /* CrossCountry */
background-color: #660000;
color: #E4D5B1;
}
</style>
.XC {
/* CrossCountry */
background-color: #660000;
color: #e4d5b1;
}
</style>

View File

@@ -0,0 +1,69 @@
<script lang="ts">
import BaseCard from '$lib/components/ui/cards/BaseCard.svelte';
import Button from '$lib/components/ui/Button.svelte';
import { fade } from 'svelte/transition';
import { flip } from 'svelte/animate';
import { nearestStationsState } from '$lib/geohash.svelte';
const flipDuration = 300;
</script>
<BaseCard header={'Nearby Stations'}>
<div class="card-content">
{#if nearestStationsState.error && nearestStationsState.list.length === 0}
<p class="msg">{nearestStationsState.error}</p>
{:else if nearestStationsState.loading && nearestStationsState.list.length === 0}
<p class="msg">Locating stations...</p>
{:else}
<div class="stations-flex">
{#each nearestStationsState.list as station (station.c)}
<div
class="btn-container"
animate:flip={{ duration: flipDuration }}
in:fade={{ duration: 200, delay: 100 }}
out:fade={{ duration: 150 }}
>
<Button href={`/board?loc=${station.c}`}
><span class="stn-name">{station.n}</span></Button
>
</div>
{/each}
</div>
{/if}
</div>
</BaseCard>
<style>
.card-content {
text-align: center;
width: 90%;
margin: auto;
padding: 10px 0 10px 0;
}
.stations-flex {
display: flex;
flex-wrap: wrap;
gap: 0.75rem;
justify-content: center;
align-items: flex-start;
}
.btn-container {
display: block;
width: fit-content;
will-change: transform;
}
.msg {
font-size: 1.1rem;
font-weight: 600;
color: var(--color-title);
}
.stn-name {
text-transform: capitalize;
}
</style>

View File

@@ -1,28 +1,34 @@
<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';
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 { onsearch }: { onsearch: (c: string) => void } = $props();
let codeValue = $state('');
let codeValue = $state('');
function resetValues(): void {
codeValue = '';
}
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 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>
@@ -34,21 +40,21 @@ function resetValues(): void {
padding: 10px 0 10px 0;
}
.textbox-container {
display: flex;
width: 100%;
justify-content: center;
gap: 4rem;
}
.textbox-container {
display: flex;
width: 100%;
justify-content: center;
gap: 4rem;
}
.textbox-item-wrapper {
width: 30%;
}
.textbox-item-wrapper {
width: 30%;
}
.button-wrapper {
display: flex;
justify-content: center;
gap: 1rem;
margin-top: 15px;
}
</style>
.button-wrapper {
display: flex;
justify-content: center;
gap: 1rem;
margin-top: 15px;
}
</style>

View File

@@ -1,34 +1,33 @@
<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';
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 { onsearch }: { onsearch: (s: string, e: string) => void } = $props();
let startValue = $state('');
let endValue = $state('');
let startValue = $state('');
let endValue = $state('');
function resetValues(): void {
startValue = '';
endValue = '';
}
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 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>
@@ -40,21 +39,21 @@ function resetValues(): void {
padding: 10px 0 10px 0;
}
.textbox-container {
display: flex;
width: 100%;
justify-content: center;
gap: 4rem;
}
.textbox-container {
display: flex;
width: 100%;
justify-content: center;
gap: 4rem;
}
.textbox-item-wrapper {
width: 30%;
}
.textbox-item-wrapper {
width: 30%;
}
.button-wrapper {
display: flex;
justify-content: center;
gap: 1rem;
margin-top: 15px;
}
</style>
.button-wrapper {
display: flex;
justify-content: center;
gap: 1rem;
margin-top: 15px;
}
</style>