Files
pleno-vue/src/App.vue
T

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>