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/index.prerender.html
|
||||||
/public/Konfigurator
|
/public/Konfigurator
|
||||||
/public/profile
|
/public/profile
|
||||||
|
/public/404
|
||||||
|
|
||||||
/public/products.xml
|
/public/products.xml
|
||||||
/public/llms*
|
/public/llms*
|
||||||
@@ -45,7 +46,6 @@ yarn-debug.log*
|
|||||||
yarn-error.log*
|
yarn-error.log*
|
||||||
|
|
||||||
# Editor directories and files
|
# Editor directories and files
|
||||||
.vscode
|
|
||||||
.idea
|
.idea
|
||||||
*.suo
|
*.suo
|
||||||
*.ntvs*
|
*.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",
|
"name": "reactshop",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"license": "ISC",
|
"license": "0BSD",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@emotion/react": "^11.14.0",
|
"@emotion/react": "^11.14.0",
|
||||||
"@emotion/styled": "^11.14.0",
|
"@emotion/styled": "^11.14.0",
|
||||||
@@ -106,9 +106,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/compat-data": {
|
"node_modules/@babel/compat-data": {
|
||||||
"version": "7.27.5",
|
"version": "7.28.0",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.27.5.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.0.tgz",
|
||||||
"integrity": "sha512-KiRAp/VoJaWkkte84TvUd9qjdbZAdiqyvMxrGl1N6vzFogKmaLgoM3L1kgtLicp2HP5fBJS8JrZKLVIZGVJAVg==",
|
"integrity": "sha512-60X7qkglvrap8mn1lh2ebxXdZYtUcpd7gsmy9kLaBJ4i/WdY8PqTSdxyA8qraikqKQK5C1KRBKXqznrVapyNaw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -116,22 +116,22 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/core": {
|
"node_modules/@babel/core": {
|
||||||
"version": "7.27.4",
|
"version": "7.28.0",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.27.4.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.0.tgz",
|
||||||
"integrity": "sha512-bXYxrXFubeYdvB0NhD/NBB3Qi6aZeV20GOWVI47t2dkecCEoneR4NPVcb7abpXDEvejgrUfFtG6vG/zxAKmg+g==",
|
"integrity": "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ampproject/remapping": "^2.2.0",
|
"@ampproject/remapping": "^2.2.0",
|
||||||
"@babel/code-frame": "^7.27.1",
|
"@babel/code-frame": "^7.27.1",
|
||||||
"@babel/generator": "^7.27.3",
|
"@babel/generator": "^7.28.0",
|
||||||
"@babel/helper-compilation-targets": "^7.27.2",
|
"@babel/helper-compilation-targets": "^7.27.2",
|
||||||
"@babel/helper-module-transforms": "^7.27.3",
|
"@babel/helper-module-transforms": "^7.27.3",
|
||||||
"@babel/helpers": "^7.27.4",
|
"@babel/helpers": "^7.27.6",
|
||||||
"@babel/parser": "^7.27.4",
|
"@babel/parser": "^7.28.0",
|
||||||
"@babel/template": "^7.27.2",
|
"@babel/template": "^7.27.2",
|
||||||
"@babel/traverse": "^7.27.4",
|
"@babel/traverse": "^7.28.0",
|
||||||
"@babel/types": "^7.27.3",
|
"@babel/types": "^7.28.0",
|
||||||
"convert-source-map": "^2.0.0",
|
"convert-source-map": "^2.0.0",
|
||||||
"debug": "^4.1.0",
|
"debug": "^4.1.0",
|
||||||
"gensync": "^1.0.0-beta.2",
|
"gensync": "^1.0.0-beta.2",
|
||||||
@@ -147,9 +147,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/eslint-parser": {
|
"node_modules/@babel/eslint-parser": {
|
||||||
"version": "7.27.5",
|
"version": "7.28.0",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/eslint-parser/-/eslint-parser-7.27.5.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/eslint-parser/-/eslint-parser-7.28.0.tgz",
|
||||||
"integrity": "sha512-HLkYQfRICudzcOtjGwkPvGc5nF1b4ljLZh1IRDj50lRZ718NAKVgQpIAUX8bfg6u/yuSKY3L7E0YzIV+OxrB8Q==",
|
"integrity": "sha512-N4ntErOlKvcbTt01rr5wj3y55xnIdx1ymrfIr8C2WnM1Y9glFgWaGDEULJIazOX3XM9NRzhfJ6zZnQ1sBNWU+w==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -166,15 +166,15 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/generator": {
|
"node_modules/@babel/generator": {
|
||||||
"version": "7.27.5",
|
"version": "7.28.0",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.27.5.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.0.tgz",
|
||||||
"integrity": "sha512-ZGhA37l0e/g2s1Cnzdix0O3aLYm66eF8aufiVteOgnwxgnRP8GoyMj7VWsgWnQbVKXyge7hqrFh2K2TQM6t1Hw==",
|
"integrity": "sha512-lJjzvrbEeWrhB4P3QBsH7tey117PjLZnDbLiQEKjQ/fNJTjuq4HSqgFA+UNSwZT8D7dxxbnuSBMsa1lrWzKlQg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/parser": "^7.27.5",
|
"@babel/parser": "^7.28.0",
|
||||||
"@babel/types": "^7.27.3",
|
"@babel/types": "^7.28.0",
|
||||||
"@jridgewell/gen-mapping": "^0.3.5",
|
"@jridgewell/gen-mapping": "^0.3.12",
|
||||||
"@jridgewell/trace-mapping": "^0.3.25",
|
"@jridgewell/trace-mapping": "^0.3.28",
|
||||||
"jsesc": "^3.0.2"
|
"jsesc": "^3.0.2"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -252,22 +252,31 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/helper-define-polyfill-provider": {
|
"node_modules/@babel/helper-define-polyfill-provider": {
|
||||||
"version": "0.6.4",
|
"version": "0.6.5",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.4.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.5.tgz",
|
||||||
"integrity": "sha512-jljfR1rGnXXNWnmQg2K3+bvhkxB51Rl32QRaOTuwwjviGrHzIbSc8+x9CpraDtbT7mfyjXObULP4w/adunNwAw==",
|
"integrity": "sha512-uJnGFcPsWQK8fvjgGP5LZUZZsYGIoPeRjSF5PGwrelYgq7Q15/Ft9NGFp1zglwgIv//W0uG4BevRuSJRyylZPg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/helper-compilation-targets": "^7.22.6",
|
"@babel/helper-compilation-targets": "^7.27.2",
|
||||||
"@babel/helper-plugin-utils": "^7.22.5",
|
"@babel/helper-plugin-utils": "^7.27.1",
|
||||||
"debug": "^4.1.1",
|
"debug": "^4.4.1",
|
||||||
"lodash.debounce": "^4.0.8",
|
"lodash.debounce": "^4.0.8",
|
||||||
"resolve": "^1.14.2"
|
"resolve": "^1.22.10"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0"
|
"@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@babel/helper-globals": {
|
||||||
|
"version": "7.28.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz",
|
||||||
|
"integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.9.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@babel/helper-member-expression-to-functions": {
|
"node_modules/@babel/helper-member-expression-to-functions": {
|
||||||
"version": "7.27.1",
|
"version": "7.27.1",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.27.1.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.27.1.tgz",
|
||||||
@@ -444,12 +453,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/parser": {
|
"node_modules/@babel/parser": {
|
||||||
"version": "7.27.5",
|
"version": "7.28.0",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.5.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.0.tgz",
|
||||||
"integrity": "sha512-OsQd175SxWkGlzbny8J3K8TnnDD0N3lrIUtB92xwyRpzaenGZhxDvxN/JgU00U3CDZNj9tPuDJ5H0WS4Nt3vKg==",
|
"integrity": "sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/types": "^7.27.3"
|
"@babel/types": "^7.28.0"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"parser": "bin/babel-parser.js"
|
"parser": "bin/babel-parser.js"
|
||||||
@@ -637,15 +646,15 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/plugin-transform-async-generator-functions": {
|
"node_modules/@babel/plugin-transform-async-generator-functions": {
|
||||||
"version": "7.27.1",
|
"version": "7.28.0",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.27.1.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.28.0.tgz",
|
||||||
"integrity": "sha512-eST9RrwlpaoJBDHShc+DS2SG4ATTi2MYNb4OxYkf3n+7eb49LWpnS+HSpVfW4x927qQwgk8A2hGNVaajAEw0EA==",
|
"integrity": "sha512-BEOdvX4+M765icNPZeidyADIvQ1m1gmunXufXxvRESy/jNNyfovIqUyE7MVgGBjWktCoJlzvFA1To2O4ymIO3Q==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/helper-plugin-utils": "^7.27.1",
|
"@babel/helper-plugin-utils": "^7.27.1",
|
||||||
"@babel/helper-remap-async-to-generator": "^7.27.1",
|
"@babel/helper-remap-async-to-generator": "^7.27.1",
|
||||||
"@babel/traverse": "^7.27.1"
|
"@babel/traverse": "^7.28.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6.9.0"
|
"node": ">=6.9.0"
|
||||||
@@ -689,9 +698,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/plugin-transform-block-scoping": {
|
"node_modules/@babel/plugin-transform-block-scoping": {
|
||||||
"version": "7.27.5",
|
"version": "7.28.0",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.27.5.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.28.0.tgz",
|
||||||
"integrity": "sha512-JF6uE2s67f0y2RZcm2kpAUEbD50vH62TyWVebxwHAlbSdM49VqPz8t4a1uIjp4NIOIZ4xzLfjY5emt/RCyC7TQ==",
|
"integrity": "sha512-gKKnwjpdx5sER/wl0WN0efUBFzF/56YZO0RJrSYP4CljXnP31ByY7fol89AzomdlLNzI36AvOTmYHsnZTCkq8Q==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -739,18 +748,18 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/plugin-transform-classes": {
|
"node_modules/@babel/plugin-transform-classes": {
|
||||||
"version": "7.27.1",
|
"version": "7.28.0",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.27.1.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.28.0.tgz",
|
||||||
"integrity": "sha512-7iLhfFAubmpeJe/Wo2TVuDrykh/zlWXLzPNdL0Jqn/Xu8R3QQ8h9ff8FQoISZOsw74/HFqFI7NX63HN7QFIHKA==",
|
"integrity": "sha512-IjM1IoJNw72AZFlj33Cu8X0q2XK/6AaVC3jQu+cgQ5lThWD5ajnuUAml80dqRmOhmPkTH8uAwnpMu9Rvj0LTRA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/helper-annotate-as-pure": "^7.27.1",
|
"@babel/helper-annotate-as-pure": "^7.27.3",
|
||||||
"@babel/helper-compilation-targets": "^7.27.1",
|
"@babel/helper-compilation-targets": "^7.27.2",
|
||||||
|
"@babel/helper-globals": "^7.28.0",
|
||||||
"@babel/helper-plugin-utils": "^7.27.1",
|
"@babel/helper-plugin-utils": "^7.27.1",
|
||||||
"@babel/helper-replace-supers": "^7.27.1",
|
"@babel/helper-replace-supers": "^7.27.1",
|
||||||
"@babel/traverse": "^7.27.1",
|
"@babel/traverse": "^7.28.0"
|
||||||
"globals": "^11.1.0"
|
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6.9.0"
|
"node": ">=6.9.0"
|
||||||
@@ -759,16 +768,6 @@
|
|||||||
"@babel/core": "^7.0.0-0"
|
"@babel/core": "^7.0.0-0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/plugin-transform-classes/node_modules/globals": {
|
|
||||||
"version": "11.12.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz",
|
|
||||||
"integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=4"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@babel/plugin-transform-computed-properties": {
|
"node_modules/@babel/plugin-transform-computed-properties": {
|
||||||
"version": "7.27.1",
|
"version": "7.27.1",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.27.1.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.27.1.tgz",
|
||||||
@@ -787,13 +786,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/plugin-transform-destructuring": {
|
"node_modules/@babel/plugin-transform-destructuring": {
|
||||||
"version": "7.27.3",
|
"version": "7.28.0",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.27.3.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.28.0.tgz",
|
||||||
"integrity": "sha512-s4Jrok82JpiaIprtY2nHsYmrThKvvwgHwjgd7UMiYhZaN0asdXNLr0y+NjTfkA7SyQE5i2Fb7eawUOZmLvyqOA==",
|
"integrity": "sha512-v1nrSMBiKcodhsyJ4Gf+Z0U/yawmJDBOTpEB3mcQY52r9RIyPneGyAS/yM6seP/8I+mWI3elOMtT5dB8GJVs+A==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/helper-plugin-utils": "^7.27.1"
|
"@babel/helper-plugin-utils": "^7.27.1",
|
||||||
|
"@babel/traverse": "^7.28.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6.9.0"
|
"node": ">=6.9.0"
|
||||||
@@ -868,6 +868,23 @@
|
|||||||
"@babel/core": "^7.0.0-0"
|
"@babel/core": "^7.0.0-0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@babel/plugin-transform-explicit-resource-management": {
|
||||||
|
"version": "7.28.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-explicit-resource-management/-/plugin-transform-explicit-resource-management-7.28.0.tgz",
|
||||||
|
"integrity": "sha512-K8nhUcn3f6iB+P3gwCv/no7OdzOZQcKchW6N389V6PD8NUWKZHzndOd9sPDVbMoBsbmjMqlB4L9fm+fEFNVlwQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/helper-plugin-utils": "^7.27.1",
|
||||||
|
"@babel/plugin-transform-destructuring": "^7.28.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.9.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@babel/core": "^7.0.0-0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@babel/plugin-transform-exponentiation-operator": {
|
"node_modules/@babel/plugin-transform-exponentiation-operator": {
|
||||||
"version": "7.27.1",
|
"version": "7.27.1",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.27.1.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.27.1.tgz",
|
||||||
@@ -1135,16 +1152,17 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/plugin-transform-object-rest-spread": {
|
"node_modules/@babel/plugin-transform-object-rest-spread": {
|
||||||
"version": "7.27.3",
|
"version": "7.28.0",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.27.3.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.28.0.tgz",
|
||||||
"integrity": "sha512-7ZZtznF9g4l2JCImCo5LNKFHB5eXnN39lLtLY5Tg+VkR0jwOt7TBciMckuiQIOIW7L5tkQOCh3bVGYeXgMx52Q==",
|
"integrity": "sha512-9VNGikXxzu5eCiQjdE4IZn8sb9q7Xsk5EXLDBKUYg1e/Tve8/05+KJEtcxGxAgCY5t/BpKQM+JEL/yT4tvgiUA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/helper-compilation-targets": "^7.27.2",
|
"@babel/helper-compilation-targets": "^7.27.2",
|
||||||
"@babel/helper-plugin-utils": "^7.27.1",
|
"@babel/helper-plugin-utils": "^7.27.1",
|
||||||
"@babel/plugin-transform-destructuring": "^7.27.3",
|
"@babel/plugin-transform-destructuring": "^7.28.0",
|
||||||
"@babel/plugin-transform-parameters": "^7.27.1"
|
"@babel/plugin-transform-parameters": "^7.27.7",
|
||||||
|
"@babel/traverse": "^7.28.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6.9.0"
|
"node": ">=6.9.0"
|
||||||
@@ -1204,9 +1222,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/plugin-transform-parameters": {
|
"node_modules/@babel/plugin-transform-parameters": {
|
||||||
"version": "7.27.1",
|
"version": "7.27.7",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.27.1.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.27.7.tgz",
|
||||||
"integrity": "sha512-018KRk76HWKeZ5l4oTj2zPpSh+NbGdt0st5S6x0pga6HgrjBOJb24mMDHorFopOOd6YHkLgOZ+zaCjZGPO4aKg==",
|
"integrity": "sha512-qBkYTYCb76RRxUM6CcZA5KRu8K4SM8ajzVeUgVdMVO9NN9uI/GaVmBg/WKJJGnNokV9SY8FxNOVWGXzqzUidBg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -1340,9 +1358,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/plugin-transform-regenerator": {
|
"node_modules/@babel/plugin-transform-regenerator": {
|
||||||
"version": "7.27.5",
|
"version": "7.28.0",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.27.5.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.28.0.tgz",
|
||||||
"integrity": "sha512-uhB8yHerfe3MWnuLAhEbeQ4afVoqv8BQsPqrTv7e/jZ9y00kJL6l9a/f4OWaKxotmjzewfEyXE1vgDJenkQ2/Q==",
|
"integrity": "sha512-LOAozRVbqxEVjSKfhGnuLoE4Kz4Oc5UJzuvFUhSsQzdCdaAQu06mG8zDv2GFSerM62nImUZ7K92vxnQcLSDlCQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -1537,13 +1555,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/preset-env": {
|
"node_modules/@babel/preset-env": {
|
||||||
"version": "7.27.2",
|
"version": "7.28.0",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.27.2.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.28.0.tgz",
|
||||||
"integrity": "sha512-Ma4zSuYSlGNRlCLO+EAzLnCmJK2vdstgv+n7aUP+/IKZrOfWHOJVdSJtuub8RzHTj3ahD37k5OKJWvzf16TQyQ==",
|
"integrity": "sha512-VmaxeGOwuDqzLl5JUkIRM1X2Qu2uKGxHEQWh+cvvbl7JuJRgKGJSfsEF/bUaxFhJl/XAyxBe7q7qSuTbKFuCyg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/compat-data": "^7.27.2",
|
"@babel/compat-data": "^7.28.0",
|
||||||
"@babel/helper-compilation-targets": "^7.27.2",
|
"@babel/helper-compilation-targets": "^7.27.2",
|
||||||
"@babel/helper-plugin-utils": "^7.27.1",
|
"@babel/helper-plugin-utils": "^7.27.1",
|
||||||
"@babel/helper-validator-option": "^7.27.1",
|
"@babel/helper-validator-option": "^7.27.1",
|
||||||
@@ -1557,19 +1575,20 @@
|
|||||||
"@babel/plugin-syntax-import-attributes": "^7.27.1",
|
"@babel/plugin-syntax-import-attributes": "^7.27.1",
|
||||||
"@babel/plugin-syntax-unicode-sets-regex": "^7.18.6",
|
"@babel/plugin-syntax-unicode-sets-regex": "^7.18.6",
|
||||||
"@babel/plugin-transform-arrow-functions": "^7.27.1",
|
"@babel/plugin-transform-arrow-functions": "^7.27.1",
|
||||||
"@babel/plugin-transform-async-generator-functions": "^7.27.1",
|
"@babel/plugin-transform-async-generator-functions": "^7.28.0",
|
||||||
"@babel/plugin-transform-async-to-generator": "^7.27.1",
|
"@babel/plugin-transform-async-to-generator": "^7.27.1",
|
||||||
"@babel/plugin-transform-block-scoped-functions": "^7.27.1",
|
"@babel/plugin-transform-block-scoped-functions": "^7.27.1",
|
||||||
"@babel/plugin-transform-block-scoping": "^7.27.1",
|
"@babel/plugin-transform-block-scoping": "^7.28.0",
|
||||||
"@babel/plugin-transform-class-properties": "^7.27.1",
|
"@babel/plugin-transform-class-properties": "^7.27.1",
|
||||||
"@babel/plugin-transform-class-static-block": "^7.27.1",
|
"@babel/plugin-transform-class-static-block": "^7.27.1",
|
||||||
"@babel/plugin-transform-classes": "^7.27.1",
|
"@babel/plugin-transform-classes": "^7.28.0",
|
||||||
"@babel/plugin-transform-computed-properties": "^7.27.1",
|
"@babel/plugin-transform-computed-properties": "^7.27.1",
|
||||||
"@babel/plugin-transform-destructuring": "^7.27.1",
|
"@babel/plugin-transform-destructuring": "^7.28.0",
|
||||||
"@babel/plugin-transform-dotall-regex": "^7.27.1",
|
"@babel/plugin-transform-dotall-regex": "^7.27.1",
|
||||||
"@babel/plugin-transform-duplicate-keys": "^7.27.1",
|
"@babel/plugin-transform-duplicate-keys": "^7.27.1",
|
||||||
"@babel/plugin-transform-duplicate-named-capturing-groups-regex": "^7.27.1",
|
"@babel/plugin-transform-duplicate-named-capturing-groups-regex": "^7.27.1",
|
||||||
"@babel/plugin-transform-dynamic-import": "^7.27.1",
|
"@babel/plugin-transform-dynamic-import": "^7.27.1",
|
||||||
|
"@babel/plugin-transform-explicit-resource-management": "^7.28.0",
|
||||||
"@babel/plugin-transform-exponentiation-operator": "^7.27.1",
|
"@babel/plugin-transform-exponentiation-operator": "^7.27.1",
|
||||||
"@babel/plugin-transform-export-namespace-from": "^7.27.1",
|
"@babel/plugin-transform-export-namespace-from": "^7.27.1",
|
||||||
"@babel/plugin-transform-for-of": "^7.27.1",
|
"@babel/plugin-transform-for-of": "^7.27.1",
|
||||||
@@ -1586,15 +1605,15 @@
|
|||||||
"@babel/plugin-transform-new-target": "^7.27.1",
|
"@babel/plugin-transform-new-target": "^7.27.1",
|
||||||
"@babel/plugin-transform-nullish-coalescing-operator": "^7.27.1",
|
"@babel/plugin-transform-nullish-coalescing-operator": "^7.27.1",
|
||||||
"@babel/plugin-transform-numeric-separator": "^7.27.1",
|
"@babel/plugin-transform-numeric-separator": "^7.27.1",
|
||||||
"@babel/plugin-transform-object-rest-spread": "^7.27.2",
|
"@babel/plugin-transform-object-rest-spread": "^7.28.0",
|
||||||
"@babel/plugin-transform-object-super": "^7.27.1",
|
"@babel/plugin-transform-object-super": "^7.27.1",
|
||||||
"@babel/plugin-transform-optional-catch-binding": "^7.27.1",
|
"@babel/plugin-transform-optional-catch-binding": "^7.27.1",
|
||||||
"@babel/plugin-transform-optional-chaining": "^7.27.1",
|
"@babel/plugin-transform-optional-chaining": "^7.27.1",
|
||||||
"@babel/plugin-transform-parameters": "^7.27.1",
|
"@babel/plugin-transform-parameters": "^7.27.7",
|
||||||
"@babel/plugin-transform-private-methods": "^7.27.1",
|
"@babel/plugin-transform-private-methods": "^7.27.1",
|
||||||
"@babel/plugin-transform-private-property-in-object": "^7.27.1",
|
"@babel/plugin-transform-private-property-in-object": "^7.27.1",
|
||||||
"@babel/plugin-transform-property-literals": "^7.27.1",
|
"@babel/plugin-transform-property-literals": "^7.27.1",
|
||||||
"@babel/plugin-transform-regenerator": "^7.27.1",
|
"@babel/plugin-transform-regenerator": "^7.28.0",
|
||||||
"@babel/plugin-transform-regexp-modifiers": "^7.27.1",
|
"@babel/plugin-transform-regexp-modifiers": "^7.27.1",
|
||||||
"@babel/plugin-transform-reserved-words": "^7.27.1",
|
"@babel/plugin-transform-reserved-words": "^7.27.1",
|
||||||
"@babel/plugin-transform-shorthand-properties": "^7.27.1",
|
"@babel/plugin-transform-shorthand-properties": "^7.27.1",
|
||||||
@@ -1607,10 +1626,10 @@
|
|||||||
"@babel/plugin-transform-unicode-regex": "^7.27.1",
|
"@babel/plugin-transform-unicode-regex": "^7.27.1",
|
||||||
"@babel/plugin-transform-unicode-sets-regex": "^7.27.1",
|
"@babel/plugin-transform-unicode-sets-regex": "^7.27.1",
|
||||||
"@babel/preset-modules": "0.1.6-no-external-plugins",
|
"@babel/preset-modules": "0.1.6-no-external-plugins",
|
||||||
"babel-plugin-polyfill-corejs2": "^0.4.10",
|
"babel-plugin-polyfill-corejs2": "^0.4.14",
|
||||||
"babel-plugin-polyfill-corejs3": "^0.11.0",
|
"babel-plugin-polyfill-corejs3": "^0.13.0",
|
||||||
"babel-plugin-polyfill-regenerator": "^0.6.1",
|
"babel-plugin-polyfill-regenerator": "^0.6.5",
|
||||||
"core-js-compat": "^3.40.0",
|
"core-js-compat": "^3.43.0",
|
||||||
"semver": "^6.3.1"
|
"semver": "^6.3.1"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -1700,36 +1719,27 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/traverse": {
|
"node_modules/@babel/traverse": {
|
||||||
"version": "7.27.4",
|
"version": "7.28.0",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.27.4.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.0.tgz",
|
||||||
"integrity": "sha512-oNcu2QbHqts9BtOWJosOVJapWjBDSxGCpFvikNR5TGDYDQf3JwpIoMzIKrvfoti93cLfPJEG4tH9SPVeyCGgdA==",
|
"integrity": "sha512-mGe7UK5wWyh0bKRfupsUchrQGqvDbZDbKJw+kcRGSmdHVYrv+ltd0pnpDTVpiTqnaBru9iEvA8pz8W46v0Amwg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/code-frame": "^7.27.1",
|
"@babel/code-frame": "^7.27.1",
|
||||||
"@babel/generator": "^7.27.3",
|
"@babel/generator": "^7.28.0",
|
||||||
"@babel/parser": "^7.27.4",
|
"@babel/helper-globals": "^7.28.0",
|
||||||
|
"@babel/parser": "^7.28.0",
|
||||||
"@babel/template": "^7.27.2",
|
"@babel/template": "^7.27.2",
|
||||||
"@babel/types": "^7.27.3",
|
"@babel/types": "^7.28.0",
|
||||||
"debug": "^4.3.1",
|
"debug": "^4.3.1"
|
||||||
"globals": "^11.1.0"
|
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6.9.0"
|
"node": ">=6.9.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/traverse/node_modules/globals": {
|
|
||||||
"version": "11.12.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz",
|
|
||||||
"integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=4"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@babel/types": {
|
"node_modules/@babel/types": {
|
||||||
"version": "7.27.6",
|
"version": "7.28.0",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.6.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.0.tgz",
|
||||||
"integrity": "sha512-ETyHEk2VHHvl9b9jZP5IHPavHYk57EhanlRRuae9XCpb/j5bDCbPPMOBfCWhnl/7EDJz0jEMCi/RhccCE8r1+Q==",
|
"integrity": "sha512-jYnje+JyZG5YThjHiF28oT4SIZLnYOcSBb6+SDaFIyzDVSkXQmQQYclJ2R+YxcdmK0AX6x1E5OQNtuh3jHDrUg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/helper-string-parser": "^7.27.1",
|
"@babel/helper-string-parser": "^7.27.1",
|
||||||
@@ -2708,17 +2718,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@jridgewell/gen-mapping": {
|
"node_modules/@jridgewell/gen-mapping": {
|
||||||
"version": "0.3.8",
|
"version": "0.3.12",
|
||||||
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz",
|
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.12.tgz",
|
||||||
"integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==",
|
"integrity": "sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@jridgewell/set-array": "^1.2.1",
|
"@jridgewell/sourcemap-codec": "^1.5.0",
|
||||||
"@jridgewell/sourcemap-codec": "^1.4.10",
|
|
||||||
"@jridgewell/trace-mapping": "^0.3.24"
|
"@jridgewell/trace-mapping": "^0.3.24"
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=6.0.0"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@jridgewell/resolve-uri": {
|
"node_modules/@jridgewell/resolve-uri": {
|
||||||
@@ -2730,15 +2736,6 @@
|
|||||||
"node": ">=6.0.0"
|
"node": ">=6.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@jridgewell/set-array": {
|
|
||||||
"version": "1.2.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz",
|
|
||||||
"integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=6.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@jridgewell/source-map": {
|
"node_modules/@jridgewell/source-map": {
|
||||||
"version": "0.3.6",
|
"version": "0.3.6",
|
||||||
"resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.6.tgz",
|
"resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.6.tgz",
|
||||||
@@ -2757,9 +2754,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@jridgewell/trace-mapping": {
|
"node_modules/@jridgewell/trace-mapping": {
|
||||||
"version": "0.3.25",
|
"version": "0.3.29",
|
||||||
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz",
|
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.29.tgz",
|
||||||
"integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==",
|
"integrity": "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@jridgewell/resolve-uri": "^3.1.0",
|
"@jridgewell/resolve-uri": "^3.1.0",
|
||||||
@@ -3118,9 +3115,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@pmmmwh/react-refresh-webpack-plugin": {
|
"node_modules/@pmmmwh/react-refresh-webpack-plugin": {
|
||||||
"version": "0.6.0",
|
"version": "0.6.1",
|
||||||
"resolved": "https://registry.npmjs.org/@pmmmwh/react-refresh-webpack-plugin/-/react-refresh-webpack-plugin-0.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/@pmmmwh/react-refresh-webpack-plugin/-/react-refresh-webpack-plugin-0.6.1.tgz",
|
||||||
"integrity": "sha512-AAc+QWfZ1KQ/e1C6OHWVlxU+ks6zFGOA44IJUlvju7RlDS8nsX6poPFOIlsg/rTofO9vKov12+WCjMhKkRKD5g==",
|
"integrity": "sha512-95DXXJxNkpYu+sqmpDp7vbw9JCyiNpHuCsvuMuOgVFrKQlwEIn9Y1+NNIQJq+zFL+eWyxw6htthB5CtdwJupNA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -4188,14 +4185,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/babel-plugin-polyfill-corejs2": {
|
"node_modules/babel-plugin-polyfill-corejs2": {
|
||||||
"version": "0.4.13",
|
"version": "0.4.14",
|
||||||
"resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.13.tgz",
|
"resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.14.tgz",
|
||||||
"integrity": "sha512-3sX/eOms8kd3q2KZ6DAhKPc0dgm525Gqq5NtWKZ7QYYZEv57OQ54KtblzJzH1lQF/eQxO8KjWGIK9IPUJNus5g==",
|
"integrity": "sha512-Co2Y9wX854ts6U8gAAPXfn0GmAyctHuK8n0Yhfjd6t30g7yvKjspvvOo9yG+z52PZRgFErt7Ka2pYnXCjLKEpg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/compat-data": "^7.22.6",
|
"@babel/compat-data": "^7.27.7",
|
||||||
"@babel/helper-define-polyfill-provider": "^0.6.4",
|
"@babel/helper-define-polyfill-provider": "^0.6.5",
|
||||||
"semver": "^6.3.1"
|
"semver": "^6.3.1"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
@@ -4203,27 +4200,27 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/babel-plugin-polyfill-corejs3": {
|
"node_modules/babel-plugin-polyfill-corejs3": {
|
||||||
"version": "0.11.1",
|
"version": "0.13.0",
|
||||||
"resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.11.1.tgz",
|
"resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.13.0.tgz",
|
||||||
"integrity": "sha512-yGCqvBT4rwMczo28xkH/noxJ6MZ4nJfkVYdoDaC/utLtWrXxv27HVrzAeSbqR8SxDsp46n0YF47EbHoixy6rXQ==",
|
"integrity": "sha512-U+GNwMdSFgzVmfhNm8GJUX88AadB3uo9KpJqS3FaqNIPKgySuvMb+bHPsOmmuWyIcuqZj/pzt1RUIUZns4y2+A==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/helper-define-polyfill-provider": "^0.6.3",
|
"@babel/helper-define-polyfill-provider": "^0.6.5",
|
||||||
"core-js-compat": "^3.40.0"
|
"core-js-compat": "^3.43.0"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0"
|
"@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/babel-plugin-polyfill-regenerator": {
|
"node_modules/babel-plugin-polyfill-regenerator": {
|
||||||
"version": "0.6.4",
|
"version": "0.6.5",
|
||||||
"resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.4.tgz",
|
"resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.5.tgz",
|
||||||
"integrity": "sha512-7gD3pRadPrbjhjLyxebmx/WrFYcuSjZ0XbdUujQMZ/fcE9oeewk2U/7PCvez84UeuK3oSjmPZ0Ch0dlupQvGzw==",
|
"integrity": "sha512-ISqQ2frbiNU9vIJkzg7dlPpznPZ4jOiUQ1uSmB0fEHeowtN3COYRsXr/xexn64NpU13P06jc/L5TgiJXOgrbEg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/helper-define-polyfill-provider": "^0.6.4"
|
"@babel/helper-define-polyfill-provider": "^0.6.5"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0"
|
"@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0"
|
||||||
@@ -4317,9 +4314,9 @@
|
|||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
"node_modules/brace-expansion": {
|
"node_modules/brace-expansion": {
|
||||||
"version": "1.1.11",
|
"version": "1.1.12",
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
|
||||||
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
|
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -4341,9 +4338,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/browserslist": {
|
"node_modules/browserslist": {
|
||||||
"version": "4.24.4",
|
"version": "4.25.1",
|
||||||
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.4.tgz",
|
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.1.tgz",
|
||||||
"integrity": "sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==",
|
"integrity": "sha512-KGj0KoOMXLpSNkkEI6Z6mShmQy0bc1I+T7K9N81k4WWMrfz+6fQ6es80B/YLAeRoKvjYE1YSHHOW1qe9xIVzHw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
@@ -4361,10 +4358,10 @@
|
|||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"caniuse-lite": "^1.0.30001688",
|
"caniuse-lite": "^1.0.30001726",
|
||||||
"electron-to-chromium": "^1.5.73",
|
"electron-to-chromium": "^1.5.173",
|
||||||
"node-releases": "^2.0.19",
|
"node-releases": "^2.0.19",
|
||||||
"update-browserslist-db": "^1.1.1"
|
"update-browserslist-db": "^1.1.3"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"browserslist": "cli.js"
|
"browserslist": "cli.js"
|
||||||
@@ -4477,9 +4474,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/caniuse-lite": {
|
"node_modules/caniuse-lite": {
|
||||||
"version": "1.0.30001715",
|
"version": "1.0.30001726",
|
||||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001715.tgz",
|
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001726.tgz",
|
||||||
"integrity": "sha512-7ptkFGMm2OAOgvZpwgA4yjQ5SQbrNVGdRjzH0pBdy1Fasvcr+KAeECmbCAECzTuDuoX0FCY8KzUxjf9+9kfZEw==",
|
"integrity": "sha512-VQAUIUzBiZ/UnlM28fSp2CRF3ivUn1BWEvxMcVTNwpw91Py1pGbPIyIKtd+tzct9C3ouceCVdGAXxZOpZAsgdw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
@@ -4815,13 +4812,13 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/core-js-compat": {
|
"node_modules/core-js-compat": {
|
||||||
"version": "3.41.0",
|
"version": "3.43.0",
|
||||||
"resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.41.0.tgz",
|
"resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.43.0.tgz",
|
||||||
"integrity": "sha512-RFsU9LySVue9RTwdDVX/T0e2Y6jRYWXERKElIjpuEOEnxaXffI0X7RUwVzfYLfzuLXSNJDYoRYUAmRUcyln20A==",
|
"integrity": "sha512-2GML2ZsCc5LR7hZYz4AXmjQw8zuy2T//2QntwdnpuYI7jteT6GVYJL7F6C2C57R7gSYrcqVW3lAALefdbhBLDA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"browserslist": "^4.24.4"
|
"browserslist": "^4.25.0"
|
||||||
},
|
},
|
||||||
"funding": {
|
"funding": {
|
||||||
"type": "opencollective",
|
"type": "opencollective",
|
||||||
@@ -5141,9 +5138,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/debug": {
|
"node_modules/debug": {
|
||||||
"version": "4.4.0",
|
"version": "4.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
|
||||||
"integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==",
|
"integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ms": "^2.1.3"
|
"ms": "^2.1.3"
|
||||||
@@ -5478,9 +5475,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/electron-to-chromium": {
|
"node_modules/electron-to-chromium": {
|
||||||
"version": "1.5.139",
|
"version": "1.5.179",
|
||||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.139.tgz",
|
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.179.tgz",
|
||||||
"integrity": "sha512-GGnRYOTdN5LYpwbIr0rwP/ZHOQSvAF6TG0LSzp28uCBb9JiXHJGmaaKw29qjNJc5bGnnp6kXJqRnGMQoELwi5w==",
|
"integrity": "sha512-UWKi/EbBopgfFsc5k61wFpV7WrnnSlSzW/e2XcBmS6qKYTivZlLtoll5/rdqRTxGglGHkmkW0j0pFNJG10EUIQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -4,8 +4,8 @@
|
|||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "webpack serve --progress --mode development --no-open",
|
"start": "cross-env NODE_OPTIONS=\"--no-deprecation\" webpack serve --progress --mode development --no-open",
|
||||||
"start:seedheads": "cross-env PROXY_TARGET=https://seedheads.de webpack serve --progress --mode development --no-open",
|
"start:seedheads": "cross-env PROXY_TARGET=https://seedheads.de NODE_OPTIONS=\"--no-deprecation\" webpack serve --progress --mode development --no-open",
|
||||||
"prod": "webpack serve --progress --mode production --no-client-overlay --no-client --no-web-socket-server --no-open --no-live-reload --no-hot --compress --no-devtool",
|
"prod": "webpack serve --progress --mode production --no-client-overlay --no-client --no-web-socket-server --no-open --no-live-reload --no-hot --compress --no-devtool",
|
||||||
"build:client": "cross-env NODE_ENV=production webpack --progress --mode production && shx cp dist/index.html dist/index_template.html",
|
"build:client": "cross-env NODE_ENV=production webpack --progress --mode production && shx cp dist/index.html dist/index_template.html",
|
||||||
"build": "npm run build:client",
|
"build": "npm run build:client",
|
||||||
@@ -17,7 +17,7 @@
|
|||||||
},
|
},
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
"author": "",
|
"author": "",
|
||||||
"license": "ISC",
|
"license": "0BSD",
|
||||||
"description": "",
|
"description": "",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@emotion/react": "^11.14.0",
|
"@emotion/react": "^11.14.0",
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ const {
|
|||||||
generateProductsXml,
|
generateProductsXml,
|
||||||
generateLlmsTxt,
|
generateLlmsTxt,
|
||||||
generateCategoryLlmsTxt,
|
generateCategoryLlmsTxt,
|
||||||
|
generateAllCategoryLlmsPages,
|
||||||
} = require("./prerender/seo.cjs");
|
} = require("./prerender/seo.cjs");
|
||||||
const {
|
const {
|
||||||
fetchCategoryProducts,
|
fetchCategoryProducts,
|
||||||
@@ -71,10 +72,12 @@ const Batteriegesetzhinweise =
|
|||||||
require("./src/pages/Batteriegesetzhinweise.js").default;
|
require("./src/pages/Batteriegesetzhinweise.js").default;
|
||||||
const Widerrufsrecht = require("./src/pages/Widerrufsrecht.js").default;
|
const Widerrufsrecht = require("./src/pages/Widerrufsrecht.js").default;
|
||||||
const Sitemap = require("./src/pages/Sitemap.js").default;
|
const Sitemap = require("./src/pages/Sitemap.js").default;
|
||||||
|
const PrerenderSitemap = require("./src/PrerenderSitemap.js").default;
|
||||||
const AGB = require("./src/pages/AGB.js").default;
|
const AGB = require("./src/pages/AGB.js").default;
|
||||||
|
const NotFound404 = require("./src/pages/NotFound404.js").default;
|
||||||
|
|
||||||
// Worker function for parallel product rendering
|
// 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 socketUrl = "http://127.0.0.1:9303";
|
||||||
const workerSocket = io(socketUrl, {
|
const workerSocket = io(socketUrl, {
|
||||||
path: "/socket.io/",
|
path: "/socket.io/",
|
||||||
@@ -113,10 +116,13 @@ const renderProductWorker = async (productSeoNames, workerId, progressCallback)
|
|||||||
...productDetails.product,
|
...productDetails.product,
|
||||||
seoName: actualSeoName,
|
seoName: actualSeoName,
|
||||||
}, shopConfig.baseUrl, shopConfig);
|
}, shopConfig.baseUrl, shopConfig);
|
||||||
|
// Get category info from categoryMap if available
|
||||||
|
const categoryInfo = productDetails.product.categoryId ? categoryMap[productDetails.product.categoryId] : null;
|
||||||
|
|
||||||
const jsonLdScript = generateProductJsonLd({
|
const jsonLdScript = generateProductJsonLd({
|
||||||
...productDetails.product,
|
...productDetails.product,
|
||||||
seoName: actualSeoName,
|
seoName: actualSeoName,
|
||||||
}, shopConfig.baseUrl, shopConfig);
|
}, shopConfig.baseUrl, shopConfig, categoryInfo);
|
||||||
const combinedMetaTags = metaTags + "\n" + jsonLdScript;
|
const combinedMetaTags = metaTags + "\n" + jsonLdScript;
|
||||||
|
|
||||||
const success = renderPage(
|
const success = renderPage(
|
||||||
@@ -182,7 +188,7 @@ const renderProductWorker = async (productSeoNames, workerId, progressCallback)
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Function to render products in parallel
|
// Function to render products in parallel
|
||||||
const renderProductsInParallel = async (allProductsArray, maxWorkers, totalProducts) => {
|
const renderProductsInParallel = async (allProductsArray, maxWorkers, totalProducts, categoryMap = {}) => {
|
||||||
// Shared progress tracking
|
// Shared progress tracking
|
||||||
let completedProducts = 0;
|
let completedProducts = 0;
|
||||||
let totalSuccessCount = 0;
|
let totalSuccessCount = 0;
|
||||||
@@ -244,7 +250,7 @@ const renderProductsInParallel = async (allProductsArray, maxWorkers, totalProdu
|
|||||||
|
|
||||||
// Update progress bar with worker stats
|
// Update progress bar with worker stats
|
||||||
updateProgressBar(completedProducts, totalProducts, lastProductName);
|
updateProgressBar(completedProducts, totalProducts, lastProductName);
|
||||||
});
|
}, categoryMap);
|
||||||
|
|
||||||
workerPromises.push(promise);
|
workerPromises.push(promise);
|
||||||
}
|
}
|
||||||
@@ -312,7 +318,8 @@ const renderApp = async (categoryData, socket) => {
|
|||||||
? "index.html"
|
? "index.html"
|
||||||
: "index.prerender.html";
|
: "index.prerender.html";
|
||||||
const homeMetaTags = generateHomepageMetaTags(shopConfig.baseUrl, shopConfig);
|
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 combinedHomeMeta = homeMetaTags + "\n" + homeJsonLd;
|
||||||
const homeSuccess = render(
|
const homeSuccess = render(
|
||||||
homeComponent,
|
homeComponent,
|
||||||
@@ -356,12 +363,14 @@ const renderApp = async (categoryData, socket) => {
|
|||||||
description: "Widerrufsrecht page",
|
description: "Widerrufsrecht page",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
component: Sitemap,
|
component: PrerenderSitemap,
|
||||||
path: "/sitemap",
|
path: "/sitemap",
|
||||||
filename: "sitemap",
|
filename: "sitemap",
|
||||||
description: "Sitemap page",
|
description: "Sitemap page",
|
||||||
|
needsCategoryData: true,
|
||||||
},
|
},
|
||||||
{ component: AGB, path: "/agb", filename: "agb", description: "AGB page" },
|
{ component: AGB, path: "/agb", filename: "agb", description: "AGB page" },
|
||||||
|
{ component: NotFound404, path: "/404", filename: "404", description: "404 Not Found page" },
|
||||||
{
|
{
|
||||||
component: PrerenderKonfigurator,
|
component: PrerenderKonfigurator,
|
||||||
path: "/Konfigurator",
|
path: "/Konfigurator",
|
||||||
@@ -378,13 +387,15 @@ const renderApp = async (categoryData, socket) => {
|
|||||||
|
|
||||||
let staticPagesRendered = 0;
|
let staticPagesRendered = 0;
|
||||||
for (const page of staticPages) {
|
for (const page of staticPages) {
|
||||||
const pageComponent = React.createElement(page.component, null);
|
// Pass category data as props if needed
|
||||||
|
const pageProps = page.needsCategoryData ? { categoryData } : null;
|
||||||
|
const pageComponent = React.createElement(page.component, pageProps);
|
||||||
let metaTags = "";
|
let metaTags = "";
|
||||||
|
|
||||||
// Special handling for Sitemap page to include category data
|
// Special handling for Sitemap page to include category data
|
||||||
if (page.filename === "sitemap" && categoryData) {
|
if (page.filename === "sitemap" && categoryData) {
|
||||||
const allCategories = collectAllCategories(categoryData);
|
const sitemapCategories = collectAllCategories(categoryData);
|
||||||
metaTags = generateSitemapJsonLd(allCategories, shopConfig.baseUrl, shopConfig);
|
metaTags = generateSitemapJsonLd(sitemapCategories, shopConfig.baseUrl, shopConfig);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Special handling for Konfigurator page to include SEO tags
|
// Special handling for Konfigurator page to include SEO tags
|
||||||
@@ -531,6 +542,15 @@ const renderApp = async (categoryData, socket) => {
|
|||||||
const numCPUs = os.cpus().length;
|
const numCPUs = os.cpus().length;
|
||||||
const maxWorkers = Math.min(numCPUs, totalProducts, 8); // Cap at 8 workers to avoid overwhelming the server
|
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(
|
console.log(
|
||||||
`\n📦 Rendering ${totalProducts} individual product pages using ${maxWorkers} parallel workers...`
|
`\n📦 Rendering ${totalProducts} individual product pages using ${maxWorkers} parallel workers...`
|
||||||
);
|
);
|
||||||
@@ -538,7 +558,8 @@ const renderApp = async (categoryData, socket) => {
|
|||||||
const productPagesRendered = await renderProductsInParallel(
|
const productPagesRendered = await renderProductsInParallel(
|
||||||
Array.from(allProducts),
|
Array.from(allProducts),
|
||||||
maxWorkers,
|
maxWorkers,
|
||||||
totalProducts
|
totalProducts,
|
||||||
|
categoryMap
|
||||||
);
|
);
|
||||||
|
|
||||||
console.log(
|
console.log(
|
||||||
@@ -641,27 +662,39 @@ const renderApp = async (categoryData, socket) => {
|
|||||||
productsByCategory[categoryId].push(product);
|
productsByCategory[categoryId].push(product);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Generate category-specific LLM files
|
// Generate category-specific LLM files with pagination
|
||||||
let categoryFilesGenerated = 0;
|
let categoryFilesGenerated = 0;
|
||||||
let totalCategoryProducts = 0;
|
let totalCategoryProducts = 0;
|
||||||
|
let totalPaginatedFiles = 0;
|
||||||
|
|
||||||
for (const category of allCategories) {
|
for (const category of allCategories) {
|
||||||
if (category.seoName) {
|
if (category.seoName) {
|
||||||
const categoryProducts = productsByCategory[category.id] || [];
|
const categoryProducts = productsByCategory[category.id] || [];
|
||||||
const categorySlug = category.seoName.toLowerCase().replace(/[^a-z0-9]/g, '-');
|
const categorySlug = category.seoName.toLowerCase().replace(/[^a-z0-9]/g, '-');
|
||||||
|
|
||||||
const categoryLlmsTxt = generateCategoryLlmsTxt(category, categoryProducts, shopConfig.baseUrl, shopConfig);
|
// Generate all paginated files for this category
|
||||||
const categoryLlmsTxtPath = path.resolve(__dirname, config.outputDir, `llms-${categorySlug}.txt`);
|
const categoryPages = generateAllCategoryLlmsPages(category, categoryProducts, shopConfig.baseUrl, shopConfig);
|
||||||
|
|
||||||
fs.writeFileSync(categoryLlmsTxtPath, categoryLlmsTxt, { encoding: 'utf8' });
|
// Write each paginated file
|
||||||
|
for (const page of categoryPages) {
|
||||||
|
const pagePath = path.resolve(__dirname, config.outputDir, page.fileName);
|
||||||
|
fs.writeFileSync(pagePath, page.content, { encoding: 'utf8' });
|
||||||
|
totalPaginatedFiles++;
|
||||||
|
}
|
||||||
|
|
||||||
console.log(` ✅ llms-${categorySlug}.txt - ${categoryProducts.length} products (${Math.round(categoryLlmsTxt.length / 1024)}KB)`);
|
const pageCount = categoryPages.length;
|
||||||
|
const totalSize = categoryPages.reduce((sum, page) => sum + page.content.length, 0);
|
||||||
|
|
||||||
|
console.log(` ✅ llms-${categorySlug}-page-*.txt - ${categoryProducts.length} products across ${pageCount} pages (${Math.round(totalSize / 1024)}KB total)`);
|
||||||
|
|
||||||
categoryFilesGenerated++;
|
categoryFilesGenerated++;
|
||||||
totalCategoryProducts += categoryProducts.length;
|
totalCategoryProducts += categoryProducts.length;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log(` 📄 Total paginated files generated: ${totalPaginatedFiles}`);
|
||||||
|
console.log(` 📦 Total products across all categories: ${totalCategoryProducts}`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const verification = fs.readFileSync(llmsTxtPath, 'utf8');
|
const verification = fs.readFileSync(llmsTxtPath, 'utf8');
|
||||||
console.log(` - File verification: ✅ All files valid UTF-8`);
|
console.log(` - File verification: ✅ All files valid UTF-8`);
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ const fetchCategoryProducts = (socket, categoryId) => {
|
|||||||
|
|
||||||
socket.emit(
|
socket.emit(
|
||||||
"getCategoryProducts",
|
"getCategoryProducts",
|
||||||
{ categoryId: parseInt(categoryId) },
|
{ full:true, categoryId: parseInt(categoryId) },
|
||||||
(response) => {
|
(response) => {
|
||||||
clearTimeout(timeout);
|
clearTimeout(timeout);
|
||||||
if (response && response.products !== undefined) {
|
if (response && response.products !== undefined) {
|
||||||
|
|||||||
@@ -1,881 +1,4 @@
|
|||||||
const generateProductMetaTags = (product, baseUrl, config) => {
|
// Re-export all SEO functions from the new modular structure
|
||||||
const productUrl = `${baseUrl}/Artikel/${product.seoName}`;
|
// This maintains backward compatibility while using the new split files
|
||||||
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)
|
module.exports = require('./seo/index.cjs');
|
||||||
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,
|
|
||||||
};
|
|
||||||
|
|||||||
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>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="de">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<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="/">
|
<base href="/">
|
||||||
<title>SeedHeads.de</title>
|
<title>GrowHeads.de</title>
|
||||||
<link rel="icon" type="image/x-icon" href="/favicon.ico">
|
<link rel="icon" type="image/x-icon" href="/favicon.ico">
|
||||||
</head>
|
</head>
|
||||||
<body style="overflow-y: scroll;">
|
<body style="overflow-y: scroll;">
|
||||||
|
|||||||
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
|
// Lazy load legal pages - rarely accessed
|
||||||
const Datenschutz = lazy(() => import(/* webpackChunkName: "legal" */ "./pages/Datenschutz.js"));
|
const Datenschutz = lazy(() => import(/* webpackChunkName: "legal" */ "./pages/Datenschutz.js"));
|
||||||
const AGB = lazy(() => import(/* webpackChunkName: "legal" */ "./pages/AGB.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 Sitemap = lazy(() => import(/* webpackChunkName: "sitemap" */ "./pages/Sitemap.js"));
|
||||||
const Impressum = lazy(() => import(/* webpackChunkName: "legal" */ "./pages/Impressum.js"));
|
const Impressum = lazy(() => import(/* webpackChunkName: "legal" */ "./pages/Impressum.js"));
|
||||||
const Batteriegesetzhinweise = lazy(() => import(/* webpackChunkName: "legal" */ "./pages/Batteriegesetzhinweise.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
|
// Check if we're in development mode
|
||||||
const isDevelopment = process.env.NODE_ENV === "development";
|
const isDevelopment = process.env.NODE_ENV === "development";
|
||||||
|
|
||||||
const socket = useContext(SocketContext);
|
const {socket,socketB} = useContext(SocketContext);
|
||||||
|
console.log("AppContent: socket", socket);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
@@ -200,7 +202,7 @@ const AppContent = ({ currentTheme, onThemeChange }) => {
|
|||||||
{/* Category page - Render Content in parallel */}
|
{/* Category page - Render Content in parallel */}
|
||||||
<Route
|
<Route
|
||||||
path="/Kategorie/:categoryId"
|
path="/Kategorie/:categoryId"
|
||||||
element={<Content socket={socket} />}
|
element={<Content socket={socket} socketB={socketB} />}
|
||||||
/>
|
/>
|
||||||
{/* Single product page */}
|
{/* Single product page */}
|
||||||
<Route
|
<Route
|
||||||
@@ -209,7 +211,7 @@ const AppContent = ({ currentTheme, onThemeChange }) => {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Search page - Render Content in parallel */}
|
{/* Search page - Render Content in parallel */}
|
||||||
<Route path="/search" element={<Content socket={socket} />} />
|
<Route path="/search" element={<Content socket={socket} socketB={socketB} />} />
|
||||||
|
|
||||||
{/* Profile page */}
|
{/* Profile page */}
|
||||||
<Route path="/profile" element={<ProfilePageWithSocket />} />
|
<Route path="/profile" element={<ProfilePageWithSocket />} />
|
||||||
@@ -217,21 +219,22 @@ const AppContent = ({ currentTheme, onThemeChange }) => {
|
|||||||
{/* Reset password page */}
|
{/* Reset password page */}
|
||||||
<Route
|
<Route
|
||||||
path="/resetPassword"
|
path="/resetPassword"
|
||||||
element={<ResetPassword socket={socket} />}
|
element={<ResetPassword socket={socket} socketB={socketB} />}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Admin page */}
|
{/* Admin page */}
|
||||||
<Route path="/admin" element={<AdminPage socket={socket} />} />
|
<Route path="/admin" element={<AdminPage socket={socket} socketB={socketB} />} />
|
||||||
|
|
||||||
{/* Admin Users page */}
|
{/* 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 */}
|
{/* Admin Server Logs page */}
|
||||||
<Route path="/admin/logs" element={<ServerLogsPage socket={socket} />} />
|
<Route path="/admin/logs" element={<ServerLogsPage socket={socket} socketB={socketB} />} />
|
||||||
|
|
||||||
{/* Legal pages */}
|
{/* Legal pages */}
|
||||||
<Route path="/datenschutz" element={<Datenschutz />} />
|
<Route path="/datenschutz" element={<Datenschutz />} />
|
||||||
<Route path="/agb" element={<AGB />} />
|
<Route path="/agb" element={<AGB />} />
|
||||||
|
<Route path="/404" element={<NotFound404 />} />
|
||||||
<Route path="/sitemap" element={<Sitemap />} />
|
<Route path="/sitemap" element={<Sitemap />} />
|
||||||
<Route path="/impressum" element={<Impressum />} />
|
<Route path="/impressum" element={<Impressum />} />
|
||||||
<Route
|
<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 [imageUrl, setImageUrl] = useState(null);
|
||||||
const [imageError, setImageError] = useState(false);
|
const [imageError, setImageError] = useState(false);
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const socket = useContext(SocketContext);
|
const context = useContext(SocketContext);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let objectUrl = null;
|
let objectUrl = null;
|
||||||
@@ -61,10 +61,10 @@ const CategoryBox = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
// If socket is available and connected, fetch the image
|
// 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);
|
setIsLoading(true);
|
||||||
|
|
||||||
socket.emit('getCategoryPic', { categoryId: id }, (response) => {
|
context.socket.emit('getCategoryPic', { categoryId: id }, (response) => {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
|
|
||||||
if (response.success) {
|
if (response.success) {
|
||||||
@@ -119,7 +119,7 @@ const CategoryBox = ({
|
|||||||
URL.revokeObjectURL(objectUrl);
|
URL.revokeObjectURL(objectUrl);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}, [socket, socket?.connected, id, isLoading]);
|
}, [context, context?.socket?.connected, id, isLoading]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Paper
|
<Paper
|
||||||
|
|||||||
@@ -97,7 +97,12 @@ function getFilteredProducts(unfilteredProducts, attributes) {
|
|||||||
let filteredProducts = (unfilteredProducts || []).filter(product => {
|
let filteredProducts = (unfilteredProducts || []).filter(product => {
|
||||||
const availabilityFilter = sessionStorage.getItem('filter_availability');
|
const availabilityFilter = sessionStorage.getItem('filter_availability');
|
||||||
let inStockMatch = availabilityFilter == 1 ? true : (product.available>0);
|
let inStockMatch = availabilityFilter == 1 ? true : (product.available>0);
|
||||||
const isNewMatch = availabilityFilters.includes('2') ? isNew(product.neu) : true;
|
|
||||||
|
// Check if there are any new products in the entire set
|
||||||
|
const hasNewProducts = (unfilteredProducts || []).some(product => isNew(product.neu));
|
||||||
|
|
||||||
|
// Only apply the new filter if there are actually new products and the filter is active
|
||||||
|
const isNewMatch = availabilityFilters.includes('2') && hasNewProducts ? isNew(product.neu) : true;
|
||||||
let soonMatch = availabilityFilters.includes('3') ? !product.available && product.incoming : true;
|
let soonMatch = availabilityFilters.includes('3') ? !product.available && product.incoming : true;
|
||||||
|
|
||||||
const soon2Match = (availabilityFilter != 1)&&availabilityFilters.includes('3') ? (product.available) || (!product.available && product.incoming) : true;
|
const soon2Match = (availabilityFilter != 1)&&availabilityFilters.includes('3') ? (product.available) || (!product.available && product.incoming) : true;
|
||||||
@@ -133,7 +138,31 @@ function getFilteredProducts(unfilteredProducts, attributes) {
|
|||||||
const manufacturer = uniqueManufacturersWithName.find(manufacturer => manufacturer.id === filter);
|
const manufacturer = uniqueManufacturersWithName.find(manufacturer => manufacturer.id === filter);
|
||||||
return {name: manufacturer.value, value: manufacturer.id};
|
return {name: manufacturer.value, value: manufacturer.id};
|
||||||
});
|
});
|
||||||
return {filteredProducts,activeAttributeFilters:activeAttributeFiltersWithNames,activeManufacturerFilters:activeManufacturerFiltersWithNames};
|
|
||||||
|
// Extract active availability filters
|
||||||
|
const availabilityFilter = sessionStorage.getItem('filter_availability');
|
||||||
|
const activeAvailabilityFilters = [];
|
||||||
|
|
||||||
|
// Check if there are actually products with these characteristics
|
||||||
|
const hasNewProducts = (unfilteredProducts || []).some(product => isNew(product.neu));
|
||||||
|
const hasComingSoonProducts = (unfilteredProducts || []).some(product => !product.available && product.incoming);
|
||||||
|
|
||||||
|
// Check for "auf Lager" filter (in stock) - it's active when filter_availability is NOT set to '1'
|
||||||
|
if (availabilityFilter !== '1') {
|
||||||
|
activeAvailabilityFilters.push({id: '1', name: 'auf Lager'});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for "Neu" filter (new) - only show if there are actually new products and filter is active
|
||||||
|
if (availabilityFilters.includes('2') && hasNewProducts) {
|
||||||
|
activeAvailabilityFilters.push({id: '2', name: 'Neu'});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for "Bald verfügbar" filter (coming soon) - only show if there are actually coming soon products and filter is active
|
||||||
|
if (availabilityFilters.includes('3') && hasComingSoonProducts) {
|
||||||
|
activeAvailabilityFilters.push({id: '3', name: 'Bald verfügbar'});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {filteredProducts,activeAttributeFilters:activeAttributeFiltersWithNames,activeManufacturerFilters:activeManufacturerFiltersWithNames,activeAvailabilityFilters};
|
||||||
}
|
}
|
||||||
function setCachedCategoryData(categoryId, data) {
|
function setCachedCategoryData(categoryId, data) {
|
||||||
if (!window.productCache) {
|
if (!window.productCache) {
|
||||||
@@ -238,6 +267,8 @@ class Content extends Component {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
fetchCategoryData(categoryId) {
|
fetchCategoryData(categoryId) {
|
||||||
const cachedData = getCachedCategoryData(categoryId);
|
const cachedData = getCachedCategoryData(categoryId);
|
||||||
if (cachedData) {
|
if (cachedData) {
|
||||||
@@ -251,15 +282,37 @@ class Content extends Component {
|
|||||||
console.log("Socket not connected yet, waiting for connection to fetch category data");
|
console.log("Socket not connected yet, waiting for connection to fetch category data");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
console.log(`productList:${categoryId}`);
|
||||||
|
this.props.socket.off(`productList:${categoryId}`);
|
||||||
|
|
||||||
this.props.socket.emit("getCategoryProducts", { categoryId: categoryId },
|
// Track if we've received the full response to ignore stub response if needed
|
||||||
(response) => {
|
let receivedFullResponse = false;
|
||||||
|
|
||||||
|
this.props.socket.on(`productList:${categoryId}`,(response) => {
|
||||||
|
console.log("getCategoryProducts full response", response);
|
||||||
|
receivedFullResponse = true;
|
||||||
setCachedCategoryData(categoryId, response);
|
setCachedCategoryData(categoryId, response);
|
||||||
if (response && response.products !== undefined) {
|
if (response && response.products !== undefined) {
|
||||||
this.processDataWithCategoryTree(response, categoryId);
|
this.processDataWithCategoryTree(response, categoryId);
|
||||||
} else {
|
} else {
|
||||||
console.log("fetchCategoryData in Content failed", response);
|
console.log("fetchCategoryData in Content failed", response);
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.props.socket.emit("getCategoryProducts", { categoryId: categoryId },
|
||||||
|
(response) => {
|
||||||
|
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("Ignoring stub response - full response already received");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -417,7 +470,7 @@ class Content extends Component {
|
|||||||
|
|
||||||
|
|
||||||
return (
|
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 ? (
|
{showCategoryBoxes ? (
|
||||||
// Show category boxes layout when no products but have child categories
|
// Show category boxes layout when no products but have child categories
|
||||||
@@ -467,30 +520,20 @@ class Content extends Component {
|
|||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{/* Subcategories Grid */}
|
{/* Subcategories Grid */}
|
||||||
<Box sx={{ flex: 1, minWidth: 0 }}>
|
<Box sx={{ flexGrow: 1 }}>
|
||||||
<CategoryBoxGrid
|
<CategoryBoxGrid categories={this.state.childCategories} />
|
||||||
categories={this.state.childCategories}
|
|
||||||
showTitle={false}
|
|
||||||
spacing={3}
|
|
||||||
/>
|
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
// Just show subcategories without parent
|
// No parent category, just show subcategories
|
||||||
return (
|
return <CategoryBoxGrid categories={this.state.childCategories} />;
|
||||||
<CategoryBoxGrid
|
|
||||||
categories={this.state.childCategories}
|
|
||||||
showTitle={false}
|
|
||||||
spacing={3}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
})()}
|
})()}
|
||||||
</Box>
|
</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.state.loaded &&
|
||||||
this.props.params.categoryId &&
|
this.props.params.categoryId &&
|
||||||
!(this.state.unfilteredProducts.length > 0 && this.state.childCategories.length > 0) && (() => {
|
!(this.state.unfilteredProducts.length > 0 && this.state.childCategories.length > 0) && (() => {
|
||||||
@@ -534,7 +577,7 @@ class Content extends Component {
|
|||||||
<Box sx={{
|
<Box sx={{
|
||||||
display: 'grid',
|
display: 'grid',
|
||||||
gridTemplateColumns: { xs: '1fr', sm: '1fr 2fr', md: '1fr 3fr', lg: '1fr 4fr', xl: '1fr 4fr' },
|
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={{
|
<Stack direction="row" spacing={0} sx={{
|
||||||
@@ -661,10 +704,12 @@ class Content extends Component {
|
|||||||
<Box>
|
<Box>
|
||||||
<ProductList
|
<ProductList
|
||||||
socket={this.props.socket}
|
socket={this.props.socket}
|
||||||
|
socketB={this.props.socketB}
|
||||||
totalProductCount={(this.state.unfilteredProducts || []).length}
|
totalProductCount={(this.state.unfilteredProducts || []).length}
|
||||||
products={this.state.filteredProducts || []}
|
products={this.state.filteredProducts || []}
|
||||||
activeAttributeFilters={this.state.activeAttributeFilters || []}
|
activeAttributeFilters={this.state.activeAttributeFilters || []}
|
||||||
activeManufacturerFilters={this.state.activeManufacturerFilters || []}
|
activeManufacturerFilters={this.state.activeManufacturerFilters || []}
|
||||||
|
activeAvailabilityFilters={this.state.activeAvailabilityFilters || []}
|
||||||
onFilterChange={()=>{this.filterProducts()}}
|
onFilterChange={()=>{this.filterProducts()}}
|
||||||
dataType={this.state.dataType}
|
dataType={this.state.dataType}
|
||||||
dataParam={this.state.dataParam}
|
dataParam={this.state.dataParam}
|
||||||
|
|||||||
@@ -37,13 +37,17 @@ class Header extends Component {
|
|||||||
|
|
||||||
render() {
|
render() {
|
||||||
// Get socket directly from context in render method
|
// Get socket directly from context in render method
|
||||||
const socket = this.context;
|
const {socket,socketB} = this.context;
|
||||||
const { isHomePage, isProfilePage } = this.props;
|
const { isHomePage, isProfilePage } = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AppBar position="sticky" color="primary" elevation={0} sx={{ zIndex: 1100 }}>
|
<AppBar position="sticky" color="primary" elevation={0} sx={{ zIndex: 1100 }}>
|
||||||
<Toolbar sx={{ minHeight: 64 }}>
|
<Toolbar sx={{ minHeight: 64, py: { xs: 0.5, sm: 0 } }}>
|
||||||
<Container maxWidth="lg" sx={{ display: 'flex', alignItems: 'center' }}>
|
<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 */}
|
{/* First row: Logo and ButtonGroup on xs, all items on larger screens */}
|
||||||
<Box sx={{
|
<Box sx={{
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
@@ -56,28 +60,41 @@ class Header extends Component {
|
|||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
width: '100%',
|
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 />
|
<Logo />
|
||||||
{/* SearchBar visible on sm and up */}
|
{/* SearchBar visible on sm and up */}
|
||||||
<Box sx={{ display: { xs: 'none', sm: 'block' }, flexGrow: 1 }}>
|
<Box sx={{ display: { xs: 'none', sm: 'block' }, flexGrow: 1 }}>
|
||||||
<SearchBar />
|
<SearchBar />
|
||||||
</Box>
|
</Box>
|
||||||
|
<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}/>
|
<ButtonGroupWithRouter socket={socket}/>
|
||||||
</Box>
|
</Box>
|
||||||
|
</Box>
|
||||||
|
|
||||||
{/* Second row: SearchBar only on xs */}
|
{/* Second row: SearchBar only on xs - make it wider */}
|
||||||
<Box sx={{
|
<Box sx={{
|
||||||
display: { xs: 'block', sm: 'none' },
|
display: { xs: 'block', sm: 'none' },
|
||||||
width: '100%',
|
width: '100%',
|
||||||
mt: 1,mb: 1
|
mt: { xs: 1, sm: 0 },
|
||||||
|
mb: { xs: 0.5, sm: 0 },
|
||||||
|
px: { xs: 0, sm: 0 }
|
||||||
}}>
|
}}>
|
||||||
|
<Box sx={{ width: '100%' }}>
|
||||||
<SearchBar />
|
<SearchBar />
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
</Box>
|
||||||
</Container>
|
</Container>
|
||||||
</Toolbar>
|
</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>
|
</AppBar>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -91,7 +108,7 @@ const HeaderWithContext = (props) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<SocketContext.Consumer>
|
<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>
|
</SocketContext.Consumer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ class Images extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
loadPic = (size,bildId,index) => {
|
loadPic = (size,bildId,index) => {
|
||||||
this.props.socket.emit('getPic', { bildId, size }, (res) => {
|
this.props.socketB.emit('getPic', { bildId, size }, (res) => {
|
||||||
if(res.success){
|
if(res.success){
|
||||||
const url = URL.createObjectURL(new Blob([res.imageBuffer], { type: 'image/jpeg' }));
|
const url = URL.createObjectURL(new Blob([res.imageBuffer], { type: 'image/jpeg' }));
|
||||||
|
|
||||||
|
|||||||
@@ -543,7 +543,7 @@ export class LoginComponent extends Component {
|
|||||||
onClick={this.handleOpen}
|
onClick={this.handleOpen}
|
||||||
sx={{ my: 1, mx: 1.5 }}
|
sx={{ my: 1, mx: 1.5 }}
|
||||||
>
|
>
|
||||||
Anmelden
|
Login
|
||||||
</Button>
|
</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}
|
this.state = {image:window.smallPicCache[bildId],loading:false, error: false}
|
||||||
}else{
|
}else{
|
||||||
this.state = {image: null, loading: true, error: false};
|
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){
|
if(res.success){
|
||||||
window.smallPicCache[bildId] = URL.createObjectURL(new Blob([res.imageBuffer], { type: 'image/jpeg' }));
|
window.smallPicCache[bildId] = URL.createObjectURL(new Blob([res.imageBuffer], { type: 'image/jpeg' }));
|
||||||
if (this._isMounted) {
|
if (this._isMounted) {
|
||||||
@@ -179,18 +180,21 @@ class Product extends Component {
|
|||||||
|
|
||||||
<Card
|
<Card
|
||||||
sx={{
|
sx={{
|
||||||
width: { xs: 'calc(100vw - 48px)', sm: '250px' },
|
width: { xs: '100vw', sm: '250px' },
|
||||||
minWidth: { xs: 'calc(100vw - 48px)', sm: '250px' },
|
minWidth: { xs: '100vw', sm: '250px' },
|
||||||
height: '100%',
|
height: '100%',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
transition: 'transform 0.3s ease, box-shadow 0.3s ease',
|
transition: 'transform 0.3s ease, box-shadow 0.3s ease',
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
overflow: 'hidden',
|
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': {
|
'&:hover': {
|
||||||
transform: 'translateY(-5px)',
|
transform: { xs: 'none', sm: 'translateY(-5px)' },
|
||||||
boxShadow: '0px 10px 20px rgba(0,0,0,0.1)'
|
boxShadow: { xs: 'none', sm: '0px 10px 20px rgba(0,0,0,0.1)' }
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -110,8 +110,8 @@ class ProductDetailPage extends Component {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Not in cache, fetch from server
|
// Not in cache, fetch from server
|
||||||
if (this.props.socket && this.props.socket.connected) {
|
if (this.props.socketB && this.props.socketB.connected) {
|
||||||
this.props.socket.emit(
|
this.props.socketB.emit(
|
||||||
"getAttributePicture",
|
"getAttributePicture",
|
||||||
{ id: cacheKey },
|
{ id: cacheKey },
|
||||||
(res) => {
|
(res) => {
|
||||||
@@ -334,6 +334,7 @@ class ProductDetailPage extends Component {
|
|||||||
{product.pictureList && (
|
{product.pictureList && (
|
||||||
<Images
|
<Images
|
||||||
socket={this.props.socket}
|
socket={this.props.socket}
|
||||||
|
socketB={this.props.socketB}
|
||||||
pictureList={product.pictureList}
|
pictureList={product.pictureList}
|
||||||
fullscreenOpen={this.state.imageDialogOpen}
|
fullscreenOpen={this.state.imageDialogOpen}
|
||||||
onOpenFullscreen={this.handleOpenDialog}
|
onOpenFullscreen={this.handleOpenDialog}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ const ProductDetailWithSocket = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<SocketContext.Consumer>
|
<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>
|
</SocketContext.Consumer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -162,13 +162,17 @@ class ProductFilters extends Component {
|
|||||||
return (
|
return (
|
||||||
<Paper
|
<Paper
|
||||||
id="filters-paper"
|
id="filters-paper"
|
||||||
elevation={1}
|
elevation={window.innerWidth < 600 ? 0 : 1}
|
||||||
sx={{
|
sx={{
|
||||||
p: 2,
|
p: { xs: 1, sm: 2 },
|
||||||
borderRadius: 2,
|
borderRadius: { xs: 0, sm: 2 },
|
||||||
bgcolor: 'background.paper',
|
bgcolor: 'background.paper',
|
||||||
display: 'flex',
|
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) => {
|
renderPagination = (pages, page) => {
|
||||||
|
// Make pagination invisible when there are zero products to avoid layout shifts
|
||||||
|
const hasProducts = this.props.products.length > 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box sx={{ height: 64, display: 'flex', alignItems: 'center', justifyContent: 'left' }}>
|
<Box sx={{
|
||||||
{((this.state.itemsPerPage==='all')||(this.props.products.length<this.state.itemsPerPage))?null:
|
height: 64,
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'left',
|
||||||
|
width: '100%',
|
||||||
|
visibility: hasProducts ? 'visible' : 'hidden'
|
||||||
|
}}>
|
||||||
|
{(this.state.itemsPerPage==='all')?null:
|
||||||
<Pagination
|
<Pagination
|
||||||
count={pages}
|
count={pages}
|
||||||
page={page}
|
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() {
|
render() {
|
||||||
//console.log('products',this.props.activeAttributeFilters,this.props.activeManufacturerFilters,window.currentSearchQuery,this.state.sortBy);
|
//console.log('products',this.props.activeAttributeFilters,this.props.activeManufacturerFilters,window.currentSearchQuery,this.state.sortBy);
|
||||||
|
|
||||||
@@ -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);
|
const products = this.state.itemsPerPage==='all'?[...filteredProducts]:filteredProducts.slice((this.state.page - 1) * this.state.itemsPerPage , this.state.page * this.state.itemsPerPage);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box sx={{ height: '100%' }}>
|
<Box sx={{ height: '100%', px: { xs: 0, sm: 0 } }}>
|
||||||
<Box sx={{
|
<Box sx={{
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
justifyContent: 'space-between',
|
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) => (
|
{this.props.activeAttributeFilters.map((filter,index) => (
|
||||||
<Chip
|
<Chip
|
||||||
size="medium"
|
size="medium"
|
||||||
@@ -205,11 +307,26 @@ class ProductList extends Component {
|
|||||||
clickable
|
clickable
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
</Box>
|
</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 */}
|
{/* 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>
|
<InputLabel id="sort-by-label">Sortierung</InputLabel>
|
||||||
<Select
|
<Select
|
||||||
size="small"
|
size="small"
|
||||||
@@ -244,7 +361,13 @@ class ProductList extends Component {
|
|||||||
</FormControl>
|
</FormControl>
|
||||||
|
|
||||||
{/* Per Page Dropdown */}
|
{/* 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>
|
<InputLabel id="products-per-page-label">pro Seite</InputLabel>
|
||||||
<Select
|
<Select
|
||||||
labelId="products-per-page-label"
|
labelId="products-per-page-label"
|
||||||
@@ -278,39 +401,56 @@ class ProductList extends Component {
|
|||||||
<MenuItem value="all">Alle</MenuItem>
|
<MenuItem value="all">Alle</MenuItem>
|
||||||
</Select>
|
</Select>
|
||||||
</FormControl>
|
</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>
|
</Box>
|
||||||
|
|
||||||
<Box sx={{
|
<Box sx={{
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
justifyContent: 'space-between',
|
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 }
|
||||||
}}>
|
}}>
|
||||||
|
<Box sx={{ px: { xs: 1, sm: 0 }, width: '100%' }}>
|
||||||
{ this.renderPagination(Math.ceil(filteredProducts.length / this.state.itemsPerPage), this.state.page) }
|
{ this.renderPagination(Math.ceil(filteredProducts.length / this.state.itemsPerPage), this.state.page) }
|
||||||
<Stack direction="row" spacing={2}>
|
</Box>
|
||||||
|
<Stack direction="row" spacing={2} sx={{ display: { xs: 'none', sm: 'flex' }, px: { xs: 1, sm: 0 } }}>
|
||||||
<Typography variant="body2" color="text.secondary">
|
<Typography variant="body2" color="text.secondary">
|
||||||
{/*this.props.dataType == 'category' && (<>Kategorie: {this.props.dataParam}</>)}*/}
|
{/*this.props.dataType == 'category' && (<>Kategorie: {this.props.dataParam}</>)}*/}
|
||||||
{this.props.dataType == 'search' && (<>Suchergebnisse für: "{this.props.dataParam}"</>)}
|
{this.props.dataType == 'search' && (<>Suchergebnisse für: "{this.props.dataParam}"</>)}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography variant="body2" color="text.secondary">
|
<Typography variant="body2" color="text.secondary" sx={{ whiteSpace: 'nowrap' }}>
|
||||||
{
|
{this.getProductCountText()}
|
||||||
this.props.totalProductCount==this.props.products.length && this.props.totalProductCount>0 ?
|
|
||||||
`${this.props.totalProductCount} Produkte`
|
|
||||||
:
|
|
||||||
`${this.props.products.length} von ${this.props.totalProductCount} Produkte`
|
|
||||||
}
|
|
||||||
</Typography>
|
</Typography>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Grid container spacing={2}>
|
<Grid container spacing={{ xs: 0, sm: 2 }}>
|
||||||
{products.map((product) => (
|
{this.renderNoProductsMessage()}
|
||||||
|
{products.map((product, index) => (
|
||||||
<Grid
|
<Grid
|
||||||
key={product.id}
|
key={product.id}
|
||||||
sx={{
|
sx={{
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
justifyContent: { xs: 'stretch', sm: 'center' },
|
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
|
<Product
|
||||||
@@ -331,6 +471,7 @@ class ProductList extends Component {
|
|||||||
versandklasse={product.versandklasse}
|
versandklasse={product.versandklasse}
|
||||||
weight={product.weight}
|
weight={product.weight}
|
||||||
socket={this.props.socket}
|
socket={this.props.socket}
|
||||||
|
socketB={this.props.socketB}
|
||||||
pictureList={product.pictureList}
|
pictureList={product.pictureList}
|
||||||
availableSupplier={product.availableSupplier}
|
availableSupplier={product.availableSupplier}
|
||||||
/>
|
/>
|
||||||
@@ -338,8 +479,18 @@ class ProductList extends Component {
|
|||||||
))}
|
))}
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
|
{/* 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)}
|
{this.renderPagination(Math.ceil(filteredProducts.length / this.state.itemsPerPage), this.state.page)}
|
||||||
</Box>
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -142,7 +142,7 @@ class ButtonGroup extends Component {
|
|||||||
onClose={this.toggleCart}
|
onClose={this.toggleCart}
|
||||||
disableScrollLock={true}
|
disableScrollLock={true}
|
||||||
>
|
>
|
||||||
<Box sx={{ width: 420, p: 2 }}>
|
<Box sx={{ width: { xs: '100vw', sm: 420 }, p: { xs: 1, sm: 2 } }}>
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
|
|||||||
@@ -3,8 +3,11 @@ import Box from "@mui/material/Box";
|
|||||||
import Container from "@mui/material/Container";
|
import Container from "@mui/material/Container";
|
||||||
import Button from "@mui/material/Button";
|
import Button from "@mui/material/Button";
|
||||||
import Typography from "@mui/material/Typography";
|
import Typography from "@mui/material/Typography";
|
||||||
|
import Collapse from "@mui/material/Collapse";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import HomeIcon from "@mui/icons-material/Home";
|
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 {
|
class CategoryList extends Component {
|
||||||
findCategoryById = (category, targetId) => {
|
findCategoryById = (category, targetId) => {
|
||||||
@@ -54,6 +57,7 @@ class CategoryList extends Component {
|
|||||||
level3Categories: [], // Children of active level 2 category
|
level3Categories: [], // Children of active level 2 category
|
||||||
activePath: [], // Array of active category objects for each level
|
activePath: [], // Array of active category objects for each level
|
||||||
fetchedCategories: false,
|
fetchedCategories: false,
|
||||||
|
mobileMenuOpen: false, // State for mobile collapsible menu
|
||||||
};
|
};
|
||||||
|
|
||||||
// Try to get cached data for SSR
|
// Try to get cached data for SSR
|
||||||
@@ -124,11 +128,13 @@ class CategoryList extends Component {
|
|||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
componentDidUpdate(prevProps) {
|
||||||
// Handle socket connection changes
|
// Handle socket connection changes
|
||||||
|
|
||||||
const wasConnected = prevProps.socket && prevProps.socket.connected;
|
const wasConnected = prevProps.socket && prevProps.socket.connected;
|
||||||
const isNowConnected = this.props.socket && this.props.socket.connected;
|
const isNowConnected = this.props.socket && this.props.socket.connected;
|
||||||
|
|
||||||
if (!wasConnected && isNowConnected && !this.state.fetchedCategories) {
|
if (!wasConnected && isNowConnected && !this.state.fetchedCategories) {
|
||||||
// Socket just connected and we haven't fetched categories yet
|
// Socket just connected and we haven't fetched categories yet
|
||||||
|
|
||||||
this.setState(
|
this.setState(
|
||||||
{
|
{
|
||||||
fetchedCategories: false,
|
fetchedCategories: false,
|
||||||
@@ -158,7 +164,7 @@ class CategoryList extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (this.state.fetchedCategories) {
|
if (this.state.fetchedCategories) {
|
||||||
//console.log('Categories already fetched, skipping');
|
console.log('Categories already fetched, skipping');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -222,7 +228,6 @@ class CategoryList extends Component {
|
|||||||
//console.log('CategoryList: Fetching categories from socket');
|
//console.log('CategoryList: Fetching categories from socket');
|
||||||
socket.emit("categoryList", { categoryId: 209 }, (response) => {
|
socket.emit("categoryList", { categoryId: 209 }, (response) => {
|
||||||
if (response && response.categoryTree) {
|
if (response && response.categoryTree) {
|
||||||
//console.log('Category tree received:', response.categoryTree);
|
|
||||||
|
|
||||||
// Store in global cache with timestamp
|
// Store in global cache with timestamp
|
||||||
try {
|
try {
|
||||||
@@ -237,7 +242,6 @@ class CategoryList extends Component {
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Error writing to cache:", err);
|
console.error("Error writing to cache:", err);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.processCategoryTree(response.categoryTree);
|
this.processCategoryTree(response.categoryTree);
|
||||||
} else {
|
} else {
|
||||||
try {
|
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() {
|
render() {
|
||||||
const { level1Categories, level2Categories, level3Categories, activePath } =
|
const { level1Categories, activePath, mobileMenuOpen } =
|
||||||
this.state;
|
this.state;
|
||||||
|
|
||||||
const renderCategoryRow = (categories, level = 1) => (
|
const renderCategoryRow = (categories, level = 1, isMobile = false) => (
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
display: "flex",
|
display: "flex",
|
||||||
justifyContent: "flex-start",
|
justifyContent: "flex-start",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
flexWrap: "nowrap",
|
flexWrap: isMobile ? "wrap" : "nowrap",
|
||||||
overflowX: "auto",
|
overflowX: isMobile ? "visible" : "auto",
|
||||||
|
flexDirection: isMobile ? "column" : "row",
|
||||||
py: 0.5, // Add vertical padding to prevent border clipping
|
py: 0.5, // Add vertical padding to prevent border clipping
|
||||||
"&::-webkit-scrollbar": {
|
"&::-webkit-scrollbar": {
|
||||||
display: "none",
|
display: "none",
|
||||||
@@ -340,34 +358,76 @@ class CategoryList extends Component {
|
|||||||
color="inherit"
|
color="inherit"
|
||||||
size="small"
|
size="small"
|
||||||
aria-label="Zur Startseite"
|
aria-label="Zur Startseite"
|
||||||
|
onClick={isMobile ? this.handleMobileCategoryClick : undefined}
|
||||||
sx={{
|
sx={{
|
||||||
fontSize: "0.75rem",
|
fontSize: "0.75rem",
|
||||||
fontWeight: "normal",
|
|
||||||
textTransform: "none",
|
textTransform: "none",
|
||||||
whiteSpace: "nowrap",
|
whiteSpace: "nowrap",
|
||||||
opacity: 0.9,
|
opacity: 0.9,
|
||||||
mx: 0.5,
|
mx: isMobile ? 0 : 0.5,
|
||||||
my: 0.25, // Add consistent vertical margin to account for borders
|
my: 0.25,
|
||||||
minWidth: "auto",
|
minWidth: isMobile ? "100%" : "auto",
|
||||||
border: "2px solid transparent", // Always have border space
|
borderRadius: 1,
|
||||||
borderRadius: 1, // Always have border radius
|
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 && {
|
...(this.props.activeCategoryId === null && {
|
||||||
boxShadow: "0 4px 12px rgba(0,0,0,0.5)",
|
bgcolor: "#fff",
|
||||||
transform: "translateY(-2px)",
|
textShadow: "none",
|
||||||
bgcolor: "rgba(255,255,255,0.25)",
|
|
||||||
borderColor: "rgba(255,255,255,0.6)", // Change border color instead of adding border
|
|
||||||
fontWeight: "bold",
|
|
||||||
opacity: 1,
|
opacity: 1,
|
||||||
}),
|
}),
|
||||||
"&:hover": {
|
"&:hover": {
|
||||||
opacity: 1,
|
opacity: 1,
|
||||||
bgcolor: "rgba(255,255,255,0.15)",
|
bgcolor: "#fff",
|
||||||
transform: "translateY(-1px)",
|
textShadow: "none",
|
||||||
boxShadow: "0 2px 6px rgba(0,0,0,0.3)",
|
"& .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>
|
</Button>
|
||||||
)}
|
)}
|
||||||
{this.state.fetchedCategories && categories.length > 0 ? (
|
{this.state.fetchedCategories && categories.length > 0 ? (
|
||||||
@@ -385,39 +445,72 @@ class CategoryList extends Component {
|
|||||||
to={`/Kategorie/${category.seoName}`}
|
to={`/Kategorie/${category.seoName}`}
|
||||||
color="inherit"
|
color="inherit"
|
||||||
size="small"
|
size="small"
|
||||||
|
onClick={isMobile ? this.handleMobileCategoryClick : undefined}
|
||||||
sx={{
|
sx={{
|
||||||
fontSize: "0.75rem",
|
fontSize: "0.75rem",
|
||||||
fontWeight: "normal",
|
|
||||||
textTransform: "none",
|
textTransform: "none",
|
||||||
whiteSpace: "nowrap",
|
whiteSpace: "nowrap",
|
||||||
opacity: 0.9,
|
opacity: 0.9,
|
||||||
mx: 0.5,
|
mx: isMobile ? 0 : 0.5,
|
||||||
my: 0.25, // Add consistent vertical margin to account for borders
|
my: 0.25,
|
||||||
border: "2px solid transparent", // Always have border space
|
minWidth: isMobile ? "100%" : "auto",
|
||||||
borderRadius: 1, // Always have border radius
|
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 && {
|
...(isActiveAtThisLevel && {
|
||||||
boxShadow: "0 4px 12px rgba(0,0,0,0.5)",
|
bgcolor: "#fff",
|
||||||
transform: "translateY(-2px)",
|
textShadow: "none",
|
||||||
bgcolor: "rgba(255,255,255,0.25)",
|
|
||||||
borderColor: "rgba(255,255,255,0.6)", // Change border color instead of adding border
|
|
||||||
fontWeight: "bold",
|
|
||||||
opacity: 1,
|
opacity: 1,
|
||||||
}),
|
}),
|
||||||
"&:hover": {
|
"&:hover": {
|
||||||
opacity: 1,
|
opacity: 1,
|
||||||
bgcolor: "rgba(255,255,255,0.15)",
|
bgcolor: "#fff",
|
||||||
transform: "translateY(-1px)",
|
textShadow: "none",
|
||||||
boxShadow: "0 2px 6px rgba(0,0,0,0.3)",
|
"& .bold-text": {
|
||||||
|
color: "#2e7d32 !important",
|
||||||
|
},
|
||||||
|
"& .thin-text": {
|
||||||
|
color: "transparent !important",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
|
>
|
||||||
|
<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}
|
{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>
|
</Button>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
level === 1 && (
|
level === 1 && !isMobile && (
|
||||||
<Typography
|
<Typography
|
||||||
variant="caption"
|
variant="caption"
|
||||||
color="inherit"
|
color="inherit"
|
||||||
@@ -447,30 +540,87 @@ class CategoryList extends Component {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Profiler id="CategoryList" onRender={onRenderCallback}>
|
<Profiler id="CategoryList" onRender={onRenderCallback}>
|
||||||
|
{/* Desktop Menu - Hidden on xs, shown on sm and up */}
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
width: "100%",
|
width: "100%",
|
||||||
bgcolor: "primary.dark",
|
bgcolor: "primary.dark",
|
||||||
display: { xs: "none", md: "block" },
|
display: { xs: "none", sm: "block" },
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Container maxWidth="lg" sx={{ px: 2 }}>
|
<Container maxWidth="lg" sx={{ px: 2 }}>
|
||||||
{/* Level 1 Categories Row - Always shown */}
|
{/* Level 1 Categories Row - Always shown */}
|
||||||
{renderCategoryRow(level1Categories, 1)}
|
{renderCategoryRow(level1Categories, 1, false)}
|
||||||
|
|
||||||
{/* Level 2 Categories Row - Show when level 1 is selected */}
|
{/* Level 2 Categories Row - Show when level 1 is selected */}
|
||||||
|
{/* DISABLED FOR NOW
|
||||||
{level2Categories.length > 0 && (
|
{level2Categories.length > 0 && (
|
||||||
<Box sx={{ mt: 0.5 }}>
|
<Box sx={{ mt: 0.5 }}>
|
||||||
{renderCategoryRow(level2Categories, 2)}
|
{renderCategoryRow(level2Categories, 2, false)}
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Level 3 Categories Row - Show when level 2 is selected */}
|
{/* Level 3 Categories Row - Show when level 2 is selected */}
|
||||||
|
{/* DISABLED FOR NOW
|
||||||
{level3Categories.length > 0 && (
|
{level3Categories.length > 0 && (
|
||||||
<Box sx={{ mt: 0.5 }}>
|
<Box sx={{ mt: 0.5 }}>
|
||||||
{renderCategoryRow(level3Categories, 3)}
|
{renderCategoryRow(level3Categories, 3, false)}
|
||||||
</Box>
|
</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>
|
</Container>
|
||||||
</Box>
|
</Box>
|
||||||
</Profiler>
|
</Profiler>
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import SocketContext from "../../contexts/SocketContext.js";
|
|||||||
const SearchBar = () => {
|
const SearchBar = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const socket = React.useContext(SocketContext);
|
const context = React.useContext(SocketContext);
|
||||||
const searchParams = new URLSearchParams(location.search);
|
const searchParams = new URLSearchParams(location.search);
|
||||||
|
|
||||||
// State management
|
// State management
|
||||||
@@ -58,7 +58,7 @@ const SearchBar = () => {
|
|||||||
// @note Autocomplete function using getSearchProducts Socket.io API - returns objects with name and seoName
|
// @note Autocomplete function using getSearchProducts Socket.io API - returns objects with name and seoName
|
||||||
const fetchAutocomplete = React.useCallback(
|
const fetchAutocomplete = React.useCallback(
|
||||||
(query) => {
|
(query) => {
|
||||||
if (!socket || !query || query.length < 2) {
|
if (!context || !context.socket || !context.socket.connected || !query || query.length < 2) {
|
||||||
setSuggestions([]);
|
setSuggestions([]);
|
||||||
setShowSuggestions(false);
|
setShowSuggestions(false);
|
||||||
setLoadingSuggestions(false);
|
setLoadingSuggestions(false);
|
||||||
@@ -67,7 +67,7 @@ const SearchBar = () => {
|
|||||||
|
|
||||||
setLoadingSuggestions(true);
|
setLoadingSuggestions(true);
|
||||||
|
|
||||||
socket.emit(
|
context.socket.emit(
|
||||||
"getSearchProducts",
|
"getSearchProducts",
|
||||||
{
|
{
|
||||||
query: query.trim(),
|
query: query.trim(),
|
||||||
@@ -90,7 +90,7 @@ const SearchBar = () => {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
[socket]
|
[context]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleSearchChange = (e) => {
|
const handleSearchChange = (e) => {
|
||||||
@@ -219,7 +219,7 @@ const SearchBar = () => {
|
|||||||
onSubmit={handleSearch}
|
onSubmit={handleSearch}
|
||||||
sx={{
|
sx={{
|
||||||
flexGrow: 1,
|
flexGrow: 1,
|
||||||
mx: { xs: 1, sm: 2, md: 4 },
|
mx: { xs: 0, sm: 2, md: 4 },
|
||||||
position: "relative",
|
position: "relative",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -51,6 +51,9 @@ class CartTab extends Component {
|
|||||||
showStripePayment: false,
|
showStripePayment: false,
|
||||||
StripeComponent: null,
|
StripeComponent: null,
|
||||||
isLoadingStripe: false,
|
isLoadingStripe: false,
|
||||||
|
showMolliePayment: false,
|
||||||
|
MollieComponent: null,
|
||||||
|
isLoadingMollie: false,
|
||||||
showPaymentConfirmation: false,
|
showPaymentConfirmation: false,
|
||||||
orderCompleted: false,
|
orderCompleted: false,
|
||||||
originalCartItems: []
|
originalCartItems: []
|
||||||
@@ -67,8 +70,8 @@ class CartTab extends Component {
|
|||||||
|
|
||||||
// @note Add method to fetch and apply order template prefill data
|
// @note Add method to fetch and apply order template prefill data
|
||||||
fetchOrderTemplate = () => {
|
fetchOrderTemplate = () => {
|
||||||
if (this.context && this.context.connected) {
|
if (this.context && this.context.socket && this.context.socket.connected) {
|
||||||
this.context.emit('getOrderTemplate', (response) => {
|
this.context.socket.emit('getOrderTemplate', (response) => {
|
||||||
if (response.success && response.orderTemplate) {
|
if (response.success && response.orderTemplate) {
|
||||||
const template = response.orderTemplate;
|
const template = response.orderTemplate;
|
||||||
|
|
||||||
@@ -116,7 +119,7 @@ class CartTab extends Component {
|
|||||||
// Determine payment method - respect constraints
|
// Determine payment method - respect constraints
|
||||||
let prefillPaymentMethod = template.payment_method || "wire";
|
let prefillPaymentMethod = template.payment_method || "wire";
|
||||||
const paymentMethodMap = {
|
const paymentMethodMap = {
|
||||||
"credit_card": "stripe",
|
"credit_card": "mollie",//stripe
|
||||||
"bank_transfer": "wire",
|
"bank_transfer": "wire",
|
||||||
"cash_on_delivery": "onDelivery",
|
"cash_on_delivery": "onDelivery",
|
||||||
"cash": "cash"
|
"cash": "cash"
|
||||||
@@ -319,6 +322,27 @@ class CartTab extends Component {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
loadMollieComponent = async () => {
|
||||||
|
this.setState({ isLoadingMollie: true });
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { default: Mollie } = await import("../Mollie.js");
|
||||||
|
this.setState({
|
||||||
|
MollieComponent: Mollie,
|
||||||
|
showMolliePayment: true,
|
||||||
|
isCompletingOrder: false,
|
||||||
|
isLoadingMollie: false,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to load Mollie component:", error);
|
||||||
|
this.setState({
|
||||||
|
isCompletingOrder: false,
|
||||||
|
isLoadingMollie: false,
|
||||||
|
completionError: "Failed to load payment component. Please try again.",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
handleCompleteOrder = () => {
|
handleCompleteOrder = () => {
|
||||||
this.setState({ completionError: null }); // Clear previous errors
|
this.setState({ completionError: null }); // Clear previous errors
|
||||||
|
|
||||||
@@ -363,6 +387,25 @@ class CartTab extends Component {
|
|||||||
this.orderService.createStripeIntent(totalAmount, this.loadStripeComponent);
|
this.orderService.createStripeIntent(totalAmount, this.loadStripeComponent);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// Handle Mollie payment differently
|
||||||
|
if (paymentMethod === "mollie") {
|
||||||
|
// Store the cart items used for Mollie payment in sessionStorage for later reference
|
||||||
|
try {
|
||||||
|
sessionStorage.setItem('molliePaymentCart', JSON.stringify(cartItems));
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to store Mollie payment cart:", error);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate total amount for Mollie
|
||||||
|
const subtotal = cartItems.reduce(
|
||||||
|
(total, item) => total + item.price * item.quantity,
|
||||||
|
0
|
||||||
|
);
|
||||||
|
const totalAmount = Math.round((subtotal + deliveryCost) * 100); // Convert to cents
|
||||||
|
|
||||||
|
this.orderService.createMollieIntent(totalAmount, this.loadMollieComponent);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Handle regular orders
|
// Handle regular orders
|
||||||
const orderData = {
|
const orderData = {
|
||||||
@@ -398,6 +441,9 @@ class CartTab extends Component {
|
|||||||
showStripePayment,
|
showStripePayment,
|
||||||
StripeComponent,
|
StripeComponent,
|
||||||
isLoadingStripe,
|
isLoadingStripe,
|
||||||
|
showMolliePayment,
|
||||||
|
MollieComponent,
|
||||||
|
isLoadingMollie,
|
||||||
showPaymentConfirmation,
|
showPaymentConfirmation,
|
||||||
orderCompleted,
|
orderCompleted,
|
||||||
} = this.state;
|
} = this.state;
|
||||||
@@ -409,7 +455,7 @@ class CartTab extends Component {
|
|||||||
const displayError = completionError || preSubmitError;
|
const displayError = completionError || preSubmitError;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box sx={{ p: 3 }}>
|
<Box sx={{ p: { xs: 1, sm: 3 } }}>
|
||||||
{/* Payment Confirmation */}
|
{/* Payment Confirmation */}
|
||||||
{showPaymentConfirmation && (
|
{showPaymentConfirmation && (
|
||||||
<PaymentConfirmationDialog
|
<PaymentConfirmationDialog
|
||||||
@@ -433,8 +479,8 @@ class CartTab extends Component {
|
|||||||
{!showPaymentConfirmation && (
|
{!showPaymentConfirmation && (
|
||||||
<CartDropdown
|
<CartDropdown
|
||||||
cartItems={cartItems}
|
cartItems={cartItems}
|
||||||
socket={this.context}
|
socket={this.context.socket}
|
||||||
showDetailedSummary={showStripePayment}
|
showDetailedSummary={showStripePayment || showMolliePayment}
|
||||||
deliveryMethod={deliveryMethod}
|
deliveryMethod={deliveryMethod}
|
||||||
deliveryCost={deliveryCost}
|
deliveryCost={deliveryCost}
|
||||||
/>
|
/>
|
||||||
@@ -442,7 +488,7 @@ class CartTab extends Component {
|
|||||||
|
|
||||||
{cartItems.length > 0 && (
|
{cartItems.length > 0 && (
|
||||||
<Box sx={{ mt: 3 }}>
|
<Box sx={{ mt: 3 }}>
|
||||||
{isLoadingStripe ? (
|
{isLoadingStripe || isLoadingMollie ? (
|
||||||
<Box sx={{ textAlign: 'center', py: 4 }}>
|
<Box sx={{ textAlign: 'center', py: 4 }}>
|
||||||
<Typography variant="body1">
|
<Typography variant="body1">
|
||||||
Zahlungskomponente wird geladen...
|
Zahlungskomponente wird geladen...
|
||||||
@@ -468,6 +514,26 @@ class CartTab extends Component {
|
|||||||
</Box>
|
</Box>
|
||||||
<StripeComponent clientSecret={stripeClientSecret} />
|
<StripeComponent clientSecret={stripeClientSecret} />
|
||||||
</>
|
</>
|
||||||
|
) : showMolliePayment && MollieComponent ? (
|
||||||
|
<>
|
||||||
|
<Box sx={{ mb: 2 }}>
|
||||||
|
<Button
|
||||||
|
variant="outlined"
|
||||||
|
onClick={() => this.setState({ showMolliePayment: false })}
|
||||||
|
sx={{
|
||||||
|
color: '#2e7d32',
|
||||||
|
borderColor: '#2e7d32',
|
||||||
|
'&:hover': {
|
||||||
|
backgroundColor: 'rgba(46, 125, 50, 0.04)',
|
||||||
|
borderColor: '#1b5e20'
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
← Zurück zur Bestellung
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
<MollieComponent />
|
||||||
|
</>
|
||||||
) : (
|
) : (
|
||||||
<CheckoutForm
|
<CheckoutForm
|
||||||
paymentMethod={paymentMethod}
|
paymentMethod={paymentMethod}
|
||||||
|
|||||||
@@ -120,7 +120,7 @@ class OrderProcessingService {
|
|||||||
|
|
||||||
// If socket is ready, process immediately
|
// If socket is ready, process immediately
|
||||||
const context = this.getContext();
|
const context = this.getContext();
|
||||||
if (context && context.connected) {
|
if (context && context.socket && context.socket.connected) {
|
||||||
const { isLoggedIn: isAuthenticated } = isUserLoggedIn();
|
const { isLoggedIn: isAuthenticated } = isUserLoggedIn();
|
||||||
if (isAuthenticated) {
|
if (isAuthenticated) {
|
||||||
this.sendStripeOrder();
|
this.sendStripeOrder();
|
||||||
@@ -131,7 +131,7 @@ class OrderProcessingService {
|
|||||||
// Wait for socket to be ready
|
// Wait for socket to be ready
|
||||||
this.socketHandler = () => {
|
this.socketHandler = () => {
|
||||||
const context = this.getContext();
|
const context = this.getContext();
|
||||||
if (context && context.connected) {
|
if (context && context.socket && context.socket.connected) {
|
||||||
const { isLoggedIn: isAuthenticated } = isUserLoggedIn();
|
const { isLoggedIn: isAuthenticated } = isUserLoggedIn();
|
||||||
const state = this.getState();
|
const state = this.getState();
|
||||||
|
|
||||||
@@ -189,7 +189,7 @@ class OrderProcessingService {
|
|||||||
|
|
||||||
// Emit stripe order to backend via socket.io
|
// Emit stripe order to backend via socket.io
|
||||||
const context = this.getContext();
|
const context = this.getContext();
|
||||||
context.emit("issueStripeOrder", orderData, (response) => {
|
context.socket.emit("issueStripeOrder", orderData, (response) => {
|
||||||
if (response.success) {
|
if (response.success) {
|
||||||
this.setState({
|
this.setState({
|
||||||
isCompletingOrder: false,
|
isCompletingOrder: false,
|
||||||
@@ -208,8 +208,8 @@ class OrderProcessingService {
|
|||||||
// Process regular (non-Stripe) orders
|
// Process regular (non-Stripe) orders
|
||||||
processRegularOrder(orderData) {
|
processRegularOrder(orderData) {
|
||||||
const context = this.getContext();
|
const context = this.getContext();
|
||||||
if (context) {
|
if (context && context.socket && context.socket.connected) {
|
||||||
context.emit("issueOrder", orderData, (response) => {
|
context.socket.emit("issueOrder", orderData, (response) => {
|
||||||
if (response.success) {
|
if (response.success) {
|
||||||
// Clear the cart
|
// Clear the cart
|
||||||
window.cart = [];
|
window.cart = [];
|
||||||
@@ -246,8 +246,8 @@ class OrderProcessingService {
|
|||||||
// Create Stripe payment intent
|
// Create Stripe payment intent
|
||||||
createStripeIntent(totalAmount, loadStripeComponent) {
|
createStripeIntent(totalAmount, loadStripeComponent) {
|
||||||
const context = this.getContext();
|
const context = this.getContext();
|
||||||
if (context) {
|
if (context && context.socket && context.socket.connected) {
|
||||||
context.emit(
|
context.socket.emit(
|
||||||
"createStripeIntent",
|
"createStripeIntent",
|
||||||
{ amount: totalAmount },
|
{ amount: totalAmount },
|
||||||
(response) => {
|
(response) => {
|
||||||
@@ -270,6 +270,10 @@ class OrderProcessingService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Create Mollie payment intent
|
||||||
|
createMollieIntent(totalAmount, loadMollieComponent) {
|
||||||
|
loadMollieComponent();
|
||||||
|
}
|
||||||
|
|
||||||
// Calculate delivery cost
|
// Calculate delivery cost
|
||||||
getDeliveryCost() {
|
getDeliveryCost() {
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ const OrdersTab = ({ orderIdFromHash }) => {
|
|||||||
const [selectedOrder, setSelectedOrder] = useState(null);
|
const [selectedOrder, setSelectedOrder] = useState(null);
|
||||||
const [isDetailsDialogOpen, setIsDetailsDialogOpen] = useState(false);
|
const [isDetailsDialogOpen, setIsDetailsDialogOpen] = useState(false);
|
||||||
|
|
||||||
const socket = useContext(SocketContext);
|
const {socket} = useContext(SocketContext);
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const handleViewDetails = useCallback(
|
const handleViewDetails = useCallback(
|
||||||
@@ -139,7 +139,7 @@ const OrdersTab = ({ orderIdFromHash }) => {
|
|||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<Box sx={{ p: 3, display: "flex", justifyContent: "center" }}>
|
<Box sx={{ p: { xs: 1, sm: 3 }, display: "flex", justifyContent: "center" }}>
|
||||||
<CircularProgress />
|
<CircularProgress />
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
@@ -147,14 +147,14 @@ const OrdersTab = ({ orderIdFromHash }) => {
|
|||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
return (
|
return (
|
||||||
<Box sx={{ p: 3 }}>
|
<Box sx={{ p: { xs: 1, sm: 3 } }}>
|
||||||
<Alert severity="error">{error}</Alert>
|
<Alert severity="error">{error}</Alert>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box sx={{ p: 3 }}>
|
<Box sx={{ p: { xs: 1, sm: 3 } }}>
|
||||||
{orders.length > 0 ? (
|
{orders.length > 0 ? (
|
||||||
<TableContainer component={Paper}>
|
<TableContainer component={Paper}>
|
||||||
<Table>
|
<Table>
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ const PaymentMethodSelector = ({ paymentMethod, onChange, deliveryMethod, onDeli
|
|||||||
// Handle delivery method changes - auto-switch to stripe when DHL/DPD is selected
|
// Handle delivery method changes - auto-switch to stripe when DHL/DPD is selected
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if ((deliveryMethod === "DHL" || deliveryMethod === "DPD") && paymentMethod === "cash") {
|
if ((deliveryMethod === "DHL" || deliveryMethod === "DPD") && paymentMethod === "cash") {
|
||||||
handlePaymentMethodChange({ target: { value: "stripe" } });
|
handlePaymentMethodChange({ target: { value: "mollie" } });
|
||||||
}
|
}
|
||||||
}, [deliveryMethod, paymentMethod, handlePaymentMethodChange]);
|
}, [deliveryMethod, paymentMethod, handlePaymentMethodChange]);
|
||||||
|
|
||||||
@@ -42,8 +42,22 @@ const PaymentMethodSelector = ({ paymentMethod, onChange, deliveryMethod, onDeli
|
|||||||
description: "Bezahlen Sie per Banküberweisung",
|
description: "Bezahlen Sie per Banküberweisung",
|
||||||
disabled: totalAmount === 0,
|
disabled: totalAmount === 0,
|
||||||
},
|
},
|
||||||
{
|
/*{
|
||||||
id: "stripe",
|
id: "stripe",
|
||||||
|
name: "Karte oder Sofortüberweisung (Stripe)",
|
||||||
|
description: totalAmount < 0.50 && totalAmount > 0
|
||||||
|
? "Bezahlen Sie per Karte oder Sofortüberweisung (Mindestbetrag: 0,50 €)"
|
||||||
|
: "Bezahlen Sie per Karte oder Sofortüberweisung",
|
||||||
|
disabled: totalAmount < 0.50 || (deliveryMethod !== "DHL" && deliveryMethod !== "DPD" && deliveryMethod !== "Abholung"),
|
||||||
|
icons: [
|
||||||
|
"/assets/images/giropay.png",
|
||||||
|
"/assets/images/maestro.png",
|
||||||
|
"/assets/images/mastercard.png",
|
||||||
|
"/assets/images/visa_electron.png",
|
||||||
|
],
|
||||||
|
},*/
|
||||||
|
{
|
||||||
|
id: "mollie",
|
||||||
name: "Karte oder Sofortüberweisung",
|
name: "Karte oder Sofortüberweisung",
|
||||||
description: totalAmount < 0.50 && totalAmount > 0
|
description: totalAmount < 0.50 && totalAmount > 0
|
||||||
? "Bezahlen Sie per Karte oder Sofortüberweisung (Mindestbetrag: 0,50 €)"
|
? "Bezahlen Sie per Karte oder Sofortüberweisung (Mindestbetrag: 0,50 €)"
|
||||||
|
|||||||
@@ -235,8 +235,8 @@ class SettingsTab extends Component {
|
|||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<Box sx={{ p: 3 }}>
|
<Box sx={{ p: { xs: 1, sm: 3 } }}>
|
||||||
<Paper sx={{ p: 3}}>
|
<Paper sx={{ p: { xs: 2, sm: 3 } }}>
|
||||||
<Typography variant="subtitle1" fontWeight="bold" gutterBottom>
|
<Typography variant="subtitle1" fontWeight="bold" gutterBottom>
|
||||||
Passwort ändern
|
Passwort ändern
|
||||||
</Typography>
|
</Typography>
|
||||||
@@ -289,7 +289,7 @@ class SettingsTab extends Component {
|
|||||||
|
|
||||||
<Divider sx={{ my: 4 }} />
|
<Divider sx={{ my: 4 }} />
|
||||||
|
|
||||||
<Paper sx={{ p: 3 }}>
|
<Paper sx={{ p: { xs: 2, sm: 3 } }}>
|
||||||
<Typography variant="subtitle1" fontWeight="bold" gutterBottom>
|
<Typography variant="subtitle1" fontWeight="bold" gutterBottom>
|
||||||
E-Mail-Adresse ändern
|
E-Mail-Adresse ändern
|
||||||
</Typography>
|
</Typography>
|
||||||
@@ -332,7 +332,7 @@ class SettingsTab extends Component {
|
|||||||
|
|
||||||
<Divider sx={{ my: 4 }} />
|
<Divider sx={{ my: 4 }} />
|
||||||
|
|
||||||
<Paper sx={{ p: 3 }}>
|
<Paper sx={{ p: { xs: 2, sm: 3 } }}>
|
||||||
<Typography variant="subtitle1" fontWeight="bold" gutterBottom>
|
<Typography variant="subtitle1" fontWeight="bold" gutterBottom>
|
||||||
API-Schlüssel
|
API-Schlüssel
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|||||||
@@ -1,24 +1,25 @@
|
|||||||
const config = {
|
const config = {
|
||||||
baseUrl: "https://seedheads.de",
|
baseUrl: "https://growheads.de",
|
||||||
apiBaseUrl: "",
|
apiBaseUrl: "",
|
||||||
googleClientId: "928121624463-jbgfdlgem22scs1k9c87ucg4ffvaik6o.apps.googleusercontent.com",
|
googleClientId: "928121624463-jbgfdlgem22scs1k9c87ucg4ffvaik6o.apps.googleusercontent.com",
|
||||||
stripePublishableKey: "pk_test_51R7lltRtpe3h1vwJzIrDb5bcEigTLBHrtqj9SiPX7FOEATSuD6oJmKc8xpNp49ShpGJZb2GShHIUqj4zlSIz4olj00ipOuOAnu",
|
stripePublishableKey: "pk_test_51R7lltRtpe3h1vwJzIrDb5bcEigTLBHrtqj9SiPX7FOEATSuD6oJmKc8xpNp49ShpGJZb2GShHIUqj4zlSIz4olj00ipOuOAnu",
|
||||||
|
mollieProfileKey: "pfl_AtcRTimCff",
|
||||||
|
|
||||||
// SEO and Business Information
|
// SEO and Business Information
|
||||||
siteName: "SeedHeads.de",
|
siteName: "Growheads.de",
|
||||||
brandName: "SeedHeads",
|
brandName: "GrowHeads",
|
||||||
currency: "EUR",
|
currency: "EUR",
|
||||||
language: "de-DE",
|
language: "de-DE",
|
||||||
country: "DE",
|
country: "DE",
|
||||||
|
|
||||||
// Shop Descriptions
|
// Shop Descriptions
|
||||||
descriptions: {
|
descriptions: {
|
||||||
short: "SeedHeads - Online-Shop für Samen, Pflanzen und Gartenbedarf",
|
short: "GrowHeads - Online-Shop für Cannanis-Samen, Stecklinge 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."
|
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
|
||||||
keywords: "Samen, Pflanzen, Gartenbedarf, Saatgut, Online-Shop, SeedHeads, Garten, Pflanzen kaufen",
|
keywords: "Seeds, Steckling, Cannabis, Biobizz, Growheads",
|
||||||
|
|
||||||
// Shipping
|
// Shipping
|
||||||
shipping: {
|
shipping: {
|
||||||
|
|||||||
@@ -151,16 +151,16 @@ const Home = () => {
|
|||||||
const [rootCategories, setRootCategories] = useState(() =>
|
const [rootCategories, setRootCategories] = useState(() =>
|
||||||
initializeCategories()
|
initializeCategories()
|
||||||
);
|
);
|
||||||
const socket = useContext(SocketContext);
|
const context = useContext(SocketContext);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Only fetch from socket if we don't already have categories and we're in browser
|
// Only fetch from socket if we don't already have categories and we're in browser
|
||||||
if (
|
if (
|
||||||
rootCategories.length === 0 &&
|
rootCategories.length === 0 &&
|
||||||
socket &&
|
context && context.socket && context.socket.connected &&
|
||||||
typeof window !== "undefined"
|
typeof window !== "undefined"
|
||||||
) {
|
) {
|
||||||
socket.emit("categoryList", { categoryId: 209 }, (response) => {
|
context.socket.emit("categoryList", { categoryId: 209 }, (response) => {
|
||||||
if (response && response.categoryTree) {
|
if (response && response.categoryTree) {
|
||||||
// Store in cache
|
// Store in cache
|
||||||
try {
|
try {
|
||||||
@@ -176,7 +176,7 @@ const Home = () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [socket, rootCategories.length]);
|
}, [context, context?.socket?.connected, rootCategories.length]);
|
||||||
|
|
||||||
// Filter categories (excluding specific IDs)
|
// Filter categories (excluding specific IDs)
|
||||||
const filteredCategories = rootCategories.filter(
|
const filteredCategories = rootCategories.filter(
|
||||||
@@ -407,7 +407,7 @@ const Home = () => {
|
|||||||
color: "text.primary",
|
color: "text.primary",
|
||||||
borderRadius: 2,
|
borderRadius: 2,
|
||||||
overflow: "hidden",
|
overflow: "hidden",
|
||||||
height: { xs: 250, sm: 300 },
|
height: { xs: 150, sm: 200, md: 300 },
|
||||||
display: "flex",
|
display: "flex",
|
||||||
flexDirection: "column",
|
flexDirection: "column",
|
||||||
transition: "all 0.3s ease",
|
transition: "all 0.3s ease",
|
||||||
@@ -468,7 +468,7 @@ const Home = () => {
|
|||||||
color: "text.primary",
|
color: "text.primary",
|
||||||
borderRadius: 2,
|
borderRadius: 2,
|
||||||
overflow: "hidden",
|
overflow: "hidden",
|
||||||
height: { xs: 250, sm: 300 },
|
height: { xs: 150, sm: 200, md: 300 },
|
||||||
display: "flex",
|
display: "flex",
|
||||||
flexDirection: "column",
|
flexDirection: "column",
|
||||||
boxShadow: 10,
|
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 (
|
return (
|
||||||
<Container maxWidth="md" sx={{ py: 4 }}>
|
<Container maxWidth="md" sx={{ py: { xs: 0, sm: 4 }, px: { xs: 0, sm: 3 } }}>
|
||||||
<Paper elevation={2} sx={{ borderRadius: 2, overflow: 'hidden' }}>
|
<Paper elevation={{ xs: 0, sm: 2 }} sx={{ borderRadius: { xs: 0, sm: 2 }, overflow: 'hidden' }}>
|
||||||
<Box sx={{ bgcolor: '#2e7d32', p: 3, color: 'white' }}>
|
<Box sx={{ bgcolor: '#2e7d32', p: { xs: 2, sm: 3 }, color: 'white' }}>
|
||||||
<Typography variant="h5" fontWeight="bold">
|
<Typography variant="h5" fontWeight="bold">
|
||||||
Mein Profil
|
{window.innerWidth < 600 ?
|
||||||
|
(tabValue === 0 ? 'Bestellabschluss' :
|
||||||
|
tabValue === 1 ? 'Bestellungen' :
|
||||||
|
tabValue === 2 ? 'Einstellungen' : 'Mein Profil')
|
||||||
|
: 'Mein Profil'
|
||||||
|
}
|
||||||
</Typography>
|
</Typography>
|
||||||
{user && (
|
{user && (
|
||||||
<Typography variant="body1" sx={{ mt: 1 }}>
|
<Typography variant="body1" sx={{ mt: 1 }}>
|
||||||
@@ -185,7 +190,11 @@ const ProfilePage = (props) => {
|
|||||||
value={tabValue}
|
value={tabValue}
|
||||||
onChange={handleTabChange}
|
onChange={handleTabChange}
|
||||||
variant="fullWidth"
|
variant="fullWidth"
|
||||||
sx={{ borderBottom: 1, borderColor: 'divider' }}
|
sx={{
|
||||||
|
borderBottom: 1,
|
||||||
|
borderColor: 'divider',
|
||||||
|
display: { xs: 'none', sm: 'flex' }
|
||||||
|
}}
|
||||||
TabIndicatorProps={{
|
TabIndicatorProps={{
|
||||||
style: { backgroundColor: '#2e7d32' }
|
style: { backgroundColor: '#2e7d32' }
|
||||||
}}
|
}}
|
||||||
@@ -225,8 +234,8 @@ const ProfilePage = (props) => {
|
|||||||
|
|
||||||
// Wrap with socket context
|
// Wrap with socket context
|
||||||
const ProfilePageWithSocket = (props) => {
|
const ProfilePageWithSocket = (props) => {
|
||||||
const socket = useContext(SocketContext);
|
const {socket,socketB} = useContext(SocketContext);
|
||||||
return <ProfilePage {...props} socket={socket} />;
|
return <ProfilePage {...props} socket={socket} socketB={socketB} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ProfilePageWithSocket;
|
export default ProfilePageWithSocket;
|
||||||
@@ -33,10 +33,43 @@ const collectAllCategories = (categoryNode, categories = [], level = 0) => {
|
|||||||
return categories;
|
return categories;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Check for cached data - handle both browser and prerender environments
|
||||||
|
const getProductCache = () => {
|
||||||
|
if (typeof window !== "undefined" && window.productCache) {
|
||||||
|
return window.productCache;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
typeof global !== "undefined" &&
|
||||||
|
global.window &&
|
||||||
|
global.window.productCache
|
||||||
|
) {
|
||||||
|
return global.window.productCache;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Initialize categories from cache if available (for prerendering)
|
||||||
|
const initializeCategories = () => {
|
||||||
|
const productCache = getProductCache();
|
||||||
|
|
||||||
|
if (productCache && productCache["categoryTree_209"]) {
|
||||||
|
const cached = productCache["categoryTree_209"];
|
||||||
|
const cacheAge = Date.now() - cached.timestamp;
|
||||||
|
const tenMinutes = 10 * 60 * 1000;
|
||||||
|
if (cacheAge < tenMinutes && cached.categoryTree) {
|
||||||
|
return collectAllCategories(cached.categoryTree);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
};
|
||||||
|
|
||||||
const Sitemap = () => {
|
const Sitemap = () => {
|
||||||
const [categories, setCategories] = useState([]);
|
// Initialize categories and loading state together
|
||||||
const [loading, setLoading] = useState(true);
|
const initialCategories = initializeCategories();
|
||||||
const socket = useContext(SocketContext);
|
const [categories, setCategories] = useState(initialCategories);
|
||||||
|
const [loading, setLoading] = useState(initialCategories.length === 0);
|
||||||
|
const context = useContext(SocketContext);
|
||||||
|
|
||||||
|
|
||||||
const sitemapLinks = [
|
const sitemapLinks = [
|
||||||
{ title: 'Startseite', url: '/' },
|
{ title: 'Startseite', url: '/' },
|
||||||
@@ -52,8 +85,14 @@ const Sitemap = () => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchCategories = () => {
|
const fetchCategories = () => {
|
||||||
// Try cache first
|
// If we already have categories from prerendering, we're done
|
||||||
if (window.productCache && window.productCache['categoryTree_209']) {
|
if (categories.length > 0) {
|
||||||
|
setLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try cache first (for browser environment)
|
||||||
|
if (typeof window !== "undefined" && window.productCache && window.productCache['categoryTree_209']) {
|
||||||
const cached = window.productCache['categoryTree_209'];
|
const cached = window.productCache['categoryTree_209'];
|
||||||
const cacheAge = Date.now() - cached.timestamp;
|
const cacheAge = Date.now() - cached.timestamp;
|
||||||
const tenMinutes = 10 * 60 * 1000;
|
const tenMinutes = 10 * 60 * 1000;
|
||||||
@@ -65,9 +104,9 @@ const Sitemap = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Otherwise, fetch from socket if available
|
// Otherwise, fetch from socket if available (only in browser)
|
||||||
if (socket) {
|
if (context && context.socket && context.socket.connected && typeof window !== "undefined") {
|
||||||
socket.emit('categoryList', { categoryId: 209 }, (response) => {
|
context.socket.emit('categoryList', { categoryId: 209 }, (response) => {
|
||||||
if (response && response.categoryTree) {
|
if (response && response.categoryTree) {
|
||||||
// Store in cache
|
// Store in cache
|
||||||
try {
|
try {
|
||||||
@@ -94,7 +133,7 @@ const Sitemap = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
fetchCategories();
|
fetchCategories();
|
||||||
}, [socket]);
|
}, [context, categories.length]);
|
||||||
|
|
||||||
const content = (
|
const content = (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -7,8 +7,10 @@ class SocketProvider extends Component {
|
|||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
this.socket = null;
|
this.socket = null;
|
||||||
|
this.socketB = null;
|
||||||
this.state = {
|
this.state = {
|
||||||
connected: false,
|
connected: false,
|
||||||
|
connectedB: false,
|
||||||
showPrerenderFallback: true,
|
showPrerenderFallback: true,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -77,6 +79,66 @@ class SocketProvider extends Component {
|
|||||||
console.error("SocketProvider: Failed to reconnect");
|
console.error("SocketProvider: Failed to reconnect");
|
||||||
this.handleConnectionFailure();
|
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() {
|
handleConnectionFailure() {
|
||||||
@@ -96,6 +158,10 @@ class SocketProvider extends Component {
|
|||||||
console.log("SocketProvider: Disconnecting socket");
|
console.log("SocketProvider: Disconnecting socket");
|
||||||
this.socket.disconnect();
|
this.socket.disconnect();
|
||||||
}
|
}
|
||||||
|
if (this.socketB) {
|
||||||
|
console.log("SocketProvider: Disconnecting socketB");
|
||||||
|
this.socketB.disconnect();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
@@ -104,7 +170,7 @@ class SocketProvider extends Component {
|
|||||||
window.__PRERENDER_FALLBACK__;
|
window.__PRERENDER_FALLBACK__;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SocketContext.Provider value={this.socket}>
|
<SocketContext.Provider value={{socket:this.socket,socketB:this.socketB}}>
|
||||||
{/* Always render children but control visibility */}
|
{/* Always render children but control visibility */}
|
||||||
<div style={{ display: this.state.connected ? 'block' : 'none' }}>
|
<div style={{ display: this.state.connected ? 'block' : 'none' }}>
|
||||||
{this.props.children}
|
{this.props.children}
|
||||||
|
|||||||
@@ -314,7 +314,11 @@ export default {
|
|||||||
hot: true,
|
hot: true,
|
||||||
port: 9500,
|
port: 9500,
|
||||||
open: false,
|
open: false,
|
||||||
historyApiFallback: true,
|
historyApiFallback: {
|
||||||
|
index: '/index.html',
|
||||||
|
disableDotRule: true,
|
||||||
|
htmlAcceptHeaders: ['text/html', 'application/xhtml+xml']
|
||||||
|
},
|
||||||
client: {
|
client: {
|
||||||
logging: 'verbose',
|
logging: 'verbose',
|
||||||
overlay: {
|
overlay: {
|
||||||
|
|||||||
Reference in New Issue
Block a user