Compare commits
175 Commits
Author | SHA1 | Date | |
---|---|---|---|
4f7acf9ffb | |||
1c308321de | |||
182136fc6b | |||
46c15f9601 | |||
ec413b6e5c | |||
0011bdb751 | |||
059eae3784 | |||
ee6e81de62 | |||
f6223ee826 | |||
58832ea2a8 | |||
479cc3051f | |||
de2258e309 | |||
c254588a55 | |||
4f84653c79 | |||
7cfcdc7205 | |||
6d2ddb9966 | |||
58ef9c153e | |||
e9028153cb | |||
fb540c7a46 | |||
c672495a5f | |||
8b361bb7de | |||
29b2054b4c | |||
c9262d64c8 | |||
2bc6efc677 | |||
a7d4158fb5 | |||
1d1e9416ab | |||
8a030964a4 | |||
fe3e6be4ad | |||
be850f5bd1 | |||
0c635d99dd | |||
70bba6635f | |||
53c5309485 | |||
7763f567f6 | |||
d48d4ffe4a | |||
10d749a5a7 | |||
ec4ba07cf7 | |||
78fc63fe29 | |||
4526cfa3e0 | |||
b6a8bd0461 | |||
f92f01af16 | |||
d75b69df26 | |||
2a07b0fa3e | |||
3d40445728 | |||
31b4653ca2 | |||
2b8d32f3c9 | |||
e3632986c2 | |||
693ad67980 | |||
a8b7379700 | |||
6d175f300f | |||
7f1dc1ac3f | |||
6f800dab67 | |||
5d84ac8ae2 | |||
872ea9f1d6 | |||
284dedbb3f | |||
512c77e81c | |||
9ad046dd9f | |||
91d523e372 | |||
b48795563f | |||
bf28984b80 | |||
38ceb1aadd | |||
eaa8c192a2 | |||
ba09910ff3 | |||
06edc49967 | |||
b4a3da5174 | |||
bebf2eba99 | |||
a3bf2af68d | |||
b779429346 | |||
db2a764167 | |||
89109a3a48 | |||
d17b9c23af | |||
62f6454b83 | |||
c8cd0f30d1 | |||
f82d015e52 | |||
30240edf00 | |||
7472f96b5d | |||
b63c63f679 | |||
f81acf348a | |||
5a9e55c695 | |||
fddf9cbbaf | |||
e0227516d8 | |||
95e45c8cb1 | |||
d09b24655a | |||
f93113ec14 | |||
75641bd245 | |||
12ef391ec4 | |||
982cee6bfe | |||
d3530063f3 | |||
313517605b | |||
13f7163dd7 | |||
35dd00499f | |||
4fbec34f24 | |||
3db490a0bb | |||
cd0e051a5a | |||
9e5d6d4732 | |||
8560d61348 | |||
98c9b5cc03 | |||
1df751c9ca | |||
60ece7661c | |||
c93f36102e | |||
0d0875f893 | |||
1fba04b2aa | |||
af58e923de | |||
1484a9068e | |||
8bd97c308c | |||
0f2b097c34 | |||
9e1984566b | |||
ef1c958d66 | |||
955c275ac9 | |||
5bec33c388 | |||
41f673c68f | |||
2a615a822e | |||
1b21dacfd9 | |||
735853aa8d | |||
958eabe76e | |||
7dcf0c8b1b | |||
ee8b547a19 | |||
3b7f34bdab | |||
3abdc7d740 | |||
8c91a50a34 | |||
21eabfc7d7 | |||
94434cdcf8 | |||
b6d3d128bc | |||
0f7deee78a | |||
d107416bb0 | |||
70fb62fd6f | |||
33fb2a607f | |||
0a666afc58 | |||
3f4a172f48 | |||
42e695d89f | |||
4537ff51a8 | |||
a52d1fa173 | |||
1bb9db3bc3 | |||
80b3c235af | |||
4dd9ea05d6 | |||
b9d18950b9 | |||
6c0d152358 | |||
a4276bd0e9 | |||
008e106877 | |||
bf93df98cd | |||
27e56b4177 | |||
782a60191b | |||
476b5e45c3 | |||
9cd082193b | |||
b9f5f3dc1d | |||
f5760acd47 | |||
5029ca7088 | |||
e3c8cc069f | |||
23d75880b5 | |||
e6ee0dc321 | |||
6505ba7f60 | |||
b17fa46151 | |||
84c497f3c5 | |||
20695549b5 | |||
ceb8533154 | |||
2fc19dfab8 | |||
1be9fea029 | |||
08cd8257a7 | |||
a8a81970d6 | |||
59d6eb7001 | |||
4a2e4fd2aa | |||
bb3ae45a37 | |||
a4294c36a8 | |||
45b64ef4ea | |||
b02e015496 | |||
5092de122a | |||
a60448613b | |||
74079e6d9a | |||
d49c725215 | |||
72495a63be | |||
e2cefdda08 | |||
bb0d389b09 | |||
8773b080f3 | |||
3d22bf5910 | |||
daf7085d60 | |||
41cc0b5ea1 |
9
.dockerignore
Normal file
9
.dockerignore
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
.DS_Store
|
||||||
|
node_modules
|
||||||
|
/build
|
||||||
|
/package
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
|
vite.config.js.timestamp-*
|
||||||
|
vite.config.ts.timestamp-*
|
@ -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",
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
};
|
};
|
||||||
|
14
.prettierrc
14
.prettierrc
@ -1,11 +1,7 @@
|
|||||||
{
|
{
|
||||||
"useTabs": false,
|
"tabWidth": 4,
|
||||||
"tabWidth": 2,
|
"printWidth": 180,
|
||||||
"semi": true,
|
"plugins": ["prettier-plugin-svelte"],
|
||||||
"singleQuote": true,
|
"pluginSearchDirs": ["."],
|
||||||
"trailingComma": "none",
|
"overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
|
||||||
"printWidth": 180,
|
|
||||||
"plugins": ["prettier-plugin-svelte"],
|
|
||||||
"pluginSearchDirs": ["."],
|
|
||||||
"overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
|
|
||||||
}
|
}
|
||||||
|
33
nginx.conf
33
nginx.conf
@ -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 backend:8460;
|
||||||
}
|
}
|
||||||
|
|
||||||
server {
|
server {
|
||||||
@ -44,24 +52,33 @@ http {
|
|||||||
|
|
||||||
location / {
|
location / {
|
||||||
root /usr/share/nginx/html;
|
root /usr/share/nginx/html;
|
||||||
index index.html;
|
index /index.html;
|
||||||
gzip_static on;
|
gzip_static on;
|
||||||
brotli_static on;
|
brotli_static on;
|
||||||
error_page 500 502 503 504 /50x.html;
|
error_page 500 502 503 504 /err/50x.html;
|
||||||
try_files $uri $uri.html $uri/ =404;
|
try_files $uri $uri/ $uri.html /index.html;
|
||||||
add_header Cache-Control "public, no-transform, max-age=1209600";
|
add_header Cache-Control "public, no-transform, max-age=1209600";
|
||||||
}
|
}
|
||||||
|
|
||||||
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";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
5622
package-lock.json
generated
5622
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
60
package.json
60
package.json
@ -1,30 +1,34 @@
|
|||||||
{
|
{
|
||||||
"name": "owlboard-svelte",
|
"name": "owlboard-svelte",
|
||||||
"version": "0.0.1",
|
"version": "2024.11.4",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite dev",
|
"dev": "vite dev",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"preview": "vite preview",
|
"postbuild": "npx svelte-sitemap --domain https://owlboard.info --ignore '**/err/**' --ignore '**/reg/submit'",
|
||||||
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
"preview": "vite preview",
|
||||||
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||||
"lint": "prettier --plugin-search-dir . --check . && eslint .",
|
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
||||||
"format": "prettier --plugin-search-dir . --write ."
|
"lint": "prettier --plugin-search-dir . --check . && eslint .",
|
||||||
},
|
"format": "prettier --plugin-search-dir . --write ."
|
||||||
"devDependencies": {
|
},
|
||||||
"@owlboard/ts-types": "^0.0.8",
|
"devDependencies": {
|
||||||
"@sveltejs/adapter-auto": "^2.0.0",
|
"@owlboard/ts-types": "^1.2.1",
|
||||||
"@sveltejs/adapter-static": "^2.0.2",
|
"@sveltejs/adapter-auto": "^2.0.0",
|
||||||
"@sveltejs/kit": "^1.5.0",
|
"@sveltejs/adapter-static": "^2.0.2",
|
||||||
"eslint": "^8.28.0",
|
"@sveltejs/kit": "^1.5.0",
|
||||||
"eslint-config-prettier": "^8.5.0",
|
"eslint": "^8.28.0",
|
||||||
"eslint-plugin-svelte": "^2.26.0",
|
"eslint-config-prettier": "^8.5.0",
|
||||||
"prettier": "^2.8.0",
|
"eslint-plugin-svelte": "^2.26.0",
|
||||||
"prettier-plugin-svelte": "^2.8.1",
|
"prettier": "^2.8.0",
|
||||||
"svelte": "^3.54.0",
|
"prettier-plugin-svelte": "^2.8.1",
|
||||||
"svelte-check": "^3.0.1",
|
"svelte": "^3.54.0",
|
||||||
"typescript": "^5.0.0",
|
"svelte-check": "^3.0.1",
|
||||||
"vite": "^4.3.0"
|
"svelte-french-toast": "^1.2.0",
|
||||||
},
|
"svelte-sitemap": "^2.6.0",
|
||||||
"type": "module"
|
"typescript": "^5.0.0",
|
||||||
|
"vite": "^4.3.0",
|
||||||
|
"@tabler/icons-svelte": "^3.2.0"
|
||||||
|
},
|
||||||
|
"type": "module"
|
||||||
}
|
}
|
||||||
|
76
src/app.css
76
src/app.css
@ -1,76 +0,0 @@
|
|||||||
/* FONTS */
|
|
||||||
@font-face {
|
|
||||||
font-family: 'firamono';
|
|
||||||
src: url('/font/firamono/firamono-regular.woff2') format('woff2'), url('/font/firamono/firamono-regular.woff') format('woff'),
|
|
||||||
url('/font/firamono/firamono-regular.ttf') format('truetype');
|
|
||||||
font-weight: normal;
|
|
||||||
font-style: normal;
|
|
||||||
}
|
|
||||||
@font-face {
|
|
||||||
font-family: 'firamono';
|
|
||||||
src: url('/font/firamono/firamono-500.woff2') format('woff2'), url('/font/firamono/firamono-500.woff') format('woff'), url('/font/firamono/firamono-500.ttf') format('truetype');
|
|
||||||
font-weight: 500;
|
|
||||||
font-style: normal;
|
|
||||||
}
|
|
||||||
@font-face {
|
|
||||||
font-family: 'urwgothic';
|
|
||||||
src: url('/font/urwgothic/urwgothic.woff2') format('woff2'), url('/font/urwgothic/urwgothic.woff') format('woff'), url('/font/urwgothic/urwgothic.ttf') format('truetype');
|
|
||||||
font-weight: normal;
|
|
||||||
font-style: normal;
|
|
||||||
}
|
|
||||||
@font-face {
|
|
||||||
font-family: 'urwgothic';
|
|
||||||
src: url('/font/urwgothic/urwgothicDemi.woff2') format('woff2'), url('/font/urwgothic/urwgothicDemi.woff') format('woff'),
|
|
||||||
url('/font/urwgothic/urwgothicDemi.ttf') format('truetype');
|
|
||||||
font-weight: 900;
|
|
||||||
font-style: normal;
|
|
||||||
}
|
|
||||||
|
|
||||||
@font-face {
|
|
||||||
font-family: 'ubuntu';
|
|
||||||
src: url('/font/ubuntumono/ubuntumono-regular.woff2') format('woff2'), url('/font/ubuntumono/ubuntumono-regular.woff') format('woff'),
|
|
||||||
url('/font/ubuntumono/ubuntumono-regular.ttf') format('truetype');
|
|
||||||
font-weight: 400;
|
|
||||||
font-style: normal;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* COLOR VARS */
|
|
||||||
:root {
|
|
||||||
--main-bg-color: #404c55;
|
|
||||||
--second-bg-color: #2b343c; /* Use as first arg in radial gradient */
|
|
||||||
--accent-color: #007979;
|
|
||||||
--overlay-color: #3c6f79de;
|
|
||||||
--overlay-color-solid: #3c6f79;
|
|
||||||
--main-text-color: #00b7b7;
|
|
||||||
--second-text-color: #0afdfd;
|
|
||||||
--note-text-color: #9de7ff;
|
|
||||||
--link-color: azure;
|
|
||||||
--box-border-color: ;
|
|
||||||
--link-visited-color: azure;
|
|
||||||
--main-alert-color: #ed6d00;
|
|
||||||
--second-alert-color: #e77f00; /* Use as second arg in radial gradient */
|
|
||||||
--main-warning-color: orange;
|
|
||||||
--board-name-color: #fcfc09;
|
|
||||||
}
|
|
||||||
|
|
||||||
html {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
background-color: var(--main-bg-color);
|
|
||||||
background-image: radial-gradient(var(--second-bg-color), var(--main-bg-color));
|
|
||||||
color: var(--second-text-color);
|
|
||||||
font-family: urwgothic, sans-serif;
|
|
||||||
text-align: center;
|
|
||||||
margin: auto;
|
|
||||||
width: 100%;
|
|
||||||
padding-bottom: 65px;
|
|
||||||
}
|
|
||||||
a {
|
|
||||||
color: var(--link-color);
|
|
||||||
}
|
|
||||||
button:hover {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
12
src/app.d.ts
vendored
12
src/app.d.ts
vendored
@ -1,12 +1,12 @@
|
|||||||
// See https://kit.svelte.dev/docs/types#app
|
// See https://kit.svelte.dev/docs/types#app
|
||||||
// for information about these interfaces
|
// for information about these interfaces
|
||||||
declare global {
|
declare global {
|
||||||
namespace App {
|
namespace App {
|
||||||
// interface Error {}
|
// interface Error {}
|
||||||
// interface Locals {}
|
// interface Locals {}
|
||||||
// interface PageData {}
|
// interface PageData {}
|
||||||
// interface Platform {}
|
// interface Platform {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export {};
|
export {};
|
||||||
|
14
src/app.html
14
src/app.html
@ -1,10 +1,10 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
%sveltekit.head%
|
%sveltekit.head%
|
||||||
</head>
|
</head>
|
||||||
<body data-sveltekit-preload-data="hover">
|
<body data-sveltekit-preload-data="hover">
|
||||||
<div style="display: contents">%sveltekit.body%</div>
|
<div style="display: contents">%sveltekit.body%</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
@ -1,19 +1,20 @@
|
|||||||
<div id="banner">DEVMODE</div>
|
<div id="banner">DEVMODE</div>
|
||||||
|
|
||||||
<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;
|
||||||
line-height: 40px;
|
line-height: 40px;
|
||||||
right: -50px;
|
right: -50px;
|
||||||
left: auto;
|
left: auto;
|
||||||
-ms-transform: rotate(45deg);
|
-ms-transform: rotate(45deg);
|
||||||
-webkit-transform: rotate(45deg);
|
-webkit-transform: rotate(45deg);
|
||||||
transform: rotate(45deg);
|
transform: rotate(45deg);
|
||||||
z-index: 100;
|
z-index: 100;
|
||||||
}
|
box-shadow: 5px 5px 30px rgba(0, 0, 0, 0.451);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
59
src/lib/Tooltip.svelte
Normal file
59
src/lib/Tooltip.svelte
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { 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;
|
||||||
|
top: 50%;
|
||||||
|
right: 125%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
margin-left: -60px;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.5s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip:hover .tooltiptext {
|
||||||
|
visibility: visible;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
</style>
|
35
src/lib/buttons/LinkButton.svelte
Normal file
35
src/lib/buttons/LinkButton.svelte
Normal 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>
|
34
src/lib/buttons/ScriptButton.svelte
Normal file
34
src/lib/buttons/ScriptButton.svelte
Normal 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
108
src/lib/cards/Card.svelte
Normal 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>
|
19
src/lib/cards/Card.types.ts
Normal file
19
src/lib/cards/Card.types.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
interface CardConfig {
|
||||||
|
title: string;
|
||||||
|
showHelp: boolean;
|
||||||
|
showRefresh: boolean;
|
||||||
|
helpText: string;
|
||||||
|
onRefresh: () => void;
|
||||||
|
refreshing: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface LookupCardConfig {
|
||||||
|
title: string;
|
||||||
|
formAction: string;
|
||||||
|
maxLen: number;
|
||||||
|
placeholder: string;
|
||||||
|
helpText: string;
|
||||||
|
fieldName: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type {CardConfig, LookupCardConfig}
|
14
src/lib/cards/FindByHeadcodeCard.svelte
Normal file
14
src/lib/cards/FindByHeadcodeCard.svelte
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import LookupCard from "./LookupCard.svelte";
|
||||||
|
|
||||||
|
const LookupCardConfig = {
|
||||||
|
title: "Find By Headcode",
|
||||||
|
helpText: "",
|
||||||
|
formAction: "/train",
|
||||||
|
placeholder: "enter headcode",
|
||||||
|
maxLen: 4,
|
||||||
|
fieldName: "headcode",
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<LookupCard config={LookupCardConfig} />
|
64
src/lib/cards/LookupCard.svelte
Normal file
64
src/lib/cards/LookupCard.svelte
Normal 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>
|
107
src/lib/cards/NearToMeCard.svelte
Normal file
107
src/lib/cards/NearToMeCard.svelte
Normal 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>
|
45
src/lib/cards/QuickLinkCard.svelte
Normal file
45
src/lib/cards/QuickLinkCard.svelte
Normal 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>
|
@ -1,10 +1,10 @@
|
|||||||
<img src="/images/logo/wide_logo.svg" alt="Logo" />
|
<img src="/images/logo/wide_logo.svg" alt="Logo" />
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
img {
|
img {
|
||||||
width: 50%;
|
width: 50%;
|
||||||
max-width: 250px;
|
max-width: 250px;
|
||||||
margin-top: 55px;
|
margin-top: 55px;
|
||||||
margin-bottom: 55px;
|
margin-bottom: 55px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -1,45 +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;
|
|
||||||
}
|
|
||||||
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(--main-bg-color);
|
|
||||||
color: var(--link-color);
|
|
||||||
}
|
|
||||||
</style>
|
|
@ -1,28 +1,29 @@
|
|||||||
<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 }}>
|
||||||
<span>{variables.title}</span>
|
<span>{variables.title}</span>
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<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(--main-text-color);
|
color: var(--island-header-color);
|
||||||
}
|
}
|
||||||
div {
|
div {
|
||||||
width: 85%;
|
width: 85%;
|
||||||
max-width: 400px;
|
max-width: 400px;
|
||||||
margin: auto;
|
margin: auto;
|
||||||
margin-top: 25px;
|
margin-top: 25px;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
background-color: var(--overlay-color);
|
background-color: var(--island-bg-color);
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
}
|
box-shadow: 5px 5px 30px rgba(0, 0, 0, 0.29);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -1,36 +1,36 @@
|
|||||||
<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 }}>
|
||||||
<span>{variables.title}</span>
|
<span>{variables.title}</span>
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<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(--main-text-color);
|
color: var(--island-header-color);
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
}
|
}
|
||||||
div {
|
div {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 50%;
|
top: 50%;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
transform: translateY(-50%) translateX(-50%);
|
transform: translateY(-50%) translateX(-50%);
|
||||||
width: 85%;
|
width: 85%;
|
||||||
height: auto;
|
height: auto;
|
||||||
max-height: 85vh;
|
max-height: 75vh;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
max-width: 400px;
|
max-width: 400px;
|
||||||
margin: auto;
|
margin: auto;
|
||||||
margin-top: 25px;
|
margin-top: 25px;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
background-color: var(--overlay-color);
|
background-color: var(--island-bg-solid);
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
z-index: 1000;
|
z-index: 1000;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -1,53 +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(--main-bg-color);
|
|
||||||
color: var(--link-color);
|
|
||||||
}
|
|
||||||
</style>
|
|
@ -1,115 +1,126 @@
|
|||||||
<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";
|
||||||
export let variables = {
|
import toast from "svelte-french-toast";
|
||||||
title: 'Quick Links'
|
export let variables = {
|
||||||
};
|
title: "Quick Links",
|
||||||
|
};
|
||||||
|
|
||||||
let qlData: string[] = [];
|
let qlData: string[] = [];
|
||||||
$: {
|
$: {
|
||||||
qlData = $ql;
|
qlData = $ql;
|
||||||
console.log(qlData);
|
console.log(qlData);
|
||||||
}
|
|
||||||
|
|
||||||
let saveButton = 'Save';
|
|
||||||
|
|
||||||
async function timeout(ms: number): Promise<any> {
|
|
||||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
||||||
}
|
|
||||||
|
|
||||||
async function saveQl() {
|
|
||||||
// Fetch the content of all text entries within the island then run ql.set([ARRAY OF INPUT CONTENT])
|
|
||||||
const inputs = document.getElementsByClassName('qlInput');
|
|
||||||
let inputLinks: string[] = [];
|
|
||||||
for (let item of inputs) {
|
|
||||||
let text = (<HTMLInputElement>item)?.value;
|
|
||||||
if (text !== '') {
|
|
||||||
inputLinks.push(text);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
console.log(inputLinks);
|
|
||||||
ql.set(inputLinks);
|
|
||||||
saveButton = '✔';
|
|
||||||
await timeout(3000);
|
|
||||||
saveButton = 'Saved';
|
|
||||||
}
|
|
||||||
|
|
||||||
function clearQl() {
|
let saveButton = "Save";
|
||||||
ql.set([]);
|
|
||||||
saveButton = 'Saved';
|
|
||||||
}
|
|
||||||
|
|
||||||
function addQlBox() {
|
async function timeout(ms: number): Promise<any> {
|
||||||
saveButton = 'Save';
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||||
const updatedQl = [...$ql, ''];
|
}
|
||||||
$ql = updatedQl;
|
|
||||||
ql.set(updatedQl);
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleClick(event: any) {
|
function save() {
|
||||||
// Handle the click event here
|
toast.promise(saveQl(), {
|
||||||
console.log('Island Clicked');
|
loading: "Saving...",
|
||||||
// You can access the `variables` passed to the Island component here if needed
|
success: "Quick Links saved!",
|
||||||
}
|
error: "Failed to save.",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveQl() {
|
||||||
|
// Fetch the content of all text entries within the island then run ql.set([ARRAY OF INPUT CONTENT])
|
||||||
|
const inputs = document.getElementsByClassName("qlInput");
|
||||||
|
let inputLinks: string[] = [];
|
||||||
|
for (let item of inputs) {
|
||||||
|
let text = (<HTMLInputElement>item)?.value;
|
||||||
|
if (text !== "") {
|
||||||
|
inputLinks.push(text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.log(inputLinks);
|
||||||
|
ql.set(inputLinks);
|
||||||
|
await timeout(1000);
|
||||||
|
saveButton = "Saved";
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearQl() {
|
||||||
|
ql.set([]);
|
||||||
|
saveButton = "Saved";
|
||||||
|
toast.success("Cleared Quick Links.");
|
||||||
|
}
|
||||||
|
|
||||||
|
function addQlBox() {
|
||||||
|
saveButton = "Save";
|
||||||
|
const updatedQl = [...$ql, ""];
|
||||||
|
$ql = updatedQl;
|
||||||
|
ql.set(updatedQl);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleClick(event: any) {
|
||||||
|
// Handle the click event here
|
||||||
|
console.log("Island Clicked");
|
||||||
|
// You can access the `variables` passed to the Island component here if needed
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Island on:click={handleClick} {variables}>
|
<Island on:click={handleClick} {variables}>
|
||||||
{#if $ql.length === 0}
|
{#if $ql.length === 0}
|
||||||
<p>Click the + button to add links</p>
|
<p>Click the + button to add links</p>
|
||||||
{/if}
|
{/if}
|
||||||
<div id="buttons" class="buttons">
|
<div id="buttons" class="buttons">
|
||||||
<p>Quick links can be CRS Codes or Headcodes</p>
|
<p>Quick links can be CRS Codes or Headcodes</p>
|
||||||
{#each qlData as link}
|
{#each qlData as link}
|
||||||
<input class="qlInput" type="text" value={link} />
|
<input class="qlInput" type="text" value={link} />
|
||||||
{/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>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
p {
|
p {
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
#qlAdd {
|
#qlAdd {
|
||||||
width: 40px;
|
width: 40px;
|
||||||
}
|
}
|
||||||
.buttons {
|
.buttons {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
width: 90%;
|
width: 90%;
|
||||||
margin: auto;
|
margin: auto;
|
||||||
padding-top: 5px;
|
padding-top: 5px;
|
||||||
}
|
}
|
||||||
input {
|
input {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
width: 20%;
|
width: 20%;
|
||||||
min-width: 50px;
|
min-width: 50px;
|
||||||
margin: 5px;
|
margin: 5px;
|
||||||
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;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
}
|
box-shadow: var(--box-shadow);
|
||||||
button {
|
}
|
||||||
width: 30%;
|
button {
|
||||||
margin-bottom: 5px;
|
width: 30%;
|
||||||
margin-top: 10px;
|
margin-bottom: 5px;
|
||||||
border: none;
|
margin-top: 10px;
|
||||||
border-radius: 20px;
|
border: none;
|
||||||
padding: 5px;
|
border-radius: 20px;
|
||||||
font-family: urwgothic, 'Lucida Sans', 'Lucida Sans Regular', 'Lucida Grande', 'Lucida Sans Unicode', Geneva, Verdana, sans-serif;
|
padding: 5px;
|
||||||
font-size: 16px;
|
font-family: urwgothic, "Lucida Sans", "Lucida Sans Regular", "Lucida Grande", "Lucida Sans Unicode", Geneva, Verdana, sans-serif;
|
||||||
font-weight: 400;
|
font-size: 16px;
|
||||||
background-color: var(--main-bg-color);
|
font-weight: 400;
|
||||||
color: var(--link-color);
|
background-color: var(--island-button-color);
|
||||||
}
|
color: var(--island-link-color);
|
||||||
|
box-shadow: var(--box-shadow);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -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: white;
|
|
||||||
}
|
|
||||||
</style>
|
|
106
src/lib/ldb/common/nrcc/alert-bar.svelte
Normal file
106
src/lib/ldb/common/nrcc/alert-bar.svelte
Normal 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>
|
@ -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'];
|
|
||||||
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>
|
|
@ -1,485 +1,488 @@
|
|||||||
<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 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;
|
||||||
|
|
||||||
let jsonData = null;
|
let jsonData = null;
|
||||||
let services = [];
|
let services = [];
|
||||||
let busServices = [];
|
let busServices = [];
|
||||||
let ferryServices = [];
|
let ferryServices = [];
|
||||||
let dataAge = null;
|
let dataAge = null;
|
||||||
let isLoading = true;
|
let isLoading = true;
|
||||||
let dataExists = false;
|
let dataExists = false;
|
||||||
let alerts = [];
|
let alerts = [];
|
||||||
let serviceDetail;
|
let serviceDetail;
|
||||||
|
|
||||||
$: {
|
$: {
|
||||||
if (jsonData === null && requestedStation) {
|
if (jsonData === null && requestedStation) {
|
||||||
fetchData();
|
fetchData();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (jsonData?.GetStationBoardResult?.generatedAt) {
|
||||||
|
dataAge = new Date(jsonData.GetStationBoardResult.generatedAt);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (jsonData?.GetStationBoardResult?.trainServices?.service) {
|
||||||
|
services = jsonData.GetStationBoardResult.trainServices.service;
|
||||||
|
} else {
|
||||||
|
services = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (jsonData?.GetStationBoardResult?.busServices?.service) {
|
||||||
|
busServices = jsonData.GetStationBoardResult.busServices.service;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (jsonData?.GetStationBoardResult?.ferryServices?.service) {
|
||||||
|
ferryServices = jsonData.GetStationBoardResult.ferryServices.service;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (jsonData?.GetStationBoardResult?.locationName) {
|
||||||
|
title = jsonData.GetStationBoardResult.locationName;
|
||||||
|
} else {
|
||||||
|
title = requestedStation.toUpperCase();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (jsonData?.GetStationBoardResult?.generatedAt) {
|
async function fetchData() {
|
||||||
dataAge = new Date(jsonData.GetStationBoardResult.generatedAt);
|
dataExists = true;
|
||||||
|
isLoading = true; // Set loading state
|
||||||
|
try {
|
||||||
|
console.log(`Requested Station: ${requestedStation}`);
|
||||||
|
const data = await fetch(`${getApiUrl()}/api/v2/live/station/${requestedStation}/public`);
|
||||||
|
jsonData = await data.json();
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching data:", error);
|
||||||
|
dataExists = false;
|
||||||
|
title = "Not Found";
|
||||||
|
} finally {
|
||||||
|
isLoading = false; // Clear loading state
|
||||||
|
}
|
||||||
|
prepareNrcc();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (jsonData?.GetStationBoardResult?.trainServices?.service) {
|
function parseTime(string) {
|
||||||
services = jsonData.GetStationBoardResult.trainServices.service;
|
let output;
|
||||||
} else {
|
let change;
|
||||||
services = [];
|
switch (string) {
|
||||||
|
case "Delayed":
|
||||||
|
output = "LATE";
|
||||||
|
change = "changed";
|
||||||
|
break;
|
||||||
|
case "Cancelled":
|
||||||
|
output = "CANC";
|
||||||
|
change = "cancelled";
|
||||||
|
break;
|
||||||
|
case "On Time":
|
||||||
|
case "On time":
|
||||||
|
output = "RT";
|
||||||
|
change = "";
|
||||||
|
break;
|
||||||
|
case "":
|
||||||
|
output = "-";
|
||||||
|
change = "";
|
||||||
|
break;
|
||||||
|
case undefined:
|
||||||
|
output = "-";
|
||||||
|
change = "";
|
||||||
|
break;
|
||||||
|
case "No report":
|
||||||
|
output = "-";
|
||||||
|
change = "";
|
||||||
|
break;
|
||||||
|
case "undefined":
|
||||||
|
output = false;
|
||||||
|
change = "";
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
output = string;
|
||||||
|
change = "changed";
|
||||||
|
}
|
||||||
|
return { data: output, changed: change };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (jsonData?.GetStationBoardResult?.busServices?.service) {
|
async function loadService(sid) {
|
||||||
busServices = jsonData.GetStationBoardResult.busServices.service;
|
for (const service of services) {
|
||||||
|
if (service.serviceID == sid) {
|
||||||
|
serviceDetail = service;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (jsonData?.GetStationBoardResult?.ferryServices?.service) {
|
async function loadBusService(sid) {
|
||||||
ferryServices = jsonData.GetStationBoardResult.ferryServices.service;
|
for (const service of busServices) {
|
||||||
|
if (service.serviceID == sid) {
|
||||||
|
serviceDetail = service;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (jsonData?.GetStationBoardResult?.locationName) {
|
async function closeService() {
|
||||||
title = jsonData.GetStationBoardResult.locationName;
|
serviceDetail = null;
|
||||||
} else {
|
|
||||||
title = requestedStation.toUpperCase();
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
async function fetchData() {
|
async function prepareNrcc() {
|
||||||
dataExists = true;
|
if (jsonData?.GetStationBoardResult?.nrccMessages?.message) {
|
||||||
isLoading = true; // Set loading state
|
const nrcc = jsonData.GetStationBoardResult.nrccMessages.message;
|
||||||
try {
|
if (Array.isArray(nrcc)) {
|
||||||
console.log(`Requested Station: ${requestedStation}`);
|
alerts = nrcc;
|
||||||
const data = await fetch(`https://owlboard.info/api/v2/live/station/${requestedStation}/public`);
|
return;
|
||||||
jsonData = await data.json();
|
}
|
||||||
} catch (error) {
|
alerts.push(nrcc);
|
||||||
console.error('Error fetching data:', error);
|
return;
|
||||||
dataExists = false;
|
}
|
||||||
title = 'Not Found';
|
|
||||||
} finally {
|
|
||||||
isLoading = false; // Clear loading state
|
|
||||||
}
|
}
|
||||||
prepareNrcc();
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseTime(string) {
|
onMount(() => {
|
||||||
let output;
|
toast("Register for more detailed departure boards")
|
||||||
let change;
|
if (requestedStation && jsonData === null) {
|
||||||
switch (string) {
|
fetchData();
|
||||||
case 'Delayed':
|
}
|
||||||
output = 'LATE';
|
});
|
||||||
change = 'changed';
|
|
||||||
break;
|
|
||||||
case 'Cancelled':
|
|
||||||
output = 'CANC';
|
|
||||||
change = 'cancelled';
|
|
||||||
break;
|
|
||||||
case 'On Time':
|
|
||||||
case 'On time':
|
|
||||||
output = 'RT';
|
|
||||||
change = '';
|
|
||||||
break;
|
|
||||||
case '':
|
|
||||||
output = '-';
|
|
||||||
change = '';
|
|
||||||
break;
|
|
||||||
case undefined:
|
|
||||||
output = '-';
|
|
||||||
change = '';
|
|
||||||
break;
|
|
||||||
case 'No report':
|
|
||||||
output = '-';
|
|
||||||
change = '';
|
|
||||||
break;
|
|
||||||
case 'undefined':
|
|
||||||
output = false;
|
|
||||||
change = '';
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
output = string;
|
|
||||||
change = 'changed';
|
|
||||||
}
|
|
||||||
return { data: output, changed: change };
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadService(sid) {
|
|
||||||
for (const service of services) {
|
|
||||||
if (service.serviceID == sid) {
|
|
||||||
serviceDetail = service;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadBusService(sid) {
|
|
||||||
for (const service of busServices) {
|
|
||||||
if (service.serviceID == sid) {
|
|
||||||
serviceDetail = service;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function closeService() {
|
|
||||||
serviceDetail = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function prepareNrcc() {
|
|
||||||
if (jsonData?.GetStationBoardResult?.nrccMessages?.message) {
|
|
||||||
const nrcc = jsonData.GetStationBoardResult.nrccMessages.message;
|
|
||||||
if (Array.isArray(nrcc)) {
|
|
||||||
alerts = nrcc;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
alerts.push(nrcc);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onMount(() => {
|
|
||||||
if (requestedStation && jsonData === null) {
|
|
||||||
fetchData();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if alerts.length}
|
{#if alerts.length}
|
||||||
<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>
|
<th class="from">From</th>
|
||||||
<th class="from">From</th>
|
<th class="to">To</th>
|
||||||
<th class="to">To</th>
|
<th class="plat">Plat.</th>
|
||||||
<th class="plat">Plat.</th>
|
<th class="time">Sch Arr.</th>
|
||||||
<th class="time">Sch Arr.</th>
|
<th class="time">Exp Arr.</th>
|
||||||
<th class="time">Exp Arr.</th>
|
<th class="time">Sch Dep.</th>
|
||||||
<th class="time">Sch Dep.</th>
|
<th class="time">Exp Dep.</th>
|
||||||
<th class="time">Exp Dep.</th>
|
</tr>
|
||||||
</tr>
|
{#each services as service}
|
||||||
{#each services as service}
|
<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>
|
<td class="time {parseTime(service.etd).changed}">{parseTime(service.etd).data}</td>
|
||||||
<td class="time {parseTime(service.etd).changed}">{parseTime(service.etd).data}</td>
|
</tr>
|
||||||
</tr>
|
|
||||||
|
|
||||||
<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}
|
||||||
<p class="service-detail">{service.delayReason}</p>
|
<p class="service-detail">{service.delayReason}</p>
|
||||||
{/if}
|
{/if}
|
||||||
{#if service.cancelReason}
|
{#if service.cancelReason}
|
||||||
<p class="service-detail">{service.cancelReason}</p>
|
<p class="service-detail">{service.cancelReason}</p>
|
||||||
{/if}
|
{/if}
|
||||||
</td></tr
|
</td></tr
|
||||||
>
|
>
|
||||||
{/each}
|
{/each}
|
||||||
</table>
|
</table>
|
||||||
{:else}
|
{:else}
|
||||||
<p class="table-head-text">No Scheduled Train Services</p>
|
<p class="table-head-text">No Scheduled Train Services</p>
|
||||||
{/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>
|
||||||
<th class="from">From</th>
|
<th class="from">From</th>
|
||||||
<th class="to">To</th>
|
<th class="to">To</th>
|
||||||
<th class="time">Sch Arr.</th>
|
<th class="time">Sch Arr.</th>
|
||||||
<th class="time">Exp Arr.</th>
|
<th class="time">Exp Arr.</th>
|
||||||
<th class="time">Sch Dep.</th>
|
<th class="time">Sch Dep.</th>
|
||||||
<th class="time">Exp Dep.</th>
|
<th class="time">Exp Dep.</th>
|
||||||
</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)}
|
||||||
<td class="origdest to" on:click={loadBusService(service.serviceID)} on:keypress={loadBusService(service.serviceID)}
|
>{service.origin?.location?.locationName || ""}</td
|
||||||
>{service.destination?.location?.locationName || ''}</td
|
>
|
||||||
>
|
<td class="origdest to" on:click={loadBusService(service.serviceID)} on:keypress={loadBusService(service.serviceID)}
|
||||||
<td class="time">{parseTime(service.sta).data}</td>
|
>{service.destination?.location?.locationName || ""}</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.sta).data}</td>
|
||||||
<td class="time {parseTime(service.etd).changed}">{parseTime(service.etd).data}</td>
|
<td class="time {parseTime(service.eta).changed}">{parseTime(service.eta).data}</td>
|
||||||
</tr>
|
<td class="time">{parseTime(service.std).data}</td>
|
||||||
|
<td class="time {parseTime(service.etd).changed}">{parseTime(service.etd).data}</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
<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>
|
||||||
{/if}
|
{/if}
|
||||||
{#if service.cancelReason}
|
{#if service.cancelReason}
|
||||||
<p class="service-detail">{service.cancelReason}</p>
|
<p class="service-detail">{service.cancelReason}</p>
|
||||||
{/if}
|
{/if}
|
||||||
</td></tr
|
</td></tr
|
||||||
>
|
>
|
||||||
{/each}
|
{/each}
|
||||||
</table>
|
</table>
|
||||||
{/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>
|
||||||
<th class="from">From</th>
|
<th class="from">From</th>
|
||||||
<th class="to">To</th>
|
<th class="to">To</th>
|
||||||
<th class="time">Sch Arr.</th>
|
<th class="time">Sch Arr.</th>
|
||||||
<th class="time">Exp Arr.</th>
|
<th class="time">Exp Arr.</th>
|
||||||
<th class="time">Sch Dep.</th>
|
<th class="time">Sch Dep.</th>
|
||||||
<th class="time">Exp Dep.</th>
|
<th class="time">Exp Dep.</th>
|
||||||
</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>
|
||||||
<td class="time {parseTime(service.etd).changed}">{parseTime(service.etd).data}</td>
|
<td class="time {parseTime(service.etd).changed}">{parseTime(service.etd).data}</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
<tr
|
<tr
|
||||||
><td colspan="7">
|
><td colspan="7">
|
||||||
{#if service.delayReason}
|
{#if service.delayReason}
|
||||||
<p class="service-detail">{service.delayReason}</p>
|
<p class="service-detail">{service.delayReason}</p>
|
||||||
{/if}
|
{/if}
|
||||||
{#if service.cancelReason}
|
{#if service.cancelReason}
|
||||||
<p class="service-detail">{service.cancelReason}</p>
|
<p class="service-detail">{service.cancelReason}</p>
|
||||||
{/if}
|
{/if}
|
||||||
</td></tr
|
</td></tr
|
||||||
>
|
>
|
||||||
{/each}
|
{/each}
|
||||||
</table>
|
</table>
|
||||||
{/if}
|
{/if}
|
||||||
{:else}
|
{:else}
|
||||||
<Island>
|
<Island>
|
||||||
<p style="font-weight:600">Unable to load data</p>
|
<p style="font-weight:600">Unable to load data</p>
|
||||||
</Island>
|
</Island>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if serviceDetail}
|
{#if serviceDetail}
|
||||||
<OverlayIsland>
|
<OverlayIsland>
|
||||||
<div id="detailBox">
|
<div id="detailBox">
|
||||||
<h6>Service Detail</h6>
|
<h6>Service Detail</h6>
|
||||||
<button type="button" id="closeService" on:click={closeService}>X</button>
|
<button type="button" id="closeService" on:click={closeService}>X</button>
|
||||||
<table id="detailTable">
|
<table id="detailTable">
|
||||||
<tr>
|
<tr>
|
||||||
<th>Location</th>
|
<th>Location</th>
|
||||||
<th>Sch</th>
|
<th>Sch</th>
|
||||||
<th>Exp</th>
|
<th>Exp</th>
|
||||||
</tr>
|
</tr>
|
||||||
{#if serviceDetail?.previousCallingPoints?.callingPointList?.callingPoint}
|
{#if serviceDetail?.previousCallingPoints?.callingPointList?.callingPoint}
|
||||||
{#if Array.isArray(serviceDetail?.previousCallingPoints?.callingPointList?.callingPoint)}
|
{#if Array.isArray(serviceDetail?.previousCallingPoints?.callingPointList?.callingPoint)}
|
||||||
{#each serviceDetail.previousCallingPoints.callingPointList.callingPoint as prevPoint}
|
{#each serviceDetail.previousCallingPoints.callingPointList.callingPoint as prevPoint}
|
||||||
<tr>
|
<tr>
|
||||||
<td>{prevPoint.locationName}</td>
|
<td>{prevPoint.locationName}</td>
|
||||||
<td>{prevPoint.st}</td>
|
<td>{prevPoint.st}</td>
|
||||||
<td class="time {parseTime(prevPoint.at || prevPoint.et).changed}">{parseTime(prevPoint.at || prevPoint.et).data}</td>
|
<td class="time {parseTime(prevPoint.at || prevPoint.et).changed}">{parseTime(prevPoint.at || prevPoint.et).data}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{/each}
|
{/each}
|
||||||
{:else}
|
{:else}
|
||||||
<tr>
|
<tr>
|
||||||
<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(
|
||||||
</tr>
|
serviceDetail.previousCallingPoints.callingPointList.callingPoint.at || serviceDetail.previousCallingPoints.callingPointList.callingPoint.et
|
||||||
{/if}
|
).data}</td
|
||||||
{/if}
|
>
|
||||||
<tr class="thisStop">
|
</tr>
|
||||||
<td>{title}</td>
|
{/if}
|
||||||
<td>{serviceDetail.std || serviceDetail.sta}</td>
|
{/if}
|
||||||
<td class="time {parseTime(serviceDetail.etd || serviceDetail.eta).changed}">{parseTime(serviceDetail.etd || serviceDetail.eta).data}</td>
|
<tr class="thisStop">
|
||||||
</tr>
|
<td>{title}</td>
|
||||||
{#if serviceDetail?.subsequentCallingPoints?.callingPointList?.callingPoint}
|
<td>{serviceDetail.std || serviceDetail.sta}</td>
|
||||||
{#if Array.isArray(serviceDetail?.subsequentCallingPoints?.callingPointList?.callingPoint)}
|
<td class="time {parseTime(serviceDetail.etd || serviceDetail.eta).changed}">{parseTime(serviceDetail.etd || serviceDetail.eta).data}</td>
|
||||||
{#each serviceDetail.subsequentCallingPoints.callingPointList.callingPoint as nextPoint}
|
</tr>
|
||||||
<tr>
|
{#if serviceDetail?.subsequentCallingPoints?.callingPointList?.callingPoint}
|
||||||
<td>{nextPoint.locationName}</td>
|
{#if Array.isArray(serviceDetail?.subsequentCallingPoints?.callingPointList?.callingPoint)}
|
||||||
<td>{nextPoint.st}</td>
|
{#each serviceDetail.subsequentCallingPoints.callingPointList.callingPoint as nextPoint}
|
||||||
<td class="time {parseTime(nextPoint.et).changed}">{parseTime(nextPoint.et).data}</td>
|
<tr>
|
||||||
</tr>
|
<td>{nextPoint.locationName}</td>
|
||||||
{/each}
|
<td>{nextPoint.st}</td>
|
||||||
{:else}
|
<td class="time {parseTime(nextPoint.et).changed}">{parseTime(nextPoint.et).data}</td>
|
||||||
<tr class="detailRow">
|
</tr>
|
||||||
<td>{serviceDetail.subsequentCallingPoints.callingPointList.callingPoint.locationName}</td>
|
{/each}
|
||||||
<td>{serviceDetail.subsequentCallingPoints.callingPointList.callingPoint.st}</td>
|
{:else}
|
||||||
<td class="time {parseTime(serviceDetail.subsequentCallingPoints.callingPointList.callingPoint.et).changed}"
|
<tr class="detailRow">
|
||||||
>{parseTime(serviceDetail.subsequentCallingPoints.callingPointList.callingPoint.et).data}</td
|
<td>{serviceDetail.subsequentCallingPoints.callingPointList.callingPoint.locationName}</td>
|
||||||
>
|
<td>{serviceDetail.subsequentCallingPoints.callingPointList.callingPoint.st}</td>
|
||||||
</tr>
|
<td class="time {parseTime(serviceDetail.subsequentCallingPoints.callingPointList.callingPoint.et).changed}"
|
||||||
{/if}
|
>{parseTime(serviceDetail.subsequentCallingPoints.callingPointList.callingPoint.et).data}</td
|
||||||
{/if}
|
>
|
||||||
</table>
|
</tr>
|
||||||
</div>
|
{/if}
|
||||||
</OverlayIsland>
|
{/if}
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</OverlayIsland>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
#timestamp {
|
.ldbTable {
|
||||||
margin: auto;
|
width: 100%;
|
||||||
text-align: left;
|
min-width: 300px;
|
||||||
font-size: 14px;
|
margin: auto;
|
||||||
}
|
padding-right: 2px;
|
||||||
.ldbTable {
|
padding-left: 0px;
|
||||||
width: 100%;
|
color: white;
|
||||||
min-width: 300px;
|
font-size: 13px;
|
||||||
margin: auto;
|
|
||||||
padding-right: 2px;
|
|
||||||
padding-left: 0px;
|
|
||||||
color: white;
|
|
||||||
font-size: 13px;
|
|
||||||
}
|
|
||||||
.service-detail {
|
|
||||||
color: cyan;
|
|
||||||
text-align: left;
|
|
||||||
font-size: 12px;
|
|
||||||
padding: 0;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
.transport-mode {
|
|
||||||
width: 30px;
|
|
||||||
}
|
|
||||||
.table-head-text {
|
|
||||||
color: white;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
@media (min-width: 800px) {
|
|
||||||
table {
|
|
||||||
font-size: 15px;
|
|
||||||
max-width: 850px;
|
|
||||||
}
|
}
|
||||||
.service-detail {
|
.service-detail {
|
||||||
font-size: 14px;
|
color: cyan;
|
||||||
|
text-align: left;
|
||||||
|
font-size: 12px;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
}
|
}
|
||||||
.transport-mode {
|
.transport-mode {
|
||||||
width: 50px;
|
width: 30px;
|
||||||
}
|
|
||||||
#timestamp {
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@media (min-width: 1000px) {
|
|
||||||
table {
|
|
||||||
font-size: 17px;
|
|
||||||
}
|
|
||||||
.service-detail {
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
}
|
||||||
.table-head-text {
|
.table-head-text {
|
||||||
font-size: 16px;
|
color: white;
|
||||||
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
}
|
@media (min-width: 800px) {
|
||||||
@media (min-width: 1600px) {
|
table {
|
||||||
table {
|
font-size: 15px;
|
||||||
font-size: 19px;
|
max-width: 850px;
|
||||||
|
}
|
||||||
|
.service-detail {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
.transport-mode {
|
||||||
|
width: 50px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.service-detail {
|
@media (min-width: 1000px) {
|
||||||
font-size: 18px;
|
table {
|
||||||
|
font-size: 17px;
|
||||||
|
}
|
||||||
|
.service-detail {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
.table-head-text {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media (min-width: 1600px) {
|
||||||
|
table {
|
||||||
|
font-size: 19px;
|
||||||
|
}
|
||||||
|
.service-detail {
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.origdest {
|
||||||
|
color: yellow;
|
||||||
|
}
|
||||||
|
.from {
|
||||||
|
width: 25%;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
.to {
|
||||||
|
width: 25%;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
.plat {
|
||||||
|
width: 10%;
|
||||||
|
}
|
||||||
|
.time {
|
||||||
|
width: 10%;
|
||||||
|
}
|
||||||
|
.changed {
|
||||||
|
animation: pulse-change 1.5s linear infinite;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
.origdest {
|
|
||||||
color: yellow;
|
|
||||||
}
|
|
||||||
.from {
|
|
||||||
width: 25%;
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
.to {
|
|
||||||
width: 25%;
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
.plat {
|
|
||||||
width: 10%;
|
|
||||||
}
|
|
||||||
.time {
|
|
||||||
width: 10%;
|
|
||||||
}
|
|
||||||
.changed {
|
|
||||||
animation: pulse-change 1.5s linear infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cancelled {
|
.cancelled {
|
||||||
animation: pulse-cancel 1.5s linear infinite;
|
animation: pulse-cancel 1.5s linear infinite;
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes pulse-change {
|
|
||||||
50% {
|
|
||||||
color: var(--main-warning-color);
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes pulse-cancel {
|
@keyframes pulse-change {
|
||||||
50% {
|
50% {
|
||||||
color: var(--main-alert-color);
|
color: var(--main-warning-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse-cancel {
|
||||||
|
50% {
|
||||||
|
color: var(--main-alert-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#detailBox {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
h6 {
|
||||||
|
position: absolute;
|
||||||
|
top: -25px;
|
||||||
|
left: 20px;
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
#closeService {
|
||||||
|
position: absolute;
|
||||||
|
top: 10px;
|
||||||
|
right: 10px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 60px;
|
||||||
|
width: 35px;
|
||||||
|
height: 35px;
|
||||||
|
background-color: var(--main-bg-color);
|
||||||
|
color: white;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
#detailTable {
|
||||||
|
margin-top: 40px;
|
||||||
|
color: white;
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
.thisStop {
|
||||||
|
color: yellow;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
#detailBox {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
h6 {
|
|
||||||
position: absolute;
|
|
||||||
top: -25px;
|
|
||||||
left: 20px;
|
|
||||||
font-size: 18px;
|
|
||||||
}
|
|
||||||
#closeService {
|
|
||||||
position: absolute;
|
|
||||||
top: 10px;
|
|
||||||
right: 10px;
|
|
||||||
border: none;
|
|
||||||
border-radius: 60px;
|
|
||||||
width: 35px;
|
|
||||||
height: 35px;
|
|
||||||
background-color: var(--main-bg-color);
|
|
||||||
color: white;
|
|
||||||
font-weight: 700;
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
|
||||||
#detailTable {
|
|
||||||
margin-top: 40px;
|
|
||||||
color: white;
|
|
||||||
font-size: 15px;
|
|
||||||
}
|
|
||||||
.thisStop {
|
|
||||||
color: yellow;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
39
src/lib/ldb/staff/fetch.ts
Normal file
39
src/lib/ldb/staff/fetch.ts
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
// Fetches StaffLDB Data, correctly formats DATE fields and returns the data
|
||||||
|
|
||||||
|
import { getApiUrl } from "$lib/scripts/upstream";
|
||||||
|
import { uuid } from "$lib/stores/uuid";
|
||||||
|
import type { ApiResponse, StaffLdb } from "@owlboard/ts-types";
|
||||||
|
|
||||||
|
// Fetch StaffLDB Data, and returns the data after hydration (convert date types etc.)
|
||||||
|
export async function fetchStaffLdb(station: string): Promise<ApiResponse<StaffLdb>> {
|
||||||
|
const url = `${getApiUrl()}/api/v2/live/station/${station}/staff`;
|
||||||
|
|
||||||
|
let uuid_value: string = "";
|
||||||
|
const unsubscribe = uuid.subscribe((value) => {
|
||||||
|
uuid_value = value;
|
||||||
|
});
|
||||||
|
|
||||||
|
const fetchOpts = {
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
uuid: uuid_value,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const res = await fetch(url, fetchOpts);
|
||||||
|
unsubscribe();
|
||||||
|
const resJs = await res.json();
|
||||||
|
return parseFormat(JSON.stringify(resJs));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse dates within the JSON response
|
||||||
|
function parseFormat(jsonString: any): ApiResponse<StaffLdb> {
|
||||||
|
return JSON.parse(jsonString, (key, value) => {
|
||||||
|
if (typeof value === "string") {
|
||||||
|
const dateRegex = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z$/;
|
||||||
|
if (dateRegex.test(value)) {
|
||||||
|
return new Date(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
});
|
||||||
|
}
|
@ -1,135 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
export let station = '';
|
|
||||||
export let title = 'Loading...';
|
|
||||||
import { onMount } from 'svelte';
|
|
||||||
import AlertBar from '$lib/ldb/nrcc/alert-bar.svelte';
|
|
||||||
import StaffTrainDetail from '$lib/ldb/staff/train-detail.svelte';
|
|
||||||
import Loading from '$lib/navigation/loading.svelte';
|
|
||||||
import { uuid } from '$lib/stores/uuid';
|
|
||||||
import Island from '$lib/islands/island.svelte';
|
|
||||||
import TableGeneratorDev from './table/table-generator_dev.svelte';
|
|
||||||
|
|
||||||
const TableGenerator = TableGeneratorDev;
|
|
||||||
|
|
||||||
import type { StaffLdb, NrccMessage, TrainServices, ApiResponse } from '@owlboard/ts-types';
|
|
||||||
|
|
||||||
let jsonData: ApiResponse<StaffLdb>;
|
|
||||||
let isLoading = true;
|
|
||||||
let isErr = false;
|
|
||||||
let errMsg: string;
|
|
||||||
let alerts: NrccMessage[];
|
|
||||||
let detail = { show: false, rid: '', uid: '', headcode: '' };
|
|
||||||
|
|
||||||
$: {
|
|
||||||
if (isLoading) {
|
|
||||||
title = 'Loading...';
|
|
||||||
} else {
|
|
||||||
title = station;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function fetchData() {
|
|
||||||
isLoading = true; // Set loading state
|
|
||||||
try {
|
|
||||||
console.log(`Requested Station: ${requestedStation}`);
|
|
||||||
const url = `https://owlboard.info/api/v2/live/station/${requestedStation}/staff`;
|
|
||||||
const opt = {
|
|
||||||
method: 'GET',
|
|
||||||
headers: {
|
|
||||||
uuid: $uuid
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const data = await fetch(url, opt);
|
|
||||||
const json = await data.json();
|
|
||||||
if (json.ERROR === 'NOT_FOUND') {
|
|
||||||
isErr = true;
|
|
||||||
errMsg = 'Unable to find this station';
|
|
||||||
} else {
|
|
||||||
jsonData = json;
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error fetching data:', error);
|
|
||||||
isLoading = false;
|
|
||||||
isErr = true;
|
|
||||||
errMsg = 'Connection error, try again later';
|
|
||||||
} finally {
|
|
||||||
isLoading = false; // Clear loading state
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function showDetail(rid = '', uid = '', tid = '') {
|
|
||||||
detail = {
|
|
||||||
rid: rid,
|
|
||||||
uid: uid,
|
|
||||||
headcode: tid,
|
|
||||||
show: true
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function hideDetails() {
|
|
||||||
detail = {
|
|
||||||
rid: '',
|
|
||||||
uid: '',
|
|
||||||
headcode: '',
|
|
||||||
show: false
|
|
||||||
};
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
{#key detail}
|
|
||||||
{#if detail.show}
|
|
||||||
<StaffTrainDetail {detail} close={hideDetails} />
|
|
||||||
{/if}
|
|
||||||
{/key}
|
|
||||||
|
|
||||||
{#if isLoading}
|
|
||||||
<Loading />
|
|
||||||
{:else if isErr}
|
|
||||||
<Island>
|
|
||||||
<p><strong>{errMsg}</strong></p>
|
|
||||||
</Island>
|
|
||||||
{:else}
|
|
||||||
{#if alerts.length}
|
|
||||||
<AlertBar {alerts} />
|
|
||||||
{/if}
|
|
||||||
<p class="dataTime">Data from: {dataAge.toLocaleString([])}</p>
|
|
||||||
{#if trainServices && trainServices.length}
|
|
||||||
<TableGenerator services={trainServices} click={showDetail} />
|
|
||||||
{:else}
|
|
||||||
<p id="noservices">There are no scheduled train services in the next two hours</p>
|
|
||||||
{/if}
|
|
||||||
{#if busServices && busServices.length}
|
|
||||||
<img class="transport-mode-image" src="/images/transport-modes/bus.svg" alt="" />
|
|
||||||
<br />
|
|
||||||
<span class="transport-mode-text">Bus Services</span>
|
|
||||||
<TableGenerator services={busServices} click={showDetail} />
|
|
||||||
{/if}
|
|
||||||
{#if ferryServices && ferryServices.length}
|
|
||||||
<img class="transport-mode-image" src="/images/transport-modes/ferry.svg" alt="" />
|
|
||||||
<br />
|
|
||||||
<span class="transport-mode-text">Ferry Services</span>
|
|
||||||
<TableGenerator services={ferryServices} click={showDetail} />
|
|
||||||
{/if}
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<style>
|
|
||||||
p.dataTime {
|
|
||||||
margin-top: 5px;
|
|
||||||
margin-bottom: 0px;
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
#noservices {
|
|
||||||
margin: 20px;
|
|
||||||
padding-top: 20px;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.transport-mode-image {
|
|
||||||
width: 30px;
|
|
||||||
margin: auto;
|
|
||||||
padding-top: 25px;
|
|
||||||
}
|
|
||||||
.transport-mode-text {
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
</style>
|
|
@ -1,220 +1,102 @@
|
|||||||
<script>
|
<script lang="ts">
|
||||||
export let station = '';
|
import TableGenerator from "./table/table-generator.svelte";
|
||||||
export let title = 'Loading...';
|
import Loading from "$lib/navigation/loading.svelte";
|
||||||
import { onMount } from 'svelte';
|
import type { StaffLdb } from "@owlboard/ts-types";
|
||||||
import AlertBar from '$lib/ldb/nrcc/alert-bar.svelte';
|
import { detailInit, defineDetail } from "./train-detail";
|
||||||
import StaffTrainDetail from '$lib/ldb/staff/train-detail.svelte';
|
import TrainDetail from "./train-detail.svelte";
|
||||||
import Loading from '$lib/navigation/loading.svelte';
|
import { fetchStaffLdb } from "./fetch";
|
||||||
import { uuid } from '$lib/stores/uuid';
|
import AlertBar from "../common/nrcc/alert-bar.svelte";
|
||||||
import Island from '$lib/islands/island.svelte';
|
import TimeBar from "$lib/navigation/TimeBar.svelte";
|
||||||
import TableGenerator from './table/table-generator.svelte';
|
import { onMount } from "svelte";
|
||||||
|
import { IconBus, IconSailboat } from "@tabler/icons-svelte";
|
||||||
|
|
||||||
let requestedStation = '';
|
export let station: string;
|
||||||
$: requestedStation = station;
|
export let title: string | undefined = "Loading...";
|
||||||
|
|
||||||
let jsonData = {};
|
let errorDetail = {
|
||||||
/**
|
code: "",
|
||||||
* @type {string | any[]}
|
message: "",
|
||||||
*/
|
};
|
||||||
let trainServices = [];
|
let nrcc: string[] = [];
|
||||||
/**
|
|
||||||
* @type {string | any[]}
|
|
||||||
*/
|
|
||||||
let busServices = [];
|
|
||||||
/**
|
|
||||||
* @type {string | any[]}
|
|
||||||
*/
|
|
||||||
let ferryServices = [];
|
|
||||||
let dataAge = new Date(0);
|
|
||||||
let isLoading = true;
|
|
||||||
let isErr = false;
|
|
||||||
let errMsg = '';
|
|
||||||
let alerts = [''];
|
|
||||||
let detail = { show: false, rid: '', uid: '', headcode: '' };
|
|
||||||
|
|
||||||
$: {
|
let detail = detailInit();
|
||||||
// @ts-ignore
|
function hideDetail() {
|
||||||
if (jsonData?.GetBoardResult?.generatedAt) {
|
detail = detailInit();
|
||||||
// @ts-ignore
|
}
|
||||||
dataAge = new Date(jsonData.GetBoardResult.generatedAt);
|
function showDetail(rid: string, uid: string, tid: string) {
|
||||||
|
detail = defineDetail(rid, uid, tid);
|
||||||
}
|
}
|
||||||
|
|
||||||
// @ts-ignore
|
console.log(`Station: ${station}`);
|
||||||
if (jsonData?.GetBoardResult?.trainServices?.service) {
|
|
||||||
// @ts-ignore
|
|
||||||
trainServices = ensureArray((trainServices = jsonData.GetBoardResult.trainServices.service));
|
|
||||||
} else {
|
|
||||||
trainServices = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
// @ts-ignore
|
let updatedTime: Date;
|
||||||
if (jsonData?.GetBoardResult?.busServices?.service) {
|
|
||||||
// @ts-ignore
|
|
||||||
busServices = ensureArray((busServices = jsonData.GetBoardResult.busServices.service));
|
|
||||||
} else {
|
|
||||||
busServices = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
// @ts-ignore
|
async function callFetch(station: string): Promise<StaffLdb> {
|
||||||
if (jsonData?.GetBoardResult?.ferryServices?.service) {
|
console.log("callFetch function called");
|
||||||
// @ts-ignore
|
const data = await fetchStaffLdb(station);
|
||||||
ensureArray((ferryServices = jsonData.GetBoardResult.ferryServices.service));
|
if (data.data) {
|
||||||
} else {
|
title = data.data.locationName;
|
||||||
ferryServices = [];
|
if (data.data?.nrccMessages) {
|
||||||
}
|
for (const msg of data.data.nrccMessages) {
|
||||||
|
nrcc.push(msg.xhtmlMessage);
|
||||||
// @ts-ignore
|
}
|
||||||
if (jsonData?.GetBoardResult?.locationName) {
|
nrcc = nrcc; // Reassign to ensure Svelte reloads
|
||||||
// @ts-ignore
|
}
|
||||||
title = jsonData.GetBoardResult.locationName;
|
if (data.data.generatedAt) {
|
||||||
} else {
|
updatedTime = new Date(data.data.generatedAt);
|
||||||
title = 'Loading Board';
|
}
|
||||||
}
|
return data.data;
|
||||||
|
|
||||||
// @ts-ignore
|
|
||||||
if (jsonData?.GetBoardResult?.nrccMessages) {
|
|
||||||
// @ts-ignore
|
|
||||||
alerts = processNrcc(jsonData.GetBoardResult?.nrccMessages?.message);
|
|
||||||
} else {
|
|
||||||
alerts = [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {any} item
|
|
||||||
*/
|
|
||||||
function ensureArray(item) {
|
|
||||||
if (Array.isArray(item)) {
|
|
||||||
return item;
|
|
||||||
}
|
|
||||||
return [item];
|
|
||||||
}
|
|
||||||
|
|
||||||
async function fetchData() {
|
|
||||||
isLoading = true; // Set loading state
|
|
||||||
try {
|
|
||||||
console.log(`Requested Station: ${requestedStation}`);
|
|
||||||
const url = `https://owlboard.info/api/v2/live/station/${requestedStation}/staff`;
|
|
||||||
const opt = {
|
|
||||||
method: 'GET',
|
|
||||||
headers: {
|
|
||||||
uuid: $uuid
|
|
||||||
}
|
}
|
||||||
};
|
errorDetail.code = data.obStatus.toString() || "UNKNOWN";
|
||||||
const data = await fetch(url, opt);
|
errorDetail.message = data.obMsg || "An unknown error occoured";
|
||||||
const json = await data.json();
|
throw new Error("Unable to Fetch Data");
|
||||||
if (json.ERROR === 'NOT_FOUND') {
|
|
||||||
isErr = true;
|
|
||||||
errMsg = 'Unable to find this station';
|
|
||||||
} else {
|
|
||||||
jsonData = json;
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error fetching data:', error);
|
|
||||||
isLoading = false;
|
|
||||||
isErr = true;
|
|
||||||
errMsg = 'Connection error, try again later';
|
|
||||||
} finally {
|
|
||||||
isLoading = false; // Clear loading state
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
onMount(async () => {
|
||||||
* @param {any} messages
|
console.log("staff-ldb component mounted");
|
||||||
*/
|
});
|
||||||
function processNrcc(messages) {
|
|
||||||
// Remove newlines and then <p> tags from input and append to array
|
|
||||||
let arrMessages;
|
|
||||||
if (!Array.isArray(messages)) {
|
|
||||||
arrMessages = [messages];
|
|
||||||
} else {
|
|
||||||
arrMessages = messages;
|
|
||||||
}
|
|
||||||
let processedMessages = [];
|
|
||||||
for (const message of arrMessages) {
|
|
||||||
const msgText = message.xhtmlMessage;
|
|
||||||
processedMessages.push(msgText.replace(/[\n\r]/g, '').replace(/<\/?p[^>]*>/g, ''));
|
|
||||||
}
|
|
||||||
return processedMessages;
|
|
||||||
}
|
|
||||||
|
|
||||||
function showDetail(rid = '', uid = '', tid = '') {
|
|
||||||
detail = {
|
|
||||||
rid: rid,
|
|
||||||
uid: uid,
|
|
||||||
headcode: tid,
|
|
||||||
show: true
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function hideDetails() {
|
|
||||||
detail = {
|
|
||||||
rid: '',
|
|
||||||
uid: '',
|
|
||||||
headcode: '',
|
|
||||||
show: false
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
onMount(() => {
|
|
||||||
fetchData();
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#key detail}
|
{#if nrcc.length}
|
||||||
{#if detail.show}
|
<AlertBar alerts={nrcc} />
|
||||||
<StaffTrainDetail {detail} close={hideDetails} />
|
|
||||||
{/if}
|
|
||||||
{/key}
|
|
||||||
|
|
||||||
{#if isLoading}
|
|
||||||
<Loading />
|
|
||||||
{:else if isErr}
|
|
||||||
<Island>
|
|
||||||
<p><strong>{errMsg}</strong></p>
|
|
||||||
</Island>
|
|
||||||
{:else}
|
|
||||||
{#if alerts.length}
|
|
||||||
<AlertBar {alerts} />
|
|
||||||
{/if}
|
|
||||||
<p class="dataTime">Data from: {dataAge.toLocaleString([])}</p>
|
|
||||||
{#if trainServices && trainServices.length}
|
|
||||||
<TableGenerator services={trainServices} click={showDetail} />
|
|
||||||
{:else}
|
|
||||||
<p id="noservices">There are no scheduled train services in the next two hours</p>
|
|
||||||
{/if}
|
|
||||||
{#if busServices && busServices.length}
|
|
||||||
<img class="transport-mode-image" src="/images/transport-modes/bus.svg" alt="" />
|
|
||||||
<br />
|
|
||||||
<span class="transport-mode-text">Bus Services</span>
|
|
||||||
<TableGenerator services={busServices} click={showDetail} />
|
|
||||||
{/if}
|
|
||||||
{#if ferryServices && ferryServices.length}
|
|
||||||
<img class="transport-mode-image" src="/images/transport-modes/ferry.svg" alt="" />
|
|
||||||
<br />
|
|
||||||
<span class="transport-mode-text">Ferry Services</span>
|
|
||||||
<TableGenerator services={ferryServices} click={showDetail} />
|
|
||||||
{/if}
|
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<style>
|
<TimeBar bind:updatedTime />
|
||||||
p.dataTime {
|
|
||||||
margin-top: 5px;
|
|
||||||
margin-bottom: 0px;
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
#noservices {
|
|
||||||
margin: 20px;
|
|
||||||
padding-top: 20px;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.transport-mode-image {
|
{#key detail}
|
||||||
width: 30px;
|
{#if detail.show}
|
||||||
margin: auto;
|
<TrainDetail {detail} close={hideDetail} />
|
||||||
padding-top: 25px;
|
{/if}
|
||||||
}
|
{/key}
|
||||||
.transport-mode-text {
|
|
||||||
color: white;
|
{#await callFetch(station)}
|
||||||
}
|
<Loading />
|
||||||
|
{:then data}
|
||||||
|
{#if data}
|
||||||
|
{#if data.trainServices?.length}
|
||||||
|
<TableGenerator services={data.trainServices} click={showDetail} />
|
||||||
|
{/if}
|
||||||
|
{#if data.busServices?.length}
|
||||||
|
<IconBus />
|
||||||
|
<br />
|
||||||
|
<span class="table-head-text">Bus Services</span>
|
||||||
|
<TableGenerator services={data.busServices} click={showDetail} />
|
||||||
|
{/if}
|
||||||
|
{#if data.ferryServices?.length}
|
||||||
|
<IconSailboat />
|
||||||
|
<br>
|
||||||
|
<span class="table-head-text">Ferry Services</span>
|
||||||
|
<TableGenerator services={data.ferryServices} click={showDetail} />
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
|
{:catch}
|
||||||
|
<h2>Error</h2>
|
||||||
|
<p>ERR-CODE: {errorDetail.code}</p>
|
||||||
|
<p>Message:<br />{errorDetail.message}</p>
|
||||||
|
{/await}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.table-head-text {
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -1,485 +1,320 @@
|
|||||||
<script>
|
<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";
|
||||||
|
|
||||||
export let services;
|
import type { TrainServices, ServiceLocation } from "@owlboard/ts-types";
|
||||||
export let click;
|
import { fade } from "svelte/transition";
|
||||||
|
|
||||||
async function generateServiceData(service) {
|
export let services: TrainServices[];
|
||||||
const timeDetails = parseTimes(service);
|
export let click: Function;
|
||||||
let serviceData = {
|
|
||||||
from: await parseLocation(service.origin),
|
|
||||||
to: await parseLocation(service.destination),
|
|
||||||
length: await getTrainLength(service),
|
|
||||||
platform: await parsePlatform(service?.platform || 'undefined'),
|
|
||||||
platformHidden: service?.platformIsHidden === 'true',
|
|
||||||
schArr: timeDetails.schArr,
|
|
||||||
expArr: timeDetails.expArr,
|
|
||||||
schDep: timeDetails.schDep,
|
|
||||||
expDep: timeDetails.expDep,
|
|
||||||
isEarlyArr: timeDetails.earArr,
|
|
||||||
isLateArr: timeDetails.delArr,
|
|
||||||
isEarlyDep: timeDetails.earDep,
|
|
||||||
isLateDep: timeDetails.delDep,
|
|
||||||
isCancelled: Boolean(service?.isCancelled),
|
|
||||||
canArr: timeDetails.canArr,
|
|
||||||
canDep: timeDetails.canDep,
|
|
||||||
isDelayed: service?.arrivalType === 'Delayed',
|
|
||||||
isArrDelayed: service?.arrivalType === 'Delayed',
|
|
||||||
isDepDelayed: service?.departureType === 'Delayed',
|
|
||||||
isNonPublic: service?.isPassengerService === 'false' ? true : false
|
|
||||||
};
|
|
||||||
return serviceData;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getTrainLength(service) {
|
function detail(event: any, rid: string, uid: string, tid: string) {
|
||||||
if (service?.length) {
|
const target = event.target;
|
||||||
return parseInt(service?.length);
|
click(rid, uid, tid);
|
||||||
} else if (service?.formation?.coaches) {
|
|
||||||
return service.formation.coaches.coach.length;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function parseLocation(location) {
|
|
||||||
if (!Array.isArray(location.location)) {
|
|
||||||
return location.location?.tiploc;
|
|
||||||
}
|
|
||||||
let locations = [];
|
|
||||||
for (const singleLocation of location?.location) {
|
|
||||||
locations.push(singleLocation?.tiploc);
|
|
||||||
}
|
|
||||||
return locations.join(' & ');
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseTimes(service) {
|
|
||||||
let schArr = new Date(service?.sta);
|
|
||||||
let expArr = new Date(service?.eta || service?.ata);
|
|
||||||
let schDep = new Date(service?.std);
|
|
||||||
let expDep = new Date(service?.etd || service?.atd);
|
|
||||||
let isEarlyArr = false,
|
|
||||||
isDelayedArr = false,
|
|
||||||
isArr = false,
|
|
||||||
canArr = false;
|
|
||||||
let isEarlyDep = false,
|
|
||||||
isDelayedDep = false,
|
|
||||||
isDep = false,
|
|
||||||
canDep = false;
|
|
||||||
const timeDifferenceThreshold = 60 * 1000; // 60 seconds in milliseconds
|
|
||||||
if (expArr - schArr < -timeDifferenceThreshold) {
|
|
||||||
isEarlyArr = true;
|
|
||||||
isArr = true;
|
|
||||||
} else if (expArr - schArr > timeDifferenceThreshold) {
|
|
||||||
isDelayedArr = true;
|
|
||||||
isArr = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (expDep - schDep < -timeDifferenceThreshold) {
|
async function formatLocations(locations: ServiceLocation[]): Promise<string> {
|
||||||
isEarlyDep = true;
|
let tiplocs: string[] = [];
|
||||||
isDep = true;
|
for (const location of locations) {
|
||||||
} else if (expDep - schDep > timeDifferenceThreshold) {
|
tiplocs.push(location.tiploc);
|
||||||
isDelayedDep = true;
|
}
|
||||||
isDep = true;
|
return tiplocs.join(" & ");
|
||||||
}
|
|
||||||
let parsedExpArr;
|
|
||||||
if (expArr instanceof Date && !isNaN(expArr)) {
|
|
||||||
if (!isEarlyArr && !isDelayedArr) {
|
|
||||||
parsedExpArr = 'RT';
|
|
||||||
} else {
|
|
||||||
parsedExpArr = parseIndividualTime(expArr);
|
|
||||||
}
|
|
||||||
} else if (service.isCancelled === 'true') {
|
|
||||||
parsedExpArr = 'CANC';
|
|
||||||
canArr = true;
|
|
||||||
} else {
|
|
||||||
parsedExpArr = '-';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let parsedExpDep;
|
async function classGenerator(service: TrainServices) {
|
||||||
if (expDep instanceof Date && !isNaN(expDep)) {
|
// This function needs updating next
|
||||||
if (!isEarlyDep && !isDelayedDep) {
|
let otherArr: string[] = [];
|
||||||
parsedExpDep = 'RT';
|
let arrArr: string[] = [];
|
||||||
} else {
|
let depArr: string[] = [];
|
||||||
parsedExpDep = parseIndividualTime(expDep);
|
let platArr: string[] = [];
|
||||||
}
|
|
||||||
} else if (service.isCancelled === 'true') {
|
|
||||||
parsedExpDep = 'CANC';
|
|
||||||
canDep = true;
|
|
||||||
} else {
|
|
||||||
parsedExpDep = '-';
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
schArr: parseIndividualTime(schArr),
|
|
||||||
expArr: parsedExpArr,
|
|
||||||
schDep: parseIndividualTime(schDep),
|
|
||||||
expDep: parsedExpDep,
|
|
||||||
earArr: isEarlyArr,
|
|
||||||
delArr: isDelayedArr,
|
|
||||||
earDep: isEarlyDep,
|
|
||||||
delDep: isDelayedDep,
|
|
||||||
canArr: canArr,
|
|
||||||
canDep: canDep
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseIndividualTime(input) {
|
if (service.isCancelled) {
|
||||||
const dt = new Date(input);
|
otherArr.push("canc");
|
||||||
const output = dt.toLocaleTimeString([], {
|
}
|
||||||
hour: '2-digit',
|
if (service.serviceIsSupressed) {
|
||||||
minute: '2-digit'
|
otherArr.push("nonPass");
|
||||||
});
|
}
|
||||||
if (output !== 'Invalid Date') {
|
if (service.platformIsHidden) {
|
||||||
return output;
|
platArr.push("nonPass");
|
||||||
}
|
}
|
||||||
return '-';
|
|
||||||
}
|
|
||||||
|
|
||||||
async function parsePlatform(platform) {
|
function checkLateEarly(originalTime: Date | undefined, comparedTime: Date | undefined, arr: string[]) {
|
||||||
if (!platform) {
|
if (originalTime !== undefined && comparedTime instanceof Date) {
|
||||||
return '-';
|
if (originalTime < comparedTime) {
|
||||||
}
|
arr.push("late");
|
||||||
if (platform === 'TBC' || platform == 'undefined') {
|
} else if (originalTime > comparedTime) {
|
||||||
return '-';
|
arr.push("early");
|
||||||
}
|
}
|
||||||
return {
|
}
|
||||||
number: platform
|
}
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function detail(event, rid, uid, tid) {
|
checkLateEarly(service.sta, service.eta, arrArr);
|
||||||
const target = event.target;
|
checkLateEarly(service.sta, service.ata, arrArr);
|
||||||
click(rid, uid, tid);
|
checkLateEarly(service.std, service.etd, depArr);
|
||||||
}
|
checkLateEarly(service.std, service.atd, depArr);
|
||||||
|
|
||||||
|
return {
|
||||||
|
other: otherArr.join(" "),
|
||||||
|
arr: arrArr.join(" "),
|
||||||
|
dep: depArr.join(" "),
|
||||||
|
plat: platArr.join(" "),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function fmtTime(date: Date | string | undefined): string | false {
|
||||||
|
if (typeof date === "string") return date;
|
||||||
|
if (date instanceof Date) {
|
||||||
|
const hours = date.getHours().toString().padStart(2, "0");
|
||||||
|
const minutes = date.getMinutes().toString().padStart(2, "0");
|
||||||
|
return `${hours}:${minutes}`;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<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>
|
||||||
<table>
|
<p class="smallScreen">Try rotating your device</p>
|
||||||
<tr>
|
<table in:fade={{ duration: 500 }}>
|
||||||
<th class="id">ID</th>
|
<tr>
|
||||||
<th class="from">From</th>
|
<th class="id">ID</th>
|
||||||
<th class="to">To</th>
|
<th class="from">From</th>
|
||||||
<th class="plat">Plat</th>
|
<th class="to">To</th>
|
||||||
<th class="time">Sch</th>
|
<th class="plat">Plat</th>
|
||||||
<th class="time">Exp</th>
|
<th class="time arrsch">Sch</th>
|
||||||
<th class="time">Sch</th>
|
<th class="time arrexp">Exp</th>
|
||||||
<th class="time">Exp</th>
|
<th class="time depsch">Sch</th>
|
||||||
</tr>
|
<th class="time depexp">Exp</th>
|
||||||
<tr>
|
</tr>
|
||||||
<th class="other" colspan="4" />
|
<tr>
|
||||||
<th class="timepair" colspan="2">Arrival</th>
|
<th class="other" colspan="4" />
|
||||||
<th class="timepair" colspan="2">Departure</th>
|
<th class="timepair" colspan="2">Arrival</th>
|
||||||
</tr>
|
<th class="timepair" colspan="2">Departure</th>
|
||||||
{#each services as service}
|
</tr>
|
||||||
{#await generateServiceData(service)}
|
{#each services as service}
|
||||||
<tr><td colspan="8">Loading Service Data...</td></tr>
|
<tr
|
||||||
{:then serviceData}
|
class="dataRow"
|
||||||
<tr
|
on:click={(event) => detail(event, service.rid, service.uid, service.trainid)}
|
||||||
class="dataRow"
|
on:keypress={(event) => detail(event, service.rid, service.uid, service.trainid)}
|
||||||
on:click={(event) => detail(event, service.rid, service.uid, service.trainid)}
|
|
||||||
on:keypress={(event) => detail(event, service.rid, service.uid, service.trainid)}
|
|
||||||
>
|
|
||||||
<td class="id">{service.trainid}</td>
|
|
||||||
<td class="from {serviceData.isNonPublic && 'nonPass'} {serviceData.isCancelled && 'cancTxt'}">{serviceData.from}</td>
|
|
||||||
<td class="to {serviceData.isNonPublic && 'nonPass'} {serviceData.isCancelled && 'cancTxt'}">{serviceData.to}</td>
|
|
||||||
<td class="plat {serviceData.isNonPublic && 'nonPass'} {serviceData.isCancelled && 'cancTxt'} {serviceData.platformHidden && 'nonPass'}"
|
|
||||||
>{serviceData.platform.number || '-'}</td
|
|
||||||
>
|
>
|
||||||
<td class="time schTime {serviceData.isNonPublic && 'nonPass'} {serviceData.isCancelled && 'cancTxt'}">{serviceData.schArr}</td>
|
{#await classGenerator(service) then classes}
|
||||||
<td
|
<!-- HEADCODE -->
|
||||||
class="time {serviceData.isNonPublic && 'nonPass'} {serviceData.isLateArr && 'late'} {serviceData.isArrDelayed && 'late'} {serviceData.isCancelled &&
|
<td class="id">{service.trainid}</td>
|
||||||
'canc'} {serviceData.isEarlyArr && 'early'}">{serviceData.isArrDelayed ? 'LATE' : serviceData.expArr}</td
|
<!-- ORIGIN -->
|
||||||
>
|
<td class="loc from {classes.other}">{#await formatLocations(service.origin) then origin}{origin}{/await}</td>
|
||||||
<td class="time schTime {serviceData.isNonPublic && 'nonPass'} {serviceData.isCancelled && 'cancTxt'}">{serviceData.schDep}</td>
|
<!-- DESTINATION -->
|
||||||
<td
|
<td class="loc to {classes.other}">{#await formatLocations(service.destination) then dest}<span class="locName">{dest}</span>{/await}</td>
|
||||||
class="time {serviceData.isNonPublic && 'nonPass'} {serviceData.isLateDep && 'late'} {serviceData.isDepDelayed && 'late'}
|
<!-- PLATFORM -->
|
||||||
{serviceData.isCancelled && 'canc'} {serviceData.isEarlyDep && 'early'}">{serviceData.isDepDelayed ? 'LATE' : serviceData.expDep}</td
|
<td class="plat {classes.other} {classes.plat}">{service.platform || "-"}</td>
|
||||||
>
|
<!-- SCHEDULED ARR -->
|
||||||
</tr>
|
<td class="time schTime {classes.other}">{fmtTime(service?.sta) || "-"}</td>
|
||||||
<tr>
|
<!-- EXPECTED/ACTUAL ARR -->
|
||||||
<td class="tableTxt" colspan="8">
|
<td class="time {classes.other} {classes.arr}">{fmtTime(service.eta) || fmtTime(service.ata) || "-"}</td>
|
||||||
{tocMap.get(service.operatorCode.toLowerCase()) || service.operatorCode}
|
<!-- SCHEDULED DEP -->
|
||||||
{#if service.isCharter}charter{/if}
|
<td class="time schTime {classes.other}">{fmtTime(service.std) || "-"}</td>
|
||||||
{#if serviceData.length} | {serviceData.length} carriages{/if}
|
<!-- EXPECTED/ACTUAL DEP -->
|
||||||
{#if service.delayReason}
|
<td class="time {classes.other} {classes.dep}">{fmtTime(service.etd) || fmtTime(service.atd) || "-"}</td>
|
||||||
<br />
|
{/await}
|
||||||
<Reason type={'delay'} code={service.delayReason} />
|
</tr>
|
||||||
{/if}
|
<tr>
|
||||||
{#if service.cancelReason}
|
<td colspan="1" />
|
||||||
<br />
|
<td class="tableTxt" colspan="7">
|
||||||
<Reason type={'cancel'} code={service.cancelReason} />
|
{#if service.destination?.[0] && service.destination[0].via}<span class="via">{service.destination[0].via}</span><br />{/if}
|
||||||
{/if}
|
{tocMap.get(service.operatorCode.toLowerCase()) || service.operatorCode}
|
||||||
</td>
|
{#if service.length} | {service.length} carriages{/if}
|
||||||
</tr>
|
{#if service.delayReason}
|
||||||
{:catch}
|
<br />
|
||||||
<tr>
|
<span class="delayTxt">
|
||||||
<td colspan="8">Unable to display service</td>
|
<Reason type={"delay"} code={service.delayReason} />
|
||||||
</tr>
|
</span>
|
||||||
{/await}
|
{/if}
|
||||||
{/each}
|
{#if service.cancelReason}
|
||||||
|
<br />
|
||||||
|
<span class="cancTxt">
|
||||||
|
<Reason type={"cancel"} code={service.cancelReason} />
|
||||||
|
</span>
|
||||||
|
{/if}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{/each}
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
table {
|
|
||||||
table-layout: fixed;
|
|
||||||
width: 100%;
|
|
||||||
max-width: 875px;
|
|
||||||
margin: auto;
|
|
||||||
padding: 0px;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
th {
|
|
||||||
font-size: 12px;
|
|
||||||
margin: 0px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dataRow {
|
|
||||||
font-family: ubuntu, monospace;
|
|
||||||
vertical-align: bottom;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Table Columns */
|
|
||||||
.id {
|
|
||||||
width: 8%;
|
|
||||||
}
|
|
||||||
.from {
|
|
||||||
width: 14%;
|
|
||||||
}
|
|
||||||
.to {
|
|
||||||
width: 14%;
|
|
||||||
}
|
|
||||||
.plat {
|
|
||||||
width: 6%;
|
|
||||||
}
|
|
||||||
.time {
|
|
||||||
width: 9%;
|
|
||||||
}
|
|
||||||
|
|
||||||
td.id {
|
|
||||||
color: lightblue;
|
|
||||||
text-align: left;
|
|
||||||
padding-left: 2px;
|
|
||||||
}
|
|
||||||
td.from,
|
|
||||||
td.to {
|
|
||||||
color: yellow;
|
|
||||||
}
|
|
||||||
|
|
||||||
td.to,
|
|
||||||
th.to {
|
|
||||||
text-align: right;
|
|
||||||
}
|
|
||||||
td.from,
|
|
||||||
th.from {
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
td.time {
|
|
||||||
font-size: 15px;
|
|
||||||
vertical-align: middle;
|
|
||||||
}
|
|
||||||
.tableTxt {
|
|
||||||
text-align: left;
|
|
||||||
padding-left: 2px;
|
|
||||||
color: var(--second-text-color);
|
|
||||||
vertical-align: top;
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Handle small screens */
|
|
||||||
.smallScreen {
|
|
||||||
display: none;
|
|
||||||
margin: 20px;
|
|
||||||
}
|
|
||||||
@media screen and (max-width: 335px) {
|
|
||||||
th {
|
|
||||||
font-size: 10px;
|
|
||||||
}
|
|
||||||
.dataRow {
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
td.time {
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
.tableTxt {
|
|
||||||
font-size: 10px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@media screen and (max-width: 279px) {
|
|
||||||
table {
|
table {
|
||||||
display: none;
|
table-layout: fixed;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 875px;
|
||||||
|
margin: auto;
|
||||||
|
padding: 0px;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
th {
|
||||||
|
font-size: 12px;
|
||||||
|
margin: 0px;
|
||||||
}
|
}
|
||||||
.smallScreen {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Handle Large Screens */
|
|
||||||
@media screen and (min-width: 375px) {
|
|
||||||
.dataRow {
|
.dataRow {
|
||||||
font-size: 18px;
|
font-family: ubuntu, monospace;
|
||||||
|
vertical-align: top;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 1;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Table Columns */
|
||||||
|
.id {
|
||||||
|
width: 8%;
|
||||||
|
}
|
||||||
|
.from {
|
||||||
|
width: 14%;
|
||||||
|
}
|
||||||
|
.to {
|
||||||
|
width: 14%;
|
||||||
|
}
|
||||||
|
.plat {
|
||||||
|
width: 6%;
|
||||||
|
}
|
||||||
|
.time {
|
||||||
|
width: 9%;
|
||||||
|
}
|
||||||
|
|
||||||
|
td.id {
|
||||||
|
color: lightblue;
|
||||||
|
text-align: left;
|
||||||
|
padding-left: 2px;
|
||||||
|
}
|
||||||
|
td.from,
|
||||||
|
td.to {
|
||||||
|
color: yellow;
|
||||||
|
}
|
||||||
|
|
||||||
|
td.to {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
td.from,
|
||||||
|
th.from {
|
||||||
|
text-align: left;
|
||||||
}
|
}
|
||||||
td.time {
|
td.time {
|
||||||
font-size: 16px;
|
font-size: 15px;
|
||||||
}
|
vertical-align: top;
|
||||||
}
|
|
||||||
@media screen and (min-width: 450px) {
|
|
||||||
.dataRow {
|
|
||||||
font-size: 20px;
|
|
||||||
}
|
|
||||||
td.time {
|
|
||||||
font-size: 19px;
|
|
||||||
}
|
}
|
||||||
.tableTxt {
|
.tableTxt {
|
||||||
font-size: 14px;
|
text-align: left;
|
||||||
|
padding-left: 2px;
|
||||||
|
color: var(--secondary-text-color);
|
||||||
|
vertical-align: top;
|
||||||
|
font-size: 12px;
|
||||||
|
padding-bottom: 10px;
|
||||||
}
|
}
|
||||||
td.to,
|
.delayTxt {
|
||||||
td.from,
|
color: var(--main-warning-color);
|
||||||
th.to,
|
|
||||||
th.from {
|
|
||||||
text-align: center;
|
|
||||||
}
|
}
|
||||||
}
|
.cancTxt {
|
||||||
|
color: var(--main-alert-color);
|
||||||
/* Conditional Classes */
|
|
||||||
.cancTxt {
|
|
||||||
color: grey !important;
|
|
||||||
text-decoration: line-through;
|
|
||||||
opacity: 0.8;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nonPass {
|
|
||||||
opacity: 0.6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.late {
|
|
||||||
animation: pulse-late 1.5s linear infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
.canc {
|
|
||||||
animation: pulse-cancel 1.5s linear infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
.early {
|
|
||||||
animation: pulse-early 1.5s linear infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Animation Definitions */
|
|
||||||
@keyframes pulse-late {
|
|
||||||
50% {
|
|
||||||
color: var(--main-warning-color);
|
|
||||||
}
|
}
|
||||||
}
|
.via {
|
||||||
|
color: yellow;
|
||||||
@keyframes pulse-cancel {
|
padding-left: 0px;
|
||||||
50% {
|
|
||||||
color: var(--main-alert-color);
|
|
||||||
}
|
}
|
||||||
}
|
/* Handle small screens */
|
||||||
|
.smallScreen {
|
||||||
@keyframes pulse-early {
|
display: none;
|
||||||
50% {
|
margin: 20px;
|
||||||
color: rgb(136, 164, 255);
|
}
|
||||||
|
@media screen and (max-width: 335px) {
|
||||||
|
th {
|
||||||
|
font-size: 10px;
|
||||||
|
}
|
||||||
|
.dataRow {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
td.time {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
.tableTxt {
|
||||||
|
font-size: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media screen and (max-width: 279px) {
|
||||||
|
table {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.smallScreen {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/* CARRIED OVER FROM OLD COMPONENT:
|
/* Handle Large Screens */
|
||||||
|
@media screen and (min-width: 375px) {
|
||||||
|
.dataRow {
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
td.time {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media screen and (min-width: 450px) {
|
||||||
|
.dataRow {
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
td.time {
|
||||||
|
font-size: 19px;
|
||||||
|
}
|
||||||
|
.tableTxt {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
td.to,
|
||||||
|
td.from,
|
||||||
|
th.to,
|
||||||
|
th.from {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#timestamp {
|
/* Conditional Classes */
|
||||||
color: var(--second-text-color);
|
.loc.canc,
|
||||||
}
|
.canc {
|
||||||
|
color: grey;
|
||||||
|
text-decoration: line-through;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
.transport-mode {
|
.nonPass {
|
||||||
width: 30px;
|
opacity: 0.4;
|
||||||
margin: auto;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
.dataTable {
|
.late {
|
||||||
color: white;
|
animation: pulse-late 1.5s linear infinite;
|
||||||
font-weight: normal;
|
}
|
||||||
width: 100%;
|
|
||||||
margin: 0px, 0px;
|
|
||||||
padding-left: 8px;
|
|
||||||
padding-right: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.id {
|
.canc.time {
|
||||||
width: 12%;
|
animation: pulse-cancel 1.5s linear infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
.from {
|
.early {
|
||||||
width: 20%;
|
animation: pulse-early 1.5s linear infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
.to {
|
/* Animation Definitions */
|
||||||
width: 20%;
|
@keyframes pulse-late {
|
||||||
}
|
50% {
|
||||||
|
color: var(--main-warning-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.plat {
|
@keyframes pulse-cancel {
|
||||||
width: 8%;
|
50% {
|
||||||
}
|
color: var(--main-alert-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.timePair {
|
@keyframes pulse-early {
|
||||||
width: 20%;
|
50% {
|
||||||
}
|
color: rgb(136, 164, 255);
|
||||||
|
}
|
||||||
.time {
|
}
|
||||||
width: 10%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.data {
|
|
||||||
font-weight: normal;
|
|
||||||
}
|
|
||||||
|
|
||||||
.id-data {
|
|
||||||
color: lightgray;
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
.from-data,
|
|
||||||
.to-data {
|
|
||||||
color: yellow;
|
|
||||||
text-decoration: none;
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
.text-row {
|
|
||||||
margin-top: 0px;
|
|
||||||
padding-bottom: 5px;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.text-data {
|
|
||||||
text-align: left;
|
|
||||||
color: cyan;
|
|
||||||
font-size: smaller;
|
|
||||||
}
|
|
||||||
|
|
||||||
.can-dat {
|
|
||||||
color: grey;
|
|
||||||
text-decoration: line-through;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hidden {
|
|
||||||
opacity: 0.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ecs {
|
|
||||||
opacity: 0.75;
|
|
||||||
}
|
|
||||||
|
|
||||||
.can-time {
|
|
||||||
animation: pulse-cancel 1.5s linear infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
.early {
|
|
||||||
animation: pulse-early 1.5s linear infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
.late {
|
|
||||||
animation: pulse-late 1.5s linear infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
*/
|
|
||||||
</style>
|
</style>
|
||||||
|
@ -1,313 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import Reason from '$lib/raw-fetchers/reason.svelte';
|
|
||||||
import { tocs as tocMap } from '$lib/stores/tocMap';
|
|
||||||
|
|
||||||
import type { TrainServices, ServiceLocation } from '@owlboard/ts-types';
|
|
||||||
|
|
||||||
export let services: TrainServices[];
|
|
||||||
export let click: any; // Not sure of the type here!
|
|
||||||
|
|
||||||
function detail(event: any, rid: string, uid: string, tid: string) {
|
|
||||||
const target = event.target;
|
|
||||||
click(rid, uid, tid);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function formatLocations(locations: ServiceLocation[]): Promise<string> {
|
|
||||||
let tiplocs: string[] = [];
|
|
||||||
for (const location of locations) {
|
|
||||||
tiplocs.push(location.tiploc);
|
|
||||||
}
|
|
||||||
return tiplocs.join(' & ');
|
|
||||||
}
|
|
||||||
|
|
||||||
async function classGenerator(service: TrainServices) {
|
|
||||||
let otherArr: string[] = [];
|
|
||||||
let arrArr: string[] = [];
|
|
||||||
let depArr: string[] = [];
|
|
||||||
let platArr: string[] = [];
|
|
||||||
|
|
||||||
if (service.isCancelled) {
|
|
||||||
otherArr.push('canc');
|
|
||||||
}
|
|
||||||
if (service.serviceIsSupressed) {
|
|
||||||
otherArr.push('nonPass');
|
|
||||||
}
|
|
||||||
if (service.platformIsHidden) {
|
|
||||||
platArr.push('nonPass');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (service.sta !== undefined) {
|
|
||||||
if (service.eta !== undefined) {
|
|
||||||
if (service.sta < service.eta) {
|
|
||||||
arrArr.push('late');
|
|
||||||
}
|
|
||||||
} else if (service.ata !== undefined) {
|
|
||||||
if (service.sta < service.ata) {
|
|
||||||
arrArr.push('late');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (service.eta !== undefined) {
|
|
||||||
if (service.sta > service.eta) {
|
|
||||||
arrArr.push('early');
|
|
||||||
}
|
|
||||||
} else if (service.ata !== undefined) {
|
|
||||||
if (service.sta > service.ata) {
|
|
||||||
arrArr.push('early');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (service.std !== undefined) {
|
|
||||||
if (service.etd !== undefined) {
|
|
||||||
if (service.std < service.etd) {
|
|
||||||
depArr.push('late');
|
|
||||||
}
|
|
||||||
} else if (service.atd !== undefined) {
|
|
||||||
if (service.std < service.atd) {
|
|
||||||
depArr.push('late');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (service.etd !== undefined) {
|
|
||||||
if (service.std > service.etd) {
|
|
||||||
depArr.push('early');
|
|
||||||
}
|
|
||||||
} else if (service.atd !== undefined) {
|
|
||||||
if (service.std > service.atd) {
|
|
||||||
depArr.push('early');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
other: otherArr.join(' '),
|
|
||||||
arr: arrArr.join(' '),
|
|
||||||
dep: depArr.join(' '),
|
|
||||||
plat: platArr.join(' ')
|
|
||||||
};
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<p class="smallScreen">Your display is too small to view this data</p>
|
|
||||||
<table>
|
|
||||||
<tr>
|
|
||||||
<th class="id">ID</th>
|
|
||||||
<th class="from">From</th>
|
|
||||||
<th class="to">To</th>
|
|
||||||
<th class="plat">Plat</th>
|
|
||||||
<th class="time">Sch</th>
|
|
||||||
<th class="time">Exp</th>
|
|
||||||
<th class="time">Sch</th>
|
|
||||||
<th class="time">Exp</th>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th class="other" colspan="4" />
|
|
||||||
<th class="timepair" colspan="2">Arrival</th>
|
|
||||||
<th class="timepair" colspan="2">Departure</th>
|
|
||||||
</tr>
|
|
||||||
{#each services as service}
|
|
||||||
<tr
|
|
||||||
class="dataRow"
|
|
||||||
on:click={(event) => detail(event, service.rid, service.uid, service.trainid)}
|
|
||||||
on:keypress={(event) => detail(event, service.rid, service.uid, service.trainid)}
|
|
||||||
>
|
|
||||||
{#await classGenerator(service) then classes}
|
|
||||||
<td class="id {classes.other}">{service.trainid}</td>
|
|
||||||
<td class="from {classes.other}">{#await formatLocations(service.origin) then txt}{txt}{/await}</td>
|
|
||||||
<td class="to {classes.other}">{#await formatLocations(service.destination) then txt}{txt}{/await}</td>
|
|
||||||
<td class="plat">{service.platform || '-'}</td>
|
|
||||||
<td class="time schTime {classes.other}">{service.sta || '-'}</td>
|
|
||||||
<!-- All time need to be displayed appropriately -->
|
|
||||||
<td class="time {classes.other} {classes.arr}">{service.eta || service.ata || '-'}</td>
|
|
||||||
<!-- All time need to be displayed appropriately -->
|
|
||||||
<td class="time schTime {classes.other}">{service.std || '-'}</td>
|
|
||||||
<!-- All time need to be displayed appropriately -->
|
|
||||||
<td class="time {classes.other} {classes.dep}">{service.etd || service.atd || '-'}</td>
|
|
||||||
<!-- All time need to be displayed appropriately -->
|
|
||||||
{/await}
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td class="tableTxt" colspan="8">
|
|
||||||
{tocMap.get(service.operatorCode.toLowerCase()) || service.operatorCode}
|
|
||||||
{#if service.length} | {service.length} carriages{/if}
|
|
||||||
{#if service.delayReason}
|
|
||||||
<br />
|
|
||||||
<Reason type={'delay'} code={service.delayReason} />
|
|
||||||
{/if}
|
|
||||||
{#if service.cancelReason}
|
|
||||||
<br />
|
|
||||||
<Reason type={'cancel'} code={service.cancelReason} />
|
|
||||||
{/if}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td colspan="8">Unable to display service</td>
|
|
||||||
</tr>
|
|
||||||
{/each}
|
|
||||||
</table>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
table {
|
|
||||||
table-layout: fixed;
|
|
||||||
width: 100%;
|
|
||||||
max-width: 875px;
|
|
||||||
margin: auto;
|
|
||||||
padding: 0px;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
th {
|
|
||||||
font-size: 12px;
|
|
||||||
margin: 0px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dataRow {
|
|
||||||
font-family: ubuntu, monospace;
|
|
||||||
vertical-align: bottom;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Table Columns */
|
|
||||||
.id {
|
|
||||||
width: 8%;
|
|
||||||
}
|
|
||||||
.from {
|
|
||||||
width: 14%;
|
|
||||||
}
|
|
||||||
.to {
|
|
||||||
width: 14%;
|
|
||||||
}
|
|
||||||
.plat {
|
|
||||||
width: 6%;
|
|
||||||
}
|
|
||||||
.time {
|
|
||||||
width: 9%;
|
|
||||||
}
|
|
||||||
|
|
||||||
td.id {
|
|
||||||
color: lightblue;
|
|
||||||
text-align: left;
|
|
||||||
padding-left: 2px;
|
|
||||||
}
|
|
||||||
td.from,
|
|
||||||
td.to {
|
|
||||||
color: yellow;
|
|
||||||
}
|
|
||||||
|
|
||||||
td.to,
|
|
||||||
th.to {
|
|
||||||
text-align: right;
|
|
||||||
}
|
|
||||||
td.from,
|
|
||||||
th.from {
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
td.time {
|
|
||||||
font-size: 15px;
|
|
||||||
vertical-align: middle;
|
|
||||||
}
|
|
||||||
.tableTxt {
|
|
||||||
text-align: left;
|
|
||||||
padding-left: 2px;
|
|
||||||
color: var(--second-text-color);
|
|
||||||
vertical-align: top;
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Handle small screens */
|
|
||||||
.smallScreen {
|
|
||||||
display: none;
|
|
||||||
margin: 20px;
|
|
||||||
}
|
|
||||||
@media screen and (max-width: 335px) {
|
|
||||||
th {
|
|
||||||
font-size: 10px;
|
|
||||||
}
|
|
||||||
.dataRow {
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
td.time {
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
.tableTxt {
|
|
||||||
font-size: 10px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@media screen and (max-width: 279px) {
|
|
||||||
table {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
.smallScreen {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Handle Large Screens */
|
|
||||||
@media screen and (min-width: 375px) {
|
|
||||||
.dataRow {
|
|
||||||
font-size: 18px;
|
|
||||||
}
|
|
||||||
td.time {
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@media screen and (min-width: 450px) {
|
|
||||||
.dataRow {
|
|
||||||
font-size: 20px;
|
|
||||||
}
|
|
||||||
td.time {
|
|
||||||
font-size: 19px;
|
|
||||||
}
|
|
||||||
.tableTxt {
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
td.to,
|
|
||||||
td.from,
|
|
||||||
th.to,
|
|
||||||
th.from {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Conditional Classes */
|
|
||||||
.cancTxt {
|
|
||||||
color: grey !important;
|
|
||||||
text-decoration: line-through;
|
|
||||||
opacity: 0.8;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nonPass {
|
|
||||||
opacity: 0.6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.late {
|
|
||||||
animation: pulse-late 1.5s linear infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
.canc {
|
|
||||||
animation: pulse-cancel 1.5s linear infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
.early {
|
|
||||||
animation: pulse-early 1.5s linear infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Animation Definitions */
|
|
||||||
@keyframes pulse-late {
|
|
||||||
50% {
|
|
||||||
color: var(--main-warning-color);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes pulse-cancel {
|
|
||||||
50% {
|
|
||||||
color: var(--main-alert-color);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes pulse-early {
|
|
||||||
50% {
|
|
||||||
color: rgb(136, 164, 255);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
@ -1,285 +1,286 @@
|
|||||||
<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";
|
||||||
export let detail = {
|
import { getApiUrl } from "$lib/scripts/upstream";
|
||||||
uid: '',
|
export let detail = {
|
||||||
rid: '',
|
uid: "",
|
||||||
headcode: '',
|
rid: "",
|
||||||
show: true
|
headcode: "",
|
||||||
};
|
show: true,
|
||||||
export let close;
|
|
||||||
|
|
||||||
function handleClick() {
|
|
||||||
close();
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getTrain(rid) {
|
|
||||||
try {
|
|
||||||
console.log(`Requested Station: ${rid}`);
|
|
||||||
const url = `https://owlboard.info/api/v2/live/train/rid/${rid}`;
|
|
||||||
const opt = {
|
|
||||||
method: 'GET',
|
|
||||||
headers: {
|
|
||||||
uuid: $uuid
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const data = await fetch(url, opt);
|
|
||||||
return await data.json();
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error fetching data:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function parseDelay(location) {
|
|
||||||
let string, state;
|
|
||||||
if (location?.lateness) {
|
|
||||||
try {
|
|
||||||
const result = Math.floor(location.lateness / 60);
|
|
||||||
if (result === 0) {
|
|
||||||
(string = 'RT'), (state = '');
|
|
||||||
} else if (result < 0) {
|
|
||||||
(string = -result + 'E'), (state = 'early');
|
|
||||||
} else if (result > 0) {
|
|
||||||
(string = result + 'L'), (state = 'late');
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
(string = ''), (state = '');
|
|
||||||
}
|
|
||||||
} else if (location.arrivalType === 'Delayed') {
|
|
||||||
(string = ''), (state = 'late');
|
|
||||||
} else {
|
|
||||||
(string = ''), (state = 'noreport');
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
string: string,
|
|
||||||
state: state
|
|
||||||
};
|
};
|
||||||
}
|
export let close;
|
||||||
|
|
||||||
function parseTime(date) {
|
function handleClick() {
|
||||||
const parsedTime = date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
close();
|
||||||
return parsedTime !== 'Invalid Date' ? parsedTime : null;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
function parseTimes(service) {
|
async function getTrain(rid) {
|
||||||
const sta = new Date(service.sta),
|
try {
|
||||||
eta = new Date(service.eta),
|
console.log(`Requested Station: ${rid}`);
|
||||||
ata = new Date(service.ata);
|
const url = `${getApiUrl()}/api/v2/live/train/rid/${rid}`;
|
||||||
const std = new Date(service.std),
|
const opt = {
|
||||||
etd = new Date(service.etd),
|
method: "GET",
|
||||||
atd = new Date(service.atd);
|
headers: {
|
||||||
let parsedSta = parseTime(sta),
|
uuid: $uuid,
|
||||||
parsedEta = parseTime(eta),
|
},
|
||||||
parsedAta = parseTime(ata);
|
};
|
||||||
let parsedStd = parseTime(std),
|
const data = await fetch(url, opt);
|
||||||
parsedEtd = parseTime(etd),
|
return await data.json();
|
||||||
parsedAtd = parseTime(atd);
|
} catch (error) {
|
||||||
if (service.isCancelled) {
|
console.error("Error fetching data:", error);
|
||||||
(parsedEta = 'CANC'), (parsedEtd = 'CANC');
|
}
|
||||||
}
|
}
|
||||||
let times = {
|
|
||||||
sta: parsedSta || '-',
|
async function parseDelay(location) {
|
||||||
eata: parsedEta || parsedAta || '-',
|
let string, state;
|
||||||
aEst: parsedEta ? 'estimate' : '',
|
if (location?.lateness) {
|
||||||
std: parsedStd || '-',
|
try {
|
||||||
eatd: parsedEtd || parsedAtd || '-',
|
const result = Math.floor(location.lateness / 60);
|
||||||
dEst: parsedEtd ? 'estimate' : ''
|
if (result === 0) {
|
||||||
};
|
(string = "RT"), (state = "");
|
||||||
if (service.isCancelled) {
|
} else if (result < 0) {
|
||||||
(parsedEta = 'CANC'), (parsedEtd = 'CANC');
|
(string = -result + "E"), (state = "early");
|
||||||
(times.aEst = 'canc'), (times.dEst = 'canc');
|
} else if (result > 0) {
|
||||||
|
(string = result + "L"), (state = "late");
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
(string = ""), (state = "");
|
||||||
|
}
|
||||||
|
} else if (location.arrivalType === "Delayed") {
|
||||||
|
(string = ""), (state = "late");
|
||||||
|
} else {
|
||||||
|
(string = ""), (state = "noreport");
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
string: string,
|
||||||
|
state: state,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseTime(date) {
|
||||||
|
const parsedTime = date.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
|
||||||
|
return parsedTime !== "Invalid Date" ? parsedTime : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseTimes(service) {
|
||||||
|
const sta = new Date(service.sta),
|
||||||
|
eta = new Date(service.eta),
|
||||||
|
ata = new Date(service.ata);
|
||||||
|
const std = new Date(service.std),
|
||||||
|
etd = new Date(service.etd),
|
||||||
|
atd = new Date(service.atd);
|
||||||
|
let parsedSta = parseTime(sta),
|
||||||
|
parsedEta = parseTime(eta),
|
||||||
|
parsedAta = parseTime(ata);
|
||||||
|
let parsedStd = parseTime(std),
|
||||||
|
parsedEtd = parseTime(etd),
|
||||||
|
parsedAtd = parseTime(atd);
|
||||||
|
if (service.isCancelled) {
|
||||||
|
(parsedEta = "CANC"), (parsedEtd = "CANC");
|
||||||
|
}
|
||||||
|
let times = {
|
||||||
|
sta: parsedSta || "-",
|
||||||
|
eata: parsedEta || parsedAta || "-",
|
||||||
|
aEst: parsedEta ? "estimate" : "",
|
||||||
|
std: parsedStd || "-",
|
||||||
|
eatd: parsedEtd || parsedAtd || "-",
|
||||||
|
dEst: parsedEtd ? "estimate" : "",
|
||||||
|
};
|
||||||
|
if (service.isCancelled) {
|
||||||
|
(parsedEta = "CANC"), (parsedEtd = "CANC");
|
||||||
|
(times.aEst = "canc"), (times.dEst = "canc");
|
||||||
|
}
|
||||||
|
return times;
|
||||||
}
|
}
|
||||||
return times;
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<OverlayIsland>
|
<OverlayIsland>
|
||||||
<div id="detailBox">
|
<div id="detailBox">
|
||||||
<button type="button" id="closeService" on:click={handleClick}>X</button>
|
<button type="button" id="closeService" on:click={handleClick}>X</button>
|
||||||
{#await getTrain(detail.rid)}
|
{#await getTrain(detail.rid)}
|
||||||
<h6>{detail.headcode}</h6>
|
<h6>{detail.headcode}</h6>
|
||||||
<p in:fade id="loading">Loading Data...</p>
|
<p in:fade id="loading">Loading Data...</p>
|
||||||
{:then train}
|
{:then train}
|
||||||
<h6><StylesToc toc={train.GetServiceDetailsResult.operatorCode} full={true} /> {detail.headcode}</h6>
|
<h6><StylesToc toc={train.GetServiceDetailsResult.operatorCode} full={true} /> {detail.headcode}</h6>
|
||||||
<p>
|
<p>
|
||||||
Locations in grey are not scheduled stops
|
Locations in grey are not scheduled stops
|
||||||
<br />
|
<br />
|
||||||
Times in <span class="estimate">yellow</span> are estimated times
|
Times in <span class="estimate">yellow</span> are estimated times
|
||||||
</p>
|
</p>
|
||||||
{#if train.GetServiceDetailsResult.delayReason}
|
{#if train.GetServiceDetailsResult.delayReason}
|
||||||
<p class="reason late">
|
<p class="reason late">
|
||||||
<Reason type="delay" code={train.GetServiceDetailsResult.delayReason} />
|
<Reason type="delay" code={train.GetServiceDetailsResult.delayReason} />
|
||||||
</p>
|
</p>
|
||||||
{/if}
|
{/if}
|
||||||
{#if train.GetServiceDetailsResult.cancelReason}
|
{#if train.GetServiceDetailsResult.cancelReason}
|
||||||
<p class="reason canc">
|
<p class="reason canc">
|
||||||
<Reason type="cancel" code={train.GetServiceDetailsResult.cancelReason} />
|
<Reason type="cancel" code={train.GetServiceDetailsResult.cancelReason} />
|
||||||
</p>
|
</p>
|
||||||
{/if}
|
{/if}
|
||||||
<table id="detailTable">
|
<table id="detailTable">
|
||||||
<tr>
|
<tr>
|
||||||
<th class="tableLocation">Loc.</th>
|
<th class="tableLocation">Loc.</th>
|
||||||
<th class="tablePlatform">Pl.</th>
|
<th class="tablePlatform">Pl.</th>
|
||||||
<th class="tableTime">Sch</th>
|
<th class="tableTime">Sch</th>
|
||||||
<th class="tableTime">Est/<br />Act</th>
|
<th class="tableTime">Est/<br />Act</th>
|
||||||
<th class="tableTime">Sch</th>
|
<th class="tableTime">Sch</th>
|
||||||
<th class="tableTime">Est/<br />Act</th>
|
<th class="tableTime">Est/<br />Act</th>
|
||||||
<th class="tableDelay" />
|
<th class="tableDelay" />
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th colspan="2" />
|
<th colspan="2" />
|
||||||
<th colspan="2">Arrival</th>
|
<th colspan="2">Arrival</th>
|
||||||
<th colspan="2">Departure</th>
|
<th colspan="2">Departure</th>
|
||||||
<th />
|
<th />
|
||||||
</tr>
|
</tr>
|
||||||
{#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)}
|
||||||
<td>-</td>
|
<td>-</td>
|
||||||
{:then delay}
|
{:then delay}
|
||||||
<td class={delay.state}>{delay.string}</td>
|
<td class={delay.state}>{delay.string}</td>
|
||||||
{/await}
|
{/await}
|
||||||
</tr>
|
</tr>
|
||||||
{/each}
|
{/each}
|
||||||
</table>
|
</table>
|
||||||
{:catch}
|
{:catch}
|
||||||
<h6>Error loading data</h6>
|
<h6>Error loading data</h6>
|
||||||
{/await}
|
{/await}
|
||||||
</div>
|
</div>
|
||||||
</OverlayIsland>
|
</OverlayIsland>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
#detailBox {
|
#detailBox {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
min-height: 100px;
|
min-height: 100px;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
h6 {
|
h6 {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: -16px;
|
top: -16px;
|
||||||
left: 20px;
|
left: 20px;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
color: whitesmoke;
|
color: whitesmoke;
|
||||||
}
|
}
|
||||||
#loading {
|
#loading {
|
||||||
color: white;
|
color: white;
|
||||||
animation: pulse-early 2.5s linear infinite;
|
animation: pulse-early 2.5s linear infinite;
|
||||||
}
|
}
|
||||||
p {
|
p {
|
||||||
margin-top: 45px;
|
margin-top: 45px;
|
||||||
margin-bottom: 0px;
|
margin-bottom: 0px;
|
||||||
}
|
}
|
||||||
p.reason {
|
p.reason {
|
||||||
margin-top: 5px;
|
margin-top: 5px;
|
||||||
}
|
}
|
||||||
#closeService {
|
#closeService {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 10px;
|
top: 10px;
|
||||||
right: 10px;
|
right: 10px;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 60px;
|
border-radius: 60px;
|
||||||
width: 35px;
|
width: 35px;
|
||||||
height: 35px;
|
height: 35px;
|
||||||
background-color: var(--main-bg-color);
|
background-color: var(--main-bg-color);
|
||||||
color: white;
|
color: white;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
#detailTable {
|
|
||||||
margin-top: 10px;
|
|
||||||
table-layout: fixed;
|
|
||||||
width: 100%;
|
|
||||||
margin-top: 12px;
|
|
||||||
margin-left: 0px;
|
|
||||||
margin-right: 0px;
|
|
||||||
padding: 0px;
|
|
||||||
color: white;
|
|
||||||
font-family: ubuntu, monospace;
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
|
||||||
@media screen and (max-width: 338px) {
|
|
||||||
#detailTable {
|
#detailTable {
|
||||||
font-size: 14px;
|
margin-top: 10px;
|
||||||
|
table-layout: fixed;
|
||||||
|
width: 100%;
|
||||||
|
margin-top: 12px;
|
||||||
|
margin-left: 0px;
|
||||||
|
margin-right: 0px;
|
||||||
|
padding: 0px;
|
||||||
|
color: white;
|
||||||
|
font-family: ubuntu, monospace;
|
||||||
|
font-size: 16px;
|
||||||
}
|
}
|
||||||
}
|
@media screen and (max-width: 338px) {
|
||||||
@media screen and (max-width: 301px) {
|
#detailTable {
|
||||||
#detailTable {
|
font-size: 14px;
|
||||||
font-size: 12px;
|
}
|
||||||
}
|
}
|
||||||
}
|
@media screen and (max-width: 301px) {
|
||||||
@media screen and (min-width: 469px) {
|
#detailTable {
|
||||||
#detailTable {
|
font-size: 12px;
|
||||||
font-size: 20px;
|
}
|
||||||
|
}
|
||||||
|
@media screen and (min-width: 469px) {
|
||||||
|
#detailTable {
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.tableLocation {
|
||||||
|
width: 18%;
|
||||||
|
}
|
||||||
|
td.location {
|
||||||
|
color: yellow;
|
||||||
|
}
|
||||||
|
.tablePlatform {
|
||||||
|
width: 9%;
|
||||||
|
}
|
||||||
|
.tableTime {
|
||||||
|
width: 14%;
|
||||||
|
}
|
||||||
|
.tableDelay {
|
||||||
|
width: 7%;
|
||||||
|
}
|
||||||
|
.estimate {
|
||||||
|
color: rgb(255, 255, 119);
|
||||||
|
}
|
||||||
|
.pass {
|
||||||
|
color: white !important;
|
||||||
|
opacity: 0.45;
|
||||||
|
}
|
||||||
|
.canc {
|
||||||
|
color: white;
|
||||||
|
animation: pulse-cancel 1.5s linear infinite;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
.tableLocation {
|
|
||||||
width: 18%;
|
|
||||||
}
|
|
||||||
td.location {
|
|
||||||
color: yellow;
|
|
||||||
}
|
|
||||||
.tablePlatform {
|
|
||||||
width: 9%;
|
|
||||||
}
|
|
||||||
.tableTime {
|
|
||||||
width: 14%;
|
|
||||||
}
|
|
||||||
.tableDelay {
|
|
||||||
width: 7%;
|
|
||||||
}
|
|
||||||
.estimate {
|
|
||||||
color: rgb(255, 255, 119);
|
|
||||||
}
|
|
||||||
.pass {
|
|
||||||
color: white !important;
|
|
||||||
opacity: 0.45;
|
|
||||||
}
|
|
||||||
.canc {
|
|
||||||
color: white;
|
|
||||||
animation: pulse-cancel 1.5s linear infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
.early {
|
.early {
|
||||||
animation: pulse-early 1.5s linear infinite;
|
animation: pulse-early 1.5s linear infinite;
|
||||||
}
|
|
||||||
|
|
||||||
.late {
|
|
||||||
color: white;
|
|
||||||
animation: pulse-late 1.5s linear infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes pulse-late {
|
|
||||||
50% {
|
|
||||||
color: var(--main-warning-color);
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes pulse-cancel {
|
.late {
|
||||||
50% {
|
color: white;
|
||||||
color: var(--main-alert-color);
|
animation: pulse-late 1.5s linear infinite;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes pulse-early {
|
@keyframes pulse-late {
|
||||||
50% {
|
50% {
|
||||||
color: rgb(136, 164, 255);
|
color: var(--main-warning-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse-cancel {
|
||||||
|
50% {
|
||||||
|
color: var(--main-alert-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse-early {
|
||||||
|
50% {
|
||||||
|
color: rgb(136, 164, 255);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
29
src/lib/ldb/staff/train-detail.ts
Normal file
29
src/lib/ldb/staff/train-detail.ts
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
// Contains the details required to lookup train details
|
||||||
|
export interface Detail {
|
||||||
|
show: boolean;
|
||||||
|
headcode: string;
|
||||||
|
rid: string;
|
||||||
|
uid: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initiates/Resets a `Detail` interface
|
||||||
|
export function detailInit(): Detail {
|
||||||
|
const detail: Detail = {
|
||||||
|
show: false,
|
||||||
|
headcode: "",
|
||||||
|
rid: "",
|
||||||
|
uid: "",
|
||||||
|
};
|
||||||
|
return detail;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initiates/Updates a `Detail` interface using the given values
|
||||||
|
export function defineDetail(rid: string, uid: string, tid: string) {
|
||||||
|
const detail: Detail = {
|
||||||
|
rid: rid,
|
||||||
|
uid: uid,
|
||||||
|
headcode: tid,
|
||||||
|
show: true,
|
||||||
|
};
|
||||||
|
return detail;
|
||||||
|
}
|
@ -1,76 +1,77 @@
|
|||||||
import { uuid } from './stores/uuid';
|
import { getApiUrl } from "./scripts/upstream";
|
||||||
|
import { uuid } from "./stores/uuid";
|
||||||
|
|
||||||
export interface libauthResponse {
|
export interface libauthResponse {
|
||||||
uuidPresent?: boolean;
|
uuidPresent?: boolean;
|
||||||
serverAuthCheck?: boolean;
|
serverAuthCheck?: boolean;
|
||||||
uuidValue?: string;
|
uuidValue?: string;
|
||||||
serverAuthCheckResponseCode?: number;
|
serverAuthCheckResponseCode?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface uuidCheckRes {
|
interface uuidCheckRes {
|
||||||
uuidValue?: string;
|
uuidValue?: string;
|
||||||
uuidPresent?: boolean;
|
uuidPresent?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function checkAuth(): Promise<libauthResponse> {
|
export async function checkAuth(): Promise<libauthResponse> {
|
||||||
let result: libauthResponse = {};
|
let result: libauthResponse = {};
|
||||||
const uuidCheck = await checkUuid();
|
const uuidCheck = await checkUuid();
|
||||||
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;
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
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);
|
|
||||||
if (uuid_value && uuid_value != 'null') {
|
|
||||||
res = {
|
|
||||||
uuidPresent: true,
|
|
||||||
uuidValue: uuid_value
|
|
||||||
};
|
};
|
||||||
} else {
|
console.log("uuid-value is: ", uuid_value);
|
||||||
res = {
|
if (uuid_value && uuid_value != "null") {
|
||||||
uuidPresent: false,
|
res = {
|
||||||
uuidValue: uuid_value
|
uuidPresent: true,
|
||||||
};
|
uuidValue: uuid_value,
|
||||||
}
|
};
|
||||||
unsubscribe();
|
} else {
|
||||||
return res;
|
res = {
|
||||||
|
uuidPresent: false,
|
||||||
|
uuidValue: uuid_value,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
unsubscribe();
|
||||||
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function checkServerAuth(uuidString: string) {
|
async function checkServerAuth(uuidString: string) {
|
||||||
const url = 'https://owlboard.info/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);
|
||||||
|
let ok: boolean;
|
||||||
|
if (res.status !== 401) {
|
||||||
|
ok = true;
|
||||||
|
} else {
|
||||||
|
ok = false;
|
||||||
}
|
}
|
||||||
};
|
return {
|
||||||
const res = await fetch(url, options);
|
authOk: ok,
|
||||||
let ok: boolean;
|
status: res.status,
|
||||||
if (res.status !== 401) {
|
};
|
||||||
ok = true;
|
|
||||||
} else {
|
|
||||||
ok = false;
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
authOk: ok,
|
|
||||||
status: res.status
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function logout(): Promise<boolean> {
|
export async function logout(): Promise<boolean> {
|
||||||
uuid.set(null);
|
uuid.set(null);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
64
src/lib/main.css
Normal file
64
src/lib/main.css
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
/* FONTS */
|
||||||
|
@font-face {
|
||||||
|
font-family: "firamono";
|
||||||
|
src: url("/font/firamono/firamono-regular.woff2") format("woff2"), url("/font/firamono/firamono-regular.woff") format("woff");
|
||||||
|
font-weight: normal;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family: "firamono";
|
||||||
|
src: url("/font/firamono/firamono-500.woff2") format("woff2"), url("/font/firamono/firamono-500.woff") format("woff");
|
||||||
|
font-weight: 500;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family: "urwgothic";
|
||||||
|
src: url("/font/urwgothic/urwgothic.woff2") format("woff2"), url("/font/urwgothic/urwgothic.woff") format("woff");
|
||||||
|
font-weight: normal;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family: "urwgothic";
|
||||||
|
src: url("/font/urwgothic/urwgothicDemi.woff2") format("woff2"), url("/font/urwgothic/urwgothicDemi.woff") format("woff");
|
||||||
|
font-weight: 900;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: "ubuntu";
|
||||||
|
src: url("/font/ubuntumono/ubuntumono-regular.woff2") format("woff2"), url("/font/ubuntumono/ubuntumono-regular.woff") format("woff");
|
||||||
|
font-weight: 400;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
html {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
background-color: var(--main-bg-color);
|
||||||
|
background-image: radial-gradient(var(--second-bg-color), var(--main-bg-color));
|
||||||
|
color: var(--main-text-color);
|
||||||
|
font-family: urwgothic, sans-serif;
|
||||||
|
text-align: center;
|
||||||
|
margin: auto;
|
||||||
|
width: 100%;
|
||||||
|
padding-bottom: 65px;
|
||||||
|
}
|
||||||
|
a {
|
||||||
|
color: var(--main-link-color);
|
||||||
|
}
|
||||||
|
button:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
-webkit-touch-callout: none;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
-khtml-user-select: none;
|
||||||
|
-moz-user-select: none;
|
||||||
|
-ms-user-select: none;
|
||||||
|
user-select: none;
|
||||||
|
-webkit-tap-highlight-color: transparent;
|
||||||
|
}
|
28
src/lib/navigation/InLineLoading.svelte
Normal file
28
src/lib/navigation/InLineLoading.svelte
Normal 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>
|
@ -1,24 +1,25 @@
|
|||||||
<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();
|
||||||
location.reload();
|
location.reload();
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<button class="logout" type="button" on:click={logoutAction}>Logout</button>
|
<button class="logout" type="button" on:click={logoutAction}>Logout</button>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.logout {
|
.logout {
|
||||||
border: none;
|
border: none;
|
||||||
background-color: var(--overlay-color);
|
background-color: var(--island-button-color);
|
||||||
color: white;
|
color: var(--island-link-color);
|
||||||
width: 35%;
|
width: 35%;
|
||||||
border-radius: 50px;
|
border-radius: 50px;
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
min-width: 90px;
|
min-width: 90px;
|
||||||
margin: 30px;
|
margin: 30px;
|
||||||
height: 48px;
|
height: 48px;
|
||||||
}
|
box-shadow: var(--box-shadow);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
57
src/lib/navigation/TimeBar.svelte
Normal file
57
src/lib/navigation/TimeBar.svelte
Normal 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>
|
@ -1,31 +1,32 @@
|
|||||||
<div id="container">
|
<div id="container">
|
||||||
<p id="tick">✔</p>
|
<p id="tick">✔</p>
|
||||||
<p>Done</p>
|
<p>Done</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
#tick {
|
#tick {
|
||||||
font-size: 45px;
|
font-size: 45px;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
#container {
|
#container {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 50%;
|
top: 50%;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
transform: translate(-50%, -50%);
|
transform: translate(-50%, -50%);
|
||||||
margin: auto;
|
margin: auto;
|
||||||
background-color: var(--overlay-color);
|
background-color: var(--overlay-island-bg-color);
|
||||||
border-radius: 15px;
|
border-radius: 15px;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
padding-bottom: 1px;
|
padding-bottom: 1px;
|
||||||
min-width: 90px;
|
min-width: 90px;
|
||||||
max-width: 90px;
|
max-width: 90px;
|
||||||
}
|
box-shadow: var(--box-shadow);
|
||||||
p {
|
}
|
||||||
padding-top: 0px;
|
p {
|
||||||
font-weight: bolder;
|
padding-top: 0px;
|
||||||
overflow-wrap: normal;
|
font-weight: bolder;
|
||||||
color: white;
|
overflow-wrap: normal;
|
||||||
}
|
color: var(--island-text-color);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -1,55 +1,53 @@
|
|||||||
<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" />
|
</a>
|
||||||
<img src="/images/logo/wide_logo_200.png" alt="OwlBoard Logo" />
|
<header>{title}</header>
|
||||||
</picture>
|
|
||||||
</a>
|
|
||||||
<header>{title}</header>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="headerBlock">
|
<div class="headerBlock">
|
||||||
<!-- This exists to prevent the headerBar overlapping anything below it -->
|
<!-- This exists to prevent the headerBar overlapping anything below it -->
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.headerBar {
|
.headerBar {
|
||||||
background: var(--overlay-color-solid);
|
background: var(--island-bg-solid);
|
||||||
color: var(--main-text-color);
|
color: var(--main-header-color);
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
height: 50px;
|
height: 50px;
|
||||||
z-index: 20;
|
z-index: 20;
|
||||||
}
|
box-shadow: 0 3px 30px var(--box-shadow-color);
|
||||||
|
}
|
||||||
|
|
||||||
img {
|
img {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
height: 40px;
|
height: 40px;
|
||||||
right: 8px;
|
right: 8px;
|
||||||
top: 5px;
|
top: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
header {
|
header {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 0px;
|
left: 0px;
|
||||||
top: 3px;
|
top: 3px;
|
||||||
font-family: urwgothic, sans-serif;
|
font-family: urwgothic, sans-serif;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
margin-top: 7px;
|
margin-top: 7px;
|
||||||
margin-left: 20px;
|
margin-left: 20px;
|
||||||
font-size: 15pt;
|
font-size: 15pt;
|
||||||
}
|
}
|
||||||
|
|
||||||
.headerBlock {
|
.headerBlock {
|
||||||
height: 50px;
|
height: 50px;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -1,16 +1,16 @@
|
|||||||
<p id="load">Loading...</p>
|
<p id="load">Loading...</p>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
#load {
|
#load {
|
||||||
margin-top: 5px;
|
margin-top: 5px;
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: white;
|
color: white;
|
||||||
animation: pulse-loading 2.5s linear infinite;
|
animation: pulse-loading 2.5s linear infinite;
|
||||||
}
|
}
|
||||||
@keyframes pulse-loading {
|
@keyframes pulse-loading {
|
||||||
50% {
|
50% {
|
||||||
color: rgb(136, 164, 255);
|
color: rgb(136, 164, 255);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
@ -1,53 +1,54 @@
|
|||||||
<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 }}>
|
||||||
<div class="spinner" />
|
<div class="spinner" />
|
||||||
<p>Loading...</p>
|
<p>Loading...</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
@keyframes spinner {
|
@keyframes spinner {
|
||||||
0% {
|
0% {
|
||||||
transform: translate3d(-50%, -50%, 0) rotate(0);
|
transform: translate3d(-50%, -50%, 0) rotate(0);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: translate3d(-50%, -50%, 0) rotate(360deg);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
100% {
|
.spinner::before {
|
||||||
transform: translate3d(-50%, -50%, 0) rotate(360deg);
|
animation: 1.5s linear infinite spinner;
|
||||||
|
animation-play-state: inherit;
|
||||||
|
border: solid 5px var(--overlay-island-bg-color);
|
||||||
|
border-bottom-color: white;
|
||||||
|
border-radius: 50%;
|
||||||
|
content: "";
|
||||||
|
height: 40px;
|
||||||
|
width: 40px;
|
||||||
|
position: absolute;
|
||||||
|
top: 30%;
|
||||||
|
margin: auto;
|
||||||
|
transform: translate3d(-50%, -50%, 0);
|
||||||
|
will-change: transform;
|
||||||
|
}
|
||||||
|
#container {
|
||||||
|
position: fixed;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
margin: auto;
|
||||||
|
background-color: var(--overlay-island-bg-color);
|
||||||
|
border-radius: 15px;
|
||||||
|
padding: 20px;
|
||||||
|
padding-bottom: 1px;
|
||||||
|
min-width: 90px;
|
||||||
|
max-width: 90px;
|
||||||
|
box-shadow: var(--box-shadow);
|
||||||
|
}
|
||||||
|
p {
|
||||||
|
padding-top: 50px;
|
||||||
|
font-weight: bolder;
|
||||||
|
overflow-wrap: normal;
|
||||||
|
color: var(--island-text-color);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
.spinner::before {
|
|
||||||
animation: 1.5s linear infinite spinner;
|
|
||||||
animation-play-state: inherit;
|
|
||||||
border: solid 5px var(--overlay-color);
|
|
||||||
border-bottom-color: white;
|
|
||||||
border-radius: 50%;
|
|
||||||
content: '';
|
|
||||||
height: 40px;
|
|
||||||
width: 40px;
|
|
||||||
position: absolute;
|
|
||||||
top: 30%;
|
|
||||||
margin: auto;
|
|
||||||
transform: translate3d(-50%, -50%, 0);
|
|
||||||
will-change: transform;
|
|
||||||
}
|
|
||||||
#container {
|
|
||||||
position: fixed;
|
|
||||||
top: 50%;
|
|
||||||
left: 50%;
|
|
||||||
transform: translate(-50%, -50%);
|
|
||||||
margin: auto;
|
|
||||||
background-color: var(--overlay-color);
|
|
||||||
border-radius: 15px;
|
|
||||||
padding: 20px;
|
|
||||||
padding-bottom: 1px;
|
|
||||||
min-width: 90px;
|
|
||||||
max-width: 90px;
|
|
||||||
}
|
|
||||||
p {
|
|
||||||
padding-top: 50px;
|
|
||||||
font-weight: bolder;
|
|
||||||
overflow-wrap: normal;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
@ -1,91 +1,93 @@
|
|||||||
<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">
|
||||||
<a href="https://nationalrail.co.uk" target="_blank">
|
<a href="https://nationalrail.co.uk" target="_blank">
|
||||||
<picture>
|
<picture>
|
||||||
<source srcset="/images/nre/nre-powered_200w.jxl" type="image/jxl" />
|
<source srcset="/images/nre/nre-powered_200w.jxl" type="image/jxl" />
|
||||||
<source srcset="/images/nre/nre-powered_200w.webp" type="image/webp" />
|
<source srcset="/images/nre/nre-powered_200w.webp" type="image/webp" />
|
||||||
<img id="nre-logo" src="/images/nre/nre-powered_200w.png" alt="Data sourced from National Rail and others" />
|
<img id="nre-logo" src="/images/nre/nre-powered_200w.png" alt="Data sourced from National Rail and others" />
|
||||||
</picture>
|
</picture>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
footer {
|
footer {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
display: flex;
|
display: flex;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 50px;
|
height: 50px;
|
||||||
background-color: var(--overlay-color);
|
background-color: var(--island-bg-solid);
|
||||||
}
|
}
|
||||||
|
|
||||||
.footerLink {
|
.footerLink {
|
||||||
width: 30%;
|
width: 30%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
background-color: var(--overlay-color);
|
background-color: var(--island-bg-solid);
|
||||||
border-style: solid;
|
border-style: solid;
|
||||||
border-width: 1px;
|
border-width: 1px;
|
||||||
border-top: none;
|
border-top: none;
|
||||||
border-bottom: none;
|
border-bottom: none;
|
||||||
border-color: rgba(0, 0, 0, 0.24);
|
border-color: var(--box-shadow-color);
|
||||||
text-decoration: double;
|
color: var(--main-text-color);
|
||||||
font-weight: 600;
|
text-decoration: double;
|
||||||
}
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
footer a.active {
|
footer a.active {
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
}
|
}
|
||||||
.data-source {
|
|
||||||
flex-grow: 2;
|
|
||||||
background: rgb(255, 255, 255);
|
|
||||||
}
|
|
||||||
#nre-logo {
|
|
||||||
width: 150px;
|
|
||||||
height: auto;
|
|
||||||
margin: auto;
|
|
||||||
margin-top: 13px;
|
|
||||||
}
|
|
||||||
@media only screen and (min-width: 475px) {
|
|
||||||
.data-source {
|
.data-source {
|
||||||
background: rgb(255, 255, 255);
|
flex-grow: 2;
|
||||||
background: linear-gradient(90deg, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 1) 40%);
|
background: rgb(255, 255, 255);
|
||||||
}
|
}
|
||||||
#nre-logo {
|
#nre-logo {
|
||||||
position: absolute;
|
width: 150px;
|
||||||
right: 0;
|
height: auto;
|
||||||
right: 20px;
|
margin: auto;
|
||||||
|
margin-top: 13px;
|
||||||
|
}
|
||||||
|
@media only screen and (min-width: 475px) {
|
||||||
|
.data-source {
|
||||||
|
background: rgb(255, 255, 255);
|
||||||
|
background: linear-gradient(90deg, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 1) 40%);
|
||||||
|
}
|
||||||
|
#nre-logo {
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
right: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
img {
|
||||||
|
height: 20px;
|
||||||
|
width: 20px;
|
||||||
|
margin: 0;
|
||||||
|
margin-top: 3px;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
.title {
|
||||||
|
margin: 0;
|
||||||
|
margin-bottom: 3px;
|
||||||
|
padding: 0;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
img {
|
|
||||||
height: 20px;
|
|
||||||
width: 20px;
|
|
||||||
margin: 0;
|
|
||||||
margin-top: 3px;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
span {
|
|
||||||
margin: 0;
|
|
||||||
margin-bottom: 3px;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
@ -1,74 +1,69 @@
|
|||||||
<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>
|
||||||
{/each}
|
{/each}
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
footer {
|
footer {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
display: flex;
|
display: flex;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 50px;
|
height: 50px;
|
||||||
background-color: rgb(54, 54, 54);
|
background-color: rgb(54, 54, 54);
|
||||||
}
|
box-shadow: 0 -2px 30px rgba(0, 0, 0, 0.19);
|
||||||
|
}
|
||||||
|
|
||||||
footer a {
|
footer a {
|
||||||
flex: 12;
|
flex: 12;
|
||||||
width: 30%;
|
width: 30%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
background-color: var(--overlay-color);
|
background-color: var(--island-bg-solid);
|
||||||
border-style: solid;
|
border-style: solid;
|
||||||
border-width: 1px;
|
border-width: 1px;
|
||||||
border-top: none;
|
border-top: none;
|
||||||
border-bottom: none;
|
border-bottom: none;
|
||||||
border-color: rgba(0, 0, 0, 0.24);
|
border-color: var(--box-shadow-color);
|
||||||
text-decoration: double;
|
text-decoration: double;
|
||||||
font-weight: 600;
|
color: var(--main-text-color);
|
||||||
}
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
footer a.active {
|
footer a.active {
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
img {
|
span {
|
||||||
height: 20px;
|
margin: 0;
|
||||||
width: 20px;
|
margin-bottom: 3px;
|
||||||
margin: 0;
|
padding: 0;
|
||||||
margin-top: 3px;
|
}
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
span {
|
|
||||||
margin: 0;
|
|
||||||
margin-bottom: 3px;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
@ -1,102 +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>Sign-up Fixed</h3>' +
|
|
||||||
'<p>An issue present since 28/07/2023 has meant that new users or users with expired logins are unable to register.</p>' +
|
|
||||||
'<p>This issue has now been fixed and new users will be able to register for Rail Staff Access</p>',
|
|
||||||
'<h3>Always Improving</h3>' +
|
|
||||||
'<p>OwlBoard is always improving, the current focus is to improve performance when you have low mobile signal by reducing the amount of data being sent.</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}><</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}>></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: grey;
|
|
||||||
border-radius: 10px;
|
|
||||||
z-index: 2500;
|
|
||||||
}
|
|
||||||
|
|
||||||
.navButton {
|
|
||||||
border-radius: 50px;
|
|
||||||
border: none;
|
|
||||||
color: white;
|
|
||||||
background-color: var(--overlay-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: white;
|
|
||||||
}
|
|
||||||
</style>
|
|
113
src/lib/popover/analytics-consent.svelte
Normal file
113
src/lib/popover/analytics-consent.svelte
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { setTelemetryFalse, setTelemetryTrue } from "$lib/stores/SetTelemetryConsent";
|
||||||
|
import { telemetry } from "$lib/stores/telemetryConsent";
|
||||||
|
import { onMount } from "svelte";
|
||||||
|
import { browser } from "$app/environment";
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
if (!localStorage.getItem("telemetryRequested")) {
|
||||||
|
document.querySelector<HTMLDialogElement>("#analytics-consent")?.showModal();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Setting Function Calls
|
||||||
|
function setAcceptAnalytics() {
|
||||||
|
setTelemetryTrue();
|
||||||
|
localStorage.setItem("telemetryRequested", "yes");
|
||||||
|
document.querySelector<HTMLDialogElement>("#analytics-consent")?.close();
|
||||||
|
}
|
||||||
|
function setDenyAnalytics() {
|
||||||
|
setTelemetryFalse();
|
||||||
|
localStorage.setItem("telemetryRequested", "yes");
|
||||||
|
document.querySelector<HTMLDialogElement>("#analytics-consent")?.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reactively call telemetry script functions
|
||||||
|
$: {
|
||||||
|
if (browser) {
|
||||||
|
if ($telemetry) {
|
||||||
|
loadTelemetryScript();
|
||||||
|
} else {
|
||||||
|
removeTelemetryScript();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadTelemetryScript() {
|
||||||
|
console.log("Activating telemetry script")
|
||||||
|
if (browser) {
|
||||||
|
if (document.querySelector("script[data-entity='owlboard-frontend']")) return; // Prevent duplicate loading
|
||||||
|
|
||||||
|
const script = document.createElement("script");
|
||||||
|
script.type = "module";
|
||||||
|
script.dataset.entity = "owlboard-frontend";
|
||||||
|
script.src = "https://liwan.fjla.uk/script.js";
|
||||||
|
document.body.appendChild(script);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeTelemetryScript() {
|
||||||
|
console.log("Deactivating telemetry script")
|
||||||
|
if (browser) {
|
||||||
|
document.querySelector("script[data-entity='owlboard-frontend']")?.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<dialog id="analytics-consent">
|
||||||
|
<h1>Telemetry</h1>
|
||||||
|
<p>
|
||||||
|
OwlBoard collects <strong>anonymous</strong> usage data, such as the most visited pages. Any personal data is anonymized to ensure it cannot be linked to individuals.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
This data is used to focus efforts, improving the most used features.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
By selecting Accept, you are helping to steer OwlBoard's future - if
|
||||||
|
you change your mind, head over to Settings.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Nobody can be identified using any data stored, all data is available for
|
||||||
|
all to see <a href="https://liwan.fjla.uk" target="_blank">here</a>.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Further information can be found in the <a href="/more/privacy">Privacy Policy</a>.
|
||||||
|
</p>
|
||||||
|
<button class="modal-button" type="button" on:click={setAcceptAnalytics}>Accept</button>
|
||||||
|
<button class="modal-button" id="deny-button" type="button" on:click={setDenyAnalytics}>Deny</button>
|
||||||
|
</dialog>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
::backdrop {
|
||||||
|
background-color: var(--main-bg-color);
|
||||||
|
opacity: 75%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#analytics-consent {
|
||||||
|
width: 75vw;
|
||||||
|
max-width: 700px;
|
||||||
|
border-radius: 25px;
|
||||||
|
background-color: var(--island-bg-color);
|
||||||
|
opacity: 100;
|
||||||
|
color: var(--island-text-color);
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-button {
|
||||||
|
width: 25%;
|
||||||
|
min-width: 120px;
|
||||||
|
height: 40px;
|
||||||
|
font-size: larger;
|
||||||
|
margin: 5px;
|
||||||
|
border-radius: 30px;
|
||||||
|
background-color: var(--second-bg-color);
|
||||||
|
color: var(--island-text-color);
|
||||||
|
box-shadow: var(--box-shadow);
|
||||||
|
border: none;
|
||||||
|
font-family: urwgothic, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
#deny-button {
|
||||||
|
background-color: var(--main-bg-color);
|
||||||
|
}
|
||||||
|
</style>
|
@ -1,48 +1,49 @@
|
|||||||
<script>
|
<script lang="ts">
|
||||||
import { uuid } from '$lib/stores/uuid';
|
import { apiGet } from "$lib/scripts/apiFetch";
|
||||||
|
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);
|
if (data) {
|
||||||
return data[0].lateReason || 'This train has been delayed';
|
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);
|
if (data) {
|
||||||
return data[0].cancReason || 'This train has been cancelled';
|
return data[0].cancReason || "This train has been cancelled";
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function getReason(code = '') {
|
async function getReason(code: string): Promise<ReasonCode[] | undefined> {
|
||||||
const url = `https://owlboard.info/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}
|
||||||
{reason}
|
{reason}
|
||||||
{: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}
|
||||||
{reason}
|
{reason}
|
||||||
{:catch}
|
{:catch}
|
||||||
This train has been delayed
|
This train has been delayed
|
||||||
{/await}
|
{/await}
|
||||||
{/if}
|
{/if}
|
||||||
|
55
src/lib/scripts/apiFetch.ts
Normal file
55
src/lib/scripts/apiFetch.ts
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
49
src/lib/scripts/featureDetect.ts
Normal file
49
src/lib/scripts/featureDetect.ts
Normal 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;
|
||||||
|
}
|
39
src/lib/scripts/getLocation.ts
Normal file
39
src/lib/scripts/getLocation.ts
Normal 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;
|
||||||
|
}
|
12
src/lib/scripts/upstream.ts
Normal file
12
src/lib/scripts/upstream.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { dev } from "$app/environment";
|
||||||
|
|
||||||
|
const testUrl: string = "http://localhost:8460";
|
||||||
|
const prodUrl: string = "https://owlboard.info";
|
||||||
|
|
||||||
|
export function getApiUrl() {
|
||||||
|
if (dev) {
|
||||||
|
console.info("DEVMODE active, using testing URL: ", testUrl);
|
||||||
|
return testUrl;
|
||||||
|
}
|
||||||
|
return prodUrl;
|
||||||
|
}
|
14
src/lib/stores/SetTelemetryConsent.ts
Normal file
14
src/lib/stores/SetTelemetryConsent.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import { get } from "svelte/store";
|
||||||
|
import { telemetry } from "./telemetryConsent";
|
||||||
|
|
||||||
|
export function setTelemetryTrue() {
|
||||||
|
telemetry.set(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setTelemetryFalse() {
|
||||||
|
telemetry.set(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getTelemetry(): boolean {
|
||||||
|
return get(telemetry);
|
||||||
|
}
|
25
src/lib/stores/location.ts
Normal file
25
src/lib/stores/location.ts
Normal 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));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
26
src/lib/stores/nearToMeCache.ts
Normal file
26
src/lib/stores/nearToMeCache.ts
Normal 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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -1,25 +1,25 @@
|
|||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
25
src/lib/stores/telemetryConsent.ts
Normal file
25
src/lib/stores/telemetryConsent.ts
Normal 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 telemetry: Writable<boolean> = writable(fromLocalStorage("telemetry", false));
|
||||||
|
toLocalStorage(telemetry, "telemetry");
|
||||||
|
|
||||||
|
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));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -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"],
|
||||||
['id', '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"],
|
||||||
]);
|
]);
|
||||||
|
@ -1,23 +1,23 @@
|
|||||||
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
return fallback;
|
||||||
return fallback;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function toLocalStorage(store, storageKey) {
|
function toLocalStorage(store, storageKey) {
|
||||||
if (browser) {
|
if (browser) {
|
||||||
store.subscribe((value) => {
|
store.subscribe((value) => {
|
||||||
localStorage.setItem(storageKey, value);
|
localStorage.setItem(storageKey, value);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,3 +1,2 @@
|
|||||||
export const version: string = '2023.8.3';
|
export const version: string = "2025.05.1";
|
||||||
export const versionTag: string = '';
|
export const versionTag: string = "";
|
||||||
export const showWelcome: boolean = false;
|
|
||||||
|
@ -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);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
60
src/lib/themes.css
Normal file
60
src/lib/themes.css
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
/* OLD THEME
|
||||||
|
:root {
|
||||||
|
--main-bg-color: #404c55;
|
||||||
|
--second-bg-color: #2b343c;
|
||||||
|
--accent-color: #007979;
|
||||||
|
--overlay-color: #3c6f79de;
|
||||||
|
--overlay-color-solid: #3c6f79;
|
||||||
|
--main-text-color: #00b7b7;
|
||||||
|
--second-text-color: #0afdfd;
|
||||||
|
--note-text-color: #9de7ff;
|
||||||
|
--link-color: azure;
|
||||||
|
--box-border-color: rgba(0,0,0,0.19);
|
||||||
|
--link-visited-color: azure;
|
||||||
|
--main-alert-color: #ed6d00;
|
||||||
|
--second-alert-color: #e77f00;
|
||||||
|
--main-warning-color: orange;
|
||||||
|
--board-name-color: #fcfc09;
|
||||||
|
}*/
|
||||||
|
|
||||||
|
/* Main Theme */
|
||||||
|
:root {
|
||||||
|
--main-bg-color: #404c55;
|
||||||
|
--second-bg-color: #2b343c;
|
||||||
|
--main-header-color: #dff3f3;
|
||||||
|
--main-text-color: #cce9e9;
|
||||||
|
--secondary-text-color: #02fcfc;
|
||||||
|
--main-link-color: #00b7b7;
|
||||||
|
--island-bg-color: #3c6f79de;
|
||||||
|
--island-bg-solid: #3c6f79;
|
||||||
|
--island-button-color: #404c55;
|
||||||
|
--island-link-color: #e1ebeb;
|
||||||
|
--island-header-color: #4fd1d1;
|
||||||
|
--island-text-color: #e1ebeb;
|
||||||
|
--overlay-island-bg-color: #3c6f79;
|
||||||
|
--box-shadow-color: rgba(0, 0, 0, 0.19);
|
||||||
|
--box-shadow: 2px 2px 4px rgba(0, 0, 0, 0.19);
|
||||||
|
--box-shadow-dark: 2px 2px 4px rgba(0, 0, 0, 0.392);
|
||||||
|
--main-alert-color: #ed6d00;
|
||||||
|
--second-alert-color: #e77f00;
|
||||||
|
--main-warning-color: orange;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Christmas Theme
|
||||||
|
:root {
|
||||||
|
--main-bg-color: #404c55;
|
||||||
|
--second-bg-color: #2b343c;
|
||||||
|
--main-header-color: #00ff00;
|
||||||
|
--main-text-color: #ff0000;
|
||||||
|
--main-link-color: #2bc500;
|
||||||
|
--island-bg-color: #9900ffde;
|
||||||
|
--island-bg-solid: #3c6f79;
|
||||||
|
--island-button-color: #404c55;
|
||||||
|
--island-link-color: #cce9e9;
|
||||||
|
--island-header-color: #4fd1d1;
|
||||||
|
--island-text-color: #cce9e9;
|
||||||
|
--overlay-island-bg-color: #3c6f79;
|
||||||
|
--box-shadow-color: rgba(0,0,0,0.19);
|
||||||
|
--box-shadow: 2px 2px 4px rgba(0,0,0,0.19);
|
||||||
|
}
|
||||||
|
*/
|
33
src/lib/train/pis-handler.svelte
Normal file
33
src/lib/train/pis-handler.svelte
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { OB_Pis_SimpleObject } from "@owlboard/ts-types";
|
||||||
|
|
||||||
|
export let pisObject: OB_Pis_SimpleObject;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if pisObject}
|
||||||
|
{#if typeof pisObject === "string" || typeof pisObject === "number"}
|
||||||
|
<span class="pis">PIS: {pisObject}</span>
|
||||||
|
{:else if pisObject["skipCount"] === 0}
|
||||||
|
<span class="pis">PIS: {pisObject.code}</span>
|
||||||
|
{:else if pisObject["skipCount"] > 0}
|
||||||
|
<span class="pis">PIS: {pisObject.code}</span>
|
||||||
|
<br />
|
||||||
|
<span class="pis-text"
|
||||||
|
>(skip {pisObject.skipType}{#if pisObject.skipCount > 1} {" " + pisObject.skipCount} stops{:else} stop{/if})</span
|
||||||
|
>
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.pis-text {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--main-text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pis {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--main-text-color);
|
||||||
|
}
|
||||||
|
</style>
|
@ -1,223 +1,223 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
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;
|
||||||
|
|
||||||
$: {
|
$: {
|
||||||
if (full) {
|
if (full) {
|
||||||
text = map.get(toc.toLowerCase()) || toc;
|
text = map.get(toc.toLowerCase()) || toc;
|
||||||
} else {
|
} else {
|
||||||
text = toc;
|
text = toc;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<span class={toc.toLocaleLowerCase()}>{text}</span>
|
<span class={toc.toLocaleLowerCase()}>{text}</span>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
span {
|
span {
|
||||||
padding: 4px;
|
padding: 4px;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
border-style: solid;
|
border-style: solid;
|
||||||
border-width: 1px;
|
border-width: 1px;
|
||||||
background-color: white;
|
background-color: white;
|
||||||
color: black;
|
color: black;
|
||||||
}
|
}
|
||||||
.gw {
|
.gw {
|
||||||
/* GWR */
|
/* GWR */
|
||||||
background-color: #07352d;
|
background-color: #07352d;
|
||||||
color: white;
|
color: white;
|
||||||
border-color: #041d18;
|
border-color: #041d18;
|
||||||
}
|
}
|
||||||
.sw,
|
.sw,
|
||||||
.il {
|
.il {
|
||||||
/* SWR & Island Line */
|
/* SWR & Island Line */
|
||||||
background-color: rgb(17, 23, 23);
|
background-color: rgb(17, 23, 23);
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
.nt {
|
.nt {
|
||||||
/* Northern */
|
/* Northern */
|
||||||
background-color: rgb(38, 34, 98);
|
background-color: rgb(38, 34, 98);
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
.aw {
|
.aw {
|
||||||
/* TfW */
|
/* TfW */
|
||||||
background-color: red;
|
background-color: red;
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
.cc {
|
.cc {
|
||||||
/* c2c */
|
/* c2c */
|
||||||
background-color: rgb(168, 25, 127);
|
background-color: rgb(168, 25, 127);
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
.cs {
|
.cs {
|
||||||
/* Caledonian Sleeper */
|
/* Caledonian Sleeper */
|
||||||
background-color: #033c3c;
|
background-color: #033c3c;
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
.ch {
|
.ch {
|
||||||
/* Chiltern Railways */
|
/* Chiltern Railways */
|
||||||
background-color: white;
|
background-color: white;
|
||||||
color: blue;
|
color: blue;
|
||||||
}
|
}
|
||||||
.xc {
|
.xc {
|
||||||
/* CrossCountry */
|
/* CrossCountry */
|
||||||
background-color: rgb(76, 18, 58);
|
background-color: rgb(76, 18, 58);
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
.em {
|
.em {
|
||||||
/* EMR */
|
/* EMR */
|
||||||
background-color: rgb(69, 29, 69);
|
background-color: rgb(69, 29, 69);
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
.es {
|
.es {
|
||||||
/* Eurostar */
|
/* Eurostar */
|
||||||
background-color: rgb(13, 13, 98);
|
background-color: rgb(13, 13, 98);
|
||||||
color: yellow;
|
color: yellow;
|
||||||
}
|
}
|
||||||
.zz {
|
.zz {
|
||||||
/* Freight, Charters, etc */
|
/* Freight, Charters, etc */
|
||||||
background-color: rgba(255, 255, 255, 0);
|
background-color: rgba(255, 255, 255, 0);
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
.ht {
|
.ht {
|
||||||
/* Hull Trains */
|
/* Hull Trains */
|
||||||
background-color: rgb(160, 0, 136);
|
background-color: rgb(160, 0, 136);
|
||||||
color: rgb(19, 19, 173);
|
color: rgb(19, 19, 173);
|
||||||
}
|
}
|
||||||
.gn {
|
.gn {
|
||||||
/* GTR Great Northern */
|
/* GTR Great Northern */
|
||||||
background-color: rgb(79, 0, 128);
|
background-color: rgb(79, 0, 128);
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
.tl {
|
.tl {
|
||||||
/* GTR Thameslink */
|
/* GTR Thameslink */
|
||||||
background-color: rgb(191, 25, 183);
|
background-color: rgb(191, 25, 183);
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
.gc {
|
.gc {
|
||||||
/* Grand Central */
|
/* Grand Central */
|
||||||
background-color: rgb(40, 40, 40);
|
background-color: rgb(40, 40, 40);
|
||||||
color: rgb(219, 123, 5);
|
color: rgb(219, 123, 5);
|
||||||
}
|
}
|
||||||
.le,
|
.le,
|
||||||
.ga {
|
.ga {
|
||||||
/*Greater Anglia */
|
/*Greater Anglia */
|
||||||
background-color: rgb(122, 124, 154);
|
background-color: rgb(122, 124, 154);
|
||||||
color: rgb(151, 0, 0);
|
color: rgb(151, 0, 0);
|
||||||
border-color: red;
|
border-color: red;
|
||||||
}
|
}
|
||||||
.hx {
|
.hx {
|
||||||
/* Heathrow Express */
|
/* Heathrow Express */
|
||||||
background-color: rgb(181, 142, 211);
|
background-color: rgb(181, 142, 211);
|
||||||
color: black;
|
color: black;
|
||||||
}
|
}
|
||||||
.ls {
|
.ls {
|
||||||
/* Locomotive Services Limited */
|
/* Locomotive Services Limited */
|
||||||
background-color: white;
|
background-color: white;
|
||||||
color: black;
|
color: black;
|
||||||
}
|
}
|
||||||
.lm {
|
.lm {
|
||||||
/* West Midlands Railway */
|
/* West Midlands Railway */
|
||||||
background-color: rgb(120, 120, 120);
|
background-color: rgb(120, 120, 120);
|
||||||
border-color: rgb(230, 150, 0);
|
border-color: rgb(230, 150, 0);
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
.lo {
|
.lo {
|
||||||
/*London Overground */
|
/*London Overground */
|
||||||
background-color: rgb(231, 150, 0);
|
background-color: rgb(231, 150, 0);
|
||||||
color: rgb(0, 0, 210);
|
color: rgb(0, 0, 210);
|
||||||
}
|
}
|
||||||
.lt {
|
.lt {
|
||||||
/* London Underground (when on Network Rail Metals) */
|
/* London Underground (when on Network Rail Metals) */
|
||||||
background-color: rgb(203, 17, 17);
|
background-color: rgb(203, 17, 17);
|
||||||
color: rgb(0, 0, 210);
|
color: rgb(0, 0, 210);
|
||||||
}
|
}
|
||||||
.me {
|
.me {
|
||||||
/* Merseyrail */
|
/* Merseyrail */
|
||||||
background-color: rgb(229, 229, 16);
|
background-color: rgb(229, 229, 16);
|
||||||
color: rgb(96, 96, 96);
|
color: rgb(96, 96, 96);
|
||||||
}
|
}
|
||||||
.lr {
|
.lr {
|
||||||
/* NR On-Track Machines */
|
/* NR On-Track Machines */
|
||||||
background-color: yellow;
|
background-color: yellow;
|
||||||
color: black;
|
color: black;
|
||||||
}
|
}
|
||||||
.tw {
|
.tw {
|
||||||
/* Tyne & Wear Metro (when on Network Rail Metals) */
|
/* Tyne & Wear Metro (when on Network Rail Metals) */
|
||||||
background-color: rgb(212, 169, 0);
|
background-color: rgb(212, 169, 0);
|
||||||
color: black;
|
color: black;
|
||||||
}
|
}
|
||||||
.sr {
|
.sr {
|
||||||
/* ScotRail */
|
/* ScotRail */
|
||||||
background-color: rgb(16, 16, 200);
|
background-color: rgb(16, 16, 200);
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
.sj {
|
.sj {
|
||||||
/* South Yorkshire (Sheffield) SuperTram (When on network rail metals) */
|
/* South Yorkshire (Sheffield) SuperTram (When on network rail metals) */
|
||||||
background-color: rgb(255, 185, 56);
|
background-color: rgb(255, 185, 56);
|
||||||
color: rgb(58, 58, 255);
|
color: rgb(58, 58, 255);
|
||||||
}
|
}
|
||||||
.se {
|
.se {
|
||||||
/* Southeastern */
|
/* Southeastern */
|
||||||
background-color: darkblue;
|
background-color: darkblue;
|
||||||
color: rgb(107, 152, 207);
|
color: rgb(107, 152, 207);
|
||||||
}
|
}
|
||||||
.sn {
|
.sn {
|
||||||
/* GTR (Southern) */
|
/* GTR (Southern) */
|
||||||
background-color: rgb(11, 74, 11);
|
background-color: rgb(11, 74, 11);
|
||||||
color: rgb(231, 231, 188);
|
color: rgb(231, 231, 188);
|
||||||
}
|
}
|
||||||
.xr {
|
.xr {
|
||||||
/* Elizabeth Line (EastWest CrossRail) */
|
/* Elizabeth Line (EastWest CrossRail) */
|
||||||
background-color: rgb(91, 0, 171);
|
background-color: rgb(91, 0, 171);
|
||||||
color: rgb(207, 207, 255);
|
color: rgb(207, 207, 255);
|
||||||
}
|
}
|
||||||
.tp {
|
.tp {
|
||||||
/* TransPennine Express */
|
/* TransPennine Express */
|
||||||
background-color: rgb(197, 130, 238);
|
background-color: rgb(197, 130, 238);
|
||||||
color: rgb(0, 98, 226);
|
color: rgb(0, 98, 226);
|
||||||
}
|
}
|
||||||
.vt {
|
.vt {
|
||||||
/* Avanti West Coast */
|
/* Avanti West Coast */
|
||||||
background-color: rgb(37, 37, 86);
|
background-color: rgb(37, 37, 86);
|
||||||
color: rgb(230, 96, 0);
|
color: rgb(230, 96, 0);
|
||||||
}
|
}
|
||||||
.gr {
|
.gr {
|
||||||
/* LNER */
|
/* LNER */
|
||||||
background-color: rgb(202, 0, 0);
|
background-color: rgb(202, 0, 0);
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
.wc {
|
.wc {
|
||||||
/* West Coast Railway (Spot Hire/Charter) */
|
/* West Coast Railway (Spot Hire/Charter) */
|
||||||
background-color: maroon;
|
background-color: maroon;
|
||||||
color: rgb(225, 190, 92);
|
color: rgb(225, 190, 92);
|
||||||
}
|
}
|
||||||
.ty {
|
.ty {
|
||||||
/* Vintage Trains (Tour Operator) */
|
/* Vintage Trains (Tour Operator) */
|
||||||
background-color: green;
|
background-color: green;
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
.ld {
|
.ld {
|
||||||
/* Lumo */
|
/* Lumo */
|
||||||
background-color: whitesmoke;
|
background-color: whitesmoke;
|
||||||
color: blue;
|
color: blue;
|
||||||
}
|
}
|
||||||
.so {
|
.so {
|
||||||
/* Rail Adventure */
|
/* Rail Adventure */
|
||||||
background-color: rgb(93, 93, 93);
|
background-color: rgb(93, 93, 93);
|
||||||
color: rgb(93, 195, 93);
|
color: rgb(93, 195, 93);
|
||||||
}
|
}
|
||||||
.ln {
|
.ln {
|
||||||
/* Grand Union Trains */
|
/* Grand Union Trains */
|
||||||
background-color: rgb(89, 89, 89);
|
background-color: rgb(89, 89, 89);
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
.uk {
|
.uk {
|
||||||
background-color: whitesmoke;
|
background-color: whitesmoke;
|
||||||
color: black;
|
color: black;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -1,162 +1,188 @@
|
|||||||
<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 PisHandler from "$lib/train/pis-handler.svelte";
|
||||||
|
|
||||||
export let service = '';
|
import type { OB_TrainTT_service } from "@owlboard/ts-types";
|
||||||
|
import TrainIcons from "./trainIcons.svelte";
|
||||||
|
|
||||||
let isExpanded = false;
|
export let service: OB_TrainTT_service;
|
||||||
|
export let date: Date;
|
||||||
|
|
||||||
async function getTrainByUID(tuid = '') {
|
let isExpanded = false;
|
||||||
const url = `https://owlboard.info/api/v2/timetable/train/now/byTrainUid/${tuid}`;
|
|
||||||
const options = {
|
async function getTrainByUID(tuid = "") {
|
||||||
method: 'GET',
|
const url = `${getApiUrl()}/api/v2/timetable/train/${date.toISOString().split('T')[0]}/byTrainUid/${tuid}`;
|
||||||
headers: {
|
const options = {
|
||||||
uuid: $uuid
|
method: "GET",
|
||||||
}
|
headers: {
|
||||||
};
|
uuid: $uuid,
|
||||||
const res = await fetch(url, options);
|
},
|
||||||
if (res.status === 200) {
|
};
|
||||||
return await res.json();
|
const res = await fetch(url, options);
|
||||||
} else {
|
if (res.status === 200) {
|
||||||
throw new Error('Unable to Fetch');
|
return await res.json();
|
||||||
|
} else {
|
||||||
|
throw new Error("Unable to Fetch");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
async function expand() {
|
async function expand() {
|
||||||
isExpanded = !isExpanded;
|
isExpanded = !isExpanded;
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- The next version of backend will only return a subset of information required to display the container-header
|
|
||||||
, the container will then be responsible for fetching more data using the trainUid. -->
|
|
||||||
<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>
|
|
||||||
{#if isExpanded}
|
|
||||||
<div class="container-detail" in:fly={{ y: -20, duration: 200 }}>
|
|
||||||
{#await getTrainByUID(service.trainUid)}
|
|
||||||
<LoadingText />
|
|
||||||
{:then serviceDetail}
|
|
||||||
<div class="detailOperator"><StylesToc toc={service?.operator || ''} full={true} /></div>
|
|
||||||
{#if serviceDetail.pis}
|
|
||||||
<p class="pis">PIS: {serviceDetail.pis}</p>
|
|
||||||
{/if}
|
|
||||||
<p class="svc-detail">
|
|
||||||
Planned Type: {parseInt(serviceDetail.planSpeed) || '--'}mph {serviceDetail.powerType || 'Non-Rail vehicle'}
|
|
||||||
</p>
|
|
||||||
<p class="svc-detail">
|
|
||||||
Days Run: {serviceDetail?.daysRun.join(', ').toUpperCase() || 'Unknown'}
|
|
||||||
</p>
|
|
||||||
<p class="svc-detail validity">
|
|
||||||
Valid From: {new Date(serviceDetail.scheduleStartDate).toLocaleDateString('en-GB', {
|
|
||||||
timeZone: 'UTC'
|
|
||||||
})} - {new Date(serviceDetail.scheduleEndDate).toLocaleDateString('en-GB', {
|
|
||||||
timeZone: 'UTC'
|
|
||||||
})}
|
|
||||||
</p>
|
|
||||||
<table>
|
|
||||||
<tr>
|
|
||||||
<th>Location</th>
|
|
||||||
<th>Sch Arr.</th>
|
|
||||||
<th>Sch Dep.</th>
|
|
||||||
</tr>
|
|
||||||
{#if serviceDetail.stops[0]['publicDeparture']}
|
|
||||||
{#each serviceDetail.stops as stop}
|
|
||||||
{#if stop.publicArrival || stop.publicDeparture}
|
|
||||||
<tr>
|
|
||||||
<td>{stop.tiploc}</td>
|
|
||||||
<td>{stop.publicArrival || '-'}</td>
|
|
||||||
<td>{stop.publicDeparture || '-'}</td>
|
|
||||||
</tr>
|
|
||||||
{/if}
|
|
||||||
{/each}
|
|
||||||
{:else}
|
|
||||||
{#each serviceDetail.stops as stop}
|
|
||||||
<tr>
|
|
||||||
<td>{stop.tiploc}</td>
|
|
||||||
<td>{stop.wttArrival || '-'}</td>
|
|
||||||
<td>{stop.wttDeparture || '-'}</td>
|
|
||||||
</tr>
|
|
||||||
{/each}
|
|
||||||
{/if}
|
|
||||||
</table>
|
|
||||||
{:catch}
|
|
||||||
<p>Unable to fetch train data</p>
|
|
||||||
{/await}
|
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{#if isExpanded}
|
||||||
|
<div class="container-detail" in:fly={{ y: -20, duration: 200 }}>
|
||||||
|
{#await getTrainByUID(service.trainUid)}
|
||||||
|
<LoadingText />
|
||||||
|
{:then serviceDetail}
|
||||||
|
{#if serviceDetail.stpIndicator === "C"}
|
||||||
|
<p class="text-message">This has been removed from the timetable for today.</p>
|
||||||
|
<p class="text-message">The service may have been retimed, re-routed or removed from todays timetable completely.</p>
|
||||||
|
<p class="text-message">If it has been retimed or re-routed, there is likely to be another service with the same headcode booked to run.</p>
|
||||||
|
{:else}
|
||||||
|
<div class="detailOperator"><StylesToc toc={service?.operator || ""} full={true} /></div>
|
||||||
|
|
||||||
|
<TrainIcons serviceDetails={serviceDetail.serviceDetail} />
|
||||||
|
{#if serviceDetail.pis}
|
||||||
|
<PisHandler pisObject={serviceDetail.pis} />
|
||||||
|
{/if}
|
||||||
|
<p class="svc-detail">
|
||||||
|
Planned Type: {parseInt(serviceDetail.planSpeed) || "--"}mph {serviceDetail.powerType || "Non-Rail vehicle"}
|
||||||
|
</p>
|
||||||
|
<p class="svc-detail">
|
||||||
|
Days Run: {serviceDetail?.daysRun.join(", ").toUpperCase() || "Unknown"}
|
||||||
|
</p>
|
||||||
|
<p class="svc-detail validity">
|
||||||
|
Valid From: {new Date(serviceDetail.scheduleStart).toLocaleDateString("en-GB", {
|
||||||
|
timeZone: "UTC",
|
||||||
|
})} - {new Date(serviceDetail.scheduleEnd).toLocaleDateString("en-GB", {
|
||||||
|
timeZone: "UTC",
|
||||||
|
})}
|
||||||
|
</p>
|
||||||
|
<table>
|
||||||
|
<caption>Italics are 'pass' times, grey times are non-passenger stops</caption>
|
||||||
|
<tr>
|
||||||
|
<th>Location</th>
|
||||||
|
<th>Plt.</th>
|
||||||
|
<th>Sch Arr.</th>
|
||||||
|
<th>Sch Dep.</th>
|
||||||
|
</tr>
|
||||||
|
{#each serviceDetail.stops as stop}
|
||||||
|
<tr>
|
||||||
|
{#if stop.publicArrival || stop.publicDeparture}
|
||||||
|
<td>{stop.tiploc}</td>
|
||||||
|
<td>{stop.platform || "-"}</td>
|
||||||
|
<td>{stop.publicArrival || "-"}</td>
|
||||||
|
<td>{stop.publicDeparture || "-"}</td>
|
||||||
|
{:else if stop.wttArrival || stop.wttDeparture}
|
||||||
|
<td class="wtt">{stop.tiploc}</td>
|
||||||
|
<td class="wtt">{stop.platform || stop.depLine || stop.arrLine || "-"}</td>
|
||||||
|
<td class="wtt">{stop.wttArrival || "-"}</td>
|
||||||
|
<td class="wtt">{stop.wttDeparture || "-"}</td>
|
||||||
|
{:else}
|
||||||
|
<td class="pass">{stop.tiploc}</td>
|
||||||
|
<td class="pass">{stop.platform || stop.depLine || stop.arrLine || "-"}</td>
|
||||||
|
<td class="pass">-</td>
|
||||||
|
<td class="pass">{stop.pass || "-"}</td>
|
||||||
|
{/if}
|
||||||
|
</tr>
|
||||||
|
{/each}
|
||||||
|
</table>
|
||||||
|
{/if}
|
||||||
|
{:catch}
|
||||||
|
<p>Unable to fetch train data</p>
|
||||||
|
{/await}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.container {
|
.container {
|
||||||
position: relative;
|
position: relative;
|
||||||
margin: auto;
|
margin: auto;
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
width: 95%;
|
width: 95%;
|
||||||
max-width: 600px;
|
max-width: 600px;
|
||||||
height: auto;
|
height: auto;
|
||||||
background-color: var(--overlay-color);
|
background-color: var(--island-bg-solid);
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
transition: height 500ms ease-in-out;
|
transition: height 500ms ease-in-out;
|
||||||
}
|
box-shadow: 5px 5px 30px var(--box-shadow-color);
|
||||||
.container-header {
|
}
|
||||||
text-align: left;
|
.container-header {
|
||||||
padding-left: 10px;
|
text-align: left;
|
||||||
font-size: 18px;
|
padding-left: 10px;
|
||||||
font-weight: 600;
|
font-size: 18px;
|
||||||
padding-top: 12px;
|
font-weight: 600;
|
||||||
padding-bottom: 10px;
|
padding-top: 12px;
|
||||||
font-family: ubuntu, monospace;
|
padding-bottom: 10px;
|
||||||
color: white;
|
font-family: ubuntu, monospace;
|
||||||
}
|
color: var(--island-text-color);
|
||||||
#container-arrow {
|
}
|
||||||
font-family: Arial, Helvetica, sans-serif;
|
#container-arrow {
|
||||||
font-weight: 600;
|
font-family: Arial, Helvetica, sans-serif;
|
||||||
color: white;
|
font-weight: 600;
|
||||||
margin: 0;
|
color: var(--island-text-color);
|
||||||
padding: 0;
|
margin: 0;
|
||||||
position: absolute;
|
padding: 0;
|
||||||
right: 16px;
|
position: absolute;
|
||||||
top: 10px;
|
right: 16px;
|
||||||
transition-duration: 300ms;
|
top: 10px;
|
||||||
z-index: 2;
|
transition-duration: 300ms;
|
||||||
}
|
z-index: 2;
|
||||||
.isExpanded {
|
}
|
||||||
transform: rotate(180deg);
|
.isExpanded {
|
||||||
}
|
transform: rotate(180deg);
|
||||||
.detailOperator {
|
}
|
||||||
padding-top: 15px;
|
.detailOperator {
|
||||||
margin-bottom: 15px;
|
padding-top: 15px;
|
||||||
font-weight: 600;
|
margin-bottom: 15px;
|
||||||
}
|
font-weight: 600;
|
||||||
.pis {
|
}
|
||||||
font-size: 18px;
|
.svc-detail {
|
||||||
font-weight: 600;
|
margin-top: 6px;
|
||||||
color: azure;
|
margin-bottom: 2px;
|
||||||
margin-top: 10px;
|
color: var(--island-text-color);
|
||||||
margin-bottom: 5px;
|
}
|
||||||
}
|
.validity {
|
||||||
.svc-detail {
|
font-size: 14px;
|
||||||
margin-top: 2px;
|
}
|
||||||
margin-bottom: 2px;
|
table {
|
||||||
color: white;
|
margin: auto;
|
||||||
}
|
padding-top: 5px;
|
||||||
.validity {
|
padding-bottom: 10px;
|
||||||
font-size: 14px;
|
color: var(--island-text-color);
|
||||||
}
|
}
|
||||||
table {
|
caption {
|
||||||
margin: auto;
|
padding-top: 15px;
|
||||||
padding-top: 10px;
|
font-size: small;
|
||||||
padding-bottom: 10px;
|
}
|
||||||
color: white;
|
.wtt {
|
||||||
}
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
.pass {
|
||||||
|
font-style: italic;
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
.text-message {
|
||||||
|
margin: 5px;
|
||||||
|
margin-left: 20px;
|
||||||
|
margin-right: 20px;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
39
src/lib/train/trainIcons.svelte
Normal file
39
src/lib/train/trainIcons.svelte
Normal 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>
|
@ -1,25 +1,29 @@
|
|||||||
<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>
|
||||||
|
|
||||||
|
<svelte:head>
|
||||||
|
<meta name="robots" content="noindex">
|
||||||
|
</svelte:head>
|
||||||
|
|
||||||
<Header {title} />
|
<Header {title} />
|
||||||
|
|
||||||
<h1>{$page.status}: {$page?.error?.message}</h1>
|
<h1>{$page.status}: {$page?.error?.message}</h1>
|
||||||
|
|
||||||
{#if $page.status === 404}
|
{#if $page.status === 404}
|
||||||
<p>This is not the page you're looking for.</p>
|
<p>There's no light at the end of this tunnel</p>
|
||||||
<p>The page you are looking for doesn't exist, use the tabs below to find another page.</p>
|
<p>The page you are looking for doesn't exist, use the tabs below to find another page.</p>
|
||||||
{:else if $page.status === 500}
|
{:else if $page.status === 500}
|
||||||
<p>
|
<p>
|
||||||
Something went wrong loading the app.<br />
|
Something went wrong loading the app.<br />
|
||||||
Try going <a href="/">home</a> and try again.
|
Try going <a href="/">home</a> and try again.
|
||||||
</p>
|
</p>
|
||||||
<p>If the problem persists, you can report an issue from the 'More' menu.</p>
|
<p>If the problem persists, you can report an issue from the 'More' menu.</p>
|
||||||
{:else}
|
{:else}
|
||||||
<p>Not sure what went wrong, please try again later or report an issue from the 'More' menu.</p>
|
<p>Not sure what went wrong, please try again later or report an issue from the 'More' menu.</p>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<Nav />
|
<Nav />
|
||||||
|
@ -1,24 +1,45 @@
|
|||||||
<script>
|
<script>
|
||||||
import '../app.css';
|
import "$lib/themes.css";
|
||||||
import { dev } from '$app/environment';
|
import "$lib/main.css";
|
||||||
import DevBanner from '$lib/DevBanner.svelte';
|
import { dev } from "$app/environment";
|
||||||
|
import DevBanner from "$lib/DevBanner.svelte";
|
||||||
|
import { page } from "$app/stores";
|
||||||
|
import { Toaster } from "svelte-french-toast";
|
||||||
|
import AnalyticsConsent from "$lib/popover/analytics-consent.svelte";
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
<meta name="application-name" content="OwlBoard" />
|
<!--
|
||||||
<meta name="author" content="Frederick Boniface" />
|
___ _ ___ _
|
||||||
<meta
|
/ _ \__ __ _| | _ ) ___ __ _ _ _ __| |
|
||||||
name="description"
|
| (_) \ V V / | _ \/ _ \/ _` | '_/ _` |
|
||||||
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."
|
\___/ \_/\_/|_|___/\___/\__,_|_| \__,_|
|
||||||
/>
|
Check out the source code on Gitea (https://git.fjla.net/OwlBoard)
|
||||||
<meta name="viewport" content="width=device-width" />
|
It's easier to read there
|
||||||
<meta name="theme-color" content="#00b7b7" />
|
-->
|
||||||
<link rel="icon" href="/images/icon.svg" type="image/svg+xml" />
|
|
||||||
<link rel="apple-touch-icon" href="/images/app-icons/any/apple-192.png" />
|
<meta name="application-name" content="OwlBoard" />
|
||||||
<link rel="manifest" href="/manifest.json" />
|
<meta name="author" content="Frederick Boniface" />
|
||||||
<title>OwlBoard</title>
|
<meta
|
||||||
|
name="description"
|
||||||
|
content="Get instant access to live train data, PIS codes, and location reference codes. Built by railway staff, for railway staff - your fastest route to accurate information."
|
||||||
|
/>
|
||||||
|
<meta name="viewport" content="width=device-width" />
|
||||||
|
<meta name="theme-color" content="#00b7b7" />
|
||||||
|
<link rel="canonical" href="https://owlboard.info{$page.url.pathname}" />
|
||||||
|
<link rel="icon" href="/images/icon.svg" type="image/svg+xml" />
|
||||||
|
<link rel="apple-touch-icon" href="/images/app-icons/any/apple-192.png" />
|
||||||
|
<link rel="manifest" href="/manifest.json" />
|
||||||
|
<title>OwlBoard</title>
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
|
<Toaster />
|
||||||
|
<AnalyticsConsent />
|
||||||
|
|
||||||
{#if dev}
|
{#if dev}
|
||||||
<DevBanner />
|
<DevBanner />
|
||||||
{/if}
|
{/if}
|
||||||
<slot />
|
<slot />
|
||||||
|
|
||||||
|
<style>
|
||||||
|
</style>
|
||||||
|
@ -1,38 +1,42 @@
|
|||||||
<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";
|
||||||
|
import QuickLinkCard from "$lib/cards/QuickLinkCard.svelte";
|
||||||
|
import FindByHeadcodeCard from "$lib/cards/FindByHeadcodeCard.svelte";
|
||||||
|
const title = "OwlBoard";
|
||||||
|
const lookupCards: LookupCardConfig[] = [
|
||||||
|
{
|
||||||
|
title: "Live Arr/Dep Boards",
|
||||||
|
helpText: "",
|
||||||
|
formAction: "/ldb",
|
||||||
|
placeholder: "enter crs/tiploc",
|
||||||
|
maxLen: 7,
|
||||||
|
fieldName: "station",
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
const title = 'OwlBoard';
|
onMount(async () => {
|
||||||
const inputIslands = [
|
const featureSupport = featureDetect();
|
||||||
{
|
if (!featureSupport.critical) {
|
||||||
title: 'Live Departure Boards',
|
toast.error("Use a newer browser or OwlBoard might not work properly. See `Menu > Statistics` for more information.");
|
||||||
action: '/ldb',
|
} else if (!featureSupport.nice) {
|
||||||
placeholder: 'Enter CRS/TIPLOC',
|
toast.error("Use a newer browser for the best experience, see `Menu > Statistics` for more information.");
|
||||||
queryName: 'station'
|
}
|
||||||
},
|
});
|
||||||
{
|
|
||||||
title: 'Train Details & PIS',
|
|
||||||
action: '/train',
|
|
||||||
placeholder: 'Enter Headcode',
|
|
||||||
queryName: 'headcode'
|
|
||||||
}
|
|
||||||
];
|
|
||||||
</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}
|
||||||
|
<FindByHeadcodeCard />
|
||||||
<QuickLinkIsland />
|
<NearToMeCard />
|
||||||
|
<QuickLinkCard />
|
||||||
|
|
||||||
<Nav />
|
<Nav />
|
||||||
|
27
src/routes/err/404/+page.svelte
Normal file
27
src/routes/err/404/+page.svelte
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import Header from "$lib/navigation/header.svelte";
|
||||||
|
import Nav from "$lib/navigation/nav.svelte";
|
||||||
|
|
||||||
|
const title = "404 - Not Found";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:head>
|
||||||
|
<meta name="robots" content="noindex">
|
||||||
|
</svelte:head>
|
||||||
|
|
||||||
|
<Header {title} />
|
||||||
|
<h1 class="heading">There's no light at the end of this tunnel</h1>
|
||||||
|
<p>The page you were looking for wasn't found</p>
|
||||||
|
<p>Use the menu bar to try another destination</p>
|
||||||
|
<br />
|
||||||
|
<p class="err_code">Status: 404<br />Message: NOT_FOUND</p>
|
||||||
|
<Nav />
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.heading {
|
||||||
|
color: var(--second-text-color);
|
||||||
|
}
|
||||||
|
.err_code {
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
</style>
|
27
src/routes/err/50x/+page.svelte
Normal file
27
src/routes/err/50x/+page.svelte
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import Header from "$lib/navigation/header.svelte";
|
||||||
|
import Nav from "$lib/navigation/nav.svelte";
|
||||||
|
|
||||||
|
const title = "50x - Server Error";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:head>
|
||||||
|
<meta name="robots" content="noindex">
|
||||||
|
</svelte:head>
|
||||||
|
|
||||||
|
<Header {title} />
|
||||||
|
<h1 class="heading">This page has been delayed by more servers than usual needing repairs at the same time</h1>
|
||||||
|
<p>There was an error with the server, please try again later</p>
|
||||||
|
<p>Use the menu bar to try another destination, you can report an issue from the 'Menu'</p>
|
||||||
|
<br />
|
||||||
|
<p class="err_code">Status: 50x<br />Message: INTERNAL_SERVER_ERROR</p>
|
||||||
|
<Nav />
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.heading {
|
||||||
|
color: var(--second-text-color);
|
||||||
|
}
|
||||||
|
.err_code {
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
</style>
|
@ -1,42 +1,49 @@
|
|||||||
<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 staff = false;
|
|
||||||
let uuidValue = '';
|
|
||||||
|
|
||||||
$: uuidValue = $uuid;
|
|
||||||
|
|
||||||
onMount(async () => {
|
|
||||||
station = (await getHeadcode()) || '';
|
|
||||||
if (uuidValue !== null && uuidValue !== '' && uuidValue !== 'null') {
|
|
||||||
staff = true;
|
|
||||||
title = 'Staff Board';
|
|
||||||
} else {
|
|
||||||
title = 'Public Board';
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
let station: string;
|
||||||
|
let staff: boolean;
|
||||||
|
let uuidValue: string;
|
||||||
|
let blockLoading: boolean = true;
|
||||||
|
|
||||||
|
$: uuidValue = $uuid;
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
station = (await getHeadcode()) || "";
|
||||||
|
if (uuidValue !== null && uuidValue !== "" && uuidValue !== "null") {
|
||||||
|
staff = true;
|
||||||
|
title = "Staff Board";
|
||||||
|
} else {
|
||||||
|
title = "Public Board";
|
||||||
|
}
|
||||||
|
|
||||||
|
blockLoading = false;
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<svelte:head>
|
||||||
|
<meta name="robots" content="noindex">
|
||||||
|
</svelte:head>
|
||||||
|
|
||||||
<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 />
|
||||||
|
14
src/routes/maintenance-mode/+page.svelte
Normal file
14
src/routes/maintenance-mode/+page.svelte
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
<script>
|
||||||
|
import LargeLogo from "$lib/images/large-logo.svelte";
|
||||||
|
|
||||||
|
</script>
|
||||||
|
<svelte:head>
|
||||||
|
<meta name="robots" content="noindex">
|
||||||
|
</svelte:head>
|
||||||
|
<LargeLogo />
|
||||||
|
<h1>
|
||||||
|
OwlBoard is down for maintenance
|
||||||
|
</h1>
|
||||||
|
<p>
|
||||||
|
Maintenance is expected to be complete by 23:59 on 22/11/2024
|
||||||
|
</p>
|
@ -1,68 +1,92 @@
|
|||||||
<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 {
|
||||||
|
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: "Help", path: "https://docs.owlboard.info", icon: IconHelp },
|
||||||
{ title: 'Registration', path: '/more/reg' },
|
{ title: "Settings", path: "/more/settings", icon: IconSettings },
|
||||||
{ title: 'Settings', path: '/more/settings' },
|
{ title: "Your Data", path: "/more/data", icon: IconUser },
|
||||||
{ title: 'Report Issue', path: '/more/report' },
|
{ title: "About", path: "/more/about", icon: IconInfoCircle },
|
||||||
{ title: 'About', path: '/more/about' },
|
{ title: "Registration", path: "/more/reg", icon: IconUserPlus },
|
||||||
{ 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>
|
||||||
|
|
||||||
|
<svelte:head>
|
||||||
|
<meta name="robots" content="noindex">
|
||||||
|
</svelte:head>
|
||||||
|
|
||||||
<Header {title} />
|
<Header {title} />
|
||||||
|
|
||||||
{#each links as item}
|
{#each links as item}
|
||||||
<a href={item.path}>
|
<a href={item.path}>
|
||||||
<div>
|
<div>
|
||||||
<p>{item.title}</p>
|
<svelte:component this={item.icon} />
|
||||||
</div>
|
<p>{item.title}</p>
|
||||||
</a>
|
</div>
|
||||||
|
</a>
|
||||||
{/each}
|
{/each}
|
||||||
|
|
||||||
<Nav />
|
<Nav />
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
div {
|
div {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
background: rgba(0, 0, 0, 0.226);
|
background: rgba(0, 0, 0, 0.226);
|
||||||
border-color: aliceblue;
|
border-color: aliceblue;
|
||||||
border-width: 1px;
|
border-width: 1px;
|
||||||
border-style: solid;
|
border-style: solid;
|
||||||
border-left: none;
|
border-left: none;
|
||||||
border-right: none;
|
padding-left: 0.5rem;
|
||||||
height: 50px;
|
overflow-x: hidden;
|
||||||
}
|
border-right: none;
|
||||||
a {
|
height: 50px;
|
||||||
text-decoration: none;
|
display: flex;
|
||||||
height: 100%;
|
align-items: center;
|
||||||
vertical-align: middle;
|
margin-bottom: 0.5rem;
|
||||||
}
|
|
||||||
p {
|
|
||||||
color: white;
|
|
||||||
margin: 0;
|
|
||||||
padding-left: 10px;
|
|
||||||
text-align: left;
|
|
||||||
line-height: 50px;
|
|
||||||
vertical-align: middle;
|
|
||||||
font-weight: 600;
|
|
||||||
font-size: 20px;
|
|
||||||
}
|
|
||||||
@media (min-width: 600px) {
|
|
||||||
p {
|
|
||||||
text-align: center;
|
|
||||||
}
|
}
|
||||||
}
|
a {
|
||||||
@media (max-width: 380px) {
|
text-decoration: none;
|
||||||
p {
|
height: 100%;
|
||||||
font-size: 16px;
|
vertical-align: middle;
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
p {
|
||||||
|
color: white;
|
||||||
|
margin: 0;
|
||||||
|
padding-left: 10px;
|
||||||
|
text-align: left;
|
||||||
|
line-height: 50px;
|
||||||
|
vertical-align: middle;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 20px;
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
@media (min-width: 600px) {
|
||||||
|
p {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media (max-width: 380px) {
|
||||||
|
p {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
@ -1,31 +1,31 @@
|
|||||||
<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} />
|
||||||
<LargeLogo />
|
<LargeLogo />
|
||||||
<p class="neg">© 2022-2023 Frederick Boniface</p>
|
<p class="neg">© 2022-2025 Frederick Boniface</p>
|
||||||
<p>OwlBoard was created by train crew for train crew.</p>
|
<p>OwlBoard was created by train crew for train crew.</p>
|
||||||
<p>I developed OwlBoard in 2022 with the aim of providing fast and easy access to the information we need on a daily basis.</p>
|
<p>I developed OwlBoard in 2022 with the aim of providing fast and easy access to the information we need on a daily basis.</p>
|
||||||
<p>Data is sourced from National Rail Enquiries, the OwlBoard Project, and Network Rail.</p>
|
<p>Data is sourced from National Rail Enquiries, the OwlBoard Project, and Network Rail.</p>
|
||||||
<p>
|
<p>
|
||||||
OwlBoard components are available under various Open Source licenses. Please refer to the
|
OwlBoard components are available under various Open Source licenses. Please refer to the
|
||||||
<a href="https://git.fjla.uk/OwlBoard" target="_blank">code repository</a>
|
<a href="https://git.fjla.uk/OwlBoard" target="_blank">code repository</a>
|
||||||
for more detailed information.
|
for more detailed information.
|
||||||
</p>
|
</p>
|
||||||
<Nav />
|
<Nav />
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
p {
|
p {
|
||||||
margin-left: 10px;
|
margin-left: 10px;
|
||||||
margin-right: 10px;
|
margin-right: 10px;
|
||||||
}
|
}
|
||||||
.neg {
|
.neg {
|
||||||
margin-top: -40px;
|
margin-top: -40px;
|
||||||
margin-bottom: 40px;
|
margin-bottom: 40px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -1,77 +1,78 @@
|
|||||||
<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";
|
||||||
const title = 'Location Codes';
|
import { getApiUrl } from "$lib/scripts/upstream";
|
||||||
|
const title = "Location Codes";
|
||||||
|
|
||||||
let val = {
|
let val = {
|
||||||
crs: '',
|
crs: "",
|
||||||
tiploc: '',
|
tiploc: "",
|
||||||
stanox: '',
|
stanox: "",
|
||||||
nlc: '',
|
nlc: "",
|
||||||
name: '',
|
name: "",
|
||||||
uic: ''
|
uic: "",
|
||||||
};
|
|
||||||
|
|
||||||
let isLoading = false;
|
|
||||||
|
|
||||||
async function getData(type = '', value = '') {
|
|
||||||
const url = `https://owlboard.info/api/v2/ref/locationCode/${type}/${value}`;
|
|
||||||
const res = await fetch(url);
|
|
||||||
const data = await res.json();
|
|
||||||
isLoading = false;
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function processData(data) {
|
|
||||||
//console.log("data",JSON.stringify(data))
|
|
||||||
if (data.ERROR == 'Offline') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
val = {
|
|
||||||
crs: data[0]['3ALPHA'] || 'None',
|
|
||||||
tiploc: data[0]['TIPLOC'] || 'None',
|
|
||||||
stanox: data[0]['STANOX'] || 'None',
|
|
||||||
nlc: data[0]['NLC'] || 'None',
|
|
||||||
name: data[0]['NLCDESC'] || 'None',
|
|
||||||
uic: data[0]['UIC'] || 'None'
|
|
||||||
};
|
};
|
||||||
//console.log("val",JSON.stringify(val));
|
|
||||||
}
|
|
||||||
|
|
||||||
async function submit() {
|
let isLoading = false;
|
||||||
isLoading = true;
|
|
||||||
let data = [];
|
async function getData(type = "", value = "") {
|
||||||
if (val?.crs) {
|
const url = `${getApiUrl()}/api/v2/ref/locationCode/${type}/${value}`;
|
||||||
data = await getData('crs', val.crs);
|
const res = await fetch(url);
|
||||||
} else if (val?.tiploc) {
|
const data = await res.json();
|
||||||
data = await getData('tiploc', val.tiploc);
|
isLoading = false;
|
||||||
} else if (val?.stanox) {
|
return data;
|
||||||
data = await getData('stanox', val.stanox);
|
|
||||||
} else if (val?.nlc) {
|
|
||||||
data = await getData('nlc', val.nlc);
|
|
||||||
} else {
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
processData(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function reset() {
|
async function processData(data) {
|
||||||
val = {
|
//console.log("data",JSON.stringify(data))
|
||||||
crs: '',
|
if (data.ERROR == "Offline") {
|
||||||
tiploc: '',
|
return;
|
||||||
stanox: '',
|
}
|
||||||
nlc: '',
|
val = {
|
||||||
name: '',
|
crs: data[0]["3ALPHA"] || "None",
|
||||||
uic: ''
|
tiploc: data[0]["TIPLOC"] || "None",
|
||||||
};
|
stanox: data[0]["STANOX"] || "None",
|
||||||
}
|
nlc: data[0]["NLC"] || "None",
|
||||||
|
name: data[0]["NLCDESC"] || "None",
|
||||||
|
uic: data[0]["UIC"] || "None",
|
||||||
|
};
|
||||||
|
//console.log("val",JSON.stringify(val));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function submit() {
|
||||||
|
isLoading = true;
|
||||||
|
let data = [];
|
||||||
|
if (val?.crs) {
|
||||||
|
data = await getData("crs", val.crs);
|
||||||
|
} else if (val?.tiploc) {
|
||||||
|
data = await getData("tiploc", val.tiploc);
|
||||||
|
} else if (val?.stanox) {
|
||||||
|
data = await getData("stanox", val.stanox);
|
||||||
|
} else if (val?.nlc) {
|
||||||
|
data = await getData("nlc", val.nlc);
|
||||||
|
} else {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
processData(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function reset() {
|
||||||
|
val = {
|
||||||
|
crs: "",
|
||||||
|
tiploc: "",
|
||||||
|
stanox: "",
|
||||||
|
nlc: "",
|
||||||
|
name: "",
|
||||||
|
uic: "",
|
||||||
|
};
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Header {title} />
|
<Header {title} />
|
||||||
|
|
||||||
{#if isLoading}
|
{#if isLoading}
|
||||||
<Loading />
|
<Loading />
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<p>Enter one of the codes below and press submit.</p>
|
<p>Enter one of the codes below and press submit.</p>
|
||||||
@ -79,57 +80,59 @@
|
|||||||
<p class="desc">Some locations only have some applicable location codes.</p>
|
<p class="desc">Some locations only have some applicable location codes.</p>
|
||||||
|
|
||||||
<div class="inputs">
|
<div class="inputs">
|
||||||
<form on:submit={submit}>
|
<form on:submit={submit}>
|
||||||
<input type="text" readonly placeholder="Name" bind:value={val.name} />
|
<input type="text" readonly placeholder="Name" bind:value={val.name} />
|
||||||
<br />
|
<br />
|
||||||
<input type="text" maxlength="3" autocomplete="off" placeholder="CRS/3ALPHA" bind:value={val.crs} />
|
<input type="text" maxlength="3" autocomplete="off" placeholder="CRS/3ALPHA" bind:value={val.crs} />
|
||||||
<br />
|
<br />
|
||||||
<input type="text" maxlength="7" autocomplete="off" placeholder="TIPLOC" bind:value={val.tiploc} />
|
<input type="text" maxlength="7" autocomplete="off" placeholder="TIPLOC" bind:value={val.tiploc} />
|
||||||
<br />
|
<br />
|
||||||
<input type="text" maxlength="10" autocomplete="off" placeholder="STANOX" bind:value={val.stanox} />
|
<input type="text" maxlength="10" autocomplete="off" placeholder="STANOX" bind:value={val.stanox} />
|
||||||
<br />
|
<br />
|
||||||
<input type="number" maxlength="6" min="0" autocomplete="off" placeholder="NLC" bind:value={val.nlc} />
|
<input type="number" maxlength="6" min="0" autocomplete="off" placeholder="NLC" bind:value={val.nlc} />
|
||||||
<br />
|
<br />
|
||||||
<input type="text" readonly autocomplete="off" placeholder="UIC" bind:value={val.uic} />
|
<input type="text" readonly autocomplete="off" placeholder="UIC" bind:value={val.uic} />
|
||||||
<br />
|
<br />
|
||||||
<button type="submit">Submit</button>
|
<button type="submit">Submit</button>
|
||||||
<button type="reset" on:click={reset}>Reset</button>
|
<button type="reset" on:click={reset}>Reset</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Nav />
|
<Nav />
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
p {
|
p {
|
||||||
margin: 10px;
|
margin: 10px;
|
||||||
}
|
}
|
||||||
input {
|
input {
|
||||||
font-family: urwgothic, sans-serif;
|
font-family: urwgothic, sans-serif;
|
||||||
border-radius: 50px;
|
border-radius: 50px;
|
||||||
border: none;
|
border: none;
|
||||||
width: 60%;
|
width: 60%;
|
||||||
max-width: 200px;
|
max-width: 200px;
|
||||||
min-width: 130px;
|
min-width: 130px;
|
||||||
height: 30px;
|
height: 30px;
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
}
|
box-shadow: var(--box-shadow);
|
||||||
button {
|
}
|
||||||
border-radius: 50px;
|
button {
|
||||||
font-family: urwgothic, sans-serif;
|
border-radius: 50px;
|
||||||
background-color: var(--overlay-color);
|
font-family: urwgothic, sans-serif;
|
||||||
border: none;
|
background-color: var(--island-bg-color);
|
||||||
color: white;
|
border: none;
|
||||||
height: 30px;
|
color: white;
|
||||||
width: 20%;
|
height: 30px;
|
||||||
max-width: 100px;
|
width: 20%;
|
||||||
min-width: 75px;
|
max-width: 100px;
|
||||||
font-size: 18px;
|
min-width: 75px;
|
||||||
}
|
font-size: 18px;
|
||||||
.desc {
|
box-shadow: var(--box-shadow);
|
||||||
color: white;
|
}
|
||||||
}
|
.desc {
|
||||||
input {
|
color: white;
|
||||||
text-align: center;
|
}
|
||||||
}
|
input {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -1,39 +1,44 @@
|
|||||||
<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 { uuid } from '$lib/stores/uuid.js';
|
import { getApiUrl } from "$lib/scripts/upstream";
|
||||||
import { onMount } from 'svelte';
|
import { uuid } from "$lib/stores/uuid.js";
|
||||||
const title = 'Your Data';
|
import { onMount } from "svelte";
|
||||||
|
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;
|
||||||
|
|
||||||
|
async function fetchData() {
|
||||||
|
if ($uuid != "null") {
|
||||||
|
const url = `${getApiUrl()}/api/v2/user/${$uuid}`;
|
||||||
|
const res = await fetch(url);
|
||||||
|
const json = await res.json();
|
||||||
|
if (json.length) {
|
||||||
|
data = json;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
];
|
|
||||||
|
|
||||||
let isLoading = false;
|
onMount(async () => {
|
||||||
|
isLoading = true;
|
||||||
async function fetchData() {
|
await fetchData();
|
||||||
if ($uuid != 'null') {
|
isLoading = false;
|
||||||
const url = `https://owlboard.info/api/v2/user/${$uuid}`;
|
});
|
||||||
const res = await fetch(url);
|
|
||||||
const json = await res.json();
|
|
||||||
if (json.length) {
|
|
||||||
data = json;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onMount(async () => {
|
|
||||||
isLoading = true;
|
|
||||||
await fetchData();
|
|
||||||
isLoading = false;
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<svelte:head>
|
||||||
|
<meta name="robots" content="noindex">
|
||||||
|
</svelte:head>
|
||||||
|
|
||||||
<Header {title} />
|
<Header {title} />
|
||||||
|
|
||||||
<p>OwlBoard stores as little data about you as possible to offer the service.</p>
|
<p>OwlBoard stores as little data about you as possible to offer the service.</p>
|
||||||
@ -42,26 +47,26 @@
|
|||||||
<br /><br />
|
<br /><br />
|
||||||
|
|
||||||
{#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>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<Nav />
|
<Nav />
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
p {
|
p {
|
||||||
margin: 10px;
|
margin: 10px;
|
||||||
}
|
}
|
||||||
.api_response {
|
.api_response {
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
21
src/routes/more/help/+page.svelte
Normal file
21
src/routes/more/help/+page.svelte
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
<script>
|
||||||
|
import Header from "$lib/navigation/header.svelte";
|
||||||
|
import Nav from "$lib/navigation/nav.svelte";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:head>
|
||||||
|
<meta name="robots" content="noindex">
|
||||||
|
</svelte:head>
|
||||||
|
|
||||||
|
<Header title={"Help"} />
|
||||||
|
<Nav />
|
||||||
|
<br /><br />
|
||||||
|
<p>
|
||||||
|
You can find extensive help and documentation at <a href="https://docs.owlboard.info">the documentation site</a>.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
You can also use the <a href="https://www.facebook.com/owlboard.support">OwlBoard Support</a> page on Facebook.
|
||||||
|
</p>
|
||||||
|
<br /><br />
|
||||||
|
|
||||||
|
<p>Alternatively, you can report an issue on the <a href="/more/report">Report an Issue</a> page.</p>
|
@ -1,57 +1,103 @@
|
|||||||
<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>
|
||||||
|
|
||||||
|
<svelte:head>
|
||||||
|
<meta name="robots" content="noindex">
|
||||||
|
</svelte:head>
|
||||||
|
|
||||||
<Header {title} />
|
<Header {title} />
|
||||||
<div>
|
<div>
|
||||||
<p>
|
<h2>Your Data</h2>
|
||||||
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
|
<p>
|
||||||
that we store, please visit <a href="/more/data">My Data</a>.
|
OwlBoard logs the IP addresses of its users, this is done on the basis of legitimate
|
||||||
</p>
|
interest to ensure the security of the platform and to protect all users from
|
||||||
<p>OwlBoard does not utilize any cookies. IP addresses and browser fingerprints are not logged.</p>
|
malicious activity. See <a href="#datasharing">Data Sharing</a> for details on how
|
||||||
<h2>If You Do Not Sign Up</h2>
|
we may share this data.
|
||||||
<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
|
<p>
|
||||||
your device.
|
With the exception of sending emails, all data is held - and always remains within -
|
||||||
</p>
|
the United Kingdom.
|
||||||
<h2>If You Sign Up</h2>
|
</p>
|
||||||
<p>
|
<h3>Telemetry</h3>
|
||||||
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
|
<p>
|
||||||
settings are stored locally in your browser and do not leave your device.
|
If you opt-in to Telemetry, you will share your IP address and information about the
|
||||||
</p>
|
type of device and software you're using to access the service. This data is
|
||||||
<p>
|
anonymised and cannot be traced back to any individual. You can opt-in or opt-out in
|
||||||
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
|
your <a href="/more/settings">Settings</a>. All of the anonymised data can be viewed
|
||||||
containing a registration link will be sent to you. At this point, the username portion of your email address is discarded. For example, if your email address is
|
at: <a target="_blank" href="https://liwan.fjla.uk">liwan.fjla.uk</a> at any time.
|
||||||
'a-user@owlboard.info', only 'owlboard.info' will be stored. This host portion of your email address is stored to filter and display relevant results prominently.
|
</p>
|
||||||
</p>
|
<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>
|
All of the data that is stored is held within the United Kingdom.
|
||||||
<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>
|
||||||
<h2>Reporting an Issue</h2>
|
<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>
|
Telemetry data is used to identify which areas of the webapp are used most frequently
|
||||||
<p>
|
and where improvements need to be made.
|
||||||
Any data submitted when reporting an issue will be publicly viewable alongside the <a href="https://git.fjla.uk/owlboard/backend/issues" target="_blank"
|
</p>
|
||||||
>OwlBoard/backend git repository</a
|
<h3>Registering</h3>
|
||||||
>.
|
<p>
|
||||||
</p>
|
If you register, your email address will be used to verify that you work for an
|
||||||
|
organisation that is permitted to access staff data on the service. An activation
|
||||||
|
email will be sent before your email address is anonymised. For example, if your
|
||||||
|
email address is hello@owlboard.info, it will be anonymized to @owlboard.info.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
OwlBoard emails are sent using Proton Mail, a privacy-first email service based in
|
||||||
|
Switzerland. To facilitate this, your email address will be securely sent to
|
||||||
|
Proton Mail securely.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
You will be assigned a unique identifier which will be stored alongside your
|
||||||
|
anonymised email address as well as in your browser. This is how the service
|
||||||
|
verifies that you are authorised to access staff data.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h2 id="datasharing">Data Sharing</h2>
|
||||||
|
<p>
|
||||||
|
OwlBoard utilises CrowdSec, a security service that helps to protect the platform from
|
||||||
|
malicious activity. As part of its operation, CrowdSec analyzes IP addresses and may
|
||||||
|
share certain information with its community of users to identify and mitigate
|
||||||
|
security threats. If your IP address is identified as part of a security incident or
|
||||||
|
threat, it may be shared with CrowdSec's network for further analysis and to prevent
|
||||||
|
malicious activity. This sharing of IP addresses is done under the legitimate
|
||||||
|
interest basis for ensuring the security of the platform and protecting all users
|
||||||
|
from malicious activity.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
CrowdSec anonymizes and processes data in accordance with its own privacy policy, which
|
||||||
|
is available for review here. We recommend reviewing their policy to understand how
|
||||||
|
they handle any data collected.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Nav />
|
<Nav />
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
div {
|
div {
|
||||||
text-align: left;
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
h2 {
|
h2 {
|
||||||
color: var(--second-text-color);
|
color: var(--second-text-color);
|
||||||
margin: 10px;
|
margin: auto;
|
||||||
padding-top: 20px;
|
width: 90vw;
|
||||||
}
|
padding-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
p {
|
h3 {
|
||||||
color: white;
|
color: var(--second-text-color);
|
||||||
margin: 10px;
|
margin: auto;
|
||||||
}
|
width: 90vw;
|
||||||
|
padding-top: 10px;
|
||||||
|
}
|
||||||
|
p {
|
||||||
|
color: white;
|
||||||
|
margin: auto;
|
||||||
|
padding-top: 5px;
|
||||||
|
padding-bottom: 8px;
|
||||||
|
width: 90vw;
|
||||||
|
max-width: 550px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -1,118 +1,140 @@
|
|||||||
<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 type { CardConfig } from "$lib/cards/Card.types";
|
||||||
|
import { apiGet } from "$lib/scripts/apiFetch";
|
||||||
|
|
||||||
const title = 'Reason Codes';
|
interface ApiResponse {
|
||||||
let isLoading = false;
|
results: boolean;
|
||||||
let inputValue = '';
|
title: string;
|
||||||
let resultObject = {
|
resultLines: string[];
|
||||||
results: false,
|
|
||||||
title: '',
|
|
||||||
resultLines: []
|
|
||||||
};
|
|
||||||
|
|
||||||
function load() {
|
|
||||||
isLoading = true;
|
|
||||||
resultObject.results = false;
|
|
||||||
getData().then((result) => {
|
|
||||||
handleData(result);
|
|
||||||
isLoading = false;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getData() {
|
|
||||||
if (inputValue) {
|
|
||||||
const url = `https://owlboard.info/api/v2/ref/reasonCode/${inputValue}`;
|
|
||||||
const res = await fetch(url);
|
|
||||||
return await res.json();
|
|
||||||
} else {
|
|
||||||
return [];
|
|
||||||
}
|
}
|
||||||
}
|
const title = "Reason Codes";
|
||||||
|
let isLoading = false;
|
||||||
async function handleData(data) {
|
let inputValue = "";
|
||||||
let resultLines = [];
|
let resultObject: ApiResponse = {
|
||||||
if (data.length) {
|
|
||||||
resultObject.title = data[0]['code'];
|
|
||||||
resultLines = [data[0]['lateReason'], data[0]['cancReason']];
|
|
||||||
} else {
|
|
||||||
resultObject = {
|
|
||||||
results: false,
|
results: false,
|
||||||
title: 'Not Found',
|
title: "",
|
||||||
resultLines: []
|
resultLines: [],
|
||||||
};
|
};
|
||||||
|
let config: CardConfig = {
|
||||||
|
title: "",
|
||||||
|
showHelp: false,
|
||||||
|
showRefresh: false,
|
||||||
|
helpText: "",
|
||||||
|
onRefresh: () => {},
|
||||||
|
refreshing: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
function load() {
|
||||||
|
isLoading = true;
|
||||||
|
resultObject.results = false;
|
||||||
|
getData().then((result) => {
|
||||||
|
handleData(result);
|
||||||
|
isLoading = false;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
resultObject.resultLines = resultLines;
|
|
||||||
resultObject.results = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
onMount(() => {
|
async function getData() {
|
||||||
isLoading = false;
|
if (inputValue) {
|
||||||
});
|
const apiPath = `/api/v2/ref/reasonCode/${inputValue}`;
|
||||||
|
const res = await apiGet(apiPath);
|
||||||
|
return res;
|
||||||
|
} else {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function handleInput(event) {
|
async function handleData(data) {
|
||||||
inputValue = event.target.value;
|
let resultLines = [];
|
||||||
}
|
if (data.length) {
|
||||||
|
resultObject.title = data[0]["code"];
|
||||||
|
resultLines = [data[0]["lateReason"], data[0]["cancReason"]];
|
||||||
|
} else {
|
||||||
|
resultObject = {
|
||||||
|
results: false,
|
||||||
|
title: "Not Found",
|
||||||
|
resultLines: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
resultObject.resultLines = resultLines;
|
||||||
|
config.title = resultObject.title;
|
||||||
|
resultObject.results = true;
|
||||||
|
}
|
||||||
|
|
||||||
function handleSubmit(event) {
|
onMount(() => {
|
||||||
event.preventDefault();
|
isLoading = false;
|
||||||
load();
|
});
|
||||||
}
|
|
||||||
|
function handleInput(event) {
|
||||||
|
inputValue = event.target.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleSubmit(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
load();
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Header {title} />
|
<Header {title} />
|
||||||
<p>A reason code is a three digit number that maps to a reason for a delay or cancellation</p>
|
<p>A reason code is a three digit number that maps to a reason for a delay or cancellation</p>
|
||||||
<form on:submit={handleSubmit}>
|
<form on:submit={handleSubmit}>
|
||||||
<input type="text" placeholder="Enter Code" autocomplete="off" bind:value={inputValue} on:input={handleInput} />
|
<input type="text" placeholder="Enter Code" autocomplete="off" bind:value={inputValue} on:input={handleInput} />
|
||||||
|
|
||||||
<br />
|
<br />
|
||||||
|
|
||||||
<button type="submit">Submit</button>
|
<button type="submit">Submit</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
{#if isLoading}
|
{#if isLoading}
|
||||||
<Loading />
|
<Loading />
|
||||||
{/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 />
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
p {
|
p {
|
||||||
margin: 10px;
|
margin: 10px;
|
||||||
margin-top: 20px;
|
margin-top: 20px;
|
||||||
}
|
}
|
||||||
input {
|
input {
|
||||||
width: 25%;
|
width: 25%;
|
||||||
min-width: 150px;
|
min-width: 150px;
|
||||||
height: 32px;
|
height: 32px;
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
margin-bottom: 5px;
|
margin-bottom: 5px;
|
||||||
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);
|
||||||
button {
|
}
|
||||||
width: 15%;
|
button {
|
||||||
min-width: 100px;
|
width: 15%;
|
||||||
margin-bottom: 5px;
|
min-width: 100px;
|
||||||
margin-top: 5px;
|
margin-bottom: 5px;
|
||||||
border: none;
|
margin-top: 5px;
|
||||||
border-radius: 20px;
|
border: none;
|
||||||
padding: 5px;
|
border-radius: 20px;
|
||||||
font-family: urwgothic, 'Lucida Sans', 'Lucida Sans Regular', 'Lucida Grande', 'Lucida Sans Unicode', Geneva, Verdana, sans-serif;
|
padding: 5px;
|
||||||
font-size: 16px;
|
font-family: urwgothic, "Lucida Sans", "Lucida Sans Regular", "Lucida Grande", "Lucida Sans Unicode", Geneva, Verdana, sans-serif;
|
||||||
font-weight: 400;
|
font-size: 16px;
|
||||||
background-color: var(--overlay-color);
|
font-weight: 400;
|
||||||
color: var(--link-color);
|
background-color: var(--island-bg-color);
|
||||||
}
|
color: var(--main-text-color);
|
||||||
|
box-shadow: var(--box-shadow);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -1,135 +1,153 @@
|
|||||||
<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 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() {
|
|
||||||
isLoading = true;
|
|
||||||
const url = 'https://owlboard.info/api/v2/user/request';
|
|
||||||
const request = {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
email: inputValue
|
|
||||||
})
|
|
||||||
};
|
|
||||||
const res = await fetch(url, request);
|
|
||||||
if (res.status == 400 || res.status == 403) {
|
|
||||||
state = 'unauth';
|
|
||||||
} else if (res.status == 201) {
|
|
||||||
state = 'sent';
|
|
||||||
} else {
|
|
||||||
state = 'error';
|
|
||||||
}
|
}
|
||||||
isLoading = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
onMount(async () => {
|
async function request() {
|
||||||
const auth = await checkAuth();
|
isLoading = true;
|
||||||
if (auth.uuidPresent === false) {
|
const url = `${getApiUrl()}/api/v2/user/request`;
|
||||||
state = 'unreg';
|
const request = {
|
||||||
} else if (auth.uuidPresent === true) {
|
method: "POST",
|
||||||
state = 'reg';
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
email: inputValue,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
const res = await fetch(url, request);
|
||||||
|
if (res.status == 400 || res.status == 403) {
|
||||||
|
state = "unauth";
|
||||||
|
} else if (res.status == 201) {
|
||||||
|
state = "sent";
|
||||||
|
} else {
|
||||||
|
state = "error";
|
||||||
|
}
|
||||||
|
isLoading = false;
|
||||||
}
|
}
|
||||||
isLoading = false;
|
|
||||||
});
|
function send() {
|
||||||
|
toast.promise(request(), {
|
||||||
|
loading: "Sending email...",
|
||||||
|
success: "Sent, check your inbox",
|
||||||
|
error: "Error sending email",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
const auth = await checkAuth();
|
||||||
|
if (auth.uuidPresent === false) {
|
||||||
|
state = "unreg";
|
||||||
|
} else if (auth.uuidPresent === true) {
|
||||||
|
state = "reg";
|
||||||
|
}
|
||||||
|
isLoading = false;
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Header {title} />
|
<Header {title} />
|
||||||
|
|
||||||
<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>
|
<p>To register, you will need to enter a work email address to receive a confirmation email</p>
|
||||||
<ul>
|
<p class="bold">Already have a registration code? <a href="/more/reg/submit">enter it here</a></p>
|
||||||
<li>Access the Train Finder</li>
|
<form on:submit={send}>
|
||||||
<li>Access the PIS Finder</li>
|
<input type="text" autocomplete="email" placeholder="Enter work email" bind:value={inputValue} on:input={handleInput} /><br />
|
||||||
<li>More detailed departure boards:</li>
|
<label for="checkbox">
|
||||||
<ul>
|
I have read and accept the terms of the <a href="/more/privacy">Privacy Policy</a><br />
|
||||||
<li>Non-Passenger movements</li>
|
<input id="checkbox" type="checkbox" required />
|
||||||
<li>Hidden platform numbers</li>
|
</label><br />
|
||||||
<li>Display up to 25 services</li>
|
<button type="submit">Submit</button>
|
||||||
</ul>
|
</form>
|
||||||
</ul>
|
<br />
|
||||||
<p>To register, you will need to enter a work email address to receive a confirmation email</p>
|
<p class="bold">What do you get?</p>
|
||||||
<form on:submit={request}>
|
<li>Access to Train details</li>
|
||||||
<input type="text" autocomplete="email" placeholder="Enter work email" bind:value={inputValue} on:input={handleInput} /><br />
|
<li>Access to PIS Codes</li>
|
||||||
<label for="checkbox">
|
<li>ECS Movements on departure boards</li>
|
||||||
I have read and accept the terms of the <a href="/more/privacy">Privacy Policy</a><br />
|
<li>Non-Public trains on departure boards</li>
|
||||||
<input id="checkbox" type="checkbox" required />
|
<li>Hidden platform numbers on departure boards</li>
|
||||||
</label><br />
|
<li>See up to the next 40 trains departing a station over the next two hours</li>
|
||||||
<button type="submit">Submit</button>
|
{:else if state == "sent"}
|
||||||
</form>
|
<p>An email has been sent, enter the code in the email to activate your profile.</p>
|
||||||
{:else if state == 'sent'}
|
<p class="bold"><a href="/more/reg/submit">Ready to enter your code?</a></p>
|
||||||
<p>An email has been sent, click the link in the email to activate your profile.</p>
|
<p>If you use multiple browsers, you will only be logged in using the browser you open the link with.</p>
|
||||||
<p>When you click the link, your authorisation key will be automatically be stored in your browser.</p>
|
<p>You will be able to register again using the same email address</p>
|
||||||
<p>If you use multiple browsers, you will only be logged in using the browser you open the link with.</p>
|
{:else if state == "unauth"}
|
||||||
<p>You will be able to register again using the same email address</p>
|
<p>The email address you entered does not belong to an authorised business.</p>
|
||||||
{:else if state == 'unauth'}
|
<p>If you think this is an error, you can get help on <a href="/more/help">the help page</a>.</p>
|
||||||
<p>The email address you entered does not belong to an authorised business.</p>
|
{:else if state == "error"}
|
||||||
<p>If you think this is an error, you can report an issue by <a href="/more/report">reporting an issue</a>.</p>
|
<p>There was an error processing your request.</p>
|
||||||
{:else if state == 'error'}
|
<p>Check that the email you entered was correct or try again later.</p>
|
||||||
<p>There was an error processing your request.</p>
|
{:else if state == "reg"}
|
||||||
<p>Check that the email you entered was correct or try again later.</p>
|
<p>
|
||||||
{:else if state == 'reg'}
|
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
|
||||||
<p>
|
your browser.
|
||||||
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
|
</p>
|
||||||
browser.
|
<LogoutButton />
|
||||||
</p>
|
{/if}
|
||||||
<LogoutButton />
|
|
||||||
{/if}
|
|
||||||
</section>
|
</section>
|
||||||
<Nav />
|
<Nav />
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.content {
|
.bold {
|
||||||
margin-top: 30px;
|
font-weight: 800;
|
||||||
}
|
}
|
||||||
p {
|
.content {
|
||||||
margin: 10px;
|
margin-top: 30px;
|
||||||
}
|
}
|
||||||
ul {
|
p {
|
||||||
text-align: left;
|
margin: 10px;
|
||||||
color: white;
|
margin-left: 20px;
|
||||||
}
|
margin-right: 20px;
|
||||||
input {
|
}
|
||||||
height: 40px;
|
li {
|
||||||
width: 80%;
|
text-align: center;
|
||||||
max-width: 375px;
|
font-size: 14px;
|
||||||
font-family: urwgothic, 'Franklin Gothic Medium', 'Arial Narrow', Arial, sans-serif;
|
color: white;
|
||||||
text-align: center;
|
margin-top: 5px;
|
||||||
font-size: 20px;
|
margin-left: 15px;
|
||||||
border-radius: 50px;
|
margin-right: 15px;
|
||||||
border: none;
|
list-style-type: none;
|
||||||
margin-bottom: 20px;
|
}
|
||||||
}
|
input {
|
||||||
#checkbox {
|
height: 40px;
|
||||||
height: 30px;
|
width: 80%;
|
||||||
width: 30px;
|
max-width: 375px;
|
||||||
}
|
font-family: urwgothic, "Franklin Gothic Medium", "Arial Narrow", Arial, sans-serif;
|
||||||
button {
|
text-align: center;
|
||||||
border: none;
|
font-size: 20px;
|
||||||
background-color: var(--overlay-color);
|
border-radius: 50px;
|
||||||
color: white;
|
border: none;
|
||||||
width: 35%;
|
margin-bottom: 20px;
|
||||||
height: 30px;
|
}
|
||||||
border-radius: 50px;
|
#checkbox {
|
||||||
font-size: 18px;
|
height: 30px;
|
||||||
}
|
width: 30px;
|
||||||
|
}
|
||||||
|
button {
|
||||||
|
border: none;
|
||||||
|
background-color: var(--island-bg-color);
|
||||||
|
color: var(--main-text-color);
|
||||||
|
width: 35%;
|
||||||
|
height: 30px;
|
||||||
|
border-radius: 50px;
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -1,79 +1,104 @@
|
|||||||
<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 { uuid } from '$lib/stores/uuid.js';
|
import { uuid } from "$lib/stores/uuid";
|
||||||
import { onMount } from 'svelte';
|
|
||||||
|
|
||||||
const title = 'Registration';
|
const title = "Submit Registration";
|
||||||
let state = '';
|
let state = false;
|
||||||
let isLoading = true;
|
let status: string;
|
||||||
|
let inputString: string;
|
||||||
|
|
||||||
async function getUUID() {
|
async function handleSubmit() {
|
||||||
return new URLSearchParams(window.location.search).get('key');
|
console.log(`Code: ${inputString}`);
|
||||||
}
|
const res = await submit(inputString);
|
||||||
|
console.log(`Registration Status: ${res}`);
|
||||||
async function submit(id) {
|
if (res == 201) {
|
||||||
const url = 'https://owlboard.info/api/v2/user/register';
|
status = "okay";
|
||||||
const request = {
|
} else if (res == 401) {
|
||||||
method: 'POST',
|
status = "fail";
|
||||||
headers: {
|
} else {
|
||||||
'Content-Type': 'application/json'
|
console.error("Unable to register: ", status);
|
||||||
},
|
}
|
||||||
body: JSON.stringify({
|
state = true;
|
||||||
uuid: id
|
|
||||||
})
|
|
||||||
};
|
|
||||||
const res = await fetch(url, request);
|
|
||||||
const body = await res.json();
|
|
||||||
if (body.api_key) {
|
|
||||||
uuid.set(body.api_key);
|
|
||||||
return 201;
|
|
||||||
}
|
}
|
||||||
return res.status;
|
|
||||||
}
|
|
||||||
|
|
||||||
onMount(async () => {
|
async function submit(id: string): Promise<number> {
|
||||||
const id = (await getUUID()) || '';
|
const url = `${getApiUrl()}/api/v2/user/register`;
|
||||||
if (id == '' || !id) {
|
const request = {
|
||||||
state = 'none';
|
method: "POST",
|
||||||
isLoading = false;
|
headers: {
|
||||||
return;
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
uuid: id,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
const res = await fetch(url, request);
|
||||||
|
const body = await res.json();
|
||||||
|
if (body.api_key) {
|
||||||
|
uuid.set(body.api_key);
|
||||||
|
return 201;
|
||||||
|
} else {
|
||||||
|
return res.status;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
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>
|
||||||
|
|
||||||
|
<svelte:head>
|
||||||
|
<meta name="robots" content="noindex">
|
||||||
|
</svelte:head>
|
||||||
|
|
||||||
<Header {title} />
|
<Header {title} />
|
||||||
{#if isLoading}
|
|
||||||
<Loading />
|
{#if state}
|
||||||
{/if}
|
{#if status == "okay"}
|
||||||
{#if state == 'none'}
|
<p class="title-ish">You are now registered</p>
|
||||||
<p>Unable to read your access key.</p>
|
<p>Your secret key will be stored in your browser.</p>
|
||||||
<p>Please click the link in your email again.</p>
|
<p>If you change browsers, change device or clear your browsing data, you may have to register again.</p>
|
||||||
{:else if state == 'unauth'}
|
{:else if status == "fail"}
|
||||||
<p>Your link is not valid, links expire after 30 minutes.</p>
|
<p class="title-ish">Your code was not accepted</p>
|
||||||
<p>You can try to register again.</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>
|
||||||
{:else if state == 'error'}
|
{/if}
|
||||||
<p>There was an error on our end, please try again later</p>
|
{:else}
|
||||||
{:else if state == 'done'}
|
<p class="title-ish">Enter your registration code below</p>
|
||||||
<p>You are now logged in</p>
|
<form on:submit={handleSubmit} id="codeInputForm">
|
||||||
|
<input class="code-in" bind:value={inputString} id={'input'} maxlength="6" autocomplete="off">
|
||||||
|
<br />
|
||||||
|
<button type="submit">Submit</button>
|
||||||
|
</form>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<Nav />
|
<Nav />
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
p {
|
.title-ish {
|
||||||
margin: 14px;
|
font-size: 20px;
|
||||||
}
|
}
|
||||||
|
.code-in {
|
||||||
|
margin: 3px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
width: calc(29px * 6);
|
||||||
|
height: 39px;
|
||||||
|
font-size: 30px;
|
||||||
|
text-align: center;
|
||||||
|
text-transform: uppercase;
|
||||||
|
border-radius: 10px;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
button {
|
||||||
|
border: none;
|
||||||
|
background-color: var(--island-bg-color);
|
||||||
|
color: var(--main-text-color);
|
||||||
|
width: 35%;
|
||||||
|
min-width: 95px;
|
||||||
|
max-width: 231px;
|
||||||
|
height: 30px;
|
||||||
|
border-radius: 50px;
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
p {
|
||||||
|
margin-left: 40px;
|
||||||
|
margin-right: 40px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -1,184 +1,192 @@
|
|||||||
<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";
|
||||||
|
|
||||||
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 = {
|
||||||
userAgent: navigator.userAgent,
|
userAgent: navigator.userAgent,
|
||||||
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}`,
|
||||||
};
|
};
|
||||||
});
|
|
||||||
|
|
||||||
let preFlight = false;
|
|
||||||
|
|
||||||
async function submit() {
|
|
||||||
console.log(reportType, reportSubject, reportMsg);
|
|
||||||
preFlight = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function send() {
|
|
||||||
console.log('SEND DATA REQUESTED');
|
|
||||||
isLoading = true;
|
|
||||||
const formData = JSON.stringify({
|
|
||||||
label: reportType,
|
|
||||||
subject: reportSubject,
|
|
||||||
msg:
|
|
||||||
`User Agent: ${reportCollected.userAgent}\n` +
|
|
||||||
`Browser: ${reportCollected.browser}\n` +
|
|
||||||
`BrowserVersion: ${reportCollected.version}\n` +
|
|
||||||
`Platform: ${reportCollected.platform}\n` +
|
|
||||||
`Viewport: ${reportCollected.viewport}\n\n\n` +
|
|
||||||
`User Message:\n` +
|
|
||||||
`${reportMsg}`
|
|
||||||
});
|
});
|
||||||
const url = `https://owlboard.info/misc/issue`;
|
|
||||||
const options = {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
},
|
|
||||||
body: formData
|
|
||||||
};
|
|
||||||
const res = await fetch(url, options);
|
|
||||||
if (res.status == 200) {
|
|
||||||
isLoading = false;
|
|
||||||
isDone = true;
|
|
||||||
await new Promise((r) => setTimeout(r, 2000));
|
|
||||||
window.location.href = '/';
|
|
||||||
} else {
|
|
||||||
isLoading = false;
|
|
||||||
isError = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function cancel() {
|
let preFlight = false;
|
||||||
preFlight = false;
|
|
||||||
isLoading = false;
|
async function submit() {
|
||||||
isDone = false;
|
console.log(reportType, reportSubject, reportMsg);
|
||||||
isError = false;
|
preFlight = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function send() {
|
||||||
|
console.log("SEND DATA REQUESTED");
|
||||||
|
isLoading = true;
|
||||||
|
const formData = JSON.stringify({
|
||||||
|
label: reportType,
|
||||||
|
subject: reportSubject,
|
||||||
|
msg:
|
||||||
|
`User Agent: ${reportCollected.userAgent}\n` +
|
||||||
|
`Browser: ${reportCollected.browser}\n` +
|
||||||
|
`BrowserVersion: ${reportCollected.version}\n` +
|
||||||
|
`Platform: ${reportCollected.platform}\n` +
|
||||||
|
`Viewport: ${reportCollected.viewport}\n\n\n` +
|
||||||
|
`User Message:\n` +
|
||||||
|
`${reportMsg}`,
|
||||||
|
});
|
||||||
|
const url = `${getApiUrl()}/misc/issue`;
|
||||||
|
const options = {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: formData,
|
||||||
|
};
|
||||||
|
const res = await fetch(url, options);
|
||||||
|
if (res.status == 200) {
|
||||||
|
isLoading = false;
|
||||||
|
isDone = true;
|
||||||
|
await new Promise((r) => setTimeout(r, 2000));
|
||||||
|
window.location.href = "/";
|
||||||
|
} else {
|
||||||
|
isLoading = false;
|
||||||
|
isError = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function cancel() {
|
||||||
|
preFlight = false;
|
||||||
|
isLoading = false;
|
||||||
|
isDone = false;
|
||||||
|
isError = false;
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<svelte:head>
|
||||||
|
<meta name="robots" content="noindex">
|
||||||
|
</svelte:head>
|
||||||
|
|
||||||
<Header {title} />
|
<Header {title} />
|
||||||
|
|
||||||
{#if isLoading}
|
{#if isLoading}
|
||||||
<Loading />
|
<Loading />
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if isDone}
|
{#if isDone}
|
||||||
<Done />
|
<Done />
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if !preFlight && !isDone}
|
{#if !preFlight && !isDone}
|
||||||
<p>
|
<p>Get help on the <a href="https://www.facebook.com/owlboard.support">OwlBoard Support Facebook Page</a></p>
|
||||||
Any data that you enter here will be visible publicly
|
|
||||||
<a href="https://git.fjla.uk/OwlBoard/backend/issues" target="_blank">here</a>
|
|
||||||
</p>
|
|
||||||
<p>You will be shown all of the collected data before the form is submitted.</p>
|
|
||||||
<form on:submit={submit}>
|
|
||||||
<select class="formInputs" name="type" bind:value={reportType} placeholder="Choose Category">
|
|
||||||
<option value="" disabled selected>Choose an Issue Type</option>
|
|
||||||
<option value="bug">Problem</option>
|
|
||||||
<option value="enhancement">Feature Request</option>
|
|
||||||
<option value="question">Question</option>
|
|
||||||
<option value="user-support">Unable to sign up</option>
|
|
||||||
</select>
|
|
||||||
<br />
|
|
||||||
<input class="formInputs" type="text" bind:value={reportSubject} placeholder="Subject" />
|
|
||||||
<br />
|
|
||||||
<textarea class="formInputs" bind:value={reportMsg} placeholder="Enter your message..." />
|
|
||||||
<br />
|
|
||||||
<button type="submit">Submit</button>
|
|
||||||
<button type="reset">Reset</button>
|
|
||||||
</form>
|
|
||||||
{:else}
|
|
||||||
<Island>
|
|
||||||
<h2>Device Data:</h2>
|
|
||||||
<p><span class="dataType">User Agent: </span>{reportCollected.userAgent}</p>
|
|
||||||
<p>
|
<p>
|
||||||
<span class="dataType">Browser: </span>{reportCollected.browser} - {reportCollected.version}
|
Or submit an issue, you can check for existing reports
|
||||||
|
<a href="https://git.fjla.uk/OwlBoard/backend/issues" target="_blank">here</a>
|
||||||
|
before submitting a new issue.
|
||||||
</p>
|
</p>
|
||||||
<p><span class="dataType">Platform: </span>{reportCollected.platform}</p>
|
<p>You will be shown all of the collected data before the form is submitted.</p>
|
||||||
<p><span class="dataType">Viewport: </span> {reportCollected.viewport}</p>
|
<form on:submit={submit}>
|
||||||
<h2>Reported Data:</h2>
|
<select class="formInputs" name="type" bind:value={reportType} placeholder="Choose Category">
|
||||||
<p><span class="dataType">Report Type: </span>{reportType}</p>
|
<option value="" disabled selected>Choose an Issue Type</option>
|
||||||
<p>{reportSubject}</p>
|
<option value="bug">Problem</option>
|
||||||
<p>{reportMsg}</p>
|
<option value="enhancement">Feature Request</option>
|
||||||
<button class="overlayButtons" on:click={send}>Send</button>
|
<option value="question">Question</option>
|
||||||
<button class="overlayButtons" on:click={cancel}>Cancel</button>
|
<option value="user-support">Unable to sign up</option>
|
||||||
</Island>
|
</select>
|
||||||
|
<br />
|
||||||
|
<input class="formInputs" type="text" bind:value={reportSubject} placeholder="Subject" />
|
||||||
|
<br />
|
||||||
|
<textarea class="formInputs" bind:value={reportMsg} placeholder="Enter your message..." />
|
||||||
|
<br />
|
||||||
|
<button type="submit">Submit</button>
|
||||||
|
<button type="reset">Reset</button>
|
||||||
|
</form>
|
||||||
|
{:else}
|
||||||
|
<Island>
|
||||||
|
<h2>Device Data:</h2>
|
||||||
|
<p><span class="dataType">User Agent: </span>{reportCollected.userAgent}</p>
|
||||||
|
<p>
|
||||||
|
<span class="dataType">Browser: </span>{reportCollected.browser} - {reportCollected.version}
|
||||||
|
</p>
|
||||||
|
<p><span class="dataType">Platform: </span>{reportCollected.platform}</p>
|
||||||
|
<p><span class="dataType">Viewport: </span> {reportCollected.viewport}</p>
|
||||||
|
<h2>Reported Data:</h2>
|
||||||
|
<p><span class="dataType">Report Type: </span>{reportType}</p>
|
||||||
|
<p>{reportSubject}</p>
|
||||||
|
<p>{reportMsg}</p>
|
||||||
|
<button class="overlayButtons" on:click={send}>Send</button>
|
||||||
|
<button class="overlayButtons" on:click={cancel}>Cancel</button>
|
||||||
|
</Island>
|
||||||
{/if}
|
{/if}
|
||||||
<Nav />
|
<Nav />
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
p {
|
p {
|
||||||
margin-left: 10px;
|
margin-left: 10px;
|
||||||
margin-right: 10px;
|
margin-right: 10px;
|
||||||
}
|
}
|
||||||
select {
|
select {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 50px;
|
border-radius: 50px;
|
||||||
height: 30px;
|
height: 30px;
|
||||||
background-color: white;
|
background-color: white;
|
||||||
}
|
}
|
||||||
.formInputs {
|
.formInputs {
|
||||||
margin: 10px;
|
margin: 10px;
|
||||||
font-family: urwgothic, sans-serif;
|
font-family: urwgothic, sans-serif;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
border: none;
|
border: none;
|
||||||
width: 50%;
|
width: 50%;
|
||||||
max-width: 450px;
|
max-width: 450px;
|
||||||
min-width: 250px;
|
min-width: 250px;
|
||||||
}
|
}
|
||||||
input {
|
input {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
border-radius: 50px;
|
border-radius: 50px;
|
||||||
height: 30px;
|
height: 30px;
|
||||||
}
|
}
|
||||||
textarea {
|
textarea {
|
||||||
text-align: left;
|
text-align: left;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
padding-left: 5px;
|
padding-left: 5px;
|
||||||
padding-right: 5px;
|
padding-right: 5px;
|
||||||
height: 30vh;
|
height: 30vh;
|
||||||
min-height: 150px;
|
min-height: 150px;
|
||||||
max-height: 400px;
|
max-height: 400px;
|
||||||
}
|
}
|
||||||
button {
|
button {
|
||||||
background-color: var(--overlay-color);
|
background-color: var(--island-bg-color);
|
||||||
color: white;
|
color: var(--main-text-color);
|
||||||
font-family: urwgothic, sans-serif;
|
font-family: urwgothic, sans-serif;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 50px;
|
border-radius: 50px;
|
||||||
width: 25%;
|
width: 25%;
|
||||||
height: 30px;
|
height: 30px;
|
||||||
max-width: 100px;
|
max-width: 100px;
|
||||||
}
|
box-shadow: var(--box-shadow);
|
||||||
h2 {
|
}
|
||||||
color: white;
|
h2 {
|
||||||
}
|
color: white;
|
||||||
.dataType {
|
}
|
||||||
color: white;
|
.dataType {
|
||||||
}
|
color: white;
|
||||||
.overlayButtons {
|
}
|
||||||
background-color: var(--main-bg-color);
|
.overlayButtons {
|
||||||
}
|
background-color: var(--main-bg-color);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -1,12 +1,65 @@
|
|||||||
<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 { telemetry } from "$lib/stores/telemetryConsent";
|
||||||
|
import { getCurrentLocation } from "$lib/scripts/getLocation";
|
||||||
|
import toast from "svelte-french-toast";
|
||||||
|
const title = "Settings";
|
||||||
|
|
||||||
|
$: if ($location) {
|
||||||
|
getCurrentLocation();
|
||||||
|
}
|
||||||
|
|
||||||
|
function confirmationToast() {
|
||||||
|
toast.success("Settings updated");
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<svelte:head>
|
||||||
|
<meta name="robots" content="noindex">
|
||||||
|
</svelte:head>
|
||||||
|
|
||||||
<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={confirmationToast} />
|
||||||
|
</div>
|
||||||
|
</Island>
|
||||||
|
|
||||||
|
<Island variables={{title:"Telemetry"}}>
|
||||||
|
<p>Telemetry helps shape the future of OwlBoard - all data is anonymised. To find out more, see the
|
||||||
|
<a href="/more/privacy">privacy policy</a>.
|
||||||
|
</p>
|
||||||
|
<div class="checkbox-container">
|
||||||
|
<label for="telemetry_enable">Enabled</label>
|
||||||
|
<input id="telemetry_enable" type="checkbox" bind:checked={$telemetry} on:click={confirmationToast} />
|
||||||
|
</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>
|
||||||
|
@ -1,78 +1,201 @@
|
|||||||
<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";
|
||||||
const title = 'Statistics';
|
import { getApiUrl } from "$lib/scripts/upstream";
|
||||||
|
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 = 'https://owlboard.info/misc/server/stats';
|
const url = `${getApiUrl()}/misc/server/stats`;
|
||||||
const res = await fetch(url);
|
const res = await fetch(url);
|
||||||
return await res.json();
|
return await res.json();
|
||||||
}
|
}
|
||||||
|
|
||||||
function U2L(input) {
|
async function loadFeatures() {
|
||||||
try {
|
try {
|
||||||
const datetime = new Date(input * 1000);
|
features = await featureDetect();
|
||||||
return datetime.toLocaleString();
|
} catch (e) {
|
||||||
} catch (err) {
|
error = e;
|
||||||
console.log(err);
|
}
|
||||||
return false;
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
loadFeatures();
|
||||||
|
});
|
||||||
|
|
||||||
|
function U2L(input: Date | number): string {
|
||||||
|
if (input instanceof Date) {
|
||||||
|
return input.toLocaleString();
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const datetime = new Date(input * 1000);
|
||||||
|
return datetime.toLocaleString();
|
||||||
|
} catch (err) {
|
||||||
|
console.log(err);
|
||||||
|
return "Unknown";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<svelte:head>
|
||||||
|
<meta name="robots" content="noindex">
|
||||||
|
</svelte:head>
|
||||||
|
|
||||||
<Header {title} />
|
<Header {title} />
|
||||||
|
|
||||||
{#await getData()}
|
{#await getData()}
|
||||||
<Loading />
|
<Loading />
|
||||||
{:then data}
|
{:then data}
|
||||||
<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>
|
||||||
<p>PIS Codes: <span>{U2L(data?.updateTimes?.pis)}</span></p>
|
<p>PIS Codes: <span>{U2L(data?.updateTimes?.pis)}</span></p>
|
||||||
<p>Reason Codes: <span>{U2L(data?.updateTimes?.reasonCodes)}</span></p>
|
<p>Reason Codes: <span>{U2L(data?.updateTimes?.reasonCodes)}</span></p>
|
||||||
|
|
||||||
<h2>Request Counts</h2>
|
<h2>Request Counts</h2>
|
||||||
<p>LDBWS API: <span>{data?.requestCounts?.ldbws_api}</span></p>
|
<p>LDBWS API: <span>{data?.requestCounts?.ldbws_api}</span></p>
|
||||||
<p>LDBSVWS API: <span>{data?.requestCounts?.lsbsvws_api}</span></p>
|
<p>LDBSVWS API: <span>{data?.requestCounts?.lsbsvws_api}</span></p>
|
||||||
<p>Location Reference API: <span>{data?.requestCounts?.corpus_api}</span></p>
|
<p>Location Reference API: <span>{data?.requestCounts?.corpus_api}</span></p>
|
||||||
<p>Timetable: <span>{data?.requestCounts?.timetable_db}</span></p>
|
<p>Timetable: <span>{data?.requestCounts?.timetable_db}</span></p>
|
||||||
<p>PIS: <span>{data?.requestCounts?.pis_db}</span></p>
|
<p>PIS: <span>{data?.requestCounts?.pis_db}</span></p>
|
||||||
<p>Location Reference: <span>{data?.requestCounts?.corpus_db}</span></p>
|
<p>Location Reference: <span>{data?.requestCounts?.corpus_db}</span></p>
|
||||||
<p>Stations: <span>{data?.requestCounts?.stations_db}</span></p>
|
<p>Stations: <span>{data?.requestCounts?.stations_db}</span></p>
|
||||||
|
|
||||||
<h2>Database Lengths</h2>
|
<h2>Database Lengths</h2>
|
||||||
<p>Users: <span>{data?.dbLengths?.users}</span></p>
|
<p>Users: <span>{data?.dbLengths?.users}</span></p>
|
||||||
<p>Pending Registrations: <span>{data?.dbLengths?.registrations}</span></p>
|
<p>Pending Registrations: <span>{data?.dbLengths?.registrations}</span></p>
|
||||||
<p>CORPUS: <span>{data?.dbLengths?.corpus}</span></p>
|
<p>CORPUS: <span>{data?.dbLengths?.corpus}</span></p>
|
||||||
<p>Stations: <span>{data?.dbLengths?.stations}</span></p>
|
<p>Stations: <span>{data?.dbLengths?.stations}</span></p>
|
||||||
<p>PIS: <span>{data?.dbLengths?.pis}</span></p>
|
<p>PIS: <span>{data?.dbLengths?.pis}</span></p>
|
||||||
<p>Timetable: <span>{data?.dbLengths?.timetable}</span></p>
|
<p>Timetable: <span>{data?.dbLengths?.timetable}</span></p>
|
||||||
<p>Reason Codes: <span>{data?.dbLengths?.reasonCodes}</span></p>
|
<p>Reason Codes: <span>{data?.dbLengths?.reasonCodes}</span></p>
|
||||||
{:catch}
|
{:catch}
|
||||||
<Island>
|
<Island>
|
||||||
<p style="font-weight:600">Unable to connect to server</p>
|
<p style="font-weight:600">Unable to connect to server</p>
|
||||||
</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>
|
||||||
span {
|
span {
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
p {
|
p {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
color: lightgray;
|
color: lightgray;
|
||||||
}
|
}
|
||||||
h2 {
|
h2 {
|
||||||
font-family: urwgothic, sans-serif;
|
font-family: urwgothic, sans-serif;
|
||||||
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>
|
||||||
|
@ -1,56 +1,83 @@
|
|||||||
<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";
|
||||||
const title = 'Versions';
|
import { getApiUrl } from "$lib/scripts/upstream";
|
||||||
|
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 = 'https://owlboard.info/misc/server/versions';
|
const url = `${getApiUrl()}/misc/server/versions`;
|
||||||
const res = await fetch(url);
|
const res = await fetch(url);
|
||||||
return await res.json();
|
return await res.json();
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<svelte:head>
|
||||||
|
<meta name="robots" content="noindex">
|
||||||
|
</svelte:head>
|
||||||
|
|
||||||
<Header {title} />
|
<Header {title} />
|
||||||
|
|
||||||
<LargeLogo />
|
<LargeLogo />
|
||||||
|
|
||||||
{#await getData()}
|
{#await getData()}
|
||||||
<Loading />
|
<Loading />
|
||||||
{:then data}
|
{:then data}
|
||||||
<Island>
|
<Island>
|
||||||
<p>
|
<p>
|
||||||
<a class="data" href="https://git.fjla.uk/owlboard/owlboard-svelte" target="_blank">Web-app version<br /><span class="data">{version}-{versionTag}</span></a>
|
<Tooltip text="Svelte"><IconBrandSvelte /></Tooltip>
|
||||||
</p>
|
<Tooltip text="Javascript"><IconBrandJavascript /></Tooltip>
|
||||||
<p>
|
<Tooltip text="Typescript"><IconBrandTypescript /></Tooltip>
|
||||||
<a class="data" href="https://git.fjla.uk/owlboard/backend" target="_blank">API Server version<br /><span class="data">{data?.backend || 'Unknown'}</span></a>
|
<br />
|
||||||
</p>
|
<a class="data" href="https://git.fjla.uk/owlboard/owlboard-svelte" target="_blank"
|
||||||
<p>
|
>Web-app version<br /><span class="data"
|
||||||
<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>
|
>{version}{#if versionTag}-{versionTag}{/if}</span
|
||||||
</p>
|
></a
|
||||||
<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>
|
</p>
|
||||||
</p>
|
<p>
|
||||||
</Island>
|
<Tooltip text="NodeJS"><IconBrandNodejs /></Tooltip>
|
||||||
|
<Tooltip text="Javascript"><IconBrandJavascript /></Tooltip>
|
||||||
|
<Tooltip text="Typescript"><IconBrandTypescript /></Tooltip>
|
||||||
|
<br />
|
||||||
|
<a class="data" href="https://git.fjla.uk/owlboard/backend" target="_blank">API Server version<br /><span class="data">{data?.backend || "Unknown"}</span></a>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<Tooltip text="Python"><IconBrandPython /></Tooltip>
|
||||||
|
<br />
|
||||||
|
<a class="data" href="https://git.fjla.uk/owlboard/db-manager" target="_blank">DB Manager version<br /><span class="data">{data?.["db-manager"] || "Unknown"}</span></a>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<Tooltip text="Go"><IconBrandGolang /></Tooltip>
|
||||||
|
<br />
|
||||||
|
<a class="data" href="https://git.fjla.uk/owlboard/mq-client" target="_blank">timetable-mgr<br /><span class="data">{data?.["mq-client"] || "Not installed"}</span></a>
|
||||||
|
</p>
|
||||||
|
</Island>
|
||||||
{:catch}
|
{:catch}
|
||||||
<Island>
|
<Island>
|
||||||
<p>
|
<p>
|
||||||
Web-app Version<br /><span class="data">{version}-{versionTag}</span>
|
<IconBrandSvelte /><IconBrandJavascript /><IconBrandTypescript /><br />
|
||||||
</p>
|
Web-app Version<br /><span class="data"
|
||||||
<p>Unable to fetch server versions</p>
|
>{version}{#if versionTag}-{versionTag}{/if}</span
|
||||||
</Island>
|
>
|
||||||
|
</p>
|
||||||
|
<p>Unable to fetch server application versions</p>
|
||||||
|
</Island>
|
||||||
{/await}
|
{/await}
|
||||||
<Nav />
|
<Nav />
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
p {
|
p {
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
padding: 15px;
|
||||||
.data {
|
}
|
||||||
color: white;
|
.data {
|
||||||
text-decoration: none;
|
color: white;
|
||||||
}
|
text-decoration: none;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -1,180 +1,230 @@
|
|||||||
<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 { uuid } from "$lib/stores/uuid";
|
||||||
import Loading from '$lib/navigation/loading.svelte';
|
import StylesToc from "$lib/train/styles-toc.svelte";
|
||||||
import { uuid } from '$lib/stores/uuid';
|
import { getApiUrl } from "$lib/scripts/upstream";
|
||||||
import StylesToc from '$lib/train/styles-toc.svelte';
|
import toast from "svelte-french-toast";
|
||||||
|
import { onMount } from "svelte";
|
||||||
|
import type { OB_Pis_FullObject } from "@owlboard/ts-types";
|
||||||
|
import Card from "$lib/cards/Card.svelte";
|
||||||
|
import type { CardConfig } from "$lib/cards/Card.types";
|
||||||
|
import FindByHeadcodeCard from "$lib/cards/FindByHeadcodeCard.svelte";
|
||||||
|
|
||||||
const title = 'PIS Finder';
|
const title = "PIS Finder";
|
||||||
const variables = { title: 'Results' };
|
let entryPIS = "";
|
||||||
let entryPIS = '';
|
let entryStartCRS = "";
|
||||||
let entryStartCRS = '';
|
let entryEndCRS = "";
|
||||||
let entryEndCRS = '';
|
let data: OB_Pis_FullObject[] = [];
|
||||||
let data = [];
|
let error = false;
|
||||||
let error = false;
|
let errMsg = "Unknown Error";
|
||||||
let errMsg = 'Unknown Error';
|
|
||||||
let isLoading = false;
|
|
||||||
|
|
||||||
async function findByStartEnd() {
|
async function findByStartEnd() {
|
||||||
isLoading = true;
|
const url = `${getApiUrl()}/api/v2/pis/byStartEnd/${entryStartCRS}/${entryEndCRS}`;
|
||||||
const url = `https://owlboard.info/api/v2/pis/byStartEnd/${entryStartCRS}/${entryEndCRS}`;
|
await fetchData(url);
|
||||||
await fetchData(url);
|
|
||||||
isLoading = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function findByPis() {
|
|
||||||
isLoading = true;
|
|
||||||
const url = `https://owlboard.info/api/v2/pis/byCode/${entryPIS}`;
|
|
||||||
await fetchData(url);
|
|
||||||
isLoading = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function fetchData(url) {
|
|
||||||
const options = {
|
|
||||||
method: 'GET',
|
|
||||||
headers: {
|
|
||||||
uuid: $uuid
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const res = await fetch(url, options); // Enable Auth
|
|
||||||
if (res.status == 401) {
|
|
||||||
errMsg = 'You must be logged in to the staff version';
|
|
||||||
error = true;
|
|
||||||
return false;
|
|
||||||
} else if (res.status == 500) {
|
|
||||||
errMsg = 'Server Error, try again later';
|
|
||||||
error = true;
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
const jsonData = await res.json();
|
|
||||||
if (jsonData.ERROR == 'offline') {
|
|
||||||
errMsg = 'Connection error, check your internet connection and try again';
|
|
||||||
error = true;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
data = jsonData;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function reset() {
|
|
||||||
data = [];
|
async function findByPis() {
|
||||||
error = false;
|
const url = `${getApiUrl()}/api/v2/pis/byCode/${entryPIS}`;
|
||||||
entryPIS = '';
|
await fetchData(url);
|
||||||
entryStartCRS = '';
|
}
|
||||||
entryEndCRS = '';
|
|
||||||
}
|
async function fetchData(url: string) {
|
||||||
|
const options = {
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
uuid: $uuid,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const res = await fetch(url, options); // Enable Auth
|
||||||
|
if (res.status == 401) {
|
||||||
|
errMsg = "You must be logged in to the staff version";
|
||||||
|
toast.error("You must be registered");
|
||||||
|
error = true;
|
||||||
|
return false;
|
||||||
|
} else if (res.status == 500) {
|
||||||
|
errMsg = "Server Error, try again later";
|
||||||
|
toast.error("Server Error.", { duration: 7500 });
|
||||||
|
error = true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const jsonData = await res.json();
|
||||||
|
if (jsonData.ERROR == "offline") {
|
||||||
|
errMsg = "Connection error, check your internet connection and try again";
|
||||||
|
toast.error("You are offline.");
|
||||||
|
error = true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
data = jsonData;
|
||||||
|
let count = data.length;
|
||||||
|
if (!count) {
|
||||||
|
toast.error("No PIS Codes found.");
|
||||||
|
} else {
|
||||||
|
toast.success(`${count} PIS Codes found`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function reset() {
|
||||||
|
data = [];
|
||||||
|
error = false;
|
||||||
|
entryPIS = "";
|
||||||
|
entryStartCRS = "";
|
||||||
|
entryEndCRS = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
if ($uuid == null || $uuid == "") {
|
||||||
|
toast("You must register to see results", {
|
||||||
|
duration: 3000,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const resultsCard: CardConfig = {
|
||||||
|
title: "Results",
|
||||||
|
showHelp: false,
|
||||||
|
helpText: "",
|
||||||
|
showRefresh: false,
|
||||||
|
onRefresh: ()=>{},
|
||||||
|
refreshing: false
|
||||||
|
}
|
||||||
|
const errorCard: CardConfig = {
|
||||||
|
title: "Error",
|
||||||
|
showHelp: true,
|
||||||
|
helpText: "There was an error searching for PIS Codes",
|
||||||
|
showRefresh: false,
|
||||||
|
onRefresh: () => {},
|
||||||
|
refreshing: false,
|
||||||
|
}
|
||||||
|
const findByStartEndCard: CardConfig = {
|
||||||
|
title: "Find by Start/End CRS",
|
||||||
|
showHelp: true,
|
||||||
|
helpText: "Enter a start and end CRS Code",
|
||||||
|
showRefresh: false,
|
||||||
|
onRefresh: () => {},
|
||||||
|
refreshing: false,
|
||||||
|
}
|
||||||
|
const findByPisCodeCard: CardConfig = {
|
||||||
|
title: "Find by PIS Code",
|
||||||
|
showHelp: true,
|
||||||
|
helpText: "Enter a PIS Code to see its details. (Four digits)",
|
||||||
|
showRefresh: false,
|
||||||
|
onRefresh: () => {},
|
||||||
|
refreshing: false,
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Header {title} />
|
<Header {title} />
|
||||||
|
|
||||||
{#if isLoading}
|
|
||||||
<Loading />
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
{#if error}
|
{#if error}
|
||||||
<Island {variables}>
|
<Card config={errorCard}>
|
||||||
<p class="error">{errMsg}</p>
|
<p class="error">{errMsg}</p>
|
||||||
</Island>
|
</Card>
|
||||||
{:else if data.length}
|
{:else if data.length}
|
||||||
<Island {variables}>
|
<Card config={resultsCard}>
|
||||||
<table>
|
<table>
|
||||||
<tr>
|
<tr>
|
||||||
<th class="toc">TOC</th>
|
<th class="toc">TOC</th>
|
||||||
<th class="code">Code</th>
|
<th class="code">Code</th>
|
||||||
<th class="stops">Stops</th>
|
<th class="stops">Stops</th>
|
||||||
</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>
|
</Card>
|
||||||
|
<button id="reset" type="reset" on:click={reset}>Reset</button>
|
||||||
{:else}
|
{:else}
|
||||||
<p>To search by headcode use the Train Finder on the homepage</p>
|
<FindByHeadcodeCard />
|
||||||
<p>This feature is only supported for GWR West & Sleeper services</p>
|
<Card config={findByStartEndCard}>
|
||||||
<p class="label">Find By Start/End CRS:</p>
|
<form on:submit={findByStartEnd}>
|
||||||
<form on:submit={findByStartEnd}>
|
<input type="text" maxlength="3" pattern="^[A-Za-z]+$" autocomplete="off" placeholder="Start" required bind:value={entryStartCRS} />
|
||||||
<input type="text" maxlength="3" autocomplete="off" placeholder="Start" bind:value={entryStartCRS} />
|
<input type="text" maxlength="3" pattern="^[A-Za-z]+$" autocomplete="off" placeholder="End" required bind:value={entryEndCRS} />
|
||||||
<input type="text" maxlength="3" autocomplete="off" placeholder="End" bind:value={entryEndCRS} />
|
<br />
|
||||||
<br />
|
<button type="submit">Search</button>
|
||||||
<button type="submit">Search</button>
|
<button type="reset">Clear</button>
|
||||||
</form>
|
</form>
|
||||||
|
</Card>
|
||||||
<p class="label">Find By PIS Code:</p>
|
<Card config={findByPisCodeCard}>
|
||||||
<form on:submit={findByPis}>
|
<form on:submit={findByPis}>
|
||||||
<input type="number" max="9999" autocomplete="off" placeholder="PIS" bind:value={entryPIS} />
|
<input type="text" maxlength="4" pattern="^\d+$" autocomplete="off" placeholder="PIS" required bind:value={entryPIS} />
|
||||||
<br />
|
<br />
|
||||||
<button type="submit">Search</button>
|
<button type="submit">Search</button>
|
||||||
</form>
|
<button type="reset" >Clear</button>
|
||||||
|
</form>
|
||||||
|
</Card>
|
||||||
{/if}
|
{/if}
|
||||||
<button id="reset" type="reset" on:click={reset}>Reset</button>
|
|
||||||
<Nav />
|
<Nav />
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
p {
|
p {
|
||||||
margin-left: 10px;
|
margin-left: 10px;
|
||||||
margin-right: 10px;
|
margin-right: 10px;
|
||||||
}
|
}
|
||||||
.label {
|
input {
|
||||||
font-weight: 600;
|
border: none;
|
||||||
color: var(--main-text-color);
|
border-radius: 50px;
|
||||||
}
|
font-family: urwgothic, sans-serif;
|
||||||
input {
|
text-align: center;
|
||||||
border: none;
|
text-transform: uppercase;
|
||||||
border-radius: 50px;
|
width: 25%;
|
||||||
font-family: urwgothic, sans-serif;
|
max-width: 250px;
|
||||||
text-align: center;
|
height: 30px;
|
||||||
text-transform: uppercase;
|
font-size: 16px;
|
||||||
width: 30%;
|
margin-top: 10px;
|
||||||
max-width: 250px;
|
margin-bottom: 15px;
|
||||||
height: 30px;
|
margin-left: 10px;
|
||||||
font-size: 16px;
|
margin-right: 10px;
|
||||||
margin-bottom: 15px;
|
box-shadow: var(--box-shadow);
|
||||||
}
|
}
|
||||||
button {
|
button {
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 50px;
|
border-radius: 50px;
|
||||||
font-family: urwgothic, sans-serif;
|
font-family: urwgothic, sans-serif;
|
||||||
width: 25%;
|
width: 25%;
|
||||||
max-width: 175px;
|
max-width: 175px;
|
||||||
margin: 0px;
|
margin: 0px;
|
||||||
margin-left: 10px;
|
margin-left: 10px;
|
||||||
margin-right: 10px;
|
margin-right: 10px;
|
||||||
height: 30px;
|
margin-bottom: 15px;
|
||||||
background-color: var(--overlay-color);
|
height: 30px;
|
||||||
color: white;
|
background-color: var(--island-button-color);
|
||||||
font-size: 16px;
|
color: white;
|
||||||
}
|
font-size: 16px;
|
||||||
table {
|
box-shadow: var(--box-shadow);
|
||||||
width: 100%;
|
}
|
||||||
margin: auto;
|
table {
|
||||||
color: white;
|
width: 100%;
|
||||||
}
|
margin: auto;
|
||||||
td {
|
color: white;
|
||||||
padding-top: 5px;
|
}
|
||||||
padding-bottom: 5px;
|
td {
|
||||||
}
|
padding-top: 5px;
|
||||||
.toc {
|
padding-bottom: 5px;
|
||||||
width: 15%;
|
}
|
||||||
}
|
.toc {
|
||||||
.code {
|
width: 15%;
|
||||||
width: 20%;
|
}
|
||||||
}
|
.code {
|
||||||
.toc-data {
|
width: 20%;
|
||||||
text-transform: uppercase;
|
}
|
||||||
}
|
.toc-data {
|
||||||
.stops-data {
|
text-transform: uppercase;
|
||||||
text-align: left;
|
}
|
||||||
font-family: firamono, monospace;
|
.stops-data {
|
||||||
text-transform: uppercase;
|
text-align: left;
|
||||||
}
|
font-family: firamono, monospace;
|
||||||
.error {
|
text-transform: uppercase;
|
||||||
color: white;
|
}
|
||||||
}
|
.error {
|
||||||
#reset {
|
color: white;
|
||||||
margin: 25px;
|
}
|
||||||
}
|
#reset {
|
||||||
|
margin: 25px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -1,105 +1,163 @@
|
|||||||
<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 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 toast from "svelte-french-toast";
|
||||||
|
import TimeBar from "$lib/navigation/TimeBar.svelte";
|
||||||
|
import { IconArrowLeft, IconArrowRight, IconCheck } from "@tabler/icons-svelte";
|
||||||
|
import Error from "../+error.svelte";
|
||||||
|
|
||||||
let title = 'Timetable Results';
|
let title = "Timetable Results";
|
||||||
let id = '';
|
let id = "";
|
||||||
let data = [];
|
let data = [];
|
||||||
let isLoading = true;
|
let error = false;
|
||||||
let error = false;
|
let errMsg = "";
|
||||||
let errMsg = '';
|
|
||||||
|
|
||||||
$: {
|
let formattedDate = new Date().toISOString().split('T')[0];
|
||||||
if (id) {
|
|
||||||
title = id.toUpperCase();
|
$: {
|
||||||
} else {
|
if (id) {
|
||||||
title = 'Querying Timetable';
|
title = id.toUpperCase();
|
||||||
|
} else {
|
||||||
|
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 () => {
|
|
||||||
isLoading = true;
|
|
||||||
id = (await getHeadcode()) || '';
|
|
||||||
const res = await fetchData(id);
|
|
||||||
if (res) {
|
|
||||||
data = res;
|
|
||||||
if (!data.length) {
|
|
||||||
error = true;
|
|
||||||
errMsg = 'No services found';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
isLoading = false;
|
|
||||||
});
|
|
||||||
|
|
||||||
async function fetchData(id = '') {
|
function incrementDate() {
|
||||||
const date = 'now';
|
let dateInput = new Date(formattedDate)
|
||||||
const searchType = 'headcode';
|
dateInput.setDate(dateInput.getDate() + 1);
|
||||||
const options = {
|
formattedDate = dateInput.toISOString().split('T')[0];
|
||||||
method: 'GET',
|
}
|
||||||
headers: {
|
|
||||||
uuid: $uuid
|
function decrementDate() {
|
||||||
}
|
let dateInput = new Date(formattedDate)
|
||||||
};
|
dateInput.setDate(dateInput.getDate() - 1);
|
||||||
const url = `https://owlboard.info/api/v2/timetable/train/${date}/${searchType}/${id}`;
|
formattedDate = dateInput.toISOString().split('T')[0];
|
||||||
try {
|
}
|
||||||
const res = await fetch(url, options);
|
|
||||||
if (res.status == 200) {
|
onMount(async () => {
|
||||||
return await res.json();
|
id = (await getHeadcode()) || "";
|
||||||
} else if (res.status === 401) {
|
load();
|
||||||
error = true;
|
|
||||||
errMsg = 'You must be logged into the staff version for this feature';
|
if ($uuid == null || $uuid == "") {
|
||||||
return false;
|
toast("Register to see PIS codes", {
|
||||||
} else {
|
duration: 3000,
|
||||||
error = true;
|
});
|
||||||
errMsg = 'Unable to connect, check your connection and try again';
|
}
|
||||||
return false;
|
});
|
||||||
}
|
|
||||||
} catch (err) {
|
function load() {
|
||||||
error = true;
|
error = false;
|
||||||
errMsg = 'Connection error, try again later';
|
const selectedDate = new Date(formattedDate);
|
||||||
|
const currentDate = new Date();
|
||||||
|
const difference: number = currentDate.getTime() - selectedDate.getTime();
|
||||||
|
const differenceDays: number = difference / (1000 * 60 * 60 * 24)
|
||||||
|
if (differenceDays > 7) {
|
||||||
|
toast.error("Timetable data is not available for dates older than a week")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
toast.promise(
|
||||||
|
fetchData(id),
|
||||||
|
{
|
||||||
|
loading: 'Searching Timetable',
|
||||||
|
success: 'Done',
|
||||||
|
error: 'No Services Found'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchData(id = "") {
|
||||||
|
data = [];
|
||||||
|
const searchType = "headcode";
|
||||||
|
const options = {
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
uuid: $uuid,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const url = `${getApiUrl()}/api/v2/timetable/train/${formattedDate}/${searchType}/${id}`;
|
||||||
|
try {
|
||||||
|
const res = await fetch(url, options);
|
||||||
|
if (res.status < 300) {
|
||||||
|
let services = await res.json();
|
||||||
|
if (!services.length) {
|
||||||
|
error = true;
|
||||||
|
errMsg = "No services found";
|
||||||
|
return Promise.reject(new Error(errMsg));
|
||||||
|
}
|
||||||
|
data = services
|
||||||
|
} else if (res.status === 401) {
|
||||||
|
error = true;
|
||||||
|
errMsg = "You must be logged into the staff version for this feature";
|
||||||
|
return Promise.reject(new Error(errMsg));
|
||||||
|
} else {
|
||||||
|
error = true;
|
||||||
|
errMsg = "Unable to connect, check your connection and try again";
|
||||||
|
return Promise.reject(new Error(errMsg));
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
error = true;
|
||||||
|
errMsg = "Connection error, try again later";
|
||||||
|
return Promise.reject(new Error(errMsg));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
isLoading = false;
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<svelte:head>
|
||||||
|
<meta name="robots" content="noindex">
|
||||||
|
</svelte:head>
|
||||||
|
|
||||||
<Header {title} />
|
<Header {title} />
|
||||||
<div id="whitespace" />
|
<TimeBar updatedTime={undefined} />
|
||||||
|
<div id="dateSelector">
|
||||||
|
<button on:click={decrementDate}><IconArrowLeft /></button>
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
id="dateInput"
|
||||||
|
bind:value={formattedDate}
|
||||||
|
/>
|
||||||
|
<button on:click={incrementDate}><IconArrowRight /></button>
|
||||||
|
<button on:click={load}><IconCheck /></button>
|
||||||
|
</div>
|
||||||
{#if error}
|
{#if error}
|
||||||
<Island>
|
<Island>
|
||||||
<p style="font-weight:600">{errMsg}</p>
|
<p style="font-weight:600">{errMsg}</p>
|
||||||
</Island>
|
</Island>
|
||||||
{/if}
|
|
||||||
|
|
||||||
{#if isLoading}
|
|
||||||
<Loading />
|
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#each data as service}
|
{#each data as service}
|
||||||
{#if service}
|
{#if service}
|
||||||
<TrainDetail {service} />
|
<TrainDetail {service} date={new Date(formattedDate)} />
|
||||||
{/if}
|
{/if}
|
||||||
{/each}
|
{/each}
|
||||||
|
|
||||||
<Nav />
|
<Nav />
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
#whitespace {
|
p {
|
||||||
height: 15px;
|
color: white;
|
||||||
}
|
font-size: 18px;
|
||||||
p {
|
font-weight: 600;
|
||||||
color: white;
|
}
|
||||||
font-size: 18px;
|
|
||||||
font-weight: 600;
|
#dateInput {
|
||||||
}
|
height: 25px;
|
||||||
|
transform: translateY(-8px) translateX(20px);
|
||||||
|
}
|
||||||
|
button {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: white;
|
||||||
|
transform: translateX(20px);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -1,48 +1,58 @@
|
|||||||
/// <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 assets = [...build, ...files, '/service-worker.js'];
|
const assets = [...build, ...files, "/service-worker.js"];
|
||||||
|
|
||||||
self.addEventListener('install', (event) => {
|
const excludePatterns = [
|
||||||
async function addToCache() {
|
"/static/images/screnshots",
|
||||||
const cache = await caches.open(cacheName);
|
"/images/screenshots",
|
||||||
await cache.addAll(assets);
|
"/static/images/shortcuts",
|
||||||
}
|
"/images/shortcuts",
|
||||||
|
];
|
||||||
|
|
||||||
event.waitUntil(addToCache());
|
self.addEventListener("install", (event) => {
|
||||||
});
|
async function addToCache() {
|
||||||
|
const cache = await caches.open(cacheName);
|
||||||
self.addEventListener('activate', (event) => {
|
const assetsToCache = assets.filter(asset => {
|
||||||
async function deleteOldCache() {
|
return !excludePatterns.some(pattern => asset.startsWith(pattern));
|
||||||
for (const key of await caches.keys()) {
|
});
|
||||||
if (key !== cacheName) {
|
await cache.addAll(assetsToCache);
|
||||||
await caches.delete(key);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
event.waitUntil(deleteOldCache());
|
|
||||||
});
|
|
||||||
|
|
||||||
self.addEventListener('fetch', (event) => {
|
|
||||||
if (event.request.method !== 'GET') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
async function respond() {
|
|
||||||
const cacheRes = await caches.match(event.request, { ignoreSearch: true });
|
|
||||||
if (cacheRes) {
|
|
||||||
return cacheRes;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
event.waitUntil(addToCache());
|
||||||
return await fetch(event.request);
|
});
|
||||||
} catch (err) {
|
|
||||||
return { error: 'OFFLINE', errorMsg: 'You are offline' };
|
self.addEventListener("activate", (event) => {
|
||||||
}
|
async function deleteOldCache() {
|
||||||
}
|
for (const key of await caches.keys()) {
|
||||||
|
if (key !== cacheName) {
|
||||||
event.respondWith(respond());
|
await caches.delete(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
event.waitUntil(deleteOldCache());
|
||||||
|
});
|
||||||
|
|
||||||
|
self.addEventListener("fetch", (event) => {
|
||||||
|
if (event.request.method !== "GET") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
async function respond() {
|
||||||
|
const cacheRes = await caches.match(event.request, { ignoreSearch: true });
|
||||||
|
if (cacheRes) {
|
||||||
|
return cacheRes;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return await fetch(event.request);
|
||||||
|
} catch (err) {
|
||||||
|
return { error: "OFFLINE", errorMsg: "You are offline" };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
event.respondWith(respond());
|
||||||
});
|
});
|
||||||
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
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 |
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user