This commit is contained in:
corsacca 2026-04-21 14:24:53 +01:00
parent 7eb566a96b
commit d5e06c8c71
10 changed files with 1607 additions and 13 deletions

View file

@ -0,0 +1,261 @@
#prayer-map {
position: relative;
width: 100%;
height: 80vh;
min-height: 400px;
border-radius: 0.5rem;
overflow: hidden;
font: var(--font-size-sm) var(--font-family-base);
}
#prayer-map .mapboxgl-canvas {
outline: none;
}
#prayer-map .mapboxgl-ctrl-bottom-left {
left: auto;
right: 0;
}
/* Legend */
.prayer-map-legend {
position: absolute;
bottom: 1.5rem;
left: 1rem;
background: rgba(255, 255, 255, 0.95);
padding: 0.75rem 1rem;
border-radius: 0.5rem;
line-height: 1;
display: flex;
flex-direction: column;
gap: 0.5rem;
z-index: 1;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.15);
}
.prayer-map-legend__item {
display: flex;
align-items: center;
gap: 0.5rem;
}
.prayer-map-legend__dot {
width: 10px;
height: 10px;
border-radius: 50%;
flex-shrink: 0;
}
/* Search */
.prayer-map-search {
position: absolute;
top: 1rem;
left: 1rem;
z-index: 2;
width: 420px;
max-width: calc(100% - 5rem);
}
.prayer-map-search__input {
width: 100%;
padding: 0.875rem 1.25rem;
border: none;
border-radius: 0.5rem;
background: rgba(255, 255, 255, 0.95);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
font-family: inherit;
font-size: inherit;
color: var(--color-text-on-white, #2a332a);
outline: none;
box-sizing: border-box;
}
.prayer-map-search__input::placeholder {
color: var(--color-brand-lighter, #798279);
}
.prayer-map-search__input:focus {
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.25);
}
.prayer-map-search__results {
display: none;
margin-top: 0.375rem;
background: #fff;
border-radius: 0.5rem;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
overflow: hidden;
max-height: 320px;
overflow-y: auto;
}
.prayer-map-search__item {
display: flex;
flex-direction: column;
width: 100%;
padding: 0.75rem 1.25rem;
border: none;
background: none;
text-align: left;
cursor: pointer;
gap: 0.125rem;
font-family: var(--font-family-base, 'Poppins', sans-serif);
color: var(--color-text-on-white, #2a332a);
}
.prayer-map-search__item:hover {
background: var(--color-brand-secondary-very-light, #F3F3F1);
}
.prayer-map-search__item-sub {
font-size: 0.8125em;
color: var(--color-brand-lighter, #798279);
}
/* Modal overlay */
.prayer-map-overlay {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.5);
z-index: 9999;
display: flex;
align-items: center;
justify-content: center;
padding: 1rem;
opacity: 0;
pointer-events: none;
transition: opacity 0.2s ease;
}
.prayer-map-overlay.is-visible {
opacity: 1;
pointer-events: auto;
}
/* Modal card */
.prayer-map-modal {
background: #fff;
border-radius: 0.75rem;
overflow: hidden;
width: fit-content;
max-height: 90vh;
display: flex;
flex-direction: row;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
transform: translateY(10px);
transition: transform 0.2s ease;
}
.prayer-map-overlay.is-visible .prayer-map-modal {
transform: translateY(0);
}
.prayer-map-modal__image {
width: 180px;
flex-shrink: 0;
object-fit: cover;
display: block;
border-radius: 0.75rem 0 0 0.75rem;
}
.prayer-map-modal__body {
padding: var(--spacing-xl) var(--spacing-md);
display: flex;
flex-direction: column;
gap: 0.75rem;
flex-grow: 1;
}
.prayer-map-modal__name {
margin: 0;
}
.prayer-map-modal__details {
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.prayer-map-modal__actions {
display: flex;
gap: var(--spacing-md);
}
.prayer-map-modal__actions a {
flex: 1;
display: inline-flex;
align-items: center;
justify-content: center;
padding: 0.625rem 1rem;
border-radius: 100px;
font-weight: 600;
text-decoration: none;
text-align: center;
transition: opacity 0.15s ease;
}
.prayer-map-modal__actions a:hover {
opacity: 0.85;
}
.prayer-map-modal__btn-pray {
background: var(--color-brand);
color: var(--color-white);
}
.prayer-map-modal__btn-info {
background: var(--color-brand-secondary);
color: var(--color-brand-dark);
}
.prayer-map-modal__close {
position: absolute;
top: 0.5rem;
right: 0.5rem;
background: rgba(0, 0, 0, 0.4);
border: none;
color: #fff;
width: 2.5rem;
height: 2.5rem;
border-radius: 50%;
cursor: pointer;
font-size: 2.5rem;
display: flex;
align-items: center;
justify-content: center;
line-height: 1;
}
.prayer-map-modal__close:hover {
background: rgba(0, 0, 0, 0.6);
}
.mapboxgl-ctrl-attrib-inner {
font-size: 0.8125em;
}
/* Mobile: bottom sheet style */
@media (max-width: 640px) {
.prayer-map-overlay {
align-items: flex-end;
padding: 0;
}
.prayer-map-modal {
max-width: 100%;
flex-direction: row;
border-radius: 1rem 1rem 0 0;
max-height: 85vh;
transform: translateY(100%);
}
.prayer-map-overlay.is-visible .prayer-map-modal {
transform: translateY(0);
}
.prayer-map-modal__image {
width: 120px;
height: auto;
border-radius: 1rem 0 0 0;
}
}

View file

@ -0,0 +1,434 @@
<script setup lang="ts">
// Port of marketing-theme/js/prayer-map.js. Mapbox GL Mercator globe
// showing ~2k people groups coloured by prayer coverage. Includes the
// same search box (local name match + Mapbox geocoding fallback) and
// modal overlay with `Pray for them` / `Info` links.
//
// The markup is kept minimal — #prayer-map is the container, the legend,
// search, and modal are built imperatively to match the source exactly
// (same classes, same DOM structure). prayer-map.css is imported at
// module scope and applies via the component's scope.
import mapboxgl, { type Map as MapboxMap } from 'mapbox-gl'
import 'mapbox-gl/dist/mapbox-gl.css'
interface PrayerMapT {
no_prayer?: string
has_prayer?: string
search_placeholder?: string
close?: string
language?: string
country?: string
population?: string
prayer_coverage?: string
pray_for_them?: string
info?: string
unknown?: string
}
const props = withDefaults(defineProps<{
researchUrl?: string
languageCode?: string
t?: PrayerMapT
}>(), {
researchUrl: '/research',
languageCode: 'en',
t: () => ({})
})
const config = useRuntimeConfig()
const mapboxToken = config.public.mapboxToken as string
const prayBaseUrl = config.public.prayBaseUrl as string
const COLOR_NO_PRAYER = '#e57373'
const COLOR_HAS_PRAYER = '#4caf50'
const containerRef = ref<HTMLDivElement | null>(null)
interface FeatureProps {
slug: string
name: string
people_committed?: number
population?: number
language: string | null
country: string | null
picture_url?: string
hasPrayer: 0 | 1
}
type PGFeature = {
type: 'Feature'
geometry: { type: 'Point'; coordinates: [number, number] }
properties: FeatureProps
}
let map: MapboxMap | null = null
let allFeatures: PGFeature[] = []
let highlightedSlug: string | null = null
let overlayEl: HTMLDivElement | null = null
let debounceTimer: ReturnType<typeof setTimeout> | null = null
let geocodeController: AbortController | null = null
function escapeHtml(str: string): string {
const div = document.createElement('div')
div.textContent = str
return div.innerHTML
}
function formatNumber(n: number | null | undefined): string {
if (n == null) return props.t.unknown || 'Unknown'
return Number(n).toLocaleString()
}
function highlightFeature(feature: PGFeature) {
highlightedSlug = feature.properties.slug
if (!map) return
map.setPaintProperty('people-groups-dots', 'circle-color', [
'case',
['==', ['get', 'slug'], highlightedSlug],
'#ff9800',
['==', ['get', 'hasPrayer'], 1],
COLOR_HAS_PRAYER,
COLOR_NO_PRAYER
])
map.setPaintProperty('people-groups-dots', 'circle-radius', [
'case',
['==', ['get', 'slug'], highlightedSlug],
12,
['interpolate', ['linear'], ['zoom'], 1, 3, 5, 5, 10, 8]
])
}
function clearHighlight() {
if (!highlightedSlug || !map) return
highlightedSlug = null
map.setPaintProperty('people-groups-dots', 'circle-color', [
'case',
['==', ['get', 'hasPrayer'], 1],
COLOR_HAS_PRAYER,
COLOR_NO_PRAYER
])
map.setPaintProperty('people-groups-dots', 'circle-radius', [
'interpolate', ['linear'], ['zoom'],
1, 3, 5, 5, 10, 8
])
}
function buildLegend(container: HTMLElement) {
const legend = document.createElement('div')
legend.className = 'prayer-map-legend'
legend.innerHTML
= '<div class="prayer-map-legend__item">'
+ `<span class="prayer-map-legend__dot" style="background:${COLOR_NO_PRAYER}"></span>`
+ `<span>${props.t.no_prayer || 'No prayer coverage'}</span>`
+ '</div>'
+ '<div class="prayer-map-legend__item">'
+ `<span class="prayer-map-legend__dot" style="background:${COLOR_HAS_PRAYER}"></span>`
+ `<span>${props.t.has_prayer || 'Has prayer coverage'}</span>`
+ '</div>'
container.appendChild(legend)
}
function buildSearch(container: HTMLElement) {
const searchWrap = document.createElement('div')
searchWrap.className = 'prayer-map-search'
searchWrap.innerHTML
= `<input class="prayer-map-search__input" type="search" placeholder="${props.t.search_placeholder || 'Search people groups or locations'}" autocomplete="off">`
+ '<div class="prayer-map-search__results"></div>'
container.appendChild(searchWrap)
const searchInput = searchWrap.querySelector<HTMLInputElement>('.prayer-map-search__input')!
const searchResults = searchWrap.querySelector<HTMLDivElement>('.prayer-map-search__results')!
function closeSearch() {
searchResults.innerHTML = ''
searchResults.style.display = 'none'
}
type SearchItem = { type: 'people'; name: string; sub: string; index: number } | { type: 'location'; name: string; lng: number; lat: number }
function showResults(items: SearchItem[]) {
if (items.length === 0) {
closeSearch()
return
}
searchResults.innerHTML = items.map((item) => {
const idx = item.type === 'people' ? String(item.index) : ''
const lng = item.type === 'location' ? String(item.lng) : ''
const lat = item.type === 'location' ? String(item.lat) : ''
const sub = item.type === 'people' ? item.sub : ''
return `<button class="prayer-map-search__item" data-type="${item.type}" data-index="${idx}" data-lng="${lng}" data-lat="${lat}">`
+ `<span class="prayer-map-search__item-name">${escapeHtml(item.name)}</span>`
+ (sub ? `<span class="prayer-map-search__item-sub">${escapeHtml(sub)}</span>` : '')
+ '</button>'
}).join('')
searchResults.style.display = 'block'
}
searchResults.addEventListener('click', (e) => {
const btn = (e.target as HTMLElement).closest<HTMLButtonElement>('.prayer-map-search__item')
if (!btn) return
const type = btn.getAttribute('data-type')
if (type === 'people') {
const i = parseInt(btn.getAttribute('data-index') || '', 10)
const feature = allFeatures[i]
if (feature && map) {
const coords = feature.geometry.coordinates
map.flyTo({ center: coords, zoom: 8 })
highlightFeature(feature)
}
} else {
const lng = parseFloat(btn.getAttribute('data-lng') || '')
const lat = parseFloat(btn.getAttribute('data-lat') || '')
if (!isNaN(lng) && !isNaN(lat) && map) {
map.flyTo({ center: [lng, lat], zoom: 5 })
}
}
searchInput.value = ''
closeSearch()
})
searchInput.addEventListener('input', () => {
const query = searchInput.value.trim()
if (debounceTimer) clearTimeout(debounceTimer)
if (geocodeController) {
geocodeController.abort()
geocodeController = null
}
if (query.length < 2) {
closeSearch()
return
}
debounceTimer = setTimeout(() => {
const lower = query.toLowerCase()
const peopleResults: SearchItem[] = []
for (let i = 0; i < allFeatures.length && peopleResults.length < 5; i++) {
const name = allFeatures[i]!.properties.name
if (name && name.toLowerCase().includes(lower)) {
peopleResults.push({
type: 'people',
name,
sub: allFeatures[i]!.properties.country || '',
index: i
})
}
}
if (peopleResults.length >= 2) {
showResults(peopleResults)
return
}
geocodeController = new AbortController()
const geocodeUrl = `https://api.mapbox.com/geocoding/v5/mapbox.places/${encodeURIComponent(query)}.json?access_token=${mapboxToken}&limit=3`
fetch(geocodeUrl, { signal: geocodeController.signal })
.then(res => res.json())
.then((data) => {
const locationResults: SearchItem[] = (data.features || []).map((f: any) => ({
type: 'location' as const,
name: f.place_name,
lng: f.center[0],
lat: f.center[1]
}))
showResults(peopleResults.concat(locationResults))
})
.catch((err) => {
if (err?.name !== 'AbortError') showResults(peopleResults)
})
}, 300)
})
searchInput.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
searchInput.value = ''
closeSearch()
searchInput.blur()
}
})
document.addEventListener('click', (e) => {
if (!searchWrap.contains(e.target as Node)) closeSearch()
})
}
function buildOverlay() {
const overlay = document.createElement('div')
overlay.className = 'prayer-map-overlay'
overlay.innerHTML
= '<div class="prayer-map-modal" role="dialog">'
+ `<button class="prayer-map-modal__close" aria-label="${props.t.close || 'Close'}">&times;</button>`
+ '<img class="prayer-map-modal__image" src="" alt="">'
+ '<div class="prayer-map-modal__body stack stack--md">'
+ '<h3 class="prayer-map-modal__name"></h3>'
+ '<div class="prayer-map-modal__details"></div>'
+ '<div class="prayer-map-modal__actions">'
+ `<a id="prayer-map-modal__btn-pray" class="button" href="#" target="_blank">${props.t.pray_for_them || 'Pray for them'}</a>`
+ `<a id="prayer-map-modal__btn-info" class="button outline" href="#" target="_blank">${props.t.info || 'Info'}</a>`
+ '</div>'
+ '</div>'
+ '</div>'
document.body.appendChild(overlay)
const modalImage = overlay.querySelector<HTMLImageElement>('.prayer-map-modal__image')!
const modalName = overlay.querySelector<HTMLHeadingElement>('.prayer-map-modal__name')!
const modalDetails = overlay.querySelector<HTMLDivElement>('.prayer-map-modal__details')!
const btnPray = overlay.querySelector<HTMLAnchorElement>('#prayer-map-modal__btn-pray')!
const btnInfo = overlay.querySelector<HTMLAnchorElement>('#prayer-map-modal__btn-info')!
function closeModal() {
overlay.classList.remove('is-visible')
}
overlay.querySelector<HTMLButtonElement>('.prayer-map-modal__close')!.addEventListener('click', closeModal)
overlay.addEventListener('click', (e) => {
if (e.target === overlay) closeModal()
})
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') closeModal()
})
return { overlay, modalImage, modalName, modalDetails, btnPray, btnInfo }
}
onMounted(async () => {
if (!containerRef.value || !mapboxToken) return
mapboxgl.accessToken = mapboxToken
map = new mapboxgl.Map({
container: containerRef.value,
style: 'mapbox://styles/mapbox/light-v11',
projection: 'mercator',
center: [20, 10],
zoom: 1.5,
minZoom: 1,
maxZoom: 12
})
map.addControl(new mapboxgl.NavigationControl(), 'top-right')
map.scrollZoom.disable()
containerRef.value.addEventListener('click', () => map?.scrollZoom.enable())
containerRef.value.addEventListener('mouseleave', () => map?.scrollZoom.disable())
buildLegend(containerRef.value)
buildSearch(containerRef.value)
const { overlay, modalImage, modalName, modalDetails, btnPray, btnInfo } = buildOverlay()
overlayEl = overlay
function openModal(raw: Record<string, unknown>) {
// Mapbox serializes feature.properties to strings — parse back where needed.
const props2 = raw as FeatureProps & Record<string, string>
const fallbackImage = `${prayBaseUrl}/images/default-people-group.jpg`
modalImage.src = props2.picture_url || fallbackImage
modalImage.alt = props2.name
modalName.textContent = props2.name
modalDetails.innerHTML
= `<span><strong>${props.t.language || 'Language'}:</strong> ${props2.language || props.t.unknown || 'Unknown'}</span>`
+ `<span><strong>${props.t.country || 'Country'}:</strong> ${props2.country || props.t.unknown || 'Unknown'}</span>`
+ `<span><strong>${props.t.population || 'Population'}:</strong> ${formatNumber(Number(props2.population))}</span>`
+ `<span><strong>${props.t.prayer_coverage || 'Prayer Coverage'}:</strong> ${props2.people_committed || 0}/144</span>`
btnPray.href = `${prayBaseUrl}/${props2.slug}?source=doxalife`
btnInfo.href = `${props.researchUrl.replace(/\/+$/, '')}/${props2.slug}`
overlay.classList.add('is-visible')
}
map.on('click', (e) => {
if (highlightedSlug && map) {
const features = map.queryRenderedFeatures(e.point, { layers: ['people-groups-dots'] })
if (!features.length || (features[0]!.properties as any).slug !== highlightedSlug) {
clearHighlight()
}
}
})
map.on('load', async () => {
try {
const apiUrl = `${prayBaseUrl}/api/people-groups/list?fields=slug,name,latitude,longitude,people_committed,population,image_url,country_code,primary_language&lang=${props.languageCode}`
const res = await fetch(apiUrl)
const data = await res.json()
const posts: any[] = data.posts || []
const features: PGFeature[] = []
for (const p of posts) {
const lat = parseFloat(p.latitude)
const lng = parseFloat(p.longitude)
if (isNaN(lat) || isNaN(lng)) continue
features.push({
type: 'Feature',
geometry: { type: 'Point', coordinates: [lng, lat] },
properties: {
slug: p.slug,
name: p.name,
people_committed: p.people_committed,
population: p.population,
language: p.primary_language ? p.primary_language.label : null,
country: p.country_code ? p.country_code.label : null,
picture_url: p.image_url,
hasPrayer: p.people_committed > 0 ? 1 : 0
}
})
}
allFeatures = features
if (!map) return
if (map.getSource('people-groups')) return
map.addSource('people-groups', {
type: 'geojson',
data: { type: 'FeatureCollection', features } as any
})
map.addLayer({
id: 'people-groups-dots',
type: 'circle',
source: 'people-groups',
paint: {
'circle-radius': [
'interpolate', ['linear'], ['zoom'],
1, 3, 5, 5, 10, 8
],
'circle-color': [
'case',
['==', ['get', 'hasPrayer'], 1],
COLOR_HAS_PRAYER,
COLOR_NO_PRAYER
],
'circle-opacity': 0.85,
'circle-stroke-width': 1,
'circle-stroke-color': '#ffffff',
'circle-stroke-opacity': 0.5
}
})
map.on('click', 'people-groups-dots', (e) => {
if (e.features && e.features.length > 0) {
openModal(e.features[0]!.properties as Record<string, unknown>)
}
})
map.on('mouseenter', 'people-groups-dots', () => {
if (map) map.getCanvas().style.cursor = 'pointer'
})
map.on('mouseleave', 'people-groups-dots', () => {
if (map) map.getCanvas().style.cursor = ''
})
} catch (err) {
console.error('Prayer map: failed to load people groups', err)
}
})
})
onBeforeUnmount(() => {
if (debounceTimer) clearTimeout(debounceTimer)
if (geocodeController) geocodeController.abort()
if (overlayEl && overlayEl.parentNode) overlayEl.parentNode.removeChild(overlayEl)
if (map) {
map.remove()
map = null
}
})
</script>
<template>
<div ref="containerRef" id="prayer-map" />
</template>
<style src="~/assets/styles/prayer-map.css"></style>

