Compare commits

...

7 Commits

Author SHA1 Message Date
nub31
8dfbfc564d Move routes to main 2026-03-17 18:29:48 +01:00
nub31
e496147aa4 Layout 2026-03-17 18:20:46 +01:00
nub31
ca036467ca ... 2026-03-17 17:59:43 +01:00
nub31
73c1343b0d ... 2026-03-17 17:53:17 +01:00
nub31
ceb05f4a75 formatting 2026-03-17 17:12:45 +01:00
nub31
794a0549ab Fix navigation updates 2026-03-17 00:13:21 +01:00
nub31
67a83edb57 Fix alignment of footer 2026-03-17 00:11:13 +01:00
41 changed files with 2715 additions and 3063 deletions

21
.gitignore vendored
View File

@@ -1,24 +1,3 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules node_modules
dist dist
dist-ssr
*.local *.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

5
.prettierrc Normal file
View File

@@ -0,0 +1,5 @@
{
"plugins": ["prettier-plugin-svelte"],
"pluginSearchDirs": ["."],
"overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
}

View File

@@ -7,10 +7,7 @@
<meta name="viewport" content="width=device-width" /> <meta name="viewport" content="width=device-width" />
<link rel="preconnect" href="https://fonts.googleapis.com" /> <link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin /> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&family=Roboto+Slab:wght@400;500;600;700;800;900&display=swap" rel="stylesheet" />
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&family=Roboto+Slab:wght@400;500;600;700;800;900&display=swap"
rel="stylesheet"
/>
<title>Minibusservice</title> <title>Minibusservice</title>
</head> </head>
<body> <body>

3829
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,26 +1,37 @@
{ {
"name": "minibusservice.no", "name": "minibusservice.no",
"private": true, "private": true,
"version": "0.0.0", "version": "0.0.0",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
"build": "vite build", "build": "vite build",
"preview": "vite preview", "preview": "vite preview",
"check": "svelte-check --tsconfig ./tsconfig.app.json && tsc -p tsconfig.node.json" "check": "svelte-check --tsconfig ./tsconfig.app.json && tsc -p tsconfig.node.json",
}, "format": "prettier --write ."
"devDependencies": { },
"@sveltejs/vite-plugin-svelte": "^7.0.0", "devDependencies": {
"@tailwindcss/vite": "^0.0.0-insiders.aaaefe8", "@sveltejs/vite-plugin-svelte": "^7.0.0",
"@tsconfig/svelte": "^5.0.8", "@tailwindcss/vite": "^0.0.0-insiders.aaaefe8",
"@types/leaflet": "^1.9.21", "@tsconfig/svelte": "^5.0.8",
"@types/node": "^24.12.0", "@types/leaflet": "^1.9.21",
"leaflet": "^1.9.4", "@types/node": "^24.12.0",
"svelte": "^5.53.7", "leaflet": "^1.9.4",
"svelte-check": "^4.4.5", "prettier": "^3.8.1",
"tailwind-merge": "^3.5.0", "prettier-plugin-svelte": "^3.5.1",
"tailwindcss": "^0.0.0-insiders.aaaefe8", "svelte": "^5.53.7",
"typescript": "~5.9.3", "svelte-check": "^4.4.5",
"vite": "^8.0.0" "tailwind-merge": "^3.5.0",
} "tailwindcss": "^0.0.0-insiders.aaaefe8",
"typescript": "~5.9.3",
"vite": "^8.0.0"
},
"prettier": {
"trailingComma": "none",
"tabWidth": 4,
"printWidth": 180,
"plugins": [
"prettier-plugin-svelte"
]
}
} }

View File

