diff --git a/src/components/ChatAssistant.js b/src/components/ChatAssistant.js
index 1edf3fa..05da261 100644
--- a/src/components/ChatAssistant.js
+++ b/src/components/ChatAssistant.js
@@ -58,17 +58,48 @@ class ChatAssistant extends Component {
return `${t('chat.privacyPromptBefore')}${t('chat.privacyPolicyLink')}${t('chat.privacyPromptAfter')}`;
};
+ /** 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() {
// Add socket listeners if socket is available and connected
this.addSocketListeners();
+ this.props.i18n?.on('languageChanged', this.handleI18nLanguageChanged);
+
const userStatus = isUserLoggedIn();
const isGuest = !userStatus.isLoggedIn;
if (isGuest && !this.state.privacyConfirmed) {
this.setState(prevState => {
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 = {
@@ -90,17 +121,7 @@ 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 };
- });
+ this.applyPrivacyPromptTranslation();
}
if (prevState.messages !== this.state.messages || prevState.isTyping !== this.state.isTyping) {
this.scrollToBottom();
@@ -108,6 +129,7 @@ class ChatAssistant extends Component {
}
componentWillUnmount() {
+ this.props.i18n?.off('languageChanged', this.handleI18nLanguageChanged);
this.removeSocketListeners();
this.stopRecording();
if (this.recordingTimer) {
diff --git a/src/components/Content.js b/src/components/Content.js
index ce10053..4d01c68 100644
--- a/src/components/Content.js
+++ b/src/components/Content.js
@@ -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);
}
diff --git a/src/components/ProductFilters.js b/src/components/ProductFilters.js
index 6578c71..632d42d 100644
--- a/src/components/ProductFilters.js
+++ b/src/components/ProductFilters.js
@@ -2,13 +2,32 @@ import React, { Component } from 'react';
import Box from '@mui/material/Box';
import Paper from '@mui/material/Paper';
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 { useParams, useSearchParams, useNavigate, useLocation } from 'react-router-dom';
import { setSessionSetting, removeSessionSetting, clearAllSessionSettings } from '../utils/sessionStorage.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);
+/** 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
const withRouter = (ClassComponent) => {
return (props) => {
@@ -39,6 +58,10 @@ class ProductFilters extends Component {
uniqueManufacturerArray,
attributeGroups,
manufacturerImages: new Map(), // id (number) → object URL
+ pushInteractive: false,
+ pushSubscribed: false,
+ pushBusy: false,
+ pushError: null,
};
this._manufacturerImageUrls = []; // track for cleanup
@@ -48,6 +71,7 @@ class ProductFilters extends Component {
this.adjustPaperHeight();
window.addEventListener('resize', this.adjustPaperHeight);
this._loadManufacturerImages();
+ this.refreshCategoryPushStatus();
}
componentWillUnmount() {
@@ -103,8 +127,135 @@ class ProductFilters extends Component {
const attributeGroups = this._getAttributeGroups(this.props.attributes);
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 = () => {
// Skip height adjustment on xs screens
@@ -201,6 +352,25 @@ class ProductFilters extends Component {
}
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 (
{this.props.dataType == 'category' && (
-
- {this.props.categoryName}
-
+
+
+ {this.props.categoryName}
+
+ {showCategoryPush && (
+
+
+
+
+ ) : pushSubscribed ? (
+
+ ) : (
+
+ )
+ }
+ 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'}
+
+
+
+ {pushError && (
+
+ {pushError}
+
+ )}
+
+ )}
+
)}
diff --git a/src/i18n/locales/ar/filters.js b/src/i18n/locales/ar/filters.js
index 8f3c713..632fb64 100644
--- a/src/i18n/locales/ar/filters.js
+++ b/src/i18n/locales/ar/filters.js
@@ -3,5 +3,7 @@ export default {
"perPage": "لكل صفحة",
"availability": "التوفر",
"manufacturer": "المصنّع",
- "all": "الكل"
+ "all": "الكل",
+ "notifyNewArticles": "إشعار عند توفر منتجات جديدة",
+ "notifyNewArticlesBrowserUnsupported": "المتصفح لا يدعم إشعارات الدفع."
};
diff --git a/src/i18n/locales/bg/filters.js b/src/i18n/locales/bg/filters.js
index 2ce28c4..19fe9ee 100644
--- a/src/i18n/locales/bg/filters.js
+++ b/src/i18n/locales/bg/filters.js
@@ -3,5 +3,7 @@ export default {
"perPage": "на страница",
"availability": "Наличност",
"manufacturer": "Производител",
- "all": "Всички"
+ "all": "Всички",
+ "notifyNewArticles": "Уведомявай ме за нови продукти",
+ "notifyNewArticlesBrowserUnsupported": "Вашият браузър не поддържа push известия."
};
diff --git a/src/i18n/locales/cs/filters.js b/src/i18n/locales/cs/filters.js
index 783f8c8..bd73e1d 100644
--- a/src/i18n/locales/cs/filters.js
+++ b/src/i18n/locales/cs/filters.js
@@ -3,5 +3,7 @@ export default {
"perPage": "na stránku",
"availability": "Dostupnost",
"manufacturer": "Výrobce",
- "all": "Vše"
+ "all": "Vše",
+ "notifyNewArticles": "Upozornit na nové produkty",
+ "notifyNewArticlesBrowserUnsupported": "Váš prohlížeč nepodporuje push oznámení."
};
diff --git a/src/i18n/locales/de/filters.js b/src/i18n/locales/de/filters.js
index 5b565b6..da1e88a 100644
--- a/src/i18n/locales/de/filters.js
+++ b/src/i18n/locales/de/filters.js
@@ -3,5 +3,7 @@ export default {
"perPage": "pro Seite",
"availability": "Verfügbarkeit",
"manufacturer": "Hersteller",
- "all": "Alle"
+ "all": "Alle",
+ "notifyNewArticles": "Bei neuen Artikeln benachrichtigen",
+ "notifyNewArticlesBrowserUnsupported": "Ihr Browser unterstützt keine Push-Benachrichtigungen."
};
\ No newline at end of file
diff --git a/src/i18n/locales/el/filters.js b/src/i18n/locales/el/filters.js
index 75b1ac0..d1f9c36 100644
--- a/src/i18n/locales/el/filters.js
+++ b/src/i18n/locales/el/filters.js
@@ -3,5 +3,7 @@ export default {
"perPage": "ανά σελίδα",
"availability": "Διαθεσιμότητα",
"manufacturer": "Κατασκευαστής",
- "all": "Όλα"
+ "all": "Όλα",
+ "notifyNewArticles": "Ειδοποίησέ με για νέα προϊόντα",
+ "notifyNewArticlesBrowserUnsupported": "Ο φυλλομετρητής σας δεν υποστηρίζει push ειδοποιήσεις."
};
diff --git a/src/i18n/locales/en/filters.js b/src/i18n/locales/en/filters.js
index c8a1c07..90f1ebe 100644
--- a/src/i18n/locales/en/filters.js
+++ b/src/i18n/locales/en/filters.js
@@ -3,5 +3,7 @@ export default {
"perPage": "per page", // pro Seite
"availability": "Availability", // Verfügbarkeit
"manufacturer": "Manufacturer", // Hersteller
- "all": "All" // Alle
+ "all": "All", // Alle
+ "notifyNewArticles": "Notify me about new articles",
+ "notifyNewArticlesBrowserUnsupported": "Your browser does not support push notifications."
};
diff --git a/src/i18n/locales/es/filters.js b/src/i18n/locales/es/filters.js
index bb96be8..9041fe5 100644
--- a/src/i18n/locales/es/filters.js
+++ b/src/i18n/locales/es/filters.js
@@ -3,5 +3,7 @@ export default {
"perPage": "por página",
"availability": "Disponibilidad",
"manufacturer": "Fabricante",
- "all": "Todos"
+ "all": "Todos",
+ "notifyNewArticles": "Notificarme sobre artículos nuevos",
+ "notifyNewArticlesBrowserUnsupported": "Su navegador no admite notificaciones push."
};
diff --git a/src/i18n/locales/fr/filters.js b/src/i18n/locales/fr/filters.js
index 40835ab..3d9758b 100644
--- a/src/i18n/locales/fr/filters.js
+++ b/src/i18n/locales/fr/filters.js
@@ -3,5 +3,7 @@ export default {
"perPage": "par page",
"availability": "Disponibilité",
"manufacturer": "Fabricant",
- "all": "Tous"
+ "all": "Tous",
+ "notifyNewArticles": "Être notifié des nouveaux articles",
+ "notifyNewArticlesBrowserUnsupported": "Votre navigateur ne prend pas en charge les notifications push."
};
diff --git a/src/i18n/locales/hr/filters.js b/src/i18n/locales/hr/filters.js
index 4023bd5..987ba9d 100644
--- a/src/i18n/locales/hr/filters.js
+++ b/src/i18n/locales/hr/filters.js
@@ -3,5 +3,7 @@ export default {
"perPage": "po stranici",
"availability": "Dostupnost",
"manufacturer": "Proizvođač",
- "all": "Sve"
+ "all": "Sve",
+ "notifyNewArticles": "Obavijesti me o novim artiklima",
+ "notifyNewArticlesBrowserUnsupported": "Vaš preglednik ne podržava push obavijesti."
};
diff --git a/src/i18n/locales/hu/filters.js b/src/i18n/locales/hu/filters.js
index 8180571..6dab116 100644
--- a/src/i18n/locales/hu/filters.js
+++ b/src/i18n/locales/hu/filters.js
@@ -3,5 +3,7 @@ export default {
"perPage": "oldalanként",
"availability": "Elérhetőség",
"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."
};
diff --git a/src/i18n/locales/it/filters.js b/src/i18n/locales/it/filters.js
index 7c8f097..ffdf282 100644
--- a/src/i18n/locales/it/filters.js
+++ b/src/i18n/locales/it/filters.js
@@ -3,5 +3,7 @@ export default {
"perPage": "per pagina",
"availability": "Disponibilità",
"manufacturer": "Produttore",
- "all": "Tutti"
+ "all": "Tutti",
+ "notifyNewArticles": "Avvisami sui nuovi articoli",
+ "notifyNewArticlesBrowserUnsupported": "Il tuo browser non supporta le notifiche push."
};
diff --git a/src/i18n/locales/pl/filters.js b/src/i18n/locales/pl/filters.js
index a915107..77db660 100644
--- a/src/i18n/locales/pl/filters.js
+++ b/src/i18n/locales/pl/filters.js
@@ -3,5 +3,7 @@ export default {
"perPage": "na stronę",
"availability": "Dostępność",
"manufacturer": "Producent",
- "all": "Wszystkie"
+ "all": "Wszystkie",
+ "notifyNewArticles": "Powiadamiaj o nowych produktach",
+ "notifyNewArticlesBrowserUnsupported": "Twoja przeglądarka nie obsługuje powiadomień push."
};
diff --git a/src/i18n/locales/ro/filters.js b/src/i18n/locales/ro/filters.js
index 32859f4..b6a9712 100644
--- a/src/i18n/locales/ro/filters.js
+++ b/src/i18n/locales/ro/filters.js
@@ -3,5 +3,7 @@ export default {
"perPage": "pe pagină",
"availability": "Disponibilitate",
"manufacturer": "Producător",
- "all": "Toate"
+ "all": "Toate",
+ "notifyNewArticles": "Anunță-mă despre articole noi",
+ "notifyNewArticlesBrowserUnsupported": "Browserul nu acceptă notificări push."
};
diff --git a/src/i18n/locales/ru/filters.js b/src/i18n/locales/ru/filters.js
index a2779ce..545db13 100644
--- a/src/i18n/locales/ru/filters.js
+++ b/src/i18n/locales/ru/filters.js
@@ -3,5 +3,7 @@ export default {
"perPage": "на странице",
"availability": "Наличие",
"manufacturer": "Производитель",
- "all": "Все"
+ "all": "Все",
+ "notifyNewArticles": "Уведомлять о новых товарах",
+ "notifyNewArticlesBrowserUnsupported": "Ваш браузер не поддерживает push-уведомления."
};
diff --git a/src/i18n/locales/sk/filters.js b/src/i18n/locales/sk/filters.js
index 944900c..db03f8b 100644
--- a/src/i18n/locales/sk/filters.js
+++ b/src/i18n/locales/sk/filters.js
@@ -3,5 +3,7 @@ export default {
"perPage": "na stránku",
"availability": "Dostupnosť",
"manufacturer": "Výrobca",
- "all": "Všetko"
+ "all": "Všetko",
+ "notifyNewArticles": "Upozorni ma na nové produkty",
+ "notifyNewArticlesBrowserUnsupported": "Váš prehliadač nepodporuje push notifikácie."
};
diff --git a/src/i18n/locales/sl/filters.js b/src/i18n/locales/sl/filters.js
index 4517d97..7715165 100644
--- a/src/i18n/locales/sl/filters.js
+++ b/src/i18n/locales/sl/filters.js
@@ -3,5 +3,7 @@ export default {
"perPage": "na stran",
"availability": "Razpoložljivost",
"manufacturer": "Proizvajalec",
- "all": "Vse"
+ "all": "Vse",
+ "notifyNewArticles": "Obvesti me o novih izdelkih",
+ "notifyNewArticlesBrowserUnsupported": "Vaš brskalnik ne podpira potisnih obvestil."
};
diff --git a/src/i18n/locales/sq/filters.js b/src/i18n/locales/sq/filters.js
index a499281..58a8dd6 100644
--- a/src/i18n/locales/sq/filters.js
+++ b/src/i18n/locales/sq/filters.js
@@ -3,5 +3,7 @@ export default {
"perPage": "për faqe",
"availability": "Disponueshmëria",
"manufacturer": "Prodhuesi",
- "all": "Të gjitha"
+ "all": "Të gjitha",
+ "notifyNewArticles": "Njoftom për artikuj të rinj",
+ "notifyNewArticlesBrowserUnsupported": "Shfletuesi juaj nuk mbështet njoftimet push."
};
diff --git a/src/i18n/locales/sr/filters.js b/src/i18n/locales/sr/filters.js
index 4023bd5..83e0908 100644
--- a/src/i18n/locales/sr/filters.js
+++ b/src/i18n/locales/sr/filters.js
@@ -3,5 +3,7 @@ export default {
"perPage": "po stranici",
"availability": "Dostupnost",
"manufacturer": "Proizvođač",
- "all": "Sve"
+ "all": "Sve",
+ "notifyNewArticles": "Obavesti me o novim artiklima",
+ "notifyNewArticlesBrowserUnsupported": "Vaš pregledač ne podržava push obaveštenja."
};
diff --git a/src/i18n/locales/sv/filters.js b/src/i18n/locales/sv/filters.js
index 7fb9e34..6ec78da 100644
--- a/src/i18n/locales/sv/filters.js
+++ b/src/i18n/locales/sv/filters.js
@@ -3,5 +3,7 @@ export default {
"perPage": "per sida",
"availability": "Tillgänglighet",
"manufacturer": "Tillverkare",
- "all": "Alla"
+ "all": "Alla",
+ "notifyNewArticles": "Meddela mig om nya artiklar",
+ "notifyNewArticlesBrowserUnsupported": "Din webbläsare stöder inte push-notiser."
};
diff --git a/src/i18n/locales/tr/filters.js b/src/i18n/locales/tr/filters.js
index f257513..dffeb68 100644
--- a/src/i18n/locales/tr/filters.js
+++ b/src/i18n/locales/tr/filters.js
@@ -3,5 +3,7 @@ export default {
"perPage": "sayfa başına",
"availability": "Stok Durumu",
"manufacturer": "Üretici",
- "all": "Tümü"
+ "all": "Tümü",
+ "notifyNewArticles": "Yeni ürünlerden haberdar et",
+ "notifyNewArticlesBrowserUnsupported": "Tarayıcınız anlık bildirimleri desteklemiyor."
};
diff --git a/src/i18n/locales/uk/filters.js b/src/i18n/locales/uk/filters.js
index 2e57a50..05562f6 100644
--- a/src/i18n/locales/uk/filters.js
+++ b/src/i18n/locales/uk/filters.js
@@ -3,5 +3,7 @@ export default {
"perPage": "на сторінку",
"availability": "Наявність",
"manufacturer": "Виробник",
- "all": "Усі"
+ "all": "Усі",
+ "notifyNewArticles": "Повідомляти про нові товари",
+ "notifyNewArticlesBrowserUnsupported": "Ваш браузер не підтримує push-сповіщення."
};
diff --git a/src/i18n/locales/zh/filters.js b/src/i18n/locales/zh/filters.js
index 1ac7559..2b5abe6 100644
--- a/src/i18n/locales/zh/filters.js
+++ b/src/i18n/locales/zh/filters.js
@@ -3,5 +3,7 @@ export default {
"perPage": "每页",
"availability": "库存情况",
"manufacturer": "制造商",
- "all": "全部"
+ "all": "全部",
+ "notifyNewArticles": "新商品上架时通知我",
+ "notifyNewArticlesBrowserUnsupported": "您的浏览器不支持推送通知。"
};
diff --git a/src/utils/categoryPush.js b/src/utils/categoryPush.js
new file mode 100644
index 0000000..05210b9
--- /dev/null
+++ b/src/utils/categoryPush.js
@@ -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 };
+}