Compare commits

...

175 Commits

Author SHA1 Message Date
4f7acf9ffb Adjust policy and error message language to be more friendly. 2025-05-02 21:02:46 +01:00
1c308321de Friendly error messages for featureDetect 2025-05-02 20:47:26 +01:00
182136fc6b Replace lookup card on homepage with FindByHeadcodeCard. 2025-05-02 20:45:05 +01:00
46c15f9601 Add find by headcode Card to PIS page 2025-05-02 20:41:51 +01:00
ec413b6e5c Fix train page: don't show error if no services are found 2025-05-02 20:24:02 +01:00
0011bdb751 Privacy improvements:
- Add telemetry consent modal
 - Conditionally load telemetry script
 - Add telemetry consent to settings
 - Update and clarify privacy policy
 - Bump version number
2025-03-09 22:50:42 +00:00
059eae3784 - Remove Matomo analytics code
- Add noindex tags on some pages
 - Bump version
 - Move Liwan analytics code from app.html to +layout.svelte
2025-03-06 14:50:11 +00:00
ee6e81de62 Upgrade analytics script 2025-03-06 14:10:31 +00:00
f6223ee826 Add Matomo tag mgr 2025-03-06 13:53:03 +00:00
58832ea2a8 Add Matomo analytics 2025-03-06 13:14:25 +00:00
479cc3051f Add anonymous analytics 2025-03-05 21:57:22 +00:00
de2258e309 Fix manifest image width 2025-02-13 01:55:03 +00:00
c254588a55 Bump version 2025-02-13 01:47:57 +00:00
4f84653c79 Fix small screenshots in manifest 2025-02-13 01:46:55 +00:00
7cfcdc7205 Bump version number 2025-01-22 00:10:55 +00:00
6d2ddb9966 Remove superfluous warning toasts.
Change registration code input to single input field
2025-01-22 00:10:30 +00:00
58ef9c153e Add maintenance mode 2024-11-22 18:04:10 +00:00
e9028153cb Fix translation of date selector elements 2024-11-15 11:35:48 +00:00
fb540c7a46 Fix date handling on train page 2024-11-15 11:34:02 +00:00
c672495a5f Adjust menu order 2024-11-14 01:52:25 +00:00
8b361bb7de Trial date selection in timetable search 2024-11-13 14:35:19 +00:00
29b2054b4c Update import to import type on more/reasons 2024-11-13 12:42:14 +00:00
c9262d64c8 Update PIS Finder to use Cards rather than Islands 2024-11-13 12:40:25 +00:00
2bc6efc677 Bump version to 2024.11.1 2024-11-11 10:56:02 +00:00
a7d4158fb5 Update src/routes/pis/+page.svelte 2024-11-10 19:17:38 +00:00
1d1e9416ab Re-enable 'Find by Code'
Backend fixes now mean this feature works again.
2024-11-10 19:13:32 +00:00
8a030964a4 Fix nginx-cnf 2024-10-12 20:22:57 +01:00
fe3e6be4ad Update nginx config 2024-10-12 19:57:36 +01:00
be850f5bd1 Stop caching shortcut icons - that is down to the OS. 2024-07-12 20:50:05 +01:00
0c635d99dd Change dash type used 2024-07-12 20:49:53 +01:00
70bba6635f Remove unused image sizes 2024-07-12 20:45:10 +01:00
53c5309485 Remove PNG logo images 2024-07-12 20:43:09 +01:00
7763f567f6 Move @tabler icons from dependencies to dev-dependencies 2024-07-12 20:17:29 +01:00
d48d4ffe4a Remove unneeded XCF version of file 2024-07-12 20:03:51 +01:00
10d749a5a7 Remove unused CSS Selector 2024-07-12 19:58:21 +01:00
ec4ba07cf7 Bump version 2024-07-12 19:56:19 +01:00
78fc63fe29 Remove screenshots from SW Cache 2024-07-12 19:53:45 +01:00
4526cfa3e0 Remove truetype font files 2024-07-12 19:37:48 +01:00
b6a8bd0461 Add toast to Public LDB suggesting registering 2024-07-12 19:34:58 +01:00
f92f01af16 Migrate transport mode icons to Tabler Icons 2024-07-12 19:29:45 +01:00
d75b69df26 Remove redundant images 2024-07-12 15:46:44 +01:00
2a07b0fa3e Adjust Link & Script button margins and fix layout of NearToMeCard to allow for it. 2024-07-12 15:44:37 +01:00
3d40445728 Remove text shadow from header bar - it looks stupid 2024-07-12 15:35:37 +01:00
31b4653ca2 Run prettier for formatting fixes 2024-07-12 15:29:55 +01:00
2b8d32f3c9 blockLoading of near to me card until stores have been read 2024-07-12 15:26:51 +01:00
e3632986c2 Remove 'welcome' components 2024-07-12 15:14:39 +01:00
693ad67980 Remove redundant navigartion icons 2024-07-12 15:11:51 +01:00
a8b7379700 Remove redundant alert icon 2024-07-12 15:10:18 +01:00
6d175f300f Implement time bar and update LDB components to handle it 2024-07-12 15:04:06 +01:00
7f1dc1ac3f Update and implement TimeBar, needs testing with NRCC messages present. 2024-07-12 12:20:36 +01:00
6f800dab67 Add TimeBar 2024-07-11 21:19:42 +01:00
5d84ac8ae2 Update manifest screenshots 2024-07-11 21:02:56 +01:00
872ea9f1d6 Bump version 2024-07-07 21:27:52 +01:00
284dedbb3f Replace result-island 2024-07-07 21:23:15 +01:00
512c77e81c Replace homescreen 'islands' with 'cards' and remove now unused 'island' components 2024-07-07 21:02:10 +01:00
9ad046dd9f Update link URL in QuickLinkCard 2024-07-07 10:02:37 +01:00
91d523e372 Adjust ScriptButton size for better touch target. 2024-07-07 10:02:15 +01:00
b48795563f Introduce LinkButton and ScriptButton components and updated NearToMeCard to make use of the new components 2024-07-07 09:56:07 +01:00
bf28984b80 Remove inneccessary type definitions 2024-07-07 09:33:24 +01:00
38ceb1aadd Create QuickLinkCard 2024-07-07 09:32:59 +01:00
eaa8c192a2 Add dynamic margin if alerts bar is displayed on staffLDB page 2024-07-07 08:45:41 +01:00
ba09910ff3 Remove uneeded import 2024-07-05 12:28:14 +01:00
06edc49967 Add time to LDB 2024-07-05 11:00:38 +01:00
b4a3da5174 Add nearToMeCache store using session storage 2024-07-05 10:45:38 +01:00
bebf2eba99 Adjust near to me refresh animation 2024-07-05 01:18:27 +01:00
a3bf2af68d Add rotating reload icon 2024-07-05 01:12:56 +01:00
b779429346 Remove unused imports and CSS Selectors 2024-07-03 11:47:58 +01:00
db2a764167 Update link spacing on NearToMeCard 2024-07-03 11:47:03 +01:00
89109a3a48 Adjust Card subtype styling 2024-07-03 11:25:21 +01:00
d17b9c23af Adjust checking order for NearToMeCard 2024-07-03 11:16:53 +01:00
62f6454b83 Fix incorrect type import 2024-07-03 11:09:53 +01:00
c8cd0f30d1 Bmp version 2024-07-03 11:02:57 +01:00
f82d015e52 Format 2024-07-03 11:02:22 +01:00
30240edf00 Flesh out new NearToMeCard 2024-07-03 11:02:11 +01:00
7472f96b5d Add inline loading spinner component 2024-07-03 11:01:58 +01:00
b63c63f679 Update test page to test new Cards 2024-07-03 11:01:29 +01:00
f81acf348a Update reason fetcher to use apiGet function 2024-07-03 11:01:05 +01:00
5a9e55c695 Add message for when unauthorised 2024-07-03 11:00:50 +01:00
fddf9cbbaf Begin migration to new Cards component 2024-07-02 21:02:28 +01:00
e0227516d8 Rewrite Cards - not yet implemented on public pages 2024-07-02 20:18:01 +01:00
95e45c8cb1 Format 2024-07-02 20:16:57 +01:00
d09b24655a Bump version, add toast 2024-07-01 16:07:08 +01:00
f93113ec14 Complete location based work for "Near to Me" feature 2024-07-01 16:01:24 +01:00
75641bd245 Add fetch to nearest-to-me-island 2024-07-01 13:25:44 +01:00
12ef391ec4 Bump ts-types 2024-07-01 12:07:54 +01:00
982cee6bfe Comment out temporarily unused CSS 2024-06-30 11:15:10 +01:00
d3530063f3 Add near-to-me pre-feature 2024-06-30 11:13:32 +01:00
313517605b Update feature block on stats page 2024-06-24 00:20:30 +01:00
13f7163dd7 Add location to statistics 2024-06-24 00:06:30 +01:00
35dd00499f Bump version 2024-06-23 23:49:01 +01:00
4fbec34f24 Run prettier 2024-06-23 23:48:40 +01:00
3db490a0bb Allow statistics to handle date ibject or timestamp 2024-06-23 23:47:47 +01:00
cd0e051a5a Add feature detect information to statistics page 2024-06-23 23:43:16 +01:00
9e5d6d4732 Remove 'Pointless Constant' from service worker 2024-06-23 23:16:08 +01:00
8560d61348 Add feature detection and warnings, run formatter 2024-06-23 11:21:45 +01:00
98c9b5cc03 Adjust path types in build output 2024-06-18 23:42:05 +01:00
1df751c9ca Adjust forwarding 2024-06-18 23:04:39 +01:00
60ece7661c Update nginx caching 2024-06-18 23:04:09 +01:00
c93f36102e Introduce toasts 2024-06-13 21:59:01 +01:00
0d0875f893 Prettier - add trailing commas 2024-04-30 11:18:21 +01:00
1fba04b2aa Update prettierrc 2024-04-30 11:17:45 +01:00
af58e923de Update prettier settings 2024-04-30 11:17:06 +01:00
1484a9068e Improve apiGet error handling 2024-04-30 11:09:42 +01:00
8bd97c308c Add central apiFetch function 2024-04-30 11:03:13 +01:00
0f2b097c34 Update screenshot images 2024-04-30 10:30:09 +01:00
9e1984566b Bump version 2024-04-28 11:56:31 +01:00
ef1c958d66 Add link to documentation website 2024-04-27 21:53:02 +01:00
955c275ac9 Update tocMap 2024-04-24 23:28:22 +01:00
5bec33c388 Adjust TOC Names 2024-04-24 23:20:48 +01:00
41f673c68f Update train detail layout to improve icon placement 2024-04-23 19:53:43 +01:00
2a615a822e Update to handle new serviceDetails object 2024-04-23 14:58:27 +01:00
1b21dacfd9 Update Dependencies 2024-04-23 14:56:15 +01:00
735853aa8d Optimise nginx configuration and enable dynamic compression 2024-04-19 21:17:16 +01:00
958eabe76e Bump version, add .dockerignore to improve buildtimes 2024-04-19 20:51:12 +01:00
7dcf0c8b1b Update PIS Icon from numbers to dialpad 2024-04-19 20:46:34 +01:00
ee8b547a19 Add tooltip to version icons 2024-04-19 17:10:55 +01:00
3b7f34bdab Update nginx.conf to default to kubernetes external deployment 2024-04-17 13:13:51 +01:00
3abdc7d740 Add non-public, pass times, and booked platforms to train details 2024-04-17 13:04:31 +01:00
8c91a50a34 Add non-passenger locations to train detail and prepare for 'pass' times to be made available from API. 2024-04-17 12:20:50 +01:00
21eabfc7d7 Remove 'Find by PIS Code' as it is not working, and probably isn't that useful! 2024-04-17 12:20:29 +01:00
94434cdcf8 Fix icon layout on versions page 2024-04-17 12:19:57 +01:00
b6d3d128bc Bump version 2024-04-17 12:19:43 +01:00
0f7deee78a Add commented out Docker version upstream 2024-04-17 12:19:28 +01:00
d107416bb0 Re-add package-lock 2024-04-17 10:16:54 +01:00
70fb62fd6f Adjust CSS 2024-04-17 10:11:45 +01:00
33fb2a607f Add icons to menu and versions page 2024-04-17 09:59:32 +01:00
0a666afc58 Add train detail icons with tooltips 2024-04-16 21:28:11 +01:00
3f4a172f48 Add tabler icons, run npm update 2024-04-15 21:34:45 +01:00
42e695d89f Update version string in NPM files 2024-03-31 19:59:07 +01:00
4537ff51a8 Adjust the text on the PIS page to clarify support of all GWR services. 2024-03-31 19:58:13 +01:00
a52d1fa173 Adjust the version string format to ensure integer comparison can be made. Changed from 2024.3.0 to 2024.03.2 2024-03-31 19:57:46 +01:00
1bb9db3bc3 Adjust the welcome messages 2024-03-31 19:56:52 +01:00
80b3c235af Replace registration stream with code based 2024-03-10 20:43:29 +00:00
4dd9ea05d6 Add code registration page 2024-03-09 20:35:17 +00:00
b9d18950b9 Add ASCII art to <head> 2024-03-09 19:07:56 +00:00
6c0d152358 Change welcome message referencing OwlBoard/backend#71 2024-03-01 22:13:08 +00:00
a4276bd0e9 Bump version 2024-02-19 11:25:21 +00:00
008e106877 Remove extra 'first' from PIS handler pis-text string 2024-02-19 11:24:54 +00:00
bf93df98cd Fix & close OwlBoard/backend#67
Page pathname now contains trailing '/' due to Sveltekit changes.
2024-02-19 11:23:31 +00:00
27e56b4177 Adjust welcome page 2024-02-11 12:51:56 +00:00
782a60191b Fix PIS handler spacing 2024-02-11 12:50:30 +00:00
476b5e45c3 Update version strings and adjust 'Welcome' text 2024-02-05 21:30:06 +00:00
9cd082193b Adjust train detail dtop box to space out PIS details 2024-02-05 21:07:56 +00:00
b9f5f3dc1d Add note when train is planned cancelled 2024-02-05 20:10:11 +00:00
f5760acd47 Add VSTP Text 2024-02-03 21:53:57 +00:00
5029ca7088 Prepare PIS Handler - requires testing 2024-02-03 21:46:08 +00:00
e3c8cc069f Bump version 2024-01-16 20:47:34 +00:00
23d75880b5 format files 2024-01-16 20:34:41 +00:00
e6ee0dc321 Add handler for PIS object 2024-01-16 20:34:31 +00:00
6505ba7f60 Fix Island Line in TOC Map.
Closes OwlBoard/backend#61
2024-01-13 22:25:32 +00:00
b17fa46151 Updated upstream URL 2024-01-13 21:13:10 +00:00
84c497f3c5 Update Help links to include Facebook Page. 2023-12-04 11:10:53 +00:00
20695549b5 Bump @owlboard/ts-types version 2023-11-30 09:17:48 +00:00
ceb8533154 Push useless update to fix cache failure on one device 2023-11-24 12:41:55 +00:00
2fc19dfab8 Add sorry message 2023-11-14 21:06:29 +00:00
1be9fea029 Add welcome message - November downtime 2023-11-10 20:51:36 +00:00
08cd8257a7 Closes OwlBoard/backend#52 2023-11-09 20:30:46 +00:00
a8a81970d6 Reduce overlay-island.svelte height.
Closes OwlBoard/backend#30
2023-11-09 20:29:27 +00:00
59d6eb7001 Add styling to report issue buttons
Closes OwlBoard/backend#53
2023-11-09 20:26:58 +00:00
4a2e4fd2aa Add PWA Shortcuts and associated icons 2023-10-16 12:32:04 +01:00
bb3ae45a37 Adjust screenshot sizes 2023-10-16 12:16:52 +01:00
a4294c36a8 Add screenshots to PWA Manifest 2023-10-16 12:08:52 +01:00
45b64ef4ea Add screenshots to PWA Manifest 2023-10-16 11:59:14 +01:00
b02e015496 Update remaining elements with new theme colors 2023-10-16 11:52:05 +01:00
5092de122a Add canonical link tags 2023-10-16 11:17:34 +01:00
a60448613b Assign new colour scheme 2023-10-10 12:46:42 +01:00
74079e6d9a Adjust theme 2023-10-06 12:50:21 +01:00
d49c725215 Update color vars to new var names 2023-10-06 12:47:18 +01:00
72495a63be Update color vars in Svelte components - BUT NOT PAGES 2023-10-06 12:31:17 +01:00
e2cefdda08 Move themes to separate CSS file 2023-10-06 11:53:35 +01:00
bb0d389b09 Adjust navigation shadow 2023-10-06 11:23:46 +01:00
8773b080f3 Add box shadow to overlay and islands 2023-10-06 11:21:00 +01:00
3d22bf5910 Remove errant comma 2023-10-04 11:55:35 +01:00
daf7085d60 Add welcome message 2023-10-04 11:53:14 +01:00
41cc0b5ea1 StaffLDB-Minify (#1)
New staffLDB API Ready to merge

Reviewed-on: #1
2023-10-03 21:36:58 +01:00
142 changed files with 8315 additions and 7381 deletions

9
.dockerignore Normal file
View File

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

View File

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

View File

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

View File

@ -1,4 +1,5 @@
load_module modules/ngx_http_brotli_static_module.so;
load_module modules/ngx_http_brotli_filter_module.so;
user nginx;
worker_processes auto;
@ -20,7 +21,7 @@ http {
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;
access_log /dev/stdout main;
sendfile on;
@ -31,7 +32,14 @@ http {
proxy_cache_path /var/cache/nginx keys_zone=owl_cache:20m inactive=24h;
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 {
@ -44,24 +52,33 @@ http {
location / {
root /usr/share/nginx/html;
index index.html;
index /index.html;
gzip_static on;
brotli_static on;
error_page 500 502 503 504 /50x.html;
try_files $uri $uri.html $uri/ =404;
error_page 500 502 503 504 /err/50x.html;
try_files $uri $uri/ $uri.html /index.html;
add_header Cache-Control "public, no-transform, max-age=1209600";
}
location /misc/ {
proxy_pass http://backend;
brotli on;
brotli_comp_level 6;
brotli_types *;
gzip on;
gzip_comp_level 6;
gzip_types *;
}
location /api/ {
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_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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

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

View File

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

View File

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

59
src/lib/Tooltip.svelte Normal file
View 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>

View File

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

View File

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

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

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

View File

@ -0,0 +1,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}

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,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>

View File

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

View File

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

View File

@ -1,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>

View File

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

View File

@ -1,31 +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>

View File

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

View File

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

View File

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

View File

@ -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;
});
}

View File

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

View File

@ -1,220 +1,102 @@
<script>
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 TableGenerator from './table/table-generator.svelte';
<script lang="ts">
import TableGenerator from "./table/table-generator.svelte";
import Loading from "$lib/navigation/loading.svelte";
import type { StaffLdb } from "@owlboard/ts-types";
import { detailInit, defineDetail } from "./train-detail";
import TrainDetail from "./train-detail.svelte";
import { fetchStaffLdb } from "./fetch";
import AlertBar from "../common/nrcc/alert-bar.svelte";
import TimeBar from "$lib/navigation/TimeBar.svelte";
import { onMount } from "svelte";
import { IconBus, IconSailboat } from "@tabler/icons-svelte";
let requestedStation = '';
$: requestedStation = station;
export let station: string;
export let title: string | undefined = "Loading...";
let jsonData = {};
/**
* @type {string | any[]}
*/
let trainServices = [];
/**
* @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 errorDetail = {
code: "",
message: "",
};
let nrcc: string[] = [];
$: {
// @ts-ignore
if (jsonData?.GetBoardResult?.generatedAt) {
// @ts-ignore
dataAge = new Date(jsonData.GetBoardResult.generatedAt);
let detail = detailInit();
function hideDetail() {
detail = detailInit();
}
function showDetail(rid: string, uid: string, tid: string) {
detail = defineDetail(rid, uid, tid);
}
// @ts-ignore
if (jsonData?.GetBoardResult?.trainServices?.service) {
// @ts-ignore
trainServices = ensureArray((trainServices = jsonData.GetBoardResult.trainServices.service));
} else {
trainServices = [];
}
console.log(`Station: ${station}`);
// @ts-ignore
if (jsonData?.GetBoardResult?.busServices?.service) {
// @ts-ignore
busServices = ensureArray((busServices = jsonData.GetBoardResult.busServices.service));
} else {
busServices = [];
}
let updatedTime: Date;
// @ts-ignore
if (jsonData?.GetBoardResult?.ferryServices?.service) {
// @ts-ignore
ensureArray((ferryServices = jsonData.GetBoardResult.ferryServices.service));
} else {
ferryServices = [];
}
// @ts-ignore
if (jsonData?.GetBoardResult?.locationName) {
// @ts-ignore
title = jsonData.GetBoardResult.locationName;
} else {
title = 'Loading Board';
}
// @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
async function callFetch(station: string): Promise<StaffLdb> {
console.log("callFetch function called");
const data = await fetchStaffLdb(station);
if (data.data) {
title = data.data.locationName;
if (data.data?.nrccMessages) {
for (const msg of data.data.nrccMessages) {
nrcc.push(msg.xhtmlMessage);
}
nrcc = nrcc; // Reassign to ensure Svelte reloads
}
if (data.data.generatedAt) {
updatedTime = new Date(data.data.generatedAt);
}
return data.data;
}
};
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
errorDetail.code = data.obStatus.toString() || "UNKNOWN";
errorDetail.message = data.obMsg || "An unknown error occoured";
throw new Error("Unable to Fetch Data");
}
}
/**
* @param {any} messages
*/
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();
});
onMount(async () => {
console.log("staff-ldb component mounted");
});
</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 nrcc.length}
<AlertBar alerts={nrcc} />
{/if}
<style>
p.dataTime {
margin-top: 5px;
margin-bottom: 0px;
font-size: 12px;
}
#noservices {
margin: 20px;
padding-top: 20px;
color: white;
}
<TimeBar bind:updatedTime />
.transport-mode-image {
width: 30px;
margin: auto;
padding-top: 25px;
}
.transport-mode-text {
color: white;
}
{#key detail}
{#if detail.show}
<TrainDetail {detail} close={hideDetail} />
{/if}
{/key}
{#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>

View File

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

View File

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

View File

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

View File

@ -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;
}

View File

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

64
src/lib/main.css Normal file
View 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;
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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}>&lt;</button>
{/if}
{#if pageNum < pageText.length - 1}
<button in:fade={{ delay: 350, duration: 250 }} out:fade={{ duration: 250 }} class="navButton" id="buttonRight" type="button" on:click={pageUp}>&gt;</button>
{/if}
</div>
<style>
#popup {
position: fixed;
top: 50px;
left: 50%;
transform: translateX(-50%);
width: 85%;
height: 75vh;
max-height: 600px;
overflow-y: auto;
max-width: 400px;
margin: auto;
margin-top: 25px;
padding: 10px;
background-color: 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>

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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;
}

View 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);
}