View file

@ -1,6 +0,0 @@
<template>
<section class="container stack stack--md">
<h1>Adopt</h1>
<p>The adopt page is ported in Phase 4.</p>
</section>
</template>

336
app/pages/adopt/[slug].vue Normal file
View file

@ -0,0 +1,336 @@
<script setup lang="ts">
// Port of marketing-theme/template-adoption-form.php (WP rewrite
// /adopt/{slug} → template-adoption-form.php). Fetches the people
// group by slug from `{prayBaseUrl}/api/people-groups/detail/{slug}`,
// renders the adoption form, and submits to `/api/adopt` which proxies
// to pray.doxa.life.
import { COUNTRIES } from '~/utils/countries'
import type { Uupg } from '~/types/uupg'
const { t, locale } = useI18n()
const localePath = useLocalePath()
const route = useRoute()
const config = useRuntimeConfig()
const slug = computed(() => String(route.params.slug || ''))
useTextHighlight()
// Fetch UUPG details. Mirrors get_uupg_by_slug() in functions.php.
interface UupgDetail extends Uupg {
error?: string
}
const { data: uupg, error: uupgError } = await useAsyncData<UupgDetail | null>(
`uupg-${slug.value}-${locale.value}`,
async () => {
if (!slug.value) return null
const url = `${config.public.prayBaseUrl}/api/people-groups/detail/${encodeURIComponent(slug.value)}?lang=${locale.value}`
try {
return await $fetch<UupgDetail>(url)
} catch {
return { error: 'not_found' } as UupgDetail
}
}
)
const notFound = computed(() => !uupg.value || 'error' in (uupg.value as UupgDetail))
// Adoption form state
const firstName = ref('')
const lastName = ref('')
const email = ref('')
const honeypot = ref('')
const phone = ref('')
const role = ref('')
const churchName = ref('')
const country = ref('')
const confirmAdoption = ref(false)
const permissionToContact = ref(false)
const confirmPublicDisplay = ref(false)
const turnstileToken = ref('')
const turnstileRef = ref<{ reset: () => void } | null>(null)
const submitting = ref(false)
const submitted = ref(false)
const verificationPending = ref(false)
const message = ref('')
const messageClass = ref<'success' | 'error' | ''>('')
const formRef = ref<HTMLFormElement | null>(null)
const submitDisabled = computed(() => submitting.value || !turnstileToken.value)
// Pre-fill phone country from the same geo service the PHP template hits.
const initialCountry = ref('')
onMounted(async () => {
try {
const geo = await $fetch<{ country?: { iso_code?: string } }>('https://geo.prayer.global/json')
if (geo?.country?.iso_code) initialCountry.value = geo.country.iso_code.toLowerCase()
} catch {
// ignore geo failure
}
})
async function onSubmit(e: Event) {
e.preventDefault()
if (honeypot.value) return
if (formRef.value && !formRef.value.checkValidity()) {
formRef.value.reportValidity()
return
}
if (!confirmAdoption.value) {
messageClass.value = 'error'
message.value = t('Please confirm your adoption commitment.')
return
}
submitting.value = true
message.value = ''
messageClass.value = ''
try {
const response = await $fetch<{ status: string; message?: string }>('/api/adopt', {
method: 'POST',
body: {
first_name: firstName.value,
last_name: lastName.value,
email: email.value,
phone: phone.value,
church_name: churchName.value,
country: country.value,
role: role.value,
confirm_adoption: confirmAdoption.value,
permission_to_contact: permissionToContact.value,
confirm_public_display: confirmPublicDisplay.value,
people_group: slug.value,
language: locale.value,
cf_turnstile: turnstileToken.value
}
})
if (response?.status === 'needs_verification') {
verificationPending.value = true
submitted.value = true
} else if (response?.status === 'success') {
messageClass.value = 'success'
submitted.value = true
// Reset fields
firstName.value = ''
lastName.value = ''
email.value = ''
phone.value = ''
role.value = ''
churchName.value = ''
country.value = ''
confirmAdoption.value = false
permissionToContact.value = false
confirmPublicDisplay.value = false
formRef.value?.reset()
} else {
messageClass.value = 'error'
message.value = response?.message ?? t('There was an error submitting your adoption. Please try again.')
}
} catch (err: any) {
messageClass.value = 'error'
message.value = err?.data?.message ?? t('There was an error submitting your adoption. Please try again.')
} finally {
submitting.value = false
turnstileToken.value = ''
turnstileRef.value?.reset()
}
}
</script>
<template>
<div v-if="notFound || uupgError" class="container page-content uupg-detail-page">
<div class="stack stack--lg">
<h1>{{ t('People Group Not Found') }}</h1>
<p>{{ t('The people group you are looking for could not be found. Please try again.') }}</p>
<NuxtLink :to="localePath('/adopt')" class="button font-size-lg">
<span class="sr-only">{{ t('Back') }}</span>
<svg class="icon | rotate-270" viewBox="0 0 489.67 289.877">
<path d="M439.017,211.678L263.258,35.919c-3.9-3.9-8.635-6.454-13.63-7.665-9.539-2.376-20.051.161-27.509,7.619L46.361,211.632c-11.311,11.311-11.311,29.65,0,40.961h0c11.311,11.311,29.65,11.311,40.961,0L242.667,97.248l155.39,155.39c11.311,11.311,29.65,11.311,40.961,0h0c11.311-11.311,11.311-29.65,0-40.961Z" />
</svg>
{{ t('Back') }}
</NuxtLink>
</div>
</div>
<div v-else-if="uupg" class="container page-content uupg-detail-page">
<div class="stack stack--lg">
<h1 class="highlight" data-highlight-index="1">{{ t('Adoption Form') }}</h1>
<p class="subtext">{{ t("Thank you for taking a step toward adopting an unengaged people group. Please complete the form below so we can confirm your church's adoption, connect with your Champion, and begin sending prayer updates and resources.") }}</p>
<div class="switcher | adoption-card shadow">
<div class="grow-none">
<img
class="uupg__image"
data-size="small"
:src="uupg.image_url"
:alt="uupg.name || 'People Group Photo'"
>
</div>
<div class="repel align-center">
<div class="stack stack--md lh-0">
<p class="font-size-xl font-weight-medium">{{ uupg.name }}</p>
<p class="font-size-lg font-weight-medium">
{{ uupg.country_code?.label }} ({{ uupg.rop1?.label }})
</p>
</div>
</div>
</div>
<!-- Post-submit verification screen (from template-adoption-form.php lines 236-254) -->
<div v-if="submitted && verificationPending" class="text-card shadow text-center">
<div class="stack">
<div class="color-primary">
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<rect width="20" height="16" x="2" y="4" rx="2" />
<path d="m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7" />
</svg>
</div>
<h2 class="highlight" data-highlight-index="1">{{ t('Success! Form submitted.') }}</h2>
<p>
{{ t('To confirm your adoption, please verify your email address. We sent a verification email to') }}
<strong>{{ email }}</strong>.
</p>
<p class="font-size-xs">
{{ t("Don't see the email? Check your spam folder.") }}
</p>
</div>
</div>
<!-- Adoption form (from template-adoption-form.php lines 61-148) -->
<form
v-else
id="adoption-form"
ref="formRef"
class="text-card shadow"
@submit="onSubmit"
>
<input v-model="honeypot" type="email" name="email" style="display:none;" autocomplete="off" tabindex="-1">
<input type="hidden" name="people_group" :value="slug">
<div class="stack stack--lg | max-width-lg mx-auto">
<section class="stack">
<h3 class="highlight" data-highlight-index="4">{{ t('What is your commitment? Pray. Give. Send.') }}</h3>
<p>{{ t('When you adopt a people group, you step into a leadership role on their behalf—standing in the gap until the gospel takes root. This involves a commitment to:') }}</p>
<ul class="stack" data-list-color="primary">
<li>
<strong>{{ t('Pray') }} – </strong>
{{ t('Mobilize toward the goal of at least 144 prayer partners to cover the people group in continuous, daily prayer (10 minutes each, 24 hours a day).') }}
</li>
<li>
<strong>{{ t('Give') }} – </strong>
{{ t('Partner financially on a monthly basis with the Doxa Foundation to help sustain prayer mobilization, campaign operations, and the sending of gospel workers.') }}
<br><br>
{{ t('We recommend that churches give a monthly pledge based on their congregational size. i.e. $100 a month for a congregation around 100. $500 a month for a congregation around 500. $1000 a month for a congregation around 1000, etc.') }}
</li>
<li>
<strong>{{ t('Send') }} – </strong>
{{ t('Actively help surface and support potential goers, encouraging those God may be calling to cross cultures and serve this people group directly.') }}
</li>
</ul>
</section>
<div class="stack">
<h3 class="h5">{{ t('Champion Details') }}</h3>
<i class="color-primary font-size-sm">{{ t('The Champion is the person who will organize the 144 intercessors and receive updates.') }}</i>
<div class="">
<label for="first-name">{{ t('First Name') }}</label>
<input id="first-name" v-model="firstName" type="text" name="first_name" required :placeholder="t('First Name')">
</div>
<div class="">
<label for="last-name">{{ t('Last Name') }}</label>
<input id="last-name" v-model="lastName" type="text" name="last_name" required :placeholder="t('Last Name')">
</div>
<div class="">
<label for="contact-email">{{ t('Email') }}</label>
<input id="contact-email" v-model="email" type="email" name="contact_email" required :placeholder="t('Enter your email')">
</div>
<div class="">
<label for="phone">{{ t('Phone') }}</label>
<PhoneInput
:value="phone"
:initialCountry="initialCountry"
:t="{
phone_error: t('Please enter a valid phone number'),
phone_error_too_short: t('Phone number is too short'),
phone_error_too_long: t('Phone number is too long')
}"
@phone-input="(d) => phone = d.number"
/>
</div>
<div class="">
<label for="role">{{ t('Role') }}</label>
<input id="role" v-model="role" type="text" name="role" required :placeholder="t('Example: Pastor, Missions Pastor, Elder, Volunteer Leader etc.')">
</div>
</div>
<div class="stack">
<h3 class="h5">{{ t('Partnering Church') }}</h3>
<div class="">
<label for="church-name">{{ t('Church/Group Name') }}</label>
<input id="church-name" v-model="churchName" type="text" name="church_name" required :placeholder="t('Enter Church/Group Name')">
</div>
<div class="form-control color-primary-darker">
<input id="confirm-public-display" v-model="confirmPublicDisplay" type="checkbox" name="confirm_public_display">
<label for="confirm-public-display">{{ t('I am happy for this church name to appear publicly on this site.') }}</label>
</div>
<div>
<label for="country">{{ t('Location of Church/Group') }}</label>
<select id="country" v-model="country" name="country" required>
<option value="" disabled selected hidden>{{ t('Select Country') }}</option>
<option v-for="c in COUNTRIES" :key="c.value" :value="c.label">{{ c.label }}</option>
</select>
</div>
</div>
<div class="form-control color-primary-darker">
<input id="confirm-adoption" v-model="confirmAdoption" type="checkbox" name="confirm_adoption">
<label for="confirm-adoption">{{ t('I/my church/group commits to adopting this People Group for prayer, partnership and support.') }}</label>
</div>
<div class="form-control color-primary-darker">
<input id="permission-to-contact" v-model="permissionToContact" type="checkbox" name="permission_to_contact">
<label for="permission-to-contact">{{ t('I give permission for DOXA to connect me with others adopting this people group.') }}</label>
</div>
<NuxtTurnstile
ref="turnstileRef"
v-model="turnstileToken"
:options="{ theme: 'light' }"
/>
<div
v-if="message"
id="adoption-message"
class="contact-message"
:class="messageClass"
style="display: block;"
v-html="message"
/>
<button
id="adoption-submit"
type="submit"
class="button compact"
:disabled="submitDisabled"
>
{{ submitting ? t('Submitting...') : t('Submit') }}
</button>
</div>
</form>
<!-- Post-submit success screen (matches the inner innerHTML write from template-adoption-form.php) -->
<div v-if="submitted && !verificationPending && messageClass === 'success'" class="contact-message success" style="display: block;">
<p>{{ t('Thank you for your adoption commitment! We will be in touch soon.') }}</p>
<NuxtLink
v-if="locale === 'en'"
:to="`${localePath('/research') || '/research'}/${slug}/resources`"
class="button compact | resources-button"
>{{ t('View Adoption Resources') }}</NuxtLink>
</div>
</div>
</div>
</template>

