@shopbite-de/storefront 1.15.0 → 1.16.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/settings.local.json +8 -0
- package/app/app.vue +53 -34
- package/app/components/AddToWishlist.vue +0 -3
- package/app/components/Category/Listing.vue +2 -2
- package/app/components/Navigation/{DesktopLeft2.vue → DesktopLeft.vue} +3 -3
- package/app/components/Navigation/MobileTop.vue +24 -0
- package/app/components/User/LoginForm.vue +1 -4
- package/app/composables/useNavigation.ts +30 -0
- package/app/composables/useNavigationSearch.ts +68 -0
- package/app/layouts/account.vue +0 -1
- package/app/layouts/{listing2.vue → listing.vue} +1 -1
- package/app/pages/anmelden.vue +8 -4
- package/app/pages/speisekarte/[...all].vue +2 -1
- package/nuxt.config.ts +2 -0
- package/package.json +1 -1
- package/test/nuxt/LoginForm.test.ts +25 -15
- package/app/components/Navigation/MobileTop2.vue +0 -61
- package/app/middleware/trailing-slash.global.ts +0 -19
package/app/app.vue
CHANGED
|
@@ -1,37 +1,6 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import type { Schemas } from "#shopware";
|
|
3
2
|
import type { Toast } from "#ui/composables/useToast";
|
|
4
3
|
|
|
5
|
-
// Composables
|
|
6
|
-
const toast = useToast();
|
|
7
|
-
const appConfig = useAppConfig();
|
|
8
|
-
const { apiClient } = useShopwareContext();
|
|
9
|
-
const { refresh: refreshToppings } = useShopBiteConfig();
|
|
10
|
-
const { refreshCart } = useCart();
|
|
11
|
-
const { getWishlistProducts } = useWishlist();
|
|
12
|
-
|
|
13
|
-
const {
|
|
14
|
-
getNextOpeningTime,
|
|
15
|
-
isStoreOpen,
|
|
16
|
-
refresh: refreshBusinessHours,
|
|
17
|
-
} = useBusinessHours();
|
|
18
|
-
const { isClosedHoliday, refresh: refreshHolidays } = useHolidays();
|
|
19
|
-
|
|
20
|
-
await Promise.all([refreshBusinessHours(), refreshHolidays()]);
|
|
21
|
-
|
|
22
|
-
const sessionContextData = ref<Schemas["SalesChannelContext"]>();
|
|
23
|
-
|
|
24
|
-
const contextResponse = await apiClient
|
|
25
|
-
.invoke("readContext get /context")
|
|
26
|
-
.catch((error) => {
|
|
27
|
-
console.error("Error fetching session context data:", error);
|
|
28
|
-
return { data: undefined };
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
sessionContextData.value = contextResponse.data;
|
|
32
|
-
useSessionContext(sessionContextData.value);
|
|
33
|
-
|
|
34
|
-
// Toast configuration
|
|
35
4
|
const TOAST_CONFIG = {
|
|
36
5
|
open: {
|
|
37
6
|
id: "currently-open",
|
|
@@ -59,6 +28,41 @@ const TOAST_CONFIG = {
|
|
|
59
28
|
} as Partial<Toast>,
|
|
60
29
|
} as const;
|
|
61
30
|
|
|
31
|
+
const { apiClient } = useShopwareContext();
|
|
32
|
+
const appConfig = useAppConfig();
|
|
33
|
+
const router = useRouter();
|
|
34
|
+
const toast = useToast();
|
|
35
|
+
|
|
36
|
+
const { data: sessionContextData } = await useAsyncData(
|
|
37
|
+
"sessionContext",
|
|
38
|
+
async () => {
|
|
39
|
+
try {
|
|
40
|
+
const { data } = await apiClient.invoke("readContext get /context");
|
|
41
|
+
return data;
|
|
42
|
+
} catch (error) {
|
|
43
|
+
console.error("Failed to load session context", error);
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
default: () => null,
|
|
49
|
+
},
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
if (sessionContextData.value) {
|
|
53
|
+
usePrice({
|
|
54
|
+
currencyCode: sessionContextData.value.currency?.isoCode || "",
|
|
55
|
+
});
|
|
56
|
+
useSessionContext(sessionContextData.value);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const {
|
|
60
|
+
getNextOpeningTime,
|
|
61
|
+
isStoreOpen,
|
|
62
|
+
refresh: refreshBusinessHours,
|
|
63
|
+
} = useBusinessHours();
|
|
64
|
+
const { isClosedHoliday, refresh: refreshHolidays } = useHolidays();
|
|
65
|
+
|
|
62
66
|
function displayStoreStatus() {
|
|
63
67
|
const isOpen = isStoreOpen(undefined, isClosedHoliday);
|
|
64
68
|
|
|
@@ -75,10 +79,25 @@ function displayStoreStatus() {
|
|
|
75
79
|
}
|
|
76
80
|
}
|
|
77
81
|
|
|
78
|
-
|
|
82
|
+
const { refreshCart } = useCart();
|
|
83
|
+
const { getWishlistProducts } = useWishlist();
|
|
84
|
+
|
|
85
|
+
if (import.meta.client) {
|
|
86
|
+
// getting the wishlist products should not block SSR
|
|
87
|
+
if (!(router.currentRoute.value.name as string).includes("wishlist")) {
|
|
88
|
+
getWishlistProducts(); // initial page loading
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const { refresh: refreshToppings } = useShopBiteConfig();
|
|
93
|
+
|
|
79
94
|
onMounted(async () => {
|
|
80
|
-
await
|
|
81
|
-
|
|
95
|
+
await Promise.all([
|
|
96
|
+
refreshHolidays(),
|
|
97
|
+
refreshBusinessHours(),
|
|
98
|
+
refreshToppings(),
|
|
99
|
+
]);
|
|
100
|
+
refreshCart();
|
|
82
101
|
displayStoreStatus();
|
|
83
102
|
});
|
|
84
103
|
|
|
@@ -8,17 +8,14 @@ const props = defineProps<{
|
|
|
8
8
|
const { addToWishlist, isInWishlist, removeFromWishlist } = useProductWishlist(
|
|
9
9
|
props.product.id,
|
|
10
10
|
);
|
|
11
|
-
const { getWishlistProducts } = useWishlist();
|
|
12
11
|
const { trackAddToWishlist } = useTrackEvent();
|
|
13
12
|
|
|
14
13
|
const toggleWishlistProduct = async () => {
|
|
15
14
|
try {
|
|
16
15
|
if (isInWishlist.value) {
|
|
17
16
|
await removeFromWishlist();
|
|
18
|
-
await getWishlistProducts();
|
|
19
17
|
} else {
|
|
20
18
|
await addToWishlist();
|
|
21
|
-
await getWishlistProducts();
|
|
22
19
|
trackAddToWishlist(props.product);
|
|
23
20
|
}
|
|
24
21
|
} catch (error) {
|
|
@@ -118,7 +118,7 @@ const moreThanOneFilterAndOption = computed<boolean>(
|
|
|
118
118
|
<UPage>
|
|
119
119
|
<template #left>
|
|
120
120
|
<UPageAside>
|
|
121
|
-
<
|
|
121
|
+
<NavigationDesktopLeft />
|
|
122
122
|
</UPageAside>
|
|
123
123
|
</template>
|
|
124
124
|
|
|
@@ -212,7 +212,7 @@ const moreThanOneFilterAndOption = computed<boolean>(
|
|
|
212
212
|
<template #right>
|
|
213
213
|
<UPageAside>
|
|
214
214
|
<div v-if="moreThanOneFilterAndOption" class="flex flex-col gap-4">
|
|
215
|
-
<h2 class="text-3xl md:text-4xl
|
|
215
|
+
<h2 class="text-3xl md:text-4xl mb-3 pb-2">Filter</h2>
|
|
216
216
|
<div
|
|
217
217
|
v-for="filter in propertyFilters"
|
|
218
218
|
:key="filter.id"
|
|
@@ -3,7 +3,7 @@ import type { NavigationMenuItem } from "@nuxt/ui";
|
|
|
3
3
|
import type { Schemas } from "#shopware";
|
|
4
4
|
import { useNavigation } from "~/composables/useNavigation";
|
|
5
5
|
|
|
6
|
-
const {
|
|
6
|
+
const { menuCardNavigation } = useNavigation(true);
|
|
7
7
|
|
|
8
8
|
const mapCategoryToNavItem = (
|
|
9
9
|
category: Schemas["Category"],
|
|
@@ -21,13 +21,13 @@ const mapCategoryToNavItem = (
|
|
|
21
21
|
};
|
|
22
22
|
|
|
23
23
|
const navItems = computed<NavigationMenuItem[]>(() => {
|
|
24
|
-
return (
|
|
24
|
+
return (menuCardNavigation.value ?? []).map(mapCategoryToNavItem);
|
|
25
25
|
});
|
|
26
26
|
</script>
|
|
27
27
|
|
|
28
28
|
<template>
|
|
29
29
|
<div>
|
|
30
|
-
<h2 class="text-3xl md:text-4xl
|
|
30
|
+
<h2 class="text-3xl md:text-4xl mb-3 pb-2">Speisekarte</h2>
|
|
31
31
|
<UNavigationMenu
|
|
32
32
|
variant="link"
|
|
33
33
|
orientation="vertical"
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { useNavigation } from "~/composables/useNavigation";
|
|
3
|
+
|
|
4
|
+
const { menuCardMenu } = useNavigation(true);
|
|
5
|
+
</script>
|
|
6
|
+
|
|
7
|
+
<template>
|
|
8
|
+
<UNavigationMenu
|
|
9
|
+
class="lg:hidden"
|
|
10
|
+
orientation="horizontal"
|
|
11
|
+
:items="menuCardMenu"
|
|
12
|
+
:ui="{
|
|
13
|
+
list: 'overflow-x-auto',
|
|
14
|
+
item: 'flex-shrink-0',
|
|
15
|
+
}"
|
|
16
|
+
>
|
|
17
|
+
<template #list-leading>
|
|
18
|
+
<UIcon name="i-lucide-chevron-left" class="size-8" />
|
|
19
|
+
</template>
|
|
20
|
+
<template #list-trailing>
|
|
21
|
+
<UIcon name="i-lucide-chevron-right" class="size-8" />
|
|
22
|
+
</template>
|
|
23
|
+
</UNavigationMenu>
|
|
24
|
+
</template>
|
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import * as z from "zod";
|
|
3
|
-
import { useWishlist, useUser } from "@shopware/composables";
|
|
4
3
|
import { ApiClientError } from "@shopware/api-client";
|
|
5
4
|
import type { FormSubmitEvent } from "@nuxt/ui";
|
|
6
5
|
|
|
7
6
|
const { isLoggedIn, login, user } = useUser();
|
|
8
|
-
const { mergeWishlistProducts } = useWishlist();
|
|
9
7
|
const toast = useToast();
|
|
10
8
|
|
|
11
9
|
const props = withDefaults(
|
|
@@ -64,10 +62,9 @@ async function onSubmit(payload: FormSubmitEvent<Schema>) {
|
|
|
64
62
|
toast.add({
|
|
65
63
|
title:
|
|
66
64
|
"Hallo " + user.value?.firstName + " " + user.value?.lastName + "!",
|
|
67
|
-
description: "
|
|
65
|
+
description: "Erfolgreich angemeldet.",
|
|
68
66
|
color: "success",
|
|
69
67
|
});
|
|
70
|
-
mergeWishlistProducts();
|
|
71
68
|
emit("login-success", payload.data.email);
|
|
72
69
|
} catch (error) {
|
|
73
70
|
console.error("Login failed:", error);
|
|
@@ -4,6 +4,10 @@ import type { Schemas } from "#shopware";
|
|
|
4
4
|
|
|
5
5
|
export function useNavigation(withChildren: boolean | undefined) {
|
|
6
6
|
const { apiClient } = useShopwareContext();
|
|
7
|
+
const config = useRuntimeConfig();
|
|
8
|
+
const menuCategoryId = computed(
|
|
9
|
+
() => config.public.shopBite.menuCategoryId ?? "main-navigation",
|
|
10
|
+
);
|
|
7
11
|
|
|
8
12
|
const criteria = encodeForQuery({
|
|
9
13
|
includes: {
|
|
@@ -77,10 +81,36 @@ export function useNavigation(withChildren: boolean | undefined) {
|
|
|
77
81
|
return footerNavigation.value?.map(mapCategoryToMenuItem);
|
|
78
82
|
});
|
|
79
83
|
|
|
84
|
+
const { data: menuCardNavigation } = useAsyncData(
|
|
85
|
+
"menu-category",
|
|
86
|
+
async () => {
|
|
87
|
+
const response = await apiClient.invoke(
|
|
88
|
+
"readNavigationGet get /navigation/{activeId}/{rootId}",
|
|
89
|
+
{
|
|
90
|
+
query: { _criteria: criteria },
|
|
91
|
+
pathParams: {
|
|
92
|
+
activeId: "main-navigation",
|
|
93
|
+
rootId: menuCategoryId.value,
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
return response.data;
|
|
99
|
+
},
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
const menuCardMenu = computed<NavigationMenuItem[]>(() => {
|
|
103
|
+
if (!menuCardNavigation.value) return [];
|
|
104
|
+
|
|
105
|
+
return menuCardNavigation.value?.map(mapCategoryToMenuItem);
|
|
106
|
+
});
|
|
107
|
+
|
|
80
108
|
return {
|
|
81
109
|
mainNavigation,
|
|
82
110
|
mainMenu,
|
|
83
111
|
footerNavigation,
|
|
84
112
|
footerMenu,
|
|
113
|
+
menuCardNavigation,
|
|
114
|
+
menuCardMenu,
|
|
85
115
|
};
|
|
86
116
|
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { useSessionContext, useShopwareContext } from "#imports";
|
|
2
|
+
import type { Schemas } from "#shopware";
|
|
3
|
+
import { encodeForQuery } from "@shopware/api-client/helpers";
|
|
4
|
+
|
|
5
|
+
export type UseNavigationSearchReturn = {
|
|
6
|
+
/**
|
|
7
|
+
* Get {@link SeoUrl} entity for given path
|
|
8
|
+
* @example resolvePath("/my-category/my-product") or resolvePath("/") for home page
|
|
9
|
+
*/
|
|
10
|
+
resolvePath(path: string): Promise<Schemas["SeoUrl"] | null>;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Composable to get search for SeoUrl entity for given path.
|
|
15
|
+
* @public
|
|
16
|
+
* @category Navigation & Routing
|
|
17
|
+
*/
|
|
18
|
+
export function useNavigationSearch(): UseNavigationSearchReturn {
|
|
19
|
+
const { apiClient } = useShopwareContext();
|
|
20
|
+
const { sessionContext } = useSessionContext();
|
|
21
|
+
|
|
22
|
+
async function resolvePath(path: string): Promise<Schemas["SeoUrl"] | null> {
|
|
23
|
+
if (path === "/") {
|
|
24
|
+
// please ignore optional chaining for salesChannel object as it's always present (type definition issue)
|
|
25
|
+
const categoryId =
|
|
26
|
+
sessionContext.value?.salesChannel?.navigationCategoryId;
|
|
27
|
+
|
|
28
|
+
return {
|
|
29
|
+
routeName: "frontend.navigation.page",
|
|
30
|
+
foreignKey: categoryId,
|
|
31
|
+
} as Schemas["SeoUrl"];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const isTechnicalUrl =
|
|
35
|
+
path.startsWith("/navigation/") ||
|
|
36
|
+
path.startsWith("/detail/") ||
|
|
37
|
+
path.startsWith("/landingPage/");
|
|
38
|
+
|
|
39
|
+
// remove leading slash in case of seo url or remove trailing slash in case of technical url
|
|
40
|
+
const normalizedPath = isTechnicalUrl
|
|
41
|
+
? path.endsWith("/")
|
|
42
|
+
? path.slice(0, -1)
|
|
43
|
+
: path
|
|
44
|
+
: path.substring(1);
|
|
45
|
+
|
|
46
|
+
const criteria = encodeForQuery({
|
|
47
|
+
filter: [
|
|
48
|
+
{
|
|
49
|
+
type: "equals",
|
|
50
|
+
field: isTechnicalUrl ? "pathInfo" : "seoPathInfo",
|
|
51
|
+
value: normalizedPath,
|
|
52
|
+
},
|
|
53
|
+
],
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
const seoResult = await apiClient.invoke("readSeoUrlGet get /seo-url", {
|
|
57
|
+
query: {
|
|
58
|
+
_criteria: criteria,
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
return seoResult.data.elements?.[0] ?? null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return {
|
|
66
|
+
resolvePath,
|
|
67
|
+
};
|
|
68
|
+
}
|
package/app/layouts/account.vue
CHANGED
package/app/pages/anmelden.vue
CHANGED
|
@@ -1,19 +1,23 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
+
import { useWishlist } from "@shopware/composables";
|
|
3
|
+
|
|
2
4
|
const { isLoggedIn } = useUser();
|
|
5
|
+
const { mergeWishlistProducts } = useWishlist();
|
|
3
6
|
|
|
4
|
-
|
|
5
|
-
if (isLoggedIn.value) {
|
|
6
|
-
navigateTo("/konto");
|
|
7
|
+
onBeforeMount(async () => {
|
|
8
|
+
if (import.meta.client && isLoggedIn.value) {
|
|
9
|
+
navigateTo({ path: "/konto" });
|
|
7
10
|
}
|
|
8
11
|
});
|
|
9
12
|
|
|
10
13
|
watch(isLoggedIn, (newValue) => {
|
|
11
14
|
if (newValue) {
|
|
12
|
-
navigateTo("/konto");
|
|
15
|
+
navigateTo({ path: "/konto" });
|
|
13
16
|
}
|
|
14
17
|
});
|
|
15
18
|
|
|
16
19
|
function handleLoginSuccess() {
|
|
20
|
+
mergeWishlistProducts();
|
|
17
21
|
navigateTo("/");
|
|
18
22
|
}
|
|
19
23
|
</script>
|
package/nuxt.config.ts
CHANGED
|
@@ -55,6 +55,7 @@ export default defineNuxtConfig({
|
|
|
55
55
|
geoapifyApiKey: "",
|
|
56
56
|
public: {
|
|
57
57
|
shopBite: {
|
|
58
|
+
menuCategoryId: "",
|
|
58
59
|
feature: {
|
|
59
60
|
multiChannel: "",
|
|
60
61
|
secureKey: "",
|
|
@@ -85,6 +86,7 @@ export default defineNuxtConfig({
|
|
|
85
86
|
endpoint: "",
|
|
86
87
|
accessToken: "",
|
|
87
88
|
devStorefrontUrl: "",
|
|
89
|
+
useUserContextInSSR: true,
|
|
88
90
|
},
|
|
89
91
|
|
|
90
92
|
modules: [
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
-
import { mountSuspended } from "@nuxt/test-utils/runtime";
|
|
2
|
+
import { mountSuspended, mockNuxtImport } from "@nuxt/test-utils/runtime";
|
|
3
|
+
import { ref } from "vue";
|
|
3
4
|
import LoginForm from "@/components/User/LoginForm.vue";
|
|
4
|
-
import { useUser } from "@shopware/composables";
|
|
5
5
|
import { ApiClientError } from "@shopware/api-client";
|
|
6
6
|
|
|
7
7
|
vi.mock("@shopware/api-client", () => ({
|
|
@@ -16,9 +16,7 @@ vi.mock("@shopware/api-client", () => ({
|
|
|
16
16
|
|
|
17
17
|
vi.mock("@shopware/composables", () => ({
|
|
18
18
|
useUser: vi.fn(),
|
|
19
|
-
useWishlist: vi.fn(
|
|
20
|
-
mergeWishlistProducts: vi.fn(),
|
|
21
|
-
})),
|
|
19
|
+
useWishlist: vi.fn(),
|
|
22
20
|
}));
|
|
23
21
|
|
|
24
22
|
const mockToastAdd = vi.fn();
|
|
@@ -28,19 +26,30 @@ mockNuxtImport("useToast", () => {
|
|
|
28
26
|
});
|
|
29
27
|
});
|
|
30
28
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
29
|
+
const loginMock = vi.fn();
|
|
30
|
+
const userMock = ref({ firstName: "John", lastName: "Doe" });
|
|
31
|
+
const isLoggedInMock = ref(false);
|
|
34
32
|
|
|
33
|
+
mockNuxtImport("useUser", () => {
|
|
34
|
+
return () => ({
|
|
35
|
+
isLoggedIn: isLoggedInMock,
|
|
36
|
+
login: loginMock,
|
|
37
|
+
user: userMock,
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
mockNuxtImport("useWishlist", () => {
|
|
42
|
+
return () => ({
|
|
43
|
+
mergeWishlistProducts: vi.fn(),
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
describe("LoginForm", () => {
|
|
35
48
|
beforeEach(() => {
|
|
36
49
|
vi.clearAllMocks();
|
|
37
|
-
loginMock
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
isLoggedIn: { value: false },
|
|
41
|
-
login: loginMock,
|
|
42
|
-
user: userMock,
|
|
43
|
-
});
|
|
50
|
+
loginMock.mockReset();
|
|
51
|
+
isLoggedInMock.value = false;
|
|
52
|
+
userMock.value = { firstName: "John", lastName: "Doe" };
|
|
44
53
|
});
|
|
45
54
|
|
|
46
55
|
it("should show success toast on successful login", async () => {
|
|
@@ -63,6 +72,7 @@ describe("LoginForm", () => {
|
|
|
63
72
|
expect(mockToastAdd).toHaveBeenCalledWith(
|
|
64
73
|
expect.objectContaining({
|
|
65
74
|
title: "Hallo John Doe!",
|
|
75
|
+
description: "Erfolgreich angemeldet.",
|
|
66
76
|
color: "success",
|
|
67
77
|
}),
|
|
68
78
|
);
|
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
<script setup lang="ts">
|
|
2
|
-
import { useNavigation } from "~/composables/useNavigation";
|
|
3
|
-
import type { NavigationMenuItem } from "@nuxt/ui";
|
|
4
|
-
import type { Schemas } from "#shopware";
|
|
5
|
-
|
|
6
|
-
type Category = Schemas["Category"];
|
|
7
|
-
|
|
8
|
-
const props = withDefaults(
|
|
9
|
-
defineProps<{
|
|
10
|
-
shouldSkipFirstLevel?: boolean;
|
|
11
|
-
}>(),
|
|
12
|
-
{
|
|
13
|
-
shouldSkipFirstLevel: false,
|
|
14
|
-
},
|
|
15
|
-
);
|
|
16
|
-
|
|
17
|
-
const { mainNavigation } = useNavigation(true);
|
|
18
|
-
|
|
19
|
-
const navItems = computed<NavigationMenuItem[]>(() => {
|
|
20
|
-
const elements = mainNavigation.value ?? [];
|
|
21
|
-
|
|
22
|
-
const mapCategoryRecursively = (category: Category): NavigationMenuItem => {
|
|
23
|
-
const hasChildren = (category.children?.length ?? 0) > 0;
|
|
24
|
-
|
|
25
|
-
return {
|
|
26
|
-
label: category.translated?.name ?? "",
|
|
27
|
-
to: hasChildren ? undefined : category.seoUrl,
|
|
28
|
-
children: hasChildren
|
|
29
|
-
? category.children!.map(mapCategoryRecursively)
|
|
30
|
-
: undefined,
|
|
31
|
-
};
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
if (props.shouldSkipFirstLevel) {
|
|
35
|
-
return elements
|
|
36
|
-
.flatMap((item: Category) => item.children ?? [])
|
|
37
|
-
.map(mapCategoryRecursively);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
return elements.map(mapCategoryRecursively);
|
|
41
|
-
});
|
|
42
|
-
</script>
|
|
43
|
-
|
|
44
|
-
<template>
|
|
45
|
-
<UNavigationMenu
|
|
46
|
-
class="lg:hidden"
|
|
47
|
-
orientation="horizontal"
|
|
48
|
-
:items="navItems"
|
|
49
|
-
:ui="{
|
|
50
|
-
list: 'overflow-x-auto',
|
|
51
|
-
item: 'flex-shrink-0',
|
|
52
|
-
}"
|
|
53
|
-
>
|
|
54
|
-
<template #list-leading>
|
|
55
|
-
<UIcon name="i-lucide-chevron-left" class="size-8" />
|
|
56
|
-
</template>
|
|
57
|
-
<template #list-trailing>
|
|
58
|
-
<UIcon name="i-lucide-chevron-right" class="size-8" />
|
|
59
|
-
</template>
|
|
60
|
-
</UNavigationMenu>
|
|
61
|
-
</template>
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import { defineNuxtRouteMiddleware, navigateTo } from "#app";
|
|
2
|
-
|
|
3
|
-
export default defineNuxtRouteMiddleware((to) => {
|
|
4
|
-
// Only handle category pages
|
|
5
|
-
if (!to.path.startsWith("/c/")) return;
|
|
6
|
-
|
|
7
|
-
// Skip if it already has a trailing slash or it's just "/c/"
|
|
8
|
-
if (to.path.endsWith("/") || to.path === "/c/") return;
|
|
9
|
-
|
|
10
|
-
// Preserve query and hash while adding the trailing slash
|
|
11
|
-
return navigateTo(
|
|
12
|
-
{
|
|
13
|
-
path: `${to.path}/`,
|
|
14
|
-
query: to.query,
|
|
15
|
-
hash: to.hash,
|
|
16
|
-
},
|
|
17
|
-
{ redirectCode: 301 },
|
|
18
|
-
);
|
|
19
|
-
});
|