Browse Source

Fix admin panel for mobile and improve settings persistence

Major improvements:
1. Mobile-friendly redesign with vertical stacking
2. Touch-optimized inputs (44px minimum targets)
3. Better event handling with input events
4. Debounced saving to prevent excessive saves
5. Visual save indicator (toast notification)
6. Collapsible sections for better organization
7. Range sliders with value display
8. Responsive design (mobile-first approach)

Fixes:
- Settings now save immediately on input
- Visual feedback when settings are saved
- Proper mobile layout with full-width inputs
- Larger touch targets for better usability
- Sections can be collapsed and state persists

🤖 Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
master
HikeMap User 1 month ago
parent
commit
49543f09ca
  1. 86
      geocaches.json
  2. 272
      index.html

86
geocaches.json

@ -23,70 +23,6 @@
"createdAt": 1767115055219, "createdAt": 1767115055219,
"alerted": true "alerted": true
}, },
{
"id": "gc_1767127362829_hbnrs61u2",
"lat": 30.527765731982754,
"lng": -97.83747911453248,
"messages": [
{
"author": "Dr. Poopinshitz",
"text": "Home is where you poop",
"timestamp": 1767127384562
},
{
"author": "Riker",
"text": "I poop here too",
"timestamp": 1767132954284
},
{
"author": "Sulu",
"text": "I poop outside weirdos.",
"timestamp": 1767132973928
},
{
"author": "Ibby Dibby",
"text": "CAN I EAT IT?",
"timestamp": 1767133040824
},
{
"author": "Riker",
"text": "Stupid little cousin ibby dibby...",
"timestamp": 1767133204544
},
{
"author": "Presley",
"text": "BARK! god damnit BARK!",
"timestamp": 1767199495387
},
{
"author": "test",
"text": "test",
"timestamp": 1767199518737
},
{
"author": "ji",
"text": "ji",
"timestamp": 1767200126654
},
{
"author": "noice",
"text": "noice",
"timestamp": 1767200152466
},
{
"author": "noice 2",
"text": "ceecac",
"timestamp": 1767200173936
},
{
"author": "ear",
"text": "aer",
"timestamp": 1767200335702
}
],
"createdAt": 1767127362829,
"alerted": true
},
{ {
"id": "gc_1767133043404_9zkbxphry", "id": "gc_1767133043404_9zkbxphry",
"lat": 30.52230723615832, "lat": 30.52230723615832,
@ -126,6 +62,26 @@
"timestamp": 1767201911761 "timestamp": 1767201911761
} }
], ],
"createdAt": 1767201872423
"createdAt": 1767201872423,
"alerted": false,
"color": "#000000",
"visibilityDistance": 6
},
{
"id": "gc_1767206407844_pajkdohom",
"lat": 30.527761111163603,
"lng": -97.83752202987671,
"title": "RikerWuzHurr",
"icon": "dog",
"color": "#000000",
"visibilityDistance": 10,
"messages": [
{
"author": "Riker",
"text": "wuz hurr!",
"timestamp": 1767206476888
}
],
"createdAt": 1767206407844
} }
] ]

272
index.html