182
app/pages/adopt/index.vue Normal file
View file

@ -0,0 +1,182 @@
<script setup lang="ts">
// Port of marketing-theme/page-adopt.php.
import { buildUupgListTranslations } from '~/utils/uupgListTranslations'
const { t, locale } = useI18n()
const localePath = useLocalePath()
const translations = computed(() => ({
...buildUupgListTranslations(t),
select: t('Adopt')
}))
const adoptBase = computed(() => {
const p = localePath('/adopt')
return p.endsWith('/') ? p : `${p}/`
})
const { stats, adoptedPercent, reload } = usePrayerStatistics()
onMounted(() => reload())
useTextHighlight()
</script>
<template>
<div class="adopt-page bg-secondary">
<section>
<div class="container stack stack--2xl">
<div class="stack stack-md">
<h1 class="h2 highlight" data-highlight-index="1">{{ t('Adopt an unengaged people group') }}</h1>
<p class="subtext">{{ t('A church-led commitment to pray, give, and send so that gospel access begins') }}</p>
</div>
<div class="three-part-switcher">
<div class="card-two-tone | text-center grow-1">
<div class="stack stack--lg">
<h2 class="h3">{{ t('Adoption Goal') }}</h2>
<p class="subtext font-size-md">{{ t('Every unengaged people group adopted by a church committed to prayer, giving, and sending') }}</p>
</div>
<div>
<h2 class="h3">{{ t('Current Status') }}</h2>
<span class="font-size-4xl font-weight-bold font-button">
<span id="adopted-current-status">{{ stats.total_adopted }}</span> / 2085
</span>
<div class="stack stack--3xs">
<p class="subtext font-size-md">{{ t('people groups adopted') }}</p>
<div class="progress-bar" data-size="md">
<div
id="adopted-current-status-percentage"
class="progress-bar__slider"
:style="{ width: `${adoptedPercent}%` }"
/>
</div>
</div>
</div>
</div>
<div
class="grow-2 bg-image rounded-md"
style="background-image: url('/assets/images/adopt-01-africa-4women.jpg');"
/>
</div>
</div>
</section>
<section class="surface-brand-light">
<div class="container stack stack--3xl">
<h2>{{ t('How Adoption Works') }}</h2>
<div class="switcher | gap-md">
<div class="step-card">
<div class="step-card__number">1</div>
<div class="step-card__content" data-no-action>
<h2 class="step-card__title overflow-wrap-anywhere">{{ t('Choose') }}</h2>
<p>{{ t('Prayerfully select an unengaged people group to adopt.') }}</p>
</div>
</div>
<div class="step-card">
<div class="step-card__number">2</div>
<div class="step-card__content" data-no-action>
<h2 class="step-card__title overflow-wrap-anywhere">{{ t('Mobilize') }}</h2>
<p>{{ t('Raise up 144+ daily intercessors to pray 10 minutes a day.') }}</p>
</div>
</div>
<div class="step-card">
<div class="step-card__number">3</div>
<div class="step-card__content" data-no-action>
<h2 class="step-card__title overflow-wrap-anywhere">{{ t('Partner') }}</h2>
<p>{{ t('Partner through prayer, giving, and sending so that gospel access begins.') }}</p>
</div>
</div>
</div>
<a href="#choose-people-group" class="button | compact mx-auto">{{ t('Get Started') }}</a>
</div>
</section>
<section>
<div class="container">
<div class="switcher | align-center" data-width="xl">
<div class="stack | grow-2 align-center">
<div class="stack stack--2xl">
<h2 class="highlight" data-highlight-index="1">{{ t('Adoption Guide & Resources') }}</h2>
<ul class="stack stack--sm" data-list-color="primary">
<li>{{ t('Step-by-step guidance for churches adopting a people group') }}</li>
<li>{{ t('Tools to mobilize prayer and participation across your congregation') }}</li>
<li>{{ t('Printable and digital resources to promote awareness and engagement') }}</li>
<li>{{ t('Helpful tips for sustaining long-term commitment') }}</li>
</ul>
</div>
</div>
<div>
<img class="center" src="/assets/images/adopt-02-ipad-mockup.png" :alt="t('Your daily prayer guide')">
</div>
</div>
</div>
</section>
<section class="surface-white">
<div class="container">
<div class="stack stack--lg">
<h2>{{ t('Why adoption matters') }}</h2>
<div class="switcher | gap-md" data-width="xl">
<div class="switcher-item center grow-none">
<img src="/assets/images/Adopt-03-DurbetInMongolia.jpg" :alt="t('Adopt an unengaged people group')">
</div>
<div class="stack stack--lg | text-card | surface-brand-lightest justify-center">
<h4 class="font-heading font-size-2xl">{{ t('They have no gospel access') }}</h4>
<p>{{ t('Unengaged people groups have no missionaries, no churches, and often no known believers. Adoption helps ensure they are finally seen, prayed for, and intentionally pursued with the gospel.') }}</p>
</div>
</div>
<div class="switcher | gap-md" data-width="xl">
<div class="switcher-item center grow-none">
<img src="/assets/images/Adopt-04-Maha-Brahmin-in-India.jpg" :alt="t('Adopt an unengaged people group')">
</div>
<div class="stack stack--lg | text-card | surface-brand-lightest justify-center">
<h4 class="font-heading font-size-2xl overflow-wrap-anywhere">{{ t('Prayer opens the door for engagement') }}</h4>
<p>{{ t('Adoption mobilizes 144+ daily intercessors, creating 24 hours of prayer that prepares the soil, breaks spiritual barriers, and supports workers who go. Every gospel movement begins with prayer.') }}</p>
</div>
</div>
<div class="switcher | gap-md" data-width="xl">
<div class="switcher-item center grow-none">
<img src="/assets/images/Adopt-05-KamaraInGhana.jpg" :alt="t('Adopt an unengaged people group')">
</div>
<div class="stack stack--lg | text-card | surface-brand-lightest justify-center">
<h4 class="font-heading font-size-2xl">{{ t("Churches become active partners in God's mission") }}</h4>
<p>{{ t('Adoption invites the local church into meaningful participation - praying, giving, and sending – so that a people group moves from unengaged to engaged, and ultimately becomes a worshipping community for Jesus.') }}</p>
</div>
</div>
</div>
</div>
</section>
<section class="surface-brand-dark">
<div class="container stack stack--3xl">
<div class="stack stack--md">
<h2 id="choose-people-group">{{ t('Choose a people group') }}</h2>
<p class="subtext">{{ t('Select a highlighted unengaged people group, or search for a specific group or location below.') }}</p>
</div>
<UupgsList
:languageCode="locale"
:selectUrl="adoptBase"
:researchUrl="localePath('/research') + '/'"
:t="translations"
:perPage="6"
:morePerPage="12"
dontShowListOnLoad
useSelectCard
useHighlightedUUPGs
/>
</div>
</section>
<section>
<div class="container stack stack--5xl">
<figure class="text-center font-size-5xl font-heading">
<blockquote class="overflow-wrap-anywhere">{{ t('I was a stranger and you invited me in.') }}</blockquote>
<figcaption>- {{ t('Jesus') }}</figcaption>
</figure>
<div>
<img src="/assets/images/adopt-bottom-banner.jpg" :alt="t('Jesus')">
</div>
</div>
</section>
</div>
</template>

