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:
parent
59ca2379f2
commit
bb7032d649
@ -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>
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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();
|
||||
}
|
||||
|
||||
|
||||
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`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
@ -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>
|
||||
|
||||
@ -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 {
|
||||
|
||||
179
map/app/utils/cacheBusting.js
Normal file
179
map/app/utils/cacheBusting.js
Normal 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
|
||||
};
|
||||
Loading…
x
Reference in New Issue
Block a user