Compare commits

...

43 Commits

Author SHA1 Message Date
sebseb7
08c04909e0 translate 2025-07-16 09:21:40 +02:00
sebseb7
510907b48a Fix syntax error in English translation file: Corrected closing bracket in translation.js to ensure proper export structure. 2025-07-16 08:26:20 +02:00
sebseb7
e02b18e17f Refactor translation files and update import syntax: Converted CommonJS to ES module syntax in translate-i18n.js. Enhanced English translation file with new phrases for profile, order summary, and settings sections, while improving existing translations for clarity and consistency. 2025-07-16 08:24:09 +02:00
sebseb7
9ffbd5b84e Enhance German translation file: Updated phrases for clarity and consistency, added new terms for delivery and settings sections, and improved error messages for better user guidance. 2025-07-16 08:20:58 +02:00
sebseb7
f8dbb24823 Refactor project for improved localization and user experience: Renamed project to "reactshop" and updated package-lock.json with new dependencies. Enhanced profile navigation by updating the ButtonGroup component to navigate to the cart section. Improved German translation files with additional phrases for better context and clarity. Updated CartTab component to utilize the latest i18n functionality. 2025-07-16 08:14:16 +02:00
sebseb7
13f1e14a3d Update MainPageLayout styles for improved layout consistency: Adjusted positioning properties for titles to enhance visual alignment and responsiveness across different page types, ensuring a more cohesive user experience. 2025-07-16 06:30:49 +02:00
sebseb7
6b7bcf4155 Remove unnecessary alignment properties from MainPageLayout: Simplified layout code by eliminating redundant alignItems settings for better readability and maintainability. 2025-07-16 06:28:21 +02:00
sebseb7
45258ac522 Refactor MainPageLayout for improved responsiveness: Updated layout to stack title above navigation on portrait phones, adjusted flex properties for better alignment, and enhanced navigation rendering for different screen sizes, ensuring a more user-friendly experience across devices. 2025-07-16 06:26:37 +02:00
sebseb7
080515af68 Add new language support for Slovenian, Croatian, Swedish, Turkish, Greek, Arabic, and Chinese: Updated config.js to include additional language configurations, enhancing localization capabilities across the application. 2025-07-16 06:25:50 +02:00
sebseb7
33b229728f Update Arabic and Chinese translation files: Improved localization by enhancing existing translations with better context and comments for clarity. Adjusted navigation, authentication, cart, product, and other sections to ensure accurate representation of terms and phrases in both languages. 2025-07-16 06:19:37 +02:00
sebseb7
8d69b0566b Enhance translation functionality and localization support: Updated translate-i18n.js to include new command line options for skipping and only translating English. Modified package.json to add new translation scripts. Improved localization files for multiple languages with better comments for clarity and accuracy, ensuring comprehensive support for internationalization. 2025-07-16 06:17:27 +02:00
sebseb7
280916224a Enhance localization support for Polish translations: Updated Polish translation files with improved comments for clarity and accuracy, ensuring better context for users. Added export of i18n utilities for easier access in the application. 2025-07-16 06:10:40 +02:00
sebseb7
fd77fc8f7f Integrate i18n support for German and enhance localization: Initialize i18next for prerendering with German as the default language. Import and configure translation files for multiple languages, including Hungarian, Italian, and others, ensuring comprehensive localization support across the application. Update Hungarian and Italian translation files with improved comments for clarity and accuracy. 2025-07-16 06:09:01 +02:00
sebseb7
f5d6778def Refactor project structure and enhance localization: Rename project to "reactshop" and update package.json with new dependencies and scripts for development and production. Update Greek, Spanish, French, and Croatian translation files with improved comments for clarity and accuracy, ensuring better localization support across the application. 2025-07-16 06:06:08 +02:00
sebseb7
11a3522a97 Enhance i18n support by adding new language translations: Introduced Arabic, Croatian, Czech, Greek, Hungarian, Slovak, Slovenian, Swedish, Turkish, and updated existing language configurations. Updated available languages in LanguageContext and LanguageProvider to reflect the new additions, ensuring comprehensive localization across the application. 2025-07-16 06:02:04 +02:00
sebseb7
51471d4a55 Refactor project for i18n support: Rename project to "i18n-translator" and update package.json and package-lock.json accordingly. Enhance localization by integrating translation functions across various components, including AddToCartButton, Content, GoogleLoginButton, and others, to provide dynamic text rendering based on user language preferences. Update localization files for multiple languages, ensuring comprehensive support for internationalization. 2025-07-16 05:59:48 +02:00
sebseb7
859a2c06d8 Add Chinese language support and update localization files: Introduced translations for Chinese (zh) in LanguageSwitcher and i18n configuration. Removed outdated translation files for several languages, streamlining localization resources. Enhanced language context to include Chinese in available languages. 2025-07-16 03:34:10 +02:00
sebseb7
5c90d048fb Integrate i18n support across multiple components: Update AddToCartButton, CartDropdown, CartItem, Footer, ProductFilters, ProductList, and profile components to utilize translation functions for dynamic text rendering. Enhance user experience by providing localized content for various UI elements, including buttons, labels, and tax information. 2025-07-16 03:03:47 +02:00
sebseb7
cff9c88808 Implement multilingual support: Integrate i18next for language translation across components, update configuration for multilingual descriptions and keywords, and enhance user interface elements with dynamic language switching. Add new dependencies for i18next and related libraries in package.json and package-lock.json. 2025-07-16 02:34:36 +02:00
sebseb7
b78de53786 Enhance delivery cost calculation and shipping information display: Implement free shipping threshold for cart value in DeliveryMethodSelector and OrderProcessingService. Update CartDropdown and OrderSummary to reflect shipping costs and free shipping messages based on cart value, improving user clarity on shipping fees. 2025-07-16 01:59:43 +02:00
sebseb7
925667fc2c Update font size and padding in CategoryBox component for improved readability and layout consistency. 2025-07-15 21:00:27 +02:00
sebseb7
251352c660 Update payment method name in PaymentMethodSelector to include additional options: Apple Pay, Google Pay, and PayPal, enhancing clarity for users on available payment methods. 2025-07-15 13:25:52 +02:00
sebseb7
88c757fd35 Refactor webpack configuration to copy index.html to payment directory, enhancing clarity in log messages and ensuring proper directory structure for asset management. 2025-07-15 13:06:06 +02:00
sebseb7
d8c802c2f1 Update webpack configuration to copy index.html to payment/success/success, improving clarity in log messages for asset copying. 2025-07-15 13:04:42 +02:00
sebseb7
056b63efa0 Update webpack configuration to copy index.html to payment/success directory and change base URL in config.js to production URL. 2025-07-15 13:03:34 +02:00
sebseb7
c7afad68b0 Refactor error handling in PaymentSuccess component to redirect users to profile on payment failure, simplifying the user experience by removing the error display box. 2025-07-15 12:29:57 +02:00
sebseb7
5157b7d781 Add Mollie payment integration: Implement lazy loading for PaymentSuccess page, update CartTab and OrderProcessingService to handle Mollie payment intents, and enhance ProfilePage to manage payment completion for both Stripe and Mollie. Update base URL for development environment. 2025-07-15 12:13:57 +02:00
seb
9072a3c977 Implement Mollie payment integration across CartTab, CheckoutValidation, and PaymentMethodSelector components. Update payment method handling to prioritize Mollie for specific delivery methods and ensure proper session storage for Mollie transactions. Enhance Datenschutz page to include Mollie payment processing details. 2025-07-15 10:41:10 +02:00
sebseb7
838e2fd786 upd 2025-07-15 09:46:25 +02:00
seb
abbb5e222d Fix CSS formatting in index.css by removing unnecessary whitespace after closing code block, ensuring cleaner styling and consistency across the stylesheet. 2025-07-08 00:09:19 +02:00
seb
c216154bd7 Update launch configuration for development environment and enhance ProductDetailPage styling. Added environment variables and skip files to launch.json, and improved image handling in ProductDetailPage with consistent dimensions and border styling. 2025-07-07 11:41:49 +02:00
seb
9000b28ce5 Enhance product detail handling by integrating komponenten data into relevant components. Updated AddToCartButton, CartItem, Product, and ProductDetailPage to support new komponenten properties, improving product representation and pricing logic. Updated 404 image asset for better user experience. 2025-07-07 08:25:24 +02:00
seb
8f2253f155 Update GrowTentKonfigurator to include socket-based category data fetching and caching. Modify MainPageLayout titles for improved clarity and update route to pass socket props for configurator component. 2025-07-07 05:28:01 +02:00
seb
b33ece2875 Update link in MainPageLayout from German to English category for improved clarity and consistency in navigation. 2025-07-07 02:59:42 +02:00
seb
02aff1e456 Enhance unit pricing logic in feeds.cjs by adding a comprehensive unit mapping for German to Google Shopping units. Implemented a helper function to convert units and adjusted base measure calculations to ensure compliance with German regulations. Improved formatting of unit pricing measures for better clarity. 2025-07-07 02:34:13 +02:00
seb
9e14827c91 Enhance 404 handling in webpack configuration with async middleware for prerendering. Updated NotFound404 page to improve user experience with localized messaging and responsive image styling. Added taxonomy ID mappings in feeds.cjs for better compliance and clarity in product categorization. 2025-07-07 02:12:19 +02:00
seb
8698816875 Add middleware to handle 404 routes in webpack configuration. Implemented custom response handling to ensure proper 404 status and no-cache headers, along with rewrites for SPA fallback to improve routing behavior. 2025-07-06 23:50:30 +02:00
seb
987de641e4 Refactor unit pricing logic in feeds.cjs to enhance compliance with German regulations. Updated the helper function to return structured unit pricing data, including both unit and base measures, and adjusted XML generation accordingly. 2025-07-06 22:54:13 +02:00
seb
23e1742e40 Add unit pricing measure to product XML generation in feeds.cjs. Updated Product, ProductDetailPage, and AddToCartButton components to support new pricing fields (fGrundPreis, cGrundEinheit) for compliance with German regulations. Enhanced SearchBar with enter icon functionality for improved user experience. 2025-07-06 20:36:23 +02:00
seb
205558d06c Add CarouselProvider to Prerender components for improved layout structure. Updated PrerenderAppContent and PrerenderHome to wrap MainPageLayout with CarouselProvider, enhancing component organization and consistency. 2025-07-06 09:35:34 +02:00
seb
046979a64d Refactor Prerender components to replace Home page with MainPageLayout, improving structure and consistency across the application. Updated routing in PrerenderAppContent and PrerenderHome to utilize the new layout component. 2025-07-06 09:33:34 +02:00
seb
161e377de4 Update category mappings in feeds.cjs for improved accuracy and clarity. Adjusted several category paths to reflect more specific classifications, and added validation to ensure non-empty category returns. Updated language setting to 'de-DE' for consistency. 2025-07-06 09:30:10 +02:00
seb
73a88f508b Refactor App component to replace Home page with MainPageLayout, integrating CarouselProvider for improved page structure. Added new routes for Presseverleih and ThcTest pages, enhancing navigation and organization. Updated Header component to support new page states for Aktionen and Filiale. 2025-07-06 09:25:39 +02:00
524 changed files with 12420 additions and 1721 deletions

2
.gitignore vendored
View File

@@ -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
View File