View file

@ -1,6 +1,199 @@
<script setup lang="ts">
// Port of marketing-theme/page-pray.php.
import { buildUupgListTranslations } from '~/utils/uupgListTranslations'
const { t, locale } = useI18n()
const localePath = useLocalePath()
const config = useRuntimeConfig()
const prayBaseUrl = config.public.prayBaseUrl as string
const selectUrl = computed(() =>
locale.value !== 'en' ? `${prayBaseUrl}/${locale.value}/` : `${prayBaseUrl}/`
)
const translations = computed(() => buildUupgListTranslations(t))
const { stats, prayerCoveragePercent } = usePrayerStatistics()
const { reload } = usePrayerStatistics()
onMounted(() => reload())
useTextHighlight()
</script>
<template>
<section class="container stack stack--md">
<h1>Pray</h1>
<p>The prayer page is ported in Phase 4.</p>
</section>
<div class="pray-page bg-secondary">
<section>
<div class="container stack stack--2xl">
<div class="stack stack-md">
<h1 class="h2 highlight" data-highlight-index="1">
{{ t('Prayer for an unengaged people group') }}
</h1>
<p class="subtext">{{ t('Help prepare the way for gospel engagement through prayer') }}</p>
</div>
<div class="three-part-switcher">
<div class="card-two-tone | text-center grow-1">
<div class="stack stack--lg">
<h2 class="h3">{{ t('Prayer Goal') }}</h2>
<p class="subtext font-size-md">{{ t('24-Hour Prayer Coverage') }}</p>
<p class="subtext font-size-md">{{ t('Mobilize 144+ people praying 10 minutes a day for all 2,085 people groups') }}</p>
</div>
<div>
<h2 class="h3">{{ t('Current Status') }}</h2>
<span class="font-size-4xl font-weight-bold font-button">
<span id="prayer-current-status">{{ stats.total_with_full_prayer }}</span> / 2085
</span>
<div class="stack stack--3xs">
<p class="subtext font-size-md">{{ t('People groups with committed 24-hour prayer coverage') }}</p>
<div class="progress-bar" data-size="md">
<div
id="prayer-current-status-percentage"
class="progress-bar__slider"
:style="{ width: `${prayerCoveragePercent}%` }"
/>
</div>
</div>
</div>
</div>
<div
class="grow-2 bg-image rounded-md"
style="background-image: url('/assets/images/pray-01-hero.jpg');"
/>
</div>
</div>
</section>
<section class="surface-brand-light">
<div class="container stack stack--3xl">
<h2>{{ t('Where do I start?') }}</h2>
<div class="switcher | gap-md">
<div class="step-card">
<div class="step-card__number">1</div>
<div class="step-card__content" data-no-action>
<h2 class="step-card__title overflow-wrap-anywhere">{{ t('Choose') }}</h2>
<p>{{ t('Choose an unengaged people group you will commit to pray for daily.') }}</p>
</div>
</div>
<div class="step-card">
<div class="step-card__number">2</div>
<div class="step-card__content" data-no-action>
<h2 class="step-card__title overflow-wrap-anywhere">{{ t('Sign up') }}</h2>
<p>{{ t('Sign up to receive daily prayer points and updates for this people group.') }}</p>
</div>
</div>
<div class="step-card">
<div class="step-card__number">3</div>
<div class="step-card__content" data-no-action>
<h2 class="step-card__title overflow-wrap-anywhere">{{ t('Pray') }}</h2>
<p>{{ t('Pray for 10 minutes a day and help provide 24-hour prayer coverage.') }}</p>
</div>
</div>
</div>
<a href="#choose-people-group" class="button | compact mx-auto">{{ t('Get Started') }}</a>
</div>
</section>
<section>
<div class="container">
<div class="switcher | align-center" data-width="xl">
<div class="stack | grow-2 align-center">
<div class="stack stack--2xl">
<h2 class="highlight" data-highlight-index="2">{{ t('Your daily prayer guide') }}</h2>
<ul class="stack stack--sm" data-list-color="primary">
<li>{{ t('Scripture-centered prayer themes') }}</li>
<li>{{ t('Spirit-led reflection and guidance') }}</li>
<li>{{ t('Real prayer needs from the field') }}</li>
<li>{{ t('Photos, stories and testimonies') }}</li>
<li>{{ t('Key insights about the people group') }}</li>
</ul>
</div>
</div>
<div>
<img
class="center"
src="/assets/images/pray-02-PrayerFUEL-Phone-graphic-2.png"
:alt="t('Your daily prayer guide')"
>
</div>
</div>
</div>
</section>
<section class="surface-white">
<div class="container">
<div class="stack stack--lg">
<h2>{{ t('Why prayer matters') }}</h2>
<div class="switcher | gap-md" data-width="xl">
<div class="switcher-item center grow-none">
<img src="/assets/images/Pray-04-Doxa.jpg" :alt="t('Adopt an unengaged people group')">
</div>
<div class="stack stack--lg | text-card | surface-brand-lightest justify-center">
<h4 class="font-heading font-size-2xl">{{ t('They have no one praying for them') }}</h4>
<p>{{ t('Many unengaged people groups have no churches, no missionaries, and often no believers, meaning little to no consistent prayer is being offered on their behalf.') }}</p>
</div>
</div>
<div class="switcher | gap-md" data-width="xl">
<div class="switcher-item center grow-none">
<img src="/assets/images/Pray-05-Doxa.jpg" :alt="t('Adopt an unengaged people group')">
</div>
<div class="stack stack--lg | text-card | surface-brand-lightest justify-center">
<h4 class="font-heading font-size-2xl">{{ t('Prayer prepares the way for the gospel') }}</h4>
<p>{{ t('Prayer softens hearts, opens doors, and invites the work of the Holy Spirit long before workers arrive or the gospel is proclaimed.') }}</p>
</div>
</div>
<div class="switcher | gap-md" data-width="xl">
<div class="switcher-item center grow-none">
<img src="/assets/images/Pray-06-Doxa.jpg" :alt="t('Adopt an unengaged people group')">
</div>
<div class="stack stack--lg | text-card | surface-brand-lightest justify-center">
<h4 class="font-heading font-size-2xl">{{ t('Prayer unites the global church') }}</h4>
<p>{{ t("When we pray, we join believers around the world in God's mission, standing together for peoples still waiting to hear the name of Jesus.") }}</p>
</div>
</div>
</div>
</div>
</section>
<section class="surface-brand-dark">
<div class="container stack stack--3xl">
<div class="stack stack--md">
<h2 id="choose-people-group">{{ t('Choose a people group') }}</h2>
<p class="subtext">{{ t('Select a highlighted unengaged people group, or search for a specific group or location below.') }}</p>
</div>
<UupgsList
:languageCode="locale"
:selectUrl="selectUrl"
:researchUrl="localePath('/research') + '/'"
:t="translations"
:perPage="6"
:morePerPage="12"
dontShowListOnLoad
useSelectCard
useHighlightedUUPGs
/>
</div>
</section>
<section>
<div class="container stack stack--lg">
<h2>{{ t('Prayer Progress') }}</h2>
<PrayerMap
:research-url="localePath('/research') + '/'"
:language-code="locale"
/>
</div>
</section>
<section>
<div class="container stack stack--5xl">
<figure class="text-center font-size-5xl font-heading">
<blockquote class="overflow-wrap-anywhere">{{ t('Pray earnestly to the Lord of the harvest ...that He would send laborers to the [Unengaged].') }}</blockquote>
<figcaption>- {{ t('Jesus') }}</figcaption>
</figure>
<div>
<img src="/assets/images/pray-03-bottom-unsplash.jpg" :alt="t('Jesus')">
</div>
</div>
</section>
</div>
</template>

View file

