PageSourceSearch

https://www.stingerssportsrestaurant.com/assets/js/chat-notification.js

js stingerssportsrestaurant.com collected 2026-09-29 00:45:00 UTC 19,733 bytes, 594 lines download raw bytes

1/**
2 * Customer Chat Push Notification Manager
3 * Handles browser push notification subscription for customers
4 * Version: 1.0.0
5 * Date: 2026-01-19
6 */
7
8const ChatNotification = {
9    // State
10    status: 'not-supported', // not-supported, not-subscribed, subscribed, blocked
11    subscription: null,
12    vapidPublicKey: null, // Will be loaded from server
13    
14    /**
15     * Initialize notification manager
16     */
17    async init() {
18        // Check if user is logged in (only for logged-in users)
19        if (!this.isUserLoggedIn()) {
20            this.updateUI('not-supported');
21            return;
22        }
23        
24        // Check browser support
25        if (!this.isSupported()) {
26            this.updateUI('not-supported');
27            return;
28        }
29        
30        // Load VAPID key from server
31        await this.loadVapidKey();
32        
33        if (!this.vapidPublicKey) {
34            this.updateUI('not-supported');
35            return;
36        }
37        
38        // Register service worker if needed
39        await this.registerServiceWorker();
40        
41        // Check current permission
42        const permission = Notification.permission;
43        
44        if (permission === 'denied') {
45            this.updateUI('blocked');
46        } else if (permission === 'granted') {
47            // Check if already subscribed
48            const isSubscribed = await this.checkSubscriptionStatus();
49            this.updateUI(isSubscribed ? 'subscribed' : 'not-subscribed');
50        } else {
51            // Default: not subscribed yet
52            this.updateUI('not-subscribed');
53            this.showInfoBar();
54        }
55    },
56    
57    /**
58     * Check if user is logged in
59     */
60    isUserLoggedIn() {
61        const metaTag = document.querySelector('meta[name="user-logged-in"]');
62        return metaTag ? metaTag.getAttribute('content') === 'true' : false;
63    },
64    
65    /**
66     * Check if browser supports push notifications
67     */
68    isSupported() {
69        return (
70            'serviceWorker' in navigator &&
71            'PushManager' in window &&
72            'Notification' in window
73        );
74    },
75    
76    /**
77     * Load VAPID public key from server
78     */
79    async loadVapidKey() {
80        try {
81            const response = await fetch('/api/chat/notifications/vapid_key', {
82                method: 'GET',
83                headers: {
84                    'Accept': 'application/json'
85                }
86            });
87            
88            if (!response.ok) {
89                return false;
90            }
91            
92            const data = await response.json();
93            if (data.success && data.data?.public_key) {
94                this.vapidPublicKey = data.data.public_key;
95                return true;
96            }
97            
98            return false;
99            
100        } catch (error) {
101            console.error('[ChatNotification] Error loading VAPID key:', error);
102            return false;
103        }
104    },
105    
106    /**
107     * Register service worker
108     */
109    async registerServiceWorker() {
110        if (!('serviceWorker' in navigator)) return null;
111        
112        try {
113            const registration = await navigator.serviceWorker.register('/sw-push.js', {
114                scope: '/'
115            });
116            return registration;
117        } catch (error) {
118            return null;
119        }
120    },
121    
122    /**
123     * Check subscription status from server
124     */
125    async checkSubscriptionStatus() {
126        try {
127            const response = await fetch('/api/chat/notifications/status', {
128                method: 'GET',
129                credentials: 'include',
130                headers: {
131                    'Accept': 'application/json'
132                }
133            });
134            
135            if (!response.ok) {
136                return false;
137            }
138            
139            const data = await response.json();
140            return data.data?.subscribed || false;
141            
142        } catch (error) {
143            console.error('[ChatNotification] Error checking subscription:', error);
144            return false;
145        }
146    },
147    
148    /**
149     * Update UI based on status
150     */
151    updateUI(status) {
152        this.status = status;
153        const btn = document.getElementById('chatNotifToggle');
154        
155        if (!btn) {
156            // Retry after a short delay (button might not be in DOM yet)
157            setTimeout(() => this.updateUI(status), 100);
158            return;
159        }
160        
161        btn.setAttribute('data-status', status);
162        
163        // Show/hide button based on support
164        if (status === 'not-supported') {
165            btn.style.display = 'none';
166        } else {
167            btn.style.display = 'block';
168        }
169        
170        // Update SVG icon (bell vs bell-slash)
171        const svg = btn.querySelector('svg');
172        if (svg) {
173            if (status === 'subscribed') {
174                // Bell icon (active)
175                svg.innerHTML = '<path d="M12 22c1.1 0 2-.9 2-2h-4c0 1.1.89 2 2 2zm6-6v-5c0-3.07-1.64-5.64-4.5-6.32V4c0-.83-.67-1.5-1.5-1.5s-1.5.67-1.5 1.5v.68C7.63 5.36 6 7.92 6 11v5l-2 2v1h16v-1l-2-2z"/>';
176            } else {
177                // Bell slash icon (inactive/blocked)
178                svg.innerHTML = '<path d="M20 18.69L7.84 6.14 5.27 3.49 4 4.76l2.8 2.8v.01c-.52.99-.8 2.16-.8 3.42v5l-2 2v1h13.73l2 2L21 19.72l-1-1.03zM12 22c1.1 0 2-.9 2-2h-4c0 1.1.89 2 2 2zm6-7.32V11c0-3.08-1.64-5.64-4.5-6.32V4c0-.83-.67-1.5-1.5-1.5s-1.5.67-1.5 1.5v.68c-.15.03-.29.08-.42.12-.1.03-.2.07-.3.11h-.01c-.01 0-.01 0-.02.01-.23.09-.46.2-.68.31 0 0-.01 0-.01.01L18 14.68z"/>';
179            }
180        }
181        
182        // Update tooltip
183        const tooltips = {
184            'not-subscribed': 'Klik untuk aktifkan notifikasi',
185            'subscribed': 'Notifikasi aktif',
186            'blocked': 'Notifikasi diblokir'
187        };
188        
189        if (tooltips[status]) {
190            btn.title = tooltips[status];
191        }
192        
193        // Hide info bar if subscribed or blocked
194        if (status === 'subscribed' || status === 'blocked') {
195            this.hideInfoBar();
196        }
197    },
198    
199    /**
200     * Show info bar (if not dismissed)
201     */
202    showInfoBar() {
203        if (localStorage.getItem('chat_notif_info_dismissed')) {
204            return;
205        }
206        
207        const infoBar = document.getElementById('chatNotifInfoBar');
208        const popupBody = document.getElementById('chat-popup')?.querySelector('.chat-popup-body');
209        
210        if (infoBar) {
211            infoBar.style.display = 'flex';
212        }
213        
214        if (popupBody) {
215            popupBody.classList.add('has-info-bar');
216        }
217    },
218    
219    /**
220     * Hide info bar
221     */
222    hideInfoBar() {
223        const infoBar = document.getElementById('chatNotifInfoBar');
224        const popupBody = document.getElementById('chat-popup')?.querySelector('.chat-popup-body');
225        
226        if (infoBar) {
227            infoBar.style.display = 'none';
228        }
229        
230        if (popupBody) {
231            popupBody.classList.remove('has-info-bar');
232        }
233    },
234    
235    /**
236     * Dismiss info bar permanently
237     */
238    dismissInfo() {
239        this.hideInfoBar();
240        localStorage.setItem('chat_notif_info_dismissed', '1');
241    },
242    
243    /**
244     * Handle bell icon click
245     */
246    async handleToggleClick() {
247        if (this.status === 'not-subscribed') {
248            await this.subscribe();
249        } else if (this.status === 'subscribed') {
250            await this.unsubscribe();
251        } else if (this.status === 'blocked') {
252            this.showUnblockHelp();
253        }
254    },
255    
256    /**
257     * Subscribe to push notifications
258     */
259    async subscribe() {
260        try {
261            // Request permission
262            const permission = await Notification.requestPermission();
263            
264            if (permission === 'denied') {
265                this.updateUI('blocked');
266                this.showToast('Notifikasi diblokir', 'error');
267                return;
268            }
269            
270            if (permission !== 'granted') {
271                return;
272            }
273            
274            // Get service worker registration
275            const registration = await navigator.serviceWorker.ready;
276            
277            // Subscribe to push
278            const subscription = await registration.pushManager.subscribe({
279                userVisibleOnly: true,
280                applicationServerKey: this.urlBase64ToUint8Array(this.vapidPublicKey)
281            });
282            
283            // Send subscription to server
284            const response = await fetch('/api/chat/notifications/subscribe', {
285                method: 'POST',
286                credentials: 'include',
287                headers: {
288                    'Content-Type': 'application/json',
289                    'Accept': 'application/json'
290                },
291                body: JSON.stringify(subscription.toJSON())
292            });
293            
294            if (!response.ok) {
295                throw new Error('Failed to save subscription');
296            }
297            
298            const result = await response.json();
299            
300            this.subscription = subscription;
301            this.updateUI('subscribed');
302            this.hideInfoBar();
303            this.showToast('Notifikasi berhasil diaktifkan!', 'success');
304            
305            // Send test notification
306            setTimeout(() => this.sendTestNotification(), 1000);
307            
308        } catch (error) {
309            console.error('[ChatNotification] Subscribe error:', error);
310            this.showToast('Gagal mengaktifkan notifikasi', 'error');
311        }
312    },
313    
314    /**
315     * Unsubscribe from push notifications
316     */
317    async unsubscribe() {
318        try {
319            // Confirm first
320            if (!confirm('Nonaktifkan notifikasi chat?')) {
321                return;
322            }
323            
324            // Get current subscription
325            const registration = await navigator.serviceWorker.ready;
326            const subscription = await registration.pushManager.getSubscription();
327            
328            if (subscription) {
329                // Unsubscribe from browser
330                await subscription.unsubscribe();
331                
332                // Tell server
333                await fetch('/api/chat/notifications/unsubscribe', {
334                    method: 'POST',
335                    credentials: 'include',
336                    headers: {
337                        'Content-Type': 'application/json',
338                        'Accept': 'application/json'
339                    },
340                    body: JSON.stringify(subscription.toJSON())
341                });
342            }
343            
344            this.subscription = null;
345            this.updateUI('not-subscribed');
346            this.showInfoBar();
347            this.showToast('Notifikasi dinonaktifkan', 'info');
348            
349        } catch (error) {
350            console.error('[ChatNotification] Unsubscribe error:', error);
351            this.showToast('Gagal menonaktifkan notifikasi', 'error');
352        }
353    },
354    
355    /**
356     * Send test notification
357     */
358    async sendTestNotification() {
359        try {
360            const response = await fetch('/api/chat/notifications/test', {
361                method: 'POST',
362                credentials: 'include',
363                headers: {
364                    'Accept': 'application/json'
365                }
366            });
367            
368            if (response.ok) {
369                // Test notification sent successfully
370            }
371        } catch (error) {
372            // Test notification failed silently
373        }
374    },
375    
376    /**
377     * Show unblock help modal/message
378     */
379    showUnblockHelp() {
380        const message = `
381            Notifikasi diblokir oleh browser.\n\n
382            Untuk mengaktifkan:\n
383            1. Klik ikon gembok/info di address bar\n
384            2. Cari "Notifications" atau "Notifikasi"\n
385            3. Ubah menjadi "Allow" atau "Izinkan"\n
386            4. Refresh halaman ini
387        `.trim().replace(/\s+/g, ' ');
388        
389        alert(message);
390    },
391    
392    /**
393     * Show toast notification
394     */
395    showToast(message, type = 'info') {
396        // Simple toast implementation
397        const toast = document.createElement('div');
398        toast.className = `chat-toast chat-toast-${type}`;
399        toast.textContent = message;
400        toast.style.cssText = `
401            position: fixed;
402            bottom: 80px;
403            right: 20px;
404            background: ${type === 'success' ? '#4caf50' : type === 'error' ? '#f44336' : '#2196f3'};
405            color: white;
406            padding: 12px 20px;
407            border-radius: 4px;
408            box-shadow: 0 2px 8px rgba(0,0,0,0.2);
409            z-index: 10000;
410            animation: slideInUp 0.3s ease;
411        `;
412        
413        document.body.appendChild(toast);
414        
415        setTimeout(() => {
416            toast.style.animation = 'slideOutDown 0.3s ease';
417            setTimeout(() => toast.remove(), 300);
418        }, 3000);
419    },
420    
421    /**
422     * Convert VAPID key to Uint8Array
423     */
424    urlBase64ToUint8Array(base64String) {
425        const padding = '='.repeat((4 - base64String.length % 4) % 4);
426        const base64 = (base64String + padding)
427            .replace(/\-/g, '+')
428            .replace(/_/g, '/');
429        
430        const rawData = window.atob(base64);
431        const outputArray = new Uint8Array(rawData.length);
432        
433        for (let i = 0; i < rawData.length; ++i) {
434            outputArray[i] = rawData.charCodeAt(i);
435        }
436        return outputArray;
437    },
438    
439    /**
440     * Check if push notification is currently active
441     * @returns {Promise<boolean>}
442     */
443    async isPushActive() {
444        try {
445            // Check browser subscription
446            if (!('serviceWorker' in navigator)) return false;
447            
448            const registration = await navigator.serviceWorker.getRegistration();
449            if (!registration) return false;
450            
451            const subscription = await registration.pushManager.getSubscription();
452            if (!subscription) return false;
453            
454            // Also check server status
455            const serverStatus = await this.checkSubscriptionStatus();
456            
457            return serverStatus;
458        } catch (error) {
459            return false;
460        }
461    },
462    
463    /**
464     * Show logout confirmation if push notification is active
465     * Call this before logout action
466     * @returns {Promise<boolean>} true if user confirms logout, false if cancelled
467     */
468    async confirmLogoutWithPushNotification() {
469        const isActive = await this.isPushActive();
470        
471        if (!isActive) {
472            // No active push, proceed with logout
473            return true;
474        }
475        
476        // Show modal confirmation using modal.css
477        return new Promise((resolve) => {
478            // Create modal HTML
479            const modalHTML = `
480                <div class="modal-overlay" id="logoutConfirmModal">
481                    <div class="modal-dialog">
482                        <div class="modal-header">
483                            <h3 class="modal-title">
484                                <span class="modal-icon warning">⚠️</span>
485                                Konfirmasi Logout
486                            </h3>
487                        </div>
488                        <div class="modal-body">
489                            <p class="modal-message">
490                                Anda memiliki <strong>notifikasi push yang aktif</strong>.<br><br>
491                                Setelah logout, Anda tidak akan menerima notifikasi chat lagi di perangkat ini.<br><br>
492                                Apakah Anda yakin ingin melanjutkan logout?
493                            </p>
494                        </div>
495                        <div class="modal-actions" style="padding: 0 20px 20px;">
496                            <button class="modal-btn modal-btn-secondary" id="logoutCancelBtn">
497                                Batal
498                            </button>
499                            <button class="modal-btn modal-btn-danger" id="logoutConfirmBtn">
500                                Lanjutkan Logout
501                            </button>
502                        </div>
503                    </div>
504                </div>
505            `;
506            
507            // Append to body
508            const modalContainer = document.createElement('div');
509            modalContainer.innerHTML = modalHTML;
510            document.body.appendChild(modalContainer.firstElementChild);
511            
512            const modal = document.getElementById('logoutConfirmModal');
513            const confirmBtn = document.getElementById('logoutConfirmBtn');
514            const cancelBtn = document.getElementById('logoutCancelBtn');
515            
516            // Show modal with animation
517            requestAnimationFrame(() => {
518                modal.classList.add('show');
519            });
520            
521            // Handle confirm
522            const handleConfirm = () => {
523                cleanup();
524                resolve(true);
525            };
526            
527            // Handle cancel
528            const handleCancel = () => {
529                cleanup();
530                resolve(false);
531            };
532            
533            // Cleanup function
534            const cleanup = () => {
535                modal.classList.remove('show');
536                setTimeout(() => {
537                    modal.remove();
538                }, 200);
539                confirmBtn.removeEventListener('click', handleConfirm);
540                cancelBtn.removeEventListener('click', handleCancel);
541                modal.removeEventListener('click', handleBackdropClick);
542            };
543            
544            // Handle backdrop click
545            const handleBackdropClick = (e) => {
546                if (e.target === modal) {
547                    handleCancel();
548                }
549            };
550            
551            // Add event listeners
552            confirmBtn.addEventListener('click', handleConfirm);
553            cancelBtn.addEventListener('click', handleCancel);
554            modal.addEventListener('click', handleBackdropClick);
555        });
556    }
557};
558
559// Add CSS animations
560const chatNotifStyle = document.createElement('style');
561chatNotifStyle.textContent = `
562    @keyframes slideInUp {
563        from {
564            transform: translateY(100%);
565            opacity: 0;
566        }
567        to {
568            transform: translateY(0);
569            opacity: 1;
570        }
571    }
572    
573    @keyframes slideOutDown {
574        from {
575            transform: translateY(0);
576            opacity: 1;
577        }
578        to {
579            transform: translateY(100%);
580            opacity: 0;
581        }
582    }
583`;
584document.head.appendChild(chatNotifStyle);
585
586// Auto-initialize when DOM is ready
587if (document.readyState === 'loading') {
588    document.addEventListener('DOMContentLoaded', () => ChatNotification.init());
589} else {
590    ChatNotification.init();
591}
592
593// Expose to window for access from other scripts
594window.ChatNotification = ChatNotification;

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.