pages
This commit is contained in:
12
package-lock.json
generated
12
package-lock.json
generated
@@ -7,6 +7,10 @@
|
||||
"": {
|
||||
"name": "minibusservice.no",
|
||||
"version": "0.0.0",
|
||||
"dependencies": {
|
||||
"leaflet": "^1.9.4",
|
||||
"tailwind-merge": "^3.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sveltejs/vite-plugin-svelte": "^7.0.0",
|
||||
"@tailwindcss/vite": "^0.0.0-insiders.aaaefe8",
|
||||
@@ -14,7 +18,6 @@
|
||||
"@types/node": "^24.12.0",
|
||||
"svelte": "^5.53.7",
|
||||
"svelte-check": "^4.4.5",
|
||||
"tailwind-merge": "^3.5.0",
|
||||
"tailwindcss": "^0.0.0-insiders.aaaefe8",
|
||||
"typescript": "~5.9.3",
|
||||
"vite": "^8.0.0"
|
||||
@@ -1200,6 +1203,12 @@
|
||||
"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": {
|
||||
"version": "1.32.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz",
|
||||
@@ -1694,7 +1703,6 @@
|
||||
"version": "3.5.0",
|
||||
"resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.5.0.tgz",
|
||||
"integrity": "sha512-I8K9wewnVDkL1NTGoqWmVEIlUcB9gFriAEkXkfCjX5ib8ezGxtR3xD7iZIxrfArjEsH7F1CHD4RFUtxefdqV/A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
|
||||
@@ -16,9 +16,12 @@
|
||||
"@types/node": "^24.12.0",
|
||||
"svelte": "^5.53.7",
|
||||
"svelte-check": "^4.4.5",
|
||||
"tailwind-merge": "^3.5.0",
|
||||
"tailwindcss": "^0.0.0-insiders.aaaefe8",
|
||||
"typescript": "~5.9.3",
|
||||
"vite": "^8.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"leaflet": "^1.9.4",
|
||||
"tailwind-merge": "^3.5.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,40 @@
|
||||
<script lang="ts">
|
||||
import Router, { route } from "./lib/components/Router.svelte";
|
||||
import "./app.css";
|
||||
|
||||
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("/taxi", TaxiPage);
|
||||
route("/bus", BusPage);
|
||||
route("/contact", ContactPage);
|
||||
route("/accessibility", AccessibilityPage);
|
||||
</script>
|
||||
|
||||
<Router>
|
||||
{#snippet notfound()}
|
||||
<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 class="flex min-h-screen flex-col bg-primary-200 text-contrast-900">
|
||||
<Navigation>
|
||||
<main class="grow px-4 pb-16 pt-8">
|
||||
<div class="mx-auto max-w-7xl">
|
||||
<Router>
|
||||
{#snippet notfound()}
|
||||
<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">
|
||||
import type { MessageResponseBodyData } from '$lib/types/dtos/message/MessageResponseBodyData';
|
||||
import type { MinibusserviceResponseBody } from '$lib/types/dtos/MinibusserviceResponseBody';
|
||||
import Button from './button/Button.svelte';
|
||||
import Link from './link/Link.svelte';
|
||||
import MapPoint from './MapPoint.svelte';
|
||||
import { toast } from './toast/toast';
|
||||
|
||||
let buttonDisabled = false;
|
||||
|
||||
let name = '';
|
||||
let email = '';
|
||||
let phone = '';
|
||||
let message = '';
|
||||
let posting = false;
|
||||
|
||||
async function sendMessage() {
|
||||
if (!(name && email && message)) {
|
||||
toast.warning('Alle påkrevde felt må fyllest inn');
|
||||
return;
|
||||
}
|
||||
|
||||
posting = true;
|
||||
buttonDisabled = true;
|
||||
|
||||
try {
|
||||
let res = await fetch('/api/v1/message', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
name,
|
||||
email,
|
||||
phone,
|
||||
message
|
||||
})
|
||||
});
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
import Link from "./link/Link.svelte";
|
||||
import MapPoint from "./MapPoint.svelte";
|
||||
</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>
|
||||
<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>
|
||||
<MapPoint
|
||||
class="card mt-5 min-h-96 grow overflow-hidden rounded-md border-2"
|
||||
coordinates={[
|
||||
{
|
||||
latitude: 62.48303957042255,
|
||||
longitude: 6.8108274964451745,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,79 +1,86 @@
|
||||
<script lang="ts">
|
||||
import { onDestroy, onMount } from 'svelte';
|
||||
import 'leaflet/dist/leaflet.css';
|
||||
import { env } from '$env/dynamic/public';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
import { onDestroy, onMount } from "svelte";
|
||||
import "leaflet/dist/leaflet.css";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
interface MarkerPoint {
|
||||
latitude: number;
|
||||
longitude: number;
|
||||
tooltip?: string;
|
||||
}
|
||||
interface MarkerPoint {
|
||||
latitude: number;
|
||||
longitude: number;
|
||||
tooltip?: string;
|
||||
}
|
||||
|
||||
export let coordinates: MarkerPoint[];
|
||||
export let coordinates: MarkerPoint[];
|
||||
|
||||
let mapElement: HTMLDivElement;
|
||||
let map: L.Map | null = null;
|
||||
let markers: L.Marker<any>[] = [];
|
||||
var icon: L.Icon;
|
||||
let L: typeof import('leaflet/index');
|
||||
let mapElement: HTMLDivElement;
|
||||
let map: L.Map | null = null;
|
||||
let markers: L.Marker<any>[] = [];
|
||||
var icon: L.Icon;
|
||||
let L: typeof import("leaflet/index");
|
||||
|
||||
onMount(async () => {
|
||||
L = await import('leaflet');
|
||||
icon = L.icon({
|
||||
iconUrl: '/map_marker.png',
|
||||
onMount(async () => {
|
||||
L = await import("leaflet");
|
||||
icon = L.icon({
|
||||
iconUrl: "/map_marker.png",
|
||||
|
||||
iconSize: [32, 32],
|
||||
iconAnchor: [16, 32],
|
||||
popupAnchor: [-3, -76]
|
||||
});
|
||||
iconSize: [32, 32],
|
||||
iconAnchor: [16, 32],
|
||||
popupAnchor: [-3, -76],
|
||||
});
|
||||
|
||||
map = L.map(mapElement);
|
||||
map = L.map(mapElement);
|
||||
|
||||
if (coordinates.length >= 1) {
|
||||
map.setView([coordinates[0].latitude, coordinates[0].longitude], 15);
|
||||
}
|
||||
if (coordinates.length >= 1) {
|
||||
map.setView(
|
||||
[coordinates[0].latitude, coordinates[0].longitude],
|
||||
15,
|
||||
);
|
||||
}
|
||||
|
||||
L.tileLayer(env.PUBLIC_TILE_SERVER_URL, {
|
||||
maxZoom: 18,
|
||||
attribution:
|
||||
'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>',
|
||||
id: 'base'
|
||||
}).addTo(map);
|
||||
L.tileLayer("https://tile.openstreetmap.org/{z}/{x}/{y}.png", {
|
||||
maxZoom: 18,
|
||||
attribution:
|
||||
'© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
|
||||
id: "base",
|
||||
}).addTo(map);
|
||||
|
||||
updateMarkers();
|
||||
});
|
||||
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) {
|
||||
let marker = L.marker([point.latitude, point.longitude], {
|
||||
icon: icon,
|
||||
});
|
||||
|
||||
if (point.tooltip)
|
||||
marker.bindTooltip(point.tooltip, {
|
||||
direction: 'bottom'
|
||||
});
|
||||
if (point.tooltip)
|
||||
marker.bindTooltip(point.tooltip, {
|
||||
direction: "bottom",
|
||||
});
|
||||
|
||||
markers.push(marker);
|
||||
map?.addLayer(marker);
|
||||
}
|
||||
markers.push(marker);
|
||||
map?.addLayer(marker);
|
||||
}
|
||||
|
||||
function updateMarkers() {
|
||||
if (L && map) {
|
||||
markers.forEach((point) => map?.removeLayer(point));
|
||||
markers = [];
|
||||
function updateMarkers() {
|
||||
if (L && map) {
|
||||
markers.forEach((point) => map?.removeLayer(point));
|
||||
markers = [];
|
||||
|
||||
coordinates.forEach(addMarker);
|
||||
coordinates.forEach(addMarker);
|
||||
|
||||
map?.flyToBounds(L.featureGroup(markers).getBounds(), {
|
||||
duration: 1
|
||||
});
|
||||
}
|
||||
}
|
||||
map?.flyToBounds(L.featureGroup(markers).getBounds(), {
|
||||
duration: 1,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
$: coordinates && updateMarkers();
|
||||
$: coordinates && updateMarkers();
|
||||
</script>
|
||||
|
||||
<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>
|
||||
|
||||
@@ -1,41 +1,42 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/stores';
|
||||
import Button from '$lib/components/button/Button.svelte';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
import Button from "./button/Button.svelte";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
export let maxItems: number;
|
||||
export let maxItems: number;
|
||||
|
||||
let pageParam = $page.url.searchParams.get('page');
|
||||
let currentPage = 1;
|
||||
if (pageParam) {
|
||||
let pageParamAsNumber = Number(pageParam);
|
||||
if (pageParamAsNumber) {
|
||||
currentPage = pageParamAsNumber;
|
||||
}
|
||||
}
|
||||
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
|
||||
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>
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { onMount, type Component, type Snippet } from "svelte";
|
||||
|
||||
type RouteInfo = {
|
||||
path: string;
|
||||
pattern: RegExp;
|
||||
paramNames: string[];
|
||||
component: Component<{}>;
|
||||
@@ -44,7 +45,16 @@
|
||||
});
|
||||
|
||||
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}'.`);
|
||||
}
|
||||
|
||||
const params = Array.from(
|
||||
path.matchAll(/\[([^\]]+)\]/g).map((x) => x[1]),
|
||||
const params = Array.from(path.matchAll(/\[([^\]]+)\]/g)).map(
|
||||
(x) => x[1],
|
||||
);
|
||||
|
||||
const normalizedPath = normalizePath(path);
|
||||
const escapedPath = normalizedPath.replace(/[.*+?^${}()|\\/]/g, "\\$&");
|
||||
const pathPattern = escapedPath.replace(/\[([^\]]+)\]/g, "([^/]+)");
|
||||
const regexPattern = `^${pathPattern}\\/?$`;
|
||||
|
||||
routes.set(path, {
|
||||
pattern: new RegExp(regexPattern),
|
||||
pattern: new RegExp(createRoutePattern(path)),
|
||||
path: path,
|
||||
paramNames: params,
|
||||
component: component,
|
||||
});
|
||||
@@ -75,6 +81,19 @@
|
||||
export class MissingRouteError 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.
|
||||
*/
|
||||
@@ -178,8 +197,9 @@
|
||||
function makeProxy(target: any) {
|
||||
return new Proxy(target, {
|
||||
apply: (target, thisArg, argArray) => {
|
||||
target.apply(thisArg, argArray);
|
||||
const result = target.apply(thisArg, argArray);
|
||||
refresh();
|
||||
return result;
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -197,19 +217,17 @@
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.target instanceof HTMLAnchorElement) {
|
||||
if (e.target.target === "_blank") {
|
||||
return;
|
||||
}
|
||||
const anchor = (e.target as Element)?.closest("a");
|
||||
if (!(anchor instanceof HTMLAnchorElement)) return;
|
||||
if (anchor.target === "_blank") return;
|
||||
|
||||
const targetUrl = new URL(e.target.href, document.baseURI);
|
||||
const documentUrl = new URL(document.baseURI);
|
||||
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;
|
||||
}
|
||||
if (targetUrl.origin === documentUrl.origin) {
|
||||
e.preventDefault();
|
||||
history.pushState({}, "", targetUrl);
|
||||
currentUrl = targetUrl;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,58 +1,68 @@
|
||||
<script lang="ts">
|
||||
import { toast, toasts } from '$lib/components/toast/toast';
|
||||
import { fade } from 'svelte/transition';
|
||||
import InfoIcon from '../icons/severity/InfoIcon.svelte';
|
||||
import WarningIcon from '../icons/severity/WarningIcon.svelte';
|
||||
import ErrorIcon from '../icons/severity/ErrorIcon.svelte';
|
||||
import SuccessIcon from '../icons/severity/SuccessIcon.svelte';
|
||||
import { toast, toasts } from "./toast";
|
||||
import { fade } from "svelte/transition";
|
||||
import InfoIcon from "../icons/severity/InfoIcon.svelte";
|
||||
import WarningIcon from "../icons/severity/WarningIcon.svelte";
|
||||
import ErrorIcon from "../icons/severity/ErrorIcon.svelte";
|
||||
import SuccessIcon from "../icons/severity/SuccessIcon.svelte";
|
||||
</script>
|
||||
|
||||
{#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">
|
||||
{#each $toasts as toastItem}
|
||||
<div
|
||||
class:bg-red-500={toastItem.type === 'error'}
|
||||
class:bg-green-500={toastItem.type === 'success'}
|
||||
class:bg-orange-500={toastItem.type === 'warning'}
|
||||
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"
|
||||
transition:fade={{ duration: 200 }}
|
||||
>
|
||||
<div
|
||||
class:text-red-900={toastItem.type === 'error'}
|
||||
class:text-green-900={toastItem.type === 'success'}
|
||||
class:text-orange-900={toastItem.type === 'warning'}
|
||||
class:text-blue-900={toastItem.type === 'info'}
|
||||
class="shrink-0"
|
||||
>
|
||||
{#if toastItem.type === 'info'}
|
||||
<InfoIcon class="w-7"></InfoIcon>
|
||||
{:else if toastItem.type === 'warning'}
|
||||
<WarningIcon class="w-7"></WarningIcon>
|
||||
{:else if toastItem.type === 'error'}
|
||||
<ErrorIcon class="w-7"></ErrorIcon>
|
||||
{:else if toastItem.type === 'success'}
|
||||
<SuccessIcon class="w-7"></SuccessIcon>
|
||||
{/if}
|
||||
</div>
|
||||
<div
|
||||
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}
|
||||
<div
|
||||
class:bg-red-500={toastItem.type === "error"}
|
||||
class:bg-green-500={toastItem.type === "success"}
|
||||
class:bg-orange-500={toastItem.type === "warning"}
|
||||
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"
|
||||
transition:fade={{ duration: 200 }}
|
||||
>
|
||||
<div
|
||||
class:text-red-900={toastItem.type === "error"}
|
||||
class:text-green-900={toastItem.type === "success"}
|
||||
class:text-orange-900={toastItem.type === "warning"}
|
||||
class:text-blue-900={toastItem.type === "info"}
|
||||
class="shrink-0"
|
||||
>
|
||||
{#if toastItem.type === "info"}
|
||||
<InfoIcon class="w-7"></InfoIcon>
|
||||
{:else if toastItem.type === "warning"}
|
||||
<WarningIcon class="w-7"></WarningIcon>
|
||||
{:else if toastItem.type === "error"}
|
||||
<ErrorIcon class="w-7"></ErrorIcon>
|
||||
{:else if toastItem.type === "success"}
|
||||
<SuccessIcon class="w-7"></SuccessIcon>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<p class="grow">
|
||||
{toastItem.text}
|
||||
</p>
|
||||
<p class="grow">
|
||||
{toastItem.text}
|
||||
</p>
|
||||
|
||||
<button on:click={() => toast.dismiss(toastItem.id)} class="shrink-0">
|
||||
<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>
|
||||
</button>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
<button
|
||||
on:click={() => toast.dismiss(toastItem.id)}
|
||||
class="shrink-0"
|
||||
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>
|
||||
</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 { browser } from "$app/environment";
|
||||
|
||||
function createThemeToggler() {
|
||||
let defaultValue = false;
|
||||
if (browser) {
|
||||
let savedValue = localStorage.getItem("dark_theme");
|
||||
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 (savedValue) {
|
||||
defaultValue = JSON.parse(savedValue);
|
||||
} else if (
|
||||
window.matchMedia &&
|
||||
window.matchMedia("(prefers-color-scheme: dark)").matches
|
||||
) {
|
||||
defaultValue = true;
|
||||
}
|
||||
|
||||
if (defaultValue) {
|
||||
document.documentElement.classList.add("dark");
|
||||
} else {
|
||||
document.documentElement.classList.remove("dark");
|
||||
}
|
||||
if (defaultValue) {
|
||||
document.documentElement.style.colorScheme = "dark";
|
||||
} else {
|
||||
document.documentElement.style.colorScheme = "light";
|
||||
}
|
||||
|
||||
const { subscribe, update } = writable(defaultValue);
|
||||
@@ -28,15 +25,13 @@ function createThemeToggler() {
|
||||
subscribe,
|
||||
set: (value: boolean) => {
|
||||
update(() => value);
|
||||
if (browser) {
|
||||
if (value) {
|
||||
document.documentElement.classList.add("dark");
|
||||
} else {
|
||||
document.documentElement.classList.remove("dark");
|
||||
}
|
||||
|
||||
localStorage.setItem("dark_theme", JSON.stringify(value));
|
||||
if (value) {
|
||||
document.documentElement.style.colorScheme = "dark";
|
||||
} else {
|
||||
document.documentElement.style.colorScheme = "light";
|
||||
}
|
||||
|
||||
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