feat: Enhance ChatAssistant component with dynamic privacy prompt and localization support; update various UI elements for improved accessibility and user experience

Fix product card width on mobile.
This commit is contained in:
sebseb7
2026-03-26 14:21:03 +01:00
parent bfeb5be1d5
commit e0c6d47d98
31 changed files with 485 additions and 91 deletions

View File

@@ -53,6 +53,11 @@ class ChatAssistant extends Component {
this.recordingTimer = null; this.recordingTimer = null;
} }
buildPrivacyPromptHtml = () => {
const { t } = this.props;
return `${t('chat.privacyPromptBefore')}<a href="/datenschutz" target="_blank" rel="noopener noreferrer">${t('chat.privacyPolicyLink')}</a>${t('chat.privacyPromptAfter')}<button data-confirm-privacy="true">${t('chat.privacyRead')}</button>`;
};
componentDidMount() { componentDidMount() {
// Add socket listeners if socket is available and connected // Add socket listeners if socket is available and connected
this.addSocketListeners(); this.addSocketListeners();
@@ -69,7 +74,7 @@ class ChatAssistant extends Component {
const privacyMessage = { const privacyMessage = {
id: 'privacy-prompt', id: 'privacy-prompt',
sender: 'bot', sender: 'bot',
text: 'Bitte bestätigen Sie, dass Sie die <a href="/datenschutz" target="_blank" rel="noopener noreferrer">Datenschutzbestimmungen</a> gelesen haben und damit einverstanden sind. <button data-confirm-privacy="true">Gelesen & Akzeptiert</button>', text: this.buildPrivacyPromptHtml(),
}; };
const updatedMessages = [privacyMessage, ...prevState.messages]; const updatedMessages = [privacyMessage, ...prevState.messages];
window.chatMessages = updatedMessages; window.chatMessages = updatedMessages;
@@ -84,6 +89,19 @@ class ChatAssistant extends Component {
} }
componentDidUpdate(prevProps, prevState) { componentDidUpdate(prevProps, prevState) {
if (prevProps.i18n?.language !== this.props.i18n?.language) {
this.setState((prev) => {
const idx = prev.messages.findIndex((m) => m.id === 'privacy-prompt');
if (idx === -1) return null;
const updatedMessages = [...prev.messages];
updatedMessages[idx] = {
...updatedMessages[idx],
text: this.buildPrivacyPromptHtml(),
};
window.chatMessages = updatedMessages;
return { messages: updatedMessages };
});
}
if (prevState.messages !== this.state.messages || prevState.isTyping !== this.state.isTyping) { if (prevState.messages !== this.state.messages || prevState.isTyping !== this.state.isTyping) {
this.scrollToBottom(); this.scrollToBottom();
} }
@@ -244,7 +262,7 @@ class ChatAssistant extends Component {
}); });
} catch (err) { } catch (err) {
console.error("Error accessing microphone:", err); console.error("Error accessing microphone:", err);
alert("Could not access microphone. Please check your browser permissions."); alert(this.props.t('chat.micPermissionDenied'));
} }
}; };
@@ -359,7 +377,7 @@ class ChatAssistant extends Component {
const newUserMessage = { const newUserMessage = {
id: Date.now(), id: Date.now(),
sender: 'user', sender: 'user',
text: `<img src="${imageUrl}" alt="Uploaded image" style="max-width: 100%; height: auto; border-radius: 8px;" />`, text: `<img src="${imageUrl}" alt="${this.props.t('chat.uploadedImageAlt')}" style="max-width: 100%; height: auto; border-radius: 8px;" />`,
isImage: true isImage: true
}; };
@@ -451,7 +469,7 @@ class ChatAssistant extends Component {
} }
if (domNode.name === 'button' && domNode.attribs && domNode.attribs['data-confirm-privacy']) { if (domNode.name === 'button' && domNode.attribs && domNode.attribs['data-confirm-privacy']) {
return <Button variant="contained" size="small" onClick={this.handlePrivacyConfirm}>Gelesen & Akzeptiert</Button>; return <Button variant="contained" size="small" onClick={this.handlePrivacyConfirm}>{this.props.t('chat.privacyRead')}</Button>;
} }
} }
}); });
@@ -505,12 +523,12 @@ class ChatAssistant extends Component {
}} }}
> >
<Typography variant="h6" component="div"> <Typography variant="h6" component="div">
Assistent {t('chat.assistantTitle')}
<Typography component="span" color={this.state.aiThink ? "error" : "text.disabled"} sx={{ display: 'inline' }}>🧠</Typography> <Typography component="span" color={this.state.aiThink ? "error" : "text.disabled"} sx={{ display: 'inline' }}>🧠</Typography>
<Typography component="span" color={this.state.atDatabase ? "error" : "text.disabled"} sx={{ display: 'inline' }}>🛢</Typography> <Typography component="span" color={this.state.atDatabase ? "error" : "text.disabled"} sx={{ display: 'inline' }}>🛢</Typography>
<Typography component="span" color={this.state.atWeb ? "error" : "text.disabled"} sx={{ display: 'inline' }}>🌐</Typography> <Typography component="span" color={this.state.atWeb ? "error" : "text.disabled"} sx={{ display: 'inline' }}>🌐</Typography>
</Typography> </Typography>
<IconButton onClick={onClose} size="small" aria-label="Assistent schließen" sx={{ color: 'primary.contrastText' }}> <IconButton onClick={onClose} size="small" aria-label={t('chat.closeAria')} sx={{ color: 'primary.contrastText' }}>
<CloseIcon /> <CloseIcon />
</IconButton> </IconButton>
</Box> </Box>
@@ -648,7 +666,7 @@ class ChatAssistant extends Component {
autoFocus autoFocus
autoCapitalize="off" autoCapitalize="off"
autoCorrect="off" autoCorrect="off"
placeholder={isRecording ? "Aufnahme läuft..." : "Du kannst mich nach Cannabissorten fragen..."} placeholder={isRecording ? t('chat.placeholderRecording') : t('chat.inputPlaceholder')}
value={inputValue} value={inputValue}
onChange={this.handleInputChange} onChange={this.handleInputChange}
onKeyDown={this.handleKeyDown} onKeyDown={this.handleKeyDown}
@@ -670,7 +688,7 @@ class ChatAssistant extends Component {
<IconButton <IconButton
color="error" color="error"
onClick={this.stopRecording} onClick={this.stopRecording}
aria-label="Aufnahme stoppen" aria-label={t('chat.micStopAria')}
sx={{ ml: { xs: 0, sm: 1 } }} sx={{ ml: { xs: 0, sm: 1 } }}
> >
<StopIcon /> <StopIcon />
@@ -679,7 +697,7 @@ class ChatAssistant extends Component {
<IconButton <IconButton
color="primary" color="primary"
onClick={this.startRecording} onClick={this.startRecording}
aria-label="Sprachaufnahme starten" aria-label={t('chat.micStartAria')}
sx={{ ml: { xs: 0, sm: 1 } }} sx={{ ml: { xs: 0, sm: 1 } }}
disabled={isTyping || inputsDisabled} disabled={isTyping || inputsDisabled}
> >
@@ -690,7 +708,7 @@ class ChatAssistant extends Component {
<IconButton <IconButton
color="primary" color="primary"
onClick={this.handleImageUpload} onClick={this.handleImageUpload}
aria-label="Bild hochladen" aria-label={t('chat.uploadImageAria')}
sx={{ ml: { xs: 0, sm: 1 } }} sx={{ ml: { xs: 0, sm: 1 } }}
disabled={isTyping || isRecording || inputsDisabled} disabled={isTyping || isRecording || inputsDisabled}
> >
@@ -703,7 +721,7 @@ class ChatAssistant extends Component {
onClick={this.handleSendMessage} onClick={this.handleSendMessage}
disabled={isTyping || isRecording || inputsDisabled} disabled={isTyping || isRecording || inputsDisabled}
> >
Senden {t('chat.send')}
</Button> </Button>
</Box> </Box>
</Box> </Box>

View File

@@ -11,6 +11,11 @@ import { Link, useNavigate } from 'react-router-dom';
import { withI18n } from '../i18n/withTranslation.js'; import { withI18n } from '../i18n/withTranslation.js';
import ZoomInIcon from '@mui/icons-material/ZoomIn'; import ZoomInIcon from '@mui/icons-material/ZoomIn';
import { STAR_POLYGON_POINTS } from '../utils/starPolygon.js'; import { STAR_POLYGON_POINTS } from '../utils/starPolygon.js';
import {
PRODUCT_CARD_MOBILE_MAX_WIDTH_PX,
PRODUCT_CARD_WIDTH_SM_PX,
PRODUCT_CARD_WIDTH_XS_PX,
} from '../utils/productCardLayout.js';
// Helper function to find level 1 category ID from any category ID // Helper function to find level 1 category ID from any category ID
const findLevel1CategoryId = (categoryId) => { const findLevel1CategoryId = (categoryId) => {
@@ -276,7 +281,16 @@ class Product extends Component {
<Box sx={{ <Box sx={{
position: 'relative', position: 'relative',
height: '100%', height: '100%',
width: { xs: '100%', sm: 'auto' } /* Match card width on xs so absolute NEU star is relative to the card, not the full grid row */
width: {
xs: `${PRODUCT_CARD_WIDTH_XS_PX}px`,
sm: 'auto',
},
minWidth: { xs: `${PRODUCT_CARD_WIDTH_XS_PX}px`, sm: 'auto' },
maxWidth: { xs: `${PRODUCT_CARD_WIDTH_XS_PX}px`, sm: 'none' },
display: 'flex',
justifyContent: { xs: 'center', sm: 'flex-start' },
mx: { xs: 'auto', sm: 0 },
}}> }}>
{isNew && ( {isNew && (
<div <div
@@ -362,22 +376,36 @@ class Product extends Component {
<Card <Card
sx={{ sx={{
width: { xs: '100vw', sm: '250px' }, width: {
minWidth: { xs: '100vw', sm: '250px' }, xs: `${PRODUCT_CARD_WIDTH_XS_PX}px`,
sm: `${PRODUCT_CARD_WIDTH_SM_PX}px`,
},
minWidth: {
xs: `${PRODUCT_CARD_WIDTH_XS_PX}px`,
sm: `${PRODUCT_CARD_WIDTH_SM_PX}px`,
},
maxWidth: {
xs: `${PRODUCT_CARD_WIDTH_XS_PX}px`,
sm: `${PRODUCT_CARD_WIDTH_SM_PX}px`,
},
height: '100%', height: '100%',
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
transition: 'transform 0.3s ease, box-shadow 0.3s ease', transition: 'transform 0.3s ease, box-shadow 0.3s ease',
position: 'relative', position: 'relative',
overflow: 'hidden', overflow: 'hidden',
borderRadius: { xs: 0, sm: '8px' }, borderRadius: { xs: '8px', sm: '8px' },
border: { xs: 'none', sm: 'inherit' }, border: { xs: '1px solid', sm: 'inherit' },
boxShadow: { xs: 'none', sm: 'inherit' }, borderColor: { xs: 'divider', sm: 'inherit' },
mx: { xs: 0, sm: 'auto' }, boxShadow: { xs: '0 1px 4px rgba(0,0,0,0.08)', sm: 'inherit' },
mx: { xs: 'auto', sm: 'auto' },
'&:hover': { '&:hover': {
transform: { xs: 'none', sm: 'translateY(-5px)' }, transform: { xs: 'none', sm: 'translateY(-5px)' },
boxShadow: { xs: 'none', sm: '0px 10px 20px rgba(0,0,0,0.1)' } boxShadow: {
} xs: '0 1px 4px rgba(0,0,0,0.08)',
sm: '0px 10px 20px rgba(0,0,0,0.1)',
},
},
}} }}
> >
{showThcBadge && ( {showThcBadge && (
@@ -460,7 +488,7 @@ class Product extends Component {
<CardMedia <CardMedia
key={index} key={index}
component="img" component="img"
height={window.innerWidth < 600 ? "240" : "180"} height={window.innerWidth < PRODUCT_CARD_MOBILE_MAX_WIDTH_PX ? "240" : "180"}
image={imgSrc} image={imgSrc}
alt={name} alt={name}
fetchPriority={this.props.priority === 'high' && index === 0 ? 'high' : 'auto'} fetchPriority={this.props.priority === 'high' && index === 0 ? 'high' : 'auto'}
@@ -489,7 +517,7 @@ class Product extends Component {
) : ( ) : (
<CardMedia <CardMedia
component="img" component="img"
height={window.innerWidth < 600 ? "240" : "180"} height={window.innerWidth < PRODUCT_CARD_MOBILE_MAX_WIDTH_PX ? "240" : "180"}
image="/assets/images/nopicture.jpg" image="/assets/images/nopicture.jpg"
alt={name} alt={name}
fetchPriority={this.props.priority === 'high' ? 'high' : 'auto'} fetchPriority={this.props.priority === 'high' ? 'high' : 'auto'}

View File

@@ -8,8 +8,11 @@ import ChevronRight from "@mui/icons-material/ChevronRight";
import Product from "./Product.js"; import Product from "./Product.js";
import { withTranslation } from 'react-i18next'; import { withTranslation } from 'react-i18next';
import { withLanguage } from '../i18n/withTranslation.js'; import { withLanguage } from '../i18n/withTranslation.js';
import {
const ITEM_WIDTH = 250 + 16; // 250px width + 16px gap getProductCarouselItemStridePx,
PRODUCT_CARD_WIDTH_SM_PX,
PRODUCT_CARD_WIDTH_XS_PX,
} from "../utils/productCardLayout.js";
const AUTO_SCROLL_SPEED = 0.5; // px per frame (~60fps, so ~30px/sec) const AUTO_SCROLL_SPEED = 0.5; // px per frame (~60fps, so ~30px/sec)
const AUTOSCROLL_RESTART_DELAY = 5000; // 5 seconds of inactivity before restarting autoscroll const AUTOSCROLL_RESTART_DELAY = 5000; // 5 seconds of inactivity before restarting autoscroll
const SCROLLBAR_FLASH_DURATION = 3000; // 3 seconds to show the virtual scrollbar const SCROLLBAR_FLASH_DURATION = 3000; // 3 seconds to show the virtual scrollbar
@@ -81,13 +84,31 @@ class ProductCarousel extends React.Component {
products: [], products: [],
currentLanguage: (i18n && i18n.language) || 'de', currentLanguage: (i18n && i18n.language) || 'de',
showScrollbar: false, showScrollbar: false,
itemStride:
typeof window !== "undefined"
? getProductCarouselItemStridePx()
: PRODUCT_CARD_WIDTH_SM_PX + 16,
}; };
this.carouselTrackRef = React.createRef(); this.carouselTrackRef = React.createRef();
} }
handleCarouselResize = () => {
if (!this._isMounted) return;
const next = getProductCarouselItemStridePx();
if (next !== this.state.itemStride) {
this.translateX = 0;
this.updateTrackTransform();
this.setState({ itemStride: next });
}
};
componentDidMount() { componentDidMount() {
this._isMounted = true; this._isMounted = true;
if (typeof window !== "undefined") {
window.addEventListener("resize", this.handleCarouselResize);
this.setState({ itemStride: getProductCarouselItemStridePx() });
}
const currentLanguage = this.props.languageContext?.currentLanguage || this.props.i18n.language; const currentLanguage = this.props.languageContext?.currentLanguage || this.props.i18n.language;
logCarousel("mount", { logCarousel("mount", {
@@ -370,6 +391,9 @@ class ProductCarousel extends React.Component {
componentWillUnmount() { componentWillUnmount() {
this._isMounted = false; this._isMounted = false;
if (typeof window !== "undefined") {
window.removeEventListener("resize", this.handleCarouselResize);
}
this.stopAutoScroll(); this.stopAutoScroll();
this.clearInactivityTimer(); this.clearInactivityTimer();
this.clearScrollbarTimer(); this.clearScrollbarTimer();
@@ -430,8 +454,9 @@ class ProductCarousel extends React.Component {
this.translateX -= AUTO_SCROLL_SPEED; this.translateX -= AUTO_SCROLL_SPEED;
this.updateTrackTransform(); this.updateTrackTransform();
const { itemStride } = this.state;
const originalItemCount = this.originalProducts.length; const originalItemCount = this.originalProducts.length;
const maxScroll = ITEM_WIDTH * originalItemCount; const maxScroll = itemStride * originalItemCount;
// Check if we've scrolled past the first set of items // Check if we've scrolled past the first set of items
if (Math.abs(this.translateX) >= maxScroll) { if (Math.abs(this.translateX) >= maxScroll) {
@@ -467,14 +492,15 @@ class ProductCarousel extends React.Component {
if (this.originalProducts.length === 0) return; if (this.originalProducts.length === 0) return;
// direction: 1 = left (scroll content right), -1 = right (scroll content left) // direction: 1 = left (scroll content right), -1 = right (scroll content left)
const { itemStride } = this.state;
const originalItemCount = this.originalProducts.length; const originalItemCount = this.originalProducts.length;
const maxScroll = ITEM_WIDTH * originalItemCount; const maxScroll = itemStride * originalItemCount;
this.translateX += direction * ITEM_WIDTH; this.translateX += direction * itemStride;
// Handle wrap-around when scrolling left (positive translateX) // Handle wrap-around when scrolling left (positive translateX)
if (this.translateX > 0) { if (this.translateX > 0) {
this.translateX = -(maxScroll - ITEM_WIDTH); this.translateX = -(maxScroll - itemStride);
} }
// Handle wrap-around when scrolling right (negative translateX beyond limit) // Handle wrap-around when scrolling right (negative translateX beyond limit)
else if (Math.abs(this.translateX) >= maxScroll) { else if (Math.abs(this.translateX) >= maxScroll) {
@@ -494,9 +520,13 @@ class ProductCarousel extends React.Component {
return null; return null;
} }
const { itemStride } = this.state;
const originalItemCount = this.originalProducts.length; const originalItemCount = this.originalProducts.length;
const viewportWidth = 1080; // carousel container max-width const viewportWidth =
const itemsInView = Math.floor(viewportWidth / ITEM_WIDTH); typeof window !== "undefined"
? Math.min(1080, Math.max(0, window.innerWidth - 56))
: 1080;
const itemsInView = Math.max(1, Math.floor(viewportWidth / itemStride));
// Calculate which item is currently at the left edge (first visible) // Calculate which item is currently at the left edge (first visible)
let currentItemIndex; let currentItemIndex;
@@ -504,11 +534,11 @@ class ProductCarousel extends React.Component {
if (this.translateX === 0) { if (this.translateX === 0) {
currentItemIndex = 0; currentItemIndex = 0;
} else if (this.translateX > 0) { } else if (this.translateX > 0) {
const maxScroll = ITEM_WIDTH * originalItemCount; const maxScroll = itemStride * originalItemCount;
const effectivePosition = maxScroll + this.translateX; const effectivePosition = maxScroll + this.translateX;
currentItemIndex = Math.floor(effectivePosition / ITEM_WIDTH); currentItemIndex = Math.floor(effectivePosition / itemStride);
} else { } else {
currentItemIndex = Math.floor(Math.abs(this.translateX) / ITEM_WIDTH); currentItemIndex = Math.floor(Math.abs(this.translateX) / itemStride);
} }
// Ensure we stay within bounds // Ensure we stay within bounds
@@ -615,7 +645,7 @@ class ProductCarousel extends React.Component {
top: '50%', top: '50%',
left: '8px', left: '8px',
transform: 'translateY(-50%)', transform: 'translateY(-50%)',
zIndex: 1200, zIndex: 1000,
backgroundColor: 'rgba(255, 255, 255, 0.9)', backgroundColor: 'rgba(255, 255, 255, 0.9)',
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.15)', boxShadow: '0 2px 8px rgba(0, 0, 0, 0.15)',
width: '48px', width: '48px',
@@ -635,7 +665,7 @@ class ProductCarousel extends React.Component {
top: '50%', top: '50%',
right: '8px', right: '8px',
transform: 'translateY(-50%)', transform: 'translateY(-50%)',
zIndex: 1200, zIndex: 1000,
backgroundColor: 'rgba(255, 255, 255, 0.9)', backgroundColor: 'rgba(255, 255, 255, 0.9)',
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.15)', boxShadow: '0 2px 8px rgba(0, 0, 0, 0.15)',
width: '48px', width: '48px',
@@ -676,16 +706,19 @@ class ProductCarousel extends React.Component {
}} }}
> >
{products.map((product, index) => ( {products.map((product, index) => (
<div <Box
key={`${product.id}-${index}`} key={`${product.id}-${index}`}
className="product-carousel-item" className="product-carousel-item"
style={{ sx={{
flex: '0 0 250px', flex: {
width: '250px', xs: `0 0 ${PRODUCT_CARD_WIDTH_XS_PX}px`,
maxWidth: '250px', sm: `0 0 ${PRODUCT_CARD_WIDTH_SM_PX}px`,
minWidth: '250px', },
boxSizing: 'border-box', width: { xs: PRODUCT_CARD_WIDTH_XS_PX, sm: PRODUCT_CARD_WIDTH_SM_PX },
position: 'relative' maxWidth: { xs: PRODUCT_CARD_WIDTH_XS_PX, sm: PRODUCT_CARD_WIDTH_SM_PX },
minWidth: { xs: PRODUCT_CARD_WIDTH_XS_PX, sm: PRODUCT_CARD_WIDTH_SM_PX },
boxSizing: "border-box",
position: "relative",
}} }}
> >
<Product <Product
@@ -713,7 +746,7 @@ class ProductCarousel extends React.Component {
priority={index < 6 ? 'high' : 'auto'} priority={index < 6 ? 'high' : 'auto'}
t={t} t={t}
/> />
</div> </Box>
))} ))}
</div> </div>

View File

@@ -438,7 +438,11 @@ class ProductList extends Component {
</Stack> </Stack>
</Box> </Box>
<Grid container spacing={{ xs: 0, sm: 2 }}> <Grid
container
spacing={{ xs: 0, sm: 2 }}
sx={{ bgcolor: { xs: '#e8f5e8', sm: 'transparent' } }}
>
{this.renderNoProductsMessage()} {this.renderNoProductsMessage()}
{products.map((product, index) => ( {products.map((product, index) => (
<Grid <Grid
@@ -448,6 +452,7 @@ class ProductList extends Component {
justifyContent: { xs: 'stretch', sm: 'center' }, justifyContent: { xs: 'stretch', sm: 'center' },
mb: { xs: 0, sm: 1 }, mb: { xs: 0, sm: 1 },
width: { xs: '100%', sm: 'auto' }, width: { xs: '100%', sm: 'auto' },
bgcolor: { xs: '#e8f5e8', sm: 'transparent' },
borderBottom: { borderBottom: {
xs: index < products.length - 1 ? '16px solid #e8f5e8' : 'none', xs: index < products.length - 1 ? '16px solid #e8f5e8' : 'none',
sm: 'none' sm: 'none'

View File

@@ -327,7 +327,7 @@ class SharedCarousel extends React.Component {
top: '50%', top: '50%',
left: '8px', left: '8px',
transform: 'translateY(-50%)', transform: 'translateY(-50%)',
zIndex: 1200, zIndex: 1000,
backgroundColor: 'rgba(255, 255, 255, 0.9)', backgroundColor: 'rgba(255, 255, 255, 0.9)',
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.15)', boxShadow: '0 2px 8px rgba(0, 0, 0, 0.15)',
width: '48px', width: '48px',
@@ -347,7 +347,7 @@ class SharedCarousel extends React.Component {
top: '50%', top: '50%',
right: '8px', right: '8px',
transform: 'translateY(-50%)', transform: 'translateY(-50%)',
zIndex: 1200, zIndex: 1000,
backgroundColor: 'rgba(255, 255, 255, 0.9)', backgroundColor: 'rgba(255, 255, 255, 0.9)',
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.15)', boxShadow: '0 2px 8px rgba(0, 0, 0, 0.15)',
width: '48px', width: '48px',

View File

@@ -519,7 +519,7 @@ class CategoryList extends Component {
to="/Konfigurator" to="/Konfigurator"
color="inherit" color="inherit"
size="small" size="small"
aria-label="Zur Startseite" aria-label={this.props.t ? this.props.t('navigation.konfiguratorAria') : 'Zum Konfigurator'}
onClick={isMobile ? this.handleMobileCategoryClick : undefined} onClick={isMobile ? this.handleMobileCategoryClick : undefined}
sx={{ sx={{
fontSize: "0.75rem", fontSize: "0.75rem",
@@ -572,7 +572,7 @@ class CategoryList extends Component {
zIndex: 2, zIndex: 2,
}} }}
> >
{this.props.t ? this.props.t('navigation.home') : 'Startseite'} {this.props.t ? this.props.t('sections.konfigurator') : 'Konfigurator'}
</Box> </Box>
{/* Thin text (positioned on top) */} {/* Thin text (positioned on top) */}
<Box <Box
@@ -586,7 +586,7 @@ class CategoryList extends Component {
zIndex: 1, zIndex: 1,
}} }}
> >
{this.props.t ? this.props.t('navigation.home') : 'Startseite'} {this.props.t ? this.props.t('sections.konfigurator') : 'Konfigurator'}
</Box> </Box>
</Box> </Box>
)} )}

View File

@@ -1,5 +1,18 @@
export default { export default {
"privacyRead": "مقروء ومقبول", "privacyRead": "قريت ووافقت",
"telegramAssistantIntro": "تقدر كمان توصل لمساعد Growheads على تيليجرام:", "privacyPromptBefore": "من فضلك أكد إنك قرأت ",
"telegramAssistantLink": "t.me/Growheads_de_Bot" "privacyPolicyLink": "سياسة الخصوصية",
"privacyPromptAfter": " ووافقت عليها. ",
"telegramAssistantIntro": "كمان تقدر تتواصل مع مساعد Growheads على Telegram:",
"telegramAssistantLink": "t.me/Growheads_de_Bot",
"assistantTitle": "المساعد",
"placeholderRecording": "جارٍ التسجيل…",
"inputPlaceholder": "تقدر تسألني عن سلالات القنب…",
"send": "إرسال",
"closeAria": "إغلاق المساعد",
"micStartAria": "ابدأ تسجيل الصوت",
"micStopAria": "إيقاف التسجيل",
"uploadImageAria": "رفع صورة",
"micPermissionDenied": "تعذر الوصول إلى الميكروفون. من فضلك راجع أذونات المتصفح.",
"uploadedImageAlt": "صورة مرفوعة"
}; };

View File

@@ -1,5 +1,18 @@
export default { export default {
"privacyRead": "Прочетено и прието", "privacyRead": "Прочетено и прието",
"privacyPromptBefore": "Моля, потвърдете, че сте прочели ",
"privacyPolicyLink": "политиката за поверителност",
"privacyPromptAfter": " и сте съгласни с нея. ",
"telegramAssistantIntro": "Можете също да се свържете с асистента на Growheads в Telegram:", "telegramAssistantIntro": "Можете също да се свържете с асистента на Growheads в Telegram:",
"telegramAssistantLink": "t.me/Growheads_de_Bot" "telegramAssistantLink": "t.me/Growheads_de_Bot",
"assistantTitle": "Асистент",
"placeholderRecording": "Записване…",
"inputPlaceholder": "Можете да ме питате за сортове канабис…",
"send": "Изпрати",
"closeAria": "Затвори асистента",
"micStartAria": "Стартиране на гласов запис",
"micStopAria": "Спиране на записа",
"uploadImageAria": "Качване на изображение",
"micPermissionDenied": "Не беше възможен достъп до микрофона. Моля, проверете разрешенията на браузъра си.",
"uploadedImageAlt": "Качено изображение"
}; };

View File

@@ -1,5 +1,18 @@
export default { export default {
"privacyRead": "Přečteno a přijato", "privacyRead": "Přečteno a přijato",
"privacyPromptBefore": "Prosím potvrďte, že jste si přečetli ",
"privacyPolicyLink": "zásady ochrany osobních údajů",
"privacyPromptAfter": " a souhlasíte s nimi. ",
"telegramAssistantIntro": "Asistenta Growheads můžete také kontaktovat na Telegramu:", "telegramAssistantIntro": "Asistenta Growheads můžete také kontaktovat na Telegramu:",
"telegramAssistantLink": "t.me/Growheads_de_Bot" "telegramAssistantLink": "t.me/Growheads_de_Bot",
"assistantTitle": "Asistent",
"placeholderRecording": "Nahrávání…",
"inputPlaceholder": "Můžete se mě zeptat na odrůdy konopí…",
"send": "Odeslat",
"closeAria": "Zavřít asistenta",
"micStartAria": "Spustit nahrávání hlasu",
"micStopAria": "Zastavit nahrávání",
"uploadImageAria": "Nahrát obrázek",
"micPermissionDenied": "Nepodařilo se získat přístup k mikrofonu. Zkontrolujte prosím oprávnění ve svém prohlížeči.",
"uploadedImageAlt": "Nahraný obrázek"
}; };

View File

@@ -1,5 +1,18 @@
export default { export default {
"privacyRead": "Gelesen & Akzeptiert", "privacyRead": "Gelesen & Akzeptiert",
"privacyPromptBefore": "Bitte bestätigen Sie, dass Sie die ",
"privacyPolicyLink": "Datenschutzbestimmungen",
"privacyPromptAfter": " gelesen haben und damit einverstanden sind. ",
"telegramAssistantIntro": "Du kannst den Growheads Assistenten auch per Telegram erreichen:", "telegramAssistantIntro": "Du kannst den Growheads Assistenten auch per Telegram erreichen:",
"telegramAssistantLink": "t.me/Growheads_de_Bot" "telegramAssistantLink": "t.me/Growheads_de_Bot",
"assistantTitle": "Assistent",
"placeholderRecording": "Aufnahme läuft...",
"inputPlaceholder": "Du kannst mich nach Cannabissorten fragen...",
"send": "Senden",
"closeAria": "Assistent schließen",
"micStartAria": "Sprachaufnahme starten",
"micStopAria": "Aufnahme stoppen",
"uploadImageAria": "Bild hochladen",
"micPermissionDenied": "Mikrofon-Zugriff nicht möglich. Bitte prüfen Sie die Browser-Berechtigungen.",
"uploadedImageAlt": "Hochgeladenes Bild"
}; };

View File

@@ -1,5 +1,6 @@
export default { export default {
"home": "Startseite", "home": "Startseite",
"konfiguratorAria": "Zum Konfigurator",
"new": "Neuheiten", "new": "Neuheiten",
"soon": "Demnächst", "soon": "Demnächst",
"aktionen": "Aktionen", "aktionen": "Aktionen",

View File

@@ -1,5 +1,18 @@
export default { export default {
"privacyRead": "Διαβάστηκε & Έγινε αποδεκτό", "privacyRead": "Διαβάστηκε & Εγκρίθηκε",
"telegramAssistantIntro": "Μπορείτε επίσης να επικοινωνήσετε με τον βοηθό Growheads στο Telegram:", "privacyPromptBefore": "Παρακαλώ επιβεβαιώστε ότι έχετε διαβάσει την ",
"telegramAssistantLink": "t.me/Growheads_de_Bot" "privacyPolicyLink": "πολιτική απορρήτου",
"privacyPromptAfter": " και ότι συμφωνείτε με αυτήν. ",
"telegramAssistantIntro": "Μπορείτε επίσης να επικοινωνήσετε με τον βοηθό του Growheads στο Telegram:",
"telegramAssistantLink": "t.me/Growheads_de_Bot",
"assistantTitle": "Βοηθός",
"placeholderRecording": "Γίνεται εγγραφή…",
"inputPlaceholder": "Μπορείτε να με ρωτήσετε για ποικιλίες cannabis…",
"send": "Αποστολή",
"closeAria": "Κλείσιμο βοηθού",
"micStartAria": "Έναρξη ηχογράφησης φωνής",
"micStopAria": "Διακοπή εγγραφής",
"uploadImageAria": "Μεταφόρτωση εικόνας",
"micPermissionDenied": "Δεν ήταν δυνατή η πρόσβαση στο μικρόφωνο. Παρακαλώ ελέγξτε τα δικαιώματα του browser σας.",
"uploadedImageAlt": "Ανεβασμένη εικόνα"
}; };

View File

@@ -1,5 +1,18 @@
export default { export default {
"privacyRead": "Read & Accepted", // Gelesen & Akzeptiert "privacyRead": "Read & Accepted",
"privacyPromptBefore": "Please confirm that you have read the ",
"privacyPolicyLink": "privacy policy",
"privacyPromptAfter": " and agree to it. ",
"telegramAssistantIntro": "You can also reach the Growheads assistant on Telegram:", "telegramAssistantIntro": "You can also reach the Growheads assistant on Telegram:",
"telegramAssistantLink": "t.me/Growheads_de_Bot" "telegramAssistantLink": "t.me/Growheads_de_Bot",
"assistantTitle": "Assistant",
"placeholderRecording": "Recording…",
"inputPlaceholder": "You can ask me about cannabis strains…",
"send": "Send",
"closeAria": "Close assistant",
"micStartAria": "Start voice recording",
"micStopAria": "Stop recording",
"uploadImageAria": "Upload image",
"micPermissionDenied": "Could not access the microphone. Please check your browser permissions.",
"uploadedImageAlt": "Uploaded image"
}; };

View File

@@ -1,5 +1,6 @@
export default { export default {
"home": "Home", // Startseite "home": "Home", // Startseite
"konfiguratorAria": "Go to Configurator", // Zum Konfigurator
"new": "New Arrivals", // Neuheiten "new": "New Arrivals", // Neuheiten
"soon": "Coming Soon", // Demnächst "soon": "Coming Soon", // Demnächst
"aktionen": "Promotions", // Aktionen "aktionen": "Promotions", // Aktionen

View File

@@ -1,5 +1,18 @@
export default { export default {
"privacyRead": "Leído y aceptado", "privacyRead": "Leído y aceptado",
"telegramAssistantIntro": "También puedes contactar al asistente de Growheads en Telegram:", "privacyPromptBefore": "Por favor, confirma que has leído la ",
"telegramAssistantLink": "t.me/Growheads_de_Bot" "privacyPolicyLink": "política de privacidad",
"privacyPromptAfter": " y que estás de acuerdo con ella. ",
"telegramAssistantIntro": "También puedes contactar con el asistente de Growheads en Telegram:",
"telegramAssistantLink": "t.me/Growheads_de_Bot",
"assistantTitle": "Asistente",
"placeholderRecording": "Grabando…",
"inputPlaceholder": "Puedes preguntarme sobre cepas de cannabis…",
"send": "Enviar",
"closeAria": "Cerrar asistente",
"micStartAria": "Iniciar grabación de voz",
"micStopAria": "Detener grabación",
"uploadImageAria": "Subir imagen",
"micPermissionDenied": "No se pudo acceder al micrófono. Por favor, revisa los permisos de tu navegador.",
"uploadedImageAlt": "Imagen subida"
}; };

View File

@@ -1,5 +1,18 @@
export default { export default {
"privacyRead": "Lu et accepté", "privacyRead": "Lu et accepté",
"telegramAssistantIntro": "Vous pouvez également joindre l'assistant Growheads sur Telegram :", "privacyPromptBefore": "Veuillez confirmer que vous avez lu la ",
"telegramAssistantLink": "t.me/Growheads_de_Bot" "privacyPolicyLink": "politique de confidentialité",
"privacyPromptAfter": " et que vous l'acceptez. ",
"telegramAssistantIntro": "Vous pouvez également contacter l'assistant Growheads sur Telegram :",
"telegramAssistantLink": "t.me/Growheads_de_Bot",
"assistantTitle": "Assistant",
"placeholderRecording": "Enregistrement…",
"inputPlaceholder": "Vous pouvez me demander des informations sur les variétés de cannabis…",
"send": "Envoyer",
"closeAria": "Fermer l'assistant",
"micStartAria": "Démarrer l'enregistrement vocal",
"micStopAria": "Arrêter l'enregistrement",
"uploadImageAria": "Télécharger une image",
"micPermissionDenied": "Impossible d'accéder au microphone. Veuillez vérifier les autorisations de votre navigateur.",
"uploadedImageAlt": "Image téléchargée"
}; };

View File

@@ -1,5 +1,18 @@
export default { export default {
"privacyRead": "Pročitano i prihvaćeno", "privacyRead": "Pročitano i prihvaćeno",
"privacyPromptBefore": "Molimo potvrdite da ste pročitali ",
"privacyPolicyLink": "pravila privatnosti",
"privacyPromptAfter": " i da ih prihvaćate. ",
"telegramAssistantIntro": "Također možete kontaktirati Growheads asistenta na Telegramu:", "telegramAssistantIntro": "Također možete kontaktirati Growheads asistenta na Telegramu:",
"telegramAssistantLink": "t.me/Growheads_de_Bot" "telegramAssistantLink": "t.me/Growheads_de_Bot",
"assistantTitle": "Asistent",
"placeholderRecording": "Snimanje…",
"inputPlaceholder": "Možete me pitati o sortama kanabisa…",
"send": "Pošalji",
"closeAria": "Zatvori asistenta",
"micStartAria": "Pokreni glasovno snimanje",
"micStopAria": "Zaustavi snimanje",
"uploadImageAria": "Prenesi sliku",
"micPermissionDenied": "Nije moguće pristupiti mikrofonu. Molimo provjerite dopuštenja u pregledniku.",
"uploadedImageAlt": "Prenesena slika"
}; };

View File

@@ -1,5 +1,18 @@
export default { export default {
"privacyRead": "Elolvasva és elfogadva", "privacyRead": "Elolvastam és elfogadtam",
"telegramAssistantIntro": "A Growheads asszisztenst Telegramon is elérheted:", "privacyPromptBefore": "Kérjük, erősítse meg, hogy elolvasta a ",
"telegramAssistantLink": "t.me/Growheads_de_Bot" "privacyPolicyLink": "adatvédelmi szabályzatot",
"privacyPromptAfter": " és elfogadja azt. ",
"telegramAssistantIntro": "A Growheads asszisztenst Telegramon is elérheti:",
"telegramAssistantLink": "t.me/Growheads_de_Bot",
"assistantTitle": "Asszisztens",
"placeholderRecording": "Rögzítés…",
"inputPlaceholder": "Kérdezhet tőlem kannabiszfajtákról…",
"send": "Küldés",
"closeAria": "Asszisztens bezárása",
"micStartAria": "Hangrögzítés indítása",
"micStopAria": "Rögzítés leállítása",
"uploadImageAria": "Kép feltöltése",
"micPermissionDenied": "Nem sikerült hozzáférni a mikrofonhoz. Kérjük, ellenőrizze a böngésző engedélyeit.",
"uploadedImageAlt": "Feltöltött kép"
}; };

View File

@@ -1,5 +1,18 @@
export default { export default {
"privacyRead": "Letto e accettato", "privacyRead": "Letto e accettato",
"telegramAssistantIntro": "Puoi anche contattare l'assistente Growheads su Telegram:", "privacyPromptBefore": "Conferma di aver letto la ",
"telegramAssistantLink": "t.me/Growheads_de_Bot" "privacyPolicyLink": "informativa sulla privacy",
"privacyPromptAfter": " e di accettarla. ",
"telegramAssistantIntro": "Puoi أيضًا contattare l'assistente di Growheads su Telegram:",
"telegramAssistantLink": "t.me/Growheads_de_Bot",
"assistantTitle": "Assistente",
"placeholderRecording": "Registrazione…",
"inputPlaceholder": "Puoi chiedermi informazioni sulle varietà di cannabis…",
"send": "Invia",
"closeAria": "Chiudi assistente",
"micStartAria": "Avvia registrazione vocale",
"micStopAria": "Interrompi registrazione",
"uploadImageAria": "Carica immagine",
"micPermissionDenied": "Impossibile accedere al microfono. Controlla i permessi del browser.",
"uploadedImageAlt": "Immagine caricata"
}; };

View File

@@ -1,5 +1,18 @@
export default { export default {
"privacyRead": "Przeczytano i zaakceptowano", "privacyRead": "Przeczytano i zaakceptowano",
"privacyPromptBefore": "Proszę potwierdzić, że przeczytałeś(aś) ",
"privacyPolicyLink": "politykę prywatności",
"privacyPromptAfter": " i zgadzasz się na nią. ",
"telegramAssistantIntro": "Możesz również skontaktować się z asystentem Growheads na Telegramie:", "telegramAssistantIntro": "Możesz również skontaktować się z asystentem Growheads na Telegramie:",
"telegramAssistantLink": "t.me/Growheads_de_Bot" "telegramAssistantLink": "t.me/Growheads_de_Bot",
"assistantTitle": "Asystent",
"placeholderRecording": "Nagrywanie…",
"inputPlaceholder": "Możesz zapytać mnie o odmiany konopi…",
"send": "Wyślij",
"closeAria": "Zamknij asystenta",
"micStartAria": "Rozpocznij nagrywanie głosu",
"micStopAria": "Zatrzymaj nagrywanie",
"uploadImageAria": "Prześlij obraz",
"micPermissionDenied": "Nie udało się uzyskać dostępu do mikrofonu. Sprawdź uprawnienia w przeglądarce.",
"uploadedImageAlt": "Przesłany obraz"
}; };

View File

@@ -1,5 +1,18 @@
export default { export default {
"privacyRead": "Citit și acceptat", "privacyRead": "Citit și acceptat",
"telegramAssistantIntro": "Puteți, de asemenea, contacta asistentul Growheads pe Telegram:", "privacyPromptBefore": "Te rugăm să confirmi că ai citit ",
"telegramAssistantLink": "t.me/Growheads_de_Bot" "privacyPolicyLink": "politica de confidențialitate",
"privacyPromptAfter": " și ești de acord cu ea. ",
"telegramAssistantIntro": "Poți contacta și asistentul Growheads pe Telegram:",
"telegramAssistantLink": "t.me/Growheads_de_Bot",
"assistantTitle": "Asistent",
"placeholderRecording": "Înregistrare…",
"inputPlaceholder": "Mă poți întreba despre soiuri de cannabis…",
"send": "Trimite",
"closeAria": "Închide asistentul",
"micStartAria": "Începe înregistrarea vocală",
"micStopAria": "Oprește înregistrarea",
"uploadImageAria": "Încarcă imaginea",
"micPermissionDenied": "Nu s-a putut accesa microfonul. Te rugăm să verifici permisiunile din browser.",
"uploadedImageAlt": "Imagine încărcată"
}; };

View File

@@ -1,5 +1,18 @@
export default { export default {
"privacyRead": "Прочитано и принято", "privacyRead": "Прочитано и принято",
"privacyPromptBefore": "Пожалуйста, подтвердите, что вы прочитали ",
"privacyPolicyLink": "политику конфиденциальности",
"privacyPromptAfter": " и согласны с ней. ",
"telegramAssistantIntro": "Вы также можете связаться с помощником Growheads в Telegram:", "telegramAssistantIntro": "Вы также можете связаться с помощником Growheads в Telegram:",
"telegramAssistantLink": "t.me/Growheads_de_Bot" "telegramAssistantLink": "t.me/Growheads_de_Bot",
"assistantTitle": "Помощник",
"placeholderRecording": "Запись…",
"inputPlaceholder": "Вы можете спросить меня о сортах каннабиса…",
"send": "Отправить",
"closeAria": "Закрыть помощника",
"micStartAria": "Начать запись голоса",
"micStopAria": "Остановить запись",
"uploadImageAria": "Загрузить изображение",
"micPermissionDenied": "Не удалось получить доступ к микрофону. Пожалуйста, проверьте разрешения вашего браузера.",
"uploadedImageAlt": "Загруженное изображение"
}; };

View File

@@ -1,5 +1,18 @@
export default { export default {
"privacyRead": "Prečítané a akceptované", "privacyRead": "Prečítané a prijaté",
"telegramAssistantIntro": "Môžete tiež kontaktovať asistenta Growheads na Telegrame:", "privacyPromptBefore": "Prosím potvrďte, že ste si prečítali ",
"telegramAssistantLink": "t.me/Growheads_de_Bot" "privacyPolicyLink": "zásady ochrany osobných údajov",
"privacyPromptAfter": " a súhlasíte s nimi. ",
"telegramAssistantIntro": "Asistenta Growheads môžete tiež kontaktovať na Telegrame:",
"telegramAssistantLink": "t.me/Growheads_de_Bot",
"assistantTitle": "Asistent",
"placeholderRecording": "Nahráva sa…",
"inputPlaceholder": "Môžete sa ma opýtať na odrody kanabisu…",
"send": "Odoslať",
"closeAria": "Zavrieť asistenta",
"micStartAria": "Spustiť hlasové nahrávanie",
"micStopAria": "Zastaviť nahrávanie",
"uploadImageAria": "Nahrať obrázok",
"micPermissionDenied": "Nepodarilo sa získať prístup k mikrofónu. Skontrolujte, prosím, povolenia vo vašom prehliadači.",
"uploadedImageAlt": "Nahraný obrázok"
}; };

View File

@@ -1,5 +1,18 @@
export default { export default {
"privacyRead": "Prebrano in sprejeto", "privacyRead": "Prebrano in sprejeto",
"privacyPromptBefore": "Prosimo, potrdite, da ste prebrali ",
"privacyPolicyLink": "politiko zasebnosti",
"privacyPromptAfter": " in se z njo strinjate. ",
"telegramAssistantIntro": "Pomočnika Growheads lahko dosežete tudi na Telegramu:", "telegramAssistantIntro": "Pomočnika Growheads lahko dosežete tudi na Telegramu:",
"telegramAssistantLink": "t.me/Growheads_de_Bot" "telegramAssistantLink": "t.me/Growheads_de_Bot",
"assistantTitle": "Pomočnik",
"placeholderRecording": "Snemanje…",
"inputPlaceholder": "Lahko me vprašate o sortah konoplje…",
"send": "Pošlji",
"closeAria": "Zapri pomočnika",
"micStartAria": "Začni glasovno snemanje",
"micStopAria": "Ustavi snemanje",
"uploadImageAria": "Naloži sliko",
"micPermissionDenied": "Dostop do mikrofona ni bil mogoč. Preverite dovoljenja v brskalniku.",
"uploadedImageAlt": "Naložena slika"
}; };

View File

@@ -1,5 +1,18 @@
export default { export default {
"privacyRead": "Lexuar & Pranuar", "privacyRead": "Lexuar dhe pranuar",
"telegramAssistantIntro": "Mund ta kontaktoni gjithashtu asistentin e Growheads në Telegram:", "privacyPromptBefore": "Ju lutemi konfirmoni që e keni lexuar ",
"telegramAssistantLink": "t.me/Growheads_de_Bot" "privacyPolicyLink": "politikën e privatësisë",
"privacyPromptAfter": " dhe pajtoheni me të. ",
"telegramAssistantIntro": "Mund të kontaktoni gjithashtu asistentin Growheads në Telegram:",
"telegramAssistantLink": "t.me/Growheads_de_Bot",
"assistantTitle": "Asistent",
"placeholderRecording": "Duke regjistruar…",
"inputPlaceholder": "Mund të më pyesni për varietete kanabisi…",
"send": "Dërgo",
"closeAria": "Mbyll asistentin",
"micStartAria": "Fillo regjistrimin e zërit",
"micStopAria": "Ndalo regjistrimin",
"uploadImageAria": "Ngarko imazh",
"micPermissionDenied": "Nuk mund të aksesohej mikrofoni. Ju lutemi kontrolloni lejet e shfletuesit tuaj.",
"uploadedImageAlt": "Imazh i ngarkuar"
}; };

View File

@@ -1,5 +1,18 @@
export default { export default {
"privacyRead": "Pročitano i prihvaćeno", "privacyRead": "Pročitano i prihvaćeno",
"privacyPromptBefore": "Molimo potvrdite da ste pročitali ",
"privacyPolicyLink": "politiku privatnosti",
"privacyPromptAfter": " i da se slažete sa njom. ",
"telegramAssistantIntro": "Takođe možete kontaktirati Growheads asistenta na Telegramu:", "telegramAssistantIntro": "Takođe možete kontaktirati Growheads asistenta na Telegramu:",
"telegramAssistantLink": "t.me/Growheads_de_Bot" "telegramAssistantLink": "t.me/Growheads_de_Bot",
"assistantTitle": "Asistent",
"placeholderRecording": "Snimanje…",
"inputPlaceholder": "Možete da me pitate o sortama kanabisa…",
"send": "Pošalji",
"closeAria": "Zatvori asistenta",
"micStartAria": "Započni glasovno snimanje",
"micStopAria": "Zaustavi snimanje",
"uploadImageAria": "Otpremi sliku",
"micPermissionDenied": "Nije moguće pristupiti mikrofonu. Proverite dozvole u pregledaču.",
"uploadedImageAlt": "Otpremana slika"
}; };

View File

@@ -1,5 +1,18 @@
export default { export default {
"privacyRead": "Läst & accepterat", "privacyRead": "Läst & godkänt",
"privacyPromptBefore": "Bekräfta att du har läst ",
"privacyPolicyLink": "integritetspolicyn",
"privacyPromptAfter": " och godkänner den. ",
"telegramAssistantIntro": "Du kan också nå Growheads-assistenten på Telegram:", "telegramAssistantIntro": "Du kan också nå Growheads-assistenten på Telegram:",
"telegramAssistantLink": "t.me/Growheads_de_Bot" "telegramAssistantLink": "t.me/Growheads_de_Bot",
"assistantTitle": "Assistent",
"placeholderRecording": "Spelar in…",
"inputPlaceholder": "Du kan fråga mig om cannabissorter…",
"send": "Skicka",
"closeAria": "Stäng assistenten",
"micStartAria": "Starta röstinspelning",
"micStopAria": "Stoppa inspelning",
"uploadImageAria": "Ladda upp bild",
"micPermissionDenied": "Kunde inte komma åt mikrofonen. Kontrollera webbläsarens behörigheter.",
"uploadedImageAlt": "Uppladdad bild"
}; };

View File

@@ -1,5 +1,18 @@
export default { export default {
"privacyRead": "Okundu ve Kabul Edildi", "privacyRead": "Okundu ve Kabul Edildi",
"privacyPromptBefore": "Lütfen ",
"privacyPolicyLink": "gizlilik politikasını",
"privacyPromptAfter": " okuduğunuzu ve kabul ettiğinizi onaylayın. ",
"telegramAssistantIntro": "Growheads asistanına Telegram üzerinden de ulaşabilirsiniz:", "telegramAssistantIntro": "Growheads asistanına Telegram üzerinden de ulaşabilirsiniz:",
"telegramAssistantLink": "t.me/Growheads_de_Bot" "telegramAssistantLink": "t.me/Growheads_de_Bot",
"assistantTitle": "Asistan",
"placeholderRecording": "Kaydediliyor…",
"inputPlaceholder": "Bana cannabis strains hakkında sorabilirsiniz…",
"send": "Gönder",
"closeAria": "Asistanı kapat",
"micStartAria": "Ses kaydını başlat",
"micStopAria": "Kaydı durdur",
"uploadImageAria": "Resim yükle",
"micPermissionDenied": "Mikrofona erişilemedi. Lütfen tarayıcı izinlerinizi kontrol edin.",
"uploadedImageAlt": "Yüklenen resim"
}; };

View File

@@ -1,5 +1,18 @@
export default { export default {
"privacyRead": "Прочитано та прийнято", "privacyRead": "Прочитано та прийнято",
"telegramAssistantIntro": "Ви також можете зв’язатися з асистентом Growheads у Telegram:", "privacyPromptBefore": "Будь ласка, підтвердьте, що ви прочитали ",
"telegramAssistantLink": "t.me/Growheads_de_Bot" "privacyPolicyLink": "політику конфіденційності",
"privacyPromptAfter": " і погоджуєтеся з нею. ",
"telegramAssistantIntro": "Ви також можете звернутися до асистента Growheads у Telegram:",
"telegramAssistantLink": "t.me/Growheads_de_Bot",
"assistantTitle": "Асистент",
"placeholderRecording": "Запис…",
"inputPlaceholder": "Ви можете запитати мене про сорти канабісу…",
"send": "Надіслати",
"closeAria": "Закрити асистента",
"micStartAria": "Почати голосовий запис",
"micStopAria": "Зупинити запис",
"uploadImageAria": "Завантажити зображення",
"micPermissionDenied": "Не вдалося отримати доступ до мікрофона. Будь ласка, перевірте дозволи вашого браузера.",
"uploadedImageAlt": "Завантажене зображення"
}; };

View File

@@ -1,5 +1,18 @@
export default { export default {
"privacyRead": "已阅读并接受", "privacyRead": "已阅读并接受",
"telegramAssistantIntro": "你也可以在 Telegram 上联系 Growheads 助手:", "privacyPromptBefore": "请确认您已阅读 ",
"telegramAssistantLink": "t.me/Growheads_de_Bot" "privacyPolicyLink": "隐私政策",
"privacyPromptAfter": " 并同意。 ",
"telegramAssistantIntro": "您也可以在 Telegram 上联系 Growheads 助手:",
"telegramAssistantLink": "t.me/Growheads_de_Bot",
"assistantTitle": "助手",
"placeholderRecording": "录音中…",
"inputPlaceholder": "您可以向我询问关于 cannabis 品种的问题…",
"send": "发送",
"closeAria": "关闭助手",
"micStartAria": "开始语音录音",
"micStopAria": "停止录音",
"uploadImageAria": "上传图片",
"micPermissionDenied": "无法访问麦克风。请检查您的浏览器权限。",
"uploadedImageAlt": "已上传的图片"
}; };

View File

@@ -0,0 +1,22 @@
/** Matches MUI default `sm` breakpoint (600px). */
export const PRODUCT_CARD_MOBILE_MAX_WIDTH_PX = 600;
export const PRODUCT_CARD_GAP_PX = 16;
export const PRODUCT_CARD_WIDTH_XS_PX = 260;
export const PRODUCT_CARD_WIDTH_SM_PX = 250;
export function isCompactProductCardViewport() {
return (
typeof window !== "undefined" &&
window.innerWidth < PRODUCT_CARD_MOBILE_MAX_WIDTH_PX
);
}
/** Card width + flex gap — must match Product carousel item stride for translate math. */
export function getProductCarouselItemStridePx() {
return (
(isCompactProductCardViewport()
? PRODUCT_CARD_WIDTH_XS_PX
: PRODUCT_CARD_WIDTH_SM_PX) + PRODUCT_CARD_GAP_PX
);
}