@ -11,10 +11,14 @@
"@iconify-json/simple-icons": "^1.2.78",
"@nuxt/ui": "^4.6.1",
"@nuxtjs/i18n": "^10.2.4",
"@nuxtjs/turnstile": "^1.1.2",
"bcrypt": "^6.0.0",
"fuse.js": "^7.3.0",
"intl-tel-input": "^26.7.5",
"jsonwebtoken": "^9.0.2",
"kysely": "^0.28.0",
"kysely-postgres-js": "^2.0.0",
"mapbox-gl": "^3.22.0",
"nodemailer": "^7.0.6",
"nodemailer-mailgun-transport": "^2.1.5",
"nuxt": "^4.4.2",
@ -351,8 +355,18 @@
"@kwsites/promise-deferred": ["@kwsites/promise-deferred@1.1.1", "", {}, "sha512-GaHYm+c0O9MjZRu0ongGBRbinu8gVAMd2UZjji6jVmqKtZluZnptXGWhz1E8j8D2HJ3f/yMxKAUC0b+57wncIw=="],
"@mapbox/mapbox-gl-supported": ["@mapbox/mapbox-gl-supported@3.0.0", "", {}, "sha512-2XghOwu16ZwPJLOFVuIOaLbN0iKMn867evzXFyf0P22dqugezfJwLmdanAgU25ITvz1TvOfVP4jsDImlDJzcWg=="],
"@mapbox/node-pre-gyp": ["@mapbox/node-pre-gyp@2.0.3", "", { "dependencies": { "consola": "^3.2.3", "detect-libc": "^2.0.0", "https-proxy-agent": "^7.0.5", "node-fetch": "^2.6.7", "nopt": "^8.0.0", "semver": "^7.5.3", "tar": "^7.4.0" }, "bin": { "node-pre-gyp": "bin/node-pre-gyp" } }, "sha512-uwPAhccfFJlsfCxMYTwOdVfOz3xqyj8xYL3zJj8f0pb30tLohnnFPhLuqp4/qoEz8sNxe4SESZedcBojRefIzg=="],
"@mapbox/point-geometry": ["@mapbox/point-geometry@1.1.0", "", {}, "sha512-YGcBz1cg4ATXDCM/71L9xveh4dynfGmcLDqufR+nQQy3fKwsAZsWd/x4621/6uJaeB9mwOHE6hPeDgXz9uViUQ=="],
"@mapbox/tiny-sdf": ["@mapbox/tiny-sdf@2.1.0", "", {}, "sha512-uFJhNh36BR4OCuWIEiWaEix9CA2WzT6CAIcqVjWYpnx8+QDtS+oC4QehRrx5cX4mgWs37MmKnwUejeHxVymzNg=="],
"@mapbox/unitbezier": ["@mapbox/unitbezier@0.0.1", "", {}, "sha512-nMkuDXFv60aBr9soUG5q+GvZYL+2KZHVvsqFCzqnkGEf46U2fvmytHaEVc1/YZbiLn8X+eR3QzX1+dwDO1lxlw=="],
"@mapbox/vector-tile": ["@mapbox/vector-tile@2.0.4", "", { "dependencies": { "@mapbox/point-geometry": "~1.1.0", "@types/geojson": "^7946.0.16", "pbf": "^4.0.1" } }, "sha512-AkOLcbgGTdXScosBWwmmD7cDlvOjkg/DetGva26pIRiZPdeJYjYKarIlb4uxVzi6bwHO6EWH82eZ5Nuv4T5DUg=="],
"@miyaneee/rollup-plugin-json5": ["@miyaneee/rollup-plugin-json5@1.2.0", "", { "dependencies": { "@rollup/pluginutils": "^5.1.0", "json5": "^2.2.3" }, "peerDependencies": { "rollup": "^1.20.0 || ^2.0.0 || ^3.0.0 || ^4.0.0" } }, "sha512-JjTIaXZp9WzhUHpElrqPnl1AzBi/rvRs065F71+aTmlqvTMVkdbjZ8vfFl4nRlgJy+TPBw69ZK4pwFdmOAt4aA=="],
"@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.4", "", { "dependencies": { "@tybys/wasm-util": "^0.10.1" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" } }, "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow=="],
@ -389,6 +403,8 @@
"@nuxt/schema": ["@nuxt/schema@4.4.2", "", { "dependencies": { "@vue/shared": "^3.5.30", "defu": "^6.1.4", "pathe": "^2.0.3", "pkg-types": "^2.3.0", "std-env": "^4.0.0" } }, "sha512-/q6C7Qhiricgi+PKR7ovBnJlKTL0memCbA1CzRT+itCW/oeYzUfeMdQ35mGntlBoyRPNrMXbzuSUhfDbSCU57w=="],
"@nuxt/scripts": ["@nuxt/scripts@0.13.4", "", { "dependencies": { "@nuxt/kit": "^4.2.2", "@vueuse/core": "^14.1.0", "consola": "^3.4.2", "defu": "^6.1.4", "h3": "^1.15.4", "magic-string": "^0.30.21", "ofetch": "^1.5.1", "ohash": "^2.0.11", "pathe": "^2.0.3", "pkg-types": "^2.3.0", "sirv": "^3.0.2", "std-env": "^3.10.0", "ufo": "^1.6.1", "unplugin": "^2.3.11", "unstorage": "^1.17.3", "valibot": "^1.2.0" }, "peerDependencies": { "@googlemaps/markerclusterer": "^2.6.2", "@paypal/paypal-js": "^8.1.2 || ^9.0.0", "@stripe/stripe-js": "^7.0.0 || ^8.0.0", "@types/google.maps": "^3.58.1", "@types/vimeo__player": "^2.18.3", "@types/youtube": "^0.1.0", "@unhead/vue": "^2.0.3" }, "optionalPeers": ["@googlemaps/markerclusterer", "@paypal/paypal-js", "@stripe/stripe-js", "@types/google.maps", "@types/vimeo__player", "@types/youtube"] }, "sha512-VYsoSAA7JIfSHN7f7WgBbSeKunwLFIzIlx171Cep2LTszgLFX20SjsRTeNbw746LuSB2aWjVSN40Sx+/g/6WdA=="],
"@nuxt/telemetry": ["@nuxt/telemetry@2.8.0", "", { "dependencies": { "citty": "^0.2.1", "consola": "^3.4.2", "ofetch": "^2.0.0-alpha.3", "rc9": "^3.0.0", "std-env": "^4.0.0" }, "peerDependencies": { "@nuxt/kit": ">=3.0.0" }, "bin": { "nuxt-telemetry": "bin/nuxt-telemetry.mjs" } }, "sha512-zAwXY24KYvpLTmiV+osagd2EHkfs5IF+7oDZYTQoit5r0kPlwaCNlzHp5I/wUAWT4LBw6lG8gZ6bWidAdv/erQ=="],
"@nuxt/ui": ["@nuxt/ui@4.6.1", "", { "dependencies": { "@floating-ui/dom": "^1.7.6", "@iconify/vue": "^5.0.0", "@internationalized/date": "^3.12.0", "@internationalized/number": "^3.6.5", "@nuxt/fonts": "^0.14.0", "@nuxt/icon": "^2.2.1", "@nuxt/kit": "^4.4.2", "@nuxt/schema": "^4.4.2", "@nuxtjs/color-mode": "^3.5.2", "@standard-schema/spec": "^1.1.0", "@tailwindcss/postcss": "^4.2.2", "@tailwindcss/vite": "^4.2.2", "@tanstack/vue-table": "^8.21.3", "@tanstack/vue-virtual": "^3.13.23", "@tiptap/core": "^3.21.0", "@tiptap/extension-bubble-menu": "^3.21.0", "@tiptap/extension-code": "^3.21.0", "@tiptap/extension-collaboration": "^3.21.0", "@tiptap/extension-drag-handle": "^3.21.0", "@tiptap/extension-drag-handle-vue-3": "^3.21.0", "@tiptap/extension-floating-menu": "^3.21.0", "@tiptap/extension-horizontal-rule": "^3.21.0", "@tiptap/extension-image": "^3.21.0", "@tiptap/extension-mention": "^3.21.0", "@tiptap/extension-node-range": "^3.21.0", "@tiptap/extension-placeholder": "^3.21.0", "@tiptap/markdown": "^3.21.0", "@tiptap/pm": "^3.21.0", "@tiptap/starter-kit": "^3.21.0", "@tiptap/suggestion": "^3.21.0", "@tiptap/vue-3": "^3.21.0", "@unhead/vue": "^2.1.12", "@vueuse/core": "^14.2.1", "@vueuse/integrations": "^14.2.1", "@vueuse/shared": "^14.2.1", "colortranslator": "^5.0.0", "consola": "^3.4.2", "defu": "^6.1.4", "embla-carousel-auto-height": "^8.6.0", "embla-carousel-auto-scroll": "^8.6.0", "embla-carousel-autoplay": "^8.6.0", "embla-carousel-class-names": "^8.6.0", "embla-carousel-fade": "^8.6.0", "embla-carousel-vue": "^8.6.0", "embla-carousel-wheel-gestures": "^8.1.0", "fuse.js": "^7.1.0", "hookable": "^6.1.0", "knitwork": "^1.3.0", "magic-string": "^0.30.21", "mlly": "^1.8.2", "motion-v": "^2.2.0", "ohash": "^2.0.11", "pathe": "^2.0.3", "reka-ui": "2.9.3", "scule": "^1.3.0", "tailwind-merge": "^3.5.0", "tailwind-variants": "^3.2.2", "tailwindcss": "^4.2.2", "tinyglobby": "^0.2.15", "ufo": "^1.6.3", "unplugin": "^3.0.0", "unplugin-auto-import": "^21.0.0", "unplugin-vue-components": "^32.0.0", "vaul-vue": "0.4.1", "vue-component-type-helpers": "^3.2.6" }, "peerDependencies": { "@inertiajs/vue3": "^2.0.7 || ^3.0.0", "@nuxt/content": "^3.0.0", "joi": "^18.0.0", "superstruct": "^2.0.0", "typescript": "^5.6.3 || ^6.0.0", "valibot": "^1.0.0", "vue-router": "^4.5.0 || ^5.0.0", "yup": "^1.7.0", "zod": "^3.24.0 || ^4.0.0" }, "optionalPeers": ["@inertiajs/vue3", "@nuxt/content", "joi", "superstruct", "valibot", "vue-router", "yup", "zod"], "bin": { "nuxt-ui": "cli/index.mjs" } }, "sha512-mBbBTaVDTR6ohOoAJUiV4T2RPXo2hyLewGPTiHjy1arzHPNFnmb/Tkl/2/JipF3Y8cahV4LhVUdkWKsdgI1OXw=="],
@ -399,6 +415,8 @@
"@nuxtjs/i18n": ["@nuxtjs/i18n@10.2.4", "", { "dependencies": { "@intlify/core": "^11.2.8", "@intlify/h3": "^0.7.4", "@intlify/shared": "^11.2.8", "@intlify/unplugin-vue-i18n": "^11.0.7", "@intlify/utils": "^0.14.1", "@miyaneee/rollup-plugin-json5": "^1.2.0", "@nuxt/kit": "^4.4.2", "@rollup/plugin-yaml": "^4.1.2", "@vue/compiler-sfc": "^3.5.22", "defu": "^6.1.4", "devalue": "^5.1.1", "h3": "^1.15.4", "knitwork": "^1.2.0", "magic-string": "^0.30.21", "mlly": "^1.7.4", "nuxt-define": "^1.0.0", "ohash": "^2.0.11", "oxc-parser": "^0.112.0", "oxc-transform": "^0.112.0", "oxc-walker": "^0.7.0", "pathe": "^2.0.3", "ufo": "^1.6.1", "unplugin": "^2.3.11", "unrouting": "^0.1.5", "unstorage": "^1.16.1", "vue-i18n": "^11.1.11", "vue-router": "^5.0.4" } }, "sha512-T5nN7hT/tbExant+CeXtKXs9GbjWsEWnymCHJyU3Ujxfcw1WQsZZP6tyEtFRhkwjBwy8m4mj7GtU2qUCggUkkg=="],
"@nuxtjs/turnstile": ["@nuxtjs/turnstile@1.1.2", "", { "dependencies": { "@nuxt/kit": "^3.16.0", "@types/cloudflare-turnstile": "0.2.2", "defu": "^6.1.4", "pathe": "^2.0.3" }, "peerDependencies": { "@nuxt/scripts": "^0.11.0 || ^0.12.0 || ^0.13.0" } }, "sha512-EIaorWTYdmaAuL9UHrflyjaQRvpGjciUQkP7phVop+RSd6W6bACkIMhjKre8isNv5a+b3m5b67EVKhle4zZgOA=="],
"@oxc-minify/binding-android-arm-eabi": ["@oxc-minify/binding-android-arm-eabi@0.117.0", "", { "os": "android", "cpu": "arm" }, "sha512-5Hf2KsGRjxp3HnPU/mse7cQJa5tWfMFUPZQcgSMVsv2JZnGFFOIDzA0Oja2KDD+VPJqMpEJKc2dCHAGZgJxsGg=="],
"@oxc-minify/binding-android-arm64": ["@oxc-minify/binding-android-arm64@0.117.0", "", { "os": "android", "cpu": "arm64" }, "sha512-uuxGwxA5J4Sap+gz4nxyM/rer6q2A4X1Oe8HpE0CZQyb5cSBULQ15btZiVG3xOBctI5O+c2dwR1aZAP4oGKcLw=="],
@ -871,10 +889,16 @@
"@types/bcrypt": ["@types/bcrypt@6.0.0", "", { "dependencies": { "@types/node": "*" } }, "sha512-/oJGukuH3D2+D+3H4JWLaAsJ/ji86dhRidzZ/Od7H/i8g+aCmvkeCc6Ni/f9uxGLSQVCRZkX2/lqEFG2BvWtlQ=="],
"@types/cloudflare-turnstile": ["@types/cloudflare-turnstile@0.2.2", "", {}, "sha512-3Yf7b1Glci+V2bFWwWBbZkRgTuegp7RDgNTOG4U0UNPB9RV4AWvwqg2/qqLff8G+SwKFNXoXvTkqaRBZrAFdKA=="],
"@types/esrecurse": ["@types/esrecurse@4.3.1", "", {}, "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw=="],
"@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
"@types/geojson": ["@types/geojson@7946.0.16", "", {}, "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg=="],
"@types/geojson-vt": ["@types/geojson-vt@3.2.5", "", { "dependencies": { "@types/geojson": "*" } }, "sha512-qDO7wqtprzlpe8FfQ//ClPV9xiuoh2nkIgiouIptON9w5jvD/fA4szvP9GBlDVdJ5dldAl0kX/sy3URbWwLx0g=="],
"@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="],
"@types/jsonwebtoken": ["@types/jsonwebtoken@9.0.10", "", { "dependencies": { "@types/ms": "*", "@types/node": "*" } }, "sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA=="],
@ -887,8 +911,12 @@
"@types/nodemailer-mailgun-transport": ["@types/nodemailer-mailgun-transport@1.4.6", "", { "dependencies": { "@types/nodemailer": "*" } }, "sha512-6qhtDo+1ZLtrmmpQN7O9e3NLK5ggnTS2Oca+22SvmhwChNKxDZErecTlF6qTOLnNW/CCcHmDaSmG2MXUeP1w9g=="],
"@types/pbf": ["@types/pbf@3.0.5", "", {}, "sha512-j3pOPiEcWZ34R6a6mN07mUkM4o4Lwf6hPNt8eilOeZhTFbxFXmKhvXl9Y28jotFPaI1bpPDJsbCprUoNke6OrA=="],
"@types/resolve": ["@types/resolve@1.20.2", "", {}, "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q=="],
"@types/supercluster": ["@types/supercluster@7.1.3", "", { "dependencies": { "@types/geojson": "*" } }, "sha512-Z0pOY34GDFl3Q6hUFYf3HkTwKEE02e7QgtJppBt+beEAxnyOpJua+voGFvxINBHa06GwLFFym7gRPY2SiKIfIA=="],
"@types/web-bluetooth": ["@types/web-bluetooth@0.0.21", "", {}, "sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA=="],
"@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.59.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.59.0", "@typescript-eslint/type-utils": "8.59.0", "@typescript-eslint/utils": "8.59.0", "@typescript-eslint/visitor-keys": "8.59.0", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.59.0", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-HyAZtpdkgZwpq8Sz3FSUvCR4c+ScbuWa9AksK2Jweub7w4M3yTz4O11AqVJzLYjy/B9ZWPyc81I+mOdJU/bDQw=="],
@ -979,7 +1007,7 @@
"@vue/compiler-ssr": ["@vue/compiler-ssr@3.5.32", "", { "dependencies": { "@vue/compiler-dom": "3.5.32", "@vue/shared": "3.5.32" } }, "sha512-Gp4gTs22T3DgRotZ8aA/6m2jMR+GMztvBXUBEUOYOcST+giyGWJ4WvFd7QLHBkzTxkfOt8IELKNdpzITLbA2rw=="],
"@vue/devtools-api": ["@vue/devtools-api@6.6.4", "", {}, "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g=="],
"@vue/devtools-api": ["@vue/devtools-api@8.1.1", "", { "dependencies": { "@vue/devtools-kit": "^8.1.1" } }, "sha512-bsDMJ07b3GN1puVwJb/fyFnj/U2imyswK5UQVLZwVl7O05jDrt6BHxeG5XffmOOdasOj/bOmIjxJvGPxU7pcqw=="],
"@vue/devtools-core": ["@vue/devtools-core@8.1.1", "", { "dependencies": { "@vue/devtools-kit": "^8.1.1", "@vue/devtools-shared": "^8.1.1" }, "peerDependencies": { "vue": "^3.0.0" } }, "sha512-bCCsSABp1/ot4j8xJEycM6Mtt2wbuucfByr6hMgjbYhrtlscOJypZKvy8f1FyWLYrLTchB5Qz216Lm92wfbq0A=="],
@ -1121,6 +1149,8 @@
"change-case": ["change-case@5.4.4", "", {}, "sha512-HRQyTk2/YPEkt9TnUPbOpr64Uw3KOicFWPVBb+xiHvd6eBx/qPr9xqfBFDT8P2vWsvvz4jbEkfDe71W3VyNu2w=="],
"cheap-ruler": ["cheap-ruler@4.0.0", "", {}, "sha512-0BJa8f4t141BYKQyn9NSQt1PguFQXMXwZiA5shfoaBYHAb2fFk2RAX+tiWMoQU+Agtzt3mdt0JtuyshAXqZ+Vw=="],
"chokidar": ["chokidar@5.0.0", "", { "dependencies": { "readdirp": "^5.0.0" } }, "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw=="],
"chownr": ["chownr@3.0.0", "", {}, "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g=="],
@ -1185,6 +1215,8 @@
"css-what": ["css-what@6.2.2", "", {}, "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA=="],
"csscolorparser": ["csscolorparser@1.0.3", "", {}, "sha512-umPSgYwZkdFoUrH5hIq5kf0wPSXiro51nPw0j2K/c83KflkPSTBGMz6NJvMB+07VlL0y7VPo6QJcDjcgKTTm3w=="],
"cssesc": ["cssesc@3.0.0", "", { "bin": { "cssesc": "bin/cssesc" } }, "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg=="],
"cssnano": ["cssnano@7.1.7", "", { "dependencies": { "cssnano-preset-default": "^7.0.15", "lilconfig": "^3.1.3" }, "peerDependencies": { "postcss": "^8.5.10" } }, "sha512-N5LGn/OlhMxDTvKACwUPMzT34SSj1b022pvUAE/Vh6r2WD1aUCbc+QNIP/JjX9VVxebdJWZQ3352Lt4oF7dQ/g=="],
@ -1243,6 +1275,8 @@
"duplexer": ["duplexer@0.1.2", "", {}, "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg=="],
"earcut": ["earcut@3.0.2", "", {}, "sha512-X7hshQbLyMJ/3RPhyObLARM2sNxxmRALLKx1+NVFFnQ9gKzmCrxm9+uLIAdBcvc8FNLpctqlQ2V6AE92Ol9UDQ=="],
"eastasianwidth": ["eastasianwidth@0.2.0", "", {}, "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="],
"ecdsa-sig-formatter": ["ecdsa-sig-formatter@1.0.11", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ=="],
@ -1425,6 +1459,8 @@
"gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="],
"geojson-vt": ["geojson-vt@4.0.2", "", {}, "sha512-AV9ROqlNqoZEIJGfm1ncNjEXfkz2hdFlZf0qkVfmkwdKa8vj7H16YUOT81rJw1rdFhyEDlN2Tds91p/glzbl5A=="],
"get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="],
"get-east-asian-width": ["get-east-asian-width@1.5.0", "", {}, "sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA=="],
@ -1441,6 +1477,8 @@
"giget": ["giget@3.2.0", "", { "bin": { "giget": "dist/cli.mjs" } }, "sha512-GvHTWcykIR/fP8cj8dMpuMMkvaeJfPvYnhq0oW+chSeIr+ldX21ifU2Ms6KBoyKZQZmVaUAAhQ2EZ68KJF8a7A=="],
"gl-matrix": ["gl-matrix@3.4.4", "", {}, "sha512-latSnyDNt/8zYUB6VIJ6PCh2jBjJX6gnDsoCZ7LyW7GkqrD51EWwa9qCoGixj8YqBtETQK/xY7OmpTF8xz1DdQ=="],
"glob": ["glob@13.0.6", "", { "dependencies": { "minimatch": "^10.2.2", "minipass": "^7.1.3", "path-scurry": "^2.0.2" } }, "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw=="],
"glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="],
@ -1455,6 +1493,8 @@
"graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
"grid-index": ["grid-index@1.1.0", "", {}, "sha512-HZRwumpOGUrHyxO5bqKZL0B0GlUpwtCAzZ42sgxUPniu33R1LSFH5yrIcBCHjkctCAh3mtWKcKd9J4vDDdeVHA=="],
"gzip-size": ["gzip-size@7.0.0", "", { "dependencies": { "duplexer": "^0.1.2" } }, "sha512-O1Ld7Dr+nqPnmGpdhzLmMTQ4vAsD+rHwMm1NLUmoUFFymBOMKxCCrtDxqdBRYXdeEPEi3SyoR4TizJLQrnKBNA=="],
"h3": ["h3@1.15.11", "", { "dependencies": { "cookie-es": "^1.2.3", "crossws": "^0.3.5", "defu": "^6.1.6", "destr": "^2.0.5", "iron-webcrypto": "^1.2.1", "node-mock-http": "^1.0.4", "radix3": "^1.1.2", "ufo": "^1.6.3", "uncrypto": "^0.1.3" } }, "sha512-L3THSe2MPeBwgIZVSH5zLdBBU90TOxarvhK9d04IDY2AmVS8j2Jz2LIWtwsGOU3lu2I5jCN7FNvVfY2+XyF+mg=="],
@ -1499,6 +1539,8 @@
"ini": ["ini@4.1.1", "", {}, "sha512-QQnnxNyfvmHFIsj7gkPcYymR8Jdw/o7mp5ZFihxn6h8Ci6fh3Dx4E1gPjpQEpIuPo9XVNY/ZUwh4BPMjGyL01g=="],
"intl-tel-input": ["intl-tel-input@26.9.2", "", {}, "sha512-40v/uBlE7Wpg3Dz79T1AaE9eH66D86fCXucnI8UVCayypG91lBDyoXLXb7WAQuAdjLtB2+OuTJtlihjByWYL1g=="],
"ioredis": ["ioredis@5.10.1", "", { "dependencies": { "@ioredis/commands": "1.5.1", "cluster-key-slot": "^1.1.0", "debug": "^4.3.4", "denque": "^2.1.0", "lodash.defaults": "^4.2.0", "lodash.isarguments": "^3.1.0", "redis-errors": "^1.2.0", "redis-parser": "^3.0.0", "standard-as-callback": "^2.1.0" } }, "sha512-HuEDBTI70aYdx1v6U97SbNx9F1+svQKBDo30o0b9fw055LMepzpOOd0Ccg9Q6tbqmBSJaMuY0fB7yw9/vjBYCA=="],
"iron-webcrypto": ["iron-webcrypto@1.2.1", "", {}, "sha512-feOM6FaSr6rEABp/eDfVseKyTMDt+KGpeB35SkVn9Tyn0CqvVsY3EwI0v5i8nMHyJnzCIQf7nsy3p41TPkJZhg=="],
@ -1569,6 +1611,8 @@
"jws": ["jws@4.0.1", "", { "dependencies": { "jwa": "^2.0.1", "safe-buffer": "^5.0.1" } }, "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA=="],
"kdbush": ["kdbush@4.0.2", "", {}, "sha512-WbCVYJ27Sz8zi9Q7Q0xHC+05iwkm3Znipc2XTlrnJbsHMYktW4hPhXUE8Ys1engBrvffoSCqbil1JQAa7clRpA=="],
"keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="],
"kleur": ["kleur@4.1.5", "", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="],
@ -1661,8 +1705,12 @@
"mailgun.js": ["mailgun.js@8.2.2", "", { "dependencies": { "axios": "^1.3.3", "base-64": "^1.0.0", "url-join": "^4.0.1" } }, "sha512-po/KtofzrTuKhHLenbmliDsVVOFANwcfDFUGnggwnyZJmZz7JgBlV6nzK9o2Fk+OK2SiBmJTK25RbkAj57Hd+Q=="],
"mapbox-gl": ["mapbox-gl@3.22.0", "", { "dependencies": { "@mapbox/mapbox-gl-supported": "^3.0.0", "@mapbox/point-geometry": "^1.1.0", "@mapbox/tiny-sdf": "^2.0.6", "@mapbox/unitbezier": "^0.0.1", "@mapbox/vector-tile": "^2.0.4", "@types/geojson": "^7946.0.16", "@types/geojson-vt": "^3.2.5", "@types/pbf": "^3.0.5", "@types/supercluster": "^7.1.3", "cheap-ruler": "^4.0.0", "csscolorparser": "~1.0.3", "earcut": "^3.0.1", "geojson-vt": "^4.0.2", "gl-matrix": "^3.4.4", "grid-index": "^1.1.0", "kdbush": "^4.0.2", "martinez-polygon-clipping": "^0.8.1", "murmurhash-js": "^1.0.0", "pbf": "^4.0.1", "potpack": "^2.0.0", "quickselect": "^3.0.0", "supercluster": "^8.0.1", "tinyqueue": "^3.0.0" } }, "sha512-ZIpF+oAMcQoDlvABmiRkHoydyBR9zI6CyDeVRa2/iyua0/B2+rPuIzoCV/CgN14P5F0HVk53GIZw220WSqJPyA=="],
"marked": ["marked@17.0.6", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-gB0gkNafnonOw0obSTEGZTT86IuhILt2Wfx0mWH/1Au83kybTayroZ/V6nS25mN7u8ASy+5fMhgB3XPNrOZdmA=="],
"martinez-polygon-clipping": ["martinez-polygon-clipping@0.8.1", "", { "dependencies": { "robust-predicates": "^2.0.4", "splaytree": "^0.1.4", "tinyqueue": "3.0.0" } }, "sha512-9PLLMzMPI6ihHox4Ns6LpVBLpRc7sbhULybZ/wyaY8sY3ECNe2+hxm1hA2/9bEEpRrdpjoeduBuZLg2aq1cSIQ=="],
"math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
"mdn-data": ["mdn-data@2.27.1", "", {}, "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ=="],
@ -1703,6 +1751,8 @@
"muggle-string": ["muggle-string@0.4.1", "", {}, "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ=="],
"murmurhash-js": ["murmurhash-js@1.0.0", "", {}, "sha512-TvmkNhkv8yct0SVBSy+o8wYzXjE4Zz3PCesbfs8HiCXXdcTuocApFv11UWlNFWKYsP2okqrhb7JNlSm9InBhIw=="],
"nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
"nanotar": ["nanotar@0.3.0", "", {}, "sha512-Kv2JYYiCzt16Kt5QwAc9BFG89xfPNBx+oQL4GQXD9nLqPkZBiNaqaCWtwnbk/q7UVsTYevvM1b0UF8zmEI4pCg=="],
@ -1801,6 +1851,8 @@
"pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
"pbf": ["pbf@4.0.1", "", { "dependencies": { "resolve-protobuf-schema": "^2.1.0" }, "bin": { "pbf": "bin/pbf" } }, "sha512-SuLdBvS42z33m8ejRbInMapQe8n0D3vN/Xd5fmWM3tufNgRQFBpaW2YVJxQZV4iPNqb0vEFvssMEo5w9c6BTIA=="],
"perfect-debounce": ["perfect-debounce@2.1.0", "", {}, "sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g=="],
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
@ -1873,6 +1925,8 @@
"postgres": ["postgres@3.4.9", "", {}, "sha512-GD3qdB0x1z9xgFI6cdRD6xu2Sp2WCOEoe3mtnyB5Ee0XrrL5Pe+e4CCnJrRMnL1zYtRDZmQQVbvOttLnKDLnaw=="],
"potpack": ["potpack@2.1.0", "", {}, "sha512-pcaShQc1Shq0y+E7GqJqvZj8DTthWV1KeHGdi0Z6IAin2Oi3JnLCOfwnCo84qc+HAp52wT9nK9H7FAJp5a44GQ=="],
"powershell-utils": ["powershell-utils@0.1.0", "", {}, "sha512-dM0jVuXJPsDN6DvRpea484tCUaMiXWjuCn++HGTqUWzGDjv5tZkEZldAJ/UMlqRYGFrD/etByo4/xOuC/snX2A=="],
"prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="],
@ -1907,6 +1961,8 @@
"prosemirror-view": ["prosemirror-view@1.41.8", "", { "dependencies": { "prosemirror-model": "^1.20.0", "prosemirror-state": "^1.0.0", "prosemirror-transform": "^1.1.0" } }, "sha512-TnKDdohEatgyZNGCDWIdccOHXhYloJwbwU+phw/a23KBvJIR9lWQWW7WHHK3vBdOLDNuF7TaX98GObUZOWkOnA=="],
"protocol-buffers-schema": ["protocol-buffers-schema@3.6.1", "", {}, "sha512-VG2K63Igkiv9p76tk1lilczEK1cT+kCjKtkdhw1dQZV3k3IXJbd3o6Ho8b9zJZaHSnT2hKe4I+ObmX9w6m5SmQ=="],
"proxy-from-env": ["proxy-from-env@2.1.0", "", {}, "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA=="],
"punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
@ -1915,6 +1971,8 @@
"queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="],
"quickselect": ["quickselect@3.0.0", "", {}, "sha512-XdjUArbK4Bm5fLLvlm5KpTFOiOThgfWWI4axAZDWg4E/0mKdZyI9tNEfds27qCi1ze/vwTR16kvmmGhRra3c2g=="],
"radix3": ["radix3@1.1.2", "", {}, "sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA=="],
"range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="],
@ -1949,8 +2007,12 @@
"resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="],
"resolve-protobuf-schema": ["resolve-protobuf-schema@2.1.0", "", { "dependencies": { "protocol-buffers-schema": "^3.3.1" } }, "sha512-kI5ffTiZWmJaS/huM8wZfEMer1eRd7oJQhDuxeCLe3t7N7mX3z94CN0xPxBQxFYQTSNz9T0i+v6inKqSdK8xrQ=="],
"reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="],
"robust-predicates": ["robust-predicates@2.0.4", "", {}, "sha512-l4NwboJM74Ilm4VKfbAtFeGq7aEjWL+5kVFcmgFA2MrdnQWx9iE/tUGvxY5HyMI7o/WpSIUFLbC5fbeaHgSCYg=="],
"rollup": ["rollup@4.60.2", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.60.2", "@rollup/rollup-android-arm64": "4.60.2", "@rollup/rollup-darwin-arm64": "4.60.2", "@rollup/rollup-darwin-x64": "4.60.2", "@rollup/rollup-freebsd-arm64": "4.60.2", "@rollup/rollup-freebsd-x64": "4.60.2", "@rollup/rollup-linux-arm-gnueabihf": "4.60.2", "@rollup/rollup-linux-arm-musleabihf": "4.60.2", "@rollup/rollup-linux-arm64-gnu": "4.60.2", "@rollup/rollup-linux-arm64-musl": "4.60.2", "@rollup/rollup-linux-loong64-gnu": "4.60.2", "@rollup/rollup-linux-loong64-musl": "4.60.2", "@rollup/rollup-linux-ppc64-gnu": "4.60.2", "@rollup/rollup-linux-ppc64-musl": "4.60.2", "@rollup/rollup-linux-riscv64-gnu": "4.60.2", "@rollup/rollup-linux-riscv64-musl": "4.60.2", "@rollup/rollup-linux-s390x-gnu": "4.60.2", "@rollup/rollup-linux-x64-gnu": "4.60.2", "@rollup/rollup-linux-x64-musl": "4.60.2", "@rollup/rollup-openbsd-x64": "4.60.2", "@rollup/rollup-openharmony-arm64": "4.60.2", "@rollup/rollup-win32-arm64-msvc": "4.60.2", "@rollup/rollup-win32-ia32-msvc": "4.60.2", "@rollup/rollup-win32-x64-gnu": "4.60.2", "@rollup/rollup-win32-x64-msvc": "4.60.2", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-J9qZyW++QK/09NyN/zeO0dG/1GdGfyp9lV8ajHnRVLfo/uFsbji5mHnDgn/qYdUHyCkM2N+8VyspgZclfAh0eQ=="],
"rollup-plugin-visualizer": ["rollup-plugin-visualizer@7.0.1", "", { "dependencies": { "open": "^11.0.0", "picomatch": "^4.0.2", "source-map": "^0.7.4", "yargs": "^18.0.0" }, "peerDependencies": { "rolldown": "1.x || ^1.0.0-beta || ^1.0.0-rc", "rollup": "2.x || 3.x || 4.x" }, "optionalPeers": ["rolldown", "rollup"], "bin": { "rollup-plugin-visualizer": "dist/bin/cli.js" } }, "sha512-UJUT4+1Ho4OcWmPYU3sYXgUqI8B8Ayfe06MX7y0qCJ1K8aGoKtR/NDd/2nZqM7ADkrzny+I99Ul7GgyoiVNAgg=="],
@ -2017,6 +2079,8 @@
"spdx-license-ids": ["spdx-license-ids@3.0.23", "", {}, "sha512-CWLcCCH7VLu13TgOH+r8p1O/Znwhqv/dbb6lqWy67G+pT1kHmeD/+V36AVb/vq8QMIQwVShJ6Ssl5FPh0fuSdw=="],
"splaytree": ["splaytree@0.1.4", "", {}, "sha512-D50hKrjZgBzqD3FT2Ek53f2dcDLAQT8SSGrzj3vidNH5ISRgceeGVJ2dQIthKOuayqFXfFjXheHNo4bbt9LhRQ=="],
"srvx": ["srvx@0.11.15", "", { "bin": { "srvx": "bin/srvx.mjs" } }, "sha512-iXsux0UcOjdvs0LCMa2Ws3WwcDUozA3JN3BquNXkaFPP7TpRqgunKdEgoZ/uwb1J6xaYHfxtz9Twlh6yzwM6Tg=="],
"stable-hash-x": ["stable-hash-x@0.2.0", "", {}, "sha512-o3yWv49B/o4QZk5ZcsALc6t0+eCelPc44zZsLtCQnZPDwFpDYSWcDnrv2TtMmMbQ7uKo3J0HTURCqckw23czNQ=="],
@ -2051,6 +2115,8 @@
"stylehacks": ["stylehacks@7.0.10", "", { "dependencies": { "browserslist": "^4.28.2", "postcss-selector-parser": "^7.1.1" }, "peerDependencies": { "postcss": "^8.5.10" } }, "sha512-sRJ7klmhe/Fl5woJcbJUa2qP1Ueffsl1CQI4ePvqXLkZmcIuAt09aP9uT/FOFPqXh9Rh8M5UkgEnwTdTKn/Aag=="],
"supercluster": ["supercluster@8.0.1", "", { "dependencies": { "kdbush": "^4.0.2" } }, "sha512-IiOea5kJ9iqzD2t7QJq/cREyLHTtSmUT6gQsweojg9WH2sYJqZK9SswTu6jrscO6D1G5v5vYZ9ru/eq85lXeZQ=="],
"supports-color": ["supports-color@10.2.2", "", {}, "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g=="],
"supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="],
@ -2087,6 +2153,8 @@
"tinyglobby": ["tinyglobby@0.2.16", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg=="],
"tinyqueue": ["tinyqueue@3.0.0", "", {}, "sha512-gRa9gwYU3ECmQYv3lslts5hxuIa90veaEcxDYuu3QGOIAEM2mOZkVHp48ANJuu1CURtRdHKUBY5Lm1tHV+sD4g=="],
"to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="],
"to-valid-identifier": ["to-valid-identifier@1.0.0", "", { "dependencies": { "@sindresorhus/base62": "^1.0.0", "reserved-identifiers": "^1.0.0" } }, "sha512-41wJyvKep3yT2tyPqX/4blcfybknGB4D+oETKLs7Q76UiPqRpUJK3hr1nxelyYO0PHKVzJwlu0aCeEAsGI6rpw=="],
@ -2161,6 +2229,8 @@
"util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="],
"valibot": ["valibot@1.3.1", "", { "peerDependencies": { "typescript": ">=5" }, "optionalPeers": ["typescript"] }, "sha512-sfdRir/QFM0JaF22hqTroPc5xy4DimuGQVKFrzF1YfGwaS1nJot3Y8VqMdLO2Lg27fMzat2yD3pY5PbAYO39Gg=="],
"vaul-vue": ["vaul-vue@0.4.1", "", { "dependencies": { "@vueuse/core": "^10.8.0", "reka-ui": "^2.0.0", "vue": "^3.4.5" } }, "sha512-A6jOWOZX5yvyo1qMn7IveoWN91mJI5L3BUKsIwkg6qrTGgHs1Sb1JF/vyLJgnbN1rH4OOOxFbtqL9A46bOyGUQ=="],
"vite": ["vite@7.3.2", "", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-Bby3NOsna2jsjfLVOHKes8sGwgl4TT0E6vvpYgnAYDIF/tie7MRaFthmKuHx1NSXjiTueXH3do80FMQgvEktRg=="],
@ -2289,6 +2359,10 @@
"@nuxt/nitro-server/escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="],
"@nuxt/scripts/std-env": ["std-env@3.10.0", "", {}, "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg=="],
"@nuxt/scripts/unplugin": ["unplugin@2.3.11", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "acorn": "^8.15.0", "picomatch": "^4.0.3", "webpack-virtual-modules": "^0.6.2" } }, "sha512-5uKD0nqiYVzlmCRs01Fhs2BdkEgBS3SAVP6ndrBsuK42iC2+JHyxM05Rm9G8+5mkmRtzMZGY8Ct5+mliZxU/Ww=="],
"@nuxt/telemetry/ofetch": ["ofetch@2.0.0-alpha.3", "", {}, "sha512-zpYTCs2byOuft65vI3z43Dd6iSdFbOZZLb9/d21aCpx2rGastVU9dOCv0lu4ykc1Ur1anAYjDi3SUvR0vq50JA=="],
"@nuxt/vite-builder/escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="],
@ -2301,6 +2375,8 @@
"@nuxtjs/i18n/unplugin": ["unplugin@2.3.11", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "acorn": "^8.15.0", "picomatch": "^4.0.3", "webpack-virtual-modules": "^0.6.2" } }, "sha512-5uKD0nqiYVzlmCRs01Fhs2BdkEgBS3SAVP6ndrBsuK42iC2+JHyxM05Rm9G8+5mkmRtzMZGY8Ct5+mliZxU/Ww=="],
"@nuxtjs/turnstile/@nuxt/kit": ["@nuxt/kit@3.21.2", "", { "dependencies": { "c12": "^3.3.3", "consola": "^3.4.2", "defu": "^6.1.4", "destr": "^2.0.5", "errx": "^0.1.0", "exsolve": "^1.0.8", "ignore": "^7.0.5", "jiti": "^2.6.1", "klona": "^2.0.6", "knitwork": "^1.3.0", "mlly": "^1.8.1", "ohash": "^2.0.11", "pathe": "^2.0.3", "pkg-types": "^2.3.0", "rc9": "^3.0.0", "scule": "^1.3.0", "semver": "^7.7.4", "tinyglobby": "^0.2.15", "ufo": "^1.6.3", "unctx": "^2.5.0", "untyped": "^2.0.0" } }, "sha512-Bd6m6mrDrqpBEbX+g0rc66/ALd1sxlgdx5nfK9MAYO0yKLTOSK7McSYz1KcOYn3LQFCXOWfvXwaqih/b+REI1g=="],
"@parcel/watcher/node-addon-api": ["node-addon-api@7.1.1", "", {}, "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ=="],
"@parcel/watcher-wasm/napi-wasm": ["napi-wasm@1.1.3", "", { "bundled": true }, "sha512-h/4nMGsHjZDCYmQVNODIrYACVJ+I9KItbG+0si6W/jSjdA9JbWDoU4LLeMXVcEQGHjttI2tuXqDrbGF7qkUHHg=="],
@ -2447,7 +2523,7 @@
"vite-plugin-checker/npm-run-path": ["npm-run-path@6.0.0", "", { "dependencies": { "path-key": "^4.0.0", "unicorn-magic": "^0.3.0" } }, "sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA=="],
"vue-router/@vue/devtools-api": ["@vue/devtools-api@8.1.1", "", { "dependencies": { "@vue/devtools-kit": "^8.1.1" } }, "sha512-bsDMJ07b3GN1puVwJb/fyFnj/U2imyswK5UQVLZwVl7O05jDrt6BHxeG5XffmOOdasOj/bOmIjxJvGPxU7pcqw=="],
"vue-i18n/@vue/devtools-api": ["@vue/devtools-api@6.6.4", "", {}, "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g=="],
"wrap-ansi-cjs/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
@ -2519,6 +2595,8 @@
"@intlify/vue-i18n-extensions/vue-i18n/@intlify/core-base": ["@intlify/core-base@10.0.8", "", { "dependencies": { "@intlify/message-compiler": "10.0.8", "@intlify/shared": "10.0.8" } }, "sha512-FoHslNWSoHjdUBLy35bpm9PV/0LVI/DSv9L6Km6J2ad8r/mm0VaGg06C40FqlE8u2ADcGUM60lyoU7Myo4WNZQ=="],
"@intlify/vue-i18n-extensions/vue-i18n/@vue/devtools-api": ["@vue/devtools-api@6.6.4", "", {}, "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g=="],
"@isaacs/cliui/string-width/emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="],
"@nuxt/devtools/which/isexe": ["isexe@4.0.0", "", {}, "sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw=="],
@ -2533,6 +2611,8 @@
"@nuxtjs/color-mode/pkg-types/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
"@nuxtjs/turnstile/@nuxt/kit/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="],
"archiver-utils/glob/minimatch": ["minimatch@9.0.9", "", { "dependencies": { "brace-expansion": "^2.0.2" } }, "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg=="],
"archiver-utils/glob/path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="],

