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:
@@ -52,7 +52,12 @@ class ChatAssistant extends Component {
|
||||
this.fileInputRef = React.createRef();
|
||||
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() {
|
||||
// Add socket listeners if socket is available and connected
|
||||
this.addSocketListeners();
|
||||
@@ -69,7 +74,7 @@ class ChatAssistant extends Component {
|
||||
const privacyMessage = {
|
||||
id: 'privacy-prompt',
|
||||
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];
|
||||
window.chatMessages = updatedMessages;
|
||||
@@ -84,6 +89,19 @@ class ChatAssistant extends Component {
|
||||
}
|
||||
|
||||
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) {
|
||||
this.scrollToBottom();
|
||||
}
|
||||
@@ -244,7 +262,7 @@ class ChatAssistant extends Component {
|
||||
});
|
||||
} catch (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 = {
|
||||
id: Date.now(),
|
||||
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
|
||||
};
|
||||
|
||||
@@ -451,7 +469,7 @@ class ChatAssistant extends Component {
|
||||
}
|
||||
|
||||
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">
|
||||
Assistent
|
||||
{t('chat.assistantTitle')}
|
||||
<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.atWeb ? "error" : "text.disabled"} sx={{ display: 'inline' }}>🌐</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 />
|
||||
</IconButton>
|
||||
</Box>
|
||||
@@ -648,7 +666,7 @@ class ChatAssistant extends Component {
|
||||
autoFocus
|
||||
autoCapitalize="off"
|
||||
autoCorrect="off"
|
||||
placeholder={isRecording ? "Aufnahme läuft..." : "Du kannst mich nach Cannabissorten fragen..."}
|
||||
placeholder={isRecording ? t('chat.placeholderRecording') : t('chat.inputPlaceholder')}
|
||||
value={inputValue}
|
||||
onChange={this.handleInputChange}
|
||||
onKeyDown={this.handleKeyDown}
|
||||
@@ -670,7 +688,7 @@ class ChatAssistant extends Component {
|
||||
<IconButton
|
||||
color="error"
|
||||
onClick={this.stopRecording}
|
||||
aria-label="Aufnahme stoppen"
|
||||
aria-label={t('chat.micStopAria')}
|
||||
sx={{ ml: { xs: 0, sm: 1 } }}
|
||||
>
|
||||
<StopIcon />
|
||||
@@ -679,7 +697,7 @@ class ChatAssistant extends Component {
|
||||
<IconButton
|
||||
color="primary"
|
||||
onClick={this.startRecording}
|
||||
aria-label="Sprachaufnahme starten"
|
||||
aria-label={t('chat.micStartAria')}
|
||||
sx={{ ml: { xs: 0, sm: 1 } }}
|
||||
disabled={isTyping || inputsDisabled}
|
||||
>
|
||||
@@ -690,7 +708,7 @@ class ChatAssistant extends Component {
|
||||
<IconButton
|
||||
color="primary"
|
||||
onClick={this.handleImageUpload}
|
||||
aria-label="Bild hochladen"
|
||||
aria-label={t('chat.uploadImageAria')}
|
||||
sx={{ ml: { xs: 0, sm: 1 } }}
|
||||
disabled={isTyping || isRecording || inputsDisabled}
|
||||
>
|
||||
@@ -703,7 +721,7 @@ class ChatAssistant extends Component {
|
||||
onClick={this.handleSendMessage}
|
||||
disabled={isTyping || isRecording || inputsDisabled}
|
||||
>
|
||||
Senden
|
||||
{t('chat.send')}
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
@@ -11,6 +11,11 @@ import { Link, useNavigate } from 'react-router-dom';
|
||||
import { withI18n } from '../i18n/withTranslation.js';
|
||||
import ZoomInIcon from '@mui/icons-material/ZoomIn';
|
||||
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
|
||||
const findLevel1CategoryId = (categoryId) => {
|
||||
@@ -276,7 +281,16 @@ class Product extends Component {
|
||||
<Box sx={{
|
||||
position: 'relative',
|
||||
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 && (
|
||||
<div
|
||||
@@ -362,22 +376,36 @@ class Product extends Component {
|
||||
|
||||
<Card
|
||||
sx={{
|
||||
width: { xs: '100vw', sm: '250px' },
|
||||
minWidth: { xs: '100vw', sm: '250px' },
|
||||
width: {
|
||||
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%',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
transition: 'transform 0.3s ease, box-shadow 0.3s ease',
|
||||
position: 'relative',
|
||||
overflow: 'hidden',
|
||||
borderRadius: { xs: 0, sm: '8px' },
|
||||
border: { xs: 'none', sm: 'inherit' },
|
||||
boxShadow: { xs: 'none', sm: 'inherit' },
|
||||
mx: { xs: 0, sm: 'auto' },
|
||||
borderRadius: { xs: '8px', sm: '8px' },
|
||||
border: { xs: '1px solid', sm: 'inherit' },
|
||||
borderColor: { xs: 'divider', sm: 'inherit' },
|
||||
boxShadow: { xs: '0 1px 4px rgba(0,0,0,0.08)', sm: 'inherit' },
|
||||
mx: { xs: 'auto', sm: 'auto' },
|
||||
'&:hover': {
|
||||
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 && (
|
||||
@@ -460,7 +488,7 @@ class Product extends Component {
|
||||
<CardMedia
|
||||
key={index}
|
||||
component="img"
|
||||
height={window.innerWidth < 600 ? "240" : "180"}
|
||||
height={window.innerWidth < PRODUCT_CARD_MOBILE_MAX_WIDTH_PX ? "240" : "180"}
|
||||
image={imgSrc}
|
||||
alt={name}
|
||||
fetchPriority={this.props.priority === 'high' && index === 0 ? 'high' : 'auto'}
|
||||
@@ -489,7 +517,7 @@ class Product extends Component {
|
||||
) : (
|
||||
<CardMedia
|
||||
component="img"
|
||||
height={window.innerWidth < 600 ? "240" : "180"}
|
||||
height={window.innerWidth < PRODUCT_CARD_MOBILE_MAX_WIDTH_PX ? "240" : "180"}
|
||||
image="/assets/images/nopicture.jpg"
|
||||
alt={name}
|
||||
fetchPriority={this.props.priority === 'high' ? 'high' : 'auto'}
|
||||
|
||||
@@ -8,8 +8,11 @@ import ChevronRight from "@mui/icons-material/ChevronRight";
|
||||
import Product from "./Product.js";
|
||||
import { withTranslation } from 'react-i18next';
|
||||
import { withLanguage } from '../i18n/withTranslation.js';
|
||||
|
||||
const ITEM_WIDTH = 250 + 16; // 250px width + 16px gap
|
||||
import {
|
||||
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 AUTOSCROLL_RESTART_DELAY = 5000; // 5 seconds of inactivity before restarting autoscroll
|
||||
const SCROLLBAR_FLASH_DURATION = 3000; // 3 seconds to show the virtual scrollbar
|
||||
@@ -81,13 +84,31 @@ class ProductCarousel extends React.Component {
|
||||
products: [],
|
||||
currentLanguage: (i18n && i18n.language) || 'de',
|
||||
showScrollbar: false,
|
||||
itemStride:
|
||||
typeof window !== "undefined"
|
||||
? getProductCarouselItemStridePx()
|
||||
: PRODUCT_CARD_WIDTH_SM_PX + 16,
|
||||
};
|
||||
|
||||
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() {
|
||||
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;
|
||||
|
||||
logCarousel("mount", {
|
||||
@@ -370,6 +391,9 @@ class ProductCarousel extends React.Component {
|
||||
|
||||
componentWillUnmount() {
|
||||
this._isMounted = false;
|
||||
if (typeof window !== "undefined") {
|
||||
window.removeEventListener("resize", this.handleCarouselResize);
|
||||
}
|
||||
this.stopAutoScroll();
|
||||
this.clearInactivityTimer();
|
||||
this.clearScrollbarTimer();
|
||||
@@ -430,8 +454,9 @@ class ProductCarousel extends React.Component {
|
||||
this.translateX -= AUTO_SCROLL_SPEED;
|
||||
this.updateTrackTransform();
|
||||
|
||||
const { itemStride } = this.state;
|
||||
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
|
||||
if (Math.abs(this.translateX) >= maxScroll) {
|
||||
@@ -467,14 +492,15 @@ class ProductCarousel extends React.Component {
|
||||
if (this.originalProducts.length === 0) return;
|
||||
|
||||
// direction: 1 = left (scroll content right), -1 = right (scroll content left)
|
||||
const { itemStride } = this.state;
|
||||
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)
|
||||
if (this.translateX > 0) {
|
||||
this.translateX = -(maxScroll - ITEM_WIDTH);
|
||||
this.translateX = -(maxScroll - itemStride);
|
||||
}
|
||||
// Handle wrap-around when scrolling right (negative translateX beyond limit)
|
||||
else if (Math.abs(this.translateX) >= maxScroll) {
|
||||
@@ -494,9 +520,13 @@ class ProductCarousel extends React.Component {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { itemStride } = this.state;
|
||||
const originalItemCount = this.originalProducts.length;
|
||||
const viewportWidth = 1080; // carousel container max-width
|
||||
const itemsInView = Math.floor(viewportWidth / ITEM_WIDTH);
|
||||
const viewportWidth =
|
||||
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)
|
||||
let currentItemIndex;
|
||||
@@ -504,11 +534,11 @@ class ProductCarousel extends React.Component {
|
||||
if (this.translateX === 0) {
|
||||
currentItemIndex = 0;
|
||||
} else if (this.translateX > 0) {
|
||||
const maxScroll = ITEM_WIDTH * originalItemCount;
|
||||
const maxScroll = itemStride * originalItemCount;
|
||||
const effectivePosition = maxScroll + this.translateX;
|
||||
currentItemIndex = Math.floor(effectivePosition / ITEM_WIDTH);
|
||||
currentItemIndex = Math.floor(effectivePosition / itemStride);
|
||||
} else {
|
||||
currentItemIndex = Math.floor(Math.abs(this.translateX) / ITEM_WIDTH);
|
||||
currentItemIndex = Math.floor(Math.abs(this.translateX) / itemStride);
|
||||
}
|
||||
|
||||
// Ensure we stay within bounds
|
||||
@@ -615,7 +645,7 @@ class ProductCarousel extends React.Component {
|
||||
top: '50%',
|
||||
left: '8px',
|
||||
transform: 'translateY(-50%)',
|
||||
zIndex: 1200,
|
||||
zIndex: 1000,
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.9)',
|
||||
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.15)',
|
||||
width: '48px',
|
||||
@@ -635,7 +665,7 @@ class ProductCarousel extends React.Component {
|
||||
top: '50%',
|
||||
right: '8px',
|
||||
transform: 'translateY(-50%)',
|
||||
zIndex: 1200,
|
||||
zIndex: 1000,
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.9)',
|
||||
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.15)',
|
||||
width: '48px',
|
||||
@@ -676,16 +706,19 @@ class ProductCarousel extends React.Component {
|
||||
}}
|
||||
>
|
||||
{products.map((product, index) => (
|
||||
<div
|
||||
<Box
|
||||
key={`${product.id}-${index}`}
|
||||
className="product-carousel-item"
|
||||
style={{
|
||||
flex: '0 0 250px',
|
||||
width: '250px',
|
||||
maxWidth: '250px',
|
||||
minWidth: '250px',
|
||||
boxSizing: 'border-box',
|
||||
position: 'relative'
|
||||
sx={{
|
||||
flex: {
|
||||
xs: `0 0 ${PRODUCT_CARD_WIDTH_XS_PX}px`,
|
||||
sm: `0 0 ${PRODUCT_CARD_WIDTH_SM_PX}px`,
|
||||
},
|
||||
width: { xs: PRODUCT_CARD_WIDTH_XS_PX, sm: PRODUCT_CARD_WIDTH_SM_PX },
|
||||
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
|
||||
@@ -713,7 +746,7 @@ class ProductCarousel extends React.Component {
|
||||
priority={index < 6 ? 'high' : 'auto'}
|
||||
t={t}
|
||||
/>
|
||||
</div>
|
||||
</Box>
|
||||
))}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -438,7 +438,11 @@ class ProductList extends Component {
|
||||
</Stack>
|
||||
</Box>
|
||||
|
||||
<Grid container spacing={{ xs: 0, sm: 2 }}>
|
||||
<Grid
|
||||
container
|
||||
spacing={{ xs: 0, sm: 2 }}
|
||||
sx={{ bgcolor: { xs: '#e8f5e8', sm: 'transparent' } }}
|
||||
>
|
||||
{this.renderNoProductsMessage()}
|
||||
{products.map((product, index) => (
|
||||
<Grid
|
||||
@@ -448,6 +452,7 @@ class ProductList extends Component {
|
||||
justifyContent: { xs: 'stretch', sm: 'center' },
|
||||
mb: { xs: 0, sm: 1 },
|
||||
width: { xs: '100%', sm: 'auto' },
|
||||
bgcolor: { xs: '#e8f5e8', sm: 'transparent' },
|
||||
borderBottom: {
|
||||
xs: index < products.length - 1 ? '16px solid #e8f5e8' : 'none',
|
||||
sm: 'none'
|
||||
|
||||
@@ -327,7 +327,7 @@ class SharedCarousel extends React.Component {
|
||||
top: '50%',
|
||||
left: '8px',
|
||||
transform: 'translateY(-50%)',
|
||||
zIndex: 1200,
|
||||
zIndex: 1000,
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.9)',
|
||||
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.15)',
|
||||
width: '48px',
|
||||
@@ -347,7 +347,7 @@ class SharedCarousel extends React.Component {
|
||||
top: '50%',
|
||||
right: '8px',
|
||||
transform: 'translateY(-50%)',
|
||||
zIndex: 1200,
|
||||
zIndex: 1000,
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.9)',
|
||||
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.15)',
|
||||
width: '48px',
|
||||
|
||||
@@ -519,7 +519,7 @@ class CategoryList extends Component {
|
||||
to="/Konfigurator"
|
||||
color="inherit"
|
||||
size="small"
|
||||
aria-label="Zur Startseite"
|
||||
aria-label={this.props.t ? this.props.t('navigation.konfiguratorAria') : 'Zum Konfigurator'}
|
||||
onClick={isMobile ? this.handleMobileCategoryClick : undefined}
|
||||
sx={{
|
||||
fontSize: "0.75rem",
|
||||
@@ -572,7 +572,7 @@ class CategoryList extends Component {
|
||||
zIndex: 2,
|
||||
}}
|
||||
>
|
||||
{this.props.t ? this.props.t('navigation.home') : 'Startseite'}
|
||||
{this.props.t ? this.props.t('sections.konfigurator') : 'Konfigurator'}
|
||||
</Box>
|
||||
{/* Thin text (positioned on top) */}
|
||||
<Box
|
||||
@@ -586,7 +586,7 @@ class CategoryList extends Component {
|
||||
zIndex: 1,
|
||||
}}
|
||||
>
|
||||
{this.props.t ? this.props.t('navigation.home') : 'Startseite'}
|
||||
{this.props.t ? this.props.t('sections.konfigurator') : 'Konfigurator'}
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
@@ -1,5 +1,18 @@
|
||||
export default {
|
||||
"privacyRead": "مقروء ومقبول",
|
||||
"telegramAssistantIntro": "تقدر كمان توصل لمساعد Growheads على تيليجرام:",
|
||||
"telegramAssistantLink": "t.me/Growheads_de_Bot"
|
||||
"privacyRead": "قريت ووافقت",
|
||||
"privacyPromptBefore": "من فضلك أكد إنك قرأت ",
|
||||
"privacyPolicyLink": "سياسة الخصوصية",
|
||||
"privacyPromptAfter": " ووافقت عليها. ",
|
||||
"telegramAssistantIntro": "كمان تقدر تتواصل مع مساعد Growheads على Telegram:",
|
||||
"telegramAssistantLink": "t.me/Growheads_de_Bot",
|
||||
"assistantTitle": "المساعد",
|
||||
"placeholderRecording": "جارٍ التسجيل…",
|
||||
"inputPlaceholder": "تقدر تسألني عن سلالات القنب…",
|
||||
"send": "إرسال",
|
||||
"closeAria": "إغلاق المساعد",
|
||||
"micStartAria": "ابدأ تسجيل الصوت",
|
||||
"micStopAria": "إيقاف التسجيل",
|
||||
"uploadImageAria": "رفع صورة",
|
||||
"micPermissionDenied": "تعذر الوصول إلى الميكروفون. من فضلك راجع أذونات المتصفح.",
|
||||
"uploadedImageAlt": "صورة مرفوعة"
|
||||
};
|
||||
|
||||
@@ -1,5 +1,18 @@
|
||||
export default {
|
||||
"privacyRead": "Прочетено и прието",
|
||||
"privacyPromptBefore": "Моля, потвърдете, че сте прочели ",
|
||||
"privacyPolicyLink": "политиката за поверителност",
|
||||
"privacyPromptAfter": " и сте съгласни с нея. ",
|
||||
"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": "Качено изображение"
|
||||
};
|
||||
|
||||
@@ -1,5 +1,18 @@
|
||||
export default {
|
||||
"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:",
|
||||
"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"
|
||||
};
|
||||
|
||||
@@ -1,5 +1,18 @@
|
||||
export default {
|
||||
"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:",
|
||||
"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"
|
||||
};
|
||||
@@ -1,5 +1,6 @@
|
||||
export default {
|
||||
"home": "Startseite",
|
||||
"konfiguratorAria": "Zum Konfigurator",
|
||||
"new": "Neuheiten",
|
||||
"soon": "Demnächst",
|
||||
"aktionen": "Aktionen",
|
||||
|
||||
@@ -1,5 +1,18 @@
|
||||
export default {
|
||||
"privacyRead": "Διαβάστηκε & Έγινε αποδεκτό",
|
||||
"telegramAssistantIntro": "Μπορείτε επίσης να επικοινωνήσετε με τον βοηθό Growheads στο Telegram:",
|
||||
"telegramAssistantLink": "t.me/Growheads_de_Bot"
|
||||
"privacyRead": "Διαβάστηκε & Εγκρίθηκε",
|
||||
"privacyPromptBefore": "Παρακαλώ επιβεβαιώστε ότι έχετε διαβάσει την ",
|
||||
"privacyPolicyLink": "πολιτική απορρήτου",
|
||||
"privacyPromptAfter": " και ότι συμφωνείτε με αυτήν. ",
|
||||
"telegramAssistantIntro": "Μπορείτε επίσης να επικοινωνήσετε με τον βοηθό του Growheads στο Telegram:",
|
||||
"telegramAssistantLink": "t.me/Growheads_de_Bot",
|
||||
"assistantTitle": "Βοηθός",
|
||||
"placeholderRecording": "Γίνεται εγγραφή…",
|
||||
"inputPlaceholder": "Μπορείτε να με ρωτήσετε για ποικιλίες cannabis…",
|
||||
"send": "Αποστολή",
|
||||
"closeAria": "Κλείσιμο βοηθού",
|
||||
"micStartAria": "Έναρξη ηχογράφησης φωνής",
|
||||
"micStopAria": "Διακοπή εγγραφής",
|
||||
"uploadImageAria": "Μεταφόρτωση εικόνας",
|
||||
"micPermissionDenied": "Δεν ήταν δυνατή η πρόσβαση στο μικρόφωνο. Παρακαλώ ελέγξτε τα δικαιώματα του browser σας.",
|
||||
"uploadedImageAlt": "Ανεβασμένη εικόνα"
|
||||
};
|
||||
|
||||
@@ -1,5 +1,18 @@
|
||||
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:",
|
||||
"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"
|
||||
};
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
export default {
|
||||
"home": "Home", // Startseite
|
||||
"konfiguratorAria": "Go to Configurator", // Zum Konfigurator
|
||||
"new": "New Arrivals", // Neuheiten
|
||||
"soon": "Coming Soon", // Demnächst
|
||||
"aktionen": "Promotions", // Aktionen
|
||||
|
||||
@@ -1,5 +1,18 @@
|
||||
export default {
|
||||
"privacyRead": "Leído y aceptado",
|
||||
"telegramAssistantIntro": "También puedes contactar al asistente de Growheads en Telegram:",
|
||||
"telegramAssistantLink": "t.me/Growheads_de_Bot"
|
||||
"privacyPromptBefore": "Por favor, confirma que has leído la ",
|
||||
"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"
|
||||
};
|
||||
|
||||
@@ -1,5 +1,18 @@
|
||||
export default {
|
||||
"privacyRead": "Lu et accepté",
|
||||
"telegramAssistantIntro": "Vous pouvez également joindre l'assistant Growheads sur Telegram :",
|
||||
"telegramAssistantLink": "t.me/Growheads_de_Bot"
|
||||
"privacyPromptBefore": "Veuillez confirmer que vous avez lu la ",
|
||||
"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"
|
||||
};
|
||||
|
||||
@@ -1,5 +1,18 @@
|
||||
export default {
|
||||
"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:",
|
||||
"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"
|
||||
};
|
||||
|
||||
@@ -1,5 +1,18 @@
|
||||
export default {
|
||||
"privacyRead": "Elolvasva és elfogadva",
|
||||
"telegramAssistantIntro": "A Growheads asszisztenst Telegramon is elérheted:",
|
||||
"telegramAssistantLink": "t.me/Growheads_de_Bot"
|
||||
"privacyRead": "Elolvastam és elfogadtam",
|
||||
"privacyPromptBefore": "Kérjük, erősítse meg, hogy elolvasta a ",
|
||||
"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"
|
||||
};
|
||||
|
||||
@@ -1,5 +1,18 @@
|
||||
export default {
|
||||
"privacyRead": "Letto e accettato",
|
||||
"telegramAssistantIntro": "Puoi anche contattare l'assistente Growheads su Telegram:",
|
||||
"telegramAssistantLink": "t.me/Growheads_de_Bot"
|
||||
"privacyPromptBefore": "Conferma di aver letto la ",
|
||||
"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"
|
||||
};
|
||||
|
||||
@@ -1,5 +1,18 @@
|
||||
export default {
|
||||
"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:",
|
||||
"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"
|
||||
};
|
||||
|
||||
@@ -1,5 +1,18 @@
|
||||
export default {
|
||||
"privacyRead": "Citit și acceptat",
|
||||
"telegramAssistantIntro": "Puteți, de asemenea, contacta asistentul Growheads pe Telegram:",
|
||||
"telegramAssistantLink": "t.me/Growheads_de_Bot"
|
||||
"privacyPromptBefore": "Te rugăm să confirmi că ai citit ",
|
||||
"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ă"
|
||||
};
|
||||
|
||||
@@ -1,5 +1,18 @@
|
||||
export default {
|
||||
"privacyRead": "Прочитано и принято",
|
||||
"privacyPromptBefore": "Пожалуйста, подтвердите, что вы прочитали ",
|
||||
"privacyPolicyLink": "политику конфиденциальности",
|
||||
"privacyPromptAfter": " и согласны с ней. ",
|
||||
"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": "Загруженное изображение"
|
||||
};
|
||||
|
||||
@@ -1,5 +1,18 @@
|
||||
export default {
|
||||
"privacyRead": "Prečítané a akceptované",
|
||||
"telegramAssistantIntro": "Môžete tiež kontaktovať asistenta Growheads na Telegrame:",
|
||||
"telegramAssistantLink": "t.me/Growheads_de_Bot"
|
||||
"privacyRead": "Prečítané a prijaté",
|
||||
"privacyPromptBefore": "Prosím potvrďte, že ste si prečítali ",
|
||||
"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"
|
||||
};
|
||||
|
||||
@@ -1,5 +1,18 @@
|
||||
export default {
|
||||
"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:",
|
||||
"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"
|
||||
};
|
||||
|
||||
@@ -1,5 +1,18 @@
|
||||
export default {
|
||||
"privacyRead": "Lexuar & Pranuar",
|
||||
"telegramAssistantIntro": "Mund ta kontaktoni gjithashtu asistentin e Growheads në Telegram:",
|
||||
"telegramAssistantLink": "t.me/Growheads_de_Bot"
|
||||
"privacyRead": "Lexuar dhe pranuar",
|
||||
"privacyPromptBefore": "Ju lutemi konfirmoni që e keni lexuar ",
|
||||
"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"
|
||||
};
|
||||
|
||||
@@ -1,5 +1,18 @@
|
||||
export default {
|
||||
"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:",
|
||||
"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"
|
||||
};
|
||||
|
||||
@@ -1,5 +1,18 @@
|
||||
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:",
|
||||
"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"
|
||||
};
|
||||
|
||||
@@ -1,5 +1,18 @@
|
||||
export default {
|
||||
"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:",
|
||||
"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"
|
||||
};
|
||||
|
||||
@@ -1,5 +1,18 @@
|
||||
export default {
|
||||
"privacyRead": "Прочитано та прийнято",
|
||||
"telegramAssistantIntro": "Ви також можете зв’язатися з асистентом Growheads у Telegram:",
|
||||
"telegramAssistantLink": "t.me/Growheads_de_Bot"
|
||||
"privacyPromptBefore": "Будь ласка, підтвердьте, що ви прочитали ",
|
||||
"privacyPolicyLink": "політику конфіденційності",
|
||||
"privacyPromptAfter": " і погоджуєтеся з нею. ",
|
||||
"telegramAssistantIntro": "Ви також можете звернутися до асистента Growheads у Telegram:",
|
||||
"telegramAssistantLink": "t.me/Growheads_de_Bot",
|
||||
"assistantTitle": "Асистент",
|
||||
"placeholderRecording": "Запис…",
|
||||
"inputPlaceholder": "Ви можете запитати мене про сорти канабісу…",
|
||||
"send": "Надіслати",
|
||||
"closeAria": "Закрити асистента",
|
||||
"micStartAria": "Почати голосовий запис",
|
||||
"micStopAria": "Зупинити запис",
|
||||
"uploadImageAria": "Завантажити зображення",
|
||||
"micPermissionDenied": "Не вдалося отримати доступ до мікрофона. Будь ласка, перевірте дозволи вашого браузера.",
|
||||
"uploadedImageAlt": "Завантажене зображення"
|
||||
};
|
||||
|
||||
@@ -1,5 +1,18 @@
|
||||
export default {
|
||||
"privacyRead": "已阅读并接受",
|
||||
"telegramAssistantIntro": "你也可以在 Telegram 上联系 Growheads 助手:",
|
||||
"telegramAssistantLink": "t.me/Growheads_de_Bot"
|
||||
"privacyPromptBefore": "请确认您已阅读 ",
|
||||
"privacyPolicyLink": "隐私政策",
|
||||
"privacyPromptAfter": " 并同意。 ",
|
||||
"telegramAssistantIntro": "您也可以在 Telegram 上联系 Growheads 助手:",
|
||||
"telegramAssistantLink": "t.me/Growheads_de_Bot",
|
||||
"assistantTitle": "助手",
|
||||
"placeholderRecording": "录音中…",
|
||||
"inputPlaceholder": "您可以向我询问关于 cannabis 品种的问题…",
|
||||
"send": "发送",
|
||||
"closeAria": "关闭助手",
|
||||
"micStartAria": "开始语音录音",
|
||||
"micStopAria": "停止录音",
|
||||
"uploadImageAria": "上传图片",
|
||||
"micPermissionDenied": "无法访问麦克风。请检查您的浏览器权限。",
|
||||
"uploadedImageAlt": "已上传的图片"
|
||||
};
|
||||
|
||||
22
src/utils/productCardLayout.js
Normal file
22
src/utils/productCardLayout.js
Normal 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
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user