Compare commits
41 Commits
b4b334609a
...
mollie
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ccceb8fe78 | ||
|
|
ea5ac762b2 | ||
|
|
40ec0287fd | ||
|
|
47364d3ad8 | ||
|
|
a6d7ed3e27 | ||
|
|
f8f03b45b8 | ||
|
|
eb0d5621e6 | ||
|
|
f81b9d12df | ||
|
|
8ea3b1b6a3 | ||
|
|
fb3450aa23 | ||
|
|
11f5b2cbfd | ||
|
|
5fc0c3213b | ||
|
|
8abf64ca38 | ||
|
|
e16ae9f5a6 | ||
|
|
63a8d7b81b | ||
|
|
d2d5d884d9 | ||
|
|
9fc4286b8e | ||
|
|
6afe2ca90f | ||
|
|
c2758d6dd4 | ||
|
|
f4787ad523 | ||
|
|
6b0b54dc16 | ||
|
|
ceed9f0714 | ||
|
|
0d92495914 | ||
|
|
341d575c83 | ||
|
|
db966bae12 | ||
|
|
20ea55705e | ||
|
|
569f053757 | ||
|
|
5d5c09abbf | ||
|
|
2dde151f87 | ||
|
|
379f75947a | ||
|
|
f24429caef | ||
|
|
e4d077e402 | ||
|
|
245f5067ed | ||
|
|
1ed06804a0 | ||
|
|
f326596f48 | ||
|
|
fca10153fc | ||
|
|
8884255696 | ||
|
|
de51f5f409 | ||
|
|
94b274b10d | ||
|
|
d6a92a79a3 | ||
|
|
f1860e0c8b |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -27,6 +27,7 @@
|
||||
/public/index.prerender.html
|
||||
/public/Konfigurator
|
||||
/public/profile
|
||||
/public/404
|
||||
|
||||
/public/products.xml
|
||||
/public/llms*
|
||||
@@ -45,7 +46,6 @@ yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# Editor directories and files
|
||||
.vscode
|
||||
.idea
|
||||
*.suo
|
||||
*.ntvs*
|
||||
|
||||
22
.vscode/launch.json
vendored
Normal file
22
.vscode/launch.json
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
// Launch configuration for starting the project in VSCode
|
||||
// This will install dependencies before starting the dev server
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
|
||||
{
|
||||
"type": "node-terminal",
|
||||
"name": "Start with API propxy to seedheads.de (Install Deps)",
|
||||
"request": "launch",
|
||||
"command": "npm run start:seedheads",
|
||||
"preLaunchTask": "npm: install",
|
||||
"cwd": "${workspaceFolder}"
|
||||
}, {
|
||||
"type": "node-terminal",
|
||||
"name": "Start",
|
||||
"request": "launch",
|
||||
"command": "npm run start",
|
||||
"cwd": "${workspaceFolder}"
|
||||
}
|
||||
]
|
||||
}
|
||||
14
LICENSE
Normal file
14
LICENSE
Normal file
@@ -0,0 +1,14 @@
|
||||
BSD Zero Clause License
|
||||
|
||||
Copyright (c) 2025 Growheads.de
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
||||
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||
PERFORMANCE OF THIS SOFTWARE.
|
||||
329
package-lock.json
generated
329
package-lock.json
generated
@@ -7,7 +7,7 @@
|
||||
"": {
|
||||
"name": "reactshop",
|
||||
"version": "1.0.0",
|
||||
"license": "ISC",
|
||||
"license": "0BSD",
|
||||
"dependencies": {
|
||||
"@emotion/react": "^11.14.0",
|
||||
"@emotion/styled": "^11.14.0",
|
||||
@@ -106,9 +106,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/compat-data": {
|
||||
"version": "7.27.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.27.5.tgz",
|
||||
"integrity": "sha512-KiRAp/VoJaWkkte84TvUd9qjdbZAdiqyvMxrGl1N6vzFogKmaLgoM3L1kgtLicp2HP5fBJS8JrZKLVIZGVJAVg==",
|
||||
"version": "7.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.0.tgz",
|
||||
"integrity": "sha512-60X7qkglvrap8mn1lh2ebxXdZYtUcpd7gsmy9kLaBJ4i/WdY8PqTSdxyA8qraikqKQK5C1KRBKXqznrVapyNaw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -116,22 +116,22 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/core": {
|
||||
"version": "7.27.4",
|
||||
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.27.4.tgz",
|
||||
"integrity": "sha512-bXYxrXFubeYdvB0NhD/NBB3Qi6aZeV20GOWVI47t2dkecCEoneR4NPVcb7abpXDEvejgrUfFtG6vG/zxAKmg+g==",
|
||||
"version": "7.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.0.tgz",
|
||||
"integrity": "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@ampproject/remapping": "^2.2.0",
|
||||
"@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-module-transforms": "^7.27.3",
|
||||
"@babel/helpers": "^7.27.4",
|
||||
"@babel/parser": "^7.27.4",
|
||||
"@babel/helpers": "^7.27.6",
|
||||
"@babel/parser": "^7.28.0",
|
||||
"@babel/template": "^7.27.2",
|
||||
"@babel/traverse": "^7.27.4",
|
||||
"@babel/types": "^7.27.3",
|
||||
"@babel/traverse": "^7.28.0",
|
||||
"@babel/types": "^7.28.0",
|
||||
"convert-source-map": "^2.0.0",
|
||||
"debug": "^4.1.0",
|
||||
"gensync": "^1.0.0-beta.2",
|
||||
@@ -147,9 +147,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/eslint-parser": {
|
||||
"version": "7.27.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/eslint-parser/-/eslint-parser-7.27.5.tgz",
|
||||
"integrity": "sha512-HLkYQfRICudzcOtjGwkPvGc5nF1b4ljLZh1IRDj50lRZ718NAKVgQpIAUX8bfg6u/yuSKY3L7E0YzIV+OxrB8Q==",
|
||||
"version": "7.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/eslint-parser/-/eslint-parser-7.28.0.tgz",
|
||||
"integrity": "sha512-N4ntErOlKvcbTt01rr5wj3y55xnIdx1ymrfIr8C2WnM1Y9glFgWaGDEULJIazOX3XM9NRzhfJ6zZnQ1sBNWU+w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -166,15 +166,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/generator": {
|
||||
"version": "7.27.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.27.5.tgz",
|
||||
"integrity": "sha512-ZGhA37l0e/g2s1Cnzdix0O3aLYm66eF8aufiVteOgnwxgnRP8GoyMj7VWsgWnQbVKXyge7hqrFh2K2TQM6t1Hw==",
|
||||
"version": "7.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.0.tgz",
|
||||
"integrity": "sha512-lJjzvrbEeWrhB4P3QBsH7tey117PjLZnDbLiQEKjQ/fNJTjuq4HSqgFA+UNSwZT8D7dxxbnuSBMsa1lrWzKlQg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/parser": "^7.27.5",
|
||||
"@babel/types": "^7.27.3",
|
||||
"@jridgewell/gen-mapping": "^0.3.5",
|
||||
"@jridgewell/trace-mapping": "^0.3.25",
|
||||
"@babel/parser": "^7.28.0",
|
||||
"@babel/types": "^7.28.0",
|
||||
"@jridgewell/gen-mapping": "^0.3.12",
|
||||
"@jridgewell/trace-mapping": "^0.3.28",
|
||||
"jsesc": "^3.0.2"
|
||||
},
|
||||
"engines": {
|
||||
@@ -252,22 +252,31 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-define-polyfill-provider": {
|
||||
"version": "0.6.4",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.4.tgz",
|
||||
"integrity": "sha512-jljfR1rGnXXNWnmQg2K3+bvhkxB51Rl32QRaOTuwwjviGrHzIbSc8+x9CpraDtbT7mfyjXObULP4w/adunNwAw==",
|
||||
"version": "0.6.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.5.tgz",
|
||||
"integrity": "sha512-uJnGFcPsWQK8fvjgGP5LZUZZsYGIoPeRjSF5PGwrelYgq7Q15/Ft9NGFp1zglwgIv//W0uG4BevRuSJRyylZPg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-compilation-targets": "^7.22.6",
|
||||
"@babel/helper-plugin-utils": "^7.22.5",
|
||||
"debug": "^4.1.1",
|
||||
"@babel/helper-compilation-targets": "^7.27.2",
|
||||
"@babel/helper-plugin-utils": "^7.27.1",
|
||||
"debug": "^4.4.1",
|
||||
"lodash.debounce": "^4.0.8",
|
||||
"resolve": "^1.14.2"
|
||||
"resolve": "^1.22.10"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@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": {
|
||||
"version": "7.27.1",
|
||||
"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": {
|
||||
"version": "7.27.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.5.tgz",
|
||||
"integrity": "sha512-OsQd175SxWkGlzbny8J3K8TnnDD0N3lrIUtB92xwyRpzaenGZhxDvxN/JgU00U3CDZNj9tPuDJ5H0WS4Nt3vKg==",
|
||||
"version": "7.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.0.tgz",
|
||||
"integrity": "sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/types": "^7.27.3"
|
||||
"@babel/types": "^7.28.0"
|
||||
},
|
||||
"bin": {
|
||||
"parser": "bin/babel-parser.js"
|
||||
@@ -637,15 +646,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/plugin-transform-async-generator-functions": {
|
||||
"version": "7.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.27.1.tgz",
|
||||
"integrity": "sha512-eST9RrwlpaoJBDHShc+DS2SG4ATTi2MYNb4OxYkf3n+7eb49LWpnS+HSpVfW4x927qQwgk8A2hGNVaajAEw0EA==",
|
||||
"version": "7.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.28.0.tgz",
|
||||
"integrity": "sha512-BEOdvX4+M765icNPZeidyADIvQ1m1gmunXufXxvRESy/jNNyfovIqUyE7MVgGBjWktCoJlzvFA1To2O4ymIO3Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-plugin-utils": "^7.27.1",
|
||||
"@babel/helper-remap-async-to-generator": "^7.27.1",
|
||||
"@babel/traverse": "^7.27.1"
|
||||
"@babel/traverse": "^7.28.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
@@ -689,9 +698,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/plugin-transform-block-scoping": {
|
||||
"version": "7.27.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.27.5.tgz",
|
||||
"integrity": "sha512-JF6uE2s67f0y2RZcm2kpAUEbD50vH62TyWVebxwHAlbSdM49VqPz8t4a1uIjp4NIOIZ4xzLfjY5emt/RCyC7TQ==",
|
||||
"version": "7.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.28.0.tgz",
|
||||
"integrity": "sha512-gKKnwjpdx5sER/wl0WN0efUBFzF/56YZO0RJrSYP4CljXnP31ByY7fol89AzomdlLNzI36AvOTmYHsnZTCkq8Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -739,18 +748,18 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/plugin-transform-classes": {
|
||||
"version": "7.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.27.1.tgz",
|
||||
"integrity": "sha512-7iLhfFAubmpeJe/Wo2TVuDrykh/zlWXLzPNdL0Jqn/Xu8R3QQ8h9ff8FQoISZOsw74/HFqFI7NX63HN7QFIHKA==",
|
||||
"version": "7.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.28.0.tgz",
|
||||
"integrity": "sha512-IjM1IoJNw72AZFlj33Cu8X0q2XK/6AaVC3jQu+cgQ5lThWD5ajnuUAml80dqRmOhmPkTH8uAwnpMu9Rvj0LTRA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-annotate-as-pure": "^7.27.1",
|
||||
"@babel/helper-compilation-targets": "^7.27.1",
|
||||
"@babel/helper-annotate-as-pure": "^7.27.3",
|
||||
"@babel/helper-compilation-targets": "^7.27.2",
|
||||
"@babel/helper-globals": "^7.28.0",
|
||||
"@babel/helper-plugin-utils": "^7.27.1",
|
||||
"@babel/helper-replace-supers": "^7.27.1",
|
||||
"@babel/traverse": "^7.27.1",
|
||||
"globals": "^11.1.0"
|
||||
"@babel/traverse": "^7.28.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
@@ -759,16 +768,6 @@
|
||||
"@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": {
|
||||
"version": "7.27.1",
|
||||
"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": {
|
||||
"version": "7.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.27.3.tgz",
|
||||
"integrity": "sha512-s4Jrok82JpiaIprtY2nHsYmrThKvvwgHwjgd7UMiYhZaN0asdXNLr0y+NjTfkA7SyQE5i2Fb7eawUOZmLvyqOA==",
|
||||
"version": "7.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.28.0.tgz",
|
||||
"integrity": "sha512-v1nrSMBiKcodhsyJ4Gf+Z0U/yawmJDBOTpEB3mcQY52r9RIyPneGyAS/yM6seP/8I+mWI3elOMtT5dB8GJVs+A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-plugin-utils": "^7.27.1"
|
||||
"@babel/helper-plugin-utils": "^7.27.1",
|
||||
"@babel/traverse": "^7.28.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
@@ -868,6 +868,23 @@
|
||||
"@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": {
|
||||
"version": "7.27.1",
|
||||
"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": {
|
||||
"version": "7.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.27.3.tgz",
|
||||
"integrity": "sha512-7ZZtznF9g4l2JCImCo5LNKFHB5eXnN39lLtLY5Tg+VkR0jwOt7TBciMckuiQIOIW7L5tkQOCh3bVGYeXgMx52Q==",
|
||||
"version": "7.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.28.0.tgz",
|
||||
"integrity": "sha512-9VNGikXxzu5eCiQjdE4IZn8sb9q7Xsk5EXLDBKUYg1e/Tve8/05+KJEtcxGxAgCY5t/BpKQM+JEL/yT4tvgiUA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-compilation-targets": "^7.27.2",
|
||||
"@babel/helper-plugin-utils": "^7.27.1",
|
||||
"@babel/plugin-transform-destructuring": "^7.27.3",
|
||||
"@babel/plugin-transform-parameters": "^7.27.1"
|
||||
"@babel/plugin-transform-destructuring": "^7.28.0",
|
||||
"@babel/plugin-transform-parameters": "^7.27.7",
|
||||
"@babel/traverse": "^7.28.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
@@ -1204,9 +1222,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/plugin-transform-parameters": {
|
||||
"version": "7.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.27.1.tgz",
|
||||
"integrity": "sha512-018KRk76HWKeZ5l4oTj2zPpSh+NbGdt0st5S6x0pga6HgrjBOJb24mMDHorFopOOd6YHkLgOZ+zaCjZGPO4aKg==",
|
||||
"version": "7.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.27.7.tgz",
|
||||
"integrity": "sha512-qBkYTYCb76RRxUM6CcZA5KRu8K4SM8ajzVeUgVdMVO9NN9uI/GaVmBg/WKJJGnNokV9SY8FxNOVWGXzqzUidBg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -1340,9 +1358,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/plugin-transform-regenerator": {
|
||||
"version": "7.27.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.27.5.tgz",
|
||||
"integrity": "sha512-uhB8yHerfe3MWnuLAhEbeQ4afVoqv8BQsPqrTv7e/jZ9y00kJL6l9a/f4OWaKxotmjzewfEyXE1vgDJenkQ2/Q==",
|
||||
"version": "7.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.28.0.tgz",
|
||||
"integrity": "sha512-LOAozRVbqxEVjSKfhGnuLoE4Kz4Oc5UJzuvFUhSsQzdCdaAQu06mG8zDv2GFSerM62nImUZ7K92vxnQcLSDlCQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -1537,13 +1555,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/preset-env": {
|
||||
"version": "7.27.2",
|
||||
"resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.27.2.tgz",
|
||||
"integrity": "sha512-Ma4zSuYSlGNRlCLO+EAzLnCmJK2vdstgv+n7aUP+/IKZrOfWHOJVdSJtuub8RzHTj3ahD37k5OKJWvzf16TQyQ==",
|
||||
"version": "7.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.28.0.tgz",
|
||||
"integrity": "sha512-VmaxeGOwuDqzLl5JUkIRM1X2Qu2uKGxHEQWh+cvvbl7JuJRgKGJSfsEF/bUaxFhJl/XAyxBe7q7qSuTbKFuCyg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/compat-data": "^7.27.2",
|
||||
"@babel/compat-data": "^7.28.0",
|
||||
"@babel/helper-compilation-targets": "^7.27.2",
|
||||
"@babel/helper-plugin-utils": "^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-unicode-sets-regex": "^7.18.6",
|
||||
"@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-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-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-destructuring": "^7.27.1",
|
||||
"@babel/plugin-transform-destructuring": "^7.28.0",
|
||||
"@babel/plugin-transform-dotall-regex": "^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-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-export-namespace-from": "^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-nullish-coalescing-operator": "^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-optional-catch-binding": "^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-property-in-object": "^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-reserved-words": "^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-sets-regex": "^7.27.1",
|
||||
"@babel/preset-modules": "0.1.6-no-external-plugins",
|
||||
"babel-plugin-polyfill-corejs2": "^0.4.10",
|
||||
"babel-plugin-polyfill-corejs3": "^0.11.0",
|
||||
"babel-plugin-polyfill-regenerator": "^0.6.1",
|
||||
"core-js-compat": "^3.40.0",
|
||||
"babel-plugin-polyfill-corejs2": "^0.4.14",
|
||||
"babel-plugin-polyfill-corejs3": "^0.13.0",
|
||||
"babel-plugin-polyfill-regenerator": "^0.6.5",
|
||||
"core-js-compat": "^3.43.0",
|
||||
"semver": "^6.3.1"
|
||||
},
|
||||
"engines": {
|
||||
@@ -1700,36 +1719,27 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/traverse": {
|
||||
"version": "7.27.4",
|
||||
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.27.4.tgz",
|
||||
"integrity": "sha512-oNcu2QbHqts9BtOWJosOVJapWjBDSxGCpFvikNR5TGDYDQf3JwpIoMzIKrvfoti93cLfPJEG4tH9SPVeyCGgdA==",
|
||||
"version": "7.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.0.tgz",
|
||||
"integrity": "sha512-mGe7UK5wWyh0bKRfupsUchrQGqvDbZDbKJw+kcRGSmdHVYrv+ltd0pnpDTVpiTqnaBru9iEvA8pz8W46v0Amwg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.27.1",
|
||||
"@babel/generator": "^7.27.3",
|
||||
"@babel/parser": "^7.27.4",
|
||||
"@babel/generator": "^7.28.0",
|
||||
"@babel/helper-globals": "^7.28.0",
|
||||
"@babel/parser": "^7.28.0",
|
||||
"@babel/template": "^7.27.2",
|
||||
"@babel/types": "^7.27.3",
|
||||
"debug": "^4.3.1",
|
||||
"globals": "^11.1.0"
|
||||
"@babel/types": "^7.28.0",
|
||||
"debug": "^4.3.1"
|
||||
},
|
||||
"engines": {
|
||||
"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": {
|
||||
"version": "7.27.6",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.6.tgz",
|
||||
"integrity": "sha512-ETyHEk2VHHvl9b9jZP5IHPavHYk57EhanlRRuae9XCpb/j5bDCbPPMOBfCWhnl/7EDJz0jEMCi/RhccCE8r1+Q==",
|
||||
"version": "7.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.0.tgz",
|
||||
"integrity": "sha512-jYnje+JyZG5YThjHiF28oT4SIZLnYOcSBb6+SDaFIyzDVSkXQmQQYclJ2R+YxcdmK0AX6x1E5OQNtuh3jHDrUg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-string-parser": "^7.27.1",
|
||||
@@ -2708,17 +2718,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/gen-mapping": {
|
||||
"version": "0.3.8",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz",
|
||||
"integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==",
|
||||
"version": "0.3.12",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.12.tgz",
|
||||
"integrity": "sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/set-array": "^1.2.1",
|
||||
"@jridgewell/sourcemap-codec": "^1.4.10",
|
||||
"@jridgewell/sourcemap-codec": "^1.5.0",
|
||||
"@jridgewell/trace-mapping": "^0.3.24"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/resolve-uri": {
|
||||
@@ -2730,15 +2736,6 @@
|
||||
"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": {
|
||||
"version": "0.3.6",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.6.tgz",
|
||||
@@ -2757,9 +2754,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@jridgewell/trace-mapping": {
|
||||
"version": "0.3.25",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz",
|
||||
"integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==",
|
||||
"version": "0.3.29",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.29.tgz",
|
||||
"integrity": "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/resolve-uri": "^3.1.0",
|
||||
@@ -3118,9 +3115,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@pmmmwh/react-refresh-webpack-plugin": {
|
||||
"version": "0.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@pmmmwh/react-refresh-webpack-plugin/-/react-refresh-webpack-plugin-0.6.0.tgz",
|
||||
"integrity": "sha512-AAc+QWfZ1KQ/e1C6OHWVlxU+ks6zFGOA44IJUlvju7RlDS8nsX6poPFOIlsg/rTofO9vKov12+WCjMhKkRKD5g==",
|
||||
"version": "0.6.1",
|
||||
"resolved": "https://registry.npmjs.org/@pmmmwh/react-refresh-webpack-plugin/-/react-refresh-webpack-plugin-0.6.1.tgz",
|
||||
"integrity": "sha512-95DXXJxNkpYu+sqmpDp7vbw9JCyiNpHuCsvuMuOgVFrKQlwEIn9Y1+NNIQJq+zFL+eWyxw6htthB5CtdwJupNA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -4188,14 +4185,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/babel-plugin-polyfill-corejs2": {
|
||||
"version": "0.4.13",
|
||||
"resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.13.tgz",
|
||||
"integrity": "sha512-3sX/eOms8kd3q2KZ6DAhKPc0dgm525Gqq5NtWKZ7QYYZEv57OQ54KtblzJzH1lQF/eQxO8KjWGIK9IPUJNus5g==",
|
||||
"version": "0.4.14",
|
||||
"resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.14.tgz",
|
||||
"integrity": "sha512-Co2Y9wX854ts6U8gAAPXfn0GmAyctHuK8n0Yhfjd6t30g7yvKjspvvOo9yG+z52PZRgFErt7Ka2pYnXCjLKEpg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/compat-data": "^7.22.6",
|
||||
"@babel/helper-define-polyfill-provider": "^0.6.4",
|
||||
"@babel/compat-data": "^7.27.7",
|
||||
"@babel/helper-define-polyfill-provider": "^0.6.5",
|
||||
"semver": "^6.3.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
@@ -4203,27 +4200,27 @@
|
||||
}
|
||||
},
|
||||
"node_modules/babel-plugin-polyfill-corejs3": {
|
||||
"version": "0.11.1",
|
||||
"resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.11.1.tgz",
|
||||
"integrity": "sha512-yGCqvBT4rwMczo28xkH/noxJ6MZ4nJfkVYdoDaC/utLtWrXxv27HVrzAeSbqR8SxDsp46n0YF47EbHoixy6rXQ==",
|
||||
"version": "0.13.0",
|
||||
"resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.13.0.tgz",
|
||||
"integrity": "sha512-U+GNwMdSFgzVmfhNm8GJUX88AadB3uo9KpJqS3FaqNIPKgySuvMb+bHPsOmmuWyIcuqZj/pzt1RUIUZns4y2+A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-define-polyfill-provider": "^0.6.3",
|
||||
"core-js-compat": "^3.40.0"
|
||||
"@babel/helper-define-polyfill-provider": "^0.6.5",
|
||||
"core-js-compat": "^3.43.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/babel-plugin-polyfill-regenerator": {
|
||||
"version": "0.6.4",
|
||||
"resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.4.tgz",
|
||||
"integrity": "sha512-7gD3pRadPrbjhjLyxebmx/WrFYcuSjZ0XbdUujQMZ/fcE9oeewk2U/7PCvez84UeuK3oSjmPZ0Ch0dlupQvGzw==",
|
||||
"version": "0.6.5",
|
||||
"resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.5.tgz",
|
||||
"integrity": "sha512-ISqQ2frbiNU9vIJkzg7dlPpznPZ4jOiUQ1uSmB0fEHeowtN3COYRsXr/xexn64NpU13P06jc/L5TgiJXOgrbEg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-define-polyfill-provider": "^0.6.4"
|
||||
"@babel/helper-define-polyfill-provider": "^0.6.5"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0"
|
||||
@@ -4317,9 +4314,9 @@
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/brace-expansion": {
|
||||
"version": "1.1.11",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
||||
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
|
||||
"version": "1.1.12",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
|
||||
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -4341,9 +4338,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/browserslist": {
|
||||
"version": "4.24.4",
|
||||
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.4.tgz",
|
||||
"integrity": "sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==",
|
||||
"version": "4.25.1",
|
||||
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.1.tgz",
|
||||
"integrity": "sha512-KGj0KoOMXLpSNkkEI6Z6mShmQy0bc1I+T7K9N81k4WWMrfz+6fQ6es80B/YLAeRoKvjYE1YSHHOW1qe9xIVzHw==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
@@ -4361,10 +4358,10 @@
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"caniuse-lite": "^1.0.30001688",
|
||||
"electron-to-chromium": "^1.5.73",
|
||||
"caniuse-lite": "^1.0.30001726",
|
||||
"electron-to-chromium": "^1.5.173",
|
||||
"node-releases": "^2.0.19",
|
||||
"update-browserslist-db": "^1.1.1"
|
||||
"update-browserslist-db": "^1.1.3"
|
||||
},
|
||||
"bin": {
|
||||
"browserslist": "cli.js"
|
||||
@@ -4477,9 +4474,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/caniuse-lite": {
|
||||
"version": "1.0.30001715",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001715.tgz",
|
||||
"integrity": "sha512-7ptkFGMm2OAOgvZpwgA4yjQ5SQbrNVGdRjzH0pBdy1Fasvcr+KAeECmbCAECzTuDuoX0FCY8KzUxjf9+9kfZEw==",
|
||||
"version": "1.0.30001726",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001726.tgz",
|
||||
"integrity": "sha512-VQAUIUzBiZ/UnlM28fSp2CRF3ivUn1BWEvxMcVTNwpw91Py1pGbPIyIKtd+tzct9C3ouceCVdGAXxZOpZAsgdw==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
@@ -4815,13 +4812,13 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/core-js-compat": {
|
||||
"version": "3.41.0",
|
||||
"resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.41.0.tgz",
|
||||
"integrity": "sha512-RFsU9LySVue9RTwdDVX/T0e2Y6jRYWXERKElIjpuEOEnxaXffI0X7RUwVzfYLfzuLXSNJDYoRYUAmRUcyln20A==",
|
||||
"version": "3.43.0",
|
||||
"resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.43.0.tgz",
|
||||
"integrity": "sha512-2GML2ZsCc5LR7hZYz4AXmjQw8zuy2T//2QntwdnpuYI7jteT6GVYJL7F6C2C57R7gSYrcqVW3lAALefdbhBLDA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"browserslist": "^4.24.4"
|
||||
"browserslist": "^4.25.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
@@ -5141,9 +5138,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "4.4.0",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
|
||||
"integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==",
|
||||
"version": "4.4.1",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
|
||||
"integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "^2.1.3"
|
||||
@@ -5478,9 +5475,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/electron-to-chromium": {
|
||||
"version": "1.5.139",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.139.tgz",
|
||||
"integrity": "sha512-GGnRYOTdN5LYpwbIr0rwP/ZHOQSvAF6TG0LSzp28uCBb9JiXHJGmaaKw29qjNJc5bGnnp6kXJqRnGMQoELwi5w==",
|
||||
"version": "1.5.179",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.179.tgz",
|
||||
"integrity": "sha512-UWKi/EbBopgfFsc5k61wFpV7WrnnSlSzW/e2XcBmS6qKYTivZlLtoll5/rdqRTxGglGHkmkW0j0pFNJG10EUIQ==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
"type": "module",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"start": "webpack serve --progress --mode development --no-open",
|
||||
"start:seedheads": "cross-env PROXY_TARGET=https://seedheads.de 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 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",
|
||||
"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",
|
||||
@@ -17,7 +17,7 @@
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"license": "0BSD",
|
||||
"description": "",
|
||||
"dependencies": {
|
||||
"@emotion/react": "^11.14.0",
|
||||
|
||||
@@ -50,6 +50,7 @@ const {
|
||||
generateProductsXml,
|
||||
generateLlmsTxt,
|
||||
generateCategoryLlmsTxt,
|
||||
generateAllCategoryLlmsPages,
|
||||
} = require("./prerender/seo.cjs");
|
||||
const {
|
||||
fetchCategoryProducts,
|
||||
@@ -71,10 +72,12 @@ const Batteriegesetzhinweise =
|
||||
require("./src/pages/Batteriegesetzhinweise.js").default;
|
||||
const Widerrufsrecht = require("./src/pages/Widerrufsrecht.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 NotFound404 = require("./src/pages/NotFound404.js").default;
|
||||
|
||||
// Worker function for parallel product rendering
|
||||
const renderProductWorker = async (productSeoNames, workerId, progressCallback) => {
|
||||
const renderProductWorker = async (productSeoNames, workerId, progressCallback, categoryMap = {}) => {
|
||||
const socketUrl = "http://127.0.0.1:9303";
|
||||
const workerSocket = io(socketUrl, {
|
||||
path: "/socket.io/",
|
||||
@@ -113,10 +116,13 @@ const renderProductWorker = async (productSeoNames, workerId, progressCallback)
|
||||
...productDetails.product,
|
||||
seoName: actualSeoName,
|
||||
}, shopConfig.baseUrl, shopConfig);
|
||||
// Get category info from categoryMap if available
|
||||
const categoryInfo = productDetails.product.categoryId ? categoryMap[productDetails.product.categoryId] : null;
|
||||
|
||||
const jsonLdScript = generateProductJsonLd({
|
||||
...productDetails.product,
|
||||
seoName: actualSeoName,
|
||||
}, shopConfig.baseUrl, shopConfig);
|
||||
}, shopConfig.baseUrl, shopConfig, categoryInfo);
|
||||
const combinedMetaTags = metaTags + "\n" + jsonLdScript;
|
||||
|
||||
const success = renderPage(
|
||||
@@ -182,7 +188,7 @@ const renderProductWorker = async (productSeoNames, workerId, progressCallback)
|
||||
};
|
||||
|
||||
// Function to render products in parallel
|
||||
const renderProductsInParallel = async (allProductsArray, maxWorkers, totalProducts) => {
|
||||
const renderProductsInParallel = async (allProductsArray, maxWorkers, totalProducts, categoryMap = {}) => {
|
||||
// Shared progress tracking
|
||||
let completedProducts = 0;
|
||||
let totalSuccessCount = 0;
|
||||
@@ -244,7 +250,7 @@ const renderProductsInParallel = async (allProductsArray, maxWorkers, totalProdu
|
||||
|
||||
// Update progress bar with worker stats
|
||||
updateProgressBar(completedProducts, totalProducts, lastProductName);
|
||||
});
|
||||
}, categoryMap);
|
||||
|
||||
workerPromises.push(promise);
|
||||
}
|
||||
@@ -312,7 +318,8 @@ const renderApp = async (categoryData, socket) => {
|
||||
? "index.html"
|
||||
: "index.prerender.html";
|
||||
const homeMetaTags = generateHomepageMetaTags(shopConfig.baseUrl, shopConfig);
|
||||
const homeJsonLd = generateHomepageJsonLd(shopConfig.baseUrl, shopConfig);
|
||||
const homepageCategories = categoryData ? collectAllCategories(categoryData) : [];
|
||||
const homeJsonLd = generateHomepageJsonLd(shopConfig.baseUrl, shopConfig, homepageCategories);
|
||||
const combinedHomeMeta = homeMetaTags + "\n" + homeJsonLd;
|
||||
const homeSuccess = render(
|
||||
homeComponent,
|
||||
@@ -356,12 +363,14 @@ const renderApp = async (categoryData, socket) => {
|
||||
description: "Widerrufsrecht page",
|
||||
},
|
||||
{
|
||||
component: Sitemap,
|
||||
component: PrerenderSitemap,
|
||||
path: "/sitemap",
|
||||
filename: "sitemap",
|
||||
description: "Sitemap page",
|
||||
needsCategoryData: true,
|
||||
},
|
||||
{ component: AGB, path: "/agb", filename: "agb", description: "AGB page" },
|
||||
{ component: NotFound404, path: "/404", filename: "404", description: "404 Not Found page" },
|
||||
{
|
||||
component: PrerenderKonfigurator,
|
||||
path: "/Konfigurator",
|
||||
@@ -378,13 +387,15 @@ const renderApp = async (categoryData, socket) => {
|
||||
|
||||
let staticPagesRendered = 0;
|
||||
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 = "";
|
||||
|
||||
// Special handling for Sitemap page to include category data
|
||||
if (page.filename === "sitemap" && categoryData) {
|
||||
const allCategories = collectAllCategories(categoryData);
|
||||
metaTags = generateSitemapJsonLd(allCategories, shopConfig.baseUrl, shopConfig);
|
||||
const sitemapCategories = collectAllCategories(categoryData);
|
||||
metaTags = generateSitemapJsonLd(sitemapCategories, shopConfig.baseUrl, shopConfig);
|
||||
}
|
||||
|
||||
// Special handling for Konfigurator page to include SEO tags
|
||||
@@ -531,6 +542,15 @@ const renderApp = async (categoryData, socket) => {
|
||||
const numCPUs = os.cpus().length;
|
||||
const maxWorkers = Math.min(numCPUs, totalProducts, 8); // Cap at 8 workers to avoid overwhelming the server
|
||||
|
||||
// Create category map for breadcrumbs
|
||||
const categoryMap = {};
|
||||
allCategories.forEach(category => {
|
||||
categoryMap[category.id] = {
|
||||
name: category.name,
|
||||
seoName: category.seoName
|
||||
};
|
||||
});
|
||||
|
||||
console.log(
|
||||
`\n📦 Rendering ${totalProducts} individual product pages using ${maxWorkers} parallel workers...`
|
||||
);
|
||||
@@ -538,7 +558,8 @@ const renderApp = async (categoryData, socket) => {
|
||||
const productPagesRendered = await renderProductsInParallel(
|
||||
Array.from(allProducts),
|
||||
maxWorkers,
|
||||
totalProducts
|
||||
totalProducts,
|
||||
categoryMap
|
||||
);
|
||||
|
||||
console.log(
|
||||
@@ -641,27 +662,39 @@ const renderApp = async (categoryData, socket) => {
|
||||
productsByCategory[categoryId].push(product);
|
||||
});
|
||||
|
||||
// Generate category-specific LLM files
|
||||
// Generate category-specific LLM files with pagination
|
||||
let categoryFilesGenerated = 0;
|
||||
let totalCategoryProducts = 0;
|
||||
let totalPaginatedFiles = 0;
|
||||
|
||||
for (const category of allCategories) {
|
||||
if (category.seoName) {
|
||||
const categoryProducts = productsByCategory[category.id] || [];
|
||||
const categorySlug = category.seoName.toLowerCase().replace(/[^a-z0-9]/g, '-');
|
||||
|
||||
const categoryLlmsTxt = generateCategoryLlmsTxt(category, categoryProducts, shopConfig.baseUrl, shopConfig);
|
||||
const categoryLlmsTxtPath = path.resolve(__dirname, config.outputDir, `llms-${categorySlug}.txt`);
|
||||
// Generate all paginated files for this category
|
||||
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++;
|
||||
totalCategoryProducts += categoryProducts.length;
|
||||
}
|
||||
}
|
||||
|
||||
console.log(` 📄 Total paginated files generated: ${totalPaginatedFiles}`);
|
||||
console.log(` 📦 Total products across all categories: ${totalCategoryProducts}`);
|
||||
|
||||
try {
|
||||
const verification = fs.readFileSync(llmsTxtPath, 'utf8');
|
||||
console.log(` - File verification: ✅ All files valid UTF-8`);
|
||||
|
||||
@@ -39,7 +39,7 @@ const fetchCategoryProducts = (socket, categoryId) => {
|
||||
|
||||
socket.emit(
|
||||
"getCategoryProducts",
|
||||
{ categoryId: parseInt(categoryId) },
|
||||
{ full:true, categoryId: parseInt(categoryId) },
|
||||
(response) => {
|
||||
clearTimeout(timeout);
|
||||
if (response && response.products !== undefined) {
|
||||
|
||||
@@ -1,881 +1,4 @@
|
||||
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`;
|
||||
// Re-export all SEO functions from the new modular structure
|
||||
// This maintains backward compatibility while using the new split files
|
||||
|
||||
// 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) => {
|
||||
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,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
return `<script type="application/ld+json">${JSON.stringify(
|
||||
jsonLd
|
||||
)}</script>`;
|
||||
};
|
||||
|
||||
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`,
|
||||
offers: {
|
||||
"@type": "Offer",
|
||||
price: product.price.toString(),
|
||||
priceCurrency: config.currency,
|
||||
availability: product.available
|
||||
? "https://schema.org/InStock"
|
||||
: "https://schema.org/OutOfStock",
|
||||
},
|
||||
},
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
return `<script type="application/ld+json">${JSON.stringify(
|
||||
jsonLd
|
||||
)}</script>`;
|
||||
};
|
||||
|
||||
const generateHomepageMetaTags = (baseUrl, config) => {
|
||||
const description = config.descriptions.long;
|
||||
const keywords = config.keywords;
|
||||
const imageUrl = `${baseUrl}${config.images.logo}`;
|
||||
|
||||
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="${baseUrl}">
|
||||
<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="${baseUrl}">
|
||||
`;
|
||||
};
|
||||
|
||||
const generateHomepageJsonLd = (baseUrl, config) => {
|
||||
|
||||
const jsonLd = {
|
||||
"@context": "https://schema.org/",
|
||||
"@type": "WebSite",
|
||||
name: config.brandName,
|
||||
url: baseUrl,
|
||||
description: config.descriptions.long,
|
||||
publisher: {
|
||||
"@type": "Organization",
|
||||
name: config.brandName,
|
||||
url: baseUrl,
|
||||
logo: {
|
||||
"@type": "ImageObject",
|
||||
url: `${baseUrl}${config.images.logo}`,
|
||||
},
|
||||
},
|
||||
potentialAction: {
|
||||
"@type": "SearchAction",
|
||||
target: {
|
||||
"@type": "EntryPoint",
|
||||
urlTemplate: `${baseUrl}/search?q={search_term_string}`,
|
||||
},
|
||||
"query-input": "required name=search_term_string",
|
||||
},
|
||||
mainEntity: {
|
||||
"@type": "WebPage",
|
||||
name: "Sitemap",
|
||||
url: `${baseUrl}/sitemap`,
|
||||
description: "Vollständige Sitemap mit allen Kategorien und Seiten",
|
||||
},
|
||||
sameAs: [
|
||||
// Add your social media URLs here if available
|
||||
],
|
||||
};
|
||||
|
||||
return `<script type="application/ld+json">${JSON.stringify(
|
||||
jsonLd
|
||||
)}</script>`;
|
||||
};
|
||||
|
||||
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: "/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;
|
||||
};
|
||||
|
||||
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";
|
||||
const imageUrl = `${baseUrl}${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="${baseUrl}/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="${baseUrl}/Konfigurator">
|
||||
`;
|
||||
};
|
||||
|
||||
const generateRobotsTxt = (baseUrl) => {
|
||||
|
||||
const robotsTxt = `User-agent: *
|
||||
Allow: /
|
||||
Sitemap: ${baseUrl}/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, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """)
|
||||
.replace(/'/g, "'");
|
||||
};
|
||||
|
||||
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}`;
|
||||
|
||||
// Generate GTIN/EAN if available (using articleNumber as fallback)
|
||||
const rawGtin = product.gtin || "";
|
||||
const gtin = escapeXml(rawGtin.toString().trim());
|
||||
|
||||
// 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;
|
||||
};
|
||||
|
||||
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}
|
||||
SeedHeads is a German online shop specializing in high-quality seeds, plants, and gardening supplies. We offer a comprehensive range of products for indoor and outdoor growing, including seeds, cuttings, grow equipment, lighting, ventilation, fertilizers, and accessories.
|
||||
|
||||
## 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, '-');
|
||||
|
||||
llmsTxt += `#### ${category.name} (${productCount} products)
|
||||
- **Product Catalog**: ${baseUrl}/llms-${categorySlug}.txt
|
||||
|
||||
`;
|
||||
}
|
||||
});
|
||||
|
||||
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) => {
|
||||
const currentDate = new Date().toISOString().split("T")[0]; // YYYY-MM-DD format
|
||||
|
||||
let categoryLlmsTxt = `# ${category.name} - Product Catalog
|
||||
|
||||
Generated: ${currentDate}
|
||||
Base URL: ${baseUrl}
|
||||
Category: ${category.name} (ID: ${category.id})
|
||||
Category URL: ${baseUrl}/Kategorie/${category.seoName}
|
||||
|
||||
## Category Overview
|
||||
This file contains all products in the "${category.name}" category from ${config.siteName}.
|
||||
|
||||
**Statistics:**
|
||||
- **Total Products**: ${categoryProducts.length}
|
||||
- **Category ID**: ${category.id}
|
||||
- **Category URL**: ${baseUrl}/Kategorie/${category.seoName}
|
||||
- **Back to Main Sitemap**: ${baseUrl}/llms.txt
|
||||
|
||||
`;
|
||||
|
||||
if (categoryProducts.length > 0) {
|
||||
categoryProducts.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)
|
||||
: "";
|
||||
|
||||
categoryLlmsTxt += `## ${index + 1}. ${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.
|
||||
|
||||
`;
|
||||
}
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
generateProductMetaTags,
|
||||
generateProductJsonLd,
|
||||
generateCategoryJsonLd,
|
||||
generateHomepageMetaTags,
|
||||
generateHomepageJsonLd,
|
||||
generateSitemapJsonLd,
|
||||
generateKonfiguratorMetaTags,
|
||||
generateXmlSitemap,
|
||||
generateRobotsTxt,
|
||||
generateProductsXml,
|
||||
generateLlmsTxt,
|
||||
generateCategoryLlmsTxt,
|
||||
};
|
||||
module.exports = require('./seo/index.cjs');
|
||||
|
||||
81
prerender/seo/category.cjs
Normal file
81
prerender/seo/category.cjs
Normal 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
344
prerender/seo/feeds.cjs
Normal 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, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """)
|
||||
.replace(/'/g, "'");
|
||||
};
|
||||
|
||||
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
215
prerender/seo/homepage.cjs
Normal 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
64
prerender/seo/index.cjs
Normal 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,
|
||||
};
|
||||
36
prerender/seo/konfigurator.cjs
Normal file
36
prerender/seo/konfigurator.cjs
Normal 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
277
prerender/seo/llms.cjs
Normal 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
135
prerender/seo/product.cjs
Normal 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
117
prerender/seo/sitemap.cjs
Normal 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,
|
||||
};
|
||||
BIN
public/assets/images/404.png
Normal file
BIN
public/assets/images/404.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.9 MiB |
Binary file not shown.
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 48 KiB |
@@ -1,11 +1,11 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="description" content="SeedHeads - Ihr Online-Shop für hochwertige Samen, Pflanzen und Gartenbedarf. Entdecken Sie unser großes Sortiment an Saatgut, Pflanzen und Gartenzubehör für Ihren grünen Daumen.">
|
||||
<meta name="description" content="Growheads - Dein Anlaufpunkt für alles rund um den Grow in Dresden. Substrate, Dünger, Steckling & Seeds, Lampen, Lüfter, Filter, Growboxen">
|
||||
<base href="/">
|
||||
<title>SeedHeads.de</title>
|
||||
<title>GrowHeads.de</title>
|
||||
<link rel="icon" type="image/x-icon" href="/favicon.ico">
|
||||
</head>
|
||||
<body style="overflow-y: scroll;">
|
||||
@@ -36,4 +36,4 @@
|
||||
setTimeout(checkForUpdates, 1000 * 1000);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
|
||||
17
src/App.js
17
src/App.js
@@ -40,6 +40,7 @@ const ServerLogsPage = lazy(() => import(/* webpackChunkName: "admin-logs" */ ".
|
||||
// Lazy load legal pages - rarely accessed
|
||||
const Datenschutz = lazy(() => import(/* webpackChunkName: "legal" */ "./pages/Datenschutz.js"));
|
||||
const AGB = lazy(() => import(/* webpackChunkName: "legal" */ "./pages/AGB.js"));
|
||||
const NotFound404 = lazy(() => import(/* webpackChunkName: "legal" */ "./pages/NotFound404.js"));
|
||||
const Sitemap = lazy(() => import(/* webpackChunkName: "sitemap" */ "./pages/Sitemap.js"));
|
||||
const Impressum = lazy(() => import(/* webpackChunkName: "legal" */ "./pages/Impressum.js"));
|
||||
const Batteriegesetzhinweise = lazy(() => import(/* webpackChunkName: "legal" */ "./pages/Batteriegesetzhinweise.js"));
|
||||
@@ -164,7 +165,8 @@ const AppContent = ({ currentTheme, onThemeChange }) => {
|
||||
// Check if we're in development mode
|
||||
const isDevelopment = process.env.NODE_ENV === "development";
|
||||
|
||||
const socket = useContext(SocketContext);
|
||||
const {socket,socketB} = useContext(SocketContext);
|
||||
console.log("AppContent: socket", socket);
|
||||
|
||||
return (
|
||||
<Box
|
||||
@@ -200,7 +202,7 @@ const AppContent = ({ currentTheme, onThemeChange }) => {
|
||||
{/* Category page - Render Content in parallel */}
|
||||
<Route
|
||||
path="/Kategorie/:categoryId"
|
||||
element={<Content socket={socket} />}
|
||||
element={<Content socket={socket} socketB={socketB} />}
|
||||
/>
|
||||
{/* Single product page */}
|
||||
<Route
|
||||
@@ -209,7 +211,7 @@ const AppContent = ({ currentTheme, onThemeChange }) => {
|
||||
/>
|
||||
|
||||
{/* Search page - Render Content in parallel */}
|
||||
<Route path="/search" element={<Content socket={socket} />} />
|
||||
<Route path="/search" element={<Content socket={socket} socketB={socketB} />} />
|
||||
|
||||
{/* Profile page */}
|
||||
<Route path="/profile" element={<ProfilePageWithSocket />} />
|
||||
@@ -217,21 +219,22 @@ const AppContent = ({ currentTheme, onThemeChange }) => {
|
||||
{/* Reset password page */}
|
||||
<Route
|
||||
path="/resetPassword"
|
||||
element={<ResetPassword socket={socket} />}
|
||||
element={<ResetPassword socket={socket} socketB={socketB} />}
|
||||
/>
|
||||
|
||||
{/* Admin page */}
|
||||
<Route path="/admin" element={<AdminPage socket={socket} />} />
|
||||
<Route path="/admin" element={<AdminPage socket={socket} socketB={socketB} />} />
|
||||
|
||||
{/* Admin Users page */}
|
||||
<Route path="/admin/users" element={<UsersPage socket={socket} />} />
|
||||
<Route path="/admin/users" element={<UsersPage socket={socket} socketB={socketB} />} />
|
||||
|
||||
{/* Admin Server Logs page */}
|
||||
<Route path="/admin/logs" element={<ServerLogsPage socket={socket} />} />
|
||||
<Route path="/admin/logs" element={<ServerLogsPage socket={socket} socketB={socketB} />} />
|
||||
|
||||
{/* Legal pages */}
|
||||
<Route path="/datenschutz" element={<Datenschutz />} />
|
||||
<Route path="/agb" element={<AGB />} />
|
||||
<Route path="/404" element={<NotFound404 />} />
|
||||
<Route path="/sitemap" element={<Sitemap />} />
|
||||
<Route path="/impressum" element={<Impressum />} />
|
||||
<Route
|
||||
|
||||
137
src/PrerenderSitemap.js
Normal file
137
src/PrerenderSitemap.js
Normal 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 };
|
||||
@@ -22,7 +22,7 @@ const CategoryBox = ({
|
||||
const [imageUrl, setImageUrl] = useState(null);
|
||||
const [imageError, setImageError] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const socket = useContext(SocketContext);
|
||||
const context = useContext(SocketContext);
|
||||
|
||||
useEffect(() => {
|
||||
let objectUrl = null;
|
||||
@@ -61,10 +61,10 @@ const CategoryBox = ({
|
||||
}
|
||||
|
||||
// If socket is available and connected, fetch the image
|
||||
if (socket && socket.connected && id && !isLoading) {
|
||||
if (context && context.socket && context.socket.connected && id && !isLoading) {
|
||||
setIsLoading(true);
|
||||
|
||||
socket.emit('getCategoryPic', { categoryId: id }, (response) => {
|
||||
context.socket.emit('getCategoryPic', { categoryId: id }, (response) => {
|
||||
setIsLoading(false);
|
||||
|
||||
if (response.success) {
|
||||
@@ -119,7 +119,7 @@ const CategoryBox = ({
|
||||
URL.revokeObjectURL(objectUrl);
|
||||
}
|
||||
};
|
||||
}, [socket, socket?.connected, id, isLoading]);
|
||||
}, [context, context?.socket?.connected, id, isLoading]);
|
||||
|
||||
return (
|
||||
<Paper
|
||||
|
||||
@@ -97,7 +97,12 @@ function getFilteredProducts(unfilteredProducts, attributes) {
|
||||
let filteredProducts = (unfilteredProducts || []).filter(product => {
|
||||
const availabilityFilter = sessionStorage.getItem('filter_availability');
|
||||
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;
|
||||
|
||||
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);
|
||||
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) {
|
||||
if (!window.productCache) {
|
||||
@@ -238,6 +267,8 @@ class Content extends Component {
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
fetchCategoryData(categoryId) {
|
||||
const cachedData = getCachedCategoryData(categoryId);
|
||||
if (cachedData) {
|
||||
@@ -251,14 +282,36 @@ class Content extends Component {
|
||||
console.log("Socket not connected yet, waiting for connection to fetch category data");
|
||||
return;
|
||||
}
|
||||
console.log(`productList:${categoryId}`);
|
||||
this.props.socket.off(`productList:${categoryId}`);
|
||||
|
||||
// Track if we've received the full response to ignore stub response if needed
|
||||
let receivedFullResponse = false;
|
||||
|
||||
this.props.socket.on(`productList:${categoryId}`,(response) => {
|
||||
console.log("getCategoryProducts full response", response);
|
||||
receivedFullResponse = true;
|
||||
setCachedCategoryData(categoryId, response);
|
||||
if (response && response.products !== undefined) {
|
||||
this.processDataWithCategoryTree(response, categoryId);
|
||||
} else {
|
||||
console.log("fetchCategoryData in Content failed", response);
|
||||
}
|
||||
});
|
||||
|
||||
this.props.socket.emit("getCategoryProducts", { categoryId: categoryId },
|
||||
(response) => {
|
||||
setCachedCategoryData(categoryId, response);
|
||||
if (response && response.products !== undefined) {
|
||||
this.processDataWithCategoryTree(response, categoryId);
|
||||
console.log("getCategoryProducts stub response", response);
|
||||
// Only process stub response if we haven't received the full response yet
|
||||
if (!receivedFullResponse) {
|
||||
setCachedCategoryData(categoryId, response);
|
||||
if (response && response.products !== undefined) {
|
||||
this.processDataWithCategoryTree(response, categoryId);
|
||||
} else {
|
||||
console.log("fetchCategoryData in Content failed", response);
|
||||
}
|
||||
} else {
|
||||
console.log("fetchCategoryData in Content failed", response);
|
||||
console.log("Ignoring stub response - full response already received");
|
||||
}
|
||||
}
|
||||
);
|
||||
@@ -417,7 +470,7 @@ class Content extends Component {
|
||||
|
||||
|
||||
return (
|
||||
<Container maxWidth="xl" sx={{ py: 2, flexGrow: 1, display: 'grid', gridTemplateRows: '1fr' }}>
|
||||
<Container maxWidth="xl" sx={{ py: { xs: 0, sm: 2 }, px: { xs: 0, sm: 3 }, flexGrow: 1, display: 'grid', gridTemplateRows: '1fr' }}>
|
||||
|
||||
{showCategoryBoxes ? (
|
||||
// Show category boxes layout when no products but have child categories
|
||||
@@ -467,30 +520,20 @@ class Content extends Component {
|
||||
</Box>
|
||||
|
||||
{/* Subcategories Grid */}
|
||||
<Box sx={{ flex: 1, minWidth: 0 }}>
|
||||
<CategoryBoxGrid
|
||||
categories={this.state.childCategories}
|
||||
showTitle={false}
|
||||
spacing={3}
|
||||
/>
|
||||
<Box sx={{ flexGrow: 1 }}>
|
||||
<CategoryBoxGrid categories={this.state.childCategories} />
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
} else {
|
||||
// Just show subcategories without parent
|
||||
return (
|
||||
<CategoryBoxGrid
|
||||
categories={this.state.childCategories}
|
||||
showTitle={false}
|
||||
spacing={3}
|
||||
/>
|
||||
);
|
||||
// No parent category, just show subcategories
|
||||
return <CategoryBoxGrid categories={this.state.childCategories} />;
|
||||
}
|
||||
})()}
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Show parent category navigation when in 2nd or 3rd level but no subcategories */}
|
||||
{/* Show standalone parent category navigation when there are only products */}
|
||||
{this.state.loaded &&
|
||||
this.props.params.categoryId &&
|
||||
!(this.state.unfilteredProducts.length > 0 && this.state.childCategories.length > 0) && (() => {
|
||||
@@ -534,7 +577,7 @@ class Content extends Component {
|
||||
<Box sx={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: { xs: '1fr', sm: '1fr 2fr', md: '1fr 3fr', lg: '1fr 4fr', xl: '1fr 4fr' },
|
||||
gap: 3
|
||||
gap: { xs: 0, sm: 3 }
|
||||
}}>
|
||||
|
||||
<Stack direction="row" spacing={0} sx={{
|
||||
@@ -661,10 +704,12 @@ class Content extends Component {
|
||||
<Box>
|
||||
<ProductList
|
||||
socket={this.props.socket}
|
||||
socketB={this.props.socketB}
|
||||
totalProductCount={(this.state.unfilteredProducts || []).length}
|
||||
products={this.state.filteredProducts || []}
|
||||
activeAttributeFilters={this.state.activeAttributeFilters || []}
|
||||
activeManufacturerFilters={this.state.activeManufacturerFilters || []}
|
||||
activeAvailabilityFilters={this.state.activeAvailabilityFilters || []}
|
||||
onFilterChange={()=>{this.filterProducts()}}
|
||||
dataType={this.state.dataType}
|
||||
dataParam={this.state.dataParam}
|
||||
|
||||
@@ -37,13 +37,17 @@ class Header extends Component {
|
||||
|
||||
render() {
|
||||
// Get socket directly from context in render method
|
||||
const socket = this.context;
|
||||
const {socket,socketB} = this.context;
|
||||
const { isHomePage, isProfilePage } = this.props;
|
||||
|
||||
return (
|
||||
<AppBar position="sticky" color="primary" elevation={0} sx={{ zIndex: 1100 }}>
|
||||
<Toolbar sx={{ minHeight: 64 }}>
|
||||
<Container maxWidth="lg" sx={{ display: 'flex', alignItems: 'center' }}>
|
||||
<Toolbar sx={{ minHeight: 64, py: { xs: 0.5, sm: 0 } }}>
|
||||
<Container maxWidth="lg" sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
px: { xs: 0, sm: 3 }
|
||||
}}>
|
||||
{/* First row: Logo and ButtonGroup on xs, all items on larger screens */}
|
||||
<Box sx={{
|
||||
display: 'flex',
|
||||
@@ -56,28 +60,41 @@ class Header extends Component {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
width: '100%',
|
||||
justifyContent: { xs: 'space-between', sm: 'flex-start' }
|
||||
justifyContent: { xs: 'space-between', sm: 'flex-start' },
|
||||
minHeight: { xs: 52, sm: 'auto' },
|
||||
px: { xs: 0, sm: 0 }
|
||||
}}>
|
||||
<Logo />
|
||||
{/* SearchBar visible on sm and up */}
|
||||
<Box sx={{ display: { xs: 'none', sm: 'block' }, flexGrow: 1 }}>
|
||||
<SearchBar />
|
||||
</Box>
|
||||
<ButtonGroupWithRouter socket={socket}/>
|
||||
<Box sx={{
|
||||
display: 'flex',
|
||||
alignItems: { xs: 'flex-end', sm: 'center' },
|
||||
transform: { xs: 'translateY(4px) translateX(9px)', sm: 'none' },
|
||||
ml: { xs: 0, sm: 0 }
|
||||
}}>
|
||||
<ButtonGroupWithRouter socket={socket}/>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* Second row: SearchBar only on xs */}
|
||||
{/* Second row: SearchBar only on xs - make it wider */}
|
||||
<Box sx={{
|
||||
display: { xs: 'block', sm: 'none' },
|
||||
width: '100%',
|
||||
mt: 1,mb: 1
|
||||
mt: { xs: 1, sm: 0 },
|
||||
mb: { xs: 0.5, sm: 0 },
|
||||
px: { xs: 0, sm: 0 }
|
||||
}}>
|
||||
<SearchBar />
|
||||
<Box sx={{ width: '100%' }}>
|
||||
<SearchBar />
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</Container>
|
||||
</Toolbar>
|
||||
{(isHomePage || this.props.categoryId || isProfilePage) && <CategoryList categoryId={209} activeCategoryId={this.props.categoryId} socket={socket} />}
|
||||
{(isHomePage || this.props.categoryId || isProfilePage) && <CategoryList categoryId={209} activeCategoryId={this.props.categoryId} socket={socket} socketB={socketB} />}
|
||||
</AppBar>
|
||||
);
|
||||
}
|
||||
@@ -91,7 +108,7 @@ const HeaderWithContext = (props) => {
|
||||
|
||||
return (
|
||||
<SocketContext.Consumer>
|
||||
{socket => <Header {...props} socket={socket} isHomePage={isHomePage} isProfilePage={isProfilePage} />}
|
||||
{({socket,socketB}) => <Header {...props} socket={socket} socketB={socketB} isHomePage={isHomePage} isProfilePage={isProfilePage} />}
|
||||
</SocketContext.Consumer>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -77,7 +77,7 @@ class Images extends Component {
|
||||
}
|
||||
|
||||
loadPic = (size,bildId,index) => {
|
||||
this.props.socket.emit('getPic', { bildId, size }, (res) => {
|
||||
this.props.socketB.emit('getPic', { bildId, size }, (res) => {
|
||||
if(res.success){
|
||||
const url = URL.createObjectURL(new Blob([res.imageBuffer], { type: 'image/jpeg' }));
|
||||
|
||||
|
||||
@@ -543,7 +543,7 @@ export class LoginComponent extends Component {
|
||||
onClick={this.handleOpen}
|
||||
sx={{ my: 1, mx: 1.5 }}
|
||||
>
|
||||
Anmelden
|
||||
Login
|
||||
</Button>
|
||||
)
|
||||
)}
|
||||
|
||||
381
src/components/Mollie.js
Normal file
381
src/components/Mollie.js
Normal 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;
|
||||
@@ -26,7 +26,8 @@ class Product extends Component {
|
||||
this.state = {image:window.smallPicCache[bildId],loading:false, error: false}
|
||||
}else{
|
||||
this.state = {image: null, loading: true, error: false};
|
||||
this.props.socket.emit('getPic', { bildId, size:'small' }, (res) => {
|
||||
console.log("Product: Fetching image from socketB", this.props.socketB);
|
||||
this.props.socketB.emit('getPic', { bildId, size:'small' }, (res) => {
|
||||
if(res.success){
|
||||
window.smallPicCache[bildId] = URL.createObjectURL(new Blob([res.imageBuffer], { type: 'image/jpeg' }));
|
||||
if (this._isMounted) {
|
||||
@@ -179,18 +180,21 @@ class Product extends Component {
|
||||
|
||||
<Card
|
||||
sx={{
|
||||
width: { xs: 'calc(100vw - 48px)', sm: '250px' },
|
||||
minWidth: { xs: 'calc(100vw - 48px)', sm: '250px' },
|
||||
width: { xs: '100vw', sm: '250px' },
|
||||
minWidth: { xs: '100vw', sm: '250px' },
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
transition: 'transform 0.3s ease, box-shadow 0.3s ease',
|
||||
position: 'relative',
|
||||
overflow: 'hidden',
|
||||
borderRadius: '8px',
|
||||
borderRadius: { xs: 0, sm: '8px' },
|
||||
border: { xs: 'none', sm: 'inherit' },
|
||||
boxShadow: { xs: 'none', sm: 'inherit' },
|
||||
mx: { xs: 0, sm: 'auto' },
|
||||
'&:hover': {
|
||||
transform: 'translateY(-5px)',
|
||||
boxShadow: '0px 10px 20px rgba(0,0,0,0.1)'
|
||||
transform: { xs: 'none', sm: 'translateY(-5px)' },
|
||||
boxShadow: { xs: 'none', sm: '0px 10px 20px rgba(0,0,0,0.1)' }
|
||||
}
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -110,8 +110,8 @@ class ProductDetailPage extends Component {
|
||||
}
|
||||
} else {
|
||||
// Not in cache, fetch from server
|
||||
if (this.props.socket && this.props.socket.connected) {
|
||||
this.props.socket.emit(
|
||||
if (this.props.socketB && this.props.socketB.connected) {
|
||||
this.props.socketB.emit(
|
||||
"getAttributePicture",
|
||||
{ id: cacheKey },
|
||||
(res) => {
|
||||
@@ -334,6 +334,7 @@ class ProductDetailPage extends Component {
|
||||
{product.pictureList && (
|
||||
<Images
|
||||
socket={this.props.socket}
|
||||
socketB={this.props.socketB}
|
||||
pictureList={product.pictureList}
|
||||
fullscreenOpen={this.state.imageDialogOpen}
|
||||
onOpenFullscreen={this.handleOpenDialog}
|
||||
|
||||
@@ -11,7 +11,7 @@ const ProductDetailWithSocket = () => {
|
||||
|
||||
return (
|
||||
<SocketContext.Consumer>
|
||||
{socket => <ProductDetailPage seoName={seoName} navigate={navigate} location={location} socket={socket} />}
|
||||
{({socket,socketB}) => <ProductDetailPage seoName={seoName} navigate={navigate} location={location} socket={socket} socketB={socketB} />}
|
||||
</SocketContext.Consumer>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -162,13 +162,17 @@ class ProductFilters extends Component {
|
||||
return (
|
||||
<Paper
|
||||
id="filters-paper"
|
||||
elevation={1}
|
||||
elevation={window.innerWidth < 600 ? 0 : 1}
|
||||
sx={{
|
||||
p: 2,
|
||||
borderRadius: 2,
|
||||
p: { xs: 1, sm: 2 },
|
||||
borderRadius: { xs: 0, sm: 2 },
|
||||
bgcolor: 'background.paper',
|
||||
display: 'flex',
|
||||
flexDirection: 'column'
|
||||
flexDirection: 'column',
|
||||
border: { xs: 'none', sm: 'inherit' },
|
||||
boxShadow: { xs: 'none', sm: 'inherit' },
|
||||
mx: { xs: 0, sm: 'auto' },
|
||||
width: { xs: '100%', sm: 'auto' }
|
||||
}}
|
||||
>
|
||||
|
||||
|
||||
@@ -122,9 +122,19 @@ class ProductList extends Component {
|
||||
}
|
||||
|
||||
renderPagination = (pages, page) => {
|
||||
// Make pagination invisible when there are zero products to avoid layout shifts
|
||||
const hasProducts = this.props.products.length > 0;
|
||||
|
||||
return (
|
||||
<Box sx={{ height: 64, display: 'flex', alignItems: 'center', justifyContent: 'left' }}>
|
||||
{((this.state.itemsPerPage==='all')||(this.props.products.length<this.state.itemsPerPage))?null:
|
||||
<Box sx={{
|
||||
height: 64,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'left',
|
||||
width: '100%',
|
||||
visibility: hasProducts ? 'visible' : 'hidden'
|
||||
}}>
|
||||
{(this.state.itemsPerPage==='all')?null:
|
||||
<Pagination
|
||||
count={pages}
|
||||
page={page}
|
||||
@@ -150,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() {
|
||||
//console.log('products',this.props.activeAttributeFilters,this.props.activeManufacturerFilters,window.currentSearchQuery,this.state.sortBy);
|
||||
|
||||
@@ -164,15 +225,56 @@ class ProductList extends Component {
|
||||
const products = this.state.itemsPerPage==='all'?[...filteredProducts]:filteredProducts.slice((this.state.page - 1) * this.state.itemsPerPage , this.state.page * this.state.itemsPerPage);
|
||||
|
||||
return (
|
||||
<Box sx={{ height: '100%' }}>
|
||||
<Box sx={{ height: '100%', px: { xs: 0, sm: 0 } }}>
|
||||
<Box sx={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center'
|
||||
alignItems: 'center',
|
||||
flexDirection: { xs: 'column', sm: 'row' },
|
||||
gap: { xs: 1, sm: 0 },
|
||||
px: { xs: 0, sm: 0 },
|
||||
py: { xs: 1, sm: 0 },
|
||||
bgcolor: { xs: '#e8f5e8', sm: 'transparent' },
|
||||
mb: { xs: 0, sm: 0 }
|
||||
}}>
|
||||
|
||||
|
||||
<Box sx={{ display: 'flex', gap: 1, alignItems: 'center', flexWrap: 'wrap' }}>
|
||||
<Box sx={{
|
||||
display: 'flex',
|
||||
gap: { xs: 0.5, sm: 1 },
|
||||
alignItems: 'center',
|
||||
flexWrap: 'wrap',
|
||||
order: { xs: 2, sm: 1 },
|
||||
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) => (
|
||||
<Chip
|
||||
size="medium"
|
||||
@@ -205,11 +307,26 @@ class ProductList extends Component {
|
||||
clickable
|
||||
/>
|
||||
))}
|
||||
|
||||
</Box>
|
||||
|
||||
<Box sx={{ display: 'flex', gap: 2, alignItems: 'center' }}>
|
||||
<Box sx={{
|
||||
display: 'flex',
|
||||
gap: { xs: 1, sm: 2 },
|
||||
alignItems: 'center',
|
||||
order: { xs: 1, sm: 2 },
|
||||
width: { xs: '100%', sm: 'auto' },
|
||||
justifyContent: { xs: 'space-between', sm: 'flex-end' },
|
||||
px: { xs: 1, sm: 0 }
|
||||
}}>
|
||||
{/* Sort Dropdown */}
|
||||
<FormControl variant="outlined" size="small" sx={{ minWidth: 140 }}>
|
||||
<FormControl
|
||||
variant={window.innerWidth < 600 ? 'standard' : 'outlined'}
|
||||
size="small"
|
||||
sx={{
|
||||
minWidth: { xs: 120, sm: 140 }
|
||||
}}
|
||||
>
|
||||
<InputLabel id="sort-by-label">Sortierung</InputLabel>
|
||||
<Select
|
||||
size="small"
|
||||
@@ -244,7 +361,13 @@ class ProductList extends Component {
|
||||
</FormControl>
|
||||
|
||||
{/* Per Page Dropdown */}
|
||||
<FormControl variant="outlined" size="small" sx={{ minWidth: 100 }}>
|
||||
<FormControl
|
||||
variant={window.innerWidth < 600 ? 'standard' : 'outlined'}
|
||||
size="small"
|
||||
sx={{
|
||||
minWidth: { xs: 80, sm: 100 }
|
||||
}}
|
||||
>
|
||||
<InputLabel id="products-per-page-label">pro Seite</InputLabel>
|
||||
<Select
|
||||
labelId="products-per-page-label"
|
||||
@@ -278,39 +401,56 @@ class ProductList extends Component {
|
||||
<MenuItem value="all">Alle</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
|
||||
{/* Product count info - mobile only */}
|
||||
<Box sx={{
|
||||
display: { xs: 'block', sm: 'none' },
|
||||
ml: 1
|
||||
}}>
|
||||
<Typography variant="body2" color="text.secondary" sx={{ fontSize: '0.75rem', whiteSpace: 'nowrap' }}>
|
||||
{this.getProductCountText()}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Box sx={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center'
|
||||
alignItems: 'center',
|
||||
px: { xs: 0, sm: 0 },
|
||||
py: { xs: 1, sm: 0 },
|
||||
bgcolor: { xs: '#e8f5e8', sm: 'transparent' },
|
||||
mt: { xs: 0, sm: 0 }
|
||||
}}>
|
||||
{ this.renderPagination(Math.ceil(filteredProducts.length / this.state.itemsPerPage), this.state.page) }
|
||||
<Stack direction="row" spacing={2}>
|
||||
<Box sx={{ px: { xs: 1, sm: 0 }, width: '100%' }}>
|
||||
{ this.renderPagination(Math.ceil(filteredProducts.length / this.state.itemsPerPage), this.state.page) }
|
||||
</Box>
|
||||
<Stack direction="row" spacing={2} sx={{ display: { xs: 'none', sm: 'flex' }, px: { xs: 1, sm: 0 } }}>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{/*this.props.dataType == 'category' && (<>Kategorie: {this.props.dataParam}</>)}*/}
|
||||
{this.props.dataType == 'search' && (<>Suchergebnisse für: "{this.props.dataParam}"</>)}
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{
|
||||
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 variant="body2" color="text.secondary" sx={{ whiteSpace: 'nowrap' }}>
|
||||
{this.getProductCountText()}
|
||||
</Typography>
|
||||
</Stack>
|
||||
</Box>
|
||||
|
||||
<Grid container spacing={2}>
|
||||
{products.map((product) => (
|
||||
<Grid container spacing={{ xs: 0, sm: 2 }}>
|
||||
{this.renderNoProductsMessage()}
|
||||
{products.map((product, index) => (
|
||||
<Grid
|
||||
key={product.id}
|
||||
key={product.id}
|
||||
sx={{
|
||||
display: 'flex',
|
||||
justifyContent: { xs: 'stretch', sm: 'center' },
|
||||
mb: 1
|
||||
mb: { xs: 0, sm: 1 },
|
||||
width: { xs: '100%', sm: 'auto' },
|
||||
borderBottom: {
|
||||
xs: index < products.length - 1 ? '16px solid #e8f5e8' : 'none',
|
||||
sm: 'none'
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Product
|
||||
@@ -331,14 +471,25 @@ class ProductList extends Component {
|
||||
versandklasse={product.versandklasse}
|
||||
weight={product.weight}
|
||||
socket={this.props.socket}
|
||||
socketB={this.props.socketB}
|
||||
pictureList={product.pictureList}
|
||||
availableSupplier={product.availableSupplier}
|
||||
/>
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
|
||||
{this.renderPagination(Math.ceil(filteredProducts.length / this.state.itemsPerPage), this.state.page)}
|
||||
|
||||
{/* Bottom pagination */}
|
||||
<Box sx={{
|
||||
px: { xs: 0, sm: 0 },
|
||||
py: { xs: 1, sm: 1 },
|
||||
bgcolor: { xs: '#e8f5e8', sm: 'transparent' },
|
||||
mt: { xs: 0, sm: 2 }
|
||||
}}>
|
||||
<Box sx={{ px: { xs: 1, sm: 0 } }}>
|
||||
{this.renderPagination(Math.ceil(filteredProducts.length / this.state.itemsPerPage), this.state.page)}
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -142,7 +142,7 @@ class ButtonGroup extends Component {
|
||||
onClose={this.toggleCart}
|
||||
disableScrollLock={true}
|
||||
>
|
||||
<Box sx={{ width: 420, p: 2 }}>
|
||||
<Box sx={{ width: { xs: '100vw', sm: 420 }, p: { xs: 1, sm: 2 } }}>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
|
||||
@@ -3,8 +3,11 @@ import Box from "@mui/material/Box";
|
||||
import Container from "@mui/material/Container";
|
||||
import Button from "@mui/material/Button";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import Collapse from "@mui/material/Collapse";
|
||||
import { Link } from "react-router-dom";
|
||||
import HomeIcon from "@mui/icons-material/Home";
|
||||
import MenuIcon from "@mui/icons-material/Menu";
|
||||
import CloseIcon from "@mui/icons-material/Close";
|
||||
|
||||
class CategoryList extends Component {
|
||||
findCategoryById = (category, targetId) => {
|
||||
@@ -54,6 +57,7 @@ class CategoryList extends Component {
|
||||
level3Categories: [], // Children of active level 2 category
|
||||
activePath: [], // Array of active category objects for each level
|
||||
fetchedCategories: false,
|
||||
mobileMenuOpen: false, // State for mobile collapsible menu
|
||||
};
|
||||
|
||||
// Try to get cached data for SSR
|
||||
@@ -124,11 +128,13 @@ class CategoryList extends Component {
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
// Handle socket connection changes
|
||||
|
||||
const wasConnected = prevProps.socket && prevProps.socket.connected;
|
||||
const isNowConnected = this.props.socket && this.props.socket.connected;
|
||||
|
||||
if (!wasConnected && isNowConnected && !this.state.fetchedCategories) {
|
||||
// Socket just connected and we haven't fetched categories yet
|
||||
|
||||
this.setState(
|
||||
{
|
||||
fetchedCategories: false,
|
||||
@@ -158,7 +164,7 @@ class CategoryList extends Component {
|
||||
}
|
||||
|
||||
if (this.state.fetchedCategories) {
|
||||
//console.log('Categories already fetched, skipping');
|
||||
console.log('Categories already fetched, skipping');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -222,7 +228,6 @@ class CategoryList extends Component {
|
||||
//console.log('CategoryList: Fetching categories from socket');
|
||||
socket.emit("categoryList", { categoryId: 209 }, (response) => {
|
||||
if (response && response.categoryTree) {
|
||||
//console.log('Category tree received:', response.categoryTree);
|
||||
|
||||
// Store in global cache with timestamp
|
||||
try {
|
||||
@@ -237,7 +242,6 @@ class CategoryList extends Component {
|
||||
} catch (err) {
|
||||
console.error("Error writing to cache:", err);
|
||||
}
|
||||
|
||||
this.processCategoryTree(response.categoryTree);
|
||||
} else {
|
||||
try {
|
||||
@@ -313,18 +317,32 @@ class CategoryList extends Component {
|
||||
});
|
||||
};
|
||||
|
||||
handleMobileMenuToggle = () => {
|
||||
this.setState(prevState => ({
|
||||
mobileMenuOpen: !prevState.mobileMenuOpen
|
||||
}));
|
||||
};
|
||||
|
||||
handleMobileCategoryClick = () => {
|
||||
// Close the mobile menu when a category is selected
|
||||
this.setState({
|
||||
mobileMenuOpen: false
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
const { level1Categories, level2Categories, level3Categories, activePath } =
|
||||
const { level1Categories, activePath, mobileMenuOpen } =
|
||||
this.state;
|
||||
|
||||
const renderCategoryRow = (categories, level = 1) => (
|
||||
const renderCategoryRow = (categories, level = 1, isMobile = false) => (
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
justifyContent: "flex-start",
|
||||
alignItems: "center",
|
||||
flexWrap: "nowrap",
|
||||
overflowX: "auto",
|
||||
flexWrap: isMobile ? "wrap" : "nowrap",
|
||||
overflowX: isMobile ? "visible" : "auto",
|
||||
flexDirection: isMobile ? "column" : "row",
|
||||
py: 0.5, // Add vertical padding to prevent border clipping
|
||||
"&::-webkit-scrollbar": {
|
||||
display: "none",
|
||||
@@ -340,34 +358,76 @@ class CategoryList extends Component {
|
||||
color="inherit"
|
||||
size="small"
|
||||
aria-label="Zur Startseite"
|
||||
onClick={isMobile ? this.handleMobileCategoryClick : undefined}
|
||||
sx={{
|
||||
fontSize: "0.75rem",
|
||||
fontWeight: "normal",
|
||||
textTransform: "none",
|
||||
whiteSpace: "nowrap",
|
||||
opacity: 0.9,
|
||||
mx: 0.5,
|
||||
my: 0.25, // Add consistent vertical margin to account for borders
|
||||
minWidth: "auto",
|
||||
border: "2px solid transparent", // Always have border space
|
||||
borderRadius: 1, // Always have border radius
|
||||
mx: isMobile ? 0 : 0.5,
|
||||
my: 0.25,
|
||||
minWidth: isMobile ? "100%" : "auto",
|
||||
borderRadius: 1,
|
||||
justifyContent: isMobile ? "flex-start" : "center",
|
||||
transition: "all 0.2s ease",
|
||||
textShadow: "0 1px 2px rgba(0,0,0,0.3)",
|
||||
position: "relative",
|
||||
...(this.props.activeCategoryId === null && {
|
||||
boxShadow: "0 4px 12px rgba(0,0,0,0.5)",
|
||||
transform: "translateY(-2px)",
|
||||
bgcolor: "rgba(255,255,255,0.25)",
|
||||
borderColor: "rgba(255,255,255,0.6)", // Change border color instead of adding border
|
||||
fontWeight: "bold",
|
||||
bgcolor: "#fff",
|
||||
textShadow: "none",
|
||||
opacity: 1,
|
||||
}),
|
||||
"&:hover": {
|
||||
opacity: 1,
|
||||
bgcolor: "rgba(255,255,255,0.15)",
|
||||
transform: "translateY(-1px)",
|
||||
boxShadow: "0 2px 6px rgba(0,0,0,0.3)",
|
||||
bgcolor: "#fff",
|
||||
textShadow: "none",
|
||||
"& .MuiSvgIcon-root": {
|
||||
color: "#2e7d32 !important",
|
||||
},
|
||||
"& .bold-text": {
|
||||
color: "#2e7d32 !important",
|
||||
},
|
||||
"& .thin-text": {
|
||||
color: "transparent !important",
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
<HomeIcon sx={{ fontSize: "1rem" }} />
|
||||
<HomeIcon sx={{
|
||||
fontSize: "1rem",
|
||||
mr: isMobile ? 1 : 0,
|
||||
color: this.props.activeCategoryId === null ? "#2e7d32" : "inherit"
|
||||
}} />
|
||||
{isMobile && (
|
||||
<Box sx={{ position: "relative", display: "inline-block" }}>
|
||||
{/* Bold text (always rendered to set width) */}
|
||||
<Box
|
||||
className="bold-text"
|
||||
sx={{
|
||||
fontWeight: "bold",
|
||||
color: this.props.activeCategoryId === null ? "#2e7d32" : "transparent",
|
||||
position: "relative",
|
||||
zIndex: 2,
|
||||
}}
|
||||
>
|
||||
Startseite
|
||||
</Box>
|
||||
{/* Thin text (positioned on top) */}
|
||||
<Box
|
||||
className="thin-text"
|
||||
sx={{
|
||||
fontWeight: "400",
|
||||
color: this.props.activeCategoryId === null ? "transparent" : "inherit",
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
zIndex: 1,
|
||||
}}
|
||||
>
|
||||
Startseite
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
{this.state.fetchedCategories && categories.length > 0 ? (
|
||||
@@ -385,39 +445,72 @@ class CategoryList extends Component {
|
||||
to={`/Kategorie/${category.seoName}`}
|
||||
color="inherit"
|
||||
size="small"
|
||||
onClick={isMobile ? this.handleMobileCategoryClick : undefined}
|
||||
sx={{
|
||||
fontSize: "0.75rem",
|
||||
fontWeight: "normal",
|
||||
textTransform: "none",
|
||||
whiteSpace: "nowrap",
|
||||
opacity: 0.9,
|
||||
mx: 0.5,
|
||||
my: 0.25, // Add consistent vertical margin to account for borders
|
||||
border: "2px solid transparent", // Always have border space
|
||||
borderRadius: 1, // Always have border radius
|
||||
mx: isMobile ? 0 : 0.5,
|
||||
my: 0.25,
|
||||
minWidth: isMobile ? "100%" : "auto",
|
||||
borderRadius: 1,
|
||||
justifyContent: isMobile ? "flex-start" : "center",
|
||||
transition: "all 0.2s ease",
|
||||
textShadow: "0 1px 2px rgba(0,0,0,0.3)",
|
||||
position: "relative",
|
||||
...(isActiveAtThisLevel && {
|
||||
boxShadow: "0 4px 12px rgba(0,0,0,0.5)",
|
||||
transform: "translateY(-2px)",
|
||||
bgcolor: "rgba(255,255,255,0.25)",
|
||||
borderColor: "rgba(255,255,255,0.6)", // Change border color instead of adding border
|
||||
fontWeight: "bold",
|
||||
bgcolor: "#fff",
|
||||
textShadow: "none",
|
||||
opacity: 1,
|
||||
}),
|
||||
"&:hover": {
|
||||
opacity: 1,
|
||||
bgcolor: "rgba(255,255,255,0.15)",
|
||||
transform: "translateY(-1px)",
|
||||
boxShadow: "0 2px 6px rgba(0,0,0,0.3)",
|
||||
bgcolor: "#fff",
|
||||
textShadow: "none",
|
||||
"& .bold-text": {
|
||||
color: "#2e7d32 !important",
|
||||
},
|
||||
"& .thin-text": {
|
||||
color: "transparent !important",
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
{category.name}
|
||||
<Box sx={{ position: "relative", display: "inline-block" }}>
|
||||
{/* Bold text (always rendered to set width) */}
|
||||
<Box
|
||||
className="bold-text"
|
||||
sx={{
|
||||
fontWeight: "bold",
|
||||
color: isActiveAtThisLevel ? "#2e7d32" : "transparent",
|
||||
position: "relative",
|
||||
zIndex: 2,
|
||||
}}
|
||||
>
|
||||
{category.name}
|
||||
</Box>
|
||||
{/* Thin text (positioned on top) */}
|
||||
<Box
|
||||
className="thin-text"
|
||||
sx={{
|
||||
fontWeight: "400",
|
||||
color: isActiveAtThisLevel ? "transparent" : "inherit",
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
zIndex: 1,
|
||||
}}
|
||||
>
|
||||
{category.name}
|
||||
</Box>
|
||||
</Box>
|
||||
</Button>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
) : (
|
||||
level === 1 && (
|
||||
level === 1 && !isMobile && (
|
||||
<Typography
|
||||
variant="caption"
|
||||
color="inherit"
|
||||
@@ -447,30 +540,87 @@ class CategoryList extends Component {
|
||||
|
||||
return (
|
||||
<Profiler id="CategoryList" onRender={onRenderCallback}>
|
||||
{/* Desktop Menu - Hidden on xs, shown on sm and up */}
|
||||
<Box
|
||||
sx={{
|
||||
width: "100%",
|
||||
bgcolor: "primary.dark",
|
||||
display: { xs: "none", md: "block" },
|
||||
display: { xs: "none", sm: "block" },
|
||||
}}
|
||||
>
|
||||
<Container maxWidth="lg" sx={{ px: 2 }}>
|
||||
{/* Level 1 Categories Row - Always shown */}
|
||||
{renderCategoryRow(level1Categories, 1)}
|
||||
{renderCategoryRow(level1Categories, 1, false)}
|
||||
|
||||
{/* Level 2 Categories Row - Show when level 1 is selected */}
|
||||
{/* DISABLED FOR NOW
|
||||
{level2Categories.length > 0 && (
|
||||
<Box sx={{ mt: 0.5 }}>
|
||||
{renderCategoryRow(level2Categories, 2)}
|
||||
{renderCategoryRow(level2Categories, 2, false)}
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Level 3 Categories Row - Show when level 2 is selected */}
|
||||
{/* DISABLED FOR NOW
|
||||
{level3Categories.length > 0 && (
|
||||
<Box sx={{ mt: 0.5 }}>
|
||||
{renderCategoryRow(level3Categories, 3)}
|
||||
{renderCategoryRow(level3Categories, 3, false)}
|
||||
</Box>
|
||||
)}
|
||||
*/}
|
||||
</Container>
|
||||
</Box>
|
||||
|
||||
{/* Mobile Menu - Shown only on xs screens */}
|
||||
<Box
|
||||
sx={{
|
||||
width: "100%",
|
||||
bgcolor: "primary.dark",
|
||||
display: { xs: "block", sm: "none" },
|
||||
}}
|
||||
>
|
||||
<Container maxWidth="lg" sx={{ px: 2 }}>
|
||||
{/* Toggle Button */}
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
py: 1,
|
||||
cursor: "pointer",
|
||||
"&:hover": {
|
||||
bgcolor: "rgba(255,255,255,0.1)"
|
||||
}
|
||||
}}
|
||||
onClick={this.handleMobileMenuToggle}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
aria-label={mobileMenuOpen ? "Kategorien schließen" : "Kategorien öffnen"}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
this.handleMobileMenuToggle();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Typography variant="subtitle2" color="inherit" sx={{
|
||||
fontWeight: "bold",
|
||||
textShadow: "0 1px 2px rgba(0,0,0,0.3)"
|
||||
}}>
|
||||
Kategorien
|
||||
</Typography>
|
||||
<Box sx={{ display: "flex", alignItems: "center" }}>
|
||||
{mobileMenuOpen ? <CloseIcon /> : <MenuIcon />}
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* Collapsible Menu Content */}
|
||||
<Collapse in={mobileMenuOpen}>
|
||||
<Box sx={{ pb: 2 }}>
|
||||
{/* Level 1 Categories - Only level shown in mobile menu */}
|
||||
{renderCategoryRow(level1Categories, 1, true)}
|
||||
</Box>
|
||||
</Collapse>
|
||||
</Container>
|
||||
</Box>
|
||||
</Profiler>
|
||||
|
||||
@@ -15,7 +15,7 @@ import SocketContext from "../../contexts/SocketContext.js";
|
||||
const SearchBar = () => {
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const socket = React.useContext(SocketContext);
|
||||
const context = React.useContext(SocketContext);
|
||||
const searchParams = new URLSearchParams(location.search);
|
||||
|
||||
// State management
|
||||
@@ -58,7 +58,7 @@ const SearchBar = () => {
|
||||
// @note Autocomplete function using getSearchProducts Socket.io API - returns objects with name and seoName
|
||||
const fetchAutocomplete = React.useCallback(
|
||||
(query) => {
|
||||
if (!socket || !query || query.length < 2) {
|
||||
if (!context || !context.socket || !context.socket.connected || !query || query.length < 2) {
|
||||
setSuggestions([]);
|
||||
setShowSuggestions(false);
|
||||
setLoadingSuggestions(false);
|
||||
@@ -67,7 +67,7 @@ const SearchBar = () => {
|
||||
|
||||
setLoadingSuggestions(true);
|
||||
|
||||
socket.emit(
|
||||
context.socket.emit(
|
||||
"getSearchProducts",
|
||||
{
|
||||
query: query.trim(),
|
||||
@@ -90,7 +90,7 @@ const SearchBar = () => {
|
||||
}
|
||||
);
|
||||
},
|
||||
[socket]
|
||||
[context]
|
||||
);
|
||||
|
||||
const handleSearchChange = (e) => {
|
||||
@@ -219,7 +219,7 @@ const SearchBar = () => {
|
||||
onSubmit={handleSearch}
|
||||
sx={{
|
||||
flexGrow: 1,
|
||||
mx: { xs: 1, sm: 2, md: 4 },
|
||||
mx: { xs: 0, sm: 2, md: 4 },
|
||||
position: "relative",
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -51,6 +51,9 @@ class CartTab extends Component {
|
||||
showStripePayment: false,
|
||||
StripeComponent: null,
|
||||
isLoadingStripe: false,
|
||||
showMolliePayment: false,
|
||||
MollieComponent: null,
|
||||
isLoadingMollie: false,
|
||||
showPaymentConfirmation: false,
|
||||
orderCompleted: false,
|
||||
originalCartItems: []
|
||||
@@ -67,8 +70,8 @@ class CartTab extends Component {
|
||||
|
||||
// @note Add method to fetch and apply order template prefill data
|
||||
fetchOrderTemplate = () => {
|
||||
if (this.context && this.context.connected) {
|
||||
this.context.emit('getOrderTemplate', (response) => {
|
||||
if (this.context && this.context.socket && this.context.socket.connected) {
|
||||
this.context.socket.emit('getOrderTemplate', (response) => {
|
||||
if (response.success && response.orderTemplate) {
|
||||
const template = response.orderTemplate;
|
||||
|
||||
@@ -116,7 +119,7 @@ class CartTab extends Component {
|
||||
// Determine payment method - respect constraints
|
||||
let prefillPaymentMethod = template.payment_method || "wire";
|
||||
const paymentMethodMap = {
|
||||
"credit_card": "stripe",
|
||||
"credit_card": "mollie",//stripe
|
||||
"bank_transfer": "wire",
|
||||
"cash_on_delivery": "onDelivery",
|
||||
"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 = () => {
|
||||
this.setState({ completionError: null }); // Clear previous errors
|
||||
|
||||
@@ -363,6 +387,25 @@ class CartTab extends Component {
|
||||
this.orderService.createStripeIntent(totalAmount, this.loadStripeComponent);
|
||||
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
|
||||
const orderData = {
|
||||
@@ -398,6 +441,9 @@ class CartTab extends Component {
|
||||
showStripePayment,
|
||||
StripeComponent,
|
||||
isLoadingStripe,
|
||||
showMolliePayment,
|
||||
MollieComponent,
|
||||
isLoadingMollie,
|
||||
showPaymentConfirmation,
|
||||
orderCompleted,
|
||||
} = this.state;
|
||||
@@ -409,7 +455,7 @@ class CartTab extends Component {
|
||||
const displayError = completionError || preSubmitError;
|
||||
|
||||
return (
|
||||
<Box sx={{ p: 3 }}>
|
||||
<Box sx={{ p: { xs: 1, sm: 3 } }}>
|
||||
{/* Payment Confirmation */}
|
||||
{showPaymentConfirmation && (
|
||||
<PaymentConfirmationDialog
|
||||
@@ -433,8 +479,8 @@ class CartTab extends Component {
|
||||
{!showPaymentConfirmation && (
|
||||
<CartDropdown
|
||||
cartItems={cartItems}
|
||||
socket={this.context}
|
||||
showDetailedSummary={showStripePayment}
|
||||
socket={this.context.socket}
|
||||
showDetailedSummary={showStripePayment || showMolliePayment}
|
||||
deliveryMethod={deliveryMethod}
|
||||
deliveryCost={deliveryCost}
|
||||
/>
|
||||
@@ -442,7 +488,7 @@ class CartTab extends Component {
|
||||
|
||||
{cartItems.length > 0 && (
|
||||
<Box sx={{ mt: 3 }}>
|
||||
{isLoadingStripe ? (
|
||||
{isLoadingStripe || isLoadingMollie ? (
|
||||
<Box sx={{ textAlign: 'center', py: 4 }}>
|
||||
<Typography variant="body1">
|
||||
Zahlungskomponente wird geladen...
|
||||
@@ -468,9 +514,29 @@ class CartTab extends Component {
|
||||
</Box>
|
||||
<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
|
||||
paymentMethod={paymentMethod}
|
||||
paymentMethod={paymentMethod}
|
||||
invoiceAddress={invoiceAddress}
|
||||
deliveryAddress={deliveryAddress}
|
||||
useSameAddress={useSameAddress}
|
||||
@@ -478,7 +544,7 @@ class CartTab extends Component {
|
||||
addressFormErrors={addressFormErrors}
|
||||
termsAccepted={termsAccepted}
|
||||
note={note}
|
||||
deliveryMethod={deliveryMethod}
|
||||
deliveryMethod={deliveryMethod}
|
||||
hasStecklinge={hasStecklinge}
|
||||
isPickupOnly={isPickupOnly}
|
||||
deliveryCost={deliveryCost}
|
||||
|
||||
@@ -120,7 +120,7 @@ class OrderProcessingService {
|
||||
|
||||
// If socket is ready, process immediately
|
||||
const context = this.getContext();
|
||||
if (context && context.connected) {
|
||||
if (context && context.socket && context.socket.connected) {
|
||||
const { isLoggedIn: isAuthenticated } = isUserLoggedIn();
|
||||
if (isAuthenticated) {
|
||||
this.sendStripeOrder();
|
||||
@@ -131,7 +131,7 @@ class OrderProcessingService {
|
||||
// Wait for socket to be ready
|
||||
this.socketHandler = () => {
|
||||
const context = this.getContext();
|
||||
if (context && context.connected) {
|
||||
if (context && context.socket && context.socket.connected) {
|
||||
const { isLoggedIn: isAuthenticated } = isUserLoggedIn();
|
||||
const state = this.getState();
|
||||
|
||||
@@ -189,7 +189,7 @@ class OrderProcessingService {
|
||||
|
||||
// Emit stripe order to backend via socket.io
|
||||
const context = this.getContext();
|
||||
context.emit("issueStripeOrder", orderData, (response) => {
|
||||
context.socket.emit("issueStripeOrder", orderData, (response) => {
|
||||
if (response.success) {
|
||||
this.setState({
|
||||
isCompletingOrder: false,
|
||||
@@ -208,8 +208,8 @@ class OrderProcessingService {
|
||||
// Process regular (non-Stripe) orders
|
||||
processRegularOrder(orderData) {
|
||||
const context = this.getContext();
|
||||
if (context) {
|
||||
context.emit("issueOrder", orderData, (response) => {
|
||||
if (context && context.socket && context.socket.connected) {
|
||||
context.socket.emit("issueOrder", orderData, (response) => {
|
||||
if (response.success) {
|
||||
// Clear the cart
|
||||
window.cart = [];
|
||||
@@ -246,8 +246,8 @@ class OrderProcessingService {
|
||||
// Create Stripe payment intent
|
||||
createStripeIntent(totalAmount, loadStripeComponent) {
|
||||
const context = this.getContext();
|
||||
if (context) {
|
||||
context.emit(
|
||||
if (context && context.socket && context.socket.connected) {
|
||||
context.socket.emit(
|
||||
"createStripeIntent",
|
||||
{ amount: totalAmount },
|
||||
(response) => {
|
||||
@@ -270,6 +270,10 @@ class OrderProcessingService {
|
||||
});
|
||||
}
|
||||
}
|
||||
// Create Mollie payment intent
|
||||
createMollieIntent(totalAmount, loadMollieComponent) {
|
||||
loadMollieComponent();
|
||||
}
|
||||
|
||||
// Calculate delivery cost
|
||||
getDeliveryCost() {
|
||||
|
||||
@@ -68,7 +68,7 @@ const OrdersTab = ({ orderIdFromHash }) => {
|
||||
const [selectedOrder, setSelectedOrder] = useState(null);
|
||||
const [isDetailsDialogOpen, setIsDetailsDialogOpen] = useState(false);
|
||||
|
||||
const socket = useContext(SocketContext);
|
||||
const {socket} = useContext(SocketContext);
|
||||
const navigate = useNavigate();
|
||||
|
||||
const handleViewDetails = useCallback(
|
||||
@@ -139,7 +139,7 @@ const OrdersTab = ({ orderIdFromHash }) => {
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<Box sx={{ p: 3, display: "flex", justifyContent: "center" }}>
|
||||
<Box sx={{ p: { xs: 1, sm: 3 }, display: "flex", justifyContent: "center" }}>
|
||||
<CircularProgress />
|
||||
</Box>
|
||||
);
|
||||
@@ -147,14 +147,14 @@ const OrdersTab = ({ orderIdFromHash }) => {
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<Box sx={{ p: 3 }}>
|
||||
<Box sx={{ p: { xs: 1, sm: 3 } }}>
|
||||
<Alert severity="error">{error}</Alert>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box sx={{ p: 3 }}>
|
||||
<Box sx={{ p: { xs: 1, sm: 3 } }}>
|
||||
{orders.length > 0 ? (
|
||||
<TableContainer component={Paper}>
|
||||
<Table>
|
||||
|
||||
@@ -24,7 +24,7 @@ const PaymentMethodSelector = ({ paymentMethod, onChange, deliveryMethod, onDeli
|
||||
// Handle delivery method changes - auto-switch to stripe when DHL/DPD is selected
|
||||
useEffect(() => {
|
||||
if ((deliveryMethod === "DHL" || deliveryMethod === "DPD") && paymentMethod === "cash") {
|
||||
handlePaymentMethodChange({ target: { value: "stripe" } });
|
||||
handlePaymentMethodChange({ target: { value: "mollie" } });
|
||||
}
|
||||
}, [deliveryMethod, paymentMethod, handlePaymentMethodChange]);
|
||||
|
||||
@@ -42,8 +42,22 @@ const PaymentMethodSelector = ({ paymentMethod, onChange, deliveryMethod, onDeli
|
||||
description: "Bezahlen Sie per Banküberweisung",
|
||||
disabled: totalAmount === 0,
|
||||
},
|
||||
{
|
||||
/*{
|
||||
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",
|
||||
description: totalAmount < 0.50 && totalAmount > 0
|
||||
? "Bezahlen Sie per Karte oder Sofortüberweisung (Mindestbetrag: 0,50 €)"
|
||||
|
||||
@@ -235,8 +235,8 @@ class SettingsTab extends Component {
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Box sx={{ p: 3 }}>
|
||||
<Paper sx={{ p: 3}}>
|
||||
<Box sx={{ p: { xs: 1, sm: 3 } }}>
|
||||
<Paper sx={{ p: { xs: 2, sm: 3 } }}>
|
||||
<Typography variant="subtitle1" fontWeight="bold" gutterBottom>
|
||||
Passwort ändern
|
||||
</Typography>
|
||||
@@ -289,7 +289,7 @@ class SettingsTab extends Component {
|
||||
|
||||
<Divider sx={{ my: 4 }} />
|
||||
|
||||
<Paper sx={{ p: 3 }}>
|
||||
<Paper sx={{ p: { xs: 2, sm: 3 } }}>
|
||||
<Typography variant="subtitle1" fontWeight="bold" gutterBottom>
|
||||
E-Mail-Adresse ändern
|
||||
</Typography>
|
||||
@@ -332,7 +332,7 @@ class SettingsTab extends Component {
|
||||
|
||||
<Divider sx={{ my: 4 }} />
|
||||
|
||||
<Paper sx={{ p: 3 }}>
|
||||
<Paper sx={{ p: { xs: 2, sm: 3 } }}>
|
||||
<Typography variant="subtitle1" fontWeight="bold" gutterBottom>
|
||||
API-Schlüssel
|
||||
</Typography>
|
||||
|
||||
@@ -1,24 +1,25 @@
|
||||
const config = {
|
||||
baseUrl: "https://seedheads.de",
|
||||
baseUrl: "https://growheads.de",
|
||||
apiBaseUrl: "",
|
||||
googleClientId: "928121624463-jbgfdlgem22scs1k9c87ucg4ffvaik6o.apps.googleusercontent.com",
|
||||
stripePublishableKey: "pk_test_51R7lltRtpe3h1vwJzIrDb5bcEigTLBHrtqj9SiPX7FOEATSuD6oJmKc8xpNp49ShpGJZb2GShHIUqj4zlSIz4olj00ipOuOAnu",
|
||||
mollieProfileKey: "pfl_AtcRTimCff",
|
||||
|
||||
// SEO and Business Information
|
||||
siteName: "SeedHeads.de",
|
||||
brandName: "SeedHeads",
|
||||
siteName: "Growheads.de",
|
||||
brandName: "GrowHeads",
|
||||
currency: "EUR",
|
||||
language: "de-DE",
|
||||
country: "DE",
|
||||
|
||||
// Shop Descriptions
|
||||
descriptions: {
|
||||
short: "SeedHeads - Online-Shop für Samen, Pflanzen und Gartenbedarf",
|
||||
long: "SeedHeads - Ihr Online-Shop für hochwertige Samen, Pflanzen und Gartenbedarf. Entdecken Sie unser großes Sortiment an Saatgut, Pflanzen und Gartenzubehör für Ihren grünen Daumen."
|
||||
short: "GrowHeads - Online-Shop für Cannanis-Samen, Stecklinge und Gartenbedarf",
|
||||
long: "GrowHeads - Ihr Online-Shop für hochwertige Samen, Pflanzen und Gartenbedarf zur Cannabis Kultivierung. Entdecken Sie unser großes Sortiment an Saatgut, Pflanzen und Gartenzubehör für Ihren grünen Daumen."
|
||||
},
|
||||
|
||||
// Keywords
|
||||
keywords: "Samen, Pflanzen, Gartenbedarf, Saatgut, Online-Shop, SeedHeads, Garten, Pflanzen kaufen",
|
||||
keywords: "Seeds, Steckling, Cannabis, Biobizz, Growheads",
|
||||
|
||||
// Shipping
|
||||
shipping: {
|
||||
|
||||
@@ -151,16 +151,16 @@ const Home = () => {
|
||||
const [rootCategories, setRootCategories] = useState(() =>
|
||||
initializeCategories()
|
||||
);
|
||||
const socket = useContext(SocketContext);
|
||||
const context = useContext(SocketContext);
|
||||
|
||||
useEffect(() => {
|
||||
// Only fetch from socket if we don't already have categories and we're in browser
|
||||
if (
|
||||
rootCategories.length === 0 &&
|
||||
socket &&
|
||||
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) {
|
||||
// Store in cache
|
||||
try {
|
||||
@@ -176,7 +176,7 @@ const Home = () => {
|
||||
}
|
||||
});
|
||||
}
|
||||
}, [socket, rootCategories.length]);
|
||||
}, [context, context?.socket?.connected, rootCategories.length]);
|
||||
|
||||
// Filter categories (excluding specific IDs)
|
||||
const filteredCategories = rootCategories.filter(
|
||||
@@ -407,7 +407,7 @@ const Home = () => {
|
||||
color: "text.primary",
|
||||
borderRadius: 2,
|
||||
overflow: "hidden",
|
||||
height: { xs: 250, sm: 300 },
|
||||
height: { xs: 150, sm: 200, md: 300 },
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
transition: "all 0.3s ease",
|
||||
@@ -468,7 +468,7 @@ const Home = () => {
|
||||
color: "text.primary",
|
||||
borderRadius: 2,
|
||||
overflow: "hidden",
|
||||
height: { xs: 250, sm: 300 },
|
||||
height: { xs: 150, sm: 200, md: 300 },
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
boxShadow: 10,
|
||||
|
||||
34
src/pages/NotFound404.js
Normal file
34
src/pages/NotFound404.js
Normal file
@@ -0,0 +1,34 @@
|
||||
import React from 'react';
|
||||
import { Typography, Box } from '@mui/material';
|
||||
import LegalPage from './LegalPage.js';
|
||||
|
||||
const NotFound404 = () => {
|
||||
const content = (
|
||||
<>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
mb: 3,
|
||||
}}
|
||||
>
|
||||
<img
|
||||
src="/assets/images/404.png"
|
||||
alt="404 - Page Not Found"
|
||||
style={{
|
||||
maxWidth: '100%',
|
||||
height: 'auto',
|
||||
maxHeight: '300px',
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
<Typography variant="body1" paragraph align="center">
|
||||
This page is no longer available.
|
||||
</Typography>
|
||||
</>
|
||||
);
|
||||
|
||||
return <LegalPage title="Page Not Found" content={content} />;
|
||||
};
|
||||
|
||||
export default NotFound404;
|
||||
@@ -167,11 +167,16 @@ const ProfilePage = (props) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<Container maxWidth="md" sx={{ py: 4 }}>
|
||||
<Paper elevation={2} sx={{ borderRadius: 2, overflow: 'hidden' }}>
|
||||
<Box sx={{ bgcolor: '#2e7d32', p: 3, color: 'white' }}>
|
||||
<Container maxWidth="md" sx={{ py: { xs: 0, sm: 4 }, px: { xs: 0, sm: 3 } }}>
|
||||
<Paper elevation={{ xs: 0, sm: 2 }} sx={{ borderRadius: { xs: 0, sm: 2 }, overflow: 'hidden' }}>
|
||||
<Box sx={{ bgcolor: '#2e7d32', p: { xs: 2, sm: 3 }, color: 'white' }}>
|
||||
<Typography variant="h5" fontWeight="bold">
|
||||
Mein Profil
|
||||
{window.innerWidth < 600 ?
|
||||
(tabValue === 0 ? 'Bestellabschluss' :
|
||||
tabValue === 1 ? 'Bestellungen' :
|
||||
tabValue === 2 ? 'Einstellungen' : 'Mein Profil')
|
||||
: 'Mein Profil'
|
||||
}
|
||||
</Typography>
|
||||
{user && (
|
||||
<Typography variant="body1" sx={{ mt: 1 }}>
|
||||
@@ -185,7 +190,11 @@ const ProfilePage = (props) => {
|
||||
value={tabValue}
|
||||
onChange={handleTabChange}
|
||||
variant="fullWidth"
|
||||
sx={{ borderBottom: 1, borderColor: 'divider' }}
|
||||
sx={{
|
||||
borderBottom: 1,
|
||||
borderColor: 'divider',
|
||||
display: { xs: 'none', sm: 'flex' }
|
||||
}}
|
||||
TabIndicatorProps={{
|
||||
style: { backgroundColor: '#2e7d32' }
|
||||
}}
|
||||
@@ -225,8 +234,8 @@ const ProfilePage = (props) => {
|
||||
|
||||
// Wrap with socket context
|
||||
const ProfilePageWithSocket = (props) => {
|
||||
const socket = useContext(SocketContext);
|
||||
return <ProfilePage {...props} socket={socket} />;
|
||||
const {socket,socketB} = useContext(SocketContext);
|
||||
return <ProfilePage {...props} socket={socket} socketB={socketB} />;
|
||||
};
|
||||
|
||||
export default ProfilePageWithSocket;
|
||||
@@ -33,10 +33,43 @@ const collectAllCategories = (categoryNode, categories = [], level = 0) => {
|
||||
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 [categories, setCategories] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const socket = useContext(SocketContext);
|
||||
// Initialize categories and loading state together
|
||||
const initialCategories = initializeCategories();
|
||||
const [categories, setCategories] = useState(initialCategories);
|
||||
const [loading, setLoading] = useState(initialCategories.length === 0);
|
||||
const context = useContext(SocketContext);
|
||||
|
||||
|
||||
const sitemapLinks = [
|
||||
{ title: 'Startseite', url: '/' },
|
||||
@@ -52,8 +85,14 @@ const Sitemap = () => {
|
||||
|
||||
useEffect(() => {
|
||||
const fetchCategories = () => {
|
||||
// Try cache first
|
||||
if (window.productCache && window.productCache['categoryTree_209']) {
|
||||
// If we already have categories from prerendering, we're done
|
||||
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 cacheAge = Date.now() - cached.timestamp;
|
||||
const tenMinutes = 10 * 60 * 1000;
|
||||
@@ -65,9 +104,9 @@ const Sitemap = () => {
|
||||
}
|
||||
}
|
||||
|
||||
// Otherwise, fetch from socket if available
|
||||
if (socket) {
|
||||
socket.emit('categoryList', { categoryId: 209 }, (response) => {
|
||||
// Otherwise, fetch from socket if available (only in browser)
|
||||
if (context && context.socket && context.socket.connected && typeof window !== "undefined") {
|
||||
context.socket.emit('categoryList', { categoryId: 209 }, (response) => {
|
||||
if (response && response.categoryTree) {
|
||||
// Store in cache
|
||||
try {
|
||||
@@ -94,7 +133,7 @@ const Sitemap = () => {
|
||||
};
|
||||
|
||||
fetchCategories();
|
||||
}, [socket]);
|
||||
}, [context, categories.length]);
|
||||
|
||||
const content = (
|
||||
<>
|
||||
|
||||
@@ -7,8 +7,10 @@ class SocketProvider extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.socket = null;
|
||||
this.socketB = null;
|
||||
this.state = {
|
||||
connected: false,
|
||||
connectedB: false,
|
||||
showPrerenderFallback: true,
|
||||
};
|
||||
}
|
||||
@@ -77,6 +79,66 @@ class SocketProvider extends Component {
|
||||
console.error("SocketProvider: Failed to reconnect");
|
||||
this.handleConnectionFailure();
|
||||
});
|
||||
|
||||
this.socketB = io(url, {
|
||||
transports: ["websocket"],
|
||||
});
|
||||
|
||||
this.socketB.on("connect", () => {
|
||||
console.log("SocketProvider: connectedB");
|
||||
//this.setState({ connectedB: true });
|
||||
});
|
||||
|
||||
this.socketB.on("disconnect", () => {
|
||||
//this.setState({ connectedB: false });
|
||||
console.log("SocketProvider: Socket disconnectedB");
|
||||
});
|
||||
|
||||
this.socketB.on("connect_error", (error) => {
|
||||
console.error("SocketProvider: Connection errorB:", error);
|
||||
});
|
||||
|
||||
this.socketB.on("reconnect_attempt", (attemptNumber) => {
|
||||
console.log(`SocketProvider: Reconnection attemptB ${attemptNumber}`);
|
||||
});
|
||||
|
||||
this.socketB.on("reconnect_failed", () => {
|
||||
console.error("SocketProvider: Failed to reconnectB");
|
||||
});
|
||||
this.socketB.waitForConnect = (timeout = 10000) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (this.socketB.connected) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
let timeoutId;
|
||||
const connectHandler = () => {
|
||||
clearTimeout(timeoutId);
|
||||
this.socketB.off("connect", connectHandler);
|
||||
this.socketB.off("connect_error", errorHandler);
|
||||
resolve();
|
||||
};
|
||||
|
||||
const errorHandler = (error) => {
|
||||
clearTimeout(timeoutId);
|
||||
this.socketB.off("connect", connectHandler);
|
||||
this.socketB.off("connect_error", errorHandler);
|
||||
reject(new Error(`Socket connection failed: ${error.message}`));
|
||||
};
|
||||
|
||||
// Set up timeout
|
||||
timeoutId = setTimeout(() => {
|
||||
this.socketB.off("connect", connectHandler);
|
||||
this.socketB.off("connect_error", errorHandler);
|
||||
reject(new Error(`Socket connection timeout after ${timeout}ms`));
|
||||
}, timeout);
|
||||
|
||||
// Add event listeners
|
||||
this.socketB.on("connect", connectHandler);
|
||||
this.socketB.on("connect_error", errorHandler);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
handleConnectionFailure() {
|
||||
@@ -96,6 +158,10 @@ class SocketProvider extends Component {
|
||||
console.log("SocketProvider: Disconnecting socket");
|
||||
this.socket.disconnect();
|
||||
}
|
||||
if (this.socketB) {
|
||||
console.log("SocketProvider: Disconnecting socketB");
|
||||
this.socketB.disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
@@ -104,7 +170,7 @@ class SocketProvider extends Component {
|
||||
window.__PRERENDER_FALLBACK__;
|
||||
|
||||
return (
|
||||
<SocketContext.Provider value={this.socket}>
|
||||
<SocketContext.Provider value={{socket:this.socket,socketB:this.socketB}}>
|
||||
{/* Always render children but control visibility */}
|
||||
<div style={{ display: this.state.connected ? 'block' : 'none' }}>
|
||||
{this.props.children}
|
||||
|
||||
@@ -314,7 +314,11 @@ export default {
|
||||
hot: true,
|
||||
port: 9500,
|
||||
open: false,
|
||||
historyApiFallback: true,
|
||||
historyApiFallback: {
|
||||
index: '/index.html',
|
||||
disableDotRule: true,
|
||||
htmlAcceptHeaders: ['text/html', 'application/xhtml+xml']
|
||||
},
|
||||
client: {
|
||||
logging: 'verbose',
|
||||
overlay: {
|
||||
|
||||
Reference in New Issue
Block a user