Compare commits

...

11 Commits

Author SHA1 Message Date
seb
ccceb8fe78 Add Mollie payment integration to CartTab and OrderProcessingService
- Introduced Mollie payment method in CartTab, allowing users to select it alongside existing payment options.
- Implemented loading and error handling for the Mollie component.
- Updated OrderProcessingService to create Mollie payment intents.
- Adjusted PaymentMethodSelector to switch to Mollie when specific delivery methods are selected.
- Enhanced CartTab to store cart items for Mollie payments and calculate total amounts accordingly.
2025-07-06 02:21:52 +02:00
seb
ea5ac762b2 Update .gitignore to exclude 404 page and enhance prerendering logic to support pagination for category-specific LLM files. Implemented generateAllCategoryLlmsPages function to create multiple paginated files, improving product catalog clarity and organization. 2025-07-05 16:20:00 +02:00
seb
40ec0287fd Refactor prerendering logic to include PrerenderSitemap component, enabling category data handling for the sitemap page. Update Sitemap component to initialize categories and loading state more efficiently, improving performance and clarity. 2025-07-05 16:13:38 +02:00
seb
47364d3ad8 Implement caching mechanism in Sitemap component to optimize category data retrieval. Added functions to check for cached product data and initialize categories from cache, improving performance for prerendered environments and reducing unnecessary socket requests. 2025-07-05 16:06:42 +02:00
seb
a6d7ed3e27 Refactor Sitemap component to improve context usage by replacing direct socket reference with context destructuring. This change enhances code clarity and maintains functionality for fetching category data. 2025-07-05 15:57:00 +02:00
seb
f8f03b45b8 Refactor SEO module to utilize a modular structure by re-exporting all SEO functions from a new index file. This change maintains backward compatibility while streamlining the codebase for future enhancements. 2025-07-05 15:52:34 +02:00
seb
eb0d5621e6 Enhance SEO text generation for product categories by implementing pagination support. Updated generateCategoryLlmsTxt to include page navigation and product count details, improving clarity for users accessing product catalogs. Added helper function generateAllCategoryLlmsPages to facilitate the creation of multiple category pages. 2025-07-05 15:41:36 +02:00
seb
f81b9d12df Refactor pagination visibility in ProductList component to prevent layout shifts when no products are available. Updated logic to use CSS visibility instead of conditional rendering. 2025-07-05 15:21:38 +02:00
seb
8ea3b1b6a3 Enhance ProductList component by adding conditional rendering for pagination and no products message based on active filters. Implement helper function for product count text to improve clarity in product display. 2025-07-05 15:18:55 +02:00
seb
fb3450aa23 Implement availability filters in Content and ProductList components to enhance product filtering functionality. Active filters for "auf Lager", "Neu", and "Bald verfügbar" are now dynamically displayed based on product availability, improving user experience and filtering accuracy. 2025-07-05 13:57:29 +02:00
seb
11f5b2cbfd Update package dependencies to latest versions, enhancing compatibility and performance. Adjust start scripts in package.json to include NODE_OPTIONS for deprecation warnings. Add new launch configuration in VSCode for easier development setup. Enhance product filtering logic in Content component to ensure new product filters are only applied when applicable. 2025-07-05 13:48:42 +02:00
23 changed files with 2247 additions and 1279 deletions

1
.gitignore vendored
View File

@@ -27,6 +27,7 @@
/public/index.prerender.html /public/index.prerender.html
/public/Konfigurator /public/Konfigurator
/public/profile /public/profile
/public/404
/public/products.xml /public/products.xml
/public/llms* /public/llms*

7
.vscode/launch.json vendored
View File

@@ -3,6 +3,7 @@
// This will install dependencies before starting the dev server // This will install dependencies before starting the dev server
"version": "0.2.0", "version": "0.2.0",
"configurations": [ "configurations": [
{ {
"type": "node-terminal", "type": "node-terminal",
"name": "Start with API propxy to seedheads.de (Install Deps)", "name": "Start with API propxy to seedheads.de (Install Deps)",
@@ -10,6 +11,12 @@
"command": "npm run start:seedheads", "command": "npm run start:seedheads",
"preLaunchTask": "npm: install", "preLaunchTask": "npm: install",
"cwd": "${workspaceFolder}" "cwd": "${workspaceFolder}"
}, {
"type": "node-terminal",
"name": "Start",
"request": "launch",
"command": "npm run start",
"cwd": "${workspaceFolder}"
} }
] ]
} }

327
package-lock.json generated
View File