View File

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

View File

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

View File

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

View File

@ -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));
});
}
}

View File

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

View File

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

View File

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

View File

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

60
src/lib/themes.css Normal file
View 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);
}
*/

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

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

View File

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

View File

@ -1,31 +1,31 @@
<script>
import LargeLogo from '$lib/images/large-logo.svelte';
import Header from '$lib/navigation/header.svelte';
import Nav from '$lib/navigation/nav.svelte';
import LargeLogo from "$lib/images/large-logo.svelte";
import Header from "$lib/navigation/header.svelte";
import Nav from "$lib/navigation/nav.svelte";
const title = 'About';
const title = "About";
</script>
<Header {title} />
<LargeLogo />
<p class="neg">&copy; 2022-2023 Frederick Boniface</p>
<p class="neg">&copy; 2022-2025 Frederick Boniface</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>Data is sourced from National Rail Enquiries, the OwlBoard Project, and Network Rail.</p>
<p>
OwlBoard components are available under various Open Source licenses. Please refer to the
<a href="https://git.fjla.uk/OwlBoard" target="_blank">code repository</a>
for more detailed information.
OwlBoard components are available under various Open Source licenses. Please refer to the
<a href="https://git.fjla.uk/OwlBoard" target="_blank">code repository</a>
for more detailed information.
</p>
<Nav />
<style>
p {
margin-left: 10px;
margin-right: 10px;
}
.neg {
margin-top: -40px;
margin-bottom: 40px;
}
p {
margin-left: 10px;
margin-right: 10px;
}
.neg {
margin-top: -40px;
margin-bottom: 40px;
}
</style>