View file

@ -78,7 +78,8 @@ export default defineNuxtConfig({
appName: process.env.APP_TITLE || 'My App',
nodeEnv: process.env.NODE_ENV || '',
siteUrl: process.env.NUXT_PUBLIC_SITE_URL || '',
prayBaseUrl: process.env.NUXT_PUBLIC_PRAY_BASE_URL || 'https://pray.doxa.life'
prayBaseUrl: process.env.NUXT_PUBLIC_PRAY_BASE_URL || 'https://pray.doxa.life',
mapboxToken: process.env.NUXT_PUBLIC_MAPBOX_TOKEN || ''
}
},

View file

@ -17,10 +17,14 @@
"@iconify-json/simple-icons": "^1.2.78",
"@nuxt/ui": "^4.6.1",
"@nuxtjs/i18n": "^10.2.4",
"@nuxtjs/turnstile": "^1.1.2",
"bcrypt": "^6.0.0",
"fuse.js": "^7.3.0",
"intl-tel-input": "^26.7.5",
"jsonwebtoken": "^9.0.2",
"kysely": "^0.28.0",
"kysely-postgres-js": "^2.0.0",
"mapbox-gl": "^3.22.0",
"nodemailer": "^7.0.6",
"nodemailer-mailgun-transport": "^2.1.5",
"nuxt": "^4.4.2",

