Compare commits

...

111 Commits

Author SHA1 Message Date
Fred Boniface be850f5bd1 Stop caching shortcut icons - that is down to the OS. 2024-07-12 20:50:05 +01:00
Fred Boniface 0c635d99dd Change dash type used 2024-07-12 20:49:53 +01:00
Fred Boniface 70bba6635f Remove unused image sizes 2024-07-12 20:45:10 +01:00
Fred Boniface 53c5309485 Remove PNG logo images 2024-07-12 20:43:09 +01:00
Fred Boniface 7763f567f6 Move @tabler icons from dependencies to dev-dependencies 2024-07-12 20:17:29 +01:00
Fred Boniface d48d4ffe4a Remove unneeded XCF version of file 2024-07-12 20:03:51 +01:00
Fred Boniface 10d749a5a7 Remove unused CSS Selector 2024-07-12 19:58:21 +01:00
Fred Boniface ec4ba07cf7 Bump version 2024-07-12 19:56:19 +01:00
Fred Boniface 78fc63fe29 Remove screenshots from SW Cache 2024-07-12 19:53:45 +01:00
Fred Boniface 4526cfa3e0 Remove truetype font files 2024-07-12 19:37:48 +01:00
Fred Boniface b6a8bd0461 Add toast to Public LDB suggesting registering 2024-07-12 19:34:58 +01:00
Fred Boniface f92f01af16 Migrate transport mode icons to Tabler Icons 2024-07-12 19:29:45 +01:00
Fred Boniface d75b69df26 Remove redundant images 2024-07-12 15:46:44 +01:00
Fred Boniface 2a07b0fa3e Adjust Link & Script button margins and fix layout of NearToMeCard to allow for it. 2024-07-12 15:44:37 +01:00
Fred Boniface 3d40445728 Remove text shadow from header bar - it looks stupid 2024-07-12 15:35:37 +01:00
Fred Boniface 31b4653ca2 Run prettier for formatting fixes 2024-07-12 15:29:55 +01:00
Fred Boniface 2b8d32f3c9 blockLoading of near to me card until stores have been read 2024-07-12 15:26:51 +01:00
Fred Boniface e3632986c2 Remove 'welcome' components 2024-07-12 15:14:39 +01:00
Fred Boniface 693ad67980 Remove redundant navigartion icons 2024-07-12 15:11:51 +01:00
Fred Boniface a8b7379700 Remove redundant alert icon 2024-07-12 15:10:18 +01:00
Fred Boniface 6d175f300f Implement time bar and update LDB components to handle it 2024-07-12 15:04:06 +01:00
Fred Boniface 7f1dc1ac3f Update and implement TimeBar, needs testing with NRCC messages present. 2024-07-12 12:20:36 +01:00
Fred Boniface 6f800dab67 Add TimeBar 2024-07-11 21:19:42 +01:00
Fred Boniface 5d84ac8ae2 Update manifest screenshots 2024-07-11 21:02:56 +01:00
Fred Boniface 872ea9f1d6 Bump version 2024-07-07 21:27:52 +01:00
Fred Boniface 284dedbb3f Replace `result-island` 2024-07-07 21:23:15 +01:00
Fred Boniface 512c77e81c Replace homescreen 'islands' with 'cards' and remove now unused 'island' components 2024-07-07 21:02:10 +01:00
Fred Boniface 9ad046dd9f Update link URL in QuickLinkCard 2024-07-07 10:02:37 +01:00
Fred Boniface 91d523e372 Adjust ScriptButton size for better touch target. 2024-07-07 10:02:15 +01:00
Fred Boniface b48795563f Introduce LinkButton and ScriptButton components and updated NearToMeCard to make use of the new components 2024-07-07 09:56:07 +01:00
Fred Boniface bf28984b80 Remove inneccessary type definitions 2024-07-07 09:33:24 +01:00
Fred Boniface 38ceb1aadd Create QuickLinkCard 2024-07-07 09:32:59 +01:00
Fred Boniface eaa8c192a2 Add dynamic margin if alerts bar is displayed on staffLDB page 2024-07-07 08:45:41 +01:00
Fred Boniface ba09910ff3 Remove uneeded import 2024-07-05 12:28:14 +01:00
Fred Boniface 06edc49967 Add time to LDB 2024-07-05 11:00:38 +01:00
Fred Boniface b4a3da5174 Add nearToMeCache store using session storage 2024-07-05 10:45:38 +01:00
Fred Boniface bebf2eba99 Adjust near to me refresh animation 2024-07-05 01:18:27 +01:00
Fred Boniface a3bf2af68d Add rotating reload icon 2024-07-05 01:12:56 +01:00
Fred Boniface b779429346 Remove unused imports and CSS Selectors 2024-07-03 11:47:58 +01:00
Fred Boniface db2a764167 Update link spacing on NearToMeCard 2024-07-03 11:47:03 +01:00
Fred Boniface 89109a3a48 Adjust Card subtype styling 2024-07-03 11:25:21 +01:00
Fred Boniface d17b9c23af Adjust checking order for NearToMeCard 2024-07-03 11:16:53 +01:00
Fred Boniface 62f6454b83 Fix incorrect type import 2024-07-03 11:09:53 +01:00
Fred Boniface c8cd0f30d1 Bmp version 2024-07-03 11:02:57 +01:00
Fred Boniface f82d015e52 Format 2024-07-03 11:02:22 +01:00
Fred Boniface 30240edf00 Flesh out new NearToMeCard 2024-07-03 11:02:11 +01:00
Fred Boniface 7472f96b5d Add inline loading spinner component 2024-07-03 11:01:58 +01:00
Fred Boniface b63c63f679 Update test page to test new Cards 2024-07-03 11:01:29 +01:00
Fred Boniface f81acf348a Update reason fetcher to use apiGet function 2024-07-03 11:01:05 +01:00
Fred Boniface 5a9e55c695 Add message for when unauthorised 2024-07-03 11:00:50 +01:00
Fred Boniface fddf9cbbaf Begin migration to new Cards component 2024-07-02 21:02:28 +01:00
Fred Boniface e0227516d8 Rewrite Cards - not yet implemented on public pages 2024-07-02 20:18:01 +01:00
Fred Boniface 95e45c8cb1 Format 2024-07-02 20:16:57 +01:00
Fred Boniface d09b24655a Bump version, add toast 2024-07-01 16:07:08 +01:00
Fred Boniface f93113ec14 Complete location based work for "Near to Me" feature 2024-07-01 16:01:24 +01:00
Fred Boniface 75641bd245 Add fetch to nearest-to-me-island 2024-07-01 13:25:44 +01:00
Fred Boniface 12ef391ec4 Bump ts-types 2024-07-01 12:07:54 +01:00
Fred Boniface 982cee6bfe Comment out temporarily unused CSS 2024-06-30 11:15:10 +01:00
Fred Boniface d3530063f3 Add near-to-me pre-feature 2024-06-30 11:13:32 +01:00
Fred Boniface 313517605b Update feature block on stats page 2024-06-24 00:20:30 +01:00
Fred Boniface 13f7163dd7 Add location to statistics 2024-06-24 00:06:30 +01:00
Fred Boniface 35dd00499f Bump version 2024-06-23 23:49:01 +01:00
Fred Boniface 4fbec34f24 Run prettier 2024-06-23 23:48:40 +01:00
Fred Boniface 3db490a0bb Allow statistics to handle date ibject or timestamp 2024-06-23 23:47:47 +01:00
Fred Boniface cd0e051a5a Add feature detect information to statistics page 2024-06-23 23:43:16 +01:00
Fred Boniface 9e5d6d4732 Remove 'Pointless Constant' from service worker 2024-06-23 23:16:08 +01:00
Fred Boniface 8560d61348 Add feature detection and warnings, run formatter 2024-06-23 11:21:45 +01:00
Fred Boniface 98c9b5cc03 Adjust path types in build output 2024-06-18 23:42:05 +01:00
Fred Boniface 1df751c9ca Adjust forwarding 2024-06-18 23:04:39 +01:00
Fred Boniface 60ece7661c Update nginx caching 2024-06-18 23:04:09 +01:00
Fred Boniface c93f36102e Introduce toasts 2024-06-13 21:59:01 +01:00
Fred Boniface 0d0875f893 Prettier - add trailing commas 2024-04-30 11:18:21 +01:00
Fred Boniface 1fba04b2aa Update prettierrc 2024-04-30 11:17:45 +01:00
Fred Boniface af58e923de Update prettier settings 2024-04-30 11:17:06 +01:00
Fred Boniface 1484a9068e Improve apiGet error handling 2024-04-30 11:09:42 +01:00
Fred Boniface 8bd97c308c Add central apiFetch function 2024-04-30 11:03:13 +01:00
Fred Boniface 0f2b097c34 Update screenshot images 2024-04-30 10:30:09 +01:00
Fred Boniface 9e1984566b Bump version 2024-04-28 11:56:31 +01:00
Fred Boniface ef1c958d66 Add link to documentation website 2024-04-27 21:53:02 +01:00
Fred Boniface 955c275ac9 Update tocMap 2024-04-24 23:28:22 +01:00
Fred Boniface 5bec33c388 Adjust TOC Names 2024-04-24 23:20:48 +01:00
Fred Boniface 41f673c68f Update train detail layout to improve icon placement 2024-04-23 19:53:43 +01:00
Fred Boniface 2a615a822e Update to handle new serviceDetails object 2024-04-23 14:58:27 +01:00
Fred Boniface 1b21dacfd9 Update Dependencies 2024-04-23 14:56:15 +01:00
Fred Boniface 735853aa8d Optimise nginx configuration and enable dynamic compression 2024-04-19 21:17:16 +01:00
Fred Boniface 958eabe76e Bump version, add .dockerignore to improve buildtimes 2024-04-19 20:51:12 +01:00
Fred Boniface 7dcf0c8b1b Update PIS Icon from numbers to dialpad 2024-04-19 20:46:34 +01:00
Fred Boniface ee8b547a19 Add tooltip to version icons 2024-04-19 17:10:55 +01:00
Fred Boniface 3b7f34bdab Update nginx.conf to default to kubernetes external deployment 2024-04-17 13:13:51 +01:00
Fred Boniface 3abdc7d740 Add non-public, pass times, and booked platforms to train details 2024-04-17 13:04:31 +01:00
Fred Boniface 8c91a50a34 Add non-passenger locations to train detail and prepare for 'pass' times to be made available from API. 2024-04-17 12:20:50 +01:00
Fred Boniface 21eabfc7d7 Remove 'Find by PIS Code' as it is not working, and probably isn't that useful! 2024-04-17 12:20:29 +01:00
Fred Boniface 94434cdcf8 Fix icon layout on versions page 2024-04-17 12:19:57 +01:00
Fred Boniface b6d3d128bc Bump version 2024-04-17 12:19:43 +01:00
Fred Boniface 0f7deee78a Add commented out Docker version upstream 2024-04-17 12:19:28 +01:00
Fred Boniface d107416bb0 Re-add package-lock 2024-04-17 10:16:54 +01:00
Fred Boniface 70fb62fd6f Adjust CSS 2024-04-17 10:11:45 +01:00
Fred Boniface 33fb2a607f Add icons to menu and versions page 2024-04-17 09:59:32 +01:00
Fred Boniface 0a666afc58 Add train detail icons with tooltips 2024-04-16 21:28:11 +01:00
Fred Boniface 3f4a172f48 Add tabler icons, run npm update 2024-04-15 21:34:45 +01:00
Fred Boniface 42e695d89f Update version string in NPM files 2024-03-31 19:59:07 +01:00
Fred Boniface 4537ff51a8 Adjust the text on the PIS page to clarify support of all GWR services. 2024-03-31 19:58:13 +01:00
Fred Boniface a52d1fa173 Adjust the version string format to ensure integer comparison can be made. Changed from 2024.3.0 to 2024.03.2 2024-03-31 19:57:46 +01:00
Fred Boniface 1bb9db3bc3 Adjust the welcome messages 2024-03-31 19:56:52 +01:00
Fred Boniface 80b3c235af Replace registration stream with code based 2024-03-10 20:43:29 +00:00
Fred Boniface 4dd9ea05d6 Add code registration page 2024-03-09 20:35:17 +00:00
Fred Boniface b9d18950b9 Add ASCII art to <head> 2024-03-09 19:07:56 +00:00
Fred Boniface 6c0d152358 Change welcome message referencing OwlBoard/backend#71 2024-03-01 22:13:08 +00:00
Fred Boniface a4276bd0e9 Bump version 2024-02-19 11:25:21 +00:00
Fred Boniface 008e106877 Remove extra 'first' from PIS handler pis-text string 2024-02-19 11:24:54 +00:00
Fred Boniface bf93df98cd Fix & close OwlBoard/backend#67
Page pathname now contains trailing '/' due to Sveltekit changes.
2024-02-19 11:23:31 +00:00
144 changed files with 7906 additions and 6888 deletions

9
.dockerignore Normal file
View File

@ -0,0 +1,9 @@
.DS_Store
node_modules
/build
/package
.env
.env.*
!.env.example
vite.config.js.timestamp-*
vite.config.ts.timestamp-*

View File

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

View File

