diff --git a/index.html b/index.html index c1d1247..eff6212 100644 --- a/index.html +++ b/index.html @@ -20,6 +20,10 @@
+ Click any marker to open the home
@@ -27,6 +31,17 @@
+
Orphanage location
diff --git a/src/main.js b/src/main.js index 3da4460..ccfe336 100644 --- a/src/main.js +++ b/src/main.js @@ -1,11 +1,18 @@ import Globe from 'globe.gl'; import { orphanages } from './data.js'; -const globeEl = document.getElementById('globe'); -const panelEl = document.getElementById('panel'); -const resetBtn = document.getElementById('reset-view'); -const hintEl = document.getElementById('hint'); -const countryListEl = document.getElementById('country-list'); +const $ = sel => document.querySelector(sel); + +const globeEl = $('#globe'); +const panelEl = $('#panel'); +const resetBtn = $('#reset-view'); +const hintEl = $('#hint'); +const searchInput = $('#search-input'); +const searchResults = $('#search-results'); +const searchClear = $('#search-clear'); +const tourBtn = $('#tour-btn'); +const tourIcon = $('#tour-icon'); +const tourLabel = $('#tour-label'); // ---- globe --------------------------------------------------------------- @@ -63,10 +70,12 @@ resize(); window.addEventListener('resize', resize); // default view (a nice angle that shows all 10) -world.pointOfView({ lat: 24, lng: 38, altitude: 2.4 }, 0); +const HOME_VIEW = { lat: 24, lng: 38, altitude: 2.4 }; +world.pointOfView(HOME_VIEW, 0); resetBtn.addEventListener('click', () => { - world.pointOfView({ lat: 24, lng: 38, altitude: 2.4 }, 900); + stopTour(); + world.pointOfView(HOME_VIEW, 900); renderEmpty(); }); @@ -82,29 +91,51 @@ function renderEmpty() {
`; fillCountryList(); + if (hintEl) hintEl.textContent = 'Click any marker to open the home'; } function fillCountryList() { - const ul = document.getElementById('country-list'); + const ul = $('#country-list'); if (!ul) return; ul.innerHTML = orphanages - .map( - (o, i) => - `
  • ${i + 1}. ${o.name}${o.children}
  • ` + .map((o, i) => + `
  • ${i + 1}. ${o.name}${o.children}
  • ` ) .join(''); + ul.querySelectorAll('li[data-id]').forEach(li => { + li.addEventListener('click', () => { + const o = orphanages.find(x => x.id === li.dataset.id); + if (o) selectOrphanage(o); + }); + }); +} + +function escapeHtml(s) { + return String(s) + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); } function selectOrphanage(o) { - // fly to the marker world.pointOfView({ lat: o.lat, lng: o.lng, altitude: 1.6 }, 900); + // OSM embed: use the standard "export/embed.html" iframe endpoint. + // bbox is roughly ±0.04 degrees around the marker. + const d = 0.04; + const bbox = `${o.lng - d}%2C${o.lat - d}%2C${o.lng + d}%2C${o.lat + d}`; + const osmSrc = `https://www.openstreetmap.org/export/embed.html?bbox=${bbox}&layer=mapnik&marker=${o.lat}%2C${o.lng}`; + const gmapsHref = `https://www.google.com/maps/search/?api=1&query=${o.lat}%2C${o.lng}`; + const osmHref = `https://www.openstreetmap.org/?mlat=${o.lat}&mlon=${o.lng}#map=14/${o.lat}/${o.lng}`; + panelEl.innerHTML = `
    -
    ${o.city}
    -

    ${o.name}

    -
    ${o.country}
    +
    ${escapeHtml(o.city)}
    +

    ${escapeHtml(o.name)}

    +
    ${escapeHtml(o.country)}
    ${o.children}
    @@ -113,20 +144,36 @@ function selectOrphanage(o) {
    -
    City
    ${o.city}
    -
    Country
    ${o.country}
    +
    City
    ${escapeHtml(o.city)}
    +
    Country
    ${escapeHtml(o.country)}
    Founded
    ${o.founded}
    -
    Community
    ${o.religion}
    +
    Community
    ${escapeHtml(o.religion)}
    -

    ${o.bio}

    +

    ${escapeHtml(o.bio)}

    + +
    +
    📍 Location · ${o.lat.toFixed(3)}, ${o.lng.toFixed(3)}
    +
    + +
    + +
    ${o.photos .map( (url, i) => `
    - ${o.name} photo ${i + 1} + ${escapeHtml(o.name)} photo ${i + 1} Photo ${i + 1} · click to zoom
    ` ) @@ -134,7 +181,6 @@ function selectOrphanage(o) {
    `; - // photo lightbox panelEl.querySelectorAll('.photo').forEach(el => { el.addEventListener('click', () => openLightbox(el.dataset.url)); }); @@ -150,6 +196,118 @@ function openLightbox(url) { document.body.appendChild(lb); } +// ---- search --------------------------------------------------------------- + +function highlight(text, query) { + if (!query) return escapeHtml(text); + const idx = text.toLowerCase().indexOf(query.toLowerCase()); + if (idx === -1) return escapeHtml(text); + return ( + escapeHtml(text.slice(0, idx)) + + '' + escapeHtml(text.slice(idx, idx + query.length)) + '' + + escapeHtml(text.slice(idx + query.length)) + ); +} + +function runSearch() { + const q = searchInput.value.trim(); + if (!q) { + searchResults.hidden = true; + searchClear.hidden = true; + return; + } + searchClear.hidden = false; + const ql = q.toLowerCase(); + const matches = orphanages.filter(o => + o.name.toLowerCase().includes(ql) || + o.city.toLowerCase().includes(ql) || + o.country.toLowerCase().includes(ql) + ); + if (matches.length === 0) { + searchResults.innerHTML = `
  • No match for "${escapeHtml(q)}"
  • `; + } else { + searchResults.innerHTML = matches + .slice(0, 6) + .map(o => `
  • +
    ${highlight(o.name, q)}
    +
    ${highlight(o.city, q)} · ${highlight(o.country, q)} · ${o.children} children
    +
  • `) + .join(''); + searchResults.querySelectorAll('li[data-id]').forEach(li => { + li.addEventListener('click', () => { + const o = orphanages.find(x => x.id === li.dataset.id); + if (o) { + stopTour(); + selectOrphanage(o); + searchResults.hidden = true; + } + }); + }); + } + searchResults.hidden = false; +} + +searchInput.addEventListener('input', runSearch); +searchInput.addEventListener('focus', () => { + if (searchInput.value.trim()) searchResults.hidden = false; +}); +searchInput.addEventListener('keydown', e => { + if (e.key === 'Escape') { + searchInput.value = ''; + searchResults.hidden = true; + searchClear.hidden = true; + searchInput.blur(); + } +}); +searchClear.addEventListener('click', () => { + searchInput.value = ''; + searchResults.hidden = true; + searchClear.hidden = true; + searchInput.focus(); +}); +document.addEventListener('click', e => { + if (!e.target.closest('.search-bar')) searchResults.hidden = true; +}); + +// ---- auto-tour ------------------------------------------------------------ + +let tourTimer = null; +let tourIndex = 0; +const TOUR_INTERVAL = 4200; // ms per stop +const TOUR_TRANSITION = 1400; // ms for camera fly + +function startTour() { + if (tourTimer) return; + tourIndex = 0; + tourBtn.classList.add('is-playing'); + tourIcon.textContent = '■'; + tourLabel.textContent = 'Stop Tour'; + if (hintEl) hintEl.textContent = 'Tour playing · click Stop to exit'; + stepTour(); + tourTimer = setInterval(stepTour, TOUR_INTERVAL); +} + +function stepTour() { + const o = orphanages[tourIndex % orphanages.length]; + tourIndex++; + selectOrphanage(o); +} + +function stopTour() { + if (!tourTimer) return; + clearInterval(tourTimer); + tourTimer = null; + tourBtn.classList.remove('is-playing'); + tourIcon.textContent = '▶'; + tourLabel.textContent = 'Play Tour'; + if (hintEl) hintEl.textContent = 'Click any marker to open the home'; +} + +tourBtn.addEventListener('click', () => { + if (tourTimer) stopTour(); + else startTour(); +}); + // initial empty panel renderEmpty(); @@ -158,3 +316,4 @@ window.__select = id => { const o = orphanages.find(x => x.id === id); if (o) selectOrphanage(o); }; +window.__tour = (on = true) => (on ? startTour() : stopTour()); diff --git a/src/styles.css b/src/styles.css index 4da5d04..e615242 100644 --- a/src/styles.css +++ b/src/styles.css @@ -105,6 +105,121 @@ html, body { color: var(--ink-3); } +/* ---- tour button state ---- */ +#tour-btn.is-playing { + background: var(--accent); + color: #1a1206; + border-color: var(--accent); +} +#tour-btn.is-playing:hover { + background: #e6b06a; + border-color: #e6b06a; +} + +/* ---- search bar ---- */ +.search-bar { + position: absolute; + top: 18px; + left: 18px; + z-index: 6; + display: flex; + align-items: center; + background: rgba(10, 14, 22, 0.78); + backdrop-filter: blur(8px); + border: 1px solid var(--border); + border-radius: 10px; + padding: 6px 12px; + width: 360px; + max-width: calc(100% - 36px); + font-size: 13px; + color: var(--ink-1); +} + +.search-bar:focus-within { + border-color: rgba(214, 160, 92, 0.55); + box-shadow: 0 0 0 3px rgba(214, 160, 92, 0.15); +} + +.search-icon { + margin-right: 8px; + opacity: 0.55; + font-size: 13px; +} + +#search-input { + background: transparent; + border: none; + outline: none; + color: var(--ink-0); + font: inherit; + font-size: 13.5px; + flex: 1; + padding: 6px 0; + min-width: 0; +} + +#search-input::placeholder { + color: var(--ink-3); +} + +.search-clear { + background: transparent; + border: none; + color: var(--ink-3); + cursor: pointer; + font-size: 14px; + padding: 4px 6px; + border-radius: 6px; + margin-left: 4px; +} +.search-clear:hover { color: var(--ink-0); background: rgba(255,255,255,0.06); } + +.search-results { + position: absolute; + top: calc(100% + 6px); + left: 0; + right: 0; + list-style: none; + margin: 0; + padding: 6px; + background: rgba(10, 14, 22, 0.96); + backdrop-filter: blur(10px); + border: 1px solid var(--border); + border-radius: 10px; + max-height: 320px; + overflow-y: auto; + box-shadow: var(--shadow); +} + +.search-results li { + padding: 9px 10px; + border-radius: 7px; + cursor: pointer; + display: flex; + flex-direction: column; + gap: 2px; +} + +.search-results li:hover, +.search-results li.is-active { + background: rgba(214, 160, 92, 0.14); +} + +.search-results .name { + font-size: 13.5px; + color: var(--ink-0); + font-weight: 500; +} +.search-results .meta { + font-size: 11.5px; + color: var(--ink-3); +} +.search-results mark { + background: transparent; + color: var(--accent); + font-weight: 600; +} + .layout { flex: 1; display: grid; @@ -319,6 +434,61 @@ html, body { color: var(--accent); } +/* ---- map embed ---- */ +.map-wrap { + margin-bottom: 22px; +} + +.map-wrap .map-label { + font-size: 11px; + text-transform: uppercase; + letter-spacing: 0.08em; + color: var(--ink-3); + margin-bottom: 6px; +} + +.map-frame { + width: 100%; + height: 220px; + border: 1px solid var(--border); + border-radius: 10px; + overflow: hidden; + background: var(--bg-2); +} + +.map-frame iframe { + width: 100%; + height: 100%; + border: 0; + display: block; + filter: invert(0.92) hue-rotate(180deg) brightness(0.95) saturate(0.7); +} + +.map-links { + display: flex; + gap: 8px; + margin-top: 8px; +} + +.map-link { + flex: 1; + text-align: center; + font-size: 12px; + color: var(--ink-1); + text-decoration: none; + background: var(--bg-2); + border: 1px solid var(--border); + padding: 7px 10px; + border-radius: 8px; + transition: background 0.15s, border-color 0.15s; +} + +.map-link:hover { + background: rgba(214, 160, 92, 0.14); + border-color: rgba(214, 160, 92, 0.45); + color: var(--ink-0); +} + .photos { display: grid; grid-template-columns: 1fr 1fr;