@shopbite-de/storefront 1.16.1 → 1.17.0

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.
Files changed (54) hide show
  1. package/.claude/settings.local.json +14 -1
  2. package/.env.example +0 -4
  3. package/.github/workflows/build.yaml +14 -8
  4. package/.github/workflows/ci.yaml +125 -42
  5. package/.nuxtrc +1 -1
  6. package/CLAUDE.md +106 -0
  7. package/app/components/Address/Form.vue +5 -2
  8. package/app/components/Category/Listing.vue +5 -4
  9. package/app/components/Checkout/DeliveryTimeSelect.vue +4 -2
  10. package/app/components/Contact/Form.vue +3 -2
  11. package/app/components/Hero.vue +1 -1
  12. package/app/components/ImageGallery.vue +1 -1
  13. package/app/components/Navigation/DesktopLeft.vue +2 -1
  14. package/app/components/Order/Detail.vue +2 -2
  15. package/app/components/Product/Card.vue +14 -8
  16. package/app/components/SalesChannelSwitch.vue +5 -3
  17. package/app/components/User/RegistrationForm.vue +5 -2
  18. package/app/composables/useBusinessHours.ts +11 -4
  19. package/app/composables/useCategory.ts +1 -0
  20. package/app/composables/useTopSellers.ts +2 -2
  21. package/app/pages/c/[...all].vue +2 -1
  22. package/app/pages/konto/adressen.vue +4 -1
  23. package/app/pages/konto/bestellung/[id].vue +14 -2
  24. package/app/pages/konto/profil.vue +5 -1
  25. package/app/utils/formatDate.ts +2 -1
  26. package/content.config.ts +1 -2
  27. package/eslint.config.mjs +10 -6
  28. package/node.dockerfile +7 -6
  29. package/nuxt.config.ts +16 -3
  30. package/package.json +38 -32
  31. package/renovate.json +9 -1
  32. package/server/api/address/autocomplete.get.ts +3 -2
  33. package/test/e2e/simple-checkout-as-recurring-customer.test.ts +1 -1
  34. package/test/nuxt/AddressFields.test.ts +12 -3
  35. package/test/nuxt/ContactForm.test.ts +31 -11
  36. package/test/nuxt/HeaderRight.test.ts +15 -6
  37. package/test/nuxt/LoginForm.test.ts +16 -8
  38. package/test/nuxt/PaymentAndDelivery.test.ts +2 -2
  39. package/test/nuxt/RegistrationForm.test.ts +6 -3
  40. package/test/nuxt/registrationSchema.test.ts +8 -8
  41. package/test/nuxt/useAddToCart.test.ts +5 -4
  42. package/test/nuxt/useAddressAutocomplete.test.ts +2 -0
  43. package/test/nuxt/useBusinessHours.test.ts +5 -5
  44. package/test/nuxt/useDeliveryTime.test.ts +17 -9
  45. package/test/nuxt/useProductConfigurator.test.ts +6 -4
  46. package/test/nuxt/useProductVariants.test.ts +16 -10
  47. package/test/nuxt/useProductVariantsZwei.test.ts +6 -2
  48. package/test/nuxt/useScrollAnimation.test.ts +16 -13
  49. package/test/nuxt/useTopSellers.test.ts +2 -2
  50. package/test/nuxt/useWishlistActions.test.ts +4 -3
  51. package/test/unit/useCategorySeo.spec.ts +26 -12
  52. package/tsconfig.json +21 -1
  53. package/server/utils/shopware/adminApiClient.ts +0 -24
  54. package/test/unit/sales-channels.test.ts +0 -66
