doxa-maps-cdn/app/pages/research/index.vue
doxa-maps fa3591ca9b Subdomain branch: the gallery and maps addresses are read per request, as on the Plugin Studio branch
dx-base's three commits from research-gallery-plugin-studio (the runtime loader, check:origins and
its lint pass) apply here unchanged: the code is byte-identical to that branch. What differs is what
the two settings point at on this route. The Research Gallery is the research site the team's
dx-maps container serves, and the maps load from the container's published tree, which the research
subdomain serves at /dx-maps (the Studio's host serves the same tree at /cdn/dx-maps; both send
CORS). The comments, .env.example and DX-MAPS-GALLERY.md now say so, with this route's laptop
values (research.localhost:4615, as compose.laptop.yaml already sets).

DX-MAPS-GALLERY.md's "Run it" said no .env is needed for /research. That stopped being true when the
default maps host was removed, so it now names the one setting the maps need.

check:origins: 15 problems on research-gallery-subdomain before, PASS (306 files) after.
2026-09-29 05:19:41 +00:00

169 lines
6.5 KiB
Vue

<script setup lang="ts">
// The Research page IS the Research Gallery: every research map, dashboard and list in one app,
// filling the page below the header. It replaces the single research map, the UUPG list and the
// "By Region" link that were here. Those three are NOT deleted: they are kept below, switched off by
// SHOW_LEGACY_RESEARCH, so the previous page returns by flipping one constant.
//
// The gallery is a web component served from the deployment's dx-maps tree (NUXT_PUBLIC_DX_MAPS_BASE,
// the team container's published tree), asked from this site's server at runtime: this page is prerendered,
// and a prerendered page cannot carry a deployment's address. It reads its manifest and every child map
// from the same tree. The menu's Research item goes to the gallery's own subdomain instead; this page
// is kept, unlinked. See DX-MAPS-GALLERY.md at the repo root.
//
// The element is created in onMounted rather than written in the template: it is a custom element
// that only exists in the browser, so this keeps it out of SSR and out of Vue's component resolver
// without any nuxt.config change.
import { mapboxTokenIfChosen } from '~/utils/mapProvider'
import { buildUupgListTranslations } from '~/utils/uupgListTranslations'
// ONE scrollbar: no footer here, and the page itself never scrolls; the gallery's own content does.
definePageMeta({ hideFooter: true })
const { t, locale } = useI18n()
const runtimeConfig = useRuntimeConfig()
// The previous Research page (the "By Region" link, the single research map and the UUPG list).
// Off on this branch: the Research Gallery replaces all three. Set true to bring them back.
const SHOW_LEGACY_RESEARCH = false
const localePath = useLocalePath()
const route = useRoute()
const initialSearchTerm = computed(() =>
(Array.isArray(route.query.uupg_search) ? route.query.uupg_search[0] : route.query.uupg_search) ?? ''
)
const translations = computed(() => buildUupgListTranslations(t))
const researchProfileConfig = computed(() => JSON.stringify({
profile: 'research-map',
tk: (runtimeConfig.public as { mapboxToken?: string }).mapboxToken || '',
dataSource: 'pray-tools',
instanceId: 'research-page'
}))
if (SHOW_LEGACY_RESEARCH) useTextHighlight()
useHead({ title: () => t('Research') })
const host = ref<HTMLElement | null>(null)
let gallery: HTMLElement | null = null
let resizeObs: ResizeObserver | null = null
// The gallery fills the viewport below the header. The header's height differs between desktop
// and phone (and with the language), so it is measured, not assumed.
function fitBelowHeader() {
const header = document.getElementById('masthead')
const h = header ? Math.round(header.getBoundingClientRect().height) : 0
document.documentElement.style.setProperty('--rg-header-h', h + 'px')
}
onMounted(() => {
fitBelowHeader()
// The gallery is sized to the viewport below the header; a sub-pixel header height must not add a
// page scrollbar, so the page does not scroll while the gallery is shown.
document.documentElement.classList.add('rg-no-page-scroll')
const header = document.getElementById('masthead')
if (header && 'ResizeObserver' in window) {
resizeObs = new ResizeObserver(fitBelowHeader)
resizeObs.observe(header)
}
gallery = document.createElement('doxa-maps-spa')
// The site's language drives the gallery's (and every map's); it follows the language switcher.
gallery.setAttribute('language', locale.value)
// MapLibre (keyless) by default; Mapbox only when NUXT_PUBLIC_MAP_PROVIDER=mapbox and a token is set.
const token = mapboxTokenIfChosen(runtimeConfig.public)
if (token) gallery.setAttribute('profile-config', JSON.stringify({ tk: token }))
else gallery.setAttribute('provider', 'maplibre')
// The tree, its script and every embed URL come from the deployment's address, asked at RUNTIME, so
// the same build works on any host (laptop, staging, production) with nothing hardcoded.
dxMapsBaseOrThrow().then((tree) => {
if (!gallery) return
gallery.setAttribute('tree-base', tree)
host.value?.appendChild(gallery)
if (!document.querySelector('script[data-research-gallery]')) {
const s = document.createElement('script')
s.src = `${tree}/doxa-maps-spa/doxa-maps-spa.js`
s.dataset.researchGallery = '1'
document.head.appendChild(s)
}
}).catch((err) => {
const p = document.createElement('p')
p.className = 'research-gallery-error'
p.setAttribute('role', 'status')
p.textContent = String((err as Error)?.message || err)
host.value?.replaceChildren(p)
})
})
watch(locale, (lang) => { gallery?.setAttribute('language', lang) })
onBeforeUnmount(() => {
document.documentElement.classList.remove('rg-no-page-scroll')
resizeObs?.disconnect()
gallery?.remove()
gallery = null
})
</script>
<template>
<div>
<div class="research-gallery-page">
<h1 class="visually-hidden">{{ t('Research') }}</h1>
<div ref="host" class="research-gallery-host" />
</div>
<!-- The previous Research page, switched off (SHOW_LEGACY_RESEARCH): kept, not deleted. -->
<div v-if="SHOW_LEGACY_RESEARCH" class="container page-content uupgs-page stack stack--3xl">
<div class="stack stack--xs text-center">
<h1 class="highlight" data-highlight-last>{{ t('Find a UUPG') }}</h1>
<p><NuxtLink class="light-link" :to="localePath('/regions')">{{ t('By Region') }} &rarr;</NuxtLink></p>
</div>
<DoxaMapSlot
map-id="research-page-map"
bundle="research-map"
:profile-config="researchProfileConfig"
class="rounded-xlg"
/>
<UupgsList
:language-code="locale"
:research-url="localePath('/research') + '/'"
:t="translations"
:initial-search-term="initialSearchTerm"
hide-see-all-link
/>
</div>
</div>
</template>
<style scoped>
.research-gallery-page {
/* 100svh: the phone's SMALL viewport, so the browser's own toolbars never cover the gallery. */
height: calc(100svh - var(--rg-header-h, 0px));
}
.research-gallery-host,
.research-gallery-host :deep(doxa-maps-spa) {
display: block;
width: 100%;
height: 100%;
}
.research-gallery-host :deep(.research-gallery-error) {
max-width: 32rem;
margin: 2rem auto;
padding: 0 1rem;
text-align: center;
}
.visually-hidden {
position: absolute;
width: 1px;
height: 1px;
overflow: hidden;
clip: rect(0 0 0 0);
white-space: nowrap;
}
</style>
<style>
/* Not scoped: set on <html> by this page only while the gallery is mounted. */
html.rg-no-page-scroll,
html.rg-no-page-scroll body {
overflow: hidden;
}
</style>