From 1897ceb7c5c6b3922ebfde59eefbb4083ccbe5da Mon Sep 17 00:00:00 2001 From: sebseb7 Date: Thu, 26 Mar 2026 11:56:07 +0100 Subject: [PATCH] feat: Enhance image processing in data-fetching and update SEO meta tags for product images; add Telegram assistant link in ChatAssistant component with localization support --- prerender/data-fetching.cjs | 136 ++++++++++-------- prerender/seo/product.cjs | 34 +++-- src/components/ChatAssistant.js | 63 +++++++- src/components/Product.js | 2 +- src/i18n/locales/ar/chat.js | 4 +- .../locales/ar/legal-datenschutz-chatbot.js | 14 +- src/i18n/locales/bg/chat.js | 2 + .../locales/bg/legal-datenschutz-chatbot.js | 18 +-- src/i18n/locales/cs/chat.js | 2 + .../locales/cs/legal-datenschutz-chatbot.js | 16 +-- src/i18n/locales/de/chat.js | 4 +- src/i18n/locales/el/chat.js | 4 +- .../locales/el/legal-datenschutz-chatbot.js | 16 +-- src/i18n/locales/en/chat.js | 2 + src/i18n/locales/es/chat.js | 2 + .../locales/es/legal-datenschutz-chatbot.js | 12 +- src/i18n/locales/fr/chat.js | 2 + .../locales/fr/legal-datenschutz-chatbot.js | 10 +- src/i18n/locales/hr/chat.js | 2 + .../locales/hr/legal-datenschutz-chatbot.js | 14 +- src/i18n/locales/hu/chat.js | 2 + .../locales/hu/legal-datenschutz-chatbot.js | 20 +-- src/i18n/locales/it/chat.js | 2 + .../locales/it/legal-datenschutz-chatbot.js | 20 +-- src/i18n/locales/pl/chat.js | 2 + .../locales/pl/legal-datenschutz-chatbot.js | 16 +-- src/i18n/locales/ro/chat.js | 2 + .../locales/ro/legal-datenschutz-chatbot.js | 18 +-- src/i18n/locales/ru/chat.js | 2 + .../locales/ru/legal-datenschutz-chatbot.js | 14 +- src/i18n/locales/sk/chat.js | 4 +- .../locales/sk/legal-datenschutz-chatbot.js | 12 +- src/i18n/locales/sl/chat.js | 2 + .../locales/sl/legal-datenschutz-chatbot.js | 18 +-- src/i18n/locales/sq/chat.js | 2 + .../locales/sq/legal-datenschutz-chatbot.js | 18 +-- src/i18n/locales/sr/chat.js | 2 + .../locales/sr/legal-datenschutz-chatbot.js | 16 +-- src/i18n/locales/sv/chat.js | 4 +- .../locales/sv/legal-datenschutz-chatbot.js | 20 +-- src/i18n/locales/tr/chat.js | 4 +- .../locales/tr/legal-datenschutz-chatbot.js | 20 +-- src/i18n/locales/uk/chat.js | 2 + .../locales/uk/legal-datenschutz-chatbot.js | 14 +- src/i18n/locales/zh/chat.js | 2 + .../locales/zh/legal-datenschutz-chatbot.js | 16 +-- src/utils/starPolygon.js | 3 +- 47 files changed, 372 insertions(+), 244 deletions(-) diff --git a/prerender/data-fetching.cjs b/prerender/data-fetching.cjs index 0b1b6d0..09e2391 100644 --- a/prerender/data-fetching.cjs +++ b/prerender/data-fetching.cjs @@ -186,80 +186,98 @@ const saveProductImages = async (socket, products, categoryName, outputDir) => { .filter((id) => id); if (imageIds.length > 0) { - // Process first image for each product + // Process first image for each product — store AVIF + JPEG (e.g. for Twitter / social) const bildId = parseInt(imageIds[0]); - const estimatedFilename = `prod${bildId}.avif`; // We'll generate a filename based on the ID + const avifFilename = `prod${bildId}.avif`; + const jpegFilename = `prod${bildId}.jpg`; + const avifPath = path.join(assetsPath, avifFilename); + const jpegPath = path.join(assetsPath, jpegFilename); - const imagePath = path.join(assetsPath, estimatedFilename); - - // Skip if image already exists - if (fs.existsSync(imagePath)) { + if (fs.existsSync(avifPath) && fs.existsSync(jpegPath)) { imagesSkipped++; continue; } + const writeAvifAndJpegFromBuffer = async (buf) => { + if (!fs.existsSync(avifPath)) { + await sharp(buf).avif().toFile(avifPath); + } + if (!fs.existsSync(jpegPath)) { + await sharp(buf) + .jpeg({ quality: 85, mozjpeg: true }) + .toFile(jpegPath); + } + }; + try { - const imageBuffer = await fetchProductImage(socket, bildId); - - // If overlay exists, apply it to the image - if (false && fs.existsSync(overlayPath)) { - try { - // Get image dimensions to center the overlay - const baseImage = sharp(Buffer.from(imageBuffer)); - const baseMetadata = await baseImage.metadata(); - - const overlaySize = Math.min(baseMetadata.width, baseMetadata.height) * 0.4; - - // Resize overlay to 20% of base image size and get its buffer - const resizedOverlayBuffer = await sharp(overlayPath) - .resize({ - width: Math.round(overlaySize), - height: Math.round(overlaySize), - fit: 'contain', // Keep full overlay visible - background: { r: 0, g: 0, b: 0, alpha: 0 } // Transparent background instead of black bars - }) - .toBuffer(); - - // Calculate center position for the resized overlay - const centerX = Math.floor((baseMetadata.width - overlaySize) / 2); - const centerY = Math.floor((baseMetadata.height - overlaySize) / 2); - - const processedImageBuffer = await baseImage - .composite([ - { - input: resizedOverlayBuffer, - top: centerY, - left: centerX, - blend: "multiply", // Darkens the image, visible on all backgrounds - opacity: 0.3, - }, - ]) - .avif() // Ensure output is AVIF - .toBuffer(); - - fs.writeFileSync(imagePath, processedImageBuffer); - console.log( - ` ✅ Applied centered inverted sh.avif overlay to ${estimatedFilename}` - ); - } catch (overlayError) { - console.log( - ` ⚠️ Failed to apply overlay to ${estimatedFilename}: ${overlayError.message}` - ); - // Fallback: save without overlay - fs.writeFileSync(imagePath, Buffer.from(imageBuffer)); - } + if (fs.existsSync(avifPath) && !fs.existsSync(jpegPath)) { + await sharp(avifPath) + .jpeg({ quality: 85, mozjpeg: true }) + .toFile(jpegPath); + } else if (!fs.existsSync(avifPath) && fs.existsSync(jpegPath)) { + await sharp(jpegPath).avif().toFile(avifPath); } else { - // Save without overlay if overlay file doesn't exist - fs.writeFileSync(imagePath, Buffer.from(imageBuffer)); + const imageBuffer = await fetchProductImage(socket, bildId); + const buf = Buffer.from(imageBuffer); + + // If overlay exists, apply it to the image + if (false && fs.existsSync(overlayPath)) { + try { + const baseImage = sharp(buf); + const baseMetadata = await baseImage.metadata(); + + const overlaySize = + Math.min(baseMetadata.width, baseMetadata.height) * 0.4; + + const resizedOverlayBuffer = await sharp(overlayPath) + .resize({ + width: Math.round(overlaySize), + height: Math.round(overlaySize), + fit: "contain", + background: { r: 0, g: 0, b: 0, alpha: 0 }, + }) + .toBuffer(); + + const centerX = Math.floor( + (baseMetadata.width - overlaySize) / 2 + ); + const centerY = Math.floor( + (baseMetadata.height - overlaySize) / 2 + ); + + const processedImageBuffer = await baseImage + .composite([ + { + input: resizedOverlayBuffer, + top: centerY, + left: centerX, + blend: "multiply", + opacity: 0.3, + }, + ]) + .toBuffer(); + + await writeAvifAndJpegFromBuffer(processedImageBuffer); + console.log( + ` ✅ Applied overlay → ${avifFilename} + ${jpegFilename}` + ); + } catch (overlayError) { + console.log( + ` ⚠️ Failed to apply overlay to prod${bildId}: ${overlayError.message}` + ); + await writeAvifAndJpegFromBuffer(buf); + } + } else { + await writeAvifAndJpegFromBuffer(buf); + } } imagesSaved++; - // Small delay to avoid overwhelming server await new Promise((resolve) => setTimeout(resolve, 50)); } catch (error) { console.log( - ` ⚠️ Failed to fetch image ${estimatedFilename} (ID: ${bildId}): ${error.message}` + ` ⚠️ Failed to fetch/save prod${bildId} (${avifFilename} / ${jpegFilename}): ${error.message}` ); } } diff --git a/prerender/seo/product.cjs b/prerender/seo/product.cjs index 01dad48..dc2e71e 100644 --- a/prerender/seo/product.cjs +++ b/prerender/seo/product.cjs @@ -1,13 +1,18 @@ const generateProductMetaTags = (product, baseUrl, config) => { const productUrl = `${baseUrl}/Artikel/${product.seoName}`; - - const imageUrl = - product.pictureList && product.pictureList.trim() - ? `${baseUrl}/assets/images/prod${product.pictureList - .split(",")[0] - .trim()}.avif` - : `${baseUrl}/assets/images/nopicture.jpg`; + const pictureFirstId = + product.pictureList && product.pictureList.trim() + ? product.pictureList.split(",")[0].trim() + : null; + + const imageUrl = pictureFirstId + ? `${baseUrl}/assets/images/prod${pictureFirstId}.avif` + : `${baseUrl}/assets/images/nopicture.jpg`; + + const twitterImageUrl = pictureFirstId + ? `${baseUrl}/assets/images/prod${pictureFirstId}.jpg` + : `${baseUrl}/assets/images/nopicture.jpg`; // Clean description for meta (remove HTML tags and limit length) const cleanDescription = product.kurzBeschreibung @@ -32,7 +37,7 @@ const generateProductMetaTags = (product, baseUrl, config) => { - + @@ -49,7 +54,7 @@ const generateProductMetaTags = (product, baseUrl, config) => { - + @@ -64,12 +69,13 @@ const generateProductMetaTags = (product, baseUrl, config) => { const generateProductJsonLd = (product, baseUrl, config, categoryInfo = null) => { const productUrl = `${baseUrl}/Artikel/${product.seoName}`; - const imageUrl = + const pictureFirstId = product.pictureList && product.pictureList.trim() - ? `${baseUrl}/assets/images/prod${product.pictureList - .split(",")[0] - .trim()}.avif` - : `${baseUrl}/assets/images/nopicture.jpg`; + ? product.pictureList.split(",")[0].trim() + : null; + const imageUrl = pictureFirstId + ? `${baseUrl}/assets/images/prod${pictureFirstId}.avif` + : `${baseUrl}/assets/images/nopicture.jpg`; // Clean description for JSON-LD (remove HTML tags) const cleanDescription = product.description diff --git a/src/components/ChatAssistant.js b/src/components/ChatAssistant.js index 93b7c69..4a8b7c5 100644 --- a/src/components/ChatAssistant.js +++ b/src/components/ChatAssistant.js @@ -15,7 +15,13 @@ import StopIcon from '@mui/icons-material/Stop'; import PhotoCameraIcon from '@mui/icons-material/PhotoCamera'; import parse, { domToReact } from 'html-react-parser'; import { Link } from 'react-router-dom'; +import MuiLink from '@mui/material/Link'; +import { alpha } from '@mui/material/styles'; +import TelegramIcon from '@mui/icons-material/Telegram'; import { isUserLoggedIn } from './LoginComponent.js'; +import { withTranslation } from '../i18n/withTranslation.js'; + +const TELEGRAM_ASSISTANT_URL = 'https://t.me/Growheads_de_Bot'; // Initialize window object for storing messages if (!window.chatMessages) { window.chatMessages = []; @@ -451,8 +457,9 @@ class ChatAssistant extends Component { }); render() { - const { open, onClose } = this.props; + const { open, onClose, t } = this.props; const { messages, inputValue, isTyping, isRecording, recordingTime, isGuest, privacyConfirmed } = this.state; + const showTelegramHint = !messages.some((m) => m.sender === 'user'); if (!open) { return null; @@ -517,6 +524,58 @@ class ChatAssistant extends Component { gap: 2, }} > + {showTelegramHint && ( + alpha(theme.palette.primary.main, 0.14), + boxShadow: (theme) => + `0 4px 14px ${alpha(theme.palette.primary.main, 0.35)}`, + }} + > + + + `drop-shadow(0 1px 2px ${alpha(theme.palette.primary.dark, 0.45)})`, + }} + /> + + + {t('chat.telegramAssistantIntro')} + + + {t('chat.telegramAssistantLink')} + + + + + )} {messages &&messages.map((message) => (