Compare commits
21 Commits
v3.0.0-dev
...
fd213d6340
| Author | SHA1 | Date | |
|---|---|---|---|
| fd213d6340 | |||
| 3eceddf20a | |||
| 1d461780ab | |||
| ec4dd5dd3b | |||
| a7c244171c | |||
| 3467f97889 | |||
| b1d8eea518 | |||
| deb151075a | |||
| d9b60daa8b | |||
| 2f0a6b9646 | |||
| 1165c02e26 | |||
| 45dd5a1cf5 | |||
| e47bebe7d4 | |||
| b7007d2fb3 | |||
| 64bc5b979d | |||
| 3240560a0b | |||
| a327582629 | |||
| 35877ae8ac | |||
| f5c3775f59 | |||
| e5d3c0a3a7 | |||
| 1b0b93b34b |
@@ -3,7 +3,7 @@ run-name: ${{ gitea.actor }} is building and pushing
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
create:
|
create:
|
||||||
tags: "*"
|
tags: '*'
|
||||||
|
|
||||||
env:
|
env:
|
||||||
GITEA_DOMAIN: git.fjla.uk
|
GITEA_DOMAIN: git.fjla.uk
|
||||||
@@ -36,4 +36,4 @@ jobs:
|
|||||||
push: true
|
push: true
|
||||||
tags: |
|
tags: |
|
||||||
${{ env.GITEA_DOMAIN }}/${{ env.RESULT_IMAGE_NAME }}:${{ gitea.ref_name }}
|
${{ env.GITEA_DOMAIN }}/${{ env.RESULT_IMAGE_NAME }}:${{ gitea.ref_name }}
|
||||||
${{ env.GITEA_DOMAIN }}/${{ env.RESULT_IMAGE_NAME }}:latest
|
${{ env.GITEA_DOMAIN }}/${{ env.RESULT_IMAGE_NAME }}:latest
|
||||||
|
|||||||
1
.npmrc
1
.npmrc
@@ -1 +1,2 @@
|
|||||||
engine-strict=true
|
engine-strict=true
|
||||||
|
@owlboard:registry=https://git.fjla.uk/api/packages/OwlBoard/npm/
|
||||||
25
package-lock.json
generated
25
package-lock.json
generated
@@ -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"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
209
src/lib/components/ui/LocationSearchBox.svelte
Normal file
209
src/lib/components/ui/LocationSearchBox.svelte
Normal file
@@ -0,0 +1,209 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import Textbox from '$lib/components/ui/Textbox.svelte';
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
import { fade } from 'svelte/transition';
|
||||||
|
import { goto } from '$app/navigation';
|
||||||
|
|
||||||
|
import { LOCATIONS } from '$lib/locations-object.svelte';
|
||||||
|
import type { ApiLocationFilter } from '@owlboard/api-schema-types';
|
||||||
|
|
||||||
|
let { value = $bindable() } = $props();
|
||||||
|
|
||||||
|
let showResults = $state(false);
|
||||||
|
let selectedIndex = $state(-1);
|
||||||
|
|
||||||
|
const MAX_RESULTS = 5;
|
||||||
|
|
||||||
|
function tokenize(query: string) {
|
||||||
|
return query.toLowerCase().trim().split(/\s+/).filter(Boolean);
|
||||||
|
}
|
||||||
|
|
||||||
|
let results = $derived.by(() => {
|
||||||
|
if (value.length < 3) return [];
|
||||||
|
|
||||||
|
const tokens = tokenize(value);
|
||||||
|
const lowerQuery = value.toLowerCase().trim();
|
||||||
|
|
||||||
|
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;
|
||||||
|
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);
|
||||||
|
})
|
||||||
|
.slice(0, MAX_RESULTS);
|
||||||
|
});
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
if (results) selectedIndex = -1;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Hide results when click outside of container
|
||||||
|
$effect(() => {
|
||||||
|
if (showResults) {
|
||||||
|
const onClick = (e: MouseEvent) => {
|
||||||
|
const target = e.target as HTMLElement;
|
||||||
|
if (!target.closest('.location-search')) {
|
||||||
|
showResults = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener('click', onClick);
|
||||||
|
return () => document.removeEventListener('click', onClick);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function choose(loc: ApiLocationFilter.LocationFilterObject) {
|
||||||
|
showResults = false;
|
||||||
|
selectedIndex = -1;
|
||||||
|
value = '';
|
||||||
|
console.log('Selected Location: ', JSON.stringify(loc));
|
||||||
|
const queryString = loc.c || loc.t;
|
||||||
|
goto(`/board?loc=${queryString.toLowerCase()}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleKey(e: KeyboardEvent) {
|
||||||
|
if (!results.length) return;
|
||||||
|
|
||||||
|
if (e.key === 'ArrowDown') {
|
||||||
|
e.preventDefault();
|
||||||
|
selectedIndex = Math.min(selectedIndex + 1, results.length - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e.key === 'ArrowUp') {
|
||||||
|
e.preventDefault();
|
||||||
|
selectedIndex = Math.max(selectedIndex - 1, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e.key === 'Enter' && selectedIndex >= 0) {
|
||||||
|
choose(results[selectedIndex]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="location-search">
|
||||||
|
<Textbox
|
||||||
|
bind:value
|
||||||
|
placeholder="Enter Location"
|
||||||
|
oninput={() => (showResults = true)}
|
||||||
|
onkeydown={handleKey}
|
||||||
|
capital
|
||||||
|
/>
|
||||||
|
|
||||||
|
{#if showResults && results.length}
|
||||||
|
<ul
|
||||||
|
id="location-results"
|
||||||
|
popover={showResults && results.length ? 'manual' : null}
|
||||||
|
role="listbox"
|
||||||
|
class="suggestions"
|
||||||
|
transition:fade={{ duration: 200 }}
|
||||||
|
>
|
||||||
|
{#each results as loc, i}
|
||||||
|
<li class="result-item" class:selected={i === selectedIndex} onclick={() => choose(loc)}>
|
||||||
|
<div class="crs-badge-container">
|
||||||
|
{#if loc.c}
|
||||||
|
<span class="crs-badge">{loc.c}</span>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<div class="details">
|
||||||
|
<span class="name">{loc.n || loc.t}</span>
|
||||||
|
<span class="tiploc">{loc.t}</span>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
{/each}
|
||||||
|
</ul>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.location-search {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.suggestions[popover] {
|
||||||
|
position: absolute;
|
||||||
|
inset: unset;
|
||||||
|
margin: 0;
|
||||||
|
margin-top: 3px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 5px;
|
||||||
|
padding: 0;
|
||||||
|
width: 100%;
|
||||||
|
max-height: 350px;
|
||||||
|
top: 100%;
|
||||||
|
background-color: var(--color-title);
|
||||||
|
color: var(--color-bg-dark);
|
||||||
|
box-shadow: var(--shadow-std);
|
||||||
|
display: block;
|
||||||
|
z-index: 9999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.suggestions:not([popover]) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-item {
|
||||||
|
font-family: 'URW Gothic', sans-serif;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
cursor: pointer;
|
||||||
|
min-height: 48px;
|
||||||
|
transition: all 0.15s;
|
||||||
|
border-bottom: 1px solid rgba(0, 0, 0, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-item.selected,
|
||||||
|
.result-item:hover {
|
||||||
|
background-color: var(--color-accent);
|
||||||
|
color: var(--color-title);
|
||||||
|
}
|
||||||
|
|
||||||
|
.crs-badge {
|
||||||
|
font-family: ui-monospace, monospace;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
font-weight: 700;
|
||||||
|
background: var(--color-accent);
|
||||||
|
color: var(--color-title);
|
||||||
|
padding: 3px 6px;
|
||||||
|
border-radius: 10px;
|
||||||
|
transition: all 0.1s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.crs-badge.empty {
|
||||||
|
filter: opacity(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-item:hover .crs-badge {
|
||||||
|
filter: brightness(1.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.details {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.name {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
font-weight: 700;
|
||||||
|
text-align: right;
|
||||||
|
text-transform: capitalize;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tiploc {
|
||||||
|
text-align: right;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,94 +1,95 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { fade } from 'svelte/transition';
|
import { fade } from 'svelte/transition';
|
||||||
import type { HTLMInputAttributes } from 'svelte/elements';
|
import type { HTMLInputAttributes } from 'svelte/elements';
|
||||||
|
|
||||||
interface Props extends HTMLInputAttributes {
|
interface Props extends HTMLInputAttributes {
|
||||||
value?: string;
|
value?: string;
|
||||||
label?: string;
|
label?: string;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
type?: 'text' | 'password' | 'email' | 'number' | 'search' | 'tel' | 'url';
|
type?: 'text' | 'password' | 'email' | 'number' | 'search' | 'tel' | 'url';
|
||||||
error?: string;
|
error?: string;
|
||||||
uppercase?: boolean;
|
uppercase?: boolean;
|
||||||
[key: string]: any;
|
[key: string]: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
let {
|
let {
|
||||||
value = $bindable(''),
|
value = $bindable(''),
|
||||||
label,
|
label,
|
||||||
placeholder = '',
|
placeholder = '',
|
||||||
type = 'text',
|
type = 'text',
|
||||||
error = '',
|
error = '',
|
||||||
uppercase = false,
|
uppercase = false,
|
||||||
...rest
|
...rest
|
||||||
}: Props = $props();
|
}: Props = $props();
|
||||||
|
|
||||||
let isFocussed = $state(false);
|
let isFocussed = $state(false);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="input-wrapper" class:focussed={isFocussed} class:has-error={!!error}>
|
<div class="input-wrapper" class:focussed={isFocussed} class:has-error={!!error}>
|
||||||
{#if label}
|
{#if label}
|
||||||
<label for="adaptive-input">{label}</label>
|
<label for="adaptive-input">{label}</label>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<input
|
<input
|
||||||
id="adaptive-input"
|
id="adaptive-input"
|
||||||
class:all-caps={uppercase}
|
class:all-caps={uppercase}
|
||||||
{type}
|
{type}
|
||||||
{placeholder}
|
{placeholder}
|
||||||
bind:value={value}
|
bind:value
|
||||||
onfocus={() => isFocussed = true}
|
onfocus={() => (isFocussed = true)}
|
||||||
onblur={() => isFocussed = false}
|
onblur={() => (isFocussed = false)}
|
||||||
{...rest}
|
{...rest}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{#if error}
|
{#if error}
|
||||||
<span class="error-message" transition:fade>{error}</span>
|
<span class="error-message" transition:fade>{error}</span>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.input-wrapper {
|
.input-wrapper {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
font-family: 'URW Gothic', sans-serif;
|
font-family: 'URW Gothic', sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
label {
|
label {
|
||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
color: var(--color-title)
|
color: var(--color-title);
|
||||||
}
|
}
|
||||||
|
|
||||||
input {
|
input {
|
||||||
min-height: 48px;
|
min-height: 40px;
|
||||||
padding: 0 16px;
|
padding: 0 16px;
|
||||||
background-color: var(--color-title);
|
background-color: var(--color-title);
|
||||||
border: 2px solid transparent;
|
border: 2px solid transparent;
|
||||||
border-radius: 20px;
|
border-radius: 20px;
|
||||||
color: var(--color-bg-dark);
|
color: var(--color-bg-dark);
|
||||||
font-size: 1.5rem;
|
font-size: 1.2rem;
|
||||||
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 {
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
}
|
}
|
||||||
|
|
||||||
.focussed input {
|
.focussed input {
|
||||||
border-color: var(--color-bg-light);
|
border-color: var(--color-bg-light);
|
||||||
}
|
}
|
||||||
|
|
||||||
.has-error input {
|
.has-error input {
|
||||||
border-color: #ff4d4d;
|
border-color: #ff4d4d;
|
||||||
}
|
}
|
||||||
|
|
||||||
.error-message {
|
.error-message {
|
||||||
color: #ff4d4d;
|
color: #ff4d4d;
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
53
src/lib/components/ui/TocStyle.svelte
Normal file
53
src/lib/components/ui/TocStyle.svelte
Normal 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>
|
||||||
@@ -1,94 +1,102 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { Snippet } from 'svelte';
|
import type { Snippet } from 'svelte';
|
||||||
import { IconHelpCircle } from '@tabler/icons-svelte';
|
import { IconHelpCircle } from '@tabler/icons-svelte';
|
||||||
import { slide } from 'svelte/transition';
|
import { slide } from 'svelte/transition';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
children: Snippet;
|
children: Snippet;
|
||||||
header?: string;
|
header?: string;
|
||||||
helpText?: string;
|
helpText?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
let {
|
let { children, header = '', helpText }: Props = $props();
|
||||||
children,
|
|
||||||
header = "",
|
|
||||||
helpText,
|
|
||||||
}: Props = $props();
|
|
||||||
|
|
||||||
let showHelp = $state(false);
|
let showHelp = $state(false);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="card">
|
<div class="card">
|
||||||
{#if header || helpText}
|
{#if header || helpText}
|
||||||
<header class="card-header">
|
<header class="card-header">
|
||||||
<div class="header-content">
|
<div class="header-content">
|
||||||
{header}
|
{header}
|
||||||
</div>
|
</div>
|
||||||
{#if helpText}
|
{#if helpText}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="help-toggle"
|
class="help-toggle"
|
||||||
onclick={() => showHelp = !showHelp}
|
onclick={() => (showHelp = !showHelp)}
|
||||||
aria-label="Show Help"
|
aria-label="Show Help"
|
||||||
>
|
>
|
||||||
<IconHelpCircle size={26} stroke={2.25} color={showHelp ? 'var(--color-brand)' : 'var(--color-title)'} />
|
<IconHelpCircle
|
||||||
</button>
|
size={26}
|
||||||
{/if}
|
stroke={2.25}
|
||||||
</header>
|
color={showHelp ? 'var(--color-brand)' : 'var(--color-title)'}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
</header>
|
||||||
|
|
||||||
{#if showHelp && helpText}
|
{#if showHelp && helpText}
|
||||||
<div class="help-drawer" transition:slide={{ duration: 400 }}>
|
<div class="help-drawer" transition:slide={{ duration: 400 }}>
|
||||||
<p>{helpText}</p>
|
<p>{helpText}</p>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
{@render children?.()}
|
{@render children?.()}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.card {
|
.card {
|
||||||
background: var(--color-accent);
|
background: var(--color-accent);
|
||||||
position: relative;
|
position: relative;
|
||||||
border-radius: 20px;
|
border-radius: 20px;
|
||||||
overflow: hidden;
|
overflow: visible;
|
||||||
width: 95%;
|
width: 95%;
|
||||||
max-width: 600px;
|
max-width: 600px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
font-family: 'URW Gothic', sans-serif;
|
font-family: 'URW Gothic', sans-serif;
|
||||||
color: var(--color-title);
|
color: var(--color-brand);
|
||||||
}
|
padding: 10px 0;
|
||||||
|
box-shadow: var(--shadow-std);
|
||||||
|
}
|
||||||
|
|
||||||
.header-content { flex: 1;
|
.header-content {
|
||||||
font-size: 1.5rem; font-weight: 600; }
|
flex: 1;
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.3rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
.help-toggle {
|
.help-toggle {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 8px;
|
top: 8px;
|
||||||
right: 8px;
|
right: 8px;
|
||||||
background: none;
|
background: none;
|
||||||
border: none;
|
border: none;
|
||||||
padding: 4px;
|
padding: 4px;
|
||||||
cursor: help;
|
cursor: help;
|
||||||
opacity: 0.6;
|
opacity: 0.6;
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
transition: opacity 0.2s, transform 0.2s;
|
transition:
|
||||||
}
|
opacity 0.2s,
|
||||||
|
transform 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
.help-toggle:hover {
|
.help-toggle:hover {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
transform: scale(1.1);
|
transform: scale(1.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.help-drawer {
|
.help-drawer {
|
||||||
background-color: var(--color-accent);
|
background-color: var(--color-accent);
|
||||||
padding: 4px 16px;
|
padding: 4px 16px;
|
||||||
font-size: 0.95rem;
|
font-size: 0.95rem;
|
||||||
line-height: 1.2;
|
line-height: 1.2;
|
||||||
margin: auto;
|
margin: auto;
|
||||||
border-bottom: 1px solid rgba(255,255,255,0.05);
|
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
|
||||||
color: var(--color-title);
|
color: var(--color-title);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
25
src/lib/components/ui/cards/LocationBoardCard.svelte
Normal file
25
src/lib/components/ui/cards/LocationBoardCard.svelte
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import BaseCard from '$lib/components/ui/cards/BaseCard.svelte';
|
||||||
|
import LocationSearchBox from '$lib/components/ui/LocationSearchBox.svelte';
|
||||||
|
|
||||||
|
let locationValue = $state('');
|
||||||
|
|
||||||
|
function resetSearchBox() {
|
||||||
|
value = '';
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<BaseCard header={'Live Arrivals & Departures'}>
|
||||||
|
<div class="card-content">
|
||||||
|
<LocationSearchBox bind:value={locationValue} />
|
||||||
|
</div>
|
||||||
|
</BaseCard>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.card-content {
|
||||||
|
text-align: center;
|
||||||
|
width: 90%;
|
||||||
|
margin: auto;
|
||||||
|
padding: 10px 0 10px 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
54
src/lib/components/ui/cards/pis/PisCode.svelte
Normal file
54
src/lib/components/ui/cards/pis/PisCode.svelte
Normal 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>
|
||||||
60
src/lib/components/ui/cards/pis/PisStartEndCard.svelte
Normal file
60
src/lib/components/ui/cards/pis/PisStartEndCard.svelte
Normal 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>
|
||||||
33
src/lib/locations-object.svelte.ts
Normal file
33
src/lib/locations-object.svelte.ts
Normal 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
21
src/lib/owlClient.ts
Normal 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 };
|
||||||
@@ -58,13 +58,15 @@
|
|||||||
font-size: 1.1rem;
|
font-size: 1.1rem;
|
||||||
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;
|
||||||
|
|||||||
@@ -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);
|
||||||
@@ -34,7 +39,7 @@
|
|||||||
if (navWidth === 0) return navItems.length;
|
if (navWidth === 0) return navItems.length;
|
||||||
const available = navWidth;
|
const available = navWidth;
|
||||||
const totalItems = navItems.length;
|
const totalItems = navItems.length;
|
||||||
const countWithoutMore = Math.floor(available/ ITEM_WIDTH);
|
const countWithoutMore = Math.floor(available / ITEM_WIDTH);
|
||||||
|
|
||||||
if (countWithoutMore >= totalItems) return totalItems;
|
if (countWithoutMore >= totalItems) return totalItems;
|
||||||
|
|
||||||
@@ -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"
|
||||||
@@ -128,10 +133,11 @@
|
|||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<div class="viewport-guard">
|
<div class="viewport-guard">
|
||||||
<img src={logoPlain} alt="OwlBoard Logo" width=100 height=100>
|
<img src={logoPlain} alt="OwlBoard Logo" width="100" height="100" />
|
||||||
<h1 class="viewport-guard-title">Narrow Gauge Detected</h1>
|
<h1 class="viewport-guard-title">Narrow Gauge Detected</h1>
|
||||||
<p>
|
<p>
|
||||||
Just as trains need the right track width, our data needs a bit more room to stay on the rails. Please expand your view to at least 300px to view the app.
|
Just as trains need the right track width, our data needs a bit more room to stay on the rails.
|
||||||
|
Please expand your view to at least 300px to view the app.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -156,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 {
|
||||||
@@ -193,7 +201,8 @@
|
|||||||
box-shadow: var(--shadow-up);
|
box-shadow: var(--shadow-up);
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-item, .more-menu-wrapper {
|
.nav-item,
|
||||||
|
.more-menu-wrapper {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -317,8 +326,10 @@
|
|||||||
margin: auto;
|
margin: auto;
|
||||||
padding-top: 30px;
|
padding-top: 30px;
|
||||||
}
|
}
|
||||||
|
|
||||||
header, main, nav {
|
header,
|
||||||
|
main,
|
||||||
|
nav {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -1,19 +1,18 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Button from '$lib/components/ui/Button.svelte';
|
import LocationBoardCard from '$lib/components/ui/cards/LocationBoardCard.svelte';
|
||||||
import Textbox from '$lib/components/ui/Textbox.svelte';
|
|
||||||
import BaseCard from '$lib/components/ui/cards/BaseCard.svelte';
|
|
||||||
|
|
||||||
function test() {
|
|
||||||
console.log('Button Clicked');
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<br /><br /><br />
|
<div class="card-container">
|
||||||
<Button>Default</Button>
|
<LocationBoardCard />
|
||||||
<Button color={'brand'} onclick={test}>Brand</Button>
|
</div>
|
||||||
<Button color={'accent'}>Accent</Button>
|
|
||||||
<Textbox placeholder={"Textbox am I"} uppercase={true} error={""} />
|
|
||||||
|
|
||||||
<BaseCard header={"Hello"} helpText={"This is help text"}>Hello</BaseCard>
|
<style>
|
||||||
|
.card-container {
|
||||||
<h2>OwlBoard</h2>
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 20px;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 20px 10px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -29,7 +29,10 @@
|
|||||||
daily basis.
|
daily basis.
|
||||||
</p>
|
</p>
|
||||||
<p class="amble">
|
<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 many of the sculptures still in the area.
|
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.
|
||||||
</p>
|
</p>
|
||||||
<p class="opensource">
|
<p class="opensource">
|
||||||
Some components that combine to form OwlBoard are open-source, see the <a
|
Some components that combine to form OwlBoard are open-source, see the <a
|
||||||
|
|||||||
13
src/routes/board/+page.svelte
Normal file
13
src/routes/board/+page.svelte
Normal 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
37
src/routes/board/+page.ts
Normal 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
158
src/routes/pis/+page.svelte
Normal 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
5
src/routes/pis/+page.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
export const load = () => {
|
||||||
|
return {
|
||||||
|
title: 'PIS Codes'
|
||||||
|
};
|
||||||
|
};
|
||||||
106
static/api/tiplocs
Normal file
106
static/api/tiplocs
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
[
|
||||||
|
{"n":"Manchester Piccadilly","t":"MANPICD","c":"MAN","s":"manchester piccadilly man manpicd"},
|
||||||
|
{"n":"Manchester Victoria","t":"MCV","c":"MCV","s":"manchester victoria mcv"},
|
||||||
|
{"n":"Manchester Oxford Road","t":"MCOR","c":"MCO","s":"manchester oxford road mco mcor"},
|
||||||
|
{"n":"Manchester Airport","t":"MANAPTL","c":"MIA","s":"manchester airport mia manaptl"},
|
||||||
|
{"n":"London Euston","t":"EUSTON","c":"EUS","s":"london euston eus euston"},
|
||||||
|
{"n":"London Kings Cross","t":"KGX","c":"KGX","s":"london kings cross kgx kingscross"},
|
||||||
|
{"n":"London St Pancras International","t":"STPANCR","c":"STP","s":"london st pancras international stp stpancr"},
|
||||||
|
{"n":"London Paddington","t":"PADTON","c":"PAD","s":"london paddington pad padton"},
|
||||||
|
{"n":"London Victoria","t":"VIC","c":"VIC","s":"london victoria vic"},
|
||||||
|
{"n":"London Liverpool Street","t":"LIVST","c":"LST","s":"london liverpool street lst livst"},
|
||||||
|
{"n":"London Bridge","t":"LONGBR","c":"LBG","s":"london bridge lbg longbr"},
|
||||||
|
{"n":"Birmingham New Street","t":"BHMNEWST","c":"BHM","s":"birmingham new street bhm bhmnewst bham"},
|
||||||
|
{"n":"Birmingham Moor Street","t":"BHMMRS","c":"BMO","s":"birmingham moor street bmo bhmmrs"},
|
||||||
|
{"n":"Birmingham Snow Hill","t":"BHMSH","c":"BSW","s":"birmingham snow hill bsw bhmsh"},
|
||||||
|
{"n":"Leeds","t":"LEEDS","c":"LDS","s":"leeds lds"},
|
||||||
|
{"n":"York","t":"YORK","c":"YRK","s":"york yrk"},
|
||||||
|
{"n":"Liverpool Lime Street","t":"LIVLST","c":"LIV","s":"liverpool lime street liv livlst"},
|
||||||
|
{"n":"Liverpool Central","t":"LIVCEN","c":"LVC","s":"liverpool central lvc livcen"},
|
||||||
|
{"n":"Sheffield","t":"SHEFFLD","c":"SHF","s":"sheffield shf sheffld"},
|
||||||
|
{"n":"Nottingham","t":"NOTTM","c":"NOT","s":"nottingham not nottm"},
|
||||||
|
{"n":"Derby","t":"DERBY","c":"DBY","s":"derby dby"},
|
||||||
|
{"n":"Leicester","t":"LEICEST","c":"LEI","s":"leicester lei leicest"},
|
||||||
|
{"n":"Bristol Temple Meads","t":"BRSTLTM","c":"BRI","s":"bristol temple meads bri brstltm"},
|
||||||
|
{"n":"Cardiff Central","t":"CDFCEN","c":"CDF","s":"cardiff central cdf cdfcen"},
|
||||||
|
{"n":"Newcastle","t":"NEWCAST","c":"NCL","s":"newcastle ncl newcast"},
|
||||||
|
{"n":"Edinburgh Waverley","t":"EDINBUR","c":"EDB","s":"edinburgh waverley edb edinbur"},
|
||||||
|
{"n":"Glasgow Central","t":"GLASCEN","c":"GLC","s":"glasgow central glc glascen"},
|
||||||
|
{"n":"Glasgow Queen Street","t":"GLAQS","c":"GLQ","s":"glasgow queen street glq glaqs"},
|
||||||
|
{"n":"Reading","t":"READING","c":"RDG","s":"reading rdg"},
|
||||||
|
{"n":"Oxford","t":"OXFORD","c":"OXF","s":"oxford oxf"},
|
||||||
|
{"n":"Cambridge","t":"CAMBRDG","c":"CBG","s":"cambridge cbg cambrdg"},
|
||||||
|
{"n":"Peterborough","t":"PBOUGH","c":"PBO","s":"peterborough pbo pbough"},
|
||||||
|
{"n":"Doncaster","t":"DONCAST","c":"DON","s":"doncaster don doncast"},
|
||||||
|
{"n":"Crewe","t":"CREWE","c":"CRE","s":"crewe cre"},
|
||||||
|
{"n":"Preston","t":"PRESTON","c":"PRE","s":"preston pre"},
|
||||||
|
{"n":"Blackpool North","t":"BPLNOR","c":"BPN","s":"blackpool north bpn bplnor"},
|
||||||
|
{"n":"Bolton","t":"BOLTON","c":"BON","s":"bolton bon"},
|
||||||
|
{"n":"Huddersfield","t":"HUDDSFD","c":"HUD","s":"huddersfield hud huddsfd"},
|
||||||
|
{"n":"Stockport","t":"STOCKPT","c":"SPT","s":"stockport spt stockpt"},
|
||||||
|
{"n":"Wigan North Western","t":"WIGNW","c":"WGN","s":"wigan north western wgn wignw"},
|
||||||
|
{"n":"Bath Spa","t":"BATHSPA","c":"BTH","s":"bath spa bth bathspa"},
|
||||||
|
{"n":"Exeter St Davids","t":"EXD","c":"EXD","s":"exeter st davids exd"},
|
||||||
|
{"n":"Plymouth","t":"PLYMTH","c":"PLY","s":"plymouth ply plymth"},
|
||||||
|
{"n":"Truro","t":"TRURO","c":"TRU","s":"truro tru"},
|
||||||
|
{"n":"Aberdeen","t":"ABERDN","c":"ABD","s":"aberdeen abd aberdn"},
|
||||||
|
{"n":"Inverness","t":"INVNESS","c":"INV","s":"inverness inv invness"},
|
||||||
|
{"n":"Perth","t":"PERTH","c":"PTH","s":"perth pth"},
|
||||||
|
{"n":"Dundee","t":"DUNDEE","c":"DEE","s":"dundee dee"},
|
||||||
|
{"n":"Stirling","t":"STIRLNG","c":"STG","s":"stirling stg stirlng"},
|
||||||
|
{"n":"Falkirk Grahamston","t":"FLKGRA","c":"FKG","s":"falkirk grahamston fkg flkgra"},
|
||||||
|
{"n":"Motherwell","t":"MOTHRWL","c":"MTH","s":"motherwell mth mothrwl"},
|
||||||
|
{"n":"Paisley Gilmour Street","t":"PAISGL","c":"PYG","s":"paisley gilmour street pyg paisgl"},
|
||||||
|
{"n":"Greenock Central","t":"GRNOCK","c":"GKC","s":"greenock central gkc grnock"},
|
||||||
|
{"n":"Ayr","t":"AYR","c":"AYR","s":"ayr"},
|
||||||
|
{"n":"Carlisle","t":"CARLISL","c":"CAR","s":"carlisle car carlisl"},
|
||||||
|
{"n":"Penrith North Lakes","t":"PNRITH","c":"PNR","s":"penrith north lakes pnr pnrith"},
|
||||||
|
{"n":"Kendal","t":"KENDAL","c":"KEN","s":"kendal ken"},
|
||||||
|
{"n":"Windermere","t":"WNDRMRE","c":"WDM","s":"windermere wdm wndrme"},
|
||||||
|
{"n":"Lancaster","t":"LANCAST","c":"LAN","s":"lancaster lan lancast"},
|
||||||
|
{"n":"Chester","t":"CHESTER","c":"CTR","s":"chester ctr"},
|
||||||
|
{"n":"Warrington Bank Quay","t":"WRRGBQ","c":"WBQ","s":"warrington bank quay wbq wrrgbq"},
|
||||||
|
{"n":"Warrington Central","t":"WRRGCN","c":"WAC","s":"warrington central wac wrrgcn"},
|
||||||
|
{"n":"Runcorn","t":"RUNCORN","c":"RUN","s":"runcorn run"},
|
||||||
|
{"n":"Widnes","t":"WIDNES","c":"WID","s":"widnes wid"},
|
||||||
|
{"n":"Southport","t":"STHPORT","c":"SOP","s":"southport sop sthport"},
|
||||||
|
{"n":"Ormskirk","t":"ORMSKRK","c":"OMS","s":"ormskirk oms ormskrk"},
|
||||||
|
{"n":"Blackburn","t":"BLKBRN","c":"BBN","s":"blackburn bbn blkbrn"},
|
||||||
|
{"n":"Burnley Manchester Road","t":"BURNMR","c":"BYM","s":"burnley manchester road bym burnmr"},
|
||||||
|
{"n":"Rochdale","t":"ROCHDAL","c":"RCD","s":"rochdale rcd rochdal"},
|
||||||
|
{"n":"Oldham Mumps","t":"OLDMUM","c":"OMM","s":"oldham mumps omm oldmum"},
|
||||||
|
{"n":"Ashton-under-Lyne","t":"ASHTON","c":"AHN","s":"ashton under lyne ahn ashton"},
|
||||||
|
{"n":"Stalybridge","t":"STALYBG","c":"SYB","s":"stalybridge syb stalybg"},
|
||||||
|
{"n":"Macclesfield","t":"MACCLFD","c":"MAC","s":"macclesfield mac macclfd"},
|
||||||
|
{"n":"Congleton","t":"CONGLTN","c":"CNG","s":"congleton cng conglt"},
|
||||||
|
{"n":"Stoke-on-Trent","t":"STOKETR","c":"SOT","s":"stoke on trent sot stoketr"},
|
||||||
|
{"n":"Stafford","t":"STAFFRD","c":"STA","s":"stafford sta staffrd"},
|
||||||
|
{"n":"Tamworth","t":"TAMWTH","c":"TAM","s":"tamworth tam tamwth"},
|
||||||
|
{"n":"Nuneaton","t":"NUNEATN","c":"NUN","s":"nuneaton nun nuneatn"},
|
||||||
|
{"n":"Coventry","t":"COVNTRY","c":"COV","s":"coventry cov covntry"},
|
||||||
|
{"n":"Rugby","t":"RUGBY","c":"RUG","s":"rugby rug"},
|
||||||
|
{"n":"Milton Keynes Central","t":"MKCEN","c":"MKC","s":"milton keynes central mkc mkcen"},
|
||||||
|
{"n":"Birmingham Washwood Heath Junction","t":"BWHJCT","c":"","s":"birmingham washwood heath junction bwhjct"},
|
||||||
|
{"n":"Manchester Trafford Park Yard","t":"MTRYD","c":"","s":"manchester trafford park yard mtryd"},
|
||||||
|
{"n":"London Willesden Junction","t":"WLSDJCT","c":"","s":"london willesden junction wlsdjct"},
|
||||||
|
{"n":"Leeds Neville Hill Depot","t":"NVHLDP","c":"","s":"leeds neville hill depot nvhldp"},
|
||||||
|
{"n":"York Holgate Junction","t":"YHGJCT","c":"","s":"york holgate junction yhgjct"},
|
||||||
|
{"n":"Crewe Basford Hall Junction","t":"CBHJCT","c":"","s":"crewe basford hall junction cbhjct"},
|
||||||
|
{"n":"Doncaster Decoy Sidings","t":"DCDSID","c":"","s":"doncaster decoy sidings dcdsid"},
|
||||||
|
{"n":"Liverpool Edge Hill Yard","t":"LEHYD","c":"","s":"liverpool edge hill yard lehyd"},
|
||||||
|
{"n":"Bristol East Junction","t":"BREJCT","c":"","s":"bristol east junction brejct"},
|
||||||
|
{"n":"Glasgow Polmadie Depot","t":"GLPDEP","c":"","s":"glasgow polmadie depot glpdep"},
|
||||||
|
{"n":"Newcastle Manors Junction","t":"NCMJCT","c":"","s":"newcastle manors junction ncmjct"},
|
||||||
|
{"n":"Edinburgh Haymarket Sidings","t":"EHSID","c":"","s":"edinburgh haymarket sidings ehsid"},
|
||||||
|
{"n":"Reading South Junction","t":"RDSJCT","c":"","s":"reading south junction rdsjct"},
|
||||||
|
{"n":"Oxford Rewley Road Depot","t":"OXRDEP","c":"","s":"oxford rewley road depot oxrdep"},
|
||||||
|
{"n":"Cambridge Coldham Lane Junction","t":"CCLJCT","c":"","s":"cambridge coldham lane junction ccljct"},
|
||||||
|
{"n":"Watford North Junction","t":"WFNJCT","c":"","s":"watford north junction wfnjct"},
|
||||||
|
{"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":"Chelmsford New Hall Junction","t":"CHNJCT","c":"","s":"chelmsford new hall junction chnjct"},
|
||||||
|
{"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"}
|
||||||
|
]
|
||||||
@@ -2,19 +2,19 @@ import adapter from '@sveltejs/adapter-static';
|
|||||||
|
|
||||||
/** @type {import('@sveltejs/kit').Config} */
|
/** @type {import('@sveltejs/kit').Config} */
|
||||||
const config = {
|
const config = {
|
||||||
kit: {
|
kit: {
|
||||||
adapter: adapter({
|
adapter: adapter({
|
||||||
pages: 'build',
|
pages: 'build',
|
||||||
assets: 'build',
|
assets: 'build',
|
||||||
fallback: 'index.html',
|
fallback: 'index.html',
|
||||||
precompress: 'true',
|
precompress: 'true',
|
||||||
strict: 'true'
|
strict: 'true'
|
||||||
}),
|
}),
|
||||||
prerender: {
|
prerender: {
|
||||||
// Temporary option during testing
|
// Temporary option during testing
|
||||||
handleHttpError: 'ignore'
|
handleHttpError: 'ignore'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export default config;
|
export default config;
|
||||||
|
|||||||
Reference in New Issue
Block a user