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:
2025-07-24 12:42:27 -06:00
parent 8e133fb9b4
commit 37632d1dfc
17 changed files with 1253 additions and 98 deletions

View File

@@ -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();
}

View 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;
}

View File

@@ -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 {

View File

@@ -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();

View File

@@ -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();

View File

@@ -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`;
}
}
}