- Changed VAPID subject from public URL to mailto format - Updated docker-compose.yml to use mailto:admin@littleshop.local - Removed dependency on thebankofdebbie.giize.com public domain - All push notifications now work through VPN (admin.dark.side) only - Added update-push-internal.sh helper script for deployment - Improved security by keeping all admin traffic internal Push notifications will continue working normally through FCM, but all configuration and management stays on the internal network. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
221 lines
7.3 KiB
JavaScript
221 lines
7.3 KiB
JavaScript
// 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; |