Update prettier settings

This commit is contained in:
Fred Boniface 2024-04-30 11:17:06 +01:00
parent 1484a9068e
commit af58e923de
71 changed files with 4133 additions and 4114 deletions

View File

@ -1,25 +1,25 @@
module.exports = {
root: true,
extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended', 'plugin:svelte/recommended', 'prettier'],
parser: '@typescript-eslint/parser',
plugins: ['@typescript-eslint'],
parserOptions: {
sourceType: 'module',
ecmaVersion: 2020,
extraFileExtensions: ['.svelte']
},
env: {
browser: true,
es2017: true,
node: true
},
overrides: [
{
files: ['*.svelte'],
parser: 'svelte-eslint-parser',
parserOptions: {
parser: '@typescript-eslint/parser'
}
}
]
root: true,
extends: ["eslint:recommended", "plugin:@typescript-eslint/recommended", "plugin:svelte/recommended", "prettier"],
parser: "@typescript-eslint/parser",
plugins: ["@typescript-eslint"],
parserOptions: {
sourceType: "module",
ecmaVersion: 2020,
extraFileExtensions: [".svelte"]
},
env: {
browser: true,
es2017: true,
node: true
},
overrides: [
{
files: ["*.svelte"],
parser: "svelte-eslint-parser",
parserOptions: {
parser: "@typescript-eslint/parser"
}
}
]
};

View File

@ -1,11 +1,11 @@
{
"useTabs": false,
"tabWidth": 2,
"semi": true,
"singleQuote": true,
"trailingComma": "none",
"printWidth": 180,
"plugins": ["prettier-plugin-svelte"],
"pluginSearchDirs": ["."],
"overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
"useTabs": false,
"tabWidth": 4,
"semi": true,
"singleQuote": false,
"trailingComma": "none",
"printWidth": 180,
"plugins": ["prettier-plugin-svelte"],
"pluginSearchDirs": ["."],
"overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
}

View File

@ -1,35 +1,35 @@
{
"name": "owlboard-svelte",
"version": "2024.03.2",
"private": true,
"scripts": {
"dev": "vite dev",
"build": "vite build",
"postbuild": "npx svelte-sitemap --domain https://owlboard.info --ignore '**/err/**' --ignore '**/reg/submit'",
"preview": "vite preview",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
"lint": "prettier --plugin-search-dir . --check . && eslint .",
"format": "prettier --plugin-search-dir . --write ."
},
"devDependencies": {
"@owlboard/ts-types": "^1.1.0",
"@sveltejs/adapter-auto": "^2.0.0",
"@sveltejs/adapter-static": "^2.0.2",
"@sveltejs/kit": "^1.5.0",
"eslint": "^8.28.0",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-svelte": "^2.26.0",
"prettier": "^2.8.0",
"prettier-plugin-svelte": "^2.8.1",
"svelte": "^3.54.0",
"svelte-check": "^3.0.1",
"svelte-sitemap": "^2.6.0",
"typescript": "^5.0.0",
"vite": "^4.3.0"
},
"type": "module",
"dependencies": {
"@tabler/icons-svelte": "^3.2.0"
}
"name": "owlboard-svelte",
"version": "2024.03.2",
"private": true,
"scripts": {
"dev": "vite dev",
"build": "vite build",
"postbuild": "npx svelte-sitemap --domain https://owlboard.info --ignore '**/err/**' --ignore '**/reg/submit'",
"preview": "vite preview",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
"lint": "prettier --plugin-search-dir . --check . && eslint .",
"format": "prettier --plugin-search-dir . --write ."
},
"devDependencies": {
"@owlboard/ts-types": "^1.1.0",
"@sveltejs/adapter-auto": "^2.0.0",
"@sveltejs/adapter-static": "^2.0.2",
"@sveltejs/kit": "^1.5.0",
"eslint": "^8.28.0",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-svelte": "^2.26.0",
"prettier": "^2.8.0",
"prettier-plugin-svelte": "^2.8.1",
"svelte": "^3.54.0",
"svelte-check": "^3.0.1",
"svelte-sitemap": "^2.6.0",
"typescript": "^5.0.0",
"vite": "^4.3.0"
},
"type": "module",
"dependencies": {
"@tabler/icons-svelte": "^3.2.0"
}
}

12
src/app.d.ts vendored
View File

@ -1,12 +1,12 @@
// See https://kit.svelte.dev/docs/types#app
// for information about these interfaces
declare global {
namespace App {
// interface Error {}
// interface Locals {}
// interface PageData {}
// interface Platform {}
}
namespace App {
// interface Error {}
// interface Locals {}
// interface PageData {}
// interface Platform {}
}
}
export {};

View File

@ -1,10 +1,10 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover">
<div style="display: contents">%sveltekit.body%</div>
</body>
<head>
<meta charset="utf-8" />
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover">
<div style="display: contents">%sveltekit.body%</div>
</body>
</html>

View File

@ -1,20 +1,20 @@
<div id="banner">DEVMODE</div>
<style>
#banner {
width: 200px;
background: red;
color: #fff;
position: fixed;
text-align: center;
top: 25px;
line-height: 40px;
right: -50px;
left: auto;
-ms-transform: rotate(45deg);
-webkit-transform: rotate(45deg);
transform: rotate(45deg);
z-index: 100;
box-shadow: 5px 5px 30px rgba(0, 0, 0, 0.451);
}
#banner {
width: 200px;
background: red;
color: #fff;
position: fixed;
text-align: center;
top: 25px;
line-height: 40px;
right: -50px;
left: auto;
-ms-transform: rotate(45deg);
-webkit-transform: rotate(45deg);
transform: rotate(45deg);
z-index: 100;
box-shadow: 5px 5px 30px rgba(0, 0, 0, 0.451);
}
</style>

View File

@ -1,58 +1,58 @@
<script lang="ts">
import { onMount, onDestroy } from 'svelte';
export let text: string;
import { onMount, onDestroy } from "svelte";
export let text: string;
let isVisible: boolean = false;
let timer: number;
let isVisible: boolean = false;
let timer: number;
function showTooltip() {
isVisible = true;
timer = setTimeout(() => {
isVisible = false;
}, 2000);
}
function showTooltip() {
isVisible = true;
timer = setTimeout(() => {
isVisible = false;
}, 2000);
}
function hideTooltip() {
isVisible = false;
clearTimeout(timer);
}
function hideTooltip() {
isVisible = false;
clearTimeout(timer);
}
onDestroy(() => {
clearTimeout(timer);
});
onDestroy(() => {
clearTimeout(timer);
});
</script>
<div class="tooltip" on:touchstart={showTooltip} on:touchend={hideTooltip} on:touchcancel={hideTooltip}>
<slot />
<span class="tooltiptext">{text}</span>
<slot />
<span class="tooltiptext">{text}</span>
</div>
<style>
.tooltip {
position: relative;
display: inline-block;
cursor: pointer;
}
.tooltip {
position: relative;
display: inline-block;
cursor: pointer;
}
.tooltip .tooltiptext {
visibility: hidden;
width: 120px;
background-color: var(--island-button-color);
color: #fff;
text-align: center;
border-radius: 6px;
padding: 5px;
position: absolute;
z-index: 1;
bottom: 125%;
left: 50%;
margin-left: -60px;
opacity: 0;
transition: opacity 0.3s;
}
.tooltip .tooltiptext {
visibility: hidden;
width: 120px;
background-color: var(--island-button-color);
color: #fff;
text-align: center;
border-radius: 6px;
padding: 5px;
position: absolute;
z-index: 1;
bottom: 125%;
left: 50%;
margin-left: -60px;
opacity: 0;
transition: opacity 0.3s;
}
.tooltip:hover .tooltiptext {
visibility: visible;
opacity: 1;
}
.tooltip:hover .tooltiptext {
visibility: visible;
opacity: 1;
}
</style>

View File

@ -1,10 +1,10 @@
<img src="/images/logo/wide_logo.svg" alt="Logo" />
<style>
img {
width: 50%;
max-width: 250px;
margin-top: 55px;
margin-bottom: 55px;
}
img {
width: 50%;
max-width: 250px;
margin-top: 55px;
margin-bottom: 55px;
}
</style>

View File

@ -1,47 +1,47 @@
<script lang="ts">
import Island from '$lib/islands/island.svelte';
export let variables = {
title: 'Uninitialised',
action: '/',
placeholder: 'Uninitialised',
queryName: 'uninitiailsed'
};
import Island from "$lib/islands/island.svelte";
export let variables = {
title: "Uninitialised",
action: "/",
placeholder: "Uninitialised",
queryName: "uninitiailsed"
};
</script>
<Island {variables}>
<form action={variables.action}>
<input class="form-input" type="text" id="input-headcode" name={variables.queryName} placeholder={variables.placeholder} autocomplete="off" />
<br />
<button type="submit">Submit</button>
</form>
<form action={variables.action}>
<input class="form-input" type="text" id="input-headcode" name={variables.queryName} placeholder={variables.placeholder} autocomplete="off" />
<br />
<button type="submit">Submit</button>
</form>
</Island>
<style>
.form-input {
width: 75%;
height: 32px;
margin-top: 5px;
margin-bottom: 5px;
border-radius: 50px;
border: none;
text-align: center;
font-family: urwgothic, 'Lucida Sans', 'Lucida Sans Regular', 'Lucida Grande', 'Lucida Sans Unicode', Geneva, Verdana, sans-serif;
text-transform: uppercase;
font-size: 15px;
box-shadow: var(--box-shadow);
}
button {
width: 50%;
margin-bottom: 5px;
margin-top: 5px;
border: none;
border-radius: 20px;
padding: 5px;
font-family: urwgothic, 'Lucida Sans', 'Lucida Sans Regular', 'Lucida Grande', 'Lucida Sans Unicode', Geneva, Verdana, sans-serif;
font-size: 16px;
font-weight: 400;
background-color: var(--island-button-color);
color: var(--island-link-color);
box-shadow: var(--box-shadow);
}
.form-input {
width: 75%;
height: 32px;
margin-top: 5px;
margin-bottom: 5px;
border-radius: 50px;
border: none;
text-align: center;
font-family: urwgothic, "Lucida Sans", "Lucida Sans Regular", "Lucida Grande", "Lucida Sans Unicode", Geneva, Verdana, sans-serif;
text-transform: uppercase;
font-size: 15px;
box-shadow: var(--box-shadow);
}
button {
width: 50%;
margin-bottom: 5px;
margin-top: 5px;
border: none;
border-radius: 20px;
padding: 5px;
font-family: urwgothic, "Lucida Sans", "Lucida Sans Regular", "Lucida Grande", "Lucida Sans Unicode", Geneva, Verdana, sans-serif;
font-size: 16px;
font-weight: 400;
background-color: var(--island-button-color);
color: var(--island-link-color);
box-shadow: var(--box-shadow);
}
</style>

View File

@ -1,29 +1,29 @@
<script lang="ts">
import { fade } from 'svelte/transition';
import { fade } from "svelte/transition";
export let variables = { title: '' };
export let variables = { title: "" };
</script>
<div in:fade={{ duration: 250 }}>
<span>{variables.title}</span>
<slot />
<span>{variables.title}</span>
<slot />
</div>
<style>
span {
font-family: urwgothic, 'Lucida Sans', 'Lucida Sans Regular', 'Lucida Grande', 'Lucida Sans Unicode', Geneva, Verdana, sans-serif;
font-weight: 600;
font-size: 20px;
color: var(--island-header-color);
}
div {
width: 85%;
max-width: 400px;
margin: auto;
margin-top: 25px;
padding: 10px;
background-color: var(--island-bg-color);
border-radius: 10px;
box-shadow: 5px 5px 30px rgba(0, 0, 0, 0.29);
}
span {
font-family: urwgothic, "Lucida Sans", "Lucida Sans Regular", "Lucida Grande", "Lucida Sans Unicode", Geneva, Verdana, sans-serif;
font-weight: 600;
font-size: 20px;
color: var(--island-header-color);
}
div {
width: 85%;
max-width: 400px;
margin: auto;
margin-top: 25px;
padding: 10px;
background-color: var(--island-bg-color);
border-radius: 10px;
box-shadow: 5px 5px 30px rgba(0, 0, 0, 0.29);
}
</style>

View File

@ -1,36 +1,36 @@
<script lang="ts">
import { fade } from 'svelte/transition';
import { fade } from "svelte/transition";
export let variables = { title: '' };
export let variables = { title: "" };
</script>
<div in:fade={{ duration: 150 }} out:fade={{ duration: 150 }}>
<span>{variables.title}</span>
<slot />
<span>{variables.title}</span>
<slot />
</div>
<style>
span {
font-family: urwgothic, 'Lucida Sans', 'Lucida Sans Regular', 'Lucida Grande', 'Lucida Sans Unicode', Geneva, Verdana, sans-serif;
color: var(--island-header-color);
font-weight: 600;
font-size: 20px;
}
div {
position: fixed;
top: 50%;
left: 50%;
transform: translateY(-50%) translateX(-50%);
width: 85%;
height: auto;
max-height: 75vh;
overflow-y: auto;
max-width: 400px;
margin: auto;
margin-top: 25px;
padding: 10px;
background-color: var(--island-bg-solid);
border-radius: 10px;
z-index: 1000;
}
span {
font-family: urwgothic, "Lucida Sans", "Lucida Sans Regular", "Lucida Grande", "Lucida Sans Unicode", Geneva, Verdana, sans-serif;
color: var(--island-header-color);
font-weight: 600;
font-size: 20px;
}
div {
position: fixed;
top: 50%;
left: 50%;
transform: translateY(-50%) translateX(-50%);
width: 85%;
height: auto;
max-height: 75vh;
overflow-y: auto;
max-width: 400px;
margin: auto;
margin-top: 25px;
padding: 10px;
background-color: var(--island-bg-solid);
border-radius: 10px;
z-index: 1000;
}
</style>

View File