@@ -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>/**"
]
} }
] ]
} }

View 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

File diff suppressed because it is too large Load Diff

View File

@@ -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"

View File

@@ -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");

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 MiB

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 208 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 242 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

View File

@@ -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">

View File

@@ -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
@@ -195,9 +208,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 +232,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 +253,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 +262,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 +366,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 +396,7 @@ const App = () => {
/> />
</SocketProvider> </SocketProvider>
</ThemeProvider> </ThemeProvider>
</LanguageProvider>
); );
}; };

View File

@@ -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/>

View File

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

92
src/PrerenderNotFound.js Normal file
View 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 };

View File

@@ -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);

View File

@@ -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;
@@ -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,7 +128,7 @@ 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>
@@ -139,14 +140,23 @@ class CartDropdown extends Component {
{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' }}>
Versandkosten:
{deliveryCost === 0 && priceCalculations.totalGross < 100 && (
<span style={{ color: '#2e7d32', fontSize: '0.8em', marginLeft: '4px' }}>
(kostenlos ab 100)
</span>
)}
</TableCell>
<TableCell align="right" sx={{ fontWeight: 'bold' }}> <TableCell align="right" sx={{ fontWeight: 'bold' }}>
{currencyFormatter.format(deliveryCost)} {deliveryCost === 0 ? (
<span style={{ color: '#2e7d32' }}>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 }}>Gesamtsumme:</TableCell>
<TableCell align="right" sx={{ fontWeight: 'bold', pt: 2 }}> <TableCell align="right" sx={{ fontWeight: 'bold', pt: 2 }}>
@@ -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);

View File

@@ -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 {
@@ -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);

View File

@@ -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>

View File

@@ -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));

View File

@@ -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);

View File

@@ -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);

View File

@@ -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>
); );
}; };

View 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);

View File

@@ -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>
)} )}
@@ -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));

View File

@@ -0,0 +1,407 @@
import React from "react";
import { useLocation } from "react-router-dom";
import Container from "@mui/material/Container";
import Box from "@mui/material/Box";
import Typography from "@mui/material/Typography";
import Paper from "@mui/material/Paper";
import Grid from "@mui/material/Grid";
import ChevronLeft from "@mui/icons-material/ChevronLeft";
import ChevronRight from "@mui/icons-material/ChevronRight";
import { Link } from "react-router-dom";
import SharedCarousel from "./SharedCarousel.js";
import { getCombinedAnimatedBorderStyles } from "../utils/animatedBorderStyles.js";
import { useTranslation } from 'react-i18next';
const MainPageLayout = () => {
const location = useLocation();
const currentPath = location.pathname;
const { t } = useTranslation();
// Determine which page we're on
const isHome = currentPath === "/";
const isAktionen = currentPath === "/aktionen";
const isFiliale = currentPath === "/filiale";
// Get navigation config based on current page
const getNavigationConfig = () => {
if (isHome) {
return {
leftNav: { text: t('navigation.aktionen'), link: "/aktionen" },
rightNav: { text: t('navigation.filiale'), link: "/filiale" }
};
} else if (isAktionen) {
return {
leftNav: { text: t('navigation.filiale'), link: "/filiale" },
rightNav: { text: t('navigation.home'), link: "/" }
};
} else if (isFiliale) {
return {
leftNav: { text: t('navigation.home'), link: "/" },
rightNav: { text: t('navigation.aktionen'), link: "/aktionen" }
};
}
return { leftNav: null, rightNav: null };
};
const allTitles = {
home: t('titles.home'),
aktionen: t('titles.aktionen'),
filiale: t('titles.filiale')
};
// Define all content boxes for layered rendering
const allContentBoxes = {
home: [
{
title: t('sections.seeds'),
image: "/assets/images/seeds.jpg",
bgcolor: "#e1f0d3",
link: "/Kategorie/Seeds"
},
{
title: t('sections.stecklinge'),
image: "/assets/images/cutlings.jpg",
bgcolor: "#e8f5d6",
link: "/Kategorie/Stecklinge"
}
],
aktionen: [
{
title: t('sections.oilPress'),
image: "/assets/images/presse.jpg",
bgcolor: "#e1f0d3",
link: "/presseverleih"
},
{
title: t('sections.thcTest'),
image: "/assets/images/purpl.jpg",
bgcolor: "#e8f5d6",
link: "/thc-test"
}
],
filiale: [
{
title: t('sections.address1'),
image: "/assets/images/filiale1.jpg",
bgcolor: "#e1f0d3",
link: "/filiale"
},
{
title: t('sections.address2'),
image: "/assets/images/filiale2.jpg",
bgcolor: "#e8f5d6",
link: "/filiale"
}
]
};
// Get opacity for each page layer
const getOpacity = (pageType) => {
if (pageType === "home" && isHome) return 1;
if (pageType === "aktionen" && isAktionen) return 1;
if (pageType === "filiale" && isFiliale) return 1;
return 0;
};
const navConfig = getNavigationConfig();
// Navigation text mapping for translation
const navTexts = [
{ key: 'aktionen', text: t('navigation.aktionen'), link: '/aktionen' },
{ key: 'filiale', text: t('navigation.filiale'), link: '/filiale' },
{ key: 'home', text: t('navigation.home'), link: '/' }
];
return (
<Container maxWidth="lg" sx={{ py: 2 }}>
<style>{getCombinedAnimatedBorderStyles(['seeds', 'cutlings'])}</style>
{/* Main Navigation Header */}
<Box sx={{
display: "flex",
alignItems: "center",
justifyContent: "space-between",
mb: 4,
mt: 2,
px: 0,
transition: "all 0.3s ease-in-out",
// Portrait phone: stack title above navigation
flexDirection: {
xs: "column",
sm: "row"
}
}}>
{/* Title for portrait phones - shown first */}
<Box sx={{
display: { xs: "block", sm: "none" },
mb: { xs: 2, sm: 0 },
width: "100%",
textAlign: "center",
position: "relative"
}}>
{Object.entries(allTitles).map(([pageType, title]) => (
<Typography
key={pageType}
variant="h3"
component="h1"
sx={{
fontFamily: "SwashingtonCP",
fontSize: { xs: "2.125rem", sm: "2.125rem", md: "3rem" },
textAlign: "center",
color: "primary.main",
textShadow: "3px 3px 10px rgba(0, 0, 0, 0.4)",
transition: "opacity 0.5s ease-in-out",
opacity: getOpacity(pageType),
position: pageType === "home" ? "relative" : "absolute",
top: pageType !== "home" ? 0 : "auto",
left: pageType !== "home" ? 0 : "auto",
transform: "none",
width: "100%",
pointerEvents: getOpacity(pageType) === 1 ? "auto" : "none",
lineHeight: { xs: "1.2", sm: "1.2", md: "1.1" },
wordWrap: "break-word",
hyphens: "auto"
}}
>
{title}
</Typography>
))}
</Box>
{/* Navigation container for portrait phones */}
<Box sx={{
display: { xs: "flex", sm: "contents" },
width: { xs: "100%", sm: "auto" },
justifyContent: { xs: "space-between", sm: "initial" },
alignItems: "center"
}}>
{/* Left Navigation - Layered rendering */}
<Box sx={{
display: "flex",
alignItems: "center",
flexShrink: 0,
justifyContent: "flex-start",
position: "relative",
mr: { xs: 0, sm: 2 }
}}>
{navTexts.map((navItem, index) => {
const isActive = navConfig.leftNav && navConfig.leftNav.text === navItem.text;
const link = navItem.link;
return (
<Box
key={navItem.key}
component={Link}
to={link}
sx={{
display: "flex",
alignItems: "center",
textDecoration: "none",
color: "inherit",
transition: "all 0.3s ease",
opacity: isActive ? 1 : 0,
position: index === 0 ? "relative" : "absolute",
left: index !== 0 ? 0 : "auto",
pointerEvents: isActive ? "auto" : "none",
"&:hover": {
transform: "translateX(-5px)",
color: "primary.main"
}
}}
>
<ChevronLeft sx={{ fontSize: "2rem", mr: 1 }} />
<Typography
sx={{
fontFamily: "SwashingtonCP",
fontSize: { xs: "1.25rem", sm: "1.25rem", md: "2.125rem" },
textShadow: "2px 2px 4px rgba(0, 0, 0, 0.3)",
lineHeight: { xs: "1.2", sm: "1.2", md: "1.1" },
whiteSpace: "nowrap"
}}
>
{navItem.text}
</Typography>
</Box>
);
})}
</Box>
{/* Center Title - Layered rendering - Hidden on portrait phones, shown on larger screens */}
<Box sx={{
flex: 1,
display: { xs: "none", sm: "flex" },
justifyContent: "center",
alignItems: "center",
px: 0,
position: "relative",
minWidth: 0
}}>
{Object.entries(allTitles).map(([pageType, title]) => (
<Typography
key={pageType}
variant="h3"
component="h1"
sx={{
fontFamily: "SwashingtonCP",
fontSize: { xs: "2.125rem", sm: "2.125rem", md: "3rem" },
textAlign: "center",
color: "primary.main",
textShadow: "3px 3px 10px rgba(0, 0, 0, 0.4)",
transition: "opacity 0.5s ease-in-out",
opacity: getOpacity(pageType),
position: pageType === "home" ? "relative" : "absolute",
top: pageType !== "home" ? "50%" : "auto",
left: pageType !== "home" ? "50%" : "auto",
transform: pageType !== "home" ? "translate(-50%, -50%)" : "none",
width: "100%",
pointerEvents: getOpacity(pageType) === 1 ? "auto" : "none",
lineHeight: { xs: "1.2", sm: "1.2", md: "1.1" },
wordWrap: "break-word",
hyphens: "auto"
}}
>
{title}
</Typography>
))}
</Box>
{/* Right Navigation - Layered rendering */}
<Box sx={{
display: "flex",
alignItems: "center",
flexShrink: 0,
justifyContent: "flex-end",
position: "relative",
ml: { xs: 0, sm: 2 }
}}>
{navTexts.map((navItem, index) => {
const isActive = navConfig.rightNav && navConfig.rightNav.text === navItem.text;
const link = navItem.link;
return (
<Box
key={navItem.key}
component={Link}
to={link}
sx={{
display: "flex",
alignItems: "center",
textDecoration: "none",
color: "inherit",
transition: "all 0.3s ease",
opacity: isActive ? 1 : 0,
position: index === 0 ? "relative" : "absolute",
right: index !== 0 ? 0 : "auto",
pointerEvents: isActive ? "auto" : "none",
"&:hover": {
transform: "translateX(5px)",
color: "primary.main"
}
}}
>
<Typography
sx={{
fontFamily: "SwashingtonCP",
fontSize: { xs: "1.25rem", sm: "1.25rem", md: "2.125rem" },
textShadow: "2px 2px 4px rgba(0, 0, 0, 0.3)",
lineHeight: { xs: "1.2", sm: "1.2", md: "1.1" },
whiteSpace: "nowrap"
}}
>
{navItem.text}
</Typography>
<ChevronRight sx={{ fontSize: "2rem", ml: 1 }} />
</Box>
);
})}
</Box>
</Box>
</Box>
{/* Content Boxes - Layered rendering */}
<Box sx={{ position: "relative", mb: 4 }}>
{Object.entries(allContentBoxes).map(([pageType, contentBoxes]) => (
<Grid
key={pageType}
container
spacing={0}
sx={{
transition: "opacity 0.5s ease-in-out",
opacity: getOpacity(pageType),
position: pageType === "home" ? "relative" : "absolute",
top: 0,
left: 0,
width: "100%",
pointerEvents: getOpacity(pageType) === 1 ? "auto" : "none"
}}
>
{contentBoxes.map((box, index) => (
<Grid key={`${pageType}-${index}`} item xs={12} sm={6} sx={{ p: 2, width: "50%" }}>
<div className={`animated-border-card ${index === 0 ? 'seeds-card' : 'cutlings-card'}`}>
<Paper
component={Link}
to={box.link}
sx={{
p: 0,
textDecoration: "none",
color: "text.primary",
borderRadius: 2,
overflow: "hidden",
height: { xs: 150, sm: 200, md: 300 },
display: "flex",
flexDirection: "column",
boxShadow: 10,
transition: "all 0.3s ease",
"&:hover": {
transform: "translateY(-5px)",
boxShadow: 20,
},
}}
>
<Box
sx={{
height: "100%",
bgcolor: box.bgcolor,
backgroundImage: `url("${box.image}")`,
backgroundSize: "contain",
backgroundPosition: "center",
backgroundRepeat: "no-repeat",
position: "relative",
}}
>
<Box
sx={{
position: "absolute",
bottom: 0,
left: 0,
right: 0,
bgcolor: "rgba(27, 94, 32, 0.8)",
p: 1,
}}
>
<Typography
sx={{
fontSize: "1.6rem",
color: "white",
fontFamily: "SwashingtonCP",
}}
>
{box.title}
</Typography>
</Box>
</Box>
</Paper>
</div>
</Grid>
))}
</Grid>
))}
</Box>
{/* Shared Carousel */}
<SharedCarousel />
</Container>
);
};
export default MainPageLayout;

View File

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

View File

