Compare commits

..

12 Commits

24 changed files with 1305 additions and 88 deletions

View File

@@ -0,0 +1,39 @@
name: Build and Push container image
run-name: ${{ gitea.actor }} is building and pushing
on:
create:
tags: '*'
env:
GITEA_DOMAIN: git.fjla.uk
GITEA_REGISTRY_USER: owlbot
RESULT_IMAGE_NAME: owlboard/web-pwa
jobs:
build-and-push-image:
runs-on: ubuntu-latest
container:
image: catthehacker/ubuntu:act-latest
options: --privileged
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Gitea Container Registry
uses: docker/login-action@v3
with:
registry: ${{ env.GITEA_DOMAIN }}
username: ${{ env.GITEA_REGISTRY_USER }}
password: ${{ secrets.REGISTRY_TOKEN }}
- name: Build and Push image
uses: docker/build-push-action@v6
with:
context: .
file: ./Dockerfile
push: true
tags: |
${{ env.GITEA_DOMAIN }}/${{ env.RESULT_IMAGE_NAME }}:${{ gitea.ref_name }}
${{ env.GITEA_DOMAIN }}/${{ env.RESULT_IMAGE_NAME }}:latest

14
Dockerfile Normal file
View File

@@ -0,0 +1,14 @@
FROM node:22-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM alpine:3.20
WORKDIR /web-content
LABEL org.opencontainers.image.description="SvelteKit Static build - Designed to be an InitContainer to copy files from to the output web server container's filesystem"
COPY --from=builder /app/build ./
CMD ["sh", "-c", "cp -R /web-content/* /target-dir/"]

20
package-lock.json generated
View File

@@ -1178,9 +1178,9 @@
}
},
"node_modules/@sveltejs/kit": {
"version": "2.53.4",
"resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.53.4.tgz",
"integrity": "sha512-iAIPEahFgDJJyvz8g0jP08KvqnM6JvdW8YfsygZ+pMeMvyM2zssWMltcsotETvjSZ82G3VlitgDtBIvpQSZrTA==",
"version": "2.55.0",
"resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.55.0.tgz",
"integrity": "sha512-MdFRjevVxmAknf2NbaUkDF16jSIzXMWd4Nfah0Qp8TtQVoSp3bV4jKt8mX7z7qTUTWvgSaxtR0EG5WJf53gcuA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1189,7 +1189,7 @@
"@types/cookie": "^0.6.0",
"acorn": "^8.14.1",
"cookie": "^0.6.0",
"devalue": "^5.6.3",
"devalue": "^5.6.4",
"esm-env": "^1.2.2",
"kleur": "^4.1.5",
"magic-string": "^0.30.5",
@@ -2036,9 +2036,9 @@
}
},
"node_modules/devalue": {
"version": "5.6.3",
"resolved": "https://registry.npmjs.org/devalue/-/devalue-5.6.3.tgz",
"integrity": "sha512-nc7XjUU/2Lb+SvEFVGcWLiKkzfw8+qHI7zn8WYXKkLMgfGSHbgCEaR6bJpev8Cm6Rmrb19Gfd/tZvGqx9is3wg==",
"version": "5.6.4",
"resolved": "https://registry.npmjs.org/devalue/-/devalue-5.6.4.tgz",
"integrity": "sha512-Gp6rDldRsFh/7XuouDbxMH3Mx8GMCcgzIb1pDTvNyn8pZGQ22u+Wa+lGV9dQCltFQ7uVw0MhRyb8XDskNFOReA==",
"license": "MIT"
},
"node_modules/es-module-lexer": {
@@ -2452,9 +2452,9 @@
}
},
"node_modules/flatted": {
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz",
"integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==",
"version": "3.4.1",
"resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.1.tgz",
"integrity": "sha512-IxfVbRFVlV8V/yRaGzk0UVIcsKKHMSfYw66T/u4nTwlWteQePsxe//LjudR1AMX4tZW3WFCh3Zqa/sjlqpbURQ==",
"dev": true,
"license": "ISC"
},

View File

