pages
This commit is contained in:
12
package-lock.json
generated
12
package-lock.json
generated
@@ -7,6 +7,10 @@
|
|||||||
"": {
|
"": {
|
||||||
"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",
|
||||||
@@ -14,7 +18,6 @@
|
|||||||
"@types/node": "^24.12.0",
|
"@types/node": "^24.12.0",
|
||||||
"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"
|
||||||
@@ -1200,6 +1203,12 @@
|
|||||||
"jiti": "lib/jiti-cli.mjs"
|
"jiti": "lib/jiti-cli.mjs"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/leaflet": {
|
||||||
|
"version": "1.9.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz",
|
||||||
|
"integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==",
|
||||||
|
"license": "BSD-2-Clause"
|
||||||
|
},
|
||||||
"node_modules/lightningcss": {
|
"node_modules/lightningcss": {
|
||||||
"version": "1.32.0",
|
"version": "1.32.0",
|
||||||
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz",
|
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz",
|
||||||
@@ -1694,7 +1703,6 @@
|
|||||||
"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",
|
||||||
|
|||||||
@@ -16,9 +16,12 @@
|
|||||||
"@types/node": "^24.12.0",
|
"@types/node": "^24.12.0",
|
||||||
"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": {
|
||||||
|
"leaflet": "^1.9.4",
|
||||||
|
"tailwind-merge": "^3.5.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,40 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Router, { route } from "./lib/components/Router.svelte";
|
import Router, { route } from "./lib/components/Router.svelte";
|
||||||
import "./app.css";
|
import "./app.css";
|
||||||
|
|
||||||
import HomePage from "./pages/HomePage.svelte";
|
import HomePage from "./pages/HomePage.svelte";
|
||||||
|
import ToastProvider from "./lib/components/toast/ToastProvider.svelte";
|
||||||
|
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("/", HomePage);
|
||||||
|
route("/taxi", TaxiPage);
|
||||||
|
route("/bus", BusPage);
|
||||||
|
route("/contact", ContactPage);
|
||||||
|
route("/accessibility", AccessibilityPage);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Router>
|
<div class="flex min-h-screen flex-col bg-primary-200 text-contrast-900">
|
||||||
{#snippet notfound()}
|
<Navigation>
|
||||||
<h1>Not found</h1>
|
<main class="grow px-4 pb-16 pt-8">
|
||||||
<p>Sorry, the page you are looking for does not exist.</p>
|
<div class="mx-auto max-w-7xl">
|
||||||
<p>Return to <a href="/">home</a>.</p>
|
<Router>
|
||||||
{/snippet}
|
{#snippet notfound()}
|
||||||
</Router>
|
<h1>Not found</h1>
|
||||||
|
<p>
|
||||||
|
Sorry, the page you are looking for does not exist.
|
||||||
|
</p>
|
||||||
|
<p>Return to <a href="/">home</a>.</p>
|
||||||
|
{/snippet}
|
||||||
|
</Router>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<Footer />
|
||||||
|
</Navigation>
|
||||||
|
</div>
|
||||||
|
<ToastProvider />
|
||||||
|
|||||||
31
src/Footer.svelte
Normal file
31
src/Footer.svelte
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
<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>
|
||||||
138
src/Navigation.svelte
Normal file
138
src/Navigation.svelte
Normal file
@@ -0,0 +1,138 @@
|
|||||||
|
<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.svelte";
|
||||||
|
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>
|
||||||
@@ -1,122 +1,38 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { MessageResponseBodyData } from '$lib/types/dtos/message/MessageResponseBodyData';
|
import Link from "./link/Link.svelte";
|
||||||
import type { MinibusserviceResponseBody } from '$lib/types/dtos/MinibusserviceResponseBody';
|
import MapPoint from "./MapPoint.svelte";
|
||||||
import Button from './button/Button.svelte';
|
</script>
|
||||||
import Link from './link/Link.svelte';
|
|
||||||
import MapPoint from './MapPoint.svelte';
|
<div class="mt-2 flex flex-col gap-8 md:flex-row md:text-lg">
|
||||||
import { toast } from './toast/toast';
|
<div class="flex flex-1 flex-col">
|
||||||
|
<p class="block pt-2 font-medium opacity-70 md:hidden">
|
||||||
let buttonDisabled = false;
|
Spørsmål?<br />Kontakt oss via tlf, e-post eller via skjemaet under
|
||||||
|
</p>
|
||||||
let name = '';
|
<p class="hidden pt-2 font-medium opacity-70 md:block">
|
||||||
let email = '';
|
Spørsmål?<br />Kontakt oss via tlf, e-post eller via skjemaet til
|
||||||
let phone = '';
|
høyre
|
||||||
let message = '';
|
</p>
|
||||||
let posting = false;
|
<Link
|
||||||
|
class="w-fit font-bold text-accent hover:underline"
|
||||||
async function sendMessage() {
|
href="tel:45256161"
|
||||||
if (!(name && email && message)) {
|
>
|
||||||
toast.warning('Alle påkrevde felt må fyllest inn');
|
Tlf: +47 45 25 61 61
|
||||||
return;
|
</Link>
|
||||||
}
|
<Link
|
||||||
|
class="w-fit font-bold text-accent hover:underline"
|
||||||
posting = true;
|
href="mailto:minibusstur@hotmail.com"
|
||||||
buttonDisabled = true;
|
>
|
||||||
|
E-post: minibusstur@hotmail.com
|
||||||
try {
|
</Link>
|
||||||
let res = await fetch('/api/v1/message', {
|
|
||||||
method: 'POST',
|
<MapPoint
|
||||||
headers: {
|
class="card mt-5 min-h-96 grow overflow-hidden rounded-md border-2"
|
||||||
'Content-Type': 'application/json'
|
coordinates={[
|
||||||
},
|
{
|
||||||
body: JSON.stringify({
|
latitude: 62.48303957042255,
|
||||||
name,
|
longitude: 6.8108274964451745,
|
||||||
email,
|
},
|
||||||
phone,
|
]}
|
||||||
message
|
/>
|
||||||
})
|
</div>
|
||||||
});
|
</div>
|
||||||
|
|
||||||
let data: MinibusserviceResponseBody<MessageResponseBodyData> = await res.json();
|
|
||||||
if (res.ok) {
|
|
||||||
toast.success(data.message);
|
|
||||||
} else {
|
|
||||||
console.error(data.message);
|
|
||||||
toast.error(data.message);
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err);
|
|
||||||
buttonDisabled = false;
|
|
||||||
toast.error('En feil oppstod under sending av meldingen');
|
|
||||||
} finally {
|
|
||||||
posting = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="mt-2 flex flex-col gap-8 md:flex-row md:text-lg">
|
|
||||||
<div class="flex flex-1 flex-col">
|
|
||||||
<p class="block pt-2 font-medium opacity-70 md:hidden">
|
|
||||||
Spørsmål?<br />Kontakt oss via tlf, e-post eller via skjemaet under
|
|
||||||
</p>
|
|
||||||
<p class="hidden pt-2 font-medium opacity-70 md:block">
|
|
||||||
Spørsmål?<br />Kontakt oss via tlf, e-post eller via skjemaet til 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
|
|
||||||
class="card mt-5 min-h-96 grow overflow-hidden rounded-md border-2"
|
|
||||||
coordinates={[
|
|
||||||
{
|
|
||||||
latitude: 62.48303957042255,
|
|
||||||
longitude: 6.8108274964451745
|
|
||||||
}
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<form
|
|
||||||
on:submit|preventDefault={sendMessage}
|
|
||||||
class="flex flex-1 flex-col gap-4 pt-4 text-xl md:pt-0"
|
|
||||||
>
|
|
||||||
<label>
|
|
||||||
<span class="text-xl font-semibold">
|
|
||||||
<span class="text-accent"> * </span>
|
|
||||||
Navn
|
|
||||||
</span>
|
|
||||||
<input required bind:value={name} placeholder="Navn" class="bg-primary-100" type="text" />
|
|
||||||
</label>
|
|
||||||
<label>
|
|
||||||
<span class="text-xl font-semibold">
|
|
||||||
<span class="text-accent"> * </span>
|
|
||||||
E-post
|
|
||||||
</span>
|
|
||||||
<input
|
|
||||||
required
|
|
||||||
bind:value={email}
|
|
||||||
placeholder="E-postadresse"
|
|
||||||
class="bg-primary-100"
|
|
||||||
type="email"
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
<label>
|
|
||||||
<span class="text-xl font-semibold">Telefonnummer</span>
|
|
||||||
<input bind:value={phone} placeholder="Telefonnummer" class="bg-primary-100" type="tel" />
|
|
||||||
</label>
|
|
||||||
<label>
|
|
||||||
<span class="text-xl font-semibold">
|
|
||||||
<span class="text-accent"> * </span>
|
|
||||||
Melding
|
|
||||||
</span>
|
|
||||||
<textarea required bind:value={message} class="h-60 bg-primary-100" placeholder="Melding" />
|
|
||||||
</label>
|
|
||||||
<div>
|
|
||||||
<Button class="w-full" disabled={buttonDisabled} loading={posting} type="submit">Send</Button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
|
|||||||
@@ -1,79 +1,86 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onDestroy, onMount } from 'svelte';
|
import { onDestroy, onMount } from "svelte";
|
||||||
import 'leaflet/dist/leaflet.css';
|
import "leaflet/dist/leaflet.css";
|
||||||
import { env } from '$env/dynamic/public';
|
import { twMerge } from "tailwind-merge";
|
||||||
import { twMerge } from 'tailwind-merge';
|
|
||||||
|
interface MarkerPoint {
|
||||||
interface MarkerPoint {
|
latitude: number;
|
||||||
latitude: number;
|
longitude: number;
|
||||||
longitude: number;
|
tooltip?: string;
|
||||||
tooltip?: string;
|
}
|
||||||
}
|
|
||||||
|
export let coordinates: MarkerPoint[];
|
||||||
export let coordinates: MarkerPoint[];
|
|
||||||
|
let mapElement: HTMLDivElement;
|
||||||
let mapElement: HTMLDivElement;
|
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");
|
||||||
let L: typeof import('leaflet/index');
|
|
||||||
|
onMount(async () => {
|
||||||
onMount(async () => {
|
L = await import("leaflet");
|
||||||
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(env.PUBLIC_TILE_SERVER_URL, {
|
}
|
||||||
maxZoom: 18,
|
|
||||||
attribution:
|
L.tileLayer("https://tile.openstreetmap.org/{z}/{x}/{y}.png", {
|
||||||
'Map data © <a href="https://www.openstreetmap.org/">OpenStreetMap</a> contributors, <a href="https://creativecommons.org/licenses/by-sa/2.0/">CC-BY-SA</a>',
|
maxZoom: 18,
|
||||||
id: 'base'
|
attribution:
|
||||||
}).addTo(map);
|
'© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
|
||||||
|
id: "base",
|
||||||
updateMarkers();
|
}).addTo(map);
|
||||||
});
|
|
||||||
|
updateMarkers();
|
||||||
onDestroy(async () => {
|
});
|
||||||
map?.remove();
|
|
||||||
});
|
onDestroy(async () => {
|
||||||
|
map?.remove();
|
||||||
function addMarker(point: MarkerPoint) {
|
});
|
||||||
let marker = L.marker([point.latitude, point.longitude], { icon: icon });
|
|
||||||
|
function addMarker(point: MarkerPoint) {
|
||||||
if (point.tooltip)
|
let marker = L.marker([point.latitude, point.longitude], {
|
||||||
marker.bindTooltip(point.tooltip, {
|
icon: icon,
|
||||||
direction: 'bottom'
|
});
|
||||||
});
|
|
||||||
|
if (point.tooltip)
|
||||||
markers.push(marker);
|
marker.bindTooltip(point.tooltip, {
|
||||||
map?.addLayer(marker);
|
direction: "bottom",
|
||||||
}
|
});
|
||||||
|
|
||||||
function updateMarkers() {
|
markers.push(marker);
|
||||||
if (L && map) {
|
map?.addLayer(marker);
|
||||||
markers.forEach((point) => map?.removeLayer(point));
|
}
|
||||||
markers = [];
|
|
||||||
|
function updateMarkers() {
|
||||||
coordinates.forEach(addMarker);
|
if (L && map) {
|
||||||
|
markers.forEach((point) => map?.removeLayer(point));
|
||||||
map?.flyToBounds(L.featureGroup(markers).getBounds(), {
|
markers = [];
|
||||||
duration: 1
|
|
||||||
});
|
coordinates.forEach(addMarker);
|
||||||
}
|
|
||||||
}
|
map?.flyToBounds(L.featureGroup(markers).getBounds(), {
|
||||||
|
duration: 1,
|
||||||
$: coordinates && updateMarkers();
|
});
|
||||||
</script>
|
}
|
||||||
|
}
|
||||||
<div bind:this={mapElement} class={twMerge('z-0 h-full w-full', $$restProps['class'])} />
|
|
||||||
|
$: coordinates && updateMarkers();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
bind:this={mapElement}
|
||||||
|
class={twMerge("z-0 h-full w-full", $$restProps["class"])}
|
||||||
|
></div>
|
||||||
|
|||||||
@@ -1,41 +1,42 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { page } from '$app/stores';
|
import Button from "./button/Button.svelte";
|
||||||
import Button from '$lib/components/button/Button.svelte';
|
import { twMerge } from "tailwind-merge";
|
||||||
import { twMerge } from 'tailwind-merge';
|
|
||||||
|
export let maxItems: number;
|
||||||
export let maxItems: number;
|
|
||||||
|
let pageParam = $page.url.searchParams.get("page");
|
||||||
let pageParam = $page.url.searchParams.get('page');
|
let currentPage = 1;
|
||||||
let currentPage = 1;
|
if (pageParam) {
|
||||||
if (pageParam) {
|
let pageParamAsNumber = Number(pageParam);
|
||||||
let pageParamAsNumber = Number(pageParam);
|
if (pageParamAsNumber) {
|
||||||
if (pageParamAsNumber) {
|
currentPage = pageParamAsNumber;
|
||||||
currentPage = pageParamAsNumber;
|
}
|
||||||
}
|
}
|
||||||
}
|
</script>
|
||||||
</script>
|
|
||||||
|
<section
|
||||||
<section class={twMerge('mt-8 flex justify-center gap-5', $$restProps['class'])}>
|
class={twMerge("mt-8 flex justify-center gap-5", $$restProps["class"])}
|
||||||
{#if currentPage > 1}
|
>
|
||||||
<a
|
{#if currentPage > 1}
|
||||||
on:click={() =>
|
<a
|
||||||
setTimeout(() => {
|
on:click={() =>
|
||||||
currentPage = currentPage - 1;
|
setTimeout(() => {
|
||||||
}, 10)}
|
currentPage = currentPage - 1;
|
||||||
href={`?page=${currentPage - 1}`}
|
}, 10)}
|
||||||
>
|
href={`?page=${currentPage - 1}`}
|
||||||
<Button>Forrige</Button>
|
>
|
||||||
</a>
|
<Button>Forrige</Button>
|
||||||
{/if}
|
</a>
|
||||||
{#if (currentPage + 1) * 10 - 10 < maxItems}
|
{/if}
|
||||||
<a
|
{#if (currentPage + 1) * 10 - 10 < maxItems}
|
||||||
on:click={() =>
|
<a
|
||||||
setTimeout(() => {
|
on:click={() =>
|
||||||
currentPage = currentPage + 1;
|
setTimeout(() => {
|
||||||
}, 10)}
|
currentPage = currentPage + 1;
|
||||||
href={`?page=${currentPage + 1}`}
|
}, 10)}
|
||||||
>
|
href={`?page=${currentPage + 1}`}
|
||||||
<Button>Neste</Button>
|
>
|
||||||
</a>
|
<Button>Neste</Button>
|
||||||
{/if}
|
</a>
|
||||||
</section>
|
{/if}
|
||||||
|
</section>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
import { onMount, type Component, type Snippet } from "svelte";
|
import { onMount, type Component, type Snippet } from "svelte";
|
||||||
|
|
||||||
type RouteInfo = {
|
type RouteInfo = {
|
||||||
|
path: string;
|
||||||
pattern: RegExp;
|
pattern: RegExp;
|
||||||
paramNames: string[];
|
paramNames: string[];
|
||||||
component: Component<{}>;
|
component: Component<{}>;
|
||||||
@@ -44,7 +45,16 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
function normalizePath(path: string): string {
|
function normalizePath(path: string): string {
|
||||||
return path.replace(/\/+$/, "");
|
return path.replace(/\/+$/, "").trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function createRoutePattern(path: string): string {
|
||||||
|
const normalizedPath = normalizePath(path);
|
||||||
|
const escapedPath = normalizedPath.replace(/[.*+?^${}()|\\/]/g, "\\$&");
|
||||||
|
const pathPattern = escapedPath.replace(/\[([^\]]+)\]/g, "([^/]+)");
|
||||||
|
const regexPattern = `^${pathPattern}\\/?$`;
|
||||||
|
|
||||||
|
return regexPattern;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -55,17 +65,13 @@
|
|||||||
console.warn(`Route already registered for path: '${path}'.`);
|
console.warn(`Route already registered for path: '${path}'.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const params = Array.from(
|
const params = Array.from(path.matchAll(/\[([^\]]+)\]/g)).map(
|
||||||
path.matchAll(/\[([^\]]+)\]/g).map((x) => x[1]),
|
(x) => x[1],
|
||||||
);
|
);
|
||||||
|
|
||||||
const normalizedPath = normalizePath(path);
|
|
||||||
const escapedPath = normalizedPath.replace(/[.*+?^${}()|\\/]/g, "\\$&");
|
|
||||||
const pathPattern = escapedPath.replace(/\[([^\]]+)\]/g, "([^/]+)");
|
|
||||||
const regexPattern = `^${pathPattern}\\/?$`;
|
|
||||||
|
|
||||||
routes.set(path, {
|
routes.set(path, {
|
||||||
pattern: new RegExp(regexPattern),
|
pattern: new RegExp(createRoutePattern(path)),
|
||||||
|
path: path,
|
||||||
paramNames: params,
|
paramNames: params,
|
||||||
component: component,
|
component: component,
|
||||||
});
|
});
|
||||||
@@ -75,6 +81,19 @@
|
|||||||
export class MissingRouteError extends RouteError {}
|
export class MissingRouteError extends RouteError {}
|
||||||
export class RouteFormatError extends RouteError {}
|
export class RouteFormatError extends RouteError {}
|
||||||
|
|
||||||
|
export function isCurrentPath(path: string): boolean {
|
||||||
|
if (!currentRoute) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const match = path.match(currentRoute.pattern);
|
||||||
|
if (!match) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the value of a route parameter.
|
* Gets the value of a route parameter.
|
||||||
*/
|
*/
|
||||||
@@ -178,8 +197,9 @@
|
|||||||
function makeProxy(target: any) {
|
function makeProxy(target: any) {
|
||||||
return new Proxy(target, {
|
return new Proxy(target, {
|
||||||
apply: (target, thisArg, argArray) => {
|
apply: (target, thisArg, argArray) => {
|
||||||
target.apply(thisArg, argArray);
|
const result = target.apply(thisArg, argArray);
|
||||||
refresh();
|
refresh();
|
||||||
|
return result;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -197,19 +217,17 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (e.target instanceof HTMLAnchorElement) {
|
const anchor = (e.target as Element)?.closest("a");
|
||||||
if (e.target.target === "_blank") {
|
if (!(anchor instanceof HTMLAnchorElement)) return;
|
||||||
return;
|
if (anchor.target === "_blank") return;
|
||||||
}
|
|
||||||
|
|
||||||
const targetUrl = new URL(e.target.href, document.baseURI);
|
const targetUrl = new URL(anchor.href, document.baseURI);
|
||||||
const documentUrl = new URL(document.baseURI);
|
const documentUrl = new URL(document.baseURI);
|
||||||
|
|
||||||
if (targetUrl.origin == documentUrl.origin) {
|
if (targetUrl.origin === documentUrl.origin) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
history.pushState({}, "", targetUrl);
|
history.pushState({}, "", targetUrl);
|
||||||
currentUrl = targetUrl;
|
currentUrl = targetUrl;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,58 +1,68 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { toast, toasts } from '$lib/components/toast/toast';
|
import { toast, toasts } from "./toast";
|
||||||
import { fade } from 'svelte/transition';
|
import { fade } from "svelte/transition";
|
||||||
import InfoIcon from '../icons/severity/InfoIcon.svelte';
|
import InfoIcon from "../icons/severity/InfoIcon.svelte";
|
||||||
import WarningIcon from '../icons/severity/WarningIcon.svelte';
|
import WarningIcon from "../icons/severity/WarningIcon.svelte";
|
||||||
import ErrorIcon from '../icons/severity/ErrorIcon.svelte';
|
import ErrorIcon from "../icons/severity/ErrorIcon.svelte";
|
||||||
import SuccessIcon from '../icons/severity/SuccessIcon.svelte';
|
import SuccessIcon from "../icons/severity/SuccessIcon.svelte";
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if $toasts.length >= 1}
|
{#if $toasts.length >= 1}
|
||||||
<div class="fixed bottom-0 right-0 z-50 flex w-full flex-col gap-2 p-4 text-white sm:w-96">
|
<div
|
||||||
{#each $toasts as toastItem}
|
class="fixed bottom-0 right-0 z-50 flex w-full flex-col gap-2 p-4 text-white sm:w-96"
|
||||||
<div
|
>
|
||||||
class:bg-red-500={toastItem.type === 'error'}
|
{#each $toasts as toastItem}
|
||||||
class:bg-green-500={toastItem.type === 'success'}
|
<div
|
||||||
class:bg-orange-500={toastItem.type === 'warning'}
|
class:bg-red-500={toastItem.type === "error"}
|
||||||
class:bg-blue-500={toastItem.type === 'info'}
|
class:bg-green-500={toastItem.type === "success"}
|
||||||
class="card flex items-center gap-3 border-contrast-900 px-4 py-3 shadow-md dark:border-contrast-100"
|
class:bg-orange-500={toastItem.type === "warning"}
|
||||||
transition:fade={{ duration: 200 }}
|
class:bg-blue-500={toastItem.type === "info"}
|
||||||
>
|
class="card flex items-center gap-3 border-contrast-900 px-4 py-3 shadow-md dark:border-contrast-100"
|
||||||
<div
|
transition:fade={{ duration: 200 }}
|
||||||
class:text-red-900={toastItem.type === 'error'}
|
>
|
||||||
class:text-green-900={toastItem.type === 'success'}
|
<div
|
||||||
class:text-orange-900={toastItem.type === 'warning'}
|
class:text-red-900={toastItem.type === "error"}
|
||||||
class:text-blue-900={toastItem.type === 'info'}
|
class:text-green-900={toastItem.type === "success"}
|
||||||
class="shrink-0"
|
class:text-orange-900={toastItem.type === "warning"}
|
||||||
>
|
class:text-blue-900={toastItem.type === "info"}
|
||||||
{#if toastItem.type === 'info'}
|
class="shrink-0"
|
||||||
<InfoIcon class="w-7"></InfoIcon>
|
>
|
||||||
{:else if toastItem.type === 'warning'}
|
{#if toastItem.type === "info"}
|
||||||
<WarningIcon class="w-7"></WarningIcon>
|
<InfoIcon class="w-7"></InfoIcon>
|
||||||
{:else if toastItem.type === 'error'}
|
{:else if toastItem.type === "warning"}
|
||||||
<ErrorIcon class="w-7"></ErrorIcon>
|
<WarningIcon class="w-7"></WarningIcon>
|
||||||
{:else if toastItem.type === 'success'}
|
{:else if toastItem.type === "error"}
|
||||||
<SuccessIcon class="w-7"></SuccessIcon>
|
<ErrorIcon class="w-7"></ErrorIcon>
|
||||||
{/if}
|
{:else if toastItem.type === "success"}
|
||||||
</div>
|
<SuccessIcon class="w-7"></SuccessIcon>
|
||||||
|
{/if}
|
||||||
<p class="grow">
|
</div>
|
||||||
{toastItem.text}
|
|
||||||
</p>
|
<p class="grow">
|
||||||
|
{toastItem.text}
|
||||||
<button on:click={() => toast.dismiss(toastItem.id)} class="shrink-0">
|
</p>
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
<button
|
||||||
fill="none"
|
on:click={() => toast.dismiss(toastItem.id)}
|
||||||
viewBox="0 0 24 24"
|
class="shrink-0"
|
||||||
stroke-width="1.5"
|
aria-label="dismiss"
|
||||||
stroke="currentColor"
|
>
|
||||||
class="w-6"
|
<svg
|
||||||
>
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
|
fill="none"
|
||||||
</svg>
|
viewBox="0 0 24 24"
|
||||||
</button>
|
stroke-width="1.5"
|
||||||
</div>
|
stroke="currentColor"
|
||||||
{/each}
|
class="w-6"
|
||||||
</div>
|
>
|
||||||
{/if}
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
d="M6 18L18 6M6 6l12 12"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|||||||
99
src/lib/global/routes.ts
Normal file
99
src/lib/global/routes.ts
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
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],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
@@ -1,25 +1,22 @@
|
|||||||
import { writable } from "svelte/store";
|
import { writable } from "svelte/store";
|
||||||
import { browser } from "$app/environment";
|
|
||||||
|
|
||||||
function createThemeToggler() {
|
function createThemeToggler() {
|
||||||
let defaultValue = false;
|
let defaultValue = false;
|
||||||
if (browser) {
|
let savedValue = localStorage.getItem("dark_theme");
|
||||||
let savedValue = localStorage.getItem("dark_theme");
|
|
||||||
|
|
||||||
if (savedValue) {
|
if (savedValue) {
|
||||||
defaultValue = JSON.parse(savedValue);
|
defaultValue = JSON.parse(savedValue);
|
||||||
} else if (
|
} else if (
|
||||||
window.matchMedia &&
|
window.matchMedia &&
|
||||||
window.matchMedia("(prefers-color-scheme: dark)").matches
|
window.matchMedia("(prefers-color-scheme: dark)").matches
|
||||||
) {
|
) {
|
||||||
defaultValue = true;
|
defaultValue = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (defaultValue) {
|
if (defaultValue) {
|
||||||
document.documentElement.classList.add("dark");
|
document.documentElement.style.colorScheme = "dark";
|
||||||
} else {
|
} else {
|
||||||
document.documentElement.classList.remove("dark");
|
document.documentElement.style.colorScheme = "light";
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const { subscribe, update } = writable(defaultValue);
|
const { subscribe, update } = writable(defaultValue);
|
||||||
@@ -28,15 +25,13 @@ function createThemeToggler() {
|
|||||||
subscribe,
|
subscribe,
|
||||||
set: (value: boolean) => {
|
set: (value: boolean) => {
|
||||||
update(() => value);
|
update(() => value);
|
||||||
if (browser) {
|
if (value) {
|
||||||
if (value) {
|
document.documentElement.style.colorScheme = "dark";
|
||||||
document.documentElement.classList.add("dark");
|
} else {
|
||||||
} else {
|
document.documentElement.style.colorScheme = "light";
|
||||||
document.documentElement.classList.remove("dark");
|
|
||||||
}
|
|
||||||
|
|
||||||
localStorage.setItem("dark_theme", JSON.stringify(value));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
localStorage.setItem("dark_theme", JSON.stringify(value));
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
40
src/pages/AccessibilityPage.svelte
Normal file
40
src/pages/AccessibilityPage.svelte
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import singleRampLeftClosed from "../assets/images/single_ramp_left_closed.jpg";
|
||||||
|
import singleRampRightClosed from "../assets/images/single_ramp_right_closed.jpg";
|
||||||
|
import threeRampsOpen from "../assets/images/three_ramps_open.jpg";
|
||||||
|
import Carousel from "../lib/components/carousel/Carousel.svelte";
|
||||||
|
|
||||||
|
let rampCarouselItems: string[] = [
|
||||||
|
singleRampLeftClosed,
|
||||||
|
singleRampRightClosed,
|
||||||
|
threeRampsOpen,
|
||||||
|
];
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<h1 class="text-xl font-semibold md:text-2xl">Tilgjengelighetsfunksjoner</h1>
|
||||||
|
|
||||||
|
<section
|
||||||
|
class="mt-4 justify-between gap-5 rounded-md bg-primary-300 p-4 shadow-inner md:flex"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<h2 class="grow text-lg font-semibold md:text-xl">
|
||||||
|
Sitter du i rullestol?
|
||||||
|
</h2>
|
||||||
|
<p class="md:text-lg">
|
||||||
|
Vi har flere busser med rullestolstøtte, med kapasitet opp til 2
|
||||||
|
rullestoler per buss.
|
||||||
|
<br />Total kapasitet er 5 rullestilbrukere fordelt på 3 busser
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="md:w-96">
|
||||||
|
<Carousel
|
||||||
|
items={rampCarouselItems}
|
||||||
|
buttonClass="bg-primary-200"
|
||||||
|
let:item
|
||||||
|
>
|
||||||
|
<div class="h-full w-96 overflow-hidden shadow-sm">
|
||||||
|
<img class="card w-full object-cover" src={item} alt="" />
|
||||||
|
</div>
|
||||||
|
</Carousel>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
81
src/pages/BusPage.svelte
Normal file
81
src/pages/BusPage.svelte
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import threeBusses from "../assets/images/three_busses.jpg";
|
||||||
|
import twoBusses from "../assets/images/two_busses.jpg";
|
||||||
|
import fiveBusses from "../assets/images/five_busses.jpg";
|
||||||
|
import singleRampLeftClosed from "../assets/images/single_ramp_left_closed.jpg";
|
||||||
|
import threeRampsOpen from "../assets/images/three_ramps_open.jpg";
|
||||||
|
import twoTourBuses from "../assets/images/two_tour_busses.jpg";
|
||||||
|
import tourBusSide from "../assets/images/tour_bus_side.jpg";
|
||||||
|
import tourBusSideLogo from "../assets/images/tour_bus_side_logo.jpg";
|
||||||
|
import Carousel from "../lib/components/carousel/Carousel.svelte";
|
||||||
|
|
||||||
|
let minibusCarouselItems: string[] = [
|
||||||
|
threeBusses,
|
||||||
|
twoBusses,
|
||||||
|
fiveBusses,
|
||||||
|
singleRampLeftClosed,
|
||||||
|
threeRampsOpen,
|
||||||
|
];
|
||||||
|
|
||||||
|
let tourBusCarouselItems: string[] = [
|
||||||
|
twoTourBuses,
|
||||||
|
tourBusSide,
|
||||||
|
tourBusSideLogo,
|
||||||
|
];
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<h1 class="text-xl font-semibold md:text-2xl">Buss</h1>
|
||||||
|
|
||||||
|
<section
|
||||||
|
id="minibus"
|
||||||
|
class="mt-4 justify-between gap-5 rounded-md bg-primary-300 p-4 shadow-inner md:flex"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<h2 class="grow text-lg font-semibold md:text-xl">Minibusser</h2>
|
||||||
|
<p class="md:text-lg">
|
||||||
|
Vi har idag fire minibusser, to Mercedes Sprinter og to Ford
|
||||||
|
Transit. Vi har også en fullelektrisk Mercedes EQV og en Mitsubishi
|
||||||
|
Outlander.
|
||||||
|
<br />Vi finner løsninger for grupper fra 5 - 32 passasjerer med
|
||||||
|
støtte for inntil 3 rullestolbrukere samtidig
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="md:w-96">
|
||||||
|
<Carousel
|
||||||
|
items={minibusCarouselItems}
|
||||||
|
buttonClass="bg-primary-200"
|
||||||
|
let:item
|
||||||
|
>
|
||||||
|
<div class="w-96 overflow-hidden shadow-sm">
|
||||||
|
<img class="card w-full object-cover" src={item} alt="" />
|
||||||
|
</div>
|
||||||
|
</Carousel>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section
|
||||||
|
id="tour"
|
||||||
|
class="mt-12 justify-between gap-5 rounded-md bg-primary-300 p-4 shadow-inner md:flex"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<h2 class="grow text-lg font-semibold md:text-xl">Turbusser</h2>
|
||||||
|
<p class="md:text-lg">
|
||||||
|
Vi er medeier i Storfjord Turbuss AS som har tre turbusser.
|
||||||
|
<br />Storfjord Turbuss AS ble etablert i 2018, og vi utfører
|
||||||
|
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
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="md:w-96">
|
||||||
|
<Carousel
|
||||||
|
items={tourBusCarouselItems}
|
||||||
|
buttonClass="bg-primary-200"
|
||||||
|
let:item
|
||||||
|
>
|
||||||
|
<div class="w-96 overflow-hidden shadow-sm">
|
||||||
|
<img class="card w-full object-cover" src={item} alt="" />
|
||||||
|
</div>
|
||||||
|
</Carousel>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
6
src/pages/ContactPage.svelte
Normal file
6
src/pages/ContactPage.svelte
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<script>
|
||||||
|
import ContactForm from "../lib/components/ContactForm.svelte";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<h1 class="text-xl font-semibold md:text-2xl">Kontakt oss</h1>
|
||||||
|
<ContactForm />
|
||||||
@@ -1 +1,120 @@
|
|||||||
Home
|
<script lang="ts">
|
||||||
|
import header_image from "../assets/images/header_image.jpg";
|
||||||
|
import Carousel from "../lib/components/carousel/Carousel.svelte";
|
||||||
|
import ContactForm from "../lib/components/ContactForm.svelte";
|
||||||
|
import threeBusses from "../assets/images/three_busses.jpg";
|
||||||
|
import taxis from "../assets/images/taxis.jpg";
|
||||||
|
import wheelchairRamps from "../assets/images/three_ramps_open.jpg";
|
||||||
|
import twoTourBusses from "../assets/images/two_tour_busses.jpg";
|
||||||
|
import van from "../assets/images/van.webp";
|
||||||
|
import Link from "../lib/components/link/Link.svelte";
|
||||||
|
import { Routes } from "../lib/global/routes";
|
||||||
|
|
||||||
|
type CardCarouselItem = {
|
||||||
|
header: string;
|
||||||
|
text: string;
|
||||||
|
url: string;
|
||||||
|
image: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
let carouselItems: CardCarouselItem[] = [
|
||||||
|
{
|
||||||
|
image: threeBusses,
|
||||||
|
header: "Minibusstransport",
|
||||||
|
text: "Vi har 4 minibusser i ulike størrelser",
|
||||||
|
url: `${Routes.bus.url}#minibus`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
image: taxis,
|
||||||
|
header: "Taxikjøring",
|
||||||
|
text: "Vi kjører taxi med kapasitet opptil 8 personer",
|
||||||
|
url: `${Routes.taxi.url}#person`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
image: wheelchairRamps,
|
||||||
|
header: "Rullestoltransport",
|
||||||
|
text: "Fire av våre biler er rullestoltilpasset med plass til inntil 3 rullestoler",
|
||||||
|
url: `${Routes.accessibility.url}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
image: twoTourBusses,
|
||||||
|
header: "Turbuss",
|
||||||
|
text: "Vi er medeier i Storfjord Turbuss AS, som har tre moderne turbusser for inntil 50 personer",
|
||||||
|
url: `${Routes.bus.url}#tour`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
image: van,
|
||||||
|
header: "Pakketaxi",
|
||||||
|
text: "Vi har tilbud om pakketaxi. Både småpakker og volumkrevende gods kan transporteres",
|
||||||
|
url: `${Routes.taxi.url}#package`,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="flex gap-8">
|
||||||
|
<div class="overflow-hidden">
|
||||||
|
<div>
|
||||||
|
<img
|
||||||
|
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">
|
||||||
|
Med komfort og sikkerhet i førersetet<br />Bli med på en trygg
|
||||||
|
og behagelig tur
|
||||||
|
</h1>
|
||||||
|
<p class="mt-2 font-medium opacity-70 md:text-lg">
|
||||||
|
Minibusservice Stene AS driver personbefordring med taxi,
|
||||||
|
minibuss og busser.
|
||||||
|
<br />Vi har lang erfaring og finner gode løsninger for kundene
|
||||||
|
våre.
|
||||||
|
<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 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>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-12">
|
||||||
|
<h1 class="text-xl font-semibold md:text-2xl">Våre tjenester</h1>
|
||||||
|
|
||||||
|
<Carousel
|
||||||
|
class="mt-2 rounded-md border-2 border-contrast-100 bg-primary-300 p-4 shadow-inner"
|
||||||
|
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}
|
||||||
|
<img
|
||||||
|
class="h-44 w-full border-b-2 border-contrast-100 object-cover"
|
||||||
|
src={item.image}
|
||||||
|
alt=""
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
<div class="flex grow flex-col p-4">
|
||||||
|
<h3 class="font-semibold">
|
||||||
|
{item.header}
|
||||||
|
</h3>
|
||||||
|
<p class="grow whitespace-normal">
|
||||||
|
{item.text}
|
||||||
|
</p>
|
||||||
|
<Link
|
||||||
|
class="hi mt-2 w-fit font-semibold"
|
||||||
|
href={item.url}>Les mer...</Link
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Carousel>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-12">
|
||||||
|
<h1 class="text-xl font-semibold md:text-2xl">Kontakt oss</h1>
|
||||||
|
<ContactForm />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|||||||
60
src/pages/TaxiPage.svelte
Normal file
60
src/pages/TaxiPage.svelte
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import taxis from "../assets/images/taxis.jpg";
|
||||||
|
import van from "../assets/images/van.webp";
|
||||||
|
import Carousel from "../lib/components/carousel/Carousel.svelte";
|
||||||
|
|
||||||
|
let taxiCarouselItems: string[] = [taxis];
|
||||||
|
let packageTaxiCarouselItems: string[] = [van];
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<h1 class="text-xl font-semibold md:text-2xl">Taxi</h1>
|
||||||
|
|
||||||
|
<section
|
||||||
|
id="person"
|
||||||
|
class="mt-4 justify-between gap-5 rounded-md bg-primary-300 p-4 shadow-inner md:flex"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<h2 class="grow text-lg font-semibold md:text-xl">Persontaxi</h2>
|
||||||
|
<p class="md:text-lg">
|
||||||
|
Vi har taxier som dekker de fleste behov.
|
||||||
|
<br />Rullestoltransport, firehjulstrekk og helelektrisk bil er noen
|
||||||
|
av tilbudene våre.
|
||||||
|
<br />Taxiene våre kan ta opp til 8 passasjerer
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="md:w-96">
|
||||||
|
<Carousel
|
||||||
|
items={taxiCarouselItems}
|
||||||
|
buttonClass="bg-primary-200"
|
||||||
|
let:item
|
||||||
|
>
|
||||||
|
<div class="card w-96 overflow-hidden shadow-sm">
|
||||||
|
<img class="w-full object-cover" src={item} alt="" />
|
||||||
|
</div>
|
||||||
|
</Carousel>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section
|
||||||
|
id="package"
|
||||||
|
class="mt-4 justify-between gap-5 rounded-md bg-primary-300 p-4 shadow-inner md:flex"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<h2 class="grow text-lg font-semibold md:text-xl">Pakketaxi</h2>
|
||||||
|
<p class="md:text-lg">
|
||||||
|
Vi har tilbud om pakketaxi. Både småpakker og volumkrevende gods kan
|
||||||
|
transporteres
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="md:w-96">
|
||||||
|
<Carousel
|
||||||
|
items={packageTaxiCarouselItems}
|
||||||
|
buttonClass="bg-primary-200"
|
||||||
|
let:item
|
||||||
|
>
|
||||||
|
<div class="card w-96 overflow-hidden">
|
||||||
|
<img class="w-full object-cover" src={item} alt="" />
|
||||||
|
</div>
|
||||||
|
</Carousel>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
Reference in New Issue
Block a user