@shopbite-de/storefront 1.5.3 → 1.6.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.
Files changed (35) hide show
  1. package/.env.example +6 -1
  2. package/LICENSE +21 -0
  3. package/README.md +81 -0
  4. package/app/components/Address/Fields.vue +128 -0
  5. package/app/components/Checkout/PaymentAndDelivery.vue +49 -27
  6. package/app/components/Header.vue +26 -13
  7. package/app/components/User/LoginForm.vue +32 -11
  8. package/app/components/User/RegistrationForm.vue +105 -180
  9. package/app/composables/useAddressAutocomplete.ts +84 -0
  10. package/app/composables/useAddressValidation.ts +95 -0
  11. package/app/composables/useValidCitiesForDelivery.ts +12 -0
  12. package/app/pages/[...all].vue +28 -0
  13. package/app/pages/index.vue +4 -0
  14. package/app/pages/menu/[...all].vue +52 -0
  15. package/app/validation/registrationSchema.ts +105 -124
  16. package/content/{unternehmen/impressum.md → impressum.md} +5 -0
  17. package/content/index.yml +2 -1
  18. package/content/unternehmen/agb.md +5 -0
  19. package/content/unternehmen/datenschutz.md +5 -0
  20. package/content/unternehmen/zahlung-und-versand.md +34 -0
  21. package/content.config.ts +6 -2
  22. package/eslint.config.mjs +5 -3
  23. package/nuxt.config.ts +33 -22
  24. package/package.json +2 -1
  25. package/public/card.png +0 -0
  26. package/server/api/address/autocomplete.get.ts +33 -0
  27. package/test/nuxt/AddressFields.test.ts +284 -0
  28. package/test/nuxt/Header.test.ts +124 -0
  29. package/test/nuxt/LoginForm.test.ts +141 -0
  30. package/test/nuxt/PaymentAndDelivery.test.ts +78 -0
  31. package/test/nuxt/RegistrationForm.test.ts +255 -0
  32. package/test/nuxt/RegistrationValidation.test.ts +39 -0
  33. package/test/nuxt/registrationSchema.test.ts +242 -0
  34. package/test/nuxt/useAddressAutocomplete.test.ts +161 -0
  35. package/app/pages/unternehmen/[slug].vue +0 -66
@@ -1,14 +1,47 @@
1
1
  // schemas/registrationSchema.ts
2
2
  import * as z from "zod";
3
3
 
4
+ const baseAddressSchema = z.object({
5
+ firstName: z.string().optional(),
6
+ lastName: z.string().optional(),
7
+ company: z.string().optional(),
8
+ department: z.string().optional(),
9
+ salutationId: z.string().optional(),
10
+ street: z
11
+ .string()
12
+ .min(1, { message: "Bitte geben Sie Ihre Straße und Hausnummer an." })
13
+ .refine((val) => /[0-9]/.test(val), {
14
+ message: "Bitte geben Sie Ihre Straße und Hausnummer an.",
15
+ }),
16
+ additionalAddressLine1: z.string().optional(),
17
+ zipcode: z.string().optional(),
18
+ city: z.string().min(1, {
19
+ message: "Bitte geben Sie Ihren Ort an.",
20
+ }),
21
+ countryId: z.string().min(1, { message: "Bitte wählen Sie ein Land aus." }),
22
+ phoneNumber: z.string().min(7, {
23
+ message: "Bitte geben Sie eine gültige Telefonnummer an (mind. 7 Zeichen).",
24
+ }),
25
+ });
26
+
4
27
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
5
28
  export const createRegistrationSchema = (state: any) =>
6
29
  z.object({
7
- accountType: z.string(),
8
- email: z.string().email("Invalid email"),
9
- firstName: z.string().min(1),
10
- lastName: z.string().min(1),
30
+ accountType: z.enum(["private", "business"], {
31
+ errorMap: () => ({ message: "Bitte wählen Sie einen Kontotyp aus." }),
32
+ }),
33
+ email: z.string().email("Bitte geben Sie eine gültige E-Mail-Adresse ein."),
34
+ firstName: z
35
+ .string()
36
+ .min(1, { message: "Bitte geben Sie Ihren Vornamen an." }),
37
+ lastName: z
38
+ .string()
39
+ .min(1, { message: "Bitte geben Sie Ihren Nachnamen an." }),
11
40
  guest: z.boolean(),
41
+ acceptedDataProtection: z.boolean().refine((val) => val, {
42
+ message: "Bitte akzeptieren Sie die Datenschutzbestimmungen.",
43
+ }),
44
+ storefrontUrl: z.string().optional(),
12
45
  password: z
13
46
  .string()
14
47
  .optional()
@@ -17,7 +50,7 @@ export const createRegistrationSchema = (state: any) =>
17
50
  return state.guest || (val && val.length >= 8);
18
51
  },
19
52
  {
20
- message: "Password muss mindestens 8 Zeichen lang sein.",
53
+ message: "Das Passwort muss mindestens 8 Zeichen lang sein.",
21
54
  },
22
55
  ),
