@shopbite-de/storefront 1.1.1 → 1.2.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.
@@ -60,7 +60,9 @@ const handleRemoveItem = () => {
60
60
  </h3>
61
61
 
62
62
  <p
63
- v-for="option in cartItem?.payload?.options"
63
+ v-for="option in cartItem?.type === 'container'
64
+ ? cartItem?.children?.[0]?.payload?.options
65
+ : cartItem?.payload?.options"
64
66
  :key="option.group + option.option"
65
67
  class="text-sm text-pretty text-toned mt-1"
66
68
  >
@@ -7,63 +7,6 @@ const props = defineProps<{
7
7
 
8
8
  const { id: categoryId } = toRefs(props);
9
9
 
10
- const {
11
- resetFilters,
12
- loading,
13
- search,
14
- getElements,
15
- getCurrentSortingOrder,
16
- getSortingOrders,
17
- changeCurrentSortingOrder,
18
- getAvailableFilters,
19
- getCurrentFilters,
20
- setCurrentFilters,
21
- } = useListing({
22
- listingType: "categoryListing",
23
- categoryId: props.id,
24
- });
25
-
26
- const { search: categorySearch } = useCategorySearch();
27
-
28
- const { data: category } = await useAsyncData(
29
- `category${categoryId.value}`,
30
- async () => {
31
- return await categorySearch(categoryId.value);
32
- },
33
- );
34
-
35
- const pageTitle = computed(
36
- () =>
37
- `${category.value?.translated.name ?? category.value?.name} | Speisekarte`,
38
- );
39
-
40
- useSeoMeta({
41
- title: pageTitle,
42
- robots: "index,follow",
43
- });
44
-
45
- const currentSorting = ref(getCurrentSortingOrder.value ?? "Sortieren");
46
-
47
- const propertyFilters = computed<Schemas["PropertyGroup"][]>(() =>
48
- getAvailableFilters.value?.filter(
49
- (availableFilter) => availableFilter.code === "properties",
50
- ),
51
- );
52
-
53
- const selectedPropertyFilters = ref(getCurrentFilters.value?.properties ?? []);
54
- const selectedPropertyFiltersString = computed(() =>
55
- selectedPropertyFilters.value?.join("|"),
56
- );
57
-
58
- const selectedListingFilters = computed<ShortcutFilterParam[]>(() => {
59
- return [
60
- {
61
- code: "properties",
62
- value: selectedPropertyFiltersString.value,
63
- },
64
- ];
65
- });
66
-
67
10
  const query = {
68
11
  includes: {
69
12
  product: [
@@ -139,6 +82,64 @@ const query = {
139
82
  },
140
83
  } as operations["searchPage post /search"]["body"];
141
84
 
85
+ const {
86
+ resetFilters,
87
+ loading,
88
+ search,
89
+ getElements,
90
+ getCurrentSortingOrder,
91
+ getSortingOrders,
92
+ changeCurrentSortingOrder,
93
+ getAvailableFilters,
94
+ getCurrentFilters,
95
+ setCurrentFilters,
96
+ } = useListing({
97
+ listingType: "categoryListing",
98
+ categoryId: props.id,
99
+ defaultSearchCriteria: query,
100
+ });
101
+
102
+ const { search: categorySearch } = useCategorySearch();
103
+
104
+ const { data: category } = await useAsyncData(
105
+ `category${categoryId.value}`,
106
+ async () => {
107
+ return await categorySearch(categoryId.value);
108
+ },
109
+ );
110
+
111
+ const pageTitle = computed(
112
+ () =>
113
+ `${category.value?.translated.name ?? category.value?.name} | Speisekarte`,
114
+ );
115
+
116
+ useSeoMeta({
117
+ title: pageTitle,
118
+ robots: "index,follow",
119
+ });
120
+
121
+ const currentSorting = ref(getCurrentSortingOrder.value ?? "Sortieren");
122
+
123
+ const propertyFilters = computed<Schemas["PropertyGroup"][]>(() =>
124
+ getAvailableFilters.value?.filter(
125
+ (availableFilter) => availableFilter.code === "properties",
126
+ ),
127
+ );
128
+
129
+ const selectedPropertyFilters = ref(getCurrentFilters.value?.properties ?? []);
130
+ const selectedPropertyFiltersString = computed(() =>
131
+ selectedPropertyFilters.value?.join("|"),
132
+ );
133
+
134
+ const selectedListingFilters = computed<ShortcutFilterParam[]>(() => {
135
+ return [
136
+ {
137
+ code: "properties",
138
+ value: selectedPropertyFiltersString.value,
139
+ },
140
+ ];
141
+ });
142
+
142
143
  await useAsyncData(`listing${categoryId.value}`, async () => {
143
144
  await search(query);
144
145
  });
@@ -1,32 +1,26 @@
1
1
  <script setup lang="ts">
2
2
  import type { ButtonProps } from "#ui/components/Button.vue";
3
3
 
4
- const links = ref<ButtonProps[]>([
5
- {
6
- label: "Zur Speisekarte",
7
- color: "primary",
8
- to: "/speisekarte",
9
- },
10
- {
11
- label: "Tisch reservieren",
12
- variant: "outline",
13
- trailingIcon: "i-lucide-phone",
14
- to: "tel:+490610471427",
15
- },
16
- ]);
4
+ defineProps<{
5
+ title: string;
6
+ description?: string | undefined;
7
+ backgroundImage?: string | undefined;
8
+ links?: ButtonProps[] | undefined;
9
+ }>();
17
10
  </script>
18
11
 
19
12
  <template>
20
13
  <div class="relative overflow-hidden">
21
14
  <NuxtImg
22
- src="https://shopware.shopbite.de/media/f5/19/4a/1762546880/category-pizza-header.webp"
15
+ v-if="backgroundImage"
16
+ :src="backgroundImage"
23
17
  class="absolute inset-0 z-0 w-full h-full object-cover opacity-40"
24
18
  alt="CTA Background"
25
19
  />
26
20
  <UPageCTA
27
21
  variant="soft"
28
- title="Jetzt bestellen!"
29
- description="Genieße die italienische Küche, frisch zubereitet und direkt zu dir geliefert oder vor Ort genießen."
22
+ :title="title"
23
+ :description="description"
30
24
  :links="links"
31
25
  class="relative z-10"
32
26
  />
@@ -123,7 +123,7 @@ const mainIngredients = computed<Schemas["PropertyGroupOption"][]>(() => {
123
123
  </div>
124
124
  <UCollapsible v-model:open="openDetails" class="flex flex-col gap-2">
125
125
  <template #content>
126
- <ProductDetail :product="product" @product-added="toggleDetails" />
126
+ <ProductDetail2 :product="product" @product-added="toggleDetails" />
127
127
  </template>
128
128
  </UCollapsible>
129
129
  </template>
@@ -0,0 +1,65 @@
1
+ <script setup lang="ts">
2
+ import type { Schemas } from "#shopware";
3
+ import { useProductConfigurator } from "~/composables/useProductConfigurator";
4
+
5
+ const props = defineProps<{
6
+ p: Schemas["Product"];
7
+ c: Schemas["PropertyGroup"][];
8
+ }>();
9
+
10
+ const { p, c } = toRefs(props);
11
+ const { product, changeVariant, configurator } = useProduct(p, c);
12
+ const { findVariantForSelectedOptions } = useProductConfigurator();
13
+ const { variants: selectableOptions } = useProductVariantsZwei(configurator);
14
+
15
+ const selectedOptions = ref<Record<string, string>>({});
16
+
17
+ function initialOptions(variant: Ref<Schemas["Product"]>) {
18
+ const options = variant.value.options as Schemas["PropertyGroupOption"][];
19
+ for (const option of options) {
20
+ if (option.group && option.id) {
21
+ selectedOptions.value[option.group.id] = option.id;
22
+ }
23
+ }
24
+ }
25
+ onMounted(() => {
26
+ initialOptions(product);
27
+ });
28
+
29
+ watch(
30
+ selectedOptions.value,
31
+ async () => {
32
+ const foundVariant = await findVariantForSelectedOptions(
33
+ selectedOptions.value,
34
+ );
35
+
36
+ if (foundVariant) {
37
+ changeVariant(foundVariant);
38
+ emit("variant-switched", foundVariant);
39
+ }
40
+ },
41
+ { deep: true },
42
+ );
43
+
44
+ const emit = defineEmits<{
45
+ "variant-switched": [variant: Schemas["Product"]];
46
+ }>();
47
+ </script>
48
+ <template>
49
+ <div
50
+ v-for="(variantGroup, propertyGroupId) in selectableOptions"
51
+ :key="propertyGroupId"
52
+ class="my-6"
53
+ >
54
+ <div class="flex flex-row gap-2 items-center">
55
+ <div class="basis-1/3">{{ variantGroup.name }}:</div>
56
+ <USelect
57
+ v-model="selectedOptions[propertyGroupId]"
58
+ value-key="productId"
59
+ :items="variantGroup.options"
60
+ class="w-full"
61
+ icon="i-lucide-square-stack"
62
+ />
63
+ </div>
64
+ </div>
65
+ </template>
@@ -1,31 +1,22 @@
1
1
  <script setup lang="ts">
2
- import { onMounted, watch, computed, ref, toRefs } from "vue";
3
2
  import type { Schemas } from "#shopware";
4
3
  import type {
5
4
  AssociationItemProduct,
6
5
  AssociationItem,
7
6
  } from "~/types/Association";
7
+ import { useShopwareContext } from "#imports";
8
8
 
9
9
  const DEFAULT_CURRENCY = "EUR";
10
10
  const DEFAULT_LOCALE = "de-DE";
11
- const ASSOCIATION_CONTEXT = "cross-selling";
12
11
  const LOADING_ICON = "i-lucide-loader";
13
12
  const DEFAULT_ICON = "i-lucide-plus";
14
- const LOAD_ASSOCIATIONS_METHOD = "post";
15
13
 
16
14
  const props = defineProps<{
17
15
  product: Schemas["Product"];
18
16
  }>();
19
- const { product } = toRefs(props);
20
- const selectedExtras = ref<AssociationItemProduct[]>([]);
21
17
 
22
- const {
23
- loadAssociations,
24
- isLoading: isAssociationsLoading,
25
- productAssociations,
26
- } = useProductAssociations(product, {
27
- associationContext: ASSOCIATION_CONTEXT,
28
- });
18
+ const product = computed(() => props.product);
19
+ const selectedExtras = ref<AssociationItemProduct[]>([]);
29
20
 
30
21
  const { getFormattedPrice } = usePrice({
31
22
  currencyCode: DEFAULT_CURRENCY,
@@ -48,14 +39,24 @@ function mapAssociationToItems(
48
39
  }));
49
40
  }
50
41
 
42
+ const { apiClient } = useShopwareContext();
43
+
44
+ const { data: productAssociations, pending: isAssociationsLoading } =
45
+ useAsyncData(`cross-selling-${product.value.id}`, async () => {
46
+ const response = await apiClient.invoke(
47
+ "readProductCrossSellings post /product/{productId}/cross-selling",
48
+ {
49
+ pathParams: { productId: product.value.id },
50
+ },
51
+ );
52
+
53
+ return response.data;
54
+ });
55
+
51
56
  const associationItems = computed(() =>
52
- mapAssociationToItems(productAssociations.value),
57
+ mapAssociationToItems(productAssociations.value ?? []),
53
58
  );
54
59
 
55
- onMounted(() => {
56
- loadAssociations({ method: LOAD_ASSOCIATIONS_METHOD, searchParams: {} });
57
- });
58
-
59
60
  watch(selectedExtras, () => emit("extras-selected", selectedExtras.value), {
60
61
  deep: true,
61
62
  });
@@ -0,0 +1,100 @@
1
+ <script setup lang="ts">
2
+ import type { Schemas } from "#shopware";
3
+ import type { AssociationItemProduct } from "~/types/Association";
4
+
5
+ const props = defineProps<{
6
+ product: Schemas["Product"];
7
+ }>();
8
+
9
+ const { apiClient } = useShopwareContext();
10
+
11
+ const productId = toRef(props.product.id);
12
+ const { data: productDetails, pending } = useAsyncData(
13
+ () => `product-${productId.value ?? "none"}`,
14
+ async () => {
15
+ if (!productId.value) return null;
16
+ const response = await apiClient.invoke(
17
+ "readProductDetail post /product/{productId}",
18
+ {
19
+ pathParams: { productId: productId.value },
20
+ },
21
+ );
22
+ return response.data;
23
+ },
24
+ );
25
+
26
+ const {
27
+ selectedProduct,
28
+ selectedQuantity,
29
+ addToCart,
30
+ setSelectedProduct,
31
+ setSelectedExtras,
32
+ setDeselectedIngredients,
33
+ } = useAddToCart();
34
+
35
+ // Initialize when productDetails is loaded
36
+ watch(
37
+ () => productDetails.value?.product,
38
+ (product) => {
39
+ if (product) {
40
+ setSelectedProduct(product);
41
+ }
42
+ },
43
+ { immediate: true },
44
+ );
45
+
46
+ const onVariantSwitched = (variant: Schemas["Product"]) => {
47
+ setSelectedProduct(variant);
48
+ };
49
+
50
+ const onExtrasSelected = (extras: AssociationItemProduct[]) => {
51
+ setSelectedExtras(extras);
52
+ };
53
+
54
+ const onIngredientsDeselected = (deselected: string[]) => {
55
+ setDeselectedIngredients(deselected);
56
+ };
57
+
58
+ const onAddToCart = () => emit("product-added");
59
+
60
+ const emit = defineEmits(["product-added"]);
61
+ </script>
62
+
63
+ <template>
64
+ <div v-if="!pending">
65
+ <div v-if="productDetails?.configurator">
66
+ <ProductConfigurator2
67
+ v-if="productDetails?.configurator"
68
+ :p="product"
69
+ :c="productDetails.configurator"
70
+ @variant-switched="onVariantSwitched"
71
+ />
72
+ <ProductCrossSelling
73
+ v-if="selectedProduct"
74
+ :product="selectedProduct"
75
+ @extras-selected="onExtrasSelected"
76
+ />
77
+ <ProductDeselectIngredient
78
+ v-if="selectedProduct"
79
+ :product="selectedProduct"
80
+ @ingredients-deselected="onIngredientsDeselected"
81
+ />
82
+ </div>
83
+ <div class="flex flex-row gap-4 mt-8">
84
+ <UInputNumber
85
+ v-model="selectedQuantity"
86
+ size="xl"
87
+ placeholder="Anzahl"
88
+ :min="1"
89
+ :max="100"
90
+ />
91
+ <UButton
92
+ size="xl"
93
+ label="In den Warenkorb"
94
+ icon="i-lucide-shopping-cart"
95
+ block
96
+ @click="addToCart(onAddToCart)"
97
+ />
98
+ </div>
99
+ </div>
100
+ </template>
@@ -61,7 +61,6 @@ const toast = useToast();
61
61
  async function onSubmit(event: FormSubmitEvent<RegistrationSchema>) {
62
62
  const registrationData = { ...event.data };
63
63
 
64
- console.log(registrationData);
65
64
  if (
66
65
  !registrationData.billingAddress.firstName &&
67
66
  registrationData.firstName
@@ -0,0 +1,174 @@
1
+ import type { Schemas, operations } from "#shopware";
2
+ import type { AssociationItemProduct } from "~/types/Association";
3
+ import { v5 as uuidv5 } from "uuid";
4
+ import { computed, ref } from "vue";
5
+
6
+ const UUID_NAMESPACE = "b098ef7e-0fa2-4073-b002-7ceec4360fbf";
7
+ const CART_SUCCESS_TITLE = "Gute Wahl!";
8
+ const LINE_ITEM_PRODUCT = "product";
9
+ const LINE_ITEM_CONTAINER = "container";
10
+
11
+ export function useAddToCart() {
12
+ const { addProducts, refreshCart } = useCart();
13
+ const toast = useToast();
14
+ const { triggerProductAdded } = useProductEvents();
15
+
16
+ const selectedExtras = ref<AssociationItemProduct[]>([]);
17
+ const deselectedIngredients = ref<string[]>([]);
18
+ const selectedQuantity = ref(1);
19
+ const selectedProduct = ref<Schemas["Product"] | null>(null);
20
+
21
+ const cartItemLabel = computed(() => {
22
+ if (!selectedProduct.value) return "";
23
+
24
+ const formatIngredientModifications = (
25
+ items: Array<{ label: string } | string>,
26
+ prefix: string,
27
+ ): string => {
28
+ if (!items.length) return "";
29
+
30
+ const labels = items.map((item) =>
31
+ typeof item === "string" ? item : item.label,
32
+ );
33
+ const separator = " " + prefix;
34
+ return " " + prefix + labels.join(separator);
35
+ };
36
+
37
+ const extrasFormatted = formatIngredientModifications(
38
+ selectedExtras.value,
39
+ "+",
40
+ );
41
+ const removedFormatted = formatIngredientModifications(
42
+ deselectedIngredients.value,
43
+ "-",
44
+ );
45
+
46
+ console.log(
47
+ selectedProduct.value.translated.name,
48
+ extrasFormatted,
49
+ removedFormatted,
50
+ );
51
+
52
+ return `${selectedProduct.value.translated.name}${extrasFormatted}${removedFormatted}`;
53
+ });
54
+
55
+ const createExtras = () =>
56
+ selectedExtras.value.map((extra) => ({
57
+ id: extra.value,
58
+ type: LINE_ITEM_PRODUCT,
59
+ quantity: selectedQuantity.value,
60
+ }));
61
+
62
+ const generateSortedExtrasString = (extras: AssociationItemProduct[]) =>
63
+ extras
64
+ .map((extra) => extra.value)
65
+ .sort()
66
+ .join("");
67
+
68
+ const generateProductId = (
69
+ baseId: string,
70
+ extras: AssociationItemProduct[],
71
+ ) => (extras.length ? baseId + generateSortedExtrasString(extras) : baseId);
72
+
73
+ function createCartItems(): operations["addLineItem post /checkout/cart/line-item"]["body"]["items"] {
74
+ if (!selectedProduct.value) return [];
75
+
76
+ const extras = createExtras();
77
+
78
+ // Simple product when no extras
79
+ if (extras.length === 0 && deselectedIngredients.value.length === 0) {
80
+ return [
81
+ {
82
+ id: selectedProduct.value.id,
83
+ quantity: selectedQuantity.value,
84
+ type: LINE_ITEM_PRODUCT,
85
+ },
86
+ ];
87
+ }
88
+
89
+ // Container product when extras are selected
90
+ const generatedUuid = uuidv5(
91
+ generateProductId(selectedProduct.value.id, selectedExtras.value),
92
+ UUID_NAMESPACE,
93
+ );
94
+
95
+ return [
96
+ {
97
+ id: generatedUuid,
98
+ quantity: selectedQuantity.value,
99
+ type: LINE_ITEM_CONTAINER,
100
+ label: cartItemLabel.value,
101
+ payload: {
102
+ productNumber: selectedProduct.value.productNumber,
103
+ },
104
+ children: [
105
+ {
106
+ id: selectedProduct.value.id,
107
+ quantity: selectedQuantity.value,
108
+ type: LINE_ITEM_PRODUCT,
109
+ },
110
+ ...extras,
111
+ ],
112
+ },
113
+ ];
114
+ }
115
+
116
+ async function showSuccessToast() {
117
+ if (!selectedProduct.value) return;
118
+
119
+ toast.add({
120
+ title: CART_SUCCESS_TITLE,
121
+ description: `${selectedProduct.value.translated.name} wurde in den Warenkorb gelegt.`,
122
+ icon: "i-lucide-shopping-cart",
123
+ color: "primary",
124
+ progress: true,
125
+ duration: 2000,
126
+ });
127
+ }
128
+
129
+ async function addToCart(onSuccess?: () => void) {
130
+ if (!selectedProduct.value) return;
131
+
132
+ const cartItems = createCartItems();
133
+ const newCart = await addProducts(cartItems);
134
+ await refreshCart(newCart);
135
+ await showSuccessToast();
136
+
137
+ triggerProductAdded();
138
+
139
+ useTrackEvent("add_to_cart", {
140
+ props: {
141
+ product_number: selectedProduct.value.productNumber,
142
+ quantity: selectedQuantity.value,
143
+ },
144
+ });
145
+
146
+ if (onSuccess) {
147
+ onSuccess();
148
+ }
149
+ }
150
+
151
+ function setSelectedProduct(product: Schemas["Product"]) {
152
+ selectedProduct.value = product;
153
+ }
154
+
155
+ function setSelectedExtras(extras: AssociationItemProduct[]) {
156
+ selectedExtras.value = extras;
157
+ }
158
+
159
+ function setDeselectedIngredients(ingredients: string[]) {
160
+ deselectedIngredients.value = ingredients;
161
+ }
162
+
163
+ return {
164
+ selectedProduct,
165
+ selectedExtras,
166
+ deselectedIngredients,
167
+ selectedQuantity,
168
+ cartItemLabel,
169
+ addToCart,
170
+ setSelectedProduct,
171
+ setSelectedExtras,
172
+ setDeselectedIngredients,
173
+ };
174
+ }
@@ -0,0 +1,107 @@
1
+ import { computed, ref } from "vue";
2
+ import type { Schemas } from "#shopware";
3
+ import {
4
+ useShopwareContext,
5
+ useProductConfigurator as useProductConfiguratorOriginal,
6
+ useProduct,
7
+ } from "@shopware/composables";
8
+ import { getTranslatedProperty } from "@shopware/helpers";
9
+
10
+ export function useProductConfigurator() {
11
+ const { apiClient } = useShopwareContext();
12
+
13
+ const original = useProductConfiguratorOriginal();
14
+
15
+ const { configurator, product } = useProduct();
16
+
17
+ const selected = ref<{
18
+ [key: string]: string;
19
+ }>({});
20
+ const isLoadingOptions = ref(!!product.value.options?.length);
21
+ const parentProductId = computed(() => product.value?.parentId);
22
+ const getOptionGroups = computed<Schemas["PropertyGroup"][]>(() => {
23
+ return configurator.value || [];
24
+ });
25
+
26
+ const findGroupCodeForOption = (optionId: string) => {
27
+ const group = getOptionGroups.value.find((optionGroup) => {
28
+ const optionFound = optionGroup.options?.find((option) => {
29
+ return option.id === optionId;
30
+ });
31
+ return !!optionFound;
32
+ });
33
+
34
+ return getTranslatedProperty(group, "name");
35
+ };
36
+
37
+ // create a group -> optionId map
38
+ for (const optionId of product.value.optionIds || []) {
39
+ const optionGroupCode = findGroupCodeForOption(optionId);
40
+ if (optionGroupCode) {
41
+ selected.value[optionGroupCode] = optionId;
42
+ }
43
+ }
44
+
45
+ async function findVariantForSelectedOptions(options?: {
46
+ [code: string]: string;
47
+ }): Promise<Schemas["Product"] | undefined> {
48
+ const filter: Schemas["Filters"] = [
49
+ {
50
+ type: "equals",
51
+ field: "parentId",
52
+ value: parentProductId.value as string,
53
+ },
54
+ ...Object.values(options || selected.value).map(
55
+ (id) =>
56
+ ({
57
+ type: "equals",
58
+ field: "optionIds",
59
+ value: id,
60
+ }) as Schemas["EqualsFilter"],
61
+ ),
62
+ ];
63
+ try {
64
+ const response = await apiClient.invoke("readProduct post /product", {
65
+ body: {
66
+ filter,
67
+ limit: 1,
68
+ includes: {
69
+ product: [
70
+ "id",
71
+ "name",
72
+ "translated",
73
+ "productNumber",
74
+ "options",
75
+ "properties",
76
+ ],
77
+ product_option: ["id", "groupId", "name", "translated", "group"],
78
+ property: ["id", "name", "translated", "options"],
79
+ property_group_option: ["id", "name", "translated", "group"],
80
+ },
81
+ associations: {
82
+ options: {
83
+ associations: {
84
+ group: {},
85
+ },
86
+ },
87
+ properties: {
88
+ associations: {
89
+ group: {},
90
+ },
91
+ },
92
+ },
93
+ },
94
+ });
95
+ return response.data.elements?.[0]; // return first matching product
96
+ } catch (e) {
97
+ console.error("SwProductDetails:findVariantForSelectedOptions", e);
98
+ return undefined;
99
+ }
100
+ }
101
+
102
+ return {
103
+ ...original,
104
+ findVariantForSelectedOptions,
105
+ isLoadingOptions,
106
+ };
107
+ }
@@ -0,0 +1,62 @@
1
+ import { computed, type ComputedRef, type Ref } from "vue";
2
+ import type { Schemas } from "#shopware";
3
+
4
+ // Define the SelectItem interface to match what USelect expects
5
+ interface SelectItem {
6
+ label: string;
7
+ value: string;
8
+ productId: string;
9
+ }
10
+
11
+ type VariantOptions = {
12
+ name: string;
13
+ options: SelectItem[];
14
+ };
15
+
16
+ type useProductVariantsReturn = {
17
+ variants: ComputedRef<Record<string, VariantOptions>>;
18
+ };
19
+
20
+ // Accept configuratorSettings as a Ref of PropertyGroup[]
21
+ export function useProductVariantsZwei(
22
+ configuratorSettings: Ref<Schemas["PropertyGroup"][]>,
23
+ ): useProductVariantsReturn {
24
+ const variants = computed(() => {
25
+ const groups = configuratorSettings.value || [];
26
+ const result: Record<string, VariantOptions> = {};
27
+
28
+ for (const group of groups) {
29
+ const groupId = group.id;
30
+ const groupName = group.translated?.name || group.name;
31
+ const options = group.options || [];
32
+
33
+ if (!groupId || !groupName) continue;
34
+
35
+ result[groupId] ??= {
36
+ name: groupName,
37
+ options: [],
38
+ };
39
+
40
+ for (const opt of options) {
41
+ const optionId = opt.id;
42
+ const optionName = opt.translated?.name || opt.name;
43
+ if (!optionId || !optionName) continue;
44
+
45
+ const exists = result[groupId].options.find(
46
+ (o) => o.value === optionId,
47
+ );
48
+ if (!exists) {
49
+ result[groupId].options.push({
50
+ label: optionName,
51
+ value: optionId,
52
+ productId: optionId,
53
+ });
54
+ }
55
+ }
56
+ }
57
+
58
+ return result;
59
+ });
60
+
61
+ return { variants };
62
+ }
package/app/error.vue CHANGED
@@ -5,12 +5,18 @@ const props = defineProps<{
5
5
  error: NuxtError;
6
6
  }>();
7
7
 
8
+ const {
9
+ public: { site },
10
+ } = useRuntimeConfig();
11
+
12
+ const pageTitle = computed(() => `${statusCode} | ${site?.name}`);
13
+
8
14
  console.error(props.error);
9
15
 
10
16
  const statusCode = props.error.statusCode;
11
17
 
12
18
  useSeoMeta({
13
- title: `${statusCode} | Pizzeria La Fattoria`,
19
+ title: pageTitle,
14
20
  });
15
21
  </script>
16
22
 
@@ -54,6 +54,11 @@ useSeoMeta({
54
54
  :images="page.gallery.images"
55
55
  :links="page.gallery.links"
56
56
  />
57
- <Cta />
57
+ <Cta
58
+ :title="page.cta.title"
59
+ :description="page.cta.description"
60
+ :background-image="page.cta.backgroundImage"
61
+ :links="page.cta.links"
62
+ />
58
63
  </div>
59
64
  </template>
@@ -1,6 +1,12 @@
1
1
  <script setup lang="ts">
2
+ const {
3
+ public: { site },
4
+ } = useRuntimeConfig();
5
+
6
+ const pageTitle = computed(() => `Merkliste | ${site?.name}`);
7
+
2
8
  useSeoMeta({
3
- title: "Merkliste | Pizzeria La Fattoria",
9
+ title: pageTitle,
4
10
  });
5
11
  </script>
6
12
 
package/content/index.yml CHANGED
@@ -1,17 +1,17 @@
1
1
  seo:
2
2
  title: ShopBite – Kostenloses Online-Bestellsystem für Gastronomie
3
3
  description: Dein eigenes Bestellsystem ohne Provisionen, ohne monatliche Kosten – 100% Open Source und individuell anpassbar. Perfekt für Pizzerien, Imbisse und Lieferdienste.
4
- title: Pizzeria La Fattoria
5
- description: Italienisch-deutsche Küche in Obertshausen seit 1997.
4
+ title: ShopBite
5
+ description: Reduziere Kosten und steigere deinen Umsatz
6
6
  hero:
7
7
  backgroundVideo: https://shopware.shopbite.de/media/10/59/96/1762465181/background.mp4
8
- headline: ALTE SCHMIEDE
8
+ headline: SHOPBITE
9
9
  usps:
10
10
  - title: 4.5 ⭐
11
11
  subtitle: 720+ Reviews
12
12
  icon: i-simple-icons-google
13
- link: https://www.google.com/maps/place/?q=place_id:ChIJJ-6VNfcTvUcR2r7Sax3SN2s
14
- - title: 28 Jahre
13
+ link: https://www.google.com/maps/place/?q=place_id
14
+ - title: 20 Jahre
15
15
  subtitle: in Obertshausen
16
16
  icon: i-lucide-award
17
17
  - title: Lieferservice
@@ -29,12 +29,12 @@ hero:
29
29
  size: xl
30
30
  color: neutral
31
31
  variant: outline
32
- to: 'tel:+490610474127'
32
+ to: 'tel:+490610477777'
33
33
  target: _blank
34
34
  features:
35
35
  title: Alle [Informationen]{.text-primary} auf einen Blick
36
36
  description: Sie können Speisen und Getränke abholen, liefern lassen, oder vor Ort in unserem Restaurant genießen.
37
- headline: ALTE SCHMIEDE
37
+ headline: SHOPBITE
38
38
  features:
39
39
  - title: Öffnungszeiten
40
40
  description: Von 11:30 - 14:30 und 17:30 - 23:00 Uhr. Samstags von 17:30 - 23:30 Uhr. Dienstag ist Ruhetag.
@@ -54,7 +54,7 @@ features:
54
54
  marquee:
55
55
  title: Das lieben unsere Kunden
56
56
  description: Kundenbilder
57
- headline: ALTE SCHMIEDE
57
+ headline: SHOPBITE
58
58
  items:
59
59
  - productId: 019a4f2e717b7df7a349761a56c43ac3
60
60
  image: https://shopware.shopbite.de/media/e4/82/55/1762465420/72.webp
@@ -67,7 +67,7 @@ marquee:
67
67
  gallery:
68
68
  title: Restaurant
69
69
  description: Genießen Sie leckeres Essen in einem rustikalen und gemütlichen Ambiente.
70
- headline: ALTE SCHMIEDE
70
+ headline: SHOPBITE
71
71
  links:
72
72
  - label: Tisch reservieren
73
73
  to: tel:+49610471427
@@ -89,3 +89,15 @@ gallery:
89
89
  alt: La Fattoria Restaurant Innenbereich 6
90
90
  - image: https://nbg1.your-objectstorage.com/lafattoria-public/media/30/f7/76/1763383122/restaurant10.webp
91
91
  alt: La Fattoria Restaurant Innenbereich 10
92
+ cta:
93
+ title: Jetzt bestellen!
94
+ description: Genieße die italienische Küche, frisch zubereitet und direkt zu dir geliefert oder vor Ort genießen.
95
+ backgroundImage: https://shopware.shopbite.de/media/f5/19/4a/1762546880/category-pizza-header.webp
96
+ links:
97
+ - label: Zur Speisekarte
98
+ to: /speisekarte
99
+ color: primary
100
+ - label: Tisch reservieren
101
+ to: tel:+49610471427
102
+ variant: outline
103
+ trailingIcon: i-lucide-phone
@@ -2,25 +2,21 @@
2
2
 
3
3
  ## Angaben gemäß § 5 TMG
4
4
 
5
- Bekim Veliu und Cefajet Veliu GbR
5
+ Lirim Veliu<br>
6
+ Hausener Straße 31<br>
7
+ 63165 Mühlheim am Main<br>
8
+ Deutschland
6
9
 
7
- Kantstr. 6<br>63179 Obertshausen<br>Deutschland
8
-
9
- **Vertreten durch:**<br>Bekim Veliu
10
-
11
- **Kontakt:**<br>Telefon: +49 6104 71427<br>E-Mail: service@pizzeria-lafattoria.de
12
-
13
- ## Umsatzsteuer-ID
14
-
15
- Umsatzsteuer-Identifikationsnummer gemäß § 27 a Umsatzsteuergesetz:
10
+ lirim@veliu.net
16
11
 
17
12
  ## Verantwortlich für den Inhalt nach § 55 Abs. 2 RStV
18
13
 
19
- Bekim Veliu<br>Kantstr. 6<br>63179 Obertshausen
20
-
21
- ## Administrator und technischer Ansprechpartner dieser Seite
14
+ Lirim Veliu<br>
15
+ Hausener Straße 31<br>
16
+ 63165 Mühlheim am Main<br>
17
+ Deutschland
22
18
 
23
- Lirim Veliu<br>lirim@veliu.net
19
+ lirim@veliu.net
24
20
 
25
21
  ## Haftungshinweis
26
22
 
package/content.config.ts CHANGED
@@ -10,6 +10,7 @@ const createLinkSchema = () =>
10
10
  icon: z.string().optional().editor({ input: "icon" }),
11
11
  size: createEnum(["xs", "sm", "md", "lg", "xl"]),
12
12
  trailing: z.boolean().optional(),
13
+ trailingIcon: z.string().optional().editor({ input: "icon" }),
13
14
  target: createEnum(["_blank", "_self"]),
14
15
  color: createEnum([
15
16
  "primary",
@@ -107,6 +108,10 @@ export default defineContentConfig({
107
108
  ),
108
109
  links: z.array(createLinkSchema()),
109
110
  }),
111
+ cta: createBaseSchema().extend({
112
+ links: z.array(createLinkSchema()),
113
+ backgroundImage: z.string().optional(),
114
+ }),
110
115
  }),
111
116
  }),
112
117
  navigation: defineCollection({
package/nuxt.config.ts CHANGED
@@ -47,8 +47,8 @@ export default defineNuxtConfig({
47
47
  apiClientConfig: {},
48
48
  public: {
49
49
  site: {
50
- name: "Pizzeria La Fattoria",
51
- description: "Italienisch-deutsche Küche in Obertshausen",
50
+ name: "ShopBite",
51
+ description: "Reduziere deine Kosten und steigere deinen Umsatz",
52
52
  countryId: "",
53
53
  },
54
54
  storeUrl: "",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shopbite-de/storefront",
3
- "version": "1.1.1",
3
+ "version": "1.2.1",
4
4
  "main": "nuxt.config.ts",
5
5
  "description": "Shopware storefront for food delivery shops",
6
6
  "keywords": [