@@ -106,9 +106,9 @@
} }
}, },
"node_modules/@babel/compat-data": { "node_modules/@babel/compat-data": {
"version": "7.27.5", "version": "7.28.0",
"resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.27.5.tgz", "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.0.tgz",
"integrity": "sha512-KiRAp/VoJaWkkte84TvUd9qjdbZAdiqyvMxrGl1N6vzFogKmaLgoM3L1kgtLicp2HP5fBJS8JrZKLVIZGVJAVg==", "integrity": "sha512-60X7qkglvrap8mn1lh2ebxXdZYtUcpd7gsmy9kLaBJ4i/WdY8PqTSdxyA8qraikqKQK5C1KRBKXqznrVapyNaw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
@@ -116,22 +116,22 @@
} }
}, },
"node_modules/@babel/core": { "node_modules/@babel/core": {
"version": "7.27.4", "version": "7.28.0",
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.27.4.tgz", "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.0.tgz",
"integrity": "sha512-bXYxrXFubeYdvB0NhD/NBB3Qi6aZeV20GOWVI47t2dkecCEoneR4NPVcb7abpXDEvejgrUfFtG6vG/zxAKmg+g==", "integrity": "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@ampproject/remapping": "^2.2.0", "@ampproject/remapping": "^2.2.0",
"@babel/code-frame": "^7.27.1", "@babel/code-frame": "^7.27.1",
"@babel/generator": "^7.27.3", "@babel/generator": "^7.28.0",
"@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-compilation-targets": "^7.27.2",
"@babel/helper-module-transforms": "^7.27.3", "@babel/helper-module-transforms": "^7.27.3",
"@babel/helpers": "^7.27.4", "@babel/helpers": "^7.27.6",
"@babel/parser": "^7.27.4", "@babel/parser": "^7.28.0",
"@babel/template": "^7.27.2", "@babel/template": "^7.27.2",
"@babel/traverse": "^7.27.4", "@babel/traverse": "^7.28.0",
"@babel/types": "^7.27.3", "@babel/types": "^7.28.0",
"convert-source-map": "^2.0.0", "convert-source-map": "^2.0.0",
"debug": "^4.1.0", "debug": "^4.1.0",
"gensync": "^1.0.0-beta.2", "gensync": "^1.0.0-beta.2",
@@ -147,9 +147,9 @@
} }
}, },
"node_modules/@babel/eslint-parser": { "node_modules/@babel/eslint-parser": {
"version": "7.27.5", "version": "7.28.0",
"resolved": "https://registry.npmjs.org/@babel/eslint-parser/-/eslint-parser-7.27.5.tgz", "resolved": "https://registry.npmjs.org/@babel/eslint-parser/-/eslint-parser-7.28.0.tgz",
"integrity": "sha512-HLkYQfRICudzcOtjGwkPvGc5nF1b4ljLZh1IRDj50lRZ718NAKVgQpIAUX8bfg6u/yuSKY3L7E0YzIV+OxrB8Q==", "integrity": "sha512-N4ntErOlKvcbTt01rr5wj3y55xnIdx1ymrfIr8C2WnM1Y9glFgWaGDEULJIazOX3XM9NRzhfJ6zZnQ1sBNWU+w==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@@ -166,15 +166,15 @@
} }
}, },
"node_modules/@babel/generator": { "node_modules/@babel/generator": {
"version": "7.27.5", "version": "7.28.0",
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.27.5.tgz", "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.0.tgz",
"integrity": "sha512-ZGhA37l0e/g2s1Cnzdix0O3aLYm66eF8aufiVteOgnwxgnRP8GoyMj7VWsgWnQbVKXyge7hqrFh2K2TQM6t1Hw==", "integrity": "sha512-lJjzvrbEeWrhB4P3QBsH7tey117PjLZnDbLiQEKjQ/fNJTjuq4HSqgFA+UNSwZT8D7dxxbnuSBMsa1lrWzKlQg==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@babel/parser": "^7.27.5", "@babel/parser": "^7.28.0",
"@babel/types": "^7.27.3", "@babel/types": "^7.28.0",
"@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/gen-mapping": "^0.3.12",
"@jridgewell/trace-mapping": "^0.3.25", "@jridgewell/trace-mapping": "^0.3.28",
"jsesc": "^3.0.2" "jsesc": "^3.0.2"
}, },
"engines": { "engines": {
@@ -252,22 +252,31 @@
} }
}, },
"node_modules/@babel/helper-define-polyfill-provider": { "node_modules/@babel/helper-define-polyfill-provider": {
"version": "0.6.4", "version": "0.6.5",
"resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.4.tgz", "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.5.tgz",
"integrity": "sha512-jljfR1rGnXXNWnmQg2K3+bvhkxB51Rl32QRaOTuwwjviGrHzIbSc8+x9CpraDtbT7mfyjXObULP4w/adunNwAw==", "integrity": "sha512-uJnGFcPsWQK8fvjgGP5LZUZZsYGIoPeRjSF5PGwrelYgq7Q15/Ft9NGFp1zglwgIv//W0uG4BevRuSJRyylZPg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@babel/helper-compilation-targets": "^7.22.6", "@babel/helper-compilation-targets": "^7.27.2",
"@babel/helper-plugin-utils": "^7.22.5", "@babel/helper-plugin-utils": "^7.27.1",
"debug": "^4.1.1", "debug": "^4.4.1",
"lodash.debounce": "^4.0.8", "lodash.debounce": "^4.0.8",
"resolve": "^1.14.2" "resolve": "^1.22.10"
}, },
"peerDependencies": { "peerDependencies": {
"@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0"
} }
}, },
"node_modules/@babel/helper-globals": {
"version": "7.28.0",
"resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz",
"integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==",
"license": "MIT",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-member-expression-to-functions": { "node_modules/@babel/helper-member-expression-to-functions": {
"version": "7.27.1", "version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.27.1.tgz", "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.27.1.tgz",
@@ -444,12 +453,12 @@
} }
}, },
"node_modules/@babel/parser": { "node_modules/@babel/parser": {
"version": "7.27.5", "version": "7.28.0",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.5.tgz", "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.0.tgz",
"integrity": "sha512-OsQd175SxWkGlzbny8J3K8TnnDD0N3lrIUtB92xwyRpzaenGZhxDvxN/JgU00U3CDZNj9tPuDJ5H0WS4Nt3vKg==", "integrity": "sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@babel/types": "^7.27.3" "@babel/types": "^7.28.0"
}, },
"bin": { "bin": {
"parser": "bin/babel-parser.js" "parser": "bin/babel-parser.js"
@@ -637,15 +646,15 @@
} }
}, },
"node_modules/@babel/plugin-transform-async-generator-functions": { "node_modules/@babel/plugin-transform-async-generator-functions": {
"version": "7.27.1", "version": "7.28.0",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.27.1.tgz", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.28.0.tgz",
"integrity": "sha512-eST9RrwlpaoJBDHShc+DS2SG4ATTi2MYNb4OxYkf3n+7eb49LWpnS+HSpVfW4x927qQwgk8A2hGNVaajAEw0EA==", "integrity": "sha512-BEOdvX4+M765icNPZeidyADIvQ1m1gmunXufXxvRESy/jNNyfovIqUyE7MVgGBjWktCoJlzvFA1To2O4ymIO3Q==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1",
"@babel/helper-remap-async-to-generator": "^7.27.1", "@babel/helper-remap-async-to-generator": "^7.27.1",
"@babel/traverse": "^7.27.1" "@babel/traverse": "^7.28.0"
}, },
"engines": { "engines": {
"node": ">=6.9.0" "node": ">=6.9.0"
@@ -689,9 +698,9 @@
} }
}, },
"node_modules/@babel/plugin-transform-block-scoping": { "node_modules/@babel/plugin-transform-block-scoping": {
"version": "7.27.5", "version": "7.28.0",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.27.5.tgz", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.28.0.tgz",
"integrity": "sha512-JF6uE2s67f0y2RZcm2kpAUEbD50vH62TyWVebxwHAlbSdM49VqPz8t4a1uIjp4NIOIZ4xzLfjY5emt/RCyC7TQ==", "integrity": "sha512-gKKnwjpdx5sER/wl0WN0efUBFzF/56YZO0RJrSYP4CljXnP31ByY7fol89AzomdlLNzI36AvOTmYHsnZTCkq8Q==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@@ -739,18 +748,18 @@
} }
}, },
"node_modules/@babel/plugin-transform-classes": { "node_modules/@babel/plugin-transform-classes": {
"version": "7.27.1", "version": "7.28.0",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.27.1.tgz", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.28.0.tgz",
"integrity": "sha512-7iLhfFAubmpeJe/Wo2TVuDrykh/zlWXLzPNdL0Jqn/Xu8R3QQ8h9ff8FQoISZOsw74/HFqFI7NX63HN7QFIHKA==", "integrity": "sha512-IjM1IoJNw72AZFlj33Cu8X0q2XK/6AaVC3jQu+cgQ5lThWD5ajnuUAml80dqRmOhmPkTH8uAwnpMu9Rvj0LTRA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@babel/helper-annotate-as-pure": "^7.27.1", "@babel/helper-annotate-as-pure": "^7.27.3",
"@babel/helper-compilation-targets": "^7.27.1", "@babel/helper-compilation-targets": "^7.27.2",
"@babel/helper-globals": "^7.28.0",
"@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1",
"@babel/helper-replace-supers": "^7.27.1", "@babel/helper-replace-supers": "^7.27.1",
"@babel/traverse": "^7.27.1", "@babel/traverse": "^7.28.0"
"globals": "^11.1.0"
}, },
"engines": { "engines": {
"node": ">=6.9.0" "node": ">=6.9.0"
@@ -759,16 +768,6 @@
"@babel/core": "^7.0.0-0" "@babel/core": "^7.0.0-0"
} }
}, },
"node_modules/@babel/plugin-transform-classes/node_modules/globals": {
"version": "11.12.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz",
"integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=4"
}
},
"node_modules/@babel/plugin-transform-computed-properties": { "node_modules/@babel/plugin-transform-computed-properties": {
"version": "7.27.1", "version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.27.1.tgz", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.27.1.tgz",
@@ -787,13 +786,14 @@
} }
}, },
"node_modules/@babel/plugin-transform-destructuring": { "node_modules/@babel/plugin-transform-destructuring": {
"version": "7.27.3", "version": "7.28.0",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.27.3.tgz", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.28.0.tgz",
"integrity": "sha512-s4Jrok82JpiaIprtY2nHsYmrThKvvwgHwjgd7UMiYhZaN0asdXNLr0y+NjTfkA7SyQE5i2Fb7eawUOZmLvyqOA==", "integrity": "sha512-v1nrSMBiKcodhsyJ4Gf+Z0U/yawmJDBOTpEB3mcQY52r9RIyPneGyAS/yM6seP/8I+mWI3elOMtT5dB8GJVs+A==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@babel/helper-plugin-utils": "^7.27.1" "@babel/helper-plugin-utils": "^7.27.1",
"@babel/traverse": "^7.28.0"
}, },
"engines": { "engines": {
"node": ">=6.9.0" "node": ">=6.9.0"
@@ -868,6 +868,23 @@
"@babel/core": "^7.0.0-0" "@babel/core": "^7.0.0-0"
} }
}, },
"node_modules/@babel/plugin-transform-explicit-resource-management": {
"version": "7.28.0",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-explicit-resource-management/-/plugin-transform-explicit-resource-management-7.28.0.tgz",
"integrity": "sha512-K8nhUcn3f6iB+P3gwCv/no7OdzOZQcKchW6N389V6PD8NUWKZHzndOd9sPDVbMoBsbmjMqlB4L9fm+fEFNVlwQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.27.1",
"@babel/plugin-transform-destructuring": "^7.28.0"
},
"engines": {
"node": ">=6.9.0"
},
"peerDependencies": {
"@babel/core": "^7.0.0-0"
}
},
"node_modules/@babel/plugin-transform-exponentiation-operator": { "node_modules/@babel/plugin-transform-exponentiation-operator": {
"version": "7.27.1", "version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.27.1.tgz", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.27.1.tgz",
@@ -1135,16 +1152,17 @@
} }
}, },
"node_modules/@babel/plugin-transform-object-rest-spread": { "node_modules/@babel/plugin-transform-object-rest-spread": {
"version": "7.27.3", "version": "7.28.0",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.27.3.tgz", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.28.0.tgz",
"integrity": "sha512-7ZZtznF9g4l2JCImCo5LNKFHB5eXnN39lLtLY5Tg+VkR0jwOt7TBciMckuiQIOIW7L5tkQOCh3bVGYeXgMx52Q==", "integrity": "sha512-9VNGikXxzu5eCiQjdE4IZn8sb9q7Xsk5EXLDBKUYg1e/Tve8/05+KJEtcxGxAgCY5t/BpKQM+JEL/yT4tvgiUA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-compilation-targets": "^7.27.2",
"@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1",
"@babel/plugin-transform-destructuring": "^7.27.3", "@babel/plugin-transform-destructuring": "^7.28.0",
"@babel/plugin-transform-parameters": "^7.27.1" "@babel/plugin-transform-parameters": "^7.27.7",
"@babel/traverse": "^7.28.0"
}, },
"engines": { "engines": {
"node": ">=6.9.0" "node": ">=6.9.0"
@@ -1204,9 +1222,9 @@
} }
}, },
"node_modules/@babel/plugin-transform-parameters": { "node_modules/@babel/plugin-transform-parameters": {
"version": "7.27.1", "version": "7.27.7",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.27.1.tgz", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.27.7.tgz",
"integrity": "sha512-018KRk76HWKeZ5l4oTj2zPpSh+NbGdt0st5S6x0pga6HgrjBOJb24mMDHorFopOOd6YHkLgOZ+zaCjZGPO4aKg==", "integrity": "sha512-qBkYTYCb76RRxUM6CcZA5KRu8K4SM8ajzVeUgVdMVO9NN9uI/GaVmBg/WKJJGnNokV9SY8FxNOVWGXzqzUidBg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@@ -1340,9 +1358,9 @@
} }
}, },
"node_modules/@babel/plugin-transform-regenerator": { "node_modules/@babel/plugin-transform-regenerator": {
"version": "7.27.5", "version": "7.28.0",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.27.5.tgz", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.28.0.tgz",
"integrity": "sha512-uhB8yHerfe3MWnuLAhEbeQ4afVoqv8BQsPqrTv7e/jZ9y00kJL6l9a/f4OWaKxotmjzewfEyXE1vgDJenkQ2/Q==", "integrity": "sha512-LOAozRVbqxEVjSKfhGnuLoE4Kz4Oc5UJzuvFUhSsQzdCdaAQu06mG8zDv2GFSerM62nImUZ7K92vxnQcLSDlCQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@@ -1537,13 +1555,13 @@
} }
}, },
"node_modules/@babel/preset-env": { "node_modules/@babel/preset-env": {
"version": "7.27.2", "version": "7.28.0",
"resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.27.2.tgz", "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.28.0.tgz",
"integrity": "sha512-Ma4zSuYSlGNRlCLO+EAzLnCmJK2vdstgv+n7aUP+/IKZrOfWHOJVdSJtuub8RzHTj3ahD37k5OKJWvzf16TQyQ==", "integrity": "sha512-VmaxeGOwuDqzLl5JUkIRM1X2Qu2uKGxHEQWh+cvvbl7JuJRgKGJSfsEF/bUaxFhJl/XAyxBe7q7qSuTbKFuCyg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@babel/compat-data": "^7.27.2", "@babel/compat-data": "^7.28.0",
"@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-compilation-targets": "^7.27.2",
"@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1",
"@babel/helper-validator-option": "^7.27.1", "@babel/helper-validator-option": "^7.27.1",
@@ -1557,19 +1575,20 @@
"@babel/plugin-syntax-import-attributes": "^7.27.1", "@babel/plugin-syntax-import-attributes": "^7.27.1",
"@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6",
"@babel/plugin-transform-arrow-functions": "^7.27.1", "@babel/plugin-transform-arrow-functions": "^7.27.1",
"@babel/plugin-transform-async-generator-functions": "^7.27.1", "@babel/plugin-transform-async-generator-functions": "^7.28.0",
"@babel/plugin-transform-async-to-generator": "^7.27.1", "@babel/plugin-transform-async-to-generator": "^7.27.1",
"@babel/plugin-transform-block-scoped-functions": "^7.27.1", "@babel/plugin-transform-block-scoped-functions": "^7.27.1",
"@babel/plugin-transform-block-scoping": "^7.27.1", "@babel/plugin-transform-block-scoping": "^7.28.0",
"@babel/plugin-transform-class-properties": "^7.27.1", "@babel/plugin-transform-class-properties": "^7.27.1",
"@babel/plugin-transform-class-static-block": "^7.27.1", "@babel/plugin-transform-class-static-block": "^7.27.1",
"@babel/plugin-transform-classes": "^7.27.1", "@babel/plugin-transform-classes": "^7.28.0",
"@babel/plugin-transform-computed-properties": "^7.27.1", "@babel/plugin-transform-computed-properties": "^7.27.1",
"@babel/plugin-transform-destructuring": "^7.27.1", "@babel/plugin-transform-destructuring": "^7.28.0",
"@babel/plugin-transform-dotall-regex": "^7.27.1", "@babel/plugin-transform-dotall-regex": "^7.27.1",
"@babel/plugin-transform-duplicate-keys": "^7.27.1", "@babel/plugin-transform-duplicate-keys": "^7.27.1",
"@babel/plugin-transform-duplicate-named-capturing-groups-regex": "^7.27.1", "@babel/plugin-transform-duplicate-named-capturing-groups-regex": "^7.27.1",
"@babel/plugin-transform-dynamic-import": "^7.27.1", "@babel/plugin-transform-dynamic-import": "^7.27.1",
"@babel/plugin-transform-explicit-resource-management": "^7.28.0",
"@babel/plugin-transform-exponentiation-operator": "^7.27.1", "@babel/plugin-transform-exponentiation-operator": "^7.27.1",
"@babel/plugin-transform-export-namespace-from": "^7.27.1", "@babel/plugin-transform-export-namespace-from": "^7.27.1",
"@babel/plugin-transform-for-of": "^7.27.1", "@babel/plugin-transform-for-of": "^7.27.1",
@@ -1586,15 +1605,15 @@
"@babel/plugin-transform-new-target": "^7.27.1", "@babel/plugin-transform-new-target": "^7.27.1",
"@babel/plugin-transform-nullish-coalescing-operator": "^7.27.1", "@babel/plugin-transform-nullish-coalescing-operator": "^7.27.1",
"@babel/plugin-transform-numeric-separator": "^7.27.1", "@babel/plugin-transform-numeric-separator": "^7.27.1",
"@babel/plugin-transform-object-rest-spread": "^7.27.2", "@babel/plugin-transform-object-rest-spread": "^7.28.0",
"@babel/plugin-transform-object-super": "^7.27.1", "@babel/plugin-transform-object-super": "^7.27.1",
"@babel/plugin-transform-optional-catch-binding": "^7.27.1", "@babel/plugin-transform-optional-catch-binding": "^7.27.1",
"@babel/plugin-transform-optional-chaining": "^7.27.1", "@babel/plugin-transform-optional-chaining": "^7.27.1",
"@babel/plugin-transform-parameters": "^7.27.1", "@babel/plugin-transform-parameters": "^7.27.7",
"@babel/plugin-transform-private-methods": "^7.27.1", "@babel/plugin-transform-private-methods": "^7.27.1",
"@babel/plugin-transform-private-property-in-object": "^7.27.1", "@babel/plugin-transform-private-property-in-object": "^7.27.1",
"@babel/plugin-transform-property-literals": "^7.27.1", "@babel/plugin-transform-property-literals": "^7.27.1",
"@babel/plugin-transform-regenerator": "^7.27.1", "@babel/plugin-transform-regenerator": "^7.28.0",
"@babel/plugin-transform-regexp-modifiers": "^7.27.1", "@babel/plugin-transform-regexp-modifiers": "^7.27.1",
"@babel/plugin-transform-reserved-words": "^7.27.1", "@babel/plugin-transform-reserved-words": "^7.27.1",
"@babel/plugin-transform-shorthand-properties": "^7.27.1", "@babel/plugin-transform-shorthand-properties": "^7.27.1",
@@ -1607,10 +1626,10 @@
"@babel/plugin-transform-unicode-regex": "^7.27.1", "@babel/plugin-transform-unicode-regex": "^7.27.1",
"@babel/plugin-transform-unicode-sets-regex": "^7.27.1", "@babel/plugin-transform-unicode-sets-regex": "^7.27.1",
"@babel/preset-modules": "0.1.6-no-external-plugins", "@babel/preset-modules": "0.1.6-no-external-plugins",
"babel-plugin-polyfill-corejs2": "^0.4.10", "babel-plugin-polyfill-corejs2": "^0.4.14",
"babel-plugin-polyfill-corejs3": "^0.11.0", "babel-plugin-polyfill-corejs3": "^0.13.0",
"babel-plugin-polyfill-regenerator": "^0.6.1", "babel-plugin-polyfill-regenerator": "^0.6.5",
"core-js-compat": "^3.40.0", "core-js-compat": "^3.43.0",
"semver": "^6.3.1" "semver": "^6.3.1"
}, },
"engines": { "engines": {
@@ -1700,36 +1719,27 @@
} }
}, },
"node_modules/@babel/traverse": { "node_modules/@babel/traverse": {
"version": "7.27.4", "version": "7.28.0",
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.27.4.tgz", "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.0.tgz",
"integrity": "sha512-oNcu2QbHqts9BtOWJosOVJapWjBDSxGCpFvikNR5TGDYDQf3JwpIoMzIKrvfoti93cLfPJEG4tH9SPVeyCGgdA==", "integrity": "sha512-mGe7UK5wWyh0bKRfupsUchrQGqvDbZDbKJw+kcRGSmdHVYrv+ltd0pnpDTVpiTqnaBru9iEvA8pz8W46v0Amwg==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@babel/code-frame": "^7.27.1", "@babel/code-frame": "^7.27.1",
"@babel/generator": "^7.27.3", "@babel/generator": "^7.28.0",
"@babel/parser": "^7.27.4", "@babel/helper-globals": "^7.28.0",
"@babel/parser": "^7.28.0",
"@babel/template": "^7.27.2", "@babel/template": "^7.27.2",
"@babel/types": "^7.27.3", "@babel/types": "^7.28.0",
"debug": "^4.3.1", "debug": "^4.3.1"
"globals": "^11.1.0"
}, },
"engines": { "engines": {
"node": ">=6.9.0" "node": ">=6.9.0"
} }
}, },
"node_modules/@babel/traverse/node_modules/globals": {
"version": "11.12.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz",
"integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==",
"license": "MIT",
"engines": {
"node": ">=4"
}
},
"node_modules/@babel/types": { "node_modules/@babel/types": {
"version": "7.27.6", "version": "7.28.0",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.6.tgz", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.0.tgz",
"integrity": "sha512-ETyHEk2VHHvl9b9jZP5IHPavHYk57EhanlRRuae9XCpb/j5bDCbPPMOBfCWhnl/7EDJz0jEMCi/RhccCE8r1+Q==", "integrity": "sha512-jYnje+JyZG5YThjHiF28oT4SIZLnYOcSBb6+SDaFIyzDVSkXQmQQYclJ2R+YxcdmK0AX6x1E5OQNtuh3jHDrUg==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@babel/helper-string-parser": "^7.27.1", "@babel/helper-string-parser": "^7.27.1",
@@ -2708,17 +2718,13 @@
} }
}, },
"node_modules/@jridgewell/gen-mapping": { "node_modules/@jridgewell/gen-mapping": {
"version": "0.3.8", "version": "0.3.12",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.12.tgz",
"integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", "integrity": "sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@jridgewell/set-array": "^1.2.1", "@jridgewell/sourcemap-codec": "^1.5.0",
"@jridgewell/sourcemap-codec": "^1.4.10",
"@jridgewell/trace-mapping": "^0.3.24" "@jridgewell/trace-mapping": "^0.3.24"
},
"engines": {
"node": ">=6.0.0"
} }
}, },
"node_modules/@jridgewell/resolve-uri": { "node_modules/@jridgewell/resolve-uri": {
@@ -2730,15 +2736,6 @@
"node": ">=6.0.0" "node": ">=6.0.0"
} }
}, },
"node_modules/@jridgewell/set-array": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz",
"integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==",
"license": "MIT",
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/@jridgewell/source-map": { "node_modules/@jridgewell/source-map": {
"version": "0.3.6", "version": "0.3.6",
"resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.6.tgz", "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.6.tgz",
@@ -2757,9 +2754,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/@jridgewell/trace-mapping": { "node_modules/@jridgewell/trace-mapping": {
"version": "0.3.25", "version": "0.3.29",
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.29.tgz",
"integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", "integrity": "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/resolve-uri": "^3.1.0",
@@ -3118,9 +3115,9 @@
} }
}, },
"node_modules/@pmmmwh/react-refresh-webpack-plugin": { "node_modules/@pmmmwh/react-refresh-webpack-plugin": {
"version": "0.6.0", "version": "0.6.1",
"resolved": "https://registry.npmjs.org/@pmmmwh/react-refresh-webpack-plugin/-/react-refresh-webpack-plugin-0.6.0.tgz", "resolved": "https://registry.npmjs.org/@pmmmwh/react-refresh-webpack-plugin/-/react-refresh-webpack-plugin-0.6.1.tgz",
"integrity": "sha512-AAc+QWfZ1KQ/e1C6OHWVlxU+ks6zFGOA44IJUlvju7RlDS8nsX6poPFOIlsg/rTofO9vKov12+WCjMhKkRKD5g==", "integrity": "sha512-95DXXJxNkpYu+sqmpDp7vbw9JCyiNpHuCsvuMuOgVFrKQlwEIn9Y1+NNIQJq+zFL+eWyxw6htthB5CtdwJupNA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@@ -4188,14 +4185,14 @@
} }
}, },
"node_modules/babel-plugin-polyfill-corejs2": { "node_modules/babel-plugin-polyfill-corejs2": {
"version": "0.4.13", "version": "0.4.14",
"resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.13.tgz", "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.14.tgz",
"integrity": "sha512-3sX/eOms8kd3q2KZ6DAhKPc0dgm525Gqq5NtWKZ7QYYZEv57OQ54KtblzJzH1lQF/eQxO8KjWGIK9IPUJNus5g==", "integrity": "sha512-Co2Y9wX854ts6U8gAAPXfn0GmAyctHuK8n0Yhfjd6t30g7yvKjspvvOo9yG+z52PZRgFErt7Ka2pYnXCjLKEpg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@babel/compat-data": "^7.22.6", "@babel/compat-data": "^7.27.7",
"@babel/helper-define-polyfill-provider": "^0.6.4", "@babel/helper-define-polyfill-provider": "^0.6.5",
"semver": "^6.3.1" "semver": "^6.3.1"
}, },
"peerDependencies": { "peerDependencies": {
@@ -4203,27 +4200,27 @@
} }
}, },
"node_modules/babel-plugin-polyfill-corejs3": { "node_modules/babel-plugin-polyfill-corejs3": {
"version": "0.11.1", "version": "0.13.0",
"resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.11.1.tgz", "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.13.0.tgz",
"integrity": "sha512-yGCqvBT4rwMczo28xkH/noxJ6MZ4nJfkVYdoDaC/utLtWrXxv27HVrzAeSbqR8SxDsp46n0YF47EbHoixy6rXQ==", "integrity": "sha512-U+GNwMdSFgzVmfhNm8GJUX88AadB3uo9KpJqS3FaqNIPKgySuvMb+bHPsOmmuWyIcuqZj/pzt1RUIUZns4y2+A==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@babel/helper-define-polyfill-provider": "^0.6.3", "@babel/helper-define-polyfill-provider": "^0.6.5",
"core-js-compat": "^3.40.0" "core-js-compat": "^3.43.0"
}, },
"peerDependencies": { "peerDependencies": {
"@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0"
} }
}, },
"node_modules/babel-plugin-polyfill-regenerator": { "node_modules/babel-plugin-polyfill-regenerator": {
"version": "0.6.4", "version": "0.6.5",
"resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.4.tgz", "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.5.tgz",
"integrity": "sha512-7gD3pRadPrbjhjLyxebmx/WrFYcuSjZ0XbdUujQMZ/fcE9oeewk2U/7PCvez84UeuK3oSjmPZ0Ch0dlupQvGzw==", "integrity": "sha512-ISqQ2frbiNU9vIJkzg7dlPpznPZ4jOiUQ1uSmB0fEHeowtN3COYRsXr/xexn64NpU13P06jc/L5TgiJXOgrbEg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@babel/helper-define-polyfill-provider": "^0.6.4" "@babel/helper-define-polyfill-provider": "^0.6.5"
}, },
"peerDependencies": { "peerDependencies": {
"@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0"
@@ -4317,9 +4314,9 @@
"license": "ISC" "license": "ISC"
}, },
"node_modules/brace-expansion": { "node_modules/brace-expansion": {
"version": "1.1.11", "version": "1.1.12",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@@ -4341,9 +4338,9 @@
} }
}, },
"node_modules/browserslist": { "node_modules/browserslist": {
"version": "4.24.4", "version": "4.25.1",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.4.tgz", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.1.tgz",
"integrity": "sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==", "integrity": "sha512-KGj0KoOMXLpSNkkEI6Z6mShmQy0bc1I+T7K9N81k4WWMrfz+6fQ6es80B/YLAeRoKvjYE1YSHHOW1qe9xIVzHw==",
"dev": true, "dev": true,
"funding": [ "funding": [
{ {
@@ -4361,10 +4358,10 @@
], ],
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"caniuse-lite": "^1.0.30001688", "caniuse-lite": "^1.0.30001726",
"electron-to-chromium": "^1.5.73", "electron-to-chromium": "^1.5.173",
"node-releases": "^2.0.19", "node-releases": "^2.0.19",
"update-browserslist-db": "^1.1.1" "update-browserslist-db": "^1.1.3"
}, },
"bin": { "bin": {
"browserslist": "cli.js" "browserslist": "cli.js"
@@ -4477,9 +4474,9 @@
} }
}, },
"node_modules/caniuse-lite": { "node_modules/caniuse-lite": {
"version": "1.0.30001715", "version": "1.0.30001726",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001715.tgz", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001726.tgz",
"integrity": "sha512-7ptkFGMm2OAOgvZpwgA4yjQ5SQbrNVGdRjzH0pBdy1Fasvcr+KAeECmbCAECzTuDuoX0FCY8KzUxjf9+9kfZEw==", "integrity": "sha512-VQAUIUzBiZ/UnlM28fSp2CRF3ivUn1BWEvxMcVTNwpw91Py1pGbPIyIKtd+tzct9C3ouceCVdGAXxZOpZAsgdw==",
"dev": true, "dev": true,
"funding": [ "funding": [
{ {
@@ -4815,13 +4812,13 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/core-js-compat": { "node_modules/core-js-compat": {
"version": "3.41.0", "version": "3.43.0",
"resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.41.0.tgz", "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.43.0.tgz",
"integrity": "sha512-RFsU9LySVue9RTwdDVX/T0e2Y6jRYWXERKElIjpuEOEnxaXffI0X7RUwVzfYLfzuLXSNJDYoRYUAmRUcyln20A==", "integrity": "sha512-2GML2ZsCc5LR7hZYz4AXmjQw8zuy2T//2QntwdnpuYI7jteT6GVYJL7F6C2C57R7gSYrcqVW3lAALefdbhBLDA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"browserslist": "^4.24.4" "browserslist": "^4.25.0"
}, },
"funding": { "funding": {
"type": "opencollective", "type": "opencollective",
@@ -5141,9 +5138,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/debug": { "node_modules/debug": {
"version": "4.4.0", "version": "4.4.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
"integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"ms": "^2.1.3" "ms": "^2.1.3"
@@ -5478,9 +5475,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/electron-to-chromium": { "node_modules/electron-to-chromium": {
"version": "1.5.139", "version": "1.5.179",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.139.tgz", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.179.tgz",
"integrity": "sha512-GGnRYOTdN5LYpwbIr0rwP/ZHOQSvAF6TG0LSzp28uCBb9JiXHJGmaaKw29qjNJc5bGnnp6kXJqRnGMQoELwi5w==", "integrity": "sha512-UWKi/EbBopgfFsc5k61wFpV7WrnnSlSzW/e2XcBmS6qKYTivZlLtoll5/rdqRTxGglGHkmkW0j0pFNJG10EUIQ==",
"dev": true, "dev": true,
"license": "ISC" "license": "ISC"
}, },

View File

@@ -4,8 +4,8 @@
"type": "module", "type": "module",
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {
"start": "webpack serve --progress --mode development --no-open", "start": "cross-env NODE_OPTIONS=\"--no-deprecation\" webpack serve --progress --mode development --no-open",
"start:seedheads": "cross-env PROXY_TARGET=https://seedheads.de webpack serve --progress --mode development --no-open", "start:seedheads": "cross-env PROXY_TARGET=https://seedheads.de NODE_OPTIONS=\"--no-deprecation\" webpack serve --progress --mode development --no-open",
"prod": "webpack serve --progress --mode production --no-client-overlay --no-client --no-web-socket-server --no-open --no-live-reload --no-hot --compress --no-devtool", "prod": "webpack serve --progress --mode production --no-client-overlay --no-client --no-web-socket-server --no-open --no-live-reload --no-hot --compress --no-devtool",
"build:client": "cross-env NODE_ENV=production webpack --progress --mode production && shx cp dist/index.html dist/index_template.html", "build:client": "cross-env NODE_ENV=production webpack --progress --mode production && shx cp dist/index.html dist/index_template.html",
"build": "npm run build:client", "build": "npm run build:client",

View File

@@ -50,6 +50,7 @@ const {
generateProductsXml, generateProductsXml,
generateLlmsTxt, generateLlmsTxt,
generateCategoryLlmsTxt, generateCategoryLlmsTxt,
generateAllCategoryLlmsPages,
} = require("./prerender/seo.cjs"); } = require("./prerender/seo.cjs");
const { const {
fetchCategoryProducts, fetchCategoryProducts,
@@ -71,6 +72,7 @@ const Batteriegesetzhinweise =
require("./src/pages/Batteriegesetzhinweise.js").default; require("./src/pages/Batteriegesetzhinweise.js").default;
const Widerrufsrecht = require("./src/pages/Widerrufsrecht.js").default; const Widerrufsrecht = require("./src/pages/Widerrufsrecht.js").default;
const Sitemap = require("./src/pages/Sitemap.js").default; const Sitemap = require("./src/pages/Sitemap.js").default;
const PrerenderSitemap = require("./src/PrerenderSitemap.js").default;
const AGB = require("./src/pages/AGB.js").default; const AGB = require("./src/pages/AGB.js").default;
const NotFound404 = require("./src/pages/NotFound404.js").default; const NotFound404 = require("./src/pages/NotFound404.js").default;
@@ -361,10 +363,11 @@ const renderApp = async (categoryData, socket) => {
description: "Widerrufsrecht page", description: "Widerrufsrecht page",
}, },
{ {
component: Sitemap, component: PrerenderSitemap,
path: "/sitemap", path: "/sitemap",
filename: "sitemap", filename: "sitemap",
description: "Sitemap page", description: "Sitemap page",
needsCategoryData: true,
}, },
{ component: AGB, path: "/agb", filename: "agb", description: "AGB page" }, { component: AGB, path: "/agb", filename: "agb", description: "AGB page" },
{ component: NotFound404, path: "/404", filename: "404", description: "404 Not Found page" }, { component: NotFound404, path: "/404", filename: "404", description: "404 Not Found page" },
@@ -384,7 +387,9 @@ const renderApp = async (categoryData, socket) => {
let staticPagesRendered = 0; let staticPagesRendered = 0;
for (const page of staticPages) { for (const page of staticPages) {
const pageComponent = React.createElement(page.component, null); // Pass category data as props if needed
const pageProps = page.needsCategoryData ? { categoryData } : null;
const pageComponent = React.createElement(page.component, pageProps);
let metaTags = ""; let metaTags = "";
// Special handling for Sitemap page to include category data // Special handling for Sitemap page to include category data
@@ -657,27 +662,39 @@ const renderApp = async (categoryData, socket) => {
productsByCategory[categoryId].push(product); productsByCategory[categoryId].push(product);
}); });
// Generate category-specific LLM files // Generate category-specific LLM files with pagination
let categoryFilesGenerated = 0; let categoryFilesGenerated = 0;
let totalCategoryProducts = 0; let totalCategoryProducts = 0;
let totalPaginatedFiles = 0;
for (const category of allCategories) { for (const category of allCategories) {
if (category.seoName) { if (category.seoName) {
const categoryProducts = productsByCategory[category.id] || []; const categoryProducts = productsByCategory[category.id] || [];
const categorySlug = category.seoName.toLowerCase().replace(/[^a-z0-9]/g, '-'); const categorySlug = category.seoName.toLowerCase().replace(/[^a-z0-9]/g, '-');
const categoryLlmsTxt = generateCategoryLlmsTxt(category, categoryProducts, shopConfig.baseUrl, shopConfig); // Generate all paginated files for this category
const categoryLlmsTxtPath = path.resolve(__dirname, config.outputDir, `llms-${categorySlug}.txt`); const categoryPages = generateAllCategoryLlmsPages(category, categoryProducts, shopConfig.baseUrl, shopConfig);
fs.writeFileSync(categoryLlmsTxtPath, categoryLlmsTxt, { encoding: 'utf8' }); // Write each paginated file
for (const page of categoryPages) {
const pagePath = path.resolve(__dirname, config.outputDir, page.fileName);
fs.writeFileSync(pagePath, page.content, { encoding: 'utf8' });
totalPaginatedFiles++;
}
console.log(` ✅ llms-${categorySlug}.txt - ${categoryProducts.length} products (${Math.round(categoryLlmsTxt.length / 1024)}KB)`); const pageCount = categoryPages.length;
const totalSize = categoryPages.reduce((sum, page) => sum + page.content.length, 0);
console.log(` ✅ llms-${categorySlug}-page-*.txt - ${categoryProducts.length} products across ${pageCount} pages (${Math.round(totalSize / 1024)}KB total)`);
categoryFilesGenerated++; categoryFilesGenerated++;
totalCategoryProducts += categoryProducts.length; totalCategoryProducts += categoryProducts.length;
} }
} }
console.log(` 📄 Total paginated files generated: ${totalPaginatedFiles}`);
console.log(` 📦 Total products across all categories: ${totalCategoryProducts}`);
try { try {
const verification = fs.readFileSync(llmsTxtPath, 'utf8'); const verification = fs.readFileSync(llmsTxtPath, 'utf8');
console.log(` - File verification: ✅ All files valid UTF-8`); console.log(` - File verification: ✅ All files valid UTF-8`);

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,81 @@
const generateCategoryJsonLd = (category, products = [], baseUrl, config) => {
const categoryUrl = `${baseUrl}/Kategorie/${category.seoName}`;
const jsonLd = {
"@context": "https://schema.org/",
"@type": "CollectionPage",
name: category.name,
url: categoryUrl,
description: `${category.name} - Entdecken Sie unsere Auswahl an hochwertigen Produkten`,
breadcrumb: {
"@type": "BreadcrumbList",
itemListElement: [
{
"@type": "ListItem",
position: 1,
name: "Home",
item: baseUrl,
},
{
"@type": "ListItem",
position: 2,
name: category.name,
item: categoryUrl,
},
],
},
};
// Add product list if products are available
if (products && products.length > 0) {
jsonLd.mainEntity = {
"@type": "ItemList",
numberOfItems: products.length,
itemListElement: products.slice(0, 20).map((product, index) => ({
"@type": "ListItem",
position: index + 1,
item: {
"@type": "Product",
name: product.name,
url: `${baseUrl}/Artikel/${product.seoName}`,
image:
product.pictureList && product.pictureList.trim()
? `${baseUrl}/assets/images/prod${product.pictureList
.split(",")[0]
.trim()}.jpg`
: `${baseUrl}/assets/images/nopicture.jpg`,
description: product.description
? product.description.replace(/<[^>]*>/g, "").substring(0, 200)
: `${product.name} - Hochwertiges Growshop Produkt`,
sku: product.articleNumber || product.seoName,
brand: {
"@type": "Brand",
name: product.manufacturer || config.brandName,
},
offers: {
"@type": "Offer",
url: `${baseUrl}/Artikel/${product.seoName}`,
price: product.price && !isNaN(product.price) ? product.price.toString() : "0.00",
priceCurrency: config.currency,
availability: product.available
? "https://schema.org/InStock"
: "https://schema.org/OutOfStock",
seller: {
"@type": "Organization",
name: config.brandName,
},
itemCondition: "https://schema.org/NewCondition",
},
},
})),
};
}
return `<script type="application/ld+json">${JSON.stringify(
jsonLd
)}</script>`;
};
module.exports = {
generateCategoryJsonLd,
};

344
prerender/seo/feeds.cjs Normal file
View File

@@ -0,0 +1,344 @@
const generateRobotsTxt = (baseUrl) => {
// Ensure URLs are properly formatted
const canonicalUrl = baseUrl.endsWith('/') ? baseUrl.slice(0, -1) : baseUrl;
const robotsTxt = `User-agent: *
Allow: /
Sitemap: ${canonicalUrl}/sitemap.xml
Crawl-delay: 0
`;
return robotsTxt;
};
const generateProductsXml = (allProductsData = [], baseUrl, config) => {
const currentDate = new Date().toISOString();
// Validate input
if (!Array.isArray(allProductsData) || allProductsData.length === 0) {
throw new Error("No valid product data provided");
}
// Category mapping function
const getGoogleProductCategory = (categoryId) => {
const categoryMappings = {
// Seeds & Plants
689: "Home & Garden > Plants > Seeds",
706: "Home & Garden > Plants", // Stecklinge (cuttings)
376: "Home & Garden > Plants > Plant & Herb Growing Kits", // Grow-Sets
// Headshop & Accessories
709: "Arts & Entertainment > Hobbies & Creative Arts", // Headshop
711: "Arts & Entertainment > Hobbies & Creative Arts", // Bongs
714: "Arts & Entertainment > Hobbies & Creative Arts", // Zubehör
748: "Arts & Entertainment > Hobbies & Creative Arts", // Köpfe
749: "Arts & Entertainment > Hobbies & Creative Arts", // Chillums / Diffusoren / Kupplungen
896: "Electronics > Electronics Accessories", // Vaporizer
710: "Home & Garden > Kitchen & Dining > Kitchen Tools & Utensils", // Grinder
// Measuring & Packaging
186: "Business & Industrial", // Wiegen & Verpacken
187: "Business & Industrial > Science & Laboratory > Lab Equipment", // Waagen
346: "Home & Garden > Kitchen & Dining > Food Storage", // Vakuumbeutel
355: "Home & Garden > Kitchen & Dining > Food Storage", // Boveda & Integra Boost
407: "Home & Garden > Kitchen & Dining > Food Storage", // Grove Bags
449: "Home & Garden > Kitchen & Dining > Food Storage", // Cliptütchen
539: "Home & Garden > Kitchen & Dining > Food Storage", // Gläser & Dosen
// Lighting & Equipment
694: "Home & Garden > Lighting", // Lampen
261: "Home & Garden > Lighting", // Lampenzubehör
// Plants & Growing
691: "Home & Garden > Lawn & Garden > Fertilizers", // Dünger
692: "Home & Garden > Lawn & Garden > Fertilizers", // Dünger - Zubehör
693: "Sporting Goods > Outdoor Recreation > Camping & Hiking > Tents", // Zelte
// Pots & Containers
219: "Home & Garden > Decor > Planters & Pots", // Töpfe
220: "Home & Garden > Decor > Planters & Pots", // Untersetzer
301: "Home & Garden > Decor > Planters & Pots", // Stofftöpfe
317: "Home & Garden > Decor > Planters & Pots", // Air-Pot
364: "Home & Garden > Decor > Planters & Pots", // Kunststofftöpfe
292: "Home & Garden > Decor > Planters & Pots", // Trays & Fluttische
// Ventilation & Climate
703: "Home & Garden > Outdoor Power Tools", // Abluft-Sets
247: "Home & Garden > Outdoor Power Tools", // Belüftung
214: "Home & Garden > Outdoor Power Tools", // Umluft-Ventilatoren
308: "Home & Garden > Outdoor Power Tools", // Ab- und Zuluft
609: "Home & Garden > Outdoor Power Tools", // Schalldämpfer
248: "Home & Garden > Pool & Spa > Pool & Spa Filters", // Aktivkohlefilter
392: "Home & Garden > Pool & Spa > Pool & Spa Filters", // Zuluftfilter
658: "Home & Garden > Climate Control > Dehumidifiers", // Luftbe- und entfeuchter
310: "Home & Garden > Climate Control > Heating", // Heizmatten
379: "Home & Garden > Household Supplies > Air Fresheners", // Geruchsneutralisation
// Irrigation & Watering
221: "Home & Garden > Lawn & Garden > Watering Equipment", // Bewässerung
250: "Home & Garden > Lawn & Garden > Watering Equipment", // Schläuche
297: "Home & Garden > Lawn & Garden > Watering Equipment", // Pumpen
354: "Home & Garden > Lawn & Garden > Watering Equipment", // Sprüher
372: "Home & Garden > Lawn & Garden > Watering Equipment", // AutoPot
389: "Home & Garden > Lawn & Garden > Watering Equipment", // Blumat
405: "Home & Garden > Lawn & Garden > Watering Equipment", // Schläuche
425: "Home & Garden > Lawn & Garden > Watering Equipment", // Wassertanks
480: "Home & Garden > Lawn & Garden > Watering Equipment", // Tropfer
519: "Home & Garden > Lawn & Garden > Watering Equipment", // Pumpsprüher
// Growing Media & Soils
242: "Home & Garden > Lawn & Garden > Fertilizers", // Böden
243: "Home & Garden > Lawn & Garden > Fertilizers", // Erde
269: "Home & Garden > Lawn & Garden > Fertilizers", // Kokos
580: "Home & Garden > Lawn & Garden > Fertilizers", // Perlite & Blähton
// Propagation & Starting
286: "Home & Garden > Plants", // Anzucht
298: "Home & Garden > Plants", // Steinwolltrays
421: "Home & Garden > Plants", // Vermehrungszubehör
489: "Home & Garden > Plants", // EazyPlug & Jiffy
359: "Home & Garden > Outdoor Structures > Greenhouses", // Gewächshäuser
// Tools & Equipment
373: "Home & Garden > Tools > Hand Tools", // GrowTool
403: "Home & Garden > Kitchen & Dining > Kitchen Tools & Utensils", // Messbecher & mehr
259: "Home & Garden > Tools > Hand Tools", // Pressen
280: "Home & Garden > Tools > Hand Tools", // Erntescheeren
258: "Home & Garden > Tools", // Ernte & Verarbeitung
278: "Home & Garden > Tools", // Extraktion
302: "Home & Garden > Tools", // Erntemaschinen
// Hardware & Plumbing
222: "Hardware > Plumbing", // PE-Teile
374: "Hardware > Plumbing > Plumbing Fittings", // Verbindungsteile
// Electronics & Control
314: "Electronics > Electronics Accessories", // Steuergeräte
408: "Electronics > Electronics Accessories", // GrowControl
344: "Business & Industrial > Science & Laboratory > Lab Equipment", // Messgeräte
555: "Business & Industrial > Science & Laboratory > Lab Equipment > Microscopes", // Mikroskope
// Camping & Outdoor
226: "Sporting Goods > Outdoor Recreation > Camping & Hiking", // Zeltzubehör
// Plant Care & Protection
239: "Home & Garden > Lawn & Garden > Pest Control", // Pflanzenschutz
240: "Home & Garden > Plants", // Anbauzubehör
// Office & Media
424: "Office Supplies > Labels", // Etiketten & Schilder
387: "Media > Books", // Literatur
// General categories
705: "Home & Garden", // Set-Konfigurator
686: "Home & Garden", // Zubehör
741: "Home & Garden", // Zubehör
294: "Home & Garden", // Zubehör
695: "Home & Garden", // Zubehör
293: "Home & Garden", // Trockennetze
4: "Home & Garden", // Sonstiges
450: "Home & Garden", // Restposten
};
return categoryMappings[categoryId] || "Home & Garden > Plants";
};
let productsXml = `<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:g="http://base.google.com/ns/1.0" version="2.0">
<channel>
<title>${config.descriptions.short}</title>
<link>${baseUrl}</link>
<description>${config.descriptions.short}</description>
<lastBuildDate>${currentDate}</lastBuildDate>
<language>${config.language}</language>`;
// Helper function to clean text content of problematic characters
const cleanTextContent = (text) => {
if (!text) return "";
return text.toString()
// Remove HTML tags
.replace(/<[^>]*>/g, "")
// Remove non-printable characters and control characters
.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F-\x9F]/g, '')
// Remove BOM and other Unicode formatting characters
.replace(/[\uFEFF\u200B-\u200D\u2060]/g, '')
// Replace multiple whitespace with single space
.replace(/\s+/g, ' ')
// Remove leading/trailing whitespace
.trim();
};
// Helper function to properly escape XML content and remove invalid characters
const escapeXml = (unsafe) => {
if (!unsafe) return "";
// Convert to string and remove invalid XML characters
const cleaned = unsafe.toString()
// Remove control characters except tab (0x09), newline (0x0A), and carriage return (0x0D)
.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, '')
// Remove invalid Unicode characters and surrogates
.replace(/[\uD800-\uDFFF]/g, '')
// Remove other problematic characters
.replace(/[\uFFFE\uFFFF]/g, '')
// Normalize whitespace
.replace(/\s+/g, ' ')
.trim();
// Escape XML entities
return cleaned
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&apos;");
};
let processedCount = 0;
let skippedCount = 0;
// Category IDs to skip (seeds, plants, headshop items)
const skipCategoryIds = [689, 706, 709, 711, 714, 748, 749, 896, 710];
// Add each product as an item
allProductsData.forEach((product, index) => {
try {
// Skip products without essential data
if (!product || !product.seoName) {
skippedCount++;
return;
}
// Skip products from excluded categories
const productCategoryId = product.categoryId || product.category_id || product.category || null;
if (productCategoryId && skipCategoryIds.includes(parseInt(productCategoryId))) {
skippedCount++;
return;
}
// Skip products without GTIN
if (!product.gtin || !product.gtin.toString().trim()) {
skippedCount++;
return;
}
// Skip products without pictures
if (!product.pictureList || !product.pictureList.trim()) {
skippedCount++;
return;
}
// Clean description for feed (remove HTML tags and limit length)
const rawDescription = product.description
? cleanTextContent(product.description).substring(0, 500)
: `${product.name || 'Product'} - Art.-Nr.: ${product.articleNumber || 'N/A'}`;
const cleanDescription = escapeXml(rawDescription) || "Produktbeschreibung nicht verfügbar";
// Clean product name
const rawName = product.name || "Unnamed Product";
const cleanName = escapeXml(cleanTextContent(rawName)) || "Unnamed Product";
// Validate essential fields
if (!cleanName || cleanName.length < 2) {
skippedCount++;
return;
}
// Generate product URL
const productUrl = `${baseUrl}/Artikel/${encodeURIComponent(product.seoName)}`;
// Generate image URL
const imageUrl = product.pictureList && product.pictureList.trim()
? `${baseUrl}/assets/images/prod${product.pictureList.split(",")[0].trim()}.jpg`
: `${baseUrl}/assets/images/nopicture.jpg`;
// Generate brand (manufacturer)
const rawBrand = product.manufacturer || config.brandName;
const brand = escapeXml(cleanTextContent(rawBrand));
// Generate condition (always new for this type of shop)
const condition = "new";
// Generate availability
const availability = product.available ? "in stock" : "out of stock";
// Generate price (ensure it's a valid number)
const price = product.price && !isNaN(product.price)
? `${parseFloat(product.price).toFixed(2)} ${config.currency}`
: `0.00 ${config.currency}`;
// Skip products with price == 0
if (!product.price || parseFloat(product.price) === 0) {
skippedCount++;
return;
}
// Generate GTIN/EAN if available
const gtin = product.gtin ? escapeXml(product.gtin.toString().trim()) : null;
// Generate product ID (using articleNumber or seoName)
const rawProductId = product.articleNumber || product.seoName || `product_${Date.now()}_${Math.random().toString(36).substr(2, 5)}`;
const productId = escapeXml(rawProductId.toString().trim()) || `fallback_${Date.now()}_${Math.random().toString(36).substr(2, 5)}`;
// Get Google product category based on product's category ID
const categoryId = product.categoryId || product.category_id || product.category || null;
const googleCategory = getGoogleProductCategory(categoryId);
const escapedGoogleCategory = escapeXml(googleCategory);
// Build item XML with proper formatting
productsXml += `
<item>
<g:id>${productId}</g:id>
<g:title>${cleanName}</g:title>
<g:description>${cleanDescription}</g:description>
<g:link>${productUrl}</g:link>
<g:image_link>${imageUrl}</g:image_link>
<g:condition>${condition}</g:condition>
<g:availability>${availability}</g:availability>
<g:price>${price}</g:price>
<g:shipping>
<g:country>${config.country}</g:country>
<g:service>${config.shipping.defaultService}</g:service>
<g:price>${config.shipping.defaultCost}</g:price>
</g:shipping>
<g:brand>${brand}</g:brand>
<g:google_product_category>${escapedGoogleCategory}</g:google_product_category>
<g:product_type>Gartenbedarf</g:product_type>`;
// Add GTIN if available
if (gtin && gtin.trim()) {
productsXml += `
<g:gtin>${gtin}</g:gtin>`;
}
// Add weight if available
if (product.weight && !isNaN(product.weight)) {
productsXml += `
<g:shipping_weight>${parseFloat(product.weight).toFixed(2)} g</g:shipping_weight>`;
}
productsXml += `
</item>`;
processedCount++;
} catch (itemError) {
console.log(` ⚠️ Skipped product ${index + 1}: ${itemError.message}`);
skippedCount++;
}
});
productsXml += `
</channel>
</rss>`;
console.log(` 📊 Processing summary: ${processedCount} products included, ${skippedCount} skipped`);
return productsXml;
};
module.exports = {
generateRobotsTxt,
generateProductsXml,
};

215
prerender/seo/homepage.cjs Normal file
View File

@@ -0,0 +1,215 @@
const generateHomepageMetaTags = (baseUrl, config) => {
const description = config.descriptions.long;
const keywords = config.keywords;
const imageUrl = `${baseUrl}${config.images.logo}`;
// Ensure URLs are properly formatted
const canonicalUrl = baseUrl.endsWith('/') ? baseUrl.slice(0, -1) : baseUrl;
return `
<!-- SEO Meta Tags -->
<meta name="description" content="${description}">
<meta name="keywords" content="${keywords}">
<!-- Open Graph Meta Tags -->
<meta property="og:title" content="${config.descriptions.short}">
<meta property="og:description" content="${description}">
<meta property="og:image" content="${imageUrl}">
<meta property="og:url" content="${canonicalUrl}">
<meta property="og:type" content="website">
<meta property="og:site_name" content="${config.siteName}">
<!-- Twitter Card Meta Tags -->
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="${config.descriptions.short}">
<meta name="twitter:description" content="${description}">
<meta name="twitter:image" content="${imageUrl}">
<!-- Additional Meta Tags -->
<meta name="robots" content="index, follow">
<link rel="canonical" href="${canonicalUrl}">
`;
};
const generateHomepageJsonLd = (baseUrl, config, categories = []) => {
// Ensure URLs are properly formatted
const canonicalUrl = baseUrl.endsWith('/') ? baseUrl.slice(0, -1) : baseUrl;
const logoUrl = `${canonicalUrl}${config.images.logo}`;
const websiteJsonLd = {
"@context": "https://schema.org/",
"@type": "WebSite",
name: config.brandName,
url: canonicalUrl,
description: config.descriptions.long,
publisher: {
"@type": "Organization",
name: config.brandName,
url: canonicalUrl,
logo: {
"@type": "ImageObject",
url: logoUrl,
},
},
potentialAction: {
"@type": "SearchAction",
target: `${canonicalUrl}/search?q={search_term_string}`,
query: "required name=search_term_string"
},
mainEntity: {
"@type": "WebPage",
name: "Sitemap",
url: `${canonicalUrl}/sitemap`,
description: "Vollständige Sitemap mit allen Kategorien und Seiten",
},
sameAs: [
// Add your social media URLs here if available
],
};
// Organization/LocalBusiness Schema for rich results
const organizationJsonLd = {
"@context": "https://schema.org",
"@type": "LocalBusiness",
"name": config.brandName,
"alternateName": config.siteName,
"description": config.descriptions.long,
"url": canonicalUrl,
"logo": logoUrl,
"image": logoUrl,
"telephone": "015208491860",
"email": "service@growheads.de",
"address": {
"@type": "PostalAddress",
"streetAddress": "Trachenberger Strasse 14",
"addressLocality": "Dresden",
"postalCode": "01129",
"addressCountry": "DE",
"addressRegion": "Sachsen"
},
"geo": {
"@type": "GeoCoordinates",
"latitude": "51.083675",
"longitude": "13.727215"
},
"openingHours": [
"Mo-Fr 10:00:00-20:00:00",
"Sa 11:00:00-19:00:00"
],
"paymentAccepted": "Cash, Credit Card, PayPal, Bank Transfer",
"currenciesAccepted": "EUR",
"priceRange": "€€",
"areaServed": {
"@type": "Country",
"name": "Germany"
},
"contactPoint": [
{
"@type": "ContactPoint",
"telephone": "015208491860",
"contactType": "customer service",
"availableLanguage": "German",
"hoursAvailable": {
"@type": "OpeningHoursSpecification",
"dayOfWeek": ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday"],
"opens": "10:00:00",
"closes": "20:00:00"
}
},
{
"@type": "ContactPoint",
"email": "service@growheads.de",
"contactType": "customer service",
"availableLanguage": "German"
}
],
"sameAs": [
// Add social media URLs when available
// "https://www.facebook.com/growheads",
// "https://www.instagram.com/growheads"
]
};
// FAQPage Schema for common questions
const faqJsonLd = {
"@context": "https://schema.org",
"@type": "FAQPage",
"mainEntity": [
{
"@type": "Question",
"name": "Welche Zahlungsmethoden akzeptiert GrowHeads?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Wir akzeptieren Kreditkarten, PayPal, Banküberweisung, Nachnahme und Barzahlung bei Abholung in unserem Laden in Dresden."
}
},
{
"@type": "Question",
"name": "Liefert GrowHeads deutschlandweit?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Ja, wir liefern deutschlandweit. Zusätzlich haben wir einen Laden in Dresden (Trachenberger Strasse 14) für lokale Kunden."
}
},
{
"@type": "Question",
"name": "Welche Produkte bietet GrowHeads?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Wir bieten ein komplettes Sortiment für den Indoor-Anbau: Beleuchtung, Belüftung, Dünger, Töpfe, Zelte, Messgeräte und vieles mehr für professionelle Zuchtanlagen."
}
},
{
"@type": "Question",
"name": "Hat GrowHeads einen physischen Laden?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Ja, unser Laden befindet sich in Dresden, Trachenberger Strasse 14. Öffnungszeiten: Mo-Fr 10-20 Uhr, Sa 11-19 Uhr. Sie können auch online bestellen."
}
},
{
"@type": "Question",
"name": "Bietet GrowHeads Beratung zum Indoor-Anbau?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Ja, unser erfahrenes Team berät Sie gerne zu allen Aspekten des Indoor-Anbaus. Kontaktieren Sie uns telefonisch unter 015208491860 oder besuchen Sie unseren Laden."
}
}
]
};
// Generate ItemList for all categories (more appropriate for homepage)
const categoriesListJsonLd = {
"@context": "https://schema.org",
"@type": "ItemList",
"name": "Produktkategorien",
"description": "Alle verfügbaren Produktkategorien in unserem Online-Shop",
"numberOfItems": categories.filter(category => category.seoName).length,
"itemListElement": categories
.filter(category => category.seoName) // Only include categories with seoName
.map((category, index) => ({
"@type": "ListItem",
"position": index + 1,
"item": {
"@type": "Thing",
"name": category.name,
"url": `${canonicalUrl}/Kategorie/${category.seoName}`
}
}))
};
// Return all JSON-LD scripts
const websiteScript = `<script type="application/ld+json">${JSON.stringify(websiteJsonLd)}</script>`;
const organizationScript = `<script type="application/ld+json">${JSON.stringify(organizationJsonLd)}</script>`;
const faqScript = `<script type="application/ld+json">${JSON.stringify(faqJsonLd)}</script>`;
const categoriesScript = categories.length > 0
? `<script type="application/ld+json">${JSON.stringify(categoriesListJsonLd)}</script>`
: '';
return websiteScript + '\n' + organizationScript + '\n' + faqScript + (categoriesScript ? '\n' + categoriesScript : '');
};
module.exports = {
generateHomepageMetaTags,
generateHomepageJsonLd,
};