109
server/api/adopt.post.ts Normal file
View file

@ -0,0 +1,109 @@
// Port of marketing-theme/functions/adopt-rest-api.php.
// Browser POSTs adoption form + Turnstile token here; we verify the
// token server-side and forward the sanitized payload to the
// campaigns-sever public endpoint at `{prayBaseUrl}/api/adopt` with
// X-API-Key. Response passes through `{ status: 'needs_verification' }`
// or `{ status: 'success' }` so the client shows the matching screen.
import { defineEventHandler, readBody, createError } from 'h3'
interface AdoptBody {
first_name?: string
last_name?: string
email?: string
phone?: string
role?: string
church_name?: string
country?: string
people_group?: string
permission_to_contact?: boolean
confirm_public_display?: boolean
confirm_adoption?: boolean
language?: string
cf_turnstile?: string
}
function sanitizeText(s: unknown): string {
if (typeof s !== 'string') return ''
return s.trim().replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, '').slice(0, 5000)
}
function sanitizeEmail(s: unknown): string {
if (typeof s !== 'string') return ''
return s.trim().toLowerCase().slice(0, 320)
}
export default defineEventHandler(async (event) => {
const body = await readBody<AdoptBody>(event)
const config = useRuntimeConfig()
// 1. Turnstile verification.
const token = body?.cf_turnstile
if (!token) {
throw createError({ statusCode: 400, statusMessage: 'Invalid token' })
}
const turnstileResult = await verifyTurnstileToken(token)
if (!turnstileResult?.success) {
throw createError({ statusCode: 400, statusMessage: 'Invalid token' })
}
// 2. Sanitize inputs (matches PHP sanitize_text_field / sanitize_email).
const first_name = sanitizeText(body.first_name)
const last_name = sanitizeText(body.last_name)
const email = sanitizeEmail(body.email)
const phone = sanitizeText(body.phone)
const church_name = sanitizeText(body.church_name)
const country = sanitizeText(body.country)
const role = sanitizeText(body.role)
const confirm_adoption = Boolean(body.confirm_adoption)
const permission_to_contact = Boolean(body.permission_to_contact)
const confirm_public_display = Boolean(body.confirm_public_display)
const people_group = sanitizeText(body.people_group)
const language = sanitizeText(body.language) || 'en'
if (!email || !first_name || !last_name) {
throw createError({ statusCode: 400, statusMessage: 'Name and email are required' })
}
if (!confirm_adoption) {
throw createError({ statusCode: 400, statusMessage: 'Please confirm your adoption commitment' })
}
// 3. Forward to campaigns-sever (mirrors adopt-rest-api.php lines 88-107).
if (!config.prayBaseUrl || !config.formApiKey) {
throw createError({ statusCode: 500, statusMessage: 'Prayer Tools integration not configured' })
}
try {
const upstream = await $fetch<{ needs_verification?: boolean }>(
`${String(config.prayBaseUrl).replace(/\/$/, '')}/api/adopt`,
{
method: 'POST',
headers: {
'X-API-Key': config.formApiKey as string,
'Content-Type': 'application/json'
},
body: {
first_name,
last_name,
email,
phone,
role,
church_name,
country,
people_group,
permission_to_contact,
confirm_public_display,
language
},
timeout: 15_000
}
)
if (upstream?.needs_verification) {
return { status: 'needs_verification' as const }
}
return { status: 'success' as const }
} catch {
throw createError({ statusCode: 500, statusMessage: 'Failed to submit adoption. Please try again.' })
}
})