Compare commits

...

21 Commits

Author SHA1 Message Date
sebseb7
9df5642a6e refactor: reimplement category page display with a recursive tree structure and add configurator image. 2025-12-13 02:14:20 +01:00
sebseb7
a50dd086c3 feat: Include 'Kategorien' in Nginx location regex for HTML content. 2025-12-06 21:02:22 +01:00
sebseb7
e88370ff3e feat: add Categories page with refined layout and translation support 2025-12-06 14:29:33 +01:00
sebseb7
5d3e0832fe doc 2025-12-01 13:11:24 +01:00
sebseb7
3347ba2754 Add missing auth translations and update components to use i18n keys
- Added new translation keys to de/auth.js:
  - resetPassword section (title, button, success, invalidToken, error, emailSent, emailError)
  - errors section (fillAllFields, invalidEmail, passwordsNotMatch, passwordsNotMatchShort, enterEmail, loginFailed, registerFailed, googleLoginFailed, emailExists)
  - success section (registerComplete)
  - newPassword, backToHome keys

- Updated ResetPassword.js to use translation keys instead of hardcoded German strings
- Updated LoginComponent.js to use translation keys instead of hardcoded German strings
- translate-i18n.js generated translations for other languages
2025-12-01 13:02:03 +01:00
sebseb7
013a38ca98 fix: update caniuse-lite version and enhance SPA routing for resetPassword
- Updated caniuse-lite to version 1.0.30001757 in package-lock.json for improved compatibility.
- Added functionality to copy index.html to the resetPassword directory for better SPA routing in production environments.
2025-12-01 12:50:41 +01:00
sebseb7
2d6c8ff25f feat(Orders): add tracking shipment link and update translations
- Implemented a tracking shipment link in the OrdersTab component for DHL deliveries, enhancing user experience by allowing direct access to shipment tracking.
- Added 'trackShipment' translation key across multiple languages to support the new feature.
- Updated existing translations for consistency and improved localization in the orders module.
2025-11-29 14:05:59 +01:00
sebseb7
d2ac8d3fc1 feat(Orders): update order status translations and colors
- Refactored order status translations to use English keys for 'new', 'shipped', and 'delivered'.
- Updated corresponding status colors to maintain consistency in the UI.
- Adjusted the display logic to reflect the new status keys in the OrdersTab component.
2025-11-29 13:45:39 +01:00
sebseb7
8928b3f283 feat(Orders): add 'paid' status and update translations across multiple languages
- Introduced 'paid' status to the orders system, enhancing order tracking capabilities.
- Updated translations for 'paid' status in various languages including German, Spanish, French, and more.
- Adjusted related UI components to reflect the new status and ensure consistent user experience across the application.
2025-11-29 13:21:35 +01:00
sebseb7
87db7ba3ea feat(Content, ProductDetail, SearchBar): enhance product handling with translation support
- Updated Content component to process products using translated attributes, improving localization.
- Modified ProductDetailPage to utilize translatedProduct for similar products.
- Adjusted SearchBar to provide suggestions based on translated products, enhancing user experience across components.
2025-11-23 07:53:37 +01:00
sebseb7
766fef2796 feat(ProductDetail): enhance attribute handling with translation support
- Updated ProductDetailPage to utilize translated attributes if available, improving localization.
- Cached both product and attribute data for better performance.
- Adjusted state management to reflect the use of translated attributes in the component.
2025-11-22 12:48:40 +01:00
sebseb7
a08c90a521 fix 2025-11-22 10:12:41 +01:00
sebseb7
10d60d5827 fix 2025-11-22 10:02:59 +01:00
sebseb7
905eee57d5 feat(Translation): add kitConfig.js for improved localization in GrowTentKonfigurator
- Updated the translation model by adding kitConfig.js to the list of translation files.
- Enhanced the GrowTentKonfigurator component to utilize translation functions for various UI texts, improving localization support throughout the configuration process.
2025-11-22 09:59:47 +01:00
sebseb7
3389a9b66c feat(Translation): enhance product dialogs and update translation model
- Added new translation files for product dialogs to support additional languages.
- Refactored various components to utilize translation functions for error messages, labels, and placeholders, enhancing localization support.
2025-11-22 09:43:51 +01:00
sebseb7
d63c385a97 feat(Content): pass categoryName prop to ProductFilters for improved filtering
- Added categoryName prop to the ProductFilters component to have it translated
2025-11-22 08:49:58 +01:00
sebseb7
1b51da69a9 feat(Images): update image URLs to AVIF format in SEO components
- Changed image file extensions from JPG to AVIF in category, feeds, and product SEO components to enhance performance and reduce file sizes.
- Ensured consistent image handling across the application by updating relevant image paths.
2025-11-21 13:21:58 +01:00
sebseb7
da81479d9b refactor(App): update Box component to use 'main' role for improved semantics
- Changed Box component in App.js and PrerenderAppContent.js to use 'main' as the component role, enhancing accessibility and semantic structure of the application.
2025-11-21 11:57:39 +01:00
sebseb7
d8678e261d feat(Images): update image handling to AVIF format across components
- Changed image file extensions from JPG to AVIF in data-fetching, product, category, and image components for improved performance and reduced file sizes.
- Updated image blob creation to reflect the new AVIF format in various components, ensuring consistency in image handling throughout the application.
2025-11-21 11:10:50 +01:00
sebseb7
ef91e50aa5 feat(Images): convert images to AVIF format for improved performance
- Updated image references in various components and configuration files to use AVIF format instead of PNG and JPG.
- Modified the build process to include a script for converting images to AVIF, enhancing loading times and reducing file sizes.
- Ensured consistency across the application by updating image paths in the footer, main layout, and content components.
2025-11-20 11:43:07 +01:00
sebseb7
061bf5ff17 feat(SEO): add price validity date to category JSON-LD
- Introduced a priceValidUntil field in the category JSON-LD to indicate the validity period of product prices, set to three months from the current date.
- This enhancement improves the structured data for SEO, providing clearer information about pricing timelines.
2025-11-20 11:13:48 +01:00
180 changed files with 4624 additions and 920 deletions

154
docs/nginx.conf Normal file
View File

@@ -0,0 +1,154 @@
server {
client_max_body_size 64M;
listen 443 ssl;
http2 on;
server_name example.de;
ssl_certificate /etc/letsencrypt/live/example.de/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.de/privkey.pem;
gzip on;
gzip_comp_level 6;
gzip_min_length 256;
gzip_vary on;
gzip_proxied any;
gzip_types
text/css
text/javascript
application/javascript
application/x-javascript
application/json
application/xml
image/svg+xml;
index index.html;
root /example/dist;
error_log logs/error.log info;
access_log logs/access.log combined;
location /socket.io/ {
proxy_pass http://localhost:9303/socket.io/;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
proxy_connect_timeout 3600s;
proxy_send_timeout 3600s;
proxy_read_timeout 3600s;
send_timeout 3600s;
proxy_buffering off;
proxy_cache off;
keepalive_timeout 65;
keepalive_requests 100;
}
location /api/ {
proxy_pass http://localhost:9303/api/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header User-Agent $http_user_agent;
proxy_set_header Content-Type $content_type;
proxy_set_header Content-Length $content_length;
proxy_set_header X-API-Key $http_x_api_key;
proxy_connect_timeout 30s;
proxy_send_timeout 30s;
proxy_read_timeout 30s;
proxy_buffering off;
client_max_body_size 10M;
}
location ^~ /Kategorie/ {
types {}
default_type text/html;
}
location ^~ /Artikel/ {
types {}
default_type text/html;
}
location = /sitemap.xml {
types {}
default_type application/xml;
}
location ~ ^/(datenschutz|impressum|batteriegesetzhinweise|widerrufsrecht|sitemap|agb|Kategorien|Konfigurator|404|profile|resetPassword|thc-test|filiale|aktionen|presseverleih|payment/success)(/|$) {
types {}
default_type text/html;
}
location = /404 {
error_page 404 =404 /404-big.html;
return 404;
}
location = /404-big.html {
internal;
alias /home/seb/src/growheads_de/dist/404;
default_type text/html;
}
error_page 404 /404.html;
location = /404.html {
internal;
default_type text/html;
return 404 '<!doctype html><html><body>
<script>
if (!navigator.userAgent.includes("bot")) { location.href="/404"; }
</script>
</body></html>';
}
location ~* \.(js|css)\?.*$ {
expires 1y;
add_header Cache-Control "public, immutable";
add_header Vary Accept-Encoding;
}
location ~* \.(js|css)$ {
if ($uri ~ "\.[a-f0-9]{7,}\.(js|css)$") {
expires 1y;
add_header Cache-Control "public, immutable";
break;
}
expires 1d;
add_header Cache-Control "public";
add_header Vary Accept-Encoding;
}
location ~* \.(ttf|otf|woff|woff2|eot)$ {
expires 1y;
add_header Cache-Control "public";
add_header Access-Control-Allow-Origin "*";
}
location ~* \.(jpg|jpeg|png|gif|ico|svg|webp)$ {
expires 1y;
add_header Cache-Control "public";
add_header Vary Accept-Encoding;
}
location = /prerender.css {
expires 1w;
add_header Cache-Control "public";
add_header Vary Accept-Encoding;
}
location /assets/ {
expires 1y;
add_header Cache-Control "public";
add_header Vary Accept-Encoding;
}
}

6
package-lock.json generated
View File