64
prerender/seo/index.cjs Normal file
View File

@@ -0,0 +1,64 @@
// Import all SEO functions from their respective modules
const {
generateProductMetaTags,
generateProductJsonLd,
} = require('./product.cjs');
const {
generateCategoryJsonLd,
} = require('./category.cjs');
const {
generateHomepageMetaTags,
generateHomepageJsonLd,
} = require('./homepage.cjs');
const {
generateSitemapJsonLd,
generateXmlSitemap,
} = require('./sitemap.cjs');
const {
generateKonfiguratorMetaTags,
} = require('./konfigurator.cjs');
const {
generateRobotsTxt,
generateProductsXml,
} = require('./feeds.cjs');
const {
generateLlmsTxt,
generateCategoryLlmsTxt,
generateAllCategoryLlmsPages,
} = require('./llms.cjs');
// Export all functions for use in the main application
module.exports = {
// Product functions
generateProductMetaTags,
generateProductJsonLd,
// Category functions
generateCategoryJsonLd,
// Homepage functions
generateHomepageMetaTags,
generateHomepageJsonLd,
// Sitemap functions
generateSitemapJsonLd,
generateXmlSitemap,
// Konfigurator functions
generateKonfiguratorMetaTags,
// Feed/Export functions
generateRobotsTxt,
generateProductsXml,
// LLMs/AI functions
generateLlmsTxt,
generateCategoryLlmsTxt,
generateAllCategoryLlmsPages,
};

