doxa-maps/plugin-designs/upg-neighbor/upg-neighbor.vue
Doxa Maps dbead061ed
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
Update from source 810ffa26
2026-09-27 07:05:27 +00:00

95 lines
5.8 KiB
Vue
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<script setup>
/**
* upg-neighbor.vue — My UPG Neighbors: mobilize the reached neighbors nearest to each unreached group.
*
* @description Near-neighbor mobilization on Winter's E-scale: who to mobilize, in what language, where
*
* Two views over one dataset (data/tables/near-neighbors.json, built from peoplegroups.org by
* scripts/build-near-neighbors.mjs):
* Mobilize pick an unreached group; see its E-1 / E-2 / E-3 reached neighbors and export them as
* campaign audience segments.
* Campaigns bridge languages: languages spoken by believers AND by unreached groups, ranked by the
* unreached population one campaign in that language can reach.
* @profile upg-neighbor
* @bundle upg-neighbor
* @element upg-neighbor
*
* profile-config: { "profile":"upg-neighbor", "view":"mobilize"|"campaigns", "pgid":"PG…", "lang":"hin" }
* The five earlier lenses (Send Map, the cockpit, Flower Mesh, Lang Cluster, Flowers) were retired
* 2026-09-27: they ran on a 40-country snapshot and answered one question five ways. Their view names
* still open the nearest new view, so existing embeds keep working.
*/
import { inject, reactive, computed, provide } from 'vue'
import { useShadowStyles } from '@map/composables/useShadowStyles.js'
import Mobilize from './src/mobilize.vue'
import Campaigns from './src/campaigns.vue'
import { NAVY } from './src/nearData.js'
const profileConfig = inject('profileConfig', null)
const cfg = computed(() => profileConfig?.value || {})
const VIEWS = ['mobilize', 'campaigns']
// Old view names → the view that now answers their question.
const LEGACY = { 'send-map': 'mobilize', cockpit: 'mobilize', 'upg-neighbors': 'mobilize', 'bloom-cockpit': 'mobilize',
'flower-mesh': 'mobilize', 'lang-cluster': 'campaigns', flowers: 'campaigns' }
const asked = cfg.value.view || cfg.value.legacyElement
const initial = VIEWS.includes(asked) ? asked : (LEGACY[asked] || (cfg.value.lang ? 'campaigns' : 'mobilize'))
// Shared by both views, so a choice made in one is still there in the other.
const nbState = reactive({ view: initial, pgid: cfg.value.pgid || null, lang: cfg.value.lang || null, about: false })
provide('nbState', nbState)
useShadowStyles(`
.dun-root { position:absolute; inset:0; display:flex; flex-direction:column; overflow:hidden; background:#fff; isolation:isolate;
font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif; }
.dun-bar { flex:0 0 auto; display:flex; align-items:center; gap:10px; padding:8px 12px; background:${NAVY}; z-index:50; flex-wrap:wrap; }
.dun-brand { font:700 13px/1 inherit; color:#fff; white-space:nowrap; margin-right:4px; }
.dun-seg { display:inline-flex; background:rgba(255,255,255,.08); border-radius:999px; padding:3px; gap:2px; }
.dun-seg button { border:0; background:transparent; color:#c7d3ea; cursor:pointer; font:600 13px/1 inherit; padding:7px 14px; border-radius:999px; }
.dun-seg button.on { background:#fff; color:${NAVY}; }
.dun-seg button:focus-visible, .dun-about:focus-visible { outline:2px solid #fff; outline-offset:2px; }
.dun-about { margin-left:auto; border:1px solid rgba(255,255,255,.35); background:transparent; color:#fff; border-radius:999px; padding:6px 12px; font:600 12px/1 inherit; cursor:pointer; }
.dun-stage { flex:1 1 0; position:relative; min-height:0; }
.dun-modal { position:absolute; inset:0; z-index:60; background:rgba(0,20,50,.45); display:flex; align-items:center; justify-content:center; padding:16px; }
.dun-card { background:#fff; border-radius:12px; max-width:620px; max-height:100%; overflow-y:auto; padding:18px 20px; font-size:14px; line-height:1.5; color:#1f2937; }
.dun-card h2 { margin:0 0 8px; font-size:18px; }
.dun-card h3 { margin:14px 0 4px; font-size:14px; }
.dun-card p { margin:0 0 8px; }
.dun-card .src { font-size:12px; color:#6b7280; }
.dun-close { float:right; border:0; background:none; font-size:20px; line-height:1; cursor:pointer; color:#6b7280; }
@media (max-width:640px){ .dun-brand{ display:none; } .dun-seg button { padding:7px 10px; } }
`, 'upg-neighbor')
</script>
<template>
<div class="dun-root">
<div class="dun-bar">
<span class="dun-brand">{{ $t('neighbor.brand') }}</span>
<div class="dun-seg" role="tablist" :aria-label="$t('neighbor.chooseView')">
<button v-for="v in VIEWS" :key="v" role="tab" :aria-selected="nbState.view === v"
:class="{ on: nbState.view === v }" @click="nbState.view = v">{{ $t(v === 'mobilize' ? 'neighbor.views.mobilize' : 'neighbor.views.campaigns') }}</button>
</div>
<button class="dun-about" @click="nbState.about = true">{{ $t('neighbor.about.button') }}</button>
</div>
<div class="dun-stage">
<Mobilize v-if="nbState.view === 'mobilize'" />
<Campaigns v-else />
<div v-if="nbState.about" class="dun-modal" role="dialog" aria-modal="true" :aria-label="$t('neighbor.about.title')" @click.self="nbState.about = false">
<div class="dun-card">
<button class="dun-close" :aria-label="$t('neighbor.about.close')" @click="nbState.about = false">×</button>
<h2>{{ $t('neighbor.about.title') }}</h2>
<p>{{ $t('neighbor.about.why') }}</p>
<h3>{{ $t('neighbor.about.eTitle') }}</h3>
<p><b>E-1</b> · {{ $t('neighbor.e.long1') }}<br><b>E-2</b> · {{ $t('neighbor.e.long2') }}<br><b>E-3</b> · {{ $t('neighbor.e.long3') }}</p>
<h3>{{ $t('neighbor.about.pTitle') }}</h3>
<p><b>P-1</b> · {{ $t('neighbor.p.long1') }}<br><b>P-2</b> · {{ $t('neighbor.p.long2') }}<br><b>P-3</b> · {{ $t('neighbor.p.long3') }}</p>
<h3>{{ $t('neighbor.about.dataTitle') }}</h3>
<p>{{ $t('neighbor.about.data') }}</p>
<p class="src">{{ $t('neighbor.about.source') }}</p>
</div>
</div>
</div>
</div>
</template>