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:
admin 2025-07-24 12:42:27 -06:00
parent 59ca2379f2
commit bb7032d649
17 changed files with 1253 additions and 98 deletions

View File

@ -2,7 +2,7 @@
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no">
<meta name="description" content="Admin Panel - BNKops Map - Interactive canvassing web-app & viewer">
<title>Admin Panel</title>
@ -272,59 +272,35 @@
<div class="users-admin-container">
<div class="user-form">
<h3>Create New User</h3>
<form id="user-form">
<form id="create-user-form">
<div class="form-group">
<label for="user-email">Email *</label>
<label for="user-email">Email</label>
<input type="email" id="user-email" required>
</div>
<div class="form-group">
<label for="user-password">Password *</label>
<input type="password" id="user-password" required minlength="6">
</div>
<div class="form-group">
<label for="user-name">Name</label>
<input type="text" id="user-name">
<input type="text" id="user-name" required>
</div>
<div class="form-group">
<label for="user-password">Password</label>
<input type="password" id="user-password" required>
</div>
<div class="form-group">
<label>
<input type="checkbox" id="user-admin"> Admin Access
<input type="checkbox" id="user-is-admin">
Is Admin
</label>
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary">Create User</button>
<button type="button" class="btn btn-secondary" id="clear-user-form">Clear</button>
<button type="button" id="clear-user-form" class="btn btn-secondary">Clear</button>
</div>
</form>
</div>
<div class="users-list">
<h3>Existing Users</h3>
<div id="users-table-container">
<table id="users-table" class="users-table">
<thead>
<tr>
<th>Email</th>
<th>Name</th>
<th>Admin</th>
<th>Created</th>
<th>Actions</th>
</tr>
</thead>
<tbody id="users-table-body">
<!-- Users will be loaded here -->
</tbody>
</table>
</div>
<div id="users-loading" class="loading-message" style="display: none;">
Loading users...
</div>
<div id="users-empty" class="empty-message" style="display: none;">
No users found.
</div>
<!-- User table will be dynamically inserted here -->
<p id="users-loading" class="loading-message">Loading users...</p>
</div>
</div>
</section>
@ -393,6 +369,9 @@
console.log('Local QR Code implementation loaded');
</script>
<!-- Cache Management -->
<script src="js/cache-manager.js"></script>
<!-- Admin JavaScript -->
<script src="js/admin.js"></script>
</body>

View File