@@ -2,7 +2,20 @@
2
2
  "permissions": {
3
3
  "allow": [
4
4
  "Bash(find:*)",
5
- "Bash(grep:*)"
5
+ "Bash(grep:*)",
6
+ "Bash(pnpm add:*)",
7
+ "Bash(pnpm nuxt:*)",
8
+ "Bash(ls /home/lirim/workspace/shopbite/storefront/.nuxt/eslint* 2>&1)",
9
+ "Bash(pnpm typecheck:*)",
10
+ "Bash(git stash:*)",
11
+ "Bash(ls node_modules/.bin/vue-tsc 2>&1 && node_modules/.bin/vue-tsc --version 2>&1)",
12
+ "Bash(cat /home/lirim/workspace/shopbite/storefront/node_modules/.pnpm/nuxt@4.2.2_*/node_modules/nuxt/package.json 2>/dev/null | python3 -c \"import json,sys; p=json.load\\(sys.stdin\\); deps={**p.get\\('dependencies',{}\\),**p.get\\('devDependencies',{}\\),**p.get\\('peerDependencies',{}\\)}; print\\({k:v for k,v in deps.items\\(\\) if 'tsc' in k or 'volar' in k or 'vue-tsc' in k}\\)\" 2>/dev/null | head -5)",
13
+ "Bash(find /home/lirim/workspace/shopbite/storefront/node_modules/.pnpm -path \"*/nuxt@4.2.2*/nuxt/package.json\" 2>/dev/null | head -2 | xargs -I{} python3 -c \"import json,sys; p=json.load\\(open\\('{}',\\)\\); all_deps={**p.get\\('dependencies',{}\\),**p.get\\('devDependencies',{}\\),**p.get\\('optionalDependencies',{}\\)}; [print\\(k,v\\) for k,v in all_deps.items\\(\\) if any\\(x in k for x in ['tsc','volar','typescript']\\)]\" 2>/dev/null)",
14
+ "Bash(pnpm run:*)",
15
+ "Bash(git log:*)",
16
+ "Bash(pnpm test:unit 2>&1 | tail -80)",
17
+ "Bash(pnpm test:unit 2>&1 | grep -E \"\\(FAIL|PASS|TypeError|ContextError|error\\)\" | head -30)",
18
+ "Bash(pnpm test:unit 2>&1 | tail -30)"
6
19
  ]
7
20
  }
8
21
  }
package/.env.example CHANGED
@@ -7,10 +7,6 @@ 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
10
  NUXT_PUBLIC_SHOP_BITE_FEATURE_MULTI_CHANNEL=false
15
11
  NUXT_PUBLIC_SHOP_BITE_FEATURE_SECURE_KEY=
16
12
 
@@ -14,8 +14,11 @@ jobs:
14
14
  - name: Checkout repository
15
15
  uses: actions/checkout@v6
16
16
 
17
+ - name: Set up Docker Buildx
18
+ uses: docker/setup-buildx-action@v4
19
+
17
20
  - name: Log in to the Container registry
18
- uses: docker/login-action@v3
21
+ uses: docker/login-action@v4
19
22
  with:
20
23
  registry: ghcr.io
21
24
  username: ${{ github.actor }}
@@ -23,7 +26,7 @@ jobs:
23
26
 
24
27
  - name: Extract metadata (tags, labels) for app image
25
28
  id: meta-app
26
- uses: docker/metadata-action@v5
29
+ uses: docker/metadata-action@v6
27
30
  with:
28
31
  images: ghcr.io/${{ github.repository }}-app
29
32
  tags: |
@@ -35,7 +38,7 @@ jobs:
35
38
  type=raw,value=latest,enable=${{ github.event_name == 'release' }}
36
39
 
37
40
  - name: Build and push Docker app image
38
- uses: docker/build-push-action@v6
41
+ uses: docker/build-push-action@v7
39
42
  with:
40
43
  context: .
41
44
  file: ./node.dockerfile
@@ -46,6 +49,8 @@ jobs:
46
49
  push: true
47
50
  tags: ${{ steps.meta-app.outputs.tags }}
48
51
  labels: ${{ steps.meta-app.outputs.labels }}
52
+ cache-from: type=gha
53
+ cache-to: type=gha,mode=max
49
54
 
50
55
  release-npm:
51
56
  runs-on: ubuntu-latest
@@ -57,17 +62,18 @@ jobs:
57
62
  - name: Checkout repository
58
63
  uses: actions/checkout@v6
59
64
 
65
+ - name: Install pnpm
66
+ uses: pnpm/action-setup@v4
67
+ with:
68
+ version: 10.32.1
69
+
60
70
  - name: Install Node.js
61
71
  uses: actions/setup-node@v6
62
72
  with:
63
73
  node-version: '24'
74
+ cache: 'pnpm'
64
75
  registry-url: 'https://registry.npmjs.org'
65
76
 
66
- - name: Install pnpm
67
- uses: pnpm/action-setup@v4
68
- with:
69
- version: 10.30.1
70
-
71
77
  - name: Install dependencies