@ -1,9 +1,5 @@
{ {
"useTabs": false, "tabWidth": 4,
"tabWidth": 2,
"semi": true,
"singleQuote": true,
"trailingComma": "none",
"printWidth": 180, "printWidth": 180,
"plugins": ["prettier-plugin-svelte"], "plugins": ["prettier-plugin-svelte"],
"pluginSearchDirs": ["."], "pluginSearchDirs": ["."],

View File

@ -1,4 +1,5 @@
load_module modules/ngx_http_brotli_static_module.so; load_module modules/ngx_http_brotli_static_module.so;
load_module modules/ngx_http_brotli_filter_module.so;
user nginx; user nginx;
worker_processes auto; worker_processes auto;
@ -20,7 +21,7 @@ http {
'$status $body_bytes_sent "$http_referer" ' '$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"'; '"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main; access_log /dev/stdout main;
sendfile on; sendfile on;
@ -31,7 +32,14 @@ http {
proxy_cache_path /var/cache/nginx keys_zone=owl_cache:20m inactive=24h; proxy_cache_path /var/cache/nginx keys_zone=owl_cache:20m inactive=24h;
upstream backend { upstream backend {
server owlboard-backend:8460; # Within Kubernetes:
#server owlboard-backend:8460;
# External to Kubernetes:
server 172.30.129.19:8460;
# Within Docker:
#server owlboard-backend:8460
} }
server { server {
@ -44,29 +52,33 @@ http {
location / { location / {
root /usr/share/nginx/html; root /usr/share/nginx/html;
index index; index /index.html;
gzip_static on; gzip_static on;
brotli_static on; brotli_static on;
error_page 500 502 503 504 /err/50x.html; error_page 500 502 503 504 /err/50x.html;
error_page 404 /err/404; try_files $uri $uri/ $uri.html /index.html;
try_files $uri.html $uri $uri/index.html $uri/ =404;
add_header Cache-Control "public, no-transform, max-age=1209600"; add_header Cache-Control "public, no-transform, max-age=1209600";
if ($uri ~* \.html$) {
return 404;
}
} }
location /misc/ { location /misc/ {
proxy_pass http://backend; proxy_pass http://backend;
brotli on;
brotli_comp_level 6;
brotli_types *;
gzip on;
gzip_comp_level 6;
gzip_types *;
} }
location /api/ { location /api/ {
proxy_pass http://backend; proxy_pass http://backend;
brotli on;
brotli_comp_level 6;
brotli_types *;
gzip on;
gzip_comp_level 6;
gzip_types *;
proxy_cache_key $scheme://$host$uri$is_args$query_string; proxy_cache_key $scheme://$host$uri$is_args$query_string;
proxy_ignore_headers Cache-Control;
proxy_cache_valid 200 2m; # Evaluate whether 2m or 1m is more appropriate
add_header Cache-Control "private, no-transform, max-age=120";
} }
} }

561
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{ {
"name": "owlboard-svelte", "name": "owlboard-svelte",
"version": "2024.2.1", "version": "2024.03.2",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "vite dev", "dev": "vite dev",
@ -13,7 +13,7 @@
"format": "prettier --plugin-search-dir . --write ." "format": "prettier --plugin-search-dir . --write ."
}, },
"devDependencies": { "devDependencies": {
"@owlboard/ts-types": "^0.1.8", "@owlboard/ts-types": "^1.2.0",
"@sveltejs/adapter-auto": "^2.0.0", "@sveltejs/adapter-auto": "^2.0.0",
"@sveltejs/adapter-static": "^2.0.2", "@sveltejs/adapter-static": "^2.0.2",
"@sveltejs/kit": "^1.5.0", "@sveltejs/kit": "^1.5.0",
@ -24,9 +24,11 @@
"prettier-plugin-svelte": "^2.8.1", "prettier-plugin-svelte": "^2.8.1",
"svelte": "^3.54.0", "svelte": "^3.54.0",
"svelte-check": "^3.0.1", "svelte-check": "^3.0.1",
"svelte-french-toast": "^1.2.0",
"svelte-sitemap": "^2.6.0", "svelte-sitemap": "^2.6.0",
"typescript": "^5.0.0", "typescript": "^5.0.0",
"vite": "^4.3.0" "vite": "^4.3.0",
"@tabler/icons-svelte": "^3.2.0"
}, },
"type": "module" "type": "module"
} }

View File

@ -3,8 +3,8 @@
<style> <style>
#banner { #banner {
width: 200px; width: 200px;
background: red; background: rgba(255, 0, 0, 0.5);
color: #fff; color: #ffffff7a;
position: fixed; position: fixed;
text-align: center; text-align: center;
top: 25px; top: 25px;

58
src/lib/Tooltip.svelte Normal file
View File

@ -0,0 +1,58 @@
<script lang="ts">
import { onMount, onDestroy } from "svelte";
export let text: string;
let isVisible: boolean = false;
let timer: number;
function showTooltip() {
isVisible = true;
timer = setTimeout(() => {
isVisible = false;
}, 2000);
}
function hideTooltip() {
isVisible = false;
clearTimeout(timer);
}
onDestroy(() => {
clearTimeout(timer);
});
</script>
<div class="tooltip" on:touchstart={showTooltip} on:touchend={hideTooltip} on:touchcancel={hideTooltip}>
<slot />
<span class="tooltiptext">{text}</span>
</div>
<style>
.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:hover .tooltiptext {
visibility: visible;
opacity: 1;
}
</style>

View File

@ -0,0 +1,35 @@
<script lang="ts">
export let link: string;
export let text: string;
</script>
<a class="link-button" href={link}>{text}</a>
<style>
.link-button {
color: aliceblue;
border: none;
border-radius: 20px;
text-decoration: none;
margin: 5px;
background-color: var(--island-button-color);
font-family: urwgothic, "Lucida Sans", "Lucida Sans Regular", "Lucida Grande", "Lucida Sans Unicode", Geneva, Verdana, sans-serif;
padding: 5px 25px;
min-width: 40px;
width: fit-content;
height: 22px;
font-size: 16px;
font-weight: 400;
box-shadow: var(--box-shadow);
transition: all 0.3s ease;
cursor: pointer;
display: flex;
justify-content: center;
align-items: center;
}
.link-button:hover {
box-shadow: var(--box-shadow-dark);
background-color: rgb(45, 45, 45);
}
</style>

View File

@ -0,0 +1,34 @@
<script lang="ts">
export let fn = () => {};
export let text: string;
</script>
<button class="script-button" on:click={fn} on:keypress={fn}>{text}</button>
<style>
.script-button {
color: aliceblue;
border: none;
border-radius: 20px;
text-decoration: none;
margin: 5px;
font-family: urwgothic, "Lucida Sans", "Lucida Sans Regular", "Lucida Grande", "Lucida Sans Unicode", Geneva, Verdana, sans-serif;
background-color: var(--island-button-color);
padding: 20px;
min-width: 40px;
height: 25px;
font-size: 16px;
font-weight: 400;
box-shadow: var(--box-shadow);
transition: all 0.3s ease;
cursor: pointer;
display: flex;
justify-content: center;
align-items: center;
}
.script-button:hover {
box-shadow: var(--box-shadow-dark);
background-color: rgb(45, 45, 45);
}
</style>

108
src/lib/cards/Card.svelte Normal file
View File

@ -0,0 +1,108 @@
<script lang="ts">
import { fade } from "svelte/transition";
import type { CardConfig } from "./Card.types";
import { IconHelpCircle, IconRefresh } from "@tabler/icons-svelte";
import Tooltip from "$lib/Tooltip.svelte";
export let config: CardConfig;
</script>
<div class="card" in:fade={{ duration: 250 }}>
<div class="header">
<h2 class="title">{config.title}</h2>
<div class="actions">
{#if config.showHelp}
<Tooltip text={config.helpText}>
<button aria-label="Help">
<IconHelpCircle />
</button>
</Tooltip>
{/if}
{#if config.showRefresh}
<button class:refreshing={config.refreshing} on:click={config.onRefresh} aria-label="Refresh">
<IconRefresh />
</button>
{/if}
</div>
</div>
<div class="content">
<slot />
</div>
</div>
<style>
.card {
width: 85%;
max-width: 400px;
margin: auto;
margin-top: 10px;
margin-bottom: 20px;
padding: 10px 5px 5px 5px;
background-color: var(--island-bg-color);
border-radius: 10px;
box-shadow: 5px 5px 30px rgba(0, 0, 0, 0.29);
min-height: 75px;
}
.header {
color: var(--island-header-color);
font-size: 14px;
display: flex;
margin: 0;
width: 100%;
justify-content: center;
align-items: center;
position: relative;
text-shadow: 2px 1px 10px rgba(0, 0, 0, 0.29);
}
.title {
flex-grow: 1;
text-align: center;
margin: 0;
}
.actions {
display: flex;
position: absolute;
top: 5px;
right: 0;
display: flex;
gap: 0px;
}
.refreshing {
animation: spin 1.5s linear infinite;
transform-origin: 50% 45%;
}
.content {
margin-top: 5px;
}
button {
cursor: pointer;
color: white;
background: none;
border: none;
transition: all 0.3s ease;
}
button:hover {
color: var(--island-header-color);
}
@keyframes spin {
0% {
transform: rotate(0deg);
color: white;
}
50% {
color: rgb(185, 185, 255);
}
100% {
transform: rotate(-360deg);
color: white;
}
}
</style>

View File

@ -0,0 +1,17 @@
export interface CardConfig {
title: string;
showHelp: boolean;
showRefresh: boolean;
helpText: string;
onRefresh: () => void;
refreshing: boolean;
}
export interface LookupCardConfig {
title: string;
formAction: string;
maxLen: number;
placeholder: string;
helpText: string;
fieldName: string;
}

View File

@ -0,0 +1,64 @@
<script lang="ts">
import Card from "./Card.svelte";
import type { CardConfig, LookupCardConfig } from "./Card.types";
export let config: LookupCardConfig;
let upstreamConfig: CardConfig = {
title: config.title,
showHelp: false, // If enabled without showRefresh, cards will be shifted to left!
showRefresh: false,
helpText: config.helpText,
onRefresh: () => {},
refreshing: false,
};
</script>
<Card config={upstreamConfig}>
<form action={config.formAction}>
<input type="text" name={config.fieldName} placeholder={config.placeholder} maxlength={config.maxLen} autocomplete="off" />
<br />
<button type="submit">Submit</button>
</form>
</Card>
<style>
input {
margin: 4px;
width: 75%;
max-width: 250px;
text-align: center;
text-transform: uppercase;
font-family: urwgothic, "Lucida Sans", "Lucida Sans Regular", "Lucida Grande", "Lucida Sans Unicode", Geneva, Verdana, sans-serif;
font-size: 15px;
height: 30px;
border: none;
border-radius: 20px;
box-shadow: var(--box-shadow);
transition: all 0.3s ease;
}
button {
margin: 5px;
background-color: var(--island-button-color);
color: white;
border: none;
border-radius: 20px;
font-family: urwgothic, "Lucida Sans", "Lucida Sans Regular", "Lucida Grande", "Lucida Sans Unicode", Geneva, Verdana, sans-serif;
font-size: 16px;
width: 40%;
max-width: 200px;
height: 30px;
box-shadow: var(--box-shadow);
transition: all 0.3s ease;
}
input:hover {
box-shadow: var(--box-shadow-dark);
}
button:hover {
background-color: rgb(45, 45, 45);
box-shadow: var(--box-shadow-dark);
}
</style>

View File

@ -0,0 +1,107 @@
<script lang="ts">
import { getCurrentLocation } from "$lib/scripts/getLocation";
import toast from "svelte-french-toast";
import Card from "./Card.svelte";
import type { CardConfig } from "./Card.types";
import type { NearestStationResponse } from "@owlboard/ts-types";
import { uuid } from "$lib/stores/uuid";
import { location } from "$lib/stores/location";
import InLineLoading from "$lib/navigation/InLineLoading.svelte";
import { apiGet } from "$lib/scripts/apiFetch";
import { onMount } from "svelte";
import { nearToMeCache } from "$lib/stores/nearToMeCache";
import LinkButton from "$lib/buttons/LinkButton.svelte";
import ScriptButton from "$lib/buttons/ScriptButton.svelte";
let errorMessage: string;
let stations: NearestStationResponse[] = [];
let blockLoading: boolean = true;
let config: CardConfig = {
title: "Near to Me",
showHelp: true,
showRefresh: true,
helpText: "Your location may not be accurate on desktop and laptop devices.",
onRefresh: getNearestStations,
refreshing: false,
};
function turnOnLocation() {
location.set(true);
getCurrentLocation();
toast.success("Done\nTo disable location, go to settings");
}
async function getNearestStations() {
// Get location, then fetch nearest stations and push to `stations` variable
config.refreshing = true;
const currentLocation = await getCurrentLocation();
const apiPath: string = `/api/v2/live/station/nearest/${currentLocation.latitude}/${currentLocation.longitude}`;
try {
const apiResponse = (await apiGet(apiPath)) as NearestStationResponse[];
stations = apiResponse;
nearToMeCache.set(apiResponse);
} catch (err) {
errorMessage = err as string;
} finally {
config.refreshing = false;
}
}
onMount(() => {
if ($location) {
if (!$uuid || $uuid === "null") {
errorMessage = "Register to use this feature";
} else {
if ($nearToMeCache.length) {
stations = $nearToMeCache;
}
getNearestStations();
}
}
blockLoading = false;
});
</script>
<Card {config}>
{#if blockLoading}
<InLineLoading />
{:else}
<div id="buttons">
{#if !$uuid || $uuid === "null"}
<LinkButton text="Register to use this feature" link="/more/reg" />
{:else if $location}
{#if !stations.length}
{#if errorMessage}
<p>{errorMessage}</p>
{:else}
<InLineLoading />
{/if}
{:else}
{#each stations as station}
<LinkButton text={`${station.NLCDESC} - ${station.miles}mi`} link={`/ldb?station=${station["3ALPHA"]}`} />
{/each}
{/if}
{:else}
<ScriptButton text={"Turn on Location"} fn={turnOnLocation} />
{/if}
</div>
{/if}
</Card>
<style>
#buttons {
display: flex;
flex-wrap: wrap;
justify-content: center;
align-items: center;
width: 95%;
margin: auto;
padding-top: 5px;
}
p {
text-align: center;
margin: auto;
}
</style>

View File

@ -0,0 +1,45 @@
<script lang="ts">
import Card from "./Card.svelte";
import type { CardConfig } from "./Card.types";
import { ql } from "$lib/stores/quick-links";
import LinkButton from "$lib/buttons/LinkButton.svelte";
let upstreamProps: CardConfig = {
title: "Quick Links",
showHelp: false,
showRefresh: false,
helpText: "",
onRefresh: () => {},
refreshing: false,
};
</script>
<Card config={upstreamProps}>
<div class="quick-links">
{#if !$ql.length}
<LinkButton text={"Add Quick Links"} link={"/more/settings"} />
{:else}
{#each $ql as link}
{#if link.length === 3}
<LinkButton text={link.toUpperCase()} link={`/ldb?station=${link.toLowerCase()}`} />
{:else if link.length === 4}
<LinkButton text={link.toUpperCase()} link={`/train?headcode=${link.toLowerCase()}`} />
{/if}
{/each}
{/if}
</div>
</Card>
<style>
.quick-links {
display: flex;
flex-wrap: wrap;
justify-content: center;
align-items: center;
width: 90%;
margin: auto;
padding-top: 5px;
}
</style>

View File

@ -1,47 +0,0 @@
<script lang="ts">
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>
</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);
}
</style>

View File

@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import { fade } from 'svelte/transition'; import { fade } from "svelte/transition";
export let variables = { title: '' }; export let variables = { title: "" };
</script> </script>
<div in:fade={{ duration: 250 }}> <div in:fade={{ duration: 250 }}>
@ -11,7 +11,7 @@
<style> <style>
span { span {
font-family: urwgothic, 'Lucida Sans', 'Lucida Sans Regular', 'Lucida Grande', 'Lucida Sans Unicode', Geneva, Verdana, sans-serif; font-family: urwgothic, "Lucida Sans", "Lucida Sans Regular", "Lucida Grande", "Lucida Sans Unicode", Geneva, Verdana, sans-serif;
font-weight: 600; font-weight: 600;
font-size: 20px; font-size: 20px;
color: var(--island-header-color); color: var(--island-header-color);

View File

@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import { fade } from 'svelte/transition'; import { fade } from "svelte/transition";
export let variables = { title: '' }; export let variables = { title: "" };
</script> </script>
<div in:fade={{ duration: 150 }} out:fade={{ duration: 150 }}> <div in:fade={{ duration: 150 }} out:fade={{ duration: 150 }}>
@ -11,7 +11,7 @@
<style> <style>
span { span {
font-family: urwgothic, 'Lucida Sans', 'Lucida Sans Regular', 'Lucida Grande', 'Lucida Sans Unicode', Geneva, Verdana, sans-serif; font-family: urwgothic, "Lucida Sans", "Lucida Sans Regular", "Lucida Grande", "Lucida Sans Unicode", Geneva, Verdana, sans-serif;
color: var(--island-header-color); color: var(--island-header-color);
font-weight: 600; font-weight: 600;
font-size: 20px; font-size: 20px;

View File

@ -1,54 +0,0 @@
<script lang="ts">
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>
</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);
}
</style>

View File

@ -1,8 +1,9 @@
<script lang="ts"> <script lang="ts">
import Island from '$lib/islands/island.svelte'; import Island from "$lib/islands/island.svelte";
import { ql } from '$lib/stores/quick-links'; import { ql } from "$lib/stores/quick-links";
import toast from "svelte-french-toast";
export let variables = { export let variables = {
title: 'Quick Links' title: "Quick Links",
}; };
let qlData: string[] = []; let qlData: string[] = [];
@ -11,44 +12,52 @@
console.log(qlData); console.log(qlData);
} }
let saveButton = 'Save'; let saveButton = "Save";
async function timeout(ms: number): Promise<any> { async function timeout(ms: number): Promise<any> {
return new Promise((resolve) => setTimeout(resolve, ms)); return new Promise((resolve) => setTimeout(resolve, ms));
} }
function save() {
toast.promise(saveQl(), {
loading: "Saving...",
success: "Quick Links saved!",
error: "Failed to save.",
});
}
async function saveQl() { async function saveQl() {
// Fetch the content of all text entries within the island then run ql.set([ARRAY OF INPUT CONTENT]) // Fetch the content of all text entries within the island then run ql.set([ARRAY OF INPUT CONTENT])
const inputs = document.getElementsByClassName('qlInput'); const inputs = document.getElementsByClassName("qlInput");
let inputLinks: string[] = []; let inputLinks: string[] = [];
for (let item of inputs) { for (let item of inputs) {
let text = (<HTMLInputElement>item)?.value; let text = (<HTMLInputElement>item)?.value;
if (text !== '') { if (text !== "") {
inputLinks.push(text); inputLinks.push(text);
} }
} }
console.log(inputLinks); console.log(inputLinks);
ql.set(inputLinks); ql.set(inputLinks);
saveButton = '&#10004;'; await timeout(1000);
await timeout(3000); saveButton = "Saved";
saveButton = 'Saved';
} }
function clearQl() { function clearQl() {
ql.set([]); ql.set([]);
saveButton = 'Saved'; saveButton = "Saved";
toast.success("Cleared Quick Links.");
} }
function addQlBox() { function addQlBox() {
saveButton = 'Save'; saveButton = "Save";
const updatedQl = [...$ql, '']; const updatedQl = [...$ql, ""];
$ql = updatedQl; $ql = updatedQl;
ql.set(updatedQl); ql.set(updatedQl);
} }
function handleClick(event: any) { function handleClick(event: any) {
// Handle the click event here // Handle the click event here
console.log('Island Clicked'); console.log("Island Clicked");
// You can access the `variables` passed to the Island component here if needed // You can access the `variables` passed to the Island component here if needed
} }
</script> </script>
@ -64,7 +73,7 @@
{/each} {/each}
<button on:click={addQlBox} id="qlAdd">+</button> <button on:click={addQlBox} id="qlAdd">+</button>
</div> </div>
<button on:click={saveQl}>{@html saveButton}</button> <button on:click={save}>{@html saveButton}</button>
<button on:click={clearQl}>Clear</button> <button on:click={clearQl}>Clear</button>
</Island> </Island>
@ -92,7 +101,7 @@
border: none; border: none;
border-radius: 20px; border-radius: 20px;
padding: 5px; padding: 5px;
font-family: urwgothic, 'Lucida Sans', 'Lucida Sans Regular', 'Lucida Grande', 'Lucida Sans Unicode', Geneva, Verdana, sans-serif; font-family: urwgothic, "Lucida Sans", "Lucida Sans Regular", "Lucida Grande", "Lucida Sans Unicode", Geneva, Verdana, sans-serif;
font-size: 16px; font-size: 16px;
font-weight: 400; font-weight: 400;
text-decoration: none; text-decoration: none;
@ -107,7 +116,7 @@
border: none; border: none;
border-radius: 20px; border-radius: 20px;
padding: 5px; padding: 5px;
font-family: urwgothic, 'Lucida Sans', 'Lucida Sans Regular', 'Lucida Grande', 'Lucida Sans Unicode', Geneva, Verdana, sans-serif; font-family: urwgothic, "Lucida Sans", "Lucida Sans Regular", "Lucida Grande", "Lucida Sans Unicode", Geneva, Verdana, sans-serif;
font-size: 16px; font-size: 16px;
font-weight: 400; font-weight: 400;
background-color: var(--island-button-color); background-color: var(--island-button-color);

View File

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

View File

@ -0,0 +1,106 @@
<script>
import { IconAlertCircle } from "@tabler/icons-svelte";
import { fly } from "svelte/transition";
export let alerts = [];
$: uniqueAlerts = [...new Set(alerts)];
let displayAlerts = false;
async function alertsToggle() {
displayAlerts = !displayAlerts;
}
function numberAsWord(number = 0) {
const words = ["zero", "one", "two", "three", "four", "five", "six", "seven", "eight", "nine"];
let word = words[number];
if (word) {
return word;
}
return number;
}
</script>
<div id="bar" on:click={alertsToggle} on:keypress={alertsToggle}>
<span class="image"><IconAlertCircle /></span>
{#if uniqueAlerts.length == 1}
<p id="bartext">There is one active alert</p>
{:else}
<p id="bartext">
There are {numberAsWord(uniqueAlerts.length)} 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>
{/if}
<style>
#bar {
position: relative;
margin-top: 0;
padding: 0;
width: 100%;
height: 40px;
background-color: var(--main-alert-color);
display: flex;
justify-content: space-between;
align-items: center;
z-index: 2;
}
.image {
margin-left: 15px;
margin-top: 4px;
}
#bartext {
color: white;
margin: auto;
font-weight: 600;
margin: 0 0 0 0;
padding: 0;
}
#arrow {
color: white;
font-family: Arial, Helvetica, sans-serif;
font-weight: 900;
border: none;
margin-right: 15px;
background-color: transparent;
transition-duration: 500ms;
transition-delay: 00ms;
}
#arrow:focus {
background-color: transparent;
}
#alerts {
position: absolute;
background-color: var(--main-alert-color);
opacity: 0.9;
width: 100%;
max-height: 80vh;
overflow-y: auto;
overflow-x: clip;
left: 0;
top: 89px;
z-index: 1;
}
.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,118 +0,0 @@
<script>
import { fly } from 'svelte/transition';
export let alerts = [];
$: uniqueAlerts = [...new Set(alerts)];
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;
}
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>
</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>
{/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);
}
</style>

View File

@ -1,12 +1,15 @@
<script> <script>
export let station = ''; export let station = "";
export let title = 'Loading...'; export let title = "Loading...";
import { onMount } from 'svelte'; import { onMount } from "svelte";
import Loading from '$lib/navigation/loading.svelte'; import Loading from "$lib/navigation/loading.svelte";
import OverlayIsland from '$lib/islands/overlay-island.svelte'; import OverlayIsland from "$lib/islands/overlay-island.svelte";
import AlertBar from '$lib/ldb/nrcc/alert-bar.svelte'; import AlertBar from "$lib/ldb/common/nrcc/alert-bar.svelte";
import Island from '$lib/islands/island.svelte'; import Island from "$lib/islands/island.svelte";
import { getApiUrl } from '$lib/scripts/upstream'; import { getApiUrl } from "$lib/scripts/upstream";
import TimeBar from "$lib/navigation/TimeBar.svelte";
import { IconBus, IconSailboat } from "@tabler/icons-svelte";
import toast from "svelte-french-toast";
let requestedStation; let requestedStation;
$: requestedStation = station; $: requestedStation = station;
@ -59,9 +62,9 @@
const data = await fetch(`${getApiUrl()}/api/v2/live/station/${requestedStation}/public`); const data = await fetch(`${getApiUrl()}/api/v2/live/station/${requestedStation}/public`);
jsonData = await data.json(); jsonData = await data.json();
} catch (error) { } catch (error) {
console.error('Error fetching data:', error); console.error("Error fetching data:", error);
dataExists = false; dataExists = false;
title = 'Not Found'; title = "Not Found";
} finally { } finally {
isLoading = false; // Clear loading state isLoading = false; // Clear loading state
} }
@ -72,38 +75,38 @@
let output; let output;
let change; let change;
switch (string) { switch (string) {
case 'Delayed': case "Delayed":
output = 'LATE'; output = "LATE";
change = 'changed'; change = "changed";
break; break;
case 'Cancelled': case "Cancelled":
output = 'CANC'; output = "CANC";
change = 'cancelled'; change = "cancelled";
break; break;
case 'On Time': case "On Time":
case 'On time': case "On time":
output = 'RT'; output = "RT";
change = ''; change = "";
break; break;
case '': case "":
output = '-'; output = "-";
change = ''; change = "";
break; break;
case undefined: case undefined:
output = '-'; output = "-";
change = ''; change = "";
break; break;
case 'No report': case "No report":
output = '-'; output = "-";
change = ''; change = "";
break; break;
case 'undefined': case "undefined":
output = false; output = false;
change = ''; change = "";
break; break;
default: default:
output = string; output = string;
change = 'changed'; change = "changed";
} }
return { data: output, changed: change }; return { data: output, changed: change };
} }
@ -141,6 +144,7 @@
} }
onMount(() => { onMount(() => {
toast("Register for more detailed departure boards")
if (requestedStation && jsonData === null) { if (requestedStation && jsonData === null) {
fetchData(); fetchData();
} }
@ -151,10 +155,11 @@
<AlertBar {alerts} /> <AlertBar {alerts} />
{/if} {/if}
<TimeBar updatedTime={dataAge} />
{#if isLoading} {#if isLoading}
<Loading /> <Loading />
{:else if dataAge} {:else if dataAge}
<p id="timestamp">Updated: {dataAge.toLocaleTimeString()}</p>
{#if services.length} {#if services.length}
<table class="ldbTable"> <table class="ldbTable">
<tr> <tr>
@ -170,19 +175,19 @@
<tr> <tr>
<td class="origdest from" on:click={loadService(service.serviceID)} on:keypress={loadService(service.serviceID)}> <td class="origdest from" on:click={loadService(service.serviceID)} on:keypress={loadService(service.serviceID)}>
{#if Array.isArray(service.origin?.location)} {#if Array.isArray(service.origin?.location)}
{service.origin.location[0]['locationName'] + ' & ' + service.origin.location[1]['locationName']} {service.origin.location[0]["locationName"] + " & " + service.origin.location[1]["locationName"]}
{:else} {:else}
{service.origin?.location?.locationName || ''} {service.origin?.location?.locationName || ""}
{/if} {/if}
</td> </td>
<td class="origdest to" on:click={loadService(service.serviceID)} on:keypress={loadService(service.serviceID)}> <td class="origdest to" on:click={loadService(service.serviceID)} on:keypress={loadService(service.serviceID)}>
{#if Array.isArray(service.destination?.location)} {#if Array.isArray(service.destination?.location)}
{service.destination.location[0]['locationName'] + ' & ' + service.destination.location[0]['locationName']} {service.destination.location[0]["locationName"] + " & " + service.destination.location[0]["locationName"]}
{:else} {:else}
{service.destination?.location?.locationName || ''} {service.destination?.location?.locationName || ""}
{/if} {/if}
</td> </td>
<td class="plat">{service.platform || '-'}</td> <td class="plat">{service.platform || "-"}</td>
<td class="time">{parseTime(service.sta).data}</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.eta).changed}">{parseTime(service.eta).data}</td>
<td class="time">{parseTime(service.std).data}</td> <td class="time">{parseTime(service.std).data}</td>
@ -192,9 +197,9 @@
<tr <tr
><td colspan="7"> ><td colspan="7">
<p class="service-detail"> <p class="service-detail">
A {service.operator || 'Unknown'} service A {service.operator || "Unknown"} service
{#if service['length']} {#if service["length"]}
with {service['length'] || 'some'} coaches with {service["length"] || "some"} coaches
{/if} {/if}
</p> </p>
{#if service.delayReason} {#if service.delayReason}
@ -212,7 +217,7 @@
{/if} {/if}
{#if busServices.length} {#if busServices.length}
<br /> <br />
<img class="transport-mode" src="/images/transport-modes/bus.svg" alt="Bus services" /><br /> <IconBus /><br />
<span class="table-head-text">Bus Services</span> <span class="table-head-text">Bus Services</span>
<table class="ldbTable"> <table class="ldbTable">
<tr> <tr>
@ -225,9 +230,11 @@
</tr> </tr>
{#each busServices as service} {#each busServices as service}
<tr> <tr>
<td class="origdest from" on:click={loadBusService(service.serviceID)} on:keypress={loadBusService(service.serviceID)}>{service.origin?.location?.locationName || ''}</td> <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)} <td class="origdest to" on:click={loadBusService(service.serviceID)} on:keypress={loadBusService(service.serviceID)}
>{service.destination?.location?.locationName || ''}</td >{service.destination?.location?.locationName || ""}</td
> >
<td class="time">{parseTime(service.sta).data}</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.eta).changed}">{parseTime(service.eta).data}</td>
@ -238,7 +245,7 @@
<tr <tr
><td colspan="7"> ><td colspan="7">
<p class="service-detail"> <p class="service-detail">
A {service.operator || 'Unknown'} service A {service.operator || "Unknown"} service
</p> </p>
{#if service.delayReason} {#if service.delayReason}
<p class="service-detail">{service.delayReason}</p> <p class="service-detail">{service.delayReason}</p>
@ -253,7 +260,7 @@
{/if} {/if}
{#if ferryServices.length} {#if ferryServices.length}
<br /> <br />
<img class="transport-mode" src="/images/transport-modes/ferry.svg" alt="Bus services" /><br /> <IconSailboat /><br />
<span class="table-head-text">Ferry Services</span> <span class="table-head-text">Ferry Services</span>
<table class="ldbTable"> <table class="ldbTable">
<tr> <tr>
@ -266,8 +273,8 @@
</tr> </tr>
{#each ferryServices as service} {#each ferryServices as service}
<tr> <tr>
<td class="origdest from">{service.origin?.location?.locationName || ''}</td> <td class="origdest from">{service.origin?.location?.locationName || ""}</td>
<td class="origdest to">{service.destination?.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.sta).data}</td>
<td class="time {parseTime(service.eta).changed}">{parseTime(service.eta).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.std).data}</td>
@ -318,9 +325,12 @@
<td>{serviceDetail.previousCallingPoints.callingPointList.callingPoint.locationName}</td> <td>{serviceDetail.previousCallingPoints.callingPointList.callingPoint.locationName}</td>
<td>{serviceDetail.previousCallingPoints.callingPointList.callingPoint.st}</td> <td>{serviceDetail.previousCallingPoints.callingPointList.callingPoint.st}</td>
<td <td
class="time {parseTime(serviceDetail.previousCallingPoints.callingPointList.callingPoint.at || serviceDetail.previousCallingPoints.callingPointList.callingPoint.et) class="time {parseTime(
.changed}" serviceDetail.previousCallingPoints.callingPointList.callingPoint.at || serviceDetail.previousCallingPoints.callingPointList.callingPoint.et
>{parseTime(serviceDetail.previousCallingPoints.callingPointList.callingPoint.at || serviceDetail.previousCallingPoints.callingPointList.callingPoint.et).data}</td ).changed}"
>{parseTime(
serviceDetail.previousCallingPoints.callingPointList.callingPoint.at || serviceDetail.previousCallingPoints.callingPointList.callingPoint.et
).data}</td
> >
</tr> </tr>
{/if} {/if}
@ -355,11 +365,6 @@
{/if} {/if}
<style> <style>
#timestamp {
margin: auto;
text-align: left;
font-size: 14px;
}
.ldbTable { .ldbTable {
width: 100%; width: 100%;
min-width: 300px; min-width: 300px;
@ -394,9 +399,6 @@
.transport-mode { .transport-mode {
width: 50px; width: 50px;
} }
#timestamp {
font-size: 16px;
}
} }
@media (min-width: 1000px) { @media (min-width: 1000px) {
table { table {

View File

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

View File

@ -1,18 +1,21 @@
<script lang="ts"> <script lang="ts">
import TableGenerator from './table/table-generator.svelte'; import TableGenerator from "./table/table-generator.svelte";
import Loading from '$lib/navigation/loading.svelte'; import Loading from "$lib/navigation/loading.svelte";
import type { ApiResponse, StaffLdb } from '@owlboard/ts-types'; import type { StaffLdb } from "@owlboard/ts-types";
import { detailInit, defineDetail } from './train-detail'; import { detailInit, defineDetail } from "./train-detail";
import TrainDetail from './train-detail.svelte'; import TrainDetail from "./train-detail.svelte";
import { fetchStaffLdb } from './fetch'; import { fetchStaffLdb } from "./fetch";
import AlertBar from '../nrcc/alert-bar.svelte'; import AlertBar from "../common/nrcc/alert-bar.svelte";
import TimeBar from "$lib/navigation/TimeBar.svelte";
import { onMount } from "svelte";
import { IconBus, IconSailboat } from "@tabler/icons-svelte";
export let station: string; export let station: string;
export let title: string | undefined = 'Loading...'; export let title: string | undefined = "Loading...";
let errorDetail = { let errorDetail = {
code: '', code: "",
message: '' message: "",
}; };
let nrcc: string[] = []; let nrcc: string[] = [];
@ -26,7 +29,10 @@
console.log(`Station: ${station}`); console.log(`Station: ${station}`);
let updatedTime: Date;
async function callFetch(station: string): Promise<StaffLdb> { async function callFetch(station: string): Promise<StaffLdb> {
console.log("callFetch function called");
const data = await fetchStaffLdb(station); const data = await fetchStaffLdb(station);
if (data.data) { if (data.data) {
title = data.data.locationName; title = data.data.locationName;
@ -34,15 +40,29 @@
for (const msg of data.data.nrccMessages) { for (const msg of data.data.nrccMessages) {
nrcc.push(msg.xhtmlMessage); nrcc.push(msg.xhtmlMessage);
} }
nrcc = nrcc; // Reassign to ensure Svelte reloads
}
if (data.data.generatedAt) {
updatedTime = new Date(data.data.generatedAt);
} }
return data.data; return data.data;
} }
errorDetail.code = data.obStatus.toString() || 'UNKNOWN'; errorDetail.code = data.obStatus.toString() || "UNKNOWN";
errorDetail.message = data.obMsg || 'An unknown error occoured'; errorDetail.message = data.obMsg || "An unknown error occoured";
throw new Error('Unable to Fetch Data'); throw new Error("Unable to Fetch Data");
} }
onMount(async () => {
console.log("staff-ldb component mounted");
});
</script> </script>
{#if nrcc.length}
<AlertBar alerts={nrcc} />
{/if}
<TimeBar bind:updatedTime />
{#key detail} {#key detail}
{#if detail.show} {#if detail.show}
<TrainDetail {detail} close={hideDetail} /> <TrainDetail {detail} close={hideDetail} />
@ -53,24 +73,21 @@
<Loading /> <Loading />
{:then data} {:then data}
{#if data} {#if data}
<p class="generatedTime">Updated: {new Date(data.generatedAt).toLocaleTimeString()}</p>
{#if data.trainServices?.length} {#if data.trainServices?.length}
<TableGenerator services={data.trainServices} click={showDetail} /> <TableGenerator services={data.trainServices} click={showDetail} />
{/if} {/if}
{#if data.busServices?.length} {#if data.busServices?.length}
<img class="transport-mode" src="/images/transport-modes/bus.svg" alt="Bus services" /><br /> <IconBus />
<br />
<span class="table-head-text">Bus Services</span> <span class="table-head-text">Bus Services</span>
<TableGenerator services={data.busServices} click={showDetail} /> <TableGenerator services={data.busServices} click={showDetail} />
{/if} {/if}
{#if data.ferryServices?.length} {#if data.ferryServices?.length}
<img class="transport-mode" src="/images/transport-modes/ferry.svg" alt="Ferry services" /><br /> <IconSailboat />
<br>
<span class="table-head-text">Ferry Services</span> <span class="table-head-text">Ferry Services</span>
<TableGenerator services={data.ferryServices} click={showDetail} /> <TableGenerator services={data.ferryServices} click={showDetail} />
{/if} {/if}
{#if nrcc.length}
<AlertBar alerts={nrcc} />
{/if}
<!-- NRCC Alerts are not available -->
{/if} {/if}
{:catch} {:catch}
<h2>Error</h2> <h2>Error</h2>
@ -79,10 +96,6 @@
{/await} {/await}
<style> <style>
.transport-mode {
padding-top: 20px;
height: 17px;
}
.table-head-text { .table-head-text {
color: white; color: white;
} }

View File

@ -1,8 +1,9 @@
<script lang="ts"> <script lang="ts">
import Reason from '$lib/raw-fetchers/reason.svelte'; import Reason from "$lib/raw-fetchers/reason.svelte";
import { tocs as tocMap } from '$lib/stores/tocMap'; import { tocs as tocMap } from "$lib/stores/tocMap";
import type { TrainServices, ServiceLocation } from '@owlboard/ts-types'; import type { TrainServices, ServiceLocation } from "@owlboard/ts-types";
import { fade } from "svelte/transition";
export let services: TrainServices[]; export let services: TrainServices[];
export let click: Function; export let click: Function;
@ -17,7 +18,7 @@
for (const location of locations) { for (const location of locations) {
tiplocs.push(location.tiploc); tiplocs.push(location.tiploc);
} }
return tiplocs.join(' & '); return tiplocs.join(" & ");
} }
async function classGenerator(service: TrainServices) { async function classGenerator(service: TrainServices) {
@ -28,21 +29,21 @@
let platArr: string[] = []; let platArr: string[] = [];
if (service.isCancelled) { if (service.isCancelled) {
otherArr.push('canc'); otherArr.push("canc");
} }
if (service.serviceIsSupressed) { if (service.serviceIsSupressed) {
otherArr.push('nonPass'); otherArr.push("nonPass");
} }
if (service.platformIsHidden) { if (service.platformIsHidden) {
platArr.push('nonPass'); platArr.push("nonPass");
} }
function checkLateEarly(originalTime: Date | undefined, comparedTime: Date | undefined, arr: string[]) { function checkLateEarly(originalTime: Date | undefined, comparedTime: Date | undefined, arr: string[]) {
if (originalTime !== undefined && comparedTime instanceof Date) { if (originalTime !== undefined && comparedTime instanceof Date) {
if (originalTime < comparedTime) { if (originalTime < comparedTime) {
arr.push('late'); arr.push("late");
} else if (originalTime > comparedTime) { } else if (originalTime > comparedTime) {
arr.push('early'); arr.push("early");
} }
} }
} }
@ -53,18 +54,18 @@
checkLateEarly(service.std, service.atd, depArr); checkLateEarly(service.std, service.atd, depArr);
return { return {
other: otherArr.join(' '), other: otherArr.join(" "),
arr: arrArr.join(' '), arr: arrArr.join(" "),
dep: depArr.join(' '), dep: depArr.join(" "),
plat: platArr.join(' ') plat: platArr.join(" "),
}; };
} }
function fmtTime(date: Date | string | undefined): string | false { function fmtTime(date: Date | string | undefined): string | false {
if (typeof date === 'string') return date; if (typeof date === "string") return date;
if (date instanceof Date) { if (date instanceof Date) {
const hours = date.getHours().toString().padStart(2, '0'); const hours = date.getHours().toString().padStart(2, "0");
const minutes = date.getMinutes().toString().padStart(2, '0'); const minutes = date.getMinutes().toString().padStart(2, "0");
return `${hours}:${minutes}`; return `${hours}:${minutes}`;
} else { } else {
return false; return false;
@ -74,7 +75,7 @@
<p class="smallScreen">Your display is too small to view this data</p> <p class="smallScreen">Your display is too small to view this data</p>
<p class="smallScreen">Try rotating your device</p> <p class="smallScreen">Try rotating your device</p>
<table> <table in:fade={{ duration: 500 }}>
<tr> <tr>
<th class="id">ID</th> <th class="id">ID</th>
<th class="from">From</th> <th class="from">From</th>
@ -104,15 +105,15 @@
<!-- DESTINATION --> <!-- DESTINATION -->
<td class="loc to {classes.other}">{#await formatLocations(service.destination) then dest}<span class="locName">{dest}</span>{/await}</td> <td class="loc to {classes.other}">{#await formatLocations(service.destination) then dest}<span class="locName">{dest}</span>{/await}</td>
<!-- PLATFORM --> <!-- PLATFORM -->
<td class="plat {classes.other} {classes.plat}">{service.platform || '-'}</td> <td class="plat {classes.other} {classes.plat}">{service.platform || "-"}</td>
<!-- SCHEDULED ARR --> <!-- SCHEDULED ARR -->
<td class="time schTime {classes.other}">{fmtTime(service?.sta) || '-'}</td> <td class="time schTime {classes.other}">{fmtTime(service?.sta) || "-"}</td>
<!-- EXPECTED/ACTUAL ARR --> <!-- EXPECTED/ACTUAL ARR -->
<td class="time {classes.other} {classes.arr}">{fmtTime(service.eta) || fmtTime(service.ata) || '-'}</td> <td class="time {classes.other} {classes.arr}">{fmtTime(service.eta) || fmtTime(service.ata) || "-"}</td>
<!-- SCHEDULED DEP --> <!-- SCHEDULED DEP -->
<td class="time schTime {classes.other}">{fmtTime(service.std) || '-'}</td> <td class="time schTime {classes.other}">{fmtTime(service.std) || "-"}</td>
<!-- EXPECTED/ACTUAL DEP --> <!-- EXPECTED/ACTUAL DEP -->
<td class="time {classes.other} {classes.dep}">{fmtTime(service.etd) || fmtTime(service.atd) || '-'}</td> <td class="time {classes.other} {classes.dep}">{fmtTime(service.etd) || fmtTime(service.atd) || "-"}</td>
{/await} {/await}
</tr> </tr>
<tr> <tr>
@ -124,13 +125,13 @@
{#if service.delayReason} {#if service.delayReason}
<br /> <br />
<span class="delayTxt"> <span class="delayTxt">
<Reason type={'delay'} code={service.delayReason} /> <Reason type={"delay"} code={service.delayReason} />
</span> </span>
{/if} {/if}
{#if service.cancelReason} {#if service.cancelReason}
<br /> <br />
<span class="cancTxt"> <span class="cancTxt">
<Reason type={'cancel'} code={service.cancelReason} /> <Reason type={"cancel"} code={service.cancelReason} />
</span> </span>
{/if} {/if}
</td> </td>

View File

@ -1,15 +1,15 @@
<script> <script>
import OverlayIsland from '$lib/islands/overlay-island.svelte'; import OverlayIsland from "$lib/islands/overlay-island.svelte";
import { fade } from 'svelte/transition'; import { fade } from "svelte/transition";
import Reason from '$lib/raw-fetchers/reason.svelte'; import Reason from "$lib/raw-fetchers/reason.svelte";
import { uuid } from '$lib/stores/uuid'; import { uuid } from "$lib/stores/uuid";
import StylesToc from '$lib/train/styles-toc.svelte'; import StylesToc from "$lib/train/styles-toc.svelte";
import { getApiUrl } from '$lib/scripts/upstream'; import { getApiUrl } from "$lib/scripts/upstream";
export let detail = { export let detail = {
uid: '', uid: "",
rid: '', rid: "",
headcode: '', headcode: "",
show: true show: true,
}; };
export let close; export let close;
@ -22,15 +22,15 @@
console.log(`Requested Station: ${rid}`); console.log(`Requested Station: ${rid}`);
const url = `${getApiUrl()}/api/v2/live/train/rid/${rid}`; const url = `${getApiUrl()}/api/v2/live/train/rid/${rid}`;
const opt = { const opt = {
method: 'GET', method: "GET",
headers: { headers: {
uuid: $uuid uuid: $uuid,
} },
}; };
const data = await fetch(url, opt); const data = await fetch(url, opt);
return await data.json(); return await data.json();
} catch (error) { } catch (error) {
console.error('Error fetching data:', error); console.error("Error fetching data:", error);
} }
} }
@ -40,29 +40,29 @@
try { try {
const result = Math.floor(location.lateness / 60); const result = Math.floor(location.lateness / 60);
if (result === 0) { if (result === 0) {
(string = 'RT'), (state = ''); (string = "RT"), (state = "");
} else if (result < 0) { } else if (result < 0) {
(string = -result + 'E'), (state = 'early'); (string = -result + "E"), (state = "early");
} else if (result > 0) { } else if (result > 0) {
(string = result + 'L'), (state = 'late'); (string = result + "L"), (state = "late");
} }
} catch { } catch {
(string = ''), (state = ''); (string = ""), (state = "");
} }
} else if (location.arrivalType === 'Delayed') { } else if (location.arrivalType === "Delayed") {
(string = ''), (state = 'late'); (string = ""), (state = "late");
} else { } else {
(string = ''), (state = 'noreport'); (string = ""), (state = "noreport");
} }
return { return {
string: string, string: string,
state: state state: state,
}; };
} }
function parseTime(date) { function parseTime(date) {
const parsedTime = date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); const parsedTime = date.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
return parsedTime !== 'Invalid Date' ? parsedTime : null; return parsedTime !== "Invalid Date" ? parsedTime : null;
} }
function parseTimes(service) { function parseTimes(service) {
@ -79,19 +79,19 @@
parsedEtd = parseTime(etd), parsedEtd = parseTime(etd),
parsedAtd = parseTime(atd); parsedAtd = parseTime(atd);
if (service.isCancelled) { if (service.isCancelled) {
(parsedEta = 'CANC'), (parsedEtd = 'CANC'); (parsedEta = "CANC"), (parsedEtd = "CANC");
} }
let times = { let times = {
sta: parsedSta || '-', sta: parsedSta || "-",
eata: parsedEta || parsedAta || '-', eata: parsedEta || parsedAta || "-",
aEst: parsedEta ? 'estimate' : '', aEst: parsedEta ? "estimate" : "",
std: parsedStd || '-', std: parsedStd || "-",
eatd: parsedEtd || parsedAtd || '-', eatd: parsedEtd || parsedAtd || "-",
dEst: parsedEtd ? 'estimate' : '' dEst: parsedEtd ? "estimate" : "",
}; };
if (service.isCancelled) { if (service.isCancelled) {
(parsedEta = 'CANC'), (parsedEtd = 'CANC'); (parsedEta = "CANC"), (parsedEtd = "CANC");
(times.aEst = 'canc'), (times.dEst = 'canc'); (times.aEst = "canc"), (times.dEst = "canc");
} }
return times; return times;
} }
@ -139,16 +139,16 @@
{#each train.GetServiceDetailsResult.locations.location as location} {#each train.GetServiceDetailsResult.locations.location as location}
<tr> <tr>
<td class="location {location?.isPass === 'true' ? 'pass' : ''}">{location.tiploc}</td> <td class="location {location?.isPass === 'true' ? 'pass' : ''}">{location.tiploc}</td>
<td class={location?.isPass === 'true' ? 'pass' : ''}>{location.platform || ''}</td> <td class={location?.isPass === "true" ? "pass" : ""}>{location.platform || ""}</td>
{#await parseTimes(location)} {#await parseTimes(location)}
<td /> <td />
<td /> <td />
<td /> <td />
<td /> <td />
{:then times} {:then times}
<td class={location?.isPass === 'true' ? 'pass' : ''}>{times.sta}</td> <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.aEst}">{times.eata}</td>
<td class={location?.isPass === 'true' ? 'pass' : ''}>{times.std}</td> <td class={location?.isPass === "true" ? "pass" : ""}>{times.std}</td>
<td class="{location?.isPass === 'true' ? 'pass' : ''} {times.dEst}">{times.eatd}</td> <td class="{location?.isPass === 'true' ? 'pass' : ''} {times.dEst}">{times.eatd}</td>
{/await} {/await}
{#await parseDelay(location)} {#await parseDelay(location)}

View File

@ -10,9 +10,9 @@ export interface Detail {
export function detailInit(): Detail { export function detailInit(): Detail {
const detail: Detail = { const detail: Detail = {
show: false, show: false,
headcode: '', headcode: "",
rid: '', rid: "",
uid: '' uid: "",
}; };
return detail; return detail;
} }
@ -23,7 +23,7 @@ export function defineDetail(rid: string, uid: string, tid: string) {
rid: rid, rid: rid,
uid: uid, uid: uid,
headcode: tid, headcode: tid,
show: true show: true,
}; };
return detail; return detail;
} }

View File

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

View File

@ -1,35 +1,32 @@
/* FONTS */ /* FONTS */
@font-face { @font-face {
font-family: 'firamono'; font-family: "firamono";
src: url('/font/firamono/firamono-regular.woff2') format('woff2'), url('/font/firamono/firamono-regular.woff') format('woff'), 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-weight: normal;
font-style: normal; font-style: normal;
} }
@font-face { @font-face {
font-family: 'firamono'; 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'); src: url("/font/firamono/firamono-500.woff2") format("woff2"), url("/font/firamono/firamono-500.woff") format("woff");
font-weight: 500; font-weight: 500;
font-style: normal; font-style: normal;
} }
@font-face { @font-face {
font-family: 'urwgothic'; 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'); src: url("/font/urwgothic/urwgothic.woff2") format("woff2"), url("/font/urwgothic/urwgothic.woff") format("woff");
font-weight: normal; font-weight: normal;
font-style: normal; font-style: normal;
} }
@font-face { @font-face {
font-family: 'urwgothic'; font-family: "urwgothic";
src: url('/font/urwgothic/urwgothicDemi.woff2') format('woff2'), url('/font/urwgothic/urwgothicDemi.woff') format('woff'), 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-weight: 900;
font-style: normal; font-style: normal;
} }
@font-face { @font-face {
font-family: 'ubuntu'; font-family: "ubuntu";
src: url('/font/ubuntumono/ubuntumono-regular.woff2') format('woff2'), url('/font/ubuntumono/ubuntumono-regular.woff') format('woff'), 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-weight: 400;
font-style: normal; font-style: normal;
} }

View File

@ -0,0 +1,28 @@
<script lang="ts">
export let size: string = "1em";
export let color: string = "aliceblue";
</script>
<div class="spinner" style="--spinner-size: {size}; --spinner-color: {color};" />
<style>
.spinner {
display: inline-block;
width: var(--spinner-size);
height: var(--spinner-size);
border: 2px solid transparent;
border-top-color: var(--spinner-color);
border-radius: 50%;
animation: spin 1s linear infinite;
margin: 10px;
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
</style>

View File

@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import { logout } from '$lib/libauth'; import { logout } from "$lib/libauth";
async function logoutAction() { async function logoutAction() {
await logout(); await logout();

View File

@ -0,0 +1,57 @@
<script lang="ts">
import { onMount } from "svelte";
import { fade } from "svelte/transition";
export let updatedTime: Date | undefined;
let currentTime: string = "00:00:00";
let updateDisplay: string;
function updateTime() {
const now = new Date();
const hours = now.getHours().toString().padStart(2, "0");
const mins = now.getMinutes().toString().padStart(2, "0");
const secs = now.getSeconds().toString().padStart(2, "0");
currentTime = `${hours}:${mins}:${secs}`;
}
onMount(() => {
console.log("TimeBar component mounted");
updateTime();
const interval = setInterval(updateTime, 250);
return () => clearInterval(interval);
});
</script>
<div id="TimeBar">
{#if updatedTime}
<span in:fade class="updated-time">Updated: {updatedTime.toLocaleTimeString()}</span>
{:else}
<span />
{/if}
<span class="current-time">{currentTime}</span>
</div>
<style>
#TimeBar {
width: 100%;
background-color: transparent;
height: 30px;
display: flex;
justify-content: space-between;
align-items: center;
padding: 0;
}
.updated-time {
font-family: firamono, "Courier New", Courier, monospace;
margin-left: 10px;
font-size: 14px;
}
.current-time {
font-family: firamono, "Courier New", Courier, monospace;
font-weight: 900;
vertical-align: middle;
color: aliceblue;
margin-right: 10px;
font-size: 17px;
}
</style>

View File

@ -1,13 +1,10 @@
<script> <script>
export let title = 'title'; export let title = "title";
</script> </script>
<div class="headerBar"> <div class="headerBar">
<a href="/"> <a href="/">
<picture> <img src="/images/logo/wide_logo.svg" alt="OwlBoard Logo" />
<source srcset="/images/logo/wide_logo.svg" type="image/svg+xml" />
<img src="/images/logo/wide_logo_200.png" alt="OwlBoard Logo" />
</picture>
</a> </a>
<header>{title}</header> <header>{title}</header>
</div> </div>

View File

@ -1,5 +1,5 @@
<script> <script>
import { fade } from 'svelte/transition'; import { fade } from "svelte/transition";
</script> </script>
<div id="container" in:fade={{ delay: 150, duration: 250 }} out:fade={{ duration: 250 }}> <div id="container" in:fade={{ delay: 150, duration: 250 }} out:fade={{ duration: 250 }}>
@ -22,7 +22,7 @@
border: solid 5px var(--overlay-island-bg-color); border: solid 5px var(--overlay-island-bg-color);
border-bottom-color: white; border-bottom-color: white;
border-radius: 50%; border-radius: 50%;
content: ''; content: "";
height: 40px; height: 40px;
width: 40px; width: 40px;
position: absolute; position: absolute;

View File

@ -1,20 +1,21 @@
<script> <script lang="ts">
const links = [ const links = [
{ {
title: 'Home', title: "Home",
path: '/', path: "/",
svgPath: '/images/navigation/home.svg' icon: IconHome,
} },
]; ];
import { page } from '$app/stores'; import { page } from "$app/stores";
import { IconHome } from "@tabler/icons-svelte";
</script> </script>
<footer> <footer>
{#each links as item} {#each links as item}
<a class="footerLink" href={item.path} class:active={$page.url.pathname == item.path}> <a class="footerLink" href={item.path} class:active={$page.url.pathname == item.path}>
<img src={item.svgPath} alt={item.title} /> <svelte:component this={item.icon} />
<br /> <br />
<span>{item.title}</span> <span class="title">{item.title}</span>
</a> </a>
{/each} {/each}
<div class="data-source"> <div class="data-source">
@ -84,7 +85,7 @@
margin-top: 3px; margin-top: 3px;
padding: 0; padding: 0;
} }
span { .title {
margin: 0; margin: 0;
margin-bottom: 3px; margin-bottom: 3px;
padding: 0; padding: 0;

View File

@ -1,28 +1,29 @@
<script> <script>
const links = [ const links = [
{ {
title: 'Home', title: "Home",
path: '/', path: "/",
svgPath: '/images/navigation/home.svg' icon: IconHome,
}, },
{ {
title: 'PIS Finder', title: "PIS Finder",
path: '/pis', path: "/pis/",
svgPath: '/images/navigation/info.svg' icon: IconDialpad,
}, },
{ {
title: 'Menu', title: "Menu",
path: '/more', path: "/more/",
svgPath: '/images/navigation/more.svg' icon: IconMenu2,
} },
]; ];
import { page } from '$app/stores'; import { page } from "$app/stores";
import { IconHome, IconMenu2, IconDialpad } from "@tabler/icons-svelte";
</script> </script>
<footer> <footer>
{#each links as item} {#each links as item}
<a href={item.path} class:active={$page.url.pathname == item.path}> <a href={item.path} class:active={$page.url.pathname == item.path || $page.url.pathname == item.path + "/"}>
<img src={item.svgPath} alt={item.title} /> <svelte:component this={item.icon} />
<br /> <br />
<span>{item.title}</span> <span>{item.title}</span>
</a> </a>
@ -60,14 +61,6 @@
background-color: transparent; background-color: transparent;
} }
img {
height: 20px;
width: 20px;
margin: 0;
margin-top: 3px;
padding: 0;
}
span { span {
margin: 0; margin: 0;
margin-bottom: 3px; margin-bottom: 3px;

View File

@ -1,103 +0,0 @@
<script lang="ts">
import { welcome } from '$lib/stores/welcome';
import { fade } from 'svelte/transition';
import { version } from '$lib/stores/version';
let pageNum: number = 0;
function pageUp() {
pageNum++;
console.log(`Welcome page: ${pageNum}`);
}
function pageDn() {
pageNum--;
console.log(`Welcome page: ${pageNum}`);
}
function close() {
welcome.set(version);
}
const pageText: string[] = [
'<h3>A new way to find PIS Codes</h3>' +
"<p>You can now use a headcode to find a PIS code where there is only a partial match</p>" +
'<p>For now, you can see a PIS code where you need to skip some stops at the start,</p>' +
'<p>soon, you will be able to find PIS codes where you need to skip stops at the end.</p>' +
'<p>Check out our <a href="https://www.facebook.com/owlboard.support">Facebook page</a> where a video guide will be available soon.</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}
</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);
}
.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;
}
#buttonCentre {
position: absolute;
margin: auto;
left: 50%;
transform: translateX(-50%);
}
#buttonRight {
position: absolute;
margin: auto;
right: 50px;
}
div {
color: var(--island-text-color);
}
</style>

View File

@ -1,36 +1,36 @@
<script> <script lang="ts">
import { getApiUrl } from '$lib/scripts/upstream'; import { apiGet } from "$lib/scripts/apiFetch";
import { uuid } from '$lib/stores/uuid'; import type { ReasonCode } from "@owlboard/ts-types";
export let code = ''; export let code: string;
export let type = ''; export let type: string;
async function getDelay(code = '') { async function getDelay(code: string): Promise<string | undefined> {
console.log(`Fetching delay reason ${code}`);
const data = await getReason(code); const data = await getReason(code);
return data[0].lateReason || 'This train has been delayed'; if (data) {
return data[0].lateReason || "This train has been delayed";
}
} }
async function getCancel(code = '') { async function getCancel(code: string): Promise<string | undefined> {
console.log(`Fetching cancel reason ${code}`);
const data = await getReason(code); const data = await getReason(code);
return data[0].cancReason || 'This train has been cancelled'; if (data) {
return data[0].cancReason || "This train has been cancelled";
}
} }
async function getReason(code = '') { async function getReason(code: string): Promise<ReasonCode[] | undefined> {
const url = `${getApiUrl()}/api/v2/ref/reasonCode/${code}`; const apiString = `/api/v2/ref/reasonCode/${code}`;
const options = { try {
method: 'GET', const apiRes = (await apiGet(apiString)) as ReasonCode[];
headers: { return apiRes;
uuid: $uuid } catch (err) {
console.error("Unable to define reason code");
} }
};
const res = await fetch(url, options);
return await res.json();
} }
</script> </script>
{#if type === 'cancel'} {#if type === "cancel"}
{#await getCancel(code)} {#await getCancel(code)}
This train has been cancelled This train has been cancelled
{:then reason} {:then reason}
@ -38,7 +38,7 @@
{:catch} {:catch}
This train has been cancelled This train has been cancelled
{/await} {/await}
{:else if type === 'delay'} {:else if type === "delay"}
{#await getDelay(code)} {#await getDelay(code)}
This train has been delayed This train has been delayed
{:then reason} {:then reason}

View File

@ -0,0 +1,55 @@
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;
}
}
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,
},
};
try {
const res = await fetch(getUrlString() + path, options);
if (res.status === 401) {
throw new Error("Registration not accepted. Register at `Menu > Registration`");
}
if (!res.ok) {
throw new Error(`Failed: ${res.status}: ${res.statusText}`);
}
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

@ -0,0 +1,49 @@
export interface FeatureDetectionResult {
critical: boolean;
nice: boolean;
missing: string[];
}
export function featureDetect(): FeatureDetectionResult {
console.info("Running feature detection");
// Define critical features
const criticalFeatures = {
fetch: "fetch" in window,
localStorage: "localStorage" in window && window["localStorage"] !== null,
sessionStorage: "sessionStorage" in window && window["sessionStorage"] !== null,
promise: "Promise" in window,
};
// Define nice-to-have features
const niceToHaveFeatures = {
geolocation: "geolocation" in navigator,
serviceWorker: "serviceWorker" in navigator,
dialog: "HTMLDialogElement" in window,
popover: "showPopover" in HTMLElement.prototype, // Note: 'Popover' might need more specific checks based on implementation
};
// Initialize result object
const result = {
critical: true,
nice: true,
missing: [],
};
// Check critical features
for (const [feature, available] of Object.entries(criticalFeatures)) {
if (!available) {
result.critical = false;
result.missing.push(feature);
}
}
// Check nice-to-have features
for (const [feature, available] of Object.entries(niceToHaveFeatures)) {
if (!available) {
result.nice = false;
result.missing.push(feature);
}
}
return result;
}

View File

@ -0,0 +1,39 @@
export async function getCurrentLocation(): Promise<locationObj> {
console.debug("Fetching location");
if (typeof window === "undefined") {
console.error("Location fetch has run serverside - invalid method");
}
if (!navigator.geolocation) {
console.error("Geolocation is not supported");
throw new Error("Geolocation is not supported");
}
return new Promise((resolve, reject) => {
const options = {
timeout: 10000,
maximumAge: 300,
};
navigator.geolocation.getCurrentPosition(
(position) => {
console.debug(`Position obtained: ${position.coords}`);
resolve({
latitude: position.coords.latitude,
longitude: position.coords.longitude,
});
},
(error) => {
console.error("Error fetching location: ", error);
reject(error);
}
);
options;
});
}
export interface locationObj {
latitude: number;
longitude: number;
}

View File

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

View File

@ -0,0 +1,25 @@
// src/stores.js
import { writable, type Writable } from "svelte/store";
import { browser } from "$app/environment";
// Initialize the store with a boolean value from local storage
export const location: Writable<boolean> = writable(fromLocalStorage("location", false));
toLocalStorage(location, "location");
function fromLocalStorage(storageKey: string, fallback: boolean): boolean {
if (browser) {
const storedValue = localStorage.getItem(storageKey);
if (storedValue !== null && storedValue !== "undefined") {
return storedValue === "true";
}
}
return fallback;
}
function toLocalStorage(store: Writable<boolean>, storageKey: string) {
if (browser) {
store.subscribe((value: boolean) => {
localStorage.setItem(storageKey, String(value));
});
}
}

View File

@ -0,0 +1,26 @@
import { writable, type Writable } from "svelte/store";
import { browser } from "$app/environment";
import type { NearestStationResponse } from "@owlboard/ts-types";
export const nearToMeCache = writable(fromSessionStorage("nearToMeCache", []));
toSessionStorage(nearToMeCache, "nearToMeCache");
function fromSessionStorage(storageKey: string, fallback: NearestStationResponse[]): NearestStationResponse[] {
if (browser) {
const storedValue = sessionStorage.getItem(storageKey);
if (storedValue !== "undefined" && storedValue !== null) {
return typeof fallback === "object" ? JSON.parse(storedValue) : storedValue;
}
}
return fallback;
}
function toSessionStorage(store: Writable<NearestStationResponse[]>, storageKey: string) {
if (browser) {
store.subscribe((value) => {
let storageValue = typeof value === "object" ? JSON.stringify(value.sort()) : value;
sessionStorage.setItem(storageKey, storageValue);
});
}
}

View File

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

View File

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

View File

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

View File

@ -1,3 +1,2 @@
export const version: string = '2024.2.1'; export const version: string = "2024.07.4";
export const versionTag: string = ''; export const versionTag: string = "";
export const showWelcome: boolean = true;

View File

@ -1,23 +0,0 @@
import { writable, type Writable } from 'svelte/store';
import { browser } from '$app/environment';
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;
}
}
return fallback;
}
function toLocalStorage(store: Writable<any>, storageKey: string) {
if (browser) {
store.subscribe((value: string) => {
localStorage.setItem(storageKey, value);
});
}
}

View File

@ -34,6 +34,7 @@
--overlay-island-bg-color: #3c6f79; --overlay-island-bg-color: #3c6f79;
--box-shadow-color: rgba(0, 0, 0, 0.19); --box-shadow-color: rgba(0, 0, 0, 0.19);
--box-shadow: 2px 2px 4px rgba(0, 0, 0, 0.19); --box-shadow: 2px 2px 4px rgba(0, 0, 0, 0.19);
--box-shadow-dark: 2px 2px 4px rgba(0, 0, 0, 0.392);
--main-alert-color: #ed6d00; --main-alert-color: #ed6d00;
--second-alert-color: #e77f00; --second-alert-color: #e77f00;
--main-warning-color: orange; --main-warning-color: orange;

View File

@ -1,18 +1,20 @@
<script lang="ts"> <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> </script>
{#if pisObject} {#if pisObject}
{#if typeof pisObject === 'string' || typeof pisObject === 'number'} {#if typeof pisObject === "string" || typeof pisObject === "number"}
<span class="pis">PIS: {pisObject}</span> <span class="pis">PIS: {pisObject}</span>
{:else if pisObject['skipCount'] === 0} {:else if pisObject["skipCount"] === 0}
<span class="pis">PIS: {pisObject.code}</span> <span class="pis">PIS: {pisObject.code}</span>
{:else if pisObject['skipCount'] > 0} {:else if pisObject["skipCount"] > 0}
<span class="pis">PIS: {pisObject.code}</span> <span class="pis">PIS: {pisObject.code}</span>
<br> <br />
<span class="pis-text">(skip first {pisObject.skipType}{#if pisObject.skipCount > 1} {" " + pisObject.skipCount} stops{:else} stop{/if})</span> <span class="pis-text"
>(skip {pisObject.skipType}{#if pisObject.skipCount > 1} {" " + pisObject.skipCount} stops{:else} stop{/if})</span
>
{/if} {/if}
{/if} {/if}

View File

@ -2,7 +2,7 @@
export let toc: string; export let toc: string;
export let full: boolean = false; 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;

View File

@ -1,28 +1,31 @@
<script> <script lang="ts">
import { fly } from 'svelte/transition'; import { fly } from "svelte/transition";
import { uuid } from '$lib/stores/uuid'; import { uuid } from "$lib/stores/uuid";
import LoadingText from '$lib/navigation/loading-text.svelte'; import LoadingText from "$lib/navigation/loading-text.svelte";
import StylesToc from './styles-toc.svelte'; import StylesToc from "./styles-toc.svelte";
import { getApiUrl } from '$lib/scripts/upstream'; import { getApiUrl } from "$lib/scripts/upstream";
import PisHandler from '$lib/train/pis-handler.svelte'; import PisHandler from "$lib/train/pis-handler.svelte";
export let service = ''; import type { OB_TrainTT_service } from "@owlboard/ts-types";
import TrainIcons from "./trainIcons.svelte";
export let service: OB_TrainTT_service;
let isExpanded = false; let isExpanded = false;
async function getTrainByUID(tuid = '') { async function getTrainByUID(tuid = "") {
const url = `${getApiUrl()}/api/v2/timetable/train/now/byTrainUid/${tuid}`; const url = `${getApiUrl()}/api/v2/timetable/train/now/byTrainUid/${tuid}`;
const options = { const options = {
method: 'GET', method: "GET",
headers: { headers: {
uuid: $uuid uuid: $uuid,
} },
}; };
const res = await fetch(url, options); const res = await fetch(url, options);
if (res.status === 200) { if (res.status === 200) {
return await res.json(); return await res.json();
} else { } else {
throw new Error('Unable to Fetch'); throw new Error("Unable to Fetch");
} }
} }
@ -34,9 +37,9 @@
<div class="container"> <div class="container">
<div class="container-header" on:click={expand} on:keypress={expand}> <div class="container-header" on:click={expand} on:keypress={expand}>
<span class="header" <span class="header"
><StylesToc toc={service?.operator || ''} /> ><StylesToc toc={service?.operator || ""} />
{service?.stops[0]['publicDeparture'] || service?.stops[0]['wttDeparture']} {service?.stops[0]["publicDeparture"] || service?.stops[0]["wttDeparture"]}
{service?.stops[0]['tiploc']} to {service?.stops[service['stops'].length - 1]['tiploc']}</span {service?.stops[0]["tiploc"]} to {service?.stops[service["stops"].length - 1]["tiploc"]}</span
> >
<span id="container-arrow" class:isExpanded>V</span> <span id="container-arrow" class:isExpanded>V</span>
</div> </div>
@ -45,55 +48,58 @@
{#await getTrainByUID(service.trainUid)} {#await getTrainByUID(service.trainUid)}
<LoadingText /> <LoadingText />
{:then serviceDetail} {:then serviceDetail}
{#if serviceDetail.stpIndicator === 'C'} {#if serviceDetail.stpIndicator === "C"}
<p class="text-message">This has been removed from the timetable for today.<br /><br> <p class="text-message">This has been removed from the timetable for today.</p>
The service will not run, another service may be running in its place.</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} {:else}
{#if serviceDetail.vstp} <div class="detailOperator"><StylesToc toc={service?.operator || ""} full={true} /></div>
<div class="vstp">VSTP</div>
{/if} <TrainIcons serviceDetails={serviceDetail.serviceDetail} />
<div class="detailOperator"><StylesToc toc={service?.operator || ''} full={true} /></div>
{#if serviceDetail.pis} {#if serviceDetail.pis}
<PisHandler pisObject={serviceDetail.pis} /> <PisHandler pisObject={serviceDetail.pis} />
{/if} {/if}
<p class="svc-detail"> <p class="svc-detail">
Planned Type: {parseInt(serviceDetail.planSpeed) || '--'}mph {serviceDetail.powerType || 'Non-Rail vehicle'} Planned Type: {parseInt(serviceDetail.planSpeed) || "--"}mph {serviceDetail.powerType || "Non-Rail vehicle"}
</p> </p>
<p class="svc-detail"> <p class="svc-detail">
Days Run: {serviceDetail?.daysRun.join(', ').toUpperCase() || 'Unknown'} Days Run: {serviceDetail?.daysRun.join(", ").toUpperCase() || "Unknown"}
</p> </p>
<p class="svc-detail validity"> <p class="svc-detail validity">
Valid From: {new Date(serviceDetail.scheduleStart).toLocaleDateString('en-GB', { Valid From: {new Date(serviceDetail.scheduleStart).toLocaleDateString("en-GB", {
timeZone: 'UTC' timeZone: "UTC",
})} - {new Date(serviceDetail.scheduleEnd).toLocaleDateString('en-GB', { })} - {new Date(serviceDetail.scheduleEnd).toLocaleDateString("en-GB", {
timeZone: 'UTC' timeZone: "UTC",
})} })}
</p> </p>
<table> <table>
<caption>Italics are 'pass' times, grey times are non-passenger stops</caption>
<tr> <tr>
<th>Location</th> <th>Location</th>
<th>Plt.</th>
<th>Sch Arr.</th> <th>Sch Arr.</th>
<th>Sch Dep.</th> <th>Sch Dep.</th>
</tr> </tr>
{#if serviceDetail.stops[0]['publicDeparture']}
{#each serviceDetail.stops as stop} {#each serviceDetail.stops as stop}
<tr>
{#if stop.publicArrival || stop.publicDeparture} {#if stop.publicArrival || stop.publicDeparture}
<tr>
<td>{stop.tiploc}</td> <td>{stop.tiploc}</td>
<td>{stop.publicArrival || '-'}</td> <td>{stop.platform || "-"}</td>
<td>{stop.publicDeparture || '-'}</td> <td>{stop.publicArrival || "-"}</td>
</tr> <td>{stop.publicDeparture || "-"}</td>
{/if} {:else if stop.wttArrival || stop.wttDeparture}
{/each} <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} {:else}
{#each serviceDetail.stops as stop} <td class="pass">{stop.tiploc}</td>
<tr> <td class="pass">{stop.platform || stop.depLine || stop.arrLine || "-"}</td>
<td>{stop.tiploc}</td> <td class="pass">-</td>
<td>{stop.wttArrival || '-'}</td> <td class="pass">{stop.pass || "-"}</td>
<td>{stop.wttDeparture || '-'}</td> {/if}
</tr> </tr>
{/each} {/each}
{/if}
</table> </table>
{/if} {/if}
{:catch} {:catch}
@ -157,23 +163,25 @@
} }
table { table {
margin: auto; margin: auto;
padding-top: 10px; padding-top: 5px;
padding-bottom: 10px; padding-bottom: 10px;
color: var(--island-text-color); 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 { .text-message {
margin: 5px; margin: 5px;
margin-left: 20px; margin-left: 20px;
margin-right: 20px; margin-right: 20px;
padding-bottom: 10px; padding-bottom: 10px;
} }
.vstp {
background-color: red;
margin: auto;
padding: 5px;
width: 50px;
border-radius: 5px;
border-color: darkred;
font-weight: bolder;
}
</style> </style>

View File

@ -0,0 +1,39 @@
<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";
export let serviceDetails: ServiceDetail;
</script>
{#if serviceDetails.firstClass}
<Tooltip text="First Class is available">
<IconSquare1 />
</Tooltip>
{/if}
{#if serviceDetails.catering}
<Tooltip text="Catering is available">
<IconToolsKitchen2 />
</Tooltip>
{/if}
{#if serviceDetails.sleeper}
<Tooltip text="Sleeping Berths are available">
<IconBed />
</Tooltip>
{/if}
{#if serviceDetails.vstp}
<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 />
{/if}
<style>
</style>

View File

@ -1,8 +1,8 @@
<script> <script>
import { page } from '$app/stores'; import { page } from "$app/stores";
import Header from '$lib/navigation/header.svelte'; import Header from "$lib/navigation/header.svelte";
import Nav from '$lib/navigation/nav.svelte'; import Nav from "$lib/navigation/nav.svelte";
const title = 'OwlBoard - Error'; const title = "OwlBoard - Error";
</script> </script>
<Header {title} /> <Header {title} />

View File

@ -1,17 +1,27 @@
<script> <script>
import '$lib/themes.css'; import "$lib/themes.css";
import '$lib/main.css'; import "$lib/main.css";
import { dev } from '$app/environment'; import { dev } from "$app/environment";
import DevBanner from '$lib/DevBanner.svelte'; import DevBanner from "$lib/DevBanner.svelte";
import { page } from '$app/stores'; import { page } from "$app/stores";
import { Toaster } from "svelte-french-toast";
</script> </script>
<svelte:head> <svelte:head>
<!--
___ _ ___ _
/ _ \__ __ _| | _ ) ___ __ _ _ _ __| |
| (_) \ V V / | _ \/ _ \/ _` | '_/ _` |
\___/ \_/\_/|_|___/\___/\__,_|_| \__,_|
Check out the source code on Gitea (https://git.fjla.net/OwlBoard)
It's easier to read there
-->
<meta name="application-name" content="OwlBoard" /> <meta name="application-name" content="OwlBoard" />
<meta name="author" content="Frederick Boniface" /> <meta name="author" content="Frederick Boniface" />
<meta <meta
name="description" 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." 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="viewport" content="width=device-width" />
<meta name="theme-color" content="#00b7b7" /> <meta name="theme-color" content="#00b7b7" />
@ -21,7 +31,13 @@
<link rel="manifest" href="/manifest.json" /> <link rel="manifest" href="/manifest.json" />
<title>OwlBoard</title> <title>OwlBoard</title>
</svelte:head> </svelte:head>
<Toaster />
{#if dev} {#if dev}
<DevBanner /> <DevBanner />
{/if} {/if}
<slot /> <slot />
<style>
</style>

View File

@ -1,38 +1,48 @@
<script> <script lang="ts">
import Header from '$lib/navigation/header.svelte'; import Header from "$lib/navigation/header.svelte";
import Nav from '$lib/navigation/nav.svelte'; import Nav from "$lib/navigation/nav.svelte";
import InputIsland from '$lib/islands/input-island-form.svelte'; import { featureDetect } from "$lib/scripts/featureDetect";
import QuickLinkIsland from '$lib/islands/quick-link-island.svelte'; import { onMount } from "svelte";
import Welcome from '$lib/overlays/welcome.svelte'; import toast from "svelte-french-toast";
import { welcome } from '$lib/stores/welcome'; import type { LookupCardConfig } from "$lib/cards/Card.types";
import { version, showWelcome } from '$lib/stores/version'; import LookupCard from "$lib/cards/LookupCard.svelte";
import NearToMeCard from "$lib/cards/NearToMeCard.svelte";
const title = 'OwlBoard'; import QuickLinkCard from "$lib/cards/QuickLinkCard.svelte";
const inputIslands = [ const title = "OwlBoard";
const lookupCards: LookupCardConfig[] = [
{ {
title: 'Live Departure Boards', title: "Live Arr/Dep Boards",
action: '/ldb', helpText: "",
placeholder: 'Enter CRS/TIPLOC', formAction: "/ldb",
queryName: 'station' placeholder: "enter crs/tiploc",
maxLen: 7,
fieldName: "station",
}, },
{ {
title: 'Train Details & PIS', title: "Timetable & PIS",
action: '/train', helpText: "",
placeholder: 'Enter Headcode', formAction: "/train",
queryName: 'headcode' placeholder: "enter headcode",
} maxLen: 4,
fieldName: "headcode",
},
]; ];
onMount(async () => {
const featureSupport = featureDetect();
if (!featureSupport.critical) {
toast.error("Your browser is missing critical features, OwlBoard might not work properly. See `Menu > Statistics` for more information.");
} else if (!featureSupport.nice) {
toast.error("Your browser is missing some features, see `Menu > Statistics` for more information.");
}
});
</script> </script>
{#if showWelcome && ($welcome === 'null' || !$welcome || parseInt($welcome.replace(/\./g, '')) < parseInt(version.replace(/\./g, '')))}
<Welcome />
{/if}
<Header {title} /> <Header {title} />
{#each lookupCards as config}
{#each inputIslands as variables} <LookupCard {config} />
<InputIsland {variables} />
{/each} {/each}
<NearToMeCard />
<QuickLinkIsland /> <QuickLinkCard />
<Nav /> <Nav />

View File

@ -1,8 +1,8 @@
<script lang="ts"> <script lang="ts">
import Header from '$lib/navigation/header.svelte'; import Header from "$lib/navigation/header.svelte";
import Nav from '$lib/navigation/nav.svelte'; import Nav from "$lib/navigation/nav.svelte";
const title = '404 - Not Found'; const title = "404 - Not Found";
</script> </script>
<Header {title} /> <Header {title} />

View File

@ -1,8 +1,8 @@
<script lang="ts"> <script lang="ts">
import Header from '$lib/navigation/header.svelte'; import Header from "$lib/navigation/header.svelte";
import Nav from '$lib/navigation/nav.svelte'; import Nav from "$lib/navigation/nav.svelte";
const title = '50x - Server Error'; const title = "50x - Server Error";
</script> </script>
<Header {title} /> <Header {title} />

View File

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

View File

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

View File

@ -1,9 +1,9 @@
<script> <script>
import LargeLogo from '$lib/images/large-logo.svelte'; import LargeLogo from "$lib/images/large-logo.svelte";
import Header from '$lib/navigation/header.svelte'; import Header from "$lib/navigation/header.svelte";
import Nav from '$lib/navigation/nav.svelte'; import Nav from "$lib/navigation/nav.svelte";
const title = 'About'; const title = "About";
</script> </script>
<Header {title} /> <Header {title} />

View File

@ -1,22 +1,22 @@
<script> <script>
import Header from '$lib/navigation/header.svelte'; import Header from "$lib/navigation/header.svelte";
import Loading from '$lib/navigation/loading.svelte'; import Loading from "$lib/navigation/loading.svelte";
import Nav from '$lib/navigation/nav.svelte'; import Nav from "$lib/navigation/nav.svelte";
import { getApiUrl } from '$lib/scripts/upstream'; import { getApiUrl } from "$lib/scripts/upstream";
const title = 'Location Codes'; const title = "Location Codes";
let val = { let val = {
crs: '', crs: "",
tiploc: '', tiploc: "",
stanox: '', stanox: "",
nlc: '', nlc: "",
name: '', name: "",
uic: '' uic: "",
}; };
let isLoading = false; let isLoading = false;
async function getData(type = '', value = '') { async function getData(type = "", value = "") {
const url = `${getApiUrl()}/api/v2/ref/locationCode/${type}/${value}`; const url = `${getApiUrl()}/api/v2/ref/locationCode/${type}/${value}`;
const res = await fetch(url); const res = await fetch(url);
const data = await res.json(); const data = await res.json();
@ -26,16 +26,16 @@
async function processData(data) { async function processData(data) {
//console.log("data",JSON.stringify(data)) //console.log("data",JSON.stringify(data))
if (data.ERROR == 'Offline') { if (data.ERROR == "Offline") {
return; return;
} }
val = { val = {
crs: data[0]['3ALPHA'] || 'None', crs: data[0]["3ALPHA"] || "None",
tiploc: data[0]['TIPLOC'] || 'None', tiploc: data[0]["TIPLOC"] || "None",
stanox: data[0]['STANOX'] || 'None', stanox: data[0]["STANOX"] || "None",
nlc: data[0]['NLC'] || 'None', nlc: data[0]["NLC"] || "None",
name: data[0]['NLCDESC'] || 'None', name: data[0]["NLCDESC"] || "None",
uic: data[0]['UIC'] || 'None' uic: data[0]["UIC"] || "None",
}; };
//console.log("val",JSON.stringify(val)); //console.log("val",JSON.stringify(val));
} }
@ -44,13 +44,13 @@
isLoading = true; isLoading = true;
let data = []; let data = [];
if (val?.crs) { if (val?.crs) {
data = await getData('crs', val.crs); data = await getData("crs", val.crs);
} else if (val?.tiploc) { } else if (val?.tiploc) {
data = await getData('tiploc', val.tiploc); data = await getData("tiploc", val.tiploc);
} else if (val?.stanox) { } else if (val?.stanox) {
data = await getData('stanox', val.stanox); data = await getData("stanox", val.stanox);
} else if (val?.nlc) { } else if (val?.nlc) {
data = await getData('nlc', val.nlc); data = await getData("nlc", val.nlc);
} else { } else {
return; return;
} }
@ -59,12 +59,12 @@
async function reset() { async function reset() {
val = { val = {
crs: '', crs: "",
tiploc: '', tiploc: "",
stanox: '', stanox: "",
nlc: '', nlc: "",
name: '', name: "",
uic: '' uic: "",
}; };
} }
</script> </script>

View File

@ -1,24 +1,24 @@
<script> <script>
import LogoutButton from '$lib/navigation/LogoutButton.svelte'; import LogoutButton from "$lib/navigation/LogoutButton.svelte";
import Header from '$lib/navigation/header.svelte'; import Header from "$lib/navigation/header.svelte";
import Loading from '$lib/navigation/loading.svelte'; import Loading from "$lib/navigation/loading.svelte";
import Nav from '$lib/navigation/nav.svelte'; import Nav from "$lib/navigation/nav.svelte";
import { getApiUrl } from '$lib/scripts/upstream'; import { getApiUrl } from "$lib/scripts/upstream";
import { uuid } from '$lib/stores/uuid.js'; import { uuid } from "$lib/stores/uuid.js";
import { onMount } from 'svelte'; import { onMount } from "svelte";
const title = 'Your Data'; const title = "Your Data";
let data = [ let data = [
{ {
domain: 'User not Found', domain: "User not Found",
atime: 'User not Found' atime: "User not Found",
} },
]; ];
let isLoading = false; let isLoading = false;
async function fetchData() { async function fetchData() {
if ($uuid != 'null') { if ($uuid != "null") {
const url = `${getApiUrl()}/api/v2/user/${$uuid}`; const url = `${getApiUrl()}/api/v2/user/${$uuid}`;
const res = await fetch(url); const res = await fetch(url);
const json = await res.json(); const json = await res.json();
@ -44,13 +44,13 @@
{#if isLoading} {#if isLoading}
<Loading /> <Loading />
{:else if data[0].domain != 'User not Found'} {:else if data[0].domain != "User not Found"}
<p class="api_response">Registration Domain: {data[0]['domain']}</p> <p class="api_response">Registration Domain: {data[0]["domain"]}</p>
<p class="api_response">Access Time: {data[0]['atime']}</p> <p class="api_response">Access Time: {data[0]["atime"]}</p>
<LogoutButton /> <LogoutButton />
<p> <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 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
days. 90 days.
</p> </p>
{:else} {: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>

View File

@ -1,15 +1,17 @@
<script> <script>
import Header from '$lib/navigation/header.svelte'; import Header from "$lib/navigation/header.svelte";
import Nav from '$lib/navigation/nav.svelte'; import Nav from "$lib/navigation/nav.svelte";
</script> </script>
<Header title={'Help'} /> <Header title={"Help"} />
<Nav /> <Nav />
<br /><br /> <br /><br />
<p> <p>
If you need help you can use the <a href="https://www.facebook.com/owlboard.support">OwlBoard Support</a> page on Facebook. 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.
</p> </p>
<br /><br /> <br /><br />
<p>There, you can watch help videos, ask a question or suggest a feature.</p>
<p>Alternatively, you can report an issue on the <a href="/more/report">Report an Issue</a> page.</p> <p>Alternatively, you can report an issue on the <a href="/more/report">Report an Issue</a> page.</p>

View File

@ -1,25 +1,25 @@
<script> <script>
import Header from '$lib/navigation/header.svelte'; import Header from "$lib/navigation/header.svelte";
import Nav from '$lib/navigation/nav.svelte'; import Nav from "$lib/navigation/nav.svelte";
const title = 'Privacy Policy'; const title = "Privacy Policy";
</script> </script>
<Header {title} /> <Header {title} />
<div> <div>
<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 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
that we store, please visit <a href="/more/data">My Data</a>. data that we store, please visit <a href="/more/data">My Data</a>.
</p> </p>
<p>OwlBoard does not utilize any cookies. IP addresses and browser fingerprints are not logged.</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> <h2>If You Do Not Sign Up</h2>
<p> <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 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
your device. leave your device.
</p> </p>
<h2>If You Sign Up</h2> <h2>If You Sign Up</h2>
<p> <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 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
settings are stored locally in your browser and do not leave your device. personal settings are stored locally in your browser and do not leave your device.
</p> </p>
<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 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
@ -28,6 +28,10 @@
</p> </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>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> <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>
<p>
If you enable location data, your location will be sent to the server when you navigate to the homepage to determine your closest stations. This data is never stored on the
server after the nearest stations have been send to your device.
</p>
<h2>Reporting an Issue</h2> <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>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> <p>

View File

@ -1,18 +1,32 @@
<script> <script lang="ts">
import { onMount } from 'svelte'; import { onMount } from "svelte";
import Header from '$lib/navigation/header.svelte'; import Header from "$lib/navigation/header.svelte";
import Nav from '$lib/navigation/nav.svelte'; import Nav from "$lib/navigation/nav.svelte";
import Loading from '$lib/navigation/loading.svelte'; import Loading from "$lib/navigation/loading.svelte";
import ResultIsland from '$lib/islands/result-island.svelte'; import Card from "$lib/cards/Card.svelte";
import { getApiUrl } from '$lib/scripts/upstream'; import { CardConfig } from "$lib/cards/Card.types";
import { apiGet } from "$lib/scripts/apiFetch";
const title = 'Reason Codes'; interface ApiResponse {
results: boolean;
title: string;
resultLines: string[];
}
const title = "Reason Codes";
let isLoading = false; let isLoading = false;
let inputValue = ''; let inputValue = "";
let resultObject = { let resultObject: ApiResponse = {
results: false, results: false,
title: '', title: "",
resultLines: [] resultLines: [],
};
let config: CardConfig = {
title: "",
showHelp: false,
showRefresh: false,
helpText: "",
onRefresh: () => {},
refreshing: false,
}; };
function load() { function load() {
@ -26,9 +40,9 @@
async function getData() { async function getData() {
if (inputValue) { if (inputValue) {
const url = `${getApiUrl()}/api/v2/ref/reasonCode/${inputValue}`; const apiPath = `/api/v2/ref/reasonCode/${inputValue}`;
const res = await fetch(url); const res = await apiGet(apiPath);
return await res.json(); return res;
} else { } else {
return []; return [];
} }
@ -37,16 +51,17 @@
async function handleData(data) { async function handleData(data) {
let resultLines = []; let resultLines = [];
if (data.length) { if (data.length) {
resultObject.title = data[0]['code']; resultObject.title = data[0]["code"];
resultLines = [data[0]['lateReason'], data[0]['cancReason']]; resultLines = [data[0]["lateReason"], data[0]["cancReason"]];
} else { } else {
resultObject = { resultObject = {
results: false, results: false,
title: 'Not Found', title: "Not Found",
resultLines: [] resultLines: [],
}; };
} }
resultObject.resultLines = resultLines; resultObject.resultLines = resultLines;
config.title = resultObject.title;
resultObject.results = true; resultObject.results = true;
} }
@ -79,7 +94,11 @@
{/if} {/if}
{#if resultObject.results} {#if resultObject.results}
<ResultIsland {resultObject} /> <Card {config}>
{#each resultObject.resultLines as line}
<p>{line}</p>
{/each}
</Card>
{/if} {/if}
<Nav /> <Nav />
@ -98,7 +117,7 @@
border-radius: 50px; border-radius: 50px;
border: none; border: none;
text-align: center; text-align: center;
font-family: urwgothic, 'Lucida Sans', 'Lucida Sans Regular', 'Lucida Grande', 'Lucida Sans Unicode', Geneva, Verdana, sans-serif; font-family: urwgothic, "Lucida Sans", "Lucida Sans Regular", "Lucida Grande", "Lucida Sans Unicode", Geneva, Verdana, sans-serif;
text-transform: uppercase; text-transform: uppercase;
font-size: 15px; font-size: 15px;
box-shadow: var(--box-shadow); box-shadow: var(--box-shadow);
@ -111,7 +130,7 @@
border: none; border: none;
border-radius: 20px; border-radius: 20px;
padding: 5px; padding: 5px;
font-family: urwgothic, 'Lucida Sans', 'Lucida Sans Regular', 'Lucida Grande', 'Lucida Sans Unicode', Geneva, Verdana, sans-serif; font-family: urwgothic, "Lucida Sans", "Lucida Sans Regular", "Lucida Grande", "Lucida Sans Unicode", Geneva, Verdana, sans-serif;
font-size: 16px; font-size: 16px;
font-weight: 400; font-weight: 400;
background-color: var(--island-bg-color); background-color: var(--island-bg-color);

View File

@ -1,51 +1,60 @@
<script> <script lang="ts">
import Header from '$lib/navigation/header.svelte'; import Header from "$lib/navigation/header.svelte";
import Nav from '$lib/navigation/nav.svelte'; import Nav from "$lib/navigation/nav.svelte";
import Loading from '$lib/navigation/loading.svelte'; import Loading from "$lib/navigation/loading.svelte";
import { onMount } from 'svelte'; import { onMount } from "svelte";
import { checkAuth, logout } from '$lib/libauth'; import { checkAuth } from "$lib/libauth";
import LogoutButton from '$lib/navigation/LogoutButton.svelte'; import LogoutButton from "$lib/navigation/LogoutButton.svelte";
import { getApiUrl } from '$lib/scripts/upstream'; import { getApiUrl } from "$lib/scripts/upstream";
import toast from "svelte-french-toast";
const title = 'Register'; const title = "Register";
let state = 'unreg'; let state = "unreg";
let isLoading = true; let isLoading = true;
let inputValue = ''; let inputValue = "";
function handleInput(event) { function handleInput(event: KeyboardEvent) {
inputValue = event.target.value; inputValue = event?.target?.value;
} }
async function request() { async function request() {
isLoading = true; isLoading = true;
const url = `${getApiUrl()}/api/v2/user/request`; const url = `${getApiUrl()}/api/v2/user/request`;
const request = { const request = {
method: 'POST', method: "POST",
headers: { headers: {
'Content-Type': 'application/json' "Content-Type": "application/json",
}, },
body: JSON.stringify({ body: JSON.stringify({
email: inputValue email: inputValue,
}) }),
}; };
const res = await fetch(url, request); const res = await fetch(url, request);
if (res.status == 400 || res.status == 403) { if (res.status == 400 || res.status == 403) {
state = 'unauth'; state = "unauth";
} else if (res.status == 201) { } else if (res.status == 201) {
state = 'sent'; state = "sent";
} else { } else {
state = 'error'; state = "error";
} }
isLoading = false; isLoading = false;
} }
function send() {
toast.promise(request(), {
loading: "Contacting Server...",
success: "Request Answered.",
error: "Unable to contact server.",
});
}
onMount(async () => { onMount(async () => {
const auth = await checkAuth(); const auth = await checkAuth();
if (auth.uuidPresent === false) { if (auth.uuidPresent === false) {
state = 'unreg'; state = "unreg";
} else if (auth.uuidPresent === true) { } else if (auth.uuidPresent === true) {
state = 'reg'; state = "reg";
} }
isLoading = false; isLoading = false;
}); });
@ -56,20 +65,10 @@
<section class="content"> <section class="content">
{#if isLoading} {#if isLoading}
<Loading /> <Loading />
{:else if state == 'unreg'} {:else if state == "unreg"}
<p>The staff version of OwlBoard offers several extra features:</p>
<ul>
<li>Access the Train Finder</li>
<li>Access the PIS Finder</li>
<li>More detailed departure boards:</li>
<ul>
<li>Non-Passenger movements</li>
<li>Hidden platform numbers</li>
<li>Display up to 25 services</li>
</ul>
</ul>
<p>To register, you will need to enter a work email address to receive a confirmation email</p> <p>To register, you will need to enter a work email address to receive a confirmation email</p>
<form on:submit={request}> <p class="bold">Already have a registration code? <a href="/more/reg/submit">enter it here</a></p>
<form on:submit={send}>
<input type="text" autocomplete="email" placeholder="Enter work email" bind:value={inputValue} on:input={handleInput} /><br /> <input type="text" autocomplete="email" placeholder="Enter work email" bind:value={inputValue} on:input={handleInput} /><br />
<label for="checkbox"> <label for="checkbox">
I have read and accept the terms of the <a href="/more/privacy">Privacy Policy</a><br /> I have read and accept the terms of the <a href="/more/privacy">Privacy Policy</a><br />
@ -77,21 +76,29 @@
</label><br /> </label><br />
<button type="submit">Submit</button> <button type="submit">Submit</button>
</form> </form>
{:else if state == 'sent'} <br />
<p>An email has been sent, click the link in the email to activate your profile.</p> <p class="bold">What do you get?</p>
<p>When you click the link, your authorisation key will be automatically be stored in your browser.</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>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> <p>You will be able to register again using the same email address</p>
{:else if state == 'unauth'} {:else if state == "unauth"}
<p>The email address you entered does not belong to an authorised business.</p> <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> <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'} {:else if state == "error"}
<p>There was an error processing your request.</p> <p>There was an error processing your request.</p>
<p>Check that the email you entered was correct or try again later.</p> <p>Check that the email you entered was correct or try again later.</p>
{:else if state == 'reg'} {:else if state == "reg"}
<p> <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 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
browser. your browser.
</p> </p>
<LogoutButton /> <LogoutButton />
{/if} {/if}
@ -99,21 +106,31 @@
<Nav /> <Nav />
<style> <style>
.bold {
font-weight: 800;
}
.content { .content {
margin-top: 30px; margin-top: 30px;
} }
p { p {
margin: 10px; margin: 10px;
margin-left: 20px;
margin-right: 20px;
} }
ul { li {
text-align: left; text-align: center;
font-size: 14px;
color: white; color: white;
margin-top: 5px;
margin-left: 15px;
margin-right: 15px;
list-style-type: none;
} }
input { input {
height: 40px; height: 40px;
width: 80%; width: 80%;
max-width: 375px; max-width: 375px;
font-family: urwgothic, 'Franklin Gothic Medium', 'Arial Narrow', Arial, sans-serif; font-family: urwgothic, "Franklin Gothic Medium", "Arial Narrow", Arial, sans-serif;
text-align: center; text-align: center;
font-size: 20px; font-size: 20px;
border-radius: 50px; border-radius: 50px;

View File

@ -1,80 +1,128 @@
<script> <script lang="ts">
import Header from '$lib/navigation/header.svelte'; import Header from "$lib/navigation/header.svelte";
import Loading from '$lib/navigation/loading.svelte'; import Nav from "$lib/navigation/nav.svelte";
import Nav from '$lib/navigation/nav.svelte'; import { getApiUrl } from "$lib/scripts/upstream";
import { getApiUrl } from '$lib/scripts/upstream'; import { uuid } from "$lib/stores/uuid";
import { uuid } from '$lib/stores/uuid.js';
import { onMount } from 'svelte';
const title = 'Registration'; const title = "Submit Registration";
let state = ''; let state = false;
let isLoading = true; let status: string;
async function getUUID() { let inputs: { id: string; value: string }[] = [
return new URLSearchParams(window.location.search).get('key'); { 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();
}
}
} }
async function submit(id) { 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;
}
async function submit(id: string): Promise<number> {
const url = `${getApiUrl()}/api/v2/user/register`; const url = `${getApiUrl()}/api/v2/user/register`;
const request = { const request = {
method: 'POST', method: "POST",
headers: { headers: {
'Content-Type': 'application/json' "Content-Type": "application/json",
}, },
body: JSON.stringify({ body: JSON.stringify({
uuid: id uuid: id,
}) }),
}; };
const res = await fetch(url, request); const res = await fetch(url, request);
const body = await res.json(); const body = await res.json();
if (body.api_key) { if (body.api_key) {
uuid.set(body.api_key); uuid.set(body.api_key);
return 201; return 201;
} } else {
return res.status; return res.status;
} }
onMount(async () => {
const id = (await getUUID()) || '';
if (id == '' || !id) {
state = 'none';
isLoading = false;
return;
} }
const status = await submit(id);
if (status == 201) {
console.log('Registered Successfully');
state = 'done';
} else if (status == 401) {
console.log('Invalid Key');
state = 'unauth';
} else {
console.log('Error');
state = 'error';
}
isLoading = false;
});
</script> </script>
<Header {title} /> <Header {title} />
{#if isLoading}
<Loading /> {#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}
{#if state == 'none'} {:else}
<p>Unable to read your access key.</p> <p class="title-ish">Enter your registration code below</p>
<p>Please click the link in your email again.</p> <form on:submit={handleSubmit} id="codeInputForm">
{:else if state == 'unauth'} {#each inputs as input, index}
<p>Your link is not valid, links expire after 30 minutes.</p> <input class="code-in" bind:value={input.value} id={`input-${input.id}`} maxlength="1" autocomplete="off" on:keydown={(event) => handleInput(index, event)} />
<p>You can try to register again.</p> {/each}
{:else if state == 'error'} <br />
<p>There was an error on our end, please try again later</p> <button type="submit">Submit</button>
{:else if state == 'done'} </form>
<p>You are now logged in</p>
{/if} {/if}
<Nav /> <Nav />
<style> <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 { p {
margin: 14px; margin-left: 40px;
margin-right: 40px;
} }
</style> </style>

View File

@ -1,20 +1,20 @@
<script> <script>
import Header from '$lib/navigation/header.svelte'; import Header from "$lib/navigation/header.svelte";
import Island from '$lib/islands/island.svelte'; import Island from "$lib/islands/island.svelte";
import Nav from '$lib/navigation/nav.svelte'; import Nav from "$lib/navigation/nav.svelte";
import { onMount } from 'svelte'; import { onMount } from "svelte";
import Loading from '$lib/navigation/loading.svelte'; import Loading from "$lib/navigation/loading.svelte";
import Done from '$lib/navigation/done.svelte'; import Done from "$lib/navigation/done.svelte";
import { getApiUrl } from '$lib/scripts/upstream'; import { getApiUrl } from "$lib/scripts/upstream";
const title = 'Report Issue'; const title = "Report Issue";
let isLoading = false; let isLoading = false;
let isDone = false; let isDone = false;
let isError = false; let isError = false;
let reportType = '', let reportType = "",
reportSubject = '', reportSubject = "",
reportMsg = '', reportMsg = "",
reportCollected; reportCollected;
onMount(async () => { onMount(async () => {
reportCollected = { reportCollected = {
@ -22,7 +22,7 @@
browser: navigator.appName, browser: navigator.appName,
version: navigator.appVersion, version: navigator.appVersion,
platform: navigator.platform, platform: navigator.platform,
viewport: `${window.innerWidth} x ${window.innerHeight}` viewport: `${window.innerWidth} x ${window.innerHeight}`,
}; };
}); });
@ -34,7 +34,7 @@
} }
async function send() { async function send() {
console.log('SEND DATA REQUESTED'); console.log("SEND DATA REQUESTED");
isLoading = true; isLoading = true;
const formData = JSON.stringify({ const formData = JSON.stringify({
label: reportType, label: reportType,
@ -46,22 +46,22 @@
`Platform: ${reportCollected.platform}\n` + `Platform: ${reportCollected.platform}\n` +
`Viewport: ${reportCollected.viewport}\n\n\n` + `Viewport: ${reportCollected.viewport}\n\n\n` +
`User Message:\n` + `User Message:\n` +
`${reportMsg}` `${reportMsg}`,
}); });
const url = `${getApiUrl()}/misc/issue`; const url = `${getApiUrl()}/misc/issue`;
const options = { const options = {
method: 'POST', method: "POST",
headers: { headers: {
'Content-Type': 'application/json' "Content-Type": "application/json",
}, },
body: formData body: formData,
}; };
const res = await fetch(url, options); const res = await fetch(url, options);
if (res.status == 200) { if (res.status == 200) {
isLoading = false; isLoading = false;
isDone = true; isDone = true;
await new Promise((r) => setTimeout(r, 2000)); await new Promise((r) => setTimeout(r, 2000));
window.location.href = '/'; window.location.href = "/";
} else { } else {
isLoading = false; isLoading = false;
isError = true; isError = true;

View File

@ -1,12 +1,50 @@
<script> <script>
import Header from '$lib/navigation/header.svelte'; import Header from "$lib/navigation/header.svelte";
import Nav from '$lib/navigation/nav.svelte'; import Nav from "$lib/navigation/nav.svelte";
import QlSet from '$lib/islands/quick-link-set-island.svelte'; import QlSet from "$lib/islands/quick-link-set-island.svelte";
const title = 'Settings'; import Island from "$lib/islands/island.svelte";
import { location } from "$lib/stores/location";
import { getCurrentLocation } from "$lib/scripts/getLocation";
import toast from "svelte-french-toast";
const title = "Settings";
$: if ($location) {
getCurrentLocation();
}
function locationToast() {
toast("Settings updated");
}
</script> </script>
<Header {title} /> <Header {title} />
<QlSet /> <QlSet />
<Island variables={{ title: "Location" }}>
<p>Use your location to quickly check departure boards near you</p>
<div class="checkbox-container">
<label for="location_enable">Enabled</label>
<input id="location_enable" type="checkbox" bind:checked={$location} on:click={locationToast} />
</div>
</Island>
<Nav /> <Nav />
<style>
.checkbox-container {
display: inline-flex;
align-items: center;
margin: auto;
}
.checkbox-container input[type="checkbox"] {
margin: 0;
height: 25px;
width: 25px;
}
.checkbox-container label {
margin-left: 0;
margin-right: 25px;
font-weight: 800;
}
</style>

View File

@ -1,10 +1,15 @@
<script> <script lang="ts">
import Island from '$lib/islands/island.svelte'; import Island from "$lib/islands/island.svelte";
import Header from '$lib/navigation/header.svelte'; import Header from "$lib/navigation/header.svelte";
import Loading from '$lib/navigation/loading.svelte'; import Loading from "$lib/navigation/loading.svelte";
import Nav from '$lib/navigation/nav.svelte'; import Nav from "$lib/navigation/nav.svelte";
import { getApiUrl } from '$lib/scripts/upstream'; import { getApiUrl } from "$lib/scripts/upstream";
const title = 'Statistics'; import { featureDetect, type FeatureDetectionResult } from "$lib/scripts/featureDetect";
import { getCurrentLocation } from "$lib/scripts/getLocation";
import { onMount } from "svelte";
const title = "Statistics";
let features: FeatureDetectionResult | null = null;
let error: Error | null = null;
async function getData() { async function getData() {
const url = `${getApiUrl()}/misc/server/stats`; const url = `${getApiUrl()}/misc/server/stats`;
@ -12,13 +17,28 @@
return await res.json(); return await res.json();
} }
function U2L(input) { async function loadFeatures() {
try {
features = await featureDetect();
} catch (e) {
error = e;
}
}
onMount(() => {
loadFeatures();
});
function U2L(input: Date | number): string {
if (input instanceof Date) {
return input.toLocaleString();
}
try { try {
const datetime = new Date(input * 1000); const datetime = new Date(input * 1000);
return datetime.toLocaleString(); return datetime.toLocaleString();
} catch (err) { } catch (err) {
console.log(err); console.log(err);
return false; return "Unknown";
} }
} }
</script> </script>
@ -31,7 +51,7 @@
<br /> <br />
<p>API Server:<br /><span>{data?.hostname}</span></p> <p>API Server:<br /><span>{data?.hostname}</span></p>
<p>Runtime Mode: <span>{data?.runtimeMode}</span></p> <p>Runtime Mode: <span>{data?.runtimeMode}</span></p>
<p>Stats Reset: <span>{U2L(data?.reset) || 'Unknown'}</span></p> <p>Stats Reset: <span>{U2L(data?.reset) || "Unknown"}</span></p>
<h2>Last Update</h2> <h2>Last Update</h2>
<p>Timetable: <span>{U2L(data?.updateTimes?.timetable)}</span></p> <p>Timetable: <span>{U2L(data?.updateTimes?.timetable)}</span></p>
<p>Location Ref: <span>{U2L(data?.updateTimes?.corpus)}</span></p> <p>Location Ref: <span>{U2L(data?.updateTimes?.corpus)}</span></p>
@ -61,6 +81,73 @@
</Island> </Island>
{/await} {/await}
<h2>Browser Features</h2>
{#if features === null && error === null}
Checking browser features...
{:else if features !== null}
<h3>Critical Features</h3>
{#if !features.critical}
<p>
OwlBoard will not function properly without these browser features. If you see any crosses here you may need to update your browser or choose another browser. Chrome,
Edge, Firefox, Brave & Samsung Browser have been tested.
</p>
{/if}
<ul class="feature-list">
<li>
Fetch <span class="feature-status {features.missing.includes('fetch') ? 'cross' : 'tick'}">
{features.missing.includes("fetch") ? "✗" : "✓"}
</span>
</li>
<li>
Local Storage <span class="feature-status {features.missing.includes('localStorage') ? 'cross' : 'tick'}">
{features.missing.includes("localStorage") ? "✗" : "✓"}
</span>
</li>
<li>
Session Storage <span class="feature-status {features.missing.includes('sessionStorage') ? 'cross' : 'tick'}">
{features.missing.includes("sessionStorage") ? "✗" : "✓"}
</span>
</li>
<li>
Promises <span class="feature-status {features.missing.includes('promise') ? 'cross' : 'tick'}">
{features.missing.includes("promise") ? "✗" : "✓"}
</span>
</li>
</ul>
<h3>Nice-to-have Features</h3>
{#if !features.nice}
<p>
OwlBoard may run slowly or be missing some functions without these browser features. If you see any crosses here you may want to update your browser or choose another
browser for improved performance. Chrome, Edge, Firefox, Brave & Samsung Browser have been tested.
</p>
{/if}
<ul class="feature-list">
<li>
Geolocation <span class="feature-status {features.missing.includes('geolocation') ? 'cross' : 'tick'}">
{features.missing.includes("geolocation") ? "✗" : "✓"}
</span>
</li>
<li>
Service Worker <span class="feature-status {features.missing.includes('serviceWorker') ? 'cross' : 'tick'}">
{features.missing.includes("serviceWorker") ? "✗" : "✓"}
</span>
</li>
<li>
Dialog <span class="feature-status {features.missing.includes('dialog') ? 'cross' : 'tick'}">
{features.missing.includes("dialog") ? "✗" : "✓"}
</span>
</li>
<li>
Popover API <span class="feature-status {features.missing.includes('popover') ? 'cross' : 'tick'}">
{features.missing.includes("popover") ? "✗" : "✓"}
</span>
</li>
</ul>
{:else if error !== null}
Failed to detect browser features: {error.message}
{/if}
<Nav /> <Nav />
<style> <style>
@ -76,4 +163,35 @@
margin-bottom: 2px; margin-bottom: 2px;
margin-top: 8px; margin-top: 8px;
} }
.feature-list {
list-style-type: none; /* Remove bullet points */
padding: 0;
text-align: center; /* Center the list */
}
.feature-list li {
display: flex;
justify-content: center;
align-items: center;
margin: 8px 0;
}
.feature-status {
display: inline-block;
width: 20px;
height: 20px;
line-height: 20px;
text-align: center;
border-radius: 50%;
color: white;
margin-left: 8px;
}
.tick {
background-color: green;
}
.cross {
background-color: red;
}
</style> </style>

View File

@ -1,12 +1,14 @@
<script> <script>
import Header from '$lib/navigation/header.svelte'; import Header from "$lib/navigation/header.svelte";
import Island from '$lib/islands/island.svelte'; import Island from "$lib/islands/island.svelte";
import Loading from '$lib/navigation/loading.svelte'; import Loading from "$lib/navigation/loading.svelte";
import Nav from '$lib/navigation/nav.svelte'; import Nav from "$lib/navigation/nav.svelte";
import LargeLogo from '$lib/images/large-logo.svelte'; import LargeLogo from "$lib/images/large-logo.svelte";
import { version, versionTag } from '$lib/stores/version'; import { version, versionTag } from "$lib/stores/version";
import { getApiUrl } from '$lib/scripts/upstream'; import { getApiUrl } from "$lib/scripts/upstream";
const title = 'Versions'; import { IconBrandGolang, IconBrandJavascript, IconBrandNodejs, IconBrandPython, IconBrandSvelte, IconBrandTypescript } from "@tabler/icons-svelte";
import Tooltip from "$lib/Tooltip.svelte";
const title = "Versions";
async function getData() { async function getData() {
const url = `${getApiUrl()}/misc/server/versions`; const url = `${getApiUrl()}/misc/server/versions`;
@ -24,6 +26,10 @@
{:then data} {:then data}
<Island> <Island>
<p> <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" <a class="data" href="https://git.fjla.uk/owlboard/owlboard-svelte" target="_blank"
>Web-app version<br /><span class="data" >Web-app version<br /><span class="data"
>{version}{#if versionTag}-{versionTag}{/if}</span >{version}{#if versionTag}-{versionTag}{/if}</span
@ -31,21 +37,32 @@
> >
</p> </p>
<p> <p>
<a class="data" href="https://git.fjla.uk/owlboard/backend" target="_blank">API Server version<br /><span class="data">{data?.backend || 'Unknown'}</span></a> <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>
<p> <p>
<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> <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>
<p> <p>
<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> <Tooltip text="Go"><IconBrandGolang /></Tooltip>
<br />
<a class="data" href="https://git.fjla.uk/owlboard/mq-client" target="_blank">timetable-mgr<br /><span class="data">{data?.["mq-client"] || "Not installed"}</span></a>
</p> </p>
</Island> </Island>
{:catch} {:catch}
<Island> <Island>
<p> <p>
Web-app Version<br /><span class="data">{version}-{versionTag}</span> <IconBrandSvelte /><IconBrandJavascript /><IconBrandTypescript /><br />
Web-app Version<br /><span class="data"
>{version}{#if versionTag}-{versionTag}{/if}</span
>
</p> </p>
<p>Unable to fetch server versions</p> <p>Unable to fetch server application versions</p>
</Island> </Island>
{/await} {/await}
<Nav /> <Nav />
@ -53,6 +70,7 @@
<style> <style>
p { p {
text-decoration: none; text-decoration: none;
padding: 15px;
} }
.data { .data {
color: white; color: white;

View File

@ -1,20 +1,22 @@
<script> <script lang="ts">
import Header from '$lib/navigation/header.svelte'; import Header from "$lib/navigation/header.svelte";
import Nav from '$lib/navigation/nav.svelte'; import Nav from "$lib/navigation/nav.svelte";
import Island from '$lib/islands/island.svelte'; import Island from "$lib/islands/island.svelte";
import Loading from '$lib/navigation/loading.svelte'; import Loading from "$lib/navigation/loading.svelte";
import { uuid } from '$lib/stores/uuid'; import { uuid } from "$lib/stores/uuid";
import StylesToc from '$lib/train/styles-toc.svelte'; import StylesToc from "$lib/train/styles-toc.svelte";
import { getApiUrl } from '$lib/scripts/upstream'; import { getApiUrl } from "$lib/scripts/upstream";
import toast from "svelte-french-toast";
import { onMount } from "svelte";
const title = 'PIS Finder'; const title = "PIS Finder";
const variables = { title: 'Results' }; const variables = { title: "Results" };
let entryPIS = ''; let entryPIS = "";
let entryStartCRS = ''; let entryStartCRS = "";
let entryEndCRS = ''; let entryEndCRS = "";
let data = []; let data = [];
let error = false; let error = false;
let errMsg = 'Unknown Error'; let errMsg = "Unknown Error";
let isLoading = false; let isLoading = false;
async function findByStartEnd() { async function findByStartEnd() {
@ -24,46 +26,65 @@
isLoading = false; isLoading = false;
} }
/* Temporarily Disabled
async function findByPis() { async function findByPis() {
isLoading = true; isLoading = true;
const url = `${getApiUrl()}/api/v2/pis/byCode/${entryPIS}`; const url = `${getApiUrl()}/api/v2/pis/byCode/${entryPIS}`;
await fetchData(url); await fetchData(url);
isLoading = false; isLoading = false;
} }
*/
async function fetchData(url) { async function fetchData(url: string) {
const options = { const options = {
method: 'GET', method: "GET",
headers: { headers: {
uuid: $uuid uuid: $uuid,
} },
}; };
const res = await fetch(url, options); // Enable Auth const res = await fetch(url, options); // Enable Auth
if (res.status == 401) { if (res.status == 401) {
errMsg = 'You must be logged in to the staff version'; errMsg = "You must be logged in to the staff version";
toast.error("You must be registered");
error = true; error = true;
return false; return false;
} else if (res.status == 500) { } else if (res.status == 500) {
errMsg = 'Server Error, try again later'; errMsg = "Server Error, try again later";
toast.error("Server Error.", { duration: 7500 });
error = true; error = true;
return false; return false;
} }
const jsonData = await res.json(); const jsonData = await res.json();
if (jsonData.ERROR == 'offline') { if (jsonData.ERROR == "offline") {
errMsg = 'Connection error, check your internet connection and try again'; errMsg = "Connection error, check your internet connection and try again";
toast.error("You are offline.");
error = true; error = true;
return false; return false;
} }
data = jsonData; data = jsonData;
let count = data.length;
if (!count) {
toast.error("No PIS Codes found.");
} else {
toast.success(`${count} PIS Codes found`);
}
} }
async function reset() { async function reset() {
data = []; data = [];
error = false; error = false;
entryPIS = ''; entryPIS = "";
entryStartCRS = ''; entryStartCRS = "";
entryEndCRS = ''; entryEndCRS = "";
toast.success("Form reset");
isLoading = false;
} }
onMount(() => {
toast("Registration soon required for PIS features.\n\nClick 'Register' in the menu.", {
duration: 3000,
});
});
</script> </script>
<Header {title} /> <Header {title} />
@ -86,16 +107,16 @@
</tr> </tr>
{#each data as item} {#each data as item}
<tr> <tr>
<td class="toc toc-data"><StylesToc toc={item.toc || '-'} /></td> <td class="toc toc-data"><StylesToc toc={item.toc || "-"} /></td>
<td class="code">{item.code}</td> <td class="code">{item.code}</td>
<td class="stops stops-data">{item.stops.join(' ')}</td> <td class="stops stops-data">{item.stops.join(" ")}</td>
</tr> </tr>
{/each} {/each}
</table> </table>
</Island> </Island>
{:else} {:else}
<p>To search by headcode use the Train Finder on the homepage</p> <p>To search by headcode use the Train Finder on the homepage</p>
<p>This feature is only supported for GWR West & Sleeper services</p> <p>This feature now supports all GWR Services</p>
<p class="label">Find By Start/End CRS:</p> <p class="label">Find By Start/End CRS:</p>
<form on:submit={findByStartEnd}> <form on:submit={findByStartEnd}>
<input type="text" maxlength="3" autocomplete="off" placeholder="Start" bind:value={entryStartCRS} /> <input type="text" maxlength="3" autocomplete="off" placeholder="Start" bind:value={entryStartCRS} />
@ -103,13 +124,14 @@
<br /> <br />
<button type="submit">Search</button> <button type="submit">Search</button>
</form> </form>
<!-- FIND BY PIS CODE NOT WORKING AT PRESENT
<p class="label">Find By PIS Code:</p> <p class="label">Find By PIS Code:</p>
<form on:submit={findByPis}> <form on:submit={findByPis}>
<input type="number" max="9999" autocomplete="off" placeholder="PIS" bind:value={entryPIS} /> <input type="number" max="9999" autocomplete="off" placeholder="PIS" bind:value={entryPIS} />
<br /> <br />
<button type="submit">Search</button> <button type="submit">Search</button>
</form> </form>
-->
{/if} {/if}
<button id="reset" type="reset" on:click={reset}>Reset</button> <button id="reset" type="reset" on:click={reset}>Reset</button>
<Nav /> <Nav />

View File

@ -0,0 +1,39 @@
<script lang="ts">
import type { LookupCardConfig } from "$lib/cards/Card.types";
import LookupCard from "$lib/cards/LookupCard.svelte";
import NearToMeCard from "$lib/cards/NearToMeCard.svelte";
import QuickLinkCard from "$lib/cards/QuickLinkCard.svelte";
import Header from "$lib/navigation/header.svelte";
import Nav from "$lib/navigation/nav.svelte";
import TimeBar from "$lib/navigation/TimeBar.svelte";
let LookupConfig: LookupCardConfig = {
title: "Live Arr/Dep Boards",
helpText: "Enter CRS, TIPLOC or STANOX code to see live departures",
placeholder: "Enter CRS/TIPLOC",
maxLen: 7,
formAction: "/ldb/",
fieldName: "station",
};
let TimetableConfig: LookupCardConfig = {
title: "Timetable & PIS",
helpText: "Enter a headcode to search the timetable and check PIS Codes",
placeholder: "Enter headcode",
maxLen: 4,
formAction: "/train/",
fieldName: "headcode",
};
function onRefresh() {
console.log("Refresh");
}
</script>
<Header title={"Test"} />
<TimeBar updatedTime={new Date()} />
<LookupCard config={LookupConfig} />
<LookupCard config={TimetableConfig} />
<NearToMeCard />
<QuickLinkCard />
<Nav />

View File

@ -1,55 +1,61 @@
<script> <script lang="ts">
import Header from '$lib/navigation/header.svelte'; import Header from "$lib/navigation/header.svelte";
import Loading from '$lib/navigation/loading.svelte'; import Loading from "$lib/navigation/loading.svelte";
import Island from '$lib/islands/island.svelte'; import Island from "$lib/islands/island.svelte";
import Nav from '$lib/navigation/nav.svelte'; import Nav from "$lib/navigation/nav.svelte";
import { uuid } from '$lib/stores/uuid'; import { uuid } from "$lib/stores/uuid";
import { onMount } from 'svelte'; import { onMount } from "svelte";
import TrainDetail from '$lib/train/train-detail.svelte'; import TrainDetail from "$lib/train/train-detail.svelte";
import { getApiUrl } from '$lib/scripts/upstream'; import { getApiUrl } from "$lib/scripts/upstream";
import toast from "svelte-french-toast";
import TimeBar from "$lib/navigation/TimeBar.svelte";
let title = 'Timetable Results'; let title = "Timetable Results";
let id = ''; let id = "";
let data = []; let data = [];
let isLoading = true; let isLoading = true;
let error = false; let error = false;
let errMsg = ''; let errMsg = "";
$: { $: {
if (id) { if (id) {
title = id.toUpperCase(); title = id.toUpperCase();
} else { } else {
title = 'Querying Timetable'; title = "Querying Timetable";
} }
} }
async function getHeadcode() { async function getHeadcode() {
return new URLSearchParams(window.location.search).get('headcode'); return new URLSearchParams(window.location.search).get("headcode");
} }
onMount(async () => { onMount(async () => {
isLoading = true; isLoading = true;
id = (await getHeadcode()) || ''; id = (await getHeadcode()) || "";
const res = await fetchData(id); const res = await fetchData(id);
if (res) { if (res) {
data = res; data = res;
if (!data.length) { if (!data.length) {
error = true; error = true;
errMsg = 'No services found'; errMsg = "No services found";
} }
} }
isLoading = false; isLoading = false;
toast("Registration soon required for timetable features.\n\nClick 'Register' in the menu.", {
duration: 3000,
});
}); });
async function fetchData(id = '') { async function fetchData(id = "") {
const date = 'now'; const date = "now";
const searchType = 'headcode'; const searchType = "headcode";
const options = { const options = {
method: 'GET', method: "GET",
headers: { headers: {
uuid: $uuid uuid: $uuid,
} },
}; };
const url = `${getApiUrl()}/api/v2/timetable/train/${date}/${searchType}/${id}`; const url = `${getApiUrl()}/api/v2/timetable/train/${date}/${searchType}/${id}`;
try { try {
@ -58,23 +64,23 @@
return await res.json(); return await res.json();
} else if (res.status === 401) { } else if (res.status === 401) {
error = true; error = true;
errMsg = 'You must be logged into the staff version for this feature'; errMsg = "You must be logged into the staff version for this feature";
return false; return false;
} else { } else {
error = true; error = true;
errMsg = 'Unable to connect, check your connection and try again'; errMsg = "Unable to connect, check your connection and try again";
return false; return false;
} }
} catch (err) { } catch (err) {
error = true; error = true;
errMsg = 'Connection error, try again later'; errMsg = "Connection error, try again later";
} }
isLoading = false; isLoading = false;
} }
</script> </script>
<Header {title} /> <Header {title} />
<div id="whitespace" /> <TimeBar updatedTime={undefined} />
{#if error} {#if error}
<Island> <Island>
@ -95,9 +101,6 @@
<Nav /> <Nav />
<style> <style>
#whitespace {
height: 15px;
}
p { p {
color: white; color: white;
font-size: 18px; font-size: 18px;

View File

@ -1,23 +1,31 @@
/// <reference types="@sveltejs/kit" /> /// <reference types="@sveltejs/kit" />
import { build, files, version } from '$service-worker'; import { build, files, version } from "$service-worker";
const cacheName = `ob-${version}`; 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) => { const excludePatterns = [
"/static/images/screnshots",
"/images/screenshots",
"/static/images/shortcuts",
"/images/shortcuts",
];
self.addEventListener("install", (event) => {
async function addToCache() { async function addToCache() {
const cache = await caches.open(cacheName); const cache = await caches.open(cacheName);
await cache.addAll(assets); const assetsToCache = assets.filter(asset => {
return !excludePatterns.some(pattern => asset.startsWith(pattern));
});
await cache.addAll(assetsToCache);
} }
event.waitUntil(addToCache()); event.waitUntil(addToCache());
}); });
self.addEventListener('activate', (event) => { self.addEventListener("activate", (event) => {
async function deleteOldCache() { async function deleteOldCache() {
for (const key of await caches.keys()) { for (const key of await caches.keys()) {
if (key !== cacheName) { if (key !== cacheName) {
@ -29,8 +37,8 @@ self.addEventListener('activate', (event) => {
event.waitUntil(deleteOldCache()); event.waitUntil(deleteOldCache());
}); });
self.addEventListener('fetch', (event) => { self.addEventListener("fetch", (event) => {
if (event.request.method !== 'GET') { if (event.request.method !== "GET") {
return; return;
} }
async function respond() { async function respond() {
@ -42,7 +50,7 @@ self.addEventListener('fetch', (event) => {
try { try {
return await fetch(event.request); return await fetch(event.request);
} catch (err) { } catch (err) {
return { error: 'OFFLINE', errorMsg: 'You are offline' }; return { error: "OFFLINE", errorMsg: "You are offline" };
} }
} }

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

View File

@ -1,10 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg enable-background="new 0 0 448 433" version="1.1" viewBox="0 0 448 433" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:cc="http://web.resource.org/cc/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
<radialGradient id="a" cx="216.7" cy="393.79" r="296.7" gradientUnits="userSpaceOnUse">
<stop stop-color="#F4D708" offset="0"/>
<stop stop-color="#FCB400" offset="1"/>
</radialGradient>
<path d="m8.551 390.5 184.85-368.8s26.409-31.504 52.815 0c26.41 31.501 180.19 370.65 180.19 370.65s3.105 18.534-27.961 18.534-361.94 0-361.94 0-23.299 0-27.959-20.38z" fill="url(#a)"/>
<path d="m8.551 390.5 184.85-368.8s26.409-31.504 52.815 0c26.41 31.501 180.19 370.65 180.19 370.65s3.105 18.534-27.961 18.534-361.94 0-361.94 0-23.299 0-27.959-20.38z" fill="none" stroke="#E2A713" stroke-width="5"/>
<path d="m212.5 292.63c-13.168-79.969-19.75-123.12-19.75-129.45 0-7.703 2.551-13.926 7.66-18.676 5.105-4.746 10.871-7.121 17.293-7.121 6.949 0 12.82 2.535 17.609 7.598s7.188 11.023 7.188 17.883c0 6.543-6.668 49.801-20 129.77h-10zm27 38.17c0 6.098-2.156 11.301-6.469 15.613-4.313 4.309-9.461 6.465-15.453 6.465-6.098 0-11.301-2.156-15.613-6.465-4.313-4.313-6.465-9.516-6.465-15.613 0-5.992 2.152-11.141 6.465-15.453s9.516-6.469 15.613-6.469c5.992 0 11.141 2.156 15.453 6.469s6.48 9.45 6.48 15.44z"/>
<metadata><rdf:RDF><cc:Work><dc:format>image/svg+xml</dc:format><dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/><cc:license rdf:resource="http://creativecommons.org/licenses/publicdomain/"/><dc:publisher><cc:Agent rdf:about="http://openclipart.org/"><dc:title>Openclipart</dc:title></cc:Agent></dc:publisher><dc:title>Warning Notification</dc:title><dc:date>2007-02-08T17:08:47</dc:date><dc:description>Beveled yellow caution sign</dc:description><dc:source>http://openclipart.org/detail/3130/warning-notification-by-eastshores</dc:source><dc:creator><cc:Agent><dc:title>eastshores</dc:title></cc:Agent></dc:creator><dc:subject><rdf:Bag><rdf:li>alert</rdf:li><rdf:li>caution</rdf:li><rdf:li>clip art</rdf:li><rdf:li>clipart</rdf:li><rdf:li>icon</rdf:li><rdf:li>image</rdf:li><rdf:li>media</rdf:li><rdf:li>public domain</rdf:li><rdf:li>svg</rdf:li><rdf:li>warning</rdf:li></rdf:Bag></dc:subject></cc:Work><cc:License rdf:about="http://creativecommons.org/licenses/publicdomain/"><cc:permits rdf:resource="http://creativecommons.org/ns#Reproduction"/><cc:permits rdf:resource="http://creativecommons.org/ns#Distribution"/><cc:permits rdf:resource="http://creativecommons.org/ns#DerivativeWorks"/></cc:License></rdf:RDF></metadata></svg>

Before

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 195 B

View File

@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="4.191mm" height="4.191mm" version="1.1" viewBox="0 0 4.191 4.191" xmlns="http://www.w3.org/2000/svg">
<g transform="translate(-91.032 -156.47)">
<path d="m95.223 158.3v0.52916h-3.175c0.48507 0.48507 0.97014 0.97014 1.4552 1.4552-0.12524 0.12524-0.25047 0.25047-0.37571 0.37571l-2.0955-2.0955 2.0955-2.0955c0.12524 0.12524 0.25047 0.25047 0.37571 0.37571-0.48507 0.48507-0.97014 0.97014-1.4552 1.4552h3.175z" fill="#f9f9f9" stroke-width=".26458"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 510 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 468 B

View File

@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="3.7042mm" height="3.7042mm" version="1.1" viewBox="0 0 3.7042 3.7042" xmlns="http://www.w3.org/2000/svg">
<g transform="translate(-20.8 -106.38)">
<path d="m24.505 106.75-0.37306-0.37306-1.479 1.479-1.479-1.479-0.37306 0.37306 1.479 1.479-1.479 1.479 0.37306 0.37306 1.479-1.479 1.479 1.479 0.37306-0.37306-1.479-1.479z" fill="#f9f9f9" stroke-width=".26458"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 422 B

View File

@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="4.7625mm" height="3.175mm" version="1.1" viewBox="0 0 4.7625 3.175" xmlns="http://www.w3.org/2000/svg">
<g transform="translate(-54.25 -207.32)">
<path d="m54.25 207.32h4.7625v0.52917h-4.7625v-0.52917m0 1.3229h4.7625v0.52916h-4.7625v-0.52916m0 1.3229h4.7625v0.52917h-4.7625z" fill="#f9f9f9" stroke-width=".26458"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 378 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -1,4 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="32" height="32" version="1.1" viewBox="0 96 640 640" xmlns="http://www.w3.org/2000/svg">
<path d="m320 736q-133 0-226.5-93.5t-93.5-226.5 93.5-226.5 226.5-93.5q85 0 149 34.5t111 94.5v-129h60v254h-254v-60h168q-38-60-97-97t-137-37q-109 0-184.5 75.5t-75.5 184.5 75.5 184.5 184.5 75.5q83 0 152-47.5t96-125.5h62q-29 105-115 169t-195 64z" fill="#fff"/>
</svg>

Before

Width:  |  Height:  |  Size: 404 B

View File

@ -1,10 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg enable-background="new 0 0 448 433" version="1.1" viewBox="0 0 448 433" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:cc="http://web.resource.org/cc/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
<radialGradient id="a" cx="216.7" cy="393.79" r="296.7" gradientUnits="userSpaceOnUse">
<stop stop-color="#F4D708" offset="0"/>
<stop stop-color="#FCB400" offset="1"/>
</radialGradient>
<path d="m8.551 390.5 184.85-368.8s26.409-31.504 52.815 0c26.41 31.501 180.19 370.65 180.19 370.65s3.105 18.534-27.961 18.534-361.94 0-361.94 0-23.299 0-27.959-20.38z" fill="url(#a)"/>
<path d="m8.551 390.5 184.85-368.8s26.409-31.504 52.815 0c26.41 31.501 180.19 370.65 180.19 370.65s3.105 18.534-27.961 18.534-361.94 0-361.94 0-23.299 0-27.959-20.38z" fill="none" stroke="#E2A713" stroke-width="5"/>
<path d="m212.5 292.63c-13.168-79.969-19.75-123.12-19.75-129.45 0-7.703 2.551-13.926 7.66-18.676 5.105-4.746 10.871-7.121 17.293-7.121 6.949 0 12.82 2.535 17.609 7.598s7.188 11.023 7.188 17.883c0 6.543-6.668 49.801-20 129.77h-10zm27 38.17c0 6.098-2.156 11.301-6.469 15.613-4.313 4.309-9.461 6.465-15.453 6.465-6.098 0-11.301-2.156-15.613-6.465-4.313-4.313-6.465-9.516-6.465-15.613 0-5.992 2.152-11.141 6.465-15.453s9.516-6.469 15.613-6.469c5.992 0 11.141 2.156 15.453 6.469s6.48 9.45 6.48 15.44z"/>
<metadata><rdf:RDF><cc:Work><dc:format>image/svg+xml</dc:format><dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/><cc:license rdf:resource="http://creativecommons.org/licenses/publicdomain/"/><dc:publisher><cc:Agent rdf:about="http://openclipart.org/"><dc:title>Openclipart</dc:title></cc:Agent></dc:publisher><dc:title>Warning Notification</dc:title><dc:date>2007-02-08T17:08:47</dc:date><dc:description>Beveled yellow caution sign</dc:description><dc:source>http://openclipart.org/detail/3130/warning-notification-by-eastshores</dc:source><dc:creator><cc:Agent><dc:title>eastshores</dc:title></cc:Agent></dc:creator><dc:subject><rdf:Bag><rdf:li>alert</rdf:li><rdf:li>caution</rdf:li><rdf:li>clip art</rdf:li><rdf:li>clipart</rdf:li><rdf:li>icon</rdf:li><rdf:li>image</rdf:li><rdf:li>media</rdf:li><rdf:li>public domain</rdf:li><rdf:li>svg</rdf:li><rdf:li>warning</rdf:li></rdf:Bag></dc:subject></cc:Work><cc:License rdf:about="http://creativecommons.org/licenses/publicdomain/"><cc:permits rdf:resource="http://creativecommons.org/ns#Reproduction"/><cc:permits rdf:resource="http://creativecommons.org/ns#Distribution"/><cc:permits rdf:resource="http://creativecommons.org/ns#DerivativeWorks"/></cc:License></rdf:RDF></metadata></svg>

Before

Width:  |  Height:  |  Size: 2.6 KiB

View File

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" height="48" viewBox="0 -960 960 960" width="48" xmlns:v="https://vecta.io/nano"><path d="M137.74-81.017h197.458v-329.096h289.605v329.096H822.26v-513.39L480-851.102 137.74-594.407zM58.757-2.034v-631.864L480-949.831l421.243 315.932V-2.034H545.819V-331.13H414.181V-2.034zM480-466.718z" fill="#fff"/></svg>

Before

Width:  |  Height:  |  Size: 342 B

View File

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" height="40" viewBox="0 -960 800 800" width="40" xmlns:v="https://vecta.io/nano"><path d="M373-360h60v-240h-60zm26.982-314q14.018 0 23.518-9.2 9.5-9.2 9.5-22.8 0-14.45-9.482-24.225-9.483-9.775-23.5-9.775-14.018 0-23.518 9.775Q367-720.45 367-706q0 13.6 9.482 22.8 9.483 9.2 23.5 9.2zm.284 514q-82.734 0-155.5-31.5Q172-223 117.5-277.5 63-332 31.5-404.841 0-477.681 0-560.5q0-82.819 31.5-155.659Q63-789 117.5-843q54.5-54 127.341-85.5Q317.681-960 400.5-960q82.819 0 155.659 31.5Q629-897 683-843q54 54 85.5 127Q800-643 800-560.266q0 82.734-31.5 155.5Q737-332 683-277.684q-54 54.316-127 86Q483-160 400.266-160zm.234-60Q542-220 641-319.5q99-99.5 99-241Q740-702 641.188-801 542.375-900 400-900q-141 0-240.5 98.812Q60-702.375 60-560q0 141 99.5 240.5Q259-220 400.5-220zm-.5-340z" fill="#fff"/></svg>

Before

Width:  |  Height:  |  Size: 828 B

Some files were not shown because too many files have changed in this diff Show More