Compare commits
1 Commits
8bc80c872d
...
mollie
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ccceb8fe78 |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -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
21
.vscode/launch.json
vendored
@@ -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}"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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
1710
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
11
package.json
11
package.json
@@ -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"
|
||||
|
||||
@@ -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}`;
|
||||
|
||||
@@ -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.
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 |
@@ -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">
|
||||
|
||||
182
src/App.js
182
src/App.js
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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/>
|
||||
|
||||
@@ -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)
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 };
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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
381
src/components/Mollie.js
Normal 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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
|
||||
@@ -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" },
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
@@ -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";
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
191
src/config.js
191
src/config.js
@@ -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: {
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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": "مستخدمو المسؤول"
|
||||
}
|
||||
};
|
||||
@@ -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": "العربة المحفوظة في ملفك الشخصي"
|
||||
}
|
||||
};
|
||||
@@ -1,3 +0,0 @@
|
||||
export default {
|
||||
"privacyRead": "تم القراءة والموافقة",
|
||||
};
|
||||
@@ -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": "المدينة مطلوبة"
|
||||
}
|
||||
};
|
||||
@@ -1,19 +0,0 @@
|
||||
export default {
|
||||
"loading": "جارٍ التحميل...",
|
||||
"error": "خطأ",
|
||||
"close": "إغلاق",
|
||||
"save": "حفظ",
|
||||
"cancel": "إلغاء",
|
||||
"ok": "موافق",
|
||||
"yes": "نعم",
|
||||
"no": "لا",
|
||||
"next": "التالي",
|
||||
"back": "رجوع",
|
||||
"edit": "تعديل",
|
||||
"delete": "حذف",
|
||||
"add": "إضافة",
|
||||
"remove": "إزالة",
|
||||
"products": "منتجات",
|
||||
"product": "منتج",
|
||||
"days": "أيام"
|
||||
};
|
||||
@@ -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}}€ مؤهلة للشحن المجاني."
|
||||
}
|
||||
};
|
||||
@@ -1,7 +0,0 @@
|
||||
export default {
|
||||
"sorting": "الترتيب",
|
||||
"perPage": "لكل صفحة",
|
||||
"availability": "التوفر",
|
||||
"manufacturer": "الشركة المصنعة",
|
||||
"all": "الكل"
|
||||
};
|
||||
@@ -1,15 +0,0 @@
|
||||
export default {
|
||||
"hours": "السبت 11 صباحًا - 7 مساءً",
|
||||
"address": "شارع تراشنبرجر 14 - دريسدن",
|
||||
"location": "بين محطة بيسشن وميدان تراشنبرجر",
|
||||
"allPricesIncl": "* جميع الأسعار تشمل ضريبة القيمة المضافة القانونية، بالإضافة إلى الشحن",
|
||||
"copyright": "© {{year}} GrowHeads.de",
|
||||
"legal": {
|
||||
"datenschutz": "سياسة الخصوصية",
|
||||
"agb": "الشروط والأحكام",
|
||||
"sitemap": "خريطة الموقع",
|
||||
"impressum": "الإشعار القانوني",
|
||||
"batteriegesetzhinweise": "معلومات قانون البطاريات",
|
||||
"widerrufsrecht": "حق الانسحاب"
|
||||
}
|
||||
};
|
||||
@@ -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
|
||||
};
|
||||
@@ -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": "مكان التنفيذ والاختصاص القضائي لجميع المطالبات هو دريسدن، ما لم تنص أحكام قانونية إلزامية على خلاف ذلك."
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -1,8 +0,0 @@
|
||||
export default {
|
||||
"title": "معلومات قانون البطاريات",
|
||||
"intro": "فيما يتعلق ببيع البطاريات أو تسليم الأجهزة التي تحتوي على بطاريات، نحن ملزمون بإبلاغكم بما يلي:",
|
||||
"returnObligation": "بصفتك مستخدم نهائي، أنت ملزم قانونيًا بإعادة البطاريات المستخدمة. يمكنك إعادة البطاريات القديمة التي نمتلكها أو التي كانت ضمن مجموعتنا كبطاريات جديدة مجانًا إلى مستودع الشحن الخاص بنا (عنوان الشحن).",
|
||||
"symbolsInfo": "الرموز المعروضة على البطاريات تعني ما يلي:",
|
||||
"wasteSymbol": "رمز سلة المهملات المعلمة بعلامة إلغاء يعني أنه لا يجوز التخلص من البطارية مع النفايات المنزلية.",
|
||||
"chemicalSymbols": "Pb = البطارية تحتوي على أكثر من 0.004 بالمئة بالوزن من الرصاص\nCd = البطارية تحتوي على أكثر من 0.002 بالمئة بالوزن من الكادميوم\nHg = البطارية تحتوي على أكثر من 0.0005 بالمئة بالوزن من الزئبق."
|
||||
};
|
||||
@@ -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، لديك الحق في تقديم شكوى إلى السلطة الرقابية إذا كنت تعتقد أن معالجة بياناتك الشخصية غير قانونية.",
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -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حقوق المواد المصورة أو النصية الخاصة بأطراف أخرى ليست مقيدة أو ملغاة بهذا البند."
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -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": "لا ينطبق حق الانسحاب على البضائع التي تم تصنيعها أو تفصيلها حسب طلب العميل (الأفلام والأنابيب)، لكن يمكن منحه باتفاق. كما أن حاويات الأسمدة التي تم إزالة ختمها أو تدميره بفتحها مستثناة أيضًا من حق الانسحاب."
|
||||
};
|
||||
@@ -1,3 +0,0 @@
|
||||
export default {
|
||||
"code": "ar-EG"
|
||||
};
|
||||
@@ -1,9 +0,0 @@
|
||||
export default {
|
||||
"home": "الرئيسية",
|
||||
"aktionen": "العروض",
|
||||
"filiale": "الفرع",
|
||||
"categories": "الفئات",
|
||||
"categoriesOpen": "افتح الفئات",
|
||||
"categoriesClose": "أغلق الفئات",
|
||||
"otherCategories": "فئات أخرى"
|
||||
};
|
||||
@@ -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": "يتم إكمال الطلب...",
|
||||
};
|
||||
@@ -1,10 +0,0 @@
|
||||
export default {
|
||||
"oilPress": {
|
||||
"title": "استعارة معصرة زيت",
|
||||
"comingSoon": "المحتوى قادم قريباً..."
|
||||
},
|
||||
"thcTest": {
|
||||
"title": "اختبار THC",
|
||||
"comingSoon": "المحتوى قادم قريباً..."
|
||||
}
|
||||
};
|
||||
@@ -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": "ادفع عند الاستلام",
|
||||
}
|
||||
};
|
||||
@@ -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": "كلما اخترت منتجات أكثر، كلما وفرت أكثر!"
|
||||
}
|
||||
};
|
||||
@@ -1,5 +0,0 @@
|
||||
export default {
|
||||
"placeholder": "ممكن تسألني عن أنواع الحشيش...",
|
||||
"recording": "جاري التسجيل...",
|
||||
"searchProducts": "ابحث عن المنتجات...",
|
||||
};
|
||||
@@ -1,11 +0,0 @@
|
||||
export default {
|
||||
"seeds": "بذور",
|
||||
"stecklinge": "قصاصات",
|
||||
"oilPress": "استعارة معصرة زيت",
|
||||
"thcTest": "اختبار THC",
|
||||
"address1": "Trachenberger Straße 14",
|
||||
"address2": "01129 Dresden",
|
||||
"showUsPhoto": "ورينا أجمل صورة عندك",
|
||||
"selectSeedRate": "اختار البذرة واضغط تقييم",
|
||||
"indoorSeason": "موسم الزراعة الداخلية بدأ"
|
||||
};
|
||||
@@ -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 ثوانٍ."
|
||||
}
|
||||
};
|
||||
@@ -1,6 +0,0 @@
|
||||
export default {
|
||||
"name": "الاسم",
|
||||
"searchField": "كلمة البحث",
|
||||
"priceLowHigh": "السعر: من الأقل للأعلى",
|
||||
"priceHighLow": "السعر: من الأعلى للأقل"
|
||||
};
|
||||
@@ -1,12 +0,0 @@
|
||||
export default {
|
||||
"vat": "ضريبة القيمة المضافة",
|
||||
"vat7": "ضريبة القيمة المضافة 7%",
|
||||
"vat19": "ضريبة القيمة المضافة 19%",
|
||||
"vat19WithShipping": "ضريبة القيمة المضافة 19% (شاملة الشحن)",
|
||||
"totalNet": "إجمالي السعر الصافي",
|
||||
"totalGross": "إجمالي السعر الإجمالي بدون الشحن",
|
||||
"subtotal": "المجموع الفرعي",
|
||||
"incl7Vat": "شاملة ضريبة القيمة المضافة 7%",
|
||||
"inclVatWithFooter": "(شاملة {{vat}}% ضريبة القيمة المضافة،*)",
|
||||
"inclVatAmount": "شاملة {{amount}} € ضريبة القيمة المضافة ({{rate}}%)"
|
||||
};
|
||||
@@ -1,5 +0,0 @@
|
||||
export default {
|
||||
"home": "بذور وقصاصات القنب الممتازة",
|
||||
"aktionen": "العروض والتخفيضات الحالية",
|
||||
"filiale": "متجرنا في دريسدن",
|
||||
};
|
||||
@@ -1,3 +0,0 @@
|
||||
import translations from './index.js';
|
||||
|
||||
export default translations;
|
||||
@@ -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": "Админ потребители"
|
||||
}
|
||||
};
|
||||
@@ -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": "Количка, запазена във вашия профил"
|
||||
}
|
||||
};
|
||||
@@ -1,3 +0,0 @@
|
||||
export default {
|
||||
"privacyRead": "Прочетено и прието",
|
||||
};
|
||||
@@ -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": "Градът е задължителен"
|
||||
}
|
||||
};
|
||||
@@ -1,19 +0,0 @@
|
||||
export default {
|
||||
"loading": "Зареждане...",
|
||||
"error": "Грешка",
|
||||
"close": "Затвори",
|
||||
"save": "Запази",
|
||||
"cancel": "Отказ",
|
||||
"ok": "OK",
|
||||
"yes": "Да",
|
||||
"no": "Не",
|
||||
"next": "Напред",
|
||||
"back": "Назад",
|
||||
"edit": "Редактирай",
|
||||
"delete": "Изтрий",
|
||||
"add": "Добави",
|
||||
"remove": "Премахни",
|
||||
"products": "Продукти",
|
||||
"product": "Продукт",
|
||||
"days": "Дни"
|
||||
};
|
||||
@@ -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}}€ се квалифицира за безплатна доставка."
|
||||
}
|
||||
};
|
||||
@@ -1,7 +0,0 @@
|
||||
export default {
|
||||
"sorting": "Сортиране",
|
||||
"perPage": "на страница",
|
||||
"availability": "Наличност",
|
||||
"manufacturer": "Производител",
|
||||
"all": "Всички"
|
||||
};
|
||||
@@ -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": "Право на отказ"
|
||||
}
|
||||
};
|
||||
@@ -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
|
||||
};
|
||||
@@ -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": "Мястото на изпълнение и подсъдността за всички претенции е Дрезден, освен ако задължителни законови разпоредби не предвиждат друго."
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -1,8 +0,0 @@
|
||||
export default {
|
||||
"title": "Информация за Закона за батериите",
|
||||
"intro": "Във връзка с продажбата на батерии или доставката на устройства, съдържащи батерии, ние сме задължени да ви информираме за следното:",
|
||||
"returnObligation": "Като краен потребител сте законово задължени да връщате използвани батерии. Можете да върнете стари батерии, които ние имаме или сме имали в продуктовия си асортимент като нови батерии, безплатно в нашия склад за изпращане (адрес за доставка).",
|
||||
"symbolsInfo": "Символите, показани на батериите, имат следното значение:",
|
||||
"wasteSymbol": "Символът на пресечената кофа за отпадъци означава, че батерията не трябва да се изхвърля с битовите отпадъци.",
|
||||
"chemicalSymbols": "Pb = Батерията съдържа повече от 0,004 масови процента олово\nCd = Батерията съдържа повече от 0,002 масови процента кадмий\nHg = Батерията съдържа повече от 0,0005 масови процента живак."
|
||||
};
|
||||
@@ -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."
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -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
Reference in New Issue
Block a user