72
78
  run: pnpm install --frozen-lockfile
73
79
 
@@ -4,56 +4,39 @@ on:
4
4
  branches: ["main", "dev"]
5
5
  pull_request:
6
6
  types: [opened, synchronize]
7
+
8
+ env:
9
+ NUXT_PUBLIC_APP_ENV: 'test'
10
+ NUXT_PUBLIC_STORE_URL: 'http://localhost:3005'
11
+ NUXT_PUBLIC_SHOPWARE_DEV_STORE_FRONT_URL: 'http://localhost:3005'
12
+ PORT: '3005'
13
+ NUXT_PUBLIC_SHOPWARE_ENDPOINT: ${{ secrets.NUXT_PUBLIC_SHOPWARE_ENDPOINT }}
14
+ NUXT_PUBLIC_SHOPWARE_ACCESS_TOKEN: ${{ secrets.NUXT_PUBLIC_SHOPWARE_ACCESS_TOKEN }}
15
+ NUXT_PUBLIC_SITE_COUNTRY_ID: ${{ secrets.NUXT_PUBLIC_SITE_COUNTRY_ID }}
16
+ GEOPIFY: ${{ secrets.GEOPIFY }}
17
+
7
18
  jobs:
8
- test:
9
- name: Run unit and e2e tests
10
- environment: test
11
- env:
12
- NUXT_PUBLIC_APP_ENV: 'test'
13
- NUXT_PUBLIC_STORE_URL: 'http://localhost:3005'
14
- NUXT_PUBLIC_SHOPWARE_DEV_STORE_FRONT_URL: 'http://localhost:3005'
15
- PORT: '3005'
16
- NUXT_PUBLIC_SHOPWARE_ENDPOINT: ${{ secrets.NUXT_PUBLIC_SHOPWARE_ENDPOINT }}
17
- NUXT_PUBLIC_SHOPWARE_ACCESS_TOKEN: ${{ secrets.NUXT_PUBLIC_SHOPWARE_ACCESS_TOKEN }}
18
- NUXT_PUBLIC_SITE_COUNTRY_ID: ${{ secrets.NUXT_PUBLIC_SITE_COUNTRY_ID }}
19
- GEOPIFY: ${{ secrets.GEOPIFY }}
20
- TEST_USER: ${{ secrets.TEST_USER }}
21
- TEST_USER_PASS: ${{ secrets.TEST_USER_PASS }}
19
+ setup:
20
+ name: Install & Build
22
21
  if: github.event.pull_request.draft == false
23
- timeout-minutes: 15
24
22
  runs-on: ubuntu-latest
25
23
  steps:
26
- - name: Check out code
27
- uses: actions/checkout@v6
24
+ - uses: actions/checkout@v6
28
25
 
29
- - name: Fetch main
30
- run: git fetch origin main
26
+ - uses: pnpm/action-setup@v4
27
+ with:
28
+ version: 10.32.1
31
29
 
32
- - name: Install Node.js
33
- uses: actions/setup-node@v6
30
+ - uses: actions/setup-node@v6
34
31
  with:
35
32
  node-version: '24'
36
-
37
- - name: Enable corepack and pnpm
38
- run: |
39
- corepack enable
40
- corepack prepare pnpm@10.26.2 --activate
33
+ cache: 'pnpm'
41
34
 
42
35
  - name: Install build dependencies
43
36
  run: |
44
37
  sudo apt-get update
45
38
  sudo apt-get install -y build-essential python3
46
39
 
47
- - name: Cache pnpm dependencies
48
- uses: actions/cache@v5
49
- with:
50
- path: |
51
- /home/runner/.local/share/pnpm/store/v3
52
- ./node_modules
53
- key: ${{ runner.os }}-pnpm-${{ inputs.workdir }}-${{ hashFiles('**/pnpm-lock.yaml') }}
54
- restore-keys: |
55
- ${{ runner.os }}-pnpm-
56
-
57
40
  - name: Install dependencies
58
41
  run: pnpm install --frozen-lockfile --prefer-offline
59
42
 
@@ -65,24 +48,124 @@ jobs:
65
48
  NODE_OPTIONS: "--max_old_space_size=4096"
66
49
  run: pnpm run build
67
50
 
