show people group details in country map pin popup
- enrich pin popup with photo, country, people group, population, religion, description, and pray / full-profile links - fetch the extra fields for pins; reuse existing translated labels - render photos as a centred portrait (4:5) thumbnail, never landscape
This commit is contained in:
parent
39494fba65
commit
94102174be
1 changed files with 154 additions and 7 deletions
|
|
@ -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<HTMLElement | null>(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(`<div><dt>${escapeHtml(t('Population'))}</dt><dd>${pop}</dd></div>`)
|
||||
if (p.religion) rows.push(`<div><dt>${escapeHtml(t('Religion'))}</dt><dd>${escapeHtml(p.religion)}</dd></div>`)
|
||||
|
||||
return `<div class="cm-popup">
|
||||
${p.imageUrl ? `<img class="cm-popup__img" src="${escapeHtml(p.imageUrl)}" alt="${name}" loading="lazy">` : ''}
|
||||
<div class="cm-popup__body">
|
||||
<div class="cm-popup__name">${name}</div>
|
||||
${subtitle ? `<div class="cm-popup__sub">${subtitle}</div>` : ''}
|
||||
${rows.length ? `<dl class="cm-popup__stats">${rows.join('')}</dl>` : ''}
|
||||
${p.description ? `<p class="cm-popup__desc">${escapeHtml(p.description)}</p>` : ''}
|
||||
<div class="cm-popup__actions">
|
||||
<a class="cm-popup__btn cm-popup__btn--primary" href="${prayUrl}">${escapeHtml(t('Pray'))}</a>
|
||||
<a class="cm-popup__btn" href="${profileUrl}">${escapeHtml(t('Full Profile'))}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>`
|
||||
}
|
||||
|
||||
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(`<strong>${escapeHtml(name)}</strong><br><a href="${href}">${escapeHtml(name)} →</a>`)
|
||||
.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 } })
|
|||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style>
|
||||
/* Pin popup. Injected into the Mapbox popup as raw HTML, which scoped styles
|
||||
can't reach, so these rules are global — namespaced under .cm-popup. */
|
||||
.mapboxgl-popup-content:has(.cm-popup) {
|
||||
padding: 12px;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.18);
|
||||
}
|
||||
|
||||
.cm-popup {
|
||||
width: 240px;
|
||||
}
|
||||
|
||||
/* Portrait thumbnail (4:5), centred — never a wide landscape crop. */
|
||||
.cm-popup__img {
|
||||
display: block;
|
||||
width: 150px;
|
||||
aspect-ratio: 4 / 5;
|
||||
object-fit: cover;
|
||||
border-radius: 8px;
|
||||
margin: 0 auto 8px;
|
||||
}
|
||||
|
||||
.cm-popup__body {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.cm-popup__name {
|
||||
font-weight: 700;
|
||||
font-size: 15px;
|
||||
line-height: 1.25;
|
||||
color: var(--color-black, #1f1f1f);
|
||||
}
|
||||
|
||||
.cm-popup__sub {
|
||||
margin-top: 2px;
|
||||
font-size: 12px;
|
||||
color: var(--color-brand-lighter, #6d766e);
|
||||
}
|
||||
|
||||
.cm-popup__stats {
|
||||
margin: 8px 0 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 3px;
|
||||
}
|
||||
|
||||
.cm-popup__stats > div {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.cm-popup__stats dt {
|
||||
margin: 0;
|
||||
color: var(--color-brand-lighter, #6d766e);
|
||||
}
|
||||
|
||||
.cm-popup__stats dd {
|
||||
margin: 0;
|
||||
text-align: right;
|
||||
font-weight: 600;
|
||||
color: var(--color-black, #1f1f1f);
|
||||
}
|
||||
|
||||
.cm-popup__desc {
|
||||
margin: 8px 0 0;
|
||||
font-size: 12px;
|
||||
line-height: 1.45;
|
||||
color: #555;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 3;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.cm-popup__actions {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.cm-popup__btn {
|
||||
flex: 1;
|
||||
padding: 6px 8px;
|
||||
border-radius: 6px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
background: #ecf0f1;
|
||||
color: var(--color-black, #1f1f1f);
|
||||
}
|
||||
|
||||
.cm-popup__btn--primary {
|
||||
background: var(--color-brand, #3b463d);
|
||||
color: #fff;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
Loading…
Reference in a new issue