phase 4
This commit is contained in:
parent
7eb566a96b
commit
d5e06c8c71
10 changed files with 1607 additions and 13 deletions
261
app/assets/styles/prayer-map.css
Normal file
261
app/assets/styles/prayer-map.css
Normal 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;
|
||||
}
|
||||
}
|
||||
434
app/components/public/PrayerMap.vue
Normal file
434
app/components/public/PrayerMap.vue
Normal 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'}">×</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>
|
||||
|
|
@ -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
336
app/pages/adopt/[slug].vue
Normal 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
182
app/pages/adopt/index.vue
Normal 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>
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
84
bun.lock
84
bun.lock
|
|
@ -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=="],
|
||||
|
|
|
|||
|
|
@ -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 || ''
|
||||
}
|
||||
},
|
||||
|
||||
|
|
|
|||
|
|
@ -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
109
server/api/adopt.post.ts
Normal 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.' })
|
||||
}
|
||||
})
|
||||
Loading…
Reference in a new issue