View File

@@ -0,0 +1,36 @@
const generateKonfiguratorMetaTags = (baseUrl, config) => {
const description = "Unser interaktiver Growbox Konfigurator hilft dir dabei, das perfekte Indoor Growing Setup zusammenzustellen. Wähle aus verschiedenen Growbox-Größen, Beleuchtung, Belüftung und Extras. Bundle-Rabatte bis 36%!";
const keywords = "Growbox Konfigurator, Indoor Growing, Growzelt, Beleuchtung, Belüftung, Growbox Setup, Indoor Garden";
// Ensure URLs are properly formatted
const canonicalUrl = baseUrl.endsWith('/') ? baseUrl.slice(0, -1) : baseUrl;
const imageUrl = `${canonicalUrl}${config.images.placeholder}`; // Placeholder image
return `
<!-- SEO Meta Tags -->
<meta name="description" content="${description}">
<meta name="keywords" content="${keywords}">
<!-- Open Graph Meta Tags -->
<meta property="og:title" content="Growbox Konfigurator - Stelle dein perfektes Indoor Grow Setup zusammen">
<meta property="og:description" content="${description}">
<meta property="og:image" content="${imageUrl}">
<meta property="og:url" content="${canonicalUrl}/Konfigurator">
<meta property="og:type" content="website">
<meta property="og:site_name" content="${config.siteName}">
<!-- Twitter Card Meta Tags -->
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="Growbox Konfigurator - Indoor Grow Setup">
<meta name="twitter:description" content="${description}">
<meta name="twitter:image" content="${imageUrl}">
<!-- Additional Meta Tags -->
<meta name="robots" content="index, follow">
<link rel="canonical" href="${canonicalUrl}/Konfigurator">
`;
};
module.exports = {
generateKonfiguratorMetaTags,
};

