diff --git a/app/components/public/CountryMap.vue b/app/components/public/CountryMap.vue index 0d87b04a..2f4f39e4 100644 --- a/app/components/public/CountryMap.vue +++ b/app/components/public/CountryMap.vue @@ -3,7 +3,10 @@ // outlined polygon from Mapbox's country-boundaries tileset) and plots every // people group as a pin — the focus country's groups in a bright brand green, // groups in surrounding countries muted — so the viewer sees both the country's -// people groups and the general area around it. +// people groups and the general area around it. Clicking a pin opens a popup +// with that people group's details (photo, country, population, religion, +// description) plus links to its full profile and prayer page, mirroring the +// research map's detail panel. // // Self-contained (uses the npm mapbox-gl directly, not the shared embed bundle): // the country page is the only map on the page, so there's no second copy to @@ -30,6 +33,7 @@ const props = withDefaults(defineProps<{ const config = useRuntimeConfig() const mapboxToken = (config.public as { mapboxToken?: string }).mapboxToken || '' const prayBaseUrl = config.public.prayBaseUrl as string +const { t } = useI18n() const container = ref(null) // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -54,8 +58,10 @@ function mapColors() { } } +const PIN_FIELDS = 'name,slug,country_code,rop1,religion,population,location_description,image_url,has_photo,latitude,longitude' + async function loadPins() { - const url = `${prayBaseUrl}/api/people-groups/list?fields=name,slug,country_code,latitude,longitude&lang=${props.languageCode}` + const url = `${prayBaseUrl}/api/people-groups/list?fields=${PIN_FIELDS}&lang=${props.languageCode}` const res = await fetch(url) const data = await res.json() // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -66,10 +72,17 @@ async function loadPins() { .map((p: any) => ({ type: 'Feature' as const, geometry: { type: 'Point' as const, coordinates: [parseFloat(p.longitude), parseFloat(p.latitude)] }, + // Only JSON primitives — Mapbox feature properties can't hold nested objects. properties: { name: p.name, slug: p.slug, - inCountry: p.country_code?.value === props.countryCode + inCountry: p.country_code?.value === props.countryCode, + country: p.country_code?.label ?? '', + rop1: p.rop1?.label ?? '', + religion: p.religion?.label ?? '', + population: p.population ?? 0, + description: p.location_description ?? '', + imageUrl: (p.has_photo && p.image_url) ? p.image_url : '' } })) return { type: 'FeatureCollection' as const, features } @@ -80,6 +93,41 @@ function escapeHtml(s: string): string { .replace(/"/g, '"').replace(/'/g, ''') } +function formatPopulation(pop: number): string { + if (!pop || Number.isNaN(pop)) return '' + return Number(pop).toLocaleString(props.languageCode || 'en') +} + +// Build the pin popup's inner HTML from a feature's properties. Mirrors the +// research map's detail panel: photo, name, country · people-group, the +// population/religion stats, a short description, and Pray / Full Profile links. +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function popupHtml(p: any): string { + const name = escapeHtml(p.name || '') + const subtitle = [p.country, p.rop1].filter(Boolean).map(escapeHtml).join(' · ') + const pop = formatPopulation(p.population) + const profileUrl = `${props.researchUrl}${p.slug}` + const prayUrl = `${prayBaseUrl}/${p.slug}?source=doxalife` + + const rows: string[] = [] + if (pop) rows.push(`
${escapeHtml(t('Population'))}
${pop}
`) + if (p.religion) rows.push(`
${escapeHtml(t('Religion'))}
${escapeHtml(p.religion)}
`) + + return `
+ ${p.imageUrl ? `${name}` : ''} +
+
${name}
+ ${subtitle ? `
${subtitle}
` : ''} + ${rows.length ? `
${rows.join('')}
` : ''} + ${p.description ? `

${escapeHtml(p.description)}

` : ''} + +
+
` +} + async function initMap() { if (!container.value || !mapboxToken) return @@ -149,11 +197,9 @@ async function initMap() { map.on('click', 'people-group-pins', (e: any) => { const f = e.features?.[0] if (!f) return - const { name, slug } = f.properties - const href = `${props.researchUrl}${slug}` - new mapboxgl.Popup({ closeButton: true, offset: 10 }) + new mapboxgl.Popup({ closeButton: true, maxWidth: '264px', offset: 12 }) .setLngLat(f.geometry.coordinates) - .setHTML(`${escapeHtml(name)}
${escapeHtml(name)} →`) + .setHTML(popupHtml(f.properties)) .addTo(map) }) map.on('mouseenter', 'people-group-pins', () => { map.getCanvas().style.cursor = 'pointer' }) @@ -187,3 +233,104 @@ onBeforeUnmount(() => { if (map) { map.remove(); map = null } }) } } + +