Compare commits
71 Commits
mollie
...
8bc80c872d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8bc80c872d | ||
|
|
85504c725f | ||
|
|
04e97c2522 | ||
|
|
93887ce397 | ||
|
|
33fadc0279 | ||
|
|
5c2b4172da | ||
|
|
c663e902ea | ||
|
|
aa82e8d1d2 | ||
|
|
67f0126343 | ||
|
|
47a882b667 | ||
|
|
e43b894bfc | ||
|
|
c9477e53b6 | ||
|
|
0015872894 | ||
|
|
cb8ce69903 | ||
|
|
64048e6d0b | ||
|
|
e4b70dcbe2 | ||
|
|
1de3ba0115 | ||
|
|
4ef27da561 | ||
|
|
0895919448 | ||
|
|
6a017400fa | ||
|
|
3611908021 | ||
|
|
98393cea21 | ||
|
|
c078abec1a | ||
|
|
a7cfbce072 | ||
|
|
65611865c8 | ||
|
|
a8c77e1107 | ||
|
|
4f5bc96c9b | ||
|
|
3e3e676ded | ||
|
|
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
|
# Local configuration
|
||||||
src/config.local.js
|
src/config.local.js
|
||||||
|
|
||||||
|
taxonomy-with-ids.de-DE*
|
||||||
|
|
||||||
# Local development notes
|
# Local development notes
|
||||||
dev-notes.md
|
dev-notes.md
|
||||||
dev-notes.local.md
|
dev-notes.local.md
|
||||||
19
.vscode/launch.json
vendored
19
.vscode/launch.json
vendored
@@ -3,20 +3,31 @@
|
|||||||
// This will install dependencies before starting the dev server
|
// This will install dependencies before starting the dev server
|
||||||
"version": "0.2.0",
|
"version": "0.2.0",
|
||||||
"configurations": [
|
"configurations": [
|
||||||
|
|
||||||
{
|
{
|
||||||
"type": "node-terminal",
|
"type": "node-terminal",
|
||||||
"name": "Start with API propxy to seedheads.de (Install Deps)",
|
"name": "Start with API propxy to seedheads.de (Install Deps)",
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"command": "npm run start:seedheads",
|
"command": "npm run start:seedheads",
|
||||||
"preLaunchTask": "npm: install",
|
"preLaunchTask": "npm: install",
|
||||||
"cwd": "${workspaceFolder}"
|
"env": {
|
||||||
|
"NODE_ENV": "development"
|
||||||
|
},
|
||||||
|
"envFile": "${workspaceFolder}/.env",
|
||||||
|
"skipFiles": [
|
||||||
|
"<node_internals>/**"
|
||||||
|
]
|
||||||
},{
|
},{
|
||||||
"type": "node-terminal",
|
|
||||||
"name": "Start",
|
"name": "Start",
|
||||||
|
"type": "node-terminal",
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"command": "npm run start",
|
"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}",
|
"lint": "eslint src/**/*.{js,jsx}",
|
||||||
"prerender": "node prerender.cjs",
|
"prerender": "node prerender.cjs",
|
||||||
"prerender:prod": "cross-env NODE_ENV=production 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": [],
|
"keywords": [],
|
||||||
"author": "",
|
"author": "",
|
||||||
@@ -27,10 +31,15 @@
|
|||||||
"@stripe/react-stripe-js": "^3.7.0",
|
"@stripe/react-stripe-js": "^3.7.0",
|
||||||
"@stripe/stripe-js": "^7.3.1",
|
"@stripe/stripe-js": "^7.3.1",
|
||||||
"chart.js": "^4.5.0",
|
"chart.js": "^4.5.0",
|
||||||
|
"country-flag-icons": "^1.5.19",
|
||||||
"html-react-parser": "^5.2.5",
|
"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": "^19.1.0",
|
||||||
"react-chartjs-2": "^5.3.0",
|
"react-chartjs-2": "^5.3.0",
|
||||||
"react-dom": "^19.1.0",
|
"react-dom": "^19.1.0",
|
||||||
|
"react-i18next": "^15.6.0",
|
||||||
"react-router-dom": "^7.6.2",
|
"react-router-dom": "^7.6.2",
|
||||||
"sharp": "^0.34.2",
|
"sharp": "^0.34.2",
|
||||||
"socket.io-client": "^4.7.5"
|
"socket.io-client": "^4.7.5"
|
||||||
|
|||||||
@@ -27,6 +27,74 @@ const io = require("socket.io-client");
|
|||||||
const os = require("os");
|
const os = require("os");
|
||||||
const { Worker, isMainThread, parentPort, workerData } = require("worker_threads");
|
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
|
// Import split modules
|
||||||
const config = require("./prerender/config.cjs");
|
const config = require("./prerender/config.cjs");
|
||||||
|
|
||||||
@@ -107,6 +175,7 @@ const renderProductWorker = async (productSeoNames, workerId, progressCallback,
|
|||||||
const actualSeoName = productDetails.product.seoName || productSeoName;
|
const actualSeoName = productDetails.product.seoName || productSeoName;
|
||||||
const productComponent = React.createElement(PrerenderProduct, {
|
const productComponent = React.createElement(PrerenderProduct, {
|
||||||
productData: productDetails,
|
productData: productDetails,
|
||||||
|
t: global.i18n.t.bind(global.i18n),
|
||||||
});
|
});
|
||||||
|
|
||||||
const filename = `Artikel/${actualSeoName}`;
|
const filename = `Artikel/${actualSeoName}`;
|
||||||
|
|||||||
@@ -11,6 +11,99 @@ Crawl-delay: 0
|
|||||||
return robotsTxt;
|
return robotsTxt;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Helper function to determine unit pricing data based on product data
|
||||||
|
const determineUnitPricingData = (product) => {
|
||||||
|
const result = {
|
||||||
|
unit_pricing_measure: null,
|
||||||
|
unit_pricing_base_measure: null
|
||||||
|
};
|
||||||
|
|
||||||
|
// Unit mapping from German to Google Shopping accepted units
|
||||||
|
const unitMapping = {
|
||||||
|
// Volume (German -> Google)
|
||||||
|
'Milliliter': 'ml',
|
||||||
|
'milliliter': 'ml',
|
||||||
|
'ml': 'ml',
|
||||||
|
'Liter': 'l',
|
||||||
|
'liter': 'l',
|
||||||
|
'l': 'l',
|
||||||
|
'Zentiliter': 'cl',
|
||||||
|
'zentiliter': 'cl',
|
||||||
|
'cl': 'cl',
|
||||||
|
|
||||||
|
// Weight (German -> Google)
|
||||||
|
'Gramm': 'g',
|
||||||
|
'gramm': 'g',
|
||||||
|
'g': 'g',
|
||||||
|
'Kilogramm': 'kg',
|
||||||
|
'kilogramm': 'kg',
|
||||||
|
'kg': 'kg',
|
||||||
|
'Milligramm': 'mg',
|
||||||
|
'milligramm': 'mg',
|
||||||
|
'mg': 'mg',
|
||||||
|
|
||||||
|
// Length (German -> Google)
|
||||||
|
'Meter': 'm',
|
||||||
|
'meter': 'm',
|
||||||
|
'm': 'm',
|
||||||
|
'Zentimeter': 'cm',
|
||||||
|
'zentimeter': 'cm',
|
||||||
|
'cm': 'cm',
|
||||||
|
|
||||||
|
// Count (German -> Google)
|
||||||
|
'Stück': 'ct',
|
||||||
|
'stück': 'ct',
|
||||||
|
'Stk': 'ct',
|
||||||
|
'stk': 'ct',
|
||||||
|
'ct': 'ct',
|
||||||
|
'Blatt': 'sheet',
|
||||||
|
'blatt': 'sheet',
|
||||||
|
'sheet': 'sheet'
|
||||||
|
};
|
||||||
|
|
||||||
|
// Helper function to convert German unit to Google Shopping unit
|
||||||
|
const convertUnit = (unit) => {
|
||||||
|
if (!unit) return null;
|
||||||
|
const trimmedUnit = unit.trim();
|
||||||
|
return unitMapping[trimmedUnit] || trimmedUnit.toLowerCase();
|
||||||
|
};
|
||||||
|
|
||||||
|
// unit_pricing_measure: The quantity unit of the product as it's sold
|
||||||
|
if (product.fEinheitMenge && product.cEinheit) {
|
||||||
|
const amount = parseFloat(product.fEinheitMenge);
|
||||||
|
const unit = convertUnit(product.cEinheit);
|
||||||
|
|
||||||
|
if (amount > 0 && unit) {
|
||||||
|
result.unit_pricing_measure = `${amount} ${unit}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// unit_pricing_base_measure: The base quantity unit for unit pricing
|
||||||
|
if (product.cGrundEinheit && product.cGrundEinheit.trim()) {
|
||||||
|
const baseUnit = convertUnit(product.cGrundEinheit);
|
||||||
|
if (baseUnit) {
|
||||||
|
// Base measure usually needs a quantity (like 100g, 1l, etc.)
|
||||||
|
// If it's just a unit, we'll add a default quantity
|
||||||
|
if (baseUnit.match(/^[a-z]+$/)) {
|
||||||
|
// For weight/volume units, use standard base quantities
|
||||||
|
if (['g', 'kg', 'mg'].includes(baseUnit)) {
|
||||||
|
result.unit_pricing_base_measure = baseUnit === 'kg' ? '1 kg' : '100 g';
|
||||||
|
} else if (['ml', 'l', 'cl'].includes(baseUnit)) {
|
||||||
|
result.unit_pricing_base_measure = baseUnit === 'l' ? '1 l' : '100 ml';
|
||||||
|
} else if (['m', 'cm'].includes(baseUnit)) {
|
||||||
|
result.unit_pricing_base_measure = baseUnit === 'm' ? '1 m' : '100 cm';
|
||||||
|
} else {
|
||||||
|
result.unit_pricing_base_measure = `1 ${baseUnit}`;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
result.unit_pricing_base_measure = baseUnit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
const generateProductsXml = (allProductsData = [], baseUrl, config) => {
|
const generateProductsXml = (allProductsData = [], baseUrl, config) => {
|
||||||
const currentDate = new Date().toISOString();
|
const currentDate = new Date().toISOString();
|
||||||
|
|
||||||
@@ -23,124 +116,131 @@ const generateProductsXml = (allProductsData = [], baseUrl, config) => {
|
|||||||
const getGoogleProductCategory = (categoryId) => {
|
const getGoogleProductCategory = (categoryId) => {
|
||||||
const categoryMappings = {
|
const categoryMappings = {
|
||||||
// Seeds & Plants
|
// Seeds & Plants
|
||||||
689: "Home & Garden > Plants > Seeds",
|
689: "543561", // Seeds (Saatgut)
|
||||||
706: "Home & Garden > Plants", // Stecklinge (cuttings)
|
706: "543561", // Stecklinge (cuttings) – ebenfalls Pflanzen/Saatgut
|
||||||
376: "Home & Garden > Plants > Plant & Herb Growing Kits", // Grow-Sets
|
376: "2802", // Grow-Sets – Pflanzen- & Kräuteranbausets
|
||||||
|
|
||||||
// Headshop & Accessories
|
// Headshop & Accessories
|
||||||
709: "Arts & Entertainment > Hobbies & Creative Arts", // Headshop
|
709: "4082", // Headshop – Rauchzubehör
|
||||||
711: "Arts & Entertainment > Hobbies & Creative Arts", // Bongs
|
711: "4082", // Headshop > Bongs – Rauchzubehör
|
||||||
714: "Arts & Entertainment > Hobbies & Creative Arts", // Zubehör
|
714: "4082", // Headshop > Bongs > Zubehör – Rauchzubehör
|
||||||
748: "Arts & Entertainment > Hobbies & Creative Arts", // Köpfe
|
748: "4082", // Headshop > Bongs > Köpfe – Rauchzubehör
|
||||||
749: "Arts & Entertainment > Hobbies & Creative Arts", // Chillums / Diffusoren / Kupplungen
|
749: "4082", // Headshop > Bongs > Chillums/Diffusoren/Kupplungen – Rauchzubehör
|
||||||
896: "Electronics > Electronics Accessories", // Vaporizer
|
896: "3151", // Headshop > Vaporizer – Vaporizer
|
||||||
710: "Home & Garden > Kitchen & Dining > Kitchen Tools & Utensils", // Grinder
|
710: "5109", // Headshop > Grinder – Gewürzmühlen (Küchenhelfer)
|
||||||
|
|
||||||
// Measuring & Packaging
|
// Measuring & Packaging
|
||||||
186: "Business & Industrial", // Wiegen & Verpacken
|
186: "5631", // Headshop > Wiegen & Verpacken – Aufbewahrung/Zubehör
|
||||||
187: "Business & Industrial > Science & Laboratory > Lab Equipment", // Waagen
|
187: "4767", // Headshop > Waagen – Personenwaagen (Medizinisch)
|
||||||
346: "Home & Garden > Kitchen & Dining > Food Storage", // Vakuumbeutel
|
346: "7118", // Headshop > Vakuumbeutel – Vakuumierer-Beutel
|
||||||
355: "Home & Garden > Kitchen & Dining > Food Storage", // Boveda & Integra Boost
|
355: "606", // Headshop > Boveda & Integra Boost – Luftentfeuchter (nächstmögliche)
|
||||||
407: "Home & Garden > Kitchen & Dining > Food Storage", // Grove Bags
|
407: "3561", // Headshop > Grove Bags – Aufbewahrungsbehälter
|
||||||
449: "Home & Garden > Kitchen & Dining > Food Storage", // Cliptütchen
|
449: "1496", // Headshop > Cliptütchen – Lebensmittelverpackungsmaterial
|
||||||
539: "Home & Garden > Kitchen & Dining > Food Storage", // Gläser & Dosen
|
539: "3110", // Headshop > Gläser & Dosen – Lebensmittelbehälter
|
||||||
|
|
||||||
// Lighting & Equipment
|
// Lighting & Equipment
|
||||||
694: "Home & Garden > Lighting", // Lampen
|
694: "3006", // Lampen – Lampen (Beleuchtung)
|
||||||
261: "Home & Garden > Lighting", // Lampenzubehör
|
261: "3006", // Zubehör > Lampenzubehör – Lampen
|
||||||
|
|
||||||
// Plants & Growing
|
// Plants & Growing
|
||||||
691: "Home & Garden > Lawn & Garden > Fertilizers", // Dünger
|
691: "500033", // Dünger – Dünger
|
||||||
692: "Home & Garden > Lawn & Garden > Fertilizers", // Dünger - Zubehör
|
692: "5633", // Zubehör > Dünger-Zubehör – Zubehör für Gartenarbeit
|
||||||
693: "Sporting Goods > Outdoor Recreation > Camping & Hiking > Tents", // Zelte
|
693: "5655", // Zelte – Zelte
|
||||||
|
|
||||||
// Pots & Containers
|
// Pots & Containers
|
||||||
219: "Home & Garden > Decor > Planters & Pots", // Töpfe
|
219: "113", // Töpfe – Blumentöpfe & Pflanzgefäße
|
||||||
220: "Home & Garden > Decor > Planters & Pots", // Untersetzer
|
220: "3173", // Töpfe > Untersetzer – Gartentopfuntersetzer und Trays
|
||||||
301: "Home & Garden > Decor > Planters & Pots", // Stofftöpfe
|
301: "113", // Töpfe > Stofftöpfe – (Blumentöpfe/Pflanzgefäße)
|
||||||
317: "Home & Garden > Decor > Planters & Pots", // Air-Pot
|
317: "113", // Töpfe > Air-Pot – (Blumentöpfe/Pflanzgefäße)
|
||||||
364: "Home & Garden > Decor > Planters & Pots", // Kunststofftöpfe
|
364: "113", // Töpfe > Kunststofftöpfe – (Blumentöpfe/Pflanzgefäße)
|
||||||
292: "Home & Garden > Decor > Planters & Pots", // Trays & Fluttische
|
292: "3568", // Bewässerung > Trays & Fluttische – Bewässerungssysteme
|
||||||
|
|
||||||
// Ventilation & Climate
|
// Ventilation & Climate
|
||||||
703: "Home & Garden > Outdoor Power Tools", // Abluft-Sets
|
703: "2802", // Grow-Sets > Abluft-Sets – (verwendet Pflanzen-Kräuter-Anbausets)
|
||||||
247: "Home & Garden > Outdoor Power Tools", // Belüftung
|
247: "1700", // Belüftung – Ventilatoren (Klimatisierung)
|
||||||
214: "Home & Garden > Outdoor Power Tools", // Umluft-Ventilatoren
|
214: "1700", // Belüftung > Umluft-Ventilatoren – Ventilatoren
|
||||||
308: "Home & Garden > Outdoor Power Tools", // Ab- und Zuluft
|
308: "1700", // Belüftung > Ab- und Zuluft – Ventilatoren
|
||||||
609: "Home & Garden > Outdoor Power Tools", // Schalldämpfer
|
609: "1700", // Belüftung > Ab- und Zuluft > Schalldämpfer – Ventilatoren
|
||||||
248: "Home & Garden > Pool & Spa > Pool & Spa Filters", // Aktivkohlefilter
|
248: "1700", // Belüftung > Aktivkohlefilter – Ventilatoren (nächstmögliche)
|
||||||
392: "Home & Garden > Pool & Spa > Pool & Spa Filters", // Zuluftfilter
|
392: "1700", // Belüftung > Ab- und Zuluft > Zuluftfilter – Ventilatoren
|
||||||
658: "Home & Garden > Climate Control > Dehumidifiers", // Luftbe- und entfeuchter
|
658: "606", // Belüftung > Luftbe- und -entfeuchter – Luftentfeuchter
|
||||||
310: "Home & Garden > Climate Control > Heating", // Heizmatten
|
310: "2802", // Anzucht > Heizmatten – Pflanzen- & Kräuteranbausets
|
||||||
379: "Home & Garden > Household Supplies > Air Fresheners", // Geruchsneutralisation
|
379: "5631", // Belüftung > Geruchsneutralisation – Haushaltsbedarf: Aufbewahrung
|
||||||
|
|
||||||
// Irrigation & Watering
|
// Irrigation & Watering
|
||||||
221: "Home & Garden > Lawn & Garden > Watering Equipment", // Bewässerung
|
221: "3568", // Bewässerung – Bewässerungssysteme (Gesamt)
|
||||||
250: "Home & Garden > Lawn & Garden > Watering Equipment", // Schläuche
|
250: "6318", // Bewässerung > Schläuche – Gartenschläuche
|
||||||
297: "Home & Garden > Lawn & Garden > Watering Equipment", // Pumpen
|
297: "500100", // Bewässerung > Pumpen – Bewässerung-/Sprinklerpumpen
|
||||||
354: "Home & Garden > Lawn & Garden > Watering Equipment", // Sprüher
|
354: "3780", // Bewässerung > Sprüher – Sprinkler & Sprühköpfe
|
||||||
372: "Home & Garden > Lawn & Garden > Watering Equipment", // AutoPot
|
372: "3568", // Bewässerung > AutoPot – Bewässerungssysteme
|
||||||
389: "Home & Garden > Lawn & Garden > Watering Equipment", // Blumat
|
389: "3568", // Bewässerung > Blumat – Bewässerungssysteme
|
||||||
405: "Home & Garden > Lawn & Garden > Watering Equipment", // Schläuche
|
405: "6318", // Bewässerung > Schläuche – Gartenschläuche
|
||||||
425: "Home & Garden > Lawn & Garden > Watering Equipment", // Wassertanks
|
425: "3568", // Bewässerung > Wassertanks – Bewässerungssysteme
|
||||||
480: "Home & Garden > Lawn & Garden > Watering Equipment", // Tropfer
|
480: "3568", // Bewässerung > Tropfer – Bewässerungssysteme
|
||||||
519: "Home & Garden > Lawn & Garden > Watering Equipment", // Pumpsprüher
|
519: "3568", // Bewässerung > Pumpsprüher – Bewässerungssysteme
|
||||||
|
|
||||||
// Growing Media & Soils
|
// Growing Media & Soils
|
||||||
242: "Home & Garden > Lawn & Garden > Fertilizers", // Böden
|
242: "543677", // Böden – Gartenerde
|
||||||
243: "Home & Garden > Lawn & Garden > Fertilizers", // Erde
|
243: "543677", // Böden > Erde – Gartenerde
|
||||||
269: "Home & Garden > Lawn & Garden > Fertilizers", // Kokos
|
269: "543677", // Böden > Kokos – Gartenerde
|
||||||
580: "Home & Garden > Lawn & Garden > Fertilizers", // Perlite & Blähton
|
580: "543677", // Böden > Perlite & Blähton – Gartenerde
|
||||||
|
|
||||||
// Propagation & Starting
|
// Propagation & Starting
|
||||||
286: "Home & Garden > Plants", // Anzucht
|
286: "2802", // Anzucht – Pflanzen- & Kräuteranbausets
|
||||||
298: "Home & Garden > Plants", // Steinwolltrays
|
298: "2802", // Anzucht > Steinwolltrays – Pflanzen- & Kräuteranbausets
|
||||||
421: "Home & Garden > Plants", // Vermehrungszubehör
|
421: "2802", // Anzucht > Vermehrungszubehör – Pflanzen- & Kräuteranbausets
|
||||||
489: "Home & Garden > Plants", // EazyPlug & Jiffy
|
489: "2802", // Anzucht > EazyPlug & Jiffy – Pflanzen- & Kräuteranbausets
|
||||||
359: "Home & Garden > Outdoor Structures > Greenhouses", // Gewächshäuser
|
359: "3103", // Anzucht > Gewächshäuser – Gewächshäuser
|
||||||
|
|
||||||
// Tools & Equipment
|
// Tools & Equipment
|
||||||
373: "Home & Garden > Tools > Hand Tools", // GrowTool
|
373: "3568", // Bewässerung > GrowTool – Bewässerungssysteme
|
||||||
403: "Home & Garden > Kitchen & Dining > Kitchen Tools & Utensils", // Messbecher & mehr
|
403: "3999", // Bewässerung > Messbecher & mehr – Messbecher & Dosierlöffel
|
||||||
259: "Home & Garden > Tools > Hand Tools", // Pressen
|
259: "756", // Zubehör > Ernte & Verarbeitung > Pressen – Nudelmaschinen
|
||||||
280: "Home & Garden > Tools > Hand Tools", // Erntescheeren
|
280: "2948", // Zubehör > Ernte & Verarbeitung > Erntescheeren – Küchenmesser
|
||||||
258: "Home & Garden > Tools", // Ernte & Verarbeitung
|
258: "684", // Zubehör > Ernte & Verarbeitung – Abfallzerkleinerer
|
||||||
278: "Home & Garden > Tools", // Extraktion
|
278: "5057", // Zubehör > Ernte & Verarbeitung > Extraktion – Slush-Eis-Maschinen
|
||||||
302: "Home & Garden > Tools", // Erntemaschinen
|
302: "7332", // Zubehör > Ernte & Verarbeitung > Erntemaschinen – Gartenmaschinen
|
||||||
|
|
||||||
// Hardware & Plumbing
|
// Hardware & Plumbing
|
||||||
222: "Hardware > Plumbing", // PE-Teile
|
222: "3568", // Bewässerung > PE-Teile – Bewässerungssysteme
|
||||||
374: "Hardware > Plumbing > Plumbing Fittings", // Verbindungsteile
|
374: "1700", // Belüftung > Ab- und Zuluft > Verbindungsteile – Ventilatoren
|
||||||
|
|
||||||
// Electronics & Control
|
// Electronics & Control
|
||||||
314: "Electronics > Electronics Accessories", // Steuergeräte
|
314: "1700", // Belüftung > Steuergeräte – Ventilatoren
|
||||||
408: "Electronics > Electronics Accessories", // GrowControl
|
408: "1700", // Belüftung > Steuergeräte > GrowControl – Ventilatoren
|
||||||
344: "Business & Industrial > Science & Laboratory > Lab Equipment", // Messgeräte
|
344: "1207", // Zubehör > Messgeräte – Messwerkzeuge & Messwertgeber
|
||||||
555: "Business & Industrial > Science & Laboratory > Lab Equipment > Microscopes", // Mikroskope
|
555: "4555", // Zubehör > Anbauzubehör > Mikroskope – Mikroskope
|
||||||
|
|
||||||
// Camping & Outdoor
|
// Camping & Outdoor
|
||||||
226: "Sporting Goods > Outdoor Recreation > Camping & Hiking", // Zeltzubehör
|
226: "5655", // Zubehör > Zeltzubehör – Zelte
|
||||||
|
|
||||||
// Plant Care & Protection
|
// Plant Care & Protection
|
||||||
239: "Home & Garden > Lawn & Garden > Pest Control", // Pflanzenschutz
|
239: "4085", // Zubehör > Anbauzubehör > Pflanzenschutz – Herbizide
|
||||||
240: "Home & Garden > Plants", // Anbauzubehör
|
240: "5633", // Zubehör > Anbauzubehör – Zubehör für Gartenarbeit
|
||||||
|
|
||||||
// Office & Media
|
// Office & Media
|
||||||
424: "Office Supplies > Labels", // Etiketten & Schilder
|
424: "4377", // Zubehör > Anbauzubehör > Etiketten & Schilder – Etiketten & Anhängerschilder
|
||||||
387: "Media > Books", // Literatur
|
387: "543541", // Zubehör > Anbauzubehör > Literatur – Bücher
|
||||||
|
|
||||||
// General categories
|
// General categories
|
||||||
705: "Home & Garden", // Set-Konfigurator
|
705: "2802", // Grow-Sets > Set-Konfigurator – (ebenfalls Pflanzen-Anbausets)
|
||||||
686: "Home & Garden", // Zubehör
|
686: "1700", // Belüftung > Aktivkohlefilter > Zubehör – Ventilatoren
|
||||||
741: "Home & Garden", // Zubehör
|
741: "1700", // Belüftung > Ab- und Zuluft > Zubehör – Ventilatoren
|
||||||
294: "Home & Garden", // Zubehör
|
294: "3568", // Bewässerung > Zubehör – Bewässerungssysteme
|
||||||
695: "Home & Garden", // Zubehör
|
695: "5631", // Zubehör – Haushaltsbedarf: Aufbewahrung
|
||||||
293: "Home & Garden", // Trockennetze
|
293: "5631", // Zubehör > Ernte & Verarbeitung > Trockennetze – Haushaltsbedarf: Aufbewahrung
|
||||||
4: "Home & Garden", // Sonstiges
|
4: "5631", // Zubehör > Anbauzubehör > Sonstiges – Haushaltsbedarf: Aufbewahrung
|
||||||
450: "Home & Garden", // Restposten
|
450: "5631", // Zubehör > Anbauzubehör > Restposten – Haushaltsbedarf: Aufbewahrung
|
||||||
};
|
};
|
||||||
|
|
||||||
return categoryMappings[categoryId] || "Home & Garden > Plants";
|
const categoryId_str = categoryMappings[categoryId] || "5631"; // Default to Haushaltsbedarf: Aufbewahrung
|
||||||
|
|
||||||
|
// Validate that the category ID is not empty
|
||||||
|
if (!categoryId_str || categoryId_str.trim() === "") {
|
||||||
|
return "5631"; // Haushaltsbedarf: Aufbewahrung
|
||||||
|
}
|
||||||
|
|
||||||
|
return categoryId_str;
|
||||||
};
|
};
|
||||||
|
|
||||||
let productsXml = `<?xml version="1.0" encoding="UTF-8"?>
|
let productsXml = `<?xml version="1.0" encoding="UTF-8"?>
|
||||||
@@ -150,7 +250,7 @@ const generateProductsXml = (allProductsData = [], baseUrl, config) => {
|
|||||||
<link>${baseUrl}</link>
|
<link>${baseUrl}</link>
|
||||||
<description>${config.descriptions.short}</description>
|
<description>${config.descriptions.short}</description>
|
||||||
<lastBuildDate>${currentDate}</lastBuildDate>
|
<lastBuildDate>${currentDate}</lastBuildDate>
|
||||||
<language>${config.language}</language>`;
|
<language>de-DE</language>`;
|
||||||
|
|
||||||
// Helper function to clean text content of problematic characters
|
// Helper function to clean text content of problematic characters
|
||||||
const cleanTextContent = (text) => {
|
const cleanTextContent = (text) => {
|
||||||
@@ -216,12 +316,63 @@ const generateProductsXml = (allProductsData = [], baseUrl, config) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Skip products without GTIN
|
// Skip products with excluded terms in title or description
|
||||||
|
const productTitle = (product.name || "").toLowerCase();
|
||||||
|
const productDescription = (product.description || "").toLowerCase();
|
||||||
|
|
||||||
|
const excludedTerms = {
|
||||||
|
title: ['canna', 'hash', 'marijuana', 'marihuana'],
|
||||||
|
description: ['cannabis']
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check title for excluded terms
|
||||||
|
if (excludedTerms.title.some(term => productTitle.includes(term))) {
|
||||||
|
skippedCount++;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check description for excluded terms
|
||||||
|
if (excludedTerms.description.some(term => productDescription.includes(term))) {
|
||||||
|
skippedCount++;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip products without GTIN or with invalid GTIN
|
||||||
if (!product.gtin || !product.gtin.toString().trim()) {
|
if (!product.gtin || !product.gtin.toString().trim()) {
|
||||||
skippedCount++;
|
skippedCount++;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Validate GTIN format and checksum
|
||||||
|
const gtinString = product.gtin.toString().trim();
|
||||||
|
|
||||||
|
// Helper function to validate GTIN with proper checksum validation
|
||||||
|
const isValidGTIN = (gtin) => {
|
||||||
|
if (!/^\d{8}$|^\d{12,14}$/.test(gtin)) return false; // Only 8, 12, 13, 14 digits allowed
|
||||||
|
|
||||||
|
const digits = gtin.split('').map(Number);
|
||||||
|
const length = digits.length;
|
||||||
|
let sum = 0;
|
||||||
|
|
||||||
|
for (let i = 0; i < length - 1; i++) {
|
||||||
|
// Even/odd multiplier depends on GTIN length
|
||||||
|
let multiplier = 1;
|
||||||
|
if (length === 8) {
|
||||||
|
multiplier = (i % 2 === 0) ? 3 : 1;
|
||||||
|
} else {
|
||||||
|
multiplier = ((length - i) % 2 === 0) ? 3 : 1;
|
||||||
|
}
|
||||||
|
sum += digits[i] * multiplier;
|
||||||
|
}
|
||||||
|
const checkDigit = (10 - (sum % 10)) % 10;
|
||||||
|
return checkDigit === digits[length - 1];
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!isValidGTIN(gtinString)) {
|
||||||
|
skippedCount++;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Skip products without pictures
|
// Skip products without pictures
|
||||||
if (!product.pictureList || !product.pictureList.trim()) {
|
if (!product.pictureList || !product.pictureList.trim()) {
|
||||||
skippedCount++;
|
skippedCount++;
|
||||||
@@ -274,8 +425,8 @@ const generateProductsXml = (allProductsData = [], baseUrl, config) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate GTIN/EAN if available
|
// Generate GTIN/EAN if available (use the already validated gtinString)
|
||||||
const gtin = product.gtin ? escapeXml(product.gtin.toString().trim()) : null;
|
const gtin = gtinString ? escapeXml(gtinString) : null;
|
||||||
|
|
||||||
// Generate product ID (using articleNumber or seoName)
|
// Generate product ID (using articleNumber or seoName)
|
||||||
const rawProductId = product.articleNumber || product.seoName || `product_${Date.now()}_${Math.random().toString(36).substr(2, 5)}`;
|
const rawProductId = product.articleNumber || product.seoName || `product_${Date.now()}_${Math.random().toString(36).substr(2, 5)}`;
|
||||||
@@ -318,6 +469,17 @@ const generateProductsXml = (allProductsData = [], baseUrl, config) => {
|
|||||||
<g:shipping_weight>${parseFloat(product.weight).toFixed(2)} g</g:shipping_weight>`;
|
<g:shipping_weight>${parseFloat(product.weight).toFixed(2)} g</g:shipping_weight>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add unit pricing data (required by German law for many products)
|
||||||
|
const unitPricingData = determineUnitPricingData(product);
|
||||||
|
if (unitPricingData.unit_pricing_measure) {
|
||||||
|
productsXml += `
|
||||||
|
<g:unit_pricing_measure>${unitPricingData.unit_pricing_measure}</g:unit_pricing_measure>`;
|
||||||
|
}
|
||||||
|
if (unitPricingData.unit_pricing_base_measure) {
|
||||||
|
productsXml += `
|
||||||
|
<g:unit_pricing_base_measure>${unitPricingData.unit_pricing_base_measure}</g:unit_pricing_base_measure>`;
|
||||||
|
}
|
||||||
|
|
||||||
productsXml += `
|
productsXml += `
|
||||||
</item>`;
|
</item>`;
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
|
Before Width: | Height: | Size: 1.9 MiB After Width: | Height: | Size: 85 KiB |
BIN
public/assets/images/filiale1.jpg
Normal file
BIN
public/assets/images/filiale1.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 208 KiB |
BIN
public/assets/images/filiale2.jpg
Normal file
BIN
public/assets/images/filiale2.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 242 KiB |
BIN
public/assets/images/presse.jpg
Normal file
BIN
public/assets/images/presse.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 71 KiB |
BIN
public/assets/images/purpl.jpg
Normal file
BIN
public/assets/images/purpl.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 68 KiB |
@@ -1,5 +1,5 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="de">
|
<html lang="de" data-i18n-lang="de">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
|||||||
48
src/App.js
48
src/App.js
@@ -18,13 +18,19 @@ import BugReportIcon from "@mui/icons-material/BugReport";
|
|||||||
|
|
||||||
import SocketProvider from "./providers/SocketProvider.js";
|
import SocketProvider from "./providers/SocketProvider.js";
|
||||||
import SocketContext from "./contexts/SocketContext.js";
|
import SocketContext from "./contexts/SocketContext.js";
|
||||||
|
import { CarouselProvider } from "./contexts/CarouselContext.js";
|
||||||
import config from "./config.js";
|
import config from "./config.js";
|
||||||
import ScrollToTop from "./components/ScrollToTop.js";
|
import ScrollToTop from "./components/ScrollToTop.js";
|
||||||
|
|
||||||
|
// Import i18n
|
||||||
|
import './i18n/index.js';
|
||||||
|
import { LanguageProvider } from './i18n/withTranslation.js';
|
||||||
|
import i18n from './i18n/index.js';
|
||||||
//import TelemetryService from './services/telemetryService.js';
|
//import TelemetryService from './services/telemetryService.js';
|
||||||
|
|
||||||
import Header from "./components/Header.js";
|
import Header from "./components/Header.js";
|
||||||
import Footer from "./components/Footer.js";
|
import Footer from "./components/Footer.js";
|
||||||
import Home from "./pages/Home.js";
|
import MainPageLayout from "./components/MainPageLayout.js";
|
||||||
|
|
||||||
// Lazy load all route components to reduce initial bundle size
|
// Lazy load all route components to reduce initial bundle size
|
||||||
const Content = lazy(() => import(/* webpackChunkName: "content" */ "./components/Content.js"));
|
const Content = lazy(() => import(/* webpackChunkName: "content" */ "./components/Content.js"));
|
||||||
@@ -40,7 +46,7 @@ const ServerLogsPage = lazy(() => import(/* webpackChunkName: "admin-logs" */ ".
|
|||||||
// Lazy load legal pages - rarely accessed
|
// Lazy load legal pages - rarely accessed
|
||||||
const Datenschutz = lazy(() => import(/* webpackChunkName: "legal" */ "./pages/Datenschutz.js"));
|
const Datenschutz = lazy(() => import(/* webpackChunkName: "legal" */ "./pages/Datenschutz.js"));
|
||||||
const AGB = lazy(() => import(/* webpackChunkName: "legal" */ "./pages/AGB.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 Sitemap = lazy(() => import(/* webpackChunkName: "sitemap" */ "./pages/Sitemap.js"));
|
||||||
const Impressum = lazy(() => import(/* webpackChunkName: "legal" */ "./pages/Impressum.js"));
|
const Impressum = lazy(() => import(/* webpackChunkName: "legal" */ "./pages/Impressum.js"));
|
||||||
const Batteriegesetzhinweise = lazy(() => import(/* webpackChunkName: "legal" */ "./pages/Batteriegesetzhinweise.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 GrowTentKonfigurator = lazy(() => import(/* webpackChunkName: "konfigurator" */ "./pages/GrowTentKonfigurator.js"));
|
||||||
const ChatAssistant = lazy(() => import(/* webpackChunkName: "chat" */ "./components/ChatAssistant.js"));
|
const ChatAssistant = lazy(() => import(/* webpackChunkName: "chat" */ "./components/ChatAssistant.js"));
|
||||||
|
|
||||||
|
// Lazy load separate pages that are truly different
|
||||||
|
const PresseverleihPage = lazy(() => import(/* webpackChunkName: "presseverleih" */ "./pages/PresseverleihPage.js"));
|
||||||
|
const ThcTestPage = lazy(() => import(/* webpackChunkName: "thc-test" */ "./pages/ThcTestPage.js"));
|
||||||
|
|
||||||
|
// 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 theme from separate file to reduce main bundle size
|
||||||
import defaultTheme from "./theme.js";
|
import defaultTheme from "./theme.js";
|
||||||
// Lazy load theme customizer for development only
|
// Lazy load theme customizer for development only
|
||||||
@@ -94,11 +107,15 @@ const AppContent = ({ currentTheme, onThemeChange }) => {
|
|||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (location.hash && location.hash.startsWith("#ORD-")) {
|
if (location.hash && location.hash.length > 1) {
|
||||||
|
// Check if it's a potential order ID (starts with # and has alphanumeric characters with dashes)
|
||||||
|
const potentialOrderId = location.hash.substring(1);
|
||||||
|
if (/^[A-Z0-9]+-[A-Z0-9]+$/i.test(potentialOrderId)) {
|
||||||
if (location.pathname !== "/profile") {
|
if (location.pathname !== "/profile") {
|
||||||
navigate(`/profile${location.hash}`, { replace: true });
|
navigate(`/profile${location.hash}`, { replace: true });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}, [location, navigate]);
|
}, [location, navigate]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -195,9 +212,12 @@ const AppContent = ({ currentTheme, onThemeChange }) => {
|
|||||||
<CircularProgress color="primary" />
|
<CircularProgress color="primary" />
|
||||||
</Box>
|
</Box>
|
||||||
}>
|
}>
|
||||||
|
<CarouselProvider>
|
||||||
<Routes>
|
<Routes>
|
||||||
{/* Home page with text only */}
|
{/* Main pages using unified component */}
|
||||||
<Route path="/" element={<Home />} />
|
<Route path="/" element={<MainPageLayout />} />
|
||||||
|
<Route path="/aktionen" element={<MainPageLayout />} />
|
||||||
|
<Route path="/filiale" element={<MainPageLayout />} />
|
||||||
|
|
||||||
{/* Category page - Render Content in parallel */}
|
{/* Category page - Render Content in parallel */}
|
||||||
<Route
|
<Route
|
||||||
@@ -216,6 +236,9 @@ const AppContent = ({ currentTheme, onThemeChange }) => {
|
|||||||
{/* Profile page */}
|
{/* Profile page */}
|
||||||
<Route path="/profile" element={<ProfilePageWithSocket />} />
|
<Route path="/profile" element={<ProfilePageWithSocket />} />
|
||||||
|
|
||||||
|
{/* Payment success page for Mollie redirects */}
|
||||||
|
<Route path="/payment/success" element={<PaymentSuccess />} />
|
||||||
|
|
||||||
{/* Reset password page */}
|
{/* Reset password page */}
|
||||||
<Route
|
<Route
|
||||||
path="/resetPassword"
|
path="/resetPassword"
|
||||||
@@ -234,7 +257,6 @@ const AppContent = ({ currentTheme, onThemeChange }) => {
|
|||||||
{/* Legal pages */}
|
{/* Legal pages */}
|
||||||
<Route path="/datenschutz" element={<Datenschutz />} />
|
<Route path="/datenschutz" element={<Datenschutz />} />
|
||||||
<Route path="/agb" element={<AGB />} />
|
<Route path="/agb" element={<AGB />} />
|
||||||
<Route path="/404" element={<NotFound404 />} />
|
|
||||||
<Route path="/sitemap" element={<Sitemap />} />
|
<Route path="/sitemap" element={<Sitemap />} />
|
||||||
<Route path="/impressum" element={<Impressum />} />
|
<Route path="/impressum" element={<Impressum />} />
|
||||||
<Route
|
<Route
|
||||||
@@ -244,11 +266,16 @@ const AppContent = ({ currentTheme, onThemeChange }) => {
|
|||||||
<Route path="/widerrufsrecht" element={<Widerrufsrecht />} />
|
<Route path="/widerrufsrecht" element={<Widerrufsrecht />} />
|
||||||
|
|
||||||
{/* Grow Tent Configurator */}
|
{/* Grow Tent Configurator */}
|
||||||
<Route path="/Konfigurator" element={<GrowTentKonfigurator />} />
|
<Route path="/Konfigurator" element={<GrowTentKonfigurator socket={socket} socketB={socketB} />} />
|
||||||
|
|
||||||
|
{/* Separate pages that are truly different */}
|
||||||
|
<Route path="/presseverleih" element={<PresseverleihPage />} />
|
||||||
|
<Route path="/thc-test" element={<ThcTestPage />} />
|
||||||
|
|
||||||
{/* Fallback for undefined routes */}
|
{/* Fallback for undefined routes */}
|
||||||
<Route path="*" element={<Navigate to="/" replace />} />
|
<Route path="*" element={<Navigate to="/" replace />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
|
</CarouselProvider>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
</Box>
|
</Box>
|
||||||
{/* Conditionally render the Chat Assistant */}
|
{/* Conditionally render the Chat Assistant */}
|
||||||
@@ -343,7 +370,13 @@ const App = () => {
|
|||||||
setDynamicTheme(createTheme(newTheme));
|
setDynamicTheme(createTheme(newTheme));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Make config globally available for language switching
|
||||||
|
useEffect(() => {
|
||||||
|
window.shopConfig = config;
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<LanguageProvider i18n={i18n}>
|
||||||
<ThemeProvider theme={dynamicTheme}>
|
<ThemeProvider theme={dynamicTheme}>
|
||||||
<CssBaseline />
|
<CssBaseline />
|
||||||
<SocketProvider
|
<SocketProvider
|
||||||
@@ -367,6 +400,7 @@ const App = () => {
|
|||||||
/>
|
/>
|
||||||
</SocketProvider>
|
</SocketProvider>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
|
</LanguageProvider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,8 @@ import { Box, AppBar, Toolbar, Container} from '@mui/material';
|
|||||||
import { Routes, Route } from 'react-router-dom';
|
import { Routes, Route } from 'react-router-dom';
|
||||||
import Footer from './components/Footer.js';
|
import Footer from './components/Footer.js';
|
||||||
import { Logo, CategoryList } from './components/header/index.js';
|
import { Logo, CategoryList } from './components/header/index.js';
|
||||||
import Home from './pages/Home.js';
|
import MainPageLayout from './components/MainPageLayout.js';
|
||||||
|
import { CarouselProvider } from './contexts/CarouselContext.js';
|
||||||
|
|
||||||
const PrerenderAppContent = (socket) => (
|
const PrerenderAppContent = (socket) => (
|
||||||
<Box
|
<Box
|
||||||
@@ -44,9 +45,11 @@ const PrerenderAppContent = (socket) => (
|
|||||||
</AppBar>
|
</AppBar>
|
||||||
|
|
||||||
<Box sx={{ flexGrow: 1 }}>
|
<Box sx={{ flexGrow: 1 }}>
|
||||||
|
<CarouselProvider>
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/" element={<Home />} />
|
<Route path="/" element={<MainPageLayout />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
|
</CarouselProvider>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Footer/>
|
<Footer/>
|
||||||
|
|||||||
@@ -7,7 +7,8 @@ const {
|
|||||||
} = require('@mui/material');
|
} = require('@mui/material');
|
||||||
const Footer = require('./components/Footer.js').default;
|
const Footer = require('./components/Footer.js').default;
|
||||||
const { Logo, CategoryList } = require('./components/header/index.js');
|
const { Logo, CategoryList } = require('./components/header/index.js');
|
||||||
const Home = require('./pages/Home.js').default;
|
const MainPageLayout = require('./components/MainPageLayout.js').default;
|
||||||
|
const { CarouselProvider } = require('./contexts/CarouselContext.js');
|
||||||
|
|
||||||
class PrerenderHome extends React.Component {
|
class PrerenderHome extends React.Component {
|
||||||
render() {
|
render() {
|
||||||
@@ -62,7 +63,7 @@ class PrerenderHome extends React.Component {
|
|||||||
React.createElement(
|
React.createElement(
|
||||||
Box,
|
Box,
|
||||||
{ sx: { flexGrow: 1 } },
|
{ sx: { flexGrow: 1 } },
|
||||||
React.createElement(Home)
|
React.createElement(CarouselProvider, null, React.createElement(MainPageLayout))
|
||||||
),
|
),
|
||||||
React.createElement(Footer)
|
React.createElement(Footer)
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -66,6 +66,7 @@ class PrerenderKonfigurator extends Component {
|
|||||||
15%
|
15%
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography variant="body2">
|
<Typography variant="body2">
|
||||||
|
{/* Note: This is a prerender file - translation key would be: product.discount.from3Products */}
|
||||||
ab 3 Produkten
|
ab 3 Produkten
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
@@ -74,6 +75,7 @@ class PrerenderKonfigurator extends Component {
|
|||||||
24%
|
24%
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography variant="body2">
|
<Typography variant="body2">
|
||||||
|
{/* Note: This is a prerender file - translation key would be: product.discount.from5Products */}
|
||||||
ab 5 Produkten
|
ab 5 Produkten
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
@@ -82,11 +84,13 @@ class PrerenderKonfigurator extends Component {
|
|||||||
36%
|
36%
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography variant="body2">
|
<Typography variant="body2">
|
||||||
|
{/* Note: This is a prerender file - translation key would be: product.discount.from7Products */}
|
||||||
ab 7 Produkten
|
ab 7 Produkten
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
<Typography variant="caption" sx={{ color: '#666', mt: 1, display: 'block' }}>
|
<Typography variant="caption" sx={{ color: '#666', mt: 1, display: 'block' }}>
|
||||||
|
{/* Note: This is a prerender file - translation key would be: product.discount.moreProductsMoreSavings */}
|
||||||
Je mehr Produkte du auswählst, desto mehr sparst du!
|
Je mehr Produkte du auswählst, desto mehr sparst du!
|
||||||
</Typography>
|
</Typography>
|
||||||
</Paper>
|
</Paper>
|
||||||
|
|||||||
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 };
|
||||||
@@ -101,7 +101,7 @@ class PrerenderProduct extends React.Component {
|
|||||||
React.createElement(
|
React.createElement(
|
||||||
Typography,
|
Typography,
|
||||||
{ variant: 'h6', color: 'text.secondary' },
|
{ variant: 'h6', 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}` : "")
|
||||||
),
|
),
|
||||||
React.createElement(
|
React.createElement(
|
||||||
Box,
|
Box,
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import AddIcon from "@mui/icons-material/Add";
|
|||||||
import RemoveIcon from "@mui/icons-material/Remove";
|
import RemoveIcon from "@mui/icons-material/Remove";
|
||||||
import ShoppingCartIcon from "@mui/icons-material/ShoppingCart";
|
import ShoppingCartIcon from "@mui/icons-material/ShoppingCart";
|
||||||
import DeleteIcon from "@mui/icons-material/Delete";
|
import DeleteIcon from "@mui/icons-material/Delete";
|
||||||
|
import { withI18n } from "../i18n/withTranslation.js";
|
||||||
|
|
||||||
if (!Array.isArray(window.cart)) window.cart = [];
|
if (!Array.isArray(window.cart)) window.cart = [];
|
||||||
|
|
||||||
@@ -51,11 +52,14 @@ class AddToCartButton extends Component {
|
|||||||
seoName: this.props.seoName,
|
seoName: this.props.seoName,
|
||||||
pictureList: this.props.pictureList,
|
pictureList: this.props.pictureList,
|
||||||
price: this.props.price,
|
price: this.props.price,
|
||||||
|
fGrundPreis: this.props.fGrundPreis,
|
||||||
|
cGrundEinheit: this.props.cGrundEinheit,
|
||||||
quantity: 1,
|
quantity: 1,
|
||||||
weight: this.props.weight,
|
weight: this.props.weight,
|
||||||
vat: this.props.vat,
|
vat: this.props.vat,
|
||||||
versandklasse: this.props.versandklasse,
|
versandklasse: this.props.versandklasse,
|
||||||
availableSupplier: this.props.availableSupplier,
|
availableSupplier: this.props.availableSupplier,
|
||||||
|
komponenten: this.props.komponenten,
|
||||||
available: this.props.available
|
available: this.props.available
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
@@ -150,12 +154,17 @@ class AddToCartButton extends Component {
|
|||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Ab{" "}
|
{this.props.t ? this.props.t('cart.availableFrom', {
|
||||||
{new Date(incoming).toLocaleDateString("de-DE", {
|
date: new Date(incoming).toLocaleDateString("de-DE", {
|
||||||
year: "numeric",
|
year: "numeric",
|
||||||
month: "long",
|
month: "long",
|
||||||
day: "numeric",
|
day: "numeric",
|
||||||
})}
|
})
|
||||||
|
}) : `Ab ${new Date(incoming).toLocaleDateString("de-DE", {
|
||||||
|
year: "numeric",
|
||||||
|
month: "long",
|
||||||
|
day: "numeric",
|
||||||
|
})}`}
|
||||||
</Button>
|
</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>
|
</Button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -259,7 +270,7 @@ class AddToCartButton extends Component {
|
|||||||
<AddIcon />
|
<AddIcon />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
|
|
||||||
<Tooltip title="Aus dem Warenkorb entfernen" arrow>
|
<Tooltip title={this.props.t ? this.props.t('cart.removeFromCart') : 'Aus dem Warenkorb entfernen'} arrow>
|
||||||
<IconButton
|
<IconButton
|
||||||
color="inherit"
|
color="inherit"
|
||||||
onClick={this.handleClearCart}
|
onClick={this.handleClearCart}
|
||||||
@@ -272,7 +283,7 @@ class AddToCartButton extends Component {
|
|||||||
</IconButton>
|
</IconButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
{this.props.cartButton && (
|
{this.props.cartButton && (
|
||||||
<Tooltip title="Warenkorb öffnen" arrow>
|
<Tooltip title={this.props.t ? this.props.t('cart.openCart') : 'Warenkorb öffnen'} arrow>
|
||||||
<IconButton
|
<IconButton
|
||||||
color="inherit"
|
color="inherit"
|
||||||
onClick={this.toggleCart}
|
onClick={this.toggleCart}
|
||||||
@@ -302,7 +313,7 @@ class AddToCartButton extends Component {
|
|||||||
fontWeight: "bold",
|
fontWeight: "bold",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Out of Stock
|
{this.props.t ? this.props.t('product.outOfStock') : 'Out of Stock'}
|
||||||
</Button>
|
</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>
|
</Button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -404,7 +417,7 @@ class AddToCartButton extends Component {
|
|||||||
<AddIcon />
|
<AddIcon />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
|
|
||||||
<Tooltip title="Aus dem Warenkorb entfernen" arrow>
|
<Tooltip title={this.props.t ? this.props.t('cart.removeFromCart') : 'Aus dem Warenkorb entfernen'} arrow>
|
||||||
<IconButton
|
<IconButton
|
||||||
color="inherit"
|
color="inherit"
|
||||||
onClick={this.handleClearCart}
|
onClick={this.handleClearCart}
|
||||||
@@ -417,7 +430,7 @@ class AddToCartButton extends Component {
|
|||||||
</IconButton>
|
</IconButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
{this.props.cartButton && (
|
{this.props.cartButton && (
|
||||||
<Tooltip title="Warenkorb öffnen" arrow>
|
<Tooltip title={this.props.t ? this.props.t('cart.openCart') : 'Warenkorb öffnen'} arrow>
|
||||||
<IconButton
|
<IconButton
|
||||||
color="inherit"
|
color="inherit"
|
||||||
onClick={this.toggleCart}
|
onClick={this.toggleCart}
|
||||||
@@ -436,4 +449,4 @@ class AddToCartButton extends Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default AddToCartButton;
|
export default withI18n()(AddToCartButton);
|
||||||
|
|||||||
242
src/components/ArticleAvailabilityForm.js
Normal file
242
src/components/ArticleAvailabilityForm.js
Normal file
@@ -0,0 +1,242 @@
|
|||||||
|
import React, { Component } from 'react';
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Typography,
|
||||||
|
TextField,
|
||||||
|
Button,
|
||||||
|
Paper,
|
||||||
|
Alert,
|
||||||
|
CircularProgress,
|
||||||
|
FormControl,
|
||||||
|
FormLabel,
|
||||||
|
RadioGroup,
|
||||||
|
FormControlLabel,
|
||||||
|
Radio
|
||||||
|
} from '@mui/material';
|
||||||
|
import { withI18n } from '../i18n/withTranslation.js';
|
||||||
|
|
||||||
|
class ArticleAvailabilityForm extends Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
name: '',
|
||||||
|
email: '',
|
||||||
|
telegramId: '',
|
||||||
|
notificationMethod: 'email',
|
||||||
|
message: '',
|
||||||
|
loading: false,
|
||||||
|
success: false,
|
||||||
|
error: null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
handleInputChange = (field) => (event) => {
|
||||||
|
this.setState({ [field]: event.target.value });
|
||||||
|
};
|
||||||
|
|
||||||
|
handleNotificationMethodChange = (event) => {
|
||||||
|
this.setState({
|
||||||
|
notificationMethod: event.target.value,
|
||||||
|
// Clear the other field when switching methods
|
||||||
|
email: event.target.value === 'email' ? this.state.email : '',
|
||||||
|
telegramId: event.target.value === 'telegram' ? this.state.telegramId : ''
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
handleSubmit = (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
// Prepare data for API emission
|
||||||
|
const availabilityData = {
|
||||||
|
type: 'availability_inquiry',
|
||||||
|
productId: this.props.productId,
|
||||||
|
productName: this.props.productName,
|
||||||
|
name: this.state.name,
|
||||||
|
notificationMethod: this.state.notificationMethod,
|
||||||
|
email: this.state.notificationMethod === 'email' ? this.state.email : '',
|
||||||
|
telegramId: this.state.notificationMethod === 'telegram' ? this.state.telegramId : '',
|
||||||
|
message: this.state.message,
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
};
|
||||||
|
|
||||||
|
// Emit data via socket
|
||||||
|
console.log('Availability Inquiry Data to emit:', availabilityData);
|
||||||
|
|
||||||
|
if (this.props.socket) {
|
||||||
|
this.props.socket.emit('availability_inquiry_submit', availabilityData);
|
||||||
|
|
||||||
|
// Set up response handler
|
||||||
|
this.props.socket.once('availability_inquiry_response', (response) => {
|
||||||
|
if (response.success) {
|
||||||
|
this.setState({
|
||||||
|
loading: false,
|
||||||
|
success: true,
|
||||||
|
name: '',
|
||||||
|
email: '',
|
||||||
|
telegramId: '',
|
||||||
|
notificationMethod: 'email',
|
||||||
|
message: ''
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.setState({
|
||||||
|
loading: false,
|
||||||
|
error: response.error || 'Ein Fehler ist aufgetreten'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear messages after 3 seconds
|
||||||
|
setTimeout(() => {
|
||||||
|
this.setState({ success: false, error: null });
|
||||||
|
}, 3000);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setState({ loading: true });
|
||||||
|
|
||||||
|
// Fallback timeout in case backend doesn't respond
|
||||||
|
setTimeout(() => {
|
||||||
|
if (this.state.loading) {
|
||||||
|
this.setState({
|
||||||
|
loading: false,
|
||||||
|
success: true,
|
||||||
|
name: '',
|
||||||
|
email: '',
|
||||||
|
telegramId: '',
|
||||||
|
notificationMethod: 'email',
|
||||||
|
message: ''
|
||||||
|
});
|
||||||
|
|
||||||
|
// Clear success message after 3 seconds
|
||||||
|
setTimeout(() => {
|
||||||
|
this.setState({ success: false });
|
||||||
|
}, 3000);
|
||||||
|
}
|
||||||
|
}, 5000);
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { name, email, telegramId, notificationMethod, message, loading, success, error } = this.state;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Paper id="availability-form" sx={{ p: 3, mt: 4, borderRadius: 2, boxShadow: "0 2px 8px rgba(0,0,0,0.08)" }}>
|
||||||
|
<Typography variant="h5" gutterBottom sx={{ fontWeight: 600, color: '#333' }}>
|
||||||
|
Verfügbarkeit anfragen
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<Typography variant="body2" color="text.secondary" sx={{ mb: 3 }}>
|
||||||
|
Dieser Artikel ist derzeit nicht verfügbar. Gerne informieren wir Sie, sobald er wieder lieferbar ist.
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
{success && (
|
||||||
|
<Alert severity="success" sx={{ mb: 3 }}>
|
||||||
|
Vielen Dank für Ihre Anfrage! Wir werden Sie {notificationMethod === 'email' ? 'per E-Mail' : 'über Telegram'} informieren, sobald der Artikel wieder verfügbar ist.
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<Alert severity="error" sx={{ mb: 3 }}>
|
||||||
|
{error}
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Box component="form" onSubmit={this.handleSubmit} sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
|
||||||
|
<TextField
|
||||||
|
label="Name"
|
||||||
|
value={name}
|
||||||
|
onChange={this.handleInputChange('name')}
|
||||||
|
required
|
||||||
|
fullWidth
|
||||||
|
disabled={loading}
|
||||||
|
placeholder="Ihr Name"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormControl component="fieldset" disabled={loading}>
|
||||||
|
<FormLabel component="legend" sx={{ mb: 1 }}>
|
||||||
|
Wie möchten Sie benachrichtigt werden?
|
||||||
|
</FormLabel>
|
||||||
|
<RadioGroup
|
||||||
|
value={notificationMethod}
|
||||||
|
onChange={this.handleNotificationMethodChange}
|
||||||
|
row
|
||||||
|
>
|
||||||
|
<FormControlLabel
|
||||||
|
value="email"
|
||||||
|
control={<Radio />}
|
||||||
|
label="E-Mail"
|
||||||
|
/>
|
||||||
|
<FormControlLabel
|
||||||
|
value="telegram"
|
||||||
|
control={<Radio />}
|
||||||
|
label="Telegram Bot"
|
||||||
|
/>
|
||||||
|
</RadioGroup>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
{notificationMethod === 'email' && (
|
||||||
|
<TextField
|
||||||
|
label="E-Mail"
|
||||||
|
type="email"
|
||||||
|
value={email}
|
||||||
|
onChange={this.handleInputChange('email')}
|
||||||
|
required
|
||||||
|
fullWidth
|
||||||
|
disabled={loading}
|
||||||
|
placeholder="ihre.email@example.com"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{notificationMethod === 'telegram' && (
|
||||||
|
<TextField
|
||||||
|
label="Telegram ID"
|
||||||
|
value={telegramId}
|
||||||
|
onChange={this.handleInputChange('telegramId')}
|
||||||
|
required
|
||||||
|
fullWidth
|
||||||
|
disabled={loading}
|
||||||
|
placeholder="@IhrTelegramName oder Telegram ID"
|
||||||
|
helperText="Geben Sie Ihren Telegram-Benutzernamen (mit @) oder Ihre Telegram-ID ein"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<TextField
|
||||||
|
label="Nachricht (optional)"
|
||||||
|
value={message}
|
||||||
|
onChange={this.handleInputChange('message')}
|
||||||
|
fullWidth
|
||||||
|
multiline
|
||||||
|
rows={3}
|
||||||
|
disabled={loading}
|
||||||
|
placeholder="Zusätzliche Informationen oder Fragen..."
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
variant="contained"
|
||||||
|
disabled={loading || !name || (notificationMethod === 'email' && !email) || (notificationMethod === 'telegram' && !telegramId)}
|
||||||
|
sx={{
|
||||||
|
mt: 2,
|
||||||
|
py: 1.5,
|
||||||
|
fontSize: '1rem',
|
||||||
|
fontWeight: 600,
|
||||||
|
backgroundColor: 'warning.main',
|
||||||
|
'&:hover': {
|
||||||
|
backgroundColor: 'warning.dark'
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{loading ? (
|
||||||
|
<>
|
||||||
|
<CircularProgress size={20} sx={{ mr: 1 }} />
|
||||||
|
Wird gesendet...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
'Verfügbarkeit anfragen'
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
</Paper>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default withI18n()(ArticleAvailabilityForm);
|
||||||
235
src/components/ArticleQuestionForm.js
Normal file
235
src/components/ArticleQuestionForm.js
Normal file
@@ -0,0 +1,235 @@
|
|||||||
|
import React, { Component } from 'react';
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Typography,
|
||||||
|
TextField,
|
||||||
|
Button,
|
||||||
|
Paper,
|
||||||
|
Alert,
|
||||||
|
CircularProgress
|
||||||
|
} from '@mui/material';
|
||||||
|
import { withI18n } from '../i18n/withTranslation.js';
|
||||||
|
import PhotoUpload from './PhotoUpload.js';
|
||||||
|
|
||||||
|
class ArticleQuestionForm extends Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
name: '',
|
||||||
|
email: '',
|
||||||
|
question: '',
|
||||||
|
photos: [],
|
||||||
|
loading: false,
|
||||||
|
success: false,
|
||||||
|
error: null
|
||||||
|
};
|
||||||
|
this.photoUploadRef = React.createRef();
|
||||||
|
}
|
||||||
|
|
||||||
|
handleInputChange = (field) => (event) => {
|
||||||
|
this.setState({ [field]: event.target.value });
|
||||||
|
};
|
||||||
|
|
||||||
|
handlePhotosChange = (files) => {
|
||||||
|
this.setState({ photos: files });
|
||||||
|
};
|
||||||
|
|
||||||
|
convertPhotosToBase64 = (photos) => {
|
||||||
|
return Promise.all(
|
||||||
|
photos.map(photo => {
|
||||||
|
return new Promise((resolve, _reject) => {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = (e) => {
|
||||||
|
resolve({
|
||||||
|
name: photo.name,
|
||||||
|
type: photo.type,
|
||||||
|
size: photo.size,
|
||||||
|
data: e.target.result // base64 string
|
||||||
|
});
|
||||||
|
};
|
||||||
|
reader.readAsDataURL(photo);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
handleSubmit = async (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
this.setState({ loading: true });
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Convert photos to base64
|
||||||
|
const photosBase64 = await this.convertPhotosToBase64(this.state.photos);
|
||||||
|
|
||||||
|
// Prepare data for API emission
|
||||||
|
const questionData = {
|
||||||
|
type: 'article_question',
|
||||||
|
productId: this.props.productId,
|
||||||
|
productName: this.props.productName,
|
||||||
|
name: this.state.name,
|
||||||
|
email: this.state.email,
|
||||||
|
question: this.state.question,
|
||||||
|
photos: photosBase64,
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
};
|
||||||
|
|
||||||
|
// Emit data via socket
|
||||||
|
console.log('Article Question Data to emit:', questionData);
|
||||||
|
|
||||||
|
if (this.props.socket) {
|
||||||
|
this.props.socket.emit('article_question_submit', questionData);
|
||||||
|
|
||||||
|
// Set up response handler
|
||||||
|
this.props.socket.once('article_question_response', (response) => {
|
||||||
|
if (response.success) {
|
||||||
|
this.setState({
|
||||||
|
loading: false,
|
||||||
|
success: true,
|
||||||
|
name: '',
|
||||||
|
email: '',
|
||||||
|
question: '',
|
||||||
|
photos: []
|
||||||
|
});
|
||||||
|
// Reset the photo upload component
|
||||||
|
if (this.photoUploadRef.current) {
|
||||||
|
this.photoUploadRef.current.reset();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.setState({
|
||||||
|
loading: false,
|
||||||
|
error: response.error || 'Ein Fehler ist aufgetreten'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear messages after 3 seconds
|
||||||
|
setTimeout(() => {
|
||||||
|
this.setState({ success: false, error: null });
|
||||||
|
}, 3000);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
this.setState({
|
||||||
|
loading: false,
|
||||||
|
error: 'Fehler beim Verarbeiten der Fotos'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback timeout in case backend doesn't respond
|
||||||
|
setTimeout(() => {
|
||||||
|
if (this.state.loading) {
|
||||||
|
this.setState({
|
||||||
|
loading: false,
|
||||||
|
success: true,
|
||||||
|
name: '',
|
||||||
|
email: '',
|
||||||
|
question: '',
|
||||||
|
photos: []
|
||||||
|
});
|
||||||
|
// Reset the photo upload component
|
||||||
|
if (this.photoUploadRef.current) {
|
||||||
|
this.photoUploadRef.current.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear success message after 3 seconds
|
||||||
|
setTimeout(() => {
|
||||||
|
this.setState({ success: false });
|
||||||
|
}, 3000);
|
||||||
|
}
|
||||||
|
}, 5000);
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { name, email, question, loading, success, error } = this.state;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Paper sx={{ p: 3, mt: 4, borderRadius: 2, boxShadow: "0 2px 8px rgba(0,0,0,0.08)" }}>
|
||||||
|
<Typography variant="h5" gutterBottom sx={{ fontWeight: 600, color: '#333' }}>
|
||||||
|
Frage zum Artikel
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<Typography variant="body2" color="text.secondary" sx={{ mb: 3 }}>
|
||||||
|
Haben Sie eine Frage zu diesem Artikel? Wir helfen Ihnen gerne weiter.
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
{success && (
|
||||||
|
<Alert severity="success" sx={{ mb: 3 }}>
|
||||||
|
Vielen Dank für Ihre Frage! Wir werden uns schnellstmöglich bei Ihnen melden.
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<Alert severity="error" sx={{ mb: 3 }}>
|
||||||
|
{error}
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Box component="form" onSubmit={this.handleSubmit} sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
|
||||||
|
<TextField
|
||||||
|
label="Name"
|
||||||
|
value={name}
|
||||||
|
onChange={this.handleInputChange('name')}
|
||||||
|
required
|
||||||
|
fullWidth
|
||||||
|
disabled={loading}
|
||||||
|
placeholder="Ihr Name"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TextField
|
||||||
|
label="E-Mail"
|
||||||
|
type="email"
|
||||||
|
value={email}
|
||||||
|
onChange={this.handleInputChange('email')}
|
||||||
|
required
|
||||||
|
fullWidth
|
||||||
|
disabled={loading}
|
||||||
|
placeholder="ihre.email@example.com"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TextField
|
||||||
|
label="Ihre Frage"
|
||||||
|
value={question}
|
||||||
|
onChange={this.handleInputChange('question')}
|
||||||
|
required
|
||||||
|
fullWidth
|
||||||
|
multiline
|
||||||
|
rows={4}
|
||||||
|
disabled={loading}
|
||||||
|
placeholder="Beschreiben Sie Ihre Frage zu diesem Artikel..."
|
||||||
|
/>
|
||||||
|
|
||||||
|
<PhotoUpload
|
||||||
|
ref={this.photoUploadRef}
|
||||||
|
onChange={this.handlePhotosChange}
|
||||||
|
disabled={loading}
|
||||||
|
maxFiles={3}
|
||||||
|
label="Fotos zur Frage anhängen (optional)"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
variant="contained"
|
||||||
|
disabled={loading || !name || !email || !question}
|
||||||
|
sx={{
|
||||||
|
mt: 2,
|
||||||
|
py: 1.5,
|
||||||
|
fontSize: '1rem',
|
||||||
|
fontWeight: 600
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{loading ? (
|
||||||
|
<>
|
||||||
|
<CircularProgress size={20} sx={{ mr: 1 }} />
|
||||||
|
Wird gesendet...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
'Frage senden'
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
</Paper>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default withI18n()(ArticleQuestionForm);
|
||||||
264
src/components/ArticleRatingForm.js
Normal file
264
src/components/ArticleRatingForm.js
Normal file
@@ -0,0 +1,264 @@
|
|||||||
|
import React, { Component } from 'react';
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Typography,
|
||||||
|
TextField,
|
||||||
|
Button,
|
||||||
|
Paper,
|
||||||
|
Alert,
|
||||||
|
CircularProgress,
|
||||||
|
Rating
|
||||||
|
} from '@mui/material';
|
||||||
|
import StarIcon from '@mui/icons-material/Star';
|
||||||
|
import { withI18n } from '../i18n/withTranslation.js';
|
||||||
|
import PhotoUpload from './PhotoUpload.js';
|
||||||
|
|
||||||
|
class ArticleRatingForm extends Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
name: '',
|
||||||
|
email: '',
|
||||||
|
rating: 0,
|
||||||
|
review: '',
|
||||||
|
photos: [],
|
||||||
|
loading: false,
|
||||||
|
success: false,
|
||||||
|
error: null
|
||||||
|
};
|
||||||
|
this.photoUploadRef = React.createRef();
|
||||||
|
}
|
||||||
|
|
||||||
|
handleInputChange = (field) => (event) => {
|
||||||
|
this.setState({ [field]: event.target.value });
|
||||||
|
};
|
||||||
|
|
||||||
|
handleRatingChange = (event, newValue) => {
|
||||||
|
this.setState({ rating: newValue });
|
||||||
|
};
|
||||||
|
|
||||||
|
handlePhotosChange = (files) => {
|
||||||
|
this.setState({ photos: files });
|
||||||
|
};
|
||||||
|
|
||||||
|
convertPhotosToBase64 = (photos) => {
|
||||||
|
return Promise.all(
|
||||||
|
photos.map(photo => {
|
||||||
|
return new Promise((resolve, _reject) => {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = (e) => {
|
||||||
|
resolve({
|
||||||
|
name: photo.name,
|
||||||
|
type: photo.type,
|
||||||
|
size: photo.size,
|
||||||
|
data: e.target.result // base64 string
|
||||||
|
});
|
||||||
|
};
|
||||||
|
reader.readAsDataURL(photo);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
handleSubmit = async (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
this.setState({ loading: true });
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Convert photos to base64
|
||||||
|
const photosBase64 = await this.convertPhotosToBase64(this.state.photos);
|
||||||
|
|
||||||
|
// Prepare data for API emission
|
||||||
|
const ratingData = {
|
||||||
|
type: 'article_rating',
|
||||||
|
productId: this.props.productId,
|
||||||
|
productName: this.props.productName,
|
||||||
|
name: this.state.name,
|
||||||
|
email: this.state.email,
|
||||||
|
rating: this.state.rating,
|
||||||
|
review: this.state.review,
|
||||||
|
photos: photosBase64,
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
};
|
||||||
|
|
||||||
|
// Emit data via socket
|
||||||
|
console.log('Article Rating Data to emit:', ratingData);
|
||||||
|
|
||||||
|
if (this.props.socket) {
|
||||||
|
this.props.socket.emit('article_rating_submit', ratingData);
|
||||||
|
|
||||||
|
// Set up response handler
|
||||||
|
this.props.socket.once('article_rating_response', (response) => {
|
||||||
|
if (response.success) {
|
||||||
|
this.setState({
|
||||||
|
loading: false,
|
||||||
|
success: true,
|
||||||
|
name: '',
|
||||||
|
email: '',
|
||||||
|
rating: 0,
|
||||||
|
review: '',
|
||||||
|
photos: []
|
||||||
|
});
|
||||||
|
// Reset the photo upload component
|
||||||
|
if (this.photoUploadRef.current) {
|
||||||
|
this.photoUploadRef.current.reset();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.setState({
|
||||||
|
loading: false,
|
||||||
|
error: response.error || 'Ein Fehler ist aufgetreten'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear messages after 3 seconds
|
||||||
|
setTimeout(() => {
|
||||||
|
this.setState({ success: false, error: null });
|
||||||
|
}, 3000);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
this.setState({
|
||||||
|
loading: false,
|
||||||
|
error: 'Fehler beim Verarbeiten der Fotos'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback timeout in case backend doesn't respond
|
||||||
|
setTimeout(() => {
|
||||||
|
if (this.state.loading) {
|
||||||
|
this.setState({
|
||||||
|
loading: false,
|
||||||
|
success: true,
|
||||||
|
name: '',
|
||||||
|
email: '',
|
||||||
|
rating: 0,
|
||||||
|
review: '',
|
||||||
|
photos: []
|
||||||
|
});
|
||||||
|
// Reset the photo upload component
|
||||||
|
if (this.photoUploadRef.current) {
|
||||||
|
this.photoUploadRef.current.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear success message after 3 seconds
|
||||||
|
setTimeout(() => {
|
||||||
|
this.setState({ success: false });
|
||||||
|
}, 3000);
|
||||||
|
}
|
||||||
|
}, 5000);
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { name, email, rating, review, loading, success, error } = this.state;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Paper sx={{ p: 3, mt: 4, borderRadius: 2, boxShadow: "0 2px 8px rgba(0,0,0,0.08)" }}>
|
||||||
|
<Typography variant="h5" gutterBottom sx={{ fontWeight: 600, color: '#333' }}>
|
||||||
|
Artikel Bewerten
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<Typography variant="body2" color="text.secondary" sx={{ mb: 3 }}>
|
||||||
|
Teilen Sie Ihre Erfahrungen mit diesem Artikel und helfen Sie anderen Kunden bei der Entscheidung.
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
{success && (
|
||||||
|
<Alert severity="success" sx={{ mb: 3 }}>
|
||||||
|
Vielen Dank für Ihre Bewertung! Sie wird nach Prüfung veröffentlicht.
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<Alert severity="error" sx={{ mb: 3 }}>
|
||||||
|
{error}
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Box component="form" onSubmit={this.handleSubmit} sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
|
||||||
|
<TextField
|
||||||
|
label="Name"
|
||||||
|
value={name}
|
||||||
|
onChange={this.handleInputChange('name')}
|
||||||
|
required
|
||||||
|
fullWidth
|
||||||
|
disabled={loading}
|
||||||
|
placeholder="Ihr Name"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TextField
|
||||||
|
label="E-Mail"
|
||||||
|
type="email"
|
||||||
|
value={email}
|
||||||
|
onChange={this.handleInputChange('email')}
|
||||||
|
required
|
||||||
|
fullWidth
|
||||||
|
disabled={loading}
|
||||||
|
placeholder="ihre.email@example.com"
|
||||||
|
helperText="Ihre E-Mail wird nicht veröffentlicht"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
|
||||||
|
<Typography variant="body2" sx={{ fontWeight: 500 }}>
|
||||||
|
Bewertung *
|
||||||
|
</Typography>
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||||
|
<Rating
|
||||||
|
name="article-rating"
|
||||||
|
value={rating}
|
||||||
|
onChange={this.handleRatingChange}
|
||||||
|
size="large"
|
||||||
|
disabled={loading}
|
||||||
|
emptyIcon={<StarIcon style={{ opacity: 0.55 }} fontSize="inherit" />}
|
||||||
|
/>
|
||||||
|
<Typography variant="body2" color="text.secondary">
|
||||||
|
{rating > 0 ? `${rating} von 5 Sternen` : 'Bitte bewerten'}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<TextField
|
||||||
|
label="Ihre Bewertung (optional)"
|
||||||
|
value={review}
|
||||||
|
onChange={this.handleInputChange('review')}
|
||||||
|
fullWidth
|
||||||
|
multiline
|
||||||
|
rows={4}
|
||||||
|
disabled={loading}
|
||||||
|
placeholder="Beschreiben Sie Ihre Erfahrungen mit diesem Artikel..."
|
||||||
|
/>
|
||||||
|
|
||||||
|
<PhotoUpload
|
||||||
|
ref={this.photoUploadRef}
|
||||||
|
onChange={this.handlePhotosChange}
|
||||||
|
disabled={loading}
|
||||||
|
maxFiles={5}
|
||||||
|
label="Fotos zur Bewertung anhängen (optional)"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
variant="contained"
|
||||||
|
disabled={loading || !name || !email || rating === 0}
|
||||||
|
sx={{
|
||||||
|
mt: 2,
|
||||||
|
py: 1.5,
|
||||||
|
fontSize: '1rem',
|
||||||
|
fontWeight: 600
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{loading ? (
|
||||||
|
<>
|
||||||
|
<CircularProgress size={20} sx={{ mr: 1 }} />
|
||||||
|
Wird gesendet...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
'Bewertung abgeben'
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
</Paper>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default withI18n()(ArticleRatingForm);
|
||||||
@@ -8,6 +8,7 @@ import TableBody from '@mui/material/TableBody';
|
|||||||
import TableCell from '@mui/material/TableCell';
|
import TableCell from '@mui/material/TableCell';
|
||||||
import TableRow from '@mui/material/TableRow';
|
import TableRow from '@mui/material/TableRow';
|
||||||
import CartItem from './CartItem.js';
|
import CartItem from './CartItem.js';
|
||||||
|
import { withI18n } from '../i18n/withTranslation.js';
|
||||||
|
|
||||||
|
|
||||||
class CartDropdown extends Component {
|
class CartDropdown extends Component {
|
||||||
@@ -53,8 +54,8 @@ class CartDropdown extends Component {
|
|||||||
currency: 'EUR'
|
currency: 'EUR'
|
||||||
});
|
});
|
||||||
|
|
||||||
const shippingNetPrice = deliveryCost / (1 + 19 / 100);
|
const shippingNetPrice = deliveryCost > 0 ? deliveryCost / (1 + 19 / 100) : 0;
|
||||||
const shippingVat = deliveryCost - shippingNetPrice;
|
const shippingVat = deliveryCost > 0 ? deliveryCost - shippingNetPrice : 0;
|
||||||
const totalVat7 = priceCalculations.vat7;
|
const totalVat7 = priceCalculations.vat7;
|
||||||
const totalVat19 = priceCalculations.vat19 + shippingVat;
|
const totalVat19 = priceCalculations.vat19 + shippingVat;
|
||||||
const totalGross = priceCalculations.totalGross + deliveryCost;
|
const totalGross = priceCalculations.totalGross + deliveryCost;
|
||||||
@@ -63,7 +64,7 @@ class CartDropdown extends Component {
|
|||||||
<>
|
<>
|
||||||
<Box sx={{ bgcolor: 'primary.main', color: 'white', p: 2 }}>
|
<Box sx={{ bgcolor: 'primary.main', color: 'white', p: 2 }}>
|
||||||
<Typography variant="h6">
|
<Typography variant="h6">
|
||||||
{cartItems.length} {cartItems.length === 1 ? 'Produkt' : 'Produkte'}
|
{cartItems.length} {cartItems.length === 1 ? (this.props.t ? this.props.t('cart.itemCount.singular') : 'Produkt') : (this.props.t ? this.props.t('cart.itemCount.plural') : 'Produkte')}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
@@ -83,7 +84,7 @@ class CartDropdown extends Component {
|
|||||||
{/* Display total weight if greater than 0 */}
|
{/* Display total weight if greater than 0 */}
|
||||||
{totalWeight > 0 && (
|
{totalWeight > 0 && (
|
||||||
<Typography variant="subtitle2" sx={{ px: 2, mb: 1 }}>
|
<Typography variant="subtitle2" sx={{ px: 2, mb: 1 }}>
|
||||||
Gesamtgewicht: {totalWeight.toFixed(2)} kg
|
{this.props.t ? this.props.t('cart.summary.totalWeight', { weight: totalWeight.toFixed(2) }) : `Gesamtgewicht: ${totalWeight.toFixed(2)} kg`}
|
||||||
</Typography>
|
</Typography>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -94,7 +95,7 @@ class CartDropdown extends Component {
|
|||||||
// Detailed summary with shipping costs
|
// Detailed summary with shipping costs
|
||||||
<>
|
<>
|
||||||
<Typography variant="h6" gutterBottom sx={{ mt: 2 }}>
|
<Typography variant="h6" gutterBottom sx={{ mt: 2 }}>
|
||||||
Bestellübersicht
|
{this.props.t ? this.props.t('cart.summary.title') : 'Bestellübersicht'}
|
||||||
</Typography>
|
</Typography>
|
||||||
{deliveryMethod && (
|
{deliveryMethod && (
|
||||||
<Typography variant="body2" sx={{ mb: 1, color: 'text.secondary' }}>
|
<Typography variant="body2" sx={{ mb: 1, color: 'text.secondary' }}>
|
||||||
@@ -104,14 +105,14 @@ class CartDropdown extends Component {
|
|||||||
<Table size="small">
|
<Table size="small">
|
||||||
<TableBody>
|
<TableBody>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell>Waren (netto):</TableCell>
|
<TableCell>{this.props.t ? this.props.t('cart.summary.goodsNet') : 'Waren (netto):'}</TableCell>
|
||||||
<TableCell align="right">
|
<TableCell align="right">
|
||||||
{currencyFormatter.format(priceCalculations.totalNet)}
|
{currencyFormatter.format(priceCalculations.totalNet)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
{deliveryCost > 0 && (
|
{deliveryCost > 0 && (
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell>Versandkosten (netto):</TableCell>
|
<TableCell>{this.props.t ? this.props.t('cart.summary.shippingNet') : 'Versandkosten (netto):'}</TableCell>
|
||||||
<TableCell align="right">
|
<TableCell align="right">
|
||||||
{currencyFormatter.format(shippingNetPrice)}
|
{currencyFormatter.format(shippingNetPrice)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
@@ -119,7 +120,7 @@ class CartDropdown extends Component {
|
|||||||
)}
|
)}
|
||||||
{totalVat7 > 0 && (
|
{totalVat7 > 0 && (
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell>7% Mehrwertsteuer:</TableCell>
|
<TableCell>{this.props.t ? this.props.t('tax.vat7') : '7% Mehrwertsteuer'}:</TableCell>
|
||||||
<TableCell align="right">
|
<TableCell align="right">
|
||||||
{currencyFormatter.format(totalVat7)}
|
{currencyFormatter.format(totalVat7)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
@@ -127,28 +128,37 @@ class CartDropdown extends Component {
|
|||||||
)}
|
)}
|
||||||
{totalVat19 > 0 && (
|
{totalVat19 > 0 && (
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell>19% Mehrwertsteuer (inkl. Versand):</TableCell>
|
<TableCell>{this.props.t ? this.props.t('tax.vat19WithShipping') : '19% Mehrwertsteuer (inkl. Versand)'}:</TableCell>
|
||||||
<TableCell align="right">
|
<TableCell align="right">
|
||||||
{currencyFormatter.format(totalVat19)}
|
{currencyFormatter.format(totalVat19)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
)}
|
)}
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell sx={{ fontWeight: 'bold' }}>Gesamtsumme Waren:</TableCell>
|
<TableCell sx={{ fontWeight: 'bold' }}>{this.props.t ? this.props.t('cart.summary.totalGoods') : 'Gesamtsumme Waren:'}</TableCell>
|
||||||
<TableCell align="right" sx={{ fontWeight: 'bold' }}>
|
<TableCell align="right" sx={{ fontWeight: 'bold' }}>
|
||||||
{currencyFormatter.format(priceCalculations.totalGross)}
|
{currencyFormatter.format(priceCalculations.totalGross)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
{deliveryCost > 0 && (
|
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell sx={{ fontWeight: 'bold' }}>Versandkosten:</TableCell>
|
<TableCell sx={{ fontWeight: 'bold' }}>
|
||||||
|
{this.props.t ? this.props.t('cart.summary.shippingCosts') : 'Versandkosten:'}
|
||||||
|
{deliveryCost === 0 && priceCalculations.totalGross < 100 && (
|
||||||
|
<span style={{ color: '#2e7d32', fontSize: '0.8em', marginLeft: '4px' }}>
|
||||||
|
{this.props.t ? this.props.t('cart.summary.freeFrom100') : '(kostenlos ab 100€)'}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</TableCell>
|
||||||
<TableCell align="right" sx={{ fontWeight: 'bold' }}>
|
<TableCell align="right" sx={{ fontWeight: 'bold' }}>
|
||||||
{currencyFormatter.format(deliveryCost)}
|
{deliveryCost === 0 ? (
|
||||||
|
<span style={{ color: '#2e7d32' }}>{this.props.t ? this.props.t('cart.summary.free') : 'kostenlos'}</span>
|
||||||
|
) : (
|
||||||
|
currencyFormatter.format(deliveryCost)
|
||||||
|
)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
)}
|
|
||||||
<TableRow sx={{ borderTop: '1px solid #e0e0e0' }}>
|
<TableRow sx={{ borderTop: '1px solid #e0e0e0' }}>
|
||||||
<TableCell sx={{ fontWeight: 'bold', pt: 2 }}>Gesamtsumme:</TableCell>
|
<TableCell sx={{ fontWeight: 'bold', pt: 2 }}>{this.props.t ? this.props.t('cart.summary.total') : 'Gesamtsumme:'}</TableCell>
|
||||||
<TableCell align="right" sx={{ fontWeight: 'bold', pt: 2 }}>
|
<TableCell align="right" sx={{ fontWeight: 'bold', pt: 2 }}>
|
||||||
{currencyFormatter.format(totalGross)}
|
{currencyFormatter.format(totalGross)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
@@ -161,14 +171,14 @@ class CartDropdown extends Component {
|
|||||||
<Table size="small">
|
<Table size="small">
|
||||||
<TableBody>
|
<TableBody>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell>Gesamtnettopreis:</TableCell>
|
<TableCell>{this.props.t ? this.props.t('tax.totalNet') : 'Gesamtnettopreis'}:</TableCell>
|
||||||
<TableCell align="right">
|
<TableCell align="right">
|
||||||
{new Intl.NumberFormat('de-DE', {style: 'currency', currency: 'EUR'}).format(priceCalculations.totalNet)}
|
{new Intl.NumberFormat('de-DE', {style: 'currency', currency: 'EUR'}).format(priceCalculations.totalNet)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
{priceCalculations.vat7 > 0 && (
|
{priceCalculations.vat7 > 0 && (
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell>7% Mehrwertsteuer:</TableCell>
|
<TableCell>{this.props.t ? this.props.t('tax.vat7') : '7% Mehrwertsteuer'}:</TableCell>
|
||||||
<TableCell align="right">
|
<TableCell align="right">
|
||||||
{new Intl.NumberFormat('de-DE', {style: 'currency', currency: 'EUR'}).format(priceCalculations.vat7)}
|
{new Intl.NumberFormat('de-DE', {style: 'currency', currency: 'EUR'}).format(priceCalculations.vat7)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
@@ -176,14 +186,14 @@ class CartDropdown extends Component {
|
|||||||
)}
|
)}
|
||||||
{priceCalculations.vat19 > 0 && (
|
{priceCalculations.vat19 > 0 && (
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell>19% Mehrwertsteuer:</TableCell>
|
<TableCell>{this.props.t ? this.props.t('tax.vat19') : '19% Mehrwertsteuer'}:</TableCell>
|
||||||
<TableCell align="right">
|
<TableCell align="right">
|
||||||
{new Intl.NumberFormat('de-DE', {style: 'currency', currency: 'EUR'}).format(priceCalculations.vat19)}
|
{new Intl.NumberFormat('de-DE', {style: 'currency', currency: 'EUR'}).format(priceCalculations.vat19)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
)}
|
)}
|
||||||
<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' }}>
|
<TableCell align="right" sx={{ fontWeight: 'bold' }}>
|
||||||
{new Intl.NumberFormat('de-DE', {style: 'currency', currency: 'EUR'}).format(priceCalculations.totalGross)}
|
{new Intl.NumberFormat('de-DE', {style: 'currency', currency: 'EUR'}).format(priceCalculations.totalGross)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
@@ -201,7 +211,7 @@ class CartDropdown extends Component {
|
|||||||
fullWidth
|
fullWidth
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
>
|
>
|
||||||
Weiter einkaufen
|
{this.props.t ? this.props.t('cart.continueShopping') : 'Weiter einkaufen'}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -213,7 +223,7 @@ class CartDropdown extends Component {
|
|||||||
sx={{ mt: 2 }}
|
sx={{ mt: 2 }}
|
||||||
onClick={onCheckout}
|
onClick={onCheckout}
|
||||||
>
|
>
|
||||||
Weiter zur Kasse
|
{this.props.t ? this.props.t('cart.proceedToCheckout') : 'Weiter zur Kasse'}
|
||||||
</Button>
|
</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 Box from '@mui/material/Box';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import AddToCartButton from './AddToCartButton.js';
|
import AddToCartButton from './AddToCartButton.js';
|
||||||
|
import { withI18n } from '../i18n/withTranslation.js';
|
||||||
|
|
||||||
class CartItem extends Component {
|
class CartItem extends Component {
|
||||||
|
|
||||||
@@ -116,7 +117,7 @@ class CartItem extends Component {
|
|||||||
)}
|
)}
|
||||||
{item.versandklasse && item.versandklasse != 'standard' && item.versandklasse != 'kostenlos' && (
|
{item.versandklasse && item.versandklasse != 'standard' && item.versandklasse != 'kostenlos' && (
|
||||||
<Typography variant="body2" color="warning.main" fontWeight="medium" component="div">
|
<Typography variant="body2" color="warning.main" fontWeight="medium" component="div">
|
||||||
{item.versandklasse}
|
{item.versandklasse == 'nur Abholung' ? this.props.t('delivery.descriptions.pickupOnly') : item.versandklasse}
|
||||||
</Typography>
|
</Typography>
|
||||||
)}
|
)}
|
||||||
{item.vat && (
|
{item.vat && (
|
||||||
@@ -126,9 +127,9 @@ class CartItem extends Component {
|
|||||||
fontStyle="italic"
|
fontStyle="italic"
|
||||||
component="div"
|
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))
|
(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>
|
</Typography>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -146,11 +147,14 @@ class CartItem extends Component {
|
|||||||
display: "block"
|
display: "block"
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{this.props.id.toString().endsWith("steckling") ? "Lieferzeit: 14 Tage" :
|
{this.props.id.toString().endsWith("steckling") ?
|
||||||
item.available == 1 ? "Lieferzeit: 2-3 Tage" :
|
(this.props.t ? this.props.t('delivery.times.cutting14Days') : "Lieferzeit: 14 Tage") :
|
||||||
item.availableSupplier == 1 ? "Lieferzeit: 7-9 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>
|
</Typography>
|
||||||
<AddToCartButton available={1} id={this.props.id} availableSupplier={item.availableSupplier} price={item.price} seoName={item.seoName} name={item.name} weight={item.weight} vat={item.vat} versandklasse={item.versandklasse}/>
|
<AddToCartButton available={1} id={this.props.id} komponenten={item.komponenten} availableSupplier={item.availableSupplier} price={item.price} seoName={item.seoName} name={item.name} weight={item.weight} vat={item.vat} versandklasse={item.versandklasse}/>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
@@ -159,4 +163,4 @@ class CartItem extends Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default CartItem;
|
export default withI18n()(CartItem);
|
||||||
@@ -16,7 +16,7 @@ const CategoryBox = ({
|
|||||||
name,
|
name,
|
||||||
seoName,
|
seoName,
|
||||||
bgcolor,
|
bgcolor,
|
||||||
fontSize = '0.8rem',
|
fontSize = '1.2rem',
|
||||||
...props
|
...props
|
||||||
}) => {
|
}) => {
|
||||||
const [imageUrl, setImageUrl] = useState(null);
|
const [imageUrl, setImageUrl] = useState(null);
|
||||||
@@ -186,7 +186,7 @@ const CategoryBox = ({
|
|||||||
fontFamily: 'SwashingtonCP, "Times New Roman", Georgia, serif',
|
fontFamily: 'SwashingtonCP, "Times New Roman", Georgia, serif',
|
||||||
fontWeight: 'normal',
|
fontWeight: 'normal',
|
||||||
lineHeight: '1.2',
|
lineHeight: '1.2',
|
||||||
padding: '0 8px'
|
padding: '12px 8px'
|
||||||
}}>
|
}}>
|
||||||
{name}
|
{name}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import CategoryBox from './CategoryBox.js';
|
|||||||
|
|
||||||
import { useParams, useSearchParams } from 'react-router-dom';
|
import { useParams, useSearchParams } from 'react-router-dom';
|
||||||
import { getAllSettingsWithPrefix } from '../utils/sessionStorage.js';
|
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);
|
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 attributeSettings = getAllSettingsWithPrefix('filter_attribute_');
|
||||||
const manufacturerSettings = getAllSettingsWithPrefix('filter_manufacturer_');
|
const manufacturerSettings = getAllSettingsWithPrefix('filter_manufacturer_');
|
||||||
const availabilitySettings = getAllSettingsWithPrefix('filter_availability_');
|
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'
|
// Check for "auf Lager" filter (in stock) - it's active when filter_availability is NOT set to '1'
|
||||||
if (availabilityFilter !== '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
|
// Check for "Neu" filter (new) - only show if there are actually new products and filter is active
|
||||||
if (availabilityFilters.includes('2') && hasNewProducts) {
|
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
|
// 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) {
|
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};
|
return {filteredProducts,activeAttributeFilters:activeAttributeFiltersWithNames,activeManufacturerFilters:activeManufacturerFiltersWithNames,activeAvailabilityFilters};
|
||||||
@@ -256,7 +257,8 @@ class Content extends Component {
|
|||||||
unfilteredProducts: unfilteredProducts,
|
unfilteredProducts: unfilteredProducts,
|
||||||
...getFilteredProducts(
|
...getFilteredProducts(
|
||||||
unfilteredProducts,
|
unfilteredProducts,
|
||||||
response.attributes
|
response.attributes,
|
||||||
|
this.props.t
|
||||||
),
|
),
|
||||||
categoryName: response.categoryName || response.name || null,
|
categoryName: response.categoryName || response.name || null,
|
||||||
dataType: response.dataType,
|
dataType: response.dataType,
|
||||||
@@ -385,7 +387,8 @@ class Content extends Component {
|
|||||||
this.setState({
|
this.setState({
|
||||||
...getFilteredProducts(
|
...getFilteredProducts(
|
||||||
this.state.unfilteredProducts,
|
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) &&
|
{(this.getCurrentCategoryId() == 706 || this.getCurrentCategoryId() == 689) &&
|
||||||
<Box sx={{ display: { xs: 'none', sm: 'block' } }}>
|
<Box sx={{ display: { xs: 'none', sm: 'block' } }}>
|
||||||
<Typography variant="h6" sx={{mt:3}}>
|
<Typography variant="h6" sx={{mt:3}}>
|
||||||
Andere Kategorien
|
{this.props.t ? this.props.t('navigation.otherCategories') : 'Andere Kategorien'}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
}
|
}
|
||||||
@@ -647,7 +650,7 @@ class Content extends Component {
|
|||||||
p: 2,
|
p: 2,
|
||||||
}}>
|
}}>
|
||||||
<Typography sx={{ fontSize: '1.3rem', color: 'white', fontFamily: 'SwashingtonCP' }}>
|
<Typography sx={{ fontSize: '1.3rem', color: 'white', fontFamily: 'SwashingtonCP' }}>
|
||||||
Seeds
|
{this.props.t('sections.seeds')}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
@@ -694,7 +697,7 @@ class Content extends Component {
|
|||||||
p: 2,
|
p: 2,
|
||||||
}}>
|
}}>
|
||||||
<Typography sx={{ fontSize: '1.3rem', color: 'white', fontFamily: 'SwashingtonCP' }}>
|
<Typography sx={{ fontSize: '1.3rem', color: 'white', fontFamily: 'SwashingtonCP' }}>
|
||||||
Stecklinge
|
{this.props.t('sections.stecklinge')}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
</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 { Link as RouterLink } from 'react-router-dom';
|
||||||
import { styled } from '@mui/material/styles';
|
import { styled } from '@mui/material/styles';
|
||||||
import Paper from '@mui/material/Paper';
|
import Paper from '@mui/material/Paper';
|
||||||
|
import { withI18n } from '../i18n/withTranslation.js';
|
||||||
|
|
||||||
// Styled component for the router links
|
// Styled component for the router links
|
||||||
const StyledRouterLink = styled(RouterLink)(() => ({
|
const StyledRouterLink = styled(RouterLink)(() => ({
|
||||||
@@ -229,9 +230,9 @@ class Footer extends Component {
|
|||||||
alignItems={{ xs: 'center', md: 'left' }}
|
alignItems={{ xs: 'center', md: 'left' }}
|
||||||
flexWrap="wrap"
|
flexWrap="wrap"
|
||||||
>
|
>
|
||||||
<StyledRouterLink to="/datenschutz">Datenschutz</StyledRouterLink>
|
<StyledRouterLink to="/datenschutz">{this.props.t ? this.props.t('footer.legal.datenschutz') : 'Datenschutz'}</StyledRouterLink>
|
||||||
<StyledRouterLink to="/agb">AGB</StyledRouterLink>
|
<StyledRouterLink to="/agb">{this.props.t ? this.props.t('footer.legal.agb') : 'AGB'}</StyledRouterLink>
|
||||||
<StyledRouterLink to="/sitemap">Sitemap</StyledRouterLink>
|
<StyledRouterLink to="/sitemap">{this.props.t ? this.props.t('footer.legal.sitemap') : 'Sitemap'}</StyledRouterLink>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
<Stack
|
<Stack
|
||||||
@@ -241,9 +242,9 @@ class Footer extends Component {
|
|||||||
alignItems={{ xs: 'center', md: 'left' }}
|
alignItems={{ xs: 'center', md: 'left' }}
|
||||||
flexWrap="wrap"
|
flexWrap="wrap"
|
||||||
>
|
>
|
||||||
<StyledRouterLink to="/impressum">Impressum</StyledRouterLink>
|
<StyledRouterLink to="/impressum">{this.props.t ? this.props.t('footer.legal.impressum') : 'Impressum'}</StyledRouterLink>
|
||||||
<StyledRouterLink to="/batteriegesetzhinweise">Batteriegesetzhinweise</StyledRouterLink>
|
<StyledRouterLink to="/batteriegesetzhinweise">{this.props.t ? this.props.t('footer.legal.batteriegesetzhinweise') : 'Batteriegesetzhinweise'}</StyledRouterLink>
|
||||||
<StyledRouterLink to="/widerrufsrecht">Widerrufsrecht</StyledRouterLink>
|
<StyledRouterLink to="/widerrufsrecht">{this.props.t ? this.props.t('footer.legal.widerrufsrecht') : 'Widerrufsrecht'}</StyledRouterLink>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
{/* Payment Methods Section */}
|
{/* Payment Methods Section */}
|
||||||
@@ -338,7 +339,7 @@ class Footer extends Component {
|
|||||||
{/* Copyright Section */}
|
{/* Copyright Section */}
|
||||||
<Box sx={{ pb:'20px',textAlign: 'center', filter: 'drop-shadow(0 4px 8px rgba(0, 0, 0, 0.3))', opacity: 0.7 }}>
|
<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 }}>
|
<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>
|
||||||
<Typography variant="body2" sx={{ fontSize: { xs: '11px', md: '14px' }, lineHeight: 1.5 }}>
|
<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>
|
© {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 Button from '@mui/material/Button';
|
||||||
import GoogleIcon from '@mui/icons-material/Google';
|
import GoogleIcon from '@mui/icons-material/Google';
|
||||||
import GoogleAuthContext from '../contexts/GoogleAuthContext.js';
|
import GoogleAuthContext from '../contexts/GoogleAuthContext.js';
|
||||||
|
import { withI18n } from '../i18n/index.js';
|
||||||
|
|
||||||
class GoogleLoginButton extends Component {
|
class GoogleLoginButton extends Component {
|
||||||
static contextType = GoogleAuthContext;
|
static contextType = GoogleAuthContext;
|
||||||
@@ -186,7 +187,7 @@ class GoogleLoginButton extends Component {
|
|||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
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 { isInitializing, isPrompting } = this.state;
|
||||||
const isLoading = isInitializing || isPrompting || (this.context && !this.context.isLoaded);
|
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() {
|
render() {
|
||||||
// Get socket directly from context in render method
|
// Get socket directly from context in render method
|
||||||
const {socket,socketB} = this.context;
|
const {socket,socketB} = this.context;
|
||||||
const { isHomePage, isProfilePage } = this.props;
|
const { isHomePage, isProfilePage, isAktionenPage, isFilialePage } = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AppBar position="sticky" color="primary" elevation={0} sx={{ zIndex: 1100 }}>
|
<AppBar position="sticky" color="primary" elevation={0} sx={{ zIndex: 1100 }}>
|
||||||
@@ -94,7 +94,7 @@ class Header extends Component {
|
|||||||
</Box>
|
</Box>
|
||||||
</Container>
|
</Container>
|
||||||
</Toolbar>
|
</Toolbar>
|
||||||
{(isHomePage || this.props.categoryId || isProfilePage) && <CategoryList categoryId={209} activeCategoryId={this.props.categoryId} socket={socket} socketB={socketB} />}
|
{(isHomePage || this.props.categoryId || isProfilePage || isAktionenPage || isFilialePage) && <CategoryList categoryId={209} activeCategoryId={this.props.categoryId} socket={socket} socketB={socketB} />}
|
||||||
</AppBar>
|
</AppBar>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -105,10 +105,12 @@ const HeaderWithContext = (props) => {
|
|||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const isHomePage = location.pathname === '/';
|
const isHomePage = location.pathname === '/';
|
||||||
const isProfilePage = location.pathname === '/profile';
|
const isProfilePage = location.pathname === '/profile';
|
||||||
|
const isAktionenPage = location.pathname === '/aktionen';
|
||||||
|
const isFilialePage = location.pathname === '/filiale';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SocketContext.Consumer>
|
<SocketContext.Consumer>
|
||||||
{({socket,socketB}) => <Header {...props} socket={socket} socketB={socketB} isHomePage={isHomePage} isProfilePage={isProfilePage} />}
|
{({socket,socketB}) => <Header {...props} socket={socket} socketB={socketB} isHomePage={isHomePage} isProfilePage={isProfilePage} isAktionenPage={isAktionenPage} isFilialePage={isFilialePage} />}
|
||||||
</SocketContext.Consumer>
|
</SocketContext.Consumer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
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 CartSyncDialog from './CartSyncDialog.js';
|
||||||
import { localAndArchiveServer, mergeCarts } from '../utils/cartUtils.js';
|
import { localAndArchiveServer, mergeCarts } from '../utils/cartUtils.js';
|
||||||
import config from '../config.js';
|
import config from '../config.js';
|
||||||
|
import { withI18n } from '../i18n/withTranslation.js';
|
||||||
|
|
||||||
// Lazy load GoogleAuthProvider
|
// Lazy load GoogleAuthProvider
|
||||||
const GoogleAuthProvider = lazy(() => import('../providers/GoogleAuthProvider.js'));
|
const GoogleAuthProvider = lazy(() => import('../providers/GoogleAuthProvider.js'));
|
||||||
@@ -510,7 +511,7 @@ export class LoginComponent extends Component {
|
|||||||
color={isAdmin ? 'secondary' : 'inherit'}
|
color={isAdmin ? 'secondary' : 'inherit'}
|
||||||
sx={{ my: 1, mx: 1.5 }}
|
sx={{ my: 1, mx: 1.5 }}
|
||||||
>
|
>
|
||||||
Profil
|
{this.props.t ? this.props.t('auth.profile') : 'Profil'}
|
||||||
</Button>
|
</Button>
|
||||||
<Menu
|
<Menu
|
||||||
disableScrollLock={true}
|
disableScrollLock={true}
|
||||||
@@ -526,14 +527,28 @@ export class LoginComponent extends Component {
|
|||||||
horizontal: 'right',
|
horizontal: 'right',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<MenuItem component={Link} to="/profile" onClick={this.handleUserMenuClose}>Profil</MenuItem>
|
<MenuItem component={Link} to="/profile" onClick={this.handleUserMenuClose}>
|
||||||
<MenuItem component={Link} to="/profile#cart" onClick={this.handleUserMenuClose} sx={{ pl: 4 }}>Bestellabschluss</MenuItem>
|
{this.props.t ? this.props.t('auth.menu.profile') : 'Profil'}
|
||||||
<MenuItem component={Link} to="/profile#orders" onClick={this.handleUserMenuClose} sx={{ pl: 4 }}>Bestellungen</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem component={Link} to="/profile#settings" onClick={this.handleUserMenuClose} sx={{ pl: 4 }}>Einstellungen</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 />
|
<Divider />
|
||||||
{isAdmin ? <MenuItem component={Link} to="/admin" onClick={this.handleUserMenuClose}>Admin Dashboard</MenuItem> : null}
|
{isAdmin ? <MenuItem component={Link} to="/admin" onClick={this.handleUserMenuClose}>
|
||||||
{isAdmin ? <MenuItem component={Link} to="/admin/users" onClick={this.handleUserMenuClose}>Admin Users</MenuItem> : null}
|
{this.props.t ? this.props.t('auth.menu.adminDashboard') : 'Admin Dashboard'}
|
||||||
<MenuItem onClick={this.handleLogout}>Abmelden</MenuItem>
|
</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>
|
</Menu>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
@@ -543,7 +558,7 @@ export class LoginComponent extends Component {
|
|||||||
onClick={this.handleOpen}
|
onClick={this.handleOpen}
|
||||||
sx={{ my: 1, mx: 1.5 }}
|
sx={{ my: 1, mx: 1.5 }}
|
||||||
>
|
>
|
||||||
Login
|
{this.props.t ? this.props.t('auth.login') : 'Login'}
|
||||||
</Button>
|
</Button>
|
||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
@@ -558,7 +573,10 @@ export class LoginComponent extends Component {
|
|||||||
<DialogTitle sx={{ bgcolor: 'white', pb: 0 }}>
|
<DialogTitle sx={{ bgcolor: 'white', pb: 0 }}>
|
||||||
<Box display="flex" justifyContent="space-between" alignItems="center">
|
<Box display="flex" justifyContent="space-between" alignItems="center">
|
||||||
<Typography variant="h6" color="#2e7d32" fontWeight="bold">
|
<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>
|
</Typography>
|
||||||
<IconButton edge="end" onClick={this.handleClose} aria-label="close">
|
<IconButton edge="end" onClick={this.handleClose} aria-label="close">
|
||||||
<CloseIcon />
|
<CloseIcon />
|
||||||
@@ -578,14 +596,14 @@ export class LoginComponent extends Component {
|
|||||||
textColor="inherit"
|
textColor="inherit"
|
||||||
>
|
>
|
||||||
<Tab
|
<Tab
|
||||||
label="ANMELDEN"
|
label={this.props.t ? this.props.t('auth.login').toUpperCase() : "ANMELDEN"}
|
||||||
sx={{
|
sx={{
|
||||||
color: tabValue === 0 ? '#2e7d32' : 'inherit',
|
color: tabValue === 0 ? '#2e7d32' : 'inherit',
|
||||||
fontWeight: 'bold'
|
fontWeight: 'bold'
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Tab
|
<Tab
|
||||||
label="REGISTRIEREN"
|
label={this.props.t ? this.props.t('auth.register').toUpperCase() : "REGISTRIEREN"}
|
||||||
sx={{
|
sx={{
|
||||||
color: tabValue === 1 ? '#2e7d32' : 'inherit',
|
color: tabValue === 1 ? '#2e7d32' : 'inherit',
|
||||||
fontWeight: 'bold'
|
fontWeight: 'bold'
|
||||||
@@ -598,7 +616,14 @@ export class LoginComponent extends Component {
|
|||||||
<Box sx={{ width: '100%', display: 'flex', flexDirection: 'column', alignItems: 'center', mb: 2 }}>
|
<Box sx={{ width: '100%', display: 'flex', flexDirection: 'column', alignItems: 'center', mb: 2 }}>
|
||||||
{!privacyConfirmed && (
|
{!privacyConfirmed && (
|
||||||
<Typography variant="caption" sx={{ mb: 1, textAlign: 'center' }}>
|
<Typography variant="caption" sx={{ mb: 1, textAlign: 'center' }}>
|
||||||
|
{this.props.t ?
|
||||||
|
<>
|
||||||
|
{this.props.t('auth.privacyAccept')} <Link to="/datenschutz" style={{ color: '#4285F4' }}>{this.props.t('auth.privacyPolicy')}</Link>
|
||||||
|
</> :
|
||||||
|
<>
|
||||||
Mit dem Click auf "Mit Google anmelden" akzeptiere ich die <Link to="/datenschutz" style={{ color: '#4285F4' }}>Datenschutzbestimmungen</Link>
|
Mit dem Click auf "Mit Google anmelden" akzeptiere ich die <Link to="/datenschutz" style={{ color: '#4285F4' }}>Datenschutzbestimmungen</Link>
|
||||||
|
</>
|
||||||
|
}
|
||||||
</Typography>
|
</Typography>
|
||||||
)}
|
)}
|
||||||
{!showGoogleAuth && (
|
{!showGoogleAuth && (
|
||||||
@@ -611,7 +636,7 @@ export class LoginComponent extends Component {
|
|||||||
}}
|
}}
|
||||||
sx={{ width: '100%', backgroundColor: '#4285F4', color: 'white' }}
|
sx={{ width: '100%', backgroundColor: '#4285F4', color: 'white' }}
|
||||||
>
|
>
|
||||||
Mit Google anmelden
|
{this.props.t ? this.props.t('auth.loginWithGoogle') : 'Mit Google anmelden'}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -643,7 +668,9 @@ export class LoginComponent extends Component {
|
|||||||
{/* OR Divider */}
|
{/* OR Divider */}
|
||||||
<Box sx={{ display: 'flex', alignItems: 'center', my: 2 }}>
|
<Box sx={{ display: 'flex', alignItems: 'center', my: 2 }}>
|
||||||
<Box sx={{ flex: 1, height: '1px', backgroundColor: '#e0e0e0' }} />
|
<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 sx={{ flex: 1, height: '1px', backgroundColor: '#e0e0e0' }} />
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
@@ -654,7 +681,7 @@ export class LoginComponent extends Component {
|
|||||||
<Box sx={{ py: 1 }}>
|
<Box sx={{ py: 1 }}>
|
||||||
<TextField
|
<TextField
|
||||||
margin="dense"
|
margin="dense"
|
||||||
label="E-Mail"
|
label={this.props.t ? this.props.t('auth.email') : 'E-Mail'}
|
||||||
type="email"
|
type="email"
|
||||||
fullWidth
|
fullWidth
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
@@ -665,7 +692,7 @@ export class LoginComponent extends Component {
|
|||||||
|
|
||||||
<TextField
|
<TextField
|
||||||
margin="dense"
|
margin="dense"
|
||||||
label="Passwort"
|
label={this.props.t ? this.props.t('auth.password') : 'Passwort'}
|
||||||
type="password"
|
type="password"
|
||||||
fullWidth
|
fullWidth
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
@@ -687,7 +714,7 @@ export class LoginComponent extends Component {
|
|||||||
'&:hover': { backgroundColor: 'transparent', textDecoration: 'underline' }
|
'&:hover': { backgroundColor: 'transparent', textDecoration: 'underline' }
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Passwort vergessen?
|
{this.props.t ? this.props.t('auth.forgotPassword') : 'Passwort vergessen?'}
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
@@ -695,7 +722,7 @@ export class LoginComponent extends Component {
|
|||||||
{tabValue === 1 && (
|
{tabValue === 1 && (
|
||||||
<TextField
|
<TextField
|
||||||
margin="dense"
|
margin="dense"
|
||||||
label="Passwort bestätigen"
|
label={this.props.t ? this.props.t('auth.confirmPassword') : 'Passwort bestätigen'}
|
||||||
type="password"
|
type="password"
|
||||||
fullWidth
|
fullWidth
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
@@ -717,7 +744,7 @@ export class LoginComponent extends Component {
|
|||||||
onClick={tabValue === 0 ? this.handleLogin : this.handleRegister}
|
onClick={tabValue === 0 ? this.handleLogin : this.handleRegister}
|
||||||
sx={{ mt: 2, bgcolor: '#2e7d32', '&:hover': { bgcolor: '#1b5e20' } }}
|
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>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
@@ -740,4 +767,4 @@ export class LoginComponent extends Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withRouter(LoginComponent);
|
export default withRouter(withI18n()(LoginComponent));
|
||||||
647
src/components/MainPageLayout.js
Normal file
647
src/components/MainPageLayout.js
Normal file
@@ -0,0 +1,647 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { useLocation } from "react-router-dom";
|
||||||
|
import Container from "@mui/material/Container";
|
||||||
|
import Box from "@mui/material/Box";
|
||||||
|
import Typography from "@mui/material/Typography";
|
||||||
|
import Paper from "@mui/material/Paper";
|
||||||
|
import Grid from "@mui/material/Grid";
|
||||||
|
import ChevronLeft from "@mui/icons-material/ChevronLeft";
|
||||||
|
import ChevronRight from "@mui/icons-material/ChevronRight";
|
||||||
|
import { Link } from "react-router-dom";
|
||||||
|
import SharedCarousel from "./SharedCarousel.js";
|
||||||
|
import { getCombinedAnimatedBorderStyles } from "../utils/animatedBorderStyles.js";
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
const MainPageLayout = () => {
|
||||||
|
const location = useLocation();
|
||||||
|
const currentPath = location.pathname;
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [starHovered, setStarHovered] = React.useState(false);
|
||||||
|
|
||||||
|
// Determine which page we're on
|
||||||
|
const isHome = currentPath === "/";
|
||||||
|
const isAktionen = currentPath === "/aktionen";
|
||||||
|
const isFiliale = currentPath === "/filiale";
|
||||||
|
|
||||||
|
// Add CSS animations for rotating stars
|
||||||
|
React.useEffect(() => {
|
||||||
|
const style = document.createElement('style');
|
||||||
|
style.textContent = `
|
||||||
|
@keyframes rotateClockwise {
|
||||||
|
from { transform: rotate(0deg); }
|
||||||
|
to { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes rotateCounterClockwise {
|
||||||
|
from { transform: rotate(0deg); }
|
||||||
|
to { transform: rotate(-360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.star-rotate-slow-cw {
|
||||||
|
animation: rotateClockwise 60s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.star-rotate-slow-ccw {
|
||||||
|
animation: rotateCounterClockwise 45s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.star-rotate-medium-cw {
|
||||||
|
animation: rotateClockwise 30s linear infinite;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
document.head.appendChild(style);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.head.removeChild(style);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Get navigation config based on current page
|
||||||
|
const getNavigationConfig = () => {
|
||||||
|
if (isHome) {
|
||||||
|
return {
|
||||||
|
leftNav: { text: t('navigation.aktionen'), link: "/aktionen" },
|
||||||
|
rightNav: { text: t('navigation.filiale'), link: "/filiale" }
|
||||||
|
};
|
||||||
|
} else if (isAktionen) {
|
||||||
|
return {
|
||||||
|
leftNav: { text: t('navigation.filiale'), link: "/filiale" },
|
||||||
|
rightNav: { text: t('navigation.home'), link: "/" }
|
||||||
|
};
|
||||||
|
} else if (isFiliale) {
|
||||||
|
return {
|
||||||
|
leftNav: { text: t('navigation.home'), link: "/" },
|
||||||
|
rightNav: { text: t('navigation.aktionen'), link: "/aktionen" }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return { leftNav: null, rightNav: null };
|
||||||
|
};
|
||||||
|
|
||||||
|
const allTitles = {
|
||||||
|
home: t('titles.home'),
|
||||||
|
aktionen: t('titles.aktionen'),
|
||||||
|
filiale: t('titles.filiale')
|
||||||
|
};
|
||||||
|
|
||||||
|
// Define all content boxes for layered rendering
|
||||||
|
const allContentBoxes = {
|
||||||
|
home: [
|
||||||
|
{
|
||||||
|
title: t('sections.seeds'),
|
||||||
|
image: "/assets/images/seeds.jpg",
|
||||||
|
bgcolor: "#e1f0d3",
|
||||||
|
link: "/Kategorie/Seeds"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t('sections.stecklinge'),
|
||||||
|
image: "/assets/images/cutlings.jpg",
|
||||||
|
bgcolor: "#e8f5d6",
|
||||||
|
link: "/Kategorie/Stecklinge"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
aktionen: [
|
||||||
|
{
|
||||||
|
title: t('sections.oilPress'),
|
||||||
|
image: "/assets/images/presse.jpg",
|
||||||
|
bgcolor: "#e1f0d3",
|
||||||
|
link: "/presseverleih"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t('sections.thcTest'),
|
||||||
|
image: "/assets/images/purpl.jpg",
|
||||||
|
bgcolor: "#e8f5d6",
|
||||||
|
link: "/thc-test"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
filiale: [
|
||||||
|
{
|
||||||
|
title: t('sections.address1'),
|
||||||
|
image: "/assets/images/filiale1.jpg",
|
||||||
|
bgcolor: "#e1f0d3",
|
||||||
|
link: "/filiale"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t('sections.address2'),
|
||||||
|
image: "/assets/images/filiale2.jpg",
|
||||||
|
bgcolor: "#e8f5d6",
|
||||||
|
link: "/filiale"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get opacity for each page layer
|
||||||
|
const getOpacity = (pageType) => {
|
||||||
|
if (pageType === "home" && isHome) return 1;
|
||||||
|
if (pageType === "aktionen" && isAktionen) return 1;
|
||||||
|
if (pageType === "filiale" && isFiliale) return 1;
|
||||||
|
return 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
const navConfig = getNavigationConfig();
|
||||||
|
|
||||||
|
// Navigation text mapping for translation
|
||||||
|
const navTexts = [
|
||||||
|
{ key: 'aktionen', text: t('navigation.aktionen'), link: '/aktionen' },
|
||||||
|
{ key: 'filiale', text: t('navigation.filiale'), link: '/filiale' },
|
||||||
|
{ key: 'home', text: t('navigation.home'), link: '/' }
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container maxWidth="lg" sx={{ py: 2 }}>
|
||||||
|
<style>{getCombinedAnimatedBorderStyles(['seeds', 'cutlings'])}</style>
|
||||||
|
|
||||||
|
{/* Main Navigation Header */}
|
||||||
|
<Box sx={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
mb: 4,
|
||||||
|
mt: 2,
|
||||||
|
px: 0,
|
||||||
|
transition: "all 0.3s ease-in-out",
|
||||||
|
// Portrait phone: stack title above navigation
|
||||||
|
flexDirection: {
|
||||||
|
xs: "column",
|
||||||
|
sm: "row"
|
||||||
|
}
|
||||||
|
}}>
|
||||||
|
{/* Title for portrait phones - shown first */}
|
||||||
|
<Box sx={{
|
||||||
|
display: { xs: "block", sm: "none" },
|
||||||
|
mb: { xs: 2, sm: 0 },
|
||||||
|
width: "100%",
|
||||||
|
textAlign: "center",
|
||||||
|
position: "relative"
|
||||||
|
}}>
|
||||||
|
{Object.entries(allTitles).map(([pageType, title]) => (
|
||||||
|
<Typography
|
||||||
|
key={pageType}
|
||||||
|
variant="h3"
|
||||||
|
component="h1"
|
||||||
|
sx={{
|
||||||
|
fontFamily: "SwashingtonCP",
|
||||||
|
fontSize: { xs: "2.125rem", sm: "2.125rem", md: "3rem" },
|
||||||
|
textAlign: "center",
|
||||||
|
color: "primary.main",
|
||||||
|
textShadow: "3px 3px 10px rgba(0, 0, 0, 0.4)",
|
||||||
|
transition: "opacity 0.5s ease-in-out",
|
||||||
|
opacity: getOpacity(pageType),
|
||||||
|
position: pageType === "home" ? "relative" : "absolute",
|
||||||
|
top: pageType !== "home" ? 0 : "auto",
|
||||||
|
left: pageType !== "home" ? 0 : "auto",
|
||||||
|
transform: "none",
|
||||||
|
width: "100%",
|
||||||
|
pointerEvents: getOpacity(pageType) === 1 ? "auto" : "none",
|
||||||
|
lineHeight: { xs: "1.2", sm: "1.2", md: "1.1" },
|
||||||
|
wordWrap: "break-word",
|
||||||
|
hyphens: "auto"
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
</Typography>
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Navigation container for portrait phones */}
|
||||||
|
<Box sx={{
|
||||||
|
display: { xs: "flex", sm: "contents" },
|
||||||
|
width: { xs: "100%", sm: "auto" },
|
||||||
|
justifyContent: { xs: "space-between", sm: "initial" },
|
||||||
|
alignItems: "center"
|
||||||
|
}}>
|
||||||
|
{/* Left Navigation - Layered rendering */}
|
||||||
|
<Box sx={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
flexShrink: 0,
|
||||||
|
justifyContent: "flex-start",
|
||||||
|
position: "relative",
|
||||||
|
mr: { xs: 0, sm: 2 }
|
||||||
|
}}>
|
||||||
|
{navTexts.map((navItem, index) => {
|
||||||
|
const isActive = navConfig.leftNav && navConfig.leftNav.text === navItem.text;
|
||||||
|
const link = navItem.link;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
key={navItem.key}
|
||||||
|
component={Link}
|
||||||
|
to={link}
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
textDecoration: "none",
|
||||||
|
color: "inherit",
|
||||||
|
transition: "all 0.3s ease",
|
||||||
|
opacity: isActive ? 1 : 0,
|
||||||
|
position: index === 0 ? "relative" : "absolute",
|
||||||
|
left: index !== 0 ? 0 : "auto",
|
||||||
|
pointerEvents: isActive ? "auto" : "none",
|
||||||
|
"&:hover": {
|
||||||
|
transform: "translateX(-5px)",
|
||||||
|
color: "primary.main"
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ChevronLeft sx={{ fontSize: "2rem", mr: 1 }} />
|
||||||
|
<Typography
|
||||||
|
sx={{
|
||||||
|
fontFamily: "SwashingtonCP",
|
||||||
|
fontSize: { xs: "1.25rem", sm: "1.25rem", md: "2.125rem" },
|
||||||
|
textShadow: "2px 2px 4px rgba(0, 0, 0, 0.3)",
|
||||||
|
lineHeight: { xs: "1.2", sm: "1.2", md: "1.1" },
|
||||||
|
whiteSpace: "nowrap"
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{navItem.text}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Center Title - Layered rendering - Hidden on portrait phones, shown on larger screens */}
|
||||||
|
<Box sx={{
|
||||||
|
flex: 1,
|
||||||
|
display: { xs: "none", sm: "flex" },
|
||||||
|
justifyContent: "center",
|
||||||
|
alignItems: "center",
|
||||||
|
px: 0,
|
||||||
|
position: "relative",
|
||||||
|
minWidth: 0
|
||||||
|
}}>
|
||||||
|
{Object.entries(allTitles).map(([pageType, title]) => (
|
||||||
|
<Typography
|
||||||
|
key={pageType}
|
||||||
|
variant="h3"
|
||||||
|
component="h1"
|
||||||
|
sx={{
|
||||||
|
fontFamily: "SwashingtonCP",
|
||||||
|
fontSize: { xs: "2.125rem", sm: "2.125rem", md: "3rem" },
|
||||||
|
textAlign: "center",
|
||||||
|
color: "primary.main",
|
||||||
|
textShadow: "3px 3px 10px rgba(0, 0, 0, 0.4)",
|
||||||
|
transition: "opacity 0.5s ease-in-out",
|
||||||
|
opacity: getOpacity(pageType),
|
||||||
|
position: pageType === "home" ? "relative" : "absolute",
|
||||||
|
top: pageType !== "home" ? "50%" : "auto",
|
||||||
|
left: pageType !== "home" ? "50%" : "auto",
|
||||||
|
transform: pageType !== "home" ? "translate(-50%, -50%)" : "none",
|
||||||
|
width: "100%",
|
||||||
|
pointerEvents: getOpacity(pageType) === 1 ? "auto" : "none",
|
||||||
|
lineHeight: { xs: "1.2", sm: "1.2", md: "1.1" },
|
||||||
|
wordWrap: "break-word",
|
||||||
|
hyphens: "auto"
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
</Typography>
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Right Navigation - Layered rendering */}
|
||||||
|
<Box sx={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
flexShrink: 0,
|
||||||
|
justifyContent: "flex-end",
|
||||||
|
position: "relative",
|
||||||
|
ml: { xs: 0, sm: 2 }
|
||||||
|
}}>
|
||||||
|
{navTexts.map((navItem, index) => {
|
||||||
|
const isActive = navConfig.rightNav && navConfig.rightNav.text === navItem.text;
|
||||||
|
const link = navItem.link;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
key={navItem.key}
|
||||||
|
component={Link}
|
||||||
|
to={link}
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
textDecoration: "none",
|
||||||
|
color: "inherit",
|
||||||
|
transition: "all 0.3s ease",
|
||||||
|
opacity: isActive ? 1 : 0,
|
||||||
|
position: index === 0 ? "relative" : "absolute",
|
||||||
|
right: index !== 0 ? 0 : "auto",
|
||||||
|
pointerEvents: isActive ? "auto" : "none",
|
||||||
|
"&:hover": {
|
||||||
|
transform: "translateX(5px)",
|
||||||
|
color: "primary.main"
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography
|
||||||
|
sx={{
|
||||||
|
fontFamily: "SwashingtonCP",
|
||||||
|
fontSize: { xs: "1.25rem", sm: "1.25rem", md: "2.125rem" },
|
||||||
|
textShadow: "2px 2px 4px rgba(0, 0, 0, 0.3)",
|
||||||
|
lineHeight: { xs: "1.2", sm: "1.2", md: "1.1" },
|
||||||
|
whiteSpace: "nowrap"
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{navItem.text}
|
||||||
|
</Typography>
|
||||||
|
<ChevronRight sx={{ fontSize: "2rem", ml: 1 }} />
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Content Boxes - Layered rendering */}
|
||||||
|
<Box sx={{ position: "relative", mb: 4 }}>
|
||||||
|
{Object.entries(allContentBoxes).map(([pageType, contentBoxes]) => (
|
||||||
|
<Grid
|
||||||
|
key={pageType}
|
||||||
|
container
|
||||||
|
spacing={0}
|
||||||
|
sx={{
|
||||||
|
transition: "opacity 0.5s ease-in-out",
|
||||||
|
opacity: getOpacity(pageType),
|
||||||
|
position: pageType === "home" ? "relative" : "absolute",
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
width: "100%",
|
||||||
|
pointerEvents: getOpacity(pageType) === 1 ? "auto" : "none"
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{contentBoxes.map((box, index) => (
|
||||||
|
<Grid key={`${pageType}-${index}`} item xs={12} sm={6} sx={{ p: 2, width: "50%", position: 'relative' }}>
|
||||||
|
{/* Multi-pointed star for seeds box - moved to Grid level */}
|
||||||
|
{index === 0 && pageType === "home" && (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
top: '-45px',
|
||||||
|
left: '-45px',
|
||||||
|
width: '180px',
|
||||||
|
height: '180px',
|
||||||
|
zIndex: 999,
|
||||||
|
pointerEvents: 'auto',
|
||||||
|
cursor: 'pointer'
|
||||||
|
}}
|
||||||
|
onMouseEnter={() => setStarHovered(true)}
|
||||||
|
onMouseLeave={() => setStarHovered(false)}
|
||||||
|
>
|
||||||
|
{/* Background star - slightly larger and rotated */}
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 60 60"
|
||||||
|
width="168"
|
||||||
|
height="168"
|
||||||
|
className="star-rotate-slow-cw"
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
top: '-9px',
|
||||||
|
left: '-9px',
|
||||||
|
transform: 'rotate(20deg)'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<polygon
|
||||||
|
points="30,0 38,20 60,22 43,37 48,60 30,48 12,60 17,37 0,22 22,20"
|
||||||
|
fill="#B8860B"
|
||||||
|
stroke="none"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
{/* Middle star - medium size with different rotation */}
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 60 60"
|
||||||
|
width="159"
|
||||||
|
height="159"
|
||||||
|
className="star-rotate-slow-ccw"
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
top: '-4.5px',
|
||||||
|
left: '-4.5px',
|
||||||
|
transform: 'rotate(-25deg)'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<polygon
|
||||||
|
points="30,0 38,20 60,22 43,37 48,60 30,48 12,60 17,37 0,22 22,20"
|
||||||
|
fill="#DAA520"
|
||||||
|
stroke="none"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
{/* Foreground star - main star with text */}
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 60 60"
|
||||||
|
width="150"
|
||||||
|
height="150"
|
||||||
|
className="star-rotate-medium-cw"
|
||||||
|
>
|
||||||
|
<polygon
|
||||||
|
points="30,0 38,20 60,22 43,37 48,60 30,48 12,60 17,37 0,22 22,20"
|
||||||
|
fill="#FFD700"
|
||||||
|
stroke="none"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
{/* Text positioned in the center of the star */}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
top: '45%',
|
||||||
|
left: '43%',
|
||||||
|
transform: 'translate(-50%, -50%) rotate(-10deg)',
|
||||||
|
color: 'white',
|
||||||
|
fontWeight: '900',
|
||||||
|
fontSize: '20px',
|
||||||
|
textShadow: '0px 3px 6px rgba(0,0,0,0.5)',
|
||||||
|
zIndex: 1000,
|
||||||
|
textAlign: 'center',
|
||||||
|
lineHeight: '1.1',
|
||||||
|
width: '135px',
|
||||||
|
transition: 'opacity 0.3s ease',
|
||||||
|
opacity: starHovered ? 0 : 1
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t('sections.showUsPhoto')}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Hover text */}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
top: '45%',
|
||||||
|
left: '43%',
|
||||||
|
transform: 'translate(-50%, -50%) rotate(-10deg)',
|
||||||
|
color: 'white',
|
||||||
|
fontWeight: '900',
|
||||||
|
fontSize: '20px',
|
||||||
|
textShadow: '0px 3px 6px rgba(0,0,0,0.5)',
|
||||||
|
zIndex: 1000,
|
||||||
|
textAlign: 'center',
|
||||||
|
lineHeight: '1.1',
|
||||||
|
width: '135px',
|
||||||
|
opacity: starHovered ? 1 : 0,
|
||||||
|
transition: 'opacity 0.3s ease'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t('sections.selectSeedRate')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Multi-pointed star for stecklinge box - bottom right */}
|
||||||
|
{index === 1 && pageType === "home" && (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
bottom: '-65px',
|
||||||
|
right: '-65px',
|
||||||
|
width: '180px',
|
||||||
|
height: '180px',
|
||||||
|
zIndex: 999,
|
||||||
|
pointerEvents: 'auto',
|
||||||
|
cursor: 'pointer'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* Background star - slightly larger and rotated */}
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 60 60"
|
||||||
|
width="168"
|
||||||
|
height="168"
|
||||||
|
className="star-rotate-slow-ccw"
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
top: '-9px',
|
||||||
|
left: '-9px',
|
||||||
|
transform: 'rotate(20deg)'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<polygon
|
||||||
|
points="30,0 38,20 60,22 43,37 48,60 30,48 12,60 17,37 0,22 22,20"
|
||||||
|
fill="#5F9EA0"
|
||||||
|
stroke="none"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
{/* Middle star - medium size with different rotation */}
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 60 60"
|
||||||
|
width="159"
|
||||||
|
height="159"
|
||||||
|
className="star-rotate-medium-cw"
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
top: '-4.5px',
|
||||||
|
left: '-4.5px',
|
||||||
|
transform: 'rotate(-25deg)'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<polygon
|
||||||
|
points="30,0 38,20 60,22 43,37 48,60 30,48 12,60 17,37 0,22 22,20"
|
||||||
|
fill="#7FCDCD"
|
||||||
|
stroke="none"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
{/* Foreground star - main star with text */}
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 60 60"
|
||||||
|
width="150"
|
||||||
|
height="150"
|
||||||
|
className="star-rotate-slow-cw"
|
||||||
|
>
|
||||||
|
<polygon
|
||||||
|
points="30,0 38,20 60,22 43,37 48,60 30,48 12,60 17,37 0,22 22,20"
|
||||||
|
fill="#AFEEEE"
|
||||||
|
stroke="none"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
{/* Text positioned in the center of the star */}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
top: '42%',
|
||||||
|
left: '45%',
|
||||||
|
transform: 'translate(-50%, -50%) rotate(10deg)',
|
||||||
|
color: 'white',
|
||||||
|
fontWeight: '900',
|
||||||
|
fontSize: '20px',
|
||||||
|
textShadow: '0px 3px 6px rgba(0,0,0,0.5)',
|
||||||
|
zIndex: 1000,
|
||||||
|
textAlign: 'center',
|
||||||
|
lineHeight: '1.1',
|
||||||
|
width: '135px'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t('sections.indoorSeason')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className={`animated-border-card ${index === 0 ? 'seeds-card' : 'cutlings-card'}`}>
|
||||||
|
<Paper
|
||||||
|
component={Link}
|
||||||
|
to={box.link}
|
||||||
|
sx={{
|
||||||
|
p: 0,
|
||||||
|
textDecoration: "none",
|
||||||
|
color: "text.primary",
|
||||||
|
borderRadius: 2,
|
||||||
|
overflow: "hidden",
|
||||||
|
height: { xs: 150, sm: 200, md: 300 },
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
boxShadow: 10,
|
||||||
|
transition: "all 0.3s ease",
|
||||||
|
"&:hover": {
|
||||||
|
transform: "translateY(-5px)",
|
||||||
|
boxShadow: 20,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
height: "100%",
|
||||||
|
bgcolor: box.bgcolor,
|
||||||
|
backgroundImage: `url("${box.image}")`,
|
||||||
|
backgroundSize: "contain",
|
||||||
|
backgroundPosition: "center",
|
||||||
|
backgroundRepeat: "no-repeat",
|
||||||
|
position: "relative",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
position: "absolute",
|
||||||
|
bottom: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
bgcolor: "rgba(27, 94, 32, 0.8)",
|
||||||
|
p: 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography
|
||||||
|
sx={{
|
||||||
|
fontSize: "1.6rem",
|
||||||
|
color: "white",
|
||||||
|
fontFamily: "SwashingtonCP",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{box.title}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Paper>
|
||||||
|
</div>
|
||||||
|
</Grid>
|
||||||
|
))}
|
||||||
|
</Grid>
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Shared Carousel */}
|
||||||
|
<SharedCarousel />
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MainPageLayout;
|
||||||
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 based on payment success: orders for successful payments, cart for failed payments
|
||||||
|
profileUrl.hash = response.payment.isPaid ? '#orders' : '#cart';
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
redirectUrl: profileUrl.pathname + profileUrl.search + profileUrl.hash,
|
||||||
|
processing: false
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.error('Failed to check payment status:', response.error);
|
||||||
|
this.setState({
|
||||||
|
redirectUrl: '/profile#cart',
|
||||||
|
processing: false,
|
||||||
|
error: response.error || 'Payment verification failed'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { redirectUrl, processing, error } = this.state;
|
||||||
|
|
||||||
|
if (processing) {
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
minHeight: '60vh',
|
||||||
|
gap: 2
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CircularProgress size={60} />
|
||||||
|
<Typography variant="h6" color="text.secondary">
|
||||||
|
Zahlung wird überprüft...
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" color="text.secondary">
|
||||||
|
Bitte warten Sie, während wir Ihre Zahlung bei Mollie überprüfen.
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return <Navigate to="/profile#cart" replace />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (redirectUrl) {
|
||||||
|
return <Navigate to={redirectUrl} replace />;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback redirect to profile
|
||||||
|
return <Navigate to="/profile#cart" replace />;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PaymentSuccess;
|
||||||
286
src/components/PhotoUpload.js
Normal file
286
src/components/PhotoUpload.js
Normal file
@@ -0,0 +1,286 @@
|
|||||||
|
import React, { Component } from 'react';
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
Typography,
|
||||||
|
IconButton,
|
||||||
|
Paper,
|
||||||
|
Grid,
|
||||||
|
Alert
|
||||||
|
} from '@mui/material';
|
||||||
|
import {
|
||||||
|
Delete,
|
||||||
|
CloudUpload
|
||||||
|
} from '@mui/icons-material';
|
||||||
|
|
||||||
|
class PhotoUpload extends Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
files: [],
|
||||||
|
previews: [],
|
||||||
|
error: null
|
||||||
|
};
|
||||||
|
this.fileInputRef = React.createRef();
|
||||||
|
}
|
||||||
|
|
||||||
|
handleFileSelect = (event) => {
|
||||||
|
const selectedFiles = Array.from(event.target.files);
|
||||||
|
const maxFiles = this.props.maxFiles || 5;
|
||||||
|
const maxSize = this.props.maxSize || 50 * 1024 * 1024; // 50MB default - will be compressed
|
||||||
|
|
||||||
|
// Validate file count
|
||||||
|
if (this.state.files.length + selectedFiles.length > maxFiles) {
|
||||||
|
this.setState({
|
||||||
|
error: `Maximal ${maxFiles} Dateien erlaubt`
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate file types and sizes
|
||||||
|
const validTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
|
||||||
|
const validFiles = [];
|
||||||
|
const newPreviews = [];
|
||||||
|
|
||||||
|
for (const file of selectedFiles) {
|
||||||
|
if (!validTypes.includes(file.type)) {
|
||||||
|
this.setState({
|
||||||
|
error: 'Nur Bilddateien (JPEG, PNG, GIF, WebP) sind erlaubt'
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (file.size > maxSize) {
|
||||||
|
this.setState({
|
||||||
|
error: `Datei zu groß. Maximum: ${Math.round(maxSize / (1024 * 1024))}MB`
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
validFiles.push(file);
|
||||||
|
|
||||||
|
// Create preview and compress image
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = (e) => {
|
||||||
|
// Compress the image
|
||||||
|
this.compressImage(e.target.result, file.name, (compressedFile) => {
|
||||||
|
newPreviews.push({
|
||||||
|
file: compressedFile,
|
||||||
|
preview: e.target.result,
|
||||||
|
name: file.name,
|
||||||
|
originalSize: file.size,
|
||||||
|
compressedSize: compressedFile.size
|
||||||
|
});
|
||||||
|
|
||||||
|
if (newPreviews.length === validFiles.length) {
|
||||||
|
const compressedFiles = newPreviews.map(p => p.file);
|
||||||
|
this.setState(prevState => ({
|
||||||
|
files: [...prevState.files, ...compressedFiles],
|
||||||
|
previews: [...prevState.previews, ...newPreviews],
|
||||||
|
error: null
|
||||||
|
}), () => {
|
||||||
|
// Notify parent component
|
||||||
|
if (this.props.onChange) {
|
||||||
|
this.props.onChange(this.state.files);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset input
|
||||||
|
event.target.value = '';
|
||||||
|
};
|
||||||
|
|
||||||
|
handleRemoveFile = (index) => {
|
||||||
|
this.setState(prevState => {
|
||||||
|
const newFiles = prevState.files.filter((_, i) => i !== index);
|
||||||
|
const newPreviews = prevState.previews.filter((_, i) => i !== index);
|
||||||
|
|
||||||
|
// Notify parent component
|
||||||
|
if (this.props.onChange) {
|
||||||
|
this.props.onChange(newFiles);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
files: newFiles,
|
||||||
|
previews: newPreviews
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
compressImage = (dataURL, fileName, callback) => {
|
||||||
|
const canvas = document.createElement('canvas');
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
const img = new Image();
|
||||||
|
|
||||||
|
img.onload = () => {
|
||||||
|
// Calculate new dimensions (max 1920x1080 for submission)
|
||||||
|
const maxWidth = 1920;
|
||||||
|
const maxHeight = 1080;
|
||||||
|
let { width, height } = img;
|
||||||
|
|
||||||
|
if (width > height) {
|
||||||
|
if (width > maxWidth) {
|
||||||
|
height = (height * maxWidth) / width;
|
||||||
|
width = maxWidth;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (height > maxHeight) {
|
||||||
|
width = (width * maxHeight) / height;
|
||||||
|
height = maxHeight;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
canvas.width = width;
|
||||||
|
canvas.height = height;
|
||||||
|
|
||||||
|
// Draw and compress
|
||||||
|
ctx.drawImage(img, 0, 0, width, height);
|
||||||
|
|
||||||
|
// Convert to blob with compression
|
||||||
|
canvas.toBlob((blob) => {
|
||||||
|
const compressedFile = new File([blob], fileName, {
|
||||||
|
type: 'image/jpeg',
|
||||||
|
lastModified: Date.now()
|
||||||
|
});
|
||||||
|
callback(compressedFile);
|
||||||
|
}, 'image/jpeg', 0.8); // 80% quality
|
||||||
|
};
|
||||||
|
|
||||||
|
img.src = dataURL;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Method to reset the component
|
||||||
|
reset = () => {
|
||||||
|
this.setState({
|
||||||
|
files: [],
|
||||||
|
previews: [],
|
||||||
|
error: null
|
||||||
|
});
|
||||||
|
|
||||||
|
// Also reset the file input
|
||||||
|
if (this.fileInputRef.current) {
|
||||||
|
this.fileInputRef.current.value = '';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { files, previews, error } = this.state;
|
||||||
|
const { disabled, label } = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
<Typography variant="body2" sx={{ mb: 1, fontWeight: 500 }}>
|
||||||
|
{label || 'Fotos anhängen (optional)'}
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<input
|
||||||
|
ref={this.fileInputRef}
|
||||||
|
type="file"
|
||||||
|
multiple
|
||||||
|
accept="image/*"
|
||||||
|
style={{ display: 'none' }}
|
||||||
|
onChange={this.handleFileSelect}
|
||||||
|
disabled={disabled}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="outlined"
|
||||||
|
startIcon={<CloudUpload />}
|
||||||
|
onClick={() => this.fileInputRef.current?.click()}
|
||||||
|
disabled={disabled}
|
||||||
|
sx={{ mb: 2 }}
|
||||||
|
>
|
||||||
|
Fotos auswählen
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<Alert severity="error" sx={{ mb: 2 }}>
|
||||||
|
{error}
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{previews.length > 0 && (
|
||||||
|
<Grid container spacing={2}>
|
||||||
|
{previews.map((preview, index) => (
|
||||||
|
<Grid item xs={6} sm={4} md={3} key={index}>
|
||||||
|
<Paper
|
||||||
|
sx={{
|
||||||
|
position: 'relative',
|
||||||
|
p: 1,
|
||||||
|
borderRadius: 1,
|
||||||
|
overflow: 'hidden'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
component="img"
|
||||||
|
src={preview.preview}
|
||||||
|
alt={preview.name}
|
||||||
|
sx={{
|
||||||
|
width: '100%',
|
||||||
|
height: '100px',
|
||||||
|
objectFit: 'cover',
|
||||||
|
borderRadius: 1
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<IconButton
|
||||||
|
size="small"
|
||||||
|
onClick={() => this.handleRemoveFile(index)}
|
||||||
|
disabled={disabled}
|
||||||
|
sx={{
|
||||||
|
position: 'absolute',
|
||||||
|
top: 4,
|
||||||
|
right: 4,
|
||||||
|
backgroundColor: 'rgba(0,0,0,0.7)',
|
||||||
|
color: 'white',
|
||||||
|
'&:hover': {
|
||||||
|
backgroundColor: 'rgba(0,0,0,0.9)'
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Delete fontSize="small" />
|
||||||
|
</IconButton>
|
||||||
|
<Typography
|
||||||
|
variant="caption"
|
||||||
|
sx={{
|
||||||
|
position: 'absolute',
|
||||||
|
bottom: 4,
|
||||||
|
left: 4,
|
||||||
|
right: 4,
|
||||||
|
backgroundColor: 'rgba(0,0,0,0.7)',
|
||||||
|
color: 'white',
|
||||||
|
p: 0.5,
|
||||||
|
borderRadius: 0.5,
|
||||||
|
fontSize: '0.7rem',
|
||||||
|
overflow: 'hidden',
|
||||||
|
textOverflow: 'ellipsis',
|
||||||
|
whiteSpace: 'nowrap'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{preview.name}
|
||||||
|
</Typography>
|
||||||
|
</Paper>
|
||||||
|
</Grid>
|
||||||
|
))}
|
||||||
|
</Grid>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{files.length > 0 && (
|
||||||
|
<Typography variant="caption" color="text.secondary" sx={{ mt: 1, display: 'block' }}>
|
||||||
|
{files.length} Datei(en) ausgewählt
|
||||||
|
{previews.length > 0 && previews.some(p => p.originalSize && p.compressedSize) && (
|
||||||
|
<span style={{ marginLeft: '8px' }}>
|
||||||
|
(komprimiert für Upload)
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PhotoUpload;
|
||||||
@@ -8,6 +8,7 @@ import CircularProgress from '@mui/material/CircularProgress';
|
|||||||
import IconButton from '@mui/material/IconButton';
|
import IconButton from '@mui/material/IconButton';
|
||||||
import AddToCartButton from './AddToCartButton.js';
|
import AddToCartButton from './AddToCartButton.js';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
|
import { withI18n } from '../i18n/withTranslation.js';
|
||||||
import ZoomInIcon from '@mui/icons-material/ZoomIn';
|
import ZoomInIcon from '@mui/icons-material/ZoomIn';
|
||||||
|
|
||||||
class Product extends Component {
|
class Product extends Component {
|
||||||
@@ -68,8 +69,8 @@ class Product extends Component {
|
|||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
id, name, price, available, manufacturer, seoName,
|
id, name, price, available, manufacturer, seoName,
|
||||||
currency, vat, massMenge, massEinheit, thc,
|
currency, vat, cGrundEinheit, fGrundPreis, thc,
|
||||||
floweringWeeks,incoming, neu, weight, versandklasse, availableSupplier
|
floweringWeeks,incoming, neu, weight, versandklasse, availableSupplier, komponenten
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
const isNew = neu && (new Date().getTime() - new Date(neu).getTime() < 30 * 24 * 60 * 60 * 1000);
|
const isNew = neu && (new Date().getTime() - new Date(neu).getTime() < 30 * 24 * 60 * 60 * 1000);
|
||||||
@@ -173,7 +174,7 @@ class Product extends Component {
|
|||||||
zIndex: 1000
|
zIndex: 1000
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
NEU
|
{this.props.t ? this.props.t('product.new') : 'NEU'}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -240,7 +241,7 @@ class Product extends Component {
|
|||||||
transformOrigin: 'top left'
|
transformOrigin: 'top left'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{floweringWeeks} Wochen
|
{floweringWeeks} {this.props.t ? this.props.t('product.weeks') : 'Wochen'}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -336,13 +337,13 @@ class Product extends Component {
|
|||||||
sx={{ mt: 'auto', fontWeight: 'bold', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}
|
sx={{ mt: 'auto', fontWeight: 'bold', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}
|
||||||
>
|
>
|
||||||
<span>{new Intl.NumberFormat('de-DE', {style: 'currency', currency: currency || 'EUR'}).format(price)}</span>
|
<span>{new Intl.NumberFormat('de-DE', {style: 'currency', currency: currency || 'EUR'}).format(price)}</span>
|
||||||
<small style={{ color: '#77aa77', fontSize: '0.6em' }}>(incl. {vat}% USt.,*)</small>
|
<small style={{ color: '#77aa77', fontSize: '0.6em' }}>({this.props.t ? this.props.t('product.inclVatFooter', { vat }) : `incl. ${vat}% USt.,*`})</small>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</Typography>
|
</Typography>
|
||||||
{massMenge != 1 && massEinheit && (<Typography variant="body2" color="text.secondary" sx={{ m: 0,p: 0 }}>
|
{cGrundEinheit && fGrundPreis && fGrundPreis != price && (<Typography variant="body2" color="text.secondary" sx={{ m: 0,p: 0 }}>
|
||||||
({new Intl.NumberFormat('de-DE', {style: 'currency', currency: currency || 'EUR'}).format(price/massMenge)}/{massEinheit})
|
({new Intl.NumberFormat('de-DE', {style: 'currency', currency: currency || 'EUR'}).format(fGrundPreis)}/{cGrundEinheit})
|
||||||
</Typography> )}
|
</Typography> )}
|
||||||
</div>
|
</div>
|
||||||
{/*incoming*/}
|
{/*incoming*/}
|
||||||
@@ -358,7 +359,7 @@ class Product extends Component {
|
|||||||
>
|
>
|
||||||
<ZoomInIcon />
|
<ZoomInIcon />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
<AddToCartButton cartButton={true} availableSupplier={availableSupplier} available={available} incoming={incoming} seoName={seoName} pictureList={this.props.pictureList} id={id} price={price} vat={vat} weight={weight} name={name} versandklasse={versandklasse}/>
|
<AddToCartButton cartButton={true} availableSupplier={availableSupplier} komponenten={komponenten} cGrundEinheit={cGrundEinheit} fGrundPreis={fGrundPreis} available={available} incoming={incoming} seoName={seoName} pictureList={this.props.pictureList} id={id} price={price} vat={vat} weight={weight} name={name} versandklasse={versandklasse}/>
|
||||||
</Box>
|
</Box>
|
||||||
</Card>
|
</Card>
|
||||||
</Box>
|
</Box>
|
||||||
@@ -366,4 +367,4 @@ class Product extends Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Product;
|
export default withI18n()(Product);
|
||||||
|
|||||||
@@ -1,9 +1,13 @@
|
|||||||
import React, { Component } from "react";
|
import React, { Component } from "react";
|
||||||
import { Box, Typography, CardMedia, Stack, Chip } from "@mui/material";
|
import { Box, Typography, CardMedia, Stack, Chip, Button, Collapse } from "@mui/material";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import parse from "html-react-parser";
|
import parse from "html-react-parser";
|
||||||
import AddToCartButton from "./AddToCartButton.js";
|
import AddToCartButton from "./AddToCartButton.js";
|
||||||
import Images from "./Images.js";
|
import Images from "./Images.js";
|
||||||
|
import { withI18n } from "../i18n/withTranslation.js";
|
||||||
|
import ArticleQuestionForm from "./ArticleQuestionForm.js";
|
||||||
|
import ArticleRatingForm from "./ArticleRatingForm.js";
|
||||||
|
import ArticleAvailabilityForm from "./ArticleAvailabilityForm.js";
|
||||||
|
|
||||||
// Utility function to clean product names by removing trailing number in parentheses
|
// Utility function to clean product names by removing trailing number in parentheses
|
||||||
const cleanProductName = (name) => {
|
const cleanProductName = (name) => {
|
||||||
@@ -29,6 +33,16 @@ class ProductDetailPage extends Component {
|
|||||||
attributes: [],
|
attributes: [],
|
||||||
isSteckling: false,
|
isSteckling: false,
|
||||||
imageDialogOpen: false,
|
imageDialogOpen: false,
|
||||||
|
komponenten: [],
|
||||||
|
komponentenLoaded: false,
|
||||||
|
komponentenData: {}, // Store individual komponent data with loading states
|
||||||
|
komponentenImages: {}, // Store tiny pictures for komponenten
|
||||||
|
totalKomponentenPrice: 0,
|
||||||
|
totalSavings: 0,
|
||||||
|
// Collapsible sections state
|
||||||
|
showQuestionForm: false,
|
||||||
|
showRatingForm: false,
|
||||||
|
showAvailabilityForm: false
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
this.state = {
|
this.state = {
|
||||||
@@ -39,6 +53,16 @@ class ProductDetailPage extends Component {
|
|||||||
attributes: [],
|
attributes: [],
|
||||||
isSteckling: false,
|
isSteckling: false,
|
||||||
imageDialogOpen: false,
|
imageDialogOpen: false,
|
||||||
|
komponenten: [],
|
||||||
|
komponentenLoaded: false,
|
||||||
|
komponentenData: {}, // Store individual komponent data with loading states
|
||||||
|
komponentenImages: {}, // Store tiny pictures for komponenten
|
||||||
|
totalKomponentenPrice: 0,
|
||||||
|
totalSavings: 0,
|
||||||
|
// Collapsible sections state
|
||||||
|
showQuestionForm: false,
|
||||||
|
showRatingForm: false,
|
||||||
|
showAvailabilityForm: false
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -64,6 +88,248 @@ class ProductDetailPage extends Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
loadKomponentImage = (komponentId, pictureList) => {
|
||||||
|
// Initialize cache if it doesn't exist
|
||||||
|
if (!window.smallPicCache) {
|
||||||
|
window.smallPicCache = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip if no pictureList
|
||||||
|
if (!pictureList || pictureList.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the first image ID from pictureList
|
||||||
|
const bildId = pictureList.split(',')[0];
|
||||||
|
|
||||||
|
// Check if already cached
|
||||||
|
if (window.smallPicCache[bildId]) {
|
||||||
|
this.setState(prevState => ({
|
||||||
|
komponentenImages: {
|
||||||
|
...prevState.komponentenImages,
|
||||||
|
[komponentId]: window.smallPicCache[bildId]
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if socketB is available
|
||||||
|
if (!this.props.socketB || !this.props.socketB.connected) {
|
||||||
|
console.log("SocketB not connected yet, skipping image load for komponent:", komponentId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch image from server
|
||||||
|
this.props.socketB.emit('getPic', { bildId, size: 'small' }, (res) => {
|
||||||
|
if (res.success) {
|
||||||
|
// Cache the image
|
||||||
|
window.smallPicCache[bildId] = URL.createObjectURL(new Blob([res.imageBuffer], { type: 'image/jpeg' }));
|
||||||
|
|
||||||
|
// Update state
|
||||||
|
this.setState(prevState => ({
|
||||||
|
komponentenImages: {
|
||||||
|
...prevState.komponentenImages,
|
||||||
|
[komponentId]: window.smallPicCache[bildId]
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
} else {
|
||||||
|
console.log('Error loading komponent image:', res);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
loadKomponent = (id, count) => {
|
||||||
|
// Initialize cache if it doesn't exist
|
||||||
|
if (!window.productDetailCache) {
|
||||||
|
window.productDetailCache = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if this komponent is already cached
|
||||||
|
if (window.productDetailCache[id]) {
|
||||||
|
const cachedProduct = window.productDetailCache[id];
|
||||||
|
|
||||||
|
// Load komponent image if available
|
||||||
|
if (cachedProduct.pictureList) {
|
||||||
|
this.loadKomponentImage(id, cachedProduct.pictureList);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update state with cached data
|
||||||
|
this.setState(prevState => {
|
||||||
|
const newKomponentenData = {
|
||||||
|
...prevState.komponentenData,
|
||||||
|
[id]: {
|
||||||
|
...cachedProduct,
|
||||||
|
count: parseInt(count),
|
||||||
|
loaded: true
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check if all remaining komponenten are loaded
|
||||||
|
const allLoaded = prevState.komponenten.every(k =>
|
||||||
|
newKomponentenData[k.id] && newKomponentenData[k.id].loaded
|
||||||
|
);
|
||||||
|
|
||||||
|
// Calculate totals if all loaded
|
||||||
|
let totalKomponentenPrice = 0;
|
||||||
|
let totalSavings = 0;
|
||||||
|
|
||||||
|
if (allLoaded) {
|
||||||
|
totalKomponentenPrice = prevState.komponenten.reduce((sum, k) => {
|
||||||
|
const komponentData = newKomponentenData[k.id];
|
||||||
|
if (komponentData && komponentData.loaded) {
|
||||||
|
return sum + (komponentData.price * parseInt(k.count));
|
||||||
|
}
|
||||||
|
return sum;
|
||||||
|
}, 0);
|
||||||
|
|
||||||
|
// Calculate savings (difference between buying individually vs as set)
|
||||||
|
const setPrice = prevState.product ? prevState.product.price : 0;
|
||||||
|
totalSavings = Math.max(0, totalKomponentenPrice - setPrice);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("Cached komponent loaded:", id, "data:", newKomponentenData[id]);
|
||||||
|
console.log("All loaded (cached):", allLoaded);
|
||||||
|
|
||||||
|
return {
|
||||||
|
komponentenData: newKomponentenData,
|
||||||
|
komponentenLoaded: allLoaded,
|
||||||
|
totalKomponentenPrice,
|
||||||
|
totalSavings
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If not cached, fetch from server (similar to loadProductData)
|
||||||
|
if (!this.props.socket || !this.props.socket.connected) {
|
||||||
|
console.log("Socket not connected yet, waiting for connection to load komponent data");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mark this komponent as loading
|
||||||
|
this.setState(prevState => ({
|
||||||
|
komponentenData: {
|
||||||
|
...prevState.komponentenData,
|
||||||
|
[id]: {
|
||||||
|
...prevState.komponentenData[id],
|
||||||
|
loading: true,
|
||||||
|
loaded: false,
|
||||||
|
count: parseInt(count)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
this.props.socket.emit(
|
||||||
|
"getProductView",
|
||||||
|
{ articleId: id },
|
||||||
|
(res) => {
|
||||||
|
if (res.success) {
|
||||||
|
// Cache the successful response
|
||||||
|
window.productDetailCache[id] = res.product;
|
||||||
|
|
||||||
|
// Load komponent image if available
|
||||||
|
if (res.product.pictureList) {
|
||||||
|
this.loadKomponentImage(id, res.product.pictureList);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update state with loaded data
|
||||||
|
this.setState(prevState => {
|
||||||
|
const newKomponentenData = {
|
||||||
|
...prevState.komponentenData,
|
||||||
|
[id]: {
|
||||||
|
...res.product,
|
||||||
|
count: parseInt(count),
|
||||||
|
loading: false,
|
||||||
|
loaded: true
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check if all remaining komponenten are loaded
|
||||||
|
const allLoaded = prevState.komponenten.every(k =>
|
||||||
|
newKomponentenData[k.id] && newKomponentenData[k.id].loaded
|
||||||
|
);
|
||||||
|
|
||||||
|
// Calculate totals if all loaded
|
||||||
|
let totalKomponentenPrice = 0;
|
||||||
|
let totalSavings = 0;
|
||||||
|
|
||||||
|
if (allLoaded) {
|
||||||
|
totalKomponentenPrice = prevState.komponenten.reduce((sum, k) => {
|
||||||
|
const komponentData = newKomponentenData[k.id];
|
||||||
|
if (komponentData && komponentData.loaded) {
|
||||||
|
return sum + (komponentData.price * parseInt(k.count));
|
||||||
|
}
|
||||||
|
return sum;
|
||||||
|
}, 0);
|
||||||
|
|
||||||
|
// Calculate savings (difference between buying individually vs as set)
|
||||||
|
const setPrice = prevState.product ? prevState.product.price : 0;
|
||||||
|
totalSavings = Math.max(0, totalKomponentenPrice - setPrice);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("Updated komponentenData for", id, ":", newKomponentenData[id]);
|
||||||
|
console.log("All loaded:", allLoaded);
|
||||||
|
|
||||||
|
return {
|
||||||
|
komponentenData: newKomponentenData,
|
||||||
|
komponentenLoaded: allLoaded,
|
||||||
|
totalKomponentenPrice,
|
||||||
|
totalSavings
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("getProductView (komponent)", res);
|
||||||
|
} else {
|
||||||
|
console.error("Error loading komponent:", res.error || "Unknown error", res);
|
||||||
|
|
||||||
|
// Remove failed komponent from the list and check if all remaining are loaded
|
||||||
|
this.setState(prevState => {
|
||||||
|
const newKomponenten = prevState.komponenten.filter(k => k.id !== id);
|
||||||
|
const newKomponentenData = { ...prevState.komponentenData };
|
||||||
|
|
||||||
|
// Remove failed komponent from data
|
||||||
|
delete newKomponentenData[id];
|
||||||
|
|
||||||
|
// Check if all remaining komponenten are loaded
|
||||||
|
const allLoaded = newKomponenten.length === 0 || newKomponenten.every(k =>
|
||||||
|
newKomponentenData[k.id] && newKomponentenData[k.id].loaded
|
||||||
|
);
|
||||||
|
|
||||||
|
// Calculate totals if all loaded
|
||||||
|
let totalKomponentenPrice = 0;
|
||||||
|
let totalSavings = 0;
|
||||||
|
|
||||||
|
if (allLoaded && newKomponenten.length > 0) {
|
||||||
|
totalKomponentenPrice = newKomponenten.reduce((sum, k) => {
|
||||||
|
const komponentData = newKomponentenData[k.id];
|
||||||
|
if (komponentData && komponentData.loaded) {
|
||||||
|
return sum + (komponentData.price * parseInt(k.count));
|
||||||
|
}
|
||||||
|
return sum;
|
||||||
|
}, 0);
|
||||||
|
|
||||||
|
// Calculate savings (difference between buying individually vs as set)
|
||||||
|
const setPrice = this.state.product ? this.state.product.price : 0;
|
||||||
|
totalSavings = Math.max(0, totalKomponentenPrice - setPrice);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("Removed failed komponent:", id, "remaining:", newKomponenten.length);
|
||||||
|
console.log("All loaded after removal:", allLoaded);
|
||||||
|
|
||||||
|
return {
|
||||||
|
komponenten: newKomponenten,
|
||||||
|
komponentenData: newKomponentenData,
|
||||||
|
komponentenLoaded: allLoaded,
|
||||||
|
totalKomponentenPrice,
|
||||||
|
totalSavings
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
loadProductData = () => {
|
loadProductData = () => {
|
||||||
if (!this.props.socket || !this.props.socket.connected) {
|
if (!this.props.socket || !this.props.socket.connected) {
|
||||||
// Socket not connected yet, but don't show error immediately on first load
|
// Socket not connected yet, but don't show error immediately on first load
|
||||||
@@ -78,12 +344,37 @@ class ProductDetailPage extends Component {
|
|||||||
(res) => {
|
(res) => {
|
||||||
if (res.success) {
|
if (res.success) {
|
||||||
res.product.seoName = this.props.seoName;
|
res.product.seoName = this.props.seoName;
|
||||||
|
|
||||||
|
// Initialize cache if it doesn't exist
|
||||||
|
if (!window.productDetailCache) {
|
||||||
|
window.productDetailCache = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cache the product data
|
||||||
|
window.productDetailCache[this.props.seoName] = res.product;
|
||||||
|
|
||||||
|
const komponenten = [];
|
||||||
|
if(res.product.komponenten) {
|
||||||
|
for(const komponent of res.product.komponenten.split(",")) {
|
||||||
|
// Handle both "x" and "×" as separators
|
||||||
|
const [id, count] = komponent.split(/[x×]/);
|
||||||
|
komponenten.push({id: id.trim(), count: count.trim()});
|
||||||
|
}
|
||||||
|
}
|
||||||
this.setState({
|
this.setState({
|
||||||
product: res.product,
|
product: res.product,
|
||||||
loading: false,
|
loading: false,
|
||||||
error: null,
|
error: null,
|
||||||
imageDialogOpen: false,
|
imageDialogOpen: false,
|
||||||
attributes: res.attributes
|
attributes: res.attributes,
|
||||||
|
komponenten: komponenten,
|
||||||
|
komponentenLoaded: komponenten.length === 0 // If no komponenten, mark as loaded
|
||||||
|
}, () => {
|
||||||
|
if(komponenten.length > 0) {
|
||||||
|
for(const komponent of komponenten) {
|
||||||
|
this.loadKomponent(komponent.id, komponent.count);
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
console.log("getProductView", res);
|
console.log("getProductView", res);
|
||||||
|
|
||||||
@@ -179,8 +470,54 @@ class ProductDetailPage extends Component {
|
|||||||
this.setState({ imageDialogOpen: false });
|
this.setState({ imageDialogOpen: false });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
toggleQuestionForm = () => {
|
||||||
|
this.setState(prevState => ({
|
||||||
|
showQuestionForm: !prevState.showQuestionForm,
|
||||||
|
showRatingForm: false,
|
||||||
|
showAvailabilityForm: false
|
||||||
|
}), () => {
|
||||||
|
if (this.state.showQuestionForm) {
|
||||||
|
setTimeout(() => this.scrollToSection('question-form'), 100);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
toggleRatingForm = () => {
|
||||||
|
this.setState(prevState => ({
|
||||||
|
showRatingForm: !prevState.showRatingForm,
|
||||||
|
showQuestionForm: false,
|
||||||
|
showAvailabilityForm: false
|
||||||
|
}), () => {
|
||||||
|
if (this.state.showRatingForm) {
|
||||||
|
setTimeout(() => this.scrollToSection('rating-form'), 100);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
toggleAvailabilityForm = () => {
|
||||||
|
this.setState(prevState => ({
|
||||||
|
showAvailabilityForm: !prevState.showAvailabilityForm,
|
||||||
|
showQuestionForm: false,
|
||||||
|
showRatingForm: false
|
||||||
|
}), () => {
|
||||||
|
if (this.state.showAvailabilityForm) {
|
||||||
|
setTimeout(() => this.scrollToSection('availability-form'), 100);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
scrollToSection = (sectionId) => {
|
||||||
|
const element = document.getElementById(sectionId);
|
||||||
|
if (element) {
|
||||||
|
element.scrollIntoView({
|
||||||
|
behavior: 'smooth',
|
||||||
|
block: 'start'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { product, loading, error, attributeImages, isSteckling, attributes } =
|
const { product, loading, error, attributeImages, isSteckling, attributes, komponentenLoaded, komponentenData, komponentenImages, totalKomponentenPrice, totalSavings } =
|
||||||
this.state;
|
this.state;
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
@@ -211,7 +548,7 @@ class ProductDetailPage extends Component {
|
|||||||
<Typography>{error}</Typography>
|
<Typography>{error}</Typography>
|
||||||
<Link to="/" style={{ textDecoration: "none" }}>
|
<Link to="/" style={{ textDecoration: "none" }}>
|
||||||
<Typography color="primary" sx={{ mt: 2 }}>
|
<Typography color="primary" sx={{ mt: 2 }}>
|
||||||
Zurück zur Startseite
|
{this.props.t ? this.props.t('product.backToHome') : 'Zurück zur Startseite'}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Link>
|
</Link>
|
||||||
</Box>
|
</Box>
|
||||||
@@ -229,7 +566,7 @@ class ProductDetailPage extends Component {
|
|||||||
</Typography>
|
</Typography>
|
||||||
<Link to="/" style={{ textDecoration: "none" }}>
|
<Link to="/" style={{ textDecoration: "none" }}>
|
||||||
<Typography color="primary" sx={{ mt: 2 }}>
|
<Typography color="primary" sx={{ mt: 2 }}>
|
||||||
Zurück zur Startseite
|
{this.props.t ? this.props.t('product.backToHome') : 'Zurück zur Startseite'}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Link>
|
</Link>
|
||||||
</Box>
|
</Box>
|
||||||
@@ -294,7 +631,7 @@ class ProductDetailPage extends Component {
|
|||||||
fontWeight: "bold",
|
fontWeight: "bold",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Zurück
|
{this.props.t ? this.props.t('common.back') : 'Zurück'}
|
||||||
</Link>
|
</Link>
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
@@ -355,7 +692,7 @@ class ProductDetailPage extends Component {
|
|||||||
{/* Product identifiers */}
|
{/* Product identifiers */}
|
||||||
<Box sx={{ mb: 1 }}>
|
<Box sx={{ mb: 1 }}>
|
||||||
<Typography variant="body2" color="text.secondary">
|
<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>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
@@ -373,31 +710,26 @@ class ProductDetailPage extends Component {
|
|||||||
{product.manufacturer && (
|
{product.manufacturer && (
|
||||||
<Box sx={{ display: "flex", alignItems: "center", mb: 2 }}>
|
<Box sx={{ display: "flex", alignItems: "center", mb: 2 }}>
|
||||||
<Typography variant="body2" sx={{ fontStyle: "italic" }}>
|
<Typography variant="body2" sx={{ fontStyle: "italic" }}>
|
||||||
Hersteller: {product.manufacturer}
|
{this.props.t ? this.props.t('product.manufacturer') : 'Hersteller'}: {product.manufacturer}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Attribute images and chips */}
|
{/* Attribute images and chips with action buttons */}
|
||||||
{(attributes.some(attr => attributeImages[attr.kMerkmalWert]) || attributes.some(attr => !attributeImages[attr.kMerkmalWert])) && (
|
{(attributes.some(attr => attributeImages[attr.kMerkmalWert]) || attributes.some(attr => !attributeImages[attr.kMerkmalWert])) && (
|
||||||
<Stack direction="row" spacing={2} sx={{ flexWrap: "wrap", gap: 1, mb: 2 }}>
|
<Box sx={{ display: "flex", justifyContent: "space-between", alignItems: "flex-start", mb: 2, gap: 2 }}>
|
||||||
|
<Stack direction="row" spacing={2} sx={{ flexWrap: "wrap", gap: 1, flex: 1 }}>
|
||||||
{attributes
|
{attributes
|
||||||
.filter(attribute => attributeImages[attribute.kMerkmalWert])
|
.filter(attribute => attributeImages[attribute.kMerkmalWert])
|
||||||
.map((attribute) => {
|
.map((attribute) => {
|
||||||
const key = attribute.kMerkmalWert;
|
const key = attribute.kMerkmalWert;
|
||||||
return (
|
return (
|
||||||
<Box key={key} sx={{ mb: 1 }}>
|
<Box key={key} sx={{ mb: 1,border: "1px solid #e0e0e0", borderRadius: 1 }}>
|
||||||
<CardMedia
|
<CardMedia
|
||||||
component="img"
|
component="img"
|
||||||
|
style={{ width: "72px", height: "98px" }}
|
||||||
image={attributeImages[key]}
|
image={attributeImages[key]}
|
||||||
alt={`Attribute ${key}`}
|
alt={`Attribute ${key}`}
|
||||||
sx={{
|
|
||||||
maxWidth: "100px",
|
|
||||||
maxHeight: "100px",
|
|
||||||
objectFit: "contain",
|
|
||||||
border: "1px solid #e0e0e0",
|
|
||||||
borderRadius: 1,
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
@@ -413,13 +745,68 @@ class ProductDetailPage extends Component {
|
|||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
|
{/* Right-aligned action buttons */}
|
||||||
|
<Stack direction="column" spacing={1} sx={{ flexShrink: 0 }}>
|
||||||
|
<Button
|
||||||
|
variant="outlined"
|
||||||
|
size="small"
|
||||||
|
onClick={this.toggleQuestionForm}
|
||||||
|
sx={{
|
||||||
|
fontSize: "0.75rem",
|
||||||
|
px: 1.5,
|
||||||
|
py: 0.5,
|
||||||
|
minWidth: "auto",
|
||||||
|
whiteSpace: "nowrap"
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Frage zum Artikel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outlined"
|
||||||
|
size="small"
|
||||||
|
onClick={this.toggleRatingForm}
|
||||||
|
sx={{
|
||||||
|
fontSize: "0.75rem",
|
||||||
|
px: 1.5,
|
||||||
|
py: 0.5,
|
||||||
|
minWidth: "auto",
|
||||||
|
whiteSpace: "nowrap"
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Artikel Bewerten
|
||||||
|
</Button>
|
||||||
|
{(product.available !== 1 && product.availableSupplier !== 1) && (
|
||||||
|
<Button
|
||||||
|
variant="outlined"
|
||||||
|
size="small"
|
||||||
|
onClick={this.toggleAvailabilityForm}
|
||||||
|
sx={{
|
||||||
|
fontSize: "0.75rem",
|
||||||
|
px: 1.5,
|
||||||
|
py: 0.5,
|
||||||
|
minWidth: "auto",
|
||||||
|
whiteSpace: "nowrap",
|
||||||
|
borderColor: "warning.main",
|
||||||
|
color: "warning.main",
|
||||||
|
"&:hover": {
|
||||||
|
borderColor: "warning.dark",
|
||||||
|
backgroundColor: "warning.light"
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Verfügbarkeit anfragen
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
</Box>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Weight */}
|
{/* Weight */}
|
||||||
{product.weight > 0 && (
|
{product.weight > 0 && (
|
||||||
<Box sx={{ mb: 2 }}>
|
<Box sx={{ mb: 2 }}>
|
||||||
<Typography variant="body2" color="text.secondary">
|
<Typography variant="body2" color="text.secondary">
|
||||||
Gewicht: {product.weight.toFixed(1).replace(".", ",")} kg
|
{this.props.t ? this.props.t('product.weight', { weight: product.weight.toFixed(1).replace(".", ",") }) : `Gewicht: ${product.weight.toFixed(1).replace(".", ",")} kg`}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
@@ -451,8 +838,12 @@ class ProductDetailPage extends Component {
|
|||||||
{priceWithTax}
|
{priceWithTax}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography variant="body2" color="text.secondary">
|
<Typography variant="body2" color="text.secondary">
|
||||||
inkl. {product.vat}% MwSt.
|
{this.props.t ? this.props.t('product.inclVat', { vat: product.vat }) : `inkl. ${product.vat}% MwSt.`}
|
||||||
|
{product.cGrundEinheit && product.fGrundPreis && (
|
||||||
|
<>; {new Intl.NumberFormat('de-DE', {style: 'currency', currency: 'EUR'}).format(product.fGrundPreis)}/{product.cGrundEinheit}</>
|
||||||
|
)}
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
{product.versandklasse &&
|
{product.versandklasse &&
|
||||||
product.versandklasse != "standard" &&
|
product.versandklasse != "standard" &&
|
||||||
product.versandklasse != "kostenlos" && (
|
product.versandklasse != "kostenlos" && (
|
||||||
@@ -461,6 +852,42 @@ class ProductDetailPage extends Component {
|
|||||||
</Typography>
|
</Typography>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
|
{/* Savings comparison - positioned between price and cart button */}
|
||||||
|
{product.komponenten && komponentenLoaded && totalKomponentenPrice > product.price &&
|
||||||
|
(totalKomponentenPrice - product.price >= 2 &&
|
||||||
|
(totalKomponentenPrice - product.price) / product.price >= 0.02) && (
|
||||||
|
<Box sx={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
minWidth: { xs: "100%", sm: "200px" }
|
||||||
|
}}>
|
||||||
|
<Box sx={{ p: 2, borderRadius: 1, backgroundColor: "#e8f5e8", textAlign: "center" }}>
|
||||||
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
sx={{
|
||||||
|
fontWeight: "bold",
|
||||||
|
color: "success.main"
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{this.props.t ? this.props.t('product.youSave', {
|
||||||
|
amount: new Intl.NumberFormat("de-DE", {
|
||||||
|
style: "currency",
|
||||||
|
currency: "EUR",
|
||||||
|
}).format(totalKomponentenPrice - product.price)
|
||||||
|
}) : `Sie sparen: ${new Intl.NumberFormat("de-DE", {
|
||||||
|
style: "currency",
|
||||||
|
currency: "EUR",
|
||||||
|
}).format(totalKomponentenPrice - product.price)}`}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="caption" color="text.secondary">
|
||||||
|
{this.props.t ? this.props.t('product.cheaperThanIndividual') : 'Günstiger als Einzelkauf'}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
display: "flex",
|
display: "flex",
|
||||||
@@ -487,6 +914,7 @@ class ProductDetailPage extends Component {
|
|||||||
vat={product.vat}
|
vat={product.vat}
|
||||||
weight={product.weight}
|
weight={product.weight}
|
||||||
availableSupplier={product.availableSupplier}
|
availableSupplier={product.availableSupplier}
|
||||||
|
komponenten={product.komponenten}
|
||||||
name={cleanProductName(product.name) + " Stecklingsvorbestellung 1 Stück"}
|
name={cleanProductName(product.name) + " Stecklingsvorbestellung 1 Stück"}
|
||||||
versandklasse={"nur Abholung"}
|
versandklasse={"nur Abholung"}
|
||||||
/>
|
/>
|
||||||
@@ -499,7 +927,7 @@ class ProductDetailPage extends Component {
|
|||||||
mt: 1
|
mt: 1
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Abholpreis: 19,90 € pro Steckling.
|
{this.props.t ? this.props.t('product.pickupPrice') : 'Abholpreis: 19,90 € pro Steckling.'}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
@@ -516,12 +944,16 @@ class ProductDetailPage extends Component {
|
|||||||
available={product.available}
|
available={product.available}
|
||||||
id={product.id}
|
id={product.id}
|
||||||
availableSupplier={product.availableSupplier}
|
availableSupplier={product.availableSupplier}
|
||||||
|
komponenten={product.komponenten}
|
||||||
|
cGrundEinheit={product.cGrundEinheit}
|
||||||
|
fGrundPreis={product.fGrundPreis}
|
||||||
price={product.price}
|
price={product.price}
|
||||||
vat={product.vat}
|
vat={product.vat}
|
||||||
weight={product.weight}
|
weight={product.weight}
|
||||||
name={cleanProductName(product.name)}
|
name={cleanProductName(product.name)}
|
||||||
versandklasse={product.versandklasse}
|
versandklasse={product.versandklasse}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Typography
|
<Typography
|
||||||
variant="caption"
|
variant="caption"
|
||||||
sx={{
|
sx={{
|
||||||
@@ -531,9 +963,12 @@ class ProductDetailPage extends Component {
|
|||||||
mt: 1
|
mt: 1
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{product.id.toString().endsWith("steckling") ? "Lieferzeit: 14 Tage" :
|
{product.id.toString().endsWith("steckling") ?
|
||||||
product.available == 1 ? "Lieferzeit: 2-3 Tage" :
|
(this.props.t ? this.props.t('delivery.times.cutting14Days') : "Lieferzeit: 14 Tage") :
|
||||||
product.availableSupplier == 1 ? "Lieferzeit: 7-9 Tage" : ""}
|
product.available == 1 ?
|
||||||
|
(this.props.t ? this.props.t('delivery.times.standard2to3Days') : "Lieferzeit: 2-3 Tage") :
|
||||||
|
product.availableSupplier == 1 ?
|
||||||
|
(this.props.t ? this.props.t('delivery.times.supplier7to9Days') : "Lieferzeit: 7-9 Tage") : ""}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
@@ -565,9 +1000,242 @@ class ProductDetailPage extends Component {
|
|||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Article Question Form */}
|
||||||
|
<Collapse in={this.state.showQuestionForm}>
|
||||||
|
<div id="question-form">
|
||||||
|
<ArticleQuestionForm
|
||||||
|
productId={product.id}
|
||||||
|
productName={cleanProductName(product.name)}
|
||||||
|
socket={this.props.socket}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Collapse>
|
||||||
|
|
||||||
|
{/* Article Rating Form */}
|
||||||
|
<Collapse in={this.state.showRatingForm}>
|
||||||
|
<div id="rating-form">
|
||||||
|
<ArticleRatingForm
|
||||||
|
productId={product.id}
|
||||||
|
productName={cleanProductName(product.name)}
|
||||||
|
socket={this.props.socket}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Collapse>
|
||||||
|
|
||||||
|
{/* Article Availability Form - only show for out of stock items */}
|
||||||
|
{(product.available !== 1 && product.availableSupplier !== 1) && (
|
||||||
|
<Collapse in={this.state.showAvailabilityForm}>
|
||||||
|
<ArticleAvailabilityForm
|
||||||
|
productId={product.id}
|
||||||
|
productName={cleanProductName(product.name)}
|
||||||
|
socket={this.props.socket}
|
||||||
|
/>
|
||||||
|
</Collapse>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{product.komponenten && product.komponenten.split(",").length > 0 && (
|
||||||
|
<Box sx={{ mt: 4, p: 4, background: "#fff", borderRadius: 2, boxShadow: "0 2px 8px rgba(0,0,0,0.08)" }}>
|
||||||
|
<Typography variant="h4" gutterBottom>{this.props.t ? this.props.t('product.consistsOf') : 'Bestehend aus:'}</Typography>
|
||||||
|
<Box sx={{ maxWidth: 800, mx: "auto" }}>
|
||||||
|
|
||||||
|
{(console.log("komponentenLoaded:", komponentenLoaded), komponentenLoaded) ? (
|
||||||
|
<>
|
||||||
|
{console.log("Rendering loaded komponenten:", this.state.komponenten.length, "komponentenData:", Object.keys(komponentenData).length)}
|
||||||
|
{this.state.komponenten.map((komponent, index) => {
|
||||||
|
const komponentData = komponentenData[komponent.id];
|
||||||
|
console.log(`Rendering komponent ${komponent.id}:`, komponentData);
|
||||||
|
|
||||||
|
// Don't show border on last item (pricing section has its own top border)
|
||||||
|
const isLastItem = index === this.state.komponenten.length - 1;
|
||||||
|
const showBorder = !isLastItem;
|
||||||
|
|
||||||
|
if (!komponentData || !komponentData.loaded) {
|
||||||
|
return (
|
||||||
|
<Box key={komponent.id} sx={{
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
alignItems: "center",
|
||||||
|
py: 1,
|
||||||
|
borderBottom: showBorder ? "1px solid #eee" : "none",
|
||||||
|
minHeight: "70px" // Consistent height to prevent layout shifts
|
||||||
|
}}>
|
||||||
|
<Box sx={{ display: "flex", alignItems: "center", gap: 2 }}>
|
||||||
|
<Box sx={{ width: 50, height: 50, flexShrink: 0, backgroundColor: "#f5f5f5", borderRadius: 1, border: "1px solid #e0e0e0" }}>
|
||||||
|
{/* Empty placeholder for image */}
|
||||||
|
</Box>
|
||||||
|
<Box>
|
||||||
|
<Typography variant="body1">
|
||||||
|
{index + 1}. Lädt...
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" color="text.secondary">
|
||||||
|
{komponent.count}x
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
<Typography variant="body1" sx={{ fontWeight: 500 }}>
|
||||||
|
-
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const itemPrice = komponentData.price * parseInt(komponent.count);
|
||||||
|
const formattedPrice = new Intl.NumberFormat("de-DE", {
|
||||||
|
style: "currency",
|
||||||
|
currency: "EUR",
|
||||||
|
}).format(itemPrice);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
key={komponent.id}
|
||||||
|
component={Link}
|
||||||
|
to={`/Artikel/${komponentData.seoName}`}
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
alignItems: "center",
|
||||||
|
py: 1,
|
||||||
|
borderBottom: showBorder ? "1px solid #eee" : "none",
|
||||||
|
textDecoration: "none",
|
||||||
|
color: "inherit",
|
||||||
|
minHeight: "70px", // Consistent height to prevent layout shifts
|
||||||
|
"&:hover": {
|
||||||
|
backgroundColor: "#f5f5f5"
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box sx={{ display: "flex", alignItems: "center", gap: 2 }}>
|
||||||
|
<Box sx={{ width: 50, height: 50, flexShrink: 0 }}>
|
||||||
|
{komponentenImages[komponent.id] ? (
|
||||||
|
<CardMedia
|
||||||
|
component="img"
|
||||||
|
height="50"
|
||||||
|
image={komponentenImages[komponent.id]}
|
||||||
|
alt={komponentData.name}
|
||||||
|
sx={{
|
||||||
|
objectFit: "contain",
|
||||||
|
borderRadius: 1,
|
||||||
|
border: "1px solid #e0e0e0"
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<CardMedia
|
||||||
|
component="img"
|
||||||
|
height="50"
|
||||||
|
image="/assets/images/nopicture.jpg"
|
||||||
|
alt={komponentData.name}
|
||||||
|
sx={{
|
||||||
|
objectFit: "contain",
|
||||||
|
borderRadius: 1,
|
||||||
|
border: "1px solid #e0e0e0"
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
<Box>
|
||||||
|
<Typography variant="body1" sx={{ fontWeight: 500 }}>
|
||||||
|
{index + 1}. {cleanProductName(komponentData.name)}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" color="text.secondary">
|
||||||
|
{komponent.count}x à {new Intl.NumberFormat("de-DE", {
|
||||||
|
style: "currency",
|
||||||
|
currency: "EUR",
|
||||||
|
}).format(komponentData.price)}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
<Typography variant="body1" sx={{ fontWeight: 500 }}>
|
||||||
|
{formattedPrice}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
|
||||||
|
{/* Total price and savings display - only show when prices differ meaningfully */}
|
||||||
|
{totalKomponentenPrice > product.price &&
|
||||||
|
(totalKomponentenPrice - product.price >= 2 &&
|
||||||
|
(totalKomponentenPrice - product.price) / product.price >= 0.02) && (
|
||||||
|
<Box sx={{ mt: 3, pt: 2, borderTop: "2px solid #eee" }}>
|
||||||
|
<Box sx={{ display: "flex", justifyContent: "space-between", alignItems: "center", mb: 1 }}>
|
||||||
|
<Typography variant="h6">
|
||||||
|
{this.props.t ? this.props.t('product.individualPriceTotal') : 'Einzelpreis gesamt:'}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="h6" sx={{ textDecoration: "line-through", color: "text.secondary" }}>
|
||||||
|
{new Intl.NumberFormat("de-DE", {
|
||||||
|
style: "currency",
|
||||||
|
currency: "EUR",
|
||||||
|
}).format(totalKomponentenPrice)}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
<Box sx={{ display: "flex", justifyContent: "space-between", alignItems: "center", mb: 1 }}>
|
||||||
|
<Typography variant="h6">
|
||||||
|
{this.props.t ? this.props.t('product.setPrice') : 'Set-Preis:'}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="h6" color="primary" sx={{ fontWeight: "bold" }}>
|
||||||
|
{new Intl.NumberFormat("de-DE", {
|
||||||
|
style: "currency",
|
||||||
|
currency: "EUR",
|
||||||
|
}).format(product.price)}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
{totalSavings > 0 && (
|
||||||
|
<Box sx={{ display: "flex", justifyContent: "space-between", alignItems: "center", mt: 2, p: 2, backgroundColor: "#e8f5e8", borderRadius: 1 }}>
|
||||||
|
<Typography variant="h6" color="success.main" sx={{ fontWeight: "bold" }}>
|
||||||
|
{this.props.t ? this.props.t('product.yourSavings') : 'Ihre Ersparnis:'}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="h6" color="success.main" sx={{ fontWeight: "bold" }}>
|
||||||
|
{new Intl.NumberFormat("de-DE", {
|
||||||
|
style: "currency",
|
||||||
|
currency: "EUR",
|
||||||
|
}).format(totalSavings)}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
// Loading state
|
||||||
|
<Box>
|
||||||
|
{this.state.komponenten.map((komponent, index) => {
|
||||||
|
// For loading state, we don't know if pricing will be shown, so show all borders
|
||||||
|
return (
|
||||||
|
<Box key={komponent.id} sx={{
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
alignItems: "center",
|
||||||
|
py: 1,
|
||||||
|
borderBottom: "1px solid #eee",
|
||||||
|
minHeight: "70px" // Consistent height to prevent layout shifts
|
||||||
|
}}>
|
||||||
|
<Box sx={{ display: "flex", alignItems: "center", gap: 2 }}>
|
||||||
|
<Box sx={{ width: 50, height: 50, flexShrink: 0, backgroundColor: "#f5f5f5", borderRadius: 1, border: "1px solid #e0e0e0" }}>
|
||||||
|
{/* Empty placeholder for image */}
|
||||||
|
</Box>
|
||||||
|
<Box>
|
||||||
|
<Typography variant="body1">
|
||||||
|
{this.props.t ? this.props.t('product.loadingComponentDetails', { index: index + 1 }) : `${index + 1}. Lädt Komponent-Details...`}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" color="text.secondary">
|
||||||
|
{komponent.count}x
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
<Typography variant="body1" sx={{ fontWeight: 500 }}>
|
||||||
|
-
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ProductDetailPage;
|
export default withI18n()(ProductDetailPage);
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import Typography from '@mui/material/Typography';
|
|||||||
import Filter from './Filter.js';
|
import Filter from './Filter.js';
|
||||||
import { useParams, useSearchParams, useNavigate, useLocation } from 'react-router-dom';
|
import { useParams, useSearchParams, useNavigate, useLocation } from 'react-router-dom';
|
||||||
import { setSessionSetting, removeSessionSetting, clearAllSessionSettings } from '../utils/sessionStorage.js';
|
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);
|
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) => {
|
_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){
|
for(const product of products){
|
||||||
if(isNew(product.neu)){
|
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(!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
|
return filters
|
||||||
@@ -193,7 +194,7 @@ class ProductFilters extends Component {
|
|||||||
|
|
||||||
{this.props.products.length > 0 && (
|
{this.props.products.length > 0 && (
|
||||||
<><Filter
|
<><Filter
|
||||||
title="Verfügbarkeit"
|
title={this.props.t ? this.props.t('filters.availability') : 'Verfügbarkeit'}
|
||||||
options={this.state.availabilityValues}
|
options={this.state.availabilityValues}
|
||||||
searchParams={this.props.searchParams}
|
searchParams={this.props.searchParams}
|
||||||
products={this.props.products}
|
products={this.props.products}
|
||||||
@@ -236,7 +237,7 @@ class ProductFilters extends Component {
|
|||||||
{this.generateAttributeFilters()}
|
{this.generateAttributeFilters()}
|
||||||
|
|
||||||
<Filter
|
<Filter
|
||||||
title="Hersteller"
|
title={this.props.t ? this.props.t('filters.manufacturer') : 'Hersteller'}
|
||||||
options={this.state.uniqueManufacturerArray}
|
options={this.state.uniqueManufacturerArray}
|
||||||
filterType="manufacturer"
|
filterType="manufacturer"
|
||||||
products={this.props.products}
|
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 Stack from '@mui/material/Stack';
|
||||||
import Product from './Product.js';
|
import Product from './Product.js';
|
||||||
import { removeSessionSetting } from '../utils/sessionStorage.js';
|
import { removeSessionSetting } from '../utils/sessionStorage.js';
|
||||||
|
import { withI18n } from '../i18n/withTranslation.js';
|
||||||
|
|
||||||
// Sort products by fuzzy similarity to their name/description
|
// Sort products by fuzzy similarity to their name/description
|
||||||
function sortProductsByFuzzySimilarity(products, searchTerm) {
|
function sortProductsByFuzzySimilarity(products, searchTerm) {
|
||||||
@@ -141,12 +142,12 @@ class ProductList extends Component {
|
|||||||
onChange={this.handlePageChange}
|
onChange={this.handlePageChange}
|
||||||
color="primary"
|
color="primary"
|
||||||
size={"large"}
|
size={"large"}
|
||||||
siblingCount={window.innerWidth < 600 ? 0 : 1}
|
siblingCount={1}
|
||||||
boundaryCount={window.innerWidth < 600 ? 1 : 1}
|
boundaryCount={1}
|
||||||
hideNextButton={false}
|
hideNextButton={true}
|
||||||
hidePrevButton={false}
|
hidePrevButton={true}
|
||||||
showFirstButton={window.innerWidth >= 600}
|
showFirstButton={false}
|
||||||
showLastButton={window.innerWidth >= 600}
|
showLastButton={false}
|
||||||
sx={{
|
sx={{
|
||||||
'& .MuiPagination-ul': {
|
'& .MuiPagination-ul': {
|
||||||
flexWrap: 'nowrap',
|
flexWrap: 'nowrap',
|
||||||
@@ -184,7 +185,7 @@ class ProductList extends Component {
|
|||||||
px: 2
|
px: 2
|
||||||
}}>
|
}}>
|
||||||
<Typography variant="h6" color="text.secondary" sx={{ textAlign: 'center' }}>
|
<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>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
@@ -200,14 +201,14 @@ class ProductList extends Component {
|
|||||||
|
|
||||||
if (!isFiltered) {
|
if (!isFiltered) {
|
||||||
// No filters applied
|
// No filters applied
|
||||||
if (filteredCount === 0) return "0 Produkte";
|
if (filteredCount === 0) return this.props.t ? this.props.t('product.countDisplay.noProducts') : "0 Produkte";
|
||||||
if (filteredCount === 1) return "1 Produkt";
|
if (filteredCount === 1) return this.props.t ? this.props.t('product.countDisplay.oneProduct') : "1 Produkt";
|
||||||
return `${filteredCount} Produkte`;
|
return this.props.t ? this.props.t('product.countDisplay.multipleProducts', { count: filteredCount }) : `${filteredCount} Produkte`;
|
||||||
} else {
|
} else {
|
||||||
// Filters applied
|
// Filters applied
|
||||||
if (totalCount === 0) return "0 Produkte";
|
if (totalCount === 0) return this.props.t ? this.props.t('product.countDisplay.noProducts') : "0 Produkte";
|
||||||
if (totalCount === 1) return `${filteredCount} von 1 Produkt`;
|
if (totalCount === 1) return this.props.t ? this.props.t('product.countDisplay.filteredOneProduct', { filtered: filteredCount }) : `${filteredCount} von 1 Produkt`;
|
||||||
return `${filteredCount} von ${totalCount} Produkten`;
|
return this.props.t ? this.props.t('product.countDisplay.filteredProducts', { filtered: filteredCount, total: totalCount }) : `${filteredCount} von ${totalCount} Produkten`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -327,13 +328,13 @@ class ProductList extends Component {
|
|||||||
minWidth: { xs: 120, sm: 140 }
|
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
|
<Select
|
||||||
size="small"
|
size="small"
|
||||||
labelId="sort-by-label"
|
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'}
|
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}
|
onChange={this.handleSortChange}
|
||||||
label="Sortierung"
|
label={this.props.t ? this.props.t('filters.sorting') : 'Sortierung'}
|
||||||
MenuProps={{
|
MenuProps={{
|
||||||
disableScrollLock: true,
|
disableScrollLock: true,
|
||||||
anchorOrigin: {
|
anchorOrigin: {
|
||||||
@@ -353,10 +354,10 @@ class ProductList extends Component {
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<MenuItem value="name">Name</MenuItem>
|
<MenuItem value="name">{this.props.t ? this.props.t('sorting.name') : 'Name'}</MenuItem>
|
||||||
{window.currentSearchQuery && <MenuItem value="searchField">Suchbegriff</MenuItem>}
|
{window.currentSearchQuery && <MenuItem value="searchField">{this.props.t ? this.props.t('sorting.searchField') : 'Suchbegriff'}</MenuItem>}
|
||||||
<MenuItem value="price-low-high">Preis: Niedrig zu Hoch</MenuItem>
|
<MenuItem value="price-low-high">{this.props.t ? this.props.t('sorting.priceLowHigh') : 'Preis: Niedrig zu Hoch'}</MenuItem>
|
||||||
<MenuItem value="price-high-low">Preis: Hoch zu Niedrig</MenuItem>
|
<MenuItem value="price-high-low">{this.props.t ? this.props.t('sorting.priceHighLow') : 'Preis: Hoch zu Niedrig'}</MenuItem>
|
||||||
</Select>
|
</Select>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|
||||||
@@ -368,12 +369,12 @@ class ProductList extends Component {
|
|||||||
minWidth: { xs: 80, sm: 100 }
|
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
|
<Select
|
||||||
labelId="products-per-page-label"
|
labelId="products-per-page-label"
|
||||||
value={this.state.itemsPerPage}
|
value={this.state.itemsPerPage}
|
||||||
onChange={this.handleProductsPerPageChange}
|
onChange={this.handleProductsPerPageChange}
|
||||||
label="pro Seite"
|
label={this.props.t ? this.props.t('filters.perPage') : 'pro Seite'}
|
||||||
MenuProps={{
|
MenuProps={{
|
||||||
disableScrollLock: true,
|
disableScrollLock: true,
|
||||||
anchorOrigin: {
|
anchorOrigin: {
|
||||||
@@ -462,8 +463,8 @@ class ProductList extends Component {
|
|||||||
available={product.available}
|
available={product.available}
|
||||||
manufacturer={product.manufacturer}
|
manufacturer={product.manufacturer}
|
||||||
vat={product.vat}
|
vat={product.vat}
|
||||||
massMenge={product.massMenge}
|
cGrundEinheit={product.cGrundEinheit}
|
||||||
massEinheit={product.massEinheit}
|
fGrundPreis={product.fGrundPreis}
|
||||||
incoming={product.incomingDate}
|
incoming={product.incomingDate}
|
||||||
neu={product.neu}
|
neu={product.neu}
|
||||||
thc={product.thc}
|
thc={product.thc}
|
||||||
@@ -474,6 +475,8 @@ class ProductList extends Component {
|
|||||||
socketB={this.props.socketB}
|
socketB={this.props.socketB}
|
||||||
pictureList={product.pictureList}
|
pictureList={product.pictureList}
|
||||||
availableSupplier={product.availableSupplier}
|
availableSupplier={product.availableSupplier}
|
||||||
|
komponenten={product.komponenten}
|
||||||
|
t={this.props.t}
|
||||||
/>
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
))}
|
))}
|
||||||
@@ -495,4 +498,4 @@ class ProductList extends Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ProductList;
|
export default withI18n()(ProductList);
|
||||||
273
src/components/SharedCarousel.js
Normal file
273
src/components/SharedCarousel.js
Normal file
@@ -0,0 +1,273 @@
|
|||||||
|
import React, { useContext, useEffect, useState } from "react";
|
||||||
|
import Box from "@mui/material/Box";
|
||||||
|
import Typography from "@mui/material/Typography";
|
||||||
|
import IconButton from "@mui/material/IconButton";
|
||||||
|
import ChevronLeft from "@mui/icons-material/ChevronLeft";
|
||||||
|
import ChevronRight from "@mui/icons-material/ChevronRight";
|
||||||
|
import CategoryBox from "./CategoryBox.js";
|
||||||
|
import SocketContext from "../contexts/SocketContext.js";
|
||||||
|
import { useCarousel } from "../contexts/CarouselContext.js";
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
// Helper to process and set categories
|
||||||
|
const processCategoryTree = (categoryTree) => {
|
||||||
|
if (
|
||||||
|
categoryTree &&
|
||||||
|
categoryTree.id === 209 &&
|
||||||
|
Array.isArray(categoryTree.children)
|
||||||
|
) {
|
||||||
|
return categoryTree.children;
|
||||||
|
} else {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check for cached data
|
||||||
|
const getProductCache = () => {
|
||||||
|
if (typeof window !== "undefined" && window.productCache) {
|
||||||
|
return window.productCache;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
typeof global !== "undefined" &&
|
||||||
|
global.window &&
|
||||||
|
global.window.productCache
|
||||||
|
) {
|
||||||
|
return global.window.productCache;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Initialize categories
|
||||||
|
const initializeCategories = (language = 'en') => {
|
||||||
|
const productCache = getProductCache();
|
||||||
|
|
||||||
|
if (productCache && productCache[`categoryTree_209_${language}`]) {
|
||||||
|
const cached = productCache[`categoryTree_209_${language}`];
|
||||||
|
if (cached.categoryTree) {
|
||||||
|
return processCategoryTree(cached.categoryTree);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to old cache format if language-specific cache doesn't exist
|
||||||
|
if (productCache && productCache["categoryTree_209"]) {
|
||||||
|
const cached = productCache["categoryTree_209"];
|
||||||
|
if (cached.categoryTree) {
|
||||||
|
return processCategoryTree(cached.categoryTree);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
|
};
|
||||||
|
|
||||||
|
const SharedCarousel = () => {
|
||||||
|
const { carouselRef, filteredCategories, setFilteredCategories, moveCarousel } = useCarousel();
|
||||||
|
const context = useContext(SocketContext);
|
||||||
|
const { t, i18n } = useTranslation();
|
||||||
|
const [rootCategories, setRootCategories] = useState([]);
|
||||||
|
const [currentLanguage, setCurrentLanguage] = useState(i18n.language);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const initialCategories = initializeCategories(currentLanguage);
|
||||||
|
setRootCategories(initialCategories);
|
||||||
|
}, [currentLanguage]);
|
||||||
|
|
||||||
|
// Listen for language changes
|
||||||
|
useEffect(() => {
|
||||||
|
const handleLanguageChange = (lng) => {
|
||||||
|
setCurrentLanguage(lng);
|
||||||
|
// Clear categories to force refetch
|
||||||
|
setRootCategories([]);
|
||||||
|
};
|
||||||
|
|
||||||
|
i18n.on('languageChanged', handleLanguageChange);
|
||||||
|
return () => {
|
||||||
|
i18n.off('languageChanged', handleLanguageChange);
|
||||||
|
};
|
||||||
|
}, [i18n]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Only fetch from socket if we don't already have categories
|
||||||
|
if (
|
||||||
|
rootCategories.length === 0 &&
|
||||||
|
context && context.socket && context.socket.connected &&
|
||||||
|
typeof window !== "undefined"
|
||||||
|
) {
|
||||||
|
context.socket.emit("categoryList", { categoryId: 209, language: currentLanguage, requestTranslation: true }, (response) => {
|
||||||
|
if (response && response.success) {
|
||||||
|
// Use translated data if available, otherwise fall back to original
|
||||||
|
const categoryTreeToUse = response.translation || response.categoryTree;
|
||||||
|
|
||||||
|
if (categoryTreeToUse) {
|
||||||
|
// Store in cache with language-specific key
|
||||||
|
try {
|
||||||
|
if (!window.productCache) window.productCache = {};
|
||||||
|
window.productCache[`categoryTree_209_${currentLanguage}`] = {
|
||||||
|
categoryTree: categoryTreeToUse,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
};
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
setRootCategories(categoryTreeToUse.children || []);
|
||||||
|
}
|
||||||
|
} else if (response && response.categoryTree) {
|
||||||
|
// Fallback for old response format
|
||||||
|
// Store in cache with language-specific key
|
||||||
|
try {
|
||||||
|
if (!window.productCache) window.productCache = {};
|
||||||
|
window.productCache[`categoryTree_209_${currentLanguage}`] = {
|
||||||
|
categoryTree: response.categoryTree,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
};
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
setRootCategories(response.categoryTree.children || []);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [context, context?.socket?.connected, rootCategories.length, currentLanguage]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const filtered = rootCategories.filter(
|
||||||
|
(cat) => cat.id !== 689 && cat.id !== 706
|
||||||
|
);
|
||||||
|
setFilteredCategories(filtered);
|
||||||
|
}, [rootCategories, setFilteredCategories]);
|
||||||
|
|
||||||
|
// Create duplicated array for seamless scrolling
|
||||||
|
const displayCategories = [...filteredCategories, ...filteredCategories];
|
||||||
|
|
||||||
|
if (filteredCategories.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box sx={{ mt: 3 }}>
|
||||||
|
<Typography
|
||||||
|
variant="h4"
|
||||||
|
component="h1"
|
||||||
|
sx={{
|
||||||
|
mb: 2,
|
||||||
|
fontFamily: "SwashingtonCP",
|
||||||
|
color: "primary.main",
|
||||||
|
textAlign: "center",
|
||||||
|
textShadow: "3px 3px 10px rgba(0, 0, 0, 0.4)"
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t('navigation.categories')}
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className="carousel-wrapper"
|
||||||
|
style={{
|
||||||
|
position: 'relative',
|
||||||
|
overflow: 'hidden',
|
||||||
|
width: '100%',
|
||||||
|
maxWidth: '1200px',
|
||||||
|
margin: '0 auto',
|
||||||
|
padding: '0 20px',
|
||||||
|
boxSizing: 'border-box'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* Left Arrow */}
|
||||||
|
<IconButton
|
||||||
|
onClick={() => moveCarousel("left")}
|
||||||
|
aria-label="Vorherige Kategorien anzeigen"
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
top: '50%',
|
||||||
|
left: '8px',
|
||||||
|
transform: 'translateY(-50%)',
|
||||||
|
zIndex: 1200,
|
||||||
|
backgroundColor: 'rgba(255, 255, 255, 0.9)',
|
||||||
|
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.15)',
|
||||||
|
width: '48px',
|
||||||
|
height: '48px',
|
||||||
|
borderRadius: '50%'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ChevronLeft />
|
||||||
|
</IconButton>
|
||||||
|
|
||||||
|
{/* Right Arrow */}
|
||||||
|
<IconButton
|
||||||
|
onClick={() => moveCarousel("right")}
|
||||||
|
aria-label="Nächste Kategorien anzeigen"
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
top: '50%',
|
||||||
|
right: '8px',
|
||||||
|
transform: 'translateY(-50%)',
|
||||||
|
zIndex: 1200,
|
||||||
|
backgroundColor: 'rgba(255, 255, 255, 0.9)',
|
||||||
|
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.15)',
|
||||||
|
width: '48px',
|
||||||
|
height: '48px',
|
||||||
|
borderRadius: '50%'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ChevronRight />
|
||||||
|
</IconButton>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className="carousel-container"
|
||||||
|
style={{
|
||||||
|
position: 'relative',
|
||||||
|
overflow: 'hidden',
|
||||||
|
padding: '20px 0',
|
||||||
|
width: '100%',
|
||||||
|
maxWidth: '1080px',
|
||||||
|
margin: '0 auto',
|
||||||
|
zIndex: 1,
|
||||||
|
boxSizing: 'border-box'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="home-carousel-track"
|
||||||
|
ref={carouselRef}
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
gap: '16px',
|
||||||
|
transition: 'none',
|
||||||
|
alignItems: 'flex-start',
|
||||||
|
width: 'fit-content',
|
||||||
|
overflow: 'visible',
|
||||||
|
position: 'relative',
|
||||||
|
transform: 'translateX(0px)',
|
||||||
|
margin: '0 auto'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{displayCategories.map((category, index) => (
|
||||||
|
<div
|
||||||
|
key={`${category.id}-${index}`}
|
||||||
|
className="carousel-item"
|
||||||
|
style={{
|
||||||
|
flex: '0 0 130px',
|
||||||
|
width: '130px',
|
||||||
|
maxWidth: '130px',
|
||||||
|
minWidth: '130px',
|
||||||
|
height: '130px',
|
||||||
|
maxHeight: '130px',
|
||||||
|
minHeight: '130px',
|
||||||
|
boxSizing: 'border-box',
|
||||||
|
position: 'relative'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CategoryBox
|
||||||
|
id={category.id}
|
||||||
|
name={category.name}
|
||||||
|
seoName={category.seoName}
|
||||||
|
image={category.image}
|
||||||
|
bgcolor={category.bgcolor}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SharedCarousel;
|
||||||
@@ -10,7 +10,9 @@ import CloseIcon from '@mui/icons-material/Close';
|
|||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import LoginComponent from '../LoginComponent.js';
|
import LoginComponent from '../LoginComponent.js';
|
||||||
import CartDropdown from '../CartDropdown.js';
|
import CartDropdown from '../CartDropdown.js';
|
||||||
|
import LanguageSwitcher from '../LanguageSwitcher.js';
|
||||||
import { isUserLoggedIn } from '../LoginComponent.js';
|
import { isUserLoggedIn } from '../LoginComponent.js';
|
||||||
|
import { withI18n } from '../../i18n/withTranslation.js';
|
||||||
|
|
||||||
function getBadgeNumber() {
|
function getBadgeNumber() {
|
||||||
let count = 0;
|
let count = 0;
|
||||||
@@ -116,14 +118,14 @@ class ButtonGroup extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { socket, navigate } = this.props;
|
const { socket, navigate, t } = this.props;
|
||||||
const { isCartOpen } = this.state;
|
const { isCartOpen } = this.state;
|
||||||
const cartItems = Array.isArray(window.cart) ? window.cart : [];
|
const cartItems = Array.isArray(window.cart) ? window.cart : [];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box sx={{ display: 'flex', gap: { xs: 0.5, sm: 1 } }}>
|
<Box sx={{ display: 'flex', gap: { xs: 0.5, sm: 1 } }}>
|
||||||
|
|
||||||
|
<LanguageSwitcher />
|
||||||
<LoginComponent socket={socket} />
|
<LoginComponent socket={socket} />
|
||||||
|
|
||||||
<IconButton
|
<IconButton
|
||||||
@@ -164,7 +166,7 @@ class ButtonGroup extends Component {
|
|||||||
>
|
>
|
||||||
<CloseIcon />
|
<CloseIcon />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
<Typography variant="h6">Warenkorb</Typography>
|
<Typography variant="h6">{t ? t('cart.title') : 'Warenkorb'}</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
<Divider sx={{ mb: 2 }} />
|
<Divider sx={{ mb: 2 }} />
|
||||||
|
|
||||||
@@ -173,7 +175,7 @@ class ButtonGroup extends Component {
|
|||||||
|
|
||||||
if (isUserLoggedIn().isLoggedIn) {
|
if (isUserLoggedIn().isLoggedIn) {
|
||||||
this.toggleCart(); // Close the cart drawer
|
this.toggleCart(); // Close the cart drawer
|
||||||
navigate('/profile');
|
navigate('/profile#cart');
|
||||||
} else if (window.openLoginDrawer) {
|
} else if (window.openLoginDrawer) {
|
||||||
window.openLoginDrawer(); // Call global function to open login drawer
|
window.openLoginDrawer(); // Call global function to open login drawer
|
||||||
this.toggleCart(); // Close the cart 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 ButtonGroupWithRouter = (props) => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
return <ButtonGroup {...props} navigate={navigate} />;
|
const ButtonGroupWithTranslation = withI18n()(ButtonGroup);
|
||||||
|
return <ButtonGroupWithTranslation {...props} navigate={navigate} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ButtonGroupWithRouter;
|
export default ButtonGroupWithRouter;
|
||||||
@@ -8,6 +8,7 @@ import { Link } from "react-router-dom";
|
|||||||
import HomeIcon from "@mui/icons-material/Home";
|
import HomeIcon from "@mui/icons-material/Home";
|
||||||
import MenuIcon from "@mui/icons-material/Menu";
|
import MenuIcon from "@mui/icons-material/Menu";
|
||||||
import CloseIcon from "@mui/icons-material/Close";
|
import CloseIcon from "@mui/icons-material/Close";
|
||||||
|
import { withI18n } from "../../i18n/withTranslation.js";
|
||||||
|
|
||||||
class CategoryList extends Component {
|
class CategoryList extends Component {
|
||||||
findCategoryById = (category, targetId) => {
|
findCategoryById = (category, targetId) => {
|
||||||
@@ -49,6 +50,9 @@ class CategoryList extends Component {
|
|||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
|
// Get current language from props (provided by withI18n HOC)
|
||||||
|
const currentLanguage = props.languageContext?.currentLanguage || 'de';
|
||||||
|
|
||||||
// Check for cached data during SSR/initial render
|
// Check for cached data during SSR/initial render
|
||||||
let initialState = {
|
let initialState = {
|
||||||
categoryTree: null,
|
categoryTree: null,
|
||||||
@@ -58,6 +62,7 @@ class CategoryList extends Component {
|
|||||||
activePath: [], // Array of active category objects for each level
|
activePath: [], // Array of active category objects for each level
|
||||||
fetchedCategories: false,
|
fetchedCategories: false,
|
||||||
mobileMenuOpen: false, // State for mobile collapsible menu
|
mobileMenuOpen: false, // State for mobile collapsible menu
|
||||||
|
currentLanguage: currentLanguage,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Try to get cached data for SSR
|
// Try to get cached data for SSR
|
||||||
@@ -67,7 +72,7 @@ class CategoryList extends Component {
|
|||||||
(typeof window !== "undefined" && window.productCache);
|
(typeof window !== "undefined" && window.productCache);
|
||||||
|
|
||||||
if (productCache) {
|
if (productCache) {
|
||||||
const cacheKey = "categoryTree_209";
|
const cacheKey = `categoryTree_209_${currentLanguage}`;
|
||||||
const cachedData = productCache[cacheKey];
|
const cachedData = productCache[cacheKey];
|
||||||
if (cachedData && cachedData.categoryTree) {
|
if (cachedData && cachedData.categoryTree) {
|
||||||
const { categoryTree, timestamp } = cachedData;
|
const { categoryTree, timestamp } = cachedData;
|
||||||
@@ -127,8 +132,27 @@ class CategoryList extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
componentDidUpdate(prevProps) {
|
||||||
// Handle socket connection changes
|
// Handle language changes
|
||||||
|
const currentLanguage = this.props.languageContext?.currentLanguage || 'de';
|
||||||
|
const prevLanguage = prevProps.languageContext?.currentLanguage || 'de';
|
||||||
|
|
||||||
|
if (currentLanguage !== prevLanguage) {
|
||||||
|
// Language changed, need to refetch categories
|
||||||
|
this.setState({
|
||||||
|
currentLanguage: currentLanguage,
|
||||||
|
fetchedCategories: false,
|
||||||
|
categoryTree: null,
|
||||||
|
level1Categories: [],
|
||||||
|
level2Categories: [],
|
||||||
|
level3Categories: [],
|
||||||
|
activePath: [],
|
||||||
|
}, () => {
|
||||||
|
this.fetchCategories();
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle socket connection changes
|
||||||
const wasConnected = prevProps.socket && prevProps.socket.connected;
|
const wasConnected = prevProps.socket && prevProps.socket.connected;
|
||||||
const isNowConnected = this.props.socket && this.props.socket.connected;
|
const isNowConnected = this.props.socket && this.props.socket.connected;
|
||||||
|
|
||||||
@@ -168,6 +192,9 @@ class CategoryList extends Component {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get current language from state
|
||||||
|
const currentLanguage = this.state.currentLanguage || 'de';
|
||||||
|
|
||||||
// Initialize global cache object if it doesn't exist
|
// Initialize global cache object if it doesn't exist
|
||||||
// @note Handle both SSR (global.window) and browser (window) environments
|
// @note Handle both SSR (global.window) and browser (window) environments
|
||||||
const windowObj = (typeof global !== "undefined" && global.window) ||
|
const windowObj = (typeof global !== "undefined" && global.window) ||
|
||||||
@@ -179,7 +206,7 @@ class CategoryList extends Component {
|
|||||||
|
|
||||||
// Check if we have a valid cache in the global object
|
// Check if we have a valid cache in the global object
|
||||||
try {
|
try {
|
||||||
const cacheKey = "categoryTree_209";
|
const cacheKey = `categoryTree_209_${currentLanguage}`;
|
||||||
const cachedData = windowObj && windowObj.productCache ? windowObj.productCache[cacheKey] : null;
|
const cachedData = windowObj && windowObj.productCache ? windowObj.productCache[cacheKey] : null;
|
||||||
if (cachedData) {
|
if (cachedData) {
|
||||||
const { categoryTree, fetching } = cachedData;
|
const { categoryTree, fetching } = cachedData;
|
||||||
@@ -216,7 +243,7 @@ class CategoryList extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Mark as being fetched to prevent concurrent calls
|
// Mark as being fetched to prevent concurrent calls
|
||||||
const cacheKey = "categoryTree_209";
|
const cacheKey = `categoryTree_209_${currentLanguage}`;
|
||||||
if (windowObj && windowObj.productCache) {
|
if (windowObj && windowObj.productCache) {
|
||||||
windowObj.productCache[cacheKey] = {
|
windowObj.productCache[cacheKey] = {
|
||||||
fetching: true,
|
fetching: true,
|
||||||
@@ -226,15 +253,18 @@ class CategoryList extends Component {
|
|||||||
this.setState({ fetchedCategories: true });
|
this.setState({ fetchedCategories: true });
|
||||||
|
|
||||||
//console.log('CategoryList: Fetching categories from socket');
|
//console.log('CategoryList: Fetching categories from socket');
|
||||||
socket.emit("categoryList", { categoryId: 209 }, (response) => {
|
socket.emit("categoryList", { categoryId: 209, language: currentLanguage, requestTranslation: true }, (response) => {
|
||||||
if (response && response.categoryTree) {
|
if (response && response.success) {
|
||||||
|
// Use translated data if available, otherwise fall back to original
|
||||||
|
const categoryTreeToUse = response.translation || response.categoryTree;
|
||||||
|
|
||||||
|
if (categoryTreeToUse) {
|
||||||
// Store in global cache with timestamp
|
// Store in global cache with timestamp
|
||||||
try {
|
try {
|
||||||
const cacheKey = "categoryTree_209";
|
const cacheKey = `categoryTree_209_${currentLanguage}`;
|
||||||
if (windowObj && windowObj.productCache) {
|
if (windowObj && windowObj.productCache) {
|
||||||
windowObj.productCache[cacheKey] = {
|
windowObj.productCache[cacheKey] = {
|
||||||
categoryTree: response.categoryTree,
|
categoryTree: categoryTreeToUse,
|
||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
fetching: false,
|
fetching: false,
|
||||||
};
|
};
|
||||||
@@ -242,14 +272,40 @@ class CategoryList extends Component {
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Error writing to cache:", err);
|
console.error("Error writing to cache:", err);
|
||||||
}
|
}
|
||||||
this.processCategoryTree(response.categoryTree);
|
this.processCategoryTree(categoryTreeToUse);
|
||||||
} else {
|
} else {
|
||||||
|
console.error('No category tree found in response');
|
||||||
|
// Clear cache on error
|
||||||
try {
|
try {
|
||||||
const cacheKey = "categoryTree_209";
|
const cacheKey = `categoryTree_209_${currentLanguage}`;
|
||||||
if (windowObj && windowObj.productCache) {
|
if (windowObj && windowObj.productCache) {
|
||||||
windowObj.productCache[cacheKey] = {
|
windowObj.productCache[cacheKey] = {
|
||||||
categoryTree: null,
|
categoryTree: null,
|
||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
|
fetching: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Error writing to cache:", err);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
categoryTree: null,
|
||||||
|
level1Categories: [],
|
||||||
|
level2Categories: [],
|
||||||
|
level3Categories: [],
|
||||||
|
activePath: [],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.error('Failed to fetch categories:', response);
|
||||||
|
try {
|
||||||
|
const cacheKey = `categoryTree_209_${currentLanguage}`;
|
||||||
|
if (windowObj && windowObj.productCache) {
|
||||||
|
windowObj.productCache[cacheKey] = {
|
||||||
|
categoryTree: null,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
fetching: false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -410,7 +466,7 @@ class CategoryList extends Component {
|
|||||||
zIndex: 2,
|
zIndex: 2,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Startseite
|
{this.props.t ? this.props.t('navigation.home') : 'Startseite'}
|
||||||
</Box>
|
</Box>
|
||||||
{/* Thin text (positioned on top) */}
|
{/* Thin text (positioned on top) */}
|
||||||
<Box
|
<Box
|
||||||
@@ -424,7 +480,7 @@ class CategoryList extends Component {
|
|||||||
zIndex: 1,
|
zIndex: 1,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Startseite
|
{this.props.t ? this.props.t('navigation.home') : 'Startseite'}
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
@@ -595,7 +651,10 @@ class CategoryList extends Component {
|
|||||||
onClick={this.handleMobileMenuToggle}
|
onClick={this.handleMobileMenuToggle}
|
||||||
role="button"
|
role="button"
|
||||||
tabIndex={0}
|
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) => {
|
onKeyDown={(e) => {
|
||||||
if (e.key === 'Enter' || e.key === ' ') {
|
if (e.key === 'Enter' || e.key === ' ') {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -607,7 +666,7 @@ class CategoryList extends Component {
|
|||||||
fontWeight: "bold",
|
fontWeight: "bold",
|
||||||
textShadow: "0 1px 2px rgba(0,0,0,0.3)"
|
textShadow: "0 1px 2px rgba(0,0,0,0.3)"
|
||||||
}}>
|
}}>
|
||||||
Kategorien
|
{this.props.t ? this.props.t('navigation.categories') : 'Kategorien'}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Box sx={{ display: "flex", alignItems: "center" }}>
|
<Box sx={{ display: "flex", alignItems: "center" }}>
|
||||||
{mobileMenuOpen ? <CloseIcon /> : <MenuIcon />}
|
{mobileMenuOpen ? <CloseIcon /> : <MenuIcon />}
|
||||||
@@ -628,4 +687,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 ListItemText from "@mui/material/ListItemText";
|
||||||
import Typography from "@mui/material/Typography";
|
import Typography from "@mui/material/Typography";
|
||||||
import CircularProgress from "@mui/material/CircularProgress";
|
import CircularProgress from "@mui/material/CircularProgress";
|
||||||
|
import IconButton from "@mui/material/IconButton";
|
||||||
import SearchIcon from "@mui/icons-material/Search";
|
import SearchIcon from "@mui/icons-material/Search";
|
||||||
|
import KeyboardReturnIcon from "@mui/icons-material/KeyboardReturn";
|
||||||
import { useNavigate, useLocation } from "react-router-dom";
|
import { useNavigate, useLocation } from "react-router-dom";
|
||||||
import SocketContext from "../../contexts/SocketContext.js";
|
import SocketContext from "../../contexts/SocketContext.js";
|
||||||
|
|
||||||
@@ -184,6 +186,15 @@ const SearchBar = () => {
|
|||||||
}, 200);
|
}, 200);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Handle enter icon click
|
||||||
|
const handleEnterClick = () => {
|
||||||
|
delete window.currentSearchQuery;
|
||||||
|
setShowSuggestions(false);
|
||||||
|
if (searchQuery.trim()) {
|
||||||
|
navigate(`/search?q=${encodeURIComponent(searchQuery)}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Clean up timers on unmount
|
// Clean up timers on unmount
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
return () => {
|
return () => {
|
||||||
@@ -244,9 +255,23 @@ const SearchBar = () => {
|
|||||||
<SearchIcon />
|
<SearchIcon />
|
||||||
</InputAdornment>
|
</InputAdornment>
|
||||||
),
|
),
|
||||||
endAdornment: loadingSuggestions && (
|
endAdornment: (
|
||||||
<InputAdornment position="end">
|
<InputAdornment position="end">
|
||||||
<CircularProgress size={16} />
|
{loadingSuggestions && <CircularProgress size={16} />}
|
||||||
|
<IconButton
|
||||||
|
size="small"
|
||||||
|
onClick={handleEnterClick}
|
||||||
|
sx={{
|
||||||
|
ml: loadingSuggestions ? 0.5 : 0,
|
||||||
|
p: 0.5,
|
||||||
|
color: "text.secondary",
|
||||||
|
"&:hover": {
|
||||||
|
color: "primary.main",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<KeyboardReturnIcon fontSize="small" />
|
||||||
|
</IconButton>
|
||||||
</InputAdornment>
|
</InputAdornment>
|
||||||
),
|
),
|
||||||
sx: { borderRadius: 2, bgcolor: "background.paper" },
|
sx: { borderRadius: 2, bgcolor: "background.paper" },
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { Box, TextField, Typography } from "@mui/material";
|
import { Box, TextField, Typography } from "@mui/material";
|
||||||
|
import { withI18n } from "../../i18n/withTranslation.js";
|
||||||
|
|
||||||
const AddressForm = ({ title, address, onChange, errors, namePrefix }) => {
|
const AddressForm = ({ title, address, onChange, errors, namePrefix, t }) => {
|
||||||
// Helper function to determine if a required field should show error styling
|
// Helper function to determine if a required field should show error styling
|
||||||
const getRequiredFieldError = (fieldName, value) => {
|
const getRequiredFieldError = (fieldName, value) => {
|
||||||
const isEmpty = !value || value.trim() === "";
|
const isEmpty = !value || value.trim() === "";
|
||||||
@@ -36,7 +37,7 @@ const AddressForm = ({ title, address, onChange, errors, namePrefix }) => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<TextField
|
<TextField
|
||||||
label="Vorname"
|
label={t ? t('checkout.addressFields.firstName') : 'Vorname'}
|
||||||
name="firstName"
|
name="firstName"
|
||||||
value={address.firstName}
|
value={address.firstName}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
@@ -49,7 +50,7 @@ const AddressForm = ({ title, address, onChange, errors, namePrefix }) => {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<TextField
|
<TextField
|
||||||
label="Nachname"
|
label={t ? t('checkout.addressFields.lastName') : 'Nachname'}
|
||||||
name="lastName"
|
name="lastName"
|
||||||
value={address.lastName}
|
value={address.lastName}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
@@ -62,7 +63,7 @@ const AddressForm = ({ title, address, onChange, errors, namePrefix }) => {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<TextField
|
<TextField
|
||||||
label="Adresszusatz"
|
label={t ? t('checkout.addressFields.addressSupplement') : 'Adresszusatz'}
|
||||||
name="addressAddition"
|
name="addressAddition"
|
||||||
value={address.addressAddition || ""}
|
value={address.addressAddition || ""}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
@@ -70,7 +71,7 @@ const AddressForm = ({ title, address, onChange, errors, namePrefix }) => {
|
|||||||
InputLabelProps={{ shrink: true }}
|
InputLabelProps={{ shrink: true }}
|
||||||
/>
|
/>
|
||||||
<TextField
|
<TextField
|
||||||
label="Straße"
|
label={t ? t('checkout.addressFields.street') : 'Straße'}
|
||||||
name="street"
|
name="street"
|
||||||
value={address.street}
|
value={address.street}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
@@ -83,7 +84,7 @@ const AddressForm = ({ title, address, onChange, errors, namePrefix }) => {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<TextField
|
<TextField
|
||||||
label="Hausnummer"
|
label={t ? t('checkout.addressFields.houseNumber') : 'Hausnummer'}
|
||||||
name="houseNumber"
|
name="houseNumber"
|
||||||
value={address.houseNumber}
|
value={address.houseNumber}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
@@ -96,7 +97,7 @@ const AddressForm = ({ title, address, onChange, errors, namePrefix }) => {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<TextField
|
<TextField
|
||||||
label="PLZ"
|
label={t ? t('checkout.addressFields.postalCode') : 'PLZ'}
|
||||||
name="postalCode"
|
name="postalCode"
|
||||||
value={address.postalCode}
|
value={address.postalCode}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
@@ -109,7 +110,7 @@ const AddressForm = ({ title, address, onChange, errors, namePrefix }) => {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<TextField
|
<TextField
|
||||||
label="Stadt"
|
label={t ? t('checkout.addressFields.city') : 'Stadt'}
|
||||||
name="city"
|
name="city"
|
||||||
value={address.city}
|
value={address.city}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
@@ -122,7 +123,7 @@ const AddressForm = ({ title, address, onChange, errors, namePrefix }) => {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<TextField
|
<TextField
|
||||||
label="Land"
|
label={t ? t('checkout.addressFields.country') : 'Land'}
|
||||||
name="country"
|
name="country"
|
||||||
value={address.country}
|
value={address.country}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
@@ -135,4 +136,4 @@ const AddressForm = ({ title, address, onChange, errors, namePrefix }) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default AddressForm;
|
export default withI18n()(AddressForm);
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import PaymentConfirmationDialog from "./PaymentConfirmationDialog.js";
|
|||||||
import OrderProcessingService from "./OrderProcessingService.js";
|
import OrderProcessingService from "./OrderProcessingService.js";
|
||||||
import CheckoutValidation from "./CheckoutValidation.js";
|
import CheckoutValidation from "./CheckoutValidation.js";
|
||||||
import SocketContext from "../../contexts/SocketContext.js";
|
import SocketContext from "../../contexts/SocketContext.js";
|
||||||
|
import { withI18n } from "../../i18n/index.js";
|
||||||
|
|
||||||
class CartTab extends Component {
|
class CartTab extends Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
@@ -116,7 +117,7 @@ class CartTab extends Component {
|
|||||||
// Determine payment method - respect constraints
|
// Determine payment method - respect constraints
|
||||||
let prefillPaymentMethod = template.payment_method || "wire";
|
let prefillPaymentMethod = template.payment_method || "wire";
|
||||||
const paymentMethodMap = {
|
const paymentMethodMap = {
|
||||||
"credit_card": "stripe",
|
"credit_card": "mollie",/*stripe*/
|
||||||
"bank_transfer": "wire",
|
"bank_transfer": "wire",
|
||||||
"cash_on_delivery": "onDelivery",
|
"cash_on_delivery": "onDelivery",
|
||||||
"cash": "cash"
|
"cash": "cash"
|
||||||
@@ -292,7 +293,7 @@ class CartTab extends Component {
|
|||||||
};
|
};
|
||||||
|
|
||||||
validateAddressForm = () => {
|
validateAddressForm = () => {
|
||||||
const errors = CheckoutValidation.validateAddressForm(this.state);
|
const errors = CheckoutValidation.validateAddressForm(this.state, this.props.t);
|
||||||
this.setState({ addressFormErrors: errors });
|
this.setState({ addressFormErrors: errors });
|
||||||
return Object.keys(errors).length === 0;
|
return Object.keys(errors).length === 0;
|
||||||
};
|
};
|
||||||
@@ -322,7 +323,7 @@ class CartTab extends Component {
|
|||||||
handleCompleteOrder = () => {
|
handleCompleteOrder = () => {
|
||||||
this.setState({ completionError: null }); // Clear previous errors
|
this.setState({ completionError: null }); // Clear previous errors
|
||||||
|
|
||||||
const validationError = CheckoutValidation.getValidationErrorMessage(this.state);
|
const validationError = CheckoutValidation.getValidationErrorMessage(this.state, false, this.props.t);
|
||||||
if (validationError) {
|
if (validationError) {
|
||||||
this.setState({ completionError: validationError });
|
this.setState({ completionError: validationError });
|
||||||
this.validateAddressForm(); // To show field-specific errors
|
this.validateAddressForm(); // To show field-specific errors
|
||||||
@@ -363,6 +364,40 @@ class CartTab extends Component {
|
|||||||
this.orderService.createStripeIntent(totalAmount, this.loadStripeComponent);
|
this.orderService.createStripeIntent(totalAmount, this.loadStripeComponent);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// Handle molllie payment differently
|
||||||
|
if (paymentMethod === "mollie") {
|
||||||
|
// Store the cart items used for mollie payment in sessionStorage for later reference
|
||||||
|
try {
|
||||||
|
sessionStorage.setItem('molliePaymentCart', JSON.stringify(cartItems));
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to store mollie payment cart:", error);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate total amount for mollie
|
||||||
|
const subtotal = cartItems.reduce(
|
||||||
|
(total, item) => total + item.price * item.quantity,
|
||||||
|
0
|
||||||
|
);
|
||||||
|
const totalAmount = Math.round((subtotal + deliveryCost) * 100) / 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);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Handle regular orders
|
// Handle regular orders
|
||||||
const orderData = {
|
const orderData = {
|
||||||
@@ -405,7 +440,7 @@ class CartTab extends Component {
|
|||||||
const deliveryCost = this.orderService.getDeliveryCost();
|
const deliveryCost = this.orderService.getDeliveryCost();
|
||||||
const { isPickupOnly, hasStecklinge } = CheckoutValidation.getCartItemFlags(cartItems);
|
const { isPickupOnly, hasStecklinge } = CheckoutValidation.getCartItemFlags(cartItems);
|
||||||
|
|
||||||
const preSubmitError = CheckoutValidation.getValidationErrorMessage(this.state);
|
const preSubmitError = CheckoutValidation.getValidationErrorMessage(this.state, false, this.props.t);
|
||||||
const displayError = completionError || preSubmitError;
|
const displayError = completionError || preSubmitError;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -445,7 +480,7 @@ class CartTab extends Component {
|
|||||||
{isLoadingStripe ? (
|
{isLoadingStripe ? (
|
||||||
<Box sx={{ textAlign: 'center', py: 4 }}>
|
<Box sx={{ textAlign: 'center', py: 4 }}>
|
||||||
<Typography variant="body1">
|
<Typography variant="body1">
|
||||||
Zahlungskomponente wird geladen...
|
{this.props.t ? this.props.t('payment.loadingPaymentComponent') : 'Zahlungskomponente wird geladen...'}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
) : showStripePayment && StripeComponent ? (
|
) : showStripePayment && StripeComponent ? (
|
||||||
@@ -463,7 +498,7 @@ class CartTab extends Component {
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
← Zurück zur Bestellung
|
{this.props.t ? this.props.t('cart.backToOrder') : '← Zurück zur Bestellung'}
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
<StripeComponent clientSecret={stripeClientSecret} />
|
<StripeComponent clientSecret={stripeClientSecret} />
|
||||||
@@ -507,4 +542,4 @@ class CartTab extends Component {
|
|||||||
// Set static contextType to access the socket
|
// Set static contextType to access the socket
|
||||||
CartTab.contextType = SocketContext;
|
CartTab.contextType = SocketContext;
|
||||||
|
|
||||||
export default CartTab;
|
export default withI18n()(CartTab);
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import AddressForm from "./AddressForm.js";
|
|||||||
import DeliveryMethodSelector from "./DeliveryMethodSelector.js";
|
import DeliveryMethodSelector from "./DeliveryMethodSelector.js";
|
||||||
import PaymentMethodSelector from "./PaymentMethodSelector.js";
|
import PaymentMethodSelector from "./PaymentMethodSelector.js";
|
||||||
import OrderSummary from "./OrderSummary.js";
|
import OrderSummary from "./OrderSummary.js";
|
||||||
|
import { withI18n } from "../../i18n/withTranslation.js";
|
||||||
|
|
||||||
class CheckoutForm extends Component {
|
class CheckoutForm extends Component {
|
||||||
render() {
|
render() {
|
||||||
@@ -40,7 +41,7 @@ class CheckoutForm extends Component {
|
|||||||
{paymentMethod !== "cash" && (
|
{paymentMethod !== "cash" && (
|
||||||
<>
|
<>
|
||||||
<AddressForm
|
<AddressForm
|
||||||
title="Rechnungsadresse"
|
title={this.props.t ? this.props.t('checkout.invoiceAddress') : 'Rechnungsadresse'}
|
||||||
address={invoiceAddress}
|
address={invoiceAddress}
|
||||||
onChange={onInvoiceAddressChange}
|
onChange={onInvoiceAddressChange}
|
||||||
errors={addressFormErrors}
|
errors={addressFormErrors}
|
||||||
@@ -57,7 +58,7 @@ class CheckoutForm extends Component {
|
|||||||
}
|
}
|
||||||
label={
|
label={
|
||||||
<Typography variant="body2">
|
<Typography variant="body2">
|
||||||
Für zukünftige Bestellungen speichern
|
{this.props.t ? this.props.t('checkout.saveForFuture') : 'Für zukünftige Bestellungen speichern'}
|
||||||
</Typography>
|
</Typography>
|
||||||
}
|
}
|
||||||
sx={{ mb: 2 }}
|
sx={{ mb: 2 }}
|
||||||
@@ -70,13 +71,12 @@ class CheckoutForm extends Component {
|
|||||||
variant="body1"
|
variant="body1"
|
||||||
sx={{ mb: 2, fontWeight: "bold", color: "#2e7d32" }}
|
sx={{ mb: 2, fontWeight: "bold", color: "#2e7d32" }}
|
||||||
>
|
>
|
||||||
Für welchen Termin ist die Abholung der Stecklinge
|
{this.props.t ? this.props.t('checkout.pickupDate') : 'Für welchen Termin ist die Abholung der Stecklinge gewünscht?'}
|
||||||
gewünscht?
|
|
||||||
</Typography>
|
</Typography>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<TextField
|
<TextField
|
||||||
label="Anmerkung"
|
label={this.props.t ? this.props.t('checkout.note') : 'Anmerkung'}
|
||||||
name="note"
|
name="note"
|
||||||
value={note}
|
value={note}
|
||||||
onChange={onNoteChange}
|
onChange={onNoteChange}
|
||||||
@@ -93,6 +93,7 @@ class CheckoutForm extends Component {
|
|||||||
deliveryMethod={deliveryMethod}
|
deliveryMethod={deliveryMethod}
|
||||||
onChange={onDeliveryMethodChange}
|
onChange={onDeliveryMethodChange}
|
||||||
isPickupOnly={isPickupOnly || hasStecklinge}
|
isPickupOnly={isPickupOnly || hasStecklinge}
|
||||||
|
cartItems={cartItems}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{(deliveryMethod === "DHL" || deliveryMethod === "DPD") && (
|
{(deliveryMethod === "DHL" || deliveryMethod === "DPD") && (
|
||||||
@@ -107,7 +108,7 @@ class CheckoutForm extends Component {
|
|||||||
}
|
}
|
||||||
label={
|
label={
|
||||||
<Typography variant="body1">
|
<Typography variant="body1">
|
||||||
Lieferadresse ist identisch mit Rechnungsadresse
|
{this.props.t ? this.props.t('checkout.sameAddress') : 'Lieferadresse ist identisch mit Rechnungsadresse'}
|
||||||
</Typography>
|
</Typography>
|
||||||
}
|
}
|
||||||
sx={{ mb: 2 }}
|
sx={{ mb: 2 }}
|
||||||
@@ -115,7 +116,7 @@ class CheckoutForm extends Component {
|
|||||||
|
|
||||||
{!useSameAddress && (
|
{!useSameAddress && (
|
||||||
<AddressForm
|
<AddressForm
|
||||||
title="Lieferadresse"
|
title={this.props.t ? this.props.t('checkout.deliveryAddress') : 'Lieferadresse'}
|
||||||
address={deliveryAddress}
|
address={deliveryAddress}
|
||||||
onChange={onDeliveryAddressChange}
|
onChange={onDeliveryAddressChange}
|
||||||
errors={addressFormErrors}
|
errors={addressFormErrors}
|
||||||
@@ -150,8 +151,7 @@ class CheckoutForm extends Component {
|
|||||||
}
|
}
|
||||||
label={
|
label={
|
||||||
<Typography variant="body2">
|
<Typography variant="body2">
|
||||||
Ich habe die AGBs, die Datenschutzerklärung und die
|
{this.props.t ? this.props.t('checkout.termsAccept') : 'Ich habe die AGBs, die Datenschutzerklärung und die Bestimmungen zum Widerrufsrecht gelesen'}
|
||||||
Bestimmungen zum Widerrufsrecht gelesen
|
|
||||||
</Typography>
|
</Typography>
|
||||||
}
|
}
|
||||||
sx={{ mb: 3, mt: 2 }}
|
sx={{ mb: 3, mt: 2 }}
|
||||||
@@ -174,12 +174,12 @@ class CheckoutForm extends Component {
|
|||||||
disabled={isCompletingOrder || !!preSubmitError}
|
disabled={isCompletingOrder || !!preSubmitError}
|
||||||
>
|
>
|
||||||
{isCompletingOrder
|
{isCompletingOrder
|
||||||
? "Bestellung wird verarbeitet..."
|
? (this.props.t ? this.props.t('checkout.processingOrder') : 'Bestellung wird verarbeitet...')
|
||||||
: "Bestellung abschließen"}
|
: (this.props.t ? this.props.t('checkout.completeOrder') : 'Bestellung abschließen')}
|
||||||
</Button>
|
</Button>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default CheckoutForm;
|
export default withI18n()(CheckoutForm);
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
class CheckoutValidation {
|
class CheckoutValidation {
|
||||||
static validateAddressForm(state) {
|
static validateAddressForm(state, t = null) {
|
||||||
const {
|
const {
|
||||||
invoiceAddress,
|
invoiceAddress,
|
||||||
deliveryAddress,
|
deliveryAddress,
|
||||||
@@ -12,15 +12,15 @@ class CheckoutValidation {
|
|||||||
// Validate invoice address (skip if payment method is "cash")
|
// Validate invoice address (skip if payment method is "cash")
|
||||||
if (paymentMethod !== "cash") {
|
if (paymentMethod !== "cash") {
|
||||||
if (!invoiceAddress.firstName)
|
if (!invoiceAddress.firstName)
|
||||||
errors.invoiceFirstName = "Vorname erforderlich";
|
errors.invoiceFirstName = t ? t('checkout.validationErrors.firstNameRequired') : "Vorname erforderlich";
|
||||||
if (!invoiceAddress.lastName)
|
if (!invoiceAddress.lastName)
|
||||||
errors.invoiceLastName = "Nachname erforderlich";
|
errors.invoiceLastName = t ? t('checkout.validationErrors.lastNameRequired') : "Nachname erforderlich";
|
||||||
if (!invoiceAddress.street) errors.invoiceStreet = "Straße erforderlich";
|
if (!invoiceAddress.street) errors.invoiceStreet = t ? t('checkout.validationErrors.streetRequired') : "Straße erforderlich";
|
||||||
if (!invoiceAddress.houseNumber)
|
if (!invoiceAddress.houseNumber)
|
||||||
errors.invoiceHouseNumber = "Hausnummer erforderlich";
|
errors.invoiceHouseNumber = t ? t('checkout.validationErrors.houseNumberRequired') : "Hausnummer erforderlich";
|
||||||
if (!invoiceAddress.postalCode)
|
if (!invoiceAddress.postalCode)
|
||||||
errors.invoicePostalCode = "PLZ erforderlich";
|
errors.invoicePostalCode = t ? t('checkout.validationErrors.postalCodeRequired') : "PLZ erforderlich";
|
||||||
if (!invoiceAddress.city) errors.invoiceCity = "Stadt erforderlich";
|
if (!invoiceAddress.city) errors.invoiceCity = t ? t('checkout.validationErrors.cityRequired') : "Stadt erforderlich";
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate delivery address for shipping methods that require it
|
// Validate delivery address for shipping methods that require it
|
||||||
@@ -29,37 +29,37 @@ class CheckoutValidation {
|
|||||||
(deliveryMethod === "DHL" || deliveryMethod === "DPD")
|
(deliveryMethod === "DHL" || deliveryMethod === "DPD")
|
||||||
) {
|
) {
|
||||||
if (!deliveryAddress.firstName)
|
if (!deliveryAddress.firstName)
|
||||||
errors.deliveryFirstName = "Vorname erforderlich";
|
errors.deliveryFirstName = t ? t('checkout.validationErrors.firstNameRequired') : "Vorname erforderlich";
|
||||||
if (!deliveryAddress.lastName)
|
if (!deliveryAddress.lastName)
|
||||||
errors.deliveryLastName = "Nachname erforderlich";
|
errors.deliveryLastName = t ? t('checkout.validationErrors.lastNameRequired') : "Nachname erforderlich";
|
||||||
if (!deliveryAddress.street)
|
if (!deliveryAddress.street)
|
||||||
errors.deliveryStreet = "Straße erforderlich";
|
errors.deliveryStreet = t ? t('checkout.validationErrors.streetRequired') : "Straße erforderlich";
|
||||||
if (!deliveryAddress.houseNumber)
|
if (!deliveryAddress.houseNumber)
|
||||||
errors.deliveryHouseNumber = "Hausnummer erforderlich";
|
errors.deliveryHouseNumber = t ? t('checkout.validationErrors.houseNumberRequired') : "Hausnummer erforderlich";
|
||||||
if (!deliveryAddress.postalCode)
|
if (!deliveryAddress.postalCode)
|
||||||
errors.deliveryPostalCode = "PLZ erforderlich";
|
errors.deliveryPostalCode = t ? t('checkout.validationErrors.postalCodeRequired') : "PLZ erforderlich";
|
||||||
if (!deliveryAddress.city) errors.deliveryCity = "Stadt erforderlich";
|
if (!deliveryAddress.city) errors.deliveryCity = t ? t('checkout.validationErrors.cityRequired') : "Stadt erforderlich";
|
||||||
}
|
}
|
||||||
|
|
||||||
return errors;
|
return errors;
|
||||||
}
|
}
|
||||||
|
|
||||||
static getValidationErrorMessage(state, isAddressOnly = false) {
|
static getValidationErrorMessage(state, isAddressOnly = false, t = null) {
|
||||||
const { termsAccepted } = state;
|
const { termsAccepted } = state;
|
||||||
|
|
||||||
const addressErrors = this.validateAddressForm(state);
|
const addressErrors = this.validateAddressForm(state, t);
|
||||||
|
|
||||||
if (isAddressOnly) {
|
if (isAddressOnly) {
|
||||||
return addressErrors;
|
return addressErrors;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Object.keys(addressErrors).length > 0) {
|
if (Object.keys(addressErrors).length > 0) {
|
||||||
return "Bitte überprüfen Sie Ihre Eingaben in den Adressfeldern.";
|
return t ? t('checkout.addressValidationError') : "Bitte überprüfen Sie Ihre Eingaben in den Adressfeldern.";
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate terms acceptance
|
// Validate terms acceptance
|
||||||
if (!termsAccepted) {
|
if (!termsAccepted) {
|
||||||
return "Bitte akzeptieren Sie die AGBs, Datenschutzerklärung und Widerrufsrecht, um fortzufahren.";
|
return t ? t('checkout.termsValidationError') : "Bitte akzeptieren Sie die AGBs, Datenschutzerklärung und Widerrufsrecht, um fortzufahren.";
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
@@ -82,7 +82,7 @@ class CheckoutValidation {
|
|||||||
|
|
||||||
// Prefer stripe when available and meets minimum amount
|
// Prefer stripe when available and meets minimum amount
|
||||||
if (deliveryMethod === "DHL" || deliveryMethod === "DPD" || deliveryMethod === "Abholung") {
|
if (deliveryMethod === "DHL" || deliveryMethod === "DPD" || deliveryMethod === "Abholung") {
|
||||||
return "stripe";
|
return "wire";/*stripe*/
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fall back to wire transfer
|
// Fall back to wire transfer
|
||||||
@@ -106,11 +106,21 @@ class CheckoutValidation {
|
|||||||
newPaymentMethod = "wire";
|
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
|
// Check minimum amount for stripe payments
|
||||||
if (paymentMethod === "stripe" && totalAmount < 0.50) {
|
if (paymentMethod === "stripe" && totalAmount < 0.50) {
|
||||||
newPaymentMethod = "wire";
|
newPaymentMethod = "wire";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check minimum amount for mollie payments
|
||||||
|
if (paymentMethod === "mollie" && totalAmount < 0.50) {
|
||||||
|
newPaymentMethod = "wire";
|
||||||
|
}
|
||||||
|
|
||||||
if (deliveryMethod !== "Abholung" && paymentMethod === "cash") {
|
if (deliveryMethod !== "Abholung" && paymentMethod === "cash") {
|
||||||
newPaymentMethod = "wire";
|
newPaymentMethod = "wire";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,34 +3,42 @@ import Box from '@mui/material/Box';
|
|||||||
import Typography from '@mui/material/Typography';
|
import Typography from '@mui/material/Typography';
|
||||||
import Radio from '@mui/material/Radio';
|
import Radio from '@mui/material/Radio';
|
||||||
import Checkbox from '@mui/material/Checkbox';
|
import Checkbox from '@mui/material/Checkbox';
|
||||||
|
import { withI18n } from '../../i18n/withTranslation.js';
|
||||||
|
|
||||||
|
const DeliveryMethodSelector = ({ deliveryMethod, onChange, isPickupOnly, cartItems = [], t }) => {
|
||||||
|
// Calculate cart value for free shipping threshold
|
||||||
|
const cartValue = cartItems.reduce((total, item) => total + item.price * item.quantity, 0);
|
||||||
|
const isFreeShipping = cartValue >= 100;
|
||||||
|
const remainingForFreeShipping = Math.max(0, 100 - cartValue);
|
||||||
|
|
||||||
const DeliveryMethodSelector = ({ deliveryMethod, onChange, isPickupOnly }) => {
|
|
||||||
const deliveryOptions = [
|
const deliveryOptions = [
|
||||||
{
|
{
|
||||||
id: 'DHL',
|
id: 'DHL',
|
||||||
name: 'DHL',
|
name: 'DHL',
|
||||||
description: isPickupOnly ? "nicht auswählbar weil ein oder mehrere Artikel nur abgeholt werden können" : 'Standardversand',
|
description: isPickupOnly ? (t ? t('delivery.descriptions.notAvailable') : "nicht auswählbar weil ein oder mehrere Artikel nur abgeholt werden können") :
|
||||||
price: '6,99 €',
|
isFreeShipping ? (t ? t('delivery.descriptions.standardFree') : 'Standardversand - KOSTENLOS ab 100€ Warenwert!') : (t ? t('delivery.descriptions.standard') : 'Standardversand'),
|
||||||
|
price: isFreeShipping ? (t ? t('delivery.prices.free') : 'kostenlos') : (t ? t('delivery.prices.dhl') : '6,99 €'),
|
||||||
disabled: isPickupOnly
|
disabled: isPickupOnly
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'DPD',
|
id: 'DPD',
|
||||||
name: 'DPD',
|
name: 'DPD',
|
||||||
description: isPickupOnly ? "nicht auswählbar weil ein oder mehrere Artikel nur abgeholt werden können" : 'Standardversand',
|
description: isPickupOnly ? (t ? t('delivery.descriptions.notAvailable') : "nicht auswählbar weil ein oder mehrere Artikel nur abgeholt werden können") :
|
||||||
price: '4,90 €',
|
isFreeShipping ? (t ? t('delivery.descriptions.standardFree') : 'Standardversand - KOSTENLOS ab 100€ Warenwert!') : (t ? t('delivery.descriptions.standard') : 'Standardversand'),
|
||||||
|
price: isFreeShipping ? (t ? t('delivery.prices.free') : 'kostenlos') : (t ? t('delivery.prices.dpd') : '4,90 €'),
|
||||||
disabled: isPickupOnly
|
disabled: isPickupOnly
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'Sperrgut',
|
id: 'Sperrgut',
|
||||||
name: 'Sperrgut',
|
name: t ? t('delivery.methods.sperrgutName') : 'Sperrgut',
|
||||||
description: 'Für große und schwere Artikel',
|
description: t ? t('delivery.descriptions.bulky') : 'Für große und schwere Artikel',
|
||||||
price: '28,99 €',
|
price: t ? t('delivery.prices.sperrgut') : '28,99 €',
|
||||||
disabled: true,
|
disabled: true,
|
||||||
isCheckbox: true
|
isCheckbox: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'Abholung',
|
id: 'Abholung',
|
||||||
name: 'Abholung in der Filiale',
|
name: t ? t('delivery.methods.pickup') : 'Abholung in der Filiale',
|
||||||
description: '',
|
description: '',
|
||||||
price: ''
|
price: ''
|
||||||
}
|
}
|
||||||
@@ -39,7 +47,7 @@ const DeliveryMethodSelector = ({ deliveryMethod, onChange, isPickupOnly }) => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Typography variant="h6" gutterBottom>
|
<Typography variant="h6" gutterBottom>
|
||||||
Versandart wählen
|
{t ? t('delivery.selector.title') : 'Versandart wählen'}
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
<Box sx={{ mb: 3 }}>
|
<Box sx={{ mb: 3 }}>
|
||||||
@@ -114,9 +122,44 @@ const DeliveryMethodSelector = ({ deliveryMethod, onChange, isPickupOnly }) => {
|
|||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
|
{/* Free shipping information */}
|
||||||
|
{!isFreeShipping && remainingForFreeShipping > 0 && (
|
||||||
|
<Box sx={{
|
||||||
|
mt: 2,
|
||||||
|
p: 2,
|
||||||
|
backgroundColor: '#f0f8ff',
|
||||||
|
borderRadius: 1,
|
||||||
|
border: '1px solid #2196f3'
|
||||||
|
}}>
|
||||||
|
<Typography variant="body2" color="primary" sx={{ fontWeight: 'medium' }}>
|
||||||
|
{t ? t('delivery.selector.freeShippingInfo') : '💡 Versandkostenfrei ab 100€ Warenwert!'}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" color="text.secondary">
|
||||||
|
{t ? t('delivery.selector.remainingForFree', { amount: remainingForFreeShipping.toFixed(2).replace('.', ',') }) : `Noch ${remainingForFreeShipping.toFixed(2).replace('.', ',')}€ für kostenlosen Versand hinzufügen.`}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{isFreeShipping && (
|
||||||
|
<Box sx={{
|
||||||
|
mt: 2,
|
||||||
|
p: 2,
|
||||||
|
backgroundColor: '#e8f5e8',
|
||||||
|
borderRadius: 1,
|
||||||
|
border: '1px solid #2e7d32'
|
||||||
|
}}>
|
||||||
|
<Typography variant="body2" color="success.main" sx={{ fontWeight: 'medium' }}>
|
||||||
|
{t ? t('delivery.selector.congratsFreeShipping') : '🎉 Glückwunsch! Sie erhalten kostenlosen Versand!'}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" color="text.secondary">
|
||||||
|
{t ? t('delivery.selector.cartQualifiesFree', { amount: cartValue.toFixed(2).replace('.', ',') }) : `Ihr Warenkorb von ${cartValue.toFixed(2).replace('.', ',')}€ qualifiziert sich für kostenlosen Versand.`}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default DeliveryMethodSelector;
|
export default withI18n()(DeliveryMethodSelector);
|
||||||
@@ -15,22 +15,36 @@ import {
|
|||||||
TableRow,
|
TableRow,
|
||||||
Paper
|
Paper
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
const OrderDetailsDialog = ({ open, onClose, order }) => {
|
const OrderDetailsDialog = ({ open, onClose, order }) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
if (!order) {
|
if (!order) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const currencyFormatter = new Intl.NumberFormat("de-DE", { style: "currency", currency: "EUR" });
|
const currencyFormatter = new Intl.NumberFormat("de-DE", { style: "currency", currency: "EUR" });
|
||||||
|
|
||||||
|
// Helper function to translate payment methods
|
||||||
|
const getPaymentMethodDisplay = (paymentMethod) => {
|
||||||
|
if (!paymentMethod) return t('orders.details.notSpecified');
|
||||||
|
|
||||||
|
switch (paymentMethod.toLowerCase()) {
|
||||||
|
case 'wire':
|
||||||
|
return t('payment.methods.bankTransfer');
|
||||||
|
default:
|
||||||
|
return paymentMethod;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleCancelOrder = () => {
|
const handleCancelOrder = () => {
|
||||||
// Implement order cancellation logic here
|
// Implement order cancellation logic here
|
||||||
console.log(`Cancel order: ${order.orderId}`);
|
console.log(`Cancel order: ${order.orderId}`);
|
||||||
onClose(); // Close the dialog after action
|
onClose(); // Close the dialog after action
|
||||||
};
|
};
|
||||||
|
|
||||||
const subtotal = order.items.reduce((acc, item) => acc + item.price * item.quantity_ordered, 0);
|
const total = order.items.reduce((acc, item) => acc + item.price * item.quantity_ordered, 0);
|
||||||
const total = subtotal + order.delivery_cost;
|
|
||||||
|
|
||||||
// Calculate VAT breakdown similar to CartDropdown
|
// Calculate VAT breakdown similar to CartDropdown
|
||||||
const vatCalculations = order.items.reduce((acc, item) => {
|
const vatCalculations = order.items.reduce((acc, item) => {
|
||||||
@@ -52,10 +66,10 @@ const OrderDetailsDialog = ({ open, onClose, order }) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onClose={onClose} maxWidth="md" fullWidth>
|
<Dialog open={open} onClose={onClose} maxWidth="md" fullWidth>
|
||||||
<DialogTitle>Bestelldetails: {order.orderId}</DialogTitle>
|
<DialogTitle>{t('orders.details.title', { orderId: order.orderId })}</DialogTitle>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<Box sx={{ mb: 2 }}>
|
<Box sx={{ mb: 2 }}>
|
||||||
<Typography variant="h6">Lieferadresse</Typography>
|
<Typography variant="h6">{t('orders.details.deliveryAddress')}</Typography>
|
||||||
<Typography>{order.shipping_address_name}</Typography>
|
<Typography>{order.shipping_address_name}</Typography>
|
||||||
<Typography>{order.shipping_address_street} {order.shipping_address_house_number}</Typography>
|
<Typography>{order.shipping_address_street} {order.shipping_address_house_number}</Typography>
|
||||||
<Typography>{order.shipping_address_postal_code} {order.shipping_address_city}</Typography>
|
<Typography>{order.shipping_address_postal_code} {order.shipping_address_city}</Typography>
|
||||||
@@ -63,7 +77,7 @@ const OrderDetailsDialog = ({ open, onClose, order }) => {
|
|||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Box sx={{ mb: 2 }}>
|
<Box sx={{ mb: 2 }}>
|
||||||
<Typography variant="h6">Rechnungsadresse</Typography>
|
<Typography variant="h6">{t('orders.details.invoiceAddress')}</Typography>
|
||||||
<Typography>{order.invoice_address_name}</Typography>
|
<Typography>{order.invoice_address_name}</Typography>
|
||||||
<Typography>{order.invoice_address_street} {order.invoice_address_house_number}</Typography>
|
<Typography>{order.invoice_address_street} {order.invoice_address_house_number}</Typography>
|
||||||
<Typography>{order.invoice_address_postal_code} {order.invoice_address_city}</Typography>
|
<Typography>{order.invoice_address_postal_code} {order.invoice_address_city}</Typography>
|
||||||
@@ -72,28 +86,29 @@ const OrderDetailsDialog = ({ open, onClose, order }) => {
|
|||||||
|
|
||||||
{/* Order Details Section */}
|
{/* Order Details Section */}
|
||||||
<Box sx={{ mb: 2 }}>
|
<Box sx={{ mb: 2 }}>
|
||||||
<Typography variant="h6" gutterBottom>Bestelldetails</Typography>
|
<Typography variant="h6" gutterBottom>{t('orders.details.orderDetails')}</Typography>
|
||||||
<Box sx={{ display: 'flex', gap: 4 }}>
|
<Box sx={{ display: 'flex', gap: 4 }}>
|
||||||
<Box>
|
<Box>
|
||||||
<Typography variant="body2" color="text.secondary">Lieferart:</Typography>
|
<Typography variant="body2" color="text.secondary">{t('orders.details.deliveryMethod')}</Typography>
|
||||||
<Typography variant="body1">{order.deliveryMethod || order.delivery_method || 'Nicht angegeben'}</Typography>
|
<Typography variant="body1">{order.deliveryMethod || order.delivery_method || t('orders.details.notSpecified')}</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
<Box>
|
<Box>
|
||||||
<Typography variant="body2" color="text.secondary">Zahlungsart:</Typography>
|
<Typography variant="body2" color="text.secondary">{t('orders.details.paymentMethod')}</Typography>
|
||||||
<Typography variant="body1">{order.paymentMethod || order.payment_method || 'Nicht angegeben'}</Typography>
|
<Typography variant="body1">{getPaymentMethodDisplay(order.paymentMethod || order.payment_method)}</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Typography variant="h6" gutterBottom>Bestellte Artikel</Typography>
|
<Typography variant="h6" gutterBottom>{t('orders.details.orderedItems')}</Typography>
|
||||||
<TableContainer component={Paper}>
|
<TableContainer component={Paper}>
|
||||||
<Table>
|
<Table>
|
||||||
<TableHead>
|
<TableHead>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell>Artikel</TableCell>
|
<TableCell>{t('orders.details.item')}</TableCell>
|
||||||
<TableCell align="right">Menge</TableCell>
|
<TableCell align="right">{t('orders.details.quantity')}</TableCell>
|
||||||
<TableCell align="right">Preis</TableCell>
|
<TableCell align="right">{t('orders.details.price')}</TableCell>
|
||||||
<TableCell align="right">Gesamt</TableCell>
|
<TableCell align="right">{t('product.vatShort')}</TableCell>
|
||||||
|
<TableCell align="right">{t('orders.details.total')}</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHead>
|
</TableHead>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
@@ -102,13 +117,13 @@ const OrderDetailsDialog = ({ open, onClose, order }) => {
|
|||||||
<TableCell>{item.name}</TableCell>
|
<TableCell>{item.name}</TableCell>
|
||||||
<TableCell align="right">{item.quantity_ordered}</TableCell>
|
<TableCell align="right">{item.quantity_ordered}</TableCell>
|
||||||
<TableCell align="right">{currencyFormatter.format(item.price)}</TableCell>
|
<TableCell align="right">{currencyFormatter.format(item.price)}</TableCell>
|
||||||
|
<TableCell align="right">{item.vat}%</TableCell>
|
||||||
<TableCell align="right">{currencyFormatter.format(item.price * item.quantity_ordered)}</TableCell>
|
<TableCell align="right">{currencyFormatter.format(item.price * item.quantity_ordered)}</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
))}
|
))}
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell colSpan={2} />
|
<TableCell colSpan={4} align="right">
|
||||||
<TableCell align="right">
|
<Typography fontWeight="bold">{t ? t('tax.totalNet') : 'Gesamtnettopreis'}</Typography>
|
||||||
<Typography fontWeight="bold">Gesamtnettopreis</Typography>
|
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell align="right">
|
<TableCell align="right">
|
||||||
<Typography fontWeight="bold">{currencyFormatter.format(vatCalculations.totalNet)}</Typography>
|
<Typography fontWeight="bold">{currencyFormatter.format(vatCalculations.totalNet)}</Typography>
|
||||||
@@ -116,36 +131,19 @@ const OrderDetailsDialog = ({ open, onClose, order }) => {
|
|||||||
</TableRow>
|
</TableRow>
|
||||||
{vatCalculations.vat7 > 0 && (
|
{vatCalculations.vat7 > 0 && (
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell colSpan={2} />
|
<TableCell colSpan={4} align="right">{t ? t('tax.vat7') : '7% Mehrwertsteuer'}</TableCell>
|
||||||
<TableCell align="right">7% Mehrwertsteuer</TableCell>
|
|
||||||
<TableCell align="right">{currencyFormatter.format(vatCalculations.vat7)}</TableCell>
|
<TableCell align="right">{currencyFormatter.format(vatCalculations.vat7)}</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
)}
|
)}
|
||||||
{vatCalculations.vat19 > 0 && (
|
{vatCalculations.vat19 > 0 && (
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell colSpan={2} />
|
<TableCell colSpan={4} align="right">{t ? t('tax.vat19') : '19% Mehrwertsteuer'}</TableCell>
|
||||||
<TableCell align="right">19% Mehrwertsteuer</TableCell>
|
|
||||||
<TableCell align="right">{currencyFormatter.format(vatCalculations.vat19)}</TableCell>
|
<TableCell align="right">{currencyFormatter.format(vatCalculations.vat19)}</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
)}
|
)}
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell colSpan={2} />
|
<TableCell colSpan={4} align="right">
|
||||||
<TableCell align="right">
|
<Typography fontWeight="bold">{t ? t('cart.summary.total') : 'Gesamtsumme'}</Typography>
|
||||||
<Typography fontWeight="bold">Zwischensumme</Typography>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell align="right">
|
|
||||||
<Typography fontWeight="bold">{currencyFormatter.format(subtotal)}</Typography>
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
<TableRow>
|
|
||||||
<TableCell colSpan={2} />
|
|
||||||
<TableCell align="right">Lieferkosten</TableCell>
|
|
||||||
<TableCell align="right">{currencyFormatter.format(order.delivery_cost)}</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
<TableRow>
|
|
||||||
<TableCell colSpan={2} />
|
|
||||||
<TableCell align="right">
|
|
||||||
<Typography fontWeight="bold">Gesamtsumme</Typography>
|
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell align="right">
|
<TableCell align="right">
|
||||||
<Typography fontWeight="bold">{currencyFormatter.format(total)}</Typography>
|
<Typography fontWeight="bold">{currencyFormatter.format(total)}</Typography>
|
||||||
@@ -159,10 +157,10 @@ const OrderDetailsDialog = ({ open, onClose, order }) => {
|
|||||||
<DialogActions>
|
<DialogActions>
|
||||||
{order.status === 'new' && (
|
{order.status === 'new' && (
|
||||||
<Button onClick={handleCancelOrder} color="error">
|
<Button onClick={handleCancelOrder} color="error">
|
||||||
Bestellung stornieren
|
{t('orders.details.cancelOrder')}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
<Button onClick={onClose}>Schließen</Button>
|
<Button onClick={onClose}>{t ? t('common.close') : 'Schließen'}</Button>
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -49,18 +49,29 @@ class OrderProcessingService {
|
|||||||
waitForVerifyTokenAndProcessOrder() {
|
waitForVerifyTokenAndProcessOrder() {
|
||||||
// Check if window.cart is already populated (verifyToken already completed)
|
// Check if window.cart is already populated (verifyToken already completed)
|
||||||
if (Array.isArray(window.cart) && window.cart.length > 0) {
|
if (Array.isArray(window.cart) && window.cart.length > 0) {
|
||||||
|
if (this.paymentCompletionData && this.paymentCompletionData.paymentType === 'mollie') {
|
||||||
|
this.processMollieOrderWithCart(window.cart);
|
||||||
|
} else {
|
||||||
this.processStripeOrderWithCart(window.cart);
|
this.processStripeOrderWithCart(window.cart);
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Listen for cart event which is dispatched after verifyToken completes
|
// Listen for cart event which is dispatched after verifyToken completes
|
||||||
this.verifyTokenHandler = () => {
|
this.verifyTokenHandler = () => {
|
||||||
if (Array.isArray(window.cart) && window.cart.length > 0) {
|
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
|
// Clear window.cart after copying
|
||||||
window.cart = [];
|
window.cart = [];
|
||||||
window.dispatchEvent(new CustomEvent("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 {
|
} else {
|
||||||
this.setState({
|
this.setState({
|
||||||
completionError: "Cart is empty. Please add items to your cart before placing an order."
|
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() {
|
processStripeOrder() {
|
||||||
// If no original cart items, don't process
|
// If no original cart items, don't process
|
||||||
if (!this.getState().originalCartItems || this.getState().originalCartItems.length === 0) {
|
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
|
// Process regular (non-Stripe) orders
|
||||||
processRegularOrder(orderData) {
|
processRegularOrder(orderData) {
|
||||||
const context = this.getContext();
|
const context = this.getContext();
|
||||||
@@ -271,9 +311,43 @@ class OrderProcessingService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Create Mollie payment intent
|
||||||
|
createMollieIntent(mollieOrderData) {
|
||||||
|
const context = this.getContext();
|
||||||
|
if (context && context.socket && context.socket.connected) {
|
||||||
|
context.socket.emit(
|
||||||
|
"createMollieIntent",
|
||||||
|
mollieOrderData,
|
||||||
|
(response) => {
|
||||||
|
if (response.success) {
|
||||||
|
// Store pending payment info and redirect
|
||||||
|
localStorage.setItem('pendingPayment', JSON.stringify({
|
||||||
|
paymentId: response.paymentId,
|
||||||
|
amount: mollieOrderData.amount,
|
||||||
|
timestamp: Date.now()
|
||||||
|
}));
|
||||||
|
window.location.href = response.checkoutUrl;
|
||||||
|
} else {
|
||||||
|
console.error("Error:", response.error);
|
||||||
|
this.setState({
|
||||||
|
isCompletingOrder: false,
|
||||||
|
completionError: response.error || "Failed to create Mollie payment intent. Please try again.",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
console.error("Socket context not available");
|
||||||
|
this.setState({
|
||||||
|
isCompletingOrder: false,
|
||||||
|
completionError: "Cannot connect to server. Please try again later.",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Calculate delivery cost
|
// Calculate delivery cost
|
||||||
getDeliveryCost() {
|
getDeliveryCost() {
|
||||||
const { deliveryMethod, paymentMethod } = this.getState();
|
const { deliveryMethod, paymentMethod, cartItems } = this.getState();
|
||||||
let cost = 0;
|
let cost = 0;
|
||||||
|
|
||||||
switch (deliveryMethod) {
|
switch (deliveryMethod) {
|
||||||
@@ -293,7 +367,16 @@ class OrderProcessingService {
|
|||||||
cost = 6.99;
|
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") {
|
if (paymentMethod === "onDelivery") {
|
||||||
cost += 8.99;
|
cost += 8.99;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,8 +5,10 @@ import Table from '@mui/material/Table';
|
|||||||
import TableBody from '@mui/material/TableBody';
|
import TableBody from '@mui/material/TableBody';
|
||||||
import TableCell from '@mui/material/TableCell';
|
import TableCell from '@mui/material/TableCell';
|
||||||
import TableRow from '@mui/material/TableRow';
|
import TableRow from '@mui/material/TableRow';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
const OrderSummary = ({ deliveryCost, cartItems = [] }) => {
|
const OrderSummary = ({ deliveryCost, cartItems = [] }) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
const currencyFormatter = new Intl.NumberFormat('de-DE', {
|
const currencyFormatter = new Intl.NumberFormat('de-DE', {
|
||||||
style: 'currency',
|
style: 'currency',
|
||||||
currency: 'EUR'
|
currency: 'EUR'
|
||||||
@@ -30,9 +32,9 @@ const OrderSummary = ({ deliveryCost, cartItems = [] }) => {
|
|||||||
return acc;
|
return acc;
|
||||||
}, { totalGross: 0, totalNet: 0, vat7: 0, vat19: 0 });
|
}, { totalGross: 0, totalNet: 0, vat7: 0, vat19: 0 });
|
||||||
|
|
||||||
// Calculate shipping VAT (19% VAT for shipping costs)
|
// Calculate shipping VAT (19% VAT for shipping costs) - only if there are shipping costs
|
||||||
const shippingNetPrice = deliveryCost / (1 + 19 / 100);
|
const shippingNetPrice = deliveryCost > 0 ? deliveryCost / (1 + 19 / 100) : 0;
|
||||||
const shippingVat = deliveryCost - shippingNetPrice;
|
const shippingVat = deliveryCost > 0 ? deliveryCost - shippingNetPrice : 0;
|
||||||
|
|
||||||
// Combine totals - add shipping VAT to the 19% VAT total
|
// Combine totals - add shipping VAT to the 19% VAT total
|
||||||
const totalVat7 = cartVatCalculations.vat7;
|
const totalVat7 = cartVatCalculations.vat7;
|
||||||
@@ -42,20 +44,20 @@ const OrderSummary = ({ deliveryCost, cartItems = [] }) => {
|
|||||||
return (
|
return (
|
||||||
<Box sx={{ my: 3, p: 2, bgcolor: '#f5f5f5', borderRadius: 1 }}>
|
<Box sx={{ my: 3, p: 2, bgcolor: '#f5f5f5', borderRadius: 1 }}>
|
||||||
<Typography variant="h6" gutterBottom>
|
<Typography variant="h6" gutterBottom>
|
||||||
Bestellübersicht
|
{t ? t('cart.summary.title') : 'Bestellübersicht'}
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
<Table size="small">
|
<Table size="small">
|
||||||
<TableBody>
|
<TableBody>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell>Waren (netto):</TableCell>
|
<TableCell>{t ? t('cart.summary.goodsNet') : 'Waren (netto):'}</TableCell>
|
||||||
<TableCell align="right">
|
<TableCell align="right">
|
||||||
{currencyFormatter.format(cartVatCalculations.totalNet)}
|
{currencyFormatter.format(cartVatCalculations.totalNet)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
{deliveryCost > 0 && (
|
{deliveryCost > 0 && (
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell>Versandkosten (netto):</TableCell>
|
<TableCell>{t ? t('cart.summary.shippingNet') : 'Versandkosten (netto):'}</TableCell>
|
||||||
<TableCell align="right">
|
<TableCell align="right">
|
||||||
{currencyFormatter.format(shippingNetPrice)}
|
{currencyFormatter.format(shippingNetPrice)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
@@ -63,7 +65,7 @@ const OrderSummary = ({ deliveryCost, cartItems = [] }) => {
|
|||||||
)}
|
)}
|
||||||
{totalVat7 > 0 && (
|
{totalVat7 > 0 && (
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell>7% Mehrwertsteuer:</TableCell>
|
<TableCell>{t ? t('tax.vat7') : '7% Mehrwertsteuer'}:</TableCell>
|
||||||
<TableCell align="right">
|
<TableCell align="right">
|
||||||
{currencyFormatter.format(totalVat7)}
|
{currencyFormatter.format(totalVat7)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
@@ -71,28 +73,37 @@ const OrderSummary = ({ deliveryCost, cartItems = [] }) => {
|
|||||||
)}
|
)}
|
||||||
{totalVat19 > 0 && (
|
{totalVat19 > 0 && (
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell>19% Mehrwertsteuer (inkl. Versand):</TableCell>
|
<TableCell>{t ? t('tax.vat19WithShipping') : '19% Mehrwertsteuer (inkl. Versand)'}:</TableCell>
|
||||||
<TableCell align="right">
|
<TableCell align="right">
|
||||||
{currencyFormatter.format(totalVat19)}
|
{currencyFormatter.format(totalVat19)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
)}
|
)}
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell sx={{ fontWeight: 'bold' }}>Gesamtsumme Waren:</TableCell>
|
<TableCell sx={{ fontWeight: 'bold' }}>{t ? t('cart.summary.totalGoods') : 'Gesamtsumme Waren:'}</TableCell>
|
||||||
<TableCell align="right" sx={{ fontWeight: 'bold' }}>
|
<TableCell align="right" sx={{ fontWeight: 'bold' }}>
|
||||||
{currencyFormatter.format(cartVatCalculations.totalGross)}
|
{currencyFormatter.format(cartVatCalculations.totalGross)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
{deliveryCost > 0 && (
|
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell sx={{ fontWeight: 'bold' }}>Versandkosten:</TableCell>
|
<TableCell sx={{ fontWeight: 'bold' }}>
|
||||||
|
{t ? t('cart.summary.shippingCosts') : 'Versandkosten:'}
|
||||||
|
{deliveryCost === 0 && cartVatCalculations.totalGross < 100 && (
|
||||||
|
<span style={{ color: '#2e7d32', fontSize: '0.8em', marginLeft: '4px' }}>
|
||||||
|
{t ? t('cart.summary.freeFrom100') : '(kostenlos ab 100€)'}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</TableCell>
|
||||||
<TableCell align="right" sx={{ fontWeight: 'bold' }}>
|
<TableCell align="right" sx={{ fontWeight: 'bold' }}>
|
||||||
{currencyFormatter.format(deliveryCost)}
|
{deliveryCost === 0 ? (
|
||||||
|
<span style={{ color: '#2e7d32' }}>{t ? t('cart.summary.free') : 'kostenlos'}</span>
|
||||||
|
) : (
|
||||||
|
currencyFormatter.format(deliveryCost)
|
||||||
|
)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
)}
|
|
||||||
<TableRow sx={{ borderTop: '1px solid #e0e0e0' }}>
|
<TableRow sx={{ borderTop: '1px solid #e0e0e0' }}>
|
||||||
<TableCell sx={{ fontWeight: 'bold', pt: 2 }}>Gesamtsumme:</TableCell>
|
<TableCell sx={{ fontWeight: 'bold', pt: 2 }}>{t ? t('cart.summary.total') : 'Gesamtsumme:'}</TableCell>
|
||||||
<TableCell align="right" sx={{ fontWeight: 'bold', pt: 2 }}>
|
<TableCell align="right" sx={{ fontWeight: 'bold', pt: 2 }}>
|
||||||
{currencyFormatter.format(totalGross)}
|
{currencyFormatter.format(totalGross)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import React, { useState, useEffect, useContext, useCallback } from "react";
|
import React, { useState, useEffect, useContext, useCallback } from "react";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import { withI18n } from "../../i18n/withTranslation.js";
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
Paper,
|
Paper,
|
||||||
@@ -14,19 +15,28 @@ import {
|
|||||||
Tooltip,
|
Tooltip,
|
||||||
CircularProgress,
|
CircularProgress,
|
||||||
Typography,
|
Typography,
|
||||||
|
Dialog,
|
||||||
|
DialogTitle,
|
||||||
|
DialogContent,
|
||||||
|
DialogActions,
|
||||||
|
Button,
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
import SearchIcon from "@mui/icons-material/Search";
|
import SearchIcon from "@mui/icons-material/Search";
|
||||||
|
import CancelIcon from "@mui/icons-material/Cancel";
|
||||||
import SocketContext from "../../contexts/SocketContext.js";
|
import SocketContext from "../../contexts/SocketContext.js";
|
||||||
import OrderDetailsDialog from "./OrderDetailsDialog.js";
|
import OrderDetailsDialog from "./OrderDetailsDialog.js";
|
||||||
|
|
||||||
// Constants
|
// Constants
|
||||||
const statusTranslations = {
|
const getStatusTranslation = (status, t) => {
|
||||||
new: "in Bearbeitung",
|
const statusMap = {
|
||||||
pending: "Neu",
|
new: t ? t('orders.status.new') : "in Bearbeitung",
|
||||||
processing: "in Bearbeitung",
|
pending: t ? t('orders.status.pending') : "Neu",
|
||||||
cancelled: "Storniert",
|
processing: t ? t('orders.status.processing') : "in Bearbeitung",
|
||||||
shipped: "Verschickt",
|
cancelled: t ? t('orders.status.cancelled') : "Storniert",
|
||||||
delivered: "Geliefert",
|
shipped: t ? t('orders.status.shipped') : "Verschickt",
|
||||||
|
delivered: t ? t('orders.status.delivered') : "Geliefert",
|
||||||
|
};
|
||||||
|
return statusMap[status] || status;
|
||||||
};
|
};
|
||||||
|
|
||||||
const statusEmojis = {
|
const statusEmojis = {
|
||||||
@@ -61,12 +71,15 @@ const currencyFormatter = new Intl.NumberFormat("de-DE", {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Orders Tab Content Component
|
// Orders Tab Content Component
|
||||||
const OrdersTab = ({ orderIdFromHash }) => {
|
const OrdersTab = ({ orderIdFromHash, t }) => {
|
||||||
const [orders, setOrders] = useState([]);
|
const [orders, setOrders] = useState([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [error, setError] = useState(null);
|
const [error, setError] = useState(null);
|
||||||
const [selectedOrder, setSelectedOrder] = useState(null);
|
const [selectedOrder, setSelectedOrder] = useState(null);
|
||||||
const [isDetailsDialogOpen, setIsDetailsDialogOpen] = useState(false);
|
const [isDetailsDialogOpen, setIsDetailsDialogOpen] = useState(false);
|
||||||
|
const [cancelConfirmOpen, setCancelConfirmOpen] = useState(false);
|
||||||
|
const [orderToCancel, setOrderToCancel] = useState(null);
|
||||||
|
const [isCancelling, setIsCancelling] = useState(false);
|
||||||
|
|
||||||
const {socket} = useContext(SocketContext);
|
const {socket} = useContext(SocketContext);
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@@ -77,9 +90,11 @@ const OrdersTab = ({ orderIdFromHash }) => {
|
|||||||
if (orderToView) {
|
if (orderToView) {
|
||||||
setSelectedOrder(orderToView);
|
setSelectedOrder(orderToView);
|
||||||
setIsDetailsDialogOpen(true);
|
setIsDetailsDialogOpen(true);
|
||||||
|
// Update the hash to include the order ID
|
||||||
|
navigate(`/profile#${orderId}`, { replace: true });
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[orders]
|
[orders, navigate]
|
||||||
);
|
);
|
||||||
|
|
||||||
const fetchOrders = useCallback(() => {
|
const fetchOrders = useCallback(() => {
|
||||||
@@ -120,7 +135,7 @@ const OrdersTab = ({ orderIdFromHash }) => {
|
|||||||
}, [orderIdFromHash, orders, handleViewDetails]);
|
}, [orderIdFromHash, orders, handleViewDetails]);
|
||||||
|
|
||||||
const getStatusDisplay = (status) => {
|
const getStatusDisplay = (status) => {
|
||||||
return statusTranslations[status] || status;
|
return getStatusTranslation(status, t);
|
||||||
};
|
};
|
||||||
|
|
||||||
const getStatusEmoji = (status) => {
|
const getStatusEmoji = (status) => {
|
||||||
@@ -134,7 +149,48 @@ const OrdersTab = ({ orderIdFromHash }) => {
|
|||||||
const handleCloseDetailsDialog = () => {
|
const handleCloseDetailsDialog = () => {
|
||||||
setIsDetailsDialogOpen(false);
|
setIsDetailsDialogOpen(false);
|
||||||
setSelectedOrder(null);
|
setSelectedOrder(null);
|
||||||
navigate("/profile", { replace: true });
|
navigate("/profile#orders", { replace: true });
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check if order can be cancelled
|
||||||
|
const isOrderCancelable = (order) => {
|
||||||
|
const cancelableStatuses = ['new', 'pending', 'processing'];
|
||||||
|
return cancelableStatuses.includes(order.status);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle cancel button click
|
||||||
|
const handleCancelClick = (order) => {
|
||||||
|
setOrderToCancel(order);
|
||||||
|
setCancelConfirmOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle cancel confirmation
|
||||||
|
const handleConfirmCancel = () => {
|
||||||
|
if (!orderToCancel || !socket) return;
|
||||||
|
|
||||||
|
setIsCancelling(true);
|
||||||
|
socket.emit('cancelOrder', { orderId: orderToCancel.orderId }, (response) => {
|
||||||
|
setIsCancelling(false);
|
||||||
|
setCancelConfirmOpen(false);
|
||||||
|
|
||||||
|
if (response.success) {
|
||||||
|
console.log('Order cancelled:', response.orderId);
|
||||||
|
// Refresh orders list
|
||||||
|
fetchOrders();
|
||||||
|
} else {
|
||||||
|
setError(response.error || 'Failed to cancel order');
|
||||||
|
}
|
||||||
|
|
||||||
|
setOrderToCancel(null);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle cancel dialog close
|
||||||
|
const handleCancelDialogClose = () => {
|
||||||
|
if (!isCancelling) {
|
||||||
|
setCancelConfirmOpen(false);
|
||||||
|
setOrderToCancel(null);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
@@ -160,22 +216,21 @@ const OrdersTab = ({ orderIdFromHash }) => {
|
|||||||
<Table>
|
<Table>
|
||||||
<TableHead>
|
<TableHead>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell>Bestellnummer</TableCell>
|
<TableCell>{t ? t('orders.table.orderNumber') : 'Bestellnummer'}</TableCell>
|
||||||
<TableCell>Datum</TableCell>
|
<TableCell>{t ? t('orders.table.date') : 'Datum'}</TableCell>
|
||||||
<TableCell>Status</TableCell>
|
<TableCell>{t ? t('orders.table.status') : 'Status'}</TableCell>
|
||||||
<TableCell>Artikel</TableCell>
|
<TableCell>{t ? t('orders.table.items') : 'Artikel'}</TableCell>
|
||||||
<TableCell align="right">Summe</TableCell>
|
<TableCell align="right">{t ? t('orders.table.total') : 'Summe'}</TableCell>
|
||||||
<TableCell align="center">Aktionen</TableCell>
|
<TableCell align="center">{t ? t('orders.table.actions') : 'Aktionen'}</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHead>
|
</TableHead>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{orders.map((order) => {
|
{orders.map((order) => {
|
||||||
const displayStatus = getStatusDisplay(order.status);
|
const displayStatus = getStatusDisplay(order.status);
|
||||||
const subtotal = order.items.reduce(
|
const total = order.items.reduce(
|
||||||
(acc, item) => acc + item.price * item.quantity_ordered,
|
(acc, item) => acc + item.price * item.quantity_ordered,
|
||||||
0
|
0
|
||||||
);
|
);
|
||||||
const total = subtotal + order.delivery_cost;
|
|
||||||
return (
|
return (
|
||||||
<TableRow key={order.orderId} hover>
|
<TableRow key={order.orderId} hover>
|
||||||
<TableCell>{order.orderId}</TableCell>
|
<TableCell>{order.orderId}</TableCell>
|
||||||
@@ -204,7 +259,16 @@ const OrdersTab = ({ orderIdFromHash }) => {
|
|||||||
</Box>
|
</Box>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
{order.items.reduce(
|
{order.items
|
||||||
|
.filter(item => {
|
||||||
|
// Exclude delivery items - backend uses deliveryMethod ID as item name
|
||||||
|
const itemName = item.name || '';
|
||||||
|
return itemName !== 'DHL' &&
|
||||||
|
itemName !== 'DPD' &&
|
||||||
|
itemName !== 'Sperrgut' &&
|
||||||
|
itemName !== 'Abholung';
|
||||||
|
})
|
||||||
|
.reduce(
|
||||||
(acc, item) => acc + item.quantity_ordered,
|
(acc, item) => acc + item.quantity_ordered,
|
||||||
0
|
0
|
||||||
)}
|
)}
|
||||||
@@ -213,7 +277,8 @@ const OrdersTab = ({ orderIdFromHash }) => {
|
|||||||
{currencyFormatter.format(total)}
|
{currencyFormatter.format(total)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell align="center">
|
<TableCell align="center">
|
||||||
<Tooltip title="Details anzeigen">
|
<Box sx={{ display: 'flex', gap: 1, justifyContent: 'center' }}>
|
||||||
|
<Tooltip title={t ? t('orders.tooltips.viewDetails') : 'Details anzeigen'}>
|
||||||
<IconButton
|
<IconButton
|
||||||
size="small"
|
size="small"
|
||||||
color="primary"
|
color="primary"
|
||||||
@@ -222,6 +287,18 @@ const OrdersTab = ({ orderIdFromHash }) => {
|
|||||||
<SearchIcon />
|
<SearchIcon />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
{isOrderCancelable(order) && (
|
||||||
|
<Tooltip title={t ? t('orders.tooltips.cancelOrder') : 'Bestellung stornieren'}>
|
||||||
|
<IconButton
|
||||||
|
size="small"
|
||||||
|
color="error"
|
||||||
|
onClick={() => handleCancelClick(order)}
|
||||||
|
>
|
||||||
|
<CancelIcon />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
);
|
);
|
||||||
@@ -231,7 +308,7 @@ const OrdersTab = ({ orderIdFromHash }) => {
|
|||||||
</TableContainer>
|
</TableContainer>
|
||||||
) : (
|
) : (
|
||||||
<Alert severity="info">
|
<Alert severity="info">
|
||||||
Sie haben noch keine Bestellungen aufgegeben.
|
{t ? t('orders.noOrders') : 'Sie haben noch keine Bestellungen aufgegeben.'}
|
||||||
</Alert>
|
</Alert>
|
||||||
)}
|
)}
|
||||||
<OrderDetailsDialog
|
<OrderDetailsDialog
|
||||||
@@ -239,8 +316,49 @@ const OrdersTab = ({ orderIdFromHash }) => {
|
|||||||
onClose={handleCloseDetailsDialog}
|
onClose={handleCloseDetailsDialog}
|
||||||
order={selectedOrder}
|
order={selectedOrder}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* Cancel Confirmation Dialog */}
|
||||||
|
<Dialog
|
||||||
|
open={cancelConfirmOpen}
|
||||||
|
onClose={handleCancelDialogClose}
|
||||||
|
maxWidth="sm"
|
||||||
|
fullWidth
|
||||||
|
>
|
||||||
|
<DialogTitle>
|
||||||
|
{t ? t('orders.cancelConfirm.title') : 'Bestellung stornieren'}
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogContent>
|
||||||
|
<Typography>
|
||||||
|
{t ? t('orders.cancelConfirm.message') : 'Sind Sie sicher, dass Sie diese Bestellung stornieren möchten?'}
|
||||||
|
</Typography>
|
||||||
|
{orderToCancel && (
|
||||||
|
<Typography variant="body2" sx={{ mt: 1, fontWeight: 'bold' }}>
|
||||||
|
{t ? t('orders.table.orderNumber') : 'Bestellnummer'}: {orderToCancel.orderId}
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<Button
|
||||||
|
onClick={handleCancelDialogClose}
|
||||||
|
disabled={isCancelling}
|
||||||
|
>
|
||||||
|
{t ? t('common.cancel') : 'Abbrechen'}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={handleConfirmCancel}
|
||||||
|
color="error"
|
||||||
|
variant="contained"
|
||||||
|
disabled={isCancelling}
|
||||||
|
>
|
||||||
|
{isCancelling
|
||||||
|
? (t ? t('orders.cancelConfirm.cancelling') : 'Wird storniert...')
|
||||||
|
: (t ? t('orders.cancelConfirm.confirm') : 'Stornieren')
|
||||||
|
}
|
||||||
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default OrdersTab;
|
export default withI18n()(OrdersTab);
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import React, { Component } from "react";
|
import React, { Component } from "react";
|
||||||
import { Box, Typography, Button } from "@mui/material";
|
import { Box, Typography, Button } from "@mui/material";
|
||||||
|
import { withI18n } from "../../i18n/withTranslation.js";
|
||||||
|
|
||||||
class PaymentConfirmationDialog extends Component {
|
class PaymentConfirmationDialog extends Component {
|
||||||
render() {
|
render() {
|
||||||
@@ -28,30 +29,32 @@ class PaymentConfirmationDialog extends Component {
|
|||||||
color: paymentCompletionData.isSuccessful ? '#2e7d32' : '#d32f2f',
|
color: paymentCompletionData.isSuccessful ? '#2e7d32' : '#d32f2f',
|
||||||
fontWeight: 'bold'
|
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>
|
</Typography>
|
||||||
|
|
||||||
{paymentCompletionData.isSuccessful ? (
|
{paymentCompletionData.isSuccessful ? (
|
||||||
<>
|
<>
|
||||||
{orderCompleted ? (
|
{orderCompleted ? (
|
||||||
<Typography variant="body1" sx={{ mt: 2, color: '#2e7d32' }}>
|
<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>
|
||||||
) : (
|
) : (
|
||||||
<Typography variant="body1" sx={{ mt: 2, color: '#2e7d32' }}>
|
<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>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<Typography variant="body1" sx={{ mt: 2, color: '#d32f2f' }}>
|
<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>
|
</Typography>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{isCompletingOrder && (
|
{isCompletingOrder && (
|
||||||
<Typography variant="body2" sx={{ mt: 2, color: '#2e7d32', p: 2, bgcolor: '#e8f5e8', borderRadius: 1 }}>
|
<Typography variant="body2" sx={{ mt: 2, color: '#2e7d32', p: 2, bgcolor: '#e8f5e8', borderRadius: 1 }}>
|
||||||
Bestellung wird abgeschlossen...
|
{this.props.t ? this.props.t('orders.processing') : 'Bestellung wird abgeschlossen...'}
|
||||||
</Typography>
|
</Typography>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -75,7 +78,7 @@ class PaymentConfirmationDialog extends Component {
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Weiter einkaufen
|
{this.props.t ? this.props.t('cart.continueShopping') : 'Weiter einkaufen'}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
onClick={onViewOrders}
|
onClick={onViewOrders}
|
||||||
@@ -85,7 +88,7 @@ class PaymentConfirmationDialog extends Component {
|
|||||||
'&:hover': { bgcolor: '#1b5e20' }
|
'&:hover': { bgcolor: '#1b5e20' }
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Zu meinen Bestellungen
|
{this.props.t ? this.props.t('payment.viewOrders') : 'Zu meinen Bestellungen'}
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
@@ -94,4 +97,4 @@ class PaymentConfirmationDialog extends Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default PaymentConfirmationDialog;
|
export default withI18n()(PaymentConfirmationDialog);
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
import React, { useEffect, useCallback } from "react";
|
import React, { useEffect, useCallback } from "react";
|
||||||
import { Box, Typography, Radio } from "@mui/material";
|
import { Box, Typography, Radio } from "@mui/material";
|
||||||
|
import { withI18n } from "../../i18n/withTranslation.js";
|
||||||
|
|
||||||
const PaymentMethodSelector = ({ paymentMethod, onChange, deliveryMethod, onDeliveryMethodChange, cartItems = [], deliveryCost = 0 }) => {
|
const PaymentMethodSelector = ({ paymentMethod, onChange, deliveryMethod, onDeliveryMethodChange, cartItems = [], deliveryCost = 0, t }) => {
|
||||||
|
|
||||||
// Calculate total amount
|
// Calculate total amount
|
||||||
const subtotal = cartItems.reduce((total, item) => total + item.price * item.quantity, 0);
|
const subtotal = cartItems.reduce((total, item) => total + item.price * item.quantity, 0);
|
||||||
@@ -24,7 +25,7 @@ const PaymentMethodSelector = ({ paymentMethod, onChange, deliveryMethod, onDeli
|
|||||||
// Handle delivery method changes - auto-switch to stripe when DHL/DPD is selected
|
// Handle delivery method changes - auto-switch to stripe when DHL/DPD is selected
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if ((deliveryMethod === "DHL" || deliveryMethod === "DPD") && paymentMethod === "cash") {
|
if ((deliveryMethod === "DHL" || deliveryMethod === "DPD") && paymentMethod === "cash") {
|
||||||
handlePaymentMethodChange({ target: { value: "stripe" } });
|
handlePaymentMethodChange({ target: { value: "wire" /*stripe*/ } });
|
||||||
}
|
}
|
||||||
}, [deliveryMethod, paymentMethod, handlePaymentMethodChange]);
|
}, [deliveryMethod, paymentMethod, handlePaymentMethodChange]);
|
||||||
|
|
||||||
@@ -38,11 +39,11 @@ const PaymentMethodSelector = ({ paymentMethod, onChange, deliveryMethod, onDeli
|
|||||||
const paymentOptions = [
|
const paymentOptions = [
|
||||||
{
|
{
|
||||||
id: "wire",
|
id: "wire",
|
||||||
name: "Überweisung",
|
name: t ? t('payment.methods.bankTransfer') : "Überweisung",
|
||||||
description: "Bezahlen Sie per Banküberweisung",
|
description: t ? t('payment.methods.bankTransferDescription') : "Bezahlen Sie per Banküberweisung",
|
||||||
disabled: totalAmount === 0,
|
disabled: totalAmount === 0,
|
||||||
},
|
},
|
||||||
{
|
/*{
|
||||||
id: "stripe",
|
id: "stripe",
|
||||||
name: "Karte oder Sofortüberweisung",
|
name: "Karte oder Sofortüberweisung",
|
||||||
description: totalAmount < 0.50 && totalAmount > 0
|
description: totalAmount < 0.50 && totalAmount > 0
|
||||||
@@ -55,18 +56,32 @@ const PaymentMethodSelector = ({ paymentMethod, onChange, deliveryMethod, onDeli
|
|||||||
"/assets/images/mastercard.png",
|
"/assets/images/mastercard.png",
|
||||||
"/assets/images/visa_electron.png",
|
"/assets/images/visa_electron.png",
|
||||||
],
|
],
|
||||||
},
|
},*/
|
||||||
|
/*{
|
||||||
|
id: "mollie",
|
||||||
|
name: t ? t('payment.methods.cardPayment') : "Karte, Sofortüberweisung, Apple Pay, Google Pay, PayPal",
|
||||||
|
description: totalAmount < 0.50 && totalAmount > 0
|
||||||
|
? (t ? t('payment.methods.cardPaymentMinAmount') : "Bezahlen Sie per Karte oder Sofortüberweisung (Mindestbetrag: 0,50 €)")
|
||||||
|
: (t ? t('payment.methods.cardPaymentDescription') : "Bezahlen Sie per Karte oder Sofortüberweisung"),
|
||||||
|
disabled: totalAmount < 0.50 || (deliveryMethod !== "DHL" && deliveryMethod !== "DPD" && deliveryMethod !== "Abholung"),
|
||||||
|
icons: [
|
||||||
|
"/assets/images/giropay.png",
|
||||||
|
"/assets/images/maestro.png",
|
||||||
|
"/assets/images/mastercard.png",
|
||||||
|
"/assets/images/visa_electron.png",
|
||||||
|
],
|
||||||
|
},*/
|
||||||
{
|
{
|
||||||
id: "onDelivery",
|
id: "onDelivery",
|
||||||
name: "Nachnahme",
|
name: t ? t('payment.methods.cashOnDelivery') : "Nachnahme",
|
||||||
description: "Bezahlen Sie bei Lieferung (8,99 € Aufschlag)",
|
description: t ? t('payment.methods.cashOnDeliveryDescription') : "Bezahlen Sie bei Lieferung (8,99 € Aufschlag)",
|
||||||
disabled: totalAmount === 0 || deliveryMethod !== "DHL",
|
disabled: totalAmount === 0 || deliveryMethod !== "DHL",
|
||||||
icons: ["/assets/images/cash.png"],
|
icons: ["/assets/images/cash.png"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "cash",
|
id: "cash",
|
||||||
name: "Zahlung in der Filiale",
|
name: t ? t('payment.methods.cashInStore') : "Zahlung in der Filiale",
|
||||||
description: "Bei Abholung bezahlen",
|
description: t ? t('payment.methods.cashInStoreDescription') : "Bei Abholung bezahlen",
|
||||||
disabled: false, // Always enabled
|
disabled: false, // Always enabled
|
||||||
icons: ["/assets/images/cash.png"],
|
icons: ["/assets/images/cash.png"],
|
||||||
},
|
},
|
||||||
@@ -75,7 +90,7 @@ const PaymentMethodSelector = ({ paymentMethod, onChange, deliveryMethod, onDeli
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Typography variant="h6" gutterBottom>
|
<Typography variant="h6" gutterBottom>
|
||||||
Zahlungsart wählen
|
{t ? t('payment.methods.selectPaymentMethod') : 'Zahlungsart wählen'}
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
<Box sx={{ mb: 3 }}>
|
<Box sx={{ mb: 3 }}>
|
||||||
@@ -175,4 +190,4 @@ const PaymentMethodSelector = ({ paymentMethod, onChange, deliveryMethod, onDeli
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default PaymentMethodSelector;
|
export default withI18n()(PaymentMethodSelector);
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
Snackbar
|
Snackbar
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
import { ContentCopy } from '@mui/icons-material';
|
import { ContentCopy } from '@mui/icons-material';
|
||||||
|
import { withI18n } from '../../i18n/withTranslation.js';
|
||||||
|
|
||||||
class SettingsTab extends Component {
|
class SettingsTab extends Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
@@ -72,17 +73,17 @@ class SettingsTab extends Component {
|
|||||||
|
|
||||||
// Validation
|
// Validation
|
||||||
if (!this.state.currentPassword || !this.state.newPassword || !this.state.confirmPassword) {
|
if (!this.state.currentPassword || !this.state.newPassword || !this.state.confirmPassword) {
|
||||||
this.setState({ passwordError: 'Bitte füllen Sie alle Felder aus' });
|
this.setState({ passwordError: this.props.t ? this.props.t('settings.errors.fillAllFields') : 'Bitte füllen Sie alle Felder aus' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.state.newPassword !== this.state.confirmPassword) {
|
if (this.state.newPassword !== this.state.confirmPassword) {
|
||||||
this.setState({ passwordError: 'Die neuen Passwörter stimmen nicht überein' });
|
this.setState({ passwordError: this.props.t ? this.props.t('settings.errors.passwordsNotMatch') : 'Die neuen Passwörter stimmen nicht überein' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.state.newPassword.length < 8) {
|
if (this.state.newPassword.length < 8) {
|
||||||
this.setState({ passwordError: 'Das neue Passwort muss mindestens 8 Zeichen lang sein' });
|
this.setState({ passwordError: this.props.t ? this.props.t('settings.errors.passwordTooShort') : 'Das neue Passwort muss mindestens 8 Zeichen lang sein' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,14 +97,14 @@ class SettingsTab extends Component {
|
|||||||
|
|
||||||
if (response.success) {
|
if (response.success) {
|
||||||
this.setState({
|
this.setState({
|
||||||
passwordSuccess: 'Passwort erfolgreich aktualisiert',
|
passwordSuccess: this.props.t ? this.props.t('settings.success.passwordUpdated') : 'Passwort erfolgreich aktualisiert',
|
||||||
currentPassword: '',
|
currentPassword: '',
|
||||||
newPassword: '',
|
newPassword: '',
|
||||||
confirmPassword: ''
|
confirmPassword: ''
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
this.setState({
|
this.setState({
|
||||||
passwordError: response.message || 'Fehler beim Aktualisieren des Passworts'
|
passwordError: response.message || (this.props.t ? this.props.t('settings.errors.passwordUpdateError') : 'Fehler beim Aktualisieren des Passworts')
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -121,12 +122,12 @@ class SettingsTab extends Component {
|
|||||||
|
|
||||||
// Validation
|
// Validation
|
||||||
if (!this.state.password || !this.state.newEmail) {
|
if (!this.state.password || !this.state.newEmail) {
|
||||||
this.setState({ emailError: 'Bitte füllen Sie alle Felder aus' });
|
this.setState({ emailError: this.props.t ? this.props.t('settings.errors.fillAllFields') : 'Bitte füllen Sie alle Felder aus' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(this.state.newEmail)) {
|
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(this.state.newEmail)) {
|
||||||
this.setState({ emailError: 'Bitte geben Sie eine gültige E-Mail-Adresse ein' });
|
this.setState({ emailError: this.props.t ? this.props.t('settings.errors.invalidEmail') : 'Bitte geben Sie eine gültige E-Mail-Adresse ein' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -140,7 +141,7 @@ class SettingsTab extends Component {
|
|||||||
|
|
||||||
if (response.success) {
|
if (response.success) {
|
||||||
this.setState({
|
this.setState({
|
||||||
emailSuccess: 'E-Mail-Adresse erfolgreich aktualisiert',
|
emailSuccess: this.props.t ? this.props.t('settings.success.emailUpdated') : 'E-Mail-Adresse erfolgreich aktualisiert',
|
||||||
password: ''
|
password: ''
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -157,7 +158,7 @@ class SettingsTab extends Component {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
this.setState({
|
this.setState({
|
||||||
emailError: response.message || 'Fehler beim Aktualisieren der E-Mail-Adresse'
|
emailError: response.message || (this.props.t ? this.props.t('settings.errors.emailUpdateError') : 'Fehler beim Aktualisieren der E-Mail-Adresse')
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -238,7 +239,7 @@ class SettingsTab extends Component {
|
|||||||
<Box sx={{ p: { xs: 1, sm: 3 } }}>
|
<Box sx={{ p: { xs: 1, sm: 3 } }}>
|
||||||
<Paper sx={{ p: { xs: 2, sm: 3 } }}>
|
<Paper sx={{ p: { xs: 2, sm: 3 } }}>
|
||||||
<Typography variant="subtitle1" fontWeight="bold" gutterBottom>
|
<Typography variant="subtitle1" fontWeight="bold" gutterBottom>
|
||||||
Passwort ändern
|
{this.props.t ? this.props.t('settings.changePassword') : 'Passwort ändern'}
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
{this.state.passwordError && <Alert severity="error" sx={{ mb: 2 }}>{this.state.passwordError}</Alert>}
|
{this.state.passwordError && <Alert severity="error" sx={{ mb: 2 }}>{this.state.passwordError}</Alert>}
|
||||||
@@ -247,7 +248,7 @@ class SettingsTab extends Component {
|
|||||||
<Box component="form" onSubmit={this.handleUpdatePassword}>
|
<Box component="form" onSubmit={this.handleUpdatePassword}>
|
||||||
<TextField
|
<TextField
|
||||||
margin="normal"
|
margin="normal"
|
||||||
label="Aktuelles Passwort"
|
label={this.props.t ? this.props.t('settings.currentPassword') : 'Aktuelles Passwort'}
|
||||||
type="password"
|
type="password"
|
||||||
fullWidth
|
fullWidth
|
||||||
value={this.state.currentPassword}
|
value={this.state.currentPassword}
|
||||||
@@ -257,7 +258,7 @@ class SettingsTab extends Component {
|
|||||||
|
|
||||||
<TextField
|
<TextField
|
||||||
margin="normal"
|
margin="normal"
|
||||||
label="Neues Passwort"
|
label={this.props.t ? this.props.t('settings.newPassword') : 'Neues Passwort'}
|
||||||
type="password"
|
type="password"
|
||||||
fullWidth
|
fullWidth
|
||||||
value={this.state.newPassword}
|
value={this.state.newPassword}
|
||||||
@@ -267,7 +268,7 @@ class SettingsTab extends Component {
|
|||||||
|
|
||||||
<TextField
|
<TextField
|
||||||
margin="normal"
|
margin="normal"
|
||||||
label="Neues Passwort bestätigen"
|
label={this.props.t ? this.props.t('settings.confirmNewPassword') : 'Neues Passwort bestätigen'}
|
||||||
type="password"
|
type="password"
|
||||||
fullWidth
|
fullWidth
|
||||||
value={this.state.confirmPassword}
|
value={this.state.confirmPassword}
|
||||||
@@ -282,7 +283,7 @@ class SettingsTab extends Component {
|
|||||||
sx={{ mt: 2, bgcolor: '#2e7d32', '&:hover': { bgcolor: '#1b5e20' } }}
|
sx={{ mt: 2, bgcolor: '#2e7d32', '&:hover': { bgcolor: '#1b5e20' } }}
|
||||||
disabled={this.state.loading}
|
disabled={this.state.loading}
|
||||||
>
|
>
|
||||||
{this.state.loading ? <CircularProgress size={24} /> : 'Passwort aktualisieren'}
|
{this.state.loading ? <CircularProgress size={24} /> : (this.props.t ? this.props.t('settings.updatePassword') : 'Passwort aktualisieren')}
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
</Paper>
|
</Paper>
|
||||||
@@ -291,7 +292,7 @@ class SettingsTab extends Component {
|
|||||||
|
|
||||||
<Paper sx={{ p: { xs: 2, sm: 3 } }}>
|
<Paper sx={{ p: { xs: 2, sm: 3 } }}>
|
||||||
<Typography variant="subtitle1" fontWeight="bold" gutterBottom>
|
<Typography variant="subtitle1" fontWeight="bold" gutterBottom>
|
||||||
E-Mail-Adresse ändern
|
{this.props.t ? this.props.t('settings.changeEmail') : 'E-Mail-Adresse ändern'}
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
{this.state.emailError && <Alert severity="error" sx={{ mb: 2 }}>{this.state.emailError}</Alert>}
|
{this.state.emailError && <Alert severity="error" sx={{ mb: 2 }}>{this.state.emailError}</Alert>}
|
||||||
@@ -300,7 +301,7 @@ class SettingsTab extends Component {
|
|||||||
<Box component="form" onSubmit={this.handleUpdateEmail}>
|
<Box component="form" onSubmit={this.handleUpdateEmail}>
|
||||||
<TextField
|
<TextField
|
||||||
margin="normal"
|
margin="normal"
|
||||||
label="Passwort"
|
label={this.props.t ? this.props.t('settings.password') : 'Passwort'}
|
||||||
type="password"
|
type="password"
|
||||||
fullWidth
|
fullWidth
|
||||||
value={this.state.password}
|
value={this.state.password}
|
||||||
@@ -310,7 +311,7 @@ class SettingsTab extends Component {
|
|||||||
|
|
||||||
<TextField
|
<TextField
|
||||||
margin="normal"
|
margin="normal"
|
||||||
label="Neue E-Mail-Adresse"
|
label={this.props.t ? this.props.t('settings.newEmail') : 'Neue E-Mail-Adresse'}
|
||||||
type="email"
|
type="email"
|
||||||
fullWidth
|
fullWidth
|
||||||
value={this.state.newEmail}
|
value={this.state.newEmail}
|
||||||
@@ -325,7 +326,7 @@ class SettingsTab extends Component {
|
|||||||
sx={{ mt: 2, bgcolor: '#2e7d32', '&:hover': { bgcolor: '#1b5e20' } }}
|
sx={{ mt: 2, bgcolor: '#2e7d32', '&:hover': { bgcolor: '#1b5e20' } }}
|
||||||
disabled={this.state.loading}
|
disabled={this.state.loading}
|
||||||
>
|
>
|
||||||
{this.state.loading ? <CircularProgress size={24} /> : 'E-Mail aktualisieren'}
|
{this.state.loading ? <CircularProgress size={24} /> : (this.props.t ? this.props.t('settings.updateEmail') : 'E-Mail aktualisieren')}
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
</Paper>
|
</Paper>
|
||||||
@@ -334,11 +335,11 @@ class SettingsTab extends Component {
|
|||||||
|
|
||||||
<Paper sx={{ p: { xs: 2, sm: 3 } }}>
|
<Paper sx={{ p: { xs: 2, sm: 3 } }}>
|
||||||
<Typography variant="subtitle1" fontWeight="bold" gutterBottom>
|
<Typography variant="subtitle1" fontWeight="bold" gutterBottom>
|
||||||
API-Schlüssel
|
{this.props.t ? this.props.t('settings.apiKey') : 'API-Schlüssel'}
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
<Typography variant="body2" color="text.secondary" gutterBottom>
|
<Typography variant="body2" color="text.secondary" gutterBottom>
|
||||||
Verwenden Sie Ihren API-Schlüssel für die Integration mit externen Anwendungen.
|
{this.props.t ? this.props.t('settings.apiKeyDescription') : 'Verwenden Sie Ihren API-Schlüssel für die Integration mit externen Anwendungen.'}
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
{this.state.apiKeyError && <Alert severity="error" sx={{ mb: 2 }}>{this.state.apiKeyError}</Alert>}
|
{this.state.apiKeyError && <Alert severity="error" sx={{ mb: 2 }}>{this.state.apiKeyError}</Alert>}
|
||||||
@@ -347,14 +348,14 @@ class SettingsTab extends Component {
|
|||||||
{this.state.apiKeySuccess}
|
{this.state.apiKeySuccess}
|
||||||
{this.state.apiKey && this.state.apiKeyDisplay !== '************' && (
|
{this.state.apiKey && this.state.apiKeyDisplay !== '************' && (
|
||||||
<Typography variant="body2" sx={{ mt: 1 }}>
|
<Typography variant="body2" sx={{ mt: 1 }}>
|
||||||
Speichern Sie diesen Schlüssel sicher. Er wird aus Sicherheitsgründen in 10 Sekunden ausgeblendet.
|
{this.props.t ? this.props.t('settings.success.apiKeyWarning') : 'Speichern Sie diesen Schlüssel sicher. Er wird aus Sicherheitsgründen in 10 Sekunden ausgeblendet.'}
|
||||||
</Typography>
|
</Typography>
|
||||||
)}
|
)}
|
||||||
</Alert>
|
</Alert>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Typography variant="body2" sx={{ mb: 2 }}>
|
<Typography variant="body2" sx={{ mb: 2 }}>
|
||||||
API-Dokumentation: {' '}
|
{this.props.t ? this.props.t('settings.apiDocumentation') : 'API-Dokumentation:'} {' '}
|
||||||
<a
|
<a
|
||||||
href={`${window.location.protocol}//${window.location.host}/api/`}
|
href={`${window.location.protocol}//${window.location.host}/api/`}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
@@ -367,7 +368,7 @@ class SettingsTab extends Component {
|
|||||||
|
|
||||||
<Box sx={{ display: 'flex', gap: 2, alignItems: 'center', mt: 2 }}>
|
<Box sx={{ display: 'flex', gap: 2, alignItems: 'center', mt: 2 }}>
|
||||||
<TextField
|
<TextField
|
||||||
label="API-Schlüssel"
|
label={this.props.t ? this.props.t('settings.apiKey') : 'API-Schlüssel'}
|
||||||
value={this.state.apiKeyDisplay}
|
value={this.state.apiKeyDisplay}
|
||||||
disabled
|
disabled
|
||||||
fullWidth
|
fullWidth
|
||||||
@@ -385,7 +386,7 @@ class SettingsTab extends Component {
|
|||||||
color: '#2e7d32',
|
color: '#2e7d32',
|
||||||
'&:hover': { bgcolor: 'rgba(46, 125, 50, 0.1)' }
|
'&:hover': { bgcolor: 'rgba(46, 125, 50, 0.1)' }
|
||||||
}}
|
}}
|
||||||
title="In Zwischenablage kopieren"
|
title={this.props.t ? this.props.t('settings.copyToClipboard') : 'In Zwischenablage kopieren'}
|
||||||
>
|
>
|
||||||
<ContentCopy />
|
<ContentCopy />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
@@ -405,7 +406,7 @@ class SettingsTab extends Component {
|
|||||||
{this.state.loadingApiKey ? (
|
{this.state.loadingApiKey ? (
|
||||||
<CircularProgress size={24} />
|
<CircularProgress size={24} />
|
||||||
) : (
|
) : (
|
||||||
this.state.hasApiKey ? 'Regenerieren' : 'Generieren'
|
this.state.hasApiKey ? (this.props.t ? this.props.t('settings.regenerate') : 'Regenerieren') : (this.props.t ? this.props.t('settings.generate') : 'Generieren')
|
||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
@@ -415,7 +416,7 @@ class SettingsTab extends Component {
|
|||||||
open={this.state.copySnackbarOpen}
|
open={this.state.copySnackbarOpen}
|
||||||
autoHideDuration={3000}
|
autoHideDuration={3000}
|
||||||
onClose={this.handleCloseSnackbar}
|
onClose={this.handleCloseSnackbar}
|
||||||
message="API-Schlüssel in Zwischenablage kopiert"
|
message={this.props.t ? this.props.t('settings.apiKeyCopied') : 'API-Schlüssel in Zwischenablage kopiert'}
|
||||||
anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}
|
anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
@@ -423,4 +424,4 @@ class SettingsTab extends Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default SettingsTab;
|
export default withI18n()(SettingsTab);
|
||||||
192
src/config.js
192
src/config.js
@@ -8,17 +8,195 @@ const config = {
|
|||||||
siteName: "Growheads.de",
|
siteName: "Growheads.de",
|
||||||
brandName: "GrowHeads",
|
brandName: "GrowHeads",
|
||||||
currency: "EUR",
|
currency: "EUR",
|
||||||
language: "de-DE",
|
language: "de-DE", // Will be updated dynamically based on i18n
|
||||||
country: "DE",
|
country: "DE",
|
||||||
|
|
||||||
// Shop Descriptions
|
// Multilingual configurations
|
||||||
descriptions: {
|
languages: {
|
||||||
short: "GrowHeads - Online-Shop für Cannanis-Samen, Stecklinge und Gartenbedarf",
|
de: {
|
||||||
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."
|
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
|
// Shop Descriptions - Multilingual
|
||||||
keywords: "Seeds, Steckling, Cannabis, Biobizz, Growheads",
|
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
|
||||||
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;
|
||||||
381
src/i18n/index.js
Normal file
381
src/i18n/index.js
Normal file
@@ -0,0 +1,381 @@
|
|||||||
|
import i18n from 'i18next';
|
||||||
|
import { initReactI18next } from 'react-i18next';
|
||||||
|
import LanguageDetector from 'i18next-browser-languagedetector';
|
||||||
|
|
||||||
|
// Import all translation files
|
||||||
|
import translationDE from './locales/de/index.js';
|
||||||
|
import translationEN from './locales/en/index.js';
|
||||||
|
import translationAR from './locales/ar/translation.js';
|
||||||
|
import translationBG from './locales/bg/translation.js';
|
||||||
|
import translationCS from './locales/cs/translation.js';
|
||||||
|
import translationEL from './locales/el/translation.js';
|
||||||
|
import translationES from './locales/es/translation.js';
|
||||||
|
import translationFR from './locales/fr/translation.js';
|
||||||
|
import translationHR from './locales/hr/translation.js';
|
||||||
|
import translationHU from './locales/hu/translation.js';
|
||||||
|
import translationIT from './locales/it/translation.js';
|
||||||
|
import translationPL from './locales/pl/translation.js';
|
||||||
|
import translationRO from './locales/ro/translation.js';
|
||||||
|
import translationRU from './locales/ru/translation.js';
|
||||||
|
import translationSK from './locales/sk/translation.js';
|
||||||
|
import translationSL from './locales/sl/translation.js';
|
||||||
|
import translationSR from './locales/sr/translation.js';
|
||||||
|
import translationSV from './locales/sv/translation.js';
|
||||||
|
import translationTR from './locales/tr/translation.js';
|
||||||
|
import translationUK from './locales/uk/translation.js';
|
||||||
|
import translationZH from './locales/zh/translation.js';
|
||||||
|
|
||||||
|
// Import legal translations for all languages
|
||||||
|
// German
|
||||||
|
import legalAgbDE from './locales/de/legal-agb.js';
|
||||||
|
import legalDatenschutzDE from './locales/de/legal-datenschutz.js';
|
||||||
|
import legalImpressumDE from './locales/de/legal-impressum.js';
|
||||||
|
import legalWiderrufDE from './locales/de/legal-widerruf.js';
|
||||||
|
import legalBatterieDE from './locales/de/legal-batterie.js';
|
||||||
|
|
||||||
|
// English
|
||||||
|
import legalAgbEN from './locales/en/legal-agb.js';
|
||||||
|
import legalDatenschutzEN from './locales/en/legal-datenschutz.js';
|
||||||
|
import legalImpressumEN from './locales/en/legal-impressum.js';
|
||||||
|
import legalWiderrufEN from './locales/en/legal-widerruf.js';
|
||||||
|
import legalBatterieEN from './locales/en/legal-batterie.js';
|
||||||
|
|
||||||
|
// Arabic
|
||||||
|
import legalAgbAR from './locales/ar/legal-agb.js';
|
||||||
|
import legalDatenschutzAR from './locales/ar/legal-datenschutz.js';
|
||||||
|
import legalImpressumAR from './locales/ar/legal-impressum.js';
|
||||||
|
import legalWiderrufAR from './locales/ar/legal-widerruf.js';
|
||||||
|
import legalBatterieAR from './locales/ar/legal-batterie.js';
|
||||||
|
|
||||||
|
// Bulgarian
|
||||||
|
import legalAgbBG from './locales/bg/legal-agb.js';
|
||||||
|
import legalDatenschutzBG from './locales/bg/legal-datenschutz.js';
|
||||||
|
import legalImpressumBG from './locales/bg/legal-impressum.js';
|
||||||
|
import legalWiderrufBG from './locales/bg/legal-widerruf.js';
|
||||||
|
import legalBatterieBG from './locales/bg/legal-batterie.js';
|
||||||
|
|
||||||
|
// Czech
|
||||||
|
import legalAgbCS from './locales/cs/legal-agb.js';
|
||||||
|
import legalDatenschutzCS from './locales/cs/legal-datenschutz.js';
|
||||||
|
import legalImpressumCS from './locales/cs/legal-impressum.js';
|
||||||
|
import legalWiderrufCS from './locales/cs/legal-widerruf.js';
|
||||||
|
import legalBatterieCS from './locales/cs/legal-batterie.js';
|
||||||
|
|
||||||
|
// Greek
|
||||||
|
import legalAgbEL from './locales/el/legal-agb.js';
|
||||||
|
import legalDatenschutzEL from './locales/el/legal-datenschutz.js';
|
||||||
|
import legalImpressumEL from './locales/el/legal-impressum.js';
|
||||||
|
import legalWiderrufEL from './locales/el/legal-widerruf.js';
|
||||||
|
import legalBatterieEL from './locales/el/legal-batterie.js';
|
||||||
|
|
||||||
|
// Spanish
|
||||||
|
import legalAgbES from './locales/es/legal-agb.js';
|
||||||
|
import legalDatenschutzES from './locales/es/legal-datenschutz.js';
|
||||||
|
import legalImpressumES from './locales/es/legal-impressum.js';
|
||||||
|
import legalWiderrufES from './locales/es/legal-widerruf.js';
|
||||||
|
import legalBatterieES from './locales/es/legal-batterie.js';
|
||||||
|
|
||||||
|
// French
|
||||||
|
import legalAgbFR from './locales/fr/legal-agb.js';
|
||||||
|
import legalDatenschutzFR from './locales/fr/legal-datenschutz.js';
|
||||||
|
import legalImpressumFR from './locales/fr/legal-impressum.js';
|
||||||
|
import legalWiderrufFR from './locales/fr/legal-widerruf.js';
|
||||||
|
import legalBatterieFR from './locales/fr/legal-batterie.js';
|
||||||
|
|
||||||
|
// Croatian
|
||||||
|
import legalAgbHR from './locales/hr/legal-agb.js';
|
||||||
|
import legalDatenschutzHR from './locales/hr/legal-datenschutz.js';
|
||||||
|
import legalImpressumHR from './locales/hr/legal-impressum.js';
|
||||||
|
import legalWiderrufHR from './locales/hr/legal-widerruf.js';
|
||||||
|
import legalBatterieHR from './locales/hr/legal-batterie.js';
|
||||||
|
|
||||||
|
// Hungarian
|
||||||
|
import legalAgbHU from './locales/hu/legal-agb.js';
|
||||||
|
import legalDatenschutzHU from './locales/hu/legal-datenschutz.js';
|
||||||
|
import legalImpressumHU from './locales/hu/legal-impressum.js';
|
||||||
|
import legalWiderrufHU from './locales/hu/legal-widerruf.js';
|
||||||
|
import legalBatterieHU from './locales/hu/legal-batterie.js';
|
||||||
|
|
||||||
|
// Italian
|
||||||
|
import legalAgbIT from './locales/it/legal-agb.js';
|
||||||
|
import legalDatenschutzIT from './locales/it/legal-datenschutz.js';
|
||||||
|
import legalImpressumIT from './locales/it/legal-impressum.js';
|
||||||
|
import legalWiderrufIT from './locales/it/legal-widerruf.js';
|
||||||
|
import legalBatterieIT from './locales/it/legal-batterie.js';
|
||||||
|
|
||||||
|
// Polish
|
||||||
|
import legalAgbPL from './locales/pl/legal-agb.js';
|
||||||
|
import legalDatenschutzPL from './locales/pl/legal-datenschutz.js';
|
||||||
|
import legalImpressumPL from './locales/pl/legal-impressum.js';
|
||||||
|
import legalWiderrufPL from './locales/pl/legal-widerruf.js';
|
||||||
|
import legalBatteriePL from './locales/pl/legal-batterie.js';
|
||||||
|
|
||||||
|
// Romanian
|
||||||
|
import legalAgbRO from './locales/ro/legal-agb.js';
|
||||||
|
import legalDatenschutzRO from './locales/ro/legal-datenschutz.js';
|
||||||
|
import legalImpressumRO from './locales/ro/legal-impressum.js';
|
||||||
|
import legalWiderrufRO from './locales/ro/legal-widerruf.js';
|
||||||
|
import legalBatterieRO from './locales/ro/legal-batterie.js';
|
||||||
|
|
||||||
|
// Russian
|
||||||
|
import legalAgbRU from './locales/ru/legal-agb.js';
|
||||||
|
import legalDatenschutzRU from './locales/ru/legal-datenschutz.js';
|
||||||
|
import legalImpressumRU from './locales/ru/legal-impressum.js';
|
||||||
|
import legalWiderrufRU from './locales/ru/legal-widerruf.js';
|
||||||
|
import legalBatterieRU from './locales/ru/legal-batterie.js';
|
||||||
|
|
||||||
|
// Slovak
|
||||||
|
import legalAgbSK from './locales/sk/legal-agb.js';
|
||||||
|
import legalDatenschutzSK from './locales/sk/legal-datenschutz.js';
|
||||||
|
import legalImpressumSK from './locales/sk/legal-impressum.js';
|
||||||
|
import legalWiderrufSK from './locales/sk/legal-widerruf.js';
|
||||||
|
import legalBatterieSK from './locales/sk/legal-batterie.js';
|
||||||
|
|
||||||
|
// Slovenian
|
||||||
|
import legalAgbSL from './locales/sl/legal-agb.js';
|
||||||
|
import legalDatenschutzSL from './locales/sl/legal-datenschutz.js';
|
||||||
|
import legalImpressumSL from './locales/sl/legal-impressum.js';
|
||||||
|
import legalWiderrufSL from './locales/sl/legal-widerruf.js';
|
||||||
|
import legalBatterieSL from './locales/sl/legal-batterie.js';
|
||||||
|
|
||||||
|
// Serbian
|
||||||
|
import legalAgbSR from './locales/sr/legal-agb.js';
|
||||||
|
import legalDatenschutzSR from './locales/sr/legal-datenschutz.js';
|
||||||
|
import legalImpressumSR from './locales/sr/legal-impressum.js';
|
||||||
|
import legalWiderrufSR from './locales/sr/legal-widerruf.js';
|
||||||
|
import legalBatterieSR from './locales/sr/legal-batterie.js';
|
||||||
|
|
||||||
|
// Swedish
|
||||||
|
import legalAgbSV from './locales/sv/legal-agb.js';
|
||||||
|
import legalDatenschutzSV from './locales/sv/legal-datenschutz.js';
|
||||||
|
import legalImpressumSV from './locales/sv/legal-impressum.js';
|
||||||
|
import legalWiderrufSV from './locales/sv/legal-widerruf.js';
|
||||||
|
import legalBatterieSV from './locales/sv/legal-batterie.js';
|
||||||
|
|
||||||
|
// Turkish
|
||||||
|
import legalAgbTR from './locales/tr/legal-agb.js';
|
||||||
|
import legalDatenschutzTR from './locales/tr/legal-datenschutz.js';
|
||||||
|
import legalImpressumTR from './locales/tr/legal-impressum.js';
|
||||||
|
import legalWiderrufTR from './locales/tr/legal-widerruf.js';
|
||||||
|
import legalBatterieTR from './locales/tr/legal-batterie.js';
|
||||||
|
|
||||||
|
// Ukrainian
|
||||||
|
import legalAgbUK from './locales/uk/legal-agb.js';
|
||||||
|
import legalDatenschutzUK from './locales/uk/legal-datenschutz.js';
|
||||||
|
import legalImpressumUK from './locales/uk/legal-impressum.js';
|
||||||
|
import legalWiderrufUK from './locales/uk/legal-widerruf.js';
|
||||||
|
import legalBatterieUK from './locales/uk/legal-batterie.js';
|
||||||
|
|
||||||
|
// Chinese
|
||||||
|
import legalAgbZH from './locales/zh/legal-agb.js';
|
||||||
|
import legalDatenschutzZH from './locales/zh/legal-datenschutz.js';
|
||||||
|
import legalImpressumZH from './locales/zh/legal-impressum.js';
|
||||||
|
import legalWiderrufZH from './locales/zh/legal-widerruf.js';
|
||||||
|
import legalBatterieZH from './locales/zh/legal-batterie.js';
|
||||||
|
|
||||||
|
const resources = {
|
||||||
|
de: {
|
||||||
|
translation: translationDE,
|
||||||
|
'legal-agb': legalAgbDE,
|
||||||
|
'legal-datenschutz': legalDatenschutzDE,
|
||||||
|
'legal-impressum': legalImpressumDE,
|
||||||
|
'legal-widerruf': legalWiderrufDE,
|
||||||
|
'legal-batterie': legalBatterieDE
|
||||||
|
},
|
||||||
|
en: {
|
||||||
|
translation: translationEN,
|
||||||
|
'legal-agb': legalAgbEN,
|
||||||
|
'legal-datenschutz': legalDatenschutzEN,
|
||||||
|
'legal-impressum': legalImpressumEN,
|
||||||
|
'legal-widerruf': legalWiderrufEN,
|
||||||
|
'legal-batterie': legalBatterieEN
|
||||||
|
},
|
||||||
|
ar: {
|
||||||
|
translation: translationAR,
|
||||||
|
'legal-agb': legalAgbAR,
|
||||||
|
'legal-datenschutz': legalDatenschutzAR,
|
||||||
|
'legal-impressum': legalImpressumAR,
|
||||||
|
'legal-widerruf': legalWiderrufAR,
|
||||||
|
'legal-batterie': legalBatterieAR
|
||||||
|
},
|
||||||
|
bg: {
|
||||||
|
translation: translationBG,
|
||||||
|
'legal-agb': legalAgbBG,
|
||||||
|
'legal-datenschutz': legalDatenschutzBG,
|
||||||
|
'legal-impressum': legalImpressumBG,
|
||||||
|
'legal-widerruf': legalWiderrufBG,
|
||||||
|
'legal-batterie': legalBatterieBG
|
||||||
|
},
|
||||||
|
cs: {
|
||||||
|
translation: translationCS,
|
||||||
|
'legal-agb': legalAgbCS,
|
||||||
|
'legal-datenschutz': legalDatenschutzCS,
|
||||||
|
'legal-impressum': legalImpressumCS,
|
||||||
|
'legal-widerruf': legalWiderrufCS,
|
||||||
|
'legal-batterie': legalBatterieCS
|
||||||
|
},
|
||||||
|
el: {
|
||||||
|
translation: translationEL,
|
||||||
|
'legal-agb': legalAgbEL,
|
||||||
|
'legal-datenschutz': legalDatenschutzEL,
|
||||||
|
'legal-impressum': legalImpressumEL,
|
||||||
|
'legal-widerruf': legalWiderrufEL,
|
||||||
|
'legal-batterie': legalBatterieEL
|
||||||
|
},
|
||||||
|
es: {
|
||||||
|
translation: translationES,
|
||||||
|
'legal-agb': legalAgbES,
|
||||||
|
'legal-datenschutz': legalDatenschutzES,
|
||||||
|
'legal-impressum': legalImpressumES,
|
||||||
|
'legal-widerruf': legalWiderrufES,
|
||||||
|
'legal-batterie': legalBatterieES
|
||||||
|
},
|
||||||
|
fr: {
|
||||||
|
translation: translationFR,
|
||||||
|
'legal-agb': legalAgbFR,
|
||||||
|
'legal-datenschutz': legalDatenschutzFR,
|
||||||
|
'legal-impressum': legalImpressumFR,
|
||||||
|
'legal-widerruf': legalWiderrufFR,
|
||||||
|
'legal-batterie': legalBatterieFR
|
||||||
|
},
|
||||||
|
hr: {
|
||||||
|
translation: translationHR,
|
||||||
|
'legal-agb': legalAgbHR,
|
||||||
|
'legal-datenschutz': legalDatenschutzHR,
|
||||||
|
'legal-impressum': legalImpressumHR,
|
||||||
|
'legal-widerruf': legalWiderrufHR,
|
||||||
|
'legal-batterie': legalBatterieHR
|
||||||
|
},
|
||||||
|
hu: {
|
||||||
|
translation: translationHU,
|
||||||
|
'legal-agb': legalAgbHU,
|
||||||
|
'legal-datenschutz': legalDatenschutzHU,
|
||||||
|
'legal-impressum': legalImpressumHU,
|
||||||
|
'legal-widerruf': legalWiderrufHU,
|
||||||
|
'legal-batterie': legalBatterieHU
|
||||||
|
},
|
||||||
|
it: {
|
||||||
|
translation: translationIT,
|
||||||
|
'legal-agb': legalAgbIT,
|
||||||
|
'legal-datenschutz': legalDatenschutzIT,
|
||||||
|
'legal-impressum': legalImpressumIT,
|
||||||
|
'legal-widerruf': legalWiderrufIT,
|
||||||
|
'legal-batterie': legalBatterieIT
|
||||||
|
},
|
||||||
|
pl: {
|
||||||
|
translation: translationPL,
|
||||||
|
'legal-agb': legalAgbPL,
|
||||||
|
'legal-datenschutz': legalDatenschutzPL,
|
||||||
|
'legal-impressum': legalImpressumPL,
|
||||||
|
'legal-widerruf': legalWiderrufPL,
|
||||||
|
'legal-batterie': legalBatteriePL
|
||||||
|
},
|
||||||
|
ro: {
|
||||||
|
translation: translationRO,
|
||||||
|
'legal-agb': legalAgbRO,
|
||||||
|
'legal-datenschutz': legalDatenschutzRO,
|
||||||
|
'legal-impressum': legalImpressumRO,
|
||||||
|
'legal-widerruf': legalWiderrufRO,
|
||||||
|
'legal-batterie': legalBatterieRO
|
||||||
|
},
|
||||||
|
ru: {
|
||||||
|
translation: translationRU,
|
||||||
|
'legal-agb': legalAgbRU,
|
||||||
|
'legal-datenschutz': legalDatenschutzRU,
|
||||||
|
'legal-impressum': legalImpressumRU,
|
||||||
|
'legal-widerruf': legalWiderrufRU,
|
||||||
|
'legal-batterie': legalBatterieRU
|
||||||
|
},
|
||||||
|
sk: {
|
||||||
|
translation: translationSK,
|
||||||
|
'legal-agb': legalAgbSK,
|
||||||
|
'legal-datenschutz': legalDatenschutzSK,
|
||||||
|
'legal-impressum': legalImpressumSK,
|
||||||
|
'legal-widerruf': legalWiderrufSK,
|
||||||
|
'legal-batterie': legalBatterieSK
|
||||||
|
},
|
||||||
|
sl: {
|
||||||
|
translation: translationSL,
|
||||||
|
'legal-agb': legalAgbSL,
|
||||||
|
'legal-datenschutz': legalDatenschutzSL,
|
||||||
|
'legal-impressum': legalImpressumSL,
|
||||||
|
'legal-widerruf': legalWiderrufSL,
|
||||||
|
'legal-batterie': legalBatterieSL
|
||||||
|
},
|
||||||
|
sr: {
|
||||||
|
translation: translationSR,
|
||||||
|
'legal-agb': legalAgbSR,
|
||||||
|
'legal-datenschutz': legalDatenschutzSR,
|
||||||
|
'legal-impressum': legalImpressumSR,
|
||||||
|
'legal-widerruf': legalWiderrufSR,
|
||||||
|
'legal-batterie': legalBatterieSR
|
||||||
|
},
|
||||||
|
sv: {
|
||||||
|
translation: translationSV,
|
||||||
|
'legal-agb': legalAgbSV,
|
||||||
|
'legal-datenschutz': legalDatenschutzSV,
|
||||||
|
'legal-impressum': legalImpressumSV,
|
||||||
|
'legal-widerruf': legalWiderrufSV,
|
||||||
|
'legal-batterie': legalBatterieSV
|
||||||
|
},
|
||||||
|
tr: {
|
||||||
|
translation: translationTR,
|
||||||
|
'legal-agb': legalAgbTR,
|
||||||
|
'legal-datenschutz': legalDatenschutzTR,
|
||||||
|
'legal-impressum': legalImpressumTR,
|
||||||
|
'legal-widerruf': legalWiderrufTR,
|
||||||
|
'legal-batterie': legalBatterieTR
|
||||||
|
},
|
||||||
|
uk: {
|
||||||
|
translation: translationUK,
|
||||||
|
'legal-agb': legalAgbUK,
|
||||||
|
'legal-datenschutz': legalDatenschutzUK,
|
||||||
|
'legal-impressum': legalImpressumUK,
|
||||||
|
'legal-widerruf': legalWiderrufUK,
|
||||||
|
'legal-batterie': legalBatterieUK
|
||||||
|
},
|
||||||
|
zh: {
|
||||||
|
translation: translationZH,
|
||||||
|
'legal-agb': legalAgbZH,
|
||||||
|
'legal-datenschutz': legalDatenschutzZH,
|
||||||
|
'legal-impressum': legalImpressumZH,
|
||||||
|
'legal-widerruf': legalWiderrufZH,
|
||||||
|
'legal-batterie': legalBatterieZH
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
i18n
|
||||||
|
.use(LanguageDetector)
|
||||||
|
.use(initReactI18next)
|
||||||
|
.init({
|
||||||
|
resources,
|
||||||
|
fallbackLng: 'de', // German as fallback since it's your primary language
|
||||||
|
debug: process.env.NODE_ENV === 'development',
|
||||||
|
|
||||||
|
// Language detection options
|
||||||
|
detection: {
|
||||||
|
// Order of language detection methods
|
||||||
|
order: ['localStorage', 'navigator', 'htmlTag'],
|
||||||
|
// Cache the language selection
|
||||||
|
caches: ['localStorage'],
|
||||||
|
// Check for language in localStorage
|
||||||
|
lookupLocalStorage: 'i18nextLng'
|
||||||
|
},
|
||||||
|
|
||||||
|
interpolation: {
|
||||||
|
escapeValue: false // React already escapes values
|
||||||
|
},
|
||||||
|
|
||||||
|
// Namespace configuration
|
||||||
|
defaultNS: 'translation',
|
||||||
|
|
||||||
|
// React-specific options
|
||||||
|
react: {
|
||||||
|
useSuspense: false // Disable suspense for class components compatibility
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Export withI18n and other utilities for easy access
|
||||||
|
export { withI18n, withTranslation, withLanguage, LanguageContext, LanguageProvider } from './withTranslation.js';
|
||||||
|
|
||||||
|
export default i18n;
|
||||||
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": "تسجيل الدخول",
|
||||||
|
"register": "تسجيل",
|
||||||
|
"logout": "تسجيل خروج",
|
||||||
|
"profile": "الملف الشخصي",
|
||||||
|
"email": "البريد الإلكتروني",
|
||||||
|
"password": "كلمة المرور",
|
||||||
|
"confirmPassword": "تأكيد كلمة المرور",
|
||||||
|
"forgotPassword": "هل نسيت كلمة المرور؟",
|
||||||
|
"loginWithGoogle": "تسجيل الدخول باستخدام جوجل",
|
||||||
|
"or": "أو",
|
||||||
|
"privacyAccept": "بالنقر على \"تسجيل الدخول باستخدام جوجل\" أوافق على",
|
||||||
|
"privacyPolicy": "سياسة الخصوصية",
|
||||||
|
"passwordMinLength": "يجب أن تكون كلمة المرور 8 أحرف على الأقل",
|
||||||
|
"newPasswordMinLength": "يجب أن تكون كلمة المرور الجديدة 8 أحرف على الأقل",
|
||||||
|
"menu": {
|
||||||
|
"profile": "الملف الشخصي",
|
||||||
|
"myProfile": "ملفي الشخصي",
|
||||||
|
"checkout": "إتمام الشراء",
|
||||||
|
"orders": "الطلبات",
|
||||||
|
"settings": "الإعدادات",
|
||||||
|
"adminDashboard": "لوحة تحكم المسؤول",
|
||||||
|
"adminUsers": "مستخدمو المسؤول"
|
||||||
|
}
|
||||||
|
};
|
||||||
39
src/i18n/locales/ar/cart.js
Normal file
39
src/i18n/locales/ar/cart.js
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
export default {
|
||||||
|
"title": "العربة",
|
||||||
|
"empty": "فارغ",
|
||||||
|
"addToCart": "أضف إلى العربة",
|
||||||
|
"preorderCutting": "اطلب مسبقًا كقطع",
|
||||||
|
"continueShopping": "تابع التسوق",
|
||||||
|
"proceedToCheckout": "المتابعة إلى الدفع",
|
||||||
|
"productCount": "{{count}} {{count, plural, one {منتج} other {منتجات}}}",
|
||||||
|
"productSingular": "منتج",
|
||||||
|
"productPlural": "منتجات",
|
||||||
|
"removeFromCart": "إزالة من العربة",
|
||||||
|
"openCart": "افتح العربة",
|
||||||
|
"availableFrom": "متاح من {{date}}",
|
||||||
|
"backToOrder": "← العودة إلى الطلب",
|
||||||
|
"summary": {
|
||||||
|
"title": "ملخص الطلب",
|
||||||
|
"goodsNet": "البضائع (صافي):",
|
||||||
|
"shippingNet": "الشحن (صافي):",
|
||||||
|
"totalGoods": "إجمالي البضائع:",
|
||||||
|
"shippingCosts": "تكاليف الشحن:",
|
||||||
|
"total": "الإجمالي:",
|
||||||
|
"totalWeight": "الوزن الكلي: {{weight}} كجم",
|
||||||
|
"freeFrom100": "(مجاني من €100)",
|
||||||
|
"free": "مجاني"
|
||||||
|
},
|
||||||
|
"itemCount": {
|
||||||
|
"singular": "منتج",
|
||||||
|
"plural": "منتجات"
|
||||||
|
},
|
||||||
|
"sync": {
|
||||||
|
"title": "مزامنة العربة",
|
||||||
|
"description": "لديك عربة محفوظة في حسابك. يرجى اختيار كيفية المتابعة:",
|
||||||
|
"deleteServer": "حذف عربة الخادم",
|
||||||
|
"useServer": "استخدام عربة الخادم",
|
||||||
|
"merge": "دمج العربات",
|
||||||
|
"currentCart": "عربتك الحالية",
|
||||||
|
"serverCart": "العربة المحفوظة في ملفك الشخصي"
|
||||||
|
}
|
||||||
|
};
|
||||||
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": "تم القراءة والموافقة",
|
||||||
|
};
|
||||||
34
src/i18n/locales/ar/checkout.js
Normal file
34
src/i18n/locales/ar/checkout.js
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
export default {
|
||||||
|
"invoiceAddress": "عنوان الفاتورة",
|
||||||
|
"deliveryAddress": "عنوان التوصيل",
|
||||||
|
"saveForFuture": "احفظ للطلبات المستقبلية",
|
||||||
|
"pickupDate": "لمين التاريخ مطلوب استلام القصاصات؟",
|
||||||
|
"note": "ملاحظة",
|
||||||
|
"sameAddress": "عنوان التوصيل هو نفسه عنوان الفاتورة",
|
||||||
|
"termsAccept": "لقد قرأت الشروط والأحكام، سياسة الخصوصية، وأحكام حق الانسحاب",
|
||||||
|
"selectDeliveryMethod": "اختر طريقة الشحن",
|
||||||
|
"selectPaymentMethod": "اختر طريقة الدفع",
|
||||||
|
"orderSummary": "ملخص الطلب",
|
||||||
|
"addressValidationError": "يرجى التحقق من بياناتك في حقول العنوان.",
|
||||||
|
"processingOrder": "يتم معالجة الطلب...",
|
||||||
|
"completeOrder": "إتمام الطلب",
|
||||||
|
"termsValidationError": "يرجى قبول الشروط والأحكام، سياسة الخصوصية، وحق الانسحاب للمتابعة.",
|
||||||
|
"addressFields": {
|
||||||
|
"firstName": "الاسم الأول",
|
||||||
|
"lastName": "اسم العائلة",
|
||||||
|
"addressSupplement": "إضافة للعنوان",
|
||||||
|
"street": "الشارع",
|
||||||
|
"houseNumber": "رقم المنزل",
|
||||||
|
"postalCode": "الرمز البريدي",
|
||||||
|
"city": "المدينة",
|
||||||
|
"country": "البلد"
|
||||||
|
},
|
||||||
|
"validationErrors": {
|
||||||
|
"firstNameRequired": "الاسم الأول مطلوب",
|
||||||
|
"lastNameRequired": "اسم العائلة مطلوب",
|
||||||
|
"streetRequired": "الشارع مطلوب",
|
||||||
|
"houseNumberRequired": "رقم المنزل مطلوب",
|
||||||
|
"postalCodeRequired": "الرمز البريدي مطلوب",
|
||||||
|
"cityRequired": "المدينة مطلوبة"
|
||||||
|
}
|
||||||
|
};
|
||||||
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": "جارٍ التحميل...",
|
||||||
|
"error": "خطأ",
|
||||||
|
"close": "إغلاق",
|
||||||
|
"save": "حفظ",
|
||||||
|
"cancel": "إلغاء",
|
||||||
|
"ok": "موافق",
|
||||||
|
"yes": "نعم",
|
||||||
|
"no": "لا",
|
||||||
|
"next": "التالي",
|
||||||
|
"back": "رجوع",
|
||||||
|
"edit": "تعديل",
|
||||||
|
"delete": "حذف",
|
||||||
|
"add": "إضافة",
|
||||||
|
"remove": "إزالة",
|
||||||
|
"products": "منتجات",
|
||||||
|
"product": "منتج",
|
||||||
|
"days": "أيام"
|
||||||
|
};
|
||||||
35
src/i18n/locales/ar/delivery.js
Normal file
35
src/i18n/locales/ar/delivery.js
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
export default {
|
||||||
|
"methods": {
|
||||||
|
"dhl": "DHL",
|
||||||
|
"dpd": "DPD",
|
||||||
|
"sperrgut": "بضائع ضخمة",
|
||||||
|
"sperrgutName": "بضائع ضخمة",
|
||||||
|
"pickup": "استلام من المتجر"
|
||||||
|
},
|
||||||
|
"descriptions": {
|
||||||
|
"standard": "الشحن العادي",
|
||||||
|
"standardFree": "الشحن العادي - مجاني للطلبات فوق 100€!",
|
||||||
|
"notAvailable": "غير متاح للاختيار لأن عنصر واحد أو أكثر يمكن استلامه فقط",
|
||||||
|
"bulky": "للعناصر الكبيرة والثقيلة",
|
||||||
|
"pickupOnly": "الاستلام فقط"
|
||||||
|
},
|
||||||
|
"prices": {
|
||||||
|
"free": "مجاني",
|
||||||
|
"freeFrom100": "(مجاني من 100€)",
|
||||||
|
"dhl": "6.99 €",
|
||||||
|
"dpd": "4.90 €",
|
||||||
|
"sperrgut": "28.99 €"
|
||||||
|
},
|
||||||
|
"times": {
|
||||||
|
"cutting14Days": "مدة التوصيل: 14 يوم",
|
||||||
|
"standard2to3Days": "مدة التوصيل: 2-3 أيام",
|
||||||
|
"supplier7to9Days": "مدة التوصيل: 7-9 أيام"
|
||||||
|
},
|
||||||
|
"selector": {
|
||||||
|
"title": "اختر طريقة الشحن",
|
||||||
|
"freeShippingInfo": "💡 شحن مجاني للطلبات فوق 100€!",
|
||||||
|
"remainingForFree": "أضف {{amount}}€ أخرى للشحن المجاني.",
|
||||||
|
"congratsFreeShipping": "🎉 مبروك! حصلت على شحن مجاني!",
|
||||||
|
"cartQualifiesFree": "سلة مشترياتك بقيمة {{amount}}€ مؤهلة للشحن المجاني."
|
||||||
|
}
|
||||||
|
};
|
||||||
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": "الترتيب",
|
||||||
|
"perPage": "لكل صفحة",
|
||||||
|
"availability": "التوفر",
|
||||||
|
"manufacturer": "الشركة المصنعة",
|
||||||
|
"all": "الكل"
|
||||||
|
};
|
||||||
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 مساءً",
|
||||||
|
"address": "شارع تراشنبرجر 14 - دريسدن",
|
||||||
|
"location": "بين محطة بيسشن وميدان تراشنبرجر",
|
||||||
|
"allPricesIncl": "* جميع الأسعار تشمل ضريبة القيمة المضافة القانونية، بالإضافة إلى الشحن",
|
||||||
|
"copyright": "© {{year}} GrowHeads.de",
|
||||||
|
"legal": {
|
||||||
|
"datenschutz": "سياسة الخصوصية",
|
||||||
|
"agb": "الشروط والأحكام",
|
||||||
|
"sitemap": "خريطة الموقع",
|
||||||
|
"impressum": "الإشعار القانوني",
|
||||||
|
"batteriegesetzhinweise": "معلومات قانون البطاريات",
|
||||||
|
"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
|
||||||
|
};
|
||||||
69
src/i18n/locales/ar/legal-agb.js
Normal file
69
src/i18n/locales/ar/legal-agb.js
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
export default {
|
||||||
|
"title": "الشروط والأحكام العامة",
|
||||||
|
"deliveryShippingConditions": "شروط التسليم والشحن",
|
||||||
|
"deliveryTerms": {
|
||||||
|
"1": "يستغرق الشحن ما بين 1 إلى 7 أيام.",
|
||||||
|
"2": "تظل البضاعة ملكًا لشركة Growheads حتى يتم استلام الدفعة كاملة.",
|
||||||
|
"3": "إذا كان هناك اشتباه في تلف البضاعة أثناء النقل أو نقص في الأصناف، يجب الاحتفاظ بتغليف الشحن لفحصه من قبل خبير. يجب أن يؤكد الناقل أي تلف في التغليف على مذكرة التسليم مع تحديد نوع ومدى التلف. يجب الإبلاغ عن أضرار الشحن إلى Growheads فورًا كتابيًا عبر الفاكس أو البريد الإلكتروني أو البريد. لهذا الغرض، يجب التقاط صور للبضاعة التالفة وكذلك لصندوق الشحن التالف مع ملصق العنوان. يجب أيضًا الاحتفاظ بصندوق الشحن التالف. هذه الوثائق مطلوبة للمطالبة بالتعويض من شركة الشحن.",
|
||||||
|
"4": "عند إعادة البضائع المعيبة، يجب على العميل التأكد من تغليف البضائع بشكل صحيح.",
|
||||||
|
"5": "يجب تسجيل جميع المرتجعات مسبقًا لدى Growheads.",
|
||||||
|
"6": "يتحمل العميل مخاطر إرسال الأصناف إلينا، ما لم تكن إعادة البضائع المعيبة.",
|
||||||
|
"7": "يحق لـ Growheads أن تطلب استلام البضائع من خلال Deutsche Post/GLS أو ناقل من اختيارها.",
|
||||||
|
"8": "يتم حساب تكاليف البريد بناءً على الوزن. تحتفظ Growheads بحق تمرير أي زيادات في الأسعار من شركات الشحن (رسوم المرور، رسوم الوقود).",
|
||||||
|
"9": "عادةً ما يتم شحن طرودنا عبر: GLS، DHL و Deutsche Post AG.",
|
||||||
|
"10": "بالنسبة للأصناف الثقيلة أو الضخمة بشكل خاص، نحتفظ بحق فرض رسوم إضافية على تكاليف التسليم. عادةً ما تكون هذه الرسوم مذكورة في قائمة الأسعار.",
|
||||||
|
"11": "يمكن الدفع مقدمًا عن طريق التحويل البنكي إلى الحساب المصرفي المحدد.",
|
||||||
|
"12": "إذا حدث تأخير في التسليم نتحمل مسؤوليته، فإن فترة السماح التي يحق للمشتري تحديدها محدودة بأسبوعين. تبدأ الفترة من استلام Growheads لإشعار فترة السماح.",
|
||||||
|
"13": "يجب الإبلاغ كتابيًا عن العيوب الظاهرة في البضاعة فور التسليم. إذا لم يفعل العميل ذلك، تُستبعد مطالبات الضمان عن العيوب الظاهرة.",
|
||||||
|
"14": "إذا اشتكى العميل من عيب، يجب عليه إعادة البضاعة المعيبة إلينا مع وصف دقيق للعيب. يجب إرفاق نسخة من فاتورتنا مع الشحنة. يجب إعادة البضاعة في التغليف الأصلي أو في تغليف يحمي البضاعة بنفس طريقة التغليف الأصلي لتجنب التلف أثناء النقل العكسي."
|
||||||
|
},
|
||||||
|
"consultationLiability": {
|
||||||
|
"title": "الاستشارة والمسؤولية",
|
||||||
|
"1": "نقدم استشارات فنية تطبيقية بأفضل ما لدينا من معرفة بناءً على خبرتنا ومهاراتنا.",
|
||||||
|
"2": "المشتري مسؤول عن الالتزام باللوائح القانونية المتعلقة بالتخزين والنقل والاستخدام للبضائع."
|
||||||
|
},
|
||||||
|
"paymentConditions": {
|
||||||
|
"title": "شروط الدفع",
|
||||||
|
"1": "تظل البضاعة ملكًا لشركة Growheads حتى يتم استلام الدفعة كاملة.",
|
||||||
|
"2": "تُدفع الفواتير مقدمًا عن طريق التحويل البنكي إلى حسابنا. إذا دفعت مقدمًا، سيتم شحن البضاعة بمجرد تسجيل المبلغ في حسابنا."
|
||||||
|
},
|
||||||
|
"retentionOfTitle": {
|
||||||
|
"title": "الاحتفاظ بالملكية",
|
||||||
|
"content": "تظل البضاعة المسلمة ملكًا لشركة Growheads حتى يسدد المشتري جميع المطالبات الموجهة إليه. إذا قام البائع بإعادة بيع البضاعة، فإنه يتنازل بموجب هذا عن المطالبات الناشئة من البيع لنا. إذا تأخر المشتري في دفعاته، يمكننا في أي وقت طلب إعادة البضاعة دون الانسحاب من العقد."
|
||||||
|
},
|
||||||
|
"distanceSelling": {
|
||||||
|
"title": "معلومات وفقًا لقانون البيع عن بعد",
|
||||||
|
"intro": "تنطبق المعلومات التالية فقط على العقود المبرمة بين Growheads والمستهلكين عن طريق طلب الكتالوج أو الإنترنت أو وسائل الاتصال عن بعد الأخرى. وهي محدودة للمستهلكين داخل الاتحاد الأوروبي.",
|
||||||
|
"sections": {
|
||||||
|
"1": {
|
||||||
|
"title": "الخصائص الأساسية للبضاعة",
|
||||||
|
"content": "يرجى الرجوع إلى الشروحات في الكتالوج أو على موقعنا الإلكتروني لمعرفة الخصائص الأساسية للبضاعة. العروض في كتالوجنا وعلى موقعنا غير ملزمة. الطلبات المقدمة إلينا تعتبر عروضًا ملزمة. يمكن لـ Growheads قبولها خلال 14 يومًا من استلام الطلب عن طريق إرسال تأكيد الطلب أو شحن البضاعة."
|
||||||
|
},
|
||||||
|
"2": {
|
||||||
|
"title": "التحفظ",
|
||||||
|
"content": "إذا لم تكن جميع الأصناف المطلوبة متوفرة للتسليم، نحتفظ بحق التسليم الجزئي إذا كان ذلك معقولًا للعميل. قد تختلف بعض الأصناف عن الصور والوصف في الكتالوج والموقع الإلكتروني، خاصةً البضائع المصنوعة يدويًا. لذلك نحتفظ بحق تسليم بضائع ذات جودة وسعر معادل إذا لزم الأمر."
|
||||||
|
},
|
||||||
|
"3": {
|
||||||
|
"title": "الأسعار والضرائب",
|
||||||
|
"content": "يمكنك العثور على أسعار الأصناف الفردية شاملة ضريبة القيمة المضافة في الكتالوج أو على موقعنا. تصبح الأسعار غير صالحة عند صدور كتالوج جديد."
|
||||||
|
},
|
||||||
|
"4": "جميع الأسعار عرضة للأخطاء أو تقلبات الأسعار. إذا حدث تغيير في السعر، يحق للمشتري ممارسة حقه في الإرجاع.",
|
||||||
|
"5": {
|
||||||
|
"title": "مدة الضمان",
|
||||||
|
"content": "تطبق فترة الضمان القانونية لمدة 24 (أربعة وعشرين) شهرًا. في حالات فردية، قد تنطبق فترات أطول إذا منحها المصنع."
|
||||||
|
},
|
||||||
|
"6": {
|
||||||
|
"title": "حق الإرجاع / حق الانسحاب",
|
||||||
|
"content": "يتمتع العميل بحق إرجاع لمدة 14 يومًا.\nتبدأ الفترة من استلام العميل للبضاعة وتُعتبر محفوظة بإرسال الانسحاب في الوقت المناسب إلى Growheads. تستثنى من ذلك الأغذية والسلع القابلة للتلف، وكذلك المنتجات المصممة خصيصًا أو البضائع التي تم طلبها بناءً على رغبة العميل. يجب أن يتم الإرجاع عن طريق إعادة البضاعة خلال الفترة. إذا لم يكن بالإمكان شحن البضاعة، يجب إرسال طلب الإرجاع إلينا خلال الفترة عن طريق رسالة، بطاقة بريدية، بريد إلكتروني، أو وسيلة دائمة أخرى. يكفي الإرسال في الوقت المناسب إلى العنوان المذكور تحت 7) للحفاظ على المهلة. لا يتطلب الانسحاب سببًا. سيتم رد ثمن الشراء وأي تكاليف تسليم وشحن بعد استلامنا للبضاعة. القيمة الحاسمة هي قيمة البضاعة المعادة وقت الشراء، وليس قيمة الطلب الكامل. عادةً ما يمكن لـ Growheads ترتيب الاستلام منك."
|
||||||
|
},
|
||||||
|
"7": {
|
||||||
|
"title": "اسم وعنوان الشركة، الشكاوى، الاستدعاءات",
|
||||||
|
"content": "Growheads\nTrachenberger Straße 14\n01129 Dresden"
|
||||||
|
},
|
||||||
|
"8": {
|
||||||
|
"title": "مكان التنفيذ والاختصاص القضائي",
|
||||||
|
"content": "مكان التنفيذ والاختصاص القضائي لجميع المطالبات هو دريسدن، ما لم تنص أحكام قانونية إلزامية على خلاف ذلك."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
8
src/i18n/locales/ar/legal-batterie.js
Normal file
8
src/i18n/locales/ar/legal-batterie.js
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
export default {
|
||||||
|
"title": "معلومات قانون البطاريات",
|
||||||
|
"intro": "فيما يتعلق ببيع البطاريات أو تسليم الأجهزة التي تحتوي على بطاريات، نحن ملزمون بإبلاغكم بما يلي:",
|
||||||
|
"returnObligation": "بصفتك مستخدم نهائي، أنت ملزم قانونيًا بإعادة البطاريات المستخدمة. يمكنك إعادة البطاريات القديمة التي نمتلكها أو التي كانت ضمن مجموعتنا كبطاريات جديدة مجانًا إلى مستودع الشحن الخاص بنا (عنوان الشحن).",
|
||||||
|
"symbolsInfo": "الرموز المعروضة على البطاريات تعني ما يلي:",
|
||||||
|
"wasteSymbol": "رمز سلة المهملات المعلمة بعلامة إلغاء يعني أنه لا يجوز التخلص من البطارية مع النفايات المنزلية.",
|
||||||
|
"chemicalSymbols": "Pb = البطارية تحتوي على أكثر من 0.004 بالمئة بالوزن من الرصاص\nCd = البطارية تحتوي على أكثر من 0.002 بالمئة بالوزن من الكادميوم\nHg = البطارية تحتوي على أكثر من 0.0005 بالمئة بالوزن من الزئبق."
|
||||||
|
};
|
||||||
70
src/i18n/locales/ar/legal-datenschutz.js
Normal file
70
src/i18n/locales/ar/legal-datenschutz.js
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
export default {
|
||||||
|
"title": "سياسة الخصوصية",
|
||||||
|
"responsibleParty": {
|
||||||
|
"title": "الجهة المسؤولة بموجب قانون حماية البيانات:",
|
||||||
|
"company": "Growheads\nTrachenberger Straße 14\n01129 Dresden"
|
||||||
|
},
|
||||||
|
"generalInfo": "ما لم يُذكر خلاف ذلك أدناه، فإن تقديم بياناتك الشخصية ليس مطلوبًا قانونيًا أو تعاقديًا، ولا هو ضروري لإبرام عقد. أنت غير ملزم بتقديم البيانات. عدم تقديمها لا يترتب عليه أي عواقب. هذا ينطبق فقط طالما لم يُذكر خلاف ذلك في عمليات المعالجة التالية. \"البيانات الشخصية\" تعني جميع المعلومات المتعلقة بشخص طبيعي محدد أو قابل للتحديد.",
|
||||||
|
"sections": {
|
||||||
|
"informationDeletion": {
|
||||||
|
"title": "المعلومات، الحذف، الحظر",
|
||||||
|
"content": "يمكنك في أي وقت طلب معلومات عن بياناتك الشخصية، مصدرها والمستلمين لها، وهدف معالجة البيانات، كما يمكنك طلب تصحيح أو حظر أو حذف هذه البيانات مجانًا. يرجى استخدام خيارات الاتصال الموجودة في تذييل الصفحة أو في الإشعار القانوني لهذا الغرض. نحن متاحون أيضًا في أي وقت لأي أسئلة إضافية حول الموضوع. يرجى ملاحظة أننا غير مخولين ولن نقوم بحذف بيانات الفواتير، بيانات البنك، والبيانات التي تم إرسالها لمزود خدمة الشحن. البيانات التي يمكن حذفها تشمل: حسابات العملاء على خادم الويب، وكذلك في نظام إدارة البضائع، والبريد الإلكتروني الذي لا يرتبط مباشرة بطلب.",
|
||||||
|
},
|
||||||
|
"serverLogfiles": {
|
||||||
|
"title": "ملفات سجل الخادم",
|
||||||
|
"content": "يمكنك زيارة مواقعنا الإلكترونية دون تقديم أي معلومات عن نفسك. في كل مرة تصل فيها إلى موقعنا، يتم إرسال بيانات الاستخدام من متصفح الإنترنت الخاص بك وتخزينها في بيانات السجل (ملفات سجل الخادم). تشمل هذه البيانات المخزنة، على سبيل المثال، اسم الصفحة التي تم الوصول إليها، تاريخ ووقت الوصول، كمية البيانات المنقولة، ومزود الخدمة الذي يطلب البيانات. تُستخدم هذه البيانات فقط لضمان التشغيل السلس لموقعنا وتحسين عرضنا. هذه البيانات ليست بيانات شخصية. لا يتم دمج هذه البيانات مع مصادر بيانات أخرى. إذا علمنا بمؤشرات محددة على استخدام غير قانوني، نحتفظ بالحق في مراجعة هذه البيانات لاحقًا.",
|
||||||
|
},
|
||||||
|
"customerAccount": {
|
||||||
|
"title": "حساب العميل",
|
||||||
|
"content": "عند فتح حساب عميل، نجمع بياناتك الشخصية بالقدر المحدد هناك. تهدف معالجة البيانات إلى تحسين تجربة التسوق الخاصة بك وتبسيط معالجة الطلبات. تتم المعالجة بناءً على المادة 6 (1) حرف أ من DSGVO بموافقتك. يمكنك سحب موافقتك في أي وقت بإبلاغنا، دون التأثير على قانونية المعالجة التي تمت بناءً على الموافقة حتى السحب. سيتم حذف حساب العميل الخاص بك بعد ذلك.",
|
||||||
|
},
|
||||||
|
"googleSSO": {
|
||||||
|
"title": "تسجيل الدخول باستخدام Google (تسجيل الدخول الموحد من Google)",
|
||||||
|
"content": "نقدم لك خيار تسجيل الدخول إلى حساب العميل الخاص بك باستخدام حساب Google الخاص بك. عند استخدام وظيفة \"تسجيل الدخول باستخدام Google\"، يتم التحقق من الهوية عبر خدمة Google Single Sign-On. في هذه العملية، قد يتم تخزين ملفات تعريف الارتباط من Google على جهازك، وهي ضرورية لعملية تسجيل الدخول والتحقق من الهوية. كجزء من تسجيل الدخول عبر Google، نتلقى من Google بيانات شخصية معينة للتحقق من هويتك. على وجه الخصوص، تنقل Google لنا اسمك، عنوان بريدك الإلكتروني، وإذا كان مخزنًا في حساب Google الخاص بك، صورة ملفك الشخصي. يتم توفير هذه المعلومات من Google بمجرد تسجيل دخولك إلى متجرنا الإلكتروني باستخدام حساب Google الخاص بك. يمكن لـ Google، كمزود طرف ثالث، الوصول إلى هذه البيانات ومعالجتها؛ وقد يشمل ذلك نقل البيانات إلى الولايات المتحدة الأمريكية. لقد أبرمنا مع Google بنود حماية بيانات قياسية وفقًا للمادة 46 (2) حرف ج من DSGVO لضمان مستوى مناسب من حماية البيانات عند نقل بياناتك. يمكن العثور على مزيد من التفاصيل حول معالجة البيانات بواسطة Google في سياسة الخصوصية الخاصة بـ Google (على https://policies.google.com/privacy?hl=en).",
|
||||||
|
"legalBasis": "تتم معالجة البيانات المتعلقة بتسجيل الدخول عبر Google بناءً على المادة 6 (1) حرف ب من DSGVO (تنفيذ التدابير التمهيدية للعقد وتنفيذ العقد، مثل إنشاء واستخدام حساب العميل الخاص بك) وكذلك المادة 6 (1) حرف ف من DSGVO (مصلحتنا المشروعة في توفير خيار تسجيل دخول سريع ومريح لك).",
|
||||||
|
"voluntaryUse": "استخدام وظيفة \"تسجيل الدخول باستخدام Google\" هو أمر طوعي. بالطبع يمكنك أيضًا استخدام متجرنا الإلكتروني وحساب العميل الخاص بك بدون Google SSO عن طريق التسجيل أو تسجيل الدخول باستخدام بريدك الإلكتروني وكلمة المرور كالمعتاد. إذا اخترت استخدام تسجيل الدخول عبر Google، يمكنك قطع هذا الرابط في أي وقت عن طريق إزالة الاتصال في إعدادات حساب Google الخاص بك.",
|
||||||
|
"yourRights": "فيما يتعلق بالبيانات الشخصية المعالجة عبر Google SSO، لديك الحقوق القانونية كصاحب بيانات. على وجه الخصوص، لديك الحق في الحصول على معلومات حول البيانات المخزنة عنك (المادة 15 DSGVO)، وتصحيح البيانات غير الدقيقة (المادة 16 DSGVO)، أو طلب حذف بياناتك (المادة 17 DSGVO). علاوة على ذلك، لديك الحق في تقييد معالجة بياناتك (المادة 18 DSGVO) والحق في نقل البيانات (المادة 20 DSGVO). إذا استندنا في المعالجة إلى مصلحتنا المشروعة، يمكنك الاعتراض على المعالجة (المادة 21 DSGVO). بالإضافة إلى ذلك، يمكنك الاتصال في أي وقت بالسلطة المختصة لحماية البيانات لتقديم شكوى. تنطبق حقوقك وخياراتك القائمة من بقية سياسة الخصوصية أيضًا على استخدام تسجيل الدخول عبر Google.",
|
||||||
|
},
|
||||||
|
"orders": {
|
||||||
|
"title": "جمع، معالجة واستخدام البيانات الشخصية للطلبات",
|
||||||
|
"content": "عند تقديم طلب، نجمع ونستخدم بياناتك الشخصية فقط بالقدر اللازم لتنفيذ ومعالجة طلبك وللتعامل مع استفساراتك. تقديم البيانات مطلوب لإبرام العقد. عدم تقديمها يؤدي إلى عدم إبرام العقد. تتم المعالجة بناءً على المادة 6 (1) حرف ب من DSGVO وهي ضرورية لتنفيذ عقد معك. لن يتم تمرير بياناتك إلى أطراف ثالثة بدون موافقتك الصريحة. الاستثناء الوحيد هم شركاؤنا في الخدمة الذين نحتاجهم لمعالجة العلاقة التعاقدية أو مزودو الخدمات الذين نستخدمهم في إطار معالجة بالنيابة. بالإضافة إلى المستلمين المذكورين في البنود الخاصة بهذه السياسة، يشمل ذلك على سبيل المثال مستلمي الفئات التالية: مزودو خدمات الشحن، مزودو خدمات الدفع، مزودو خدمات إدارة البضائع، مزودو خدمات معالجة الطلبات، مستضيفو الويب، مزودو خدمات تكنولوجيا المعلومات وتجار الدروبشيبينغ. في جميع الحالات، نلتزم بدقة بالمتطلبات القانونية. نطاق نقل البيانات محدود إلى الحد الأدنى.",
|
||||||
|
},
|
||||||
|
"newsletter": {
|
||||||
|
"title": "استخدام عنوان البريد الإلكتروني لإرسال النشرات الإخبارية",
|
||||||
|
"content": "نستخدم عنوان بريدك الإلكتروني بشكل مستقل عن معالجة العقد فقط لأغراضنا الإعلانية الخاصة لإرسال النشرات الإخبارية، بشرط أن تكون قد وافقت على ذلك صراحة. تتم المعالجة بناءً على المادة 6 (1) حرف أ من DSGVO بموافقتك. يمكنك سحب موافقتك في أي وقت دون التأثير على قانونية المعالجة التي تمت بناءً على الموافقة حتى السحب. يمكنك إلغاء الاشتراك في النشرة الإخبارية في أي وقت باستخدام الرابط المناسب في النشرة أو بإبلاغنا. سيتم بعد ذلك إزالة عنوان بريدك الإلكتروني من قائمة التوزيع. يتم تمرير بياناتك إلى مزود خدمة للتسويق عبر البريد الإلكتروني في إطار معالجة بالنيابة. لا يتم نقلها إلى أطراف ثالثة أخرى. سيتم نقل بياناتك إلى دولة ثالثة يوجد بشأنها قرار كفاية من المفوضية الأوروبية.",
|
||||||
|
},
|
||||||
|
"chatbot": {
|
||||||
|
"title": "استخدام روبوت الدردشة الذكي (OpenAI API)",
|
||||||
|
"content": "نستخدم روبوت دردشة مدعوم بالذكاء الاصطناعي على موقعنا الإلكتروني، يتم تشغيله عبر واجهة برمجة التطبيقات (API) لمزود الخدمة OpenAI. يهدف روبوت الدردشة إلى الرد على استفسارات الزوار بكفاءة وبشكل آلي، مما يوفر وظيفة دعم. عند استخدام روبوت الدردشة، تتم معالجة مدخلاتك بواسطة النظام لتوليد ردود مناسبة. تتم المعالجة بشكل مجهول - لا يتم جمع أو تخزين عناوين IP أو بيانات شخصية أخرى (مثل الاسم أو بيانات الاتصال).",
|
||||||
|
"legalBasis": "الأساس القانوني لاستخدام روبوت الدردشة هو مصلحتنا المشروعة وفقًا للمادة 6 (1) حرف ف من DSGVO. تكمن هذه المصلحة في توفير دعم فعال للزوار وتحسين تجربة المستخدم على موقعنا.",
|
||||||
|
"dataRecipient": "المستلم لبيانات الدردشة هو OpenAI (OpenAI OpCo, LLC) كمزود خدمة فني. تقوم OpenAI بمعالجة محتوى الدردشة المرسل على خوادمها فقط لغرض توليد الردود. تعمل OpenAI كمعالج بيانات وفقًا للمادة 28 من DSGVO ولا تستخدم البيانات لأغراضها الخاصة. لقد أبرمنا عقد معالجة بيانات مع OpenAI يتضمن بنودًا تعاقدية قياسية للاتحاد الأوروبي كضمانات مناسبة لحماية البيانات. يقع المقر الرئيسي لـ OpenAI في الولايات المتحدة الأمريكية؛ يضمن الاتفاق على البنود التعاقدية القياسية مستوى حماية بيانات مناسبًا يتوافق مع الاتحاد الأوروبي عند نقل بياناتك.",
|
||||||
|
"dataRetention": "نحتفظ بطلبات الدردشة الخاصة بك فقط طالما كان ذلك ضروريًا للمعالجة والرد. بمجرد الانتهاء من طلبك، يتم حذف أو إخفاء سجلات الدردشة بسرعة. وفقًا لما صرحت به OpenAI، يتم الاحتفاظ ببيانات الدردشة المعالجة مؤقتًا فقط ويتم حذفها تلقائيًا بعد 30 يومًا كحد أقصى.",
|
||||||
|
"voluntaryUse": "استخدام روبوت الدردشة هو أمر طوعي. إذا لم تستخدم روبوت الدردشة، فلن يتم نقل أي بيانات إلى OpenAI. يرجى عدم إدخال أي بيانات شخصية حساسة في الدردشة.",
|
||||||
|
},
|
||||||
|
"cookies": {
|
||||||
|
"title": "ملفات تعريف الارتباط (Cookies)",
|
||||||
|
"intro": "يستخدم موقعنا ملفات تعريف الارتباط في الحالات التالية:",
|
||||||
|
"payment": "1. عملية الدفع: عند الدفع ببطاقات الائتمان أو التحويلات الفورية (مثل Klarna Instant)، يتم استخدام ملفات تعريف ارتباط ضرورية تقنيًا. تحتوي هذه على سلسلة مميزة تتيح التعرف الفريد على المتصفح. يتم تعيين ملفات تعريف الارتباط بواسطة مزود خدمة الدفع Stripe وهي ضرورية تمامًا لمعالجة المدفوعات بأمان وسلاسة. بدون هذه الملفات، لا يمكن إتمام الطلب باستخدام هذه طرق الدفع. تتم المعالجة بناءً على المادة 6 (1) حرف ب من DSGVO لتنفيذ العقد.",
|
||||||
|
"googleSSO": "2. تسجيل الدخول الموحد من Google (SSO): عند استخدام تسجيل الدخول عبر Google، يتم تعيين ملفات تعريف ارتباط بواسطة Google ضرورية لعملية تسجيل الدخول والتحقق من الهوية. تتيح لك هذه الملفات تسجيل الدخول بسهولة باستخدام حساب Google الخاص بك دون الحاجة لتسجيل الدخول في كل مرة. تتم المعالجة بناءً على المادة 6 (1) حرف ب من DSGVO (تنفيذ العقد) والمادة 6 (1) حرف ف من DSGVO (مصلحة مشروعة في تسجيل دخول سهل الاستخدام).",
|
||||||
|
"otherPayments": "بالنسبة لطرق الدفع الأخرى – الخصم المباشر، الاستلام أو الدفع عند الاستلام – لا يتم استخدام ملفات تعريف ارتباط إضافية، ما لم تستخدم تسجيل الدخول عبر Google.",
|
||||||
|
},
|
||||||
|
"mollie": {
|
||||||
|
"title": "Mollie (معالجة الدفع)",
|
||||||
|
"content": "نستخدم مزود خدمة الدفع Mollie على موقعنا لمعالجة المدفوعات. مزود الخدمة هو Mollie B.V.، Keizersgracht 126، 1015 CW Amsterdam، هولندا. في هذا السياق، يتم نقل البيانات الشخصية اللازمة لمعالجة الدفع إلى Mollie - على وجه الخصوص اسمك، عنوان بريدك الإلكتروني، عنوان الفاتورة، معلومات الدفع (مثل بيانات بطاقة الائتمان) وعنوان IP. تتم معالجة البيانات لغرض معالجة الدفع؛ الأساس القانوني هو المادة 6 (1) حرف ب من DSGVO، لأنها تخدم تنفيذ عقد معك.",
|
||||||
|
"responsibility": "تعالج Mollie أيضًا بعض البيانات كمسؤول مستقل، على سبيل المثال للوفاء بالالتزامات القانونية (مثل مكافحة غسيل الأموال) ومنع الاحتيال. بالإضافة إلى ذلك، أبرمنا عقد معالجة بيانات مع Mollie وفقًا للمادة 28 من DSGVO؛ في إطار هذا العقد، تتصرف Mollie عند معالجة المدفوعات فقط وفقًا لتعليماتنا.",
|
||||||
|
"dataTransfer": "في حال معالجة Mollie بيانات شخصية خارج الاتحاد الأوروبي، وخاصة في الولايات المتحدة الأمريكية، يتم ذلك مع الالتزام بضمانات مناسبة. تستخدم Mollie البنود التعاقدية القياسية للاتحاد الأوروبي وفقًا للمادة 46 من DSGVO لضمان مستوى مناسب من حماية البيانات. ومع ذلك، نشير إلى أن الولايات المتحدة تُعتبر دولة ثالثة قد لا توفر مستوى حماية بيانات كافٍ بموجب قانون حماية البيانات. يمكن العثور على مزيد من المعلومات في سياسة الخصوصية الخاصة بـ Mollie على https://www.mollie.com/en/privacy.",
|
||||||
|
},
|
||||||
|
"dataRetention": {
|
||||||
|
"title": "مدة التخزين",
|
||||||
|
"content": "بعد إتمام معالجة العقد بالكامل، يتم تخزين البيانات في البداية لمدة فترة الضمان، ثم مع مراعاة الفترات القانونية، وخاصة فترات الحفظ الضريبية والتجارية، ثم يتم حذفها بعد انتهاء الفترة، ما لم تكن قد وافقت على المعالجة والاستخدام الإضافيين.",
|
||||||
|
},
|
||||||
|
"dataSubjectRights": {
|
||||||
|
"title": "حقوق صاحب البيانات",
|
||||||
|
"content": "إذا توفرت الشروط القانونية، لديك الحقوق التالية وفقًا للمادة 15 إلى 20 من DSGVO: الحق في الحصول على المعلومات، التصحيح، الحذف، تقييد المعالجة، نقل البيانات. بالإضافة إلى ذلك، وفقًا للمادة 21 (1) من DSGVO، لديك الحق في الاعتراض على المعالجة التي تستند إلى المادة 6 (1) حرف ف من DSGVO، وكذلك على المعالجة لأغراض التسويق المباشر. اتصل بنا إذا رغبت. يمكنك العثور على بيانات الاتصال في إشعارنا القانوني.",
|
||||||
|
},
|
||||||
|
"supervisoryAuthority": {
|
||||||
|
"title": "الحق في تقديم شكوى إلى السلطة الرقابية",
|
||||||
|
"content": "وفقًا للمادة 77 من DSGVO، لديك الحق في تقديم شكوى إلى السلطة الرقابية إذا كنت تعتقد أن معالجة بياناتك الشخصية غير قانونية.",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
25
src/i18n/locales/ar/legal-impressum.js
Normal file
25
src/i18n/locales/ar/legal-impressum.js
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
export default {
|
||||||
|
"title": "الإشعار القانوني (Impressum)",
|
||||||
|
"sections": {
|
||||||
|
"operator": {
|
||||||
|
"title": "المشغل والمسؤول عن محتوى هذا المتجر هو:",
|
||||||
|
"content": "Growheads\nMax Schön\nTrachenberger Straße 14\n01129 Dresden"
|
||||||
|
},
|
||||||
|
"contact": {
|
||||||
|
"title": "الاتصال:",
|
||||||
|
"content": "البريد الإلكتروني: service@growheads.de"
|
||||||
|
},
|
||||||
|
"vatId": {
|
||||||
|
"title": "رقم ضريبة القيمة المضافة:",
|
||||||
|
"content": "رقم ضريبة القيمة المضافة: DE323017152"
|
||||||
|
},
|
||||||
|
"disclaimer": {
|
||||||
|
"title": "تنصل من المسؤولية:",
|
||||||
|
"content": "لا نتحمل أي مسؤولية عن محتوى عناوين الإنترنت الخارجية المرتبطة بهذه الصفحات. المشغلون المعنيون هم المسؤولون عن محتوى المواقع غير التابعة للشركة."
|
||||||
|
},
|
||||||
|
"copyright": {
|
||||||
|
"title": "بند حقوق النشر:",
|
||||||
|
"content": "المحتوى المعروض هنا يخضع بشكل عام لحقوق النشر ولا يجوز توزيعه إلا بموافقة كتابية.\nحقوق المواد المصورة أو النصية الخاصة بأطراف أخرى ليست مقيدة أو ملغاة بهذا البند."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
11
src/i18n/locales/ar/legal-widerruf.js
Normal file
11
src/i18n/locales/ar/legal-widerruf.js
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
export default {
|
||||||
|
"title": "حق الانسحاب",
|
||||||
|
"withdrawalRight": "لديك الحق في الانسحاب من هذا العقد خلال أربعة عشر يومًا دون إبداء أي سبب. تبدأ فترة الانسحاب من اليوم الذي تستلم فيه أنت أو طرف ثالث تعينه، وليس الناقل، البضائع.",
|
||||||
|
"exerciseWithdrawal": "لممارسة حقك في الانسحاب، يجب عليك إبلاغنا",
|
||||||
|
"contactInfo": "Growheads\nTrachenberger Straße 14\n01129 Dresden\nE-Mail: service@growheads.de",
|
||||||
|
"withdrawalProcess": "ببيان واضح (مثل رسالة مرسلة بالبريد، فاكس أو بريد إلكتروني) عن قرارك بالانسحاب من هذا العقد. يمكنك استخدام نموذج الانسحاب المرفق لهذا الغرض، لكنه ليس إلزاميًا. وللحفاظ على مهلة الانسحاب، يكفي أن ترسل إشعارك بممارسة حق الانسحاب قبل انتهاء فترة الانسحاب.",
|
||||||
|
"consequencesTitle": "عواقب الانسحاب",
|
||||||
|
"consequences": "إذا انسحبت من هذا العقد، سنرد لك جميع المدفوعات التي تلقيناها منك، بما في ذلك تكاليف التوصيل (باستثناء التكاليف الإضافية الناتجة عن اختيارك لنوع توصيل غير أرخص توصيل قياسي نقدمه)، دون تأخير غير مبرر وفي موعد أقصاه أربعة عشر يومًا من اليوم الذي استلمنا فيه إشعار انسحابك من هذا العقد. سنستخدم نفس وسيلة الدفع التي استخدمتها في المعاملة الأصلية لهذا السداد، ما لم يتم الاتفاق معك صراحة على خلاف ذلك؛ ولن تُفرض عليك أي رسوم مقابل هذا السداد. قد نرفض السداد حتى نستلم البضائع مرة أخرى أو تقدم دليلاً على إرسال البضائع، أيهما أسبق. يجب عليك إعادة البضائع أو تسليمها لنا دون تأخير غير مبرر وفي كل الأحوال خلال أربعة عشر يومًا من اليوم الذي تخطرنا فيه بانسحابك من هذا العقد. تُعتبر المهلة محفوظة إذا أرسلت البضائع قبل انتهاء فترة الأربعة عشر يومًا. تتحمل أنت التكاليف المباشرة لإعادة البضائع. أنت مسؤول فقط عن أي انخفاض في قيمة البضائع ناتج عن تعامل غير ضروري لتحديد طبيعة وخصائص وعمل البضائع.",
|
||||||
|
"noWithdrawalTitle": "إشعار بعدم وجود حق الانسحاب",
|
||||||
|
"noWithdrawal": "لا ينطبق حق الانسحاب على البضائع التي تم تصنيعها أو تفصيلها حسب طلب العميل (الأفلام والأنابيب)، لكن يمكن منحه باتفاق. كما أن حاويات الأسمدة التي تم إزالة ختمها أو تدميره بفتحها مستثناة أيضًا من حق الانسحاب."
|
||||||
|
};
|
||||||
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"
|
||||||
|
};
|
||||||
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": "الرئيسية",
|
||||||
|
"aktionen": "العروض",
|
||||||
|
"filiale": "الفرع",
|
||||||
|
"categories": "الفئات",
|
||||||
|
"categoriesOpen": "افتح الفئات",
|
||||||
|
"categoriesClose": "أغلق الفئات",
|
||||||
|
"otherCategories": "فئات أخرى"
|
||||||
|
};
|
||||||
50
src/i18n/locales/ar/orders.js
Normal file
50
src/i18n/locales/ar/orders.js
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
export default {
|
||||||
|
"status": {
|
||||||
|
"new": "قيد التنفيذ",
|
||||||
|
"pending": "جديد",
|
||||||
|
"processing": "قيد التنفيذ",
|
||||||
|
"cancelled": "ملغاة",
|
||||||
|
"shipped": "تم الشحن",
|
||||||
|
"delivered": "تم التوصيل",
|
||||||
|
"return": "إرجاع",
|
||||||
|
"partialReturn": "إرجاع جزئي",
|
||||||
|
"partialDelivered": "تم التوصيل جزئياً"
|
||||||
|
},
|
||||||
|
"table": {
|
||||||
|
"orderNumber": "رقم الطلب",
|
||||||
|
"date": "التاريخ",
|
||||||
|
"status": "الحالة",
|
||||||
|
"items": "العناصر",
|
||||||
|
"total": "الإجمالي",
|
||||||
|
"actions": "الإجراءات",
|
||||||
|
"viewDetails": "عرض التفاصيل"
|
||||||
|
},
|
||||||
|
"tooltips": {
|
||||||
|
"viewDetails": "عرض التفاصيل",
|
||||||
|
"cancelOrder": "إلغاء الطلب"
|
||||||
|
},
|
||||||
|
"noOrders": "لم تقم بوضع أي طلبات بعد.",
|
||||||
|
"details": {
|
||||||
|
"title": "تفاصيل الطلب: {{orderId}}",
|
||||||
|
"deliveryAddress": "عنوان التوصيل",
|
||||||
|
"invoiceAddress": "عنوان الفاتورة",
|
||||||
|
"orderDetails": "تفاصيل الطلب",
|
||||||
|
"deliveryMethod": "طريقة التوصيل:",
|
||||||
|
"paymentMethod": "طريقة الدفع:",
|
||||||
|
"notSpecified": "غير محدد",
|
||||||
|
"orderedItems": "العناصر المطلوبة",
|
||||||
|
"item": "العنصر",
|
||||||
|
"quantity": "الكمية",
|
||||||
|
"price": "السعر",
|
||||||
|
"vat": "ضريبة القيمة المضافة",
|
||||||
|
"total": "الإجمالي",
|
||||||
|
"cancelOrder": "إلغاء الطلب"
|
||||||
|
},
|
||||||
|
"cancelConfirm": {
|
||||||
|
"title": "إلغاء الطلب",
|
||||||
|
"message": "هل أنت متأكد أنك تريد إلغاء هذا الطلب؟",
|
||||||
|
"confirm": "إلغاء الطلب",
|
||||||
|
"cancelling": "جارٍ الإلغاء..."
|
||||||
|
},
|
||||||
|
"processing": "يتم إكمال الطلب...",
|
||||||
|
};
|
||||||
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": "استعارة معصرة زيت",
|
||||||
|
"comingSoon": "المحتوى قادم قريباً..."
|
||||||
|
},
|
||||||
|
"thcTest": {
|
||||||
|
"title": "اختبار THC",
|
||||||
|
"comingSoon": "المحتوى قادم قريباً..."
|
||||||
|
}
|
||||||
|
};
|
||||||
21
src/i18n/locales/ar/payment.js
Normal file
21
src/i18n/locales/ar/payment.js
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
export default {
|
||||||
|
"successful": "تم الدفع بنجاح!",
|
||||||
|
"failed": "فشل الدفع",
|
||||||
|
"orderCompleted": "🎉 تم إكمال طلبك بنجاح! يمكنك الآن عرض طلباتك.",
|
||||||
|
"orderProcessing": "تمت معالجة دفعتك بنجاح. سيتم إكمال الطلب تلقائيًا.",
|
||||||
|
"paymentError": "لم نتمكن من معالجة دفعتك. يرجى المحاولة مرة أخرى أو اختيار طريقة دفع أخرى.",
|
||||||
|
"viewOrders": "عرض طلباتي",
|
||||||
|
"loadingPaymentComponent": "جارٍ تحميل مكون الدفع...",
|
||||||
|
"methods": {
|
||||||
|
"selectPaymentMethod": "اختر طريقة الدفع",
|
||||||
|
"bankTransfer": "تحويل بنكي",
|
||||||
|
"bankTransferDescription": "ادفع عن طريق التحويل البنكي",
|
||||||
|
"cardPayment": "بطاقة، Sofortüberweisung، Apple Pay، Google Pay، PayPal",
|
||||||
|
"cardPaymentDescription": "ادفع بالبطاقة أو Sofortüberweisung",
|
||||||
|
"cardPaymentMinAmount": "ادفع بالبطاقة أو Sofortüberweisung (الحد الأدنى: €0.50)",
|
||||||
|
"cashOnDelivery": "الدفع عند الاستلام",
|
||||||
|
"cashOnDeliveryDescription": "ادفع عند الاستلام (رسوم إضافية €8.99)",
|
||||||
|
"cashInStore": "الدفع في المتجر",
|
||||||
|
"cashInStoreDescription": "ادفع عند الاستلام",
|
||||||
|
}
|
||||||
|
};
|
||||||
47
src/i18n/locales/ar/product.js
Normal file
47
src/i18n/locales/ar/product.js
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
export default {
|
||||||
|
"loading": "جارٍ تحميل المنتج...",
|
||||||
|
"notFound": "المنتج غير موجود",
|
||||||
|
"notFoundDescription": "المنتج الذي تبحث عنه غير موجود أو تم إزالته.",
|
||||||
|
"backToHome": "العودة إلى الصفحة الرئيسية",
|
||||||
|
"error": "خطأ",
|
||||||
|
"articleNumber": "رقم الصنف",
|
||||||
|
"manufacturer": "الشركة المصنعة",
|
||||||
|
"inclVat": "شامل {{vat}}% ضريبة القيمة المضافة",
|
||||||
|
"priceUnit": "{{price}}/{{unit}}",
|
||||||
|
"new": "جديد",
|
||||||
|
"weeks": "أسابيع",
|
||||||
|
"arriving": "الوصول:",
|
||||||
|
"inclVatFooter": "شامل {{vat}}% ضريبة القيمة المضافة,*",
|
||||||
|
"availability": "التوفر",
|
||||||
|
"inStock": "متوفر في المخزون",
|
||||||
|
"comingSoon": "قريبًا",
|
||||||
|
"deliveryTime": "مدة التوصيل",
|
||||||
|
"inclShort": "شامل",
|
||||||
|
"vatShort": "ضريبة القيمة المضافة",
|
||||||
|
"weight": "الوزن: {{weight}} كجم",
|
||||||
|
"youSave": "أنت توفر: {{amount}}",
|
||||||
|
"cheaperThanIndividual": "أرخص من الشراء بشكل فردي",
|
||||||
|
"pickupPrice": "سعر الاستلام: 19.90 € لكل قطعة.",
|
||||||
|
"consistsOf": "يتكون من:",
|
||||||
|
"loadingComponentDetails": "{{index}}. جارٍ تحميل تفاصيل المكون...",
|
||||||
|
"individualPriceTotal": "إجمالي السعر الفردي:",
|
||||||
|
"setPrice": "سعر المجموعة:",
|
||||||
|
"yourSavings": "توفيرك:",
|
||||||
|
"countDisplay": {
|
||||||
|
"noProducts": "0 منتجات",
|
||||||
|
"oneProduct": "منتج واحد",
|
||||||
|
"multipleProducts": "{{count}} منتجات",
|
||||||
|
"filteredProducts": "{{filtered}} من {{total}} منتجات",
|
||||||
|
"filteredOneProduct": "{{filtered}} من منتج واحد",
|
||||||
|
"xOfYProducts": "{{x}} من {{y}} منتجات"
|
||||||
|
},
|
||||||
|
"removeFiltersToSee": "قم بإزالة الفلاتر لرؤية المنتجات",
|
||||||
|
"outOfStock": "غير متوفر في المخزون",
|
||||||
|
"fromXProducts": "من {{count}} منتجات",
|
||||||
|
"discount": {
|
||||||
|
"from3Products": "من 3 منتجات",
|
||||||
|
"from5Products": "من 5 منتجات",
|
||||||
|
"from7Products": "من 7 منتجات",
|
||||||
|
"moreProductsMoreSavings": "كلما اخترت منتجات أكثر، كلما وفرت أكثر!"
|
||||||
|
}
|
||||||
|
};
|
||||||
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": "ممكن تسألني عن أنواع الحشيش...",
|
||||||
|
"recording": "جاري التسجيل...",
|
||||||
|
"searchProducts": "ابحث عن المنتجات...",
|
||||||
|
};
|
||||||
11
src/i18n/locales/ar/sections.js
Normal file
11
src/i18n/locales/ar/sections.js
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
export default {
|
||||||
|
"seeds": "بذور",
|
||||||
|
"stecklinge": "قصاصات",
|
||||||
|
"oilPress": "استعارة معصرة زيت",
|
||||||
|
"thcTest": "اختبار THC",
|
||||||
|
"address1": "Trachenberger Straße 14",
|
||||||
|
"address2": "01129 Dresden",
|
||||||
|
"showUsPhoto": "ورينا أجمل صورة عندك",
|
||||||
|
"selectSeedRate": "اختار البذرة واضغط تقييم",
|
||||||
|
"indoorSeason": "موسم الزراعة الداخلية بدأ"
|
||||||
|
};
|
||||||
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": "تغيير كلمة المرور",
|
||||||
|
"currentPassword": "كلمة المرور الحالية",
|
||||||
|
"newPassword": "كلمة المرور الجديدة",
|
||||||
|
"confirmNewPassword": "تأكيد كلمة المرور الجديدة",
|
||||||
|
"updatePassword": "تحديث كلمة المرور",
|
||||||
|
"changeEmail": "تغيير عنوان البريد الإلكتروني",
|
||||||
|
"password": "كلمة المرور",
|
||||||
|
"newEmail": "عنوان البريد الإلكتروني الجديد",
|
||||||
|
"updateEmail": "تحديث البريد الإلكتروني",
|
||||||
|
"apiKey": "مفتاح API",
|
||||||
|
"apiKeyDescription": "استخدم مفتاح API الخاص بك للتكامل مع التطبيقات الخارجية.",
|
||||||
|
"apiDocumentation": "توثيق API:",
|
||||||
|
"copyToClipboard": "نسخ إلى الحافظة",
|
||||||
|
"generate": "إنشاء",
|
||||||
|
"regenerate": "إعادة إنشاء",
|
||||||
|
"apiKeyCopied": "تم نسخ مفتاح API إلى الحافظة",
|
||||||
|
"errors": {
|
||||||
|
"fillAllFields": "يرجى ملء جميع الحقول",
|
||||||
|
"passwordsNotMatch": "كلمات المرور الجديدة غير متطابقة",
|
||||||
|
"passwordTooShort": "يجب أن تكون كلمة المرور الجديدة 8 أحرف على الأقل",
|
||||||
|
"passwordUpdateError": "حدث خطأ أثناء تحديث كلمة المرور",
|
||||||
|
"invalidEmail": "يرجى إدخال عنوان بريد إلكتروني صالح",
|
||||||
|
"emailUpdateError": "حدث خطأ أثناء تحديث عنوان البريد الإلكتروني",
|
||||||
|
"userNotFound": "المستخدم غير موجود",
|
||||||
|
"apiKeyGenerationError": "حدث خطأ أثناء إنشاء مفتاح API"
|
||||||
|
},
|
||||||
|
"success": {
|
||||||
|
"passwordUpdated": "تم تحديث كلمة المرور بنجاح",
|
||||||
|
"emailUpdated": "تم تحديث عنوان البريد الإلكتروني بنجاح",
|
||||||
|
"apiKeyGenerated": "تم إنشاء مفتاح API بنجاح",
|
||||||
|
"apiKeyWarning": "احفظ هذا المفتاح بأمان. لأسباب أمنية، سيتم إخفاؤه خلال 10 ثوانٍ."
|
||||||
|
}
|
||||||
|
};
|
||||||
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": "الاسم",
|
||||||
|
"searchField": "كلمة البحث",
|
||||||
|
"priceLowHigh": "السعر: من الأقل للأعلى",
|
||||||
|
"priceHighLow": "السعر: من الأعلى للأقل"
|
||||||
|
};
|
||||||
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": "ضريبة القيمة المضافة",
|
||||||
|
"vat7": "ضريبة القيمة المضافة 7%",
|
||||||
|
"vat19": "ضريبة القيمة المضافة 19%",
|
||||||
|
"vat19WithShipping": "ضريبة القيمة المضافة 19% (شاملة الشحن)",
|
||||||
|
"totalNet": "إجمالي السعر الصافي",
|
||||||
|
"totalGross": "إجمالي السعر الإجمالي بدون الشحن",
|
||||||
|
"subtotal": "المجموع الفرعي",
|
||||||
|
"incl7Vat": "شاملة ضريبة القيمة المضافة 7%",
|
||||||
|
"inclVatWithFooter": "(شاملة {{vat}}% ضريبة القيمة المضافة،*)",
|
||||||
|
"inclVatAmount": "شاملة {{amount}} € ضريبة القيمة المضافة ({{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": "بذور وقصاصات القنب الممتازة",
|
||||||
|
"aktionen": "العروض والتخفيضات الحالية",
|
||||||
|
"filiale": "متجرنا في دريسدن",
|
||||||
|
};
|
||||||
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": "Вход",
|
||||||
|
"register": "Регистрация",
|
||||||
|
"logout": "Изход",
|
||||||
|
"profile": "Профил",
|
||||||
|
"email": "Имейл",
|
||||||
|
"password": "Парола",
|
||||||
|
"confirmPassword": "Потвърдете паролата",
|
||||||
|
"forgotPassword": "Забравена парола?",
|
||||||
|
"loginWithGoogle": "Вход с Google",
|
||||||
|
"or": "ИЛИ",
|
||||||
|
"privacyAccept": "С натискане на \"Вход с Google\" приемам",
|
||||||
|
"privacyPolicy": "Политиката за поверителност",
|
||||||
|
"passwordMinLength": "Паролата трябва да е поне 8 символа",
|
||||||
|
"newPasswordMinLength": "Новата парола трябва да е поне 8 символа",
|
||||||
|
"menu": {
|
||||||
|
"profile": "Профил",
|
||||||
|
"myProfile": "Моят профил",
|
||||||
|
"checkout": "Плащане",
|
||||||
|
"orders": "Поръчки",
|
||||||
|
"settings": "Настройки",
|
||||||
|
"adminDashboard": "Админ табло",
|
||||||
|
"adminUsers": "Админ потребители"
|
||||||
|
}
|
||||||
|
};
|
||||||
39
src/i18n/locales/bg/cart.js
Normal file
39
src/i18n/locales/bg/cart.js
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
export default {
|
||||||
|
"title": "Количка",
|
||||||
|
"empty": "празна",
|
||||||
|
"addToCart": "Добави в количката",
|
||||||
|
"preorderCutting": "Предварителна поръчка като резник",
|
||||||
|
"continueShopping": "Продължи пазаруването",
|
||||||
|
"proceedToCheckout": "Продължи към плащане",
|
||||||
|
"productCount": "{{count}} {{count, plural, one {продукт} other {продукта}}}",
|
||||||
|
"productSingular": "продукт",
|
||||||
|
"productPlural": "продукта",
|
||||||
|
"removeFromCart": "Премахни от количката",
|
||||||
|
"openCart": "Отвори количката",
|
||||||
|
"availableFrom": "Наличен от {{date}}",
|
||||||
|
"backToOrder": "← Обратно към поръчката",
|
||||||
|
"summary": {
|
||||||
|
"title": "Обобщение на поръчката",
|
||||||
|
"goodsNet": "Стоки (нето):",
|
||||||
|
"shippingNet": "Доставка (нето):",
|
||||||
|
"totalGoods": "Общо стоки:",
|
||||||
|
"shippingCosts": "Разходи за доставка:",
|
||||||
|
"total": "Общо:",
|
||||||
|
"totalWeight": "Общо тегло: {{weight}} кг",
|
||||||
|
"freeFrom100": "(безплатно над 100€)",
|
||||||
|
"free": "безплатно"
|
||||||
|
},
|
||||||
|
"itemCount": {
|
||||||
|
"singular": "продукт",
|
||||||
|
"plural": "продукта"
|
||||||
|
},
|
||||||
|
"sync": {
|
||||||
|
"title": "Синхронизация на количката",
|
||||||
|
"description": "Имате запазена количка в профила си. Моля, изберете как искате да продължите:",
|
||||||
|
"deleteServer": "Изтрий количката на сървъра",
|
||||||
|
"useServer": "Използвай количката от сървъра",
|
||||||
|
"merge": "Обедини количките",
|
||||||
|
"currentCart": "Вашата текуща количка",
|
||||||
|
"serverCart": "Количка, запазена във вашия профил"
|
||||||
|
}
|
||||||
|
};
|
||||||
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": "Прочетено и прието",
|
||||||
|
};
|
||||||
34
src/i18n/locales/bg/checkout.js
Normal file
34
src/i18n/locales/bg/checkout.js
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
export default {
|
||||||
|
"invoiceAddress": "Адрес за фактура",
|
||||||
|
"deliveryAddress": "Адрес за доставка",
|
||||||
|
"saveForFuture": "Запази за бъдещи поръчки",
|
||||||
|
"pickupDate": "За коя дата е желано вземането на резниците?",
|
||||||
|
"note": "Бележка",
|
||||||
|
"sameAddress": "Адресът за доставка е същият като адреса за фактура",
|
||||||
|
"termsAccept": "Прочетох Общите условия, Политиката за поверителност и разпоредбите за правото на отказ",
|
||||||
|
"selectDeliveryMethod": "Изберете метод на доставка",
|
||||||
|
"selectPaymentMethod": "Изберете метод на плащане",
|
||||||
|
"orderSummary": "Обобщение на поръчката",
|
||||||
|
"addressValidationError": "Моля, проверете въведените данни в полетата за адрес.",
|
||||||
|
"processingOrder": "Поръчката се обработва...",
|
||||||
|
"completeOrder": "Завърши поръчката",
|
||||||
|
"termsValidationError": "Моля, приемете Общите условия, Политиката за поверителност и правото на отказ, за да продължите.",
|
||||||
|
"addressFields": {
|
||||||
|
"firstName": "Име",
|
||||||
|
"lastName": "Фамилия",
|
||||||
|
"addressSupplement": "Допълнение към адреса",
|
||||||
|
"street": "Улица",
|
||||||
|
"houseNumber": "Номер на къща",
|
||||||
|
"postalCode": "Пощенски код",
|
||||||
|
"city": "Град",
|
||||||
|
"country": "Държава"
|
||||||
|
},
|
||||||
|
"validationErrors": {
|
||||||
|
"firstNameRequired": "Името е задължително",
|
||||||
|
"lastNameRequired": "Фамилията е задължителна",
|
||||||
|
"streetRequired": "Улицата е задължителна",
|
||||||
|
"houseNumberRequired": "Номерът на къщата е задължителен",
|
||||||
|
"postalCodeRequired": "Пощенският код е задължителен",
|
||||||
|
"cityRequired": "Градът е задължителен"
|
||||||
|
}
|
||||||
|
};
|
||||||
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": "Зареждане...",
|
||||||
|
"error": "Грешка",
|
||||||
|
"close": "Затвори",
|
||||||
|
"save": "Запази",
|
||||||
|
"cancel": "Отказ",
|
||||||
|
"ok": "OK",
|
||||||
|
"yes": "Да",
|
||||||
|
"no": "Не",
|
||||||
|
"next": "Напред",
|
||||||
|
"back": "Назад",
|
||||||
|
"edit": "Редактирай",
|
||||||
|
"delete": "Изтрий",
|
||||||
|
"add": "Добави",
|
||||||
|
"remove": "Премахни",
|
||||||
|
"products": "Продукти",
|
||||||
|
"product": "Продукт",
|
||||||
|
"days": "Дни"
|
||||||
|
};
|
||||||
35
src/i18n/locales/bg/delivery.js
Normal file
35
src/i18n/locales/bg/delivery.js
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
export default {
|
||||||
|
"methods": {
|
||||||
|
"dhl": "DHL",
|
||||||
|
"dpd": "DPD",
|
||||||
|
"sperrgut": "Обемни стоки",
|
||||||
|
"sperrgutName": "Обемни стоки",
|
||||||
|
"pickup": "Вземане от магазина"
|
||||||
|
},
|
||||||
|
"descriptions": {
|
||||||
|
"standard": "Стандартна доставка",
|
||||||
|
"standardFree": "Стандартна доставка - БЕЗПЛАТНО при поръчка над 100€!",
|
||||||
|
"notAvailable": "Не може да се избере, защото един или повече артикули могат да се вземат само на място",
|
||||||
|
"bulky": "За големи и тежки артикули",
|
||||||
|
"pickupOnly": "Само вземане на място"
|
||||||
|
},
|
||||||
|
"prices": {
|
||||||
|
"free": "безплатно",
|
||||||
|
"freeFrom100": "(безплатно от 100€)",
|
||||||
|
"dhl": "6.99 €",
|
||||||
|
"dpd": "4.90 €",
|
||||||
|
"sperrgut": "28.99 €"
|
||||||
|
},
|
||||||
|
"times": {
|
||||||
|
"cutting14Days": "Срок за доставка: 14 дни",
|
||||||
|
"standard2to3Days": "Срок за доставка: 2-3 дни",
|
||||||
|
"supplier7to9Days": "Срок за доставка: 7-9 дни"
|
||||||
|
},
|
||||||
|
"selector": {
|
||||||
|
"title": "Изберете метод на доставка",
|
||||||
|
"freeShippingInfo": "💡 Безплатна доставка при поръчка над 100€!",
|
||||||
|
"remainingForFree": "Добавете още {{amount}}€ за безплатна доставка.",
|
||||||
|
"congratsFreeShipping": "🎉 Поздравления! Вие получавате безплатна доставка!",
|
||||||
|
"cartQualifiesFree": "Вашата количка на стойност {{amount}}€ се квалифицира за безплатна доставка."
|
||||||
|
}
|
||||||
|
};
|
||||||
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": "Сортиране",
|
||||||
|
"perPage": "на страница",
|
||||||
|
"availability": "Наличност",
|
||||||
|
"manufacturer": "Производител",
|
||||||
|
"all": "Всички"
|
||||||
|
};
|
||||||
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",
|
||||||
|
"address": "Trachenberger Straße 14 - Dresden",
|
||||||
|
"location": "Между спирка Pieschen и Trachenberger Platz",
|
||||||
|
"allPricesIncl": "* Всички цени включват законен ДДС, плюс доставка",
|
||||||
|
"copyright": "© {{year}} GrowHeads.de",
|
||||||
|
"legal": {
|
||||||
|
"datenschutz": "Политика за поверителност",
|
||||||
|
"agb": "Общи условия",
|
||||||
|
"sitemap": "Карта на сайта",
|
||||||
|
"impressum": "Правно известие",
|
||||||
|
"batteriegesetzhinweise": "Информация за закона за батериите",
|
||||||
|
"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
|
||||||
|
};
|
||||||
69
src/i18n/locales/bg/legal-agb.js
Normal file
69
src/i18n/locales/bg/legal-agb.js
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
export default {
|
||||||
|
"title": "Общи условия",
|
||||||
|
"deliveryShippingConditions": "Условия за доставка и изпращане",
|
||||||
|
"deliveryTerms": {
|
||||||
|
"1": "Доставката отнема между 1 и 7 дни.",
|
||||||
|
"2": "Стоките остават собственост на Growheads до пълното им заплащане.",
|
||||||
|
"3": "Ако се подозира, че стоките са повредени по време на транспорта или липсват артикули, опаковката за изпращане трябва да се запази за преглед от експерт. Всякакви повреди на опаковката трябва да бъдат потвърдени от превозвача в товарителницата, като се посочи видът и обхватът. Повредите при транспортиране трябва незабавно да бъдат съобщени на Growheads писмено по факс, имейл или поща. За това трябва да се направят снимки на повредените стоки, както и на повредената опаковка за изпращане с етикета с адреса. Повредената опаковка за изпращане също трябва да се запази. Те са необходими за предявяване на претенция към транспортната фирма.",
|
||||||
|
"4": "При връщане на дефектни стоки клиентът трябва да се увери, че стоките са правилно опаковани.",
|
||||||
|
"5": "Всички връщания трябва да бъдат предварително регистрирани в Growheads.",
|
||||||
|
"6": "Рискът при изпращане на артикули към нас се носи от клиента, освен ако не става въпрос за връщане на дефектни стоки.",
|
||||||
|
"7": "Growheads има право да организира вземането на стоките от Deutsche Post/GLS или превозвач по свой избор.",
|
||||||
|
"8": "Разходите за пощенски услуги се изчисляват според теглото. Growheads си запазва правото да прехвърля евентуални увеличения на цените от транспортните компании (такси, горивни надбавки).",
|
||||||
|
"9": "Нашите пратки обикновено се изпращат с: GLS, DHL & Deutsche Post AG.",
|
||||||
|
"10": "За особено тежки или обемисти артикули си запазваме правото да начисляваме допълнителни такси за доставка. Тези такси обикновено са посочени в ценовия лист.",
|
||||||
|
"11": "Плащането може да се извърши предварително чрез банков превод по посочената банкова сметка.",
|
||||||
|
"12": "Ако има забавяне на доставката, за което ние носим отговорност, срокът за предоставяне на допълнителен срок, който купувачът има право да определи, е ограничен до две седмици. Срокът започва с получаването на уведомлението за допълнителния срок от Growheads.",
|
||||||
|
"13": "Очевидни дефекти на стоките трябва да бъдат съобщени писмено незабавно след доставката. Ако клиентът не го направи, претенции по гаранцията за очевидни дефекти са изключени.",
|
||||||
|
"14": "Ако клиентът подаде рекламация за дефект, той трябва да върне дефектните стоки при нас с възможно най-точно описание на дефекта. Копие от нашата фактура трябва да бъде приложено към пратката. Стоките трябва да бъдат върнати в оригиналната опаковка или в опаковка, която защитава стоките по същия начин като оригиналната, за да се избегнат повреди по време на връщането."
|
||||||
|
},
|
||||||
|
"consultationLiability": {
|
||||||
|
"title": "Консултации и отговорност",
|
||||||
|
"1": "Ние предоставяме технически съвети за приложение според най-добрите ни знания, базирани на нашия опит и експертиза.",
|
||||||
|
"2": "Купувачът носи отговорност за спазването на законовите разпоредби относно съхранението, по-нататъшния транспорт и използването на нашите стоки."
|
||||||
|
},
|
||||||
|
"paymentConditions": {
|
||||||
|
"title": "Условия за плащане",
|
||||||
|
"1": "Стоките остават собственост на Growheads до пълното им заплащане.",
|
||||||
|
"2": "Фактурите се заплащат предварително чрез банков превод по нашата сметка. Ако платите предварително, стоките ще бъдат изпратени веднага след като сумата бъде кредитирана по нашата сметка."
|
||||||
|
},
|
||||||
|
"retentionOfTitle": {
|
||||||
|
"title": "Запазване на собствеността",
|
||||||
|
"content": "Доставените стоки остават собственост на Growheads, докато купувачът не уреди всички свои задължения към нас. Ако продавачът препродаде стоките, той с настоящото прехвърля на нас вземанията, произтичащи от продажбата. Ако купувачът закъснее с плащанията си, ние можем по всяко време да изискаме връщането на стоките без да се отказваме от договора."
|
||||||
|
},
|
||||||
|
"distanceSelling": {
|
||||||
|
"title": "Информация съгласно Закона за дистанционна търговия",
|
||||||
|
"intro": "Следната информация важи само за договори, сключени между Growheads и потребители чрез поръчка по каталог, интернет поръчка или други средства за дистанционна комуникация. Тя е ограничена до потребители в рамките на ЕС.",
|
||||||
|
"sections": {
|
||||||
|
"1": {
|
||||||
|
"title": "Съществени характеристики на стоките",
|
||||||
|
"content": "Моля, вижте обясненията в каталога или на нашия уебсайт за съществените характеристики на стоките. Офертите в нашия каталог и на уебсайта са без задължение. Поръчките към нас се считат за обвързващи оферти. Growheads може да ги приеме в срок от 14 дни след получаване на поръчката чрез изпращане на потвърждение на поръчката или чрез изпращане на стоките."
|
||||||
|
},
|
||||||
|
"2": {
|
||||||
|
"title": "Резервация",
|
||||||
|
"content": "Ако не всички поръчани артикули са налични за доставка, си запазваме правото да извършваме частични доставки, ако това е разумно за клиента. Отделни артикули могат да се различават от илюстрациите и описанията в каталога и на уебсайта. Това важи особено за стоки, изработени ръчно. Затова си запазваме правото да доставяме стоки с равностойно качество и цена, ако е необходимо."
|
||||||
|
},
|
||||||
|
"3": {
|
||||||
|
"title": "Цени и данъци",
|
||||||
|
"content": "Цените на отделните артикули с включен ДДС можете да намерите в каталога или на нашия уебсайт. Цените губят валидност с публикуването на нов каталог."
|
||||||
|
},
|
||||||
|
"4": "Всички цени са с резервация за грешки или колебания в цените. Ако има промяна в цената, купувачът може да упражни правото си на връщане.",
|
||||||
|
"5": {
|
||||||
|
"title": "Гаранционен срок",
|
||||||
|
"content": "Приложим е законовият гаранционен срок от 24 (двадесет и четири) месеца. В отделни случаи могат да важат по-дълги срокове, ако са предоставени от производителя."
|
||||||
|
},
|
||||||
|
"6": {
|
||||||
|
"title": "Право на връщане / Право на отказ",
|
||||||
|
"content": "Клиентът има 14-дневно право на връщане.\nСрокът започва с получаването на стоките от клиента и се спазва чрез навременното изпращане на отказа до Growheads. Изключени от това са храни и други бързоразвалящи се стоки, както и поръчки по индивидуален дизайн или стоки, специално поръчани по желание на клиента. Връщането трябва да се извърши чрез изпращане на стоките обратно в срока. Ако стоките не могат да бъдат изпратени, в срока трябва да бъде изпратено искане за връщане до нас по писмо, пощенска картичка, имейл или друг траен носител на данни. За спазване на срока е достатъчно навременното изпращане до посочения под точка 7) адрес на фирмата. Отказът не изисква посочване на причина. Цената на покупката и евентуалните разходи за доставка и изпращане ще бъдат възстановени след получаване на стоките от нас. Решаваща е стойността на върнатите стоки към момента на покупката, а не стойността на цялата поръчка. Growheads обикновено може да организира вземането от вас."
|
||||||
|
},
|
||||||
|
"7": {
|
||||||
|
"title": "Име и адрес на фирмата, рекламации, призовки",
|
||||||
|
"content": "Growheads\nTrachenberger Straße 14\n01129 Dresden"
|
||||||
|
},
|
||||||
|
"8": {
|
||||||
|
"title": "Място на изпълнение и подсъдност",
|
||||||
|
"content": "Мястото на изпълнение и подсъдността за всички претенции е Дрезден, освен ако задължителни законови разпоредби не предвиждат друго."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
8
src/i18n/locales/bg/legal-batterie.js
Normal file
8
src/i18n/locales/bg/legal-batterie.js
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
export default {
|
||||||
|
"title": "Информация за Закона за батериите",
|
||||||
|
"intro": "Във връзка с продажбата на батерии или доставката на устройства, съдържащи батерии, ние сме задължени да ви информираме за следното:",
|
||||||
|
"returnObligation": "Като краен потребител сте законово задължени да връщате използвани батерии. Можете да върнете стари батерии, които ние имаме или сме имали в продуктовия си асортимент като нови батерии, безплатно в нашия склад за изпращане (адрес за доставка).",
|
||||||
|
"symbolsInfo": "Символите, показани на батериите, имат следното значение:",
|
||||||
|
"wasteSymbol": "Символът на пресечената кофа за отпадъци означава, че батерията не трябва да се изхвърля с битовите отпадъци.",
|
||||||
|
"chemicalSymbols": "Pb = Батерията съдържа повече от 0,004 масови процента олово\nCd = Батерията съдържа повече от 0,002 масови процента кадмий\nHg = Батерията съдържа повече от 0,0005 масови процента живак."
|
||||||
|
};
|
||||||
58
src/i18n/locales/bg/legal-datenschutz.js
Normal file
58
src/i18n/locales/bg/legal-datenschutz.js
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
export default {
|
||||||
|
"title": "Политика за поверителност",
|
||||||
|
"responsibleParty": {
|
||||||
|
"title": "Отговорно лице по смисъла на закона за защита на данните:",
|
||||||
|
"company": "Growheads\nTrachenberger Straße 14\n01129 Dresden"
|
||||||
|
},
|
||||||
|
"generalInfo": "Освен ако по-долу не е посочено друго, предоставянето на вашите лични данни не е нито законово, нито договорно изискване, нито е необходимо за сключване на договор. Не сте задължени да предоставяте данните. Непредоставянето им няма последствия. Това важи само докато при следващите обработки не е посочено друго. „Лични данни“ означава всяка информация, отнасяща се до идентифицирано или идентифицируемо физическо лице.",
|
||||||
|
"sections": {
|
||||||
|
"informationDeletion": {
|
||||||
|
"title": "Информация, изтриване, блокиране",
|
||||||
|
"content": "По всяко време можете да поискате информация за вашите лични данни, техния произход и получатели, както и целта на обработката на данните, и можете безплатно да поискате корекция, блокиране или изтриване на тези данни. Моля, използвайте посочените в долния колонтитул на страницата или в правния импресум контакти за тази цел. Ние сме на разположение по всяко време за допълнителни въпроси по темата. Моля, имайте предвид, че не сме упълномощени и няма да изтриваме данни за фактури, банкови данни и данни, изпратени до доставчик на куриерски услуги. Данни, които могат да бъдат изтрити, включват: клиентски акаунти на уеб сървъра, както и в системата за управление на стоките, и имейли, които не са пряко свързани с поръчка."
|
||||||
|
},
|
||||||
|
"serverLogfiles": {
|
||||||
|
"title": "Сървърни лог файлове",
|
||||||
|
"content": "Можете да посещавате нашите уебсайтове без да предоставяте информация за себе си. При всяко посещение на нашия уебсайт, данни за използването се предават от вашия интернет браузър и се съхраняват в протоколни данни (сървърни лог файлове). Тези съхранени данни включват например името на посетената страница, дата и час на достъп, количество прехвърлени данни и доставчика, който прави заявката. Тези данни се използват изключително за осигуряване на безпроблемната работа на нашия уебсайт и за подобряване на нашето предложение. Тези данни не са лични данни. Не се извършва обединяване на тези данни с други източници на данни. Ако станем наясно с конкретни индикации за неправомерна употреба, си запазваме правото да проверим тези данни впоследствие."
|
||||||
|
},
|
||||||
|
"customerAccount": {
|
||||||
|
"title": "Клиентски акаунт",
|
||||||
|
"content": "При откриване на клиентски акаунт събираме вашите лични данни в посочения там обем. Обработката на данните служи за подобряване на вашето пазаруване и улесняване на обработката на поръчките. Обработката се извършва въз основа на чл. 6 (1) буква а DSGVO с вашето съгласие. Можете да оттеглите съгласието си по всяко време чрез уведомяване до нас, без да се засяга законосъобразността на обработката, извършена въз основа на съгласието до withdrawing. Вашият клиентски акаунт ще бъде изтрит след това."
|
||||||
|
},
|
||||||
|
"googleSSO": {
|
||||||
|
"title": "Вход с Google (Google Single Sign-On)",
|
||||||
|
"content": "Предлагаме ви възможността да влезете в клиентския си акаунт с вашия Google акаунт. Когато използвате функцията „Вход с Google“, удостоверяването се извършва чрез услугата Google Single Sign-On. В този процес на вашето устройство могат да се съхраняват бисквитки от Google, които са необходими за процеса на вход и удостоверяване. В рамките на Google входа получаваме от Google определени лични данни за проверка на вашата самоличност. По-специално, Google ни предава вашето име, вашия имейл адрес и – ако е съхранена във вашия Google акаунт – вашата профилна снимка. Тази информация се предоставя от Google веднага щом влезете в нашия онлайн магазин с вашия Google акаунт. Google, като трета страна, може да има достъп до тези данни и да ги обработва; това може да включва и прехвърляне на данни в САЩ. Сключили сме стандартни клаузи за защита на данните с Google съгласно чл. 46 (2) буква c DSGVO, за да осигурим адекватно ниво на защита на данните при прехвърлянето на вашите данни. Допълнителни подробности за обработката на данни от Google можете да намерите в Политиката за поверителност на Google (на https://policies.google.com/privacy?hl=en).",
|
||||||
|
"legalBasis": "Обработката на данни във връзка с Google входа се основава на чл. 6 (1) буква b DSGVO (изпълнение на предварителни договорни мерки и изпълнение на договор, напр. за създаване и използване на вашия клиентски акаунт) както и чл. 6 (1) буква f DSGVO (нашият легитимен интерес да ви предоставим бърза и удобна възможност за вход).",
|
||||||
|
"voluntaryUse": "Използването на функцията „Вход с Google“ е доброволно. Разбира се, можете да използвате нашия онлайн магазин и вашия клиентски акаунт и без Google SSO, като се регистрирате или влезете с вашия имейл адрес и парола както обикновено. Ако изберете да използвате Google вход, можете по всяко време да прекъснете тази връзка, като премахнете свързването в настройките на вашия Google акаунт.",
|
||||||
|
"yourRights": "Относно личните данни, обработвани чрез Google SSO, имате законните права на субекти на данни. По-специално имате право да получите информация за съхраняваните за вас данни (чл. 15 DSGVO), да коригирате неточни данни (чл. 16 DSGVO) или да поискате изтриване на вашите данни (чл. 17 DSGVO). Освен това имате право да ограничите обработката на вашите данни (чл. 18 DSGVO) и право на преносимост на данните (чл. 20 DSGVO). Ако основаваме обработката на нашия легитимен интерес, можете да възразите срещу обработката (чл. 21 DSGVO). Освен това можете по всяко време да се обърнете с жалба към компетентния надзорен орган за защита на данните. Вашите вече съществуващи права и възможности за избор от останалата част на политиката за поверителност важат също и за използването на Google вход."
|
||||||
|
},
|
||||||
|
"orders": {
|
||||||
|
"title": "Събиране, обработка и използване на лични данни при поръчки",
|
||||||
|
"content": "При подаване на поръчка събираме и използваме вашите лични данни само в обема, необходим за изпълнение и обработка на вашата поръчка и за обработка на вашите запитвания. Предоставянето на данните е необходимо за сключване на договора. Непредоставянето им води до невъзможност за сключване на договор. Обработката се основава на чл. 6 (1) буква b DSGVO и е необходима за изпълнение на договор с вас. Вашите данни няма да бъдат предавани на трети страни без вашето изрично съгласие. Единствените изключения са нашите партньори по услуги, които са необходими за обработка на договорните отношения, или доставчици, които използваме в рамките на възложена обработка. Освен получателите, посочени в съответните клаузи на тази политика за поверителност, това включва например получатели от следните категории: доставчици на куриерски услуги, доставчици на платежни услуги, доставчици на системи за управление на стоките, доставчици на услуги за обработка на поръчки, уеб хостинг доставчици, IT доставчици и дропшипинг търговци. Във всички случаи строго спазваме законовите изисквания. Обемът на предаваните данни е ограничен до минимум."
|
||||||
|
},
|
||||||
|
"newsletter": {
|
||||||
|
"title": "Използване на имейл адрес за изпращане на бюлетини",
|
||||||
|
"content": "Използваме вашия имейл адрес независимо от обработката на договора изключително за наши собствени рекламни цели за изпращане на бюлетини, при условие че сте дали изричното си съгласие за това. Обработката се извършва въз основа на чл. 6 (1) буква а DSGVO с вашето съгласие. Можете да оттеглите съгласието си по всяко време, без да се засяга законосъобразността на обработката, извършена въз основа на съгласието до withdrawing. Можете да се отпишете от бюлетина по всяко време, като използвате съответната връзка в бюлетина или чрез уведомяване до нас. Вашият имейл адрес ще бъде премахнат от списъка за разпространение. Вашите данни ще бъдат предадени на доставчик на услуги за имейл маркетинг в рамките на възложена обработка. Не се извършва предаване на други трети страни. Вашите данни ще бъдат предадени в трета страна, за която има решение за адекватност от Европейската комисия."
|
||||||
|
},
|
||||||
|
"chatbot": {
|
||||||
|
"title": "Използване на AI чатбот (OpenAI API)",
|
||||||
|
"content": "Използваме AI-базиран чатбот на нашия уебсайт, който се управлява чрез програмния интерфейс (API) на доставчика OpenAI. Чатботът служи за ефективно и автоматизирано отговаряне на запитвания от посетители, като по този начин предоставя функция за поддръжка. Когато използвате чатбота, вашите въвеждания се обработват от системата за генериране на подходящи отговори. Обработката е анонимизирана – не се събират или съхраняват IP адреси или други лични данни (като име или контактни данни).",
|
||||||
|
"legalBasis": "Правото основание за използването на чатбота е нашият легитимен интерес съгласно чл. 6 (1) буква f DSGVO. Този интерес се състои в предоставянето на ефективна поддръжка на посетителите и подобряване на потребителското изживяване на нашия уебсайт.",
|
||||||
|
"dataRecipient": "Получател на чат данните е OpenAI (OpenAI OpCo, LLC) като технически доставчик на услуги. OpenAI обработва предаденото чат съдържание на своите сървъри изключително с цел генериране на отговори. OpenAI действа като обработващ данни по смисъла на чл. 28 DSGVO и не използва данните за свои цели. Сключили сме договор за обработка на данни с OpenAI, който включва стандартните договорни клаузи на ЕС като подходящи гаранции за защита на данните. OpenAI има седалище в САЩ; чрез споразумението за стандартните договорни клаузи се гарантира, че при прехвърлянето на вашите данни се осигурява ниво на защита на данните, съответстващо на Европейския съюз.",
|
||||||
|
"dataRetention": "Съхраняваме вашите чат заявки само толкова дълго, колкото е необходимо за обработка и отговор. След като вашето запитване бъде приключено, чат историите се изтриват или анонимизират своевременно. Според собствените си изявления OpenAI съхранява обработените чат данни само временно и ги изтрива автоматично най-късно след 30 дни.",
|
||||||
|
"voluntaryUse": "Използването на чатбота е доброволно. Ако не използвате чатбота, не се извършва предаване на данни към OpenAI. Моля, не въвеждайте чувствителни лични данни в чата."
|
||||||
|
},
|
||||||
|
"cookies": {
|
||||||
|
"title": "Бисквитки",
|
||||||
|
"intro": "Нашият уебсайт използва бисквитки в следните случаи:",
|
||||||
|
"payment": "1. Процес на плащане: При плащания с кредитна карта или моментални преводи (напр. Klarna Instant) се използват технически необходими бисквитки. Те съдържат характерна низова стойност, която позволява уникална идентификация на браузъра. Бисквитките се задават от платежния доставчик Stripe и са абсолютно необходими за сигурното и безпроблемно обработване на плащанията. Без тези бисквитки поръчка с тези методи на плащане не е възможна. Обработката се извършва въз основа на чл. 6 (1) буква b DSGVO за изпълнение на договор.",
|
||||||
|
"googleSSO": "2. Google Single Sign-On (SSO): При използване на Google вход се задават бисквитки от Google, които са необходими за процеса на вход и удостоверяване. Тези бисквитки ви позволяват удобно да влизате с вашия Google акаунт, без да се налага да въвеждате отново данните си всеки път. Обработката се извършва въз основа на чл. 6 (1) буква b DSGVO (изпълнение на договор) и чл. 6 (1) буква f DSGVO (легитимен интерес за удобен вход).",
|
||||||
|
"otherPayments": "За други методи на плащане – директен дебит, вземане на място или наложен платеж – не се използват допълнителни бисквитки, освен ако не използвате Google вход."
|
||||||
|
},
|
||||||
|
"mollie": {
|
||||||
|
"title": "Mollie (Обработка на плащания)",
|
||||||
|
"content": "Използваме платежния доставчик Mollie на нашия уебсайт за обработка на плащания. Доставчик на услугата е Mollie B.V., Keizersgracht 126, 1015 CW Amsterdam, Нидерландия. В този контекст се предават на Mollie лични данни, необходими за обработка на плащания – по-специално вашето име, имейл адрес, адрес за фактуриране, платежна информация (напр. данни за кредитна карта) и IP адрес. Обработката на данните се извършва с цел обработка на плащания; правното основание е чл. 6 (1) буква b DSGVO, тъй като служи за изпълнение на договор с вас.",
|
||||||
|
"responsibility": "Mollie също така обработва определени данни като самостоятелен администратор, например за изпълнение на законови задължения (като предотвратяване на пране на пари) и предотвратяване на измами. Освен това сме сключили договор за обработка на данни с Mollie съгласно чл. 28 DSGVO; в рамките на това споразумение Mollie действа при обработката на плащания изключително по наши инструкции.",
|
||||||
|
"dataTransfer": "В случай че Mollie обработва лични данни извън ЕС, особено в САЩ, това се извършва при спазване на подходящи гаранции. Mollie използва стандартните договорни клаузи на ЕС съгласно чл. 46 DSGVO, за да осигури адекватно ниво на защита на данните. Въпреки това, ние посочваме, че САЩ се считат за трета страна с потенциално недостатъчно ниво на защита на данните по смисъла на законодателството за защита на данните. Допълнителна информация можете да намерите в политиката за поверителност на Mollie на https://www.mollie.com/en/privacy."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
25
src/i18n/locales/bg/legal-impressum.js
Normal file
25
src/i18n/locales/bg/legal-impressum.js
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
export default {
|
||||||
|
"title": "Правно известие (Impressum)",
|
||||||
|
"sections": {
|
||||||
|
"operator": {
|
||||||
|
"title": "Оператор и отговорен за съдържанието на този магазин е:",
|
||||||
|
"content": "Growheads\nMax Schön\nTrachenberger Straße 14\n01129 Dresden"
|
||||||
|
},
|
||||||
|
"contact": {
|
||||||
|
"title": "Контакт:",
|
||||||
|
"content": "E-mail: service@growheads.de"
|
||||||
|
},
|
||||||
|
"vatId": {
|
||||||
|
"title": "ДДС номер:",
|
||||||
|
"content": "VAT ID No.: DE323017152"
|
||||||
|
},
|
||||||
|
"disclaimer": {
|
||||||
|
"title": "Отказ от отговорност:",
|
||||||
|
"content": "Не поемаме отговорност за съдържанието на външни интернет адреси, свързани на тези страници. Съответните оператори са отговорни за съдържанието на домейни, които не са част от компанията."
|
||||||
|
},
|
||||||
|
"copyright": {
|
||||||
|
"title": "Клауза за авторски права:",
|
||||||
|
"content": "Представеното тук съдържание е общо взето защитено с авторски права и може да бъде разпространявано само с писмено разрешение.\nПравата върху фото- или текстов материал от други страни не са ограничени или отменени с тази клауза."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
11
src/i18n/locales/bg/legal-widerruf.js
Normal file
11
src/i18n/locales/bg/legal-widerruf.js
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
export default {
|
||||||
|
"title": "Право на отказ",
|
||||||
|
"withdrawalRight": "Имате право да се откажете от този договор в рамките на четиринадесет дни без да посочвате причина. Срокът за отказ е четиринадесет дни от деня, в който вие или трето лице, посочено от вас, което не е превозвачът, сте взели притежание на стоките.",
|
||||||
|
"exerciseWithdrawal": "За да упражните правото си на отказ, трябва да ни уведомите",
|
||||||
|
"contactInfo": "Growheads\nTrachenberger Straße 14\n01129 Dresden\nE-Mail: service@growheads.de",
|
||||||
|
"withdrawalProcess": "чрез ясно изявление (например писмо, изпратено по пощата, факс или имейл) за решението ви да се откажете от този договор. Можете да използвате приложената образец-форма за отказ, но това не е задължително. За спазване на срока за отказ е достатъчно да изпратите уведомлението си за упражняване на правото на отказ преди изтичането на срока за отказ.",
|
||||||
|
"consequencesTitle": "Последствия от отказа",
|
||||||
|
"consequences": "Ако се откажете от този договор, ние ще ви възстановим всички плащания, които сме получили от вас, включително разходите за доставка (с изключение на допълнителните разходи, възникнали поради избора ви на вид доставка, различен от най-евтината стандартна доставка, предлагана от нас), без неоправдано забавяне и най-късно в рамките на четиринадесет дни от деня, в който получихме уведомлението за вашия отказ от този договор. За това възстановяване ще използваме същото платежно средство, което сте използвали при първоначалната транзакция, освен ако изрично не е уговорено друго с вас; в никакъв случай няма да ви бъдат начислявани такси за това възстановяване. Можем да откажем възстановяване, докато не получим обратно стоките или докато не предоставите доказателство, че сте изпратили обратно стоките, в зависимост от това кое настъпи по-рано. Трябва да върнете или предадете стоките без неоправдано забавяне и във всеки случай не по-късно от четиринадесет дни от деня, в който ни уведомите за отказа си от този договор. Срокът се счита за спазен, ако изпратите стоките преди изтичането на четиринадесетдневния срок. Вие носите преки разходи за връщане на стоките. Отговаряте само за намалена стойност на стоките, която се дължи на боравене с тях, различно от необходимото за установяване на характера, характеристиките и функционирането на стоките.",
|
||||||
|
"noWithdrawalTitle": "Уведомление за липса на право на отказ",
|
||||||
|
"noWithdrawal": "Правото на отказ не се прилага за стоки, които са изработени или пригодени по поръчка на клиента (фолиа и тръби), но може да бъде предоставено по споразумение. Контейнерите за торове, чието уплътнение е премахнато или унищожено при отваряне, също са изключени от правото на отказ.",
|
||||||
|
};
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user