Compare commits

..

1 Commits

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

2
.gitignore vendored
View File

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

21
.vscode/launch.json vendored
View File

@@ -3,31 +3,20 @@
// This will install dependencies before starting the dev server
"version": "0.2.0",
"configurations": [
{
"type": "node-terminal",
"name": "Start with API propxy to seedheads.de (Install Deps)",
"request": "launch",
"command": "npm run start:seedheads",
"preLaunchTask": "npm: install",
"env": {
"NODE_ENV": "development"
},
"envFile": "${workspaceFolder}/.env",
"skipFiles": [
"<node_internals>/**"
]
},{
"name": "Start",
"cwd": "${workspaceFolder}"
}, {
"type": "node-terminal",
"name": "Start",
"request": "launch",
"command": "npm run start",
"env": {
"NODE_ENV": "development"
},
"envFile": "${workspaceFolder}/.env",
"skipFiles": [
"<node_internals>/**"
]
"cwd": "${workspaceFolder}"
}
]
}

View File

@@ -1,209 +0,0 @@
# Multilingual Implementation Guide
## Overview
Your website now supports multiple languages using **react-i18next**. The implementation is designed to work seamlessly with your existing class components and provides:
- **German (default)** and **English** support
- Language persistence in localStorage
- Dynamic language switching
- SEO-friendly language attributes
- Class component compatibility
## Features Implemented
### 1. Language Switcher
- Located in the header next to the login/profile button
- Shows current language (DE/EN) with flag icon
- Dropdown menu for language selection
- Persists selection in browser storage
### 2. Translated Components
- **Header navigation**: Categories, Home links
- **Authentication**: Login/register forms, profile menu
- **Main pages**: Home, Actions, Store pages
- **Cart**: Shopping cart title and sync dialog
- **Product pages**: Basic UI elements (more can be added)
- **Footer**: Basic elements (can be expanded)
### 3. Architecture
- `src/i18n/index.js` - Main i18n configuration
- `src/i18n/withTranslation.js` - HOCs for class components
- `src/i18n/locales/de/translation.json` - German translations
- `src/i18n/locales/en/translation.json` - English translations
- `src/components/LanguageSwitcher.js` - Language selection component
## Usage for Developers
### Using Translations in Class Components
```javascript
import { withI18n } from '../i18n/withTranslation.js';
class MyComponent extends Component {
render() {
const { t } = this.props; // Translation function
return (
<Typography>
{t('navigation.home')} // Translates to "Startseite" or "Home"
</Typography>
);
}
}
export default withI18n()(MyComponent);
```
### Using Translations in Function Components
```javascript
import { useTranslation } from 'react-i18next';
const MyComponent = () => {
const { t } = useTranslation();
return (
<Typography>
{t('navigation.home')}
</Typography>
);
};
```
### Language Context Access
```javascript
import { withLanguage } from '../i18n/withTranslation.js';
class MyComponent extends Component {
render() {
const { languageContext } = this.props;
return (
<Button onClick={() => languageContext.changeLanguage('en')}>
Switch to English
</Button>
);
}
}
export default withLanguage(MyComponent);
```
## Adding New Translations
### 1. Add to German (`src/i18n/locales/de/translation.json`)
```json
{
"newSection": {
"title": "Neuer Titel",
"description": "Neue Beschreibung"
}
}
```
### 2. Add to English (`src/i18n/locales/en/translation.json`)
```json
{
"newSection": {
"title": "New Title",
"description": "New Description"
}
}
```
### 3. Use in Components
```javascript
{t('newSection.title')}
```
## Adding New Languages
### 1. Create Translation File
Create `src/i18n/locales/fr/translation.json` for French
### 2. Update i18n Configuration
```javascript
// src/i18n/index.js
import translationFR from './locales/fr/translation.json';
const resources = {
de: { translation: translationDE },
en: { translation: translationEN },
fr: { translation: translationFR } // Add new language
};
```
### 3. Update Language Provider
```javascript
// src/i18n/withTranslation.js
availableLanguages: ['de', 'en', 'fr'] // Add to available languages
```
### 4. Update Language Switcher
```javascript
// src/components/LanguageSwitcher.js
const names = {
'de': 'Deutsch',
'en': 'English',
'fr': 'Français' // Add language name
};
```
## Configuration Options
### Language Detection Order
Currently set to: `['localStorage', 'navigator', 'htmlTag']`
- First checks localStorage for saved preference
- Falls back to browser language
- Finally checks HTML lang attribute
### Fallback Language
Set to German (`de`) as your primary language
### Debug Mode
Enabled in development mode for easier debugging
## SEO Considerations
- HTML `lang` attribute updates automatically
- Config object provides language-specific metadata
- Descriptions and keywords are language-aware
- Can be extended for hreflang tags and URL localization
## Best Practices
1. **Namespace your translations** - Use nested objects for organization
2. **Provide fallbacks** - Always have German as fallback since it's your primary market
3. **Use interpolation** - For dynamic content: `t('welcome', { name: 'John' })`
4. **Keep translations consistent** - Use same structure in all language files
5. **Test thoroughly** - Verify all UI elements in both languages
## Current Translation Coverage
- ✅ Navigation and menus
- ✅ Authentication flows
- ✅ Basic product elements
- ✅ Cart functionality
- ✅ Main page content
- ⏳ Detailed product descriptions (can be added)
- ⏳ Legal pages content (can be added)
- ⏳ Form validation messages (can be added)
- ⏳ Error messages (can be added)
## Performance
- Translations are bundled and loaded immediately
- No additional network requests
- Lightweight implementation
- Language switching is instant
## Browser Support
Works with all modern browsers that support:
- ES6 modules
- localStorage
- React 19
The implementation is production-ready and can be extended based on your specific needs!

1710
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -13,11 +13,7 @@
"lint": "eslint src/**/*.{js,jsx}",
"prerender": "node prerender.cjs",
"prerender:prod": "cross-env NODE_ENV=production node prerender.cjs",
"build:prerender": "npm run build:client && npm run prerender:prod",
"translate": "node translate-i18n.js",
"translate:english": "node translate-i18n.js --only-english",
"translate:skip-english": "node translate-i18n.js --skip-english",
"translate:others": "node translate-i18n.js --skip-english"
"build:prerender": "npm run build:client && npm run prerender:prod"
},
"keywords": [],
"author": "",
@@ -31,15 +27,10 @@
"@stripe/react-stripe-js": "^3.7.0",
"@stripe/stripe-js": "^7.3.1",
"chart.js": "^4.5.0",
"country-flag-icons": "^1.5.19",
"html-react-parser": "^5.2.5",
"i18next": "^25.3.2",
"i18next-browser-languagedetector": "^8.2.0",
"openai": "^4.0.0",
"react": "^19.1.0",
"react-chartjs-2": "^5.3.0",
"react-dom": "^19.1.0",
"react-i18next": "^15.6.0",
"react-router-dom": "^7.6.2",
"sharp": "^0.34.2",
"socket.io-client": "^4.7.5"

View File

