215 lines
7.9 KiB
Vue
215 lines
7.9 KiB
Vue
<script setup>
|
|
import { computed, defineAsyncComponent, onBeforeUnmount, onMounted, watch } from "vue";
|
|
// Set the favicon to the /favicon.ico file
|
|
import { useHead } from "@vueuse/head";
|
|
import favicon from "@/assets/favicon.ico";
|
|
import { useRoute } from "vue-router";
|
|
import { useI18n } from "vue-i18n";
|
|
import { hasStoredSessionToken } from "@/services/sessionStorage.js";
|
|
import BrandedLoadingScreen from "@/components/global/BrandedLoadingScreen.vue";
|
|
import {
|
|
releaseChannelSwitchedStatus,
|
|
releaseChannelUnavailableStatus,
|
|
startReleaseChannelAvailabilityClock,
|
|
stopReleaseChannelAvailabilityClock,
|
|
} from "@/services/releaseChannelAvailability.js";
|
|
|
|
const route = useRoute();
|
|
const { t, te, locale } = useI18n({ useScope: "global" });
|
|
const APP_TITLE = "Truck Wash";
|
|
const pageRouteLoadingProps = {
|
|
title: "Indlæser side...",
|
|
subtitle: "Vi gør siden klar",
|
|
testId: "app-route-loading",
|
|
statusTestId: "app-route-loading-status",
|
|
ariaLabel: "Indlæser side",
|
|
};
|
|
const sessionRouteLoadingProps = {
|
|
title: "Bekræfter bruger...",
|
|
subtitle: "Vi indlæser din session",
|
|
testId: "app-route-loading",
|
|
statusTestId: "app-route-loading-status",
|
|
ariaLabel: "Bekræfter bruger",
|
|
};
|
|
const LayoutV2 = defineAsyncComponent(() => import("@/components/page/wrappers/LayoutV2.vue"));
|
|
const DefaultPageWrapper = defineAsyncComponent(() => import("@/components/page/wrappers/DefaultPageWrapper.vue"));
|
|
const RequestQueueProgress = defineAsyncComponent(() => import("@/components/global/RequestQueueProgress.vue"));
|
|
const ErrorReportLauncher = defineAsyncComponent(() => import("@/components/global/ErrorReportLauncher.vue"));
|
|
const FrontendMaintenanceMenu = defineAsyncComponent(() => import("@/components/global/FrontendMaintenanceMenu.vue"));
|
|
const ReleaseChannelUnavailable = defineAsyncComponent(() =>
|
|
import("@/components/release/ReleaseChannelUnavailable.vue")
|
|
);
|
|
const ReleaseChannelSwitched = defineAsyncComponent(() => import("@/components/release/ReleaseChannelSwitched.vue"));
|
|
|
|
const routeTitleI18nKeys = {
|
|
"about-us": ["header.about_us"],
|
|
"book-a-demo": ["about_us.solutions.operator.book_demo"],
|
|
"customer-creation": ["header.customer_creation"],
|
|
login: ["header.customer_login"],
|
|
subuserlogin: ["header.customer_login"],
|
|
adminlogin: ["header.operator_login"],
|
|
guestHome: ["guest.home.title"],
|
|
PwaDownload: ["guest.pwa.title"],
|
|
"connectivity-issue": ["connectivity.title"],
|
|
selfservestudio: ["admin.self_serve.title"],
|
|
};
|
|
|
|
const toReadableTitle = (value) => {
|
|
if (!value) return "";
|
|
return value
|
|
.replace(/([a-z])([A-Z])/g, "$1 $2")
|
|
.replace(/[-_]+/g, " ")
|
|
.replace(/\s+/g, " ")
|
|
.trim()
|
|
.replace(/\b\w/g, (char) => char.toUpperCase());
|
|
};
|
|
|
|
const resolveRouteTitle = () => {
|
|
const titleKey = route.meta?.titleKey;
|
|
if (typeof titleKey === "string" && te(titleKey)) {
|
|
return t(titleKey);
|
|
}
|
|
|
|
if (typeof route.meta?.title === "string" && route.meta.title.trim()) {
|
|
return route.meta.title.trim();
|
|
}
|
|
|
|
const routeName = typeof route.name === "string" ? route.name : "";
|
|
const knownKeys = routeName ? routeTitleI18nKeys[routeName] || [] : [];
|
|
for (const key of knownKeys) {
|
|
if (te(key)) return t(key);
|
|
}
|
|
|
|
return toReadableTitle(routeName);
|
|
};
|
|
|
|
const updateDocumentTitle = () => {
|
|
const pageTitle = resolveRouteTitle();
|
|
document.title = pageTitle ? `${pageTitle} | ${APP_TITLE}` : APP_TITLE;
|
|
};
|
|
|
|
const shouldRenderRequestQueueProgress = computed(() => hasStoredSessionToken());
|
|
const shouldRenderReleaseChannelUnavailable = computed(() => releaseChannelUnavailableStatus.value.shouldBlock);
|
|
const shouldRenderReleaseChannelSwitched = computed(
|
|
() => !shouldRenderReleaseChannelUnavailable.value && releaseChannelSwitchedStatus.value.shouldShow
|
|
);
|
|
const shouldHideGlobalOverlays = computed(
|
|
() => shouldRenderReleaseChannelUnavailable.value || shouldRenderReleaseChannelSwitched.value
|
|
);
|
|
const sessionLoaderRoutePattern = /^\/(?:user|admin|superuser|backoffice)(?:\/|$)/;
|
|
const guestSessionLoaderRoutePattern =
|
|
/^\/(?:$|login(?:\/driver|\/qr)?$|admin\/login$|register$|auth\/password-reset(?:\/|$)|qr\/new-(?:customer|driver)$)/;
|
|
const currentBrowserPath = () => {
|
|
if (typeof window === "undefined") {
|
|
return "";
|
|
}
|
|
return window.location.pathname.replace(/^\/[^/]+\/frontend(?=\/|$)/, "") || "/";
|
|
};
|
|
const routeMiddlewareList = () =>
|
|
route.matched.flatMap((record) => {
|
|
const middleware = record.meta?.middleware;
|
|
if (!middleware) {
|
|
return [];
|
|
}
|
|
return Array.isArray(middleware) ? middleware : [middleware];
|
|
});
|
|
const hasRouteMiddleware = (middlewareName) =>
|
|
routeMiddlewareList().some((middleware) => middleware?.name === middlewareName);
|
|
const shouldUseSessionPathLoader = (path) =>
|
|
sessionLoaderRoutePattern.test(path || "") &&
|
|
(!guestSessionLoaderRoutePattern.test(path || "") || hasStoredSessionToken());
|
|
const shouldUseGuestSessionPathLoader = (path) =>
|
|
guestSessionLoaderRoutePattern.test(path || "") && hasStoredSessionToken();
|
|
const shouldUseSessionRouteLoader = computed(() =>
|
|
route.name !== "default" &&
|
|
(shouldUseSessionPathLoader(route.path || "") ||
|
|
shouldUseSessionPathLoader(currentBrowserPath()) ||
|
|
shouldUseGuestSessionPathLoader(route.path || "") ||
|
|
shouldUseGuestSessionPathLoader(currentBrowserPath()) ||
|
|
hasRouteMiddleware("authMiddleware") ||
|
|
(hasStoredSessionToken() && hasRouteMiddleware("guestMiddleware")))
|
|
);
|
|
const appRouteLoadingProps = computed(() =>
|
|
shouldUseSessionRouteLoader.value ? sessionRouteLoadingProps : pageRouteLoadingProps
|
|
);
|
|
|
|
const initiateStoredSession = async () => {
|
|
if (!hasStoredSessionToken()) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const { SessionUser } = await import("@/components/session/token/SessionUser.vue");
|
|
await SessionUser.initiateOnAppStart();
|
|
} catch (error) {
|
|
console.error("Failed to initiate stored session:", error);
|
|
}
|
|
};
|
|
|
|
useHead({
|
|
link: [{ rel: "icon", type: "image/x-icon", href: favicon }],
|
|
});
|
|
onMounted(() => {
|
|
startReleaseChannelAvailabilityClock();
|
|
void initiateStoredSession();
|
|
});
|
|
|
|
onBeforeUnmount(() => {
|
|
stopReleaseChannelAvailabilityClock();
|
|
});
|
|
|
|
watch([() => route.fullPath, locale], updateDocumentTitle, { immediate: true });
|
|
</script>
|
|
|
|
<template>
|
|
<Suspense :timeout="0">
|
|
<template #default>
|
|
<ReleaseChannelUnavailable v-if="shouldRenderReleaseChannelUnavailable" />
|
|
<ReleaseChannelSwitched v-else-if="shouldRenderReleaseChannelSwitched" />
|
|
<LayoutV2 v-else-if="route.meta?.template === 'with-header'">
|
|
<router-view v-slot="{ Component }">
|
|
<Suspense :timeout="0">
|
|
<component :is="Component" />
|
|
<template #fallback>
|
|
<BrandedLoadingScreen v-bind="appRouteLoadingProps" />
|
|
</template>
|
|
</Suspense>
|
|
</router-view>
|
|
</LayoutV2>
|
|
<main v-else-if="route.meta?.template === 'clear-main'">
|
|
<router-view v-slot="{ Component }">
|
|
<Suspense :timeout="0">
|
|
<component :is="Component" />
|
|
<template #fallback>
|
|
<BrandedLoadingScreen v-bind="appRouteLoadingProps" />
|
|
</template>
|
|
</Suspense>
|
|
</router-view>
|
|
</main>
|
|
<template v-else>
|
|
<header></header>
|
|
<main>
|
|
<DefaultPageWrapper>
|
|
<router-view v-slot="{ Component }">
|
|
<Suspense :timeout="0">
|
|
<component :is="Component" />
|
|
<template #fallback>
|
|
<BrandedLoadingScreen v-bind="appRouteLoadingProps" />
|
|
</template>
|
|
</Suspense>
|
|
</router-view>
|
|
</DefaultPageWrapper>
|
|
</main>
|
|
</template>
|
|
</template>
|
|
<template #fallback>
|
|
<BrandedLoadingScreen v-bind="appRouteLoadingProps" />
|
|
</template>
|
|
</Suspense>
|
|
<RequestQueueProgress v-if="shouldRenderRequestQueueProgress && !shouldHideGlobalOverlays" />
|
|
<ErrorReportLauncher v-if="shouldRenderRequestQueueProgress && !shouldHideGlobalOverlays" />
|
|
<FrontendMaintenanceMenu />
|
|
</template>
|
|
|
|
<style scoped></style>
|