Compare commits

..

21 Commits

Author SHA1 Message Date
sebseb7
dbd5df28f8 feat: Wrap product carousel title in a React Router Link and add a ChevronRight icon. 2025-12-14 10:01:37 +01:00
sebseb7
57515bfb85 feat: Refine i18n content across multiple locales and improve LLM SEO data processing for catalog generation. 2025-12-14 09:47:51 +01:00
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
219 changed files with 5086 additions and 1414 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": {
"version": "1.0.30001727",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001727.tgz",
"integrity": "sha512-pB68nIHmbN6L/4C6MH1DokyR3bYqFwjaSs/sWDHGj4CTcFtQUQMuJftVwWkXq7mNWOybD3KhUv3oWHoGxgP14Q==",
"version": "1.0.30001757",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001757.tgz",
"integrity": "sha512-r0nnL/I28Zi/yjk1el6ilj27tKcdjLsNqAOZr0yVjWPrSQyHgKI2INaEWw21bAQSv2LXRt1XuCS/GomNpWOxsQ==",
"dev": true,
"funding": [
{

View File

@@ -159,6 +159,7 @@ const Batteriegesetzhinweise =
const Widerrufsrecht = require("./src/pages/Widerrufsrecht.js").default;
const Sitemap = require("./src/pages/Sitemap.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 NotFound404 = require("./src/pages/NotFound404.js").default;
@@ -422,6 +423,14 @@ const renderApp = async (categoryData, socket) => {
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
console.log("\n📄 Rendering static pages...");
@@ -457,6 +466,13 @@ const renderApp = async (categoryData, socket) => {
description: "Sitemap page",
needsCategoryData: true,
},
{
component: PrerenderCategoriesPage,
path: "/Kategorien",
filename: "Kategorien",
description: "Categories page",
needsCategoryData: true,
},
{ component: AGB, path: "/agb", filename: "agb", description: "AGB page" },
{ component: NotFound404, path: "/404", filename: "404", description: "404 Not Found page" },
{
@@ -551,8 +567,7 @@ const renderApp = async (categoryData, socket) => {
try {
productData = await fetchCategoryProducts(socket, category.id);
console.log(
` ✅ Found ${
productData.products ? productData.products.length : 0
` ✅ Found ${productData.products ? productData.products.length : 0
} products`
);
@@ -841,7 +856,7 @@ const fetchCategoryDataAndRender = () => {
const socket = io(socketUrl, {
path: "/socket.io/",
transports: [ "websocket"],
transports: ["websocket"],
reconnection: false,
timeout: 10000,
});

View File

@@ -185,7 +185,7 @@ const saveProductImages = async (socket, products, categoryName, outputDir) => {
if (imageIds.length > 0) {
// Process first image for each product
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);
@@ -231,7 +231,7 @@ const saveProductImages = async (socket, products, categoryName, outputDir) => {
opacity: 0.3,
},
])
.jpeg() // Ensure output is JPEG
.avif() // Ensure output is AVIF
.toBuffer();
fs.writeFileSync(imagePath, processedImageBuffer);
@@ -281,7 +281,7 @@ const saveCategoryImages = async (socket, categories, outputDir) => {
// Debug: Log categories that will be processed
console.log(" 🔍 Categories to process:");
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(
@@ -308,7 +308,7 @@ const saveCategoryImages = async (socket, categories, outputDir) => {
for (const category of categories) {
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);
// Skip if image already exists

View File

@@ -55,7 +55,7 @@ const generateCategoryJsonLd = (category, products = [], baseUrl, config) => {
product.pictureList && product.pictureList.trim()
? `${baseUrl}/assets/images/prod${product.pictureList
.split(",")[0]
.trim()}.jpg`
.trim()}.avif`
: `${baseUrl}/assets/images/nopicture.jpg`,
description: product.description
? product.description.replace(/<[^>]*>/g, "").substring(0, 200)

View File

@@ -535,7 +535,7 @@ const generateProductsXml = (allProductsData = [], baseUrl, config) => {
// Generate image URL
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`;
// Generate brand (manufacturer)

View File

@@ -158,7 +158,7 @@ This file contains products ${startIndex + 1}-${endIndex} of ${totalProducts} in
`;
for (let i = 1; i <= totalPages; i++) {
categoryLlmsTxt += `- **Page ${i}**: ${baseUrl}/llms-${categorySlug}-page-${i}.txt (Products ${((i-1) * productsPerPage) + 1}-${Math.min(i * productsPerPage, totalProducts)})
categoryLlmsTxt += `- **Page ${i}**: ${baseUrl}/llms-${categorySlug}-page-${i}.txt (Products ${((i - 1) * productsPerPage) + 1}-${Math.min(i * productsPerPage, totalProducts)})
`;
}
@@ -173,10 +173,10 @@ This file contains products ${startIndex + 1}-${endIndex} of ${totalProducts} in
// Clean description for markdown (remove HTML tags and limit length)
const cleanDescription = product.description
? product.description
.replace(/<[^>]*>/g, "")
.replace(/\n/g, " ")
.trim()
.substring(0, 300)
.replace(/<[^>]*>/g, "")
.replace(/\n/g, " ")
.trim()
.substring(0, 300)
: "";
const globalIndex = startIndex + index + 1;
@@ -260,7 +260,7 @@ const generateCategoryProductList = (category, categoryProducts = []) => {
const fileName = `llms-${categorySlug}-list.txt`;
const subcategoryIds = (category.subcategories || []).join(',');
let content = `${String(category.name)},${String(category.id)},[${subcategoryIds}]\n`;
let content = '';//`${String(category.name)},${String(category.id)},[${subcategoryIds}]\n`;
categoryProducts.forEach((product) => {
const artnr = String(product.articleNumber || '');

View File

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

View File

@@ -1,9 +1,10 @@
const fs = require('fs');
const path = require('path');
// Read the input file
const inputFile = path.join(__dirname, 'dist', 'llms-cat.txt');
const outputFile = path.join(__dirname, 'output.csv');
// Read the input file from public
const inputFile = path.join(__dirname, 'public', 'llms-cat.txt');
// Write the output file to dist
const outputFile = path.join(__dirname, 'dist', 'llms-cat.txt');
// Function to parse a CSV line with escaped quotes
function parseCSVLine(line) {
@@ -38,44 +39,65 @@ function parseCSVLine(line) {
}
try {
if (!fs.existsSync(inputFile)) {
throw new Error(`Input file not found: ${inputFile}`);
}
const data = fs.readFileSync(inputFile, 'utf8');
const lines = data.trim().split('\n');
const outputLines = ['URL,SEO Description'];
// Keep the header as intended: URL and Description
const outputLines = ['URL of product list for article numbers,SEO Description'];
for (const line of lines) {
let skippedLines = 0;
let processedLines = 0;
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
if (line.trim() === '') continue;
// Skip comment lines or lines not starting with a number/quote (simple heuristic for header/comments)
// The file starts with text "this file has..." and then header "categoryId..."
// Actual data lines start with "
if (!line.trim().startsWith('"')) {
continue;
}
// Parse the CSV line properly handling escaped quotes
const fields = parseCSVLine(line);
if (fields.length !== 3) {
console.warn(`Skipping malformed line (got ${fields.length} fields): ${line.substring(0, 100)}...`);
console.warn(`Skipping malformed line ${i + 1} (got ${fields.length} fields): ${line.substring(0, 50)}...`);
skippedLines++;
continue;
}
const [field1, field2, field3] = fields;
const url = field2;
// Input: categoryId, listFileName, seoDescription
// Output: URL, SEO Description
const [categoryId, listFileName, seoDescription] = fields;
// field3 is a JSON string - parse it directly
let seoDescription = '';
try {
const parsed = JSON.parse(field3);
seoDescription = parsed.seo_description || '';
} catch (e) {
console.warn(`Failed to parse JSON for URL ${url}: ${e.message}`);
console.warn(`JSON string: ${field3.substring(0, 200)}...`);
}
// Use listFileName as URL
const url = listFileName;
// Escape quotes for CSV output - URL doesn't need quotes, description does
const escapedDescription = '"' + seoDescription.replace(/"/g, '""') + '"';
// Use seoDescription as description directly (it's already a string)
const description = seoDescription;
// Escape quotes for CSV output
const escapedDescription = '"' + description.replace(/"/g, '""') + '"';
outputLines.push(`${url},${escapedDescription}`);
processedLines++;
}
// Ensure dist directory exists
const distDir = path.dirname(outputFile);
if (!fs.existsSync(distDir)) {
fs.mkdirSync(distDir, { recursive: true });
}
// Write the output CSV
fs.writeFileSync(outputFile, outputLines.join('\n'), 'utf8');
console.log(`Processed ${lines.length} lines and created ${outputFile}`);
console.log(`Processed ${processedLines} lines (skipped ${skippedLines}) and created ${outputFile}`);
} catch (error) {
console.error('Error processing file:', error.message);

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 362 KiB

View File

@@ -1,3 +1,5 @@
this file has the list of category overview lists, where you can find article numbers
categoryId,listFileName,seoDescription
"703","https://growheads.de/llms-abluft-sets-list.txt","Abluft-Sets für Growbox & Indoor-Grow: leise, energiesparend & mit Aktivkohlefilter zur Geruchsneutralisation. Ideal für Zelte von 60 cm bis 1 m²."
"317","https://growheads.de/llms-air-pot-list.txt","Air-Pot Pflanztöpfe für maximales Wurzelwachstum: Air-Pruning, optimale Belüftung & Drainage. Ideal für Indoor, Outdoor, Hydroponik & Anzucht."

View File

@@ -7,6 +7,7 @@ const imagesToConvert = [
{ 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' }
];
@@ -18,6 +19,7 @@ const run = async () => {
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;
@@ -45,8 +47,8 @@ const run = async () => {
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)`);
// Silent skip if already up to date to keep logs clean, or use verbose flag
// console.log(`Skipping ${image.src} (already up to date)`);
}
}
@@ -55,4 +57,5 @@ const run = async () => {
}
};
console.log('dsfs');
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 NotFound404 = lazy(() => import(/* webpackChunkName: "legal" */ "./pages/NotFound404.js")); <Route path="/404" element={<NotFound404 />} />
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 Batteriegesetzhinweise = lazy(() => import(/* webpackChunkName: "legal" */ "./pages/Batteriegesetzhinweise.js"));
const Widerrufsrecht = lazy(() => import(/* webpackChunkName: "legal" */ "./pages/Widerrufsrecht.js"));
@@ -228,7 +229,7 @@ const AppContent = ({ currentTheme, dynamicTheme, onThemeChange }) => {
<TitleUpdater />
<ScrollToTop />
<Header active categoryId={categoryId} key={authVersion} />
<Box sx={{ flexGrow: 1 }}>
<Box component="main" sx={{ flexGrow: 1 }}>
<Suspense fallback={
// Use prerender fallback if available, otherwise show loading spinner
typeof window !== "undefined" && window.__PRERENDER_FALLBACK__ ? (
@@ -260,19 +261,19 @@ const AppContent = ({ currentTheme, dynamicTheme, onThemeChange }) => {
{/* Category page - Render Content in parallel */}
<Route
path="/Kategorie/:categoryId"
element={<Content/>}
element={<Content />}
/>
{/* Single product page */}
<Route
path="/Artikel/:seoName"
element={<ProductDetail/>}
element={<ProductDetail />}
/>
{/* Search page - Render Content in parallel */}
<Route path="/search" element={<Content/>} />
<Route path="/search" element={<Content />} />
{/* Profile page */}
<Route path="/profile" element={<ProfilePage/>} />
<Route path="/profile" element={<ProfilePage />} />
{/* Payment success page for Mollie redirects */}
<Route path="/payment/success" element={<PaymentSuccess />} />
@@ -280,22 +281,23 @@ const AppContent = ({ currentTheme, dynamicTheme, onThemeChange }) => {
{/* Reset password page */}
<Route
path="/resetPassword"
element={<ResetPassword/>}
element={<ResetPassword />}
/>
{/* Admin page */}
<Route path="/admin" element={<AdminPage/>} />
<Route path="/admin" element={<AdminPage />} />
{/* Admin Users page */}
<Route path="/admin/users" element={<UsersPage/>} />
<Route path="/admin/users" element={<UsersPage />} />
{/* Admin Server Logs page */}
<Route path="/admin/logs" element={<ServerLogsPage/>} />
<Route path="/admin/logs" element={<ServerLogsPage />} />
{/* Legal pages */}
<Route path="/datenschutz" element={<Datenschutz />} />
<Route path="/agb" element={<AGB />} />
<Route path="/sitemap" element={<Sitemap />} />
<Route path="/Kategorien" element={<CategoriesPage />} />
<Route path="/impressum" element={<Impressum />} />
<Route
path="/batteriegesetzhinweise"
@@ -304,7 +306,7 @@ const AppContent = ({ currentTheme, dynamicTheme, onThemeChange }) => {
<Route path="/widerrufsrecht" element={<Widerrufsrecht />} />
{/* Grow Tent Configurator */}
<Route path="/Konfigurator" element={<GrowTentKonfigurator/>} />
<Route path="/Konfigurator" element={<GrowTentKonfigurator />} />
{/* Separate pages that are truly different */}
<Route path="/presseverleih" element={<PresseverleihPage />} />
@@ -457,11 +459,11 @@ const App = () => {
<ProductContextProvider>
<CategoryContextProvider>
<CssBaseline />
<AppContent
currentTheme={currentTheme}
dynamicTheme={dynamicTheme}
onThemeChange={handleThemeChange}
/>
<AppContent
currentTheme={currentTheme}
dynamicTheme={dynamicTheme}
onThemeChange={handleThemeChange}
/>
</CategoryContextProvider>
</ProductContextProvider>
</ThemeProvider>

View File

@@ -44,7 +44,7 @@ const PrerenderAppContent = (socket) => (
<CategoryList categoryId={209} activeCategoryId={null} socket={socket}/>
</AppBar>
<Box sx={{ flexGrow: 1 }}>
<Box component="main" sx={{ flexGrow: 1 }}>
<CarouselProvider>
<Routes>
<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"
height="200"
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'
}
alt={product.name}

View File

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

View File

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

View File

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

View File

@@ -23,7 +23,7 @@ class CartItem extends Component {
window.socketManager.emit('getPic', { bildId:picid, size:'tiny' }, (res) => {
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});
}
})

View File

@@ -47,7 +47,7 @@ const CategoryBox = ({
// Create fresh blob URL from cached binary data
try {
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);
setImageUrl(objectUrl);
setImageError(false);
@@ -73,7 +73,7 @@ const CategoryBox = ({
try {
// Convert binary data to blob URL
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);
setImageUrl(objectUrl);
setImageError(false);
@@ -158,7 +158,7 @@ const CategoryBox = ({
position: 'relative',
backgroundImage: ((typeof window !== 'undefined' && 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'),
backgroundSize: 'cover',
backgroundPosition: 'center',

View File

@@ -82,7 +82,7 @@ function getFilteredProducts(unfilteredProducts, attributes, t) {
const uniqueAttributes = [...new Set((attributes || []).map(attr => attr.kMerkmalWert ? attr.kMerkmalWert.toString() : ''))];
const uniqueManufacturers = [...new Set((unfilteredProducts || []).filter(product => product.manufacturerId).map(product => product.manufacturerId ? product.manufacturerId.toString() : ''))];
const uniqueManufacturersWithName = [...new Set((unfilteredProducts || []).filter(product => product.manufacturerId).map(product => ({id:product.manufacturerId ? product.manufacturerId.toString() : '',value:product.manufacturer})))];
const uniqueManufacturersWithName = [...new Set((unfilteredProducts || []).filter(product => product.manufacturerId).map(product => ({ id: product.manufacturerId ? product.manufacturerId.toString() : '', value: product.manufacturer })))];
const activeAttributeFilters = attributeFilters.filter(filter => uniqueAttributes.includes(filter));
const activeManufacturerFilters = manufacturerFilters.filter(filter => uniqueManufacturers.includes(filter));
const attributeFiltersByGroup = {};
@@ -98,7 +98,7 @@ function getFilteredProducts(unfilteredProducts, attributes, t) {
let filteredProducts = (unfilteredProducts || []).filter(product => {
const availabilityFilter = sessionStorage.getItem('filter_availability');
let inStockMatch = availabilityFilter == 1 ? true : (product.available>0);
let inStockMatch = availabilityFilter == 1 ? true : (product.available > 0);
// Check if there are any new products in the entire set
const hasNewProducts = (unfilteredProducts || []).some(product => isNew(product.neu));
@@ -107,8 +107,8 @@ function getFilteredProducts(unfilteredProducts, attributes, t) {
const isNewMatch = availabilityFilters.includes('2') && hasNewProducts ? isNew(product.neu) : true;
let soonMatch = availabilityFilters.includes('3') ? !product.available && product.incoming : true;
const soon2Match = (availabilityFilter != 1)&&availabilityFilters.includes('3') ? (product.available) || (!product.available && product.incoming) : true;
if( (availabilityFilter != 1)&&availabilityFilters.includes('3') && ((product.available) || (!product.available && product.incoming))){
const soon2Match = (availabilityFilter != 1) && availabilityFilters.includes('3') ? (product.available) || (!product.available && product.incoming) : true;
if ((availabilityFilter != 1) && availabilityFilters.includes('3') && ((product.available) || (!product.available && product.incoming))) {
inStockMatch = true;
soonMatch = true;
console.log("soon2Match", product.cName);
@@ -134,11 +134,11 @@ function getFilteredProducts(unfilteredProducts, attributes, t) {
const activeAttributeFiltersWithNames = activeAttributeFilters.map(filter => {
const attribute = attributes.find(attr => attr.kMerkmalWert.toString() === filter);
return {name: attribute.cName, value: attribute.cWert, id: attribute.kMerkmalWert};
return { name: attribute.cName, value: attribute.cWert, id: attribute.kMerkmalWert };
});
const activeManufacturerFiltersWithNames = activeManufacturerFilters.map(filter => {
const manufacturer = uniqueManufacturersWithName.find(manufacturer => manufacturer.id === filter);
return {name: manufacturer.value, value: manufacturer.id};
return { name: manufacturer.value, value: manufacturer.id };
});
// Extract active availability filters
@@ -151,20 +151,20 @@ function getFilteredProducts(unfilteredProducts, attributes, t) {
// Check for "auf Lager" filter (in stock) - it's active when filter_availability is NOT set to '1'
if (availabilityFilter !== '1') {
activeAvailabilityFilters.push({id: '1', name: t ? t('product.inStock') : 'auf Lager'});
activeAvailabilityFilters.push({ id: '1', name: t ? t('product.inStock') : 'auf Lager' });
}
// Check for "Neu" filter (new) - only show if there are actually new products and filter is active
if (availabilityFilters.includes('2') && hasNewProducts) {
activeAvailabilityFilters.push({id: '2', name: t ? t('product.new') : 'Neu'});
activeAvailabilityFilters.push({ id: '2', name: t ? t('product.new') : 'Neu' });
}
// Check for "Bald verfügbar" filter (coming soon) - only show if there are actually coming soon products and filter is active
if (availabilityFilters.includes('3') && hasComingSoonProducts) {
activeAvailabilityFilters.push({id: '3', name: t ? t('product.comingSoon') : 'Bald verfügbar'});
activeAvailabilityFilters.push({ id: '3', name: t ? t('product.comingSoon') : 'Bald verfügbar' });
}
return {filteredProducts,activeAttributeFilters:activeAttributeFiltersWithNames,activeManufacturerFilters:activeManufacturerFiltersWithNames,activeAvailabilityFilters};
return { filteredProducts, activeAttributeFilters: activeAttributeFiltersWithNames, activeManufacturerFilters: activeManufacturerFiltersWithNames, activeAvailabilityFilters };
}
function setCachedCategoryData(categoryId, data, language = 'de') {
if (!window.productCache) {
@@ -176,7 +176,7 @@ function setCachedCategoryData(categoryId, data, language = 'de') {
try {
const cacheKey = `categoryProducts_${categoryId}_${language}`;
if(data.products) for(const product of data.products) {
if (data.products) for (const product of data.products) {
const productCacheKey = `product_${product.id}_${language}`;
window.productDetailCache[productCacheKey] = product;
}
@@ -206,11 +206,13 @@ class Content extends Component {
componentDidMount() {
const currentLanguage = this.props.i18n?.language || 'de';
if(this.props.params.categoryId) {this.setState({loaded: false, unfilteredProducts: [], filteredProducts: [], attributes: [], categoryName: null, childCategories: [], lastFetchedLanguage: currentLanguage}, () => {
if (this.props.params.categoryId) {
this.setState({ loaded: false, unfilteredProducts: [], filteredProducts: [], attributes: [], categoryName: null, childCategories: [], lastFetchedLanguage: currentLanguage }, () => {
this.fetchCategoryData(this.props.params.categoryId);
})}
})
}
else if (this.props.searchParams?.get('q')) {
this.setState({loaded: false, unfilteredProducts: [], filteredProducts: [], attributes: [], categoryName: null, childCategories: [], lastFetchedLanguage: currentLanguage}, () => {
this.setState({ loaded: false, unfilteredProducts: [], filteredProducts: [], attributes: [], categoryName: null, childCategories: [], lastFetchedLanguage: currentLanguage }, () => {
this.fetchSearchData(this.props.searchParams?.get('q'));
})
}
@@ -221,20 +223,20 @@ class Content extends Component {
const categoryChanged = this.props.params.categoryId && (prevProps.params.categoryId !== this.props.params.categoryId);
const searchChanged = this.props.searchParams?.get('q') && (prevProps.searchParams?.get('q') !== this.props.searchParams?.get('q'));
if(categoryChanged) {
// Clear context for new category loading
if (this.props.categoryContext && this.props.categoryContext.setCurrentCategory) {
this.props.categoryContext.setCurrentCategory(null);
}
if (categoryChanged) {
// Clear context for new category loading
if (this.props.categoryContext && this.props.categoryContext.setCurrentCategory) {
this.props.categoryContext.setCurrentCategory(null);
}
window.currentSearchQuery = null;
this.setState({loaded: false, unfilteredProducts: [], filteredProducts: [], attributes: [], categoryName: null, childCategories: [], lastFetchedLanguage: currentLanguage}, () => {
this.fetchCategoryData(this.props.params.categoryId);
});
return; // Don't check language change if category changed
window.currentSearchQuery = null;
this.setState({ loaded: false, unfilteredProducts: [], filteredProducts: [], attributes: [], categoryName: null, childCategories: [], lastFetchedLanguage: currentLanguage }, () => {
this.fetchCategoryData(this.props.params.categoryId);
});
return; // Don't check language change if category changed
}
else if (searchChanged) {
this.setState({loaded: false, unfilteredProducts: [], filteredProducts: [], attributes: [], categoryName: null, childCategories: [], lastFetchedLanguage: currentLanguage}, () => {
this.setState({ loaded: false, unfilteredProducts: [], filteredProducts: [], attributes: [], categoryName: null, childCategories: [], lastFetchedLanguage: currentLanguage }, () => {
this.fetchSearchData(this.props.searchParams?.get('q'));
});
return; // Don't check language change if search changed
@@ -253,43 +255,51 @@ class Content extends Component {
hasSearchQuery: !!this.props.searchParams?.get('q')
});
if(languageChanged) {
if (languageChanged) {
console.log('Content: Language changed! Re-fetching data...');
// Re-fetch current data with new language
// Note: Language is now part of the cache key, so it will automatically fetch fresh data
if(this.props.params.categoryId) {
if (this.props.params.categoryId) {
// Re-fetch category data with new language
console.log('Content: Re-fetching category', this.props.params.categoryId);
this.setState({loaded: false, lastFetchedLanguage: currentLanguage}, () => {
this.setState({ loaded: false, lastFetchedLanguage: currentLanguage }, () => {
this.fetchCategoryData(this.props.params.categoryId);
});
} else if(this.props.searchParams?.get('q')) {
} else if (this.props.searchParams?.get('q')) {
// Re-fetch search data with new language
console.log('Content: Re-fetching search', this.props.searchParams?.get('q'));
this.setState({loaded: false, lastFetchedLanguage: currentLanguage}, () => {
this.setState({ loaded: false, lastFetchedLanguage: currentLanguage }, () => {
this.fetchSearchData(this.props.searchParams?.get('q'));
});
} else {
// If not viewing category or search, just re-filter existing products
console.log('Content: Just re-filtering existing products');
this.setState({lastFetchedLanguage: currentLanguage});
this.setState({ lastFetchedLanguage: currentLanguage });
this.filterProducts();
}
}
}
processData(response) {
const unfilteredProducts = response.products;
const rawProducts = response.products;
const currentLanguage = this.props.languageContext?.currentLanguage || this.props.i18n?.language || 'de';
if (!window.individualProductCache) {
window.individualProductCache = {};
}
//console.log("processData", unfilteredProducts);
if(unfilteredProducts) unfilteredProducts.forEach(product => {
window.individualProductCache[product.id] = {
data: product,
const unfilteredProducts = [];
//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()
};
unfilteredProducts.push(effectiveProduct);
});
this.setState({
@@ -314,13 +324,13 @@ class Content extends Component {
if (this.props.categoryContext && this.props.categoryContext.setCurrentCategory) {
if (response.categoryName || response.name) {
console.log('Content: Setting category context');
this.props.categoryContext.setCurrentCategory({
id: this.props.params.categoryId,
name: response.categoryName || response.name
});
console.log('Content: Setting category context');
this.props.categoryContext.setCurrentCategory({
id: this.props.params.categoryId,
name: response.categoryName || response.name
});
} else {
console.log('Content: No category name found to set in context');
console.log('Content: No category name found to set in context');
}
} else {
console.warn('Content: categoryContext prop is missing!');
@@ -344,7 +354,7 @@ class Content extends Component {
// Track if we've received the full response to ignore stub response if needed
let receivedFullResponse = false;
window.socketManager.on(`productList:${categoryId}`,(response) => {
window.socketManager.on(`productList:${categoryId}`, (response) => {
console.log("getCategoryProducts full response", response);
receivedFullResponse = true;
setCachedCategoryData(categoryId, response, currentLanguage);
@@ -404,23 +414,23 @@ class Content extends Component {
// Attempt to set category name from the tree if missing in response
if (!enhancedResponse.categoryName && !enhancedResponse.name) {
// Try to find name in the tree using the ID or SEO name
try {
const currentLanguage = this.props.languageContext?.currentLanguage || this.props.i18n?.language || 'de';
const categoryTreeCache = window.categoryService.getSync(209, currentLanguage);
// Try to find name in the tree using the ID or SEO name
try {
const currentLanguage = this.props.languageContext?.currentLanguage || this.props.i18n?.language || 'de';
const categoryTreeCache = window.categoryService.getSync(209, currentLanguage);
if (categoryTreeCache) {
const targetCategory = typeof categoryId === 'string'
? this.findCategoryBySeoName(categoryTreeCache, categoryId)
: this.findCategoryById(categoryTreeCache, categoryId);
if (categoryTreeCache) {
const targetCategory = typeof categoryId === 'string'
? this.findCategoryBySeoName(categoryTreeCache, categoryId)
: this.findCategoryById(categoryTreeCache, categoryId);
if (targetCategory && targetCategory.name) {
enhancedResponse.categoryName = targetCategory.name;
}
}
} catch (err) {
console.error('Error finding category name in tree:', err);
}
if (targetCategory && targetCategory.name) {
enhancedResponse.categoryName = targetCategory.name;
}
}
} catch (err) {
console.error('Error finding category name in tree:', err);
}
}
this.processData(enhancedResponse);
@@ -450,7 +460,12 @@ class Content extends Component {
{ query, language: currentLanguage, requestTranslation: currentLanguage === 'de' ? false : true },
(response) => {
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 {
console.log("fetchSearchData in Content failed", response);
}
@@ -546,8 +561,8 @@ class Content extends Component {
// console.log('Content props:', this.props);
// Check if we should show category boxes instead of product list
const showCategoryBoxes = this.state.loaded &&
this.state.unfilteredProducts.length === 0 &&
this.state.childCategories.length > 0;
this.state.unfilteredProducts.length === 0 &&
this.state.childCategories.length > 0;
console.log("showCategoryBoxes", showCategoryBoxes, this.state.unfilteredProducts.length, this.state.childCategories.length);
@@ -565,98 +580,98 @@ class Content extends Component {
<>
{/* Show subcategories above main layout when there are both products and child categories */}
{this.state.loaded &&
this.state.unfilteredProducts.length > 0 &&
this.state.childCategories.length > 0 && (
<Box sx={{ mb: 4 }}>
{(() => {
const parentCategory = this.renderParentCategoryNavigation();
if (parentCategory) {
// Show parent category to the left of subcategories
return (
<Box sx={{ display: 'flex', alignItems: 'flex-start', gap: 3, flexWrap: 'wrap' }}>
{/* Parent Category Box */}
<Box sx={{ mt:2,position: 'relative', flexShrink: 0 }}>
<CategoryBox
id={parentCategory.id}
seoName={parentCategory.seoName}
name={parentCategory.name}
image={parentCategory.image}
height={130}
fontSize="1.0rem"
/>
{/* Up Arrow Overlay */}
<Box sx={{
position: 'absolute',
top: 8,
right: 8,
bgcolor: 'rgba(27, 94, 32, 0.8)',
borderRadius: '50%',
zIndex: 100,
width: 32,
height: 32,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
pointerEvents: 'none'
}}>
<KeyboardArrowUpIcon sx={{ color: 'white', fontSize: '1.2rem' }} />
this.state.unfilteredProducts.length > 0 &&
this.state.childCategories.length > 0 && (
<Box sx={{ mb: 4 }}>
{(() => {
const parentCategory = this.renderParentCategoryNavigation();
if (parentCategory) {
// Show parent category to the left of subcategories
return (
<Box sx={{ display: 'flex', alignItems: 'flex-start', gap: 3, flexWrap: 'wrap' }}>
{/* Parent Category Box */}
<Box sx={{ mt: 2, position: 'relative', flexShrink: 0 }}>
<CategoryBox
id={parentCategory.id}
seoName={parentCategory.seoName}
name={parentCategory.name}
image={parentCategory.image}
height={130}
fontSize="1.0rem"
/>
{/* Up Arrow Overlay */}
<Box sx={{
position: 'absolute',
top: 8,
right: 8,
bgcolor: 'rgba(27, 94, 32, 0.8)',
borderRadius: '50%',
zIndex: 100,
width: 32,
height: 32,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
pointerEvents: 'none'
}}>
<KeyboardArrowUpIcon sx={{ color: 'white', fontSize: '1.2rem' }} />
</Box>
</Box>
{/* Subcategories Grid */}
<Box sx={{ flexGrow: 1 }}>
<CategoryBoxGrid categories={this.state.childCategories} />
</Box>
</Box>
{/* Subcategories Grid */}
<Box sx={{ flexGrow: 1 }}>
<CategoryBoxGrid categories={this.state.childCategories} />
</Box>
</Box>
);
} else {
// No parent category, just show subcategories
return <CategoryBoxGrid categories={this.state.childCategories} />;
}
})()}
</Box>
)}
);
} else {
// No parent category, just show subcategories
return <CategoryBoxGrid categories={this.state.childCategories} />;
}
})()}
</Box>
)}
{/* Show standalone parent category navigation when there are only products */}
{this.state.loaded &&
this.props.params.categoryId &&
!(this.state.unfilteredProducts.length > 0 && this.state.childCategories.length > 0) && (() => {
const parentCategory = this.renderParentCategoryNavigation();
if (parentCategory) {
return (
<Box sx={{ mb: 3 }}>
<Box sx={{ position: 'relative', width: 'fit-content' }}>
<CategoryBox
id={parentCategory.id}
seoName={parentCategory.seoName}
name={parentCategory.name}
image={parentCategory.image}
height={130}
fontSize="1.0rem"
/>
{/* Up Arrow Overlay */}
<Box sx={{
position: 'absolute',
top: 8,
right: 8,
bgcolor: 'rgba(27, 94, 32, 0.8)',
borderRadius: '50%',
zIndex: 100,
width: 32,
height: 32,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
pointerEvents: 'none'
}}>
<KeyboardArrowUpIcon sx={{ color: 'white', fontSize: '1.2rem' }} />
this.props.params.categoryId &&
!(this.state.unfilteredProducts.length > 0 && this.state.childCategories.length > 0) && (() => {
const parentCategory = this.renderParentCategoryNavigation();
if (parentCategory) {
return (
<Box sx={{ mb: 3 }}>
<Box sx={{ position: 'relative', width: 'fit-content' }}>
<CategoryBox
id={parentCategory.id}
seoName={parentCategory.seoName}
name={parentCategory.name}
image={parentCategory.image}
height={130}
fontSize="1.0rem"
/>
{/* Up Arrow Overlay */}
<Box sx={{
position: 'absolute',
top: 8,
right: 8,
bgcolor: 'rgba(27, 94, 32, 0.8)',
borderRadius: '50%',
zIndex: 100,
width: 32,
height: 32,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
pointerEvents: 'none'
}}>
<KeyboardArrowUpIcon sx={{ color: 'white', fontSize: '1.2rem' }} />
</Box>
</Box>
</Box>
</Box>
);
}
return null;
})()}
);
}
return null;
})()}
{/* Show normal product list layout */}
<Box sx={{
@@ -665,168 +680,169 @@ class Content extends Component {
gap: { xs: 0, sm: 3 }
}}>
<Stack direction="row" spacing={0} sx={{
display: 'flex',
flexDirection: 'column',
minHeight: { xs: 'min-content', sm: '100%' }
}}>
<Box >
<ProductFilters
products={this.state.unfilteredProducts}
filteredProducts={this.state.filteredProducts}
attributes={this.state.attributes}
searchParams={this.props.searchParams}
onFilterChange={()=>{this.filterProducts()}}
dataType={this.state.dataType}
dataParam={this.state.dataParam}
/>
</Box>
{(this.props.params.categoryId == 'Stecklinge' || this.props.params.categoryId == 'Seeds') &&
<Box sx={{ display: { xs: 'none', sm: 'block' } }}>
<Typography variant="h6" sx={{mt:3}}>
{this.props.t ? this.props.t('navigation.otherCategories') : 'Andere Kategorien'}
</Typography>
</Box>
}
{this.props.params.categoryId == 'Stecklinge' && <Paper
component={Link}
to="/Kategorie/Seeds"
sx={{
p:0,
mt: 1,
textDecoration: 'none',
color: 'text.primary',
borderRadius: 2,
overflow: 'hidden',
height: 300,
transition: 'all 0.3s ease',
boxShadow: 10,
display: { xs: 'none', sm: 'block' },
'&:hover': {
transform: 'translateY(-5px)',
boxShadow: 20
}
}}
>
{/* Image Container - Place your seeds image here */}
<Box sx={{
height: '100%',
bgcolor: '#e1f0d3',
position: 'relative',
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
}}>
<img
src="/assets/images/seeds.avif"
alt="Seeds"
fetchPriority="high"
loading="eager"
style={{
maxWidth: '100%',
maxHeight: '100%',
objectFit: 'contain',
position: 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)'
}}
/>
{/* Overlay text - optional */}
<Box sx={{
position: 'absolute',
bottom: 0,
left: 0,
right: 0,
bgcolor: 'rgba(27, 94, 32, 0.8)',
p: 2,
<Stack direction="row" spacing={0} sx={{
display: 'flex',
flexDirection: 'column',
minHeight: { xs: 'min-content', sm: '100%' }
}}>
<Typography sx={{ fontSize: '1.3rem', color: 'white', fontFamily: 'SwashingtonCP' }}>
{this.props.t('sections.seeds')}
</Typography>
<Box >
<ProductFilters
products={this.state.unfilteredProducts}
filteredProducts={this.state.filteredProducts}
attributes={this.state.attributes}
searchParams={this.props.searchParams}
onFilterChange={() => { this.filterProducts() }}
dataType={this.state.dataType}
dataParam={this.state.dataParam}
categoryName={this.state.categoryName}
/>
</Box>
{(this.props.params.categoryId == 'Stecklinge___' || this.props.params.categoryId == 'Seeds___') &&
<Box sx={{ display: { xs: 'none', sm: 'block' } }}>
<Typography variant="h6" sx={{ mt: 3 }}>
{this.props.t ? this.props.t('navigation.otherCategories') : 'Andere Kategorien'}
</Typography>
</Box>
}
{this.props.params.categoryId == 'Stecklinge' && <Paper
component={Link}
to="/Kategorie/Seeds"
sx={{
p: 0,
mt: 1,
textDecoration: 'none',
color: 'text.primary',
borderRadius: 2,
overflow: 'hidden',
height: 300,
transition: 'all 0.3s ease',
boxShadow: 10,
display: { xs: 'none', sm: 'block' },
'&:hover': {
transform: 'translateY(-5px)',
boxShadow: 20
}
}}
>
{/* Image Container - Place your seeds image here */}
<Box sx={{
height: '100%',
bgcolor: '#e1f0d3',
position: 'relative',
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
}}>
<img
src="/assets/images/seeds.avif"
alt="Seeds"
fetchPriority="high"
loading="eager"
style={{
maxWidth: '100%',
maxHeight: '100%',
objectFit: 'contain',
position: 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)'
}}
/>
{/* Overlay text - optional */}
<Box sx={{
position: 'absolute',
bottom: 0,
left: 0,
right: 0,
bgcolor: 'rgba(27, 94, 32, 0.8)',
p: 2,
}}>
<Typography sx={{ fontSize: '1.3rem', color: 'white', fontFamily: 'SwashingtonCP' }}>
{this.props.t('sections.seeds')}
</Typography>
</Box>
</Box>
</Paper>
}
{this.props.params.categoryId == 'Seeds___' && <Paper
component={Link}
to="/Kategorie/Stecklinge"
sx={{
p: 0,
mt: 1,
textDecoration: 'none',
color: 'text.primary',
borderRadius: 2,
overflow: 'hidden',
height: 300,
boxShadow: 10,
transition: 'all 0.3s ease',
display: { xs: 'none', sm: 'block' },
'&:hover': {
transform: 'translateY(-5px)',
boxShadow: 20
}
}}
>
{/* Image Container - Place your cutlings image here */}
<Box sx={{
height: '100%',
bgcolor: '#e8f5d6',
position: 'relative',
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
}}>
<img
src="/assets/images/cutlings.avif"
alt="Stecklinge"
fetchPriority="high"
loading="eager"
style={{
maxWidth: '100%',
maxHeight: '100%',
objectFit: 'contain',
position: 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)'
}}
/>
{/* Overlay text - optional */}
<Box sx={{
position: 'absolute',
bottom: 0,
left: 0,
right: 0,
bgcolor: 'rgba(27, 94, 32, 0.8)',
p: 2,
}}>
<Typography sx={{ fontSize: '1.3rem', color: 'white', fontFamily: 'SwashingtonCP' }}>
{this.props.t('sections.stecklinge')}
</Typography>
</Box>
</Box>
</Paper>}
</Stack>
<Box>
<ProductList
totalProductCount={(this.state.unfilteredProducts || []).length}
products={this.state.filteredProducts || []}
activeAttributeFilters={this.state.activeAttributeFilters || []}
activeManufacturerFilters={this.state.activeManufacturerFilters || []}
activeAvailabilityFilters={this.state.activeAvailabilityFilters || []}
onFilterChange={() => { this.filterProducts() }}
dataType={this.state.dataType}
dataParam={this.state.dataParam}
/>
</Box>
</Box>
</Paper>
}
{this.props.params.categoryId == 'Seeds' && <Paper
component={Link}
to="/Kategorie/Stecklinge"
sx={{
p: 0,
mt: 1,
textDecoration: 'none',
color: 'text.primary',
borderRadius: 2,
overflow: 'hidden',
height: 300,
boxShadow: 10,
transition: 'all 0.3s ease',
display: { xs: 'none', sm: 'block' },
'&:hover': {
transform: 'translateY(-5px)',
boxShadow: 20
}
}}
>
{/* Image Container - Place your cutlings image here */}
<Box sx={{
height: '100%',
bgcolor: '#e8f5d6',
position: 'relative',
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
}}>
<img
src="/assets/images/cutlings.avif"
alt="Stecklinge"
fetchPriority="high"
loading="eager"
style={{
maxWidth: '100%',
maxHeight: '100%',
objectFit: 'contain',
position: 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)'
}}
/>
{/* Overlay text - optional */}
<Box sx={{
position: 'absolute',
bottom: 0,
left: 0,
right: 0,
bgcolor: 'rgba(27, 94, 32, 0.8)',
p: 2,
}}>
<Typography sx={{ fontSize: '1.3rem', color: 'white', fontFamily: 'SwashingtonCP' }}>
{this.props.t('sections.stecklinge')}
</Typography>
</Box>
</Box>
</Paper>}
</Stack>
<Box>
<ProductList
totalProductCount={(this.state.unfilteredProducts || []).length}
products={this.state.filteredProducts || []}
activeAttributeFilters={this.state.activeAttributeFilters || []}
activeManufacturerFilters={this.state.activeManufacturerFilters || []}
activeAvailabilityFilters={this.state.activeAvailabilityFilters || []}
onFilterChange={()=>{this.filterProducts()}}
dataType={this.state.dataType}
dataParam={this.state.dataParam}
/>
</Box>
</Box>
</>
)}
</Container>

View File

@@ -352,6 +352,9 @@ class Footer extends Component {
<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>
</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>
</Stack>
</Box>

View File

@@ -56,7 +56,7 @@ class Images extends Component {
pics.push(window.tinyPicCache[bildId]);
this.loadPic(this.props.fullscreenOpen ? 'large' : 'medium',bildId,newMainPic);
}else{
pics.push(`/assets/images/prod${bildId}.jpg`);
pics.push(`/assets/images/prod${bildId}.avif`);
this.loadPic(this.props.fullscreenOpen ? 'large' : 'medium',bildId,newMainPic);
}
}else{
@@ -84,7 +84,7 @@ class Images extends Component {
window.socketManager.emit('getPic', { bildId, size }, (res) => {
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 === 'small') window.smallPicCache[bildId] = url;
@@ -118,7 +118,7 @@ class Images extends Component {
if (!this.props.pictureList || !this.props.pictureList.trim()) {
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 (

View File

@@ -175,12 +175,12 @@ export class LoginComponent extends Component {
const { location, navigate } = this.props;
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;
}
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;
}
@@ -238,7 +238,7 @@ export class LoginComponent extends Component {
} else {
this.setState({
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;
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;
}
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;
}
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;
}
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;
}
@@ -274,14 +274,14 @@ export class LoginComponent extends Component {
if (response.success) {
this.setState({
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
});
} else {
let errorMessage = 'Registrierung fehlgeschlagen';
let errorMessage = this.props.t ? this.props.t('auth.errors.registerFailed') : 'Registrierung fehlgeschlagen';
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) {
errorMessage = response.message;
}
@@ -322,12 +322,12 @@ export class LoginComponent extends Component {
const { email } = this.state;
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;
}
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;
}
@@ -342,12 +342,12 @@ export class LoginComponent extends Component {
if (response.success) {
this.setState({
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 {
this.setState({
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 {
this.setState({
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
});
}
@@ -418,7 +418,7 @@ export class LoginComponent extends Component {
handleGoogleLoginError = (error) => {
console.error('Google Login Error:', error);
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
loading: false
});

View File

@@ -156,7 +156,7 @@ const MainPageLayout = () => {
};
const allTitles = {
home: t('titles.home') ,
home: t('titles.home'),
aktionen: t('titles.aktionen'),
filiale: t('titles.filiale')
};
@@ -164,7 +164,7 @@ const MainPageLayout = () => {
const allContentBoxes = {
home: [
{ title: t('sections.seeds'), image: "/assets/images/seeds.avif", bgcolor: "#e1f0d3", link: "/Kategorie/Seeds" },
{ title: t('sections.stecklinge'), image: "/assets/images/cutlings.avif", bgcolor: "#e8f5d6", link: "/Kategorie/Stecklinge" }
{ title: t('sections.konfigurator'), image: "/assets/images/konfigurator.avif", bgcolor: "#e8f5d6", link: "/Konfigurator" }
],
aktionen: [
{ title: t('sections.oilPress'), image: "/assets/images/presse.jpg", bgcolor: "#e1f0d3", link: "/presseverleih" },
@@ -262,16 +262,16 @@ const MainPageLayout = () => {
position: pageType === "home" ? "relative" : "absolute", top: 0, left: 0, width: "100%", pointerEvents: getOpacity(pageType) === 1 ? "auto" : "none"
}}>
{contentBoxes.map((box, index) => (
<ContentBox
key={`${pageType}-${index}`}
box={box}
index={index}
pageType={pageType}
starHovered={starHovered}
setStarHovered={setStarHovered}
opacity={getOpacity(pageType)}
translatedContent={translatedContent}
/>
<ContentBox
key={`${pageType}-${index}`}
box={box}
index={index}
pageType={pageType}
starHovered={starHovered}
setStarHovered={setStarHovered}
opacity={getOpacity(pageType)}
translatedContent={translatedContent}
/>
))}
</Grid>
))}

View File

@@ -10,6 +10,7 @@ import {
} from '@mui/material';
import Delete from '@mui/icons-material/Delete';
import CloudUpload from '@mui/icons-material/CloudUpload';
import { withI18n } from '../i18n/withTranslation.js';
class PhotoUpload extends Component {
constructor(props) {
@@ -30,7 +31,7 @@ class PhotoUpload extends Component {
// Validate file count
if (this.state.files.length + selectedFiles.length > maxFiles) {
this.setState({
error: `Maximal ${maxFiles} Dateien erlaubt`
error: this.props.t("productDialogs.photoUploadErrorMaxFiles", { max: maxFiles })
});
return;
}
@@ -43,14 +44,14 @@ class PhotoUpload extends Component {
for (const file of selectedFiles) {
if (!validTypes.includes(file.type)) {
this.setState({
error: 'Nur Bilddateien (JPEG, PNG, GIF, WebP) sind erlaubt'
error: this.props.t("productDialogs.photoUploadErrorFileType")
});
continue;
}
if (file.size > maxSize) {
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;
}
@@ -167,12 +168,12 @@ class PhotoUpload extends Component {
render() {
const { files, previews, error } = this.state;
const { disabled, label } = this.props;
const { disabled, label, t } = this.props;
return (
<Box>
<Typography variant="body2" sx={{ mb: 1, fontWeight: 500 }}>
{label || 'Fotos anhängen (optional)'}
{label || t("productDialogs.photoUploadLabelDefault")}
</Typography>
<input
@@ -192,7 +193,7 @@ class PhotoUpload extends Component {
disabled={disabled}
sx={{ mb: 2 }}
>
Fotos auswählen
{t("productDialogs.photoUploadSelect")}
</Button>
{error && (
@@ -228,7 +229,7 @@ class PhotoUpload extends Component {
size="small"
onClick={() => this.handleRemoveFile(index)}
disabled={disabled}
aria-label="Bild entfernen"
aria-label={t("productDialogs.photoUploadRemove")}
sx={{
position: 'absolute',
top: 4,
@@ -269,10 +270,10 @@ class PhotoUpload extends Component {
{files.length > 0 && (
<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) && (
<span style={{ marginLeft: '8px' }}>
(komprimiert für Upload)
{t("productDialogs.photoUploadCompressed")}
</span>
)}
</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);
window.socketManager.emit('getPic', { bildId, size:'small' }, (res) => {
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) {
this.setState({image: window.smallPicCache[bildId], loading: false});
} else {

View File

@@ -1,4 +1,5 @@
import React from 'react';
import { Link } from "react-router-dom";
import Box from "@mui/material/Box";
import Typography from "@mui/material/Typography";
import IconButton from "@mui/material/IconButton";
@@ -46,7 +47,7 @@ class ProductCarousel extends React.Component {
componentDidUpdate(prevProps) {
console.log("ProductCarousel 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({ products: [] }, () => {
this.loadProducts(this.props.languageContext?.currentLanguage || this.props.i18n.language);
});
@@ -277,25 +278,41 @@ class ProductCarousel extends React.Component {
const { t, title } = this.props;
const { products } = this.state;
if(!products || products.length === 0) {
if (!products || products.length === 0) {
return null;
}
return (
<Box sx={{ mt: 3 }}>
<Typography
variant="h4"
component="h2"
<Box
component={Link}
to="/Kategorie/neu"
sx={{
mb: 2,
fontFamily: "SwashingtonCP",
display: "flex",
alignItems: "center",
justifyContent: "center",
textDecoration: "none",
color: "primary.main",
textAlign: "center",
textShadow: "3px 3px 10px rgba(0, 0, 0, 0.4)"
mb: 2,
transition: "all 0.3s ease",
"&:hover": {
transform: "translateX(5px)",
color: "primary.dark"
}
}}
>
{title || t('product.new')}
</Typography>
<Typography
variant="h4"
component="span"
sx={{
fontFamily: "SwashingtonCP",
textShadow: "3px 3px 10px rgba(0, 0, 0, 0.4)"
}}
>
{title || t('product.new')}
</Typography>
<ChevronRight sx={{ fontSize: "2.5rem", ml: 1 }} />
</Box>
<div
className="product-carousel-wrapper"

View File

@@ -305,7 +305,7 @@ class ProductDetailPage extends Component {
window.socketManager.emit('getPic', { bildId, size: 'small' }, (res) => {
if (res.success) {
// 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
this.setState(prevState => ({
@@ -546,7 +546,7 @@ class ProductDetailPage extends Component {
console.log("getAttributePicture", res);
if (res.success && !res.noPicture) {
const blob = new Blob([res.imageBuffer], {
type: "image/jpeg",
type: "image/avif",
});
const url = URL.createObjectURL(blob);
@@ -599,13 +599,16 @@ class ProductDetailPage extends Component {
const productData = res.translatedProduct || res.product;
productData.seoName = this.props.seoName;
// Use translated attributes if available
const attributesData = res.translatedAttributes || res.attributes;
// Initialize cache if it doesn't exist
if (!window.productDetailCache) {
window.productDetailCache = {};
}
// 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;
// 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
error: null,
imageDialogOpen: false,
attributes: res.attributes,
attributes: attributesData,
komponenten: komponenten,
komponentenLoaded: komponenten.length === 0, // If no komponenten, mark as loaded
similarProducts: res.similarProducts || []
@@ -653,7 +656,7 @@ class ProductDetailPage extends Component {
console.log("getProductView", res);
// Load attribute images
this.loadAttributeImages(res.attributes);
this.loadAttributeImages(attributesData);
} else {
console.error(
"Error loading product:",
@@ -762,7 +765,7 @@ class ProductDetailPage extends Component {
handleEmbedShare = () => {
const embedCode = `<iframe src="${this.getProductUrl()}" width="100%" height="600" frameborder="0"></iframe>`;
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(() => {
// Fallback for older browsers
try {
@@ -772,9 +775,9 @@ class ProductDetailPage extends Component {
textArea.select();
document.execCommand('copy');
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 {
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();
@@ -782,7 +785,10 @@ class ProductDetailPage extends Component {
handleWhatsAppShare = () => {
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)}`;
window.open(whatsappUrl, '_blank');
this.handleShareClose();
@@ -797,7 +803,10 @@ class ProductDetailPage extends Component {
handleTelegramShare = () => {
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)}`;
window.open(telegramUrl, '_blank');
this.handleShareClose();
@@ -805,8 +814,18 @@ class ProductDetailPage extends Component {
handleEmailShare = () => {
const url = this.getProductUrl();
const subject = `Produktempfehlung: ${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 productName = cleanProductName(this.state.product.name);
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)}`;
window.location.href = emailUrl;
this.handleShareClose();
@@ -815,7 +834,7 @@ class ProductDetailPage extends Component {
handleLinkCopy = () => {
const url = this.getProductUrl();
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(() => {
// Fallback for older browsers
try {
@@ -825,7 +844,7 @@ class ProductDetailPage extends Component {
textArea.select();
document.execCommand('copy');
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 {
this.showSnackbar("Fehler beim Kopieren des Links", "error");
}
@@ -968,7 +987,7 @@ class ProductDetailPage extends Component {
}).format(productData.price)}
</Typography>
<Typography variant="caption" color="text.secondary">
inkl. MwSt.
{this.props.t ? this.props.t('product.inclVatSimple') : 'inkl. MwSt.'}
</Typography>
</Box>
</Box>
@@ -1047,7 +1066,7 @@ class ProductDetailPage extends Component {
console.log('loadEmbeddedProductImage response:', articleNr, 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 => {
console.log('Setting embedded product image for', articleNr);
return {
@@ -1331,7 +1350,7 @@ class ProductDetailPage extends Component {
whiteSpace: "nowrap"
}}
>
Frage zum Artikel
{this.props.t ? this.props.t('productDialogs.questionTitle') : "Frage zum Artikel"}
</Button>
<Button
variant="outlined"
@@ -1345,7 +1364,7 @@ class ProductDetailPage extends Component {
whiteSpace: "nowrap"
}}
>
Artikel Bewerten
{this.props.t ? this.props.t('productDialogs.ratingTitle') : "Artikel Bewerten"}
</Button>
{(product.available !== 1 && product.availableSupplier !== 1) && (
<Button
@@ -1366,7 +1385,7 @@ class ProductDetailPage extends Component {
}
}}
>
Verfügbarkeit anfragen
{this.props.t ? this.props.t('productDialogs.availabilityTitle') : "Verfügbarkeit anfragen"}
</Button>
)}
</Stack>
@@ -1595,7 +1614,7 @@ class ProductDetailPage extends Component {
}}
size="small"
>
Teilen
{this.props.t ? this.props.t("productDialogs.shareTitle") : "Teilen"}
</Button>
<Box
sx={{
@@ -1669,7 +1688,7 @@ class ProductDetailPage extends Component {
<ListItemIcon>
<CodeIcon fontSize="small" />
</ListItemIcon>
<ListItemText primary="Einbetten" />
<ListItemText primary={this.props.t ? this.props.t("productDialogs.shareEmbed") : "Einbetten"} />
</MenuItem>
<MenuItem onClick={this.handleWhatsAppShare}>
<ListItemIcon>
@@ -1699,7 +1718,7 @@ class ProductDetailPage extends Component {
<ListItemIcon>
<LinkIcon fontSize="small" />
</ListItemIcon>
<ListItemText primary="Link kopieren" />
<ListItemText primary={this.props.t ? this.props.t("productDialogs.shareCopyLink") : "Link kopieren"} />
</MenuItem>
</MenuList>
</Box>
@@ -1953,7 +1972,7 @@ class ProductDetailPage extends Component {
gap: 2
}}>
{this.state.similarProducts.map((similarProductData, index) => {
const product = similarProductData.product;
const product = similarProductData.translatedProduct || similarProductData.product;
return (
<Box key={product.id} sx={{ display: 'flex', justifyContent: 'center' }}>
<Product

View File

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

View File

@@ -1,4 +1,5 @@
import React from 'react';
import { Link } from 'react-router-dom';
import Box from "@mui/material/Box";
import Typography from "@mui/material/Typography";
import IconButton from "@mui/material/IconButton";
@@ -60,9 +61,9 @@ class SharedCarousel extends React.Component {
componentDidUpdate(prevProps) {
console.log("componentDidUpdate", prevProps.languageContext?.currentLanguage, this.props.languageContext?.currentLanguage);
if(prevProps.languageContext?.currentLanguage !== this.props.languageContext?.currentLanguage) {
this.setState({ categories: [] },() => {
window.categoryService.get(209,this.props.languageContext?.currentLanguage || this.props.i18n.language).then((response) => {
if (prevProps.languageContext?.currentLanguage !== this.props.languageContext?.currentLanguage) {
this.setState({ categories: [] }, () => {
window.categoryService.get(209, this.props.languageContext?.currentLanguage || this.props.i18n.language).then((response) => {
console.log("response", response);
if (response.children && response.children.length > 0) {
this.originalCategories = response.children;
@@ -268,25 +269,41 @@ class SharedCarousel extends React.Component {
const { t } = this.props;
const { categories } = this.state;
if(!categories || categories.length === 0) {
if (!categories || categories.length === 0) {
return null;
}
return (
<Box sx={{ mt: 3 }}>
<Typography
variant="h4"
component="h1"
<Box
component={Link}
to="/Kategorien"
sx={{
mb: 2,
fontFamily: "SwashingtonCP",
display: "flex",
alignItems: "center",
justifyContent: "center",
textDecoration: "none",
color: "primary.main",
textAlign: "center",
textShadow: "3px 3px 10px rgba(0, 0, 0, 0.4)"
mb: 2,
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
className="carousel-wrapper"

View File

@@ -63,7 +63,7 @@ class ExtrasSelector extends Component {
this.loadingImages.add(bildId);
window.socketManager.emit('getPic', { bildId, size:'small' }, (res) => {
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.loadingImages.delete(bildId);

View File

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

View File

@@ -53,12 +53,12 @@ class CategoryList extends Component {
componentDidUpdate(prevProps) {
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: [],
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);
if (response.children && response.children.length > 0) {
this.setState({
@@ -69,14 +69,14 @@ class CategoryList extends Component {
});
});
}
if (prevProps.activeCategoryId !== this.props.activeCategoryId) {
this.setLevel1CategoryId(this.props.activeCategoryId);
}
if (prevProps.activeCategoryId !== this.props.activeCategoryId) {
this.setLevel1CategoryId(this.props.activeCategoryId);
}
}
setLevel1CategoryId = (input) => {
if(input) {
if (input) {
const language = this.props.languageContext?.currentLanguage || this.props.i18n.language;
const categoryTreeCache = window.categoryService.getSync(209, language);
@@ -173,141 +173,141 @@ class CategoryList extends Component {
py: 0.5, // Add vertical padding to prevent border clipping
}}
>
<Button
component={Link}
to="/"
color="inherit"
size="small"
aria-label="Zur Startseite"
onClick={isMobile ? this.handleMobileCategoryClick : undefined}
sx={{
fontSize: "0.75rem",
textTransform: "none",
whiteSpace: "nowrap",
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",
},
<Button
component={Link}
to="/"
color="inherit"
size="small"
aria-label="Zur Startseite"
onClick={isMobile ? this.handleMobileCategoryClick : undefined}
sx={{
fontSize: "0.75rem",
textTransform: "none",
whiteSpace: "nowrap",
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",
},
}}
>
<HomeIcon 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>
{/* Thin text (positioned on top) */}
<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>
"& .bold-text": {
color: "#2e7d32 !important",
},
"& .thin-text": {
color: "transparent !important",
},
},
}}
>
<HomeIcon 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>
)}
</Button>
{/* Thin text (positioned on top) */}
<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
component={Link}
to="/Kategorie/neu"
color="inherit"
size="small"
aria-label="Neuheiten"
onClick={isMobile ? this.handleMobileCategoryClick : undefined}
sx={{
fontSize: "0.75rem",
textTransform: "none",
whiteSpace: "nowrap",
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"
}}
>
<FiberNewIcon sx={{
fontSize: "1rem",
mr: isMobile ? 1 : 0
}} />
{isMobile && (
<Box sx={{ position: "relative", display: "inline-block" }}>
{/* Bold text (always rendered to set width) */}
<Box
className="bold-text"
sx={{
fontWeight: "bold",
color: "transparent",
position: "relative",
zIndex: 2,
}}
>
{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>
<Button
component={Link}
to="/Kategorie/neu"
color="inherit"
size="small"
aria-label="Neuheiten"
onClick={isMobile ? this.handleMobileCategoryClick : undefined}
sx={{
fontSize: "0.75rem",
textTransform: "none",
whiteSpace: "nowrap",
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"
}}
>
<FiberNewIcon sx={{
fontSize: "1rem",
mr: isMobile ? 1 : 0
}} />
{isMobile && (
<Box sx={{ position: "relative", display: "inline-block" }}>
{/* Bold text (always rendered to set width) */}
<Box
className="bold-text"
sx={{
fontWeight: "bold",
color: "transparent",
position: "relative",
zIndex: 2,
}}
>
{this.props.t ? this.props.t('navigation.new') : 'Neuheiten'}
</Box>
)}
</Button>
{/* 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>
)}
</Button>
{categories.length > 0 ? (
@@ -385,100 +385,100 @@ class CategoryList extends Component {
);
})}
</>
) : ( !isMobile && (
<Typography
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"
) : (!isMobile && (
<Typography
variant="caption"
color="inherit"
size="small"
aria-label="Zur Startseite"
onClick={isMobile ? this.handleMobileCategoryClick : undefined}
sx={{
display: "inline-flex",
alignItems: "center",
height: "33px", // Match small button height
px: 1,
fontSize: "0.75rem",
textTransform: "none",
whiteSpace: "nowrap",
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={{
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>
{/* Thin text (positioned on top) */}
<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>
&nbsp;
</Typography>
)
)}
<Button
component={Link}
to="/Konfigurator"
color="inherit"
size="small"
aria-label="Zur Startseite"
onClick={isMobile ? this.handleMobileCategoryClick : undefined}
sx={{
fontSize: "0.75rem",
textTransform: "none",
whiteSpace: "nowrap",
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={{
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>
)}
</Button>
{/* Thin text (positioned on top) */}
<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>
);
@@ -545,7 +545,7 @@ class CategoryList extends Component {
fontWeight: "bold",
textShadow: "0 1px 2px rgba(0,0,0,0.3)"
}}>
{this.props.t ? this.props.t('navigation.categories') : 'Kategorien'}
{this.props.t ? this.props.t('navigation.categories') : 'Kategorien'}
</Typography>
<Box sx={{ display: "flex", alignItems: "center" }}>
{mobileMenuOpen ? <CloseIcon /> : <MenuIcon />}

View File

@@ -79,7 +79,7 @@ const SearchBar = () => {
(response) => {
if (response && response.products) {
// 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);
setShowSuggestions(suggestions.length > 0);
setSelectedIndex(-1); // Reset selection

View File

@@ -31,6 +31,7 @@ const getStatusTranslation = (status, t) => {
new: t ? t('orders.status.new') : "in Bearbeitung",
pending: t ? t('orders.status.pending') : "Neu",
processing: t ? t('orders.status.processing') : "in Bearbeitung",
paid: t ? t('orders.status.paid') : "Bezahlt",
cancelled: t ? t('orders.status.cancelled') : "Storniert",
shipped: t ? t('orders.status.shipped') : "Verschickt",
delivered: t ? t('orders.status.delivered') : "Geliefert",
@@ -39,29 +40,23 @@ const getStatusTranslation = (status, t) => {
};
const statusEmojis = {
"in Bearbeitung": "⚙️",
new: "⚙️",
pending: "⏳",
processing: "🔄",
paid: "🏦",
cancelled: "❌",
Verschickt: "🚚",
Geliefert: "✅",
Storniert: "❌",
Retoure: "↩️",
"Teil Retoure": "↪️",
"Teil geliefert": "⚡",
shipped: "🚚",
delivered: "✅",
};
const statusColors = {
"in Bearbeitung": "#ed6c02", // orange
new: "#ed6c02", // orange
pending: "#ff9800", // orange for pending
processing: "#2196f3", // blue for processing
paid: "#2e7d32", // green
cancelled: "#d32f2f", // red for cancelled
Verschickt: "#2e7d32", // green
Geliefert: "#2e7d32", // green
Storniert: "#d32f2f", // red
Retoure: "#9c27b0", // purple
"Teil Retoure": "#9c27b0", // purple
"Teil geliefert": "#009688", // teal
shipped: "#2e7d32", // green
delivered: "#2e7d32", // green
};
const currencyFormatter = new Intl.NumberFormat("de-DE", {
@@ -229,11 +224,11 @@ const OrdersTab = ({ orderIdFromHash, t }) => {
display: "flex",
alignItems: "center",
gap: "8px",
color: getStatusColor(displayStatus),
color: getStatusColor(order.status),
}}
>
<span style={{ fontSize: "1.2rem" }}>
{getStatusEmoji(displayStatus)}
{getStatusEmoji(order.status)}
</span>
<Typography
variant="body2"
@@ -243,6 +238,18 @@ const OrdersTab = ({ orderIdFromHash, t }) => {
{displayStatus}
</Typography>
</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>
{order.items

View File

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

View File

@@ -5,6 +5,7 @@ export default {
"profile": "الملف الشخصي",
"email": "البريد الإلكتروني",
"password": "كلمة المرور",
"newPassword": "كلمة المرور الجديدة",
"confirmPassword": "تأكيد كلمة المرور",
"forgotPassword": "هل نسيت كلمة المرور؟",
"loginWithGoogle": "تسجيل الدخول باستخدام جوجل",
@@ -13,6 +14,7 @@ export default {
"privacyPolicy": "سياسة الخصوصية",
"passwordMinLength": "يجب أن تكون كلمة المرور 8 أحرف على الأقل",
"newPasswordMinLength": "يجب أن تكون كلمة المرور الجديدة 8 أحرف على الأقل",
"backToHome": "العودة إلى الصفحة الرئيسية",
"menu": {
"profile": "الملف الشخصي",
"myProfile": "ملفي الشخصي",
@@ -21,5 +23,28 @@ export default {
"settings": "الإعدادات",
"adminDashboard": "لوحة تحكم المسؤول",
"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 cart from './cart.js';
import product from './product.js';
import productDialogs from './productDialogs.js';
import search from './search.js';
import sorting from './sorting.js';
import chat from './chat.js';
@@ -18,6 +19,7 @@ import pages from './pages.js';
import orders from './orders.js';
import settings from './settings.js';
import common from './common.js';
import kitConfig from './kitConfig.js';
import legalDatenschutzBasic from './legal-datenschutz-basic.js';
import legalDatenschutzCustomer from './legal-datenschutz-customer.js';
import legalDatenschutzGoogleOrders from './legal-datenschutz-google-orders.js';
@@ -35,6 +37,7 @@ export default {
"auth": auth,
"cart": cart,
"product": product,
"productDialogs": productDialogs,
"search": search,
"sorting": sorting,
"chat": chat,
@@ -50,6 +53,7 @@ export default {
"orders": orders,
"settings": settings,
"common": common,
"kitConfig": kitConfig,
"legalDatenschutzBasic": legalDatenschutzBasic,
"legalDatenschutzCustomer": legalDatenschutzCustomer,
"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": "قيد التنفيذ",
"pending": "جديد",
"processing": "قيد التنفيذ",
"cancelled": لغاة",
"paid": دفوع",
"cancelled": "ملغي",
"shipped": "تم الشحن",
"delivered": "تم التوصيل",
"return": "إرجاع",
@@ -24,6 +25,7 @@ export default {
"cancelOrder": "إلغاء الطلب"
},
"noOrders": "لم تقم بوضع أي طلبات بعد.",
"trackShipment": "تتبع الشحنة",
"details": {
"title": "تفاصيل الطلب: {{orderId}}",
"deliveryAddress": "عنوان التوصيل",
@@ -36,14 +38,13 @@ export default {
"item": "العنصر",
"quantity": "الكمية",
"price": "السعر",
"vat": "ضريبة القيمة المضافة",
"total": "الإجمالي",
"cancelOrder": "إلغاء الطلب"
},
"cancelConfirm": {
"title": "إلغاء الطلب",
"message": "هل أنت متأكد أنك تريد إلغاء هذا الطلب؟",
"confirm": "إلغاء الطلب",
"confirm": "إلغاء",
"cancelling": "جارٍ الإلغاء..."
},
"processing": "يتم إكمال الطلب..."

View File

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

@@ -1,11 +1,12 @@
export default {
"seeds": "بذور",
"stecklinge": "قصاصات",
"oilPress": "استعارة معصرة الزيت",
"konfigurator": "المُكوّن",
"oilPress": "استعارة مكبس الزيت",
"thcTest": "اختبار THC",
"address1": "Trachenberger Straße 14",
"address2": "01129 Dresden",
"showUsPhoto": "ورينا أجمل صورة عندك",
"selectSeedRate": "اختار البذرة واضغط تقييم",
"indoorSeason": "موسم الزراعة الداخلية بدأ"
"address1": "شارع تراشينبرجر 14",
"address2": "01129 دريسدن",
"showUsPhoto": "اعرض لنا أجمل صورة لديك",
"selectSeedRate": "اختر البذرة، واضغط للتقييم",
"indoorSeason": "بدأ موسم الزراعة الداخلية"
};

View File

@@ -1,5 +1,5 @@
export default {
"home": "بذور وقصاصات القنب الممتازة",
"home": "بذور القنب الممتازة",
"aktionen": "العروض والتخفيضات الحالية",
"filiale": "متجرنا في دريسدن",
"filiale": "متجرنا في دريسدن"
};

View File

@@ -5,14 +5,16 @@ export default {
"profile": "Профил",
"email": "Имейл",
"password": "Парола",
"newPassword": "Нова парола",
"confirmPassword": "Потвърдете паролата",
"forgotPassword": "Забравена парола?",
"loginWithGoogle": "Вход с Google",
"or": "ИЛИ",
"privacyAccept": "С натискане на \"Вход с Google\" приемам",
"privacyAccept": "С натискането на \"Вход с Google\" приемам",
"privacyPolicy": "Политиката за поверителност",
"passwordMinLength": "Паролата трябва да е поне 8 символа",
"newPasswordMinLength": "Новата парола трябва да е поне 8 символа",
"backToHome": "Обратно към началната страница",
"menu": {
"profile": "Профил",
"myProfile": "Моят профил",
@@ -21,5 +23,28 @@ export default {
"settings": "Настройки",
"adminDashboard": "Админ табло",
"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 cart from './cart.js';
import product from './product.js';
import productDialogs from './productDialogs.js';
import search from './search.js';
import sorting from './sorting.js';
import chat from './chat.js';
@@ -18,6 +19,7 @@ import pages from './pages.js';
import orders from './orders.js';
import settings from './settings.js';
import common from './common.js';
import kitConfig from './kitConfig.js';
import legalDatenschutzBasic from './legal-datenschutz-basic.js';
import legalDatenschutzCustomer from './legal-datenschutz-customer.js';
import legalDatenschutzGoogleOrders from './legal-datenschutz-google-orders.js';
@@ -35,6 +37,7 @@ export default {
"auth": auth,
"cart": cart,
"product": product,
"productDialogs": productDialogs,
"search": search,
"sorting": sorting,
"chat": chat,
@@ -50,6 +53,7 @@ export default {
"orders": orders,
"settings": settings,
"common": common,
"kitConfig": kitConfig,
"legalDatenschutzBasic": legalDatenschutzBasic,
"legalDatenschutzCustomer": legalDatenschutzCustomer,
"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 {
"status": {
"new": "В процес",
"pending": "Нова",
"processing": "В процес",
"cancelled": "Отменена",
"shipped": "Изпратена",
"delivered": "Доставена",
"new": "в процес",
"pending": "Ново",
"processing": "в процес",
"paid": "Платено",
"cancelled": "Отменено",
"shipped": "Изпратено",
"delivered": "Доставено",
"return": "Връщане",
"partialReturn": "Частично връщане",
"partialDelivered": "Частично доставена"
"partialDelivered": "Частично доставено"
},
"table": {
"orderNumber": "Номер на поръчка",
@@ -24,6 +25,7 @@ export default {
"cancelOrder": "Отмени поръчката"
},
"noOrders": "Все още не сте направили поръчки.",
"trackShipment": "Проследи пратката",
"details": {
"title": "Подробности за поръчка: {{orderId}}",
"deliveryAddress": "Адрес за доставка",
@@ -36,15 +38,14 @@ export default {
"item": "Артикул",
"quantity": "Количество",
"price": "Цена",
"vat": "ДДС",
"total": "Общо",
"cancelOrder": "Отмени поръчката"
},
"cancelConfirm": {
"title": "Отмяна на поръчка",
"title": "Отмени поръчката",
"message": "Сигурни ли сте, че искате да отмените тази поръчка?",
"confirm": "Отмени поръчката",
"confirm": "Отмени",
"cancelling": "Отмяна..."
},
"processing": "Поръчката се обработва...",
"processing": "Поръчката се обработва..."
};

View File

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

@@ -1,7 +1,8 @@
export default {
"seeds": "Семена",
"stecklinge": "Резници",
"oilPress": "Наеми преса за масло",
"konfigurator": "Конфигуратор",
"oilPress": "Наеми преса за олио",
"thcTest": "THC тест",
"address1": "Trachenberger Straße 14",
"address2": "01129 Dresden",

View File

@@ -1,5 +1,5 @@
export default {
"home": "Фини семена и резници от канабис",
"home": "Качествени канабис семена",
"aktionen": "Текущи промоции и оферти",
"filiale": "Нашият магазин в Дрезден",
"filiale": "Нашият магазин в Дрезден"
};

View File

@@ -5,6 +5,7 @@ export default {
"profile": "Profil",
"email": "Email",
"password": "Heslo",
"newPassword": "Nové heslo",
"confirmPassword": "Potvrdit heslo",
"forgotPassword": "Zapomněli jste heslo?",
"loginWithGoogle": "Přihlásit se přes Google",
@@ -13,6 +14,7 @@ export default {
"privacyPolicy": "Zásadami ochrany osobních údajů",
"passwordMinLength": "Heslo musí mít alespoň 8 znaků",
"newPasswordMinLength": "Nové heslo musí mít alespoň 8 znaků",
"backToHome": "Zpět na domovskou stránku",
"menu": {
"profile": "Profil",
"myProfile": "Můj profil",
@@ -21,5 +23,28 @@ export default {
"settings": "Nastavení",
"adminDashboard": "Admin Dashboard",
"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 cart from './cart.js';
import product from './product.js';
import productDialogs from './productDialogs.js';
import search from './search.js';
import sorting from './sorting.js';
import chat from './chat.js';
@@ -18,6 +19,7 @@ import pages from './pages.js';
import orders from './orders.js';
import settings from './settings.js';
import common from './common.js';
import kitConfig from './kitConfig.js';
import legalDatenschutzBasic from './legal-datenschutz-basic.js';
import legalDatenschutzCustomer from './legal-datenschutz-customer.js';
import legalDatenschutzGoogleOrders from './legal-datenschutz-google-orders.js';
@@ -35,6 +37,7 @@ export default {
"auth": auth,
"cart": cart,
"product": product,
"productDialogs": productDialogs,
"search": search,
"sorting": sorting,
"chat": chat,
@@ -50,6 +53,7 @@ export default {
"orders": orders,
"settings": settings,
"common": common,
"kitConfig": kitConfig,
"legalDatenschutzBasic": legalDatenschutzBasic,
"legalDatenschutzCustomer": legalDatenschutzCustomer,
"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,8 +1,9 @@
export default {
"status": {
"new": "Probíhá",
"pending": "Nová",
"processing": "Probíhá",
"new": "probíhá",
"pending": "Nové",
"processing": "probíhá",
"paid": "Zaplaceno",
"cancelled": "Zrušeno",
"shipped": "Odesláno",
"delivered": "Doručeno",
@@ -24,6 +25,7 @@ export default {
"cancelOrder": "Zrušit objednávku"
},
"noOrders": "Ještě jste neprovedli žádné objednávky.",
"trackShipment": "Sledovat zásilku",
"details": {
"title": "Detaily objednávky: {{orderId}}",
"deliveryAddress": "Dodací adresa",
@@ -36,15 +38,14 @@ export default {
"item": "Položka",
"quantity": "Množství",
"price": "Cena",
"vat": "DPH",
"total": "Celkem",
"cancelOrder": "Zrušit objednávku"
},
"cancelConfirm": {
"title": "Zrušit objednávku",
"message": "Opravdu chcete tuto objednávku zrušit?",
"confirm": "Zrušit objednávku",
"confirm": "Zrušit",
"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",
"manufacturer": "Výrobce",
"inclVat": "včetně {{vat}}% DPH",
"inclVatSimple": "včetně DPH",
"priceUnit": "{{price}}/{{unit}}",
"new": "Nové",
"weeks": "týdny",
"arriving": "Příchod:",
"weeks": "Týdny",
"arriving": "Příjezd:",
"inclVatFooter": "včetně {{vat}}% DPH,*",
"availability": "Dostupnost",
"inStock": "skladem",
@@ -22,10 +23,10 @@ export default {
"weight": "Hmotnost: {{weight}} kg",
"youSave": "Ušetříte: {{amount}}",
"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:",
"loadingComponentDetails": "{{index}}. Načítání detailů komponenty...",
"loadingProduct": "Produkt se načítá...",
"loadingProduct": "Načítání produktu...",
"individualPriceTotal": "Celková cena jednotlivě:",
"setPrice": "Cena sady:",
"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

@@ -1,6 +1,7 @@
export default {
"seeds": "Semena",
"stecklinge": "Řízky",
"konfigurator": "Konfigurátor",
"oilPress": "Půjčit lis na olej",
"thcTest": "THC test",
"address1": "Trachenberger Straße 14",

View File

@@ -1,5 +1,5 @@
export default {
"home": "Kvalitní semena a řízky konopí",
"home": "Kvalitní semena konopí",
"aktionen": "Aktuální akce a nabídky",
"filiale": "Naše prodejna v Drážďanech",
"filiale": "Naše prodejna v Drážďanech"
};

View File

@@ -5,6 +5,7 @@ export default {
"profile": "Profil",
"email": "E-Mail",
"password": "Passwort",
"newPassword": "Neues Passwort",
"confirmPassword": "Passwort bestätigen",
"forgotPassword": "Passwort vergessen?",
"loginWithGoogle": "Mit Google anmelden",
@@ -13,6 +14,7 @@ export default {
"privacyPolicy": "Datenschutzbestimmungen",
"passwordMinLength": "Das Passwort muss mindestens 8 Zeichen lang sein",
"newPasswordMinLength": "Das neue Passwort muss mindestens 8 Zeichen lang sein",
"backToHome": "Zurück zur Startseite",
"menu": {
"profile": "Profil",
"myProfile": "Mein Profil",
@@ -21,5 +23,28 @@ export default {
"settings": "Einstellungen",
"adminDashboard": "Admin Dashboard",
"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 cart from './cart.js';
import product from './product.js';
import productDialogs from './productDialogs.js';
import search from './search.js';
import sorting from './sorting.js';
import chat from './chat.js';
@@ -18,6 +19,7 @@ import pages from './pages.js';
import orders from './orders.js';
import settings from './settings.js';
import common from './common.js';
import kitConfig from './kitConfig.js';
import legalDatenschutzBasic from './legal-datenschutz-basic.js';
import legalDatenschutzCustomer from './legal-datenschutz-customer.js';
import legalDatenschutzGoogleOrders from './legal-datenschutz-google-orders.js';
@@ -35,6 +37,7 @@ export default {
"auth": auth,
"cart": cart,
"product": product,
"productDialogs": productDialogs,
"search": search,
"sorting": sorting,
"chat": chat,
@@ -50,6 +53,7 @@ export default {
"orders": orders,
"settings": settings,
"common": common,
"kitConfig": kitConfig,
"legalDatenschutzBasic": legalDatenschutzBasic,
"legalDatenschutzCustomer": legalDatenschutzCustomer,
"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",
"pending": "Neu",
"processing": "in Bearbeitung",
"paid": "Bezahlt",
"cancelled": "Storniert",
"shipped": "Verschickt",
"delivered": "Geliefert",
@@ -24,6 +25,7 @@ export default {
"cancelOrder": "Bestellung stornieren"
},
"noOrders": "Sie haben noch keine Bestellungen aufgegeben.",
"trackShipment": "Sendung verfolgen",
"details": {
"title": "Bestelldetails: {{orderId}}",
"deliveryAddress": "Lieferadresse",

View File

@@ -8,6 +8,7 @@ export default {
"articleNumber": "Artikelnummer",
"manufacturer": "Hersteller",
"inclVat": "inkl. {{vat}}% MwSt.",
"inclVatSimple": "inkl. MwSt.",
"priceUnit": "{{price}}/{{unit}}",
"new": "Neu",
"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

@@ -1,6 +1,7 @@
export default {
"seeds": "Seeds",
"stecklinge": "Stecklinge",
"konfigurator": "Konfigurator",
"oilPress": "Ölpresse ausleihen",
"thcTest": "THC Test",
"address1": "Trachenberger Straße 14",

View File

@@ -1,5 +1,5 @@
export default {
"home": "Fine Cannabis Seeds & Cuttings",
"home": "Fine Cannabis Seeds",
"aktionen": "Aktuelle Aktionen & Angebote",
"filiale": "Unsere Filiale in Dresden"
};

View File

@@ -5,14 +5,16 @@ export default {
"profile": "Προφίλ",
"email": "Email",
"password": "Κωδικός",
"newPassword": "Νέος κωδικός",
"confirmPassword": "Επιβεβαίωση κωδικού",
"forgotPassword": "Ξεχάσατε τον κωδικό;",
"loginWithGoogle": "Σύνδεση με Google",
"or": "Ή",
"privacyAccept": "Κάνοντας κλικ στο \"Σύνδεση με Google\" αποδέχομαι την",
"privacyPolicy": "Πολιτική Απορρήτου",
"privacyPolicy": "Πολιτική απορρήτου",
"passwordMinLength": "Ο κωδικός πρέπει να έχει τουλάχιστον 8 χαρακτήρες",
"newPasswordMinLength": "Ο νέος κωδικός πρέπει να έχει τουλάχιστον 8 χαρακτήρες",
"backToHome": "Επιστροφή στην αρχική σελίδα",
"menu": {
"profile": "Προφίλ",
"myProfile": "Το προφίλ μου",
@@ -21,5 +23,28 @@ export default {
"settings": "Ρυθμίσεις",
"adminDashboard": "Πίνακας διαχείρισης",
"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 cart from './cart.js';
import product from './product.js';
import productDialogs from './productDialogs.js';
import search from './search.js';
import sorting from './sorting.js';
import chat from './chat.js';
@@ -18,6 +19,7 @@ import pages from './pages.js';
import orders from './orders.js';
import settings from './settings.js';
import common from './common.js';
import kitConfig from './kitConfig.js';
import legalDatenschutzBasic from './legal-datenschutz-basic.js';
import legalDatenschutzCustomer from './legal-datenschutz-customer.js';
import legalDatenschutzGoogleOrders from './legal-datenschutz-google-orders.js';
@@ -35,6 +37,7 @@ export default {
"auth": auth,
"cart": cart,
"product": product,
"productDialogs": productDialogs,
"search": search,
"sorting": sorting,
"chat": chat,
@@ -50,6 +53,7 @@ export default {
"orders": orders,
"settings": settings,
"common": common,
"kitConfig": kitConfig,
"legalDatenschutzBasic": legalDatenschutzBasic,
"legalDatenschutzCustomer": legalDatenschutzCustomer,
"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 {
"status": {
"new": "Σε εξέλιξη",
"new": "σε εξέλιξη",
"pending": "Νέο",
"processing": "Σε εξέλιξη",
"processing": "σε εξέλιξη",
"paid": "Πληρωμένο",
"cancelled": "Ακυρώθηκε",
"shipped": "Απεστάλη",
"shipped": "Απεσταλμένο",
"delivered": "Παραδόθηκε",
"return": "Επιστροφή",
"partialReturn": "Μερική επιστροφή",
@@ -24,10 +25,11 @@ export default {
"cancelOrder": "Ακύρωση παραγγελίας"
},
"noOrders": "Δεν έχετε κάνει ακόμα καμία παραγγελία.",
"trackShipment": "Παρακολούθηση αποστολής",
"details": {
"title": "Λεπτομέρειες παραγγελίας: {{orderId}}",
"deliveryAddress": "Διεύθυνση παράδοσης",
"invoiceAddress": "Διεύθυνση τιμολόγησης",
"invoiceAddress": "Διεύθυνση τιμολογίου",
"orderDetails": "Λεπτομέρειες παραγγελίας",
"deliveryMethod": "Τρόπος παράδοσης:",
"paymentMethod": "Τρόπος πληρωμής:",
@@ -36,15 +38,14 @@ export default {
"item": "Είδος",
"quantity": "Ποσότητα",
"price": "Τιμή",
"vat": "ΦΠΑ",
"total": "Σύνολο",
"cancelOrder": "Ακύρωση παραγγελίας"
},
"cancelConfirm": {
"title": "Ακύρωση παραγγελίας",
"message": "Είστε σίγουροι ότι θέλετε να ακυρώσετε αυτήν την παραγγελία;",
"confirm": "Ακύρωση παραγγελίας",
"cancelling": "Ακύρωση..."
"message": "Είστε σίγουροι ότι θέλετε να ακυρώσετε αυτή την παραγγελία;",
"confirm": "Ακύρωση",
"cancelling": "Ακύρωση σε εξέλιξη..."
},
"processing": "Η παραγγελία ολοκληρώνεται..."
};

View File

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

@@ -1,6 +1,7 @@
export default {
"seeds": "Σπόροι",
"stecklinge": "Μοσχεύματα",
"konfigurator": "Διαμορφωτής",
"oilPress": "Δανείσου πρέσα λαδιού",
"thcTest": "Τεστ THC",
"address1": "Trachenberger Straße 14",

View File

@@ -1,5 +1,5 @@
export default {
"home": "Ποιοτικοί Σπόροι & Μοσχεύματα Κάνναβης",
"aktionen": "Τρέχουσες Προσφορές & Εκπτώσεις",
"filiale": "Το Κατάστημά μας στη Δρέσδη",
"home": "Ποιοτικοί Σπόροι Κάνναβης",
"aktionen": "Τρέχουσες προσφορές & εκπτώσεις",
"filiale": "Το κατάστημά μας στη Δρέσδη"
};

View File

@@ -5,14 +5,16 @@ export default {
"profile": "Profile", // Profil
"email": "Email", // E-Mail
"password": "Password", // Passwort
"newPassword": "New password", // Neues Passwort
"confirmPassword": "Confirm password", // Passwort bestätigen
"forgotPassword": "Forgot password?", // Passwort vergessen?
"loginWithGoogle": "Sign in with Google", // Mit Google anmelden
"or": "OR", // ODER
"privacyAccept": "By clicking \"Sign in with Google\" I accept the", // Mit dem Click auf "Mit Google anmelden" akzeptiere ich die
"privacyPolicy": "Privacy Policy", // Datenschutzbestimmungen
"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
"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
"backToHome": "Back to homepage", // Zurück zur Startseite
"menu": {
"profile": "Profile", // Profil
"myProfile": "My profile", // Mein Profil
@@ -21,5 +23,28 @@ export default {
"settings": "Settings", // Einstellungen
"adminDashboard": "Admin Dashboard", // Admin Dashboard
"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 cart from './cart.js';
import product from './product.js';
import productDialogs from './productDialogs.js';
import search from './search.js';
import sorting from './sorting.js';
import chat from './chat.js';
@@ -18,6 +19,7 @@ import pages from './pages.js';
import orders from './orders.js';
import settings from './settings.js';
import common from './common.js';
import kitConfig from './kitConfig.js';
import legalDatenschutzBasic from './legal-datenschutz-basic.js';
import legalDatenschutzCustomer from './legal-datenschutz-customer.js';
import legalDatenschutzGoogleOrders from './legal-datenschutz-google-orders.js';
@@ -35,6 +37,7 @@ export default {
"auth": auth,
"cart": cart,
"product": product,
"productDialogs": productDialogs,
"search": search,
"sorting": sorting,
"chat": chat,
@@ -50,6 +53,7 @@ export default {
"orders": orders,
"settings": settings,
"common": common,
"kitConfig": kitConfig,
"legalDatenschutzBasic": legalDatenschutzBasic,
"legalDatenschutzCustomer": legalDatenschutzCustomer,
"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 {
"status": {
"new": "In progress", // in Bearbeitung
"new": "in progress", // in Bearbeitung
"pending": "New", // Neu
"processing": "In progress", // in Bearbeitung
"processing": "in progress", // in Bearbeitung
"paid": "Paid", // Bezahlt
"cancelled": "Cancelled", // Storniert
"shipped": "Shipped", // Verschickt
"delivered": "Delivered", // Geliefert
@@ -24,6 +25,7 @@ export default {
"cancelOrder": "Cancel order" // Bestellung stornieren
},
"noOrders": "You have not placed any orders yet.", // Sie haben noch keine Bestellungen aufgegeben.
"trackShipment": "Track shipment", // Sendung verfolgen
"details": {
"title": "Order details: {{orderId}}", // Bestelldetails: {{orderId}}
"deliveryAddress": "Delivery address", // Lieferadresse
@@ -36,15 +38,14 @@ export default {
"item": "Item", // Artikel
"quantity": "Quantity", // Menge
"price": "Price", // Preis
"vat": "VAT", // MwSt.
"total": "Total", // Gesamt
"cancelOrder": "Cancel order" // Bestellung stornieren
},
"cancelConfirm": {
"title": "Cancel Order",
"message": "Are you sure you want to cancel this order?",
"confirm": "Cancel Order",
"cancelling": "Cancelling..."
"title": "Cancel order", // Bestellung stornieren
"message": "Are you sure you want to cancel this order?", // Sind Sie sicher, dass Sie diese Bestellung stornieren möchten?
"confirm": "Cancel", // Stornieren
"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
"manufacturer": "Manufacturer", // Hersteller
"inclVat": "incl. {{vat}}% VAT", // inkl. {{vat}}% MwSt.
"inclVatSimple": "incl. VAT", // inkl. MwSt.
"priceUnit": "{{price}}/{{unit}}", // {{price}}/{{unit}}
"new": "New", // Neu
"weeks": "weeks", // Wochen
"weeks": "Weeks", // Wochen
"arriving": "Arrival:", // Ankunft:
"inclVatFooter": "incl. {{vat}}% VAT,*", // inkl. {{vat}}% MwSt.,*
"availability": "Availability", // Verfügbarkeit
@@ -23,13 +24,13 @@ export default {
"youSave": "You save: {{amount}}", // Sie sparen: {{amount}}
"cheaperThanIndividual": "Cheaper than buying individually", // Günstiger als Einzelkauf
"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...
"loadingProduct": "Product is loading...", // Produkt wird geladen...
"loadingProduct": "Loading product...", // Produkt wird geladen...
"individualPriceTotal": "Total individual price:", // Einzelpreis gesamt:
"setPrice": "Set price:", // Set-Preis:
"yourSavings": "Your savings:", // Ihre Ersparnis:
"similarProducts": "Similar Products", // Ähnliche Produkte
"similarProducts": "Similar products", // Ähnliche Produkte
"countDisplay": {
"noProducts": "0 products", // 0 Produkte
"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

@@ -1,6 +1,7 @@
export default {
"seeds": "Seeds", // Seeds
"stecklinge": "Cuttings", // Stecklinge
"konfigurator": "Configurator", // Konfigurator
"oilPress": "Borrow oil press", // Ölpresse ausleihen
"thcTest": "THC test", // THC Test
"address1": "Trachenberger Straße 14", // Trachenberger Straße 14

View File

@@ -1,5 +1,5 @@
export default {
"home": "Fine Cannabis Seeds & Cuttings", // Fine Cannabis Samen & Stecklinge
"aktionen": "Current Promotions & Offers", // Aktuelle Aktionen & Angebote
"filiale": "Our Store in Dresden", // Unsere Filiale in Dresden
"home": "Fine Cannabis Seeds", // Fine Cannabis Seeds
"aktionen": "Current promotions & offers", // Aktuelle Aktionen & Angebote
"filiale": "Our store in Dresden" // Unsere Filiale in Dresden
};

View File

@@ -5,6 +5,7 @@ export default {
"profile": "Perfil",
"email": "Correo electrónico",
"password": "Contraseña",
"newPassword": "Nueva contraseña",
"confirmPassword": "Confirmar contraseña",
"forgotPassword": "¿Olvidaste tu contraseña?",
"loginWithGoogle": "Iniciar sesión con Google",
@@ -13,6 +14,7 @@ export default {
"privacyPolicy": "Política de privacidad",
"passwordMinLength": "La 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": {
"profile": "Perfil",
"myProfile": "Mi perfil",
@@ -21,5 +23,28 @@ export default {
"settings": "Configuración",
"adminDashboard": "Panel de administración",
"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 cart from './cart.js';
import product from './product.js';
import productDialogs from './productDialogs.js';
import search from './search.js';
import sorting from './sorting.js';
import chat from './chat.js';
@@ -18,6 +19,7 @@ import pages from './pages.js';
import orders from './orders.js';
import settings from './settings.js';
import common from './common.js';
import kitConfig from './kitConfig.js';
import legalDatenschutzBasic from './legal-datenschutz-basic.js';
import legalDatenschutzCustomer from './legal-datenschutz-customer.js';
import legalDatenschutzGoogleOrders from './legal-datenschutz-google-orders.js';
@@ -35,6 +37,7 @@ export default {
"auth": auth,
"cart": cart,
"product": product,
"productDialogs": productDialogs,
"search": search,
"sorting": sorting,
"chat": chat,
@@ -50,6 +53,7 @@ export default {
"orders": orders,
"settings": settings,
"common": common,
"kitConfig": kitConfig,
"legalDatenschutzBasic": legalDatenschutzBasic,
"legalDatenschutzCustomer": legalDatenschutzCustomer,
"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 {
"status": {
"new": "En progreso",
"new": "en progreso",
"pending": "Nuevo",
"processing": "En progreso",
"processing": "en progreso",
"paid": "Pagado",
"cancelled": "Cancelado",
"shipped": "Enviado",
"delivered": "Entregado",
@@ -24,6 +25,7 @@ export default {
"cancelOrder": "Cancelar pedido"
},
"noOrders": "Aún no has realizado ningún pedido.",
"trackShipment": "Rastrear envío",
"details": {
"title": "Detalles del pedido: {{orderId}}",
"deliveryAddress": "Dirección de entrega",
@@ -36,15 +38,14 @@ export default {
"item": "Artículo",
"quantity": "Cantidad",
"price": "Precio",
"vat": "IVA",
"total": "Total",
"cancelOrder": "Cancelar pedido"
},
"cancelConfirm": {
"title": "Cancelar pedido",
"message": "¿Estás seguro de que deseas cancelar este pedido?",
"confirm": "Cancelar pedido",
"message": "¿Está seguro de que desea cancelar este pedido?",
"confirm": "Cancelar",
"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",
"manufacturer": "Fabricante",
"inclVat": "incl. {{vat}}% IVA",
"inclVatSimple": "incl. IVA",
"priceUnit": "{{price}}/{{unit}}",
"new": "Nuevo",
"weeks": "semanas",
"weeks": "Semanas",
"arriving": "Llegada:",
"inclVatFooter": "incl. {{vat}}% IVA,*",
"availability": "Disponibilidad",
@@ -25,7 +26,7 @@ export default {
"pickupPrice": "Precio de recogida: 19,90 € por esqueje.",
"consistsOf": "Consiste en:",
"loadingComponentDetails": "{{index}}. Cargando detalles del componente...",
"loadingProduct": "Producto cargando...",
"loadingProduct": "Cargando producto...",
"individualPriceTotal": "Precio individual total:",
"setPrice": "Precio del set:",
"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,11 +1,12 @@
export default {
"seeds": "Semillas",
"stecklinge": "Esquejes",
"konfigurator": "Configurador",
"oilPress": "Pedir prestada prensa de aceite",
"thcTest": "Prueba de THC",
"address1": "Trachenberger Straße 14",
"address2": "01129 Dresden",
"showUsPhoto": "Muéstranos tu foto más hermosa",
"selectSeedRate": "Selecciona semilla, haz clic para valorar",
"selectSeedRate": "Selecciona semilla, haz clic en valorar",
"indoorSeason": "Comienza la temporada de interior"
};

View File

@@ -1,5 +1,5 @@
export default {
"home": "Semillas y esquejes de cannabis de calidad",
"home": "Semillas de Cannabis de Calidad",
"aktionen": "Promociones y ofertas actuales",
"filiale": "Nuestra tienda en Dresden",
"filiale": "Nuestra tienda en Dresden"
};

View File

@@ -1,10 +1,11 @@
export default {
"login": "Connexion",
"register": "S'inscrire",
"register": "Inscription",
"logout": "Déconnexion",
"profile": "Profil",
"email": "Email",
"password": "Mot de passe",
"newPassword": "Nouveau mot de passe",
"confirmPassword": "Confirmer le mot de passe",
"forgotPassword": "Mot de passe oublié ?",
"loginWithGoogle": "Se connecter avec Google",
@@ -13,6 +14,7 @@ export default {
"privacyPolicy": "Politique de confidentialité",
"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",
"backToHome": "Retour à la page d'accueil",
"menu": {
"profile": "Profil",
"myProfile": "Mon profil",
@@ -21,5 +23,28 @@ export default {
"settings": "Paramètres",
"adminDashboard": "Tableau de bord 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 cart from './cart.js';
import product from './product.js';
import productDialogs from './productDialogs.js';
import search from './search.js';
import sorting from './sorting.js';
import chat from './chat.js';
@@ -18,6 +19,7 @@ import pages from './pages.js';
import orders from './orders.js';
import settings from './settings.js';
import common from './common.js';
import kitConfig from './kitConfig.js';
import legalDatenschutzBasic from './legal-datenschutz-basic.js';
import legalDatenschutzCustomer from './legal-datenschutz-customer.js';
import legalDatenschutzGoogleOrders from './legal-datenschutz-google-orders.js';
@@ -35,6 +37,7 @@ export default {
"auth": auth,
"cart": cart,
"product": product,
"productDialogs": productDialogs,
"search": search,
"sorting": sorting,
"chat": chat,
@@ -50,6 +53,7 @@ export default {
"orders": orders,
"settings": settings,
"common": common,
"kitConfig": kitConfig,
"legalDatenschutzBasic": legalDatenschutzBasic,
"legalDatenschutzCustomer": legalDatenschutzCustomer,
"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 {
"status": {
"new": "En cours",
"new": "en cours",
"pending": "Nouveau",
"processing": "En cours",
"processing": "en cours",
"paid": "Payé",
"cancelled": "Annulé",
"shipped": "Expédié",
"delivered": "Livré",
@@ -24,6 +25,7 @@ export default {
"cancelOrder": "Annuler la commande"
},
"noOrders": "Vous n'avez pas encore passé de commandes.",
"trackShipment": "Suivre l'envoi",
"details": {
"title": "Détails de la commande : {{orderId}}",
"deliveryAddress": "Adresse de livraison",
@@ -36,14 +38,13 @@ export default {
"item": "Article",
"quantity": "Quantité",
"price": "Prix",
"vat": "TVA",
"total": "Total",
"cancelOrder": "Annuler la commande"
},
"cancelConfirm": {
"title": "Annuler la commande",
"message": "Êtes-vous sûr de vouloir annuler cette commande ?",
"confirm": "Annuler la commande",
"confirm": "Annuler",
"cancelling": "Annulation en cours..."
},
"processing": "La commande est en cours de traitement..."

View File

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

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