@@ -68,8 +68,8 @@ class Product extends Component {
render() { render() {
const { const {
id, name, price, available, manufacturer, seoName, id, name, price, available, manufacturer, seoName,
currency, vat, massMenge, massEinheit, thc, currency, vat, cGrundEinheit, fGrundPreis, thc,
floweringWeeks,incoming, neu, weight, versandklasse, availableSupplier floweringWeeks,incoming, neu, weight, versandklasse, availableSupplier, komponenten
} = this.props; } = this.props;
const isNew = neu && (new Date().getTime() - new Date(neu).getTime() < 30 * 24 * 60 * 60 * 1000); const isNew = neu && (new Date().getTime() - new Date(neu).getTime() < 30 * 24 * 60 * 60 * 1000);
@@ -173,7 +173,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 +240,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>
)} )}
@@ -341,8 +341,8 @@ class Product extends Component {
</Typography> </Typography>
{massMenge != 1 && massEinheit && (<Typography variant="body2" color="text.secondary" sx={{ m: 0,p: 0 }}> {cGrundEinheit && fGrundPreis && fGrundPreis != price && (<Typography variant="body2" color="text.secondary" sx={{ m: 0,p: 0 }}>
({new Intl.NumberFormat('de-DE', {style: 'currency', currency: currency || 'EUR'}).format(price/massMenge)}/{massEinheit}) ({new Intl.NumberFormat('de-DE', {style: 'currency', currency: currency || 'EUR'}).format(fGrundPreis)}/{cGrundEinheit})
</Typography> )} </Typography> )}
</div> </div>
{/*incoming*/} {/*incoming*/}
@@ -358,7 +358,7 @@ class Product extends Component {
> >
<ZoomInIcon /> <ZoomInIcon />
</IconButton> </IconButton>
<AddToCartButton cartButton={true} availableSupplier={availableSupplier} available={available} incoming={incoming} seoName={seoName} pictureList={this.props.pictureList} id={id} price={price} vat={vat} weight={weight} name={name} versandklasse={versandklasse}/> <AddToCartButton cartButton={true} availableSupplier={availableSupplier} komponenten={komponenten} cGrundEinheit={cGrundEinheit} fGrundPreis={fGrundPreis} available={available} incoming={incoming} seoName={seoName} pictureList={this.props.pictureList} id={id} price={price} vat={vat} weight={weight} name={name} versandklasse={versandklasse}/>
</Box> </Box>
</Card> </Card>
</Box> </Box>

View File

@@ -29,6 +29,12 @@ class ProductDetailPage extends Component {
attributes: [], attributes: [],
isSteckling: false, isSteckling: false,
imageDialogOpen: false, imageDialogOpen: false,
komponenten: [],
komponentenLoaded: false,
komponentenData: {}, // Store individual komponent data with loading states
komponentenImages: {}, // Store tiny pictures for komponenten
totalKomponentenPrice: 0,
totalSavings: 0
}; };
} else { } else {
this.state = { this.state = {
@@ -39,6 +45,12 @@ class ProductDetailPage extends Component {
attributes: [], attributes: [],
isSteckling: false, isSteckling: false,
imageDialogOpen: false, imageDialogOpen: false,
komponenten: [],
komponentenLoaded: false,
komponentenData: {}, // Store individual komponent data with loading states
komponentenImages: {}, // Store tiny pictures for komponenten
totalKomponentenPrice: 0,
totalSavings: 0
}; };
} }
} }
@@ -64,6 +76,248 @@ class ProductDetailPage extends Component {
} }
} }
loadKomponentImage = (komponentId, pictureList) => {
// Initialize cache if it doesn't exist
if (!window.smallPicCache) {
window.smallPicCache = {};
}
// Skip if no pictureList
if (!pictureList || pictureList.length === 0) {
return;
}
// Get the first image ID from pictureList
const bildId = pictureList.split(',')[0];
// Check if already cached
if (window.smallPicCache[bildId]) {
this.setState(prevState => ({
komponentenImages: {
...prevState.komponentenImages,
[komponentId]: window.smallPicCache[bildId]
}
}));
return;
}
// Check if socketB is available
if (!this.props.socketB || !this.props.socketB.connected) {
console.log("SocketB not connected yet, skipping image load for komponent:", komponentId);
return;
}
// Fetch image from server
this.props.socketB.emit('getPic', { bildId, size: 'small' }, (res) => {
if (res.success) {
// Cache the image
window.smallPicCache[bildId] = URL.createObjectURL(new Blob([res.imageBuffer], { type: 'image/jpeg' }));
// Update state
this.setState(prevState => ({
komponentenImages: {
...prevState.komponentenImages,
[komponentId]: window.smallPicCache[bildId]
}
}));
} else {
console.log('Error loading komponent image:', res);
}
});
}
loadKomponent = (id, count) => {
// Initialize cache if it doesn't exist
if (!window.productDetailCache) {
window.productDetailCache = {};
}
// Check if this komponent is already cached
if (window.productDetailCache[id]) {
const cachedProduct = window.productDetailCache[id];
// Load komponent image if available
if (cachedProduct.pictureList) {
this.loadKomponentImage(id, cachedProduct.pictureList);
}
// Update state with cached data
this.setState(prevState => {
const newKomponentenData = {
...prevState.komponentenData,
[id]: {
...cachedProduct,
count: parseInt(count),
loaded: true
}
};
// Check if all remaining komponenten are loaded
const allLoaded = prevState.komponenten.every(k =>
newKomponentenData[k.id] && newKomponentenData[k.id].loaded
);
// Calculate totals if all loaded
let totalKomponentenPrice = 0;
let totalSavings = 0;
if (allLoaded) {
totalKomponentenPrice = prevState.komponenten.reduce((sum, k) => {
const komponentData = newKomponentenData[k.id];
if (komponentData && komponentData.loaded) {
return sum + (komponentData.price * parseInt(k.count));
}
return sum;
}, 0);
// Calculate savings (difference between buying individually vs as set)
const setPrice = prevState.product ? prevState.product.price : 0;
totalSavings = Math.max(0, totalKomponentenPrice - setPrice);
}
console.log("Cached komponent loaded:", id, "data:", newKomponentenData[id]);
console.log("All loaded (cached):", allLoaded);
return {
komponentenData: newKomponentenData,
komponentenLoaded: allLoaded,
totalKomponentenPrice,
totalSavings
};
});
return;
}
// If not cached, fetch from server (similar to loadProductData)
if (!this.props.socket || !this.props.socket.connected) {
console.log("Socket not connected yet, waiting for connection to load komponent data");
return;
}
// Mark this komponent as loading
this.setState(prevState => ({
komponentenData: {
...prevState.komponentenData,
[id]: {
...prevState.komponentenData[id],
loading: true,
loaded: false,
count: parseInt(count)
}
}
}));
this.props.socket.emit(
"getProductView",
{ articleId: id },
(res) => {
if (res.success) {
// Cache the successful response
window.productDetailCache[id] = res.product;
// Load komponent image if available
if (res.product.pictureList) {
this.loadKomponentImage(id, res.product.pictureList);
}
// Update state with loaded data
this.setState(prevState => {
const newKomponentenData = {
...prevState.komponentenData,
[id]: {
...res.product,
count: parseInt(count),
loading: false,
loaded: true
}
};
// Check if all remaining komponenten are loaded
const allLoaded = prevState.komponenten.every(k =>
newKomponentenData[k.id] && newKomponentenData[k.id].loaded
);
// Calculate totals if all loaded
let totalKomponentenPrice = 0;
let totalSavings = 0;
if (allLoaded) {
totalKomponentenPrice = prevState.komponenten.reduce((sum, k) => {
const komponentData = newKomponentenData[k.id];
if (komponentData && komponentData.loaded) {
return sum + (komponentData.price * parseInt(k.count));
}
return sum;
}, 0);
// Calculate savings (difference between buying individually vs as set)
const setPrice = prevState.product ? prevState.product.price : 0;
totalSavings = Math.max(0, totalKomponentenPrice - setPrice);
}
console.log("Updated komponentenData for", id, ":", newKomponentenData[id]);
console.log("All loaded:", allLoaded);
return {
komponentenData: newKomponentenData,
komponentenLoaded: allLoaded,
totalKomponentenPrice,
totalSavings
};
});
console.log("getProductView (komponent)", res);
} else {
console.error("Error loading komponent:", res.error || "Unknown error", res);
// Remove failed komponent from the list and check if all remaining are loaded
this.setState(prevState => {
const newKomponenten = prevState.komponenten.filter(k => k.id !== id);
const newKomponentenData = { ...prevState.komponentenData };
// Remove failed komponent from data
delete newKomponentenData[id];
// Check if all remaining komponenten are loaded
const allLoaded = newKomponenten.length === 0 || newKomponenten.every(k =>
newKomponentenData[k.id] && newKomponentenData[k.id].loaded
);
// Calculate totals if all loaded
let totalKomponentenPrice = 0;
let totalSavings = 0;
if (allLoaded && newKomponenten.length > 0) {
totalKomponentenPrice = newKomponenten.reduce((sum, k) => {
const komponentData = newKomponentenData[k.id];
if (komponentData && komponentData.loaded) {
return sum + (komponentData.price * parseInt(k.count));
}
return sum;
}, 0);
// Calculate savings (difference between buying individually vs as set)
const setPrice = this.state.product ? this.state.product.price : 0;
totalSavings = Math.max(0, totalKomponentenPrice - setPrice);
}
console.log("Removed failed komponent:", id, "remaining:", newKomponenten.length);
console.log("All loaded after removal:", allLoaded);
return {
komponenten: newKomponenten,
komponentenData: newKomponentenData,
komponentenLoaded: allLoaded,
totalKomponentenPrice,
totalSavings
};
});
}
}
);
}
loadProductData = () => { loadProductData = () => {
if (!this.props.socket || !this.props.socket.connected) { if (!this.props.socket || !this.props.socket.connected) {
// Socket not connected yet, but don't show error immediately on first load // Socket not connected yet, but don't show error immediately on first load
@@ -78,12 +332,37 @@ class ProductDetailPage extends Component {
(res) => { (res) => {
if (res.success) { if (res.success) {
res.product.seoName = this.props.seoName; res.product.seoName = this.props.seoName;
// Initialize cache if it doesn't exist
if (!window.productDetailCache) {
window.productDetailCache = {};
}
// Cache the product data
window.productDetailCache[this.props.seoName] = res.product;
const komponenten = [];
if(res.product.komponenten) {
for(const komponent of res.product.komponenten.split(",")) {
// Handle both "x" and "×" as separators
const [id, count] = komponent.split(/[x×]/);
komponenten.push({id: id.trim(), count: count.trim()});
}
}
this.setState({ this.setState({
product: res.product, product: res.product,
loading: false, loading: false,
error: null, error: null,
imageDialogOpen: false, imageDialogOpen: false,
attributes: res.attributes attributes: res.attributes,
komponenten: komponenten,
komponentenLoaded: komponenten.length === 0 // If no komponenten, mark as loaded
}, () => {
if(komponenten.length > 0) {
for(const komponent of komponenten) {
this.loadKomponent(komponent.id, komponent.count);
}
}
}); });
console.log("getProductView", res); console.log("getProductView", res);
@@ -180,7 +459,7 @@ class ProductDetailPage extends Component {
}; };
render() { render() {
const { product, loading, error, attributeImages, isSteckling, attributes } = const { product, loading, error, attributeImages, isSteckling, attributes, komponentenLoaded, komponentenData, komponentenImages, totalKomponentenPrice, totalSavings } =
this.state; this.state;
if (loading) { if (loading) {
@@ -211,7 +490,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 +508,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 +573,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 +634,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,7 +652,7 @@ 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>
)} )}
@@ -386,18 +665,12 @@ class ProductDetailPage extends Component {
.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>
); );
@@ -452,7 +725,11 @@ class ProductDetailPage extends Component {
</Typography> </Typography>
<Typography variant="body2" color="text.secondary"> <Typography variant="body2" color="text.secondary">
inkl. {product.vat}% MwSt. inkl. {product.vat}% MwSt.
{product.cGrundEinheit && product.fGrundPreis && (
<>; {new Intl.NumberFormat('de-DE', {style: 'currency', currency: 'EUR'}).format(product.fGrundPreis)}/{product.cGrundEinheit}</>
)}
</Typography> </Typography>
{product.versandklasse && {product.versandklasse &&
product.versandklasse != "standard" && product.versandklasse != "standard" &&
product.versandklasse != "kostenlos" && ( product.versandklasse != "kostenlos" && (
@@ -461,6 +738,37 @@ class ProductDetailPage extends Component {
</Typography> </Typography>
)} )}
</Box> </Box>
{/* Savings comparison - positioned between price and cart button */}
{product.komponenten && komponentenLoaded && totalKomponentenPrice > product.price &&
(totalKomponentenPrice - product.price >= 2 &&
(totalKomponentenPrice - product.price) / product.price >= 0.02) && (
<Box sx={{
display: "flex",
alignItems: "center",
justifyContent: "center",
minWidth: { xs: "100%", sm: "200px" }
}}>
<Box sx={{ p: 2, borderRadius: 1, backgroundColor: "#e8f5e8", textAlign: "center" }}>
<Typography
variant="body2"
sx={{
fontWeight: "bold",
color: "success.main"
}}
>
Sie sparen: {new Intl.NumberFormat("de-DE", {
style: "currency",
currency: "EUR",
}).format(totalKomponentenPrice - product.price)}
</Typography>
<Typography variant="caption" color="text.secondary">
Günstiger als Einzelkauf
</Typography>
</Box>
</Box>
)}
<Box <Box
sx={{ sx={{
display: "flex", display: "flex",
@@ -487,6 +795,7 @@ class ProductDetailPage extends Component {
vat={product.vat} vat={product.vat}
weight={product.weight} weight={product.weight}
availableSupplier={product.availableSupplier} availableSupplier={product.availableSupplier}
komponenten={product.komponenten}
name={cleanProductName(product.name) + " Stecklingsvorbestellung 1 Stück"} name={cleanProductName(product.name) + " Stecklingsvorbestellung 1 Stück"}
versandklasse={"nur Abholung"} versandklasse={"nur Abholung"}
/> />
@@ -516,12 +825,16 @@ class ProductDetailPage extends Component {
available={product.available} available={product.available}
id={product.id} id={product.id}
availableSupplier={product.availableSupplier} availableSupplier={product.availableSupplier}
komponenten={product.komponenten}
cGrundEinheit={product.cGrundEinheit}
fGrundPreis={product.fGrundPreis}
price={product.price} price={product.price}
vat={product.vat} vat={product.vat}
weight={product.weight} weight={product.weight}
name={cleanProductName(product.name)} name={cleanProductName(product.name)}
versandklasse={product.versandklasse} versandklasse={product.versandklasse}
/> />
<Typography <Typography
variant="caption" variant="caption"
sx={{ sx={{
@@ -565,6 +878,206 @@ class ProductDetailPage extends Component {
</Box> </Box>
</Box> </Box>
)} )}
{product.komponenten && product.komponenten.split(",").length > 0 && (
<Box sx={{ mt: 4, p: 4, background: "#fff", borderRadius: 2, boxShadow: "0 2px 8px rgba(0,0,0,0.08)" }}>
<Typography variant="h4" gutterBottom>Bestehend aus:</Typography>
<Box sx={{ maxWidth: 800, mx: "auto" }}>
{(console.log("komponentenLoaded:", komponentenLoaded), komponentenLoaded) ? (
<>
{console.log("Rendering loaded komponenten:", this.state.komponenten.length, "komponentenData:", Object.keys(komponentenData).length)}
{this.state.komponenten.map((komponent, index) => {
const komponentData = komponentenData[komponent.id];
console.log(`Rendering komponent ${komponent.id}:`, komponentData);
// Don't show border on last item (pricing section has its own top border)
const isLastItem = index === this.state.komponenten.length - 1;
const showBorder = !isLastItem;
if (!komponentData || !komponentData.loaded) {
return (
<Box key={komponent.id} sx={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
py: 1,
borderBottom: showBorder ? "1px solid #eee" : "none",
minHeight: "70px" // Consistent height to prevent layout shifts
}}>
<Box sx={{ display: "flex", alignItems: "center", gap: 2 }}>
<Box sx={{ width: 50, height: 50, flexShrink: 0, backgroundColor: "#f5f5f5", borderRadius: 1, border: "1px solid #e0e0e0" }}>
{/* Empty placeholder for image */}
</Box>
<Box>
<Typography variant="body1">
{index + 1}. Lädt...
</Typography>
<Typography variant="body2" color="text.secondary">
{komponent.count}x
</Typography>
</Box>
</Box>
<Typography variant="body1" sx={{ fontWeight: 500 }}>
-
</Typography>
</Box>
);
}
const itemPrice = komponentData.price * parseInt(komponent.count);
const formattedPrice = new Intl.NumberFormat("de-DE", {
style: "currency",
currency: "EUR",
}).format(itemPrice);
return (
<Box
key={komponent.id}
component={Link}
to={`/Artikel/${komponentData.seoName}`}
sx={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
py: 1,
borderBottom: showBorder ? "1px solid #eee" : "none",
textDecoration: "none",
color: "inherit",
minHeight: "70px", // Consistent height to prevent layout shifts
"&:hover": {
backgroundColor: "#f5f5f5"
}
}}
>
<Box sx={{ display: "flex", alignItems: "center", gap: 2 }}>
<Box sx={{ width: 50, height: 50, flexShrink: 0 }}>
{komponentenImages[komponent.id] ? (
<CardMedia
component="img"
height="50"
image={komponentenImages[komponent.id]}
alt={komponentData.name}
sx={{
objectFit: "contain",
borderRadius: 1,
border: "1px solid #e0e0e0"
}}
/>
) : (
<CardMedia
component="img"
height="50"
image="/assets/images/nopicture.jpg"
alt={komponentData.name}
sx={{
objectFit: "contain",
borderRadius: 1,
border: "1px solid #e0e0e0"
}}
/>
)}
</Box>
<Box>
<Typography variant="body1" sx={{ fontWeight: 500 }}>
{index + 1}. {cleanProductName(komponentData.name)}
</Typography>
<Typography variant="body2" color="text.secondary">
{komponent.count}x à {new Intl.NumberFormat("de-DE", {
style: "currency",
currency: "EUR",
}).format(komponentData.price)}
</Typography>
</Box>
</Box>
<Typography variant="body1" sx={{ fontWeight: 500 }}>
{formattedPrice}
</Typography>
</Box>
);
})}
{/* Total price and savings display - only show when prices differ meaningfully */}
{totalKomponentenPrice > product.price &&
(totalKomponentenPrice - product.price >= 2 &&
(totalKomponentenPrice - product.price) / product.price >= 0.02) && (
<Box sx={{ mt: 3, pt: 2, borderTop: "2px solid #eee" }}>
<Box sx={{ display: "flex", justifyContent: "space-between", alignItems: "center", mb: 1 }}>
<Typography variant="h6">
Einzelpreis gesamt:
</Typography>
<Typography variant="h6" sx={{ textDecoration: "line-through", color: "text.secondary" }}>
{new Intl.NumberFormat("de-DE", {
style: "currency",
currency: "EUR",
}).format(totalKomponentenPrice)}
</Typography>
</Box>
<Box sx={{ display: "flex", justifyContent: "space-between", alignItems: "center", mb: 1 }}>
<Typography variant="h6">
Set-Preis:
</Typography>
<Typography variant="h6" color="primary" sx={{ fontWeight: "bold" }}>
{new Intl.NumberFormat("de-DE", {
style: "currency",
currency: "EUR",
}).format(product.price)}
</Typography>
</Box>
{totalSavings > 0 && (
<Box sx={{ display: "flex", justifyContent: "space-between", alignItems: "center", mt: 2, p: 2, backgroundColor: "#e8f5e8", borderRadius: 1 }}>
<Typography variant="h6" color="success.main" sx={{ fontWeight: "bold" }}>
Ihre Ersparnis:
</Typography>
<Typography variant="h6" color="success.main" sx={{ fontWeight: "bold" }}>
{new Intl.NumberFormat("de-DE", {
style: "currency",
currency: "EUR",
}).format(totalSavings)}
</Typography>
</Box>
)}
</Box>
)}
</>
) : (
// Loading state
<Box>
{this.state.komponenten.map((komponent, index) => {
// For loading state, we don't know if pricing will be shown, so show all borders
return (
<Box key={komponent.id} sx={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
py: 1,
borderBottom: "1px solid #eee",
minHeight: "70px" // Consistent height to prevent layout shifts
}}>
<Box sx={{ display: "flex", alignItems: "center", gap: 2 }}>
<Box sx={{ width: 50, height: 50, flexShrink: 0, backgroundColor: "#f5f5f5", borderRadius: 1, border: "1px solid #e0e0e0" }}>
{/* Empty placeholder for image */}
</Box>
<Box>
<Typography variant="body1">
{index + 1}. Lädt Komponent-Details...
</Typography>
<Typography variant="body2" color="text.secondary">
{komponent.count}x
</Typography>
</Box>
</Box>
<Typography variant="body1" sx={{ fontWeight: 500 }}>
-
</Typography>
</Box>
);
})}
</Box>
)}
</Box>
</Box>
)}
</Box> </Box>
); );
} }

View File

@@ -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));

View File

@@ -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>
); );
@@ -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);

View File

@@ -0,0 +1,231 @@
import React, { useContext, useEffect, useState } from "react";
import Box from "@mui/material/Box";
import Typography from "@mui/material/Typography";
import IconButton from "@mui/material/IconButton";
import ChevronLeft from "@mui/icons-material/ChevronLeft";
import ChevronRight from "@mui/icons-material/ChevronRight";
import CategoryBox from "./CategoryBox.js";
import SocketContext from "../contexts/SocketContext.js";
import { useCarousel } from "../contexts/CarouselContext.js";
import { useTranslation } from 'react-i18next';
// Helper to process and set categories
const processCategoryTree = (categoryTree) => {
if (
categoryTree &&
categoryTree.id === 209 &&
Array.isArray(categoryTree.children)
) {
return categoryTree.children;
} else {
return [];
}
};
// Check for cached data
const getProductCache = () => {
if (typeof window !== "undefined" && window.productCache) {
return window.productCache;
}
if (
typeof global !== "undefined" &&
global.window &&
global.window.productCache
) {
return global.window.productCache;
}
return null;
};
// Initialize categories
const initializeCategories = () => {
const productCache = getProductCache();
if (productCache && productCache["categoryTree_209"]) {
const cached = productCache["categoryTree_209"];
if (cached.categoryTree) {
return processCategoryTree(cached.categoryTree);
}
}
return [];
};
const SharedCarousel = () => {
const { carouselRef, filteredCategories, setFilteredCategories, moveCarousel } = useCarousel();
const context = useContext(SocketContext);
const { t } = useTranslation();
const [rootCategories, setRootCategories] = useState([]);
useEffect(() => {
const initialCategories = initializeCategories();
setRootCategories(initialCategories);
}, []);
useEffect(() => {
// Only fetch from socket if we don't already have categories
if (
rootCategories.length === 0 &&
context && context.socket && context.socket.connected &&
typeof window !== "undefined"
) {
context.socket.emit("categoryList", { categoryId: 209 }, (response) => {
if (response && response.categoryTree) {
// Store in cache
try {
if (!window.productCache) window.productCache = {};
window.productCache["categoryTree_209"] = {
categoryTree: response.categoryTree,
timestamp: Date.now(),
};
} catch (err) {
console.error(err);
}
setRootCategories(response.categoryTree.children || []);
}
});
}
}, [context, context?.socket?.connected, rootCategories.length]);
useEffect(() => {
const filtered = rootCategories.filter(
(cat) => cat.id !== 689 && cat.id !== 706
);
setFilteredCategories(filtered);
}, [rootCategories, setFilteredCategories]);
// Create duplicated array for seamless scrolling
const displayCategories = [...filteredCategories, ...filteredCategories];
if (filteredCategories.length === 0) {
return null;
}
return (
<Box sx={{ mt: 3 }}>
<Typography
variant="h4"
component="h1"
sx={{
mb: 2,
fontFamily: "SwashingtonCP",
color: "primary.main",
textAlign: "center",
textShadow: "3px 3px 10px rgba(0, 0, 0, 0.4)"
}}
>
{t('navigation.categories')}
</Typography>
<div
className="carousel-wrapper"
style={{
position: 'relative',
overflow: 'hidden',
width: '100%',
maxWidth: '1200px',
margin: '0 auto',
padding: '0 20px',
boxSizing: 'border-box'
}}
>
{/* Left Arrow */}
<IconButton
onClick={() => moveCarousel("left")}
aria-label="Vorherige Kategorien anzeigen"
style={{
position: 'absolute',
top: '50%',
left: '8px',
transform: 'translateY(-50%)',
zIndex: 1200,
backgroundColor: 'rgba(255, 255, 255, 0.9)',
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.15)',
width: '48px',
height: '48px',
borderRadius: '50%'
}}
>
<ChevronLeft />
</IconButton>
{/* Right Arrow */}
<IconButton
onClick={() => moveCarousel("right")}
aria-label="Nächste Kategorien anzeigen"
style={{
position: 'absolute',
top: '50%',
right: '8px',
transform: 'translateY(-50%)',
zIndex: 1200,
backgroundColor: 'rgba(255, 255, 255, 0.9)',
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.15)',
width: '48px',
height: '48px',
borderRadius: '50%'
}}
>
<ChevronRight />
</IconButton>
<div
className="carousel-container"
style={{
position: 'relative',
overflow: 'hidden',
padding: '20px 0',
width: '100%',
maxWidth: '1080px',
margin: '0 auto',
zIndex: 1,
boxSizing: 'border-box'
}}
>
<div
className="home-carousel-track"
ref={carouselRef}
style={{
display: 'flex',
gap: '16px',
transition: 'none',
alignItems: 'flex-start',
width: 'fit-content',
overflow: 'visible',
position: 'relative',
transform: 'translateX(0px)',
margin: '0 auto'
}}
>
{displayCategories.map((category, index) => (
<div
key={`${category.id}-${index}`}
className="carousel-item"
style={{
flex: '0 0 130px',
width: '130px',
maxWidth: '130px',
minWidth: '130px',
height: '130px',
maxHeight: '130px',
minHeight: '130px',
boxSizing: 'border-box',
position: 'relative'
}}
>
<CategoryBox
id={category.id}
name={category.name}
seoName={category.seoName}
image={category.image}
bgcolor={category.bgcolor}
/>
</div>
))}
</div>
</div>
</div>
</Box>
);
};
export default SharedCarousel;

View File

@@ -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;

View File

@@ -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) => {
@@ -410,7 +411,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 +425,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 +596,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 +611,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 +632,4 @@ class CategoryList extends Component {
} }
} }
export default CategoryList; export default withI18n()(CategoryList);

