Compare commits
12 Commits
mollie
...
9000b28ce5
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9000b28ce5 | ||
|
|
8f2253f155 | ||
|
|
b33ece2875 | ||
|
|
02aff1e456 | ||
|
|
9e14827c91 | ||
|
|
8698816875 | ||
|
|
987de641e4 | ||
|
|
23e1742e40 | ||
|
|
205558d06c | ||
|
|
046979a64d | ||
|
|
161e377de4 | ||
|
|
73a88f508b |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -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
|
||||||
@@ -11,6 +11,99 @@ 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 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 generateProductsXml = (allProductsData = [], baseUrl, config) => {
|
||||||
const currentDate = new Date().toISOString();
|
const currentDate = new Date().toISOString();
|
||||||
|
|
||||||
@@ -23,124 +116,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 +250,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 +418,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.
Binary file not shown.
|
Before Width: | Height: | Size: 1.9 MiB After Width: | Height: | Size: 85 KiB |
BIN
public/assets/images/filiale1.jpg
Normal file
BIN
public/assets/images/filiale1.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 208 KiB |
BIN
public/assets/images/filiale2.jpg
Normal file
BIN
public/assets/images/filiale2.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 242 KiB |
BIN
public/assets/images/presse.jpg
Normal file
BIN
public/assets/images/presse.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 71 KiB |
BIN
public/assets/images/purpl.jpg
Normal file
BIN
public/assets/images/purpl.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 68 KiB |
103
src/App.js
103
src/App.js
@@ -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 socket={socket} socketB={socketB} />} />
|
||||||
|
|
||||||
{/* 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 */}
|
||||||
|
|||||||
@@ -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/>
|
||||||
|
|||||||
@@ -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
92
src/PrerenderNotFound.js
Normal 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 };
|
||||||
@@ -51,11 +51,14 @@ 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,
|
||||||
versandklasse: this.props.versandklasse,
|
versandklasse: this.props.versandklasse,
|
||||||
availableSupplier: this.props.availableSupplier,
|
availableSupplier: this.props.availableSupplier,
|
||||||
|
komponenten: this.props.komponenten,
|
||||||
available: this.props.available
|
available: this.props.available
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -150,7 +150,7 @@ class CartItem extends Component {
|
|||||||
item.available == 1 ? "Lieferzeit: 2-3 Tage" :
|
item.available == 1 ? "Lieferzeit: 2-3 Tage" :
|
||||||
item.availableSupplier == 1 ? "Lieferzeit: 7-9 Tage" : ""}
|
item.availableSupplier == 1 ? "Lieferzeit: 7-9 Tage" : ""}
|
||||||
</Typography>
|
</Typography>
|
||||||
<AddToCartButton available={1} id={this.props.id} availableSupplier={item.availableSupplier} price={item.price} seoName={item.seoName} name={item.name} weight={item.weight} vat={item.vat} versandklasse={item.versandklasse}/>
|
<AddToCartButton available={1} id={this.props.id} komponenten={item.komponenten} availableSupplier={item.availableSupplier} price={item.price} seoName={item.seoName} name={item.name} weight={item.weight} vat={item.vat} versandklasse={item.versandklasse}/>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
348
src/components/MainPageLayout.js
Normal file
348
src/components/MainPageLayout.js
Normal file
@@ -0,0 +1,348 @@
|
|||||||
|
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 };
|
||||||
|
};
|
||||||
|
|
||||||
|
const allTitles = {
|
||||||
|
home: "ine annabis eeds & uttings",
|
||||||
|
aktionen: "tuelle ktionen & gebote",
|
||||||
|
filiale: "nsere iliale in resden"
|
||||||
|
};
|
||||||
|
|
||||||
|
// Define all content boxes for layered rendering
|
||||||
|
const allContentBoxes = {
|
||||||
|
home: [
|
||||||
|
{
|
||||||
|
title: "Seeds",
|
||||||
|
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;
|
||||||
@@ -1,381 +0,0 @@
|
|||||||
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;
|
|
||||||
@@ -68,8 +68,8 @@ 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, komponenten
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
const isNew = neu && (new Date().getTime() - new Date(neu).getTime() < 30 * 24 * 60 * 60 * 1000);
|
const isNew = neu && (new Date().getTime() - new Date(neu).getTime() < 30 * 24 * 60 * 60 * 1000);
|
||||||
@@ -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} komponenten={komponenten} 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>
|
||||||
|
|||||||
@@ -29,6 +29,12 @@ class ProductDetailPage extends Component {
|
|||||||
attributes: [],
|
attributes: [],
|
||||||
isSteckling: false,
|
isSteckling: false,
|
||||||
imageDialogOpen: false,
|
imageDialogOpen: false,
|
||||||
|
komponenten: [],
|
||||||
|
komponentenLoaded: false,
|
||||||
|
komponentenData: {}, // Store individual komponent data with loading states
|
||||||
|
komponentenImages: {}, // Store tiny pictures for komponenten
|
||||||
|
totalKomponentenPrice: 0,
|
||||||
|
totalSavings: 0
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
this.state = {
|
this.state = {
|
||||||
@@ -39,6 +45,12 @@ class ProductDetailPage extends Component {
|
|||||||
attributes: [],
|
attributes: [],
|
||||||
isSteckling: false,
|
isSteckling: false,
|
||||||
imageDialogOpen: false,
|
imageDialogOpen: false,
|
||||||
|
komponenten: [],
|
||||||
|
komponentenLoaded: false,
|
||||||
|
komponentenData: {}, // Store individual komponent data with loading states
|
||||||
|
komponentenImages: {}, // Store tiny pictures for komponenten
|
||||||
|
totalKomponentenPrice: 0,
|
||||||
|
totalSavings: 0
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -64,6 +76,248 @@ class ProductDetailPage extends Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
loadKomponentImage = (komponentId, pictureList) => {
|
||||||
|
// Initialize cache if it doesn't exist
|
||||||
|
if (!window.smallPicCache) {
|
||||||
|
window.smallPicCache = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip if no pictureList
|
||||||
|
if (!pictureList || pictureList.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the first image ID from pictureList
|
||||||
|
const bildId = pictureList.split(',')[0];
|
||||||
|
|
||||||
|
// Check if already cached
|
||||||
|
if (window.smallPicCache[bildId]) {
|
||||||
|
this.setState(prevState => ({
|
||||||
|
komponentenImages: {
|
||||||
|
...prevState.komponentenImages,
|
||||||
|
[komponentId]: window.smallPicCache[bildId]
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if socketB is available
|
||||||
|
if (!this.props.socketB || !this.props.socketB.connected) {
|
||||||
|
console.log("SocketB not connected yet, skipping image load for komponent:", komponentId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch image from server
|
||||||
|
this.props.socketB.emit('getPic', { bildId, size: 'small' }, (res) => {
|
||||||
|
if (res.success) {
|
||||||
|
// Cache the image
|
||||||
|
window.smallPicCache[bildId] = URL.createObjectURL(new Blob([res.imageBuffer], { type: 'image/jpeg' }));
|
||||||
|
|
||||||
|
// Update state
|
||||||
|
this.setState(prevState => ({
|
||||||
|
komponentenImages: {
|
||||||
|
...prevState.komponentenImages,
|
||||||
|
[komponentId]: window.smallPicCache[bildId]
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
} else {
|
||||||
|
console.log('Error loading komponent image:', res);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
loadKomponent = (id, count) => {
|
||||||
|
// Initialize cache if it doesn't exist
|
||||||
|
if (!window.productDetailCache) {
|
||||||
|
window.productDetailCache = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if this komponent is already cached
|
||||||
|
if (window.productDetailCache[id]) {
|
||||||
|
const cachedProduct = window.productDetailCache[id];
|
||||||
|
|
||||||
|
// Load komponent image if available
|
||||||
|
if (cachedProduct.pictureList) {
|
||||||
|
this.loadKomponentImage(id, cachedProduct.pictureList);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update state with cached data
|
||||||
|
this.setState(prevState => {
|
||||||
|
const newKomponentenData = {
|
||||||
|
...prevState.komponentenData,
|
||||||
|
[id]: {
|
||||||
|
...cachedProduct,
|
||||||
|
count: parseInt(count),
|
||||||
|
loaded: true
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check if all remaining komponenten are loaded
|
||||||
|
const allLoaded = prevState.komponenten.every(k =>
|
||||||
|
newKomponentenData[k.id] && newKomponentenData[k.id].loaded
|
||||||
|
);
|
||||||
|
|
||||||
|
// Calculate totals if all loaded
|
||||||
|
let totalKomponentenPrice = 0;
|
||||||
|
let totalSavings = 0;
|
||||||
|
|
||||||
|
if (allLoaded) {
|
||||||
|
totalKomponentenPrice = prevState.komponenten.reduce((sum, k) => {
|
||||||
|
const komponentData = newKomponentenData[k.id];
|
||||||
|
if (komponentData && komponentData.loaded) {
|
||||||
|
return sum + (komponentData.price * parseInt(k.count));
|
||||||
|
}
|
||||||
|
return sum;
|
||||||
|
}, 0);
|
||||||
|
|
||||||
|
// Calculate savings (difference between buying individually vs as set)
|
||||||
|
const setPrice = prevState.product ? prevState.product.price : 0;
|
||||||
|
totalSavings = Math.max(0, totalKomponentenPrice - setPrice);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("Cached komponent loaded:", id, "data:", newKomponentenData[id]);
|
||||||
|
console.log("All loaded (cached):", allLoaded);
|
||||||
|
|
||||||
|
return {
|
||||||
|
komponentenData: newKomponentenData,
|
||||||
|
komponentenLoaded: allLoaded,
|
||||||
|
totalKomponentenPrice,
|
||||||
|
totalSavings
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If not cached, fetch from server (similar to loadProductData)
|
||||||
|
if (!this.props.socket || !this.props.socket.connected) {
|
||||||
|
console.log("Socket not connected yet, waiting for connection to load komponent data");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mark this komponent as loading
|
||||||
|
this.setState(prevState => ({
|
||||||
|
komponentenData: {
|
||||||
|
...prevState.komponentenData,
|
||||||
|
[id]: {
|
||||||
|
...prevState.komponentenData[id],
|
||||||
|
loading: true,
|
||||||
|
loaded: false,
|
||||||
|
count: parseInt(count)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
this.props.socket.emit(
|
||||||
|
"getProductView",
|
||||||
|
{ articleId: id },
|
||||||
|
(res) => {
|
||||||
|
if (res.success) {
|
||||||
|
// Cache the successful response
|
||||||
|
window.productDetailCache[id] = res.product;
|
||||||
|
|
||||||
|
// Load komponent image if available
|
||||||
|
if (res.product.pictureList) {
|
||||||
|
this.loadKomponentImage(id, res.product.pictureList);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update state with loaded data
|
||||||
|
this.setState(prevState => {
|
||||||
|
const newKomponentenData = {
|
||||||
|
...prevState.komponentenData,
|
||||||
|
[id]: {
|
||||||
|
...res.product,
|
||||||
|
count: parseInt(count),
|
||||||
|
loading: false,
|
||||||
|
loaded: true
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check if all remaining komponenten are loaded
|
||||||
|
const allLoaded = prevState.komponenten.every(k =>
|
||||||
|
newKomponentenData[k.id] && newKomponentenData[k.id].loaded
|
||||||
|
);
|
||||||
|
|
||||||
|
// Calculate totals if all loaded
|
||||||
|
let totalKomponentenPrice = 0;
|
||||||
|
let totalSavings = 0;
|
||||||
|
|
||||||
|
if (allLoaded) {
|
||||||
|
totalKomponentenPrice = prevState.komponenten.reduce((sum, k) => {
|
||||||
|
const komponentData = newKomponentenData[k.id];
|
||||||
|
if (komponentData && komponentData.loaded) {
|
||||||
|
return sum + (komponentData.price * parseInt(k.count));
|
||||||
|
}
|
||||||
|
return sum;
|
||||||
|
}, 0);
|
||||||
|
|
||||||
|
// Calculate savings (difference between buying individually vs as set)
|
||||||
|
const setPrice = prevState.product ? prevState.product.price : 0;
|
||||||
|
totalSavings = Math.max(0, totalKomponentenPrice - setPrice);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("Updated komponentenData for", id, ":", newKomponentenData[id]);
|
||||||
|
console.log("All loaded:", allLoaded);
|
||||||
|
|
||||||
|
return {
|
||||||
|
komponentenData: newKomponentenData,
|
||||||
|
komponentenLoaded: allLoaded,
|
||||||
|
totalKomponentenPrice,
|
||||||
|
totalSavings
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("getProductView (komponent)", res);
|
||||||
|
} else {
|
||||||
|
console.error("Error loading komponent:", res.error || "Unknown error", res);
|
||||||
|
|
||||||
|
// Remove failed komponent from the list and check if all remaining are loaded
|
||||||
|
this.setState(prevState => {
|
||||||
|
const newKomponenten = prevState.komponenten.filter(k => k.id !== id);
|
||||||
|
const newKomponentenData = { ...prevState.komponentenData };
|
||||||
|
|
||||||
|
// Remove failed komponent from data
|
||||||
|
delete newKomponentenData[id];
|
||||||
|
|
||||||
|
// Check if all remaining komponenten are loaded
|
||||||
|
const allLoaded = newKomponenten.length === 0 || newKomponenten.every(k =>
|
||||||
|
newKomponentenData[k.id] && newKomponentenData[k.id].loaded
|
||||||
|
);
|
||||||
|
|
||||||
|
// Calculate totals if all loaded
|
||||||
|
let totalKomponentenPrice = 0;
|
||||||
|
let totalSavings = 0;
|
||||||
|
|
||||||
|
if (allLoaded && newKomponenten.length > 0) {
|
||||||
|
totalKomponentenPrice = newKomponenten.reduce((sum, k) => {
|
||||||
|
const komponentData = newKomponentenData[k.id];
|
||||||
|
if (komponentData && komponentData.loaded) {
|
||||||
|
return sum + (komponentData.price * parseInt(k.count));
|
||||||
|
}
|
||||||
|
return sum;
|
||||||
|
}, 0);
|
||||||
|
|
||||||
|
// Calculate savings (difference between buying individually vs as set)
|
||||||
|
const setPrice = this.state.product ? this.state.product.price : 0;
|
||||||
|
totalSavings = Math.max(0, totalKomponentenPrice - setPrice);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("Removed failed komponent:", id, "remaining:", newKomponenten.length);
|
||||||
|
console.log("All loaded after removal:", allLoaded);
|
||||||
|
|
||||||
|
return {
|
||||||
|
komponenten: newKomponenten,
|
||||||
|
komponentenData: newKomponentenData,
|
||||||
|
komponentenLoaded: allLoaded,
|
||||||
|
totalKomponentenPrice,
|
||||||
|
totalSavings
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
loadProductData = () => {
|
loadProductData = () => {
|
||||||
if (!this.props.socket || !this.props.socket.connected) {
|
if (!this.props.socket || !this.props.socket.connected) {
|
||||||
// Socket not connected yet, but don't show error immediately on first load
|
// Socket not connected yet, but don't show error immediately on first load
|
||||||
@@ -78,12 +332,37 @@ class ProductDetailPage extends Component {
|
|||||||
(res) => {
|
(res) => {
|
||||||
if (res.success) {
|
if (res.success) {
|
||||||
res.product.seoName = this.props.seoName;
|
res.product.seoName = this.props.seoName;
|
||||||
|
|
||||||
|
// Initialize cache if it doesn't exist
|
||||||
|
if (!window.productDetailCache) {
|
||||||
|
window.productDetailCache = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cache the product data
|
||||||
|
window.productDetailCache[this.props.seoName] = res.product;
|
||||||
|
|
||||||
|
const komponenten = [];
|
||||||
|
if(res.product.komponenten) {
|
||||||
|
for(const komponent of res.product.komponenten.split(",")) {
|
||||||
|
// Handle both "x" and "×" as separators
|
||||||
|
const [id, count] = komponent.split(/[x×]/);
|
||||||
|
komponenten.push({id: id.trim(), count: count.trim()});
|
||||||
|
}
|
||||||
|
}
|
||||||
this.setState({
|
this.setState({
|
||||||
product: res.product,
|
product: res.product,
|
||||||
loading: false,
|
loading: false,
|
||||||
error: null,
|
error: null,
|
||||||
imageDialogOpen: false,
|
imageDialogOpen: false,
|
||||||
attributes: res.attributes
|
attributes: res.attributes,
|
||||||
|
komponenten: komponenten,
|
||||||
|
komponentenLoaded: komponenten.length === 0 // If no komponenten, mark as loaded
|
||||||
|
}, () => {
|
||||||
|
if(komponenten.length > 0) {
|
||||||
|
for(const komponent of komponenten) {
|
||||||
|
this.loadKomponent(komponent.id, komponent.count);
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
console.log("getProductView", res);
|
console.log("getProductView", res);
|
||||||
|
|
||||||
@@ -180,7 +459,7 @@ class ProductDetailPage extends Component {
|
|||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { product, loading, error, attributeImages, isSteckling, attributes } =
|
const { product, loading, error, attributeImages, isSteckling, attributes, komponentenLoaded, komponentenData, komponentenImages, totalKomponentenPrice, totalSavings } =
|
||||||
this.state;
|
this.state;
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
@@ -452,7 +731,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" && (
|
||||||
@@ -461,6 +744,37 @@ class ProductDetailPage extends Component {
|
|||||||
</Typography>
|
</Typography>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
|
{/* Savings comparison - positioned between price and cart button */}
|
||||||
|
{product.komponenten && komponentenLoaded && totalKomponentenPrice > product.price &&
|
||||||
|
(totalKomponentenPrice - product.price >= 2 &&
|
||||||
|
(totalKomponentenPrice - product.price) / product.price >= 0.02) && (
|
||||||
|
<Box sx={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
minWidth: { xs: "100%", sm: "200px" }
|
||||||
|
}}>
|
||||||
|
<Box sx={{ p: 2, borderRadius: 1, backgroundColor: "#e8f5e8", textAlign: "center" }}>
|
||||||
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
sx={{
|
||||||
|
fontWeight: "bold",
|
||||||
|
color: "success.main"
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Sie sparen: {new Intl.NumberFormat("de-DE", {
|
||||||
|
style: "currency",
|
||||||
|
currency: "EUR",
|
||||||
|
}).format(totalKomponentenPrice - product.price)}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="caption" color="text.secondary">
|
||||||
|
Günstiger als Einzelkauf
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
display: "flex",
|
display: "flex",
|
||||||
@@ -487,6 +801,7 @@ class ProductDetailPage extends Component {
|
|||||||
vat={product.vat}
|
vat={product.vat}
|
||||||
weight={product.weight}
|
weight={product.weight}
|
||||||
availableSupplier={product.availableSupplier}
|
availableSupplier={product.availableSupplier}
|
||||||
|
komponenten={product.komponenten}
|
||||||
name={cleanProductName(product.name) + " Stecklingsvorbestellung 1 Stück"}
|
name={cleanProductName(product.name) + " Stecklingsvorbestellung 1 Stück"}
|
||||||
versandklasse={"nur Abholung"}
|
versandklasse={"nur Abholung"}
|
||||||
/>
|
/>
|
||||||
@@ -516,12 +831,16 @@ class ProductDetailPage extends Component {
|
|||||||
available={product.available}
|
available={product.available}
|
||||||
id={product.id}
|
id={product.id}
|
||||||
availableSupplier={product.availableSupplier}
|
availableSupplier={product.availableSupplier}
|
||||||
|
komponenten={product.komponenten}
|
||||||
|
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={{
|
||||||
@@ -565,6 +884,206 @@ class ProductDetailPage extends Component {
|
|||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{product.komponenten && product.komponenten.split(",").length > 0 && (
|
||||||
|
<Box sx={{ mt: 4, p: 4, background: "#fff", borderRadius: 2, boxShadow: "0 2px 8px rgba(0,0,0,0.08)" }}>
|
||||||
|
<Typography variant="h4" gutterBottom>Bestehend aus:</Typography>
|
||||||
|
<Box sx={{ maxWidth: 800, mx: "auto" }}>
|
||||||
|
|
||||||
|
{(console.log("komponentenLoaded:", komponentenLoaded), komponentenLoaded) ? (
|
||||||
|
<>
|
||||||
|
{console.log("Rendering loaded komponenten:", this.state.komponenten.length, "komponentenData:", Object.keys(komponentenData).length)}
|
||||||
|
{this.state.komponenten.map((komponent, index) => {
|
||||||
|
const komponentData = komponentenData[komponent.id];
|
||||||
|
console.log(`Rendering komponent ${komponent.id}:`, komponentData);
|
||||||
|
|
||||||
|
// Don't show border on last item (pricing section has its own top border)
|
||||||
|
const isLastItem = index === this.state.komponenten.length - 1;
|
||||||
|
const showBorder = !isLastItem;
|
||||||
|
|
||||||
|
if (!komponentData || !komponentData.loaded) {
|
||||||
|
return (
|
||||||
|
<Box key={komponent.id} sx={{
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
alignItems: "center",
|
||||||
|
py: 1,
|
||||||
|
borderBottom: showBorder ? "1px solid #eee" : "none",
|
||||||
|
minHeight: "70px" // Consistent height to prevent layout shifts
|
||||||
|
}}>
|
||||||
|
<Box sx={{ display: "flex", alignItems: "center", gap: 2 }}>
|
||||||
|
<Box sx={{ width: 50, height: 50, flexShrink: 0, backgroundColor: "#f5f5f5", borderRadius: 1, border: "1px solid #e0e0e0" }}>
|
||||||
|
{/* Empty placeholder for image */}
|
||||||
|
</Box>
|
||||||
|
<Box>
|
||||||
|
<Typography variant="body1">
|
||||||
|
{index + 1}. Lädt...
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" color="text.secondary">
|
||||||
|
{komponent.count}x
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
<Typography variant="body1" sx={{ fontWeight: 500 }}>
|
||||||
|
-
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const itemPrice = komponentData.price * parseInt(komponent.count);
|
||||||
|
const formattedPrice = new Intl.NumberFormat("de-DE", {
|
||||||
|
style: "currency",
|
||||||
|
currency: "EUR",
|
||||||
|
}).format(itemPrice);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
key={komponent.id}
|
||||||
|
component={Link}
|
||||||
|
to={`/Artikel/${komponentData.seoName}`}
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
alignItems: "center",
|
||||||
|
py: 1,
|
||||||
|
borderBottom: showBorder ? "1px solid #eee" : "none",
|
||||||
|
textDecoration: "none",
|
||||||
|
color: "inherit",
|
||||||
|
minHeight: "70px", // Consistent height to prevent layout shifts
|
||||||
|
"&:hover": {
|
||||||
|
backgroundColor: "#f5f5f5"
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box sx={{ display: "flex", alignItems: "center", gap: 2 }}>
|
||||||
|
<Box sx={{ width: 50, height: 50, flexShrink: 0 }}>
|
||||||
|
{komponentenImages[komponent.id] ? (
|
||||||
|
<CardMedia
|
||||||
|
component="img"
|
||||||
|
height="50"
|
||||||
|
image={komponentenImages[komponent.id]}
|
||||||
|
alt={komponentData.name}
|
||||||
|
sx={{
|
||||||
|
objectFit: "contain",
|
||||||
|
borderRadius: 1,
|
||||||
|
border: "1px solid #e0e0e0"
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<CardMedia
|
||||||
|
component="img"
|
||||||
|
height="50"
|
||||||
|
image="/assets/images/nopicture.jpg"
|
||||||
|
alt={komponentData.name}
|
||||||
|
sx={{
|
||||||
|
objectFit: "contain",
|
||||||
|
borderRadius: 1,
|
||||||
|
border: "1px solid #e0e0e0"
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
<Box>
|
||||||
|
<Typography variant="body1" sx={{ fontWeight: 500 }}>
|
||||||
|
{index + 1}. {cleanProductName(komponentData.name)}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" color="text.secondary">
|
||||||
|
{komponent.count}x à {new Intl.NumberFormat("de-DE", {
|
||||||
|
style: "currency",
|
||||||
|
currency: "EUR",
|
||||||
|
}).format(komponentData.price)}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
<Typography variant="body1" sx={{ fontWeight: 500 }}>
|
||||||
|
{formattedPrice}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
|
||||||
|
{/* Total price and savings display - only show when prices differ meaningfully */}
|
||||||
|
{totalKomponentenPrice > product.price &&
|
||||||
|
(totalKomponentenPrice - product.price >= 2 &&
|
||||||
|
(totalKomponentenPrice - product.price) / product.price >= 0.02) && (
|
||||||
|
<Box sx={{ mt: 3, pt: 2, borderTop: "2px solid #eee" }}>
|
||||||
|
<Box sx={{ display: "flex", justifyContent: "space-between", alignItems: "center", mb: 1 }}>
|
||||||
|
<Typography variant="h6">
|
||||||
|
Einzelpreis gesamt:
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="h6" sx={{ textDecoration: "line-through", color: "text.secondary" }}>
|
||||||
|
{new Intl.NumberFormat("de-DE", {
|
||||||
|
style: "currency",
|
||||||
|
currency: "EUR",
|
||||||
|
}).format(totalKomponentenPrice)}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
<Box sx={{ display: "flex", justifyContent: "space-between", alignItems: "center", mb: 1 }}>
|
||||||
|
<Typography variant="h6">
|
||||||
|
Set-Preis:
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="h6" color="primary" sx={{ fontWeight: "bold" }}>
|
||||||
|
{new Intl.NumberFormat("de-DE", {
|
||||||
|
style: "currency",
|
||||||
|
currency: "EUR",
|
||||||
|
}).format(product.price)}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
{totalSavings > 0 && (
|
||||||
|
<Box sx={{ display: "flex", justifyContent: "space-between", alignItems: "center", mt: 2, p: 2, backgroundColor: "#e8f5e8", borderRadius: 1 }}>
|
||||||
|
<Typography variant="h6" color="success.main" sx={{ fontWeight: "bold" }}>
|
||||||
|
Ihre Ersparnis:
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="h6" color="success.main" sx={{ fontWeight: "bold" }}>
|
||||||
|
{new Intl.NumberFormat("de-DE", {
|
||||||
|
style: "currency",
|
||||||
|
currency: "EUR",
|
||||||
|
}).format(totalSavings)}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
// Loading state
|
||||||
|
<Box>
|
||||||
|
{this.state.komponenten.map((komponent, index) => {
|
||||||
|
// For loading state, we don't know if pricing will be shown, so show all borders
|
||||||
|
return (
|
||||||
|
<Box key={komponent.id} sx={{
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
alignItems: "center",
|
||||||
|
py: 1,
|
||||||
|
borderBottom: "1px solid #eee",
|
||||||
|
minHeight: "70px" // Consistent height to prevent layout shifts
|
||||||
|
}}>
|
||||||
|
<Box sx={{ display: "flex", alignItems: "center", gap: 2 }}>
|
||||||
|
<Box sx={{ width: 50, height: 50, flexShrink: 0, backgroundColor: "#f5f5f5", borderRadius: 1, border: "1px solid #e0e0e0" }}>
|
||||||
|
{/* Empty placeholder for image */}
|
||||||
|
</Box>
|
||||||
|
<Box>
|
||||||
|
<Typography variant="body1">
|
||||||
|
{index + 1}. Lädt Komponent-Details...
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" color="text.secondary">
|
||||||
|
{komponent.count}x
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
<Typography variant="body1" sx={{ fontWeight: 500 }}>
|
||||||
|
-
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -141,12 +141,12 @@ class ProductList extends Component {
|
|||||||
onChange={this.handlePageChange}
|
onChange={this.handlePageChange}
|
||||||
color="primary"
|
color="primary"
|
||||||
size={"large"}
|
size={"large"}
|
||||||
siblingCount={window.innerWidth < 600 ? 0 : 1}
|
siblingCount={1}
|
||||||
boundaryCount={window.innerWidth < 600 ? 1 : 1}
|
boundaryCount={1}
|
||||||
hideNextButton={false}
|
hideNextButton={true}
|
||||||
hidePrevButton={false}
|
hidePrevButton={true}
|
||||||
showFirstButton={window.innerWidth >= 600}
|
showFirstButton={false}
|
||||||
showLastButton={window.innerWidth >= 600}
|
showLastButton={false}
|
||||||
sx={{
|
sx={{
|
||||||
'& .MuiPagination-ul': {
|
'& .MuiPagination-ul': {
|
||||||
flexWrap: 'nowrap',
|
flexWrap: 'nowrap',
|
||||||
@@ -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}
|
||||||
@@ -474,6 +474,7 @@ class ProductList extends Component {
|
|||||||
socketB={this.props.socketB}
|
socketB={this.props.socketB}
|
||||||
pictureList={product.pictureList}
|
pictureList={product.pictureList}
|
||||||
availableSupplier={product.availableSupplier}
|
availableSupplier={product.availableSupplier}
|
||||||
|
komponenten={product.komponenten}
|
||||||
/>
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
))}
|
))}
|
||||||
|
|||||||
229
src/components/SharedCarousel.js
Normal file
229
src/components/SharedCarousel.js
Normal 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;
|
||||||
@@ -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" },
|
||||||
|
|||||||
@@ -51,9 +51,6 @@ class CartTab extends Component {
|
|||||||
showStripePayment: false,
|
showStripePayment: false,
|
||||||
StripeComponent: null,
|
StripeComponent: null,
|
||||||
isLoadingStripe: false,
|
isLoadingStripe: false,
|
||||||
showMolliePayment: false,
|
|
||||||
MollieComponent: null,
|
|
||||||
isLoadingMollie: false,
|
|
||||||
showPaymentConfirmation: false,
|
showPaymentConfirmation: false,
|
||||||
orderCompleted: false,
|
orderCompleted: false,
|
||||||
originalCartItems: []
|
originalCartItems: []
|
||||||
@@ -119,7 +116,7 @@ class CartTab extends Component {
|
|||||||
// Determine payment method - respect constraints
|
// Determine payment method - respect constraints
|
||||||
let prefillPaymentMethod = template.payment_method || "wire";
|
let prefillPaymentMethod = template.payment_method || "wire";
|
||||||
const paymentMethodMap = {
|
const paymentMethodMap = {
|
||||||
"credit_card": "mollie",//stripe
|
"credit_card": "stripe",
|
||||||
"bank_transfer": "wire",
|
"bank_transfer": "wire",
|
||||||
"cash_on_delivery": "onDelivery",
|
"cash_on_delivery": "onDelivery",
|
||||||
"cash": "cash"
|
"cash": "cash"
|
||||||
@@ -322,27 +319,6 @@ class CartTab extends Component {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
loadMollieComponent = async () => {
|
|
||||||
this.setState({ isLoadingMollie: true });
|
|
||||||
|
|
||||||
try {
|
|
||||||
const { default: Mollie } = await import("../Mollie.js");
|
|
||||||
this.setState({
|
|
||||||
MollieComponent: Mollie,
|
|
||||||
showMolliePayment: true,
|
|
||||||
isCompletingOrder: false,
|
|
||||||
isLoadingMollie: false,
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Failed to load Mollie component:", error);
|
|
||||||
this.setState({
|
|
||||||
isCompletingOrder: false,
|
|
||||||
isLoadingMollie: false,
|
|
||||||
completionError: "Failed to load payment component. Please try again.",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
handleCompleteOrder = () => {
|
handleCompleteOrder = () => {
|
||||||
this.setState({ completionError: null }); // Clear previous errors
|
this.setState({ completionError: null }); // Clear previous errors
|
||||||
|
|
||||||
@@ -387,25 +363,6 @@ class CartTab extends Component {
|
|||||||
this.orderService.createStripeIntent(totalAmount, this.loadStripeComponent);
|
this.orderService.createStripeIntent(totalAmount, this.loadStripeComponent);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Handle Mollie payment differently
|
|
||||||
if (paymentMethod === "mollie") {
|
|
||||||
// Store the cart items used for Mollie payment in sessionStorage for later reference
|
|
||||||
try {
|
|
||||||
sessionStorage.setItem('molliePaymentCart', JSON.stringify(cartItems));
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Failed to store Mollie payment cart:", error);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Calculate total amount for Mollie
|
|
||||||
const subtotal = cartItems.reduce(
|
|
||||||
(total, item) => total + item.price * item.quantity,
|
|
||||||
0
|
|
||||||
);
|
|
||||||
const totalAmount = Math.round((subtotal + deliveryCost) * 100); // Convert to cents
|
|
||||||
|
|
||||||
this.orderService.createMollieIntent(totalAmount, this.loadMollieComponent);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle regular orders
|
// Handle regular orders
|
||||||
const orderData = {
|
const orderData = {
|
||||||
@@ -441,9 +398,6 @@ class CartTab extends Component {
|
|||||||
showStripePayment,
|
showStripePayment,
|
||||||
StripeComponent,
|
StripeComponent,
|
||||||
isLoadingStripe,
|
isLoadingStripe,
|
||||||
showMolliePayment,
|
|
||||||
MollieComponent,
|
|
||||||
isLoadingMollie,
|
|
||||||
showPaymentConfirmation,
|
showPaymentConfirmation,
|
||||||
orderCompleted,
|
orderCompleted,
|
||||||
} = this.state;
|
} = this.state;
|
||||||
@@ -480,7 +434,7 @@ class CartTab extends Component {
|
|||||||
<CartDropdown
|
<CartDropdown
|
||||||
cartItems={cartItems}
|
cartItems={cartItems}
|
||||||
socket={this.context.socket}
|
socket={this.context.socket}
|
||||||
showDetailedSummary={showStripePayment || showMolliePayment}
|
showDetailedSummary={showStripePayment}
|
||||||
deliveryMethod={deliveryMethod}
|
deliveryMethod={deliveryMethod}
|
||||||
deliveryCost={deliveryCost}
|
deliveryCost={deliveryCost}
|
||||||
/>
|
/>
|
||||||
@@ -488,7 +442,7 @@ class CartTab extends Component {
|
|||||||
|
|
||||||
{cartItems.length > 0 && (
|
{cartItems.length > 0 && (
|
||||||
<Box sx={{ mt: 3 }}>
|
<Box sx={{ mt: 3 }}>
|
||||||
{isLoadingStripe || isLoadingMollie ? (
|
{isLoadingStripe ? (
|
||||||
<Box sx={{ textAlign: 'center', py: 4 }}>
|
<Box sx={{ textAlign: 'center', py: 4 }}>
|
||||||
<Typography variant="body1">
|
<Typography variant="body1">
|
||||||
Zahlungskomponente wird geladen...
|
Zahlungskomponente wird geladen...
|
||||||
@@ -514,29 +468,9 @@ class CartTab extends Component {
|
|||||||
</Box>
|
</Box>
|
||||||
<StripeComponent clientSecret={stripeClientSecret} />
|
<StripeComponent clientSecret={stripeClientSecret} />
|
||||||
</>
|
</>
|
||||||
) : showMolliePayment && MollieComponent ? (
|
|
||||||
<>
|
|
||||||
<Box sx={{ mb: 2 }}>
|
|
||||||
<Button
|
|
||||||
variant="outlined"
|
|
||||||
onClick={() => this.setState({ showMolliePayment: false })}
|
|
||||||
sx={{
|
|
||||||
color: '#2e7d32',
|
|
||||||
borderColor: '#2e7d32',
|
|
||||||
'&:hover': {
|
|
||||||
backgroundColor: 'rgba(46, 125, 50, 0.04)',
|
|
||||||
borderColor: '#1b5e20'
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
← Zurück zur Bestellung
|
|
||||||
</Button>
|
|
||||||
</Box>
|
|
||||||
<MollieComponent />
|
|
||||||
</>
|
|
||||||
) : (
|
) : (
|
||||||
<CheckoutForm
|
<CheckoutForm
|
||||||
paymentMethod={paymentMethod}
|
paymentMethod={paymentMethod}
|
||||||
invoiceAddress={invoiceAddress}
|
invoiceAddress={invoiceAddress}
|
||||||
deliveryAddress={deliveryAddress}
|
deliveryAddress={deliveryAddress}
|
||||||
useSameAddress={useSameAddress}
|
useSameAddress={useSameAddress}
|
||||||
@@ -544,7 +478,7 @@ class CartTab extends Component {
|
|||||||
addressFormErrors={addressFormErrors}
|
addressFormErrors={addressFormErrors}
|
||||||
termsAccepted={termsAccepted}
|
termsAccepted={termsAccepted}
|
||||||
note={note}
|
note={note}
|
||||||
deliveryMethod={deliveryMethod}
|
deliveryMethod={deliveryMethod}
|
||||||
hasStecklinge={hasStecklinge}
|
hasStecklinge={hasStecklinge}
|
||||||
isPickupOnly={isPickupOnly}
|
isPickupOnly={isPickupOnly}
|
||||||
deliveryCost={deliveryCost}
|
deliveryCost={deliveryCost}
|
||||||
|
|||||||
@@ -270,10 +270,6 @@ class OrderProcessingService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Create Mollie payment intent
|
|
||||||
createMollieIntent(totalAmount, loadMollieComponent) {
|
|
||||||
loadMollieComponent();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Calculate delivery cost
|
// Calculate delivery cost
|
||||||
getDeliveryCost() {
|
getDeliveryCost() {
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ const PaymentMethodSelector = ({ paymentMethod, onChange, deliveryMethod, onDeli
|
|||||||
// Handle delivery method changes - auto-switch to stripe when DHL/DPD is selected
|
// Handle delivery method changes - auto-switch to stripe when DHL/DPD is selected
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if ((deliveryMethod === "DHL" || deliveryMethod === "DPD") && paymentMethod === "cash") {
|
if ((deliveryMethod === "DHL" || deliveryMethod === "DPD") && paymentMethod === "cash") {
|
||||||
handlePaymentMethodChange({ target: { value: "mollie" } });
|
handlePaymentMethodChange({ target: { value: "stripe" } });
|
||||||
}
|
}
|
||||||
}, [deliveryMethod, paymentMethod, handlePaymentMethodChange]);
|
}, [deliveryMethod, paymentMethod, handlePaymentMethodChange]);
|
||||||
|
|
||||||
@@ -42,22 +42,8 @@ const PaymentMethodSelector = ({ paymentMethod, onChange, deliveryMethod, onDeli
|
|||||||
description: "Bezahlen Sie per Banküberweisung",
|
description: "Bezahlen Sie per Banküberweisung",
|
||||||
disabled: totalAmount === 0,
|
disabled: totalAmount === 0,
|
||||||
},
|
},
|
||||||
/*{
|
|
||||||
id: "stripe",
|
|
||||||
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",
|
id: "stripe",
|
||||||
name: "Karte oder Sofortüberweisung",
|
name: "Karte oder Sofortüberweisung",
|
||||||
description: totalAmount < 0.50 && totalAmount > 0
|
description: totalAmount < 0.50 && totalAmount > 0
|
||||||
? "Bezahlen Sie per Karte oder Sofortüberweisung (Mindestbetrag: 0,50 €)"
|
? "Bezahlen Sie per Karte oder Sofortüberweisung (Mindestbetrag: 0,50 €)"
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ const config = {
|
|||||||
apiBaseUrl: "",
|
apiBaseUrl: "",
|
||||||
googleClientId: "928121624463-jbgfdlgem22scs1k9c87ucg4ffvaik6o.apps.googleusercontent.com",
|
googleClientId: "928121624463-jbgfdlgem22scs1k9c87ucg4ffvaik6o.apps.googleusercontent.com",
|
||||||
stripePublishableKey: "pk_test_51R7lltRtpe3h1vwJzIrDb5bcEigTLBHrtqj9SiPX7FOEATSuD6oJmKc8xpNp49ShpGJZb2GShHIUqj4zlSIz4olj00ipOuOAnu",
|
stripePublishableKey: "pk_test_51R7lltRtpe3h1vwJzIrDb5bcEigTLBHrtqj9SiPX7FOEATSuD6oJmKc8xpNp49ShpGJZb2GShHIUqj4zlSIz4olj00ipOuOAnu",
|
||||||
mollieProfileKey: "pfl_AtcRTimCff",
|
|
||||||
|
|
||||||
// SEO and Business Information
|
// SEO and Business Information
|
||||||
siteName: "Growheads.de",
|
siteName: "Growheads.de",
|
||||||
|
|||||||
225
src/contexts/CarouselContext.js
Normal file
225
src/contexts/CarouselContext.js
Normal 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;
|
||||||
@@ -17,6 +17,51 @@ import {
|
|||||||
import { TentShapeSelector, ProductSelector, ExtrasSelector } from '../components/configurator/index.js';
|
import { TentShapeSelector, ProductSelector, ExtrasSelector } from '../components/configurator/index.js';
|
||||||
import { tentShapes, tentSizes, lightTypes, ventilationTypes, extras } from '../data/configuratorData.js';
|
import { tentShapes, tentSizes, lightTypes, ventilationTypes, extras } from '../data/configuratorData.js';
|
||||||
|
|
||||||
|
function setCachedCategoryData(categoryId, data) {
|
||||||
|
if (!window.productCache) {
|
||||||
|
window.productCache = {};
|
||||||
|
}
|
||||||
|
if (!window.productDetailCache) {
|
||||||
|
window.productDetailCache = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const cacheKey = `categoryProducts_${categoryId}`;
|
||||||
|
if(data.products) for(const product of data.products) {
|
||||||
|
window.productDetailCache[product.id] = product;
|
||||||
|
}
|
||||||
|
window.productCache[cacheKey] = {
|
||||||
|
...data,
|
||||||
|
timestamp: Date.now()
|
||||||
|
};
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error writing to cache:', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function getCachedCategoryData(categoryId) {
|
||||||
|
if (!window.productCache) {
|
||||||
|
window.productCache = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const cacheKey = `categoryProducts_${categoryId}`;
|
||||||
|
const cachedData = window.productCache[cacheKey];
|
||||||
|
|
||||||
|
if (cachedData) {
|
||||||
|
const { timestamp } = cachedData;
|
||||||
|
const cacheAge = Date.now() - timestamp;
|
||||||
|
const tenMinutes = 10 * 60 * 1000;
|
||||||
|
if (cacheAge < tenMinutes) {
|
||||||
|
return cachedData;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error reading from cache:', err);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
class GrowTentKonfigurator extends Component {
|
class GrowTentKonfigurator extends Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
@@ -40,6 +85,8 @@ class GrowTentKonfigurator extends Component {
|
|||||||
this.handleExtraToggle = this.handleExtraToggle.bind(this);
|
this.handleExtraToggle = this.handleExtraToggle.bind(this);
|
||||||
this.calculateTotalPrice = this.calculateTotalPrice.bind(this);
|
this.calculateTotalPrice = this.calculateTotalPrice.bind(this);
|
||||||
this.saveStateToWindow = this.saveStateToWindow.bind(this);
|
this.saveStateToWindow = this.saveStateToWindow.bind(this);
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
saveStateToWindow() {
|
saveStateToWindow() {
|
||||||
@@ -57,7 +104,10 @@ class GrowTentKonfigurator extends Component {
|
|||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
// @note Calculate initial total price with preselected products
|
// @note Calculate initial total price with preselected products
|
||||||
this.calculateTotalPrice();
|
//this.calculateTotalPrice();
|
||||||
|
this.fetchCategoryData("Zelte");
|
||||||
|
this.fetchCategoryData("Lampen");
|
||||||
|
this.fetchCategoryData("Abluft-sets");
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate(prevProps, prevState) {
|
componentDidUpdate(prevProps, prevState) {
|
||||||
@@ -89,6 +139,31 @@ class GrowTentKonfigurator extends Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fetchCategoryData(categoryId) {
|
||||||
|
const cachedData = getCachedCategoryData(categoryId);
|
||||||
|
if (cachedData) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.props.socket || !this.props.socket.connected) {
|
||||||
|
console.log("Socket not connected yet, waiting for connection to fetch category data");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.log(`productList:${categoryId}`);
|
||||||
|
this.props.socket.off(`productList:${categoryId}`);
|
||||||
|
|
||||||
|
this.props.socket.on(`productList:${categoryId}`,(response) => {
|
||||||
|
console.log("getCategoryProducts full response", response);
|
||||||
|
setCachedCategoryData(categoryId, response);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.props.socket.emit("getCategoryProducts", { categoryId: categoryId },
|
||||||
|
(response) => {
|
||||||
|
console.log("getCategoryProducts stub response", response);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
handleTentShapeSelect(shapeId) {
|
handleTentShapeSelect(shapeId) {
|
||||||
this.setState({ selectedTentShape: shapeId });
|
this.setState({ selectedTentShape: shapeId });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
|
||||||
@@ -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;
|
||||||
46
src/pages/PresseverleihPage.js
Normal file
46
src/pages/PresseverleihPage.js
Normal 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
46
src/pages/ThcTestPage.js
Normal 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;
|
||||||
@@ -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',
|
||||||
|
|||||||
Reference in New Issue
Block a user