Compare commits
43 Commits
mollie
...
08c04909e0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
08c04909e0 | ||
|
|
510907b48a | ||
|
|
e02b18e17f | ||
|
|
9ffbd5b84e | ||
|
|
f8dbb24823 | ||
|
|
13f1e14a3d | ||
|
|
6b7bcf4155 | ||
|
|
45258ac522 | ||
|
|
080515af68 | ||
|
|
33b229728f | ||
|
|
8d69b0566b | ||
|
|
280916224a | ||
|
|
fd77fc8f7f | ||
|
|
f5d6778def | ||
|
|
11a3522a97 | ||
|
|
51471d4a55 | ||
|
|
859a2c06d8 | ||
|
|
5c90d048fb | ||
|
|
cff9c88808 | ||
|
|
b78de53786 | ||
|
|
925667fc2c | ||
|
|
251352c660 | ||
|
|
88c757fd35 | ||
|
|
d8c802c2f1 | ||
|
|
056b63efa0 | ||
|
|
c7afad68b0 | ||
|
|
5157b7d781 | ||
|
|
9072a3c977 | ||
|
|
838e2fd786 | ||
|
|
abbb5e222d | ||
|
|
c216154bd7 | ||
|
|
9000b28ce5 | ||
|
|
8f2253f155 | ||
|
|
b33ece2875 | ||
|
|
02aff1e456 | ||
|
|
9e14827c91 | ||
|
|
8698816875 | ||
|
|
987de641e4 | ||
|
|
23e1742e40 | ||
|
|
205558d06c | ||
|
|
046979a64d | ||
|
|
161e377de4 | ||
|
|
73a88f508b |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -56,6 +56,8 @@ yarn-error.log*
|
||||
# Local configuration
|
||||
src/config.local.js
|
||||
|
||||
taxonomy-with-ids.de-DE*
|
||||
|
||||
# Local development notes
|
||||
dev-notes.md
|
||||
dev-notes.local.md
|
||||
19
.vscode/launch.json
vendored
19
.vscode/launch.json
vendored
@@ -3,20 +3,31 @@
|
||||
// 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",
|
||||
"cwd": "${workspaceFolder}"
|
||||
"env": {
|
||||
"NODE_ENV": "development"
|
||||
},
|
||||
"envFile": "${workspaceFolder}/.env",
|
||||
"skipFiles": [
|
||||
"<node_internals>/**"
|
||||
]
|
||||
},{
|
||||
"type": "node-terminal",
|
||||
"name": "Start",
|
||||
"type": "node-terminal",
|
||||
"request": "launch",
|
||||
"command": "npm run start",
|
||||
"cwd": "${workspaceFolder}"
|
||||
"env": {
|
||||
"NODE_ENV": "development"
|
||||
},
|
||||
"envFile": "${workspaceFolder}/.env",
|
||||
"skipFiles": [
|
||||
"<node_internals>/**"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
209
MULTILINGUAL_IMPLEMENTATION.md
Normal file
209
MULTILINGUAL_IMPLEMENTATION.md
Normal file
@@ -0,0 +1,209 @@
|
||||
# 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,7 +13,11 @@
|
||||
"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"
|
||||
"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"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
@@ -27,10 +31,15 @@
|
||||
"@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,6 +27,74 @@ 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");
|
||||
|
||||
|
||||
@@ -11,6 +11,99 @@ 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();
|
||||
|
||||
@@ -23,124 +116,131 @@ const generateProductsXml = (allProductsData = [], baseUrl, config) => {
|
||||
const getGoogleProductCategory = (categoryId) => {
|
||||
const categoryMappings = {
|
||||
// Seeds & Plants
|
||||
689: "Home & Garden > Plants > Seeds",
|
||||
706: "Home & Garden > Plants", // Stecklinge (cuttings)
|
||||
376: "Home & Garden > Plants > Plant & Herb Growing Kits", // Grow-Sets
|
||||
689: "543561", // Seeds (Saatgut)
|
||||
706: "543561", // Stecklinge (cuttings) – ebenfalls Pflanzen/Saatgut
|
||||
376: "2802", // Grow-Sets – Pflanzen- & Kräuteranbausets
|
||||
|
||||
// Headshop & Accessories
|
||||
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
|
||||
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)
|
||||
|
||||
// Measuring & Packaging
|
||||
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
|
||||
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
|
||||
|
||||
// Lighting & Equipment
|
||||
694: "Home & Garden > Lighting", // Lampen
|
||||
261: "Home & Garden > Lighting", // Lampenzubehör
|
||||
694: "3006", // Lampen – Lampen (Beleuchtung)
|
||||
261: "3006", // Zubehör > Lampenzubehör – Lampen
|
||||
|
||||
// Plants & Growing
|
||||
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
|
||||
691: "500033", // Dünger – Dünger
|
||||
692: "5633", // Zubehör > Dünger-Zubehör – Zubehör für Gartenarbeit
|
||||
693: "5655", // Zelte – Zelte
|
||||
|
||||
// Pots & Containers
|
||||
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
|
||||
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
|
||||
|
||||
// Ventilation & Climate
|
||||
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
|
||||
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
|
||||
|
||||
// Irrigation & Watering
|
||||
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
|
||||
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
|
||||
|
||||
// Growing Media & Soils
|
||||
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
|
||||
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
|
||||
|
||||
// Propagation & Starting
|
||||
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
|
||||
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
|
||||
|
||||
// Tools & Equipment
|
||||
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
|
||||
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
|
||||
|
||||
// Hardware & Plumbing
|
||||
222: "Hardware > Plumbing", // PE-Teile
|
||||
374: "Hardware > Plumbing > Plumbing Fittings", // Verbindungsteile
|
||||
222: "3568", // Bewässerung > PE-Teile – Bewässerungssysteme
|
||||
374: "1700", // Belüftung > Ab- und Zuluft > Verbindungsteile – Ventilatoren
|
||||
|
||||
// Electronics & Control
|
||||
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
|
||||
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
|
||||
|
||||
// Camping & Outdoor
|
||||
226: "Sporting Goods > Outdoor Recreation > Camping & Hiking", // Zeltzubehör
|
||||
226: "5655", // Zubehör > Zeltzubehör – Zelte
|
||||
|
||||
// Plant Care & Protection
|
||||
239: "Home & Garden > Lawn & Garden > Pest Control", // Pflanzenschutz
|
||||
240: "Home & Garden > Plants", // Anbauzubehör
|
||||
239: "4085", // Zubehör > Anbauzubehör > Pflanzenschutz – Herbizide
|
||||
240: "5633", // Zubehör > Anbauzubehör – Zubehör für Gartenarbeit
|
||||
|
||||
// Office & Media
|
||||
424: "Office Supplies > Labels", // Etiketten & Schilder
|
||||
387: "Media > Books", // Literatur
|
||||
424: "4377", // Zubehör > Anbauzubehör > Etiketten & Schilder – Etiketten & Anhängerschilder
|
||||
387: "543541", // Zubehör > Anbauzubehör > Literatur – Bücher
|
||||
|
||||
// General categories
|
||||
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
|
||||
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
|
||||
};
|
||||
|
||||
return categoryMappings[categoryId] || "Home & Garden > Plants";
|
||||
const categoryId_str = categoryMappings[categoryId] || "5631"; // Default to Haushaltsbedarf: Aufbewahrung
|
||||
|
||||
// Validate that the category ID is not empty
|
||||
if (!categoryId_str || categoryId_str.trim() === "") {
|
||||
return "5631"; // Haushaltsbedarf: Aufbewahrung
|
||||
}
|
||||
|
||||
return categoryId_str;
|
||||
};
|
||||
|
||||
let productsXml = `<?xml version="1.0" encoding="UTF-8"?>
|
||||
@@ -150,7 +250,7 @@ const generateProductsXml = (allProductsData = [], baseUrl, config) => {
|
||||
<link>${baseUrl}</link>
|
||||
<description>${config.descriptions.short}</description>
|
||||
<lastBuildDate>${currentDate}</lastBuildDate>
|
||||
<language>${config.language}</language>`;
|
||||
<language>de-DE</language>`;
|
||||
|
||||
// Helper function to clean text content of problematic characters
|
||||
const cleanTextContent = (text) => {
|
||||
@@ -318,6 +418,17 @@ 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: 1.9 MiB After Width: | Height: | Size: 85 KiB |
BIN
public/assets/images/filiale1.jpg
Normal file
BIN
public/assets/images/filiale1.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 208 KiB |
BIN
public/assets/images/filiale2.jpg
Normal file
BIN
public/assets/images/filiale2.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 242 KiB |
BIN
public/assets/images/presse.jpg
Normal file
BIN
public/assets/images/presse.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 71 KiB |
BIN
public/assets/images/purpl.jpg
Normal file
BIN
public/assets/images/purpl.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 68 KiB |
@@ -1,5 +1,5 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<html lang="de" data-i18n-lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
|
||||
42
src/App.js
42
src/App.js
@@ -18,13 +18,19 @@ 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 Home from "./pages/Home.js";
|
||||
import MainPageLayout from "./components/MainPageLayout.js";
|
||||
|
||||
// Lazy load all route components to reduce initial bundle size
|
||||
const Content = lazy(() => import(/* webpackChunkName: "content" */ "./components/Content.js"));
|
||||
@@ -40,7 +46,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"));
|
||||
//const NotFound404 = lazy(() => import(/* webpackChunkName: "legal" */ "./pages/NotFound404.js")); <Route path="/404" element={<NotFound404 />} />
|
||||
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"));
|
||||
@@ -50,6 +56,13 @@ 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
|
||||
@@ -195,9 +208,12 @@ const AppContent = ({ currentTheme, onThemeChange }) => {
|
||||
<CircularProgress color="primary" />
|
||||
</Box>
|
||||
}>
|
||||
<CarouselProvider>
|
||||
<Routes>
|
||||
{/* Home page with text only */}
|
||||
<Route path="/" element={<Home />} />
|
||||
{/* Main pages using unified component */}
|
||||
<Route path="/" element={<MainPageLayout />} />
|
||||
<Route path="/aktionen" element={<MainPageLayout />} />
|
||||
<Route path="/filiale" element={<MainPageLayout />} />
|
||||
|
||||
{/* Category page - Render Content in parallel */}
|
||||
<Route
|
||||
@@ -216,6 +232,9 @@ const AppContent = ({ currentTheme, onThemeChange }) => {
|
||||
{/* 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"
|
||||
@@ -234,7 +253,6 @@ const AppContent = ({ currentTheme, onThemeChange }) => {
|
||||
{/* 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
|
||||
@@ -244,11 +262,16 @@ const AppContent = ({ currentTheme, onThemeChange }) => {
|
||||
<Route path="/widerrufsrecht" element={<Widerrufsrecht />} />
|
||||
|
||||
{/* Grow Tent Configurator */}
|
||||
<Route path="/Konfigurator" element={<GrowTentKonfigurator />} />
|
||||
<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>
|
||||
</Suspense>
|
||||
</Box>
|
||||
{/* Conditionally render the Chat Assistant */}
|
||||
@@ -343,7 +366,13 @@ 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
|
||||
@@ -367,6 +396,7 @@ const App = () => {
|
||||
/>
|
||||
</SocketProvider>
|
||||
</ThemeProvider>
|
||||
</LanguageProvider>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -3,7 +3,8 @@ 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 Home from './pages/Home.js';
|
||||
import MainPageLayout from './components/MainPageLayout.js';
|
||||
import { CarouselProvider } from './contexts/CarouselContext.js';
|
||||
|
||||
const PrerenderAppContent = (socket) => (
|
||||
<Box
|
||||
@@ -44,9 +45,11 @@ const PrerenderAppContent = (socket) => (
|
||||
</AppBar>
|
||||
|
||||
<Box sx={{ flexGrow: 1 }}>
|
||||
<CarouselProvider>
|
||||
<Routes>
|
||||
<Route path="/" element={<Home />} />
|
||||
<Route path="/" element={<MainPageLayout />} />
|
||||
</Routes>
|
||||
</CarouselProvider>
|
||||
</Box>
|
||||
|
||||
<Footer/>
|
||||
|
||||
@@ -7,7 +7,8 @@ const {
|
||||
} = require('@mui/material');
|
||||
const Footer = require('./components/Footer.js').default;
|
||||
const { Logo, CategoryList } = require('./components/header/index.js');
|
||||
const Home = require('./pages/Home.js').default;
|
||||
const MainPageLayout = require('./components/MainPageLayout.js').default;
|
||||
const { CarouselProvider } = require('./contexts/CarouselContext.js');
|
||||
|
||||
class PrerenderHome extends React.Component {
|
||||
render() {
|
||||
@@ -62,7 +63,7 @@ class PrerenderHome extends React.Component {
|
||||
React.createElement(
|
||||
Box,
|
||||
{ sx: { flexGrow: 1 } },
|
||||
React.createElement(Home)
|
||||
React.createElement(CarouselProvider, null, React.createElement(MainPageLayout))
|
||||
),
|
||||
React.createElement(Footer)
|
||||
);
|
||||
|
||||
92
src/PrerenderNotFound.js
Normal file
92
src/PrerenderNotFound.js
Normal file
@@ -0,0 +1,92 @@
|
||||
const React = require('react');
|
||||
const {
|
||||
Box,
|
||||
AppBar,
|
||||
Toolbar,
|
||||
Container
|
||||
} = require('@mui/material');
|
||||
const Footer = require('./components/Footer.js').default;
|
||||
const { Logo } = require('./components/header/index.js');
|
||||
const NotFound404 = require('./pages/NotFound404.js').default;
|
||||
|
||||
class PrerenderNotFound extends React.Component {
|
||||
render() {
|
||||
return React.createElement(
|
||||
Box,
|
||||
{
|
||||
sx: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
minHeight: '100vh',
|
||||
mb: 0,
|
||||
pb: 0,
|
||||
bgcolor: 'background.default'
|
||||
}
|
||||
},
|
||||
React.createElement(
|
||||
AppBar,
|
||||
{ position: 'sticky', color: 'primary', elevation: 0, sx: { zIndex: 1100 } },
|
||||
React.createElement(
|
||||
Toolbar,
|
||||
{ sx: { minHeight: 64, py: { xs: 0.5, sm: 0 } } },
|
||||
React.createElement(
|
||||
Container,
|
||||
{
|
||||
maxWidth: 'lg',
|
||||
sx: {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
px: { xs: 0, sm: 3 }
|
||||
}
|
||||
},
|
||||
React.createElement(
|
||||
Box,
|
||||
{ sx: {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
width: '100%',
|
||||
flexDirection: { xs: 'column', sm: 'row' }
|
||||
}
|
||||
},
|
||||
React.createElement(
|
||||
Box,
|
||||
{ sx: {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
width: '100%',
|
||||
justifyContent: { xs: 'space-between', sm: 'flex-start' },
|
||||
minHeight: { xs: 52, sm: 'auto' },
|
||||
px: { xs: 0, sm: 0 }
|
||||
}
|
||||
},
|
||||
React.createElement(Logo)
|
||||
),
|
||||
// Reserve space for SearchBar on mobile (invisible placeholder)
|
||||
React.createElement(
|
||||
Box,
|
||||
{ sx: {
|
||||
display: { xs: 'block', sm: 'none' },
|
||||
width: '100%',
|
||||
mt: { xs: 1, sm: 0 },
|
||||
mb: { xs: 0.5, sm: 0 },
|
||||
px: { xs: 0, sm: 0 },
|
||||
height: 40, // Reserve space for SearchBar
|
||||
opacity: 0 // Invisible placeholder
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
),
|
||||
React.createElement(
|
||||
Box,
|
||||
{ sx: { flexGrow: 1 } },
|
||||
React.createElement(NotFound404)
|
||||
),
|
||||
React.createElement(Footer)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { default: PrerenderNotFound };
|
||||
@@ -10,6 +10,7 @@ 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 = [];
|
||||
|
||||
@@ -51,11 +52,14 @@ 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 {
|
||||
@@ -150,12 +154,17 @@ class AddToCartButton extends Component {
|
||||
},
|
||||
}}
|
||||
>
|
||||
Ab{" "}
|
||||
{new Date(incoming).toLocaleDateString("de-DE", {
|
||||
{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", {
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
})}`}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
@@ -181,7 +190,9 @@ class AddToCartButton extends Component {
|
||||
},
|
||||
}}
|
||||
>
|
||||
{this.props.steckling ? "Als Steckling vorbestellen" : "In den Korb"}
|
||||
{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")}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
@@ -259,7 +270,7 @@ class AddToCartButton extends Component {
|
||||
<AddIcon />
|
||||
</IconButton>
|
||||
|
||||
<Tooltip title="Aus dem Warenkorb entfernen" arrow>
|
||||
<Tooltip title={this.props.t ? this.props.t('cart.removeFromCart') : 'Aus dem Warenkorb entfernen'} arrow>
|
||||
<IconButton
|
||||
color="inherit"
|
||||
onClick={this.handleClearCart}
|
||||
@@ -272,7 +283,7 @@ class AddToCartButton extends Component {
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
{this.props.cartButton && (
|
||||
<Tooltip title="Warenkorb öffnen" arrow>
|
||||
<Tooltip title={this.props.t ? this.props.t('cart.openCart') : 'Warenkorb öffnen'} arrow>
|
||||
<IconButton
|
||||
color="inherit"
|
||||
onClick={this.toggleCart}
|
||||
@@ -302,7 +313,7 @@ class AddToCartButton extends Component {
|
||||
fontWeight: "bold",
|
||||
}}
|
||||
>
|
||||
Out of Stock
|
||||
{this.props.t ? this.props.t('product.outOfStock') : 'Out of Stock'}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
@@ -327,7 +338,9 @@ class AddToCartButton extends Component {
|
||||
},
|
||||
}}
|
||||
>
|
||||
{this.props.steckling ? "Als Steckling vorbestellen" : "In den Korb"}
|
||||
{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")}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
@@ -404,7 +417,7 @@ class AddToCartButton extends Component {
|
||||
<AddIcon />
|
||||
</IconButton>
|
||||
|
||||
<Tooltip title="Aus dem Warenkorb entfernen" arrow>
|
||||
<Tooltip title={this.props.t ? this.props.t('cart.removeFromCart') : 'Aus dem Warenkorb entfernen'} arrow>
|
||||
<IconButton
|
||||
color="inherit"
|
||||
onClick={this.handleClearCart}
|
||||
@@ -417,7 +430,7 @@ class AddToCartButton extends Component {
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
{this.props.cartButton && (
|
||||
<Tooltip title="Warenkorb öffnen" arrow>
|
||||
<Tooltip title={this.props.t ? this.props.t('cart.openCart') : 'Warenkorb öffnen'} arrow>
|
||||
<IconButton
|
||||
color="inherit"
|
||||
onClick={this.toggleCart}
|
||||
@@ -436,4 +449,4 @@ class AddToCartButton extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
export default AddToCartButton;
|
||||
export default withI18n()(AddToCartButton);
|
||||
|
||||
@@ -8,6 +8,7 @@ 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 {
|
||||
@@ -53,8 +54,8 @@ class CartDropdown extends Component {
|
||||
currency: 'EUR'
|
||||
});
|
||||
|
||||
const shippingNetPrice = deliveryCost / (1 + 19 / 100);
|
||||
const shippingVat = deliveryCost - shippingNetPrice;
|
||||
const shippingNetPrice = deliveryCost > 0 ? deliveryCost / (1 + 19 / 100) : 0;
|
||||
const shippingVat = deliveryCost > 0 ? deliveryCost - shippingNetPrice : 0;
|
||||
const totalVat7 = priceCalculations.vat7;
|
||||
const totalVat19 = priceCalculations.vat19 + shippingVat;
|
||||
const totalGross = priceCalculations.totalGross + deliveryCost;
|
||||
@@ -119,7 +120,7 @@ class CartDropdown extends Component {
|
||||
)}
|
||||
{totalVat7 > 0 && (
|
||||
<TableRow>
|
||||
<TableCell>7% Mehrwertsteuer:</TableCell>
|
||||
<TableCell>{this.props.t ? this.props.t('tax.vat7') : '7% Mehrwertsteuer'}:</TableCell>
|
||||
<TableCell align="right">
|
||||
{currencyFormatter.format(totalVat7)}
|
||||
</TableCell>
|
||||
@@ -127,7 +128,7 @@ class CartDropdown extends Component {
|
||||
)}
|
||||
{totalVat19 > 0 && (
|
||||
<TableRow>
|
||||
<TableCell>19% Mehrwertsteuer (inkl. Versand):</TableCell>
|
||||
<TableCell>{this.props.t ? this.props.t('tax.vat19WithShipping') : '19% Mehrwertsteuer (inkl. Versand)'}:</TableCell>
|
||||
<TableCell align="right">
|
||||
{currencyFormatter.format(totalVat19)}
|
||||
</TableCell>
|
||||
@@ -139,14 +140,23 @@ class CartDropdown extends Component {
|
||||
{currencyFormatter.format(priceCalculations.totalGross)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
{deliveryCost > 0 && (
|
||||
<TableRow>
|
||||
<TableCell sx={{ fontWeight: 'bold' }}>Versandkosten:</TableCell>
|
||||
<TableCell sx={{ fontWeight: 'bold' }}>
|
||||
Versandkosten:
|
||||
{deliveryCost === 0 && priceCalculations.totalGross < 100 && (
|
||||
<span style={{ color: '#2e7d32', fontSize: '0.8em', marginLeft: '4px' }}>
|
||||
(kostenlos ab 100€)
|
||||
</span>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell align="right" sx={{ fontWeight: 'bold' }}>
|
||||
{currencyFormatter.format(deliveryCost)}
|
||||
{deliveryCost === 0 ? (
|
||||
<span style={{ color: '#2e7d32' }}>kostenlos</span>
|
||||
) : (
|
||||
currencyFormatter.format(deliveryCost)
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
<TableRow sx={{ borderTop: '1px solid #e0e0e0' }}>
|
||||
<TableCell sx={{ fontWeight: 'bold', pt: 2 }}>Gesamtsumme:</TableCell>
|
||||
<TableCell align="right" sx={{ fontWeight: 'bold', pt: 2 }}>
|
||||
@@ -161,14 +171,14 @@ class CartDropdown extends Component {
|
||||
<Table size="small">
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableCell>Gesamtnettopreis:</TableCell>
|
||||
<TableCell>{this.props.t ? this.props.t('tax.totalNet') : 'Gesamtnettopreis'}:</TableCell>
|
||||
<TableCell align="right">
|
||||
{new Intl.NumberFormat('de-DE', {style: 'currency', currency: 'EUR'}).format(priceCalculations.totalNet)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
{priceCalculations.vat7 > 0 && (
|
||||
<TableRow>
|
||||
<TableCell>7% Mehrwertsteuer:</TableCell>
|
||||
<TableCell>{this.props.t ? this.props.t('tax.vat7') : '7% Mehrwertsteuer'}:</TableCell>
|
||||
<TableCell align="right">
|
||||
{new Intl.NumberFormat('de-DE', {style: 'currency', currency: 'EUR'}).format(priceCalculations.vat7)}
|
||||
</TableCell>
|
||||
@@ -176,14 +186,14 @@ class CartDropdown extends Component {
|
||||
)}
|
||||
{priceCalculations.vat19 > 0 && (
|
||||
<TableRow>
|
||||
<TableCell>19% Mehrwertsteuer:</TableCell>
|
||||
<TableCell>{this.props.t ? this.props.t('tax.vat19') : '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' }}>Gesamtbruttopreis ohne Versand:</TableCell>
|
||||
<TableCell sx={{ fontWeight: 'bold' }}>{this.props.t ? this.props.t('tax.totalGross') : 'Gesamtbruttopreis ohne Versand'}:</TableCell>
|
||||
<TableCell align="right" sx={{ fontWeight: 'bold' }}>
|
||||
{new Intl.NumberFormat('de-DE', {style: 'currency', currency: 'EUR'}).format(priceCalculations.totalGross)}
|
||||
</TableCell>
|
||||
@@ -201,7 +211,7 @@ class CartDropdown extends Component {
|
||||
fullWidth
|
||||
onClick={onClose}
|
||||
>
|
||||
Weiter einkaufen
|
||||
{this.props.t ? this.props.t('cart.continueShopping') : 'Weiter einkaufen'}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
@@ -213,7 +223,7 @@ class CartDropdown extends Component {
|
||||
sx={{ mt: 2 }}
|
||||
onClick={onCheckout}
|
||||
>
|
||||
Weiter zur Kasse
|
||||
{this.props.t ? this.props.t('cart.proceedToCheckout') : 'Weiter zur Kasse'}
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
@@ -223,4 +233,4 @@ class CartDropdown extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
export default CartDropdown;
|
||||
export default withI18n()(CartDropdown);
|
||||
@@ -6,6 +6,7 @@ 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 {
|
||||
|
||||
@@ -126,9 +127,9 @@ class CartItem extends Component {
|
||||
fontStyle="italic"
|
||||
component="div"
|
||||
>
|
||||
inkl. {new Intl.NumberFormat('de-DE', {style: 'currency', currency: 'EUR'}).format(
|
||||
{this.props.t ? this.props.t('product.inclShort') : 'inkl.'} {new Intl.NumberFormat('de-DE', {style: 'currency', currency: 'EUR'}).format(
|
||||
(item.price * item.quantity) - ((item.price * item.quantity) / (1 + item.vat / 100))
|
||||
)} MwSt. ({item.vat}%)
|
||||
)} {this.props.t ? this.props.t('product.vatShort') : 'MwSt.'} ({item.vat}%)
|
||||
</Typography>
|
||||
)}
|
||||
|
||||
@@ -146,11 +147,14 @@ class CartItem extends Component {
|
||||
display: "block"
|
||||
}}
|
||||
>
|
||||
{this.props.id.toString().endsWith("steckling") ? "Lieferzeit: 14 Tage" :
|
||||
item.available == 1 ? "Lieferzeit: 2-3 Tage" :
|
||||
item.availableSupplier == 1 ? "Lieferzeit: 7-9 Tage" : ""}
|
||||
{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") : ""}
|
||||
</Typography>
|
||||
<AddToCartButton available={1} id={this.props.id} availableSupplier={item.availableSupplier} price={item.price} seoName={item.seoName} name={item.name} weight={item.weight} vat={item.vat} versandklasse={item.versandklasse}/>
|
||||
<AddToCartButton available={1} id={this.props.id} komponenten={item.komponenten} availableSupplier={item.availableSupplier} price={item.price} seoName={item.seoName} name={item.name} weight={item.weight} vat={item.vat} versandklasse={item.versandklasse}/>
|
||||
</Box>
|
||||
</Box>
|
||||
</ListItem>
|
||||
@@ -159,4 +163,4 @@ class CartItem extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
export default CartItem;
|
||||
export default withI18n()(CartItem);
|
||||
@@ -16,7 +16,7 @@ const CategoryBox = ({
|
||||
name,
|
||||
seoName,
|
||||
bgcolor,
|
||||
fontSize = '0.8rem',
|
||||
fontSize = '1.2rem',
|
||||
...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: '0 8px'
|
||||
padding: '12px 8px'
|
||||
}}>
|
||||
{name}
|
||||
</div>
|
||||
|
||||
@@ -13,6 +13,7 @@ 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);
|
||||
|
||||
@@ -52,7 +53,7 @@ function getCachedCategoryData(categoryId) {
|
||||
|
||||
|
||||
|
||||
function getFilteredProducts(unfilteredProducts, attributes) {
|
||||
function getFilteredProducts(unfilteredProducts, attributes, t) {
|
||||
const attributeSettings = getAllSettingsWithPrefix('filter_attribute_');
|
||||
const manufacturerSettings = getAllSettingsWithPrefix('filter_manufacturer_');
|
||||
const availabilitySettings = getAllSettingsWithPrefix('filter_availability_');
|
||||
@@ -149,17 +150,17 @@ function getFilteredProducts(unfilteredProducts, attributes) {
|
||||
|
||||
// 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: 'auf Lager'});
|
||||
activeAvailabilityFilters.push({id: '1', name: t ? t('product.inStock') : '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: 'Neu'});
|
||||
activeAvailabilityFilters.push({id: '2', name: t ? t('product.new') : '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: 'Bald verfügbar'});
|
||||
activeAvailabilityFilters.push({id: '3', name: t ? t('product.comingSoon') : 'Bald verfügbar'});
|
||||
}
|
||||
|
||||
return {filteredProducts,activeAttributeFilters:activeAttributeFiltersWithNames,activeManufacturerFilters:activeManufacturerFiltersWithNames,activeAvailabilityFilters};
|
||||
@@ -256,7 +257,8 @@ class Content extends Component {
|
||||
unfilteredProducts: unfilteredProducts,
|
||||
...getFilteredProducts(
|
||||
unfilteredProducts,
|
||||
response.attributes
|
||||
response.attributes,
|
||||
this.props.t
|
||||
),
|
||||
categoryName: response.categoryName || response.name || null,
|
||||
dataType: response.dataType,
|
||||
@@ -385,7 +387,8 @@ class Content extends Component {
|
||||
this.setState({
|
||||
...getFilteredProducts(
|
||||
this.state.unfilteredProducts,
|
||||
this.state.attributes
|
||||
this.state.attributes,
|
||||
this.props.t
|
||||
)
|
||||
});
|
||||
}
|
||||
@@ -602,7 +605,7 @@ class Content extends Component {
|
||||
{(this.getCurrentCategoryId() == 706 || this.getCurrentCategoryId() == 689) &&
|
||||
<Box sx={{ display: { xs: 'none', sm: 'block' } }}>
|
||||
<Typography variant="h6" sx={{mt:3}}>
|
||||
Andere Kategorien
|
||||
{this.props.t ? this.props.t('navigation.otherCategories') : 'Andere Kategorien'}
|
||||
</Typography>
|
||||
</Box>
|
||||
}
|
||||
@@ -647,7 +650,7 @@ class Content extends Component {
|
||||
p: 2,
|
||||
}}>
|
||||
<Typography sx={{ fontSize: '1.3rem', color: 'white', fontFamily: 'SwashingtonCP' }}>
|
||||
Seeds
|
||||
{this.props.t('sections.seeds')}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
@@ -694,7 +697,7 @@ class Content extends Component {
|
||||
p: 2,
|
||||
}}>
|
||||
<Typography sx={{ fontSize: '1.3rem', color: 'white', fontFamily: 'SwashingtonCP' }}>
|
||||
Stecklinge
|
||||
{this.props.t('sections.stecklinge')}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
@@ -723,4 +726,4 @@ class Content extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
export default withRouter(Content);
|
||||
export default withRouter(withI18n()(Content));
|
||||
@@ -6,6 +6,7 @@ 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)(() => ({
|
||||
@@ -229,9 +230,9 @@ class Footer extends Component {
|
||||
alignItems={{ xs: 'center', md: 'left' }}
|
||||
flexWrap="wrap"
|
||||
>
|
||||
<StyledRouterLink to="/datenschutz">Datenschutz</StyledRouterLink>
|
||||
<StyledRouterLink to="/agb">AGB</StyledRouterLink>
|
||||
<StyledRouterLink to="/sitemap">Sitemap</StyledRouterLink>
|
||||
<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>
|
||||
</Stack>
|
||||
|
||||
<Stack
|
||||
@@ -241,9 +242,9 @@ class Footer extends Component {
|
||||
alignItems={{ xs: 'center', md: 'left' }}
|
||||
flexWrap="wrap"
|
||||
>
|
||||
<StyledRouterLink to="/impressum">Impressum</StyledRouterLink>
|
||||
<StyledRouterLink to="/batteriegesetzhinweise">Batteriegesetzhinweise</StyledRouterLink>
|
||||
<StyledRouterLink to="/widerrufsrecht">Widerrufsrecht</StyledRouterLink>
|
||||
<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>
|
||||
</Stack>
|
||||
|
||||
{/* Payment Methods Section */}
|
||||
@@ -338,7 +339,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 }}>
|
||||
* Alle Preise inkl. gesetzlicher USt., zzgl. Versand
|
||||
{this.props.t ? this.props.t('footer.allPricesIncl') : '* 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>
|
||||
@@ -351,4 +352,4 @@ class Footer extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
export default Footer;
|
||||
export default withI18n()(Footer);
|
||||
|
||||
@@ -2,6 +2,7 @@ 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;
|
||||
@@ -186,7 +187,7 @@ class GoogleLoginButton extends Component {
|
||||
};
|
||||
|
||||
render() {
|
||||
const { disabled, style, className, text = 'Mit Google anmelden' } = this.props;
|
||||
const { disabled, style, className, text = (this.props.t ? this.props.t('auth.loginWithGoogle') : 'Mit Google anmelden') } = this.props;
|
||||
const { isInitializing, isPrompting } = this.state;
|
||||
const isLoading = isInitializing || isPrompting || (this.context && !this.context.isLoaded);
|
||||
|
||||
@@ -205,4 +206,4 @@ class GoogleLoginButton extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
export default GoogleLoginButton;
|
||||
export default withI18n(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 } = this.props;
|
||||
const { isHomePage, isProfilePage, isAktionenPage, isFilialePage } = 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) && <CategoryList categoryId={209} activeCategoryId={this.props.categoryId} socket={socket} socketB={socketB} />}
|
||||
{(isHomePage || this.props.categoryId || isProfilePage || isAktionenPage || isFilialePage) && <CategoryList categoryId={209} activeCategoryId={this.props.categoryId} socket={socket} socketB={socketB} />}
|
||||
</AppBar>
|
||||
);
|
||||
}
|
||||
@@ -105,10 +105,12 @@ 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} />}
|
||||
{({socket,socketB}) => <Header {...props} socket={socket} socketB={socketB} isHomePage={isHomePage} isProfilePage={isProfilePage} isAktionenPage={isAktionenPage} isFilialePage={isFilialePage} />}
|
||||
</SocketContext.Consumer>
|
||||
);
|
||||
};
|
||||
|
||||
269
src/components/LanguageSwitcher.js
Normal file
269
src/components/LanguageSwitcher.js
Normal file
@@ -0,0 +1,269 @@
|
||||
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,6 +22,7 @@ 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'));
|
||||
@@ -510,7 +511,7 @@ export class LoginComponent extends Component {
|
||||
color={isAdmin ? 'secondary' : 'inherit'}
|
||||
sx={{ my: 1, mx: 1.5 }}
|
||||
>
|
||||
Profil
|
||||
{this.props.t ? this.props.t('auth.profile') : 'Profil'}
|
||||
</Button>
|
||||
<Menu
|
||||
disableScrollLock={true}
|
||||
@@ -526,14 +527,28 @@ export class LoginComponent extends Component {
|
||||
horizontal: 'right',
|
||||
}}
|
||||
>
|
||||
<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>
|
||||
<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>
|
||||
<Divider />
|
||||
{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>
|
||||
{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>
|
||||
</Menu>
|
||||
</>
|
||||
) : (
|
||||
@@ -543,7 +558,7 @@ export class LoginComponent extends Component {
|
||||
onClick={this.handleOpen}
|
||||
sx={{ my: 1, mx: 1.5 }}
|
||||
>
|
||||
Login
|
||||
{this.props.t ? this.props.t('auth.login') : 'Login'}
|
||||
</Button>
|
||||
)
|
||||
)}
|
||||
@@ -558,7 +573,10 @@ 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 ? 'Anmelden' : 'Registrieren'}
|
||||
{tabValue === 0 ?
|
||||
(this.props.t ? this.props.t('auth.login') : 'Anmelden') :
|
||||
(this.props.t ? this.props.t('auth.register') : 'Registrieren')
|
||||
}
|
||||
</Typography>
|
||||
<IconButton edge="end" onClick={this.handleClose} aria-label="close">
|
||||
<CloseIcon />
|
||||
@@ -578,14 +596,14 @@ export class LoginComponent extends Component {
|
||||
textColor="inherit"
|
||||
>
|
||||
<Tab
|
||||
label="ANMELDEN"
|
||||
label={this.props.t ? this.props.t('auth.login').toUpperCase() : "ANMELDEN"}
|
||||
sx={{
|
||||
color: tabValue === 0 ? '#2e7d32' : 'inherit',
|
||||
fontWeight: 'bold'
|
||||
}}
|
||||
/>
|
||||
<Tab
|
||||
label="REGISTRIEREN"
|
||||
label={this.props.t ? this.props.t('auth.register').toUpperCase() : "REGISTRIEREN"}
|
||||
sx={{
|
||||
color: tabValue === 1 ? '#2e7d32' : 'inherit',
|
||||
fontWeight: 'bold'
|
||||
@@ -598,7 +616,14 @@ 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>
|
||||
</>
|
||||
}
|
||||
</Typography>
|
||||
)}
|
||||
{!showGoogleAuth && (
|
||||
@@ -611,7 +636,7 @@ export class LoginComponent extends Component {
|
||||
}}
|
||||
sx={{ width: '100%', backgroundColor: '#4285F4', color: 'white' }}
|
||||
>
|
||||
Mit Google anmelden
|
||||
{this.props.t ? this.props.t('auth.loginWithGoogle') : 'Mit Google anmelden'}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
@@ -643,7 +668,9 @@ 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' }}>ODER</Typography>
|
||||
<Typography variant="body2" sx={{ px: 2, color: '#757575' }}>
|
||||
{this.props.t ? this.props.t('auth.or') : 'ODER'}
|
||||
</Typography>
|
||||
<Box sx={{ flex: 1, height: '1px', backgroundColor: '#e0e0e0' }} />
|
||||
</Box>
|
||||
|
||||
@@ -654,7 +681,7 @@ export class LoginComponent extends Component {
|
||||
<Box sx={{ py: 1 }}>
|
||||
<TextField
|
||||
margin="dense"
|
||||
label="E-Mail"
|
||||
label={this.props.t ? this.props.t('auth.email') : 'E-Mail'}
|
||||
type="email"
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
@@ -665,7 +692,7 @@ export class LoginComponent extends Component {
|
||||
|
||||
<TextField
|
||||
margin="dense"
|
||||
label="Passwort"
|
||||
label={this.props.t ? this.props.t('auth.password') : 'Passwort'}
|
||||
type="password"
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
@@ -687,7 +714,7 @@ export class LoginComponent extends Component {
|
||||
'&:hover': { backgroundColor: 'transparent', textDecoration: 'underline' }
|
||||
}}
|
||||
>
|
||||
Passwort vergessen?
|
||||
{this.props.t ? this.props.t('auth.forgotPassword') : 'Passwort vergessen?'}
|
||||
</Button>
|
||||
</Box>
|
||||
)}
|
||||
@@ -717,7 +744,7 @@ export class LoginComponent extends Component {
|
||||
onClick={tabValue === 0 ? this.handleLogin : this.handleRegister}
|
||||
sx={{ mt: 2, bgcolor: '#2e7d32', '&:hover': { bgcolor: '#1b5e20' } }}
|
||||
>
|
||||
{tabValue === 0 ? 'ANMELDEN' : 'REGISTRIEREN'}
|
||||
{tabValue === 0 ? (this.props.t ? this.props.t('auth.login').toUpperCase() : 'ANMELDEN') : (this.props.t ? this.props.t('auth.register').toUpperCase() : 'REGISTRIEREN')}
|
||||
</Button>
|
||||
)}
|
||||
</Box>
|
||||
@@ -740,4 +767,4 @@ export class LoginComponent extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
export default withRouter(LoginComponent);
|
||||
export default withRouter(withI18n()(LoginComponent));
|
||||
407
src/components/MainPageLayout.js
Normal file
407
src/components/MainPageLayout.js
Normal file
@@ -0,0 +1,407 @@
|
||||
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();
|
||||
|
||||
// Determine which page we're on
|
||||
const isHome = currentPath === "/";
|
||||
const isAktionen = currentPath === "/aktionen";
|
||||
const isFiliale = currentPath === "/filiale";
|
||||
|
||||
// Get navigation config based on current page
|
||||
const getNavigationConfig = () => {
|
||||
if (isHome) {
|
||||
return {
|
||||
leftNav: { text: 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%" }}>
|
||||
<div className={`animated-border-card ${index === 0 ? 'seeds-card' : 'cutlings-card'}`}>
|
||||
<Paper
|
||||
component={Link}
|
||||
to={box.link}
|
||||
sx={{
|
||||
p: 0,
|
||||
textDecoration: "none",
|
||||
color: "text.primary",
|
||||
borderRadius: 2,
|
||||
overflow: "hidden",
|
||||
height: { xs: 150, sm: 200, md: 300 },
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
boxShadow: 10,
|
||||
transition: "all 0.3s ease",
|
||||
"&:hover": {
|
||||
transform: "translateY(-5px)",
|
||||
boxShadow: 20,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
height: "100%",
|
||||
bgcolor: box.bgcolor,
|
||||
backgroundImage: `url("${box.image}")`,
|
||||
backgroundSize: "contain",
|
||||
backgroundPosition: "center",
|
||||
backgroundRepeat: "no-repeat",
|
||||
position: "relative",
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
position: "absolute",
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bgcolor: "rgba(27, 94, 32, 0.8)",
|
||||
p: 1,
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
sx={{
|
||||
fontSize: "1.6rem",
|
||||
color: "white",
|
||||
fontFamily: "SwashingtonCP",
|
||||
}}
|
||||
>
|
||||
{box.title}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
</Paper>
|
||||
</div>
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
))}
|
||||
</Box>
|
||||
|
||||
{/* Shared Carousel */}
|
||||
<SharedCarousel />
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
export default MainPageLayout;
|
||||
@@ -1,381 +0,0 @@
|
||||
import React, { Component, useState } from "react";
|
||||
import { Button, Box, Typography, CircularProgress } from "@mui/material";
|
||||
import config from "../config.js";
|
||||
|
||||
// Function to lazy load Mollie script
|
||||
const loadMollie = () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
// Check if Mollie is already loaded
|
||||
if (window.Mollie) {
|
||||
resolve(window.Mollie);
|
||||
return;
|
||||
}
|
||||
|
||||
// Create script element
|
||||
const script = document.createElement('script');
|
||||
script.src = 'https://js.mollie.com/v1/mollie.js';
|
||||
script.async = true;
|
||||
|
||||
script.onload = () => {
|
||||
if (window.Mollie) {
|
||||
resolve(window.Mollie);
|
||||
} else {
|
||||
reject(new Error('Mollie failed to load'));
|
||||
}
|
||||
};
|
||||
|
||||
script.onerror = () => {
|
||||
reject(new Error('Failed to load Mollie script'));
|
||||
};
|
||||
|
||||
document.head.appendChild(script);
|
||||
});
|
||||
};
|
||||
|
||||
const CheckoutForm = ({ mollie }) => {
|
||||
const [errorMessage, setErrorMessage] = useState(null);
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!mollie) return;
|
||||
|
||||
let mountedComponents = {
|
||||
cardNumber: null,
|
||||
cardHolder: null,
|
||||
expiryDate: null,
|
||||
verificationCode: null
|
||||
};
|
||||
|
||||
try {
|
||||
// Create Mollie components
|
||||
const cardNumber = mollie.createComponent('cardNumber');
|
||||
const cardHolder = mollie.createComponent('cardHolder');
|
||||
const expiryDate = mollie.createComponent('expiryDate');
|
||||
const verificationCode = mollie.createComponent('verificationCode');
|
||||
|
||||
// Store references for cleanup
|
||||
mountedComponents = {
|
||||
cardNumber,
|
||||
cardHolder,
|
||||
expiryDate,
|
||||
verificationCode
|
||||
};
|
||||
|
||||
// Mount components
|
||||
cardNumber.mount('#card-number');
|
||||
cardHolder.mount('#card-holder');
|
||||
expiryDate.mount('#expiry-date');
|
||||
verificationCode.mount('#verification-code');
|
||||
|
||||
// Set up error handling
|
||||
cardNumber.addEventListener('change', event => {
|
||||
const errorElement = document.querySelector('#card-number-error');
|
||||
if (errorElement) {
|
||||
if (event.error && event.touched) {
|
||||
errorElement.textContent = event.error;
|
||||
} else {
|
||||
errorElement.textContent = '';
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
cardHolder.addEventListener('change', event => {
|
||||
const errorElement = document.querySelector('#card-holder-error');
|
||||
if (errorElement) {
|
||||
if (event.error && event.touched) {
|
||||
errorElement.textContent = event.error;
|
||||
} else {
|
||||
errorElement.textContent = '';
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
expiryDate.addEventListener('change', event => {
|
||||
const errorElement = document.querySelector('#expiry-date-error');
|
||||
if (errorElement) {
|
||||
if (event.error && event.touched) {
|
||||
errorElement.textContent = event.error;
|
||||
} else {
|
||||
errorElement.textContent = '';
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
verificationCode.addEventListener('change', event => {
|
||||
const errorElement = document.querySelector('#verification-code-error');
|
||||
if (errorElement) {
|
||||
if (event.error && event.touched) {
|
||||
errorElement.textContent = event.error;
|
||||
} else {
|
||||
errorElement.textContent = '';
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Components are now mounted and ready
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error creating Mollie components:', error);
|
||||
setErrorMessage('Failed to initialize payment form. Please try again.');
|
||||
}
|
||||
|
||||
// Cleanup function
|
||||
return () => {
|
||||
try {
|
||||
if (mountedComponents.cardNumber) mountedComponents.cardNumber.unmount();
|
||||
if (mountedComponents.cardHolder) mountedComponents.cardHolder.unmount();
|
||||
if (mountedComponents.expiryDate) mountedComponents.expiryDate.unmount();
|
||||
if (mountedComponents.verificationCode) mountedComponents.verificationCode.unmount();
|
||||
} catch (error) {
|
||||
console.error('Error cleaning up Mollie components:', error);
|
||||
}
|
||||
};
|
||||
}, [mollie]);
|
||||
|
||||
const handleSubmit = async (event) => {
|
||||
event.preventDefault();
|
||||
|
||||
if (!mollie || isSubmitting) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsSubmitting(true);
|
||||
setErrorMessage(null);
|
||||
|
||||
try {
|
||||
const { token, error } = await mollie.createToken();
|
||||
|
||||
if (error) {
|
||||
setErrorMessage(error.message || 'Payment failed. Please try again.');
|
||||
setIsSubmitting(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (token) {
|
||||
// Handle successful token creation
|
||||
// Create a payment completion event similar to Stripe
|
||||
const mollieCompletionData = {
|
||||
mollieToken: token,
|
||||
paymentMethod: 'mollie'
|
||||
};
|
||||
|
||||
// Dispatch a custom event to notify the parent component
|
||||
const completionEvent = new CustomEvent('molliePaymentComplete', {
|
||||
detail: mollieCompletionData
|
||||
});
|
||||
window.dispatchEvent(completionEvent);
|
||||
|
||||
// For now, redirect to profile with completion data
|
||||
const returnUrl = `${window.location.origin}/profile?complete&mollie_token=${token}`;
|
||||
window.location.href = returnUrl;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error creating Mollie token:', error);
|
||||
setErrorMessage('Payment failed. Please try again.');
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Box sx={{ maxWidth: 600, mx: 'auto', p: 3 }}>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
Kreditkarte oder Sofortüberweisung
|
||||
</Typography>
|
||||
|
||||
<form onSubmit={handleSubmit}>
|
||||
<Box sx={{ mb: 3 }}>
|
||||
<Typography variant="body2" gutterBottom>
|
||||
Kartennummer
|
||||
</Typography>
|
||||
<Box
|
||||
id="card-number"
|
||||
sx={{
|
||||
p: 2,
|
||||
border: '1px solid #e0e0e0',
|
||||
borderRadius: 1,
|
||||
minHeight: 40,
|
||||
backgroundColor: '#fff'
|
||||
}}
|
||||
/>
|
||||
<Typography
|
||||
id="card-number-error"
|
||||
variant="caption"
|
||||
sx={{ color: 'error.main', minHeight: 16, display: 'block' }}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ mb: 3 }}>
|
||||
<Typography variant="body2" gutterBottom>
|
||||
Karteninhaber
|
||||
</Typography>
|
||||
<Box
|
||||
id="card-holder"
|
||||
sx={{
|
||||
p: 2,
|
||||
border: '1px solid #e0e0e0',
|
||||
borderRadius: 1,
|
||||
minHeight: 40,
|
||||
backgroundColor: '#fff'
|
||||
}}
|
||||
/>
|
||||
<Typography
|
||||
id="card-holder-error"
|
||||
variant="caption"
|
||||
sx={{ color: 'error.main', minHeight: 16, display: 'block' }}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ display: 'flex', gap: 2, mb: 3 }}>
|
||||
<Box sx={{ flex: 1 }}>
|
||||
<Typography variant="body2" gutterBottom>
|
||||
Ablaufdatum
|
||||
</Typography>
|
||||
<Box
|
||||
id="expiry-date"
|
||||
sx={{
|
||||
p: 2,
|
||||
border: '1px solid #e0e0e0',
|
||||
borderRadius: 1,
|
||||
minHeight: 40,
|
||||
backgroundColor: '#fff'
|
||||
}}
|
||||
/>
|
||||
<Typography
|
||||
id="expiry-date-error"
|
||||
variant="caption"
|
||||
sx={{ color: 'error.main', minHeight: 16, display: 'block' }}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ flex: 1 }}>
|
||||
<Typography variant="body2" gutterBottom>
|
||||
Sicherheitscode
|
||||
</Typography>
|
||||
<Box
|
||||
id="verification-code"
|
||||
sx={{
|
||||
p: 2,
|
||||
border: '1px solid #e0e0e0',
|
||||
borderRadius: 1,
|
||||
minHeight: 40,
|
||||
backgroundColor: '#fff'
|
||||
}}
|
||||
/>
|
||||
<Typography
|
||||
id="verification-code-error"
|
||||
variant="caption"
|
||||
sx={{ color: 'error.main', minHeight: 16, display: 'block' }}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Button
|
||||
variant="contained"
|
||||
disabled={!mollie || isSubmitting}
|
||||
type="submit"
|
||||
fullWidth
|
||||
sx={{
|
||||
mt: 2,
|
||||
backgroundColor: '#2e7d32',
|
||||
'&:hover': {
|
||||
backgroundColor: '#1b5e20'
|
||||
}
|
||||
}}
|
||||
>
|
||||
{isSubmitting ? (
|
||||
<>
|
||||
<CircularProgress size={20} sx={{ mr: 1, color: 'white' }} />
|
||||
Verarbeitung...
|
||||
</>
|
||||
) : (
|
||||
'Bezahlung Abschließen'
|
||||
)}
|
||||
</Button>
|
||||
|
||||
{errorMessage && (
|
||||
<Typography
|
||||
variant="body2"
|
||||
sx={{ color: 'error.main', mt: 2, textAlign: 'center' }}
|
||||
>
|
||||
{errorMessage}
|
||||
</Typography>
|
||||
)}
|
||||
</form>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
class Mollie extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
mollie: null,
|
||||
loading: true,
|
||||
error: null,
|
||||
};
|
||||
this.molliePromise = loadMollie();
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.molliePromise
|
||||
.then((MollieClass) => {
|
||||
try {
|
||||
// Initialize Mollie with profile key
|
||||
const mollie = MollieClass(config.mollieProfileKey, {
|
||||
locale: 'de_DE',
|
||||
testmode: true // Set to false for production
|
||||
});
|
||||
this.setState({ mollie, loading: false });
|
||||
} catch (error) {
|
||||
console.error('Error initializing Mollie:', error);
|
||||
this.setState({
|
||||
error: 'Failed to initialize payment system. Please try again.',
|
||||
loading: false
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Error loading Mollie:', error);
|
||||
this.setState({
|
||||
error: 'Failed to load payment system. Please try again.',
|
||||
loading: false
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const { mollie, loading, error } = this.state;
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<Box sx={{ textAlign: 'center', py: 4 }}>
|
||||
<CircularProgress sx={{ color: '#2e7d32' }} />
|
||||
<Typography variant="body1" sx={{ mt: 2 }}>
|
||||
Zahlungskomponente wird geladen...
|
||||
</Typography>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<Box sx={{ textAlign: 'center', py: 4 }}>
|
||||
<Typography variant="body1" sx={{ color: 'error.main' }}>
|
||||
{error}
|
||||
</Typography>
|
||||
<Button
|
||||
variant="outlined"
|
||||
onClick={() => window.location.reload()}
|
||||
sx={{ mt: 2 }}
|
||||
>
|
||||
Seite neu laden
|
||||
</Button>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return <CheckoutForm mollie={mollie} />;
|
||||
}
|
||||
}
|
||||
|
||||
export default Mollie;
|
||||
168
src/components/PaymentSuccess.js
Normal file
168
src/components/PaymentSuccess.js
Normal file
@@ -0,0 +1,168 @@
|
||||
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 to cart tab
|
||||
profileUrl.hash = '#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;
|
||||
@@ -68,8 +68,8 @@ class Product extends Component {
|
||||
render() {
|
||||
const {
|
||||
id, name, price, available, manufacturer, seoName,
|
||||
currency, vat, massMenge, massEinheit, thc,
|
||||
floweringWeeks,incoming, neu, weight, versandklasse, availableSupplier
|
||||
currency, vat, cGrundEinheit, fGrundPreis, thc,
|
||||
floweringWeeks,incoming, neu, weight, versandklasse, availableSupplier, komponenten
|
||||
} = this.props;
|
||||
|
||||
const isNew = neu && (new Date().getTime() - new Date(neu).getTime() < 30 * 24 * 60 * 60 * 1000);
|
||||
@@ -173,7 +173,7 @@ class Product extends Component {
|
||||
zIndex: 1000
|
||||
}}
|
||||
>
|
||||
NEU
|
||||
{this.props.t ? this.props.t('product.new') : 'NEU'}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@@ -240,7 +240,7 @@ class Product extends Component {
|
||||
transformOrigin: 'top left'
|
||||
}}
|
||||
>
|
||||
{floweringWeeks} Wochen
|
||||
{floweringWeeks} {this.props.t ? this.props.t('product.weeks') : 'Wochen'}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -341,8 +341,8 @@ class Product extends Component {
|
||||
|
||||
|
||||
</Typography>
|
||||
{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})
|
||||
{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})
|
||||
</Typography> )}
|
||||
</div>
|
||||
{/*incoming*/}
|
||||
@@ -358,7 +358,7 @@ class Product extends Component {
|
||||
>
|
||||
<ZoomInIcon />
|
||||
</IconButton>
|
||||
<AddToCartButton cartButton={true} availableSupplier={availableSupplier} available={available} incoming={incoming} seoName={seoName} pictureList={this.props.pictureList} id={id} price={price} vat={vat} weight={weight} name={name} versandklasse={versandklasse}/>
|
||||
<AddToCartButton cartButton={true} availableSupplier={availableSupplier} komponenten={komponenten} cGrundEinheit={cGrundEinheit} fGrundPreis={fGrundPreis} available={available} incoming={incoming} seoName={seoName} pictureList={this.props.pictureList} id={id} price={price} vat={vat} weight={weight} name={name} versandklasse={versandklasse}/>
|
||||
</Box>
|
||||
</Card>
|
||||
</Box>
|
||||
|
||||
@@ -29,6 +29,12 @@ 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
|
||||
};
|
||||
} else {
|
||||
this.state = {
|
||||
@@ -39,6 +45,12 @@ 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
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -64,6 +76,248 @@ class ProductDetailPage extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
loadKomponentImage = (komponentId, pictureList) => {
|
||||
// Initialize cache if it doesn't exist
|
||||
if (!window.smallPicCache) {
|
||||
window.smallPicCache = {};
|
||||
}
|
||||
|
||||
// Skip if no pictureList
|
||||
if (!pictureList || pictureList.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the first image ID from pictureList
|
||||
const bildId = pictureList.split(',')[0];
|
||||
|
||||
// Check if already cached
|
||||
if (window.smallPicCache[bildId]) {
|
||||
this.setState(prevState => ({
|
||||
komponentenImages: {
|
||||
...prevState.komponentenImages,
|
||||
[komponentId]: window.smallPicCache[bildId]
|
||||
}
|
||||
}));
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if socketB is available
|
||||
if (!this.props.socketB || !this.props.socketB.connected) {
|
||||
console.log("SocketB not connected yet, skipping image load for komponent:", komponentId);
|
||||
return;
|
||||
}
|
||||
|
||||
// Fetch image from server
|
||||
this.props.socketB.emit('getPic', { bildId, size: 'small' }, (res) => {
|
||||
if (res.success) {
|
||||
// Cache the image
|
||||
window.smallPicCache[bildId] = URL.createObjectURL(new Blob([res.imageBuffer], { type: 'image/jpeg' }));
|
||||
|
||||
// Update state
|
||||
this.setState(prevState => ({
|
||||
komponentenImages: {
|
||||
...prevState.komponentenImages,
|
||||
[komponentId]: window.smallPicCache[bildId]
|
||||
}
|
||||
}));
|
||||
} else {
|
||||
console.log('Error loading komponent image:', res);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
loadKomponent = (id, count) => {
|
||||
// Initialize cache if it doesn't exist
|
||||
if (!window.productDetailCache) {
|
||||
window.productDetailCache = {};
|
||||
}
|
||||
|
||||
// Check if this komponent is already cached
|
||||
if (window.productDetailCache[id]) {
|
||||
const cachedProduct = window.productDetailCache[id];
|
||||
|
||||
// Load komponent image if available
|
||||
if (cachedProduct.pictureList) {
|
||||
this.loadKomponentImage(id, cachedProduct.pictureList);
|
||||
}
|
||||
|
||||
// Update state with cached data
|
||||
this.setState(prevState => {
|
||||
const newKomponentenData = {
|
||||
...prevState.komponentenData,
|
||||
[id]: {
|
||||
...cachedProduct,
|
||||
count: parseInt(count),
|
||||
loaded: true
|
||||
}
|
||||
};
|
||||
|
||||
// Check if all remaining komponenten are loaded
|
||||
const allLoaded = prevState.komponenten.every(k =>
|
||||
newKomponentenData[k.id] && newKomponentenData[k.id].loaded
|
||||
);
|
||||
|
||||
// Calculate totals if all loaded
|
||||
let totalKomponentenPrice = 0;
|
||||
let totalSavings = 0;
|
||||
|
||||
if (allLoaded) {
|
||||
totalKomponentenPrice = prevState.komponenten.reduce((sum, k) => {
|
||||
const komponentData = newKomponentenData[k.id];
|
||||
if (komponentData && komponentData.loaded) {
|
||||
return sum + (komponentData.price * parseInt(k.count));
|
||||
}
|
||||
return sum;
|
||||
}, 0);
|
||||
|
||||
// Calculate savings (difference between buying individually vs as set)
|
||||
const setPrice = prevState.product ? prevState.product.price : 0;
|
||||
totalSavings = Math.max(0, totalKomponentenPrice - setPrice);
|
||||
}
|
||||
|
||||
console.log("Cached komponent loaded:", id, "data:", newKomponentenData[id]);
|
||||
console.log("All loaded (cached):", allLoaded);
|
||||
|
||||
return {
|
||||
komponentenData: newKomponentenData,
|
||||
komponentenLoaded: allLoaded,
|
||||
totalKomponentenPrice,
|
||||
totalSavings
|
||||
};
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// If not cached, fetch from server (similar to loadProductData)
|
||||
if (!this.props.socket || !this.props.socket.connected) {
|
||||
console.log("Socket not connected yet, waiting for connection to load komponent data");
|
||||
return;
|
||||
}
|
||||
|
||||
// Mark this komponent as loading
|
||||
this.setState(prevState => ({
|
||||
komponentenData: {
|
||||
...prevState.komponentenData,
|
||||
[id]: {
|
||||
...prevState.komponentenData[id],
|
||||
loading: true,
|
||||
loaded: false,
|
||||
count: parseInt(count)
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
this.props.socket.emit(
|
||||
"getProductView",
|
||||
{ articleId: id },
|
||||
(res) => {
|
||||
if (res.success) {
|
||||
// Cache the successful response
|
||||
window.productDetailCache[id] = res.product;
|
||||
|
||||
// Load komponent image if available
|
||||
if (res.product.pictureList) {
|
||||
this.loadKomponentImage(id, res.product.pictureList);
|
||||
}
|
||||
|
||||
// Update state with loaded data
|
||||
this.setState(prevState => {
|
||||
const newKomponentenData = {
|
||||
...prevState.komponentenData,
|
||||
[id]: {
|
||||
...res.product,
|
||||
count: parseInt(count),
|
||||
loading: false,
|
||||
loaded: true
|
||||
}
|
||||
};
|
||||
|
||||
// Check if all remaining komponenten are loaded
|
||||
const allLoaded = prevState.komponenten.every(k =>
|
||||
newKomponentenData[k.id] && newKomponentenData[k.id].loaded
|
||||
);
|
||||
|
||||
// Calculate totals if all loaded
|
||||
let totalKomponentenPrice = 0;
|
||||
let totalSavings = 0;
|
||||
|
||||
if (allLoaded) {
|
||||
totalKomponentenPrice = prevState.komponenten.reduce((sum, k) => {
|
||||
const komponentData = newKomponentenData[k.id];
|
||||
if (komponentData && komponentData.loaded) {
|
||||
return sum + (komponentData.price * parseInt(k.count));
|
||||
}
|
||||
return sum;
|
||||
}, 0);
|
||||
|
||||
// Calculate savings (difference between buying individually vs as set)
|
||||
const setPrice = prevState.product ? prevState.product.price : 0;
|
||||
totalSavings = Math.max(0, totalKomponentenPrice - setPrice);
|
||||
}
|
||||
|
||||
console.log("Updated komponentenData for", id, ":", newKomponentenData[id]);
|
||||
console.log("All loaded:", allLoaded);
|
||||
|
||||
return {
|
||||
komponentenData: newKomponentenData,
|
||||
komponentenLoaded: allLoaded,
|
||||
totalKomponentenPrice,
|
||||
totalSavings
|
||||
};
|
||||
});
|
||||
|
||||
console.log("getProductView (komponent)", res);
|
||||
} else {
|
||||
console.error("Error loading komponent:", res.error || "Unknown error", res);
|
||||
|
||||
// Remove failed komponent from the list and check if all remaining are loaded
|
||||
this.setState(prevState => {
|
||||
const newKomponenten = prevState.komponenten.filter(k => k.id !== id);
|
||||
const newKomponentenData = { ...prevState.komponentenData };
|
||||
|
||||
// Remove failed komponent from data
|
||||
delete newKomponentenData[id];
|
||||
|
||||
// Check if all remaining komponenten are loaded
|
||||
const allLoaded = newKomponenten.length === 0 || newKomponenten.every(k =>
|
||||
newKomponentenData[k.id] && newKomponentenData[k.id].loaded
|
||||
);
|
||||
|
||||
// Calculate totals if all loaded
|
||||
let totalKomponentenPrice = 0;
|
||||
let totalSavings = 0;
|
||||
|
||||
if (allLoaded && newKomponenten.length > 0) {
|
||||
totalKomponentenPrice = newKomponenten.reduce((sum, k) => {
|
||||
const komponentData = newKomponentenData[k.id];
|
||||
if (komponentData && komponentData.loaded) {
|
||||
return sum + (komponentData.price * parseInt(k.count));
|
||||
}
|
||||
return sum;
|
||||
}, 0);
|
||||
|
||||
// Calculate savings (difference between buying individually vs as set)
|
||||
const setPrice = this.state.product ? this.state.product.price : 0;
|
||||
totalSavings = Math.max(0, totalKomponentenPrice - setPrice);
|
||||
}
|
||||
|
||||
console.log("Removed failed komponent:", id, "remaining:", newKomponenten.length);
|
||||
console.log("All loaded after removal:", allLoaded);
|
||||
|
||||
return {
|
||||
komponenten: newKomponenten,
|
||||
komponentenData: newKomponentenData,
|
||||
komponentenLoaded: allLoaded,
|
||||
totalKomponentenPrice,
|
||||
totalSavings
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
loadProductData = () => {
|
||||
if (!this.props.socket || !this.props.socket.connected) {
|
||||
// Socket not connected yet, but don't show error immediately on first load
|
||||
@@ -78,12 +332,37 @@ 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
|
||||
attributes: res.attributes,
|
||||
komponenten: komponenten,
|
||||
komponentenLoaded: komponenten.length === 0 // If no komponenten, mark as loaded
|
||||
}, () => {
|
||||
if(komponenten.length > 0) {
|
||||
for(const komponent of komponenten) {
|
||||
this.loadKomponent(komponent.id, komponent.count);
|
||||
}
|
||||
}
|
||||
});
|
||||
console.log("getProductView", res);
|
||||
|
||||
@@ -180,7 +459,7 @@ class ProductDetailPage extends Component {
|
||||
};
|
||||
|
||||
render() {
|
||||
const { product, loading, error, attributeImages, isSteckling, attributes } =
|
||||
const { product, loading, error, attributeImages, isSteckling, attributes, komponentenLoaded, komponentenData, komponentenImages, totalKomponentenPrice, totalSavings } =
|
||||
this.state;
|
||||
|
||||
if (loading) {
|
||||
@@ -211,7 +490,7 @@ class ProductDetailPage extends Component {
|
||||
<Typography>{error}</Typography>
|
||||
<Link to="/" style={{ textDecoration: "none" }}>
|
||||
<Typography color="primary" sx={{ mt: 2 }}>
|
||||
Zurück zur Startseite
|
||||
{this.props.t ? this.props.t('product.backToHome') : 'Zurück zur Startseite'}
|
||||
</Typography>
|
||||
</Link>
|
||||
</Box>
|
||||
@@ -229,7 +508,7 @@ class ProductDetailPage extends Component {
|
||||
</Typography>
|
||||
<Link to="/" style={{ textDecoration: "none" }}>
|
||||
<Typography color="primary" sx={{ mt: 2 }}>
|
||||
Zurück zur Startseite
|
||||
{this.props.t ? this.props.t('product.backToHome') : 'Zurück zur Startseite'}
|
||||
</Typography>
|
||||
</Link>
|
||||
</Box>
|
||||
@@ -294,7 +573,7 @@ class ProductDetailPage extends Component {
|
||||
fontWeight: "bold",
|
||||
}}
|
||||
>
|
||||
Zurück
|
||||
{this.props.t ? this.props.t('common.back') : 'Zurück'}
|
||||
</Link>
|
||||
</Typography>
|
||||
</Box>
|
||||
@@ -355,7 +634,7 @@ class ProductDetailPage extends Component {
|
||||
{/* Product identifiers */}
|
||||
<Box sx={{ mb: 1 }}>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
Artikelnummer: {product.articleNumber} {product.gtin ? ` | GTIN: ${product.gtin}` : ""}
|
||||
{this.props.t ? this.props.t('product.articleNumber') : 'Artikelnummer'}: {product.articleNumber} {product.gtin ? ` | GTIN: ${product.gtin}` : ""}
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
@@ -373,7 +652,7 @@ class ProductDetailPage extends Component {
|
||||
{product.manufacturer && (
|
||||
<Box sx={{ display: "flex", alignItems: "center", mb: 2 }}>
|
||||
<Typography variant="body2" sx={{ fontStyle: "italic" }}>
|
||||
Hersteller: {product.manufacturer}
|
||||
{this.props.t ? this.props.t('product.manufacturer') : 'Hersteller'}: {product.manufacturer}
|
||||
</Typography>
|
||||
</Box>
|
||||
)}
|
||||
@@ -386,18 +665,12 @@ class ProductDetailPage extends Component {
|
||||
.map((attribute) => {
|
||||
const key = attribute.kMerkmalWert;
|
||||
return (
|
||||
<Box key={key} sx={{ mb: 1 }}>
|
||||
<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}`}
|
||||
sx={{
|
||||
maxWidth: "100px",
|
||||
maxHeight: "100px",
|
||||
objectFit: "contain",
|
||||
border: "1px solid #e0e0e0",
|
||||
borderRadius: 1,
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
@@ -452,7 +725,11 @@ class ProductDetailPage extends Component {
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
inkl. {product.vat}% MwSt.
|
||||
{product.cGrundEinheit && product.fGrundPreis && (
|
||||
<>; {new Intl.NumberFormat('de-DE', {style: 'currency', currency: 'EUR'}).format(product.fGrundPreis)}/{product.cGrundEinheit}</>
|
||||
)}
|
||||
</Typography>
|
||||
|
||||
{product.versandklasse &&
|
||||
product.versandklasse != "standard" &&
|
||||
product.versandklasse != "kostenlos" && (
|
||||
@@ -461,6 +738,37 @@ 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"
|
||||
}}
|
||||
>
|
||||
Sie sparen: {new Intl.NumberFormat("de-DE", {
|
||||
style: "currency",
|
||||
currency: "EUR",
|
||||
}).format(totalKomponentenPrice - product.price)}
|
||||
</Typography>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
Günstiger als Einzelkauf
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
@@ -487,6 +795,7 @@ 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"}
|
||||
/>
|
||||
@@ -516,12 +825,16 @@ 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={{
|
||||
@@ -565,6 +878,206 @@ class ProductDetailPage extends Component {
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{product.komponenten && product.komponenten.split(",").length > 0 && (
|
||||
<Box sx={{ mt: 4, p: 4, background: "#fff", borderRadius: 2, boxShadow: "0 2px 8px rgba(0,0,0,0.08)" }}>
|
||||
<Typography variant="h4" gutterBottom>Bestehend aus:</Typography>
|
||||
<Box sx={{ maxWidth: 800, mx: "auto" }}>
|
||||
|
||||
{(console.log("komponentenLoaded:", komponentenLoaded), komponentenLoaded) ? (
|
||||
<>
|
||||
{console.log("Rendering loaded komponenten:", this.state.komponenten.length, "komponentenData:", Object.keys(komponentenData).length)}
|
||||
{this.state.komponenten.map((komponent, index) => {
|
||||
const komponentData = komponentenData[komponent.id];
|
||||
console.log(`Rendering komponent ${komponent.id}:`, komponentData);
|
||||
|
||||
// Don't show border on last item (pricing section has its own top border)
|
||||
const isLastItem = index === this.state.komponenten.length - 1;
|
||||
const showBorder = !isLastItem;
|
||||
|
||||
if (!komponentData || !komponentData.loaded) {
|
||||
return (
|
||||
<Box key={komponent.id} sx={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
py: 1,
|
||||
borderBottom: showBorder ? "1px solid #eee" : "none",
|
||||
minHeight: "70px" // Consistent height to prevent layout shifts
|
||||
}}>
|
||||
<Box sx={{ display: "flex", alignItems: "center", gap: 2 }}>
|
||||
<Box sx={{ width: 50, height: 50, flexShrink: 0, backgroundColor: "#f5f5f5", borderRadius: 1, border: "1px solid #e0e0e0" }}>
|
||||
{/* Empty placeholder for image */}
|
||||
</Box>
|
||||
<Box>
|
||||
<Typography variant="body1">
|
||||
{index + 1}. Lädt...
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{komponent.count}x
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
<Typography variant="body1" sx={{ fontWeight: 500 }}>
|
||||
-
|
||||
</Typography>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
const itemPrice = komponentData.price * parseInt(komponent.count);
|
||||
const formattedPrice = new Intl.NumberFormat("de-DE", {
|
||||
style: "currency",
|
||||
currency: "EUR",
|
||||
}).format(itemPrice);
|
||||
|
||||
return (
|
||||
<Box
|
||||
key={komponent.id}
|
||||
component={Link}
|
||||
to={`/Artikel/${komponentData.seoName}`}
|
||||
sx={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
py: 1,
|
||||
borderBottom: showBorder ? "1px solid #eee" : "none",
|
||||
textDecoration: "none",
|
||||
color: "inherit",
|
||||
minHeight: "70px", // Consistent height to prevent layout shifts
|
||||
"&:hover": {
|
||||
backgroundColor: "#f5f5f5"
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Box sx={{ display: "flex", alignItems: "center", gap: 2 }}>
|
||||
<Box sx={{ width: 50, height: 50, flexShrink: 0 }}>
|
||||
{komponentenImages[komponent.id] ? (
|
||||
<CardMedia
|
||||
component="img"
|
||||
height="50"
|
||||
image={komponentenImages[komponent.id]}
|
||||
alt={komponentData.name}
|
||||
sx={{
|
||||
objectFit: "contain",
|
||||
borderRadius: 1,
|
||||
border: "1px solid #e0e0e0"
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<CardMedia
|
||||
component="img"
|
||||
height="50"
|
||||
image="/assets/images/nopicture.jpg"
|
||||
alt={komponentData.name}
|
||||
sx={{
|
||||
objectFit: "contain",
|
||||
borderRadius: 1,
|
||||
border: "1px solid #e0e0e0"
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
<Box>
|
||||
<Typography variant="body1" sx={{ fontWeight: 500 }}>
|
||||
{index + 1}. {cleanProductName(komponentData.name)}
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{komponent.count}x à {new Intl.NumberFormat("de-DE", {
|
||||
style: "currency",
|
||||
currency: "EUR",
|
||||
}).format(komponentData.price)}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
<Typography variant="body1" sx={{ fontWeight: 500 }}>
|
||||
{formattedPrice}
|
||||
</Typography>
|
||||
</Box>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* Total price and savings display - only show when prices differ meaningfully */}
|
||||
{totalKomponentenPrice > product.price &&
|
||||
(totalKomponentenPrice - product.price >= 2 &&
|
||||
(totalKomponentenPrice - product.price) / product.price >= 0.02) && (
|
||||
<Box sx={{ mt: 3, pt: 2, borderTop: "2px solid #eee" }}>
|
||||
<Box sx={{ display: "flex", justifyContent: "space-between", alignItems: "center", mb: 1 }}>
|
||||
<Typography variant="h6">
|
||||
Einzelpreis gesamt:
|
||||
</Typography>
|
||||
<Typography variant="h6" sx={{ textDecoration: "line-through", color: "text.secondary" }}>
|
||||
{new Intl.NumberFormat("de-DE", {
|
||||
style: "currency",
|
||||
currency: "EUR",
|
||||
}).format(totalKomponentenPrice)}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box sx={{ display: "flex", justifyContent: "space-between", alignItems: "center", mb: 1 }}>
|
||||
<Typography variant="h6">
|
||||
Set-Preis:
|
||||
</Typography>
|
||||
<Typography variant="h6" color="primary" sx={{ fontWeight: "bold" }}>
|
||||
{new Intl.NumberFormat("de-DE", {
|
||||
style: "currency",
|
||||
currency: "EUR",
|
||||
}).format(product.price)}
|
||||
</Typography>
|
||||
</Box>
|
||||
{totalSavings > 0 && (
|
||||
<Box sx={{ display: "flex", justifyContent: "space-between", alignItems: "center", mt: 2, p: 2, backgroundColor: "#e8f5e8", borderRadius: 1 }}>
|
||||
<Typography variant="h6" color="success.main" sx={{ fontWeight: "bold" }}>
|
||||
Ihre Ersparnis:
|
||||
</Typography>
|
||||
<Typography variant="h6" color="success.main" sx={{ fontWeight: "bold" }}>
|
||||
{new Intl.NumberFormat("de-DE", {
|
||||
style: "currency",
|
||||
currency: "EUR",
|
||||
}).format(totalSavings)}
|
||||
</Typography>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
// Loading state
|
||||
<Box>
|
||||
{this.state.komponenten.map((komponent, index) => {
|
||||
// For loading state, we don't know if pricing will be shown, so show all borders
|
||||
return (
|
||||
<Box key={komponent.id} sx={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
py: 1,
|
||||
borderBottom: "1px solid #eee",
|
||||
minHeight: "70px" // Consistent height to prevent layout shifts
|
||||
}}>
|
||||
<Box sx={{ display: "flex", alignItems: "center", gap: 2 }}>
|
||||
<Box sx={{ width: 50, height: 50, flexShrink: 0, backgroundColor: "#f5f5f5", borderRadius: 1, border: "1px solid #e0e0e0" }}>
|
||||
{/* Empty placeholder for image */}
|
||||
</Box>
|
||||
<Box>
|
||||
<Typography variant="body1">
|
||||
{index + 1}. Lädt Komponent-Details...
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{komponent.count}x
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
<Typography variant="body1" sx={{ fontWeight: 500 }}>
|
||||
-
|
||||
</Typography>
|
||||
</Box>
|
||||
);
|
||||
})}
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ 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);
|
||||
|
||||
@@ -93,14 +94,14 @@ class ProductFilters extends Component {
|
||||
}
|
||||
|
||||
_getAvailabilityValues = (products) => {
|
||||
const filters = [{id:1,name:'auf Lager'}];
|
||||
const filters = [{id:1,name: this.props.t ? this.props.t('product.inStock') : 'auf Lager'}];
|
||||
|
||||
for(const product of products){
|
||||
if(isNew(product.neu)){
|
||||
if(!filters.find(filter => filter.id == 2)) filters.push({id:2,name:'Neu'});
|
||||
if(!filters.find(filter => filter.id == 2)) filters.push({id:2,name: this.props.t ? this.props.t('product.new') : 'Neu'});
|
||||
}
|
||||
if(!product.available && product.incomingDate){
|
||||
if(!filters.find(filter => filter.id == 3)) filters.push({id:3,name:'Bald verfügbar'});
|
||||
if(!filters.find(filter => filter.id == 3)) filters.push({id:3,name: this.props.t ? this.props.t('product.comingSoon') : 'Bald verfügbar'});
|
||||
}
|
||||
}
|
||||
return filters
|
||||
@@ -193,7 +194,7 @@ class ProductFilters extends Component {
|
||||
|
||||
{this.props.products.length > 0 && (
|
||||
<><Filter
|
||||
title="Verfügbarkeit"
|
||||
title={this.props.t ? this.props.t('filters.availability') : 'Verfügbarkeit'}
|
||||
options={this.state.availabilityValues}
|
||||
searchParams={this.props.searchParams}
|
||||
products={this.props.products}
|
||||
@@ -236,7 +237,7 @@ class ProductFilters extends Component {
|
||||
{this.generateAttributeFilters()}
|
||||
|
||||
<Filter
|
||||
title="Hersteller"
|
||||
title={this.props.t ? this.props.t('filters.manufacturer') : 'Hersteller'}
|
||||
options={this.state.uniqueManufacturerArray}
|
||||
filterType="manufacturer"
|
||||
products={this.props.products}
|
||||
@@ -257,4 +258,4 @@ class ProductFilters extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
export default withRouter(ProductFilters);
|
||||
export default withRouter(withI18n()(ProductFilters));
|
||||
@@ -11,6 +11,7 @@ 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) {
|
||||
@@ -141,12 +142,12 @@ class ProductList extends Component {
|
||||
onChange={this.handlePageChange}
|
||||
color="primary"
|
||||
size={"large"}
|
||||
siblingCount={window.innerWidth < 600 ? 0 : 1}
|
||||
boundaryCount={window.innerWidth < 600 ? 1 : 1}
|
||||
hideNextButton={false}
|
||||
hidePrevButton={false}
|
||||
showFirstButton={window.innerWidth >= 600}
|
||||
showLastButton={window.innerWidth >= 600}
|
||||
siblingCount={1}
|
||||
boundaryCount={1}
|
||||
hideNextButton={true}
|
||||
hidePrevButton={true}
|
||||
showFirstButton={false}
|
||||
showLastButton={false}
|
||||
sx={{
|
||||
'& .MuiPagination-ul': {
|
||||
flexWrap: 'nowrap',
|
||||
@@ -184,7 +185,7 @@ class ProductList extends Component {
|
||||
px: 2
|
||||
}}>
|
||||
<Typography variant="h6" color="text.secondary" sx={{ textAlign: 'center' }}>
|
||||
Entferne Filter um Produkte zu sehen
|
||||
{this.props.t ? this.props.t('product.removeFiltersToSee') : 'Entferne Filter um Produkte zu sehen'}
|
||||
</Typography>
|
||||
</Box>
|
||||
);
|
||||
@@ -327,13 +328,13 @@ class ProductList extends Component {
|
||||
minWidth: { xs: 120, sm: 140 }
|
||||
}}
|
||||
>
|
||||
<InputLabel id="sort-by-label">Sortierung</InputLabel>
|
||||
<InputLabel id="sort-by-label">{this.props.t ? this.props.t('filters.sorting') : '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="Sortierung"
|
||||
label={this.props.t ? this.props.t('filters.sorting') : 'Sortierung'}
|
||||
MenuProps={{
|
||||
disableScrollLock: true,
|
||||
anchorOrigin: {
|
||||
@@ -353,10 +354,10 @@ class ProductList extends Component {
|
||||
}
|
||||
}}
|
||||
>
|
||||
<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>
|
||||
<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>
|
||||
</Select>
|
||||
</FormControl>
|
||||
|
||||
@@ -368,12 +369,12 @@ class ProductList extends Component {
|
||||
minWidth: { xs: 80, sm: 100 }
|
||||
}}
|
||||
>
|
||||
<InputLabel id="products-per-page-label">pro Seite</InputLabel>
|
||||
<InputLabel id="products-per-page-label">{this.props.t ? this.props.t('filters.perPage') : 'pro Seite'}</InputLabel>
|
||||
<Select
|
||||
labelId="products-per-page-label"
|
||||
value={this.state.itemsPerPage}
|
||||
onChange={this.handleProductsPerPageChange}
|
||||
label="pro Seite"
|
||||
label={this.props.t ? this.props.t('filters.perPage') : 'pro Seite'}
|
||||
MenuProps={{
|
||||
disableScrollLock: true,
|
||||
anchorOrigin: {
|
||||
@@ -462,8 +463,8 @@ class ProductList extends Component {
|
||||
available={product.available}
|
||||
manufacturer={product.manufacturer}
|
||||
vat={product.vat}
|
||||
massMenge={product.massMenge}
|
||||
massEinheit={product.massEinheit}
|
||||
cGrundEinheit={product.cGrundEinheit}
|
||||
fGrundPreis={product.fGrundPreis}
|
||||
incoming={product.incomingDate}
|
||||
neu={product.neu}
|
||||
thc={product.thc}
|
||||
@@ -474,6 +475,8 @@ class ProductList extends Component {
|
||||
socketB={this.props.socketB}
|
||||
pictureList={product.pictureList}
|
||||
availableSupplier={product.availableSupplier}
|
||||
komponenten={product.komponenten}
|
||||
t={this.props.t}
|
||||
/>
|
||||
</Grid>
|
||||
))}
|
||||
@@ -495,4 +498,4 @@ class ProductList extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
export default ProductList;
|
||||
export default withI18n()(ProductList);
|
||||
231
src/components/SharedCarousel.js
Normal file
231
src/components/SharedCarousel.js
Normal file
@@ -0,0 +1,231 @@
|
||||
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 = () => {
|
||||
const productCache = getProductCache();
|
||||
|
||||
if (productCache && productCache["categoryTree_209"]) {
|
||||
const cached = productCache["categoryTree_209"];
|
||||
if (cached.categoryTree) {
|
||||
return processCategoryTree(cached.categoryTree);
|
||||
}
|
||||
}
|
||||
return [];
|
||||
};
|
||||
|
||||
const SharedCarousel = () => {
|
||||
const { carouselRef, filteredCategories, setFilteredCategories, moveCarousel } = useCarousel();
|
||||
const context = useContext(SocketContext);
|
||||
const { t } = useTranslation();
|
||||
const [rootCategories, setRootCategories] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
const initialCategories = initializeCategories();
|
||||
setRootCategories(initialCategories);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
// Only fetch from socket if we don't already have categories
|
||||
if (
|
||||
rootCategories.length === 0 &&
|
||||
context && context.socket && context.socket.connected &&
|
||||
typeof window !== "undefined"
|
||||
) {
|
||||
context.socket.emit("categoryList", { categoryId: 209 }, (response) => {
|
||||
if (response && response.categoryTree) {
|
||||
// Store in cache
|
||||
try {
|
||||
if (!window.productCache) window.productCache = {};
|
||||
window.productCache["categoryTree_209"] = {
|
||||
categoryTree: response.categoryTree,
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
setRootCategories(response.categoryTree.children || []);
|
||||
}
|
||||
});
|
||||
}
|
||||
}, [context, context?.socket?.connected, rootCategories.length]);
|
||||
|
||||
useEffect(() => {
|
||||
const filtered = rootCategories.filter(
|
||||
(cat) => cat.id !== 689 && cat.id !== 706
|
||||
);
|
||||
setFilteredCategories(filtered);
|
||||
}, [rootCategories, setFilteredCategories]);
|
||||
|
||||
// Create duplicated array for seamless scrolling
|
||||
const displayCategories = [...filteredCategories, ...filteredCategories];
|
||||
|
||||
if (filteredCategories.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Box sx={{ mt: 3 }}>
|
||||
<Typography
|
||||
variant="h4"
|
||||
component="h1"
|
||||
sx={{
|
||||
mb: 2,
|
||||
fontFamily: "SwashingtonCP",
|
||||
color: "primary.main",
|
||||
textAlign: "center",
|
||||
textShadow: "3px 3px 10px rgba(0, 0, 0, 0.4)"
|
||||
}}
|
||||
>
|
||||
{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,7 +10,9 @@ 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;
|
||||
@@ -116,14 +118,14 @@ class ButtonGroup extends Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
const { socket, navigate } = this.props;
|
||||
const { socket, navigate, t } = 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
|
||||
@@ -164,7 +166,7 @@ class ButtonGroup extends Component {
|
||||
>
|
||||
<CloseIcon />
|
||||
</IconButton>
|
||||
<Typography variant="h6">Warenkorb</Typography>
|
||||
<Typography variant="h6">{t ? t('cart.title') : 'Warenkorb'}</Typography>
|
||||
</Box>
|
||||
<Divider sx={{ mb: 2 }} />
|
||||
|
||||
@@ -173,7 +175,7 @@ class ButtonGroup extends Component {
|
||||
|
||||
if (isUserLoggedIn().isLoggedIn) {
|
||||
this.toggleCart(); // Close the cart drawer
|
||||
navigate('/profile');
|
||||
navigate('/profile#cart');
|
||||
} else if (window.openLoginDrawer) {
|
||||
window.openLoginDrawer(); // Call global function to open login drawer
|
||||
this.toggleCart(); // Close the cart drawer
|
||||
@@ -189,10 +191,11 @@ class ButtonGroup extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
// Wrapper for ButtonGroup to provide navigate function
|
||||
// Wrapper for ButtonGroup to provide navigate function and translations
|
||||
const ButtonGroupWithRouter = (props) => {
|
||||
const navigate = useNavigate();
|
||||
return <ButtonGroup {...props} navigate={navigate} />;
|
||||
const ButtonGroupWithTranslation = withI18n()(ButtonGroup);
|
||||
return <ButtonGroupWithTranslation {...props} navigate={navigate} />;
|
||||
};
|
||||
|
||||
export default ButtonGroupWithRouter;
|
||||
@@ -8,6 +8,7 @@ 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) => {
|
||||
@@ -410,7 +411,7 @@ class CategoryList extends Component {
|
||||
zIndex: 2,
|
||||
}}
|
||||
>
|
||||
Startseite
|
||||
{this.props.t ? this.props.t('navigation.home') : 'Startseite'}
|
||||
</Box>
|
||||
{/* Thin text (positioned on top) */}
|
||||
<Box
|
||||
@@ -424,7 +425,7 @@ class CategoryList extends Component {
|
||||
zIndex: 1,
|
||||
}}
|
||||
>
|
||||
Startseite
|
||||
{this.props.t ? this.props.t('navigation.home') : 'Startseite'}
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
@@ -595,7 +596,10 @@ class CategoryList extends Component {
|
||||
onClick={this.handleMobileMenuToggle}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
aria-label={mobileMenuOpen ? "Kategorien schließen" : "Kategorien öffnen"}
|
||||
aria-label={this.props.t ?
|
||||
(mobileMenuOpen ? this.props.t('navigation.categoriesClose') : this.props.t('navigation.categoriesOpen')) :
|
||||
(mobileMenuOpen ? "Kategorien schließen" : "Kategorien öffnen")
|
||||
}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
@@ -607,7 +611,7 @@ class CategoryList extends Component {
|
||||
fontWeight: "bold",
|
||||
textShadow: "0 1px 2px rgba(0,0,0,0.3)"
|
||||
}}>
|
||||
Kategorien
|
||||
{this.props.t ? this.props.t('navigation.categories') : 'Kategorien'}
|
||||
</Typography>
|
||||
<Box sx={{ display: "flex", alignItems: "center" }}>
|
||||
{mobileMenuOpen ? <CloseIcon /> : <MenuIcon />}
|
||||
@@ -628,4 +632,4 @@ class CategoryList extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
export default CategoryList;
|
||||
export default withI18n()(CategoryList);
|
||||
|
||||
@@ -8,7 +8,9 @@ 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";
|
||||
|
||||
@@ -184,6 +186,15 @@ 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 () => {
|
||||
@@ -244,9 +255,23 @@ const SearchBar = () => {
|
||||
<SearchIcon />
|
||||
</InputAdornment>
|
||||
),
|
||||
endAdornment: loadingSuggestions && (
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">
|
||||
<CircularProgress size={16} />
|
||||
{loadingSuggestions && <CircularProgress size={16} />}
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={handleEnterClick}
|
||||
sx={{
|
||||
ml: loadingSuggestions ? 0.5 : 0,
|
||||
p: 0.5,
|
||||
color: "text.secondary",
|
||||
"&:hover": {
|
||||
color: "primary.main",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<KeyboardReturnIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
),
|
||||
sx: { borderRadius: 2, bgcolor: "background.paper" },
|
||||
|
||||
@@ -6,6 +6,7 @@ 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) {
|
||||
@@ -51,9 +52,6 @@ class CartTab extends Component {
|
||||
showStripePayment: false,
|
||||
StripeComponent: null,
|
||||
isLoadingStripe: false,
|
||||
showMolliePayment: false,
|
||||
MollieComponent: null,
|
||||
isLoadingMollie: false,
|
||||
showPaymentConfirmation: false,
|
||||
orderCompleted: false,
|
||||
originalCartItems: []
|
||||
@@ -119,7 +117,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"
|
||||
@@ -322,27 +320,6 @@ class CartTab extends Component {
|
||||
}
|
||||
};
|
||||
|
||||
loadMollieComponent = async () => {
|
||||
this.setState({ isLoadingMollie: true });
|
||||
|
||||
try {
|
||||
const { default: Mollie } = await import("../Mollie.js");
|
||||
this.setState({
|
||||
MollieComponent: Mollie,
|
||||
showMolliePayment: true,
|
||||
isCompletingOrder: false,
|
||||
isLoadingMollie: false,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Failed to load Mollie component:", error);
|
||||
this.setState({
|
||||
isCompletingOrder: false,
|
||||
isLoadingMollie: false,
|
||||
completionError: "Failed to load payment component. Please try again.",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
handleCompleteOrder = () => {
|
||||
this.setState({ completionError: null }); // Clear previous errors
|
||||
|
||||
@@ -387,23 +364,38 @@ class CartTab extends Component {
|
||||
this.orderService.createStripeIntent(totalAmount, this.loadStripeComponent);
|
||||
return;
|
||||
}
|
||||
// Handle Mollie payment differently
|
||||
// Handle molllie 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); // Convert to cents
|
||||
const totalAmount = Math.round((subtotal + deliveryCost) * 100) / 100;
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
@@ -441,9 +433,6 @@ class CartTab extends Component {
|
||||
showStripePayment,
|
||||
StripeComponent,
|
||||
isLoadingStripe,
|
||||
showMolliePayment,
|
||||
MollieComponent,
|
||||
isLoadingMollie,
|
||||
showPaymentConfirmation,
|
||||
orderCompleted,
|
||||
} = this.state;
|
||||
@@ -480,7 +469,7 @@ class CartTab extends Component {
|
||||
<CartDropdown
|
||||
cartItems={cartItems}
|
||||
socket={this.context.socket}
|
||||
showDetailedSummary={showStripePayment || showMolliePayment}
|
||||
showDetailedSummary={showStripePayment}
|
||||
deliveryMethod={deliveryMethod}
|
||||
deliveryCost={deliveryCost}
|
||||
/>
|
||||
@@ -488,7 +477,7 @@ class CartTab extends Component {
|
||||
|
||||
{cartItems.length > 0 && (
|
||||
<Box sx={{ mt: 3 }}>
|
||||
{isLoadingStripe || isLoadingMollie ? (
|
||||
{isLoadingStripe ? (
|
||||
<Box sx={{ textAlign: 'center', py: 4 }}>
|
||||
<Typography variant="body1">
|
||||
Zahlungskomponente wird geladen...
|
||||
@@ -509,31 +498,11 @@ class CartTab extends Component {
|
||||
}
|
||||
}}
|
||||
>
|
||||
← Zurück zur Bestellung
|
||||
{this.props.t ? this.props.t('cart.backToOrder') : '← 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}
|
||||
@@ -573,4 +542,4 @@ class CartTab extends Component {
|
||||
// Set static contextType to access the socket
|
||||
CartTab.contextType = SocketContext;
|
||||
|
||||
export default CartTab;
|
||||
export default withI18n()(CartTab);
|
||||
|
||||
@@ -93,6 +93,7 @@ class CheckoutForm extends Component {
|
||||
deliveryMethod={deliveryMethod}
|
||||
onChange={onDeliveryMethodChange}
|
||||
isPickupOnly={isPickupOnly || hasStecklinge}
|
||||
cartItems={cartItems}
|
||||
/>
|
||||
|
||||
{(deliveryMethod === "DHL" || deliveryMethod === "DPD") && (
|
||||
|
||||
@@ -82,7 +82,7 @@ class CheckoutValidation {
|
||||
|
||||
// Prefer stripe when available and meets minimum amount
|
||||
if (deliveryMethod === "DHL" || deliveryMethod === "DPD" || deliveryMethod === "Abholung") {
|
||||
return "stripe";
|
||||
return "mollie";/*stripe*/
|
||||
}
|
||||
|
||||
// Fall back to wire transfer
|
||||
@@ -106,11 +106,21 @@ 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";
|
||||
}
|
||||
|
||||
@@ -4,20 +4,27 @@ import Typography from '@mui/material/Typography';
|
||||
import Radio from '@mui/material/Radio';
|
||||
import Checkbox from '@mui/material/Checkbox';
|
||||
|
||||
const DeliveryMethodSelector = ({ deliveryMethod, onChange, isPickupOnly }) => {
|
||||
const DeliveryMethodSelector = ({ deliveryMethod, onChange, isPickupOnly, cartItems = [] }) => {
|
||||
// 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 deliveryOptions = [
|
||||
{
|
||||
id: 'DHL',
|
||||
name: 'DHL',
|
||||
description: isPickupOnly ? "nicht auswählbar weil ein oder mehrere Artikel nur abgeholt werden können" : 'Standardversand',
|
||||
price: '6,99 €',
|
||||
description: isPickupOnly ? "nicht auswählbar weil ein oder mehrere Artikel nur abgeholt werden können" :
|
||||
isFreeShipping ? 'Standardversand - KOSTENLOS ab 100€ Warenwert!' : 'Standardversand',
|
||||
price: isFreeShipping ? 'kostenlos' : '6,99 €',
|
||||
disabled: isPickupOnly
|
||||
},
|
||||
{
|
||||
id: 'DPD',
|
||||
name: 'DPD',
|
||||
description: isPickupOnly ? "nicht auswählbar weil ein oder mehrere Artikel nur abgeholt werden können" : 'Standardversand',
|
||||
price: '4,90 €',
|
||||
description: isPickupOnly ? "nicht auswählbar weil ein oder mehrere Artikel nur abgeholt werden können" :
|
||||
isFreeShipping ? 'Standardversand - KOSTENLOS ab 100€ Warenwert!' : 'Standardversand',
|
||||
price: isFreeShipping ? 'kostenlos' : '4,90 €',
|
||||
disabled: isPickupOnly
|
||||
},
|
||||
{
|
||||
@@ -114,6 +121,41 @@ const DeliveryMethodSelector = ({ deliveryMethod, onChange, isPickupOnly }) => {
|
||||
</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' }}>
|
||||
💡 Versandkostenfrei ab 100€ Warenwert!
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
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' }}>
|
||||
🎉 Glückwunsch! Sie erhalten kostenlosen Versand!
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
Ihr Warenkorb von {cartValue.toFixed(2).replace('.', ',')}€ qualifiziert sich für kostenlosen Versand.
|
||||
</Typography>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -15,8 +15,11 @@ import {
|
||||
TableRow,
|
||||
Paper
|
||||
} from '@mui/material';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const OrderDetailsDialog = ({ open, onClose, order }) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
if (!order) {
|
||||
return null;
|
||||
}
|
||||
@@ -108,7 +111,7 @@ const OrderDetailsDialog = ({ open, onClose, order }) => {
|
||||
<TableRow>
|
||||
<TableCell colSpan={2} />
|
||||
<TableCell align="right">
|
||||
<Typography fontWeight="bold">Gesamtnettopreis</Typography>
|
||||
<Typography fontWeight="bold">{t ? t('tax.totalNet') : 'Gesamtnettopreis'}</Typography>
|
||||
</TableCell>
|
||||
<TableCell align="right">
|
||||
<Typography fontWeight="bold">{currencyFormatter.format(vatCalculations.totalNet)}</Typography>
|
||||
@@ -117,21 +120,21 @@ const OrderDetailsDialog = ({ open, onClose, order }) => {
|
||||
{vatCalculations.vat7 > 0 && (
|
||||
<TableRow>
|
||||
<TableCell colSpan={2} />
|
||||
<TableCell align="right">7% Mehrwertsteuer</TableCell>
|
||||
<TableCell align="right">{t ? t('tax.vat7') : '7% Mehrwertsteuer'}</TableCell>
|
||||
<TableCell align="right">{currencyFormatter.format(vatCalculations.vat7)}</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
{vatCalculations.vat19 > 0 && (
|
||||
<TableRow>
|
||||
<TableCell colSpan={2} />
|
||||
<TableCell align="right">19% Mehrwertsteuer</TableCell>
|
||||
<TableCell align="right">{t ? t('tax.vat19') : '19% Mehrwertsteuer'}</TableCell>
|
||||
<TableCell align="right">{currencyFormatter.format(vatCalculations.vat19)}</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
<TableRow>
|
||||
<TableCell colSpan={2} />
|
||||
<TableCell align="right">
|
||||
<Typography fontWeight="bold">Zwischensumme</Typography>
|
||||
<Typography fontWeight="bold">{t ? t('tax.subtotal') : 'Zwischensumme'}</Typography>
|
||||
</TableCell>
|
||||
<TableCell align="right">
|
||||
<Typography fontWeight="bold">{currencyFormatter.format(subtotal)}</Typography>
|
||||
@@ -162,7 +165,7 @@ const OrderDetailsDialog = ({ open, onClose, order }) => {
|
||||
Bestellung stornieren
|
||||
</Button>
|
||||
)}
|
||||
<Button onClick={onClose}>Schließen</Button>
|
||||
<Button onClick={onClose}>{t ? t('common.close') : 'Schließen'}</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
|
||||
@@ -49,18 +49,29 @@ 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);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Listen for cart event which is dispatched after verifyToken completes
|
||||
this.verifyTokenHandler = () => {
|
||||
if (Array.isArray(window.cart) && window.cart.length > 0) {
|
||||
this.processStripeOrderWithCart([...window.cart]); // Copy the cart
|
||||
const cartCopy = [...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."
|
||||
@@ -111,6 +122,21 @@ 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) {
|
||||
@@ -205,6 +231,20 @@ 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();
|
||||
@@ -270,14 +310,44 @@ class OrderProcessingService {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Create Mollie payment intent
|
||||
createMollieIntent(totalAmount, loadMollieComponent) {
|
||||
loadMollieComponent();
|
||||
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.",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate delivery cost
|
||||
getDeliveryCost() {
|
||||
const { deliveryMethod, paymentMethod } = this.getState();
|
||||
const { deliveryMethod, paymentMethod, cartItems } = this.getState();
|
||||
let cost = 0;
|
||||
|
||||
switch (deliveryMethod) {
|
||||
@@ -297,7 +367,16 @@ class OrderProcessingService {
|
||||
cost = 6.99;
|
||||
}
|
||||
|
||||
// Add onDelivery surcharge if selected
|
||||
// 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)
|
||||
if (paymentMethod === "onDelivery") {
|
||||
cost += 8.99;
|
||||
}
|
||||
|
||||
@@ -5,8 +5,10 @@ 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'
|
||||
@@ -30,9 +32,9 @@ const OrderSummary = ({ deliveryCost, cartItems = [] }) => {
|
||||
return acc;
|
||||
}, { totalGross: 0, totalNet: 0, vat7: 0, vat19: 0 });
|
||||
|
||||
// Calculate shipping VAT (19% VAT for shipping costs)
|
||||
const shippingNetPrice = deliveryCost / (1 + 19 / 100);
|
||||
const shippingVat = deliveryCost - shippingNetPrice;
|
||||
// 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;
|
||||
|
||||
// Combine totals - add shipping VAT to the 19% VAT total
|
||||
const totalVat7 = cartVatCalculations.vat7;
|
||||
@@ -63,7 +65,7 @@ const OrderSummary = ({ deliveryCost, cartItems = [] }) => {
|
||||
)}
|
||||
{totalVat7 > 0 && (
|
||||
<TableRow>
|
||||
<TableCell>7% Mehrwertsteuer:</TableCell>
|
||||
<TableCell>{t ? t('tax.vat7') : '7% Mehrwertsteuer'}:</TableCell>
|
||||
<TableCell align="right">
|
||||
{currencyFormatter.format(totalVat7)}
|
||||
</TableCell>
|
||||
@@ -71,7 +73,7 @@ const OrderSummary = ({ deliveryCost, cartItems = [] }) => {
|
||||
)}
|
||||
{totalVat19 > 0 && (
|
||||
<TableRow>
|
||||
<TableCell>19% Mehrwertsteuer (inkl. Versand):</TableCell>
|
||||
<TableCell>{t ? t('tax.vat19WithShipping') : '19% Mehrwertsteuer (inkl. Versand)'}:</TableCell>
|
||||
<TableCell align="right">
|
||||
{currencyFormatter.format(totalVat19)}
|
||||
</TableCell>
|
||||
@@ -83,14 +85,23 @@ const OrderSummary = ({ deliveryCost, cartItems = [] }) => {
|
||||
{currencyFormatter.format(cartVatCalculations.totalGross)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
{deliveryCost > 0 && (
|
||||
<TableRow>
|
||||
<TableCell sx={{ fontWeight: 'bold' }}>Versandkosten:</TableCell>
|
||||
<TableCell sx={{ fontWeight: 'bold' }}>
|
||||
Versandkosten:
|
||||
{deliveryCost === 0 && cartVatCalculations.totalGross < 100 && (
|
||||
<span style={{ color: '#2e7d32', fontSize: '0.8em', marginLeft: '4px' }}>
|
||||
(kostenlos ab 100€)
|
||||
</span>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell align="right" sx={{ fontWeight: 'bold' }}>
|
||||
{currencyFormatter.format(deliveryCost)}
|
||||
{deliveryCost === 0 ? (
|
||||
<span style={{ color: '#2e7d32' }}>kostenlos</span>
|
||||
) : (
|
||||
currencyFormatter.format(deliveryCost)
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
<TableRow sx={{ borderTop: '1px solid #e0e0e0' }}>
|
||||
<TableCell sx={{ fontWeight: 'bold', pt: 2 }}>Gesamtsumme:</TableCell>
|
||||
<TableCell align="right" sx={{ fontWeight: 'bold', pt: 2 }}>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React, { Component } from "react";
|
||||
import { Box, Typography, Button } from "@mui/material";
|
||||
import { withI18n } from "../../i18n/withTranslation.js";
|
||||
|
||||
class PaymentConfirmationDialog extends Component {
|
||||
render() {
|
||||
@@ -28,24 +29,26 @@ class PaymentConfirmationDialog extends Component {
|
||||
color: paymentCompletionData.isSuccessful ? '#2e7d32' : '#d32f2f',
|
||||
fontWeight: 'bold'
|
||||
}}>
|
||||
{paymentCompletionData.isSuccessful ? 'Zahlung erfolgreich!' : 'Zahlung fehlgeschlagen'}
|
||||
{paymentCompletionData.isSuccessful ?
|
||||
(this.props.t ? this.props.t('payment.successful') : 'Zahlung erfolgreich!') :
|
||||
(this.props.t ? this.props.t('payment.failed') : 'Zahlung fehlgeschlagen')}
|
||||
</Typography>
|
||||
|
||||
{paymentCompletionData.isSuccessful ? (
|
||||
<>
|
||||
{orderCompleted ? (
|
||||
<Typography variant="body1" sx={{ mt: 2, color: '#2e7d32' }}>
|
||||
🎉 Ihre Bestellung wurde erfolgreich abgeschlossen! Sie können jetzt Ihre Bestellungen einsehen.
|
||||
{this.props.t ? this.props.t('payment.orderCompleted') : '🎉 Ihre Bestellung wurde erfolgreich abgeschlossen! Sie können jetzt Ihre Bestellungen einsehen.'}
|
||||
</Typography>
|
||||
) : (
|
||||
<Typography variant="body1" sx={{ mt: 2, color: '#2e7d32' }}>
|
||||
Ihre Zahlung wurde erfolgreich verarbeitet. Die Bestellung wird automatisch abgeschlossen.
|
||||
{this.props.t ? this.props.t('payment.orderProcessing') : 'Ihre Zahlung wurde erfolgreich verarbeitet. Die Bestellung wird automatisch abgeschlossen.'}
|
||||
</Typography>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<Typography variant="body1" sx={{ mt: 2, color: '#d32f2f' }}>
|
||||
Ihre Zahlung konnte nicht verarbeitet werden. Bitte versuchen Sie es erneut oder wählen Sie eine andere Zahlungsmethode.
|
||||
{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.'}
|
||||
</Typography>
|
||||
)}
|
||||
|
||||
@@ -75,7 +78,7 @@ class PaymentConfirmationDialog extends Component {
|
||||
}
|
||||
}}
|
||||
>
|
||||
Weiter einkaufen
|
||||
{this.props.t ? this.props.t('cart.continueShopping') : 'Weiter einkaufen'}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={onViewOrders}
|
||||
@@ -85,7 +88,7 @@ class PaymentConfirmationDialog extends Component {
|
||||
'&:hover': { bgcolor: '#1b5e20' }
|
||||
}}
|
||||
>
|
||||
Zu meinen Bestellungen
|
||||
{this.props.t ? this.props.t('payment.viewOrders') : 'Zu meinen Bestellungen'}
|
||||
</Button>
|
||||
</Box>
|
||||
)}
|
||||
@@ -94,4 +97,4 @@ class PaymentConfirmationDialog extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
export default PaymentConfirmationDialog;
|
||||
export default withI18n()(PaymentConfirmationDialog);
|
||||
@@ -24,7 +24,7 @@ const PaymentMethodSelector = ({ paymentMethod, onChange, deliveryMethod, onDeli
|
||||
// Handle delivery method changes - auto-switch to stripe when DHL/DPD is selected
|
||||
useEffect(() => {
|
||||
if ((deliveryMethod === "DHL" || deliveryMethod === "DPD") && paymentMethod === "cash") {
|
||||
handlePaymentMethodChange({ target: { value: "mollie" } });
|
||||
handlePaymentMethodChange({ target: { value: "mollie" /*stripe*/ } });
|
||||
}
|
||||
}, [deliveryMethod, paymentMethod, handlePaymentMethodChange]);
|
||||
|
||||
@@ -44,7 +44,7 @@ const PaymentMethodSelector = ({ paymentMethod, onChange, deliveryMethod, onDeli
|
||||
},
|
||||
/*{
|
||||
id: "stripe",
|
||||
name: "Karte oder Sofortüberweisung (Stripe)",
|
||||
name: "Karte oder Sofortüberweisung",
|
||||
description: totalAmount < 0.50 && totalAmount > 0
|
||||
? "Bezahlen Sie per Karte oder Sofortüberweisung (Mindestbetrag: 0,50 €)"
|
||||
: "Bezahlen Sie per Karte oder Sofortüberweisung",
|
||||
@@ -58,7 +58,7 @@ const PaymentMethodSelector = ({ paymentMethod, onChange, deliveryMethod, onDeli
|
||||
},*/
|
||||
{
|
||||
id: "mollie",
|
||||
name: "Karte oder Sofortüberweisung",
|
||||
name: "Karte, Sofortüberweisung, Apple Pay, Google Pay, PayPal",
|
||||
description: totalAmount < 0.50 && totalAmount > 0
|
||||
? "Bezahlen Sie per Karte oder Sofortüberweisung (Mindestbetrag: 0,50 €)"
|
||||
: "Bezahlen Sie per Karte oder Sofortüberweisung",
|
||||
|
||||
193
src/config.js
193
src/config.js
@@ -3,23 +3,200 @@ 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",
|
||||
language: "de-DE", // Will be updated dynamically based on i18n
|
||||
country: "DE",
|
||||
|
||||
// Shop Descriptions
|
||||
descriptions: {
|
||||
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."
|
||||
// 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"
|
||||
}
|
||||
},
|
||||
|
||||
// Keywords
|
||||
keywords: "Seeds, Steckling, Cannabis, Biobizz, Growheads",
|
||||
// Shop Descriptions - Multilingual
|
||||
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."
|
||||
}
|
||||
},
|
||||
|
||||
// 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"
|
||||
},
|
||||
|
||||
// Shipping
|
||||
shipping: {
|
||||
|
||||
225
src/contexts/CarouselContext.js
Normal file
225
src/contexts/CarouselContext.js
Normal file
@@ -0,0 +1,225 @@
|
||||
import React, { createContext, useContext, useRef, useEffect, useState } from 'react';
|
||||
|
||||
const CarouselContext = createContext();
|
||||
|
||||
export const useCarousel = () => {
|
||||
const context = useContext(CarouselContext);
|
||||
if (!context) {
|
||||
throw new Error('useCarousel must be used within a CarouselProvider');
|
||||
}
|
||||
return context;
|
||||
};
|
||||
|
||||
export const CarouselProvider = ({ children }) => {
|
||||
const carouselRef = useRef(null);
|
||||
const scrollPositionRef = useRef(0);
|
||||
const animationIdRef = useRef(null);
|
||||
const isPausedRef = useRef(false);
|
||||
const resumeTimeoutRef = useRef(null);
|
||||
|
||||
const [filteredCategories, setFilteredCategories] = useState([]);
|
||||
|
||||
// Initialize refs properly
|
||||
useEffect(() => {
|
||||
isPausedRef.current = false;
|
||||
scrollPositionRef.current = 0;
|
||||
}, []);
|
||||
|
||||
// Auto-scroll effect
|
||||
useEffect(() => {
|
||||
if (filteredCategories.length === 0) return;
|
||||
|
||||
const startAnimation = () => {
|
||||
if (!carouselRef.current) {
|
||||
return false;
|
||||
}
|
||||
|
||||
isPausedRef.current = false;
|
||||
|
||||
const itemWidth = 146; // 130px + 16px gap
|
||||
const totalWidth = filteredCategories.length * itemWidth;
|
||||
|
||||
const animate = () => {
|
||||
if (!animationIdRef.current || isPausedRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
scrollPositionRef.current += 0.5;
|
||||
|
||||
if (scrollPositionRef.current >= totalWidth) {
|
||||
scrollPositionRef.current = 0;
|
||||
}
|
||||
|
||||
if (carouselRef.current) {
|
||||
const transform = `translateX(-${scrollPositionRef.current}px)`;
|
||||
carouselRef.current.style.transform = transform;
|
||||
}
|
||||
|
||||
animationIdRef.current = requestAnimationFrame(animate);
|
||||
};
|
||||
|
||||
if (!isPausedRef.current) {
|
||||
animationIdRef.current = requestAnimationFrame(animate);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
// Try immediately, then with increasing delays
|
||||
if (!startAnimation()) {
|
||||
const timeout1 = setTimeout(() => {
|
||||
if (!startAnimation()) {
|
||||
const timeout2 = setTimeout(() => {
|
||||
if (!startAnimation()) {
|
||||
const timeout3 = setTimeout(startAnimation, 2000);
|
||||
return () => clearTimeout(timeout3);
|
||||
}
|
||||
}, 1000);
|
||||
return () => clearTimeout(timeout2);
|
||||
}
|
||||
}, 100);
|
||||
|
||||
return () => {
|
||||
isPausedRef.current = true;
|
||||
clearTimeout(timeout1);
|
||||
if (animationIdRef.current) {
|
||||
cancelAnimationFrame(animationIdRef.current);
|
||||
}
|
||||
if (resumeTimeoutRef.current) {
|
||||
clearTimeout(resumeTimeoutRef.current);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return () => {
|
||||
isPausedRef.current = true;
|
||||
if (animationIdRef.current) {
|
||||
cancelAnimationFrame(animationIdRef.current);
|
||||
}
|
||||
if (resumeTimeoutRef.current) {
|
||||
clearTimeout(resumeTimeoutRef.current);
|
||||
}
|
||||
};
|
||||
}, [filteredCategories]);
|
||||
|
||||
// Additional effect for when ref becomes available
|
||||
useEffect(() => {
|
||||
if (filteredCategories.length > 0 && carouselRef.current && !animationIdRef.current) {
|
||||
isPausedRef.current = false;
|
||||
|
||||
const itemWidth = 146;
|
||||
const totalWidth = filteredCategories.length * itemWidth;
|
||||
|
||||
const animate = () => {
|
||||
if (!animationIdRef.current || isPausedRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
scrollPositionRef.current += 0.5;
|
||||
|
||||
if (scrollPositionRef.current >= totalWidth) {
|
||||
scrollPositionRef.current = 0;
|
||||
}
|
||||
|
||||
if (carouselRef.current) {
|
||||
const transform = `translateX(-${scrollPositionRef.current}px)`;
|
||||
carouselRef.current.style.transform = transform;
|
||||
}
|
||||
|
||||
animationIdRef.current = requestAnimationFrame(animate);
|
||||
};
|
||||
|
||||
if (!isPausedRef.current) {
|
||||
animationIdRef.current = requestAnimationFrame(animate);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Manual navigation
|
||||
const moveCarousel = (direction) => {
|
||||
if (!carouselRef.current) return;
|
||||
|
||||
// Pause auto-scroll
|
||||
isPausedRef.current = true;
|
||||
if (animationIdRef.current) {
|
||||
cancelAnimationFrame(animationIdRef.current);
|
||||
animationIdRef.current = null;
|
||||
}
|
||||
|
||||
const itemWidth = 146;
|
||||
const moveAmount = itemWidth * 3;
|
||||
const totalWidth = filteredCategories.length * itemWidth;
|
||||
|
||||
if (direction === "left") {
|
||||
scrollPositionRef.current -= moveAmount;
|
||||
if (scrollPositionRef.current < 0) {
|
||||
scrollPositionRef.current = totalWidth + scrollPositionRef.current;
|
||||
}
|
||||
} else {
|
||||
scrollPositionRef.current += moveAmount;
|
||||
if (scrollPositionRef.current >= totalWidth) {
|
||||
scrollPositionRef.current = scrollPositionRef.current % totalWidth;
|
||||
}
|
||||
}
|
||||
|
||||
// Apply smooth transition
|
||||
carouselRef.current.style.transition = "transform 0.5s ease-in-out";
|
||||
carouselRef.current.style.transform = `translateX(-${scrollPositionRef.current}px)`;
|
||||
|
||||
// Remove transition after animation
|
||||
setTimeout(() => {
|
||||
if (carouselRef.current) {
|
||||
carouselRef.current.style.transition = "none";
|
||||
}
|
||||
}, 500);
|
||||
|
||||
// Clear existing timeout
|
||||
if (resumeTimeoutRef.current) {
|
||||
clearTimeout(resumeTimeoutRef.current);
|
||||
}
|
||||
|
||||
// Resume auto-scroll after 3 seconds
|
||||
resumeTimeoutRef.current = setTimeout(() => {
|
||||
isPausedRef.current = false;
|
||||
|
||||
const animate = () => {
|
||||
if (!animationIdRef.current || isPausedRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
scrollPositionRef.current += 0.5;
|
||||
|
||||
if (scrollPositionRef.current >= totalWidth) {
|
||||
scrollPositionRef.current = 0;
|
||||
}
|
||||
|
||||
if (carouselRef.current) {
|
||||
carouselRef.current.style.transform = `translateX(-${scrollPositionRef.current}px)`;
|
||||
}
|
||||
|
||||
animationIdRef.current = requestAnimationFrame(animate);
|
||||
};
|
||||
|
||||
animationIdRef.current = requestAnimationFrame(animate);
|
||||
}, 3000);
|
||||
};
|
||||
|
||||
const value = {
|
||||
carouselRef,
|
||||
scrollPositionRef,
|
||||
animationIdRef,
|
||||
isPausedRef,
|
||||
resumeTimeoutRef,
|
||||
filteredCategories,
|
||||
setFilteredCategories,
|
||||
moveCarousel
|
||||
};
|
||||
|
||||
return (
|
||||
<CarouselContext.Provider value={value}>
|
||||
{children}
|
||||
</CarouselContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export default CarouselContext;
|
||||
129
src/i18n/index.js
Normal file
129
src/i18n/index.js
Normal file
@@ -0,0 +1,129 @@
|
||||
import i18n from 'i18next';
|
||||
import { initReactI18next } from 'react-i18next';
|
||||
import LanguageDetector from 'i18next-browser-languagedetector';
|
||||
|
||||
// Import all translation files
|
||||
import translationDE from './locales/de/translation.js';
|
||||
import translationEN from './locales/en/translation.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';
|
||||
|
||||
const 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
|
||||
}
|
||||
};
|
||||
|
||||
i18n
|
||||
.use(LanguageDetector)
|
||||
.use(initReactI18next)
|
||||
.init({
|
||||
resources,
|
||||
fallbackLng: 'de', // German as fallback since it's your primary language
|
||||
lng: 'de', // Default 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;
|
||||
25
src/i18n/locales/ar/auth.js
Normal file
25
src/i18n/locales/ar/auth.js
Normal file
@@ -0,0 +1,25 @@
|
||||
export default {
|
||||
"login": "تسجيل الدخول", // Anmelden
|
||||
"register": "تسجيل", // Registrieren
|
||||
"logout": "تسجيل خروج", // Abmelden
|
||||
"profile": "الملف الشخصي", // Profil
|
||||
"email": "البريد الإلكتروني", // E-Mail
|
||||
"password": "كلمة المرور", // Passwort
|
||||
"confirmPassword": "تأكيد كلمة المرور", // Passwort bestätigen
|
||||
"forgotPassword": "هل نسيت كلمة المرور؟", // Passwort vergessen?
|
||||
"loginWithGoogle": "تسجيل الدخول باستخدام Google", // Mit Google anmelden
|
||||
"or": "أو", // ODER
|
||||
"privacyAccept": "بالنقر على \"تسجيل الدخول باستخدام Google\" أوافق على", // Mit dem Click auf "Mit Google anmelden" akzeptiere ich die
|
||||
"privacyPolicy": "سياسة الخصوصية", // Datenschutzbestimmungen
|
||||
"passwordMinLength": "يجب أن تكون كلمة المرور مكونة من 8 أحرف على الأقل", // Das Passwort muss mindestens 8 Zeichen lang sein
|
||||
"newPasswordMinLength": "يجب أن تكون كلمة المرور الجديدة مكونة من 8 أحرف على الأقل", // Das neue Passwort muss mindestens 8 Zeichen lang sein
|
||||
"menu": {
|
||||
"profile": "الملف الشخصي", // Profil
|
||||
"myProfile": "ملفي الشخصي", // Mein Profil
|
||||
"checkout": "إتمام الشراء", // Bestellabschluss
|
||||
"orders": "الطلبات", // Bestellungen
|
||||
"settings": "الإعدادات", // Einstellungen
|
||||
"adminDashboard": "لوحة تحكم المسؤول", // Admin Dashboard
|
||||
"adminUsers": "مستخدمو المسؤول" // Admin Users
|
||||
}
|
||||
};
|
||||
24
src/i18n/locales/ar/cart.js
Normal file
24
src/i18n/locales/ar/cart.js
Normal file
@@ -0,0 +1,24 @@
|
||||
export default {
|
||||
"title": "العربة", // Warenkorb
|
||||
"empty": "فارغ", // leer
|
||||
"addToCart": "أضف إلى العربة", // In den Korb
|
||||
"preorderCutting": "اطلب مسبقًا كشتلة", // Als Steckling vorbestellen
|
||||
"continueShopping": "تابع التسوق", // Weiter einkaufen
|
||||
"proceedToCheckout": "المتابعة إلى الدفع", // Weiter zur Kasse
|
||||
"productCount": "{{count}} {{count, plural, one {منتج} other {منتجات}}}", // {{count}} {{count, plural, one {Produkt} other {Produkte}}}
|
||||
"productSingular": "منتج", // Produkt
|
||||
"productPlural": "منتجات", // Produkte
|
||||
"removeFromCart": "إزالة من العربة", // Aus dem Warenkorb entfernen
|
||||
"openCart": "افتح العربة", // Warenkorb öffnen
|
||||
"availableFrom": "متوفر من {{date}}", // Ab {{date}}
|
||||
"backToOrder": "← العودة إلى الطلب", // ← Zurück zur Bestellung
|
||||
"sync": {
|
||||
"title": "مزامنة العربة", // Warenkorb-Synchronisierung
|
||||
"description": "لديك عربة محفوظة في حسابك. من فضلك اختر كيف تريد المتابعة:", // Sie haben einen gespeicherten Warenkorb in ihrem Account. Bitte wählen Sie, wie Sie verfahren möchten:
|
||||
"deleteServer": "حذف عربة الخادم", // Server-Warenkorb löschen
|
||||
"useServer": "استخدام عربة الخادم", // Server-Warenkorb übernehmen
|
||||
"merge": "دمج العربات", // Warenkörbe zusammenführen
|
||||
"currentCart": "عربتك الحالية", // Ihr aktueller Warenkorb
|
||||
"serverCart": "العربة المحفوظة في ملفك الشخصي" // In Ihrem Profil gespeicherter Warenkorb
|
||||
}
|
||||
};
|
||||
3
src/i18n/locales/ar/chat.js
Normal file
3
src/i18n/locales/ar/chat.js
Normal file
@@ -0,0 +1,3 @@
|
||||
export default {
|
||||
"privacyRead": "تم القراءة والموافقة", // Gelesen & Akzeptiert
|
||||
};
|
||||
13
src/i18n/locales/ar/checkout.js
Normal file
13
src/i18n/locales/ar/checkout.js
Normal file
@@ -0,0 +1,13 @@
|
||||
export default {
|
||||
"invoiceAddress": "عنوان الفاتورة", // Rechnungsadresse
|
||||
"deliveryAddress": "عنوان التوصيل", // Lieferadresse
|
||||
"saveForFuture": "احفظ للطلبات المستقبلية", // Für zukünftige Bestellungen speichern
|
||||
"pickupDate": "لموعد استلام القصاصات المطلوب؟", // Für welchen Termin ist die Abholung der Stecklinge gewünscht?
|
||||
"note": "ملاحظة", // Anmerkung
|
||||
"sameAddress": "عنوان التوصيل مطابق لعنوان الفاتورة", // Lieferadresse ist identisch mit Rechnungsadresse
|
||||
"termsAccept": "لقد قرأت الشروط والأحكام، سياسة الخصوصية، وأحكام حق الانسحاب", // Ich habe die AGBs, die Datenschutzerklärung und die Bestimmungen zum Widerrufsrecht gelesen
|
||||
"selectDeliveryMethod": "اختر طريقة الشحن", // Versandart wählen
|
||||
"selectPaymentMethod": "اختر طريقة الدفع", // Zahlungsart wählen
|
||||
"orderSummary": "ملخص الطلب", // Bestellübersicht
|
||||
"addressValidationError": "يرجى التحقق من بياناتك في حقول العنوان." // Bitte überprüfen Sie Ihre Eingaben in den Adressfeldern.
|
||||
};
|
||||
19
src/i18n/locales/ar/common.js
Normal file
19
src/i18n/locales/ar/common.js
Normal file
@@ -0,0 +1,19 @@
|
||||
export default {
|
||||
"loading": "جارٍ التحميل...", // Lädt...
|
||||
"error": "خطأ", // Fehler
|
||||
"close": "إغلاق", // Schließen
|
||||
"save": "حفظ", // Speichern
|
||||
"cancel": "إلغاء", // Abbrechen
|
||||
"ok": "موافق", // OK
|
||||
"yes": "نعم", // Ja
|
||||
"no": "لا", // Nein
|
||||
"next": "التالي", // Weiter
|
||||
"back": "رجوع", // Zurück
|
||||
"edit": "تعديل", // Bearbeiten
|
||||
"delete": "حذف", // Löschen
|
||||
"add": "إضافة", // Hinzufügen
|
||||
"remove": "إزالة", // Entfernen
|
||||
"products": "منتجات", // Produkte
|
||||
"product": "منتج", // Produkt
|
||||
"days": "أيام" // Tage
|
||||
};
|
||||
27
src/i18n/locales/ar/delivery.js
Normal file
27
src/i18n/locales/ar/delivery.js
Normal file
@@ -0,0 +1,27 @@
|
||||
export default {
|
||||
"methods": {
|
||||
"dhl": "DHL", // DHL
|
||||
"dpd": "DPD", // DPD
|
||||
"sperrgut": "بضائع ضخمة", // Sperrgut
|
||||
"pickup": "الاستلام من المتجر" // Abholung in der Filiale
|
||||
},
|
||||
"descriptions": {
|
||||
"standard": "الشحن العادي", // Standardversand
|
||||
"standardFree": "الشحن العادي - مجاني للطلبات فوق 100€!", // Standardversand - KOSTENLOS ab 100€ Warenwert!
|
||||
"notAvailable": "غير متاح للاختيار لأن عنصر واحد أو أكثر يمكن استلامه فقط", // nicht auswählbar weil ein oder mehrere Artikel nur abgeholt werden können
|
||||
"bulky": "للعناصر الكبيرة والثقيلة", // Für große und schwere Artikel
|
||||
"pickupOnly": "الاستلام فقط" // nur Abholung
|
||||
},
|
||||
"prices": {
|
||||
"free": "مجاني", // kostenlos
|
||||
"freeFrom100": "(مجاني للطلبات فوق 100€)", // (kostenlos ab 100€)
|
||||
"dhl": "6.99 €", // 6,99 €
|
||||
"dpd": "4.90 €", // 4,90 €
|
||||
"sperrgut": "28.99 €" // 28,99 €
|
||||
},
|
||||
"times": {
|
||||
"cutting14Days": "مدة التوصيل: 14 يوم", // Lieferzeit: 14 Tage
|
||||
"standard2to3Days": "مدة التوصيل: 2-3 أيام", // Lieferzeit: 2-3 Tage
|
||||
"supplier7to9Days": "مدة التوصيل: 7-9 أيام" // Lieferzeit: 7-9 Tage
|
||||
}
|
||||
};
|
||||
7
src/i18n/locales/ar/filters.js
Normal file
7
src/i18n/locales/ar/filters.js
Normal file
@@ -0,0 +1,7 @@
|
||||
export default {
|
||||
"sorting": "الترتيب", // Sortierung
|
||||
"perPage": "لكل صفحة", // pro Seite
|
||||
"availability": "التوفر", // Verfügbarkeit
|
||||
"manufacturer": "الشركة المصنعة", // Hersteller
|
||||
"all": "الكل", // Alle
|
||||
};
|
||||
15
src/i18n/locales/ar/footer.js
Normal file
15
src/i18n/locales/ar/footer.js
Normal file
@@ -0,0 +1,15 @@
|
||||
export default {
|
||||
"hours": "السبت 11 صباحًا - 7 مساءً", // Sa 11-19
|
||||
"address": "شارع تراشنبرجر 14 - دريسدن", // Trachenberger Straße 14 - Dresden
|
||||
"location": "بين محطة بيسشن وساحة تراشنبرجر", // Zwischen Haltepunkt Pieschen und Trachenberger Platz
|
||||
"allPricesIncl": "* جميع الأسعار تشمل ضريبة القيمة المضافة القانونية، بالإضافة إلى الشحن", // * Alle Preise inkl. gesetzlicher USt., zzgl. Versand
|
||||
"copyright": "© {{year}} GrowHeads.de", // © {{year}} GrowHeads.de
|
||||
"legal": {
|
||||
"datenschutz": "سياسة الخصوصية", // Datenschutz
|
||||
"agb": "الشروط والأحكام", // AGB
|
||||
"sitemap": "خريطة الموقع", // Sitemap
|
||||
"impressum": "الإشعار القانوني", // Impressum
|
||||
"batteriegesetzhinweise": "معلومات قانون البطاريات", // Batteriegesetzhinweise
|
||||
"widerrufsrecht": "حق الانسحاب" // Widerrufsrecht
|
||||
}
|
||||
};
|
||||
43
src/i18n/locales/ar/index.js
Normal file
43
src/i18n/locales/ar/index.js
Normal file
@@ -0,0 +1,43 @@
|
||||
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
|
||||
};
|
||||
3
src/i18n/locales/ar/locale.js
Normal file
3
src/i18n/locales/ar/locale.js
Normal file
@@ -0,0 +1,3 @@
|
||||
export default {
|
||||
"code": "ar-EG" // de-DE
|
||||
};
|
||||
9
src/i18n/locales/ar/navigation.js
Normal file
9
src/i18n/locales/ar/navigation.js
Normal file
@@ -0,0 +1,9 @@
|
||||
export default {
|
||||
"home": "الرئيسية", // Startseite
|
||||
"aktionen": "العروض", // Aktionen
|
||||
"filiale": "الفرع", // Filiale
|
||||
"categories": "الفئات", // Kategorien
|
||||
"categoriesOpen": "فتح الفئات", // Kategorien öffnen
|
||||
"categoriesClose": "إغلاق الفئات", // Kategorien schließen
|
||||
"otherCategories": "فئات أخرى" // Andere Kategorien
|
||||
};
|
||||
23
src/i18n/locales/ar/orders.js
Normal file
23
src/i18n/locales/ar/orders.js
Normal file
@@ -0,0 +1,23 @@
|
||||
export default {
|
||||
"status": {
|
||||
"new": "قيد التنفيذ", // in Bearbeitung
|
||||
"pending": "جديد", // Neu
|
||||
"processing": "قيد التنفيذ", // in Bearbeitung
|
||||
"cancelled": "ملغي", // Storniert
|
||||
"shipped": "تم الشحن", // Verschickt
|
||||
"delivered": "تم التوصيل", // Geliefert
|
||||
"return": "إرجاع", // Retoure
|
||||
"partialReturn": "إرجاع جزئي", // Teil Retoure
|
||||
"partialDelivered": "تم التوصيل جزئياً" // Teil geliefert
|
||||
},
|
||||
"table": {
|
||||
"orderNumber": "رقم الطلب", // Bestellnummer
|
||||
"date": "التاريخ", // Datum
|
||||
"status": "الحالة", // Status
|
||||
"items": "العناصر", // Artikel
|
||||
"total": "الإجمالي", // Summe
|
||||
"actions": "الإجراءات", // Aktionen
|
||||
"viewDetails": "عرض التفاصيل" // Details anzeigen
|
||||
},
|
||||
"noOrders": "لم تقم بوضع أي طلبات بعد." // Sie haben noch keine Bestellungen aufgegeben.
|
||||
};
|
||||
10
src/i18n/locales/ar/pages.js
Normal file
10
src/i18n/locales/ar/pages.js
Normal file
@@ -0,0 +1,10 @@
|
||||
export default {
|
||||
"oilPress": {
|
||||
"title": "استعارة مكبس الزيت", // Ölpresse ausleihen
|
||||
"comingSoon": "المحتوى قادم قريباً...", // Inhalt kommt bald...
|
||||
},
|
||||
"thcTest": {
|
||||
"title": "اختبار THC", // THC Test
|
||||
"comingSoon": "المحتوى قادم قريباً...", // Inhalt kommt bald...
|
||||
}
|
||||
};
|
||||
8
src/i18n/locales/ar/payment.js
Normal file
8
src/i18n/locales/ar/payment.js
Normal file
@@ -0,0 +1,8 @@
|
||||
export default {
|
||||
"successful": "تم الدفع بنجاح!", // Zahlung erfolgreich!
|
||||
"failed": "فشل الدفع", // Zahlung fehlgeschlagen
|
||||
"orderCompleted": "🎉 تم إكمال طلبك بنجاح! يمكنك الآن عرض طلباتك.", // 🎉 Ihre Bestellung wurde erfolgreich abgeschlossen! Sie können jetzt Ihre Bestellungen einsehen.
|
||||
"orderProcessing": "تمت معالجة دفعتك بنجاح. سيتم إكمال الطلب تلقائيًا.", // Ihre Zahlung wurde erfolgreich verarbeitet. Die Bestellung wird automatisch abgeschlossen.
|
||||
"paymentError": "لم نتمكن من معالجة دفعتك. يرجى المحاولة مرة أخرى أو اختيار طريقة دفع أخرى.", // Ihre Zahlung konnte nicht verarbeitet werden. Bitte versuchen Sie es erneut oder wählen Sie eine andere Zahlungsmethode.
|
||||
"viewOrders": "اذهب إلى طلباتي" // Zu meinen Bestellungen
|
||||
};
|
||||
32
src/i18n/locales/ar/product.js
Normal file
32
src/i18n/locales/ar/product.js
Normal file
@@ -0,0 +1,32 @@
|
||||
export default {
|
||||
"loading": "جارٍ تحميل المنتج...", // Produkt wird geladen...
|
||||
"notFound": "المنتج غير موجود", // Produkt nicht gefunden
|
||||
"notFoundDescription": "المنتج الذي تبحث عنه غير موجود أو تم إزالته.", // Das gesuchte Produkt existiert nicht oder wurde entfernt.
|
||||
"backToHome": "العودة إلى الصفحة الرئيسية", // Zurück zur Startseite
|
||||
"error": "خطأ", // Fehler
|
||||
"articleNumber": "رقم الصنف", // Artikelnummer
|
||||
"manufacturer": "الشركة المصنعة", // Hersteller
|
||||
"inclVat": "شامل {{vat}}% ضريبة القيمة المضافة", // inkl. {{vat}}% MwSt.
|
||||
"priceUnit": "{{price}}/{{unit}}", // {{price}}/{{unit}}
|
||||
"new": "جديد", // Neu
|
||||
"weeks": "أسابيع", // Wochen
|
||||
"arriving": "الوصول:", // Ankunft:
|
||||
"inclVatFooter": "شامل {{vat}}% ضريبة القيمة المضافة,*", // inkl. {{vat}}% MwSt.,*
|
||||
"availability": "التوفر", // Verfügbarkeit
|
||||
"inStock": "متوفر في المخزون", // auf Lager
|
||||
"comingSoon": "قريباً", // Bald verfügbar
|
||||
"deliveryTime": "مدة التوصيل", // Lieferzeit
|
||||
"inclShort": "شامل", // inkl.
|
||||
"vatShort": "ضريبة القيمة المضافة", // MwSt.
|
||||
"countDisplay": {
|
||||
"noProducts": "0 منتجات", // 0 Produkte
|
||||
"oneProduct": "منتج واحد", // 1 Produkt
|
||||
"multipleProducts": "{{count}} منتجات", // {{count}} Produkte
|
||||
"filteredProducts": "{{filtered}} من {{total}} منتجات", // {{filtered}} von {{total}} Produkten
|
||||
"filteredOneProduct": "{{filtered}} من منتج واحد", // {{filtered}} von 1 Produkt
|
||||
"xOfYProducts": "{{x}} من {{y}} منتجات" // {{x}} von {{y}} Produkten
|
||||
},
|
||||
"removeFiltersToSee": "أزل الفلاتر لرؤية المنتجات", // Entferne Filter um Produkte zu sehen
|
||||
"outOfStock": "غير متوفر في المخزون", // Out of Stock
|
||||
"fromXProducts": "من {{count}} منتجات" // ab {{count}} Produkten
|
||||
};
|
||||
5
src/i18n/locales/ar/search.js
Normal file
5
src/i18n/locales/ar/search.js
Normal file
@@ -0,0 +1,5 @@
|
||||
export default {
|
||||
"placeholder": "ممكن تسألني عن أنواع الحشيش...", // Du kannst mich nach Cannabissorten fragen...
|
||||
"recording": "جاري التسجيل...", // Aufnahme läuft...
|
||||
"searchProducts": "ابحث عن المنتجات...", // Produkte suchen...
|
||||
};
|
||||
8
src/i18n/locales/ar/sections.js
Normal file
8
src/i18n/locales/ar/sections.js
Normal file
@@ -0,0 +1,8 @@
|
||||
export default {
|
||||
"seeds": "بذور", // Seeds
|
||||
"stecklinge": "قصاصات", // Stecklinge
|
||||
"oilPress": "استعارة معصرة زيت", // Ölpresse ausleihen
|
||||
"thcTest": "اختبار THC", // THC Test
|
||||
"address1": "Trachenberger Straße 14", // Trachenberger Straße 14
|
||||
"address2": "01129 Dresden" // 01129 Dresden
|
||||
};
|
||||
34
src/i18n/locales/ar/settings.js
Normal file
34
src/i18n/locales/ar/settings.js
Normal file
@@ -0,0 +1,34 @@
|
||||
export default {
|
||||
"changePassword": "تغيير كلمة المرور", // Passwort ändern
|
||||
"currentPassword": "كلمة المرور الحالية", // Aktuelles Passwort
|
||||
"newPassword": "كلمة المرور الجديدة", // Neues Passwort
|
||||
"confirmNewPassword": "تأكيد كلمة المرور الجديدة", // Neues Passwort bestätigen
|
||||
"updatePassword": "تحديث كلمة المرور", // Passwort aktualisieren
|
||||
"changeEmail": "تغيير عنوان البريد الإلكتروني", // E-Mail-Adresse ändern
|
||||
"password": "كلمة المرور", // Passwort
|
||||
"newEmail": "عنوان البريد الإلكتروني الجديد", // Neue E-Mail-Adresse
|
||||
"updateEmail": "تحديث البريد الإلكتروني", // E-Mail aktualisieren
|
||||
"apiKey": "مفتاح API", // API-Schlüssel
|
||||
"apiKeyDescription": "استخدم مفتاح API الخاص بك للتكامل مع التطبيقات الخارجية.", // Verwenden Sie Ihren API-Schlüssel für die Integration mit externen Anwendungen.
|
||||
"apiDocumentation": "توثيق API:", // API-Dokumentation:
|
||||
"copyToClipboard": "نسخ إلى الحافظة", // In Zwischenablage kopieren
|
||||
"generate": "إنشاء", // Generieren
|
||||
"regenerate": "إعادة إنشاء", // Regenerieren
|
||||
"apiKeyCopied": "تم نسخ مفتاح API إلى الحافظة", // API-Schlüssel in Zwischenablage kopiert
|
||||
"errors": {
|
||||
"fillAllFields": "يرجى ملء جميع الحقول", // Bitte füllen Sie alle Felder aus
|
||||
"passwordsNotMatch": "كلمات المرور الجديدة غير متطابقة", // Die neuen Passwörter stimmen nicht überein
|
||||
"passwordTooShort": "يجب أن تكون كلمة المرور الجديدة 8 أحرف على الأقل", // Das neue Passwort muss mindestens 8 Zeichen lang sein
|
||||
"passwordUpdateError": "حدث خطأ أثناء تحديث كلمة المرور", // Fehler beim Aktualisieren des Passworts
|
||||
"invalidEmail": "يرجى إدخال عنوان بريد إلكتروني صالح", // Bitte geben Sie eine gültige E-Mail-Adresse ein
|
||||
"emailUpdateError": "حدث خطأ أثناء تحديث عنوان البريد الإلكتروني", // Fehler beim Aktualisieren der E-Mail-Adresse
|
||||
"userNotFound": "المستخدم غير موجود", // Benutzer nicht gefunden
|
||||
"apiKeyGenerationError": "حدث خطأ أثناء إنشاء مفتاح API" // Fehler beim Generieren des API-Schlüssels
|
||||
},
|
||||
"success": {
|
||||
"passwordUpdated": "تم تحديث كلمة المرور بنجاح", // Passwort erfolgreich aktualisiert
|
||||
"emailUpdated": "تم تحديث عنوان البريد الإلكتروني بنجاح", // E-Mail-Adresse erfolgreich aktualisiert
|
||||
"apiKeyGenerated": "تم إنشاء مفتاح API بنجاح", // API-Schlüssel erfolgreich generiert
|
||||
"apiKeyWarning": "احفظ هذا المفتاح بأمان. لأسباب أمنية، سيتم إخفاؤه خلال 10 ثوانٍ." // Speichern Sie diesen Schlüssel sicher. Er wird aus Sicherheitsgründen in 10 Sekunden ausgeblendet.
|
||||
}
|
||||
};
|
||||
6
src/i18n/locales/ar/sorting.js
Normal file
6
src/i18n/locales/ar/sorting.js
Normal file
@@ -0,0 +1,6 @@
|
||||
export default {
|
||||
"name": "الاسم", // Name
|
||||
"searchField": "كلمة البحث", // Suchbegriff
|
||||
"priceLowHigh": "السعر: من الأقل للأعلى", // Preis: Niedrig zu Hoch
|
||||
"priceHighLow": "السعر: من الأعلى للأقل" // Preis: Hoch zu Niedrig
|
||||
};
|
||||
12
src/i18n/locales/ar/tax.js
Normal file
12
src/i18n/locales/ar/tax.js
Normal file
@@ -0,0 +1,12 @@
|
||||
export default {
|
||||
"vat": "ضريبة القيمة المضافة", // Mehrwertsteuer
|
||||
"vat7": "ضريبة القيمة المضافة 7%", // 7% Mehrwertsteuer
|
||||
"vat19": "ضريبة القيمة المضافة 19%", // 19% Mehrwertsteuer
|
||||
"vat19WithShipping": "ضريبة القيمة المضافة 19% (شاملة الشحن)", // 19% Mehrwertsteuer (inkl. Versand)
|
||||
"totalNet": "إجمالي السعر الصافي", // Gesamtnettopreis
|
||||
"totalGross": "إجمالي السعر الإجمالي بدون الشحن", // Gesamtbruttopreis ohne Versand
|
||||
"subtotal": "المجموع الفرعي", // Zwischensumme
|
||||
"incl7Vat": "شاملة ضريبة القيمة المضافة 7%", // inkl. 7% MwSt.
|
||||
"inclVatWithFooter": "(شاملة {{vat}}% ضريبة القيمة المضافة،*)", // (incl. {{vat}}% USt.,*)
|
||||
"inclVatAmount": "شاملة {{amount}} € ضريبة القيمة المضافة ({{rate}}%)" // nkl. {{amount}} € MwSt. ({{rate}}%)
|
||||
};
|
||||
5
src/i18n/locales/ar/titles.js
Normal file
5
src/i18n/locales/ar/titles.js
Normal file
@@ -0,0 +1,5 @@
|
||||
export default {
|
||||
"home": "بذور وقصاصات القنب الممتازة", // Fine Cannabis Seeds & Cuttings
|
||||
"aktionen": "العروض والتخفيضات الحالية", // Aktuelle Aktionen & Angebote
|
||||
"filiale": "متجرنا في دريسدن" // Unsere Filiale in Dresden
|
||||
};
|
||||
3
src/i18n/locales/ar/translation.js
Normal file
3
src/i18n/locales/ar/translation.js
Normal file
@@ -0,0 +1,3 @@
|
||||
import translations from './index.js';
|
||||
|
||||
export default translations;
|
||||
25
src/i18n/locales/bg/auth.js
Normal file
25
src/i18n/locales/bg/auth.js
Normal file
@@ -0,0 +1,25 @@
|
||||
export default {
|
||||
"login": "Вход", // Anmelden
|
||||
"register": "Регистрация", // Registrieren
|
||||
"logout": "Изход", // Abmelden
|
||||
"profile": "Профил", // Profil
|
||||
"email": "Имейл", // E-Mail
|
||||
"password": "Парола", // Passwort
|
||||
"confirmPassword": "Потвърдете паролата", // Passwort bestätigen
|
||||
"forgotPassword": "Забравена парола?", // Passwort vergessen?
|
||||
"loginWithGoogle": "Вход с Google", // Mit Google anmelden
|
||||
"or": "ИЛИ", // ODER
|
||||
"privacyAccept": "С натискането на „Вход с Google“ приемам", // Mit dem Click auf "Mit Google anmelden" akzeptiere ich die
|
||||
"privacyPolicy": "Политиката за поверителност", // Datenschutzbestimmungen
|
||||
"passwordMinLength": "Паролата трябва да е поне 8 символа", // Das Passwort muss mindestens 8 Zeichen lang sein
|
||||
"newPasswordMinLength": "Новата парола трябва да е поне 8 символа", // Das neue Passwort muss mindestens 8 Zeichen lang sein
|
||||
"menu": {
|
||||
"profile": "Профил", // Profil
|
||||
"myProfile": "Моят профил", // Mein Profil
|
||||
"checkout": "Плащане", // Bestellabschluss
|
||||
"orders": "Поръчки", // Bestellungen
|
||||
"settings": "Настройки", // Einstellungen
|
||||
"adminDashboard": "Админ панел", // Admin Dashboard
|
||||
"adminUsers": "Админ потребители" // Admin Users
|
||||
}
|
||||
};
|
||||
24
src/i18n/locales/bg/cart.js
Normal file
24
src/i18n/locales/bg/cart.js
Normal file
@@ -0,0 +1,24 @@
|
||||
export default {
|
||||
"title": "Количка", // Warenkorb
|
||||
"empty": "празна", // leer
|
||||
"addToCart": "Добави в количката", // In den Korb
|
||||
"preorderCutting": "Предварителна поръчка като резник", // Als Steckling vorbestellen
|
||||
"continueShopping": "Продължи пазаруването", // Weiter einkaufen
|
||||
"proceedToCheckout": "Продължи към плащане", // Weiter zur Kasse
|
||||
"productCount": "{{count}} {{count, plural, one {продукт} other {продукта}}}", // {{count}} {{count, plural, one {Produkt} other {Produkte}}}
|
||||
"productSingular": "продукт", // Produkt
|
||||
"productPlural": "продукта", // Produkte
|
||||
"removeFromCart": "Премахни от количката", // Aus dem Warenkorb entfernen
|
||||
"openCart": "Отвори количката", // Warenkorb öffnen
|
||||
"availableFrom": "Наличен от {{date}}", // Ab {{date}}
|
||||
"backToOrder": "← Обратно към поръчката", // ← Zurück zur Bestellung
|
||||
"sync": {
|
||||
"title": "Синхронизация на количката", // Warenkorb-Synchronisierung
|
||||
"description": "Имате запазена количка във вашия акаунт. Моля, изберете как искате да продължите:", // Sie haben einen gespeicherten Warenkorb in ihrem Account. Bitte wählen Sie, wie Sie verfahren möchten:
|
||||
"deleteServer": "Изтрий количката на сървъра", // Server-Warenkorb löschen
|
||||
"useServer": "Използвай количката от сървъра", // Server-Warenkorb übernehmen
|
||||
"merge": "Обедини количките", // Warenkörbe zusammenführen
|
||||
"currentCart": "Вашата текуща количка", // Ihr aktueller Warenkorb
|
||||
"serverCart": "Количка, запазена във вашия профил" // In Ihrem Profil gespeicherter Warenkorb
|
||||
}
|
||||
};
|
||||
3
src/i18n/locales/bg/chat.js
Normal file
3
src/i18n/locales/bg/chat.js
Normal file
@@ -0,0 +1,3 @@
|
||||
export default {
|
||||
"privacyRead": "Прочетено и прието", // Gelesen & Akzeptiert
|
||||
};
|
||||
13
src/i18n/locales/bg/checkout.js
Normal file
13
src/i18n/locales/bg/checkout.js
Normal file
@@ -0,0 +1,13 @@
|
||||
export default {
|
||||
"invoiceAddress": "Адрес за фактура", // Rechnungsadresse
|
||||
"deliveryAddress": "Адрес за доставка", // Lieferadresse
|
||||
"saveForFuture": "Запази за бъдещи поръчки", // Für zukünftige Bestellungen speichern
|
||||
"pickupDate": "За коя дата е желано вземането на резниците?", // Für welchen Termin ist die Abholung der Stecklinge gewünscht?
|
||||
"note": "Бележка", // Anmerkung
|
||||
"sameAddress": "Адресът за доставка е същият като адреса за фактура", // Lieferadresse ist identisch mit Rechnungsadresse
|
||||
"termsAccept": "Прочетох общите условия, политиката за поверителност и разпоредбите относно правото на отказ", // Ich habe die AGBs, die Datenschutzerklärung und die Bestimmungen zum Widerrufsrecht gelesen
|
||||
"selectDeliveryMethod": "Изберете метод на доставка", // Versandart wählen
|
||||
"selectPaymentMethod": "Изберете метод на плащане", // Zahlungsart wählen
|
||||
"orderSummary": "Обобщение на поръчката", // Bestellübersicht
|
||||
"addressValidationError": "Моля, проверете въведените данни в полетата за адрес." // Bitte überprüfen Sie Ihre Eingaben in den Adressfeldern.
|
||||
};
|
||||
19
src/i18n/locales/bg/common.js
Normal file
19
src/i18n/locales/bg/common.js
Normal file
@@ -0,0 +1,19 @@
|
||||
export default {
|
||||
"loading": "Зареждане...", // Lädt...
|
||||
"error": "Грешка", // Fehler
|
||||
"close": "Затвори", // Schließen
|
||||
"save": "Запази", // Speichern
|
||||
"cancel": "Отказ", // Abbrechen
|
||||
"ok": "OK", // OK
|
||||
"yes": "Да", // Ja
|
||||
"no": "Не", // Nein
|
||||
"next": "Напред", // Weiter
|
||||
"back": "Назад", // Zurück
|
||||
"edit": "Редактирай", // Bearbeiten
|
||||
"delete": "Изтрий", // Löschen
|
||||
"add": "Добави", // Hinzufügen
|
||||
"remove": "Премахни", // Entfernen
|
||||
"products": "Продукти", // Produkte
|
||||
"product": "Продукт", // Produkt
|
||||
"days": "Дни" // Tage
|
||||
};
|
||||
27
src/i18n/locales/bg/delivery.js
Normal file
27
src/i18n/locales/bg/delivery.js
Normal file
@@ -0,0 +1,27 @@
|
||||
export default {
|
||||
"methods": {
|
||||
"dhl": "DHL", // DHL
|
||||
"dpd": "DPD", // DPD
|
||||
"sperrgut": "Обемни стоки", // Sperrgut
|
||||
"pickup": "Вземане от магазина" // Abholung in der Filiale
|
||||
},
|
||||
"descriptions": {
|
||||
"standard": "Стандартна доставка", // Standardversand
|
||||
"standardFree": "Стандартна доставка - БЕЗПЛАТНО при поръчка над 100€!", // Standardversand - KOSTENLOS ab 100€ Warenwert!
|
||||
"notAvailable": "Не може да се избере, защото един или повече артикули могат да бъдат взети само на място", // nicht auswählbar weil ein oder mehrere Artikel nur abgeholt werden können
|
||||
"bulky": "За големи и тежки артикули", // Für große und schwere Artikel
|
||||
"pickupOnly": "Само вземане" // nur Abholung
|
||||
},
|
||||
"prices": {
|
||||
"free": "безплатно", // kostenlos
|
||||
"freeFrom100": "(безплатно от 100€)", // (kostenlos ab 100€)
|
||||
"dhl": "6.99 €", // 6,99 €
|
||||
"dpd": "4.90 €", // 4,90 €
|
||||
"sperrgut": "28.99 €" // 28,99 €
|
||||
},
|
||||
"times": {
|
||||
"cutting14Days": "Срок на доставка: 14 дни", // Lieferzeit: 14 Tage
|
||||
"standard2to3Days": "Срок на доставка: 2-3 дни", // Lieferzeit: 2-3 Tage
|
||||
"supplier7to9Days": "Срок на доставка: 7-9 дни" // Lieferzeit: 7-9 Tage
|
||||
}
|
||||
};
|
||||
7
src/i18n/locales/bg/filters.js
Normal file
7
src/i18n/locales/bg/filters.js
Normal file
@@ -0,0 +1,7 @@
|
||||
export default {
|
||||
"sorting": "Сортиране", // Sortierung
|
||||
"perPage": "на страница", // pro Seite
|
||||
"availability": "Наличност", // Verfügbarkeit
|
||||
"manufacturer": "Производител", // Hersteller
|
||||
"all": "Всички", // Alle
|
||||
};
|
||||
15
src/i18n/locales/bg/footer.js
Normal file
15
src/i18n/locales/bg/footer.js
Normal file
@@ -0,0 +1,15 @@
|
||||
export default {
|
||||
"hours": "Съб 11:00-19:00", // Sa 11-19
|
||||
"address": "Trachenberger Straße 14 - Dresden", // Trachenberger Straße 14 - Dresden
|
||||
"location": "Между спирка Пишен и Trachenberger Platz", // Zwischen Haltepunkt Pieschen und Trachenberger Platz
|
||||
"allPricesIncl": "* Всички цени включват законен ДДС, плюс доставка", // * Alle Preise inkl. gesetzlicher USt., zzgl. Versand
|
||||
"copyright": "© {{year}} GrowHeads.de", // © {{year}} GrowHeads.de
|
||||
"legal": {
|
||||
"datenschutz": "Политика за поверителност", // Datenschutz
|
||||
"agb": "Общи условия", // AGB
|
||||
"sitemap": "Карта на сайта", // Sitemap
|
||||
"impressum": "Правен адрес", // Impressum
|
||||
"batteriegesetzhinweise": "Информация за закона за батериите", // Batteriegesetzhinweise
|
||||
"widerrufsrecht": "Право на отказ" // Widerrufsrecht
|
||||
}
|
||||
};
|
||||
43
src/i18n/locales/bg/index.js
Normal file
43
src/i18n/locales/bg/index.js
Normal file
@@ -0,0 +1,43 @@
|
||||
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
|
||||
};
|
||||
3
src/i18n/locales/bg/locale.js
Normal file
3
src/i18n/locales/bg/locale.js
Normal file
@@ -0,0 +1,3 @@
|
||||
export default {
|
||||
"code": "bg-BG" // de-DE
|
||||
};
|
||||
9
src/i18n/locales/bg/navigation.js
Normal file
9
src/i18n/locales/bg/navigation.js
Normal file
@@ -0,0 +1,9 @@
|
||||
export default {
|
||||
"home": "Начало", // Startseite
|
||||
"aktionen": "Промоции", // Aktionen
|
||||
"filiale": "Клон", // Filiale
|
||||
"categories": "Категории", // Kategorien
|
||||
"categoriesOpen": "Отвори категории", // Kategorien öffnen
|
||||
"categoriesClose": "Затвори категории", // Kategorien schließen
|
||||
"otherCategories": "Други категории" // Andere Kategorien
|
||||
};
|
||||
23
src/i18n/locales/bg/orders.js
Normal file
23
src/i18n/locales/bg/orders.js
Normal file
@@ -0,0 +1,23 @@
|
||||
export default {
|
||||
"status": {
|
||||
"new": "В процес", // in Bearbeitung
|
||||
"pending": "Ново", // Neu
|
||||
"processing": "В процес", // in Bearbeitung
|
||||
"cancelled": "Отменено", // Storniert
|
||||
"shipped": "Изпратено", // Verschickt
|
||||
"delivered": "Доставено", // Geliefert
|
||||
"return": "Връщане", // Retoure
|
||||
"partialReturn": "Частично връщане", // Teil Retoure
|
||||
"partialDelivered": "Частично доставено" // Teil geliefert
|
||||
},
|
||||
"table": {
|
||||
"orderNumber": "Номер на поръчка", // Bestellnummer
|
||||
"date": "Дата", // Datum
|
||||
"status": "Статус", // Status
|
||||
"items": "Артикули", // Artikel
|
||||
"total": "Общо", // Summe
|
||||
"actions": "Действия", // Aktionen
|
||||
"viewDetails": "Виж подробности" // Details anzeigen
|
||||
},
|
||||
"noOrders": "Все още не сте направили поръчки." // Sie haben noch keine Bestellungen aufgegeben.
|
||||
};
|
||||
10
src/i18n/locales/bg/pages.js
Normal file
10
src/i18n/locales/bg/pages.js
Normal file
@@ -0,0 +1,10 @@
|
||||
export default {
|
||||
"oilPress": {
|
||||
"title": "Наемане на маслобойка", // Ölpresse ausleihen
|
||||
"comingSoon": "Съдържанието ще бъде налично скоро...", // Inhalt kommt bald...
|
||||
},
|
||||
"thcTest": {
|
||||
"title": "THC тест", // THC Test
|
||||
"comingSoon": "Съдържанието ще бъде налично скоро...", // Inhalt kommt bald...
|
||||
}
|
||||
};
|
||||
8
src/i18n/locales/bg/payment.js
Normal file
8
src/i18n/locales/bg/payment.js
Normal file
@@ -0,0 +1,8 @@
|
||||
export default {
|
||||
"successful": "Плащането беше успешно!", // Zahlung erfolgreich!
|
||||
"failed": "Плащането не бе успешно", // Zahlung fehlgeschlagen
|
||||
"orderCompleted": "🎉 Вашата поръчка беше успешно завършена! Сега можете да видите вашите поръчки.", // 🎉 Ihre Bestellung wurde erfolgreich abgeschlossen! Sie können jetzt Ihre Bestellungen einsehen.
|
||||
"orderProcessing": "Вашето плащане беше успешно обработено. Поръчката ще бъде завършена автоматично.", // Ihre Zahlung wurde erfolgreich verarbeitet. Die Bestellung wird automatisch abgeschlossen.
|
||||
"paymentError": "Вашето плащане не можа да бъде обработено. Моля, опитайте отново или изберете друг метод на плащане.", // Ihre Zahlung konnte nicht verarbeitet werden. Bitte versuchen Sie es erneut oder wählen Sie eine andere Zahlungsmethode.
|
||||
"viewOrders": "Отиди на моите поръчки" // Zu meinen Bestellungen
|
||||
};
|
||||
32
src/i18n/locales/bg/product.js
Normal file
32
src/i18n/locales/bg/product.js
Normal file
@@ -0,0 +1,32 @@
|
||||
export default {
|
||||
"loading": "Зареждане на продукта...", // Produkt wird geladen...
|
||||
"notFound": "Продуктът не е намерен", // Produkt nicht gefunden
|
||||
"notFoundDescription": "Продуктът, който търсите, не съществува или е бил премахнат.", // Das gesuchte Produkt existiert nicht oder wurde entfernt.
|
||||
"backToHome": "Обратно към началната страница", // Zurück zur Startseite
|
||||
"error": "Грешка", // Fehler
|
||||
"articleNumber": "Номер на артикул", // Artikelnummer
|
||||
"manufacturer": "Производител", // Hersteller
|
||||
"inclVat": "вкл. {{vat}}% ДДС", // inkl. {{vat}}% MwSt.
|
||||
"priceUnit": "{{price}}/{{unit}}", // {{price}}/{{unit}}
|
||||
"new": "Нов", // Neu
|
||||
"weeks": "седмици", // Wochen
|
||||
"arriving": "Пристига:", // Ankunft:
|
||||
"inclVatFooter": "вкл. {{vat}}% ДДС,*", // inkl. {{vat}}% MwSt.,*
|
||||
"availability": "Наличност", // Verfügbarkeit
|
||||
"inStock": "налично на склад", // auf Lager
|
||||
"comingSoon": "Очаквайте скоро", // Bald verfügbar
|
||||
"deliveryTime": "Срок на доставка", // Lieferzeit
|
||||
"inclShort": "вкл.", // inkl.
|
||||
"vatShort": "ДДС", // MwSt.
|
||||
"countDisplay": {
|
||||
"noProducts": "0 продукта", // 0 Produkte
|
||||
"oneProduct": "1 продукт", // 1 Produkt
|
||||
"multipleProducts": "{{count}} продукта", // {{count}} Produkte
|
||||
"filteredProducts": "{{filtered}} от {{total}} продукта", // {{filtered}} von {{total}} Produkten
|
||||
"filteredOneProduct": "{{filtered}} от 1 продукт", // {{filtered}} von 1 Produkt
|
||||
"xOfYProducts": "{{x}} от {{y}} продукта" // {{x}} von {{y}} Produkten
|
||||
},
|
||||
"removeFiltersToSee": "Премахнете филтрите, за да видите продуктите", // Entferne Filter um Produkte zu sehen
|
||||
"outOfStock": "Изчерпано количество", // Out of Stock
|
||||
"fromXProducts": "от {{count}} продукта" // ab {{count}} Produkten
|
||||
};
|
||||
5
src/i18n/locales/bg/search.js
Normal file
5
src/i18n/locales/bg/search.js
Normal file
@@ -0,0 +1,5 @@
|
||||
export default {
|
||||
"placeholder": "Можете да ме попитате за сортове канабис...", // Du kannst mich nach Cannabissorten fragen...
|
||||
"recording": "Записът е в ход...", // Aufnahme läuft...
|
||||
"searchProducts": "Търсене на продукти...", // Produkte suchen...
|
||||
};
|
||||
8
src/i18n/locales/bg/sections.js
Normal file
8
src/i18n/locales/bg/sections.js
Normal file
@@ -0,0 +1,8 @@
|
||||
export default {
|
||||
"seeds": "Семена", // Seeds
|
||||
"stecklinge": "Резници", // Stecklinge
|
||||
"oilPress": "Наемане на маслопреса", // Ölpresse ausleihen
|
||||
"thcTest": "THC тест", // THC Test
|
||||
"address1": "Trachenberger Straße 14", // Trachenberger Straße 14
|
||||
"address2": "01129 Dresden" // 01129 Dresden
|
||||
};
|
||||
34
src/i18n/locales/bg/settings.js
Normal file
34
src/i18n/locales/bg/settings.js
Normal file
@@ -0,0 +1,34 @@
|
||||
export default {
|
||||
"changePassword": "Смяна на парола", // Passwort ändern
|
||||
"currentPassword": "Текуща парола", // Aktuelles Passwort
|
||||
"newPassword": "Нова парола", // Neues Passwort
|
||||
"confirmNewPassword": "Потвърдете новата парола", // Neues Passwort bestätigen
|
||||
"updatePassword": "Актуализиране на паролата", // Passwort aktualisieren
|
||||
"changeEmail": "Смяна на имейл адрес", // E-Mail-Adresse ändern
|
||||
"password": "Парола", // Passwort
|
||||
"newEmail": "Нов имейл адрес", // Neue E-Mail-Adresse
|
||||
"updateEmail": "Актуализиране на имейла", // E-Mail aktualisieren
|
||||
"apiKey": "API ключ", // API-Schlüssel
|
||||
"apiKeyDescription": "Използвайте своя API ключ за интеграция с външни приложения.", // Verwenden Sie Ihren API-Schlüssel für die Integration mit externen Anwendungen.
|
||||
"apiDocumentation": "API документация:", // API-Dokumentation:
|
||||
"copyToClipboard": "Копиране в клипборда", // In Zwischenablage kopieren
|
||||
"generate": "Генериране", // Generieren
|
||||
"regenerate": "Генериране отново", // Regenerieren
|
||||
"apiKeyCopied": "API ключът е копиран в клипборда", // API-Schlüssel in Zwischenablage kopiert
|
||||
"errors": {
|
||||
"fillAllFields": "Моля, попълнете всички полета", // Bitte füllen Sie alle Felder aus
|
||||
"passwordsNotMatch": "Новите пароли не съвпадат", // Die neuen Passwörter stimmen nicht überein
|
||||
"passwordTooShort": "Новата парола трябва да е поне 8 знака", // Das neue Passwort muss mindestens 8 Zeichen lang sein
|
||||
"passwordUpdateError": "Грешка при актуализиране на паролата", // Fehler beim Aktualisieren des Passworts
|
||||
"invalidEmail": "Моля, въведете валиден имейл адрес", // Bitte geben Sie eine gültige E-Mail-Adresse ein
|
||||
"emailUpdateError": "Грешка при актуализиране на имейл адреса", // Fehler beim Aktualisieren der E-Mail-Adresse
|
||||
"userNotFound": "Потребителят не е намерен", // Benutzer nicht gefunden
|
||||
"apiKeyGenerationError": "Грешка при генериране на API ключ" // Fehler beim Generieren des API-Schlüssels
|
||||
},
|
||||
"success": {
|
||||
"passwordUpdated": "Паролата е успешно актуализирана", // Passwort erfolgreich aktualisiert
|
||||
"emailUpdated": "Имейл адресът е успешно актуализиран", // E-Mail-Adresse erfolgreich aktualisiert
|
||||
"apiKeyGenerated": "API ключът е успешно генериран", // API-Schlüssel erfolgreich generiert
|
||||
"apiKeyWarning": "Съхранявайте този ключ на сигурно място. По съображения за сигурност той ще бъде скрит след 10 секунди." // Speichern Sie diesen Schlüssel sicher. Er wird aus Sicherheitsgründen in 10 Sekunden ausgeblendet.
|
||||
}
|
||||
};
|
||||
6
src/i18n/locales/bg/sorting.js
Normal file
6
src/i18n/locales/bg/sorting.js
Normal file
@@ -0,0 +1,6 @@
|
||||
export default {
|
||||
"name": "Име", // Name
|
||||
"searchField": "Търсене", // Suchbegriff
|
||||
"priceLowHigh": "Цена: от ниска към висока", // Preis: Niedrig zu Hoch
|
||||
"priceHighLow": "Цена: от висока към ниска" // Preis: Hoch zu Niedrig
|
||||
};
|
||||
12
src/i18n/locales/bg/tax.js
Normal file
12
src/i18n/locales/bg/tax.js
Normal file
@@ -0,0 +1,12 @@
|
||||
export default {
|
||||
"vat": "Данък върху добавената стойност", // Mehrwertsteuer
|
||||
"vat7": "7% Данък върху добавената стойност", // 7% Mehrwertsteuer
|
||||
"vat19": "19% Данък върху добавената стойност", // 19% Mehrwertsteuer
|
||||
"vat19WithShipping": "19% Данък върху добавената стойност (вкл. доставка)", // 19% Mehrwertsteuer (inkl. Versand)
|
||||
"totalNet": "Обща нетна цена", // Gesamtnettopreis
|
||||
"totalGross": "Обща брутна цена без доставка", // Gesamtbruttopreis ohne Versand
|
||||
"subtotal": "Междинна сума", // Zwischensumme
|
||||
"incl7Vat": "вкл. 7% ДДС", // inkl. 7% MwSt.
|
||||
"inclVatWithFooter": "(вкл. {{vat}}% ДДС,*)", // (incl. {{vat}}% USt.,*)
|
||||
"inclVatAmount": "вкл. {{amount}} € ДДС ({{rate}}%)" // nkl. {{amount}} € MwSt. ({{rate}}%)
|
||||
};
|
||||
5
src/i18n/locales/bg/titles.js
Normal file
5
src/i18n/locales/bg/titles.js
Normal file
@@ -0,0 +1,5 @@
|
||||
export default {
|
||||
"home": "Фини семена и резници от канабис", // Fine Cannabis Seeds & Cuttings
|
||||
"aktionen": "Текущи промоции и оферти", // Aktuelle Aktionen & Angebote
|
||||
"filiale": "Нашият магазин в Дрезден" // Unsere Filiale in Dresden
|
||||
};
|
||||
3
src/i18n/locales/bg/translation.js
Normal file
3
src/i18n/locales/bg/translation.js
Normal file
@@ -0,0 +1,3 @@
|
||||
import translations from './index.js';
|
||||
|
||||
export default translations;
|
||||
25
src/i18n/locales/cs/auth.js
Normal file
25
src/i18n/locales/cs/auth.js
Normal file
@@ -0,0 +1,25 @@
|
||||
export default {
|
||||
"login": "Přihlásit se", // Anmelden
|
||||
"register": "Registrovat se", // Registrieren
|
||||
"logout": "Odhlásit se", // Abmelden
|
||||
"profile": "Profil", // Profil
|
||||
"email": "Email", // E-Mail
|
||||
"password": "Heslo", // Passwort
|
||||
"confirmPassword": "Potvrdit heslo", // Passwort bestätigen
|
||||
"forgotPassword": "Zapomněli jste heslo?", // Passwort vergessen?
|
||||
"loginWithGoogle": "Přihlásit se přes Google", // Mit Google anmelden
|
||||
"or": "NEBO", // ODER
|
||||
"privacyAccept": "Kliknutím na „Přihlásit se přes Google“ souhlasím s", // Mit dem Click auf "Mit Google anmelden" akzeptiere ich die
|
||||
"privacyPolicy": "Zásadami ochrany osobních údajů", // Datenschutzbestimmungen
|
||||
"passwordMinLength": "Heslo musí mít alespoň 8 znaků", // Das Passwort muss mindestens 8 Zeichen lang sein
|
||||
"newPasswordMinLength": "Nové heslo musí mít alespoň 8 znaků", // Das neue Passwort muss mindestens 8 Zeichen lang sein
|
||||
"menu": {
|
||||
"profile": "Profil", // Profil
|
||||
"myProfile": "Můj profil", // Mein Profil
|
||||
"checkout": "Dokončení objednávky", // Bestellabschluss
|
||||
"orders": "Objednávky", // Bestellungen
|
||||
"settings": "Nastavení", // Einstellungen
|
||||
"adminDashboard": "Administrátorský panel", // Admin Dashboard
|
||||
"adminUsers": "Administrátoři" // Admin Users
|
||||
}
|
||||
};
|
||||
24
src/i18n/locales/cs/cart.js
Normal file
24
src/i18n/locales/cs/cart.js
Normal file
@@ -0,0 +1,24 @@
|
||||
export default {
|
||||
"title": "Košík", // Warenkorb
|
||||
"empty": "prázdný", // leer
|
||||
"addToCart": "Přidat do košíku", // In den Korb
|
||||
"preorderCutting": "Předobjednat jako řízek", // Als Steckling vorbestellen
|
||||
"continueShopping": "Pokračovat v nákupu", // Weiter einkaufen
|
||||
"proceedToCheckout": "Přejít k pokladně", // Weiter zur Kasse
|
||||
"productCount": "{{count}} {{count, plural, one {produkt} other {produkty}}}", // {{count}} {{count, plural, one {Produkt} other {Produkte}}}
|
||||
"productSingular": "produkt", // Produkt
|
||||
"productPlural": "produkty", // Produkte
|
||||
"removeFromCart": "Odstranit z košíku", // Aus dem Warenkorb entfernen
|
||||
"openCart": "Otevřít košík", // Warenkorb öffnen
|
||||
"availableFrom": "Dostupné od {{date}}", // Ab {{date}}
|
||||
"backToOrder": "← Zpět k objednávce", // ← Zurück zur Bestellung
|
||||
"sync": {
|
||||
"title": "Synchronizace košíku", // Warenkorb-Synchronisierung
|
||||
"description": "Máte uložený košík ve svém účtu. Vyberte, jak chcete pokračovat:", // Sie haben einen gespeicherten Warenkorb in ihrem Account. Bitte wählen Sie, wie Sie verfahren möchten:
|
||||
"deleteServer": "Smazat košík na serveru", // Server-Warenkorb löschen
|
||||
"useServer": "Použít košík ze serveru", // Server-Warenkorb übernehmen
|
||||
"merge": "Sloučit košíky", // Warenkörbe zusammenführen
|
||||
"currentCart": "Váš aktuální košík", // Ihr aktueller Warenkorb
|
||||
"serverCart": "Košík uložený ve vašem profilu" // In Ihrem Profil gespeicherter Warenkorb
|
||||
}
|
||||
};
|
||||
3
src/i18n/locales/cs/chat.js
Normal file
3
src/i18n/locales/cs/chat.js
Normal file
@@ -0,0 +1,3 @@
|
||||
export default {
|
||||
"privacyRead": "Přečteno a přijato", // Gelesen & Akzeptiert
|
||||
};
|
||||
13
src/i18n/locales/cs/checkout.js
Normal file
13
src/i18n/locales/cs/checkout.js
Normal file
@@ -0,0 +1,13 @@
|
||||
export default {
|
||||
"invoiceAddress": "Fakturační adresa", // Rechnungsadresse
|
||||
"deliveryAddress": "Dodací adresa", // Lieferadresse
|
||||
"saveForFuture": "Uložit pro budoucí objednávky", // Für zukünftige Bestellungen speichern
|
||||
"pickupDate": "Na který datum je požadován odběr řízků?", // Für welchen Termin ist die Abholung der Stecklinge gewünscht?
|
||||
"note": "Poznámka", // Anmerkung
|
||||
"sameAddress": "Dodací adresa je shodná s fakturační adresou", // Lieferadresse ist identisch mit Rechnungsadresse
|
||||
"termsAccept": "Přečetl(a) jsem si obchodní podmínky, zásady ochrany osobních údajů a ustanovení o právu na odstoupení od smlouvy", // Ich habe die AGBs, die Datenschutzerklärung und die Bestimmungen zum Widerrufsrecht gelesen
|
||||
"selectDeliveryMethod": "Vyberte způsob doručení", // Versandart wählen
|
||||
"selectPaymentMethod": "Vyberte způsob platby", // Zahlungsart wählen
|
||||
"orderSummary": "Souhrn objednávky", // Bestellübersicht
|
||||
"addressValidationError": "Zkontrolujte prosím své údaje v polích adresy." // Bitte überprüfen Sie Ihre Eingaben in den Adressfeldern.
|
||||
};
|
||||
19
src/i18n/locales/cs/common.js
Normal file
19
src/i18n/locales/cs/common.js
Normal file
@@ -0,0 +1,19 @@
|
||||
export default {
|
||||
"loading": "Načítání...", // Lädt...
|
||||
"error": "Chyba", // Fehler
|
||||
"close": "Zavřít", // Schließen
|
||||
"save": "Uložit", // Speichern
|
||||
"cancel": "Zrušit", // Abbrechen
|
||||
"ok": "OK", // OK
|
||||
"yes": "Ano", // Ja
|
||||
"no": "Ne", // Nein
|
||||
"next": "Další", // Weiter
|
||||
"back": "Zpět", // Zurück
|
||||
"edit": "Upravit", // Bearbeiten
|
||||
"delete": "Smazat", // Löschen
|
||||
"add": "Přidat", // Hinzufügen
|
||||
"remove": "Odebrat", // Entfernen
|
||||
"products": "Produkty", // Produkte
|
||||
"product": "Produkt", // Produkt
|
||||
"days": "Dny" // Tage
|
||||
};
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user