Add Pending Payment tab and rebrand to TeleShop Admin

- Added new 'Pending Payment' tab to show orders awaiting payment (4 orders)
- Rebranded admin panel from 'LittleShop Admin' to 'TeleShop Admin'
- Updated login page, layout, and dashboard with new branding
- Fixed visibility issue where PendingPayment orders had no tab
- All 13 orders are now visible across appropriate tabs

🤖 Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-09-24 15:55:15 +01:00
parent 10cd2ac7a1
commit 6e3c11ad75
248 changed files with 116667 additions and 7 deletions

View File

@@ -0,0 +1,537 @@
// 🤖 Holographic Tech Robot Effects System 🤖
// Advanced pixel-perfect holographic animations and tech aesthetics
class HolographicEffectsSystem {
constructor() {
this.isInitialized = false;
this.effectsActive = true;
this.dataStreams = [];
this.glitchElements = [];
this.init();
}
init() {
if (this.isInitialized) return;
console.log('🤖 Initializing Holographic Effects System...');
// Initialize effects when DOM is ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => this.setupEffects());
} else {
this.setupEffects();
}
this.isInitialized = true;
}
setupEffects() {
// this.createDataStreamBackground(); // Removed
// this.setupHolographicBorders(); // Removed
this.initializeGlitchEffects();
this.createParticleSystem();
this.setupTechScanlines();
this.initializeRobotAnimations();
// this.setupInteractiveHovers(); // Removed
}
// Create animated data streams in background
createDataStreamBackground() {
const dataStreamContainer = document.createElement('div');
dataStreamContainer.className = 'data-stream-container';
dataStreamContainer.style.cssText = `
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
pointer-events: none;
z-index: -1;
overflow: hidden;
`;
// Create multiple data streams
for (let i = 0; i < 8; i++) {
setTimeout(() => {
this.createDataStream(dataStreamContainer, i);
}, i * 500);
}
document.body.appendChild(dataStreamContainer);
}
createDataStream(container, index) {
const stream = document.createElement('div');
stream.className = 'data-stream';
const left = Math.random() * 100;
const animationDuration = 3 + Math.random() * 4;
const delay = Math.random() * 2;
stream.style.cssText = `
position: absolute;
left: ${left}%;
width: 2px;
height: 100px;
background: linear-gradient(to bottom,
transparent 0%,
rgba(0, 255, 255, 0.8) 20%,
rgba(138, 43, 226, 0.6) 80%,
transparent 100%);
animation: dataStreamFlow ${animationDuration}s linear infinite;
animation-delay: ${delay}s;
box-shadow: 0 0 10px rgba(0, 255, 255, 0.5);
`;
// Add random tech symbols
const symbols = ['01', '10', '11', '00', 'Ω', '∆', '∇', '◊', '◈'];
const symbol = document.createElement('span');
symbol.textContent = symbols[Math.floor(Math.random() * symbols.length)];
symbol.style.cssText = `
position: absolute;
top: 10px;
left: -5px;
color: rgba(0, 255, 255, 0.8);
font-size: 8px;
font-family: 'Courier New', monospace;
text-shadow: 0 0 5px rgba(0, 255, 255, 0.8);
`;
stream.appendChild(symbol);
container.appendChild(stream);
// Remove and recreate after animation
setTimeout(() => {
if (stream.parentNode) {
stream.parentNode.removeChild(stream);
}
if (this.effectsActive) {
this.createDataStream(container, index);
}
}, (animationDuration + delay) * 1000);
}
// Setup holographic borders for cards
setupHolographicBorders() {
const style = document.createElement('style');
style.textContent = `
@keyframes dataStreamFlow {
0% {
transform: translateY(-100px);
opacity: 0;
}
10% { opacity: 1; }
90% { opacity: 1; }
100% {
transform: translateY(calc(100vh + 100px));
opacity: 0;
}
}
@keyframes holographicBorderShift {
0% { border-image-source: linear-gradient(45deg, #8A2BE2, #9932CC, #DA70D6, #FF00FF); }
25% { border-image-source: linear-gradient(90deg, #00FFFF, #8A2BE2, #9932CC, #DA70D6); }
50% { border-image-source: linear-gradient(135deg, #FF00FF, #00FFFF, #8A2BE2, #9932CC); }
75% { border-image-source: linear-gradient(180deg, #DA70D6, #FF00FF, #00FFFF, #8A2BE2); }
100% { border-image-source: linear-gradient(45deg, #8A2BE2, #9932CC, #DA70D6, #FF00FF); }
}
`;
document.head.appendChild(style);
// Apply to cards
setTimeout(() => {
const cards = document.querySelectorAll('.rz-card, .card, .mobile-card');
cards.forEach(card => {
card.style.borderImage = 'linear-gradient(45deg, #8A2BE2, #9932CC, #DA70D6, #FF00FF) 1';
card.style.animation = 'holographicBorderShift 4s ease-in-out infinite';
});
}, 100);
}
// Initialize glitch effects on hover
initializeGlitchEffects() {
const glitchStyle = document.createElement('style');
glitchStyle.textContent = `
.tech-glitch {
position: relative;
}
.tech-glitch::before,
.tech-glitch::after {
content: attr(data-text);
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
opacity: 0;
transition: opacity 0.3s;
}
.tech-glitch::before {
color: #FF00FF;
animation: glitch1 0.5s infinite;
clip: rect(0, 900px, 0, 0);
}
.tech-glitch::after {
color: #00FFFF;
animation: glitch2 0.5s infinite;
clip: rect(0, 900px, 0, 0);
}
.tech-glitch:hover::before,
.tech-glitch:hover::after {
opacity: 0.8;
}
@keyframes glitch1 {
0% { clip: rect(42px, 9999px, 44px, 0); }
20% { clip: rect(12px, 9999px, 59px, 0); }
40% { clip: rect(63px, 9999px, 34px, 0); }
60% { clip: rect(18px, 9999px, 76px, 0); }
80% { clip: rect(54px, 9999px, 91px, 0); }
100% { clip: rect(25px, 9999px, 38px, 0); }
}
@keyframes glitch2 {
0% { clip: rect(65px, 9999px, 23px, 0); }
20% { clip: rect(87px, 9999px, 45px, 0); }
40% { clip: rect(29px, 9999px, 78px, 0); }
60% { clip: rect(52px, 9999px, 31px, 0); }
80% { clip: rect(15px, 9999px, 89px, 0); }
100% { clip: rect(73px, 9999px, 16px, 0); }
}
`;
document.head.appendChild(glitchStyle);
// Apply to headings
setTimeout(() => {
const headings = document.querySelectorAll('h1, h2, h3, h4, h5, h6');
headings.forEach(heading => {
heading.classList.add('tech-glitch');
heading.setAttribute('data-text', heading.textContent);
});
}, 200);
}
// Create particle system
createParticleSystem() {
const particleContainer = document.createElement('div');
particleContainer.className = 'particle-system';
particleContainer.style.cssText = `
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
pointer-events: none;
z-index: -2;
overflow: hidden;
`;
// Create floating particles
for (let i = 0; i < 20; i++) {
setTimeout(() => {
this.createParticle(particleContainer);
}, i * 100);
}
document.body.appendChild(particleContainer);
}
createParticle(container) {
const particle = document.createElement('div');
const size = Math.random() * 3 + 1;
const x = Math.random() * window.innerWidth;
const y = Math.random() * window.innerHeight;
const duration = Math.random() * 20 + 10;
particle.style.cssText = `
position: absolute;
width: ${size}px;
height: ${size}px;
background: radial-gradient(circle, rgba(138, 43, 226, 0.8) 0%, transparent 70%);
left: ${x}px;
top: ${y}px;
border-radius: 50%;
animation: particleFloat ${duration}s linear infinite;
box-shadow: 0 0 ${size * 3}px rgba(138, 43, 226, 0.6);
`;
const floatDistance = Math.random() * 100 + 50;
const angle = Math.random() * 360;
particle.style.setProperty('--float-x', `${Math.cos(angle * Math.PI / 180) * floatDistance}px`);
particle.style.setProperty('--float-y', `${Math.sin(angle * Math.PI / 180) * floatDistance}px`);
container.appendChild(particle);
// Remove after animation
setTimeout(() => {
if (particle.parentNode) {
particle.parentNode.removeChild(particle);
}
if (this.effectsActive) {
this.createParticle(container);
}
}, duration * 1000);
}
// Setup tech scanlines
setupTechScanlines() {
const scanlineStyle = document.createElement('style');
scanlineStyle.textContent = `
@keyframes particleFloat {
0%, 100% {
transform: translate(0, 0) scale(1);
opacity: 0.3;
}
50% {
transform: translate(var(--float-x), var(--float-y)) scale(1.5);
opacity: 0.8;
}
}
.tech-scanlines::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: repeating-linear-gradient(
0deg,
transparent,
transparent 2px,
rgba(0, 255, 255, 0.03) 2px,
rgba(0, 255, 255, 0.03) 4px
);
pointer-events: none;
animation: scanlineMove 2s linear infinite;
}
@keyframes scanlineMove {
0% { transform: translateY(0); }
100% { transform: translateY(4px); }
}
`;
document.head.appendChild(scanlineStyle);
// Apply to main containers
setTimeout(() => {
const containers = document.querySelectorAll('.container, .container-fluid, .main-content');
containers.forEach(container => {
container.classList.add('tech-scanlines');
container.style.position = 'relative';
});
}, 300);
}
// Initialize robot-style animations
initializeRobotAnimations() {
const robotStyle = document.createElement('style');
robotStyle.textContent = `
.robot-pulse {
animation: robotPulse 3s ease-in-out infinite;
}
.robot-scan {
position: relative;
overflow: hidden;
}
.robot-scan::after {
content: '';
position: absolute;
top: -50%;
left: -50%;
width: 200%;
height: 200%;
background: linear-gradient(45deg,
transparent 40%,
rgba(0, 255, 255, 0.1) 50%,
transparent 60%);
animation: robotScan 4s ease-in-out infinite;
}
@keyframes robotPulse {
0%, 100% {
transform: scale(1);
box-shadow: 0 0 20px rgba(138, 43, 226, 0.3);
}
50% {
transform: scale(1.02);
box-shadow: 0 0 40px rgba(138, 43, 226, 0.8);
}
}
@keyframes robotScan {
0% { transform: translate(-100%, -100%) rotate(0deg); }
100% { transform: translate(100%, 100%) rotate(360deg); }
}
.tech-loading {
position: relative;
}
.tech-loading::before {
content: '';
position: absolute;
top: -2px;
left: -2px;
right: -2px;
bottom: -2px;
background: linear-gradient(45deg, #8A2BE2, #00FFFF, #8A2BE2);
background-size: 200% 200%;
border-radius: inherit;
z-index: -1;
animation: techLoadingPulse 2s ease-in-out infinite;
}
@keyframes techLoadingPulse {
0%, 100% {
opacity: 0.3;
background-position: 0% 50%;
}
50% {
opacity: 0.8;
background-position: 100% 50%;
}
}
`;
document.head.appendChild(robotStyle);
// Apply to cards only (removed button glow effects)
setTimeout(() => {
// const buttons = document.querySelectorAll('.rz-button, .btn');
// buttons.forEach(button => {
// button.classList.add('robot-pulse');
// });
const cards = document.querySelectorAll('.rz-card, .card');
cards.forEach(card => {
card.classList.add('robot-scan');
});
}, 400);
}
// Setup interactive hover effects
setupInteractiveHovers() {
setTimeout(() => {
// Add hover effects to interactive elements
const interactiveElements = document.querySelectorAll('button, .rz-button, .btn, a, .card');
interactiveElements.forEach(element => {
element.addEventListener('mouseenter', (e) => {
this.createHoverEffect(e.target);
});
element.addEventListener('mouseleave', (e) => {
this.removeHoverEffect(e.target);
});
});
}, 500);
}
createHoverEffect(element) {
// Create ripple effect
const ripple = document.createElement('div');
ripple.style.cssText = `
position: absolute;
border-radius: 50%;
background: radial-gradient(circle, rgba(0, 255, 255, 0.6) 0%, transparent 70%);
transform: scale(0);
animation: rippleEffect 0.6s linear;
pointer-events: none;
z-index: 1000;
`;
const rect = element.getBoundingClientRect();
const size = Math.max(rect.width, rect.height);
ripple.style.width = ripple.style.height = size + 'px';
ripple.style.left = (rect.left + rect.width / 2 - size / 2) + 'px';
ripple.style.top = (rect.top + rect.height / 2 - size / 2) + 'px';
document.body.appendChild(ripple);
// Remove after animation
setTimeout(() => {
if (ripple.parentNode) {
ripple.parentNode.removeChild(ripple);
}
}, 600);
// Add tech glow to element
element.style.transition = 'all 0.3s ease';
element.style.boxShadow = '0 0 30px rgba(0, 255, 255, 0.8), inset 0 0 20px rgba(138, 43, 226, 0.4)';
element.style.transform = 'scale(1.05)';
if (!document.getElementById('ripple-style')) {
const rippleStyle = document.createElement('style');
rippleStyle.id = 'ripple-style';
rippleStyle.textContent = `
@keyframes rippleEffect {
to {
transform: scale(4);
opacity: 0;
}
}
`;
document.head.appendChild(rippleStyle);
}
}
removeHoverEffect(element) {
element.style.boxShadow = '';
element.style.transform = '';
}
// Toggle effects on/off
toggleEffects() {
this.effectsActive = !this.effectsActive;
if (!this.effectsActive) {
// Remove all effect containers
const effectContainers = document.querySelectorAll('.data-stream-container, .particle-system');
effectContainers.forEach(container => {
if (container.parentNode) {
container.parentNode.removeChild(container);
}
});
} else {
// Restart effects
this.setupEffects();
}
console.log(`🤖 Holographic effects ${this.effectsActive ? 'enabled' : 'disabled'}`);
}
// Performance monitoring
checkPerformance() {
const start = performance.now();
setTimeout(() => {
const delta = performance.now() - start;
if (delta > 50) { // If frame time is too long
console.warn('🤖 Performance warning: Consider reducing effects');
}
}, 0);
}
}
// Initialize the system
const holographicSystem = new HolographicEffectsSystem();
// Expose control functions globally
window.toggleHolographicEffects = () => holographicSystem.toggleEffects();
window.holographicSystem = holographicSystem;
// Add keyboard shortcut (Ctrl+H) to toggle effects
document.addEventListener('keydown', (e) => {
if (e.ctrlKey && e.key === 'h') {
e.preventDefault();
holographicSystem.toggleEffects();
}
});
console.log('🤖 Holographic Tech System loaded! Press Ctrl+H to toggle effects.');