68
- - name: Install Playwright Browsers
69
- run: pnpm exec playwright install --with-deps chromium
51
+ - name: Save workspace to cache
52
+ uses: actions/cache/save@v5
53
+ with:
54
+ path: |
55
+ ~/.local/share/pnpm/store
56
+ node_modules
57
+ .nuxt
58
+ .output
59
+ key: workspace-${{ runner.os }}-${{ github.sha }}
60
+
61
+ lint:
62
+ name: Lint & format
63
+ environment: test
64
+ needs: [setup]
65
+ timeout-minutes: 5
66
+ runs-on: ubuntu-latest
67
+ steps:
68
+ - uses: actions/checkout@v6
69
+
70
+ - uses: pnpm/action-setup@v4
71
+ with:
72
+ version: 10.32.1
73
+
74
+ - uses: actions/setup-node@v6
75
+ with:
76
+ node-version: '24'
77
+
78
+ - name: Restore workspace from cache
79
+ uses: actions/cache/restore@v5
80
+ with:
81
+ path: |
82
+ ~/.local/share/pnpm/store
83
+ node_modules
84
+ .nuxt
85
+ .output
86
+ key: workspace-${{ runner.os }}-${{ github.sha }}
87
+
88
+ - name: Prettier
89
+ run: pnpm prettier
90
+
91
+ - name: ESLint
92
+ run: pnpm eslint
93
+
94
+ unit-test:
95
+ name: Unit tests
96
+ environment: test
97
+ needs: [setup]
98
+ timeout-minutes: 10
99
+ runs-on: ubuntu-latest
100
+ steps:
101
+ - uses: actions/checkout@v6
102
+
103
+ - uses: pnpm/action-setup@v4
104
+ with:
105
+ version: 10.32.1
106
+
107
+ - uses: actions/setup-node@v6
108
+ with:
109
+ node-version: '24'
110
+
111
+ - name: Restore workspace from cache
112
+ uses: actions/cache/restore@v5
113
+ with:
114
+ path: |
115
+ ~/.local/share/pnpm/store
116
+ node_modules
117
+ .nuxt
118
+ .output
119
+ key: workspace-${{ runner.os }}-${{ github.sha }}
70
120
 
71
121
  - name: Unit tests
72
122
  run: pnpm test:unit
73
123
 
124
+ e2e-test:
125
+ name: E2E tests
126
+ environment: test
127
+ needs: [setup]
128
+ timeout-minutes: 15
129
+ runs-on: ubuntu-latest
130
+ steps:
131
+ - uses: actions/checkout@v6
132
+
133
+ - uses: pnpm/action-setup@v4
134
+ with:
135
+ version: 10.32.1
136
+
137
+ - uses: actions/setup-node@v6
138
+ with:
139
+ node-version: '24'
140
+
141
+ - name: Restore workspace from cache
142
+ uses: actions/cache/restore@v5
143
+ with:
144
+ path: |
145
+ ~/.local/share/pnpm/store
146
+ node_modules
147
+ .nuxt
148
+ .output
149
+ key: workspace-${{ runner.os }}-${{ github.sha }}
150
+
151
+ - name: Cache Playwright browsers
152
+ uses: actions/cache@v5
153
+ with:
154
+ path: ~/.cache/ms-playwright
155
+ key: playwright-${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}
156
+
157
+ - name: Install Playwright browsers
158
+ run: pnpm exec playwright install --with-deps chromium
159
+
74
160
  - name: E2E tests
75
161
  env:
76
162
  TEST_USER: ${{ secrets.TEST_USER }}
77
163
  TEST_USER_PASS: ${{ secrets.TEST_USER_PASS }}
78
-
79
164
  run: pnpm test:e2e
80
165
 
81
- - uses: actions/upload-artifact@v6
166
+ - uses: actions/upload-artifact@v7
82
167
  if: ${{ !cancelled() }}
83
168
  with:
84
169
  name: playwright-report
85
170
  path: playwright-report/
86
171
  retention-days: 30
