Compare commits

..

1 Commits

Author SHA1 Message Date
seb
ccceb8fe78 Add Mollie payment integration to CartTab and OrderProcessingService
- Introduced Mollie payment method in CartTab, allowing users to select it alongside existing payment options.
- Implemented loading and error handling for the Mollie component.
- Updated OrderProcessingService to create Mollie payment intents.
- Adjusted PaymentMethodSelector to switch to Mollie when specific delivery methods are selected.
- Enhanced CartTab to store cart items for Mollie payments and calculate total amounts accordingly.
2025-07-06 02:21:52 +02:00
29 changed files with 1279 additions and 1539 deletions

2
.gitignore vendored
View File

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

View File

@@ -11,99 +11,6 @@ Crawl-delay: 0
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 mapping from German to Google Shopping accepted units
const unitMapping = {
// Volume (German -> Google)
'Milliliter': 'ml',
'milliliter': 'ml',
'ml': 'ml',
'Liter': 'l',
'liter': 'l',
'l': 'l',
'Zentiliter': 'cl',
'zentiliter': 'cl',
'cl': 'cl',
// Weight (German -> Google)
'Gramm': 'g',
'gramm': 'g',
'g': 'g',
'Kilogramm': 'kg',
'kilogramm': 'kg',
'kg': 'kg',
'Milligramm': 'mg',
'milligramm': 'mg',
'mg': 'mg',
// Length (German -> Google)
'Meter': 'm',
'meter': 'm',
'm': 'm',
'Zentimeter': 'cm',
'zentimeter': 'cm',
'cm': 'cm',
// Count (German -> Google)
'Stück': 'ct',
'stück': 'ct',
'Stk': 'ct',
'stk': 'ct',
'ct': 'ct',
'Blatt': 'sheet',
'blatt': 'sheet',
'sheet': 'sheet'
};
// Helper function to convert German unit to Google Shopping unit
const convertUnit = (unit) => {
if (!unit) return null;
const trimmedUnit = unit.trim();
return unitMapping[trimmedUnit] || trimmedUnit.toLowerCase();
};
// 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 = convertUnit(product.cEinheit);
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()) {
const baseUnit = convertUnit(product.cGrundEinheit);
if (baseUnit) {
// Base measure usually needs a quantity (like 100g, 1l, etc.)
// If it's just a unit, we'll add a default quantity
if (baseUnit.match(/^[a-z]+$/)) {
// For weight/volume units, use standard base quantities
if (['g', 'kg', 'mg'].includes(baseUnit)) {
result.unit_pricing_base_measure = baseUnit === 'kg' ? '1 kg' : '100 g';
} else if (['ml', 'l', 'cl'].includes(baseUnit)) {
result.unit_pricing_base_measure = baseUnit === 'l' ? '1 l' : '100 ml';
} else if (['m', 'cm'].includes(baseUnit)) {
result.unit_pricing_base_measure = baseUnit === 'm' ? '1 m' : '100 cm';
} else {
result.unit_pricing_base_measure = `1 ${baseUnit}`;
}
} else {
result.unit_pricing_base_measure = baseUnit;
}
}
}
return result;
};
const generateProductsXml = (allProductsData = [], baseUrl, config) => {
const currentDate = new Date().toISOString();
@@ -116,131 +23,124 @@ const generateProductsXml = (allProductsData = [], baseUrl, config) => {
const getGoogleProductCategory = (categoryId) => {
const categoryMappings = {
// Seeds & Plants
689: "543561", // Seeds (Saatgut)
706: "543561", // Stecklinge (cuttings) ebenfalls Pflanzen/Saatgut
376: "2802", // Grow-Sets Pflanzen- & Kräuteranbausets
689: "Home & Garden > Plants > Seeds",
706: "Home & Garden > Plants", // Stecklinge (cuttings)
376: "Home & Garden > Plants > Plant & Herb Growing Kits", // Grow-Sets
// Headshop & Accessories
709: "4082", // Headshop Rauchzubehör
711: "4082", // Headshop > Bongs Rauchzubehör
714: "4082", // Headshop > Bongs > Zubehör Rauchzubehör
748: "4082", // Headshop > Bongs > Köpfe Rauchzubehör
749: "4082", // Headshop > Bongs > Chillums/Diffusoren/Kupplungen Rauchzubehör
896: "3151", // Headshop > Vaporizer Vaporizer
710: "5109", // Headshop > Grinder Gewürzmühlen (Küchenhelfer)
709: "Arts & Entertainment > Hobbies & Creative Arts", // Headshop
711: "Arts & Entertainment > Hobbies & Creative Arts", // Bongs
714: "Arts & Entertainment > Hobbies & Creative Arts", // Zubehör
748: "Arts & Entertainment > Hobbies & Creative Arts", // Köpfe
749: "Arts & Entertainment > Hobbies & Creative Arts", // Chillums / Diffusoren / Kupplungen
896: "Electronics > Electronics Accessories", // Vaporizer
710: "Home & Garden > Kitchen & Dining > Kitchen Tools & Utensils", // Grinder
// Measuring & Packaging
186: "5631", // Headshop > Wiegen & Verpacken Aufbewahrung/Zubehör
187: "4767", // Headshop > Waagen Personenwaagen (Medizinisch)
346: "7118", // Headshop > Vakuumbeutel Vakuumierer-Beutel
355: "606", // Headshop > Boveda & Integra Boost Luftentfeuchter (nächstmögliche)
407: "3561", // Headshop > Grove Bags Aufbewahrungsbehälter
449: "1496", // Headshop > Cliptütchen Lebensmittelverpackungsmaterial
539: "3110", // Headshop > Gläser & Dosen Lebensmittelbehälter
186: "Business & Industrial", // Wiegen & Verpacken
187: "Business & Industrial > Science & Laboratory > Lab Equipment", // Waagen
346: "Home & Garden > Kitchen & Dining > Food Storage", // Vakuumbeutel
355: "Home & Garden > Kitchen & Dining > Food Storage", // Boveda & Integra Boost
407: "Home & Garden > Kitchen & Dining > Food Storage", // Grove Bags
449: "Home & Garden > Kitchen & Dining > Food Storage", // Cliptütchen
539: "Home & Garden > Kitchen & Dining > Food Storage", // Gläser & Dosen
// Lighting & Equipment
694: "3006", // Lampen Lampen (Beleuchtung)
261: "3006", // Zubehör > Lampenzubehör Lampen
694: "Home & Garden > Lighting", // Lampen
261: "Home & Garden > Lighting", // Lampenzubehör
// Plants & Growing
691: "500033", // Dünger Dünger
692: "5633", // Zubehör > Dünger-Zubehör Zubehör für Gartenarbeit
693: "5655", // Zelte Zelte
691: "Home & Garden > Lawn & Garden > Fertilizers", // Dünger
692: "Home & Garden > Lawn & Garden > Fertilizers", // Dünger - Zubehör
693: "Sporting Goods > Outdoor Recreation > Camping & Hiking > Tents", // Zelte
// Pots & Containers
219: "113", // Töpfe Blumentöpfe & Pflanzgefäße
220: "3173", // Töpfe > Untersetzer Gartentopfuntersetzer und Trays
301: "113", // Töpfe > Stofftöpfe (Blumentöpfe/Pflanzgefäße)
317: "113", // Töpfe > Air-Pot (Blumentöpfe/Pflanzgefäße)
364: "113", // Töpfe > Kunststofftöpfe (Blumentöpfe/Pflanzgefäße)
292: "3568", // Bewässerung > Trays & Fluttische Bewässerungssysteme
219: "Home & Garden > Decor > Planters & Pots", // Töpfe
220: "Home & Garden > Decor > Planters & Pots", // Untersetzer
301: "Home & Garden > Decor > Planters & Pots", // Stofftöpfe
317: "Home & Garden > Decor > Planters & Pots", // Air-Pot
364: "Home & Garden > Decor > Planters & Pots", // Kunststofftöpfe
292: "Home & Garden > Decor > Planters & Pots", // Trays & Fluttische
// Ventilation & Climate
703: "2802", // Grow-Sets > Abluft-Sets (verwendet Pflanzen-Kräuter-Anbausets)
247: "1700", // Belüftung Ventilatoren (Klimatisierung)
214: "1700", // Belüftung > Umluft-Ventilatoren Ventilatoren
308: "1700", // Belüftung > Ab- und Zuluft Ventilatoren
609: "1700", // Belüftung > Ab- und Zuluft > Schalldämpfer Ventilatoren
248: "1700", // Belüftung > Aktivkohlefilter Ventilatoren (nächstmögliche)
392: "1700", // Belüftung > Ab- und Zuluft > Zuluftfilter Ventilatoren
658: "606", // Belüftung > Luftbe- und -entfeuchter Luftentfeuchter
310: "2802", // Anzucht > Heizmatten Pflanzen- & Kräuteranbausets
379: "5631", // Belüftung > Geruchsneutralisation Haushaltsbedarf: Aufbewahrung
703: "Home & Garden > Outdoor Power Tools", // Abluft-Sets
247: "Home & Garden > Outdoor Power Tools", // Belüftung
214: "Home & Garden > Outdoor Power Tools", // Umluft-Ventilatoren
308: "Home & Garden > Outdoor Power Tools", // Ab- und Zuluft
609: "Home & Garden > Outdoor Power Tools", // Schalldämpfer
248: "Home & Garden > Pool & Spa > Pool & Spa Filters", // Aktivkohlefilter
392: "Home & Garden > Pool & Spa > Pool & Spa Filters", // Zuluftfilter
658: "Home & Garden > Climate Control > Dehumidifiers", // Luftbe- und entfeuchter
310: "Home & Garden > Climate Control > Heating", // Heizmatten
379: "Home & Garden > Household Supplies > Air Fresheners", // Geruchsneutralisation
// Irrigation & Watering
221: "3568", // Bewässerung Bewässerungssysteme (Gesamt)
250: "6318", // Bewässerung > Schläuche Gartenschläuche
297: "500100", // Bewässerung > Pumpen Bewässerung-/Sprinklerpumpen
354: "3780", // Bewässerung > Sprüher Sprinkler & Sprühköpfe
372: "3568", // Bewässerung > AutoPot Bewässerungssysteme
389: "3568", // Bewässerung > Blumat Bewässerungssysteme
405: "6318", // Bewässerung > Schläuche Gartenschläuche
425: "3568", // Bewässerung > Wassertanks Bewässerungssysteme
480: "3568", // Bewässerung > Tropfer Bewässerungssysteme
519: "3568", // Bewässerung > Pumpsprüher Bewässerungssysteme
221: "Home & Garden > Lawn & Garden > Watering Equipment", // Bewässerung
250: "Home & Garden > Lawn & Garden > Watering Equipment", // Schläuche
297: "Home & Garden > Lawn & Garden > Watering Equipment", // Pumpen
354: "Home & Garden > Lawn & Garden > Watering Equipment", // Sprüher
372: "Home & Garden > Lawn & Garden > Watering Equipment", // AutoPot
389: "Home & Garden > Lawn & Garden > Watering Equipment", // Blumat
405: "Home & Garden > Lawn & Garden > Watering Equipment", // Schläuche
425: "Home & Garden > Lawn & Garden > Watering Equipment", // Wassertanks
480: "Home & Garden > Lawn & Garden > Watering Equipment", // Tropfer
519: "Home & Garden > Lawn & Garden > Watering Equipment", // Pumpsprüher
// Growing Media & Soils
242: "543677", // Böden Gartenerde
243: "543677", // Böden > Erde Gartenerde
269: "543677", // Böden > Kokos Gartenerde
580: "543677", // Böden > Perlite & Blähton Gartenerde
242: "Home & Garden > Lawn & Garden > Fertilizers", // Böden
243: "Home & Garden > Lawn & Garden > Fertilizers", // Erde
269: "Home & Garden > Lawn & Garden > Fertilizers", // Kokos
580: "Home & Garden > Lawn & Garden > Fertilizers", // Perlite & Blähton
// Propagation & Starting
286: "2802", // Anzucht Pflanzen- & Kräuteranbausets
298: "2802", // Anzucht > Steinwolltrays Pflanzen- & Kräuteranbausets
421: "2802", // Anzucht > Vermehrungszubehör Pflanzen- & Kräuteranbausets
489: "2802", // Anzucht > EazyPlug & Jiffy Pflanzen- & Kräuteranbausets
359: "3103", // Anzucht > Gewächshäuser Gewächshäuser
286: "Home & Garden > Plants", // Anzucht
298: "Home & Garden > Plants", // Steinwolltrays
421: "Home & Garden > Plants", // Vermehrungszubehör
489: "Home & Garden > Plants", // EazyPlug & Jiffy
359: "Home & Garden > Outdoor Structures > Greenhouses", // Gewächshäuser
// Tools & Equipment
373: "3568", // Bewässerung > GrowTool Bewässerungssysteme
403: "3999", // Bewässerung > Messbecher & mehr Messbecher & Dosierlöffel
259: "756", // Zubehör > Ernte & Verarbeitung > Pressen Nudelmaschinen
280: "2948", // Zubehör > Ernte & Verarbeitung > Erntescheeren Küchenmesser
258: "684", // Zubehör > Ernte & Verarbeitung Abfallzerkleinerer
278: "5057", // Zubehör > Ernte & Verarbeitung > Extraktion Slush-Eis-Maschinen
302: "7332", // Zubehör > Ernte & Verarbeitung > Erntemaschinen Gartenmaschinen
373: "Home & Garden > Tools > Hand Tools", // GrowTool
403: "Home & Garden > Kitchen & Dining > Kitchen Tools & Utensils", // Messbecher & mehr
259: "Home & Garden > Tools > Hand Tools", // Pressen
280: "Home & Garden > Tools > Hand Tools", // Erntescheeren
258: "Home & Garden > Tools", // Ernte & Verarbeitung
278: "Home & Garden > Tools", // Extraktion
302: "Home & Garden > Tools", // Erntemaschinen
// Hardware & Plumbing
222: "3568", // Bewässerung > PE-Teile Bewässerungssysteme
374: "1700", // Belüftung > Ab- und Zuluft > Verbindungsteile Ventilatoren
222: "Hardware > Plumbing", // PE-Teile
374: "Hardware > Plumbing > Plumbing Fittings", // Verbindungsteile
// Electronics & Control
314: "1700", // Belüftung > Steuergeräte Ventilatoren
408: "1700", // Belüftung > Steuergeräte > GrowControl Ventilatoren
344: "1207", // Zubehör > Messgeräte Messwerkzeuge & Messwertgeber
555: "4555", // Zubehör > Anbauzubehör > Mikroskope Mikroskope
314: "Electronics > Electronics Accessories", // Steuergeräte
408: "Electronics > Electronics Accessories", // GrowControl
344: "Business & Industrial > Science & Laboratory > Lab Equipment", // Messgeräte
555: "Business & Industrial > Science & Laboratory > Lab Equipment > Microscopes", // Mikroskope
// Camping & Outdoor
226: "5655", // Zubehör > Zeltzubehör Zelte
226: "Sporting Goods > Outdoor Recreation > Camping & Hiking", // Zeltzubehör
// Plant Care & Protection
239: "4085", // Zubehör > Anbauzubehör > Pflanzenschutz Herbizide
240: "5633", // Zubehör > Anbauzubehör Zubehör für Gartenarbeit
239: "Home & Garden > Lawn & Garden > Pest Control", // Pflanzenschutz
240: "Home & Garden > Plants", // Anbauzubehör
// Office & Media
424: "4377", // Zubehör > Anbauzubehör > Etiketten & Schilder Etiketten & Anhängerschilder
387: "543541", // Zubehör > Anbauzubehör > Literatur Bücher
424: "Office Supplies > Labels", // Etiketten & Schilder
387: "Media > Books", // Literatur
// General categories
705: "2802", // Grow-Sets > Set-Konfigurator (ebenfalls Pflanzen-Anbausets)
686: "1700", // Belüftung > Aktivkohlefilter > Zubehör Ventilatoren
741: "1700", // Belüftung > Ab- und Zuluft > Zubehör Ventilatoren
294: "3568", // Bewässerung > Zubehör Bewässerungssysteme
695: "5631", // Zubehör Haushaltsbedarf: Aufbewahrung
293: "5631", // Zubehör > Ernte & Verarbeitung > Trockennetze Haushaltsbedarf: Aufbewahrung
4: "5631", // Zubehör > Anbauzubehör > Sonstiges Haushaltsbedarf: Aufbewahrung
450: "5631", // Zubehör > Anbauzubehör > Restposten Haushaltsbedarf: Aufbewahrung
705: "Home & Garden", // Set-Konfigurator
686: "Home & Garden", // Zubehör
741: "Home & Garden", // Zubehör
294: "Home & Garden", // Zubehör
695: "Home & Garden", // Zubehör
293: "Home & Garden", // Trockennetze
4: "Home & Garden", // Sonstiges
450: "Home & Garden", // Restposten
};
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;
return categoryMappings[categoryId] || "Home & Garden > Plants";
};
let productsXml = `<?xml version="1.0" encoding="UTF-8"?>
@@ -250,7 +150,7 @@ const generateProductsXml = (allProductsData = [], baseUrl, config) => {
<link>${baseUrl}</link>
<description>${config.descriptions.short}</description>
<lastBuildDate>${currentDate}</lastBuildDate>
<language>de-DE</language>`;
<language>${config.language}</language>`;
// Helper function to clean text content of problematic characters
const cleanTextContent = (text) => {
@@ -418,17 +318,6 @@ const generateProductsXml = (allProductsData = [], baseUrl, config) => {
<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 += `
</item>`;

Binary file not shown.

Before

Width:  |  Height:  |  Size: 208 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 242 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 KiB

View File

@@ -18,14 +18,13 @@ import BugReportIcon from "@mui/icons-material/BugReport";
import SocketProvider from "./providers/SocketProvider.js";
import SocketContext from "./contexts/SocketContext.js";
import { CarouselProvider } from "./contexts/CarouselContext.js";
import config from "./config.js";
import ScrollToTop from "./components/ScrollToTop.js";
//import TelemetryService from './services/telemetryService.js';
import Header from "./components/Header.js";
import Footer from "./components/Footer.js";
import MainPageLayout from "./components/MainPageLayout.js";
import Home from "./pages/Home.js";
// Lazy load all route components to reduce initial bundle size
const Content = lazy(() => import(/* webpackChunkName: "content" */ "./components/Content.js"));
@@ -51,10 +50,6 @@ const Widerrufsrecht = lazy(() => import(/* webpackChunkName: "legal" */ "./page
const GrowTentKonfigurator = lazy(() => import(/* webpackChunkName: "konfigurator" */ "./pages/GrowTentKonfigurator.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 defaultTheme from "./theme.js";
// Lazy load theme customizer for development only
@@ -200,12 +195,9 @@ const AppContent = ({ currentTheme, onThemeChange }) => {
<CircularProgress color="primary" />
</Box>
}>
<CarouselProvider>
<Routes>
{/* Main pages using unified component */}
<Route path="/" element={<MainPageLayout />} />
<Route path="/aktionen" element={<MainPageLayout />} />
<Route path="/filiale" element={<MainPageLayout />} />
{/* Home page with text only */}
<Route path="/" element={<Home />} />
{/* Category page - Render Content in parallel */}
<Route
@@ -254,14 +246,9 @@ const AppContent = ({ currentTheme, onThemeChange }) => {
{/* Grow Tent Configurator */}
<Route path="/Konfigurator" element={<GrowTentKonfigurator />} />
{/* Separate pages that are truly different */}
<Route path="/presseverleih" element={<PresseverleihPage />} />
<Route path="/thc-test" element={<ThcTestPage />} />
{/* Fallback for undefined routes */}
<Route path="*" element={<Navigate to="/" replace />} />
</Routes>
</CarouselProvider>
</Suspense>
</Box>
{/* Conditionally render the Chat Assistant */}

View File

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

View File

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

View File

@@ -1,92 +0,0 @@
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,8 +51,6 @@ class AddToCartButton extends Component {
seoName: this.props.seoName,
pictureList: this.props.pictureList,
price: this.props.price,
fGrundPreis: this.props.fGrundPreis,
cGrundEinheit: this.props.cGrundEinheit,
quantity: 1,
weight: this.props.weight,
vat: this.props.vat,

View File

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

View File

@@ -1,349 +0,0 @@
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/Seeds"
},
{
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;

381
src/components/Mollie.js Normal file
View File

@@ -0,0 +1,381 @@
import React, { Component, useState } from "react";
import { Button, Box, Typography, CircularProgress } from "@mui/material";
import config from "../config.js";
// Function to lazy load Mollie script
const loadMollie = () => {
return new Promise((resolve, reject) => {
// Check if Mollie is already loaded
if (window.Mollie) {
resolve(window.Mollie);
return;
}
// Create script element
const script = document.createElement('script');
script.src = 'https://js.mollie.com/v1/mollie.js';
script.async = true;
script.onload = () => {
if (window.Mollie) {
resolve(window.Mollie);
} else {
reject(new Error('Mollie failed to load'));
}
};
script.onerror = () => {
reject(new Error('Failed to load Mollie script'));
};
document.head.appendChild(script);
});
};
const CheckoutForm = ({ mollie }) => {
const [errorMessage, setErrorMessage] = useState(null);
const [isSubmitting, setIsSubmitting] = useState(false);
React.useEffect(() => {
if (!mollie) return;
let mountedComponents = {
cardNumber: null,
cardHolder: null,
expiryDate: null,
verificationCode: null
};
try {
// Create Mollie components
const cardNumber = mollie.createComponent('cardNumber');
const cardHolder = mollie.createComponent('cardHolder');
const expiryDate = mollie.createComponent('expiryDate');
const verificationCode = mollie.createComponent('verificationCode');
// Store references for cleanup
mountedComponents = {
cardNumber,
cardHolder,
expiryDate,
verificationCode
};
// Mount components
cardNumber.mount('#card-number');
cardHolder.mount('#card-holder');
expiryDate.mount('#expiry-date');
verificationCode.mount('#verification-code');
// Set up error handling
cardNumber.addEventListener('change', event => {
const errorElement = document.querySelector('#card-number-error');
if (errorElement) {
if (event.error && event.touched) {
errorElement.textContent = event.error;
} else {
errorElement.textContent = '';
}
}
});
cardHolder.addEventListener('change', event => {
const errorElement = document.querySelector('#card-holder-error');
if (errorElement) {
if (event.error && event.touched) {
errorElement.textContent = event.error;
} else {
errorElement.textContent = '';
}
}
});
expiryDate.addEventListener('change', event => {
const errorElement = document.querySelector('#expiry-date-error');
if (errorElement) {
if (event.error && event.touched) {
errorElement.textContent = event.error;
} else {
errorElement.textContent = '';
}
}
});
verificationCode.addEventListener('change', event => {
const errorElement = document.querySelector('#verification-code-error');
if (errorElement) {
if (event.error && event.touched) {
errorElement.textContent = event.error;
} else {
errorElement.textContent = '';
}
}
});
// Components are now mounted and ready
} catch (error) {
console.error('Error creating Mollie components:', error);
setErrorMessage('Failed to initialize payment form. Please try again.');
}
// Cleanup function
return () => {
try {
if (mountedComponents.cardNumber) mountedComponents.cardNumber.unmount();
if (mountedComponents.cardHolder) mountedComponents.cardHolder.unmount();
if (mountedComponents.expiryDate) mountedComponents.expiryDate.unmount();
if (mountedComponents.verificationCode) mountedComponents.verificationCode.unmount();
} catch (error) {
console.error('Error cleaning up Mollie components:', error);
}
};
}, [mollie]);
const handleSubmit = async (event) => {
event.preventDefault();
if (!mollie || isSubmitting) {
return;
}
setIsSubmitting(true);
setErrorMessage(null);
try {
const { token, error } = await mollie.createToken();
if (error) {
setErrorMessage(error.message || 'Payment failed. Please try again.');
setIsSubmitting(false);
return;
}
if (token) {
// Handle successful token creation
// Create a payment completion event similar to Stripe
const mollieCompletionData = {
mollieToken: token,
paymentMethod: 'mollie'
};
// Dispatch a custom event to notify the parent component
const completionEvent = new CustomEvent('molliePaymentComplete', {
detail: mollieCompletionData
});
window.dispatchEvent(completionEvent);
// For now, redirect to profile with completion data
const returnUrl = `${window.location.origin}/profile?complete&mollie_token=${token}`;
window.location.href = returnUrl;
}
} catch (error) {
console.error('Error creating Mollie token:', error);
setErrorMessage('Payment failed. Please try again.');
setIsSubmitting(false);
}
};
return (
<Box sx={{ maxWidth: 600, mx: 'auto', p: 3 }}>
<Typography variant="h6" gutterBottom>
Kreditkarte oder Sofortüberweisung
</Typography>
<form onSubmit={handleSubmit}>
<Box sx={{ mb: 3 }}>
<Typography variant="body2" gutterBottom>
Kartennummer
</Typography>
<Box
id="card-number"
sx={{
p: 2,
border: '1px solid #e0e0e0',
borderRadius: 1,
minHeight: 40,
backgroundColor: '#fff'
}}
/>
<Typography
id="card-number-error"
variant="caption"
sx={{ color: 'error.main', minHeight: 16, display: 'block' }}
/>
</Box>
<Box sx={{ mb: 3 }}>
<Typography variant="body2" gutterBottom>
Karteninhaber
</Typography>
<Box
id="card-holder"
sx={{
p: 2,
border: '1px solid #e0e0e0',
borderRadius: 1,
minHeight: 40,
backgroundColor: '#fff'
}}
/>
<Typography
id="card-holder-error"
variant="caption"
sx={{ color: 'error.main', minHeight: 16, display: 'block' }}
/>
</Box>
<Box sx={{ display: 'flex', gap: 2, mb: 3 }}>
<Box sx={{ flex: 1 }}>
<Typography variant="body2" gutterBottom>
Ablaufdatum
</Typography>
<Box
id="expiry-date"
sx={{
p: 2,
border: '1px solid #e0e0e0',
borderRadius: 1,
minHeight: 40,
backgroundColor: '#fff'
}}
/>
<Typography
id="expiry-date-error"
variant="caption"
sx={{ color: 'error.main', minHeight: 16, display: 'block' }}
/>
</Box>
<Box sx={{ flex: 1 }}>
<Typography variant="body2" gutterBottom>
Sicherheitscode
</Typography>
<Box
id="verification-code"
sx={{
p: 2,
border: '1px solid #e0e0e0',
borderRadius: 1,
minHeight: 40,
backgroundColor: '#fff'
}}
/>
<Typography
id="verification-code-error"
variant="caption"
sx={{ color: 'error.main', minHeight: 16, display: 'block' }}
/>
</Box>
</Box>
<Button
variant="contained"
disabled={!mollie || isSubmitting}
type="submit"
fullWidth
sx={{
mt: 2,
backgroundColor: '#2e7d32',
'&:hover': {
backgroundColor: '#1b5e20'
}
}}
>
{isSubmitting ? (
<>
<CircularProgress size={20} sx={{ mr: 1, color: 'white' }} />
Verarbeitung...
</>
) : (
'Bezahlung Abschließen'
)}
</Button>
{errorMessage && (
<Typography
variant="body2"
sx={{ color: 'error.main', mt: 2, textAlign: 'center' }}
>
{errorMessage}
</Typography>
)}
</form>
</Box>
);
};
class Mollie extends Component {
constructor(props) {
super(props);
this.state = {
mollie: null,
loading: true,
error: null,
};
this.molliePromise = loadMollie();
}
componentDidMount() {
this.molliePromise
.then((MollieClass) => {
try {
// Initialize Mollie with profile key
const mollie = MollieClass(config.mollieProfileKey, {
locale: 'de_DE',
testmode: true // Set to false for production
});
this.setState({ mollie, loading: false });
} catch (error) {
console.error('Error initializing Mollie:', error);
this.setState({
error: 'Failed to initialize payment system. Please try again.',
loading: false
});
}
})
.catch((error) => {
console.error('Error loading Mollie:', error);
this.setState({
error: 'Failed to load payment system. Please try again.',
loading: false
});
});
}
render() {
const { mollie, loading, error } = this.state;
if (loading) {
return (
<Box sx={{ textAlign: 'center', py: 4 }}>
<CircularProgress sx={{ color: '#2e7d32' }} />
<Typography variant="body1" sx={{ mt: 2 }}>
Zahlungskomponente wird geladen...
</Typography>
</Box>
);
}
if (error) {
return (
<Box sx={{ textAlign: 'center', py: 4 }}>
<Typography variant="body1" sx={{ color: 'error.main' }}>
{error}
</Typography>
<Button
variant="outlined"
onClick={() => window.location.reload()}
sx={{ mt: 2 }}
>
Seite neu laden
</Button>
</Box>
);
}
return <CheckoutForm mollie={mollie} />;
}
}
export default Mollie;

View File

@@ -68,7 +68,7 @@ class Product extends Component {
render() {
const {
id, name, price, available, manufacturer, seoName,
currency, vat, cGrundEinheit, fGrundPreis, thc,
currency, vat, massMenge, massEinheit, thc,
floweringWeeks,incoming, neu, weight, versandklasse, availableSupplier
} = this.props;
@@ -341,8 +341,8 @@ class Product extends Component {
</Typography>
{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(fGrundPreis)}/{cGrundEinheit})
{massMenge != 1 && massEinheit && (<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})
</Typography> )}
</div>
{/*incoming*/}
@@ -358,7 +358,7 @@ class Product extends Component {
>
<ZoomInIcon />
</IconButton>
<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}/>
<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}/>
</Box>
</Card>
</Box>

View File

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

View File

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

View File

@@ -1,229 +0,0 @@
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,9 +8,7 @@ import ListItem from "@mui/material/ListItem";
import ListItemText from "@mui/material/ListItemText";
import Typography from "@mui/material/Typography";
import CircularProgress from "@mui/material/CircularProgress";
import IconButton from "@mui/material/IconButton";
import SearchIcon from "@mui/icons-material/Search";
import KeyboardReturnIcon from "@mui/icons-material/KeyboardReturn";
import { useNavigate, useLocation } from "react-router-dom";
import SocketContext from "../../contexts/SocketContext.js";
@@ -186,15 +184,6 @@ const SearchBar = () => {
}, 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
React.useEffect(() => {
return () => {
@@ -255,23 +244,9 @@ const SearchBar = () => {
<SearchIcon />
</InputAdornment>
),
endAdornment: (
endAdornment: loadingSuggestions && (
<InputAdornment position="end">
{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>
<CircularProgress size={16} />
</InputAdornment>
),
sx: { borderRadius: 2, bgcolor: "background.paper" },

View File

@@ -51,6 +51,9 @@ class CartTab extends Component {
showStripePayment: false,
StripeComponent: null,
isLoadingStripe: false,
showMolliePayment: false,
MollieComponent: null,
isLoadingMollie: false,
showPaymentConfirmation: false,
orderCompleted: false,
originalCartItems: []
@@ -116,7 +119,7 @@ class CartTab extends Component {
// Determine payment method - respect constraints
let prefillPaymentMethod = template.payment_method || "wire";
const paymentMethodMap = {
"credit_card": "stripe",
"credit_card": "mollie",//stripe
"bank_transfer": "wire",
"cash_on_delivery": "onDelivery",
"cash": "cash"
@@ -319,6 +322,27 @@ class CartTab extends Component {
}
};
loadMollieComponent = async () => {
this.setState({ isLoadingMollie: true });
try {
const { default: Mollie } = await import("../Mollie.js");
this.setState({
MollieComponent: Mollie,
showMolliePayment: true,
isCompletingOrder: false,
isLoadingMollie: false,
});
} catch (error) {
console.error("Failed to load Mollie component:", error);
this.setState({
isCompletingOrder: false,
isLoadingMollie: false,
completionError: "Failed to load payment component. Please try again.",
});
}
};
handleCompleteOrder = () => {
this.setState({ completionError: null }); // Clear previous errors
@@ -363,6 +387,25 @@ class CartTab extends Component {
this.orderService.createStripeIntent(totalAmount, this.loadStripeComponent);
return;
}
// Handle Mollie payment differently
if (paymentMethod === "mollie") {
// Store the cart items used for Mollie payment in sessionStorage for later reference
try {
sessionStorage.setItem('molliePaymentCart', JSON.stringify(cartItems));
} catch (error) {
console.error("Failed to store Mollie payment cart:", error);
}
// Calculate total amount for Mollie
const subtotal = cartItems.reduce(
(total, item) => total + item.price * item.quantity,
0
);
const totalAmount = Math.round((subtotal + deliveryCost) * 100); // Convert to cents
this.orderService.createMollieIntent(totalAmount, this.loadMollieComponent);
return;
}
// Handle regular orders
const orderData = {
@@ -398,6 +441,9 @@ class CartTab extends Component {
showStripePayment,
StripeComponent,
isLoadingStripe,
showMolliePayment,
MollieComponent,
isLoadingMollie,
showPaymentConfirmation,
orderCompleted,
} = this.state;
@@ -434,7 +480,7 @@ class CartTab extends Component {
<CartDropdown
cartItems={cartItems}
socket={this.context.socket}
showDetailedSummary={showStripePayment}
showDetailedSummary={showStripePayment || showMolliePayment}
deliveryMethod={deliveryMethod}
deliveryCost={deliveryCost}
/>
@@ -442,7 +488,7 @@ class CartTab extends Component {
{cartItems.length > 0 && (
<Box sx={{ mt: 3 }}>
{isLoadingStripe ? (
{isLoadingStripe || isLoadingMollie ? (
<Box sx={{ textAlign: 'center', py: 4 }}>
<Typography variant="body1">
Zahlungskomponente wird geladen...
@@ -468,6 +514,26 @@ class CartTab extends Component {
</Box>
<StripeComponent clientSecret={stripeClientSecret} />
</>
) : showMolliePayment && MollieComponent ? (
<>
<Box sx={{ mb: 2 }}>
<Button
variant="outlined"
onClick={() => this.setState({ showMolliePayment: false })}
sx={{
color: '#2e7d32',
borderColor: '#2e7d32',
'&:hover': {
backgroundColor: 'rgba(46, 125, 50, 0.04)',
borderColor: '#1b5e20'
}
}}
>
Zurück zur Bestellung
</Button>
</Box>
<MollieComponent />
</>
) : (
<CheckoutForm
paymentMethod={paymentMethod}

View File

@@ -270,6 +270,10 @@ class OrderProcessingService {
});
}
}
// Create Mollie payment intent
createMollieIntent(totalAmount, loadMollieComponent) {
loadMollieComponent();
}
// Calculate delivery cost
getDeliveryCost() {

View File

@@ -24,7 +24,7 @@ const PaymentMethodSelector = ({ paymentMethod, onChange, deliveryMethod, onDeli
// Handle delivery method changes - auto-switch to stripe when DHL/DPD is selected
useEffect(() => {
if ((deliveryMethod === "DHL" || deliveryMethod === "DPD") && paymentMethod === "cash") {
handlePaymentMethodChange({ target: { value: "stripe" } });
handlePaymentMethodChange({ target: { value: "mollie" } });
}
}, [deliveryMethod, paymentMethod, handlePaymentMethodChange]);
@@ -42,8 +42,22 @@ const PaymentMethodSelector = ({ paymentMethod, onChange, deliveryMethod, onDeli
description: "Bezahlen Sie per Banküberweisung",
disabled: totalAmount === 0,
},
{
/*{
id: "stripe",
name: "Karte oder Sofortüberweisung (Stripe)",
description: totalAmount < 0.50 && totalAmount > 0
? "Bezahlen Sie per Karte oder Sofortüberweisung (Mindestbetrag: 0,50 €)"
: "Bezahlen Sie per Karte oder Sofortüberweisung",
disabled: totalAmount < 0.50 || (deliveryMethod !== "DHL" && deliveryMethod !== "DPD" && deliveryMethod !== "Abholung"),
icons: [
"/assets/images/giropay.png",
"/assets/images/maestro.png",
"/assets/images/mastercard.png",
"/assets/images/visa_electron.png",
],
},*/
{
id: "mollie",
name: "Karte oder Sofortüberweisung",
description: totalAmount < 0.50 && totalAmount > 0
? "Bezahlen Sie per Karte oder Sofortüberweisung (Mindestbetrag: 0,50 €)"

View File

@@ -3,6 +3,7 @@ const config = {
apiBaseUrl: "",
googleClientId: "928121624463-jbgfdlgem22scs1k9c87ucg4ffvaik6o.apps.googleusercontent.com",
stripePublishableKey: "pk_test_51R7lltRtpe3h1vwJzIrDb5bcEigTLBHrtqj9SiPX7FOEATSuD6oJmKc8xpNp49ShpGJZb2GShHIUqj4zlSIz4olj00ipOuOAnu",
mollieProfileKey: "pfl_AtcRTimCff",
// SEO and Business Information
siteName: "Growheads.de",

View File

@@ -1,225 +0,0 @@
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;

652
src/pages/Home.js Normal file
View File

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

View File

@@ -1,46 +0,0 @@
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;

View File

@@ -1,46 +0,0 @@
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,219 +309,6 @@ export default {
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;
},
hot: true,
@@ -530,18 +317,7 @@ export default {
historyApiFallback: {
index: '/index.html',
disableDotRule: true,
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' }
]
htmlAcceptHeaders: ['text/html', 'application/xhtml+xml']
},
client: {
logging: 'verbose',