From de8e59f1bba8d8b651bfd6be6173b5a9f47ec769 Mon Sep 17 00:00:00 2001 From: sebseb7 Date: Thu, 26 Mar 2026 20:51:28 +0100 Subject: [PATCH] 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 --- src/components/ChatAssistant.js | 46 ++++-- src/components/Content.js | 23 +++ src/components/ProductFilters.js | 266 +++++++++++++++++++++++++++++-- src/i18n/locales/ar/filters.js | 4 +- src/i18n/locales/bg/filters.js | 4 +- src/i18n/locales/cs/filters.js | 4 +- src/i18n/locales/de/filters.js | 4 +- src/i18n/locales/el/filters.js | 4 +- src/i18n/locales/en/filters.js | 4 +- src/i18n/locales/es/filters.js | 4 +- src/i18n/locales/fr/filters.js | 4 +- src/i18n/locales/hr/filters.js | 4 +- src/i18n/locales/hu/filters.js | 4 +- src/i18n/locales/it/filters.js | 4 +- src/i18n/locales/pl/filters.js | 4 +- src/i18n/locales/ro/filters.js | 4 +- src/i18n/locales/ru/filters.js | 4 +- src/i18n/locales/sk/filters.js | 4 +- src/i18n/locales/sl/filters.js | 4 +- src/i18n/locales/sq/filters.js | 4 +- src/i18n/locales/sr/filters.js | 4 +- src/i18n/locales/sv/filters.js | 4 +- src/i18n/locales/tr/filters.js | 4 +- src/i18n/locales/uk/filters.js | 4 +- src/i18n/locales/zh/filters.js | 4 +- src/utils/categoryPush.js | 57 +++++++ 26 files changed, 435 insertions(+), 45 deletions(-) create mode 100644 src/utils/categoryPush.js 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 && ( + + + + + + + {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 }; +}