23
56
  passwordConfirm: z
@@ -28,129 +61,77 @@ export const createRegistrationSchema = (state: any) =>
28
61
  return state.guest || val === state.password;
29
62
  },
30
63
  {
31
- message: "Password stimmt nicht überein.",
64
+ message: "Die Passwörter stimmen nicht überein.",
32
65
  },
33
66
  ),
34
- billingAddress: z.object({
35
- firstName: z.string().min(1).optional(),
36
- lastName: z.string().min(1).optional(),
37
- company: z
38
- .string()
39
- .optional()
40
- .refine(
41
- (val) => {
42
- return (
43
- state.accountType !== "commercial" ||
44
- (val !== undefined && val !== "")
45
- );
46
- },
47
- {
48
- message: "Als Geschäftskunde ist dieses Feld pflicht.",
49
- },
50
- ),
51
- department: z.string().optional(),
52
- street: z.string().min(1),
53
- zipcode: z.string().min(1).optional(),
54
- city: z.string().min(1, {
55
- message: "Bitte geben Sie den Ort an.",
56
- }),
57
- countryId: z.string().min(1),
58
- phoneNumber: z.string().min(7),
59
- }),
60
- shippingAddress: z.object({
61
- firstName: z
62
- .string()
63
- .min(1)
64
- .optional()
65
- .refine(
66
- (val) => {
67
- return (
68
- !state.isShippingAddressDifferent ||
69
- (val !== undefined && val !== "")
70
- );
71
- },
72
- {
73
- message: "Erforderlich.",
74
- },
75
- ),
76
- lastName: z
77
- .string()
78
- .min(1)
79
- .optional()
80
- .refine(
81
- (val) => {
82
- return (
83
- !state.isShippingAddressDifferent ||
84
- (val !== undefined && val !== "")
85
- );
86
- },
87
- {
88
- message: "Erforderlich.",
89
- },
90
- ),
91
- company: z
92
- .string()
93
- .min(1)
94
- .optional()
95
- .refine(
96
- (val) => {
97
- return (
98
- !state.isShippingAddressDifferent ||
99
- (val !== undefined && val !== "")
100
- );
101
- },
102
- {
103
- message: "Erforderlich.",
104
- },
105
- ),
106
- department: z.string().optional(),
107
- phoneNumber: z
108
- .string()
109
- .optional()
110
- .refine(
111
- (val) => {
112
- return (
113
- !state.isShippingAddressDifferent ||
114
- (val !== undefined && val !== "")
115
- );
116
- },
117
- {
118
- message: "Erforderlich.",
119
- },
120
- ),
121
- street: z
122
- .string()
123
- .optional()
124
- .refine(
125
- (val) => {
126
- return (
127
- !state.isShippingAddressDifferent ||
128
- (val !== undefined && val !== "")
129
- );
130
- },
131
- {
132
- message: "Erforderlich.",
133
- },
134
- ),
135
- zipcode: z.string().min(1).optional(),
136
- city: z
137
- .string()
138
- .optional()
139
- .refine(
140
- (val) => {
141
- return (
142
- !state.isShippingAddressDifferent ||
143
- (val !== undefined && val !== "")
144
- );
145
- },
146
- {
147
- message: "Erforderlich.",
148
- },
149
- ),
150
- countryId: z.string().min(1),
151
- }),
67
+ billingAddress: baseAddressSchema.refine(
68
+ (data) => {
69
+ return (
70
+ state.accountType !== "business" ||
71
+ (data.company !== undefined && data.company !== "")
72
+ );
73
+ },
74
+ {
75
+ message: "Als Geschäftskunde ist der Firmenname ein Pflichtfeld.",
76
+ path: ["company"],
77
+ },
78
+ ),
79
+ shippingAddress: baseAddressSchema
80
+ .extend({
81
+ firstName: z.string().optional(),
82
+ lastName: z.string().optional(),
83
+ company: z.string().optional(),
84
+ phoneNumber: z.string().optional(),
85
+ street: z.string().optional(),
86
+ city: z.string().optional(),
87
+ zipcode: z.string().optional(),
88
+ countryId: z.string().optional(),
89
+ })
90
+ .superRefine((data, ctx) => {
91
+ if (!state.isShippingAddressDifferent) return;
92
+
93
+ const requiredFields: (keyof typeof data)[] = [
94
+ "firstName",
95
+ "lastName",
96
+ "company",
97
+ "phoneNumber",
98
+ "street",
99
+ "city",
100
+ ];
101
+
102
+ const fieldMessages: Record<string, string> = {
103
+ firstName: "Bitte geben Sie den Vornamen für die Lieferadresse an.",
104
+ lastName: "Bitte geben Sie den Nachnamen für die Lieferadresse an.",
105
+ company: "Bitte geben Sie den Firmennamen für die Lieferadresse an.",
106
+ phoneNumber:
107
+ "Bitte geben Sie eine Telefonnummer für die Lieferadresse an.",
108
+ street: "Bitte geben Sie die Straße für die Lieferadresse an.",
109
+ city: "Bitte geben Sie den Ort für die Lieferadresse an.",
110
+ };
111
+
112
+ requiredFields.forEach((field) => {
113
+ if (field === "company" && state.accountType !== "business") return;
114
+
115
+ if (!data[field] || data[field] === "") {
116
+ ctx.addIssue({
117
+ code: z.ZodIssueCode.custom,
118
+ message: fieldMessages[field] || "Dieses Feld ist erforderlich.",
119
+ path: [field],
120
+ });
121
+ } else if (field === "street" && !/[0-9]/.test(data.street || "")) {
122
+ ctx.addIssue({
123
+ code: z.ZodIssueCode.custom,
124
+ message: "Bitte geben Sie Ihre Straße und Hausnummer an.",
125
+ path: ["street"],
126
+ });
127
+ }
128
+ });
129
+ })
130
+ .optional(),
152
131
  });