@@ -5,7 +5,10 @@
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="manifest" href="/manifest.webmanifest" />
<meta name="title" content="OwlBoard | Your fasted route to live and reference data" />
<meta name="description" content="Live station departures, Live train tracking, PIS Codes & more" />
<meta
name="description"
content="Live station departures, Live train tracking, PIS Codes & more"
/>
<meta name="theme-color" content="#4fd1d1" />
<link rel="canonical" href="https://owlboard.info" />
<meta property="og:type" content="website" />

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" viewBox="0 0 2496 3495"><path stroke="#000" stroke-miterlimit="10" d="M-29-28h2552v4439H-29z"/><path fill="#fff" stroke="#000" stroke-miterlimit="10" stroke-width=".9" d="M0 0h2495v3494H0z"/><path d="m814 2164-137 2q-2-28-11-46-28-54-120-54-52 0-83 21a65 65 0 0 0-32 56q0 27 18 40 21 17 97 26 158 19 213 65 61 51 61 137 0 97-71 154-71 56-193 56-124 0-199-62-77-62-80-167h140q3 52 41 82 37 29 102 29 56 0 90-23 33-22 33-61 0-17-9-31-20-31-93-40-122-16-167-33a175 175 0 0 1-117-164q0-92 68-146 69-54 181-54 122 0 192 57 72 57 76 156m104-216h126v154h108v101h-108v251q0 34 11 42 11 9 88 9h8v107l-36 1h-89q-61-4-88-37-12-17-16-41t-4-115v-217h-72v-101h72zm508 140q111 0 180 72t69 190q0 122-66 194-67 72-180 72-112 0-180-71-67-70-67-190 0-121 67-194 68-73 177-73m2 106q-57 0-91 44t-34 119q0 70 34 112 35 42 91 42 57 0 90-42 32-42 32-115t-33-117q-33-43-89-43m311-92h122v33q49-47 129-47 102 0 166 74 65 73 65 190 0 119-63 191-63 73-167 73-78 0-124-46v184h-128zm233 92q-66 0-91 51-16 31-16 121 0 61 16 94 25 50 95 50 56 0 88-41 32-42 32-115 0-74-34-117-33-43-90-43"/><circle cx="1247.7" cy="1049.7" r="708.7" fill="#e31837"/><path fill="#1a1a1a" d="M-17 2908h2530v42H-17z"/><path fill="#1a1a1a" d="M978 3051v46H857v77h94v44h-94v79h121v46H799v-292zm39 292v-219h33q9 0 12 3 4 4 5 11l3 22q10-18 23-29t30-11q15 0 24 7l-4 42-3 6-6 1-9-1-12-1-14 2-10 7q-5 4-9 10l-7 14v136zm161 0v-219h33q8 0 12 3 3 4 4 11l3 22q10-18 23-29t31-11q14 0 23 7l-4 42q0 4-3 6l-5 1-10-1-11-1q-8 0-14 2l-11 7q-5 4-8 10l-7 14v136zm252-222q24 0 44 8 20 7 34 22t21 36 8 46-8 47-21 36-34 22-44 8-44-8q-20-7-34-22-15-15-22-36-8-21-8-47t8-46 22-36 34-22 44-8m0 183q25 0 37-18t12-52-12-53-37-17-38 17-12 53 12 52 38 18m145 39v-219h33q8 0 12 3 3 4 4 11l3 22q10-18 23-29 14-11 31-11 14 0 24 7l-5 42q0 4-2 6l-6 1-10-1-11-1q-8 0-14 2l-11 7-8 10-7 14v136z" aria-label="Error" font-family="Carlito" font-size="451" font-weight="700" style="line-height:.9;-inkscape-font-specification:&quot;Carlito Bold&quot;;text-align:center;white-space:pre" text-anchor="middle"/></svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@@ -0,0 +1,82 @@
<script lang="ts">
import type { HTMLButtonAttributes, HTMLAnchorAttributes } from 'svelte/elements';
type ButtonColor = 'brand' | 'accent' | 'bg';
interface Props {
children: import('svelte').Snippet;
href?: string;
color?: ButtonColor;
onclick?: (e: MouseEvent) => void;
[key: string]: any;
}
let { children, href, color = 'bg', onclick, ...rest }: Props = $props();
const isLink = $derived(!!href);
const isExternal = $derived(href?.startsWith('http'));
</script>
{#if isLink}
<a
{href}
class="btn {color}"
target={isExternal ? '_blank' : undefined}
rel={isExternal ? 'noopener noreferrer' : undefined}
{...rest}
>
{@render children?.()}
</a>
{:else}
<button class="btn {color}" {onclick} {...rest}>
{@render children?.()}
</button>
{/if}
<style>
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 0.4rem 1.2rem;
width: fit-content;
min-width: 90px;
min-height: 48px;
border: none;
border-radius: 20px;
cursor: pointer;
text-decoration: none;
font-family: 'URW Gothic', sans-serif;
letter-spacing: 0.05ch;
font-size: 1rem;
font-weight: 600;
transition: all 0.2s;
box-shadow: var(--shadow-std);
user-select: none;
-webkit-user-select: none;
-webkit-tap-highlight-color: transparent;
touch-action: manipulation;
}
.accent {
background-color: var(--color-accent);
color: var(--color-title);
}
.brand {
background-color: var(--color-brand);
color: rgb(30, 30, 30);
}
.bg {
background-color: var(--color-bg-light);
color: var(--color-title);
}
.btn:hover {
filter: brightness(1.5);
}
.btn.active {
transform: scale(0.98);
}
</style>

