| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258 |
- /* ── dashboard app JS ──────────────────────────────────────────────── */
- document.addEventListener('DOMContentLoaded', () => {
- // Confirm delete buttons
- document.querySelectorAll('[data-confirm]').forEach(el => {
- el.addEventListener('click', (e) => {
- if (!confirm(el.dataset.confirm || 'Are you sure?')) {
- e.preventDefault();
- }
- });
- });
- // ── person form: assemble ISO datetime on submit ──────────────────
- const form = document.getElementById('person-form');
- if (form) {
- form.addEventListener('submit', (e) => {
- const date = document.getElementById('birth_date').value;
- const time = document.getElementById('birth_time').value;
- const tz = document.getElementById('tz').value;
- if (!date || !time || !tz) {
- e.preventDefault();
- alert('Birth date, time, and timezone are required.');
- return;
- }
- // Convert 12h to 24h if needed
- let time24 = time;
- const ampmSel = document.getElementById('birth_ampm');
- if (ampmSel) {
- const ampm = ampmSel.value;
- const parts = time.split(':');
- let h = parseInt(parts[0], 10);
- const m = parts[1] || '00';
- if (ampm === 'PM' && h < 12) h += 12;
- if (ampm === 'AM' && h === 12) h = 0;
- time24 = `${String(h).padStart(2, '0')}:${m}`;
- }
- // Build ISO string with timezone offset
- const offset = getTzOffsetMinutes(tz, date, time24);
- const sign = offset >= 0 ? '+' : '-';
- const abs = Math.abs(offset);
- const oh = String(Math.floor(abs / 60)).padStart(2, '0');
- const om = String(abs % 60).padStart(2, '0');
- const iso = `${date}T${time24}${sign}${oh}:${om}`;
- // Remove the original time/ampm fields so they don't conflict
- const origTime = document.getElementById('birth_time');
- if (origTime) origTime.removeAttribute('name');
- if (ampmSel) ampmSel.removeAttribute('name');
- // Add a hidden field with the assembled datetime
- let hidden = document.getElementById('birth_datetime_assembled');
- if (!hidden) {
- hidden = document.createElement('input');
- hidden.type = 'hidden';
- hidden.id = 'birth_datetime_assembled';
- hidden.name = 'birth_datetime';
- form.appendChild(hidden);
- }
- hidden.value = iso;
- });
- }
- // ── UTC offset display ─────────────────────────────────────────────
- const tzSelect = document.getElementById('tz');
- const dateInput = document.getElementById('birth_date');
- const timeInput = document.getElementById('birth_time');
- const offsetDisplay = document.getElementById('utc-offset-display');
- function updateOffsetDisplay() {
- const tz = tzSelect.value;
- const date = dateInput.value;
- const time = timeInput.value;
- if (!tz || !date || !time) {
- offsetDisplay.textContent = '';
- return;
- }
- const offset = getTzOffsetMinutes(tz, date, time);
- const sign = offset >= 0 ? '+' : '-';
- const abs = Math.abs(offset);
- const oh = String(Math.floor(abs / 60)).padStart(2, '0');
- const om = String(abs % 60).padStart(2, '0');
- const tzAbbr = getTzAbbreviation(tz, date);
- offsetDisplay.textContent = `UTC${sign}${oh}:${om} (${tzAbbr})`;
- offsetDisplay.style.color = '#3fb950';
- }
- if (tzSelect && dateInput && timeInput) {
- tzSelect.addEventListener('change', updateOffsetDisplay);
- dateInput.addEventListener('input', updateOffsetDisplay);
- timeInput.addEventListener('input', updateOffsetDisplay);
- // Initial display if values are pre-filled
- updateOffsetDisplay();
- }
- // ── GeoNames autocomplete for birthplace ───────────────────────────
- const bpInput = document.getElementById('birthplace');
- const bpResults = document.getElementById('birthplace-results');
- const bpStatus = document.getElementById('birthplace-status');
- let debounceTimer = null;
- let activeIndex = -1;
- if (bpInput && bpResults) {
- bpInput.addEventListener('input', () => {
- clearTimeout(debounceTimer);
- const q = bpInput.value.trim();
- if (q.length < 2) {
- bpResults.style.display = 'none';
- bpStatus.textContent = '';
- return;
- }
- bpStatus.textContent = 'Searching...';
- debounceTimer = setTimeout(() => searchGeoNames(q), 300);
- });
- bpInput.addEventListener('keydown', (e) => {
- const items = bpResults.querySelectorAll('.autocomplete-item');
- if (!items.length) return;
- if (e.key === 'ArrowDown') {
- e.preventDefault();
- activeIndex = Math.min(activeIndex + 1, items.length - 1);
- updateActive(items);
- } else if (e.key === 'ArrowUp') {
- e.preventDefault();
- activeIndex = Math.max(activeIndex - 1, 0);
- updateActive(items);
- } else if (e.key === 'Enter') {
- e.preventDefault();
- if (activeIndex >= 0 && items[activeIndex]) {
- items[activeIndex].click();
- }
- } else if (e.key === 'Escape') {
- bpResults.style.display = 'none';
- activeIndex = -1;
- }
- });
- // Close dropdown on outside click
- document.addEventListener('click', (e) => {
- if (!bpInput.contains(e.target) && !bpResults.contains(e.target)) {
- bpResults.style.display = 'none';
- }
- });
- }
- function updateActive(items) {
- items.forEach((item, i) => {
- item.classList.toggle('active', i === activeIndex);
- });
- }
- async function searchGeoNames(q) {
- try {
- const resp = await fetch(`/api/geonames/search?q=${encodeURIComponent(q)}`);
- const results = await resp.json();
- if (!results.length) {
- bpStatus.textContent = 'No results found.';
- bpResults.style.display = 'none';
- return;
- }
- bpStatus.textContent = '';
- activeIndex = -1;
- bpResults.innerHTML = '';
- results.forEach((r, i) => {
- const div = document.createElement('div');
- div.className = 'autocomplete-item';
- const parts = [r.name, r.admin1, r.country].filter(Boolean);
- const main = parts[0];
- const sub = parts.slice(1).join(', ');
- const elev = r.elevation ? ` ${r.elevation}m` : '';
- div.innerHTML = `
- <span class="ac-main">${esc(main)}</span>
- <span class="ac-sub"> — ${esc(sub)}${esc(elev)}</span>
- <span class="ac-coords">${r.lat.toFixed(2)}, ${r.lng.toFixed(2)}</span>
- `;
- div.addEventListener('click', () => selectPlace(r));
- bpResults.appendChild(div);
- });
- bpResults.style.display = 'block';
- } catch (err) {
- bpStatus.textContent = 'Search failed.';
- bpResults.style.display = 'none';
- }
- }
- function selectPlace(r) {
- bpInput.value = [r.name, r.admin1, r.country].filter(Boolean).join(', ');
- document.getElementById('latitude').value = r.lat;
- document.getElementById('longitude').value = r.lng;
- if (r.elevation) {
- document.getElementById('elevation').value = r.elevation;
- }
- if (r.timezone) {
- const tzSel = document.getElementById('tz');
- // Check if the timezone exists in our select
- let found = false;
- for (const opt of tzSel.options) {
- if (opt.value === r.timezone) {
- tzSel.value = r.timezone;
- found = true;
- break;
- }
- }
- if (!found) {
- // Add it as a new option
- const opt = document.createElement('option');
- opt.value = r.timezone;
- opt.textContent = `${r.timezone} (auto-detected)`;
- tzSel.appendChild(opt);
- tzSel.value = r.timezone;
- }
- updateOffsetDisplay();
- }
- bpResults.style.display = 'none';
- bpStatus.textContent = `✓ ${r.timezone ? 'Timezone auto-filled' : 'Coordinates filled'}`;
- bpStatus.style.color = '#3fb950';
- }
- // ── helpers ─────────────────────────────────────────────────────────
- function getTzOffsetMinutes(tz, date, time) {
- try {
- const dt = new Date(`${date}T${time}:00`);
- // Use Intl to get the offset for the specific timezone
- const formatter = new Intl.DateTimeFormat('en-US', {
- timeZone: tz,
- year: 'numeric', month: '2-digit', day: '2-digit',
- hour: '2-digit', minute: '2-digit', second: '2-digit',
- hour12: false,
- });
- const parts = formatter.formatToParts(dt);
- const p = {};
- parts.forEach(part => { p[part.type] = part.value; });
- const local = new Date(`${p.year}-${p.month}-${p.day}T${p.hour}:${p.second}:00`);
- return Math.round((local - dt) / 60000);
- } catch (e) {
- return 0;
- }
- }
- function getTzAbbreviation(tz, date) {
- try {
- const dt = new Date(`${date}T12:00:00`);
- const formatter = new Intl.DateTimeFormat('en-US', {
- timeZone: tz,
- timeZoneName: 'short',
- });
- const parts = formatter.formatToParts(dt);
- const tzPart = parts.find(p => p.type === 'timeZoneName');
- return tzPart ? tzPart.value : tz;
- } catch (e) {
- return tz;
- }
- }
- function esc(str) {
- const d = document.createElement('div');
- d.textContent = str;
- return d.innerHTML;
- }
- });
|