153
132
 
154
133
  export type RegistrationSchema = z.infer<
155
134
  ReturnType<typeof createRegistrationSchema>
156
135
  >;
136
+
137
+ export type AddressSchema = z.infer<typeof baseAddressSchema>;
@@ -1,3 +1,8 @@
1
+ ---
2
+ title: Impressum
3
+ description: Impressum für unsere Website
4
+ ---
5
+
1
6
  # Impressum
2
7
 
3
8
  ## Angaben gemäß § 5 TMG
package/content/index.yml CHANGED
@@ -1,6 +1,7 @@
1
1
  seo:
2
- title: ShopBite – Kostenloses Online-Bestellsystem für Gastronomie
2
+ title: ShopBite – Demo Store
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
+ image: /card.png
4
5
  title: ShopBite
5
6
  description: Reduziere Kosten und steigere deinen Umsatz
6
7
  hero:
@@ -1 +1,6 @@
1
+ ---
2
+ title: AGB
3
+ description: Allgemeine Geschäftsbedingungen
4
+ ---
5
+
1
6
  # AGB
@@ -1 +1,6 @@
1
+ ---
2
+ title: Datenschutz
3
+ description: Datenschutzbestimmungen für unsere Website
4
+ ---
5
+
1
6
  # Datenschutz
