278 lines
10 KiB
JavaScript
278 lines
10 KiB
JavaScript
document.addEventListener('DOMContentLoaded', function () {
|
|
const sidebarToggle = document.querySelector('.sidebar-toggle');
|
|
const sidebarOverlay = document.querySelector('.sidebar-overlay');
|
|
const sidebarClose = document.querySelector('.sidebar-close');
|
|
const modeToggles = document.querySelectorAll('.mode-toggle');
|
|
const geolocateButtons = document.querySelectorAll('[data-geolocate-btn]');
|
|
const body = document.body;
|
|
|
|
const collapseKey = 'hk_nav_collapsed';
|
|
const themeKey = 'hk_theme';
|
|
|
|
const syncThemeToggleLabels = function (dark) {
|
|
modeToggles.forEach(function (toggle) {
|
|
const icon = toggle.querySelector('.theme-icon');
|
|
const label = toggle.querySelector('.theme-label');
|
|
const text = dark ? 'Light Mode' : 'Dark Mode';
|
|
|
|
if (icon) {
|
|
icon.textContent = dark ? '☀️' : '🌙';
|
|
}
|
|
|
|
if (label) {
|
|
label.textContent = dark ? 'Light' : 'Dark';
|
|
} else {
|
|
toggle.textContent = text;
|
|
}
|
|
|
|
toggle.setAttribute('aria-label', dark ? 'Switch to light mode' : 'Switch to dark mode');
|
|
});
|
|
};
|
|
|
|
const setCollapsed = function (collapsed) {
|
|
body.classList.toggle('nav-collapsed', collapsed);
|
|
localStorage.setItem(collapseKey, collapsed ? '1' : '0');
|
|
};
|
|
|
|
const setTheme = function (theme) {
|
|
const dark = theme === 'dark';
|
|
body.classList.toggle('theme-dark', dark);
|
|
syncThemeToggleLabels(dark);
|
|
localStorage.setItem(themeKey, dark ? 'dark' : 'light');
|
|
};
|
|
|
|
const savedTheme = localStorage.getItem(themeKey) || 'light';
|
|
setTheme(savedTheme);
|
|
|
|
if (window.innerWidth > 1024) {
|
|
setCollapsed(localStorage.getItem(collapseKey) === '1');
|
|
}
|
|
|
|
if (modeToggles.length) {
|
|
modeToggles.forEach(function (toggle) {
|
|
toggle.addEventListener('click', function () {
|
|
setTheme(body.classList.contains('theme-dark') ? 'light' : 'dark');
|
|
});
|
|
});
|
|
}
|
|
|
|
if (sidebarToggle) {
|
|
sidebarToggle.addEventListener('click', function () {
|
|
if (window.innerWidth <= 1024) {
|
|
body.classList.toggle('sidebar-open');
|
|
return;
|
|
}
|
|
setCollapsed(!body.classList.contains('nav-collapsed'));
|
|
});
|
|
}
|
|
|
|
if (sidebarOverlay) {
|
|
sidebarOverlay.addEventListener('click', function () {
|
|
body.classList.remove('sidebar-open');
|
|
});
|
|
}
|
|
|
|
if (sidebarClose) {
|
|
sidebarClose.addEventListener('click', function () {
|
|
body.classList.remove('sidebar-open');
|
|
});
|
|
}
|
|
|
|
document.addEventListener('keydown', function (event) {
|
|
if (event.key === 'Escape') {
|
|
body.classList.remove('sidebar-open');
|
|
}
|
|
});
|
|
|
|
const revealElements = document.querySelectorAll('.reveal');
|
|
if (revealElements.length) {
|
|
if ('IntersectionObserver' in window) {
|
|
const observer = new IntersectionObserver(
|
|
function (entries) {
|
|
entries.forEach(function (entry) {
|
|
if (entry.isIntersecting) {
|
|
entry.target.classList.add('is-visible');
|
|
observer.unobserve(entry.target);
|
|
}
|
|
});
|
|
},
|
|
{
|
|
threshold: 0.12,
|
|
rootMargin: '0px 0px -40px 0px',
|
|
}
|
|
);
|
|
|
|
revealElements.forEach(function (element) {
|
|
observer.observe(element);
|
|
});
|
|
} else {
|
|
revealElements.forEach(function (element) {
|
|
element.classList.add('is-visible');
|
|
});
|
|
}
|
|
}
|
|
|
|
const findLocationField = function (container, names) {
|
|
for (let index = 0; index < names.length; index += 1) {
|
|
const field = container.querySelector('[name="' + names[index] + '"]');
|
|
if (field) {
|
|
return field;
|
|
}
|
|
}
|
|
return null;
|
|
};
|
|
|
|
const setStatus = function (container, type, message) {
|
|
const statusNode = container.querySelector('[data-location-status]');
|
|
if (!statusNode) {
|
|
return;
|
|
}
|
|
statusNode.textContent = message;
|
|
statusNode.classList.remove('is-success', 'is-error');
|
|
if (type === 'success') {
|
|
statusNode.classList.add('is-success');
|
|
}
|
|
if (type === 'error') {
|
|
statusNode.classList.add('is-error');
|
|
}
|
|
};
|
|
|
|
const setFieldValue = function (field, value, overwriteWhenFilled) {
|
|
if (!field) {
|
|
return;
|
|
}
|
|
if (!overwriteWhenFilled && field.value) {
|
|
return;
|
|
}
|
|
field.value = value || '';
|
|
};
|
|
|
|
const buildAddressText = function (address) {
|
|
const roadLine = [address.house_number, address.road].filter(Boolean).join(' ').trim();
|
|
const areaLine = [
|
|
address.suburb,
|
|
address.neighbourhood,
|
|
address.city || address.town || address.village || address.municipality,
|
|
address.state,
|
|
address.postcode,
|
|
address.country,
|
|
]
|
|
.filter(Boolean)
|
|
.filter(function (value, index, values) {
|
|
return values.indexOf(value) === index;
|
|
})
|
|
.join(', ')
|
|
.trim();
|
|
|
|
return [roadLine, areaLine].filter(Boolean).join('\n').trim();
|
|
};
|
|
|
|
const buildLocationLabel = function (address) {
|
|
return [
|
|
address.suburb || address.neighbourhood || address.hamlet,
|
|
address.city || address.town || address.village || address.municipality,
|
|
address.state,
|
|
]
|
|
.filter(Boolean)
|
|
.filter(function (value, index, values) {
|
|
return values.indexOf(value) === index;
|
|
})
|
|
.join(', ')
|
|
.trim();
|
|
};
|
|
|
|
const reverseGeocode = async function (latitude, longitude) {
|
|
const url = 'https://nominatim.openstreetmap.org/reverse?format=jsonv2&addressdetails=1&lat=' + encodeURIComponent(latitude) + '&lon=' + encodeURIComponent(longitude);
|
|
const response = await fetch(url, {
|
|
headers: {
|
|
Accept: 'application/json',
|
|
},
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error('Reverse geocoding is temporarily unavailable.');
|
|
}
|
|
|
|
return response.json();
|
|
};
|
|
|
|
if (geolocateButtons.length) {
|
|
geolocateButtons.forEach(function (button) {
|
|
button.addEventListener('click', function () {
|
|
const container = button.closest('[data-location-form]') || button.closest('form') || document;
|
|
const latitudeField = findLocationField(container, ['latitude']);
|
|
const longitudeField = findLocationField(container, ['longitude']);
|
|
const accuracyField = findLocationField(container, ['location_accuracy_m']);
|
|
const labelField = findLocationField(container, ['location_label']);
|
|
const addressField = findLocationField(container, ['default_address', 'address']);
|
|
|
|
if (!navigator.geolocation) {
|
|
setStatus(container, 'error', 'Your browser does not support GPS access. You can still enter your address manually.');
|
|
return;
|
|
}
|
|
|
|
button.disabled = true;
|
|
setStatus(container, '', 'Requesting GPS access from your device…');
|
|
|
|
navigator.geolocation.getCurrentPosition(
|
|
async function (position) {
|
|
const latitude = Number(position.coords.latitude).toFixed(6);
|
|
const longitude = Number(position.coords.longitude).toFixed(6);
|
|
const accuracy = Number(position.coords.accuracy || 0).toFixed(2);
|
|
|
|
if (latitudeField) {
|
|
latitudeField.value = latitude;
|
|
}
|
|
if (longitudeField) {
|
|
longitudeField.value = longitude;
|
|
}
|
|
if (accuracyField) {
|
|
accuracyField.value = accuracy;
|
|
}
|
|
|
|
try {
|
|
const payload = await reverseGeocode(latitude, longitude);
|
|
const address = payload.address || {};
|
|
const suggestedAddress = buildAddressText(address);
|
|
const locationLabel = buildLocationLabel(address);
|
|
|
|
setFieldValue(labelField, locationLabel, false);
|
|
setFieldValue(addressField, suggestedAddress || payload.display_name || '', false);
|
|
setStatus(
|
|
container,
|
|
'success',
|
|
'GPS captured successfully. Accuracy ±' + Math.round(Number(accuracy)) + ' meters. Please review the address before saving.'
|
|
);
|
|
} catch (error) {
|
|
setStatus(
|
|
container,
|
|
'success',
|
|
'GPS captured successfully. Exact coordinates were saved, but the address suggestion could not be loaded right now.'
|
|
);
|
|
} finally {
|
|
button.disabled = false;
|
|
}
|
|
},
|
|
function (error) {
|
|
let message = 'Unable to get your location. Please enter your address manually.';
|
|
|
|
if (error.code === error.PERMISSION_DENIED) {
|
|
message = 'Location permission was denied. You can still continue with a manual address.';
|
|
} else if (error.code === error.TIMEOUT) {
|
|
message = 'Location request timed out. Please retry or continue manually.';
|
|
}
|
|
|
|
setStatus(container, 'error', message);
|
|
button.disabled = false;
|
|
},
|
|
{
|
|
enableHighAccuracy: true,
|
|
timeout: 15000,
|
|
maximumAge: 0,
|
|
}
|
|
);
|
|
});
|
|
});
|
|
}
|
|
});
|