Compare commits

...

8 Commits

Author SHA1 Message Date
seb
9e14827c91 Enhance 404 handling in webpack configuration with async middleware for prerendering. Updated NotFound404 page to improve user experience with localized messaging and responsive image styling. Added taxonomy ID mappings in feeds.cjs for better compliance and clarity in product categorization. 2025-07-07 02:12:19 +02:00
seb
8698816875 Add middleware to handle 404 routes in webpack configuration. Implemented custom response handling to ensure proper 404 status and no-cache headers, along with rewrites for SPA fallback to improve routing behavior. 2025-07-06 23:50:30 +02:00
seb
987de641e4 Refactor unit pricing logic in feeds.cjs to enhance compliance with German regulations. Updated the helper function to return structured unit pricing data, including both unit and base measures, and adjusted XML generation accordingly. 2025-07-06 22:54:13 +02:00
seb
23e1742e40 Add unit pricing measure to product XML generation in feeds.cjs. Updated Product, ProductDetailPage, and AddToCartButton components to support new pricing fields (fGrundPreis, cGrundEinheit) for compliance with German regulations. Enhanced SearchBar with enter icon functionality for improved user experience. 2025-07-06 20:36:23 +02:00
seb
205558d06c Add CarouselProvider to Prerender components for improved layout structure. Updated PrerenderAppContent and PrerenderHome to wrap MainPageLayout with CarouselProvider, enhancing component organization and consistency. 2025-07-06 09:35:34 +02:00
seb
046979a64d Refactor Prerender components to replace Home page with MainPageLayout, improving structure and consistency across the application. Updated routing in PrerenderAppContent and PrerenderHome to utilize the new layout component. 2025-07-06 09:33:34 +02:00
seb
161e377de4 Update category mappings in feeds.cjs for improved accuracy and clarity. Adjusted several category paths to reflect more specific classifications, and added validation to ensure non-empty category returns. Updated language setting to 'de-DE' for consistency. 2025-07-06 09:30:10 +02:00
seb
73a88f508b Refactor App component to replace Home page with MainPageLayout, integrating CarouselProvider for improved page structure. Added new routes for Presseverleih and ThcTest pages, enhancing navigation and organization. Updated Header component to support new page states for Aktionen and Filiale. 2025-07-06 09:25:39 +02:00
24 changed files with 1464 additions and 806 deletions

2
.gitignore vendored
View File

@@ -56,6 +56,8 @@ yarn-error.log*
# Local configuration # Local configuration
src/config.local.js src/config.local.js
taxonomy-with-ids.de-DE*
# Local development notes # Local development notes
dev-notes.md dev-notes.md
dev-notes.local.md dev-notes.local.md

View File