277
prerender/seo/llms.cjs Normal file
View File

@@ -0,0 +1,277 @@
const generateLlmsTxt = (allCategories = [], allProductsData = [], baseUrl, config) => {
const currentDate = new Date().toISOString().split("T")[0]; // YYYY-MM-DD format
// Group products by category for statistics
const productsByCategory = {};
allProductsData.forEach((product) => {
const categoryId = product.categoryId || 'uncategorized';
if (!productsByCategory[categoryId]) {
productsByCategory[categoryId] = [];
}
productsByCategory[categoryId].push(product);
});
// Find category names for organization
const categoryMap = {};
allCategories.forEach((cat) => {
categoryMap[cat.id] = cat.name;
});
let llmsTxt = `# ${config.siteName} - Site Map for LLMs
Generated: ${currentDate}
Base URL: ${baseUrl}
## About ${config.brandName}
GrowHeads.de is a German online shop and local store in Dresden specializing in high-quality seeds, plants, and gardening supplies for cannabis cultivation.
## Site Structure
### Static Pages
- **Home** - ${baseUrl}/
- **Datenschutz (Privacy Policy)** - ${baseUrl}/datenschutz
- **Impressum (Legal Notice)** - ${baseUrl}/impressum
- **AGB (Terms & Conditions)** - ${baseUrl}/agb
- **Widerrufsrecht (Right of Withdrawal)** - ${baseUrl}/widerrufsrecht
- **Batteriegesetzhinweise (Battery Law Notice)** - ${baseUrl}/batteriegesetzhinweise
- **Sitemap** - ${baseUrl}/sitemap
- **Growbox Konfigurator** - ${baseUrl}/Konfigurator - Interactive tool to configure grow box setups with bundle discounts
- **Profile** - ${baseUrl}/profile - User account and order management
### Site Features
- **Language**: German (${config.language})
- **Currency**: ${config.currency} (Euro)
- **Shipping**: ${config.country}
- **Payment Methods**: Credit Cards, PayPal, Bank Transfer, Cash on Delivery, Cash on Pickup
### Product Categories (${allCategories.length} categories)
`;
// Add categories with links to their detailed LLM files
allCategories.forEach((category) => {
if (category.seoName) {
const productCount = productsByCategory[category.id]?.length || 0;
const categorySlug = category.seoName.toLowerCase().replace(/[^a-z0-9]/g, '-');
const productsPerPage = 50;
const totalPages = Math.ceil(productCount / productsPerPage);
llmsTxt += `#### ${category.name} (${productCount} products)`;
if (totalPages > 1) {
llmsTxt += `
- **Product Catalog**: ${totalPages} pages available
- **Page 1**: ${baseUrl}/llms-${categorySlug}-page-1.txt (Products 1-${Math.min(productsPerPage, productCount)})`;
if (totalPages > 2) {
llmsTxt += `
- **Page 2**: ${baseUrl}/llms-${categorySlug}-page-2.txt (Products ${productsPerPage + 1}-${Math.min(productsPerPage * 2, productCount)})`;
}
if (totalPages > 3) {
llmsTxt += `
- **...**: Additional pages available`;
}
if (totalPages > 2) {
llmsTxt += `
- **Page ${totalPages}**: ${baseUrl}/llms-${categorySlug}-page-${totalPages}.txt (Products ${((totalPages - 1) * productsPerPage) + 1}-${productCount})`;
}
llmsTxt += `
- **Access Pattern**: Replace "page-X" with desired page number (1-${totalPages})`;
} else if (productCount > 0) {
llmsTxt += `
- **Product Catalog**: ${baseUrl}/llms-${categorySlug}-page-1.txt`;
} else {
llmsTxt += `
- **Product Catalog**: No products available`;
}
llmsTxt += `
`;
}
});
llmsTxt += `
---
*This sitemap is automatically generated during the site build process and includes all publicly accessible content. For technical inquiries, please refer to our contact information in the Impressum.*
`;
return llmsTxt;
};
const generateCategoryLlmsTxt = (category, categoryProducts = [], baseUrl, config, pageNumber = 1, productsPerPage = 50) => {
const currentDate = new Date().toISOString().split("T")[0]; // YYYY-MM-DD format
const categorySlug = category.seoName.toLowerCase().replace(/[^a-z0-9]/g, '-');
// Calculate pagination
const totalProducts = categoryProducts.length;
const totalPages = Math.ceil(totalProducts / productsPerPage);
const startIndex = (pageNumber - 1) * productsPerPage;
const endIndex = Math.min(startIndex + productsPerPage, totalProducts);
const pageProducts = categoryProducts.slice(startIndex, endIndex);
let categoryLlmsTxt = `# ${category.name} - Product Catalog (Page ${pageNumber} of ${totalPages})
Generated: ${currentDate}
Base URL: ${baseUrl}
Category: ${category.name} (ID: ${category.id})
Category URL: ${baseUrl}/Kategorie/${category.seoName}
## Category Overview
This file contains products ${startIndex + 1}-${endIndex} of ${totalProducts} in the "${category.name}" category from ${config.siteName}.
**Statistics:**
- **Total Products in Category**: ${totalProducts}
- **Products on This Page**: ${pageProducts.length}
- **Current Page**: ${pageNumber} of ${totalPages}
- **Category ID**: ${category.id}
- **Category URL**: ${baseUrl}/Kategorie/${category.seoName}
- **Back to Main Sitemap**: ${baseUrl}/llms.txt
`;
// Add navigation hints for LLMs
if (totalPages > 1) {
categoryLlmsTxt += `## Navigation for LLMs
**How to access other pages in this category:**
`;
if (pageNumber > 1) {
categoryLlmsTxt += `- **Previous Page**: ${baseUrl}/llms-${categorySlug}-page-${pageNumber - 1}.txt
`;
}
if (pageNumber < totalPages) {
categoryLlmsTxt += `- **Next Page**: ${baseUrl}/llms-${categorySlug}-page-${pageNumber + 1}.txt
`;
}
categoryLlmsTxt += `- **First Page**: ${baseUrl}/llms-${categorySlug}-page-1.txt
- **Last Page**: ${baseUrl}/llms-${categorySlug}-page-${totalPages}.txt
**All pages in this category:**
`;
for (let i = 1; i <= totalPages; i++) {
categoryLlmsTxt += `- **Page ${i}**: ${baseUrl}/llms-${categorySlug}-page-${i}.txt (Products ${((i-1) * productsPerPage) + 1}-${Math.min(i * productsPerPage, totalProducts)})
`;
}
categoryLlmsTxt += `
`;
}
if (pageProducts.length > 0) {
pageProducts.forEach((product, index) => {
if (product.seoName) {
// Clean description for markdown (remove HTML tags and limit length)
const cleanDescription = product.description
? product.description
.replace(/<[^>]*>/g, "")
.replace(/\n/g, " ")
.trim()
.substring(0, 300)
: "";
const globalIndex = startIndex + index + 1;
categoryLlmsTxt += `## ${globalIndex}. ${product.name}
- **Product URL**: ${baseUrl}/Artikel/${product.seoName}
- **Article Number**: ${product.articleNumber || 'N/A'}
- **Price**: €${product.price || '0.00'}
- **Brand**: ${product.manufacturer || config.brandName}
- **Availability**: ${product.available ? 'In Stock' : 'Out of Stock'}`;
if (product.gtin) {
categoryLlmsTxt += `
- **GTIN**: ${product.gtin}`;
}
if (product.weight && !isNaN(product.weight)) {
categoryLlmsTxt += `
- **Weight**: ${product.weight}g`;
}
if (cleanDescription) {
categoryLlmsTxt += `
**Description:**
${cleanDescription}${product.description && product.description.length > 300 ? '...' : ''}`;
}
categoryLlmsTxt += `
---
`;
}
});
} else {
categoryLlmsTxt += `## No Products Available
This category currently contains no products.
`;
}
// Add footer navigation for convenience
if (totalPages > 1) {
categoryLlmsTxt += `## Page Navigation
`;
if (pageNumber > 1) {
categoryLlmsTxt += `← [Previous Page](${baseUrl}/llms-${categorySlug}-page-${pageNumber - 1}.txt) | `;
}
categoryLlmsTxt += `[Category Overview](${baseUrl}/llms-${categorySlug}-page-1.txt)`;
if (pageNumber < totalPages) {
categoryLlmsTxt += ` | [Next Page](${baseUrl}/llms-${categorySlug}-page-${pageNumber + 1}.txt) →`;
}
categoryLlmsTxt += `
`;
}
categoryLlmsTxt += `---
*This category product list is automatically generated during the site build process. Product availability and pricing are updated in real-time on the main website.*
`;
return categoryLlmsTxt;
};
// Helper function to generate all pages for a category
const generateAllCategoryLlmsPages = (category, categoryProducts = [], baseUrl, config, productsPerPage = 50) => {
const totalProducts = categoryProducts.length;
const totalPages = Math.ceil(totalProducts / productsPerPage);
const pages = [];
for (let pageNumber = 1; pageNumber <= totalPages; pageNumber++) {
const pageContent = generateCategoryLlmsTxt(category, categoryProducts, baseUrl, config, pageNumber, productsPerPage);
const categorySlug = category.seoName.toLowerCase().replace(/[^a-z0-9]/g, '-');
const fileName = `llms-${categorySlug}-page-${pageNumber}.txt`;
pages.push({
fileName,
content: pageContent,
pageNumber,
totalPages
});
}
return pages;
};
module.exports = {
generateLlmsTxt,
generateCategoryLlmsTxt,
generateAllCategoryLlmsPages,
};