@ -2,14 +2,22 @@
.admin-container {
display: flex;
height: calc(100vh - var(--header-height));
height: calc(var(--app-height) - var(--header-height));
background-color: #f5f5f5;
width: 100%;
max-width: 100vw;
overflow: hidden;
}
.admin-sidebar {
width: 250px;
min-width: 250px;
max-width: 250px;
background-color: white;
border-right: 1px solid #e0e0e0;
padding: 20px;
box-sizing: border-box;
flex-shrink: 0;
}
.admin-sidebar h2 {
@ -45,6 +53,12 @@
flex: 1;
padding: 30px;
overflow-y: auto;
overflow-x: hidden;
width: 100%;
min-width: 0; /* Allow flex item to shrink */
box-sizing: border-box;
max-height: calc(100vh - var(--header-height));
max-height: calc(var(--app-height) - var(--header-height));
}
.admin-section {
@ -52,6 +66,9 @@
border-radius: var(--border-radius);
padding: 30px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
width: 100%;
max-width: 100%;
box-sizing: border-box;
}
.admin-section h2 {
@ -619,6 +636,9 @@
grid-template-columns: 1fr 2fr;
gap: 30px;
margin-top: 20px;
width: 100%;
box-sizing: border-box;
overflow: hidden;
}
.user-form,
@ -628,6 +648,13 @@
padding: 25px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
border: 1px solid #e0e0e0;
width: 100%;
box-sizing: border-box;
}
.users-list {
overflow-x: auto; /* Allow horizontal scrolling if needed */
min-width: 0; /* Allow shrinking */
}
.user-form h3,
@ -644,6 +671,9 @@
border-collapse: collapse;
margin-top: 15px;
font-size: 14px;
table-layout: auto;
min-width: 600px; /* Ensure minimum width for readability on desktop */
box-sizing: border-box;
}
.users-table th,
@ -677,6 +707,10 @@
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
display: inline-block;
text-align: center;
min-width: 45px;
line-height: 1.2;
}
.user-role.admin {
@ -786,26 +820,108 @@
@media (max-width: 768px) {
.users-admin-container {
grid-template-columns: 1fr;
gap: 20px;
gap: 25px; /* Increased gap for better separation */
}
.user-form,
.users-list {
padding: 25px; /* Back to desktop padding for better comfort */
width: 100%;
box-sizing: border-box;
}
.users-list {
overflow-x: auto;
width: 100%;
box-sizing: border-box;
}
.users-table {
font-size: 12px;
font-size: 14px; /* Match desktop font size for better readability */
min-width: auto; /* Remove minimum width constraint on mobile */
width: 100%;
table-layout: auto; /* Changed from fixed to auto for better text flow */
border-collapse: collapse;
display: block;
}
.users-table th,
.users-table thead {
display: none;
}
.users-table tbody,
.users-table tr,
.users-table td {
padding: 8px;
display: block;
width: 100% !important;
box-sizing: border-box;
}
.users-table tr {
margin-bottom: 15px;
border: 1px solid #e0e0e0;
border-radius: var(--border-radius);
padding: 10px;
}
.users-table td {
padding: 8px 0;
border: none;
display: flex;
align-items: center;
word-wrap: break-word;
overflow-wrap: break-word;
}
.users-table td::before {
content: attr(data-label);
font-weight: bold;
width: 80px;
min-width: 80px;
margin-right: 10px;
color: #555;
}
/* Adjust column widths for mobile - less aggressive width constraints */
.users-table th:nth-child(1),
.users-table td:nth-child(1) { /* Email */
width: 35%; /* Increased back to give more space for email */
}
.users-table th:nth-child(2),
.users-table td:nth-child(2) { /* Name */
width: 25%;
}
.users-table th:nth-child(3),
.users-table td:nth-child(3) { /* Admin */
width: 15%; /* Reduced back to original for more space elsewhere */
}
.users-table th:nth-child(4),
.users-table td:nth-child(4) { /* Created */
width: 0%;
display: none; /* Hide created date on mobile to save space */
}
.users-table th:nth-child(5),
.users-table td:nth-child(5) { /* Actions */
width: 25%; /* Reduced back to give more balanced layout */
}
.user-actions {
flex-direction: column;
gap: 4px;
gap: 8px; /* Increased for better spacing */
width: 100%;
}
.user-actions .btn {
font-size: 11px;
padding: 4px 8px;
font-size: 12px; /* Increased to match more with desktop */
padding: 8px 10px; /* More comfortable padding */
width: 100%;
text-align: center;
min-height: 36px; /* Increased minimum height for better touch targets */
line-height: 1.3;
}
.user-form .form-actions {
@ -953,12 +1069,51 @@
@media (max-width: 480px) {
.users-table {
font-size: 11px;
font-size: 13px; /* Close to desktop, but slightly smaller for small screens */
min-width: auto;
}
.users-table th,
.users-table td {
padding: 10px 6px; /* Comfortable padding, not too cramped */
font-size: 13px; /* Match table font size */
line-height: 1.4;
}
/* Adjust column widths for very small screens */
.users-table th:nth-child(1),
.users-table td:nth-child(1) { /* Email */
width: 35%; /* Reduced from 40% */
word-break: break-all;
}
.users-table th:nth-child(2),
.users-table td:nth-child(2) { /* Name */
width: 25%; /* Reduced from 30% */
}
.users-table th:nth-child(3),
.users-table td:nth-child(3) { /* Admin */
width: 18%; /* Increased from 15% */
}
.users-table th:nth-child(5),
.users-table td:nth-child(5) { /* Actions */
width: 22%; /* Increased from 15% */
}
.user-role {
font-size: 10px;
padding: 2px 6px;
font-size: 10px; /* Increased from 9px for better readability */
padding: 4px 6px; /* More comfortable padding */
line-height: 1.2;
}
.user-actions .btn {
font-size: 11px; /* Increased from 9px */
padding: 6px 8px; /* More comfortable padding */
border-radius: 4px; /* Slightly larger border radius */
min-height: 32px; /* Comfortable touch target */
line-height: 1.3;
}
.user-form input[type="email"],
@ -1450,7 +1605,8 @@
/* Admin container becomes full width */
.admin-container {
flex-direction: column;
height: calc(100vh - 50px); /* Reduced header height */
height: calc(100vh - 50px); /* Fallback for older browsers */
height: calc(var(--app-height) - 50px); /* Reduced header height */
}
/* Sidebar as overlay */
@ -1459,7 +1615,8 @@
top: 0;
left: -280px;
width: 280px;
height: 100vh;
height: 100vh; /* Fallback for older browsers */
height: var(--app-height);
background: white;
z-index: 9999; /* Increased from 1000 */
transition: left 0.3s ease;
@ -1648,3 +1805,60 @@
font-size: 12px;
}
}
/* Medium screen adjustments for better content fitting */
@media (max-width: 1024px) and (min-width: 769px) {
.admin-sidebar {
width: 200px;
min-width: 200px;
max-width: 200px;
padding: 15px;
}
.admin-content {
padding: 20px;
}
.admin-section {
padding: 20px;
}
.users-admin-container {
grid-template-columns: 1fr;
gap: 20px;
}
.shifts-admin-container {
grid-template-columns: 1fr;
gap: 20px;
}
.user-form,
.users-list {
padding: 20px;
}
.users-table {
font-size: 13px;
}
.users-table th,
.users-table td {
padding: 10px 8px;
}
}
/* Small laptop adjustments */
@media (max-width: 1200px) and (min-width: 1025px) {
.admin-sidebar {
width: 220px;
min-width: 220px;
max-width: 220px;
}
.users-admin-container,
.shifts-admin-container {
grid-template-columns: 1fr 1.5fr;
gap: 25px;
}
}

View File

@ -10,18 +10,27 @@
position: sticky;
top: 0;
z-index: 100;
width: 100%;
max-width: 100vw;
box-sizing: border-box;
}
.header h1 {
margin: 0;
font-size: 1.5em;
font-weight: 600;
flex: 1;
min-width: 0;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.header-actions {
display: flex;
align-items: center;
gap: 20px;
flex-shrink: 0;
}
.user-info {
@ -74,6 +83,8 @@
max-width: 1200px;
margin: 0 auto;
padding: 20px;
width: 100%;
box-sizing: border-box;
}
/* My Signups section - now at the top */
@ -446,8 +457,18 @@
/* Mobile adjustments */
@media (max-width: 768px) {
/* Ensure proper scrolling on mobile */
body {
overflow-y: auto;
}
#app {
overflow-y: auto;
}
.shifts-container {
padding: 15px;
padding-bottom: 60px; /* Add extra bottom padding for mobile */
}
.shifts-grid {
@ -562,3 +583,16 @@
font-size: 0.75em;
}
}
/* Shifts Page Specific Overrides */
body {
height: auto;
min-height: 100vh;
min-height: var(--app-height);
}
#app {
height: auto;
min-height: 100vh;
min-height: var(--app-height);
}