@@ -4554,9 +4554,9 @@
} }
}, },
"node_modules/caniuse-lite": { "node_modules/caniuse-lite": {
"version": "1.0.30001727", "version": "1.0.30001757",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001727.tgz", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001757.tgz",
"integrity": "sha512-pB68nIHmbN6L/4C6MH1DokyR3bYqFwjaSs/sWDHGj4CTcFtQUQMuJftVwWkXq7mNWOybD3KhUv3oWHoGxgP14Q==", "integrity": "sha512-r0nnL/I28Zi/yjk1el6ilj27tKcdjLsNqAOZr0yVjWPrSQyHgKI2INaEWw21bAQSv2LXRt1XuCS/GomNpWOxsQ==",
"dev": true, "dev": true,
"funding": [ "funding": [
{ {

View File

@@ -7,7 +7,7 @@
"start": "cross-env NODE_OPTIONS=\"--no-deprecation\" webpack serve --progress --mode development --no-open", "start": "cross-env NODE_OPTIONS=\"--no-deprecation\" webpack serve --progress --mode development --no-open",
"start:seedheads": "cross-env PROXY_TARGET=https://seedheads.de NODE_OPTIONS=\"--no-deprecation\" webpack serve --progress --mode development --no-open", "start:seedheads": "cross-env PROXY_TARGET=https://seedheads.de NODE_OPTIONS=\"--no-deprecation\" webpack serve --progress --mode development --no-open",
"prod": "webpack serve --progress --mode production --no-client-overlay --no-client --no-web-socket-server --no-open --no-live-reload --no-hot --compress --no-devtool", "prod": "webpack serve --progress --mode production --no-client-overlay --no-client --no-web-socket-server --no-open --no-live-reload --no-hot --compress --no-devtool",
"build:client": "cross-env NODE_ENV=production webpack --progress --mode production && shx cp dist/index.html dist/index_template.html", "build:client": "node scripts/convert-images-to-avif.cjs && cross-env NODE_ENV=production webpack --progress --mode production && shx cp dist/index.html dist/index_template.html",
"build": "npm run build:client", "build": "npm run build:client",
"analyze": "cross-env ANALYZE=true NODE_ENV=production webpack --progress --mode production", "analyze": "cross-env ANALYZE=true NODE_ENV=production webpack --progress --mode production",
"lint": "eslint src/**/*.{js,jsx}", "lint": "eslint src/**/*.{js,jsx}",

View File

@@ -28,7 +28,7 @@ class CategoryService {
const cacheKey = `${categoryId}_${language}`; const cacheKey = `${categoryId}_${language}`;
return null; return null;
} }
async get(categoryId, language = "de") { async get(categoryId, language = "de") {
const cacheKey = `${categoryId}_${language}`; const cacheKey = `${categoryId}_${language}`;
return null; return null;
@@ -159,6 +159,7 @@ const Batteriegesetzhinweise =
const Widerrufsrecht = require("./src/pages/Widerrufsrecht.js").default; const Widerrufsrecht = require("./src/pages/Widerrufsrecht.js").default;
const Sitemap = require("./src/pages/Sitemap.js").default; const Sitemap = require("./src/pages/Sitemap.js").default;
const PrerenderSitemap = require("./src/PrerenderSitemap.js").default; const PrerenderSitemap = require("./src/PrerenderSitemap.js").default;
const PrerenderCategoriesPage = require("./src/PrerenderCategoriesPage.js").default;
const AGB = require("./src/pages/AGB.js").default; const AGB = require("./src/pages/AGB.js").default;
const NotFound404 = require("./src/pages/NotFound404.js").default; const NotFound404 = require("./src/pages/NotFound404.js").default;
@@ -189,7 +190,7 @@ const renderProductWorker = async (productSeoNames, workerId, progressCallback,
try { try {
const productDetails = await fetchProductDetails(workerSocket, productSeoName); const productDetails = await fetchProductDetails(workerSocket, productSeoName);
const actualSeoName = productDetails.product.seoName || productSeoName; const actualSeoName = productDetails.product.seoName || productSeoName;
const productComponent = React.createElement(PrerenderProduct, { const productComponent = React.createElement(PrerenderProduct, {
productData: productDetails, productData: productDetails,
@@ -205,7 +206,7 @@ const renderProductWorker = async (productSeoNames, workerId, progressCallback,
}, shopConfig.baseUrl, shopConfig); }, shopConfig.baseUrl, shopConfig);
// Get category info from categoryMap if available // Get category info from categoryMap if available
const categoryInfo = productDetails.product.categoryId ? categoryMap[productDetails.product.categoryId] : null; const categoryInfo = productDetails.product.categoryId ? categoryMap[productDetails.product.categoryId] : null;
const jsonLdScript = generateProductJsonLd({ const jsonLdScript = generateProductJsonLd({
...productDetails.product, ...productDetails.product,
seoName: actualSeoName, seoName: actualSeoName,
@@ -234,9 +235,9 @@ const renderProductWorker = async (productSeoNames, workerId, progressCallback,
success, success,
workerId workerId
}; };
results.push(result); results.push(result);
// Call progress callback if provided // Call progress callback if provided
if (progressCallback) { if (progressCallback) {
progressCallback(result); progressCallback(result);
@@ -252,14 +253,14 @@ const renderProductWorker = async (productSeoNames, workerId, progressCallback,
error: error.message, error: error.message,
workerId workerId
}; };
results.push(result); results.push(result);
// Call progress callback if provided // Call progress callback if provided
if (progressCallback) { if (progressCallback) {
progressCallback(result); progressCallback(result);
} }
setTimeout(processNextProduct, 25); setTimeout(processNextProduct, 25);
} }
}; };
@@ -291,16 +292,16 @@ const renderProductsInParallel = async (allProductsArray, maxWorkers, totalProdu
const barLength = 30; const barLength = 30;
const filledLength = Math.round((barLength * current) / total); const filledLength = Math.round((barLength * current) / total);
const bar = '█'.repeat(filledLength) + '░'.repeat(barLength - filledLength); const bar = '█'.repeat(filledLength) + '░'.repeat(barLength - filledLength);
// @note Single line progress update to prevent flickering // @note Single line progress update to prevent flickering
const truncatedName = productName ? ` - ${productName.substring(0, 25)}${productName.length > 25 ? '...' : ''}` : ''; const truncatedName = productName ? ` - ${productName.substring(0, 25)}${productName.length > 25 ? '...' : ''}` : '';
// Build worker stats on one line // Build worker stats on one line
let workerStats = ''; let workerStats = '';
for (let i = 0; i < Math.min(maxWorkers, 8); i++) { // Limit to 8 workers to fit on screen for (let i = 0; i < Math.min(maxWorkers, 8); i++) { // Limit to 8 workers to fit on screen
workerStats += `W${i + 1}:${workerCounts[i]}/${workerSuccess[i]} `; workerStats += `W${i + 1}:${workerCounts[i]}/${workerSuccess[i]} `;
} }
// Single line update without complex cursor movements // Single line update without complex cursor movements
process.stdout.write(`\r [${bar}] ${percentage}% (${current}/${total})${truncatedName}\n ${workerStats}${current < total ? '\x1b[1A' : '\n'}`); process.stdout.write(`\r [${bar}] ${percentage}% (${current}/${total})${truncatedName}\n ${workerStats}${current < total ? '\x1b[1A' : '\n'}`);
}; };
@@ -308,26 +309,26 @@ const renderProductsInParallel = async (allProductsArray, maxWorkers, totalProdu
// Split products among workers // Split products among workers
const productsPerWorker = Math.ceil(allProductsArray.length / maxWorkers); const productsPerWorker = Math.ceil(allProductsArray.length / maxWorkers);
const workerPromises = []; const workerPromises = [];
// Initial progress bar // Initial progress bar
updateProgressBar(0, totalProducts); updateProgressBar(0, totalProducts);
for (let i = 0; i < maxWorkers; i++) { for (let i = 0; i < maxWorkers; i++) {
const start = i * productsPerWorker; const start = i * productsPerWorker;
const end = Math.min(start + productsPerWorker, allProductsArray.length); const end = Math.min(start + productsPerWorker, allProductsArray.length);
const productsForWorker = allProductsArray.slice(start, end); const productsForWorker = allProductsArray.slice(start, end);
if (productsForWorker.length > 0) { if (productsForWorker.length > 0) {
const promise = renderProductWorker(productsForWorker, i + 1, (result) => { const promise = renderProductWorker(productsForWorker, i + 1, (result) => {
// Progress callback - called each time a product is completed // Progress callback - called each time a product is completed
completedProducts++; completedProducts++;
progressResults.push(result); progressResults.push(result);
lastProductName = result.productName; lastProductName = result.productName;
// Update per-worker counters // Update per-worker counters
const workerIndex = result.workerId - 1; // Convert to 0-based index const workerIndex = result.workerId - 1; // Convert to 0-based index
workerCounts[workerIndex]++; workerCounts[workerIndex]++;
if (result.success) { if (result.success) {
totalSuccessCount++; totalSuccessCount++;
workerSuccess[workerIndex]++; workerSuccess[workerIndex]++;
@@ -335,11 +336,11 @@ const renderProductsInParallel = async (allProductsArray, maxWorkers, totalProdu
// Don't log errors immediately to avoid interfering with progress bar // Don't log errors immediately to avoid interfering with progress bar
// Errors will be shown after completion // Errors will be shown after completion
} }
// Update progress bar with worker stats // Update progress bar with worker stats
updateProgressBar(completedProducts, totalProducts, lastProductName); updateProgressBar(completedProducts, totalProducts, lastProductName);
}, categoryMap); }, categoryMap);
workerPromises.push(promise); workerPromises.push(promise);
} }
} }
@@ -347,10 +348,10 @@ const renderProductsInParallel = async (allProductsArray, maxWorkers, totalProdu
try { try {
// Wait for all workers to complete // Wait for all workers to complete
await Promise.all(workerPromises); await Promise.all(workerPromises);
// Ensure final progress update // Ensure final progress update
updateProgressBar(totalProducts, totalProducts, lastProductName); updateProgressBar(totalProducts, totalProducts, lastProductName);
// Show any errors that occurred // Show any errors that occurred
const errorResults = progressResults.filter(r => !r.success && r.error); const errorResults = progressResults.filter(r => !r.success && r.error);
if (errorResults.length > 0) { if (errorResults.length > 0) {
@@ -359,7 +360,7 @@ const renderProductsInParallel = async (allProductsArray, maxWorkers, totalProdu
console.log(` - ${result.productSeoName}: ${result.error}`); console.log(` - ${result.productSeoName}: ${result.error}`);
}); });
} }
return totalSuccessCount; return totalSuccessCount;
} catch (error) { } catch (error) {
console.error('Error in parallel rendering:', error); console.error('Error in parallel rendering:', error);
@@ -422,6 +423,14 @@ const renderApp = async (categoryData, socket) => {
process.exit(1); process.exit(1);
} }
// Copy index.html to resetPassword (no file extension) for SPA routing
if (config.isProduction) {
const indexPath = path.resolve(__dirname, config.outputDir, "index.html");
const resetPasswordPath = path.resolve(__dirname, config.outputDir, "resetPassword");
fs.copyFileSync(indexPath, resetPasswordPath);
console.log(`✅ Copied index.html to ${resetPasswordPath}`);
}
// Render static pages // Render static pages
console.log("\n📄 Rendering static pages..."); console.log("\n📄 Rendering static pages...");
@@ -457,6 +466,13 @@ const renderApp = async (categoryData, socket) => {
description: "Sitemap page", description: "Sitemap page",
needsCategoryData: true, needsCategoryData: true,
}, },
{
component: PrerenderCategoriesPage,
path: "/Kategorien",
filename: "Kategorien",
description: "Categories page",
needsCategoryData: true,
},
{ component: AGB, path: "/agb", filename: "agb", description: "AGB page" }, { component: AGB, path: "/agb", filename: "agb", description: "AGB page" },
{ component: NotFound404, path: "/404", filename: "404", description: "404 Not Found page" }, { component: NotFound404, path: "/404", filename: "404", description: "404 Not Found page" },
{ {
@@ -551,8 +567,7 @@ const renderApp = async (categoryData, socket) => {
try { try {
productData = await fetchCategoryProducts(socket, category.id); productData = await fetchCategoryProducts(socket, category.id);
console.log( console.log(
` ✅ Found ${ ` ✅ Found ${productData.products ? productData.products.length : 0
productData.products ? productData.products.length : 0
} products` } products`
); );
@@ -636,7 +651,7 @@ const renderApp = async (categoryData, socket) => {
const totalProducts = allProducts.size; const totalProducts = allProducts.size;
const numCPUs = os.cpus().length; const numCPUs = os.cpus().length;
const maxWorkers = Math.min(numCPUs, totalProducts, 8); // Cap at 8 workers to avoid overwhelming the server const maxWorkers = Math.min(numCPUs, totalProducts, 8); // Cap at 8 workers to avoid overwhelming the server
// Create category map for breadcrumbs // Create category map for breadcrumbs
const categoryMap = {}; const categoryMap = {};
allCategories.forEach(category => { allCategories.forEach(category => {
@@ -645,11 +660,11 @@ const renderApp = async (categoryData, socket) => {
seoName: category.seoName seoName: category.seoName
}; };
}); });
console.log( console.log(
`\n📦 Rendering ${totalProducts} individual product pages using ${maxWorkers} parallel workers...` `\n📦 Rendering ${totalProducts} individual product pages using ${maxWorkers} parallel workers...`
); );
const productPagesRendered = await renderProductsInParallel( const productPagesRendered = await renderProductsInParallel(
Array.from(allProducts), Array.from(allProducts),
maxWorkers, maxWorkers,
@@ -701,21 +716,21 @@ const renderApp = async (categoryData, socket) => {
// Generate products.xml (Google Shopping feed) in parallel to sitemap.xml // Generate products.xml (Google Shopping feed) in parallel to sitemap.xml
if (allProductsData.length > 0) { if (allProductsData.length > 0) {
console.log("\n🛒 Generating products.xml (Google Shopping feed)..."); console.log("\n🛒 Generating products.xml (Google Shopping feed)...");
try { try {
const productsXml = generateProductsXml(allProductsData, shopConfig.baseUrl, shopConfig); const productsXml = generateProductsXml(allProductsData, shopConfig.baseUrl, shopConfig);
const productsXmlPath = path.resolve(__dirname, config.outputDir, "products.xml"); const productsXmlPath = path.resolve(__dirname, config.outputDir, "products.xml");
// Write with explicit UTF-8 encoding // Write with explicit UTF-8 encoding
fs.writeFileSync(productsXmlPath, productsXml, { encoding: 'utf8' }); fs.writeFileSync(productsXmlPath, productsXml, { encoding: 'utf8' });
console.log(`✅ products.xml generated: ${productsXmlPath}`); console.log(`✅ products.xml generated: ${productsXmlPath}`);
console.log(` - Products included: ${allProductsData.length}`); console.log(` - Products included: ${allProductsData.length}`);
console.log(` - Format: Google Shopping RSS 2.0 feed`); console.log(` - Format: Google Shopping RSS 2.0 feed`);
console.log(` - Encoding: UTF-8`); console.log(` - Encoding: UTF-8`);
console.log(` - Includes: title, description, price, availability, images`); console.log(` - Includes: title, description, price, availability, images`);
// Verify the file is valid UTF-8 // Verify the file is valid UTF-8
try { try {
const verification = fs.readFileSync(productsXmlPath, 'utf8'); const verification = fs.readFileSync(productsXmlPath, 'utf8');
@@ -723,18 +738,18 @@ const renderApp = async (categoryData, socket) => {
} catch (verifyError) { } catch (verifyError) {
console.log(` - File verification: ⚠️ ${verifyError.message}`); console.log(` - File verification: ⚠️ ${verifyError.message}`);
} }
// Validate XML against Google Shopping schema // Validate XML against Google Shopping schema
try { try {
const ProductsXmlValidator = require('./scripts/validate-products-xml.cjs'); const ProductsXmlValidator = require('./scripts/validate-products-xml.cjs');
const validator = new ProductsXmlValidator(productsXmlPath); const validator = new ProductsXmlValidator(productsXmlPath);
const validationResults = await validator.validate(); const validationResults = await validator.validate();
if (validationResults.valid) { if (validationResults.valid) {
console.log(` - Schema validation: ✅ Valid Google Shopping RSS 2.0`); console.log(` - Schema validation: ✅ Valid Google Shopping RSS 2.0`);
} else { } else {
console.log(` - Schema validation: ⚠️ ${validationResults.summary.errorCount} errors, ${validationResults.summary.warningCount} warnings`); console.log(` - Schema validation: ⚠️ ${validationResults.summary.errorCount} errors, ${validationResults.summary.warningCount} warnings`);
// Show first few errors for quick debugging // Show first few errors for quick debugging
if (validationResults.errors.length > 0) { if (validationResults.errors.length > 0) {
console.log(` - First error: ${validationResults.errors[0].message}`); console.log(` - First error: ${validationResults.errors[0].message}`);
@@ -743,7 +758,7 @@ const renderApp = async (categoryData, socket) => {
} catch (validationError) { } catch (validationError) {
console.log(` - Schema validation: ⚠️ Validation failed: ${validationError.message}`); console.log(` - Schema validation: ⚠️ Validation failed: ${validationError.message}`);
} }
} catch (error) { } catch (error) {
console.error(`❌ Error generating products.xml: ${error.message}`); console.error(`❌ Error generating products.xml: ${error.message}`);
console.log("\n⚠ Skipping products.xml generation due to errors"); console.log("\n⚠ Skipping products.xml generation due to errors");
@@ -754,18 +769,18 @@ const renderApp = async (categoryData, socket) => {
// Generate llms.txt (LLM-friendly markdown sitemap) and category-specific files // Generate llms.txt (LLM-friendly markdown sitemap) and category-specific files
console.log("\n🤖 Generating LLM sitemap files..."); console.log("\n🤖 Generating LLM sitemap files...");
try { try {
// Generate main llms.txt overview file // Generate main llms.txt overview file
const llmsTxt = generateLlmsTxt(allCategories, allProductsData, shopConfig.baseUrl, shopConfig); const llmsTxt = generateLlmsTxt(allCategories, allProductsData, shopConfig.baseUrl, shopConfig);
const llmsTxtPath = path.resolve(__dirname, config.outputDir, "llms.txt"); const llmsTxtPath = path.resolve(__dirname, config.outputDir, "llms.txt");
fs.writeFileSync(llmsTxtPath, llmsTxt, { encoding: 'utf8' }); fs.writeFileSync(llmsTxtPath, llmsTxt, { encoding: 'utf8' });
console.log(`✅ Main llms.txt generated: ${llmsTxtPath}`); console.log(`✅ Main llms.txt generated: ${llmsTxtPath}`);
console.log(` - Static pages: 8 pages`); console.log(` - Static pages: 8 pages`);
console.log(` - Categories: ${allCategories.length} with links to detailed files`); console.log(` - Categories: ${allCategories.length} with links to detailed files`);
console.log(` - File size: ${Math.round(llmsTxt.length / 1024)}KB`); console.log(` - File size: ${Math.round(llmsTxt.length / 1024)}KB`);
// Group products by category for category-specific files // Group products by category for category-specific files
const productsByCategory = {}; const productsByCategory = {};
allProductsData.forEach((product) => { allProductsData.forEach((product) => {
@@ -775,20 +790,20 @@ const renderApp = async (categoryData, socket) => {
} }
productsByCategory[categoryId].push(product); productsByCategory[categoryId].push(product);
}); });
// Generate category-specific LLM files with pagination // Generate category-specific LLM files with pagination
let categoryFilesGenerated = 0; let categoryFilesGenerated = 0;
let totalCategoryProducts = 0; let totalCategoryProducts = 0;
let totalPaginatedFiles = 0; let totalPaginatedFiles = 0;
for (const category of allCategories) { for (const category of allCategories) {
if (category.seoName) { if (category.seoName) {
const categoryProducts = productsByCategory[category.id] || []; const categoryProducts = productsByCategory[category.id] || [];
const categorySlug = category.seoName.toLowerCase().replace(/[^a-z0-9]/g, '-'); const categorySlug = category.seoName.toLowerCase().replace(/[^a-z0-9]/g, '-');
// Generate all paginated files for this category // Generate all paginated files for this category
const categoryPages = generateAllCategoryLlmsPages(category, categoryProducts, shopConfig.baseUrl, shopConfig); const categoryPages = generateAllCategoryLlmsPages(category, categoryProducts, shopConfig.baseUrl, shopConfig);
// Write each paginated file // Write each paginated file
for (const page of categoryPages) { for (const page of categoryPages) {
const pagePath = path.resolve(__dirname, config.outputDir, page.fileName); const pagePath = path.resolve(__dirname, config.outputDir, page.fileName);
@@ -806,22 +821,22 @@ const renderApp = async (categoryData, socket) => {
console.log(` ✅ llms-${categorySlug}-page-*.txt - ${categoryProducts.length} products across ${pageCount} pages (${Math.round(totalSize / 1024)}KB total)`); console.log(` ✅ llms-${categorySlug}-page-*.txt - ${categoryProducts.length} products across ${pageCount} pages (${Math.round(totalSize / 1024)}KB total)`);
console.log(` 📋 ${productList.fileName} - ${productList.productCount} products (${Math.round(productList.content.length / 1024)}KB)`); console.log(` 📋 ${productList.fileName} - ${productList.productCount} products (${Math.round(productList.content.length / 1024)}KB)`);
categoryFilesGenerated++; categoryFilesGenerated++;
totalCategoryProducts += categoryProducts.length; totalCategoryProducts += categoryProducts.length;
} }
} }
console.log(` 📄 Total paginated files generated: ${totalPaginatedFiles}`); console.log(` 📄 Total paginated files generated: ${totalPaginatedFiles}`);
console.log(` 📦 Total products across all categories: ${totalCategoryProducts}`); console.log(` 📦 Total products across all categories: ${totalCategoryProducts}`);
try { try {
const verification = fs.readFileSync(llmsTxtPath, 'utf8'); const verification = fs.readFileSync(llmsTxtPath, 'utf8');
console.log(` - File verification: ✅ All files valid UTF-8`); console.log(` - File verification: ✅ All files valid UTF-8`);
} catch (verifyError) { } catch (verifyError) {
console.log(` - File verification: ⚠️ ${verifyError.message}`); console.log(` - File verification: ⚠️ ${verifyError.message}`);
} }
} catch (error) { } catch (error) {
console.error(`❌ Error generating LLM sitemap files: ${error.message}`); console.error(`❌ Error generating LLM sitemap files: ${error.message}`);
console.log("\n⚠ Skipping LLM sitemap generation due to errors"); console.log("\n⚠ Skipping LLM sitemap generation due to errors");
@@ -841,7 +856,7 @@ const fetchCategoryDataAndRender = () => {
const socket = io(socketUrl, { const socket = io(socketUrl, {
path: "/socket.io/", path: "/socket.io/",
transports: [ "websocket"], transports: ["websocket"],
reconnection: false, reconnection: false,
timeout: 10000, timeout: 10000,
}); });

View File

@@ -152,7 +152,7 @@ const saveProductImages = async (socket, products, categoryName, outputDir) => {
"public", "public",
"assets", "assets",
"images", "images",
"sh.png" "sh.avif"
); );
// Ensure assets/images directory exists // Ensure assets/images directory exists
@@ -185,7 +185,7 @@ const saveProductImages = async (socket, products, categoryName, outputDir) => {
if (imageIds.length > 0) { if (imageIds.length > 0) {
// Process first image for each product // Process first image for each product
const bildId = parseInt(imageIds[0]); const bildId = parseInt(imageIds[0]);
const estimatedFilename = `prod${bildId}.jpg`; // We'll generate a filename based on the ID const estimatedFilename = `prod${bildId}.avif`; // We'll generate a filename based on the ID
const imagePath = path.join(assetsPath, estimatedFilename); const imagePath = path.join(assetsPath, estimatedFilename);
@@ -231,12 +231,12 @@ const saveProductImages = async (socket, products, categoryName, outputDir) => {
opacity: 0.3, opacity: 0.3,
}, },
]) ])
.jpeg() // Ensure output is JPEG .avif() // Ensure output is AVIF
.toBuffer(); .toBuffer();
fs.writeFileSync(imagePath, processedImageBuffer); fs.writeFileSync(imagePath, processedImageBuffer);
console.log( console.log(
` ✅ Applied centered inverted sh.png overlay to ${estimatedFilename}` ` ✅ Applied centered inverted sh.avif overlay to ${estimatedFilename}`
); );
} catch (overlayError) { } catch (overlayError) {
console.log( console.log(
@@ -281,7 +281,7 @@ const saveCategoryImages = async (socket, categories, outputDir) => {
// Debug: Log categories that will be processed // Debug: Log categories that will be processed
console.log(" 🔍 Categories to process:"); console.log(" 🔍 Categories to process:");
categories.forEach((cat, index) => { categories.forEach((cat, index) => {
console.log(` ${index + 1}. "${cat.name}" (ID: ${cat.id}) -> cat${cat.id}.jpg`); console.log(` ${index + 1}. "${cat.name}" (ID: ${cat.id}) -> cat${cat.id}.avif`);
}); });
const assetsPath = path.resolve( const assetsPath = path.resolve(
@@ -308,7 +308,7 @@ const saveCategoryImages = async (socket, categories, outputDir) => {
for (const category of categories) { for (const category of categories) {
categoriesProcessed++; categoriesProcessed++;
const estimatedFilename = `cat${category.id}.jpg`; // Use 'cat' prefix with category ID const estimatedFilename = `cat${category.id}.avif`; // Use 'cat' prefix with category ID
const imagePath = path.join(assetsPath, estimatedFilename); const imagePath = path.join(assetsPath, estimatedFilename);
// Skip if image already exists // Skip if image already exists

View File

@@ -1,6 +1,19 @@
const generateCategoryJsonLd = (category, products = [], baseUrl, config) => { const generateCategoryJsonLd = (category, products = [], baseUrl, config) => {
// Category IDs to skip (seeds, plants, headshop items)
const skipCategoryIds = [689, 706, 709, 711, 714, 748, 749, 896, 710, 924, 923, 922, 921, 916, 278, 259, 258];
// Check if category ID is in skip list
if (category.id && skipCategoryIds.includes(parseInt(category.id))) {
return '';
}
const categoryUrl = `${baseUrl}/Kategorie/${category.seoName}`; const categoryUrl = `${baseUrl}/Kategorie/${category.seoName}`;
// Calculate price valid date (current date + 3 months)
const priceValidDate = new Date();
priceValidDate.setMonth(priceValidDate.getMonth() + 3);
const priceValidUntil = priceValidDate.toISOString().split("T")[0];
const jsonLd = { const jsonLd = {
"@context": "https://schema.org/", "@context": "https://schema.org/",
"@type": "CollectionPage", "@type": "CollectionPage",
@@ -42,7 +55,7 @@ const generateCategoryJsonLd = (category, products = [], baseUrl, config) => {
product.pictureList && product.pictureList.trim() product.pictureList && product.pictureList.trim()
? `${baseUrl}/assets/images/prod${product.pictureList ? `${baseUrl}/assets/images/prod${product.pictureList
.split(",")[0] .split(",")[0]
.trim()}.jpg` .trim()}.avif`
: `${baseUrl}/assets/images/nopicture.jpg`, : `${baseUrl}/assets/images/nopicture.jpg`,
description: product.description description: product.description
? product.description.replace(/<[^>]*>/g, "").substring(0, 200) ? product.description.replace(/<[^>]*>/g, "").substring(0, 200)
@@ -57,6 +70,7 @@ const generateCategoryJsonLd = (category, products = [], baseUrl, config) => {
url: `${baseUrl}/Artikel/${product.seoName}`, url: `${baseUrl}/Artikel/${product.seoName}`,
price: product.price && !isNaN(product.price) ? product.price.toString() : "0.00", price: product.price && !isNaN(product.price) ? product.price.toString() : "0.00",
priceCurrency: config.currency, priceCurrency: config.currency,
priceValidUntil: priceValidUntil,
availability: product.available availability: product.available
? "https://schema.org/InStock" ? "https://schema.org/InStock"
: "https://schema.org/OutOfStock", : "https://schema.org/OutOfStock",

View File

@@ -535,7 +535,7 @@ const generateProductsXml = (allProductsData = [], baseUrl, config) => {
// Generate image URL // Generate image URL
const imageUrl = product.pictureList && product.pictureList.trim() const imageUrl = product.pictureList && product.pictureList.trim()
? `${baseUrl}/assets/images/prod${product.pictureList.split(",")[0].trim()}.jpg` ? `${baseUrl}/assets/images/prod${product.pictureList.split(",")[0].trim()}.avif`
: `${baseUrl}/assets/images/nopicture.jpg`; : `${baseUrl}/assets/images/nopicture.jpg`;
// Generate brand (manufacturer) // Generate brand (manufacturer)

View File

@@ -5,7 +5,7 @@ const generateProductMetaTags = (product, baseUrl, config) => {
product.pictureList && product.pictureList.trim() product.pictureList && product.pictureList.trim()
? `${baseUrl}/assets/images/prod${product.pictureList ? `${baseUrl}/assets/images/prod${product.pictureList
.split(",")[0] .split(",")[0]
.trim()}.jpg` .trim()}.avif`
: `${baseUrl}/assets/images/nopicture.jpg`; : `${baseUrl}/assets/images/nopicture.jpg`;
@@ -68,7 +68,7 @@ const generateProductJsonLd = (product, baseUrl, config, categoryInfo = null) =>
product.pictureList && product.pictureList.trim() product.pictureList && product.pictureList.trim()
? `${baseUrl}/assets/images/prod${product.pictureList ? `${baseUrl}/assets/images/prod${product.pictureList
.split(",")[0] .split(",")[0]
.trim()}.jpg` .trim()}.avif`
: `${baseUrl}/assets/images/nopicture.jpg`; : `${baseUrl}/assets/images/nopicture.jpg`;
// Clean description for JSON-LD (remove HTML tags) // Clean description for JSON-LD (remove HTML tags)

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 362 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

View File

@@ -0,0 +1,61 @@
const sharp = require('sharp');
const path = require('path');
const fs = require('fs');
const imagesToConvert = [
{ src: 'sh.png', dest: 'sh.avif' },
{ src: 'seeds.jpg', dest: 'seeds.avif' },
{ src: 'cutlings.jpg', dest: 'cutlings.avif' },
{ src: 'gg.png', dest: 'gg.avif' },
{ src: 'konfigurator.png', dest: 'konfigurator.avif' },
{ src: 'maps.png', dest: 'maps.avif' }
];
const run = async () => {
const imagesDir = path.join(__dirname, '../public/assets/images');
let hasError = false;
for (const image of imagesToConvert) {
const inputPath = path.join(imagesDir, image.src);
const outputPath = path.join(imagesDir, image.dest);
console.log('d');
if (!fs.existsSync(inputPath)) {
console.warn(`⚠️ Input file not found: ${inputPath}`);
continue;
}
// Check if output file exists and compare modification times
// Only convert if source is newer or destination doesn't exist
let shouldConvert = true;
if (fs.existsSync(outputPath)) {
const inputStat = fs.statSync(inputPath);
const outputStat = fs.statSync(outputPath);
if (inputStat.mtime <= outputStat.mtime) {
shouldConvert = false;
}
}
if (shouldConvert) {
try {
await sharp(inputPath)
.toFormat('avif')
.toFile(outputPath);
console.log(`✅ Converted ${image.src} to ${image.dest}`);
} catch (error) {
console.error(`❌ Error converting ${image.src}:`, error.message);
hasError = true;
}
} else {
// Silent skip if already up to date to keep logs clean, or use verbose flag
// console.log(`Skipping ${image.src} (already up to date)`);
}
}
if (hasError) {
process.exit(1);
}
};
console.log('dsfs');
run();

View File

@@ -0,0 +1,26 @@
const sharp = require('sharp');
const path = require('path');
const fs = require('fs');
const run = async () => {
const inputPath = path.join(__dirname, '../public/assets/images/sh.png');
const outputPath = path.join(__dirname, '../public/assets/images/sh.avif');
if (!fs.existsSync(inputPath)) {
console.error('Input file not found:', inputPath);
process.exit(1);
}
try {
await sharp(inputPath)
.toFormat('avif')
.toFile(outputPath);
console.log(`Successfully converted ${inputPath} to ${outputPath}`);
} catch (error) {
console.error('Error converting image:', error);
process.exit(1);
}
};
run();

View File

@@ -50,6 +50,7 @@ const Datenschutz = lazy(() => import(/* webpackChunkName: "legal" */ "./pages/D
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")); <Route path="/404" element={<NotFound404 />} /> //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 CategoriesPage = lazy(() => import(/* webpackChunkName: "categories" */ "./pages/CategoriesPage.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"));
const Widerrufsrecht = lazy(() => import(/* webpackChunkName: "legal" */ "./pages/Widerrufsrecht.js")); const Widerrufsrecht = lazy(() => import(/* webpackChunkName: "legal" */ "./pages/Widerrufsrecht.js"));
@@ -228,7 +229,7 @@ const AppContent = ({ currentTheme, dynamicTheme, onThemeChange }) => {
<TitleUpdater /> <TitleUpdater />
<ScrollToTop /> <ScrollToTop />
<Header active categoryId={categoryId} key={authVersion} /> <Header active categoryId={categoryId} key={authVersion} />
<Box sx={{ flexGrow: 1 }}> <Box component="main" sx={{ flexGrow: 1 }}>
<Suspense fallback={ <Suspense fallback={
// Use prerender fallback if available, otherwise show loading spinner // Use prerender fallback if available, otherwise show loading spinner
typeof window !== "undefined" && window.__PRERENDER_FALLBACK__ ? ( typeof window !== "undefined" && window.__PRERENDER_FALLBACK__ ? (
@@ -260,19 +261,19 @@ const AppContent = ({ currentTheme, dynamicTheme, onThemeChange }) => {
{/* Category page - Render Content in parallel */} {/* Category page - Render Content in parallel */}
<Route <Route
path="/Kategorie/:categoryId" path="/Kategorie/:categoryId"
element={<Content/>} element={<Content />}
/> />
{/* Single product page */} {/* Single product page */}
<Route <Route
path="/Artikel/:seoName" path="/Artikel/:seoName"
element={<ProductDetail/>} element={<ProductDetail />}
/> />
{/* Search page - Render Content in parallel */} {/* Search page - Render Content in parallel */}
<Route path="/search" element={<Content/>} /> <Route path="/search" element={<Content />} />
{/* Profile page */} {/* Profile page */}
<Route path="/profile" element={<ProfilePage/>} /> <Route path="/profile" element={<ProfilePage />} />
{/* Payment success page for Mollie redirects */} {/* Payment success page for Mollie redirects */}
<Route path="/payment/success" element={<PaymentSuccess />} /> <Route path="/payment/success" element={<PaymentSuccess />} />
@@ -280,22 +281,23 @@ const AppContent = ({ currentTheme, dynamicTheme, onThemeChange }) => {
{/* Reset password page */} {/* Reset password page */}
<Route <Route
path="/resetPassword" path="/resetPassword"
element={<ResetPassword/>} element={<ResetPassword />}
/> />
{/* Admin page */} {/* Admin page */}
<Route path="/admin" element={<AdminPage/>} /> <Route path="/admin" element={<AdminPage />} />
{/* Admin Users page */} {/* Admin Users page */}
<Route path="/admin/users" element={<UsersPage/>} /> <Route path="/admin/users" element={<UsersPage />} />
{/* Admin Server Logs page */} {/* Admin Server Logs page */}
<Route path="/admin/logs" element={<ServerLogsPage/>} /> <Route path="/admin/logs" element={<ServerLogsPage />} />
{/* 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="/sitemap" element={<Sitemap />} /> <Route path="/sitemap" element={<Sitemap />} />
<Route path="/Kategorien" element={<CategoriesPage />} />
<Route path="/impressum" element={<Impressum />} /> <Route path="/impressum" element={<Impressum />} />
<Route <Route
path="/batteriegesetzhinweise" path="/batteriegesetzhinweise"
@@ -304,7 +306,7 @@ const AppContent = ({ currentTheme, dynamicTheme, 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 />} />
{/* Separate pages that are truly different */} {/* Separate pages that are truly different */}
<Route path="/presseverleih" element={<PresseverleihPage />} /> <Route path="/presseverleih" element={<PresseverleihPage />} />
@@ -457,11 +459,11 @@ const App = () => {
<ProductContextProvider> <ProductContextProvider>
<CategoryContextProvider> <CategoryContextProvider>
<CssBaseline /> <CssBaseline />
<AppContent <AppContent
currentTheme={currentTheme} currentTheme={currentTheme}
dynamicTheme={dynamicTheme} dynamicTheme={dynamicTheme}
onThemeChange={handleThemeChange} onThemeChange={handleThemeChange}
/> />
</CategoryContextProvider> </CategoryContextProvider>
</ProductContextProvider> </ProductContextProvider>
</ThemeProvider> </ThemeProvider>

View File

@@ -44,7 +44,7 @@ const PrerenderAppContent = (socket) => (
<CategoryList categoryId={209} activeCategoryId={null} socket={socket}/> <CategoryList categoryId={209} activeCategoryId={null} socket={socket}/>
</AppBar> </AppBar>
<Box sx={{ flexGrow: 1 }}> <Box component="main" sx={{ flexGrow: 1 }}>
<CarouselProvider> <CarouselProvider>
<Routes> <Routes>
<Route path="/" element={<MainPageLayout />} /> <Route path="/" element={<MainPageLayout />} />

View File

@@ -0,0 +1,118 @@
import React from 'react';
import Box from '@mui/material/Box';
import Typography from '@mui/material/Typography';
import Paper from '@mui/material/Paper';
import LegalPage from './pages/LegalPage.js';
import CategoryBox from './components/CategoryBox.js';
const PrerenderCategoriesPage = ({ categoryData }) => {
// Helper function to recursively collect all categories from the tree
const collectAllCategories = (categoryNode, categories = [], level = 0) => {
if (!categoryNode) return categories;
// Add current category (skip root category 209)
if (categoryNode.id !== 209 && categoryNode.seoName) {
categories.push({
id: categoryNode.id,
name: categoryNode.name,
seoName: categoryNode.seoName,
level: level
});
}
// Recursively add children
if (categoryNode.children) {
for (const child of categoryNode.children) {
collectAllCategories(child, categories, level + 1);
}
}
return categories;
};
// The categoryData passed prop is the root tree (id: 209)
const rootTree = categoryData;
const renderLevel1Section = (l1Node) => {
// Collect all descendants (excluding the L1 node itself, which collectAllCategories would include first)
const descendants = collectAllCategories(l1Node).slice(1);
return (
<Paper
key={l1Node.id}
elevation={1}
sx={{
p: 2,
mb: 3,
display: 'flex',
flexDirection: { xs: 'column', md: 'row' },
alignItems: { xs: 'flex-start', md: 'flex-start' },
gap: 3
}}
>
{/* Level 1 Header/Box */}
<Box sx={{
minWidth: '150px',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
gap: 1
}}>
<CategoryBox
id={l1Node.id}
name={l1Node.name}
seoName={l1Node.seoName}
sx={{
boxShadow: 4,
width: '150px',
height: '150px'
}}
/>
</Box>
{/* Descendants area */}
<Box sx={{ flex: 1 }}>
<Box sx={{
display: 'flex',
flexWrap: 'wrap',
gap: 2
}}>
{descendants.map((cat) => (
<CategoryBox
key={cat.id}
id={cat.id}
name={cat.name}
seoName={cat.seoName}
sx={{
width: '100px',
height: '100px',
minWidth: '100px',
minHeight: '100px',
boxShadow: 1,
fontSize: '0.9rem'
}}
/>
))}
</Box>
</Box>
</Paper>
);
};
const content = (
<Box>
<Box>
{rootTree && rootTree.children && rootTree.children.map((child) => (
renderLevel1Section(child)
))}
{(!rootTree || !rootTree.children || rootTree.children.length === 0) && (
<Typography>Keine Kategorien gefunden.</Typography>
)}
</Box>
</Box>
);
return <LegalPage title="Kategorien" content={content} />;
};
export default PrerenderCategoriesPage;

View File

@@ -111,7 +111,7 @@ const PrerenderCategory = ({ categoryId, categoryName, categorySeoName: _categor
component="img" component="img"
height="200" height="200"
image={product.pictureList && product.pictureList.trim() image={product.pictureList && product.pictureList.trim()
? `/assets/images/prod${product.pictureList.split(',')[0].trim()}.jpg` ? `/assets/images/prod${product.pictureList.split(',')[0].trim()}.avif`
: '/assets/images/nopicture.jpg' : '/assets/images/nopicture.jpg'
} }
alt={product.name} alt={product.name}

View File

@@ -79,7 +79,7 @@ class ArticleAvailabilityForm extends Component {
} else { } else {
this.setState({ this.setState({
loading: false, loading: false,
error: response.error || 'Ein Fehler ist aufgetreten' error: response.error || this.props.t("productDialogs.errorGeneric")
}); });
} }
@@ -114,20 +114,21 @@ class ArticleAvailabilityForm extends Component {
render() { render() {
const { name, email, telegramId, notificationMethod, message, loading, success, error } = this.state; const { name, email, telegramId, notificationMethod, message, loading, success, error } = this.state;
const { t } = this.props;
return ( return (
<Paper id="availability-form" sx={{ p: 3, mt: 4, borderRadius: 2, boxShadow: "0 2px 8px rgba(0,0,0,0.08)" }}> <Paper id="availability-form" sx={{ p: 3, mt: 4, borderRadius: 2, boxShadow: "0 2px 8px rgba(0,0,0,0.08)" }}>
<Typography variant="h5" gutterBottom sx={{ fontWeight: 600, color: '#333' }}> <Typography variant="h5" gutterBottom sx={{ fontWeight: 600, color: '#333' }}>
Verfügbarkeit anfragen {t("productDialogs.availabilityTitle")}
</Typography> </Typography>
<Typography variant="body2" color="text.secondary" sx={{ mb: 3 }}> <Typography variant="body2" color="text.secondary" sx={{ mb: 3 }}>
Dieser Artikel ist derzeit nicht verfügbar. Gerne informieren wir Sie, sobald er wieder lieferbar ist. {t("productDialogs.availabilitySubtitle")}
</Typography> </Typography>
{success && ( {success && (
<Alert severity="success" sx={{ mb: 3 }}> <Alert severity="success" sx={{ mb: 3 }}>
Vielen Dank für Ihre Anfrage! Wir werden Sie {notificationMethod === 'email' ? 'per E-Mail' : 'über Telegram'} informieren, sobald der Artikel wieder verfügbar ist. {notificationMethod === 'email' ? t("productDialogs.availabilitySuccessEmail") : t("productDialogs.availabilitySuccessTelegram")}
</Alert> </Alert>
)} )}
@@ -139,18 +140,18 @@ class ArticleAvailabilityForm extends Component {
<Box component="form" onSubmit={this.handleSubmit} sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}> <Box component="form" onSubmit={this.handleSubmit} sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
<TextField <TextField
label="Name" label={t("productDialogs.nameLabel")}
value={name} value={name}
onChange={this.handleInputChange('name')} onChange={this.handleInputChange('name')}
required required
fullWidth fullWidth
disabled={loading} disabled={loading}
placeholder="Ihr Name" placeholder={t("productDialogs.namePlaceholder")}
/> />
<FormControl component="fieldset" disabled={loading}> <FormControl component="fieldset" disabled={loading}>
<FormLabel component="legend" sx={{ mb: 1 }}> <FormLabel component="legend" sx={{ mb: 1 }}>
Wie möchten Sie benachrichtigt werden? {t("productDialogs.notificationMethodLabel")}
</FormLabel> </FormLabel>
<RadioGroup <RadioGroup
value={notificationMethod} value={notificationMethod}
@@ -160,51 +161,51 @@ class ArticleAvailabilityForm extends Component {
<FormControlLabel <FormControlLabel
value="email" value="email"
control={<Radio />} control={<Radio />}
label="E-Mail" label={t("productDialogs.emailLabel")}
/> />
<FormControlLabel <FormControlLabel
value="telegram" value="telegram"
control={<Radio />} control={<Radio />}
label="Telegram Bot" label={t("productDialogs.telegramBotLabel")}
/> />
</RadioGroup> </RadioGroup>
</FormControl> </FormControl>
{notificationMethod === 'email' && ( {notificationMethod === 'email' && (
<TextField <TextField
label="E-Mail" label={t("productDialogs.emailLabel")}
type="email" type="email"
value={email} value={email}
onChange={this.handleInputChange('email')} onChange={this.handleInputChange('email')}
required required
fullWidth fullWidth
disabled={loading} disabled={loading}
placeholder="ihre.email@example.com" placeholder={t("productDialogs.emailPlaceholder")}
/> />
)} )}
{notificationMethod === 'telegram' && ( {notificationMethod === 'telegram' && (
<TextField <TextField
label="Telegram ID" label={t("productDialogs.telegramIdLabel")}
value={telegramId} value={telegramId}
onChange={this.handleInputChange('telegramId')} onChange={this.handleInputChange('telegramId')}
required required
fullWidth fullWidth
disabled={loading} disabled={loading}
placeholder="@IhrTelegramName oder Telegram ID" placeholder={t("productDialogs.telegramPlaceholder")}
helperText="Geben Sie Ihren Telegram-Benutzernamen (mit @) oder Ihre Telegram-ID ein" helperText={t("productDialogs.telegramHelper")}
/> />
)} )}
<TextField <TextField
label="Nachricht (optional)" label={t("productDialogs.messageLabel")}
value={message} value={message}
onChange={this.handleInputChange('message')} onChange={this.handleInputChange('message')}
fullWidth fullWidth
multiline multiline
rows={3} rows={3}
disabled={loading} disabled={loading}
placeholder="Zusätzliche Informationen oder Fragen..." placeholder={t("productDialogs.messagePlaceholder")}
/> />
<Button <Button
@@ -225,10 +226,10 @@ class ArticleAvailabilityForm extends Component {
{loading ? ( {loading ? (
<> <>
<CircularProgress size={20} sx={{ mr: 1 }} /> <CircularProgress size={20} sx={{ mr: 1 }} />
Wird gesendet... {t("productDialogs.sending")}
</> </>
) : ( ) : (
'Verfügbarkeit anfragen' t("productDialogs.submitAvailability")
)} )}
</Button> </Button>
</Box> </Box>

View File

@@ -98,7 +98,7 @@ class ArticleQuestionForm extends Component {
} else { } else {
this.setState({ this.setState({
loading: false, loading: false,
error: response.error || 'Ein Fehler ist aufgetreten' error: response.error || this.props.t("productDialogs.errorGeneric")
}); });
} }
@@ -110,7 +110,7 @@ class ArticleQuestionForm extends Component {
} catch { } catch {
this.setState({ this.setState({
loading: false, loading: false,
error: 'Fehler beim Verarbeiten der Fotos' error: this.props.t("productDialogs.errorPhotos")
}); });
} }
@@ -140,20 +140,21 @@ class ArticleQuestionForm extends Component {
render() { render() {
const { name, email, question, loading, success, error } = this.state; const { name, email, question, loading, success, error } = this.state;
const { t } = this.props;
return ( return (
<Paper sx={{ p: 3, mt: 4, borderRadius: 2, boxShadow: "0 2px 8px rgba(0,0,0,0.08)" }}> <Paper sx={{ p: 3, mt: 4, borderRadius: 2, boxShadow: "0 2px 8px rgba(0,0,0,0.08)" }}>
<Typography variant="h5" gutterBottom sx={{ fontWeight: 600, color: '#333' }}> <Typography variant="h5" gutterBottom sx={{ fontWeight: 600, color: '#333' }}>
Frage zum Artikel {t("productDialogs.questionTitle")}
</Typography> </Typography>
<Typography variant="body2" color="text.secondary" sx={{ mb: 3 }}> <Typography variant="body2" color="text.secondary" sx={{ mb: 3 }}>
Haben Sie eine Frage zu diesem Artikel? Wir helfen Ihnen gerne weiter. {t("productDialogs.questionSubtitle")}
</Typography> </Typography>
{success && ( {success && (
<Alert severity="success" sx={{ mb: 3 }}> <Alert severity="success" sx={{ mb: 3 }}>
Vielen Dank für Ihre Frage! Wir werden uns schnellstmöglich bei Ihnen melden. {t("productDialogs.questionSuccess")}
</Alert> </Alert>
)} )}
@@ -165,28 +166,28 @@ class ArticleQuestionForm extends Component {
<Box component="form" onSubmit={this.handleSubmit} sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}> <Box component="form" onSubmit={this.handleSubmit} sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
<TextField <TextField
label="Name" label={t("productDialogs.nameLabel")}
value={name} value={name}
onChange={this.handleInputChange('name')} onChange={this.handleInputChange('name')}
required required
fullWidth fullWidth
disabled={loading} disabled={loading}
placeholder="Ihr Name" placeholder={t("productDialogs.namePlaceholder")}
/> />
<TextField <TextField
label="E-Mail" label={t("productDialogs.emailLabel")}
type="email" type="email"
value={email} value={email}
onChange={this.handleInputChange('email')} onChange={this.handleInputChange('email')}
required required
fullWidth fullWidth
disabled={loading} disabled={loading}
placeholder="ihre.email@example.com" placeholder={t("productDialogs.emailPlaceholder")}
/> />
<TextField <TextField
label="Ihre Frage" label={t("productDialogs.questionLabel")}
value={question} value={question}
onChange={this.handleInputChange('question')} onChange={this.handleInputChange('question')}
required required
@@ -194,7 +195,7 @@ class ArticleQuestionForm extends Component {
multiline multiline
rows={4} rows={4}
disabled={loading} disabled={loading}
placeholder="Beschreiben Sie Ihre Frage zu diesem Artikel..." placeholder={t("productDialogs.questionPlaceholder")}
/> />
<PhotoUpload <PhotoUpload
@@ -202,7 +203,7 @@ class ArticleQuestionForm extends Component {
onChange={this.handlePhotosChange} onChange={this.handlePhotosChange}
disabled={loading} disabled={loading}
maxFiles={3} maxFiles={3}
label="Fotos zur Frage anhängen (optional)" label={t("productDialogs.photosLabelQuestion")}
/> />
<Button <Button
@@ -219,10 +220,10 @@ class ArticleQuestionForm extends Component {
{loading ? ( {loading ? (
<> <>
<CircularProgress size={20} sx={{ mr: 1 }} /> <CircularProgress size={20} sx={{ mr: 1 }} />
Wird gesendet... {t("productDialogs.sending")}
</> </>
) : ( ) : (
'Frage senden' t("productDialogs.submitQuestion")
)} )}
</Button> </Button>
</Box> </Box>

View File

@@ -106,7 +106,7 @@ class ArticleRatingForm extends Component {
} else { } else {
this.setState({ this.setState({
loading: false, loading: false,
error: response.error || 'Ein Fehler ist aufgetreten' error: response.error || this.props.t("productDialogs.errorGeneric")
}); });
} }
@@ -118,7 +118,7 @@ class ArticleRatingForm extends Component {
} catch { } catch {
this.setState({ this.setState({
loading: false, loading: false,
error: 'Fehler beim Verarbeiten der Fotos' error: this.props.t("productDialogs.errorPhotos")
}); });
} }
@@ -149,20 +149,21 @@ class ArticleRatingForm extends Component {
render() { render() {
const { name, email, rating, review, loading, success, error } = this.state; const { name, email, rating, review, loading, success, error } = this.state;
const { t } = this.props;
return ( return (
<Paper sx={{ p: 3, mt: 4, borderRadius: 2, boxShadow: "0 2px 8px rgba(0,0,0,0.08)" }}> <Paper sx={{ p: 3, mt: 4, borderRadius: 2, boxShadow: "0 2px 8px rgba(0,0,0,0.08)" }}>
<Typography variant="h5" gutterBottom sx={{ fontWeight: 600, color: '#333' }}> <Typography variant="h5" gutterBottom sx={{ fontWeight: 600, color: '#333' }}>
Artikel Bewerten {t("productDialogs.ratingTitle")}
</Typography> </Typography>
<Typography variant="body2" color="text.secondary" sx={{ mb: 3 }}> <Typography variant="body2" color="text.secondary" sx={{ mb: 3 }}>
Teilen Sie Ihre Erfahrungen mit diesem Artikel und helfen Sie anderen Kunden bei der Entscheidung. {t("productDialogs.ratingSubtitle")}
</Typography> </Typography>
{success && ( {success && (
<Alert severity="success" sx={{ mb: 3 }}> <Alert severity="success" sx={{ mb: 3 }}>
Vielen Dank für Ihre Bewertung! Sie wird nach Prüfung veröffentlicht. {t("productDialogs.ratingSuccess")}
</Alert> </Alert>
)} )}
@@ -174,30 +175,30 @@ class ArticleRatingForm extends Component {
<Box component="form" onSubmit={this.handleSubmit} sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}> <Box component="form" onSubmit={this.handleSubmit} sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
<TextField <TextField
label="Name" label={t("productDialogs.nameLabel")}
value={name} value={name}
onChange={this.handleInputChange('name')} onChange={this.handleInputChange('name')}
required required
fullWidth fullWidth
disabled={loading} disabled={loading}
placeholder="Ihr Name" placeholder={t("productDialogs.namePlaceholder")}
/> />
<TextField <TextField
label="E-Mail" label={t("productDialogs.emailLabel")}
type="email" type="email"
value={email} value={email}
onChange={this.handleInputChange('email')} onChange={this.handleInputChange('email')}
required required
fullWidth fullWidth
disabled={loading} disabled={loading}
placeholder="ihre.email@example.com" placeholder={t("productDialogs.emailPlaceholder")}
helperText="Ihre E-Mail wird nicht veröffentlicht" helperText={t("productDialogs.emailHelper")}
/> />
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}> <Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
<Typography variant="body2" sx={{ fontWeight: 500 }}> <Typography variant="body2" sx={{ fontWeight: 500 }}>
Bewertung * {t("productDialogs.ratingLabel")}
</Typography> </Typography>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}> <Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<Rating <Rating
@@ -209,20 +210,20 @@ class ArticleRatingForm extends Component {
emptyIcon={<StarIcon style={{ opacity: 0.55 }} fontSize="inherit" />} emptyIcon={<StarIcon style={{ opacity: 0.55 }} fontSize="inherit" />}
/> />
<Typography variant="body2" color="text.secondary"> <Typography variant="body2" color="text.secondary">
{rating > 0 ? `${rating} von 5 Sternen` : 'Bitte bewerten'} {rating > 0 ? t("productDialogs.ratingStars", { rating }) : t("productDialogs.pleaseRate")}
</Typography> </Typography>
</Box> </Box>
</Box> </Box>
<TextField <TextField
label="Ihre Bewertung (optional)" label={t("productDialogs.reviewLabel")}
value={review} value={review}
onChange={this.handleInputChange('review')} onChange={this.handleInputChange('review')}
fullWidth fullWidth
multiline multiline
rows={4} rows={4}
disabled={loading} disabled={loading}
placeholder="Beschreiben Sie Ihre Erfahrungen mit diesem Artikel..." placeholder={t("productDialogs.reviewPlaceholder")}
/> />
<PhotoUpload <PhotoUpload
@@ -230,7 +231,7 @@ class ArticleRatingForm extends Component {
onChange={this.handlePhotosChange} onChange={this.handlePhotosChange}
disabled={loading} disabled={loading}
maxFiles={5} maxFiles={5}
label="Fotos zur Bewertung anhängen (optional)" label={t("productDialogs.photosLabelRating")}
/> />
<Button <Button
@@ -247,10 +248,10 @@ class ArticleRatingForm extends Component {
{loading ? ( {loading ? (
<> <>
<CircularProgress size={20} sx={{ mr: 1 }} /> <CircularProgress size={20} sx={{ mr: 1 }} />
Wird gesendet... {t("productDialogs.sending")}
</> </>
) : ( ) : (
'Bewertung abgeben' t("productDialogs.submitRating")
)} )}
</Button> </Button>
</Box> </Box>

View File

@@ -23,7 +23,7 @@ class CartItem extends Component {
window.socketManager.emit('getPic', { bildId:picid, size:'tiny' }, (res) => { window.socketManager.emit('getPic', { bildId:picid, size:'tiny' }, (res) => {
if(res.success){ if(res.success){
window.tinyPicCache[picid] = URL.createObjectURL(new Blob([res.imageBuffer], { type: 'image/jpeg' })); window.tinyPicCache[picid] = URL.createObjectURL(new Blob([res.imageBuffer], { type: 'image/avif' }));
this.setState({image: window.tinyPicCache[picid], loading: false}); this.setState({image: window.tinyPicCache[picid], loading: false});
} }
}) })

View File

@@ -47,7 +47,7 @@ const CategoryBox = ({
// Create fresh blob URL from cached binary data // Create fresh blob URL from cached binary data
try { try {
const uint8Array = new Uint8Array(cachedImageData); const uint8Array = new Uint8Array(cachedImageData);
const blob = new Blob([uint8Array], { type: 'image/jpeg' }); const blob = new Blob([uint8Array], { type: 'image/avif' });
objectUrl = URL.createObjectURL(blob); objectUrl = URL.createObjectURL(blob);
setImageUrl(objectUrl); setImageUrl(objectUrl);
setImageError(false); setImageError(false);
@@ -73,7 +73,7 @@ const CategoryBox = ({
try { try {
// Convert binary data to blob URL // Convert binary data to blob URL
const uint8Array = new Uint8Array(imageData); const uint8Array = new Uint8Array(imageData);
const blob = new Blob([uint8Array], { type: 'image/jpeg' }); const blob = new Blob([uint8Array], { type: 'image/avif' });
objectUrl = URL.createObjectURL(blob); objectUrl = URL.createObjectURL(blob);
setImageUrl(objectUrl); setImageUrl(objectUrl);
setImageError(false); setImageError(false);
@@ -158,7 +158,7 @@ const CategoryBox = ({
position: 'relative', position: 'relative',
backgroundImage: ((typeof window !== 'undefined' && window.__PRERENDER_FALLBACK__) || backgroundImage: ((typeof window !== 'undefined' && window.__PRERENDER_FALLBACK__) ||
(typeof global !== 'undefined' && global.window && global.window.__PRERENDER_FALLBACK__)) (typeof global !== 'undefined' && global.window && global.window.__PRERENDER_FALLBACK__))
? `url("/assets/images/cat${id}.jpg")` ? `url("/assets/images/cat${id}.avif")`
: (imageUrl && !imageError ? `url("${imageUrl}")` : 'none'), : (imageUrl && !imageError ? `url("${imageUrl}")` : 'none'),
backgroundSize: 'cover', backgroundSize: 'cover',
backgroundPosition: 'center', backgroundPosition: 'center',

View File

@@ -279,17 +279,25 @@ class Content extends Component {
} }
processData(response) { processData(response) {
const unfilteredProducts = response.products; const rawProducts = response.products;
const currentLanguage = this.props.languageContext?.currentLanguage || this.props.i18n?.language || 'de';
if (!window.individualProductCache) { if (!window.individualProductCache) {
window.individualProductCache = {}; window.individualProductCache = {};
} }
//console.log("processData", unfilteredProducts);
if(unfilteredProducts) unfilteredProducts.forEach(product => { const unfilteredProducts = [];
window.individualProductCache[product.id] = {
data: product, //console.log("processData", rawProducts);
if(rawProducts) rawProducts.forEach(product => {
const effectiveProduct = product.translatedProduct || product;
const cacheKey = `${effectiveProduct.id}_${currentLanguage}`;
window.individualProductCache[cacheKey] = {
data: effectiveProduct,
timestamp: Date.now() timestamp: Date.now()
}; };
unfilteredProducts.push(effectiveProduct);
}); });
this.setState({ this.setState({
@@ -450,7 +458,12 @@ class Content extends Component {
{ query, language: currentLanguage, requestTranslation: currentLanguage === 'de' ? false : true }, { query, language: currentLanguage, requestTranslation: currentLanguage === 'de' ? false : true },
(response) => { (response) => {
if (response && response.products) { if (response && response.products) {
this.processData(response); // Map products to use translatedProduct if available
const enhancedResponse = {
...response,
products: response.products.map(p => p.translatedProduct || p)
};
this.processData(enhancedResponse);
} else { } else {
console.log("fetchSearchData in Content failed", response); console.log("fetchSearchData in Content failed", response);
} }
@@ -681,6 +694,7 @@ class Content extends Component {
onFilterChange={()=>{this.filterProducts()}} onFilterChange={()=>{this.filterProducts()}}
dataType={this.state.dataType} dataType={this.state.dataType}
dataParam={this.state.dataParam} dataParam={this.state.dataParam}
categoryName={this.state.categoryName}
/> />
</Box> </Box>
@@ -722,7 +736,7 @@ class Content extends Component {
justifyContent: 'center' justifyContent: 'center'
}}> }}>
<img <img
src="/assets/images/seeds.jpg" src="/assets/images/seeds.avif"
alt="Seeds" alt="Seeds"
fetchPriority="high" fetchPriority="high"
loading="eager" loading="eager"
@@ -783,7 +797,7 @@ class Content extends Component {
justifyContent: 'center' justifyContent: 'center'
}}> }}>
<img <img
src="/assets/images/cutlings.jpg" src="/assets/images/cutlings.avif"
alt="Stecklinge" alt="Stecklinge"
fetchPriority="high" fetchPriority="high"
loading="eager" loading="eager"

View File

@@ -296,7 +296,7 @@ class Footer extends Component {
> >
<Box <Box
component="img" component="img"
src="/assets/images/gg.png" src="/assets/images/gg.avif"
alt="Google Reviews" alt="Google Reviews"
sx={{ sx={{
height: { xs: 50, md: 60 }, height: { xs: 50, md: 60 },
@@ -326,7 +326,7 @@ class Footer extends Component {
> >
<Box <Box
component="img" component="img"
src="/assets/images/maps.png" src="/assets/images/maps.avif"
alt="Google Maps" alt="Google Maps"
sx={{ sx={{
height: { xs: 40, md: 50 }, height: { xs: 40, md: 50 },
@@ -352,6 +352,9 @@ class Footer extends Component {
<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>
</Typography> </Typography>
<Typography variant="body2" sx={{ fontSize: { xs: '9px', md: '9px' }, lineHeight: 1.5, mt: 1 }}>
<StyledDomainLink href="https://telegraf.growheads.de" target="_blank" rel="noreferrer">Telegraf - sicherer Chat mit unseren Mitarbeitern</StyledDomainLink>
</Typography>
</Box> </Box>
</Stack> </Stack>
</Box> </Box>

View File

@@ -56,7 +56,7 @@ class Images extends Component {
pics.push(window.tinyPicCache[bildId]); pics.push(window.tinyPicCache[bildId]);
this.loadPic(this.props.fullscreenOpen ? 'large' : 'medium',bildId,newMainPic); this.loadPic(this.props.fullscreenOpen ? 'large' : 'medium',bildId,newMainPic);
}else{ }else{
pics.push(`/assets/images/prod${bildId}.jpg`); pics.push(`/assets/images/prod${bildId}.avif`);
this.loadPic(this.props.fullscreenOpen ? 'large' : 'medium',bildId,newMainPic); this.loadPic(this.props.fullscreenOpen ? 'large' : 'medium',bildId,newMainPic);
} }
}else{ }else{
@@ -84,7 +84,7 @@ class Images extends Component {
window.socketManager.emit('getPic', { bildId, size }, (res) => { window.socketManager.emit('getPic', { bildId, size }, (res) => {
if(res.success){ if(res.success){
const url = URL.createObjectURL(new Blob([res.imageBuffer], { type: 'image/jpeg' })); const url = URL.createObjectURL(new Blob([res.imageBuffer], { type: 'image/avif' }));
if(size === 'medium') window.mediumPicCache[bildId] = url; if(size === 'medium') window.mediumPicCache[bildId] = url;
if(size === 'small') window.smallPicCache[bildId] = url; if(size === 'small') window.smallPicCache[bildId] = url;
@@ -118,7 +118,7 @@ class Images extends Component {
if (!this.props.pictureList || !this.props.pictureList.trim()) { if (!this.props.pictureList || !this.props.pictureList.trim()) {
return '/assets/images/nopicture.jpg'; return '/assets/images/nopicture.jpg';
} }
return `/assets/images/prod${this.props.pictureList.split(',')[0].trim()}.jpg`; return `/assets/images/prod${this.props.pictureList.split(',')[0].trim()}.avif`;
}; };
return ( return (

View File

@@ -175,12 +175,12 @@ export class LoginComponent extends Component {
const { location, navigate } = this.props; const { location, navigate } = this.props;
if (!email || !password) { if (!email || !password) {
this.setState({ error: 'Bitte füllen Sie alle Felder aus' }); this.setState({ error: this.props.t ? this.props.t('auth.errors.fillAllFields') : 'Bitte füllen Sie alle Felder aus' });
return; return;
} }
if (!this.validateEmail(email)) { if (!this.validateEmail(email)) {
this.setState({ error: 'Bitte geben Sie eine gültige E-Mail-Adresse ein' }); this.setState({ error: this.props.t ? this.props.t('auth.errors.invalidEmail') : 'Bitte geben Sie eine gültige E-Mail-Adresse ein' });
return; return;
} }
@@ -238,7 +238,7 @@ export class LoginComponent extends Component {
} else { } else {
this.setState({ this.setState({
loading: false, loading: false,
error: response.message || 'Anmeldung fehlgeschlagen' error: response.message || (this.props.t ? this.props.t('auth.errors.loginFailed') : 'Anmeldung fehlgeschlagen')
}); });
} }
}); });
@@ -248,22 +248,22 @@ export class LoginComponent extends Component {
const { email, password, confirmPassword } = this.state; const { email, password, confirmPassword } = this.state;
if (!email || !password || !confirmPassword) { if (!email || !password || !confirmPassword) {
this.setState({ error: 'Bitte füllen Sie alle Felder aus' }); this.setState({ error: this.props.t ? this.props.t('auth.errors.fillAllFields') : 'Bitte füllen Sie alle Felder aus' });
return; return;
} }
if (!this.validateEmail(email)) { if (!this.validateEmail(email)) {
this.setState({ error: 'Bitte geben Sie eine gültige E-Mail-Adresse ein' }); this.setState({ error: this.props.t ? this.props.t('auth.errors.invalidEmail') : 'Bitte geben Sie eine gültige E-Mail-Adresse ein' });
return; return;
} }
if (password !== confirmPassword) { if (password !== confirmPassword) {
this.setState({ error: 'Passwörter stimmen nicht überein' }); this.setState({ error: this.props.t ? this.props.t('auth.errors.passwordsNotMatchShort') : 'Passwörter stimmen nicht überein' });
return; return;
} }
if (password.length < 8) { if (password.length < 8) {
this.setState({ error: 'Das Passwort muss mindestens 8 Zeichen lang sein' }); this.setState({ error: this.props.t ? this.props.t('auth.passwordMinLength') : 'Das Passwort muss mindestens 8 Zeichen lang sein' });
return; return;
} }
@@ -274,14 +274,14 @@ export class LoginComponent extends Component {
if (response.success) { if (response.success) {
this.setState({ this.setState({
loading: false, loading: false,
success: 'Registrierung erfolgreich. Sie können sich jetzt anmelden.', success: this.props.t ? this.props.t('auth.success.registerComplete') : 'Registrierung erfolgreich. Sie können sich jetzt anmelden.',
tabValue: 0 // Switch to login tab tabValue: 0 // Switch to login tab
}); });
} else { } else {
let errorMessage = 'Registrierung fehlgeschlagen'; let errorMessage = this.props.t ? this.props.t('auth.errors.registerFailed') : 'Registrierung fehlgeschlagen';
if (response.cause === 'emailExists') { if (response.cause === 'emailExists') {
errorMessage = 'Ein Benutzer mit dieser E-Mail-Adresse existiert bereits. Bitte verwenden Sie eine andere E-Mail-Adresse oder melden Sie sich an.'; errorMessage = this.props.t ? this.props.t('auth.errors.emailExists') : 'Ein Benutzer mit dieser E-Mail-Adresse existiert bereits. Bitte verwenden Sie eine andere E-Mail-Adresse oder melden Sie sich an.';
} else if (response.message) { } else if (response.message) {
errorMessage = response.message; errorMessage = response.message;
} }
@@ -322,12 +322,12 @@ export class LoginComponent extends Component {
const { email } = this.state; const { email } = this.state;
if (!email) { if (!email) {
this.setState({ error: 'Bitte geben Sie Ihre E-Mail-Adresse ein' }); this.setState({ error: this.props.t ? this.props.t('auth.errors.enterEmail') : 'Bitte geben Sie Ihre E-Mail-Adresse ein' });
return; return;
} }
if (!this.validateEmail(email)) { if (!this.validateEmail(email)) {
this.setState({ error: 'Bitte geben Sie eine gültige E-Mail-Adresse ein' }); this.setState({ error: this.props.t ? this.props.t('auth.errors.invalidEmail') : 'Bitte geben Sie eine gültige E-Mail-Adresse ein' });
return; return;
} }
@@ -342,12 +342,12 @@ export class LoginComponent extends Component {
if (response.success) { if (response.success) {
this.setState({ this.setState({
loading: false, loading: false,
success: 'Ein Link zum Zurücksetzen des Passworts wurde an Ihre E-Mail-Adresse gesendet.' success: this.props.t ? this.props.t('auth.resetPassword.emailSent') : 'Ein Link zum Zurücksetzen des Passworts wurde an Ihre E-Mail-Adresse gesendet.'
}); });
} else { } else {
this.setState({ this.setState({
loading: false, loading: false,
error: response.message || 'Fehler beim Senden der E-Mail' error: response.message || (this.props.t ? this.props.t('auth.resetPassword.emailError') : 'Fehler beim Senden der E-Mail')
}); });
} }
}); });
@@ -408,7 +408,7 @@ export class LoginComponent extends Component {
} else { } else {
this.setState({ this.setState({
loading: false, loading: false,
error: 'Google-Anmeldung fehlgeschlagen', error: this.props.t ? this.props.t('auth.errors.googleLoginFailed') : 'Google-Anmeldung fehlgeschlagen',
showGoogleAuth: false // Reset Google auth state on failed login showGoogleAuth: false // Reset Google auth state on failed login
}); });
} }
@@ -418,7 +418,7 @@ export class LoginComponent extends Component {
handleGoogleLoginError = (error) => { handleGoogleLoginError = (error) => {
console.error('Google Login Error:', error); console.error('Google Login Error:', error);
this.setState({ this.setState({
error: 'Google-Anmeldung fehlgeschlagen', error: this.props.t ? this.props.t('auth.errors.googleLoginFailed') : 'Google-Anmeldung fehlgeschlagen',
showGoogleAuth: false, // Reset Google auth state on error showGoogleAuth: false, // Reset Google auth state on error
loading: false loading: false
}); });

View File

@@ -163,8 +163,8 @@ const MainPageLayout = () => {
const allContentBoxes = { const allContentBoxes = {
home: [ home: [
{ title: t('sections.seeds'), image: "/assets/images/seeds.jpg", bgcolor: "#e1f0d3", link: "/Kategorie/Seeds" }, { title: t('sections.seeds'), image: "/assets/images/seeds.avif", bgcolor: "#e1f0d3", link: "/Kategorie/Seeds" },
{ title: t('sections.stecklinge'), image: "/assets/images/cutlings.jpg", bgcolor: "#e8f5d6", link: "/Kategorie/Stecklinge" } { title: t('sections.stecklinge'), image: "/assets/images/cutlings.avif", bgcolor: "#e8f5d6", link: "/Kategorie/Stecklinge" }
], ],
aktionen: [ aktionen: [
{ title: t('sections.oilPress'), image: "/assets/images/presse.jpg", bgcolor: "#e1f0d3", link: "/presseverleih" }, { title: t('sections.oilPress'), image: "/assets/images/presse.jpg", bgcolor: "#e1f0d3", link: "/presseverleih" },

View File

@@ -10,6 +10,7 @@ import {
} from '@mui/material'; } from '@mui/material';
import Delete from '@mui/icons-material/Delete'; import Delete from '@mui/icons-material/Delete';
import CloudUpload from '@mui/icons-material/CloudUpload'; import CloudUpload from '@mui/icons-material/CloudUpload';
import { withI18n } from '../i18n/withTranslation.js';
class PhotoUpload extends Component { class PhotoUpload extends Component {
constructor(props) { constructor(props) {
@@ -30,7 +31,7 @@ class PhotoUpload extends Component {
// Validate file count // Validate file count
if (this.state.files.length + selectedFiles.length > maxFiles) { if (this.state.files.length + selectedFiles.length > maxFiles) {
this.setState({ this.setState({
error: `Maximal ${maxFiles} Dateien erlaubt` error: this.props.t("productDialogs.photoUploadErrorMaxFiles", { max: maxFiles })
}); });
return; return;
} }
@@ -43,14 +44,14 @@ class PhotoUpload extends Component {
for (const file of selectedFiles) { for (const file of selectedFiles) {
if (!validTypes.includes(file.type)) { if (!validTypes.includes(file.type)) {
this.setState({ this.setState({
error: 'Nur Bilddateien (JPEG, PNG, GIF, WebP) sind erlaubt' error: this.props.t("productDialogs.photoUploadErrorFileType")
}); });
continue; continue;
} }
if (file.size > maxSize) { if (file.size > maxSize) {
this.setState({ this.setState({
error: `Datei zu groß. Maximum: ${Math.round(maxSize / (1024 * 1024))}MB` error: this.props.t("productDialogs.photoUploadErrorFileSize", { maxSize: Math.round(maxSize / (1024 * 1024)) })
}); });
continue; continue;
} }
@@ -167,12 +168,12 @@ class PhotoUpload extends Component {
render() { render() {
const { files, previews, error } = this.state; const { files, previews, error } = this.state;
const { disabled, label } = this.props; const { disabled, label, t } = this.props;
return ( return (
<Box> <Box>
<Typography variant="body2" sx={{ mb: 1, fontWeight: 500 }}> <Typography variant="body2" sx={{ mb: 1, fontWeight: 500 }}>
{label || 'Fotos anhängen (optional)'} {label || t("productDialogs.photoUploadLabelDefault")}
</Typography> </Typography>
<input <input
@@ -192,7 +193,7 @@ class PhotoUpload extends Component {
disabled={disabled} disabled={disabled}
sx={{ mb: 2 }} sx={{ mb: 2 }}
> >
Fotos auswählen {t("productDialogs.photoUploadSelect")}
</Button> </Button>
{error && ( {error && (
@@ -228,7 +229,7 @@ class PhotoUpload extends Component {
size="small" size="small"
onClick={() => this.handleRemoveFile(index)} onClick={() => this.handleRemoveFile(index)}
disabled={disabled} disabled={disabled}
aria-label="Bild entfernen" aria-label={t("productDialogs.photoUploadRemove")}
sx={{ sx={{
position: 'absolute', position: 'absolute',
top: 4, top: 4,
@@ -269,10 +270,10 @@ class PhotoUpload extends Component {
{files.length > 0 && ( {files.length > 0 && (
<Typography variant="caption" color="text.secondary" sx={{ mt: 1, display: 'block' }}> <Typography variant="caption" color="text.secondary" sx={{ mt: 1, display: 'block' }}>
{files.length} Datei(en) ausgewählt {t("productDialogs.photoUploadSelectedFiles", { count: files.length })}
{previews.length > 0 && previews.some(p => p.originalSize && p.compressedSize) && ( {previews.length > 0 && previews.some(p => p.originalSize && p.compressedSize) && (
<span style={{ marginLeft: '8px' }}> <span style={{ marginLeft: '8px' }}>
(komprimiert für Upload) {t("productDialogs.photoUploadCompressed")}
</span> </span>
)} )}
</Typography> </Typography>
@@ -282,4 +283,4 @@ class PhotoUpload extends Component {
} }
} }
export default PhotoUpload; export default withI18n()(PhotoUpload);

View File

@@ -101,7 +101,7 @@ class Product extends Component {
console.log('loadImagevisSocket', bildId); console.log('loadImagevisSocket', bildId);
window.socketManager.emit('getPic', { bildId, size:'small' }, (res) => { window.socketManager.emit('getPic', { bildId, size:'small' }, (res) => {
if(res.success){ if(res.success){
window.smallPicCache[bildId] = URL.createObjectURL(new Blob([res.imageBuffer], { type: 'image/jpeg' })); window.smallPicCache[bildId] = URL.createObjectURL(new Blob([res.imageBuffer], { type: 'image/avif' }));
if (this._isMounted) { if (this._isMounted) {
this.setState({image: window.smallPicCache[bildId], loading: false}); this.setState({image: window.smallPicCache[bildId], loading: false});
} else { } else {

View File

@@ -305,7 +305,7 @@ class ProductDetailPage extends Component {
window.socketManager.emit('getPic', { bildId, size: 'small' }, (res) => { window.socketManager.emit('getPic', { bildId, size: 'small' }, (res) => {
if (res.success) { if (res.success) {
// Cache the image // Cache the image
window.smallPicCache[bildId] = URL.createObjectURL(new Blob([res.imageBuffer], { type: 'image/jpeg' })); window.smallPicCache[bildId] = URL.createObjectURL(new Blob([res.imageBuffer], { type: 'image/avif' }));
// Update state // Update state
this.setState(prevState => ({ this.setState(prevState => ({
@@ -546,7 +546,7 @@ class ProductDetailPage extends Component {
console.log("getAttributePicture", res); console.log("getAttributePicture", res);
if (res.success && !res.noPicture) { if (res.success && !res.noPicture) {
const blob = new Blob([res.imageBuffer], { const blob = new Blob([res.imageBuffer], {
type: "image/jpeg", type: "image/avif",
}); });
const url = URL.createObjectURL(blob); const url = URL.createObjectURL(blob);
@@ -599,13 +599,16 @@ class ProductDetailPage extends Component {
const productData = res.translatedProduct || res.product; const productData = res.translatedProduct || res.product;
productData.seoName = this.props.seoName; productData.seoName = this.props.seoName;
// Use translated attributes if available
const attributesData = res.translatedAttributes || res.attributes;
// Initialize cache if it doesn't exist // Initialize cache if it doesn't exist
if (!window.productDetailCache) { if (!window.productDetailCache) {
window.productDetailCache = {}; window.productDetailCache = {};
} }
// Cache the complete response data (product + attributes) - cache the response with translated product // Cache the complete response data (product + attributes) - cache the response with translated product
const cacheData = { ...res, product: productData }; const cacheData = { ...res, product: productData, attributes: attributesData };
window.productDetailCache[cacheKey] = cacheData; window.productDetailCache[cacheKey] = cacheData;
// Clean up prerender fallback since we now have real data // Clean up prerender fallback since we now have real data
@@ -628,7 +631,7 @@ class ProductDetailPage extends Component {
upgrading: false, // Clear upgrading state since we now have complete data upgrading: false, // Clear upgrading state since we now have complete data
error: null, error: null,
imageDialogOpen: false, imageDialogOpen: false,
attributes: res.attributes, attributes: attributesData,
komponenten: komponenten, komponenten: komponenten,
komponentenLoaded: komponenten.length === 0, // If no komponenten, mark as loaded komponentenLoaded: komponenten.length === 0, // If no komponenten, mark as loaded
similarProducts: res.similarProducts || [] similarProducts: res.similarProducts || []
@@ -653,7 +656,7 @@ class ProductDetailPage extends Component {
console.log("getProductView", res); console.log("getProductView", res);
// Load attribute images // Load attribute images
this.loadAttributeImages(res.attributes); this.loadAttributeImages(attributesData);
} else { } else {
console.error( console.error(
"Error loading product:", "Error loading product:",
@@ -762,7 +765,7 @@ class ProductDetailPage extends Component {
handleEmbedShare = () => { handleEmbedShare = () => {
const embedCode = `<iframe src="${this.getProductUrl()}" width="100%" height="600" frameborder="0"></iframe>`; const embedCode = `<iframe src="${this.getProductUrl()}" width="100%" height="600" frameborder="0"></iframe>`;
navigator.clipboard.writeText(embedCode).then(() => { navigator.clipboard.writeText(embedCode).then(() => {
this.showSnackbar("Einbettungscode wurde in die Zwischenablage kopiert!"); this.showSnackbar(this.props.t ? this.props.t("productDialogs.shareSuccessEmbed") : "Einbettungscode wurde in die Zwischenablage kopiert!");
}).catch(() => { }).catch(() => {
// Fallback for older browsers // Fallback for older browsers
try { try {
@@ -772,9 +775,9 @@ class ProductDetailPage extends Component {
textArea.select(); textArea.select();
document.execCommand('copy'); document.execCommand('copy');
document.body.removeChild(textArea); document.body.removeChild(textArea);
this.showSnackbar("Einbettungscode wurde in die Zwischenablage kopiert!"); this.showSnackbar(this.props.t ? this.props.t("productDialogs.shareSuccessEmbed") : "Einbettungscode wurde in die Zwischenablage kopiert!");
} catch { } catch {
this.showSnackbar("Fehler beim Kopieren des Einbettungscodes", "error"); this.showSnackbar(this.props.t ? this.props.t("productDialogs.shareErrorEmbed") : "Fehler beim Kopieren des Einbettungscodes", "error");
} }
}); });
this.handleShareClose(); this.handleShareClose();
@@ -782,7 +785,10 @@ class ProductDetailPage extends Component {
handleWhatsAppShare = () => { handleWhatsAppShare = () => {
const url = this.getProductUrl(); const url = this.getProductUrl();
const text = `Schau dir dieses Produkt an: ${cleanProductName(this.state.product.name)}`; const productName = cleanProductName(this.state.product.name);
const text = this.props.t
? this.props.t("productDialogs.shareWhatsAppText", { name: productName })
: `Schau dir dieses Produkt an: ${productName}`;
const whatsappUrl = `https://wa.me/?text=${encodeURIComponent(text + ' ' + url)}`; const whatsappUrl = `https://wa.me/?text=${encodeURIComponent(text + ' ' + url)}`;
window.open(whatsappUrl, '_blank'); window.open(whatsappUrl, '_blank');
this.handleShareClose(); this.handleShareClose();
@@ -797,7 +803,10 @@ class ProductDetailPage extends Component {
handleTelegramShare = () => { handleTelegramShare = () => {
const url = this.getProductUrl(); const url = this.getProductUrl();
const text = `Schau dir dieses Produkt an: ${cleanProductName(this.state.product.name)}`; const productName = cleanProductName(this.state.product.name);
const text = this.props.t
? this.props.t("productDialogs.shareTelegramText", { name: productName })
: `Schau dir dieses Produkt an: ${productName}`;
const telegramUrl = `https://t.me/share/url?url=${encodeURIComponent(url)}&text=${encodeURIComponent(text)}`; const telegramUrl = `https://t.me/share/url?url=${encodeURIComponent(url)}&text=${encodeURIComponent(text)}`;
window.open(telegramUrl, '_blank'); window.open(telegramUrl, '_blank');
this.handleShareClose(); this.handleShareClose();
@@ -805,8 +814,18 @@ class ProductDetailPage extends Component {
handleEmailShare = () => { handleEmailShare = () => {
const url = this.getProductUrl(); const url = this.getProductUrl();
const subject = `Produktempfehlung: ${cleanProductName(this.state.product.name)}`; const productName = cleanProductName(this.state.product.name);
const body = `Hallo,\n\nich möchte dir dieses Produkt empfehlen:\n\n${cleanProductName(this.state.product.name)}\n${url}\n\nViele Grüße`; const subject = this.props.t
? `${this.props.t("productDialogs.shareEmailSubject")}: ${productName}`
: `Produktempfehlung: ${productName}`;
const body = this.props.t
? this.props.t("productDialogs.shareEmailBody", {
name: productName,
url: url
})
: `Hallo,\n\nich möchte dir dieses Produkt empfehlen:\n\n${productName}\n${url}\n\nViele Grüße`;
const emailUrl = `mailto:?subject=${encodeURIComponent(subject)}&body=${encodeURIComponent(body)}`; const emailUrl = `mailto:?subject=${encodeURIComponent(subject)}&body=${encodeURIComponent(body)}`;
window.location.href = emailUrl; window.location.href = emailUrl;
this.handleShareClose(); this.handleShareClose();
@@ -815,7 +834,7 @@ class ProductDetailPage extends Component {
handleLinkCopy = () => { handleLinkCopy = () => {
const url = this.getProductUrl(); const url = this.getProductUrl();
navigator.clipboard.writeText(url).then(() => { navigator.clipboard.writeText(url).then(() => {
this.showSnackbar("Link wurde in die Zwischenablage kopiert!"); this.showSnackbar(this.props.t ? this.props.t("productDialogs.shareSuccessLink") : "Link wurde in die Zwischenablage kopiert!");
}).catch(() => { }).catch(() => {
// Fallback for older browsers // Fallback for older browsers
try { try {
@@ -825,7 +844,7 @@ class ProductDetailPage extends Component {
textArea.select(); textArea.select();
document.execCommand('copy'); document.execCommand('copy');
document.body.removeChild(textArea); document.body.removeChild(textArea);
this.showSnackbar("Link wurde in die Zwischenablage kopiert!"); this.showSnackbar(this.props.t ? this.props.t("productDialogs.shareSuccessLink") : "Link wurde in die Zwischenablage kopiert!");
} catch { } catch {
this.showSnackbar("Fehler beim Kopieren des Links", "error"); this.showSnackbar("Fehler beim Kopieren des Links", "error");
} }
@@ -968,7 +987,7 @@ class ProductDetailPage extends Component {
}).format(productData.price)} }).format(productData.price)}
</Typography> </Typography>
<Typography variant="caption" color="text.secondary"> <Typography variant="caption" color="text.secondary">
inkl. MwSt. {this.props.t ? this.props.t('product.inclVatSimple') : 'inkl. MwSt.'}
</Typography> </Typography>
</Box> </Box>
</Box> </Box>
@@ -1047,7 +1066,7 @@ class ProductDetailPage extends Component {
console.log('loadEmbeddedProductImage response:', articleNr, res.success); console.log('loadEmbeddedProductImage response:', articleNr, res.success);
if (res.success) { if (res.success) {
const imageUrl = URL.createObjectURL(new Blob([res.imageBuffer], { type: 'image/jpeg' })); const imageUrl = URL.createObjectURL(new Blob([res.imageBuffer], { type: 'image/avif' }));
this.setState(prevState => { this.setState(prevState => {
console.log('Setting embedded product image for', articleNr); console.log('Setting embedded product image for', articleNr);
return { return {
@@ -1331,7 +1350,7 @@ class ProductDetailPage extends Component {
whiteSpace: "nowrap" whiteSpace: "nowrap"
}} }}
> >
Frage zum Artikel {this.props.t ? this.props.t('productDialogs.questionTitle') : "Frage zum Artikel"}
</Button> </Button>
<Button <Button
variant="outlined" variant="outlined"
@@ -1345,7 +1364,7 @@ class ProductDetailPage extends Component {
whiteSpace: "nowrap" whiteSpace: "nowrap"
}} }}
> >
Artikel Bewerten {this.props.t ? this.props.t('productDialogs.ratingTitle') : "Artikel Bewerten"}
</Button> </Button>
{(product.available !== 1 && product.availableSupplier !== 1) && ( {(product.available !== 1 && product.availableSupplier !== 1) && (
<Button <Button
@@ -1366,7 +1385,7 @@ class ProductDetailPage extends Component {
} }
}} }}
> >
Verfügbarkeit anfragen {this.props.t ? this.props.t('productDialogs.availabilityTitle') : "Verfügbarkeit anfragen"}
</Button> </Button>
)} )}
</Stack> </Stack>
@@ -1595,7 +1614,7 @@ class ProductDetailPage extends Component {
}} }}
size="small" size="small"
> >
Teilen {this.props.t ? this.props.t("productDialogs.shareTitle") : "Teilen"}
</Button> </Button>
<Box <Box
sx={{ sx={{
@@ -1669,7 +1688,7 @@ class ProductDetailPage extends Component {
<ListItemIcon> <ListItemIcon>
<CodeIcon fontSize="small" /> <CodeIcon fontSize="small" />
</ListItemIcon> </ListItemIcon>
<ListItemText primary="Einbetten" /> <ListItemText primary={this.props.t ? this.props.t("productDialogs.shareEmbed") : "Einbetten"} />
</MenuItem> </MenuItem>
<MenuItem onClick={this.handleWhatsAppShare}> <MenuItem onClick={this.handleWhatsAppShare}>
<ListItemIcon> <ListItemIcon>
@@ -1699,7 +1718,7 @@ class ProductDetailPage extends Component {
<ListItemIcon> <ListItemIcon>
<LinkIcon fontSize="small" /> <LinkIcon fontSize="small" />
</ListItemIcon> </ListItemIcon>
<ListItemText primary="Link kopieren" /> <ListItemText primary={this.props.t ? this.props.t("productDialogs.shareCopyLink") : "Link kopieren"} />
</MenuItem> </MenuItem>
</MenuList> </MenuList>
</Box> </Box>
@@ -1953,7 +1972,7 @@ class ProductDetailPage extends Component {
gap: 2 gap: 2
}}> }}>
{this.state.similarProducts.map((similarProductData, index) => { {this.state.similarProducts.map((similarProductData, index) => {
const product = similarProductData.product; const product = similarProductData.translatedProduct || similarProductData.product;
return ( return (
<Box key={product.id} sx={{ display: 'flex', justifyContent: 'center' }}> <Box key={product.id} sx={{ display: 'flex', justifyContent: 'center' }}>
<Product <Product

View File

@@ -209,7 +209,7 @@ class ProductFilters extends Component {
color: 'primary.main' color: 'primary.main'
}} }}
> >
{this.props.dataParam} {this.props.categoryName}
</Typography> </Typography>
)} )}

View File

@@ -1,4 +1,5 @@
import React from 'react'; import React from 'react';
import { Link } from 'react-router-dom';
import Box from "@mui/material/Box"; import Box from "@mui/material/Box";
import Typography from "@mui/material/Typography"; import Typography from "@mui/material/Typography";
import IconButton from "@mui/material/IconButton"; import IconButton from "@mui/material/IconButton";
@@ -27,7 +28,7 @@ class SharedCarousel extends React.Component {
constructor(props) { constructor(props) {
super(props); super(props);
const { i18n } = props; const { i18n } = props;
// Don't load categories in constructor - will be loaded in componentDidMount with correct language // Don't load categories in constructor - will be loaded in componentDidMount with correct language
this.state = { this.state = {
categories: [], categories: [],
@@ -41,7 +42,7 @@ class SharedCarousel extends React.Component {
componentDidMount() { componentDidMount() {
this._isMounted = true; this._isMounted = true;
const currentLanguage = this.props.languageContext?.currentLanguage || this.props.i18n.language; const currentLanguage = this.props.languageContext?.currentLanguage || this.props.i18n.language;
// ALWAYS reload categories to ensure correct language // ALWAYS reload categories to ensure correct language
console.log("SharedCarousel componentDidMount: ALWAYS RELOADING categories for language", currentLanguage); console.log("SharedCarousel componentDidMount: ALWAYS RELOADING categories for language", currentLanguage);
window.categoryService.get(209, currentLanguage).then((response) => { window.categoryService.get(209, currentLanguage).then((response) => {
@@ -60,12 +61,12 @@ class SharedCarousel extends React.Component {
componentDidUpdate(prevProps) { componentDidUpdate(prevProps) {
console.log("componentDidUpdate", prevProps.languageContext?.currentLanguage, this.props.languageContext?.currentLanguage); console.log("componentDidUpdate", prevProps.languageContext?.currentLanguage, this.props.languageContext?.currentLanguage);
if(prevProps.languageContext?.currentLanguage !== this.props.languageContext?.currentLanguage) { if (prevProps.languageContext?.currentLanguage !== this.props.languageContext?.currentLanguage) {
this.setState({ categories: [] },() => { this.setState({ categories: [] }, () => {
window.categoryService.get(209,this.props.languageContext?.currentLanguage || this.props.i18n.language).then((response) => { window.categoryService.get(209, this.props.languageContext?.currentLanguage || this.props.i18n.language).then((response) => {
console.log("response", response); console.log("response", response);
if (response.children && response.children.length > 0) { if (response.children && response.children.length > 0) {
this.originalCategories = response.children; this.originalCategories = response.children;
this.categories = [...response.children, ...response.children]; this.categories = [...response.children, ...response.children];
this.setState({ categories: this.categories }); this.setState({ categories: this.categories });
this.startAutoScroll(); this.startAutoScroll();
@@ -123,7 +124,7 @@ class SharedCarousel extends React.Component {
showScrollbarFlash = () => { showScrollbarFlash = () => {
this.clearScrollbarTimer(); this.clearScrollbarTimer();
this.setState({ showScrollbar: true }); this.setState({ showScrollbar: true });
this.scrollbarTimer = setTimeout(() => { this.scrollbarTimer = setTimeout(() => {
if (this._isMounted) { if (this._isMounted) {
this.setState({ showScrollbar: false }); this.setState({ showScrollbar: false });
@@ -133,7 +134,7 @@ class SharedCarousel extends React.Component {
handleAutoScroll = () => { handleAutoScroll = () => {
if (!this.autoScrollActive || this.originalCategories.length === 0) return; if (!this.autoScrollActive || this.originalCategories.length === 0) return;
this.translateX -= AUTO_SCROLL_SPEED; this.translateX -= AUTO_SCROLL_SPEED;
this.updateTrackTransform(); this.updateTrackTransform();
@@ -172,7 +173,7 @@ class SharedCarousel extends React.Component {
scrollBy = (direction) => { scrollBy = (direction) => {
if (this.originalCategories.length === 0) return; if (this.originalCategories.length === 0) return;
// direction: 1 = left (scroll content right), -1 = right (scroll content left) // direction: 1 = left (scroll content right), -1 = right (scroll content left)
const originalItemCount = this.originalCategories.length; const originalItemCount = this.originalCategories.length;
const maxScroll = ITEM_WIDTH * originalItemCount; const maxScroll = ITEM_WIDTH * originalItemCount;
@@ -189,7 +190,7 @@ class SharedCarousel extends React.Component {
} }
this.updateTrackTransform(); this.updateTrackTransform();
// Force scrollbar to update immediately after wrap-around // Force scrollbar to update immediately after wrap-around
if (this.state.showScrollbar) { if (this.state.showScrollbar) {
this.forceUpdate(); this.forceUpdate();
@@ -204,11 +205,11 @@ class SharedCarousel extends React.Component {
const originalItemCount = this.originalCategories.length; const originalItemCount = this.originalCategories.length;
const viewportWidth = 1080; // carousel container max-width const viewportWidth = 1080; // carousel container max-width
const itemsInView = Math.floor(viewportWidth / ITEM_WIDTH); const itemsInView = Math.floor(viewportWidth / ITEM_WIDTH);
// Calculate which item is currently at the left edge (first visible) // Calculate which item is currently at the left edge (first visible)
// Map translateX directly to item index using the same logic as scrollBy // Map translateX directly to item index using the same logic as scrollBy
let currentItemIndex; let currentItemIndex;
if (this.translateX === 0) { if (this.translateX === 0) {
// At the beginning - item 0 is visible // At the beginning - item 0 is visible
currentItemIndex = 0; currentItemIndex = 0;
@@ -221,10 +222,10 @@ class SharedCarousel extends React.Component {
// Normal negative scrolling - calculate which item is at left edge // Normal negative scrolling - calculate which item is at left edge
currentItemIndex = Math.floor(Math.abs(this.translateX) / ITEM_WIDTH); currentItemIndex = Math.floor(Math.abs(this.translateX) / ITEM_WIDTH);
} }
// Ensure we stay within bounds // Ensure we stay within bounds
currentItemIndex = Math.max(0, Math.min(currentItemIndex, originalItemCount - 1)); currentItemIndex = Math.max(0, Math.min(currentItemIndex, originalItemCount - 1));
// Calculate scrollbar position: 0% when item 0 is first visible, 100% when last item is first visible // Calculate scrollbar position: 0% when item 0 is first visible, 100% when last item is first visible
const lastPossibleFirstItem = Math.max(0, originalItemCount - itemsInView); const lastPossibleFirstItem = Math.max(0, originalItemCount - itemsInView);
const thumbPosition = lastPossibleFirstItem > 0 ? Math.min((currentItemIndex / lastPossibleFirstItem) * 100, 100) : 0; const thumbPosition = lastPossibleFirstItem > 0 ? Math.min((currentItemIndex / lastPossibleFirstItem) * 100, 100) : 0;
@@ -268,25 +269,41 @@ class SharedCarousel extends React.Component {
const { t } = this.props; const { t } = this.props;
const { categories } = this.state; const { categories } = this.state;
if(!categories || categories.length === 0) { if (!categories || categories.length === 0) {
return null; return null;
} }
return ( return (
<Box sx={{ mt: 3 }}> <Box sx={{ mt: 3 }}>
<Typography <Box
variant="h4" component={Link}
component="h1" to="/Kategorien"
sx={{ sx={{
mb: 2, display: "flex",
fontFamily: "SwashingtonCP", alignItems: "center",
justifyContent: "center",
textDecoration: "none",
color: "primary.main", color: "primary.main",
textAlign: "center", mb: 2,
textShadow: "3px 3px 10px rgba(0, 0, 0, 0.4)" transition: "all 0.3s ease",
"&:hover": {
transform: "translateX(5px)",
color: "primary.dark"
}
}} }}
> >
{t('navigation.categories')} <Typography
</Typography> variant="h4"
component="span"
sx={{
fontFamily: "SwashingtonCP",
textShadow: "3px 3px 10px rgba(0, 0, 0, 0.4)"
}}
>
{t('navigation.categories')}
</Typography>
<ChevronRight sx={{ fontSize: "2.5rem", ml: 1 }} />
</Box>
<div <div
className="carousel-wrapper" className="carousel-wrapper"
@@ -394,7 +411,7 @@ class SharedCarousel extends React.Component {
</div> </div>
))} ))}
</div> </div>
{/* Virtual Scrollbar */} {/* Virtual Scrollbar */}
{this.renderVirtualScrollbar()} {this.renderVirtualScrollbar()}
</div> </div>

View File

@@ -63,7 +63,7 @@ class ExtrasSelector extends Component {
this.loadingImages.add(bildId); this.loadingImages.add(bildId);
window.socketManager.emit('getPic', { bildId, size:'small' }, (res) => { window.socketManager.emit('getPic', { bildId, size:'small' }, (res) => {
if (res.success) { if (res.success) {
window.smallPicCache[bildId] = URL.createObjectURL(new Blob([res.imageBuffer], { type: 'image/jpeg' })); window.smallPicCache[bildId] = URL.createObjectURL(new Blob([res.imageBuffer], { type: 'image/avif' }));
this.forceUpdate(); this.forceUpdate();
} }
this.loadingImages.delete(bildId); this.loadingImages.delete(bildId);

View File

@@ -5,6 +5,7 @@ import CardContent from '@mui/material/CardContent';
import Typography from '@mui/material/Typography'; import Typography from '@mui/material/Typography';
import Box from '@mui/material/Box'; import Box from '@mui/material/Box';
import Chip from '@mui/material/Chip'; import Chip from '@mui/material/Chip';
import { withI18n } from '../../i18n/withTranslation.js';
class TentShapeSelector extends Component { class TentShapeSelector extends Component {
// Generate plant layout based on tent shape // Generate plant layout based on tent shape
@@ -180,12 +181,20 @@ class TentShapeSelector extends Component {
</Box> </Box>
<Typography variant="body2" color="text.secondary" gutterBottom> <Typography variant="body2" color="text.secondary" gutterBottom>
{shape.description} {this.props.t && shape.descriptionKey ? this.props.t(shape.descriptionKey) : shape.description}
</Typography> </Typography>
<Box sx={{ mt: 2 }}> <Box sx={{ mt: 2 }}>
<Chip <Chip
label={`${shape.minPlants}-${shape.maxPlants} Pflanzen`} label={this.props.t
? (
shape.minPlants === 1 && shape.maxPlants === 2 ? this.props.t("kitConfig.plants1to2") :
shape.minPlants === 2 && shape.maxPlants === 4 ? this.props.t("kitConfig.plants2to4") :
shape.minPlants === 4 && shape.maxPlants === 6 ? this.props.t("kitConfig.plants4to6") :
shape.minPlants === 3 && shape.maxPlants === 6 ? this.props.t("kitConfig.plants3to6") :
`${shape.minPlants}-${shape.maxPlants} Pflanzen`
)
: `${shape.minPlants}-${shape.maxPlants} Pflanzen`}
size="small" size="small"
sx={{ sx={{
bgcolor: isSelected ? '#2e7d32' : '#f0f0f0', bgcolor: isSelected ? '#2e7d32' : '#f0f0f0',
@@ -205,7 +214,7 @@ class TentShapeSelector extends Component {
transition: 'opacity 0.3s ease' transition: 'opacity 0.3s ease'
}} }}
> >
Ausgewählt {this.props.t ? this.props.t("kitConfig.selected") : "✓ Ausgewählt"}
</Typography> </Typography>
</Box> </Box>
</CardContent> </CardContent>
@@ -238,4 +247,4 @@ class TentShapeSelector extends Component {
} }
} }
export default TentShapeSelector; export default withI18n()(TentShapeSelector);

View File

@@ -32,9 +32,9 @@ class CategoryList extends Component {
console.log(" i18n.language:", this.props.i18n?.language); console.log(" i18n.language:", this.props.i18n?.language);
console.log(" sessionStorage i18nextLng:", typeof sessionStorage !== 'undefined' ? sessionStorage.getItem('i18nextLng') : 'N/A'); console.log(" sessionStorage i18nextLng:", typeof sessionStorage !== 'undefined' ? sessionStorage.getItem('i18nextLng') : 'N/A');
console.log(" localStorage i18nextLng:", typeof localStorage !== 'undefined' ? localStorage.getItem('i18nextLng') : 'N/A'); console.log(" localStorage i18nextLng:", typeof localStorage !== 'undefined' ? localStorage.getItem('i18nextLng') : 'N/A');
const currentLanguage = this.props.languageContext?.currentLanguage || this.props.i18n.language; const currentLanguage = this.props.languageContext?.currentLanguage || this.props.i18n.language;
// ALWAYS reload categories to ensure correct language // ALWAYS reload categories to ensure correct language
console.log("CategoryList componentDidMount: ALWAYS RELOADING categories for language", currentLanguage); console.log("CategoryList componentDidMount: ALWAYS RELOADING categories for language", currentLanguage);
this.setState({ categories: [] }); // Clear any cached categories this.setState({ categories: [] }); // Clear any cached categories
@@ -53,15 +53,15 @@ class CategoryList extends Component {
componentDidUpdate(prevProps) { componentDidUpdate(prevProps) {
console.log("componentDidUpdate", prevProps.languageContext?.currentLanguage, this.props.languageContext?.currentLanguage); console.log("componentDidUpdate", prevProps.languageContext?.currentLanguage, this.props.languageContext?.currentLanguage);
if(prevProps.languageContext?.currentLanguage !== this.props.languageContext?.currentLanguage) { if (prevProps.languageContext?.currentLanguage !== this.props.languageContext?.currentLanguage) {
this.setState({ this.setState({
categories: [], categories: [],
activeCategoryId: null activeCategoryId: null
},() => { }, () => {
window.categoryService.get(209,this.props.languageContext?.currentLanguage || this.props.i18n.language).then((response) => { window.categoryService.get(209, this.props.languageContext?.currentLanguage || this.props.i18n.language).then((response) => {
console.log("response", response); console.log("response", response);
if (response.children && response.children.length > 0) { if (response.children && response.children.length > 0) {
this.setState({ this.setState({
categories: response.children, categories: response.children,
activeCategoryId: this.setLevel1CategoryId(this.props.activeCategoryId) activeCategoryId: this.setLevel1CategoryId(this.props.activeCategoryId)
}); });
@@ -69,14 +69,14 @@ class CategoryList extends Component {
}); });
}); });
} }
if (prevProps.activeCategoryId !== this.props.activeCategoryId) { if (prevProps.activeCategoryId !== this.props.activeCategoryId) {
this.setLevel1CategoryId(this.props.activeCategoryId); this.setLevel1CategoryId(this.props.activeCategoryId);
} }
} }
setLevel1CategoryId = (input) => { setLevel1CategoryId = (input) => {
if(input) { if (input) {
const language = this.props.languageContext?.currentLanguage || this.props.i18n.language; const language = this.props.languageContext?.currentLanguage || this.props.i18n.language;
const categoryTreeCache = window.categoryService.getSync(209, language); const categoryTreeCache = window.categoryService.getSync(209, language);
@@ -136,7 +136,7 @@ class CategoryList extends Component {
this.setState({ activeCategoryId: null }); this.setState({ activeCategoryId: null });
} }
handleMobileMenuToggle = () => { handleMobileMenuToggle = () => {
this.setState(prevState => ({ this.setState(prevState => ({
@@ -173,141 +173,141 @@ class CategoryList extends Component {
py: 0.5, // Add vertical padding to prevent border clipping py: 0.5, // Add vertical padding to prevent border clipping
}} }}
> >
<Button <Button
component={Link} component={Link}
to="/" to="/"
color="inherit" color="inherit"
size="small" size="small"
aria-label="Zur Startseite" aria-label="Zur Startseite"
onClick={isMobile ? this.handleMobileCategoryClick : undefined} onClick={isMobile ? this.handleMobileCategoryClick : undefined}
sx={{ sx={{
fontSize: "0.75rem", fontSize: "0.75rem",
textTransform: "none", textTransform: "none",
whiteSpace: "nowrap", whiteSpace: "nowrap",
opacity: 0.9, opacity: 0.9,
mx: isMobile ? 0 : 0.5, mx: isMobile ? 0 : 0.5,
my: 0.25, my: 0.25,
minWidth: isMobile ? "100%" : "auto", minWidth: isMobile ? "100%" : "auto",
borderRadius: 1, borderRadius: 1,
justifyContent: isMobile ? "flex-start" : "center", justifyContent: isMobile ? "flex-start" : "center",
transition: "all 0.2s ease", transition: "all 0.2s ease",
textShadow: "0 1px 2px rgba(0,0,0,0.3)", textShadow: "0 1px 2px rgba(0,0,0,0.3)",
position: "relative", position: "relative",
...(activeCategoryId === null && { ...(activeCategoryId === null && {
bgcolor: "#fff", bgcolor: "#fff",
textShadow: "none", textShadow: "none",
opacity: 1, opacity: 1,
}), }),
"&:hover": { "&:hover": {
opacity: 1, opacity: 1,
bgcolor: "#fff", bgcolor: "#fff",
textShadow: "none", textShadow: "none",
"& .MuiSvgIcon-root": { "& .MuiSvgIcon-root": {
color: "#2e7d32 !important", color: "#2e7d32 !important",
},
"& .bold-text": {
color: "#2e7d32 !important",
},
"& .thin-text": {
color: "transparent !important",
},
}, },
}} "& .bold-text": {
> color: "#2e7d32 !important",
<HomeIcon sx={{ },
fontSize: "1rem", "& .thin-text": {
mr: isMobile ? 1 : 0, color: "transparent !important",
color: activeCategoryId === null ? "#2e7d32" : "inherit" },
}} /> },
{isMobile && ( }}
<Box sx={{ position: "relative", display: "inline-block" }}> >
{/* Bold text (always rendered to set width) */} <HomeIcon sx={{
<Box fontSize: "1rem",
className="bold-text" mr: isMobile ? 1 : 0,
sx={{ color: activeCategoryId === null ? "#2e7d32" : "inherit"
fontWeight: "bold", }} />
color: activeCategoryId === null ? "#2e7d32" : "transparent", {isMobile && (
position: "relative", <Box sx={{ position: "relative", display: "inline-block" }}>
zIndex: 2, {/* Bold text (always rendered to set width) */}
}} <Box
> className="bold-text"
{this.props.t ? this.props.t('navigation.home') : 'Startseite'} sx={{
</Box> fontWeight: "bold",
{/* Thin text (positioned on top) */} color: activeCategoryId === null ? "#2e7d32" : "transparent",
<Box position: "relative",
className="thin-text" zIndex: 2,
sx={{ }}
fontWeight: "400", >
color: activeCategoryId === null ? "transparent" : "inherit", {this.props.t ? this.props.t('navigation.home') : 'Startseite'}
position: "absolute",
top: 0,
left: 0,
zIndex: 1,
}}
>
{this.props.t ? this.props.t('navigation.home') : 'Startseite'}
</Box>
</Box> </Box>
)} {/* Thin text (positioned on top) */}
</Button> <Box
className="thin-text"
sx={{
fontWeight: "400",
color: activeCategoryId === null ? "transparent" : "inherit",
position: "absolute",
top: 0,
left: 0,
zIndex: 1,
}}
>
{this.props.t ? this.props.t('navigation.home') : 'Startseite'}
</Box>
</Box>
)}
</Button>
<Button <Button
component={Link} component={Link}
to="/Kategorie/neu" to="/Kategorie/neu"
color="inherit" color="inherit"
size="small" size="small"
aria-label="Neuheiten" aria-label="Neuheiten"
onClick={isMobile ? this.handleMobileCategoryClick : undefined} onClick={isMobile ? this.handleMobileCategoryClick : undefined}
sx={{ sx={{
fontSize: "0.75rem", fontSize: "0.75rem",
textTransform: "none", textTransform: "none",
whiteSpace: "nowrap", whiteSpace: "nowrap",
opacity: 0.9, opacity: 0.9,
mx: isMobile ? 0 : 0.5, mx: isMobile ? 0 : 0.5,
my: 0.25, my: 0.25,
minWidth: isMobile ? "100%" : "auto", minWidth: isMobile ? "100%" : "auto",
borderRadius: 1, borderRadius: 1,
justifyContent: isMobile ? "flex-start" : "center", justifyContent: isMobile ? "flex-start" : "center",
transition: "all 0.2s ease", transition: "all 0.2s ease",
textShadow: "0 1px 2px rgba(0,0,0,0.3)", textShadow: "0 1px 2px rgba(0,0,0,0.3)",
position: "relative" position: "relative"
}} }}
> >
<FiberNewIcon sx={{ <FiberNewIcon sx={{
fontSize: "1rem", fontSize: "1rem",
mr: isMobile ? 1 : 0 mr: isMobile ? 1 : 0
}} /> }} />
{isMobile && ( {isMobile && (
<Box sx={{ position: "relative", display: "inline-block" }}> <Box sx={{ position: "relative", display: "inline-block" }}>
{/* Bold text (always rendered to set width) */} {/* Bold text (always rendered to set width) */}
<Box <Box
className="bold-text" className="bold-text"
sx={{ sx={{
fontWeight: "bold", fontWeight: "bold",
color: "transparent", color: "transparent",
position: "relative", position: "relative",
zIndex: 2, zIndex: 2,
}} }}
> >
{this.props.t ? this.props.t('navigation.new') : 'Neuheiten'} {this.props.t ? this.props.t('navigation.new') : 'Neuheiten'}
</Box>
{/* Thin text (positioned on top) */}
<Box
className="thin-text"
sx={{
fontWeight: "400",
color: "inherit",
position: "absolute",
top: 0,
left: 0,
zIndex: 1,
}}
>
{this.props.t ? this.props.t('navigation.new') : 'Neuheiten'}
</Box>
</Box> </Box>
)} {/* Thin text (positioned on top) */}
</Button> <Box
className="thin-text"
sx={{
fontWeight: "400",
color: "inherit",
position: "absolute",
top: 0,
left: 0,
zIndex: 1,
}}
>
{this.props.t ? this.props.t('navigation.new') : 'Neuheiten'}
</Box>
</Box>
)}
</Button>
{categories.length > 0 ? ( {categories.length > 0 ? (
@@ -385,100 +385,100 @@ class CategoryList extends Component {
); );
})} })}
</> </>
) : ( !isMobile && ( ) : (!isMobile && (
<Typography <Typography
variant="caption" variant="caption"
color="inherit"
sx={{
display: "inline-flex",
alignItems: "center",
height: "33px", // Match small button height
px: 1,
fontSize: "0.75rem",
opacity: 0.9,
}}
>
&nbsp;
</Typography>
)
)}
<Button
component={Link}
to="/Konfigurator"
color="inherit" color="inherit"
size="small"
aria-label="Zur Startseite"
onClick={isMobile ? this.handleMobileCategoryClick : undefined}
sx={{ sx={{
display: "inline-flex",
alignItems: "center",
height: "33px", // Match small button height
px: 1,
fontSize: "0.75rem", fontSize: "0.75rem",
textTransform: "none",
whiteSpace: "nowrap",
opacity: 0.9, opacity: 0.9,
mx: isMobile ? 0 : 0.5,
my: 0.25,
minWidth: isMobile ? "100%" : "auto",
borderRadius: 1,
justifyContent: isMobile ? "flex-start" : "center",
transition: "all 0.2s ease",
textShadow: "0 1px 2px rgba(0,0,0,0.3)",
position: "relative",
...(activeCategoryId === null && {
bgcolor: "#fff",
textShadow: "none",
opacity: 1,
}),
"&:hover": {
opacity: 1,
bgcolor: "#fff",
textShadow: "none",
"& .MuiSvgIcon-root": {
color: "#2e7d32 !important",
},
"& .bold-text": {
color: "#2e7d32 !important",
},
"& .thin-text": {
color: "transparent !important",
},
},
}} }}
> >
<SettingsIcon sx={{ &nbsp;
fontSize: "1rem", </Typography>
mr: isMobile ? 1 : 0, )
color: activeCategoryId === null ? "#2e7d32" : "inherit" )}
}} /> <Button
{isMobile && ( component={Link}
<Box sx={{ position: "relative", display: "inline-block" }}> to="/Konfigurator"
{/* Bold text (always rendered to set width) */} color="inherit"
<Box size="small"
className="bold-text" aria-label="Zur Startseite"
sx={{ onClick={isMobile ? this.handleMobileCategoryClick : undefined}
fontWeight: "bold", sx={{
color: activeCategoryId === null ? "#2e7d32" : "transparent", fontSize: "0.75rem",
position: "relative", textTransform: "none",
zIndex: 2, whiteSpace: "nowrap",
}} opacity: 0.9,
> mx: isMobile ? 0 : 0.5,
{this.props.t ? this.props.t('navigation.home') : 'Startseite'} my: 0.25,
</Box> minWidth: isMobile ? "100%" : "auto",
{/* Thin text (positioned on top) */} borderRadius: 1,
<Box justifyContent: isMobile ? "flex-start" : "center",
className="thin-text" transition: "all 0.2s ease",
sx={{ textShadow: "0 1px 2px rgba(0,0,0,0.3)",
fontWeight: "400", position: "relative",
color: activeCategoryId === null ? "transparent" : "inherit", ...(activeCategoryId === null && {
position: "absolute", bgcolor: "#fff",
top: 0, textShadow: "none",
left: 0, opacity: 1,
zIndex: 1, }),
}} "&:hover": {
> opacity: 1,
{this.props.t ? this.props.t('navigation.home') : 'Startseite'} bgcolor: "#fff",
</Box> textShadow: "none",
"& .MuiSvgIcon-root": {
color: "#2e7d32 !important",
},
"& .bold-text": {
color: "#2e7d32 !important",
},
"& .thin-text": {
color: "transparent !important",
},
},
}}
>
<SettingsIcon sx={{
fontSize: "1rem",
mr: isMobile ? 1 : 0,
color: activeCategoryId === null ? "#2e7d32" : "inherit"
}} />
{isMobile && (
<Box sx={{ position: "relative", display: "inline-block" }}>
{/* Bold text (always rendered to set width) */}
<Box
className="bold-text"
sx={{
fontWeight: "bold",
color: activeCategoryId === null ? "#2e7d32" : "transparent",
position: "relative",
zIndex: 2,
}}
>
{this.props.t ? this.props.t('navigation.home') : 'Startseite'}
</Box> </Box>
)} {/* Thin text (positioned on top) */}
</Button> <Box
className="thin-text"
sx={{
fontWeight: "400",
color: activeCategoryId === null ? "transparent" : "inherit",
position: "absolute",
top: 0,
left: 0,
zIndex: 1,
}}
>
{this.props.t ? this.props.t('navigation.home') : 'Startseite'}
</Box>
</Box>
)}
</Button>
</Box> </Box>
); );
@@ -516,11 +516,11 @@ class CategoryList extends Component {
> >
<Container maxWidth="lg" sx={{ px: 2 }}> <Container maxWidth="lg" sx={{ px: 2 }}>
{/* Toggle Button */} {/* Toggle Button */}
<Box <Box
sx={{ sx={{
display: "flex", display: "flex",
justifyContent: "space-between", justifyContent: "space-between",
alignItems: "center", alignItems: "center",
py: 1, py: 1,
cursor: "pointer", cursor: "pointer",
"&:hover": { "&:hover": {
@@ -530,7 +530,7 @@ class CategoryList extends Component {
onClick={this.handleMobileMenuToggle} onClick={this.handleMobileMenuToggle}
role="button" role="button"
tabIndex={0} tabIndex={0}
aria-label={this.props.t ? aria-label={this.props.t ?
(mobileMenuOpen ? this.props.t('navigation.categoriesClose') : this.props.t('navigation.categoriesOpen')) : (mobileMenuOpen ? this.props.t('navigation.categoriesClose') : this.props.t('navigation.categoriesOpen')) :
(mobileMenuOpen ? "Kategorien schließen" : "Kategorien öffnen") (mobileMenuOpen ? "Kategorien schließen" : "Kategorien öffnen")
} }
@@ -541,11 +541,11 @@ class CategoryList extends Component {
} }
}} }}
> >
<Typography variant="subtitle2" color="inherit" sx={{ <Typography variant="subtitle2" color="inherit" sx={{
fontWeight: "bold", fontWeight: "bold",
textShadow: "0 1px 2px rgba(0,0,0,0.3)" textShadow: "0 1px 2px rgba(0,0,0,0.3)"
}}> }}>
{this.props.t ? this.props.t('navigation.categories') : '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 />}

View File

@@ -16,7 +16,7 @@ const Logo = () => {
}} }}
> >
<img <img
src="/assets/images/sh.png" src="/assets/images/sh.avif"
alt="SH Logo" alt="SH Logo"
width="108px" width="108px"
height="45px" height="45px"

View File

@@ -79,7 +79,7 @@ const SearchBar = () => {
(response) => { (response) => {
if (response && response.products) { if (response && response.products) {
// getSearchProducts returns response.products array // getSearchProducts returns response.products array
const suggestions = response.products.slice(0, 8); // Limit to 8 suggestions const suggestions = response.products.map(p => p.translatedProduct || p).slice(0, 8); // Limit to 8 suggestions
setSuggestions(suggestions); setSuggestions(suggestions);
setShowSuggestions(suggestions.length > 0); setShowSuggestions(suggestions.length > 0);
setSelectedIndex(-1); // Reset selection setSelectedIndex(-1); // Reset selection

View File

@@ -31,6 +31,7 @@ const getStatusTranslation = (status, t) => {
new: t ? t('orders.status.new') : "in Bearbeitung", new: t ? t('orders.status.new') : "in Bearbeitung",
pending: t ? t('orders.status.pending') : "Neu", pending: t ? t('orders.status.pending') : "Neu",
processing: t ? t('orders.status.processing') : "in Bearbeitung", processing: t ? t('orders.status.processing') : "in Bearbeitung",
paid: t ? t('orders.status.paid') : "Bezahlt",
cancelled: t ? t('orders.status.cancelled') : "Storniert", cancelled: t ? t('orders.status.cancelled') : "Storniert",
shipped: t ? t('orders.status.shipped') : "Verschickt", shipped: t ? t('orders.status.shipped') : "Verschickt",
delivered: t ? t('orders.status.delivered') : "Geliefert", delivered: t ? t('orders.status.delivered') : "Geliefert",
@@ -39,29 +40,23 @@ const getStatusTranslation = (status, t) => {
}; };
const statusEmojis = { const statusEmojis = {
"in Bearbeitung": "⚙️", new: "⚙️",
pending: "⏳", pending: "⏳",
processing: "🔄", processing: "🔄",
paid: "🏦",
cancelled: "❌", cancelled: "❌",
Verschickt: "🚚", shipped: "🚚",
Geliefert: "✅", delivered: "✅",
Storniert: "❌",
Retoure: "↩️",
"Teil Retoure": "↪️",
"Teil geliefert": "⚡",
}; };
const statusColors = { const statusColors = {
"in Bearbeitung": "#ed6c02", // orange new: "#ed6c02", // orange
pending: "#ff9800", // orange for pending pending: "#ff9800", // orange for pending
processing: "#2196f3", // blue for processing processing: "#2196f3", // blue for processing
paid: "#2e7d32", // green
cancelled: "#d32f2f", // red for cancelled cancelled: "#d32f2f", // red for cancelled
Verschickt: "#2e7d32", // green shipped: "#2e7d32", // green
Geliefert: "#2e7d32", // green delivered: "#2e7d32", // green
Storniert: "#d32f2f", // red
Retoure: "#9c27b0", // purple
"Teil Retoure": "#9c27b0", // purple
"Teil geliefert": "#009688", // teal
}; };
const currencyFormatter = new Intl.NumberFormat("de-DE", { const currencyFormatter = new Intl.NumberFormat("de-DE", {
@@ -229,11 +224,11 @@ const OrdersTab = ({ orderIdFromHash, t }) => {
display: "flex", display: "flex",
alignItems: "center", alignItems: "center",
gap: "8px", gap: "8px",
color: getStatusColor(displayStatus), color: getStatusColor(order.status),
}} }}
> >
<span style={{ fontSize: "1.2rem" }}> <span style={{ fontSize: "1.2rem" }}>
{getStatusEmoji(displayStatus)} {getStatusEmoji(order.status)}
</span> </span>
<Typography <Typography
variant="body2" variant="body2"
@@ -243,6 +238,18 @@ const OrdersTab = ({ orderIdFromHash, t }) => {
{displayStatus} {displayStatus}
</Typography> </Typography>
</Box> </Box>
{order.delivery_method === 'DHL' && order.trackingCode && (
<Box sx={{ mt: 0.5 }}>
<a
href={`https://www.dhl.de/de/privatkunden/dhl-sendungsverfolgung.html?piececode=${order.trackingCode}`}
target="_blank"
rel="noopener noreferrer"
style={{ fontSize: '0.85rem', color: '#d40511' }}
>
📦 {t ? t('orders.trackShipment') : 'Sendung verfolgen'}
</a>
</Box>
)}
</TableCell> </TableCell>
<TableCell> <TableCell>
{order.items {order.items

View File

@@ -206,7 +206,7 @@ const config = {
// Images // Images
images: { images: {
logo: "/assets/images/sh.png", logo: "/assets/images/sh.avif",
placeholder: "/assets/images/nopicture.jpg" placeholder: "/assets/images/nopicture.jpg"
}, },

View File

@@ -1,9 +1,10 @@
// @note Dummy data for grow tent configurator - no backend calls // @note Dummy data for grow tent configurator - no backend calls
// descriptions now keys for translation
export const tentShapes = [ export const tentShapes = [
{ {
id: '60x60', id: '60x60',
name: '60x60cm', name: '60x60cm',
description: 'Kompakt - ideal für kleine Räume', descriptionKey: 'kitConfig.description60x60',
footprint: '60x60', footprint: '60x60',
minPlants: 1, minPlants: 1,
maxPlants: 2, maxPlants: 2,
@@ -13,7 +14,7 @@ export const tentShapes = [
{ {
id: '80x80', id: '80x80',
name: '80x80cm', name: '80x80cm',
description: 'Mittel - perfekte Balance', descriptionKey: 'kitConfig.description80x80',
footprint: '80x80', footprint: '80x80',
minPlants: 2, minPlants: 2,
maxPlants: 4, maxPlants: 4,
@@ -23,7 +24,7 @@ export const tentShapes = [
{ {
id: '100x100', id: '100x100',
name: '100x100cm', name: '100x100cm',
description: 'Groß - für erfahrene Grower', descriptionKey: 'kitConfig.description100x100',
footprint: '100x100', footprint: '100x100',
minPlants: 4, minPlants: 4,
maxPlants: 6, maxPlants: 6,
@@ -33,7 +34,7 @@ export const tentShapes = [
{ {
id: '120x60', id: '120x60',
name: '120x60cm', name: '120x60cm',
description: 'Rechteckig - maximale Raumnutzung', descriptionKey: 'kitConfig.description120x60',
footprint: '120x60', footprint: '120x60',
minPlants: 3, minPlants: 3,
maxPlants: 6, maxPlants: 6,

View File

@@ -5,6 +5,7 @@ export default {
"profile": "الملف الشخصي", "profile": "الملف الشخصي",
"email": "البريد الإلكتروني", "email": "البريد الإلكتروني",
"password": "كلمة المرور", "password": "كلمة المرور",
"newPassword": "كلمة المرور الجديدة",
"confirmPassword": "تأكيد كلمة المرور", "confirmPassword": "تأكيد كلمة المرور",
"forgotPassword": "هل نسيت كلمة المرور؟", "forgotPassword": "هل نسيت كلمة المرور؟",
"loginWithGoogle": "تسجيل الدخول باستخدام جوجل", "loginWithGoogle": "تسجيل الدخول باستخدام جوجل",
@@ -13,6 +14,7 @@ export default {
"privacyPolicy": "سياسة الخصوصية", "privacyPolicy": "سياسة الخصوصية",
"passwordMinLength": "يجب أن تكون كلمة المرور 8 أحرف على الأقل", "passwordMinLength": "يجب أن تكون كلمة المرور 8 أحرف على الأقل",
"newPasswordMinLength": "يجب أن تكون كلمة المرور الجديدة 8 أحرف على الأقل", "newPasswordMinLength": "يجب أن تكون كلمة المرور الجديدة 8 أحرف على الأقل",
"backToHome": "العودة إلى الصفحة الرئيسية",
"menu": { "menu": {
"profile": "الملف الشخصي", "profile": "الملف الشخصي",
"myProfile": "ملفي الشخصي", "myProfile": "ملفي الشخصي",
@@ -21,5 +23,28 @@ export default {
"settings": "الإعدادات", "settings": "الإعدادات",
"adminDashboard": "لوحة تحكم المسؤول", "adminDashboard": "لوحة تحكم المسؤول",
"adminUsers": "مستخدمو المسؤول" "adminUsers": "مستخدمو المسؤول"
},
"resetPassword": {
"title": "إعادة تعيين كلمة المرور",
"button": "إعادة تعيين كلمة المرور",
"success": "تم إعادة تعيين كلمة المرور بنجاح! سيتم توجيهك لتسجيل الدخول قريبًا...",
"invalidToken": "لم يتم العثور على رمز صالح. يرجى استخدام الرابط من بريدك الإلكتروني.",
"error": "حدث خطأ أثناء إعادة تعيين كلمة المرور",
"emailSent": "تم إرسال رابط لإعادة تعيين كلمة المرور إلى بريدك الإلكتروني.",
"emailError": "حدث خطأ أثناء إرسال البريد الإلكتروني"
},
"errors": {
"fillAllFields": "يرجى ملء جميع الحقول",
"invalidEmail": "يرجى إدخال بريد إلكتروني صالح",
"passwordsNotMatch": "كلمات المرور غير متطابقة",
"passwordsNotMatchShort": "كلمات المرور غير متطابقة",
"enterEmail": "يرجى إدخال بريدك الإلكتروني",
"loginFailed": "فشل تسجيل الدخول",
"registerFailed": "فشل التسجيل",
"googleLoginFailed": "فشل تسجيل الدخول عبر جوجل",
"emailExists": "يوجد مستخدم بهذا البريد الإلكتروني بالفعل. يرجى استخدام بريد إلكتروني آخر أو تسجيل الدخول."
},
"success": {
"registerComplete": "تم التسجيل بنجاح. يمكنك الآن تسجيل الدخول."
} }
}; };

View File

@@ -3,6 +3,7 @@ import navigation from './navigation.js';
import auth from './auth.js'; import auth from './auth.js';
import cart from './cart.js'; import cart from './cart.js';
import product from './product.js'; import product from './product.js';
import productDialogs from './productDialogs.js';
import search from './search.js'; import search from './search.js';
import sorting from './sorting.js'; import sorting from './sorting.js';
import chat from './chat.js'; import chat from './chat.js';
@@ -18,6 +19,7 @@ import pages from './pages.js';
import orders from './orders.js'; import orders from './orders.js';
import settings from './settings.js'; import settings from './settings.js';
import common from './common.js'; import common from './common.js';
import kitConfig from './kitConfig.js';
import legalDatenschutzBasic from './legal-datenschutz-basic.js'; import legalDatenschutzBasic from './legal-datenschutz-basic.js';
import legalDatenschutzCustomer from './legal-datenschutz-customer.js'; import legalDatenschutzCustomer from './legal-datenschutz-customer.js';
import legalDatenschutzGoogleOrders from './legal-datenschutz-google-orders.js'; import legalDatenschutzGoogleOrders from './legal-datenschutz-google-orders.js';
@@ -35,6 +37,7 @@ export default {
"auth": auth, "auth": auth,
"cart": cart, "cart": cart,
"product": product, "product": product,
"productDialogs": productDialogs,
"search": search, "search": search,
"sorting": sorting, "sorting": sorting,
"chat": chat, "chat": chat,
@@ -50,6 +53,7 @@ export default {
"orders": orders, "orders": orders,
"settings": settings, "settings": settings,
"common": common, "common": common,
"kitConfig": kitConfig,
"legalDatenschutzBasic": legalDatenschutzBasic, "legalDatenschutzBasic": legalDatenschutzBasic,
"legalDatenschutzCustomer": legalDatenschutzCustomer, "legalDatenschutzCustomer": legalDatenschutzCustomer,
"legalDatenschutzGoogleOrders": legalDatenschutzGoogleOrders, "legalDatenschutzGoogleOrders": legalDatenschutzGoogleOrders,

View File

@@ -0,0 +1,43 @@
export default {
"pageTitle": "🌱 مُكوّن جروبوكس",
"pageSubtitle": "ركّب إعداد النمو الداخلي المثالي بتاعك",
"bundleDiscountTitle": "🎯 احصل على خصم الباقة!",
"loadingProducts": "جارٍ تحميل منتجات الجروبوكس...",
"loadingLighting": "جارٍ تحميل منتجات الإضاءة...",
"loadingVentilation": "جارٍ تحميل منتجات التهوية...",
"loadingExtras": "جارٍ تحميل الإضافات...",
"noProductsAvailable": "لا توجد منتجات متاحة لهذا الحجم",
"noLightingAvailable": "لا توجد أضواء مناسبة لحجم الخيمة {{shape}}.",
"noVentilationAvailable": "لا توجد تهوية مناسبة لحجم الخيمة {{shape}}.",
"noExtrasAvailable": "لا توجد إضافات متاحة",
"selectShapeTitle": "1. اختر شكل الجروبوكس",
"selectShapeSubtitle": "اختار أولاً مساحة قاعدة الجروبوكس بتاعتك",
"selectProductTitle": "2. اختر منتج الجروبوكس",
"selectProductSubtitle": "اختار المنتج المناسب لجروبوكس {{shape}} بتاعك",
"selectLightingTitle": "3. اختر الإضاءة",
"selectLightingTitleShape": "3. اختر الإضاءة - {{shape}}",
"selectLightingSubtitle": "من فضلك اختار حجم الخيمة الأول.",
"selectVentilationTitle": "4. اختر التهوية",
"selectVentilationTitleShape": "4. اختر التهوية - {{shape}}",
"selectVentilationSubtitle": "من فضلك اختار حجم الخيمة الأول.",
"selectExtrasTitle": "5. أضف إضافات (اختياري)",
"yourConfiguration": "🎯 التكوين بتاعك",
"growboxLabel": "جروبوكس: {{name}}",
"lightingLabel": "الإضاءة: {{name}}",
"ventilationLabel": "التهوية: {{name}}",
"extraLabel": "إضافة: {{name}}",
"totalPrice": "السعر الكلي:",
"addToCart": "أضف إلى السلة",
"selected": "✓ تم الاختيار",
"notDeliverable": "غير متوفر للتوصيل",
"noPrice": "لا يوجد سعر",
"setName": "طقم جروبوكس - {{shape}}",
"description60x60": "مُدمج - مثالي للمساحات الصغيرة",
"description80x80": "متوسط - توازن مثالي",
"description100x100": "كبير - للمزارعين المتمرسين",
"description120x60": "مستطيل - استخدام أقصى للمساحة",
"plants1to2": "1-2 نباتات",
"plants2to4": "2-4 نباتات",
"plants4to6": "4-6 نباتات",
"plants3to6": "3-6 نباتات"
};

View File

@@ -3,7 +3,8 @@ export default {
"new": "قيد التنفيذ", "new": "قيد التنفيذ",
"pending": "جديد", "pending": "جديد",
"processing": "قيد التنفيذ", "processing": "قيد التنفيذ",
"cancelled": لغاة", "paid": دفوع",
"cancelled": "ملغي",
"shipped": "تم الشحن", "shipped": "تم الشحن",
"delivered": "تم التوصيل", "delivered": "تم التوصيل",
"return": "إرجاع", "return": "إرجاع",
@@ -24,6 +25,7 @@ export default {
"cancelOrder": "إلغاء الطلب" "cancelOrder": "إلغاء الطلب"
}, },
"noOrders": "لم تقم بوضع أي طلبات بعد.", "noOrders": "لم تقم بوضع أي طلبات بعد.",
"trackShipment": "تتبع الشحنة",
"details": { "details": {
"title": "تفاصيل الطلب: {{orderId}}", "title": "تفاصيل الطلب: {{orderId}}",
"deliveryAddress": "عنوان التوصيل", "deliveryAddress": "عنوان التوصيل",
@@ -36,14 +38,13 @@ export default {
"item": "العنصر", "item": "العنصر",
"quantity": "الكمية", "quantity": "الكمية",
"price": "السعر", "price": "السعر",
"vat": "ضريبة القيمة المضافة",
"total": "الإجمالي", "total": "الإجمالي",
"cancelOrder": "إلغاء الطلب" "cancelOrder": "إلغاء الطلب"
}, },
"cancelConfirm": { "cancelConfirm": {
"title": "إلغاء الطلب", "title": "إلغاء الطلب",
"message": "هل أنت متأكد أنك تريد إلغاء هذا الطلب؟", "message": "هل أنت متأكد أنك تريد إلغاء هذا الطلب؟",
"confirm": "إلغاء الطلب", "confirm": "إلغاء",
"cancelling": "جارٍ الإلغاء..." "cancelling": "جارٍ الإلغاء..."
}, },
"processing": "يتم إكمال الطلب..." "processing": "يتم إكمال الطلب..."

View File

@@ -5,9 +5,10 @@ export default {
"notFoundDescription": "المنتج الذي تبحث عنه غير موجود أو تم إزالته.", "notFoundDescription": "المنتج الذي تبحث عنه غير موجود أو تم إزالته.",
"backToHome": "العودة إلى الصفحة الرئيسية", "backToHome": "العودة إلى الصفحة الرئيسية",
"error": "خطأ", "error": "خطأ",
"articleNumber": "رقم الصنف", "articleNumber": "رقم المنتج",
"manufacturer": "الشركة المصنعة", "manufacturer": "الشركة المصنعة",
"inclVat": "شامل {{vat}}% ضريبة القيمة المضافة", "inclVat": "شامل {{vat}}% ضريبة القيمة المضافة",
"inclVatSimple": "شامل ضريبة القيمة المضافة",
"priceUnit": "{{price}}/{{unit}}", "priceUnit": "{{price}}/{{unit}}",
"new": "جديد", "new": "جديد",
"weeks": "أسابيع", "weeks": "أسابيع",
@@ -15,7 +16,7 @@ export default {
"inclVatFooter": "شامل {{vat}}% ضريبة القيمة المضافة,*", "inclVatFooter": "شامل {{vat}}% ضريبة القيمة المضافة,*",
"availability": "التوفر", "availability": "التوفر",
"inStock": "متوفر في المخزون", "inStock": "متوفر في المخزون",
"comingSoon": "قريبًا متوفر", "comingSoon": "قريبًا",
"deliveryTime": "مدة التوصيل", "deliveryTime": "مدة التوصيل",
"inclShort": "شامل", "inclShort": "شامل",
"vatShort": "ضريبة القيمة المضافة", "vatShort": "ضريبة القيمة المضافة",
@@ -32,10 +33,10 @@ export default {
"similarProducts": "منتجات مشابهة", "similarProducts": "منتجات مشابهة",
"countDisplay": { "countDisplay": {
"noProducts": "0 منتجات", "noProducts": "0 منتجات",
"oneProduct": "منتج واحد", "oneProduct": "1 منتج",
"multipleProducts": "{{count}} منتجات", "multipleProducts": "{{count}} منتجات",
"filteredProducts": "{{filtered}} من {{total}} منتجات", "filteredProducts": "{{filtered}} من {{total}} منتجات",
"filteredOneProduct": "{{filtered}} من منتج واحد", "filteredOneProduct": "{{filtered}} من 1 منتج",
"xOfYProducts": "{{x}} من {{y}} منتجات" "xOfYProducts": "{{x}} من {{y}} منتجات"
}, },
"removeFiltersToSee": "قم بإزالة الفلاتر لرؤية المنتجات", "removeFiltersToSee": "قم بإزالة الفلاتر لرؤية المنتجات",

View File

@@ -0,0 +1,61 @@
export default {
"questionTitle": "سؤال عن المنتج",
"questionSubtitle": "هل لديك سؤال عن هذا المنتج؟ نحن سعداء بمساعدتك.",
"questionSuccess": "شكرًا على سؤالك! سنرد عليك في أقرب وقت ممكن.",
"nameLabel": "الاسم",
"namePlaceholder": "اسمك",
"emailLabel": "البريد الإلكتروني",
"emailPlaceholder": "your.email@example.com",
"questionLabel": "سؤالك",
"questionPlaceholder": "صف سؤالك عن هذا المنتج...",
"photosLabelQuestion": "أرفق صورًا مع سؤالك (اختياري)",
"submitQuestion": "إرسال السؤال",
"sending": "جارٍ الإرسال...",
"ratingTitle": "قيم المنتج",
"ratingSubtitle": "شارك تجربتك مع هذا المنتج وساعد العملاء الآخرين في اتخاذ قرارهم.",
"ratingSuccess": "شكرًا على تقييمك! سيتم نشره بعد المراجعة.",
"emailHelper": "لن يتم نشر بريدك الإلكتروني",
"ratingLabel": "التقييم *",
"pleaseRate": "يرجى التقييم",
"ratingStars": "{{rating}} من 5 نجوم",
"reviewLabel": "تقييمك (اختياري)",
"reviewPlaceholder": "صف تجاربك مع هذا المنتج...",
"photosLabelRating": "أرفق صورًا مع تقييمك (اختياري)",
"submitRating": "إرسال التقييم",
"errorGeneric": "حدث خطأ",
"errorPhotos": "خطأ في معالجة الصور",
"availabilityTitle": "طلب التوفر",
"availabilitySubtitle": "هذا المنتج غير متوفر حاليًا. سنكون سعداء بإبلاغك بمجرد عودته للمخزون.",
"availabilitySuccessEmail": "شكرًا على طلبك! سنخطرك عبر البريد الإلكتروني بمجرد توفر المنتج مرة أخرى.",
"availabilitySuccessTelegram": "شكرًا على طلبك! سنخطرك عبر تيليجرام بمجرد توفر المنتج مرة أخرى.",
"notificationMethodLabel": "كيف تود أن يتم إعلامك؟",
"telegramBotLabel": "بوت تيليجرام",
"telegramIdLabel": "معرف تيليجرام",
"telegramPlaceholder": "@اسمكعلىتيليجرام أو معرف تيليجرام",
"telegramHelper": "أدخل اسم المستخدم الخاص بك على تيليجرام (مع @) أو معرف تيليجرام",
"messageLabel": "رسالة (اختياري)",
"messagePlaceholder": "معلومات إضافية أو أسئلة...",
"submitAvailability": "طلب التوفر",
"photoUploadSelect": "اختر الصور",
"photoUploadErrorMaxFiles": "الحد الأقصى {{max}} ملفات مسموح بها",
"photoUploadErrorFileType": "مسموح فقط بملفات الصور (JPEG, PNG, GIF, WebP)",
"photoUploadErrorFileSize": "الملف كبير جدًا. الحد الأقصى: {{maxSize}} ميجابايت",
"photoUploadSelectedFiles": "{{count}} ملف(ملفات) مختارة",
"photoUploadCompressed": "(تم الضغط للرفع)",
"photoUploadRemove": "إزالة الصورة",
"photoUploadLabelDefault": "أرفق صورًا (اختياري)",
"shareTitle": "مشاركة",
"shareEmbed": "تضمين",
"shareCopyLink": "نسخ الرابط",
"shareSuccessEmbed": "تم نسخ كود التضمين إلى الحافظة!",
"shareErrorEmbed": "حدث خطأ أثناء نسخ كود التضمين",
"shareSuccessLink": "تم نسخ الرابط إلى الحافظة!",
"shareWhatsAppText": "شوف المنتج ده: {{name}}",
"shareTelegramText": "شوف المنتج ده: {{name}}",
"shareEmailSubject": "توصية بمنتج",
"shareEmailBody": "مرحبًا،\n\nحابب أوصي لك بالمنتج ده:\n\n{{name}}\n{{url}}\n\nمع أطيب التحيات"
};

View File

@@ -5,14 +5,16 @@ export default {
"profile": "Профил", "profile": "Профил",
"email": "Имейл", "email": "Имейл",
"password": "Парола", "password": "Парола",
"newPassword": "Нова парола",
"confirmPassword": "Потвърдете паролата", "confirmPassword": "Потвърдете паролата",
"forgotPassword": "Забравена парола?", "forgotPassword": "Забравена парола?",
"loginWithGoogle": "Вход с Google", "loginWithGoogle": "Вход с Google",
"or": "ИЛИ", "or": "ИЛИ",
"privacyAccept": "С натискане на \"Вход с Google\" приемам", "privacyAccept": "С натискането на \"Вход с Google\" приемам",
"privacyPolicy": "Политиката за поверителност", "privacyPolicy": "Политиката за поверителност",
"passwordMinLength": "Паролата трябва да е поне 8 символа", "passwordMinLength": "Паролата трябва да е поне 8 символа",
"newPasswordMinLength": "Новата парола трябва да е поне 8 символа", "newPasswordMinLength": "Новата парола трябва да е поне 8 символа",
"backToHome": "Обратно към началната страница",
"menu": { "menu": {
"profile": "Профил", "profile": "Профил",
"myProfile": "Моят профил", "myProfile": "Моят профил",
@@ -21,5 +23,28 @@ export default {
"settings": "Настройки", "settings": "Настройки",
"adminDashboard": "Админ табло", "adminDashboard": "Админ табло",
"adminUsers": "Админ потребители" "adminUsers": "Админ потребители"
},
"resetPassword": {
"title": "Нулиране на парола",
"button": "Нулиране на парола",
"success": "Вашата парола беше успешно нулирана! Скоро ще бъдете пренасочени към вход...",
"invalidToken": "Няма валиден токен. Моля, използвайте линка от имейла си.",
"error": "Грешка при нулиране на паролата",
"emailSent": "Линк за нулиране на паролата беше изпратен на вашия имейл.",
"emailError": "Грешка при изпращане на имейла"
},
"errors": {
"fillAllFields": "Моля, попълнете всички полета",
"invalidEmail": "Моля, въведете валиден имейл адрес",
"passwordsNotMatch": "Паролите не съвпадат",
"passwordsNotMatchShort": "Паролите не съвпадат",
"enterEmail": "Моля, въведете вашия имейл адрес",
"loginFailed": "Входът не бе успешен",
"registerFailed": "Регистрацията не бе успешна",
"googleLoginFailed": "Вход с Google не бе успешен",
"emailExists": "Потребител с този имейл вече съществува. Моля, използвайте друг имейл или влезте в системата."
},
"success": {
"registerComplete": "Регистрацията беше успешна. Сега можете да влезете."
} }
}; };

View File

@@ -3,6 +3,7 @@ import navigation from './navigation.js';
import auth from './auth.js'; import auth from './auth.js';
import cart from './cart.js'; import cart from './cart.js';
import product from './product.js'; import product from './product.js';
import productDialogs from './productDialogs.js';
import search from './search.js'; import search from './search.js';
import sorting from './sorting.js'; import sorting from './sorting.js';
import chat from './chat.js'; import chat from './chat.js';
@@ -18,6 +19,7 @@ import pages from './pages.js';
import orders from './orders.js'; import orders from './orders.js';
import settings from './settings.js'; import settings from './settings.js';
import common from './common.js'; import common from './common.js';
import kitConfig from './kitConfig.js';
import legalDatenschutzBasic from './legal-datenschutz-basic.js'; import legalDatenschutzBasic from './legal-datenschutz-basic.js';
import legalDatenschutzCustomer from './legal-datenschutz-customer.js'; import legalDatenschutzCustomer from './legal-datenschutz-customer.js';
import legalDatenschutzGoogleOrders from './legal-datenschutz-google-orders.js'; import legalDatenschutzGoogleOrders from './legal-datenschutz-google-orders.js';
@@ -35,6 +37,7 @@ export default {
"auth": auth, "auth": auth,
"cart": cart, "cart": cart,
"product": product, "product": product,
"productDialogs": productDialogs,
"search": search, "search": search,
"sorting": sorting, "sorting": sorting,
"chat": chat, "chat": chat,
@@ -50,6 +53,7 @@ export default {
"orders": orders, "orders": orders,
"settings": settings, "settings": settings,
"common": common, "common": common,
"kitConfig": kitConfig,
"legalDatenschutzBasic": legalDatenschutzBasic, "legalDatenschutzBasic": legalDatenschutzBasic,
"legalDatenschutzCustomer": legalDatenschutzCustomer, "legalDatenschutzCustomer": legalDatenschutzCustomer,
"legalDatenschutzGoogleOrders": legalDatenschutzGoogleOrders, "legalDatenschutzGoogleOrders": legalDatenschutzGoogleOrders,

View File

@@ -0,0 +1,43 @@
export default {
"pageTitle": "🌱 Конфигуратор за Growbox",
"pageSubtitle": "Създайте перфектната си вътрешна система за отглеждане",
"bundleDiscountTitle": "🎯 Вземете отстъпка за комплект!",
"loadingProducts": "Зареждане на продукти за growbox...",
"loadingLighting": "Зареждане на осветителни продукти...",
"loadingVentilation": "Зареждане на вентилационни продукти...",
"loadingExtras": "Зареждане на допълнителни продукти...",
"noProductsAvailable": "Няма налични продукти за този размер",
"noLightingAvailable": "Няма подходящи лампи за размер на палатка {{shape}}.",
"noVentilationAvailable": "Няма подходяща вентилация за размер на палатка {{shape}}.",
"noExtrasAvailable": "Няма налични допълнения",
"selectShapeTitle": "1. Изберете форма на growbox",
"selectShapeSubtitle": "Първо изберете основната площ на вашия growbox",
"selectProductTitle": "2. Изберете продукт за growbox",
"selectProductSubtitle": "Изберете подходящия продукт за вашия {{shape}} growbox",
"selectLightingTitle": "3. Изберете осветление",
"selectLightingTitleShape": "3. Изберете осветление - {{shape}}",
"selectLightingSubtitle": "Моля, първо изберете размер на палатка.",
"selectVentilationTitle": "4. Изберете вентилация",
"selectVentilationTitleShape": "4. Изберете вентилация - {{shape}}",
"selectVentilationSubtitle": "Моля, първо изберете размер на палатка.",
"selectExtrasTitle": "5. Добавете допълнения (по избор)",
"yourConfiguration": "🎯 Вашата конфигурация",
"growboxLabel": "Growbox: {{name}}",
"lightingLabel": "Осветление: {{name}}",
"ventilationLabel": "Вентилация: {{name}}",
"extraLabel": "Допълнение: {{name}}",
"totalPrice": "Обща цена:",
"addToCart": "Добави в количката",
"selected": "✓ Избрано",
"notDeliverable": "Не е налично за доставка",
"noPrice": "Няма цена",
"setName": "Комплект Growbox - {{shape}}",
"description60x60": "Компактен - идеален за малки пространства",
"description80x80": "Среден - перфектен баланс",
"description100x100": "Голям - за опитни отглеждачи",
"description120x60": "Правоъгълен - максимално използване на пространството",
"plants1to2": "1-2 растения",
"plants2to4": "2-4 растения",
"plants4to6": "4-6 растения",
"plants3to6": "3-6 растения"
};

View File

@@ -1,14 +1,15 @@
export default { export default {
"status": { "status": {
"new": "В процес", "new": "в процес",
"pending": "Нова", "pending": "Ново",
"processing": "В процес", "processing": "в процес",
"cancelled": "Отменена", "paid": "Платено",
"shipped": "Изпратена", "cancelled": "Отменено",
"delivered": "Доставена", "shipped": "Изпратено",
"delivered": "Доставено",
"return": "Връщане", "return": "Връщане",
"partialReturn": "Частично връщане", "partialReturn": "Частично връщане",
"partialDelivered": "Частично доставена" "partialDelivered": "Частично доставено"
}, },
"table": { "table": {
"orderNumber": "Номер на поръчка", "orderNumber": "Номер на поръчка",
@@ -24,6 +25,7 @@ export default {
"cancelOrder": "Отмени поръчката" "cancelOrder": "Отмени поръчката"
}, },
"noOrders": "Все още не сте направили поръчки.", "noOrders": "Все още не сте направили поръчки.",
"trackShipment": "Проследи пратката",
"details": { "details": {
"title": "Подробности за поръчка: {{orderId}}", "title": "Подробности за поръчка: {{orderId}}",
"deliveryAddress": "Адрес за доставка", "deliveryAddress": "Адрес за доставка",
@@ -36,15 +38,14 @@ export default {
"item": "Артикул", "item": "Артикул",
"quantity": "Количество", "quantity": "Количество",
"price": "Цена", "price": "Цена",
"vat": "ДДС",
"total": "Общо", "total": "Общо",
"cancelOrder": "Отмени поръчката" "cancelOrder": "Отмени поръчката"
}, },
"cancelConfirm": { "cancelConfirm": {
"title": "Отмяна на поръчка", "title": "Отмени поръчката",
"message": "Сигурни ли сте, че искате да отмените тази поръчка?", "message": "Сигурни ли сте, че искате да отмените тази поръчка?",
"confirm": "Отмени поръчката", "confirm": "Отмени",
"cancelling": "Отмяна..." "cancelling": "Отмяна..."
}, },
"processing": "Поръчката се обработва...", "processing": "Поръчката се обработва..."
}; };

View File

@@ -8,24 +8,25 @@ export default {
"articleNumber": "Номер на артикул", "articleNumber": "Номер на артикул",
"manufacturer": "Производител", "manufacturer": "Производител",
"inclVat": "вкл. {{vat}}% ДДС", "inclVat": "вкл. {{vat}}% ДДС",
"inclVatSimple": "вкл. ДДС",
"priceUnit": "{{price}}/{{unit}}", "priceUnit": "{{price}}/{{unit}}",
"new": "Нов", "new": "Нов",
"weeks": "седмици", "weeks": "Седмици",
"arriving": "Пристигане:", "arriving": "Пристигане:",
"inclVatFooter": "вкл. {{vat}}% ДДС,*", "inclVatFooter": "вкл. {{vat}}% ДДС,*",
"availability": "Наличност", "availability": "Наличност",
"inStock": "налично", "inStock": "налично на склад",
"comingSoon": "Очаква се скоро", "comingSoon": "Очаква се скоро",
"deliveryTime": "Срок на доставка", "deliveryTime": "Срок на доставка",
"inclShort": "вкл.", "inclShort": "вкл.",
"vatShort": "ДДС", "vatShort": "ДДС",
"weight": "Тегло: {{weight}} кг", "weight": "Тегло: {{weight}} кг",
"youSave": "Спестявате: {{amount}}", "youSave": "Спестявате: {{amount}}",
"cheaperThanIndividual": "По-евтино от индивидуална покупка", "cheaperThanIndividual": "По-евтино от закупуване поотделно",
"pickupPrice": "Цена за вземане: 19,90 € на резник.", "pickupPrice": "Цена за вземане: 19,90 € на резник.",
"consistsOf": "Състои се от:", "consistsOf": "Състои се от:",
"loadingComponentDetails": "{{index}}. Зареждане на детайли за компонента...", "loadingComponentDetails": "{{index}}. Зареждане на детайли за компонента...",
"loadingProduct": "Продуктът се зарежда...", "loadingProduct": "Зареждане на продукта...",
"individualPriceTotal": "Обща индивидуална цена:", "individualPriceTotal": "Обща индивидуална цена:",
"setPrice": "Цена на комплекта:", "setPrice": "Цена на комплекта:",
"yourSavings": "Вашите спестявания:", "yourSavings": "Вашите спестявания:",

View File

@@ -0,0 +1,61 @@
export default {
"questionTitle": "Въпрос за продукта",
"questionSubtitle": "Имате ли въпрос за този продукт? Ще се радваме да ви помогнем.",
"questionSuccess": "Благодарим ви за въпроса! Ще се свържем с вас възможно най-скоро.",
"nameLabel": "Име",
"namePlaceholder": "Вашето име",
"emailLabel": "Имейл",
"emailPlaceholder": "your.email@example.com",
"questionLabel": "Вашият въпрос",
"questionPlaceholder": "Опишете въпроса си за този продукт...",
"photosLabelQuestion": "Прикачете снимки към въпроса си (по избор)",
"submitQuestion": "Изпрати въпроса",
"sending": "Изпращане...",
"ratingTitle": "Оценете продукта",
"ratingSubtitle": "Споделете опита си с този продукт и помогнете на други клиенти да вземат решение.",
"ratingSuccess": "Благодарим ви за вашия отзив! Той ще бъде публикуван след проверка.",
"emailHelper": "Вашият имейл няма да бъде публикуван",
"ratingLabel": "Оценка *",
"pleaseRate": "Моля, оценете",
"ratingStars": "{{rating}} от 5 звезди",
"reviewLabel": "Вашият отзив (по избор)",
"reviewPlaceholder": "Опишете опита си с този продукт...",
"photosLabelRating": "Прикачете снимки към отзива си (по избор)",
"submitRating": "Изпрати отзива",
"errorGeneric": "Възникна грешка",
"errorPhotos": "Грешка при обработка на снимките",
"availabilityTitle": "Запитване за наличност",
"availabilitySubtitle": "Този продукт в момента не е наличен. Ще се радваме да ви уведомим веднага щом бъде наличен отново.",
"availabilitySuccessEmail": "Благодарим ви за запитването! Ще ви уведомим по имейл веднага щом продуктът отново е наличен.",
"availabilitySuccessTelegram": "Благодарим ви за запитването! Ще ви уведомим чрез Telegram веднага щом продуктът отново е наличен.",
"notificationMethodLabel": "Как бихте искали да бъдете уведомени?",
"telegramBotLabel": "Telegram Bot",
"telegramIdLabel": "Telegram ID",
"telegramPlaceholder": "@yourTelegramName or Telegram ID",
"telegramHelper": "Въведете вашето потребителско име в Telegram (с @) или Telegram ID",
"messageLabel": "Съобщение (по избор)",
"messagePlaceholder": "Допълнителна информация или въпроси...",
"submitAvailability": "Запитване за наличност",
"photoUploadSelect": "Изберете снимки",
"photoUploadErrorMaxFiles": "Максимум {{max}} файла са разрешени",
"photoUploadErrorFileType": "Разрешени са само файлове с изображения (JPEG, PNG, GIF, WebP)",
"photoUploadErrorFileSize": "Файлът е твърде голям. Максимум: {{maxSize}}MB",
"photoUploadSelectedFiles": "{{count}} файл(ове) избрани",
"photoUploadCompressed": "(компресиран за качване)",
"photoUploadRemove": "Премахни изображението",
"photoUploadLabelDefault": "Прикачи снимки (по избор)",
"shareTitle": "Сподели",
"shareEmbed": "Вграждане",
"shareCopyLink": "Копирай линка",
"shareSuccessEmbed": "Кодът за вграждане е копиран в клипборда!",
"shareErrorEmbed": "Грешка при копиране на кода за вграждане",
"shareSuccessLink": "Линкът е копиран в клипборда!",
"shareWhatsAppText": "Виж този продукт: {{name}}",
"shareTelegramText": "Виж този продукт: {{name}}",
"shareEmailSubject": "Препоръка за продукт",
"shareEmailBody": "Здравейте,\n\nИскам да ви препоръчам този продукт:\n\n{{name}}\n{{url}}\n\nПоздрави"
};

View File

@@ -5,6 +5,7 @@ export default {
"profile": "Profil", "profile": "Profil",
"email": "Email", "email": "Email",
"password": "Heslo", "password": "Heslo",
"newPassword": "Nové heslo",
"confirmPassword": "Potvrdit heslo", "confirmPassword": "Potvrdit heslo",
"forgotPassword": "Zapomněli jste heslo?", "forgotPassword": "Zapomněli jste heslo?",
"loginWithGoogle": "Přihlásit se přes Google", "loginWithGoogle": "Přihlásit se přes Google",
@@ -13,6 +14,7 @@ export default {
"privacyPolicy": "Zásadami ochrany osobních údajů", "privacyPolicy": "Zásadami ochrany osobních údajů",
"passwordMinLength": "Heslo musí mít alespoň 8 znaků", "passwordMinLength": "Heslo musí mít alespoň 8 znaků",
"newPasswordMinLength": "Nové heslo musí mít alespoň 8 znaků", "newPasswordMinLength": "Nové heslo musí mít alespoň 8 znaků",
"backToHome": "Zpět na domovskou stránku",
"menu": { "menu": {
"profile": "Profil", "profile": "Profil",
"myProfile": "Můj profil", "myProfile": "Můj profil",
@@ -21,5 +23,28 @@ export default {
"settings": "Nastavení", "settings": "Nastavení",
"adminDashboard": "Admin Dashboard", "adminDashboard": "Admin Dashboard",
"adminUsers": "Admin Users" "adminUsers": "Admin Users"
},
"resetPassword": {
"title": "Obnovení hesla",
"button": "Obnovit heslo",
"success": "Vaše heslo bylo úspěšně obnoveno! Brzy budete přesměrováni na přihlášení...",
"invalidToken": "Nebyl nalezen platný token. Použijte prosím odkaz z vašeho e-mailu.",
"error": "Chyba při obnově hesla",
"emailSent": "Odkaz pro obnovení hesla byl odeslán na vaši e-mailovou adresu.",
"emailError": "Chyba při odesílání e-mailu"
},
"errors": {
"fillAllFields": "Vyplňte prosím všechna pole",
"invalidEmail": "Zadejte platnou e-mailovou adresu",
"passwordsNotMatch": "Hesla se neshodují",
"passwordsNotMatchShort": "Hesla se neshodují",
"enterEmail": "Zadejte prosím svou e-mailovou adresu",
"loginFailed": "Přihlášení selhalo",
"registerFailed": "Registrace selhala",
"googleLoginFailed": "Přihlášení přes Google selhalo",
"emailExists": "Uživatel s touto e-mailovou adresou již existuje. Použijte prosím jinou e-mailovou adresu nebo se přihlaste."
},
"success": {
"registerComplete": "Registrace byla úspěšná. Nyní se můžete přihlásit."
} }
}; };

View File

@@ -3,6 +3,7 @@ import navigation from './navigation.js';
import auth from './auth.js'; import auth from './auth.js';
import cart from './cart.js'; import cart from './cart.js';
import product from './product.js'; import product from './product.js';
import productDialogs from './productDialogs.js';
import search from './search.js'; import search from './search.js';
import sorting from './sorting.js'; import sorting from './sorting.js';
import chat from './chat.js'; import chat from './chat.js';
@@ -18,6 +19,7 @@ import pages from './pages.js';
import orders from './orders.js'; import orders from './orders.js';
import settings from './settings.js'; import settings from './settings.js';
import common from './common.js'; import common from './common.js';
import kitConfig from './kitConfig.js';
import legalDatenschutzBasic from './legal-datenschutz-basic.js'; import legalDatenschutzBasic from './legal-datenschutz-basic.js';
import legalDatenschutzCustomer from './legal-datenschutz-customer.js'; import legalDatenschutzCustomer from './legal-datenschutz-customer.js';
import legalDatenschutzGoogleOrders from './legal-datenschutz-google-orders.js'; import legalDatenschutzGoogleOrders from './legal-datenschutz-google-orders.js';
@@ -35,6 +37,7 @@ export default {
"auth": auth, "auth": auth,
"cart": cart, "cart": cart,
"product": product, "product": product,
"productDialogs": productDialogs,
"search": search, "search": search,
"sorting": sorting, "sorting": sorting,
"chat": chat, "chat": chat,
@@ -50,6 +53,7 @@ export default {
"orders": orders, "orders": orders,
"settings": settings, "settings": settings,
"common": common, "common": common,
"kitConfig": kitConfig,
"legalDatenschutzBasic": legalDatenschutzBasic, "legalDatenschutzBasic": legalDatenschutzBasic,
"legalDatenschutzCustomer": legalDatenschutzCustomer, "legalDatenschutzCustomer": legalDatenschutzCustomer,
"legalDatenschutzGoogleOrders": legalDatenschutzGoogleOrders, "legalDatenschutzGoogleOrders": legalDatenschutzGoogleOrders,

View File

@@ -0,0 +1,43 @@
export default {
"pageTitle": "🌱 Konfigurátor Growboxu",
"pageSubtitle": "Sestavte si svůj dokonalý indoor grow setup",
"bundleDiscountTitle": "🎯 Zajistěte si slevu na balíček!",
"loadingProducts": "Načítání produktů growboxu...",
"loadingLighting": "Načítání osvětlení...",
"loadingVentilation": "Načítání ventilace...",
"loadingExtras": "Načítání doplňků...",
"noProductsAvailable": "Pro tuto velikost nejsou k dispozici žádné produkty",
"noLightingAvailable": "Pro velikost stanu {{shape}} nejsou k dispozici žádná vhodná světla.",
"noVentilationAvailable": "Pro velikost stanu {{shape}} není k dispozici vhodná ventilace.",
"noExtrasAvailable": "Žádné doplňky nejsou k dispozici",
"selectShapeTitle": "1. Vyberte tvar growboxu",
"selectShapeSubtitle": "Nejprve vyberte základní plochu vašeho growboxu",
"selectProductTitle": "2. Vyberte produkt growboxu",
"selectProductSubtitle": "Vyberte správný produkt pro váš growbox {{shape}}",
"selectLightingTitle": "3. Vyberte osvětlení",
"selectLightingTitleShape": "3. Vyberte osvětlení - {{shape}}",
"selectLightingSubtitle": "Nejprve prosím vyberte velikost stanu.",
"selectVentilationTitle": "4. Vyberte ventilaci",
"selectVentilationTitleShape": "4. Vyberte ventilaci - {{shape}}",
"selectVentilationSubtitle": "Nejprve prosím vyberte velikost stanu.",
"selectExtrasTitle": "5. Přidejte doplňky (volitelné)",
"yourConfiguration": "🎯 Vaše konfigurace",
"growboxLabel": "Growbox: {{name}}",
"lightingLabel": "Osvětlení: {{name}}",
"ventilationLabel": "Ventilace: {{name}}",
"extraLabel": "Doplněk: {{name}}",
"totalPrice": "Celková cena:",
"addToCart": "Přidat do košíku",
"selected": "✓ Vybráno",
"notDeliverable": "Nedodává se",
"noPrice": "Bez ceny",
"setName": "Sada growboxu - {{shape}}",
"description60x60": "Kompaktní - ideální pro malé prostory",
"description80x80": "Střední - perfektní rovnováha",
"description100x100": "Velký - pro zkušené pěstitele",
"description120x60": "Obdélníkový - maximální využití prostoru",
"plants1to2": "1-2 rostliny",
"plants2to4": "2-4 rostliny",
"plants4to6": "4-6 rostlin",
"plants3to6": "3-6 rostlin"
};

View File

@@ -1,50 +1,51 @@
export default { export default {
"status": { "status": {
"new": "Probíhá", "new": "probíhá",
"pending": "Nová", "pending": "Nové",
"processing": "Probíhá", "processing": "probíhá",
"cancelled": "Zrušeno", "paid": "Zaplaceno",
"shipped": "Odesláno", "cancelled": "Zrušeno",
"delivered": "Doručeno", "shipped": "Odesláno",
"return": "Vrácení", "delivered": "Doručeno",
"partialReturn": "Částečné vrácení", "return": "Vrácení",
"partialDelivered": "Částečně doručeno" "partialReturn": "Částečné vrácení",
"partialDelivered": "Částečně doručeno"
}, },
"table": { "table": {
"orderNumber": "Číslo objednávky", "orderNumber": "Číslo objednávky",
"date": "Datum", "date": "Datum",
"status": "Stav", "status": "Stav",
"items": "Položky", "items": "Položky",
"total": "Celkem", "total": "Celkem",
"actions": "Akce", "actions": "Akce",
"viewDetails": "Zobrazit detaily" "viewDetails": "Zobrazit detaily"
}, },
"tooltips": { "tooltips": {
"viewDetails": "Zobrazit detaily", "viewDetails": "Zobrazit detaily",
"cancelOrder": "Zrušit objednávku" "cancelOrder": "Zrušit objednávku"
}, },
"noOrders": "Ještě jste neprovedli žádné objednávky.", "noOrders": "Ještě jste neprovedli žádné objednávky.",
"trackShipment": "Sledovat zásilku",
"details": { "details": {
"title": "Detaily objednávky: {{orderId}}", "title": "Detaily objednávky: {{orderId}}",
"deliveryAddress": "Dodací adresa", "deliveryAddress": "Dodací adresa",
"invoiceAddress": "Fakturační adresa", "invoiceAddress": "Fakturační adresa",
"orderDetails": "Detaily objednávky", "orderDetails": "Detaily objednávky",
"deliveryMethod": "Způsob doručení:", "deliveryMethod": "Způsob doručení:",
"paymentMethod": "Způsob platby:", "paymentMethod": "Způsob platby:",
"notSpecified": "Nespecifikováno", "notSpecified": "Nespecifikováno",
"orderedItems": "Objednané položky", "orderedItems": "Objednané položky",
"item": "Položka", "item": "Položka",
"quantity": "Množství", "quantity": "Množství",
"price": "Cena", "price": "Cena",
"vat": "DPH", "total": "Celkem",
"total": "Celkem", "cancelOrder": "Zrušit objednávku"
"cancelOrder": "Zrušit objednávku"
}, },
"cancelConfirm": { "cancelConfirm": {
"title": "Zrušit objednávku", "title": "Zrušit objednávku",
"message": "Opravdu chcete tuto objednávku zrušit?", "message": "Opravdu chcete tuto objednávku zrušit?",
"confirm": "Zrušit objednávku", "confirm": "Zrušit",
"cancelling": "Rušení..." "cancelling": "Rušení..."
}, },
"processing": "Objednávka se dokončuje...", "processing": "Objednávka se dokončuje..."
}; };

View File

@@ -8,10 +8,11 @@ export default {
"articleNumber": "Číslo artiklu", "articleNumber": "Číslo artiklu",
"manufacturer": "Výrobce", "manufacturer": "Výrobce",
"inclVat": "včetně {{vat}}% DPH", "inclVat": "včetně {{vat}}% DPH",
"inclVatSimple": "včetně DPH",
"priceUnit": "{{price}}/{{unit}}", "priceUnit": "{{price}}/{{unit}}",
"new": "Nové", "new": "Nové",
"weeks": "týdny", "weeks": "Týdny",
"arriving": "Příchod:", "arriving": "Příjezd:",
"inclVatFooter": "včetně {{vat}}% DPH,*", "inclVatFooter": "včetně {{vat}}% DPH,*",
"availability": "Dostupnost", "availability": "Dostupnost",
"inStock": "skladem", "inStock": "skladem",
@@ -22,10 +23,10 @@ export default {
"weight": "Hmotnost: {{weight}} kg", "weight": "Hmotnost: {{weight}} kg",
"youSave": "Ušetříte: {{amount}}", "youSave": "Ušetříte: {{amount}}",
"cheaperThanIndividual": "Levnější než nákup jednotlivě", "cheaperThanIndividual": "Levnější než nákup jednotlivě",
"pickupPrice": "Cena za vyzvednutí: 19,90 € za řízek.", "pickupPrice": "Cena za odběr: 19,90 € za řízek.",
"consistsOf": "Skládá se z:", "consistsOf": "Skládá se z:",
"loadingComponentDetails": "{{index}}. Načítání detailů komponenty...", "loadingComponentDetails": "{{index}}. Načítání detailů komponenty...",
"loadingProduct": "Produkt se načítá...", "loadingProduct": "Načítání produktu...",
"individualPriceTotal": "Celková cena jednotlivě:", "individualPriceTotal": "Celková cena jednotlivě:",
"setPrice": "Cena sady:", "setPrice": "Cena sady:",
"yourSavings": "Vaše úspory:", "yourSavings": "Vaše úspory:",

View File

@@ -0,0 +1,61 @@
export default {
"questionTitle": "Otázka ohledně produktu",
"questionSubtitle": "Máte otázku ohledně tohoto produktu? Rádi vám pomůžeme.",
"questionSuccess": "Děkujeme za vaši otázku! Ozveme se vám co nejdříve.",
"nameLabel": "Jméno",
"namePlaceholder": "Vaše jméno",
"emailLabel": "Email",
"emailPlaceholder": "vas.email@priklad.cz",
"questionLabel": "Vaše otázka",
"questionPlaceholder": "Popište svou otázku ohledně tohoto produktu...",
"photosLabelQuestion": "Přiložte fotografie k vaší otázce (volitelné)",
"submitQuestion": "Odeslat otázku",
"sending": "Odesílání...",
"ratingTitle": "Ohodnoťte produkt",
"ratingSubtitle": "Podělte se o své zkušenosti s tímto produktem a pomozte ostatním zákazníkům s rozhodnutím.",
"ratingSuccess": "Děkujeme za vaši recenzi! Bude zveřejněna po ověření.",
"emailHelper": "Váš email nebude zveřejněn",
"ratingLabel": "Hodnocení *",
"pleaseRate": "Prosím ohodnoťte",
"ratingStars": "{{rating}} z 5 hvězdiček",
"reviewLabel": "Vaše recenze (volitelné)",
"reviewPlaceholder": "Popište své zkušenosti s tímto produktem...",
"photosLabelRating": "Přiložte fotografie k vaší recenzi (volitelné)",
"submitRating": "Odeslat recenzi",
"errorGeneric": "Došlo k chybě",
"errorPhotos": "Chyba při zpracování fotografií",
"availabilityTitle": "Požádejte o dostupnost",
"availabilitySubtitle": "Tento produkt momentálně není dostupný. Rádi vás informujeme, jakmile bude opět skladem.",
"availabilitySuccessEmail": "Děkujeme za váš požadavek! Jakmile bude produkt opět dostupný, budeme vás informovat e-mailem.",
"availabilitySuccessTelegram": "Děkujeme za váš požadavek! Jakmile bude produkt opět dostupný, budeme vás informovat přes Telegram.",
"notificationMethodLabel": "Jak chcete být informováni?",
"telegramBotLabel": "Telegram Bot",
"telegramIdLabel": "Telegram ID",
"telegramPlaceholder": "@vaseTelegramJmeno nebo Telegram ID",
"telegramHelper": "Zadejte své uživatelské jméno na Telegramu (s @) nebo Telegram ID",
"messageLabel": "Zpráva (volitelné)",
"messagePlaceholder": "Další informace nebo otázky...",
"submitAvailability": "Požádat o dostupnost",
"photoUploadSelect": "Vybrat fotografie",
"photoUploadErrorMaxFiles": "Maximálně {{max}} souborů povoleno",
"photoUploadErrorFileType": "Jsou povoleny pouze obrazové soubory (JPEG, PNG, GIF, WebP)",
"photoUploadErrorFileSize": "Soubor je příliš velký. Maximum: {{maxSize}}MB",
"photoUploadSelectedFiles": "Vybráno {{count}} souborů",
"photoUploadCompressed": "(komprimováno pro nahrání)",
"photoUploadRemove": "Odstranit obrázek",
"photoUploadLabelDefault": "Přiložit fotografie (volitelné)",
"shareTitle": "Sdílet",
"shareEmbed": "Vložit",
"shareCopyLink": "Kopírovat odkaz",
"shareSuccessEmbed": "Kód pro vložení zkopírován do schránky!",
"shareErrorEmbed": "Chyba při kopírování kódu pro vložení",
"shareSuccessLink": "Odkaz zkopírován do schránky!",
"shareWhatsAppText": "Podívejte se na tento produkt: {{name}}",
"shareTelegramText": "Podívejte se na tento produkt: {{name}}",
"shareEmailSubject": "Doporučení produktu",
"shareEmailBody": "Dobrý den,\n\nrád bych vám doporučil tento produkt:\n\n{{name}}\n{{url}}\n\nS pozdravem"
};

View File

@@ -5,6 +5,7 @@ export default {
"profile": "Profil", "profile": "Profil",
"email": "E-Mail", "email": "E-Mail",
"password": "Passwort", "password": "Passwort",
"newPassword": "Neues Passwort",
"confirmPassword": "Passwort bestätigen", "confirmPassword": "Passwort bestätigen",
"forgotPassword": "Passwort vergessen?", "forgotPassword": "Passwort vergessen?",
"loginWithGoogle": "Mit Google anmelden", "loginWithGoogle": "Mit Google anmelden",
@@ -13,6 +14,7 @@ export default {
"privacyPolicy": "Datenschutzbestimmungen", "privacyPolicy": "Datenschutzbestimmungen",
"passwordMinLength": "Das Passwort muss mindestens 8 Zeichen lang sein", "passwordMinLength": "Das Passwort muss mindestens 8 Zeichen lang sein",
"newPasswordMinLength": "Das neue Passwort muss mindestens 8 Zeichen lang sein", "newPasswordMinLength": "Das neue Passwort muss mindestens 8 Zeichen lang sein",
"backToHome": "Zurück zur Startseite",
"menu": { "menu": {
"profile": "Profil", "profile": "Profil",
"myProfile": "Mein Profil", "myProfile": "Mein Profil",
@@ -21,5 +23,28 @@ export default {
"settings": "Einstellungen", "settings": "Einstellungen",
"adminDashboard": "Admin Dashboard", "adminDashboard": "Admin Dashboard",
"adminUsers": "Admin Users" "adminUsers": "Admin Users"
},
"resetPassword": {
"title": "Passwort zurücksetzen",
"button": "Passwort zurücksetzen",
"success": "Ihr Passwort wurde erfolgreich zurückgesetzt! Sie werden in Kürze zur Anmeldung weitergeleitet...",
"invalidToken": "Kein gültiger Token gefunden. Bitte verwenden Sie den Link aus Ihrer E-Mail.",
"error": "Fehler beim Zurücksetzen des Passworts",
"emailSent": "Ein Link zum Zurücksetzen des Passworts wurde an Ihre E-Mail-Adresse gesendet.",
"emailError": "Fehler beim Senden der E-Mail"
},
"errors": {
"fillAllFields": "Bitte füllen Sie alle Felder aus",
"invalidEmail": "Bitte geben Sie eine gültige E-Mail-Adresse ein",
"passwordsNotMatch": "Die Passwörter stimmen nicht überein",
"passwordsNotMatchShort": "Passwörter stimmen nicht überein",
"enterEmail": "Bitte geben Sie Ihre E-Mail-Adresse ein",
"loginFailed": "Anmeldung fehlgeschlagen",
"registerFailed": "Registrierung fehlgeschlagen",
"googleLoginFailed": "Google-Anmeldung fehlgeschlagen",
"emailExists": "Ein Benutzer mit dieser E-Mail-Adresse existiert bereits. Bitte verwenden Sie eine andere E-Mail-Adresse oder melden Sie sich an."
},
"success": {
"registerComplete": "Registrierung erfolgreich. Sie können sich jetzt anmelden."
} }
}; };

View File

@@ -3,6 +3,7 @@ import navigation from './navigation.js';
import auth from './auth.js'; import auth from './auth.js';
import cart from './cart.js'; import cart from './cart.js';
import product from './product.js'; import product from './product.js';
import productDialogs from './productDialogs.js';
import search from './search.js'; import search from './search.js';
import sorting from './sorting.js'; import sorting from './sorting.js';
import chat from './chat.js'; import chat from './chat.js';
@@ -18,6 +19,7 @@ import pages from './pages.js';
import orders from './orders.js'; import orders from './orders.js';
import settings from './settings.js'; import settings from './settings.js';
import common from './common.js'; import common from './common.js';
import kitConfig from './kitConfig.js';
import legalDatenschutzBasic from './legal-datenschutz-basic.js'; import legalDatenschutzBasic from './legal-datenschutz-basic.js';
import legalDatenschutzCustomer from './legal-datenschutz-customer.js'; import legalDatenschutzCustomer from './legal-datenschutz-customer.js';
import legalDatenschutzGoogleOrders from './legal-datenschutz-google-orders.js'; import legalDatenschutzGoogleOrders from './legal-datenschutz-google-orders.js';
@@ -35,6 +37,7 @@ export default {
"auth": auth, "auth": auth,
"cart": cart, "cart": cart,
"product": product, "product": product,
"productDialogs": productDialogs,
"search": search, "search": search,
"sorting": sorting, "sorting": sorting,
"chat": chat, "chat": chat,
@@ -50,6 +53,7 @@ export default {
"orders": orders, "orders": orders,
"settings": settings, "settings": settings,
"common": common, "common": common,
"kitConfig": kitConfig,
"legalDatenschutzBasic": legalDatenschutzBasic, "legalDatenschutzBasic": legalDatenschutzBasic,
"legalDatenschutzCustomer": legalDatenschutzCustomer, "legalDatenschutzCustomer": legalDatenschutzCustomer,
"legalDatenschutzGoogleOrders": legalDatenschutzGoogleOrders, "legalDatenschutzGoogleOrders": legalDatenschutzGoogleOrders,

View File

@@ -0,0 +1,44 @@
export default {
"pageTitle": "🌱 Growbox Konfigurator",
"pageSubtitle": "Stelle dein perfektes Indoor Grow Setup zusammen",
"bundleDiscountTitle": "🎯 Bundle-Rabatt sichern!",
"loadingProducts": "Lade Growbox-Produkte...",
"loadingLighting": "Lade Beleuchtungs-Produkte...",
"loadingVentilation": "Lade Belüftungs-Produkte...",
"loadingExtras": "Lade Extras...",
"noProductsAvailable": "Keine Produkte für diese Größe verfügbar",
"noLightingAvailable": "Keine passenden Lampen für Zeltgröße {{shape}} verfügbar.",
"noVentilationAvailable": "Keine passenden Belüftung für Zeltgröße {{shape}} verfügbar.",
"noExtrasAvailable": "Keine Extras verfügbar",
"selectShapeTitle": "1. Growbox-Form auswählen",
"selectShapeSubtitle": "Wähle zuerst die Grundfläche deiner Growbox aus",
"selectProductTitle": "2. Growbox Produkt auswählen",
"selectProductSubtitle": "Wähle das passende Produkt für deine {{shape}} Growbox",
"selectLightingTitle": "3. Beleuchtung wählen",
"selectLightingTitleShape": "3. Beleuchtung wählen - {{shape}}",
"selectLightingSubtitle": "Bitte wählen Sie zuerst eine Zeltgröße aus.",
"selectVentilationTitle": "4. Belüftung auswählen",
"selectVentilationTitleShape": "4. Belüftung auswählen - {{shape}}",
"selectVentilationSubtitle": "Bitte wählen Sie zuerst eine Zeltgröße aus.",
"selectExtrasTitle": "5. Extras hinzufügen (optional)",
"yourConfiguration": "🎯 Ihre Konfiguration",
"growboxLabel": "Growbox: {{name}}",
"lightingLabel": "Beleuchtung: {{name}}",
"ventilationLabel": "Belüftung: {{name}}",
"extraLabel": "Extra: {{name}}",
"totalPrice": "Gesamtpreis:",
"addToCart": "In den Warenkorb",
"selected": "✓ Ausgewählt",
"notDeliverable": "Nicht lieferbar",
"noPrice": "Kein Preis",
"setName": "Growbox Set - {{shape}}",
"description60x60": "Kompakt - ideal für kleine Räume",
"description80x80": "Mittel - perfekte Balance",
"description100x100": "Groß - für erfahrene Grower",
"description120x60": "Rechteckig - maximale Raumnutzung",
"plants1to2": "1-2 Pflanzen",
"plants2to4": "2-4 Pflanzen",
"plants4to6": "4-6 Pflanzen",
"plants3to6": "3-6 Pflanzen"
};

View File

@@ -3,6 +3,7 @@ export default {
"new": "in Bearbeitung", "new": "in Bearbeitung",
"pending": "Neu", "pending": "Neu",
"processing": "in Bearbeitung", "processing": "in Bearbeitung",
"paid": "Bezahlt",
"cancelled": "Storniert", "cancelled": "Storniert",
"shipped": "Verschickt", "shipped": "Verschickt",
"delivered": "Geliefert", "delivered": "Geliefert",
@@ -24,6 +25,7 @@ export default {
"cancelOrder": "Bestellung stornieren" "cancelOrder": "Bestellung stornieren"
}, },
"noOrders": "Sie haben noch keine Bestellungen aufgegeben.", "noOrders": "Sie haben noch keine Bestellungen aufgegeben.",
"trackShipment": "Sendung verfolgen",
"details": { "details": {
"title": "Bestelldetails: {{orderId}}", "title": "Bestelldetails: {{orderId}}",
"deliveryAddress": "Lieferadresse", "deliveryAddress": "Lieferadresse",

View File

@@ -8,6 +8,7 @@ export default {
"articleNumber": "Artikelnummer", "articleNumber": "Artikelnummer",
"manufacturer": "Hersteller", "manufacturer": "Hersteller",
"inclVat": "inkl. {{vat}}% MwSt.", "inclVat": "inkl. {{vat}}% MwSt.",
"inclVatSimple": "inkl. MwSt.",
"priceUnit": "{{price}}/{{unit}}", "priceUnit": "{{price}}/{{unit}}",
"new": "Neu", "new": "Neu",
"weeks": "Wochen", "weeks": "Wochen",

View File

@@ -0,0 +1,62 @@
export default {
"questionTitle": "Frage zum Artikel",
"questionSubtitle": "Haben Sie eine Frage zu diesem Artikel? Wir helfen Ihnen gerne weiter.",
"questionSuccess": "Vielen Dank für Ihre Frage! Wir werden uns schnellstmöglich bei Ihnen melden.",
"nameLabel": "Name",
"namePlaceholder": "Ihr Name",
"emailLabel": "E-Mail",
"emailPlaceholder": "ihre.email@example.com",
"questionLabel": "Ihre Frage",
"questionPlaceholder": "Beschreiben Sie Ihre Frage zu diesem Artikel...",
"photosLabelQuestion": "Fotos zur Frage anhängen (optional)",
"submitQuestion": "Frage senden",
"sending": "Wird gesendet...",
"ratingTitle": "Artikel Bewerten",
"ratingSubtitle": "Teilen Sie Ihre Erfahrungen mit diesem Artikel und helfen Sie anderen Kunden bei der Entscheidung.",
"ratingSuccess": "Vielen Dank für Ihre Bewertung! Sie wird nach Prüfung veröffentlicht.",
"emailHelper": "Ihre E-Mail wird nicht veröffentlicht",
"ratingLabel": "Bewertung *",
"pleaseRate": "Bitte bewerten",
"ratingStars": "{{rating}} von 5 Sternen",
"reviewLabel": "Ihre Bewertung (optional)",
"reviewPlaceholder": "Beschreiben Sie Ihre Erfahrungen mit diesem Artikel...",
"photosLabelRating": "Fotos zur Bewertung anhängen (optional)",
"submitRating": "Bewertung abgeben",
"errorGeneric": "Ein Fehler ist aufgetreten",
"errorPhotos": "Fehler beim Verarbeiten der Fotos",
"availabilityTitle": "Verfügbarkeit anfragen",
"availabilitySubtitle": "Dieser Artikel ist derzeit nicht verfügbar. Gerne informieren wir Sie, sobald er wieder lieferbar ist.",
"availabilitySuccessEmail": "Vielen Dank für Ihre Anfrage! Wir werden Sie per E-Mail informieren, sobald der Artikel wieder verfügbar ist.",
"availabilitySuccessTelegram": "Vielen Dank für Ihre Anfrage! Wir werden Sie über Telegram informieren, sobald der Artikel wieder verfügbar ist.",
"notificationMethodLabel": "Wie möchten Sie benachrichtigt werden?",
"telegramBotLabel": "Telegram Bot",
"telegramIdLabel": "Telegram ID",
"telegramPlaceholder": "@IhrTelegramName oder Telegram ID",
"telegramHelper": "Geben Sie Ihren Telegram-Benutzernamen (mit @) oder Ihre Telegram-ID ein",
"messageLabel": "Nachricht (optional)",
"messagePlaceholder": "Zusätzliche Informationen oder Fragen...",
"submitAvailability": "Verfügbarkeit anfragen",
"photoUploadSelect": "Fotos auswählen",
"photoUploadErrorMaxFiles": "Maximal {{max}} Dateien erlaubt",
"photoUploadErrorFileType": "Nur Bilddateien (JPEG, PNG, GIF, WebP) sind erlaubt",
"photoUploadErrorFileSize": "Datei zu groß. Maximum: {{maxSize}}MB",
"photoUploadSelectedFiles": "{{count}} Datei(en) ausgewählt",
"photoUploadCompressed": "(komprimiert für Upload)",
"photoUploadRemove": "Bild entfernen",
"photoUploadLabelDefault": "Fotos anhängen (optional)",
"shareTitle": "Teilen",
"shareEmbed": "Einbetten",
"shareCopyLink": "Link kopieren",
"shareSuccessEmbed": "Einbettungscode wurde in die Zwischenablage kopiert!",
"shareErrorEmbed": "Fehler beim Kopieren des Einbettungscodes",
"shareSuccessLink": "Link wurde in die Zwischenablage kopiert!",
"shareWhatsAppText": "Schau dir dieses Produkt an: {{name}}",
"shareTelegramText": "Schau dir dieses Produkt an: {{name}}",
"shareEmailSubject": "Produktempfehlung",
"shareEmailBody": "Hallo,\n\nich möchte dir dieses Produkt empfehlen:\n\n{{name}}\n{{url}}\n\nViele Grüße"
};

View File

@@ -5,14 +5,16 @@ export default {
"profile": "Προφίλ", "profile": "Προφίλ",
"email": "Email", "email": "Email",
"password": "Κωδικός", "password": "Κωδικός",
"newPassword": "Νέος κωδικός",
"confirmPassword": "Επιβεβαίωση κωδικού", "confirmPassword": "Επιβεβαίωση κωδικού",
"forgotPassword": "Ξεχάσατε τον κωδικό;", "forgotPassword": "Ξεχάσατε τον κωδικό;",
"loginWithGoogle": "Σύνδεση με Google", "loginWithGoogle": "Σύνδεση με Google",
"or": "Ή", "or": "Ή",
"privacyAccept": "Κάνοντας κλικ στο \"Σύνδεση με Google\" αποδέχομαι την", "privacyAccept": "Κάνοντας κλικ στο \"Σύνδεση με Google\" αποδέχομαι την",
"privacyPolicy": "Πολιτική Απορρήτου", "privacyPolicy": "Πολιτική απορρήτου",
"passwordMinLength": "Ο κωδικός πρέπει να έχει τουλάχιστον 8 χαρακτήρες", "passwordMinLength": "Ο κωδικός πρέπει να έχει τουλάχιστον 8 χαρακτήρες",
"newPasswordMinLength": "Ο νέος κωδικός πρέπει να έχει τουλάχιστον 8 χαρακτήρες", "newPasswordMinLength": "Ο νέος κωδικός πρέπει να έχει τουλάχιστον 8 χαρακτήρες",
"backToHome": "Επιστροφή στην αρχική σελίδα",
"menu": { "menu": {
"profile": "Προφίλ", "profile": "Προφίλ",
"myProfile": "Το προφίλ μου", "myProfile": "Το προφίλ μου",
@@ -21,5 +23,28 @@ export default {
"settings": "Ρυθμίσεις", "settings": "Ρυθμίσεις",
"adminDashboard": "Πίνακας διαχείρισης", "adminDashboard": "Πίνακας διαχείρισης",
"adminUsers": "Διαχειριστές" "adminUsers": "Διαχειριστές"
},
"resetPassword": {
"title": "Επαναφορά κωδικού",
"button": "Επαναφορά κωδικού",
"success": "Ο κωδικός σας επαναφέρθηκε με επιτυχία! Θα ανακατευθυνθείτε στη σύνδεση σύντομα...",
"invalidToken": "Δεν βρέθηκε έγκυρο διακριτικό. Παρακαλώ χρησιμοποιήστε τον σύνδεσμο από το email σας.",
"error": "Σφάλμα κατά την επαναφορά του κωδικού",
"emailSent": "Ένας σύνδεσμος για επαναφορά του κωδικού σας έχει σταλεί στη διεύθυνση email σας.",
"emailError": "Σφάλμα κατά την αποστολή του email"
},
"errors": {
"fillAllFields": "Παρακαλώ συμπληρώστε όλα τα πεδία",
"invalidEmail": "Παρακαλώ εισάγετε μια έγκυρη διεύθυνση email",
"passwordsNotMatch": "Οι κωδικοί δεν ταιριάζουν",
"passwordsNotMatchShort": "Οι κωδικοί δεν ταιριάζουν",
"enterEmail": "Παρακαλώ εισάγετε τη διεύθυνση email σας",
"loginFailed": "Η σύνδεση απέτυχε",
"registerFailed": "Η εγγραφή απέτυχε",
"googleLoginFailed": "Η σύνδεση με Google απέτυχε",
"emailExists": "Υπάρχει ήδη χρήστης με αυτή τη διεύθυνση email. Παρακαλώ χρησιμοποιήστε άλλη διεύθυνση ή συνδεθείτε."
},
"success": {
"registerComplete": "Η εγγραφή ολοκληρώθηκε με επιτυχία. Μπορείτε τώρα να συνδεθείτε."
} }
}; };

View File

@@ -3,6 +3,7 @@ import navigation from './navigation.js';
import auth from './auth.js'; import auth from './auth.js';
import cart from './cart.js'; import cart from './cart.js';
import product from './product.js'; import product from './product.js';
import productDialogs from './productDialogs.js';
import search from './search.js'; import search from './search.js';
import sorting from './sorting.js'; import sorting from './sorting.js';
import chat from './chat.js'; import chat from './chat.js';
@@ -18,6 +19,7 @@ import pages from './pages.js';
import orders from './orders.js'; import orders from './orders.js';
import settings from './settings.js'; import settings from './settings.js';
import common from './common.js'; import common from './common.js';
import kitConfig from './kitConfig.js';
import legalDatenschutzBasic from './legal-datenschutz-basic.js'; import legalDatenschutzBasic from './legal-datenschutz-basic.js';
import legalDatenschutzCustomer from './legal-datenschutz-customer.js'; import legalDatenschutzCustomer from './legal-datenschutz-customer.js';
import legalDatenschutzGoogleOrders from './legal-datenschutz-google-orders.js'; import legalDatenschutzGoogleOrders from './legal-datenschutz-google-orders.js';
@@ -35,6 +37,7 @@ export default {
"auth": auth, "auth": auth,
"cart": cart, "cart": cart,
"product": product, "product": product,
"productDialogs": productDialogs,
"search": search, "search": search,
"sorting": sorting, "sorting": sorting,
"chat": chat, "chat": chat,
@@ -50,6 +53,7 @@ export default {
"orders": orders, "orders": orders,
"settings": settings, "settings": settings,
"common": common, "common": common,
"kitConfig": kitConfig,
"legalDatenschutzBasic": legalDatenschutzBasic, "legalDatenschutzBasic": legalDatenschutzBasic,
"legalDatenschutzCustomer": legalDatenschutzCustomer, "legalDatenschutzCustomer": legalDatenschutzCustomer,
"legalDatenschutzGoogleOrders": legalDatenschutzGoogleOrders, "legalDatenschutzGoogleOrders": legalDatenschutzGoogleOrders,

View File

@@ -0,0 +1,43 @@
export default {
"pageTitle": "🌱 Διαμορφωτής Growbox",
"pageSubtitle": "Συνθέστε το τέλειο εσωτερικό σύστημα καλλιέργειας",
"bundleDiscountTitle": "🎯 Εξασφαλίστε έκπτωση πακέτου!",
"loadingProducts": "Φόρτωση προϊόντων growbox...",
"loadingLighting": "Φόρτωση προϊόντων φωτισμού...",
"loadingVentilation": "Φόρτωση προϊόντων αερισμού...",
"loadingExtras": "Φόρτωση επιπλέον...",
"noProductsAvailable": "Δεν υπάρχουν διαθέσιμα προϊόντα για αυτό το μέγεθος",
"noLightingAvailable": "Δεν υπάρχουν κατάλληλα φώτα για το μέγεθος σκηνής {{shape}}.",
"noVentilationAvailable": "Δεν υπάρχει κατάλληλος αερισμός για το μέγεθος σκηνής {{shape}}.",
"noExtrasAvailable": "Δεν υπάρχουν επιπλέον διαθέσιμα",
"selectShapeTitle": "1. Επιλέξτε το σχήμα του growbox",
"selectShapeSubtitle": "Επιλέξτε πρώτα την επιφάνεια βάσης του growbox σας",
"selectProductTitle": "2. Επιλέξτε προϊόν growbox",
"selectProductSubtitle": "Επιλέξτε το κατάλληλο προϊόν για το growbox {{shape}} σας",
"selectLightingTitle": "3. Επιλέξτε φωτισμό",
"selectLightingTitleShape": "3. Επιλέξτε φωτισμό - {{shape}}",
"selectLightingSubtitle": "Παρακαλώ επιλέξτε πρώτα το μέγεθος της σκηνής.",
"selectVentilationTitle": "4. Επιλέξτε αερισμό",
"selectVentilationTitleShape": "4. Επιλέξτε αερισμό - {{shape}}",
"selectVentilationSubtitle": "Παρακαλώ επιλέξτε πρώτα το μέγεθος της σκηνής.",
"selectExtrasTitle": "5. Προσθέστε επιπλέον (προαιρετικά)",
"yourConfiguration": "🎯 Η διαμόρφωσή σας",
"growboxLabel": "Growbox: {{name}}",
"lightingLabel": "Φωτισμός: {{name}}",
"ventilationLabel": "Αερισμός: {{name}}",
"extraLabel": "Επιπλέον: {{name}}",
"totalPrice": "Συνολική τιμή:",
"addToCart": "Προσθήκη στο καλάθι",
"selected": "✓ Επιλεγμένο",
"notDeliverable": "Μη διαθέσιμο για παράδοση",
"noPrice": "Χωρίς τιμή",
"setName": "Σετ Growbox - {{shape}}",
"description60x60": "Συμπαγές - ιδανικό για μικρούς χώρους",
"description80x80": "Μεσαίο - τέλεια ισορροπία",
"description100x100": "Μεγάλο - για έμπειρους καλλιεργητές",
"description120x60": "Ορθογώνιο - μέγιστη χρήση χώρου",
"plants1to2": "1-2 φυτά",
"plants2to4": "2-4 φυτά",
"plants4to6": "4-6 φυτά",
"plants3to6": "3-6 φυτά"
};

View File

@@ -1,10 +1,11 @@
export default { export default {
"status": { "status": {
"new": "Σε εξέλιξη", "new": "σε εξέλιξη",
"pending": "Νέο", "pending": "Νέο",
"processing": "Σε εξέλιξη", "processing": "σε εξέλιξη",
"paid": "Πληρωμένο",
"cancelled": "Ακυρώθηκε", "cancelled": "Ακυρώθηκε",
"shipped": "Απεστάλη", "shipped": "Απεσταλμένο",
"delivered": "Παραδόθηκε", "delivered": "Παραδόθηκε",
"return": "Επιστροφή", "return": "Επιστροφή",
"partialReturn": "Μερική επιστροφή", "partialReturn": "Μερική επιστροφή",
@@ -24,10 +25,11 @@ export default {
"cancelOrder": "Ακύρωση παραγγελίας" "cancelOrder": "Ακύρωση παραγγελίας"
}, },
"noOrders": "Δεν έχετε κάνει ακόμα καμία παραγγελία.", "noOrders": "Δεν έχετε κάνει ακόμα καμία παραγγελία.",
"trackShipment": "Παρακολούθηση αποστολής",
"details": { "details": {
"title": "Λεπτομέρειες παραγγελίας: {{orderId}}", "title": "Λεπτομέρειες παραγγελίας: {{orderId}}",
"deliveryAddress": "Διεύθυνση παράδοσης", "deliveryAddress": "Διεύθυνση παράδοσης",
"invoiceAddress": "Διεύθυνση τιμολόγησης", "invoiceAddress": "Διεύθυνση τιμολογίου",
"orderDetails": "Λεπτομέρειες παραγγελίας", "orderDetails": "Λεπτομέρειες παραγγελίας",
"deliveryMethod": "Τρόπος παράδοσης:", "deliveryMethod": "Τρόπος παράδοσης:",
"paymentMethod": "Τρόπος πληρωμής:", "paymentMethod": "Τρόπος πληρωμής:",
@@ -36,15 +38,14 @@ export default {
"item": "Είδος", "item": "Είδος",
"quantity": "Ποσότητα", "quantity": "Ποσότητα",
"price": "Τιμή", "price": "Τιμή",
"vat": "ΦΠΑ",
"total": "Σύνολο", "total": "Σύνολο",
"cancelOrder": "Ακύρωση παραγγελίας" "cancelOrder": "Ακύρωση παραγγελίας"
}, },
"cancelConfirm": { "cancelConfirm": {
"title": "Ακύρωση παραγγελίας", "title": "Ακύρωση παραγγελίας",
"message": "Είστε σίγουροι ότι θέλετε να ακυρώσετε αυτήν την παραγγελία;", "message": "Είστε σίγουροι ότι θέλετε να ακυρώσετε αυτή την παραγγελία;",
"confirm": "Ακύρωση παραγγελίας", "confirm": "Ακύρωση",
"cancelling": "Ακύρωση..." "cancelling": "Ακύρωση σε εξέλιξη..."
}, },
"processing": "Η παραγγελία ολοκληρώνεται..." "processing": "Η παραγγελία ολοκληρώνεται..."
}; };

View File

@@ -8,9 +8,10 @@ export default {
"articleNumber": "Αριθμός άρθρου", "articleNumber": "Αριθμός άρθρου",
"manufacturer": "Κατασκευαστής", "manufacturer": "Κατασκευαστής",
"inclVat": "συμπ. {{vat}}% ΦΠΑ", "inclVat": "συμπ. {{vat}}% ΦΠΑ",
"inclVatSimple": "συμπ. ΦΠΑ",
"priceUnit": "{{price}}/{{unit}}", "priceUnit": "{{price}}/{{unit}}",
"new": "Νέο", "new": "Νέο",
"weeks": "εβδομάδες", "weeks": "Εβδομάδες",
"arriving": "Άφιξη:", "arriving": "Άφιξη:",
"inclVatFooter": "συμπ. {{vat}}% ΦΠΑ,*", "inclVatFooter": "συμπ. {{vat}}% ΦΠΑ,*",
"availability": "Διαθεσιμότητα", "availability": "Διαθεσιμότητα",
@@ -28,7 +29,7 @@ export default {
"loadingProduct": "Φόρτωση προϊόντος...", "loadingProduct": "Φόρτωση προϊόντος...",
"individualPriceTotal": "Συνολική τιμή μεμονωμένων:", "individualPriceTotal": "Συνολική τιμή μεμονωμένων:",
"setPrice": "Τιμή σετ:", "setPrice": "Τιμή σετ:",
"yourSavings": "Η εξοικονόμησή σας:", "yourSavings": "Οι εξοικονομήσεις σας:",
"similarProducts": "Παρόμοια προϊόντα", "similarProducts": "Παρόμοια προϊόντα",
"countDisplay": { "countDisplay": {
"noProducts": "0 προϊόντα", "noProducts": "0 προϊόντα",

View File

@@ -0,0 +1,61 @@
export default {
"questionTitle": "Ερώτηση σχετικά με το προϊόν",
"questionSubtitle": "Έχετε κάποια ερώτηση για αυτό το προϊόν; Είμαστε εδώ για να σας βοηθήσουμε.",
"questionSuccess": "Ευχαριστούμε για την ερώτησή σας! Θα επικοινωνήσουμε μαζί σας το συντομότερο δυνατό.",
"nameLabel": "Όνομα",
"namePlaceholder": "Το όνομά σας",
"emailLabel": "Email",
"emailPlaceholder": "your.email@example.com",
"questionLabel": "Η ερώτησή σας",
"questionPlaceholder": "Περιγράψτε την ερώτησή σας σχετικά με αυτό το προϊόν...",
"photosLabelQuestion": "Επισυνάψτε φωτογραφίες στην ερώτησή σας (προαιρετικό)",
"submitQuestion": "Αποστολή ερώτησης",
"sending": "Αποστολή...",
"ratingTitle": "Αξιολογήστε το προϊόν",
"ratingSubtitle": "Μοιραστείτε την εμπειρία σας με αυτό το προϊόν και βοηθήστε άλλους πελάτες να πάρουν την απόφασή τους.",
"ratingSuccess": "Ευχαριστούμε για την αξιολόγησή σας! Θα δημοσιευτεί μετά από έλεγχο.",
"emailHelper": "Το email σας δεν θα δημοσιευτεί",
"ratingLabel": "Αξιολόγηση *",
"pleaseRate": "Παρακαλώ αξιολογήστε",
"ratingStars": "{{rating}} από 5 αστέρια",
"reviewLabel": "Η κριτική σας (προαιρετικό)",
"reviewPlaceholder": "Περιγράψτε τις εμπειρίες σας με αυτό το προϊόν...",
"photosLabelRating": "Επισυνάψτε φωτογραφίες στην κριτική σας (προαιρετικό)",
"submitRating": "Υποβολή κριτικής",
"errorGeneric": "Παρουσιάστηκε σφάλμα",
"errorPhotos": "Σφάλμα κατά την επεξεργασία των φωτογραφιών",
"availabilityTitle": "Ζητήστε διαθεσιμότητα",
"availabilitySubtitle": "Αυτό το προϊόν δεν είναι διαθέσιμο αυτή τη στιγμή. Θα χαρούμε να σας ενημερώσουμε μόλις είναι ξανά διαθέσιμο.",
"availabilitySuccessEmail": "Ευχαριστούμε για το αίτημά σας! Θα σας ενημερώσουμε μέσω email μόλις το προϊόν είναι ξανά διαθέσιμο.",
"availabilitySuccessTelegram": "Ευχαριστούμε για το αίτημά σας! Θα σας ενημερώσουμε μέσω Telegram μόλις το προϊόν είναι ξανά διαθέσιμο.",
"notificationMethodLabel": "Πώς θέλετε να ειδοποιηθείτε;",
"telegramBotLabel": "Telegram Bot",
"telegramIdLabel": "Telegram ID",
"telegramPlaceholder": "@yourTelegramName or Telegram ID",
"telegramHelper": "Εισάγετε το όνομα χρήστη Telegram (με @) ή το Telegram ID σας",
"messageLabel": "Μήνυμα (προαιρετικό)",
"messagePlaceholder": "Επιπλέον πληροφορίες ή ερωτήσεις...",
"submitAvailability": "Ζητήστε διαθεσιμότητα",
"photoUploadSelect": "Επιλέξτε φωτογραφίες",
"photoUploadErrorMaxFiles": "Επιτρέπονται έως {{max}} αρχεία",
"photoUploadErrorFileType": "Επιτρέπονται μόνο αρχεία εικόνας (JPEG, PNG, GIF, WebP)",
"photoUploadErrorFileSize": "Το αρχείο είναι πολύ μεγάλο. Μέγιστο: {{maxSize}}MB",
"photoUploadSelectedFiles": "Επιλέχθηκαν {{count}} αρχεία",
"photoUploadCompressed": "(συμπιεσμένο για αποστολή)",
"photoUploadRemove": "Αφαίρεση εικόνας",
"photoUploadLabelDefault": "Επισύναψη φωτογραφιών (προαιρετικό)",
"shareTitle": "Κοινοποίηση",
"shareEmbed": "Ενσωμάτωση",
"shareCopyLink": "Αντιγραφή συνδέσμου",
"shareSuccessEmbed": "Ο κώδικας ενσωμάτωσης αντιγράφηκε στο πρόχειρο!",
"shareErrorEmbed": "Σφάλμα κατά την αντιγραφή του κώδικα ενσωμάτωσης",
"shareSuccessLink": "Ο σύνδεσμος αντιγράφηκε στο πρόχειρο!",
"shareWhatsAppText": "Δείτε αυτό το προϊόν: {{name}}",
"shareTelegramText": "Δείτε αυτό το προϊόν: {{name}}",
"shareEmailSubject": "Σύσταση προϊόντος",
"shareEmailBody": "Γεια σας,\n\nΘα ήθελα να σας προτείνω αυτό το προϊόν:\n\n{{name}}\n{{url}}\n\nΜε εκτίμηση"
};

View File

@@ -5,14 +5,16 @@ export default {
"profile": "Profile", // Profil "profile": "Profile", // Profil
"email": "Email", // E-Mail "email": "Email", // E-Mail
"password": "Password", // Passwort "password": "Password", // Passwort
"newPassword": "New password", // Neues Passwort
"confirmPassword": "Confirm password", // Passwort bestätigen "confirmPassword": "Confirm password", // Passwort bestätigen
"forgotPassword": "Forgot password?", // Passwort vergessen? "forgotPassword": "Forgot password?", // Passwort vergessen?
"loginWithGoogle": "Sign in with Google", // Mit Google anmelden "loginWithGoogle": "Sign in with Google", // Mit Google anmelden
"or": "OR", // ODER "or": "OR", // ODER
"privacyAccept": "By clicking \"Sign in with Google\" I accept the", // Mit dem Click auf "Mit Google anmelden" akzeptiere ich die "privacyAccept": "By clicking on \"Sign in with Google\" I accept the", // Mit dem Click auf "Mit Google anmelden" akzeptiere ich die
"privacyPolicy": "Privacy Policy", // Datenschutzbestimmungen "privacyPolicy": "Privacy policy", // Datenschutzbestimmungen
"passwordMinLength": "The password must be at least 8 characters long", // Das Passwort muss mindestens 8 Zeichen lang sein "passwordMinLength": "The password must be at least 8 characters long", // Das Passwort muss mindestens 8 Zeichen lang sein
"newPasswordMinLength": "The new password must be at least 8 characters long", // Das neue Passwort muss mindestens 8 Zeichen lang sein "newPasswordMinLength": "The new password must be at least 8 characters long", // Das neue Passwort muss mindestens 8 Zeichen lang sein
"backToHome": "Back to homepage", // Zurück zur Startseite
"menu": { "menu": {
"profile": "Profile", // Profil "profile": "Profile", // Profil
"myProfile": "My profile", // Mein Profil "myProfile": "My profile", // Mein Profil
@@ -21,5 +23,28 @@ export default {
"settings": "Settings", // Einstellungen "settings": "Settings", // Einstellungen
"adminDashboard": "Admin Dashboard", // Admin Dashboard "adminDashboard": "Admin Dashboard", // Admin Dashboard
"adminUsers": "Admin Users" // Admin Users "adminUsers": "Admin Users" // Admin Users
},
"resetPassword": {
"title": "Reset password", // Passwort zurücksetzen
"button": "Reset password", // Passwort zurücksetzen
"success": "Your password has been reset successfully! You will be redirected to login shortly...", // Ihr Passwort wurde erfolgreich zurückgesetzt! Sie werden in Kürze zur Anmeldung weitergeleitet...
"invalidToken": "No valid token found. Please use the link from your email.", // Kein gültiger Token gefunden. Bitte verwenden Sie den Link aus Ihrer E-Mail.
"error": "Error resetting password", // Fehler beim Zurücksetzen des Passworts
"emailSent": "A link to reset your password has been sent to your email address.", // Ein Link zum Zurücksetzen des Passworts wurde an Ihre E-Mail-Adresse gesendet.
"emailError": "Error sending email" // Fehler beim Senden der E-Mail
},
"errors": {
"fillAllFields": "Please fill in all fields", // Bitte füllen Sie alle Felder aus
"invalidEmail": "Please enter a valid email address", // Bitte geben Sie eine gültige E-Mail-Adresse ein
"passwordsNotMatch": "The passwords do not match", // Die Passwörter stimmen nicht überein
"passwordsNotMatchShort": "Passwords do not match", // Passwörter stimmen nicht überein
"enterEmail": "Please enter your email address", // Bitte geben Sie Ihre E-Mail-Adresse ein
"loginFailed": "Login failed", // Anmeldung fehlgeschlagen
"registerFailed": "Registration failed", // Registrierung fehlgeschlagen
"googleLoginFailed": "Google login failed", // Google-Anmeldung fehlgeschlagen
"emailExists": "A user with this email address already exists. Please use another email address or log in." // Ein Benutzer mit dieser E-Mail-Adresse existiert bereits. Bitte verwenden Sie eine andere E-Mail-Adresse oder melden Sie sich an.
},
"success": {
"registerComplete": "Registration successful. You can now log in." // Registrierung erfolgreich. Sie können sich jetzt anmelden.
} }
}; };

View File

@@ -3,6 +3,7 @@ import navigation from './navigation.js';
import auth from './auth.js'; import auth from './auth.js';
import cart from './cart.js'; import cart from './cart.js';
import product from './product.js'; import product from './product.js';
import productDialogs from './productDialogs.js';
import search from './search.js'; import search from './search.js';
import sorting from './sorting.js'; import sorting from './sorting.js';
import chat from './chat.js'; import chat from './chat.js';
@@ -18,6 +19,7 @@ import pages from './pages.js';
import orders from './orders.js'; import orders from './orders.js';
import settings from './settings.js'; import settings from './settings.js';
import common from './common.js'; import common from './common.js';
import kitConfig from './kitConfig.js';
import legalDatenschutzBasic from './legal-datenschutz-basic.js'; import legalDatenschutzBasic from './legal-datenschutz-basic.js';
import legalDatenschutzCustomer from './legal-datenschutz-customer.js'; import legalDatenschutzCustomer from './legal-datenschutz-customer.js';
import legalDatenschutzGoogleOrders from './legal-datenschutz-google-orders.js'; import legalDatenschutzGoogleOrders from './legal-datenschutz-google-orders.js';
@@ -35,6 +37,7 @@ export default {
"auth": auth, "auth": auth,
"cart": cart, "cart": cart,
"product": product, "product": product,
"productDialogs": productDialogs,
"search": search, "search": search,
"sorting": sorting, "sorting": sorting,
"chat": chat, "chat": chat,
@@ -50,6 +53,7 @@ export default {
"orders": orders, "orders": orders,
"settings": settings, "settings": settings,
"common": common, "common": common,
"kitConfig": kitConfig,
"legalDatenschutzBasic": legalDatenschutzBasic, "legalDatenschutzBasic": legalDatenschutzBasic,
"legalDatenschutzCustomer": legalDatenschutzCustomer, "legalDatenschutzCustomer": legalDatenschutzCustomer,
"legalDatenschutzGoogleOrders": legalDatenschutzGoogleOrders, "legalDatenschutzGoogleOrders": legalDatenschutzGoogleOrders,

View File

@@ -0,0 +1,43 @@
export default {
"pageTitle": "🌱 Growbox Configurator", // 🌱 Growbox Konfigurator
"pageSubtitle": "Put together your perfect indoor grow setup", // Stelle dein perfektes Indoor Grow Setup zusammen
"bundleDiscountTitle": "🎯 Secure bundle discount!", // 🎯 Bundle-Rabatt sichern!
"loadingProducts": "Loading growbox products...", // Lade Growbox-Produkte...
"loadingLighting": "Loading lighting products...", // Lade Beleuchtungs-Produkte...
"loadingVentilation": "Loading ventilation products...", // Lade Belüftungs-Produkte...
"loadingExtras": "Loading extras...", // Lade Extras...
"noProductsAvailable": "No products available for this size", // Keine Produkte für diese Größe verfügbar
"noLightingAvailable": "No suitable lights available for tent size {{shape}}.", // Keine passenden Lampen für Zeltgröße {{shape}} verfügbar.
"noVentilationAvailable": "No suitable ventilation available for tent size {{shape}}.", // Keine passenden Belüftung für Zeltgröße {{shape}} verfügbar.
"noExtrasAvailable": "No extras available", // Keine Extras verfügbar
"selectShapeTitle": "1. Select growbox shape", // 1. Growbox-Form auswählen
"selectShapeSubtitle": "First select the base area of your growbox", // Wähle zuerst die Grundfläche deiner Growbox aus
"selectProductTitle": "2. Select growbox product", // 2. Growbox Produkt auswählen
"selectProductSubtitle": "Choose the right product for your {{shape}} growbox", // Wähle das passende Produkt für deine {{shape}} Growbox
"selectLightingTitle": "3. Choose lighting", // 3. Beleuchtung wählen
"selectLightingTitleShape": "3. Choose lighting - {{shape}}", // 3. Beleuchtung wählen - {{shape}}
"selectLightingSubtitle": "Please select a tent size first.", // Bitte wählen Sie zuerst eine Zeltgröße aus.
"selectVentilationTitle": "4. Select ventilation", // 4. Belüftung auswählen
"selectVentilationTitleShape": "4. Select ventilation - {{shape}}", // 4. Belüftung auswählen - {{shape}}
"selectVentilationSubtitle": "Please select a tent size first.", // Bitte wählen Sie zuerst eine Zeltgröße aus.
"selectExtrasTitle": "5. Add extras (optional)", // 5. Extras hinzufügen (optional)
"yourConfiguration": "🎯 Your configuration", // 🎯 Ihre Konfiguration
"growboxLabel": "Growbox: {{name}}", // Growbox: {{name}}
"lightingLabel": "Lighting: {{name}}", // Beleuchtung: {{name}}
"ventilationLabel": "Ventilation: {{name}}", // Belüftung: {{name}}
"extraLabel": "Extra: {{name}}", // Extra: {{name}}
"totalPrice": "Total price:", // Gesamtpreis:
"addToCart": "Add to cart", // In den Warenkorb
"selected": "✓ Selected", // ✓ Ausgewählt
"notDeliverable": "Not deliverable", // Nicht lieferbar
"noPrice": "No price", // Kein Preis
"setName": "Growbox set - {{shape}}", // Growbox Set - {{shape}}
"description60x60": "Compact - ideal for small spaces", // Kompakt - ideal für kleine Räume
"description80x80": "Medium - perfect balance", // Mittel - perfekte Balance
"description100x100": "Large - for experienced growers", // Groß - für erfahrene Grower
"description120x60": "Rectangular - maximum space usage", // Rechteckig - maximale Raumnutzung
"plants1to2": "1-2 plants", // 1-2 Pflanzen
"plants2to4": "2-4 plants", // 2-4 Pflanzen
"plants4to6": "4-6 plants", // 4-6 Pflanzen
"plants3to6": "3-6 plants" // 3-6 Pflanzen
};

View File

@@ -1,8 +1,9 @@
export default { export default {
"status": { "status": {
"new": "In progress", // in Bearbeitung "new": "in progress", // in Bearbeitung
"pending": "New", // Neu "pending": "New", // Neu
"processing": "In progress", // in Bearbeitung "processing": "in progress", // in Bearbeitung
"paid": "Paid", // Bezahlt
"cancelled": "Cancelled", // Storniert "cancelled": "Cancelled", // Storniert
"shipped": "Shipped", // Verschickt "shipped": "Shipped", // Verschickt
"delivered": "Delivered", // Geliefert "delivered": "Delivered", // Geliefert
@@ -24,6 +25,7 @@ export default {
"cancelOrder": "Cancel order" // Bestellung stornieren "cancelOrder": "Cancel order" // Bestellung stornieren
}, },
"noOrders": "You have not placed any orders yet.", // Sie haben noch keine Bestellungen aufgegeben. "noOrders": "You have not placed any orders yet.", // Sie haben noch keine Bestellungen aufgegeben.
"trackShipment": "Track shipment", // Sendung verfolgen
"details": { "details": {
"title": "Order details: {{orderId}}", // Bestelldetails: {{orderId}} "title": "Order details: {{orderId}}", // Bestelldetails: {{orderId}}
"deliveryAddress": "Delivery address", // Lieferadresse "deliveryAddress": "Delivery address", // Lieferadresse
@@ -36,15 +38,14 @@ export default {
"item": "Item", // Artikel "item": "Item", // Artikel
"quantity": "Quantity", // Menge "quantity": "Quantity", // Menge
"price": "Price", // Preis "price": "Price", // Preis
"vat": "VAT", // MwSt.
"total": "Total", // Gesamt "total": "Total", // Gesamt
"cancelOrder": "Cancel order" // Bestellung stornieren "cancelOrder": "Cancel order" // Bestellung stornieren
}, },
"cancelConfirm": { "cancelConfirm": {
"title": "Cancel Order", "title": "Cancel order", // Bestellung stornieren
"message": "Are you sure you want to cancel this order?", "message": "Are you sure you want to cancel this order?", // Sind Sie sicher, dass Sie diese Bestellung stornieren möchten?
"confirm": "Cancel Order", "confirm": "Cancel", // Stornieren
"cancelling": "Cancelling..." "cancelling": "Cancelling..." // Wird storniert...
}, },
"processing": "Order is being completed...", // Bestellung wird abgeschlossen... "processing": "Order is being completed..." // Bestellung wird abgeschlossen...
}; };

View File

@@ -8,9 +8,10 @@ export default {
"articleNumber": "Article number", // Artikelnummer "articleNumber": "Article number", // Artikelnummer
"manufacturer": "Manufacturer", // Hersteller "manufacturer": "Manufacturer", // Hersteller
"inclVat": "incl. {{vat}}% VAT", // inkl. {{vat}}% MwSt. "inclVat": "incl. {{vat}}% VAT", // inkl. {{vat}}% MwSt.
"inclVatSimple": "incl. VAT", // inkl. MwSt.
"priceUnit": "{{price}}/{{unit}}", // {{price}}/{{unit}} "priceUnit": "{{price}}/{{unit}}", // {{price}}/{{unit}}
"new": "New", // Neu "new": "New", // Neu
"weeks": "weeks", // Wochen "weeks": "Weeks", // Wochen
"arriving": "Arrival:", // Ankunft: "arriving": "Arrival:", // Ankunft:
"inclVatFooter": "incl. {{vat}}% VAT,*", // inkl. {{vat}}% MwSt.,* "inclVatFooter": "incl. {{vat}}% VAT,*", // inkl. {{vat}}% MwSt.,*
"availability": "Availability", // Verfügbarkeit "availability": "Availability", // Verfügbarkeit
@@ -23,13 +24,13 @@ export default {
"youSave": "You save: {{amount}}", // Sie sparen: {{amount}} "youSave": "You save: {{amount}}", // Sie sparen: {{amount}}
"cheaperThanIndividual": "Cheaper than buying individually", // Günstiger als Einzelkauf "cheaperThanIndividual": "Cheaper than buying individually", // Günstiger als Einzelkauf
"pickupPrice": "Pickup price: €19.90 per cutting.", // Abholpreis: 19,90 € pro Steckling. "pickupPrice": "Pickup price: €19.90 per cutting.", // Abholpreis: 19,90 € pro Steckling.
"consistsOf": "Consists of:", // Bestehend aus: "consistsOf": "Consisting of:", // Bestehend aus:
"loadingComponentDetails": "{{index}}. Loading component details...", // {{index}}. Lädt Komponent-Details... "loadingComponentDetails": "{{index}}. Loading component details...", // {{index}}. Lädt Komponent-Details...
"loadingProduct": "Product is loading...", // Produkt wird geladen... "loadingProduct": "Loading product...", // Produkt wird geladen...
"individualPriceTotal": "Total individual price:", // Einzelpreis gesamt: "individualPriceTotal": "Total individual price:", // Einzelpreis gesamt:
"setPrice": "Set price:", // Set-Preis: "setPrice": "Set price:", // Set-Preis:
"yourSavings": "Your savings:", // Ihre Ersparnis: "yourSavings": "Your savings:", // Ihre Ersparnis:
"similarProducts": "Similar Products", // Ähnliche Produkte "similarProducts": "Similar products", // Ähnliche Produkte
"countDisplay": { "countDisplay": {
"noProducts": "0 products", // 0 Produkte "noProducts": "0 products", // 0 Produkte
"oneProduct": "1 product", // 1 Produkt "oneProduct": "1 product", // 1 Produkt

View File

@@ -0,0 +1,61 @@
export default {
"questionTitle": "Question about the product", // Frage zum Artikel
"questionSubtitle": "Do you have a question about this product? We are happy to help you.", // Haben Sie eine Frage zu diesem Artikel? Wir helfen Ihnen gerne weiter.
"questionSuccess": "Thank you for your question! We will get back to you as soon as possible.", // Vielen Dank für Ihre Frage! Wir werden uns schnellstmöglich bei Ihnen melden.
"nameLabel": "Name", // Name
"namePlaceholder": "Your name", // Ihr Name
"emailLabel": "Email", // E-Mail
"emailPlaceholder": "your.email@example.com", // ihre.email@example.com
"questionLabel": "Your question", // Ihre Frage
"questionPlaceholder": "Describe your question about this product...", // Beschreiben Sie Ihre Frage zu diesem Artikel...
"photosLabelQuestion": "Attach photos to your question (optional)", // Fotos zur Frage anhängen (optional)
"submitQuestion": "Send question", // Frage senden
"sending": "Sending...", // Wird gesendet...
"ratingTitle": "Rate product", // Artikel Bewerten
"ratingSubtitle": "Share your experience with this product and help other customers make their decision.", // Teilen Sie Ihre Erfahrungen mit diesem Artikel und helfen Sie anderen Kunden bei der Entscheidung.
"ratingSuccess": "Thank you for your review! It will be published after verification.", // Vielen Dank für Ihre Bewertung! Sie wird nach Prüfung veröffentlicht.
"emailHelper": "Your email will not be published", // Ihre E-Mail wird nicht veröffentlicht
"ratingLabel": "Rating *", // Bewertung *
"pleaseRate": "Please rate", // Bitte bewerten
"ratingStars": "{{rating}} out of 5 stars", // {{rating}} von 5 Sternen
"reviewLabel": "Your review (optional)", // Ihre Bewertung (optional)
"reviewPlaceholder": "Describe your experiences with this product...", // Beschreiben Sie Ihre Erfahrungen mit diesem Artikel...
"photosLabelRating": "Attach photos to your review (optional)", // Fotos zur Bewertung anhängen (optional)
"submitRating": "Submit review", // Bewertung abgeben
"errorGeneric": "An error occurred", // Ein Fehler ist aufgetreten
"errorPhotos": "Error processing photos", // Fehler beim Verarbeiten der Fotos
"availabilityTitle": "Request availability", // Verfügbarkeit anfragen
"availabilitySubtitle": "This product is currently unavailable. We will be happy to inform you as soon as it is back in stock.", // Dieser Artikel ist derzeit nicht verfügbar. Gerne informieren wir Sie, sobald er wieder lieferbar ist.
"availabilitySuccessEmail": "Thank you for your request! We will notify you by email as soon as the product is available again.", // Vielen Dank für Ihre Anfrage! Wir werden Sie per E-Mail informieren, sobald der Artikel wieder verfügbar ist.
"availabilitySuccessTelegram": "Thank you for your request! We will notify you via Telegram as soon as the product is available again.", // Vielen Dank für Ihre Anfrage! Wir werden Sie über Telegram informieren, sobald der Artikel wieder verfügbar ist.
"notificationMethodLabel": "How would you like to be notified?", // Wie möchten Sie benachrichtigt werden?
"telegramBotLabel": "Telegram Bot", // Telegram Bot
"telegramIdLabel": "Telegram ID", // Telegram ID
"telegramPlaceholder": "@yourTelegramName or Telegram ID", // @IhrTelegramName oder Telegram ID
"telegramHelper": "Enter your Telegram username (with @) or Telegram ID", // Geben Sie Ihren Telegram-Benutzernamen (mit @) oder Ihre Telegram-ID ein
"messageLabel": "Message (optional)", // Nachricht (optional)
"messagePlaceholder": "Additional information or questions...", // Zusätzliche Informationen oder Fragen...
"submitAvailability": "Request availability", // Verfügbarkeit anfragen
"photoUploadSelect": "Select photos", // Fotos auswählen
"photoUploadErrorMaxFiles": "Maximum {{max}} files allowed", // Maximal {{max}} Dateien erlaubt
"photoUploadErrorFileType": "Only image files (JPEG, PNG, GIF, WebP) are allowed", // Nur Bilddateien (JPEG, PNG, GIF, WebP) sind erlaubt
"photoUploadErrorFileSize": "File too large. Maximum: {{maxSize}}MB", // Datei zu groß. Maximum: {{maxSize}}MB
"photoUploadSelectedFiles": "{{count}} file(s) selected", // {{count}} Datei(en) ausgewählt
"photoUploadCompressed": "(compressed for upload)", // (komprimiert für Upload)
"photoUploadRemove": "Remove image", // Bild entfernen
"photoUploadLabelDefault": "Attach photos (optional)", // Fotos anhängen (optional)
"shareTitle": "Share", // Teilen
"shareEmbed": "Embed", // Einbetten
"shareCopyLink": "Copy link", // Link kopieren
"shareSuccessEmbed": "Embed code copied to clipboard!", // Einbettungscode wurde in die Zwischenablage kopiert!
"shareErrorEmbed": "Error copying the embed code", // Fehler beim Kopieren des Einbettungscodes
"shareSuccessLink": "Link copied to clipboard!", // Link wurde in die Zwischenablage kopiert!
"shareWhatsAppText": "Check out this product: {{name}}", // Schau dir dieses Produkt an: {{name}}
"shareTelegramText": "Check out this product: {{name}}", // Schau dir dieses Produkt an: {{name}}
"shareEmailSubject": "Product recommendation", // Produktempfehlung
"shareEmailBody": "Hello,\n\nI'd like to recommend this product to you:\n\n{{name}}\n{{url}}\n\nBest regards", // Hallo,\n\nich möchte dir dieses Produkt empfehlen:\n\n{{name}}\n{{url}}\n\nViele Grüße
};

View File

@@ -5,6 +5,7 @@ export default {
"profile": "Perfil", "profile": "Perfil",
"email": "Correo electrónico", "email": "Correo electrónico",
"password": "Contraseña", "password": "Contraseña",
"newPassword": "Nueva contraseña",
"confirmPassword": "Confirmar contraseña", "confirmPassword": "Confirmar contraseña",
"forgotPassword": "¿Olvidaste tu contraseña?", "forgotPassword": "¿Olvidaste tu contraseña?",
"loginWithGoogle": "Iniciar sesión con Google", "loginWithGoogle": "Iniciar sesión con Google",
@@ -13,6 +14,7 @@ export default {
"privacyPolicy": "Política de privacidad", "privacyPolicy": "Política de privacidad",
"passwordMinLength": "La contraseña debe tener al menos 8 caracteres", "passwordMinLength": "La contraseña debe tener al menos 8 caracteres",
"newPasswordMinLength": "La nueva contraseña debe tener al menos 8 caracteres", "newPasswordMinLength": "La nueva contraseña debe tener al menos 8 caracteres",
"backToHome": "Volver a la página principal",
"menu": { "menu": {
"profile": "Perfil", "profile": "Perfil",
"myProfile": "Mi perfil", "myProfile": "Mi perfil",
@@ -21,5 +23,28 @@ export default {
"settings": "Configuración", "settings": "Configuración",
"adminDashboard": "Panel de administración", "adminDashboard": "Panel de administración",
"adminUsers": "Usuarios administradores" "adminUsers": "Usuarios administradores"
},
"resetPassword": {
"title": "Restablecer contraseña",
"button": "Restablecer contraseña",
"success": "¡Tu contraseña ha sido restablecida con éxito! Serás redirigido para iniciar sesión en breve...",
"invalidToken": "No se encontró un token válido. Por favor, usa el enlace de tu correo electrónico.",
"error": "Error al restablecer la contraseña",
"emailSent": "Se ha enviado un enlace para restablecer tu contraseña a tu dirección de correo electrónico.",
"emailError": "Error al enviar el correo electrónico"
},
"errors": {
"fillAllFields": "Por favor, completa todos los campos",
"invalidEmail": "Por favor, introduce una dirección de correo electrónico válida",
"passwordsNotMatch": "Las contraseñas no coinciden",
"passwordsNotMatchShort": "Las contraseñas no coinciden",
"enterEmail": "Por favor, introduce tu dirección de correo electrónico",
"loginFailed": "Error al iniciar sesión",
"registerFailed": "Error al registrarse",
"googleLoginFailed": "Error al iniciar sesión con Google",
"emailExists": "Ya existe un usuario con esta dirección de correo electrónico. Por favor, usa otra dirección de correo electrónico o inicia sesión."
},
"success": {
"registerComplete": "Registro exitoso. Ahora puedes iniciar sesión."
} }
}; };

View File

@@ -3,6 +3,7 @@ import navigation from './navigation.js';
import auth from './auth.js'; import auth from './auth.js';
import cart from './cart.js'; import cart from './cart.js';
import product from './product.js'; import product from './product.js';
import productDialogs from './productDialogs.js';
import search from './search.js'; import search from './search.js';
import sorting from './sorting.js'; import sorting from './sorting.js';
import chat from './chat.js'; import chat from './chat.js';
@@ -18,6 +19,7 @@ import pages from './pages.js';
import orders from './orders.js'; import orders from './orders.js';
import settings from './settings.js'; import settings from './settings.js';
import common from './common.js'; import common from './common.js';
import kitConfig from './kitConfig.js';
import legalDatenschutzBasic from './legal-datenschutz-basic.js'; import legalDatenschutzBasic from './legal-datenschutz-basic.js';
import legalDatenschutzCustomer from './legal-datenschutz-customer.js'; import legalDatenschutzCustomer from './legal-datenschutz-customer.js';
import legalDatenschutzGoogleOrders from './legal-datenschutz-google-orders.js'; import legalDatenschutzGoogleOrders from './legal-datenschutz-google-orders.js';
@@ -35,6 +37,7 @@ export default {
"auth": auth, "auth": auth,
"cart": cart, "cart": cart,
"product": product, "product": product,
"productDialogs": productDialogs,
"search": search, "search": search,
"sorting": sorting, "sorting": sorting,
"chat": chat, "chat": chat,
@@ -50,6 +53,7 @@ export default {
"orders": orders, "orders": orders,
"settings": settings, "settings": settings,
"common": common, "common": common,
"kitConfig": kitConfig,
"legalDatenschutzBasic": legalDatenschutzBasic, "legalDatenschutzBasic": legalDatenschutzBasic,
"legalDatenschutzCustomer": legalDatenschutzCustomer, "legalDatenschutzCustomer": legalDatenschutzCustomer,
"legalDatenschutzGoogleOrders": legalDatenschutzGoogleOrders, "legalDatenschutzGoogleOrders": legalDatenschutzGoogleOrders,

View File

@@ -0,0 +1,43 @@
export default {
"pageTitle": "🌱 Configurador de Growbox",
"pageSubtitle": "Arma tu configuración perfecta para cultivo en interior",
"bundleDiscountTitle": "🎯 ¡Asegura el descuento por paquete!",
"loadingProducts": "Cargando productos de growbox...",
"loadingLighting": "Cargando productos de iluminación...",
"loadingVentilation": "Cargando productos de ventilación...",
"loadingExtras": "Cargando extras...",
"noProductsAvailable": "No hay productos disponibles para este tamaño",
"noLightingAvailable": "No hay luces adecuadas disponibles para el tamaño de tienda {{shape}}.",
"noVentilationAvailable": "No hay ventilación adecuada disponible para el tamaño de tienda {{shape}}.",
"noExtrasAvailable": "No hay extras disponibles",
"selectShapeTitle": "1. Selecciona la forma de la growbox",
"selectShapeSubtitle": "Primero selecciona el área base de tu growbox",
"selectProductTitle": "2. Selecciona el producto de growbox",
"selectProductSubtitle": "Elige el producto adecuado para tu growbox {{shape}}",
"selectLightingTitle": "3. Elige la iluminación",
"selectLightingTitleShape": "3. Elige la iluminación - {{shape}}",
"selectLightingSubtitle": "Por favor selecciona primero un tamaño de tienda.",
"selectVentilationTitle": "4. Selecciona la ventilación",
"selectVentilationTitleShape": "4. Selecciona la ventilación - {{shape}}",
"selectVentilationSubtitle": "Por favor selecciona primero un tamaño de tienda.",
"selectExtrasTitle": "5. Añade extras (opcional)",
"yourConfiguration": "🎯 Tu configuración",
"growboxLabel": "Growbox: {{name}}",
"lightingLabel": "Iluminación: {{name}}",
"ventilationLabel": "Ventilación: {{name}}",
"extraLabel": "Extra: {{name}}",
"totalPrice": "Precio total:",
"addToCart": "Añadir al carrito",
"selected": "✓ Seleccionado",
"notDeliverable": "No entregable",
"noPrice": "Sin precio",
"setName": "Set de growbox - {{shape}}",
"description60x60": "Compacto - ideal para espacios pequeños",
"description80x80": "Mediano - equilibrio perfecto",
"description100x100": "Grande - para cultivadores experimentados",
"description120x60": "Rectangular - uso máximo del espacio",
"plants1to2": "1-2 plantas",
"plants2to4": "2-4 plantas",
"plants4to6": "4-6 plantas",
"plants3to6": "3-6 plantas"
};

View File

@@ -1,8 +1,9 @@
export default { export default {
"status": { "status": {
"new": "En progreso", "new": "en progreso",
"pending": "Nuevo", "pending": "Nuevo",
"processing": "En progreso", "processing": "en progreso",
"paid": "Pagado",
"cancelled": "Cancelado", "cancelled": "Cancelado",
"shipped": "Enviado", "shipped": "Enviado",
"delivered": "Entregado", "delivered": "Entregado",
@@ -24,6 +25,7 @@ export default {
"cancelOrder": "Cancelar pedido" "cancelOrder": "Cancelar pedido"
}, },
"noOrders": "Aún no has realizado ningún pedido.", "noOrders": "Aún no has realizado ningún pedido.",
"trackShipment": "Rastrear envío",
"details": { "details": {
"title": "Detalles del pedido: {{orderId}}", "title": "Detalles del pedido: {{orderId}}",
"deliveryAddress": "Dirección de entrega", "deliveryAddress": "Dirección de entrega",
@@ -36,15 +38,14 @@ export default {
"item": "Artículo", "item": "Artículo",
"quantity": "Cantidad", "quantity": "Cantidad",
"price": "Precio", "price": "Precio",
"vat": "IVA",
"total": "Total", "total": "Total",
"cancelOrder": "Cancelar pedido" "cancelOrder": "Cancelar pedido"
}, },
"cancelConfirm": { "cancelConfirm": {
"title": "Cancelar pedido", "title": "Cancelar pedido",
"message": "¿Estás seguro de que deseas cancelar este pedido?", "message": "¿Está seguro de que desea cancelar este pedido?",
"confirm": "Cancelar pedido", "confirm": "Cancelar",
"cancelling": "Cancelando..." "cancelling": "Cancelando..."
}, },
"processing": "El pedido se está completando...", "processing": "El pedido se está completando..."
}; };

View File

@@ -8,9 +8,10 @@ export default {
"articleNumber": "Número de artículo", "articleNumber": "Número de artículo",
"manufacturer": "Fabricante", "manufacturer": "Fabricante",
"inclVat": "incl. {{vat}}% IVA", "inclVat": "incl. {{vat}}% IVA",
"inclVatSimple": "incl. IVA",
"priceUnit": "{{price}}/{{unit}}", "priceUnit": "{{price}}/{{unit}}",
"new": "Nuevo", "new": "Nuevo",
"weeks": "semanas", "weeks": "Semanas",
"arriving": "Llegada:", "arriving": "Llegada:",
"inclVatFooter": "incl. {{vat}}% IVA,*", "inclVatFooter": "incl. {{vat}}% IVA,*",
"availability": "Disponibilidad", "availability": "Disponibilidad",
@@ -25,7 +26,7 @@ export default {
"pickupPrice": "Precio de recogida: 19,90 € por esqueje.", "pickupPrice": "Precio de recogida: 19,90 € por esqueje.",
"consistsOf": "Consiste en:", "consistsOf": "Consiste en:",
"loadingComponentDetails": "{{index}}. Cargando detalles del componente...", "loadingComponentDetails": "{{index}}. Cargando detalles del componente...",
"loadingProduct": "Producto cargando...", "loadingProduct": "Cargando producto...",
"individualPriceTotal": "Precio individual total:", "individualPriceTotal": "Precio individual total:",
"setPrice": "Precio del set:", "setPrice": "Precio del set:",
"yourSavings": "Tus ahorros:", "yourSavings": "Tus ahorros:",

View File

@@ -0,0 +1,61 @@
export default {
"questionTitle": "Pregunta sobre el producto",
"questionSubtitle": "¿Tiene alguna pregunta sobre este producto? Estamos encantados de ayudarle.",
"questionSuccess": "¡Gracias por su pregunta! Nos pondremos en contacto con usted lo antes posible.",
"nameLabel": "Nombre",
"namePlaceholder": "Su nombre",
"emailLabel": "Correo electrónico",
"emailPlaceholder": "su.email@ejemplo.com",
"questionLabel": "Su pregunta",
"questionPlaceholder": "Describa su pregunta sobre este producto...",
"photosLabelQuestion": "Adjunte fotos a su pregunta (opcional)",
"submitQuestion": "Enviar pregunta",
"sending": "Enviando...",
"ratingTitle": "Calificar producto",
"ratingSubtitle": "Comparta su experiencia con este producto y ayude a otros clientes a tomar su decisión.",
"ratingSuccess": "¡Gracias por su reseña! Se publicará después de la verificación.",
"emailHelper": "Su correo electrónico no será publicado",
"ratingLabel": "Calificación *",
"pleaseRate": "Por favor califique",
"ratingStars": "{{rating}} de 5 estrellas",
"reviewLabel": "Su reseña (opcional)",
"reviewPlaceholder": "Describa sus experiencias con este producto...",
"photosLabelRating": "Adjunte fotos a su reseña (opcional)",
"submitRating": "Enviar reseña",
"errorGeneric": "Ocurrió un error",
"errorPhotos": "Error al procesar las fotos",
"availabilityTitle": "Solicitar disponibilidad",
"availabilitySubtitle": "Este producto no está disponible actualmente. Le informaremos tan pronto como vuelva a estar en stock.",
"availabilitySuccessEmail": "¡Gracias por su solicitud! Le notificaremos por correo electrónico tan pronto como el producto esté disponible nuevamente.",
"availabilitySuccessTelegram": "¡Gracias por su solicitud! Le notificaremos vía Telegram tan pronto como el producto esté disponible nuevamente.",
"notificationMethodLabel": "¿Cómo desea ser notificado?",
"telegramBotLabel": "Bot de Telegram",
"telegramIdLabel": "ID de Telegram",
"telegramPlaceholder": "@suNombreTelegram o ID de Telegram",
"telegramHelper": "Ingrese su nombre de usuario de Telegram (con @) o ID de Telegram",
"messageLabel": "Mensaje (opcional)",
"messagePlaceholder": "Información adicional o preguntas...",
"submitAvailability": "Solicitar disponibilidad",
"photoUploadSelect": "Seleccionar fotos",
"photoUploadErrorMaxFiles": "Máximo {{max}} archivos permitidos",
"photoUploadErrorFileType": "Solo se permiten archivos de imagen (JPEG, PNG, GIF, WebP)",
"photoUploadErrorFileSize": "Archivo demasiado grande. Máximo: {{maxSize}}MB",
"photoUploadSelectedFiles": "{{count}} archivo(s) seleccionado(s)",
"photoUploadCompressed": "(comprimido para subir)",
"photoUploadRemove": "Eliminar imagen",
"photoUploadLabelDefault": "Adjuntar fotos (opcional)",
"shareTitle": "Compartir",
"shareEmbed": "Insertar",
"shareCopyLink": "Copiar enlace",
"shareSuccessEmbed": "¡Código de inserción copiado al portapapeles!",
"shareErrorEmbed": "Error al copiar el código de inserción",
"shareSuccessLink": "¡Enlace copiado al portapapeles!",
"shareWhatsAppText": "Mira este producto: {{name}}",
"shareTelegramText": "Mira este producto: {{name}}",
"shareEmailSubject": "Recomendación de producto",
"shareEmailBody": "Hola,\n\nQuisiera recomendarte este producto:\n\n{{name}}\n{{url}}\n\nSaludos cordiales"
};

View File

@@ -1,10 +1,11 @@
export default { export default {
"login": "Connexion", "login": "Connexion",
"register": "S'inscrire", "register": "Inscription",
"logout": "Déconnexion", "logout": "Déconnexion",
"profile": "Profil", "profile": "Profil",
"email": "Email", "email": "Email",
"password": "Mot de passe", "password": "Mot de passe",
"newPassword": "Nouveau mot de passe",
"confirmPassword": "Confirmer le mot de passe", "confirmPassword": "Confirmer le mot de passe",
"forgotPassword": "Mot de passe oublié ?", "forgotPassword": "Mot de passe oublié ?",
"loginWithGoogle": "Se connecter avec Google", "loginWithGoogle": "Se connecter avec Google",
@@ -13,6 +14,7 @@ export default {
"privacyPolicy": "Politique de confidentialité", "privacyPolicy": "Politique de confidentialité",
"passwordMinLength": "Le mot de passe doit contenir au moins 8 caractères", "passwordMinLength": "Le mot de passe doit contenir au moins 8 caractères",
"newPasswordMinLength": "Le nouveau mot de passe doit contenir au moins 8 caractères", "newPasswordMinLength": "Le nouveau mot de passe doit contenir au moins 8 caractères",
"backToHome": "Retour à la page d'accueil",
"menu": { "menu": {
"profile": "Profil", "profile": "Profil",
"myProfile": "Mon profil", "myProfile": "Mon profil",
@@ -21,5 +23,28 @@ export default {
"settings": "Paramètres", "settings": "Paramètres",
"adminDashboard": "Tableau de bord Admin", "adminDashboard": "Tableau de bord Admin",
"adminUsers": "Utilisateurs Admin" "adminUsers": "Utilisateurs Admin"
},
"resetPassword": {
"title": "Réinitialiser le mot de passe",
"button": "Réinitialiser le mot de passe",
"success": "Votre mot de passe a été réinitialisé avec succès ! Vous serez redirigé vers la connexion sous peu...",
"invalidToken": "Aucun jeton valide trouvé. Veuillez utiliser le lien de votre email.",
"error": "Erreur lors de la réinitialisation du mot de passe",
"emailSent": "Un lien pour réinitialiser votre mot de passe a été envoyé à votre adresse email.",
"emailError": "Erreur lors de l'envoi de l'email"
},
"errors": {
"fillAllFields": "Veuillez remplir tous les champs",
"invalidEmail": "Veuillez entrer une adresse email valide",
"passwordsNotMatch": "Les mots de passe ne correspondent pas",
"passwordsNotMatchShort": "Les mots de passe ne correspondent pas",
"enterEmail": "Veuillez entrer votre adresse email",
"loginFailed": "Échec de la connexion",
"registerFailed": "Échec de l'inscription",
"googleLoginFailed": "Échec de la connexion Google",
"emailExists": "Un utilisateur avec cette adresse email existe déjà. Veuillez utiliser une autre adresse email ou vous connecter."
},
"success": {
"registerComplete": "Inscription réussie. Vous pouvez maintenant vous connecter."
} }
}; };

View File

@@ -3,6 +3,7 @@ import navigation from './navigation.js';
import auth from './auth.js'; import auth from './auth.js';
import cart from './cart.js'; import cart from './cart.js';
import product from './product.js'; import product from './product.js';
import productDialogs from './productDialogs.js';
import search from './search.js'; import search from './search.js';
import sorting from './sorting.js'; import sorting from './sorting.js';
import chat from './chat.js'; import chat from './chat.js';
@@ -18,6 +19,7 @@ import pages from './pages.js';
import orders from './orders.js'; import orders from './orders.js';
import settings from './settings.js'; import settings from './settings.js';
import common from './common.js'; import common from './common.js';
import kitConfig from './kitConfig.js';
import legalDatenschutzBasic from './legal-datenschutz-basic.js'; import legalDatenschutzBasic from './legal-datenschutz-basic.js';
import legalDatenschutzCustomer from './legal-datenschutz-customer.js'; import legalDatenschutzCustomer from './legal-datenschutz-customer.js';
import legalDatenschutzGoogleOrders from './legal-datenschutz-google-orders.js'; import legalDatenschutzGoogleOrders from './legal-datenschutz-google-orders.js';
@@ -35,6 +37,7 @@ export default {
"auth": auth, "auth": auth,
"cart": cart, "cart": cart,
"product": product, "product": product,
"productDialogs": productDialogs,
"search": search, "search": search,
"sorting": sorting, "sorting": sorting,
"chat": chat, "chat": chat,
@@ -50,6 +53,7 @@ export default {
"orders": orders, "orders": orders,
"settings": settings, "settings": settings,
"common": common, "common": common,
"kitConfig": kitConfig,
"legalDatenschutzBasic": legalDatenschutzBasic, "legalDatenschutzBasic": legalDatenschutzBasic,
"legalDatenschutzCustomer": legalDatenschutzCustomer, "legalDatenschutzCustomer": legalDatenschutzCustomer,
"legalDatenschutzGoogleOrders": legalDatenschutzGoogleOrders, "legalDatenschutzGoogleOrders": legalDatenschutzGoogleOrders,

View File

@@ -0,0 +1,43 @@
export default {
"pageTitle": "🌱 Configurateur Growbox",
"pageSubtitle": "Assemblez votre installation de culture d'intérieur parfaite",
"bundleDiscountTitle": "🎯 Profitez d'une remise sur le pack !",
"loadingProducts": "Chargement des produits growbox...",
"loadingLighting": "Chargement des produits d'éclairage...",
"loadingVentilation": "Chargement des produits de ventilation...",
"loadingExtras": "Chargement des extras...",
"noProductsAvailable": "Aucun produit disponible pour cette taille",
"noLightingAvailable": "Aucun éclairage adapté disponible pour la taille de tente {{shape}}.",
"noVentilationAvailable": "Aucune ventilation adaptée disponible pour la taille de tente {{shape}}.",
"noExtrasAvailable": "Aucun extra disponible",
"selectShapeTitle": "1. Sélectionnez la forme de la growbox",
"selectShapeSubtitle": "Sélectionnez d'abord la surface de base de votre growbox",
"selectProductTitle": "2. Sélectionnez le produit growbox",
"selectProductSubtitle": "Choisissez le produit adapté pour votre growbox {{shape}}",
"selectLightingTitle": "3. Choisissez l'éclairage",
"selectLightingTitleShape": "3. Choisissez l'éclairage - {{shape}}",
"selectLightingSubtitle": "Veuillez d'abord sélectionner une taille de tente.",
"selectVentilationTitle": "4. Sélectionnez la ventilation",
"selectVentilationTitleShape": "4. Sélectionnez la ventilation - {{shape}}",
"selectVentilationSubtitle": "Veuillez d'abord sélectionner une taille de tente.",
"selectExtrasTitle": "5. Ajoutez des extras (optionnel)",
"yourConfiguration": "🎯 Votre configuration",
"growboxLabel": "Growbox : {{name}}",
"lightingLabel": "Éclairage : {{name}}",
"ventilationLabel": "Ventilation : {{name}}",
"extraLabel": "Extra : {{name}}",
"totalPrice": "Prix total :",
"addToCart": "Ajouter au panier",
"selected": "✓ Sélectionné",
"notDeliverable": "Non livrable",
"noPrice": "Pas de prix",
"setName": "Set Growbox - {{shape}}",
"description60x60": "Compact - idéal pour les petits espaces",
"description80x80": "Moyen - équilibre parfait",
"description100x100": "Grand - pour cultivateurs expérimentés",
"description120x60": "Rectangulaire - utilisation maximale de l'espace",
"plants1to2": "1-2 plantes",
"plants2to4": "2-4 plantes",
"plants4to6": "4-6 plantes",
"plants3to6": "3-6 plantes"
};

View File

@@ -1,8 +1,9 @@
export default { export default {
"status": { "status": {
"new": "En cours", "new": "en cours",
"pending": "Nouveau", "pending": "Nouveau",
"processing": "En cours", "processing": "en cours",
"paid": "Payé",
"cancelled": "Annulé", "cancelled": "Annulé",
"shipped": "Expédié", "shipped": "Expédié",
"delivered": "Livré", "delivered": "Livré",
@@ -24,6 +25,7 @@ export default {
"cancelOrder": "Annuler la commande" "cancelOrder": "Annuler la commande"
}, },
"noOrders": "Vous n'avez pas encore passé de commandes.", "noOrders": "Vous n'avez pas encore passé de commandes.",
"trackShipment": "Suivre l'envoi",
"details": { "details": {
"title": "Détails de la commande : {{orderId}}", "title": "Détails de la commande : {{orderId}}",
"deliveryAddress": "Adresse de livraison", "deliveryAddress": "Adresse de livraison",
@@ -36,14 +38,13 @@ export default {
"item": "Article", "item": "Article",
"quantity": "Quantité", "quantity": "Quantité",
"price": "Prix", "price": "Prix",
"vat": "TVA",
"total": "Total", "total": "Total",
"cancelOrder": "Annuler la commande" "cancelOrder": "Annuler la commande"
}, },
"cancelConfirm": { "cancelConfirm": {
"title": "Annuler la commande", "title": "Annuler la commande",
"message": "Êtes-vous sûr de vouloir annuler cette commande ?", "message": "Êtes-vous sûr de vouloir annuler cette commande ?",
"confirm": "Annuler la commande", "confirm": "Annuler",
"cancelling": "Annulation en cours..." "cancelling": "Annulation en cours..."
}, },
"processing": "La commande est en cours de traitement..." "processing": "La commande est en cours de traitement..."

View File

@@ -8,9 +8,10 @@ export default {
"articleNumber": "Numéro d'article", "articleNumber": "Numéro d'article",
"manufacturer": "Fabricant", "manufacturer": "Fabricant",
"inclVat": "TTC {{vat}}%", "inclVat": "TTC {{vat}}%",
"inclVatSimple": "TTC",
"priceUnit": "{{price}}/{{unit}}", "priceUnit": "{{price}}/{{unit}}",
"new": "Nouveau", "new": "Nouveau",
"weeks": "semaines", "weeks": "Semaines",
"arriving": "Arrivée :", "arriving": "Arrivée :",
"inclVatFooter": "TTC {{vat}}%,*", "inclVatFooter": "TTC {{vat}}%,*",
"availability": "Disponibilité", "availability": "Disponibilité",
@@ -25,7 +26,7 @@ export default {
"pickupPrice": "Prix de retrait : 19,90 € par bouture.", "pickupPrice": "Prix de retrait : 19,90 € par bouture.",
"consistsOf": "Composé de :", "consistsOf": "Composé de :",
"loadingComponentDetails": "{{index}}. Chargement des détails du composant...", "loadingComponentDetails": "{{index}}. Chargement des détails du composant...",
"loadingProduct": "Le produit est en cours de chargement...", "loadingProduct": "Chargement du produit...",
"individualPriceTotal": "Prix individuel total :", "individualPriceTotal": "Prix individuel total :",
"setPrice": "Prix du lot :", "setPrice": "Prix du lot :",
"yourSavings": "Vos économies :", "yourSavings": "Vos économies :",

View File

@@ -0,0 +1,61 @@
export default {
"questionTitle": "Question sur le produit",
"questionSubtitle": "Vous avez une question sur ce produit ? Nous sommes heureux de vous aider.",
"questionSuccess": "Merci pour votre question ! Nous vous répondrons dès que possible.",
"nameLabel": "Nom",
"namePlaceholder": "Votre nom",
"emailLabel": "Email",
"emailPlaceholder": "votre.email@exemple.com",
"questionLabel": "Votre question",
"questionPlaceholder": "Décrivez votre question à propos de ce produit...",
"photosLabelQuestion": "Joindre des photos à votre question (optionnel)",
"submitQuestion": "Envoyer la question",
"sending": "Envoi en cours...",
"ratingTitle": "Noter le produit",
"ratingSubtitle": "Partagez votre expérience avec ce produit et aidez les autres clients à prendre leur décision.",
"ratingSuccess": "Merci pour votre avis ! Il sera publié après vérification.",
"emailHelper": "Votre email ne sera pas publié",
"ratingLabel": "Note *",
"pleaseRate": "Veuillez noter",
"ratingStars": "{{rating}} sur 5 étoiles",
"reviewLabel": "Votre avis (optionnel)",
"reviewPlaceholder": "Décrivez vos expériences avec ce produit...",
"photosLabelRating": "Joindre des photos à votre avis (optionnel)",
"submitRating": "Soumettre l'avis",
"errorGeneric": "Une erreur est survenue",
"errorPhotos": "Erreur lors du traitement des photos",
"availabilityTitle": "Demander la disponibilité",
"availabilitySubtitle": "Ce produit est actuellement indisponible. Nous serons heureux de vous informer dès qu'il sera de nouveau en stock.",
"availabilitySuccessEmail": "Merci pour votre demande ! Nous vous informerons par email dès que le produit sera à nouveau disponible.",
"availabilitySuccessTelegram": "Merci pour votre demande ! Nous vous informerons via Telegram dès que le produit sera à nouveau disponible.",
"notificationMethodLabel": "Comment souhaitez-vous être informé ?",
"telegramBotLabel": "Bot Telegram",
"telegramIdLabel": "ID Telegram",
"telegramPlaceholder": "@votreNomTelegram ou ID Telegram",
"telegramHelper": "Entrez votre nom d'utilisateur Telegram (avec @) ou votre ID Telegram",
"messageLabel": "Message (optionnel)",
"messagePlaceholder": "Informations supplémentaires ou questions...",
"submitAvailability": "Demander la disponibilité",
"photoUploadSelect": "Sélectionner des photos",
"photoUploadErrorMaxFiles": "Maximum {{max}} fichiers autorisés",
"photoUploadErrorFileType": "Seuls les fichiers image (JPEG, PNG, GIF, WebP) sont autorisés",
"photoUploadErrorFileSize": "Fichier trop volumineux. Maximum : {{maxSize}}Mo",
"photoUploadSelectedFiles": "{{count}} fichier(s) sélectionné(s)",
"photoUploadCompressed": "(compressé pour l'envoi)",
"photoUploadRemove": "Supprimer l'image",
"photoUploadLabelDefault": "Joindre des photos (optionnel)",
"shareTitle": "Partager",
"shareEmbed": "Intégrer",
"shareCopyLink": "Copier le lien",
"shareSuccessEmbed": "Code d'intégration copié dans le presse-papiers !",
"shareErrorEmbed": "Erreur lors de la copie du code d'intégration",
"shareSuccessLink": "Lien copié dans le presse-papiers !",
"shareWhatsAppText": "Découvrez ce produit : {{name}}",
"shareTelegramText": "Découvrez ce produit : {{name}}",
"shareEmailSubject": "Recommandation de produit",
"shareEmailBody": "Bonjour,\n\nJe souhaite vous recommander ce produit :\n\n{{name}}\n{{url}}\n\nCordialement"
};

View File

@@ -5,6 +5,7 @@ export default {
"profile": "Profil", "profile": "Profil",
"email": "Email", "email": "Email",
"password": "Lozinka", "password": "Lozinka",
"newPassword": "Nova lozinka",
"confirmPassword": "Potvrdi lozinku", "confirmPassword": "Potvrdi lozinku",
"forgotPassword": "Zaboravili ste lozinku?", "forgotPassword": "Zaboravili ste lozinku?",
"loginWithGoogle": "Prijavite se putem Googlea", "loginWithGoogle": "Prijavite se putem Googlea",
@@ -13,6 +14,7 @@ export default {
"privacyPolicy": "Pravila privatnosti", "privacyPolicy": "Pravila privatnosti",
"passwordMinLength": "Lozinka mora imati najmanje 8 znakova", "passwordMinLength": "Lozinka mora imati najmanje 8 znakova",
"newPasswordMinLength": "Nova lozinka mora imati najmanje 8 znakova", "newPasswordMinLength": "Nova lozinka mora imati najmanje 8 znakova",
"backToHome": "Natrag na početnu stranicu",
"menu": { "menu": {
"profile": "Profil", "profile": "Profil",
"myProfile": "Moj profil", "myProfile": "Moj profil",
@@ -21,5 +23,28 @@ export default {
"settings": "Postavke", "settings": "Postavke",
"adminDashboard": "Admin nadzorna ploča", "adminDashboard": "Admin nadzorna ploča",
"adminUsers": "Admin korisnici" "adminUsers": "Admin korisnici"
},
"resetPassword": {
"title": "Resetiraj lozinku",
"button": "Resetiraj lozinku",
"success": "Vaša lozinka je uspješno resetirana! Uskoro ćete biti preusmjereni na prijavu...",
"invalidToken": "Nije pronađen valjani token. Molimo koristite link iz vaše e-pošte.",
"error": "Pogreška pri resetiranju lozinke",
"emailSent": "Link za resetiranje lozinke poslan je na vašu e-mail adresu.",
"emailError": "Pogreška pri slanju e-pošte"
},
"errors": {
"fillAllFields": "Molimo ispunite sva polja",
"invalidEmail": "Molimo unesite valjanu e-mail adresu",
"passwordsNotMatch": "Lozinke se ne podudaraju",
"passwordsNotMatchShort": "Lozinke se ne podudaraju",
"enterEmail": "Molimo unesite vašu e-mail adresu",
"loginFailed": "Prijava nije uspjela",
"registerFailed": "Registracija nije uspjela",
"googleLoginFailed": "Prijava putem Googlea nije uspjela",
"emailExists": "Korisnik s ovom e-mail adresom već postoji. Molimo koristite drugu e-mail adresu ili se prijavite."
},
"success": {
"registerComplete": "Registracija uspješna. Sada se možete prijaviti."
} }
}; };

View File

@@ -3,6 +3,7 @@ import navigation from './navigation.js';
import auth from './auth.js'; import auth from './auth.js';
import cart from './cart.js'; import cart from './cart.js';
import product from './product.js'; import product from './product.js';
import productDialogs from './productDialogs.js';
import search from './search.js'; import search from './search.js';
import sorting from './sorting.js'; import sorting from './sorting.js';
import chat from './chat.js'; import chat from './chat.js';
@@ -18,6 +19,7 @@ import pages from './pages.js';
import orders from './orders.js'; import orders from './orders.js';
import settings from './settings.js'; import settings from './settings.js';
import common from './common.js'; import common from './common.js';
import kitConfig from './kitConfig.js';
import legalDatenschutzBasic from './legal-datenschutz-basic.js'; import legalDatenschutzBasic from './legal-datenschutz-basic.js';
import legalDatenschutzCustomer from './legal-datenschutz-customer.js'; import legalDatenschutzCustomer from './legal-datenschutz-customer.js';
import legalDatenschutzGoogleOrders from './legal-datenschutz-google-orders.js'; import legalDatenschutzGoogleOrders from './legal-datenschutz-google-orders.js';
@@ -35,6 +37,7 @@ export default {
"auth": auth, "auth": auth,
"cart": cart, "cart": cart,
"product": product, "product": product,
"productDialogs": productDialogs,
"search": search, "search": search,
"sorting": sorting, "sorting": sorting,
"chat": chat, "chat": chat,
@@ -50,6 +53,7 @@ export default {
"orders": orders, "orders": orders,
"settings": settings, "settings": settings,
"common": common, "common": common,
"kitConfig": kitConfig,
"legalDatenschutzBasic": legalDatenschutzBasic, "legalDatenschutzBasic": legalDatenschutzBasic,
"legalDatenschutzCustomer": legalDatenschutzCustomer, "legalDatenschutzCustomer": legalDatenschutzCustomer,
"legalDatenschutzGoogleOrders": legalDatenschutzGoogleOrders, "legalDatenschutzGoogleOrders": legalDatenschutzGoogleOrders,

View File

@@ -0,0 +1,43 @@
export default {
"pageTitle": "🌱 Konfigurator Growboxa",
"pageSubtitle": "Sastavite svoju savršenu unutarnju grow postavu",
"bundleDiscountTitle": "🎯 Osigurajte popust na paket!",
"loadingProducts": "Učitavanje growbox proizvoda...",
"loadingLighting": "Učitavanje proizvoda za osvjetljenje...",
"loadingVentilation": "Učitavanje proizvoda za ventilaciju...",
"loadingExtras": "Učitavanje dodataka...",
"noProductsAvailable": "Nema dostupnih proizvoda za ovu veličinu",
"noLightingAvailable": "Nema prikladnih svjetala za veličinu šatora {{shape}}.",
"noVentilationAvailable": "Nema prikladne ventilacije za veličinu šatora {{shape}}.",
"noExtrasAvailable": "Nema dodataka",
"selectShapeTitle": "1. Odaberite oblik growboxa",
"selectShapeSubtitle": "Prvo odaberite osnovnu površinu vašeg growboxa",
"selectProductTitle": "2. Odaberite growbox proizvod",
"selectProductSubtitle": "Odaberite pravi proizvod za vaš {{shape}} growbox",
"selectLightingTitle": "3. Odaberite osvjetljenje",
"selectLightingTitleShape": "3. Odaberite osvjetljenje - {{shape}}",
"selectLightingSubtitle": "Molimo prvo odaberite veličinu šatora.",
"selectVentilationTitle": "4. Odaberite ventilaciju",
"selectVentilationTitleShape": "4. Odaberite ventilaciju - {{shape}}",
"selectVentilationSubtitle": "Molimo prvo odaberite veličinu šatora.",
"selectExtrasTitle": "5. Dodajte dodatke (opcionalno)",
"yourConfiguration": "🎯 Vaša konfiguracija",
"growboxLabel": "Growbox: {{name}}",
"lightingLabel": "Osvjetljenje: {{name}}",
"ventilationLabel": "Ventilacija: {{name}}",
"extraLabel": "Dodatak: {{name}}",
"totalPrice": "Ukupna cijena:",
"addToCart": "Dodaj u košaricu",
"selected": "✓ Odabrano",
"notDeliverable": "Nije dostupno za dostavu",
"noPrice": "Nema cijene",
"setName": "Growbox set - {{shape}}",
"description60x60": "Kompaktan - idealan za male prostore",
"description80x80": "Srednji - savršen balans",
"description100x100": "Veliki - za iskusne uzgajivače",
"description120x60": "Pravokutni - maksimalno iskorištavanje prostora",
"plants1to2": "1-2 biljke",
"plants2to4": "2-4 biljke",
"plants4to6": "4-6 biljaka",
"plants3to6": "3-6 biljaka"
};

View File

@@ -1,8 +1,9 @@
export default { export default {
"status": { "status": {
"new": "U tijeku", "new": "u tijeku",
"pending": "Novo", "pending": "Novo",
"processing": "U tijeku", "processing": "u tijeku",
"paid": "Plaćeno",
"cancelled": "Otkazano", "cancelled": "Otkazano",
"shipped": "Poslano", "shipped": "Poslano",
"delivered": "Isporučeno", "delivered": "Isporučeno",
@@ -23,7 +24,8 @@ export default {
"viewDetails": "Pogledaj detalje", "viewDetails": "Pogledaj detalje",
"cancelOrder": "Otkaži narudžbu" "cancelOrder": "Otkaži narudžbu"
}, },
"noOrders": "Još niste napravili nijednu narudžbu.", "noOrders": "Još niste izvršili nijednu narudžbu.",
"trackShipment": "Prati pošiljku",
"details": { "details": {
"title": "Detalji narudžbe: {{orderId}}", "title": "Detalji narudžbe: {{orderId}}",
"deliveryAddress": "Adresa dostave", "deliveryAddress": "Adresa dostave",
@@ -36,15 +38,14 @@ export default {
"item": "Artikl", "item": "Artikl",
"quantity": "Količina", "quantity": "Količina",
"price": "Cijena", "price": "Cijena",
"vat": "PDV",
"total": "Ukupno", "total": "Ukupno",
"cancelOrder": "Otkaži narudžbu" "cancelOrder": "Otkaži narudžbu"
}, },
"cancelConfirm": { "cancelConfirm": {
"title": "Otkaži narudžbu", "title": "Otkaži narudžbu",
"message": "Jeste li sigurni da želite otkazati ovu narudžbu?", "message": "Jeste li sigurni da želite otkazati ovu narudžbu?",
"confirm": "Otkaži narudžbu", "confirm": "Otkaži",
"cancelling": "Otkazivanje..." "cancelling": "Otkazivanje..."
}, },
"processing": "Narudžba se dovršava...", "processing": "Narudžba se obrađuje..."
}; };

View File

@@ -3,14 +3,15 @@ export default {
"loadingDescription": "Učitavanje opisa proizvoda...", "loadingDescription": "Učitavanje opisa proizvoda...",
"notFound": "Proizvod nije pronađen", "notFound": "Proizvod nije pronađen",
"notFoundDescription": "Proizvod koji tražite ne postoji ili je uklonjen.", "notFoundDescription": "Proizvod koji tražite ne postoji ili je uklonjen.",
"backToHome": "Povratak na početnu stranicu", "backToHome": "Natrag na početnu stranicu",
"error": "Greška", "error": "Greška",
"articleNumber": "Broj artikla", "articleNumber": "Broj artikla",
"manufacturer": "Proizvođač", "manufacturer": "Proizvođač",
"inclVat": "uključujući {{vat}}% PDV-a", "inclVat": "uključujući {{vat}}% PDV-a",
"inclVatSimple": "uključujući PDV",
"priceUnit": "{{price}}/{{unit}}", "priceUnit": "{{price}}/{{unit}}",
"new": "Novo", "new": "Novo",
"weeks": "tjedana", "weeks": "Tjedni",
"arriving": "Dolazak:", "arriving": "Dolazak:",
"inclVatFooter": "uključujući {{vat}}% PDV-a,*", "inclVatFooter": "uključujući {{vat}}% PDV-a,*",
"availability": "Dostupnost", "availability": "Dostupnost",
@@ -25,7 +26,7 @@ export default {
"pickupPrice": "Cijena preuzimanja: 19,90 € po reznici.", "pickupPrice": "Cijena preuzimanja: 19,90 € po reznici.",
"consistsOf": "Sastoji se od:", "consistsOf": "Sastoji se od:",
"loadingComponentDetails": "{{index}}. Učitavanje detalja komponente...", "loadingComponentDetails": "{{index}}. Učitavanje detalja komponente...",
"loadingProduct": "Proizvod se učitava...", "loadingProduct": "Učitavanje proizvoda...",
"individualPriceTotal": "Ukupna pojedinačna cijena:", "individualPriceTotal": "Ukupna pojedinačna cijena:",
"setPrice": "Cijena seta:", "setPrice": "Cijena seta:",
"yourSavings": "Vaša ušteda:", "yourSavings": "Vaša ušteda:",
@@ -38,7 +39,7 @@ export default {
"filteredOneProduct": "{{filtered}} od 1 proizvoda", "filteredOneProduct": "{{filtered}} od 1 proizvoda",
"xOfYProducts": "{{x}} od {{y}} proizvoda" "xOfYProducts": "{{x}} od {{y}} proizvoda"
}, },
"removeFiltersToSee": "Uklonite filtere da vidite proizvode", "removeFiltersToSee": "Uklonite filtre da vidite proizvode",
"outOfStock": "Nema na skladištu", "outOfStock": "Nema na skladištu",
"fromXProducts": "od {{count}} proizvoda", "fromXProducts": "od {{count}} proizvoda",
"discount": { "discount": {

View File

@@ -0,0 +1,61 @@
export default {
"questionTitle": "Pitanje o proizvodu",
"questionSubtitle": "Imate li pitanje o ovom proizvodu? Rado ćemo vam pomoći.",
"questionSuccess": "Hvala na vašem pitanju! Javili smo vam se što je prije moguće.",
"nameLabel": "Ime",
"namePlaceholder": "Vaše ime",
"emailLabel": "Email",
"emailPlaceholder": "your.email@example.com",
"questionLabel": "Vaše pitanje",
"questionPlaceholder": "Opišite svoje pitanje o ovom proizvodu...",
"photosLabelQuestion": "Priložite fotografije uz svoje pitanje (opcionalno)",
"submitQuestion": "Pošalji pitanje",
"sending": "Šalje se...",
"ratingTitle": "Ocijenite proizvod",
"ratingSubtitle": "Podijelite svoje iskustvo s ovim proizvodom i pomozite drugim kupcima u odluci.",
"ratingSuccess": "Hvala na vašoj recenziji! Bit će objavljena nakon provjere.",
"emailHelper": "Vaš email neće biti objavljen",
"ratingLabel": "Ocjena *",
"pleaseRate": "Molimo ocijenite",
"ratingStars": "{{rating}} od 5 zvjezdica",
"reviewLabel": "Vaša recenzija (opcionalno)",
"reviewPlaceholder": "Opišite svoja iskustva s ovim proizvodom...",
"photosLabelRating": "Priložite fotografije uz svoju recenziju (opcionalno)",
"submitRating": "Pošalji recenziju",
"errorGeneric": "Došlo je do pogreške",
"errorPhotos": "Pogreška pri obradi fotografija",
"availabilityTitle": "Zatražite dostupnost",
"availabilitySubtitle": "Ovaj proizvod trenutno nije dostupan. Obavijestit ćemo vas čim ponovno bude na skladištu.",
"availabilitySuccessEmail": "Hvala na vašem zahtjevu! Obavijestit ćemo vas putem emaila čim proizvod ponovno bude dostupan.",
"availabilitySuccessTelegram": "Hvala na vašem zahtjevu! Obavijestit ćemo vas putem Telegrama čim proizvod ponovno bude dostupan.",
"notificationMethodLabel": "Kako želite biti obaviješteni?",
"telegramBotLabel": "Telegram Bot",
"telegramIdLabel": "Telegram ID",
"telegramPlaceholder": "@yourTelegramName or Telegram ID",
"telegramHelper": "Unesite svoje Telegram korisničko ime (s @) ili Telegram ID",
"messageLabel": "Poruka (opcionalno)",
"messagePlaceholder": "Dodatne informacije ili pitanja...",
"submitAvailability": "Zatražite dostupnost",
"photoUploadSelect": "Odaberite fotografije",
"photoUploadErrorMaxFiles": "Dozvoljeno maksimalno {{max}} datoteka",
"photoUploadErrorFileType": "Dozvoljene su samo slikovne datoteke (JPEG, PNG, GIF, WebP)",
"photoUploadErrorFileSize": "Datoteka je prevelika. Maksimum: {{maxSize}}MB",
"photoUploadSelectedFiles": "{{count}} datoteka je odabrano",
"photoUploadCompressed": "(komprimirano za prijenos)",
"photoUploadRemove": "Ukloni sliku",
"photoUploadLabelDefault": "Priložite fotografije (opcionalno)",
"shareTitle": "Podijeli",
"shareEmbed": "Ugradi",
"shareCopyLink": "Kopiraj link",
"shareSuccessEmbed": "Kod za ugradnju kopiran u međuspremnik!",
"shareErrorEmbed": "Pogreška pri kopiranju koda za ugradnju",
"shareSuccessLink": "Link kopiran u međuspremnik!",
"shareWhatsAppText": "Pogledajte ovaj proizvod: {{name}}",
"shareTelegramText": "Pogledajte ovaj proizvod: {{name}}",
"shareEmailSubject": "Preporuka proizvoda",
"shareEmailBody": "Pozdrav,\n\nŽelio/la bih vam preporučiti ovaj proizvod:\n\n{{name}}\n{{url}}\n\nSrdačan pozdrav"
};

View File

@@ -5,6 +5,7 @@ export default {
"profile": "Profil", "profile": "Profil",
"email": "Email", "email": "Email",
"password": "Jelszó", "password": "Jelszó",
"newPassword": "Új jelszó",
"confirmPassword": "Jelszó megerősítése", "confirmPassword": "Jelszó megerősítése",
"forgotPassword": "Elfelejtett jelszó?", "forgotPassword": "Elfelejtett jelszó?",
"loginWithGoogle": "Bejelentkezés Google-lal", "loginWithGoogle": "Bejelentkezés Google-lal",
@@ -13,13 +14,37 @@ export default {
"privacyPolicy": "Adatvédelmi szabályzatot", "privacyPolicy": "Adatvédelmi szabályzatot",
"passwordMinLength": "A jelszónak legalább 8 karakter hosszúnak kell lennie", "passwordMinLength": "A jelszónak legalább 8 karakter hosszúnak kell lennie",
"newPasswordMinLength": "Az új jelszónak legalább 8 karakter hosszúnak kell lennie", "newPasswordMinLength": "Az új jelszónak legalább 8 karakter hosszúnak kell lennie",
"backToHome": "Vissza a kezdőlapra",
"menu": { "menu": {
"profile": "Profil", "profile": "Profil",
"myProfile": "Saját profilom", "myProfile": "Saját profil",
"checkout": "Pénztár", "checkout": "Pénztár",
"orders": "Rendelések", "orders": "Rendelések",
"settings": "Beállítások", "settings": "Beállítások",
"adminDashboard": "Admin Vezérlőpult", "adminDashboard": "Admin Vezérlőpult",
"adminUsers": "Admin Felhasználók" "adminUsers": "Admin Felhasználók"
},
"resetPassword": {
"title": "Jelszó visszaállítása",
"button": "Jelszó visszaállítása",
"success": "A jelszavad sikeresen visszaállítva! Hamarosan átirányítunk a bejelentkezéshez...",
"invalidToken": "Érvényes token nem található. Kérjük, használd az emailedben található linket.",
"error": "Hiba történt a jelszó visszaállítása során",
"emailSent": "Egy link a jelszó visszaállításához elküldésre került az email címedre.",
"emailError": "Hiba történt az email küldése során"
},
"errors": {
"fillAllFields": "Kérjük, tölts ki minden mezőt",
"invalidEmail": "Kérjük, adj meg egy érvényes email címet",
"passwordsNotMatch": "A jelszavak nem egyeznek",
"passwordsNotMatchShort": "A jelszavak nem egyeznek",
"enterEmail": "Kérjük, add meg az email címed",
"loginFailed": "Bejelentkezés sikertelen",
"registerFailed": "Regisztráció sikertelen",
"googleLoginFailed": "Google bejelentkezés sikertelen",
"emailExists": "Már létezik felhasználó ezzel az email címmel. Kérjük, használj másik email címet vagy jelentkezz be."
},
"success": {
"registerComplete": "Sikeres regisztráció. Most már bejelentkezhetsz."
} }
}; };

View File

@@ -3,6 +3,7 @@ import navigation from './navigation.js';
import auth from './auth.js'; import auth from './auth.js';
import cart from './cart.js'; import cart from './cart.js';
import product from './product.js'; import product from './product.js';
import productDialogs from './productDialogs.js';
import search from './search.js'; import search from './search.js';
import sorting from './sorting.js'; import sorting from './sorting.js';
import chat from './chat.js'; import chat from './chat.js';
@@ -18,6 +19,7 @@ import pages from './pages.js';
import orders from './orders.js'; import orders from './orders.js';
import settings from './settings.js'; import settings from './settings.js';
import common from './common.js'; import common from './common.js';
import kitConfig from './kitConfig.js';
import legalDatenschutzBasic from './legal-datenschutz-basic.js'; import legalDatenschutzBasic from './legal-datenschutz-basic.js';
import legalDatenschutzCustomer from './legal-datenschutz-customer.js'; import legalDatenschutzCustomer from './legal-datenschutz-customer.js';
import legalDatenschutzGoogleOrders from './legal-datenschutz-google-orders.js'; import legalDatenschutzGoogleOrders from './legal-datenschutz-google-orders.js';
@@ -35,6 +37,7 @@ export default {
"auth": auth, "auth": auth,
"cart": cart, "cart": cart,
"product": product, "product": product,
"productDialogs": productDialogs,
"search": search, "search": search,
"sorting": sorting, "sorting": sorting,
"chat": chat, "chat": chat,
@@ -50,6 +53,7 @@ export default {
"orders": orders, "orders": orders,
"settings": settings, "settings": settings,
"common": common, "common": common,
"kitConfig": kitConfig,
"legalDatenschutzBasic": legalDatenschutzBasic, "legalDatenschutzBasic": legalDatenschutzBasic,
"legalDatenschutzCustomer": legalDatenschutzCustomer, "legalDatenschutzCustomer": legalDatenschutzCustomer,
"legalDatenschutzGoogleOrders": legalDatenschutzGoogleOrders, "legalDatenschutzGoogleOrders": legalDatenschutzGoogleOrders,

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