View File

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

View File

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

View File

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

View File

@ -1,57 +1,103 @@
<script>
import Header from '$lib/navigation/header.svelte';
import Nav from '$lib/navigation/nav.svelte';
const title = 'Privacy Policy';
import Header from "$lib/navigation/header.svelte";
import Nav from "$lib/navigation/nav.svelte";
const title = "Privacy Policy";
</script>
<svelte:head>
<meta name="robots" content="noindex">
</svelte:head>
<Header {title} />
<div>
<p>
OwlBoard stores the minimum amount of data necessary to provide its functions for your use. No personal data is stored unless you report an issue. To review the specific data
that we store, please visit <a href="/more/data">My Data</a>.
</p>
<p>OwlBoard does not utilize any cookies. IP addresses and browser fingerprints are not logged.</p>
<h2>If You Do Not Sign Up</h2>
<p>
If you choose not to sign up, no personal data will be processed or stored unless you report an issue. Any personal settings are stored locally in your browser and do not leave
your device.
</p>
<h2>If You Sign Up</h2>
<p>
If you sign up for the rail staff version of OwlBoard, we do require the storage of some data. However, none of this data can be used to personally identify you. Any personal
settings are stored locally in your browser and do not leave your device.
</p>
<p>
During the sign-up process, you will be asked to provide a work email address, which will be checked to confirm its origin from a railway company. Once confirmed, an email
containing a registration link will be sent to you. At this point, the username portion of your email address is discarded. For example, if your email address is
'a-user@owlboard.info', only 'owlboard.info' will be stored. This host portion of your email address is stored to filter and display relevant results prominently.
</p>
<p>The email server may store the address and message content as part of its regular operation, and your consent to this is implied when you sign up.</p>
<p>In addition to the host portion of your email address, a randomly generated UUID is stored for the purpose of authorizing access to the rail staff data.</p>
<h2>Reporting an Issue</h2>
<p>When you report an issue, certain data is collected, including your browser's User Agent string and the size of the window in which you are viewing the website.</p>
<p>
Any data submitted when reporting an issue will be publicly viewable alongside the <a href="https://git.fjla.uk/owlboard/backend/issues" target="_blank"
>OwlBoard/backend git repository</a
>.
</p>
<h2>Your Data</h2>
<p>
OwlBoard logs the IP addresses of its users, this is done on the basis of legitimate
interest to ensure the security of the platform and to protect all users from
malicious activity. See <a href="#datasharing">Data Sharing</a> for details on how
we may share this data.
</p>
<p>
With the exception of sending emails, all data is held - and always remains within -
the United Kingdom.
</p>
<h3>Telemetry</h3>
<p>
If you opt-in to Telemetry, you will share your IP address and information about the
type of device and software you're using to access the service. This data is
anonymised and cannot be traced back to any individual. You can opt-in or opt-out in
your <a href="/more/settings">Settings</a>. All of the anonymised data can be viewed
at: <a target="_blank" href="https://liwan.fjla.uk">liwan.fjla.uk</a> at any time.
</p>
<p>
All of the data that is stored is held within the United Kingdom.
</p>
<p>
Telemetry data is used to identify which areas of the webapp are used most frequently
and where improvements need to be made.
</p>
<h3>Registering</h3>
<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>
<Nav />
<style>
div {
text-align: left;
}
div {
text-align: left;
}
h2 {
color: var(--second-text-color);
margin: 10px;
padding-top: 20px;
}
h2 {
color: var(--second-text-color);
margin: auto;
width: 90vw;
padding-top: 20px;
}
p {
color: white;
margin: 10px;
}
h3 {
color: var(--second-text-color);
margin: auto;
width: 90vw;
padding-top: 10px;
}
p {
color: white;
margin: auto;
padding-top: 5px;
padding-bottom: 8px;
width: 90vw;
max-width: 550px;
}
</style>

