diff --git a/map/app/public/admin.html b/map/app/public/admin.html
index 1698fd9..ebc3880 100644
--- a/map/app/public/admin.html
+++ b/map/app/public/admin.html
@@ -2,7 +2,7 @@
-
+
Admin Panel
@@ -272,59 +272,35 @@
-
Existing Users
-
-
-
-
- | Email |
- Name |
- Admin |
- Created |
- Actions |
-
-
-
-
-
-
-
-
- Loading users...
-
-
- No users found.
-
+
+
Loading users...
@@ -393,6 +369,9 @@
console.log('Local QR Code implementation loaded');
+
+
+
diff --git a/map/app/public/css/admin.css b/map/app/public/css/admin.css
index 17b2a76..4100a26 100644
--- a/map/app/public/css/admin.css
+++ b/map/app/public/css/admin.css
@@ -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 thead {
+ display: none;
+ }
+
+ .users-table tbody,
+ .users-table tr,
+ .users-table td {
+ 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 th,
.users-table td {
- padding: 8px;
+ 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;
+ }
+}
diff --git a/map/app/public/css/shifts.css b/map/app/public/css/shifts.css
index 871f4a0..8260fbd 100644
--- a/map/app/public/css/shifts.css
+++ b/map/app/public/css/shifts.css
@@ -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 {
@@ -561,4 +582,17 @@
.calendar-day-number {
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);
}
\ No newline at end of file
diff --git a/map/app/public/css/style.css b/map/app/public/css/style.css
index ff10ef3..192b164 100644
--- a/map/app/public/css/style.css
+++ b/map/app/public/css/style.css
@@ -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;
+ }
+}
diff --git a/map/app/public/css/user.css b/map/app/public/css/user.css
index 860deac..32a7ab8 100644
--- a/map/app/public/css/user.css
+++ b/map/app/public/css/user.css
@@ -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);
+}
diff --git a/map/app/public/index.html b/map/app/public/index.html
index 3b22400..e84775a 100644
--- a/map/app/public/index.html
+++ b/map/app/public/index.html
@@ -2,7 +2,7 @@
-
+
Map by BNKops
@@ -377,6 +377,9 @@
integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo="
crossorigin="">
+
+
+
diff --git a/map/app/public/js/admin.js b/map/app/public/js/admin.js
index f13177c..5cbf841 100644
--- a/map/app/public/js/admin.js
+++ b/map/app/public/js/admin.js
@@ -3,8 +3,37 @@ let adminMap = null;
let startMarker = null;
let storedQRCodes = {};
+// A function to set viewport dimensions for admin page
+function setAdminViewportDimensions() {
+ const doc = document.documentElement;
+
+ // Set height and width
+ doc.style.setProperty('--app-height', `${window.innerHeight}px`);
+ doc.style.setProperty('--app-width', `${window.innerWidth}px`);
+
+ // Handle safe area insets for devices with notches or home indicators
+ if (CSS.supports('padding: env(safe-area-inset-top)')) {
+ doc.style.setProperty('--safe-area-top', 'env(safe-area-inset-top)');
+ doc.style.setProperty('--safe-area-bottom', 'env(safe-area-inset-bottom)');
+ doc.style.setProperty('--safe-area-left', 'env(safe-area-inset-left)');
+ doc.style.setProperty('--safe-area-right', 'env(safe-area-inset-right)');
+ } else {
+ doc.style.setProperty('--safe-area-top', '0px');
+ doc.style.setProperty('--safe-area-bottom', '0px');
+ doc.style.setProperty('--safe-area-left', '0px');
+ doc.style.setProperty('--safe-area-right', '0px');
+ }
+}
+
// Initialize when DOM is loaded
document.addEventListener('DOMContentLoaded', () => {
+ // Set initial viewport dimensions and listen for resize events
+ setAdminViewportDimensions();
+ window.addEventListener('resize', setAdminViewportDimensions);
+ window.addEventListener('orientationchange', () => {
+ setTimeout(setAdminViewportDimensions, 100);
+ });
+
checkAdminAuth();
initializeAdminMap();
loadCurrentStartLocation();
@@ -1194,46 +1223,61 @@ async function loadUsers() {
}
function displayUsers(users) {
- const tableBody = document.getElementById('users-table-body');
- const emptyEl = document.getElementById('users-empty');
-
- if (!tableBody) return;
-
+ const container = document.querySelector('.users-list');
+ if (!container) return;
+
if (!users || users.length === 0) {
- if (emptyEl) emptyEl.style.display = 'block';
+ container.innerHTML = 'Existing Users
No users found.
';
return;
}
-
- if (emptyEl) emptyEl.style.display = 'none';
-
- tableBody.innerHTML = users.map(user => {
- const createdDate = user.created_at || user['Created At'] || user.createdAt;
- const formattedDate = createdDate ? new Date(createdDate).toLocaleDateString() : 'N/A';
- const isAdmin = user.admin || user.Admin || false;
- const userId = user.Id || user.id || user.ID;
-
- return `
-
- | ${escapeHtml(user.email || user.Email || 'N/A')} |
- ${escapeHtml(user.name || user.Name || 'N/A')} |
-
-
- ${isAdmin ? 'Admin' : 'User'}
-
- |
- ${formattedDate} |
-
-
-
-
- |
-
- `;
- }).join('');
-
- // Setup event listeners for user actions
+
+ const tableHtml = `
+ Existing Users
+
+
+
+
+ | Email |
+ Name |
+ Role |
+ Created |
+ Actions |
+
+
+
+ ${users.map(user => {
+ const createdDate = user.created_at || user['Created At'] || user.createdAt;
+ const formattedDate = createdDate ? new Date(createdDate).toLocaleDateString() : 'N/A';
+ const isAdmin = user.admin || user.Admin || false;
+ const userId = user.Id || user.id || user.ID;
+
+ return `
+
+ | ${escapeHtml(user.email || user.Email || 'N/A')} |
+ ${escapeHtml(user.name || user.Name || 'N/A')} |
+
+
+ ${isAdmin ? 'Admin' : 'User'}
+
+ |
+ ${formattedDate} |
+
+
+
+
+ |
+
+ `;
+ }).join('')}
+
+
+
+ Loading...
+ `;
+
+ container.innerHTML = tableHtml;
setupUserActionListeners();
}
diff --git a/map/app/public/js/cache-manager.js b/map/app/public/js/cache-manager.js
new file mode 100644
index 0000000..6d07c8a
--- /dev/null
+++ b/map/app/public/js/cache-manager.js
@@ -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 = `
+
+ 🔄 A new version is available!
+
+
+
+ `;
+
+ // 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;
+}
diff --git a/map/app/public/js/main.js b/map/app/public/js/main.js
index da0f20a..ace0d29 100644
--- a/map/app/public/js/main.js
+++ b/map/app/public/js/main.js
@@ -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 {
diff --git a/map/app/public/js/shifts.js b/map/app/public/js/shifts.js
index 94a14f2..ffaeb2e 100644
--- a/map/app/public/js/shifts.js
+++ b/map/app/public/js/shifts.js
@@ -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();
diff --git a/map/app/public/js/user.js b/map/app/public/js/user.js
index 88bfeae..02a4ae4 100644
--- a/map/app/public/js/user.js
+++ b/map/app/public/js/user.js
@@ -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();
diff --git a/map/app/public/js/utils.js b/map/app/public/js/utils.js
index 6d94bc7..771a702 100644
--- a/map/app/public/js/utils.js
+++ b/map/app/public/js/utils.js
@@ -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`;
+ }
+ }
+}
diff --git a/map/app/public/login.html b/map/app/public/login.html
index 5fb4f33..052fea7 100644
--- a/map/app/public/login.html
+++ b/map/app/public/login.html
@@ -2,7 +2,7 @@
-
+
Login - Map by BNKops
@@ -263,5 +263,8 @@
})
.catch(console.error);
+
+
+
diff --git a/map/app/public/shifts.html b/map/app/public/shifts.html
index 2cc9e93..f4dcf78 100644
--- a/map/app/public/shifts.html
+++ b/map/app/public/shifts.html
@@ -2,7 +2,7 @@
-
+
Volunteer Shifts - BNKops Map
@@ -78,6 +78,9 @@
+
+
+
\ No newline at end of file
diff --git a/map/app/public/user.html b/map/app/public/user.html
index d48d78e..50a431a 100644
--- a/map/app/public/user.html
+++ b/map/app/public/user.html
@@ -2,7 +2,7 @@
-
+
User Profile
@@ -53,6 +53,9 @@
+
+
+
diff --git a/map/app/server.js b/map/app/server.js
index 4082e5a..1dc6dce 100644
--- a/map/app/server.js
+++ b/map/app/server.js
@@ -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 {
diff --git a/map/app/utils/cacheBusting.js b/map/app/utils/cacheBusting.js
new file mode 100644
index 0000000..79ee9f7
--- /dev/null
+++ b/map/app/utils/cacheBusting.js
@@ -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(
+ '',
+ ` \n`
+ );
+ }
+
+ // 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
+};