87
-
88
-
package/.nuxtrc CHANGED
@@ -1 +1 @@
1
- setups.@nuxt/test-utils="3.23.0"
1
+ setups.@nuxt/test-utils="4.0.0"
package/CLAUDE.md ADDED
@@ -0,0 +1,106 @@
1
+ # CLAUDE.md
2
+
3
+ This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4
+
5
+ ## Commands
6
+
7
+ ```bash
8
+ # Development
9
+ pnpm dev # Start dev server
10
+ pnpm build # Production build
11
+ pnpm generate # Static generation
12
+ pnpm preview # Preview production build
13
+
14
+ # Type checking
15
+ pnpm typecheck # nuxt typecheck (uses project-local vue-tsc, not npx)
16
+
17
+ # Linting & formatting
18
+ pnpm lint:fix # Run prettier:fix + eslint:fix
19
+ pnpm eslint # Check ESLint
20
+ pnpm prettier # Check Prettier
21
+
22
+ # Testing
23
+ pnpm test:unit # Vitest (unit + nuxt environments, with coverage)
24
+ pnpm test:e2E # Playwright e2e (Chromium, requires .env.test)
25
+
26
+ # Shopware API types
27
+ pnpm generate-types # Regenerate api-types/storeApiTypes.d.ts from schema
28
+ pnpm load-schema # Reload Shopware API schema
29
+ ```
30
+
31
+ ### Running a single test
32
+
33
+ ```bash
34
+ # Vitest – filter by test name or file path
35
+ pnpm vitest run test/nuxt/useBusinessHours.test.ts
36
+ pnpm vitest run --reporter=verbose -t "should return true"
37
+
38
+ # Playwright – filter by file
39
+ pnpm playwright test test/e2e/checkout.spec.ts
40
+ ```
41
+
42
+ ### Typecheck note
43
+
44
+ Always run `nuxt typecheck` via the `pnpm typecheck` script, not via `npx vue-tsc`. The global npx-cached `vue-tsc` version may not be compatible with the project's installed `vue-router`. If `.nuxt/tsconfig.json` has stale pnpm paths after a dependency update, run `pnpm nuxt prepare` to regenerate it.
45
+
46
+ ## Architecture
47
+
48
+ ### Stack
49
+
50
+ - **Nuxt 4** with `app/` directory convention (not `src/`)
51
+ - **Shopware 6** headless commerce backend via `@shopware/nuxt-module`, `@shopware/composables` (extended as a Nuxt layer), and `@shopware/api-client`
52
+ - **Tailwind CSS v4** + **Nuxt UI** component library
53
+ - **Zod** for form validation schemas (`app/validation/`)
54
+ - **PWA** via `@vite-pwa/nuxt` (strategy configurable via `SW` env var)
55
+ - German-language URL routes (`/konto/`, `/warenkorb/`, etc.)
56
+
57
+ ### Shopware type system
58
+
59
+ Custom Shopware Store API types are auto-generated into `api-types/storeApiTypes.d.ts`. The module augmentation in `shopware.d.ts` wires them into the `#shopware` virtual module:
60
+
61
+ ```ts
62
+ declare module "#shopware" {
63
+ export type operations = import("./api-types/storeApiTypes").operations;
64
+ export type Schemas =
65
+ import("./api-types/storeApiTypes").components["schemas"];
66
+ }
67
+ ```
68
+
69
+ Custom ShopBite plugin endpoints are prefixed `shopbite.*` in the operations type (e.g. `shopbite.business-hour.get`, `shopbite.config.get`). Run `pnpm generate-types` after Shopware plugin changes.
70
+
71
+ ### Composables layer
72
+
73
+ `@shopware/composables` is consumed as a **Nuxt layer** (via `extends` in `nuxt.config.ts`). This means its composables, components, and pages are auto-imported alongside the project's own `app/composables/`. Project-level composables override the layer.
74
+
75
+ Key custom composables:
76
+
77
+ - **`useBusinessHours`** – opening hours with multi-interval support; Sunday is day 7 (not 0)
78
+ - **`useDeliveryTime`** – validates slots in 5-minute increments, accounts for `deliveryMinutes` (default 30), handles holidays
79
+ - **`useAddToCart`** – products with extras/toppings use UUID v5 (product ID + sorted extras) as container item reference; simple products bypass the container
80
+ - **`useAddressAutocomplete`** – proxies Geoapify through `/api/address/autocomplete` to avoid exposing the API key client-side
81
+ - **`useShopBiteConfig`** – fetches delivery config and checkout state from the custom Shopware plugin
82
+
83
+ ### Server routes
84
+
85
+ `server/api/address/autocomplete.get.ts` — Geoapify proxy (keeps API key server-side).
86
+ `server/utils/shopware/adminApiClient.ts` — Admin API client for server-side Shopware operations requiring elevated credentials.
87
+
88
+ ### Testing setup
89
+
90
+ Two Vitest projects in `vitest.config.ts`:
91
+
92
+ - **`unit`** – `test/unit/`, Node environment, pure function tests
93
+ - **`nuxt`** – `test/nuxt/`, Nuxt environment via `@nuxt/test-utils`, for composables and components
94
+
95
+ E2E tests in `test/e2e/` use Playwright (Chromium only) and require `TEST_USER` / `TEST_USER_PASS` env vars. A local dev server must be running before the suite executes.
96
+
97
+ ### Route rendering strategy
98
+
99
+ Most pages are SSR. Exceptions defined in `nuxt.config.ts` route rules:
100
+
101
+ - `/wishlist` – client-side rendered (wishlist fetching deferred to client)
102
+ - `/registrierung/bestaetigen/**` – client-side rendered
103
+
104
+ ### CI
105
+
106
+ `.github/workflows/ci.yaml` runs four jobs in sequence: **setup** (install + build, cached) → **lint** → **unit tests** → **e2e tests**. The `build.yaml` workflow handles Docker image builds and NPM publishing on releases.
@@ -34,10 +34,13 @@ const toast = useToast();
34
34
  async function onSubmit(event: FormSubmitEvent<AddressSchema>) {
35
35
  try {
36
36
  let response: undefined | Schemas["CustomerAddress"] = undefined;
37
+ const addressData = event.data as unknown as Parameters<
38
+ typeof updateCustomerAddress
39
+ >[0];
37
40
  if (event.data.id) {
38
- response = await updateCustomerAddress(event.data);
41
+ response = await updateCustomerAddress(addressData);
39
42
  } else {
40
- response = await createCustomerAddress(event.data);
43
+ response = await createCustomerAddress(addressData);
41
44
  }
42
45
 
43
46
  toast.add({
@@ -64,10 +64,11 @@ useCategorySeo(category);
64
64
 
65
65
  const currentSorting = ref(getCurrentSortingOrder.value ?? "Sortieren");
66
66
 
67
- const propertyFilters = computed<Schemas["PropertyGroup"][]>(() =>
68
- getAvailableFilters.value?.filter(
69
- (availableFilter) => availableFilter.code === "properties",
70
- ),
67
+ const propertyFilters = computed<Schemas["PropertyGroup"][]>(
68
+ () =>
69
+ (getAvailableFilters.value?.filter(
70
+ (availableFilter) => availableFilter.code === "properties",
71
+ ) ?? []) as unknown as Schemas["PropertyGroup"][],
71
72
  );
72
73
 
73
74
  const selectedPropertyFilters = ref(getCurrentFilters.value?.properties ?? []);
@@ -79,7 +79,9 @@ watch(
79
79
 
80
80
  function roundToNext5MinInterval(timeString: string | null): string {
81
81
  if (!timeString) return "";
82
- const [hours, minutes] = timeString.split(":").map(Number);
82
+ const parts = timeString.split(":").map(Number);
83
+ const hours = parts[0] ?? 0;
84
+ const minutes = parts[1] ?? 0;
83
85
  const totalMinutes = hours * 60 + minutes;
84
86
  const roundedMinutes = Math.ceil(totalMinutes / 5) * 5;
85
87
  const newHours = Math.floor(roundedMinutes / 60) % 24;
@@ -145,7 +147,7 @@ function handleTimeInput(event: Event): void {
145
147
  step="300"
146
148
  class="border rounded px-2 py-1"
147
149
  @input="handleTimeInput"
148
- >
150
+ />
149
151
  </client-only>
150
152
  </div>
151
153
  <p v-if="validationError" class="text-sm text-red-600">
@@ -69,9 +69,10 @@ async function onSubmit(event: FormSubmitEvent<Schema>) {
69
69
  },
70
70
  );
71
71
 
72
+ const resultData = result?.data as Record<string, unknown> | undefined;
72
73
  const msg =
73
- typeof result?.data?.individualSuccessMessage === "string"
74
- ? result.data.individualSuccessMessage.trim()
74
+ typeof resultData?.individualSuccessMessage === "string"
75
+ ? (resultData.individualSuccessMessage as string).trim()
75
76
  : "";
76
77
  successMessage.value =
77
78
  msg && msg.length > 0
@@ -46,7 +46,7 @@ onMounted(() => {
46
46
  fetchpriority="high"
47
47
  class="absolute inset-0 w-full h-full object-cover -z-10"
48
48
  >
49
- <source :src="backgroundVideo" type="video/mp4" >
49
+ <source :src="backgroundVideo" type="video/mp4" />
50
50
  </video>
51
51
  <div class="bg-black/50 backdrop-blur-sm">
52
52
  <UPageHero
@@ -37,7 +37,7 @@ defineProps<{
37
37
  :alt="item.alt"
38
38
  :fetchpriority="index === 0 ? 'high' : 'auto'"
39
39
  class="rounded-lg"
40
- >
40
+ />
41
41
  </UCarousel>
42
42
  </template>
43
43
  </UPageSection>
@@ -15,7 +15,8 @@ const mapCategoryToNavItem = (
15
15
  description: `${label} Kategorie`,
16
16
  to: category.seoUrl,
17
17
  defaultOpen: true,
18
- icon: category.customFields?.shopbite_category_icon,
18
+ icon: (category.customFields as Record<string, unknown> | undefined)
19
+ ?.shopbite_category_icon as string | undefined,
19
20
  children: (category.children ?? []).map(mapCategoryToNavItem),
20
21
  };
21
22
  };
@@ -52,7 +52,7 @@ onMounted(() => {
52
52
  });
53
53
 
54
54
  const columnRows = computed(() => {
55
- return order.value?.lineItems.filter(
55
+ return order.value?.lineItems?.filter(
56
56
  (lineItem: Schemas["OrderLineItem"]) => lineItem.parentId === null,
57
57
  );
58
58
  });
@@ -65,7 +65,7 @@ const columnRows = computed(() => {
65
65
  >Status: {{ status }}</UBadge
66
66
  >
67
67
  <UBadge variant="outline" color="neutral" size="xl"
68
- >Versandart: {{ order?.deliveries[0].shippingMethod.name }}</UBadge
68
+ >Versandart: {{ order?.deliveries?.[0]?.shippingMethod?.name }}</UBadge
69
69
  >
70
70
  </div>
71
71
  <UTable
@@ -26,13 +26,17 @@ const isVegi = computed<boolean>(() => {
26
26
  return false;
27
27
  }
28
28
 
29
- return product.value?.sortedProperties?.some(
30
- (propertyGroup: Schemas["PropertyGroup"]) =>
31
- propertyGroup.translated.name === "Vegetarisch" &&
32
- propertyGroup.options?.some(
33
- (option: Schemas["PropertyGroupOption"]) =>
34
- option.translated.name === "Ja",
35
- ),
29
+ return (
30
+ (
31
+ product.value?.sortedProperties as Schemas["PropertyGroup"][] | undefined
32
+ )?.some(
33
+ (propertyGroup: Schemas["PropertyGroup"]) =>
34
+ propertyGroup.translated.name === "Vegetarisch" &&
35
+ propertyGroup.options?.some(
36
+ (option: Schemas["PropertyGroupOption"]) =>
37
+ option.translated.name === "Ja",
38
+ ),
39
+ ) ?? false
36
40
  );
37
41
  });
38
42
  const openDetails = ref(false);
@@ -45,7 +49,9 @@ const MAIN_INGREDIENTS_PROPERTY_LABEL = "Hauptzutaten";
45
49
 
46
50
  const mainIngredients = computed<Schemas["PropertyGroupOption"][]>(() => {
47
51
  const sortedProps =
48
- product.value?.sortedProperties ?? ([] as Schemas["PropertyGroup"][]);
52
+ (product.value?.sortedProperties as
53
+ | Schemas["PropertyGroup"][]
54
+ | undefined) ?? ([] as Schemas["PropertyGroup"][]);
49
55
  const mainIngredientsProperty = sortedProps.find(
50
56
  (propertyGroup: Schemas["PropertyGroup"]) =>
51
57
  propertyGroup.translated.name === MAIN_INGREDIENTS_PROPERTY_LABEL,
@@ -2,6 +2,8 @@
2
2
  import type { SelectMenuItem } from "@nuxt/ui";
3
3
  import type { Schemas } from "#shopware";
4
4
 
5
+ type StoreSelectItem = SelectMenuItem & { value: string };
6
+
5
7
  const { apiClient } = useShopwareContext();
6
8
 
7
9
  const config = useRuntimeConfig();
@@ -29,7 +31,7 @@ const { data: salesChannels, pending: status } = useAsyncData(
29
31
 
30
32
  function transform(
31
33
  multiChannelGroups: Schemas["MultiChannelGroupStruct"],
32
- ): SelectMenuItem[] {
34
+ ): StoreSelectItem[] {
33
35
  const group = multiChannelGroups.multiChannelGroup?.[0];
34
36
  if (!group) return [];
35
37
 
@@ -55,10 +57,10 @@ function transform(
55
57
  }));
56
58
  }
57
59
 
58
- const selectedStore = ref<SelectMenuItem>();
60
+ const selectedStore = ref<StoreSelectItem>();
59
61
 
60
62
  watchEffect(() => {
61
- const scValue = salesChannels?.value as SelectMenuItem[] | undefined;
63
+ const scValue = salesChannels?.value as StoreSelectItem[] | undefined;
62
64
  if (Array.isArray(scValue) && storeUrl.value && !selectedStore.value) {
63
65
  const matchingChannel = scValue.find(
64
66
  (channel) => channel?.value === storeUrl.value,
@@ -18,7 +18,7 @@ watch(isLoggedIn, (isLoggedIn) => {
18
18
  });
19
19
 
20
20
  const state = reactive({
21
- accountType: "private",
21
+ accountType: "private" as "private" | "business",
22
22
  salutationId: "",
23
23
  firstName: "",
24
24
  lastName: "",
@@ -118,7 +118,10 @@ async function onSubmit(event: FormSubmitEvent<RegistrationSchema>) {
118
118
  title: "Erfolgreich Kundendaten erfasst",
119
119
  color: "success",
120
120
  });
121
- emit("registration-success", registrationData);
121
+ emit(
122
+ "registration-success",
123
+ registrationData as unknown as RegistrationSchema,
124
+ );
122
125
  } catch (error) {
123
126
  console.error("Registration failed:", error);
124
127
  let description = "Bitte versuchen Sie es erneut.";
@@ -30,15 +30,22 @@ export function useBusinessHours() {
30
30
  return dayBusinessHours
31
31
  .map((bh) => {
32
32
  if (!bh.openingTime || !bh.closingTime) return null;
33
- const [startH, startM] = bh.openingTime.split(":").map(Number);
34
- const [endH, endM] = bh.closingTime.split(":").map(Number);
33
+ const startParts = bh.openingTime.split(":").map(Number);
34
+ const endParts = bh.closingTime.split(":").map(Number);
35
+ const startH = startParts[0] ?? 0;
36
+ const startM = startParts[1] ?? 0;
37
+ const endH = endParts[0] ?? 0;
38
+ const endM = endParts[1] ?? 0;
35
39
 
36
40
  return {
37
41
  start: setTime(date, startH, startM),
38
42
  end: setTime(date, endH, endM),
39
43
  };
40
44
  })
41
- .sort((a, b) => a.start.getTime() - b.start.getTime())
45
+ .sort((a, b) => {
46
+ if (!a || !b) return 0;
47
+ return a.start.getTime() - b.start.getTime();
48
+ })
42
49
  .filter((interval): interval is ServiceInterval => interval !== null);
43
50
  };
44
51
 
@@ -109,7 +116,7 @@ export function useBusinessHours() {
109
116
  continue; // Today's openings have passed, check next days
110
117
  }
111
118
 
112
- const nextOpen = intervals[0].start;
119
+ const nextOpen = intervals[0]!.start;
113
120
  const day = nextOpen.getDate().toString().padStart(2, "0");
114
121
  const month = (nextOpen.getMonth() + 1).toString().padStart(2, "0");
115
122
  const dayName = [
@@ -21,6 +21,7 @@ export async function useCategory(categoryId: Ref<string>) {
21
21
  const response = await apiClient.invoke(
22
22
  "readCategoryGet get /category/{navigationId}",
23
23
  {
24
+ // @ts-expect-error: _criteria is not in the type definition
24
25
  query: { _criteria: criteria },
25
26
  pathParams: {
26
27
  navigationId: categoryId.value,