135
prerender/seo/product.cjs Normal file
View File

@@ -0,0 +1,135 @@
const generateProductMetaTags = (product, baseUrl, config) => {
const productUrl = `${baseUrl}/Artikel/${product.seoName}`;
const imageUrl =
product.pictureList && product.pictureList.trim()
? `${baseUrl}/assets/images/prod${product.pictureList
.split(",")[0]
.trim()}.jpg`
: `${baseUrl}/assets/images/nopicture.jpg`;
// Clean description for meta (remove HTML tags and limit length)
const cleanDescription = product.description
? product.description
.replace(/<[^>]*>/g, "")
.replace(/\n/g, " ")
.substring(0, 160)
: `${product.name} - Art.-Nr.: ${product.articleNumber}`;
return `
<!-- SEO Meta Tags -->
<meta name="description" content="${cleanDescription}">
<meta name="keywords" content="${product.name}, ${
product.manufacturer || ""
}, ${product.articleNumber}">
<!-- Open Graph Meta Tags -->
<meta property="og:title" content="${product.name}">
<meta property="og:description" content="${cleanDescription}">
<meta property="og:image" content="${imageUrl}">
<meta property="og:url" content="${productUrl}">
<meta property="og:type" content="product">
<meta property="og:site_name" content="${config.siteName}">
<meta property="product:price:amount" content="${product.price}">
<meta property="product:price:currency" content="${config.currency}">
<meta property="product:availability" content="${
product.available ? "in stock" : "out of stock"
}">
${product.gtin ? `<meta property="product:gtin" content="${product.gtin}">` : ''}
${product.articleNumber ? `<meta property="product:retailer_item_id" content="${product.articleNumber}">` : ''}
${product.manufacturer ? `<meta property="product:brand" content="${product.manufacturer}">` : ''}
<!-- Twitter Card Meta Tags -->
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="${product.name}">
<meta name="twitter:description" content="${cleanDescription}">
<meta name="twitter:image" content="${imageUrl}">
<!-- Additional Meta Tags -->
<meta name="robots" content="index, follow">
<link rel="canonical" href="${productUrl}">
`;
};
const generateProductJsonLd = (product, baseUrl, config, categoryInfo = null) => {
const productUrl = `${baseUrl}/Artikel/${product.seoName}`;
const imageUrl =
product.pictureList && product.pictureList.trim()
? `${baseUrl}/assets/images/prod${product.pictureList
.split(",")[0]
.trim()}.jpg`
: `${baseUrl}/assets/images/nopicture.jpg`;
// Clean description for JSON-LD (remove HTML tags)
const cleanDescription = product.description
? product.description.replace(/<[^>]*>/g, "").replace(/\n/g, " ")
: product.name;
// Calculate price valid date (current date + 3 months)
const priceValidDate = new Date();
priceValidDate.setMonth(priceValidDate.getMonth() + 3);
const jsonLd = {
"@context": "https://schema.org/",
"@type": "Product",
name: product.name,
image: [imageUrl],
description: cleanDescription,
sku: product.articleNumber,
...(product.gtin && { gtin: product.gtin }),
brand: {
"@type": "Brand",
name: product.manufacturer || "Unknown",
},
offers: {
"@type": "Offer",
url: productUrl,
priceCurrency: config.currency,
price: product.price.toString(),
priceValidUntil: priceValidDate.toISOString().split("T")[0],
itemCondition: "https://schema.org/NewCondition",
availability: product.available
? "https://schema.org/InStock"
: "https://schema.org/OutOfStock",
seller: {
"@type": "Organization",
name: config.brandName,
},
},
};
// Add breadcrumb if category information is available
if (categoryInfo && categoryInfo.name && categoryInfo.seoName) {
jsonLd.breadcrumb = {
"@type": "BreadcrumbList",
itemListElement: [
{
"@type": "ListItem",
position: 1,
name: "Home",
item: baseUrl,
},
{
"@type": "ListItem",
position: 2,
name: categoryInfo.name,
item: `${baseUrl}/Kategorie/${categoryInfo.seoName}`,
},
{
"@type": "ListItem",
position: 3,
name: product.name,
item: productUrl,
},
],
};
}
return `<script type="application/ld+json">${JSON.stringify(
jsonLd
)}</script>`;
};
module.exports = {
generateProductMetaTags,
generateProductJsonLd,
};

117
prerender/seo/sitemap.cjs Normal file
View File

@@ -0,0 +1,117 @@
const generateSitemapJsonLd = (allCategories = [], baseUrl, config) => {
const jsonLd = {
"@context": "https://schema.org/",
"@type": "WebPage",
name: "Sitemap",
url: `${baseUrl}/sitemap`,
description: `Sitemap - Übersicht aller Kategorien und Seiten auf ${config.siteName}`,
breadcrumb: {
"@type": "BreadcrumbList",
itemListElement: [
{
"@type": "ListItem",
position: 1,
name: "Home",
item: baseUrl,
},
{
"@type": "ListItem",
position: 2,
name: "Sitemap",
item: `${baseUrl}/sitemap`,
},
],
},
};
// Add all categories as site navigation elements
if (allCategories && allCategories.length > 0) {
jsonLd.mainEntity = {
"@type": "SiteNavigationElement",
name: "Kategorien",
hasPart: allCategories.map((category) => ({
"@type": "SiteNavigationElement",
name: category.name,
url: `${baseUrl}/Kategorie/${category.seoName}`,
description: `${category.name} Kategorie`,
})),
};
}
return `<script type="application/ld+json">${JSON.stringify(
jsonLd
)}</script>`;
};
const generateXmlSitemap = (allCategories = [], allProducts = [], baseUrl) => {
const currentDate = new Date().toISOString().split("T")[0]; // YYYY-MM-DD format
let sitemap = `<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
`;
// Homepage
sitemap += ` <url>
<loc>${baseUrl}/</loc>
<lastmod>${currentDate}</lastmod>
<changefreq>daily</changefreq>
<priority>1.0</priority>
</url>
`;
// Static pages
const staticPages = [
{ path: "/datenschutz", changefreq: "monthly", priority: "0.3" },
{ path: "/impressum", changefreq: "monthly", priority: "0.3" },
{ path: "/batteriegesetzhinweise", changefreq: "monthly", priority: "0.3" },
{ path: "/widerrufsrecht", changefreq: "monthly", priority: "0.3" },
{ path: "/sitemap", changefreq: "weekly", priority: "0.5" },
{ path: "/agb", changefreq: "monthly", priority: "0.3" },
{ path: "/404", changefreq: "monthly", priority: "0.1" },
{ path: "/Konfigurator", changefreq: "weekly", priority: "0.8" },
];
staticPages.forEach((page) => {
sitemap += ` <url>
<loc>${baseUrl}${page.path}</loc>
<lastmod>${currentDate}</lastmod>
<changefreq>${page.changefreq}</changefreq>
<priority>${page.priority}</priority>
</url>
`;
});
// Category pages
allCategories.forEach((category) => {
if (category.seoName) {
sitemap += ` <url>
<loc>${baseUrl}/Kategorie/${category.seoName}</loc>
<lastmod>${currentDate}</lastmod>
<changefreq>weekly</changefreq>
<priority>0.8</priority>
</url>
`;
}
});
// Product pages
allProducts.forEach((productSeoName) => {
sitemap += ` <url>
<loc>${baseUrl}/Artikel/${productSeoName}</loc>
<lastmod>${currentDate}</lastmod>
<changefreq>weekly</changefreq>
<priority>0.6</priority>
</url>
`;
});
sitemap += `</urlset>`;
return sitemap;
};
module.exports = {
generateSitemapJsonLd,
generateXmlSitemap,
};

137
src/PrerenderSitemap.js Normal file
View File

@@ -0,0 +1,137 @@
const React = require('react');
const {
Box,
AppBar,
Toolbar,
Container,
Typography,
List,
ListItem,
ListItemText
} = require('@mui/material');
const Footer = require('./components/Footer.js').default;
const { Logo, CategoryList } = require('./components/header/index.js');
const LegalPage = require('./pages/LegalPage.js').default;
const PrerenderSitemap = ({ categoryData }) => {
// Process category data to flatten the hierarchy
const collectAllCategories = (categoryNode, categories = [], level = 0) => {
if (!categoryNode) return categories;
// Add current category (skip root category 209)
if (categoryNode.id !== 209 && categoryNode.seoName) {
categories.push({
id: categoryNode.id,
name: categoryNode.name,
seoName: categoryNode.seoName,
level: level
});
}
// Recursively add children
if (categoryNode.children) {
for (const child of categoryNode.children) {
collectAllCategories(child, categories, level + 1);
}
}
return categories;
};
const categories = categoryData ? collectAllCategories(categoryData) : [];
const sitemapLinks = [
{ title: 'Startseite', url: '/' },
{ title: 'Mein Profil', url: '/profile' },
{ title: 'Datenschutz', url: '/datenschutz' },
{ title: 'AGB', url: '/agb' },
{ title: 'Impressum', url: '/impressum' },
{ title: 'Batteriegesetzhinweise', url: '/batteriegesetzhinweise' },
{ title: 'Widerrufsrecht', url: '/widerrufsrecht' },
{ title: 'Growbox Konfigurator', url: '/Konfigurator' },
{ title: 'API', url: '/api/', route: false },
];
const content = React.createElement(
React.Fragment,
null,
React.createElement(
Typography,
{ variant: 'body1', paragraph: true },
'Hier finden Sie eine Übersicht aller verfügbaren Seiten unserer Website.'
),
// Static site links
React.createElement(
Typography,
{ variant: 'h6', sx: { mt: 3, mb: 2, fontWeight: 'bold' } },
'Seiten'
),
React.createElement(
List,
null,
sitemapLinks.map((link) =>
React.createElement(
ListItem,
{
key: link.url,
button: true,
component: link.route === false ? 'a' : 'a',
href: link.url,
sx: {
py: 1,
borderBottom: '1px solid',
borderColor: 'divider'
}
},
React.createElement(ListItemText, { primary: link.title })
)
)
),
// Category links
React.createElement(
Typography,
{ variant: 'h6', sx: { mt: 4, mb: 2, fontWeight: 'bold' } },
'Kategorien'
),
React.createElement(
List,
null,
categories.map((category) =>
React.createElement(
ListItem,
{
key: category.id,
button: true,
component: 'a',
href: `/Kategorie/${category.seoName}`,
sx: {
py: 1,
pl: 2 + (category.level * 2), // Indent based on category level
borderBottom: '1px solid',
borderColor: 'divider'
}
},
React.createElement(
ListItemText,
{
primary: category.name,
sx: {
'& .MuiTypography-root': {
fontSize: category.level === 0 ? '1rem' : '0.9rem',
fontWeight: category.level === 0 ? 'bold' : 'normal',
color: category.level === 0 ? 'primary.main' : 'text.primary'
}
}
}
)
)
)
)
);
return React.createElement(LegalPage, { title: 'Sitemap', content: content });
};
module.exports = { default: PrerenderSitemap };

View File