View File

@@ -0,0 +1,222 @@
<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';
interface LocationRecord {
n: string; // name
t: string; // tiploc
c?: string; // crs
s: string; // search string
}
let { value = $bindable() } = $props();
let results = $state<LocationRecord[]>([]);
let locations: LocationRecord[] = [];
let showResults = $state(false);
let selectedIndex = $state(-1);
const MAX_RESULTS = 5;
async function loadLocations() {
const res = await fetch('/api/tiplocs');
locations = await res.json();
}
onMount(loadLocations);
function tokenize(query: string) {
return query.toLowerCase().trim().split(/\s+/).filter(Boolean);
}
function search(query: string) {
if (query.length < 3) {
results = [];
return;
}
const tokens = tokenize(query);
const lowerQuery = query.toLowerCase().trim();
results = locations
.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;
// Alphabetical Sort
return a.n.localeCompare(b.n);
})
.slice(0, MAX_RESULTS);
}
$effect(() => {
search(value);
});
// 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: LocationRecord) {
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;
}
.tiploc {
text-align: right;
font-size: 0.8rem;
}
</style>

View File

@@ -0,0 +1,94 @@
<script lang="ts">
import { fade } from 'svelte/transition';
import type { HTMLInputAttributes } from 'svelte/elements';
interface Props extends HTMLInputAttributes {
value?: string;
label?: string;
placeholder?: string;
type?: 'text' | 'password' | 'email' | 'number' | 'search' | 'tel' | 'url';
error?: string;
uppercase?: boolean;
[key: string]: any;
}
let {
value = $bindable(''),
label,
placeholder = '',
type = 'text',
error = '',
uppercase = false,
...rest
}: Props = $props();
let isFocussed = $state(false);
</script>
<div class="input-wrapper" class:focussed={isFocussed} class:has-error={!!error}>
{#if label}
<label for="adaptive-input">{label}</label>
{/if}
<input
id="adaptive-input"
class:all-caps={uppercase}
{type}
{placeholder}
bind:value
onfocus={() => (isFocussed = true)}
onblur={() => (isFocussed = false)}
{...rest}
/>
{#if error}
<span class="error-message" transition:fade>{error}</span>
{/if}
</div>
<style>
.input-wrapper {
display: flex;
flex-direction: column;
gap: 8px;
width: 100%;
font-family: 'URW Gothic', sans-serif;
}
label {
font-size: 0.9rem;
font-weight: 400;
color: var(--color-title);
}
input {
min-height: 40px;
padding: 0 16px;
background-color: var(--color-title);
border: 2px solid transparent;
border-radius: 20px;
color: var(--color-bg-dark);
font-size: 1.2rem;
transition: all 0.2s ease-in-out;
outline: none;
text-align: center;
}
.all-caps {
text-transform: uppercase;
}
.focussed input {
border-color: var(--color-bg-light);
}
.has-error input {
border-color: #ff4d4d;
}
.error-message {
color: #ff4d4d;
font-size: 1rem;
text-align: center;
}
</style>

View File

@@ -0,0 +1,101 @@
<script lang="ts">
import type { Snippet } from 'svelte';
import { IconHelpCircle } from '@tabler/icons-svelte';
import { slide } from 'svelte/transition';
interface Props {
children: Snippet;
header?: string;
helpText?: string;
}
let { children, header = '', helpText }: Props = $props();
let showHelp = $state(false);
</script>
<div class="card">
{#if header || helpText}
<header class="card-header">
<div class="header-content">
{header}
</div>
{#if helpText}
<button
type="button"
class="help-toggle"
onclick={() => (showHelp = !showHelp)}
aria-label="Show Help"
>
<IconHelpCircle
size={26}
stroke={2.25}
color={showHelp ? 'var(--color-brand)' : 'var(--color-title)'}
/>
</button>
{/if}
</header>
{#if showHelp && helpText}
<div class="help-drawer" transition:slide={{ duration: 400 }}>
<p>{helpText}</p>
</div>
{/if}
{/if}
<div class="card-body">
{@render children?.()}
</div>
</div>
<style>
.card {
background: var(--color-accent);
position: relative;
border-radius: 20px;
overflow: visible;
width: 95%;
max-width: 600px;
text-align: center;
font-family: 'URW Gothic', sans-serif;
color: var(--color-brand);
padding: 10px 0;
}
.header-content {
flex: 1;
margin: 0;
font-size: 1.3rem;
font-weight: 600;
}
.help-toggle {
position: absolute;
top: 8px;
right: 8px;
background: none;
border: none;
padding: 4px;
cursor: help;
opacity: 0.6;
z-index: 2;
transition:
opacity 0.2s,
transform 0.2s;
}
.help-toggle:hover {
opacity: 1;
transform: scale(1.1);
}
.help-drawer {
background-color: var(--color-accent);
padding: 4px 16px;
font-size: 0.95rem;
line-height: 1.2;
margin: auto;
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
color: var(--color-title);
}
</style>

View 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>

82
src/lib/global.css Normal file
View File

@@ -0,0 +1,82 @@
/* OwlBoard Global CSS */
/* * URW Gothic is licensed under the SIL Open Font License, Version 1.1.
* Copyright (c) 2014,2015 by (URW)++ Design & Development
*/
@font-face {
font-family: 'URW Gothic';
src: url('/type/urwgothic/urwgothic-book-webfont.woff2') format('woff2');
font-weight: 400;
font-style: normal;
font-display: swap;
}
/* * URW Gothic is licensed under the SIL Open Font License, Version 1.1.
* Copyright (c) 2014,2015 by (URW)++ Design & Development
*/
@font-face {
font-family: 'URW Gothic';
src: url('/type/urwgothic/urwgothic-bookoblique-webfont.woff2') format('woff2');
font-weight: 400;
font-style: italic;
font-display: swap;
}
/* * URW Gothic is licensed under the SIL Open Font License, Version 1.1.
* Copyright (c) 2014,2015 by (URW)++ Design & Development
*/
@font-face {
font-family: 'URW Gothic';
src: url('/type/urwgothic/urwgothic-demi-webfont.woff2') format('woff2');
font-weight: 600;
font-style: normal;
font-display: swap;
}
/* * URW Gothic is licensed under the SIL Open Font License, Version 1.1.
* Copyright (c) 2014,2015 by (URW)++ Design & Development
*/
@font-face {
font-family: 'URW Gothic';
src: url('/type/urwgothic/urwgothic-demioblique-webfont.woff2') format('woff2');
font-weight: 600;
font-style: italic;
font-display: swap;
}
:root {
/* Brand Colours */
--color-brand: #4fd1d1;
--color-accent: #3c6f79;
--color-title: #ebebeb;
--color-bg-light: #404c55;
--color-bg-dark: #2b343c;
/* Shadows */
--color-shadow: hsla(210, 20%, 5%, 0.35);
--shadow-std: 0 4px 12px var(--color-shadow);
--shadow-up: 0 -4px 12px var(--color-shadow);
--shadow-right: 4px 0 12px var(--color-shadow);
}
body {
margin: 0;
padding: 0;
background-color: var(--color-accent);
color: white;
-webkit-font-smoothing: antialiased;
-mos-osx-font-smoothing: grayscale;
overflow-x: hidden;
}
a {
color: var(--color-brand);
}
a:visited {
color: var(--color-brand);
}
a:hover {
color: var(--color-accent);
}

72
src/routes/+error.svelte Normal file
View File

@@ -0,0 +1,72 @@
<script lang="ts">
import { page } from '$app/state';
import stopErr from '$lib/assets/img/stop-error.svg';
import Button from '$lib/components/ui/Button.svelte';
</script>
<div class="error-wrapper">
<img class="err-img" src={stopErr} alt="" role="presentation" width="150" height="210" />
<h2 class="label">{page.status}</h2>
<p class="error-message">
{page.error?.message ?? 'An unexpected derailment occurred.'}
</p>
{#if page.error?.owlCode}
<div class="debug-info">
<code>Ref: {page.error.owlCode}</code>
</div>
{/if}
<Button href={'/'} color={'accent'}>Return to Home</Button>
</div>
<style>
.error-wrapper {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: calc(100vh - 140px);
padding: 0;
text-align: center;
font-family: 'URW Gothic', sans-serif;
}
.err-img {
height: 40%;
width: auto;
max-width: 180px;
max-height: 252px;
}
@media (max-height: 600px), (max-width: 270px) {
.err-img {
display: none;
}
}
.label {
color: #ff4444;
letter-spacing: 0.2rem;
margin-bottom: 0px;
}
.error-message {
font-size: 1.1rem;
color: var(--color-title);
max-width: 300px;
margin-top: 5px;
margin-bottom: 30px;
}
.debug-info {
background: rgba(255, 255, 255, 0.05);
padding: 5px 12px;
border-radius: 4px;
font-size: 0.8rem;
opacity: 0.6;
}
</style>

View File

@@ -1,5 +1,8 @@
<script lang="ts">
import { page } from '$app/state';
import { slide, fade } from 'svelte/transition';
import '$lib/global.css';
import logoText from '$lib/assets/round-logo-text.svg';
import logoPlain from '$lib/assets/round-logo.svg';
@@ -9,18 +12,46 @@
let { children } = $props();
// Navigation State
const moreIcon = IconDots;
const navItems = [
{ label: 'Home', path: '/', icon: IconHome },
{ label: 'PIS', path: '/pis', icon: IconDialpad },
{ label: 'Options', path: '/preferences', icon: IconSettings },
{ label: 'About', path: '/about', icon: IconHelp},
]
{ label: 'About', path: '/about', icon: IconHelp }
];
let navWidth = $state(0);
let menuOpen = $state(false);
const ITEM_WIDTH = 70;
const MORE_BUTTON_WIDTH = 70;
const activePath = $derived(page.url?.pathname ?? '');
const isHome = $derived(page.url.pathname === '/');
const visibleCount = $derived.by(() => {
if (navWidth === 0) return navItems.length;
const available = navWidth;
const totalItems = navItems.length;
const countWithoutMore = Math.floor(available / ITEM_WIDTH);
const moreIcon = IconDots;
if (countWithoutMore >= totalItems) return totalItems;
const availableWithMore = navWidth - MORE_BUTTON_WIDTH;
const countWithMore = Math.floor(availableWithMore / ITEM_WIDTH);
if (totalItems - countWithMore === 1) {
if (available >= (countWithMore + 1) * ITEM_WIDTH) {
return totalItems;
}
}
return countWithMore;
});
const visibleItems = $derived(navItems.slice(0, visibleCount));
const hiddenItems = $derived(navItems.slice(visibleCount));
</script>
<svelte:head>
@@ -29,14 +60,14 @@
<header>
<a href="/" aria-label="OwlBoard Home" class="logo-link">
{#if isHome}
<img src={logoText} alt="OwlBoard Logo" height="55" />
{:else}
{#if page.data.title}
<img src={logoPlain} alt="OwlBoard Icon" height="55" />
{:else}
<img src={logoText} alt="OwlBoard Logo" height="55" />
{/if}
</a>
{#if !isHome}
<h1 class="page-title">{page?.data?.title ?? ""}</h1>
{#if page.data.title}
<h1 class="page-title">{page.data.title}</h1>
{/if}
</header>
@@ -44,51 +75,68 @@
{@render children()}
</main>
<nav>
<nav bind:clientWidth={navWidth}>
<!-- Dynamic Nav Elements Here! -->
{#each navItems as item}
{#each visibleItems as item}
{@const isActive = activePath === item.path}
<a
href={item.path}
class="nav-item"
class:active={isActive}
aria-current={isActive ? 'page' : undefined}
onclick={() => (menuOpen = false)}
>
<item.icon
size={24}
stroke={isActive ? 2.5 : 2}
class="nav-icon"
/>
<item.icon size={24} stroke={isActive ? 2 : 1.5} class="nav-icon" />
<span class="label">{item.label}</span>
</a>
{/each}
{#if hiddenItems.length > 0}
<div class="more-menu-wrapper">
<button
class="nav-item more-btn"
onclick={() => (menuOpen = !menuOpen)}
aria-expanded={menuOpen}
>
<IconDots size={24} />
<span class="label">More</span>
</button>
</div>
{#if menuOpen}
<div
class="backdrop"
onclick={() => (menuOpen = false)}
transition:fade={{ duration: 150 }}
></div>
<div class="menu-popover" transition:slide={{ axis: 'y', duration: 250 }}>
{#each hiddenItems as item}
{@const isActive = activePath === item.path}
<a
href={item.path}
class="menu-popover-item"
onclick={() => (menuOpen = false)}
class:active={isActive}
aria-current={isActive ? 'page' : undefined}
>
<item.icon size={20} />
<span>{item.label}</span>
</a>
{/each}
</div>
{/if}
{/if}
</nav>
<div class="viewport-guard">
<img src={logoPlain} alt="OwlBoard Logo" width="100" height="100" />
<h1 class="viewport-guard-title">Narrow Gauge Detected</h1>
<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.
</p>
</div>
<style>
:root {
/* Brand Colours */
--color-brand: #4fd1d1;
--color-accent: #3c6f79;
--color-title: #ebebeb;
--color-bg-light: #404c55;
--color-bg-dark: #2b343c;
/* Shadows */
--color-shadow: hsla(210, 20%, 5%, 0.35);
--shadow-std: 0 4px 12px var(--color-shadow);
--shadow-up: 0 -4px 12px var(--color-shadow);
}
:global(body) {
margin: 0;
padding: 0;
background-color: var(--color-accent);
overflow-x: hidden;
}
header {
top: 0;
height: 80px;
@@ -107,11 +155,15 @@
}
.page-title {
font-family: 'URW Gothic', sans-serif;
font-weight: 600;
font-style: normal;
margin-left: 5px;
padding-bottom: 2px;
color: var(--color-title);
}
header, nav {
header,
nav {
flex-shrink: 0;
width: 100%;
background-color: var(--color-accent);
@@ -126,13 +178,6 @@
gap: 0rem;
}
nav {
bottom: 0;
height: 60px;
box-shadow: var(--shadow-up);
}
main {
padding-top: 80px;
padding-bottom: 60px;
@@ -142,19 +187,41 @@
background-image: radial-gradient(var(--color-bg-dark), var(--color-bg-light));
}
.nav-item {
nav {
display: flex;
bottom: 0;
height: 60px;
box-shadow: var(--shadow-up);
}
.nav-item,
.more-menu-wrapper {
display: flex;
flex: 1;
flex-direction: column;
width: 70px;
align-items: center;
justify-content: center;
text-decoration: none;
position: relative;
cursor: pointer;
height: 100%;
color: rgba(255,255,255,0.4);
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
border: 0;
color: var(--color-title);
background: none;
padding: 0;
border: none;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.nav-item.active {
.nav-item:hover,
.menu-popover-item:hover {
filter: brightness(1.9);
background-color: #ffffff1a;
}
.nav-item.active,
.menu-popover-item.active {
color: #ffffff;
background: rgba(255, 255, 255, 0.05);
}
@@ -170,10 +237,93 @@
box-shadow: var(--shadow-std);
}
.nav-item.active .label,
.menu-popover-item.active .label {
font-weight: 600;
}
.label {
font-size: 0.65rem;
font-weight: 700;
font-size: 0.85rem;
font-family: 'URW Gothic', sans-serif;
font-weight: 400;
margin-top: 4px;
letter-spacing: 0.2rem;
letter-spacing: 0.05rem;
}
.backdrop {
position: fixed;
top: 0;
left: 0;
bottom: 60px;
width: 100vw;
height: calc(100dvh - 60px);
background: rgba(0, 0, 0, 0.4);
backdrop-filter: blur(2px);
z-index: 999;
}
.menu-popover {
position: absolute;
bottom: 100%;
right: 10px;
background-color: var(--color-accent);
border-radius: 12px 12px 0 0;
box-shadow: 0 -4px 20px rgba(0, 0, 0, 0.15);
min-width: 140px;
z-index: 1000;
overflow: hidden;
}
.menu-popover-item {
display: flex;
align-items: center;
gap: 12px;
position: relative;
min-height: 48px;
padding: 12px 16px;
text-decoration: none;
color: var(--color-title);
}
.menu-popover-item.active::before {
content: '';
position: absolute;
left: 0;
top: 50%;
transform: translateY(-50%);
width: 4px;
height: 60%;
background-color: var(--color-brand);
border-radius: 0 4px 4px 0;
box-shadow: var(--shadow-right);
}
.viewport-guard {
display: none;
}
@media (max-width: 299px) {
.viewport-guard {
display: block;
position: fixed;
text-align: center;
font-family: 'URW Gothic', sans-serif;
margin-top: 1rem;
font-size: 1.2rem;
color: var(--color-title);
}
.viewport-guard p {
width: 90%;
margin: auto;
padding-top: 30px;
}
header,
main,
nav {
display: none;
}
}
</style>

3
src/routes/+layout.ts Normal file
View File

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

View File

@@ -1 +1,18 @@
<h2>OwlBoard</h2>
<script lang="ts">
import LocationBoardCard from '$lib/components/ui/cards/LocationBoardCard.svelte';
</script>
<div class="card-container">
<LocationBoardCard />
</div>
<style>
.card-container {
display: flex;
align-items: center;
flex-direction: column;
gap: 20px;
justify-content: center;
padding: 20px 10px;
}
</style>

View File

@@ -1,2 +1,104 @@
<script lang="ts">
import logo from '$lib/assets/round-logo.svg';
let isSpinning = $state(false);
function handleLogoTap() {
if (isSpinning) return;
isSpinning = true;
setTimeout(() => (isSpinning = false), 800);
}
</script>
<div class="logo-container">
<img
class="logo"
src={logo}
alt="OwlBoard Logo"
onclick={handleLogoTap}
class:animate={isSpinning}
/>
</div>
<section class="about">
<p class="copy">&copy; 2022-2026 Frederick Boniface</p>
<p class="tagline">Created by train crew, for train crew</p>
<p class="amble">
OwlBoard was created in 2022, evolving from 'Athena' which just provided 'Quick Links' to Tiger
departure boards. The aim was to provide fast and easy access to the information we need on a
daily basis.
</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
many of the sculptures still in the area.
</p>
<p class="opensource">
Some components that combine to form OwlBoard are open-source, see the <a
href="https://git.fjla.uk"
target="_blank"
rel="noopener">Git reposititories</a
> for more info.
</p>
</section>
<section class="data-sourcing">
<p>
Data is sourced from multiple providers, including <a
href="https://nationalrail.co.uk"
target="_blank"
rel="noopener noreferrer">National Rail Enquiries</a
> and Network Rail along side OwlBoard's own data
</p>
</section>
<style>
.logo-container {
width: 100%;
text-align: center;
}
@keyframes owl-spin {
0% {
transform: rotate(0deg);
}
15% {
transform: rotate(-20deg);
}
100% {
transform: rotate(360deg);
}
}
.logo {
padding-top: 25px;
margin: auto;
height: auto;
width: clamp(80px, 20vw, 200px);
}
.logo.animate {
animation: owl-spin 0.8s cubic-bezier(0.34, 1.56, 0.64, 1) forwards;
}
section {
margin: auto;
width: 90%;
max-width: 650px;
font-family: 'URW Gothic', sans-serif;
text-align: center;
display: flex;
flex-direction: column;
gap: 0;
}
section:last-child {
padding-bottom: 15px;
}
.tagline {
font-weight: 600;
font-style: italic;
}
</style>

View File

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

103
static/api/tiplocs Normal file
View File

@@ -0,0 +1,103 @@
[
{"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":"Ipswich Derby Road Depot","t":"IPDRDP","c":"","s":"ipswich derby road depot ipdrdp"}
]

View File

@@ -0,0 +1,93 @@
Copyright (c) 2014,2015 by (URW)++ Design & Development
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.

Binary file not shown.

Binary file not shown.

View File

@@ -1,6 +1,20 @@
import adapter from '@sveltejs/adapter-static';
/** @type {import('@sveltejs/kit').Config} */
const config = { kit: { adapter: adapter() } };
const config = {
kit: {
adapter: adapter({
pages: 'build',
assets: 'build',
fallback: 'index.html',
precompress: 'true',
strict: 'true'
}),
prerender: {
// Temporary option during testing
handleHttpError: 'ignore'
}
}
};
export default config;