@ -1,54 +1,54 @@
<script lang="ts">
import Island from '$lib/islands/island.svelte';
import { ql } from '$lib/stores/quick-links';
export let variables = {
title: 'Quick Links'
};
import Island from "$lib/islands/island.svelte";
import { ql } from "$lib/stores/quick-links";
export let variables = {
title: "Quick Links"
};
</script>
<Island {variables}>
{#if $ql.length === 0}
<p>Go to <a href="/more/settings">settings</a> to add your Quick Links</p>
{/if}
<div class="buttons">
{#each $ql as link}
{#if link.length === 3}
<a class="link" href="/ldb?station={link}">
{link.toUpperCase()}
</a>
{:else if link.length === 4}
<a class="link" href="/train?headcode={link}">
{link.toUpperCase()}
</a>
{/if}
{/each}
</div>
{#if $ql.length === 0}
<p>Go to <a href="/more/settings">settings</a> to add your Quick Links</p>
{/if}
<div class="buttons">
{#each $ql as link}
{#if link.length === 3}
<a class="link" href="/ldb?station={link}">
{link.toUpperCase()}
</a>
{:else if link.length === 4}
<a class="link" href="/train?headcode={link}">
{link.toUpperCase()}
</a>
{/if}
{/each}
</div>
</Island>
<style>
.buttons {
display: flex;
flex-wrap: wrap;
justify-content: center;
align-items: center;
width: 90%;
margin: auto;
padding-top: 5px;
}
.link {
flex: 1;
width: 20%;
min-width: 50px;
margin: 5px;
border: none;
border-radius: 20px;
padding: 5px;
font-family: urwgothic, 'Lucida Sans', 'Lucida Sans Regular', 'Lucida Grande', 'Lucida Sans Unicode', Geneva, Verdana, sans-serif;
font-size: 16px;
font-weight: 400;
text-decoration: none;
background-color: var(--island-button-color);
color: var(--island-link-color);
box-shadow: var(--box-shadow);
}
.buttons {
display: flex;
flex-wrap: wrap;
justify-content: center;
align-items: center;
width: 90%;
margin: auto;
padding-top: 5px;
}
.link {
flex: 1;
width: 20%;
min-width: 50px;
margin: 5px;
border: none;
border-radius: 20px;
padding: 5px;
font-family: urwgothic, "Lucida Sans", "Lucida Sans Regular", "Lucida Grande", "Lucida Sans Unicode", Geneva, Verdana, sans-serif;
font-size: 16px;
font-weight: 400;
text-decoration: none;
background-color: var(--island-button-color);
color: var(--island-link-color);
box-shadow: var(--box-shadow);
}
</style>

View File

@ -1,117 +1,117 @@
<script lang="ts">
import Island from '$lib/islands/island.svelte';
import { ql } from '$lib/stores/quick-links';
export let variables = {
title: 'Quick Links'
};
import Island from "$lib/islands/island.svelte";
import { ql } from "$lib/stores/quick-links";
export let variables = {
title: "Quick Links"
};
let qlData: string[] = [];
$: {
qlData = $ql;
console.log(qlData);
}
let saveButton = 'Save';
async function timeout(ms: number): Promise<any> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
async function saveQl() {
// Fetch the content of all text entries within the island then run ql.set([ARRAY OF INPUT CONTENT])
const inputs = document.getElementsByClassName('qlInput');
let inputLinks: string[] = [];
for (let item of inputs) {
let text = (<HTMLInputElement>item)?.value;
if (text !== '') {
inputLinks.push(text);
}
let qlData: string[] = [];
$: {
qlData = $ql;
console.log(qlData);
}
console.log(inputLinks);
ql.set(inputLinks);
saveButton = '&#10004;';
await timeout(3000);
saveButton = 'Saved';
}
function clearQl() {
ql.set([]);
saveButton = 'Saved';
}
let saveButton = "Save";
function addQlBox() {
saveButton = 'Save';
const updatedQl = [...$ql, ''];
$ql = updatedQl;
ql.set(updatedQl);
}
async function timeout(ms: number): Promise<any> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
function handleClick(event: any) {
// Handle the click event here
console.log('Island Clicked');
// You can access the `variables` passed to the Island component here if needed
}
async function saveQl() {
// Fetch the content of all text entries within the island then run ql.set([ARRAY OF INPUT CONTENT])
const inputs = document.getElementsByClassName("qlInput");
let inputLinks: string[] = [];
for (let item of inputs) {
let text = (<HTMLInputElement>item)?.value;
if (text !== "") {
inputLinks.push(text);
}
}
console.log(inputLinks);
ql.set(inputLinks);
saveButton = "&#10004;";
await timeout(3000);
saveButton = "Saved";
}
function clearQl() {
ql.set([]);
saveButton = "Saved";
}
function addQlBox() {
saveButton = "Save";
const updatedQl = [...$ql, ""];
$ql = updatedQl;
ql.set(updatedQl);
}
function handleClick(event: any) {
// Handle the click event here
console.log("Island Clicked");
// You can access the `variables` passed to the Island component here if needed
}
</script>
<Island on:click={handleClick} {variables}>
{#if $ql.length === 0}
<p>Click the + button to add links</p>
{/if}
<div id="buttons" class="buttons">
<p>Quick links can be CRS Codes or Headcodes</p>
{#each qlData as link}
<input class="qlInput" type="text" value={link} />
{/each}
<button on:click={addQlBox} id="qlAdd">+</button>
</div>
<button on:click={saveQl}>{@html saveButton}</button>
<button on:click={clearQl}>Clear</button>
{#if $ql.length === 0}
<p>Click the + button to add links</p>
{/if}
<div id="buttons" class="buttons">
<p>Quick links can be CRS Codes or Headcodes</p>
{#each qlData as link}
<input class="qlInput" type="text" value={link} />
{/each}
<button on:click={addQlBox} id="qlAdd">+</button>
</div>
<button on:click={saveQl}>{@html saveButton}</button>
<button on:click={clearQl}>Clear</button>
</Island>
<style>
p {
margin-bottom: 0;
}
#qlAdd {
width: 40px;
}
.buttons {
display: flex;
flex-wrap: wrap;
justify-content: center;
align-items: center;
width: 90%;
margin: auto;
padding-top: 5px;
}
input {
flex: 1;
width: 20%;
min-width: 50px;
margin: 5px;
border: none;
border-radius: 20px;
padding: 5px;
font-family: urwgothic, 'Lucida Sans', 'Lucida Sans Regular', 'Lucida Grande', 'Lucida Sans Unicode', Geneva, Verdana, sans-serif;
font-size: 16px;
font-weight: 400;
text-decoration: none;
text-align: center;
text-transform: uppercase;
box-shadow: var(--box-shadow);
}
button {
width: 30%;
margin-bottom: 5px;
margin-top: 10px;
border: none;
border-radius: 20px;
padding: 5px;
font-family: urwgothic, 'Lucida Sans', 'Lucida Sans Regular', 'Lucida Grande', 'Lucida Sans Unicode', Geneva, Verdana, sans-serif;
font-size: 16px;
font-weight: 400;
background-color: var(--island-button-color);
color: var(--island-link-color);
box-shadow: var(--box-shadow);
}
p {
margin-bottom: 0;
}
#qlAdd {
width: 40px;
}
.buttons {
display: flex;
flex-wrap: wrap;
justify-content: center;
align-items: center;
width: 90%;
margin: auto;
padding-top: 5px;
}
input {
flex: 1;
width: 20%;
min-width: 50px;
margin: 5px;
border: none;
border-radius: 20px;
padding: 5px;
font-family: urwgothic, "Lucida Sans", "Lucida Sans Regular", "Lucida Grande", "Lucida Sans Unicode", Geneva, Verdana, sans-serif;
font-size: 16px;
font-weight: 400;
text-decoration: none;
text-align: center;
text-transform: uppercase;
box-shadow: var(--box-shadow);
}
button {
width: 30%;
margin-bottom: 5px;
margin-top: 10px;
border: none;
border-radius: 20px;
padding: 5px;
font-family: urwgothic, "Lucida Sans", "Lucida Sans Regular", "Lucida Grande", "Lucida Sans Unicode", Geneva, Verdana, sans-serif;
font-size: 16px;
font-weight: 400;
background-color: var(--island-button-color);
color: var(--island-link-color);
box-shadow: var(--box-shadow);
}
</style>

View File

@ -1,31 +1,31 @@
<script lang="ts">
import Island from '$lib/islands/island.svelte';
import Island from "$lib/islands/island.svelte";
interface resultObj {
results: boolean;
title: string;
resultLines: string[];
}
interface resultObj {
results: boolean;
title: string;
resultLines: string[];
}
export let resultObject: resultObj = {
results: true,
title: '',
resultLines: []
};
export let resultObject: resultObj = {
results: true,
title: "",
resultLines: []
};
let variables = {
title: resultObject.title
};
let variables = {
title: resultObject.title
};
</script>
<Island {variables}>
{#each resultObject.resultLines as line}
<p>{line}</p>
{/each}
{#each resultObject.resultLines as line}
<p>{line}</p>
{/each}
</Island>
<style>
p {
color: var(--island-text-color);
}
p {
color: var(--island-text-color);
}
</style>

View File

@ -1,118 +1,118 @@
<script>
import { fly } from 'svelte/transition';
import { fly } from "svelte/transition";
export let alerts = [];
$: uniqueAlerts = [...new Set(alerts)];
export let alerts = [];
$: uniqueAlerts = [...new Set(alerts)];
let displayAlerts = false;
let displayAlerts = false;
async function alertsToggle() {
displayAlerts = !displayAlerts;
}
function numberAsWord(number) {
const words = ['zero', 'one', 'two', 'three', 'four', 'five', 'six', 'seven', 'eight', 'nine'];
let word = words[number];
if (word) {
return word;
async function alertsToggle() {
displayAlerts = !displayAlerts;
}
function numberAsWord(number) {
const words = ["zero", "one", "two", "three", "four", "five", "six", "seven", "eight", "nine"];
let word = words[number];
if (word) {
return word;
}
return number;
}
return number;
}
</script>
<div id="block"><!--Prevent content slipping underneath the bar--></div>
<div id="bar" on:click={alertsToggle} on:keypress={alertsToggle}>
<img src="/images/navigation/alert.svg" alt="" />
{#if uniqueAlerts.length == 1}
<p id="bartext">There is one active alert</p>
{:else if uniqueAlerts.length > 1}
<p id="bartext">
There are {numberAsWord(uniqueAlerts.length)} active alerts
</p>
{:else}
<p id="bartext">There are no active alerts</p>
{/if}
<p id="arrow" class:displayAlerts>V</p>
<img src="/images/navigation/alert.svg" alt="" />
{#if uniqueAlerts.length == 1}
<p id="bartext">There is one active alert</p>
{:else if uniqueAlerts.length > 1}
<p id="bartext">
There are {numberAsWord(uniqueAlerts.length)} active alerts
</p>
{:else}
<p id="bartext">There are no active alerts</p>
{/if}
<p id="arrow" class:displayAlerts>V</p>
</div>
{#if displayAlerts}
<div id="alerts" in:fly={{ y: -200, duration: 500 }} out:fly={{ y: -200, duration: 800 }}>
{#each uniqueAlerts as msg}
<p class="alert">{@html msg}</p>
{/each}
</div>
<div id="alerts" in:fly={{ y: -200, duration: 500 }} out:fly={{ y: -200, duration: 800 }}>
{#each uniqueAlerts as msg}
<p class="alert">{@html msg}</p>
{/each}
</div>
{/if}
<style>
#block {
height: 40px;
}
#bar {
width: 100%;
height: 40px;
background-color: var(--main-alert-color);
opacity: 1;
position: fixed;
width: 100%;
top: 50px;
left: 0px;
z-index: 10;
}
img {
height: 25px;
width: 25px;
position: absolute;
left: 8px;
top: 8px;
margin: 0;
padding: 0;
}
#bartext {
color: white;
margin: auto;
font-weight: 600;
margin-top: 8px;
margin-bottom: 0px;
padding: 0px;
}
#arrow {
color: white;
font-family: Arial, Helvetica, sans-serif;
font-weight: 900;
position: absolute;
margin-top: 0;
right: 15px;
top: 11px;
border: none;
background-color: transparent;
transition-duration: 500ms;
transition-delay: 00ms;
}
#arrow:focus {
background-color: transparent;
}
#alerts {
position: fixed;
background-color: var(--main-alert-color);
opacity: 0.9;
width: 100%;
max-height: 80vh;
overflow-y: auto;
overflow-x: clip;
left: 0;
top: 89px;
}
.alert {
color: white;
text-align: center;
width: 80%;
margin: auto;
margin-top: 10px;
margin-bottom: 10px;
font-weight: 600;
}
.displayAlerts {
transition-duration: 500ms;
transition-delay: 400ms;
transform: rotate(180deg);
}
#block {
height: 40px;
}
#bar {
width: 100%;
height: 40px;
background-color: var(--main-alert-color);
opacity: 1;
position: fixed;
width: 100%;
top: 50px;
left: 0px;
z-index: 10;
}
img {
height: 25px;
width: 25px;
position: absolute;
left: 8px;
top: 8px;
margin: 0;
padding: 0;
}
#bartext {
color: white;
margin: auto;
font-weight: 600;
margin-top: 8px;
margin-bottom: 0px;
padding: 0px;
}
#arrow {
color: white;
font-family: Arial, Helvetica, sans-serif;
font-weight: 900;
position: absolute;
margin-top: 0;
right: 15px;
top: 11px;
border: none;
background-color: transparent;
transition-duration: 500ms;
transition-delay: 00ms;
}
#arrow:focus {
background-color: transparent;
}
#alerts {
position: fixed;
background-color: var(--main-alert-color);
opacity: 0.9;
width: 100%;
max-height: 80vh;
overflow-y: auto;
overflow-x: clip;
left: 0;
top: 89px;
}
.alert {
color: white;
text-align: center;
width: 80%;
margin: auto;
margin-top: 10px;
margin-bottom: 10px;
font-weight: 600;
}
.displayAlerts {
transition-duration: 500ms;
transition-delay: 400ms;
transform: rotate(180deg);
}
</style>

View File

@ -1,486 +1,491 @@
<script>
export let station = '';
export let title = 'Loading...';
import { onMount } from 'svelte';
import Loading from '$lib/navigation/loading.svelte';
import OverlayIsland from '$lib/islands/overlay-island.svelte';
import AlertBar from '$lib/ldb/nrcc/alert-bar.svelte';
import Island from '$lib/islands/island.svelte';
import { getApiUrl } from '$lib/scripts/upstream';
export let station = "";
export let title = "Loading...";
import { onMount } from "svelte";
import Loading from "$lib/navigation/loading.svelte";
import OverlayIsland from "$lib/islands/overlay-island.svelte";
import AlertBar from "$lib/ldb/nrcc/alert-bar.svelte";
import Island from "$lib/islands/island.svelte";
import { getApiUrl } from "$lib/scripts/upstream";
let requestedStation;
$: requestedStation = station;
let requestedStation;
$: requestedStation = station;
let jsonData = null;
let services = [];
let busServices = [];
let ferryServices = [];
let dataAge = null;
let isLoading = true;
let dataExists = false;
let alerts = [];
let serviceDetail;
let jsonData = null;
let services = [];
let busServices = [];
let ferryServices = [];
let dataAge = null;
let isLoading = true;
let dataExists = false;
let alerts = [];
let serviceDetail;
$: {
if (jsonData === null && requestedStation) {
fetchData();
$: {
if (jsonData === null && requestedStation) {
fetchData();
}
if (jsonData?.GetStationBoardResult?.generatedAt) {
dataAge = new Date(jsonData.GetStationBoardResult.generatedAt);
}
if (jsonData?.GetStationBoardResult?.trainServices?.service) {
services = jsonData.GetStationBoardResult.trainServices.service;
} else {
services = [];
}
if (jsonData?.GetStationBoardResult?.busServices?.service) {
busServices = jsonData.GetStationBoardResult.busServices.service;
}
if (jsonData?.GetStationBoardResult?.ferryServices?.service) {
ferryServices = jsonData.GetStationBoardResult.ferryServices.service;
}
if (jsonData?.GetStationBoardResult?.locationName) {
title = jsonData.GetStationBoardResult.locationName;
} else {
title = requestedStation.toUpperCase();
}
}
if (jsonData?.GetStationBoardResult?.generatedAt) {
dataAge = new Date(jsonData.GetStationBoardResult.generatedAt);
async function fetchData() {
dataExists = true;
isLoading = true; // Set loading state
try {
console.log(`Requested Station: ${requestedStation}`);
const data = await fetch(`${getApiUrl()}/api/v2/live/station/${requestedStation}/public`);
jsonData = await data.json();
} catch (error) {
console.error("Error fetching data:", error);
dataExists = false;
title = "Not Found";
} finally {
isLoading = false; // Clear loading state
}
prepareNrcc();
}
if (jsonData?.GetStationBoardResult?.trainServices?.service) {
services = jsonData.GetStationBoardResult.trainServices.service;
} else {
services = [];
function parseTime(string) {
let output;
let change;
switch (string) {
case "Delayed":
output = "LATE";
change = "changed";
break;
case "Cancelled":
output = "CANC";
change = "cancelled";
break;
case "On Time":
case "On time":
output = "RT";
change = "";
break;
case "":
output = "-";
change = "";
break;
case undefined:
output = "-";
change = "";
break;
case "No report":
output = "-";
change = "";
break;
case "undefined":
output = false;
change = "";
break;
default:
output = string;
change = "changed";
}
return { data: output, changed: change };
}
if (jsonData?.GetStationBoardResult?.busServices?.service) {
busServices = jsonData.GetStationBoardResult.busServices.service;
async function loadService(sid) {
for (const service of services) {
if (service.serviceID == sid) {
serviceDetail = service;
}
}
}
if (jsonData?.GetStationBoardResult?.ferryServices?.service) {
ferryServices = jsonData.GetStationBoardResult.ferryServices.service;
async function loadBusService(sid) {
for (const service of busServices) {
if (service.serviceID == sid) {
serviceDetail = service;
}
}
}
if (jsonData?.GetStationBoardResult?.locationName) {
title = jsonData.GetStationBoardResult.locationName;
} else {
title = requestedStation.toUpperCase();
async function closeService() {
serviceDetail = null;
}
}
async function fetchData() {
dataExists = true;
isLoading = true; // Set loading state
try {
console.log(`Requested Station: ${requestedStation}`);
const data = await fetch(`${getApiUrl()}/api/v2/live/station/${requestedStation}/public`);
jsonData = await data.json();
} catch (error) {
console.error('Error fetching data:', error);
dataExists = false;
title = 'Not Found';
} finally {
isLoading = false; // Clear loading state
async function prepareNrcc() {
if (jsonData?.GetStationBoardResult?.nrccMessages?.message) {
const nrcc = jsonData.GetStationBoardResult.nrccMessages.message;
if (Array.isArray(nrcc)) {
alerts = nrcc;
return;
}
alerts.push(nrcc);
return;
}
}
prepareNrcc();
}
function parseTime(string) {
let output;
let change;
switch (string) {
case 'Delayed':
output = 'LATE';
change = 'changed';
break;
case 'Cancelled':
output = 'CANC';
change = 'cancelled';
break;
case 'On Time':
case 'On time':
output = 'RT';
change = '';
break;
case '':
output = '-';
change = '';
break;
case undefined:
output = '-';
change = '';
break;
case 'No report':
output = '-';
change = '';
break;
case 'undefined':
output = false;
change = '';
break;
default:
output = string;
change = 'changed';
}
return { data: output, changed: change };
}
async function loadService(sid) {
for (const service of services) {
if (service.serviceID == sid) {
serviceDetail = service;
}
}
}
async function loadBusService(sid) {
for (const service of busServices) {
if (service.serviceID == sid) {
serviceDetail = service;
}
}
}
async function closeService() {
serviceDetail = null;
}
async function prepareNrcc() {
if (jsonData?.GetStationBoardResult?.nrccMessages?.message) {
const nrcc = jsonData.GetStationBoardResult.nrccMessages.message;
if (Array.isArray(nrcc)) {
alerts = nrcc;
return;
}
alerts.push(nrcc);
return;
}
}
onMount(() => {
if (requestedStation && jsonData === null) {
fetchData();
}
});
onMount(() => {
if (requestedStation && jsonData === null) {
fetchData();
}
});
</script>
{#if alerts.length}
<AlertBar {alerts} />
<AlertBar {alerts} />
{/if}
{#if isLoading}
<Loading />
<Loading />
{:else if dataAge}
<p id="timestamp">Updated: {dataAge.toLocaleTimeString()}</p>
{#if services.length}
<table class="ldbTable">
<tr>
<th class="from">From</th>
<th class="to">To</th>
<th class="plat">Plat.</th>
<th class="time">Sch Arr.</th>
<th class="time">Exp Arr.</th>
<th class="time">Sch Dep.</th>
<th class="time">Exp Dep.</th>
</tr>
{#each services as service}
<tr>
<td class="origdest from" on:click={loadService(service.serviceID)} on:keypress={loadService(service.serviceID)}>
{#if Array.isArray(service.origin?.location)}
{service.origin.location[0]['locationName'] + ' & ' + service.origin.location[1]['locationName']}
{:else}
{service.origin?.location?.locationName || ''}
{/if}
</td>
<td class="origdest to" on:click={loadService(service.serviceID)} on:keypress={loadService(service.serviceID)}>
{#if Array.isArray(service.destination?.location)}
{service.destination.location[0]['locationName'] + ' & ' + service.destination.location[0]['locationName']}
{:else}
{service.destination?.location?.locationName || ''}
{/if}
</td>
<td class="plat">{service.platform || '-'}</td>
<td class="time">{parseTime(service.sta).data}</td>
<td class="time {parseTime(service.eta).changed}">{parseTime(service.eta).data}</td>
<td class="time">{parseTime(service.std).data}</td>
<td class="time {parseTime(service.etd).changed}">{parseTime(service.etd).data}</td>
</tr>
<p id="timestamp">Updated: {dataAge.toLocaleTimeString()}</p>
{#if services.length}
<table class="ldbTable">
<tr>
<th class="from">From</th>
<th class="to">To</th>
<th class="plat">Plat.</th>
<th class="time">Sch Arr.</th>
<th class="time">Exp Arr.</th>
<th class="time">Sch Dep.</th>
<th class="time">Exp Dep.</th>
</tr>
{#each services as service}
<tr>
<td class="origdest from" on:click={loadService(service.serviceID)} on:keypress={loadService(service.serviceID)}>
{#if Array.isArray(service.origin?.location)}
{service.origin.location[0]["locationName"] + " & " + service.origin.location[1]["locationName"]}
{:else}
{service.origin?.location?.locationName || ""}
{/if}
</td>
<td class="origdest to" on:click={loadService(service.serviceID)} on:keypress={loadService(service.serviceID)}>
{#if Array.isArray(service.destination?.location)}
{service.destination.location[0]["locationName"] + " & " + service.destination.location[0]["locationName"]}
{:else}
{service.destination?.location?.locationName || ""}
{/if}
</td>
<td class="plat">{service.platform || "-"}</td>
<td class="time">{parseTime(service.sta).data}</td>
<td class="time {parseTime(service.eta).changed}">{parseTime(service.eta).data}</td>
<td class="time">{parseTime(service.std).data}</td>
<td class="time {parseTime(service.etd).changed}">{parseTime(service.etd).data}</td>
</tr>
<tr
><td colspan="7">
<p class="service-detail">
A {service.operator || 'Unknown'} service
{#if service['length']}
with {service['length'] || 'some'} coaches
{/if}
</p>
{#if service.delayReason}
<p class="service-detail">{service.delayReason}</p>
{/if}
{#if service.cancelReason}
<p class="service-detail">{service.cancelReason}</p>
{/if}
</td></tr
>
{/each}
</table>
{:else}
<p class="table-head-text">No Scheduled Train Services</p>
{/if}
{#if busServices.length}
<br />
<img class="transport-mode" src="/images/transport-modes/bus.svg" alt="Bus services" /><br />
<span class="table-head-text">Bus Services</span>
<table class="ldbTable">
<tr>
<th class="from">From</th>
<th class="to">To</th>
<th class="time">Sch Arr.</th>
<th class="time">Exp Arr.</th>
<th class="time">Sch Dep.</th>
<th class="time">Exp Dep.</th>
</tr>
{#each busServices as service}
<tr>
<td class="origdest from" on:click={loadBusService(service.serviceID)} on:keypress={loadBusService(service.serviceID)}>{service.origin?.location?.locationName || ''}</td>
<td class="origdest to" on:click={loadBusService(service.serviceID)} on:keypress={loadBusService(service.serviceID)}
>{service.destination?.location?.locationName || ''}</td
>
<td class="time">{parseTime(service.sta).data}</td>
<td class="time {parseTime(service.eta).changed}">{parseTime(service.eta).data}</td>
<td class="time">{parseTime(service.std).data}</td>
<td class="time {parseTime(service.etd).changed}">{parseTime(service.etd).data}</td>
</tr>
<tr
><td colspan="7">
<p class="service-detail">
A {service.operator || "Unknown"} service
{#if service["length"]}
with {service["length"] || "some"} coaches
{/if}
</p>
{#if service.delayReason}
<p class="service-detail">{service.delayReason}</p>
{/if}
{#if service.cancelReason}
<p class="service-detail">{service.cancelReason}</p>
{/if}
</td></tr
>
{/each}
</table>
{:else}
<p class="table-head-text">No Scheduled Train Services</p>
{/if}
{#if busServices.length}
<br />
<img class="transport-mode" src="/images/transport-modes/bus.svg" alt="Bus services" /><br />
<span class="table-head-text">Bus Services</span>
<table class="ldbTable">
<tr>
<th class="from">From</th>
<th class="to">To</th>
<th class="time">Sch Arr.</th>
<th class="time">Exp Arr.</th>
<th class="time">Sch Dep.</th>
<th class="time">Exp Dep.</th>
</tr>
{#each busServices as service}
<tr>
<td class="origdest from" on:click={loadBusService(service.serviceID)} on:keypress={loadBusService(service.serviceID)}
>{service.origin?.location?.locationName || ""}</td
>
<td class="origdest to" on:click={loadBusService(service.serviceID)} on:keypress={loadBusService(service.serviceID)}
>{service.destination?.location?.locationName || ""}</td
>
<td class="time">{parseTime(service.sta).data}</td>
<td class="time {parseTime(service.eta).changed}">{parseTime(service.eta).data}</td>
<td class="time">{parseTime(service.std).data}</td>
<td class="time {parseTime(service.etd).changed}">{parseTime(service.etd).data}</td>
</tr>
<tr
><td colspan="7">
<p class="service-detail">
A {service.operator || 'Unknown'} service
</p>
{#if service.delayReason}
<p class="service-detail">{service.delayReason}</p>
{/if}
{#if service.cancelReason}
<p class="service-detail">{service.cancelReason}</p>
{/if}
</td></tr
>
{/each}
</table>
{/if}
{#if ferryServices.length}
<br />
<img class="transport-mode" src="/images/transport-modes/ferry.svg" alt="Bus services" /><br />
<span class="table-head-text">Ferry Services</span>
<table class="ldbTable">
<tr>
<th class="from">From</th>
<th class="to">To</th>
<th class="time">Sch Arr.</th>
<th class="time">Exp Arr.</th>
<th class="time">Sch Dep.</th>
<th class="time">Exp Dep.</th>
</tr>
{#each ferryServices as service}
<tr>
<td class="origdest from">{service.origin?.location?.locationName || ''}</td>
<td class="origdest to">{service.destination?.location?.locationName || ''}</td>
<td class="time">{parseTime(service.sta).data}</td>
<td class="time {parseTime(service.eta).changed}">{parseTime(service.eta).data}</td>
<td class="time">{parseTime(service.std).data}</td>
<td class="time {parseTime(service.etd).changed}">{parseTime(service.etd).data}</td>
</tr>
<tr
><td colspan="7">
<p class="service-detail">
A {service.operator || "Unknown"} service
</p>
{#if service.delayReason}
<p class="service-detail">{service.delayReason}</p>
{/if}
{#if service.cancelReason}
<p class="service-detail">{service.cancelReason}</p>
{/if}
</td></tr
>
{/each}
</table>
{/if}
{#if ferryServices.length}
<br />
<img class="transport-mode" src="/images/transport-modes/ferry.svg" alt="Bus services" /><br />
<span class="table-head-text">Ferry Services</span>
<table class="ldbTable">
<tr>
<th class="from">From</th>
<th class="to">To</th>
<th class="time">Sch Arr.</th>
<th class="time">Exp Arr.</th>
<th class="time">Sch Dep.</th>
<th class="time">Exp Dep.</th>
</tr>
{#each ferryServices as service}
<tr>
<td class="origdest from">{service.origin?.location?.locationName || ""}</td>
<td class="origdest to">{service.destination?.location?.locationName || ""}</td>
<td class="time">{parseTime(service.sta).data}</td>
<td class="time {parseTime(service.eta).changed}">{parseTime(service.eta).data}</td>
<td class="time">{parseTime(service.std).data}</td>
<td class="time {parseTime(service.etd).changed}">{parseTime(service.etd).data}</td>
</tr>
<tr
><td colspan="7">
{#if service.delayReason}
<p class="service-detail">{service.delayReason}</p>
{/if}
{#if service.cancelReason}
<p class="service-detail">{service.cancelReason}</p>
{/if}
</td></tr
>
{/each}
</table>
{/if}
<tr
><td colspan="7">
{#if service.delayReason}
<p class="service-detail">{service.delayReason}</p>
{/if}
{#if service.cancelReason}
<p class="service-detail">{service.cancelReason}</p>
{/if}
</td></tr
>
{/each}
</table>
{/if}
{:else}
<Island>
<p style="font-weight:600">Unable to load data</p>
</Island>
<Island>
<p style="font-weight:600">Unable to load data</p>
</Island>
{/if}
{#if serviceDetail}
<OverlayIsland>
<div id="detailBox">
<h6>Service Detail</h6>
<button type="button" id="closeService" on:click={closeService}>X</button>
<table id="detailTable">
<tr>
<th>Location</th>
<th>Sch</th>
<th>Exp</th>
</tr>
{#if serviceDetail?.previousCallingPoints?.callingPointList?.callingPoint}
{#if Array.isArray(serviceDetail?.previousCallingPoints?.callingPointList?.callingPoint)}
{#each serviceDetail.previousCallingPoints.callingPointList.callingPoint as prevPoint}
<tr>
<td>{prevPoint.locationName}</td>
<td>{prevPoint.st}</td>
<td class="time {parseTime(prevPoint.at || prevPoint.et).changed}">{parseTime(prevPoint.at || prevPoint.et).data}</td>
</tr>
{/each}
{:else}
<tr>
<td>{serviceDetail.previousCallingPoints.callingPointList.callingPoint.locationName}</td>
<td>{serviceDetail.previousCallingPoints.callingPointList.callingPoint.st}</td>
<td
class="time {parseTime(serviceDetail.previousCallingPoints.callingPointList.callingPoint.at || serviceDetail.previousCallingPoints.callingPointList.callingPoint.et)
.changed}"
>{parseTime(serviceDetail.previousCallingPoints.callingPointList.callingPoint.at || serviceDetail.previousCallingPoints.callingPointList.callingPoint.et).data}</td
>
</tr>
{/if}
{/if}
<tr class="thisStop">
<td>{title}</td>
<td>{serviceDetail.std || serviceDetail.sta}</td>
<td class="time {parseTime(serviceDetail.etd || serviceDetail.eta).changed}">{parseTime(serviceDetail.etd || serviceDetail.eta).data}</td>
</tr>
{#if serviceDetail?.subsequentCallingPoints?.callingPointList?.callingPoint}
{#if Array.isArray(serviceDetail?.subsequentCallingPoints?.callingPointList?.callingPoint)}
{#each serviceDetail.subsequentCallingPoints.callingPointList.callingPoint as nextPoint}
<tr>
<td>{nextPoint.locationName}</td>
<td>{nextPoint.st}</td>
<td class="time {parseTime(nextPoint.et).changed}">{parseTime(nextPoint.et).data}</td>
</tr>
{/each}
{:else}
<tr class="detailRow">
<td>{serviceDetail.subsequentCallingPoints.callingPointList.callingPoint.locationName}</td>
<td>{serviceDetail.subsequentCallingPoints.callingPointList.callingPoint.st}</td>
<td class="time {parseTime(serviceDetail.subsequentCallingPoints.callingPointList.callingPoint.et).changed}"
>{parseTime(serviceDetail.subsequentCallingPoints.callingPointList.callingPoint.et).data}</td
>
</tr>
{/if}
{/if}
</table>
</div>
</OverlayIsland>
<OverlayIsland>
<div id="detailBox">
<h6>Service Detail</h6>
<button type="button" id="closeService" on:click={closeService}>X</button>
<table id="detailTable">
<tr>
<th>Location</th>
<th>Sch</th>
<th>Exp</th>
</tr>
{#if serviceDetail?.previousCallingPoints?.callingPointList?.callingPoint}
{#if Array.isArray(serviceDetail?.previousCallingPoints?.callingPointList?.callingPoint)}
{#each serviceDetail.previousCallingPoints.callingPointList.callingPoint as prevPoint}
<tr>
<td>{prevPoint.locationName}</td>
<td>{prevPoint.st}</td>
<td class="time {parseTime(prevPoint.at || prevPoint.et).changed}">{parseTime(prevPoint.at || prevPoint.et).data}</td>
</tr>
{/each}
{:else}
<tr>
<td>{serviceDetail.previousCallingPoints.callingPointList.callingPoint.locationName}</td>
<td>{serviceDetail.previousCallingPoints.callingPointList.callingPoint.st}</td>
<td
class="time {parseTime(
serviceDetail.previousCallingPoints.callingPointList.callingPoint.at || serviceDetail.previousCallingPoints.callingPointList.callingPoint.et
).changed}"
>{parseTime(
serviceDetail.previousCallingPoints.callingPointList.callingPoint.at || serviceDetail.previousCallingPoints.callingPointList.callingPoint.et
).data}</td
>
</tr>
{/if}
{/if}
<tr class="thisStop">
<td>{title}</td>
<td>{serviceDetail.std || serviceDetail.sta}</td>
<td class="time {parseTime(serviceDetail.etd || serviceDetail.eta).changed}">{parseTime(serviceDetail.etd || serviceDetail.eta).data}</td>
</tr>
{#if serviceDetail?.subsequentCallingPoints?.callingPointList?.callingPoint}
{#if Array.isArray(serviceDetail?.subsequentCallingPoints?.callingPointList?.callingPoint)}
{#each serviceDetail.subsequentCallingPoints.callingPointList.callingPoint as nextPoint}
<tr>
<td>{nextPoint.locationName}</td>
<td>{nextPoint.st}</td>
<td class="time {parseTime(nextPoint.et).changed}">{parseTime(nextPoint.et).data}</td>
</tr>
{/each}
{:else}
<tr class="detailRow">
<td>{serviceDetail.subsequentCallingPoints.callingPointList.callingPoint.locationName}</td>
<td>{serviceDetail.subsequentCallingPoints.callingPointList.callingPoint.st}</td>
<td class="time {parseTime(serviceDetail.subsequentCallingPoints.callingPointList.callingPoint.et).changed}"
>{parseTime(serviceDetail.subsequentCallingPoints.callingPointList.callingPoint.et).data}</td
>
</tr>
{/if}
{/if}
</table>
</div>
</OverlayIsland>
{/if}
<style>
#timestamp {
margin: auto;
text-align: left;
font-size: 14px;
}
.ldbTable {
width: 100%;
min-width: 300px;
margin: auto;
padding-right: 2px;
padding-left: 0px;
color: white;
font-size: 13px;
}
.service-detail {
color: cyan;
text-align: left;
font-size: 12px;
padding: 0;
margin: 0;
}
.transport-mode {
width: 30px;
}
.table-head-text {
color: white;
font-size: 14px;
}
@media (min-width: 800px) {
table {
font-size: 15px;
max-width: 850px;
#timestamp {
margin: auto;
text-align: left;
font-size: 14px;
}
.ldbTable {
width: 100%;
min-width: 300px;
margin: auto;
padding-right: 2px;
padding-left: 0px;
color: white;
font-size: 13px;
}
.service-detail {
font-size: 14px;
color: cyan;
text-align: left;
font-size: 12px;
padding: 0;
margin: 0;
}
.transport-mode {
width: 50px;
}
#timestamp {
font-size: 16px;
}
}
@media (min-width: 1000px) {
table {
font-size: 17px;
}
.service-detail {
font-size: 16px;
width: 30px;
}
.table-head-text {
font-size: 16px;
color: white;
font-size: 14px;
}
}
@media (min-width: 1600px) {
table {
font-size: 19px;
@media (min-width: 800px) {
table {
font-size: 15px;
max-width: 850px;
}
.service-detail {
font-size: 14px;
}
.transport-mode {
width: 50px;
}
#timestamp {
font-size: 16px;
}
}
.service-detail {
font-size: 18px;
@media (min-width: 1000px) {
table {
font-size: 17px;
}
.service-detail {
font-size: 16px;
}
.table-head-text {
font-size: 16px;
}
}
@media (min-width: 1600px) {
table {
font-size: 19px;
}
.service-detail {
font-size: 18px;
}
}
.origdest {
color: yellow;
}
.from {
width: 25%;
text-align: left;
}
.to {
width: 25%;
text-align: left;
}
.plat {
width: 10%;
}
.time {
width: 10%;
}
.changed {
animation: pulse-change 1.5s linear infinite;
}
}
.origdest {
color: yellow;
}
.from {
width: 25%;
text-align: left;
}
.to {
width: 25%;
text-align: left;
}
.plat {
width: 10%;
}
.time {
width: 10%;
}
.changed {
animation: pulse-change 1.5s linear infinite;
}
.cancelled {
animation: pulse-cancel 1.5s linear infinite;
}
@keyframes pulse-change {
50% {
color: var(--main-warning-color);
.cancelled {
animation: pulse-cancel 1.5s linear infinite;
}
}
@keyframes pulse-cancel {
50% {
color: var(--main-alert-color);
@keyframes pulse-change {
50% {
color: var(--main-warning-color);
}
}
@keyframes pulse-cancel {
50% {
color: var(--main-alert-color);
}
}
#detailBox {
width: 100%;
}
h6 {
position: absolute;
top: -25px;
left: 20px;
font-size: 18px;
}
#closeService {
position: absolute;
top: 10px;
right: 10px;
border: none;
border-radius: 60px;
width: 35px;
height: 35px;
background-color: var(--main-bg-color);
color: white;
font-weight: 700;
font-size: 16px;
}
#detailTable {
margin-top: 40px;
color: white;
font-size: 15px;
}
.thisStop {
color: yellow;
}
}
#detailBox {
width: 100%;
}
h6 {
position: absolute;
top: -25px;
left: 20px;
font-size: 18px;
}
#closeService {
position: absolute;
top: 10px;
right: 10px;
border: none;
border-radius: 60px;
width: 35px;
height: 35px;
background-color: var(--main-bg-color);
color: white;
font-weight: 700;
font-size: 16px;
}
#detailTable {
margin-top: 40px;
color: white;
font-size: 15px;
}
.thisStop {
color: yellow;
}
</style>

View File

@ -1,39 +1,39 @@
// Fetches StaffLDB Data, correctly formats DATE fields and returns the data
import { getApiUrl } from '$lib/scripts/upstream';
import { uuid } from '$lib/stores/uuid';
import type { ApiResponse, StaffLdb } from '@owlboard/ts-types';
import { getApiUrl } from "$lib/scripts/upstream";
import { uuid } from "$lib/stores/uuid";
import type { ApiResponse, StaffLdb } from "@owlboard/ts-types";
// Fetch StaffLDB Data, and returns the data after hydration (convert date types etc.)
export async function fetchStaffLdb(station: string): Promise<ApiResponse<StaffLdb>> {
const url = `${getApiUrl()}/api/v2/live/station/${station}/staff`;
const url = `${getApiUrl()}/api/v2/live/station/${station}/staff`;
let uuid_value: string = '';
const unsubscribe = uuid.subscribe((value) => {
uuid_value = value;
});
let uuid_value: string = "";
const unsubscribe = uuid.subscribe((value) => {
uuid_value = value;
});
const fetchOpts = {
method: 'GET',
headers: {
uuid: uuid_value
}
};
const res = await fetch(url, fetchOpts);
unsubscribe();
const resJs = await res.json();
return parseFormat(JSON.stringify(resJs));
const fetchOpts = {
method: "GET",
headers: {
uuid: uuid_value
}
};
const res = await fetch(url, fetchOpts);
unsubscribe();
const resJs = await res.json();
return parseFormat(JSON.stringify(resJs));
}
// Parse dates within the JSON response
function parseFormat(jsonString: any): ApiResponse<StaffLdb> {
return JSON.parse(jsonString, (key, value) => {
if (typeof value === 'string') {
const dateRegex = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z$/;
if (dateRegex.test(value)) {
return new Date(value);
}
}
return value;
});
return JSON.parse(jsonString, (key, value) => {
if (typeof value === "string") {
const dateRegex = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z$/;
if (dateRegex.test(value)) {
return new Date(value);
}
}
return value;
});
}

View File

@ -1,89 +1,89 @@
<script lang="ts">
import TableGenerator from './table/table-generator.svelte';
import Loading from '$lib/navigation/loading.svelte';
import type { ApiResponse, StaffLdb } from '@owlboard/ts-types';
import { detailInit, defineDetail } from './train-detail';
import TrainDetail from './train-detail.svelte';
import { fetchStaffLdb } from './fetch';
import AlertBar from '../nrcc/alert-bar.svelte';
import TableGenerator from "./table/table-generator.svelte";
import Loading from "$lib/navigation/loading.svelte";
import type { ApiResponse, StaffLdb } from "@owlboard/ts-types";
import { detailInit, defineDetail } from "./train-detail";
import TrainDetail from "./train-detail.svelte";
import { fetchStaffLdb } from "./fetch";
import AlertBar from "../nrcc/alert-bar.svelte";
export let station: string;
export let title: string | undefined = 'Loading...';
export let station: string;
export let title: string | undefined = "Loading...";
let errorDetail = {
code: '',
message: ''
};
let nrcc: string[] = [];
let errorDetail = {
code: "",
message: ""
};
let nrcc: string[] = [];
let detail = detailInit();
function hideDetail() {
detail = detailInit();
}
function showDetail(rid: string, uid: string, tid: string) {
detail = defineDetail(rid, uid, tid);
}
console.log(`Station: ${station}`);
async function callFetch(station: string): Promise<StaffLdb> {
const data = await fetchStaffLdb(station);
if (data.data) {
title = data.data.locationName;
if (data.data?.nrccMessages) {
for (const msg of data.data.nrccMessages) {
nrcc.push(msg.xhtmlMessage);
}
}
return data.data;
let detail = detailInit();
function hideDetail() {
detail = detailInit();
}
function showDetail(rid: string, uid: string, tid: string) {
detail = defineDetail(rid, uid, tid);
}
console.log(`Station: ${station}`);
async function callFetch(station: string): Promise<StaffLdb> {
const data = await fetchStaffLdb(station);
if (data.data) {
title = data.data.locationName;
if (data.data?.nrccMessages) {
for (const msg of data.data.nrccMessages) {
nrcc.push(msg.xhtmlMessage);
}
}
return data.data;
}
errorDetail.code = data.obStatus.toString() || "UNKNOWN";
errorDetail.message = data.obMsg || "An unknown error occoured";
throw new Error("Unable to Fetch Data");
}
errorDetail.code = data.obStatus.toString() || 'UNKNOWN';
errorDetail.message = data.obMsg || 'An unknown error occoured';
throw new Error('Unable to Fetch Data');
}
</script>
{#key detail}
{#if detail.show}
<TrainDetail {detail} close={hideDetail} />
{/if}
{#if detail.show}
<TrainDetail {detail} close={hideDetail} />
{/if}
{/key}
{#await callFetch(station)}
<Loading />
<Loading />
{:then data}
{#if data}
<p class="generatedTime">Updated: {new Date(data.generatedAt).toLocaleTimeString()}</p>
{#if data.trainServices?.length}
<TableGenerator services={data.trainServices} click={showDetail} />
{#if data}
<p class="generatedTime">Updated: {new Date(data.generatedAt).toLocaleTimeString()}</p>
{#if data.trainServices?.length}
<TableGenerator services={data.trainServices} click={showDetail} />
{/if}
{#if data.busServices?.length}
<img class="transport-mode" src="/images/transport-modes/bus.svg" alt="Bus services" /><br />
<span class="table-head-text">Bus Services</span>
<TableGenerator services={data.busServices} click={showDetail} />
{/if}
{#if data.ferryServices?.length}
<img class="transport-mode" src="/images/transport-modes/ferry.svg" alt="Ferry services" /><br />
<span class="table-head-text">Ferry Services</span>
<TableGenerator services={data.ferryServices} click={showDetail} />
{/if}
{#if nrcc.length}
<AlertBar alerts={nrcc} />
{/if}
<!-- NRCC Alerts are not available -->
{/if}
{#if data.busServices?.length}
<img class="transport-mode" src="/images/transport-modes/bus.svg" alt="Bus services" /><br />
<span class="table-head-text">Bus Services</span>
<TableGenerator services={data.busServices} click={showDetail} />
{/if}
{#if data.ferryServices?.length}
<img class="transport-mode" src="/images/transport-modes/ferry.svg" alt="Ferry services" /><br />
<span class="table-head-text">Ferry Services</span>
<TableGenerator services={data.ferryServices} click={showDetail} />
{/if}
{#if nrcc.length}
<AlertBar alerts={nrcc} />
{/if}
<!-- NRCC Alerts are not available -->
{/if}
{:catch}
<h2>Error</h2>
<p>ERR-CODE: {errorDetail.code}</p>
<p>Message:<br />{errorDetail.message}</p>
<h2>Error</h2>
<p>ERR-CODE: {errorDetail.code}</p>
<p>Message:<br />{errorDetail.message}</p>
{/await}
<style>
.transport-mode {
padding-top: 20px;
height: 17px;
}
.table-head-text {
color: white;
}
.transport-mode {
padding-top: 20px;
height: 17px;
}
.table-head-text {
color: white;
}
</style>

View File

@ -1,319 +1,319 @@
<script lang="ts">
import Reason from '$lib/raw-fetchers/reason.svelte';
import { tocs as tocMap } from '$lib/stores/tocMap';
import Reason from "$lib/raw-fetchers/reason.svelte";
import { tocs as tocMap } from "$lib/stores/tocMap";
import type { TrainServices, ServiceLocation } from '@owlboard/ts-types';
import type { TrainServices, ServiceLocation } from "@owlboard/ts-types";
export let services: TrainServices[];
export let click: Function;
export let services: TrainServices[];
export let click: Function;
function detail(event: any, rid: string, uid: string, tid: string) {
const target = event.target;
click(rid, uid, tid);
}
async function formatLocations(locations: ServiceLocation[]): Promise<string> {
let tiplocs: string[] = [];
for (const location of locations) {
tiplocs.push(location.tiploc);
}
return tiplocs.join(' & ');
}
async function classGenerator(service: TrainServices) {
// This function needs updating next
let otherArr: string[] = [];
let arrArr: string[] = [];
let depArr: string[] = [];
let platArr: string[] = [];
if (service.isCancelled) {
otherArr.push('canc');
}
if (service.serviceIsSupressed) {
otherArr.push('nonPass');
}
if (service.platformIsHidden) {
platArr.push('nonPass');
function detail(event: any, rid: string, uid: string, tid: string) {
const target = event.target;
click(rid, uid, tid);
}
function checkLateEarly(originalTime: Date | undefined, comparedTime: Date | undefined, arr: string[]) {
if (originalTime !== undefined && comparedTime instanceof Date) {
if (originalTime < comparedTime) {
arr.push('late');
} else if (originalTime > comparedTime) {
arr.push('early');
async function formatLocations(locations: ServiceLocation[]): Promise<string> {
let tiplocs: string[] = [];
for (const location of locations) {
tiplocs.push(location.tiploc);
}
}
return tiplocs.join(" & ");
}
checkLateEarly(service.sta, service.eta, arrArr);
checkLateEarly(service.sta, service.ata, arrArr);
checkLateEarly(service.std, service.etd, depArr);
checkLateEarly(service.std, service.atd, depArr);
async function classGenerator(service: TrainServices) {
// This function needs updating next
let otherArr: string[] = [];
let arrArr: string[] = [];
let depArr: string[] = [];
let platArr: string[] = [];
return {
other: otherArr.join(' '),
arr: arrArr.join(' '),
dep: depArr.join(' '),
plat: platArr.join(' ')
};
}
if (service.isCancelled) {
otherArr.push("canc");
}
if (service.serviceIsSupressed) {
otherArr.push("nonPass");
}
if (service.platformIsHidden) {
platArr.push("nonPass");
}
function fmtTime(date: Date | string | undefined): string | false {
if (typeof date === 'string') return date;
if (date instanceof Date) {
const hours = date.getHours().toString().padStart(2, '0');
const minutes = date.getMinutes().toString().padStart(2, '0');
return `${hours}:${minutes}`;
} else {
return false;
function checkLateEarly(originalTime: Date | undefined, comparedTime: Date | undefined, arr: string[]) {
if (originalTime !== undefined && comparedTime instanceof Date) {
if (originalTime < comparedTime) {
arr.push("late");
} else if (originalTime > comparedTime) {
arr.push("early");
}
}
}
checkLateEarly(service.sta, service.eta, arrArr);
checkLateEarly(service.sta, service.ata, arrArr);
checkLateEarly(service.std, service.etd, depArr);
checkLateEarly(service.std, service.atd, depArr);
return {
other: otherArr.join(" "),
arr: arrArr.join(" "),
dep: depArr.join(" "),
plat: platArr.join(" ")
};
}
function fmtTime(date: Date | string | undefined): string | false {
if (typeof date === "string") return date;
if (date instanceof Date) {
const hours = date.getHours().toString().padStart(2, "0");
const minutes = date.getMinutes().toString().padStart(2, "0");
return `${hours}:${minutes}`;
} else {
return false;
}
}
}
</script>
<p class="smallScreen">Your display is too small to view this data</p>
<p class="smallScreen">Try rotating your device</p>
<table>
<tr>
<th class="id">ID</th>
<th class="from">From</th>
<th class="to">To</th>
<th class="plat">Plat</th>
<th class="time arrsch">Sch</th>
<th class="time arrexp">Exp</th>
<th class="time depsch">Sch</th>
<th class="time depexp">Exp</th>
</tr>
<tr>
<th class="other" colspan="4" />
<th class="timepair" colspan="2">Arrival</th>
<th class="timepair" colspan="2">Departure</th>
</tr>
{#each services as service}
<tr
class="dataRow"
on:click={(event) => detail(event, service.rid, service.uid, service.trainid)}
on:keypress={(event) => detail(event, service.rid, service.uid, service.trainid)}
>
{#await classGenerator(service) then classes}
<!-- HEADCODE -->
<td class="id">{service.trainid}</td>
<!-- ORIGIN -->
<td class="loc from {classes.other}">{#await formatLocations(service.origin) then origin}{origin}{/await}</td>
<!-- DESTINATION -->
<td class="loc to {classes.other}">{#await formatLocations(service.destination) then dest}<span class="locName">{dest}</span>{/await}</td>
<!-- PLATFORM -->
<td class="plat {classes.other} {classes.plat}">{service.platform || '-'}</td>
<!-- SCHEDULED ARR -->
<td class="time schTime {classes.other}">{fmtTime(service?.sta) || '-'}</td>
<!-- EXPECTED/ACTUAL ARR -->
<td class="time {classes.other} {classes.arr}">{fmtTime(service.eta) || fmtTime(service.ata) || '-'}</td>
<!-- SCHEDULED DEP -->
<td class="time schTime {classes.other}">{fmtTime(service.std) || '-'}</td>
<!-- EXPECTED/ACTUAL DEP -->
<td class="time {classes.other} {classes.dep}">{fmtTime(service.etd) || fmtTime(service.atd) || '-'}</td>
{/await}
<tr>
<th class="id">ID</th>
<th class="from">From</th>
<th class="to">To</th>
<th class="plat">Plat</th>
<th class="time arrsch">Sch</th>
<th class="time arrexp">Exp</th>
<th class="time depsch">Sch</th>
<th class="time depexp">Exp</th>
</tr>
<tr>
<td colspan="1" />
<td class="tableTxt" colspan="7">
{#if service.destination?.[0] && service.destination[0].via}<span class="via">{service.destination[0].via}</span><br />{/if}
{tocMap.get(service.operatorCode.toLowerCase()) || service.operatorCode}
{#if service.length} | {service.length} carriages{/if}
{#if service.delayReason}
<br />
<span class="delayTxt">
<Reason type={'delay'} code={service.delayReason} />
</span>
{/if}
{#if service.cancelReason}
<br />
<span class="cancTxt">
<Reason type={'cancel'} code={service.cancelReason} />
</span>
{/if}
</td>
<th class="other" colspan="4" />
<th class="timepair" colspan="2">Arrival</th>
<th class="timepair" colspan="2">Departure</th>
</tr>
{/each}
{#each services as service}
<tr
class="dataRow"
on:click={(event) => detail(event, service.rid, service.uid, service.trainid)}
on:keypress={(event) => detail(event, service.rid, service.uid, service.trainid)}
>
{#await classGenerator(service) then classes}
<!-- HEADCODE -->
<td class="id">{service.trainid}</td>
<!-- ORIGIN -->
<td class="loc from {classes.other}">{#await formatLocations(service.origin) then origin}{origin}{/await}</td>
<!-- DESTINATION -->
<td class="loc to {classes.other}">{#await formatLocations(service.destination) then dest}<span class="locName">{dest}</span>{/await}</td>
<!-- PLATFORM -->
<td class="plat {classes.other} {classes.plat}">{service.platform || "-"}</td>
<!-- SCHEDULED ARR -->
<td class="time schTime {classes.other}">{fmtTime(service?.sta) || "-"}</td>
<!-- EXPECTED/ACTUAL ARR -->
<td class="time {classes.other} {classes.arr}">{fmtTime(service.eta) || fmtTime(service.ata) || "-"}</td>
<!-- SCHEDULED DEP -->
<td class="time schTime {classes.other}">{fmtTime(service.std) || "-"}</td>
<!-- EXPECTED/ACTUAL DEP -->
<td class="time {classes.other} {classes.dep}">{fmtTime(service.etd) || fmtTime(service.atd) || "-"}</td>
{/await}
</tr>
<tr>
<td colspan="1" />
<td class="tableTxt" colspan="7">
{#if service.destination?.[0] && service.destination[0].via}<span class="via">{service.destination[0].via}</span><br />{/if}
{tocMap.get(service.operatorCode.toLowerCase()) || service.operatorCode}
{#if service.length} | {service.length} carriages{/if}
{#if service.delayReason}
<br />
<span class="delayTxt">
<Reason type={"delay"} code={service.delayReason} />
</span>
{/if}
{#if service.cancelReason}
<br />
<span class="cancTxt">
<Reason type={"cancel"} code={service.cancelReason} />
</span>
{/if}
</td>
</tr>
{/each}
</table>
<style>
table {
table-layout: fixed;
width: 100%;
max-width: 875px;
margin: auto;
padding: 0px;
color: white;
}
th {
font-size: 12px;
margin: 0px;
}
.dataRow {
font-family: ubuntu, monospace;
vertical-align: top;
cursor: pointer;
font-size: 16px;
line-height: 1;
padding: 0;
margin: 0;
}
/* Table Columns */
.id {
width: 8%;
}
.from {
width: 14%;
}
.to {
width: 14%;
}
.plat {
width: 6%;
}
.time {
width: 9%;
}
td.id {
color: lightblue;
text-align: left;
padding-left: 2px;
}
td.from,
td.to {
color: yellow;
}
td.to {
text-align: right;
}
td.from,
th.from {
text-align: left;
}
td.time {
font-size: 15px;
vertical-align: top;
}
.tableTxt {
text-align: left;
padding-left: 2px;
color: var(--secondary-text-color);
vertical-align: top;
font-size: 12px;
padding-bottom: 10px;
}
.delayTxt {
color: var(--main-warning-color);
}
.cancTxt {
color: var(--main-alert-color);
}
.via {
color: yellow;
padding-left: 0px;
}
/* Handle small screens */
.smallScreen {
display: none;
margin: 20px;
}
@media screen and (max-width: 335px) {
th {
font-size: 10px;
}
.dataRow {
font-size: 12px;
}
td.time {
font-size: 12px;
}
.tableTxt {
font-size: 10px;
}
}
@media screen and (max-width: 279px) {
table {
display: none;
table-layout: fixed;
width: 100%;
max-width: 875px;
margin: auto;
padding: 0px;
color: white;
}
th {
font-size: 12px;
margin: 0px;
}
.smallScreen {
display: block;
}
}
/* Handle Large Screens */
@media screen and (min-width: 375px) {
.dataRow {
font-size: 18px;
font-family: ubuntu, monospace;
vertical-align: top;
cursor: pointer;
font-size: 16px;
line-height: 1;
padding: 0;
margin: 0;
}
/* Table Columns */
.id {
width: 8%;
}
.from {
width: 14%;
}
.to {
width: 14%;
}
.plat {
width: 6%;
}
.time {
width: 9%;
}
td.id {
color: lightblue;
text-align: left;
padding-left: 2px;
}
td.from,
td.to {
color: yellow;
}
td.to {
text-align: right;
}
td.from,
th.from {
text-align: left;
}
td.time {
font-size: 16px;
}
}
@media screen and (min-width: 450px) {
.dataRow {
font-size: 20px;
}
td.time {
font-size: 19px;
font-size: 15px;
vertical-align: top;
}
.tableTxt {
font-size: 14px;
text-align: left;
padding-left: 2px;
color: var(--secondary-text-color);
vertical-align: top;
font-size: 12px;
padding-bottom: 10px;
}
td.to,
td.from,
th.to,
th.from {
text-align: left;
.delayTxt {
color: var(--main-warning-color);
}
}
/* Conditional Classes */
.loc.canc,
.canc {
color: grey;
text-decoration: line-through;
opacity: 0.8;
}
.nonPass {
opacity: 0.4;
}
.late {
animation: pulse-late 1.5s linear infinite;
}
.canc.time {
animation: pulse-cancel 1.5s linear infinite;
}
.early {
animation: pulse-early 1.5s linear infinite;
}
/* Animation Definitions */
@keyframes pulse-late {
50% {
color: var(--main-warning-color);
.cancTxt {
color: var(--main-alert-color);
}
}
@keyframes pulse-cancel {
50% {
color: var(--main-alert-color);
.via {
color: yellow;
padding-left: 0px;
}
}
@keyframes pulse-early {
50% {
color: rgb(136, 164, 255);
/* Handle small screens */
.smallScreen {
display: none;
margin: 20px;
}
@media screen and (max-width: 335px) {
th {
font-size: 10px;
}
.dataRow {
font-size: 12px;
}
td.time {
font-size: 12px;
}
.tableTxt {
font-size: 10px;
}
}
@media screen and (max-width: 279px) {
table {
display: none;
}
.smallScreen {
display: block;
}
}
/* Handle Large Screens */
@media screen and (min-width: 375px) {
.dataRow {
font-size: 18px;
}
td.time {
font-size: 16px;
}
}
@media screen and (min-width: 450px) {
.dataRow {
font-size: 20px;
}
td.time {
font-size: 19px;
}
.tableTxt {
font-size: 14px;
}
td.to,
td.from,
th.to,
th.from {
text-align: left;
}
}
/* Conditional Classes */
.loc.canc,
.canc {
color: grey;
text-decoration: line-through;
opacity: 0.8;
}
.nonPass {
opacity: 0.4;
}
.late {
animation: pulse-late 1.5s linear infinite;
}
.canc.time {
animation: pulse-cancel 1.5s linear infinite;
}
.early {
animation: pulse-early 1.5s linear infinite;
}
/* Animation Definitions */
@keyframes pulse-late {
50% {
color: var(--main-warning-color);
}
}
@keyframes pulse-cancel {
50% {
color: var(--main-alert-color);
}
}
@keyframes pulse-early {
50% {
color: rgb(136, 164, 255);
}
}
}
</style>

View File

@ -1,286 +1,286 @@
<script>
import OverlayIsland from '$lib/islands/overlay-island.svelte';
import { fade } from 'svelte/transition';
import Reason from '$lib/raw-fetchers/reason.svelte';
import { uuid } from '$lib/stores/uuid';
import StylesToc from '$lib/train/styles-toc.svelte';
import { getApiUrl } from '$lib/scripts/upstream';
export let detail = {
uid: '',
rid: '',
headcode: '',
show: true
};
export let close;
function handleClick() {
close();
}
async function getTrain(rid) {
try {
console.log(`Requested Station: ${rid}`);
const url = `${getApiUrl()}/api/v2/live/train/rid/${rid}`;
const opt = {
method: 'GET',
headers: {
uuid: $uuid
}
};
const data = await fetch(url, opt);
return await data.json();
} catch (error) {
console.error('Error fetching data:', error);
}
}
async function parseDelay(location) {
let string, state;
if (location?.lateness) {
try {
const result = Math.floor(location.lateness / 60);
if (result === 0) {
(string = 'RT'), (state = '');
} else if (result < 0) {
(string = -result + 'E'), (state = 'early');
} else if (result > 0) {
(string = result + 'L'), (state = 'late');
}
} catch {
(string = ''), (state = '');
}
} else if (location.arrivalType === 'Delayed') {
(string = ''), (state = 'late');
} else {
(string = ''), (state = 'noreport');
}
return {
string: string,
state: state
import OverlayIsland from "$lib/islands/overlay-island.svelte";
import { fade } from "svelte/transition";
import Reason from "$lib/raw-fetchers/reason.svelte";
import { uuid } from "$lib/stores/uuid";
import StylesToc from "$lib/train/styles-toc.svelte";
import { getApiUrl } from "$lib/scripts/upstream";
export let detail = {
uid: "",
rid: "",
headcode: "",
show: true
};
}
export let close;
function parseTime(date) {
const parsedTime = date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
return parsedTime !== 'Invalid Date' ? parsedTime : null;
}
function handleClick() {
close();
}
function parseTimes(service) {
const sta = new Date(service.sta),
eta = new Date(service.eta),
ata = new Date(service.ata);
const std = new Date(service.std),
etd = new Date(service.etd),
atd = new Date(service.atd);
let parsedSta = parseTime(sta),
parsedEta = parseTime(eta),
parsedAta = parseTime(ata);
let parsedStd = parseTime(std),
parsedEtd = parseTime(etd),
parsedAtd = parseTime(atd);
if (service.isCancelled) {
(parsedEta = 'CANC'), (parsedEtd = 'CANC');
async function getTrain(rid) {
try {
console.log(`Requested Station: ${rid}`);
const url = `${getApiUrl()}/api/v2/live/train/rid/${rid}`;
const opt = {
method: "GET",
headers: {
uuid: $uuid
}
};
const data = await fetch(url, opt);
return await data.json();
} catch (error) {
console.error("Error fetching data:", error);
}
}
let times = {
sta: parsedSta || '-',
eata: parsedEta || parsedAta || '-',
aEst: parsedEta ? 'estimate' : '',
std: parsedStd || '-',
eatd: parsedEtd || parsedAtd || '-',
dEst: parsedEtd ? 'estimate' : ''
};
if (service.isCancelled) {
(parsedEta = 'CANC'), (parsedEtd = 'CANC');
(times.aEst = 'canc'), (times.dEst = 'canc');
async function parseDelay(location) {
let string, state;
if (location?.lateness) {
try {
const result = Math.floor(location.lateness / 60);
if (result === 0) {
(string = "RT"), (state = "");
} else if (result < 0) {
(string = -result + "E"), (state = "early");
} else if (result > 0) {
(string = result + "L"), (state = "late");
}
} catch {
(string = ""), (state = "");
}
} else if (location.arrivalType === "Delayed") {
(string = ""), (state = "late");
} else {
(string = ""), (state = "noreport");
}
return {
string: string,
state: state
};
}
function parseTime(date) {
const parsedTime = date.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
return parsedTime !== "Invalid Date" ? parsedTime : null;
}
function parseTimes(service) {
const sta = new Date(service.sta),
eta = new Date(service.eta),
ata = new Date(service.ata);
const std = new Date(service.std),
etd = new Date(service.etd),
atd = new Date(service.atd);
let parsedSta = parseTime(sta),
parsedEta = parseTime(eta),
parsedAta = parseTime(ata);
let parsedStd = parseTime(std),
parsedEtd = parseTime(etd),
parsedAtd = parseTime(atd);
if (service.isCancelled) {
(parsedEta = "CANC"), (parsedEtd = "CANC");
}
let times = {
sta: parsedSta || "-",
eata: parsedEta || parsedAta || "-",
aEst: parsedEta ? "estimate" : "",
std: parsedStd || "-",
eatd: parsedEtd || parsedAtd || "-",
dEst: parsedEtd ? "estimate" : ""
};
if (service.isCancelled) {
(parsedEta = "CANC"), (parsedEtd = "CANC");
(times.aEst = "canc"), (times.dEst = "canc");
}
return times;
}
return times;
}
</script>
<OverlayIsland>
<div id="detailBox">
<button type="button" id="closeService" on:click={handleClick}>X</button>
{#await getTrain(detail.rid)}
<h6>{detail.headcode}</h6>
<p in:fade id="loading">Loading Data...</p>
{:then train}
<h6><StylesToc toc={train.GetServiceDetailsResult.operatorCode} full={true} /> {detail.headcode}</h6>
<p>
Locations in grey are not scheduled stops
<br />
Times in <span class="estimate">yellow</span> are estimated times
</p>
{#if train.GetServiceDetailsResult.delayReason}
<p class="reason late">
<Reason type="delay" code={train.GetServiceDetailsResult.delayReason} />
</p>
{/if}
{#if train.GetServiceDetailsResult.cancelReason}
<p class="reason canc">
<Reason type="cancel" code={train.GetServiceDetailsResult.cancelReason} />
</p>
{/if}
<table id="detailTable">
<tr>
<th class="tableLocation">Loc.</th>
<th class="tablePlatform">Pl.</th>
<th class="tableTime">Sch</th>
<th class="tableTime">Est/<br />Act</th>
<th class="tableTime">Sch</th>
<th class="tableTime">Est/<br />Act</th>
<th class="tableDelay" />
</tr>
<tr>
<th colspan="2" />
<th colspan="2">Arrival</th>
<th colspan="2">Departure</th>
<th />
</tr>
{#each train.GetServiceDetailsResult.locations.location as location}
<tr>
<td class="location {location?.isPass === 'true' ? 'pass' : ''}">{location.tiploc}</td>
<td class={location?.isPass === 'true' ? 'pass' : ''}>{location.platform || ''}</td>
{#await parseTimes(location)}
<td />
<td />
<td />
<td />
{:then times}
<td class={location?.isPass === 'true' ? 'pass' : ''}>{times.sta}</td>
<td class="{location?.isPass === 'true' ? 'pass' : ''} {times.aEst}">{times.eata}</td>
<td class={location?.isPass === 'true' ? 'pass' : ''}>{times.std}</td>
<td class="{location?.isPass === 'true' ? 'pass' : ''} {times.dEst}">{times.eatd}</td>
{/await}
{#await parseDelay(location)}
<td>-</td>
{:then delay}
<td class={delay.state}>{delay.string}</td>
{/await}
</tr>
{/each}
</table>
{:catch}
<h6>Error loading data</h6>
{/await}
</div>
<div id="detailBox">
<button type="button" id="closeService" on:click={handleClick}>X</button>
{#await getTrain(detail.rid)}
<h6>{detail.headcode}</h6>
<p in:fade id="loading">Loading Data...</p>
{:then train}
<h6><StylesToc toc={train.GetServiceDetailsResult.operatorCode} full={true} /> {detail.headcode}</h6>
<p>
Locations in grey are not scheduled stops
<br />
Times in <span class="estimate">yellow</span> are estimated times
</p>
{#if train.GetServiceDetailsResult.delayReason}
<p class="reason late">
<Reason type="delay" code={train.GetServiceDetailsResult.delayReason} />
</p>
{/if}
{#if train.GetServiceDetailsResult.cancelReason}
<p class="reason canc">
<Reason type="cancel" code={train.GetServiceDetailsResult.cancelReason} />
</p>
{/if}
<table id="detailTable">
<tr>
<th class="tableLocation">Loc.</th>
<th class="tablePlatform">Pl.</th>
<th class="tableTime">Sch</th>
<th class="tableTime">Est/<br />Act</th>
<th class="tableTime">Sch</th>
<th class="tableTime">Est/<br />Act</th>
<th class="tableDelay" />
</tr>
<tr>
<th colspan="2" />
<th colspan="2">Arrival</th>
<th colspan="2">Departure</th>
<th />
</tr>
{#each train.GetServiceDetailsResult.locations.location as location}
<tr>
<td class="location {location?.isPass === 'true' ? 'pass' : ''}">{location.tiploc}</td>
<td class={location?.isPass === "true" ? "pass" : ""}>{location.platform || ""}</td>
{#await parseTimes(location)}
<td />
<td />
<td />
<td />
{:then times}
<td class={location?.isPass === "true" ? "pass" : ""}>{times.sta}</td>
<td class="{location?.isPass === 'true' ? 'pass' : ''} {times.aEst}">{times.eata}</td>
<td class={location?.isPass === "true" ? "pass" : ""}>{times.std}</td>
<td class="{location?.isPass === 'true' ? 'pass' : ''} {times.dEst}">{times.eatd}</td>
{/await}
{#await parseDelay(location)}
<td>-</td>
{:then delay}
<td class={delay.state}>{delay.string}</td>
{/await}
</tr>
{/each}
</table>
{:catch}
<h6>Error loading data</h6>
{/await}
</div>
</OverlayIsland>
<style>
#detailBox {
width: 100%;
min-height: 100px;
overflow-x: hidden;
overflow-y: auto;
}
h6 {
position: absolute;
top: -16px;
left: 20px;
font-size: 14px;
color: whitesmoke;
}
#loading {
color: white;
animation: pulse-early 2.5s linear infinite;
}
p {
margin-top: 45px;
margin-bottom: 0px;
}
p.reason {
margin-top: 5px;
}
#closeService {
position: absolute;
top: 10px;
right: 10px;
border: none;
border-radius: 60px;
width: 35px;
height: 35px;
background-color: var(--main-bg-color);
color: white;
font-weight: 700;
font-size: 14px;
}
#detailTable {
margin-top: 10px;
table-layout: fixed;
width: 100%;
margin-top: 12px;
margin-left: 0px;
margin-right: 0px;
padding: 0px;
color: white;
font-family: ubuntu, monospace;
font-size: 16px;
}
@media screen and (max-width: 338px) {
#detailBox {
width: 100%;
min-height: 100px;
overflow-x: hidden;
overflow-y: auto;
}
h6 {
position: absolute;
top: -16px;
left: 20px;
font-size: 14px;
color: whitesmoke;
}
#loading {
color: white;
animation: pulse-early 2.5s linear infinite;
}
p {
margin-top: 45px;
margin-bottom: 0px;
}
p.reason {
margin-top: 5px;
}
#closeService {
position: absolute;
top: 10px;
right: 10px;
border: none;
border-radius: 60px;
width: 35px;
height: 35px;
background-color: var(--main-bg-color);
color: white;
font-weight: 700;
font-size: 14px;
}
#detailTable {
font-size: 14px;
margin-top: 10px;
table-layout: fixed;
width: 100%;
margin-top: 12px;
margin-left: 0px;
margin-right: 0px;
padding: 0px;
color: white;
font-family: ubuntu, monospace;
font-size: 16px;
}
}
@media screen and (max-width: 301px) {
#detailTable {
font-size: 12px;
@media screen and (max-width: 338px) {
#detailTable {
font-size: 14px;
}
}
}
@media screen and (min-width: 469px) {
#detailTable {
font-size: 20px;
@media screen and (max-width: 301px) {
#detailTable {
font-size: 12px;
}
}
@media screen and (min-width: 469px) {
#detailTable {
font-size: 20px;
}
}
.tableLocation {
width: 18%;
}
td.location {
color: yellow;
}
.tablePlatform {
width: 9%;
}
.tableTime {
width: 14%;
}
.tableDelay {
width: 7%;
}
.estimate {
color: rgb(255, 255, 119);
}
.pass {
color: white !important;
opacity: 0.45;
}
.canc {
color: white;
animation: pulse-cancel 1.5s linear infinite;
}
}
.tableLocation {
width: 18%;
}
td.location {
color: yellow;
}
.tablePlatform {
width: 9%;
}
.tableTime {
width: 14%;
}
.tableDelay {
width: 7%;
}
.estimate {
color: rgb(255, 255, 119);
}
.pass {
color: white !important;
opacity: 0.45;
}
.canc {
color: white;
animation: pulse-cancel 1.5s linear infinite;
}
.early {
animation: pulse-early 1.5s linear infinite;
}
.late {
color: white;
animation: pulse-late 1.5s linear infinite;
}
@keyframes pulse-late {
50% {
color: var(--main-warning-color);
.early {
animation: pulse-early 1.5s linear infinite;
}
}
@keyframes pulse-cancel {
50% {
color: var(--main-alert-color);
.late {
color: white;
animation: pulse-late 1.5s linear infinite;
}
}
@keyframes pulse-early {
50% {
color: rgb(136, 164, 255);
@keyframes pulse-late {
50% {
color: var(--main-warning-color);
}
}
@keyframes pulse-cancel {
50% {
color: var(--main-alert-color);
}
}
@keyframes pulse-early {
50% {
color: rgb(136, 164, 255);
}
}
}
</style>

View File

@ -1,29 +1,29 @@
// Contains the details required to lookup train details
export interface Detail {
show: boolean;
headcode: string;
rid: string;
uid: string;
show: boolean;
headcode: string;
rid: string;
uid: string;
}
// Initiates/Resets a `Detail` interface
export function detailInit(): Detail {
const detail: Detail = {
show: false,
headcode: '',
rid: '',
uid: ''
};
return detail;
const detail: Detail = {
show: false,
headcode: "",
rid: "",
uid: ""
};
return detail;
}
// Initiates/Updates a `Detail` interface using the given values
export function defineDetail(rid: string, uid: string, tid: string) {
const detail: Detail = {
rid: rid,
uid: uid,
headcode: tid,
show: true
};
return detail;
const detail: Detail = {
rid: rid,
uid: uid,
headcode: tid,
show: true
};
return detail;
}

View File

@ -1,77 +1,77 @@
import { getApiUrl } from './scripts/upstream';
import { uuid } from './stores/uuid';
import { getApiUrl } from "./scripts/upstream";
import { uuid } from "./stores/uuid";
export interface libauthResponse {
uuidPresent?: boolean;
serverAuthCheck?: boolean;
uuidValue?: string;
serverAuthCheckResponseCode?: number;
uuidPresent?: boolean;
serverAuthCheck?: boolean;
uuidValue?: string;
serverAuthCheckResponseCode?: number;
}
interface uuidCheckRes {
uuidValue?: string;
uuidPresent?: boolean;
uuidValue?: string;
uuidPresent?: boolean;
}
export async function checkAuth(): Promise<libauthResponse> {
let result: libauthResponse = {};
const uuidCheck = await checkUuid();
result.uuidPresent = uuidCheck?.uuidPresent;
result.uuidValue = uuidCheck?.uuidValue;
let result: libauthResponse = {};
const uuidCheck = await checkUuid();
result.uuidPresent = uuidCheck?.uuidPresent;
result.uuidValue = uuidCheck?.uuidValue;
const serverCheck = await checkServerAuth(result.uuidValue || '');
result.serverAuthCheck = serverCheck.authOk;
result.serverAuthCheckResponseCode = serverCheck.status;
const serverCheck = await checkServerAuth(result.uuidValue || "");
result.serverAuthCheck = serverCheck.authOk;
result.serverAuthCheckResponseCode = serverCheck.status;
return result;
return result;
}
async function checkUuid(): Promise<uuidCheckRes> {
let uuid_value: string = '';
const unsubscribe = uuid.subscribe((value) => {
uuid_value = value;
});
let res: uuidCheckRes = {
uuidValue: uuid_value
};
console.log('uuid-value is: ', uuid_value);
if (uuid_value && uuid_value != 'null') {
res = {
uuidPresent: true,
uuidValue: uuid_value
let uuid_value: string = "";
const unsubscribe = uuid.subscribe((value) => {
uuid_value = value;
});
let res: uuidCheckRes = {
uuidValue: uuid_value
};
} else {
res = {
uuidPresent: false,
uuidValue: uuid_value
};
}
unsubscribe();
return res;
console.log("uuid-value is: ", uuid_value);
if (uuid_value && uuid_value != "null") {
res = {
uuidPresent: true,
uuidValue: uuid_value
};
} else {
res = {
uuidPresent: false,
uuidValue: uuid_value
};
}
unsubscribe();
return res;
}
async function checkServerAuth(uuidString: string) {
const url = `${getApiUrl()}/api/v2/user/checkAuth`;
const options = {
method: 'GET',
headers: {
uuid: uuidString
const url = `${getApiUrl()}/api/v2/user/checkAuth`;
const options = {
method: "GET",
headers: {
uuid: uuidString
}
};
const res = await fetch(url, options);
let ok: boolean;
if (res.status !== 401) {
ok = true;
} else {
ok = false;
}
};
const res = await fetch(url, options);
let ok: boolean;
if (res.status !== 401) {
ok = true;
} else {
ok = false;
}
return {
authOk: ok,
status: res.status
};
return {
authOk: ok,
status: res.status
};
}
export async function logout(): Promise<boolean> {
uuid.set(null);
return true;
uuid.set(null);
return true;
}

View File

@ -1,67 +1,67 @@
/* FONTS */
@font-face {
font-family: 'firamono';
src: url('/font/firamono/firamono-regular.woff2') format('woff2'), url('/font/firamono/firamono-regular.woff') format('woff'),
url('/font/firamono/firamono-regular.ttf') format('truetype');
font-weight: normal;
font-style: normal;
font-family: "firamono";
src: url("/font/firamono/firamono-regular.woff2") format("woff2"), url("/font/firamono/firamono-regular.woff") format("woff"),
url("/font/firamono/firamono-regular.ttf") format("truetype");
font-weight: normal;
font-style: normal;
}
@font-face {
font-family: 'firamono';
src: url('/font/firamono/firamono-500.woff2') format('woff2'), url('/font/firamono/firamono-500.woff') format('woff'), url('/font/firamono/firamono-500.ttf') format('truetype');
font-weight: 500;
font-style: normal;
font-family: "firamono";
src: url("/font/firamono/firamono-500.woff2") format("woff2"), url("/font/firamono/firamono-500.woff") format("woff"), url("/font/firamono/firamono-500.ttf") format("truetype");
font-weight: 500;
font-style: normal;
}
@font-face {
font-family: 'urwgothic';
src: url('/font/urwgothic/urwgothic.woff2') format('woff2'), url('/font/urwgothic/urwgothic.woff') format('woff'), url('/font/urwgothic/urwgothic.ttf') format('truetype');
font-weight: normal;
font-style: normal;
font-family: "urwgothic";
src: url("/font/urwgothic/urwgothic.woff2") format("woff2"), url("/font/urwgothic/urwgothic.woff") format("woff"), url("/font/urwgothic/urwgothic.ttf") format("truetype");
font-weight: normal;
font-style: normal;
}
@font-face {
font-family: 'urwgothic';
src: url('/font/urwgothic/urwgothicDemi.woff2') format('woff2'), url('/font/urwgothic/urwgothicDemi.woff') format('woff'),
url('/font/urwgothic/urwgothicDemi.ttf') format('truetype');
font-weight: 900;
font-style: normal;
font-family: "urwgothic";
src: url("/font/urwgothic/urwgothicDemi.woff2") format("woff2"), url("/font/urwgothic/urwgothicDemi.woff") format("woff"),
url("/font/urwgothic/urwgothicDemi.ttf") format("truetype");
font-weight: 900;
font-style: normal;
}
@font-face {
font-family: 'ubuntu';
src: url('/font/ubuntumono/ubuntumono-regular.woff2') format('woff2'), url('/font/ubuntumono/ubuntumono-regular.woff') format('woff'),
url('/font/ubuntumono/ubuntumono-regular.ttf') format('truetype');
font-weight: 400;
font-style: normal;
font-family: "ubuntu";
src: url("/font/ubuntumono/ubuntumono-regular.woff2") format("woff2"), url("/font/ubuntumono/ubuntumono-regular.woff") format("woff"),
url("/font/ubuntumono/ubuntumono-regular.ttf") format("truetype");
font-weight: 400;
font-style: normal;
}
html {
width: 100%;
height: 100%;
width: 100%;
height: 100%;
}
body {
background-color: var(--main-bg-color);
background-image: radial-gradient(var(--second-bg-color), var(--main-bg-color));
color: var(--main-text-color);
font-family: urwgothic, sans-serif;
text-align: center;
margin: auto;
width: 100%;
padding-bottom: 65px;
background-color: var(--main-bg-color);
background-image: radial-gradient(var(--second-bg-color), var(--main-bg-color));
color: var(--main-text-color);
font-family: urwgothic, sans-serif;
text-align: center;
margin: auto;
width: 100%;
padding-bottom: 65px;
}
a {
color: var(--main-link-color);
color: var(--main-link-color);
}
button:hover {
cursor: pointer;
cursor: pointer;
}
button {
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
-webkit-tap-highlight-color: transparent;
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
-webkit-tap-highlight-color: transparent;
}

View File

@ -1,25 +1,25 @@
<script lang="ts">
import { logout } from '$lib/libauth';
import { logout } from "$lib/libauth";
async function logoutAction() {
await logout();
location.reload();
}
async function logoutAction() {
await logout();
location.reload();
}
</script>
<button class="logout" type="button" on:click={logoutAction}>Logout</button>
<style>
.logout {
border: none;
background-color: var(--island-button-color);
color: var(--island-link-color);
width: 35%;
border-radius: 50px;
font-size: 20px;
min-width: 90px;
margin: 30px;
height: 48px;
box-shadow: var(--box-shadow);
}
.logout {
border: none;
background-color: var(--island-button-color);
color: var(--island-link-color);
width: 35%;
border-radius: 50px;
font-size: 20px;
min-width: 90px;
margin: 30px;
height: 48px;
box-shadow: var(--box-shadow);
}
</style>

View File

@ -1,32 +1,32 @@
<div id="container">
<p id="tick">&#10004;</p>
<p>Done</p>
<p id="tick">&#10004;</p>
<p>Done</p>
</div>
<style>
#tick {
font-size: 45px;
margin: 0;
padding: 0;
}
#container {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
margin: auto;
background-color: var(--overlay-island-bg-color);
border-radius: 15px;
padding: 20px;
padding-bottom: 1px;
min-width: 90px;
max-width: 90px;
box-shadow: var(--box-shadow);
}
p {
padding-top: 0px;
font-weight: bolder;
overflow-wrap: normal;
color: var(--island-text-color);
}
#tick {
font-size: 45px;
margin: 0;
padding: 0;
}
#container {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
margin: auto;
background-color: var(--overlay-island-bg-color);
border-radius: 15px;
padding: 20px;
padding-bottom: 1px;
min-width: 90px;
max-width: 90px;
box-shadow: var(--box-shadow);
}
p {
padding-top: 0px;
font-weight: bolder;
overflow-wrap: normal;
color: var(--island-text-color);
}
</style>

View File

@ -1,56 +1,56 @@
<script>
export let title = 'title';
export let title = "title";
</script>
<div class="headerBar">
<a href="/">
<picture>
<source srcset="/images/logo/wide_logo.svg" type="image/svg+xml" />
<img src="/images/logo/wide_logo_200.png" alt="OwlBoard Logo" />
</picture>
</a>
<header>{title}</header>
<a href="/">
<picture>
<source srcset="/images/logo/wide_logo.svg" type="image/svg+xml" />
<img src="/images/logo/wide_logo_200.png" alt="OwlBoard Logo" />
</picture>
</a>
<header>{title}</header>
</div>
<div class="headerBlock">
<!-- This exists to prevent the headerBar overlapping anything below it -->
<!-- This exists to prevent the headerBar overlapping anything below it -->
</div>
<style>
.headerBar {
background: var(--island-bg-solid);
color: var(--main-header-color);
position: fixed;
top: 0;
left: 0;
width: 100%;
margin: 0;
padding: 0;
height: 50px;
z-index: 20;
box-shadow: 0 3px 30px var(--box-shadow-color);
}
.headerBar {
background: var(--island-bg-solid);
color: var(--main-header-color);
position: fixed;
top: 0;
left: 0;
width: 100%;
margin: 0;
padding: 0;
height: 50px;
z-index: 20;
box-shadow: 0 3px 30px var(--box-shadow-color);
}
img {
position: absolute;
height: 40px;
right: 8px;
top: 5px;
}
img {
position: absolute;
height: 40px;
right: 8px;
top: 5px;
}
header {
position: absolute;
left: 0px;
top: 3px;
font-family: urwgothic, sans-serif;
font-weight: 600;
margin-top: 7px;
margin-left: 20px;
font-size: 15pt;
}
header {
position: absolute;
left: 0px;
top: 3px;
font-family: urwgothic, sans-serif;
font-weight: 600;
margin-top: 7px;
margin-left: 20px;
font-size: 15pt;
}
.headerBlock {
height: 50px;
margin: 0;
padding: 0;
}
.headerBlock {
height: 50px;
margin: 0;
padding: 0;
}
</style>

View File

@ -1,16 +1,16 @@
<p id="load">Loading...</p>
<style>
#load {
margin-top: 5px;
font-size: 18px;
font-weight: 600;
color: white;
animation: pulse-loading 2.5s linear infinite;
}
@keyframes pulse-loading {
50% {
color: rgb(136, 164, 255);
#load {
margin-top: 5px;
font-size: 18px;
font-weight: 600;
color: white;
animation: pulse-loading 2.5s linear infinite;
}
@keyframes pulse-loading {
50% {
color: rgb(136, 164, 255);
}
}
}
</style>

View File

@ -1,54 +1,54 @@
<script>
import { fade } from 'svelte/transition';
import { fade } from "svelte/transition";
</script>
<div id="container" in:fade={{ delay: 150, duration: 250 }} out:fade={{ duration: 250 }}>
<div class="spinner" />
<p>Loading...</p>
<div class="spinner" />
<p>Loading...</p>
</div>
<style>
@keyframes spinner {
0% {
transform: translate3d(-50%, -50%, 0) rotate(0);
@keyframes spinner {
0% {
transform: translate3d(-50%, -50%, 0) rotate(0);
}
100% {
transform: translate3d(-50%, -50%, 0) rotate(360deg);
}
}
100% {
transform: translate3d(-50%, -50%, 0) rotate(360deg);
.spinner::before {
animation: 1.5s linear infinite spinner;
animation-play-state: inherit;
border: solid 5px var(--overlay-island-bg-color);
border-bottom-color: white;
border-radius: 50%;
content: "";
height: 40px;
width: 40px;
position: absolute;
top: 30%;
margin: auto;
transform: translate3d(-50%, -50%, 0);
will-change: transform;
}
#container {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
margin: auto;
background-color: var(--overlay-island-bg-color);
border-radius: 15px;
padding: 20px;
padding-bottom: 1px;
min-width: 90px;
max-width: 90px;
box-shadow: var(--box-shadow);
}
p {
padding-top: 50px;
font-weight: bolder;
overflow-wrap: normal;
color: var(--island-text-color);
}
}
.spinner::before {
animation: 1.5s linear infinite spinner;
animation-play-state: inherit;
border: solid 5px var(--overlay-island-bg-color);
border-bottom-color: white;
border-radius: 50%;
content: '';
height: 40px;
width: 40px;
position: absolute;
top: 30%;
margin: auto;
transform: translate3d(-50%, -50%, 0);
will-change: transform;
}
#container {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
margin: auto;
background-color: var(--overlay-island-bg-color);
border-radius: 15px;
padding: 20px;
padding-bottom: 1px;
min-width: 90px;
max-width: 90px;
box-shadow: var(--box-shadow);
}
p {
padding-top: 50px;
font-weight: bolder;
overflow-wrap: normal;
color: var(--island-text-color);
}
</style>

View File

@ -1,93 +1,93 @@
<script>
const links = [
{
title: 'Home',
path: '/',
icon: IconHome
}
];
import { page } from '$app/stores';
import { IconHome } from '@tabler/icons-svelte';
const links = [
{
title: "Home",
path: "/",
icon: IconHome
}
];
import { page } from "$app/stores";
import { IconHome } from "@tabler/icons-svelte";
</script>
<footer>
{#each links as item}
<a class="footerLink" href={item.path} class:active={$page.url.pathname == item.path}>
<svelte:component this={item.icon} />
<br />
<span>{item.title}</span>
</a>
{/each}
<div class="data-source">
<a href="https://nationalrail.co.uk" target="_blank">
<picture>
<source srcset="/images/nre/nre-powered_200w.jxl" type="image/jxl" />
<source srcset="/images/nre/nre-powered_200w.webp" type="image/webp" />
<img id="nre-logo" src="/images/nre/nre-powered_200w.png" alt="Data sourced from National Rail and others" />
</picture>
</a>
</div>
{#each links as item}
<a class="footerLink" href={item.path} class:active={$page.url.pathname == item.path}>
<svelte:component this={item.icon} />
<br />
<span>{item.title}</span>
</a>
{/each}
<div class="data-source">
<a href="https://nationalrail.co.uk" target="_blank">
<picture>
<source srcset="/images/nre/nre-powered_200w.jxl" type="image/jxl" />
<source srcset="/images/nre/nre-powered_200w.webp" type="image/webp" />
<img id="nre-logo" src="/images/nre/nre-powered_200w.png" alt="Data sourced from National Rail and others" />
</picture>
</a>
</div>
</footer>
<style>
footer {
position: fixed;
display: flex;
bottom: 0;
left: 0;
width: 100%;
height: 50px;
background-color: var(--island-bg-solid);
}
footer {
position: fixed;
display: flex;
bottom: 0;
left: 0;
width: 100%;
height: 50px;
background-color: var(--island-bg-solid);
}
.footerLink {
width: 30%;
height: 100%;
background-color: var(--island-bg-solid);
border-style: solid;
border-width: 1px;
border-top: none;
border-bottom: none;
border-color: var(--box-shadow-color);
color: var(--main-text-color);
text-decoration: double;
font-weight: 600;
}
.footerLink {
width: 30%;
height: 100%;
background-color: var(--island-bg-solid);
border-style: solid;
border-width: 1px;
border-top: none;
border-bottom: none;
border-color: var(--box-shadow-color);
color: var(--main-text-color);
text-decoration: double;
font-weight: 600;
}
footer a.active {
background-color: transparent;
}
.data-source {
flex-grow: 2;
background: rgb(255, 255, 255);
}
#nre-logo {
width: 150px;
height: auto;
margin: auto;
margin-top: 13px;
}
@media only screen and (min-width: 475px) {
footer a.active {
background-color: transparent;
}
.data-source {
background: rgb(255, 255, 255);
background: linear-gradient(90deg, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 1) 40%);
flex-grow: 2;
background: rgb(255, 255, 255);
}
#nre-logo {
position: absolute;
right: 0;
right: 20px;
width: 150px;
height: auto;
margin: auto;
margin-top: 13px;
}
@media only screen and (min-width: 475px) {
.data-source {
background: rgb(255, 255, 255);
background: linear-gradient(90deg, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 1) 40%);
}
#nre-logo {
position: absolute;
right: 0;
right: 20px;
}
}
img {
height: 20px;
width: 20px;
margin: 0;
margin-top: 3px;
padding: 0;
}
span {
margin: 0;
margin-bottom: 3px;
padding: 0;
}
}
img {
height: 20px;
width: 20px;
margin: 0;
margin-top: 3px;
padding: 0;
}
span {
margin: 0;
margin-bottom: 3px;
padding: 0;
}
</style>

View File

@ -1,69 +1,69 @@
<script>
const links = [
{
title: 'Home',
path: '/',
icon: IconHome
},
{
title: 'PIS Finder',
path: '/pis/',
icon: IconDialpad
},
{
title: 'Menu',
path: '/more/',
icon: IconMenu2
}
];
import { page } from '$app/stores';
import { IconHome, IconMenu2, IconDialpad } from '@tabler/icons-svelte';
const links = [
{
title: "Home",
path: "/",
icon: IconHome
},
{
title: "PIS Finder",
path: "/pis/",
icon: IconDialpad
},
{
title: "Menu",
path: "/more/",
icon: IconMenu2
}
];
import { page } from "$app/stores";
import { IconHome, IconMenu2, IconDialpad } from "@tabler/icons-svelte";
</script>
<footer>
{#each links as item}
<a href={item.path} class:active={$page.url.pathname == item.path || $page.url.pathname == item.path + '/'}>
<svelte:component this={item.icon} />
<br />
<span>{item.title}</span>
</a>
{/each}
{#each links as item}
<a href={item.path} class:active={$page.url.pathname == item.path || $page.url.pathname == item.path + "/"}>
<svelte:component this={item.icon} />
<br />
<span>{item.title}</span>
</a>
{/each}
</footer>
<style>
footer {
position: fixed;
display: flex;
bottom: 0;
left: 0;
width: 100%;
height: 50px;
background-color: rgb(54, 54, 54);
box-shadow: 0 -2px 30px rgba(0, 0, 0, 0.19);
}
footer {
position: fixed;
display: flex;
bottom: 0;
left: 0;
width: 100%;
height: 50px;
background-color: rgb(54, 54, 54);
box-shadow: 0 -2px 30px rgba(0, 0, 0, 0.19);
}
footer a {
flex: 12;
width: 30%;
height: 100%;
background-color: var(--island-bg-solid);
border-style: solid;
border-width: 1px;
border-top: none;
border-bottom: none;
border-color: var(--box-shadow-color);
text-decoration: double;
color: var(--main-text-color);
font-weight: 600;
}
footer a {
flex: 12;
width: 30%;
height: 100%;
background-color: var(--island-bg-solid);
border-style: solid;
border-width: 1px;
border-top: none;
border-bottom: none;
border-color: var(--box-shadow-color);
text-decoration: double;
color: var(--main-text-color);
font-weight: 600;
}
footer a.active {
background-color: transparent;
}
footer a.active {
background-color: transparent;
}
span {
margin: 0;
margin-bottom: 3px;
padding: 0;
}
span {
margin: 0;
margin-bottom: 3px;
padding: 0;
}
</style>

View File

@ -1,106 +1,106 @@
<script lang="ts">
import { welcome } from '$lib/stores/welcome';
import { fade } from 'svelte/transition';
import { version } from '$lib/stores/version';
import { welcome } from "$lib/stores/welcome";
import { fade } from "svelte/transition";
import { version } from "$lib/stores/version";
let pageNum: number = 0;
let pageNum: number = 0;
function pageUp() {
pageNum++;
console.log(`Welcome page: ${pageNum}`);
}
function pageUp() {
pageNum++;
console.log(`Welcome page: ${pageNum}`);
}
function pageDn() {
pageNum--;
console.log(`Welcome page: ${pageNum}`);
}
function pageDn() {
pageNum--;
console.log(`Welcome page: ${pageNum}`);
}
function close() {
welcome.set(version);
}
function close() {
welcome.set(version);
}
const pageText: string[] = [
'<h3>PIS Codes</h3>' +
'<p>An effort has been made to support PIS codes accross the GWR network</p>' +
'<p>The vast majority of codes across West, LTV, HEx and Sleeper services are now available.</p>' +
'<p>The easiest way to find a PIS code is to type your headcode into the homepage, then select your service</p>',
'<h3>Resgistration Update</h3>' +
'<p>The registration issue has been fixed and registrations are now open.</p>' +
'<p>Headcode and PIS Lookups will still be possible without registering but only for a limited time.</p>' +
'<p>You will receive further warning before mandatory registration is re-enabled.</p>'
];
const pageText: string[] = [
"<h3>PIS Codes</h3>" +
"<p>An effort has been made to support PIS codes accross the GWR network</p>" +
"<p>The vast majority of codes across West, LTV, HEx and Sleeper services are now available.</p>" +
"<p>The easiest way to find a PIS code is to type your headcode into the homepage, then select your service</p>",
"<h3>Resgistration Update</h3>" +
"<p>The registration issue has been fixed and registrations are now open.</p>" +
"<p>Headcode and PIS Lookups will still be possible without registering but only for a limited time.</p>" +
"<p>You will receive further warning before mandatory registration is re-enabled.</p>"
];
</script>
<div id="popup" in:fade={{ delay: 500, duration: 300 }} out:fade={{ duration: 300 }}>
<h2>What's new in OwlBoard {version}</h2>
{#key pageNum}
<div in:fade={{ delay: 300 }} out:fade={{ duration: 200 }}>
{@html pageText[pageNum] || "You won't see this welcome message again"}
</div>
{/key}
{#if pageNum >= pageText.length - 1}
<button in:fade={{ delay: 350, duration: 250 }} out:fade={{ duration: 250 }} class="navButton" id="buttonCentre" type="button" on:click={close}>X</button>
{/if}
{#if pageNum > 0 && pageNum < pageText.length}
<button in:fade={{ delay: 350, duration: 250 }} out:fade={{ duration: 250 }} class="navButton" id="buttonLeft" type="button" on:click={pageDn}>&lt;</button>
{/if}
{#if pageNum < pageText.length - 1}
<button in:fade={{ delay: 350, duration: 250 }} out:fade={{ duration: 250 }} class="navButton" id="buttonRight" type="button" on:click={pageUp}>&gt;</button>
{/if}
<h2>What's new in OwlBoard {version}</h2>
{#key pageNum}
<div in:fade={{ delay: 300 }} out:fade={{ duration: 200 }}>
{@html pageText[pageNum] || "You won't see this welcome message again"}
</div>
{/key}
{#if pageNum >= pageText.length - 1}
<button in:fade={{ delay: 350, duration: 250 }} out:fade={{ duration: 250 }} class="navButton" id="buttonCentre" type="button" on:click={close}>X</button>
{/if}
{#if pageNum > 0 && pageNum < pageText.length}
<button in:fade={{ delay: 350, duration: 250 }} out:fade={{ duration: 250 }} class="navButton" id="buttonLeft" type="button" on:click={pageDn}>&lt;</button>
{/if}
{#if pageNum < pageText.length - 1}
<button in:fade={{ delay: 350, duration: 250 }} out:fade={{ duration: 250 }} class="navButton" id="buttonRight" type="button" on:click={pageUp}>&gt;</button>
{/if}
</div>
<style>
#popup {
position: fixed;
top: 50px;
left: 50%;
transform: translateX(-50%);
width: 85%;
height: 75vh;
max-height: 600px;
overflow-y: auto;
max-width: 400px;
margin: auto;
margin-top: 25px;
padding: 10px;
background-color: var(--island-bg-solid);
border-radius: 10px;
z-index: 2500;
box-shadow: 5px 5px 10px var(--box-shadow-color);
}
#popup {
position: fixed;
top: 50px;
left: 50%;
transform: translateX(-50%);
width: 85%;
height: 75vh;
max-height: 600px;
overflow-y: auto;
max-width: 400px;
margin: auto;
margin-top: 25px;
padding: 10px;
background-color: var(--island-bg-solid);
border-radius: 10px;
z-index: 2500;
box-shadow: 5px 5px 10px var(--box-shadow-color);
}
.navButton {
border-radius: 50px;
border: none;
color: var(--island-link-color);
background-color: var(--island-button-color);
width: 50px;
height: 50px;
font-size: 20px;
font-weight: 600;
bottom: 50px;
}
.navButton {
border-radius: 50px;
border: none;
color: var(--island-link-color);
background-color: var(--island-button-color);
width: 50px;
height: 50px;
font-size: 20px;
font-weight: 600;
bottom: 50px;
}
#buttonLeft {
position: absolute;
margin: auto;
left: 50px;
}
#buttonLeft {
position: absolute;
margin: auto;
left: 50px;
}
#buttonCentre {
position: absolute;
margin: auto;
left: 50%;
transform: translateX(-50%);
}
#buttonCentre {
position: absolute;
margin: auto;
left: 50%;
transform: translateX(-50%);
}
#buttonRight {
position: absolute;
margin: auto;
right: 50px;
}
#buttonRight {
position: absolute;
margin: auto;
right: 50px;
}
div {
color: var(--island-text-color);
}
div {
color: var(--island-text-color);
}
</style>

View File

@ -1,49 +1,49 @@
<script>
import { getApiUrl } from '$lib/scripts/upstream';
import { uuid } from '$lib/stores/uuid';
import { getApiUrl } from "$lib/scripts/upstream";
import { uuid } from "$lib/stores/uuid";
export let code = '';
export let type = '';
export let code = "";
export let type = "";
async function getDelay(code = '') {
console.log(`Fetching delay reason ${code}`);
const data = await getReason(code);
return data[0].lateReason || 'This train has been delayed';
}
async function getDelay(code = "") {
console.log(`Fetching delay reason ${code}`);
const data = await getReason(code);
return data[0].lateReason || "This train has been delayed";
}
async function getCancel(code = '') {
console.log(`Fetching cancel reason ${code}`);
const data = await getReason(code);
return data[0].cancReason || 'This train has been cancelled';
}
async function getCancel(code = "") {
console.log(`Fetching cancel reason ${code}`);
const data = await getReason(code);
return data[0].cancReason || "This train has been cancelled";
}
async function getReason(code = '') {
const url = `${getApiUrl()}/api/v2/ref/reasonCode/${code}`;
const options = {
method: 'GET',
headers: {
uuid: $uuid
}
};
const res = await fetch(url, options);
return await res.json();
}
async function getReason(code = "") {
const url = `${getApiUrl()}/api/v2/ref/reasonCode/${code}`;
const options = {
method: "GET",
headers: {
uuid: $uuid
}
};
const res = await fetch(url, options);
return await res.json();
}
</script>
{#if type === 'cancel'}
{#await getCancel(code)}
This train has been cancelled
{:then reason}
{reason}
{:catch}
This train has been cancelled
{/await}
{:else if type === 'delay'}
{#await getDelay(code)}
This train has been delayed
{:then reason}
{reason}
{:catch}
This train has been delayed
{/await}
{#if type === "cancel"}
{#await getCancel(code)}
This train has been cancelled
{:then reason}
{reason}
{:catch}
This train has been cancelled
{/await}
{:else if type === "delay"}
{#await getDelay(code)}
This train has been delayed
{:then reason}
{reason}
{:catch}
This train has been delayed
{/await}
{/if}

View File

@ -1,51 +1,51 @@
import { dev } from '$app/environment';
import { uuid } from '$lib/stores/uuid';
import type { Unsubscriber } from 'svelte/store';
import { dev } from "$app/environment";
import { uuid } from "$lib/stores/uuid";
import type { Unsubscriber } from "svelte/store";
function getUrlString(): string {
if (dev) {
const testUrl: string = 'http://localhost:8460';
console.info('DEVMODE active, using testing URL: ', testUrl);
return testUrl;
} else {
const currentUrl: string = `https://${window.location.host}`;
return currentUrl;
}
if (dev) {
const testUrl: string = "http://localhost:8460";
console.info("DEVMODE active, using testing URL: ", testUrl);
return testUrl;
} else {
const currentUrl: string = `https://${window.location.host}`;
return currentUrl;
}
}
export async function apiGet(path: string): Promise<any> {
let uuidString: string = '';
let unsubscribe: Unsubscriber;
try {
unsubscribe = uuid.subscribe((value) => {
uuidString = value;
});
} catch (err) {
throw new Error('Unable to read UUID');
}
const options = {
method: 'GET',
headers: {
uuid: uuidString
let uuidString: string = "";
let unsubscribe: Unsubscriber;
try {
unsubscribe = uuid.subscribe((value) => {
uuidString = value;
});
} catch (err) {
throw new Error("Unable to read UUID");
}
};
try {
const res = await fetch(getUrlString() + path, options);
const options = {
method: "GET",
headers: {
uuid: uuidString
}
};
if (!res.ok) {
throw new Error('Network response not ok');
try {
const res = await fetch(getUrlString() + path, options);
if (!res.ok) {
throw new Error("Network response not ok");
}
const contentType = res.headers.get("content-type");
if (!contentType || !contentType.includes("application/json")) {
throw new Error("Invalid response. Require JSON.");
}
return await res.json();
} catch (err) {
console.error("Error fetching data:", err);
throw err;
} finally {
unsubscribe();
}
const contentType = res.headers.get('content-type');
if (!contentType || !contentType.includes('application/json')) {
throw new Error('Invalid response. Require JSON.');
}
return await res.json();
} catch (err) {
console.error('Error fetching data:', err);
throw err;
} finally {
unsubscribe();
}
}

View File

@ -1,12 +1,12 @@
import { dev } from '$app/environment';
import { dev } from "$app/environment";
const testUrl: string = 'http://localhost:8460';
const prodUrl: string = 'https://owlboard.info';
const testUrl: string = "http://localhost:8460";
const prodUrl: string = "https://owlboard.info";
export function getApiUrl() {
if (dev) {
console.info('DEVMODE active, using testing URL: ', testUrl);
return testUrl;
}
return prodUrl;
if (dev) {
console.info("DEVMODE active, using testing URL: ", testUrl);
return testUrl;
}
return prodUrl;
}

View File

@ -1,25 +1,25 @@
import { writable, type Writable } from 'svelte/store';
import { browser } from '$app/environment';
import { writable, type Writable } from "svelte/store";
import { browser } from "$app/environment";
export const ql = writable(fromLocalStorage('ql', []));
toLocalStorage(ql, 'ql');
export const ql = writable(fromLocalStorage("ql", []));
toLocalStorage(ql, "ql");
function fromLocalStorage(storageKey: string, fallback: string[]): string[] {
if (browser) {
const storedValue = localStorage.getItem(storageKey);
if (storedValue !== 'undefined' && storedValue !== null) {
return typeof fallback === 'object' ? JSON.parse(storedValue) : storedValue;
if (browser) {
const storedValue = localStorage.getItem(storageKey);
if (storedValue !== "undefined" && storedValue !== null) {
return typeof fallback === "object" ? JSON.parse(storedValue) : storedValue;
}
}
}
return fallback;
return fallback;
}
function toLocalStorage(store: Writable<string[]>, storageKey: string) {
if (browser) {
store.subscribe((value) => {
let storageValue = typeof value === 'object' ? JSON.stringify(value.sort()) : value;
if (browser) {
store.subscribe((value) => {
let storageValue = typeof value === "object" ? JSON.stringify(value.sort()) : value;
localStorage.setItem(storageKey, storageValue);
});
}
localStorage.setItem(storageKey, storageValue);
});
}
}

View File

@ -1,44 +1,44 @@
export const tocs = new Map<string, string>([
['gw', 'Great Western Railway'],
['sw', 'South Western Railway'],
['il', 'Island Line'],
['nt', 'Northern'],
['aw', 'Trafnidiaeth Cymru'],
['cc', 'c2c'],
['cs', 'Caledonian Sleeper'],
['ch', 'Chiltern Railways'],
['xc', 'CrossCountry'],
['em', 'East Midlands Railway'],
['es', 'Eurostar'],
['ht', 'Hull Trains'],
['tl', 'Thameslink'],
['gc', 'Grand Central'],
['gx', 'Gatwick Express'],
['hx', 'Heathrow Express'],
['ls', 'Locomotive Services Limited'],
['me', 'Merseyrail'],
['lr', 'Network Rail OTM'],
['xr', 'TfL Elizabeth Line'],
['se', 'Southeastern'],
['sn', 'Southern'],
['le', 'Greater Anglia'],
['ga', 'Greater Anglia'],
['lm', 'West Midlands Railway (LM)'],
['sr', 'ScotRail'],
['gn', 'Great Northern'],
['lt', 'TfL London Underground'],
['lo', 'TfL London Overground'],
['sj', 'SuperTram (Sheffield)'],
['tp', 'TransPennine Express'],
['vt', 'Avanti West Coast'],
['gr', 'LNER'],
['wr', 'West Coast Railway'],
['ty', 'Vintage Trains'],
['tw', 'Nexus (Tyne & Wear Metro)'],
['ld', 'Lumo'],
['so', 'Rail Adventure'],
['ln', 'Grand Union Trains'],
['zz', 'Freight/Charter Company'],
['wm', 'West Midlands Railway (WM)'],
['uk', 'Unknown Operator']
["gw", "Great Western Railway"],
["sw", "South Western Railway"],
["il", "Island Line"],
["nt", "Northern"],
["aw", "Trafnidiaeth Cymru"],
["cc", "c2c"],
["cs", "Caledonian Sleeper"],
["ch", "Chiltern Railways"],
["xc", "CrossCountry"],
["em", "East Midlands Railway"],
["es", "Eurostar"],
["ht", "Hull Trains"],
["tl", "Thameslink"],
["gc", "Grand Central"],
["gx", "Gatwick Express"],
["hx", "Heathrow Express"],
["ls", "Locomotive Services Limited"],
["me", "Merseyrail"],
["lr", "Network Rail OTM"],
["xr", "TfL Elizabeth Line"],
["se", "Southeastern"],
["sn", "Southern"],
["le", "Greater Anglia"],
["ga", "Greater Anglia"],
["lm", "West Midlands Railway (LM)"],
["sr", "ScotRail"],
["gn", "Great Northern"],
["lt", "TfL London Underground"],
["lo", "TfL London Overground"],
["sj", "SuperTram (Sheffield)"],
["tp", "TransPennine Express"],
["vt", "Avanti West Coast"],
["gr", "LNER"],
["wr", "West Coast Railway"],
["ty", "Vintage Trains"],
["tw", "Nexus (Tyne & Wear Metro)"],
["ld", "Lumo"],
["so", "Rail Adventure"],
["ln", "Grand Union Trains"],
["zz", "Freight/Charter Company"],
["wm", "West Midlands Railway (WM)"],
["uk", "Unknown Operator"]
]);

View File

@ -1,23 +1,23 @@
import { writable } from 'svelte/store';
import { browser } from '$app/environment';
import { writable } from "svelte/store";
import { browser } from "$app/environment";
export const uuid = writable(fromLocalStorage('uuid', null));
toLocalStorage(uuid, 'uuid');
export const uuid = writable(fromLocalStorage("uuid", null));
toLocalStorage(uuid, "uuid");
function fromLocalStorage(storageKey, fallback) {
if (browser) {
const storedValue = localStorage.getItem(storageKey);
if (storedValue !== 'undefined') {
return storedValue;
if (browser) {
const storedValue = localStorage.getItem(storageKey);
if (storedValue !== "undefined") {
return storedValue;
}
}
}
return fallback;
return fallback;
}
function toLocalStorage(store, storageKey) {
if (browser) {
store.subscribe((value) => {
localStorage.setItem(storageKey, value);
});
}
if (browser) {
store.subscribe((value) => {
localStorage.setItem(storageKey, value);
});
}
}

View File

@ -1,3 +1,3 @@
export const version: string = '2024.04.5';
export const versionTag: string = '';
export const version: string = "2024.04.5";
export const versionTag: string = "";
export const showWelcome: boolean = false;

View File

@ -1,23 +1,23 @@
import { writable, type Writable } from 'svelte/store';
import { browser } from '$app/environment';
import { writable, type Writable } from "svelte/store";
import { browser } from "$app/environment";
export const welcome = writable(fromLocalStorage('welcome', '0'));
toLocalStorage(welcome, 'welcome');
export const welcome = writable(fromLocalStorage("welcome", "0"));
toLocalStorage(welcome, "welcome");
function fromLocalStorage(storageKey: string, fallback: string) {
if (browser) {
const storedValue = localStorage.getItem(storageKey);
if (storedValue !== 'undefined') {
return storedValue;
if (browser) {
const storedValue = localStorage.getItem(storageKey);
if (storedValue !== "undefined") {
return storedValue;
}
}
}
return fallback;
return fallback;
}
function toLocalStorage(store: Writable<any>, storageKey: string) {
if (browser) {
store.subscribe((value: string) => {
localStorage.setItem(storageKey, value);
});
}
if (browser) {
store.subscribe((value: string) => {
localStorage.setItem(storageKey, value);
});
}
}

View File

@ -19,24 +19,24 @@
/* Main Theme */
:root {
--main-bg-color: #404c55;
--second-bg-color: #2b343c;
--main-header-color: #dff3f3;
--main-text-color: #cce9e9;
--secondary-text-color: #02fcfc;
--main-link-color: #00b7b7;
--island-bg-color: #3c6f79de;
--island-bg-solid: #3c6f79;
--island-button-color: #404c55;
--island-link-color: #e1ebeb;
--island-header-color: #4fd1d1;
--island-text-color: #e1ebeb;
--overlay-island-bg-color: #3c6f79;
--box-shadow-color: rgba(0, 0, 0, 0.19);
--box-shadow: 2px 2px 4px rgba(0, 0, 0, 0.19);
--main-alert-color: #ed6d00;
--second-alert-color: #e77f00;
--main-warning-color: orange;
--main-bg-color: #404c55;
--second-bg-color: #2b343c;
--main-header-color: #dff3f3;
--main-text-color: #cce9e9;
--secondary-text-color: #02fcfc;
--main-link-color: #00b7b7;
--island-bg-color: #3c6f79de;
--island-bg-solid: #3c6f79;
--island-button-color: #404c55;
--island-link-color: #e1ebeb;
--island-header-color: #4fd1d1;
--island-text-color: #e1ebeb;
--overlay-island-bg-color: #3c6f79;
--box-shadow-color: rgba(0, 0, 0, 0.19);
--box-shadow: 2px 2px 4px rgba(0, 0, 0, 0.19);
--main-alert-color: #ed6d00;
--second-alert-color: #e77f00;
--main-warning-color: orange;
}
/* Christmas Theme

View File

@ -1,33 +1,33 @@
<script lang="ts">
import type { OB_Pis_SimpleObject } from '@owlboard/ts-types';
import type { OB_Pis_SimpleObject } from "@owlboard/ts-types";
export let pisObject: OB_Pis_SimpleObject;
export let pisObject: OB_Pis_SimpleObject;
</script>
{#if pisObject}
{#if typeof pisObject === 'string' || typeof pisObject === 'number'}
<span class="pis">PIS: {pisObject}</span>
{:else if pisObject['skipCount'] === 0}
<span class="pis">PIS: {pisObject.code}</span>
{:else if pisObject['skipCount'] > 0}
<span class="pis">PIS: {pisObject.code}</span>
<br />
<span class="pis-text"
>(skip {pisObject.skipType}{#if pisObject.skipCount > 1} {' ' + pisObject.skipCount} stops{:else} stop{/if})</span
>
{/if}
{#if typeof pisObject === "string" || typeof pisObject === "number"}
<span class="pis">PIS: {pisObject}</span>
{:else if pisObject["skipCount"] === 0}
<span class="pis">PIS: {pisObject.code}</span>
{:else if pisObject["skipCount"] > 0}
<span class="pis">PIS: {pisObject.code}</span>
<br />
<span class="pis-text"
>(skip {pisObject.skipType}{#if pisObject.skipCount > 1} {" " + pisObject.skipCount} stops{:else} stop{/if})</span
>
{/if}
{/if}
<style>
.pis-text {
font-size: 14px;
font-weight: 600;
color: var(--main-text-color);
}
.pis-text {
font-size: 14px;
font-weight: 600;
color: var(--main-text-color);
}
.pis {
font-size: 18px;
font-weight: 600;
color: var(--main-text-color);
}
.pis {
font-size: 18px;
font-weight: 600;
color: var(--main-text-color);
}
</style>

View File

@ -1,223 +1,223 @@
<script lang="ts">
export let toc: string;
export let full: boolean = false;
export let toc: string;
export let full: boolean = false;
import { tocs as map } from '$lib/stores/tocMap';
import { tocs as map } from "$lib/stores/tocMap";
let text: string;
let text: string;
$: {
if (full) {
text = map.get(toc.toLowerCase()) || toc;
} else {
text = toc;
$: {
if (full) {
text = map.get(toc.toLowerCase()) || toc;
} else {
text = toc;
}
}
}
</script>
<span class={toc.toLocaleLowerCase()}>{text}</span>
<style>
span {
padding: 4px;
border-radius: 5px;
border-style: solid;
border-width: 1px;
background-color: white;
color: black;
}
.gw {
/* GWR */
background-color: #07352d;
color: white;
border-color: #041d18;
}
.sw,
.il {
/* SWR & Island Line */
background-color: rgb(17, 23, 23);
color: white;
}
.nt {
/* Northern */
background-color: rgb(38, 34, 98);
color: white;
}
.aw {
/* TfW */
background-color: red;
color: white;
}
.cc {
/* c2c */
background-color: rgb(168, 25, 127);
color: white;
}
.cs {
/* Caledonian Sleeper */
background-color: #033c3c;
color: white;
}
.ch {
/* Chiltern Railways */
background-color: white;
color: blue;
}
.xc {
/* CrossCountry */
background-color: rgb(76, 18, 58);
color: white;
}
.em {
/* EMR */
background-color: rgb(69, 29, 69);
color: white;
}
.es {
/* Eurostar */
background-color: rgb(13, 13, 98);
color: yellow;
}
.zz {
/* Freight, Charters, etc */
background-color: rgba(255, 255, 255, 0);
color: white;
}
.ht {
/* Hull Trains */
background-color: rgb(160, 0, 136);
color: rgb(19, 19, 173);
}
.gn {
/* GTR Great Northern */
background-color: rgb(79, 0, 128);
color: white;
}
.tl {
/* GTR Thameslink */
background-color: rgb(191, 25, 183);
color: white;
}
.gc {
/* Grand Central */
background-color: rgb(40, 40, 40);
color: rgb(219, 123, 5);
}
.le,
.ga {
/*Greater Anglia */
background-color: rgb(122, 124, 154);
color: rgb(151, 0, 0);
border-color: red;
}
.hx {
/* Heathrow Express */
background-color: rgb(181, 142, 211);
color: black;
}
.ls {
/* Locomotive Services Limited */
background-color: white;
color: black;
}
.lm {
/* West Midlands Railway */
background-color: rgb(120, 120, 120);
border-color: rgb(230, 150, 0);
color: white;
}
.lo {
/*London Overground */
background-color: rgb(231, 150, 0);
color: rgb(0, 0, 210);
}
.lt {
/* London Underground (when on Network Rail Metals) */
background-color: rgb(203, 17, 17);
color: rgb(0, 0, 210);
}
.me {
/* Merseyrail */
background-color: rgb(229, 229, 16);
color: rgb(96, 96, 96);
}
.lr {
/* NR On-Track Machines */
background-color: yellow;
color: black;
}
.tw {
/* Tyne & Wear Metro (when on Network Rail Metals) */
background-color: rgb(212, 169, 0);
color: black;
}
.sr {
/* ScotRail */
background-color: rgb(16, 16, 200);
color: white;
}
.sj {
/* South Yorkshire (Sheffield) SuperTram (When on network rail metals) */
background-color: rgb(255, 185, 56);
color: rgb(58, 58, 255);
}
.se {
/* Southeastern */
background-color: darkblue;
color: rgb(107, 152, 207);
}
.sn {
/* GTR (Southern) */
background-color: rgb(11, 74, 11);
color: rgb(231, 231, 188);
}
.xr {
/* Elizabeth Line (EastWest CrossRail) */
background-color: rgb(91, 0, 171);
color: rgb(207, 207, 255);
}
.tp {
/* TransPennine Express */
background-color: rgb(197, 130, 238);
color: rgb(0, 98, 226);
}
.vt {
/* Avanti West Coast */
background-color: rgb(37, 37, 86);
color: rgb(230, 96, 0);
}
.gr {
/* LNER */
background-color: rgb(202, 0, 0);
color: white;
}
.wc {
/* West Coast Railway (Spot Hire/Charter) */
background-color: maroon;
color: rgb(225, 190, 92);
}
.ty {
/* Vintage Trains (Tour Operator) */
background-color: green;
color: white;
}
.ld {
/* Lumo */
background-color: whitesmoke;
color: blue;
}
.so {
/* Rail Adventure */
background-color: rgb(93, 93, 93);
color: rgb(93, 195, 93);
}
.ln {
/* Grand Union Trains */
background-color: rgb(89, 89, 89);
color: white;
}
.uk {
background-color: whitesmoke;
color: black;
}
span {
padding: 4px;
border-radius: 5px;
border-style: solid;
border-width: 1px;
background-color: white;
color: black;
}
.gw {
/* GWR */
background-color: #07352d;
color: white;
border-color: #041d18;
}
.sw,
.il {
/* SWR & Island Line */
background-color: rgb(17, 23, 23);
color: white;
}
.nt {
/* Northern */
background-color: rgb(38, 34, 98);
color: white;
}
.aw {
/* TfW */
background-color: red;
color: white;
}
.cc {
/* c2c */
background-color: rgb(168, 25, 127);
color: white;
}
.cs {
/* Caledonian Sleeper */
background-color: #033c3c;
color: white;
}
.ch {
/* Chiltern Railways */
background-color: white;
color: blue;
}
.xc {
/* CrossCountry */
background-color: rgb(76, 18, 58);
color: white;
}
.em {
/* EMR */
background-color: rgb(69, 29, 69);
color: white;
}
.es {
/* Eurostar */
background-color: rgb(13, 13, 98);
color: yellow;
}
.zz {
/* Freight, Charters, etc */
background-color: rgba(255, 255, 255, 0);
color: white;
}
.ht {
/* Hull Trains */
background-color: rgb(160, 0, 136);
color: rgb(19, 19, 173);
}
.gn {
/* GTR Great Northern */
background-color: rgb(79, 0, 128);
color: white;
}
.tl {
/* GTR Thameslink */
background-color: rgb(191, 25, 183);
color: white;
}
.gc {
/* Grand Central */
background-color: rgb(40, 40, 40);
color: rgb(219, 123, 5);
}
.le,
.ga {
/*Greater Anglia */
background-color: rgb(122, 124, 154);
color: rgb(151, 0, 0);
border-color: red;
}
.hx {
/* Heathrow Express */
background-color: rgb(181, 142, 211);
color: black;
}
.ls {
/* Locomotive Services Limited */
background-color: white;
color: black;
}
.lm {
/* West Midlands Railway */
background-color: rgb(120, 120, 120);
border-color: rgb(230, 150, 0);
color: white;
}
.lo {
/*London Overground */
background-color: rgb(231, 150, 0);
color: rgb(0, 0, 210);
}
.lt {
/* London Underground (when on Network Rail Metals) */
background-color: rgb(203, 17, 17);
color: rgb(0, 0, 210);
}
.me {
/* Merseyrail */
background-color: rgb(229, 229, 16);
color: rgb(96, 96, 96);
}
.lr {
/* NR On-Track Machines */
background-color: yellow;
color: black;
}
.tw {
/* Tyne & Wear Metro (when on Network Rail Metals) */
background-color: rgb(212, 169, 0);
color: black;
}
.sr {
/* ScotRail */
background-color: rgb(16, 16, 200);
color: white;
}
.sj {
/* South Yorkshire (Sheffield) SuperTram (When on network rail metals) */
background-color: rgb(255, 185, 56);
color: rgb(58, 58, 255);
}
.se {
/* Southeastern */
background-color: darkblue;
color: rgb(107, 152, 207);
}
.sn {
/* GTR (Southern) */
background-color: rgb(11, 74, 11);
color: rgb(231, 231, 188);
}
.xr {
/* Elizabeth Line (EastWest CrossRail) */
background-color: rgb(91, 0, 171);
color: rgb(207, 207, 255);
}
.tp {
/* TransPennine Express */
background-color: rgb(197, 130, 238);
color: rgb(0, 98, 226);
}
.vt {
/* Avanti West Coast */
background-color: rgb(37, 37, 86);
color: rgb(230, 96, 0);
}
.gr {
/* LNER */
background-color: rgb(202, 0, 0);
color: white;
}
.wc {
/* West Coast Railway (Spot Hire/Charter) */
background-color: maroon;
color: rgb(225, 190, 92);
}
.ty {
/* Vintage Trains (Tour Operator) */
background-color: green;
color: white;
}
.ld {
/* Lumo */
background-color: whitesmoke;
color: blue;
}
.so {
/* Rail Adventure */
background-color: rgb(93, 93, 93);
color: rgb(93, 195, 93);
}
.ln {
/* Grand Union Trains */
background-color: rgb(89, 89, 89);
color: white;
}
.uk {
background-color: whitesmoke;
color: black;
}
</style>

View File

@ -1,187 +1,187 @@
<script lang="ts">
import { fly } from 'svelte/transition';
import { uuid } from '$lib/stores/uuid';
import LoadingText from '$lib/navigation/loading-text.svelte';
import StylesToc from './styles-toc.svelte';
import { getApiUrl } from '$lib/scripts/upstream';
import PisHandler from '$lib/train/pis-handler.svelte';
import { fly } from "svelte/transition";
import { uuid } from "$lib/stores/uuid";
import LoadingText from "$lib/navigation/loading-text.svelte";
import StylesToc from "./styles-toc.svelte";
import { getApiUrl } from "$lib/scripts/upstream";
import PisHandler from "$lib/train/pis-handler.svelte";
import type { OB_TrainTT_service } from '@owlboard/ts-types';
import TrainIcons from './trainIcons.svelte';
import type { OB_TrainTT_service } from "@owlboard/ts-types";
import TrainIcons from "./trainIcons.svelte";
export let service: OB_TrainTT_service;
export let service: OB_TrainTT_service;
let isExpanded = false;
let isExpanded = false;
async function getTrainByUID(tuid = '') {
const url = `${getApiUrl()}/api/v2/timetable/train/now/byTrainUid/${tuid}`;
const options = {
method: 'GET',
headers: {
uuid: $uuid
}
};
const res = await fetch(url, options);
if (res.status === 200) {
return await res.json();
} else {
throw new Error('Unable to Fetch');
async function getTrainByUID(tuid = "") {
const url = `${getApiUrl()}/api/v2/timetable/train/now/byTrainUid/${tuid}`;
const options = {
method: "GET",
headers: {
uuid: $uuid
}
};
const res = await fetch(url, options);
if (res.status === 200) {
return await res.json();
} else {
throw new Error("Unable to Fetch");
}
}
}
async function expand() {
isExpanded = !isExpanded;
}
async function expand() {
isExpanded = !isExpanded;
}
</script>
<div class="container">
<div class="container-header" on:click={expand} on:keypress={expand}>
<span class="header"
><StylesToc toc={service?.operator || ''} />
{service?.stops[0]['publicDeparture'] || service?.stops[0]['wttDeparture']}
{service?.stops[0]['tiploc']} to {service?.stops[service['stops'].length - 1]['tiploc']}</span
>
<span id="container-arrow" class:isExpanded>V</span>
</div>
{#if isExpanded}
<div class="container-detail" in:fly={{ y: -20, duration: 200 }}>
{#await getTrainByUID(service.trainUid)}
<LoadingText />
{:then serviceDetail}
{#if serviceDetail.stpIndicator === 'C'}
<p class="text-message">This has been removed from the timetable for today.</p>
<p class="text-message">The service may have been retimed, re-routed or removed from todays timetable completely.</p>
<p class="text-message">If it has been retimed or re-routed, there is likely to be another service with the same headcode booked to run.</p>
{:else}
<div class="detailOperator"><StylesToc toc={service?.operator || ''} full={true} /></div>
<TrainIcons serviceDetails={serviceDetail.serviceDetail} />
{#if serviceDetail.pis}
<PisHandler pisObject={serviceDetail.pis} />
{/if}
<p class="svc-detail">
Planned Type: {parseInt(serviceDetail.planSpeed) || '--'}mph {serviceDetail.powerType || 'Non-Rail vehicle'}
</p>
<p class="svc-detail">
Days Run: {serviceDetail?.daysRun.join(', ').toUpperCase() || 'Unknown'}
</p>
<p class="svc-detail validity">
Valid From: {new Date(serviceDetail.scheduleStart).toLocaleDateString('en-GB', {
timeZone: 'UTC'
})} - {new Date(serviceDetail.scheduleEnd).toLocaleDateString('en-GB', {
timeZone: 'UTC'
})}
</p>
<table>
<caption>Italics are 'pass' times, grey times are non-passenger stops</caption>
<tr>
<th>Location</th>
<th>Plt.</th>
<th>Sch Arr.</th>
<th>Sch Dep.</th>
</tr>
{#each serviceDetail.stops as stop}
<tr>
{#if stop.publicArrival || stop.publicDeparture}
<td>{stop.tiploc}</td>
<td>{stop.platform || '-'}</td>
<td>{stop.publicArrival || '-'}</td>
<td>{stop.publicDeparture || '-'}</td>
{:else if stop.wttArrival || stop.wttDeparture}
<td class="wtt">{stop.tiploc}</td>
<td class="wtt">{stop.platform || stop.depLine || stop.arrLine || '-'}</td>
<td class="wtt">{stop.wttArrival || '-'}</td>
<td class="wtt">{stop.wttDeparture || '-'}</td>
{:else}
<td class="pass">{stop.tiploc}</td>
<td class="pass">{stop.platform || stop.depLine || stop.arrLine || '-'}</td>
<td class="pass">-</td>
<td class="pass">{stop.pass || '-'}</td>
{/if}
</tr>
{/each}
</table>
{/if}
{:catch}
<p>Unable to fetch train data</p>
{/await}
<div class="container-header" on:click={expand} on:keypress={expand}>
<span class="header"
><StylesToc toc={service?.operator || ""} />
{service?.stops[0]["publicDeparture"] || service?.stops[0]["wttDeparture"]}
{service?.stops[0]["tiploc"]} to {service?.stops[service["stops"].length - 1]["tiploc"]}</span
>
<span id="container-arrow" class:isExpanded>V</span>
</div>
{/if}
{#if isExpanded}
<div class="container-detail" in:fly={{ y: -20, duration: 200 }}>
{#await getTrainByUID(service.trainUid)}
<LoadingText />
{:then serviceDetail}
{#if serviceDetail.stpIndicator === "C"}
<p class="text-message">This has been removed from the timetable for today.</p>
<p class="text-message">The service may have been retimed, re-routed or removed from todays timetable completely.</p>
<p class="text-message">If it has been retimed or re-routed, there is likely to be another service with the same headcode booked to run.</p>
{:else}
<div class="detailOperator"><StylesToc toc={service?.operator || ""} full={true} /></div>
<TrainIcons serviceDetails={serviceDetail.serviceDetail} />
{#if serviceDetail.pis}
<PisHandler pisObject={serviceDetail.pis} />
{/if}
<p class="svc-detail">
Planned Type: {parseInt(serviceDetail.planSpeed) || "--"}mph {serviceDetail.powerType || "Non-Rail vehicle"}
</p>
<p class="svc-detail">
Days Run: {serviceDetail?.daysRun.join(", ").toUpperCase() || "Unknown"}
</p>
<p class="svc-detail validity">
Valid From: {new Date(serviceDetail.scheduleStart).toLocaleDateString("en-GB", {
timeZone: "UTC"
})} - {new Date(serviceDetail.scheduleEnd).toLocaleDateString("en-GB", {
timeZone: "UTC"
})}
</p>
<table>
<caption>Italics are 'pass' times, grey times are non-passenger stops</caption>
<tr>
<th>Location</th>
<th>Plt.</th>
<th>Sch Arr.</th>
<th>Sch Dep.</th>
</tr>
{#each serviceDetail.stops as stop}
<tr>
{#if stop.publicArrival || stop.publicDeparture}
<td>{stop.tiploc}</td>
<td>{stop.platform || "-"}</td>
<td>{stop.publicArrival || "-"}</td>
<td>{stop.publicDeparture || "-"}</td>
{:else if stop.wttArrival || stop.wttDeparture}
<td class="wtt">{stop.tiploc}</td>
<td class="wtt">{stop.platform || stop.depLine || stop.arrLine || "-"}</td>
<td class="wtt">{stop.wttArrival || "-"}</td>
<td class="wtt">{stop.wttDeparture || "-"}</td>
{:else}
<td class="pass">{stop.tiploc}</td>
<td class="pass">{stop.platform || stop.depLine || stop.arrLine || "-"}</td>
<td class="pass">-</td>
<td class="pass">{stop.pass || "-"}</td>
{/if}
</tr>
{/each}
</table>
{/if}
{:catch}
<p>Unable to fetch train data</p>
{/await}
</div>
{/if}
</div>
<style>
.container {
position: relative;
margin: auto;
margin-bottom: 20px;
width: 95%;
max-width: 600px;
height: auto;
background-color: var(--island-bg-solid);
border-radius: 10px;
overflow: hidden;
transition: height 500ms ease-in-out;
box-shadow: 5px 5px 30px var(--box-shadow-color);
}
.container-header {
text-align: left;
padding-left: 10px;
font-size: 18px;
font-weight: 600;
padding-top: 12px;
padding-bottom: 10px;
font-family: ubuntu, monospace;
color: var(--island-text-color);
}
#container-arrow {
font-family: Arial, Helvetica, sans-serif;
font-weight: 600;
color: var(--island-text-color);
margin: 0;
padding: 0;
position: absolute;
right: 16px;
top: 10px;
transition-duration: 300ms;
z-index: 2;
}
.isExpanded {
transform: rotate(180deg);
}
.detailOperator {
padding-top: 15px;
margin-bottom: 15px;
font-weight: 600;
}
.svc-detail {
margin-top: 6px;
margin-bottom: 2px;
color: var(--island-text-color);
}
.validity {
font-size: 14px;
}
table {
margin: auto;
padding-top: 5px;
padding-bottom: 10px;
color: var(--island-text-color);
}
caption {
padding-top: 15px;
font-size: small;
}
.wtt {
opacity: 0.5;
}
.pass {
font-style: italic;
opacity: 0.5;
}
.text-message {
margin: 5px;
margin-left: 20px;
margin-right: 20px;
padding-bottom: 10px;
}
.container {
position: relative;
margin: auto;
margin-bottom: 20px;
width: 95%;
max-width: 600px;
height: auto;
background-color: var(--island-bg-solid);
border-radius: 10px;
overflow: hidden;
transition: height 500ms ease-in-out;
box-shadow: 5px 5px 30px var(--box-shadow-color);
}
.container-header {
text-align: left;
padding-left: 10px;
font-size: 18px;
font-weight: 600;
padding-top: 12px;
padding-bottom: 10px;
font-family: ubuntu, monospace;
color: var(--island-text-color);
}
#container-arrow {
font-family: Arial, Helvetica, sans-serif;
font-weight: 600;
color: var(--island-text-color);
margin: 0;
padding: 0;
position: absolute;
right: 16px;
top: 10px;
transition-duration: 300ms;
z-index: 2;
}
.isExpanded {
transform: rotate(180deg);
}
.detailOperator {
padding-top: 15px;
margin-bottom: 15px;
font-weight: 600;
}
.svc-detail {
margin-top: 6px;
margin-bottom: 2px;
color: var(--island-text-color);
}
.validity {
font-size: 14px;
}
table {
margin: auto;
padding-top: 5px;
padding-bottom: 10px;
color: var(--island-text-color);
}
caption {
padding-top: 15px;
font-size: small;
}
.wtt {
opacity: 0.5;
}
.pass {
font-style: italic;
opacity: 0.5;
}
.text-message {
margin: 5px;
margin-left: 20px;
margin-right: 20px;
padding-bottom: 10px;
}
</style>

View File

@ -1,38 +1,38 @@
<script lang="ts">
import Tooltip from '$lib/Tooltip.svelte';
import type { ServiceDetail } from '@owlboard/ts-types';
import { IconBed, IconSquare1, IconSquareLetterV, IconToolsKitchen2 } from '@tabler/icons-svelte';
import Tooltip from "$lib/Tooltip.svelte";
import type { ServiceDetail } from "@owlboard/ts-types";
import { IconBed, IconSquare1, IconSquareLetterV, IconToolsKitchen2 } from "@tabler/icons-svelte";
export let serviceDetails: ServiceDetail;
export let serviceDetails: ServiceDetail;
</script>
{#if serviceDetails.firstClass}
<Tooltip text="First Class is available">
<IconSquare1 />
</Tooltip>
<Tooltip text="First Class is available">
<IconSquare1 />
</Tooltip>
{/if}
{#if serviceDetails.catering}
<Tooltip text="Catering is available">
<IconToolsKitchen2 />
</Tooltip>
<Tooltip text="Catering is available">
<IconToolsKitchen2 />
</Tooltip>
{/if}
{#if serviceDetails.sleeper}
<Tooltip text="Sleeping Berths are available">
<IconBed />
</Tooltip>
<Tooltip text="Sleeping Berths are available">
<IconBed />
</Tooltip>
{/if}
{#if serviceDetails.vstp}
<Tooltip text="This is a VSTP planned service">
<IconSquareLetterV />
</Tooltip>
<Tooltip text="This is a VSTP planned service">
<IconSquareLetterV />
</Tooltip>
{/if}
<!-- Render a newline if any of the icons is to appear -->
{#if serviceDetails.firstClass || serviceDetails.catering || serviceDetails.sleeper || serviceDetails.vstp}
<br />
<br />
{/if}
<style>

View File

@ -1,8 +1,8 @@
<script>
import { page } from '$app/stores';
import Header from '$lib/navigation/header.svelte';
import Nav from '$lib/navigation/nav.svelte';
const title = 'OwlBoard - Error';
import { page } from "$app/stores";
import Header from "$lib/navigation/header.svelte";
import Nav from "$lib/navigation/nav.svelte";
const title = "OwlBoard - Error";
</script>
<Header {title} />
@ -10,16 +10,16 @@
<h1>{$page.status}: {$page?.error?.message}</h1>
{#if $page.status === 404}
<p>This is not the page you're looking for.</p>
<p>The page you are looking for doesn't exist, use the tabs below to find another page.</p>
<p>This is not the page you're looking for.</p>
<p>The page you are looking for doesn't exist, use the tabs below to find another page.</p>
{:else if $page.status === 500}
<p>
Something went wrong loading the app.<br />
Try going <a href="/">home</a> and try again.
</p>
<p>If the problem persists, you can report an issue from the 'More' menu.</p>
<p>
Something went wrong loading the app.<br />
Try going <a href="/">home</a> and try again.
</p>
<p>If the problem persists, you can report an issue from the 'More' menu.</p>
{:else}
<p>Not sure what went wrong, please try again later or report an issue from the 'More' menu.</p>
<p>Not sure what went wrong, please try again later or report an issue from the 'More' menu.</p>
{/if}
<Nav />

View File

@ -1,13 +1,13 @@
<script>
import '$lib/themes.css';
import '$lib/main.css';
import { dev } from '$app/environment';
import DevBanner from '$lib/DevBanner.svelte';
import { page } from '$app/stores';
import "$lib/themes.css";
import "$lib/main.css";
import { dev } from "$app/environment";
import DevBanner from "$lib/DevBanner.svelte";
import { page } from "$app/stores";
</script>
<svelte:head>
<!--
<!--
___ _ ___ _
/ _ \__ __ _| | _ ) ___ __ _ _ _ __| |
| (_) \ V V / | _ \/ _ \/ _` | '_/ _` |
@ -16,21 +16,21 @@
It's easier to read there
-->
<meta name="application-name" content="OwlBoard" />
<meta name="author" content="Frederick Boniface" />
<meta
name="description"
content="Get instant access to live train data, PIS codes, and location reference codes. Built by railway staff, for railway staff your fastest route to accurate information."
/>
<meta name="viewport" content="width=device-width" />
<meta name="theme-color" content="#00b7b7" />
<link rel="canonical" href="https://owlboard.info{$page.url.pathname}" />
<link rel="icon" href="/images/icon.svg" type="image/svg+xml" />
<link rel="apple-touch-icon" href="/images/app-icons/any/apple-192.png" />
<link rel="manifest" href="/manifest.json" />
<title>OwlBoard</title>
<meta name="application-name" content="OwlBoard" />
<meta name="author" content="Frederick Boniface" />
<meta
name="description"
content="Get instant access to live train data, PIS codes, and location reference codes. Built by railway staff, for railway staff your fastest route to accurate information."
/>
<meta name="viewport" content="width=device-width" />
<meta name="theme-color" content="#00b7b7" />
<link rel="canonical" href="https://owlboard.info{$page.url.pathname}" />
<link rel="icon" href="/images/icon.svg" type="image/svg+xml" />
<link rel="apple-touch-icon" href="/images/app-icons/any/apple-192.png" />
<link rel="manifest" href="/manifest.json" />
<title>OwlBoard</title>
</svelte:head>
{#if dev}
<DevBanner />
<DevBanner />
{/if}
<slot />

View File

@ -1,36 +1,36 @@
<script>
import Header from '$lib/navigation/header.svelte';
import Nav from '$lib/navigation/nav.svelte';
import InputIsland from '$lib/islands/input-island-form.svelte';
import QuickLinkIsland from '$lib/islands/quick-link-island.svelte';
import Welcome from '$lib/overlays/welcome.svelte';
import { welcome } from '$lib/stores/welcome';
import { version, showWelcome } from '$lib/stores/version';
import Header from "$lib/navigation/header.svelte";
import Nav from "$lib/navigation/nav.svelte";
import InputIsland from "$lib/islands/input-island-form.svelte";
import QuickLinkIsland from "$lib/islands/quick-link-island.svelte";
import Welcome from "$lib/overlays/welcome.svelte";
import { welcome } from "$lib/stores/welcome";
import { version, showWelcome } from "$lib/stores/version";
const title = 'OwlBoard';
const inputIslands = [
{
title: 'Live Departure Boards',
action: '/ldb',
placeholder: 'Enter CRS/TIPLOC',
queryName: 'station'
},
{
title: 'Train Details & PIS',
action: '/train',
placeholder: 'Enter Headcode',
queryName: 'headcode'
}
];
const title = "OwlBoard";
const inputIslands = [
{
title: "Live Departure Boards",
action: "/ldb",
placeholder: "Enter CRS/TIPLOC",
queryName: "station"
},
{
title: "Train Details & PIS",
action: "/train",
placeholder: "Enter Headcode",
queryName: "headcode"
}
];
</script>
{#if showWelcome && ($welcome === 'null' || !$welcome || parseInt($welcome.replace(/\./g, '')) < parseInt(version.replace(/\./g, '')))}
<Welcome />
{#if showWelcome && ($welcome === "null" || !$welcome || parseInt($welcome.replace(/\./g, "")) < parseInt(version.replace(/\./g, "")))}
<Welcome />
{/if}
<Header {title} />
{#each inputIslands as variables}
<InputIsland {variables} />
<InputIsland {variables} />
{/each}
<QuickLinkIsland />

View File

@ -1,8 +1,8 @@
<script lang="ts">
import Header from '$lib/navigation/header.svelte';
import Nav from '$lib/navigation/nav.svelte';
import Header from "$lib/navigation/header.svelte";
import Nav from "$lib/navigation/nav.svelte";
const title = '404 - Not Found';
const title = "404 - Not Found";
</script>
<Header {title} />
@ -14,10 +14,10 @@
<Nav />
<style>
.heading {
color: var(--second-text-color);
}
.err_code {
color: white;
}
.heading {
color: var(--second-text-color);
}
.err_code {
color: white;
}
</style>

View File

@ -1,8 +1,8 @@
<script lang="ts">
import Header from '$lib/navigation/header.svelte';
import Nav from '$lib/navigation/nav.svelte';
import Header from "$lib/navigation/header.svelte";
import Nav from "$lib/navigation/nav.svelte";
const title = '50x - Server Error';
const title = "50x - Server Error";
</script>
<Header {title} />
@ -14,10 +14,10 @@
<Nav />
<style>
.heading {
color: var(--second-text-color);
}
.err_code {
color: white;
}
.heading {
color: var(--second-text-color);
}
.err_code {
color: white;
}
</style>

View File

@ -1,42 +1,42 @@
<script>
import Header from '$lib/navigation/header.svelte';
import Nav from '$lib/navigation/nav-ldb.svelte';
import PublicLdb from '$lib/ldb/public-ldb.svelte';
import StaffLdb from '$lib/ldb/staff/staff-ldb.svelte';
import { uuid } from '$lib/stores/uuid.js';
import { onMount } from 'svelte';
import Header from "$lib/navigation/header.svelte";
import Nav from "$lib/navigation/nav-ldb.svelte";
import PublicLdb from "$lib/ldb/public-ldb.svelte";
import StaffLdb from "$lib/ldb/staff/staff-ldb.svelte";
import { uuid } from "$lib/stores/uuid.js";
import { onMount } from "svelte";
let title = 'Loading';
let title = "Loading";
async function getHeadcode() {
return new URLSearchParams(window.location.search).get('station');
}
let station = '';
let staff = false;
let uuidValue = '';
$: uuidValue = $uuid;
onMount(async () => {
station = (await getHeadcode()) || '';
if (uuidValue !== null && uuidValue !== '' && uuidValue !== 'null') {
staff = true;
title = 'Staff Board';
} else {
title = 'Public Board';
async function getHeadcode() {
return new URLSearchParams(window.location.search).get("station");
}
});
let station = "";
let staff = false;
let uuidValue = "";
$: uuidValue = $uuid;
onMount(async () => {
station = (await getHeadcode()) || "";
if (uuidValue !== null && uuidValue !== "" && uuidValue !== "null") {
staff = true;
title = "Staff Board";
} else {
title = "Public Board";
}
});
</script>
<Header {title} />
<!-- If 'uuid' exists in store then load StaffLdb else load PublicLdb -->
{#if !staff}
<PublicLdb {station} bind:title />
<PublicLdb {station} bind:title />
{:else}
<StaffLdb {station} bind:title />
<!--<StaffLdb {station} bind:title={title} /> -- Temporary, Disable StaffLdb - it isn't implemented -->
<StaffLdb {station} bind:title />
<!--<StaffLdb {station} bind:title={title} /> -- Temporary, Disable StaffLdb - it isn't implemented -->
{/if}
<Nav />

View File

@ -1,77 +1,89 @@
<script>
import Header from '$lib/navigation/header.svelte';
import Nav from '$lib/navigation/nav.svelte';
import { IconCode, IconHelp, IconInfoCircle, IconLocation, IconMessageCode, IconNumber, IconSettings, IconSpy, IconUser, IconUserPlus, IconVersions } from '@tabler/icons-svelte';
const title = 'More';
import Header from "$lib/navigation/header.svelte";
import Nav from "$lib/navigation/nav.svelte";
import {
IconCode,
IconHelp,
IconInfoCircle,
IconLocation,
IconMessageCode,
IconNumber,
IconSettings,
IconSpy,
IconUser,
IconUserPlus,
IconVersions
} from "@tabler/icons-svelte";
const title = "More";
const links = [
{ title: 'Your Data', path: '/more/data', icon: IconUser },
{ title: 'Registration', path: '/more/reg', icon: IconUserPlus },
{ title: 'Settings', path: '/more/settings', icon: IconSettings },
{ title: 'Help', path: '/more/help', icon: IconHelp },
{ title: 'About', path: '/more/about', icon: IconInfoCircle },
{ title: 'Location Reference Code Lookup', path: '/more/corpus', icon: IconLocation },
{ title: 'Reason Code Lookup', path: '/more/reasons', icon: IconMessageCode },
{ title: 'Privacy Policy', path: '/more/privacy', icon: IconSpy },
{ title: 'Component Versions', path: '/more/versions', icon: IconVersions },
{ title: 'Statistics', path: '/more/statistics', icon: IconNumber }
];
const links = [
{ title: "Your Data", path: "/more/data", icon: IconUser },
{ title: "Registration", path: "/more/reg", icon: IconUserPlus },
{ title: "Settings", path: "/more/settings", icon: IconSettings },
{ title: "Help", path: "/more/help", icon: IconHelp },
{ title: "About", path: "/more/about", icon: IconInfoCircle },
{ title: "Location Reference Code Lookup", path: "/more/corpus", icon: IconLocation },
{ title: "Reason Code Lookup", path: "/more/reasons", icon: IconMessageCode },
{ title: "Privacy Policy", path: "/more/privacy", icon: IconSpy },
{ title: "Component Versions", path: "/more/versions", icon: IconVersions },
{ title: "Statistics", path: "/more/statistics", icon: IconNumber }
];
</script>
<Header {title} />
{#each links as item}
<a href={item.path}>
<div>
<svelte:component this={item.icon} />
<p>{item.title}</p>
</div>
</a>
<a href={item.path}>
<div>
<svelte:component this={item.icon} />
<p>{item.title}</p>
</div>
</a>
{/each}
<Nav />
<style>
div {
width: 100%;
background: rgba(0, 0, 0, 0.226);
border-color: aliceblue;
border-width: 1px;
border-style: solid;
border-left: none;
padding-left: 0.5rem;
overflow-x: hidden;
border-right: none;
height: 50px;
display: flex;
align-items: center;
margin-bottom: 0.5rem;
}
a {
text-decoration: none;
height: 100%;
vertical-align: middle;
overflow-x: hidden;
}
p {
color: white;
margin: 0;
padding-left: 10px;
text-align: left;
line-height: 50px;
vertical-align: middle;
font-weight: 600;
font-size: 20px;
overflow-x: hidden;
}
@media (min-width: 600px) {
p {
text-align: center;
div {
width: 100%;
background: rgba(0, 0, 0, 0.226);
border-color: aliceblue;
border-width: 1px;
border-style: solid;
border-left: none;
padding-left: 0.5rem;
overflow-x: hidden;
border-right: none;
height: 50px;
display: flex;
align-items: center;
margin-bottom: 0.5rem;
}
}
@media (max-width: 380px) {
p {
font-size: 16px;
a {
text-decoration: none;
height: 100%;
vertical-align: middle;
overflow-x: hidden;
}
p {
color: white;
margin: 0;
padding-left: 10px;
text-align: left;
line-height: 50px;
vertical-align: middle;
font-weight: 600;
font-size: 20px;
overflow-x: hidden;
}
@media (min-width: 600px) {
p {
text-align: center;
}
}
@media (max-width: 380px) {
p {
font-size: 16px;
}
}
}
</style>

View File

@ -1,9 +1,9 @@
<script>
import LargeLogo from '$lib/images/large-logo.svelte';
import Header from '$lib/navigation/header.svelte';
import Nav from '$lib/navigation/nav.svelte';
import LargeLogo from "$lib/images/large-logo.svelte";
import Header from "$lib/navigation/header.svelte";
import Nav from "$lib/navigation/nav.svelte";
const title = 'About';
const title = "About";
</script>
<Header {title} />
@ -13,19 +13,19 @@
<p>I developed OwlBoard in 2022 with the aim of providing fast and easy access to the information we need on a daily basis.</p>
<p>Data is sourced from National Rail Enquiries, the OwlBoard Project, and Network Rail.</p>
<p>
OwlBoard components are available under various Open Source licenses. Please refer to the
<a href="https://git.fjla.uk/OwlBoard" target="_blank">code repository</a>
for more detailed information.
OwlBoard components are available under various Open Source licenses. Please refer to the
<a href="https://git.fjla.uk/OwlBoard" target="_blank">code repository</a>
for more detailed information.
</p>
<Nav />
<style>
p {
margin-left: 10px;
margin-right: 10px;
}
.neg {
margin-top: -40px;
margin-bottom: 40px;
}
p {
margin-left: 10px;
margin-right: 10px;
}
.neg {
margin-top: -40px;
margin-bottom: 40px;
}
</style>

View File

@ -1,78 +1,78 @@
<script>
import Header from '$lib/navigation/header.svelte';
import Loading from '$lib/navigation/loading.svelte';
import Nav from '$lib/navigation/nav.svelte';
import { getApiUrl } from '$lib/scripts/upstream';
const title = 'Location Codes';
import Header from "$lib/navigation/header.svelte";
import Loading from "$lib/navigation/loading.svelte";
import Nav from "$lib/navigation/nav.svelte";
import { getApiUrl } from "$lib/scripts/upstream";
const title = "Location Codes";
let val = {
crs: '',
tiploc: '',
stanox: '',
nlc: '',
name: '',
uic: ''
};
let isLoading = false;
async function getData(type = '', value = '') {
const url = `${getApiUrl()}/api/v2/ref/locationCode/${type}/${value}`;
const res = await fetch(url);
const data = await res.json();
isLoading = false;
return data;
}
async function processData(data) {
//console.log("data",JSON.stringify(data))
if (data.ERROR == 'Offline') {
return;
}
val = {
crs: data[0]['3ALPHA'] || 'None',
tiploc: data[0]['TIPLOC'] || 'None',
stanox: data[0]['STANOX'] || 'None',
nlc: data[0]['NLC'] || 'None',
name: data[0]['NLCDESC'] || 'None',
uic: data[0]['UIC'] || 'None'
let val = {
crs: "",
tiploc: "",
stanox: "",
nlc: "",
name: "",
uic: ""
};
//console.log("val",JSON.stringify(val));
}
async function submit() {
isLoading = true;
let data = [];
if (val?.crs) {
data = await getData('crs', val.crs);
} else if (val?.tiploc) {
data = await getData('tiploc', val.tiploc);
} else if (val?.stanox) {
data = await getData('stanox', val.stanox);
} else if (val?.nlc) {
data = await getData('nlc', val.nlc);
} else {
return;
let isLoading = false;
async function getData(type = "", value = "") {
const url = `${getApiUrl()}/api/v2/ref/locationCode/${type}/${value}`;
const res = await fetch(url);
const data = await res.json();
isLoading = false;
return data;
}
processData(data);
}
async function reset() {
val = {
crs: '',
tiploc: '',
stanox: '',
nlc: '',
name: '',
uic: ''
};
}
async function processData(data) {
//console.log("data",JSON.stringify(data))
if (data.ERROR == "Offline") {
return;
}
val = {
crs: data[0]["3ALPHA"] || "None",
tiploc: data[0]["TIPLOC"] || "None",
stanox: data[0]["STANOX"] || "None",
nlc: data[0]["NLC"] || "None",
name: data[0]["NLCDESC"] || "None",
uic: data[0]["UIC"] || "None"
};
//console.log("val",JSON.stringify(val));
}
async function submit() {
isLoading = true;
let data = [];
if (val?.crs) {
data = await getData("crs", val.crs);
} else if (val?.tiploc) {
data = await getData("tiploc", val.tiploc);
} else if (val?.stanox) {
data = await getData("stanox", val.stanox);
} else if (val?.nlc) {
data = await getData("nlc", val.nlc);
} else {
return;
}
processData(data);
}
async function reset() {
val = {
crs: "",
tiploc: "",
stanox: "",
nlc: "",
name: "",
uic: ""
};
}
</script>
<Header {title} />
{#if isLoading}
<Loading />
<Loading />
{/if}
<p>Enter one of the codes below and press submit.</p>
@ -80,59 +80,59 @@
<p class="desc">Some locations only have some applicable location codes.</p>
<div class="inputs">
<form on:submit={submit}>
<input type="text" readonly placeholder="Name" bind:value={val.name} />
<br />
<input type="text" maxlength="3" autocomplete="off" placeholder="CRS/3ALPHA" bind:value={val.crs} />
<br />
<input type="text" maxlength="7" autocomplete="off" placeholder="TIPLOC" bind:value={val.tiploc} />
<br />
<input type="text" maxlength="10" autocomplete="off" placeholder="STANOX" bind:value={val.stanox} />
<br />
<input type="number" maxlength="6" min="0" autocomplete="off" placeholder="NLC" bind:value={val.nlc} />
<br />
<input type="text" readonly autocomplete="off" placeholder="UIC" bind:value={val.uic} />
<br />
<button type="submit">Submit</button>
<button type="reset" on:click={reset}>Reset</button>
</form>
<form on:submit={submit}>
<input type="text" readonly placeholder="Name" bind:value={val.name} />
<br />
<input type="text" maxlength="3" autocomplete="off" placeholder="CRS/3ALPHA" bind:value={val.crs} />
<br />
<input type="text" maxlength="7" autocomplete="off" placeholder="TIPLOC" bind:value={val.tiploc} />
<br />
<input type="text" maxlength="10" autocomplete="off" placeholder="STANOX" bind:value={val.stanox} />
<br />
<input type="number" maxlength="6" min="0" autocomplete="off" placeholder="NLC" bind:value={val.nlc} />
<br />
<input type="text" readonly autocomplete="off" placeholder="UIC" bind:value={val.uic} />
<br />
<button type="submit">Submit</button>
<button type="reset" on:click={reset}>Reset</button>
</form>
</div>
<Nav />
<style>
p {
margin: 10px;
}
input {
font-family: urwgothic, sans-serif;
border-radius: 50px;
border: none;
width: 60%;
max-width: 200px;
min-width: 130px;
height: 30px;
margin-bottom: 10px;
text-transform: uppercase;
box-shadow: var(--box-shadow);
}
button {
border-radius: 50px;
font-family: urwgothic, sans-serif;
background-color: var(--island-bg-color);
border: none;
color: white;
height: 30px;
width: 20%;
max-width: 100px;
min-width: 75px;
font-size: 18px;
box-shadow: var(--box-shadow);
}
.desc {
color: white;
}
input {
text-align: center;
}
p {
margin: 10px;
}
input {
font-family: urwgothic, sans-serif;
border-radius: 50px;
border: none;
width: 60%;
max-width: 200px;
min-width: 130px;
height: 30px;
margin-bottom: 10px;
text-transform: uppercase;
box-shadow: var(--box-shadow);
}
button {
border-radius: 50px;
font-family: urwgothic, sans-serif;
background-color: var(--island-bg-color);
border: none;
color: white;
height: 30px;
width: 20%;
max-width: 100px;
min-width: 75px;
font-size: 18px;
box-shadow: var(--box-shadow);
}
.desc {
color: white;
}
input {
text-align: center;
}
</style>

View File

@ -1,38 +1,38 @@
<script>
import LogoutButton from '$lib/navigation/LogoutButton.svelte';
import Header from '$lib/navigation/header.svelte';
import Loading from '$lib/navigation/loading.svelte';
import Nav from '$lib/navigation/nav.svelte';
import { getApiUrl } from '$lib/scripts/upstream';
import { uuid } from '$lib/stores/uuid.js';
import { onMount } from 'svelte';
const title = 'Your Data';
import LogoutButton from "$lib/navigation/LogoutButton.svelte";
import Header from "$lib/navigation/header.svelte";
import Loading from "$lib/navigation/loading.svelte";
import Nav from "$lib/navigation/nav.svelte";
import { getApiUrl } from "$lib/scripts/upstream";
import { uuid } from "$lib/stores/uuid.js";
import { onMount } from "svelte";
const title = "Your Data";
let data = [
{
domain: 'User not Found',
atime: 'User not Found'
let data = [
{
domain: "User not Found",
atime: "User not Found"
}
];
let isLoading = false;
async function fetchData() {
if ($uuid != "null") {
const url = `${getApiUrl()}/api/v2/user/${$uuid}`;
const res = await fetch(url);
const json = await res.json();
if (json.length) {
data = json;
}
}
}
];
let isLoading = false;
async function fetchData() {
if ($uuid != 'null') {
const url = `${getApiUrl()}/api/v2/user/${$uuid}`;
const res = await fetch(url);
const json = await res.json();
if (json.length) {
data = json;
}
}
}
onMount(async () => {
isLoading = true;
await fetchData();
isLoading = false;
});
onMount(async () => {
isLoading = true;
await fetchData();
isLoading = false;
});
</script>
<Header {title} />
@ -43,26 +43,26 @@
<br /><br />
{#if isLoading}
<Loading />
{:else if data[0].domain != 'User not Found'}
<p class="api_response">Registration Domain: {data[0]['domain']}</p>
<p class="api_response">Access Time: {data[0]['atime']}</p>
<LogoutButton />
<p>
Clicking the logout button will delete your data from your browser. You will then be able to make a new account, your old account will remain inactive and be deleted after 90
days.
</p>
<Loading />
{:else if data[0].domain != "User not Found"}
<p class="api_response">Registration Domain: {data[0]["domain"]}</p>
<p class="api_response">Access Time: {data[0]["atime"]}</p>
<LogoutButton />
<p>
Clicking the logout button will delete your data from your browser. You will then be able to make a new account, your old account will remain inactive and be deleted after
90 days.
</p>
{:else}
<p class="api_response">You are not registered, we don't have any data stored.</p>
<p class="api_response">You are not registered, we don't have any data stored.</p>
{/if}
<Nav />
<style>
p {
margin: 10px;
}
.api_response {
color: white;
}
p {
margin: 10px;
}
.api_response {
color: white;
}
</style>

View File

@ -1,16 +1,16 @@
<script>
import Header from '$lib/navigation/header.svelte';
import Nav from '$lib/navigation/nav.svelte';
import Header from "$lib/navigation/header.svelte";
import Nav from "$lib/navigation/nav.svelte";
</script>
<Header title={'Help'} />
<Header title={"Help"} />
<Nav />
<br /><br />
<p>
You can find extensive help and documentation at <a href="https://docs.owlboard.info">the documentation site</a>.
You can find extensive help and documentation at <a href="https://docs.owlboard.info">the documentation site</a>.
</p>
<p>
You can also use the <a href="https://www.facebook.com/owlboard.support">OwlBoard Support</a> page on Facebook.
You can also use the <a href="https://www.facebook.com/owlboard.support">OwlBoard Support</a> page on Facebook.
</p>
<br /><br />

View File

@ -1,57 +1,57 @@
<script>
import Header from '$lib/navigation/header.svelte';
import Nav from '$lib/navigation/nav.svelte';
const title = 'Privacy Policy';
import Header from "$lib/navigation/header.svelte";
import Nav from "$lib/navigation/nav.svelte";
const title = "Privacy Policy";
</script>
<Header {title} />
<div>
<p>
OwlBoard stores the minimum amount of data necessary to provide its functions for your use. No personal data is stored unless you report an issue. To review the specific data
that we store, please visit <a href="/more/data">My Data</a>.
</p>
<p>OwlBoard does not utilize any cookies. IP addresses and browser fingerprints are not logged.</p>
<h2>If You Do Not Sign Up</h2>
<p>
If you choose not to sign up, no personal data will be processed or stored unless you report an issue. Any personal settings are stored locally in your browser and do not leave
your device.
</p>
<h2>If You Sign Up</h2>
<p>
If you sign up for the rail staff version of OwlBoard, we do require the storage of some data. However, none of this data can be used to personally identify you. Any personal
settings are stored locally in your browser and do not leave your device.
</p>
<p>
During the sign-up process, you will be asked to provide a work email address, which will be checked to confirm its origin from a railway company. Once confirmed, an email
containing a registration link will be sent to you. At this point, the username portion of your email address is discarded. For example, if your email address is
'a-user@owlboard.info', only 'owlboard.info' will be stored. This host portion of your email address is stored to filter and display relevant results prominently.
</p>
<p>The email server may store the address and message content as part of its regular operation, and your consent to this is implied when you sign up.</p>
<p>In addition to the host portion of your email address, a randomly generated UUID is stored for the purpose of authorizing access to the rail staff data.</p>
<h2>Reporting an Issue</h2>
<p>When you report an issue, certain data is collected, including your browser's User Agent string and the size of the window in which you are viewing the website.</p>
<p>
Any data submitted when reporting an issue will be publicly viewable alongside the <a href="https://git.fjla.uk/owlboard/backend/issues" target="_blank"
>OwlBoard/backend git repository</a
>.
</p>
<p>
OwlBoard stores the minimum amount of data necessary to provide its functions for your use. No personal data is stored unless you report an issue. To review the specific
data that we store, please visit <a href="/more/data">My Data</a>.
</p>
<p>OwlBoard does not utilize any cookies. IP addresses and browser fingerprints are not logged.</p>
<h2>If You Do Not Sign Up</h2>
<p>
If you choose not to sign up, no personal data will be processed or stored unless you report an issue. Any personal settings are stored locally in your browser and do not
leave your device.
</p>
<h2>If You Sign Up</h2>
<p>
If you sign up for the rail staff version of OwlBoard, we do require the storage of some data. However, none of this data can be used to personally identify you. Any
personal settings are stored locally in your browser and do not leave your device.
</p>
<p>
During the sign-up process, you will be asked to provide a work email address, which will be checked to confirm its origin from a railway company. Once confirmed, an email
containing a registration link will be sent to you. At this point, the username portion of your email address is discarded. For example, if your email address is
'a-user@owlboard.info', only 'owlboard.info' will be stored. This host portion of your email address is stored to filter and display relevant results prominently.
</p>
<p>The email server may store the address and message content as part of its regular operation, and your consent to this is implied when you sign up.</p>
<p>In addition to the host portion of your email address, a randomly generated UUID is stored for the purpose of authorizing access to the rail staff data.</p>
<h2>Reporting an Issue</h2>
<p>When you report an issue, certain data is collected, including your browser's User Agent string and the size of the window in which you are viewing the website.</p>
<p>
Any data submitted when reporting an issue will be publicly viewable alongside the <a href="https://git.fjla.uk/owlboard/backend/issues" target="_blank"
>OwlBoard/backend git repository</a
>.
</p>
</div>
<Nav />
<style>
div {
text-align: left;
}
div {
text-align: left;
}
h2 {
color: var(--second-text-color);
margin: 10px;
padding-top: 20px;
}
h2 {
color: var(--second-text-color);
margin: 10px;
padding-top: 20px;
}
p {
color: white;
margin: 10px;
}
p {
color: white;
margin: 10px;
}
</style>

View File

@ -1,121 +1,121 @@
<script>
import { onMount } from 'svelte';
import Header from '$lib/navigation/header.svelte';
import Nav from '$lib/navigation/nav.svelte';
import Loading from '$lib/navigation/loading.svelte';
import ResultIsland from '$lib/islands/result-island.svelte';
import { getApiUrl } from '$lib/scripts/upstream';
import { onMount } from "svelte";
import Header from "$lib/navigation/header.svelte";
import Nav from "$lib/navigation/nav.svelte";
import Loading from "$lib/navigation/loading.svelte";
import ResultIsland from "$lib/islands/result-island.svelte";
import { getApiUrl } from "$lib/scripts/upstream";
const title = 'Reason Codes';
let isLoading = false;
let inputValue = '';
let resultObject = {
results: false,
title: '',
resultLines: []
};
function load() {
isLoading = true;
resultObject.results = false;
getData().then((result) => {
handleData(result);
isLoading = false;
});
}
async function getData() {
if (inputValue) {
const url = `${getApiUrl()}/api/v2/ref/reasonCode/${inputValue}`;
const res = await fetch(url);
return await res.json();
} else {
return [];
}
}
async function handleData(data) {
let resultLines = [];
if (data.length) {
resultObject.title = data[0]['code'];
resultLines = [data[0]['lateReason'], data[0]['cancReason']];
} else {
resultObject = {
const title = "Reason Codes";
let isLoading = false;
let inputValue = "";
let resultObject = {
results: false,
title: 'Not Found',
title: "",
resultLines: []
};
};
function load() {
isLoading = true;
resultObject.results = false;
getData().then((result) => {
handleData(result);
isLoading = false;
});
}
resultObject.resultLines = resultLines;
resultObject.results = true;
}
onMount(() => {
isLoading = false;
});
async function getData() {
if (inputValue) {
const url = `${getApiUrl()}/api/v2/ref/reasonCode/${inputValue}`;
const res = await fetch(url);
return await res.json();
} else {
return [];
}
}
function handleInput(event) {
inputValue = event.target.value;
}
async function handleData(data) {
let resultLines = [];
if (data.length) {
resultObject.title = data[0]["code"];
resultLines = [data[0]["lateReason"], data[0]["cancReason"]];
} else {
resultObject = {
results: false,
title: "Not Found",
resultLines: []
};
}
resultObject.resultLines = resultLines;
resultObject.results = true;
}
function handleSubmit(event) {
event.preventDefault();
load();
}
onMount(() => {
isLoading = false;
});
function handleInput(event) {
inputValue = event.target.value;
}
function handleSubmit(event) {
event.preventDefault();
load();
}
</script>
<Header {title} />
<p>A reason code is a three digit number that maps to a reason for a delay or cancellation</p>
<form on:submit={handleSubmit}>
<input type="text" placeholder="Enter Code" autocomplete="off" bind:value={inputValue} on:input={handleInput} />
<input type="text" placeholder="Enter Code" autocomplete="off" bind:value={inputValue} on:input={handleInput} />
<br />
<br />
<button type="submit">Submit</button>
<button type="submit">Submit</button>
</form>
{#if isLoading}
<Loading />
<Loading />
{/if}
{#if resultObject.results}
<ResultIsland {resultObject} />
<ResultIsland {resultObject} />
{/if}
<Nav />
<style>
p {
margin: 10px;
margin-top: 20px;
}
input {
width: 25%;
min-width: 150px;
height: 32px;
margin-top: 10px;
margin-bottom: 5px;
border-radius: 50px;
border: none;
text-align: center;
font-family: urwgothic, 'Lucida Sans', 'Lucida Sans Regular', 'Lucida Grande', 'Lucida Sans Unicode', Geneva, Verdana, sans-serif;
text-transform: uppercase;
font-size: 15px;
box-shadow: var(--box-shadow);
}
button {
width: 15%;
min-width: 100px;
margin-bottom: 5px;
margin-top: 5px;
border: none;
border-radius: 20px;
padding: 5px;
font-family: urwgothic, 'Lucida Sans', 'Lucida Sans Regular', 'Lucida Grande', 'Lucida Sans Unicode', Geneva, Verdana, sans-serif;
font-size: 16px;
font-weight: 400;
background-color: var(--island-bg-color);
color: var(--main-text-color);
box-shadow: var(--box-shadow);
}
p {
margin: 10px;
margin-top: 20px;
}
input {
width: 25%;
min-width: 150px;
height: 32px;
margin-top: 10px;
margin-bottom: 5px;
border-radius: 50px;
border: none;
text-align: center;
font-family: urwgothic, "Lucida Sans", "Lucida Sans Regular", "Lucida Grande", "Lucida Sans Unicode", Geneva, Verdana, sans-serif;
text-transform: uppercase;
font-size: 15px;
box-shadow: var(--box-shadow);
}
button {
width: 15%;
min-width: 100px;
margin-bottom: 5px;
margin-top: 5px;
border: none;
border-radius: 20px;
padding: 5px;
font-family: urwgothic, "Lucida Sans", "Lucida Sans Regular", "Lucida Grande", "Lucida Sans Unicode", Geneva, Verdana, sans-serif;
font-size: 16px;
font-weight: 400;
background-color: var(--island-bg-color);
color: var(--main-text-color);
box-shadow: var(--box-shadow);
}
</style>

View File

@ -1,144 +1,144 @@
<script lang="ts">
import Header from '$lib/navigation/header.svelte';
import Nav from '$lib/navigation/nav.svelte';
import Loading from '$lib/navigation/loading.svelte';
import { onMount } from 'svelte';
import { checkAuth } from '$lib/libauth';
import LogoutButton from '$lib/navigation/LogoutButton.svelte';
import { getApiUrl } from '$lib/scripts/upstream';
import Header from "$lib/navigation/header.svelte";
import Nav from "$lib/navigation/nav.svelte";
import Loading from "$lib/navigation/loading.svelte";
import { onMount } from "svelte";
import { checkAuth } from "$lib/libauth";
import LogoutButton from "$lib/navigation/LogoutButton.svelte";
import { getApiUrl } from "$lib/scripts/upstream";
const title = 'Register';
const title = "Register";
let state = 'unreg';
let isLoading = true;
let inputValue = '';
let state = "unreg";
let isLoading = true;
let inputValue = "";
function handleInput(event: KeyboardEvent) {
inputValue = event?.target?.value;
}
async function request() {
isLoading = true;
const url = `${getApiUrl()}/api/v2/user/request`;
const request = {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
email: inputValue
})
};
const res = await fetch(url, request);
if (res.status == 400 || res.status == 403) {
state = 'unauth';
} else if (res.status == 201) {
state = 'sent';
} else {
state = 'error';
function handleInput(event: KeyboardEvent) {
inputValue = event?.target?.value;
}
isLoading = false;
}
onMount(async () => {
const auth = await checkAuth();
if (auth.uuidPresent === false) {
state = 'unreg';
} else if (auth.uuidPresent === true) {
state = 'reg';
async function request() {
isLoading = true;
const url = `${getApiUrl()}/api/v2/user/request`;
const request = {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
email: inputValue
})
};
const res = await fetch(url, request);
if (res.status == 400 || res.status == 403) {
state = "unauth";
} else if (res.status == 201) {
state = "sent";
} else {
state = "error";
}
isLoading = false;
}
isLoading = false;
});
onMount(async () => {
const auth = await checkAuth();
if (auth.uuidPresent === false) {
state = "unreg";
} else if (auth.uuidPresent === true) {
state = "reg";
}
isLoading = false;
});
</script>
<Header {title} />
<section class="content">
{#if isLoading}
<Loading />
{:else if state == 'unreg'}
<p>To register, you will need to enter a work email address to receive a confirmation email</p>
<p class="bold">Already have a registration code? <a href="/more/reg/submit">enter it here</a></p>
<form on:submit={request}>
<input type="text" autocomplete="email" placeholder="Enter work email" bind:value={inputValue} on:input={handleInput} /><br />
<label for="checkbox">
I have read and accept the terms of the <a href="/more/privacy">Privacy Policy</a><br />
<input id="checkbox" type="checkbox" required />
</label><br />
<button type="submit">Submit</button>
</form>
<br />
<p class="bold">What do you get?</p>
<li>Access to Train details</li>
<li>Access to PIS Codes</li>
<li>ECS Movements on departure boards</li>
<li>Non-Public trains on departure boards</li>
<li>Hidden platform numbers on departure boards</li>
<li>See up to the next 40 trains departing a station over the next two hours</li>
{:else if state == 'sent'}
<p>An email has been sent, enter the code in the email to activate your profile.</p>
<p class="bold"><a href="/more/reg/submit">Ready to enter your code?</a></p>
<p>If you use multiple browsers, you will only be logged in using the browser you open the link with.</p>
<p>You will be able to register again using the same email address</p>
{:else if state == 'unauth'}
<p>The email address you entered does not belong to an authorised business.</p>
<p>If you think this is an error, you can get help on <a href="/more/help">the help page</a>.</p>
{:else if state == 'error'}
<p>There was an error processing your request.</p>
<p>Check that the email you entered was correct or try again later.</p>
{:else if state == 'reg'}
<p>
You are already registered for OwlBoard. If you've recently logged out or updated, you may need to refresh this page to register as old data could still be stored in your
browser.
</p>
<LogoutButton />
{/if}
{#if isLoading}
<Loading />
{:else if state == "unreg"}
<p>To register, you will need to enter a work email address to receive a confirmation email</p>
<p class="bold">Already have a registration code? <a href="/more/reg/submit">enter it here</a></p>
<form on:submit={request}>
<input type="text" autocomplete="email" placeholder="Enter work email" bind:value={inputValue} on:input={handleInput} /><br />
<label for="checkbox">
I have read and accept the terms of the <a href="/more/privacy">Privacy Policy</a><br />
<input id="checkbox" type="checkbox" required />
</label><br />
<button type="submit">Submit</button>
</form>
<br />
<p class="bold">What do you get?</p>
<li>Access to Train details</li>
<li>Access to PIS Codes</li>
<li>ECS Movements on departure boards</li>
<li>Non-Public trains on departure boards</li>
<li>Hidden platform numbers on departure boards</li>
<li>See up to the next 40 trains departing a station over the next two hours</li>
{:else if state == "sent"}
<p>An email has been sent, enter the code in the email to activate your profile.</p>
<p class="bold"><a href="/more/reg/submit">Ready to enter your code?</a></p>
<p>If you use multiple browsers, you will only be logged in using the browser you open the link with.</p>
<p>You will be able to register again using the same email address</p>
{:else if state == "unauth"}
<p>The email address you entered does not belong to an authorised business.</p>
<p>If you think this is an error, you can get help on <a href="/more/help">the help page</a>.</p>
{:else if state == "error"}
<p>There was an error processing your request.</p>
<p>Check that the email you entered was correct or try again later.</p>
{:else if state == "reg"}
<p>
You are already registered for OwlBoard. If you've recently logged out or updated, you may need to refresh this page to register as old data could still be stored in
your browser.
</p>
<LogoutButton />
{/if}
</section>
<Nav />
<style>
.bold {
font-weight: 800;
}
.content {
margin-top: 30px;
}
p {
margin: 10px;
margin-left: 20px;
margin-right: 20px;
}
li {
text-align: center;
font-size: 14px;
color: white;
margin-top: 5px;
margin-left: 15px;
margin-right: 15px;
list-style-type: none;
}
input {
height: 40px;
width: 80%;
max-width: 375px;
font-family: urwgothic, 'Franklin Gothic Medium', 'Arial Narrow', Arial, sans-serif;
text-align: center;
font-size: 20px;
border-radius: 50px;
border: none;
margin-bottom: 20px;
}
#checkbox {
height: 30px;
width: 30px;
}
button {
border: none;
background-color: var(--island-bg-color);
color: var(--main-text-color);
width: 35%;
height: 30px;
border-radius: 50px;
font-size: 18px;
}
.bold {
font-weight: 800;
}
.content {
margin-top: 30px;
}
p {
margin: 10px;
margin-left: 20px;
margin-right: 20px;
}
li {
text-align: center;
font-size: 14px;
color: white;
margin-top: 5px;
margin-left: 15px;
margin-right: 15px;
list-style-type: none;
}
input {
height: 40px;
width: 80%;
max-width: 375px;
font-family: urwgothic, "Franklin Gothic Medium", "Arial Narrow", Arial, sans-serif;
text-align: center;
font-size: 20px;
border-radius: 50px;
border: none;
margin-bottom: 20px;
}
#checkbox {
height: 30px;
width: 30px;
}
button {
border: none;
background-color: var(--island-bg-color);
color: var(--main-text-color);
width: 35%;
height: 30px;
border-radius: 50px;
font-size: 18px;
}
</style>

View File

@ -1,128 +1,128 @@
<script lang="ts">
import Header from '$lib/navigation/header.svelte';
import Nav from '$lib/navigation/nav.svelte';
import { getApiUrl } from '$lib/scripts/upstream';
import { uuid } from '$lib/stores/uuid';
import Header from "$lib/navigation/header.svelte";
import Nav from "$lib/navigation/nav.svelte";
import { getApiUrl } from "$lib/scripts/upstream";
import { uuid } from "$lib/stores/uuid";
const title = 'Submit Registration';
let state = false;
let status: string;
const title = "Submit Registration";
let state = false;
let status: string;
let inputs: { id: string; value: string }[] = [
{ id: '1', value: '' },
{ id: '2', value: '' },
{ id: '3', value: '' },
{ id: '4', value: '' },
{ id: '5', value: '' },
{ id: '6', value: '' }
];
let inputs: { id: string; value: string }[] = [
{ id: "1", value: "" },
{ id: "2", value: "" },
{ id: "3", value: "" },
{ id: "4", value: "" },
{ id: "5", value: "" },
{ id: "6", value: "" }
];
function handleInput(index: number, event: KeyboardEvent): void {
if (event.key === 'Backspace' && index > 0 && inputs[index].value === '') {
const prevInput = document.getElementById(`input-${index}`);
if (prevInput) {
prevInput.focus();
}
} else if (inputs[index].value.length === 1) {
const nextInput = document.getElementById(`input-${index + 2}`);
if (nextInput) {
nextInput.focus();
}
function handleInput(index: number, event: KeyboardEvent): void {
if (event.key === "Backspace" && index > 0 && inputs[index].value === "") {
const prevInput = document.getElementById(`input-${index}`);
if (prevInput) {
prevInput.focus();
}
} else if (inputs[index].value.length === 1) {
const nextInput = document.getElementById(`input-${index + 2}`);
if (nextInput) {
nextInput.focus();
}
}
}
}
async function handleSubmit() {
let submitString: string = '';
for (const input of inputs) {
submitString += input.value.toUpperCase();
async function handleSubmit() {
let submitString: string = "";
for (const input of inputs) {
submitString += input.value.toUpperCase();
}
console.log(`Code: ${submitString}`);
const res = await submit(submitString);
console.log(`Registration Status: ${res}`);
if (res == 201) {
status = "okay";
} else if (res == 401) {
status = "fail";
} else {
console.error("Unable to register: ", status);
}
state = true;
}
console.log(`Code: ${submitString}`);
const res = await submit(submitString);
console.log(`Registration Status: ${res}`);
if (res == 201) {
status = 'okay';
} else if (res == 401) {
status = 'fail';
} else {
console.error('Unable to register: ', status);
}
state = true;
}
async function submit(id: string): Promise<number> {
const url = `${getApiUrl()}/api/v2/user/register`;
const request = {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
uuid: id
})
};
const res = await fetch(url, request);
const body = await res.json();
if (body.api_key) {
uuid.set(body.api_key);
return 201;
} else {
return res.status;
async function submit(id: string): Promise<number> {
const url = `${getApiUrl()}/api/v2/user/register`;
const request = {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
uuid: id
})
};
const res = await fetch(url, request);
const body = await res.json();
if (body.api_key) {
uuid.set(body.api_key);
return 201;
} else {
return res.status;
}
}
}
</script>
<Header {title} />
{#if state}
{#if status == 'okay'}
<p class="title-ish">You are now registered</p>
<p>Your secret key will be stored in your browser.</p>
<p>If you change browsers, change device or clear your browsing data, you may have to register again.</p>
{:else if status == 'fail'}
<p class="title-ish">Your code was not accepted</p>
<p>The code expires after 1 hour, you can check the code and enter it again or request a <a href="/more/reg">new code</a>.</p>
{/if}
{#if status == "okay"}
<p class="title-ish">You are now registered</p>
<p>Your secret key will be stored in your browser.</p>
<p>If you change browsers, change device or clear your browsing data, you may have to register again.</p>
{:else if status == "fail"}
<p class="title-ish">Your code was not accepted</p>
<p>The code expires after 1 hour, you can check the code and enter it again or request a <a href="/more/reg">new code</a>.</p>
{/if}
{:else}
<p class="title-ish">Enter your registration code below</p>
<form on:submit={handleSubmit} id="codeInputForm">
{#each inputs as input, index}
<input class="code-in" bind:value={input.value} id={`input-${input.id}`} maxlength="1" autocomplete="off" on:keydown={(event) => handleInput(index, event)} />
{/each}
<br />
<button type="submit">Submit</button>
</form>
<p class="title-ish">Enter your registration code below</p>
<form on:submit={handleSubmit} id="codeInputForm">
{#each inputs as input, index}
<input class="code-in" bind:value={input.value} id={`input-${input.id}`} maxlength="1" autocomplete="off" on:keydown={(event) => handleInput(index, event)} />
{/each}
<br />
<button type="submit">Submit</button>
</form>
{/if}
<Nav />
<style>
.title-ish {
font-size: 20px;
}
.code-in {
margin: 3px;
margin-bottom: 20px;
width: 29px;
height: 39px;
font-size: 30px;
text-align: center;
text-transform: uppercase;
border-radius: 10px;
border: none;
}
button {
border: none;
background-color: var(--island-bg-color);
color: var(--main-text-color);
width: 35%;
min-width: 95px;
max-width: 231px;
height: 30px;
border-radius: 50px;
font-size: 18px;
}
p {
margin-left: 40px;
margin-right: 40px;
}
.title-ish {
font-size: 20px;
}
.code-in {
margin: 3px;
margin-bottom: 20px;
width: 29px;
height: 39px;
font-size: 30px;
text-align: center;
text-transform: uppercase;
border-radius: 10px;
border: none;
}
button {
border: none;
background-color: var(--island-bg-color);
color: var(--main-text-color);
width: 35%;
min-width: 95px;
max-width: 231px;
height: 30px;
border-radius: 50px;
font-size: 18px;
}
p {
margin-left: 40px;
margin-right: 40px;
}
</style>

View File

@ -1,188 +1,188 @@
<script>
import Header from '$lib/navigation/header.svelte';
import Island from '$lib/islands/island.svelte';
import Nav from '$lib/navigation/nav.svelte';
import { onMount } from 'svelte';
import Loading from '$lib/navigation/loading.svelte';
import Done from '$lib/navigation/done.svelte';
import { getApiUrl } from '$lib/scripts/upstream';
import Header from "$lib/navigation/header.svelte";
import Island from "$lib/islands/island.svelte";
import Nav from "$lib/navigation/nav.svelte";
import { onMount } from "svelte";
import Loading from "$lib/navigation/loading.svelte";
import Done from "$lib/navigation/done.svelte";
import { getApiUrl } from "$lib/scripts/upstream";
const title = 'Report Issue';
let isLoading = false;
let isDone = false;
let isError = false;
const title = "Report Issue";
let isLoading = false;
let isDone = false;
let isError = false;
let reportType = '',
reportSubject = '',
reportMsg = '',
reportCollected;
onMount(async () => {
reportCollected = {
userAgent: navigator.userAgent,
browser: navigator.appName,
version: navigator.appVersion,
platform: navigator.platform,
viewport: `${window.innerWidth} x ${window.innerHeight}`
};
});
let preFlight = false;
async function submit() {
console.log(reportType, reportSubject, reportMsg);
preFlight = true;
}
async function send() {
console.log('SEND DATA REQUESTED');
isLoading = true;
const formData = JSON.stringify({
label: reportType,
subject: reportSubject,
msg:
`User Agent: ${reportCollected.userAgent}\n` +
`Browser: ${reportCollected.browser}\n` +
`BrowserVersion: ${reportCollected.version}\n` +
`Platform: ${reportCollected.platform}\n` +
`Viewport: ${reportCollected.viewport}\n\n\n` +
`User Message:\n` +
`${reportMsg}`
let reportType = "",
reportSubject = "",
reportMsg = "",
reportCollected;
onMount(async () => {
reportCollected = {
userAgent: navigator.userAgent,
browser: navigator.appName,
version: navigator.appVersion,
platform: navigator.platform,
viewport: `${window.innerWidth} x ${window.innerHeight}`
};
});
const url = `${getApiUrl()}/misc/issue`;
const options = {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: formData
};
const res = await fetch(url, options);
if (res.status == 200) {
isLoading = false;
isDone = true;
await new Promise((r) => setTimeout(r, 2000));
window.location.href = '/';
} else {
isLoading = false;
isError = true;
}
}
async function cancel() {
preFlight = false;
isLoading = false;
isDone = false;
isError = false;
}
let preFlight = false;
async function submit() {
console.log(reportType, reportSubject, reportMsg);
preFlight = true;
}
async function send() {
console.log("SEND DATA REQUESTED");
isLoading = true;
const formData = JSON.stringify({
label: reportType,
subject: reportSubject,
msg:
`User Agent: ${reportCollected.userAgent}\n` +
`Browser: ${reportCollected.browser}\n` +
`BrowserVersion: ${reportCollected.version}\n` +
`Platform: ${reportCollected.platform}\n` +
`Viewport: ${reportCollected.viewport}\n\n\n` +
`User Message:\n` +
`${reportMsg}`
});
const url = `${getApiUrl()}/misc/issue`;
const options = {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: formData
};
const res = await fetch(url, options);
if (res.status == 200) {
isLoading = false;
isDone = true;
await new Promise((r) => setTimeout(r, 2000));
window.location.href = "/";
} else {
isLoading = false;
isError = true;
}
}
async function cancel() {
preFlight = false;
isLoading = false;
isDone = false;
isError = false;
}
</script>
<Header {title} />
{#if isLoading}
<Loading />
<Loading />
{/if}
{#if isDone}
<Done />
<Done />
{/if}
{#if !preFlight && !isDone}
<p>Get help on the <a href="https://www.facebook.com/owlboard.support">OwlBoard Support Facebook Page</a></p>
<p>
Or submit an issue, you can check for existing reports
<a href="https://git.fjla.uk/OwlBoard/backend/issues" target="_blank">here</a>
before submitting a new issue.
</p>
<p>You will be shown all of the collected data before the form is submitted.</p>
<form on:submit={submit}>
<select class="formInputs" name="type" bind:value={reportType} placeholder="Choose Category">
<option value="" disabled selected>Choose an Issue Type</option>
<option value="bug">Problem</option>
<option value="enhancement">Feature Request</option>
<option value="question">Question</option>
<option value="user-support">Unable to sign up</option>
</select>
<br />
<input class="formInputs" type="text" bind:value={reportSubject} placeholder="Subject" />
<br />
<textarea class="formInputs" bind:value={reportMsg} placeholder="Enter your message..." />
<br />
<button type="submit">Submit</button>
<button type="reset">Reset</button>
</form>
{:else}
<Island>
<h2>Device Data:</h2>
<p><span class="dataType">User Agent: </span>{reportCollected.userAgent}</p>
<p>Get help on the <a href="https://www.facebook.com/owlboard.support">OwlBoard Support Facebook Page</a></p>
<p>
<span class="dataType">Browser: </span>{reportCollected.browser} - {reportCollected.version}
Or submit an issue, you can check for existing reports
<a href="https://git.fjla.uk/OwlBoard/backend/issues" target="_blank">here</a>
before submitting a new issue.
</p>
<p><span class="dataType">Platform: </span>{reportCollected.platform}</p>
<p><span class="dataType">Viewport: </span> {reportCollected.viewport}</p>
<h2>Reported Data:</h2>
<p><span class="dataType">Report Type: </span>{reportType}</p>
<p>{reportSubject}</p>
<p>{reportMsg}</p>
<button class="overlayButtons" on:click={send}>Send</button>
<button class="overlayButtons" on:click={cancel}>Cancel</button>
</Island>
<p>You will be shown all of the collected data before the form is submitted.</p>
<form on:submit={submit}>
<select class="formInputs" name="type" bind:value={reportType} placeholder="Choose Category">
<option value="" disabled selected>Choose an Issue Type</option>
<option value="bug">Problem</option>
<option value="enhancement">Feature Request</option>
<option value="question">Question</option>
<option value="user-support">Unable to sign up</option>
</select>
<br />
<input class="formInputs" type="text" bind:value={reportSubject} placeholder="Subject" />
<br />
<textarea class="formInputs" bind:value={reportMsg} placeholder="Enter your message..." />
<br />
<button type="submit">Submit</button>
<button type="reset">Reset</button>
</form>
{:else}
<Island>
<h2>Device Data:</h2>
<p><span class="dataType">User Agent: </span>{reportCollected.userAgent}</p>
<p>
<span class="dataType">Browser: </span>{reportCollected.browser} - {reportCollected.version}
</p>
<p><span class="dataType">Platform: </span>{reportCollected.platform}</p>
<p><span class="dataType">Viewport: </span> {reportCollected.viewport}</p>
<h2>Reported Data:</h2>
<p><span class="dataType">Report Type: </span>{reportType}</p>
<p>{reportSubject}</p>
<p>{reportMsg}</p>
<button class="overlayButtons" on:click={send}>Send</button>
<button class="overlayButtons" on:click={cancel}>Cancel</button>
</Island>
{/if}
<Nav />
<style>
p {
margin-left: 10px;
margin-right: 10px;
}
select {
text-align: center;
border: none;
border-radius: 50px;
height: 30px;
background-color: white;
}
.formInputs {
margin: 10px;
font-family: urwgothic, sans-serif;
font-size: 16px;
border: none;
width: 50%;
max-width: 450px;
min-width: 250px;
}
input {
text-align: center;
border-radius: 50px;
height: 30px;
}
textarea {
text-align: left;
border-radius: 10px;
padding-left: 5px;
padding-right: 5px;
height: 30vh;
min-height: 150px;
max-height: 400px;
}
button {
background-color: var(--island-bg-color);
color: var(--main-text-color);
font-family: urwgothic, sans-serif;
font-size: 16px;
border: none;
border-radius: 50px;
width: 25%;
height: 30px;
max-width: 100px;
box-shadow: var(--box-shadow);
}
h2 {
color: white;
}
.dataType {
color: white;
}
.overlayButtons {
background-color: var(--main-bg-color);
}
p {
margin-left: 10px;
margin-right: 10px;
}
select {
text-align: center;
border: none;
border-radius: 50px;
height: 30px;
background-color: white;
}
.formInputs {
margin: 10px;
font-family: urwgothic, sans-serif;
font-size: 16px;
border: none;
width: 50%;
max-width: 450px;
min-width: 250px;
}
input {
text-align: center;
border-radius: 50px;
height: 30px;
}
textarea {
text-align: left;
border-radius: 10px;
padding-left: 5px;
padding-right: 5px;
height: 30vh;
min-height: 150px;
max-height: 400px;
}
button {
background-color: var(--island-bg-color);
color: var(--main-text-color);
font-family: urwgothic, sans-serif;
font-size: 16px;
border: none;
border-radius: 50px;
width: 25%;
height: 30px;
max-width: 100px;
box-shadow: var(--box-shadow);
}
h2 {
color: white;
}
.dataType {
color: white;
}
.overlayButtons {
background-color: var(--main-bg-color);
}
</style>

View File

@ -1,8 +1,8 @@
<script>
import Header from '$lib/navigation/header.svelte';
import Nav from '$lib/navigation/nav.svelte';
import QlSet from '$lib/islands/quick-link-set-island.svelte';
const title = 'Settings';
import Header from "$lib/navigation/header.svelte";
import Nav from "$lib/navigation/nav.svelte";
import QlSet from "$lib/islands/quick-link-set-island.svelte";
const title = "Settings";
</script>
<Header {title} />

View File

@ -1,79 +1,79 @@
<script>
import Island from '$lib/islands/island.svelte';
import Header from '$lib/navigation/header.svelte';
import Loading from '$lib/navigation/loading.svelte';
import Nav from '$lib/navigation/nav.svelte';
import { getApiUrl } from '$lib/scripts/upstream';
const title = 'Statistics';
import Island from "$lib/islands/island.svelte";
import Header from "$lib/navigation/header.svelte";
import Loading from "$lib/navigation/loading.svelte";
import Nav from "$lib/navigation/nav.svelte";
import { getApiUrl } from "$lib/scripts/upstream";
const title = "Statistics";
async function getData() {
const url = `${getApiUrl()}/misc/server/stats`;
const res = await fetch(url);
return await res.json();
}
function U2L(input) {
try {
const datetime = new Date(input * 1000);
return datetime.toLocaleString();
} catch (err) {
console.log(err);
return false;
async function getData() {
const url = `${getApiUrl()}/misc/server/stats`;
const res = await fetch(url);
return await res.json();
}
function U2L(input) {
try {
const datetime = new Date(input * 1000);
return datetime.toLocaleString();
} catch (err) {
console.log(err);
return false;
}
}
}
</script>
<Header {title} />
{#await getData()}
<Loading />
<Loading />
{:then data}
<br />
<p>API Server:<br /><span>{data?.hostname}</span></p>
<p>Runtime Mode: <span>{data?.runtimeMode}</span></p>
<p>Stats Reset: <span>{U2L(data?.reset) || 'Unknown'}</span></p>
<h2>Last Update</h2>
<p>Timetable: <span>{U2L(data?.updateTimes?.timetable)}</span></p>
<p>Location Ref: <span>{U2L(data?.updateTimes?.corpus)}</span></p>
<p>PIS Codes: <span>{U2L(data?.updateTimes?.pis)}</span></p>
<p>Reason Codes: <span>{U2L(data?.updateTimes?.reasonCodes)}</span></p>
<br />
<p>API Server:<br /><span>{data?.hostname}</span></p>
<p>Runtime Mode: <span>{data?.runtimeMode}</span></p>
<p>Stats Reset: <span>{U2L(data?.reset) || "Unknown"}</span></p>
<h2>Last Update</h2>
<p>Timetable: <span>{U2L(data?.updateTimes?.timetable)}</span></p>
<p>Location Ref: <span>{U2L(data?.updateTimes?.corpus)}</span></p>
<p>PIS Codes: <span>{U2L(data?.updateTimes?.pis)}</span></p>
<p>Reason Codes: <span>{U2L(data?.updateTimes?.reasonCodes)}</span></p>
<h2>Request Counts</h2>
<p>LDBWS API: <span>{data?.requestCounts?.ldbws_api}</span></p>
<p>LDBSVWS API: <span>{data?.requestCounts?.lsbsvws_api}</span></p>
<p>Location Reference API: <span>{data?.requestCounts?.corpus_api}</span></p>
<p>Timetable: <span>{data?.requestCounts?.timetable_db}</span></p>
<p>PIS: <span>{data?.requestCounts?.pis_db}</span></p>
<p>Location Reference: <span>{data?.requestCounts?.corpus_db}</span></p>
<p>Stations: <span>{data?.requestCounts?.stations_db}</span></p>
<h2>Request Counts</h2>
<p>LDBWS API: <span>{data?.requestCounts?.ldbws_api}</span></p>
<p>LDBSVWS API: <span>{data?.requestCounts?.lsbsvws_api}</span></p>
<p>Location Reference API: <span>{data?.requestCounts?.corpus_api}</span></p>
<p>Timetable: <span>{data?.requestCounts?.timetable_db}</span></p>
<p>PIS: <span>{data?.requestCounts?.pis_db}</span></p>
<p>Location Reference: <span>{data?.requestCounts?.corpus_db}</span></p>
<p>Stations: <span>{data?.requestCounts?.stations_db}</span></p>
<h2>Database Lengths</h2>
<p>Users: <span>{data?.dbLengths?.users}</span></p>
<p>Pending Registrations: <span>{data?.dbLengths?.registrations}</span></p>
<p>CORPUS: <span>{data?.dbLengths?.corpus}</span></p>
<p>Stations: <span>{data?.dbLengths?.stations}</span></p>
<p>PIS: <span>{data?.dbLengths?.pis}</span></p>
<p>Timetable: <span>{data?.dbLengths?.timetable}</span></p>
<p>Reason Codes: <span>{data?.dbLengths?.reasonCodes}</span></p>
<h2>Database Lengths</h2>
<p>Users: <span>{data?.dbLengths?.users}</span></p>
<p>Pending Registrations: <span>{data?.dbLengths?.registrations}</span></p>
<p>CORPUS: <span>{data?.dbLengths?.corpus}</span></p>
<p>Stations: <span>{data?.dbLengths?.stations}</span></p>
<p>PIS: <span>{data?.dbLengths?.pis}</span></p>
<p>Timetable: <span>{data?.dbLengths?.timetable}</span></p>
<p>Reason Codes: <span>{data?.dbLengths?.reasonCodes}</span></p>
{:catch}
<Island>
<p style="font-weight:600">Unable to connect to server</p>
</Island>
<Island>
<p style="font-weight:600">Unable to connect to server</p>
</Island>
{/await}
<Nav />
<style>
span {
color: white;
}
p {
margin: 0;
color: lightgray;
}
h2 {
font-family: urwgothic, sans-serif;
margin-bottom: 2px;
margin-top: 8px;
}
span {
color: white;
}
p {
margin: 0;
color: lightgray;
}
h2 {
font-family: urwgothic, sans-serif;
margin-bottom: 2px;
margin-top: 8px;
}
</style>

View File

@ -1,20 +1,20 @@
<script>
import Header from '$lib/navigation/header.svelte';
import Island from '$lib/islands/island.svelte';
import Loading from '$lib/navigation/loading.svelte';
import Nav from '$lib/navigation/nav.svelte';
import LargeLogo from '$lib/images/large-logo.svelte';
import { version, versionTag } from '$lib/stores/version';
import { getApiUrl } from '$lib/scripts/upstream';
import { IconBrandGolang, IconBrandJavascript, IconBrandNodejs, IconBrandPython, IconBrandSvelte, IconBrandTypescript, IconFileTypeSql } from '@tabler/icons-svelte';
import Tooltip from '$lib/Tooltip.svelte';
const title = 'Versions';
import Header from "$lib/navigation/header.svelte";
import Island from "$lib/islands/island.svelte";
import Loading from "$lib/navigation/loading.svelte";
import Nav from "$lib/navigation/nav.svelte";
import LargeLogo from "$lib/images/large-logo.svelte";
import { version, versionTag } from "$lib/stores/version";
import { getApiUrl } from "$lib/scripts/upstream";
import { IconBrandGolang, IconBrandJavascript, IconBrandNodejs, IconBrandPython, IconBrandSvelte, IconBrandTypescript, IconFileTypeSql } from "@tabler/icons-svelte";
import Tooltip from "$lib/Tooltip.svelte";
const title = "Versions";
async function getData() {
const url = `${getApiUrl()}/misc/server/versions`;
const res = await fetch(url);
return await res.json();
}
async function getData() {
const url = `${getApiUrl()}/misc/server/versions`;
const res = await fetch(url);
return await res.json();
}
</script>
<Header {title} />
@ -22,58 +22,60 @@
<LargeLogo />
{#await getData()}
<Loading />
<Loading />
{:then data}
<Island>
<p>
<Tooltip text="Svelte"><IconBrandSvelte /></Tooltip>
<Tooltip text="Javascript"><IconBrandJavascript /></Tooltip>
<Tooltip text="Typescript"><IconBrandTypescript /></Tooltip>
<br />
<a class="data" href="https://git.fjla.uk/owlboard/owlboard-svelte" target="_blank"
>Web-app version<br /><span class="data"
>{version}{#if versionTag}-{versionTag}{/if}</span
></a
>
</p>
<p>
<Tooltip text="NodeJS"><IconBrandNodejs /></Tooltip>
<Tooltip text="Javascript"><IconBrandJavascript /></Tooltip>
<Tooltip text="Typescript"><IconBrandTypescript /></Tooltip>
<br />
<a class="data" href="https://git.fjla.uk/owlboard/backend" target="_blank">API Server version<br /><span class="data">{data?.backend || 'Unknown'}</span></a>
</p>
<p>
<Tooltip text="Python"><IconBrandPython /></Tooltip>
<br />
<a class="data" href="https://git.fjla.uk/owlboard/db-manager" target="_blank">DB Manager version<br /><span class="data">{data?.['db-manager'] || 'Unknown'}</span></a>
</p>
<p>
<Tooltip text="Go"><IconBrandGolang /></Tooltip>
<br />
<a class="data" href="https://git.fjla.uk/owlboard/mq-client" target="_blank">MQ Client version<br /><span class="data">{data?.['mq-client'] || 'Not installed'}</span></a>
</p>
</Island>
<Island>
<p>
<Tooltip text="Svelte"><IconBrandSvelte /></Tooltip>
<Tooltip text="Javascript"><IconBrandJavascript /></Tooltip>
<Tooltip text="Typescript"><IconBrandTypescript /></Tooltip>
<br />
<a class="data" href="https://git.fjla.uk/owlboard/owlboard-svelte" target="_blank"
>Web-app version<br /><span class="data"
>{version}{#if versionTag}-{versionTag}{/if}</span
></a
>
</p>
<p>
<Tooltip text="NodeJS"><IconBrandNodejs /></Tooltip>
<Tooltip text="Javascript"><IconBrandJavascript /></Tooltip>
<Tooltip text="Typescript"><IconBrandTypescript /></Tooltip>
<br />
<a class="data" href="https://git.fjla.uk/owlboard/backend" target="_blank">API Server version<br /><span class="data">{data?.backend || "Unknown"}</span></a>
</p>
<p>
<Tooltip text="Python"><IconBrandPython /></Tooltip>
<br />
<a class="data" href="https://git.fjla.uk/owlboard/db-manager" target="_blank">DB Manager version<br /><span class="data">{data?.["db-manager"] || "Unknown"}</span></a>
</p>
<p>
<Tooltip text="Go"><IconBrandGolang /></Tooltip>
<br />
<a class="data" href="https://git.fjla.uk/owlboard/mq-client" target="_blank"
>MQ Client version<br /><span class="data">{data?.["mq-client"] || "Not installed"}</span></a
>
</p>
</Island>
{:catch}
<Island>
<p>
<IconBrandSvelte /><IconBrandJavascript /><IconBrandTypescript /><br />
Web-app Version<br /><span class="data"
>{version}{#if versionTag}-{versionTag}{/if}</span
>
</p>
<p>Unable to fetch server application versions</p>
</Island>
<Island>
<p>
<IconBrandSvelte /><IconBrandJavascript /><IconBrandTypescript /><br />
Web-app Version<br /><span class="data"
>{version}{#if versionTag}-{versionTag}{/if}</span
>
</p>
<p>Unable to fetch server application versions</p>
</Island>
{/await}
<Nav />
<style>
p {
text-decoration: none;
padding: 15px;
}
.data {
color: white;
text-decoration: none;
}
p {
text-decoration: none;
padding: 15px;
}
.data {
color: white;
text-decoration: none;
}
</style>

View File

@ -1,109 +1,109 @@
<script>
import Header from '$lib/navigation/header.svelte';
import Nav from '$lib/navigation/nav.svelte';
import Island from '$lib/islands/island.svelte';
import Loading from '$lib/navigation/loading.svelte';
import { uuid } from '$lib/stores/uuid';
import StylesToc from '$lib/train/styles-toc.svelte';
import { getApiUrl } from '$lib/scripts/upstream';
import Header from "$lib/navigation/header.svelte";
import Nav from "$lib/navigation/nav.svelte";
import Island from "$lib/islands/island.svelte";
import Loading from "$lib/navigation/loading.svelte";
import { uuid } from "$lib/stores/uuid";
import StylesToc from "$lib/train/styles-toc.svelte";
import { getApiUrl } from "$lib/scripts/upstream";
const title = 'PIS Finder';
const variables = { title: 'Results' };
let entryPIS = '';
let entryStartCRS = '';
let entryEndCRS = '';
let data = [];
let error = false;
let errMsg = 'Unknown Error';
let isLoading = false;
const title = "PIS Finder";
const variables = { title: "Results" };
let entryPIS = "";
let entryStartCRS = "";
let entryEndCRS = "";
let data = [];
let error = false;
let errMsg = "Unknown Error";
let isLoading = false;
async function findByStartEnd() {
isLoading = true;
const url = `${getApiUrl()}/api/v2/pis/byStartEnd/${entryStartCRS}/${entryEndCRS}`;
await fetchData(url);
isLoading = false;
}
async function findByPis() {
isLoading = true;
const url = `${getApiUrl()}/api/v2/pis/byCode/${entryPIS}`;
await fetchData(url);
isLoading = false;
}
async function fetchData(url) {
const options = {
method: 'GET',
headers: {
uuid: $uuid
}
};
const res = await fetch(url, options); // Enable Auth
if (res.status == 401) {
errMsg = 'You must be logged in to the staff version';
error = true;
return false;
} else if (res.status == 500) {
errMsg = 'Server Error, try again later';
error = true;
return false;
async function findByStartEnd() {
isLoading = true;
const url = `${getApiUrl()}/api/v2/pis/byStartEnd/${entryStartCRS}/${entryEndCRS}`;
await fetchData(url);
isLoading = false;
}
const jsonData = await res.json();
if (jsonData.ERROR == 'offline') {
errMsg = 'Connection error, check your internet connection and try again';
error = true;
return false;
}
data = jsonData;
}
async function reset() {
data = [];
error = false;
entryPIS = '';
entryStartCRS = '';
entryEndCRS = '';
}
async function findByPis() {
isLoading = true;
const url = `${getApiUrl()}/api/v2/pis/byCode/${entryPIS}`;
await fetchData(url);
isLoading = false;
}
async function fetchData(url) {
const options = {
method: "GET",
headers: {
uuid: $uuid
}
};
const res = await fetch(url, options); // Enable Auth
if (res.status == 401) {
errMsg = "You must be logged in to the staff version";
error = true;
return false;
} else if (res.status == 500) {
errMsg = "Server Error, try again later";
error = true;
return false;
}
const jsonData = await res.json();
if (jsonData.ERROR == "offline") {
errMsg = "Connection error, check your internet connection and try again";
error = true;
return false;
}
data = jsonData;
}
async function reset() {
data = [];
error = false;
entryPIS = "";
entryStartCRS = "";
entryEndCRS = "";
}
</script>
<Header {title} />
{#if isLoading}
<Loading />
<Loading />
{/if}
{#if error}
<Island {variables}>
<p class="error">{errMsg}</p>
</Island>
<Island {variables}>
<p class="error">{errMsg}</p>
</Island>
{:else if data.length}
<Island {variables}>
<table>
<tr>
<th class="toc">TOC</th>
<th class="code">Code</th>
<th class="stops">Stops</th>
</tr>
{#each data as item}
<tr>
<td class="toc toc-data"><StylesToc toc={item.toc || '-'} /></td>
<td class="code">{item.code}</td>
<td class="stops stops-data">{item.stops.join(' ')}</td>
</tr>
{/each}
</table>
</Island>
<Island {variables}>
<table>
<tr>
<th class="toc">TOC</th>
<th class="code">Code</th>
<th class="stops">Stops</th>
</tr>
{#each data as item}
<tr>
<td class="toc toc-data"><StylesToc toc={item.toc || "-"} /></td>
<td class="code">{item.code}</td>
<td class="stops stops-data">{item.stops.join(" ")}</td>
</tr>
{/each}
</table>
</Island>
{:else}
<p>To search by headcode use the Train Finder on the homepage</p>
<p>This feature now supports all GWR Services</p>
<p class="label">Find By Start/End CRS:</p>
<form on:submit={findByStartEnd}>
<input type="text" maxlength="3" autocomplete="off" placeholder="Start" bind:value={entryStartCRS} />
<input type="text" maxlength="3" autocomplete="off" placeholder="End" bind:value={entryEndCRS} />
<br />
<button type="submit">Search</button>
</form>
<!-- FIND BY PIS CODE NOT WORKING AT PRESENT
<p>To search by headcode use the Train Finder on the homepage</p>
<p>This feature now supports all GWR Services</p>
<p class="label">Find By Start/End CRS:</p>
<form on:submit={findByStartEnd}>
<input type="text" maxlength="3" autocomplete="off" placeholder="Start" bind:value={entryStartCRS} />
<input type="text" maxlength="3" autocomplete="off" placeholder="End" bind:value={entryEndCRS} />
<br />
<button type="submit">Search</button>
</form>
<!-- FIND BY PIS CODE NOT WORKING AT PRESENT
<p class="label">Find By PIS Code:</p>
<form on:submit={findByPis}>
<input type="number" max="9999" autocomplete="off" placeholder="PIS" bind:value={entryPIS} />
@ -116,69 +116,69 @@
<Nav />
<style>
p {
margin-left: 10px;
margin-right: 10px;
}
.label {
font-weight: 600;
color: var(--main-text-color);
}
input {
border: none;
border-radius: 50px;
font-family: urwgothic, sans-serif;
text-align: center;
text-transform: uppercase;
width: 30%;
max-width: 250px;
height: 30px;
font-size: 16px;
margin-bottom: 15px;
box-shadow: var(--box-shadow);
}
button {
border: none;
border-radius: 50px;
font-family: urwgothic, sans-serif;
width: 25%;
max-width: 175px;
margin: 0px;
margin-left: 10px;
margin-right: 10px;
height: 30px;
background-color: var(--island-bg-color);
color: white;
font-size: 16px;
box-shadow: var(--box-shadow);
}
table {
width: 100%;
margin: auto;
color: white;
}
td {
padding-top: 5px;
padding-bottom: 5px;
}
.toc {
width: 15%;
}
.code {
width: 20%;
}
.toc-data {
text-transform: uppercase;
}
.stops-data {
text-align: left;
font-family: firamono, monospace;
text-transform: uppercase;
}
.error {
color: white;
}
#reset {
margin: 25px;
}
p {
margin-left: 10px;
margin-right: 10px;
}
.label {
font-weight: 600;
color: var(--main-text-color);
}
input {
border: none;
border-radius: 50px;
font-family: urwgothic, sans-serif;
text-align: center;
text-transform: uppercase;
width: 30%;
max-width: 250px;
height: 30px;
font-size: 16px;
margin-bottom: 15px;
box-shadow: var(--box-shadow);
}
button {
border: none;
border-radius: 50px;
font-family: urwgothic, sans-serif;
width: 25%;
max-width: 175px;
margin: 0px;
margin-left: 10px;
margin-right: 10px;
height: 30px;
background-color: var(--island-bg-color);
color: white;
font-size: 16px;
box-shadow: var(--box-shadow);
}
table {
width: 100%;
margin: auto;
color: white;
}
td {
padding-top: 5px;
padding-bottom: 5px;
}
.toc {
width: 15%;
}
.code {
width: 20%;
}
.toc-data {
text-transform: uppercase;
}
.stops-data {
text-align: left;
font-family: firamono, monospace;
text-transform: uppercase;
}
.error {
color: white;
}
#reset {
margin: 25px;
}
</style>

View File

@ -1,106 +1,106 @@
<script lang="ts">
import Header from '$lib/navigation/header.svelte';
import Loading from '$lib/navigation/loading.svelte';
import Island from '$lib/islands/island.svelte';
import Nav from '$lib/navigation/nav.svelte';
import { uuid } from '$lib/stores/uuid';
import Header from "$lib/navigation/header.svelte";
import Loading from "$lib/navigation/loading.svelte";
import Island from "$lib/islands/island.svelte";
import Nav from "$lib/navigation/nav.svelte";
import { uuid } from "$lib/stores/uuid";
import { onMount } from 'svelte';
import TrainDetail from '$lib/train/train-detail.svelte';
import { getApiUrl } from '$lib/scripts/upstream';
import { onMount } from "svelte";
import TrainDetail from "$lib/train/train-detail.svelte";
import { getApiUrl } from "$lib/scripts/upstream";
let title = 'Timetable Results';
let id = '';
let data = [];
let isLoading = true;
let error = false;
let errMsg = '';
let title = "Timetable Results";
let id = "";
let data = [];
let isLoading = true;
let error = false;
let errMsg = "";
$: {
if (id) {
title = id.toUpperCase();
} else {
title = 'Querying Timetable';
$: {
if (id) {
title = id.toUpperCase();
} else {
title = "Querying Timetable";
}
}
}
async function getHeadcode() {
return new URLSearchParams(window.location.search).get('headcode');
}
onMount(async () => {
isLoading = true;
id = (await getHeadcode()) || '';
const res = await fetchData(id);
if (res) {
data = res;
if (!data.length) {
error = true;
errMsg = 'No services found';
}
async function getHeadcode() {
return new URLSearchParams(window.location.search).get("headcode");
}
isLoading = false;
});
async function fetchData(id = '') {
const date = 'now';
const searchType = 'headcode';
const options = {
method: 'GET',
headers: {
uuid: $uuid
}
};
const url = `${getApiUrl()}/api/v2/timetable/train/${date}/${searchType}/${id}`;
try {
const res = await fetch(url, options);
if (res.status == 200) {
return await res.json();
} else if (res.status === 401) {
error = true;
errMsg = 'You must be logged into the staff version for this feature';
return false;
} else {
error = true;
errMsg = 'Unable to connect, check your connection and try again';
return false;
}
} catch (err) {
error = true;
errMsg = 'Connection error, try again later';
onMount(async () => {
isLoading = true;
id = (await getHeadcode()) || "";
const res = await fetchData(id);
if (res) {
data = res;
if (!data.length) {
error = true;
errMsg = "No services found";
}
}
isLoading = false;
});
async function fetchData(id = "") {
const date = "now";
const searchType = "headcode";
const options = {
method: "GET",
headers: {
uuid: $uuid
}
};
const url = `${getApiUrl()}/api/v2/timetable/train/${date}/${searchType}/${id}`;
try {
const res = await fetch(url, options);
if (res.status == 200) {
return await res.json();
} else if (res.status === 401) {
error = true;
errMsg = "You must be logged into the staff version for this feature";
return false;
} else {
error = true;
errMsg = "Unable to connect, check your connection and try again";
return false;
}
} catch (err) {
error = true;
errMsg = "Connection error, try again later";
}
isLoading = false;
}
isLoading = false;
}
</script>
<Header {title} />
<div id="whitespace" />
{#if error}
<Island>
<p style="font-weight:600">{errMsg}</p>
</Island>
<Island>
<p style="font-weight:600">{errMsg}</p>
</Island>
{/if}
{#if isLoading}
<Loading />
<Loading />
{/if}
{#each data as service}
{#if service}
<TrainDetail {service} />
{/if}
{#if service}
<TrainDetail {service} />
{/if}
{/each}
<Nav />
<style>
#whitespace {
height: 15px;
}
p {
color: white;
font-size: 18px;
font-weight: 600;
}
#whitespace {
height: 15px;
}
p {
color: white;
font-size: 18px;
font-weight: 600;
}
</style>

View File

@ -1,50 +1,50 @@
/// <reference types="@sveltejs/kit" />
import { build, files, version } from '$service-worker';
import { build, files, version } from "$service-worker";
const cacheName = `ob-${version}`;
const pointlessConstant = true;
console.log(`pointlessContant is ${pointlessConstant}`);
const assets = [...build, ...files, '/service-worker.js'];
const assets = [...build, ...files, "/service-worker.js"];
self.addEventListener('install', (event) => {
async function addToCache() {
const cache = await caches.open(cacheName);
await cache.addAll(assets);
}
event.waitUntil(addToCache());
});
self.addEventListener('activate', (event) => {
async function deleteOldCache() {
for (const key of await caches.keys()) {
if (key !== cacheName) {
await caches.delete(key);
}
}
}
event.waitUntil(deleteOldCache());
});
self.addEventListener('fetch', (event) => {
if (event.request.method !== 'GET') {
return;
}
async function respond() {
const cacheRes = await caches.match(event.request, { ignoreSearch: true });
if (cacheRes) {
return cacheRes;
self.addEventListener("install", (event) => {
async function addToCache() {
const cache = await caches.open(cacheName);
await cache.addAll(assets);
}
try {
return await fetch(event.request);
} catch (err) {
return { error: 'OFFLINE', errorMsg: 'You are offline' };
}
}
event.respondWith(respond());
event.waitUntil(addToCache());
});
self.addEventListener("activate", (event) => {
async function deleteOldCache() {
for (const key of await caches.keys()) {
if (key !== cacheName) {
await caches.delete(key);
}
}
}
event.waitUntil(deleteOldCache());
});
self.addEventListener("fetch", (event) => {
if (event.request.method !== "GET") {
return;
}
async function respond() {
const cacheRes = await caches.match(event.request, { ignoreSearch: true });
if (cacheRes) {
return cacheRes;
}
try {
return await fetch(event.request);
} catch (err) {
return { error: "OFFLINE", errorMsg: "You are offline" };
}
}
event.respondWith(respond());
});

View File

@ -1,104 +1,104 @@
{
"name": "OwlBoard",
"short_name": "OwlBoard",
"start_url": "/",
"scope": "/",
"display": "standalone",
"background_color": "#404c55",
"description": "Get instant access to live train data, PIS codes, and location reference codes. Built by railway staff, for railway staff your fastest route to accurate information.",
"categories": "travel,utilities",
"lang": "en",
"orientation": "portrait",
"theme_color": "#00b7b7",
"icons": [
{
"src": "/images/app-icons/maskable/mask-icon.svg",
"sizes": "any",
"type": "image/svg+xml",
"purpose": "maskable"
},
{
"src": "/images/app-icons/any/plain-logo.svg",
"sizes": "any",
"type": "image/svg+xml",
"purpose": "any"
},
{
"src": "/images/app-icons/any/plain-logo-512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "any"
}
],
"screenshots": [
{
"src": "/images/screenshots/narrow-home.png",
"type": "image/png",
"sizes": "720x1480",
"form_factor": "narrow",
"label": "Homescreen of OwlBoard"
},
{
"src": "/images/screenshots/narrow-ldbs.png",
"type": "image/png",
"sizes": "720x1480",
"form_factor": "narrow",
"label": "Live rail departure boards"
},
{
"src": "/images/screenshots/narrow-timetable.png",
"type": "image/png",
"sizes": "720x1480",
"form_factor": "narrow",
"label": "Train details and PIS Codes"
},
{
"src": "/images/screenshots/wide-home.png",
"type": "image/png",
"sizes": "2560x1600",
"form_factor": "wide",
"label": "Homescreen of OwlBoard"
},
{
"src": "/images/screenshots/wide-ldbs.png",
"type": "image/png",
"sizes": "2560x1600",
"form_factor": "wide",
"label": "Live rail departure boards"
},
{
"src": "/images/screenshots/wide-timetable.png",
"type": "image/png",
"sizes": "2560x1600",
"form_factor": "wide",
"label": "Train details and PIS Codes"
}
],
"shortcuts": [
{
"name": "Location Reference",
"short_name": "Loc Reference",
"description": "Lookup location reference data about the UK Rail network",
"url": "/more/corpus",
"icons": [
"name": "OwlBoard",
"short_name": "OwlBoard",
"start_url": "/",
"scope": "/",
"display": "standalone",
"background_color": "#404c55",
"description": "Get instant access to live train data, PIS codes, and location reference codes. Built by railway staff, for railway staff your fastest route to accurate information.",
"categories": "travel,utilities",
"lang": "en",
"orientation": "portrait",
"theme_color": "#00b7b7",
"icons": [
{
"src": "/images/shortcuts/location_ref.png",
"sizes": "96x96",
"type": "image/png"
}
]
},
{
"name": "Settings",
"description": "Your OwlBoard settings",
"url": "/more/settings",
"icons": [
"src": "/images/app-icons/maskable/mask-icon.svg",
"sizes": "any",
"type": "image/svg+xml",
"purpose": "maskable"
},
{
"src": "/images/shortcuts/cog.png",
"sizes": "96x96",
"type": "image/png"
"src": "/images/app-icons/any/plain-logo.svg",
"sizes": "any",
"type": "image/svg+xml",
"purpose": "any"
},
{
"src": "/images/app-icons/any/plain-logo-512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "any"
}
]
}
]
],
"screenshots": [
{
"src": "/images/screenshots/narrow-home.png",
"type": "image/png",
"sizes": "720x1480",
"form_factor": "narrow",
"label": "Homescreen of OwlBoard"
},
{
"src": "/images/screenshots/narrow-ldbs.png",
"type": "image/png",
"sizes": "720x1480",
"form_factor": "narrow",
"label": "Live rail departure boards"
},
{
"src": "/images/screenshots/narrow-timetable.png",
"type": "image/png",
"sizes": "720x1480",
"form_factor": "narrow",
"label": "Train details and PIS Codes"
},
{
"src": "/images/screenshots/wide-home.png",
"type": "image/png",
"sizes": "2560x1600",
"form_factor": "wide",
"label": "Homescreen of OwlBoard"
},
{
"src": "/images/screenshots/wide-ldbs.png",
"type": "image/png",
"sizes": "2560x1600",
"form_factor": "wide",
"label": "Live rail departure boards"
},
{
"src": "/images/screenshots/wide-timetable.png",
"type": "image/png",
"sizes": "2560x1600",
"form_factor": "wide",
"label": "Train details and PIS Codes"
}
],
"shortcuts": [
{
"name": "Location Reference",
"short_name": "Loc Reference",
"description": "Lookup location reference data about the UK Rail network",
"url": "/more/corpus",
"icons": [
{
"src": "/images/shortcuts/location_ref.png",
"sizes": "96x96",
"type": "image/png"
}
]
},
{
"name": "Settings",
"description": "Your OwlBoard settings",
"url": "/more/settings",
"icons": [
{
"src": "/images/shortcuts/cog.png",
"sizes": "96x96",
"type": "image/png"
}
]
}
]
}

View File

@ -1,17 +1,17 @@
// Remove the service worker /sw.js from legacy installations
// This should then enable the new /service-worker.js to be installed
self.addEventListener('activate', function (e) {
console.log(`Unregistering service worker`);
self.registration
.unregister()
.then(function () {
return self.clients.matchAll();
})
.then(function (clients) {
clients.forEach((client) => {
console.log(`Navigating ${client.url}`);
client.navigate(client.url);
});
});
self.addEventListener("activate", function (e) {
console.log(`Unregistering service worker`);
self.registration
.unregister()
.then(function () {
return self.clients.matchAll();
})
.then(function (clients) {
clients.forEach((client) => {
console.log(`Navigating ${client.url}`);
client.navigate(client.url);
});
});
});

View File

@ -1,16 +1,16 @@
import adapter from '@sveltejs/adapter-static';
import { vitePreprocess } from '@sveltejs/kit/vite';
import adapter from "@sveltejs/adapter-static";
import { vitePreprocess } from "@sveltejs/kit/vite";
const config = {
preprocess: vitePreprocess(),
kit: {
adapter: adapter({
pages: 'build',
assets: 'build',
precompress: true,
strict: true
})
}
preprocess: vitePreprocess(),
kit: {
adapter: adapter({
pages: "build",
assets: "build",
precompress: true,
strict: true
})
}
};
export default config;

View File

@ -1,18 +1,18 @@
{
"extends": "./.svelte-kit/tsconfig.json",
"compilerOptions": {
"allowJs": true,
"checkJs": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"sourceMap": true,
"strict": true,
"outDir": "./dist"
}
// Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias
//
// If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes
// from the referenced tsconfig.json - TypeScript does not merge them in
"extends": "./.svelte-kit/tsconfig.json",
"compilerOptions": {
"allowJs": true,
"checkJs": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"sourceMap": true,
"strict": true,
"outDir": "./dist"
}
// Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias
//
// If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes
// from the referenced tsconfig.json - TypeScript does not merge them in
}

View File

@ -1,6 +1,6 @@
import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vite';
import { sveltekit } from "@sveltejs/kit/vite";
import { defineConfig } from "vite";
export default defineConfig({
plugins: [sveltekit()]
plugins: [sveltekit()]
});