@@ -0,0 +1,34 @@
1
+ ---
2
+ title: Zahlungs- und Lieferinformationen
3
+ description: Zahlungs- und Lieferinformationen
4
+ ---
5
+
6
+ # Zahlungs- und Lieferinformationen
7
+
8
+ ### Liefermethoden
9
+
10
+ Wir bieten Ihnen zwei Möglichkeiten, Ihre Bestellung zu erhalten:
11
+
12
+ - **Lieferung:** Wir bringen Ihre Bestellung direkt zu Ihnen nach Hause (beachten Sie bitte unsere Liefergebiete).
13
+ - **Abholung:** Sie können Ihre Bestellung auch direkt bei uns abholen (Kantstraße 6, 63179 Obertshausen).
14
+
15
+ ### Zahlungsmöglichkeiten
16
+
17
+ - **Bei Lieferung:** Die Zahlung erfolgt ausschließlich **bar**.
18
+ - **Bei Abholung:** Sie können bar oder elektronisch mit **EC-Karte** bezahlen.
19
+
20
+ ### Lieferbedingungen
21
+
22
+ Wir liefern ausschließlich in folgende Gebiete:
23
+
24
+ - **63179 Obertshausen** (Stadtteile Obertshausen und Hausen)
25
+ - **63165 Mühlheim** (Stadtteil Lämmerspiel)
26
+
27
+ ### Lieferkosten
28
+
29
+ - **Ab 15,00 € Bestellwert:** Lieferung frei Haus (versandkostenfrei).
30
+ - **Unter 15,00 € Bestellwert:** Wir erheben eine Lieferpauschale von **2,00 €**.
31
+
32
+ ### Lieferzeit
33
+
34
+ Die Lieferzeit beträgt in der Regel ca. **30 Minuten**. Bitte beachten Sie, dass es zu Stoßzeiten zu Abweichungen kommen kann.
package/content.config.ts CHANGED
@@ -131,9 +131,13 @@ export default defineContentConfig({
131
131
  }),
132
132
  }),
133
133
  }),
134
- unternehmen: defineCollection({
135
- source: "unternehmen/*.md",
134
+ landingpages: defineCollection({
135
+ source: "**/*.md",
136
136
  type: "page",
137
+ schema: z.object({
138
+ title: z.string().min(1),
139
+ description: z.string().optional(),
140
+ }),
137
141
  }),
138
142
  },
139
143
  });
package/eslint.config.mjs CHANGED
@@ -3,6 +3,8 @@ import withNuxt from './.nuxt/eslint.config.mjs'
3
3
 