@@ -27,74 +27,6 @@ const io = require("socket.io-client");
const os = require("os");
const { Worker, isMainThread, parentPort, workerData } = require("worker_threads");
// Initialize i18n for prerendering with German as default
const i18n = require("i18next");
const { initReactI18next } = require("react-i18next");
// Import all translation files
const translationDE = require("./src/i18n/locales/de/translation.js").default;
const translationEN = require("./src/i18n/locales/en/translation.js").default;
const translationAR = require("./src/i18n/locales/ar/translation.js").default;
const translationBG = require("./src/i18n/locales/bg/translation.js").default;
const translationCS = require("./src/i18n/locales/cs/translation.js").default;
const translationEL = require("./src/i18n/locales/el/translation.js").default;
const translationES = require("./src/i18n/locales/es/translation.js").default;
const translationFR = require("./src/i18n/locales/fr/translation.js").default;
const translationHR = require("./src/i18n/locales/hr/translation.js").default;
const translationHU = require("./src/i18n/locales/hu/translation.js").default;
const translationIT = require("./src/i18n/locales/it/translation.js").default;
const translationPL = require("./src/i18n/locales/pl/translation.js").default;
const translationRO = require("./src/i18n/locales/ro/translation.js").default;
const translationRU = require("./src/i18n/locales/ru/translation.js").default;
const translationSK = require("./src/i18n/locales/sk/translation.js").default;
const translationSL = require("./src/i18n/locales/sl/translation.js").default;
const translationSR = require("./src/i18n/locales/sr/translation.js").default;
const translationSV = require("./src/i18n/locales/sv/translation.js").default;
const translationTR = require("./src/i18n/locales/tr/translation.js").default;
const translationUK = require("./src/i18n/locales/uk/translation.js").default;
const translationZH = require("./src/i18n/locales/zh/translation.js").default;
// Initialize i18n for prerendering
i18n
.use(initReactI18next)
.init({
resources: {
de: { translation: translationDE },
en: { translation: translationEN },
ar: { translation: translationAR },
bg: { translation: translationBG },
cs: { translation: translationCS },
el: { translation: translationEL },
es: { translation: translationES },
fr: { translation: translationFR },
hr: { translation: translationHR },
hu: { translation: translationHU },
it: { translation: translationIT },
pl: { translation: translationPL },
ro: { translation: translationRO },
ru: { translation: translationRU },
sk: { translation: translationSK },
sl: { translation: translationSL },
sr: { translation: translationSR },
sv: { translation: translationSV },
tr: { translation: translationTR },
uk: { translation: translationUK },
zh: { translation: translationZH }
},
lng: 'de', // Default to German for prerendering
fallbackLng: 'de',
debug: false,
interpolation: {
escapeValue: false
},
react: {
useSuspense: false
}
});
// Make i18n available globally for components
global.i18n = i18n;
// Import split modules
const config = require("./prerender/config.cjs");
@@ -175,7 +107,6 @@ const renderProductWorker = async (productSeoNames, workerId, progressCallback,
const actualSeoName = productDetails.product.seoName || productSeoName;
const productComponent = React.createElement(PrerenderProduct, {
productData: productDetails,
t: global.i18n.t.bind(global.i18n),
});
const filename = `Artikel/${actualSeoName}`;

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 85 KiB

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 208 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 242 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 KiB

View File

@@ -1,5 +1,5 @@
<!DOCTYPE html>
<html lang="de" data-i18n-lang="de">
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">

View File

@@ -18,19 +18,13 @@ import BugReportIcon from "@mui/icons-material/BugReport";
import SocketProvider from "./providers/SocketProvider.js";
import SocketContext from "./contexts/SocketContext.js";
import { CarouselProvider } from "./contexts/CarouselContext.js";
import config from "./config.js";
import ScrollToTop from "./components/ScrollToTop.js";
// Import i18n
import './i18n/index.js';
import { LanguageProvider } from './i18n/withTranslation.js';
import i18n from './i18n/index.js';
//import TelemetryService from './services/telemetryService.js';
import Header from "./components/Header.js";
import Footer from "./components/Footer.js";
import MainPageLayout from "./components/MainPageLayout.js";
import Home from "./pages/Home.js";
// Lazy load all route components to reduce initial bundle size
const Content = lazy(() => import(/* webpackChunkName: "content" */ "./components/Content.js"));
@@ -46,7 +40,7 @@ const ServerLogsPage = lazy(() => import(/* webpackChunkName: "admin-logs" */ ".
// Lazy load legal pages - rarely accessed
const Datenschutz = lazy(() => import(/* webpackChunkName: "legal" */ "./pages/Datenschutz.js"));
const AGB = lazy(() => import(/* webpackChunkName: "legal" */ "./pages/AGB.js"));
//const NotFound404 = lazy(() => import(/* webpackChunkName: "legal" */ "./pages/NotFound404.js")); <Route path="/404" element={<NotFound404 />} />
const NotFound404 = lazy(() => import(/* webpackChunkName: "legal" */ "./pages/NotFound404.js"));
const Sitemap = lazy(() => import(/* webpackChunkName: "sitemap" */ "./pages/Sitemap.js"));
const Impressum = lazy(() => import(/* webpackChunkName: "legal" */ "./pages/Impressum.js"));
const Batteriegesetzhinweise = lazy(() => import(/* webpackChunkName: "legal" */ "./pages/Batteriegesetzhinweise.js"));
@@ -56,13 +50,6 @@ const Widerrufsrecht = lazy(() => import(/* webpackChunkName: "legal" */ "./page
const GrowTentKonfigurator = lazy(() => import(/* webpackChunkName: "konfigurator" */ "./pages/GrowTentKonfigurator.js"));
const ChatAssistant = lazy(() => import(/* webpackChunkName: "chat" */ "./components/ChatAssistant.js"));
// Lazy load separate pages that are truly different
const PresseverleihPage = lazy(() => import(/* webpackChunkName: "presseverleih" */ "./pages/PresseverleihPage.js"));
const ThcTestPage = lazy(() => import(/* webpackChunkName: "thc-test" */ "./pages/ThcTestPage.js"));
// Lazy load payment success page
const PaymentSuccess = lazy(() => import(/* webpackChunkName: "payment" */ "./components/PaymentSuccess.js"));
// Import theme from separate file to reduce main bundle size
import defaultTheme from "./theme.js";
// Lazy load theme customizer for development only
@@ -107,13 +94,9 @@ const AppContent = ({ currentTheme, onThemeChange }) => {
const navigate = useNavigate();
useEffect(() => {
if (location.hash && location.hash.length > 1) {
// Check if it's a potential order ID (starts with # and has alphanumeric characters with dashes)
const potentialOrderId = location.hash.substring(1);
if (/^[A-Z0-9]+-[A-Z0-9]+$/i.test(potentialOrderId)) {
if (location.pathname !== "/profile") {
navigate(`/profile${location.hash}`, { replace: true });
}
if (location.hash && location.hash.startsWith("#ORD-")) {
if (location.pathname !== "/profile") {
navigate(`/profile${location.hash}`, { replace: true });
}
}
}, [location, navigate]);
@@ -212,70 +195,60 @@ const AppContent = ({ currentTheme, onThemeChange }) => {
<CircularProgress color="primary" />
</Box>
}>
<CarouselProvider>
<Routes>
{/* Main pages using unified component */}
<Route path="/" element={<MainPageLayout />} />
<Route path="/aktionen" element={<MainPageLayout />} />
<Route path="/filiale" element={<MainPageLayout />} />
<Routes>
{/* Home page with text only */}
<Route path="/" element={<Home />} />
{/* Category page - Render Content in parallel */}
<Route
path="/Kategorie/:categoryId"
element={<Content socket={socket} socketB={socketB} />}
/>
{/* Single product page */}
<Route
path="/Artikel/:seoName"
element={<ProductDetailWithSocket />}
/>
{/* Category page - Render Content in parallel */}
<Route
path="/Kategorie/:categoryId"
element={<Content socket={socket} socketB={socketB} />}
/>
{/* Single product page */}
<Route
path="/Artikel/:seoName"
element={<ProductDetailWithSocket />}
/>
{/* Search page - Render Content in parallel */}
<Route path="/search" element={<Content socket={socket} socketB={socketB} />} />
{/* Search page - Render Content in parallel */}
<Route path="/search" element={<Content socket={socket} socketB={socketB} />} />
{/* Profile page */}
<Route path="/profile" element={<ProfilePageWithSocket />} />
{/* Profile page */}
<Route path="/profile" element={<ProfilePageWithSocket />} />
{/* Payment success page for Mollie redirects */}
<Route path="/payment/success" element={<PaymentSuccess />} />
{/* Reset password page */}
<Route
path="/resetPassword"
element={<ResetPassword socket={socket} socketB={socketB} />}
/>
{/* Reset password page */}
<Route
path="/resetPassword"
element={<ResetPassword socket={socket} socketB={socketB} />}
/>
{/* Admin page */}
<Route path="/admin" element={<AdminPage socket={socket} socketB={socketB} />} />
{/* Admin Users page */}
<Route path="/admin/users" element={<UsersPage socket={socket} socketB={socketB} />} />
{/* Admin Server Logs page */}
<Route path="/admin/logs" element={<ServerLogsPage socket={socket} socketB={socketB} />} />
{/* Admin page */}
<Route path="/admin" element={<AdminPage socket={socket} socketB={socketB} />} />
{/* Admin Users page */}
<Route path="/admin/users" element={<UsersPage socket={socket} socketB={socketB} />} />
{/* Admin Server Logs page */}
<Route path="/admin/logs" element={<ServerLogsPage socket={socket} socketB={socketB} />} />
{/* Legal pages */}
<Route path="/datenschutz" element={<Datenschutz />} />
<Route path="/agb" element={<AGB />} />
<Route path="/404" element={<NotFound404 />} />
<Route path="/sitemap" element={<Sitemap />} />
<Route path="/impressum" element={<Impressum />} />
<Route
path="/batteriegesetzhinweise"
element={<Batteriegesetzhinweise />}
/>
<Route path="/widerrufsrecht" element={<Widerrufsrecht />} />
{/* Legal pages */}
<Route path="/datenschutz" element={<Datenschutz />} />
<Route path="/agb" element={<AGB />} />
<Route path="/sitemap" element={<Sitemap />} />
<Route path="/impressum" element={<Impressum />} />
<Route
path="/batteriegesetzhinweise"
element={<Batteriegesetzhinweise />}
/>
<Route path="/widerrufsrecht" element={<Widerrufsrecht />} />
{/* Grow Tent Configurator */}
<Route path="/Konfigurator" element={<GrowTentKonfigurator />} />
{/* Grow Tent Configurator */}
<Route path="/Konfigurator" element={<GrowTentKonfigurator socket={socket} socketB={socketB} />} />
{/* Separate pages that are truly different */}
<Route path="/presseverleih" element={<PresseverleihPage />} />
<Route path="/thc-test" element={<ThcTestPage />} />
{/* Fallback for undefined routes */}
<Route path="*" element={<Navigate to="/" replace />} />
</Routes>
</CarouselProvider>
{/* Fallback for undefined routes */}
<Route path="*" element={<Navigate to="/" replace />} />
</Routes>
</Suspense>
</Box>
{/* Conditionally render the Chat Assistant */}
@@ -370,37 +343,30 @@ const App = () => {
setDynamicTheme(createTheme(newTheme));
};
// Make config globally available for language switching
useEffect(() => {
window.shopConfig = config;
}, []);
return (
<LanguageProvider i18n={i18n}>
<ThemeProvider theme={dynamicTheme}>
<CssBaseline />
<SocketProvider
url={config.apiBaseUrl}
fallback={
<Box
sx={{
display: "flex",
justifyContent: "center",
alignItems: "center",
height: "100vh",
}}
>
<CircularProgress color="primary" />
</Box>
}
>
<AppContent
currentTheme={currentTheme}
onThemeChange={handleThemeChange}
/>
</SocketProvider>
</ThemeProvider>
</LanguageProvider>
<ThemeProvider theme={dynamicTheme}>
<CssBaseline />
<SocketProvider
url={config.apiBaseUrl}
fallback={
<Box
sx={{
display: "flex",
justifyContent: "center",
alignItems: "center",
height: "100vh",
}}
>
<CircularProgress color="primary" />
</Box>
}
>
<AppContent
currentTheme={currentTheme}
onThemeChange={handleThemeChange}
/>
</SocketProvider>
</ThemeProvider>
);
};

View File

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

View File

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

View File

@@ -66,7 +66,6 @@ class PrerenderKonfigurator extends Component {
15%
</Typography>
<Typography variant="body2">
{/* Note: This is a prerender file - translation key would be: product.discount.from3Products */}
ab 3 Produkten
</Typography>
</Box>
@@ -75,7 +74,6 @@ class PrerenderKonfigurator extends Component {
24%
</Typography>
<Typography variant="body2">
{/* Note: This is a prerender file - translation key would be: product.discount.from5Products */}
ab 5 Produkten
</Typography>
</Box>
@@ -84,13 +82,11 @@ class PrerenderKonfigurator extends Component {
36%
</Typography>
<Typography variant="body2">
{/* Note: This is a prerender file - translation key would be: product.discount.from7Products */}
ab 7 Produkten
</Typography>
</Box>
</Box>
<Typography variant="caption" sx={{ color: '#666', mt: 1, display: 'block' }}>
{/* Note: This is a prerender file - translation key would be: product.discount.moreProductsMoreSavings */}
Je mehr Produkte du auswählst, desto mehr sparst du!
</Typography>
</Paper>

View File

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

View File

@@ -101,7 +101,7 @@ class PrerenderProduct extends React.Component {
React.createElement(
Typography,
{ variant: 'h6', color: 'text.secondary' },
(this.props.t ? this.props.t('product.articleNumber') : 'Artikelnummer')+': '+product.articleNumber+' '+(product.gtin ? ` | GTIN: ${product.gtin}` : "")
'Artikelnummer: '+product.articleNumber+' '+(product.gtin ? ` | GTIN: ${product.gtin}` : "")
),
React.createElement(
Box,

View File

@@ -10,7 +10,6 @@ import AddIcon from "@mui/icons-material/Add";
import RemoveIcon from "@mui/icons-material/Remove";
import ShoppingCartIcon from "@mui/icons-material/ShoppingCart";
import DeleteIcon from "@mui/icons-material/Delete";
import { withI18n } from "../i18n/withTranslation.js";
if (!Array.isArray(window.cart)) window.cart = [];
@@ -52,14 +51,11 @@ class AddToCartButton extends Component {
seoName: this.props.seoName,
pictureList: this.props.pictureList,
price: this.props.price,
fGrundPreis: this.props.fGrundPreis,
cGrundEinheit: this.props.cGrundEinheit,
quantity: 1,
weight: this.props.weight,
vat: this.props.vat,
versandklasse: this.props.versandklasse,
availableSupplier: this.props.availableSupplier,
komponenten: this.props.komponenten,
available: this.props.available
});
} else {
@@ -154,17 +150,12 @@ class AddToCartButton extends Component {
},
}}
>
{this.props.t ? this.props.t('cart.availableFrom', {
date: new Date(incoming).toLocaleDateString("de-DE", {
year: "numeric",
month: "long",
day: "numeric",
})
}) : `Ab ${new Date(incoming).toLocaleDateString("de-DE", {
Ab{" "}
{new Date(incoming).toLocaleDateString("de-DE", {
year: "numeric",
month: "long",
day: "numeric",
})}`}
})}
</Button>
);
}
@@ -190,9 +181,7 @@ class AddToCartButton extends Component {
},
}}
>
{this.props.steckling ?
(this.props.t ? this.props.t('cart.preorderCutting') : "Als Steckling vorbestellen") :
(this.props.t ? this.props.t('cart.addToCart') : "In den Korb")}
{this.props.steckling ? "Als Steckling vorbestellen" : "In den Korb"}
</Button>
);
}
@@ -270,7 +259,7 @@ class AddToCartButton extends Component {
<AddIcon />
</IconButton>
<Tooltip title={this.props.t ? this.props.t('cart.removeFromCart') : 'Aus dem Warenkorb entfernen'} arrow>
<Tooltip title="Aus dem Warenkorb entfernen" arrow>
<IconButton
color="inherit"
onClick={this.handleClearCart}
@@ -283,7 +272,7 @@ class AddToCartButton extends Component {
</IconButton>
</Tooltip>
{this.props.cartButton && (
<Tooltip title={this.props.t ? this.props.t('cart.openCart') : 'Warenkorb öffnen'} arrow>
<Tooltip title="Warenkorb öffnen" arrow>
<IconButton
color="inherit"
onClick={this.toggleCart}
@@ -313,7 +302,7 @@ class AddToCartButton extends Component {
fontWeight: "bold",
}}
>
{this.props.t ? this.props.t('product.outOfStock') : 'Out of Stock'}
Out of Stock
</Button>
);
}
@@ -338,9 +327,7 @@ class AddToCartButton extends Component {
},
}}
>
{this.props.steckling ?
(this.props.t ? this.props.t('cart.preorderCutting') : "Als Steckling vorbestellen") :
(this.props.t ? this.props.t('cart.addToCart') : "In den Korb")}
{this.props.steckling ? "Als Steckling vorbestellen" : "In den Korb"}
</Button>
);
}
@@ -417,7 +404,7 @@ class AddToCartButton extends Component {
<AddIcon />
</IconButton>
<Tooltip title={this.props.t ? this.props.t('cart.removeFromCart') : 'Aus dem Warenkorb entfernen'} arrow>
<Tooltip title="Aus dem Warenkorb entfernen" arrow>
<IconButton
color="inherit"
onClick={this.handleClearCart}
@@ -430,7 +417,7 @@ class AddToCartButton extends Component {
</IconButton>
</Tooltip>
{this.props.cartButton && (
<Tooltip title={this.props.t ? this.props.t('cart.openCart') : 'Warenkorb öffnen'} arrow>
<Tooltip title="Warenkorb öffnen" arrow>
<IconButton
color="inherit"
onClick={this.toggleCart}
@@ -449,4 +436,4 @@ class AddToCartButton extends Component {
}
}
export default withI18n()(AddToCartButton);
export default AddToCartButton;

View File

@@ -1,242 +0,0 @@
import React, { Component } from 'react';
import {
Box,
Typography,
TextField,
Button,
Paper,
Alert,
CircularProgress,
FormControl,
FormLabel,
RadioGroup,
FormControlLabel,
Radio
} from '@mui/material';
import { withI18n } from '../i18n/withTranslation.js';
class ArticleAvailabilityForm extends Component {
constructor(props) {
super(props);
this.state = {
name: '',
email: '',
telegramId: '',
notificationMethod: 'email',
message: '',
loading: false,
success: false,
error: null
};
}
handleInputChange = (field) => (event) => {
this.setState({ [field]: event.target.value });
};
handleNotificationMethodChange = (event) => {
this.setState({
notificationMethod: event.target.value,
// Clear the other field when switching methods
email: event.target.value === 'email' ? this.state.email : '',
telegramId: event.target.value === 'telegram' ? this.state.telegramId : ''
});
};
handleSubmit = (event) => {
event.preventDefault();
// Prepare data for API emission
const availabilityData = {
type: 'availability_inquiry',
productId: this.props.productId,
productName: this.props.productName,
name: this.state.name,
notificationMethod: this.state.notificationMethod,
email: this.state.notificationMethod === 'email' ? this.state.email : '',
telegramId: this.state.notificationMethod === 'telegram' ? this.state.telegramId : '',
message: this.state.message,
timestamp: new Date().toISOString()
};
// Emit data via socket
console.log('Availability Inquiry Data to emit:', availabilityData);
if (this.props.socket) {
this.props.socket.emit('availability_inquiry_submit', availabilityData);
// Set up response handler
this.props.socket.once('availability_inquiry_response', (response) => {
if (response.success) {
this.setState({
loading: false,
success: true,
name: '',
email: '',
telegramId: '',
notificationMethod: 'email',
message: ''
});
} else {
this.setState({
loading: false,
error: response.error || 'Ein Fehler ist aufgetreten'
});
}
// Clear messages after 3 seconds
setTimeout(() => {
this.setState({ success: false, error: null });
}, 3000);
});
}
this.setState({ loading: true });
// Fallback timeout in case backend doesn't respond
setTimeout(() => {
if (this.state.loading) {
this.setState({
loading: false,
success: true,
name: '',
email: '',
telegramId: '',
notificationMethod: 'email',
message: ''
});
// Clear success message after 3 seconds
setTimeout(() => {
this.setState({ success: false });
}, 3000);
}
}, 5000);
};
render() {
const { name, email, telegramId, notificationMethod, message, loading, success, error } = this.state;
return (
<Paper id="availability-form" sx={{ p: 3, mt: 4, borderRadius: 2, boxShadow: "0 2px 8px rgba(0,0,0,0.08)" }}>
<Typography variant="h5" gutterBottom sx={{ fontWeight: 600, color: '#333' }}>
Verfügbarkeit anfragen
</Typography>
<Typography variant="body2" color="text.secondary" sx={{ mb: 3 }}>
Dieser Artikel ist derzeit nicht verfügbar. Gerne informieren wir Sie, sobald er wieder lieferbar ist.
</Typography>
{success && (
<Alert severity="success" sx={{ mb: 3 }}>
Vielen Dank für Ihre Anfrage! Wir werden Sie {notificationMethod === 'email' ? 'per E-Mail' : 'über Telegram'} informieren, sobald der Artikel wieder verfügbar ist.
</Alert>
)}
{error && (
<Alert severity="error" sx={{ mb: 3 }}>
{error}
</Alert>
)}
<Box component="form" onSubmit={this.handleSubmit} sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
<TextField
label="Name"
value={name}
onChange={this.handleInputChange('name')}
required
fullWidth
disabled={loading}
placeholder="Ihr Name"
/>
<FormControl component="fieldset" disabled={loading}>
<FormLabel component="legend" sx={{ mb: 1 }}>
Wie möchten Sie benachrichtigt werden?
</FormLabel>
<RadioGroup
value={notificationMethod}
onChange={this.handleNotificationMethodChange}
row
>
<FormControlLabel
value="email"
control={<Radio />}
label="E-Mail"
/>
<FormControlLabel
value="telegram"
control={<Radio />}
label="Telegram Bot"
/>
</RadioGroup>
</FormControl>
{notificationMethod === 'email' && (
<TextField
label="E-Mail"
type="email"
value={email}
onChange={this.handleInputChange('email')}
required
fullWidth
disabled={loading}
placeholder="ihre.email@example.com"
/>
)}
{notificationMethod === 'telegram' && (
<TextField
label="Telegram ID"
value={telegramId}
onChange={this.handleInputChange('telegramId')}
required
fullWidth
disabled={loading}
placeholder="@IhrTelegramName oder Telegram ID"
helperText="Geben Sie Ihren Telegram-Benutzernamen (mit @) oder Ihre Telegram-ID ein"
/>
)}
<TextField
label="Nachricht (optional)"
value={message}
onChange={this.handleInputChange('message')}
fullWidth
multiline
rows={3}
disabled={loading}
placeholder="Zusätzliche Informationen oder Fragen..."
/>
<Button
type="submit"
variant="contained"
disabled={loading || !name || (notificationMethod === 'email' && !email) || (notificationMethod === 'telegram' && !telegramId)}
sx={{
mt: 2,
py: 1.5,
fontSize: '1rem',
fontWeight: 600,
backgroundColor: 'warning.main',
'&:hover': {
backgroundColor: 'warning.dark'
}
}}
>
{loading ? (
<>
<CircularProgress size={20} sx={{ mr: 1 }} />
Wird gesendet...
</>
) : (
'Verfügbarkeit anfragen'
)}
</Button>
</Box>
</Paper>
);
}
}
export default withI18n()(ArticleAvailabilityForm);

View File

@@ -1,235 +0,0 @@
import React, { Component } from 'react';
import {
Box,
Typography,
TextField,
Button,
Paper,
Alert,
CircularProgress
} from '@mui/material';
import { withI18n } from '../i18n/withTranslation.js';
import PhotoUpload from './PhotoUpload.js';
class ArticleQuestionForm extends Component {
constructor(props) {
super(props);
this.state = {
name: '',
email: '',
question: '',
photos: [],
loading: false,
success: false,
error: null
};
this.photoUploadRef = React.createRef();
}
handleInputChange = (field) => (event) => {
this.setState({ [field]: event.target.value });
};
handlePhotosChange = (files) => {
this.setState({ photos: files });
};
convertPhotosToBase64 = (photos) => {
return Promise.all(
photos.map(photo => {
return new Promise((resolve, _reject) => {
const reader = new FileReader();
reader.onload = (e) => {
resolve({
name: photo.name,
type: photo.type,
size: photo.size,
data: e.target.result // base64 string
});
};
reader.readAsDataURL(photo);
});
})
);
};
handleSubmit = async (event) => {
event.preventDefault();
this.setState({ loading: true });
try {
// Convert photos to base64
const photosBase64 = await this.convertPhotosToBase64(this.state.photos);
// Prepare data for API emission
const questionData = {
type: 'article_question',
productId: this.props.productId,
productName: this.props.productName,
name: this.state.name,
email: this.state.email,
question: this.state.question,
photos: photosBase64,
timestamp: new Date().toISOString()
};
// Emit data via socket
console.log('Article Question Data to emit:', questionData);
if (this.props.socket) {
this.props.socket.emit('article_question_submit', questionData);
// Set up response handler
this.props.socket.once('article_question_response', (response) => {
if (response.success) {
this.setState({
loading: false,
success: true,
name: '',
email: '',
question: '',
photos: []
});
// Reset the photo upload component
if (this.photoUploadRef.current) {
this.photoUploadRef.current.reset();
}
} else {
this.setState({
loading: false,
error: response.error || 'Ein Fehler ist aufgetreten'
});
}
// Clear messages after 3 seconds
setTimeout(() => {
this.setState({ success: false, error: null });
}, 3000);
});
}
} catch {
this.setState({
loading: false,
error: 'Fehler beim Verarbeiten der Fotos'
});
}
// Fallback timeout in case backend doesn't respond
setTimeout(() => {
if (this.state.loading) {
this.setState({
loading: false,
success: true,
name: '',
email: '',
question: '',
photos: []
});
// Reset the photo upload component
if (this.photoUploadRef.current) {
this.photoUploadRef.current.reset();
}
// Clear success message after 3 seconds
setTimeout(() => {
this.setState({ success: false });
}, 3000);
}
}, 5000);
};
render() {
const { name, email, question, loading, success, error } = this.state;
return (
<Paper sx={{ p: 3, mt: 4, borderRadius: 2, boxShadow: "0 2px 8px rgba(0,0,0,0.08)" }}>
<Typography variant="h5" gutterBottom sx={{ fontWeight: 600, color: '#333' }}>
Frage zum Artikel
</Typography>
<Typography variant="body2" color="text.secondary" sx={{ mb: 3 }}>
Haben Sie eine Frage zu diesem Artikel? Wir helfen Ihnen gerne weiter.
</Typography>
{success && (
<Alert severity="success" sx={{ mb: 3 }}>
Vielen Dank für Ihre Frage! Wir werden uns schnellstmöglich bei Ihnen melden.
</Alert>
)}
{error && (
<Alert severity="error" sx={{ mb: 3 }}>
{error}
</Alert>
)}
<Box component="form" onSubmit={this.handleSubmit} sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
<TextField
label="Name"
value={name}
onChange={this.handleInputChange('name')}
required
fullWidth
disabled={loading}
placeholder="Ihr Name"
/>
<TextField
label="E-Mail"
type="email"
value={email}
onChange={this.handleInputChange('email')}
required
fullWidth
disabled={loading}
placeholder="ihre.email@example.com"
/>
<TextField
label="Ihre Frage"
value={question}
onChange={this.handleInputChange('question')}
required
fullWidth
multiline
rows={4}
disabled={loading}
placeholder="Beschreiben Sie Ihre Frage zu diesem Artikel..."
/>
<PhotoUpload
ref={this.photoUploadRef}
onChange={this.handlePhotosChange}
disabled={loading}
maxFiles={3}
label="Fotos zur Frage anhängen (optional)"
/>
<Button
type="submit"
variant="contained"
disabled={loading || !name || !email || !question}
sx={{
mt: 2,
py: 1.5,
fontSize: '1rem',
fontWeight: 600
}}
>
{loading ? (
<>
<CircularProgress size={20} sx={{ mr: 1 }} />
Wird gesendet...
</>
) : (
'Frage senden'
)}
</Button>
</Box>
</Paper>
);
}
}
export default withI18n()(ArticleQuestionForm);

View File

@@ -1,264 +0,0 @@
import React, { Component } from 'react';
import {
Box,
Typography,
TextField,
Button,
Paper,
Alert,
CircularProgress,
Rating
} from '@mui/material';
import StarIcon from '@mui/icons-material/Star';
import { withI18n } from '../i18n/withTranslation.js';
import PhotoUpload from './PhotoUpload.js';
class ArticleRatingForm extends Component {
constructor(props) {
super(props);
this.state = {
name: '',
email: '',
rating: 0,
review: '',
photos: [],
loading: false,
success: false,
error: null
};
this.photoUploadRef = React.createRef();
}
handleInputChange = (field) => (event) => {
this.setState({ [field]: event.target.value });
};
handleRatingChange = (event, newValue) => {
this.setState({ rating: newValue });
};
handlePhotosChange = (files) => {
this.setState({ photos: files });
};
convertPhotosToBase64 = (photos) => {
return Promise.all(
photos.map(photo => {
return new Promise((resolve, _reject) => {
const reader = new FileReader();
reader.onload = (e) => {
resolve({
name: photo.name,
type: photo.type,
size: photo.size,
data: e.target.result // base64 string
});
};
reader.readAsDataURL(photo);
});
})
);
};
handleSubmit = async (event) => {
event.preventDefault();
this.setState({ loading: true });
try {
// Convert photos to base64
const photosBase64 = await this.convertPhotosToBase64(this.state.photos);
// Prepare data for API emission
const ratingData = {
type: 'article_rating',
productId: this.props.productId,
productName: this.props.productName,
name: this.state.name,
email: this.state.email,
rating: this.state.rating,
review: this.state.review,
photos: photosBase64,
timestamp: new Date().toISOString()
};
// Emit data via socket
console.log('Article Rating Data to emit:', ratingData);
if (this.props.socket) {
this.props.socket.emit('article_rating_submit', ratingData);
// Set up response handler
this.props.socket.once('article_rating_response', (response) => {
if (response.success) {
this.setState({
loading: false,
success: true,
name: '',
email: '',
rating: 0,
review: '',
photos: []
});
// Reset the photo upload component
if (this.photoUploadRef.current) {
this.photoUploadRef.current.reset();
}
} else {
this.setState({
loading: false,
error: response.error || 'Ein Fehler ist aufgetreten'
});
}
// Clear messages after 3 seconds
setTimeout(() => {
this.setState({ success: false, error: null });
}, 3000);
});
}
} catch {
this.setState({
loading: false,
error: 'Fehler beim Verarbeiten der Fotos'
});
}
// Fallback timeout in case backend doesn't respond
setTimeout(() => {
if (this.state.loading) {
this.setState({
loading: false,
success: true,
name: '',
email: '',
rating: 0,
review: '',
photos: []
});
// Reset the photo upload component
if (this.photoUploadRef.current) {
this.photoUploadRef.current.reset();
}
// Clear success message after 3 seconds
setTimeout(() => {
this.setState({ success: false });
}, 3000);
}
}, 5000);
};
render() {
const { name, email, rating, review, loading, success, error } = this.state;
return (
<Paper sx={{ p: 3, mt: 4, borderRadius: 2, boxShadow: "0 2px 8px rgba(0,0,0,0.08)" }}>
<Typography variant="h5" gutterBottom sx={{ fontWeight: 600, color: '#333' }}>
Artikel Bewerten
</Typography>
<Typography variant="body2" color="text.secondary" sx={{ mb: 3 }}>
Teilen Sie Ihre Erfahrungen mit diesem Artikel und helfen Sie anderen Kunden bei der Entscheidung.
</Typography>
{success && (
<Alert severity="success" sx={{ mb: 3 }}>
Vielen Dank für Ihre Bewertung! Sie wird nach Prüfung veröffentlicht.
</Alert>
)}
{error && (
<Alert severity="error" sx={{ mb: 3 }}>
{error}
</Alert>
)}
<Box component="form" onSubmit={this.handleSubmit} sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
<TextField
label="Name"
value={name}
onChange={this.handleInputChange('name')}
required
fullWidth
disabled={loading}
placeholder="Ihr Name"
/>
<TextField
label="E-Mail"
type="email"
value={email}
onChange={this.handleInputChange('email')}
required
fullWidth
disabled={loading}
placeholder="ihre.email@example.com"
helperText="Ihre E-Mail wird nicht veröffentlicht"
/>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
<Typography variant="body2" sx={{ fontWeight: 500 }}>
Bewertung *
</Typography>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<Rating
name="article-rating"
value={rating}
onChange={this.handleRatingChange}
size="large"
disabled={loading}
emptyIcon={<StarIcon style={{ opacity: 0.55 }} fontSize="inherit" />}
/>
<Typography variant="body2" color="text.secondary">
{rating > 0 ? `${rating} von 5 Sternen` : 'Bitte bewerten'}
</Typography>
</Box>
</Box>
<TextField
label="Ihre Bewertung (optional)"
value={review}
onChange={this.handleInputChange('review')}
fullWidth
multiline
rows={4}
disabled={loading}
placeholder="Beschreiben Sie Ihre Erfahrungen mit diesem Artikel..."
/>
<PhotoUpload
ref={this.photoUploadRef}
onChange={this.handlePhotosChange}
disabled={loading}
maxFiles={5}
label="Fotos zur Bewertung anhängen (optional)"
/>
<Button
type="submit"
variant="contained"
disabled={loading || !name || !email || rating === 0}
sx={{
mt: 2,
py: 1.5,
fontSize: '1rem',
fontWeight: 600
}}
>
{loading ? (
<>
<CircularProgress size={20} sx={{ mr: 1 }} />
Wird gesendet...
</>
) : (
'Bewertung abgeben'
)}
</Button>
</Box>
</Paper>
);
}
}
export default withI18n()(ArticleRatingForm);

View File

@@ -8,7 +8,6 @@ import TableBody from '@mui/material/TableBody';
import TableCell from '@mui/material/TableCell';
import TableRow from '@mui/material/TableRow';
import CartItem from './CartItem.js';
import { withI18n } from '../i18n/withTranslation.js';
class CartDropdown extends Component {
@@ -54,8 +53,8 @@ class CartDropdown extends Component {
currency: 'EUR'
});
const shippingNetPrice = deliveryCost > 0 ? deliveryCost / (1 + 19 / 100) : 0;
const shippingVat = deliveryCost > 0 ? deliveryCost - shippingNetPrice : 0;
const shippingNetPrice = deliveryCost / (1 + 19 / 100);
const shippingVat = deliveryCost - shippingNetPrice;
const totalVat7 = priceCalculations.vat7;
const totalVat19 = priceCalculations.vat19 + shippingVat;
const totalGross = priceCalculations.totalGross + deliveryCost;
@@ -64,7 +63,7 @@ class CartDropdown extends Component {
<>
<Box sx={{ bgcolor: 'primary.main', color: 'white', p: 2 }}>
<Typography variant="h6">
{cartItems.length} {cartItems.length === 1 ? (this.props.t ? this.props.t('cart.itemCount.singular') : 'Produkt') : (this.props.t ? this.props.t('cart.itemCount.plural') : 'Produkte')}
{cartItems.length} {cartItems.length === 1 ? 'Produkt' : 'Produkte'}
</Typography>
</Box>
@@ -84,7 +83,7 @@ class CartDropdown extends Component {
{/* Display total weight if greater than 0 */}
{totalWeight > 0 && (
<Typography variant="subtitle2" sx={{ px: 2, mb: 1 }}>
{this.props.t ? this.props.t('cart.summary.totalWeight', { weight: totalWeight.toFixed(2) }) : `Gesamtgewicht: ${totalWeight.toFixed(2)} kg`}
Gesamtgewicht: {totalWeight.toFixed(2)} kg
</Typography>
)}
@@ -95,7 +94,7 @@ class CartDropdown extends Component {
// Detailed summary with shipping costs
<>
<Typography variant="h6" gutterBottom sx={{ mt: 2 }}>
{this.props.t ? this.props.t('cart.summary.title') : 'Bestellübersicht'}
Bestellübersicht
</Typography>
{deliveryMethod && (
<Typography variant="body2" sx={{ mb: 1, color: 'text.secondary' }}>
@@ -105,14 +104,14 @@ class CartDropdown extends Component {
<Table size="small">
<TableBody>
<TableRow>
<TableCell>{this.props.t ? this.props.t('cart.summary.goodsNet') : 'Waren (netto):'}</TableCell>
<TableCell>Waren (netto):</TableCell>
<TableCell align="right">
{currencyFormatter.format(priceCalculations.totalNet)}
</TableCell>
</TableRow>
{deliveryCost > 0 && (
<TableRow>
<TableCell>{this.props.t ? this.props.t('cart.summary.shippingNet') : 'Versandkosten (netto):'}</TableCell>
<TableCell>Versandkosten (netto):</TableCell>
<TableCell align="right">
{currencyFormatter.format(shippingNetPrice)}
</TableCell>
@@ -120,7 +119,7 @@ class CartDropdown extends Component {
)}
{totalVat7 > 0 && (
<TableRow>
<TableCell>{this.props.t ? this.props.t('tax.vat7') : '7% Mehrwertsteuer'}:</TableCell>
<TableCell>7% Mehrwertsteuer:</TableCell>
<TableCell align="right">
{currencyFormatter.format(totalVat7)}
</TableCell>
@@ -128,37 +127,28 @@ class CartDropdown extends Component {
)}
{totalVat19 > 0 && (
<TableRow>
<TableCell>{this.props.t ? this.props.t('tax.vat19WithShipping') : '19% Mehrwertsteuer (inkl. Versand)'}:</TableCell>
<TableCell>19% Mehrwertsteuer (inkl. Versand):</TableCell>
<TableCell align="right">
{currencyFormatter.format(totalVat19)}
</TableCell>
</TableRow>
)}
<TableRow>
<TableCell sx={{ fontWeight: 'bold' }}>{this.props.t ? this.props.t('cart.summary.totalGoods') : 'Gesamtsumme Waren:'}</TableCell>
<TableCell sx={{ fontWeight: 'bold' }}>Gesamtsumme Waren:</TableCell>
<TableCell align="right" sx={{ fontWeight: 'bold' }}>
{currencyFormatter.format(priceCalculations.totalGross)}
</TableCell>
</TableRow>
<TableRow>
<TableCell sx={{ fontWeight: 'bold' }}>
{this.props.t ? this.props.t('cart.summary.shippingCosts') : 'Versandkosten:'}
{deliveryCost === 0 && priceCalculations.totalGross < 100 && (
<span style={{ color: '#2e7d32', fontSize: '0.8em', marginLeft: '4px' }}>
{this.props.t ? this.props.t('cart.summary.freeFrom100') : '(kostenlos ab 100€)'}
</span>
)}
</TableCell>
<TableCell align="right" sx={{ fontWeight: 'bold' }}>
{deliveryCost === 0 ? (
<span style={{ color: '#2e7d32' }}>{this.props.t ? this.props.t('cart.summary.free') : 'kostenlos'}</span>
) : (
currencyFormatter.format(deliveryCost)
)}
</TableCell>
</TableRow>
{deliveryCost > 0 && (
<TableRow>
<TableCell sx={{ fontWeight: 'bold' }}>Versandkosten:</TableCell>
<TableCell align="right" sx={{ fontWeight: 'bold' }}>
{currencyFormatter.format(deliveryCost)}
</TableCell>
</TableRow>
)}
<TableRow sx={{ borderTop: '1px solid #e0e0e0' }}>
<TableCell sx={{ fontWeight: 'bold', pt: 2 }}>{this.props.t ? this.props.t('cart.summary.total') : 'Gesamtsumme:'}</TableCell>
<TableCell sx={{ fontWeight: 'bold', pt: 2 }}>Gesamtsumme:</TableCell>
<TableCell align="right" sx={{ fontWeight: 'bold', pt: 2 }}>
{currencyFormatter.format(totalGross)}
</TableCell>
@@ -171,14 +161,14 @@ class CartDropdown extends Component {
<Table size="small">
<TableBody>
<TableRow>
<TableCell>{this.props.t ? this.props.t('tax.totalNet') : 'Gesamtnettopreis'}:</TableCell>
<TableCell>Gesamtnettopreis:</TableCell>
<TableCell align="right">
{new Intl.NumberFormat('de-DE', {style: 'currency', currency: 'EUR'}).format(priceCalculations.totalNet)}
</TableCell>
</TableRow>
{priceCalculations.vat7 > 0 && (
<TableRow>
<TableCell>{this.props.t ? this.props.t('tax.vat7') : '7% Mehrwertsteuer'}:</TableCell>
<TableCell>7% Mehrwertsteuer:</TableCell>
<TableCell align="right">
{new Intl.NumberFormat('de-DE', {style: 'currency', currency: 'EUR'}).format(priceCalculations.vat7)}
</TableCell>
@@ -186,14 +176,14 @@ class CartDropdown extends Component {
)}
{priceCalculations.vat19 > 0 && (
<TableRow>
<TableCell>{this.props.t ? this.props.t('tax.vat19') : '19% Mehrwertsteuer'}:</TableCell>
<TableCell>19% Mehrwertsteuer:</TableCell>
<TableCell align="right">
{new Intl.NumberFormat('de-DE', {style: 'currency', currency: 'EUR'}).format(priceCalculations.vat19)}
</TableCell>
</TableRow>
)}
<TableRow>
<TableCell sx={{ fontWeight: 'bold' }}>{this.props.t ? this.props.t('tax.totalGross') : 'Gesamtbruttopreis ohne Versand'}:</TableCell>
<TableCell sx={{ fontWeight: 'bold' }}>Gesamtbruttopreis ohne Versand:</TableCell>
<TableCell align="right" sx={{ fontWeight: 'bold' }}>
{new Intl.NumberFormat('de-DE', {style: 'currency', currency: 'EUR'}).format(priceCalculations.totalGross)}
</TableCell>
@@ -211,7 +201,7 @@ class CartDropdown extends Component {
fullWidth
onClick={onClose}
>
{this.props.t ? this.props.t('cart.continueShopping') : 'Weiter einkaufen'}
Weiter einkaufen
</Button>
)}
@@ -223,7 +213,7 @@ class CartDropdown extends Component {
sx={{ mt: 2 }}
onClick={onCheckout}
>
{this.props.t ? this.props.t('cart.proceedToCheckout') : 'Weiter zur Kasse'}
Weiter zur Kasse
</Button>
)}
</>
@@ -233,4 +223,4 @@ class CartDropdown extends Component {
}
}
export default withI18n()(CartDropdown);
export default CartDropdown;

View File

@@ -6,7 +6,6 @@ import Typography from '@mui/material/Typography';
import Box from '@mui/material/Box';
import { Link } from 'react-router-dom';
import AddToCartButton from './AddToCartButton.js';
import { withI18n } from '../i18n/withTranslation.js';
class CartItem extends Component {
@@ -117,7 +116,7 @@ class CartItem extends Component {
)}
{item.versandklasse && item.versandklasse != 'standard' && item.versandklasse != 'kostenlos' && (
<Typography variant="body2" color="warning.main" fontWeight="medium" component="div">
{item.versandklasse == 'nur Abholung' ? this.props.t('delivery.descriptions.pickupOnly') : item.versandklasse}
{item.versandklasse}
</Typography>
)}
{item.vat && (
@@ -127,9 +126,9 @@ class CartItem extends Component {
fontStyle="italic"
component="div"
>
{this.props.t ? this.props.t('product.inclShort') : 'inkl.'} {new Intl.NumberFormat('de-DE', {style: 'currency', currency: 'EUR'}).format(
inkl. {new Intl.NumberFormat('de-DE', {style: 'currency', currency: 'EUR'}).format(
(item.price * item.quantity) - ((item.price * item.quantity) / (1 + item.vat / 100))
)} {this.props.t ? this.props.t('product.vatShort') : 'MwSt.'} ({item.vat}%)
)} MwSt. ({item.vat}%)
</Typography>
)}
@@ -147,14 +146,11 @@ class CartItem extends Component {
display: "block"
}}
>
{this.props.id.toString().endsWith("steckling") ?
(this.props.t ? this.props.t('delivery.times.cutting14Days') : "Lieferzeit: 14 Tage") :
item.available == 1 ?
(this.props.t ? this.props.t('delivery.times.standard2to3Days') : "Lieferzeit: 2-3 Tage") :
item.availableSupplier == 1 ?
(this.props.t ? this.props.t('delivery.times.supplier7to9Days') : "Lieferzeit: 7-9 Tage") : ""}
{this.props.id.toString().endsWith("steckling") ? "Lieferzeit: 14 Tage" :
item.available == 1 ? "Lieferzeit: 2-3 Tage" :
item.availableSupplier == 1 ? "Lieferzeit: 7-9 Tage" : ""}
</Typography>
<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}/>
<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}/>
</Box>
</Box>
</ListItem>
@@ -163,4 +159,4 @@ class CartItem extends Component {
}
}
export default withI18n()(CartItem);
export default CartItem;

View File

@@ -16,7 +16,7 @@ const CategoryBox = ({
name,
seoName,
bgcolor,
fontSize = '1.2rem',
fontSize = '0.8rem',
...props
}) => {
const [imageUrl, setImageUrl] = useState(null);
@@ -186,7 +186,7 @@ const CategoryBox = ({
fontFamily: 'SwashingtonCP, "Times New Roman", Georgia, serif',
fontWeight: 'normal',
lineHeight: '1.2',
padding: '12px 8px'
padding: '0 8px'
}}>
{name}
</div>

View File

@@ -13,7 +13,6 @@ import CategoryBox from './CategoryBox.js';
import { useParams, useSearchParams } from 'react-router-dom';
import { getAllSettingsWithPrefix } from '../utils/sessionStorage.js';
import { withI18n } from '../i18n/withTranslation.js';
const isNew = (neu) => neu && (new Date().getTime() - new Date(neu).getTime() < 30 * 24 * 60 * 60 * 1000);
@@ -53,7 +52,7 @@ function getCachedCategoryData(categoryId) {
function getFilteredProducts(unfilteredProducts, attributes, t) {
function getFilteredProducts(unfilteredProducts, attributes) {
const attributeSettings = getAllSettingsWithPrefix('filter_attribute_');
const manufacturerSettings = getAllSettingsWithPrefix('filter_manufacturer_');
const availabilitySettings = getAllSettingsWithPrefix('filter_availability_');
@@ -150,17 +149,17 @@ function getFilteredProducts(unfilteredProducts, attributes, t) {
// Check for "auf Lager" filter (in stock) - it's active when filter_availability is NOT set to '1'
if (availabilityFilter !== '1') {
activeAvailabilityFilters.push({id: '1', name: t ? t('product.inStock') : 'auf Lager'});
activeAvailabilityFilters.push({id: '1', name: 'auf Lager'});
}
// Check for "Neu" filter (new) - only show if there are actually new products and filter is active
if (availabilityFilters.includes('2') && hasNewProducts) {
activeAvailabilityFilters.push({id: '2', name: t ? t('product.new') : 'Neu'});
activeAvailabilityFilters.push({id: '2', name: 'Neu'});
}
// Check for "Bald verfügbar" filter (coming soon) - only show if there are actually coming soon products and filter is active
if (availabilityFilters.includes('3') && hasComingSoonProducts) {
activeAvailabilityFilters.push({id: '3', name: t ? t('product.comingSoon') : 'Bald verfügbar'});
activeAvailabilityFilters.push({id: '3', name: 'Bald verfügbar'});
}
return {filteredProducts,activeAttributeFilters:activeAttributeFiltersWithNames,activeManufacturerFilters:activeManufacturerFiltersWithNames,activeAvailabilityFilters};
@@ -257,8 +256,7 @@ class Content extends Component {
unfilteredProducts: unfilteredProducts,
...getFilteredProducts(
unfilteredProducts,
response.attributes,
this.props.t
response.attributes
),
categoryName: response.categoryName || response.name || null,
dataType: response.dataType,
@@ -387,8 +385,7 @@ class Content extends Component {
this.setState({
...getFilteredProducts(
this.state.unfilteredProducts,
this.state.attributes,
this.props.t
this.state.attributes
)
});
}
@@ -605,7 +602,7 @@ class Content extends Component {
{(this.getCurrentCategoryId() == 706 || this.getCurrentCategoryId() == 689) &&
<Box sx={{ display: { xs: 'none', sm: 'block' } }}>
<Typography variant="h6" sx={{mt:3}}>
{this.props.t ? this.props.t('navigation.otherCategories') : 'Andere Kategorien'}
Andere Kategorien
</Typography>
</Box>
}
@@ -650,7 +647,7 @@ class Content extends Component {
p: 2,
}}>
<Typography sx={{ fontSize: '1.3rem', color: 'white', fontFamily: 'SwashingtonCP' }}>
{this.props.t('sections.seeds')}
Seeds
</Typography>
</Box>
</Box>
@@ -697,7 +694,7 @@ class Content extends Component {
p: 2,
}}>
<Typography sx={{ fontSize: '1.3rem', color: 'white', fontFamily: 'SwashingtonCP' }}>
{this.props.t('sections.stecklinge')}
Stecklinge
</Typography>
</Box>
</Box>
@@ -726,4 +723,4 @@ class Content extends Component {
}
}
export default withRouter(withI18n()(Content));
export default withRouter(Content);

View File

@@ -6,7 +6,6 @@ import Link from '@mui/material/Link';
import { Link as RouterLink } from 'react-router-dom';
import { styled } from '@mui/material/styles';
import Paper from '@mui/material/Paper';
import { withI18n } from '../i18n/withTranslation.js';
// Styled component for the router links
const StyledRouterLink = styled(RouterLink)(() => ({
@@ -230,9 +229,9 @@ class Footer extends Component {
alignItems={{ xs: 'center', md: 'left' }}
flexWrap="wrap"
>
<StyledRouterLink to="/datenschutz">{this.props.t ? this.props.t('footer.legal.datenschutz') : 'Datenschutz'}</StyledRouterLink>
<StyledRouterLink to="/agb">{this.props.t ? this.props.t('footer.legal.agb') : 'AGB'}</StyledRouterLink>
<StyledRouterLink to="/sitemap">{this.props.t ? this.props.t('footer.legal.sitemap') : 'Sitemap'}</StyledRouterLink>
<StyledRouterLink to="/datenschutz">Datenschutz</StyledRouterLink>
<StyledRouterLink to="/agb">AGB</StyledRouterLink>
<StyledRouterLink to="/sitemap">Sitemap</StyledRouterLink>
</Stack>
<Stack
@@ -242,9 +241,9 @@ class Footer extends Component {
alignItems={{ xs: 'center', md: 'left' }}
flexWrap="wrap"
>
<StyledRouterLink to="/impressum">{this.props.t ? this.props.t('footer.legal.impressum') : 'Impressum'}</StyledRouterLink>
<StyledRouterLink to="/batteriegesetzhinweise">{this.props.t ? this.props.t('footer.legal.batteriegesetzhinweise') : 'Batteriegesetzhinweise'}</StyledRouterLink>
<StyledRouterLink to="/widerrufsrecht">{this.props.t ? this.props.t('footer.legal.widerrufsrecht') : 'Widerrufsrecht'}</StyledRouterLink>
<StyledRouterLink to="/impressum">Impressum</StyledRouterLink>
<StyledRouterLink to="/batteriegesetzhinweise">Batteriegesetzhinweise</StyledRouterLink>
<StyledRouterLink to="/widerrufsrecht">Widerrufsrecht</StyledRouterLink>
</Stack>
{/* Payment Methods Section */}
@@ -339,7 +338,7 @@ class Footer extends Component {
{/* Copyright Section */}
<Box sx={{ pb:'20px',textAlign: 'center', filter: 'drop-shadow(0 4px 8px rgba(0, 0, 0, 0.3))', opacity: 0.7 }}>
<Typography variant="body2" sx={{ mb: 1, fontSize: { xs: '11px', md: '14px' }, lineHeight: 1.5 }}>
{this.props.t ? this.props.t('footer.allPricesIncl') : '* Alle Preise inkl. gesetzlicher USt., zzgl. Versand'}
* Alle Preise inkl. gesetzlicher USt., zzgl. Versand
</Typography>
<Typography variant="body2" sx={{ fontSize: { xs: '11px', md: '14px' }, lineHeight: 1.5 }}>
© {new Date().getFullYear()} <StyledDomainLink href="https://growheads.de" target="_blank" rel="noopener noreferrer">GrowHeads.de</StyledDomainLink>
@@ -352,4 +351,4 @@ class Footer extends Component {
}
}
export default withI18n()(Footer);
export default Footer;

View File

@@ -2,7 +2,6 @@ import React, { Component } from 'react';
import Button from '@mui/material/Button';
import GoogleIcon from '@mui/icons-material/Google';
import GoogleAuthContext from '../contexts/GoogleAuthContext.js';
import { withI18n } from '../i18n/index.js';
class GoogleLoginButton extends Component {
static contextType = GoogleAuthContext;
@@ -187,7 +186,7 @@ class GoogleLoginButton extends Component {
};
render() {
const { disabled, style, className, text = (this.props.t ? this.props.t('auth.loginWithGoogle') : 'Mit Google anmelden') } = this.props;
const { disabled, style, className, text = 'Mit Google anmelden' } = this.props;
const { isInitializing, isPrompting } = this.state;
const isLoading = isInitializing || isPrompting || (this.context && !this.context.isLoaded);
@@ -206,4 +205,4 @@ class GoogleLoginButton extends Component {
}
}
export default withI18n(GoogleLoginButton);
export default GoogleLoginButton;

View File

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

View File

@@ -1,269 +0,0 @@
import React, { Component } from 'react';
import Box from '@mui/material/Box';
import Button from '@mui/material/Button';
import Menu from '@mui/material/Menu';
import MenuItem from '@mui/material/MenuItem';
import Typography from '@mui/material/Typography';
import { withI18n } from '../i18n/withTranslation.js';
class LanguageSwitcher extends Component {
constructor(props) {
super(props);
this.state = {
anchorEl: null,
loadedFlags: {}
};
}
handleClick = (event) => {
this.setState({ anchorEl: event.currentTarget });
};
handleClose = () => {
this.setState({ anchorEl: null });
};
handleLanguageChange = (language) => {
const { languageContext } = this.props;
if (languageContext) {
languageContext.changeLanguage(language);
}
this.handleClose();
};
// Lazy load flag components
loadFlagComponent = async (lang) => {
if (this.state.loadedFlags[lang]) {
return this.state.loadedFlags[lang];
}
try {
const flagMap = {
'ar': () => import('country-flag-icons/react/3x2').then(m => m.EG),
'bg': () => import('country-flag-icons/react/3x2').then(m => m.BG),
'cs': () => import('country-flag-icons/react/3x2').then(m => m.CZ),
'de': () => import('country-flag-icons/react/3x2').then(m => m.DE),
'el': () => import('country-flag-icons/react/3x2').then(m => m.GR),
'en': () => import('country-flag-icons/react/3x2').then(m => m.US),
'es': () => import('country-flag-icons/react/3x2').then(m => m.ES),
'fr': () => import('country-flag-icons/react/3x2').then(m => m.FR),
'hr': () => import('country-flag-icons/react/3x2').then(m => m.HR),
'hu': () => import('country-flag-icons/react/3x2').then(m => m.HU),
'it': () => import('country-flag-icons/react/3x2').then(m => m.IT),
'pl': () => import('country-flag-icons/react/3x2').then(m => m.PL),
'ro': () => import('country-flag-icons/react/3x2').then(m => m.RO),
'ru': () => import('country-flag-icons/react/3x2').then(m => m.RU),
'sk': () => import('country-flag-icons/react/3x2').then(m => m.SK),
'sl': () => import('country-flag-icons/react/3x2').then(m => m.SI),
'sr': () => import('country-flag-icons/react/3x2').then(m => m.RS),
'sv': () => import('country-flag-icons/react/3x2').then(m => m.SE),
'tr': () => import('country-flag-icons/react/3x2').then(m => m.TR),
'uk': () => import('country-flag-icons/react/3x2').then(m => m.UA),
'zh': () => import('country-flag-icons/react/3x2').then(m => m.CN)
};
const flagLoader = flagMap[lang];
if (flagLoader) {
const FlagComponent = await flagLoader();
this.setState(prevState => ({
loadedFlags: {
...prevState.loadedFlags,
[lang]: FlagComponent
}
}));
return FlagComponent;
}
} catch (error) {
console.warn(`Failed to load flag for language: ${lang}`, error);
}
return null;
};
getLanguageFlag = (lang) => {
const FlagComponent = this.state.loadedFlags[lang];
if (FlagComponent) {
return (
<FlagComponent
style={{
width: '20px',
height: '14px',
borderRadius: '2px',
border: '1px solid #ddd'
}}
/>
);
}
// Loading placeholder or fallback
return (
<Box
component="span"
sx={{
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
minWidth: '20px',
height: '14px',
backgroundColor: '#f5f5f5',
color: '#666',
fontSize: '8px',
fontWeight: 'bold',
borderRadius: '2px',
fontFamily: 'monospace',
border: '1px solid #ddd'
}}
>
{this.getLanguageLabel(lang)}
</Box>
);
};
// Load flags when menu opens
componentDidUpdate(prevProps, prevState) {
const { anchorEl } = this.state;
const { languageContext } = this.props;
if (anchorEl && !prevState.anchorEl && languageContext) {
// Menu just opened, lazy load all flags
languageContext.availableLanguages.forEach(lang => {
if (!this.state.loadedFlags[lang]) {
this.loadFlagComponent(lang);
}
});
}
}
getLanguageLabel = (lang) => {
const labels = {
'ar': 'EG',
'bg': 'BG',
'cs': 'CZ',
'de': 'DE',
'el': 'GR',
'en': 'US',
'es': 'ES',
'fr': 'FR',
'hr': 'HR',
'hu': 'HU',
'it': 'IT',
'pl': 'PL',
'ro': 'RO',
'ru': 'RU',
'sk': 'SK',
'sl': 'SI',
'sr': 'RS',
'sv': 'SE',
'tr': 'TR',
'uk': 'UA',
'zh': 'CN'
};
return labels[lang] || lang.toUpperCase();
};
getLanguageName = (lang) => {
const names = {
'ar': 'العربية',
'bg': 'Български',
'cs': 'Čeština',
'de': 'Deutsch',
'el': 'Ελληνικά',
'en': 'English',
'es': 'Español',
'fr': 'Français',
'hr': 'Hrvatski',
'hu': 'Magyar',
'it': 'Italiano',
'pl': 'Polski',
'ro': 'Română',
'ru': 'Русский',
'sk': 'Slovenčina',
'sl': 'Slovenščina',
'sr': 'Српски',
'sv': 'Svenska',
'tr': 'Türkçe',
'uk': 'Українська',
'zh': '中文'
};
return names[lang] || lang;
};
render() {
const { languageContext } = this.props;
const { anchorEl } = this.state;
if (!languageContext) {
return null;
}
const { currentLanguage, availableLanguages } = languageContext;
const open = Boolean(anchorEl);
return (
<Box sx={{ display: 'flex', alignItems: 'center' }}>
<Button
aria-controls={open ? 'language-menu' : undefined}
aria-haspopup="true"
aria-expanded={open ? 'true' : undefined}
onClick={this.handleClick}
color="inherit"
size="small"
sx={{
my: 1,
mx: 0.5,
minWidth: 'auto',
textTransform: 'none',
fontSize: '0.875rem'
}}
>
{this.getLanguageLabel(currentLanguage)}
</Button>
<Menu
id="language-menu"
anchorEl={anchorEl}
open={open}
onClose={this.handleClose}
disableScrollLock={true}
MenuListProps={{
'aria-labelledby': 'language-button',
}}
anchorOrigin={{
vertical: 'bottom',
horizontal: 'right',
}}
transformOrigin={{
vertical: 'top',
horizontal: 'right',
}}
>
{availableLanguages.map((language) => (
<MenuItem
key={language}
onClick={() => this.handleLanguageChange(language)}
selected={language === currentLanguage}
sx={{
minWidth: 160,
display: 'flex',
justifyContent: 'space-between',
gap: 2
}}
>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
{this.getLanguageFlag(language)}
<Typography variant="body2">
{this.getLanguageName(language)}
</Typography>
</Box>
<Typography variant="caption" color="text.secondary" sx={{ ml: 'auto' }}>
{this.getLanguageLabel(language)}
</Typography>
</MenuItem>
))}
</Menu>
</Box>
);
}
}
export default withI18n()(LanguageSwitcher);

View File

@@ -22,7 +22,6 @@ import GoogleLoginButton from './GoogleLoginButton.js';
import CartSyncDialog from './CartSyncDialog.js';
import { localAndArchiveServer, mergeCarts } from '../utils/cartUtils.js';
import config from '../config.js';
import { withI18n } from '../i18n/withTranslation.js';
// Lazy load GoogleAuthProvider
const GoogleAuthProvider = lazy(() => import('../providers/GoogleAuthProvider.js'));
@@ -511,7 +510,7 @@ export class LoginComponent extends Component {
color={isAdmin ? 'secondary' : 'inherit'}
sx={{ my: 1, mx: 1.5 }}
>
{this.props.t ? this.props.t('auth.profile') : 'Profil'}
Profil
</Button>
<Menu
disableScrollLock={true}
@@ -527,28 +526,14 @@ export class LoginComponent extends Component {
horizontal: 'right',
}}
>
<MenuItem component={Link} to="/profile" onClick={this.handleUserMenuClose}>
{this.props.t ? this.props.t('auth.menu.profile') : 'Profil'}
</MenuItem>
<MenuItem component={Link} to="/profile#cart" onClick={this.handleUserMenuClose} sx={{ pl: 4 }}>
{this.props.t ? this.props.t('auth.menu.checkout') : 'Bestellabschluss'}
</MenuItem>
<MenuItem component={Link} to="/profile#orders" onClick={this.handleUserMenuClose} sx={{ pl: 4 }}>
{this.props.t ? this.props.t('auth.menu.orders') : 'Bestellungen'}
</MenuItem>
<MenuItem component={Link} to="/profile#settings" onClick={this.handleUserMenuClose} sx={{ pl: 4 }}>
{this.props.t ? this.props.t('auth.menu.settings') : 'Einstellungen'}
</MenuItem>
<MenuItem component={Link} to="/profile" onClick={this.handleUserMenuClose}>Profil</MenuItem>
<MenuItem component={Link} to="/profile#cart" onClick={this.handleUserMenuClose} sx={{ pl: 4 }}>Bestellabschluss</MenuItem>
<MenuItem component={Link} to="/profile#orders" onClick={this.handleUserMenuClose} sx={{ pl: 4 }}>Bestellungen</MenuItem>
<MenuItem component={Link} to="/profile#settings" onClick={this.handleUserMenuClose} sx={{ pl: 4 }}>Einstellungen</MenuItem>
<Divider />
{isAdmin ? <MenuItem component={Link} to="/admin" onClick={this.handleUserMenuClose}>
{this.props.t ? this.props.t('auth.menu.adminDashboard') : 'Admin Dashboard'}
</MenuItem> : null}
{isAdmin ? <MenuItem component={Link} to="/admin/users" onClick={this.handleUserMenuClose}>
{this.props.t ? this.props.t('auth.menu.adminUsers') : 'Admin Users'}
</MenuItem> : null}
<MenuItem onClick={this.handleLogout}>
{this.props.t ? this.props.t('auth.logout') : 'Abmelden'}
</MenuItem>
{isAdmin ? <MenuItem component={Link} to="/admin" onClick={this.handleUserMenuClose}>Admin Dashboard</MenuItem> : null}
{isAdmin ? <MenuItem component={Link} to="/admin/users" onClick={this.handleUserMenuClose}>Admin Users</MenuItem> : null}
<MenuItem onClick={this.handleLogout}>Abmelden</MenuItem>
</Menu>
</>
) : (
@@ -558,7 +543,7 @@ export class LoginComponent extends Component {
onClick={this.handleOpen}
sx={{ my: 1, mx: 1.5 }}
>
{this.props.t ? this.props.t('auth.login') : 'Login'}
Login
</Button>
)
)}
@@ -573,10 +558,7 @@ export class LoginComponent extends Component {
<DialogTitle sx={{ bgcolor: 'white', pb: 0 }}>
<Box display="flex" justifyContent="space-between" alignItems="center">
<Typography variant="h6" color="#2e7d32" fontWeight="bold">
{tabValue === 0 ?
(this.props.t ? this.props.t('auth.login') : 'Anmelden') :
(this.props.t ? this.props.t('auth.register') : 'Registrieren')
}
{tabValue === 0 ? 'Anmelden' : 'Registrieren'}
</Typography>
<IconButton edge="end" onClick={this.handleClose} aria-label="close">
<CloseIcon />
@@ -596,14 +578,14 @@ export class LoginComponent extends Component {
textColor="inherit"
>
<Tab
label={this.props.t ? this.props.t('auth.login').toUpperCase() : "ANMELDEN"}
label="ANMELDEN"
sx={{
color: tabValue === 0 ? '#2e7d32' : 'inherit',
fontWeight: 'bold'
}}
/>
<Tab
label={this.props.t ? this.props.t('auth.register').toUpperCase() : "REGISTRIEREN"}
label="REGISTRIEREN"
sx={{
color: tabValue === 1 ? '#2e7d32' : 'inherit',
fontWeight: 'bold'
@@ -616,14 +598,7 @@ export class LoginComponent extends Component {
<Box sx={{ width: '100%', display: 'flex', flexDirection: 'column', alignItems: 'center', mb: 2 }}>
{!privacyConfirmed && (
<Typography variant="caption" sx={{ mb: 1, textAlign: 'center' }}>
{this.props.t ?
<>
{this.props.t('auth.privacyAccept')} <Link to="/datenschutz" style={{ color: '#4285F4' }}>{this.props.t('auth.privacyPolicy')}</Link>
</> :
<>
Mit dem Click auf "Mit Google anmelden" akzeptiere ich die <Link to="/datenschutz" style={{ color: '#4285F4' }}>Datenschutzbestimmungen</Link>
</>
}
Mit dem Click auf "Mit Google anmelden" akzeptiere ich die <Link to="/datenschutz" style={{ color: '#4285F4' }}>Datenschutzbestimmungen</Link>
</Typography>
)}
{!showGoogleAuth && (
@@ -636,7 +611,7 @@ export class LoginComponent extends Component {
}}
sx={{ width: '100%', backgroundColor: '#4285F4', color: 'white' }}
>
{this.props.t ? this.props.t('auth.loginWithGoogle') : 'Mit Google anmelden'}
Mit Google anmelden
</Button>
)}
@@ -668,9 +643,7 @@ export class LoginComponent extends Component {
{/* OR Divider */}
<Box sx={{ display: 'flex', alignItems: 'center', my: 2 }}>
<Box sx={{ flex: 1, height: '1px', backgroundColor: '#e0e0e0' }} />
<Typography variant="body2" sx={{ px: 2, color: '#757575' }}>
{this.props.t ? this.props.t('auth.or') : 'ODER'}
</Typography>
<Typography variant="body2" sx={{ px: 2, color: '#757575' }}>ODER</Typography>
<Box sx={{ flex: 1, height: '1px', backgroundColor: '#e0e0e0' }} />
</Box>
@@ -681,7 +654,7 @@ export class LoginComponent extends Component {
<Box sx={{ py: 1 }}>
<TextField
margin="dense"
label={this.props.t ? this.props.t('auth.email') : 'E-Mail'}
label="E-Mail"
type="email"
fullWidth
variant="outlined"
@@ -692,7 +665,7 @@ export class LoginComponent extends Component {
<TextField
margin="dense"
label={this.props.t ? this.props.t('auth.password') : 'Passwort'}
label="Passwort"
type="password"
fullWidth
variant="outlined"
@@ -714,7 +687,7 @@ export class LoginComponent extends Component {
'&:hover': { backgroundColor: 'transparent', textDecoration: 'underline' }
}}
>
{this.props.t ? this.props.t('auth.forgotPassword') : 'Passwort vergessen?'}
Passwort vergessen?
</Button>
</Box>
)}
@@ -722,7 +695,7 @@ export class LoginComponent extends Component {
{tabValue === 1 && (
<TextField
margin="dense"
label={this.props.t ? this.props.t('auth.confirmPassword') : 'Passwort bestätigen'}
label="Passwort bestätigen"
type="password"
fullWidth
variant="outlined"
@@ -744,7 +717,7 @@ export class LoginComponent extends Component {
onClick={tabValue === 0 ? this.handleLogin : this.handleRegister}
sx={{ mt: 2, bgcolor: '#2e7d32', '&:hover': { bgcolor: '#1b5e20' } }}
>
{tabValue === 0 ? (this.props.t ? this.props.t('auth.login').toUpperCase() : 'ANMELDEN') : (this.props.t ? this.props.t('auth.register').toUpperCase() : 'REGISTRIEREN')}
{tabValue === 0 ? 'ANMELDEN' : 'REGISTRIEREN'}
</Button>
)}
</Box>
@@ -767,4 +740,4 @@ export class LoginComponent extends Component {
}
}
export default withRouter(withI18n()(LoginComponent));
export default withRouter(LoginComponent);

View File

@@ -1,647 +0,0 @@
import React from "react";
import { useLocation } from "react-router-dom";
import Container from "@mui/material/Container";
import Box from "@mui/material/Box";
import Typography from "@mui/material/Typography";
import Paper from "@mui/material/Paper";
import Grid from "@mui/material/Grid";
import ChevronLeft from "@mui/icons-material/ChevronLeft";
import ChevronRight from "@mui/icons-material/ChevronRight";
import { Link } from "react-router-dom";
import SharedCarousel from "./SharedCarousel.js";
import { getCombinedAnimatedBorderStyles } from "../utils/animatedBorderStyles.js";
import { useTranslation } from 'react-i18next';
const MainPageLayout = () => {
const location = useLocation();
const currentPath = location.pathname;
const { t } = useTranslation();
const [starHovered, setStarHovered] = React.useState(false);
// Determine which page we're on
const isHome = currentPath === "/";
const isAktionen = currentPath === "/aktionen";
const isFiliale = currentPath === "/filiale";
// Add CSS animations for rotating stars
React.useEffect(() => {
const style = document.createElement('style');
style.textContent = `
@keyframes rotateClockwise {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
@keyframes rotateCounterClockwise {
from { transform: rotate(0deg); }
to { transform: rotate(-360deg); }
}
.star-rotate-slow-cw {
animation: rotateClockwise 60s linear infinite;
}
.star-rotate-slow-ccw {
animation: rotateCounterClockwise 45s linear infinite;
}
.star-rotate-medium-cw {
animation: rotateClockwise 30s linear infinite;
}
`;
document.head.appendChild(style);
return () => {
document.head.removeChild(style);
};
}, []);
// Get navigation config based on current page
const getNavigationConfig = () => {
if (isHome) {
return {
leftNav: { text: t('navigation.aktionen'), link: "/aktionen" },
rightNav: { text: t('navigation.filiale'), link: "/filiale" }
};
} else if (isAktionen) {
return {
leftNav: { text: t('navigation.filiale'), link: "/filiale" },
rightNav: { text: t('navigation.home'), link: "/" }
};
} else if (isFiliale) {
return {
leftNav: { text: t('navigation.home'), link: "/" },
rightNav: { text: t('navigation.aktionen'), link: "/aktionen" }
};
}
return { leftNav: null, rightNav: null };
};
const allTitles = {
home: t('titles.home'),
aktionen: t('titles.aktionen'),
filiale: t('titles.filiale')
};
// Define all content boxes for layered rendering
const allContentBoxes = {
home: [
{
title: t('sections.seeds'),
image: "/assets/images/seeds.jpg",
bgcolor: "#e1f0d3",
link: "/Kategorie/Seeds"
},
{
title: t('sections.stecklinge'),
image: "/assets/images/cutlings.jpg",
bgcolor: "#e8f5d6",
link: "/Kategorie/Stecklinge"
}
],
aktionen: [
{
title: t('sections.oilPress'),
image: "/assets/images/presse.jpg",
bgcolor: "#e1f0d3",
link: "/presseverleih"
},
{
title: t('sections.thcTest'),
image: "/assets/images/purpl.jpg",
bgcolor: "#e8f5d6",
link: "/thc-test"
}
],
filiale: [
{
title: t('sections.address1'),
image: "/assets/images/filiale1.jpg",
bgcolor: "#e1f0d3",
link: "/filiale"
},
{
title: t('sections.address2'),
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();
// Navigation text mapping for translation
const navTexts = [
{ key: 'aktionen', text: t('navigation.aktionen'), link: '/aktionen' },
{ key: 'filiale', text: t('navigation.filiale'), link: '/filiale' },
{ key: 'home', text: t('navigation.home'), link: '/' }
];
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",
// Portrait phone: stack title above navigation
flexDirection: {
xs: "column",
sm: "row"
}
}}>
{/* Title for portrait phones - shown first */}
<Box sx={{
display: { xs: "block", sm: "none" },
mb: { xs: 2, sm: 0 },
width: "100%",
textAlign: "center",
position: "relative"
}}>
{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" ? 0 : "auto",
left: pageType !== "home" ? 0 : "auto",
transform: "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>
{/* Navigation container for portrait phones */}
<Box sx={{
display: { xs: "flex", sm: "contents" },
width: { xs: "100%", sm: "auto" },
justifyContent: { xs: "space-between", sm: "initial" },
alignItems: "center"
}}>
{/* Left Navigation - Layered rendering */}
<Box sx={{
display: "flex",
alignItems: "center",
flexShrink: 0,
justifyContent: "flex-start",
position: "relative",
mr: { xs: 0, sm: 2 }
}}>
{navTexts.map((navItem, index) => {
const isActive = navConfig.leftNav && navConfig.leftNav.text === navItem.text;
const link = navItem.link;
return (
<Box
key={navItem.key}
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"
}}
>
{navItem.text}
</Typography>
</Box>
);
})}
</Box>
{/* Center Title - Layered rendering - Hidden on portrait phones, shown on larger screens */}
<Box sx={{
flex: 1,
display: { xs: "none", sm: "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: { xs: 0, sm: 2 }
}}>
{navTexts.map((navItem, index) => {
const isActive = navConfig.rightNav && navConfig.rightNav.text === navItem.text;
const link = navItem.link;
return (
<Box
key={navItem.key}
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"
}}
>
{navItem.text}
</Typography>
<ChevronRight sx={{ fontSize: "2rem", ml: 1 }} />
</Box>
);
})}
</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%", position: 'relative' }}>
{/* Multi-pointed star for seeds box - moved to Grid level */}
{index === 0 && pageType === "home" && (
<div
style={{
position: 'absolute',
top: '-45px',
left: '-45px',
width: '180px',
height: '180px',
zIndex: 999,
pointerEvents: 'auto',
cursor: 'pointer'
}}
onMouseEnter={() => setStarHovered(true)}
onMouseLeave={() => setStarHovered(false)}
>
{/* Background star - slightly larger and rotated */}
<svg
viewBox="0 0 60 60"
width="168"
height="168"
className="star-rotate-slow-cw"
style={{
position: 'absolute',
top: '-9px',
left: '-9px',
transform: 'rotate(20deg)'
}}
>
<polygon
points="30,0 38,20 60,22 43,37 48,60 30,48 12,60 17,37 0,22 22,20"
fill="#B8860B"
stroke="none"
/>
</svg>
{/* Middle star - medium size with different rotation */}
<svg
viewBox="0 0 60 60"
width="159"
height="159"
className="star-rotate-slow-ccw"
style={{
position: 'absolute',
top: '-4.5px',
left: '-4.5px',
transform: 'rotate(-25deg)'
}}
>
<polygon
points="30,0 38,20 60,22 43,37 48,60 30,48 12,60 17,37 0,22 22,20"
fill="#DAA520"
stroke="none"
/>
</svg>
{/* Foreground star - main star with text */}
<svg
viewBox="0 0 60 60"
width="150"
height="150"
className="star-rotate-medium-cw"
>
<polygon
points="30,0 38,20 60,22 43,37 48,60 30,48 12,60 17,37 0,22 22,20"
fill="#FFD700"
stroke="none"
/>
</svg>
{/* Text positioned in the center of the star */}
<div
style={{
position: 'absolute',
top: '45%',
left: '43%',
transform: 'translate(-50%, -50%) rotate(-10deg)',
color: 'white',
fontWeight: '900',
fontSize: '20px',
textShadow: '0px 3px 6px rgba(0,0,0,0.5)',
zIndex: 1000,
textAlign: 'center',
lineHeight: '1.1',
width: '135px',
transition: 'opacity 0.3s ease',
opacity: starHovered ? 0 : 1
}}
>
{t('sections.showUsPhoto')}
</div>
{/* Hover text */}
<div
style={{
position: 'absolute',
top: '45%',
left: '43%',
transform: 'translate(-50%, -50%) rotate(-10deg)',
color: 'white',
fontWeight: '900',
fontSize: '20px',
textShadow: '0px 3px 6px rgba(0,0,0,0.5)',
zIndex: 1000,
textAlign: 'center',
lineHeight: '1.1',
width: '135px',
opacity: starHovered ? 1 : 0,
transition: 'opacity 0.3s ease'
}}
>
{t('sections.selectSeedRate')}
</div>
</div>
)}
{/* Multi-pointed star for stecklinge box - bottom right */}
{index === 1 && pageType === "home" && (
<div
style={{
position: 'absolute',
bottom: '-65px',
right: '-65px',
width: '180px',
height: '180px',
zIndex: 999,
pointerEvents: 'auto',
cursor: 'pointer'
}}
>
{/* Background star - slightly larger and rotated */}
<svg
viewBox="0 0 60 60"
width="168"
height="168"
className="star-rotate-slow-ccw"
style={{
position: 'absolute',
top: '-9px',
left: '-9px',
transform: 'rotate(20deg)'
}}
>
<polygon
points="30,0 38,20 60,22 43,37 48,60 30,48 12,60 17,37 0,22 22,20"
fill="#5F9EA0"
stroke="none"
/>
</svg>
{/* Middle star - medium size with different rotation */}
<svg
viewBox="0 0 60 60"
width="159"
height="159"
className="star-rotate-medium-cw"
style={{
position: 'absolute',
top: '-4.5px',
left: '-4.5px',
transform: 'rotate(-25deg)'
}}
>
<polygon
points="30,0 38,20 60,22 43,37 48,60 30,48 12,60 17,37 0,22 22,20"
fill="#7FCDCD"
stroke="none"
/>
</svg>
{/* Foreground star - main star with text */}
<svg
viewBox="0 0 60 60"
width="150"
height="150"
className="star-rotate-slow-cw"
>
<polygon
points="30,0 38,20 60,22 43,37 48,60 30,48 12,60 17,37 0,22 22,20"
fill="#AFEEEE"
stroke="none"
/>
</svg>
{/* Text positioned in the center of the star */}
<div
style={{
position: 'absolute',
top: '42%',
left: '45%',
transform: 'translate(-50%, -50%) rotate(10deg)',
color: 'white',
fontWeight: '900',
fontSize: '20px',
textShadow: '0px 3px 6px rgba(0,0,0,0.5)',
zIndex: 1000,
textAlign: 'center',
lineHeight: '1.1',
width: '135px'
}}
>
{t('sections.indoorSeason')}
</div>
</div>
)}
<div className={`animated-border-card ${index === 0 ? 'seeds-card' : 'cutlings-card'}`}>
<Paper
component={Link}
to={box.link}
sx={{
p: 0,
textDecoration: "none",
color: "text.primary",
borderRadius: 2,
overflow: "hidden",
height: { xs: 150, sm: 200, md: 300 },
display: "flex",
flexDirection: "column",
boxShadow: 10,
transition: "all 0.3s ease",
"&:hover": {
transform: "translateY(-5px)",
boxShadow: 20,
},
}}
>
<Box
sx={{
height: "100%",
bgcolor: box.bgcolor,
backgroundImage: `url("${box.image}")`,
backgroundSize: "contain",
backgroundPosition: "center",
backgroundRepeat: "no-repeat",
position: "relative",
}}
>
<Box
sx={{
position: "absolute",
bottom: 0,
left: 0,
right: 0,
bgcolor: "rgba(27, 94, 32, 0.8)",
p: 1,
}}
>
<Typography
sx={{
fontSize: "1.6rem",
color: "white",
fontFamily: "SwashingtonCP",
}}
>
{box.title}
</Typography>
</Box>
</Box>
</Paper>
</div>
</Grid>
))}
</Grid>
))}
</Box>
{/* Shared Carousel */}
<SharedCarousel />
</Container>
);
};
export default MainPageLayout;

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

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

View File

@@ -1,168 +0,0 @@
import React, { Component } from 'react';
import { Navigate } from 'react-router-dom';
import { Box, CircularProgress, Typography } from '@mui/material';
import SocketContext from '../contexts/SocketContext.js';
class PaymentSuccess extends Component {
static contextType = SocketContext;
constructor(props) {
super(props);
this.state = {
redirectUrl: null,
processing: true,
error: null
};
}
componentDidMount() {
this.processMolliePayment();
}
processMolliePayment = () => {
try {
// Get the stored payment ID from localStorage
const pendingPayment = localStorage.getItem('pendingPayment');
if (!pendingPayment) {
console.error('No pending payment found in localStorage');
this.setState({
redirectUrl: '/profile#cart',
processing: false,
error: 'No payment information found'
});
return;
}
let paymentData;
try {
paymentData = JSON.parse(pendingPayment);
// Clear the pending payment data
localStorage.removeItem('pendingPayment');
} catch (error) {
console.error('Error parsing pending payment data:', error);
this.setState({
redirectUrl: '/profile#cart',
processing: false,
error: 'Invalid payment data'
});
return;
}
if (!paymentData.paymentId) {
console.error('No payment ID found in stored payment data');
this.setState({
redirectUrl: '/profile#cart',
processing: false,
error: 'Missing payment ID'
});
return;
}
// Check payment status with backend
this.checkMolliePaymentStatus(paymentData.paymentId);
} catch (error) {
console.error('Error processing Mollie payment:', error);
this.setState({
redirectUrl: '/profile#cart',
processing: false,
error: 'Payment processing failed'
});
}
};
checkMolliePaymentStatus = (paymentId) => {
const { socket } = this.context;
if (!socket || !socket.connected) {
console.error('Socket not connected');
this.setState({
redirectUrl: '/profile#cart',
processing: false,
error: 'Connection error'
});
return;
}
socket.emit('checkMollieIntent', { paymentId }, (response) => {
if (response.success) {
console.log('Payment Status:', response.payment.status);
console.log('Is Paid:', response.payment.isPaid);
console.log('Order Created:', response.order.created);
if (response.order.orderId) {
console.log('Order ID:', response.order.orderId);
}
// Build the redirect URL with Mollie completion parameters
const profileUrl = new URL('/profile', window.location.origin);
profileUrl.searchParams.set('mollieComplete', 'true');
profileUrl.searchParams.set('mollie_payment_id', paymentId);
profileUrl.searchParams.set('mollie_status', response.payment.status);
profileUrl.searchParams.set('mollie_amount', response.payment.amount);
profileUrl.searchParams.set('mollie_timestamp', Date.now().toString());
profileUrl.searchParams.set('mollie_is_paid', response.payment.isPaid.toString());
if (response.order.orderId) {
profileUrl.searchParams.set('mollie_order_id', response.order.orderId.toString());
}
// Set hash based on payment success: orders for successful payments, cart for failed payments
profileUrl.hash = response.payment.isPaid ? '#orders' : '#cart';
this.setState({
redirectUrl: profileUrl.pathname + profileUrl.search + profileUrl.hash,
processing: false
});
} else {
console.error('Failed to check payment status:', response.error);
this.setState({
redirectUrl: '/profile#cart',
processing: false,
error: response.error || 'Payment verification failed'
});
}
});
};
render() {
const { redirectUrl, processing, error } = this.state;
if (processing) {
return (
<Box
sx={{
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
minHeight: '60vh',
gap: 2
}}
>
<CircularProgress size={60} />
<Typography variant="h6" color="text.secondary">
Zahlung wird überprüft...
</Typography>
<Typography variant="body2" color="text.secondary">
Bitte warten Sie, während wir Ihre Zahlung bei Mollie überprüfen.
</Typography>
</Box>
);
}
if (error) {
return <Navigate to="/profile#cart" replace />;
}
if (redirectUrl) {
return <Navigate to={redirectUrl} replace />;
}
// Fallback redirect to profile
return <Navigate to="/profile#cart" replace />;
}
}
export default PaymentSuccess;

View File

@@ -1,286 +0,0 @@
import React, { Component } from 'react';
import {
Box,
Button,
Typography,
IconButton,
Paper,
Grid,
Alert
} from '@mui/material';
import {
Delete,
CloudUpload
} from '@mui/icons-material';
class PhotoUpload extends Component {
constructor(props) {
super(props);
this.state = {
files: [],
previews: [],
error: null
};
this.fileInputRef = React.createRef();
}
handleFileSelect = (event) => {
const selectedFiles = Array.from(event.target.files);
const maxFiles = this.props.maxFiles || 5;
const maxSize = this.props.maxSize || 50 * 1024 * 1024; // 50MB default - will be compressed
// Validate file count
if (this.state.files.length + selectedFiles.length > maxFiles) {
this.setState({
error: `Maximal ${maxFiles} Dateien erlaubt`
});
return;
}
// Validate file types and sizes
const validTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
const validFiles = [];
const newPreviews = [];
for (const file of selectedFiles) {
if (!validTypes.includes(file.type)) {
this.setState({
error: 'Nur Bilddateien (JPEG, PNG, GIF, WebP) sind erlaubt'
});
continue;
}
if (file.size > maxSize) {
this.setState({
error: `Datei zu groß. Maximum: ${Math.round(maxSize / (1024 * 1024))}MB`
});
continue;
}
validFiles.push(file);
// Create preview and compress image
const reader = new FileReader();
reader.onload = (e) => {
// Compress the image
this.compressImage(e.target.result, file.name, (compressedFile) => {
newPreviews.push({
file: compressedFile,
preview: e.target.result,
name: file.name,
originalSize: file.size,
compressedSize: compressedFile.size
});
if (newPreviews.length === validFiles.length) {
const compressedFiles = newPreviews.map(p => p.file);
this.setState(prevState => ({
files: [...prevState.files, ...compressedFiles],
previews: [...prevState.previews, ...newPreviews],
error: null
}), () => {
// Notify parent component
if (this.props.onChange) {
this.props.onChange(this.state.files);
}
});
}
});
};
reader.readAsDataURL(file);
}
// Reset input
event.target.value = '';
};
handleRemoveFile = (index) => {
this.setState(prevState => {
const newFiles = prevState.files.filter((_, i) => i !== index);
const newPreviews = prevState.previews.filter((_, i) => i !== index);
// Notify parent component
if (this.props.onChange) {
this.props.onChange(newFiles);
}
return {
files: newFiles,
previews: newPreviews
};
});
};
compressImage = (dataURL, fileName, callback) => {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
const img = new Image();
img.onload = () => {
// Calculate new dimensions (max 1920x1080 for submission)
const maxWidth = 1920;
const maxHeight = 1080;
let { width, height } = img;
if (width > height) {
if (width > maxWidth) {
height = (height * maxWidth) / width;
width = maxWidth;
}
} else {
if (height > maxHeight) {
width = (width * maxHeight) / height;
height = maxHeight;
}
}
canvas.width = width;
canvas.height = height;
// Draw and compress
ctx.drawImage(img, 0, 0, width, height);
// Convert to blob with compression
canvas.toBlob((blob) => {
const compressedFile = new File([blob], fileName, {
type: 'image/jpeg',
lastModified: Date.now()
});
callback(compressedFile);
}, 'image/jpeg', 0.8); // 80% quality
};
img.src = dataURL;
};
// Method to reset the component
reset = () => {
this.setState({
files: [],
previews: [],
error: null
});
// Also reset the file input
if (this.fileInputRef.current) {
this.fileInputRef.current.value = '';
}
};
render() {
const { files, previews, error } = this.state;
const { disabled, label } = this.props;
return (
<Box>
<Typography variant="body2" sx={{ mb: 1, fontWeight: 500 }}>
{label || 'Fotos anhängen (optional)'}
</Typography>
<input
ref={this.fileInputRef}
type="file"
multiple
accept="image/*"
style={{ display: 'none' }}
onChange={this.handleFileSelect}
disabled={disabled}
/>
<Button
variant="outlined"
startIcon={<CloudUpload />}
onClick={() => this.fileInputRef.current?.click()}
disabled={disabled}
sx={{ mb: 2 }}
>
Fotos auswählen
</Button>
{error && (
<Alert severity="error" sx={{ mb: 2 }}>
{error}
</Alert>
)}
{previews.length > 0 && (
<Grid container spacing={2}>
{previews.map((preview, index) => (
<Grid item xs={6} sm={4} md={3} key={index}>
<Paper
sx={{
position: 'relative',
p: 1,
borderRadius: 1,
overflow: 'hidden'
}}
>
<Box
component="img"
src={preview.preview}
alt={preview.name}
sx={{
width: '100%',
height: '100px',
objectFit: 'cover',
borderRadius: 1
}}
/>
<IconButton
size="small"
onClick={() => this.handleRemoveFile(index)}
disabled={disabled}
sx={{
position: 'absolute',
top: 4,
right: 4,
backgroundColor: 'rgba(0,0,0,0.7)',
color: 'white',
'&:hover': {
backgroundColor: 'rgba(0,0,0,0.9)'
}
}}
>
<Delete fontSize="small" />
</IconButton>
<Typography
variant="caption"
sx={{
position: 'absolute',
bottom: 4,
left: 4,
right: 4,
backgroundColor: 'rgba(0,0,0,0.7)',
color: 'white',
p: 0.5,
borderRadius: 0.5,
fontSize: '0.7rem',
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap'
}}
>
{preview.name}
</Typography>
</Paper>
</Grid>
))}
</Grid>
)}
{files.length > 0 && (
<Typography variant="caption" color="text.secondary" sx={{ mt: 1, display: 'block' }}>
{files.length} Datei(en) ausgewählt
{previews.length > 0 && previews.some(p => p.originalSize && p.compressedSize) && (
<span style={{ marginLeft: '8px' }}>
(komprimiert für Upload)
</span>
)}
</Typography>
)}
</Box>
);
}
}
export default PhotoUpload;

View File

@@ -8,7 +8,6 @@ import CircularProgress from '@mui/material/CircularProgress';
import IconButton from '@mui/material/IconButton';
import AddToCartButton from './AddToCartButton.js';
import { Link } from 'react-router-dom';
import { withI18n } from '../i18n/withTranslation.js';
import ZoomInIcon from '@mui/icons-material/ZoomIn';
class Product extends Component {
@@ -69,8 +68,8 @@ class Product extends Component {
render() {
const {
id, name, price, available, manufacturer, seoName,
currency, vat, cGrundEinheit, fGrundPreis, thc,
floweringWeeks,incoming, neu, weight, versandklasse, availableSupplier, komponenten
currency, vat, massMenge, massEinheit, thc,
floweringWeeks,incoming, neu, weight, versandklasse, availableSupplier
} = this.props;
const isNew = neu && (new Date().getTime() - new Date(neu).getTime() < 30 * 24 * 60 * 60 * 1000);
@@ -174,7 +173,7 @@ class Product extends Component {
zIndex: 1000
}}
>
{this.props.t ? this.props.t('product.new') : 'NEU'}
NEU
</div>
</div>
)}
@@ -241,7 +240,7 @@ class Product extends Component {
transformOrigin: 'top left'
}}
>
{floweringWeeks} {this.props.t ? this.props.t('product.weeks') : 'Wochen'}
{floweringWeeks} Wochen
</div>
)}
@@ -337,13 +336,13 @@ class Product extends Component {
sx={{ mt: 'auto', fontWeight: 'bold', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}
>
<span>{new Intl.NumberFormat('de-DE', {style: 'currency', currency: currency || 'EUR'}).format(price)}</span>
<small style={{ color: '#77aa77', fontSize: '0.6em' }}>({this.props.t ? this.props.t('product.inclVatFooter', { vat }) : `incl. ${vat}% USt.,*`})</small>
<small style={{ color: '#77aa77', fontSize: '0.6em' }}>(incl. {vat}% USt.,*)</small>
</Typography>
{cGrundEinheit && fGrundPreis && fGrundPreis != price && (<Typography variant="body2" color="text.secondary" sx={{ m: 0,p: 0 }}>
({new Intl.NumberFormat('de-DE', {style: 'currency', currency: currency || 'EUR'}).format(fGrundPreis)}/{cGrundEinheit})
{massMenge != 1 && massEinheit && (<Typography variant="body2" color="text.secondary" sx={{ m: 0,p: 0 }}>
({new Intl.NumberFormat('de-DE', {style: 'currency', currency: currency || 'EUR'}).format(price/massMenge)}/{massEinheit})
</Typography> )}
</div>
{/*incoming*/}
@@ -359,7 +358,7 @@ class Product extends Component {
>
<ZoomInIcon />
</IconButton>
<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}/>
<AddToCartButton cartButton={true} availableSupplier={availableSupplier} available={available} incoming={incoming} seoName={seoName} pictureList={this.props.pictureList} id={id} price={price} vat={vat} weight={weight} name={name} versandklasse={versandklasse}/>
</Box>
</Card>
</Box>
@@ -367,4 +366,4 @@ class Product extends Component {
}
}
export default withI18n()(Product);
export default Product;

View File

@@ -1,13 +1,9 @@
import React, { Component } from "react";
import { Box, Typography, CardMedia, Stack, Chip, Button, Collapse } from "@mui/material";
import { Box, Typography, CardMedia, Stack, Chip } from "@mui/material";
import { Link } from "react-router-dom";
import parse from "html-react-parser";
import AddToCartButton from "./AddToCartButton.js";
import Images from "./Images.js";
import { withI18n } from "../i18n/withTranslation.js";
import ArticleQuestionForm from "./ArticleQuestionForm.js";
import ArticleRatingForm from "./ArticleRatingForm.js";
import ArticleAvailabilityForm from "./ArticleAvailabilityForm.js";
// Utility function to clean product names by removing trailing number in parentheses
const cleanProductName = (name) => {
@@ -33,16 +29,6 @@ class ProductDetailPage extends Component {
attributes: [],
isSteckling: false,
imageDialogOpen: false,
komponenten: [],
komponentenLoaded: false,
komponentenData: {}, // Store individual komponent data with loading states
komponentenImages: {}, // Store tiny pictures for komponenten
totalKomponentenPrice: 0,
totalSavings: 0,
// Collapsible sections state
showQuestionForm: false,
showRatingForm: false,
showAvailabilityForm: false
};
} else {
this.state = {
@@ -53,16 +39,6 @@ class ProductDetailPage extends Component {
attributes: [],
isSteckling: false,
imageDialogOpen: false,
komponenten: [],
komponentenLoaded: false,
komponentenData: {}, // Store individual komponent data with loading states
komponentenImages: {}, // Store tiny pictures for komponenten
totalKomponentenPrice: 0,
totalSavings: 0,
// Collapsible sections state
showQuestionForm: false,
showRatingForm: false,
showAvailabilityForm: false
};
}
}
@@ -88,248 +64,6 @@ 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 = () => {
if (!this.props.socket || !this.props.socket.connected) {
// Socket not connected yet, but don't show error immediately on first load
@@ -344,37 +78,12 @@ class ProductDetailPage extends Component {
(res) => {
if (res.success) {
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({
product: res.product,
loading: false,
error: null,
imageDialogOpen: false,
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);
}
}
attributes: res.attributes
});
console.log("getProductView", res);
@@ -470,54 +179,8 @@ class ProductDetailPage extends Component {
this.setState({ imageDialogOpen: false });
};
toggleQuestionForm = () => {
this.setState(prevState => ({
showQuestionForm: !prevState.showQuestionForm,
showRatingForm: false,
showAvailabilityForm: false
}), () => {
if (this.state.showQuestionForm) {
setTimeout(() => this.scrollToSection('question-form'), 100);
}
});
};
toggleRatingForm = () => {
this.setState(prevState => ({
showRatingForm: !prevState.showRatingForm,
showQuestionForm: false,
showAvailabilityForm: false
}), () => {
if (this.state.showRatingForm) {
setTimeout(() => this.scrollToSection('rating-form'), 100);
}
});
};
toggleAvailabilityForm = () => {
this.setState(prevState => ({
showAvailabilityForm: !prevState.showAvailabilityForm,
showQuestionForm: false,
showRatingForm: false
}), () => {
if (this.state.showAvailabilityForm) {
setTimeout(() => this.scrollToSection('availability-form'), 100);
}
});
};
scrollToSection = (sectionId) => {
const element = document.getElementById(sectionId);
if (element) {
element.scrollIntoView({
behavior: 'smooth',
block: 'start'
});
}
};
render() {
const { product, loading, error, attributeImages, isSteckling, attributes, komponentenLoaded, komponentenData, komponentenImages, totalKomponentenPrice, totalSavings } =
const { product, loading, error, attributeImages, isSteckling, attributes } =
this.state;
if (loading) {
@@ -548,7 +211,7 @@ class ProductDetailPage extends Component {
<Typography>{error}</Typography>
<Link to="/" style={{ textDecoration: "none" }}>
<Typography color="primary" sx={{ mt: 2 }}>
{this.props.t ? this.props.t('product.backToHome') : 'Zurück zur Startseite'}
Zurück zur Startseite
</Typography>
</Link>
</Box>
@@ -566,7 +229,7 @@ class ProductDetailPage extends Component {
</Typography>
<Link to="/" style={{ textDecoration: "none" }}>
<Typography color="primary" sx={{ mt: 2 }}>
{this.props.t ? this.props.t('product.backToHome') : 'Zurück zur Startseite'}
Zurück zur Startseite
</Typography>
</Link>
</Box>
@@ -631,7 +294,7 @@ class ProductDetailPage extends Component {
fontWeight: "bold",
}}
>
{this.props.t ? this.props.t('common.back') : 'Zurück'}
Zurück
</Link>
</Typography>
</Box>
@@ -692,7 +355,7 @@ class ProductDetailPage extends Component {
{/* Product identifiers */}
<Box sx={{ mb: 1 }}>
<Typography variant="body2" color="text.secondary">
{this.props.t ? this.props.t('product.articleNumber') : 'Artikelnummer'}: {product.articleNumber} {product.gtin ? ` | GTIN: ${product.gtin}` : ""}
Artikelnummer: {product.articleNumber} {product.gtin ? ` | GTIN: ${product.gtin}` : ""}
</Typography>
</Box>
@@ -710,103 +373,53 @@ class ProductDetailPage extends Component {
{product.manufacturer && (
<Box sx={{ display: "flex", alignItems: "center", mb: 2 }}>
<Typography variant="body2" sx={{ fontStyle: "italic" }}>
{this.props.t ? this.props.t('product.manufacturer') : 'Hersteller'}: {product.manufacturer}
Hersteller: {product.manufacturer}
</Typography>
</Box>
)}
{/* Attribute images and chips with action buttons */}
{/* Attribute images and chips */}
{(attributes.some(attr => attributeImages[attr.kMerkmalWert]) || attributes.some(attr => !attributeImages[attr.kMerkmalWert])) && (
<Box sx={{ display: "flex", justifyContent: "space-between", alignItems: "flex-start", mb: 2, gap: 2 }}>
<Stack direction="row" spacing={2} sx={{ flexWrap: "wrap", gap: 1, flex: 1 }}>
{attributes
.filter(attribute => attributeImages[attribute.kMerkmalWert])
.map((attribute) => {
const key = attribute.kMerkmalWert;
return (
<Box key={key} sx={{ mb: 1,border: "1px solid #e0e0e0", borderRadius: 1 }}>
<CardMedia
component="img"
style={{ width: "72px", height: "98px" }}
image={attributeImages[key]}
alt={`Attribute ${key}`}
/>
</Box>
);
})}
{attributes
.filter(attribute => !attributeImages[attribute.kMerkmalWert])
.map((attribute) => (
<Chip
key={attribute.kMerkmalWert}
label={attribute.cWert}
disabled
sx={{ mb: 1 }}
/>
))}
</Stack>
{/* Right-aligned action buttons */}
<Stack direction="column" spacing={1} sx={{ flexShrink: 0 }}>
<Button
variant="outlined"
size="small"
onClick={this.toggleQuestionForm}
sx={{
fontSize: "0.75rem",
px: 1.5,
py: 0.5,
minWidth: "auto",
whiteSpace: "nowrap"
}}
>
Frage zum Artikel
</Button>
<Button
variant="outlined"
size="small"
onClick={this.toggleRatingForm}
sx={{
fontSize: "0.75rem",
px: 1.5,
py: 0.5,
minWidth: "auto",
whiteSpace: "nowrap"
}}
>
Artikel Bewerten
</Button>
{(product.available !== 1 && product.availableSupplier !== 1) && (
<Button
variant="outlined"
size="small"
onClick={this.toggleAvailabilityForm}
sx={{
fontSize: "0.75rem",
px: 1.5,
py: 0.5,
minWidth: "auto",
whiteSpace: "nowrap",
borderColor: "warning.main",
color: "warning.main",
"&:hover": {
borderColor: "warning.dark",
backgroundColor: "warning.light"
}
}}
>
Verfügbarkeit anfragen
</Button>
)}
</Stack>
</Box>
<Stack direction="row" spacing={2} sx={{ flexWrap: "wrap", gap: 1, mb: 2 }}>
{attributes
.filter(attribute => attributeImages[attribute.kMerkmalWert])
.map((attribute) => {
const key = attribute.kMerkmalWert;
return (
<Box key={key} sx={{ mb: 1 }}>
<CardMedia
component="img"
image={attributeImages[key]}
alt={`Attribute ${key}`}
sx={{
maxWidth: "100px",
maxHeight: "100px",
objectFit: "contain",
border: "1px solid #e0e0e0",
borderRadius: 1,
}}
/>
</Box>
);
})}
{attributes
.filter(attribute => !attributeImages[attribute.kMerkmalWert])
.map((attribute) => (
<Chip
key={attribute.kMerkmalWert}
label={attribute.cWert}
disabled
sx={{ mb: 1 }}
/>
))}
</Stack>
)}
{/* Weight */}
{product.weight > 0 && (
<Box sx={{ mb: 2 }}>
<Typography variant="body2" color="text.secondary">
{this.props.t ? this.props.t('product.weight', { weight: product.weight.toFixed(1).replace(".", ",") }) : `Gewicht: ${product.weight.toFixed(1).replace(".", ",")} kg`}
Gewicht: {product.weight.toFixed(1).replace(".", ",")} kg
</Typography>
</Box>
)}
@@ -838,12 +451,8 @@ class ProductDetailPage extends Component {
{priceWithTax}
</Typography>
<Typography variant="body2" color="text.secondary">
{this.props.t ? this.props.t('product.inclVat', { vat: product.vat }) : `inkl. ${product.vat}% MwSt.`}
{product.cGrundEinheit && product.fGrundPreis && (
<>; {new Intl.NumberFormat('de-DE', {style: 'currency', currency: 'EUR'}).format(product.fGrundPreis)}/{product.cGrundEinheit}</>
)}
inkl. {product.vat}% MwSt.
</Typography>
{product.versandklasse &&
product.versandklasse != "standard" &&
product.versandklasse != "kostenlos" && (
@@ -852,42 +461,6 @@ class ProductDetailPage extends Component {
</Typography>
)}
</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"
}}
>
{this.props.t ? this.props.t('product.youSave', {
amount: new Intl.NumberFormat("de-DE", {
style: "currency",
currency: "EUR",
}).format(totalKomponentenPrice - product.price)
}) : `Sie sparen: ${new Intl.NumberFormat("de-DE", {
style: "currency",
currency: "EUR",
}).format(totalKomponentenPrice - product.price)}`}
</Typography>
<Typography variant="caption" color="text.secondary">
{this.props.t ? this.props.t('product.cheaperThanIndividual') : 'Günstiger als Einzelkauf'}
</Typography>
</Box>
</Box>
)}
<Box
sx={{
display: "flex",
@@ -914,7 +487,6 @@ class ProductDetailPage extends Component {
vat={product.vat}
weight={product.weight}
availableSupplier={product.availableSupplier}
komponenten={product.komponenten}
name={cleanProductName(product.name) + " Stecklingsvorbestellung 1 Stück"}
versandklasse={"nur Abholung"}
/>
@@ -927,7 +499,7 @@ class ProductDetailPage extends Component {
mt: 1
}}
>
{this.props.t ? this.props.t('product.pickupPrice') : 'Abholpreis: 19,90 € pro Steckling.'}
Abholpreis: 19,90 pro Steckling.
</Typography>
</Box>
)}
@@ -944,16 +516,12 @@ class ProductDetailPage extends Component {
available={product.available}
id={product.id}
availableSupplier={product.availableSupplier}
komponenten={product.komponenten}
cGrundEinheit={product.cGrundEinheit}
fGrundPreis={product.fGrundPreis}
price={product.price}
vat={product.vat}
weight={product.weight}
name={cleanProductName(product.name)}
versandklasse={product.versandklasse}
/>
<Typography
variant="caption"
sx={{
@@ -963,12 +531,9 @@ class ProductDetailPage extends Component {
mt: 1
}}
>
{product.id.toString().endsWith("steckling") ?
(this.props.t ? this.props.t('delivery.times.cutting14Days') : "Lieferzeit: 14 Tage") :
product.available == 1 ?
(this.props.t ? this.props.t('delivery.times.standard2to3Days') : "Lieferzeit: 2-3 Tage") :
product.availableSupplier == 1 ?
(this.props.t ? this.props.t('delivery.times.supplier7to9Days') : "Lieferzeit: 7-9 Tage") : ""}
{product.id.toString().endsWith("steckling") ? "Lieferzeit: 14 Tage" :
product.available == 1 ? "Lieferzeit: 2-3 Tage" :
product.availableSupplier == 1 ? "Lieferzeit: 7-9 Tage" : ""}
</Typography>
</Box>
</Box>
@@ -1000,242 +565,9 @@ class ProductDetailPage extends Component {
</Box>
</Box>
)}
{/* Article Question Form */}
<Collapse in={this.state.showQuestionForm}>
<div id="question-form">
<ArticleQuestionForm
productId={product.id}
productName={cleanProductName(product.name)}
socket={this.props.socket}
/>
</div>
</Collapse>
{/* Article Rating Form */}
<Collapse in={this.state.showRatingForm}>
<div id="rating-form">
<ArticleRatingForm
productId={product.id}
productName={cleanProductName(product.name)}
socket={this.props.socket}
/>
</div>
</Collapse>
{/* Article Availability Form - only show for out of stock items */}
{(product.available !== 1 && product.availableSupplier !== 1) && (
<Collapse in={this.state.showAvailabilityForm}>
<ArticleAvailabilityForm
productId={product.id}
productName={cleanProductName(product.name)}
socket={this.props.socket}
/>
</Collapse>
)}
{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>{this.props.t ? this.props.t('product.consistsOf') : '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">
{this.props.t ? this.props.t('product.individualPriceTotal') : '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">
{this.props.t ? this.props.t('product.setPrice') : '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" }}>
{this.props.t ? this.props.t('product.yourSavings') : '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">
{this.props.t ? this.props.t('product.loadingComponentDetails', { index: index + 1 }) : `${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>
);
}
}
export default withI18n()(ProductDetailPage);
export default ProductDetailPage;

View File

@@ -4,7 +4,6 @@ import Typography from '@mui/material/Typography';
import Filter from './Filter.js';
import { useParams, useSearchParams, useNavigate, useLocation } from 'react-router-dom';
import { setSessionSetting, removeSessionSetting, clearAllSessionSettings } from '../utils/sessionStorage.js';
import { withI18n } from '../i18n/withTranslation.js';
const isNew = (neu) => neu && (new Date().getTime() - new Date(neu).getTime() < 30 * 24 * 60 * 60 * 1000);
@@ -94,14 +93,14 @@ class ProductFilters extends Component {
}
_getAvailabilityValues = (products) => {
const filters = [{id:1,name: this.props.t ? this.props.t('product.inStock') : 'auf Lager'}];
const filters = [{id:1,name:'auf Lager'}];
for(const product of products){
if(isNew(product.neu)){
if(!filters.find(filter => filter.id == 2)) filters.push({id:2,name: this.props.t ? this.props.t('product.new') : 'Neu'});
if(!filters.find(filter => filter.id == 2)) filters.push({id:2,name:'Neu'});
}
if(!product.available && product.incomingDate){
if(!filters.find(filter => filter.id == 3)) filters.push({id:3,name: this.props.t ? this.props.t('product.comingSoon') : 'Bald verfügbar'});
if(!filters.find(filter => filter.id == 3)) filters.push({id:3,name:'Bald verfügbar'});
}
}
return filters
@@ -194,7 +193,7 @@ class ProductFilters extends Component {
{this.props.products.length > 0 && (
<><Filter
title={this.props.t ? this.props.t('filters.availability') : 'Verfügbarkeit'}
title="Verfügbarkeit"
options={this.state.availabilityValues}
searchParams={this.props.searchParams}
products={this.props.products}
@@ -237,7 +236,7 @@ class ProductFilters extends Component {
{this.generateAttributeFilters()}
<Filter
title={this.props.t ? this.props.t('filters.manufacturer') : 'Hersteller'}
title="Hersteller"
options={this.state.uniqueManufacturerArray}
filterType="manufacturer"
products={this.props.products}
@@ -258,4 +257,4 @@ class ProductFilters extends Component {
}
}
export default withRouter(withI18n()(ProductFilters));
export default withRouter(ProductFilters);

View File

@@ -11,7 +11,6 @@ import Chip from '@mui/material/Chip';
import Stack from '@mui/material/Stack';
import Product from './Product.js';
import { removeSessionSetting } from '../utils/sessionStorage.js';
import { withI18n } from '../i18n/withTranslation.js';
// Sort products by fuzzy similarity to their name/description
function sortProductsByFuzzySimilarity(products, searchTerm) {
@@ -142,12 +141,12 @@ class ProductList extends Component {
onChange={this.handlePageChange}
color="primary"
size={"large"}
siblingCount={1}
boundaryCount={1}
hideNextButton={true}
hidePrevButton={true}
showFirstButton={false}
showLastButton={false}
siblingCount={window.innerWidth < 600 ? 0 : 1}
boundaryCount={window.innerWidth < 600 ? 1 : 1}
hideNextButton={false}
hidePrevButton={false}
showFirstButton={window.innerWidth >= 600}
showLastButton={window.innerWidth >= 600}
sx={{
'& .MuiPagination-ul': {
flexWrap: 'nowrap',
@@ -185,7 +184,7 @@ class ProductList extends Component {
px: 2
}}>
<Typography variant="h6" color="text.secondary" sx={{ textAlign: 'center' }}>
{this.props.t ? this.props.t('product.removeFiltersToSee') : 'Entferne Filter um Produkte zu sehen'}
Entferne Filter um Produkte zu sehen
</Typography>
</Box>
);
@@ -201,14 +200,14 @@ class ProductList extends Component {
if (!isFiltered) {
// No filters applied
if (filteredCount === 0) return this.props.t ? this.props.t('product.countDisplay.noProducts') : "0 Produkte";
if (filteredCount === 1) return this.props.t ? this.props.t('product.countDisplay.oneProduct') : "1 Produkt";
return this.props.t ? this.props.t('product.countDisplay.multipleProducts', { count: filteredCount }) : `${filteredCount} Produkte`;
if (filteredCount === 0) return "0 Produkte";
if (filteredCount === 1) return "1 Produkt";
return `${filteredCount} Produkte`;
} else {
// Filters applied
if (totalCount === 0) return this.props.t ? this.props.t('product.countDisplay.noProducts') : "0 Produkte";
if (totalCount === 1) return this.props.t ? this.props.t('product.countDisplay.filteredOneProduct', { filtered: filteredCount }) : `${filteredCount} von 1 Produkt`;
return this.props.t ? this.props.t('product.countDisplay.filteredProducts', { filtered: filteredCount, total: totalCount }) : `${filteredCount} von ${totalCount} Produkten`;
if (totalCount === 0) return "0 Produkte";
if (totalCount === 1) return `${filteredCount} von 1 Produkt`;
return `${filteredCount} von ${totalCount} Produkten`;
}
}
@@ -328,13 +327,13 @@ class ProductList extends Component {
minWidth: { xs: 120, sm: 140 }
}}
>
<InputLabel id="sort-by-label">{this.props.t ? this.props.t('filters.sorting') : 'Sortierung'}</InputLabel>
<InputLabel id="sort-by-label">Sortierung</InputLabel>
<Select
size="small"
labelId="sort-by-label"
value={(this.state.sortBy==='searchField')&&(window.currentSearchQuery)?this.state.sortBy:this.state.sortBy==='price-low-high'?this.state.sortBy:this.state.sortBy==='price-low-high'?this.state.sortBy:'name'}
onChange={this.handleSortChange}
label={this.props.t ? this.props.t('filters.sorting') : 'Sortierung'}
label="Sortierung"
MenuProps={{
disableScrollLock: true,
anchorOrigin: {
@@ -354,10 +353,10 @@ class ProductList extends Component {
}
}}
>
<MenuItem value="name">{this.props.t ? this.props.t('sorting.name') : 'Name'}</MenuItem>
{window.currentSearchQuery && <MenuItem value="searchField">{this.props.t ? this.props.t('sorting.searchField') : 'Suchbegriff'}</MenuItem>}
<MenuItem value="price-low-high">{this.props.t ? this.props.t('sorting.priceLowHigh') : 'Preis: Niedrig zu Hoch'}</MenuItem>
<MenuItem value="price-high-low">{this.props.t ? this.props.t('sorting.priceHighLow') : 'Preis: Hoch zu Niedrig'}</MenuItem>
<MenuItem value="name">Name</MenuItem>
{window.currentSearchQuery && <MenuItem value="searchField">Suchbegriff</MenuItem>}
<MenuItem value="price-low-high">Preis: Niedrig zu Hoch</MenuItem>
<MenuItem value="price-high-low">Preis: Hoch zu Niedrig</MenuItem>
</Select>
</FormControl>
@@ -369,12 +368,12 @@ class ProductList extends Component {
minWidth: { xs: 80, sm: 100 }
}}
>
<InputLabel id="products-per-page-label">{this.props.t ? this.props.t('filters.perPage') : 'pro Seite'}</InputLabel>
<InputLabel id="products-per-page-label">pro Seite</InputLabel>
<Select
labelId="products-per-page-label"
value={this.state.itemsPerPage}
onChange={this.handleProductsPerPageChange}
label={this.props.t ? this.props.t('filters.perPage') : 'pro Seite'}
label="pro Seite"
MenuProps={{
disableScrollLock: true,
anchorOrigin: {
@@ -463,8 +462,8 @@ class ProductList extends Component {
available={product.available}
manufacturer={product.manufacturer}
vat={product.vat}
cGrundEinheit={product.cGrundEinheit}
fGrundPreis={product.fGrundPreis}
massMenge={product.massMenge}
massEinheit={product.massEinheit}
incoming={product.incomingDate}
neu={product.neu}
thc={product.thc}
@@ -475,8 +474,6 @@ class ProductList extends Component {
socketB={this.props.socketB}
pictureList={product.pictureList}
availableSupplier={product.availableSupplier}
komponenten={product.komponenten}
t={this.props.t}
/>
</Grid>
))}
@@ -498,4 +495,4 @@ class ProductList extends Component {
}
}
export default withI18n()(ProductList);
export default ProductList;

View File

@@ -1,273 +0,0 @@
import React, { useContext, useEffect, useState } from "react";
import Box from "@mui/material/Box";
import Typography from "@mui/material/Typography";
import IconButton from "@mui/material/IconButton";
import ChevronLeft from "@mui/icons-material/ChevronLeft";
import ChevronRight from "@mui/icons-material/ChevronRight";
import CategoryBox from "./CategoryBox.js";
import SocketContext from "../contexts/SocketContext.js";
import { useCarousel } from "../contexts/CarouselContext.js";
import { useTranslation } from 'react-i18next';
// 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 = (language = 'en') => {
const productCache = getProductCache();
if (productCache && productCache[`categoryTree_209_${language}`]) {
const cached = productCache[`categoryTree_209_${language}`];
if (cached.categoryTree) {
return processCategoryTree(cached.categoryTree);
}
}
// Fallback to old cache format if language-specific cache doesn't exist
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 { t, i18n } = useTranslation();
const [rootCategories, setRootCategories] = useState([]);
const [currentLanguage, setCurrentLanguage] = useState(i18n.language);
useEffect(() => {
const initialCategories = initializeCategories(currentLanguage);
setRootCategories(initialCategories);
}, [currentLanguage]);
// Listen for language changes
useEffect(() => {
const handleLanguageChange = (lng) => {
setCurrentLanguage(lng);
// Clear categories to force refetch
setRootCategories([]);
};
i18n.on('languageChanged', handleLanguageChange);
return () => {
i18n.off('languageChanged', handleLanguageChange);
};
}, [i18n]);
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, language: currentLanguage, requestTranslation: true }, (response) => {
if (response && response.success) {
// Use translated data if available, otherwise fall back to original
const categoryTreeToUse = response.translation || response.categoryTree;
if (categoryTreeToUse) {
// Store in cache with language-specific key
try {
if (!window.productCache) window.productCache = {};
window.productCache[`categoryTree_209_${currentLanguage}`] = {
categoryTree: categoryTreeToUse,
timestamp: Date.now(),
};
} catch (err) {
console.error(err);
}
setRootCategories(categoryTreeToUse.children || []);
}
} else if (response && response.categoryTree) {
// Fallback for old response format
// Store in cache with language-specific key
try {
if (!window.productCache) window.productCache = {};
window.productCache[`categoryTree_209_${currentLanguage}`] = {
categoryTree: response.categoryTree,
timestamp: Date.now(),
};
} catch (err) {
console.error(err);
}
setRootCategories(response.categoryTree.children || []);
}
});
}
}, [context, context?.socket?.connected, rootCategories.length, currentLanguage]);
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)"
}}
>
{t('navigation.categories')}
</Typography>
<div
className="carousel-wrapper"
style={{
position: 'relative',
overflow: 'hidden',
width: '100%',
maxWidth: '1200px',
margin: '0 auto',
padding: '0 20px',
boxSizing: 'border-box'
}}
>
{/* Left Arrow */}
<IconButton
onClick={() => moveCarousel("left")}
aria-label="Vorherige Kategorien anzeigen"
style={{
position: 'absolute',
top: '50%',
left: '8px',
transform: 'translateY(-50%)',
zIndex: 1200,
backgroundColor: 'rgba(255, 255, 255, 0.9)',
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.15)',
width: '48px',
height: '48px',
borderRadius: '50%'
}}
>
<ChevronLeft />
</IconButton>
{/* Right Arrow */}
<IconButton
onClick={() => moveCarousel("right")}
aria-label="Nächste Kategorien anzeigen"
style={{
position: 'absolute',
top: '50%',
right: '8px',
transform: 'translateY(-50%)',
zIndex: 1200,
backgroundColor: 'rgba(255, 255, 255, 0.9)',
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.15)',
width: '48px',
height: '48px',
borderRadius: '50%'
}}
>
<ChevronRight />
</IconButton>
<div
className="carousel-container"
style={{
position: 'relative',
overflow: 'hidden',
padding: '20px 0',
width: '100%',
maxWidth: '1080px',
margin: '0 auto',
zIndex: 1,
boxSizing: 'border-box'
}}
>
<div
className="home-carousel-track"
ref={carouselRef}
style={{
display: 'flex',
gap: '16px',
transition: 'none',
alignItems: 'flex-start',
width: 'fit-content',
overflow: 'visible',
position: 'relative',
transform: 'translateX(0px)',
margin: '0 auto'
}}
>
{displayCategories.map((category, index) => (
<div
key={`${category.id}-${index}`}
className="carousel-item"
style={{
flex: '0 0 130px',
width: '130px',
maxWidth: '130px',
minWidth: '130px',
height: '130px',
maxHeight: '130px',
minHeight: '130px',
boxSizing: 'border-box',
position: 'relative'
}}
>
<CategoryBox
id={category.id}
name={category.name}
seoName={category.seoName}
image={category.image}
bgcolor={category.bgcolor}
/>
</div>
))}
</div>
</div>
</div>
</Box>
);
};
export default SharedCarousel;

View File

@@ -10,9 +10,7 @@ import CloseIcon from '@mui/icons-material/Close';
import { useNavigate } from 'react-router-dom';
import LoginComponent from '../LoginComponent.js';
import CartDropdown from '../CartDropdown.js';
import LanguageSwitcher from '../LanguageSwitcher.js';
import { isUserLoggedIn } from '../LoginComponent.js';
import { withI18n } from '../../i18n/withTranslation.js';
function getBadgeNumber() {
let count = 0;
@@ -118,14 +116,14 @@ class ButtonGroup extends Component {
}
render() {
const { socket, navigate, t } = this.props;
const { socket, navigate } = this.props;
const { isCartOpen } = this.state;
const cartItems = Array.isArray(window.cart) ? window.cart : [];
return (
<Box sx={{ display: 'flex', gap: { xs: 0.5, sm: 1 } }}>
<LanguageSwitcher />
<LoginComponent socket={socket} />
<IconButton
@@ -166,7 +164,7 @@ class ButtonGroup extends Component {
>
<CloseIcon />
</IconButton>
<Typography variant="h6">{t ? t('cart.title') : 'Warenkorb'}</Typography>
<Typography variant="h6">Warenkorb</Typography>
</Box>
<Divider sx={{ mb: 2 }} />
@@ -175,7 +173,7 @@ class ButtonGroup extends Component {
if (isUserLoggedIn().isLoggedIn) {
this.toggleCart(); // Close the cart drawer
navigate('/profile#cart');
navigate('/profile');
} else if (window.openLoginDrawer) {
window.openLoginDrawer(); // Call global function to open login drawer
this.toggleCart(); // Close the cart drawer
@@ -191,11 +189,10 @@ class ButtonGroup extends Component {
}
}
// Wrapper for ButtonGroup to provide navigate function and translations
// Wrapper for ButtonGroup to provide navigate function
const ButtonGroupWithRouter = (props) => {
const navigate = useNavigate();
const ButtonGroupWithTranslation = withI18n()(ButtonGroup);
return <ButtonGroupWithTranslation {...props} navigate={navigate} />;
return <ButtonGroup {...props} navigate={navigate} />;
};
export default ButtonGroupWithRouter;

View File

@@ -8,7 +8,6 @@ import { Link } from "react-router-dom";
import HomeIcon from "@mui/icons-material/Home";
import MenuIcon from "@mui/icons-material/Menu";
import CloseIcon from "@mui/icons-material/Close";
import { withI18n } from "../../i18n/withTranslation.js";
class CategoryList extends Component {
findCategoryById = (category, targetId) => {
@@ -50,9 +49,6 @@ class CategoryList extends Component {
constructor(props) {
super(props);
// Get current language from props (provided by withI18n HOC)
const currentLanguage = props.languageContext?.currentLanguage || 'de';
// Check for cached data during SSR/initial render
let initialState = {
categoryTree: null,
@@ -62,7 +58,6 @@ class CategoryList extends Component {
activePath: [], // Array of active category objects for each level
fetchedCategories: false,
mobileMenuOpen: false, // State for mobile collapsible menu
currentLanguage: currentLanguage,
};
// Try to get cached data for SSR
@@ -72,7 +67,7 @@ class CategoryList extends Component {
(typeof window !== "undefined" && window.productCache);
if (productCache) {
const cacheKey = `categoryTree_209_${currentLanguage}`;
const cacheKey = "categoryTree_209";
const cachedData = productCache[cacheKey];
if (cachedData && cachedData.categoryTree) {
const { categoryTree, timestamp } = cachedData;
@@ -132,27 +127,8 @@ class CategoryList extends Component {
}
componentDidUpdate(prevProps) {
// Handle language changes
const currentLanguage = this.props.languageContext?.currentLanguage || 'de';
const prevLanguage = prevProps.languageContext?.currentLanguage || 'de';
if (currentLanguage !== prevLanguage) {
// Language changed, need to refetch categories
this.setState({
currentLanguage: currentLanguage,
fetchedCategories: false,
categoryTree: null,
level1Categories: [],
level2Categories: [],
level3Categories: [],
activePath: [],
}, () => {
this.fetchCategories();
});
return;
}
// Handle socket connection changes
const wasConnected = prevProps.socket && prevProps.socket.connected;
const isNowConnected = this.props.socket && this.props.socket.connected;
@@ -192,9 +168,6 @@ class CategoryList extends Component {
return;
}
// Get current language from state
const currentLanguage = this.state.currentLanguage || 'de';
// Initialize global cache object if it doesn't exist
// @note Handle both SSR (global.window) and browser (window) environments
const windowObj = (typeof global !== "undefined" && global.window) ||
@@ -206,7 +179,7 @@ class CategoryList extends Component {
// Check if we have a valid cache in the global object
try {
const cacheKey = `categoryTree_209_${currentLanguage}`;
const cacheKey = "categoryTree_209";
const cachedData = windowObj && windowObj.productCache ? windowObj.productCache[cacheKey] : null;
if (cachedData) {
const { categoryTree, fetching } = cachedData;
@@ -243,7 +216,7 @@ class CategoryList extends Component {
}
// Mark as being fetched to prevent concurrent calls
const cacheKey = `categoryTree_209_${currentLanguage}`;
const cacheKey = "categoryTree_209";
if (windowObj && windowObj.productCache) {
windowObj.productCache[cacheKey] = {
fetching: true,
@@ -253,59 +226,30 @@ class CategoryList extends Component {
this.setState({ fetchedCategories: true });
//console.log('CategoryList: Fetching categories from socket');
socket.emit("categoryList", { categoryId: 209, language: currentLanguage, requestTranslation: true }, (response) => {
if (response && response.success) {
// Use translated data if available, otherwise fall back to original
const categoryTreeToUse = response.translation || response.categoryTree;
socket.emit("categoryList", { categoryId: 209 }, (response) => {
if (response && response.categoryTree) {
if (categoryTreeToUse) {
// Store in global cache with timestamp
try {
const cacheKey = `categoryTree_209_${currentLanguage}`;
if (windowObj && windowObj.productCache) {
windowObj.productCache[cacheKey] = {
categoryTree: categoryTreeToUse,
timestamp: Date.now(),
fetching: false,
};
}
} catch (err) {
console.error("Error writing to cache:", err);
}
this.processCategoryTree(categoryTreeToUse);
} else {
console.error('No category tree found in response');
// Clear cache on error
try {
const cacheKey = `categoryTree_209_${currentLanguage}`;
if (windowObj && windowObj.productCache) {
windowObj.productCache[cacheKey] = {
categoryTree: null,
timestamp: Date.now(),
fetching: false,
};
}
} catch (err) {
console.error("Error writing to cache:", err);
}
this.setState({
categoryTree: null,
level1Categories: [],
level2Categories: [],
level3Categories: [],
activePath: [],
});
}
} else {
console.error('Failed to fetch categories:', response);
// Store in global cache with timestamp
try {
const cacheKey = `categoryTree_209_${currentLanguage}`;
const cacheKey = "categoryTree_209";
if (windowObj && windowObj.productCache) {
windowObj.productCache[cacheKey] = {
categoryTree: response.categoryTree,
timestamp: Date.now(),
fetching: false,
};
}
} catch (err) {
console.error("Error writing to cache:", err);
}
this.processCategoryTree(response.categoryTree);
} else {
try {
const cacheKey = "categoryTree_209";
if (windowObj && windowObj.productCache) {
windowObj.productCache[cacheKey] = {
categoryTree: null,
timestamp: Date.now(),
fetching: false,
};
}
} catch (err) {
@@ -466,7 +410,7 @@ class CategoryList extends Component {
zIndex: 2,
}}
>
{this.props.t ? this.props.t('navigation.home') : 'Startseite'}
Startseite
</Box>
{/* Thin text (positioned on top) */}
<Box
@@ -480,7 +424,7 @@ class CategoryList extends Component {
zIndex: 1,
}}
>
{this.props.t ? this.props.t('navigation.home') : 'Startseite'}
Startseite
</Box>
</Box>
)}
@@ -651,10 +595,7 @@ class CategoryList extends Component {
onClick={this.handleMobileMenuToggle}
role="button"
tabIndex={0}
aria-label={this.props.t ?
(mobileMenuOpen ? this.props.t('navigation.categoriesClose') : this.props.t('navigation.categoriesOpen')) :
(mobileMenuOpen ? "Kategorien schließen" : "Kategorien öffnen")
}
aria-label={mobileMenuOpen ? "Kategorien schließen" : "Kategorien öffnen"}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
@@ -666,7 +607,7 @@ class CategoryList extends Component {
fontWeight: "bold",
textShadow: "0 1px 2px rgba(0,0,0,0.3)"
}}>
{this.props.t ? this.props.t('navigation.categories') : 'Kategorien'}
Kategorien
</Typography>
<Box sx={{ display: "flex", alignItems: "center" }}>
{mobileMenuOpen ? <CloseIcon /> : <MenuIcon />}
@@ -687,4 +628,4 @@ class CategoryList extends Component {
}
}
export default withI18n()(CategoryList);
export default CategoryList;

View File

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

View File

@@ -1,8 +1,7 @@
import React from "react";
import { Box, TextField, Typography } from "@mui/material";
import { withI18n } from "../../i18n/withTranslation.js";
const AddressForm = ({ title, address, onChange, errors, namePrefix, t }) => {
const AddressForm = ({ title, address, onChange, errors, namePrefix }) => {
// Helper function to determine if a required field should show error styling
const getRequiredFieldError = (fieldName, value) => {
const isEmpty = !value || value.trim() === "";
@@ -37,7 +36,7 @@ const AddressForm = ({ title, address, onChange, errors, namePrefix, t }) => {
}}
>
<TextField
label={t ? t('checkout.addressFields.firstName') : 'Vorname'}
label="Vorname"
name="firstName"
value={address.firstName}
onChange={onChange}
@@ -50,7 +49,7 @@ const AddressForm = ({ title, address, onChange, errors, namePrefix, t }) => {
}}
/>
<TextField
label={t ? t('checkout.addressFields.lastName') : 'Nachname'}
label="Nachname"
name="lastName"
value={address.lastName}
onChange={onChange}
@@ -63,7 +62,7 @@ const AddressForm = ({ title, address, onChange, errors, namePrefix, t }) => {
}}
/>
<TextField
label={t ? t('checkout.addressFields.addressSupplement') : 'Adresszusatz'}
label="Adresszusatz"
name="addressAddition"
value={address.addressAddition || ""}
onChange={onChange}
@@ -71,7 +70,7 @@ const AddressForm = ({ title, address, onChange, errors, namePrefix, t }) => {
InputLabelProps={{ shrink: true }}
/>
<TextField
label={t ? t('checkout.addressFields.street') : 'Straße'}
label="Straße"
name="street"
value={address.street}
onChange={onChange}
@@ -84,7 +83,7 @@ const AddressForm = ({ title, address, onChange, errors, namePrefix, t }) => {
}}
/>
<TextField
label={t ? t('checkout.addressFields.houseNumber') : 'Hausnummer'}
label="Hausnummer"
name="houseNumber"
value={address.houseNumber}
onChange={onChange}
@@ -97,7 +96,7 @@ const AddressForm = ({ title, address, onChange, errors, namePrefix, t }) => {
}}
/>
<TextField
label={t ? t('checkout.addressFields.postalCode') : 'PLZ'}
label="PLZ"
name="postalCode"
value={address.postalCode}
onChange={onChange}
@@ -110,7 +109,7 @@ const AddressForm = ({ title, address, onChange, errors, namePrefix, t }) => {
}}
/>
<TextField
label={t ? t('checkout.addressFields.city') : 'Stadt'}
label="Stadt"
name="city"
value={address.city}
onChange={onChange}
@@ -123,7 +122,7 @@ const AddressForm = ({ title, address, onChange, errors, namePrefix, t }) => {
}}
/>
<TextField
label={t ? t('checkout.addressFields.country') : 'Land'}
label="Land"
name="country"
value={address.country}
onChange={onChange}
@@ -136,4 +135,4 @@ const AddressForm = ({ title, address, onChange, errors, namePrefix, t }) => {
);
};
export default withI18n()(AddressForm);
export default AddressForm;

View File

@@ -6,7 +6,6 @@ import PaymentConfirmationDialog from "./PaymentConfirmationDialog.js";
import OrderProcessingService from "./OrderProcessingService.js";
import CheckoutValidation from "./CheckoutValidation.js";
import SocketContext from "../../contexts/SocketContext.js";
import { withI18n } from "../../i18n/index.js";
class CartTab extends Component {
constructor(props) {
@@ -52,6 +51,9 @@ class CartTab extends Component {
showStripePayment: false,
StripeComponent: null,
isLoadingStripe: false,
showMolliePayment: false,
MollieComponent: null,
isLoadingMollie: false,
showPaymentConfirmation: false,
orderCompleted: false,
originalCartItems: []
@@ -117,7 +119,7 @@ class CartTab extends Component {
// Determine payment method - respect constraints
let prefillPaymentMethod = template.payment_method || "wire";
const paymentMethodMap = {
"credit_card": "mollie",/*stripe*/
"credit_card": "mollie",//stripe
"bank_transfer": "wire",
"cash_on_delivery": "onDelivery",
"cash": "cash"
@@ -293,7 +295,7 @@ class CartTab extends Component {
};
validateAddressForm = () => {
const errors = CheckoutValidation.validateAddressForm(this.state, this.props.t);
const errors = CheckoutValidation.validateAddressForm(this.state);
this.setState({ addressFormErrors: errors });
return Object.keys(errors).length === 0;
};
@@ -320,10 +322,31 @@ class CartTab extends Component {
}
};
loadMollieComponent = async () => {
this.setState({ isLoadingMollie: true });
try {
const { default: Mollie } = await import("../Mollie.js");
this.setState({
MollieComponent: Mollie,
showMolliePayment: true,
isCompletingOrder: false,
isLoadingMollie: false,
});
} catch (error) {
console.error("Failed to load Mollie component:", error);
this.setState({
isCompletingOrder: false,
isLoadingMollie: false,
completionError: "Failed to load payment component. Please try again.",
});
}
};
handleCompleteOrder = () => {
this.setState({ completionError: null }); // Clear previous errors
const validationError = CheckoutValidation.getValidationErrorMessage(this.state, false, this.props.t);
const validationError = CheckoutValidation.getValidationErrorMessage(this.state);
if (validationError) {
this.setState({ completionError: validationError });
this.validateAddressForm(); // To show field-specific errors
@@ -364,38 +387,23 @@ class CartTab extends Component {
this.orderService.createStripeIntent(totalAmount, this.loadStripeComponent);
return;
}
// Handle molllie payment differently
// Handle Mollie payment differently
if (paymentMethod === "mollie") {
// Store the cart items used for mollie payment in sessionStorage for later reference
// 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);
console.error("Failed to store Mollie payment cart:", error);
}
// Calculate total amount for mollie
// Calculate total amount for Mollie
const subtotal = cartItems.reduce(
(total, item) => total + item.price * item.quantity,
0
);
const totalAmount = Math.round((subtotal + deliveryCost) * 100) / 100;
const totalAmount = Math.round((subtotal + deliveryCost) * 100); // Convert to cents
// Prepare complete order data for Mollie intent creation
const mollieOrderData = {
amount: totalAmount,
items: cartItems,
invoiceAddress,
deliveryAddress: useSameAddress ? invoiceAddress : deliveryAddress,
deliveryMethod,
paymentMethod: "mollie",
deliveryCost,
note,
domain: window.location.origin,
saveAddressForFuture,
};
this.orderService.createMollieIntent(mollieOrderData);
this.orderService.createMollieIntent(totalAmount, this.loadMollieComponent);
return;
}
@@ -433,6 +441,9 @@ class CartTab extends Component {
showStripePayment,
StripeComponent,
isLoadingStripe,
showMolliePayment,
MollieComponent,
isLoadingMollie,
showPaymentConfirmation,
orderCompleted,
} = this.state;
@@ -440,7 +451,7 @@ class CartTab extends Component {
const deliveryCost = this.orderService.getDeliveryCost();
const { isPickupOnly, hasStecklinge } = CheckoutValidation.getCartItemFlags(cartItems);
const preSubmitError = CheckoutValidation.getValidationErrorMessage(this.state, false, this.props.t);
const preSubmitError = CheckoutValidation.getValidationErrorMessage(this.state);
const displayError = completionError || preSubmitError;
return (
@@ -469,7 +480,7 @@ class CartTab extends Component {
<CartDropdown
cartItems={cartItems}
socket={this.context.socket}
showDetailedSummary={showStripePayment}
showDetailedSummary={showStripePayment || showMolliePayment}
deliveryMethod={deliveryMethod}
deliveryCost={deliveryCost}
/>
@@ -477,10 +488,10 @@ class CartTab extends Component {
{cartItems.length > 0 && (
<Box sx={{ mt: 3 }}>
{isLoadingStripe ? (
{isLoadingStripe || isLoadingMollie ? (
<Box sx={{ textAlign: 'center', py: 4 }}>
<Typography variant="body1">
{this.props.t ? this.props.t('payment.loadingPaymentComponent') : 'Zahlungskomponente wird geladen...'}
Zahlungskomponente wird geladen...
</Typography>
</Box>
) : showStripePayment && StripeComponent ? (
@@ -498,14 +509,34 @@ class CartTab extends Component {
}
}}
>
{this.props.t ? this.props.t('cart.backToOrder') : '← Zurück zur Bestellung'}
Zurück zur Bestellung
</Button>
</Box>
<StripeComponent clientSecret={stripeClientSecret} />
</>
) : showMolliePayment && MollieComponent ? (
<>
<Box sx={{ mb: 2 }}>
<Button
variant="outlined"
onClick={() => this.setState({ showMolliePayment: false })}
sx={{
color: '#2e7d32',
borderColor: '#2e7d32',
'&:hover': {
backgroundColor: 'rgba(46, 125, 50, 0.04)',
borderColor: '#1b5e20'
}
}}
>
Zurück zur Bestellung
</Button>
</Box>
<MollieComponent />
</>
) : (
<CheckoutForm
paymentMethod={paymentMethod}
paymentMethod={paymentMethod}
invoiceAddress={invoiceAddress}
deliveryAddress={deliveryAddress}
useSameAddress={useSameAddress}
@@ -513,7 +544,7 @@ class CartTab extends Component {
addressFormErrors={addressFormErrors}
termsAccepted={termsAccepted}
note={note}
deliveryMethod={deliveryMethod}
deliveryMethod={deliveryMethod}
hasStecklinge={hasStecklinge}
isPickupOnly={isPickupOnly}
deliveryCost={deliveryCost}
@@ -542,4 +573,4 @@ class CartTab extends Component {
// Set static contextType to access the socket
CartTab.contextType = SocketContext;
export default withI18n()(CartTab);
export default CartTab;

View File

@@ -4,7 +4,6 @@ import AddressForm from "./AddressForm.js";
import DeliveryMethodSelector from "./DeliveryMethodSelector.js";
import PaymentMethodSelector from "./PaymentMethodSelector.js";
import OrderSummary from "./OrderSummary.js";
import { withI18n } from "../../i18n/withTranslation.js";
class CheckoutForm extends Component {
render() {
@@ -41,7 +40,7 @@ class CheckoutForm extends Component {
{paymentMethod !== "cash" && (
<>
<AddressForm
title={this.props.t ? this.props.t('checkout.invoiceAddress') : 'Rechnungsadresse'}
title="Rechnungsadresse"
address={invoiceAddress}
onChange={onInvoiceAddressChange}
errors={addressFormErrors}
@@ -58,7 +57,7 @@ class CheckoutForm extends Component {
}
label={
<Typography variant="body2">
{this.props.t ? this.props.t('checkout.saveForFuture') : 'Für zukünftige Bestellungen speichern'}
Für zukünftige Bestellungen speichern
</Typography>
}
sx={{ mb: 2 }}
@@ -71,12 +70,13 @@ class CheckoutForm extends Component {
variant="body1"
sx={{ mb: 2, fontWeight: "bold", color: "#2e7d32" }}
>
{this.props.t ? this.props.t('checkout.pickupDate') : 'Für welchen Termin ist die Abholung der Stecklinge gewünscht?'}
Für welchen Termin ist die Abholung der Stecklinge
gewünscht?
</Typography>
)}
<TextField
label={this.props.t ? this.props.t('checkout.note') : 'Anmerkung'}
label="Anmerkung"
name="note"
value={note}
onChange={onNoteChange}
@@ -93,7 +93,6 @@ class CheckoutForm extends Component {
deliveryMethod={deliveryMethod}
onChange={onDeliveryMethodChange}
isPickupOnly={isPickupOnly || hasStecklinge}
cartItems={cartItems}
/>
{(deliveryMethod === "DHL" || deliveryMethod === "DPD") && (
@@ -108,7 +107,7 @@ class CheckoutForm extends Component {
}
label={
<Typography variant="body1">
{this.props.t ? this.props.t('checkout.sameAddress') : 'Lieferadresse ist identisch mit Rechnungsadresse'}
Lieferadresse ist identisch mit Rechnungsadresse
</Typography>
}
sx={{ mb: 2 }}
@@ -116,7 +115,7 @@ class CheckoutForm extends Component {
{!useSameAddress && (
<AddressForm
title={this.props.t ? this.props.t('checkout.deliveryAddress') : 'Lieferadresse'}
title="Lieferadresse"
address={deliveryAddress}
onChange={onDeliveryAddressChange}
errors={addressFormErrors}
@@ -151,7 +150,8 @@ class CheckoutForm extends Component {
}
label={
<Typography variant="body2">
{this.props.t ? this.props.t('checkout.termsAccept') : 'Ich habe die AGBs, die Datenschutzerklärung und die Bestimmungen zum Widerrufsrecht gelesen'}
Ich habe die AGBs, die Datenschutzerklärung und die
Bestimmungen zum Widerrufsrecht gelesen
</Typography>
}
sx={{ mb: 3, mt: 2 }}
@@ -174,12 +174,12 @@ class CheckoutForm extends Component {
disabled={isCompletingOrder || !!preSubmitError}
>
{isCompletingOrder
? (this.props.t ? this.props.t('checkout.processingOrder') : 'Bestellung wird verarbeitet...')
: (this.props.t ? this.props.t('checkout.completeOrder') : 'Bestellung abschließen')}
? "Bestellung wird verarbeitet..."
: "Bestellung abschließen"}
</Button>
</>
);
}
}
export default withI18n()(CheckoutForm);
export default CheckoutForm;

View File

@@ -1,5 +1,5 @@
class CheckoutValidation {
static validateAddressForm(state, t = null) {
static validateAddressForm(state) {
const {
invoiceAddress,
deliveryAddress,
@@ -12,15 +12,15 @@ class CheckoutValidation {
// Validate invoice address (skip if payment method is "cash")
if (paymentMethod !== "cash") {
if (!invoiceAddress.firstName)
errors.invoiceFirstName = t ? t('checkout.validationErrors.firstNameRequired') : "Vorname erforderlich";
errors.invoiceFirstName = "Vorname erforderlich";
if (!invoiceAddress.lastName)
errors.invoiceLastName = t ? t('checkout.validationErrors.lastNameRequired') : "Nachname erforderlich";
if (!invoiceAddress.street) errors.invoiceStreet = t ? t('checkout.validationErrors.streetRequired') : "Straße erforderlich";
errors.invoiceLastName = "Nachname erforderlich";
if (!invoiceAddress.street) errors.invoiceStreet = "Straße erforderlich";
if (!invoiceAddress.houseNumber)
errors.invoiceHouseNumber = t ? t('checkout.validationErrors.houseNumberRequired') : "Hausnummer erforderlich";
errors.invoiceHouseNumber = "Hausnummer erforderlich";
if (!invoiceAddress.postalCode)
errors.invoicePostalCode = t ? t('checkout.validationErrors.postalCodeRequired') : "PLZ erforderlich";
if (!invoiceAddress.city) errors.invoiceCity = t ? t('checkout.validationErrors.cityRequired') : "Stadt erforderlich";
errors.invoicePostalCode = "PLZ erforderlich";
if (!invoiceAddress.city) errors.invoiceCity = "Stadt erforderlich";
}
// Validate delivery address for shipping methods that require it
@@ -29,37 +29,37 @@ class CheckoutValidation {
(deliveryMethod === "DHL" || deliveryMethod === "DPD")
) {
if (!deliveryAddress.firstName)
errors.deliveryFirstName = t ? t('checkout.validationErrors.firstNameRequired') : "Vorname erforderlich";
errors.deliveryFirstName = "Vorname erforderlich";
if (!deliveryAddress.lastName)
errors.deliveryLastName = t ? t('checkout.validationErrors.lastNameRequired') : "Nachname erforderlich";
errors.deliveryLastName = "Nachname erforderlich";
if (!deliveryAddress.street)
errors.deliveryStreet = t ? t('checkout.validationErrors.streetRequired') : "Straße erforderlich";
errors.deliveryStreet = "Straße erforderlich";
if (!deliveryAddress.houseNumber)
errors.deliveryHouseNumber = t ? t('checkout.validationErrors.houseNumberRequired') : "Hausnummer erforderlich";
errors.deliveryHouseNumber = "Hausnummer erforderlich";
if (!deliveryAddress.postalCode)
errors.deliveryPostalCode = t ? t('checkout.validationErrors.postalCodeRequired') : "PLZ erforderlich";
if (!deliveryAddress.city) errors.deliveryCity = t ? t('checkout.validationErrors.cityRequired') : "Stadt erforderlich";
errors.deliveryPostalCode = "PLZ erforderlich";
if (!deliveryAddress.city) errors.deliveryCity = "Stadt erforderlich";
}
return errors;
}
static getValidationErrorMessage(state, isAddressOnly = false, t = null) {
static getValidationErrorMessage(state, isAddressOnly = false) {
const { termsAccepted } = state;
const addressErrors = this.validateAddressForm(state, t);
const addressErrors = this.validateAddressForm(state);
if (isAddressOnly) {
return addressErrors;
}
if (Object.keys(addressErrors).length > 0) {
return t ? t('checkout.addressValidationError') : "Bitte überprüfen Sie Ihre Eingaben in den Adressfeldern.";
return "Bitte überprüfen Sie Ihre Eingaben in den Adressfeldern.";
}
// Validate terms acceptance
if (!termsAccepted) {
return t ? t('checkout.termsValidationError') : "Bitte akzeptieren Sie die AGBs, Datenschutzerklärung und Widerrufsrecht, um fortzufahren.";
return "Bitte akzeptieren Sie die AGBs, Datenschutzerklärung und Widerrufsrecht, um fortzufahren.";
}
return null;
@@ -82,7 +82,7 @@ class CheckoutValidation {
// Prefer stripe when available and meets minimum amount
if (deliveryMethod === "DHL" || deliveryMethod === "DPD" || deliveryMethod === "Abholung") {
return "wire";/*stripe*/
return "stripe";
}
// Fall back to wire transfer
@@ -106,21 +106,11 @@ class CheckoutValidation {
newPaymentMethod = "wire";
}
// Allow mollie for DHL, DPD, and Abholung delivery methods, but check minimum amount
if (deliveryMethod !== "DHL" && deliveryMethod !== "DPD" && deliveryMethod !== "Abholung" && paymentMethod === "mollie") {
newPaymentMethod = "wire";
}
// Check minimum amount for stripe payments
if (paymentMethod === "stripe" && totalAmount < 0.50) {
newPaymentMethod = "wire";
}
// Check minimum amount for mollie payments
if (paymentMethod === "mollie" && totalAmount < 0.50) {
newPaymentMethod = "wire";
}
if (deliveryMethod !== "Abholung" && paymentMethod === "cash") {
newPaymentMethod = "wire";
}

View File

@@ -3,42 +3,34 @@ import Box from '@mui/material/Box';
import Typography from '@mui/material/Typography';
import Radio from '@mui/material/Radio';
import Checkbox from '@mui/material/Checkbox';
import { withI18n } from '../../i18n/withTranslation.js';
const DeliveryMethodSelector = ({ deliveryMethod, onChange, isPickupOnly, cartItems = [], t }) => {
// Calculate cart value for free shipping threshold
const cartValue = cartItems.reduce((total, item) => total + item.price * item.quantity, 0);
const isFreeShipping = cartValue >= 100;
const remainingForFreeShipping = Math.max(0, 100 - cartValue);
const DeliveryMethodSelector = ({ deliveryMethod, onChange, isPickupOnly }) => {
const deliveryOptions = [
{
id: 'DHL',
name: 'DHL',
description: isPickupOnly ? (t ? t('delivery.descriptions.notAvailable') : "nicht auswählbar weil ein oder mehrere Artikel nur abgeholt werden können") :
isFreeShipping ? (t ? t('delivery.descriptions.standardFree') : 'Standardversand - KOSTENLOS ab 100€ Warenwert!') : (t ? t('delivery.descriptions.standard') : 'Standardversand'),
price: isFreeShipping ? (t ? t('delivery.prices.free') : 'kostenlos') : (t ? t('delivery.prices.dhl') : '6,99 €'),
description: isPickupOnly ? "nicht auswählbar weil ein oder mehrere Artikel nur abgeholt werden können" : 'Standardversand',
price: '6,99 €',
disabled: isPickupOnly
},
{
id: 'DPD',
name: 'DPD',
description: isPickupOnly ? (t ? t('delivery.descriptions.notAvailable') : "nicht auswählbar weil ein oder mehrere Artikel nur abgeholt werden können") :
isFreeShipping ? (t ? t('delivery.descriptions.standardFree') : 'Standardversand - KOSTENLOS ab 100€ Warenwert!') : (t ? t('delivery.descriptions.standard') : 'Standardversand'),
price: isFreeShipping ? (t ? t('delivery.prices.free') : 'kostenlos') : (t ? t('delivery.prices.dpd') : '4,90 €'),
description: isPickupOnly ? "nicht auswählbar weil ein oder mehrere Artikel nur abgeholt werden können" : 'Standardversand',
price: '4,90 €',
disabled: isPickupOnly
},
{
id: 'Sperrgut',
name: t ? t('delivery.methods.sperrgutName') : 'Sperrgut',
description: t ? t('delivery.descriptions.bulky') : 'Für große und schwere Artikel',
price: t ? t('delivery.prices.sperrgut') : '28,99 €',
name: 'Sperrgut',
description: 'Für große und schwere Artikel',
price: '28,99 €',
disabled: true,
isCheckbox: true
},
{
id: 'Abholung',
name: t ? t('delivery.methods.pickup') : 'Abholung in der Filiale',
name: 'Abholung in der Filiale',
description: '',
price: ''
}
@@ -47,7 +39,7 @@ const DeliveryMethodSelector = ({ deliveryMethod, onChange, isPickupOnly, cartIt
return (
<>
<Typography variant="h6" gutterBottom>
{t ? t('delivery.selector.title') : 'Versandart wählen'}
Versandart wählen
</Typography>
<Box sx={{ mb: 3 }}>
@@ -122,44 +114,9 @@ const DeliveryMethodSelector = ({ deliveryMethod, onChange, isPickupOnly, cartIt
</Typography>
</Box>
))}
{/* Free shipping information */}
{!isFreeShipping && remainingForFreeShipping > 0 && (
<Box sx={{
mt: 2,
p: 2,
backgroundColor: '#f0f8ff',
borderRadius: 1,
border: '1px solid #2196f3'
}}>
<Typography variant="body2" color="primary" sx={{ fontWeight: 'medium' }}>
{t ? t('delivery.selector.freeShippingInfo') : '💡 Versandkostenfrei ab 100€ Warenwert!'}
</Typography>
<Typography variant="body2" color="text.secondary">
{t ? t('delivery.selector.remainingForFree', { amount: remainingForFreeShipping.toFixed(2).replace('.', ',') }) : `Noch ${remainingForFreeShipping.toFixed(2).replace('.', ',')}€ für kostenlosen Versand hinzufügen.`}
</Typography>
</Box>
)}
{isFreeShipping && (
<Box sx={{
mt: 2,
p: 2,
backgroundColor: '#e8f5e8',
borderRadius: 1,
border: '1px solid #2e7d32'
}}>
<Typography variant="body2" color="success.main" sx={{ fontWeight: 'medium' }}>
{t ? t('delivery.selector.congratsFreeShipping') : '🎉 Glückwunsch! Sie erhalten kostenlosen Versand!'}
</Typography>
<Typography variant="body2" color="text.secondary">
{t ? t('delivery.selector.cartQualifiesFree', { amount: cartValue.toFixed(2).replace('.', ',') }) : `Ihr Warenkorb von ${cartValue.toFixed(2).replace('.', ',')}€ qualifiziert sich für kostenlosen Versand.`}
</Typography>
</Box>
)}
</Box>
</>
);
};
export default withI18n()(DeliveryMethodSelector);
export default DeliveryMethodSelector;

View File

@@ -15,36 +15,22 @@ import {
TableRow,
Paper
} from '@mui/material';
import { useTranslation } from 'react-i18next';
const OrderDetailsDialog = ({ open, onClose, order }) => {
const { t } = useTranslation();
if (!order) {
return null;
}
const currencyFormatter = new Intl.NumberFormat("de-DE", { style: "currency", currency: "EUR" });
// Helper function to translate payment methods
const getPaymentMethodDisplay = (paymentMethod) => {
if (!paymentMethod) return t('orders.details.notSpecified');
switch (paymentMethod.toLowerCase()) {
case 'wire':
return t('payment.methods.bankTransfer');
default:
return paymentMethod;
}
};
const handleCancelOrder = () => {
// Implement order cancellation logic here
console.log(`Cancel order: ${order.orderId}`);
onClose(); // Close the dialog after action
};
const total = order.items.reduce((acc, item) => acc + item.price * item.quantity_ordered, 0);
const subtotal = order.items.reduce((acc, item) => acc + item.price * item.quantity_ordered, 0);
const total = subtotal + order.delivery_cost;
// Calculate VAT breakdown similar to CartDropdown
const vatCalculations = order.items.reduce((acc, item) => {
@@ -66,10 +52,10 @@ const OrderDetailsDialog = ({ open, onClose, order }) => {
return (
<Dialog open={open} onClose={onClose} maxWidth="md" fullWidth>
<DialogTitle>{t('orders.details.title', { orderId: order.orderId })}</DialogTitle>
<DialogTitle>Bestelldetails: {order.orderId}</DialogTitle>
<DialogContent>
<Box sx={{ mb: 2 }}>
<Typography variant="h6">{t('orders.details.deliveryAddress')}</Typography>
<Typography variant="h6">Lieferadresse</Typography>
<Typography>{order.shipping_address_name}</Typography>
<Typography>{order.shipping_address_street} {order.shipping_address_house_number}</Typography>
<Typography>{order.shipping_address_postal_code} {order.shipping_address_city}</Typography>
@@ -77,7 +63,7 @@ const OrderDetailsDialog = ({ open, onClose, order }) => {
</Box>
<Box sx={{ mb: 2 }}>
<Typography variant="h6">{t('orders.details.invoiceAddress')}</Typography>
<Typography variant="h6">Rechnungsadresse</Typography>
<Typography>{order.invoice_address_name}</Typography>
<Typography>{order.invoice_address_street} {order.invoice_address_house_number}</Typography>
<Typography>{order.invoice_address_postal_code} {order.invoice_address_city}</Typography>
@@ -86,29 +72,28 @@ const OrderDetailsDialog = ({ open, onClose, order }) => {
{/* Order Details Section */}
<Box sx={{ mb: 2 }}>
<Typography variant="h6" gutterBottom>{t('orders.details.orderDetails')}</Typography>
<Typography variant="h6" gutterBottom>Bestelldetails</Typography>
<Box sx={{ display: 'flex', gap: 4 }}>
<Box>
<Typography variant="body2" color="text.secondary">{t('orders.details.deliveryMethod')}</Typography>
<Typography variant="body1">{order.deliveryMethod || order.delivery_method || t('orders.details.notSpecified')}</Typography>
<Typography variant="body2" color="text.secondary">Lieferart:</Typography>
<Typography variant="body1">{order.deliveryMethod || order.delivery_method || 'Nicht angegeben'}</Typography>
</Box>
<Box>
<Typography variant="body2" color="text.secondary">{t('orders.details.paymentMethod')}</Typography>
<Typography variant="body1">{getPaymentMethodDisplay(order.paymentMethod || order.payment_method)}</Typography>
<Typography variant="body2" color="text.secondary">Zahlungsart:</Typography>
<Typography variant="body1">{order.paymentMethod || order.payment_method || 'Nicht angegeben'}</Typography>
</Box>
</Box>
</Box>
<Typography variant="h6" gutterBottom>{t('orders.details.orderedItems')}</Typography>
<Typography variant="h6" gutterBottom>Bestellte Artikel</Typography>
<TableContainer component={Paper}>
<Table>
<TableHead>
<TableRow>
<TableCell>{t('orders.details.item')}</TableCell>
<TableCell align="right">{t('orders.details.quantity')}</TableCell>
<TableCell align="right">{t('orders.details.price')}</TableCell>
<TableCell align="right">{t('product.vatShort')}</TableCell>
<TableCell align="right">{t('orders.details.total')}</TableCell>
<TableCell>Artikel</TableCell>
<TableCell align="right">Menge</TableCell>
<TableCell align="right">Preis</TableCell>
<TableCell align="right">Gesamt</TableCell>
</TableRow>
</TableHead>
<TableBody>
@@ -117,13 +102,13 @@ const OrderDetailsDialog = ({ open, onClose, order }) => {
<TableCell>{item.name}</TableCell>
<TableCell align="right">{item.quantity_ordered}</TableCell>
<TableCell align="right">{currencyFormatter.format(item.price)}</TableCell>
<TableCell align="right">{item.vat}%</TableCell>
<TableCell align="right">{currencyFormatter.format(item.price * item.quantity_ordered)}</TableCell>
</TableRow>
))}
<TableRow>
<TableCell colSpan={4} align="right">
<Typography fontWeight="bold">{t ? t('tax.totalNet') : 'Gesamtnettopreis'}</Typography>
<TableCell colSpan={2} />
<TableCell align="right">
<Typography fontWeight="bold">Gesamtnettopreis</Typography>
</TableCell>
<TableCell align="right">
<Typography fontWeight="bold">{currencyFormatter.format(vatCalculations.totalNet)}</Typography>
@@ -131,19 +116,36 @@ const OrderDetailsDialog = ({ open, onClose, order }) => {
</TableRow>
{vatCalculations.vat7 > 0 && (
<TableRow>
<TableCell colSpan={4} align="right">{t ? t('tax.vat7') : '7% Mehrwertsteuer'}</TableCell>
<TableCell colSpan={2} />
<TableCell align="right">7% Mehrwertsteuer</TableCell>
<TableCell align="right">{currencyFormatter.format(vatCalculations.vat7)}</TableCell>
</TableRow>
)}
{vatCalculations.vat19 > 0 && (
<TableRow>
<TableCell colSpan={4} align="right">{t ? t('tax.vat19') : '19% Mehrwertsteuer'}</TableCell>
<TableCell colSpan={2} />
<TableCell align="right">19% Mehrwertsteuer</TableCell>
<TableCell align="right">{currencyFormatter.format(vatCalculations.vat19)}</TableCell>
</TableRow>
)}
<TableRow>
<TableCell colSpan={4} align="right">
<Typography fontWeight="bold">{t ? t('cart.summary.total') : 'Gesamtsumme'}</Typography>
<TableCell colSpan={2} />
<TableCell align="right">
<Typography fontWeight="bold">Zwischensumme</Typography>
</TableCell>
<TableCell align="right">
<Typography fontWeight="bold">{currencyFormatter.format(subtotal)}</Typography>
</TableCell>
</TableRow>
<TableRow>
<TableCell colSpan={2} />
<TableCell align="right">Lieferkosten</TableCell>
<TableCell align="right">{currencyFormatter.format(order.delivery_cost)}</TableCell>
</TableRow>
<TableRow>
<TableCell colSpan={2} />
<TableCell align="right">
<Typography fontWeight="bold">Gesamtsumme</Typography>
</TableCell>
<TableCell align="right">
<Typography fontWeight="bold">{currencyFormatter.format(total)}</Typography>
@@ -157,10 +159,10 @@ const OrderDetailsDialog = ({ open, onClose, order }) => {
<DialogActions>
{order.status === 'new' && (
<Button onClick={handleCancelOrder} color="error">
{t('orders.details.cancelOrder')}
Bestellung stornieren
</Button>
)}
<Button onClick={onClose}>{t ? t('common.close') : 'Schließen'}</Button>
<Button onClick={onClose}>Schließen</Button>
</DialogActions>
</Dialog>
);

View File

@@ -49,29 +49,18 @@ class OrderProcessingService {
waitForVerifyTokenAndProcessOrder() {
// Check if window.cart is already populated (verifyToken already completed)
if (Array.isArray(window.cart) && window.cart.length > 0) {
if (this.paymentCompletionData && this.paymentCompletionData.paymentType === 'mollie') {
this.processMollieOrderWithCart(window.cart);
} else {
this.processStripeOrderWithCart(window.cart);
}
this.processStripeOrderWithCart(window.cart);
return;
}
// Listen for cart event which is dispatched after verifyToken completes
this.verifyTokenHandler = () => {
if (Array.isArray(window.cart) && window.cart.length > 0) {
const cartCopy = [...window.cart]; // Copy the cart
this.processStripeOrderWithCart([...window.cart]); // Copy the cart
// Clear window.cart after copying
window.cart = [];
window.dispatchEvent(new CustomEvent("cart"));
// Process based on payment type
if (this.paymentCompletionData && this.paymentCompletionData.paymentType === 'mollie') {
this.processMollieOrderWithCart(cartCopy);
} else {
this.processStripeOrderWithCart(cartCopy);
}
} else {
this.setState({
completionError: "Cart is empty. Please add items to your cart before placing an order."
@@ -122,21 +111,6 @@ class OrderProcessingService {
});
}
processMollieOrderWithCart(cartItems) {
// Clear timeout if it exists
if (this.verifyTokenTimeout) {
clearTimeout(this.verifyTokenTimeout);
this.verifyTokenTimeout = null;
}
// Store cart items in state and process order
this.setState({
originalCartItems: cartItems
}, () => {
this.processMollieOrder();
});
}
processStripeOrder() {
// If no original cart items, don't process
if (!this.getState().originalCartItems || this.getState().originalCartItems.length === 0) {
@@ -231,20 +205,6 @@ class OrderProcessingService {
});
}
processMollieOrder() {
// For Mollie payments, the backend handles order creation automatically
// when payment is successful. We just need to show success state.
this.setState({
isCompletingOrder: false,
orderCompleted: true,
completionError: null,
});
// Clear the cart since order was created by backend
window.cart = [];
window.dispatchEvent(new CustomEvent("cart"));
}
// Process regular (non-Stripe) orders
processRegularOrder(orderData) {
const context = this.getContext();
@@ -310,44 +270,14 @@ class OrderProcessingService {
});
}
}
// Create Mollie payment intent
createMollieIntent(mollieOrderData) {
const context = this.getContext();
if (context && context.socket && context.socket.connected) {
context.socket.emit(
"createMollieIntent",
mollieOrderData,
(response) => {
if (response.success) {
// Store pending payment info and redirect
localStorage.setItem('pendingPayment', JSON.stringify({
paymentId: response.paymentId,
amount: mollieOrderData.amount,
timestamp: Date.now()
}));
window.location.href = response.checkoutUrl;
} else {
console.error("Error:", response.error);
this.setState({
isCompletingOrder: false,
completionError: response.error || "Failed to create Mollie payment intent. Please try again.",
});
}
}
);
} else {
console.error("Socket context not available");
this.setState({
isCompletingOrder: false,
completionError: "Cannot connect to server. Please try again later.",
});
}
createMollieIntent(totalAmount, loadMollieComponent) {
loadMollieComponent();
}
// Calculate delivery cost
getDeliveryCost() {
const { deliveryMethod, paymentMethod, cartItems } = this.getState();
const { deliveryMethod, paymentMethod } = this.getState();
let cost = 0;
switch (deliveryMethod) {
@@ -367,16 +297,7 @@ class OrderProcessingService {
cost = 6.99;
}
// Check for free shipping threshold (>= 100€ cart value)
// Free shipping applies to DHL, DPD, and Sperrgut deliveries when cart value >= 100€
if (cartItems && Array.isArray(cartItems) && deliveryMethod !== "Abholung") {
const cartValue = cartItems.reduce((total, item) => total + item.price * item.quantity, 0);
if (cartValue >= 100) {
cost = 0; // Free shipping for orders >= 100€
}
}
// Add onDelivery surcharge if selected (still applies even with free shipping)
// Add onDelivery surcharge if selected
if (paymentMethod === "onDelivery") {
cost += 8.99;
}

View File

@@ -5,10 +5,8 @@ import Table from '@mui/material/Table';
import TableBody from '@mui/material/TableBody';
import TableCell from '@mui/material/TableCell';
import TableRow from '@mui/material/TableRow';
import { useTranslation } from 'react-i18next';
const OrderSummary = ({ deliveryCost, cartItems = [] }) => {
const { t } = useTranslation();
const currencyFormatter = new Intl.NumberFormat('de-DE', {
style: 'currency',
currency: 'EUR'
@@ -32,9 +30,9 @@ const OrderSummary = ({ deliveryCost, cartItems = [] }) => {
return acc;
}, { totalGross: 0, totalNet: 0, vat7: 0, vat19: 0 });
// Calculate shipping VAT (19% VAT for shipping costs) - only if there are shipping costs
const shippingNetPrice = deliveryCost > 0 ? deliveryCost / (1 + 19 / 100) : 0;
const shippingVat = deliveryCost > 0 ? deliveryCost - shippingNetPrice : 0;
// Calculate shipping VAT (19% VAT for shipping costs)
const shippingNetPrice = deliveryCost / (1 + 19 / 100);
const shippingVat = deliveryCost - shippingNetPrice;
// Combine totals - add shipping VAT to the 19% VAT total
const totalVat7 = cartVatCalculations.vat7;
@@ -44,20 +42,20 @@ const OrderSummary = ({ deliveryCost, cartItems = [] }) => {
return (
<Box sx={{ my: 3, p: 2, bgcolor: '#f5f5f5', borderRadius: 1 }}>
<Typography variant="h6" gutterBottom>
{t ? t('cart.summary.title') : 'Bestellübersicht'}
Bestellübersicht
</Typography>
<Table size="small">
<TableBody>
<TableRow>
<TableCell>{t ? t('cart.summary.goodsNet') : 'Waren (netto):'}</TableCell>
<TableCell>Waren (netto):</TableCell>
<TableCell align="right">
{currencyFormatter.format(cartVatCalculations.totalNet)}
</TableCell>
</TableRow>
{deliveryCost > 0 && (
<TableRow>
<TableCell>{t ? t('cart.summary.shippingNet') : 'Versandkosten (netto):'}</TableCell>
<TableCell>Versandkosten (netto):</TableCell>
<TableCell align="right">
{currencyFormatter.format(shippingNetPrice)}
</TableCell>
@@ -65,7 +63,7 @@ const OrderSummary = ({ deliveryCost, cartItems = [] }) => {
)}
{totalVat7 > 0 && (
<TableRow>
<TableCell>{t ? t('tax.vat7') : '7% Mehrwertsteuer'}:</TableCell>
<TableCell>7% Mehrwertsteuer:</TableCell>
<TableCell align="right">
{currencyFormatter.format(totalVat7)}
</TableCell>
@@ -73,37 +71,28 @@ const OrderSummary = ({ deliveryCost, cartItems = [] }) => {
)}
{totalVat19 > 0 && (
<TableRow>
<TableCell>{t ? t('tax.vat19WithShipping') : '19% Mehrwertsteuer (inkl. Versand)'}:</TableCell>
<TableCell>19% Mehrwertsteuer (inkl. Versand):</TableCell>
<TableCell align="right">
{currencyFormatter.format(totalVat19)}
</TableCell>
</TableRow>
)}
<TableRow>
<TableCell sx={{ fontWeight: 'bold' }}>{t ? t('cart.summary.totalGoods') : 'Gesamtsumme Waren:'}</TableCell>
<TableCell sx={{ fontWeight: 'bold' }}>Gesamtsumme Waren:</TableCell>
<TableCell align="right" sx={{ fontWeight: 'bold' }}>
{currencyFormatter.format(cartVatCalculations.totalGross)}
</TableCell>
</TableRow>
<TableRow>
<TableCell sx={{ fontWeight: 'bold' }}>
{t ? t('cart.summary.shippingCosts') : 'Versandkosten:'}
{deliveryCost === 0 && cartVatCalculations.totalGross < 100 && (
<span style={{ color: '#2e7d32', fontSize: '0.8em', marginLeft: '4px' }}>
{t ? t('cart.summary.freeFrom100') : '(kostenlos ab 100€)'}
</span>
)}
</TableCell>
<TableCell align="right" sx={{ fontWeight: 'bold' }}>
{deliveryCost === 0 ? (
<span style={{ color: '#2e7d32' }}>{t ? t('cart.summary.free') : 'kostenlos'}</span>
) : (
currencyFormatter.format(deliveryCost)
)}
</TableCell>
</TableRow>
{deliveryCost > 0 && (
<TableRow>
<TableCell sx={{ fontWeight: 'bold' }}>Versandkosten:</TableCell>
<TableCell align="right" sx={{ fontWeight: 'bold' }}>
{currencyFormatter.format(deliveryCost)}
</TableCell>
</TableRow>
)}
<TableRow sx={{ borderTop: '1px solid #e0e0e0' }}>
<TableCell sx={{ fontWeight: 'bold', pt: 2 }}>{t ? t('cart.summary.total') : 'Gesamtsumme:'}</TableCell>
<TableCell sx={{ fontWeight: 'bold', pt: 2 }}>Gesamtsumme:</TableCell>
<TableCell align="right" sx={{ fontWeight: 'bold', pt: 2 }}>
{currencyFormatter.format(totalGross)}
</TableCell>

View File

@@ -1,6 +1,5 @@
import React, { useState, useEffect, useContext, useCallback } from "react";
import { useNavigate } from "react-router-dom";
import { withI18n } from "../../i18n/withTranslation.js";
import {
Box,
Paper,
@@ -15,28 +14,19 @@ import {
Tooltip,
CircularProgress,
Typography,
Dialog,
DialogTitle,
DialogContent,
DialogActions,
Button,
} from "@mui/material";
import SearchIcon from "@mui/icons-material/Search";
import CancelIcon from "@mui/icons-material/Cancel";
import SocketContext from "../../contexts/SocketContext.js";
import OrderDetailsDialog from "./OrderDetailsDialog.js";
// Constants
const getStatusTranslation = (status, t) => {
const statusMap = {
new: t ? t('orders.status.new') : "in Bearbeitung",
pending: t ? t('orders.status.pending') : "Neu",
processing: t ? t('orders.status.processing') : "in Bearbeitung",
cancelled: t ? t('orders.status.cancelled') : "Storniert",
shipped: t ? t('orders.status.shipped') : "Verschickt",
delivered: t ? t('orders.status.delivered') : "Geliefert",
};
return statusMap[status] || status;
const statusTranslations = {
new: "in Bearbeitung",
pending: "Neu",
processing: "in Bearbeitung",
cancelled: "Storniert",
shipped: "Verschickt",
delivered: "Geliefert",
};
const statusEmojis = {
@@ -71,15 +61,12 @@ const currencyFormatter = new Intl.NumberFormat("de-DE", {
});
// Orders Tab Content Component
const OrdersTab = ({ orderIdFromHash, t }) => {
const OrdersTab = ({ orderIdFromHash }) => {
const [orders, setOrders] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [selectedOrder, setSelectedOrder] = useState(null);
const [isDetailsDialogOpen, setIsDetailsDialogOpen] = useState(false);
const [cancelConfirmOpen, setCancelConfirmOpen] = useState(false);
const [orderToCancel, setOrderToCancel] = useState(null);
const [isCancelling, setIsCancelling] = useState(false);
const {socket} = useContext(SocketContext);
const navigate = useNavigate();
@@ -90,11 +77,9 @@ const OrdersTab = ({ orderIdFromHash, t }) => {
if (orderToView) {
setSelectedOrder(orderToView);
setIsDetailsDialogOpen(true);
// Update the hash to include the order ID
navigate(`/profile#${orderId}`, { replace: true });
}
},
[orders, navigate]
[orders]
);
const fetchOrders = useCallback(() => {
@@ -135,7 +120,7 @@ const OrdersTab = ({ orderIdFromHash, t }) => {
}, [orderIdFromHash, orders, handleViewDetails]);
const getStatusDisplay = (status) => {
return getStatusTranslation(status, t);
return statusTranslations[status] || status;
};
const getStatusEmoji = (status) => {
@@ -149,53 +134,12 @@ const OrdersTab = ({ orderIdFromHash, t }) => {
const handleCloseDetailsDialog = () => {
setIsDetailsDialogOpen(false);
setSelectedOrder(null);
navigate("/profile#orders", { replace: true });
};
// Check if order can be cancelled
const isOrderCancelable = (order) => {
const cancelableStatuses = ['new', 'pending', 'processing'];
return cancelableStatuses.includes(order.status);
};
// Handle cancel button click
const handleCancelClick = (order) => {
setOrderToCancel(order);
setCancelConfirmOpen(true);
};
// Handle cancel confirmation
const handleConfirmCancel = () => {
if (!orderToCancel || !socket) return;
setIsCancelling(true);
socket.emit('cancelOrder', { orderId: orderToCancel.orderId }, (response) => {
setIsCancelling(false);
setCancelConfirmOpen(false);
if (response.success) {
console.log('Order cancelled:', response.orderId);
// Refresh orders list
fetchOrders();
} else {
setError(response.error || 'Failed to cancel order');
}
setOrderToCancel(null);
});
};
// Handle cancel dialog close
const handleCancelDialogClose = () => {
if (!isCancelling) {
setCancelConfirmOpen(false);
setOrderToCancel(null);
}
navigate("/profile", { replace: true });
};
if (loading) {
return (
<Box sx={{ p: { xs: 1, sm: 3 }, display: "flex", justifyContent: "center" }}>
<Box sx={{ p: { xs: 1, sm: 3 }, display: "flex", justifyContent: "center" }}>
<CircularProgress />
</Box>
);
@@ -210,27 +154,28 @@ const OrdersTab = ({ orderIdFromHash, t }) => {
}
return (
<Box sx={{ p: { xs: 1, sm: 3 } }}>
<Box sx={{ p: { xs: 1, sm: 3 } }}>
{orders.length > 0 ? (
<TableContainer component={Paper}>
<Table>
<TableHead>
<TableRow>
<TableCell>{t ? t('orders.table.orderNumber') : 'Bestellnummer'}</TableCell>
<TableCell>{t ? t('orders.table.date') : 'Datum'}</TableCell>
<TableCell>{t ? t('orders.table.status') : 'Status'}</TableCell>
<TableCell>{t ? t('orders.table.items') : 'Artikel'}</TableCell>
<TableCell align="right">{t ? t('orders.table.total') : 'Summe'}</TableCell>
<TableCell align="center">{t ? t('orders.table.actions') : 'Aktionen'}</TableCell>
<TableCell>Bestellnummer</TableCell>
<TableCell>Datum</TableCell>
<TableCell>Status</TableCell>
<TableCell>Artikel</TableCell>
<TableCell align="right">Summe</TableCell>
<TableCell align="center">Aktionen</TableCell>
</TableRow>
</TableHead>
<TableBody>
{orders.map((order) => {
const displayStatus = getStatusDisplay(order.status);
const total = order.items.reduce(
const subtotal = order.items.reduce(
(acc, item) => acc + item.price * item.quantity_ordered,
0
);
const total = subtotal + order.delivery_cost;
return (
<TableRow key={order.orderId} hover>
<TableCell>{order.orderId}</TableCell>
@@ -259,46 +204,24 @@ const OrdersTab = ({ orderIdFromHash, t }) => {
</Box>
</TableCell>
<TableCell>
{order.items
.filter(item => {
// Exclude delivery items - backend uses deliveryMethod ID as item name
const itemName = item.name || '';
return itemName !== 'DHL' &&
itemName !== 'DPD' &&
itemName !== 'Sperrgut' &&
itemName !== 'Abholung';
})
.reduce(
(acc, item) => acc + item.quantity_ordered,
0
)}
{order.items.reduce(
(acc, item) => acc + item.quantity_ordered,
0
)}
</TableCell>
<TableCell align="right">
{currencyFormatter.format(total)}
</TableCell>
<TableCell align="center">
<Box sx={{ display: 'flex', gap: 1, justifyContent: 'center' }}>
<Tooltip title={t ? t('orders.tooltips.viewDetails') : 'Details anzeigen'}>
<IconButton
size="small"
color="primary"
onClick={() => handleViewDetails(order.orderId)}
>
<SearchIcon />
</IconButton>
</Tooltip>
{isOrderCancelable(order) && (
<Tooltip title={t ? t('orders.tooltips.cancelOrder') : 'Bestellung stornieren'}>
<IconButton
size="small"
color="error"
onClick={() => handleCancelClick(order)}
>
<CancelIcon />
</IconButton>
</Tooltip>
)}
</Box>
<Tooltip title="Details anzeigen">
<IconButton
size="small"
color="primary"
onClick={() => handleViewDetails(order.orderId)}
>
<SearchIcon />
</IconButton>
</Tooltip>
</TableCell>
</TableRow>
);
@@ -308,7 +231,7 @@ const OrdersTab = ({ orderIdFromHash, t }) => {
</TableContainer>
) : (
<Alert severity="info">
{t ? t('orders.noOrders') : 'Sie haben noch keine Bestellungen aufgegeben.'}
Sie haben noch keine Bestellungen aufgegeben.
</Alert>
)}
<OrderDetailsDialog
@@ -316,49 +239,8 @@ const OrdersTab = ({ orderIdFromHash, t }) => {
onClose={handleCloseDetailsDialog}
order={selectedOrder}
/>
{/* Cancel Confirmation Dialog */}
<Dialog
open={cancelConfirmOpen}
onClose={handleCancelDialogClose}
maxWidth="sm"
fullWidth
>
<DialogTitle>
{t ? t('orders.cancelConfirm.title') : 'Bestellung stornieren'}
</DialogTitle>
<DialogContent>
<Typography>
{t ? t('orders.cancelConfirm.message') : 'Sind Sie sicher, dass Sie diese Bestellung stornieren möchten?'}
</Typography>
{orderToCancel && (
<Typography variant="body2" sx={{ mt: 1, fontWeight: 'bold' }}>
{t ? t('orders.table.orderNumber') : 'Bestellnummer'}: {orderToCancel.orderId}
</Typography>
)}
</DialogContent>
<DialogActions>
<Button
onClick={handleCancelDialogClose}
disabled={isCancelling}
>
{t ? t('common.cancel') : 'Abbrechen'}
</Button>
<Button
onClick={handleConfirmCancel}
color="error"
variant="contained"
disabled={isCancelling}
>
{isCancelling
? (t ? t('orders.cancelConfirm.cancelling') : 'Wird storniert...')
: (t ? t('orders.cancelConfirm.confirm') : 'Stornieren')
}
</Button>
</DialogActions>
</Dialog>
</Box>
);
};
export default withI18n()(OrdersTab);
export default OrdersTab;

View File

@@ -1,6 +1,5 @@
import React, { Component } from "react";
import { Box, Typography, Button } from "@mui/material";
import { withI18n } from "../../i18n/withTranslation.js";
class PaymentConfirmationDialog extends Component {
render() {
@@ -29,32 +28,30 @@ class PaymentConfirmationDialog extends Component {
color: paymentCompletionData.isSuccessful ? '#2e7d32' : '#d32f2f',
fontWeight: 'bold'
}}>
{paymentCompletionData.isSuccessful ?
(this.props.t ? this.props.t('payment.successful') : 'Zahlung erfolgreich!') :
(this.props.t ? this.props.t('payment.failed') : 'Zahlung fehlgeschlagen')}
{paymentCompletionData.isSuccessful ? 'Zahlung erfolgreich!' : 'Zahlung fehlgeschlagen'}
</Typography>
{paymentCompletionData.isSuccessful ? (
<>
{orderCompleted ? (
<Typography variant="body1" sx={{ mt: 2, color: '#2e7d32' }}>
{this.props.t ? this.props.t('payment.orderCompleted') : '🎉 Ihre Bestellung wurde erfolgreich abgeschlossen! Sie können jetzt Ihre Bestellungen einsehen.'}
🎉 Ihre Bestellung wurde erfolgreich abgeschlossen! Sie können jetzt Ihre Bestellungen einsehen.
</Typography>
) : (
<Typography variant="body1" sx={{ mt: 2, color: '#2e7d32' }}>
{this.props.t ? this.props.t('payment.orderProcessing') : 'Ihre Zahlung wurde erfolgreich verarbeitet. Die Bestellung wird automatisch abgeschlossen.'}
Ihre Zahlung wurde erfolgreich verarbeitet. Die Bestellung wird automatisch abgeschlossen.
</Typography>
)}
</>
) : (
<Typography variant="body1" sx={{ mt: 2, color: '#d32f2f' }}>
{this.props.t ? this.props.t('payment.paymentError') : 'Ihre Zahlung konnte nicht verarbeitet werden. Bitte versuchen Sie es erneut oder wählen Sie eine andere Zahlungsmethode.'}
Ihre Zahlung konnte nicht verarbeitet werden. Bitte versuchen Sie es erneut oder wählen Sie eine andere Zahlungsmethode.
</Typography>
)}
{isCompletingOrder && (
<Typography variant="body2" sx={{ mt: 2, color: '#2e7d32', p: 2, bgcolor: '#e8f5e8', borderRadius: 1 }}>
{this.props.t ? this.props.t('orders.processing') : 'Bestellung wird abgeschlossen...'}
Bestellung wird abgeschlossen...
</Typography>
)}
@@ -78,7 +75,7 @@ class PaymentConfirmationDialog extends Component {
}
}}
>
{this.props.t ? this.props.t('cart.continueShopping') : 'Weiter einkaufen'}
Weiter einkaufen
</Button>
<Button
onClick={onViewOrders}
@@ -88,7 +85,7 @@ class PaymentConfirmationDialog extends Component {
'&:hover': { bgcolor: '#1b5e20' }
}}
>
{this.props.t ? this.props.t('payment.viewOrders') : 'Zu meinen Bestellungen'}
Zu meinen Bestellungen
</Button>
</Box>
)}
@@ -97,4 +94,4 @@ class PaymentConfirmationDialog extends Component {
}
}
export default withI18n()(PaymentConfirmationDialog);
export default PaymentConfirmationDialog;

View File

@@ -1,8 +1,7 @@
import React, { useEffect, useCallback } from "react";
import { Box, Typography, Radio } from "@mui/material";
import { withI18n } from "../../i18n/withTranslation.js";
const PaymentMethodSelector = ({ paymentMethod, onChange, deliveryMethod, onDeliveryMethodChange, cartItems = [], deliveryCost = 0, t }) => {
const PaymentMethodSelector = ({ paymentMethod, onChange, deliveryMethod, onDeliveryMethodChange, cartItems = [], deliveryCost = 0 }) => {
// Calculate total amount
const subtotal = cartItems.reduce((total, item) => total + item.price * item.quantity, 0);
@@ -25,7 +24,7 @@ const PaymentMethodSelector = ({ paymentMethod, onChange, deliveryMethod, onDeli
// Handle delivery method changes - auto-switch to stripe when DHL/DPD is selected
useEffect(() => {
if ((deliveryMethod === "DHL" || deliveryMethod === "DPD") && paymentMethod === "cash") {
handlePaymentMethodChange({ target: { value: "wire" /*stripe*/ } });
handlePaymentMethodChange({ target: { value: "mollie" } });
}
}, [deliveryMethod, paymentMethod, handlePaymentMethodChange]);
@@ -39,13 +38,13 @@ const PaymentMethodSelector = ({ paymentMethod, onChange, deliveryMethod, onDeli
const paymentOptions = [
{
id: "wire",
name: t ? t('payment.methods.bankTransfer') : "Überweisung",
description: t ? t('payment.methods.bankTransferDescription') : "Bezahlen Sie per Banküberweisung",
name: "Überweisung",
description: "Bezahlen Sie per Banküberweisung",
disabled: totalAmount === 0,
},
/*{
id: "stripe",
name: "Karte oder Sofortüberweisung",
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",
@@ -57,12 +56,12 @@ const PaymentMethodSelector = ({ paymentMethod, onChange, deliveryMethod, onDeli
"/assets/images/visa_electron.png",
],
},*/
/*{
{
id: "mollie",
name: t ? t('payment.methods.cardPayment') : "Karte, Sofortüberweisung, Apple Pay, Google Pay, PayPal",
name: "Karte oder Sofortüberweisung",
description: totalAmount < 0.50 && totalAmount > 0
? (t ? t('payment.methods.cardPaymentMinAmount') : "Bezahlen Sie per Karte oder Sofortüberweisung (Mindestbetrag: 0,50 €)")
: (t ? t('payment.methods.cardPaymentDescription') : "Bezahlen Sie per Karte oder Sofortüberweisung"),
? "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",
@@ -70,18 +69,18 @@ const PaymentMethodSelector = ({ paymentMethod, onChange, deliveryMethod, onDeli
"/assets/images/mastercard.png",
"/assets/images/visa_electron.png",
],
},*/
},
{
id: "onDelivery",
name: t ? t('payment.methods.cashOnDelivery') : "Nachnahme",
description: t ? t('payment.methods.cashOnDeliveryDescription') : "Bezahlen Sie bei Lieferung (8,99 € Aufschlag)",
name: "Nachnahme",
description: "Bezahlen Sie bei Lieferung (8,99 € Aufschlag)",
disabled: totalAmount === 0 || deliveryMethod !== "DHL",
icons: ["/assets/images/cash.png"],
},
{
id: "cash",
name: t ? t('payment.methods.cashInStore') : "Zahlung in der Filiale",
description: t ? t('payment.methods.cashInStoreDescription') : "Bei Abholung bezahlen",
name: "Zahlung in der Filiale",
description: "Bei Abholung bezahlen",
disabled: false, // Always enabled
icons: ["/assets/images/cash.png"],
},
@@ -90,7 +89,7 @@ const PaymentMethodSelector = ({ paymentMethod, onChange, deliveryMethod, onDeli
return (
<>
<Typography variant="h6" gutterBottom>
{t ? t('payment.methods.selectPaymentMethod') : 'Zahlungsart wählen'}
Zahlungsart wählen
</Typography>
<Box sx={{ mb: 3 }}>
@@ -190,4 +189,4 @@ const PaymentMethodSelector = ({ paymentMethod, onChange, deliveryMethod, onDeli
);
};
export default withI18n()(PaymentMethodSelector);
export default PaymentMethodSelector;

View File

@@ -12,7 +12,6 @@ import {
Snackbar
} from '@mui/material';
import { ContentCopy } from '@mui/icons-material';
import { withI18n } from '../../i18n/withTranslation.js';
class SettingsTab extends Component {
constructor(props) {
@@ -73,17 +72,17 @@ class SettingsTab extends Component {
// Validation
if (!this.state.currentPassword || !this.state.newPassword || !this.state.confirmPassword) {
this.setState({ passwordError: this.props.t ? this.props.t('settings.errors.fillAllFields') : 'Bitte füllen Sie alle Felder aus' });
this.setState({ passwordError: 'Bitte füllen Sie alle Felder aus' });
return;
}
if (this.state.newPassword !== this.state.confirmPassword) {
this.setState({ passwordError: this.props.t ? this.props.t('settings.errors.passwordsNotMatch') : 'Die neuen Passwörter stimmen nicht überein' });
this.setState({ passwordError: 'Die neuen Passwörter stimmen nicht überein' });
return;
}
if (this.state.newPassword.length < 8) {
this.setState({ passwordError: this.props.t ? this.props.t('settings.errors.passwordTooShort') : 'Das neue Passwort muss mindestens 8 Zeichen lang sein' });
this.setState({ passwordError: 'Das neue Passwort muss mindestens 8 Zeichen lang sein' });
return;
}
@@ -97,14 +96,14 @@ class SettingsTab extends Component {
if (response.success) {
this.setState({
passwordSuccess: this.props.t ? this.props.t('settings.success.passwordUpdated') : 'Passwort erfolgreich aktualisiert',
passwordSuccess: 'Passwort erfolgreich aktualisiert',
currentPassword: '',
newPassword: '',
confirmPassword: ''
});
} else {
this.setState({
passwordError: response.message || (this.props.t ? this.props.t('settings.errors.passwordUpdateError') : 'Fehler beim Aktualisieren des Passworts')
passwordError: response.message || 'Fehler beim Aktualisieren des Passworts'
});
}
}
@@ -122,12 +121,12 @@ class SettingsTab extends Component {
// Validation
if (!this.state.password || !this.state.newEmail) {
this.setState({ emailError: this.props.t ? this.props.t('settings.errors.fillAllFields') : 'Bitte füllen Sie alle Felder aus' });
this.setState({ emailError: 'Bitte füllen Sie alle Felder aus' });
return;
}
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(this.state.newEmail)) {
this.setState({ emailError: this.props.t ? this.props.t('settings.errors.invalidEmail') : 'Bitte geben Sie eine gültige E-Mail-Adresse ein' });
this.setState({ emailError: 'Bitte geben Sie eine gültige E-Mail-Adresse ein' });
return;
}
@@ -141,7 +140,7 @@ class SettingsTab extends Component {
if (response.success) {
this.setState({
emailSuccess: this.props.t ? this.props.t('settings.success.emailUpdated') : 'E-Mail-Adresse erfolgreich aktualisiert',
emailSuccess: 'E-Mail-Adresse erfolgreich aktualisiert',
password: ''
});
@@ -158,7 +157,7 @@ class SettingsTab extends Component {
}
} else {
this.setState({
emailError: response.message || (this.props.t ? this.props.t('settings.errors.emailUpdateError') : 'Fehler beim Aktualisieren der E-Mail-Adresse')
emailError: response.message || 'Fehler beim Aktualisieren der E-Mail-Adresse'
});
}
}
@@ -239,7 +238,7 @@ class SettingsTab extends Component {
<Box sx={{ p: { xs: 1, sm: 3 } }}>
<Paper sx={{ p: { xs: 2, sm: 3 } }}>
<Typography variant="subtitle1" fontWeight="bold" gutterBottom>
{this.props.t ? this.props.t('settings.changePassword') : 'Passwort ändern'}
Passwort ändern
</Typography>
{this.state.passwordError && <Alert severity="error" sx={{ mb: 2 }}>{this.state.passwordError}</Alert>}
@@ -248,7 +247,7 @@ class SettingsTab extends Component {
<Box component="form" onSubmit={this.handleUpdatePassword}>
<TextField
margin="normal"
label={this.props.t ? this.props.t('settings.currentPassword') : 'Aktuelles Passwort'}
label="Aktuelles Passwort"
type="password"
fullWidth
value={this.state.currentPassword}
@@ -258,7 +257,7 @@ class SettingsTab extends Component {
<TextField
margin="normal"
label={this.props.t ? this.props.t('settings.newPassword') : 'Neues Passwort'}
label="Neues Passwort"
type="password"
fullWidth
value={this.state.newPassword}
@@ -268,7 +267,7 @@ class SettingsTab extends Component {
<TextField
margin="normal"
label={this.props.t ? this.props.t('settings.confirmNewPassword') : 'Neues Passwort bestätigen'}
label="Neues Passwort bestätigen"
type="password"
fullWidth
value={this.state.confirmPassword}
@@ -283,7 +282,7 @@ class SettingsTab extends Component {
sx={{ mt: 2, bgcolor: '#2e7d32', '&:hover': { bgcolor: '#1b5e20' } }}
disabled={this.state.loading}
>
{this.state.loading ? <CircularProgress size={24} /> : (this.props.t ? this.props.t('settings.updatePassword') : 'Passwort aktualisieren')}
{this.state.loading ? <CircularProgress size={24} /> : 'Passwort aktualisieren'}
</Button>
</Box>
</Paper>
@@ -292,7 +291,7 @@ class SettingsTab extends Component {
<Paper sx={{ p: { xs: 2, sm: 3 } }}>
<Typography variant="subtitle1" fontWeight="bold" gutterBottom>
{this.props.t ? this.props.t('settings.changeEmail') : 'E-Mail-Adresse ändern'}
E-Mail-Adresse ändern
</Typography>
{this.state.emailError && <Alert severity="error" sx={{ mb: 2 }}>{this.state.emailError}</Alert>}
@@ -301,7 +300,7 @@ class SettingsTab extends Component {
<Box component="form" onSubmit={this.handleUpdateEmail}>
<TextField
margin="normal"
label={this.props.t ? this.props.t('settings.password') : 'Passwort'}
label="Passwort"
type="password"
fullWidth
value={this.state.password}
@@ -311,7 +310,7 @@ class SettingsTab extends Component {
<TextField
margin="normal"
label={this.props.t ? this.props.t('settings.newEmail') : 'Neue E-Mail-Adresse'}
label="Neue E-Mail-Adresse"
type="email"
fullWidth
value={this.state.newEmail}
@@ -326,7 +325,7 @@ class SettingsTab extends Component {
sx={{ mt: 2, bgcolor: '#2e7d32', '&:hover': { bgcolor: '#1b5e20' } }}
disabled={this.state.loading}
>
{this.state.loading ? <CircularProgress size={24} /> : (this.props.t ? this.props.t('settings.updateEmail') : 'E-Mail aktualisieren')}
{this.state.loading ? <CircularProgress size={24} /> : 'E-Mail aktualisieren'}
</Button>
</Box>
</Paper>
@@ -335,11 +334,11 @@ class SettingsTab extends Component {
<Paper sx={{ p: { xs: 2, sm: 3 } }}>
<Typography variant="subtitle1" fontWeight="bold" gutterBottom>
{this.props.t ? this.props.t('settings.apiKey') : 'API-Schlüssel'}
API-Schlüssel
</Typography>
<Typography variant="body2" color="text.secondary" gutterBottom>
{this.props.t ? this.props.t('settings.apiKeyDescription') : 'Verwenden Sie Ihren API-Schlüssel für die Integration mit externen Anwendungen.'}
Verwenden Sie Ihren API-Schlüssel für die Integration mit externen Anwendungen.
</Typography>
{this.state.apiKeyError && <Alert severity="error" sx={{ mb: 2 }}>{this.state.apiKeyError}</Alert>}
@@ -348,14 +347,14 @@ class SettingsTab extends Component {
{this.state.apiKeySuccess}
{this.state.apiKey && this.state.apiKeyDisplay !== '************' && (
<Typography variant="body2" sx={{ mt: 1 }}>
{this.props.t ? this.props.t('settings.success.apiKeyWarning') : 'Speichern Sie diesen Schlüssel sicher. Er wird aus Sicherheitsgründen in 10 Sekunden ausgeblendet.'}
Speichern Sie diesen Schlüssel sicher. Er wird aus Sicherheitsgründen in 10 Sekunden ausgeblendet.
</Typography>
)}
</Alert>
)}
<Typography variant="body2" sx={{ mb: 2 }}>
{this.props.t ? this.props.t('settings.apiDocumentation') : 'API-Dokumentation:'} {' '}
API-Dokumentation: {' '}
<a
href={`${window.location.protocol}//${window.location.host}/api/`}
target="_blank"
@@ -368,7 +367,7 @@ class SettingsTab extends Component {
<Box sx={{ display: 'flex', gap: 2, alignItems: 'center', mt: 2 }}>
<TextField
label={this.props.t ? this.props.t('settings.apiKey') : 'API-Schlüssel'}
label="API-Schlüssel"
value={this.state.apiKeyDisplay}
disabled
fullWidth
@@ -386,7 +385,7 @@ class SettingsTab extends Component {
color: '#2e7d32',
'&:hover': { bgcolor: 'rgba(46, 125, 50, 0.1)' }
}}
title={this.props.t ? this.props.t('settings.copyToClipboard') : 'In Zwischenablage kopieren'}
title="In Zwischenablage kopieren"
>
<ContentCopy />
</IconButton>
@@ -406,7 +405,7 @@ class SettingsTab extends Component {
{this.state.loadingApiKey ? (
<CircularProgress size={24} />
) : (
this.state.hasApiKey ? (this.props.t ? this.props.t('settings.regenerate') : 'Regenerieren') : (this.props.t ? this.props.t('settings.generate') : 'Generieren')
this.state.hasApiKey ? 'Regenerieren' : 'Generieren'
)}
</Button>
</Box>
@@ -416,7 +415,7 @@ class SettingsTab extends Component {
open={this.state.copySnackbarOpen}
autoHideDuration={3000}
onClose={this.handleCloseSnackbar}
message={this.props.t ? this.props.t('settings.apiKeyCopied') : 'API-Schlüssel in Zwischenablage kopiert'}
message="API-Schlüssel in Zwischenablage kopiert"
anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}
/>
</Box>
@@ -424,4 +423,4 @@ class SettingsTab extends Component {
}
}
export default withI18n()(SettingsTab);
export default SettingsTab;

View File

@@ -3,200 +3,23 @@ const config = {
apiBaseUrl: "",
googleClientId: "928121624463-jbgfdlgem22scs1k9c87ucg4ffvaik6o.apps.googleusercontent.com",
stripePublishableKey: "pk_test_51R7lltRtpe3h1vwJzIrDb5bcEigTLBHrtqj9SiPX7FOEATSuD6oJmKc8xpNp49ShpGJZb2GShHIUqj4zlSIz4olj00ipOuOAnu",
mollieProfileKey: "pfl_AtcRTimCff",
// SEO and Business Information
siteName: "Growheads.de",
brandName: "GrowHeads",
currency: "EUR",
language: "de-DE", // Will be updated dynamically based on i18n
language: "de-DE",
country: "DE",
// Multilingual configurations
languages: {
de: {
code: "de-DE",
name: "Deutsch",
shortName: "DE"
},
en: {
code: "en-US",
name: "English",
shortName: "EN"
},
es: {
code: "es-ES",
name: "Español",
shortName: "ES"
},
fr: {
code: "fr-FR",
name: "Français",
shortName: "FR"
},
it: {
code: "it-IT",
name: "Italiano",
shortName: "IT"
},
pl: {
code: "pl-PL",
name: "Polski",
shortName: "PL"
},
hu: {
code: "hu-HU",
name: "Magyar",
shortName: "HU"
},
sr: {
code: "sr-RS",
name: "Српски",
shortName: "SR"
},
bg: {
code: "bg-BG",
name: "Български",
shortName: "BG"
},
ru: {
code: "ru-RU",
name: "Русский",
shortName: "RU"
},
uk: {
code: "uk-UA",
name: "Українська",
shortName: "UK"
},
sk: {
code: "sk-SK",
name: "Slovenčina",
shortName: "SK"
},
sl: {
code: "sl-SI",
name: "Slovenščina",
shortName: "SI"
},
cs: {
code: "cs-CZ",
name: "Čeština",
shortName: "CS"
},
ro: {
code: "ro-RO",
name: "Română",
shortName: "RO"
},
hr: {
code: "hr-HR",
name: "Hrvatski",
shortName: "HR"
},
sv: {
code: "sv-SE",
name: "Svenska",
shortName: "SE"
},
tr: {
code: "tr-TR",
name: "Türkçe",
shortName: "TR"
},
el: {
code: "el-GR",
name: "Ελληνικά",
shortName: "GR"
},
ar: {
code: "ar-EG",
name: "العربية",
shortName: "EG"
},
zh: {
code: "zh-CN",
name: "中文",
shortName: "CN"
}
},
// Shop Descriptions - Multilingual
// Shop Descriptions
descriptions: {
de: {
short: "GrowHeads - Online-Shop für Cannabis-Samen, Stecklinge und Gartenbedarf",
long: "GrowHeads - Ihr Online-Shop für hochwertige Samen, Pflanzen und Gartenbedarf zur Cannabis Kultivierung. Entdecken Sie unser großes Sortiment an Saatgut, Pflanzen und Gartenzubehör für Ihren grünen Daumen."
},
en: {
short: "GrowHeads - Online Shop for Cannabis Seeds, Cuttings and Garden Supplies",
long: "GrowHeads - Your online shop for high-quality seeds, plants and garden supplies for cannabis cultivation. Discover our large assortment of seeds, plants and garden accessories for your green thumb."
},
es: {
short: "GrowHeads - Tienda Online de Semillas de Cannabis, Esquejes y Suministros de Jardín",
long: "GrowHeads - Tu tienda online de semillas, plantas y suministros de jardín de alta calidad para el cultivo de cannabis. Descubre nuestro gran surtido de semillas, plantas y accesorios de jardín para tu pulgar verde."
},
fr: {
short: "GrowHeads - Boutique en ligne de Graines de Cannabis, Boutures et Fournitures de Jardinage",
long: "GrowHeads - Votre boutique en ligne pour des graines, plantes et fournitures de jardinage de haute qualité pour la culture du cannabis. Découvrez notre large assortiment de graines, plantes et accessoires de jardinage pour votre pouce vert."
},
it: {
short: "GrowHeads - Negozio Online di Semi di Cannabis, Talee e Forniture da Giardino",
long: "GrowHeads - Il tuo negozio online per semi, piante e forniture da giardino di alta qualità per la coltivazione di cannabis. Scopri il nostro vasto assortimento di semi, piante e accessori da giardino per il tuo pollice verde."
},
pl: {
short: "GrowHeads - Sklep Online z Nasionami Konopi, Sadzonkami i Artykułami Ogrodniczymi",
long: "GrowHeads - Twój sklep online z wysokiej jakości nasionami, roślinami i artykułami ogrodniczymi do uprawy konopi. Odkryj nasz duży asortyment nasion, roślin i akcesoriów ogrodniczych dla Twojego zielonego kciuka."
},
hu: {
short: "GrowHeads - Online Bolt Kannabisz Magokhoz, Dugványokhoz és Kerti Kellékekhez",
long: "GrowHeads - Az Ön online boltja minőségi magokhoz, növényekhez és kerti kellékekhez a kannabisz termesztéshez. Fedezze fel nagy választékunkat magokból, növényekből és kerti kiegészítőkből a zöld hüvelykujjához."
},
sr: {
short: "GrowHeads - Онлајн продавница за семена канабиса, резнице и вртларски прибор",
long: "GrowHeads - Ваша онлајн продавница за висококвалитетна семена, биљке и вртларски прибор за узгајање канабиса. Откријте наш велики асортиман семена, биљака и вртларских додатака за ваш зелени палац."
},
bg: {
short: "GrowHeads - Онлайн магазин за семена на канабис, резници и градински принадлежности",
long: "GrowHeads - Вашият онлайн магазин за висококачествени семена, растения и градински принадлежности за отглеждане на канабис. Открийте нашия голям асортимент от семена, растения и градински аксесоари за вашия зелен палец."
},
ru: {
short: "GrowHeads - Интернет-магазин семян каннабиса, черенков и садовых принадлежностей",
long: "GrowHeads - Ваш интернет-магазин высококачественных семян, растений и садовых принадлежностей для выращивания каннабиса. Откройте для себя наш большой ассортимент семян, растений и садовых аксессуаров для вашего зеленого пальца."
},
uk: {
short: "GrowHeads - Інтернет-магазин насіння канабісу, живців та садових приладдя",
long: "GrowHeads - Ваш інтернет-магазин високоякісного насіння, рослин та садових приладдя для вирощування канабісу. Відкрийте для себе наш великий асортимент насіння, рослин та садових аксесуарів для вашого зеленого пальця."
},
sk: {
short: "GrowHeads - Online obchod so semenami konopy, sadenicami a záhradnými potrebami",
long: "GrowHeads - Váš online obchod s vysoko kvalitnými semenami, rastlinami a záhradnými potrebami na pestovanie konopy. Objavte náš veľký sortiment semien, rastlín a záhradných doplnkov pre váš zelený palec."
},
cs: {
short: "GrowHeads - Online obchod se semeny konopí, sazenicemi a zahradními potřebami",
long: "GrowHeads - Váš online obchod s vysoce kvalitními semeny, rostlinami a zahradními potřebami pro pěstování konopí. Objevte náš velký sortiment semen, rostlin a zahradních doplňků pro váš zelený palec."
},
ro: {
short: "GrowHeads - Magazin Online de Semințe de Cannabis, Butași și Articole de Grădinărit",
long: "GrowHeads - Magazinul dumneavoastră online pentru semințe, plante și articole de grădinărit de înaltă calitate pentru cultivarea de cannabis. Descoperiți sortimentul nostru mare de semințe, plante și accesorii de grădinărit pentru degetul verde."
}
short: "GrowHeads - Online-Shop für Cannanis-Samen, Stecklinge und Gartenbedarf",
long: "GrowHeads - Ihr Online-Shop für hochwertige Samen, Pflanzen und Gartenbedarf zur Cannabis Kultivierung. Entdecken Sie unser großes Sortiment an Saatgut, Pflanzen und Gartenzubehör für Ihren grünen Daumen."
},
// Keywords - Multilingual
keywords: {
de: "Seeds, Steckling, Cannabis, Biobizz, Growheads",
en: "Seeds, Cuttings, Cannabis, Biobizz, Growheads",
es: "Semillas, Esquejes, Cannabis, Biobizz, Growheads",
fr: "Graines, Boutures, Cannabis, Biobizz, Growheads",
it: "Semi, Talee, Cannabis, Biobizz, Growheads",
pl: "Nasiona, Sadzonki, Konopie, Biobizz, Growheads",
hu: "Magok, Dugványok, Kannabisz, Biobizz, Growheads",
sr: "Семена, Резнице, Канабис, Biobizz, Growheads",
bg: "Семена, Резници, Канабис, Biobizz, Growheads",
ru: "Семена, Черенки, Каннабис, Biobizz, Growheads",
uk: "Насіння, Живці, Канабіс, Biobizz, Growheads",
sk: "Semená, Sadenky, Konope, Biobizz, Growheads",
cs: "Semena, Sazenice, Konopí, Biobizz, Growheads",
ro: "Semințe, Butași, Cannabis, Biobizz, Growheads"
},
// Keywords
keywords: "Seeds, Steckling, Cannabis, Biobizz, Growheads",
// Shipping
shipping: {

View File

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

View File

@@ -1,381 +0,0 @@
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
import LanguageDetector from 'i18next-browser-languagedetector';
// Import all translation files
import translationDE from './locales/de/index.js';
import translationEN from './locales/en/index.js';
import translationAR from './locales/ar/translation.js';
import translationBG from './locales/bg/translation.js';
import translationCS from './locales/cs/translation.js';
import translationEL from './locales/el/translation.js';
import translationES from './locales/es/translation.js';
import translationFR from './locales/fr/translation.js';
import translationHR from './locales/hr/translation.js';
import translationHU from './locales/hu/translation.js';
import translationIT from './locales/it/translation.js';
import translationPL from './locales/pl/translation.js';
import translationRO from './locales/ro/translation.js';
import translationRU from './locales/ru/translation.js';
import translationSK from './locales/sk/translation.js';
import translationSL from './locales/sl/translation.js';
import translationSR from './locales/sr/translation.js';
import translationSV from './locales/sv/translation.js';
import translationTR from './locales/tr/translation.js';
import translationUK from './locales/uk/translation.js';
import translationZH from './locales/zh/translation.js';
// Import legal translations for all languages
// German
import legalAgbDE from './locales/de/legal-agb.js';
import legalDatenschutzDE from './locales/de/legal-datenschutz.js';
import legalImpressumDE from './locales/de/legal-impressum.js';
import legalWiderrufDE from './locales/de/legal-widerruf.js';
import legalBatterieDE from './locales/de/legal-batterie.js';
// English
import legalAgbEN from './locales/en/legal-agb.js';
import legalDatenschutzEN from './locales/en/legal-datenschutz.js';
import legalImpressumEN from './locales/en/legal-impressum.js';
import legalWiderrufEN from './locales/en/legal-widerruf.js';
import legalBatterieEN from './locales/en/legal-batterie.js';
// Arabic
import legalAgbAR from './locales/ar/legal-agb.js';
import legalDatenschutzAR from './locales/ar/legal-datenschutz.js';
import legalImpressumAR from './locales/ar/legal-impressum.js';
import legalWiderrufAR from './locales/ar/legal-widerruf.js';
import legalBatterieAR from './locales/ar/legal-batterie.js';
// Bulgarian
import legalAgbBG from './locales/bg/legal-agb.js';
import legalDatenschutzBG from './locales/bg/legal-datenschutz.js';
import legalImpressumBG from './locales/bg/legal-impressum.js';
import legalWiderrufBG from './locales/bg/legal-widerruf.js';
import legalBatterieBG from './locales/bg/legal-batterie.js';
// Czech
import legalAgbCS from './locales/cs/legal-agb.js';
import legalDatenschutzCS from './locales/cs/legal-datenschutz.js';
import legalImpressumCS from './locales/cs/legal-impressum.js';
import legalWiderrufCS from './locales/cs/legal-widerruf.js';
import legalBatterieCS from './locales/cs/legal-batterie.js';
// Greek
import legalAgbEL from './locales/el/legal-agb.js';
import legalDatenschutzEL from './locales/el/legal-datenschutz.js';
import legalImpressumEL from './locales/el/legal-impressum.js';
import legalWiderrufEL from './locales/el/legal-widerruf.js';
import legalBatterieEL from './locales/el/legal-batterie.js';
// Spanish
import legalAgbES from './locales/es/legal-agb.js';
import legalDatenschutzES from './locales/es/legal-datenschutz.js';
import legalImpressumES from './locales/es/legal-impressum.js';
import legalWiderrufES from './locales/es/legal-widerruf.js';
import legalBatterieES from './locales/es/legal-batterie.js';
// French
import legalAgbFR from './locales/fr/legal-agb.js';
import legalDatenschutzFR from './locales/fr/legal-datenschutz.js';
import legalImpressumFR from './locales/fr/legal-impressum.js';
import legalWiderrufFR from './locales/fr/legal-widerruf.js';
import legalBatterieFR from './locales/fr/legal-batterie.js';
// Croatian
import legalAgbHR from './locales/hr/legal-agb.js';
import legalDatenschutzHR from './locales/hr/legal-datenschutz.js';
import legalImpressumHR from './locales/hr/legal-impressum.js';
import legalWiderrufHR from './locales/hr/legal-widerruf.js';
import legalBatterieHR from './locales/hr/legal-batterie.js';
// Hungarian
import legalAgbHU from './locales/hu/legal-agb.js';
import legalDatenschutzHU from './locales/hu/legal-datenschutz.js';
import legalImpressumHU from './locales/hu/legal-impressum.js';
import legalWiderrufHU from './locales/hu/legal-widerruf.js';
import legalBatterieHU from './locales/hu/legal-batterie.js';
// Italian
import legalAgbIT from './locales/it/legal-agb.js';
import legalDatenschutzIT from './locales/it/legal-datenschutz.js';
import legalImpressumIT from './locales/it/legal-impressum.js';
import legalWiderrufIT from './locales/it/legal-widerruf.js';
import legalBatterieIT from './locales/it/legal-batterie.js';
// Polish
import legalAgbPL from './locales/pl/legal-agb.js';
import legalDatenschutzPL from './locales/pl/legal-datenschutz.js';
import legalImpressumPL from './locales/pl/legal-impressum.js';
import legalWiderrufPL from './locales/pl/legal-widerruf.js';
import legalBatteriePL from './locales/pl/legal-batterie.js';
// Romanian
import legalAgbRO from './locales/ro/legal-agb.js';
import legalDatenschutzRO from './locales/ro/legal-datenschutz.js';
import legalImpressumRO from './locales/ro/legal-impressum.js';
import legalWiderrufRO from './locales/ro/legal-widerruf.js';
import legalBatterieRO from './locales/ro/legal-batterie.js';
// Russian
import legalAgbRU from './locales/ru/legal-agb.js';
import legalDatenschutzRU from './locales/ru/legal-datenschutz.js';
import legalImpressumRU from './locales/ru/legal-impressum.js';
import legalWiderrufRU from './locales/ru/legal-widerruf.js';
import legalBatterieRU from './locales/ru/legal-batterie.js';
// Slovak
import legalAgbSK from './locales/sk/legal-agb.js';
import legalDatenschutzSK from './locales/sk/legal-datenschutz.js';
import legalImpressumSK from './locales/sk/legal-impressum.js';
import legalWiderrufSK from './locales/sk/legal-widerruf.js';
import legalBatterieSK from './locales/sk/legal-batterie.js';
// Slovenian
import legalAgbSL from './locales/sl/legal-agb.js';
import legalDatenschutzSL from './locales/sl/legal-datenschutz.js';
import legalImpressumSL from './locales/sl/legal-impressum.js';
import legalWiderrufSL from './locales/sl/legal-widerruf.js';
import legalBatterieSL from './locales/sl/legal-batterie.js';
// Serbian
import legalAgbSR from './locales/sr/legal-agb.js';
import legalDatenschutzSR from './locales/sr/legal-datenschutz.js';
import legalImpressumSR from './locales/sr/legal-impressum.js';
import legalWiderrufSR from './locales/sr/legal-widerruf.js';
import legalBatterieSR from './locales/sr/legal-batterie.js';
// Swedish
import legalAgbSV from './locales/sv/legal-agb.js';
import legalDatenschutzSV from './locales/sv/legal-datenschutz.js';
import legalImpressumSV from './locales/sv/legal-impressum.js';
import legalWiderrufSV from './locales/sv/legal-widerruf.js';
import legalBatterieSV from './locales/sv/legal-batterie.js';
// Turkish
import legalAgbTR from './locales/tr/legal-agb.js';
import legalDatenschutzTR from './locales/tr/legal-datenschutz.js';
import legalImpressumTR from './locales/tr/legal-impressum.js';
import legalWiderrufTR from './locales/tr/legal-widerruf.js';
import legalBatterieTR from './locales/tr/legal-batterie.js';
// Ukrainian
import legalAgbUK from './locales/uk/legal-agb.js';
import legalDatenschutzUK from './locales/uk/legal-datenschutz.js';
import legalImpressumUK from './locales/uk/legal-impressum.js';
import legalWiderrufUK from './locales/uk/legal-widerruf.js';
import legalBatterieUK from './locales/uk/legal-batterie.js';
// Chinese
import legalAgbZH from './locales/zh/legal-agb.js';
import legalDatenschutzZH from './locales/zh/legal-datenschutz.js';
import legalImpressumZH from './locales/zh/legal-impressum.js';
import legalWiderrufZH from './locales/zh/legal-widerruf.js';
import legalBatterieZH from './locales/zh/legal-batterie.js';
const resources = {
de: {
translation: translationDE,
'legal-agb': legalAgbDE,
'legal-datenschutz': legalDatenschutzDE,
'legal-impressum': legalImpressumDE,
'legal-widerruf': legalWiderrufDE,
'legal-batterie': legalBatterieDE
},
en: {
translation: translationEN,
'legal-agb': legalAgbEN,
'legal-datenschutz': legalDatenschutzEN,
'legal-impressum': legalImpressumEN,
'legal-widerruf': legalWiderrufEN,
'legal-batterie': legalBatterieEN
},
ar: {
translation: translationAR,
'legal-agb': legalAgbAR,
'legal-datenschutz': legalDatenschutzAR,
'legal-impressum': legalImpressumAR,
'legal-widerruf': legalWiderrufAR,
'legal-batterie': legalBatterieAR
},
bg: {
translation: translationBG,
'legal-agb': legalAgbBG,
'legal-datenschutz': legalDatenschutzBG,
'legal-impressum': legalImpressumBG,
'legal-widerruf': legalWiderrufBG,
'legal-batterie': legalBatterieBG
},
cs: {
translation: translationCS,
'legal-agb': legalAgbCS,
'legal-datenschutz': legalDatenschutzCS,
'legal-impressum': legalImpressumCS,
'legal-widerruf': legalWiderrufCS,
'legal-batterie': legalBatterieCS
},
el: {
translation: translationEL,
'legal-agb': legalAgbEL,
'legal-datenschutz': legalDatenschutzEL,
'legal-impressum': legalImpressumEL,
'legal-widerruf': legalWiderrufEL,
'legal-batterie': legalBatterieEL
},
es: {
translation: translationES,
'legal-agb': legalAgbES,
'legal-datenschutz': legalDatenschutzES,
'legal-impressum': legalImpressumES,
'legal-widerruf': legalWiderrufES,
'legal-batterie': legalBatterieES
},
fr: {
translation: translationFR,
'legal-agb': legalAgbFR,
'legal-datenschutz': legalDatenschutzFR,
'legal-impressum': legalImpressumFR,
'legal-widerruf': legalWiderrufFR,
'legal-batterie': legalBatterieFR
},
hr: {
translation: translationHR,
'legal-agb': legalAgbHR,
'legal-datenschutz': legalDatenschutzHR,
'legal-impressum': legalImpressumHR,
'legal-widerruf': legalWiderrufHR,
'legal-batterie': legalBatterieHR
},
hu: {
translation: translationHU,
'legal-agb': legalAgbHU,
'legal-datenschutz': legalDatenschutzHU,
'legal-impressum': legalImpressumHU,
'legal-widerruf': legalWiderrufHU,
'legal-batterie': legalBatterieHU
},
it: {
translation: translationIT,
'legal-agb': legalAgbIT,
'legal-datenschutz': legalDatenschutzIT,
'legal-impressum': legalImpressumIT,
'legal-widerruf': legalWiderrufIT,
'legal-batterie': legalBatterieIT
},
pl: {
translation: translationPL,
'legal-agb': legalAgbPL,
'legal-datenschutz': legalDatenschutzPL,
'legal-impressum': legalImpressumPL,
'legal-widerruf': legalWiderrufPL,
'legal-batterie': legalBatteriePL
},
ro: {
translation: translationRO,
'legal-agb': legalAgbRO,
'legal-datenschutz': legalDatenschutzRO,
'legal-impressum': legalImpressumRO,
'legal-widerruf': legalWiderrufRO,
'legal-batterie': legalBatterieRO
},
ru: {
translation: translationRU,
'legal-agb': legalAgbRU,
'legal-datenschutz': legalDatenschutzRU,
'legal-impressum': legalImpressumRU,
'legal-widerruf': legalWiderrufRU,
'legal-batterie': legalBatterieRU
},
sk: {
translation: translationSK,
'legal-agb': legalAgbSK,
'legal-datenschutz': legalDatenschutzSK,
'legal-impressum': legalImpressumSK,
'legal-widerruf': legalWiderrufSK,
'legal-batterie': legalBatterieSK
},
sl: {
translation: translationSL,
'legal-agb': legalAgbSL,
'legal-datenschutz': legalDatenschutzSL,
'legal-impressum': legalImpressumSL,
'legal-widerruf': legalWiderrufSL,
'legal-batterie': legalBatterieSL
},
sr: {
translation: translationSR,
'legal-agb': legalAgbSR,
'legal-datenschutz': legalDatenschutzSR,
'legal-impressum': legalImpressumSR,
'legal-widerruf': legalWiderrufSR,
'legal-batterie': legalBatterieSR
},
sv: {
translation: translationSV,
'legal-agb': legalAgbSV,
'legal-datenschutz': legalDatenschutzSV,
'legal-impressum': legalImpressumSV,
'legal-widerruf': legalWiderrufSV,
'legal-batterie': legalBatterieSV
},
tr: {
translation: translationTR,
'legal-agb': legalAgbTR,
'legal-datenschutz': legalDatenschutzTR,
'legal-impressum': legalImpressumTR,
'legal-widerruf': legalWiderrufTR,
'legal-batterie': legalBatterieTR
},
uk: {
translation: translationUK,
'legal-agb': legalAgbUK,
'legal-datenschutz': legalDatenschutzUK,
'legal-impressum': legalImpressumUK,
'legal-widerruf': legalWiderrufUK,
'legal-batterie': legalBatterieUK
},
zh: {
translation: translationZH,
'legal-agb': legalAgbZH,
'legal-datenschutz': legalDatenschutzZH,
'legal-impressum': legalImpressumZH,
'legal-widerruf': legalWiderrufZH,
'legal-batterie': legalBatterieZH
}
};
i18n
.use(LanguageDetector)
.use(initReactI18next)
.init({
resources,
fallbackLng: 'de', // German as fallback since it's your primary language
debug: process.env.NODE_ENV === 'development',
// Language detection options
detection: {
// Order of language detection methods
order: ['localStorage', 'navigator', 'htmlTag'],
// Cache the language selection
caches: ['localStorage'],
// Check for language in localStorage
lookupLocalStorage: 'i18nextLng'
},
interpolation: {
escapeValue: false // React already escapes values
},
// Namespace configuration
defaultNS: 'translation',
// React-specific options
react: {
useSuspense: false // Disable suspense for class components compatibility
}
});
// Export withI18n and other utilities for easy access
export { withI18n, withTranslation, withLanguage, LanguageContext, LanguageProvider } from './withTranslation.js';
export default i18n;

View File

@@ -1,25 +0,0 @@
export default {
"login": "تسجيل الدخول",
"register": "تسجيل",
"logout": "تسجيل خروج",
"profile": "الملف الشخصي",
"email": "البريد الإلكتروني",
"password": "كلمة المرور",
"confirmPassword": "تأكيد كلمة المرور",
"forgotPassword": "هل نسيت كلمة المرور؟",
"loginWithGoogle": "تسجيل الدخول باستخدام جوجل",
"or": "أو",
"privacyAccept": "بالنقر على \"تسجيل الدخول باستخدام جوجل\" أوافق على",
"privacyPolicy": "سياسة الخصوصية",
"passwordMinLength": "يجب أن تكون كلمة المرور 8 أحرف على الأقل",
"newPasswordMinLength": "يجب أن تكون كلمة المرور الجديدة 8 أحرف على الأقل",
"menu": {
"profile": "الملف الشخصي",
"myProfile": "ملفي الشخصي",
"checkout": "إتمام الشراء",
"orders": "الطلبات",
"settings": "الإعدادات",
"adminDashboard": "لوحة تحكم المسؤول",
"adminUsers": "مستخدمو المسؤول"
}
};

View File

@@ -1,39 +0,0 @@
export default {
"title": "العربة",
"empty": "فارغ",
"addToCart": "أضف إلى العربة",
"preorderCutting": "اطلب مسبقًا كقطع",
"continueShopping": "تابع التسوق",
"proceedToCheckout": "المتابعة إلى الدفع",
"productCount": "{{count}} {{count, plural, one {منتج} other {منتجات}}}",
"productSingular": "منتج",
"productPlural": "منتجات",
"removeFromCart": "إزالة من العربة",
"openCart": "افتح العربة",
"availableFrom": "متاح من {{date}}",
"backToOrder": "← العودة إلى الطلب",
"summary": {
"title": "ملخص الطلب",
"goodsNet": "البضائع (صافي):",
"shippingNet": "الشحن (صافي):",
"totalGoods": "إجمالي البضائع:",
"shippingCosts": "تكاليف الشحن:",
"total": "الإجمالي:",
"totalWeight": "الوزن الكلي: {{weight}} كجم",
"freeFrom100": "(مجاني من €100)",
"free": "مجاني"
},
"itemCount": {
"singular": "منتج",
"plural": "منتجات"
},
"sync": {
"title": "مزامنة العربة",
"description": "لديك عربة محفوظة في حسابك. يرجى اختيار كيفية المتابعة:",
"deleteServer": "حذف عربة الخادم",
"useServer": "استخدام عربة الخادم",
"merge": "دمج العربات",
"currentCart": "عربتك الحالية",
"serverCart": "العربة المحفوظة في ملفك الشخصي"
}
};

View File

@@ -1,3 +0,0 @@
export default {
"privacyRead": "تم القراءة والموافقة",
};

View File

@@ -1,34 +0,0 @@
export default {
"invoiceAddress": "عنوان الفاتورة",
"deliveryAddress": "عنوان التوصيل",
"saveForFuture": "احفظ للطلبات المستقبلية",
"pickupDate": "لمين التاريخ مطلوب استلام القصاصات؟",
"note": "ملاحظة",
"sameAddress": "عنوان التوصيل هو نفسه عنوان الفاتورة",
"termsAccept": "لقد قرأت الشروط والأحكام، سياسة الخصوصية، وأحكام حق الانسحاب",
"selectDeliveryMethod": "اختر طريقة الشحن",
"selectPaymentMethod": "اختر طريقة الدفع",
"orderSummary": "ملخص الطلب",
"addressValidationError": "يرجى التحقق من بياناتك في حقول العنوان.",
"processingOrder": "يتم معالجة الطلب...",
"completeOrder": "إتمام الطلب",
"termsValidationError": "يرجى قبول الشروط والأحكام، سياسة الخصوصية، وحق الانسحاب للمتابعة.",
"addressFields": {
"firstName": "الاسم الأول",
"lastName": "اسم العائلة",
"addressSupplement": "إضافة للعنوان",
"street": "الشارع",
"houseNumber": "رقم المنزل",
"postalCode": "الرمز البريدي",
"city": "المدينة",
"country": "البلد"
},
"validationErrors": {
"firstNameRequired": "الاسم الأول مطلوب",
"lastNameRequired": "اسم العائلة مطلوب",
"streetRequired": "الشارع مطلوب",
"houseNumberRequired": "رقم المنزل مطلوب",
"postalCodeRequired": "الرمز البريدي مطلوب",
"cityRequired": "المدينة مطلوبة"
}
};

View File

@@ -1,19 +0,0 @@
export default {
"loading": "جارٍ التحميل...",
"error": "خطأ",
"close": "إغلاق",
"save": "حفظ",
"cancel": "إلغاء",
"ok": "موافق",
"yes": "نعم",
"no": "لا",
"next": "التالي",
"back": "رجوع",
"edit": "تعديل",
"delete": "حذف",
"add": "إضافة",
"remove": "إزالة",
"products": "منتجات",
"product": "منتج",
"days": "أيام"
};

View File

@@ -1,35 +0,0 @@
export default {
"methods": {
"dhl": "DHL",
"dpd": "DPD",
"sperrgut": "بضائع ضخمة",
"sperrgutName": "بضائع ضخمة",
"pickup": "استلام من المتجر"
},
"descriptions": {
"standard": "الشحن العادي",
"standardFree": "الشحن العادي - مجاني للطلبات فوق 100€!",
"notAvailable": "غير متاح للاختيار لأن عنصر واحد أو أكثر يمكن استلامه فقط",
"bulky": "للعناصر الكبيرة والثقيلة",
"pickupOnly": "الاستلام فقط"
},
"prices": {
"free": "مجاني",
"freeFrom100": "(مجاني من 100€)",
"dhl": "6.99 €",
"dpd": "4.90 €",
"sperrgut": "28.99 €"
},
"times": {
"cutting14Days": "مدة التوصيل: 14 يوم",
"standard2to3Days": "مدة التوصيل: 2-3 أيام",
"supplier7to9Days": "مدة التوصيل: 7-9 أيام"
},
"selector": {
"title": "اختر طريقة الشحن",
"freeShippingInfo": "💡 شحن مجاني للطلبات فوق 100€!",
"remainingForFree": "أضف {{amount}}€ أخرى للشحن المجاني.",
"congratsFreeShipping": "🎉 مبروك! حصلت على شحن مجاني!",
"cartQualifiesFree": "سلة مشترياتك بقيمة {{amount}}€ مؤهلة للشحن المجاني."
}
};

View File

@@ -1,7 +0,0 @@
export default {
"sorting": "الترتيب",
"perPage": "لكل صفحة",
"availability": "التوفر",
"manufacturer": "الشركة المصنعة",
"all": "الكل"
};

View File

@@ -1,15 +0,0 @@
export default {
"hours": "السبت 11 صباحًا - 7 مساءً",
"address": "شارع تراشنبرجر 14 - دريسدن",
"location": "بين محطة بيسشن وميدان تراشنبرجر",
"allPricesIncl": "* جميع الأسعار تشمل ضريبة القيمة المضافة القانونية، بالإضافة إلى الشحن",
"copyright": "© {{year}} GrowHeads.de",
"legal": {
"datenschutz": "سياسة الخصوصية",
"agb": "الشروط والأحكام",
"sitemap": "خريطة الموقع",
"impressum": "الإشعار القانوني",
"batteriegesetzhinweise": "معلومات قانون البطاريات",
"widerrufsrecht": "حق الانسحاب"
}
};

View File

@@ -1,43 +0,0 @@
import locale from './locale.js';
import navigation from './navigation.js';
import auth from './auth.js';
import cart from './cart.js';
import product from './product.js';
import search from './search.js';
import sorting from './sorting.js';
import chat from './chat.js';
import delivery from './delivery.js';
import checkout from './checkout.js';
import payment from './payment.js';
import filters from './filters.js';
import tax from './tax.js';
import footer from './footer.js';
import titles from './titles.js';
import sections from './sections.js';
import pages from './pages.js';
import orders from './orders.js';
import settings from './settings.js';
import common from './common.js';
export default {
"locale": locale,
"navigation": navigation,
"auth": auth,
"cart": cart,
"product": product,
"search": search,
"sorting": sorting,
"chat": chat,
"delivery": delivery,
"checkout": checkout,
"payment": payment,
"filters": filters,
"tax": tax,
"footer": footer,
"titles": titles,
"sections": sections,
"pages": pages,
"orders": orders,
"settings": settings,
"common": common
};

View File

@@ -1,69 +0,0 @@
export default {
"title": "الشروط والأحكام العامة",
"deliveryShippingConditions": "شروط التسليم والشحن",
"deliveryTerms": {
"1": "يستغرق الشحن ما بين 1 إلى 7 أيام.",
"2": "تظل البضاعة ملكًا لشركة Growheads حتى يتم استلام الدفعة كاملة.",
"3": "إذا كان هناك اشتباه في تلف البضاعة أثناء النقل أو نقص في الأصناف، يجب الاحتفاظ بتغليف الشحن لفحصه من قبل خبير. يجب أن يؤكد الناقل أي تلف في التغليف على مذكرة التسليم مع تحديد نوع ومدى التلف. يجب الإبلاغ عن أضرار الشحن إلى Growheads فورًا كتابيًا عبر الفاكس أو البريد الإلكتروني أو البريد. لهذا الغرض، يجب التقاط صور للبضاعة التالفة وكذلك لصندوق الشحن التالف مع ملصق العنوان. يجب أيضًا الاحتفاظ بصندوق الشحن التالف. هذه الوثائق مطلوبة للمطالبة بالتعويض من شركة الشحن.",
"4": "عند إعادة البضائع المعيبة، يجب على العميل التأكد من تغليف البضائع بشكل صحيح.",
"5": "يجب تسجيل جميع المرتجعات مسبقًا لدى Growheads.",
"6": "يتحمل العميل مخاطر إرسال الأصناف إلينا، ما لم تكن إعادة البضائع المعيبة.",
"7": "يحق لـ Growheads أن تطلب استلام البضائع من خلال Deutsche Post/GLS أو ناقل من اختيارها.",
"8": "يتم حساب تكاليف البريد بناءً على الوزن. تحتفظ Growheads بحق تمرير أي زيادات في الأسعار من شركات الشحن (رسوم المرور، رسوم الوقود).",
"9": "عادةً ما يتم شحن طرودنا عبر: GLS، DHL و Deutsche Post AG.",
"10": "بالنسبة للأصناف الثقيلة أو الضخمة بشكل خاص، نحتفظ بحق فرض رسوم إضافية على تكاليف التسليم. عادةً ما تكون هذه الرسوم مذكورة في قائمة الأسعار.",
"11": "يمكن الدفع مقدمًا عن طريق التحويل البنكي إلى الحساب المصرفي المحدد.",
"12": "إذا حدث تأخير في التسليم نتحمل مسؤوليته، فإن فترة السماح التي يحق للمشتري تحديدها محدودة بأسبوعين. تبدأ الفترة من استلام Growheads لإشعار فترة السماح.",
"13": "يجب الإبلاغ كتابيًا عن العيوب الظاهرة في البضاعة فور التسليم. إذا لم يفعل العميل ذلك، تُستبعد مطالبات الضمان عن العيوب الظاهرة.",
"14": "إذا اشتكى العميل من عيب، يجب عليه إعادة البضاعة المعيبة إلينا مع وصف دقيق للعيب. يجب إرفاق نسخة من فاتورتنا مع الشحنة. يجب إعادة البضاعة في التغليف الأصلي أو في تغليف يحمي البضاعة بنفس طريقة التغليف الأصلي لتجنب التلف أثناء النقل العكسي."
},
"consultationLiability": {
"title": "الاستشارة والمسؤولية",
"1": "نقدم استشارات فنية تطبيقية بأفضل ما لدينا من معرفة بناءً على خبرتنا ومهاراتنا.",
"2": "المشتري مسؤول عن الالتزام باللوائح القانونية المتعلقة بالتخزين والنقل والاستخدام للبضائع."
},
"paymentConditions": {
"title": "شروط الدفع",
"1": "تظل البضاعة ملكًا لشركة Growheads حتى يتم استلام الدفعة كاملة.",
"2": "تُدفع الفواتير مقدمًا عن طريق التحويل البنكي إلى حسابنا. إذا دفعت مقدمًا، سيتم شحن البضاعة بمجرد تسجيل المبلغ في حسابنا."
},
"retentionOfTitle": {
"title": "الاحتفاظ بالملكية",
"content": "تظل البضاعة المسلمة ملكًا لشركة Growheads حتى يسدد المشتري جميع المطالبات الموجهة إليه. إذا قام البائع بإعادة بيع البضاعة، فإنه يتنازل بموجب هذا عن المطالبات الناشئة من البيع لنا. إذا تأخر المشتري في دفعاته، يمكننا في أي وقت طلب إعادة البضاعة دون الانسحاب من العقد."
},
"distanceSelling": {
"title": "معلومات وفقًا لقانون البيع عن بعد",
"intro": "تنطبق المعلومات التالية فقط على العقود المبرمة بين Growheads والمستهلكين عن طريق طلب الكتالوج أو الإنترنت أو وسائل الاتصال عن بعد الأخرى. وهي محدودة للمستهلكين داخل الاتحاد الأوروبي.",
"sections": {
"1": {
"title": "الخصائص الأساسية للبضاعة",
"content": "يرجى الرجوع إلى الشروحات في الكتالوج أو على موقعنا الإلكتروني لمعرفة الخصائص الأساسية للبضاعة. العروض في كتالوجنا وعلى موقعنا غير ملزمة. الطلبات المقدمة إلينا تعتبر عروضًا ملزمة. يمكن لـ Growheads قبولها خلال 14 يومًا من استلام الطلب عن طريق إرسال تأكيد الطلب أو شحن البضاعة."
},
"2": {
"title": "التحفظ",
"content": "إذا لم تكن جميع الأصناف المطلوبة متوفرة للتسليم، نحتفظ بحق التسليم الجزئي إذا كان ذلك معقولًا للعميل. قد تختلف بعض الأصناف عن الصور والوصف في الكتالوج والموقع الإلكتروني، خاصةً البضائع المصنوعة يدويًا. لذلك نحتفظ بحق تسليم بضائع ذات جودة وسعر معادل إذا لزم الأمر."
},
"3": {
"title": "الأسعار والضرائب",
"content": "يمكنك العثور على أسعار الأصناف الفردية شاملة ضريبة القيمة المضافة في الكتالوج أو على موقعنا. تصبح الأسعار غير صالحة عند صدور كتالوج جديد."
},
"4": "جميع الأسعار عرضة للأخطاء أو تقلبات الأسعار. إذا حدث تغيير في السعر، يحق للمشتري ممارسة حقه في الإرجاع.",
"5": {
"title": "مدة الضمان",
"content": "تطبق فترة الضمان القانونية لمدة 24 (أربعة وعشرين) شهرًا. في حالات فردية، قد تنطبق فترات أطول إذا منحها المصنع."
},
"6": {
"title": "حق الإرجاع / حق الانسحاب",
"content": "يتمتع العميل بحق إرجاع لمدة 14 يومًا.\nتبدأ الفترة من استلام العميل للبضاعة وتُعتبر محفوظة بإرسال الانسحاب في الوقت المناسب إلى Growheads. تستثنى من ذلك الأغذية والسلع القابلة للتلف، وكذلك المنتجات المصممة خصيصًا أو البضائع التي تم طلبها بناءً على رغبة العميل. يجب أن يتم الإرجاع عن طريق إعادة البضاعة خلال الفترة. إذا لم يكن بالإمكان شحن البضاعة، يجب إرسال طلب الإرجاع إلينا خلال الفترة عن طريق رسالة، بطاقة بريدية، بريد إلكتروني، أو وسيلة دائمة أخرى. يكفي الإرسال في الوقت المناسب إلى العنوان المذكور تحت 7) للحفاظ على المهلة. لا يتطلب الانسحاب سببًا. سيتم رد ثمن الشراء وأي تكاليف تسليم وشحن بعد استلامنا للبضاعة. القيمة الحاسمة هي قيمة البضاعة المعادة وقت الشراء، وليس قيمة الطلب الكامل. عادةً ما يمكن لـ Growheads ترتيب الاستلام منك."
},
"7": {
"title": "اسم وعنوان الشركة، الشكاوى، الاستدعاءات",
"content": "Growheads\nTrachenberger Straße 14\n01129 Dresden"
},
"8": {
"title": "مكان التنفيذ والاختصاص القضائي",
"content": "مكان التنفيذ والاختصاص القضائي لجميع المطالبات هو دريسدن، ما لم تنص أحكام قانونية إلزامية على خلاف ذلك."
}
}
}
};

View File

@@ -1,8 +0,0 @@
export default {
"title": "معلومات قانون البطاريات",
"intro": "فيما يتعلق ببيع البطاريات أو تسليم الأجهزة التي تحتوي على بطاريات، نحن ملزمون بإبلاغكم بما يلي:",
"returnObligation": "بصفتك مستخدم نهائي، أنت ملزم قانونيًا بإعادة البطاريات المستخدمة. يمكنك إعادة البطاريات القديمة التي نمتلكها أو التي كانت ضمن مجموعتنا كبطاريات جديدة مجانًا إلى مستودع الشحن الخاص بنا (عنوان الشحن).",
"symbolsInfo": "الرموز المعروضة على البطاريات تعني ما يلي:",
"wasteSymbol": "رمز سلة المهملات المعلمة بعلامة إلغاء يعني أنه لا يجوز التخلص من البطارية مع النفايات المنزلية.",
"chemicalSymbols": "Pb = البطارية تحتوي على أكثر من 0.004 بالمئة بالوزن من الرصاص\nCd = البطارية تحتوي على أكثر من 0.002 بالمئة بالوزن من الكادميوم\nHg = البطارية تحتوي على أكثر من 0.0005 بالمئة بالوزن من الزئبق."
};

View File

@@ -1,70 +0,0 @@
export default {
"title": "سياسة الخصوصية",
"responsibleParty": {
"title": "الجهة المسؤولة بموجب قانون حماية البيانات:",
"company": "Growheads\nTrachenberger Straße 14\n01129 Dresden"
},
"generalInfo": "ما لم يُذكر خلاف ذلك أدناه، فإن تقديم بياناتك الشخصية ليس مطلوبًا قانونيًا أو تعاقديًا، ولا هو ضروري لإبرام عقد. أنت غير ملزم بتقديم البيانات. عدم تقديمها لا يترتب عليه أي عواقب. هذا ينطبق فقط طالما لم يُذكر خلاف ذلك في عمليات المعالجة التالية. \"البيانات الشخصية\" تعني جميع المعلومات المتعلقة بشخص طبيعي محدد أو قابل للتحديد.",
"sections": {
"informationDeletion": {
"title": "المعلومات، الحذف، الحظر",
"content": "يمكنك في أي وقت طلب معلومات عن بياناتك الشخصية، مصدرها والمستلمين لها، وهدف معالجة البيانات، كما يمكنك طلب تصحيح أو حظر أو حذف هذه البيانات مجانًا. يرجى استخدام خيارات الاتصال الموجودة في تذييل الصفحة أو في الإشعار القانوني لهذا الغرض. نحن متاحون أيضًا في أي وقت لأي أسئلة إضافية حول الموضوع. يرجى ملاحظة أننا غير مخولين ولن نقوم بحذف بيانات الفواتير، بيانات البنك، والبيانات التي تم إرسالها لمزود خدمة الشحن. البيانات التي يمكن حذفها تشمل: حسابات العملاء على خادم الويب، وكذلك في نظام إدارة البضائع، والبريد الإلكتروني الذي لا يرتبط مباشرة بطلب.",
},
"serverLogfiles": {
"title": "ملفات سجل الخادم",
"content": "يمكنك زيارة مواقعنا الإلكترونية دون تقديم أي معلومات عن نفسك. في كل مرة تصل فيها إلى موقعنا، يتم إرسال بيانات الاستخدام من متصفح الإنترنت الخاص بك وتخزينها في بيانات السجل (ملفات سجل الخادم). تشمل هذه البيانات المخزنة، على سبيل المثال، اسم الصفحة التي تم الوصول إليها، تاريخ ووقت الوصول، كمية البيانات المنقولة، ومزود الخدمة الذي يطلب البيانات. تُستخدم هذه البيانات فقط لضمان التشغيل السلس لموقعنا وتحسين عرضنا. هذه البيانات ليست بيانات شخصية. لا يتم دمج هذه البيانات مع مصادر بيانات أخرى. إذا علمنا بمؤشرات محددة على استخدام غير قانوني، نحتفظ بالحق في مراجعة هذه البيانات لاحقًا.",
},
"customerAccount": {
"title": "حساب العميل",
"content": "عند فتح حساب عميل، نجمع بياناتك الشخصية بالقدر المحدد هناك. تهدف معالجة البيانات إلى تحسين تجربة التسوق الخاصة بك وتبسيط معالجة الطلبات. تتم المعالجة بناءً على المادة 6 (1) حرف أ من DSGVO بموافقتك. يمكنك سحب موافقتك في أي وقت بإبلاغنا، دون التأثير على قانونية المعالجة التي تمت بناءً على الموافقة حتى السحب. سيتم حذف حساب العميل الخاص بك بعد ذلك.",
},
"googleSSO": {
"title": "تسجيل الدخول باستخدام Google (تسجيل الدخول الموحد من Google)",
"content": "نقدم لك خيار تسجيل الدخول إلى حساب العميل الخاص بك باستخدام حساب Google الخاص بك. عند استخدام وظيفة \"تسجيل الدخول باستخدام Google\"، يتم التحقق من الهوية عبر خدمة Google Single Sign-On. في هذه العملية، قد يتم تخزين ملفات تعريف الارتباط من Google على جهازك، وهي ضرورية لعملية تسجيل الدخول والتحقق من الهوية. كجزء من تسجيل الدخول عبر Google، نتلقى من Google بيانات شخصية معينة للتحقق من هويتك. على وجه الخصوص، تنقل Google لنا اسمك، عنوان بريدك الإلكتروني، وإذا كان مخزنًا في حساب Google الخاص بك، صورة ملفك الشخصي. يتم توفير هذه المعلومات من Google بمجرد تسجيل دخولك إلى متجرنا الإلكتروني باستخدام حساب Google الخاص بك. يمكن لـ Google، كمزود طرف ثالث، الوصول إلى هذه البيانات ومعالجتها؛ وقد يشمل ذلك نقل البيانات إلى الولايات المتحدة الأمريكية. لقد أبرمنا مع Google بنود حماية بيانات قياسية وفقًا للمادة 46 (2) حرف ج من DSGVO لضمان مستوى مناسب من حماية البيانات عند نقل بياناتك. يمكن العثور على مزيد من التفاصيل حول معالجة البيانات بواسطة Google في سياسة الخصوصية الخاصة بـ Google (على https://policies.google.com/privacy?hl=en).",
"legalBasis": "تتم معالجة البيانات المتعلقة بتسجيل الدخول عبر Google بناءً على المادة 6 (1) حرف ب من DSGVO (تنفيذ التدابير التمهيدية للعقد وتنفيذ العقد، مثل إنشاء واستخدام حساب العميل الخاص بك) وكذلك المادة 6 (1) حرف ف من DSGVO (مصلحتنا المشروعة في توفير خيار تسجيل دخول سريع ومريح لك).",
"voluntaryUse": "استخدام وظيفة \"تسجيل الدخول باستخدام Google\" هو أمر طوعي. بالطبع يمكنك أيضًا استخدام متجرنا الإلكتروني وحساب العميل الخاص بك بدون Google SSO عن طريق التسجيل أو تسجيل الدخول باستخدام بريدك الإلكتروني وكلمة المرور كالمعتاد. إذا اخترت استخدام تسجيل الدخول عبر Google، يمكنك قطع هذا الرابط في أي وقت عن طريق إزالة الاتصال في إعدادات حساب Google الخاص بك.",
"yourRights": "فيما يتعلق بالبيانات الشخصية المعالجة عبر Google SSO، لديك الحقوق القانونية كصاحب بيانات. على وجه الخصوص، لديك الحق في الحصول على معلومات حول البيانات المخزنة عنك (المادة 15 DSGVO)، وتصحيح البيانات غير الدقيقة (المادة 16 DSGVO)، أو طلب حذف بياناتك (المادة 17 DSGVO). علاوة على ذلك، لديك الحق في تقييد معالجة بياناتك (المادة 18 DSGVO) والحق في نقل البيانات (المادة 20 DSGVO). إذا استندنا في المعالجة إلى مصلحتنا المشروعة، يمكنك الاعتراض على المعالجة (المادة 21 DSGVO). بالإضافة إلى ذلك، يمكنك الاتصال في أي وقت بالسلطة المختصة لحماية البيانات لتقديم شكوى. تنطبق حقوقك وخياراتك القائمة من بقية سياسة الخصوصية أيضًا على استخدام تسجيل الدخول عبر Google.",
},
"orders": {
"title": "جمع، معالجة واستخدام البيانات الشخصية للطلبات",
"content": "عند تقديم طلب، نجمع ونستخدم بياناتك الشخصية فقط بالقدر اللازم لتنفيذ ومعالجة طلبك وللتعامل مع استفساراتك. تقديم البيانات مطلوب لإبرام العقد. عدم تقديمها يؤدي إلى عدم إبرام العقد. تتم المعالجة بناءً على المادة 6 (1) حرف ب من DSGVO وهي ضرورية لتنفيذ عقد معك. لن يتم تمرير بياناتك إلى أطراف ثالثة بدون موافقتك الصريحة. الاستثناء الوحيد هم شركاؤنا في الخدمة الذين نحتاجهم لمعالجة العلاقة التعاقدية أو مزودو الخدمات الذين نستخدمهم في إطار معالجة بالنيابة. بالإضافة إلى المستلمين المذكورين في البنود الخاصة بهذه السياسة، يشمل ذلك على سبيل المثال مستلمي الفئات التالية: مزودو خدمات الشحن، مزودو خدمات الدفع، مزودو خدمات إدارة البضائع، مزودو خدمات معالجة الطلبات، مستضيفو الويب، مزودو خدمات تكنولوجيا المعلومات وتجار الدروبشيبينغ. في جميع الحالات، نلتزم بدقة بالمتطلبات القانونية. نطاق نقل البيانات محدود إلى الحد الأدنى.",
},
"newsletter": {
"title": "استخدام عنوان البريد الإلكتروني لإرسال النشرات الإخبارية",
"content": "نستخدم عنوان بريدك الإلكتروني بشكل مستقل عن معالجة العقد فقط لأغراضنا الإعلانية الخاصة لإرسال النشرات الإخبارية، بشرط أن تكون قد وافقت على ذلك صراحة. تتم المعالجة بناءً على المادة 6 (1) حرف أ من DSGVO بموافقتك. يمكنك سحب موافقتك في أي وقت دون التأثير على قانونية المعالجة التي تمت بناءً على الموافقة حتى السحب. يمكنك إلغاء الاشتراك في النشرة الإخبارية في أي وقت باستخدام الرابط المناسب في النشرة أو بإبلاغنا. سيتم بعد ذلك إزالة عنوان بريدك الإلكتروني من قائمة التوزيع. يتم تمرير بياناتك إلى مزود خدمة للتسويق عبر البريد الإلكتروني في إطار معالجة بالنيابة. لا يتم نقلها إلى أطراف ثالثة أخرى. سيتم نقل بياناتك إلى دولة ثالثة يوجد بشأنها قرار كفاية من المفوضية الأوروبية.",
},
"chatbot": {
"title": "استخدام روبوت الدردشة الذكي (OpenAI API)",
"content": "نستخدم روبوت دردشة مدعوم بالذكاء الاصطناعي على موقعنا الإلكتروني، يتم تشغيله عبر واجهة برمجة التطبيقات (API) لمزود الخدمة OpenAI. يهدف روبوت الدردشة إلى الرد على استفسارات الزوار بكفاءة وبشكل آلي، مما يوفر وظيفة دعم. عند استخدام روبوت الدردشة، تتم معالجة مدخلاتك بواسطة النظام لتوليد ردود مناسبة. تتم المعالجة بشكل مجهول - لا يتم جمع أو تخزين عناوين IP أو بيانات شخصية أخرى (مثل الاسم أو بيانات الاتصال).",
"legalBasis": "الأساس القانوني لاستخدام روبوت الدردشة هو مصلحتنا المشروعة وفقًا للمادة 6 (1) حرف ف من DSGVO. تكمن هذه المصلحة في توفير دعم فعال للزوار وتحسين تجربة المستخدم على موقعنا.",
"dataRecipient": "المستلم لبيانات الدردشة هو OpenAI (OpenAI OpCo, LLC) كمزود خدمة فني. تقوم OpenAI بمعالجة محتوى الدردشة المرسل على خوادمها فقط لغرض توليد الردود. تعمل OpenAI كمعالج بيانات وفقًا للمادة 28 من DSGVO ولا تستخدم البيانات لأغراضها الخاصة. لقد أبرمنا عقد معالجة بيانات مع OpenAI يتضمن بنودًا تعاقدية قياسية للاتحاد الأوروبي كضمانات مناسبة لحماية البيانات. يقع المقر الرئيسي لـ OpenAI في الولايات المتحدة الأمريكية؛ يضمن الاتفاق على البنود التعاقدية القياسية مستوى حماية بيانات مناسبًا يتوافق مع الاتحاد الأوروبي عند نقل بياناتك.",
"dataRetention": "نحتفظ بطلبات الدردشة الخاصة بك فقط طالما كان ذلك ضروريًا للمعالجة والرد. بمجرد الانتهاء من طلبك، يتم حذف أو إخفاء سجلات الدردشة بسرعة. وفقًا لما صرحت به OpenAI، يتم الاحتفاظ ببيانات الدردشة المعالجة مؤقتًا فقط ويتم حذفها تلقائيًا بعد 30 يومًا كحد أقصى.",
"voluntaryUse": "استخدام روبوت الدردشة هو أمر طوعي. إذا لم تستخدم روبوت الدردشة، فلن يتم نقل أي بيانات إلى OpenAI. يرجى عدم إدخال أي بيانات شخصية حساسة في الدردشة.",
},
"cookies": {
"title": "ملفات تعريف الارتباط (Cookies)",
"intro": "يستخدم موقعنا ملفات تعريف الارتباط في الحالات التالية:",
"payment": "1. عملية الدفع: عند الدفع ببطاقات الائتمان أو التحويلات الفورية (مثل Klarna Instant)، يتم استخدام ملفات تعريف ارتباط ضرورية تقنيًا. تحتوي هذه على سلسلة مميزة تتيح التعرف الفريد على المتصفح. يتم تعيين ملفات تعريف الارتباط بواسطة مزود خدمة الدفع Stripe وهي ضرورية تمامًا لمعالجة المدفوعات بأمان وسلاسة. بدون هذه الملفات، لا يمكن إتمام الطلب باستخدام هذه طرق الدفع. تتم المعالجة بناءً على المادة 6 (1) حرف ب من DSGVO لتنفيذ العقد.",
"googleSSO": "2. تسجيل الدخول الموحد من Google (SSO): عند استخدام تسجيل الدخول عبر Google، يتم تعيين ملفات تعريف ارتباط بواسطة Google ضرورية لعملية تسجيل الدخول والتحقق من الهوية. تتيح لك هذه الملفات تسجيل الدخول بسهولة باستخدام حساب Google الخاص بك دون الحاجة لتسجيل الدخول في كل مرة. تتم المعالجة بناءً على المادة 6 (1) حرف ب من DSGVO (تنفيذ العقد) والمادة 6 (1) حرف ف من DSGVO (مصلحة مشروعة في تسجيل دخول سهل الاستخدام).",
"otherPayments": "بالنسبة لطرق الدفع الأخرى الخصم المباشر، الاستلام أو الدفع عند الاستلام لا يتم استخدام ملفات تعريف ارتباط إضافية، ما لم تستخدم تسجيل الدخول عبر Google.",
},
"mollie": {
"title": "Mollie (معالجة الدفع)",
"content": "نستخدم مزود خدمة الدفع Mollie على موقعنا لمعالجة المدفوعات. مزود الخدمة هو Mollie B.V.، Keizersgracht 126، 1015 CW Amsterdam، هولندا. في هذا السياق، يتم نقل البيانات الشخصية اللازمة لمعالجة الدفع إلى Mollie - على وجه الخصوص اسمك، عنوان بريدك الإلكتروني، عنوان الفاتورة، معلومات الدفع (مثل بيانات بطاقة الائتمان) وعنوان IP. تتم معالجة البيانات لغرض معالجة الدفع؛ الأساس القانوني هو المادة 6 (1) حرف ب من DSGVO، لأنها تخدم تنفيذ عقد معك.",
"responsibility": "تعالج Mollie أيضًا بعض البيانات كمسؤول مستقل، على سبيل المثال للوفاء بالالتزامات القانونية (مثل مكافحة غسيل الأموال) ومنع الاحتيال. بالإضافة إلى ذلك، أبرمنا عقد معالجة بيانات مع Mollie وفقًا للمادة 28 من DSGVO؛ في إطار هذا العقد، تتصرف Mollie عند معالجة المدفوعات فقط وفقًا لتعليماتنا.",
"dataTransfer": "في حال معالجة Mollie بيانات شخصية خارج الاتحاد الأوروبي، وخاصة في الولايات المتحدة الأمريكية، يتم ذلك مع الالتزام بضمانات مناسبة. تستخدم Mollie البنود التعاقدية القياسية للاتحاد الأوروبي وفقًا للمادة 46 من DSGVO لضمان مستوى مناسب من حماية البيانات. ومع ذلك، نشير إلى أن الولايات المتحدة تُعتبر دولة ثالثة قد لا توفر مستوى حماية بيانات كافٍ بموجب قانون حماية البيانات. يمكن العثور على مزيد من المعلومات في سياسة الخصوصية الخاصة بـ Mollie على https://www.mollie.com/en/privacy.",
},
"dataRetention": {
"title": "مدة التخزين",
"content": "بعد إتمام معالجة العقد بالكامل، يتم تخزين البيانات في البداية لمدة فترة الضمان، ثم مع مراعاة الفترات القانونية، وخاصة فترات الحفظ الضريبية والتجارية، ثم يتم حذفها بعد انتهاء الفترة، ما لم تكن قد وافقت على المعالجة والاستخدام الإضافيين.",
},
"dataSubjectRights": {
"title": "حقوق صاحب البيانات",
"content": "إذا توفرت الشروط القانونية، لديك الحقوق التالية وفقًا للمادة 15 إلى 20 من DSGVO: الحق في الحصول على المعلومات، التصحيح، الحذف، تقييد المعالجة، نقل البيانات. بالإضافة إلى ذلك، وفقًا للمادة 21 (1) من DSGVO، لديك الحق في الاعتراض على المعالجة التي تستند إلى المادة 6 (1) حرف ف من DSGVO، وكذلك على المعالجة لأغراض التسويق المباشر. اتصل بنا إذا رغبت. يمكنك العثور على بيانات الاتصال في إشعارنا القانوني.",
},
"supervisoryAuthority": {
"title": "الحق في تقديم شكوى إلى السلطة الرقابية",
"content": "وفقًا للمادة 77 من DSGVO، لديك الحق في تقديم شكوى إلى السلطة الرقابية إذا كنت تعتقد أن معالجة بياناتك الشخصية غير قانونية.",
}
}
};

View File

@@ -1,25 +0,0 @@
export default {
"title": "الإشعار القانوني (Impressum)",
"sections": {
"operator": {
"title": "المشغل والمسؤول عن محتوى هذا المتجر هو:",
"content": "Growheads\nMax Schön\nTrachenberger Straße 14\n01129 Dresden"
},
"contact": {
"title": "الاتصال:",
"content": "البريد الإلكتروني: service@growheads.de"
},
"vatId": {
"title": "رقم ضريبة القيمة المضافة:",
"content": "رقم ضريبة القيمة المضافة: DE323017152"
},
"disclaimer": {
"title": "تنصل من المسؤولية:",
"content": "لا نتحمل أي مسؤولية عن محتوى عناوين الإنترنت الخارجية المرتبطة بهذه الصفحات. المشغلون المعنيون هم المسؤولون عن محتوى المواقع غير التابعة للشركة."
},
"copyright": {
"title": "بند حقوق النشر:",
"content": "المحتوى المعروض هنا يخضع بشكل عام لحقوق النشر ولا يجوز توزيعه إلا بموافقة كتابية.\nحقوق المواد المصورة أو النصية الخاصة بأطراف أخرى ليست مقيدة أو ملغاة بهذا البند."
}
}
};

View File

@@ -1,11 +0,0 @@
export default {
"title": "حق الانسحاب",
"withdrawalRight": "لديك الحق في الانسحاب من هذا العقد خلال أربعة عشر يومًا دون إبداء أي سبب. تبدأ فترة الانسحاب من اليوم الذي تستلم فيه أنت أو طرف ثالث تعينه، وليس الناقل، البضائع.",
"exerciseWithdrawal": "لممارسة حقك في الانسحاب، يجب عليك إبلاغنا",
"contactInfo": "Growheads\nTrachenberger Straße 14\n01129 Dresden\nE-Mail: service@growheads.de",
"withdrawalProcess": "ببيان واضح (مثل رسالة مرسلة بالبريد، فاكس أو بريد إلكتروني) عن قرارك بالانسحاب من هذا العقد. يمكنك استخدام نموذج الانسحاب المرفق لهذا الغرض، لكنه ليس إلزاميًا. وللحفاظ على مهلة الانسحاب، يكفي أن ترسل إشعارك بممارسة حق الانسحاب قبل انتهاء فترة الانسحاب.",
"consequencesTitle": "عواقب الانسحاب",
"consequences": "إذا انسحبت من هذا العقد، سنرد لك جميع المدفوعات التي تلقيناها منك، بما في ذلك تكاليف التوصيل (باستثناء التكاليف الإضافية الناتجة عن اختيارك لنوع توصيل غير أرخص توصيل قياسي نقدمه)، دون تأخير غير مبرر وفي موعد أقصاه أربعة عشر يومًا من اليوم الذي استلمنا فيه إشعار انسحابك من هذا العقد. سنستخدم نفس وسيلة الدفع التي استخدمتها في المعاملة الأصلية لهذا السداد، ما لم يتم الاتفاق معك صراحة على خلاف ذلك؛ ولن تُفرض عليك أي رسوم مقابل هذا السداد. قد نرفض السداد حتى نستلم البضائع مرة أخرى أو تقدم دليلاً على إرسال البضائع، أيهما أسبق. يجب عليك إعادة البضائع أو تسليمها لنا دون تأخير غير مبرر وفي كل الأحوال خلال أربعة عشر يومًا من اليوم الذي تخطرنا فيه بانسحابك من هذا العقد. تُعتبر المهلة محفوظة إذا أرسلت البضائع قبل انتهاء فترة الأربعة عشر يومًا. تتحمل أنت التكاليف المباشرة لإعادة البضائع. أنت مسؤول فقط عن أي انخفاض في قيمة البضائع ناتج عن تعامل غير ضروري لتحديد طبيعة وخصائص وعمل البضائع.",
"noWithdrawalTitle": "إشعار بعدم وجود حق الانسحاب",
"noWithdrawal": "لا ينطبق حق الانسحاب على البضائع التي تم تصنيعها أو تفصيلها حسب طلب العميل (الأفلام والأنابيب)، لكن يمكن منحه باتفاق. كما أن حاويات الأسمدة التي تم إزالة ختمها أو تدميره بفتحها مستثناة أيضًا من حق الانسحاب."
};

View File

@@ -1,3 +0,0 @@
export default {
"code": "ar-EG"
};

View File

@@ -1,9 +0,0 @@
export default {
"home": "الرئيسية",
"aktionen": "العروض",
"filiale": "الفرع",
"categories": "الفئات",
"categoriesOpen": "افتح الفئات",
"categoriesClose": "أغلق الفئات",
"otherCategories": "فئات أخرى"
};

View File

@@ -1,50 +0,0 @@
export default {
"status": {
"new": "قيد التنفيذ",
"pending": "جديد",
"processing": "قيد التنفيذ",
"cancelled": "ملغاة",
"shipped": "تم الشحن",
"delivered": "تم التوصيل",
"return": "إرجاع",
"partialReturn": "إرجاع جزئي",
"partialDelivered": "تم التوصيل جزئياً"
},
"table": {
"orderNumber": "رقم الطلب",
"date": "التاريخ",
"status": "الحالة",
"items": "العناصر",
"total": "الإجمالي",
"actions": "الإجراءات",
"viewDetails": "عرض التفاصيل"
},
"tooltips": {
"viewDetails": "عرض التفاصيل",
"cancelOrder": "إلغاء الطلب"
},
"noOrders": "لم تقم بوضع أي طلبات بعد.",
"details": {
"title": "تفاصيل الطلب: {{orderId}}",
"deliveryAddress": "عنوان التوصيل",
"invoiceAddress": "عنوان الفاتورة",
"orderDetails": "تفاصيل الطلب",
"deliveryMethod": "طريقة التوصيل:",
"paymentMethod": "طريقة الدفع:",
"notSpecified": "غير محدد",
"orderedItems": "العناصر المطلوبة",
"item": "العنصر",
"quantity": "الكمية",
"price": "السعر",
"vat": "ضريبة القيمة المضافة",
"total": "الإجمالي",
"cancelOrder": "إلغاء الطلب"
},
"cancelConfirm": {
"title": "إلغاء الطلب",
"message": "هل أنت متأكد أنك تريد إلغاء هذا الطلب؟",
"confirm": "إلغاء الطلب",
"cancelling": "جارٍ الإلغاء..."
},
"processing": "يتم إكمال الطلب...",
};

View File

@@ -1,10 +0,0 @@
export default {
"oilPress": {
"title": "استعارة معصرة زيت",
"comingSoon": "المحتوى قادم قريباً..."
},
"thcTest": {
"title": "اختبار THC",
"comingSoon": "المحتوى قادم قريباً..."
}
};

View File

@@ -1,21 +0,0 @@
export default {
"successful": "تم الدفع بنجاح!",
"failed": "فشل الدفع",
"orderCompleted": "🎉 تم إكمال طلبك بنجاح! يمكنك الآن عرض طلباتك.",
"orderProcessing": "تمت معالجة دفعتك بنجاح. سيتم إكمال الطلب تلقائيًا.",
"paymentError": "لم نتمكن من معالجة دفعتك. يرجى المحاولة مرة أخرى أو اختيار طريقة دفع أخرى.",
"viewOrders": "عرض طلباتي",
"loadingPaymentComponent": "جارٍ تحميل مكون الدفع...",
"methods": {
"selectPaymentMethod": "اختر طريقة الدفع",
"bankTransfer": "تحويل بنكي",
"bankTransferDescription": "ادفع عن طريق التحويل البنكي",
"cardPayment": "بطاقة، Sofortüberweisung، Apple Pay، Google Pay، PayPal",
"cardPaymentDescription": "ادفع بالبطاقة أو Sofortüberweisung",
"cardPaymentMinAmount": "ادفع بالبطاقة أو Sofortüberweisung (الحد الأدنى: €0.50)",
"cashOnDelivery": "الدفع عند الاستلام",
"cashOnDeliveryDescription": "ادفع عند الاستلام (رسوم إضافية €8.99)",
"cashInStore": "الدفع في المتجر",
"cashInStoreDescription": "ادفع عند الاستلام",
}
};

View File

@@ -1,47 +0,0 @@
export default {
"loading": "جارٍ تحميل المنتج...",
"notFound": "المنتج غير موجود",
"notFoundDescription": "المنتج الذي تبحث عنه غير موجود أو تم إزالته.",
"backToHome": "العودة إلى الصفحة الرئيسية",
"error": "خطأ",
"articleNumber": "رقم الصنف",
"manufacturer": "الشركة المصنعة",
"inclVat": "شامل {{vat}}% ضريبة القيمة المضافة",
"priceUnit": "{{price}}/{{unit}}",
"new": "جديد",
"weeks": "أسابيع",
"arriving": "الوصول:",
"inclVatFooter": "شامل {{vat}}% ضريبة القيمة المضافة,*",
"availability": "التوفر",
"inStock": "متوفر في المخزون",
"comingSoon": "قريبًا",
"deliveryTime": "مدة التوصيل",
"inclShort": "شامل",
"vatShort": "ضريبة القيمة المضافة",
"weight": "الوزن: {{weight}} كجم",
"youSave": "أنت توفر: {{amount}}",
"cheaperThanIndividual": "أرخص من الشراء بشكل فردي",
"pickupPrice": "سعر الاستلام: 19.90 € لكل قطعة.",
"consistsOf": "يتكون من:",
"loadingComponentDetails": "{{index}}. جارٍ تحميل تفاصيل المكون...",
"individualPriceTotal": "إجمالي السعر الفردي:",
"setPrice": "سعر المجموعة:",
"yourSavings": "توفيرك:",
"countDisplay": {
"noProducts": "0 منتجات",
"oneProduct": "منتج واحد",
"multipleProducts": "{{count}} منتجات",
"filteredProducts": "{{filtered}} من {{total}} منتجات",
"filteredOneProduct": "{{filtered}} من منتج واحد",
"xOfYProducts": "{{x}} من {{y}} منتجات"
},
"removeFiltersToSee": "قم بإزالة الفلاتر لرؤية المنتجات",
"outOfStock": "غير متوفر في المخزون",
"fromXProducts": "من {{count}} منتجات",
"discount": {
"from3Products": "من 3 منتجات",
"from5Products": "من 5 منتجات",
"from7Products": "من 7 منتجات",
"moreProductsMoreSavings": "كلما اخترت منتجات أكثر، كلما وفرت أكثر!"
}
};

View File

@@ -1,5 +0,0 @@
export default {
"placeholder": "ممكن تسألني عن أنواع الحشيش...",
"recording": "جاري التسجيل...",
"searchProducts": "ابحث عن المنتجات...",
};

View File

@@ -1,11 +0,0 @@
export default {
"seeds": "بذور",
"stecklinge": "قصاصات",
"oilPress": "استعارة معصرة زيت",
"thcTest": "اختبار THC",
"address1": "Trachenberger Straße 14",
"address2": "01129 Dresden",
"showUsPhoto": "ورينا أجمل صورة عندك",
"selectSeedRate": "اختار البذرة واضغط تقييم",
"indoorSeason": "موسم الزراعة الداخلية بدأ"
};

View File

@@ -1,34 +0,0 @@
export default {
"changePassword": "تغيير كلمة المرور",
"currentPassword": "كلمة المرور الحالية",
"newPassword": "كلمة المرور الجديدة",
"confirmNewPassword": "تأكيد كلمة المرور الجديدة",
"updatePassword": "تحديث كلمة المرور",
"changeEmail": "تغيير عنوان البريد الإلكتروني",
"password": "كلمة المرور",
"newEmail": "عنوان البريد الإلكتروني الجديد",
"updateEmail": "تحديث البريد الإلكتروني",
"apiKey": "مفتاح API",
"apiKeyDescription": "استخدم مفتاح API الخاص بك للتكامل مع التطبيقات الخارجية.",
"apiDocumentation": "توثيق API:",
"copyToClipboard": "نسخ إلى الحافظة",
"generate": "إنشاء",
"regenerate": "إعادة إنشاء",
"apiKeyCopied": "تم نسخ مفتاح API إلى الحافظة",
"errors": {
"fillAllFields": "يرجى ملء جميع الحقول",
"passwordsNotMatch": "كلمات المرور الجديدة غير متطابقة",
"passwordTooShort": "يجب أن تكون كلمة المرور الجديدة 8 أحرف على الأقل",
"passwordUpdateError": "حدث خطأ أثناء تحديث كلمة المرور",
"invalidEmail": "يرجى إدخال عنوان بريد إلكتروني صالح",
"emailUpdateError": "حدث خطأ أثناء تحديث عنوان البريد الإلكتروني",
"userNotFound": "المستخدم غير موجود",
"apiKeyGenerationError": "حدث خطأ أثناء إنشاء مفتاح API"
},
"success": {
"passwordUpdated": "تم تحديث كلمة المرور بنجاح",
"emailUpdated": "تم تحديث عنوان البريد الإلكتروني بنجاح",
"apiKeyGenerated": "تم إنشاء مفتاح API بنجاح",
"apiKeyWarning": "احفظ هذا المفتاح بأمان. لأسباب أمنية، سيتم إخفاؤه خلال 10 ثوانٍ."
}
};

View File

@@ -1,6 +0,0 @@
export default {
"name": "الاسم",
"searchField": "كلمة البحث",
"priceLowHigh": "السعر: من الأقل للأعلى",
"priceHighLow": "السعر: من الأعلى للأقل"
};

View File

@@ -1,12 +0,0 @@
export default {
"vat": "ضريبة القيمة المضافة",
"vat7": "ضريبة القيمة المضافة 7%",
"vat19": "ضريبة القيمة المضافة 19%",
"vat19WithShipping": "ضريبة القيمة المضافة 19% (شاملة الشحن)",
"totalNet": "إجمالي السعر الصافي",
"totalGross": "إجمالي السعر الإجمالي بدون الشحن",
"subtotal": "المجموع الفرعي",
"incl7Vat": "شاملة ضريبة القيمة المضافة 7%",
"inclVatWithFooter": "(شاملة {{vat}}% ضريبة القيمة المضافة،*)",
"inclVatAmount": "شاملة {{amount}} € ضريبة القيمة المضافة ({{rate}}%)"
};

View File

@@ -1,5 +0,0 @@
export default {
"home": "بذور وقصاصات القنب الممتازة",
"aktionen": "العروض والتخفيضات الحالية",
"filiale": "متجرنا في دريسدن",
};

View File

@@ -1,3 +0,0 @@
import translations from './index.js';
export default translations;

View File

@@ -1,25 +0,0 @@
export default {
"login": "Вход",
"register": "Регистрация",
"logout": "Изход",
"profile": "Профил",
"email": "Имейл",
"password": "Парола",
"confirmPassword": "Потвърдете паролата",
"forgotPassword": "Забравена парола?",
"loginWithGoogle": "Вход с Google",
"or": "ИЛИ",
"privacyAccept": "С натискане на \"Вход с Google\" приемам",
"privacyPolicy": "Политиката за поверителност",
"passwordMinLength": "Паролата трябва да е поне 8 символа",
"newPasswordMinLength": "Новата парола трябва да е поне 8 символа",
"menu": {
"profile": "Профил",
"myProfile": "Моят профил",
"checkout": "Плащане",
"orders": "Поръчки",
"settings": "Настройки",
"adminDashboard": "Админ табло",
"adminUsers": "Админ потребители"
}
};

View File

@@ -1,39 +0,0 @@
export default {
"title": "Количка",
"empty": "празна",
"addToCart": "Добави в количката",
"preorderCutting": "Предварителна поръчка като резник",
"continueShopping": "Продължи пазаруването",
"proceedToCheckout": "Продължи към плащане",
"productCount": "{{count}} {{count, plural, one {продукт} other {продукта}}}",
"productSingular": "продукт",
"productPlural": "продукта",
"removeFromCart": "Премахни от количката",
"openCart": "Отвори количката",
"availableFrom": "Наличен от {{date}}",
"backToOrder": "← Обратно към поръчката",
"summary": {
"title": "Обобщение на поръчката",
"goodsNet": "Стоки (нето):",
"shippingNet": "Доставка (нето):",
"totalGoods": "Общо стоки:",
"shippingCosts": "Разходи за доставка:",
"total": "Общо:",
"totalWeight": "Общо тегло: {{weight}} кг",
"freeFrom100": "(безплатно над 100€)",
"free": "безплатно"
},
"itemCount": {
"singular": "продукт",
"plural": "продукта"
},
"sync": {
"title": "Синхронизация на количката",
"description": "Имате запазена количка в профила си. Моля, изберете как искате да продължите:",
"deleteServer": "Изтрий количката на сървъра",
"useServer": "Използвай количката от сървъра",
"merge": "Обедини количките",
"currentCart": "Вашата текуща количка",
"serverCart": "Количка, запазена във вашия профил"
}
};

View File

@@ -1,3 +0,0 @@
export default {
"privacyRead": "Прочетено и прието",
};

View File

@@ -1,34 +0,0 @@
export default {
"invoiceAddress": "Адрес за фактура",
"deliveryAddress": "Адрес за доставка",
"saveForFuture": "Запази за бъдещи поръчки",
"pickupDate": "За коя дата е желано вземането на резниците?",
"note": "Бележка",
"sameAddress": "Адресът за доставка е същият като адреса за фактура",
"termsAccept": "Прочетох Общите условия, Политиката за поверителност и разпоредбите за правото на отказ",
"selectDeliveryMethod": "Изберете метод на доставка",
"selectPaymentMethod": "Изберете метод на плащане",
"orderSummary": "Обобщение на поръчката",
"addressValidationError": "Моля, проверете въведените данни в полетата за адрес.",
"processingOrder": "Поръчката се обработва...",
"completeOrder": "Завърши поръчката",
"termsValidationError": "Моля, приемете Общите условия, Политиката за поверителност и правото на отказ, за да продължите.",
"addressFields": {
"firstName": "Име",
"lastName": "Фамилия",
"addressSupplement": "Допълнение към адреса",
"street": "Улица",
"houseNumber": "Номер на къща",
"postalCode": "Пощенски код",
"city": "Град",
"country": "Държава"
},
"validationErrors": {
"firstNameRequired": "Името е задължително",
"lastNameRequired": "Фамилията е задължителна",
"streetRequired": "Улицата е задължителна",
"houseNumberRequired": "Номерът на къщата е задължителен",
"postalCodeRequired": "Пощенският код е задължителен",
"cityRequired": "Градът е задължителен"
}
};

View File

@@ -1,19 +0,0 @@
export default {
"loading": "Зареждане...",
"error": "Грешка",
"close": "Затвори",
"save": "Запази",
"cancel": "Отказ",
"ok": "OK",
"yes": "Да",
"no": "Не",
"next": "Напред",
"back": "Назад",
"edit": "Редактирай",
"delete": "Изтрий",
"add": "Добави",
"remove": "Премахни",
"products": "Продукти",
"product": "Продукт",
"days": "Дни"
};

View File

@@ -1,35 +0,0 @@
export default {
"methods": {
"dhl": "DHL",
"dpd": "DPD",
"sperrgut": "Обемни стоки",
"sperrgutName": "Обемни стоки",
"pickup": "Вземане от магазина"
},
"descriptions": {
"standard": "Стандартна доставка",
"standardFree": "Стандартна доставка - БЕЗПЛАТНО при поръчка над 100€!",
"notAvailable": "Не може да се избере, защото един или повече артикули могат да се вземат само на място",
"bulky": "За големи и тежки артикули",
"pickupOnly": "Само вземане на място"
},
"prices": {
"free": "безплатно",
"freeFrom100": "(безплатно от 100€)",
"dhl": "6.99 €",
"dpd": "4.90 €",
"sperrgut": "28.99 €"
},
"times": {
"cutting14Days": "Срок за доставка: 14 дни",
"standard2to3Days": "Срок за доставка: 2-3 дни",
"supplier7to9Days": "Срок за доставка: 7-9 дни"
},
"selector": {
"title": "Изберете метод на доставка",
"freeShippingInfo": "💡 Безплатна доставка при поръчка над 100€!",
"remainingForFree": "Добавете още {{amount}}€ за безплатна доставка.",
"congratsFreeShipping": "🎉 Поздравления! Вие получавате безплатна доставка!",
"cartQualifiesFree": "Вашата количка на стойност {{amount}}€ се квалифицира за безплатна доставка."
}
};

View File

@@ -1,7 +0,0 @@
export default {
"sorting": "Сортиране",
"perPage": "на страница",
"availability": "Наличност",
"manufacturer": "Производител",
"all": "Всички"
};

View File

@@ -1,15 +0,0 @@
export default {
"hours": "Съб 11:00-19:00",
"address": "Trachenberger Straße 14 - Dresden",
"location": "Между спирка Pieschen и Trachenberger Platz",
"allPricesIncl": "* Всички цени включват законен ДДС, плюс доставка",
"copyright": "© {{year}} GrowHeads.de",
"legal": {
"datenschutz": "Политика за поверителност",
"agb": "Общи условия",
"sitemap": "Карта на сайта",
"impressum": "Правно известие",
"batteriegesetzhinweise": "Информация за закона за батериите",
"widerrufsrecht": "Право на отказ"
}
};

View File

@@ -1,43 +0,0 @@
import locale from './locale.js';
import navigation from './navigation.js';
import auth from './auth.js';
import cart from './cart.js';
import product from './product.js';
import search from './search.js';
import sorting from './sorting.js';
import chat from './chat.js';
import delivery from './delivery.js';
import checkout from './checkout.js';
import payment from './payment.js';
import filters from './filters.js';
import tax from './tax.js';
import footer from './footer.js';
import titles from './titles.js';
import sections from './sections.js';
import pages from './pages.js';
import orders from './orders.js';
import settings from './settings.js';
import common from './common.js';
export default {
"locale": locale,
"navigation": navigation,
"auth": auth,
"cart": cart,
"product": product,
"search": search,
"sorting": sorting,
"chat": chat,
"delivery": delivery,
"checkout": checkout,
"payment": payment,
"filters": filters,
"tax": tax,
"footer": footer,
"titles": titles,
"sections": sections,
"pages": pages,
"orders": orders,
"settings": settings,
"common": common
};

View File

@@ -1,69 +0,0 @@
export default {
"title": "Общи условия",
"deliveryShippingConditions": "Условия за доставка и изпращане",
"deliveryTerms": {
"1": "Доставката отнема между 1 и 7 дни.",
"2": "Стоките остават собственост на Growheads до пълното им заплащане.",
"3": "Ако се подозира, че стоките са повредени по време на транспорта или липсват артикули, опаковката за изпращане трябва да се запази за преглед от експерт. Всякакви повреди на опаковката трябва да бъдат потвърдени от превозвача в товарителницата, като се посочи видът и обхватът. Повредите при транспортиране трябва незабавно да бъдат съобщени на Growheads писмено по факс, имейл или поща. За това трябва да се направят снимки на повредените стоки, както и на повредената опаковка за изпращане с етикета с адреса. Повредената опаковка за изпращане също трябва да се запази. Те са необходими за предявяване на претенция към транспортната фирма.",
"4": "При връщане на дефектни стоки клиентът трябва да се увери, че стоките са правилно опаковани.",
"5": "Всички връщания трябва да бъдат предварително регистрирани в Growheads.",
"6": "Рискът при изпращане на артикули към нас се носи от клиента, освен ако не става въпрос за връщане на дефектни стоки.",
"7": "Growheads има право да организира вземането на стоките от Deutsche Post/GLS или превозвач по свой избор.",
"8": "Разходите за пощенски услуги се изчисляват според теглото. Growheads си запазва правото да прехвърля евентуални увеличения на цените от транспортните компании (такси, горивни надбавки).",
"9": "Нашите пратки обикновено се изпращат с: GLS, DHL & Deutsche Post AG.",
"10": "За особено тежки или обемисти артикули си запазваме правото да начисляваме допълнителни такси за доставка. Тези такси обикновено са посочени в ценовия лист.",
"11": "Плащането може да се извърши предварително чрез банков превод по посочената банкова сметка.",
"12": "Ако има забавяне на доставката, за което ние носим отговорност, срокът за предоставяне на допълнителен срок, който купувачът има право да определи, е ограничен до две седмици. Срокът започва с получаването на уведомлението за допълнителния срок от Growheads.",
"13": "Очевидни дефекти на стоките трябва да бъдат съобщени писмено незабавно след доставката. Ако клиентът не го направи, претенции по гаранцията за очевидни дефекти са изключени.",
"14": "Ако клиентът подаде рекламация за дефект, той трябва да върне дефектните стоки при нас с възможно най-точно описание на дефекта. Копие от нашата фактура трябва да бъде приложено към пратката. Стоките трябва да бъдат върнати в оригиналната опаковка или в опаковка, която защитава стоките по същия начин като оригиналната, за да се избегнат повреди по време на връщането."
},
"consultationLiability": {
"title": "Консултации и отговорност",
"1": "Ние предоставяме технически съвети за приложение според най-добрите ни знания, базирани на нашия опит и експертиза.",
"2": "Купувачът носи отговорност за спазването на законовите разпоредби относно съхранението, по-нататъшния транспорт и използването на нашите стоки."
},
"paymentConditions": {
"title": "Условия за плащане",
"1": "Стоките остават собственост на Growheads до пълното им заплащане.",
"2": "Фактурите се заплащат предварително чрез банков превод по нашата сметка. Ако платите предварително, стоките ще бъдат изпратени веднага след като сумата бъде кредитирана по нашата сметка."
},
"retentionOfTitle": {
"title": "Запазване на собствеността",
"content": "Доставените стоки остават собственост на Growheads, докато купувачът не уреди всички свои задължения към нас. Ако продавачът препродаде стоките, той с настоящото прехвърля на нас вземанията, произтичащи от продажбата. Ако купувачът закъснее с плащанията си, ние можем по всяко време да изискаме връщането на стоките без да се отказваме от договора."
},
"distanceSelling": {
"title": "Информация съгласно Закона за дистанционна търговия",
"intro": "Следната информация важи само за договори, сключени между Growheads и потребители чрез поръчка по каталог, интернет поръчка или други средства за дистанционна комуникация. Тя е ограничена до потребители в рамките на ЕС.",
"sections": {
"1": {
"title": "Съществени характеристики на стоките",
"content": "Моля, вижте обясненията в каталога или на нашия уебсайт за съществените характеристики на стоките. Офертите в нашия каталог и на уебсайта са без задължение. Поръчките към нас се считат за обвързващи оферти. Growheads може да ги приеме в срок от 14 дни след получаване на поръчката чрез изпращане на потвърждение на поръчката или чрез изпращане на стоките."
},
"2": {
"title": "Резервация",
"content": "Ако не всички поръчани артикули са налични за доставка, си запазваме правото да извършваме частични доставки, ако това е разумно за клиента. Отделни артикули могат да се различават от илюстрациите и описанията в каталога и на уебсайта. Това важи особено за стоки, изработени ръчно. Затова си запазваме правото да доставяме стоки с равностойно качество и цена, ако е необходимо."
},
"3": {
"title": "Цени и данъци",
"content": "Цените на отделните артикули с включен ДДС можете да намерите в каталога или на нашия уебсайт. Цените губят валидност с публикуването на нов каталог."
},
"4": "Всички цени са с резервация за грешки или колебания в цените. Ако има промяна в цената, купувачът може да упражни правото си на връщане.",
"5": {
"title": "Гаранционен срок",
"content": "Приложим е законовият гаранционен срок от 24 (двадесет и четири) месеца. В отделни случаи могат да важат по-дълги срокове, ако са предоставени от производителя."
},
"6": {
"title": "Право на връщане / Право на отказ",
"content": "Клиентът има 14-дневно право на връщане.\nСрокът започва с получаването на стоките от клиента и се спазва чрез навременното изпращане на отказа до Growheads. Изключени от това са храни и други бързоразвалящи се стоки, както и поръчки по индивидуален дизайн или стоки, специално поръчани по желание на клиента. Връщането трябва да се извърши чрез изпращане на стоките обратно в срока. Ако стоките не могат да бъдат изпратени, в срока трябва да бъде изпратено искане за връщане до нас по писмо, пощенска картичка, имейл или друг траен носител на данни. За спазване на срока е достатъчно навременното изпращане до посочения под точка 7) адрес на фирмата. Отказът не изисква посочване на причина. Цената на покупката и евентуалните разходи за доставка и изпращане ще бъдат възстановени след получаване на стоките от нас. Решаваща е стойността на върнатите стоки към момента на покупката, а не стойността на цялата поръчка. Growheads обикновено може да организира вземането от вас."
},
"7": {
"title": "Име и адрес на фирмата, рекламации, призовки",
"content": "Growheads\nTrachenberger Straße 14\n01129 Dresden"
},
"8": {
"title": "Място на изпълнение и подсъдност",
"content": "Мястото на изпълнение и подсъдността за всички претенции е Дрезден, освен ако задължителни законови разпоредби не предвиждат друго."
}
}
}
};

View File

@@ -1,8 +0,0 @@
export default {
"title": "Информация за Закона за батериите",
"intro": "Във връзка с продажбата на батерии или доставката на устройства, съдържащи батерии, ние сме задължени да ви информираме за следното:",
"returnObligation": "Като краен потребител сте законово задължени да връщате използвани батерии. Можете да върнете стари батерии, които ние имаме или сме имали в продуктовия си асортимент като нови батерии, безплатно в нашия склад за изпращане (адрес за доставка).",
"symbolsInfo": "Символите, показани на батериите, имат следното значение:",
"wasteSymbol": "Символът на пресечената кофа за отпадъци означава, че батерията не трябва да се изхвърля с битовите отпадъци.",
"chemicalSymbols": "Pb = Батерията съдържа повече от 0,004 масови процента олово\nCd = Батерията съдържа повече от 0,002 масови процента кадмий\nHg = Батерията съдържа повече от 0,0005 масови процента живак."
};

View File

@@ -1,58 +0,0 @@
export default {
"title": "Политика за поверителност",
"responsibleParty": {
"title": "Отговорно лице по смисъла на закона за защита на данните:",
"company": "Growheads\nTrachenberger Straße 14\n01129 Dresden"
},
"generalInfo": "Освен ако по-долу не е посочено друго, предоставянето на вашите лични данни не е нито законово, нито договорно изискване, нито е необходимо за сключване на договор. Не сте задължени да предоставяте данните. Непредоставянето им няма последствия. Това важи само докато при следващите обработки не е посочено друго. „Лични данни“ означава всяка информация, отнасяща се до идентифицирано или идентифицируемо физическо лице.",
"sections": {
"informationDeletion": {
"title": "Информация, изтриване, блокиране",
"content": "По всяко време можете да поискате информация за вашите лични данни, техния произход и получатели, както и целта на обработката на данните, и можете безплатно да поискате корекция, блокиране или изтриване на тези данни. Моля, използвайте посочените в долния колонтитул на страницата или в правния импресум контакти за тази цел. Ние сме на разположение по всяко време за допълнителни въпроси по темата. Моля, имайте предвид, че не сме упълномощени и няма да изтриваме данни за фактури, банкови данни и данни, изпратени до доставчик на куриерски услуги. Данни, които могат да бъдат изтрити, включват: клиентски акаунти на уеб сървъра, както и в системата за управление на стоките, и имейли, които не са пряко свързани с поръчка."
},
"serverLogfiles": {
"title": "Сървърни лог файлове",
"content": "Можете да посещавате нашите уебсайтове без да предоставяте информация за себе си. При всяко посещение на нашия уебсайт, данни за използването се предават от вашия интернет браузър и се съхраняват в протоколни данни (сървърни лог файлове). Тези съхранени данни включват например името на посетената страница, дата и час на достъп, количество прехвърлени данни и доставчика, който прави заявката. Тези данни се използват изключително за осигуряване на безпроблемната работа на нашия уебсайт и за подобряване на нашето предложение. Тези данни не са лични данни. Не се извършва обединяване на тези данни с други източници на данни. Ако станем наясно с конкретни индикации за неправомерна употреба, си запазваме правото да проверим тези данни впоследствие."
},
"customerAccount": {
"title": "Клиентски акаунт",
"content": "При откриване на клиентски акаунт събираме вашите лични данни в посочения там обем. Обработката на данните служи за подобряване на вашето пазаруване и улесняване на обработката на поръчките. Обработката се извършва въз основа на чл. 6 (1) буква а DSGVO с вашето съгласие. Можете да оттеглите съгласието си по всяко време чрез уведомяване до нас, без да се засяга законосъобразността на обработката, извършена въз основа на съгласието до withdrawing. Вашият клиентски акаунт ще бъде изтрит след това."
},
"googleSSO": {
"title": "Вход с Google (Google Single Sign-On)",
"content": "Предлагаме ви възможността да влезете в клиентския си акаунт с вашия Google акаунт. Когато използвате функцията „Вход с Google“, удостоверяването се извършва чрез услугата Google Single Sign-On. В този процес на вашето устройство могат да се съхраняват бисквитки от Google, които са необходими за процеса на вход и удостоверяване. В рамките на Google входа получаваме от Google определени лични данни за проверка на вашата самоличност. По-специално, Google ни предава вашето име, вашия имейл адрес и ако е съхранена във вашия Google акаунт вашата профилна снимка. Тази информация се предоставя от Google веднага щом влезете в нашия онлайн магазин с вашия Google акаунт. Google, като трета страна, може да има достъп до тези данни и да ги обработва; това може да включва и прехвърляне на данни в САЩ. Сключили сме стандартни клаузи за защита на данните с Google съгласно чл. 46 (2) буква c DSGVO, за да осигурим адекватно ниво на защита на данните при прехвърлянето на вашите данни. Допълнителни подробности за обработката на данни от Google можете да намерите в Политиката за поверителност на Google (на https://policies.google.com/privacy?hl=en).",
"legalBasis": "Обработката на данни във връзка с Google входа се основава на чл. 6 (1) буква b DSGVO (изпълнение на предварителни договорни мерки и изпълнение на договор, напр. за създаване и използване на вашия клиентски акаунт) както и чл. 6 (1) буква f DSGVO (нашият легитимен интерес да ви предоставим бърза и удобна възможност за вход).",
"voluntaryUse": "Използването на функцията „Вход с Google“ е доброволно. Разбира се, можете да използвате нашия онлайн магазин и вашия клиентски акаунт и без Google SSO, като се регистрирате или влезете с вашия имейл адрес и парола както обикновено. Ако изберете да използвате Google вход, можете по всяко време да прекъснете тази връзка, като премахнете свързването в настройките на вашия Google акаунт.",
"yourRights": "Относно личните данни, обработвани чрез Google SSO, имате законните права на субекти на данни. По-специално имате право да получите информация за съхраняваните за вас данни (чл. 15 DSGVO), да коригирате неточни данни (чл. 16 DSGVO) или да поискате изтриване на вашите данни (чл. 17 DSGVO). Освен това имате право да ограничите обработката на вашите данни (чл. 18 DSGVO) и право на преносимост на данните (чл. 20 DSGVO). Ако основаваме обработката на нашия легитимен интерес, можете да възразите срещу обработката (чл. 21 DSGVO). Освен това можете по всяко време да се обърнете с жалба към компетентния надзорен орган за защита на данните. Вашите вече съществуващи права и възможности за избор от останалата част на политиката за поверителност важат също и за използването на Google вход."
},
"orders": {
"title": "Събиране, обработка и използване на лични данни при поръчки",
"content": "При подаване на поръчка събираме и използваме вашите лични данни само в обема, необходим за изпълнение и обработка на вашата поръчка и за обработка на вашите запитвания. Предоставянето на данните е необходимо за сключване на договора. Непредоставянето им води до невъзможност за сключване на договор. Обработката се основава на чл. 6 (1) буква b DSGVO и е необходима за изпълнение на договор с вас. Вашите данни няма да бъдат предавани на трети страни без вашето изрично съгласие. Единствените изключения са нашите партньори по услуги, които са необходими за обработка на договорните отношения, или доставчици, които използваме в рамките на възложена обработка. Освен получателите, посочени в съответните клаузи на тази политика за поверителност, това включва например получатели от следните категории: доставчици на куриерски услуги, доставчици на платежни услуги, доставчици на системи за управление на стоките, доставчици на услуги за обработка на поръчки, уеб хостинг доставчици, IT доставчици и дропшипинг търговци. Във всички случаи строго спазваме законовите изисквания. Обемът на предаваните данни е ограничен до минимум."
},
"newsletter": {
"title": "Използване на имейл адрес за изпращане на бюлетини",
"content": "Използваме вашия имейл адрес независимо от обработката на договора изключително за наши собствени рекламни цели за изпращане на бюлетини, при условие че сте дали изричното си съгласие за това. Обработката се извършва въз основа на чл. 6 (1) буква а DSGVO с вашето съгласие. Можете да оттеглите съгласието си по всяко време, без да се засяга законосъобразността на обработката, извършена въз основа на съгласието до withdrawing. Можете да се отпишете от бюлетина по всяко време, като използвате съответната връзка в бюлетина или чрез уведомяване до нас. Вашият имейл адрес ще бъде премахнат от списъка за разпространение. Вашите данни ще бъдат предадени на доставчик на услуги за имейл маркетинг в рамките на възложена обработка. Не се извършва предаване на други трети страни. Вашите данни ще бъдат предадени в трета страна, за която има решение за адекватност от Европейската комисия."
},
"chatbot": {
"title": "Използване на AI чатбот (OpenAI API)",
"content": "Използваме AI-базиран чатбот на нашия уебсайт, който се управлява чрез програмния интерфейс (API) на доставчика OpenAI. Чатботът служи за ефективно и автоматизирано отговаряне на запитвания от посетители, като по този начин предоставя функция за поддръжка. Когато използвате чатбота, вашите въвеждания се обработват от системата за генериране на подходящи отговори. Обработката е анонимизирана не се събират или съхраняват IP адреси или други лични данни (като име или контактни данни).",
"legalBasis": "Правото основание за използването на чатбота е нашият легитимен интерес съгласно чл. 6 (1) буква f DSGVO. Този интерес се състои в предоставянето на ефективна поддръжка на посетителите и подобряване на потребителското изживяване на нашия уебсайт.",
"dataRecipient": "Получател на чат данните е OpenAI (OpenAI OpCo, LLC) като технически доставчик на услуги. OpenAI обработва предаденото чат съдържание на своите сървъри изключително с цел генериране на отговори. OpenAI действа като обработващ данни по смисъла на чл. 28 DSGVO и не използва данните за свои цели. Сключили сме договор за обработка на данни с OpenAI, който включва стандартните договорни клаузи на ЕС като подходящи гаранции за защита на данните. OpenAI има седалище в САЩ; чрез споразумението за стандартните договорни клаузи се гарантира, че при прехвърлянето на вашите данни се осигурява ниво на защита на данните, съответстващо на Европейския съюз.",
"dataRetention": "Съхраняваме вашите чат заявки само толкова дълго, колкото е необходимо за обработка и отговор. След като вашето запитване бъде приключено, чат историите се изтриват или анонимизират своевременно. Според собствените си изявления OpenAI съхранява обработените чат данни само временно и ги изтрива автоматично най-късно след 30 дни.",
"voluntaryUse": "Използването на чатбота е доброволно. Ако не използвате чатбота, не се извършва предаване на данни към OpenAI. Моля, не въвеждайте чувствителни лични данни в чата."
},
"cookies": {
"title": "Бисквитки",
"intro": "Нашият уебсайт използва бисквитки в следните случаи:",
"payment": "1. Процес на плащане: При плащания с кредитна карта или моментални преводи (напр. Klarna Instant) се използват технически необходими бисквитки. Те съдържат характерна низова стойност, която позволява уникална идентификация на браузъра. Бисквитките се задават от платежния доставчик Stripe и са абсолютно необходими за сигурното и безпроблемно обработване на плащанията. Без тези бисквитки поръчка с тези методи на плащане не е възможна. Обработката се извършва въз основа на чл. 6 (1) буква b DSGVO за изпълнение на договор.",
"googleSSO": "2. Google Single Sign-On (SSO): При използване на Google вход се задават бисквитки от Google, които са необходими за процеса на вход и удостоверяване. Тези бисквитки ви позволяват удобно да влизате с вашия Google акаунт, без да се налага да въвеждате отново данните си всеки път. Обработката се извършва въз основа на чл. 6 (1) буква b DSGVO (изпълнение на договор) и чл. 6 (1) буква f DSGVO (легитимен интерес за удобен вход).",
"otherPayments": "За други методи на плащане директен дебит, вземане на място или наложен платеж не се използват допълнителни бисквитки, освен ако не използвате Google вход."
},
"mollie": {
"title": "Mollie (Обработка на плащания)",
"content": "Използваме платежния доставчик Mollie на нашия уебсайт за обработка на плащания. Доставчик на услугата е Mollie B.V., Keizersgracht 126, 1015 CW Amsterdam, Нидерландия. В този контекст се предават на Mollie лични данни, необходими за обработка на плащания по-специално вашето име, имейл адрес, адрес за фактуриране, платежна информация (напр. данни за кредитна карта) и IP адрес. Обработката на данните се извършва с цел обработка на плащания; правното основание е чл. 6 (1) буква b DSGVO, тъй като служи за изпълнение на договор с вас.",
"responsibility": "Mollie също така обработва определени данни като самостоятелен администратор, например за изпълнение на законови задължения (като предотвратяване на пране на пари) и предотвратяване на измами. Освен това сме сключили договор за обработка на данни с Mollie съгласно чл. 28 DSGVO; в рамките на това споразумение Mollie действа при обработката на плащания изключително по наши инструкции.",
"dataTransfer": "В случай че Mollie обработва лични данни извън ЕС, особено в САЩ, това се извършва при спазване на подходящи гаранции. Mollie използва стандартните договорни клаузи на ЕС съгласно чл. 46 DSGVO, за да осигури адекватно ниво на защита на данните. Въпреки това, ние посочваме, че САЩ се считат за трета страна с потенциално недостатъчно ниво на защита на данните по смисъла на законодателството за защита на данните. Допълнителна информация можете да намерите в политиката за поверителност на Mollie на https://www.mollie.com/en/privacy."
}
}
};

View File

@@ -1,25 +0,0 @@
export default {
"title": "Правно известие (Impressum)",
"sections": {
"operator": {
"title": "Оператор и отговорен за съдържанието на този магазин е:",
"content": "Growheads\nMax Schön\nTrachenberger Straße 14\n01129 Dresden"
},
"contact": {
"title": "Контакт:",
"content": "E-mail: service@growheads.de"
},
"vatId": {
"title": "ДДС номер:",
"content": "VAT ID No.: DE323017152"
},
"disclaimer": {
"title": "Отказ от отговорност:",
"content": "Не поемаме отговорност за съдържанието на външни интернет адреси, свързани на тези страници. Съответните оператори са отговорни за съдържанието на домейни, които не са част от компанията."
},
"copyright": {
"title": "Клауза за авторски права:",
"content": "Представеното тук съдържание е общо взето защитено с авторски права и може да бъде разпространявано само с писмено разрешение.\nПравата върху фото- или текстов материал от други страни не са ограничени или отменени с тази клауза."
}
}
};

Some files were not shown because too many files have changed in this diff Show More