Add components and improve error page
This commit is contained in:
@@ -1,69 +1,72 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/state';
|
||||
import { error } from 'console';
|
||||
import { page } from '$app/state';
|
||||
|
||||
import stopErr from '$lib/assets/img/stop-error.svg';
|
||||
import Button from '$lib/components/ui/Button.svelte';
|
||||
</script>
|
||||
|
||||
<div class="error-wrapper">
|
||||
<div class="signal-icon">
|
||||
<div class="red-light"></div>
|
||||
</div>
|
||||
<img class="err-img" src={stopErr} alt="" role="presentation" width="150" height="210" />
|
||||
|
||||
<h2 class="label">Signal Failure: {page.status}</h2>
|
||||
<h2 class="label">{page.status}</h2>
|
||||
|
||||
<p class="error-message">
|
||||
{page.error?.message ?? "An unexpected derailment occurred."}
|
||||
</p>
|
||||
<p class="error-message">
|
||||
{page.error?.message ?? 'An unexpected derailment occurred.'}
|
||||
</p>
|
||||
|
||||
{#if page.error?.owlCode}
|
||||
<div class="debug-info">
|
||||
<code>Ref: {page.error.owlCode}</code>
|
||||
</div>
|
||||
{/if}
|
||||
{#if page.error?.owlCode}
|
||||
<div class="debug-info">
|
||||
<code>Ref: {page.error.owlCode}</code>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<a href="/" class="retry-button">Return to Home</a>
|
||||
<Button href={'/'} color={'accent'}>Return to Home</Button>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.error-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: calc(100vh - 140px);
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
font-family: 'URW Gothic', sans-serif;
|
||||
}
|
||||
.error-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: calc(100vh - 140px);
|
||||
padding: 0;
|
||||
text-align: center;
|
||||
font-family: 'URW Gothic', sans-serif;
|
||||
}
|
||||
|
||||
.label {
|
||||
color: #ff4444;
|
||||
letter-spacing: 0.2rem;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.err-img {
|
||||
height: 40%;
|
||||
width: auto;
|
||||
max-width: 180px;
|
||||
max-height: 252px;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
font-size: 1.1rem;
|
||||
color: var(--color-title);
|
||||
max-width: 300px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
@media (max-height: 600px), (max-width: 270px) {
|
||||
.err-img {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.debug-info {
|
||||
background: rgba(255,255,255,0.05);
|
||||
padding: 5px 12px;
|
||||
border-radius: 4px;
|
||||
font-size: 0.8rem;
|
||||
opacity: 0.6;
|
||||
}
|
||||
.label {
|
||||
color: #ff4444;
|
||||
letter-spacing: 0.2rem;
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
|
||||
.retry-button {
|
||||
margin-top: 40px;
|
||||
padding: 12px 24px;
|
||||
background: var(--color-brand);
|
||||
color: var(--color-title);
|
||||
text-decoration: none;
|
||||
font-weight: 600;
|
||||
border-radius: 6px;
|
||||
letter-spacing: 0.05rem;
|
||||
}
|
||||
</style>
|
||||
.error-message {
|
||||
font-size: 1.1rem;
|
||||
color: var(--color-title);
|
||||
max-width: 300px;
|
||||
margin-top: 5px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.debug-info {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
padding: 5px 12px;
|
||||
border-radius: 4px;
|
||||
font-size: 0.8rem;
|
||||
opacity: 0.6;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/state';
|
||||
import { slide, fade } from 'svelte/transition';
|
||||
|
||||
import '$lib/global.css';
|
||||
|
||||
@@ -7,20 +8,50 @@
|
||||
import logoPlain from '$lib/assets/round-logo.svg';
|
||||
import favicon from '$lib/assets/round-logo.svg';
|
||||
|
||||
import { IconHome, IconDialpad, IconSettings, IconHelp, IconDots } from '@tabler/icons-svelte';
|
||||
import { IconHome, IconDialpad, IconSettings, IconHelp, IconDots } from '@tabler/icons-svelte';
|
||||
|
||||
let { children } = $props();
|
||||
|
||||
const navItems = [
|
||||
{ label: 'Home', path: '/', icon: IconHome},
|
||||
{ label: 'PIS', path:'/pis', icon: IconDialpad},
|
||||
{ label: 'Options', path:'/preferences', icon: IconSettings},
|
||||
{ label: 'About', path: '/about', icon: IconHelp},
|
||||
]
|
||||
|
||||
const activePath = $derived(page.url?.pathname ?? '');
|
||||
// Navigation State
|
||||
|
||||
const moreIcon = IconDots;
|
||||
|
||||
const navItems = [
|
||||
{ label: 'Home', path: '/', icon: IconHome },
|
||||
{ label: 'PIS', path: '/pis', icon: IconDialpad },
|
||||
{ label: 'Options', path: '/preferences', icon: IconSettings },
|
||||
{ label: 'About', path: '/about', icon: IconHelp }
|
||||
];
|
||||
|
||||
let navWidth = $state(0);
|
||||
let menuOpen = $state(false);
|
||||
const ITEM_WIDTH = 70;
|
||||
const MORE_BUTTON_WIDTH = 70;
|
||||
|
||||
const activePath = $derived(page.url?.pathname ?? '');
|
||||
|
||||
const visibleCount = $derived.by(() => {
|
||||
if (navWidth === 0) return navItems.length;
|
||||
const available = navWidth;
|
||||
const totalItems = navItems.length;
|
||||
const countWithoutMore = Math.floor(available/ ITEM_WIDTH);
|
||||
|
||||
if (countWithoutMore >= totalItems) return totalItems;
|
||||
|
||||
const availableWithMore = navWidth - MORE_BUTTON_WIDTH;
|
||||
const countWithMore = Math.floor(availableWithMore / ITEM_WIDTH);
|
||||
|
||||
if (totalItems - countWithMore === 1) {
|
||||
if (available >= (countWithMore + 1) * ITEM_WIDTH) {
|
||||
return totalItems;
|
||||
}
|
||||
}
|
||||
|
||||
return countWithMore;
|
||||
});
|
||||
|
||||
const visibleItems = $derived(navItems.slice(0, visibleCount));
|
||||
const hiddenItems = $derived(navItems.slice(visibleCount));
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
@@ -41,37 +72,76 @@
|
||||
</header>
|
||||
|
||||
<main>
|
||||
{@render children()}
|
||||
{@render children()}
|
||||
</main>
|
||||
|
||||
<nav>
|
||||
<nav bind:clientWidth={navWidth}>
|
||||
<!-- Dynamic Nav Elements Here! -->
|
||||
{#each navItems as item}
|
||||
{@const isActive = activePath === item.path}
|
||||
<a
|
||||
{#each visibleItems as item}
|
||||
{@const isActive = activePath === item.path}
|
||||
<a
|
||||
href={item.path}
|
||||
class="nav-item"
|
||||
class:active={isActive}
|
||||
aria-current={isActive ? 'page' : undefined}
|
||||
>
|
||||
<item.icon
|
||||
size={24}
|
||||
stroke={isActive ? 2 : 1.5}
|
||||
class="nav-icon"
|
||||
/>
|
||||
onclick={() => (menuOpen = false)}
|
||||
>
|
||||
<item.icon size={24} stroke={isActive ? 2 : 1.5} class="nav-icon" />
|
||||
|
||||
<span class="label">{item.label}</span>
|
||||
</a>
|
||||
</a>
|
||||
{/each}
|
||||
{#if hiddenItems.length > 0}
|
||||
<div class="more-menu-wrapper">
|
||||
<button
|
||||
class="nav-item more-btn"
|
||||
onclick={() => (menuOpen = !menuOpen)}
|
||||
aria-expanded={menuOpen}
|
||||
>
|
||||
<IconDots size={24} />
|
||||
<span class="label">More</span>
|
||||
</button>
|
||||
</div>
|
||||
{#if menuOpen}
|
||||
<div
|
||||
class="backdrop"
|
||||
onclick={() => (menuOpen = false)}
|
||||
transition:fade={{ duration: 150 }}
|
||||
></div>
|
||||
<div class="menu-popover" transition:slide={{ axis: 'y', duration: 250 }}>
|
||||
{#each hiddenItems as item}
|
||||
{@const isActive = activePath === item.path}
|
||||
<a
|
||||
href={item.path}
|
||||
class="menu-popover-item"
|
||||
onclick={() => (menuOpen = false)}
|
||||
class:active={isActive}
|
||||
aria-current={isActive ? 'page' : undefined}
|
||||
>
|
||||
<item.icon size={20} />
|
||||
<span>{item.label}</span>
|
||||
</a>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
</nav>
|
||||
|
||||
<div class="viewport-guard">
|
||||
<img src={logoPlain} alt="OwlBoard Logo" width=100 height=100>
|
||||
<h1 class="viewport-guard-title">Narrow Gauge Detected</h1>
|
||||
<p>
|
||||
Just as trains need the right track width, our data needs a bit more room to stay on the rails. Please expand your view to at least 300px to view the app.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
header {
|
||||
header {
|
||||
top: 0;
|
||||
height: 80px;
|
||||
box-shadow: var(--shadow-std);
|
||||
}
|
||||
.logo-link {
|
||||
.logo-link {
|
||||
width: auto;
|
||||
display: block;
|
||||
height: 55px;
|
||||
@@ -91,7 +161,8 @@
|
||||
padding-bottom: 2px;
|
||||
color: var(--color-title);
|
||||
}
|
||||
header, nav {
|
||||
header,
|
||||
nav {
|
||||
flex-shrink: 0;
|
||||
width: 100%;
|
||||
background-color: var(--color-accent);
|
||||
@@ -106,13 +177,6 @@
|
||||
gap: 0rem;
|
||||
}
|
||||
|
||||
nav {
|
||||
bottom: 0;
|
||||
height: 60px;
|
||||
box-shadow: var(--shadow-up);
|
||||
}
|
||||
|
||||
|
||||
main {
|
||||
padding-top: 80px;
|
||||
padding-bottom: 60px;
|
||||
@@ -122,22 +186,42 @@
|
||||
background-image: radial-gradient(var(--color-bg-dark), var(--color-bg-light));
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
nav {
|
||||
display: flex;
|
||||
bottom: 0;
|
||||
height: 60px;
|
||||
box-shadow: var(--shadow-up);
|
||||
}
|
||||
|
||||
.nav-item, .more-menu-wrapper {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
width: 70px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-decoration: none;
|
||||
position: relative;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
height: 100%;
|
||||
border: 0;
|
||||
color: var(--color-title);
|
||||
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
background: none;
|
||||
padding: 0;
|
||||
border: none;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.nav-item.active {
|
||||
.nav-item:hover,
|
||||
.menu-popover-item:hover {
|
||||
filter: brightness(1.9);
|
||||
background-color: #ffffff1a;
|
||||
}
|
||||
|
||||
.nav-item.active,
|
||||
.menu-popover-item.active {
|
||||
color: #ffffff;
|
||||
background: rgba(255,255,255,0.05);
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.nav-item.active::before {
|
||||
@@ -151,7 +235,8 @@
|
||||
box-shadow: var(--shadow-std);
|
||||
}
|
||||
|
||||
.nav-item.active .label {
|
||||
.nav-item.active .label,
|
||||
.menu-popover-item.active .label {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
@@ -162,4 +247,79 @@
|
||||
margin-top: 4px;
|
||||
letter-spacing: 0.05rem;
|
||||
}
|
||||
|
||||
.backdrop {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 60px;
|
||||
width: 100vw;
|
||||
height: calc(100dvh - 60px);
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
backdrop-filter: blur(2px);
|
||||
z-index: 999;
|
||||
}
|
||||
|
||||
.menu-popover {
|
||||
position: absolute;
|
||||
bottom: 100%;
|
||||
right: 10px;
|
||||
|
||||
background-color: var(--color-accent);
|
||||
border-radius: 12px 12px 0 0;
|
||||
box-shadow: 0 -4px 20px rgba(0, 0, 0, 0.15);
|
||||
min-width: 140px;
|
||||
z-index: 1000;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.menu-popover-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
position: relative;
|
||||
min-height: 48px;
|
||||
padding: 12px 16px;
|
||||
text-decoration: none;
|
||||
color: var(--color-title);
|
||||
}
|
||||
|
||||
.menu-popover-item.active::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 4px;
|
||||
height: 60%;
|
||||
background-color: var(--color-brand);
|
||||
border-radius: 0 4px 4px 0;
|
||||
box-shadow: var(--shadow-right);
|
||||
}
|
||||
|
||||
.viewport-guard {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media (max-width: 299px) {
|
||||
.viewport-guard {
|
||||
display: block;
|
||||
position: fixed;
|
||||
text-align: center;
|
||||
font-family: 'URW Gothic', sans-serif;
|
||||
margin-top: 1rem;
|
||||
font-size: 1.2rem;
|
||||
color: var(--color-title);
|
||||
}
|
||||
|
||||
.viewport-guard p {
|
||||
width: 90%;
|
||||
margin: auto;
|
||||
padding-top: 30px;
|
||||
}
|
||||
|
||||
header, main, nav {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1 +1,19 @@
|
||||
<script lang="ts">
|
||||
import Button from '$lib/components/ui/Button.svelte';
|
||||
import Textbox from '$lib/components/ui/Textbox.svelte';
|
||||
import BaseCard from '$lib/components/ui/cards/BaseCard.svelte';
|
||||
|
||||
function test() {
|
||||
console.log('Button Clicked');
|
||||
}
|
||||
</script>
|
||||
|
||||
<br /><br /><br />
|
||||
<Button>Default</Button>
|
||||
<Button color={'brand'} onclick={test}>Brand</Button>
|
||||
<Button color={'accent'}>Accent</Button>
|
||||
<Textbox placeholder={"Textbox am I"} uppercase={true} error={""} />
|
||||
|
||||
<BaseCard header={"Hello"} helpText={"This is help text"}>Hello</BaseCard>
|
||||
|
||||
<h2>OwlBoard</h2>
|
||||
|
||||
@@ -1,75 +1,101 @@
|
||||
<script lang="ts">
|
||||
import logo from '$lib/assets/round-logo.svg'
|
||||
import logo from '$lib/assets/round-logo.svg';
|
||||
|
||||
let isSpinning = $state(false);
|
||||
let isSpinning = $state(false);
|
||||
|
||||
function handleLogoTap() {
|
||||
if (isSpinning) return;
|
||||
isSpinning = true;
|
||||
setTimeout(() => isSpinning = false, 800);
|
||||
}
|
||||
function handleLogoTap() {
|
||||
if (isSpinning) return;
|
||||
isSpinning = true;
|
||||
setTimeout(() => (isSpinning = false), 800);
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="logo-container">
|
||||
<img class="logo" src={logo} alt="OwlBoard Logo" onclick={handleLogoTap} class:animate={isSpinning} />
|
||||
<img
|
||||
class="logo"
|
||||
src={logo}
|
||||
alt="OwlBoard Logo"
|
||||
onclick={handleLogoTap}
|
||||
class:animate={isSpinning}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<section class="about">
|
||||
<p class="copy">
|
||||
© 2022-2026 Frederick Boniface
|
||||
</p>
|
||||
<p class="tagline">
|
||||
Created by train crew, for train crew
|
||||
</p>
|
||||
<p class="amble">
|
||||
OwlBoard was created in 2022, evolving from 'Athena' which just provided 'Quick Links' to Tiger departure boards. The aim was to provide fast and easy access to the information we need on a daily basis.
|
||||
</p>
|
||||
<p class="opensource">
|
||||
Some components that combine to form OwlBoard are open-source, see the <a href="https://git.fjla.uk" target="_blank" rel="noopener">Git reposititories</a> for more info.
|
||||
</p>
|
||||
<p class="copy">© 2022-2026 Frederick Boniface</p>
|
||||
<p class="tagline">Created by train crew, for train crew</p>
|
||||
<p class="amble">
|
||||
OwlBoard was created in 2022, evolving from 'Athena' which just provided 'Quick Links' to Tiger
|
||||
departure boards. The aim was to provide fast and easy access to the information we need on a
|
||||
daily basis.
|
||||
</p>
|
||||
<p class="amble">
|
||||
Why OwlBoard? The name was chosen as an evolution of its predecessor, 'Athena'; owls are associated with the Roman Goddess as well as with wisdom. The name also links to Bath, where the app has been built and is run, representing the 'Minerva Owl' sculpture trail in the city, with many of the sculptures still in the area.
|
||||
</p>
|
||||
<p class="opensource">
|
||||
Some components that combine to form OwlBoard are open-source, see the <a
|
||||
href="https://git.fjla.uk"
|
||||
target="_blank"
|
||||
rel="noopener">Git reposititories</a
|
||||
> for more info.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section class="data-sourcing">
|
||||
<p>
|
||||
Data is sourced from multiple providers, including <a href="https://nationalrail.co.uk" target="_blank" rel="noopener noreferrer">National Rail Enquiries</a> and Network Rail along side OwlBoard's own data
|
||||
</p>
|
||||
<p>
|
||||
Data is sourced from multiple providers, including <a
|
||||
href="https://nationalrail.co.uk"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer">National Rail Enquiries</a
|
||||
> and Network Rail along side OwlBoard's own data
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<style>
|
||||
.logo-container {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
.logo-container {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@keyframes owl-spin {
|
||||
0% {transform: rotate(0deg);}
|
||||
15% {transform: rotate(-20deg);}
|
||||
100% {transform: rorate(360deg);}
|
||||
}
|
||||
@keyframes owl-spin {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
15% {
|
||||
transform: rotate(-20deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.logo {
|
||||
padding-top: 25px;
|
||||
margin: auto;
|
||||
height: auto;
|
||||
width: clamp(80px, 20vw, 200px);
|
||||
}
|
||||
.logo {
|
||||
padding-top: 25px;
|
||||
margin: auto;
|
||||
height: auto;
|
||||
width: clamp(80px, 20vw, 200px);
|
||||
}
|
||||
|
||||
.logo.animate {
|
||||
animation: owl-spin 0.8s cubic-bezier(0.34, 1.56, 0.64, 1) forwards;
|
||||
}
|
||||
.logo.animate {
|
||||
animation: owl-spin 0.8s cubic-bezier(0.34, 1.56, 0.64, 1) forwards;
|
||||
}
|
||||
|
||||
section {
|
||||
margin: auto;
|
||||
width: 75%;
|
||||
font-family: 'URW Gothic', sans-serif;
|
||||
text-align: center;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0;
|
||||
}
|
||||
section {
|
||||
margin: auto;
|
||||
width: 90%;
|
||||
max-width: 650px;
|
||||
font-family: 'URW Gothic', sans-serif;
|
||||
text-align: center;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.tagline {
|
||||
font-weight: 600;
|
||||
font-style: italic;
|
||||
}
|
||||
</style>
|
||||
section:last-child {
|
||||
padding-bottom: 15px;
|
||||
}
|
||||
|
||||
.tagline {
|
||||
font-weight: 600;
|
||||
font-style: italic;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
export const load = () => {
|
||||
return {
|
||||
title: "About",
|
||||
};
|
||||
};
|
||||
return {
|
||||
title: 'About'
|
||||
};
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user