feat: add Hersteller page with manufacturer data fetching and SEO support
This commit is contained in:
@@ -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: {
|
||||||
|
|||||||
@@ -131,6 +131,8 @@ const {
|
|||||||
generateHomepageJsonLd,
|
generateHomepageJsonLd,
|
||||||
generateSitemapJsonLd,
|
generateSitemapJsonLd,
|
||||||
generateKonfiguratorMetaTags,
|
generateKonfiguratorMetaTags,
|
||||||
|
generateHerstellerMetaTags,
|
||||||
|
generateHerstellerJsonLd,
|
||||||
generateXmlSitemap,
|
generateXmlSitemap,
|
||||||
generateRobotsTxt,
|
generateRobotsTxt,
|
||||||
generateProductsXml,
|
generateProductsXml,
|
||||||
@@ -142,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");
|
||||||
@@ -161,6 +164,7 @@ 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;
|
||||||
|
|
||||||
@@ -376,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,
|
||||||
@@ -383,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,
|
||||||
@@ -392,7 +421,10 @@ const renderApp = async (categoryData, socket) => {
|
|||||||
description,
|
description,
|
||||||
metaTags,
|
metaTags,
|
||||||
needsRouter,
|
needsRouter,
|
||||||
config
|
config,
|
||||||
|
false, // suppressLogs
|
||||||
|
null, // productData
|
||||||
|
manufacturerDataForPage // manufacturerData - 10th parameter!
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -474,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" },
|
||||||
{
|
{
|
||||||
@@ -492,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 = "";
|
||||||
|
|
||||||
@@ -509,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++;
|
||||||
|
|||||||
@@ -140,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;
|
||||||
|
|
||||||
@@ -383,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
|
||||||
|
|||||||
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,
|
||||||
|
};
|
||||||
@@ -23,6 +23,11 @@ const {
|
|||||||
generateKonfiguratorMetaTags,
|
generateKonfiguratorMetaTags,
|
||||||
} = require('./konfigurator.cjs');
|
} = require('./konfigurator.cjs');
|
||||||
|
|
||||||
|
const {
|
||||||
|
generateHerstellerMetaTags,
|
||||||
|
generateHerstellerJsonLd,
|
||||||
|
} = require('./hersteller.cjs');
|
||||||
|
|
||||||
const {
|
const {
|
||||||
generateRobotsTxt,
|
generateRobotsTxt,
|
||||||
generateProductsXml,
|
generateProductsXml,
|
||||||
@@ -56,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,
|
||||||
|
|||||||
@@ -54,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"));
|
||||||
@@ -310,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;
|
||||||
@@ -2,17 +2,24 @@ import React from 'react';
|
|||||||
import { Link } from 'react-router-dom';
|
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);
|
||||||
@@ -29,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);
|
||||||
@@ -66,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;
|
||||||
|
|
||||||
@@ -88,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;
|
||||||
@@ -96,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={{
|
||||||
@@ -135,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',
|
||||||
@@ -157,9 +279,11 @@ class ManufacturerCarousel extends React.Component {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{items.map((item, index) => (
|
{items.map((item, index) => (
|
||||||
<Link
|
<Paper
|
||||||
key={`${item.id}-${index}`}
|
key={`${item.id}-${index}`}
|
||||||
|
component={Link}
|
||||||
to={`/Hersteller/${encodeURIComponent(item.slug || '')}`}
|
to={`/Hersteller/${encodeURIComponent(item.slug || '')}`}
|
||||||
|
elevation={3}
|
||||||
style={{
|
style={{
|
||||||
flex: '0 0 140px',
|
flex: '0 0 140px',
|
||||||
width: '140px',
|
width: '140px',
|
||||||
@@ -171,6 +295,18 @@ class ManufacturerCarousel extends React.Component {
|
|||||||
userSelect: 'none',
|
userSelect: 'none',
|
||||||
textDecoration: 'none',
|
textDecoration: 'none',
|
||||||
cursor: 'pointer',
|
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
|
||||||
@@ -184,7 +320,7 @@ class ManufacturerCarousel extends React.Component {
|
|||||||
display: 'block',
|
display: 'block',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Link>
|
</Paper>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
168
src/pages/HerstellerPage.js
Normal file
168
src/pages/HerstellerPage.js
Normal file
@@ -0,0 +1,168 @@
|
|||||||
|
import React, { Component } from 'react';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
import Box from '@mui/material/Box';
|
||||||
|
import Paper from '@mui/material/Paper';
|
||||||
|
import LegalPage from './LegalPage.js';
|
||||||
|
import { withI18n } from '../i18n/withTranslation.js';
|
||||||
|
|
||||||
|
class HerstellerPage extends Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
loading: true,
|
||||||
|
manufacturers: [],
|
||||||
|
};
|
||||||
|
this._isMounted = false;
|
||||||
|
this._objectUrls = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this._isMounted = true;
|
||||||
|
this.loadManufacturers();
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
this._isMounted = false;
|
||||||
|
for (const url of this._objectUrls) {
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
}
|
||||||
|
this._objectUrls = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
loadManufacturers = () => {
|
||||||
|
// Check if manufacturers data is already cached from prerendering
|
||||||
|
if (window.herstellerImages && Array.isArray(window.herstellerImages) && window.herstellerImages.length > 0) {
|
||||||
|
if (!this._isMounted) return;
|
||||||
|
|
||||||
|
const manufacturers = window.herstellerImages
|
||||||
|
.filter(m => m.imageBuffer)
|
||||||
|
.map(m => {
|
||||||
|
const blob = new Blob([m.imageBuffer], { type: 'image/avif' });
|
||||||
|
const src = URL.createObjectURL(blob);
|
||||||
|
this._objectUrls.push(src);
|
||||||
|
return {
|
||||||
|
id: m.id,
|
||||||
|
name: m.name || '',
|
||||||
|
slug: m.slug || '',
|
||||||
|
src,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.sort((a, b) => (a.name || '').localeCompare(b.name || '', undefined, { sensitivity: 'base' }));
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
loading: false,
|
||||||
|
manufacturers,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: fetch from socket if no cached data
|
||||||
|
window.socketManager.emit('getHerstellerImages', {}, (res) => {
|
||||||
|
if (!this._isMounted) return;
|
||||||
|
|
||||||
|
if (!res?.success || !Array.isArray(res.manufacturers)) {
|
||||||
|
this.setState({ loading: false, manufacturers: [] });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const manufacturers = res.manufacturers
|
||||||
|
.filter(m => m.imageBuffer)
|
||||||
|
.map(m => {
|
||||||
|
const blob = new Blob([m.imageBuffer], { type: 'image/avif' });
|
||||||
|
const src = URL.createObjectURL(blob);
|
||||||
|
this._objectUrls.push(src);
|
||||||
|
return {
|
||||||
|
id: m.id,
|
||||||
|
name: m.name || '',
|
||||||
|
slug: m.slug || '',
|
||||||
|
src,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.sort((a, b) => (a.name || '').localeCompare(b.name || '', undefined, { sensitivity: 'base' }));
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
loading: false,
|
||||||
|
manufacturers,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
renderManufacturerGrid = () => {
|
||||||
|
const { manufacturers } = this.state;
|
||||||
|
|
||||||
|
if (!manufacturers.length) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'grid',
|
||||||
|
gridTemplateColumns: 'repeat(auto-fit, minmax(140px, 1fr))',
|
||||||
|
gap: 2,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{manufacturers.map((manufacturer) => (
|
||||||
|
<Paper
|
||||||
|
key={manufacturer.id}
|
||||||
|
component={Link}
|
||||||
|
to={`/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,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={manufacturer.src}
|
||||||
|
alt={manufacturer.name}
|
||||||
|
draggable={false}
|
||||||
|
style={{
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
objectFit: 'contain',
|
||||||
|
display: 'block',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Paper>
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { t } = this.props;
|
||||||
|
const { loading } = this.state;
|
||||||
|
|
||||||
|
const content = (
|
||||||
|
<Box>
|
||||||
|
{loading ? null : (
|
||||||
|
this.renderManufacturerGrid()
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
|
||||||
|
return <LegalPage title={t ? t('product.manufacturer') : 'Hersteller'} content={content} />;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default withI18n()(HerstellerPage);
|
||||||
Reference in New Issue
Block a user