@@ -97,7 +97,12 @@ function getFilteredProducts(unfilteredProducts, attributes) {
let filteredProducts = (unfilteredProducts || []).filter(product => { let filteredProducts = (unfilteredProducts || []).filter(product => {
const availabilityFilter = sessionStorage.getItem('filter_availability'); const availabilityFilter = sessionStorage.getItem('filter_availability');
let inStockMatch = availabilityFilter == 1 ? true : (product.available>0); let inStockMatch = availabilityFilter == 1 ? true : (product.available>0);
const isNewMatch = availabilityFilters.includes('2') ? isNew(product.neu) : true;
// Check if there are any new products in the entire set
const hasNewProducts = (unfilteredProducts || []).some(product => isNew(product.neu));
// Only apply the new filter if there are actually new products and the filter is active
const isNewMatch = availabilityFilters.includes('2') && hasNewProducts ? isNew(product.neu) : true;
let soonMatch = availabilityFilters.includes('3') ? !product.available && product.incoming : true; let soonMatch = availabilityFilters.includes('3') ? !product.available && product.incoming : true;
const soon2Match = (availabilityFilter != 1)&&availabilityFilters.includes('3') ? (product.available) || (!product.available && product.incoming) : true; const soon2Match = (availabilityFilter != 1)&&availabilityFilters.includes('3') ? (product.available) || (!product.available && product.incoming) : true;
@@ -133,7 +138,31 @@ function getFilteredProducts(unfilteredProducts, attributes) {
const manufacturer = uniqueManufacturersWithName.find(manufacturer => manufacturer.id === filter); const manufacturer = uniqueManufacturersWithName.find(manufacturer => manufacturer.id === filter);
return {name: manufacturer.value, value: manufacturer.id}; return {name: manufacturer.value, value: manufacturer.id};
}); });
return {filteredProducts,activeAttributeFilters:activeAttributeFiltersWithNames,activeManufacturerFilters:activeManufacturerFiltersWithNames};
// Extract active availability filters
const availabilityFilter = sessionStorage.getItem('filter_availability');
const activeAvailabilityFilters = [];
// Check if there are actually products with these characteristics
const hasNewProducts = (unfilteredProducts || []).some(product => isNew(product.neu));
const hasComingSoonProducts = (unfilteredProducts || []).some(product => !product.available && product.incoming);
// Check for "auf Lager" filter (in stock) - it's active when filter_availability is NOT set to '1'
if (availabilityFilter !== '1') {
activeAvailabilityFilters.push({id: '1', name: 'auf Lager'});
}
// Check for "Neu" filter (new) - only show if there are actually new products and filter is active
if (availabilityFilters.includes('2') && hasNewProducts) {
activeAvailabilityFilters.push({id: '2', name: 'Neu'});
}
// Check for "Bald verfügbar" filter (coming soon) - only show if there are actually coming soon products and filter is active
if (availabilityFilters.includes('3') && hasComingSoonProducts) {
activeAvailabilityFilters.push({id: '3', name: 'Bald verfügbar'});
}
return {filteredProducts,activeAttributeFilters:activeAttributeFiltersWithNames,activeManufacturerFilters:activeManufacturerFiltersWithNames,activeAvailabilityFilters};
} }
function setCachedCategoryData(categoryId, data) { function setCachedCategoryData(categoryId, data) {
if (!window.productCache) { if (!window.productCache) {
@@ -680,6 +709,7 @@ class Content extends Component {
products={this.state.filteredProducts || []} products={this.state.filteredProducts || []}
activeAttributeFilters={this.state.activeAttributeFilters || []} activeAttributeFilters={this.state.activeAttributeFilters || []}
activeManufacturerFilters={this.state.activeManufacturerFilters || []} activeManufacturerFilters={this.state.activeManufacturerFilters || []}
activeAvailabilityFilters={this.state.activeAvailabilityFilters || []}
onFilterChange={()=>{this.filterProducts()}} onFilterChange={()=>{this.filterProducts()}}
dataType={this.state.dataType} dataType={this.state.dataType}
dataParam={this.state.dataParam} dataParam={this.state.dataParam}

381
src/components/Mollie.js Normal file
View File

@@ -0,0 +1,381 @@
import React, { Component, useState } from "react";
import { Button, Box, Typography, CircularProgress } from "@mui/material";
import config from "../config.js";
// Function to lazy load Mollie script
const loadMollie = () => {
return new Promise((resolve, reject) => {
// Check if Mollie is already loaded
if (window.Mollie) {
resolve(window.Mollie);
return;
}
// Create script element
const script = document.createElement('script');
script.src = 'https://js.mollie.com/v1/mollie.js';
script.async = true;
script.onload = () => {
if (window.Mollie) {
resolve(window.Mollie);
} else {
reject(new Error('Mollie failed to load'));
}
};
script.onerror = () => {
reject(new Error('Failed to load Mollie script'));
};
document.head.appendChild(script);
});
};
const CheckoutForm = ({ mollie }) => {
const [errorMessage, setErrorMessage] = useState(null);
const [isSubmitting, setIsSubmitting] = useState(false);
React.useEffect(() => {
if (!mollie) return;
let mountedComponents = {
cardNumber: null,
cardHolder: null,
expiryDate: null,
verificationCode: null
};
try {
// Create Mollie components
const cardNumber = mollie.createComponent('cardNumber');
const cardHolder = mollie.createComponent('cardHolder');
const expiryDate = mollie.createComponent('expiryDate');
const verificationCode = mollie.createComponent('verificationCode');
// Store references for cleanup
mountedComponents = {
cardNumber,
cardHolder,
expiryDate,
verificationCode
};
// Mount components
cardNumber.mount('#card-number');
cardHolder.mount('#card-holder');
expiryDate.mount('#expiry-date');
verificationCode.mount('#verification-code');
// Set up error handling
cardNumber.addEventListener('change', event => {
const errorElement = document.querySelector('#card-number-error');
if (errorElement) {
if (event.error && event.touched) {
errorElement.textContent = event.error;
} else {
errorElement.textContent = '';
}
}
});
cardHolder.addEventListener('change', event => {
const errorElement = document.querySelector('#card-holder-error');
if (errorElement) {
if (event.error && event.touched) {
errorElement.textContent = event.error;
} else {
errorElement.textContent = '';
}
}
});
expiryDate.addEventListener('change', event => {
const errorElement = document.querySelector('#expiry-date-error');
if (errorElement) {
if (event.error && event.touched) {
errorElement.textContent = event.error;
} else {
errorElement.textContent = '';
}
}
});
verificationCode.addEventListener('change', event => {
const errorElement = document.querySelector('#verification-code-error');
if (errorElement) {
if (event.error && event.touched) {
errorElement.textContent = event.error;
} else {
errorElement.textContent = '';
}
}
});
// Components are now mounted and ready
} catch (error) {
console.error('Error creating Mollie components:', error);
setErrorMessage('Failed to initialize payment form. Please try again.');
}
// Cleanup function
return () => {
try {
if (mountedComponents.cardNumber) mountedComponents.cardNumber.unmount();
if (mountedComponents.cardHolder) mountedComponents.cardHolder.unmount();
if (mountedComponents.expiryDate) mountedComponents.expiryDate.unmount();
if (mountedComponents.verificationCode) mountedComponents.verificationCode.unmount();
} catch (error) {
console.error('Error cleaning up Mollie components:', error);
}
};
}, [mollie]);
const handleSubmit = async (event) => {
event.preventDefault();
if (!mollie || isSubmitting) {
return;
}
setIsSubmitting(true);
setErrorMessage(null);
try {
const { token, error } = await mollie.createToken();
if (error) {
setErrorMessage(error.message || 'Payment failed. Please try again.');
setIsSubmitting(false);
return;
}
if (token) {
// Handle successful token creation
// Create a payment completion event similar to Stripe
const mollieCompletionData = {
mollieToken: token,
paymentMethod: 'mollie'
};
// Dispatch a custom event to notify the parent component
const completionEvent = new CustomEvent('molliePaymentComplete', {
detail: mollieCompletionData
});
window.dispatchEvent(completionEvent);
// For now, redirect to profile with completion data
const returnUrl = `${window.location.origin}/profile?complete&mollie_token=${token}`;
window.location.href = returnUrl;
}
} catch (error) {
console.error('Error creating Mollie token:', error);
setErrorMessage('Payment failed. Please try again.');
setIsSubmitting(false);
}
};
return (
<Box sx={{ maxWidth: 600, mx: 'auto', p: 3 }}>
<Typography variant="h6" gutterBottom>
Kreditkarte oder Sofortüberweisung
</Typography>
<form onSubmit={handleSubmit}>
<Box sx={{ mb: 3 }}>
<Typography variant="body2" gutterBottom>
Kartennummer
</Typography>
<Box
id="card-number"
sx={{
p: 2,
border: '1px solid #e0e0e0',
borderRadius: 1,
minHeight: 40,
backgroundColor: '#fff'
}}
/>
<Typography
id="card-number-error"
variant="caption"
sx={{ color: 'error.main', minHeight: 16, display: 'block' }}
/>
</Box>
<Box sx={{ mb: 3 }}>
<Typography variant="body2" gutterBottom>
Karteninhaber
</Typography>
<Box
id="card-holder"
sx={{
p: 2,
border: '1px solid #e0e0e0',
borderRadius: 1,
minHeight: 40,
backgroundColor: '#fff'
}}
/>
<Typography
id="card-holder-error"
variant="caption"
sx={{ color: 'error.main', minHeight: 16, display: 'block' }}
/>
</Box>
<Box sx={{ display: 'flex', gap: 2, mb: 3 }}>
<Box sx={{ flex: 1 }}>
<Typography variant="body2" gutterBottom>
Ablaufdatum
</Typography>
<Box
id="expiry-date"
sx={{
p: 2,
border: '1px solid #e0e0e0',
borderRadius: 1,
minHeight: 40,
backgroundColor: '#fff'
}}
/>
<Typography
id="expiry-date-error"
variant="caption"
sx={{ color: 'error.main', minHeight: 16, display: 'block' }}
/>
</Box>
<Box sx={{ flex: 1 }}>
<Typography variant="body2" gutterBottom>
Sicherheitscode
</Typography>
<Box
id="verification-code"
sx={{
p: 2,
border: '1px solid #e0e0e0',
borderRadius: 1,
minHeight: 40,
backgroundColor: '#fff'
}}
/>
<Typography
id="verification-code-error"
variant="caption"
sx={{ color: 'error.main', minHeight: 16, display: 'block' }}
/>
</Box>
</Box>
<Button
variant="contained"
disabled={!mollie || isSubmitting}
type="submit"
fullWidth
sx={{
mt: 2,
backgroundColor: '#2e7d32',
'&:hover': {
backgroundColor: '#1b5e20'
}
}}
>
{isSubmitting ? (
<>
<CircularProgress size={20} sx={{ mr: 1, color: 'white' }} />
Verarbeitung...
</>
) : (
'Bezahlung Abschließen'
)}
</Button>
{errorMessage && (
<Typography
variant="body2"
sx={{ color: 'error.main', mt: 2, textAlign: 'center' }}
>
{errorMessage}
</Typography>
)}
</form>
</Box>
);
};
class Mollie extends Component {
constructor(props) {
super(props);
this.state = {
mollie: null,
loading: true,
error: null,
};
this.molliePromise = loadMollie();
}
componentDidMount() {
this.molliePromise
.then((MollieClass) => {
try {
// Initialize Mollie with profile key
const mollie = MollieClass(config.mollieProfileKey, {
locale: 'de_DE',
testmode: true // Set to false for production
});
this.setState({ mollie, loading: false });
} catch (error) {
console.error('Error initializing Mollie:', error);
this.setState({
error: 'Failed to initialize payment system. Please try again.',
loading: false
});
}
})
.catch((error) => {
console.error('Error loading Mollie:', error);
this.setState({
error: 'Failed to load payment system. Please try again.',
loading: false
});
});
}
render() {
const { mollie, loading, error } = this.state;
if (loading) {
return (
<Box sx={{ textAlign: 'center', py: 4 }}>
<CircularProgress sx={{ color: '#2e7d32' }} />
<Typography variant="body1" sx={{ mt: 2 }}>
Zahlungskomponente wird geladen...
</Typography>
</Box>
);
}
if (error) {
return (
<Box sx={{ textAlign: 'center', py: 4 }}>
<Typography variant="body1" sx={{ color: 'error.main' }}>
{error}
</Typography>
<Button
variant="outlined"
onClick={() => window.location.reload()}
sx={{ mt: 2 }}
>
Seite neu laden
</Button>
</Box>
);
}
return <CheckoutForm mollie={mollie} />;
}
}
export default Mollie;

View File

@@ -122,13 +122,17 @@ class ProductList extends Component {
} }
renderPagination = (pages, page) => { renderPagination = (pages, page) => {
// Make pagination invisible when there are zero products to avoid layout shifts
const hasProducts = this.props.products.length > 0;
return ( return (
<Box sx={{ <Box sx={{
height: 64, height: 64,
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
justifyContent: 'left', justifyContent: 'left',
width: '100%' width: '100%',
visibility: hasProducts ? 'visible' : 'hidden'
}}> }}>
{(this.state.itemsPerPage==='all')?null: {(this.state.itemsPerPage==='all')?null:
<Pagination <Pagination
@@ -156,6 +160,57 @@ class ProductList extends Component {
); );
} }
// Check if filters are active
hasActiveFilters = () => {
return (
(this.props.activeAttributeFilters && this.props.activeAttributeFilters.length > 0) ||
(this.props.activeManufacturerFilters && this.props.activeManufacturerFilters.length > 0) ||
(this.props.activeAvailabilityFilters && this.props.activeAvailabilityFilters.length > 0)
);
}
// Render message when no products found but filters are active
renderNoProductsMessage = () => {
const hasFiltersActive = this.hasActiveFilters();
const hasUnfilteredProducts = this.props.totalProductCount > 0;
if (this.props.products.length === 0 && hasUnfilteredProducts && hasFiltersActive) {
return (
<Box sx={{
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
py: 4,
px: 2
}}>
<Typography variant="h6" color="text.secondary" sx={{ textAlign: 'center' }}>
Entferne Filter um Produkte zu sehen
</Typography>
</Box>
);
}
return null;
}
// Helper function for correct pluralization
getProductCountText = () => {
const filteredCount = this.props.products.length;
const totalCount = this.props.totalProductCount;
const isFiltered = totalCount !== filteredCount;
if (!isFiltered) {
// No filters applied
if (filteredCount === 0) return "0 Produkte";
if (filteredCount === 1) return "1 Produkt";
return `${filteredCount} Produkte`;
} else {
// Filters applied
if (totalCount === 0) return "0 Produkte";
if (totalCount === 1) return `${filteredCount} von 1 Produkt`;
return `${filteredCount} von ${totalCount} Produkten`;
}
}
render() { render() {
//console.log('products',this.props.activeAttributeFilters,this.props.activeManufacturerFilters,window.currentSearchQuery,this.state.sortBy); //console.log('products',this.props.activeAttributeFilters,this.props.activeManufacturerFilters,window.currentSearchQuery,this.state.sortBy);
@@ -192,6 +247,34 @@ class ProductList extends Component {
order: { xs: 2, sm: 1 }, order: { xs: 2, sm: 1 },
px: { xs: 1, sm: 0 } px: { xs: 1, sm: 0 }
}}> }}>
{this.props.activeAvailabilityFilters && this.props.activeAvailabilityFilters.map((filter,index) => (
<Chip
size="medium"
key={`availability-${index}`}
label={filter.name}
onClick={() => {
if (filter.id === '1') {
// Add "auf Lager" filter by setting the sessionStorage item to '1'
sessionStorage.setItem('filter_availability', '1');
} else {
// Remove "Neu" or "Bald verfügbar" filters
removeSessionSetting(`filter_availability_${filter.id}`);
}
this.props.onFilterChange();
}}
onDelete={() => {
if (filter.id === '1') {
// Add "auf Lager" filter by setting the sessionStorage item to '1'
sessionStorage.setItem('filter_availability', '1');
} else {
// Remove "Neu" or "Bald verfügbar" filters
removeSessionSetting(`filter_availability_${filter.id}`);
}
this.props.onFilterChange();
}}
clickable
/>
))}
{this.props.activeAttributeFilters.map((filter,index) => ( {this.props.activeAttributeFilters.map((filter,index) => (
<Chip <Chip
size="medium" size="medium"
@@ -224,6 +307,7 @@ class ProductList extends Component {
clickable clickable
/> />
))} ))}
</Box> </Box>
<Box sx={{ <Box sx={{
@@ -323,13 +407,8 @@ class ProductList extends Component {
display: { xs: 'block', sm: 'none' }, display: { xs: 'block', sm: 'none' },
ml: 1 ml: 1
}}> }}>
<Typography variant="body2" color="text.secondary" sx={{ fontSize: '0.75rem' }}> <Typography variant="body2" color="text.secondary" sx={{ fontSize: '0.75rem', whiteSpace: 'nowrap' }}>
{ {this.getProductCountText()}
this.props.totalProductCount==this.props.products.length && this.props.totalProductCount>0 ?
`${this.props.totalProductCount} Produkte`
:
`${this.props.products.length} von ${this.props.totalProductCount} Produkte`
}
</Typography> </Typography>
</Box> </Box>
</Box> </Box>
@@ -352,18 +431,14 @@ class ProductList extends Component {
{/*this.props.dataType == 'category' && (<>Kategorie: {this.props.dataParam}</>)}*/} {/*this.props.dataType == 'category' && (<>Kategorie: {this.props.dataParam}</>)}*/}
{this.props.dataType == 'search' && (<>Suchergebnisse für: "{this.props.dataParam}"</>)} {this.props.dataType == 'search' && (<>Suchergebnisse für: "{this.props.dataParam}"</>)}
</Typography> </Typography>
<Typography variant="body2" color="text.secondary"> <Typography variant="body2" color="text.secondary" sx={{ whiteSpace: 'nowrap' }}>
{ {this.getProductCountText()}
this.props.totalProductCount==this.props.products.length && this.props.totalProductCount>0 ?
`${this.props.totalProductCount} Produkte`
:
`${this.props.products.length} von ${this.props.totalProductCount} Produkte`
}
</Typography> </Typography>
</Stack> </Stack>
</Box> </Box>
<Grid container spacing={{ xs: 0, sm: 2 }}> <Grid container spacing={{ xs: 0, sm: 2 }}>
{this.renderNoProductsMessage()}
{products.map((product, index) => ( {products.map((product, index) => (
<Grid <Grid
key={product.id} key={product.id}

View File

@@ -51,6 +51,9 @@ class CartTab extends Component {
showStripePayment: false, showStripePayment: false,
StripeComponent: null, StripeComponent: null,
isLoadingStripe: false, isLoadingStripe: false,
showMolliePayment: false,
MollieComponent: null,
isLoadingMollie: false,
showPaymentConfirmation: false, showPaymentConfirmation: false,
orderCompleted: false, orderCompleted: false,
originalCartItems: [] originalCartItems: []
@@ -116,7 +119,7 @@ class CartTab extends Component {
// Determine payment method - respect constraints // Determine payment method - respect constraints
let prefillPaymentMethod = template.payment_method || "wire"; let prefillPaymentMethod = template.payment_method || "wire";
const paymentMethodMap = { const paymentMethodMap = {
"credit_card": "stripe", "credit_card": "mollie",//stripe
"bank_transfer": "wire", "bank_transfer": "wire",
"cash_on_delivery": "onDelivery", "cash_on_delivery": "onDelivery",
"cash": "cash" "cash": "cash"
@@ -319,6 +322,27 @@ class CartTab extends Component {
} }
}; };
loadMollieComponent = async () => {
this.setState({ isLoadingMollie: true });
try {
const { default: Mollie } = await import("../Mollie.js");
this.setState({
MollieComponent: Mollie,
showMolliePayment: true,
isCompletingOrder: false,
isLoadingMollie: false,
});
} catch (error) {
console.error("Failed to load Mollie component:", error);
this.setState({
isCompletingOrder: false,
isLoadingMollie: false,
completionError: "Failed to load payment component. Please try again.",
});
}
};
handleCompleteOrder = () => { handleCompleteOrder = () => {
this.setState({ completionError: null }); // Clear previous errors this.setState({ completionError: null }); // Clear previous errors
@@ -363,6 +387,25 @@ class CartTab extends Component {
this.orderService.createStripeIntent(totalAmount, this.loadStripeComponent); this.orderService.createStripeIntent(totalAmount, this.loadStripeComponent);
return; return;
} }
// Handle Mollie payment differently
if (paymentMethod === "mollie") {
// Store the cart items used for Mollie payment in sessionStorage for later reference
try {
sessionStorage.setItem('molliePaymentCart', JSON.stringify(cartItems));
} catch (error) {
console.error("Failed to store Mollie payment cart:", error);
}
// Calculate total amount for Mollie
const subtotal = cartItems.reduce(
(total, item) => total + item.price * item.quantity,
0
);
const totalAmount = Math.round((subtotal + deliveryCost) * 100); // Convert to cents
this.orderService.createMollieIntent(totalAmount, this.loadMollieComponent);
return;
}
// Handle regular orders // Handle regular orders
const orderData = { const orderData = {
@@ -398,6 +441,9 @@ class CartTab extends Component {
showStripePayment, showStripePayment,
StripeComponent, StripeComponent,
isLoadingStripe, isLoadingStripe,
showMolliePayment,
MollieComponent,
isLoadingMollie,
showPaymentConfirmation, showPaymentConfirmation,
orderCompleted, orderCompleted,
} = this.state; } = this.state;
@@ -434,7 +480,7 @@ class CartTab extends Component {
<CartDropdown <CartDropdown
cartItems={cartItems} cartItems={cartItems}
socket={this.context.socket} socket={this.context.socket}
showDetailedSummary={showStripePayment} showDetailedSummary={showStripePayment || showMolliePayment}
deliveryMethod={deliveryMethod} deliveryMethod={deliveryMethod}
deliveryCost={deliveryCost} deliveryCost={deliveryCost}
/> />
@@ -442,7 +488,7 @@ class CartTab extends Component {
{cartItems.length > 0 && ( {cartItems.length > 0 && (
<Box sx={{ mt: 3 }}> <Box sx={{ mt: 3 }}>
{isLoadingStripe ? ( {isLoadingStripe || isLoadingMollie ? (
<Box sx={{ textAlign: 'center', py: 4 }}> <Box sx={{ textAlign: 'center', py: 4 }}>
<Typography variant="body1"> <Typography variant="body1">
Zahlungskomponente wird geladen... Zahlungskomponente wird geladen...
@@ -468,6 +514,26 @@ class CartTab extends Component {
</Box> </Box>
<StripeComponent clientSecret={stripeClientSecret} /> <StripeComponent clientSecret={stripeClientSecret} />
</> </>
) : showMolliePayment && MollieComponent ? (
<>
<Box sx={{ mb: 2 }}>
<Button
variant="outlined"
onClick={() => this.setState({ showMolliePayment: false })}
sx={{
color: '#2e7d32',
borderColor: '#2e7d32',
'&:hover': {
backgroundColor: 'rgba(46, 125, 50, 0.04)',
borderColor: '#1b5e20'
}
}}
>
Zurück zur Bestellung
</Button>
</Box>
<MollieComponent />
</>
) : ( ) : (
<CheckoutForm <CheckoutForm
paymentMethod={paymentMethod} paymentMethod={paymentMethod}

View File

@@ -270,6 +270,10 @@ class OrderProcessingService {
}); });
} }
} }
// Create Mollie payment intent
createMollieIntent(totalAmount, loadMollieComponent) {
loadMollieComponent();
}
// Calculate delivery cost // Calculate delivery cost
getDeliveryCost() { getDeliveryCost() {

View File

@@ -24,7 +24,7 @@ const PaymentMethodSelector = ({ paymentMethod, onChange, deliveryMethod, onDeli
// Handle delivery method changes - auto-switch to stripe when DHL/DPD is selected // Handle delivery method changes - auto-switch to stripe when DHL/DPD is selected
useEffect(() => { useEffect(() => {
if ((deliveryMethod === "DHL" || deliveryMethod === "DPD") && paymentMethod === "cash") { if ((deliveryMethod === "DHL" || deliveryMethod === "DPD") && paymentMethod === "cash") {
handlePaymentMethodChange({ target: { value: "stripe" } }); handlePaymentMethodChange({ target: { value: "mollie" } });
} }
}, [deliveryMethod, paymentMethod, handlePaymentMethodChange]); }, [deliveryMethod, paymentMethod, handlePaymentMethodChange]);
@@ -42,8 +42,22 @@ const PaymentMethodSelector = ({ paymentMethod, onChange, deliveryMethod, onDeli
description: "Bezahlen Sie per Banküberweisung", description: "Bezahlen Sie per Banküberweisung",
disabled: totalAmount === 0, disabled: totalAmount === 0,
}, },
{ /*{
id: "stripe", id: "stripe",
name: "Karte oder Sofortüberweisung (Stripe)",
description: totalAmount < 0.50 && totalAmount > 0
? "Bezahlen Sie per Karte oder Sofortüberweisung (Mindestbetrag: 0,50 €)"
: "Bezahlen Sie per Karte oder Sofortüberweisung",
disabled: totalAmount < 0.50 || (deliveryMethod !== "DHL" && deliveryMethod !== "DPD" && deliveryMethod !== "Abholung"),
icons: [
"/assets/images/giropay.png",
"/assets/images/maestro.png",
"/assets/images/mastercard.png",
"/assets/images/visa_electron.png",
],
},*/
{
id: "mollie",
name: "Karte oder Sofortüberweisung", name: "Karte oder Sofortüberweisung",
description: totalAmount < 0.50 && totalAmount > 0 description: totalAmount < 0.50 && totalAmount > 0
? "Bezahlen Sie per Karte oder Sofortüberweisung (Mindestbetrag: 0,50 €)" ? "Bezahlen Sie per Karte oder Sofortüberweisung (Mindestbetrag: 0,50 €)"

View File

@@ -3,6 +3,7 @@ const config = {
apiBaseUrl: "", apiBaseUrl: "",
googleClientId: "928121624463-jbgfdlgem22scs1k9c87ucg4ffvaik6o.apps.googleusercontent.com", googleClientId: "928121624463-jbgfdlgem22scs1k9c87ucg4ffvaik6o.apps.googleusercontent.com",
stripePublishableKey: "pk_test_51R7lltRtpe3h1vwJzIrDb5bcEigTLBHrtqj9SiPX7FOEATSuD6oJmKc8xpNp49ShpGJZb2GShHIUqj4zlSIz4olj00ipOuOAnu", stripePublishableKey: "pk_test_51R7lltRtpe3h1vwJzIrDb5bcEigTLBHrtqj9SiPX7FOEATSuD6oJmKc8xpNp49ShpGJZb2GShHIUqj4zlSIz4olj00ipOuOAnu",
mollieProfileKey: "pfl_AtcRTimCff",
// SEO and Business Information // SEO and Business Information
siteName: "Growheads.de", siteName: "Growheads.de",

View File

@@ -33,10 +33,42 @@ const collectAllCategories = (categoryNode, categories = [], level = 0) => {
return categories; return categories;
}; };
// Check for cached data - handle both browser and prerender environments
const getProductCache = () => {
if (typeof window !== "undefined" && window.productCache) {
return window.productCache;
}
if (
typeof global !== "undefined" &&
global.window &&
global.window.productCache
) {
return global.window.productCache;
}
return null;
};
// Initialize categories from cache if available (for prerendering)
const initializeCategories = () => {
const productCache = getProductCache();
if (productCache && productCache["categoryTree_209"]) {
const cached = productCache["categoryTree_209"];
const cacheAge = Date.now() - cached.timestamp;
const tenMinutes = 10 * 60 * 1000;
if (cacheAge < tenMinutes && cached.categoryTree) {
return collectAllCategories(cached.categoryTree);
}
}
return [];
};
const Sitemap = () => { const Sitemap = () => {
const [categories, setCategories] = useState([]); // Initialize categories and loading state together
const [loading, setLoading] = useState(true); const initialCategories = initializeCategories();
const {socket} = useContext(SocketContext); const [categories, setCategories] = useState(initialCategories);
const [loading, setLoading] = useState(initialCategories.length === 0);
const context = useContext(SocketContext);
const sitemapLinks = [ const sitemapLinks = [
@@ -53,8 +85,14 @@ const Sitemap = () => {
useEffect(() => { useEffect(() => {
const fetchCategories = () => { const fetchCategories = () => {
// Try cache first // If we already have categories from prerendering, we're done
if (window.productCache && window.productCache['categoryTree_209']) { if (categories.length > 0) {
setLoading(false);
return;
}
// Try cache first (for browser environment)
if (typeof window !== "undefined" && window.productCache && window.productCache['categoryTree_209']) {
const cached = window.productCache['categoryTree_209']; const cached = window.productCache['categoryTree_209'];
const cacheAge = Date.now() - cached.timestamp; const cacheAge = Date.now() - cached.timestamp;
const tenMinutes = 10 * 60 * 1000; const tenMinutes = 10 * 60 * 1000;
@@ -66,9 +104,9 @@ const Sitemap = () => {
} }
} }
// Otherwise, fetch from socket if available // Otherwise, fetch from socket if available (only in browser)
if (socket) { if (context && context.socket && context.socket.connected && typeof window !== "undefined") {
socket.emit('categoryList', { categoryId: 209 }, (response) => { context.socket.emit('categoryList', { categoryId: 209 }, (response) => {
if (response && response.categoryTree) { if (response && response.categoryTree) {
// Store in cache // Store in cache
try { try {
@@ -95,7 +133,7 @@ const Sitemap = () => {
}; };
fetchCategories(); fetchCategories();
}, [socket]); }, [context, categories.length]);
const content = ( const content = (
<> <>