@@ -1,40 +1,20 @@
<script lang="ts"> <script lang="ts">
import Router, { route } from "./lib/components/Router.svelte"; import Router, { route } from "./lib/components/Router.svelte";
import { getTheme } from "./lib/theme.svelte";
import Layout from "./Layout.svelte";
import "./app.css"; import "./app.css";
import HomePage from "./pages/HomePage.svelte"; $effect(() => {
import ToastProvider from "./lib/components/toast/ToastProvider.svelte"; document.body.dataset.theme = getTheme();
import Navigation from "./Navigation.svelte"; });
import Footer from "./Footer.svelte";
import TaxiPage from "./pages/TaxiPage.svelte";
import BusPage from "./pages/BusPage.svelte";
import ContactPage from "./pages/ContactPage.svelte";
import AccessibilityPage from "./pages/AccessibilityPage.svelte";
route("/", HomePage);
route("/taxi", TaxiPage);
route("/bus", BusPage);
route("/contact", ContactPage);
route("/accessibility", AccessibilityPage);
</script> </script>
<div class="flex min-h-screen flex-col bg-primary-200 text-contrast-900"> <Layout>
<Navigation> <Router>
<main class="grow px-4 pb-16 pt-8"> {#snippet notfound()}
<div class="mx-auto max-w-7xl"> <h1>Not found</h1>
<Router> <p>Sorry, the page you are looking for does not exist.</p>
{#snippet notfound()} <p>Return to <a href="/">home</a>.</p>
<h1>Not found</h1> {/snippet}
<p> </Router>
Sorry, the page you are looking for does not exist. </Layout>
</p>
<p>Return to <a href="/">home</a>.</p>
{/snippet}
</Router>
</div>
</main>
<Footer />
</Navigation>
</div>
<ToastProvider />

View File

@@ -1,31 +0,0 @@
<script lang="ts">
import Link from "./lib/components/link/Link.svelte";
import { Routes } from "./lib/global/routes";
</script>
<footer
class="flex flex-col items-center border-t-2 border-contrast-900 bg-contrast-900 px-2 py-8 text-primary-200 dark:border-primary-300 dark:bg-primary-100 dark:text-contrast-900"
>
<div
class="flex flex-row flex-wrap justify-center gap-8 text-center md:text-lg"
>
{#each Routes.footerRoutes as section}
<ul class="flex-1">
<li>
<h6 class="font-semibold">{section.text}</h6>
</li>
{#each section.routes as route}
<li>
<Link target={route.target} href={route.url}>
{route.text}
</Link>
</li>
{/each}
</ul>
{/each}
</div>
<div class="mt-8 text-center font-semibold">Org nr: 816 230 942</div>
<div class="mt-2 text-center font-semibold">
© 2026 minibusservice.no - All Rights Reserved.
</div>
</footer>

133
src/Layout.svelte Normal file
View File

@@ -0,0 +1,133 @@
<script lang="ts">
import Logo from "./lib/components/icons/logo/Logo.svelte";
import sunIcon from "./assets/icons/sun.svg";
import moonIcon from "./assets/icons/moon.svg";
import { slide } from "svelte/transition";
import Toggle from "./lib/components/Toggle.svelte";
import { Routes } from "./lib/routes";
import { isCurrentPath } from "./lib/components/Router.svelte";
import { getTheme, toggleTheme } from "./lib/theme.svelte";
import ToastProvider from "./lib/components/toast/ToastProvider.svelte";
import Link from "./lib/components/link/Link.svelte";
let sidebarOpen = $state(false);
let { children } = $props();
</script>
<div class="flex min-h-screen flex-col bg-primary-200 text-contrast-900">
<header class="sticky top-0 z-20 flex h-16 justify-center border-b-2 border-contrast-100 bg-primary-100 shadow-lg">
<div class="flex w-full max-w-7xl flex-row items-center justify-between p-4">
<a class="h-full grow" href="/">
<Logo />
</a>
<ul class="hidden flex-row justify-end gap-10 font-medium uppercase md:text-lg lg:flex">
{#each Routes.topbarRoutes as route}
<li>
<a class:border-b-2={isCurrentPath(route.url)} class="border-b-0 border-accent transition-colors hover:text-accent" href={route.url} target={route.target}>
{route.text}
</a>
</li>
{/each}
</ul>
<div class="ml-12 flex aspect-square h-full items-center justify-center p-1">
<button
class="hamburger-icon relative inline-block h-full w-full cursor-pointer text-contrast-100"
aria-label="open sidebar"
class:open={sidebarOpen}
onclick={() => (sidebarOpen = !sidebarOpen)}
>
<div class="line absolute top-0 h-0.5 w-full origin-top-left rounded-full bg-contrast-900 transition-all"></div>
<div class="line middle absolute top-1/2 h-0.5 w-full -translate-y-1/2 rounded-full bg-contrast-900 transition-all delay-100"></div>
<div class="line absolute bottom-0 h-0.5 w-full origin-bottom-left rounded-full bg-contrast-900 transition-all"></div>
</button>
</div>
</div>
{#if sidebarOpen}
<aside>
<nav
transition:slide={{ axis: "x" }}
class="fixed bottom-14 right-0 top-16 z-10 flex max-w-full flex-col overflow-auto border-l-2 border-contrast-100 bg-primary-100 px-4 py-8 shadow-lg sm:bottom-0"
>
<ul class="flex grow flex-col gap-2 pr-20 text-lg font-medium lg:text-xl">
{#each Routes.sidebarRoutes as route}
<li>
<a
class:border-b-2={isCurrentPath(route.url)}
class="border-b-0 border-accent transition-colors hover:text-accent"
href={route.url}
target={route.target}
>
{route.text}
</a>
</li>
{/each}
</ul>
<div class="flex w-fit flex-col items-center gap-3">
<Toggle invertColor={true} checked={getTheme() == "dark"} leftIcon={sunIcon} rightIcon={moonIcon} on:change={toggleTheme} />
</div>
</nav>
</aside>
{/if}
</header>
<main class="grow px-4 pb-16 pt-8">
<div class="mx-auto max-w-7xl">
{@render children()}
</div>
</main>
<footer
class="flex flex-col items-center border-t-2 border-contrast-900 bg-contrast-900 px-2 py-8 text-primary-200 dark:border-primary-300 dark:bg-primary-100 dark:text-contrast-900"
>
<div class="grid grid-cols-1 md:grid-cols-3 gap-8 text-center md:text-lg">
{#each Routes.footerRoutes as section}
<ul>
<li>
<h6 class="font-semibold">{section.text}</h6>
</li>
{#each section.routes as route}
<li>
<Link target={route.target} href={route.url}>
{route.text}
</Link>
</li>
{/each}
</ul>
{/each}
</div>
<div class="mt-8 text-center font-semibold">Org nr: 816 230 942</div>
<div class="mt-2 text-center font-semibold">© 2026 minibusservice.no - All Rights Reserved.</div>
</footer>
<!-- Mobile navigation -->
<nav class="sticky bottom-0 z-10 flex h-14 flex-row items-center justify-around border-t-2 border-contrast-100 bg-primary-100 px-2 text-xs shadow-t-lg sm:hidden">
{#each Routes.topbarRoutes as route}
<a
href={route.url}
target={route.target}
class={`flex flex-col items-center decoration-accent transition-opacity ${isCurrentPath(route.url) ? "underline opacity-100" : "opacity-50"}`}
>
<img src={route.icon} alt={route.text} class="h-6 dark:invert" />
<span> {route.text} </span>
</a>
{/each}
</nav>
</div>
<ToastProvider />
<style>
.hamburger-icon.open .line:first-child {
transform: rotate(45deg) translateX(15%);
}
.hamburger-icon.open .line:last-child {
transform: rotate(-45deg) translateX(15%);
}
.hamburger-icon.open .line.middle {
width: 0;
transition: transform 0.1s;
}
</style>

View File

@@ -1,138 +0,0 @@
<script lang="ts">
import Logo from "./lib/components/icons/logo/Logo.svelte";
import sunIcon from "./assets/icons/sun.svg";
import moonIcon from "./assets/icons/moon.svg";
import { slide } from "svelte/transition";
import Toggle from "./lib/components/Toggle.svelte";
import { darkTheme } from "./lib/stores/theme";
import { Routes } from "./lib/global/routes";
import { isCurrentPath } from "./lib/components/Router.svelte";
let sidebarOpen = false;
function toggleMenuOpen() {
sidebarOpen = !sidebarOpen;
}
</script>
<header
class="sticky top-0 z-20 flex h-16 justify-center border-b-2 border-contrast-100 bg-primary-100 shadow-lg"
>
<div
class="flex w-full max-w-7xl flex-row items-center justify-between p-4"
>
<a class="h-full grow" href="/">
<Logo />
</a>
<ul
class="hidden flex-row justify-end gap-10 font-medium uppercase md:text-lg lg:flex"
>
{#each Routes.topbarRoutes as route}
<li>
<a
class:border-b-2={isCurrentPath(route.url)}
class="border-b-0 border-accent transition-colors hover:text-accent"
href={route.url}
target={route.target}
>
{route.text}
</a>
</li>
{/each}
</ul>
<div
class="ml-12 flex aspect-square h-full items-center justify-center p-1"
>
<button
class="hamburger-icon relative inline-block h-full w-full cursor-pointer text-contrast-100"
aria-label="open sidebar"
class:open={sidebarOpen}
on:click={toggleMenuOpen}
>
<div
class="line absolute top-0 h-0.5 w-full origin-top-left rounded-full bg-contrast-900 transition-all"
></div>
<div
class="line middle absolute top-1/2 h-0.5 w-full -translate-y-1/2 rounded-full bg-contrast-900 transition-all delay-100"
></div>
<div
class="line absolute bottom-0 h-0.5 w-full origin-bottom-left rounded-full bg-contrast-900 transition-all"
></div>
</button>
</div>
</div>
{#if sidebarOpen}
<aside>
<nav
transition:slide={{ axis: "x" }}
class="fixed bottom-14 right-0 top-16 z-10 flex max-w-full flex-col overflow-auto border-l-2 border-contrast-100 bg-primary-100 px-4 py-8 shadow-lg sm:bottom-0"
>
<ul
class="flex grow flex-col gap-2 pr-20 text-lg font-medium lg:text-xl"
>
{#each Routes.sidebarRoutes as route}
<li>
<a
class:border-b-2={`/${window.location.pathname.split("/")[1]}` ==
route.url}
class="border-b-0 border-accent transition-colors hover:text-accent"
href={route.url}
target={route.target}
>
{route.text}
</a>
</li>
{/each}
</ul>
<div class="flex w-fit flex-col items-center gap-3">
<Toggle
invertColor={true}
checked={$darkTheme}
leftIcon={sunIcon}
rightIcon={moonIcon}
on:change={() => {
$darkTheme = $darkTheme = !$darkTheme;
}}
/>
</div>
</nav>
</aside>
{/if}
</header>
<slot />
<!-- Mobile navigation -->
<nav
class="sticky bottom-0 z-10 flex h-14 flex-row items-center justify-around border-t-2 border-contrast-100 bg-primary-100 px-2 text-xs shadow-t-lg sm:hidden"
>
{#each Routes.topbarRoutes as route}
<a
href={route.url}
target={route.target}
class={`flex flex-col items-center decoration-accent transition-opacity ${
`/${window.location.pathname.split("/")[1]}` == route.url
? "underline opacity-100"
: "opacity-50"
}`}
>
<img src={route.icon} alt={route.text} class="h-6 dark:invert" />
<span> {route.text} </span>
</a>
{/each}
</nav>
<style>
.hamburger-icon.open .line:first-child {
transform: rotate(45deg) translateX(15%);
}
.hamburger-icon.open .line:last-child {
transform: rotate(-45deg) translateX(15%);
}
.hamburger-icon.open .line.middle {
width: 0;
transition: transform 0.1s;
}
</style>

View File

@@ -2,72 +2,36 @@
@custom-variant dark (&:where([data-theme='dark'], [data-theme='dark'] *)); @custom-variant dark (&:where([data-theme='dark'], [data-theme='dark'] *));
:root { body {
--mb-color-primary-100: rgb(250 250 250); color-scheme: light;
--mb-color-primary-200: rgb(240 240 240);
--mb-color-primary-300: rgb(230 230 230);
--mb-color-primary-400: rgb(220 220 220);
--mb-color-primary-500: rgb(210 210 210);
--mb-color-primary-600: rgb(200 200 200);
--mb-color-primary-700: rgb(190 190 190);
--mb-color-primary-800: rgb(180 180 180);
--mb-color-primary-900: rgb(170 170 170);
--mb-color-contrast-100: rgb(220 220 220);
--mb-color-contrast-200: rgb(200 200 200);
--mb-color-contrast-300: rgb(180 180 180);
--mb-color-contrast-400: rgb(160 160 160);
--mb-color-contrast-500: rgb(140 140 140);
--mb-color-contrast-600: rgb(110 110 110);
--mb-color-contrast-700: rgb(80 80 80);
--mb-color-contrast-800: rgb(60 60 60);
--mb-color-contrast-900: rgb(30 30 30);
} }
[data-theme="dark"] { body[data-theme="dark"] {
--mb-color-primary-100: rgb(28 28 28); color-scheme: dark;
--mb-color-primary-200: rgb(32 32 32);
--mb-color-primary-300: rgb(38 38 38);
--mb-color-primary-400: rgb(44 44 44);
--mb-color-primary-500: rgb(50 50 50);
--mb-color-primary-600: rgb(56 56 56);
--mb-color-primary-700: rgb(62 62 62);
--mb-color-primary-800: rgb(68 68 68);
--mb-color-primary-900: rgb(74 74 74);
--mb-color-contrast-100: rgb(25 25 25);
--mb-color-contrast-200: rgb(50 50 50);
--mb-color-contrast-300: rgb(80 80 80);
--mb-color-contrast-400: rgb(110 110 110);
--mb-color-contrast-500: rgb(140 140 140);
--mb-color-contrast-600: rgb(160 160 160);
--mb-color-contrast-700: rgb(180 180 180);
--mb-color-contrast-800: rgb(200 200 200);
--mb-color-contrast-900: rgb(220 220 220);
} }
@theme inline { @theme inline {
--color-accent: rgb(255 29 37); --color-accent: rgb(255 29 37);
--color-primary-100: var(--mb-color-primary-100); --color-primary-100: light-dark(rgb(250 250 250), rgb(28 28 28));
--color-primary-200: var(--mb-color-primary-200); --color-primary-200: light-dark(rgb(240 240 240), rgb(32 32 32));
--color-primary-300: var(--mb-color-primary-300); --color-primary-300: light-dark(rgb(230 230 230), rgb(38 38 38));
--color-primary-400: var(--mb-color-primary-400); --color-primary-400: light-dark(rgb(220 220 220), rgb(44 44 44));
--color-primary-500: var(--mb-color-primary-500); --color-primary-500: light-dark(rgb(210 210 210), rgb(50 50 50));
--color-primary-600: var(--mb-color-primary-600); --color-primary-600: light-dark(rgb(200 200 200), rgb(56 56 56));
--color-primary-700: var(--mb-color-primary-700); --color-primary-700: light-dark(rgb(190 190 190), rgb(62 62 62));
--color-primary-800: var(--mb-color-primary-800); --color-primary-800: light-dark(rgb(180 180 180), rgb(68 68 68));
--color-primary-900: var(--mb-color-primary-900); --color-primary-900: light-dark(rgb(170 170 170), rgb(74 74 74));
--color-contrast-100: var(--mb-color-contrast-100); --color-contrast-100: light-dark(rgb(220 220 220), rgb(25 25 25));
--color-contrast-200: var(--mb-color-contrast-200); --color-contrast-200: light-dark(rgb(200 200 200), rgb(50 50 50));
--color-contrast-300: var(--mb-color-contrast-300); --color-contrast-300: light-dark(rgb(180 180 180), rgb(80 80 80));
--color-contrast-400: var(--mb-color-contrast-400); --color-contrast-400: light-dark(rgb(160 160 160), rgb(110 110 110));
--color-contrast-500: var(--mb-color-contrast-500); --color-contrast-500: light-dark(rgb(140 140 140), rgb(140 140 140));
--color-contrast-600: var(--mb-color-contrast-600); --color-contrast-600: light-dark(rgb(110 110 110), rgb(160 160 160));
--color-contrast-700: var(--mb-color-contrast-700); --color-contrast-700: light-dark(rgb(80 80 80), rgb(180 180 180));
--color-contrast-800: var(--mb-color-contrast-800); --color-contrast-800: light-dark(rgb(60 60 60), rgb(200 200 200));
--color-contrast-900: var(--mb-color-contrast-900); --color-contrast-900: light-dark(rgb(30 30 30), rgb(220 220 220));
--shadow-t-lg: 0px -4px 6px -1px rgba(0, 0, 0, 0.1); --shadow-t-lg: 0px -4px 6px -1px rgba(0, 0, 0, 0.1);
} }

View File

@@ -8,26 +8,16 @@
<p class="pt-2 font-medium opacity-70"> <p class="pt-2 font-medium opacity-70">
Spørsmål?<br />Kontakt oss via tlf eller e-post Spørsmål?<br />Kontakt oss via tlf eller e-post
</p> </p>
<Link <Link class="w-fit font-bold text-accent hover:underline" href="tel:45256161">Tlf: +47 45 25 61 61</Link>
class="w-fit font-bold text-accent hover:underline" <Link class="w-fit font-bold text-accent hover:underline" href="mailto:minibusstur@hotmail.com">E-post: minibusstur@hotmail.com</Link>
href="tel:45256161"
>
Tlf: +47 45 25 61 61
</Link>
<Link
class="w-fit font-bold text-accent hover:underline"
href="mailto:minibusstur@hotmail.com"
>
E-post: minibusstur@hotmail.com
</Link>
<MapPoint <MapPoint
class="card mt-5 min-h-140 grow overflow-hidden rounded-md border-2" class="card mt-5 min-h-140 grow overflow-hidden rounded-md border-2"
coordinates={[ coordinates={[
{ {
latitude: 62.48303957042255, latitude: 62.48303957042255,
longitude: 6.8108274964451745, longitude: 6.8108274964451745
}, }
]} ]}
/> />
</div> </div>

View File

@@ -23,23 +23,19 @@
iconSize: [32, 32], iconSize: [32, 32],
iconAnchor: [16, 32], iconAnchor: [16, 32],
popupAnchor: [-3, -76], popupAnchor: [-3, -76]
}); });
map = L.map(mapElement); map = L.map(mapElement);
if (coordinates.length >= 1) { if (coordinates.length >= 1) {
map.setView( map.setView([coordinates[0].latitude, coordinates[0].longitude], 15);
[coordinates[0].latitude, coordinates[0].longitude],
15,
);
} }
L.tileLayer("https://tile.openstreetmap.org/{z}/{x}/{y}.png", { L.tileLayer("https://tile.openstreetmap.org/{z}/{x}/{y}.png", {
maxZoom: 18, maxZoom: 18,
attribution: attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
'&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors', id: "base"
id: "base",
}).addTo(map); }).addTo(map);
updateMarkers(); updateMarkers();
@@ -51,12 +47,12 @@
function addMarker(point: MarkerPoint) { function addMarker(point: MarkerPoint) {
let marker = L.marker([point.latitude, point.longitude], { let marker = L.marker([point.latitude, point.longitude], {
icon: icon, icon: icon
}); });
if (point.tooltip) if (point.tooltip)
marker.bindTooltip(point.tooltip, { marker.bindTooltip(point.tooltip, {
direction: "bottom", direction: "bottom"
}); });
markers.push(marker); markers.push(marker);
@@ -71,7 +67,7 @@
coordinates.forEach(addMarker); coordinates.forEach(addMarker);
map?.flyToBounds(L.featureGroup(markers).getBounds(), { map?.flyToBounds(L.featureGroup(markers).getBounds(), {
duration: 1, duration: 1
}); });
} }
} }
@@ -79,7 +75,4 @@
$: coordinates && updateMarkers(); $: coordinates && updateMarkers();
</script> </script>
<div <div bind:this={mapElement} class={twMerge("z-0 h-full w-full", $$restProps["class"])}></div>
bind:this={mapElement}
class={twMerge("z-0 h-full w-full", $$restProps["class"])}
></div>

View File

@@ -2,6 +2,10 @@
import { SvelteMap } from "svelte/reactivity"; import { SvelteMap } from "svelte/reactivity";
import { onMount, type Component, type Snippet } from "svelte"; import { onMount, type Component, type Snippet } from "svelte";
export class RouteError extends Error {}
export class MissingRouteError extends RouteError {}
export class RouteFormatError extends RouteError {}
type RouteInfo = { type RouteInfo = {
path: string; path: string;
pattern: RegExp; pattern: RegExp;
@@ -63,24 +67,19 @@
export function route(path: string, component: Component<{}>): void { export function route(path: string, component: Component<{}>): void {
if (routes.has(path)) { if (routes.has(path)) {
console.warn(`Route already registered for path: '${path}'.`); console.warn(`Route already registered for path: '${path}'.`);
return;
} }
const params = Array.from(path.matchAll(/\[([^\]]+)\]/g)).map( const params = Array.from(path.matchAll(/\[([^\]]+)\]/g)).map((x) => x[1]);
(x) => x[1],
);
routes.set(path, { routes.set(path, {
pattern: new RegExp(createRoutePattern(path)), pattern: new RegExp(createRoutePattern(path)),
path: path, path: path,
paramNames: params, paramNames: params,
component: component, component: component
}); });
} }
export class RouteError extends Error {}
export class MissingRouteError extends RouteError {}
export class RouteFormatError extends RouteError {}
export function isCurrentPath(path: string): boolean { export function isCurrentPath(path: string): boolean {
if (!currentRoute) { if (!currentRoute) {
return false; return false;
@@ -100,9 +99,7 @@
export function paramString(name: string): string { export function paramString(name: string): string {
const value = currentParams[name]; const value = currentParams[name];
if (!value) { if (!value) {
throw new MissingRouteError( throw new MissingRouteError(`Route does not have a parameter matching the name '${name}''`);
`Route does not have a parameter matching the name '${name}''`,
);
} }
return value; return value;
@@ -115,9 +112,7 @@
const value = paramString(name); const value = paramString(name);
const parsed = parseInt(value, 10); const parsed = parseInt(value, 10);
if (Number.isNaN(parsed)) { if (Number.isNaN(parsed)) {
throw new RouteFormatError( throw new RouteFormatError(`parameter ${value} could not be parsed as an integer`);
`parameter ${value} could not be parsed as an integer`,
);
} }
return parsed; return parsed;
@@ -129,10 +124,7 @@
*/ */
export function queryString(name: string): string | null; export function queryString(name: string): string | null;
export function queryString(name: string, defaultValue: string): string; export function queryString(name: string, defaultValue: string): string;
export function queryString( export function queryString(name: string, defaultValue?: string): string | null {
name: string,
defaultValue?: string,
): string | null {
const value = currentUrl.searchParams.get(name); const value = currentUrl.searchParams.get(name);
if (value) { if (value) {
return value; return value;
@@ -147,10 +139,7 @@
*/ */
export function queryNumber(name: string): number | null; export function queryNumber(name: string): number | null;
export function queryNumber(name: string, defaultValue: number): number; export function queryNumber(name: string, defaultValue: number): number;
export function queryNumber( export function queryNumber(name: string, defaultValue?: number): number | null {
name: string,
defaultValue?: number,
): number | null {
const value = currentUrl.searchParams.get(name); const value = currentUrl.searchParams.get(name);
if (value) { if (value) {
@@ -179,7 +168,7 @@
event.intercept({ event.intercept({
handler: async () => { handler: async () => {
currentUrl = new URL(event.destination.url); currentUrl = new URL(event.destination.url);
}, }
}); });
} }
@@ -188,59 +177,7 @@
return () => { return () => {
// @ts-ignore // @ts-ignore
window.navigation.removeEventListener( window.navigation.removeEventListener("navigate", handleNavigate);
"navigate",
handleNavigate,
);
};
} else {
function refresh() {
currentUrl = new URL(window.location.href);
}
function makeProxy(target: any) {
return new Proxy(target, {
apply: (target, thisArg, argArray) => {
const result = target.apply(thisArg, argArray);
refresh();
return result;
},
});
}
window.history.pushState = makeProxy(window.history.pushState);
window.history.replaceState = makeProxy(
window.history.replaceState,
);
window.history.go = makeProxy(window.history.go);
window.history.forward = makeProxy(window.history.forward);
window.history.back = makeProxy(window.history.back);
function handleClick(e: MouseEvent) {
if (e.ctrlKey || e.shiftKey || e.altKey || e.metaKey) {
return;
}
const anchor = (e.target as Element)?.closest("a");
if (!(anchor instanceof HTMLAnchorElement)) return;
if (anchor.target === "_blank") return;
const targetUrl = new URL(anchor.href, document.baseURI);
const documentUrl = new URL(document.baseURI);
if (targetUrl.origin === documentUrl.origin) {
e.preventDefault();
history.pushState({}, "", targetUrl);
currentUrl = targetUrl;
}
}
window.addEventListener("popstate", refresh);
document.addEventListener("click", handleClick);
return () => {
window.removeEventListener("popstate", refresh);
document.removeEventListener("click", handleClick);
}; };
} }
}); });

View File

@@ -1,34 +1,20 @@
<!-- By Sam Herbert (@sherb), for everyone. More @ http://goo.gl/7AJzbL --> <!-- By Sam Herbert (@sherb), for everyone. More @ http://goo.gl/7AJzbL -->
<svg {...$$restProps} width="38" height="38" viewBox="0 0 38 38" xmlns="http://www.w3.org/2000/svg"> <svg {...$$restProps} width="38" height="38" viewBox="0 0 38 38" xmlns="http://www.w3.org/2000/svg">
<defs> <defs>
<linearGradient x1="8.042%" y1="0%" x2="65.682%" y2="23.865%" id="a"> <linearGradient x1="8.042%" y1="0%" x2="65.682%" y2="23.865%" id="a">
<stop stop-color="currentColor" stop-opacity="0" offset="0%" /> <stop stop-color="currentColor" stop-opacity="0" offset="0%" />
<stop stop-color="currentColor" stop-opacity=".631" offset="63.146%" /> <stop stop-color="currentColor" stop-opacity=".631" offset="63.146%" />
<stop stop-color="currentColor" offset="100%" /> <stop stop-color="currentColor" offset="100%" />
</linearGradient> </linearGradient>
</defs> </defs>
<g fill="none" fill-rule="evenodd"> <g fill="none" fill-rule="evenodd">
<g transform="translate(1 1)"> <g transform="translate(1 1)">
<path d="M36 18c0-9.94-8.06-18-18-18" id="Oval-2" stroke="url(#a)" stroke-width="2"> <path d="M36 18c0-9.94-8.06-18-18-18" id="Oval-2" stroke="url(#a)" stroke-width="2">
<animateTransform <animateTransform attributeName="transform" type="rotate" from="0 18 18" to="360 18 18" dur="0.9s" repeatCount="indefinite" />
attributeName="transform" </path>
type="rotate" <circle fill="currentColor" cx="36" cy="18" r="1">
from="0 18 18" <animateTransform attributeName="transform" type="rotate" from="0 18 18" to="360 18 18" dur="0.9s" repeatCount="indefinite" />
to="360 18 18" </circle>
dur="0.9s" </g>
repeatCount="indefinite" </g>
/> </svg>
</path>
<circle fill="currentColor" cx="36" cy="18" r="1">
<animateTransform
attributeName="transform"
type="rotate"
from="0 18 18"
to="360 18 18"
dur="0.9s"
repeatCount="indefinite"
/>
</circle>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -9,29 +9,15 @@
<div class="flex items-center gap-1"> <div class="flex items-center gap-1">
{#if leftIcon} {#if leftIcon}
<img <img src={leftIcon} class={`h-6 ${invertColor && "dark:invert"}`} alt="" />
src={leftIcon}
class={`h-6 ${invertColor && "dark:invert"}`}
alt=""
/>
{/if} {/if}
<label class="relative inline-flex cursor-pointer items-center"> <label class="relative inline-flex cursor-pointer items-center">
<input <input on:change {checked} type="checkbox" value="" class="peer sr-only" />
on:change
{checked}
type="checkbox"
value=""
class="peer sr-only"
/>
<div <div
class="h-6 w-11 rounded-full border-2 border-contrast-100 outline-2 transition-colors after:absolute after:left-[2px] after:top-[2px] after:h-5 after:w-5 after:rounded-full after:border-2 after:border-contrast-100 after:bg-contrast-900 after:transition-all after:content-[''] peer-checked:bg-accent peer-checked:after:translate-x-full dark:peer-focus:ring-accent" class="h-6 w-11 rounded-full border-2 border-contrast-100 outline-2 transition-colors after:absolute after:left-0.5 after:top-0.5 after:h-5 after:w-5 after:rounded-full after:border-2 after:border-contrast-100 after:bg-contrast-900 after:transition-all after:content-[''] peer-checked:bg-accent peer-checked:after:translate-x-full dark:peer-focus:ring-accent"
></div> ></div>
</label> </label>
{#if rightIcon} {#if rightIcon}
<img <img src={rightIcon} class={`h-6 ${invertColor && "dark:invert"}`} alt="" />
src={rightIcon}
class={`h-6 ${invertColor && "dark:invert"}`}
alt=""
/>
{/if} {/if}
</div> </div>

View File

@@ -1,30 +1,30 @@
<script lang="ts"> <script lang="ts">
import Spinner from '../Spinner.svelte'; import Spinner from "../Spinner.svelte";
import { twMerge } from 'tailwind-merge'; import { twMerge } from "tailwind-merge";
/** @description Disallow clicking and reduce opacity **/ /** @description Disallow clicking and reduce opacity **/
export let disabled = false; export let disabled = false;
/** @description Show a loading spinner **/ /** @description Show a loading spinner **/
export let loading = false; export let loading = false;
</script> </script>
<button <button
on:click on:click
{...$$restProps} {...$$restProps}
disabled={disabled || loading} disabled={disabled || loading}
class:opacity-50={disabled || loading} class:opacity-50={disabled || loading}
class:cursor-not-allowed={disabled || loading} class:cursor-not-allowed={disabled || loading}
class={twMerge( class={twMerge(
'relative rounded-md border-2 border-contrast-200 bg-primary-300 px-4 py-2 transition-colors hover:border-contrast-900 dark:border-contrast-100 dark:hover:border-accent', "relative rounded-md border-2 border-contrast-200 bg-primary-300 px-4 py-2 transition-colors hover:border-contrast-900 dark:border-contrast-100 dark:hover:border-accent",
$$restProps['class'] $$restProps["class"]
)} )}
> >
<span class:opacity-0={loading}> <span class:opacity-0={loading}>
<slot /> <slot />
</span> </span>
{#if loading} {#if loading}
<span class="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2"> <span class="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2">
<Spinner class="h-6" /> <Spinner class="h-6" />
</span> </span>
{/if} {/if}
</button> </button>

View File

@@ -1,61 +1,57 @@
<script lang="ts" generics="T"> <script lang="ts" generics="T">
import { twMerge } from 'tailwind-merge'; import { twMerge } from "tailwind-merge";
export let items: T[]; export let items: T[];
/** @description Tailwind classes for styling the button colors */ /** @description Tailwind classes for styling the button colors */
export let buttonClass = ''; export let buttonClass = "";
let index = 0; let index = 0;
const next = () => { const next = () => {
index = (index + 1) % items.length; index = (index + 1) % items.length;
}; };
const prev = () => { const prev = () => {
index = (index - 1 + items.length) % items.length; index = (index - 1 + items.length) % items.length;
}; };
</script> </script>
<div <div style={`--index: ${index}`} {...$$restProps} class={twMerge("inline-flex w-full gap-5 overflow-auto sm:overflow-hidden", $$restProps["class"])}>
style={`--index: ${index}`} {#each items as item, index}
{...$$restProps} <div class="carousel transition-transform">
class={twMerge('inline-flex w-full gap-5 overflow-auto sm:overflow-hidden', $$restProps['class'])} <slot {item} {index} />
> </div>
{#each items as item, index} {/each}
<div class="carousel transition-transform"> </div>
<slot {item} {index} />
</div> <div class="hidden sm:block">
{/each} {#if items.length > 1}
</div> <div class="mt-2 flex justify-between md:text-lg">
<button
<div class="hidden sm:block"> class={twMerge(
{#if items.length > 1} "rounded-md border-2 border-contrast-100 bg-primary-300 px-4 py-2 transition-colors hover:border-contrast-200 dark:hover:border-accent",
<div class="mt-2 flex justify-between md:text-lg"> buttonClass
<button )}
class={twMerge( on:click={prev}
'rounded-md border-2 border-contrast-100 bg-primary-300 px-4 py-2 transition-colors hover:border-contrast-200 dark:hover:border-accent', >
buttonClass Forrige
)} </button>
on:click={prev} <button
> class={twMerge(
Forrige "rounded-md border-2 border-contrast-100 bg-primary-300 px-4 py-2 transition-colors hover:border-contrast-200 dark:hover:border-accent",
</button> buttonClass
<button )}
class={twMerge( on:click={next}
'rounded-md border-2 border-contrast-100 bg-primary-300 px-4 py-2 transition-colors hover:border-contrast-200 dark:hover:border-accent', >
buttonClass Neste
)} </button>
on:click={next} </div>
> {/if}
Neste </div>
</button>
</div> <style>
{/if} .carousel {
</div> transform: translateX(calc((-100% - 20px) * var(--index)));
}
<style> </style>
.carousel {
transform: translateX(calc((-100% - 20px) * var(--index)));
}
</style>

View File

@@ -1,79 +1,56 @@
<script lang="ts"> <script lang="ts">
import { twMerge } from 'tailwind-merge'; import { twMerge } from "tailwind-merge";
export let layout: 'narrow' | 'wide' = 'wide'; export let layout: "narrow" | "wide" = "wide";
</script> </script>
{#if layout === 'wide'} {#if layout === "wide"}
<svg <svg xmlns="http://www.w3.org/2000/svg" class={twMerge("h-full", $$restProps["class"])} {...$$restProps} viewBox="0 0 1840.81 395.57">
xmlns="http://www.w3.org/2000/svg" <path fill="currentColor" d="M102,153.89,50.09,81.21V191.75H.34V6.59H50.09l64.66,94.79L179.42,6.59h49.74V191.75H179.42V81.21l-51.95,72.68Z" />
class={twMerge('h-full', $$restProps['class'])} <path fill="currentColor" d="M336.11,6.59V191.75H286.36V6.59Z" />
{...$$restProps} <path fill="currentColor" d="M535.35,125.42V6.59H585.1V191.75H535.35l-97-118.83V191.75H388.61V6.59h49.74Z" />
viewBox="0 0 1840.81 395.57" <path fill="currentColor" d="M692,6.59V191.75H642.3V6.59Z" />
> <path
<path fill="currentColor"
fill="currentColor" d="M845.69,1.07h7.74c61.9,0,79,24.87,79,55.27v5.52c0,16.31-8,30.4-32.88,37,29,6.64,38.41,21.28,38.41,37.59V142c0,30.39-18.24,55.27-84.56,55.27h-7.74a927.22,927.22,0,0,1-101.14-5.53V6.59A927.25,927.25,0,0,1,845.69,1.07Zm-51.4,46.7V78.44h49.19c44.77,0,44.77-7.18,44.77-16.58,0-9.12-1.38-16.58-41.45-16.58C833.25,45.28,812.53,46.11,794.29,47.77Zm0,74.89v27.91c18.24,1.66,38.69,2.49,53.33,2.49,44.22,0,46.16-7.46,46.16-16.58,0-7.74,0-13.82-50.3-13.82Z"
d="M102,153.89,50.09,81.21V191.75H.34V6.59H50.09l64.66,94.79L179.42,6.59h49.74V191.75H179.42V81.21l-51.95,72.68Z" />
/> <path
<path fill="currentColor" d="M336.11,6.59V191.75H286.36V6.59Z" /> fill="currentColor"
<path d="M1185.32,6.59V114.65c0,45.32-20.73,82.63-94.51,82.63h-10c-73.79,0-94.51-37.31-94.51-82.63V6.59h49.74V107.46c0,24,0,45.6,49.74,45.6s49.75-21.56,49.75-45.6V6.59Z"
fill="currentColor" />
d="M535.35,125.42V6.59H585.1V191.75H535.35l-97-118.83V191.75H388.61V6.59h49.74Z" <path
/> fill="currentColor"
<path fill="currentColor" d="M692,6.59V191.75H642.3V6.59Z" /> d="M1369.37,137c0-13-11.61-14.92-43.67-15.75-28.18-.83-89.53-2.76-89.53-56.65V61c0-31.78,20.45-60,90.64-60h10a290.93,290.93,0,0,1,72.12,9.12V55.78c-26.8-6.35-53.05-10.22-80.14-10.22-40.07,0-42.83,9.12-42.83,15.47,0,13.27,12.16,15.2,43.39,16,26.25.56,89.81,2.49,89.81,56.93v3.59c0,31.51-20.45,59.7-90.92,59.7h-10a400.9,400.9,0,0,1-82.9-8.85V142c29.84,6.63,63.28,11.05,91.47,11.05C1366.33,153.06,1369.37,143.94,1369.37,137Z"
<path />
fill="currentColor" <path
d="M845.69,1.07h7.74c61.9,0,79,24.87,79,55.27v5.52c0,16.31-8,30.4-32.88,37,29,6.64,38.41,21.28,38.41,37.59V142c0,30.39-18.24,55.27-84.56,55.27h-7.74a927.22,927.22,0,0,1-101.14-5.53V6.59A927.25,927.25,0,0,1,845.69,1.07Zm-51.4,46.7V78.44h49.19c44.77,0,44.77-7.18,44.77-16.58,0-9.12-1.38-16.58-41.45-16.58C833.25,45.28,812.53,46.11,794.29,47.77Zm0,74.89v27.91c18.24,1.66,38.69,2.49,53.33,2.49,44.22,0,46.16-7.46,46.16-16.58,0-7.74,0-13.82-50.3-13.82Z" fill="#ff1d25"
/> d="M846.11,350.74c0-9.9-8.84-11.37-33.26-12-21.48-.63-68.22-2.11-68.22-43.16v-2.74c0-24.21,15.58-45.69,69.06-45.69h7.58a222,222,0,0,1,55,6.95v34.74c-20.42-4.84-40.43-7.79-61.06-7.79-30.53,0-32.63,6.95-32.63,11.79,0,10.11,9.26,11.58,33.05,12.21,20,.42,68.43,1.9,68.43,43.37v2.74c0,24-15.58,45.48-69.27,45.48h-7.58A305.66,305.66,0,0,1,744,389.9V354.53c22.74,5.05,48.21,8.42,69.69,8.42C843.79,363,846.11,356,846.11,350.74Z"
<path />
fill="currentColor" <path fill="#ff1d25" d="M1056.87,285.05H959.81V306.1h76v33.69h-76v18.95h97.06v33.69h-135V251.36h135Z" />
d="M1185.32,6.59V114.65c0,45.32-20.73,82.63-94.51,82.63h-10c-73.79,0-94.51-37.31-94.51-82.63V6.59h49.74V107.46c0,24,0,45.6,49.74,45.6s49.75-21.56,49.75-45.6V6.59Z" <path
/> fill="#ff1d25"
<path d="M1201.09,392.43l-28.21-44.22c-15,0-29.06,0-42.53-2.1v46.32h-37.9V251.36a707.77,707.77,0,0,1,77.06-4.21h5.89c47.17,0,60.22,20.85,60.22,46.32v8.42c0,17.69-5.9,33.06-24,40.85l26.1,41.26v8.43Zm-70.74-109.7v29.9a266.16,266.16,0,0,0,33.05,1.9c33.69,0,34.32-5.9,34.32-19.16,0-7.58-.21-14.53-27.79-14.53C1160.25,280.84,1144.25,281.47,1130.35,282.73Z"
fill="currentColor" />
d="M1369.37,137c0-13-11.61-14.92-43.67-15.75-28.18-.83-89.53-2.76-89.53-56.65V61c0-31.78,20.45-60,90.64-60h10a290.93,290.93,0,0,1,72.12,9.12V55.78c-26.8-6.35-53.05-10.22-80.14-10.22-40.07,0-42.83,9.12-42.83,15.47,0,13.27,12.16,15.2,43.39,16,26.25.56,89.81,2.49,89.81,56.93v3.59c0,31.51-20.45,59.7-90.92,59.7h-10a400.9,400.9,0,0,1-82.9-8.85V142c29.84,6.63,63.28,11.05,91.47,11.05C1366.33,153.06,1369.37,143.94,1369.37,137Z" <path fill="#ff1d25" d="M1426,251.36v8.43l-62.75,132.64h-37.89l-62.74-132.64v-8.43h34.1l47.58,107.38,47.59-107.38Z" />
/> <path fill="#ff1d25" d="M1496.49,251.36V392.43h-37.9V251.36Z" />
<path <path
fill="#ff1d25" fill="#ff1d25"
d="M846.11,350.74c0-9.9-8.84-11.37-33.26-12-21.48-.63-68.22-2.11-68.22-43.16v-2.74c0-24.21,15.58-45.69,69.06-45.69h7.58a222,222,0,0,1,55,6.95v34.74c-20.42-4.84-40.43-7.79-61.06-7.79-30.53,0-32.63,6.95-32.63,11.79,0,10.11,9.26,11.58,33.05,12.21,20,.42,68.43,1.9,68.43,43.37v2.74c0,24-15.58,45.48-69.27,45.48h-7.58A305.66,305.66,0,0,1,744,389.9V354.53c22.74,5.05,48.21,8.42,69.69,8.42C843.79,363,846.11,356,846.11,350.74Z" d="M1534.39,316.42c0-38.11,16.63-69.27,76.22-69.27h7.57a218.19,218.19,0,0,1,54.32,6.95v34.32c-19.37-5.69-41-7.79-55.37-7.79-44,0-44,17.26-44,41.26s0,41.48,44,41.48a212.15,212.15,0,0,0,55.37-8V389.9a228.46,228.46,0,0,1-54.32,6.74h-7.57c-59.59,0-76.22-31.16-76.22-69.27Z"
/> />
<path <path fill="#ff1d25" d="M1841.15,285.05h-97.06V306.1h76v33.69h-76v18.95h97.06v33.69h-135V251.36h135Z" />
fill="#ff1d25" <polygon fill="currentColor" points="1458.66 108.93 1458.66 83.93 1565.16 96.43 1671.66 108.93 1565.16 121.43 1458.66 133.93 1458.66 108.93" />
d="M1056.87,285.05H959.81V306.1h76v33.69h-76v18.95h97.06v33.69h-135V251.36h135Z" </svg>
/> {:else if layout === "narrow"}
<path <svg class={$$restProps["class"]} {...$$restProps} viewBox="0 0 1920 1920">
fill="#ff1d25" <g>
d="M1201.09,392.43l-28.21-44.22c-15,0-29.06,0-42.53-2.1v46.32h-37.9V251.36a707.77,707.77,0,0,1,77.06-4.21h5.89c47.17,0,60.22,20.85,60.22,46.32v8.42c0,17.69-5.9,33.06-24,40.85l26.1,41.26v8.43Zm-70.74-109.7v29.9a266.16,266.16,0,0,0,33.05,1.9c33.69,0,34.32-5.9,34.32-19.16,0-7.58-.21-14.53-27.79-14.53C1160.25,280.84,1144.25,281.47,1130.35,282.73Z" <path
/> fill="#ff1d25"
<path d="M780.1,1364.81l88.49-508.71l-323.41,419.08h-88.42L280.44,867l-87.78,497.81H8.56l149.5-847.85h162.3 l224.43,526.88l405.4-526.88h161.09l-147.08,847.85H780.1z"
fill="#ff1d25" />
d="M1426,251.36v8.43l-62.75,132.64h-37.89l-62.74-132.64v-8.43h34.1l47.58,107.38,47.59-107.38Z" <path
/> fill="#ff1d25"
<path fill="#ff1d25" d="M1496.49,251.36V392.43h-37.9V251.36Z" /> d="M1113.38,1281.23l92.67-147.77c62.28,52.08,162.46,88.42,264.2,88.42c116.28,0,170.35-38.76,179.53-90.84 c27.98-158.67-484.21-49.66-428.68-364.58c25.42-144.14,162.83-264.05,403.87-264.05c106.59,0,211.11,25.44,281.08,75.1 l-86.83,148.98c-70.83-44.81-148.13-66.62-223.23-66.62c-116.28,0-168.78,43.6-178.18,96.9 c-27.55,156.25,484.42,48.45,429.53,359.73c-24.99,141.71-163.83,262.83-406.08,262.83 C1306.83,1379.34,1178.22,1339.37,1113.38,1281.23z"
<path />
fill="#ff1d25" </g>
d="M1534.39,316.42c0-38.11,16.63-69.27,76.22-69.27h7.57a218.19,218.19,0,0,1,54.32,6.95v34.32c-19.37-5.69-41-7.79-55.37-7.79-44,0-44,17.26-44,41.26s0,41.48,44,41.48a212.15,212.15,0,0,0,55.37-8V389.9a228.46,228.46,0,0,1-54.32,6.74h-7.57c-59.59,0-76.22-31.16-76.22-69.27Z" </svg>
/> {/if}
<path
fill="#ff1d25"
d="M1841.15,285.05h-97.06V306.1h76v33.69h-76v18.95h97.06v33.69h-135V251.36h135Z"
/>
<polygon
fill="currentColor"
points="1458.66 108.93 1458.66 83.93 1565.16 96.43 1671.66 108.93 1565.16 121.43 1458.66 133.93 1458.66 108.93"
/>
</svg>
{:else if layout === 'narrow'}
<svg class={$$restProps['class']} {...$$restProps} viewBox="0 0 1920 1920">
<g>
<path
fill="#ff1d25"
d="M780.1,1364.81l88.49-508.71l-323.41,419.08h-88.42L280.44,867l-87.78,497.81H8.56l149.5-847.85h162.3 l224.43,526.88l405.4-526.88h161.09l-147.08,847.85H780.1z"
/>
<path
fill="#ff1d25"
d="M1113.38,1281.23l92.67-147.77c62.28,52.08,162.46,88.42,264.2,88.42c116.28,0,170.35-38.76,179.53-90.84 c27.98-158.67-484.21-49.66-428.68-364.58c25.42-144.14,162.83-264.05,403.87-264.05c106.59,0,211.11,25.44,281.08,75.1 l-86.83,148.98c-70.83-44.81-148.13-66.62-223.23-66.62c-116.28,0-168.78,43.6-178.18,96.9 c-27.55,156.25,484.42,48.45,429.53,359.73c-24.99,141.71-163.83,262.83-406.08,262.83 C1306.83,1379.34,1178.22,1339.37,1113.38,1281.23z"
/>
</g>
</svg>
{/if}

View File

@@ -1,22 +1,22 @@
<script lang="ts"> <script lang="ts">
import { twMerge } from 'tailwind-merge'; import { twMerge } from "tailwind-merge";
</script> </script>
<svg <svg
class={twMerge('h-full', $$restProps['class'])} class={twMerge("h-full", $$restProps["class"])}
{...$$restProps} {...$$restProps}
fill="currentColor" fill="currentColor"
version="1.1" version="1.1"
id="Layer_1" id="Layer_1"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:xlink="http://www.w3.org/1999/xlink"
viewBox="0 0 493.488 493.488" viewBox="0 0 493.488 493.488"
xml:space="preserve" xml:space="preserve"
> >
<g> <g>
<g> <g>
<path <path
d="M492.358,143.652L350.266,1.304c-0.736-0.736-1.716-1.136-2.74-1.136L146.406,0c-1.012,0-1.996,0.4-2.716,1.108 d="M492.358,143.652L350.266,1.304c-0.736-0.736-1.716-1.136-2.74-1.136L146.406,0c-1.012,0-1.996,0.4-2.716,1.108
L1.358,143.216c-0.732,0.712-1.148,1.692-1.148,2.724L0.03,347.052c0,1.02,0.404,2,1.12,2.72l142.1,142.336 L1.358,143.216c-0.732,0.712-1.148,1.692-1.148,2.724L0.03,347.052c0,1.02,0.404,2,1.12,2.72l142.1,142.336
c0.72,0.712,1.692,1.168,2.716,1.168l201.12,0.212h0.008c1.02,0,1.996-0.44,2.704-1.16l142.36-142.1 c0.72,0.712,1.692,1.168,2.716,1.168l201.12,0.212h0.008c1.02,0,1.996-0.44,2.704-1.16l142.36-142.1
c0.736-0.716,1.136-1.704,1.136-2.724l0.164-201.128C493.458,145.352,493.062,144.368,492.358,143.652z M257.318,373.324 c0.736-0.716,1.136-1.704,1.136-2.724l0.164-201.128C493.458,145.352,493.062,144.368,492.358,143.652z M257.318,373.324
@@ -26,7 +26,7 @@
c-0.008,8.484-6.916,15.408-15.428,15.408c-8.512-0.008-15.428-6.94-15.42-15.436l0.216-169.668 c-0.008,8.484-6.916,15.408-15.428,15.408c-8.512-0.008-15.428-6.94-15.42-15.436l0.216-169.668
c0.008-8.512,7.776-15.428,15.488-15.428v-0.006c4.099,0.019,7.926,1.624,10.816,4.538c2.912,2.916,4.496,6.796,4.496,10.908 c0.008-8.512,7.776-15.428,15.488-15.428v-0.006c4.099,0.019,7.926,1.624,10.816,4.538c2.912,2.916,4.496,6.796,4.496,10.908
L261.894,300.728z" L261.894,300.728z"
/> />
</g> </g>
</g> </g>
</svg> </svg>

View File

@@ -1,22 +1,22 @@
<script lang="ts"> <script lang="ts">
import { twMerge } from 'tailwind-merge'; import { twMerge } from "tailwind-merge";
</script> </script>
<svg <svg
class={twMerge('h-full', $$restProps['class'])} class={twMerge("h-full", $$restProps["class"])}
{...$$restProps} {...$$restProps}
fill="currentColor" fill="currentColor"
version="1.1" version="1.1"
id="Layer_1" id="Layer_1"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:xlink="http://www.w3.org/1999/xlink"
viewBox="0 0 493.636 493.636" viewBox="0 0 493.636 493.636"
xml:space="preserve" xml:space="preserve"
> >
<g> <g>
<g> <g>
<path <path
d="M421.428,72.476C374.868,25.84,312.86,0.104,246.724,0.044C110.792,0.044,0.112,110.624,0,246.548 d="M421.428,72.476C374.868,25.84,312.86,0.104,246.724,0.044C110.792,0.044,0.112,110.624,0,246.548
c-0.068,65.912,25.544,127.944,72.1,174.584c46.564,46.644,108.492,72.46,174.4,72.46h0.58v-0.048 c-0.068,65.912,25.544,127.944,72.1,174.584c46.564,46.644,108.492,72.46,174.4,72.46h0.58v-0.048
c134.956,0,246.428-110.608,246.556-246.532C493.7,181.12,468,119.124,421.428,72.476z M257.516,377.292 c134.956,0,246.428-110.608,246.556-246.532C493.7,181.12,468,119.124,421.428,72.476z M257.516,377.292
c-2.852,2.856-6.844,4.5-10.904,4.5c-4.052,0-8.044-1.66-10.932-4.516c-2.856-2.864-4.496-6.852-4.492-10.916 c-2.852,2.856-6.844,4.5-10.904,4.5c-4.052,0-8.044-1.66-10.932-4.516c-2.856-2.864-4.496-6.852-4.492-10.916
@@ -25,7 +25,7 @@
c-0.008,8.508-6.928,15.404-15.448,15.404c-8.5-0.008-15.42-6.916-15.416-15.432L231.528,135 c-0.008,8.508-6.928,15.404-15.448,15.404c-8.5-0.008-15.42-6.916-15.416-15.432L231.528,135
c0.004-8.484,3.975-15.387,15.488-15.414c4.093,0.021,7.895,1.613,10.78,4.522c2.912,2.916,4.476,6.788,4.472,10.912 c0.004-8.484,3.975-15.387,15.488-15.414c4.093,0.021,7.895,1.613,10.78,4.522c2.912,2.916,4.476,6.788,4.472,10.912
L262.112,304.692z" L262.112,304.692z"
/> />
</g> </g>
</g> </g>
</svg> </svg>

View File

@@ -1,28 +1,28 @@
<script lang="ts"> <script lang="ts">
import { twMerge } from 'tailwind-merge'; import { twMerge } from "tailwind-merge";
</script> </script>
<svg <svg
class={twMerge('h-full', $$restProps['class'])} class={twMerge("h-full", $$restProps["class"])}
{...$$restProps} {...$$restProps}
fill="currentColor" fill="currentColor"
version="1.1" version="1.1"
id="Layer_1" id="Layer_1"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:xlink="http://www.w3.org/1999/xlink"
viewBox="0 0 493.464 493.464" viewBox="0 0 493.464 493.464"
xml:space="preserve" xml:space="preserve"
> >
<g> <g>
<g> <g>
<path <path
d="M246.736,0C110.692,0,0.004,110.68,0.004,246.732c0,136.06,110.688,246.732,246.732,246.732 d="M246.736,0C110.692,0,0.004,110.68,0.004,246.732c0,136.06,110.688,246.732,246.732,246.732
c136.048,0,246.724-110.672,246.724-246.732C493.456,110.68,382.78,0,246.736,0z M360.524,208.716L230.98,338.268 c136.048,0,246.724-110.672,246.724-246.732C493.456,110.68,382.78,0,246.736,0z M360.524,208.716L230.98,338.268
c-2.82,2.824-7.816,2.824-10.64,0l-86.908-86.912c-1.412-1.416-2.192-3.3-2.192-5.324c0.004-2.016,0.784-3.912,2.192-5.336 c-2.82,2.824-7.816,2.824-10.64,0l-86.908-86.912c-1.412-1.416-2.192-3.3-2.192-5.324c0.004-2.016,0.784-3.912,2.192-5.336
l11.108-11.104c1.412-1.408,3.3-2.18,5.328-2.18c2.016,0,3.908,0.772,5.316,2.18l67.752,67.752c1.5,1.516,3.94,1.516,5.444,0 l11.108-11.104c1.412-1.408,3.3-2.18,5.328-2.18c2.016,0,3.908,0.772,5.316,2.18l67.752,67.752c1.5,1.516,3.94,1.516,5.444,0
l110.392-110.392c2.824-2.824,7.828-2.824,10.644,0l11.108,11.124c1.412,1.4,2.208,3.304,2.208,5.308 l110.392-110.392c2.824-2.824,7.828-2.824,10.644,0l11.108,11.124c1.412,1.4,2.208,3.304,2.208,5.308
C362.732,205.412,361.936,207.3,360.524,208.716z" C362.732,205.412,361.936,207.3,360.524,208.716z"
/> />
</g> </g>
</g> </g>
</svg> </svg>

View File

@@ -1,29 +1,29 @@
<script lang="ts"> <script lang="ts">
import { twMerge } from 'tailwind-merge'; import { twMerge } from "tailwind-merge";
</script> </script>
<svg <svg
class={twMerge('h-full', $$restProps['class'])} class={twMerge("h-full", $$restProps["class"])}
{...$$restProps} {...$$restProps}
fill="currentColor" fill="currentColor"
version="1.1" version="1.1"
id="Layer_1" id="Layer_1"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:xlink="http://www.w3.org/1999/xlink"
viewBox="0 0 512 512" viewBox="0 0 512 512"
xml:space="preserve" xml:space="preserve"
> >
<g> <g>
<g> <g>
<path <path
d="M507.494,426.066L282.864,53.537c-5.677-9.415-15.87-15.172-26.865-15.172c-10.995,0-21.188,5.756-26.865,15.172 d="M507.494,426.066L282.864,53.537c-5.677-9.415-15.87-15.172-26.865-15.172c-10.995,0-21.188,5.756-26.865,15.172
L4.506,426.066c-5.842,9.689-6.015,21.774-0.451,31.625c5.564,9.852,16.001,15.944,27.315,15.944h449.259 L4.506,426.066c-5.842,9.689-6.015,21.774-0.451,31.625c5.564,9.852,16.001,15.944,27.315,15.944h449.259
c11.314,0,21.751-6.093,27.315-15.944C513.508,447.839,513.336,435.755,507.494,426.066z M256.167,167.227 c11.314,0,21.751-6.093,27.315-15.944C513.508,447.839,513.336,435.755,507.494,426.066z M256.167,167.227
c12.901,0,23.817,7.278,23.817,20.178c0,39.363-4.631,95.929-4.631,135.292c0,10.255-11.247,14.554-19.186,14.554 c12.901,0,23.817,7.278,23.817,20.178c0,39.363-4.631,95.929-4.631,135.292c0,10.255-11.247,14.554-19.186,14.554
c-10.584,0-19.516-4.3-19.516-14.554c0-39.363-4.63-95.929-4.63-135.292C232.021,174.505,242.605,167.227,256.167,167.227z c-10.584,0-19.516-4.3-19.516-14.554c0-39.363-4.63-95.929-4.63-135.292C232.021,174.505,242.605,167.227,256.167,167.227z
M256.498,411.018c-14.554,0-25.471-11.908-25.471-25.47c0-13.893,10.916-25.47,25.471-25.47c13.562,0,25.14,11.577,25.14,25.47 M256.498,411.018c-14.554,0-25.471-11.908-25.471-25.47c0-13.893,10.916-25.47,25.471-25.47c13.562,0,25.14,11.577,25.14,25.47
C281.638,399.11,270.06,411.018,256.498,411.018z" C281.638,399.11,270.06,411.018,256.498,411.018z"
/> />
</g> </g>
</g> </g>
</svg> </svg>

View File

@@ -1,15 +1,8 @@
<script lang="ts"> <script lang="ts">
import { twMerge } from 'tailwind-merge'; import { twMerge } from "tailwind-merge";
export let href: string; export let href: string;
</script> </script>
<a <a {...$$restProps} {href} class={twMerge("font-medium transition-colors hover:text-accent hover:underline", $$restProps["class"])}>
{...$$restProps} <slot />
{href} </a>
class={twMerge(
'font-medium transition-colors hover:text-accent hover:underline',
$$restProps['class']
)}
>
<slot />
</a>

View File

@@ -1,59 +1,59 @@
<script lang="ts"> <script lang="ts">
import { scale } from 'svelte/transition'; import { scale } from "svelte/transition";
import { twMerge } from 'tailwind-merge'; import { twMerge } from "tailwind-merge";
export const toggle = () => { export const toggle = () => {
if (visible) { if (visible) {
close(); close();
} else { } else {
show(); show();
} }
}; };
export const show = () => { export const show = () => {
modalRef.showModal(); modalRef.showModal();
onShow(); onShow();
visible = true; visible = true;
}; };
export const close = () => { export const close = () => {
modalRef.close(); modalRef.close();
onClose(); onClose();
visible = false; visible = false;
}; };
export let onShow = () => {}; export let onShow = () => {};
export let onClose = () => {}; export let onClose = () => {};
let visible = false; let visible = false;
let modalRef: HTMLDialogElement; let modalRef: HTMLDialogElement;
</script> </script>
<!-- svelte-ignore a11y-click-events-have-key-events --> <!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-noninteractive-element-interactions --> <!-- svelte-ignore a11y-no-noninteractive-element-interactions -->
<dialog <dialog
bind:this={modalRef} bind:this={modalRef}
on:close={() => (visible = false)} on:close={() => (visible = false)}
transition:scale={{ duration: 200 }} transition:scale={{ duration: 200 }}
class={twMerge( class={twMerge(
`fixed inset-0 z-40 flex max-h-[calc(100vh-16rem)] flex-col rounded-md border-2 border-contrast-100 bg-primary-100 p-6 text-contrast-900 lg:w-3/4`, `fixed inset-0 z-40 flex max-h-[calc(100vh-16rem)] flex-col rounded-md border-2 border-contrast-100 bg-primary-100 p-6 text-contrast-900 lg:w-3/4`,
$$restProps['class'], $$restProps["class"],
visible ? '' : 'hidden' visible ? "" : "hidden"
)} )}
{...$$restProps} {...$$restProps}
> >
<div class="mb-4 flex"> <div class="mb-4 flex">
<h1 class="grow text-2xl font-semibold"> <h1 class="grow text-2xl font-semibold">
<slot name="header" /> <slot name="header" />
</h1> </h1>
</div> </div>
<div class="grow overflow-auto rounded-md bg-primary-300 p-4 shadow-inner"> <div class="grow overflow-auto rounded-md bg-primary-300 p-4 shadow-inner">
<slot /> <slot />
</div> </div>
<div class="mt-4 flex justify-end gap-4"> <div class="mt-4 flex justify-end gap-4">
<slot name="button-row" /> <slot name="button-row" />
</div> </div>
</dialog> </dialog>

View File

@@ -8,9 +8,7 @@
</script> </script>
{#if $toasts.length >= 1} {#if $toasts.length >= 1}
<div <div class="fixed bottom-0 right-0 z-50 flex w-full flex-col gap-2 p-4 text-white sm:w-96">
class="fixed bottom-0 right-0 z-50 flex w-full flex-col gap-2 p-4 text-white sm:w-96"
>
{#each $toasts as toastItem} {#each $toasts as toastItem}
<div <div
class:bg-red-500={toastItem.type === "error"} class:bg-red-500={toastItem.type === "error"}
@@ -42,24 +40,9 @@
{toastItem.text} {toastItem.text}
</p> </p>
<button <button on:click={() => toast.dismiss(toastItem.id)} class="shrink-0" aria-label="dismiss">
on:click={() => toast.dismiss(toastItem.id)} <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6">
class="shrink-0" <path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
aria-label="dismiss"
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="w-6"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M6 18L18 6M6 6l12 12"
/>
</svg> </svg>
</button> </button>
</div> </div>

View File

@@ -1,46 +1,36 @@
import { writable } from 'svelte/store'; import { writable } from "svelte/store";
export const toasts = writable<Toast[]>([]); export const toasts = writable<Toast[]>([]);
type Toast = { type Toast = {
id: string; id: string;
type: 'success' | 'error' | 'info' | 'warning'; type: "success" | "error" | "info" | "warning";
text: string; text: string;
}; };
function addToast( function addToast(text: string, type: "success" | "error" | "warning" | "info" = "info", dismissible: boolean) {
text: string, const id: string = `toast-${crypto.randomUUID ? crypto.randomUUID() : Math.floor(Date.now() * Math.random() * 100).toString()}`;
type: 'success' | 'error' | 'warning' | 'info' = 'info',
dismissible: boolean toasts.update((all) => [
) { ...all,
const id: string = `toast-${ {
crypto.randomUUID id: id,
? crypto.randomUUID() text: text,
: Math.floor(Date.now() * Math.random() * 100).toString() dismissible: dismissible,
}`; type: type
}
toasts.update((all) => [ ]);
...all, }
{
id: id, function dismiss(id: string) {
text: text, toasts.update((all) => all.filter((x) => x.id !== id));
dismissible: dismissible, }
type: type
} export const toast = {
]); clear: () => toasts.set([]),
} error: (text: string | null, dismissible = true) => addToast(text ?? "En feil oppstod", "error", dismissible),
info: (text: string, dismissible = true) => addToast(text, "info", dismissible),
function dismiss(id: string) { warning: (text: string, dismissible = true) => addToast(text, "warning", dismissible),
toasts.update((all) => all.filter((x) => x.id !== id)); success: (text: string | null, dismissible = true) => addToast(text ?? "Suksess", "success", dismissible),
} dismiss: dismiss
};
export const toast = {
clear: () => toasts.set([]),
error: (text: string | null, dismissible = true) =>
addToast(text ?? 'En feil oppstod', 'error', dismissible),
info: (text: string, dismissible = true) => addToast(text, 'info', dismissible),
warning: (text: string, dismissible = true) => addToast(text, 'warning', dismissible),
success: (text: string | null, dismissible = true) =>
addToast(text ?? 'Suksess', 'success', dismissible),
dismiss: dismiss
};

View File

@@ -1,99 +0,0 @@
import homeIcon from "../../assets/icons/home_icon.svg";
import contactIcon from "../../assets/icons/contact_icon.svg";
import busIcon from "../../assets/icons/bus_icon.svg";
type Route = {
url: string;
text: string;
icon: string | null;
target: string | null;
};
export class Routes {
static home: Route = {
url: "/",
text: "Hjem",
icon: homeIcon,
target: null,
};
static bus: Route = {
url: "/bus",
text: "Buss",
icon: busIcon,
target: null,
};
static taxi: Route = {
url: "/taxi",
text: "Taxi",
icon: null,
target: null,
};
static accessibility: Route = {
url: "/accessibility",
text: "Tilgjengelighetsfunksjoner",
icon: null,
target: null,
};
static contact: Route = {
url: "/contact",
text: "Ta kontakt",
icon: contactIcon,
target: null,
};
static facebook: Route = {
url: "https://www.facebook.com/MinibusserviceSteneAS/",
text: "Facebook",
icon: null,
target: "_blank",
};
static email: Route = {
url: "mailto:minibusstur@hotmail.com",
text: "E-post",
icon: null,
target: null,
};
static phone: Route = {
url: "tel:45256161",
text: "Telefon",
icon: null,
target: null,
};
static topbarRoutes: Route[] = [Routes.home, Routes.bus, Routes.contact];
static sidebarRoutes: Route[] = [
Routes.home,
Routes.bus,
Routes.taxi,
Routes.accessibility,
Routes.contact,
];
static footerRoutes: { text: string; routes: Route[] }[] = [
{
text: "HJELP",
routes: [Routes.contact],
},
{
text: "SIDER",
routes: [
Routes.home,
Routes.bus,
Routes.taxi,
Routes.accessibility,
Routes.contact,
],
},
{
text: "MINIBUSSERVICE",
routes: [Routes.facebook, Routes.email, Routes.phone],
},
];
}

87
src/lib/routes.ts Normal file
View File

@@ -0,0 +1,87 @@
import homeIcon from "../assets/icons/home_icon.svg";
import contactIcon from "../assets/icons/contact_icon.svg";
import busIcon from "../assets/icons/bus_icon.svg";
type Route = {
url: string;
text: string;
icon: string | null;
target: string | null;
};
export class Routes {
static home: Route = {
url: "/",
text: "Hjem",
icon: homeIcon,
target: null
};
static bus: Route = {
url: "/bus",
text: "Buss",
icon: busIcon,
target: null
};
static taxi: Route = {
url: "/taxi",
text: "Taxi",
icon: null,
target: null
};
static accessibility: Route = {
url: "/accessibility",
text: "Tilgjengelighetsfunksjoner",
icon: null,
target: null
};
static contact: Route = {
url: "/contact",
text: "Ta kontakt",
icon: contactIcon,
target: null
};
static facebook: Route = {
url: "https://www.facebook.com/MinibusserviceSteneAS/",
text: "Facebook",
icon: null,
target: "_blank"
};
static email: Route = {
url: "mailto:minibusstur@hotmail.com",
text: "E-post",
icon: null,
target: null
};
static phone: Route = {
url: "tel:45256161",
text: "Telefon",
icon: null,
target: null
};
static topbarRoutes: Route[] = [Routes.home, Routes.bus, Routes.contact];
static sidebarRoutes: Route[] = [Routes.home, Routes.bus, Routes.taxi, Routes.accessibility, Routes.contact];
static footerRoutes: { text: string; routes: Route[] }[] = [
{
text: "HJELP",
routes: [Routes.contact]
},
{
text: "SIDER",
routes: [Routes.home, Routes.bus, Routes.taxi, Routes.accessibility, Routes.contact]
},
{
text: "MINIBUSSERVICE",
routes: [Routes.facebook, Routes.email, Routes.phone]
}
];
}

View File

@@ -1,39 +0,0 @@
import { writable } from "svelte/store";
function createThemeToggler() {
let defaultValue = false;
let savedValue = localStorage.getItem("dark_theme");
if (savedValue) {
defaultValue = JSON.parse(savedValue);
} else if (
window.matchMedia &&
window.matchMedia("(prefers-color-scheme: dark)").matches
) {
defaultValue = true;
}
if (defaultValue) {
document.body.dataset.theme = "dark";
} else {
document.body.dataset.theme = "light";
}
const { subscribe, update } = writable(defaultValue);
return {
subscribe,
set: (value: boolean) => {
update(() => value);
if (value) {
document.body.dataset.theme = "dark";
} else {
document.body.dataset.theme = "light";
}
localStorage.setItem("dark_theme", JSON.stringify(value));
},
};
}
export const darkTheme = createThemeToggler();

24
src/lib/theme.svelte.ts Normal file
View File

@@ -0,0 +1,24 @@
function loadTheme() {
let theme = localStorage.getItem("theme");
if (!theme) {
if (window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches) {
theme = "dark";
} else {
theme = "light";
}
}
return theme;
}
let currentTheme = $state(loadTheme());
export function toggleTheme() {
const newTheme = currentTheme === "dark" ? "light" : "dark";
localStorage.setItem("theme", newTheme);
currentTheme = newTheme;
}
export function getTheme(): string {
return currentTheme;
}

View File

@@ -1,8 +0,0 @@
export function debounce<T extends Function>(cb: T, wait = 500) {
let h: any;
let callable = () => {
clearTimeout(h);
h = setTimeout(() => cb(), wait);
};
return callable;
}

View File

@@ -1,8 +1,21 @@
import { mount } from "svelte"; import { mount } from "svelte";
import { route } from "./lib/components/Router.svelte";
import App from "./App.svelte"; import App from "./App.svelte";
import HomePage from "./pages/HomePage.svelte";
import TaxiPage from "./pages/TaxiPage.svelte";
import BusPage from "./pages/BusPage.svelte";
import ContactPage from "./pages/ContactPage.svelte";
import AccessibilityPage from "./pages/AccessibilityPage.svelte";
route("/", HomePage);
route("/taxi", TaxiPage);
route("/bus", BusPage);
route("/contact", ContactPage);
route("/accessibility", AccessibilityPage);
const app = mount(App, { const app = mount(App, {
target: document.getElementById("app")!, target: document.getElementById("app")!
}); });
export default app; export default app;

View File

@@ -4,34 +4,21 @@
import threeRampsOpen from "../assets/images/three_ramps_open.jpg"; import threeRampsOpen from "../assets/images/three_ramps_open.jpg";
import Carousel from "../lib/components/carousel/Carousel.svelte"; import Carousel from "../lib/components/carousel/Carousel.svelte";
let rampCarouselItems: string[] = [ let rampCarouselItems: string[] = [singleRampLeftClosed, singleRampRightClosed, threeRampsOpen];
singleRampLeftClosed,
singleRampRightClosed,
threeRampsOpen,
];
</script> </script>
<h1 class="text-xl font-semibold md:text-2xl">Tilgjengelighetsfunksjoner</h1> <h1 class="text-xl font-semibold md:text-2xl">Tilgjengelighetsfunksjoner</h1>
<section <section class="mt-4 justify-between gap-5 rounded-md bg-primary-300 p-4 shadow-inner md:flex">
class="mt-4 justify-between gap-5 rounded-md bg-primary-300 p-4 shadow-inner md:flex"
>
<div> <div>
<h2 class="grow text-lg font-semibold md:text-xl"> <h2 class="grow text-lg font-semibold md:text-xl">Sitter du i rullestol?</h2>
Sitter du i rullestol?
</h2>
<p class="md:text-lg"> <p class="md:text-lg">
Vi har flere busser med rullestolstøtte, med kapasitet opp til 2 Vi har flere busser med rullestolstøtte, med kapasitet opp til 2 rullestoler per buss.
rullestoler per buss.
<br />Total kapasitet er 5 rullestilbrukere fordelt på 3 busser <br />Total kapasitet er 5 rullestilbrukere fordelt på 3 busser
</p> </p>
</div> </div>
<div class="md:w-96"> <div class="md:w-96">
<Carousel <Carousel items={rampCarouselItems} buttonClass="bg-primary-200" let:item>
items={rampCarouselItems}
buttonClass="bg-primary-200"
let:item
>
<div class="h-full w-96 overflow-hidden shadow-sm"> <div class="h-full w-96 overflow-hidden shadow-sm">
<img class="card w-full object-cover" src={item} alt="" /> <img class="card w-full object-cover" src={item} alt="" />
</div> </div>

View File

@@ -9,43 +9,23 @@
import tourBusSideLogo from "../assets/images/tour_bus_side_logo.jpg"; import tourBusSideLogo from "../assets/images/tour_bus_side_logo.jpg";
import Carousel from "../lib/components/carousel/Carousel.svelte"; import Carousel from "../lib/components/carousel/Carousel.svelte";
let minibusCarouselItems: string[] = [ let minibusCarouselItems: string[] = [threeBusses, twoBusses, fiveBusses, singleRampLeftClosed, threeRampsOpen];
threeBusses,
twoBusses,
fiveBusses,
singleRampLeftClosed,
threeRampsOpen,
];
let tourBusCarouselItems: string[] = [ let tourBusCarouselItems: string[] = [twoTourBuses, tourBusSide, tourBusSideLogo];
twoTourBuses,
tourBusSide,
tourBusSideLogo,
];
</script> </script>
<h1 class="text-xl font-semibold md:text-2xl">Buss</h1> <h1 class="text-xl font-semibold md:text-2xl">Buss</h1>
<section <section id="minibus" class="mt-4 justify-between gap-5 rounded-md bg-primary-300 p-4 shadow-inner md:flex">
id="minibus"
class="mt-4 justify-between gap-5 rounded-md bg-primary-300 p-4 shadow-inner md:flex"
>
<div> <div>
<h2 class="grow text-lg font-semibold md:text-xl">Minibusser</h2> <h2 class="grow text-lg font-semibold md:text-xl">Minibusser</h2>
<p class="md:text-lg"> <p class="md:text-lg">
Vi har idag fire minibusser, to Mercedes Sprinter og to Ford Vi har idag fire minibusser, to Mercedes Sprinter og to Ford Transit. Vi har også en fullelektrisk Mercedes EQV og en Mitsubishi Outlander.
Transit. Vi har også en fullelektrisk Mercedes EQV og en Mitsubishi <br />Vi finner løsninger for grupper fra 5 - 32 passasjerer med støtte for inntil 3 rullestolbrukere samtidig
Outlander.
<br />Vi finner løsninger for grupper fra 5 - 32 passasjerer med
støtte for inntil 3 rullestolbrukere samtidig
</p> </p>
</div> </div>
<div class="md:w-96"> <div class="md:w-96">
<Carousel <Carousel items={minibusCarouselItems} buttonClass="bg-primary-200" let:item>
items={minibusCarouselItems}
buttonClass="bg-primary-200"
let:item
>
<div class="w-96 overflow-hidden shadow-sm"> <div class="w-96 overflow-hidden shadow-sm">
<img class="card w-full object-cover" src={item} alt="" /> <img class="card w-full object-cover" src={item} alt="" />
</div> </div>
@@ -53,26 +33,17 @@
</div> </div>
</section> </section>
<section <section id="tour" class="mt-12 justify-between gap-5 rounded-md bg-primary-300 p-4 shadow-inner md:flex">
id="tour"
class="mt-12 justify-between gap-5 rounded-md bg-primary-300 p-4 shadow-inner md:flex"
>
<div> <div>
<h2 class="grow text-lg font-semibold md:text-xl">Turbusser</h2> <h2 class="grow text-lg font-semibold md:text-xl">Turbusser</h2>
<p class="md:text-lg"> <p class="md:text-lg">
Vi er medeier i Storfjord Turbuss AS som har tre turbusser. Vi er medeier i Storfjord Turbuss AS som har tre turbusser.
<br />Storfjord Turbuss AS ble etablert i 2018, og vi utfører <br />Storfjord Turbuss AS ble etablert i 2018, og vi utfører turkjøring og lager pakkeløsninger etter dine ønsker og behov.
turkjøring og lager pakkeløsninger etter dine ønsker og behov. <br />Vår målsetning er å alltid være det naturlige førstevalget av transportør
<br />Vår målsetning er å alltid være det naturlige førstevalget av
transportør
</p> </p>
</div> </div>
<div class="md:w-96"> <div class="md:w-96">
<Carousel <Carousel items={tourBusCarouselItems} buttonClass="bg-primary-200" let:item>
items={tourBusCarouselItems}
buttonClass="bg-primary-200"
let:item
>
<div class="w-96 overflow-hidden shadow-sm"> <div class="w-96 overflow-hidden shadow-sm">
<img class="card w-full object-cover" src={item} alt="" /> <img class="card w-full object-cover" src={item} alt="" />
</div> </div>

View File

@@ -8,7 +8,7 @@
import twoTourBusses from "../assets/images/two_tour_busses.jpg"; import twoTourBusses from "../assets/images/two_tour_busses.jpg";
import van from "../assets/images/van.webp"; import van from "../assets/images/van.webp";
import Link from "../lib/components/link/Link.svelte"; import Link from "../lib/components/link/Link.svelte";
import { Routes } from "../lib/global/routes"; import { Routes } from "../lib/routes";
type CardCarouselItem = { type CardCarouselItem = {
header: string; header: string;
@@ -22,79 +22,59 @@
image: threeBusses, image: threeBusses,
header: "Minibusstransport", header: "Minibusstransport",
text: "Vi har 4 minibusser i ulike størrelser", text: "Vi har 4 minibusser i ulike størrelser",
url: `${Routes.bus.url}#minibus`, url: `${Routes.bus.url}#minibus`
}, },
{ {
image: taxis, image: taxis,
header: "Taxikjøring", header: "Taxikjøring",
text: "Vi kjører taxi med kapasitet opptil 8 personer", text: "Vi kjører taxi med kapasitet opptil 8 personer",
url: `${Routes.taxi.url}#person`, url: `${Routes.taxi.url}#person`
}, },
{ {
image: wheelchairRamps, image: wheelchairRamps,
header: "Rullestoltransport", header: "Rullestoltransport",
text: "Fire av våre biler er rullestoltilpasset med plass til inntil 3 rullestoler", text: "Fire av våre biler er rullestoltilpasset med plass til inntil 3 rullestoler",
url: `${Routes.accessibility.url}`, url: `${Routes.accessibility.url}`
}, },
{ {
image: twoTourBusses, image: twoTourBusses,
header: "Turbuss", header: "Turbuss",
text: "Vi er medeier i Storfjord Turbuss AS, som har tre moderne turbusser for inntil 50 personer", text: "Vi er medeier i Storfjord Turbuss AS, som har tre moderne turbusser for inntil 50 personer",
url: `${Routes.bus.url}#tour`, url: `${Routes.bus.url}#tour`
}, },
{ {
image: van, image: van,
header: "Pakketaxi", header: "Pakketaxi",
text: "Vi har tilbud om pakketaxi. Både småpakker og volumkrevende gods kan transporteres", text: "Vi har tilbud om pakketaxi. Både småpakker og volumkrevende gods kan transporteres",
url: `${Routes.taxi.url}#package`, url: `${Routes.taxi.url}#package`
}, }
]; ];
</script> </script>
<div class="flex gap-8"> <div class="flex gap-8">
<div class="overflow-hidden"> <div class="overflow-hidden">
<div> <div>
<img <img src={header_image} class="rounded-md border-2 border-contrast-100" alt="Minibusses lined up in a row" />
src={header_image}
class="rounded-md border-2 border-contrast-100"
alt="Minibusses lined up in a row"
/>
<h1 class="mt-4 text-xl font-semibold md:text-2xl"> <h1 class="mt-4 text-xl font-semibold md:text-2xl">
Med komfort og sikkerhet i førersetet<br />Bli med på en trygg Med komfort og sikkerhet i førersetet<br />Bli med på en trygg og behagelig tur
og behagelig tur
</h1> </h1>
<p class="mt-2 font-medium opacity-70 md:text-lg"> <p class="mt-2 font-medium opacity-70 md:text-lg">
Minibusservice Stene AS driver personbefordring med taxi, Minibusservice Stene AS driver personbefordring med taxi, minibuss og busser.
minibuss og busser. <br />Vi har lang erfaring og finner gode løsninger for kundene våre.
<br />Vi har lang erfaring og finner gode løsninger for kundene <br />Kontraktskjøring for helseforetak, busselskap og kommune utgjør størsteparten av oppdragene våre, men vi tilbyr også turer for privatpersoner, bedrifter og
våre. turister.
<br />Kontraktskjøring for helseforetak, busselskap og kommune <br />Vi er godkjent lærebedrift og setter kunden og miljøet i fokus. Vi har både elektriske, hybride og dieselkjøretøy i flåten
utgjør størsteparten av oppdragene våre, men vi tilbyr også
turer for privatpersoner, bedrifter og turister.
<br />Vi er godkjent lærebedrift og setter kunden og miljøet i
fokus. Vi har både elektriske, hybride og dieselkjøretøy i
flåten
</p> </p>
</div> </div>
<div class="mt-12"> <div class="mt-12">
<h1 class="text-xl font-semibold md:text-2xl">Våre tjenester</h1> <h1 class="text-xl font-semibold md:text-2xl">Våre tjenester</h1>
<Carousel <Carousel class="mt-2 rounded-md border-2 border-contrast-100 bg-primary-300 p-4 shadow-inner" items={carouselItems} let:item>
class="mt-2 rounded-md border-2 border-contrast-100 bg-primary-300 p-4 shadow-inner" <div class="card flex h-full w-64 flex-col overflow-hidden border-contrast-100 shadow-sm transition-transform">
items={carouselItems}
let:item
>
<div
class="card flex h-full w-64 flex-col overflow-hidden border-contrast-100 shadow-sm transition-transform"
>
{#if item.image} {#if item.image}
<img <img class="h-44 w-full border-b-2 border-contrast-100 object-cover" src={item.image} alt="" />
class="h-44 w-full border-b-2 border-contrast-100 object-cover"
src={item.image}
alt=""
/>
{/if} {/if}
<div class="flex grow flex-col p-4"> <div class="flex grow flex-col p-4">
<h3 class="font-semibold"> <h3 class="font-semibold">
@@ -103,10 +83,7 @@
<p class="grow whitespace-normal"> <p class="grow whitespace-normal">
{item.text} {item.text}
</p> </p>
<Link <Link class="hi mt-2 w-fit font-semibold" href={item.url}>Les mer...</Link>
class="hi mt-2 w-fit font-semibold"
href={item.url}>Les mer...</Link
>
</div> </div>
</div> </div>
</Carousel> </Carousel>

View File

@@ -9,25 +9,17 @@
<h1 class="text-xl font-semibold md:text-2xl">Taxi</h1> <h1 class="text-xl font-semibold md:text-2xl">Taxi</h1>
<section <section id="person" class="mt-4 justify-between gap-5 rounded-md bg-primary-300 p-4 shadow-inner md:flex">
id="person"
class="mt-4 justify-between gap-5 rounded-md bg-primary-300 p-4 shadow-inner md:flex"
>
<div> <div>
<h2 class="grow text-lg font-semibold md:text-xl">Persontaxi</h2> <h2 class="grow text-lg font-semibold md:text-xl">Persontaxi</h2>
<p class="md:text-lg"> <p class="md:text-lg">
Vi har taxier som dekker de fleste behov. Vi har taxier som dekker de fleste behov.
<br />Rullestoltransport, firehjulstrekk og helelektrisk bil er noen <br />Rullestoltransport, firehjulstrekk og helelektrisk bil er noen av tilbudene våre.
av tilbudene våre.
<br />Taxiene våre kan ta opp til 8 passasjerer <br />Taxiene våre kan ta opp til 8 passasjerer
</p> </p>
</div> </div>
<div class="md:w-96"> <div class="md:w-96">
<Carousel <Carousel items={taxiCarouselItems} buttonClass="bg-primary-200" let:item>
items={taxiCarouselItems}
buttonClass="bg-primary-200"
let:item
>
<div class="card w-96 overflow-hidden shadow-sm"> <div class="card w-96 overflow-hidden shadow-sm">
<img class="w-full object-cover" src={item} alt="" /> <img class="w-full object-cover" src={item} alt="" />
</div> </div>
@@ -35,23 +27,13 @@
</div> </div>
</section> </section>
<section <section id="package" class="mt-4 justify-between gap-5 rounded-md bg-primary-300 p-4 shadow-inner md:flex">
id="package"
class="mt-4 justify-between gap-5 rounded-md bg-primary-300 p-4 shadow-inner md:flex"
>
<div> <div>
<h2 class="grow text-lg font-semibold md:text-xl">Pakketaxi</h2> <h2 class="grow text-lg font-semibold md:text-xl">Pakketaxi</h2>
<p class="md:text-lg"> <p class="md:text-lg">Vi har tilbud om pakketaxi. Både småpakker og volumkrevende gods kan transporteres</p>
Vi har tilbud om pakketaxi. Både småpakker og volumkrevende gods kan
transporteres
</p>
</div> </div>
<div class="md:w-96"> <div class="md:w-96">
<Carousel <Carousel items={packageTaxiCarouselItems} buttonClass="bg-primary-200" let:item>
items={packageTaxiCarouselItems}
buttonClass="bg-primary-200"
let:item
>
<div class="card w-96 overflow-hidden"> <div class="card w-96 overflow-hidden">
<img class="w-full object-cover" src={item} alt="" /> <img class="w-full object-cover" src={item} alt="" />
</div> </div>

View File

@@ -1,2 +1,2 @@
/** @type {import("@sveltejs/vite-plugin-svelte").SvelteConfig} */ /** @type {import("@sveltejs/vite-plugin-svelte").SvelteConfig} */
export default {} export default {};

View File

@@ -1,21 +1,21 @@
{ {
"extends": "@tsconfig/svelte/tsconfig.json", "extends": "@tsconfig/svelte/tsconfig.json",
"compilerOptions": { "compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"target": "ES2023", "target": "ES2023",
"useDefineForClassFields": true, "useDefineForClassFields": true,
"module": "ESNext", "module": "ESNext",
"types": ["svelte", "vite/client"], "types": ["svelte", "vite/client"],
"noEmit": true, "noEmit": true,
/** /**
* Typecheck JS in `.svelte` and `.js` files by default. * Typecheck JS in `.svelte` and `.js` files by default.
* Disable checkJs if you'd like to use dynamic types in JS. * Disable checkJs if you'd like to use dynamic types in JS.
* Note that setting allowJs false does not prevent the use * Note that setting allowJs false does not prevent the use
* of JS in `.svelte` files. * of JS in `.svelte` files.
*/ */
"allowJs": true, "allowJs": true,
"checkJs": true, "checkJs": true,
"moduleDetection": "force" "moduleDetection": "force"
}, },
"include": ["src/**/*.ts", "src/**/*.js", "src/**/*.svelte"] "include": ["src/**/*.ts", "src/**/*.js", "src/**/*.svelte"]
} }

View File

@@ -1,7 +1,4 @@
{ {
"files": [], "files": [],
"references": [ "references": [{ "path": "./tsconfig.app.json" }, { "path": "./tsconfig.node.json" }]
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
]
} }

View File

@@ -1,26 +1,26 @@
{ {
"compilerOptions": { "compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"target": "ES2023", "target": "ES2023",
"lib": ["ES2023"], "lib": ["ES2023"],
"module": "ESNext", "module": "ESNext",
"types": ["node"], "types": ["node"],
"skipLibCheck": true, "skipLibCheck": true,
/* Bundler mode */ /* Bundler mode */
"moduleResolution": "bundler", "moduleResolution": "bundler",
"allowImportingTsExtensions": true, "allowImportingTsExtensions": true,
"verbatimModuleSyntax": true, "verbatimModuleSyntax": true,
"moduleDetection": "force", "moduleDetection": "force",
"noEmit": true, "noEmit": true,
/* Linting */ /* Linting */
"strict": true, "strict": true,
"noUnusedLocals": true, "noUnusedLocals": true,
"noUnusedParameters": true, "noUnusedParameters": true,
"erasableSyntaxOnly": true, "erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true, "noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true "noUncheckedSideEffectImports": true
}, },
"include": ["vite.config.ts"] "include": ["vite.config.ts"]
} }

View File

@@ -4,5 +4,5 @@ import tailwindcss from "@tailwindcss/vite";
// https://vite.dev/config/ // https://vite.dev/config/
export default defineConfig({ export default defineConfig({
plugins: [svelte(), tailwindcss()], plugins: [svelte(), tailwindcss()]
}); });