View File

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

View File

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

View File

@ -1,79 +1,104 @@
<script>
import Header from '$lib/navigation/header.svelte';
import Loading from '$lib/navigation/loading.svelte';
import Nav from '$lib/navigation/nav.svelte';
import { uuid } from '$lib/stores/uuid.js';
import { onMount } from 'svelte';
<script lang="ts">
import Header from "$lib/navigation/header.svelte";
import Nav from "$lib/navigation/nav.svelte";
import { getApiUrl } from "$lib/scripts/upstream";
import { uuid } from "$lib/stores/uuid";
const title = 'Registration';
let state = '';
let isLoading = true;
const title = "Submit Registration";
let state = false;
let status: string;
let inputString: string;
async function getUUID() {
return new URLSearchParams(window.location.search).get('key');
}
async function submit(id) {
const url = 'https://owlboard.info/api/v2/user/register';
const request = {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
uuid: id
})
};
const res = await fetch(url, request);
const body = await res.json();
if (body.api_key) {
uuid.set(body.api_key);
return 201;
async function handleSubmit() {
console.log(`Code: ${inputString}`);
const res = await submit(inputString);
console.log(`Registration Status: ${res}`);
if (res == 201) {
status = "okay";
} else if (res == 401) {
status = "fail";
} else {
console.error("Unable to register: ", status);
}
state = true;
}
return res.status;
}
onMount(async () => {
const id = (await getUUID()) || '';
if (id == '' || !id) {
state = 'none';
isLoading = false;
return;
async function submit(id: string): Promise<number> {
const url = `${getApiUrl()}/api/v2/user/register`;
const request = {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
uuid: id,
}),
};
const res = await fetch(url, request);
const body = await res.json();
if (body.api_key) {
uuid.set(body.api_key);
return 201;
} else {
return res.status;
}
}
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>
<svelte:head>
<meta name="robots" content="noindex">
</svelte:head>
<Header {title} />
{#if isLoading}
<Loading />
{/if}
{#if state == 'none'}
<p>Unable to read your access key.</p>
<p>Please click the link in your email again.</p>
{:else if state == 'unauth'}
<p>Your link is not valid, links expire after 30 minutes.</p>
<p>You can try to register again.</p>
{:else if state == 'error'}
<p>There was an error on our end, please try again later</p>
{:else if state == 'done'}
<p>You are now logged in</p>
{#if state}
{#if status == "okay"}
<p class="title-ish">You are now registered</p>
<p>Your secret key will be stored in your browser.</p>
<p>If you change browsers, change device or clear your browsing data, you may have to register again.</p>
{:else if status == "fail"}
<p class="title-ish">Your code was not accepted</p>
<p>The code expires after 1 hour, you can check the code and enter it again or request a <a href="/more/reg">new code</a>.</p>
{/if}
{:else}
<p class="title-ish">Enter your registration code below</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}
<Nav />
<style>
p {
margin: 14px;
}
.title-ish {
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>

View File

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

View File

@ -1,12 +1,65 @@
<script>
import Header from '$lib/navigation/header.svelte';
import Nav from '$lib/navigation/nav.svelte';
import QlSet from '$lib/islands/quick-link-set-island.svelte';
const title = 'Settings';
import Header from "$lib/navigation/header.svelte";
import Nav from "$lib/navigation/nav.svelte";
import QlSet from "$lib/islands/quick-link-set-island.svelte";
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>
<svelte:head>
<meta name="robots" content="noindex">
</svelte:head>
<Header {title} />
<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 />
<style>
.checkbox-container {
display: inline-flex;
align-items: center;
margin: auto;
}
.checkbox-container input[type="checkbox"] {
margin: 0;
height: 25px;
width: 25px;
}
.checkbox-container label {
margin-left: 0;
margin-right: 25px;
font-weight: 800;
}
</style>

View File

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

View File

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

View File

@ -1,180 +1,230 @@
<script>
import Header from '$lib/navigation/header.svelte';
import Nav from '$lib/navigation/nav.svelte';
import Island from '$lib/islands/island.svelte';
import Loading from '$lib/navigation/loading.svelte';
import { uuid } from '$lib/stores/uuid';
import StylesToc from '$lib/train/styles-toc.svelte';
<script lang="ts">
import Header from "$lib/navigation/header.svelte";
import Nav from "$lib/navigation/nav.svelte";
import { uuid } from "$lib/stores/uuid";
import StylesToc from "$lib/train/styles-toc.svelte";
import { getApiUrl } from "$lib/scripts/upstream";
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 variables = { title: 'Results' };
let entryPIS = '';
let entryStartCRS = '';
let entryEndCRS = '';
let data = [];
let error = false;
let errMsg = 'Unknown Error';
let isLoading = false;
const title = "PIS Finder";
let entryPIS = "";
let entryStartCRS = "";
let entryEndCRS = "";
let data: OB_Pis_FullObject[] = [];
let error = false;
let errMsg = "Unknown Error";
async function findByStartEnd() {
isLoading = true;
const url = `https://owlboard.info/api/v2/pis/byStartEnd/${entryStartCRS}/${entryEndCRS}`;
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;
async function findByStartEnd() {
const url = `${getApiUrl()}/api/v2/pis/byStartEnd/${entryStartCRS}/${entryEndCRS}`;
await fetchData(url);
}
const jsonData = await res.json();
if (jsonData.ERROR == 'offline') {
errMsg = 'Connection error, check your internet connection and try again';
error = true;
return false;
}
data = jsonData;
}
async function reset() {
data = [];
error = false;
entryPIS = '';
entryStartCRS = '';
entryEndCRS = '';
}
async function findByPis() {
const url = `${getApiUrl()}/api/v2/pis/byCode/${entryPIS}`;
await fetchData(url);
}
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>
<Header {title} />
{#if isLoading}
<Loading />
{/if}
{#if error}
<Island {variables}>
<p class="error">{errMsg}</p>
</Island>
<Card config={errorCard}>
<p class="error">{errMsg}</p>
</Card>
{:else if data.length}
<Island {variables}>
<table>
<tr>
<th class="toc">TOC</th>
<th class="code">Code</th>
<th class="stops">Stops</th>
</tr>
{#each data as item}
<tr>
<td class="toc toc-data"><StylesToc toc={item.toc || '-'} /></td>
<td class="code">{item.code}</td>
<td class="stops stops-data">{item.stops.join(' ')}</td>
</tr>
{/each}
</table>
</Island>
<Card config={resultsCard}>
<table>
<tr>
<th class="toc">TOC</th>
<th class="code">Code</th>
<th class="stops">Stops</th>
</tr>
{#each data as item}
<tr>
<td class="toc toc-data"><StylesToc toc={item.toc || "-"} /></td>
<td class="code">{item.code}</td>
<td class="stops stops-data">{item.stops.join(" ")}</td>
</tr>
{/each}
</table>
</Card>
<button id="reset" type="reset" on:click={reset}>Reset</button>
{:else}
<p>To search by headcode use the Train Finder on the homepage</p>
<p>This feature is only supported for GWR West & Sleeper services</p>
<p class="label">Find By Start/End CRS:</p>
<form on:submit={findByStartEnd}>
<input type="text" maxlength="3" autocomplete="off" placeholder="Start" bind:value={entryStartCRS} />
<input type="text" maxlength="3" autocomplete="off" placeholder="End" bind:value={entryEndCRS} />
<br />
<button type="submit">Search</button>
</form>
<p class="label">Find By PIS Code:</p>
<form on:submit={findByPis}>
<input type="number" max="9999" autocomplete="off" placeholder="PIS" bind:value={entryPIS} />
<br />
<button type="submit">Search</button>
</form>
<FindByHeadcodeCard />
<Card config={findByStartEndCard}>
<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" pattern="^[A-Za-z]+$" autocomplete="off" placeholder="End" required bind:value={entryEndCRS} />
<br />
<button type="submit">Search</button>
<button type="reset">Clear</button>
</form>
</Card>
<Card config={findByPisCodeCard}>
<form on:submit={findByPis}>
<input type="text" maxlength="4" pattern="^\d+$" autocomplete="off" placeholder="PIS" required bind:value={entryPIS} />
<br />
<button type="submit">Search</button>
<button type="reset" >Clear</button>
</form>
</Card>
{/if}
<button id="reset" type="reset" on:click={reset}>Reset</button>
<Nav />
<style>
p {
margin-left: 10px;
margin-right: 10px;
}
.label {
font-weight: 600;
color: var(--main-text-color);
}
input {
border: none;
border-radius: 50px;
font-family: urwgothic, sans-serif;
text-align: center;
text-transform: uppercase;
width: 30%;
max-width: 250px;
height: 30px;
font-size: 16px;
margin-bottom: 15px;
}
button {
border: none;
border-radius: 50px;
font-family: urwgothic, sans-serif;
width: 25%;
max-width: 175px;
margin: 0px;
margin-left: 10px;
margin-right: 10px;
height: 30px;
background-color: var(--overlay-color);
color: white;
font-size: 16px;
}
table {
width: 100%;
margin: auto;
color: white;
}
td {
padding-top: 5px;
padding-bottom: 5px;
}
.toc {
width: 15%;
}
.code {
width: 20%;
}
.toc-data {
text-transform: uppercase;
}
.stops-data {
text-align: left;
font-family: firamono, monospace;
text-transform: uppercase;
}
.error {
color: white;
}
#reset {
margin: 25px;
}
p {
margin-left: 10px;
margin-right: 10px;
}
input {
border: none;
border-radius: 50px;
font-family: urwgothic, sans-serif;
text-align: center;
text-transform: uppercase;
width: 25%;
max-width: 250px;
height: 30px;
font-size: 16px;
margin-top: 10px;
margin-bottom: 15px;
margin-left: 10px;
margin-right: 10px;
box-shadow: var(--box-shadow);
}
button {
border: none;
border-radius: 50px;
font-family: urwgothic, sans-serif;
width: 25%;
max-width: 175px;
margin: 0px;
margin-left: 10px;
margin-right: 10px;
margin-bottom: 15px;
height: 30px;
background-color: var(--island-button-color);
color: white;
font-size: 16px;
box-shadow: var(--box-shadow);
}
table {
width: 100%;
margin: auto;
color: white;
}
td {
padding-top: 5px;
padding-bottom: 5px;
}
.toc {
width: 15%;
}
.code {
width: 20%;
}
.toc-data {
text-transform: uppercase;
}
.stops-data {
text-align: left;
font-family: firamono, monospace;
text-transform: uppercase;
}
.error {
color: white;
}
#reset {
margin: 25px;
}
</style>

View File

@ -1,105 +1,163 @@
<script>
import Header from '$lib/navigation/header.svelte';
import Loading from '$lib/navigation/loading.svelte';
import Island from '$lib/islands/island.svelte';
import Nav from '$lib/navigation/nav.svelte';
import { uuid } from '$lib/stores/uuid';
<script lang="ts">
import Header from "$lib/navigation/header.svelte";
import Island from "$lib/islands/island.svelte";
import Nav from "$lib/navigation/nav.svelte";
import { uuid } from "$lib/stores/uuid";
import { onMount } from 'svelte';
import TrainDetail from '$lib/train/train-detail.svelte';
import { onMount } from "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 id = '';
let data = [];
let isLoading = true;
let error = false;
let errMsg = '';
let title = "Timetable Results";
let id = "";
let data = [];
let error = false;
let errMsg = "";
$: {
if (id) {
title = id.toUpperCase();
} else {
title = 'Querying Timetable';
let formattedDate = new Date().toISOString().split('T')[0];
$: {
if (id) {
title = id.toUpperCase();
} else {
title = "Querying Timetable";
}
}
}
async function getHeadcode() {
return new URLSearchParams(window.location.search).get('headcode');
}
onMount(async () => {
isLoading = true;
id = (await getHeadcode()) || '';
const res = await fetchData(id);
if (res) {
data = res;
if (!data.length) {
error = true;
errMsg = 'No services found';
}
async function getHeadcode() {
return new URLSearchParams(window.location.search).get("headcode");
}
isLoading = false;
});
async function fetchData(id = '') {
const date = 'now';
const searchType = 'headcode';
const options = {
method: 'GET',
headers: {
uuid: $uuid
}
};
const url = `https://owlboard.info/api/v2/timetable/train/${date}/${searchType}/${id}`;
try {
const res = await fetch(url, options);
if (res.status == 200) {
return await res.json();
} else if (res.status === 401) {
error = true;
errMsg = 'You must be logged into the staff version for this feature';
return false;
} else {
error = true;
errMsg = 'Unable to connect, check your connection and try again';
return false;
}
} catch (err) {
error = true;
errMsg = 'Connection error, try again later';
function incrementDate() {
let dateInput = new Date(formattedDate)
dateInput.setDate(dateInput.getDate() + 1);
formattedDate = dateInput.toISOString().split('T')[0];
}
function decrementDate() {
let dateInput = new Date(formattedDate)
dateInput.setDate(dateInput.getDate() - 1);
formattedDate = dateInput.toISOString().split('T')[0];
}
onMount(async () => {
id = (await getHeadcode()) || "";
load();
if ($uuid == null || $uuid == "") {
toast("Register to see PIS codes", {
duration: 3000,
});
}
});
function load() {
error = false;
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>
<svelte:head>
<meta name="robots" content="noindex">
</svelte:head>
<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}
<Island>
<p style="font-weight:600">{errMsg}</p>
</Island>
{/if}
{#if isLoading}
<Loading />
<Island>
<p style="font-weight:600">{errMsg}</p>
</Island>
{/if}
{#each data as service}
{#if service}
<TrainDetail {service} />
{/if}
{#if service}
<TrainDetail {service} date={new Date(formattedDate)} />
{/if}
{/each}
<Nav />
<style>
#whitespace {
height: 15px;
}
p {
color: white;
font-size: 18px;
font-weight: 600;
}
p {
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>

View File

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

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