Compare commits
30 Commits
e0c6d47d98
...
live
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
66a1efd87b | ||
|
|
2c0b7aa84d | ||
|
|
a56377a1fd | ||
|
|
468eb1c3ae | ||
|
|
e699a8003f | ||
|
|
b5256d6597 | ||
|
|
18c528302d | ||
|
|
9054c8d2fd | ||
|
|
8bce10e61b | ||
|
|
2540d00c8e | ||
|
|
52c9888a6a | ||
|
|
ab55761411 | ||
|
|
5e5a733d36 | ||
|
|
36360df648 | ||
|
|
21d86565f1 | ||
|
|
c503de3a11 | ||
|
|
2ced182570 | ||
|
|
52c62541b0 | ||
|
|
7202c43dfa | ||
|
|
5b7f0f788c | ||
|
|
47ed2ec231 | ||
|
|
188c883450 | ||
|
|
ba66b82b2b | ||
|
|
defe3c9521 | ||
|
|
de8e59f1bb | ||
|
|
4b634414e5 | ||
|
|
e8517372f2 | ||
|
|
c6ea6e70fe | ||
|
|
d37eb950d1 | ||
|
|
665e48e868 |
1
docs/README.md
Normal file
1
docs/README.md
Normal file
@@ -0,0 +1 @@
|
|||||||
|
src/components/MainPageLayout.js is the Main gues homepage.
|
||||||
@@ -83,7 +83,7 @@ server {
|
|||||||
default_type application/xml;
|
default_type application/xml;
|
||||||
}
|
}
|
||||||
|
|
||||||
location ~ ^/(datenschutz|impressum|batteriegesetzhinweise|widerrufsrecht|sitemap|agb|Kategorien|Konfigurator|404|profile|resetPassword|thc-test|filiale|aktionen|presseverleih|payment/success)(/|$) {
|
location ~ ^/(datenschutz|impressum|batteriegesetzhinweise|widerrufsrecht|sitemap|agb|Kategorien|Konfigurator|404|profile|resetPassword|thc-test|linkTelegram|filiale|aktionen|presseverleih|payment/success)(/|$) {
|
||||||
types {}
|
types {}
|
||||||
default_type text/html;
|
default_type text/html;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,30 @@ import babelParser from '@babel/eslint-parser';
|
|||||||
|
|
||||||
export default [
|
export default [
|
||||||
js.configs.recommended,
|
js.configs.recommended,
|
||||||
|
{
|
||||||
|
files: ['**/*.cjs'],
|
||||||
|
languageOptions: {
|
||||||
|
ecmaVersion: 2022,
|
||||||
|
sourceType: 'commonjs',
|
||||||
|
globals: {
|
||||||
|
...globals.node,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
'no-unused-vars': 'warn',
|
||||||
|
//'no-console': 'warn',
|
||||||
|
'no-debugger': 'warn',
|
||||||
|
'no-alert': 'warn',
|
||||||
|
'no-unused-expressions': 'warn',
|
||||||
|
'no-var': 'warn',
|
||||||
|
'prefer-const': 'warn',
|
||||||
|
'no-trailing-spaces': 'warn',
|
||||||
|
'eqeqeq': ['warn', 'always'],
|
||||||
|
'no-empty': 'warn',
|
||||||
|
'no-eval': 'warn',
|
||||||
|
'no-script-url': 'warn',
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
files: ['**/*.{js,jsx}'],
|
files: ['**/*.{js,jsx}'],
|
||||||
languageOptions: {
|
languageOptions: {
|
||||||
|
|||||||
10
package-lock.json
generated
10
package-lock.json
generated
@@ -11,6 +11,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@emotion/react": "^11.14.0",
|
"@emotion/react": "^11.14.0",
|
||||||
"@emotion/styled": "^11.14.0",
|
"@emotion/styled": "^11.14.0",
|
||||||
|
"@fontsource-variable/outfit": "^5.2.8",
|
||||||
"@mui/icons-material": "^7.1.1",
|
"@mui/icons-material": "^7.1.1",
|
||||||
"@mui/material": "^7.1.1",
|
"@mui/material": "^7.1.1",
|
||||||
"@stripe/react-stripe-js": "^3.7.0",
|
"@stripe/react-stripe-js": "^3.7.0",
|
||||||
@@ -2222,6 +2223,15 @@
|
|||||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@fontsource-variable/outfit": {
|
||||||
|
"version": "5.2.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@fontsource-variable/outfit/-/outfit-5.2.8.tgz",
|
||||||
|
"integrity": "sha512-4oUDCZx/Tcz6HZP423w/niqEH31Gks5IsqHV2ZZz1qKHaVIZdj2f0/S1IK2n8jl6Xo0o3N+3RjNHlV9R73ozQA==",
|
||||||
|
"license": "OFL-1.1",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ayuhito"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@humanfs/core": {
|
"node_modules/@humanfs/core": {
|
||||||
"version": "0.19.1",
|
"version": "0.19.1",
|
||||||
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
|
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
|
||||||
|
|||||||
@@ -30,6 +30,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@emotion/react": "^11.14.0",
|
"@emotion/react": "^11.14.0",
|
||||||
"@emotion/styled": "^11.14.0",
|
"@emotion/styled": "^11.14.0",
|
||||||
|
"@fontsource-variable/outfit": "^5.2.8",
|
||||||
"@mui/icons-material": "^7.1.1",
|
"@mui/icons-material": "^7.1.1",
|
||||||
"@mui/material": "^7.1.1",
|
"@mui/material": "^7.1.1",
|
||||||
"@stripe/react-stripe-js": "^3.7.0",
|
"@stripe/react-stripe-js": "^3.7.0",
|
||||||
|
|||||||
@@ -66,12 +66,7 @@ const renderSingleProduct = async (productSeoName) => {
|
|||||||
const socketUrl = "http://127.0.0.1:9303";
|
const socketUrl = "http://127.0.0.1:9303";
|
||||||
console.log(`🔌 Connecting to socket at ${socketUrl}...`);
|
console.log(`🔌 Connecting to socket at ${socketUrl}...`);
|
||||||
|
|
||||||
const socket = io(socketUrl, {
|
const socket = io(socketUrl, config.socketIoClientOptions);
|
||||||
path: "/socket.io/",
|
|
||||||
transports: ["websocket"],
|
|
||||||
reconnection: false,
|
|
||||||
timeout: 10000,
|
|
||||||
});
|
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const timeout = setTimeout(() => {
|
const timeout = setTimeout(() => {
|
||||||
|
|||||||
@@ -125,11 +125,14 @@ const {
|
|||||||
const {
|
const {
|
||||||
generateProductMetaTags,
|
generateProductMetaTags,
|
||||||
generateProductJsonLd,
|
generateProductJsonLd,
|
||||||
|
generateCategoryMetaTags,
|
||||||
generateCategoryJsonLd,
|
generateCategoryJsonLd,
|
||||||
generateHomepageMetaTags,
|
generateHomepageMetaTags,
|
||||||
generateHomepageJsonLd,
|
generateHomepageJsonLd,
|
||||||
generateSitemapJsonLd,
|
generateSitemapJsonLd,
|
||||||
generateKonfiguratorMetaTags,
|
generateKonfiguratorMetaTags,
|
||||||
|
generateHerstellerMetaTags,
|
||||||
|
generateHerstellerJsonLd,
|
||||||
generateXmlSitemap,
|
generateXmlSitemap,
|
||||||
generateRobotsTxt,
|
generateRobotsTxt,
|
||||||
generateProductsXml,
|
generateProductsXml,
|
||||||
@@ -141,6 +144,7 @@ const {
|
|||||||
const {
|
const {
|
||||||
fetchCategoryProducts,
|
fetchCategoryProducts,
|
||||||
fetchProductDetails,
|
fetchProductDetails,
|
||||||
|
fetchManufacturers,
|
||||||
saveProductImages,
|
saveProductImages,
|
||||||
saveCategoryImages,
|
saveCategoryImages,
|
||||||
} = require("./prerender/data-fetching.cjs");
|
} = require("./prerender/data-fetching.cjs");
|
||||||
@@ -160,18 +164,14 @@ 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 PrerenderSitemap = require("./src/PrerenderSitemap.js").default;
|
||||||
const PrerenderCategoriesPage = require("./src/PrerenderCategoriesPage.js").default;
|
const PrerenderCategoriesPage = require("./src/PrerenderCategoriesPage.js").default;
|
||||||
|
const PrerenderHerstellerPage = require("./src/PrerenderHerstellerPage.js").default;
|
||||||
const AGB = require("./src/pages/AGB.js").default;
|
const AGB = require("./src/pages/AGB.js").default;
|
||||||
const NotFound404 = require("./src/pages/NotFound404.js").default;
|
const NotFound404 = require("./src/pages/NotFound404.js").default;
|
||||||
|
|
||||||
// Worker function for parallel product rendering
|
// Worker function for parallel product rendering
|
||||||
const renderProductWorker = async (productSeoNames, workerId, progressCallback, categoryMap = {}) => {
|
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, config.socketIoClientOptions);
|
||||||
path: "/socket.io/",
|
|
||||||
transports: ["websocket"],
|
|
||||||
reconnection: false,
|
|
||||||
timeout: 10000,
|
|
||||||
});
|
|
||||||
|
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
let processedCount = 0;
|
let processedCount = 0;
|
||||||
@@ -380,6 +380,29 @@ const renderApp = async (categoryData, socket) => {
|
|||||||
global.categoryCache = {};
|
global.categoryCache = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fetch manufacturers data for Hersteller page
|
||||||
|
let manufacturerData = null;
|
||||||
|
console.log("🏭 [renderApp] Starting manufacturer fetch...");
|
||||||
|
console.log("🏭 [renderApp] socket exists:", !!socket);
|
||||||
|
console.log("🏭 [renderApp] socket.connected:", socket ? socket.connected : "N/A");
|
||||||
|
|
||||||
|
if (!socket) {
|
||||||
|
console.error("🏭 [renderApp] FATAL: No socket - cannot fetch manufacturers!");
|
||||||
|
} else if (!socket.connected) {
|
||||||
|
console.error("🏭 [renderApp] FATAL: Socket not connected - cannot fetch manufacturers!");
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
console.log("🏭 [renderApp] Calling fetchManufacturers...");
|
||||||
|
manufacturerData = await fetchManufacturers(socket);
|
||||||
|
console.log("🏭 [renderApp] ✅ Fetched " + manufacturerData.length + " manufacturers");
|
||||||
|
} catch (error) {
|
||||||
|
console.error("🏭 [renderApp] ❌ Failed to fetch manufacturers:", error.message);
|
||||||
|
manufacturerData = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("🏭 [renderApp] Final manufacturerData:", manufacturerData ? (manufacturerData.length + " items") : "null");
|
||||||
|
|
||||||
// Helper to call renderPage with config
|
// Helper to call renderPage with config
|
||||||
const render = (
|
const render = (
|
||||||
component,
|
component,
|
||||||
@@ -387,8 +410,10 @@ const renderApp = async (categoryData, socket) => {
|
|||||||
filename,
|
filename,
|
||||||
description,
|
description,
|
||||||
metaTags = "",
|
metaTags = "",
|
||||||
needsRouter = false
|
needsRouter = false,
|
||||||
|
manufacturerDataForPage = null
|
||||||
) => {
|
) => {
|
||||||
|
console.log(" 📦 [render helper] Calling renderPage for", filename, "with manufacturerData:", manufacturerDataForPage ? (manufacturerDataForPage.length + " items") : "null");
|
||||||
return renderPage(
|
return renderPage(
|
||||||
component,
|
component,
|
||||||
location,
|
location,
|
||||||
@@ -396,7 +421,10 @@ const renderApp = async (categoryData, socket) => {
|
|||||||
description,
|
description,
|
||||||
metaTags,
|
metaTags,
|
||||||
needsRouter,
|
needsRouter,
|
||||||
config
|
config,
|
||||||
|
false, // suppressLogs
|
||||||
|
null, // productData
|
||||||
|
manufacturerDataForPage // manufacturerData - 10th parameter!
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -429,6 +457,11 @@ const renderApp = async (categoryData, socket) => {
|
|||||||
const resetPasswordPath = path.resolve(__dirname, config.outputDir, "resetPassword");
|
const resetPasswordPath = path.resolve(__dirname, config.outputDir, "resetPassword");
|
||||||
fs.copyFileSync(indexPath, resetPasswordPath);
|
fs.copyFileSync(indexPath, resetPasswordPath);
|
||||||
console.log(`✅ Copied index.html to ${resetPasswordPath}`);
|
console.log(`✅ Copied index.html to ${resetPasswordPath}`);
|
||||||
|
|
||||||
|
// Copy index.html to linkTelegram (no file extension) for SPA routing
|
||||||
|
const linkTelegramPath = path.resolve(__dirname, config.outputDir, "linkTelegram");
|
||||||
|
fs.copyFileSync(indexPath, linkTelegramPath);
|
||||||
|
console.log(`✅ Copied index.html to ${linkTelegramPath}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Render static pages
|
// Render static pages
|
||||||
@@ -473,6 +506,13 @@ const renderApp = async (categoryData, socket) => {
|
|||||||
description: "Categories page",
|
description: "Categories page",
|
||||||
needsCategoryData: true,
|
needsCategoryData: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
component: PrerenderHerstellerPage,
|
||||||
|
path: "/Hersteller",
|
||||||
|
filename: "Hersteller",
|
||||||
|
description: "Hersteller page",
|
||||||
|
needsManufacturerData: true,
|
||||||
|
},
|
||||||
{ component: AGB, path: "/agb", filename: "agb", description: "AGB page" },
|
{ component: AGB, path: "/agb", filename: "agb", description: "AGB page" },
|
||||||
{ component: NotFound404, path: "/404", filename: "404", description: "404 Not Found page" },
|
{ component: NotFound404, path: "/404", filename: "404", description: "404 Not Found page" },
|
||||||
{
|
{
|
||||||
@@ -491,8 +531,17 @@ const renderApp = async (categoryData, socket) => {
|
|||||||
|
|
||||||
let staticPagesRendered = 0;
|
let staticPagesRendered = 0;
|
||||||
for (const page of staticPages) {
|
for (const page of staticPages) {
|
||||||
// Pass category data as props if needed
|
// Pass category and manufacturer data as props if needed
|
||||||
const pageProps = page.needsCategoryData ? { categoryData } : null;
|
let pageProps = null;
|
||||||
|
if (page.needsCategoryData || page.needsManufacturerData) {
|
||||||
|
pageProps = {};
|
||||||
|
if (page.needsCategoryData) {
|
||||||
|
pageProps.categoryData = categoryData;
|
||||||
|
}
|
||||||
|
if (page.needsManufacturerData) {
|
||||||
|
pageProps.manufacturerData = manufacturerData;
|
||||||
|
}
|
||||||
|
}
|
||||||
const pageComponent = React.createElement(page.component, pageProps);
|
const pageComponent = React.createElement(page.component, pageProps);
|
||||||
let metaTags = "";
|
let metaTags = "";
|
||||||
|
|
||||||
@@ -508,13 +557,25 @@ const renderApp = async (categoryData, socket) => {
|
|||||||
metaTags = konfiguratorMetaTags;
|
metaTags = konfiguratorMetaTags;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Special handling for Hersteller page to include SEO tags
|
||||||
|
if (page.filename === "Hersteller") {
|
||||||
|
const manufacturerCount = manufacturerData ? manufacturerData.length : 0;
|
||||||
|
const herstellerMetaTags = generateHerstellerMetaTags(shopConfig.baseUrl, shopConfig, manufacturerCount);
|
||||||
|
const herstellerJsonLd = generateHerstellerJsonLd(shopConfig.baseUrl, shopConfig);
|
||||||
|
metaTags = herstellerMetaTags + "\n" + herstellerJsonLd;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pass manufacturerData only for Hersteller page
|
||||||
|
const pageManufacturerData = page.needsManufacturerData ? manufacturerData : null;
|
||||||
|
|
||||||
const success = render(
|
const success = render(
|
||||||
pageComponent,
|
pageComponent,
|
||||||
page.path,
|
page.path,
|
||||||
page.filename,
|
page.filename,
|
||||||
page.description,
|
page.description,
|
||||||
metaTags,
|
metaTags,
|
||||||
true
|
true,
|
||||||
|
pageManufacturerData
|
||||||
);
|
);
|
||||||
if (success) {
|
if (success) {
|
||||||
staticPagesRendered++;
|
staticPagesRendered++;
|
||||||
@@ -621,19 +682,25 @@ const renderApp = async (categoryData, socket) => {
|
|||||||
const filename = `Kategorie/${category.seoName}`;
|
const filename = `Kategorie/${category.seoName}`;
|
||||||
const location = `/Kategorie/${category.seoName}`;
|
const location = `/Kategorie/${category.seoName}`;
|
||||||
const description = `Category "${category.name}" (ID: ${category.id})`;
|
const description = `Category "${category.name}" (ID: ${category.id})`;
|
||||||
|
const categoryMetaTags = generateCategoryMetaTags(
|
||||||
|
category,
|
||||||
|
shopConfig.baseUrl,
|
||||||
|
shopConfig
|
||||||
|
);
|
||||||
const categoryJsonLd = generateCategoryJsonLd(
|
const categoryJsonLd = generateCategoryJsonLd(
|
||||||
category,
|
category,
|
||||||
productData?.products || [],
|
productData?.products || [],
|
||||||
shopConfig.baseUrl,
|
shopConfig.baseUrl,
|
||||||
shopConfig
|
shopConfig
|
||||||
);
|
);
|
||||||
|
const combinedCategoryHead = categoryMetaTags + "\n" + categoryJsonLd;
|
||||||
|
|
||||||
const success = render(
|
const success = render(
|
||||||
categoryComponent,
|
categoryComponent,
|
||||||
location,
|
location,
|
||||||
filename,
|
filename,
|
||||||
description,
|
description,
|
||||||
categoryJsonLd,
|
combinedCategoryHead,
|
||||||
true
|
true
|
||||||
);
|
);
|
||||||
if (success) {
|
if (success) {
|
||||||
@@ -863,12 +930,7 @@ const fetchCategoryDataAndRender = () => {
|
|||||||
process.exit(1);
|
process.exit(1);
|
||||||
}, 15000);
|
}, 15000);
|
||||||
|
|
||||||
const socket = io(socketUrl, {
|
const socket = io(socketUrl, config.socketIoClientOptions);
|
||||||
path: "/socket.io/",
|
|
||||||
transports: ["websocket"],
|
|
||||||
reconnection: false,
|
|
||||||
timeout: 10000,
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.on("connect", () => {
|
socket.on("connect", () => {
|
||||||
console.log('Socket connected. Emitting "categoryList"...');
|
console.log('Socket connected. Emitting "categoryList"...');
|
||||||
|
|||||||
@@ -69,11 +69,21 @@ const globalCssCollection = new Set();
|
|||||||
// Get webpack entrypoints
|
// Get webpack entrypoints
|
||||||
const webpackEntrypoints = getWebpackEntrypoints();
|
const webpackEntrypoints = getWebpackEntrypoints();
|
||||||
|
|
||||||
|
/** Socket.IO client options for prerender scripts: skip backend connection counters (balanced on disconnect). */
|
||||||
|
const socketIoClientOptions = {
|
||||||
|
path: "/socket.io/",
|
||||||
|
transports: ["websocket"],
|
||||||
|
reconnection: false,
|
||||||
|
timeout: 10000,
|
||||||
|
auth: { prerender: true },
|
||||||
|
};
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
isProduction,
|
isProduction,
|
||||||
outputDir,
|
outputDir,
|
||||||
getWebpackEntrypoints,
|
getWebpackEntrypoints,
|
||||||
globalCss,
|
globalCss,
|
||||||
globalCssCollection,
|
globalCssCollection,
|
||||||
webpackEntrypoints
|
webpackEntrypoints,
|
||||||
|
socketIoClientOptions,
|
||||||
};
|
};
|
||||||
@@ -42,6 +42,7 @@ const fetchCategoryProducts = (socket, categoryId) => {
|
|||||||
"getCategoryProducts",
|
"getCategoryProducts",
|
||||||
{
|
{
|
||||||
full: true,
|
full: true,
|
||||||
|
nocount: true,
|
||||||
categoryId:
|
categoryId:
|
||||||
categoryId === "neu" || categoryId === "bald"
|
categoryId === "neu" || categoryId === "bald"
|
||||||
? categoryId
|
? categoryId
|
||||||
@@ -139,6 +140,38 @@ const fetchCategoryImage = (socket, categoryId) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const fetchManufacturers = (socket) => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const timeout = setTimeout(() => {
|
||||||
|
reject(new Error("Timeout fetching manufacturers"));
|
||||||
|
}, 10000);
|
||||||
|
|
||||||
|
socket.emit("getHerstellerImages", {}, (response) => {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
if (response?.success && Array.isArray(response.manufacturers)) {
|
||||||
|
// Filter and format manufacturers similar to HerstellerPage.js
|
||||||
|
const manufacturers = response.manufacturers
|
||||||
|
.filter(m => m.imageBuffer)
|
||||||
|
.map(m => ({
|
||||||
|
id: m.id,
|
||||||
|
name: m.name || '',
|
||||||
|
slug: m.slug || '',
|
||||||
|
imageBuffer: m.imageBuffer,
|
||||||
|
}))
|
||||||
|
.sort((a, b) => (a.name || '').localeCompare(b.name || '', undefined, { sensitivity: 'base' }));
|
||||||
|
|
||||||
|
resolve(manufacturers);
|
||||||
|
} else {
|
||||||
|
reject(
|
||||||
|
new Error(
|
||||||
|
`Invalid manufacturers response: ${JSON.stringify(response)}`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const saveProductImages = async (socket, products, categoryName, outputDir) => {
|
const saveProductImages = async (socket, products, categoryName, outputDir) => {
|
||||||
if (!products || products.length === 0) return;
|
if (!products || products.length === 0) return;
|
||||||
|
|
||||||
@@ -382,6 +415,7 @@ module.exports = {
|
|||||||
fetchProductDetails,
|
fetchProductDetails,
|
||||||
fetchProductImage,
|
fetchProductImage,
|
||||||
fetchCategoryImage,
|
fetchCategoryImage,
|
||||||
|
fetchManufacturers,
|
||||||
saveProductImages,
|
saveProductImages,
|
||||||
saveCategoryImages,
|
saveCategoryImages,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -18,7 +18,8 @@ const renderPage = (
|
|||||||
needsRouter = false,
|
needsRouter = false,
|
||||||
config,
|
config,
|
||||||
suppressLogs = false,
|
suppressLogs = false,
|
||||||
productData = null
|
productData = null,
|
||||||
|
manufacturerData = null
|
||||||
) => {
|
) => {
|
||||||
const {
|
const {
|
||||||
isProduction,
|
isProduction,
|
||||||
@@ -171,22 +172,44 @@ const renderPage = (
|
|||||||
</script>
|
</script>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
// @note Create script to populate window.productCache with ONLY the static category tree
|
// @note Create script to populate window.productCache with static category tree and herstellerImages
|
||||||
let productCacheScript = '';
|
let productCacheScript = '';
|
||||||
if (typeof global !== "undefined" && global.window && global.window.categoryCache) {
|
const hasCategoryCache = typeof global !== "undefined" && global.window && global.window.categoryCache;
|
||||||
// Only include the static categoryTree_209, not any dynamic data that gets added during rendering
|
const hasManufacturerData = manufacturerData && manufacturerData.length > 0;
|
||||||
const staticCache = {};
|
|
||||||
if (global.window.categoryCache["209_de"]) {
|
console.log(" 📦 [" + filename + "] manufacturerData =", manufacturerData ? (manufacturerData.length + " items") : "null");
|
||||||
staticCache["209_de"] = global.window.categoryCache["209_de"];
|
|
||||||
|
if (hasCategoryCache || hasManufacturerData) {
|
||||||
|
const cacheData = {};
|
||||||
|
|
||||||
|
// Add static categoryTree_209
|
||||||
|
if (hasCategoryCache && global.window.categoryCache["209_de"]) {
|
||||||
|
cacheData["209_de"] = global.window.categoryCache["209_de"];
|
||||||
}
|
}
|
||||||
|
|
||||||
const staticCacheData = JSON.stringify(staticCache);
|
// Add herstellerImages
|
||||||
productCacheScript = `
|
if (hasManufacturerData) {
|
||||||
<script>
|
cacheData.herstellerImages = manufacturerData;
|
||||||
// Populate window.categoryCache with static category tree only
|
}
|
||||||
window.categoryCache = ${staticCacheData};
|
|
||||||
</script>
|
const cacheDataJson = JSON.stringify(cacheData);
|
||||||
`;
|
let extraScripts = '';
|
||||||
|
|
||||||
|
if (hasCategoryCache && cacheData["209_de"]) {
|
||||||
|
const categoryCacheJson = JSON.stringify({ "209_de": cacheData["209_de"] });
|
||||||
|
extraScripts += 'window.categoryCache = ' + categoryCacheJson + ';';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasManufacturerData) {
|
||||||
|
const herstellerJson = JSON.stringify(manufacturerData);
|
||||||
|
extraScripts += 'window.herstellerImages = ' + herstellerJson + ';';
|
||||||
|
}
|
||||||
|
|
||||||
|
productCacheScript = '<script>' +
|
||||||
|
'if (!window.productCache) { window.productCache = {}; }' +
|
||||||
|
'Object.assign(window.productCache, ' + cacheDataJson + ');' +
|
||||||
|
extraScripts +
|
||||||
|
'</script>';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create script to populate window.productDetailCache for individual product pages
|
// Create script to populate window.productDetailCache for individual product pages
|
||||||
|
|||||||
@@ -1,3 +1,43 @@
|
|||||||
|
/** Safe for double-quoted HTML attributes */
|
||||||
|
const escAttr = (str) =>
|
||||||
|
String(str ?? "")
|
||||||
|
.replace(/&/g, "&")
|
||||||
|
.replace(/"/g, """)
|
||||||
|
.replace(/</g, "<");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Head tags for prerendered category URLs — explicit canonical per /Kategorie/{slug}
|
||||||
|
* so Google does not cluster different listing pages (e.g. neu vs Seeds) as duplicates.
|
||||||
|
*/
|
||||||
|
const generateCategoryMetaTags = (category, baseUrl, config) => {
|
||||||
|
const root = baseUrl.endsWith("/") ? baseUrl.slice(0, -1) : baseUrl;
|
||||||
|
const categoryUrl = `${root}/Kategorie/${category.seoName}`;
|
||||||
|
const name = category.name || `Kategorie ${category.seoName}`;
|
||||||
|
const site = config.siteName || config.brandName;
|
||||||
|
const desc = `${name} bei ${config.brandName}: Growshop-Sortiment online kaufen. Schnelle Lieferung, Laden Dresden.`;
|
||||||
|
const descShort = desc.length > 160 ? `${desc.slice(0, 157)}...` : desc;
|
||||||
|
const e = escAttr;
|
||||||
|
const logoUrl =
|
||||||
|
config.images && config.images.logo
|
||||||
|
? `${root}${config.images.logo}`
|
||||||
|
: `${root}/assets/images/nopicture.jpg`;
|
||||||
|
|
||||||
|
return `
|
||||||
|
<meta name="description" content="${e(descShort)}">
|
||||||
|
<meta property="og:title" content="${e(`${name} | ${site}`)}">
|
||||||
|
<meta property="og:description" content="${e(descShort)}">
|
||||||
|
<meta property="og:url" content="${categoryUrl}">
|
||||||
|
<meta property="og:type" content="website">
|
||||||
|
<meta property="og:image" content="${e(logoUrl)}">
|
||||||
|
<meta property="og:site_name" content="${e(site)}">
|
||||||
|
<meta name="twitter:card" content="summary_large_image">
|
||||||
|
<meta name="twitter:title" content="${e(`${name} | ${site}`)}">
|
||||||
|
<meta name="twitter:description" content="${e(descShort)}">
|
||||||
|
<meta name="robots" content="index, follow">
|
||||||
|
<link rel="canonical" href="${categoryUrl}">
|
||||||
|
`;
|
||||||
|
};
|
||||||
|
|
||||||
const generateCategoryJsonLd = (category, products = [], baseUrl, config) => {
|
const generateCategoryJsonLd = (category, products = [], baseUrl, config) => {
|
||||||
// Category IDs to skip (seeds, plants, headshop items)
|
// Category IDs to skip (seeds, plants, headshop items)
|
||||||
const skipCategoryIds = [689, 706, 709, 711, 714, 748, 749, 896, 710, 924, 923, 922, 921, 916, 278, 259, 258];
|
const skipCategoryIds = [689, 706, 709, 711, 714, 748, 749, 896, 710, 924, 923, 922, 921, 916, 278, 259, 258];
|
||||||
@@ -7,124 +47,103 @@ const generateCategoryJsonLd = (category, products = [], baseUrl, config) => {
|
|||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
const categoryUrl = `${baseUrl}/Kategorie/${category.seoName}`;
|
const root = baseUrl.endsWith("/") ? baseUrl.slice(0, -1) : baseUrl;
|
||||||
|
const categoryUrl = `${root}/Kategorie/${category.seoName}`;
|
||||||
|
|
||||||
// Calculate price valid date (current date + 3 months)
|
const id = {
|
||||||
const priceValidDate = new Date();
|
business: `${root}#business`,
|
||||||
priceValidDate.setMonth(priceValidDate.getMonth() + 3);
|
website: `${root}#website`,
|
||||||
const priceValidUntil = priceValidDate.toISOString().split("T")[0];
|
breadcrumb: `${categoryUrl}#breadcrumb`,
|
||||||
|
itemList: `${categoryUrl}#itemlist`,
|
||||||
|
};
|
||||||
|
|
||||||
const jsonLd = {
|
const logoUrl =
|
||||||
"@context": "https://schema.org/",
|
config.images && config.images.logo
|
||||||
|
? `${root}${config.images.logo}`
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
const businessNode = {
|
||||||
|
"@id": id.business,
|
||||||
|
"@type": ["GardenStore", "LocalBusiness", "Organization"],
|
||||||
|
name: config.brandName,
|
||||||
|
url: root,
|
||||||
|
...(logoUrl && {
|
||||||
|
logo: { "@type": "ImageObject", url: logoUrl },
|
||||||
|
image: { "@type": "ImageObject", url: logoUrl },
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
const websiteNode = {
|
||||||
|
"@id": id.website,
|
||||||
|
"@type": "WebSite",
|
||||||
|
name: config.siteName || config.brandName,
|
||||||
|
url: root,
|
||||||
|
publisher: { "@id": id.business },
|
||||||
|
};
|
||||||
|
|
||||||
|
const breadcrumbNode = {
|
||||||
|
"@id": id.breadcrumb,
|
||||||
|
"@type": "BreadcrumbList",
|
||||||
|
itemListElement: [
|
||||||
|
{
|
||||||
|
"@type": "ListItem",
|
||||||
|
position: 1,
|
||||||
|
name: "Home",
|
||||||
|
item: root,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"@type": "ListItem",
|
||||||
|
position: 2,
|
||||||
|
name: category.name,
|
||||||
|
item: categoryUrl,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const collectionPageNode = {
|
||||||
|
"@id": categoryUrl,
|
||||||
"@type": "CollectionPage",
|
"@type": "CollectionPage",
|
||||||
name: category.name,
|
name: category.name,
|
||||||
url: categoryUrl,
|
url: categoryUrl,
|
||||||
description: `${category.name} - Entdecken Sie unsere Auswahl an hochwertigen Produkten`,
|
description: `${category.name} - Entdecken Sie unsere Auswahl an hochwertigen Produkten`,
|
||||||
breadcrumb: {
|
isPartOf: { "@id": id.website },
|
||||||
"@type": "BreadcrumbList",
|
breadcrumb: { "@id": id.breadcrumb },
|
||||||
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
|
const graph = [businessNode, websiteNode, breadcrumbNode, collectionPageNode];
|
||||||
if (products && products.length > 0) {
|
|
||||||
jsonLd.mainEntity = {
|
// ItemList: URLs only — full Product/Offer markup belongs on each /Artikel/… page (Google guidelines).
|
||||||
|
const withUrls = (products || []).filter((p) => p && p.seoName);
|
||||||
|
if (withUrls.length > 0) {
|
||||||
|
collectionPageNode.mainEntity = { "@id": id.itemList };
|
||||||
|
|
||||||
|
graph.push({
|
||||||
|
"@id": id.itemList,
|
||||||
"@type": "ItemList",
|
"@type": "ItemList",
|
||||||
numberOfItems: products.length,
|
numberOfItems: withUrls.length,
|
||||||
itemListElement: products.slice(0, 20).map((product, index) => ({
|
itemListElement: withUrls.map((product, index) => {
|
||||||
"@type": "ListItem",
|
const productPageUrl = `${root}/Artikel/${product.seoName}`;
|
||||||
position: index + 1,
|
return {
|
||||||
item: {
|
"@type": "ListItem",
|
||||||
"@type": "Product",
|
position: index + 1,
|
||||||
name: product.name,
|
url: productPageUrl,
|
||||||
url: `${baseUrl}/Artikel/${product.seoName}`,
|
item: productPageUrl,
|
||||||
image:
|
};
|
||||||
product.pictureList && product.pictureList.trim()
|
}),
|
||||||
? `${baseUrl}/assets/images/prod${product.pictureList
|
});
|
||||||
.split(",")[0]
|
|
||||||
.trim()}.avif`
|
|
||||||
: `${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,
|
|
||||||
priceValidUntil: priceValidUntil,
|
|
||||||
availability: product.available
|
|
||||||
? "https://schema.org/InStock"
|
|
||||||
: "https://schema.org/OutOfStock",
|
|
||||||
seller: {
|
|
||||||
"@type": "Organization",
|
|
||||||
name: config.brandName,
|
|
||||||
},
|
|
||||||
itemCondition: "https://schema.org/NewCondition",
|
|
||||||
hasMerchantReturnPolicy: {
|
|
||||||
"@type": "MerchantReturnPolicy",
|
|
||||||
applicableCountry: "DE",
|
|
||||||
returnPolicyCategory: "https://schema.org/MerchantReturnFiniteReturnWindow",
|
|
||||||
merchantReturnDays: 14,
|
|
||||||
returnMethod: "https://schema.org/ReturnByMail",
|
|
||||||
returnFees: "https://schema.org/FreeReturn",
|
|
||||||
},
|
|
||||||
shippingDetails: {
|
|
||||||
"@type": "OfferShippingDetails",
|
|
||||||
shippingRate: {
|
|
||||||
"@type": "MonetaryAmount",
|
|
||||||
value: 5.90,
|
|
||||||
currency: "EUR",
|
|
||||||
},
|
|
||||||
shippingDestination: {
|
|
||||||
"@type": "DefinedRegion",
|
|
||||||
addressCountry: "DE",
|
|
||||||
},
|
|
||||||
deliveryTime: {
|
|
||||||
"@type": "ShippingDeliveryTime",
|
|
||||||
handlingTime: {
|
|
||||||
"@type": "QuantitativeValue",
|
|
||||||
minValue: 0,
|
|
||||||
maxValue: 1,
|
|
||||||
unitCode: "DAY",
|
|
||||||
},
|
|
||||||
transitTime: {
|
|
||||||
"@type": "QuantitativeValue",
|
|
||||||
minValue: 2,
|
|
||||||
maxValue: 3,
|
|
||||||
unitCode: "DAY",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})),
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const categoryGraph = {
|
||||||
|
"@context": "https://schema.org",
|
||||||
|
"@graph": graph,
|
||||||
|
};
|
||||||
|
|
||||||
return `<script type="application/ld+json">${JSON.stringify(
|
return `<script type="application/ld+json">${JSON.stringify(
|
||||||
jsonLd
|
categoryGraph
|
||||||
)}</script>`;
|
)}</script>`;
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
generateCategoryMetaTags,
|
||||||
generateCategoryJsonLd,
|
generateCategoryJsonLd,
|
||||||
};
|
};
|
||||||
116
prerender/seo/hersteller.cjs
Normal file
116
prerender/seo/hersteller.cjs
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
/** Safe for double-quoted HTML attributes */
|
||||||
|
const escAttr = (str) =>
|
||||||
|
String(str ?? "")
|
||||||
|
.replace(/&/g, "&")
|
||||||
|
.replace(/"/g, """)
|
||||||
|
.replace(/</g, "<");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Head tags for prerendered Hersteller (Manufacturers) page
|
||||||
|
*/
|
||||||
|
const generateHerstellerMetaTags = (baseUrl, config, manufacturerCount = 0) => {
|
||||||
|
const root = baseUrl.endsWith("/") ? baseUrl.slice(0, -1) : baseUrl;
|
||||||
|
const herstellerUrl = root + "/Hersteller";
|
||||||
|
const site = config.siteName || config.brandName;
|
||||||
|
const desc = manufacturerCount + " Hersteller bei " + config.brandName + ": Top-Marken für Growshop-Produkte. Schnelle Lieferung, Laden Dresden.";
|
||||||
|
const descShort = desc.length > 160 ? desc.slice(0, 157) + "..." : desc;
|
||||||
|
const e = escAttr;
|
||||||
|
const logoUrl =
|
||||||
|
config.images && config.images.logo
|
||||||
|
? root + config.images.logo
|
||||||
|
: root + "/assets/images/nopicture.jpg";
|
||||||
|
|
||||||
|
return `
|
||||||
|
<meta name="description" content="${e(descShort)}">
|
||||||
|
<meta property="og:title" content="${e("Hersteller | " + site)}">
|
||||||
|
<meta property="og:description" content="${e(descShort)}">
|
||||||
|
<meta property="og:url" content="${herstellerUrl}">
|
||||||
|
<meta property="og:type" content="website">
|
||||||
|
<meta property="og:image" content="${e(logoUrl)}">
|
||||||
|
<meta property="og:site_name" content="${e(site)}">
|
||||||
|
<meta name="twitter:card" content="summary_large_image">
|
||||||
|
<meta name="twitter:title" content="${e("Hersteller | " + site)}">
|
||||||
|
<meta name="twitter:description" content="${e(descShort)}">
|
||||||
|
<meta name="robots" content="index, follow">
|
||||||
|
<link rel="canonical" href="${herstellerUrl}">
|
||||||
|
`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const generateHerstellerJsonLd = (baseUrl, config) => {
|
||||||
|
const root = baseUrl.endsWith("/") ? baseUrl.slice(0, -1) : baseUrl;
|
||||||
|
const herstellerUrl = root + "/Hersteller";
|
||||||
|
|
||||||
|
const id = {
|
||||||
|
business: root + "#business",
|
||||||
|
website: root + "#website",
|
||||||
|
breadcrumb: herstellerUrl + "#breadcrumb",
|
||||||
|
};
|
||||||
|
|
||||||
|
const logoUrl =
|
||||||
|
config.images && config.images.logo
|
||||||
|
? root + config.images.logo
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
const businessNode = {
|
||||||
|
"@id": id.business,
|
||||||
|
"@type": ["GardenStore", "LocalBusiness", "Organization"],
|
||||||
|
name: config.brandName,
|
||||||
|
url: root,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (logoUrl) {
|
||||||
|
businessNode.logo = { "@type": "ImageObject", url: logoUrl };
|
||||||
|
businessNode.image = { "@type": "ImageObject", url: logoUrl };
|
||||||
|
}
|
||||||
|
|
||||||
|
const websiteNode = {
|
||||||
|
"@id": id.website,
|
||||||
|
"@type": "WebSite",
|
||||||
|
name: config.siteName || config.brandName,
|
||||||
|
url: root,
|
||||||
|
publisher: { "@id": id.business },
|
||||||
|
};
|
||||||
|
|
||||||
|
const breadcrumbNode = {
|
||||||
|
"@id": id.breadcrumb,
|
||||||
|
"@type": "BreadcrumbList",
|
||||||
|
itemListElement: [
|
||||||
|
{
|
||||||
|
"@type": "ListItem",
|
||||||
|
position: 1,
|
||||||
|
name: "Home",
|
||||||
|
item: root,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"@type": "ListItem",
|
||||||
|
position: 2,
|
||||||
|
name: "Hersteller",
|
||||||
|
item: herstellerUrl,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const collectionPageNode = {
|
||||||
|
"@id": herstellerUrl,
|
||||||
|
"@type": "CollectionPage",
|
||||||
|
name: "Hersteller",
|
||||||
|
url: herstellerUrl,
|
||||||
|
description: "Alle Hersteller und Marken für Growshop-Produkte",
|
||||||
|
isPartOf: { "@id": id.website },
|
||||||
|
breadcrumb: { "@id": id.breadcrumb },
|
||||||
|
};
|
||||||
|
|
||||||
|
const graph = [businessNode, websiteNode, breadcrumbNode, collectionPageNode];
|
||||||
|
|
||||||
|
const herstellerGraph = {
|
||||||
|
"@context": "https://schema.org",
|
||||||
|
"@graph": graph,
|
||||||
|
};
|
||||||
|
|
||||||
|
return "<script type=\"application/ld+json\">" + JSON.stringify(herstellerGraph) + "</script>";
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
generateHerstellerMetaTags,
|
||||||
|
generateHerstellerJsonLd,
|
||||||
|
};
|
||||||
@@ -36,177 +36,198 @@ const generateHomepageJsonLd = (baseUrl, config, categories = []) => {
|
|||||||
const canonicalUrl = baseUrl.endsWith('/') ? baseUrl.slice(0, -1) : baseUrl;
|
const canonicalUrl = baseUrl.endsWith('/') ? baseUrl.slice(0, -1) : baseUrl;
|
||||||
const logoUrl = `${canonicalUrl}${config.images.logo}`;
|
const logoUrl = `${canonicalUrl}${config.images.logo}`;
|
||||||
|
|
||||||
const websiteJsonLd = {
|
const id = {
|
||||||
"@context": "https://schema.org/",
|
business: `${canonicalUrl}#business`,
|
||||||
|
website: `${canonicalUrl}#website`,
|
||||||
|
faq: `${canonicalUrl}#faq`,
|
||||||
|
categoryList: `${canonicalUrl}#category-list`,
|
||||||
|
sitemapPage: `${canonicalUrl}/sitemap#webpage`,
|
||||||
|
};
|
||||||
|
|
||||||
|
const organizationNode = {
|
||||||
|
"@id": id.business,
|
||||||
|
"@type": ["GardenStore", "LocalBusiness", "Organization"],
|
||||||
|
name: config.brandName,
|
||||||
|
alternateName: config.siteName,
|
||||||
|
description: config.descriptions.de.long,
|
||||||
|
url: canonicalUrl,
|
||||||
|
logo: {
|
||||||
|
"@type": "ImageObject",
|
||||||
|
url: logoUrl,
|
||||||
|
},
|
||||||
|
image: {
|
||||||
|
"@type": "ImageObject",
|
||||||
|
url: 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: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
const sitemapWebPageNode = {
|
||||||
|
"@id": id.sitemapPage,
|
||||||
|
"@type": "WebPage",
|
||||||
|
name: "Sitemap",
|
||||||
|
url: `${canonicalUrl}/sitemap`,
|
||||||
|
description: "Vollständige Sitemap mit allen Kategorien und Seiten",
|
||||||
|
isPartOf: { "@id": id.website },
|
||||||
|
};
|
||||||
|
|
||||||
|
const websiteNode = {
|
||||||
|
"@id": id.website,
|
||||||
"@type": "WebSite",
|
"@type": "WebSite",
|
||||||
name: config.brandName,
|
name: config.brandName,
|
||||||
url: canonicalUrl,
|
url: canonicalUrl,
|
||||||
description: config.descriptions.de.long,
|
description: config.descriptions.de.long,
|
||||||
publisher: {
|
publisher: { "@id": id.business },
|
||||||
"@type": "Organization",
|
|
||||||
name: config.brandName,
|
|
||||||
url: canonicalUrl,
|
|
||||||
logo: {
|
|
||||||
"@type": "ImageObject",
|
|
||||||
url: logoUrl,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
potentialAction: {
|
potentialAction: {
|
||||||
"@type": "SearchAction",
|
"@type": "SearchAction",
|
||||||
target: `${canonicalUrl}/search?q={search_term_string}`,
|
target: `${canonicalUrl}/search?q={search_term_string}`,
|
||||||
query: "required name=search_term_string"
|
query: "required name=search_term_string",
|
||||||
},
|
},
|
||||||
mainEntity: {
|
mainEntity: { "@id": id.sitemapPage },
|
||||||
"@type": "WebPage",
|
sameAs: [],
|
||||||
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 faqMainEntity = [
|
||||||
const organizationJsonLd = {
|
{
|
||||||
"@context": "https://schema.org",
|
"@type": "Question",
|
||||||
"@type": "LocalBusiness",
|
name: "Welche Zahlungsmethoden akzeptiert GrowHeads?",
|
||||||
"name": config.brandName,
|
acceptedAnswer: {
|
||||||
"alternateName": config.siteName,
|
"@type": "Answer",
|
||||||
"description": config.descriptions.de.long,
|
text: "Wir akzeptieren Kreditkarten, PayPal, Banküberweisung, Nachnahme und Barzahlung bei Abholung in unserem Laden in Dresden.",
|
||||||
"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",
|
"@type": "Question",
|
||||||
"contactType": "customer service",
|
name: "Liefert GrowHeads deutschlandweit?",
|
||||||
"availableLanguage": "German"
|
acceptedAnswer: {
|
||||||
}
|
"@type": "Answer",
|
||||||
],
|
text: "Ja, wir liefern deutschlandweit. Zusätzlich haben wir einen Laden in Dresden (Trachenberger Strasse 14) für lokale Kunden.",
|
||||||
"sameAs": [
|
},
|
||||||
// Add social media URLs when available
|
},
|
||||||
// "https://www.facebook.com/growheads",
|
{
|
||||||
// "https://www.instagram.com/growheads"
|
"@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.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
// FAQPage Schema for common questions
|
const faqNode = {
|
||||||
const faqJsonLd = {
|
"@id": id.faq,
|
||||||
"@context": "https://schema.org",
|
|
||||||
"@type": "FAQPage",
|
"@type": "FAQPage",
|
||||||
"mainEntity": [
|
url: canonicalUrl,
|
||||||
{
|
publisher: { "@id": id.business },
|
||||||
"@type": "Question",
|
isPartOf: { "@id": id.website },
|
||||||
"name": "Welche Zahlungsmethoden akzeptiert GrowHeads?",
|
mainEntity: faqMainEntity,
|
||||||
"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 filteredCategories = categories.filter((c) => c.seoName);
|
||||||
const categoriesListJsonLd = {
|
|
||||||
"@context": "https://schema.org",
|
const graph = [
|
||||||
"@type": "ItemList",
|
organizationNode,
|
||||||
"name": "Produktkategorien",
|
websiteNode,
|
||||||
"description": "Alle verfügbaren Produktkategorien in unserem Online-Shop",
|
sitemapWebPageNode,
|
||||||
"numberOfItems": categories.filter(category => category.seoName).length,
|
faqNode,
|
||||||
"itemListElement": categories
|
];
|
||||||
.filter(category => category.seoName) // Only include categories with seoName
|
|
||||||
.map((category, index) => ({
|
if (filteredCategories.length > 0) {
|
||||||
|
graph.push({
|
||||||
|
"@id": id.categoryList,
|
||||||
|
"@type": "ItemList",
|
||||||
|
name: "Produktkategorien",
|
||||||
|
description: "Alle verfügbaren Produktkategorien in unserem Online-Shop",
|
||||||
|
numberOfItems: filteredCategories.length,
|
||||||
|
isPartOf: { "@id": id.website },
|
||||||
|
itemListElement: filteredCategories.map((category, index) => ({
|
||||||
"@type": "ListItem",
|
"@type": "ListItem",
|
||||||
"position": index + 1,
|
position: index + 1,
|
||||||
"item": {
|
item: {
|
||||||
"@type": "Thing",
|
"@type": "Thing",
|
||||||
"name": category.name,
|
name: category.name,
|
||||||
"url": `${canonicalUrl}/Kategorie/${category.seoName}`
|
url: `${canonicalUrl}/Kategorie/${category.seoName}`,
|
||||||
}
|
},
|
||||||
}))
|
})),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const homepageGraph = {
|
||||||
|
"@context": "https://schema.org",
|
||||||
|
"@graph": graph,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Return all JSON-LD scripts
|
return `<script type="application/ld+json">${JSON.stringify(
|
||||||
const websiteScript = `<script type="application/ld+json">${JSON.stringify(websiteJsonLd)}</script>`;
|
homepageGraph
|
||||||
const organizationScript = `<script type="application/ld+json">${JSON.stringify(organizationJsonLd)}</script>`;
|
)}</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 = {
|
module.exports = {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ const {
|
|||||||
} = require('./product.cjs');
|
} = require('./product.cjs');
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
generateCategoryMetaTags,
|
||||||
generateCategoryJsonLd,
|
generateCategoryJsonLd,
|
||||||
} = require('./category.cjs');
|
} = require('./category.cjs');
|
||||||
|
|
||||||
@@ -22,6 +23,11 @@ const {
|
|||||||
generateKonfiguratorMetaTags,
|
generateKonfiguratorMetaTags,
|
||||||
} = require('./konfigurator.cjs');
|
} = require('./konfigurator.cjs');
|
||||||
|
|
||||||
|
const {
|
||||||
|
generateHerstellerMetaTags,
|
||||||
|
generateHerstellerJsonLd,
|
||||||
|
} = require('./hersteller.cjs');
|
||||||
|
|
||||||
const {
|
const {
|
||||||
generateRobotsTxt,
|
generateRobotsTxt,
|
||||||
generateProductsXml,
|
generateProductsXml,
|
||||||
@@ -41,6 +47,7 @@ module.exports = {
|
|||||||
generateProductJsonLd,
|
generateProductJsonLd,
|
||||||
|
|
||||||
// Category functions
|
// Category functions
|
||||||
|
generateCategoryMetaTags,
|
||||||
generateCategoryJsonLd,
|
generateCategoryJsonLd,
|
||||||
|
|
||||||
// Homepage functions
|
// Homepage functions
|
||||||
@@ -54,6 +61,10 @@ module.exports = {
|
|||||||
// Konfigurator functions
|
// Konfigurator functions
|
||||||
generateKonfiguratorMetaTags,
|
generateKonfiguratorMetaTags,
|
||||||
|
|
||||||
|
// Hersteller functions
|
||||||
|
generateHerstellerMetaTags,
|
||||||
|
generateHerstellerJsonLd,
|
||||||
|
|
||||||
// Feed/Export functions
|
// Feed/Export functions
|
||||||
generateRobotsTxt,
|
generateRobotsTxt,
|
||||||
generateProductsXml,
|
generateProductsXml,
|
||||||
|
|||||||
@@ -68,14 +68,15 @@ const generateProductMetaTags = (product, baseUrl, config) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const generateProductJsonLd = (product, baseUrl, config, categoryInfo = null) => {
|
const generateProductJsonLd = (product, baseUrl, config, categoryInfo = null) => {
|
||||||
const productUrl = `${baseUrl}/Artikel/${product.seoName}`;
|
const root = baseUrl.endsWith("/") ? baseUrl.slice(0, -1) : baseUrl;
|
||||||
|
const productUrl = `${root}/Artikel/${product.seoName}`;
|
||||||
const pictureFirstId =
|
const pictureFirstId =
|
||||||
product.pictureList && product.pictureList.trim()
|
product.pictureList && product.pictureList.trim()
|
||||||
? product.pictureList.split(",")[0].trim()
|
? product.pictureList.split(",")[0].trim()
|
||||||
: null;
|
: null;
|
||||||
const imageUrl = pictureFirstId
|
const imageUrl = pictureFirstId
|
||||||
? `${baseUrl}/assets/images/prod${pictureFirstId}.avif`
|
? `${root}/assets/images/prod${pictureFirstId}.avif`
|
||||||
: `${baseUrl}/assets/images/nopicture.jpg`;
|
: `${root}/assets/images/nopicture.jpg`;
|
||||||
|
|
||||||
// Clean description for JSON-LD (remove HTML tags)
|
// Clean description for JSON-LD (remove HTML tags)
|
||||||
const cleanDescription = product.description
|
const cleanDescription = product.description
|
||||||
@@ -86,8 +87,87 @@ const generateProductJsonLd = (product, baseUrl, config, categoryInfo = null) =>
|
|||||||
const priceValidDate = new Date();
|
const priceValidDate = new Date();
|
||||||
priceValidDate.setMonth(priceValidDate.getMonth() + 3);
|
priceValidDate.setMonth(priceValidDate.getMonth() + 3);
|
||||||
|
|
||||||
const jsonLd = {
|
const id = {
|
||||||
"@context": "https://schema.org/",
|
business: `${root}#business`,
|
||||||
|
website: `${root}#website`,
|
||||||
|
product: `${productUrl}#product`,
|
||||||
|
breadcrumb: `${productUrl}#breadcrumb`,
|
||||||
|
};
|
||||||
|
|
||||||
|
const logoUrl =
|
||||||
|
config.images && config.images.logo
|
||||||
|
? `${root}${config.images.logo}`
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
const businessNode = {
|
||||||
|
"@id": id.business,
|
||||||
|
"@type": ["GardenStore", "LocalBusiness", "Organization"],
|
||||||
|
name: config.brandName,
|
||||||
|
url: root,
|
||||||
|
...(logoUrl && {
|
||||||
|
logo: { "@type": "ImageObject", url: logoUrl },
|
||||||
|
image: { "@type": "ImageObject", url: logoUrl },
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
const websiteNode = {
|
||||||
|
"@id": id.website,
|
||||||
|
"@type": "WebSite",
|
||||||
|
name: config.siteName || config.brandName,
|
||||||
|
url: root,
|
||||||
|
publisher: { "@id": id.business },
|
||||||
|
};
|
||||||
|
|
||||||
|
const offer = {
|
||||||
|
"@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: { "@id": id.business },
|
||||||
|
hasMerchantReturnPolicy: {
|
||||||
|
"@type": "MerchantReturnPolicy",
|
||||||
|
applicableCountry: "DE",
|
||||||
|
returnPolicyCategory: "https://schema.org/MerchantReturnFiniteReturnWindow",
|
||||||
|
merchantReturnDays: 14,
|
||||||
|
returnMethod: "https://schema.org/ReturnByMail",
|
||||||
|
returnFees: "https://schema.org/FreeReturn",
|
||||||
|
},
|
||||||
|
shippingDetails: {
|
||||||
|
"@type": "OfferShippingDetails",
|
||||||
|
shippingRate: {
|
||||||
|
"@type": "MonetaryAmount",
|
||||||
|
value: 5.9,
|
||||||
|
currency: "EUR",
|
||||||
|
},
|
||||||
|
shippingDestination: {
|
||||||
|
"@type": "DefinedRegion",
|
||||||
|
addressCountry: "DE",
|
||||||
|
},
|
||||||
|
deliveryTime: {
|
||||||
|
"@type": "ShippingDeliveryTime",
|
||||||
|
handlingTime: {
|
||||||
|
"@type": "QuantitativeValue",
|
||||||
|
minValue: 0,
|
||||||
|
maxValue: 1,
|
||||||
|
unitCode: "DAY",
|
||||||
|
},
|
||||||
|
transitTime: {
|
||||||
|
"@type": "QuantitativeValue",
|
||||||
|
minValue: 2,
|
||||||
|
maxValue: 3,
|
||||||
|
unitCode: "DAY",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const productNode = {
|
||||||
|
"@id": id.product,
|
||||||
"@type": "Product",
|
"@type": "Product",
|
||||||
name: product.name,
|
name: product.name,
|
||||||
image: [imageUrl],
|
image: [imageUrl],
|
||||||
@@ -98,87 +178,64 @@ const generateProductJsonLd = (product, baseUrl, config, categoryInfo = null) =>
|
|||||||
"@type": "Brand",
|
"@type": "Brand",
|
||||||
name: product.manufacturer || "Unknown",
|
name: product.manufacturer || "Unknown",
|
||||||
},
|
},
|
||||||
offers: {
|
offers: offer,
|
||||||
"@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,
|
|
||||||
},
|
|
||||||
hasMerchantReturnPolicy: {
|
|
||||||
"@type": "MerchantReturnPolicy",
|
|
||||||
applicableCountry: "DE",
|
|
||||||
returnPolicyCategory: "https://schema.org/MerchantReturnFiniteReturnWindow",
|
|
||||||
merchantReturnDays: 14,
|
|
||||||
returnMethod: "https://schema.org/ReturnByMail",
|
|
||||||
returnFees: "https://schema.org/FreeReturn",
|
|
||||||
},
|
|
||||||
shippingDetails: {
|
|
||||||
"@type": "OfferShippingDetails",
|
|
||||||
shippingRate: {
|
|
||||||
"@type": "MonetaryAmount",
|
|
||||||
value: 5.90,
|
|
||||||
currency: "EUR",
|
|
||||||
},
|
|
||||||
shippingDestination: {
|
|
||||||
"@type": "DefinedRegion",
|
|
||||||
addressCountry: "DE",
|
|
||||||
},
|
|
||||||
deliveryTime: {
|
|
||||||
"@type": "ShippingDeliveryTime",
|
|
||||||
handlingTime: {
|
|
||||||
"@type": "QuantitativeValue",
|
|
||||||
minValue: 0,
|
|
||||||
maxValue: 1,
|
|
||||||
unitCode: "DAY",
|
|
||||||
},
|
|
||||||
transitTime: {
|
|
||||||
"@type": "QuantitativeValue",
|
|
||||||
minValue: 2,
|
|
||||||
maxValue: 3,
|
|
||||||
unitCode: "DAY",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Add breadcrumb if category information is available
|
const hasBreadcrumb =
|
||||||
if (categoryInfo && categoryInfo.name && categoryInfo.seoName) {
|
categoryInfo && categoryInfo.name && categoryInfo.seoName;
|
||||||
jsonLd.breadcrumb = {
|
|
||||||
"@type": "BreadcrumbList",
|
const breadcrumbList = hasBreadcrumb
|
||||||
itemListElement: [
|
? {
|
||||||
{
|
"@id": id.breadcrumb,
|
||||||
"@type": "ListItem",
|
"@type": "BreadcrumbList",
|
||||||
position: 1,
|
itemListElement: [
|
||||||
name: "Home",
|
{
|
||||||
item: baseUrl,
|
"@type": "ListItem",
|
||||||
},
|
position: 1,
|
||||||
{
|
name: "Home",
|
||||||
"@type": "ListItem",
|
item: root,
|
||||||
position: 2,
|
},
|
||||||
name: categoryInfo.name,
|
{
|
||||||
item: `${baseUrl}/Kategorie/${categoryInfo.seoName}`,
|
"@type": "ListItem",
|
||||||
},
|
position: 2,
|
||||||
{
|
name: categoryInfo.name,
|
||||||
"@type": "ListItem",
|
item: `${root}/Kategorie/${categoryInfo.seoName}`,
|
||||||
position: 3,
|
},
|
||||||
name: product.name,
|
{
|
||||||
item: productUrl,
|
"@type": "ListItem",
|
||||||
},
|
position: 3,
|
||||||
],
|
name: product.name,
|
||||||
};
|
item: productUrl,
|
||||||
}
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
: null;
|
||||||
|
|
||||||
|
const itemPageNode = {
|
||||||
|
"@id": productUrl,
|
||||||
|
"@type": "ItemPage",
|
||||||
|
url: productUrl,
|
||||||
|
name: product.name,
|
||||||
|
isPartOf: { "@id": id.website },
|
||||||
|
mainEntity: { "@id": id.product },
|
||||||
|
...(hasBreadcrumb && { breadcrumb: { "@id": id.breadcrumb } }),
|
||||||
|
};
|
||||||
|
|
||||||
|
const graph = [
|
||||||
|
businessNode,
|
||||||
|
websiteNode,
|
||||||
|
itemPageNode,
|
||||||
|
...(breadcrumbList ? [breadcrumbList] : []),
|
||||||
|
productNode,
|
||||||
|
];
|
||||||
|
|
||||||
|
const productGraph = {
|
||||||
|
"@context": "https://schema.org",
|
||||||
|
"@graph": graph,
|
||||||
|
};
|
||||||
|
|
||||||
return `<script type="application/ld+json">${JSON.stringify(
|
return `<script type="application/ld+json">${JSON.stringify(
|
||||||
jsonLd
|
productGraph
|
||||||
)}</script>`;
|
)}</script>`;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
BIN
public/assets/images/cutlings2.avif
Normal file
BIN
public/assets/images/cutlings2.avif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.2 KiB |
13
src/App.js
13
src/App.js
@@ -33,6 +33,7 @@ import i18n from './i18n/index.js';
|
|||||||
import Header from "./components/Header.js";
|
import Header from "./components/Header.js";
|
||||||
import Footer from "./components/Footer.js";
|
import Footer from "./components/Footer.js";
|
||||||
import MainPageLayout from "./components/MainPageLayout.js";
|
import MainPageLayout from "./components/MainPageLayout.js";
|
||||||
|
import IdleMainPagesSlideshow from "./components/IdleMainPagesSlideshow.js";
|
||||||
|
|
||||||
import Content from "./components/Content.js";
|
import Content from "./components/Content.js";
|
||||||
import ProductDetail from "./components/ProductDetail.js";
|
import ProductDetail from "./components/ProductDetail.js";
|
||||||
@@ -40,6 +41,7 @@ import ProductDetail from "./components/ProductDetail.js";
|
|||||||
// Lazy load rarely-accessed pages
|
// Lazy load rarely-accessed pages
|
||||||
const ProfilePage = lazy(() => import(/* webpackChunkName: "profile" */ "./pages/ProfilePage.js"));
|
const ProfilePage = lazy(() => import(/* webpackChunkName: "profile" */ "./pages/ProfilePage.js"));
|
||||||
const ResetPassword = lazy(() => import(/* webpackChunkName: "reset-password" */ "./pages/ResetPassword.js"));
|
const ResetPassword = lazy(() => import(/* webpackChunkName: "reset-password" */ "./pages/ResetPassword.js"));
|
||||||
|
const LinkTelegramPage = lazy(() => import(/* webpackChunkName: "link-telegram" */ "./pages/LinkTelegramPage.js"));
|
||||||
|
|
||||||
// Lazy load admin pages - only loaded when admin users access them
|
// Lazy load admin pages - only loaded when admin users access them
|
||||||
const AdminPage = lazy(() => import(/* webpackChunkName: "admin" */ "./pages/AdminPage.js"));
|
const AdminPage = lazy(() => import(/* webpackChunkName: "admin" */ "./pages/AdminPage.js"));
|
||||||
@@ -52,6 +54,7 @@ const AGB = lazy(() => import(/* webpackChunkName: "legal" */ "./pages/AGB.js"))
|
|||||||
//const NotFound404 = lazy(() => import(/* webpackChunkName: "legal" */ "./pages/NotFound404.js")); <Route path="/404" element={<NotFound404 />} />
|
//const NotFound404 = lazy(() => import(/* webpackChunkName: "legal" */ "./pages/NotFound404.js")); <Route path="/404" element={<NotFound404 />} />
|
||||||
const Sitemap = lazy(() => import(/* webpackChunkName: "sitemap" */ "./pages/Sitemap.js"));
|
const Sitemap = lazy(() => import(/* webpackChunkName: "sitemap" */ "./pages/Sitemap.js"));
|
||||||
const CategoriesPage = lazy(() => import(/* webpackChunkName: "categories" */ "./pages/CategoriesPage.js"));
|
const CategoriesPage = lazy(() => import(/* webpackChunkName: "categories" */ "./pages/CategoriesPage.js"));
|
||||||
|
const HerstellerPage = lazy(() => import(/* webpackChunkName: "hersteller" */ "./pages/HerstellerPage.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"));
|
||||||
const Widerrufsrecht = lazy(() => import(/* webpackChunkName: "legal" */ "./pages/Widerrufsrecht.js"));
|
const Widerrufsrecht = lazy(() => import(/* webpackChunkName: "legal" */ "./pages/Widerrufsrecht.js"));
|
||||||
@@ -253,6 +256,7 @@ const AppContent = ({ currentTheme, dynamicTheme, onThemeChange }) => {
|
|||||||
)
|
)
|
||||||
}>
|
}>
|
||||||
<CarouselProvider>
|
<CarouselProvider>
|
||||||
|
<IdleMainPagesSlideshow />
|
||||||
<Routes>
|
<Routes>
|
||||||
{/* Main pages using unified component */}
|
{/* Main pages using unified component */}
|
||||||
<Route path="/" element={<MainPageLayout />} />
|
<Route path="/" element={<MainPageLayout />} />
|
||||||
@@ -264,6 +268,11 @@ const AppContent = ({ currentTheme, dynamicTheme, onThemeChange }) => {
|
|||||||
path="/Kategorie/:categoryId"
|
path="/Kategorie/:categoryId"
|
||||||
element={<Content />}
|
element={<Content />}
|
||||||
/>
|
/>
|
||||||
|
{/* Manufacturer page - Render Content in parallel */}
|
||||||
|
<Route
|
||||||
|
path="/Hersteller/:categoryId"
|
||||||
|
element={<Content />}
|
||||||
|
/>
|
||||||
{/* Single product page */}
|
{/* Single product page */}
|
||||||
<Route
|
<Route
|
||||||
path="/Artikel/:seoName"
|
path="/Artikel/:seoName"
|
||||||
@@ -275,6 +284,9 @@ const AppContent = ({ currentTheme, dynamicTheme, onThemeChange }) => {
|
|||||||
|
|
||||||
{/* Profile page */}
|
{/* Profile page */}
|
||||||
<Route path="/profile" element={<ProfilePage />} />
|
<Route path="/profile" element={<ProfilePage />} />
|
||||||
|
{/* Link Telegram id (expects ?id=... or /linkTelegram/:id) */}
|
||||||
|
<Route path="/linkTelegram" element={<LinkTelegramPage />} />
|
||||||
|
<Route path="/linkTelegram/:id" element={<LinkTelegramPage />} />
|
||||||
|
|
||||||
{/* Payment success page for Mollie redirects */}
|
{/* Payment success page for Mollie redirects */}
|
||||||
<Route path="/payment/success" element={<PaymentSuccess />} />
|
<Route path="/payment/success" element={<PaymentSuccess />} />
|
||||||
@@ -299,6 +311,7 @@ const AppContent = ({ currentTheme, dynamicTheme, onThemeChange }) => {
|
|||||||
<Route path="/agb" element={<AGB />} />
|
<Route path="/agb" element={<AGB />} />
|
||||||
<Route path="/sitemap" element={<Sitemap />} />
|
<Route path="/sitemap" element={<Sitemap />} />
|
||||||
<Route path="/Kategorien" element={<CategoriesPage />} />
|
<Route path="/Kategorien" element={<CategoriesPage />} />
|
||||||
|
<Route path="/Hersteller" element={<HerstellerPage />} />
|
||||||
<Route path="/impressum" element={<Impressum />} />
|
<Route path="/impressum" element={<Impressum />} />
|
||||||
<Route
|
<Route
|
||||||
path="/batteriegesetzhinweise"
|
path="/batteriegesetzhinweise"
|
||||||
|
|||||||
84
src/PrerenderHerstellerPage.js
Normal file
84
src/PrerenderHerstellerPage.js
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import Box from '@mui/material/Box';
|
||||||
|
import Paper from '@mui/material/Paper';
|
||||||
|
import Typography from '@mui/material/Typography';
|
||||||
|
import Link from '@mui/material/Link';
|
||||||
|
import LegalPage from './pages/LegalPage.js';
|
||||||
|
|
||||||
|
const PrerenderHerstellerPage = ({ manufacturerData }) => {
|
||||||
|
// Use prop data (passed from prerender.cjs)
|
||||||
|
const manufacturers = manufacturerData;
|
||||||
|
|
||||||
|
// If no manufacturer data, show empty state
|
||||||
|
if (!manufacturers || manufacturers.length === 0) {
|
||||||
|
const content = (
|
||||||
|
<Box>
|
||||||
|
<Typography variant="body1" paragraph>
|
||||||
|
Keine Hersteller gefunden.
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
return <LegalPage title="Hersteller" content={content} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render manufacturers similar to HerstellerPage.js
|
||||||
|
const content = (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'grid',
|
||||||
|
gridTemplateColumns: 'repeat(auto-fit, minmax(140px, 1fr))',
|
||||||
|
gap: 2,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{manufacturers.map((manufacturer) => (
|
||||||
|
<Paper
|
||||||
|
key={manufacturer.id}
|
||||||
|
component={Link}
|
||||||
|
href={`/Hersteller/${encodeURIComponent(manufacturer.slug || '')}`}
|
||||||
|
elevation={3}
|
||||||
|
style={{
|
||||||
|
width: '140px',
|
||||||
|
height: '140px',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
overflow: 'hidden',
|
||||||
|
userSelect: 'none',
|
||||||
|
textDecoration: 'none',
|
||||||
|
cursor: 'pointer',
|
||||||
|
borderRadius: '8px',
|
||||||
|
position: 'relative',
|
||||||
|
zIndex: 10,
|
||||||
|
backgroundColor: '#f0f0f0',
|
||||||
|
boxShadow: '0px 2px 1px -1px rgba(0,0,0,0.2), 0px 1px 1px 0px rgba(0,0,0,0.14), 0px 1px 3px 0px rgba(0,0,0,0.12)'
|
||||||
|
}}
|
||||||
|
sx={{
|
||||||
|
transition: 'transform 0.2s ease, box-shadow 0.2s ease',
|
||||||
|
'&:hover': {
|
||||||
|
transform: 'translateY(-5px)',
|
||||||
|
boxShadow: 8,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{manufacturer.imageBuffer && (
|
||||||
|
<img
|
||||||
|
src={`data:image/avif;base64,${Buffer.from(manufacturer.imageBuffer).toString('base64')}`}
|
||||||
|
alt={manufacturer.name}
|
||||||
|
draggable={false}
|
||||||
|
style={{
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
objectFit: 'contain',
|
||||||
|
display: 'block',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Paper>
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
|
||||||
|
return <LegalPage title="Hersteller" content={content} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PrerenderHerstellerPage;
|
||||||
@@ -165,16 +165,15 @@ class PrerenderProduct extends React.Component {
|
|||||||
sx: {
|
sx: {
|
||||||
mb: 2,
|
mb: 2,
|
||||||
position: ["-webkit-sticky", "sticky"],
|
position: ["-webkit-sticky", "sticky"],
|
||||||
|
// No CategoryList in prerender — two-row toolbar only; safe-area for notched phones.
|
||||||
top: {
|
top: {
|
||||||
xs: "80px",
|
xs: "calc(env(safe-area-inset-top, 0px) + 128px)",
|
||||||
sm: "80px",
|
sm: "80px",
|
||||||
md: "80px",
|
|
||||||
lg: "80px",
|
|
||||||
},
|
},
|
||||||
left: 0,
|
left: 0,
|
||||||
width: "100%",
|
width: "100%",
|
||||||
display: "flex",
|
display: "flex",
|
||||||
zIndex: 999, // Just below the AppBar
|
zIndex: (theme) => theme.zIndex.appBar - 1,
|
||||||
py: 0,
|
py: 0,
|
||||||
px: 2,
|
px: 2,
|
||||||
}
|
}
|
||||||
@@ -552,7 +551,7 @@ class PrerenderProduct extends React.Component {
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
style: {
|
style: {
|
||||||
fontFamily: '"Roboto","Helvetica","Arial",sans-serif',
|
fontFamily: '"Outfit Variable","Roboto","Helvetica","Arial",sans-serif',
|
||||||
fontSize: '1rem',
|
fontSize: '1rem',
|
||||||
lineHeight: '1.7',
|
lineHeight: '1.7',
|
||||||
color: '#333'
|
color: '#333'
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ import NotificationsActiveIcon from "@mui/icons-material/NotificationsActive";
|
|||||||
import CircularProgress from "@mui/material/CircularProgress";
|
import CircularProgress from "@mui/material/CircularProgress";
|
||||||
import { withI18n } from "../i18n/withTranslation.js";
|
import { withI18n } from "../i18n/withTranslation.js";
|
||||||
import {
|
import {
|
||||||
|
PUSH_SUBSCRIPTIONS_CHANGED_EVENT,
|
||||||
|
emitPushSubscriptionsChanged,
|
||||||
isPushApiSupported,
|
isPushApiSupported,
|
||||||
fetchPushConfiguration,
|
fetchPushConfiguration,
|
||||||
registerPushServiceWorker,
|
registerPushServiceWorker,
|
||||||
@@ -109,9 +111,10 @@ class AddToCartButton extends Component {
|
|||||||
this.setState({ pushSubscribed: false, pushBusy: false });
|
this.setState({ pushSubscribed: false, pushBusy: false });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const res = await articlePushUnsubscribe(subscription.endpoint);
|
const res = await articlePushUnsubscribe(subscription.endpoint, kArtikel);
|
||||||
if (parseSuccess(res)) {
|
if (parseSuccess(res)) {
|
||||||
this.setState({ pushSubscribed: false });
|
this.setState({ pushSubscribed: false });
|
||||||
|
emitPushSubscriptionsChanged();
|
||||||
} else {
|
} else {
|
||||||
this.setState({
|
this.setState({
|
||||||
pushError:
|
pushError:
|
||||||
@@ -146,6 +149,7 @@ class AddToCartButton extends Component {
|
|||||||
const res = await articlePushSubscribe(kArtikel, subscription);
|
const res = await articlePushSubscribe(kArtikel, subscription);
|
||||||
if (parseSuccess(res)) {
|
if (parseSuccess(res)) {
|
||||||
this.setState({ pushSubscribed: true });
|
this.setState({ pushSubscribed: true });
|
||||||
|
emitPushSubscriptionsChanged();
|
||||||
} else {
|
} else {
|
||||||
this.setState({
|
this.setState({
|
||||||
pushError:
|
pushError:
|
||||||
@@ -174,7 +178,14 @@ class AddToCartButton extends Component {
|
|||||||
if (this.state.quantity !== newQuantity)
|
if (this.state.quantity !== newQuantity)
|
||||||
this.setState({ quantity: newQuantity });
|
this.setState({ quantity: newQuantity });
|
||||||
};
|
};
|
||||||
|
this.onPushSubscriptionsChanged = () => {
|
||||||
|
this.refreshIncomingPushStatus();
|
||||||
|
};
|
||||||
window.addEventListener("cart", this.cart);
|
window.addEventListener("cart", this.cart);
|
||||||
|
window.addEventListener(
|
||||||
|
PUSH_SUBSCRIPTIONS_CHANGED_EVENT,
|
||||||
|
this.onPushSubscriptionsChanged
|
||||||
|
);
|
||||||
this.refreshIncomingPushStatus();
|
this.refreshIncomingPushStatus();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -190,6 +201,10 @@ class AddToCartButton extends Component {
|
|||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
window.removeEventListener("cart", this.cart);
|
window.removeEventListener("cart", this.cart);
|
||||||
|
window.removeEventListener(
|
||||||
|
PUSH_SUBSCRIPTIONS_CHANGED_EVENT,
|
||||||
|
this.onPushSubscriptionsChanged
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleIncrement = () => {
|
handleIncrement = () => {
|
||||||
@@ -370,7 +385,7 @@ class AddToCartButton extends Component {
|
|||||||
startIcon={<ShoppingCartIcon />}
|
startIcon={<ShoppingCartIcon />}
|
||||||
sx={{
|
sx={{
|
||||||
borderRadius: 2,
|
borderRadius: 2,
|
||||||
fontWeight: "bold",
|
whiteSpace: "nowrap",
|
||||||
backgroundColor: "#9ccc65", // yellowish green
|
backgroundColor: "#9ccc65", // yellowish green
|
||||||
color: "#000000",
|
color: "#000000",
|
||||||
"&:hover": {
|
"&:hover": {
|
||||||
@@ -524,7 +539,7 @@ class AddToCartButton extends Component {
|
|||||||
startIcon={<ShoppingCartIcon />}
|
startIcon={<ShoppingCartIcon />}
|
||||||
sx={{
|
sx={{
|
||||||
borderRadius: 2,
|
borderRadius: 2,
|
||||||
fontWeight: "bold",
|
whiteSpace: "nowrap",
|
||||||
"&:hover": {
|
"&:hover": {
|
||||||
backgroundColor: "primary.dark",
|
backgroundColor: "primary.dark",
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -55,20 +55,54 @@ class ChatAssistant extends Component {
|
|||||||
|
|
||||||
buildPrivacyPromptHtml = () => {
|
buildPrivacyPromptHtml = () => {
|
||||||
const { t } = this.props;
|
const { t } = this.props;
|
||||||
return `${t('chat.privacyPromptBefore')}<a href="/datenschutz" target="_blank" rel="noopener noreferrer">${t('chat.privacyPolicyLink')}</a>${t('chat.privacyPromptAfter')}<button data-confirm-privacy="true">${t('chat.privacyRead')}</button>`;
|
return `<div style="display: flex; flex-direction: column; gap: 8px; line-height: 1.5;">
|
||||||
|
<div>${t('chat.privacyPromptBefore')}<a href="/datenschutz" target="_blank" rel="noopener noreferrer">${t('chat.privacyPolicyLink')}</a>${t('chat.privacyPromptAfter')}</div>
|
||||||
|
<div><button data-confirm-privacy="true">${t('chat.privacyRead')}</button></div>
|
||||||
|
</div>`;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Keep stored privacy bubble in sync with i18n (language switcher, lazy bundle load). */
|
||||||
|
applyPrivacyPromptTranslation = () => {
|
||||||
|
this.setState((prev) => {
|
||||||
|
if (prev.privacyConfirmed) return null;
|
||||||
|
const idx = prev.messages.findIndex((m) => m.id === 'privacy-prompt');
|
||||||
|
if (idx === -1) return null;
|
||||||
|
const updatedMessages = [...prev.messages];
|
||||||
|
updatedMessages[idx] = {
|
||||||
|
...updatedMessages[idx],
|
||||||
|
text: this.buildPrivacyPromptHtml(),
|
||||||
|
};
|
||||||
|
window.chatMessages = updatedMessages;
|
||||||
|
return { messages: updatedMessages };
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
handleI18nLanguageChanged = () => {
|
||||||
|
this.applyPrivacyPromptTranslation();
|
||||||
};
|
};
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
// Add socket listeners if socket is available and connected
|
// Add socket listeners if socket is available and connected
|
||||||
this.addSocketListeners();
|
this.addSocketListeners();
|
||||||
|
|
||||||
|
this.props.i18n?.on('languageChanged', this.handleI18nLanguageChanged);
|
||||||
|
|
||||||
const userStatus = isUserLoggedIn();
|
const userStatus = isUserLoggedIn();
|
||||||
const isGuest = !userStatus.isLoggedIn;
|
const isGuest = !userStatus.isLoggedIn;
|
||||||
|
|
||||||
if (isGuest && !this.state.privacyConfirmed) {
|
if (isGuest && !this.state.privacyConfirmed) {
|
||||||
this.setState(prevState => {
|
this.setState(prevState => {
|
||||||
if (prevState.messages.find(msg => msg.id === 'privacy-prompt')) {
|
if (prevState.messages.find(msg => msg.id === 'privacy-prompt')) {
|
||||||
return { isGuest: true };
|
const updatedMessages = prevState.messages.map((msg) =>
|
||||||
|
msg.id === 'privacy-prompt'
|
||||||
|
? { ...msg, text: this.buildPrivacyPromptHtml() }
|
||||||
|
: msg
|
||||||
|
);
|
||||||
|
window.chatMessages = updatedMessages;
|
||||||
|
return {
|
||||||
|
messages: updatedMessages,
|
||||||
|
isGuest: true,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const privacyMessage = {
|
const privacyMessage = {
|
||||||
@@ -90,17 +124,7 @@ class ChatAssistant extends Component {
|
|||||||
|
|
||||||
componentDidUpdate(prevProps, prevState) {
|
componentDidUpdate(prevProps, prevState) {
|
||||||
if (prevProps.i18n?.language !== this.props.i18n?.language) {
|
if (prevProps.i18n?.language !== this.props.i18n?.language) {
|
||||||
this.setState((prev) => {
|
this.applyPrivacyPromptTranslation();
|
||||||
const idx = prev.messages.findIndex((m) => m.id === 'privacy-prompt');
|
|
||||||
if (idx === -1) return null;
|
|
||||||
const updatedMessages = [...prev.messages];
|
|
||||||
updatedMessages[idx] = {
|
|
||||||
...updatedMessages[idx],
|
|
||||||
text: this.buildPrivacyPromptHtml(),
|
|
||||||
};
|
|
||||||
window.chatMessages = updatedMessages;
|
|
||||||
return { messages: updatedMessages };
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
if (prevState.messages !== this.state.messages || prevState.isTyping !== this.state.isTyping) {
|
if (prevState.messages !== this.state.messages || prevState.isTyping !== this.state.isTyping) {
|
||||||
this.scrollToBottom();
|
this.scrollToBottom();
|
||||||
@@ -108,6 +132,7 @@ class ChatAssistant extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
|
this.props.i18n?.off('languageChanged', this.handleI18nLanguageChanged);
|
||||||
this.removeSocketListeners();
|
this.removeSocketListeners();
|
||||||
this.stopRecording();
|
this.stopRecording();
|
||||||
if (this.recordingTimer) {
|
if (this.recordingTimer) {
|
||||||
@@ -206,7 +231,7 @@ class ChatAssistant extends Component {
|
|||||||
}, () => {
|
}, () => {
|
||||||
// Emit message to socket server after state is updated
|
// Emit message to socket server after state is updated
|
||||||
if (userMessage.trim()) {
|
if (userMessage.trim()) {
|
||||||
window.socketManager.emit('aiassyMessage', userMessage);
|
window.socketManager.emit('aiassyMessage', { message: userMessage, lang: this.props.i18n?.language });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import ProductList from './ProductList.js';
|
|||||||
import CategoryBoxGrid from './CategoryBoxGrid.js';
|
import CategoryBoxGrid from './CategoryBoxGrid.js';
|
||||||
import CategoryBox from './CategoryBox.js';
|
import CategoryBox from './CategoryBox.js';
|
||||||
|
|
||||||
import { useParams, useSearchParams } from 'react-router-dom';
|
import { useParams, useSearchParams, useLocation } from 'react-router-dom';
|
||||||
import { getAllSettingsWithPrefix } from '../utils/sessionStorage.js';
|
import { getAllSettingsWithPrefix } from '../utils/sessionStorage.js';
|
||||||
import { withI18n } from '../i18n/withTranslation.js';
|
import { withI18n } from '../i18n/withTranslation.js';
|
||||||
import { withCategory } from '../context/CategoryContext.js';
|
import { withCategory } from '../context/CategoryContext.js';
|
||||||
@@ -24,17 +24,19 @@ const withRouter = (ClassComponent) => {
|
|||||||
return (props) => {
|
return (props) => {
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
const [searchParams] = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
return <ClassComponent {...props} params={params} searchParams={searchParams} />;
|
const location = useLocation();
|
||||||
|
const isHersteller = location.pathname.startsWith('/Hersteller/');
|
||||||
|
return <ClassComponent {...props} params={params} searchParams={searchParams} isHersteller={isHersteller} />;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
function getCachedCategoryData(categoryId, language = 'de') {
|
function getCachedCategoryData(categoryId, language = 'de', isHersteller = false) {
|
||||||
if (!window.productCache) {
|
if (!window.productCache) {
|
||||||
window.productCache = {};
|
window.productCache = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const cacheKey = `categoryProducts_${categoryId}_${language}`;
|
const cacheKey = `${isHersteller ? 'manufacturer' : 'category'}Products_${categoryId}_${language}`;
|
||||||
const cachedData = window.productCache[cacheKey];
|
const cachedData = window.productCache[cacheKey];
|
||||||
|
|
||||||
if (cachedData) {
|
if (cachedData) {
|
||||||
@@ -166,7 +168,7 @@ function getFilteredProducts(unfilteredProducts, attributes, t) {
|
|||||||
|
|
||||||
return { filteredProducts, activeAttributeFilters: activeAttributeFiltersWithNames, activeManufacturerFilters: activeManufacturerFiltersWithNames, activeAvailabilityFilters };
|
return { filteredProducts, activeAttributeFilters: activeAttributeFiltersWithNames, activeManufacturerFilters: activeManufacturerFiltersWithNames, activeAvailabilityFilters };
|
||||||
}
|
}
|
||||||
function setCachedCategoryData(categoryId, data, language = 'de') {
|
function setCachedCategoryData(categoryId, data, language = 'de', isHersteller = false) {
|
||||||
if (!window.productCache) {
|
if (!window.productCache) {
|
||||||
window.productCache = {};
|
window.productCache = {};
|
||||||
}
|
}
|
||||||
@@ -175,7 +177,7 @@ function setCachedCategoryData(categoryId, data, language = 'de') {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const cacheKey = `categoryProducts_${categoryId}_${language}`;
|
const cacheKey = `${isHersteller ? 'manufacturer' : 'category'}Products_${categoryId}_${language}`;
|
||||||
if (data.products) for (const product of data.products) {
|
if (data.products) for (const product of data.products) {
|
||||||
const productCacheKey = `product_${product.id}_${language}`;
|
const productCacheKey = `product_${product.id}_${language}`;
|
||||||
window.productDetailCache[productCacheKey] = product;
|
window.productDetailCache[productCacheKey] = product;
|
||||||
@@ -221,9 +223,10 @@ class Content extends Component {
|
|||||||
componentDidUpdate(prevProps) {
|
componentDidUpdate(prevProps) {
|
||||||
const currentLanguage = this.props.i18n?.language || 'de';
|
const currentLanguage = this.props.i18n?.language || 'de';
|
||||||
const categoryChanged = this.props.params.categoryId && (prevProps.params.categoryId !== this.props.params.categoryId);
|
const categoryChanged = this.props.params.categoryId && (prevProps.params.categoryId !== this.props.params.categoryId);
|
||||||
|
const routeTypeChanged = !!prevProps.isHersteller !== !!this.props.isHersteller;
|
||||||
const searchChanged = this.props.searchParams?.get('q') && (prevProps.searchParams?.get('q') !== this.props.searchParams?.get('q'));
|
const searchChanged = this.props.searchParams?.get('q') && (prevProps.searchParams?.get('q') !== this.props.searchParams?.get('q'));
|
||||||
|
|
||||||
if (categoryChanged) {
|
if (categoryChanged || routeTypeChanged) {
|
||||||
// Clear context for new category loading
|
// Clear context for new category loading
|
||||||
if (this.props.categoryContext && this.props.categoryContext.setCurrentCategory) {
|
if (this.props.categoryContext && this.props.categoryContext.setCurrentCategory) {
|
||||||
this.props.categoryContext.setCurrentCategory(null);
|
this.props.categoryContext.setCurrentCategory(null);
|
||||||
@@ -233,7 +236,7 @@ class Content extends Component {
|
|||||||
this.setState({ loaded: false, unfilteredProducts: [], filteredProducts: [], attributes: [], categoryName: null, childCategories: [], lastFetchedLanguage: currentLanguage }, () => {
|
this.setState({ loaded: false, unfilteredProducts: [], filteredProducts: [], attributes: [], categoryName: null, childCategories: [], lastFetchedLanguage: currentLanguage }, () => {
|
||||||
this.fetchCategoryData(this.props.params.categoryId);
|
this.fetchCategoryData(this.props.params.categoryId);
|
||||||
});
|
});
|
||||||
return; // Don't check language change if category changed
|
return; // Don't check language change if category or route type changed
|
||||||
}
|
}
|
||||||
else if (searchChanged) {
|
else if (searchChanged) {
|
||||||
this.setState({ loaded: false, unfilteredProducts: [], filteredProducts: [], attributes: [], categoryName: null, childCategories: [], lastFetchedLanguage: currentLanguage }, () => {
|
this.setState({ loaded: false, unfilteredProducts: [], filteredProducts: [], attributes: [], categoryName: null, childCategories: [], lastFetchedLanguage: currentLanguage }, () => {
|
||||||
@@ -345,7 +348,8 @@ class Content extends Component {
|
|||||||
sessionStorage.setItem('filter_availability', '1');
|
sessionStorage.setItem('filter_availability', '1');
|
||||||
}
|
}
|
||||||
const currentLanguage = this.props.languageContext?.currentLanguage || this.props.i18n?.language || 'de';
|
const currentLanguage = this.props.languageContext?.currentLanguage || this.props.i18n?.language || 'de';
|
||||||
const cachedData = getCachedCategoryData(categoryId, currentLanguage);
|
const isHersteller = !!this.props.isHersteller;
|
||||||
|
const cachedData = getCachedCategoryData(categoryId, currentLanguage, isHersteller);
|
||||||
if (cachedData) {
|
if (cachedData) {
|
||||||
this.processDataWithCategoryTree(cachedData, categoryId);
|
this.processDataWithCategoryTree(cachedData, categoryId);
|
||||||
return;
|
return;
|
||||||
@@ -360,7 +364,7 @@ class Content extends Component {
|
|||||||
window.socketManager.on(`productList:${categoryId}`, (response) => {
|
window.socketManager.on(`productList:${categoryId}`, (response) => {
|
||||||
console.log("getCategoryProducts full response", response);
|
console.log("getCategoryProducts full response", response);
|
||||||
receivedFullResponse = true;
|
receivedFullResponse = true;
|
||||||
setCachedCategoryData(categoryId, response, currentLanguage);
|
setCachedCategoryData(categoryId, response, currentLanguage, isHersteller);
|
||||||
if (response && response.products !== undefined) {
|
if (response && response.products !== undefined) {
|
||||||
this.processDataWithCategoryTree(response, categoryId);
|
this.processDataWithCategoryTree(response, categoryId);
|
||||||
} else {
|
} else {
|
||||||
@@ -370,12 +374,17 @@ class Content extends Component {
|
|||||||
|
|
||||||
window.socketManager.emit(
|
window.socketManager.emit(
|
||||||
"getCategoryProducts",
|
"getCategoryProducts",
|
||||||
{ categoryId: categoryId, language: currentLanguage, requestTranslation: currentLanguage === 'de' ? false : true },
|
{
|
||||||
|
categoryId: categoryId,
|
||||||
|
language: currentLanguage,
|
||||||
|
requestTranslation: currentLanguage === 'de' ? false : true,
|
||||||
|
isHersteller,
|
||||||
|
},
|
||||||
(response) => {
|
(response) => {
|
||||||
console.log("getCategoryProducts stub response", response);
|
console.log("getCategoryProducts stub response", response);
|
||||||
// Only process stub response if we haven't received the full response yet
|
// Only process stub response if we haven't received the full response yet
|
||||||
if (!receivedFullResponse) {
|
if (!receivedFullResponse) {
|
||||||
setCachedCategoryData(categoryId, response, currentLanguage);
|
setCachedCategoryData(categoryId, response, currentLanguage, isHersteller);
|
||||||
if (response && response.products !== undefined) {
|
if (response && response.products !== undefined) {
|
||||||
this.processDataWithCategoryTree(response, categoryId);
|
this.processDataWithCategoryTree(response, categoryId);
|
||||||
} else {
|
} else {
|
||||||
@@ -448,6 +457,29 @@ class Content extends Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// JTL kKategorie for category push: backend may omit dataParam — resolve from tree (same id as product list)
|
||||||
|
const isValidJtlCategoryId = (v) => {
|
||||||
|
if (v == null || v === '') return false;
|
||||||
|
const n = typeof v === 'number' ? v : parseInt(String(v), 10);
|
||||||
|
return Number.isFinite(n) && n > 0;
|
||||||
|
};
|
||||||
|
if (!this.props.isHersteller && categoryId !== 'neu' && categoryId !== 'bald' && !isValidJtlCategoryId(enhancedResponse.dataParam)) {
|
||||||
|
try {
|
||||||
|
const currentLanguage = this.props.languageContext?.currentLanguage || this.props.i18n?.language || 'de';
|
||||||
|
const categoryTreeCache = window.categoryService.getSync(209, currentLanguage);
|
||||||
|
if (categoryTreeCache) {
|
||||||
|
const targetCategory = typeof categoryId === 'string'
|
||||||
|
? this.findCategoryBySeoName(categoryTreeCache, categoryId)
|
||||||
|
: this.findCategoryById(categoryTreeCache, categoryId);
|
||||||
|
if (targetCategory && typeof targetCategory.id === 'number' && targetCategory.id > 0) {
|
||||||
|
enhancedResponse.dataParam = targetCategory.id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error resolving dataParam from category tree:', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
this.processData(enhancedResponse);
|
this.processData(enhancedResponse);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -701,7 +733,7 @@ class Content extends Component {
|
|||||||
minHeight: { xs: 'min-content', sm: '100%' }
|
minHeight: { xs: 'min-content', sm: '100%' }
|
||||||
}}>
|
}}>
|
||||||
|
|
||||||
<Box >
|
<Box sx={{ overflow: 'visible', minWidth: 0 }}>
|
||||||
|
|
||||||
<ProductFilters
|
<ProductFilters
|
||||||
products={this.state.unfilteredProducts}
|
products={this.state.unfilteredProducts}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import Box from '@mui/material/Box';
|
import Box from '@mui/material/Box';
|
||||||
import Typography from '@mui/material/Typography';
|
import Typography from '@mui/material/Typography';
|
||||||
|
import Chip from '@mui/material/Chip';
|
||||||
import Checkbox from '@mui/material/Checkbox';
|
import Checkbox from '@mui/material/Checkbox';
|
||||||
import IconButton from '@mui/material/IconButton';
|
import IconButton from '@mui/material/IconButton';
|
||||||
import Collapse from '@mui/material/Collapse';
|
import Collapse from '@mui/material/Collapse';
|
||||||
@@ -138,21 +139,25 @@ class Filter extends Component {
|
|||||||
|
|
||||||
handleOptionChange = (event) => {
|
handleOptionChange = (event) => {
|
||||||
const { name, checked } = event.target;
|
const { name, checked } = event.target;
|
||||||
|
const narrow =
|
||||||
// Update local state first to ensure immediate UI feedback
|
typeof window !== "undefined" && window.innerWidth < 600;
|
||||||
this.setState(prevState => ({
|
|
||||||
options: {
|
this.setState((prevState) => {
|
||||||
|
const nextOptions = {
|
||||||
...prevState.options,
|
...prevState.options,
|
||||||
[name]: checked
|
[name]: checked,
|
||||||
}
|
};
|
||||||
}));
|
return {
|
||||||
|
options: nextOptions,
|
||||||
// Then notify the parent component
|
...(narrow && checked ? { isCollapsed: true } : {}),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
if (this.props.onFilterChange) {
|
if (this.props.onFilterChange) {
|
||||||
this.props.onFilterChange({
|
this.props.onFilterChange({
|
||||||
type: this.props.filterType || 'default',
|
type: this.props.filterType || "default",
|
||||||
name: name,
|
name,
|
||||||
value: checked
|
value: checked,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -181,6 +186,13 @@ class Filter extends Component {
|
|||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
clearFilterOption = (optionId) => (event) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
this.handleOptionChange({
|
||||||
|
target: { name: optionId, checked: false },
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { options, counts, isCollapsed } = this.state;
|
const { options, counts, isCollapsed } = this.state;
|
||||||
const { title, options: optionsList = [] } = this.props;
|
const { title, options: optionsList = [] } = this.props;
|
||||||
@@ -267,11 +279,79 @@ class Filter extends Component {
|
|||||||
)}
|
)}
|
||||||
</Typography>
|
</Typography>
|
||||||
{isXsScreen && (
|
{isXsScreen && (
|
||||||
<IconButton size="small" aria-label={isCollapsed ? "Filter erweitern" : "Filter einklappen"} sx={{ p: 0 }}>
|
<IconButton
|
||||||
|
size="small"
|
||||||
|
aria-label={isCollapsed ? "Filter erweitern" : "Filter einklappen"}
|
||||||
|
sx={{ p: 0 }}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
this.toggleCollapse();
|
||||||
|
}}
|
||||||
|
>
|
||||||
{isCollapsed ? <ExpandMoreIcon /> : <ExpandLessIcon />}
|
{isCollapsed ? <ExpandMoreIcon /> : <ExpandLessIcon />}
|
||||||
</IconButton>
|
</IconButton>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
|
{isXsScreen &&
|
||||||
|
isCollapsed &&
|
||||||
|
optionsList.some((o) => options[o.id]) && (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
flexWrap: "wrap",
|
||||||
|
gap: 0.75,
|
||||||
|
mt: 0.5,
|
||||||
|
mb: 1,
|
||||||
|
pl: 0.25,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{optionsList
|
||||||
|
.filter((o) => options[o.id])
|
||||||
|
.map((option) => (
|
||||||
|
<Chip
|
||||||
|
key={option.id}
|
||||||
|
size="small"
|
||||||
|
variant="outlined"
|
||||||
|
clickable
|
||||||
|
onClick={this.clearFilterOption(option.id)}
|
||||||
|
onDelete={this.clearFilterOption(option.id)}
|
||||||
|
label={
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
display: "inline-flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: 6,
|
||||||
|
maxWidth: 200,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{this.props.filterType === "manufacturer" &&
|
||||||
|
this.props.manufacturerImages?.get(option.id) && (
|
||||||
|
<img
|
||||||
|
src={this.props.manufacturerImages.get(option.id)}
|
||||||
|
alt=""
|
||||||
|
style={{
|
||||||
|
height: 14,
|
||||||
|
width: "auto",
|
||||||
|
objectFit: "contain",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
overflow: "hidden",
|
||||||
|
textOverflow: "ellipsis",
|
||||||
|
whiteSpace: "nowrap",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{option.name}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
|
||||||
<Collapse in={!isXsScreen || !isCollapsed}>
|
<Collapse in={!isXsScreen || !isCollapsed}>
|
||||||
<Box sx={{ width: '100%' }}>
|
<Box sx={{ width: '100%' }}>
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ const StyledRouterLink = styled(RouterLink)(() => ({
|
|||||||
lineHeight: '1.5',
|
lineHeight: '1.5',
|
||||||
display: 'block',
|
display: 'block',
|
||||||
padding: '4px 8px',
|
padding: '4px 8px',
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
'&:hover': {
|
'&:hover': {
|
||||||
textDecoration: 'underline',
|
textDecoration: 'underline',
|
||||||
},
|
},
|
||||||
@@ -223,25 +224,13 @@ class Footer extends Component {
|
|||||||
alignItems={{ xs: 'center', md: 'flex-end' }}
|
alignItems={{ xs: 'center', md: 'flex-end' }}
|
||||||
>
|
>
|
||||||
{/* Legal Links Section */}
|
{/* Legal Links Section */}
|
||||||
<Stack
|
<Stack direction="column" spacing={0.5} justifyContent="center" alignItems="flex-start">
|
||||||
direction={{ xs: 'row', md: 'column' }}
|
|
||||||
spacing={{ xs: 2, md: 0.5 }}
|
|
||||||
justifyContent="center"
|
|
||||||
alignItems={{ xs: 'center', md: 'left' }}
|
|
||||||
flexWrap="wrap"
|
|
||||||
>
|
|
||||||
<StyledRouterLink to="/datenschutz">{this.props.t ? this.props.t('footer.legal.datenschutz') : 'Datenschutz'}</StyledRouterLink>
|
<StyledRouterLink to="/datenschutz">{this.props.t ? this.props.t('footer.legal.datenschutz') : 'Datenschutz'}</StyledRouterLink>
|
||||||
<StyledRouterLink to="/agb">{this.props.t ? this.props.t('footer.legal.agb') : 'AGB'}</StyledRouterLink>
|
<StyledRouterLink to="/agb">{this.props.t ? this.props.t('footer.legal.agb') : 'AGB'}</StyledRouterLink>
|
||||||
<StyledRouterLink to="/sitemap">{this.props.t ? this.props.t('footer.legal.sitemap') : 'Sitemap'}</StyledRouterLink>
|
<StyledRouterLink to="/sitemap">{this.props.t ? this.props.t('footer.legal.sitemap') : 'Sitemap'}</StyledRouterLink>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
<Stack
|
<Stack direction="column" spacing={0.5} justifyContent="center" alignItems="flex-start">
|
||||||
direction={{ xs: 'row', md: 'column' }}
|
|
||||||
spacing={{ xs: 2, md: 0.5 }}
|
|
||||||
justifyContent="center"
|
|
||||||
alignItems={{ xs: 'center', md: 'left' }}
|
|
||||||
flexWrap="wrap"
|
|
||||||
>
|
|
||||||
<StyledRouterLink to="/impressum">{this.props.t ? this.props.t('footer.legal.impressum') : 'Impressum'}</StyledRouterLink>
|
<StyledRouterLink to="/impressum">{this.props.t ? this.props.t('footer.legal.impressum') : 'Impressum'}</StyledRouterLink>
|
||||||
<StyledRouterLink to="/batteriegesetzhinweise">{this.props.t ? this.props.t('footer.legal.batteriegesetzhinweise') : 'Batteriegesetzhinweise'}</StyledRouterLink>
|
<StyledRouterLink to="/batteriegesetzhinweise">{this.props.t ? this.props.t('footer.legal.batteriegesetzhinweise') : 'Batteriegesetzhinweise'}</StyledRouterLink>
|
||||||
<StyledRouterLink to="/widerrufsrecht">{this.props.t ? this.props.t('footer.legal.widerrufsrecht') : 'Widerrufsrecht'}</StyledRouterLink>
|
<StyledRouterLink to="/widerrufsrecht">{this.props.t ? this.props.t('footer.legal.widerrufsrecht') : 'Widerrufsrecht'}</StyledRouterLink>
|
||||||
@@ -346,7 +335,7 @@ class Footer extends Component {
|
|||||||
|
|
||||||
{/* Copyright Section */}
|
{/* Copyright Section */}
|
||||||
<Box sx={{ pb: 0, textAlign: 'center', filter: 'drop-shadow(0 4px 8px rgba(0, 0, 0, 0.3))', opacity: 0.7 }}>
|
<Box sx={{ pb: 0, textAlign: 'center', filter: 'drop-shadow(0 4px 8px rgba(0, 0, 0, 0.3))', opacity: 0.7 }}>
|
||||||
<Typography variant="body2" sx={{ mb: 1, fontSize: { xs: '11px', md: '14px' }, lineHeight: 1.5 }}>
|
<Typography variant="body2" sx={{ mb: 1, fontSize: { xs: '11px', md: '14px' }, lineHeight: 1.5, whiteSpace: 'nowrap' }}>
|
||||||
{this.props.t ? this.props.t('footer.allPricesIncl') : '* Alle Preise inkl. gesetzlicher USt., zzgl. Versand'}
|
{this.props.t ? this.props.t('footer.allPricesIncl') : '* Alle Preise inkl. gesetzlicher USt., zzgl. Versand'}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography
|
<Typography
|
||||||
|
|||||||
121
src/components/IdleMainPagesSlideshow.js
Normal file
121
src/components/IdleMainPagesSlideshow.js
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
import { useEffect, useRef, useCallback } from "react";
|
||||||
|
import { useLocation, useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
|
/** Same order as the main landing tiles (home → Aktionen → Filiale). */
|
||||||
|
const MAIN_PAGE_PATHS = ["/", "/aktionen", "/filiale"];
|
||||||
|
|
||||||
|
/** No input for this long before the slideshow starts. */
|
||||||
|
const IDLE_MS = 90_000;
|
||||||
|
|
||||||
|
/** Time between automatic page changes once the slideshow is running. */
|
||||||
|
const SLIDESHOW_STEP_MS = 14_000;
|
||||||
|
|
||||||
|
/** Ignore duplicate events (mousemove etc.) within this window. */
|
||||||
|
const ACTIVITY_THROTTLE_MS = 400;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* After auto-navigation, ignore user-activity handlers briefly — route changes
|
||||||
|
* often emit scroll / mousemove / focus events that would call resetIdle() and
|
||||||
|
* clear the slideshow interval (only one slide before stopping).
|
||||||
|
*/
|
||||||
|
const POST_NAV_GRACE_MS = 3_000;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* After idle on /, /aktionen, or /filiale, cycles those routes slowly.
|
||||||
|
* Lives outside MainPageLayout so it is not reset when the route changes.
|
||||||
|
*/
|
||||||
|
export default function IdleMainPagesSlideshow() {
|
||||||
|
const location = useLocation();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const idleTimerRef = useRef(null);
|
||||||
|
const slideTimerRef = useRef(null);
|
||||||
|
const pathRef = useRef(location.pathname);
|
||||||
|
const wasOnMainPageRef = useRef(false);
|
||||||
|
const lastActivityRef = useRef(0);
|
||||||
|
const ignoreActivityUntilRef = useRef(0);
|
||||||
|
|
||||||
|
const resetIdleRef = useRef(() => {});
|
||||||
|
const clearTimersRef = useRef(() => {});
|
||||||
|
|
||||||
|
pathRef.current = location.pathname;
|
||||||
|
|
||||||
|
const clearTimers = useCallback(() => {
|
||||||
|
if (idleTimerRef.current != null) {
|
||||||
|
clearTimeout(idleTimerRef.current);
|
||||||
|
idleTimerRef.current = null;
|
||||||
|
}
|
||||||
|
if (slideTimerRef.current != null) {
|
||||||
|
clearInterval(slideTimerRef.current);
|
||||||
|
slideTimerRef.current = null;
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
clearTimersRef.current = clearTimers;
|
||||||
|
|
||||||
|
const startSlideshow = useCallback(() => {
|
||||||
|
let idx = MAIN_PAGE_PATHS.indexOf(pathRef.current);
|
||||||
|
if (idx < 0) idx = 0;
|
||||||
|
const advance = () => {
|
||||||
|
idx = (idx + 1) % MAIN_PAGE_PATHS.length;
|
||||||
|
ignoreActivityUntilRef.current = Date.now() + POST_NAV_GRACE_MS;
|
||||||
|
navigate(MAIN_PAGE_PATHS[idx], { replace: true });
|
||||||
|
};
|
||||||
|
slideTimerRef.current = setInterval(advance, SLIDESHOW_STEP_MS);
|
||||||
|
}, [navigate]);
|
||||||
|
|
||||||
|
const resetIdle = useCallback(() => {
|
||||||
|
clearTimers();
|
||||||
|
if (!MAIN_PAGE_PATHS.includes(pathRef.current)) return;
|
||||||
|
idleTimerRef.current = setTimeout(() => {
|
||||||
|
idleTimerRef.current = null;
|
||||||
|
startSlideshow();
|
||||||
|
}, IDLE_MS);
|
||||||
|
}, [clearTimers, startSlideshow]);
|
||||||
|
|
||||||
|
resetIdleRef.current = resetIdle;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const nowMain = MAIN_PAGE_PATHS.includes(location.pathname);
|
||||||
|
if (!nowMain) {
|
||||||
|
clearTimers();
|
||||||
|
wasOnMainPageRef.current = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!wasOnMainPageRef.current) {
|
||||||
|
resetIdle();
|
||||||
|
}
|
||||||
|
wasOnMainPageRef.current = true;
|
||||||
|
}, [location.pathname, clearTimers, resetIdle]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const onActivity = () => {
|
||||||
|
const now = Date.now();
|
||||||
|
if (now < ignoreActivityUntilRef.current) return;
|
||||||
|
if (now - lastActivityRef.current < ACTIVITY_THROTTLE_MS) return;
|
||||||
|
lastActivityRef.current = now;
|
||||||
|
resetIdleRef.current();
|
||||||
|
};
|
||||||
|
|
||||||
|
const events = [
|
||||||
|
"mousedown",
|
||||||
|
"keydown",
|
||||||
|
"touchstart",
|
||||||
|
"touchmove",
|
||||||
|
"wheel",
|
||||||
|
"click",
|
||||||
|
"scroll",
|
||||||
|
];
|
||||||
|
events.forEach((ev) =>
|
||||||
|
window.addEventListener(ev, onActivity, { passive: true })
|
||||||
|
);
|
||||||
|
window.addEventListener("mousemove", onActivity, { passive: true });
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
events.forEach((ev) => window.removeEventListener(ev, onActivity));
|
||||||
|
window.removeEventListener("mousemove", onActivity);
|
||||||
|
clearTimersRef.current();
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
@@ -240,7 +240,15 @@ export class LoginComponent extends Component {
|
|||||||
isAdmin: !!response.user.admin
|
isAdmin: !!response.user.admin
|
||||||
});
|
});
|
||||||
|
|
||||||
const redirectTo = location && location.hash ? `/profile${location.hash}` : '/profile';
|
const redirectTo = (() => {
|
||||||
|
// If we started login from the linkTelegram flow, come back there after auth.
|
||||||
|
// This prevents LinkTelegramPage from getting unmounted before the socket emit runs.
|
||||||
|
if (location?.pathname && location.pathname.startsWith('/linkTelegram')) {
|
||||||
|
return `${location.pathname}${location.search || ''}${location.hash || ''}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return location && location.hash ? `/profile${location.hash}` : '/profile';
|
||||||
|
})();
|
||||||
const dispatchLoginEvent = () => {
|
const dispatchLoginEvent = () => {
|
||||||
window.dispatchEvent(new CustomEvent('userLoggedIn'));
|
window.dispatchEvent(new CustomEvent('userLoggedIn'));
|
||||||
navigate(redirectTo);
|
navigate(redirectTo);
|
||||||
@@ -415,7 +423,14 @@ export class LoginComponent extends Component {
|
|||||||
user: response.user
|
user: response.user
|
||||||
});
|
});
|
||||||
|
|
||||||
const redirectTo = location && location.hash ? `/profile${location.hash}` : '/profile';
|
const redirectTo = (() => {
|
||||||
|
// If we started login from the linkTelegram flow, come back there after auth.
|
||||||
|
if (location?.pathname && location.pathname.startsWith('/linkTelegram')) {
|
||||||
|
return `${location.pathname}${location.search || ''}${location.hash || ''}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return location && location.hash ? `/profile${location.hash}` : '/profile';
|
||||||
|
})();
|
||||||
const dispatchLoginEvent = () => {
|
const dispatchLoginEvent = () => {
|
||||||
window.dispatchEvent(new CustomEvent('userLoggedIn'));
|
window.dispatchEvent(new CustomEvent('userLoggedIn'));
|
||||||
navigate(redirectTo);
|
navigate(redirectTo);
|
||||||
|
|||||||
@@ -14,24 +14,162 @@ import { STAR_POLYGON_POINTS } from "../utils/starPolygon.js";
|
|||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
const HOME_STAR_LAYERS = [
|
const HOME_STAR_LAYERS = [
|
||||||
{ className: "star-rotate-slow-cw", size: 168, staticDeg: 20, fill: "#B8860B" },
|
{ className: "star-rotate-slow-cw", size: 168 },
|
||||||
{ className: "star-rotate-slow-ccw", size: 159, staticDeg: -25, fill: "#DAA520" },
|
{ className: "star-rotate-slow-ccw", size: 159 },
|
||||||
{ className: "star-rotate-medium-cw", size: 150, staticDeg: null, fill: "#FFD700" },
|
{ className: "star-rotate-medium-cw", size: 150 },
|
||||||
];
|
];
|
||||||
|
|
||||||
const FILIALE_STAR_LAYERS = [
|
/** Teal/cyan stack for the right (Konfigurator) star — same motion, blue color scheme */
|
||||||
{ className: "star-rotate-slow-ccw", size: 168, staticDeg: 20, fill: "#5F9EA0" },
|
const TEAL_STAR_LAYERS = [
|
||||||
{ className: "star-rotate-medium-cw", size: 159, staticDeg: -25, fill: "#7FCDCD" },
|
{ className: "star-rotate-slow-ccw", size: 168 },
|
||||||
{ className: "star-rotate-slow-cw", size: 150, staticDeg: null, fill: "#AFEEEE" },
|
{ className: "star-rotate-medium-cw", size: 159 },
|
||||||
|
{ className: "star-rotate-slow-cw", size: 150 },
|
||||||
];
|
];
|
||||||
|
|
||||||
const StarDecorationLayers = ({ layers }) => (
|
/** Initial fill per variant (matches keyframe 0%) — avoids black flash before CSS animates */
|
||||||
|
const STAR_INITIAL_FILLS = {
|
||||||
|
home: ["#B8860B", "#DAA520", "#FFD700"],
|
||||||
|
filiale: ["#5F9EA0", "#7FCDCD", "#AFEEEE"],
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Injected in render (not useEffect) so first paint already has keyframes — avoids angle/color snap on load */
|
||||||
|
const STAR_DECORATION_CSS = `
|
||||||
|
@keyframes rotateClockwise {
|
||||||
|
from { transform: rotate(0deg); }
|
||||||
|
to { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
@keyframes rotateCounterClockwise {
|
||||||
|
from { transform: rotate(0deg); }
|
||||||
|
to { transform: rotate(-360deg); }
|
||||||
|
}
|
||||||
|
.star-rotate-slow-cw,
|
||||||
|
.star-rotate-slow-ccw,
|
||||||
|
.star-rotate-medium-cw {
|
||||||
|
transform-box: fill-box;
|
||||||
|
transform-origin: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.star-rotate-slow-cw {
|
||||||
|
animation: rotateClockwise 60s linear infinite;
|
||||||
|
}
|
||||||
|
.star-rotate-slow-ccw {
|
||||||
|
animation: rotateCounterClockwise 45s linear infinite;
|
||||||
|
}
|
||||||
|
.star-rotate-medium-cw {
|
||||||
|
animation: rotateClockwise 30s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.star-layer-svg-home {
|
||||||
|
mix-blend-mode: screen;
|
||||||
|
opacity: 0.92;
|
||||||
|
filter: drop-shadow(3px 3px 6px rgba(0, 0, 0, 0.5)) drop-shadow(0 0 10px rgba(255, 215, 0, 0.6)) drop-shadow(0 0 20px rgba(255, 215, 0, 0.3));
|
||||||
|
}
|
||||||
|
.star-layer-svg-filiale {
|
||||||
|
mix-blend-mode: soft-light;
|
||||||
|
opacity: 0.94;
|
||||||
|
filter: drop-shadow(3px 3px 6px rgba(0, 0, 0, 0.5)) drop-shadow(0 0 10px rgba(127, 205, 205, 0.6)) drop-shadow(0 0 18px rgba(95, 158, 160, 0.35));
|
||||||
|
}
|
||||||
|
.star-layer-svg {
|
||||||
|
shape-rendering: geometricPrecision;
|
||||||
|
transform: translateZ(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes starFillHome0 {
|
||||||
|
0%, 100% { fill: #B8860B; }
|
||||||
|
33% { fill: #FFD700; }
|
||||||
|
66% { fill: #DAA520; }
|
||||||
|
}
|
||||||
|
@keyframes starFillHome1 {
|
||||||
|
0%, 100% { fill: #DAA520; }
|
||||||
|
33% { fill: #B8860B; }
|
||||||
|
66% { fill: #FFD700; }
|
||||||
|
}
|
||||||
|
@keyframes starFillHome2 {
|
||||||
|
0%, 100% { fill: #FFD700; }
|
||||||
|
33% { fill: #DAA520; }
|
||||||
|
66% { fill: #B8860B; }
|
||||||
|
}
|
||||||
|
@keyframes starDriftHome0 {
|
||||||
|
0%, 100% { transform: rotate(20deg) translate(0px, 0px); }
|
||||||
|
50% { transform: rotate(20deg) translate(5px, -5px); }
|
||||||
|
}
|
||||||
|
@keyframes starDriftHome1 {
|
||||||
|
0%, 100% { transform: rotate(-25deg) translate(0px, 0px); }
|
||||||
|
50% { transform: rotate(-25deg) translate(-4px, 6px); }
|
||||||
|
}
|
||||||
|
@keyframes starDriftHome2 {
|
||||||
|
0%, 100% { transform: translate(0px, 0px); }
|
||||||
|
50% { transform: translate(3px, 4px); }
|
||||||
|
}
|
||||||
|
.star-layer-wrap.star-layer-home-0 {
|
||||||
|
animation: starDriftHome0 6.5s ease-in-out infinite;
|
||||||
|
animation-fill-mode: both;
|
||||||
|
}
|
||||||
|
.star-layer-wrap.star-layer-home-1 {
|
||||||
|
animation: starDriftHome1 7s ease-in-out 0.4s infinite;
|
||||||
|
animation-fill-mode: both;
|
||||||
|
}
|
||||||
|
.star-layer-wrap.star-layer-home-2 {
|
||||||
|
animation: starDriftHome2 5.5s ease-in-out 0.8s infinite;
|
||||||
|
animation-fill-mode: both;
|
||||||
|
}
|
||||||
|
.star-poly-home-0 { animation: starFillHome0 10s ease-in-out 0s infinite both; }
|
||||||
|
.star-poly-home-1 { animation: starFillHome1 10s ease-in-out 1.1s infinite both; }
|
||||||
|
.star-poly-home-2 { animation: starFillHome2 10s ease-in-out 2.2s infinite both; }
|
||||||
|
|
||||||
|
@keyframes starFillFil0 {
|
||||||
|
0%, 100% { fill: #5F9EA0; }
|
||||||
|
33% { fill: #AFEEEE; }
|
||||||
|
66% { fill: #7FCDCD; }
|
||||||
|
}
|
||||||
|
@keyframes starFillFil1 {
|
||||||
|
0%, 100% { fill: #7FCDCD; }
|
||||||
|
33% { fill: #5F9EA0; }
|
||||||
|
66% { fill: #AFEEEE; }
|
||||||
|
}
|
||||||
|
@keyframes starFillFil2 {
|
||||||
|
0%, 100% { fill: #AFEEEE; }
|
||||||
|
33% { fill: #7FCDCD; }
|
||||||
|
66% { fill: #5F9EA0; }
|
||||||
|
}
|
||||||
|
@keyframes starDriftFil0 {
|
||||||
|
0%, 100% { transform: rotate(20deg) translate(0px, 0px); }
|
||||||
|
50% { transform: rotate(20deg) translate(4px, -4px); }
|
||||||
|
}
|
||||||
|
@keyframes starDriftFil1 {
|
||||||
|
0%, 100% { transform: rotate(-25deg) translate(0px, 0px); }
|
||||||
|
50% { transform: rotate(-25deg) translate(-5px, 5px); }
|
||||||
|
}
|
||||||
|
@keyframes starDriftFil2 {
|
||||||
|
0%, 100% { transform: translate(0px, 0px); }
|
||||||
|
50% { transform: translate(3px, 3px); }
|
||||||
|
}
|
||||||
|
.star-layer-wrap.star-layer-filiale-0 {
|
||||||
|
animation: starDriftFil0 6.5s ease-in-out infinite;
|
||||||
|
animation-fill-mode: both;
|
||||||
|
}
|
||||||
|
.star-layer-wrap.star-layer-filiale-1 {
|
||||||
|
animation: starDriftFil1 7s ease-in-out 0.4s infinite;
|
||||||
|
animation-fill-mode: both;
|
||||||
|
}
|
||||||
|
.star-layer-wrap.star-layer-filiale-2 {
|
||||||
|
animation: starDriftFil2 5.5s ease-in-out 0.8s infinite;
|
||||||
|
animation-fill-mode: both;
|
||||||
|
}
|
||||||
|
.star-poly-filiale-0 { animation: starFillFil0 10s ease-in-out 0s infinite both; }
|
||||||
|
.star-poly-filiale-1 { animation: starFillFil1 10s ease-in-out 1.1s infinite both; }
|
||||||
|
.star-poly-filiale-2 { animation: starFillFil2 10s ease-in-out 2.2s infinite both; }
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StarDecorationLayers = ({ layers, variant }) => (
|
||||||
<>
|
<>
|
||||||
{layers.map(({ className, size, staticDeg, fill }, i) => {
|
{layers.map(({ className, size }, i) => {
|
||||||
const half = size / 2;
|
const half = size / 2;
|
||||||
|
const initialFill = STAR_INITIAL_FILLS[variant][i];
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={i}
|
key={i}
|
||||||
|
className={`star-layer-wrap star-layer-${variant}-${i}`}
|
||||||
style={{
|
style={{
|
||||||
position: "absolute",
|
position: "absolute",
|
||||||
left: "50%",
|
left: "50%",
|
||||||
@@ -40,11 +178,21 @@ const StarDecorationLayers = ({ layers }) => (
|
|||||||
height: size,
|
height: size,
|
||||||
marginLeft: -half,
|
marginLeft: -half,
|
||||||
marginTop: -half,
|
marginTop: -half,
|
||||||
...(staticDeg != null ? { transform: `rotate(${staticDeg}deg)` } : {}),
|
zIndex: 3 - i,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<svg viewBox="0 0 60 60" width="100%" height="100%" className={className} style={{ display: "block" }}>
|
<svg
|
||||||
<polygon points={STAR_POLYGON_POINTS} fill={fill} />
|
viewBox="0 0 60 60"
|
||||||
|
width="100%"
|
||||||
|
height="100%"
|
||||||
|
className={`${className} star-layer-svg star-layer-svg-${variant}`}
|
||||||
|
style={{ display: "block" }}
|
||||||
|
>
|
||||||
|
<polygon
|
||||||
|
points={STAR_POLYGON_POINTS}
|
||||||
|
fill={initialFill}
|
||||||
|
className={`star-poly-fill star-poly-${variant}-${i}`}
|
||||||
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -65,11 +213,10 @@ const ContentBox = ({ box, index, pageType, starHovered, setStarHovered, opacity
|
|||||||
zIndex: 999,
|
zIndex: 999,
|
||||||
pointerEvents: 'none',
|
pointerEvents: 'none',
|
||||||
'& *': { pointerEvents: 'none' },
|
'& *': { pointerEvents: 'none' },
|
||||||
filter: 'drop-shadow(6px 6px 12px rgba(0, 0, 0, 0.6)) drop-shadow(0 0 20px rgba(255, 215, 0, 0.8)) drop-shadow(0 0 40px rgba(255, 215, 0, 0.4))',
|
|
||||||
display: { xs: 'none', sm: 'block' }
|
display: { xs: 'none', sm: 'block' }
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<StarDecorationLayers layers={HOME_STAR_LAYERS} />
|
<StarDecorationLayers layers={HOME_STAR_LAYERS} variant="home" />
|
||||||
<div style={{ position: 'absolute', top: '50%', left: '50%', transform: 'translate(-50%, -50%) rotate(-10deg)', color: 'white', fontWeight: '900', fontSize: '20px', textShadow: '0px 3px 6px rgba(0,0,0,0.5)', zIndex: 1000, textAlign: 'center', lineHeight: '1.1', width: '135px', transition: 'opacity 0.3s ease', opacity: starHovered ? 0 : 1 }}>
|
<div style={{ position: 'absolute', top: '50%', left: '50%', transform: 'translate(-50%, -50%) rotate(-10deg)', color: 'white', fontWeight: '900', fontSize: '20px', textShadow: '0px 3px 6px rgba(0,0,0,0.5)', zIndex: 1000, textAlign: 'center', lineHeight: '1.1', width: '135px', transition: 'opacity 0.3s ease', opacity: starHovered ? 0 : 1 }}>
|
||||||
{translatedContent.outdoorSeason}
|
{translatedContent.outdoorSeason}
|
||||||
</div>
|
</div>
|
||||||
@@ -78,7 +225,7 @@ const ContentBox = ({ box, index, pageType, starHovered, setStarHovered, opacity
|
|||||||
</div>
|
</div>
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
{index === 1 && pageType === "filiale" && (
|
{index === 1 && pageType === "home" && (
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
@@ -89,13 +236,12 @@ const ContentBox = ({ box, index, pageType, starHovered, setStarHovered, opacity
|
|||||||
zIndex: 999,
|
zIndex: 999,
|
||||||
pointerEvents: 'none',
|
pointerEvents: 'none',
|
||||||
'& *': { pointerEvents: 'none' },
|
'& *': { pointerEvents: 'none' },
|
||||||
filter: 'drop-shadow(6px 6px 12px rgba(0, 0, 0, 0.6)) drop-shadow(0 0 20px rgba(175, 238, 238, 0.8)) drop-shadow(0 0 40px rgba(175, 238, 238, 0.4))',
|
|
||||||
display: { xs: 'none', sm: 'block' }
|
display: { xs: 'none', sm: 'block' }
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<StarDecorationLayers layers={FILIALE_STAR_LAYERS} />
|
<StarDecorationLayers layers={TEAL_STAR_LAYERS} variant="filiale" />
|
||||||
<div style={{ position: 'absolute', top: '50%', left: '50%', transform: 'translate(-50%, -50%) rotate(10deg)', color: 'white', fontWeight: '900', fontSize: '20px', textShadow: '0px 3px 6px rgba(0,0,0,0.5)', zIndex: 1000, textAlign: 'center', lineHeight: '1.1', width: '135px' }}>
|
<div style={{ position: 'absolute', top: '50%', left: '50%', transform: 'translate(-50%, -50%) rotate(10deg)', color: 'white', fontWeight: '900', fontSize: '20px', textShadow: '0px 3px 6px rgba(0,0,0,0.5)', zIndex: 1000, textAlign: 'center', lineHeight: '1.1', width: '135px' }}>
|
||||||
{translatedContent.showUsPhoto}
|
{translatedContent.buildYourSet}
|
||||||
</div>
|
</div>
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
@@ -113,16 +259,16 @@ const ContentBox = ({ box, index, pageType, starHovered, setStarHovered, opacity
|
|||||||
display: "flex",
|
display: "flex",
|
||||||
flexDirection: "column",
|
flexDirection: "column",
|
||||||
boxShadow: 10,
|
boxShadow: 10,
|
||||||
transition: "all 0.3s ease",
|
transition: "box-shadow 0.3s ease",
|
||||||
"&:hover": { transform: "translateY(-5px)", boxShadow: 20 },
|
"&:hover": { boxShadow: 20 },
|
||||||
}}
|
}}
|
||||||
onMouseEnter={
|
onMouseEnter={
|
||||||
(pageType === "home" && index === 0) || (pageType === "filiale" && index === 1)
|
pageType === "home" && index === 0
|
||||||
? () => setStarHovered(true)
|
? () => setStarHovered(true)
|
||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
onMouseLeave={
|
onMouseLeave={
|
||||||
(pageType === "home" && index === 0) || (pageType === "filiale" && index === 1)
|
pageType === "home" && index === 0
|
||||||
? () => setStarHovered(false)
|
? () => setStarHovered(false)
|
||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
@@ -145,8 +291,22 @@ const MainPageLayout = () => {
|
|||||||
const currentPath = location.pathname;
|
const currentPath = location.pathname;
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [starHovered, setStarHovered] = React.useState(false);
|
const [starHovered, setStarHovered] = React.useState(false);
|
||||||
|
|
||||||
|
// State to track kiosk mode
|
||||||
|
const [isKiosk, setIsKiosk] = React.useState(() => window.growheadskiosk === true);
|
||||||
|
|
||||||
|
// Listen for the custom event
|
||||||
|
React.useEffect(() => {
|
||||||
|
const handleKioskChange = () => {
|
||||||
|
setIsKiosk(window.growheadskiosk === true);
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener('growheadskiosk-change', handleKioskChange);
|
||||||
|
return () => window.removeEventListener('growheadskiosk-change', handleKioskChange);
|
||||||
|
}, []);
|
||||||
|
|
||||||
const translatedContent = {
|
const translatedContent = {
|
||||||
showUsPhoto: t('sections.showUsPhoto'),
|
buildYourSet: isKiosk ? 'Schau in den Stecklingskatalog' : t('sections.buildYourSet'),
|
||||||
selectSeedRate: t('sections.selectSeedRate'),
|
selectSeedRate: t('sections.selectSeedRate'),
|
||||||
outdoorSeason: t('sections.outdoorSeason')
|
outdoorSeason: t('sections.outdoorSeason')
|
||||||
};
|
};
|
||||||
@@ -155,37 +315,6 @@ const MainPageLayout = () => {
|
|||||||
const isAktionen = currentPath === "/aktionen";
|
const isAktionen = currentPath === "/aktionen";
|
||||||
const isFiliale = currentPath === "/filiale";
|
const isFiliale = currentPath === "/filiale";
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
const style = document.createElement('style');
|
|
||||||
style.textContent = `
|
|
||||||
@keyframes rotateClockwise {
|
|
||||||
from { transform: rotate(0deg); }
|
|
||||||
to { transform: rotate(360deg); }
|
|
||||||
}
|
|
||||||
@keyframes rotateCounterClockwise {
|
|
||||||
from { transform: rotate(0deg); }
|
|
||||||
to { transform: rotate(-360deg); }
|
|
||||||
}
|
|
||||||
.star-rotate-slow-cw,
|
|
||||||
.star-rotate-slow-ccw,
|
|
||||||
.star-rotate-medium-cw {
|
|
||||||
transform-box: fill-box;
|
|
||||||
transform-origin: center;
|
|
||||||
}
|
|
||||||
.star-rotate-slow-cw {
|
|
||||||
animation: rotateClockwise 60s linear infinite;
|
|
||||||
}
|
|
||||||
.star-rotate-slow-ccw {
|
|
||||||
animation: rotateCounterClockwise 45s linear infinite;
|
|
||||||
}
|
|
||||||
.star-rotate-medium-cw {
|
|
||||||
animation: rotateClockwise 30s linear infinite;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
document.head.appendChild(style);
|
|
||||||
return () => document.head.removeChild(style);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const getNavigationConfig = () => {
|
const getNavigationConfig = () => {
|
||||||
if (isHome) return { leftNav: { text: t('navigation.aktionen'), link: "/aktionen" }, rightNav: { text: t('navigation.filiale'), link: "/filiale" } };
|
if (isHome) return { leftNav: { text: t('navigation.aktionen'), link: "/aktionen" }, rightNav: { text: t('navigation.filiale'), link: "/filiale" } };
|
||||||
if (isAktionen) return { leftNav: { text: t('navigation.filiale'), link: "/filiale" }, rightNav: { text: t('navigation.home'), link: "/" } };
|
if (isAktionen) return { leftNav: { text: t('navigation.filiale'), link: "/filiale" }, rightNav: { text: t('navigation.home'), link: "/" } };
|
||||||
@@ -202,11 +331,11 @@ const MainPageLayout = () => {
|
|||||||
const allContentBoxes = {
|
const allContentBoxes = {
|
||||||
home: [
|
home: [
|
||||||
{ title: t('sections.seeds'), image: "/assets/images/seeds.avif", bgcolor: "#e1f0d3", link: "/Kategorie/Seeds" },
|
{ title: t('sections.seeds'), image: "/assets/images/seeds.avif", bgcolor: "#e1f0d3", link: "/Kategorie/Seeds" },
|
||||||
{ title: t('sections.konfigurator'), image: "/assets/images/konfigurator.avif", bgcolor: "#e8f5d6", link: "/Konfigurator" }
|
{ title: isKiosk ? 'Stecklingskatalog' : t('sections.konfigurator'), image: isKiosk ? "/assets/images/cutlings2.avif" : "/assets/images/konfigurator.avif", bgcolor: "#e8f5d6", link: isKiosk ? "https://cloneheads.de" : "/Konfigurator" }
|
||||||
],
|
],
|
||||||
aktionen: [
|
aktionen: [
|
||||||
{ title: t('sections.oilPress'), image: "/assets/images/presse.jpg", bgcolor: "#e1f0d3", link: "/presseverleih" },
|
{ title: t('sections.oilPress'), image: "/assets/images/presse.jpg", bgcolor: "#e1f0d3", link: "/Artikel/Graveda-10t-presse-tagesmiete-inkl-prepress-vorpressform" },
|
||||||
{ title: t('sections.thcTest'), image: "/assets/images/purpl.jpg", bgcolor: "#e8f5d6", link: "/thc-test" }
|
{ title: t('sections.thcTest'), image: "/assets/images/purpl.jpg", bgcolor: "#e8f5d6", link: "/Artikel/1x-messung-purplpro-thc-cbd-restfeuchte-wasseraktivitaet" }
|
||||||
],
|
],
|
||||||
filiale: [
|
filiale: [
|
||||||
{ title: t('sections.address1'), image: "/assets/images/filiale1.jpg", bgcolor: "#e1f0d3", link: "/filiale" },
|
{ title: t('sections.address1'), image: "/assets/images/filiale1.jpg", bgcolor: "#e1f0d3", link: "/filiale" },
|
||||||
@@ -231,6 +360,7 @@ const MainPageLayout = () => {
|
|||||||
return (
|
return (
|
||||||
<Container maxWidth="lg" sx={{ py: 2 }}>
|
<Container maxWidth="lg" sx={{ py: 2 }}>
|
||||||
<style>{getCombinedAnimatedBorderStyles(['seeds', 'cutlings'])}</style>
|
<style>{getCombinedAnimatedBorderStyles(['seeds', 'cutlings'])}</style>
|
||||||
|
<style>{STAR_DECORATION_CSS}</style>
|
||||||
<Box sx={{ display: "flex", alignItems: "center", justifyContent: "space-between", mb: 4, mt: 2, px: 0, transition: "all 0.3s ease-in-out", flexDirection: { xs: "column", sm: "row" } }}>
|
<Box sx={{ display: "flex", alignItems: "center", justifyContent: "space-between", mb: 4, mt: 2, px: 0, transition: "all 0.3s ease-in-out", flexDirection: { xs: "column", sm: "row" } }}>
|
||||||
<Box sx={{ display: { xs: "block", sm: "none" }, mb: { xs: 2, sm: 0 }, width: "100%", textAlign: "center", position: "relative" }}>
|
<Box sx={{ display: { xs: "block", sm: "none" }, mb: { xs: 2, sm: 0 }, width: "100%", textAlign: "center", position: "relative" }}>
|
||||||
{Object.entries(allTitles).map(([pageType, title]) => (
|
{Object.entries(allTitles).map(([pageType, title]) => (
|
||||||
|
|||||||
@@ -1,17 +1,25 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
import Box from "@mui/material/Box";
|
import Box from "@mui/material/Box";
|
||||||
import Typography from "@mui/material/Typography";
|
import Typography from "@mui/material/Typography";
|
||||||
|
import Paper from "@mui/material/Paper";
|
||||||
|
import IconButton from "@mui/material/IconButton";
|
||||||
|
import ChevronLeft from "@mui/icons-material/ChevronLeft";
|
||||||
|
import ChevronRight from "@mui/icons-material/ChevronRight";
|
||||||
import { withTranslation } from 'react-i18next';
|
import { withTranslation } from 'react-i18next';
|
||||||
import { withLanguage } from '../i18n/withTranslation.js';
|
import { withLanguage } from '../i18n/withTranslation.js';
|
||||||
|
|
||||||
const ITEM_WIDTH = 140 + 16; // 140px width + 16px gap
|
const ITEM_WIDTH = 140 + 16; // 140px width + 16px gap
|
||||||
const AUTO_SCROLL_SPEED = 1.0;
|
const AUTO_SCROLL_SPEED = 1.0;
|
||||||
|
const AUTOSCROLL_RESTART_DELAY = 5000;
|
||||||
|
|
||||||
class ManufacturerCarousel extends React.Component {
|
class ManufacturerCarousel extends React.Component {
|
||||||
_isMounted = false;
|
_isMounted = false;
|
||||||
originalItems = [];
|
originalItems = [];
|
||||||
animationFrame = null;
|
animationFrame = null;
|
||||||
|
autoScrollActive = true;
|
||||||
translateX = 0;
|
translateX = 0;
|
||||||
|
inactivityTimer = null;
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
@@ -28,10 +36,8 @@ class ManufacturerCarousel extends React.Component {
|
|||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
this._isMounted = false;
|
this._isMounted = false;
|
||||||
if (this.animationFrame) {
|
this.stopAutoScroll();
|
||||||
cancelAnimationFrame(this.animationFrame);
|
this.clearInactivityTimer();
|
||||||
this.animationFrame = null;
|
|
||||||
}
|
|
||||||
// Revoke object URLs to avoid memory leaks
|
// Revoke object URLs to avoid memory leaks
|
||||||
for (const item of this.originalItems) {
|
for (const item of this.originalItems) {
|
||||||
if (item.src) URL.revokeObjectURL(item.src);
|
if (item.src) URL.revokeObjectURL(item.src);
|
||||||
@@ -46,7 +52,12 @@ class ManufacturerCarousel extends React.Component {
|
|||||||
.filter(m => m.imageBuffer)
|
.filter(m => m.imageBuffer)
|
||||||
.map(m => {
|
.map(m => {
|
||||||
const blob = new Blob([m.imageBuffer], { type: 'image/avif' });
|
const blob = new Blob([m.imageBuffer], { type: 'image/avif' });
|
||||||
return { id: m.id, name: m.name || '', src: URL.createObjectURL(blob) };
|
return {
|
||||||
|
id: m.id,
|
||||||
|
name: m.name || '',
|
||||||
|
slug: m.slug || '',
|
||||||
|
src: URL.createObjectURL(blob),
|
||||||
|
};
|
||||||
})
|
})
|
||||||
.sort(() => Math.random() - 0.5);
|
.sort(() => Math.random() - 0.5);
|
||||||
|
|
||||||
@@ -60,13 +71,38 @@ class ManufacturerCarousel extends React.Component {
|
|||||||
};
|
};
|
||||||
|
|
||||||
startAutoScroll = () => {
|
startAutoScroll = () => {
|
||||||
|
this.autoScrollActive = true;
|
||||||
if (!this.animationFrame) {
|
if (!this.animationFrame) {
|
||||||
this.animationFrame = requestAnimationFrame(this.tick);
|
this.animationFrame = requestAnimationFrame(this.tick);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
stopAutoScroll = () => {
|
||||||
|
this.autoScrollActive = false;
|
||||||
|
if (this.animationFrame) {
|
||||||
|
cancelAnimationFrame(this.animationFrame);
|
||||||
|
this.animationFrame = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
clearInactivityTimer = () => {
|
||||||
|
if (this.inactivityTimer) {
|
||||||
|
clearTimeout(this.inactivityTimer);
|
||||||
|
this.inactivityTimer = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
startInactivityTimer = () => {
|
||||||
|
this.clearInactivityTimer();
|
||||||
|
this.inactivityTimer = setTimeout(() => {
|
||||||
|
if (this._isMounted) {
|
||||||
|
this.startAutoScroll();
|
||||||
|
}
|
||||||
|
}, AUTOSCROLL_RESTART_DELAY);
|
||||||
|
};
|
||||||
|
|
||||||
tick = () => {
|
tick = () => {
|
||||||
if (!this._isMounted || this.originalItems.length === 0) return;
|
if (!this._isMounted || !this.autoScrollActive || this.originalItems.length === 0) return;
|
||||||
|
|
||||||
this.translateX -= AUTO_SCROLL_SPEED;
|
this.translateX -= AUTO_SCROLL_SPEED;
|
||||||
|
|
||||||
@@ -82,6 +118,41 @@ class ManufacturerCarousel extends React.Component {
|
|||||||
this.animationFrame = requestAnimationFrame(this.tick);
|
this.animationFrame = requestAnimationFrame(this.tick);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
updateTrackTransform = () => {
|
||||||
|
if (this.carouselTrackRef.current) {
|
||||||
|
this.carouselTrackRef.current.style.transform = `translateX(${this.translateX}px)`;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
scrollBy = (direction) => {
|
||||||
|
if (this.originalItems.length === 0) return;
|
||||||
|
|
||||||
|
const originalItemCount = this.originalItems.length;
|
||||||
|
const maxScroll = ITEM_WIDTH * originalItemCount;
|
||||||
|
|
||||||
|
this.translateX += direction * ITEM_WIDTH;
|
||||||
|
|
||||||
|
if (this.translateX > 0) {
|
||||||
|
this.translateX = -(maxScroll - ITEM_WIDTH);
|
||||||
|
} else if (Math.abs(this.translateX) >= maxScroll) {
|
||||||
|
this.translateX = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.updateTrackTransform();
|
||||||
|
};
|
||||||
|
|
||||||
|
handleLeftClick = () => {
|
||||||
|
this.stopAutoScroll();
|
||||||
|
this.scrollBy(1);
|
||||||
|
this.startInactivityTimer();
|
||||||
|
};
|
||||||
|
|
||||||
|
handleRightClick = () => {
|
||||||
|
this.stopAutoScroll();
|
||||||
|
this.scrollBy(-1);
|
||||||
|
this.startInactivityTimer();
|
||||||
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { t } = this.props;
|
const { t } = this.props;
|
||||||
const { items } = this.state;
|
const { items } = this.state;
|
||||||
@@ -90,19 +161,36 @@ class ManufacturerCarousel extends React.Component {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Box sx={{ mt: 4, mb: 4 }}>
|
<Box sx={{ mt: 4, mb: 4 }}>
|
||||||
<Typography
|
<Box
|
||||||
variant="h4"
|
component={Link}
|
||||||
component="div"
|
to="/Hersteller"
|
||||||
sx={{
|
sx={{
|
||||||
fontFamily: 'SwashingtonCP',
|
display: 'flex',
|
||||||
textShadow: '3px 3px 10px rgba(0, 0, 0, 0.4)',
|
alignItems: 'center',
|
||||||
textAlign: 'center',
|
justifyContent: 'center',
|
||||||
mb: 2,
|
textDecoration: 'none',
|
||||||
color: 'primary.main',
|
color: 'primary.main',
|
||||||
|
mb: 2,
|
||||||
|
transition: 'all 0.3s ease',
|
||||||
|
'&:hover': {
|
||||||
|
transform: 'translateX(5px)',
|
||||||
|
color: 'primary.dark',
|
||||||
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{t('product.manufacturer')}
|
<Typography
|
||||||
</Typography>
|
variant="h4"
|
||||||
|
component="span"
|
||||||
|
sx={{
|
||||||
|
fontFamily: 'SwashingtonCP',
|
||||||
|
textShadow: '3px 3px 10px rgba(0, 0, 0, 0.4)',
|
||||||
|
textAlign: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t('product.manufacturer')}
|
||||||
|
</Typography>
|
||||||
|
<ChevronRight sx={{ fontSize: '2.5rem', ml: 1 }} />
|
||||||
|
</Box>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
@@ -129,6 +217,46 @@ class ManufacturerCarousel extends React.Component {
|
|||||||
zIndex: 2, pointerEvents: 'none',
|
zIndex: 2, pointerEvents: 'none',
|
||||||
}} />
|
}} />
|
||||||
|
|
||||||
|
{/* Left Arrow */}
|
||||||
|
<IconButton
|
||||||
|
aria-label="Vorherige Hersteller anzeigen"
|
||||||
|
onClick={this.handleLeftClick}
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
top: '50%',
|
||||||
|
left: '8px',
|
||||||
|
transform: 'translateY(-50%)',
|
||||||
|
zIndex: 1000,
|
||||||
|
backgroundColor: 'rgba(255, 255, 255, 0.9)',
|
||||||
|
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.15)',
|
||||||
|
width: '48px',
|
||||||
|
height: '48px',
|
||||||
|
borderRadius: '50%'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ChevronLeft />
|
||||||
|
</IconButton>
|
||||||
|
|
||||||
|
{/* Right Arrow */}
|
||||||
|
<IconButton
|
||||||
|
aria-label="Nächste Hersteller anzeigen"
|
||||||
|
onClick={this.handleRightClick}
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
top: '50%',
|
||||||
|
right: '8px',
|
||||||
|
transform: 'translateY(-50%)',
|
||||||
|
zIndex: 1000,
|
||||||
|
backgroundColor: 'rgba(255, 255, 255, 0.9)',
|
||||||
|
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.15)',
|
||||||
|
width: '48px',
|
||||||
|
height: '48px',
|
||||||
|
borderRadius: '50%'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ChevronRight />
|
||||||
|
</IconButton>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
@@ -151,8 +279,11 @@ class ManufacturerCarousel extends React.Component {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{items.map((item, index) => (
|
{items.map((item, index) => (
|
||||||
<div
|
<Paper
|
||||||
key={`${item.id}-${index}`}
|
key={`${item.id}-${index}`}
|
||||||
|
component={Link}
|
||||||
|
to={`/Hersteller/${encodeURIComponent(item.slug || '')}`}
|
||||||
|
elevation={3}
|
||||||
style={{
|
style={{
|
||||||
flex: '0 0 140px',
|
flex: '0 0 140px',
|
||||||
width: '140px',
|
width: '140px',
|
||||||
@@ -162,7 +293,20 @@ class ManufacturerCarousel extends React.Component {
|
|||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
userSelect: 'none',
|
userSelect: 'none',
|
||||||
pointerEvents: 'none',
|
textDecoration: 'none',
|
||||||
|
cursor: 'pointer',
|
||||||
|
borderRadius: '8px',
|
||||||
|
position: 'relative',
|
||||||
|
zIndex: 10,
|
||||||
|
backgroundColor: '#f0f0f0',
|
||||||
|
boxShadow: '0px 2px 1px -1px rgba(0,0,0,0.2), 0px 1px 1px 0px rgba(0,0,0,0.14), 0px 1px 3px 0px rgba(0,0,0,0.12)'
|
||||||
|
}}
|
||||||
|
sx={{
|
||||||
|
transition: 'transform 0.2s ease, box-shadow 0.2s ease',
|
||||||
|
'&:hover': {
|
||||||
|
transform: 'translateY(-5px)',
|
||||||
|
boxShadow: 8,
|
||||||
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
@@ -176,7 +320,7 @@ class ManufacturerCarousel extends React.Component {
|
|||||||
display: 'block',
|
display: 'block',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</Paper>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1089,6 +1089,8 @@ class ProductDetailPage extends Component {
|
|||||||
const { product, loading, upgrading, error, attributeImages, /*isSteckling,*/ attributes, komponentenLoaded, komponentenData, komponentenImages, totalKomponentenPrice, totalSavings, shareAnchorEl, sharePopperOpen, snackbarOpen, snackbarMessage, snackbarSeverity } =
|
const { product, loading, upgrading, error, attributeImages, /*isSteckling,*/ attributes, komponentenLoaded, komponentenData, komponentenImages, totalKomponentenPrice, totalSavings, shareAnchorEl, sharePopperOpen, snackbarOpen, snackbarMessage, snackbarSeverity } =
|
||||||
this.state;
|
this.state;
|
||||||
|
|
||||||
|
const hasAttributeImages = attributes.some((attr) => attributeImages[attr.kMerkmalWert]);
|
||||||
|
|
||||||
// Debug alerts removed
|
// Debug alerts removed
|
||||||
|
|
||||||
|
|
||||||
@@ -1172,18 +1174,17 @@ class ProductDetailPage extends Component {
|
|||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
mb: 2,
|
mb: 2,
|
||||||
position: ["-webkit-sticky", "sticky"], // Provide both prefixed and standard
|
position: "sticky",
|
||||||
top: {
|
top: {
|
||||||
xs: "110px",
|
xs: "calc(env(safe-area-inset-top, 0px) + 160px)",
|
||||||
sm: "110px",
|
sm: "110px",
|
||||||
md: "110px",
|
md: "110px",
|
||||||
lg: "110px",
|
lg: "110px",
|
||||||
} /* Offset to sit below the header 120 mith menu for md and lg*/,
|
},
|
||||||
left: 0,
|
left: 0,
|
||||||
width: "100%",
|
width: "100%",
|
||||||
display: "flex",
|
display: "flex",
|
||||||
zIndex: (theme) =>
|
zIndex: (theme) => theme.zIndex.appBar - 1,
|
||||||
theme.zIndex.appBar - 1 /* Just below the AppBar */,
|
|
||||||
py: 0,
|
py: 0,
|
||||||
px: 2,
|
px: 2,
|
||||||
}}
|
}}
|
||||||
@@ -1198,10 +1199,19 @@ class ProductDetailPage extends Component {
|
|||||||
borderRadius: 1,
|
borderRadius: 1,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Typography variant="body2" color="text.secondary">
|
<Typography variant="body2" color="text.secondary" component="div">
|
||||||
<Link
|
<Link
|
||||||
to="/"
|
to="/"
|
||||||
onClick={() => this.props.navigate(-1)}
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (this.props.navigate) {
|
||||||
|
if (typeof window !== "undefined" && window.history.length > 1) {
|
||||||
|
this.props.navigate(-1);
|
||||||
|
} else {
|
||||||
|
this.props.navigate("/");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
style={{
|
style={{
|
||||||
paddingLeft: 16,
|
paddingLeft: 16,
|
||||||
paddingRight: 16,
|
paddingRight: 16,
|
||||||
@@ -1298,7 +1308,19 @@ class ProductDetailPage extends Component {
|
|||||||
|
|
||||||
<Box sx={{ minHeight: "107px", display: "flex", justifyContent: "space-between", alignItems: "flex-start", mb: 2, gap: 2 }}>
|
<Box sx={{ minHeight: "107px", display: "flex", justifyContent: "space-between", alignItems: "flex-start", mb: 2, gap: 2 }}>
|
||||||
{(attributes.some(attr => attributeImages[attr.kMerkmalWert]) || attributes.some(attr => !attributeImages[attr.kMerkmalWert])) && (
|
{(attributes.some(attr => attributeImages[attr.kMerkmalWert]) || attributes.some(attr => !attributeImages[attr.kMerkmalWert])) && (
|
||||||
<Stack direction="row" spacing={0} sx={{ flexWrap: "wrap", gap: 1, flex: 1 }}>
|
<Stack
|
||||||
|
direction="row"
|
||||||
|
spacing={0}
|
||||||
|
sx={{
|
||||||
|
flexWrap: "wrap",
|
||||||
|
gap: 1,
|
||||||
|
flex: 1,
|
||||||
|
display: {
|
||||||
|
xs: hasAttributeImages ? "flex" : "none",
|
||||||
|
sm: "flex",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
{attributes
|
{attributes
|
||||||
.filter(attribute => attributeImages[attribute.kMerkmalWert])
|
.filter(attribute => attributeImages[attribute.kMerkmalWert])
|
||||||
.map((attribute) => {
|
.map((attribute) => {
|
||||||
@@ -1321,7 +1343,11 @@ class ProductDetailPage extends Component {
|
|||||||
key={attribute.kMerkmalWert}
|
key={attribute.kMerkmalWert}
|
||||||
label={attribute.cWert}
|
label={attribute.cWert}
|
||||||
disabled
|
disabled
|
||||||
sx={{
|
sx={(theme) => ({
|
||||||
|
// Max-width query: reliable on portrait phones (avoids display:contents wrapper quirks)
|
||||||
|
[theme.breakpoints.down('sm')]: {
|
||||||
|
display: 'none',
|
||||||
|
},
|
||||||
'&.Mui-disabled': {
|
'&.Mui-disabled': {
|
||||||
opacity: 1, // ← Remove the "fog"
|
opacity: 1, // ← Remove the "fog"
|
||||||
},
|
},
|
||||||
@@ -1329,7 +1355,7 @@ class ProductDetailPage extends Component {
|
|||||||
fontWeight: 'bold',
|
fontWeight: 'bold',
|
||||||
color: 'inherit', // ← Keep normal text color
|
color: 'inherit', // ← Keep normal text color
|
||||||
},
|
},
|
||||||
}}
|
})}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|||||||
@@ -1,13 +1,35 @@
|
|||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
|
import Box from '@mui/material/Box';
|
||||||
import Paper from '@mui/material/Paper';
|
import Paper from '@mui/material/Paper';
|
||||||
import Typography from '@mui/material/Typography';
|
import Typography from '@mui/material/Typography';
|
||||||
|
import Button from '@mui/material/Button';
|
||||||
|
import Tooltip from '@mui/material/Tooltip';
|
||||||
|
import CircularProgress from '@mui/material/CircularProgress';
|
||||||
|
import NotificationsIcon from '@mui/icons-material/Notifications';
|
||||||
|
import NotificationsActiveIcon from '@mui/icons-material/NotificationsActive';
|
||||||
import Filter from './Filter.js';
|
import Filter from './Filter.js';
|
||||||
import { useParams, useSearchParams, useNavigate, useLocation } from 'react-router-dom';
|
import { useParams, useSearchParams, useNavigate, useLocation } from 'react-router-dom';
|
||||||
import { setSessionSetting, removeSessionSetting, clearAllSessionSettings } from '../utils/sessionStorage.js';
|
import { setSessionSetting, removeSessionSetting, clearAllSessionSettings } from '../utils/sessionStorage.js';
|
||||||
import { withI18n } from '../i18n/withTranslation.js';
|
import { withI18n } from '../i18n/withTranslation.js';
|
||||||
|
import {
|
||||||
|
PUSH_SUBSCRIPTIONS_CHANGED_EVENT,
|
||||||
|
emitPushSubscriptionsChanged,
|
||||||
|
isPushApiSupported,
|
||||||
|
fetchPushConfiguration,
|
||||||
|
registerPushServiceWorker,
|
||||||
|
ensurePushSubscription,
|
||||||
|
categoryPushStatus,
|
||||||
|
categoryPushSubscribe,
|
||||||
|
categoryPushUnsubscribe,
|
||||||
|
parseSubscribedStatus,
|
||||||
|
parseSuccess,
|
||||||
|
} from '../utils/categoryPush.js';
|
||||||
|
|
||||||
const isNew = (neu) => neu && (new Date().getTime() - new Date(neu).getTime() < 30 * 24 * 60 * 60 * 1000);
|
const isNew = (neu) => neu && (new Date().getTime() - new Date(neu).getTime() < 30 * 24 * 60 * 60 * 1000);
|
||||||
|
|
||||||
|
/** Category push subscribe UI only when the category has more than this many articles. */
|
||||||
|
const MIN_ARTICLES_FOR_CATEGORY_PUSH = 10;
|
||||||
|
|
||||||
// HOC to provide router props to class components
|
// HOC to provide router props to class components
|
||||||
const withRouter = (ClassComponent) => {
|
const withRouter = (ClassComponent) => {
|
||||||
return (props) => {
|
return (props) => {
|
||||||
@@ -38,19 +60,35 @@ class ProductFilters extends Component {
|
|||||||
uniqueManufacturerArray,
|
uniqueManufacturerArray,
|
||||||
attributeGroups,
|
attributeGroups,
|
||||||
manufacturerImages: new Map(), // id (number) → object URL
|
manufacturerImages: new Map(), // id (number) → object URL
|
||||||
|
pushInteractive: false,
|
||||||
|
pushSubscribed: false,
|
||||||
|
pushBusy: false,
|
||||||
|
pushError: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
this._manufacturerImageUrls = []; // track for cleanup
|
this._manufacturerImageUrls = []; // track for cleanup
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
|
this.onPushSubscriptionsChanged = () => {
|
||||||
|
this.refreshCategoryPushStatus();
|
||||||
|
};
|
||||||
this.adjustPaperHeight();
|
this.adjustPaperHeight();
|
||||||
window.addEventListener('resize', this.adjustPaperHeight);
|
window.addEventListener('resize', this.adjustPaperHeight);
|
||||||
|
window.addEventListener(
|
||||||
|
PUSH_SUBSCRIPTIONS_CHANGED_EVENT,
|
||||||
|
this.onPushSubscriptionsChanged
|
||||||
|
);
|
||||||
this._loadManufacturerImages();
|
this._loadManufacturerImages();
|
||||||
|
this.refreshCategoryPushStatus();
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
window.removeEventListener('resize', this.adjustPaperHeight);
|
window.removeEventListener('resize', this.adjustPaperHeight);
|
||||||
|
window.removeEventListener(
|
||||||
|
PUSH_SUBSCRIPTIONS_CHANGED_EVENT,
|
||||||
|
this.onPushSubscriptionsChanged
|
||||||
|
);
|
||||||
this._manufacturerImageUrls.forEach(url => URL.revokeObjectURL(url));
|
this._manufacturerImageUrls.forEach(url => URL.revokeObjectURL(url));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -102,17 +140,148 @@ class ProductFilters extends Component {
|
|||||||
const attributeGroups = this._getAttributeGroups(this.props.attributes);
|
const attributeGroups = this._getAttributeGroups(this.props.attributes);
|
||||||
this.setState({attributeGroups});
|
this.setState({attributeGroups});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const prevCount = prevProps.products?.length || 0;
|
||||||
|
const nextCount = this.props.products?.length || 0;
|
||||||
|
if (
|
||||||
|
prevProps.dataParam !== this.props.dataParam ||
|
||||||
|
prevProps.dataType !== this.props.dataType ||
|
||||||
|
prevProps.params?.categoryId !== this.props.params?.categoryId ||
|
||||||
|
prevCount !== nextCount
|
||||||
|
) {
|
||||||
|
this.refreshCategoryPushStatus();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
kKategorieNumber = () => {
|
||||||
|
const { dataParam, dataType } = this.props;
|
||||||
|
if (dataType !== 'category') return null;
|
||||||
|
if (dataParam == null || dataParam === '') return null;
|
||||||
|
const n = typeof dataParam === 'number' ? dataParam : parseInt(String(dataParam), 10);
|
||||||
|
return Number.isFinite(n) && n > 0 ? n : null;
|
||||||
|
};
|
||||||
|
|
||||||
|
shouldShowCategoryPush = () =>
|
||||||
|
(this.props.products?.length || 0) > MIN_ARTICLES_FOR_CATEGORY_PUSH;
|
||||||
|
|
||||||
|
refreshCategoryPushStatus = async () => {
|
||||||
|
const kKat = this.kKategorieNumber();
|
||||||
|
if (!kKat || !this.shouldShowCategoryPush() || !isPushApiSupported()) {
|
||||||
|
this.setState({
|
||||||
|
pushInteractive: false,
|
||||||
|
pushSubscribed: false,
|
||||||
|
pushError: null,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const cfg = await fetchPushConfiguration();
|
||||||
|
if (!cfg.configured || !cfg.publicKey) {
|
||||||
|
this.setState({ pushInteractive: false });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await registerPushServiceWorker();
|
||||||
|
const registration = await navigator.serviceWorker.ready;
|
||||||
|
const subscription = await registration.pushManager.getSubscription();
|
||||||
|
if (!subscription) {
|
||||||
|
this.setState({
|
||||||
|
pushInteractive: true,
|
||||||
|
pushSubscribed: false,
|
||||||
|
pushError: null,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const statusData = await categoryPushStatus(kKat, subscription.endpoint);
|
||||||
|
this.setState({
|
||||||
|
pushInteractive: true,
|
||||||
|
pushSubscribed: parseSubscribedStatus(statusData),
|
||||||
|
pushError: null,
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('ProductFilters: category push init failed', e);
|
||||||
|
this.setState({ pushInteractive: false });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
handleCategoryPushClick = async () => {
|
||||||
|
const t = this.props.t;
|
||||||
|
if (!this.state.pushInteractive || this.state.pushBusy) return;
|
||||||
|
const kKat = this.kKategorieNumber();
|
||||||
|
if (!kKat || !this.shouldShowCategoryPush()) return;
|
||||||
|
this.setState({ pushBusy: true, pushError: null });
|
||||||
|
try {
|
||||||
|
if (this.state.pushSubscribed) {
|
||||||
|
const registration = await navigator.serviceWorker.ready;
|
||||||
|
const subscription = await registration.pushManager.getSubscription();
|
||||||
|
if (!subscription) {
|
||||||
|
this.setState({ pushSubscribed: false, pushBusy: false });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const res = await categoryPushUnsubscribe(subscription.endpoint, kKat);
|
||||||
|
if (parseSuccess(res)) {
|
||||||
|
this.setState({ pushSubscribed: false });
|
||||||
|
emitPushSubscriptionsChanged();
|
||||||
|
} else {
|
||||||
|
this.setState({
|
||||||
|
pushError:
|
||||||
|
res?.message ||
|
||||||
|
res?.error ||
|
||||||
|
(t ? t('productDialogs.pushNotifyError') : ''),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const perm = await Notification.requestPermission();
|
||||||
|
if (perm !== 'granted') {
|
||||||
|
this.setState({
|
||||||
|
pushError: t ? t('productDialogs.pushNotifyPermissionDenied') : '',
|
||||||
|
pushBusy: false,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const cfg = await fetchPushConfiguration();
|
||||||
|
if (!cfg.configured || !cfg.publicKey) {
|
||||||
|
this.setState({
|
||||||
|
pushError: t ? t('productDialogs.pushNotifyServerDisabled') : '',
|
||||||
|
pushBusy: false,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await registerPushServiceWorker();
|
||||||
|
const subscription = await ensurePushSubscription(cfg.publicKey);
|
||||||
|
const res = await categoryPushSubscribe(kKat, subscription);
|
||||||
|
if (parseSuccess(res)) {
|
||||||
|
this.setState({ pushSubscribed: true });
|
||||||
|
emitPushSubscriptionsChanged();
|
||||||
|
} else {
|
||||||
|
this.setState({
|
||||||
|
pushError:
|
||||||
|
res?.message ||
|
||||||
|
res?.error ||
|
||||||
|
(t ? t('productDialogs.pushNotifyError') : ''),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('ProductFilters: category push', e);
|
||||||
|
this.setState({
|
||||||
|
pushError: e.message || (t ? t('productDialogs.pushNotifyError') : ''),
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
this.setState({ pushBusy: false });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
adjustPaperHeight = () => {
|
adjustPaperHeight = () => {
|
||||||
// Skip height adjustment on xs screens
|
|
||||||
if (window.innerWidth < 600) return;
|
|
||||||
|
|
||||||
// Get reference to our paper element
|
|
||||||
const paperEl = document.getElementById('filters-paper');
|
const paperEl = document.getElementById('filters-paper');
|
||||||
if (!paperEl) return;
|
if (!paperEl) return;
|
||||||
|
|
||||||
|
// No min-height on mobile — also clears inline style after resize from desktop
|
||||||
|
if (window.innerWidth < 600) {
|
||||||
|
paperEl.style.minHeight = '';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Get viewport height
|
// Get viewport height
|
||||||
const viewportHeight = window.innerHeight;
|
const viewportHeight = window.innerHeight;
|
||||||
|
|
||||||
@@ -200,35 +369,140 @@ class ProductFilters extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
const kKategorie = this.kKategorieNumber();
|
||||||
|
const showCategoryPush = kKategorie && this.shouldShowCategoryPush();
|
||||||
|
const {
|
||||||
|
pushInteractive,
|
||||||
|
pushSubscribed,
|
||||||
|
pushBusy,
|
||||||
|
pushError,
|
||||||
|
} = this.state;
|
||||||
|
const pushDisabledHint =
|
||||||
|
showCategoryPush && !pushInteractive && !pushBusy
|
||||||
|
? isPushApiSupported()
|
||||||
|
? this.props.t
|
||||||
|
? this.props.t('productDialogs.pushNotifyServerDisabled')
|
||||||
|
: ''
|
||||||
|
: this.props.t
|
||||||
|
? this.props.t('filters.notifyNewArticlesBrowserUnsupported')
|
||||||
|
: 'Ihr Browser unterstützt keine Push-Benachrichtigungen.'
|
||||||
|
: '';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
px: { xs: 2, sm: 0 },
|
||||||
|
pt: { xs: 2, sm: 0 },
|
||||||
|
/* Room below Paper so elevation shadow isn’t clipped by grid/parent */
|
||||||
|
pb: { xs: 2, sm: 2 },
|
||||||
|
overflow: 'visible',
|
||||||
|
/* Same green as ProductList / product strip mobile (#e8f5e8), not theme background.default (#C8E6C9) */
|
||||||
|
bgcolor: { xs: '#e8f5e8', sm: 'transparent' },
|
||||||
|
}}
|
||||||
|
>
|
||||||
<Paper
|
<Paper
|
||||||
id="filters-paper"
|
id="filters-paper"
|
||||||
elevation={window.innerWidth < 600 ? 0 : 1}
|
elevation={1}
|
||||||
sx={{
|
sx={{
|
||||||
p: { xs: 1, sm: 2 },
|
p: { xs: 2.5, sm: 2.5 },
|
||||||
borderRadius: { xs: 0, sm: 2 },
|
mx: { sm: 'auto' },
|
||||||
|
maxWidth: '100%',
|
||||||
|
borderRadius: 2,
|
||||||
bgcolor: 'background.paper',
|
bgcolor: 'background.paper',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
border: { xs: 'none', sm: 'inherit' },
|
border: { xs: 'none', sm: 'inherit' },
|
||||||
boxShadow: { xs: 'none', sm: 'inherit' },
|
boxSizing: 'border-box',
|
||||||
mx: { xs: 0, sm: 'auto' },
|
overflow: 'visible',
|
||||||
width: { xs: '100%', sm: 'auto' }
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|
||||||
{this.props.dataType == 'category' && (
|
{this.props.dataType == 'category' && (
|
||||||
<Typography
|
<Box sx={{ mb: 4 }}>
|
||||||
variant="h3"
|
<Typography
|
||||||
component="h1"
|
variant="h3"
|
||||||
sx={{
|
component="h1"
|
||||||
mb: 4,
|
sx={{
|
||||||
fontFamily: 'SwashingtonCP',
|
mb: showCategoryPush ? 1.5 : 4,
|
||||||
color: 'primary.main'
|
fontFamily: 'SwashingtonCP',
|
||||||
}}
|
color: 'primary.main',
|
||||||
>
|
}}
|
||||||
{this.props.categoryName}
|
>
|
||||||
</Typography>
|
{this.props.categoryName}
|
||||||
|
</Typography>
|
||||||
|
{showCategoryPush && (
|
||||||
|
<Box sx={{ width: '100%' }}>
|
||||||
|
<Tooltip title={pushDisabledHint} arrow>
|
||||||
|
<span style={{ display: 'block', width: '100%' }}>
|
||||||
|
<Button
|
||||||
|
fullWidth
|
||||||
|
variant="outlined"
|
||||||
|
color="inherit"
|
||||||
|
size="small"
|
||||||
|
onClick={this.handleCategoryPushClick}
|
||||||
|
disabled={!pushInteractive || pushBusy}
|
||||||
|
startIcon={
|
||||||
|
pushBusy ? (
|
||||||
|
<CircularProgress size={14} sx={{ color: 'inherit' }} />
|
||||||
|
) : pushSubscribed ? (
|
||||||
|
<NotificationsActiveIcon sx={{ fontSize: 18, color: '#2e7d32' }} />
|
||||||
|
) : (
|
||||||
|
<NotificationsIcon sx={{ fontSize: 18, color: 'rgba(0,0,0,0.65)' }} />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
sx={{
|
||||||
|
borderRadius: 1,
|
||||||
|
fontWeight: 600,
|
||||||
|
fontSize: '0.7rem',
|
||||||
|
lineHeight: 1.2,
|
||||||
|
backgroundColor: '#fff',
|
||||||
|
color: 'text.primary',
|
||||||
|
border: '1px solid',
|
||||||
|
borderColor: 'divider',
|
||||||
|
boxShadow: 'none',
|
||||||
|
whiteSpace: 'normal',
|
||||||
|
textAlign: 'center',
|
||||||
|
py: 0.4,
|
||||||
|
px: 0.75,
|
||||||
|
minHeight: 28,
|
||||||
|
'& .MuiButton-label': {
|
||||||
|
whiteSpace: 'normal',
|
||||||
|
lineHeight: 1.2,
|
||||||
|
},
|
||||||
|
'& .MuiButton-startIcon': {
|
||||||
|
mr: 0.5,
|
||||||
|
'& > *:nth-of-type(1)': { fontSize: 18 },
|
||||||
|
},
|
||||||
|
'&:hover': {
|
||||||
|
backgroundColor: 'grey.50',
|
||||||
|
borderColor: 'divider',
|
||||||
|
boxShadow: 'none',
|
||||||
|
},
|
||||||
|
'&.Mui-disabled': {
|
||||||
|
backgroundColor: '#fff',
|
||||||
|
color: 'action.disabled',
|
||||||
|
borderColor: 'action.disabledBackground',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{this.props.t
|
||||||
|
? this.props.t('filters.notifyNewArticles')
|
||||||
|
: 'Bei neuen Artikeln benachrichtigen'}
|
||||||
|
</Button>
|
||||||
|
</span>
|
||||||
|
</Tooltip>
|
||||||
|
{pushError && (
|
||||||
|
<Typography
|
||||||
|
variant="caption"
|
||||||
|
color="error"
|
||||||
|
sx={{ display: 'block', mt: 0.5, textAlign: 'center' }}
|
||||||
|
>
|
||||||
|
{pushError}
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
||||||
@@ -295,6 +569,7 @@ class ProductFilters extends Component {
|
|||||||
/>
|
/>
|
||||||
</>)}
|
</>)}
|
||||||
</Paper>
|
</Paper>
|
||||||
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -241,7 +241,7 @@ class ProductList extends Component {
|
|||||||
|
|
||||||
|
|
||||||
<Box sx={{
|
<Box sx={{
|
||||||
display: 'flex',
|
display: { xs: 'none', sm: 'flex' },
|
||||||
gap: { xs: 0.5, sm: 1 },
|
gap: { xs: 0.5, sm: 1 },
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
flexWrap: 'wrap',
|
flexWrap: 'wrap',
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ class Stripe extends Component {
|
|||||||
colorWarning: '#FF9800', // Orange for warnings
|
colorWarning: '#FF9800', // Orange for warnings
|
||||||
|
|
||||||
// Typography matching your Roboto setup
|
// Typography matching your Roboto setup
|
||||||
fontFamily: "'Roboto', 'Helvetica', 'Arial', sans-serif",
|
fontFamily: "'Outfit Variable', 'Roboto', 'Helvetica', 'Arial', sans-serif",
|
||||||
fontSizeBase: '16px', // Base font size for mobile compatibility
|
fontSizeBase: '16px', // Base font size for mobile compatibility
|
||||||
fontWeightNormal: '400', // Normal Roboto weight
|
fontWeightNormal: '400', // Normal Roboto weight
|
||||||
fontWeightMedium: '500', // Medium Roboto weight
|
fontWeightMedium: '500', // Medium Roboto weight
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ const ThemeCustomizerDialog = ({ open, onClose, theme, onThemeChange }) => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
typography: {
|
typography: {
|
||||||
fontFamily: "'Roboto', 'Helvetica', 'Arial', sans-serif",
|
fontFamily: "'Outfit Variable', 'Roboto', 'Helvetica', 'Arial', sans-serif",
|
||||||
h4: {
|
h4: {
|
||||||
fontWeight: 600,
|
fontWeight: 600,
|
||||||
color: '#33691E',
|
color: '#33691E',
|
||||||
|
|||||||
@@ -183,7 +183,7 @@ class CategoryList extends Component {
|
|||||||
display: "flex",
|
display: "flex",
|
||||||
justifyContent: "flex-start",
|
justifyContent: "flex-start",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
flexWrap: "wrap",
|
flexWrap: isMobile ? "wrap" : "nowrap",
|
||||||
overflowX: "visible",
|
overflowX: "visible",
|
||||||
flexDirection: isMobile ? "column" : "row",
|
flexDirection: isMobile ? "column" : "row",
|
||||||
py: 0.5, // Add vertical padding to prevent border clipping
|
py: 0.5, // Add vertical padding to prevent border clipping
|
||||||
@@ -197,7 +197,7 @@ class CategoryList extends Component {
|
|||||||
aria-label="Zur Startseite"
|
aria-label="Zur Startseite"
|
||||||
onClick={isMobile ? this.handleMobileCategoryClick : undefined}
|
onClick={isMobile ? this.handleMobileCategoryClick : undefined}
|
||||||
sx={{
|
sx={{
|
||||||
fontSize: "0.75rem",
|
fontSize: "0.85rem",
|
||||||
textTransform: "none",
|
textTransform: "none",
|
||||||
whiteSpace: "nowrap",
|
whiteSpace: "nowrap",
|
||||||
opacity: 0.9,
|
opacity: 0.9,
|
||||||
@@ -275,7 +275,7 @@ class CategoryList extends Component {
|
|||||||
aria-label="Neuheiten"
|
aria-label="Neuheiten"
|
||||||
onClick={isMobile ? this.handleMobileCategoryClick : undefined}
|
onClick={isMobile ? this.handleMobileCategoryClick : undefined}
|
||||||
sx={{
|
sx={{
|
||||||
fontSize: "0.75rem",
|
fontSize: "0.85rem",
|
||||||
textTransform: "none",
|
textTransform: "none",
|
||||||
whiteSpace: "nowrap",
|
whiteSpace: "nowrap",
|
||||||
opacity: 0.9,
|
opacity: 0.9,
|
||||||
@@ -353,7 +353,7 @@ class CategoryList extends Component {
|
|||||||
aria-label={this.props.t ? this.props.t('navigation.soon') : 'Demnächst'}
|
aria-label={this.props.t ? this.props.t('navigation.soon') : 'Demnächst'}
|
||||||
onClick={isMobile ? this.handleMobileCategoryClick : undefined}
|
onClick={isMobile ? this.handleMobileCategoryClick : undefined}
|
||||||
sx={{
|
sx={{
|
||||||
fontSize: "0.75rem",
|
fontSize: "0.85rem",
|
||||||
textTransform: "none",
|
textTransform: "none",
|
||||||
whiteSpace: "nowrap",
|
whiteSpace: "nowrap",
|
||||||
opacity: 0.9,
|
opacity: 0.9,
|
||||||
@@ -435,7 +435,7 @@ class CategoryList extends Component {
|
|||||||
size="small"
|
size="small"
|
||||||
onClick={isMobile ? this.handleMobileCategoryClick : undefined}
|
onClick={isMobile ? this.handleMobileCategoryClick : undefined}
|
||||||
sx={{
|
sx={{
|
||||||
fontSize: "0.75rem",
|
fontSize: "0.85rem",
|
||||||
textTransform: "none",
|
textTransform: "none",
|
||||||
whiteSpace: "nowrap",
|
whiteSpace: "nowrap",
|
||||||
opacity: 0.9,
|
opacity: 0.9,
|
||||||
@@ -506,7 +506,7 @@ class CategoryList extends Component {
|
|||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
height: "33px", // Match small button height
|
height: "33px", // Match small button height
|
||||||
px: 1,
|
px: 1,
|
||||||
fontSize: "0.75rem",
|
fontSize: "0.85rem",
|
||||||
opacity: 0.9,
|
opacity: 0.9,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -522,7 +522,7 @@ class CategoryList extends Component {
|
|||||||
aria-label={this.props.t ? this.props.t('navigation.konfiguratorAria') : 'Zum Konfigurator'}
|
aria-label={this.props.t ? this.props.t('navigation.konfiguratorAria') : 'Zum Konfigurator'}
|
||||||
onClick={isMobile ? this.handleMobileCategoryClick : undefined}
|
onClick={isMobile ? this.handleMobileCategoryClick : undefined}
|
||||||
sx={{
|
sx={{
|
||||||
fontSize: "0.75rem",
|
fontSize: "0.85rem",
|
||||||
textTransform: "none",
|
textTransform: "none",
|
||||||
whiteSpace: "nowrap",
|
whiteSpace: "nowrap",
|
||||||
opacity: 0.9,
|
opacity: 0.9,
|
||||||
|
|||||||
@@ -3,5 +3,7 @@ export default {
|
|||||||
"perPage": "لكل صفحة",
|
"perPage": "لكل صفحة",
|
||||||
"availability": "التوفر",
|
"availability": "التوفر",
|
||||||
"manufacturer": "المصنّع",
|
"manufacturer": "المصنّع",
|
||||||
"all": "الكل"
|
"all": "الكل",
|
||||||
|
"notifyNewArticles": "إشعار عند توفر منتجات جديدة",
|
||||||
|
"notifyNewArticlesBrowserUnsupported": "المتصفح لا يدعم إشعارات الدفع."
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
export default {
|
export default {
|
||||||
"home": "الرئيسية",
|
"home": "الرئيسية",
|
||||||
|
"konfiguratorAria": "اذهب إلى المُكوِّن",
|
||||||
"new": "وصل حديثًا",
|
"new": "وصل حديثًا",
|
||||||
"soon": "قريبًا",
|
"soon": "قريبًا",
|
||||||
"aktionen": "العروض الترويجية",
|
"aktionen": "العروض الترويجية",
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ export default {
|
|||||||
"thcTest": "اختبار THC",
|
"thcTest": "اختبار THC",
|
||||||
"address1": "Trachenberger Street 14",
|
"address1": "Trachenberger Street 14",
|
||||||
"address2": "01129 Dresden",
|
"address2": "01129 Dresden",
|
||||||
"showUsPhoto": "ورّينا أجمل صورة عندك",
|
"buildYourSet": "جهّز مجموعتك",
|
||||||
"selectSeedRate": "اختار البذرة، واضغط قيّم",
|
"selectSeedRate": "اختار البذرة، واضغط قيّم",
|
||||||
"outdoorSeason": "موسم الزراعة الخارجية بيبدأ"
|
"outdoorSeason": "موسم الزراعة الخارجية بيبدأ"
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,5 +3,7 @@ export default {
|
|||||||
"perPage": "на страница",
|
"perPage": "на страница",
|
||||||
"availability": "Наличност",
|
"availability": "Наличност",
|
||||||
"manufacturer": "Производител",
|
"manufacturer": "Производител",
|
||||||
"all": "Всички"
|
"all": "Всички",
|
||||||
|
"notifyNewArticles": "Уведомявай ме за нови продукти",
|
||||||
|
"notifyNewArticlesBrowserUnsupported": "Вашият браузър не поддържа push известия."
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
export default {
|
export default {
|
||||||
"home": "Начало",
|
"home": "Начало",
|
||||||
"new": "Нови артикули",
|
"konfiguratorAria": "Отидете на Конфигуратора",
|
||||||
|
"new": "Нови попълнения",
|
||||||
"soon": "Очаквайте скоро",
|
"soon": "Очаквайте скоро",
|
||||||
"aktionen": "Промоции",
|
"aktionen": "Промоции",
|
||||||
"filiale": "Магазин",
|
"filiale": "Магазин",
|
||||||
"categories": "Категории",
|
"categories": "Категории",
|
||||||
"categoriesOpen": "Отвори категориите",
|
"categoriesOpen": "Отвори категории",
|
||||||
"categoriesClose": "Затвори категориите",
|
"categoriesClose": "Затвори категории",
|
||||||
"otherCategories": "Други категории"
|
"otherCategories": "Други категории"
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,11 +2,11 @@ export default {
|
|||||||
"seeds": "Семена",
|
"seeds": "Семена",
|
||||||
"stecklinge": "Резници",
|
"stecklinge": "Резници",
|
||||||
"konfigurator": "Конфигуратор",
|
"konfigurator": "Конфигуратор",
|
||||||
"oilPress": "Вземете назаем преса за масло",
|
"oilPress": "Вземете на заем маслоизстисквачка",
|
||||||
"thcTest": "THC тест",
|
"thcTest": "THC тест",
|
||||||
"address1": "Trachenberger Street 14",
|
"address1": "Trachenberger Street 14",
|
||||||
"address2": "01129 Dresden",
|
"address2": "01129 Dresden",
|
||||||
"showUsPhoto": "Покажете ни най-красивата си снимка",
|
"buildYourSet": "Сглобете своя комплект",
|
||||||
"selectSeedRate": "Изберете семе, кликнете за оценка",
|
"selectSeedRate": "Изберете семе, кликнете върху оценка",
|
||||||
"outdoorSeason": "Започва сезонът на открито"
|
"outdoorSeason": "Открива се сезонът на открито"
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,5 +3,7 @@ export default {
|
|||||||
"perPage": "na stránku",
|
"perPage": "na stránku",
|
||||||
"availability": "Dostupnost",
|
"availability": "Dostupnost",
|
||||||
"manufacturer": "Výrobce",
|
"manufacturer": "Výrobce",
|
||||||
"all": "Vše"
|
"all": "Vše",
|
||||||
|
"notifyNewArticles": "Upozornit na nové produkty",
|
||||||
|
"notifyNewArticlesBrowserUnsupported": "Váš prohlížeč nepodporuje push oznámení."
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
export default {
|
export default {
|
||||||
"home": "Domů",
|
"home": "Domů",
|
||||||
|
"konfiguratorAria": "Přejít do konfigurátoru",
|
||||||
"new": "Novinky",
|
"new": "Novinky",
|
||||||
"soon": "Již brzy",
|
"soon": "Již brzy",
|
||||||
"aktionen": "Akce",
|
"aktionen": "Akce",
|
||||||
@@ -7,5 +8,5 @@ export default {
|
|||||||
"categories": "Kategorie",
|
"categories": "Kategorie",
|
||||||
"categoriesOpen": "Otevřít kategorie",
|
"categoriesOpen": "Otevřít kategorie",
|
||||||
"categoriesClose": "Zavřít kategorie",
|
"categoriesClose": "Zavřít kategorie",
|
||||||
"otherCategories": "Další kategorie"
|
"otherCategories": "Ostatní kategorie"
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ export default {
|
|||||||
"thcTest": "THC test",
|
"thcTest": "THC test",
|
||||||
"address1": "Trachenberger Street 14",
|
"address1": "Trachenberger Street 14",
|
||||||
"address2": "01129 Dresden",
|
"address2": "01129 Dresden",
|
||||||
"showUsPhoto": "Ukažte nám svou nejkrásnější fotografii",
|
"buildYourSet": "Sestavte si svou sadu",
|
||||||
"selectSeedRate": "Vyberte semeno, klikněte na hodnocení",
|
"selectSeedRate": "Vyberte semeno, klikněte na hodnocení",
|
||||||
"outdoorSeason": "Začíná venkovní sezóna"
|
"outdoorSeason": "Začíná venkovní sezóna"
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,5 +3,7 @@ export default {
|
|||||||
"perPage": "pro Seite",
|
"perPage": "pro Seite",
|
||||||
"availability": "Verfügbarkeit",
|
"availability": "Verfügbarkeit",
|
||||||
"manufacturer": "Hersteller",
|
"manufacturer": "Hersteller",
|
||||||
"all": "Alle"
|
"all": "Alle",
|
||||||
|
"notifyNewArticles": "Bei neuen Artikeln benachrichtigen",
|
||||||
|
"notifyNewArticlesBrowserUnsupported": "Ihr Browser unterstützt keine Push-Benachrichtigungen."
|
||||||
};
|
};
|
||||||
@@ -6,7 +6,7 @@ export default {
|
|||||||
"thcTest": "THC Test",
|
"thcTest": "THC Test",
|
||||||
"address1": "Trachenberger Straße 14",
|
"address1": "Trachenberger Straße 14",
|
||||||
"address2": "01129 Dresden",
|
"address2": "01129 Dresden",
|
||||||
"showUsPhoto": "Zeig uns dein schönstes Foto",
|
"buildYourSet": "Stelle dein Set zusammen",
|
||||||
"selectSeedRate": "Wähle Seed aus, klicke Bewerten",
|
"selectSeedRate": "Wähle Seed aus, klicke Bewerten",
|
||||||
"outdoorSeason": "Die Outdoorsaison beginnt"
|
"outdoorSeason": "Die Outdoorsaison beginnt"
|
||||||
};
|
};
|
||||||
@@ -3,5 +3,7 @@ export default {
|
|||||||
"perPage": "ανά σελίδα",
|
"perPage": "ανά σελίδα",
|
||||||
"availability": "Διαθεσιμότητα",
|
"availability": "Διαθεσιμότητα",
|
||||||
"manufacturer": "Κατασκευαστής",
|
"manufacturer": "Κατασκευαστής",
|
||||||
"all": "Όλα"
|
"all": "Όλα",
|
||||||
|
"notifyNewArticles": "Ειδοποίησέ με για νέα προϊόντα",
|
||||||
|
"notifyNewArticlesBrowserUnsupported": "Ο φυλλομετρητής σας δεν υποστηρίζει push ειδοποιήσεις."
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
export default {
|
export default {
|
||||||
"home": "Αρχική",
|
"home": "Αρχική",
|
||||||
"new": "Νέες Αφίξεις",
|
"konfiguratorAria": "Μετάβαση στον Configurator",
|
||||||
"soon": "Έρχεται Σύντομα",
|
"new": "Νέα Άφιξη",
|
||||||
|
"soon": "Σύντομα",
|
||||||
"aktionen": "Προσφορές",
|
"aktionen": "Προσφορές",
|
||||||
"filiale": "Κατάστημα",
|
"filiale": "Κατάστημα",
|
||||||
"categories": "Κατηγορίες",
|
"categories": "Κατηγορίες",
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ export default {
|
|||||||
"thcTest": "Τεστ THC",
|
"thcTest": "Τεστ THC",
|
||||||
"address1": "Trachenberger Street 14",
|
"address1": "Trachenberger Street 14",
|
||||||
"address2": "01129 Dresden",
|
"address2": "01129 Dresden",
|
||||||
"showUsPhoto": "Δείξε μας τη πιο όμορφη φωτογραφία σου",
|
"buildYourSet": "Φτιάξε το δικό σου σετ",
|
||||||
"selectSeedRate": "Επίλεξε σπόρο, κάνε κλικ στη βαθμολογία",
|
"selectSeedRate": "Επίλεξε σπόρο, κάνε κλικ στη βαθμολογία",
|
||||||
"outdoorSeason": "Ξεκινά η υπαίθρια σεζόν"
|
"outdoorSeason": "Ξεκινά η υπαίθρια σεζόν"
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,5 +3,7 @@ export default {
|
|||||||
"perPage": "per page", // pro Seite
|
"perPage": "per page", // pro Seite
|
||||||
"availability": "Availability", // Verfügbarkeit
|
"availability": "Availability", // Verfügbarkeit
|
||||||
"manufacturer": "Manufacturer", // Hersteller
|
"manufacturer": "Manufacturer", // Hersteller
|
||||||
"all": "All" // Alle
|
"all": "All", // Alle
|
||||||
|
"notifyNewArticles": "Notify me about new articles",
|
||||||
|
"notifyNewArticlesBrowserUnsupported": "Your browser does not support push notifications."
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ export default {
|
|||||||
"thcTest": "THC Test", // THC Test
|
"thcTest": "THC Test", // THC Test
|
||||||
"address1": "Trachenberger Street 14", // Trachenberger Straße 14
|
"address1": "Trachenberger Street 14", // Trachenberger Straße 14
|
||||||
"address2": "01129 Dresden", // 01129 Dresden
|
"address2": "01129 Dresden", // 01129 Dresden
|
||||||
"showUsPhoto": "Show us your most beautiful photo", // Zeig uns dein schönstes Foto
|
"buildYourSet": "Put your set together", // Stelle dein Set zusammen
|
||||||
"selectSeedRate": "Choose seed, click rate", // Wähle Seed aus, klicke Bewerten
|
"selectSeedRate": "Choose seed, click rate", // Wähle Seed aus, klicke Bewerten
|
||||||
"outdoorSeason": "The outdoor season begins" // Die Outdoorsaison beginnt
|
"outdoorSeason": "The outdoor season begins" // Die Outdoorsaison beginnt
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,5 +3,7 @@ export default {
|
|||||||
"perPage": "por página",
|
"perPage": "por página",
|
||||||
"availability": "Disponibilidad",
|
"availability": "Disponibilidad",
|
||||||
"manufacturer": "Fabricante",
|
"manufacturer": "Fabricante",
|
||||||
"all": "Todos"
|
"all": "Todos",
|
||||||
|
"notifyNewArticles": "Notificarme sobre artículos nuevos",
|
||||||
|
"notifyNewArticlesBrowserUnsupported": "Su navegador no admite notificaciones push."
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
export default {
|
export default {
|
||||||
"home": "Inicio",
|
"home": "Inicio",
|
||||||
|
"konfiguratorAria": "Ir al Configurator",
|
||||||
"new": "Novedades",
|
"new": "Novedades",
|
||||||
"soon": "Próximamente",
|
"soon": "Próximamente",
|
||||||
"aktionen": "Promociones",
|
"aktionen": "Promociones",
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ export default {
|
|||||||
"thcTest": "Prueba de THC",
|
"thcTest": "Prueba de THC",
|
||||||
"address1": "Trachenberger Street 14",
|
"address1": "Trachenberger Street 14",
|
||||||
"address2": "01129 Dresden",
|
"address2": "01129 Dresden",
|
||||||
"showUsPhoto": "Muéstranos tu foto más bonita",
|
"buildYourSet": "Monta tu equipo",
|
||||||
"selectSeedRate": "Elige la semilla, haz clic en valorar",
|
"selectSeedRate": "Elige la semilla, haz clic en valorar",
|
||||||
"outdoorSeason": "Comienza la temporada al aire libre"
|
"outdoorSeason": "Comienza la temporada al aire libre"
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,5 +3,7 @@ export default {
|
|||||||
"perPage": "par page",
|
"perPage": "par page",
|
||||||
"availability": "Disponibilité",
|
"availability": "Disponibilité",
|
||||||
"manufacturer": "Fabricant",
|
"manufacturer": "Fabricant",
|
||||||
"all": "Tous"
|
"all": "Tous",
|
||||||
|
"notifyNewArticles": "Être notifié des nouveaux articles",
|
||||||
|
"notifyNewArticlesBrowserUnsupported": "Votre navigateur ne prend pas en charge les notifications push."
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
export default {
|
export default {
|
||||||
"home": "Accueil",
|
"home": "Accueil",
|
||||||
|
"konfiguratorAria": "Aller au Configurateur",
|
||||||
"new": "Nouveautés",
|
"new": "Nouveautés",
|
||||||
"soon": "Bientôt disponible",
|
"soon": "Bientôt disponible",
|
||||||
"aktionen": "Promotions",
|
"aktionen": "Promotions",
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ export default {
|
|||||||
"thcTest": "Test THC",
|
"thcTest": "Test THC",
|
||||||
"address1": "Trachenberger Street 14",
|
"address1": "Trachenberger Street 14",
|
||||||
"address2": "01129 Dresde",
|
"address2": "01129 Dresde",
|
||||||
"showUsPhoto": "Montrez-nous votre plus belle photo",
|
"buildYourSet": "Composez votre ensemble",
|
||||||
"selectSeedRate": "Choisissez une graine, cliquez sur évaluer",
|
"selectSeedRate": "Choisissez une graine, cliquez sur évaluer",
|
||||||
"outdoorSeason": "La saison en extérieur commence"
|
"outdoorSeason": "La saison en extérieur commence"
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,5 +3,7 @@ export default {
|
|||||||
"perPage": "po stranici",
|
"perPage": "po stranici",
|
||||||
"availability": "Dostupnost",
|
"availability": "Dostupnost",
|
||||||
"manufacturer": "Proizvođač",
|
"manufacturer": "Proizvođač",
|
||||||
"all": "Sve"
|
"all": "Sve",
|
||||||
|
"notifyNewArticles": "Obavijesti me o novim artiklima",
|
||||||
|
"notifyNewArticlesBrowserUnsupported": "Vaš preglednik ne podržava push obavijesti."
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
export default {
|
export default {
|
||||||
"home": "Početna",
|
"home": "Početna",
|
||||||
|
"konfiguratorAria": "Idi na Konfigurator",
|
||||||
"new": "Novi proizvodi",
|
"new": "Novi proizvodi",
|
||||||
"soon": "Uskoro",
|
"soon": "Uskoro dostupno",
|
||||||
"aktionen": "Promocije",
|
"aktionen": "Promocije",
|
||||||
"filiale": "Trgovina",
|
"filiale": "Trgovina",
|
||||||
"categories": "Kategorije",
|
"categories": "Kategorije",
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ export default {
|
|||||||
"thcTest": "THC test",
|
"thcTest": "THC test",
|
||||||
"address1": "Trachenberger Street 14",
|
"address1": "Trachenberger Street 14",
|
||||||
"address2": "01129 Dresden",
|
"address2": "01129 Dresden",
|
||||||
"showUsPhoto": "Pokažite nam svoju najljepšu fotografiju",
|
"buildYourSet": "Sastavite svoj set",
|
||||||
"selectSeedRate": "Odaberite sjeme, kliknite ocjenu",
|
"selectSeedRate": "Odaberite sjeme, kliknite ocjenu",
|
||||||
"outdoorSeason": "Počinje sezona za vanjsku uzgoj"
|
"outdoorSeason": "Počinje sezona za vanjsku uzgoj"
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,5 +3,7 @@ export default {
|
|||||||
"perPage": "oldalanként",
|
"perPage": "oldalanként",
|
||||||
"availability": "Elérhetőség",
|
"availability": "Elérhetőség",
|
||||||
"manufacturer": "Gyártó",
|
"manufacturer": "Gyártó",
|
||||||
"all": "Összes"
|
"all": "Összes",
|
||||||
|
"notifyNewArticles": "Értesítés új termékekről",
|
||||||
|
"notifyNewArticlesBrowserUnsupported": "A böngésző nem támogatja a push értesítéseket."
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
export default {
|
export default {
|
||||||
"home": "Főoldal",
|
"home": "Kezdőlap",
|
||||||
|
"konfiguratorAria": "Ugrás a konfigurátorhoz",
|
||||||
"new": "Újdonságok",
|
"new": "Újdonságok",
|
||||||
"soon": "Hamarosan",
|
"soon": "Hamarosan",
|
||||||
"aktionen": "Promóciók",
|
"aktionen": "Akciók",
|
||||||
"filiale": "Üzlet",
|
"filiale": "Bolt",
|
||||||
"categories": "Kategóriák",
|
"categories": "Kategóriák",
|
||||||
"categoriesOpen": "Kategóriák megnyitása",
|
"categoriesOpen": "Kategóriák megnyitása",
|
||||||
"categoriesClose": "Kategóriák bezárása",
|
"categoriesClose": "Kategóriák bezárása",
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ export default {
|
|||||||
"thcTest": "THC teszt",
|
"thcTest": "THC teszt",
|
||||||
"address1": "Trachenberger utca 14",
|
"address1": "Trachenberger utca 14",
|
||||||
"address2": "01129 Dresden",
|
"address2": "01129 Dresden",
|
||||||
"showUsPhoto": "Mutasd meg nekünk a legszebb fotódat",
|
"buildYourSet": "Állítsd össze a szettet",
|
||||||
"selectSeedRate": "Válassz magot, kattints az értékelésre",
|
"selectSeedRate": "Válassz magot, kattints az értékelésre",
|
||||||
"outdoorSeason": "Kezdődik a szabadtéri szezon"
|
"outdoorSeason": "Kezdődik a szabadtéri szezon"
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,5 +3,7 @@ export default {
|
|||||||
"perPage": "per pagina",
|
"perPage": "per pagina",
|
||||||
"availability": "Disponibilità",
|
"availability": "Disponibilità",
|
||||||
"manufacturer": "Produttore",
|
"manufacturer": "Produttore",
|
||||||
"all": "Tutti"
|
"all": "Tutti",
|
||||||
|
"notifyNewArticles": "Avvisami sui nuovi articoli",
|
||||||
|
"notifyNewArticlesBrowserUnsupported": "Il tuo browser non supporta le notifiche push."
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
export default {
|
export default {
|
||||||
"home": "Home",
|
"home": "Home",
|
||||||
"new": "Novità",
|
"konfiguratorAria": "Vai al Configuratore",
|
||||||
|
"new": "Nuovi Arrivi",
|
||||||
"soon": "Prossimamente",
|
"soon": "Prossimamente",
|
||||||
"aktionen": "Promozioni",
|
"aktionen": "Promozioni",
|
||||||
"filiale": "Negozio",
|
"filiale": "Negozio",
|
||||||
"categories": "Categorie",
|
"categories": "Categorie",
|
||||||
"categoriesOpen": "Apri categorie",
|
"categoriesOpen": "Apri Categorie",
|
||||||
"categoriesClose": "Chiudi categorie",
|
"categoriesClose": "Chiudi Categorie",
|
||||||
"otherCategories": "Altre categorie"
|
"otherCategories": "Altre Categorie"
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ export default {
|
|||||||
"thcTest": "Test THC",
|
"thcTest": "Test THC",
|
||||||
"address1": "Trachenberger Street 14",
|
"address1": "Trachenberger Street 14",
|
||||||
"address2": "01129 Dresda",
|
"address2": "01129 Dresda",
|
||||||
"showUsPhoto": "Mostraci la tua foto più bella",
|
"buildYourSet": "Componi il tuo set",
|
||||||
"selectSeedRate": "Scegli il seme, clicca valuta",
|
"selectSeedRate": "Scegli il seme, clicca valuta",
|
||||||
"outdoorSeason": "La stagione outdoor inizia"
|
"outdoorSeason": "La stagione outdoor inizia"
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,5 +3,7 @@ export default {
|
|||||||
"perPage": "na stronę",
|
"perPage": "na stronę",
|
||||||
"availability": "Dostępność",
|
"availability": "Dostępność",
|
||||||
"manufacturer": "Producent",
|
"manufacturer": "Producent",
|
||||||
"all": "Wszystkie"
|
"all": "Wszystkie",
|
||||||
|
"notifyNewArticles": "Powiadamiaj o nowych produktach",
|
||||||
|
"notifyNewArticlesBrowserUnsupported": "Twoja przeglądarka nie obsługuje powiadomień push."
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
export default {
|
export default {
|
||||||
"home": "Strona główna",
|
"home": "Strona główna",
|
||||||
|
"konfiguratorAria": "Przejdź do Konfiguratora",
|
||||||
"new": "Nowości",
|
"new": "Nowości",
|
||||||
"soon": "Wkrótce",
|
"soon": "Wkrótce dostępne",
|
||||||
"aktionen": "Promocje",
|
"aktionen": "Promocje",
|
||||||
"filiale": "Sklep",
|
"filiale": "Sklep",
|
||||||
"categories": "Kategorie",
|
"categories": "Kategorie",
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ export default {
|
|||||||
"thcTest": "Test THC",
|
"thcTest": "Test THC",
|
||||||
"address1": "Trachenberger Street 14",
|
"address1": "Trachenberger Street 14",
|
||||||
"address2": "01129 Dresden",
|
"address2": "01129 Dresden",
|
||||||
"showUsPhoto": "Pokaż nam swoje najpiękniejsze zdjęcie",
|
"buildYourSet": "Złóż swój zestaw",
|
||||||
"selectSeedRate": "Wybierz nasiono, kliknij ocenę",
|
"selectSeedRate": "Wybierz nasiono, kliknij ocenę",
|
||||||
"outdoorSeason": "Sezon outdoorowy się zaczyna"
|
"outdoorSeason": "Sezon outdoorowy się zaczyna"
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,5 +3,7 @@ export default {
|
|||||||
"perPage": "pe pagină",
|
"perPage": "pe pagină",
|
||||||
"availability": "Disponibilitate",
|
"availability": "Disponibilitate",
|
||||||
"manufacturer": "Producător",
|
"manufacturer": "Producător",
|
||||||
"all": "Toate"
|
"all": "Toate",
|
||||||
|
"notifyNewArticles": "Anunță-mă despre articole noi",
|
||||||
|
"notifyNewArticlesBrowserUnsupported": "Browserul nu acceptă notificări push."
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
export default {
|
export default {
|
||||||
"home": "Acasă",
|
"home": "Acasă",
|
||||||
|
"konfiguratorAria": "Mergi la Configurator",
|
||||||
"new": "Noutăți",
|
"new": "Noutăți",
|
||||||
"soon": "În curând",
|
"soon": "În curând",
|
||||||
"aktionen": "Promoții",
|
"aktionen": "Promoții",
|
||||||
"filiale": "Magazin",
|
"filiale": "Magazin",
|
||||||
"categories": "Categorii",
|
"categories": "Categorii",
|
||||||
"categoriesOpen": "Deschide categorii",
|
"categoriesOpen": "Deschide categoriile",
|
||||||
"categoriesClose": "Închide categorii",
|
"categoriesClose": "Închide categoriile",
|
||||||
"otherCategories": "Alte categorii"
|
"otherCategories": "Alte categorii"
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ export default {
|
|||||||
"thcTest": "Test THC",
|
"thcTest": "Test THC",
|
||||||
"address1": "Trachenberger Street 14",
|
"address1": "Trachenberger Street 14",
|
||||||
"address2": "01129 Dresden",
|
"address2": "01129 Dresden",
|
||||||
"showUsPhoto": "Arată-ne cea mai frumoasă fotografie a ta",
|
"buildYourSet": "Construiește-ți setul",
|
||||||
"selectSeedRate": "Alege sămânța, apasă pe evaluare",
|
"selectSeedRate": "Alege sămânța, apasă pe evaluare",
|
||||||
"outdoorSeason": "Sezonul outdoor începe"
|
"outdoorSeason": "Sezonul outdoor începe"
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,5 +3,7 @@ export default {
|
|||||||
"perPage": "на странице",
|
"perPage": "на странице",
|
||||||
"availability": "Наличие",
|
"availability": "Наличие",
|
||||||
"manufacturer": "Производитель",
|
"manufacturer": "Производитель",
|
||||||
"all": "Все"
|
"all": "Все",
|
||||||
|
"notifyNewArticles": "Уведомлять о новых товарах",
|
||||||
|
"notifyNewArticlesBrowserUnsupported": "Ваш браузер не поддерживает push-уведомления."
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
export default {
|
export default {
|
||||||
"home": "Главная",
|
"home": "Главная",
|
||||||
|
"konfiguratorAria": "Перейти к конфигуратору",
|
||||||
"new": "Новинки",
|
"new": "Новинки",
|
||||||
"soon": "Скоро",
|
"soon": "Скоро в продаже",
|
||||||
"aktionen": "Акции",
|
"aktionen": "Акции",
|
||||||
"filiale": "Магазин",
|
"filiale": "Магазин",
|
||||||
"categories": "Категории",
|
"categories": "Категории",
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ export default {
|
|||||||
"thcTest": "Тест THC",
|
"thcTest": "Тест THC",
|
||||||
"address1": "Trachenberger Street 14",
|
"address1": "Trachenberger Street 14",
|
||||||
"address2": "01129 Dresden",
|
"address2": "01129 Dresden",
|
||||||
"showUsPhoto": "Покажите нам ваше самое красивое фото",
|
"buildYourSet": "Соберите свой комплект",
|
||||||
"selectSeedRate": "Выберите семя, нажмите оценить",
|
"selectSeedRate": "Выберите семя, нажмите оценить",
|
||||||
"outdoorSeason": "Начинается сезон на открытом воздухе"
|
"outdoorSeason": "Начинается сезон на открытом воздухе"
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,5 +3,7 @@ export default {
|
|||||||
"perPage": "na stránku",
|
"perPage": "na stránku",
|
||||||
"availability": "Dostupnosť",
|
"availability": "Dostupnosť",
|
||||||
"manufacturer": "Výrobca",
|
"manufacturer": "Výrobca",
|
||||||
"all": "Všetko"
|
"all": "Všetko",
|
||||||
|
"notifyNewArticles": "Upozorni ma na nové produkty",
|
||||||
|
"notifyNewArticlesBrowserUnsupported": "Váš prehliadač nepodporuje push notifikácie."
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
export default {
|
export default {
|
||||||
"home": "Domov",
|
"home": "Domov",
|
||||||
|
"konfiguratorAria": "Prejsť do konfigurátora",
|
||||||
"new": "Novinky",
|
"new": "Novinky",
|
||||||
"soon": "Čoskoro",
|
"soon": "Čoskoro",
|
||||||
"aktionen": "Akcie",
|
"aktionen": "Akcie",
|
||||||
"filiale": "Predajňa",
|
"filiale": "Predajňa",
|
||||||
"categories": "Kategórie",
|
"categories": "Kategórie",
|
||||||
"categoriesOpen": "Otvoriť kategórie",
|
"categoriesOpen": "Otvoriť kategórie",
|
||||||
"categoriesClose": "Zavrieť kategórie",
|
"categoriesClose": "Zatvoriť kategórie",
|
||||||
"otherCategories": "Ďalšie kategórie"
|
"otherCategories": "Ostatné kategórie"
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ export default {
|
|||||||
"thcTest": "THC test",
|
"thcTest": "THC test",
|
||||||
"address1": "Trachenberger Street 14",
|
"address1": "Trachenberger Street 14",
|
||||||
"address2": "01129 Dresden",
|
"address2": "01129 Dresden",
|
||||||
"showUsPhoto": "Ukážte nám svoju najkrajšiu fotku",
|
"buildYourSet": "Zostavte si svoj set",
|
||||||
"selectSeedRate": "Vyberte semeno, kliknite na hodnotenie",
|
"selectSeedRate": "Vyberte semeno, kliknite na hodnotenie",
|
||||||
"outdoorSeason": "Začína vonkajšia sezóna"
|
"outdoorSeason": "Začína vonkajšia sezóna"
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,5 +3,7 @@ export default {
|
|||||||
"perPage": "na stran",
|
"perPage": "na stran",
|
||||||
"availability": "Razpoložljivost",
|
"availability": "Razpoložljivost",
|
||||||
"manufacturer": "Proizvajalec",
|
"manufacturer": "Proizvajalec",
|
||||||
"all": "Vse"
|
"all": "Vse",
|
||||||
|
"notifyNewArticles": "Obvesti me o novih izdelkih",
|
||||||
|
"notifyNewArticlesBrowserUnsupported": "Vaš brskalnik ne podpira potisnih obvestil."
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
export default {
|
export default {
|
||||||
"home": "Domov",
|
"home": "Domov",
|
||||||
"new": "Nove prihajajoče",
|
"konfiguratorAria": "Pojdi v konfigurator",
|
||||||
"soon": "Kmalu prihaja",
|
"new": "Nove izdelke",
|
||||||
|
"soon": "Kmalu na voljo",
|
||||||
"aktionen": "Promocije",
|
"aktionen": "Promocije",
|
||||||
"filiale": "Trgovina",
|
"filiale": "Trgovina",
|
||||||
"categories": "Kategorije",
|
"categories": "Kategorije",
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ export default {
|
|||||||
"thcTest": "THC test",
|
"thcTest": "THC test",
|
||||||
"address1": "Trachenberger Straße 14",
|
"address1": "Trachenberger Straße 14",
|
||||||
"address2": "01129 Dresden",
|
"address2": "01129 Dresden",
|
||||||
"showUsPhoto": "Pokaži nam svojo najlepšo fotografijo",
|
"buildYourSet": "Sestavi svoj set",
|
||||||
"selectSeedRate": "Izberi seme, klikni oceno",
|
"selectSeedRate": "Izberi seme, klikni oceno",
|
||||||
"outdoorSeason": "Zunanja sezona se začenja"
|
"outdoorSeason": "Zunanja sezona se začenja"
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,5 +3,7 @@ export default {
|
|||||||
"perPage": "për faqe",
|
"perPage": "për faqe",
|
||||||
"availability": "Disponueshmëria",
|
"availability": "Disponueshmëria",
|
||||||
"manufacturer": "Prodhuesi",
|
"manufacturer": "Prodhuesi",
|
||||||
"all": "Të gjitha"
|
"all": "Të gjitha",
|
||||||
|
"notifyNewArticles": "Njoftom për artikuj të rinj",
|
||||||
|
"notifyNewArticlesBrowserUnsupported": "Shfletuesi juaj nuk mbështet njoftimet push."
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
export default {
|
export default {
|
||||||
"home": "Shtëpia",
|
"home": "Kreu",
|
||||||
|
"konfiguratorAria": "Shko te Konfiguratori",
|
||||||
"new": "Arritjet e reja",
|
"new": "Arritjet e reja",
|
||||||
"soon": "Së shpejti",
|
"soon": "Së shpejti",
|
||||||
"aktionen": "Promocione",
|
"aktionen": "Promocione",
|
||||||
@@ -7,5 +8,5 @@ export default {
|
|||||||
"categories": "Kategoritë",
|
"categories": "Kategoritë",
|
||||||
"categoriesOpen": "Hap kategoritë",
|
"categoriesOpen": "Hap kategoritë",
|
||||||
"categoriesClose": "Mbyll kategoritë",
|
"categoriesClose": "Mbyll kategoritë",
|
||||||
"otherCategories": "Kategori të tjera"
|
"otherCategories": "Kategoritë e tjera"
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ export default {
|
|||||||
"thcTest": "Test THC",
|
"thcTest": "Test THC",
|
||||||
"address1": "Rruga Trachenberger 14",
|
"address1": "Rruga Trachenberger 14",
|
||||||
"address2": "01129 Dresden",
|
"address2": "01129 Dresden",
|
||||||
"showUsPhoto": "Na tregoni foton tuaj më të bukur",
|
"buildYourSet": "Ndërto setin tënd",
|
||||||
"selectSeedRate": "Zgjidhni farën, klikoni vlerësimin",
|
"selectSeedRate": "Zgjidhni farën, klikoni vlerësimin",
|
||||||
"outdoorSeason": "Fillon sezoni i jashtëm"
|
"outdoorSeason": "Fillon sezoni i jashtëm"
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,5 +3,7 @@ export default {
|
|||||||
"perPage": "po stranici",
|
"perPage": "po stranici",
|
||||||
"availability": "Dostupnost",
|
"availability": "Dostupnost",
|
||||||
"manufacturer": "Proizvođač",
|
"manufacturer": "Proizvođač",
|
||||||
"all": "Sve"
|
"all": "Sve",
|
||||||
|
"notifyNewArticles": "Obavesti me o novim artiklima",
|
||||||
|
"notifyNewArticlesBrowserUnsupported": "Vaš pregledač ne podržava push obaveštenja."
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
export default {
|
export default {
|
||||||
"home": "Početna",
|
"home": "Početna",
|
||||||
"new": "Nove kolekcije",
|
"konfiguratorAria": "Idi na Konfigurator",
|
||||||
|
"new": "Novi artikli",
|
||||||
"soon": "Uskoro",
|
"soon": "Uskoro",
|
||||||
"aktionen": "Promocije",
|
"aktionen": "Promocije",
|
||||||
"filiale": "Prodavnica",
|
"filiale": "Prodavnica",
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ export default {
|
|||||||
"thcTest": "THC test",
|
"thcTest": "THC test",
|
||||||
"address1": "Trachenberger ulica 14",
|
"address1": "Trachenberger ulica 14",
|
||||||
"address2": "01129 Dresden",
|
"address2": "01129 Dresden",
|
||||||
"showUsPhoto": "Pokažite nam svoju najlepšu fotografiju",
|
"buildYourSet": "Sastavite svoj set",
|
||||||
"selectSeedRate": "Izaberite seme, kliknite ocenu",
|
"selectSeedRate": "Izaberite seme, kliknite ocenu",
|
||||||
"outdoorSeason": "Počinje sezona na otvorenom"
|
"outdoorSeason": "Počinje sezona na otvorenom"
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,5 +3,7 @@ export default {
|
|||||||
"perPage": "per sida",
|
"perPage": "per sida",
|
||||||
"availability": "Tillgänglighet",
|
"availability": "Tillgänglighet",
|
||||||
"manufacturer": "Tillverkare",
|
"manufacturer": "Tillverkare",
|
||||||
"all": "Alla"
|
"all": "Alla",
|
||||||
|
"notifyNewArticles": "Meddela mig om nya artiklar",
|
||||||
|
"notifyNewArticlesBrowserUnsupported": "Din webbläsare stöder inte push-notiser."
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
export default {
|
export default {
|
||||||
"home": "Hem",
|
"home": "Hem",
|
||||||
"new": "Nya produkter",
|
"konfiguratorAria": "Gå till konfiguratorn",
|
||||||
|
"new": "Nyheter",
|
||||||
"soon": "Kommer snart",
|
"soon": "Kommer snart",
|
||||||
"aktionen": "Kampanjer",
|
"aktionen": "Kampanjer",
|
||||||
"filiale": "Butik",
|
"filiale": "Butik",
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ export default {
|
|||||||
"thcTest": "THC-test",
|
"thcTest": "THC-test",
|
||||||
"address1": "Trachenberger Street 14",
|
"address1": "Trachenberger Street 14",
|
||||||
"address2": "01129 Dresden",
|
"address2": "01129 Dresden",
|
||||||
"showUsPhoto": "Visa oss ditt vackraste foto",
|
"buildYourSet": "Sätt ihop ditt set",
|
||||||
"selectSeedRate": "Välj frö, klicka på betygsätt",
|
"selectSeedRate": "Välj frö, klicka på betygsätt",
|
||||||
"outdoorSeason": "Utomhussäsongen börjar"
|
"outdoorSeason": "Utomhussäsongen börjar"
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,5 +3,7 @@ export default {
|
|||||||
"perPage": "sayfa başına",
|
"perPage": "sayfa başına",
|
||||||
"availability": "Stok Durumu",
|
"availability": "Stok Durumu",
|
||||||
"manufacturer": "Üretici",
|
"manufacturer": "Üretici",
|
||||||
"all": "Tümü"
|
"all": "Tümü",
|
||||||
|
"notifyNewArticles": "Yeni ürünlerden haberdar et",
|
||||||
|
"notifyNewArticlesBrowserUnsupported": "Tarayıcınız anlık bildirimleri desteklemiyor."
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
export default {
|
export default {
|
||||||
"home": "Ana Sayfa",
|
"home": "Ana Sayfa",
|
||||||
|
"konfiguratorAria": "Yapılandırıcıya Git",
|
||||||
"new": "Yeni Gelenler",
|
"new": "Yeni Gelenler",
|
||||||
"soon": "Çok Yakında",
|
"soon": "Yakında",
|
||||||
"aktionen": "Kampanyalar",
|
"aktionen": "Kampanyalar",
|
||||||
"filiale": "Mağaza",
|
"filiale": "Mağaza",
|
||||||
"categories": "Kategoriler",
|
"categories": "Kategoriler",
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ export default {
|
|||||||
"thcTest": "THC Testi",
|
"thcTest": "THC Testi",
|
||||||
"address1": "Trachenberger Street 14",
|
"address1": "Trachenberger Street 14",
|
||||||
"address2": "01129 Dresden",
|
"address2": "01129 Dresden",
|
||||||
"showUsPhoto": "Bize en güzel fotoğrafınızı gösterin",
|
"buildYourSet": "Setini bir araya getir",
|
||||||
"selectSeedRate": "Tohum seçin, puan verin",
|
"selectSeedRate": "Tohum seçin, puan verin",
|
||||||
"outdoorSeason": "Dış mekân sezonu başlıyor"
|
"outdoorSeason": "Dış mekân sezonu başlıyor"
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,5 +3,7 @@ export default {
|
|||||||
"perPage": "на сторінку",
|
"perPage": "на сторінку",
|
||||||
"availability": "Наявність",
|
"availability": "Наявність",
|
||||||
"manufacturer": "Виробник",
|
"manufacturer": "Виробник",
|
||||||
"all": "Усі"
|
"all": "Усі",
|
||||||
|
"notifyNewArticles": "Повідомляти про нові товари",
|
||||||
|
"notifyNewArticlesBrowserUnsupported": "Ваш браузер не підтримує push-сповіщення."
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
export default {
|
export default {
|
||||||
"home": "Головна",
|
"home": "Головна",
|
||||||
|
"konfiguratorAria": "Перейти до конфігуратора",
|
||||||
"new": "Новинки",
|
"new": "Новинки",
|
||||||
"soon": "Незабаром",
|
"soon": "Незабаром",
|
||||||
"aktionen": "Акції",
|
"aktionen": "Акції",
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ export default {
|
|||||||
"thcTest": "Тест THC",
|
"thcTest": "Тест THC",
|
||||||
"address1": "Trachenberger Street 14",
|
"address1": "Trachenberger Street 14",
|
||||||
"address2": "01129 Dresden",
|
"address2": "01129 Dresden",
|
||||||
"showUsPhoto": "Покажіть нам ваше найкрасивіше фото",
|
"buildYourSet": "Зберіть свій комплект",
|
||||||
"selectSeedRate": "Оберіть насіння, натисніть оцінити",
|
"selectSeedRate": "Оберіть насіння, натисніть оцінити",
|
||||||
"outdoorSeason": "Починається сезон вирощування на відкритому повітрі"
|
"outdoorSeason": "Починається сезон вирощування на відкритому повітрі"
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,5 +3,7 @@ export default {
|
|||||||
"perPage": "每页",
|
"perPage": "每页",
|
||||||
"availability": "库存情况",
|
"availability": "库存情况",
|
||||||
"manufacturer": "制造商",
|
"manufacturer": "制造商",
|
||||||
"all": "全部"
|
"all": "全部",
|
||||||
|
"notifyNewArticles": "新商品上架时通知我",
|
||||||
|
"notifyNewArticlesBrowserUnsupported": "您的浏览器不支持推送通知。"
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
export default {
|
export default {
|
||||||
"home": "首页",
|
"home": "首页",
|
||||||
"new": "新品",
|
"konfiguratorAria": "前往配置器",
|
||||||
|
"new": "新品上架",
|
||||||
"soon": "即将推出",
|
"soon": "即将推出",
|
||||||
"aktionen": "促销活动",
|
"aktionen": "促销活动",
|
||||||
"filiale": "门店",
|
"filiale": "门店",
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ export default {
|
|||||||
"thcTest": "THC 测试",
|
"thcTest": "THC 测试",
|
||||||
"address1": "Trachenberger 街 14",
|
"address1": "Trachenberger 街 14",
|
||||||
"address2": "01129 德累斯顿",
|
"address2": "01129 德累斯顿",
|
||||||
"showUsPhoto": "展示你最美的照片",
|
"buildYourSet": "组合你的套装",
|
||||||
"selectSeedRate": "选择种子,点击评分",
|
"selectSeedRate": "选择种子,点击评分",
|
||||||
"outdoorSeason": "户外季节开始了"
|
"outdoorSeason": "户外季节开始了"
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -41,7 +41,7 @@
|
|||||||
body {
|
body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
font-family: Roboto, Helvetica, Arial, sans-serif;
|
font-family: 'Outfit Variable', Roboto, Helvetica, Arial, sans-serif;
|
||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
-moz-osx-font-smoothing: grayscale;
|
-moz-osx-font-smoothing: grayscale;
|
||||||
overflow-y: scroll; /* Always show vertical scrollbar */
|
overflow-y: scroll; /* Always show vertical scrollbar */
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import ReactDOM from "react-dom/client";
|
import ReactDOM from "react-dom/client";
|
||||||
|
import "@fontsource-variable/outfit";
|
||||||
import "./index.css";
|
import "./index.css";
|
||||||
import App from "./App.js";
|
import App from "./App.js";
|
||||||
import { BrowserRouter } from "react-router-dom";
|
import { BrowserRouter } from "react-router-dom";
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user