View File

@@ -0,0 +1,221 @@
// Modern Mobile Enhancements
// Clean, simple mobile-friendly functionality
class ModernMobile {
constructor() {
this.init();
}
init() {
this.setupMobileTableLabels();
this.setupResponsiveNavigation();
this.setupFormEnhancements();
this.setupSmoothInteractions();
}
// Add data labels for mobile table stacking
setupMobileTableLabels() {
const tables = document.querySelectorAll('.table');
tables.forEach(table => {
const headers = Array.from(table.querySelectorAll('thead th')).map(th => th.textContent.trim());
const rows = table.querySelectorAll('tbody tr');
rows.forEach(row => {
const cells = row.querySelectorAll('td');
cells.forEach((cell, index) => {
if (headers[index]) {
cell.setAttribute('data-label', headers[index]);
}
});
});
});
}
// Enhanced mobile navigation
setupResponsiveNavigation() {
const navbar = document.querySelector('.navbar');
const toggler = document.querySelector('.navbar-toggler');
const collapse = document.querySelector('.navbar-collapse');
if (toggler && collapse) {
// Smooth collapse animation
toggler.addEventListener('click', () => {
collapse.classList.toggle('show');
});
// Close menu when clicking outside
document.addEventListener('click', (e) => {
if (!navbar.contains(e.target) && collapse.classList.contains('show')) {
collapse.classList.remove('show');
}
});
}
}
// Form enhancements for better mobile UX
setupFormEnhancements() {
// Auto-focus first input on desktop
if (window.innerWidth > 768) {
const firstInput = document.querySelector('.form-control:not([readonly]):not([disabled])');
if (firstInput) {
firstInput.focus();
}
}
// Enhanced form validation feedback
const forms = document.querySelectorAll('form');
forms.forEach(form => {
form.addEventListener('submit', (e) => {
// Skip validation for file upload forms
if (form.enctype === 'multipart/form-data') {
console.log('Mobile: Skipping validation for file upload form');
return;
}
const invalidInputs = form.querySelectorAll(':invalid');
if (invalidInputs.length > 0) {
e.preventDefault();
console.log('Mobile: Form validation failed, focusing on first invalid input');
invalidInputs[0].focus();
invalidInputs[0].scrollIntoView({ behavior: 'smooth', block: 'center' });
}
});
});
// Floating labels effect
const inputs = document.querySelectorAll('.form-control, .form-select');
inputs.forEach(input => {
if (input.value) {
input.parentElement.classList.add('has-value');
}
input.addEventListener('blur', () => {
if (input.value) {
input.parentElement.classList.add('has-value');
} else {
input.parentElement.classList.remove('has-value');
}
});
});
}
// Smooth interactions and feedback
setupSmoothInteractions() {
// Button click feedback
const buttons = document.querySelectorAll('.btn');
buttons.forEach(button => {
button.addEventListener('click', (e) => {
if (!button.disabled) {
button.style.transform = 'scale(0.95)';
setTimeout(() => {
button.style.transform = '';
}, 100);
}
});
});
// Card hover enhancement
const cards = document.querySelectorAll('.card');
cards.forEach(card => {
card.addEventListener('mouseenter', () => {
card.style.transform = 'translateY(-2px)';
});
card.addEventListener('mouseleave', () => {
card.style.transform = '';
});
});
// Smooth scroll for anchor links
const anchorLinks = document.querySelectorAll('a[href^="#"]');
anchorLinks.forEach(link => {
link.addEventListener('click', (e) => {
e.preventDefault();
const target = document.querySelector(link.getAttribute('href'));
if (target) {
target.scrollIntoView({ behavior: 'smooth' });
}
});
});
}
// Show toast notification
showToast(message, type = 'info') {
const toastContainer = document.getElementById('toast-container') || this.createToastContainer();
const toast = document.createElement('div');
toast.className = `alert alert-${type} alert-dismissible fade show`;
toast.style.cssText = `
margin-bottom: 0.5rem;
animation: slideInRight 0.3s ease;
`;
toast.innerHTML = `
${message}
<button type="button" class="btn-close" aria-label="Close"></button>
`;
toastContainer.appendChild(toast);
// Auto-remove after 4 seconds
setTimeout(() => {
toast.classList.add('fade');
setTimeout(() => {
if (toast.parentNode) {
toast.parentNode.removeChild(toast);
}
}, 150);
}, 4000);
// Manual close
const closeBtn = toast.querySelector('.btn-close');
closeBtn.addEventListener('click', () => {
toast.classList.add('fade');
setTimeout(() => {
if (toast.parentNode) {
toast.parentNode.removeChild(toast);
}
}, 150);
});
}
createToastContainer() {
const container = document.createElement('div');
container.id = 'toast-container';
container.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
z-index: 1050;
max-width: 300px;
`;
document.body.appendChild(container);
return container;
}
}
// CSS for toast animations
const style = document.createElement('style');
style.textContent = `
@keyframes slideInRight {
from {
transform: translateX(100%);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
.fade {
opacity: 0 !important;
transition: opacity 0.15s linear !important;
}
`;
document.head.appendChild(style);
// Initialize
const modernMobile = new ModernMobile();
window.modernMobile = modernMobile;

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,317 @@
// Enhanced notification management for LittleShop Admin
// Handles real-time order notifications and admin alerts
class AdminNotificationManager {
constructor() {
this.isSetupComplete = false;
this.notificationQueue = [];
this.init();
}
async init() {
console.log('Admin Notifications: Initializing...');
// Wait for PWA manager to be ready
if (window.pwaManager) {
await this.setupOrderNotifications();
} else {
// Wait for PWA manager to load
setTimeout(() => this.init(), 1000);
}
}
async setupOrderNotifications() {
try {
// Ensure push notifications are enabled
if (!window.pwaManager.pushSubscription) {
console.log('Admin Notifications: Setting up push notifications...');
// Show admin-specific notification prompt
this.showAdminNotificationPrompt();
return;
}
this.isSetupComplete = true;
this.addNotificationStatusIndicator();
this.setupTestNotificationButton();
console.log('Admin Notifications: Setup complete');
} catch (error) {
console.error('Admin Notifications: Setup failed:', error);
}
}
showAdminNotificationPrompt() {
// Check if prompt already exists
if (document.getElementById('admin-notification-prompt')) {
return;
}
const promptDiv = document.createElement('div');
promptDiv.id = 'admin-notification-prompt';
promptDiv.className = 'alert alert-warning alert-dismissible position-fixed';
promptDiv.style.cssText = `
top: 80px;
right: 20px;
z-index: 1055;
max-width: 400px;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
`;
promptDiv.innerHTML = `
<div class="d-flex align-items-center">
<i class="fas fa-bell-slash text-warning me-3 fa-2x"></i>
<div class="flex-grow-1">
<h6 class="alert-heading mb-1">Enable Order Notifications</h6>
<p class="mb-2">Get instant alerts for new orders, payments, and status changes.</p>
<div class="d-flex gap-2">
<button type="button" class="btn btn-warning btn-sm" id="enable-admin-notifications">
<i class="fas fa-bell me-1"></i>Enable Now
</button>
<button type="button" class="btn btn-outline-secondary btn-sm" id="remind-later">
Later
</button>
</div>
</div>
</div>
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
`;
document.body.appendChild(promptDiv);
// Add event listeners
document.getElementById('enable-admin-notifications').addEventListener('click', async () => {
try {
await this.enableNotifications();
promptDiv.remove();
} catch (error) {
console.error('Failed to enable notifications:', error);
this.showNotificationError('Failed to enable notifications. Please try again.');
}
});
document.getElementById('remind-later').addEventListener('click', () => {
promptDiv.remove();
// Set reminder for 1 hour
setTimeout(() => this.showAdminNotificationPrompt(), 60 * 60 * 1000);
});
}
async enableNotifications() {
const button = document.getElementById('enable-admin-notifications');
const originalText = button.innerHTML;
button.disabled = true;
button.innerHTML = '<i class="fas fa-spinner fa-spin me-1"></i>Enabling...';
try {
await window.pwaManager.subscribeToPushNotifications();
// Show success message
this.showNotificationSuccess('✅ Order notifications enabled successfully!');
// Complete setup
await this.setupOrderNotifications();
} finally {
button.disabled = false;
button.innerHTML = originalText;
}
}
addNotificationStatusIndicator() {
// Add status indicator to admin header/navbar
const navbar = document.querySelector('.navbar-nav');
if (!navbar || document.getElementById('notification-status')) {
return;
}
const statusItem = document.createElement('li');
statusItem.className = 'nav-item dropdown';
statusItem.innerHTML = `
<a class="nav-link dropdown-toggle" href="#" id="notification-status" role="button" data-bs-toggle="dropdown">
<i class="fas fa-bell text-success"></i>
<span class="d-none d-md-inline ms-1">Notifications</span>
<span id="notification-badge" class="badge bg-danger ms-1" style="display: none;">0</span>
</a>
<ul class="dropdown-menu dropdown-menu-end">
<li><h6 class="dropdown-header">Notification Settings</h6></li>
<li><a class="dropdown-item" href="#" id="test-notification">
<i class="fas fa-vial me-2"></i>Send Test Notification
</a></li>
<li><a class="dropdown-item" href="#" id="notification-history">
<i class="fas fa-history me-2"></i>Recent Notifications
</a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item text-danger" href="#" id="disable-notifications">
<i class="fas fa-bell-slash me-2"></i>Disable Notifications
</a></li>
</ul>
`;
navbar.appendChild(statusItem);
// Add event listeners
document.getElementById('test-notification').addEventListener('click', (e) => {
e.preventDefault();
this.sendTestNotification();
});
document.getElementById('disable-notifications').addEventListener('click', (e) => {
e.preventDefault();
this.disableNotifications();
});
}
setupTestNotificationButton() {
// Add test button to dashboard if we're on the dashboard page
const dashboardContent = document.querySelector('.dashboard-content, .admin-dashboard');
if (!dashboardContent) {
return;
}
const testButton = document.createElement('button');
testButton.className = 'btn btn-outline-primary btn-sm me-2';
testButton.innerHTML = '<i class="fas fa-bell me-1"></i>Test Notification';
testButton.onclick = () => this.sendTestNotification();
// Find a good place to add it (e.g., near page title)
const pageTitle = document.querySelector('h1, .page-title');
if (pageTitle) {
pageTitle.parentNode.insertBefore(testButton, pageTitle.nextSibling);
}
}
async sendTestNotification() {
try {
const response = await fetch('/api/push/test', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
title: '🧪 Test Notification',
body: 'LittleShop admin notifications are working perfectly!'
}),
credentials: 'same-origin'
});
if (response.ok) {
this.showNotificationSuccess('Test notification sent!');
} else {
throw new Error('Failed to send test notification');
}
} catch (error) {
console.error('Test notification failed:', error);
this.showNotificationError('Failed to send test notification');
}
}
async disableNotifications() {
if (confirm('Are you sure you want to disable order notifications?')) {
try {
await window.pwaManager.unsubscribeFromPushNotifications();
// Remove status indicator
const statusElement = document.getElementById('notification-status');
if (statusElement) {
statusElement.closest('.nav-item').remove();
}
this.showNotificationSuccess('Notifications disabled');
// Reset setup status
this.isSetupComplete = false;
} catch (error) {
console.error('Failed to disable notifications:', error);
this.showNotificationError('Failed to disable notifications');
}
}
}
showNotificationSuccess(message) {
this.showToast(message, 'success');
}
showNotificationError(message) {
this.showToast(message, 'danger');
}
showToast(message, type = 'info') {
const toast = document.createElement('div');
toast.className = `alert alert-${type} alert-dismissible position-fixed`;
toast.style.cssText = `
top: 20px;
right: 20px;
z-index: 1060;
min-width: 300px;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
`;
toast.innerHTML = `
${message}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
`;
document.body.appendChild(toast);
// Auto-remove after 5 seconds
setTimeout(() => {
if (toast.parentNode) {
toast.remove();
}
}, 5000);
}
// Handle incoming notifications (if using WebSocket/SignalR in future)
handleOrderNotification(data) {
if (!this.isSetupComplete) {
this.notificationQueue.push(data);
return;
}
// Update notification badge
this.updateNotificationBadge();
// Show browser notification if page is not visible
if (document.hidden && window.pwaManager) {
window.pwaManager.showNotification(data.title, {
body: data.body,
icon: '/icons/icon-192x192.png',
badge: '/icons/icon-72x72.png',
tag: 'order-notification',
requireInteraction: true,
actions: [
{ action: 'view', title: 'View Order' },
{ action: 'dismiss', title: 'Dismiss' }
]
});
}
}
updateNotificationBadge(count = null) {
const badge = document.getElementById('notification-badge');
if (!badge) return;
if (count === null) {
// Get current count and increment
const currentCount = parseInt(badge.textContent) || 0;
count = currentCount + 1;
}
if (count > 0) {
badge.textContent = count;
badge.style.display = 'inline';
} else {
badge.style.display = 'none';
}
}
}
// Initialize admin notification manager
document.addEventListener('DOMContentLoaded', () => {
window.adminNotificationManager = new AdminNotificationManager();
});
// Export for global access
window.AdminNotificationManager = AdminNotificationManager;

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,537 @@
// Progressive Web App functionality
// Handles service worker registration and PWA features
class PWAManager {
constructor() {
this.swRegistration = null;
this.vapidPublicKey = null;
this.pushSubscription = null;
this.init();
}
async init() {
console.log('PWA: Initializing PWA Manager...');
if ('serviceWorker' in navigator) {
try {
this.swRegistration = await navigator.serviceWorker.register('/sw.js');
console.log('SW: Service Worker registered successfully');
// Listen for updates
this.swRegistration.addEventListener('updatefound', () => {
console.log('SW: New version available');
this.showUpdateNotification();
});
} catch (error) {
console.log('SW: Service Worker registration failed:', error);
}
}
// Setup PWA install prompt
this.setupInstallPrompt();
// Setup notifications (if enabled)
this.setupNotifications();
// Setup push notifications
this.setupPushNotifications();
// Show manual install option after 5 seconds if no prompt appeared and app not installed
setTimeout(() => {
if (!document.getElementById('pwa-install-btn') && !this.isInstalled()) {
console.log('PWA: No install prompt appeared, showing manual install guide');
this.showManualInstallButton();
}
}, 5000);
}
setupInstallPrompt() {
let deferredPrompt;
window.addEventListener('beforeinstallprompt', (e) => {
console.log('PWA: beforeinstallprompt event fired');
// Prevent Chrome 67 and earlier from automatically showing the prompt
e.preventDefault();
deferredPrompt = e;
// Show custom install button
this.showInstallButton(deferredPrompt);
});
window.addEventListener('appinstalled', () => {
console.log('PWA: App was installed');
this.hideInstallButton();
});
// Debug: Check if app is already installed
if (this.isInstalled()) {
console.log('PWA: App is already installed (standalone mode)');
// Hide any existing install buttons
this.hideInstallButton();
} else {
console.log('PWA: App is not installed, waiting for install prompt...');
console.log('PWA: Current URL:', window.location.href);
console.log('PWA: Display mode:', window.matchMedia('(display-mode: standalone)').matches ? 'standalone' : 'browser');
console.log('PWA: User agent:', navigator.userAgent);
}
// Periodically check if app becomes installed (for cases where user installs via browser menu)
setInterval(() => {
if (this.isInstalled()) {
this.hideInstallButton();
}
}, 2000);
}
showInstallButton(deferredPrompt) {
// Don't show install button if app is already installed
if (this.isInstalled()) {
console.log('PWA: App already installed, skipping install button');
return;
}
// Create install button
const installBtn = document.createElement('button');
installBtn.id = 'pwa-install-btn';
installBtn.className = 'btn btn-primary btn-sm';
installBtn.innerHTML = '<i class="fas fa-download"></i> Install App';
installBtn.style.cssText = `
position: fixed;
bottom: 20px;
right: 20px;
z-index: 1000;
box-shadow: 0 4px 12px rgba(37, 99, 235, 0.3);
`;
installBtn.addEventListener('click', async () => {
if (deferredPrompt) {
deferredPrompt.prompt();
const { outcome } = await deferredPrompt.userChoice;
console.log('PWA: User response to install prompt:', outcome);
deferredPrompt = null;
this.hideInstallButton();
}
});
document.body.appendChild(installBtn);
}
hideInstallButton() {
const installBtn = document.getElementById('pwa-install-btn');
if (installBtn) {
installBtn.remove();
}
}
showUpdateNotification() {
// Show update available notification
const notification = document.createElement('div');
notification.className = 'alert alert-info alert-dismissible';
notification.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
z-index: 1050;
max-width: 300px;
`;
notification.innerHTML = `
<strong>Update Available!</strong><br>
A new version of the app is ready.
<button type="button" class="btn btn-sm btn-outline-info ms-2" id="update-btn">
Update Now
</button>
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
`;
document.body.appendChild(notification);
// Handle update
document.getElementById('update-btn').addEventListener('click', () => {
if (this.swRegistration && this.swRegistration.waiting) {
this.swRegistration.waiting.postMessage({ type: 'SKIP_WAITING' });
window.location.reload();
}
});
}
async setupNotifications() {
// Check if notifications are supported and get permission
if ('Notification' in window) {
const permission = await this.requestNotificationPermission();
console.log('Notifications permission:', permission);
}
}
async requestNotificationPermission() {
if (Notification.permission === 'default') {
// Only request permission when user interacts with a relevant feature
// For now, just return the current status
return Notification.permission;
}
return Notification.permission;
}
// Show notification (if permission granted)
showNotification(title, options = {}) {
if (Notification.permission === 'granted') {
const notification = new Notification(title, {
icon: '/icons/icon-192x192.png',
badge: '/icons/icon-72x72.png',
tag: 'littleshop-admin',
...options
});
// Auto-close after 5 seconds
setTimeout(() => {
notification.close();
}, 5000);
return notification;
}
}
// Show manual install button for browsers that don't auto-prompt
showManualInstallButton() {
// Don't show install button if app is already installed
if (this.isInstalled()) {
console.log('PWA: App already installed, skipping manual install button');
return;
}
console.log('PWA: Showing manual install button');
const installBtn = document.createElement('button');
installBtn.id = 'pwa-install-btn';
installBtn.className = 'btn btn-primary btn-sm';
installBtn.innerHTML = '<i class="fas fa-mobile-alt"></i> Install as App';
installBtn.style.cssText = `
position: fixed;
bottom: 20px;
right: 20px;
z-index: 1000;
box-shadow: 0 4px 12px rgba(37, 99, 235, 0.3);
`;
installBtn.addEventListener('click', () => {
const isChrome = navigator.userAgent.includes('Chrome');
const isEdge = navigator.userAgent.includes('Edge');
const isFirefox = navigator.userAgent.includes('Firefox');
let instructions = 'To install this app:\\n\\n';
if (isChrome || isEdge) {
instructions += '1. Look for the install icon (⬇️) in the address bar\\n';
instructions += '2. Or click the browser menu (⋮) → "Install LittleShop Admin"\\n';
instructions += '3. Or check if there\'s an "Install app" option in the browser menu';
} else if (isFirefox) {
instructions += '1. Firefox doesn\'t support PWA installation yet\\n';
instructions += '2. You can bookmark this page for easy access\\n';
instructions += '3. Or use Chrome/Edge for the full PWA experience';
} else {
instructions += '1. Look for an install or "Add to Home Screen" option\\n';
instructions += '2. Check your browser menu for app installation\\n';
instructions += '3. Or bookmark this page for quick access';
}
alert(instructions);
});
document.body.appendChild(installBtn);
}
// Check if app is installed
isInstalled() {
return window.matchMedia('(display-mode: standalone)').matches ||
window.navigator.standalone === true;
}
// Setup push notifications
async setupPushNotifications() {
if (!('serviceWorker' in navigator) || !('PushManager' in window)) {
console.log('PWA: Push notifications not supported');
return;
}
try {
// Get VAPID public key from server
await this.getVapidPublicKey();
// Check if user is already subscribed
await this.checkPushSubscription();
// Simple logic: only show prompt if user is not subscribed
if (!this.pushSubscription) {
// Check if we've already asked this session
if (!sessionStorage.getItem('pushNotificationPromptShown')) {
this.showPushNotificationSetup();
sessionStorage.setItem('pushNotificationPromptShown', 'true');
}
} else {
console.log('PWA: User already subscribed to push notifications');
}
} catch (error) {
console.error('PWA: Failed to setup push notifications:', error);
}
}
async getVapidPublicKey() {
try {
const response = await fetch('/api/push/vapid-key');
if (response.ok) {
const data = await response.json();
this.vapidPublicKey = data.publicKey;
console.log('PWA: VAPID public key retrieved');
} else {
throw new Error('Failed to get VAPID public key');
}
} catch (error) {
console.error('PWA: Error getting VAPID public key:', error);
throw error;
}
}
async checkPushSubscription() {
if (!this.swRegistration) {
return;
}
try {
this.pushSubscription = await this.swRegistration.pushManager.getSubscription();
if (this.pushSubscription) {
console.log('PWA: User has active push subscription');
} else {
console.log('PWA: User is not subscribed to push notifications');
}
} catch (error) {
console.error('PWA: Error checking push subscription:', error);
}
}
async subscribeToPushNotifications() {
if (!this.swRegistration || !this.vapidPublicKey) {
throw new Error('Service worker or VAPID key not available');
}
try {
// Check current permission status
if (Notification.permission === 'denied') {
throw new Error('Notification permission was denied. Please enable notifications in your browser settings.');
}
// Request notification permission if not already granted
let permission = Notification.permission;
if (permission === 'default') {
permission = await Notification.requestPermission();
}
if (permission !== 'granted') {
throw new Error('Notification permission is required for push notifications. Please allow notifications and try again.');
}
// Subscribe to push notifications
const subscription = await this.swRegistration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: this.urlBase64ToUint8Array(this.vapidPublicKey)
});
// Send subscription to server with timeout
console.log('PWA: Sending subscription to server...');
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 10000); // 10 second timeout
const response = await fetch('/api/push/subscribe', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
endpoint: subscription.endpoint,
p256dh: btoa(String.fromCharCode(...new Uint8Array(subscription.getKey('p256dh')))),
auth: btoa(String.fromCharCode(...new Uint8Array(subscription.getKey('auth'))))
}),
credentials: 'same-origin',
signal: controller.signal
});
clearTimeout(timeoutId);
console.log('PWA: Server response received:', response.status, response.statusText);
if (response.ok) {
this.pushSubscription = subscription;
console.log('PWA: Successfully subscribed to push notifications');
this.hidePushNotificationSetup();
return true;
} else {
throw new Error('Failed to save push subscription to server');
}
} catch (error) {
console.error('PWA: Failed to subscribe to push notifications:', error);
throw error;
}
}
async unsubscribeFromPushNotifications() {
if (!this.pushSubscription) {
return true;
}
try {
// Unsubscribe from push manager
await this.pushSubscription.unsubscribe();
// Notify server
await fetch('/api/push/unsubscribe', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
endpoint: this.pushSubscription.endpoint
}),
credentials: 'same-origin'
});
this.pushSubscription = null;
console.log('PWA: Successfully unsubscribed from push notifications');
return true;
} catch (error) {
console.error('PWA: Failed to unsubscribe from push notifications:', error);
throw error;
}
}
showPushNotificationSetup() {
// Check if setup UI already exists
if (document.getElementById('push-notification-setup')) {
return;
}
const setupDiv = document.createElement('div');
setupDiv.id = 'push-notification-setup';
setupDiv.className = 'alert alert-info alert-dismissible';
setupDiv.style.cssText = `
position: fixed;
top: 80px;
right: 20px;
z-index: 1050;
max-width: 350px;
`;
setupDiv.innerHTML = `
<div class="d-flex align-items-center">
<i class="fas fa-bell me-2"></i>
<div class="flex-grow-1">
<strong>Push Notifications</strong><br>
<small>Get notified of new orders and updates</small>
</div>
<div class="ms-2">
<button type="button" class="btn btn-sm btn-primary" id="subscribe-push-btn">Enable</button>
</div>
</div>
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
`;
document.body.appendChild(setupDiv);
// Add event listener for subscribe button
const subscribeBtn = document.getElementById('subscribe-push-btn');
if (subscribeBtn) {
subscribeBtn.addEventListener('click', async () => {
subscribeBtn.disabled = true;
subscribeBtn.innerHTML = '<i class="fas fa-spinner fa-spin me-1"></i>Subscribing...';
try {
// Add timeout to prevent infinite hanging
const subscriptionPromise = this.subscribeToPushNotifications();
const timeoutPromise = new Promise((_, reject) =>
setTimeout(() => reject(new Error('Push subscription timed out after 15 seconds. This may be due to network connectivity or browser push service issues.')), 15000)
);
await Promise.race([subscriptionPromise, timeoutPromise]);
this.showNotification('Push notifications enabled!', {
body: 'You will now receive notifications for new orders and updates.'
});
} catch (error) {
console.error('PWA: Push subscription failed:', error);
// Provide user-friendly error messages
let userMessage = error.message;
if (error.message.includes('permission')) {
userMessage = 'Please allow notifications when your browser asks, then try again.';
} else if (error.message.includes('timeout')) {
userMessage = 'Push notification setup timed out. This may be due to network or browser issues. Please try again or check your internet connection.';
}
alert('Failed to enable push notifications: ' + userMessage);
subscribeBtn.disabled = false;
subscribeBtn.innerHTML = 'Enable';
}
});
}
}
hidePushNotificationSetup() {
const setupDiv = document.getElementById('push-notification-setup');
if (setupDiv) {
setupDiv.remove();
console.log('PWA: Push notification setup hidden');
}
}
async sendTestNotification() {
try {
const response = await fetch('/api/push/test', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
title: 'Test Notification',
body: 'This is a test push notification from LittleShop Admin!'
}),
credentials: 'same-origin'
});
const result = await response.json();
if (response.ok) {
console.log('PWA: Test notification sent successfully');
return true;
} else {
throw new Error(result.error || 'Failed to send test notification');
}
} catch (error) {
console.error('PWA: Failed to send test notification:', error);
throw error;
}
}
// Helper function to convert VAPID key
urlBase64ToUint8Array(base64String) {
const padding = '='.repeat((4 - base64String.length % 4) % 4);
const base64 = (base64String + padding)
.replace(/\-/g, '+')
.replace(/_/g, '/');
const rawData = window.atob(base64);
const outputArray = new Uint8Array(rawData.length);
for (let i = 0; i < rawData.length; ++i) {
outputArray[i] = rawData.charCodeAt(i);
}
return outputArray;
}
}
// Initialize PWA Manager
const pwaManager = new PWAManager();
window.pwaManager = pwaManager;
// Expose notification functions globally
window.showNotification = (title, options) => pwaManager.showNotification(title, options);
window.sendTestPushNotification = () => pwaManager.sendTestNotification();
window.subscribeToPushNotifications = () => pwaManager.subscribeToPushNotifications();
window.unsubscribeFromPushNotifications = () => pwaManager.unsubscribeFromPushNotifications();

Binary file not shown.

Binary file not shown.