@@ -11,6 +11,31 @@ Crawl-delay: 0
return robotsTxt; return robotsTxt;
}; };
// Helper function to determine unit pricing data based on product data
const determineUnitPricingData = (product) => {
const result = {
unit_pricing_measure: null,
unit_pricing_base_measure: null
};
// unit_pricing_measure: The quantity unit of the product as it's sold
if (product.fEinheitMenge && product.cEinheit) {
const amount = parseFloat(product.fEinheitMenge);
const unit = product.cEinheit.trim();
if (amount > 0 && unit) {
result.unit_pricing_measure = `${amount}${unit}`;
}
}
// unit_pricing_base_measure: The base quantity unit for unit pricing
if (product.cGrundEinheit && product.cGrundEinheit.trim()) {
result.unit_pricing_base_measure = product.cGrundEinheit.trim();
}
return result;
};
const generateProductsXml = (allProductsData = [], baseUrl, config) => { const generateProductsXml = (allProductsData = [], baseUrl, config) => {
const currentDate = new Date().toISOString(); const currentDate = new Date().toISOString();
@@ -23,124 +48,131 @@ const generateProductsXml = (allProductsData = [], baseUrl, config) => {
const getGoogleProductCategory = (categoryId) => { const getGoogleProductCategory = (categoryId) => {
const categoryMappings = { const categoryMappings = {
// Seeds & Plants // Seeds & Plants
689: "Home & Garden > Plants > Seeds", 689: "543561", // Seeds (Saatgut)
706: "Home & Garden > Plants", // Stecklinge (cuttings) 706: "543561", // Stecklinge (cuttings) ebenfalls Pflanzen/Saatgut
376: "Home & Garden > Plants > Plant & Herb Growing Kits", // Grow-Sets 376: "2802", // Grow-Sets Pflanzen- & Kräuteranbausets
// Headshop & Accessories // Headshop & Accessories
709: "Arts & Entertainment > Hobbies & Creative Arts", // Headshop 709: "4082", // Headshop Rauchzubehör
711: "Arts & Entertainment > Hobbies & Creative Arts", // Bongs 711: "4082", // Headshop > Bongs Rauchzubehör
714: "Arts & Entertainment > Hobbies & Creative Arts", // Zubehör 714: "4082", // Headshop > Bongs > Zubehör Rauchzubehör
748: "Arts & Entertainment > Hobbies & Creative Arts", // Köpfe 748: "4082", // Headshop > Bongs > Köpfe Rauchzubehör
749: "Arts & Entertainment > Hobbies & Creative Arts", // Chillums / Diffusoren / Kupplungen 749: "4082", // Headshop > Bongs > Chillums/Diffusoren/Kupplungen Rauchzubehör
896: "Electronics > Electronics Accessories", // Vaporizer 896: "3151", // Headshop > Vaporizer Vaporizer
710: "Home & Garden > Kitchen & Dining > Kitchen Tools & Utensils", // Grinder 710: "5109", // Headshop > Grinder Gewürzmühlen (Küchenhelfer)
// Measuring & Packaging // Measuring & Packaging
186: "Business & Industrial", // Wiegen & Verpacken 186: "5631", // Headshop > Wiegen & Verpacken Aufbewahrung/Zubehör
187: "Business & Industrial > Science & Laboratory > Lab Equipment", // Waagen 187: "4767", // Headshop > Waagen Personenwaagen (Medizinisch)
346: "Home & Garden > Kitchen & Dining > Food Storage", // Vakuumbeutel 346: "7118", // Headshop > Vakuumbeutel Vakuumierer-Beutel
355: "Home & Garden > Kitchen & Dining > Food Storage", // Boveda & Integra Boost 355: "606", // Headshop > Boveda & Integra Boost Luftentfeuchter (nächstmögliche)
407: "Home & Garden > Kitchen & Dining > Food Storage", // Grove Bags 407: "3561", // Headshop > Grove Bags Aufbewahrungsbehälter
449: "Home & Garden > Kitchen & Dining > Food Storage", // Cliptütchen 449: "1496", // Headshop > Cliptütchen Lebensmittelverpackungsmaterial
539: "Home & Garden > Kitchen & Dining > Food Storage", // Gläser & Dosen 539: "3110", // Headshop > Gläser & Dosen Lebensmittelbehälter
// Lighting & Equipment // Lighting & Equipment
694: "Home & Garden > Lighting", // Lampen 694: "3006", // Lampen Lampen (Beleuchtung)
261: "Home & Garden > Lighting", // Lampenzubehör 261: "3006", // Zubehör > Lampenzubehör Lampen
// Plants & Growing // Plants & Growing
691: "Home & Garden > Lawn & Garden > Fertilizers", // Dünger 691: "500033", // Dünger Dünger
692: "Home & Garden > Lawn & Garden > Fertilizers", // Dünger - Zubehör 692: "5633", // Zubehör > Dünger-Zubehör Zubehör für Gartenarbeit
693: "Sporting Goods > Outdoor Recreation > Camping & Hiking > Tents", // Zelte 693: "5655", // Zelte Zelte
// Pots & Containers // Pots & Containers
219: "Home & Garden > Decor > Planters & Pots", // Töpfe 219: "113", // Töpfe Blumentöpfe & Pflanzgefäße
220: "Home & Garden > Decor > Planters & Pots", // Untersetzer 220: "3173", // Töpfe > Untersetzer Gartentopfuntersetzer und Trays
301: "Home & Garden > Decor > Planters & Pots", // Stofftöpfe 301: "113", // Töpfe > Stofftöpfe (Blumentöpfe/Pflanzgefäße)
317: "Home & Garden > Decor > Planters & Pots", // Air-Pot 317: "113", // Töpfe > Air-Pot (Blumentöpfe/Pflanzgefäße)
364: "Home & Garden > Decor > Planters & Pots", // Kunststofftöpfe 364: "113", // Töpfe > Kunststofftöpfe (Blumentöpfe/Pflanzgefäße)
292: "Home & Garden > Decor > Planters & Pots", // Trays & Fluttische 292: "3568", // Bewässerung > Trays & Fluttische Bewässerungssysteme
// Ventilation & Climate // Ventilation & Climate
703: "Home & Garden > Outdoor Power Tools", // Abluft-Sets 703: "2802", // Grow-Sets > Abluft-Sets (verwendet Pflanzen-Kräuter-Anbausets)
247: "Home & Garden > Outdoor Power Tools", // Belüftung 247: "1700", // Belüftung Ventilatoren (Klimatisierung)
214: "Home & Garden > Outdoor Power Tools", // Umluft-Ventilatoren 214: "1700", // Belüftung > Umluft-Ventilatoren Ventilatoren
308: "Home & Garden > Outdoor Power Tools", // Ab- und Zuluft 308: "1700", // Belüftung > Ab- und Zuluft Ventilatoren
609: "Home & Garden > Outdoor Power Tools", // Schalldämpfer 609: "1700", // Belüftung > Ab- und Zuluft > Schalldämpfer Ventilatoren
248: "Home & Garden > Pool & Spa > Pool & Spa Filters", // Aktivkohlefilter 248: "1700", // Belüftung > Aktivkohlefilter Ventilatoren (nächstmögliche)
392: "Home & Garden > Pool & Spa > Pool & Spa Filters", // Zuluftfilter 392: "1700", // Belüftung > Ab- und Zuluft > Zuluftfilter Ventilatoren
658: "Home & Garden > Climate Control > Dehumidifiers", // Luftbe- und entfeuchter 658: "606", // Belüftung > Luftbe- und -entfeuchter Luftentfeuchter
310: "Home & Garden > Climate Control > Heating", // Heizmatten 310: "2802", // Anzucht > Heizmatten Pflanzen- & Kräuteranbausets
379: "Home & Garden > Household Supplies > Air Fresheners", // Geruchsneutralisation 379: "5631", // Belüftung > Geruchsneutralisation Haushaltsbedarf: Aufbewahrung
// Irrigation & Watering // Irrigation & Watering
221: "Home & Garden > Lawn & Garden > Watering Equipment", // Bewässerung 221: "3568", // Bewässerung Bewässerungssysteme (Gesamt)
250: "Home & Garden > Lawn & Garden > Watering Equipment", // Schläuche 250: "6318", // Bewässerung > Schläuche Gartenschläuche
297: "Home & Garden > Lawn & Garden > Watering Equipment", // Pumpen 297: "500100", // Bewässerung > Pumpen Bewässerung-/Sprinklerpumpen
354: "Home & Garden > Lawn & Garden > Watering Equipment", // Sprüher 354: "3780", // Bewässerung > Sprüher Sprinkler & Sprühköpfe
372: "Home & Garden > Lawn & Garden > Watering Equipment", // AutoPot 372: "3568", // Bewässerung > AutoPot Bewässerungssysteme
389: "Home & Garden > Lawn & Garden > Watering Equipment", // Blumat 389: "3568", // Bewässerung > Blumat Bewässerungssysteme
405: "Home & Garden > Lawn & Garden > Watering Equipment", // Schläuche 405: "6318", // Bewässerung > Schläuche Gartenschläuche
425: "Home & Garden > Lawn & Garden > Watering Equipment", // Wassertanks 425: "3568", // Bewässerung > Wassertanks Bewässerungssysteme
480: "Home & Garden > Lawn & Garden > Watering Equipment", // Tropfer 480: "3568", // Bewässerung > Tropfer Bewässerungssysteme
519: "Home & Garden > Lawn & Garden > Watering Equipment", // Pumpsprüher 519: "3568", // Bewässerung > Pumpsprüher Bewässerungssysteme
// Growing Media & Soils // Growing Media & Soils
242: "Home & Garden > Lawn & Garden > Fertilizers", // Böden 242: "543677", // Böden Gartenerde
243: "Home & Garden > Lawn & Garden > Fertilizers", // Erde 243: "543677", // Böden > Erde Gartenerde
269: "Home & Garden > Lawn & Garden > Fertilizers", // Kokos 269: "543677", // Böden > Kokos Gartenerde
580: "Home & Garden > Lawn & Garden > Fertilizers", // Perlite & Blähton 580: "543677", // Böden > Perlite & Blähton Gartenerde
// Propagation & Starting // Propagation & Starting
286: "Home & Garden > Plants", // Anzucht 286: "2802", // Anzucht Pflanzen- & Kräuteranbausets
298: "Home & Garden > Plants", // Steinwolltrays 298: "2802", // Anzucht > Steinwolltrays Pflanzen- & Kräuteranbausets
421: "Home & Garden > Plants", // Vermehrungszubehör 421: "2802", // Anzucht > Vermehrungszubehör Pflanzen- & Kräuteranbausets
489: "Home & Garden > Plants", // EazyPlug & Jiffy 489: "2802", // Anzucht > EazyPlug & Jiffy Pflanzen- & Kräuteranbausets
359: "Home & Garden > Outdoor Structures > Greenhouses", // Gewächshäuser 359: "3103", // Anzucht > Gewächshäuser Gewächshäuser
// Tools & Equipment // Tools & Equipment
373: "Home & Garden > Tools > Hand Tools", // GrowTool 373: "3568", // Bewässerung > GrowTool Bewässerungssysteme
403: "Home & Garden > Kitchen & Dining > Kitchen Tools & Utensils", // Messbecher & mehr 403: "3999", // Bewässerung > Messbecher & mehr Messbecher & Dosierlöffel
259: "Home & Garden > Tools > Hand Tools", // Pressen 259: "756", // Zubehör > Ernte & Verarbeitung > Pressen Nudelmaschinen
280: "Home & Garden > Tools > Hand Tools", // Erntescheeren 280: "2948", // Zubehör > Ernte & Verarbeitung > Erntescheeren Küchenmesser
258: "Home & Garden > Tools", // Ernte & Verarbeitung 258: "684", // Zubehör > Ernte & Verarbeitung Abfallzerkleinerer
278: "Home & Garden > Tools", // Extraktion 278: "5057", // Zubehör > Ernte & Verarbeitung > Extraktion Slush-Eis-Maschinen
302: "Home & Garden > Tools", // Erntemaschinen 302: "7332", // Zubehör > Ernte & Verarbeitung > Erntemaschinen Gartenmaschinen
// Hardware & Plumbing // Hardware & Plumbing
222: "Hardware > Plumbing", // PE-Teile 222: "3568", // Bewässerung > PE-Teile Bewässerungssysteme
374: "Hardware > Plumbing > Plumbing Fittings", // Verbindungsteile 374: "1700", // Belüftung > Ab- und Zuluft > Verbindungsteile Ventilatoren
// Electronics & Control // Electronics & Control
314: "Electronics > Electronics Accessories", // Steuergeräte 314: "1700", // Belüftung > Steuergeräte Ventilatoren
408: "Electronics > Electronics Accessories", // GrowControl 408: "1700", // Belüftung > Steuergeräte > GrowControl Ventilatoren
344: "Business & Industrial > Science & Laboratory > Lab Equipment", // Messgeräte 344: "1207", // Zubehör > Messgeräte Messwerkzeuge & Messwertgeber
555: "Business & Industrial > Science & Laboratory > Lab Equipment > Microscopes", // Mikroskope 555: "4555", // Zubehör > Anbauzubehör > Mikroskope Mikroskope
// Camping & Outdoor // Camping & Outdoor
226: "Sporting Goods > Outdoor Recreation > Camping & Hiking", // Zeltzubehör 226: "5655", // Zubehör > Zeltzubehör Zelte
// Plant Care & Protection // Plant Care & Protection
239: "Home & Garden > Lawn & Garden > Pest Control", // Pflanzenschutz 239: "4085", // Zubehör > Anbauzubehör > Pflanzenschutz Herbizide
240: "Home & Garden > Plants", // Anbauzubehör 240: "5633", // Zubehör > Anbauzubehör Zubehör für Gartenarbeit
// Office & Media // Office & Media
424: "Office Supplies > Labels", // Etiketten & Schilder 424: "4377", // Zubehör > Anbauzubehör > Etiketten & Schilder Etiketten & Anhängerschilder
387: "Media > Books", // Literatur 387: "543541", // Zubehör > Anbauzubehör > Literatur Bücher
// General categories // General categories
705: "Home & Garden", // Set-Konfigurator 705: "2802", // Grow-Sets > Set-Konfigurator (ebenfalls Pflanzen-Anbausets)
686: "Home & Garden", // Zubehör 686: "1700", // Belüftung > Aktivkohlefilter > Zubehör Ventilatoren
741: "Home & Garden", // Zubehör 741: "1700", // Belüftung > Ab- und Zuluft > Zubehör Ventilatoren
294: "Home & Garden", // Zubehör 294: "3568", // Bewässerung > Zubehör Bewässerungssysteme
695: "Home & Garden", // Zubehör 695: "5631", // Zubehör Haushaltsbedarf: Aufbewahrung
293: "Home & Garden", // Trockennetze 293: "5631", // Zubehör > Ernte & Verarbeitung > Trockennetze Haushaltsbedarf: Aufbewahrung
4: "Home & Garden", // Sonstiges 4: "5631", // Zubehör > Anbauzubehör > Sonstiges Haushaltsbedarf: Aufbewahrung
450: "Home & Garden", // Restposten 450: "5631", // Zubehör > Anbauzubehör > Restposten Haushaltsbedarf: Aufbewahrung
}; };
return categoryMappings[categoryId] || "Home & Garden > Plants"; const categoryId_str = categoryMappings[categoryId] || "5631"; // Default to Haushaltsbedarf: Aufbewahrung
// Validate that the category ID is not empty
if (!categoryId_str || categoryId_str.trim() === "") {
return "5631"; // Haushaltsbedarf: Aufbewahrung
}
return categoryId_str;
}; };
let productsXml = `<?xml version="1.0" encoding="UTF-8"?> let productsXml = `<?xml version="1.0" encoding="UTF-8"?>
@@ -150,7 +182,7 @@ const generateProductsXml = (allProductsData = [], baseUrl, config) => {
<link>${baseUrl}</link> <link>${baseUrl}</link>
<description>${config.descriptions.short}</description> <description>${config.descriptions.short}</description>
<lastBuildDate>${currentDate}</lastBuildDate> <lastBuildDate>${currentDate}</lastBuildDate>
<language>${config.language}</language>`; <language>de-DE</language>`;
// Helper function to clean text content of problematic characters // Helper function to clean text content of problematic characters
const cleanTextContent = (text) => { const cleanTextContent = (text) => {
@@ -318,6 +350,17 @@ const generateProductsXml = (allProductsData = [], baseUrl, config) => {
<g:shipping_weight>${parseFloat(product.weight).toFixed(2)} g</g:shipping_weight>`; <g:shipping_weight>${parseFloat(product.weight).toFixed(2)} g</g:shipping_weight>`;
} }
// Add unit pricing data (required by German law for many products)
const unitPricingData = determineUnitPricingData(product);
if (unitPricingData.unit_pricing_measure) {
productsXml += `
<g:unit_pricing_measure>${unitPricingData.unit_pricing_measure}</g:unit_pricing_measure>`;
}
if (unitPricingData.unit_pricing_base_measure) {
productsXml += `
<g:unit_pricing_base_measure>${unitPricingData.unit_pricing_base_measure}</g:unit_pricing_base_measure>`;
}
productsXml += ` productsXml += `
</item>`; </item>`;

Binary file not shown.

After

Width:  |  Height:  |  Size: 208 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 242 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

View File

@@ -18,13 +18,14 @@ import BugReportIcon from "@mui/icons-material/BugReport";
import SocketProvider from "./providers/SocketProvider.js"; import SocketProvider from "./providers/SocketProvider.js";
import SocketContext from "./contexts/SocketContext.js"; import SocketContext from "./contexts/SocketContext.js";
import { CarouselProvider } from "./contexts/CarouselContext.js";
import config from "./config.js"; import config from "./config.js";
import ScrollToTop from "./components/ScrollToTop.js"; import ScrollToTop from "./components/ScrollToTop.js";
//import TelemetryService from './services/telemetryService.js'; //import TelemetryService from './services/telemetryService.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 Home from "./pages/Home.js"; import MainPageLayout from "./components/MainPageLayout.js";
// Lazy load all route components to reduce initial bundle size // Lazy load all route components to reduce initial bundle size
const Content = lazy(() => import(/* webpackChunkName: "content" */ "./components/Content.js")); const Content = lazy(() => import(/* webpackChunkName: "content" */ "./components/Content.js"));
@@ -50,6 +51,10 @@ const Widerrufsrecht = lazy(() => import(/* webpackChunkName: "legal" */ "./page
const GrowTentKonfigurator = lazy(() => import(/* webpackChunkName: "konfigurator" */ "./pages/GrowTentKonfigurator.js")); const GrowTentKonfigurator = lazy(() => import(/* webpackChunkName: "konfigurator" */ "./pages/GrowTentKonfigurator.js"));
const ChatAssistant = lazy(() => import(/* webpackChunkName: "chat" */ "./components/ChatAssistant.js")); const ChatAssistant = lazy(() => import(/* webpackChunkName: "chat" */ "./components/ChatAssistant.js"));
// Lazy load separate pages that are truly different
const PresseverleihPage = lazy(() => import(/* webpackChunkName: "presseverleih" */ "./pages/PresseverleihPage.js"));
const ThcTestPage = lazy(() => import(/* webpackChunkName: "thc-test" */ "./pages/ThcTestPage.js"));
// Import theme from separate file to reduce main bundle size // Import theme from separate file to reduce main bundle size
import defaultTheme from "./theme.js"; import defaultTheme from "./theme.js";
// Lazy load theme customizer for development only // Lazy load theme customizer for development only
@@ -195,60 +200,68 @@ const AppContent = ({ currentTheme, onThemeChange }) => {
<CircularProgress color="primary" /> <CircularProgress color="primary" />
</Box> </Box>
}> }>
<Routes> <CarouselProvider>
{/* Home page with text only */} <Routes>
<Route path="/" element={<Home />} /> {/* Main pages using unified component */}
<Route path="/" element={<MainPageLayout />} />
<Route path="/aktionen" element={<MainPageLayout />} />
<Route path="/filiale" element={<MainPageLayout />} />
{/* Category page - Render Content in parallel */} {/* Category page - Render Content in parallel */}
<Route <Route
path="/Kategorie/:categoryId" path="/Kategorie/:categoryId"
element={<Content socket={socket} socketB={socketB} />} element={<Content socket={socket} socketB={socketB} />}
/> />
{/* Single product page */} {/* Single product page */}
<Route <Route
path="/Artikel/:seoName" path="/Artikel/:seoName"
element={<ProductDetailWithSocket />} element={<ProductDetailWithSocket />}
/> />
{/* Search page - Render Content in parallel */} {/* Search page - Render Content in parallel */}
<Route path="/search" element={<Content socket={socket} socketB={socketB} />} /> <Route path="/search" element={<Content socket={socket} socketB={socketB} />} />
{/* Profile page */} {/* Profile page */}
<Route path="/profile" element={<ProfilePageWithSocket />} /> <Route path="/profile" element={<ProfilePageWithSocket />} />
{/* Reset password page */} {/* Reset password page */}
<Route <Route
path="/resetPassword" path="/resetPassword"
element={<ResetPassword socket={socket} socketB={socketB} />} element={<ResetPassword socket={socket} socketB={socketB} />}
/> />
{/* Admin page */} {/* Admin page */}
<Route path="/admin" element={<AdminPage socket={socket} socketB={socketB} />} /> <Route path="/admin" element={<AdminPage socket={socket} socketB={socketB} />} />
{/* Admin Users page */} {/* Admin Users page */}
<Route path="/admin/users" element={<UsersPage socket={socket} socketB={socketB} />} /> <Route path="/admin/users" element={<UsersPage socket={socket} socketB={socketB} />} />
{/* Admin Server Logs page */} {/* Admin Server Logs page */}
<Route path="/admin/logs" element={<ServerLogsPage socket={socket} socketB={socketB} />} /> <Route path="/admin/logs" element={<ServerLogsPage socket={socket} socketB={socketB} />} />
{/* Legal pages */} {/* Legal pages */}
<Route path="/datenschutz" element={<Datenschutz />} /> <Route path="/datenschutz" element={<Datenschutz />} />
<Route path="/agb" element={<AGB />} /> <Route path="/agb" element={<AGB />} />
<Route path="/404" element={<NotFound404 />} /> <Route path="/404" element={<NotFound404 />} />
<Route path="/sitemap" element={<Sitemap />} /> <Route path="/sitemap" element={<Sitemap />} />
<Route path="/impressum" element={<Impressum />} /> <Route path="/impressum" element={<Impressum />} />
<Route <Route
path="/batteriegesetzhinweise" path="/batteriegesetzhinweise"
element={<Batteriegesetzhinweise />} element={<Batteriegesetzhinweise />}
/> />
<Route path="/widerrufsrecht" element={<Widerrufsrecht />} /> <Route path="/widerrufsrecht" element={<Widerrufsrecht />} />
{/* Grow Tent Configurator */} {/* Grow Tent Configurator */}
<Route path="/Konfigurator" element={<GrowTentKonfigurator />} /> <Route path="/Konfigurator" element={<GrowTentKonfigurator />} />
{/* Fallback for undefined routes */} {/* Separate pages that are truly different */}
<Route path="*" element={<Navigate to="/" replace />} /> <Route path="/presseverleih" element={<PresseverleihPage />} />
</Routes> <Route path="/thc-test" element={<ThcTestPage />} />
{/* Fallback for undefined routes */}
<Route path="*" element={<Navigate to="/" replace />} />
</Routes>
</CarouselProvider>
</Suspense> </Suspense>
</Box> </Box>
{/* Conditionally render the Chat Assistant */} {/* Conditionally render the Chat Assistant */}

View File

@@ -3,7 +3,8 @@ import { Box, AppBar, Toolbar, Container} from '@mui/material';
import { Routes, Route } from 'react-router-dom'; import { Routes, Route } from 'react-router-dom';
import Footer from './components/Footer.js'; import Footer from './components/Footer.js';
import { Logo, CategoryList } from './components/header/index.js'; import { Logo, CategoryList } from './components/header/index.js';
import Home from './pages/Home.js'; import MainPageLayout from './components/MainPageLayout.js';
import { CarouselProvider } from './contexts/CarouselContext.js';
const PrerenderAppContent = (socket) => ( const PrerenderAppContent = (socket) => (
<Box <Box
@@ -44,9 +45,11 @@ const PrerenderAppContent = (socket) => (
</AppBar> </AppBar>
<Box sx={{ flexGrow: 1 }}> <Box sx={{ flexGrow: 1 }}>
<Routes> <CarouselProvider>
<Route path="/" element={<Home />} /> <Routes>
</Routes> <Route path="/" element={<MainPageLayout />} />
</Routes>
</CarouselProvider>
</Box> </Box>
<Footer/> <Footer/>

View File

@@ -7,7 +7,8 @@ const {
} = require('@mui/material'); } = require('@mui/material');
const Footer = require('./components/Footer.js').default; const Footer = require('./components/Footer.js').default;
const { Logo, CategoryList } = require('./components/header/index.js'); const { Logo, CategoryList } = require('./components/header/index.js');
const Home = require('./pages/Home.js').default; const MainPageLayout = require('./components/MainPageLayout.js').default;
const { CarouselProvider } = require('./contexts/CarouselContext.js');
class PrerenderHome extends React.Component { class PrerenderHome extends React.Component {
render() { render() {
@@ -62,7 +63,7 @@ class PrerenderHome extends React.Component {
React.createElement( React.createElement(
Box, Box,
{ sx: { flexGrow: 1 } }, { sx: { flexGrow: 1 } },
React.createElement(Home) React.createElement(CarouselProvider, null, React.createElement(MainPageLayout))
), ),
React.createElement(Footer) React.createElement(Footer)
); );

92
src/PrerenderNotFound.js Normal file
View File

@@ -0,0 +1,92 @@
const React = require('react');
const {
Box,
AppBar,
Toolbar,
Container
} = require('@mui/material');
const Footer = require('./components/Footer.js').default;
const { Logo } = require('./components/header/index.js');
const NotFound404 = require('./pages/NotFound404.js').default;
class PrerenderNotFound extends React.Component {
render() {
return React.createElement(
Box,
{
sx: {
display: 'flex',
flexDirection: 'column',
minHeight: '100vh',
mb: 0,
pb: 0,
bgcolor: 'background.default'
}
},
React.createElement(
AppBar,
{ position: 'sticky', color: 'primary', elevation: 0, sx: { zIndex: 1100 } },
React.createElement(
Toolbar,
{ sx: { minHeight: 64, py: { xs: 0.5, sm: 0 } } },
React.createElement(
Container,
{
maxWidth: 'lg',
sx: {
display: 'flex',
alignItems: 'center',
px: { xs: 0, sm: 3 }
}
},
React.createElement(
Box,
{ sx: {
display: 'flex',
alignItems: 'center',
width: '100%',
flexDirection: { xs: 'column', sm: 'row' }
}
},
React.createElement(
Box,
{ sx: {
display: 'flex',
alignItems: 'center',
width: '100%',
justifyContent: { xs: 'space-between', sm: 'flex-start' },
minHeight: { xs: 52, sm: 'auto' },
px: { xs: 0, sm: 0 }
}
},
React.createElement(Logo)
),
// Reserve space for SearchBar on mobile (invisible placeholder)
React.createElement(
Box,
{ sx: {
display: { xs: 'block', sm: 'none' },
width: '100%',
mt: { xs: 1, sm: 0 },
mb: { xs: 0.5, sm: 0 },
px: { xs: 0, sm: 0 },
height: 40, // Reserve space for SearchBar
opacity: 0 // Invisible placeholder
}
}
)
)
)
)
),
React.createElement(
Box,
{ sx: { flexGrow: 1 } },
React.createElement(NotFound404)
),
React.createElement(Footer)
);
}
}
module.exports = { default: PrerenderNotFound };

View File

@@ -51,6 +51,8 @@ class AddToCartButton extends Component {
seoName: this.props.seoName, seoName: this.props.seoName,
pictureList: this.props.pictureList, pictureList: this.props.pictureList,
price: this.props.price, price: this.props.price,
fGrundPreis: this.props.fGrundPreis,
cGrundEinheit: this.props.cGrundEinheit,
quantity: 1, quantity: 1,
weight: this.props.weight, weight: this.props.weight,
vat: this.props.vat, vat: this.props.vat,

View File

@@ -38,7 +38,7 @@ class Header extends Component {
render() { render() {
// Get socket directly from context in render method // Get socket directly from context in render method
const {socket,socketB} = this.context; const {socket,socketB} = this.context;
const { isHomePage, isProfilePage } = this.props; const { isHomePage, isProfilePage, isAktionenPage, isFilialePage } = this.props;
return ( return (
<AppBar position="sticky" color="primary" elevation={0} sx={{ zIndex: 1100 }}> <AppBar position="sticky" color="primary" elevation={0} sx={{ zIndex: 1100 }}>
@@ -94,7 +94,7 @@ class Header extends Component {
</Box> </Box>
</Container> </Container>
</Toolbar> </Toolbar>
{(isHomePage || this.props.categoryId || isProfilePage) && <CategoryList categoryId={209} activeCategoryId={this.props.categoryId} socket={socket} socketB={socketB} />} {(isHomePage || this.props.categoryId || isProfilePage || isAktionenPage || isFilialePage) && <CategoryList categoryId={209} activeCategoryId={this.props.categoryId} socket={socket} socketB={socketB} />}
</AppBar> </AppBar>
); );
} }
@@ -105,10 +105,12 @@ const HeaderWithContext = (props) => {
const location = useLocation(); const location = useLocation();
const isHomePage = location.pathname === '/'; const isHomePage = location.pathname === '/';
const isProfilePage = location.pathname === '/profile'; const isProfilePage = location.pathname === '/profile';
const isAktionenPage = location.pathname === '/aktionen';
const isFilialePage = location.pathname === '/filiale';
return ( return (
<SocketContext.Consumer> <SocketContext.Consumer>
{({socket,socketB}) => <Header {...props} socket={socket} socketB={socketB} isHomePage={isHomePage} isProfilePage={isProfilePage} />} {({socket,socketB}) => <Header {...props} socket={socket} socketB={socketB} isHomePage={isHomePage} isProfilePage={isProfilePage} isAktionenPage={isAktionenPage} isFilialePage={isFilialePage} />}
</SocketContext.Consumer> </SocketContext.Consumer>
); );
}; };

View File

@@ -0,0 +1,349 @@
import React from "react";
import { useLocation } from "react-router-dom";
import Container from "@mui/material/Container";
import Box from "@mui/material/Box";
import Typography from "@mui/material/Typography";
import Paper from "@mui/material/Paper";
import Grid from "@mui/material/Grid";
import ChevronLeft from "@mui/icons-material/ChevronLeft";
import ChevronRight from "@mui/icons-material/ChevronRight";
import { Link } from "react-router-dom";
import SharedCarousel from "./SharedCarousel.js";
import { getCombinedAnimatedBorderStyles } from "../utils/animatedBorderStyles.js";
const MainPageLayout = () => {
const location = useLocation();
const currentPath = location.pathname;
// Determine which page we're on
const isHome = currentPath === "/";
const isAktionen = currentPath === "/aktionen";
const isFiliale = currentPath === "/filiale";
// Get navigation config based on current page
const getNavigationConfig = () => {
if (isHome) {
return {
leftNav: { text: "Aktionen", link: "/aktionen" },
rightNav: { text: "Filiale", link: "/filiale" }
};
} else if (isAktionen) {
return {
leftNav: { text: "Filiale", link: "/filiale" },
rightNav: { text: "Home", link: "/" }
};
} else if (isFiliale) {
return {
leftNav: { text: "Home", link: "/" },
rightNav: { text: "Aktionen", link: "/aktionen" }
};
}
return { leftNav: null, rightNav: null };
};
// Define all titles for layered rendering
const allTitles = {
home: "ine annabis eeds & uttings",
aktionen: "Aktionen",
filiale: "Filiale"
};
// Define all content boxes for layered rendering
const allContentBoxes = {
home: [
{
title: "Samen",
image: "/assets/images/seeds.jpg",
bgcolor: "#e1f0d3",
link: "/Kategorie/Samen"
},
{
title: "Stecklinge",
image: "/assets/images/cutlings.jpg",
bgcolor: "#e8f5d6",
link: "/Kategorie/Stecklinge"
}
],
aktionen: [
{
title: "Ölpresse ausleihen",
image: "/assets/images/presse.jpg",
bgcolor: "#e1f0d3",
link: "/presseverleih"
},
{
title: "THC Test",
image: "/assets/images/purpl.jpg",
bgcolor: "#e8f5d6",
link: "/thc-test"
}
],
filiale: [
{
title: "Trachenberger Straße 14",
image: "/assets/images/filiale1.jpg",
bgcolor: "#e1f0d3",
link: "/filiale"
},
{
title: "01129 Dresden",
image: "/assets/images/filiale2.jpg",
bgcolor: "#e8f5d6",
link: "/filiale"
}
]
};
// Get opacity for each page layer
const getOpacity = (pageType) => {
if (pageType === "home" && isHome) return 1;
if (pageType === "aktionen" && isAktionen) return 1;
if (pageType === "filiale" && isFiliale) return 1;
return 0;
};
const navConfig = getNavigationConfig();
return (
<Container maxWidth="lg" sx={{ py: 2 }}>
<style>{getCombinedAnimatedBorderStyles(['seeds', 'cutlings'])}</style>
{/* Main Navigation Header */}
<Box sx={{
display: "flex",
alignItems: "center",
justifyContent: "space-between",
mb: 4,
mt: 2,
px: 0,
transition: "all 0.3s ease-in-out"
}}>
{/* Left Navigation - Layered rendering */}
<Box sx={{
display: "flex",
alignItems: "center",
flexShrink: 0,
justifyContent: "flex-start",
position: "relative",
mr: 2
}}>
{["Aktionen", "Filiale", "Home"].map((text, index) => {
const isActive = navConfig.leftNav && navConfig.leftNav.text === text;
const link = text === "Aktionen" ? "/aktionen" : text === "Filiale" ? "/filiale" : "/";
return (
<Box
key={text}
component={Link}
to={link}
sx={{
display: "flex",
alignItems: "center",
textDecoration: "none",
color: "inherit",
transition: "all 0.3s ease",
opacity: isActive ? 1 : 0,
position: index === 0 ? "relative" : "absolute",
left: index !== 0 ? 0 : "auto",
pointerEvents: isActive ? "auto" : "none",
"&:hover": {
transform: "translateX(-5px)",
color: "primary.main"
}
}}
>
<ChevronLeft sx={{ fontSize: "2rem", mr: 1 }} />
<Typography
sx={{
fontFamily: "SwashingtonCP",
fontSize: { xs: "1.25rem", sm: "1.25rem", md: "2.125rem" },
textShadow: "2px 2px 4px rgba(0, 0, 0, 0.3)",
lineHeight: { xs: "1.2", sm: "1.2", md: "1.1" },
whiteSpace: "nowrap"
}}
>
{text}
</Typography>
</Box>
);
})}
</Box>
{/* Center Title - Layered rendering - This defines the height for centering */}
<Box sx={{
flex: 1,
display: "flex",
justifyContent: "center",
alignItems: "center",
px: 0,
position: "relative",
minWidth: 0
}}>
{Object.entries(allTitles).map(([pageType, title]) => (
<Typography
key={pageType}
variant="h3"
component="h1"
sx={{
fontFamily: "SwashingtonCP",
fontSize: { xs: "2.125rem", sm: "2.125rem", md: "3rem" },
textAlign: "center",
color: "primary.main",
textShadow: "3px 3px 10px rgba(0, 0, 0, 0.4)",
transition: "opacity 0.5s ease-in-out",
opacity: getOpacity(pageType),
position: pageType === "home" ? "relative" : "absolute",
top: pageType !== "home" ? "50%" : "auto",
left: pageType !== "home" ? "50%" : "auto",
transform: pageType !== "home" ? "translate(-50%, -50%)" : "none",
width: "100%",
pointerEvents: getOpacity(pageType) === 1 ? "auto" : "none",
lineHeight: { xs: "1.2", sm: "1.2", md: "1.1" },
wordWrap: "break-word",
hyphens: "auto"
}}
>
{title}
</Typography>
))}
</Box>
{/* Right Navigation - Layered rendering */}
<Box sx={{
display: "flex",
alignItems: "center",
flexShrink: 0,
justifyContent: "flex-end",
position: "relative",
ml: 2
}}>
{["Aktionen", "Filiale", "Home"].map((text, index) => {
const isActive = navConfig.rightNav && navConfig.rightNav.text === text;
const link = text === "Aktionen" ? "/aktionen" : text === "Filiale" ? "/filiale" : "/";
return (
<Box
key={text}
component={Link}
to={link}
sx={{
display: "flex",
alignItems: "center",
textDecoration: "none",
color: "inherit",
transition: "all 0.3s ease",
opacity: isActive ? 1 : 0,
position: index === 0 ? "relative" : "absolute",
right: index !== 0 ? 0 : "auto",
pointerEvents: isActive ? "auto" : "none",
"&:hover": {
transform: "translateX(5px)",
color: "primary.main"
}
}}
>
<Typography
sx={{
fontFamily: "SwashingtonCP",
fontSize: { xs: "1.25rem", sm: "1.25rem", md: "2.125rem" },
textShadow: "2px 2px 4px rgba(0, 0, 0, 0.3)",
lineHeight: { xs: "1.2", sm: "1.2", md: "1.1" },
whiteSpace: "nowrap"
}}
>
{text}
</Typography>
<ChevronRight sx={{ fontSize: "2rem", ml: 1 }} />
</Box>
);
})}
</Box>
</Box>
{/* Content Boxes - Layered rendering */}
<Box sx={{ position: "relative", mb: 4 }}>
{Object.entries(allContentBoxes).map(([pageType, contentBoxes]) => (
<Grid
key={pageType}
container
spacing={0}
sx={{
transition: "opacity 0.5s ease-in-out",
opacity: getOpacity(pageType),
position: pageType === "home" ? "relative" : "absolute",
top: 0,
left: 0,
width: "100%",
pointerEvents: getOpacity(pageType) === 1 ? "auto" : "none"
}}
>
{contentBoxes.map((box, index) => (
<Grid key={`${pageType}-${index}`} item xs={12} sm={6} sx={{ p: 2, width: "50%" }}>
<div className={`animated-border-card ${index === 0 ? 'seeds-card' : 'cutlings-card'}`}>
<Paper
component={Link}
to={box.link}
sx={{
p: 0,
textDecoration: "none",
color: "text.primary",
borderRadius: 2,
overflow: "hidden",
height: { xs: 150, sm: 200, md: 300 },
display: "flex",
flexDirection: "column",
boxShadow: 10,
transition: "all 0.3s ease",
"&:hover": {
transform: "translateY(-5px)",
boxShadow: 20,
},
}}
>
<Box
sx={{
height: "100%",
bgcolor: box.bgcolor,
backgroundImage: `url("${box.image}")`,
backgroundSize: "contain",
backgroundPosition: "center",
backgroundRepeat: "no-repeat",
position: "relative",
}}
>
<Box
sx={{
position: "absolute",
bottom: 0,
left: 0,
right: 0,
bgcolor: "rgba(27, 94, 32, 0.8)",
p: 1,
}}
>
<Typography
sx={{
fontSize: "1.6rem",
color: "white",
fontFamily: "SwashingtonCP",
}}
>
{box.title}
</Typography>
</Box>
</Box>
</Paper>
</div>
</Grid>
))}
</Grid>
))}
</Box>
{/* Shared Carousel */}
<SharedCarousel />
</Container>
);
};
export default MainPageLayout;

View File

@@ -68,7 +68,7 @@ class Product extends Component {
render() { render() {
const { const {
id, name, price, available, manufacturer, seoName, id, name, price, available, manufacturer, seoName,
currency, vat, massMenge, massEinheit, thc, currency, vat, cGrundEinheit, fGrundPreis, thc,
floweringWeeks,incoming, neu, weight, versandklasse, availableSupplier floweringWeeks,incoming, neu, weight, versandklasse, availableSupplier
} = this.props; } = this.props;
@@ -341,8 +341,8 @@ class Product extends Component {
</Typography> </Typography>
{massMenge != 1 && massEinheit && (<Typography variant="body2" color="text.secondary" sx={{ m: 0,p: 0 }}> {cGrundEinheit && fGrundPreis && fGrundPreis != price && (<Typography variant="body2" color="text.secondary" sx={{ m: 0,p: 0 }}>
({new Intl.NumberFormat('de-DE', {style: 'currency', currency: currency || 'EUR'}).format(price/massMenge)}/{massEinheit}) ({new Intl.NumberFormat('de-DE', {style: 'currency', currency: currency || 'EUR'}).format(fGrundPreis)}/{cGrundEinheit})
</Typography> )} </Typography> )}
</div> </div>
{/*incoming*/} {/*incoming*/}
@@ -358,7 +358,7 @@ class Product extends Component {
> >
<ZoomInIcon /> <ZoomInIcon />
</IconButton> </IconButton>
<AddToCartButton cartButton={true} availableSupplier={availableSupplier} available={available} incoming={incoming} seoName={seoName} pictureList={this.props.pictureList} id={id} price={price} vat={vat} weight={weight} name={name} versandklasse={versandklasse}/> <AddToCartButton cartButton={true} availableSupplier={availableSupplier} cGrundEinheit={cGrundEinheit} fGrundPreis={fGrundPreis} available={available} incoming={incoming} seoName={seoName} pictureList={this.props.pictureList} id={id} price={price} vat={vat} weight={weight} name={name} versandklasse={versandklasse}/>
</Box> </Box>
</Card> </Card>
</Box> </Box>

View File

@@ -452,7 +452,11 @@ class ProductDetailPage extends Component {
</Typography> </Typography>
<Typography variant="body2" color="text.secondary"> <Typography variant="body2" color="text.secondary">
inkl. {product.vat}% MwSt. inkl. {product.vat}% MwSt.
{product.cGrundEinheit && product.fGrundPreis && (
<>; {new Intl.NumberFormat('de-DE', {style: 'currency', currency: 'EUR'}).format(product.fGrundPreis)}/{product.cGrundEinheit}</>
)}
</Typography> </Typography>
{product.versandklasse && {product.versandklasse &&
product.versandklasse != "standard" && product.versandklasse != "standard" &&
product.versandklasse != "kostenlos" && ( product.versandklasse != "kostenlos" && (
@@ -516,12 +520,15 @@ class ProductDetailPage extends Component {
available={product.available} available={product.available}
id={product.id} id={product.id}
availableSupplier={product.availableSupplier} availableSupplier={product.availableSupplier}
cGrundEinheit={product.cGrundEinheit}
fGrundPreis={product.fGrundPreis}
price={product.price} price={product.price}
vat={product.vat} vat={product.vat}
weight={product.weight} weight={product.weight}
name={cleanProductName(product.name)} name={cleanProductName(product.name)}
versandklasse={product.versandklasse} versandklasse={product.versandklasse}
/> />
<Typography <Typography
variant="caption" variant="caption"
sx={{ sx={{

View File

@@ -462,8 +462,8 @@ class ProductList extends Component {
available={product.available} available={product.available}
manufacturer={product.manufacturer} manufacturer={product.manufacturer}
vat={product.vat} vat={product.vat}
massMenge={product.massMenge} cGrundEinheit={product.cGrundEinheit}
massEinheit={product.massEinheit} fGrundPreis={product.fGrundPreis}
incoming={product.incomingDate} incoming={product.incomingDate}
neu={product.neu} neu={product.neu}
thc={product.thc} thc={product.thc}

View File

@@ -0,0 +1,229 @@
import React, { useContext, useEffect, useState } from "react";
import Box from "@mui/material/Box";
import Typography from "@mui/material/Typography";
import IconButton from "@mui/material/IconButton";
import ChevronLeft from "@mui/icons-material/ChevronLeft";
import ChevronRight from "@mui/icons-material/ChevronRight";
import CategoryBox from "./CategoryBox.js";
import SocketContext from "../contexts/SocketContext.js";
import { useCarousel } from "../contexts/CarouselContext.js";
// Helper to process and set categories
const processCategoryTree = (categoryTree) => {
if (
categoryTree &&
categoryTree.id === 209 &&
Array.isArray(categoryTree.children)
) {
return categoryTree.children;
} else {
return [];
}
};
// Check for cached data
const getProductCache = () => {
if (typeof window !== "undefined" && window.productCache) {
return window.productCache;
}
if (
typeof global !== "undefined" &&
global.window &&
global.window.productCache
) {
return global.window.productCache;
}
return null;
};
// Initialize categories
const initializeCategories = () => {
const productCache = getProductCache();
if (productCache && productCache["categoryTree_209"]) {
const cached = productCache["categoryTree_209"];
if (cached.categoryTree) {
return processCategoryTree(cached.categoryTree);
}
}
return [];
};
const SharedCarousel = () => {
const { carouselRef, filteredCategories, setFilteredCategories, moveCarousel } = useCarousel();
const context = useContext(SocketContext);
const [rootCategories, setRootCategories] = useState([]);
useEffect(() => {
const initialCategories = initializeCategories();
setRootCategories(initialCategories);
}, []);
useEffect(() => {
// Only fetch from socket if we don't already have categories
if (
rootCategories.length === 0 &&
context && context.socket && context.socket.connected &&
typeof window !== "undefined"
) {
context.socket.emit("categoryList", { categoryId: 209 }, (response) => {
if (response && response.categoryTree) {
// Store in cache
try {
if (!window.productCache) window.productCache = {};
window.productCache["categoryTree_209"] = {
categoryTree: response.categoryTree,
timestamp: Date.now(),
};
} catch (err) {
console.error(err);
}
setRootCategories(response.categoryTree.children || []);
}
});
}
}, [context, context?.socket?.connected, rootCategories.length]);
useEffect(() => {
const filtered = rootCategories.filter(
(cat) => cat.id !== 689 && cat.id !== 706
);
setFilteredCategories(filtered);
}, [rootCategories, setFilteredCategories]);
// Create duplicated array for seamless scrolling
const displayCategories = [...filteredCategories, ...filteredCategories];
if (filteredCategories.length === 0) {
return null;
}
return (
<Box sx={{ mt: 3 }}>
<Typography
variant="h4"
component="h1"
sx={{
mb: 2,
fontFamily: "SwashingtonCP",
color: "primary.main",
textAlign: "center",
textShadow: "3px 3px 10px rgba(0, 0, 0, 0.4)"
}}
>
Kategorien
</Typography>
<div
className="carousel-wrapper"
style={{
position: 'relative',
overflow: 'hidden',
width: '100%',
maxWidth: '1200px',
margin: '0 auto',
padding: '0 20px',
boxSizing: 'border-box'
}}
>
{/* Left Arrow */}
<IconButton
onClick={() => moveCarousel("left")}
aria-label="Vorherige Kategorien anzeigen"
style={{
position: 'absolute',
top: '50%',
left: '8px',
transform: 'translateY(-50%)',
zIndex: 1200,
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
onClick={() => moveCarousel("right")}
aria-label="Nächste Kategorien anzeigen"
style={{
position: 'absolute',
top: '50%',
right: '8px',
transform: 'translateY(-50%)',
zIndex: 1200,
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
className="carousel-container"
style={{
position: 'relative',
overflow: 'hidden',
padding: '20px 0',
width: '100%',
maxWidth: '1080px',
margin: '0 auto',
zIndex: 1,
boxSizing: 'border-box'
}}
>
<div
className="home-carousel-track"
ref={carouselRef}
style={{
display: 'flex',
gap: '16px',
transition: 'none',
alignItems: 'flex-start',
width: 'fit-content',
overflow: 'visible',
position: 'relative',
transform: 'translateX(0px)',
margin: '0 auto'
}}
>
{displayCategories.map((category, index) => (
<div
key={`${category.id}-${index}`}
className="carousel-item"
style={{
flex: '0 0 130px',
width: '130px',
maxWidth: '130px',
minWidth: '130px',
height: '130px',
maxHeight: '130px',
minHeight: '130px',
boxSizing: 'border-box',
position: 'relative'
}}
>
<CategoryBox
id={category.id}
name={category.name}
seoName={category.seoName}
image={category.image}
bgcolor={category.bgcolor}
/>
</div>
))}
</div>
</div>
</div>
</Box>
);
};
export default SharedCarousel;

View File

@@ -8,7 +8,9 @@ import ListItem from "@mui/material/ListItem";
import ListItemText from "@mui/material/ListItemText"; import ListItemText from "@mui/material/ListItemText";
import Typography from "@mui/material/Typography"; import Typography from "@mui/material/Typography";
import CircularProgress from "@mui/material/CircularProgress"; import CircularProgress from "@mui/material/CircularProgress";
import IconButton from "@mui/material/IconButton";
import SearchIcon from "@mui/icons-material/Search"; import SearchIcon from "@mui/icons-material/Search";
import KeyboardReturnIcon from "@mui/icons-material/KeyboardReturn";
import { useNavigate, useLocation } from "react-router-dom"; import { useNavigate, useLocation } from "react-router-dom";
import SocketContext from "../../contexts/SocketContext.js"; import SocketContext from "../../contexts/SocketContext.js";
@@ -184,6 +186,15 @@ const SearchBar = () => {
}, 200); }, 200);
}; };
// Handle enter icon click
const handleEnterClick = () => {
delete window.currentSearchQuery;
setShowSuggestions(false);
if (searchQuery.trim()) {
navigate(`/search?q=${encodeURIComponent(searchQuery)}`);
}
};
// Clean up timers on unmount // Clean up timers on unmount
React.useEffect(() => { React.useEffect(() => {
return () => { return () => {
@@ -244,9 +255,23 @@ const SearchBar = () => {
<SearchIcon /> <SearchIcon />
</InputAdornment> </InputAdornment>
), ),
endAdornment: loadingSuggestions && ( endAdornment: (
<InputAdornment position="end"> <InputAdornment position="end">
<CircularProgress size={16} /> {loadingSuggestions && <CircularProgress size={16} />}
<IconButton
size="small"
onClick={handleEnterClick}
sx={{
ml: loadingSuggestions ? 0.5 : 0,
p: 0.5,
color: "text.secondary",
"&:hover": {
color: "primary.main",
},
}}
>
<KeyboardReturnIcon fontSize="small" />
</IconButton>
</InputAdornment> </InputAdornment>
), ),
sx: { borderRadius: 2, bgcolor: "background.paper" }, sx: { borderRadius: 2, bgcolor: "background.paper" },

View File

@@ -0,0 +1,225 @@
import React, { createContext, useContext, useRef, useEffect, useState } from 'react';
const CarouselContext = createContext();
export const useCarousel = () => {
const context = useContext(CarouselContext);
if (!context) {
throw new Error('useCarousel must be used within a CarouselProvider');
}
return context;
};
export const CarouselProvider = ({ children }) => {
const carouselRef = useRef(null);
const scrollPositionRef = useRef(0);
const animationIdRef = useRef(null);
const isPausedRef = useRef(false);
const resumeTimeoutRef = useRef(null);
const [filteredCategories, setFilteredCategories] = useState([]);
// Initialize refs properly
useEffect(() => {
isPausedRef.current = false;
scrollPositionRef.current = 0;
}, []);
// Auto-scroll effect
useEffect(() => {
if (filteredCategories.length === 0) return;
const startAnimation = () => {
if (!carouselRef.current) {
return false;
}
isPausedRef.current = false;
const itemWidth = 146; // 130px + 16px gap
const totalWidth = filteredCategories.length * itemWidth;
const animate = () => {
if (!animationIdRef.current || isPausedRef.current) {
return;
}
scrollPositionRef.current += 0.5;
if (scrollPositionRef.current >= totalWidth) {
scrollPositionRef.current = 0;
}
if (carouselRef.current) {
const transform = `translateX(-${scrollPositionRef.current}px)`;
carouselRef.current.style.transform = transform;
}
animationIdRef.current = requestAnimationFrame(animate);
};
if (!isPausedRef.current) {
animationIdRef.current = requestAnimationFrame(animate);
return true;
}
return false;
};
// Try immediately, then with increasing delays
if (!startAnimation()) {
const timeout1 = setTimeout(() => {
if (!startAnimation()) {
const timeout2 = setTimeout(() => {
if (!startAnimation()) {
const timeout3 = setTimeout(startAnimation, 2000);
return () => clearTimeout(timeout3);
}
}, 1000);
return () => clearTimeout(timeout2);
}
}, 100);
return () => {
isPausedRef.current = true;
clearTimeout(timeout1);
if (animationIdRef.current) {
cancelAnimationFrame(animationIdRef.current);
}
if (resumeTimeoutRef.current) {
clearTimeout(resumeTimeoutRef.current);
}
};
}
return () => {
isPausedRef.current = true;
if (animationIdRef.current) {
cancelAnimationFrame(animationIdRef.current);
}
if (resumeTimeoutRef.current) {
clearTimeout(resumeTimeoutRef.current);
}
};
}, [filteredCategories]);
// Additional effect for when ref becomes available
useEffect(() => {
if (filteredCategories.length > 0 && carouselRef.current && !animationIdRef.current) {
isPausedRef.current = false;
const itemWidth = 146;
const totalWidth = filteredCategories.length * itemWidth;
const animate = () => {
if (!animationIdRef.current || isPausedRef.current) {
return;
}
scrollPositionRef.current += 0.5;
if (scrollPositionRef.current >= totalWidth) {
scrollPositionRef.current = 0;
}
if (carouselRef.current) {
const transform = `translateX(-${scrollPositionRef.current}px)`;
carouselRef.current.style.transform = transform;
}
animationIdRef.current = requestAnimationFrame(animate);
};
if (!isPausedRef.current) {
animationIdRef.current = requestAnimationFrame(animate);
}
}
});
// Manual navigation
const moveCarousel = (direction) => {
if (!carouselRef.current) return;
// Pause auto-scroll
isPausedRef.current = true;
if (animationIdRef.current) {
cancelAnimationFrame(animationIdRef.current);
animationIdRef.current = null;
}
const itemWidth = 146;
const moveAmount = itemWidth * 3;
const totalWidth = filteredCategories.length * itemWidth;
if (direction === "left") {
scrollPositionRef.current -= moveAmount;
if (scrollPositionRef.current < 0) {
scrollPositionRef.current = totalWidth + scrollPositionRef.current;
}
} else {
scrollPositionRef.current += moveAmount;
if (scrollPositionRef.current >= totalWidth) {
scrollPositionRef.current = scrollPositionRef.current % totalWidth;
}
}
// Apply smooth transition
carouselRef.current.style.transition = "transform 0.5s ease-in-out";
carouselRef.current.style.transform = `translateX(-${scrollPositionRef.current}px)`;
// Remove transition after animation
setTimeout(() => {
if (carouselRef.current) {
carouselRef.current.style.transition = "none";
}
}, 500);
// Clear existing timeout
if (resumeTimeoutRef.current) {
clearTimeout(resumeTimeoutRef.current);
}
// Resume auto-scroll after 3 seconds
resumeTimeoutRef.current = setTimeout(() => {
isPausedRef.current = false;
const animate = () => {
if (!animationIdRef.current || isPausedRef.current) {
return;
}
scrollPositionRef.current += 0.5;
if (scrollPositionRef.current >= totalWidth) {
scrollPositionRef.current = 0;
}
if (carouselRef.current) {
carouselRef.current.style.transform = `translateX(-${scrollPositionRef.current}px)`;
}
animationIdRef.current = requestAnimationFrame(animate);
};
animationIdRef.current = requestAnimationFrame(animate);
}, 3000);
};
const value = {
carouselRef,
scrollPositionRef,
animationIdRef,
isPausedRef,
resumeTimeoutRef,
filteredCategories,
setFilteredCategories,
moveCarousel
};
return (
<CarouselContext.Provider value={value}>
{children}
</CarouselContext.Provider>
);
};
export default CarouselContext;

View File

@@ -1,652 +0,0 @@
import React, { useState, useEffect, useContext, useRef } from "react";
import Container from "@mui/material/Container";
import Box from "@mui/material/Box";
import Typography from "@mui/material/Typography";
import Paper from "@mui/material/Paper";
import Grid from "@mui/material/Grid";
import IconButton from "@mui/material/IconButton";
import ChevronLeft from "@mui/icons-material/ChevronLeft";
import ChevronRight from "@mui/icons-material/ChevronRight";
import { Link } from "react-router-dom";
import CategoryBox from "../components/CategoryBox.js";
import SocketContext from "../contexts/SocketContext.js";
import { getCombinedAnimatedBorderStyles } from "../utils/animatedBorderStyles.js";
// @note SwashingtonCP font is now loaded globally via index.css
// Carousel styles - Simple styles for JavaScript-based animation
const carouselStyles = `
.carousel-wrapper {
position: relative;
overflow: hidden;
width: 100%;
max-width: 1200px;
margin: 0 auto;
padding: 0 20px;
z-index: 1;
}
.carousel-wrapper .carousel-container {
position: relative;
overflow: hidden;
padding: 20px 0;
width: 100%;
max-width: 1080px;
margin: 0 auto;
z-index: 1;
}
.carousel-wrapper .home-carousel-track {
display: flex;
gap: 16px;
transition: none;
align-items: flex-start;
width: 1200px;
max-width: 100%;
overflow: visible;
position: relative;
z-index: 1;
}
.carousel-wrapper .carousel-item {
flex: 0 0 130px;
width: 130px !important;
max-width: 130px;
min-width: 130px;
height: 130px !important;
max-height: 130px;
min-height: 130px;
box-sizing: border-box;
position: relative;
z-index: 2;
}
.carousel-nav-button {
position: absolute;
top: 50%;
transform: translateY(-50%);
z-index: 20;
background-color: rgba(255, 255, 255, 0.9);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
width: 48px;
height: 48px;
}
.carousel-nav-button:hover {
background-color: rgba(255, 255, 255, 1);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
}
.carousel-nav-left {
left: 8px;
}
.carousel-nav-right {
right: 8px;
}
`;
// Generate combined styles for both seeds and cutlings cards
const animatedBorderStyle = getCombinedAnimatedBorderStyles([
"seeds",
"cutlings",
]);
const Home = () => {
const carouselRef = useRef(null);
const scrollPositionRef = useRef(0);
const animationIdRef = useRef(null);
const isPausedRef = useRef(false);
const resumeTimeoutRef = useRef(null);
// @note Initialize refs properly
useEffect(() => {
isPausedRef.current = false;
scrollPositionRef.current = 0;
}, []);
// Helper to process and set categories
const processCategoryTree = (categoryTree) => {
if (
categoryTree &&
categoryTree.id === 209 &&
Array.isArray(categoryTree.children)
) {
return categoryTree.children;
} else {
return [];
}
};
// Check for cached data - handle both browser and prerender environments
const getProductCache = () => {
if (typeof window !== "undefined" && window.productCache) {
return window.productCache;
}
if (
typeof global !== "undefined" &&
global.window &&
global.window.productCache
) {
return global.window.productCache;
}
return null;
};
// Initialize rootCategories from cache if available (for prerendering)
const initializeCategories = () => {
const productCache = getProductCache();
if (productCache && productCache["categoryTree_209"]) {
const cached = productCache["categoryTree_209"];
//const cacheAge = Date.now() - cached.timestamp;
//const tenMinutes = 10 * 60 * 1000;
if (/*cacheAge < tenMinutes &&*/ cached.categoryTree) {
return processCategoryTree(cached.categoryTree);
}
}
return [];
};
const [rootCategories, setRootCategories] = useState(() =>
initializeCategories()
);
const context = useContext(SocketContext);
useEffect(() => {
// Only fetch from socket if we don't already have categories and we're in browser
if (
rootCategories.length === 0 &&
context && context.socket && context.socket.connected &&
typeof window !== "undefined"
) {
context.socket.emit("categoryList", { categoryId: 209 }, (response) => {
if (response && response.categoryTree) {
// Store in cache
try {
if (!window.productCache) window.productCache = {};
window.productCache["categoryTree_209"] = {
categoryTree: response.categoryTree,
timestamp: Date.now(),
};
} catch (err) {
console.error(err);
}
setRootCategories(response.categoryTree.children || []);
}
});
}
}, [context, context?.socket?.connected, rootCategories.length]);
// Filter categories (excluding specific IDs)
const filteredCategories = rootCategories.filter(
(cat) => cat.id !== 689 && cat.id !== 706
);
// Create duplicated array for seamless scrolling
const displayCategories = [...filteredCategories, ...filteredCategories];
// Auto-scroll effect
useEffect(() => {
if (filteredCategories.length === 0) return;
// @note Add a small delay to ensure DOM is ready after prerender
const startAnimation = () => {
if (!carouselRef.current) {
return false;
}
// @note Reset paused state when starting animation
isPausedRef.current = false;
const itemWidth = 146; // 130px + 16px gap
const totalWidth = filteredCategories.length * itemWidth;
const animate = () => {
// Check if we should be animating
if (!animationIdRef.current || isPausedRef.current) {
return;
}
scrollPositionRef.current += 0.5; // Speed of scrolling
// Reset position for seamless loop
if (scrollPositionRef.current >= totalWidth) {
scrollPositionRef.current = 0;
}
if (carouselRef.current) {
const transform = `translateX(-${scrollPositionRef.current}px)`;
carouselRef.current.style.transform = transform;
}
animationIdRef.current = requestAnimationFrame(animate);
};
// Only start animation if not paused
if (!isPausedRef.current) {
animationIdRef.current = requestAnimationFrame(animate);
return true;
}
return false;
};
// Try immediately, then with increasing delays to handle prerender scenarios
if (!startAnimation()) {
const timeout1 = setTimeout(() => {
if (!startAnimation()) {
const timeout2 = setTimeout(() => {
if (!startAnimation()) {
const timeout3 = setTimeout(startAnimation, 2000);
return () => clearTimeout(timeout3);
}
}, 1000);
return () => clearTimeout(timeout2);
}
}, 100);
return () => {
isPausedRef.current = true;
clearTimeout(timeout1);
if (animationIdRef.current) {
cancelAnimationFrame(animationIdRef.current);
}
if (resumeTimeoutRef.current) {
clearTimeout(resumeTimeoutRef.current);
}
};
}
return () => {
isPausedRef.current = true;
if (animationIdRef.current) {
cancelAnimationFrame(animationIdRef.current);
}
if (resumeTimeoutRef.current) {
clearTimeout(resumeTimeoutRef.current);
}
};
}, [filteredCategories]);
// Additional effect to handle cases where categories are available but ref wasn't ready
useEffect(() => {
if (filteredCategories.length > 0 && carouselRef.current && !animationIdRef.current) {
// @note Reset paused state when starting animation
isPausedRef.current = false;
const itemWidth = 146;
const totalWidth = filteredCategories.length * itemWidth;
const animate = () => {
if (!animationIdRef.current || isPausedRef.current) {
return;
}
scrollPositionRef.current += 0.5;
if (scrollPositionRef.current >= totalWidth) {
scrollPositionRef.current = 0;
}
if (carouselRef.current) {
const transform = `translateX(-${scrollPositionRef.current}px)`;
carouselRef.current.style.transform = transform;
}
animationIdRef.current = requestAnimationFrame(animate);
};
if (!isPausedRef.current) {
animationIdRef.current = requestAnimationFrame(animate);
}
}
});
// Manual navigation
const moveCarousel = (direction) => {
if (!carouselRef.current) return;
// Pause auto-scroll
isPausedRef.current = true;
if (animationIdRef.current) {
cancelAnimationFrame(animationIdRef.current);
animationIdRef.current = null;
}
const itemWidth = 146;
const moveAmount = itemWidth * 3; // Move 3 items at a time
const totalWidth = filteredCategories.length * itemWidth;
if (direction === "left") {
scrollPositionRef.current -= moveAmount;
// Handle wrapping for infinite scroll
if (scrollPositionRef.current < 0) {
scrollPositionRef.current = totalWidth + scrollPositionRef.current;
}
} else {
scrollPositionRef.current += moveAmount;
// Handle wrapping for infinite scroll
if (scrollPositionRef.current >= totalWidth) {
scrollPositionRef.current = scrollPositionRef.current % totalWidth;
}
}
// Apply smooth transition for manual navigation
carouselRef.current.style.transition = "transform 0.5s ease-in-out";
carouselRef.current.style.transform = `translateX(-${scrollPositionRef.current}px)`;
// Remove transition after animation completes
setTimeout(() => {
if (carouselRef.current) {
carouselRef.current.style.transition = "none";
}
}, 500);
// Clear any existing resume timeout
if (resumeTimeoutRef.current) {
clearTimeout(resumeTimeoutRef.current);
}
// Resume auto-scroll after 3 seconds
resumeTimeoutRef.current = setTimeout(() => {
isPausedRef.current = false;
const animate = () => {
if (!animationIdRef.current || isPausedRef.current) {
return;
}
scrollPositionRef.current += 0.5;
if (scrollPositionRef.current >= totalWidth) {
scrollPositionRef.current = 0;
}
if (carouselRef.current) {
carouselRef.current.style.transform = `translateX(-${scrollPositionRef.current}px)`;
}
animationIdRef.current = requestAnimationFrame(animate);
};
animationIdRef.current = requestAnimationFrame(animate);
}, 3000);
};
return (
<Container maxWidth="lg" sx={{ pt: 4, pb: 2, maxWidth: '1200px !important' }}>
{/* Inject the animated border and carousel styles */}
<style>{animatedBorderStyle}</style>
<style>{carouselStyles}</style>
<Typography
variant="h3"
component="h1"
sx={{
mb: 4,
fontFamily: "SwashingtonCP",
color: "primary.main",
textAlign: "center",
textShadow: "3px 3px 10px rgba(0, 0, 0, 0.4)"
}}
>
ine annabis eeds & uttings
</Typography>
<Grid container sx={{ display: "flex", flexDirection: "row" }}>
{/* Seeds Category Box */}
<Grid item xs={12} sm={6} sx={{ p: 2, width: "50%" }}>
<div className="animated-border-card seeds-card">
<Paper
component={Link}
to="/Kategorie/Seeds"
sx={{
p: 0,
textDecoration: "none",
color: "text.primary",
borderRadius: 2,
overflow: "hidden",
height: { xs: 150, sm: 200, md: 300 },
display: "flex",
flexDirection: "column",
transition: "all 0.3s ease",
boxShadow: 10,
"&:hover": {
transform: "translateY(-5px)",
boxShadow: 20,
},
}}
>
{/* Image Container - Place your seeds image here */}
<Box
sx={{
height: "100%",
bgcolor: "#e1f0d3",
backgroundImage: 'url("/assets/images/seeds.jpg")',
backgroundSize: "contain",
backgroundPosition: "center",
backgroundRepeat: "no-repeat",
position: "relative",
}}
>
{/* Overlay text - optional */}
<Box
sx={{
position: "absolute",
bottom: 0,
left: 0,
right: 0,
bgcolor: "rgba(27, 94, 32, 0.8)",
p: 1,
}}
>
<Typography
sx={{
fontSize: "1.6rem",
color: "white",
fontFamily: "SwashingtonCP",
}}
>
Seeds
</Typography>
</Box>
</Box>
</Paper>
</div>
</Grid>
{/* Cutlings Category Box */}
<Grid item xs={12} sm={6} sx={{ p: 2, width: "50%" }}>
<div className="animated-border-card cutlings-card">
<Paper
component={Link}
to="/Kategorie/Stecklinge"
sx={{
p: 0,
textDecoration: "none",
color: "text.primary",
borderRadius: 2,
overflow: "hidden",
height: { xs: 150, sm: 200, md: 300 },
display: "flex",
flexDirection: "column",
boxShadow: 10,
transition: "all 0.3s ease",
"&:hover": {
transform: "translateY(-5px)",
boxShadow: 20,
},
}}
>
{/* Image Container - Place your cutlings image here */}
<Box
sx={{
height: "100%",
bgcolor: "#e8f5d6",
backgroundImage: 'url("/assets/images/cutlings.jpg")',
backgroundSize: "contain",
backgroundPosition: "center",
backgroundRepeat: "no-repeat",
position: "relative",
}}
>
{/* Overlay text - optional */}
<Box
sx={{
position: "absolute",
bottom: 0,
left: 0,
right: 0,
bgcolor: "rgba(27, 94, 32, 0.8)",
p: 1,
}}
>
<Typography
sx={{
fontSize: "1.6rem",
color: "white",
fontFamily: "SwashingtonCP",
}}
>
Stecklinge
</Typography>
</Box>
</Box>
</Paper>
</div>
</Grid>
</Grid>
{/* Continuous Rotating Carousel for Categories */}
<Box sx={{ mt: 3 }}>
<Typography
variant="h4"
component="h1"
sx={{
mb: 2,
fontFamily: "SwashingtonCP",
color: "primary.main",
textAlign: "center",
textShadow: "3px 3px 10px rgba(0, 0, 0, 0.4)"
}}
>
Kategorien
</Typography>
{filteredCategories.length > 0 && (
<div
className="carousel-wrapper"
style={{
position: 'relative',
overflow: 'hidden',
width: '100%',
maxWidth: '1200px',
margin: '0 auto',
padding: '0 20px',
boxSizing: 'border-box'
}}
>
{/* Left Arrow */}
<IconButton
onClick={() => moveCarousel("left")}
aria-label="Vorherige Kategorien anzeigen"
style={{
position: 'absolute',
top: '50%',
left: '8px',
transform: 'translateY(-50%)',
zIndex: 1200,
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
onClick={() => moveCarousel("right")}
aria-label="Nächste Kategorien anzeigen"
style={{
position: 'absolute',
top: '50%',
right: '8px',
transform: 'translateY(-50%)',
zIndex: 1200,
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
className="carousel-container"
style={{
position: 'relative',
overflow: 'hidden',
padding: '20px 0',
width: '100%',
maxWidth: '1080px',
margin: '0 auto',
zIndex: 1,
boxSizing: 'border-box'
}}
>
<div
className="home-carousel-track"
ref={carouselRef}
style={{
display: 'flex',
gap: '16px',
transition: 'none',
alignItems: 'flex-start',
width: 'fit-content',
overflow: 'visible',
position: 'relative',
transform: 'translateX(0px)',
margin: '0 auto'
}}
>
{displayCategories.map((category, index) => (
<div
key={`${category.id}-${index}`}
className="carousel-item"
style={{
flex: '0 0 130px',
width: '130px',
maxWidth: '130px',
minWidth: '130px',
height: '130px',
maxHeight: '130px',
minHeight: '130px',
boxSizing: 'border-box',
position: 'relative'
}}
>
<CategoryBox
id={category.id}
name={category.name}
seoName={category.seoName}
image={category.image}
bgcolor={category.bgcolor}
/>
</div>
))}
</div>
</div>
</div>
)}
</Box>
</Container>
);
};
export default Home;

View File

@@ -16,19 +16,20 @@ const NotFound404 = () => {
src="/assets/images/404.png" src="/assets/images/404.png"
alt="404 - Page Not Found" alt="404 - Page Not Found"
style={{ style={{
width: '300px',
height: '300px',
maxWidth: '100%', maxWidth: '100%',
height: 'auto', display: 'block',
maxHeight: '300px',
}} }}
/> />
</Box> </Box>
<Typography variant="body1" paragraph align="center"> <Typography variant="body1" paragraph align="center">
This page is no longer available. Diese Seite scheint es nicht mehr zu geben.
</Typography> </Typography>
</> </>
); );
return <LegalPage title="Page Not Found" content={content} />; return <LegalPage content={content} />;
}; };
export default NotFound404; export default NotFound404;

View File

@@ -0,0 +1,46 @@
import React from "react";
import Container from "@mui/material/Container";
import Box from "@mui/material/Box";
import Typography from "@mui/material/Typography";
const PresseverleihPage = () => {
return (
<Container maxWidth="lg" sx={{ pt: 4, pb: 2, maxWidth: '1200px !important' }}>
<Typography
variant="h3"
component="h1"
sx={{
mb: 4,
fontFamily: "SwashingtonCP",
color: "primary.main",
textAlign: "center",
textShadow: "3px 3px 10px rgba(0, 0, 0, 0.4)"
}}
>
Ölpresse ausleihen
</Typography>
<Box
sx={{
display: "flex",
justifyContent: "center",
alignItems: "center",
minHeight: "50vh",
textAlign: "center"
}}
>
<Typography
variant="h5"
sx={{
color: "text.secondary",
fontStyle: "italic"
}}
>
Inhalt kommt bald...
</Typography>
</Box>
</Container>
);
};
export default PresseverleihPage;

46
src/pages/ThcTestPage.js Normal file
View File

@@ -0,0 +1,46 @@
import React from "react";
import Container from "@mui/material/Container";
import Box from "@mui/material/Box";
import Typography from "@mui/material/Typography";
const ThcTestPage = () => {
return (
<Container maxWidth="lg" sx={{ pt: 4, pb: 2, maxWidth: '1200px !important' }}>
<Typography
variant="h3"
component="h1"
sx={{
mb: 4,
fontFamily: "SwashingtonCP",
color: "primary.main",
textAlign: "center",
textShadow: "3px 3px 10px rgba(0, 0, 0, 0.4)"
}}
>
THC Test
</Typography>
<Box
sx={{
display: "flex",
justifyContent: "center",
alignItems: "center",
minHeight: "50vh",
textAlign: "center"
}}
>
<Typography
variant="h5"
sx={{
color: "text.secondary",
fontStyle: "italic"
}}
>
Inhalt kommt bald...
</Typography>
</Box>
</Container>
);
};
export default ThcTestPage;

View File

@@ -309,6 +309,219 @@ export default {
next(); next();
}); });
// Add middleware to handle /404 route BEFORE webpack-dev-server processing
middlewares.unshift({
name: 'handle-404-route',
middleware: async (req, res, next) => {
if (req.url === '/404') {
// Set up prerender environment
const { createRequire } = await import('module');
const require = createRequire(import.meta.url);
require('@babel/register')({
presets: [
['@babel/preset-env', { targets: { node: 'current' } }],
'@babel/preset-react'
],
extensions: ['.js', '.jsx'],
ignore: [/node_modules/]
});
// Import React first and make it globally available
const React = require('react');
global.React = React; // Make React available globally for components that don't import it
// Set up minimal globals for prerender
if (!global.window) {
global.window = {};
}
if (!global.navigator) {
global.navigator = { userAgent: 'node.js' };
}
if (!global.URL) {
global.URL = require('url').URL;
}
if (!global.Blob) {
global.Blob = class MockBlob {
constructor(data, options) {
this.data = data;
this.type = options?.type || '';
}
};
}
// Mock browser storage APIs
const mockStorage = {
getItem: () => null,
setItem: () => {},
removeItem: () => {},
clear: () => {},
key: () => null,
length: 0
};
if (!global.localStorage) {
global.localStorage = mockStorage;
}
if (!global.sessionStorage) {
global.sessionStorage = mockStorage;
}
// Also add to window object for components that access it via window
global.window.localStorage = mockStorage;
global.window.sessionStorage = mockStorage;
// Import the dedicated prerender component
const PrerenderNotFound = require('./src/PrerenderNotFound.js').default;
// Create the prerender component
const component = React.createElement(PrerenderNotFound);
// Get only the essential bundles (not lazy-loaded chunks)
let jsBundles = [];
try {
const outputFileSystem = devServer.compiler.outputFileSystem;
const outputPath = devServer.compiler.outputPath;
const jsPath = path.join(outputPath, 'js');
if (outputFileSystem.existsSync && outputFileSystem.existsSync(jsPath)) {
const jsFiles = outputFileSystem.readdirSync(jsPath);
// Only include essential bundles in correct dependency order
const essentialBundles = [];
// 1. Runtime bundle (webpack runtime - must be first)
const runtimeFile = jsFiles.find(f => f.startsWith('runtime.') && f.endsWith('.bundle.js'));
if (runtimeFile) essentialBundles.push('/js/' + runtimeFile);
// 2. Vendor bundles (libraries that main depends on)
const reactFile = jsFiles.find(f => f.startsWith('react.') && f.endsWith('.bundle.js'));
if (reactFile) essentialBundles.push('/js/' + reactFile);
const emotionFile = jsFiles.find(f => f.startsWith('emotion.') && f.endsWith('.bundle.js'));
if (emotionFile) essentialBundles.push('/js/' + emotionFile);
const muiIconsCommonFile = jsFiles.find(f => f.startsWith('mui-icons-common.') && f.endsWith('.bundle.js'));
if (muiIconsCommonFile) essentialBundles.push('/js/' + muiIconsCommonFile);
const muiCoreFile = jsFiles.find(f => f.startsWith('mui-core.') && f.endsWith('.bundle.js'));
if (muiCoreFile) essentialBundles.push('/js/' + muiCoreFile);
const vendorFile = jsFiles.find(f => f.startsWith('vendor.') && f.endsWith('.bundle.js'));
if (vendorFile) essentialBundles.push('/js/' + vendorFile);
// 3. Common shared code
const commonFile = jsFiles.find(f => f.startsWith('common.') && f.endsWith('.chunk.js'));
if (commonFile) essentialBundles.push('/js/' + commonFile);
// 4. Main bundle (your app code - must be last)
const mainFile = jsFiles.find(f => f.startsWith('main.') && f.endsWith('.bundle.js'));
if (mainFile) essentialBundles.push('/js/' + mainFile);
jsBundles = essentialBundles;
}
} catch (error) {
console.warn('Could not read webpack output filesystem:', error.message);
}
// Fallback if we can't read the filesystem
if (jsBundles.length === 0) {
jsBundles = ['/js/runtime.bundle.js', '/js/main.bundle.js'];
}
// Render the page in memory only (no file writing in dev mode)
const ReactDOMServer = require('react-dom/server');
const { StaticRouter } = require('react-router');
const { CacheProvider } = require('@emotion/react');
const { ThemeProvider } = require('@mui/material/styles');
const createEmotionCache = require('./createEmotionCache.js').default;
const theme = require('./src/theme.js').default;
const createEmotionServer = require('@emotion/server/create-instance').default;
// Create fresh Emotion cache for this page
const cache = createEmotionCache();
const { extractCriticalToChunks } = createEmotionServer(cache);
// Wrap with StaticRouter to provide React Router context for Logo's Link components
const routedComponent = React.createElement(
StaticRouter,
{ location: '/404' },
component
);
const pageElement = React.createElement(
CacheProvider,
{ value: cache },
React.createElement(ThemeProvider, { theme: theme }, routedComponent)
);
// Render to string
const renderedMarkup = ReactDOMServer.renderToString(pageElement);
const emotionChunks = extractCriticalToChunks(renderedMarkup);
// Build the full HTML page
const templatePath = path.resolve(__dirname, 'public', 'index.html');
let template = fs.readFileSync(templatePath, 'utf8');
// Add JavaScript bundles
let scriptTags = '';
jsBundles.forEach(jsFile => {
scriptTags += `<script src="${jsFile}"></script>`;
});
// Add global CSS from src/index.css
let globalCss = '';
try {
globalCss = fs.readFileSync(path.resolve(__dirname, 'src', 'index.css'), 'utf8');
// Fix relative font paths for prerendered HTML (remove ../public to make them relative to public root)
globalCss = globalCss.replace(/url\('\.\.\/public/g, "url('");
} catch (error) {
console.warn('Could not read src/index.css:', error.message);
}
// Add inline CSS from emotion
let emotionCss = '';
if (emotionChunks.styles.length > 0) {
emotionChunks.styles.forEach(style => {
if (style.css) {
emotionCss += style.css;
}
});
}
// Combine all CSS
const inlineCss = globalCss + emotionCss;
// Use the rendered markup as-is (no regex replacements)
let processedMarkup = renderedMarkup;
// Replace placeholders in template
const finalHtml = template
.replace('<div id="root"></div>', `<div id="root">${processedMarkup}</div>`)
.replace('</head>', `<style>${inlineCss}</style></head>`)
.replace('</body>', `
<script>
window.__PRERENDER_FALLBACK__ = {path: '/404', content: ${JSON.stringify(processedMarkup)}, timestamp: ${Date.now()}};
</script>
${scriptTags}
</body>`);
// Serve the prerendered HTML with 404 status
res.status(404);
res.setHeader('Content-Type', 'text/html; charset=utf-8');
res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate');
res.setHeader('Pragma', 'no-cache');
res.setHeader('Expires', '0');
return res.send(finalHtml);
// If we get here, prerender failed - let the error bubble up
throw new Error('404 prerender failed completely');
} else {
next();
}
}
});
return middlewares; return middlewares;
}, },
hot: true, hot: true,
@@ -317,7 +530,18 @@ export default {
historyApiFallback: { historyApiFallback: {
index: '/index.html', index: '/index.html',
disableDotRule: true, disableDotRule: true,
htmlAcceptHeaders: ['text/html', 'application/xhtml+xml'] htmlAcceptHeaders: ['text/html', 'application/xhtml+xml'],
rewrites: [
// Exclude prerendered routes from SPA fallback
{ from: /^\/Kategorie\//, to: function(context) {
return context.parsedUrl.pathname;
}},
{ from: /^\/Artikel\//, to: function(context) {
return context.parsedUrl.pathname;
}},
// All other routes should fallback to React SPA
{ from: /^\/(?!api|socket\.io|assets|js|css|favicon\.ico).*$/, to: '/index.html' }
]
}, },
client: { client: {
logging: 'verbose', logging: 'verbose',