"Add tablet-specific header component and responsive layout updates for improved device compatibility."

This commit is contained in:
Jeppe Bundgaard
2025-12-11 12:35:19 +01:00
parent f71451ad88
commit 94f1528d4a
5 changed files with 268 additions and 17 deletions
+40 -2
View File
@@ -418,11 +418,42 @@ export const SessionUser = {
functions: {
/** Device type checks */
device: {
dimensions: {
// Using breakpoints in accordance with Bulma CSS framework
// https://bulma.io/documentation/start/responsiveness/#breakpoints
mobileMaxWidth: 768,
tabletMinWidth: 769,
tabletMaxWidth: 1023,
desktopMinWidth: 1024,
desktopMaxWidth: 1215,
widescreenMinWidth: 1216,
widescreenMaxWidth: 1407,
fullhdMinWidth: 1408,
// Custom additions
fullhdMaxWidth: 1919,
ultrawideMinWidth: 1920,
ultrawideMaxWidth: 3840,
},
isMobile: () => {
return /Mobi|Android/i.test(navigator.userAgent);
return windowInnerWidth.value < SessionUser.functions.device.dimensions.tabletMinWidth;
//return /Mobi|Android/i.test(navigator.userAgent);
},
isDesktop: () => {
return !/Mobi|Android/i.test(navigator.userAgent);
return windowInnerWidth.value >= SessionUser.functions.device.dimensions.desktopMinWidth;
//return !/Mobi|Android/i.test(navigator.userAgent);
},
isTablet: () => {
return windowInnerWidth.value >= SessionUser.functions.device.dimensions.tabletMinWidth && windowInnerWidth.value <= SessionUser.functions.device.dimensions.tabletMaxWidth;
//return /Tablet|iPad/i.test(navigator.userAgent);
},
isWidescreen: () => {
return windowInnerWidth.value >= SessionUser.functions.device.dimensions.widescreenMinWidth && windowInnerWidth.value <= SessionUser.functions.device.dimensions.widescreenMaxWidth;
},
isFullHD: () => {
return windowInnerWidth.value >= SessionUser.functions.device.dimensions.fullhdMinWidth;
},
isUltraWideScreen: () => {
return windowInnerWidth.value >= SessionUser.functions.device.dimensions.ultrawideMinWidth;
},
},
/** Check if the user has a token */
@@ -705,6 +736,13 @@ export const SessionUser = {
// Check if this is the latest version of the PWA
}
};
const windowInnerWidth = ref(window.innerWidth);
const updateWindowInnerWidth = () => {
windowInnerWidth.value = window.innerWidth;
};
window.addEventListener('resize', updateWindowInnerWidth);
export default SessionUser;
</script>
+3 -2
View File
@@ -5,6 +5,7 @@ import ViewportContent from "@/components/viewport/page/content/ViewportContent.
import { backgroundColor } from "./page/headers/ViewportHeaderSettings.vue";
import DesktopNavigationBuefy from "@/components/viewport/page/headers/DesktopNavigationBuefy.vue";
import { ref, onMounted, computed } from "vue";
import SessionUser from "@/components/session/token/SessionUser.vue";
console.log("Viewport component loaded");
const headerHeight = ref(60); // Default header height in pixels
@@ -35,12 +36,12 @@ onMounted(() => {
<div class="fixed-grid has-12-cols">
<!-- Navigation and content wrapper -->
<div class="grid">
<div class="cell is-col-span-2-desktop is-hidden-mobile">
<div class="cell is-col-span-2-desktop is-hidden-mobile is-col-span-3-tablet" :class="{'is-col-span-1-widescreen': SessionUser.functions.device.isUltraWideScreen()}">
<div class="independent-scroll slick-scroll-layout" :style="{ maxHeight: independentScrollMaxHeight, minHeight: independentScrollMaxHeight }">
<DesktopNavigationBuefy />
</div>
</div>
<div class="cell is-col-span-10-desktop is-col-span-12-mobile">
<div class="cell is-col-span-10-desktop is-col-span-12-mobile is-col-span-9-tablet" :class="{'is-col-span-11-widescreen': SessionUser.functions.device.isUltraWideScreen()}">
<div class="independent-scroll slick-scroll-layout" :style="{ maxHeight: independentScrollMaxHeight }">
<ViewportContent>
<slot />
@@ -0,0 +1,152 @@
<script setup lang="ts">
import { toggleExpanded, isOpen, getDepartmentSelectionVisibility } from '@/components/viewport/page/headers/ViewportHeaderSettings.vue';
import NavigationMenuDepartment from "@/components/viewport/page/headers/menu/NavigationMenuDepartment.vue";
import NavigationMenuGlobalSearch from "@/components/viewport/page/headers/menu/NavigationMenuGlobalSearch.vue";
import { IS_DEV } from '@/main';
import SessionUser from "@/components/session/token/SessionUser.vue";
import { useRoute } from "vue-router";
const route = useRoute();
</script>
<template>
<!-- Company Logo | Spacer | Burger Menu -->
<!-- The burger menu is used to toggle the mobile navigation menu -->
<nav class="navbar is-white is-mobile is-fixed-top mobile-header-section" role="navigation" aria-label="main navigation" style="z-index: 1000;">
<div class="navbar-brand">
<a class="navbar-item is-align-content-center" href="/">
<img src="@/assets/branding/truckwash-banner-white-compressed.png"
alt="Truckwash Logo"
:class="{'transform-color-black': !IS_DEV, 'transform-color-red': IS_DEV}"
>
</a>
</div>
<div class="navbar-start">
<!-- Global search -->
<div class="navbar-item" style="width: 100%;" v-show="false">
<NavigationMenuGlobalSearch style="width: 100%;"/>
</div>
<!-- Department selection -->
<div class="navbar-item" v-show="getDepartmentSelectionVisibility()">
<NavigationMenuDepartment/>
</div>
</div>
<!-- Spacer to push user account to the right -->
<div class="navbar-item" style="flex-grow: 1;"></div>
<div class="navbar-end">
<!-- User account name and avatar (Desktop) -->
<div class="navbar-item mr-1">
<div class="buttons">
<!-- Back-Office button -->
<router-link class="button is-white" to="/superuser" v-if="SessionUser.canAccessSuperUser() && !route.path.includes('/superuser')">
<span class="icon">
<i class="fas fa-tools" :class="{'transform-color-black': !IS_DEV, 'transform-color-red': IS_DEV}"></i>
</span>
<span>Backoffice</span>
</router-link>
<!-- Department button -->
<router-link class="button is-white" to="/admin" v-if="SessionUser.canAccessAdmin() && !route.path.includes('/admin')">
<span class="icon">
<i class="fas fa-building" :class="{'transform-color-black': !IS_DEV, 'transform-color-red': IS_DEV}"></i>
</span>
<span>Afdelinger</span>
</router-link>
<!-- Return to own account if impersonating -->
<template v-if="SessionUser.hasSuperUserToken()">
<router-link class="button is-white" to="/auth/return">
<span class="icon">
<i class="fas fa-undo-alt" :class="{'transform-color-black': !IS_DEV, 'transform-color-red': IS_DEV}"></i>
</span>
<span>Tilbage til egen konto</span>
</router-link>
</template>
<router-link class="button is-white" to="/user/profile" v-if="SessionUser.authenticated.value">
<span class="icon">
<i class="fas fa-user-circle" :class="{'transform-color-black': !IS_DEV, 'transform-color-red': IS_DEV}"></i>
</span>
<span :class="{'has-text-black': !IS_DEV, 'has-text-red': IS_DEV}">{{ SessionUser.authenticated.value ? SessionUser.getName() : ''}}</span>
</router-link>
<template v-else>
<router-link class="button is-white" to="/register">
<span class="icon">
<i class="fas fa-user-plus" :class="{'transform-color-black': !IS_DEV, 'transform-color-red': IS_DEV}"></i>
</span>
<span :class="{'has-text-black': !IS_DEV, 'has-text-red': IS_DEV}">Opret bruger</span>
</router-link>
<router-link class="button is-white" to="/login">
<span class="icon">
<i class="fas fa-sign-in-alt" :class="{'transform-color-black': !IS_DEV, 'transform-color-red': IS_DEV}"></i>
</span>
<span :class="{'has-text-black': !IS_DEV, 'has-text-red': IS_DEV}">Log ind</span>
</router-link>
</template>
</div>
</div>
</div>
</nav>
<!-- Spacer (To avoid content being hidden behind the fixed navbar) -->
<div class="navbar-spacer"></div>
</template>
<style scoped>
.navbar-brand {
margin-left: 1rem;
}
.mobile-header-section {
height: 80px;
width: 100%;
display: flex;
align-items: center;
background-color: white;
}
.mobile-navbar-burger {
border: 2px solid #f1f1f1;
background-color: #f1f1f1;
border-radius: 50%;
width: 40px;
height: 40px;
display: flex;
justify-content: center;
align-items: center;
margin-right: 1rem;
}
.navbar-spacer {
height: 80px; /* Same height as the navbar */
min-width: 100%;
min-height: 80px; /* Ensures the spacer is always at least as tall as the navbar */
background-color: transparent; /* Transparent to avoid visual interference */
position: relative; /* Allows for proper stacking context */
}
.transform-color-black {
filter: invert(100%) grayscale(100%) brightness(0) contrast(100%);
}
.transform-color-red {
/* Invert colors and apply a red tint */
filter: invert(27%) sepia(96%) saturate(7493%) hue-rotate(357deg) brightness(103%) contrast(101%);
}
.transform-color-warning {
/* Invert colors and apply a yellow tint */
filter: invert(79%) sepia(37%) saturate(7473%) hue-rotate(200deg) brightness(103%) contrast(101%);
}
.navbar-burger {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
width: 40px;
height: 40px;
background-color: #f1f1f1; /* Make the burger button background transparent */
}
.navbar-burger-bar {
background-color: #000; /* Color of the burger lines */
margin-top: 4px; /* Space between the lines */
width: 20px; /* Width of the burger lines */
height: 2px; /* Height of the burger lines */
transition: background-color 0.3s ease; /* Smooth transition for color changes */
}
.navbar-burger-bar:nth-child(2) {
/* This is the center line of the burger icon */
width: 16px; /* Make the center line shorter */
background-color: transparent; /* Make it invisible */
}
</style>
@@ -3,6 +3,7 @@ import ViewportResponsiveWrapper from "@/components/viewport/conditions/elements
import MobileHeader from "@/components/viewport/page/headers/MobileHeader.vue";
import DesktopHeader from "@/components/viewport/page/headers/DesktopHeader.vue";
import { isHidden } from "./ViewportHeaderSettings.vue";
import TabletHeader from "@/components/viewport/page/headers/TabletHeader.vue";
</script>
<template>
@@ -13,6 +14,9 @@ import { isHidden } from "./ViewportHeaderSettings.vue";
<template #desktop>
<DesktopHeader />
</template>
<template #tablet>
<TabletHeader />
</template>
</ViewportResponsiveWrapper>
</template>
+69 -13
View File
@@ -35,6 +35,62 @@ const cleanup = () => {
onUnmounted(() => {
cleanup();
});
const isTablet = computed(() => {
return SessionUser.functions.device.isTablet();
});
const windowInnerWidth = ref(window.innerWidth);
const updateWindowInnerWidth = () => {
windowInnerWidth.value = window.innerWidth;
// Trigger any layout updates if necessary
classes.value; // Access to trigger recomputation
};
window.addEventListener('resize', updateWindowInnerWidth);
onUnmounted(() => {
window.removeEventListener('resize', updateWindowInnerWidth);
});
type ClassesType = {
column?: string[];
title?: string[];
button?: string[];
};
const classes = computed(() => {
if (computed(() => SessionUser.functions.device.isMobile()).value) {
return {
column: ['is-12-mobile', 'is-half-tablet', 'is-half-desktop'],
title: [],
button: [],
} as ClassesType;
} else if (SessionUser.functions.device.isTablet()) {
return {
column: ['is-half-tablet', 'is-12-mobile', 'is-half-desktop'],
title: ['is-size-7', 'has-text-weight-bold'],
button: ['is-size-7-tablet'],
} as ClassesType;
} else if ((SessionUser.functions.device.isDesktop() || SessionUser.functions.device.isWidescreen()) && !SessionUser.functions.device.isUltraWideScreen()) {
return {
column: ['is-half-desktop', 'is-half-tablet', 'is-12-mobile'],
title: [],
button: [],
} as ClassesType;
} else if (SessionUser.functions.device.isUltraWideScreen()) {
return {
column: ['is-one-third-desktop', 'is-half-tablet', 'is-12-mobile'],
title: [],
button: [],
} as ClassesType;
} else {
return {
// Default to desktop layout
column: ['is-half'],
title: [],
button: [],
} as ClassesType;
}
});
</script>
<template>
@@ -45,7 +101,7 @@ onUnmounted(() => {
Velkommen {{ SessionUser.getName() }}
</span>
</div>
<div class="column is-half-desktop is-hidden-desktop">
<div class="column is-half-desktop is-hidden-desktop is-12-tablet">
<!-- Start wash card -->
<WhiteBoxCard :force-state="true" :defaultOpen="true" :toggleable="false" :loading="false">
<template #header>
@@ -70,11 +126,11 @@ onUnmounted(() => {
</template>
</WhiteBoxCard>
</div>
<div class="column is-half-desktop">
<div class="column" :class="classes.column">
<!-- Book wash card -->
<WhiteBoxCard :force-state="true" :defaultOpen="true" :toggleable="false" :loading="false">
<template #header>
<div class="card-header-title">
<div class="card-header-title" :class="classes.title">
Book vask
</div>
<div class="card-header-icon">
@@ -89,18 +145,18 @@ onUnmounted(() => {
</template>
<template #footer>
<div class="card-footer-item">
<router-link to="/user/bookings/book" class="button is-link is-fullwidth">
<router-link to="/user/bookings/book" class="button is-link is-fullwidth" :class="classes.button">
Book vask
</router-link>
</div>
</template>
</WhiteBoxCard>
</div>
<div class="column is-half-desktop">
<div class="column" :class="classes.column">
<!-- Add vehicle card -->
<WhiteBoxCard :force-state="true" :defaultOpen="true" :toggleable="false" :loading="false">
<template #header>
<div class="card-header-title">
<div class="card-header-title" :class="classes.title">
Opret køretøj
</div>
<div class="card-header-icon">
@@ -115,18 +171,18 @@ onUnmounted(() => {
</template>
<template #footer>
<div class="card-footer-item">
<router-link to="/user/vehicles" class="button is-link is-fullwidth">
<router-link to="/user/vehicles" class="button is-link is-fullwidth" :class="classes.button">
Opret køretøj
</router-link>
</div>
</template>
</WhiteBoxCard>
</div>
<div class="column is-half-desktop">
<div class="column" :class="classes.column">
<!-- Download wash certificate card -->
<WhiteBoxCard :force-state="true" :defaultOpen="true" :toggleable="false" :loading="false">
<template #header>
<div class="card-header-title">
<div class="card-header-title" :class="classes.title">
Download vaskecertifikater
</div>
<div class="card-header-icon">
@@ -141,18 +197,18 @@ onUnmounted(() => {
</template>
<template #footer>
<div class="card-footer-item">
<router-link to="/user/wash-certificates" class="button is-link is-fullwidth" disabled="true">
<router-link to="/user/wash-certificates" class="button is-link is-fullwidth" :class="classes.button" disabled="true">
Download vaskecertifikater
</router-link>
</div>
</template>
</WhiteBoxCard>
</div>
<div class="column is-half-desktop">
<div class="column" :class="classes.column">
<!-- View invoices card -->
<WhiteBoxCard :force-state="true" :defaultOpen="true" :toggleable="false" :loading="false">
<template #header>
<div class="card-header-title">
<div class="card-header-title" :class="classes.title">
Download fakturaer
</div>
<div class="card-header-icon">
@@ -167,7 +223,7 @@ onUnmounted(() => {
</template>
<template #footer>
<div class="card-footer-item">
<router-link to="/user/invoices" class="button is-link is-fullwidth">
<router-link to="/user/invoices" class="button is-link is-fullwidth" :class="classes.button">
Download fakturaer
</router-link>
</div>