View File

@ -10,6 +10,16 @@
--border-radius: 4px;
--transition: all 0.3s ease;
--header-height: 60px;
/* Responsive width variables */
--container-max-width: 100%;
--content-padding: 20px;
--mobile-padding: 15px;
/* Breakpoints for consistency */
--mobile-breakpoint: 768px;
--tablet-breakpoint: 1024px;
--desktop-breakpoint: 1200px;
}
/* Reset and base styles */
@ -25,14 +35,20 @@ body {
line-height: 1.5;
color: var(--dark-color);
background-color: var(--light-color);
width: 100%;
overflow-x: hidden; /* Prevent horizontal scrolling */
}
/* App container */
#app {
display: flex;
flex-direction: column;
height: 100vh;
height: 100vh; /* Fallback for older browsers */
height: var(--app-height);
width: 100%;
max-width: 100vw;
position: relative;
overflow: hidden; /* Prevent content from overflowing */
}
/* Header */
@ -43,10 +59,13 @@ body {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 20px;
padding: 0 var(--content-padding);
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
z-index: 10001; /* Increase from 1000 to be higher than map controls */
position: relative;
width: 100%;
min-width: 0; /* Allow flex items to shrink */
flex-shrink: 0; /* Don't shrink the header */
}
.header h1 {
@ -86,7 +105,11 @@ body {
#map-container {
position: relative;
width: 100%;
max-width: 100vw;
height: calc(100vh - var(--header-height));
height: calc(var(--app-height) - var(--header-height));
flex: 1;
overflow: hidden;
}
#map {
@ -97,6 +120,7 @@ body {
bottom: 0;
width: 100%;
height: 100%;
max-width: 100%;
background-color: #f0f0f0;
}
@ -1185,8 +1209,26 @@ body {
/* Hide desktop elements on mobile */
@media (max-width: 768px) {
/* Update root variables for mobile */
:root {
--content-padding: var(--mobile-padding);
--header-height: 50px; /* Smaller header on mobile */
}
.header {
padding: 0 var(--mobile-padding);
height: var(--header-height);
min-height: 50px;
flex-wrap: nowrap;
}
.header h1 {
font-size: 18px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
flex: 1;
min-width: 0;
}
.header-actions {
@ -1200,10 +1242,19 @@ body {
.mobile-dropdown {
display: block;
flex-shrink: 0;
}
.mobile-sidebar {
display: flex;
position: fixed;
right: 10px;
top: 50%;
transform: translateY(-50%);
width: auto;
max-width: 60px;
flex-direction: column;
box-sizing: border-box;
}
.map-controls {
@ -1219,7 +1270,9 @@ body {
/* Adjust modal for mobile */
.modal-content {
width: 95%;
max-width: calc(100vw - 20px);
margin: 10px;
box-sizing: border-box;
}
.form-row {
@ -1416,3 +1469,117 @@ path.leaflet-interactive {
min-height: 44px;
}
}
/* Cache Busting Update Notification Styles */
.update-notification {
position: fixed !important;
top: 20px !important;
right: 20px !important;
background: linear-gradient(135deg, #4CAF50, #45a049) !important;
color: white !important;
padding: 15px !important;
border-radius: 8px !important;
box-shadow: 0 4px 20px rgba(0,0,0,0.3) !important;
z-index: 10000 !important;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif !important;
animation: slideInFromRight 0.3s ease-out !important;
max-width: 350px !important;
border: 1px solid rgba(255,255,255,0.2) !important;
}
@keyframes slideInFromRight {
from {
transform: translateX(100%);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
.update-notification-content {
display: flex !important;
align-items: center !important;
gap: 10px !important;
flex-wrap: wrap !important;
}
.update-message {
font-size: 14px !important;
font-weight: 500 !important;
flex: 1 !important;
min-width: 150px !important;
}
.update-button {
background: rgba(255,255,255,0.2) !important;
border: 1px solid rgba(255,255,255,0.3) !important;
color: white !important;
padding: 8px 16px !important;
border-radius: 4px !important;
cursor: pointer !important;
font-size: 12px !important;
font-weight: 500 !important;
transition: background-color 0.2s ease !important;
white-space: nowrap !important;
}
.update-button:hover {
background: rgba(255,255,255,0.3) !important;
}
.update-dismiss {
background: none !important;
border: none !important;
color: white !important;
cursor: pointer !important;
font-size: 18px !important;
padding: 0 !important;
margin-left: 5px !important;
width: 24px !important;
height: 24px !important;
display: flex !important;
align-items: center !important;
justify-content: center !important;
border-radius: 50% !important;
transition: background-color 0.2s ease !important;
}
.update-dismiss:hover {
background: rgba(255,255,255,0.2) !important;
}
/* Mobile responsive styles for update notification */
@media (max-width: 768px) {
.update-notification {
top: 10px !important;
right: 10px !important;
left: 10px !important;
max-width: none !important;
padding: 12px !important;
}
.update-notification-content {
flex-direction: column !important;
align-items: stretch !important;
gap: 8px !important;
}
.update-message {
text-align: center !important;
min-width: auto !important;
}
.update-button {
align-self: center !important;
min-width: 120px !important;
}
.update-dismiss {
position: absolute !important;
top: 8px !important;
right: 8px !important;
margin: 0 !important;
}
}

View File

@ -4,6 +4,8 @@
max-width: 800px;
margin: 0 auto;
padding: 2rem;
width: 100%;
box-sizing: border-box;
}
.user-profile {
@ -12,6 +14,9 @@
padding: 2rem;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
border: 1px solid #e0e0e0;
width: 100%;
max-width: 100%;
box-sizing: border-box;
}
.user-profile h2 {
@ -65,6 +70,10 @@
padding: 1.5rem;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
border: 1px solid #e0e0e0;
width: 100%;
box-sizing: border-box;
overflow-x: auto;
min-width: 0;
}
.user-form h3,
@ -79,6 +88,8 @@
width: 100%;
border-collapse: collapse;
margin-top: 1rem;
table-layout: auto;
word-wrap: break-word;
}
.users-table th,
@ -86,6 +97,10 @@
padding: 0.75rem;
text-align: left;
border-bottom: 1px solid #e0e0e0;
vertical-align: top;
word-wrap: break-word;
overflow-wrap: break-word;
line-height: 1.4;
}
.users-table th {
@ -103,6 +118,10 @@
border-radius: 4px;
font-size: 0.8rem;
font-weight: 600;
display: inline-block;
text-align: center;
min-width: 50px;
line-height: 1.2;
}
.user-role.admin {
@ -118,11 +137,19 @@
.user-actions {
display: flex;
gap: 0.5rem;
align-items: center;
justify-content: center;
}
.user-actions .btn {
padding: 0.25rem 0.5rem;
font-size: 0.8rem;
min-height: 32px;
display: inline-flex;
align-items: center;
justify-content: center;
line-height: 1.2;
white-space: nowrap;
}
.btn-danger {
@ -176,16 +203,36 @@
@media (max-width: 768px) {
.users-admin-container {
grid-template-columns: 1fr;
gap: 1rem;
gap: 1.5rem;
width: 100%;
box-sizing: border-box;
}
.user-form,
.users-list {
width: 100%;
box-sizing: border-box;
padding: 1.25rem;
}
.users-table {
font-size: 0.9rem;
font-size: 0.85rem;
width: 100%;
table-layout: auto;
min-width: auto;
}
.users-table th,
.users-table td {
padding: 0.5rem;
padding: 0.6rem 0.4rem;
vertical-align: top;
word-wrap: break-word;
line-height: 1.3;
}
.users-table th {
font-size: 0.8rem;
padding: 0.7rem 0.4rem;
}
.user-container {
@ -266,21 +313,83 @@
}
@media (max-width: 480px) {
.users-admin-container {
gap: 1rem;
margin-top: 0.5rem;
}
.user-form,
.users-list {
padding: 1rem;
}
.users-table {
font-size: 0.8rem;
width: 100%;
table-layout: fixed;
min-width: auto;
}
.users-table th,
.users-table td {
padding: 0.5rem 0.3rem;
line-height: 1.4;
word-wrap: break-word;
overflow-wrap: break-word;
}
/* Optimize column widths for small screens */
.users-table th:nth-child(1),
.users-table td:nth-child(1) { /* Email */
width: 35%;
}
.users-table th:nth-child(2),
.users-table td:nth-child(2) { /* Name */
width: 25%;
}
.users-table th:nth-child(3),
.users-table td:nth-child(3) { /* Role */
width: 20%;
}
.users-table th:nth-child(4),
.users-table td:nth-child(4) { /* Actions */
width: 20%;
}
.user-role {
font-size: 0.65rem;
padding: 0.2rem 0.4rem;
border-radius: 3px;
display: inline-block;
line-height: 1.2;
}
.user-actions {
flex-direction: column;
gap: 0.25rem;
gap: 0.3rem;
width: 100%;
}
.user-actions .btn {
font-size: 0.7rem;
padding: 0.4rem 0.6rem;
width: 100%;
text-align: center;
min-height: 32px;
line-height: 1.2;
}
.user-container {
padding: 0.5rem;
padding: 0.75rem;
}
.user-form h3,
.users-list h3 {
font-size: 1.1rem;
margin-bottom: 0.8rem;
}
/* Very small screen adjustments */
@ -322,3 +431,16 @@
padding: 10px 16px;
}
}
/* User Page Specific Overrides */
body {
height: auto;
min-height: 100vh;
min-height: var(--app-height);
}
#app {
height: auto;
min-height: 100vh;
min-height: var(--app-height);
}

View File

@ -2,7 +2,7 @@
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no">
<meta name="description" content="Interactive canvassing web-app & viewer Changemaker-lite nocodb">
<title>Map by BNKops</title>
@ -377,6 +377,9 @@
integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo="
crossorigin=""></script>
<!-- Cache Management -->
<script src="js/cache-manager.js"></script>
<!-- Application JavaScript -->
<script type="module" src="js/main.js"></script>
</body>

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,19 +1223,29 @@ 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 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;
@ -1231,9 +1270,14 @@ function displayUsers(users) {
</td>
</tr>
`;
}).join('');
}).join('')}
</tbody>
</table>
</div>
<p id="users-loading" class="loading-message" style="display: none;">Loading...</p>
`;
// Setup event listeners for user actions
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`;
}
}
}

View File

@ -2,7 +2,7 @@
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no">
<meta name="description" content="Login to Map by BNKops - Interactive canvassing web-app & viewer">
<title>Login - Map by BNKops</title>
@ -263,5 +263,8 @@
})
.catch(console.error);
</script>
<!-- Cache Management -->
<script src="js/cache-manager.js"></script>
</body>
</html>

View File

@ -2,7 +2,7 @@
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no">
<title>Volunteer Shifts - BNKops Map</title>
<link rel="icon" type="image/x-icon" href="/favicon.ico">
@ -78,6 +78,9 @@
<div id="status-container" class="status-container"></div>
</div>
<!-- Cache Management -->
<script src="js/cache-manager.js"></script>
<script src="js/shifts.js"></script>
</body>
</html>

View File

@ -2,7 +2,7 @@
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no">
<meta name="description" content="User Profile - BNKops Map - Interactive canvassing web-app & viewer">
<title>User Profile</title>
@ -53,6 +53,9 @@
<div id="status-container" class="status-container"></div>
</div>
<!-- Cache Management -->
<script src="js/cache-manager.js"></script>
<!-- User JavaScript -->
<script src="js/user.js"></script>
</body>

View File

@ -11,6 +11,7 @@ const config = require('./config');
const logger = require('./utils/logger');
const { getCookieConfig } = require('./utils/helpers');
const { apiLimiter } = require('./middleware/rateLimiter');
const { cacheBusting } = require('./utils/cacheBusting');
// Initialize Express app
const app = express();
@ -95,9 +96,26 @@ app.use(cors({
// Body parser middleware
app.use(express.json({ limit: '10mb' }));
// Add cache busting middleware for HTML content
app.use(cacheBusting.htmlMiddleware());
// Add cache headers middleware for static files
app.use(cacheBusting.staticCacheMiddleware());
// Serve static files with proper cache headers
app.use(express.static('public'));
// Apply rate limiting to API routes
app.use('/api/', apiLimiter);
// Cache busting version endpoint
app.get('/api/version', (req, res) => {
res.json({
version: cacheBusting.getVersion(),
timestamp: new Date().toISOString()
});
});
// Proxy endpoint for MkDocs search
app.get('/api/docs-search', async (req, res) => {
try {

View File

@ -0,0 +1,179 @@
const crypto = require('crypto');
const fs = require('fs');
const path = require('path');
/**
* Cache busting utility for preventing browser caching of static assets
*/
class CacheBusting {
constructor() {
// Generate a unique version identifier on server start
this.version = this.generateVersion();
this.fileHashes = new Map();
}
/**
* Generate a version identifier based on server start time and random data
* @returns {string} Version string
*/
generateVersion() {
const timestamp = Date.now();
const random = crypto.randomBytes(4).toString('hex');
return `${timestamp}-${random}`;
}
/**
* Get cache busting version for the application
* @returns {string} Version string
*/
getVersion() {
return this.version;
}
/**
* Generate a file hash for specific file-based cache busting
* @param {string} filePath - Path to the file
* @returns {string} File hash or version fallback
*/
getFileHash(filePath) {
try {
if (this.fileHashes.has(filePath)) {
return this.fileHashes.get(filePath);
}
const fullPath = path.join(__dirname, '..', 'public', filePath);
if (fs.existsSync(fullPath)) {
const content = fs.readFileSync(fullPath);
const hash = crypto.createHash('md5').update(content).digest('hex').substring(0, 8);
this.fileHashes.set(filePath, hash);
return hash;
}
} catch (error) {
console.warn(`Cache busting: Could not hash file ${filePath}:`, error.message);
}
// Fallback to version
return this.version;
}
/**
* Add cache busting parameter to a URL
* @param {string} url - Original URL
* @param {boolean} useFileHash - Whether to use file-specific hash
* @returns {string} URL with cache busting parameter
*/
bustCache(url, useFileHash = false) {
if (!url) return url;
const separator = url.includes('?') ? '&' : '?';
const version = useFileHash ? this.getFileHash(url) : this.version;
return `${url}${separator}v=${version}`;
}
/**
* Get cache headers for static assets
* @param {boolean} longTerm - Whether to use long-term caching
* @returns {object} Cache headers
*/
getCacheHeaders(longTerm = false) {
if (longTerm) {
// Long-term caching for versioned assets
return {
'Cache-Control': 'public, max-age=31536000, immutable', // 1 year
'ETag': this.version
};
} else {
// Short-term caching for frequently changing content
return {
'Cache-Control': 'public, max-age=300', // 5 minutes
'ETag': this.version
};
}
}
/**
* Get no-cache headers for dynamic content
* @returns {object} No-cache headers
*/
getNoCacheHeaders() {
return {
'Cache-Control': 'no-cache, no-store, must-revalidate',
'Pragma': 'no-cache',
'Expires': '0'
};
}
/**
* Middleware to add cache busting to HTML responses
* @returns {function} Express middleware
*/
htmlMiddleware() {
return (req, res, next) => {
// Store original send method
const originalSend = res.send;
// Override send method to modify HTML content
res.send = (body) => {
if (typeof body === 'string' && res.get('Content-Type')?.includes('text/html')) {
// Add cache busting to CSS and JS files
body = body.replace(
/(href|src)=["']([^"']+\.(css|js))["']/g,
(match, attr, url, ext) => {
// Skip external URLs
if (url.startsWith('http') || url.startsWith('//')) {
return match;
}
const bustedUrl = this.bustCache(url, true);
return `${attr}="${bustedUrl}"`;
}
);
// Add version info to HTML for debugging
body = body.replace(
'</head>',
` <meta name="app-version" content="${this.version}">\n</head>`
);
}
// Call original send method
originalSend.call(res, body);
};
next();
};
}
/**
* Middleware to set cache headers for static files
* @returns {function} Express middleware
*/
staticCacheMiddleware() {
return (req, res, next) => {
const ext = path.extname(req.path).toLowerCase();
const isVersioned = req.query.v !== undefined;
// Set cache headers based on file type and versioning
if (['.css', '.js', '.png', '.jpg', '.jpeg', '.gif', '.ico', '.svg'].includes(ext)) {
const headers = isVersioned ?
this.getCacheHeaders(true) :
this.getCacheHeaders(false);
Object.keys(headers).forEach(key => {
res.set(key, headers[key]);
});
}
next();
};
}
}
// Create singleton instance
const cacheBusting = new CacheBusting();
module.exports = {
CacheBusting,
cacheBusting
};