feat: Enhance ChatAssistant and ProductFilters components with dynamic privacy prompts and category push notification support; update localization strings for new article notifications across multiple languages
This commit is contained in:
@@ -58,17 +58,48 @@ class ChatAssistant extends Component {
|
|||||||
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>`;
|
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>`;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** Keep stored privacy bubble in sync with i18n (language switcher, lazy bundle load). */
|
||||||
|
applyPrivacyPromptTranslation = () => {
|
||||||
|
this.setState((prev) => {
|
||||||
|
if (prev.privacyConfirmed) return null;
|
||||||
|
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 };
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
handleI18nLanguageChanged = () => {
|
||||||
|
this.applyPrivacyPromptTranslation();
|
||||||
|
};
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
// Add socket listeners if socket is available and connected
|
// Add socket listeners if socket is available and connected
|
||||||
this.addSocketListeners();
|
this.addSocketListeners();
|
||||||
|
|
||||||
|
this.props.i18n?.on('languageChanged', this.handleI18nLanguageChanged);
|
||||||
|
|
||||||
const userStatus = isUserLoggedIn();
|
const userStatus = isUserLoggedIn();
|
||||||
const isGuest = !userStatus.isLoggedIn;
|
const isGuest = !userStatus.isLoggedIn;
|
||||||
|
|
||||||
if (isGuest && !this.state.privacyConfirmed) {
|
if (isGuest && !this.state.privacyConfirmed) {
|
||||||
this.setState(prevState => {
|
this.setState(prevState => {
|
||||||
if (prevState.messages.find(msg => msg.id === 'privacy-prompt')) {
|
if (prevState.messages.find(msg => msg.id === 'privacy-prompt')) {
|
||||||
return { isGuest: true };
|
const updatedMessages = prevState.messages.map((msg) =>
|
||||||
|
msg.id === 'privacy-prompt'
|
||||||
|
? { ...msg, text: this.buildPrivacyPromptHtml() }
|
||||||
|
: msg
|
||||||
|
);
|
||||||
|
window.chatMessages = updatedMessages;
|
||||||
|
return {
|
||||||
|
messages: updatedMessages,
|
||||||
|
isGuest: true,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const privacyMessage = {
|
const privacyMessage = {
|
||||||
@@ -90,17 +121,7 @@ class ChatAssistant extends Component {
|
|||||||
|
|
||||||
componentDidUpdate(prevProps, prevState) {
|
componentDidUpdate(prevProps, prevState) {
|
||||||
if (prevProps.i18n?.language !== this.props.i18n?.language) {
|
if (prevProps.i18n?.language !== this.props.i18n?.language) {
|
||||||
this.setState((prev) => {
|
this.applyPrivacyPromptTranslation();
|
||||||
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();
|
||||||
@@ -108,6 +129,7 @@ class ChatAssistant extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
|
this.props.i18n?.off('languageChanged', this.handleI18nLanguageChanged);
|
||||||
this.removeSocketListeners();
|
this.removeSocketListeners();
|
||||||
this.stopRecording();
|
this.stopRecording();
|
||||||
if (this.recordingTimer) {
|
if (this.recordingTimer) {
|
||||||
|
|||||||
@@ -448,6 +448,29 @@ class Content extends Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// JTL kKategorie for category push: backend may omit dataParam — resolve from tree (same id as product list)
|
||||||
|
const isValidJtlCategoryId = (v) => {
|
||||||
|
if (v == null || v === '') return false;
|
||||||
|
const n = typeof v === 'number' ? v : parseInt(String(v), 10);
|
||||||
|
return Number.isFinite(n) && n > 0;
|
||||||
|
};
|
||||||
|
if (categoryId !== 'neu' && categoryId !== 'bald' && !isValidJtlCategoryId(enhancedResponse.dataParam)) {
|
||||||
|
try {
|
||||||
|
const currentLanguage = this.props.languageContext?.currentLanguage || this.props.i18n?.language || 'de';
|
||||||
|
const categoryTreeCache = window.categoryService.getSync(209, currentLanguage);
|
||||||
|
if (categoryTreeCache) {
|
||||||
|
const targetCategory = typeof categoryId === 'string'
|
||||||
|
? this.findCategoryBySeoName(categoryTreeCache, categoryId)
|
||||||
|
: this.findCategoryById(categoryTreeCache, categoryId);
|
||||||
|
if (targetCategory && typeof targetCategory.id === 'number' && targetCategory.id > 0) {
|
||||||
|
enhancedResponse.dataParam = targetCategory.id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error resolving dataParam from category tree:', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
this.processData(enhancedResponse);
|
this.processData(enhancedResponse);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,13 +2,32 @@ import React, { Component } from 'react';
|
|||||||
import Box from '@mui/material/Box';
|
import Box from '@mui/material/Box';
|
||||||
import Paper from '@mui/material/Paper';
|
import Paper from '@mui/material/Paper';
|
||||||
import Typography from '@mui/material/Typography';
|
import Typography from '@mui/material/Typography';
|
||||||
|
import Button from '@mui/material/Button';
|
||||||
|
import Tooltip from '@mui/material/Tooltip';
|
||||||
|
import CircularProgress from '@mui/material/CircularProgress';
|
||||||
|
import NotificationsIcon from '@mui/icons-material/Notifications';
|
||||||
|
import NotificationsActiveIcon from '@mui/icons-material/NotificationsActive';
|
||||||
import Filter from './Filter.js';
|
import Filter from './Filter.js';
|
||||||
import { useParams, useSearchParams, useNavigate, useLocation } from 'react-router-dom';
|
import { useParams, useSearchParams, useNavigate, useLocation } from 'react-router-dom';
|
||||||
import { setSessionSetting, removeSessionSetting, clearAllSessionSettings } from '../utils/sessionStorage.js';
|
import { setSessionSetting, removeSessionSetting, clearAllSessionSettings } from '../utils/sessionStorage.js';
|
||||||
import { withI18n } from '../i18n/withTranslation.js';
|
import { withI18n } from '../i18n/withTranslation.js';
|
||||||
|
import {
|
||||||
|
isPushApiSupported,
|
||||||
|
fetchPushConfiguration,
|
||||||
|
registerPushServiceWorker,
|
||||||
|
ensurePushSubscription,
|
||||||
|
categoryPushStatus,
|
||||||
|
categoryPushSubscribe,
|
||||||
|
categoryPushUnsubscribe,
|
||||||
|
parseSubscribedStatus,
|
||||||
|
parseSuccess,
|
||||||
|
} from '../utils/categoryPush.js';
|
||||||
|
|
||||||
const isNew = (neu) => neu && (new Date().getTime() - new Date(neu).getTime() < 30 * 24 * 60 * 60 * 1000);
|
const isNew = (neu) => neu && (new Date().getTime() - new Date(neu).getTime() < 30 * 24 * 60 * 60 * 1000);
|
||||||
|
|
||||||
|
/** Category push subscribe UI only when the category has more than this many articles. */
|
||||||
|
const MIN_ARTICLES_FOR_CATEGORY_PUSH = 10;
|
||||||
|
|
||||||
// HOC to provide router props to class components
|
// HOC to provide router props to class components
|
||||||
const withRouter = (ClassComponent) => {
|
const withRouter = (ClassComponent) => {
|
||||||
return (props) => {
|
return (props) => {
|
||||||
@@ -39,6 +58,10 @@ class ProductFilters extends Component {
|
|||||||
uniqueManufacturerArray,
|
uniqueManufacturerArray,
|
||||||
attributeGroups,
|
attributeGroups,
|
||||||
manufacturerImages: new Map(), // id (number) → object URL
|
manufacturerImages: new Map(), // id (number) → object URL
|
||||||
|
pushInteractive: false,
|
||||||
|
pushSubscribed: false,
|
||||||
|
pushBusy: false,
|
||||||
|
pushError: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
this._manufacturerImageUrls = []; // track for cleanup
|
this._manufacturerImageUrls = []; // track for cleanup
|
||||||
@@ -48,6 +71,7 @@ class ProductFilters extends Component {
|
|||||||
this.adjustPaperHeight();
|
this.adjustPaperHeight();
|
||||||
window.addEventListener('resize', this.adjustPaperHeight);
|
window.addEventListener('resize', this.adjustPaperHeight);
|
||||||
this._loadManufacturerImages();
|
this._loadManufacturerImages();
|
||||||
|
this.refreshCategoryPushStatus();
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
@@ -103,8 +127,135 @@ class ProductFilters extends Component {
|
|||||||
const attributeGroups = this._getAttributeGroups(this.props.attributes);
|
const attributeGroups = this._getAttributeGroups(this.props.attributes);
|
||||||
this.setState({attributeGroups});
|
this.setState({attributeGroups});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const prevCount = prevProps.products?.length || 0;
|
||||||
|
const nextCount = this.props.products?.length || 0;
|
||||||
|
if (
|
||||||
|
prevProps.dataParam !== this.props.dataParam ||
|
||||||
|
prevProps.dataType !== this.props.dataType ||
|
||||||
|
prevProps.params?.categoryId !== this.props.params?.categoryId ||
|
||||||
|
prevCount !== nextCount
|
||||||
|
) {
|
||||||
|
this.refreshCategoryPushStatus();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
kKategorieNumber = () => {
|
||||||
|
const { dataParam, dataType } = this.props;
|
||||||
|
if (dataType !== 'category') return null;
|
||||||
|
if (dataParam == null || dataParam === '') return null;
|
||||||
|
const n = typeof dataParam === 'number' ? dataParam : parseInt(String(dataParam), 10);
|
||||||
|
return Number.isFinite(n) && n > 0 ? n : null;
|
||||||
|
};
|
||||||
|
|
||||||
|
shouldShowCategoryPush = () =>
|
||||||
|
(this.props.products?.length || 0) > MIN_ARTICLES_FOR_CATEGORY_PUSH;
|
||||||
|
|
||||||
|
refreshCategoryPushStatus = async () => {
|
||||||
|
const kKat = this.kKategorieNumber();
|
||||||
|
if (!kKat || !this.shouldShowCategoryPush() || !isPushApiSupported()) {
|
||||||
|
this.setState({
|
||||||
|
pushInteractive: false,
|
||||||
|
pushSubscribed: false,
|
||||||
|
pushError: null,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const cfg = await fetchPushConfiguration();
|
||||||
|
if (!cfg.configured || !cfg.publicKey) {
|
||||||
|
this.setState({ pushInteractive: false });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await registerPushServiceWorker();
|
||||||
|
const registration = await navigator.serviceWorker.ready;
|
||||||
|
const subscription = await registration.pushManager.getSubscription();
|
||||||
|
if (!subscription) {
|
||||||
|
this.setState({
|
||||||
|
pushInteractive: true,
|
||||||
|
pushSubscribed: false,
|
||||||
|
pushError: null,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const statusData = await categoryPushStatus(kKat, subscription.endpoint);
|
||||||
|
this.setState({
|
||||||
|
pushInteractive: true,
|
||||||
|
pushSubscribed: parseSubscribedStatus(statusData),
|
||||||
|
pushError: null,
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('ProductFilters: category push init failed', e);
|
||||||
|
this.setState({ pushInteractive: false });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
handleCategoryPushClick = async () => {
|
||||||
|
const t = this.props.t;
|
||||||
|
if (!this.state.pushInteractive || this.state.pushBusy) return;
|
||||||
|
const kKat = this.kKategorieNumber();
|
||||||
|
if (!kKat || !this.shouldShowCategoryPush()) return;
|
||||||
|
this.setState({ pushBusy: true, pushError: null });
|
||||||
|
try {
|
||||||
|
if (this.state.pushSubscribed) {
|
||||||
|
const registration = await navigator.serviceWorker.ready;
|
||||||
|
const subscription = await registration.pushManager.getSubscription();
|
||||||
|
if (!subscription) {
|
||||||
|
this.setState({ pushSubscribed: false, pushBusy: false });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const res = await categoryPushUnsubscribe(subscription.endpoint);
|
||||||
|
if (parseSuccess(res)) {
|
||||||
|
this.setState({ pushSubscribed: false });
|
||||||
|
} else {
|
||||||
|
this.setState({
|
||||||
|
pushError:
|
||||||
|
res?.message ||
|
||||||
|
res?.error ||
|
||||||
|
(t ? t('productDialogs.pushNotifyError') : ''),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const perm = await Notification.requestPermission();
|
||||||
|
if (perm !== 'granted') {
|
||||||
|
this.setState({
|
||||||
|
pushError: t ? t('productDialogs.pushNotifyPermissionDenied') : '',
|
||||||
|
pushBusy: false,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const cfg = await fetchPushConfiguration();
|
||||||
|
if (!cfg.configured || !cfg.publicKey) {
|
||||||
|
this.setState({
|
||||||
|
pushError: t ? t('productDialogs.pushNotifyServerDisabled') : '',
|
||||||
|
pushBusy: false,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await registerPushServiceWorker();
|
||||||
|
const subscription = await ensurePushSubscription(cfg.publicKey);
|
||||||
|
const res = await categoryPushSubscribe(kKat, subscription);
|
||||||
|
if (parseSuccess(res)) {
|
||||||
|
this.setState({ pushSubscribed: true });
|
||||||
|
} else {
|
||||||
|
this.setState({
|
||||||
|
pushError:
|
||||||
|
res?.message ||
|
||||||
|
res?.error ||
|
||||||
|
(t ? t('productDialogs.pushNotifyError') : ''),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('ProductFilters: category push', e);
|
||||||
|
this.setState({
|
||||||
|
pushError: e.message || (t ? t('productDialogs.pushNotifyError') : ''),
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
this.setState({ pushBusy: false });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
adjustPaperHeight = () => {
|
adjustPaperHeight = () => {
|
||||||
// Skip height adjustment on xs screens
|
// Skip height adjustment on xs screens
|
||||||
@@ -201,6 +352,25 @@ class ProductFilters extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
const kKategorie = this.kKategorieNumber();
|
||||||
|
const showCategoryPush = kKategorie && this.shouldShowCategoryPush();
|
||||||
|
const {
|
||||||
|
pushInteractive,
|
||||||
|
pushSubscribed,
|
||||||
|
pushBusy,
|
||||||
|
pushError,
|
||||||
|
} = this.state;
|
||||||
|
const pushDisabledHint =
|
||||||
|
showCategoryPush && !pushInteractive && !pushBusy
|
||||||
|
? isPushApiSupported()
|
||||||
|
? this.props.t
|
||||||
|
? this.props.t('productDialogs.pushNotifyServerDisabled')
|
||||||
|
: ''
|
||||||
|
: this.props.t
|
||||||
|
? this.props.t('filters.notifyNewArticlesBrowserUnsupported')
|
||||||
|
: 'Ihr Browser unterstützt keine Push-Benachrichtigungen.'
|
||||||
|
: '';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
@@ -231,17 +401,91 @@ class ProductFilters extends Component {
|
|||||||
>
|
>
|
||||||
|
|
||||||
{this.props.dataType == 'category' && (
|
{this.props.dataType == 'category' && (
|
||||||
<Typography
|
<Box sx={{ mb: 4 }}>
|
||||||
variant="h3"
|
<Typography
|
||||||
component="h1"
|
variant="h3"
|
||||||
sx={{
|
component="h1"
|
||||||
mb: 4,
|
sx={{
|
||||||
fontFamily: 'SwashingtonCP',
|
mb: showCategoryPush ? 1.5 : 4,
|
||||||
color: 'primary.main'
|
fontFamily: 'SwashingtonCP',
|
||||||
}}
|
color: 'primary.main',
|
||||||
>
|
}}
|
||||||
{this.props.categoryName}
|
>
|
||||||
</Typography>
|
{this.props.categoryName}
|
||||||
|
</Typography>
|
||||||
|
{showCategoryPush && (
|
||||||
|
<Box sx={{ width: '100%' }}>
|
||||||
|
<Tooltip title={pushDisabledHint} arrow>
|
||||||
|
<span style={{ display: 'block', width: '100%' }}>
|
||||||
|
<Button
|
||||||
|
fullWidth
|
||||||
|
variant="outlined"
|
||||||
|
color="inherit"
|
||||||
|
size="small"
|
||||||
|
onClick={this.handleCategoryPushClick}
|
||||||
|
disabled={!pushInteractive || pushBusy}
|
||||||
|
startIcon={
|
||||||
|
pushBusy ? (
|
||||||
|
<CircularProgress size={14} sx={{ color: 'inherit' }} />
|
||||||
|
) : pushSubscribed ? (
|
||||||
|
<NotificationsActiveIcon sx={{ fontSize: 18, color: '#2e7d32' }} />
|
||||||
|
) : (
|
||||||
|
<NotificationsIcon sx={{ fontSize: 18, color: 'rgba(0,0,0,0.65)' }} />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
sx={{
|
||||||
|
borderRadius: 1,
|
||||||
|
fontWeight: 600,
|
||||||
|
fontSize: '0.7rem',
|
||||||
|
lineHeight: 1.2,
|
||||||
|
backgroundColor: '#fff',
|
||||||
|
color: 'text.primary',
|
||||||
|
border: '1px solid',
|
||||||
|
borderColor: 'divider',
|
||||||
|
boxShadow: 'none',
|
||||||
|
whiteSpace: 'normal',
|
||||||
|
textAlign: 'center',
|
||||||
|
py: 0.4,
|
||||||
|
px: 0.75,
|
||||||
|
minHeight: 28,
|
||||||
|
'& .MuiButton-label': {
|
||||||
|
whiteSpace: 'normal',
|
||||||
|
lineHeight: 1.2,
|
||||||
|
},
|
||||||
|
'& .MuiButton-startIcon': {
|
||||||
|
mr: 0.5,
|
||||||
|
'& > *:nth-of-type(1)': { fontSize: 18 },
|
||||||
|
},
|
||||||
|
'&:hover': {
|
||||||
|
backgroundColor: 'grey.50',
|
||||||
|
borderColor: 'divider',
|
||||||
|
boxShadow: 'none',
|
||||||
|
},
|
||||||
|
'&.Mui-disabled': {
|
||||||
|
backgroundColor: '#fff',
|
||||||
|
color: 'action.disabled',
|
||||||
|
borderColor: 'action.disabledBackground',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{this.props.t
|
||||||
|
? this.props.t('filters.notifyNewArticles')
|
||||||
|
: 'Bei neuen Artikeln benachrichtigen'}
|
||||||
|
</Button>
|
||||||
|
</span>
|
||||||
|
</Tooltip>
|
||||||
|
{pushError && (
|
||||||
|
<Typography
|
||||||
|
variant="caption"
|
||||||
|
color="error"
|
||||||
|
sx={{ display: 'block', mt: 0.5, textAlign: 'center' }}
|
||||||
|
>
|
||||||
|
{pushError}
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -3,5 +3,7 @@ export default {
|
|||||||
"perPage": "لكل صفحة",
|
"perPage": "لكل صفحة",
|
||||||
"availability": "التوفر",
|
"availability": "التوفر",
|
||||||
"manufacturer": "المصنّع",
|
"manufacturer": "المصنّع",
|
||||||
"all": "الكل"
|
"all": "الكل",
|
||||||
|
"notifyNewArticles": "إشعار عند توفر منتجات جديدة",
|
||||||
|
"notifyNewArticlesBrowserUnsupported": "المتصفح لا يدعم إشعارات الدفع."
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,5 +3,7 @@ export default {
|
|||||||
"perPage": "на страница",
|
"perPage": "на страница",
|
||||||
"availability": "Наличност",
|
"availability": "Наличност",
|
||||||
"manufacturer": "Производител",
|
"manufacturer": "Производител",
|
||||||
"all": "Всички"
|
"all": "Всички",
|
||||||
|
"notifyNewArticles": "Уведомявай ме за нови продукти",
|
||||||
|
"notifyNewArticlesBrowserUnsupported": "Вашият браузър не поддържа push известия."
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,5 +3,7 @@ export default {
|
|||||||
"perPage": "na stránku",
|
"perPage": "na stránku",
|
||||||
"availability": "Dostupnost",
|
"availability": "Dostupnost",
|
||||||
"manufacturer": "Výrobce",
|
"manufacturer": "Výrobce",
|
||||||
"all": "Vše"
|
"all": "Vše",
|
||||||
|
"notifyNewArticles": "Upozornit na nové produkty",
|
||||||
|
"notifyNewArticlesBrowserUnsupported": "Váš prohlížeč nepodporuje push oznámení."
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,5 +3,7 @@ export default {
|
|||||||
"perPage": "pro Seite",
|
"perPage": "pro Seite",
|
||||||
"availability": "Verfügbarkeit",
|
"availability": "Verfügbarkeit",
|
||||||
"manufacturer": "Hersteller",
|
"manufacturer": "Hersteller",
|
||||||
"all": "Alle"
|
"all": "Alle",
|
||||||
|
"notifyNewArticles": "Bei neuen Artikeln benachrichtigen",
|
||||||
|
"notifyNewArticlesBrowserUnsupported": "Ihr Browser unterstützt keine Push-Benachrichtigungen."
|
||||||
};
|
};
|
||||||
@@ -3,5 +3,7 @@ export default {
|
|||||||
"perPage": "ανά σελίδα",
|
"perPage": "ανά σελίδα",
|
||||||
"availability": "Διαθεσιμότητα",
|
"availability": "Διαθεσιμότητα",
|
||||||
"manufacturer": "Κατασκευαστής",
|
"manufacturer": "Κατασκευαστής",
|
||||||
"all": "Όλα"
|
"all": "Όλα",
|
||||||
|
"notifyNewArticles": "Ειδοποίησέ με για νέα προϊόντα",
|
||||||
|
"notifyNewArticlesBrowserUnsupported": "Ο φυλλομετρητής σας δεν υποστηρίζει push ειδοποιήσεις."
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,5 +3,7 @@ export default {
|
|||||||
"perPage": "per page", // pro Seite
|
"perPage": "per page", // pro Seite
|
||||||
"availability": "Availability", // Verfügbarkeit
|
"availability": "Availability", // Verfügbarkeit
|
||||||
"manufacturer": "Manufacturer", // Hersteller
|
"manufacturer": "Manufacturer", // Hersteller
|
||||||
"all": "All" // Alle
|
"all": "All", // Alle
|
||||||
|
"notifyNewArticles": "Notify me about new articles",
|
||||||
|
"notifyNewArticlesBrowserUnsupported": "Your browser does not support push notifications."
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,5 +3,7 @@ export default {
|
|||||||
"perPage": "por página",
|
"perPage": "por página",
|
||||||
"availability": "Disponibilidad",
|
"availability": "Disponibilidad",
|
||||||
"manufacturer": "Fabricante",
|
"manufacturer": "Fabricante",
|
||||||
"all": "Todos"
|
"all": "Todos",
|
||||||
|
"notifyNewArticles": "Notificarme sobre artículos nuevos",
|
||||||
|
"notifyNewArticlesBrowserUnsupported": "Su navegador no admite notificaciones push."
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,5 +3,7 @@ export default {
|
|||||||
"perPage": "par page",
|
"perPage": "par page",
|
||||||
"availability": "Disponibilité",
|
"availability": "Disponibilité",
|
||||||
"manufacturer": "Fabricant",
|
"manufacturer": "Fabricant",
|
||||||
"all": "Tous"
|
"all": "Tous",
|
||||||
|
"notifyNewArticles": "Être notifié des nouveaux articles",
|
||||||
|
"notifyNewArticlesBrowserUnsupported": "Votre navigateur ne prend pas en charge les notifications push."
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,5 +3,7 @@ export default {
|
|||||||
"perPage": "po stranici",
|
"perPage": "po stranici",
|
||||||
"availability": "Dostupnost",
|
"availability": "Dostupnost",
|
||||||
"manufacturer": "Proizvođač",
|
"manufacturer": "Proizvođač",
|
||||||
"all": "Sve"
|
"all": "Sve",
|
||||||
|
"notifyNewArticles": "Obavijesti me o novim artiklima",
|
||||||
|
"notifyNewArticlesBrowserUnsupported": "Vaš preglednik ne podržava push obavijesti."
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,5 +3,7 @@ export default {
|
|||||||
"perPage": "oldalanként",
|
"perPage": "oldalanként",
|
||||||
"availability": "Elérhetőség",
|
"availability": "Elérhetőség",
|
||||||
"manufacturer": "Gyártó",
|
"manufacturer": "Gyártó",
|
||||||
"all": "Összes"
|
"all": "Összes",
|
||||||
|
"notifyNewArticles": "Értesítés új termékekről",
|
||||||
|
"notifyNewArticlesBrowserUnsupported": "A böngésző nem támogatja a push értesítéseket."
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,5 +3,7 @@ export default {
|
|||||||
"perPage": "per pagina",
|
"perPage": "per pagina",
|
||||||
"availability": "Disponibilità",
|
"availability": "Disponibilità",
|
||||||
"manufacturer": "Produttore",
|
"manufacturer": "Produttore",
|
||||||
"all": "Tutti"
|
"all": "Tutti",
|
||||||
|
"notifyNewArticles": "Avvisami sui nuovi articoli",
|
||||||
|
"notifyNewArticlesBrowserUnsupported": "Il tuo browser non supporta le notifiche push."
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,5 +3,7 @@ export default {
|
|||||||
"perPage": "na stronę",
|
"perPage": "na stronę",
|
||||||
"availability": "Dostępność",
|
"availability": "Dostępność",
|
||||||
"manufacturer": "Producent",
|
"manufacturer": "Producent",
|
||||||
"all": "Wszystkie"
|
"all": "Wszystkie",
|
||||||
|
"notifyNewArticles": "Powiadamiaj o nowych produktach",
|
||||||
|
"notifyNewArticlesBrowserUnsupported": "Twoja przeglądarka nie obsługuje powiadomień push."
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,5 +3,7 @@ export default {
|
|||||||
"perPage": "pe pagină",
|
"perPage": "pe pagină",
|
||||||
"availability": "Disponibilitate",
|
"availability": "Disponibilitate",
|
||||||
"manufacturer": "Producător",
|
"manufacturer": "Producător",
|
||||||
"all": "Toate"
|
"all": "Toate",
|
||||||
|
"notifyNewArticles": "Anunță-mă despre articole noi",
|
||||||
|
"notifyNewArticlesBrowserUnsupported": "Browserul nu acceptă notificări push."
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,5 +3,7 @@ export default {
|
|||||||
"perPage": "на странице",
|
"perPage": "на странице",
|
||||||
"availability": "Наличие",
|
"availability": "Наличие",
|
||||||
"manufacturer": "Производитель",
|
"manufacturer": "Производитель",
|
||||||
"all": "Все"
|
"all": "Все",
|
||||||
|
"notifyNewArticles": "Уведомлять о новых товарах",
|
||||||
|
"notifyNewArticlesBrowserUnsupported": "Ваш браузер не поддерживает push-уведомления."
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,5 +3,7 @@ export default {
|
|||||||
"perPage": "na stránku",
|
"perPage": "na stránku",
|
||||||
"availability": "Dostupnosť",
|
"availability": "Dostupnosť",
|
||||||
"manufacturer": "Výrobca",
|
"manufacturer": "Výrobca",
|
||||||
"all": "Všetko"
|
"all": "Všetko",
|
||||||
|
"notifyNewArticles": "Upozorni ma na nové produkty",
|
||||||
|
"notifyNewArticlesBrowserUnsupported": "Váš prehliadač nepodporuje push notifikácie."
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,5 +3,7 @@ export default {
|
|||||||
"perPage": "na stran",
|
"perPage": "na stran",
|
||||||
"availability": "Razpoložljivost",
|
"availability": "Razpoložljivost",
|
||||||
"manufacturer": "Proizvajalec",
|
"manufacturer": "Proizvajalec",
|
||||||
"all": "Vse"
|
"all": "Vse",
|
||||||
|
"notifyNewArticles": "Obvesti me o novih izdelkih",
|
||||||
|
"notifyNewArticlesBrowserUnsupported": "Vaš brskalnik ne podpira potisnih obvestil."
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,5 +3,7 @@ export default {
|
|||||||
"perPage": "për faqe",
|
"perPage": "për faqe",
|
||||||
"availability": "Disponueshmëria",
|
"availability": "Disponueshmëria",
|
||||||
"manufacturer": "Prodhuesi",
|
"manufacturer": "Prodhuesi",
|
||||||
"all": "Të gjitha"
|
"all": "Të gjitha",
|
||||||
|
"notifyNewArticles": "Njoftom për artikuj të rinj",
|
||||||
|
"notifyNewArticlesBrowserUnsupported": "Shfletuesi juaj nuk mbështet njoftimet push."
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,5 +3,7 @@ export default {
|
|||||||
"perPage": "po stranici",
|
"perPage": "po stranici",
|
||||||
"availability": "Dostupnost",
|
"availability": "Dostupnost",
|
||||||
"manufacturer": "Proizvođač",
|
"manufacturer": "Proizvođač",
|
||||||
"all": "Sve"
|
"all": "Sve",
|
||||||
|
"notifyNewArticles": "Obavesti me o novim artiklima",
|
||||||
|
"notifyNewArticlesBrowserUnsupported": "Vaš pregledač ne podržava push obaveštenja."
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,5 +3,7 @@ export default {
|
|||||||
"perPage": "per sida",
|
"perPage": "per sida",
|
||||||
"availability": "Tillgänglighet",
|
"availability": "Tillgänglighet",
|
||||||
"manufacturer": "Tillverkare",
|
"manufacturer": "Tillverkare",
|
||||||
"all": "Alla"
|
"all": "Alla",
|
||||||
|
"notifyNewArticles": "Meddela mig om nya artiklar",
|
||||||
|
"notifyNewArticlesBrowserUnsupported": "Din webbläsare stöder inte push-notiser."
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,5 +3,7 @@ export default {
|
|||||||
"perPage": "sayfa başına",
|
"perPage": "sayfa başına",
|
||||||
"availability": "Stok Durumu",
|
"availability": "Stok Durumu",
|
||||||
"manufacturer": "Üretici",
|
"manufacturer": "Üretici",
|
||||||
"all": "Tümü"
|
"all": "Tümü",
|
||||||
|
"notifyNewArticles": "Yeni ürünlerden haberdar et",
|
||||||
|
"notifyNewArticlesBrowserUnsupported": "Tarayıcınız anlık bildirimleri desteklemiyor."
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,5 +3,7 @@ export default {
|
|||||||
"perPage": "на сторінку",
|
"perPage": "на сторінку",
|
||||||
"availability": "Наявність",
|
"availability": "Наявність",
|
||||||
"manufacturer": "Виробник",
|
"manufacturer": "Виробник",
|
||||||
"all": "Усі"
|
"all": "Усі",
|
||||||
|
"notifyNewArticles": "Повідомляти про нові товари",
|
||||||
|
"notifyNewArticlesBrowserUnsupported": "Ваш браузер не підтримує push-сповіщення."
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,5 +3,7 @@ export default {
|
|||||||
"perPage": "每页",
|
"perPage": "每页",
|
||||||
"availability": "库存情况",
|
"availability": "库存情况",
|
||||||
"manufacturer": "制造商",
|
"manufacturer": "制造商",
|
||||||
"all": "全部"
|
"all": "全部",
|
||||||
|
"notifyNewArticles": "新商品上架时通知我",
|
||||||
|
"notifyNewArticlesBrowserUnsupported": "您的浏览器不支持推送通知。"
|
||||||
};
|
};
|
||||||
|
|||||||
57
src/utils/categoryPush.js
Normal file
57
src/utils/categoryPush.js
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
/**
|
||||||
|
* Web Push for category “new articles” notifications — same VAPID/SW as article/pickup push.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export {
|
||||||
|
fetchPushConfiguration,
|
||||||
|
registerPushServiceWorker,
|
||||||
|
ensurePushSubscription,
|
||||||
|
isPushApiSupported,
|
||||||
|
parseSubscribedStatus,
|
||||||
|
parseSuccess,
|
||||||
|
} from "./articlePush.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {number} kKategorie
|
||||||
|
* @param {string} endpoint
|
||||||
|
*/
|
||||||
|
export async function categoryPushStatus(kKategorie, endpoint) {
|
||||||
|
const res = await fetch("/api/category/push/status", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ kKategorie, endpoint }),
|
||||||
|
});
|
||||||
|
const data = await res.json().catch(() => ({}));
|
||||||
|
return { ...data, httpOk: res.ok };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {number} kKategorie
|
||||||
|
* @param {PushSubscription} subscription
|
||||||
|
*/
|
||||||
|
export async function categoryPushSubscribe(kKategorie, subscription) {
|
||||||
|
const subJson = subscription.toJSON();
|
||||||
|
const res = await fetch("/api/category/push/subscribe", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({
|
||||||
|
kKategorie,
|
||||||
|
subscription: subJson,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
const data = await res.json().catch(() => ({}));
|
||||||
|
return { ...data, httpOk: res.ok };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} endpoint
|
||||||
|
*/
|
||||||
|
export async function categoryPushUnsubscribe(endpoint) {
|
||||||
|
const res = await fetch("/api/category/push/unsubscribe", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ endpoint }),
|
||||||
|
});
|
||||||
|
const data = await res.json().catch(() => ({}));
|
||||||
|
return { ...data, httpOk: res.ok };
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user