A tonne of updates to how the system builds the view points in hopes of having a better mobile expereince
This commit is contained in:
@@ -3,8 +3,37 @@ let adminMap = null;
|
||||
let startMarker = null;
|
||||
let storedQRCodes = {};
|
||||
|
||||
// A function to set viewport dimensions for admin page
|
||||
function setAdminViewportDimensions() {
|
||||
const doc = document.documentElement;
|
||||
|
||||
// Set height and width
|
||||
doc.style.setProperty('--app-height', `${window.innerHeight}px`);
|
||||
doc.style.setProperty('--app-width', `${window.innerWidth}px`);
|
||||
|
||||
// Handle safe area insets for devices with notches or home indicators
|
||||
if (CSS.supports('padding: env(safe-area-inset-top)')) {
|
||||
doc.style.setProperty('--safe-area-top', 'env(safe-area-inset-top)');
|
||||
doc.style.setProperty('--safe-area-bottom', 'env(safe-area-inset-bottom)');
|
||||
doc.style.setProperty('--safe-area-left', 'env(safe-area-inset-left)');
|
||||
doc.style.setProperty('--safe-area-right', 'env(safe-area-inset-right)');
|
||||
} else {
|
||||
doc.style.setProperty('--safe-area-top', '0px');
|
||||
doc.style.setProperty('--safe-area-bottom', '0px');
|
||||
doc.style.setProperty('--safe-area-left', '0px');
|
||||
doc.style.setProperty('--safe-area-right', '0px');
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize when DOM is loaded
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// Set initial viewport dimensions and listen for resize events
|
||||
setAdminViewportDimensions();
|
||||
window.addEventListener('resize', setAdminViewportDimensions);
|
||||
window.addEventListener('orientationchange', () => {
|
||||
setTimeout(setAdminViewportDimensions, 100);
|
||||
});
|
||||
|
||||
checkAdminAuth();
|
||||
initializeAdminMap();
|
||||
loadCurrentStartLocation();
|
||||
@@ -1194,46 +1223,61 @@ async function loadUsers() {
|
||||
}
|
||||
|
||||
function displayUsers(users) {
|
||||
const tableBody = document.getElementById('users-table-body');
|
||||
const emptyEl = document.getElementById('users-empty');
|
||||
|
||||
if (!tableBody) return;
|
||||
|
||||
const container = document.querySelector('.users-list');
|
||||
if (!container) return;
|
||||
|
||||
if (!users || users.length === 0) {
|
||||
if (emptyEl) emptyEl.style.display = 'block';
|
||||
container.innerHTML = '<h3>Existing Users</h3><p class="empty-message">No users found.</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
if (emptyEl) emptyEl.style.display = 'none';
|
||||
|
||||
tableBody.innerHTML = users.map(user => {
|
||||
const createdDate = user.created_at || user['Created At'] || user.createdAt;
|
||||
const formattedDate = createdDate ? new Date(createdDate).toLocaleDateString() : 'N/A';
|
||||
const isAdmin = user.admin || user.Admin || false;
|
||||
const userId = user.Id || user.id || user.ID;
|
||||
|
||||
return `
|
||||
<tr>
|
||||
<td data-label="Email">${escapeHtml(user.email || user.Email || 'N/A')}</td>
|
||||
<td data-label="Name">${escapeHtml(user.name || user.Name || 'N/A')}</td>
|
||||
<td data-label="Role">
|
||||
<span class="user-role ${isAdmin ? 'admin' : 'user'}">
|
||||
${isAdmin ? 'Admin' : 'User'}
|
||||
</span>
|
||||
</td>
|
||||
<td data-label="Created">${formattedDate}</td>
|
||||
<td data-label="Actions">
|
||||
<div class="user-actions">
|
||||
<button class="btn btn-danger delete-user-btn" data-user-id="${userId}" data-user-email="${escapeHtml(user.email || user.Email)}">
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
}).join('');
|
||||
|
||||
// Setup event listeners for user actions
|
||||
|
||||
const tableHtml = `
|
||||
<h3>Existing Users</h3>
|
||||
<div class="users-table-wrapper">
|
||||
<table class="users-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Email</th>
|
||||
<th>Name</th>
|
||||
<th>Role</th>
|
||||
<th>Created</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
${users.map(user => {
|
||||
const createdDate = user.created_at || user['Created At'] || user.createdAt;
|
||||
const formattedDate = createdDate ? new Date(createdDate).toLocaleDateString() : 'N/A';
|
||||
const isAdmin = user.admin || user.Admin || false;
|
||||
const userId = user.Id || user.id || user.ID;
|
||||
|
||||
return `
|
||||
<tr>
|
||||
<td data-label="Email">${escapeHtml(user.email || user.Email || 'N/A')}</td>
|
||||
<td data-label="Name">${escapeHtml(user.name || user.Name || 'N/A')}</td>
|
||||
<td data-label="Role">
|
||||
<span class="user-role ${isAdmin ? 'admin' : 'user'}">
|
||||
${isAdmin ? 'Admin' : 'User'}
|
||||
</span>
|
||||
</td>
|
||||
<td data-label="Created">${formattedDate}</td>
|
||||
<td data-label="Actions">
|
||||
<div class="user-actions">
|
||||
<button class="btn btn-danger delete-user-btn" data-user-id="${userId}" data-user-email="${escapeHtml(user.email || user.Email)}">
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
}).join('')}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<p id="users-loading" class="loading-message" style="display: none;">Loading...</p>
|
||||
`;
|
||||
|
||||
container.innerHTML = tableHtml;
|
||||
setupUserActionListeners();
|
||||
}
|
||||
|
||||
|
||||
296
map/app/public/js/cache-manager.js
Normal file
296
map/app/public/js/cache-manager.js
Normal file
@@ -0,0 +1,296 @@
|
||||
/**
|
||||
* Client-side cache management utility
|
||||
* Handles cache busting and version checking for the application
|
||||
*/
|
||||
class ClientCacheManager {
|
||||
constructor() {
|
||||
this.currentVersion = null;
|
||||
this.versionCheckInterval = null;
|
||||
this.storageKey = 'app-version';
|
||||
this.init();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize cache manager
|
||||
*/
|
||||
init() {
|
||||
this.getCurrentVersion();
|
||||
this.startVersionChecking();
|
||||
this.setupBeforeUnload();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current app version from meta tag or API
|
||||
*/
|
||||
async getCurrentVersion() {
|
||||
try {
|
||||
// First try to get version from meta tag
|
||||
const metaVersion = document.querySelector('meta[name="app-version"]');
|
||||
if (metaVersion) {
|
||||
this.currentVersion = metaVersion.getAttribute('content');
|
||||
this.storeVersion(this.currentVersion);
|
||||
return this.currentVersion;
|
||||
}
|
||||
|
||||
// Fallback to API call
|
||||
const response = await fetch('/api/version');
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
this.currentVersion = data.version;
|
||||
this.storeVersion(this.currentVersion);
|
||||
return this.currentVersion;
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Could not retrieve app version:', error);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Store version in localStorage
|
||||
* @param {string} version - Version to store
|
||||
*/
|
||||
storeVersion(version) {
|
||||
try {
|
||||
localStorage.setItem(this.storageKey, version);
|
||||
} catch (error) {
|
||||
// Ignore localStorage errors
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get stored version from localStorage
|
||||
* @returns {string|null} Stored version
|
||||
*/
|
||||
getStoredVersion() {
|
||||
try {
|
||||
return localStorage.getItem(this.storageKey);
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if app version has changed
|
||||
* @returns {boolean} True if version changed
|
||||
*/
|
||||
async hasVersionChanged() {
|
||||
const storedVersion = this.getStoredVersion();
|
||||
const currentVersion = await this.getCurrentVersion();
|
||||
|
||||
return storedVersion && currentVersion && storedVersion !== currentVersion;
|
||||
}
|
||||
|
||||
/**
|
||||
* Force reload the page with cache busting
|
||||
*/
|
||||
forceReload() {
|
||||
// Clear cache-related storage
|
||||
try {
|
||||
localStorage.removeItem(this.storageKey);
|
||||
sessionStorage.clear();
|
||||
} catch (error) {
|
||||
// Ignore errors
|
||||
}
|
||||
|
||||
// Force reload with cache busting
|
||||
const url = new URL(window.location);
|
||||
url.searchParams.set('_cb', Date.now());
|
||||
window.location.replace(url.toString());
|
||||
}
|
||||
|
||||
/**
|
||||
* Start periodic version checking
|
||||
*/
|
||||
startVersionChecking() {
|
||||
// Check every 30 seconds
|
||||
this.versionCheckInterval = setInterval(async () => {
|
||||
try {
|
||||
if (await this.hasVersionChanged()) {
|
||||
this.handleVersionChange();
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Version check failed:', error);
|
||||
}
|
||||
}, 30000);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop version checking
|
||||
*/
|
||||
stopVersionChecking() {
|
||||
if (this.versionCheckInterval) {
|
||||
clearInterval(this.versionCheckInterval);
|
||||
this.versionCheckInterval = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle version change detection
|
||||
*/
|
||||
handleVersionChange() {
|
||||
// Show update notification
|
||||
this.showUpdateNotification();
|
||||
}
|
||||
|
||||
/**
|
||||
* Show update notification to user
|
||||
*/
|
||||
showUpdateNotification() {
|
||||
// Remove existing notification
|
||||
const existingNotification = document.querySelector('.update-notification');
|
||||
if (existingNotification) {
|
||||
existingNotification.remove();
|
||||
}
|
||||
|
||||
// Create notification element
|
||||
const notification = document.createElement('div');
|
||||
notification.className = 'update-notification';
|
||||
notification.innerHTML = `
|
||||
<div class="update-notification-content">
|
||||
<span class="update-message">🔄 A new version is available!</span>
|
||||
<button class="update-button" onclick="cacheManager.forceReload()">Reload Now</button>
|
||||
<button class="update-dismiss" onclick="this.closest('.update-notification').remove()">×</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Add styles
|
||||
notification.style.cssText = `
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
background: #4CAF50;
|
||||
color: white;
|
||||
padding: 15px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
|
||||
z-index: 10000;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
animation: slideIn 0.3s ease-out;
|
||||
`;
|
||||
|
||||
// Add animation styles
|
||||
const style = document.createElement('style');
|
||||
style.textContent = `
|
||||
@keyframes slideIn {
|
||||
from { transform: translateX(100%); opacity: 0; }
|
||||
to { transform: translateX(0); opacity: 1; }
|
||||
}
|
||||
.update-notification-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
.update-button {
|
||||
background: rgba(255,255,255,0.2);
|
||||
border: 1px solid rgba(255,255,255,0.3);
|
||||
color: white;
|
||||
padding: 5px 10px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
}
|
||||
.update-button:hover {
|
||||
background: rgba(255,255,255,0.3);
|
||||
}
|
||||
.update-dismiss {
|
||||
background: none;
|
||||
border: none;
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
font-size: 18px;
|
||||
padding: 0;
|
||||
margin-left: 5px;
|
||||
}
|
||||
.update-message {
|
||||
font-size: 14px;
|
||||
}
|
||||
`;
|
||||
|
||||
document.head.appendChild(style);
|
||||
document.body.appendChild(notification);
|
||||
|
||||
// Auto-dismiss after 10 seconds
|
||||
setTimeout(() => {
|
||||
if (notification && notification.parentNode) {
|
||||
notification.remove();
|
||||
}
|
||||
}, 10000);
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup beforeunload handler to check version on page refresh
|
||||
*/
|
||||
setupBeforeUnload() {
|
||||
window.addEventListener('beforeunload', async () => {
|
||||
// Quick version check before unload
|
||||
try {
|
||||
if (await this.hasVersionChanged()) {
|
||||
// Clear cached version to force fresh load
|
||||
this.storeVersion(null);
|
||||
}
|
||||
} catch (error) {
|
||||
// Ignore errors during unload
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Manual cache clear function
|
||||
*/
|
||||
clearCache() {
|
||||
try {
|
||||
// Clear localStorage
|
||||
localStorage.clear();
|
||||
// Clear sessionStorage
|
||||
sessionStorage.clear();
|
||||
|
||||
// Clear service worker cache if available
|
||||
if ('serviceWorker' in navigator && 'caches' in window) {
|
||||
caches.keys().then(names => {
|
||||
names.forEach(name => {
|
||||
caches.delete(name);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
console.log('Cache cleared successfully');
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Failed to clear cache:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get debug information
|
||||
* @returns {object} Debug info
|
||||
*/
|
||||
getDebugInfo() {
|
||||
return {
|
||||
currentVersion: this.currentVersion,
|
||||
storedVersion: this.getStoredVersion(),
|
||||
versionCheckActive: !!this.versionCheckInterval,
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize cache manager when DOM is ready
|
||||
let cacheManager;
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
cacheManager = new ClientCacheManager();
|
||||
});
|
||||
} else {
|
||||
cacheManager = new ClientCacheManager();
|
||||
}
|
||||
|
||||
// Make cache manager globally available for debugging
|
||||
window.cacheManager = cacheManager;
|
||||
|
||||
// Export for module systems
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = ClientCacheManager;
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
// Main application entry point
|
||||
import { CONFIG } from './config.js';
|
||||
import { hideLoading, showStatus } from './utils.js';
|
||||
import { hideLoading, showStatus, setViewportDimensions } from './utils.js';
|
||||
import { checkAuth } from './auth.js';
|
||||
import { initializeMap } from './map-manager.js';
|
||||
import { loadLocations } from './location-manager.js';
|
||||
@@ -13,6 +13,14 @@ let mkdocsSearch = null;
|
||||
|
||||
// Initialize the application
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
// Set initial viewport dimensions and listen for resize events
|
||||
setViewportDimensions();
|
||||
window.addEventListener('resize', setViewportDimensions);
|
||||
window.addEventListener('orientationchange', () => {
|
||||
// Add a small delay for orientation change to complete
|
||||
setTimeout(setViewportDimensions, 100);
|
||||
});
|
||||
|
||||
console.log('DOM loaded, initializing application...');
|
||||
|
||||
try {
|
||||
|
||||
@@ -4,8 +4,22 @@ let mySignups = [];
|
||||
let currentView = 'grid'; // 'grid' or 'calendar'
|
||||
let currentCalendarDate = new Date(); // For calendar navigation
|
||||
|
||||
// Function to set viewport dimensions for shifts page
|
||||
function setShiftsViewportDimensions() {
|
||||
const doc = document.documentElement;
|
||||
doc.style.setProperty('--app-height', `${window.innerHeight}px`);
|
||||
doc.style.setProperty('--app-width', `${window.innerWidth}px`);
|
||||
}
|
||||
|
||||
// Initialize when DOM is loaded
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
// Set initial viewport dimensions and listen for resize events
|
||||
setShiftsViewportDimensions();
|
||||
window.addEventListener('resize', setShiftsViewportDimensions);
|
||||
window.addEventListener('orientationchange', () => {
|
||||
setTimeout(setShiftsViewportDimensions, 100);
|
||||
});
|
||||
|
||||
await checkAuth();
|
||||
await loadShifts();
|
||||
await loadMySignups();
|
||||
|
||||
@@ -1,7 +1,21 @@
|
||||
// User profile JavaScript
|
||||
|
||||
// Function to set viewport dimensions for user page
|
||||
function setUserViewportDimensions() {
|
||||
const doc = document.documentElement;
|
||||
doc.style.setProperty('--app-height', `${window.innerHeight}px`);
|
||||
doc.style.setProperty('--app-width', `${window.innerWidth}px`);
|
||||
}
|
||||
|
||||
// Initialize when DOM is loaded
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// Set initial viewport dimensions and listen for resize events
|
||||
setUserViewportDimensions();
|
||||
window.addEventListener('resize', setUserViewportDimensions);
|
||||
window.addEventListener('orientationchange', () => {
|
||||
setTimeout(setUserViewportDimensions, 100);
|
||||
});
|
||||
|
||||
checkUserAuth();
|
||||
loadUserProfile();
|
||||
setupEventListeners();
|
||||
|
||||
@@ -65,3 +65,57 @@ export function updateLocationCount(count) {
|
||||
mobileCountElement.textContent = countText;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a CSS custom property `--app-height` to the window's inner height.
|
||||
* This helps create a reliable "full height" value across all browsers,
|
||||
* especially on mobile where `100vh` can be inconsistent.
|
||||
*/
|
||||
export function setAppHeight() {
|
||||
const doc = document.documentElement;
|
||||
doc.style.setProperty('--app-height', `${window.innerHeight}px`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets viewport dimensions and handles safe area insets for better mobile support
|
||||
* Also detects if we're on a scrollable page and adjusts accordingly
|
||||
*/
|
||||
export function setViewportDimensions() {
|
||||
const doc = document.documentElement;
|
||||
|
||||
// Set height
|
||||
doc.style.setProperty('--app-height', `${window.innerHeight}px`);
|
||||
|
||||
// Set width (useful for avoiding overflow issues)
|
||||
doc.style.setProperty('--app-width', `${window.innerWidth}px`);
|
||||
|
||||
// Handle safe area insets for devices with notches or home indicators
|
||||
if (CSS.supports('padding: env(safe-area-inset-top)')) {
|
||||
doc.style.setProperty('--safe-area-top', 'env(safe-area-inset-top)');
|
||||
doc.style.setProperty('--safe-area-bottom', 'env(safe-area-inset-bottom)');
|
||||
doc.style.setProperty('--safe-area-left', 'env(safe-area-inset-left)');
|
||||
doc.style.setProperty('--safe-area-right', 'env(safe-area-inset-right)');
|
||||
} else {
|
||||
doc.style.setProperty('--safe-area-top', '0px');
|
||||
doc.style.setProperty('--safe-area-bottom', '0px');
|
||||
doc.style.setProperty('--safe-area-left', '0px');
|
||||
doc.style.setProperty('--safe-area-right', '0px');
|
||||
}
|
||||
|
||||
// For pages that need scrolling (like shifts, user), don't restrict height
|
||||
const isScrollablePage = window.location.pathname.includes('shifts') ||
|
||||
window.location.pathname.includes('user') ||
|
||||
window.location.pathname.includes('admin');
|
||||
|
||||
if (isScrollablePage) {
|
||||
// Allow the body and app to grow beyond viewport height
|
||||
document.body.style.height = 'auto';
|
||||
document.body.style.minHeight = `${window.innerHeight}px`;
|
||||
|
||||
const app = document.getElementById('app');
|
||||
if (app) {
|
||||
app.style.height = 'auto';
|
||||
app.style.minHeight = `${window.innerHeight}px`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user