View File

@@ -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" },

View File

@@ -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"
@@ -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 = {
@@ -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);

View File

@@ -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") && (

View File

@@ -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 "mollie";/*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";
} }

View File

@@ -4,20 +4,27 @@ 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';
const DeliveryMethodSelector = ({ deliveryMethod, onChange, isPickupOnly }) => { const DeliveryMethodSelector = ({ deliveryMethod, onChange, isPickupOnly, cartItems = [] }) => {
// Calculate cart value for free shipping threshold
const cartValue = cartItems.reduce((total, item) => total + item.price * item.quantity, 0);
const isFreeShipping = cartValue >= 100;
const remainingForFreeShipping = Math.max(0, 100 - cartValue);
const deliveryOptions = [ 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 ? "nicht auswählbar weil ein oder mehrere Artikel nur abgeholt werden können" :
price: '6,99 €', isFreeShipping ? 'Standardversand - KOSTENLOS ab 100€ Warenwert!' : 'Standardversand',
price: isFreeShipping ? 'kostenlos' : '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 ? "nicht auswählbar weil ein oder mehrere Artikel nur abgeholt werden können" :
price: '4,90 €', isFreeShipping ? 'Standardversand - KOSTENLOS ab 100€ Warenwert!' : 'Standardversand',
price: isFreeShipping ? 'kostenlos' : '4,90 €',
disabled: isPickupOnly disabled: isPickupOnly
}, },
{ {
@@ -114,6 +121,41 @@ 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' }}>
💡 Versandkostenfrei ab 100 Warenwert!
</Typography>
<Typography variant="body2" color="text.secondary">
Noch {remainingForFreeShipping.toFixed(2).replace('.', ',')} für kostenlosen Versand hinzufügen.
</Typography>
</Box>
)}
{isFreeShipping && (
<Box sx={{
mt: 2,
p: 2,
backgroundColor: '#e8f5e8',
borderRadius: 1,
border: '1px solid #2e7d32'
}}>
<Typography variant="body2" color="success.main" sx={{ fontWeight: 'medium' }}>
🎉 Glückwunsch! Sie erhalten kostenlosen Versand!
</Typography>
<Typography variant="body2" color="text.secondary">
Ihr Warenkorb von {cartValue.toFixed(2).replace('.', ',')} qualifiziert sich für kostenlosen Versand.
</Typography>
</Box>
)}
</Box> </Box>
</> </>
); );

View File

@@ -15,8 +15,11 @@ 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;
} }
@@ -108,7 +111,7 @@ const OrderDetailsDialog = ({ open, onClose, order }) => {
<TableRow> <TableRow>
<TableCell colSpan={2} /> <TableCell colSpan={2} />
<TableCell align="right"> <TableCell align="right">
<Typography fontWeight="bold">Gesamtnettopreis</Typography> <Typography fontWeight="bold">{t ? t('tax.totalNet') : '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>
@@ -117,21 +120,21 @@ const OrderDetailsDialog = ({ open, onClose, order }) => {
{vatCalculations.vat7 > 0 && ( {vatCalculations.vat7 > 0 && (
<TableRow> <TableRow>
<TableCell colSpan={2} /> <TableCell colSpan={2} />
<TableCell align="right">7% Mehrwertsteuer</TableCell> <TableCell align="right">{t ? t('tax.vat7') : '7% Mehrwertsteuer'}</TableCell>
<TableCell align="right">{currencyFormatter.format(vatCalculations.vat7)}</TableCell> <TableCell align="right">{currencyFormatter.format(vatCalculations.vat7)}</TableCell>
</TableRow> </TableRow>
)} )}
{vatCalculations.vat19 > 0 && ( {vatCalculations.vat19 > 0 && (
<TableRow> <TableRow>
<TableCell colSpan={2} /> <TableCell colSpan={2} />
<TableCell align="right">19% Mehrwertsteuer</TableCell> <TableCell align="right">{t ? t('tax.vat19') : '19% Mehrwertsteuer'}</TableCell>
<TableCell align="right">{currencyFormatter.format(vatCalculations.vat19)}</TableCell> <TableCell align="right">{currencyFormatter.format(vatCalculations.vat19)}</TableCell>
</TableRow> </TableRow>
)} )}
<TableRow> <TableRow>
<TableCell colSpan={2} /> <TableCell colSpan={2} />
<TableCell align="right"> <TableCell align="right">
<Typography fontWeight="bold">Zwischensumme</Typography> <Typography fontWeight="bold">{t ? t('tax.subtotal') : 'Zwischensumme'}</Typography>
</TableCell> </TableCell>
<TableCell align="right"> <TableCell align="right">
<Typography fontWeight="bold">{currencyFormatter.format(subtotal)}</Typography> <Typography fontWeight="bold">{currencyFormatter.format(subtotal)}</Typography>
@@ -162,7 +165,7 @@ const OrderDetailsDialog = ({ open, onClose, order }) => {
Bestellung stornieren Bestellung stornieren
</Button> </Button>
)} )}
<Button onClick={onClose}>Schließen</Button> <Button onClick={onClose}>{t ? t('common.close') : 'Schließen'}</Button>
</DialogActions> </DialogActions>
</Dialog> </Dialog>
); );

View File

@@ -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;
} }

View File

@@ -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;
@@ -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,7 +73,7 @@ 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>
@@ -83,14 +85,23 @@ const OrderSummary = ({ deliveryCost, cartItems = [] }) => {
{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' }}>
Versandkosten:
{deliveryCost === 0 && cartVatCalculations.totalGross < 100 && (
<span style={{ color: '#2e7d32', fontSize: '0.8em', marginLeft: '4px' }}>
(kostenlos ab 100)
</span>
)}
</TableCell>
<TableCell align="right" sx={{ fontWeight: 'bold' }}> <TableCell align="right" sx={{ fontWeight: 'bold' }}>
{currencyFormatter.format(deliveryCost)} {deliveryCost === 0 ? (
<span style={{ color: '#2e7d32' }}>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 }}>Gesamtsumme:</TableCell>
<TableCell align="right" sx={{ fontWeight: 'bold', pt: 2 }}> <TableCell align="right" sx={{ fontWeight: 'bold', pt: 2 }}>

View File

@@ -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,24 +29,26 @@ 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>
)} )}
@@ -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);

View File

@@ -24,7 +24,7 @@ const PaymentMethodSelector = ({ paymentMethod, onChange, deliveryMethod, onDeli
// Handle delivery method changes - auto-switch to stripe when DHL/DPD is selected // Handle delivery method changes - auto-switch to stripe when DHL/DPD is selected
useEffect(() => { useEffect(() => {
if ((deliveryMethod === "DHL" || deliveryMethod === "DPD") && paymentMethod === "cash") { if ((deliveryMethod === "DHL" || deliveryMethod === "DPD") && paymentMethod === "cash") {
handlePaymentMethodChange({ target: { value: "stripe" } }); handlePaymentMethodChange({ target: { value: "mollie" /*stripe*/ } });
} }
}, [deliveryMethod, paymentMethod, handlePaymentMethodChange]); }, [deliveryMethod, paymentMethod, handlePaymentMethodChange]);
@@ -42,7 +42,7 @@ const PaymentMethodSelector = ({ paymentMethod, onChange, deliveryMethod, onDeli
description: "Bezahlen Sie per Banküberweisung", description: "Bezahlen Sie per Banküberweisung",
disabled: totalAmount === 0, disabled: totalAmount === 0,
}, },
{ /*{
id: "stripe", 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,6 +55,20 @@ 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: "Karte, Sofortüberweisung, Apple Pay, Google Pay, PayPal",
description: totalAmount < 0.50 && totalAmount > 0
? "Bezahlen Sie per Karte oder Sofortüberweisung (Mindestbetrag: 0,50 €)"
: "Bezahlen Sie per Karte oder Sofortüberweisung",
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",

View File

@@ -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: {

View File

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

129
src/i18n/index.js Normal file
View File

@@ -0,0 +1,129 @@
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
import LanguageDetector from 'i18next-browser-languagedetector';
// Import all translation files
import translationDE from './locales/de/translation.js';
import translationEN from './locales/en/translation.js';
import translationAR from './locales/ar/translation.js';
import translationBG from './locales/bg/translation.js';
import translationCS from './locales/cs/translation.js';
import translationEL from './locales/el/translation.js';
import translationES from './locales/es/translation.js';
import translationFR from './locales/fr/translation.js';
import translationHR from './locales/hr/translation.js';
import translationHU from './locales/hu/translation.js';
import translationIT from './locales/it/translation.js';
import translationPL from './locales/pl/translation.js';
import translationRO from './locales/ro/translation.js';
import translationRU from './locales/ru/translation.js';
import translationSK from './locales/sk/translation.js';
import translationSL from './locales/sl/translation.js';
import translationSR from './locales/sr/translation.js';
import translationSV from './locales/sv/translation.js';
import translationTR from './locales/tr/translation.js';
import translationUK from './locales/uk/translation.js';
import translationZH from './locales/zh/translation.js';
const resources = {
de: {
translation: translationDE
},
en: {
translation: translationEN
},
ar: {
translation: translationAR
},
bg: {
translation: translationBG
},
cs: {
translation: translationCS
},
el: {
translation: translationEL
},
es: {
translation: translationES
},
fr: {
translation: translationFR
},
hr: {
translation: translationHR
},
hu: {
translation: translationHU
},
it: {
translation: translationIT
},
pl: {
translation: translationPL
},
ro: {
translation: translationRO
},
ru: {
translation: translationRU
},
sk: {
translation: translationSK
},
sl: {
translation: translationSL
},
sr: {
translation: translationSR
},
sv: {
translation: translationSV
},
tr: {
translation: translationTR
},
uk: {
translation: translationUK
},
zh: {
translation: translationZH
}
};
i18n
.use(LanguageDetector)
.use(initReactI18next)
.init({
resources,
fallbackLng: 'de', // German as fallback since it's your primary language
lng: 'de', // Default language
debug: process.env.NODE_ENV === 'development',
// Language detection options
detection: {
// Order of language detection methods
order: ['localStorage', 'navigator', 'htmlTag'],
// Cache the language selection
caches: ['localStorage'],
// Check for language in localStorage
lookupLocalStorage: 'i18nextLng'
},
interpolation: {
escapeValue: false // React already escapes values
},
// Namespace configuration
defaultNS: 'translation',
// React-specific options
react: {
useSuspense: false // Disable suspense for class components compatibility
}
});
// Export withI18n and other utilities for easy access
export { withI18n, withTranslation, withLanguage, LanguageContext, LanguageProvider } from './withTranslation.js';
export default i18n;

View File

@@ -0,0 +1,25 @@
export default {
"login": "تسجيل الدخول", // Anmelden
"register": "تسجيل", // Registrieren
"logout": "تسجيل خروج", // Abmelden
"profile": "الملف الشخصي", // Profil
"email": "البريد الإلكتروني", // E-Mail
"password": "كلمة المرور", // Passwort
"confirmPassword": "تأكيد كلمة المرور", // Passwort bestätigen
"forgotPassword": "هل نسيت كلمة المرور؟", // Passwort vergessen?
"loginWithGoogle": "تسجيل الدخول باستخدام Google", // Mit Google anmelden
"or": "أو", // ODER
"privacyAccept": "بالنقر على \"تسجيل الدخول باستخدام Google\" أوافق على", // Mit dem Click auf "Mit Google anmelden" akzeptiere ich die
"privacyPolicy": "سياسة الخصوصية", // Datenschutzbestimmungen
"passwordMinLength": "يجب أن تكون كلمة المرور مكونة من 8 أحرف على الأقل", // Das Passwort muss mindestens 8 Zeichen lang sein
"newPasswordMinLength": "يجب أن تكون كلمة المرور الجديدة مكونة من 8 أحرف على الأقل", // Das neue Passwort muss mindestens 8 Zeichen lang sein
"menu": {
"profile": "الملف الشخصي", // Profil
"myProfile": "ملفي الشخصي", // Mein Profil
"checkout": "إتمام الشراء", // Bestellabschluss
"orders": "الطلبات", // Bestellungen
"settings": "الإعدادات", // Einstellungen
"adminDashboard": "لوحة تحكم المسؤول", // Admin Dashboard
"adminUsers": "مستخدمو المسؤول" // Admin Users
}
};

View File

@@ -0,0 +1,24 @@
export default {
"title": "العربة", // Warenkorb
"empty": "فارغ", // leer
"addToCart": "أضف إلى العربة", // In den Korb
"preorderCutting": "اطلب مسبقًا كشتلة", // Als Steckling vorbestellen
"continueShopping": "تابع التسوق", // Weiter einkaufen
"proceedToCheckout": "المتابعة إلى الدفع", // Weiter zur Kasse
"productCount": "{{count}} {{count, plural, one {منتج} other {منتجات}}}", // {{count}} {{count, plural, one {Produkt} other {Produkte}}}
"productSingular": "منتج", // Produkt
"productPlural": "منتجات", // Produkte
"removeFromCart": "إزالة من العربة", // Aus dem Warenkorb entfernen
"openCart": "افتح العربة", // Warenkorb öffnen
"availableFrom": "متوفر من {{date}}", // Ab {{date}}
"backToOrder": "← العودة إلى الطلب", // ← Zurück zur Bestellung
"sync": {
"title": "مزامنة العربة", // Warenkorb-Synchronisierung
"description": "لديك عربة محفوظة في حسابك. من فضلك اختر كيف تريد المتابعة:", // Sie haben einen gespeicherten Warenkorb in ihrem Account. Bitte wählen Sie, wie Sie verfahren möchten:
"deleteServer": "حذف عربة الخادم", // Server-Warenkorb löschen
"useServer": "استخدام عربة الخادم", // Server-Warenkorb übernehmen
"merge": "دمج العربات", // Warenkörbe zusammenführen
"currentCart": "عربتك الحالية", // Ihr aktueller Warenkorb
"serverCart": "العربة المحفوظة في ملفك الشخصي" // In Ihrem Profil gespeicherter Warenkorb
}
};

View File

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

View File

@@ -0,0 +1,13 @@
export default {
"invoiceAddress": "عنوان الفاتورة", // Rechnungsadresse
"deliveryAddress": "عنوان التوصيل", // Lieferadresse
"saveForFuture": "احفظ للطلبات المستقبلية", // Für zukünftige Bestellungen speichern
"pickupDate": "لموعد استلام القصاصات المطلوب؟", // Für welchen Termin ist die Abholung der Stecklinge gewünscht?
"note": "ملاحظة", // Anmerkung
"sameAddress": "عنوان التوصيل مطابق لعنوان الفاتورة", // Lieferadresse ist identisch mit Rechnungsadresse
"termsAccept": "لقد قرأت الشروط والأحكام، سياسة الخصوصية، وأحكام حق الانسحاب", // Ich habe die AGBs, die Datenschutzerklärung und die Bestimmungen zum Widerrufsrecht gelesen
"selectDeliveryMethod": "اختر طريقة الشحن", // Versandart wählen
"selectPaymentMethod": "اختر طريقة الدفع", // Zahlungsart wählen
"orderSummary": "ملخص الطلب", // Bestellübersicht
"addressValidationError": "يرجى التحقق من بياناتك في حقول العنوان." // Bitte überprüfen Sie Ihre Eingaben in den Adressfeldern.
};

View File

@@ -0,0 +1,19 @@
export default {
"loading": "جارٍ التحميل...", // Lädt...
"error": "خطأ", // Fehler
"close": "إغلاق", // Schließen
"save": "حفظ", // Speichern
"cancel": "إلغاء", // Abbrechen
"ok": "موافق", // OK
"yes": "نعم", // Ja
"no": "لا", // Nein
"next": "التالي", // Weiter
"back": "رجوع", // Zurück
"edit": "تعديل", // Bearbeiten
"delete": "حذف", // Löschen
"add": "إضافة", // Hinzufügen
"remove": "إزالة", // Entfernen
"products": "منتجات", // Produkte
"product": "منتج", // Produkt
"days": "أيام" // Tage
};

View File

@@ -0,0 +1,27 @@
export default {
"methods": {
"dhl": "DHL", // DHL
"dpd": "DPD", // DPD
"sperrgut": "بضائع ضخمة", // Sperrgut
"pickup": "الاستلام من المتجر" // Abholung in der Filiale
},
"descriptions": {
"standard": "الشحن العادي", // Standardversand
"standardFree": "الشحن العادي - مجاني للطلبات فوق 100€!", // Standardversand - KOSTENLOS ab 100€ Warenwert!
"notAvailable": "غير متاح للاختيار لأن عنصر واحد أو أكثر يمكن استلامه فقط", // nicht auswählbar weil ein oder mehrere Artikel nur abgeholt werden können
"bulky": "للعناصر الكبيرة والثقيلة", // Für große und schwere Artikel
"pickupOnly": "الاستلام فقط" // nur Abholung
},
"prices": {
"free": "مجاني", // kostenlos
"freeFrom100": "(مجاني للطلبات فوق 100€)", // (kostenlos ab 100€)
"dhl": "6.99 €", // 6,99 €
"dpd": "4.90 €", // 4,90 €
"sperrgut": "28.99 €" // 28,99 €
},
"times": {
"cutting14Days": "مدة التوصيل: 14 يوم", // Lieferzeit: 14 Tage
"standard2to3Days": "مدة التوصيل: 2-3 أيام", // Lieferzeit: 2-3 Tage
"supplier7to9Days": "مدة التوصيل: 7-9 أيام" // Lieferzeit: 7-9 Tage
}
};

View File

@@ -0,0 +1,7 @@
export default {
"sorting": "الترتيب", // Sortierung
"perPage": "لكل صفحة", // pro Seite
"availability": "التوفر", // Verfügbarkeit
"manufacturer": "الشركة المصنعة", // Hersteller
"all": "الكل", // Alle
};

View File

@@ -0,0 +1,15 @@
export default {
"hours": "السبت 11 صباحًا - 7 مساءً", // Sa 11-19
"address": "شارع تراشنبرجر 14 - دريسدن", // Trachenberger Straße 14 - Dresden
"location": "بين محطة بيسشن وساحة تراشنبرجر", // Zwischen Haltepunkt Pieschen und Trachenberger Platz
"allPricesIncl": "* جميع الأسعار تشمل ضريبة القيمة المضافة القانونية، بالإضافة إلى الشحن", // * Alle Preise inkl. gesetzlicher USt., zzgl. Versand
"copyright": "© {{year}} GrowHeads.de", // © {{year}} GrowHeads.de
"legal": {
"datenschutz": "سياسة الخصوصية", // Datenschutz
"agb": "الشروط والأحكام", // AGB
"sitemap": "خريطة الموقع", // Sitemap
"impressum": "الإشعار القانوني", // Impressum
"batteriegesetzhinweise": "معلومات قانون البطاريات", // Batteriegesetzhinweise
"widerrufsrecht": "حق الانسحاب" // Widerrufsrecht
}
};

View 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
};

View File

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

View File

@@ -0,0 +1,9 @@
export default {
"home": "الرئيسية", // Startseite
"aktionen": "العروض", // Aktionen
"filiale": "الفرع", // Filiale
"categories": "الفئات", // Kategorien
"categoriesOpen": "فتح الفئات", // Kategorien öffnen
"categoriesClose": "إغلاق الفئات", // Kategorien schließen
"otherCategories": "فئات أخرى" // Andere Kategorien
};

View File

@@ -0,0 +1,23 @@
export default {
"status": {
"new": "قيد التنفيذ", // in Bearbeitung
"pending": "جديد", // Neu
"processing": "قيد التنفيذ", // in Bearbeitung
"cancelled": "ملغي", // Storniert
"shipped": "تم الشحن", // Verschickt
"delivered": "تم التوصيل", // Geliefert
"return": "إرجاع", // Retoure
"partialReturn": "إرجاع جزئي", // Teil Retoure
"partialDelivered": "تم التوصيل جزئياً" // Teil geliefert
},
"table": {
"orderNumber": "رقم الطلب", // Bestellnummer
"date": "التاريخ", // Datum
"status": "الحالة", // Status
"items": "العناصر", // Artikel
"total": "الإجمالي", // Summe
"actions": "الإجراءات", // Aktionen
"viewDetails": "عرض التفاصيل" // Details anzeigen
},
"noOrders": "لم تقم بوضع أي طلبات بعد." // Sie haben noch keine Bestellungen aufgegeben.
};

View File

@@ -0,0 +1,10 @@
export default {
"oilPress": {
"title": "استعارة مكبس الزيت", // Ölpresse ausleihen
"comingSoon": "المحتوى قادم قريباً...", // Inhalt kommt bald...
},
"thcTest": {
"title": "اختبار THC", // THC Test
"comingSoon": "المحتوى قادم قريباً...", // Inhalt kommt bald...
}
};

View File

@@ -0,0 +1,8 @@
export default {
"successful": "تم الدفع بنجاح!", // Zahlung erfolgreich!
"failed": "فشل الدفع", // Zahlung fehlgeschlagen
"orderCompleted": "🎉 تم إكمال طلبك بنجاح! يمكنك الآن عرض طلباتك.", // 🎉 Ihre Bestellung wurde erfolgreich abgeschlossen! Sie können jetzt Ihre Bestellungen einsehen.
"orderProcessing": "تمت معالجة دفعتك بنجاح. سيتم إكمال الطلب تلقائيًا.", // Ihre Zahlung wurde erfolgreich verarbeitet. Die Bestellung wird automatisch abgeschlossen.
"paymentError": "لم نتمكن من معالجة دفعتك. يرجى المحاولة مرة أخرى أو اختيار طريقة دفع أخرى.", // Ihre Zahlung konnte nicht verarbeitet werden. Bitte versuchen Sie es erneut oder wählen Sie eine andere Zahlungsmethode.
"viewOrders": "اذهب إلى طلباتي" // Zu meinen Bestellungen
};

View File

@@ -0,0 +1,32 @@
export default {
"loading": "جارٍ تحميل المنتج...", // Produkt wird geladen...
"notFound": "المنتج غير موجود", // Produkt nicht gefunden
"notFoundDescription": "المنتج الذي تبحث عنه غير موجود أو تم إزالته.", // Das gesuchte Produkt existiert nicht oder wurde entfernt.
"backToHome": "العودة إلى الصفحة الرئيسية", // Zurück zur Startseite
"error": "خطأ", // Fehler
"articleNumber": "رقم الصنف", // Artikelnummer
"manufacturer": "الشركة المصنعة", // Hersteller
"inclVat": "شامل {{vat}}% ضريبة القيمة المضافة", // inkl. {{vat}}% MwSt.
"priceUnit": "{{price}}/{{unit}}", // {{price}}/{{unit}}
"new": "جديد", // Neu
"weeks": "أسابيع", // Wochen
"arriving": "الوصول:", // Ankunft:
"inclVatFooter": "شامل {{vat}}% ضريبة القيمة المضافة,*", // inkl. {{vat}}% MwSt.,*
"availability": "التوفر", // Verfügbarkeit
"inStock": "متوفر في المخزون", // auf Lager
"comingSoon": "قريباً", // Bald verfügbar
"deliveryTime": "مدة التوصيل", // Lieferzeit
"inclShort": "شامل", // inkl.
"vatShort": "ضريبة القيمة المضافة", // MwSt.
"countDisplay": {
"noProducts": "0 منتجات", // 0 Produkte
"oneProduct": "منتج واحد", // 1 Produkt
"multipleProducts": "{{count}} منتجات", // {{count}} Produkte
"filteredProducts": "{{filtered}} من {{total}} منتجات", // {{filtered}} von {{total}} Produkten
"filteredOneProduct": "{{filtered}} من منتج واحد", // {{filtered}} von 1 Produkt
"xOfYProducts": "{{x}} من {{y}} منتجات" // {{x}} von {{y}} Produkten
},
"removeFiltersToSee": "أزل الفلاتر لرؤية المنتجات", // Entferne Filter um Produkte zu sehen
"outOfStock": "غير متوفر في المخزون", // Out of Stock
"fromXProducts": "من {{count}} منتجات" // ab {{count}} Produkten
};

View File

@@ -0,0 +1,5 @@
export default {
"placeholder": "ممكن تسألني عن أنواع الحشيش...", // Du kannst mich nach Cannabissorten fragen...
"recording": "جاري التسجيل...", // Aufnahme läuft...
"searchProducts": "ابحث عن المنتجات...", // Produkte suchen...
};

View File

@@ -0,0 +1,8 @@
export default {
"seeds": "بذور", // Seeds
"stecklinge": "قصاصات", // Stecklinge
"oilPress": "استعارة معصرة زيت", // Ölpresse ausleihen
"thcTest": "اختبار THC", // THC Test
"address1": "Trachenberger Straße 14", // Trachenberger Straße 14
"address2": "01129 Dresden" // 01129 Dresden
};

View File

@@ -0,0 +1,34 @@
export default {
"changePassword": "تغيير كلمة المرور", // Passwort ändern
"currentPassword": "كلمة المرور الحالية", // Aktuelles Passwort
"newPassword": "كلمة المرور الجديدة", // Neues Passwort
"confirmNewPassword": "تأكيد كلمة المرور الجديدة", // Neues Passwort bestätigen
"updatePassword": "تحديث كلمة المرور", // Passwort aktualisieren
"changeEmail": "تغيير عنوان البريد الإلكتروني", // E-Mail-Adresse ändern
"password": "كلمة المرور", // Passwort
"newEmail": "عنوان البريد الإلكتروني الجديد", // Neue E-Mail-Adresse
"updateEmail": "تحديث البريد الإلكتروني", // E-Mail aktualisieren
"apiKey": "مفتاح API", // API-Schlüssel
"apiKeyDescription": "استخدم مفتاح API الخاص بك للتكامل مع التطبيقات الخارجية.", // Verwenden Sie Ihren API-Schlüssel für die Integration mit externen Anwendungen.
"apiDocumentation": "توثيق API:", // API-Dokumentation:
"copyToClipboard": "نسخ إلى الحافظة", // In Zwischenablage kopieren
"generate": "إنشاء", // Generieren
"regenerate": "إعادة إنشاء", // Regenerieren
"apiKeyCopied": "تم نسخ مفتاح API إلى الحافظة", // API-Schlüssel in Zwischenablage kopiert
"errors": {
"fillAllFields": "يرجى ملء جميع الحقول", // Bitte füllen Sie alle Felder aus
"passwordsNotMatch": "كلمات المرور الجديدة غير متطابقة", // Die neuen Passwörter stimmen nicht überein
"passwordTooShort": "يجب أن تكون كلمة المرور الجديدة 8 أحرف على الأقل", // Das neue Passwort muss mindestens 8 Zeichen lang sein
"passwordUpdateError": "حدث خطأ أثناء تحديث كلمة المرور", // Fehler beim Aktualisieren des Passworts
"invalidEmail": "يرجى إدخال عنوان بريد إلكتروني صالح", // Bitte geben Sie eine gültige E-Mail-Adresse ein
"emailUpdateError": "حدث خطأ أثناء تحديث عنوان البريد الإلكتروني", // Fehler beim Aktualisieren der E-Mail-Adresse
"userNotFound": "المستخدم غير موجود", // Benutzer nicht gefunden
"apiKeyGenerationError": "حدث خطأ أثناء إنشاء مفتاح API" // Fehler beim Generieren des API-Schlüssels
},
"success": {
"passwordUpdated": "تم تحديث كلمة المرور بنجاح", // Passwort erfolgreich aktualisiert
"emailUpdated": "تم تحديث عنوان البريد الإلكتروني بنجاح", // E-Mail-Adresse erfolgreich aktualisiert
"apiKeyGenerated": "تم إنشاء مفتاح API بنجاح", // API-Schlüssel erfolgreich generiert
"apiKeyWarning": "احفظ هذا المفتاح بأمان. لأسباب أمنية، سيتم إخفاؤه خلال 10 ثوانٍ." // Speichern Sie diesen Schlüssel sicher. Er wird aus Sicherheitsgründen in 10 Sekunden ausgeblendet.
}
};

View File

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

View File

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

View File

@@ -0,0 +1,5 @@
export default {
"home": "بذور وقصاصات القنب الممتازة", // Fine Cannabis Seeds & Cuttings
"aktionen": "العروض والتخفيضات الحالية", // Aktuelle Aktionen & Angebote
"filiale": "متجرنا في دريسدن" // Unsere Filiale in Dresden
};

View File

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

View File

@@ -0,0 +1,25 @@
export default {
"login": "Вход", // Anmelden
"register": "Регистрация", // Registrieren
"logout": "Изход", // Abmelden
"profile": "Профил", // Profil
"email": "Имейл", // E-Mail
"password": "Парола", // Passwort
"confirmPassword": "Потвърдете паролата", // Passwort bestätigen
"forgotPassword": "Забравена парола?", // Passwort vergessen?
"loginWithGoogle": "Вход с Google", // Mit Google anmelden
"or": "ИЛИ", // ODER
"privacyAccept": "С натискането на „Вход с Google“ приемам", // Mit dem Click auf "Mit Google anmelden" akzeptiere ich die
"privacyPolicy": "Политиката за поверителност", // Datenschutzbestimmungen
"passwordMinLength": "Паролата трябва да е поне 8 символа", // Das Passwort muss mindestens 8 Zeichen lang sein
"newPasswordMinLength": "Новата парола трябва да е поне 8 символа", // Das neue Passwort muss mindestens 8 Zeichen lang sein
"menu": {
"profile": "Профил", // Profil
"myProfile": "Моят профил", // Mein Profil
"checkout": "Плащане", // Bestellabschluss
"orders": "Поръчки", // Bestellungen
"settings": "Настройки", // Einstellungen
"adminDashboard": "Админ панел", // Admin Dashboard
"adminUsers": "Админ потребители" // Admin Users
}
};

View File

@@ -0,0 +1,24 @@
export default {
"title": "Количка", // Warenkorb
"empty": "празна", // leer
"addToCart": "Добави в количката", // In den Korb
"preorderCutting": "Предварителна поръчка като резник", // Als Steckling vorbestellen
"continueShopping": "Продължи пазаруването", // Weiter einkaufen
"proceedToCheckout": "Продължи към плащане", // Weiter zur Kasse
"productCount": "{{count}} {{count, plural, one {продукт} other {продукта}}}", // {{count}} {{count, plural, one {Produkt} other {Produkte}}}
"productSingular": "продукт", // Produkt
"productPlural": "продукта", // Produkte
"removeFromCart": "Премахни от количката", // Aus dem Warenkorb entfernen
"openCart": "Отвори количката", // Warenkorb öffnen
"availableFrom": "Наличен от {{date}}", // Ab {{date}}
"backToOrder": "← Обратно към поръчката", // ← Zurück zur Bestellung
"sync": {
"title": "Синхронизация на количката", // Warenkorb-Synchronisierung
"description": "Имате запазена количка във вашия акаунт. Моля, изберете как искате да продължите:", // Sie haben einen gespeicherten Warenkorb in ihrem Account. Bitte wählen Sie, wie Sie verfahren möchten:
"deleteServer": "Изтрий количката на сървъра", // Server-Warenkorb löschen
"useServer": "Използвай количката от сървъра", // Server-Warenkorb übernehmen
"merge": "Обедини количките", // Warenkörbe zusammenführen
"currentCart": "Вашата текуща количка", // Ihr aktueller Warenkorb
"serverCart": "Количка, запазена във вашия профил" // In Ihrem Profil gespeicherter Warenkorb
}
};

View File

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

View File

@@ -0,0 +1,13 @@
export default {
"invoiceAddress": "Адрес за фактура", // Rechnungsadresse
"deliveryAddress": "Адрес за доставка", // Lieferadresse
"saveForFuture": "Запази за бъдещи поръчки", // Für zukünftige Bestellungen speichern
"pickupDate": "За коя дата е желано вземането на резниците?", // Für welchen Termin ist die Abholung der Stecklinge gewünscht?
"note": "Бележка", // Anmerkung
"sameAddress": "Адресът за доставка е същият като адреса за фактура", // Lieferadresse ist identisch mit Rechnungsadresse
"termsAccept": "Прочетох общите условия, политиката за поверителност и разпоредбите относно правото на отказ", // Ich habe die AGBs, die Datenschutzerklärung und die Bestimmungen zum Widerrufsrecht gelesen
"selectDeliveryMethod": "Изберете метод на доставка", // Versandart wählen
"selectPaymentMethod": "Изберете метод на плащане", // Zahlungsart wählen
"orderSummary": "Обобщение на поръчката", // Bestellübersicht
"addressValidationError": "Моля, проверете въведените данни в полетата за адрес." // Bitte überprüfen Sie Ihre Eingaben in den Adressfeldern.
};

View File

@@ -0,0 +1,19 @@
export default {
"loading": "Зареждане...", // Lädt...
"error": "Грешка", // Fehler
"close": "Затвори", // Schließen
"save": "Запази", // Speichern
"cancel": "Отказ", // Abbrechen
"ok": "OK", // OK
"yes": "Да", // Ja
"no": "Не", // Nein
"next": "Напред", // Weiter
"back": "Назад", // Zurück
"edit": "Редактирай", // Bearbeiten
"delete": "Изтрий", // Löschen
"add": "Добави", // Hinzufügen
"remove": "Премахни", // Entfernen
"products": "Продукти", // Produkte
"product": "Продукт", // Produkt
"days": "Дни" // Tage
};

View File

@@ -0,0 +1,27 @@
export default {
"methods": {
"dhl": "DHL", // DHL
"dpd": "DPD", // DPD
"sperrgut": "Обемни стоки", // Sperrgut
"pickup": "Вземане от магазина" // Abholung in der Filiale
},
"descriptions": {
"standard": "Стандартна доставка", // Standardversand
"standardFree": "Стандартна доставка - БЕЗПЛАТНО при поръчка над 100€!", // Standardversand - KOSTENLOS ab 100€ Warenwert!
"notAvailable": "Не може да се избере, защото един или повече артикули могат да бъдат взети само на място", // nicht auswählbar weil ein oder mehrere Artikel nur abgeholt werden können
"bulky": "За големи и тежки артикули", // Für große und schwere Artikel
"pickupOnly": "Само вземане" // nur Abholung
},
"prices": {
"free": "безплатно", // kostenlos
"freeFrom100": "(безплатно от 100€)", // (kostenlos ab 100€)
"dhl": "6.99 €", // 6,99 €
"dpd": "4.90 €", // 4,90 €
"sperrgut": "28.99 €" // 28,99 €
},
"times": {
"cutting14Days": "Срок на доставка: 14 дни", // Lieferzeit: 14 Tage
"standard2to3Days": "Срок на доставка: 2-3 дни", // Lieferzeit: 2-3 Tage
"supplier7to9Days": "Срок на доставка: 7-9 дни" // Lieferzeit: 7-9 Tage
}
};

View File

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

View File

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

View 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
};

View File

@@ -0,0 +1,3 @@
export default {
"code": "bg-BG" // de-DE
};

View File

@@ -0,0 +1,9 @@
export default {
"home": "Начало", // Startseite
"aktionen": "Промоции", // Aktionen
"filiale": "Клон", // Filiale
"categories": "Категории", // Kategorien
"categoriesOpen": "Отвори категории", // Kategorien öffnen
"categoriesClose": "Затвори категории", // Kategorien schließen
"otherCategories": "Други категории" // Andere Kategorien
};

View File

@@ -0,0 +1,23 @@
export default {
"status": {
"new": "В процес", // in Bearbeitung
"pending": "Ново", // Neu
"processing": "В процес", // in Bearbeitung
"cancelled": "Отменено", // Storniert
"shipped": "Изпратено", // Verschickt
"delivered": "Доставено", // Geliefert
"return": "Връщане", // Retoure
"partialReturn": "Частично връщане", // Teil Retoure
"partialDelivered": "Частично доставено" // Teil geliefert
},
"table": {
"orderNumber": "Номер на поръчка", // Bestellnummer
"date": "Дата", // Datum
"status": "Статус", // Status
"items": "Артикули", // Artikel
"total": "Общо", // Summe
"actions": "Действия", // Aktionen
"viewDetails": "Виж подробности" // Details anzeigen
},
"noOrders": "Все още не сте направили поръчки." // Sie haben noch keine Bestellungen aufgegeben.
};

View File

@@ -0,0 +1,10 @@
export default {
"oilPress": {
"title": "Наемане на маслобойка", // Ölpresse ausleihen
"comingSoon": "Съдържанието ще бъде налично скоро...", // Inhalt kommt bald...
},
"thcTest": {
"title": "THC тест", // THC Test
"comingSoon": "Съдържанието ще бъде налично скоро...", // Inhalt kommt bald...
}
};

View File

@@ -0,0 +1,8 @@
export default {
"successful": "Плащането беше успешно!", // Zahlung erfolgreich!
"failed": "Плащането не бе успешно", // Zahlung fehlgeschlagen
"orderCompleted": "🎉 Вашата поръчка беше успешно завършена! Сега можете да видите вашите поръчки.", // 🎉 Ihre Bestellung wurde erfolgreich abgeschlossen! Sie können jetzt Ihre Bestellungen einsehen.
"orderProcessing": "Вашето плащане беше успешно обработено. Поръчката ще бъде завършена автоматично.", // Ihre Zahlung wurde erfolgreich verarbeitet. Die Bestellung wird automatisch abgeschlossen.
"paymentError": "Вашето плащане не можа да бъде обработено. Моля, опитайте отново или изберете друг метод на плащане.", // Ihre Zahlung konnte nicht verarbeitet werden. Bitte versuchen Sie es erneut oder wählen Sie eine andere Zahlungsmethode.
"viewOrders": "Отиди на моите поръчки" // Zu meinen Bestellungen
};

View File

@@ -0,0 +1,32 @@
export default {
"loading": "Зареждане на продукта...", // Produkt wird geladen...
"notFound": "Продуктът не е намерен", // Produkt nicht gefunden
"notFoundDescription": "Продуктът, който търсите, не съществува или е бил премахнат.", // Das gesuchte Produkt existiert nicht oder wurde entfernt.
"backToHome": "Обратно към началната страница", // Zurück zur Startseite
"error": "Грешка", // Fehler
"articleNumber": "Номер на артикул", // Artikelnummer
"manufacturer": "Производител", // Hersteller
"inclVat": "вкл. {{vat}}% ДДС", // inkl. {{vat}}% MwSt.
"priceUnit": "{{price}}/{{unit}}", // {{price}}/{{unit}}
"new": "Нов", // Neu
"weeks": "седмици", // Wochen
"arriving": "Пристига:", // Ankunft:
"inclVatFooter": "вкл. {{vat}}% ДДС,*", // inkl. {{vat}}% MwSt.,*
"availability": "Наличност", // Verfügbarkeit
"inStock": "налично на склад", // auf Lager
"comingSoon": "Очаквайте скоро", // Bald verfügbar
"deliveryTime": "Срок на доставка", // Lieferzeit
"inclShort": "вкл.", // inkl.
"vatShort": "ДДС", // MwSt.
"countDisplay": {
"noProducts": "0 продукта", // 0 Produkte
"oneProduct": "1 продукт", // 1 Produkt
"multipleProducts": "{{count}} продукта", // {{count}} Produkte
"filteredProducts": "{{filtered}} от {{total}} продукта", // {{filtered}} von {{total}} Produkten
"filteredOneProduct": "{{filtered}} от 1 продукт", // {{filtered}} von 1 Produkt
"xOfYProducts": "{{x}} от {{y}} продукта" // {{x}} von {{y}} Produkten
},
"removeFiltersToSee": "Премахнете филтрите, за да видите продуктите", // Entferne Filter um Produkte zu sehen
"outOfStock": "Изчерпано количество", // Out of Stock
"fromXProducts": "от {{count}} продукта" // ab {{count}} Produkten
};

View File

@@ -0,0 +1,5 @@
export default {
"placeholder": "Можете да ме попитате за сортове канабис...", // Du kannst mich nach Cannabissorten fragen...
"recording": "Записът е в ход...", // Aufnahme läuft...
"searchProducts": "Търсене на продукти...", // Produkte suchen...
};

View File

@@ -0,0 +1,8 @@
export default {
"seeds": "Семена", // Seeds
"stecklinge": "Резници", // Stecklinge
"oilPress": "Наемане на маслопреса", // Ölpresse ausleihen
"thcTest": "THC тест", // THC Test
"address1": "Trachenberger Straße 14", // Trachenberger Straße 14
"address2": "01129 Dresden" // 01129 Dresden
};

View File

@@ -0,0 +1,34 @@
export default {
"changePassword": "Смяна на парола", // Passwort ändern
"currentPassword": "Текуща парола", // Aktuelles Passwort
"newPassword": "Нова парола", // Neues Passwort
"confirmNewPassword": "Потвърдете новата парола", // Neues Passwort bestätigen
"updatePassword": "Актуализиране на паролата", // Passwort aktualisieren
"changeEmail": "Смяна на имейл адрес", // E-Mail-Adresse ändern
"password": "Парола", // Passwort
"newEmail": "Нов имейл адрес", // Neue E-Mail-Adresse
"updateEmail": "Актуализиране на имейла", // E-Mail aktualisieren
"apiKey": "API ключ", // API-Schlüssel
"apiKeyDescription": "Използвайте своя API ключ за интеграция с външни приложения.", // Verwenden Sie Ihren API-Schlüssel für die Integration mit externen Anwendungen.
"apiDocumentation": "API документация:", // API-Dokumentation:
"copyToClipboard": "Копиране в клипборда", // In Zwischenablage kopieren
"generate": "Генериране", // Generieren
"regenerate": "Генериране отново", // Regenerieren
"apiKeyCopied": "API ключът е копиран в клипборда", // API-Schlüssel in Zwischenablage kopiert
"errors": {
"fillAllFields": "Моля, попълнете всички полета", // Bitte füllen Sie alle Felder aus
"passwordsNotMatch": "Новите пароли не съвпадат", // Die neuen Passwörter stimmen nicht überein
"passwordTooShort": "Новата парола трябва да е поне 8 знака", // Das neue Passwort muss mindestens 8 Zeichen lang sein
"passwordUpdateError": "Грешка при актуализиране на паролата", // Fehler beim Aktualisieren des Passworts
"invalidEmail": "Моля, въведете валиден имейл адрес", // Bitte geben Sie eine gültige E-Mail-Adresse ein
"emailUpdateError": "Грешка при актуализиране на имейл адреса", // Fehler beim Aktualisieren der E-Mail-Adresse
"userNotFound": "Потребителят не е намерен", // Benutzer nicht gefunden
"apiKeyGenerationError": "Грешка при генериране на API ключ" // Fehler beim Generieren des API-Schlüssels
},
"success": {
"passwordUpdated": "Паролата е успешно актуализирана", // Passwort erfolgreich aktualisiert
"emailUpdated": "Имейл адресът е успешно актуализиран", // E-Mail-Adresse erfolgreich aktualisiert
"apiKeyGenerated": "API ключът е успешно генериран", // API-Schlüssel erfolgreich generiert
"apiKeyWarning": "Съхранявайте този ключ на сигурно място. По съображения за сигурност той ще бъде скрит след 10 секунди." // Speichern Sie diesen Schlüssel sicher. Er wird aus Sicherheitsgründen in 10 Sekunden ausgeblendet.
}
};

View File

@@ -0,0 +1,6 @@
export default {
"name": "Име", // Name
"searchField": "Търсене", // Suchbegriff
"priceLowHigh": "Цена: от ниска към висока", // Preis: Niedrig zu Hoch
"priceHighLow": "Цена: от висока към ниска" // Preis: Hoch zu Niedrig
};

View File

@@ -0,0 +1,12 @@
export default {
"vat": "Данък върху добавената стойност", // Mehrwertsteuer
"vat7": "7% Данък върху добавената стойност", // 7% Mehrwertsteuer
"vat19": "19% Данък върху добавената стойност", // 19% Mehrwertsteuer
"vat19WithShipping": "19% Данък върху добавената стойност (вкл. доставка)", // 19% Mehrwertsteuer (inkl. Versand)
"totalNet": "Обща нетна цена", // Gesamtnettopreis
"totalGross": "Обща брутна цена без доставка", // Gesamtbruttopreis ohne Versand
"subtotal": "Междинна сума", // Zwischensumme
"incl7Vat": "вкл. 7% ДДС", // inkl. 7% MwSt.
"inclVatWithFooter": "(вкл. {{vat}}% ДДС,*)", // (incl. {{vat}}% USt.,*)
"inclVatAmount": "вкл. {{amount}} € ДДС ({{rate}}%)" // nkl. {{amount}} € MwSt. ({{rate}}%)
};

View File

@@ -0,0 +1,5 @@
export default {
"home": "Фини семена и резници от канабис", // Fine Cannabis Seeds & Cuttings
"aktionen": "Текущи промоции и оферти", // Aktuelle Aktionen & Angebote
"filiale": "Нашият магазин в Дрезден" // Unsere Filiale in Dresden
};

View File

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

View File

@@ -0,0 +1,25 @@
export default {
"login": "Přihlásit se", // Anmelden
"register": "Registrovat se", // Registrieren
"logout": "Odhlásit se", // Abmelden
"profile": "Profil", // Profil
"email": "Email", // E-Mail
"password": "Heslo", // Passwort
"confirmPassword": "Potvrdit heslo", // Passwort bestätigen
"forgotPassword": "Zapomněli jste heslo?", // Passwort vergessen?
"loginWithGoogle": "Přihlásit se přes Google", // Mit Google anmelden
"or": "NEBO", // ODER
"privacyAccept": "Kliknutím na „Přihlásit se přes Google“ souhlasím s", // Mit dem Click auf "Mit Google anmelden" akzeptiere ich die
"privacyPolicy": "Zásadami ochrany osobních údajů", // Datenschutzbestimmungen
"passwordMinLength": "Heslo musí mít alespoň 8 znaků", // Das Passwort muss mindestens 8 Zeichen lang sein
"newPasswordMinLength": "Nové heslo musí mít alespoň 8 znaků", // Das neue Passwort muss mindestens 8 Zeichen lang sein
"menu": {
"profile": "Profil", // Profil
"myProfile": "Můj profil", // Mein Profil
"checkout": "Dokončení objednávky", // Bestellabschluss
"orders": "Objednávky", // Bestellungen
"settings": "Nastavení", // Einstellungen
"adminDashboard": "Administrátorský panel", // Admin Dashboard
"adminUsers": "Administrátoři" // Admin Users
}
};

View File

@@ -0,0 +1,24 @@
export default {
"title": "Košík", // Warenkorb
"empty": "prázdný", // leer
"addToCart": "Přidat do košíku", // In den Korb
"preorderCutting": "Předobjednat jako řízek", // Als Steckling vorbestellen
"continueShopping": "Pokračovat v nákupu", // Weiter einkaufen
"proceedToCheckout": "Přejít k pokladně", // Weiter zur Kasse
"productCount": "{{count}} {{count, plural, one {produkt} other {produkty}}}", // {{count}} {{count, plural, one {Produkt} other {Produkte}}}
"productSingular": "produkt", // Produkt
"productPlural": "produkty", // Produkte
"removeFromCart": "Odstranit z košíku", // Aus dem Warenkorb entfernen
"openCart": "Otevřít košík", // Warenkorb öffnen
"availableFrom": "Dostupné od {{date}}", // Ab {{date}}
"backToOrder": "← Zpět k objednávce", // ← Zurück zur Bestellung
"sync": {
"title": "Synchronizace košíku", // Warenkorb-Synchronisierung
"description": "Máte uložený košík ve svém účtu. Vyberte, jak chcete pokračovat:", // Sie haben einen gespeicherten Warenkorb in ihrem Account. Bitte wählen Sie, wie Sie verfahren möchten:
"deleteServer": "Smazat košík na serveru", // Server-Warenkorb löschen
"useServer": "Použít košík ze serveru", // Server-Warenkorb übernehmen
"merge": "Sloučit košíky", // Warenkörbe zusammenführen
"currentCart": "Váš aktuální košík", // Ihr aktueller Warenkorb
"serverCart": "Košík uložený ve vašem profilu" // In Ihrem Profil gespeicherter Warenkorb
}
};

View File

@@ -0,0 +1,3 @@
export default {
"privacyRead": "Přečteno a přijato", // Gelesen & Akzeptiert
};

View File

@@ -0,0 +1,13 @@
export default {
"invoiceAddress": "Fakturační adresa", // Rechnungsadresse
"deliveryAddress": "Dodací adresa", // Lieferadresse
"saveForFuture": "Uložit pro budoucí objednávky", // Für zukünftige Bestellungen speichern
"pickupDate": "Na který datum je požadován odběr řízků?", // Für welchen Termin ist die Abholung der Stecklinge gewünscht?
"note": "Poznámka", // Anmerkung
"sameAddress": "Dodací adresa je shodná s fakturační adresou", // Lieferadresse ist identisch mit Rechnungsadresse
"termsAccept": "Přečetl(a) jsem si obchodní podmínky, zásady ochrany osobních údajů a ustanovení o právu na odstoupení od smlouvy", // Ich habe die AGBs, die Datenschutzerklärung und die Bestimmungen zum Widerrufsrecht gelesen
"selectDeliveryMethod": "Vyberte způsob doručení", // Versandart wählen
"selectPaymentMethod": "Vyberte způsob platby", // Zahlungsart wählen
"orderSummary": "Souhrn objednávky", // Bestellübersicht
"addressValidationError": "Zkontrolujte prosím své údaje v polích adresy." // Bitte überprüfen Sie Ihre Eingaben in den Adressfeldern.
};

View File

@@ -0,0 +1,19 @@
export default {
"loading": "Načítání...", // Lädt...
"error": "Chyba", // Fehler
"close": "Zavřít", // Schließen
"save": "Uložit", // Speichern
"cancel": "Zrušit", // Abbrechen
"ok": "OK", // OK
"yes": "Ano", // Ja
"no": "Ne", // Nein
"next": "Další", // Weiter
"back": "Zpět", // Zurück
"edit": "Upravit", // Bearbeiten
"delete": "Smazat", // Löschen
"add": "Přidat", // Hinzufügen
"remove": "Odebrat", // Entfernen
"products": "Produkty", // Produkte
"product": "Produkt", // Produkt
"days": "Dny" // Tage
};

View File

@@ -0,0 +1,27 @@
export default {
"methods": {
"dhl": "DHL", // DHL
"dpd": "DPD", // DPD
"sperrgut": "Objemné zboží", // Sperrgut
"pickup": "Osobní odběr v obchodě" // Abholung in der Filiale
},
"descriptions": {
"standard": "Standardní doprava", // Standardversand
"standardFree": "Standardní doprava - ZDARMA od objednávky nad 100€!", // Standardversand - KOSTENLOS ab 100€ Warenwert!
"notAvailable": "Nelze vybrat, protože jeden nebo více produktů lze pouze vyzvednout", // nicht auswählbar weil ein oder mehrere Artikel nur abgeholt werden können
"bulky": "Pro velké a těžké předměty", // Für große und schwere Artikel
"pickupOnly": "Pouze osobní odběr" // nur Abholung
},
"prices": {
"free": "zdarma", // kostenlos
"freeFrom100": "(zdarma od 100€)", // (kostenlos ab 100€)
"dhl": "6,99 €", // 6,99 €
"dpd": "4,90 €", // 4,90 €
"sperrgut": "28,99 €" // 28,99 €
},
"times": {
"cutting14Days": "Doba dodání: 14 dní", // Lieferzeit: 14 Tage
"standard2to3Days": "Doba dodání: 2-3 dny", // Lieferzeit: 2-3 Tage
"supplier7to9Days": "Doba dodání: 7-9 dní" // Lieferzeit: 7-9 Tage
}
};

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