Compare commits

...

9 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
nub31
1e1e829e2d ... 2026-03-16 23:50:53 +01:00
nub31
f686c4a7d2 ... 2026-03-16 23:41:41 +01:00
49 changed files with 2741 additions and 3780 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>

55
package-lock.json generated
View File

@@ -7,17 +7,18 @@
"": { "": {
"name": "minibusservice.no", "name": "minibusservice.no",
"version": "0.0.0", "version": "0.0.0",
"dependencies": {
"leaflet": "^1.9.4",
"tailwind-merge": "^3.5.0"
},
"devDependencies": { "devDependencies": {
"@sveltejs/vite-plugin-svelte": "^7.0.0", "@sveltejs/vite-plugin-svelte": "^7.0.0",
"@tailwindcss/vite": "^0.0.0-insiders.aaaefe8", "@tailwindcss/vite": "^0.0.0-insiders.aaaefe8",
"@tsconfig/svelte": "^5.0.8", "@tsconfig/svelte": "^5.0.8",
"@types/leaflet": "^1.9.21",
"@types/node": "^24.12.0", "@types/node": "^24.12.0",
"leaflet": "^1.9.4",
"prettier": "^3.8.1",
"prettier-plugin-svelte": "^3.5.1",
"svelte": "^5.53.7", "svelte": "^5.53.7",
"svelte-check": "^4.4.5", "svelte-check": "^4.4.5",
"tailwind-merge": "^3.5.0",
"tailwindcss": "^0.0.0-insiders.aaaefe8", "tailwindcss": "^0.0.0-insiders.aaaefe8",
"typescript": "~5.9.3", "typescript": "~5.9.3",
"vite": "^8.0.0" "vite": "^8.0.0"
@@ -994,6 +995,23 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/geojson": {
"version": "7946.0.16",
"resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz",
"integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/leaflet": {
"version": "1.9.21",
"resolved": "https://registry.npmjs.org/@types/leaflet/-/leaflet-1.9.21.tgz",
"integrity": "sha512-TbAd9DaPGSnzp6QvtYngntMZgcRk+igFELwR2N99XZn7RXUdKgsXMR+28bUO0rPsWp8MIu/f47luLIQuSLYv/w==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/geojson": "*"
}
},
"node_modules/@types/node": { "node_modules/@types/node": {
"version": "24.12.0", "version": "24.12.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.12.0.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-24.12.0.tgz",
@@ -1207,6 +1225,7 @@
"version": "1.9.4", "version": "1.9.4",
"resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz", "resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz",
"integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==", "integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==",
"dev": true,
"license": "BSD-2-Clause" "license": "BSD-2-Clause"
}, },
"node_modules/lightningcss": { "node_modules/lightningcss": {
@@ -1576,6 +1595,33 @@
"node": "^10 || ^12 || >=14" "node": "^10 || ^12 || >=14"
} }
}, },
"node_modules/prettier": {
"version": "3.8.1",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.1.tgz",
"integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==",
"dev": true,
"license": "MIT",
"bin": {
"prettier": "bin/prettier.cjs"
},
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/prettier/prettier?sponsor=1"
}
},
"node_modules/prettier-plugin-svelte": {
"version": "3.5.1",
"resolved": "https://registry.npmjs.org/prettier-plugin-svelte/-/prettier-plugin-svelte-3.5.1.tgz",
"integrity": "sha512-65+fr5+cgIKWKiqM1Doum4uX6bY8iFCdztvvp2RcF+AJoieaw9kJOFMNcJo/bkmKYsxFaM9OsVZK/gWauG/5mg==",
"dev": true,
"license": "MIT",
"peerDependencies": {
"prettier": "^3.0.0",
"svelte": "^3.2.0 || ^4.0.0-next.0 || ^5.0.0-next.0"
}
},
"node_modules/readdirp": { "node_modules/readdirp": {
"version": "4.1.2", "version": "4.1.2",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
@@ -1703,6 +1749,7 @@
"version": "3.5.0", "version": "3.5.0",
"resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.5.0.tgz", "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.5.0.tgz",
"integrity": "sha512-I8K9wewnVDkL1NTGoqWmVEIlUcB9gFriAEkXkfCjX5ib8ezGxtR3xD7iZIxrfArjEsH7F1CHD4RFUtxefdqV/A==", "integrity": "sha512-I8K9wewnVDkL1NTGoqWmVEIlUcB9gFriAEkXkfCjX5ib8ezGxtR3xD7iZIxrfArjEsH7F1CHD4RFUtxefdqV/A==",
"dev": true,
"license": "MIT", "license": "MIT",
"funding": { "funding": {
"type": "github", "type": "github",

View File

@@ -7,21 +7,31 @@
"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": { "devDependencies": {
"@sveltejs/vite-plugin-svelte": "^7.0.0", "@sveltejs/vite-plugin-svelte": "^7.0.0",
"@tailwindcss/vite": "^0.0.0-insiders.aaaefe8", "@tailwindcss/vite": "^0.0.0-insiders.aaaefe8",
"@tsconfig/svelte": "^5.0.8", "@tsconfig/svelte": "^5.0.8",
"@types/leaflet": "^1.9.21",
"@types/node": "^24.12.0", "@types/node": "^24.12.0",
"leaflet": "^1.9.4",
"prettier": "^3.8.1",
"prettier-plugin-svelte": "^3.5.1",
"svelte": "^5.53.7", "svelte": "^5.53.7",
"svelte-check": "^4.4.5", "svelte-check": "^4.4.5",
"tailwind-merge": "^3.5.0",
"tailwindcss": "^0.0.0-insiders.aaaefe8", "tailwindcss": "^0.0.0-insiders.aaaefe8",
"typescript": "~5.9.3", "typescript": "~5.9.3",
"vite": "^8.0.0" "vite": "^8.0.0"
}, },
"dependencies": { "prettier": {
"leaflet": "^1.9.4", "trailingComma": "none",
"tailwind-merge": "^3.5.0" "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>
<main class="grow px-4 pb-16 pt-8">
<div class="mx-auto max-w-7xl">
<Router> <Router>
{#snippet notfound()} {#snippet notfound()}
<h1>Not found</h1> <h1>Not found</h1>
<p> <p>Sorry, the page you are looking for does not exist.</p>
Sorry, the page you are looking for does not exist.
</p>
<p>Return to <a href="/">home</a>.</p> <p>Return to <a href="/">home</a>.</p>
{/snippet} {/snippet}
</Router> </Router>
</div> </Layout>
</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

@@ -5,33 +5,19 @@
<div class="mt-2 flex flex-col gap-8 md:flex-row md:text-lg"> <div class="mt-2 flex flex-col gap-8 md:flex-row md:text-lg">
<div class="flex flex-1 flex-col"> <div class="flex flex-1 flex-col">
<p class="block pt-2 font-medium opacity-70 md:hidden"> <p class="pt-2 font-medium opacity-70">
Spørsmål?<br />Kontakt oss via tlf, e-post eller via skjemaet under Spørsmål?<br />Kontakt oss via tlf eller e-post
</p> </p>
<p class="hidden pt-2 font-medium opacity-70 md:block"> <Link class="w-fit font-bold text-accent hover:underline" href="tel:45256161">Tlf: +47 45 25 61 61</Link>
Spørsmål?<br />Kontakt oss via tlf, e-post eller via skjemaet til <Link class="w-fit font-bold text-accent hover:underline" href="mailto:minibusstur@hotmail.com">E-post: minibusstur@hotmail.com</Link>
høyre
</p>
<Link
class="w-fit font-bold text-accent hover:underline"
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

@@ -2,6 +2,7 @@
import { onDestroy, onMount } from "svelte"; import { onDestroy, onMount } from "svelte";
import "leaflet/dist/leaflet.css"; import "leaflet/dist/leaflet.css";
import { twMerge } from "tailwind-merge"; import { twMerge } from "tailwind-merge";
import L from "leaflet";
interface MarkerPoint { interface MarkerPoint {
latitude: number; latitude: number;
@@ -15,32 +16,26 @@
let map: L.Map | null = null; let map: L.Map | null = null;
let markers: L.Marker<any>[] = []; let markers: L.Marker<any>[] = [];
var icon: L.Icon; var icon: L.Icon;
let L: typeof import("leaflet/index");
onMount(async () => { onMount(async () => {
L = await import("leaflet");
icon = L.icon({ icon = L.icon({
iconUrl: "/map_marker.png", iconUrl: "/map_marker.png",
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();
@@ -52,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);
@@ -72,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
}); });
} }
} }
@@ -80,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

@@ -1,42 +0,0 @@
<script lang="ts">
import Button from "./button/Button.svelte";
import { twMerge } from "tailwind-merge";
export let maxItems: number;
let pageParam = $page.url.searchParams.get("page");
let currentPage = 1;
if (pageParam) {
let pageParamAsNumber = Number(pageParam);
if (pageParamAsNumber) {
currentPage = pageParamAsNumber;
}
}
</script>
<section
class={twMerge("mt-8 flex justify-center gap-5", $$restProps["class"])}
>
{#if currentPage > 1}
<a
on:click={() =>
setTimeout(() => {
currentPage = currentPage - 1;
}, 10)}
href={`?page=${currentPage - 1}`}
>
<Button>Forrige</Button>
</a>
{/if}
{#if (currentPage + 1) * 10 - 10 < maxItems}
<a
on:click={() =>
setTimeout(() => {
currentPage = currentPage + 1;
}, 10)}
href={`?page=${currentPage + 1}`}
>
<Button>Neste</Button>
</a>
{/if}
</section>

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) {
@@ -172,71 +161,23 @@
let { notfound }: Props = $props(); let { notfound }: Props = $props();
onMount(() => { onMount(() => {
// @ts-ignore
if (window.navigation) { if (window.navigation) {
// @ts-ignore
function handleNavigate(event: NavigateEvent) { function handleNavigate(event: NavigateEvent) {
event.intercept({ event.intercept({
handler: async () => { handler: async () => {
currentUrl = new URL(event.destination.url); currentUrl = new URL(event.destination.url);
}, }
}); });
} }
// @ts-ignore
window.navigation.addEventListener("navigate", handleNavigate); window.navigation.addEventListener("navigate", handleNavigate);
return () => { return () => {
window.navigation.removeEventListener( // @ts-ignore
"navigate", window.navigation.removeEventListener("navigate", handleNavigate);
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

@@ -10,24 +10,10 @@
<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"
type="rotate"
from="0 18 18"
to="360 18 18"
dur="0.9s"
repeatCount="indefinite"
/>
</path> </path>
<circle fill="currentColor" cx="36" cy="18" r="1"> <circle fill="currentColor" cx="36" cy="18" r="1">
<animateTransform <animateTransform attributeName="transform" type="rotate" from="0 18 18" to="360 18 18" dur="0.9s" repeatCount="indefinite" />
attributeName="transform"
type="rotate"
from="0 18 18"
to="360 18 18"
dur="0.9s"
repeatCount="indefinite"
/>
</circle> </circle>
</g> </g>
</g> </g>

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -1,322 +0,0 @@
<script lang="ts">
import Link from './link/Link.svelte';
</script>
<h2 class="mb-1 text-xl font-semibold">Innledning</h2>
<p class="mb-2">
Dette kjøpet er regulert av de nedenstående standard salgsbetingelser for forbrukerkjøp av varer
over Internett. Forbrukerkjøp over internett reguleres hovedsakelig av avtaleloven,
forbrukerkjøpsloven, markedsføringsloven, angrerettloven og ehandelsloven, og disse lovene gir
forbrukeren ufravikelige rettigheter. Lovene er tilgjengelig på
<Link href="www.lovdata.no">www.lovdata.no</Link>. Vilkårene i denne avtalen skal ikke forstås som
noen begrensning i de lovbestemte rettighetene, men oppstiller partenes viktigste rettigheter og
plikter for handelen.
</p>
<p class="mb-2">
Salgsbetingelsene er utarbeidet og anbefalt av Forbrukertilsynet. For en bedre forståelse av disse
salgsbetingelsene, se Forbrukertilsynets veileder her.
</p>
<h2 class="mb-1 text-xl font-semibold">1. Avtalen</h2>
<p class="mb-2">
Avtalen består av disse salgsbetingelsene, opplysninger gitt i bestillingsløsningen og eventuelt
særskilt avtalte vilkår. Ved eventuell motstrid mellom opplysningene, går det som særskilt er
avtalt mellom partene foran, så fremt det ikke strider mot ufravikelig lovgivning.
</p>
<p class="mb-2">
Avtalen vil i tillegg bli utfylt av relevante lovbestemmelser som regulerer kjøp av varer mellom
næringsdrivende og forbrukere.
</p>
<h2 class="mb-1 text-xl font-semibold">2. Partene</h2>
<p class="mb-2">
Selger er Tor Stian Stene, adresse: Solnørdalsvegen 40, 6240 Ørskog, e-post:
minibusservice@hotmail.com, tlf: 45266161, org.nr: 816 230 942, og betegnes i det følgende som
selgeren.
</p>
<p class="mb-2">
Kjøper er den forbrukeren som foretar bestillingen, og betegnes i det følgende som kjøperen.
</p>
<h2 class="mb-1 text-xl font-semibold">3. Pris</h2>
<p class="mb-2">
Den oppgitte prisen for varen og tjenester er den totale prisen kjøper skal betale. Denne prisen
inkluderer alle avgifter og tilleggskostnader. Ytterligere kostnader som selger før kjøpet ikke
har informert om, skal kjøper ikke bære.
</p>
<h2 class="mb-1 text-xl font-semibold">4. Avtaleinngåelse</h2>
<p class="mb-2">
Avtalen er bindende for begge parter når kjøperen har sendt sin bestilling til selgeren.
</p>
<p class="mb-2">
Avtalen er likevel ikke bindende hvis det har forekommet skrive- eller tastefeil i tilbudet fra
selgeren i bestillingsløsningen i nettbutikken eller i kjøperens bestilling, og den annen part
innså eller burde ha innsett at det forelå en slik feil.
</p>
<h2 class="mb-1 text-xl font-semibold">5. Betalingen</h2>
<p class="mb-2">
Selgeren kan kreve betaling for varen fra det tidspunkt den blir sendt fra selgeren til kjøperen.
</p>
<p class="mb-2">
Dersom kjøperen bruker kredittkort eller debetkort ved betaling, kan selgeren reservere
kjøpesummen på kortet ved bestilling. Kortet blir belastet samme dag som varen sendes.
</p>
<p class="mb-2">
Ved betaling med faktura, blir fakturaen til kjøperen utstedt ved forsendelse av varen.
Betalingsfristen fremgår av fakturaen og er på minimum 14 dager fra mottak.
</p>
<p class="mb-2">Kjøpere under 18 år kan ikke betale med etterfølgende faktura.</p>
<h2 class="mb-1 text-xl font-semibold">6. Levering</h2>
<p class="mb-2">Levering er skjedd når kjøperen, eller hans representant, har overtatt tingen.</p>
<p class="mb-2">
Hvis ikke leveringstidspunkt fremgår av bestillingsløsningen, skal selgeren levere varen til
kjøper uten unødig opphold og senest 30 dager etter bestillingen fra kunden. Varen skal leveres
hos kjøperen med mindre annet er særskilt avtalt mellom partene.
</p>
<h2 class="mb-1 text-xl font-semibold">7. Risikoen for varen</h2>
<p class="mb-2">
Risikoen for varen går over på kjøper når han, eller kjøpers representant, har fått varene levert
i tråd med punkt 6.
</p>
<h2 class="mb-1 text-xl font-semibold">8. Angrerett</h2>
<p class="mb-2">
Med mindre avtalen er unntatt fra angrerett, kan kjøperen angre kjøpet av varen i henhold til
angrerettloven.
</p>
<p class="mb-2">
Kjøperen må gi selger melding om bruk av angreretten innen 14 dager fra fristen begynner å løpe. I
fristen inkluderes alle kalenderdager. Dersom fristen ender på en lørdag, helligdag eller
høytidsdag forlenges fristen til nærmeste virkedag.
</p>
<p class="mb-2">
Angrefristen anses overholdt dersom melding er sendt før utløpet av fristen. Kjøper har
bevisbyrden for at angreretten er blitt gjort gjeldende, og meldingen bør derfor skje skriftlig
(angrerettskjema, e-post eller brev).
</p>
<p class="mb-2">Angrefristen begynner å løpe:</p>
<ul>
<li>
Ved kjøp av enkeltstående varer vil angrefristen løpe fra dagen etter varen(e) er mottatt.
</li>
<li>
Selges et abonnement, eller innebærer avtalen regelmessig levering av identiske varer, løper
fristen fra dagen etter første forsendelse er mottatt.
</li>
<li>
Består kjøpet av flere leveranser, vil angrefristen løpe fra dagen etter siste leveranse er
mottatt.
</li>
</ul>
<p class="mb-2">
Angrefristen utvides til 12 måneder etter utløpet av den opprinnelige fristen dersom selger ikke
før avtaleinngåelsen opplyser om at det foreligger angrerett og standardisert angreskjema.
Tilsvarende gjelder ved manglende opplysning om vilkår, tidsfrister og fremgangsmåte for å benytte
angreretten. Sørger den næringsdrivende for å gi opplysningene i løpet av disse 12 månedene,
utløper angrefristen likevel 14 dager etter den dagen kjøperen mottok opplysningene.
</p>
<p class="mb-2">
Ved bruk av angreretten må varen leveres tilbake til selgeren uten unødig opphold og senest 14
dager fra melding om bruk av angreretten er gitt. Kjøper dekker de direkte kostnadene ved å
returnere varen, med mindre annet er avtalt eller selger har unnlatt å opplyse om at kjøper skal
dekke returkostnadene. Selgeren kan ikke fastsette gebyr for kjøperens bruk av angreretten.
</p>
<p class="mb-2">
Kjøper kan prøve eller teste varen på en forsvarlig måte for å fastslå varens art, egenskaper og
funksjon, uten at angreretten faller bort. Dersom prøving eller test av varen går utover hva som
er forsvarlig og nødvendig, kan kjøperen bli ansvarlig for eventuell redusert verdi på varen.
</p>
<p class="mb-2">
Selgeren er forpliktet til å tilbakebetale kjøpesummen til kjøperen uten unødig opphold, og senest
14 dager fra selgeren fikk melding om kjøperens beslutning om å benytte angreretten. Selger har
rett til å holde tilbake betalingen til han/hun har mottatt varene fra kjøperen, eller til kjøper
har lagt frem dokumentasjon for at varene er sendt tilbake.
</p>
<h2 class="mb-1 text-xl font-semibold">
9. Forsinkelse og manglende levering - kjøpernes rettigheter og frist for å melde krav
</h2>
<p class="mb-2">
Dersom selgeren ikke leverer varen eller leverer den for sent i henhold til avtalen mellom
partene, og dette ikke skyldes kjøperen eller forhold på kjøperens side, kan kjøperen i henhold
til reglene i forbrukerkjøpslovens kapittel 5 etter omstendighetene holde kjøpesummen tilbake,
kreve oppfyllelse, heve avtalen og/eller kreve erstatning fra selgeren.
</p>
<p class="mb-2">
Ved krav om misligholdsbeføyelser bør meldingen av bevishensyn være skriftlig (for eksempel
e-post).
</p>
<h3 class="mt-1 text-lg font-semibold">Oppfyllelse</h3>
<p class="mb-2">
Kjøper kan fastholde kjøpet og kreve oppfyllelse fra selger. Kjøper kan imidlertid ikke kreve
oppfyllelse dersom det foreligger en hindring som selgeren ikke kan overvinne, eller dersom
oppfyllelse vil medføre en så stor ulempe eller kostnad for selger at det står i vesentlig
misforhold til kjøperens interesse i at selgeren oppfyller. Skulle vanskene falle bort innen
rimelig tid, kan kjøper likevel kreve oppfyllelse.
</p>
<p class="mb-2">
Kjøperen taper sin rett til å kreve oppfyllelse om han eller hun venter urimelig lenge med å
fremme kravet.
</p>
<h3 class="mt-1 text-lg font-semibold">Heving</h3>
<p class="mb-2">
Dersom selgeren ikke leverer varen på leveringstidspunktet, skal kjøperen oppfordre selger til å
levere innen en rimelig tilleggsfrist for oppfyllelse. Dersom selger ikke leverer varen innen
tilleggsfristen, kan kjøperen heve kjøpet.
</p>
<p class="mb-2">
Kjøper kan imidlertid heve kjøpet umiddelbart hvis selger nekter å levere varen. Tilsvarende
gjelder dersom levering til avtalt tid var avgjørende for inngåelsen av avtalen, eller dersom
kjøperen har underrettet selger om at leveringstidspunktet er avgjørende.
</p>
<p class="mb-2">
Leveres tingen etter tilleggsfristen forbrukeren har satt eller etter leveringstidspunktet som var
avgjørende for inngåelsen av avtalen, må krav om heving gjøres gjeldende innen rimelig tid etter
at kjøperen fikk vite om leveringen.
</p>
<h3 class="mt-1 text-lg font-semibold">Erstatning</h3>
<p class="mb-2">
Kjøperen kan kreve erstatning for lidt tap som følge av forsinkelsen. Dette gjelder imidlertid
ikke dersom selgeren godtgjør at forsinkelsen skyldes hindring utenfor selgers kontroll som ikke
med rimelighet kunne blitt tatt i betraktning på avtaletiden, unngått, eller overvunnet følgene
av.
</p>
<h2 class="mb-1 text-xl font-semibold">
10. Mangel ved varen - kjøperens rettigheter og reklamasjonsfrist
</h2>
<p class="mb-2">
Hvis det foreligger en mangel ved varen må kjøper innen rimelig tid etter at den ble oppdaget
eller burde ha blitt oppdaget, gi selger melding om at han eller hun vil påberope seg mangelen.
Kjøper har alltid reklamert tidsnok dersom det skjer innen 2 mnd. fra mangelen ble oppdaget eller
burde blitt oppdaget. Reklamasjon kan skje senest to år etter at kjøper overtok varen. Dersom
varen eller deler av den er ment å vare vesentlig lenger enn to år, er reklamasjonsfristen fem år.
</p>
<p class="mb-2">
Dersom varen har en mangel og dette ikke skyldes kjøperen eller forhold på kjøperens side, kan
kjøperen i henhold til reglene i forbrukerkjøpsloven kapittel 6 etter omstendighetene holde
kjøpesummen tilbake, velge mellom retting og omlevering, kreve prisavslag, kreve avtalen hevet
og/eller kreve erstatning fra selgeren.
</p>
<p class="mb-2">Reklamasjon til selgeren bør skje skriftlig.</p>
<h3 class="mt-1 text-lg font-semibold">Retting eller omlevering</h3>
<p class="mb-2">
Kjøperen kan velge mellom å kreve mangelen rettet eller levering av tilsvarende ting. Selger kan
likevel motsette seg kjøperens krav dersom gjennomføringen av kravet er umulig eller volder
selgeren urimelige kostnader. Retting eller omlevering skal foretas innen rimelig tid. Selger har
i utgangspunktet ikke rett til å foreta mer enn to avhjelpsforsøk for samme mangel.
</p>
<h3 class="mt-1 text-lg font-semibold">Prisavslag</h3>
<p class="mb-2">
Kjøper kan kreve et passende prisavslag dersom varen ikke blir rettet eller omlevert. Dette
innebærer at forholdet mellom nedsatt og avtalt pris svarer til forholdet mellom tingens verdi i
mangelfull og kontraktsmessig stand. Dersom særlige grunner taler for det, kan prisavslaget i
stedet settes lik mangelens betydning for kjøperen.
</p>
<h3 class="mt-1 text-lg font-semibold">Heving</h3>
<p class="mb-2">
Dersom varen ikke er rettet eller omlevert, kan kjøperen også heve kjøpet når mangelen ikke er
uvesentlig.
</p>
<h3 class="mt-1 text-lg font-semibold">11. Selgerens rettigheter ved kjøperens mislighold</h3>
<p class="mb-2">
Dersom kjøperen ikke betaler eller oppfyller de øvrige pliktene etter avtalen eller loven, og
dette ikke skyldes selgeren eller forhold på selgerens side, kan selgeren i henhold til reglene i
forbrukerkjøpsloven kapittel 9 etter omstendighetene holde varen tilbake, kreve oppfyllelse av
avtalen, kreve avtalen hevet samt kreve erstatning fra kjøperen. Selgeren vil også etter
omstendighetene kunne kreve renter ved forsinket betaling, inkassogebyr og et rimelig gebyr ved
uavhentede varer.
</p>
<h3 class="mt-1 text-lg font-semibold">Oppfyllelse</h3>
<p class="mb-2">
Selger kan fastholde kjøpet og kreve at kjøperen betaler kjøpesummen. Er varen ikke levert, taper
selgeren sin rett dersom han venter urimelig lenge med å fremme kravet.
</p>
<h3 class="mt-1 text-lg font-semibold">Heving</h3>
<p class="mb-2">
Selger kan heve avtalen dersom det foreligger vesentlig betalingsmislighold eller annet vesentlig
mislighold fra kjøperens side. Selger kan likevel ikke heve dersom hele kjøpesummen er betalt.
Fastsetter selger en rimelig tilleggsfrist for oppfyllelse og kjøperen ikke betaler innen denne
fristen, kan selger heve kjøpet.
</p>
<h3 class="mt-1 text-lg font-semibold">Renter ved forsinket betaling/inkassogebyr</h3>
<p class="mb-2">
Dersom kjøperen ikke betaler kjøpesummen i henhold til avtalen, kan selger kreve renter av
kjøpesummen etter forsinkelsesrenteloven. Ved manglende betaling kan kravet, etter forutgående
varsel, bli sendt til inkasso. Kjøper kan da bli holdt ansvarlig for gebyr etter inkassoloven.
</p>
<h3 class="mt-1 text-lg font-semibold">Gebyr ved uavhentede ikke-forskuddsbetalte varer</h3>
<p class="mb-2">
Dersom kjøperen unnlater å hente ubetalte varer, kan selger belaste kjøper med et gebyr. Gebyret
skal maksimalt dekke selgerens faktiske utlegg for å levere varen til kjøperen. Et slikt gebyr kan
ikke belastes kjøpere under 18 år.
</p>
<h2 class="mb-1 text-xl font-semibold">12. Garanti</h2>
<p class="mb-2">
Garanti som gis av selgeren eller produsenten, gir kjøperen rettigheter i tillegg til de kjøperen
allerede har etter ufravikelig lovgivning. En garanti innebærer dermed ingen begrensninger i
kjøperens rett til reklamasjon og krav ved forsinkelse eller mangler etter punkt 9 og 10.
</p>
<h2 class="mb-1 text-xl font-semibold">13. Personopplysninger</h2>
<p class="mb-2">
Behandlingsansvarlig for innsamlede personopplysninger er selger. Med mindre kjøperen samtykker
til noe annet, kan selgeren, i tråd med personopplysningsloven, kun innhente og lagre de
personopplysninger som er nødvendig for at selgeren skal kunne gjennomføre forpliktelsene etter
avtalen. Kjøperens personopplysninger vil kun bli utlevert til andre hvis det er nødvendig for at
selger skal få gjennomført avtalen med kjøperen, eller i lovbestemte tilfelle.
</p>
<h2 class="mb-1 text-xl font-semibold">14. Konfliktløsning</h2>
<p class="mb-2">
Klager rettes til selger innen rimelig tid, jf. punkt 9 og 10. Partene skal forsøke å løse
eventuelle tvister i minnelighet. Dersom dette ikke lykkes, kan kjøperen ta kontakt med
Forbrukertilsynet for mekling. Forbrukertilsynet er tilgjengelig på telefon 23 400 600 eller
<Link href="www.forbrukertilsynet.no">www.forbrukertilsynet.no</Link>.
</p>
<p class="mb-2">
Europa-Kommisjonens klageportal kan også brukes hvis du ønsker å inngi en klage. Det er særlig
relevant, hvis du er forbruker bosatt i et annet EU-land. Klagen inngis her:
<Link href="http://ec.europa.eu/odr">http://ec.europa.eu/odr</Link>.
</p>

View File

@@ -9,15 +9,15 @@
<div class="flex items-center gap-1"> <div class="flex items-center gap-1">
{#if leftIcon} {#if leftIcon}
<img src={leftIcon} class={`h-6 ${invertColor && 'dark:invert'}`} alt="" /> <img 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 on:change {checked} type="checkbox" value="" class="peer sr-only" /> <input 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>
</label> </label>
{#if rightIcon} {#if rightIcon}
<img src={rightIcon} class={`h-6 ${invertColor && 'dark:invert'}`} alt="" /> <img src={rightIcon} class={`h-6 ${invertColor && "dark:invert"}`} alt="" />
{/if} {/if}
</div> </div>

View File

@@ -1,6 +1,6 @@
<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;
@@ -15,8 +15,8 @@
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}>

View File

@@ -1,10 +1,10 @@
<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;
@@ -17,11 +17,7 @@
}; };
</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}`}
{...$$restProps}
class={twMerge('inline-flex w-full gap-5 overflow-auto sm:overflow-hidden', $$restProps['class'])}
>
{#each items as item, index} {#each items as item, index}
<div class="carousel transition-transform"> <div class="carousel transition-transform">
<slot {item} {index} /> <slot {item} {index} />
@@ -34,7 +30,7 @@
<div class="mt-2 flex justify-between md:text-lg"> <div class="mt-2 flex justify-between md:text-lg">
<button <button
class={twMerge( class={twMerge(
'rounded-md border-2 border-contrast-100 bg-primary-300 px-4 py-2 transition-colors hover:border-contrast-200 dark:hover:border-accent', "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 buttonClass
)} )}
on:click={prev} on:click={prev}
@@ -43,7 +39,7 @@
</button> </button>
<button <button
class={twMerge( class={twMerge(
'rounded-md border-2 border-contrast-100 bg-primary-300 px-4 py-2 transition-colors hover:border-contrast-200 dark:hover:border-accent', "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 buttonClass
)} )}
on:click={next} on:click={next}

View File

@@ -1,25 +1,14 @@
<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'])}
{...$$restProps}
viewBox="0 0 1840.81 395.57"
>
<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"
/>
<path fill="currentColor" d="M336.11,6.59V191.75H286.36V6.59Z" /> <path fill="currentColor" d="M336.11,6.59V191.75H286.36V6.59Z" />
<path <path fill="currentColor" d="M535.35,125.42V6.59H585.1V191.75H535.35l-97-118.83V191.75H388.61V6.59h49.74Z" />
fill="currentColor"
d="M535.35,125.42V6.59H585.1V191.75H535.35l-97-118.83V191.75H388.61V6.59h49.74Z"
/>
<path fill="currentColor" d="M692,6.59V191.75H642.3V6.59Z" /> <path fill="currentColor" d="M692,6.59V191.75H642.3V6.59Z" />
<path <path
fill="currentColor" fill="currentColor"
@@ -37,34 +26,22 @@
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="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 <path fill="#ff1d25" d="M1056.87,285.05H959.81V306.1h76v33.69h-76v18.95h97.06v33.69h-135V251.36h135Z" />
fill="#ff1d25"
d="M1056.87,285.05H959.81V306.1h76v33.69h-76v18.95h97.06v33.69h-135V251.36h135Z"
/>
<path <path
fill="#ff1d25" fill="#ff1d25"
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" 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 <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" />
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 fill="#ff1d25" d="M1496.49,251.36V392.43h-37.9V251.36Z" />
<path <path
fill="#ff1d25" fill="#ff1d25"
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" 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="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> </svg>
{:else if layout === 'narrow'} {:else if layout === "narrow"}
<svg class={$$restProps['class']} {...$$restProps} viewBox="0 0 1920 1920"> <svg class={$$restProps["class"]} {...$$restProps} viewBox="0 0 1920 1920">
<g> <g>
<path <path
fill="#ff1d25" fill="#ff1d25"

View File

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

View File

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

View File

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

View File

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

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}
{href}
class={twMerge(
'font-medium transition-colors hover:text-accent hover:underline',
$$restProps['class']
)}
>
<slot /> <slot />
</a> </a>

View File

@@ -1,123 +0,0 @@
<script lang="ts" generics="T">
import ListboxItem from './ListboxItem.svelte';
import { writable, type Writable } from 'svelte/store';
import { setListboxContext, type ListboxItemAbstract } from './context';
import { twMerge } from 'tailwind-merge';
/** @description Determine if multiple items can be selected */
export let allowMultiple = true;
/** @description Determine if multiple items can be selected */
export let items: T[];
/** @description Determine if multiple items can be selected */
export let defaultSelectedItems: T[];
/** @description Function called when the selected items changes */
export let onChange: (items: T[]) => void;
let selectedItems: Writable<ListboxItemAbstract<T>[]> = writable([]);
let registeredItems: Writable<ListboxItemAbstract<T>[]> = writable([]);
let focusedItem: Writable<ListboxItemAbstract<T> | null> = writable(null);
function toggleItemSelected(item: ListboxItemAbstract<T>): void {
if (item) {
setFocus(item);
if ($selectedItems.includes(item)) {
if (allowMultiple) {
$selectedItems = $selectedItems.filter((x) => x !== item);
} else {
$selectedItems = [];
}
} else {
if (allowMultiple) {
$selectedItems.push(item);
$selectedItems = $selectedItems;
} else {
$selectedItems = [item];
}
}
}
onChange($selectedItems.map((x) => x.item));
}
function setFocus(item: ListboxItemAbstract<T>) {
const itemEl = document.getElementById(item.id);
$focusedItem = item;
itemEl?.focus();
}
function onKeyDown(e: KeyboardEvent, item: ListboxItemAbstract<T>) {
const index = $registeredItems.findIndex((x) => x === item);
if (index === -1) return;
switch (e.key) {
case 'ArrowDown':
e.preventDefault();
if ($registeredItems.length - 1 === index) {
setFocus($registeredItems[0]);
} else {
setFocus($registeredItems[index + 1]);
}
break;
case 'ArrowUp':
e.preventDefault();
if (index === 0) {
setFocus($registeredItems[$registeredItems.length - 1]);
} else {
setFocus($registeredItems[index - 1]);
}
break;
case 'Enter':
case ' ':
e.preventDefault();
toggleItemSelected(item);
break;
case 'Home':
setFocus($registeredItems[0]);
break;
case 'End':
setFocus($registeredItems[$registeredItems.length - 1]);
break;
}
}
function registerItem(item: ListboxItemAbstract<T>): void {
$registeredItems.push(item);
$registeredItems = $registeredItems;
if (defaultSelectedItems.includes(item.item)) {
$selectedItems.push(item);
$selectedItems = $selectedItems;
}
if ($registeredItems.length === 1) {
$focusedItem = $registeredItems[0];
}
}
function unregisterItem(item: ListboxItemAbstract<T>): void {
$registeredItems = $registeredItems.filter((x) => x.id !== item.id);
$selectedItems = $selectedItems.filter((x) => x.id !== item.id);
}
setListboxContext<T>({
registerItem: registerItem,
unregisterItem: unregisterItem,
selectedItems: selectedItems,
focusedItem: focusedItem
});
</script>
<ul
class={twMerge('flex flex-col gap-2 p-4', $$restProps['class'])}
role="listbox"
aria-activedescendant={$focusedItem?.id}
aria-multiselectable={allowMultiple}
tabindex={0}
>
{#each items as item}
<ListboxItem {onKeyDown} onClick={(e, x) => toggleItemSelected(x)} {item}>
<slot {item} />
</ListboxItem>
{/each}
</ul>

View File

@@ -1,51 +0,0 @@
<script lang="ts" generics="T">
import { onMount } from 'svelte';
import { getListboxContext, type ListboxItemAbstract } from './context';
import { twMerge } from 'tailwind-merge';
export let item: T;
export let onClick: (e: MouseEvent, item: ListboxItemAbstract<T>) => void;
export let onKeyDown: (e: KeyboardEvent, item: ListboxItemAbstract<T>) => void;
const id: string = `listbox-item-${
crypto.randomUUID
? crypto.randomUUID()
: Math.floor(Date.now() * Math.random() * 100).toString()
}`;
const itemAbstract: ListboxItemAbstract<T> = { id: id, item: item };
let {
selectedItems: selectedItems,
registerItem: register,
unregisterItem: unregister,
focusedItem: focusedItem
} = getListboxContext<T>();
onMount(() => {
register(itemAbstract);
return () => {
unregister(itemAbstract);
};
});
</script>
<li
{id}
role="option"
aria-selected={$selectedItems.includes(itemAbstract)}
on:click={(e) => onClick(e, itemAbstract)}
on:keydown={(e) => onKeyDown(e, itemAbstract)}
tabindex={$focusedItem && $focusedItem === itemAbstract ? 0 : -1}
class={twMerge(
`cursor-pointer overflow-hidden rounded-md border-2 border-contrast-200 opacity-80 ${
$selectedItems.includes(itemAbstract)
? 'border-contrast-900 opacity-100 dark:border-accent'
: ''
}`,
$$restProps['class']
)}
>
<slot />
</li>

View File

@@ -1,22 +0,0 @@
import { getContext, setContext } from 'svelte';
import type { Writable } from 'svelte/store';
type ListboxContext<T> = {
registerItem: (item: ListboxItemAbstract<T>) => void;
unregisterItem: (item: ListboxItemAbstract<T>) => void;
selectedItems: Writable<ListboxItemAbstract<T>[]>;
focusedItem: Writable<ListboxItemAbstract<T> | null>;
};
export type ListboxItemAbstract<T> = {
id: string;
item: T;
};
export function setListboxContext<T>(context: ListboxContext<T>): void {
setContext('listbox', context);
}
export function getListboxContext<T>(): ListboxContext<T> {
return getContext('listbox') as ListboxContext<T>;
}

View File

@@ -1,6 +1,6 @@
<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) {
@@ -38,8 +38,8 @@
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}
> >

View File

@@ -1,38 +0,0 @@
<script lang="ts">
import { onMount } from 'svelte';
import { getTabsContext } from './context';
import { twMerge } from 'tailwind-merge';
export let label: string;
const id: string = `tab-${
crypto.randomUUID
? crypto.randomUUID()
: Math.floor(Date.now() * Math.random() * 100).toString()
}`;
const context = getTabsContext();
const { selectedTab } = context;
onMount(() => {
context.register({
id: id,
label: label
});
return () => {
context.unregister(id);
};
});
</script>
{#if $selectedTab === id}
<div
{...$$restProps}
class={twMerge('rounded-md bg-primary-300 p-4 shadow-inner', $$restProps['class'])}
role="tabpanel"
aria-labelledby={id}
>
<slot />
</div>
{/if}

View File

@@ -1,102 +0,0 @@
<script lang="ts">
import { twMerge } from 'tailwind-merge';
import { setTabsContext, type TabAbstract } from './context';
import { writable, type Writable } from 'svelte/store';
export let buttonClass = '';
let tabs: TabAbstract[] = [];
let selectedTab: Writable<string | null> = writable(null);
function setTab(id: string): void {
const tab = tabs.find((x) => x.id == id);
if (tab) {
$selectedTab = tab.id;
}
}
function registerTab(tab: TabAbstract): void {
tabs.push(tab);
tabs = tabs;
if (tabs.length === 1) {
$selectedTab = tabs[0].id;
}
}
function unregisterTab(id: string): void {
tabs = tabs.filter((x) => x.id !== id);
}
function setFocus(tab: TabAbstract) {
const tabEl = document.getElementById(tab.id);
tabEl?.focus();
}
function onKeyDown(e: KeyboardEvent, tab: TabAbstract) {
const index = tabs.findIndex((x) => x.id === tab.id);
if (index === -1) return;
switch (e.key) {
case 'ArrowRight':
e.preventDefault();
if (tabs.length - 1 === index) {
setFocus(tabs[0]);
} else {
setFocus(tabs[index + 1]);
}
break;
case 'ArrowLeft':
e.preventDefault();
if (index === 0) {
setFocus(tabs[tabs.length - 1]);
} else {
setFocus(tabs[index - 1]);
}
break;
case 'Home':
e.preventDefault();
setFocus(tabs[0]);
break;
case 'End':
e.preventDefault();
setFocus(tabs[tabs.length - 1]);
break;
}
}
setTabsContext({
register: registerTab,
unregister: unregisterTab,
selectedTab: selectedTab
});
</script>
<div {...$$restProps} class={twMerge('flex flex-col gap-4', $$restProps['class'])}>
<div
role="tablist"
aria-labelledby="tablist"
class="flex gap-4 overflow-auto rounded-md bg-primary-300 p-4 shadow-inner"
>
{#each tabs as tab, i}
<button
role="tab"
type="button"
id={tab.id}
tabindex={$selectedTab === tab.id || ($selectedTab === null && i === 0) ? 0 : -1}
aria-selected={$selectedTab === tab.id}
aria-controls={tab.id}
class={twMerge(
`rounded-md border-2 border-contrast-200 bg-primary-100 px-4 py-2 hover:border-contrast-900 dark:hover:border-accent ${$selectedTab === tab.id ? 'border-contrast-900 dark:border-accent' : ''}`,
buttonClass
)}
on:click={() => setTab(tab.id)}
on:keydown={(e) => onKeyDown(e, tab)}
>
{tab.label}
</button>
{/each}
</div>
<slot />
</div>

View File

@@ -1,21 +0,0 @@
import { getContext, setContext } from 'svelte';
import type { Writable } from 'svelte/store';
type TabsContext = {
register: (tab: TabAbstract) => void;
unregister: (id: string) => void;
selectedTab: Writable<string | null>;
};
export type TabAbstract = {
id: string;
label: string;
};
export function setTabsContext(context: TabsContext): void {
setContext('tabs', context);
}
export function getTabsContext(): TabsContext {
return getContext('tabs') as TabsContext;
}

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,23 +1,15 @@
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
) {
const id: string = `toast-${
crypto.randomUUID
? crypto.randomUUID()
: Math.floor(Date.now() * Math.random() * 100).toString()
}`;
toasts.update((all) => [ toasts.update((all) => [
...all, ...all,
@@ -36,11 +28,9 @@ function dismiss(id: string) {
export const toast = { export const toast = {
clear: () => toasts.set([]), clear: () => toasts.set([]),
error: (text: string | null, dismissible = true) => error: (text: string | null, dismissible = true) => addToast(text ?? "En feil oppstod", "error", dismissible),
addToast(text ?? 'En feil oppstod', 'error', dismissible), info: (text: string, dismissible = true) => addToast(text, "info", dismissible),
info: (text: string, dismissible = true) => addToast(text, 'info', dismissible), warning: (text: string, dismissible = true) => addToast(text, "warning", dismissible),
warning: (text: string, dismissible = true) => addToast(text, 'warning', dismissible), success: (text: string | null, dismissible = true) => addToast(text ?? "Suksess", "success", dismissible),
success: (text: string | null, dismissible = true) =>
addToast(text ?? 'Suksess', 'success', dismissible),
dismiss: dismiss 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,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

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