4
4
  export default withNuxt({
5
5
  rules: {
6
- 'vue/multi-word-component-names': 'off'
7
- }
8
- })
6
+ "vue/no-watch-after-await": "error",
7
+ "vue/no-lifecycle-after-await": "error",
8
+ "vue/multi-word-component-names": "off",
9
+ },
10
+ });
package/nuxt.config.ts CHANGED
@@ -2,7 +2,7 @@
2
2
  export default defineNuxtConfig({
3
3
  app: {
4
4
  head: {
5
- title: "ShopBite Demo Shop",
5
+ title: "ShopBite Kostenloses Online-Bestellsystem für Gastronomie",
6
6
  htmlAttrs: {
7
7
  lang: "de",
8
8
  },
@@ -11,18 +11,20 @@ export default defineNuxtConfig({
11
11
  { name: "viewport", content: "width=device-width, initial-scale=1" },
12
12
  {
13
13
  name: "description",
14
- content: "Italienisch-deutsche Küche in Obertshausen",
14
+ content:
15
+ "Dein eigenes Bestellsystem ohne Provisionen, ohne monatliche Kosten – 100% Open Source und individuell anpassbar. Perfekt für Pizzerien, Imbisse und Lieferdienste.",
15
16
  },
16
17
  {
17
18
  property: "og:title",
18
- content: "ShopBite - Demo Shop",
19
+ content:
20
+ "Dein eigenes Bestellsystem ohne Provisionen, ohne monatliche Kosten – 100% Open Source und individuell anpassbar. Perfekt für Pizzerien, Imbisse und Lieferdienste.",
19
21
  },
20
22
  {
21
23
  property: "og:description",
22
24
  content: "Italienisch-deutsche Küche in Obertshausen",
23
25
  },
24
26
  ],
25
- link: [{ rel: "icon", href: "/favicon.ico" }],
27
+ link: [{ rel: "icon", href: "/favicon.ico", type: "image/png" }],
26
28
  },
27
29
  },
28
30
  colorMode: {
@@ -30,12 +32,6 @@ export default defineNuxtConfig({
30
32
  },
31
33
  robots: {
32
34
  disallow: [
33
- "/unternehmen/impressum",
34
- "/unternehmen/datenschutz",
35
- "/unternehmen/agb",
36
- "/impressum",
37
- "/datenschutz",
38
- "/agb",
39
35
  "/merkliste",
40
36
  "/passwort-vergessen",
41
37
  "/account/recover/password",
@@ -45,6 +41,7 @@ export default defineNuxtConfig({
45
41
  runtimeConfig: {
46
42
  shopware: {},
47
43
  apiClientConfig: {},
44
+ geoapifyApiKey: "",
48
45
  public: {
49
46
  site: {
50
47
  name: "ShopBite",
@@ -59,15 +56,6 @@ export default defineNuxtConfig({
59
56
  },
60
57
 
61
58
  routeRules: {
62
- "/impressum": {
63
- redirect: "/unternehmen/impressum",
64
- },
65
- "/datenschutz": {
66
- redirect: "/unternehmen/datenschutz",
67
- },
68
- "/agb": {
69
- redirect: "/unternehmen/agb",
70
- },
71
59
  "/merkliste": {
72
60
  ssr: false,
73
61
  },
@@ -92,10 +80,8 @@ export default defineNuxtConfig({
92
80
  "@vite-pwa/nuxt",
93
81
  "@sentry/nuxt/module",
94
82
  "@nuxt/ui",
83
+ "@nuxt/scripts",
95
84
  "@nuxtjs/plausible",
96
- ...(process.env.NODE_ENV === "development"
97
- ? ["@nuxt/test-utils/module", "@nuxt/eslint"]
98
- : []),
99
85
  ],
100
86
 
101
87
  plausible: {
@@ -150,4 +136,29 @@ export default defineNuxtConfig({
150
136
  experimental: {
151
137
  asyncContext: true,
152
138
  },
139
+ $development: {
140
+ modules: [
141
+ "@shopware/nuxt-module",
142
+ "@nuxt/image",
143
+ "@nuxt/content",
144
+ "@nuxtjs/robots",
145
+ "@vite-pwa/nuxt",
146
+ "@sentry/nuxt/module",
147
+ "@nuxt/ui",
148
+ "@nuxt/scripts",
149
+ "@nuxtjs/plausible",
150
+ "@nuxt/test-utils/module",
151
+ "@nuxt/eslint",
152
+ ],
153
+ },
154
+ $production: {
155
+ scripts: {
156
+ registry: {
157
+ matomoAnalytics: {
158
+ matomoUrl: "",
159
+ siteId: "",
160
+ },
161
+ },
162
+ },
163
+ },
153
164
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shopbite-de/storefront",
3
- "version": "1.5.3",
3
+ "version": "1.6.1",
4
4
  "main": "nuxt.config.ts",
5
5
  "description": "Shopware storefront for food delivery shops",
6
6
  "keywords": [
@@ -20,6 +20,7 @@
20
20
  "@iconify-json/simple-icons": "^1.2.59",
21
21
  "@nuxt/content": "^3.8.2",
22
22
  "@nuxt/image": "^2.0.0",
23
+ "@nuxt/scripts": "0.13.2",
23
24
  "@nuxt/ui": "^4.1.0",
24
25
  "@nuxtjs/plausible": "2.0.1",
25
26
  "@nuxtjs/robots": "^5.5.6",
Binary file
@@ -0,0 +1,33 @@
1
+ export default defineEventHandler(async (event) => {
2
+ const config = useRuntimeConfig();
3
+ const apiKey = config.geoapifyApiKey;
4
+ const query = getQuery(event);
5
+
6
+ if (!apiKey) {
7
+ return { features: [] };
8
+ }
9
+
10
+ const { text, lang, limit, filter } = query;
11
+
12
+ if (!text) {
13
+ return { features: [] };
14
+ }
15
+
16
+ const geoapifyUrl = new URL(
17
+ "https://api.geoapify.com/v1/geocode/autocomplete",
18
+ );
19
+ geoapifyUrl.searchParams.set("apiKey", apiKey);
20
+ geoapifyUrl.searchParams.set("text", text as string);
21
+ if (lang) geoapifyUrl.searchParams.set("lang", lang as string);
22
+ if (limit) geoapifyUrl.searchParams.set("limit", limit as string);
23
+ if (filter) geoapifyUrl.searchParams.set("filter", filter as string);
24
+
25
+ try {
26
+ return await $fetch(geoapifyUrl.toString());
27
+ } catch (error) {
28
+ throw createError({
29
+ statusCode: error.response?.status || 500,
30
+ statusMessage: error.message || "Error fetching address suggestions",
31
+ });
32
+ }
33
+ });