@shopbite-de/storefront 1.6.2 → 1.7.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/.env.example +6 -0
- package/app/components/Header/Body.vue +15 -0
- package/app/components/Header/Right.vue +126 -0
- package/app/components/Header/Title.vue +16 -0
- package/app/components/Header.vue +5 -155
- package/app/components/Hero.vue +14 -2
- package/app/components/ImageGallery.vue +4 -1
- package/app/components/SalesChannelSwitch.vue +63 -0
- package/app/composables/useHeaderNavigation.ts +29 -0
- package/content/index.yml +8 -9
- package/content/navigation.yml +0 -6
- package/nuxt.config.ts +15 -1
- package/package.json +2 -1
- package/server/api/shopware/sales-channels.get.ts +79 -0
- package/server/utils/shopware/adminApiClient.ts +24 -0
- package/test/nuxt/Header.test.ts +30 -87
- package/test/nuxt/HeaderBody.test.ts +33 -0
- package/test/nuxt/HeaderRight.test.ts +141 -0
- package/test/nuxt/HeaderTitle.test.ts +42 -0
- package/test/nuxt/SalesChannelSwitch.test.ts +201 -0
- package/test/unit/sales-channels.test.ts +66 -0
package/.env.example
CHANGED
|
@@ -7,6 +7,12 @@ NUXT_PUBLIC_SHOPWARE_ENDPOINT="https://shopware.shopbite.de/store-api"
|
|
|
7
7
|
NUXT_PUBLIC_SHOPWARE_ACCESS_TOKEN="TOKEN"
|
|
8
8
|
NUXT_PUBLIC_SHOPWARE_COUNTRY_ID=019a17a0f67b706a8ec9ead3059e12ba
|
|
9
9
|
|
|
10
|
+
NUXT_SHOPWARE_ADMIN_ENDPOINT=https://your-shopware-instance.com
|
|
11
|
+
NUXT_SHOPWARE_ADMIN_CLIENT_ID=your_client_id
|
|
12
|
+
NUXT_SHOPWARE_ADMIN_CLIENT_SECRET=your_client_secret
|
|
13
|
+
|
|
14
|
+
NUXT_PUBLIC_SHOP_BITE_FEATURE_MULTI_CHANNEL=false
|
|
15
|
+
|
|
10
16
|
OPENAPI_ACCESS_KEY=key
|
|
11
17
|
OPENAPI_JSON_URL="https://shopware.shopbite.net"
|
|
12
18
|
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
const { navi } = useHeaderNavigation();
|
|
3
|
+
</script>
|
|
4
|
+
|
|
5
|
+
<template>
|
|
6
|
+
<UNavigationMenu
|
|
7
|
+
color="primary"
|
|
8
|
+
:items="navi"
|
|
9
|
+
orientation="vertical"
|
|
10
|
+
class="-mx-2.5"
|
|
11
|
+
/>
|
|
12
|
+
<div class="my-4">
|
|
13
|
+
<SalesChannelSwitch />
|
|
14
|
+
</div>
|
|
15
|
+
</template>
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { useUser } from "@shopware/composables";
|
|
3
|
+
import type { DropdownMenuItem } from "@nuxt/ui";
|
|
4
|
+
|
|
5
|
+
const cartQuickViewOpen = ref(false);
|
|
6
|
+
const { count } = useCart();
|
|
7
|
+
const { isCheckoutEnabled } = useShopBiteConfig();
|
|
8
|
+
const { isLoggedIn, isGuestSession, user, logout } = useUser();
|
|
9
|
+
const toast = useToast();
|
|
10
|
+
|
|
11
|
+
const logoutHandler = () => {
|
|
12
|
+
logout();
|
|
13
|
+
toast.add({
|
|
14
|
+
title: "Tschüss!",
|
|
15
|
+
description: "Erfolreich abgemeldet.",
|
|
16
|
+
color: "success",
|
|
17
|
+
});
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const { data: navigationData } = await useAsyncData("header-navigation", () =>
|
|
21
|
+
queryCollection("navigation").first(),
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
const accountHoverText = computed(() => {
|
|
25
|
+
return isLoggedIn.value || isGuestSession.value
|
|
26
|
+
? `${user.value?.firstName} ${user.value?.lastName}`
|
|
27
|
+
: "Hallo";
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
const loggedInDropDown = computed<DropdownMenuItem[][]>(() => {
|
|
31
|
+
if (!navigationData.value?.account.loggedIn) return [];
|
|
32
|
+
|
|
33
|
+
return navigationData.value.account.loggedIn
|
|
34
|
+
.map((group) =>
|
|
35
|
+
group
|
|
36
|
+
.filter((item) => {
|
|
37
|
+
if (isGuestSession.value) {
|
|
38
|
+
return item.type === "label" || item.action === "logout";
|
|
39
|
+
}
|
|
40
|
+
return true;
|
|
41
|
+
})
|
|
42
|
+
.map((item) => ({
|
|
43
|
+
label: item.type === "label" ? accountHoverText.value : item.label,
|
|
44
|
+
type: item.type,
|
|
45
|
+
icon: item.icon,
|
|
46
|
+
to: item.to,
|
|
47
|
+
onSelect: item.action === "logout" ? logoutHandler : undefined,
|
|
48
|
+
})),
|
|
49
|
+
)
|
|
50
|
+
.filter((group) => group.length > 0);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const loggedOutDropDown = computed<DropdownMenuItem[][]>(() => {
|
|
54
|
+
if (!navigationData.value?.account.loggedOut) return [];
|
|
55
|
+
|
|
56
|
+
return navigationData.value.account.loggedOut.map((group) =>
|
|
57
|
+
group.map((item) => ({
|
|
58
|
+
label: item.label,
|
|
59
|
+
type: item.type,
|
|
60
|
+
icon: item.icon,
|
|
61
|
+
to: item.to,
|
|
62
|
+
})),
|
|
63
|
+
);
|
|
64
|
+
});
|
|
65
|
+
</script>
|
|
66
|
+
|
|
67
|
+
<template>
|
|
68
|
+
<UButton
|
|
69
|
+
color="neutral"
|
|
70
|
+
variant="ghost"
|
|
71
|
+
to="tel:+49610471427"
|
|
72
|
+
target="_blank"
|
|
73
|
+
icon="i-lucide-phone"
|
|
74
|
+
aria-label="Anrufen"
|
|
75
|
+
/>
|
|
76
|
+
<UDropdownMenu
|
|
77
|
+
:items="isLoggedIn || isGuestSession ? loggedInDropDown : loggedOutDropDown"
|
|
78
|
+
>
|
|
79
|
+
<UChip v-if="isLoggedIn || isGuestSession" size="3xl" text="✓">
|
|
80
|
+
<UButton
|
|
81
|
+
aria-label="Konto Dropdown öffnen"
|
|
82
|
+
icon="i-lucide-user"
|
|
83
|
+
color="neutral"
|
|
84
|
+
variant="outline"
|
|
85
|
+
/>
|
|
86
|
+
</UChip>
|
|
87
|
+
<UButton
|
|
88
|
+
v-else
|
|
89
|
+
aria-label="Konto Dropdown öffnen"
|
|
90
|
+
icon="i-lucide-user"
|
|
91
|
+
color="neutral"
|
|
92
|
+
variant="outline"
|
|
93
|
+
/>
|
|
94
|
+
</UDropdownMenu>
|
|
95
|
+
<UDrawer
|
|
96
|
+
v-if="isCheckoutEnabled"
|
|
97
|
+
v-model:open="cartQuickViewOpen"
|
|
98
|
+
title="Warenkorb"
|
|
99
|
+
direction="right"
|
|
100
|
+
>
|
|
101
|
+
<UChip :text="count" size="3xl">
|
|
102
|
+
<UButton
|
|
103
|
+
aria-label="Zum Warenkorb"
|
|
104
|
+
color="neutral"
|
|
105
|
+
variant="outline"
|
|
106
|
+
icon="i-lucide-shopping-cart"
|
|
107
|
+
/>
|
|
108
|
+
</UChip>
|
|
109
|
+
|
|
110
|
+
<template #header>
|
|
111
|
+
<h2 class="text-3xl md:text-4xl mt-8 mb-3 pb-2">
|
|
112
|
+
<UIcon name="i-lucide-shopping-cart" class="size-8" color="primary" />
|
|
113
|
+
Warenkorb
|
|
114
|
+
</h2>
|
|
115
|
+
</template>
|
|
116
|
+
<template #body>
|
|
117
|
+
<CartQuickView
|
|
118
|
+
:with-to-cart-button="true"
|
|
119
|
+
class="md:min-w-90"
|
|
120
|
+
@go-to-cart="cartQuickViewOpen = false"
|
|
121
|
+
/>
|
|
122
|
+
</template>
|
|
123
|
+
</UDrawer>
|
|
124
|
+
</template>
|
|
125
|
+
|
|
126
|
+
<style scoped></style>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
const config = useRuntimeConfig();
|
|
3
|
+
const siteName = computed(() => config.public.site.name);
|
|
4
|
+
</script>
|
|
5
|
+
|
|
6
|
+
<template>
|
|
7
|
+
<NuxtLink to="/" class="-m-1.5 p-1.5">
|
|
8
|
+
<span class="sr-only">{{ siteName }}</span>
|
|
9
|
+
<UColorModeImage
|
|
10
|
+
alt="Logo"
|
|
11
|
+
light="/light/Logo.png"
|
|
12
|
+
dark="/dark/Logo.png"
|
|
13
|
+
class="h-12 w-auto"
|
|
14
|
+
/>
|
|
15
|
+
</NuxtLink>
|
|
16
|
+
</template>
|
|
@@ -1,175 +1,25 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
|
|
3
|
-
import { useRoute } from "vue-router";
|
|
4
|
-
import { useUser } from "@shopware/composables";
|
|
5
|
-
|
|
6
|
-
const route = useRoute();
|
|
7
|
-
const toast = useToast();
|
|
8
|
-
|
|
9
|
-
const { isLoggedIn, isGuestSession, user, logout } = useUser();
|
|
10
|
-
const { isCheckoutEnabled } = useShopBiteConfig();
|
|
11
|
-
const { count } = useCart();
|
|
12
|
-
const runtimeConfig = useRuntimeConfig();
|
|
13
|
-
|
|
14
|
-
// Fetch navigation from content
|
|
15
|
-
const { data: navigationData } = await useAsyncData("header-navigation", () =>
|
|
16
|
-
queryCollection("navigation").first(),
|
|
17
|
-
);
|
|
18
|
-
|
|
19
|
-
const siteName = computed(() => runtimeConfig.public.site?.name ?? "ShopBite");
|
|
20
|
-
|
|
21
|
-
const navi = computed<NavigationMenuItem[]>(() => {
|
|
22
|
-
if (!navigationData.value?.main) return [];
|
|
23
|
-
|
|
24
|
-
return navigationData.value.main.map((item) => ({
|
|
25
|
-
label: item.label,
|
|
26
|
-
icon: item.icon,
|
|
27
|
-
to: item.to,
|
|
28
|
-
target: item.target,
|
|
29
|
-
active:
|
|
30
|
-
item.to === "/"
|
|
31
|
-
? route.path.length === 1
|
|
32
|
-
: route.path.startsWith(item.to),
|
|
33
|
-
}));
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
const accountHoverText = computed(() => {
|
|
37
|
-
return isLoggedIn.value || isGuestSession.value
|
|
38
|
-
? `${user.value?.firstName} ${user.value?.lastName}`
|
|
39
|
-
: "Hallo";
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
const logoutHandler = () => {
|
|
43
|
-
logout();
|
|
44
|
-
toast.add({
|
|
45
|
-
title: "Tschüss!",
|
|
46
|
-
description: "Erfolreich abgemeldet.",
|
|
47
|
-
color: "success",
|
|
48
|
-
});
|
|
49
|
-
};
|
|
50
|
-
|
|
51
|
-
const loggedInDropDown = computed<DropdownMenuItem[][]>(() => {
|
|
52
|
-
if (!navigationData.value?.account.loggedIn) return [];
|
|
53
|
-
|
|
54
|
-
return navigationData.value.account.loggedIn
|
|
55
|
-
.map((group) =>
|
|
56
|
-
group
|
|
57
|
-
.filter((item) => {
|
|
58
|
-
if (isGuestSession.value) {
|
|
59
|
-
return item.type === "label" || item.action === "logout";
|
|
60
|
-
}
|
|
61
|
-
return true;
|
|
62
|
-
})
|
|
63
|
-
.map((item) => ({
|
|
64
|
-
label: item.type === "label" ? accountHoverText.value : item.label,
|
|
65
|
-
type: item.type,
|
|
66
|
-
icon: item.icon,
|
|
67
|
-
to: item.to,
|
|
68
|
-
onSelect: item.action === "logout" ? logoutHandler : undefined,
|
|
69
|
-
})),
|
|
70
|
-
)
|
|
71
|
-
.filter((group) => group.length > 0);
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
const loggedOutDropDown = computed<DropdownMenuItem[][]>(() => {
|
|
75
|
-
if (!navigationData.value?.account.loggedOut) return [];
|
|
76
|
-
|
|
77
|
-
return navigationData.value.account.loggedOut.map((group) =>
|
|
78
|
-
group.map((item) => ({
|
|
79
|
-
label: item.label,
|
|
80
|
-
type: item.type,
|
|
81
|
-
icon: item.icon,
|
|
82
|
-
to: item.to,
|
|
83
|
-
})),
|
|
84
|
-
);
|
|
85
|
-
});
|
|
86
|
-
|
|
2
|
+
const { navi } = useHeaderNavigation();
|
|
87
3
|
const loginSlide = ref(false);
|
|
88
|
-
const cartQuickViewOpen = ref(false);
|
|
89
4
|
</script>
|
|
90
5
|
|
|
91
6
|
<template>
|
|
92
7
|
<UHeader>
|
|
93
8
|
<template #title>
|
|
94
|
-
<
|
|
95
|
-
<span class="sr-only">{{ siteName }}</span>
|
|
96
|
-
<UColorModeImage
|
|
97
|
-
light="/light/Logo.png"
|
|
98
|
-
dark="/dark/Logo.png"
|
|
99
|
-
class="h-12 w-auto"
|
|
100
|
-
/>
|
|
101
|
-
</NuxtLink>
|
|
9
|
+
<HeaderTitle />
|
|
102
10
|
</template>
|
|
103
11
|
|
|
104
12
|
<UNavigationMenu color="primary" variant="pill" :items="navi" />
|
|
105
13
|
|
|
106
14
|
<template #right>
|
|
107
|
-
<
|
|
108
|
-
color="neutral"
|
|
109
|
-
variant="ghost"
|
|
110
|
-
to="tel:+49610471427"
|
|
111
|
-
target="_blank"
|
|
112
|
-
icon="i-lucide-phone"
|
|
113
|
-
aria-label="Anrufen"
|
|
114
|
-
/>
|
|
115
|
-
<UDropdownMenu
|
|
116
|
-
:items="
|
|
117
|
-
isLoggedIn || isGuestSession ? loggedInDropDown : loggedOutDropDown
|
|
118
|
-
"
|
|
119
|
-
>
|
|
120
|
-
<UChip v-if="isLoggedIn || isGuestSession" size="3xl" text="✓">
|
|
121
|
-
<UButton icon="i-lucide-user" color="neutral" variant="outline" />
|
|
122
|
-
</UChip>
|
|
123
|
-
<UButton
|
|
124
|
-
v-else
|
|
125
|
-
icon="i-lucide-user"
|
|
126
|
-
color="neutral"
|
|
127
|
-
variant="outline"
|
|
128
|
-
/>
|
|
129
|
-
</UDropdownMenu>
|
|
130
|
-
<UDrawer
|
|
131
|
-
v-if="isCheckoutEnabled"
|
|
132
|
-
v-model:open="cartQuickViewOpen"
|
|
133
|
-
title="Warenkorb"
|
|
134
|
-
direction="right"
|
|
135
|
-
>
|
|
136
|
-
<UChip :text="count" size="3xl">
|
|
137
|
-
<UButton
|
|
138
|
-
color="neutral"
|
|
139
|
-
variant="outline"
|
|
140
|
-
icon="i-lucide-shopping-cart"
|
|
141
|
-
/>
|
|
142
|
-
</UChip>
|
|
143
|
-
|
|
144
|
-
<template #header>
|
|
145
|
-
<h2 class="text-3xl md:text-4xl mt-8 mb-3 pb-2">
|
|
146
|
-
<UIcon
|
|
147
|
-
name="i-lucide-shopping-cart"
|
|
148
|
-
class="size-8"
|
|
149
|
-
color="primary"
|
|
150
|
-
/>
|
|
151
|
-
Warenkorb
|
|
152
|
-
</h2>
|
|
153
|
-
</template>
|
|
154
|
-
<template #body>
|
|
155
|
-
<CartQuickView
|
|
156
|
-
:with-to-cart-button="true"
|
|
157
|
-
class="md:min-w-90"
|
|
158
|
-
@go-to-cart="cartQuickViewOpen = false"
|
|
159
|
-
/>
|
|
160
|
-
</template>
|
|
161
|
-
</UDrawer>
|
|
15
|
+
<HeaderRight />
|
|
162
16
|
</template>
|
|
163
17
|
|
|
164
18
|
<template #body>
|
|
165
|
-
<
|
|
166
|
-
color="primary"
|
|
167
|
-
:items="navi"
|
|
168
|
-
orientation="vertical"
|
|
169
|
-
class="-mx-2.5"
|
|
170
|
-
/>
|
|
19
|
+
<HeaderBody />
|
|
171
20
|
</template>
|
|
172
21
|
</UHeader>
|
|
22
|
+
|
|
173
23
|
<USlideover
|
|
174
24
|
v-model:open="loginSlide"
|
|
175
25
|
title="Konto"
|
package/app/components/Hero.vue
CHANGED
|
@@ -22,18 +22,30 @@ withDefaults(
|
|
|
22
22
|
usps: () => [],
|
|
23
23
|
},
|
|
24
24
|
);
|
|
25
|
+
|
|
26
|
+
const videoRef = ref<HTMLVideoElement>();
|
|
27
|
+
|
|
28
|
+
onMounted(() => {
|
|
29
|
+
// Handle video playback on bfcache restore
|
|
30
|
+
window.addEventListener("pageshow", (event) => {
|
|
31
|
+
if (event.persisted && videoRef.value) {
|
|
32
|
+
videoRef.value.play();
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
});
|
|
25
36
|
</script>
|
|
26
37
|
|
|
27
38
|
<template>
|
|
28
39
|
<div class="relative">
|
|
29
40
|
<video
|
|
41
|
+
ref="videoRef"
|
|
30
42
|
autoplay
|
|
31
43
|
loop
|
|
32
44
|
muted
|
|
33
45
|
playsinline
|
|
34
46
|
class="absolute inset-0 w-full h-full object-cover -z-10"
|
|
35
47
|
>
|
|
36
|
-
<source :src="backgroundVideo" type="video/mp4" >
|
|
48
|
+
<source fetchpriority="high" :src="backgroundVideo" type="video/mp4" >
|
|
37
49
|
</video>
|
|
38
50
|
<div class="bg-black/50 backdrop-blur-sm">
|
|
39
51
|
<UPageHero
|
|
@@ -53,7 +65,7 @@ withDefaults(
|
|
|
53
65
|
:key="index"
|
|
54
66
|
as="button"
|
|
55
67
|
:to="usp.link ?? ''"
|
|
56
|
-
class="flex items-center text-left gap-2"
|
|
68
|
+
class="flex flex-col md:flex-row items-center text-center md:text-left gap-2"
|
|
57
69
|
target="_blank"
|
|
58
70
|
>
|
|
59
71
|
<UIcon
|
|
@@ -27,7 +27,7 @@ defineProps<{
|
|
|
27
27
|
>
|
|
28
28
|
<template #body>
|
|
29
29
|
<UCarousel
|
|
30
|
-
v-slot="{ item }"
|
|
30
|
+
v-slot="{ item, index }"
|
|
31
31
|
:items="images"
|
|
32
32
|
class="mx-auto w-full"
|
|
33
33
|
auto-height
|
|
@@ -35,6 +35,9 @@ defineProps<{
|
|
|
35
35
|
loop
|
|
36
36
|
>
|
|
37
37
|
<img
|
|
38
|
+
v-if="item"
|
|
39
|
+
:loading="index === 0 ? 'eager' : 'lazy'"
|
|
40
|
+
:fetchpriority="index === 0 ? 'high' : 'low'"
|
|
38
41
|
:src="item.image"
|
|
39
42
|
:alt="item.alt"
|
|
40
43
|
class="rounded-lg w-full max-h-screen object-contain"
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { SelectMenuItem } from "@nuxt/ui";
|
|
3
|
+
import type { Schemas } from "#shopware";
|
|
4
|
+
|
|
5
|
+
const config = useRuntimeConfig();
|
|
6
|
+
const isMultiChannel = computed(
|
|
7
|
+
() => !!config.public.shopBite.feature.multiChannel,
|
|
8
|
+
);
|
|
9
|
+
const storeUrl = computed(() => config.public.storeUrl);
|
|
10
|
+
|
|
11
|
+
const { data: salesChannels, status } = await useFetch(
|
|
12
|
+
"/api/shopware/sales-channels",
|
|
13
|
+
{
|
|
14
|
+
key: "sales-channels",
|
|
15
|
+
transform: (data: Schemas["SalesChannel"][]) => {
|
|
16
|
+
const currentUrl = storeUrl.value;
|
|
17
|
+
return data?.map((channel) => {
|
|
18
|
+
const domains = channel.domains || [];
|
|
19
|
+
const matchingDomain =
|
|
20
|
+
currentUrl != null
|
|
21
|
+
? domains.find((domain) => domain?.url === currentUrl)
|
|
22
|
+
: undefined;
|
|
23
|
+
const fallbackDomain = domains.find((domain) => !!domain?.url);
|
|
24
|
+
const domainUrl = matchingDomain?.url ?? fallbackDomain?.url ?? "";
|
|
25
|
+
return {
|
|
26
|
+
label: channel.translated.name ?? channel.name,
|
|
27
|
+
value: domainUrl,
|
|
28
|
+
};
|
|
29
|
+
});
|
|
30
|
+
},
|
|
31
|
+
lazy: true,
|
|
32
|
+
},
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
const selectedStore = ref<SelectMenuItem>();
|
|
36
|
+
|
|
37
|
+
watchEffect(() => {
|
|
38
|
+
if (salesChannels.value && storeUrl.value && !selectedStore.value) {
|
|
39
|
+
const matchingChannel = salesChannels.value.find(
|
|
40
|
+
(channel) => channel.value === storeUrl.value,
|
|
41
|
+
);
|
|
42
|
+
if (matchingChannel) {
|
|
43
|
+
selectedStore.value = matchingChannel;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
watch(selectedStore, (newStore, oldStore) => {
|
|
49
|
+
if (newStore && oldStore && newStore.value !== oldStore.value) {
|
|
50
|
+
window.location.href = newStore.value;
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
</script>
|
|
54
|
+
|
|
55
|
+
<template>
|
|
56
|
+
<USelectMenu
|
|
57
|
+
v-if="isMultiChannel"
|
|
58
|
+
v-model="selectedStore"
|
|
59
|
+
:items="salesChannels"
|
|
60
|
+
:loading="status === 'pending'"
|
|
61
|
+
icon="i-lucide-store"
|
|
62
|
+
/>
|
|
63
|
+
</template>
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { NavigationMenuItem } from "@nuxt/ui";
|
|
2
|
+
|
|
3
|
+
export const useHeaderNavigation = () => {
|
|
4
|
+
const route = useRoute();
|
|
5
|
+
|
|
6
|
+
const { data: navigationData } = useAsyncData("header-navigation", () =>
|
|
7
|
+
queryCollection("navigation").first(),
|
|
8
|
+
);
|
|
9
|
+
|
|
10
|
+
const navi = computed<NavigationMenuItem[]>(() => {
|
|
11
|
+
if (!navigationData.value?.main) return [];
|
|
12
|
+
|
|
13
|
+
return navigationData.value.main.map((item) => ({
|
|
14
|
+
label: item.label,
|
|
15
|
+
icon: item.icon,
|
|
16
|
+
to: item.to,
|
|
17
|
+
target: item.target,
|
|
18
|
+
active:
|
|
19
|
+
item.to === "/"
|
|
20
|
+
? route.path.length === 1
|
|
21
|
+
: route.path.startsWith(item.to),
|
|
22
|
+
}));
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
return {
|
|
26
|
+
navigationData,
|
|
27
|
+
navi,
|
|
28
|
+
};
|
|
29
|
+
};
|
package/content/index.yml
CHANGED
|
@@ -5,7 +5,7 @@ seo:
|
|
|
5
5
|
title: ShopBite
|
|
6
6
|
description: Reduziere Kosten und steigere deinen Umsatz
|
|
7
7
|
hero:
|
|
8
|
-
backgroundVideo: https://shopware.shopbite.de/media/10/59/96/1762465181/background.mp4
|
|
8
|
+
backgroundVideo: https://shopware.shopbite.de/media/10/59/96/1762465181/background.mp4?ts=1762465194
|
|
9
9
|
headline: SHOPBITE
|
|
10
10
|
usps:
|
|
11
11
|
- title: 4.5 ⭐
|
|
@@ -68,20 +68,19 @@ gallery:
|
|
|
68
68
|
variant: subtle
|
|
69
69
|
trailingIcon: i-lucide-phone
|
|
70
70
|
images:
|
|
71
|
-
- image: https://shopware.shopbite.de/media/71/2a/1a/1762465670/restaurant1.webp
|
|
71
|
+
- image: https://shopware.shopbite.de/media/71/2a/1a/1762465670/restaurant1.webp?ts=1762465670
|
|
72
72
|
alt: La Fattoria Restaurant Innenbereich 1
|
|
73
|
-
- image: https://shopware.shopbite.de/media/61/ea/77/1762465670/restaurant2.webp
|
|
73
|
+
- image: https://shopware.shopbite.de/media/61/ea/77/1762465670/restaurant2.webp?ts=1762465670
|
|
74
74
|
alt: La Fattoria Restaurant Innenbereich 2
|
|
75
|
-
- image: https://shopware.shopbite.de/media/6d/ac/ca/1762465670/restaurant3.webp
|
|
75
|
+
- image: https://shopware.shopbite.de/media/6d/ac/ca/1762465670/restaurant3.webp?ts=1762465670
|
|
76
76
|
alt: La Fattoria Restaurant Innenbereich 3
|
|
77
|
-
- image: https://shopware.shopbite.de/media/af/d6/da/1762465670/restaurant4.webp
|
|
77
|
+
- image: https://shopware.shopbite.de/media/af/d6/da/1762465670/restaurant4.webp?ts=1762465670
|
|
78
78
|
alt: La Fattoria Restaurant Innenbereich 4
|
|
79
|
-
- image: https://shopware.shopbite.de/media/6a/ff/e2/1762465670/restaurant5.webp
|
|
79
|
+
- image: https://shopware.shopbite.de/media/6a/ff/e2/1762465670/restaurant5.webp?ts=1762465670
|
|
80
80
|
alt: La Fattoria Restaurant Innenbereich 5
|
|
81
|
-
- image: https://shopware.shopbite.de/media/80/76/d0/1762465670/restaurant6.webp
|
|
81
|
+
- image: https://shopware.shopbite.de/media/80/76/d0/1762465670/restaurant6.webp?ts=1762465670
|
|
82
82
|
alt: La Fattoria Restaurant Innenbereich 6
|
|
83
|
-
|
|
84
|
-
alt: La Fattoria Restaurant Innenbereich 10
|
|
83
|
+
|
|
85
84
|
cta:
|
|
86
85
|
title: Jetzt bestellen!
|
|
87
86
|
description: Genieße die italienische Küche, frisch zubereitet und direkt zu dir geliefert oder vor Ort genießen.
|
package/content/navigation.yml
CHANGED
package/nuxt.config.ts
CHANGED
|
@@ -39,10 +39,19 @@ export default defineNuxtConfig({
|
|
|
39
39
|
},
|
|
40
40
|
|
|
41
41
|
runtimeConfig: {
|
|
42
|
-
shopware: {
|
|
42
|
+
shopware: {
|
|
43
|
+
adminClientId: "",
|
|
44
|
+
adminClientSecret: "",
|
|
45
|
+
adminEndpoint: "",
|
|
46
|
+
},
|
|
43
47
|
apiClientConfig: {},
|
|
44
48
|
geoapifyApiKey: "",
|
|
45
49
|
public: {
|
|
50
|
+
shopBite: {
|
|
51
|
+
feature: {
|
|
52
|
+
multiChannel: "",
|
|
53
|
+
},
|
|
54
|
+
},
|
|
46
55
|
site: {
|
|
47
56
|
name: "ShopBite",
|
|
48
57
|
description: "Reduziere deine Kosten und steigere deinen Umsatz",
|
|
@@ -82,6 +91,7 @@ export default defineNuxtConfig({
|
|
|
82
91
|
"@nuxt/ui",
|
|
83
92
|
"@nuxt/scripts",
|
|
84
93
|
"@nuxtjs/plausible",
|
|
94
|
+
"nuxt-vitalizer",
|
|
85
95
|
],
|
|
86
96
|
|
|
87
97
|
plausible: {
|
|
@@ -92,6 +102,10 @@ export default defineNuxtConfig({
|
|
|
92
102
|
experimental: { sqliteConnector: "native" },
|
|
93
103
|
},
|
|
94
104
|
|
|
105
|
+
vitalizer: {
|
|
106
|
+
disablePrefetchLinks: true,
|
|
107
|
+
},
|
|
108
|
+
|
|
95
109
|
pwa: {
|
|
96
110
|
manifest: {
|
|
97
111
|
name: "ShopBite",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shopbite-de/storefront",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.7.1",
|
|
4
4
|
"main": "nuxt.config.ts",
|
|
5
5
|
"description": "Shopware storefront for food delivery shops",
|
|
6
6
|
"keywords": [
|
|
@@ -35,6 +35,7 @@
|
|
|
35
35
|
"@vueuse/core": "^14.0.0",
|
|
36
36
|
"dotenv": "^17.2.3",
|
|
37
37
|
"nuxt": "^4.2.1",
|
|
38
|
+
"nuxt-vitalizer": "2.0.0",
|
|
38
39
|
"uuid": "^13.0.0"
|
|
39
40
|
},
|
|
40
41
|
"devDependencies": {
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import type { operations } from "@shopware/api-client/admin-api-types";
|
|
2
|
+
import { createAdminAPIClient } from "@shopware/api-client";
|
|
3
|
+
|
|
4
|
+
export default defineEventHandler(async () => {
|
|
5
|
+
const config = useRuntimeConfig();
|
|
6
|
+
|
|
7
|
+
// Get admin API credentials from runtime config
|
|
8
|
+
const adminEndpoint = config.shopware.adminEndpoint;
|
|
9
|
+
const adminClientId = config.shopware.adminClientId;
|
|
10
|
+
const adminClientSecret = config.shopware.adminClientSecret;
|
|
11
|
+
|
|
12
|
+
if (!adminEndpoint || !adminClientId || !adminClientSecret) {
|
|
13
|
+
throw createError({
|
|
14
|
+
statusCode: 500,
|
|
15
|
+
statusMessage: "Shopware admin API credentials not configured",
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
try {
|
|
20
|
+
const adminApiClient = createAdminAPIClient<operations>({
|
|
21
|
+
baseURL: adminEndpoint,
|
|
22
|
+
credentials: {
|
|
23
|
+
grant_type: "client_credentials",
|
|
24
|
+
client_id: adminClientId,
|
|
25
|
+
client_secret: adminClientSecret,
|
|
26
|
+
},
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
const data = await adminApiClient.invoke(
|
|
30
|
+
"searchSalesChannel post /search/sales-channel",
|
|
31
|
+
{
|
|
32
|
+
body: {
|
|
33
|
+
includes: {
|
|
34
|
+
sales_channel: ["id", "translated", "name", "domains", "active"],
|
|
35
|
+
sales_channel_domain: ["url"],
|
|
36
|
+
},
|
|
37
|
+
associations: {
|
|
38
|
+
domains: {},
|
|
39
|
+
},
|
|
40
|
+
filter: [
|
|
41
|
+
{
|
|
42
|
+
field: "active",
|
|
43
|
+
type: "equals",
|
|
44
|
+
value: true,
|
|
45
|
+
},
|
|
46
|
+
],
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
// Validate response data
|
|
52
|
+
if (!data || !data.data) {
|
|
53
|
+
throw createError({
|
|
54
|
+
statusCode: 500,
|
|
55
|
+
statusMessage: "Invalid response from Shopware API",
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return data.data.data;
|
|
60
|
+
} catch (error) {
|
|
61
|
+
// Handle specific error cases
|
|
62
|
+
if (error && typeof error === "object" && "statusCode" in error) {
|
|
63
|
+
// Re-throw createError instances
|
|
64
|
+
throw error;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Log the error for debugging
|
|
68
|
+
console.error("Failed to fetch sales channels:", error);
|
|
69
|
+
|
|
70
|
+
// Return a user-friendly error
|
|
71
|
+
throw createError({
|
|
72
|
+
statusCode: 503,
|
|
73
|
+
statusMessage: "Failed to fetch sales channels from Shopware",
|
|
74
|
+
data: {
|
|
75
|
+
originalError: error instanceof Error ? error.message : String(error),
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
});
|