@shopbite-de/storefront 1.16.0 → 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.
- package/.claude/settings.local.json +21 -0
- package/.env.example +0 -4
- package/.github/workflows/build.yaml +14 -8
- package/.github/workflows/ci.yaml +125 -42
- package/.nuxtrc +1 -1
- package/CLAUDE.md +106 -0
- package/app/app.vue +53 -34
- package/app/components/AddToWishlist.vue +0 -3
- package/app/components/Address/Form.vue +5 -2
- package/app/components/Category/Listing.vue +5 -4
- package/app/components/Checkout/DeliveryTimeSelect.vue +4 -2
- package/app/components/Contact/Form.vue +3 -2
- package/app/components/Hero.vue +1 -1
- package/app/components/ImageGallery.vue +1 -1
- package/app/components/Navigation/DesktopLeft.vue +2 -1
- package/app/components/Order/Detail.vue +2 -2
- package/app/components/Product/Card.vue +14 -8
- package/app/components/SalesChannelSwitch.vue +5 -3
- package/app/components/User/LoginForm.vue +1 -4
- package/app/components/User/RegistrationForm.vue +5 -2
- package/app/composables/useBusinessHours.ts +11 -4
- package/app/composables/useCategory.ts +1 -0
- package/app/composables/useTopSellers.ts +2 -2
- package/app/layouts/account.vue +0 -1
- package/app/pages/anmelden.vue +8 -4
- package/app/pages/c/[...all].vue +2 -1
- package/app/pages/konto/adressen.vue +4 -1
- package/app/pages/konto/bestellung/[id].vue +14 -2
- package/app/pages/konto/profil.vue +5 -1
- package/app/utils/formatDate.ts +2 -1
- package/content.config.ts +1 -2
- package/eslint.config.mjs +10 -6
- package/node.dockerfile +7 -6
- package/nuxt.config.ts +17 -3
- package/package.json +38 -32
- package/renovate.json +9 -1
- package/server/api/address/autocomplete.get.ts +3 -2
- package/test/e2e/simple-checkout-as-recurring-customer.test.ts +1 -1
- package/test/nuxt/AddressFields.test.ts +12 -3
- package/test/nuxt/ContactForm.test.ts +31 -11
- package/test/nuxt/HeaderRight.test.ts +15 -6
- package/test/nuxt/LoginForm.test.ts +41 -23
- package/test/nuxt/PaymentAndDelivery.test.ts +2 -2
- package/test/nuxt/RegistrationForm.test.ts +6 -3
- package/test/nuxt/registrationSchema.test.ts +8 -8
- package/test/nuxt/useAddToCart.test.ts +5 -4
- package/test/nuxt/useAddressAutocomplete.test.ts +2 -0
- package/test/nuxt/useBusinessHours.test.ts +5 -5
- package/test/nuxt/useDeliveryTime.test.ts +17 -9
- package/test/nuxt/useProductConfigurator.test.ts +6 -4
- package/test/nuxt/useProductVariants.test.ts +16 -10
- package/test/nuxt/useProductVariantsZwei.test.ts +6 -2
- package/test/nuxt/useScrollAnimation.test.ts +16 -13
- package/test/nuxt/useTopSellers.test.ts +2 -2
- package/test/nuxt/useWishlistActions.test.ts +4 -3
- package/test/unit/useCategorySeo.spec.ts +26 -12
- package/tsconfig.json +21 -1
- package/app/middleware/trailing-slash.global.ts +0 -19
- package/server/utils/shopware/adminApiClient.ts +0 -24
- package/test/unit/sales-channels.test.ts +0 -66
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"permissions": {
|
|
3
|
+
"allow": [
|
|
4
|
+
"Bash(find:*)",
|
|
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)"
|
|
19
|
+
]
|
|
20
|
+
}
|
|
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@
|
|
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@
|
|
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@
|
|
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
|
-
|
|
9
|
-
name:
|
|
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
|
-
-
|
|
27
|
-
uses: actions/checkout@v6
|
|
24
|
+
- uses: actions/checkout@v6
|
|
28
25
|
|
|
29
|
-
-
|
|
30
|
-
|
|
26
|
+
- uses: pnpm/action-setup@v4
|
|
27
|
+
with:
|
|
28
|
+
version: 10.32.1
|
|
31
29
|
|
|
32
|
-
-
|
|
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:
|
|
69
|
-
|
|
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@
|
|
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="
|
|
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.
|
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) {
|
|
@@ -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(
|
|
41
|
+
response = await updateCustomerAddress(addressData);
|
|
39
42
|
} else {
|
|
40
|
-
response = await createCustomerAddress(
|
|
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
|
-
|
|
69
|
-
(
|
|
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
|
|
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
|
|
74
|
-
?
|
|
74
|
+
typeof resultData?.individualSuccessMessage === "string"
|
|
75
|
+
? (resultData.individualSuccessMessage as string).trim()
|
|
75
76
|
: "";
|
|
76
77
|
successMessage.value =
|
|
77
78
|
msg && msg.length > 0
|
package/app/components/Hero.vue
CHANGED
|
@@ -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
|
|
@@ -15,7 +15,8 @@ const mapCategoryToNavItem = (
|
|
|
15
15
|
description: `${label} Kategorie`,
|
|
16
16
|
to: category.seoUrl,
|
|
17
17
|
defaultOpen: true,
|
|
18
|
-
icon: category.customFields
|
|
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
|
|
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]
|
|
68
|
+
>Versandart: {{ order?.deliveries?.[0]?.shippingMethod?.name }}</UBadge
|
|
69
69
|
>
|
|
70
70
|
</div>
|
|
71
71
|
<UTable
|