@ -691,56 +691,177 @@
z-index: 998; z-index: 998;
} }
/* Admin Panel Styles */
/* Admin Panel Styles - Mobile First */
.admin-setting-group { .admin-setting-group {
padding: 15px 10px; padding: 15px 10px;
} }
.admin-input-row { .admin-input-row {
display: flex; display: flex;
align-items: center;
margin-bottom: 12px;
gap: 10px;
flex-direction: column;
margin-bottom: 20px;
gap: 8px;
} }
.admin-input-row label { .admin-input-row label {
flex: 0 0 180px;
color: #ddd;
font-size: 13px;
font-weight: 500;
color: #FFA726;
font-size: 14px;
font-weight: 600;
letter-spacing: 0.3px; letter-spacing: 0.3px;
margin-bottom: 4px;
}
.admin-input-wrapper {
display: flex;
align-items: center;
gap: 10px;
} }
.admin-input-row input[type="number"] {
width: 80px;
padding: 6px 8px;
.admin-input-row input[type="number"],
.admin-input-row input[type="range"] {
flex: 1;
min-height: 44px;
padding: 10px 12px;
background: #2a2a2a; background: #2a2a2a;
color: white; color: white;
border: 1px solid #444;
border-radius: 4px;
font-size: 13px;
border: 2px solid #444;
border-radius: 8px;
font-size: 16px;
text-align: center; text-align: center;
-webkit-appearance: none;
}
.admin-input-row input[type="checkbox"] {
width: 24px;
height: 24px;
cursor: pointer;
} }
.admin-input-row input[type="number"]:focus {
.admin-input-row input[type="number"]:focus,
.admin-input-row input[type="range"]:focus {
outline: none; outline: none;
border-color: #4CAF50; border-color: #4CAF50;
background: #333; background: #333;
box-shadow: 0 0 0 3px rgba(76, 175, 80, 0.2);
} }
.admin-input-row .unit { .admin-input-row .unit {
color: #999;
font-size: 12px;
font-style: italic;
min-width: 50px;
color: #aaa;
font-size: 14px;
white-space: nowrap;
min-width: 60px;
text-align: right;
}
.admin-range-value {
background: #333;
color: #FFA726;
padding: 8px 12px;
border-radius: 6px;
font-weight: bold;
min-width: 60px;
text-align: center;
border: 1px solid #555;
}
/* Desktop layout */
@media (min-width: 768px) {
.admin-input-row {
flex-direction: row;
align-items: center;
}
.admin-input-row label {
flex: 0 0 200px;
margin-bottom: 0;
}
.admin-input-row input[type="number"],
.admin-input-row input[type="range"] {
flex: 0 0 auto;
width: 120px;
}
} }
.admin-button-group { .admin-button-group {
padding: 15px 10px;
padding: 20px 10px;
display: flex; display: flex;
gap: 10px;
flex-direction: column;
gap: 12px;
}
.admin-button-group button {
min-height: 48px;
padding: 12px 20px;
background: #4CAF50;
color: white;
border: none;
border-radius: 8px;
cursor: pointer;
font-size: 15px;
font-weight: 600;
transition: all 0.2s;
box-shadow: 0 2px 4px rgba(0,0,0,0.2);
letter-spacing: 0.5px;
}
.admin-button-group button:active {
transform: translateY(1px);
box-shadow: 0 1px 2px rgba(0,0,0,0.2);
}
.admin-button-group button:hover {
background: #45a049;
box-shadow: 0 4px 8px rgba(0,0,0,0.3);
}
.admin-button-group button.danger {
background: #f44336;
}
.admin-button-group button.danger:hover {
background: #da190b;
}
@media (min-width: 768px) {
.admin-button-group {
flex-direction: row;
flex-wrap: wrap; flex-wrap: wrap;
} }
.admin-button-group button { .admin-button-group button {
min-height: 40px;
flex: 1; flex: 1;
min-width: 140px; min-width: 140px;
font-size: 13px;
padding: 10px;
letter-spacing: 0.5px;
}
}
/* Save indicator */
.admin-save-indicator {
position: fixed;
top: 70px;
right: 20px;
background: #4CAF50;
color: white;
padding: 10px 20px;
border-radius: 20px;
box-shadow: 0 2px 8px rgba(0,0,0,0.3);
opacity: 0;
transform: translateY(-10px);
transition: all 0.3s;
z-index: 10000;
pointer-events: none;
}
.admin-save-indicator.show {
opacity: 1;
transform: translateY(0);
}
/* Collapsible sections */
.section.collapsible .section-title {
cursor: pointer;
user-select: none;
position: relative;
padding-right: 30px;
}
.section.collapsible .section-title:after {
content: '▼';
position: absolute;
right: 10px;
top: 50%;
transform: translateY(-50%);
transition: transform 0.3s;
color: #FFA726;
}
.section.collapsed .section-title:after {
transform: translateY(-50%) rotate(-90deg);
}
.section-content {
max-height: 1000px;
overflow: hidden;
transition: max-height 0.3s ease;
}
.section.collapsed .section-content {
max-height: 0;
} }
@keyframes slideUp { @keyframes slideUp {
from { transform: translateX(-50%) translateY(100%); } from { transform: translateX(-50%) translateY(100%); }
@ -907,14 +1028,19 @@
</div> </div>
<div class="tab-content" id="adminContent"> <div class="tab-content" id="adminContent">
<div class="section">
<div class="section collapsible" data-section="geocache">
<div class="section-title">Geocache Settings</div> <div class="section-title">Geocache Settings</div>
<div class="section-content">
<div class="admin-setting-group"> <div class="admin-setting-group">
<div class="admin-input-row"> <div class="admin-input-row">
<label>Interaction Range:</label> <label>Interaction Range:</label>
<input type="number" id="geocacheRange" min="1" max="50" value="5">
<div class="admin-input-wrapper">
<input type="range" id="geocacheRangeSlider" min="1" max="50" value="5">
<span class="admin-range-value" id="geocacheRangeValue">5</span>
<span class="unit">meters</span> <span class="unit">meters</span>
</div> </div>
<input type="number" id="geocacheRange" min="1" max="50" value="5" style="display:none;">
</div>
<div class="admin-input-row"> <div class="admin-input-row">
<label>Alert Distance:</label> <label>Alert Distance:</label>
<input type="number" id="geocacheAlertRange" min="1" max="50" value="5"> <input type="number" id="geocacheAlertRange" min="1" max="50" value="5">
@ -1032,6 +1158,9 @@
</div> </div>
</div> </div>
<!-- Admin Save Indicator -->
<div id="adminSaveIndicator" class="admin-save-indicator">Settings Saved ✓</div>
<!-- Geocache dialog --> <!-- Geocache dialog -->
<div id="geocacheDialog" class="geocache-dialog" style="display: none;"> <div id="geocacheDialog" class="geocache-dialog" style="display: none;">
<div class="geocache-dialog-content"> <div class="geocache-dialog-content">
@ -2051,8 +2180,39 @@
} }
} }
// Debounce function for saving
function debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}
// Show save indicator
function showSaveIndicator(message = 'Settings Saved ✓') {
const indicator = document.getElementById('adminSaveIndicator');
if (indicator) {
indicator.textContent = message;
indicator.classList.add('show');
setTimeout(() => {
indicator.classList.remove('show');
}, 2000);
}
}
// Debounced save function
const debouncedSave = debounce(() => {
saveAdminSettings();
showSaveIndicator();
}, 500);
function setupAdminInputListeners() { function setupAdminInputListeners() {
// Add change listeners to all admin inputs
// Add change and input listeners to all admin inputs
const adminInputs = [ const adminInputs = [
{ id: 'geocacheRange', setting: 'geocacheRange', type: 'number' }, { id: 'geocacheRange', setting: 'geocacheRange', type: 'number' },
{ id: 'geocacheAlertRange', setting: 'geocacheAlertRange', type: 'number' }, { id: 'geocacheAlertRange', setting: 'geocacheAlertRange', type: 'number' },
@ -2065,10 +2225,30 @@
{ id: 'proximityCheckInterval', setting: 'proximityCheckInterval', type: 'number' } { id: 'proximityCheckInterval', setting: 'proximityCheckInterval', type: 'number' }
]; ];
// Setup range sliders if they exist
const sliderPairs = [
{ slider: 'geocacheRangeSlider', input: 'geocacheRange', value: 'geocacheRangeValue' }
];
sliderPairs.forEach(pair => {
const slider = document.getElementById(pair.slider);
const input = document.getElementById(pair.input);
const valueDisplay = document.getElementById(pair.value);
if (slider && input && valueDisplay) {
slider.addEventListener('input', function() {
input.value = this.value;
valueDisplay.textContent = this.value;
input.dispatchEvent(new Event('input'));
});
}
});
adminInputs.forEach(input => { adminInputs.forEach(input => {
const element = document.getElementById(input.id); const element = document.getElementById(input.id);
if (element) { if (element) {
element.addEventListener('change', function() {
// Use both input and change events for better responsiveness
const updateHandler = function() {
let value; let value;
if (input.type === 'checkbox') { if (input.type === 'checkbox') {
value = this.checked; value = this.checked;
@ -2078,9 +2258,41 @@
value = this.value; value = this.value;
} }
adminSettings[input.setting] = value; adminSettings[input.setting] = value;
saveAdminSettings();
updateStatus(`${input.setting} updated to ${value}`);
debouncedSave();
};
if (input.type === 'checkbox') {
element.addEventListener('change', updateHandler);
} else {
element.addEventListener('input', updateHandler);
element.addEventListener('change', updateHandler);
}
}
});
// Setup collapsible sections
document.querySelectorAll('.section.collapsible').forEach(section => {
const title = section.querySelector('.section-title');
if (title) {
title.addEventListener('click', () => {
section.classList.toggle('collapsed');
// Save collapsed state
const sectionName = section.dataset.section;
if (sectionName) {
const collapsedSections = JSON.parse(localStorage.getItem('collapsedSections') || '{}');
collapsedSections[sectionName] = section.classList.contains('collapsed');
localStorage.setItem('collapsedSections', JSON.stringify(collapsedSections));
}
}); });
// Restore collapsed state
const sectionName = section.dataset.section;
if (sectionName) {
const collapsedSections = JSON.parse(localStorage.getItem('collapsedSections') || '{}');
if (collapsedSections[sectionName]) {
section.classList.add('collapsed');
}
}
} }
}); });
} }

Loading…
Cancel
Save