feat: update contact-us form to use the same format as feedback forms
This commit is contained in:
parent
b30f3c39f4
commit
0098a69d7e
18 changed files with 576 additions and 56 deletions
|
|
@ -2,7 +2,7 @@ textarea,
|
|||
select,
|
||||
input,
|
||||
.input {
|
||||
border: 1px solid var(--input-border-color);
|
||||
border: 1px solid gray;
|
||||
border-radius: 8px;
|
||||
background-color: var(--color-surface-white);
|
||||
width: 100%;
|
||||
|
|
@ -127,6 +127,43 @@ input[type=radio] {
|
|||
fill: var(--color-brand-secondary-light);
|
||||
}
|
||||
|
||||
/* Required-field marker: the red asterisk that follows a label's text. */
|
||||
.required-marker {
|
||||
color: var(--color-status-error);
|
||||
margin-inline-start: 0.15em;
|
||||
}
|
||||
|
||||
/* Per-field validation message, rendered directly under the field it belongs to. */
|
||||
.field-error {
|
||||
color: var(--color-status-error);
|
||||
font-size: var(--font-size-xs);
|
||||
}
|
||||
|
||||
/* Quieter form chrome: field outlines drop to the weight of placeholder text so
|
||||
the labels lead instead of the boxes. Opt in per form, not site-wide. */
|
||||
.form--quiet {
|
||||
textarea,
|
||||
select,
|
||||
input,
|
||||
.input {
|
||||
border-color: var(--input-border-color-muted);
|
||||
}
|
||||
}
|
||||
|
||||
/* Opt-in row: a switch beside small print, rather than a checkbox beside a label. */
|
||||
.form-consent {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-sm);
|
||||
|
||||
& .form-consent__label {
|
||||
font-size: var(--font-size-sm);
|
||||
color: var(--color-text-muted);
|
||||
max-width: 60ch;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.contact-message {
|
||||
&.error {
|
||||
color: var(--color-error, #c00);
|
||||
|
|
@ -139,4 +176,4 @@ input[type=radio] {
|
|||
.iti {
|
||||
width: 100%;
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -75,6 +75,7 @@ $color-red-600: #bd1218;
|
|||
--color-text-on-brand-lightest: #{$color-beige-50};
|
||||
--color-text-on-secondary-light: #{$color-green-600};
|
||||
--color-text-on-white: #{$color-green-600};
|
||||
--color-text-muted: #{$color-gray-500};
|
||||
--color-white: #{$color-white};
|
||||
|
||||
// Status
|
||||
|
|
@ -87,5 +88,8 @@ $color-red-600: #bd1218;
|
|||
|
||||
// forms
|
||||
--input-border-color: #{$color-gray-500};
|
||||
// Quieter field outline, the same weight as placeholder text — for forms that
|
||||
// should let their labels lead rather than their boxes (see `.form--quiet`).
|
||||
--input-border-color-muted: #{$color-gray-400};
|
||||
--table-border-color: #{$color-gray-500};
|
||||
}
|
||||
99
app/components/public/MessageTypeSelect.vue
Normal file
99
app/components/public/MessageTypeSelect.vue
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
<script setup lang="ts">
|
||||
// Message-category picker for the contact form, mirroring the campaigns-server
|
||||
// feedback form's three options. The values are the feedback types the
|
||||
// campaigns-server understands (`compliment` / `suggestion` / `problem`); only
|
||||
// the labels are worded for a contact-us page.
|
||||
|
||||
import type { MessageType } from '~/types/contact'
|
||||
|
||||
const model = defineModel<MessageType | null>({ default: null })
|
||||
|
||||
defineProps<{
|
||||
/** id of the field label, so the group is announced with it. */
|
||||
labelId?: string
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const options = computed<{ value: MessageType, label: string }[]>(() => [
|
||||
{ value: 'compliment', label: t('Compliment') },
|
||||
{ value: 'suggestion', label: t('Suggestion') },
|
||||
{ value: 'problem', label: t('Problem') }
|
||||
])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="cluster message-type-select"
|
||||
role="group"
|
||||
:aria-labelledby="labelId"
|
||||
>
|
||||
<button
|
||||
v-for="option in options"
|
||||
:key="option.value"
|
||||
type="button"
|
||||
class="button compact with-icon"
|
||||
:class="{ outline: model !== option.value }"
|
||||
:aria-pressed="model === option.value"
|
||||
@click="model = option.value"
|
||||
>
|
||||
<!-- Heart / lightbulb / warning-triangle, matching the feedback form's
|
||||
icon set. Stroked rather than filled, so `fill` is overridden here. -->
|
||||
<svg
|
||||
v-if="option.value === 'compliment'"
|
||||
class="icon width-md"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path d="M19 14c1.49-1.46 3-3.21 3-5.5A5.5 5.5 0 0 0 16.5 3c-1.76 0-3 .5-4.5 2-1.5-1.5-2.74-2-4.5-2A5.5 5.5 0 0 0 2 8.5c0 2.3 1.5 4.05 3 5.5l7 7Z" />
|
||||
</svg>
|
||||
<svg
|
||||
v-else-if="option.value === 'suggestion'"
|
||||
class="icon width-md"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path d="M15 14c.2-1 .7-1.7 1.5-2.5 1-.9 1.5-2.2 1.5-3.5A6 6 0 0 0 6 8c0 1 .2 2.2 1.5 3.5.7.7 1.3 1.5 1.5 2.5" />
|
||||
<path d="M9 18h6" />
|
||||
<path d="M10 22h4" />
|
||||
</svg>
|
||||
<svg
|
||||
v-else
|
||||
class="icon width-md"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3" />
|
||||
<path d="M12 9v4" />
|
||||
<path d="M12 17h.01" />
|
||||
</svg>
|
||||
{{ option.label }}
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* The shared `.icon` rule fills with currentColor; these are stroked outlines. */
|
||||
.message-type-select .icon {
|
||||
fill: none;
|
||||
}
|
||||
|
||||
.message-type-select .with-icon .icon {
|
||||
margin-inline-end: var(--spacing-2xs);
|
||||
}
|
||||
</style>
|
||||
72
app/components/public/ToggleSwitch.vue
Normal file
72
app/components/public/ToggleSwitch.vue
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
<script setup lang="ts">
|
||||
// Accessible on/off switch styled to match the campaigns-server feedback form.
|
||||
// Used instead of a checkbox for opt-ins where the control should read as a
|
||||
// preference rather than a form field. The label is rendered by the caller and
|
||||
// wired up through `label-id` so it stays part of the surrounding copy flow.
|
||||
|
||||
const model = defineModel<boolean>({ default: false })
|
||||
|
||||
defineProps<{
|
||||
/** id of the element labelling this switch, for `aria-labelledby`. */
|
||||
labelId?: string
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<button
|
||||
type="button"
|
||||
role="switch"
|
||||
class="toggle-switch"
|
||||
:class="{ 'is-on': model }"
|
||||
:aria-checked="model"
|
||||
:aria-labelledby="labelId"
|
||||
@click="model = !model"
|
||||
>
|
||||
<span class="toggle-switch__thumb" />
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.toggle-switch {
|
||||
--toggle-width: 2.75rem;
|
||||
--toggle-height: 1.5rem;
|
||||
--toggle-padding: 0.2rem;
|
||||
flex: none;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
width: var(--toggle-width);
|
||||
height: var(--toggle-height);
|
||||
padding: var(--toggle-padding);
|
||||
border: none;
|
||||
border-radius: 100px;
|
||||
background-color: var(--color-brand-secondary);
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.toggle-switch.is-on {
|
||||
background-color: var(--color-brand-primary);
|
||||
}
|
||||
|
||||
.toggle-switch:focus-visible {
|
||||
outline: max(1px, 0.1em) solid var(--color-brand-dark);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.toggle-switch__thumb {
|
||||
width: calc(var(--toggle-height) - (2 * var(--toggle-padding)));
|
||||
height: calc(var(--toggle-height) - (2 * var(--toggle-padding)));
|
||||
border-radius: 50%;
|
||||
background-color: var(--color-surface-white);
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
.toggle-switch.is-on .toggle-switch__thumb {
|
||||
transform: translateX(calc(var(--toggle-width) - var(--toggle-height)));
|
||||
}
|
||||
|
||||
/* The thumb travels the other way in RTL, where the track is mirrored. */
|
||||
[dir="rtl"] .toggle-switch.is-on .toggle-switch__thumb {
|
||||
transform: translateX(calc((var(--toggle-width) - var(--toggle-height)) * -1));
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,51 +1,87 @@
|
|||
<script setup lang="ts">
|
||||
// Port of marketing-theme/page-contact.php.
|
||||
// Markup, classes, field names, labels, placeholders, and submit flow
|
||||
// mirror the source exactly. The Turnstile widget is rendered via the
|
||||
// @nuxtjs/turnstile module; the form submits to our server proxy at
|
||||
// /api/contact which verifies the token and forwards to pray.doxa.life.
|
||||
// Contact form. Shares its shape and copy with the campaigns-server feedback
|
||||
// form (app/pages/feedback.vue there) — a required message category, the same
|
||||
// field labels and placeholders, a consent switch and a full-width submit —
|
||||
// because both open the same kind of inbox conversation. The category is sent
|
||||
// on as the campaigns-server `feedback_type`, while `source: doxa_life` (added
|
||||
// by the server proxy) keeps these messages identifiable in the inbox.
|
||||
//
|
||||
// The form posts to our server proxy at /api/contact which verifies the
|
||||
// Turnstile token and forwards to pray.doxa.life. Turnstile runs in
|
||||
// interaction-only mode, so it stays invisible unless Cloudflare decides the
|
||||
// visitor actually needs to be challenged.
|
||||
|
||||
import { COUNTRIES } from '~/utils/countries'
|
||||
import type { MessageType } from '~/types/contact'
|
||||
|
||||
const { t, locale } = useI18n()
|
||||
|
||||
const messageType = ref<MessageType | null>(null)
|
||||
const name = ref('')
|
||||
const email = ref('')
|
||||
const honeypot = ref('') // matches the `name="email"` trap in the PHP template
|
||||
const country = ref('')
|
||||
const message = ref('')
|
||||
const consent = ref(false)
|
||||
const turnstileToken = ref('')
|
||||
|
||||
const submitting = ref(false)
|
||||
const submitDisabled = computed(() => submitting.value || !turnstileToken.value)
|
||||
|
||||
const messageEl = ref<HTMLDivElement | null>(null)
|
||||
const typeError = ref('')
|
||||
const emailError = ref('')
|
||||
const messageError = ref('')
|
||||
|
||||
const messageText = ref('')
|
||||
const messageClass = ref<'success' | 'error' | ''>('')
|
||||
const formRef = ref<HTMLFormElement | null>(null)
|
||||
const turnstileRef = ref<{ reset: () => void } | null>(null)
|
||||
|
||||
const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
|
||||
|
||||
async function onSubmit(e: Event) {
|
||||
e.preventDefault()
|
||||
if (honeypot.value) return
|
||||
if (formRef.value && !formRef.value.checkValidity()) {
|
||||
formRef.value.reportValidity()
|
||||
|
||||
typeError.value = ''
|
||||
emailError.value = ''
|
||||
messageError.value = ''
|
||||
messageText.value = ''
|
||||
messageClass.value = ''
|
||||
|
||||
const trimmedEmail = email.value.trim()
|
||||
const trimmedMessage = message.value.trim()
|
||||
let valid = true
|
||||
if (!messageType.value) {
|
||||
typeError.value = t('Please choose what your message is about.')
|
||||
valid = false
|
||||
}
|
||||
if (!EMAIL_REGEX.test(trimmedEmail)) {
|
||||
emailError.value = t('Please enter a valid email address.')
|
||||
valid = false
|
||||
}
|
||||
if (!trimmedMessage) {
|
||||
messageError.value = t('Please enter a message.')
|
||||
valid = false
|
||||
}
|
||||
if (!valid) return
|
||||
|
||||
// The widget is invisible and re-arms itself periodically, so a missing token
|
||||
// means "not ready yet" rather than "failed" — say so instead of letting the
|
||||
// proxy reject the submission with a generic error.
|
||||
if (!turnstileToken.value) {
|
||||
messageClass.value = 'error'
|
||||
messageText.value = t('Verification is still loading. Please try again in a moment.')
|
||||
return
|
||||
}
|
||||
|
||||
submitting.value = true
|
||||
messageText.value = ''
|
||||
messageClass.value = ''
|
||||
|
||||
try {
|
||||
const response = await $fetch<{ status: string; message?: string }>('/api/contact', {
|
||||
const response = await $fetch<{ status: string, message?: string }>('/api/contact', {
|
||||
method: 'POST',
|
||||
body: {
|
||||
name: name.value,
|
||||
email: email.value,
|
||||
country: country.value,
|
||||
message: message.value,
|
||||
email: trimmedEmail,
|
||||
message: trimmedMessage,
|
||||
message_type: messageType.value,
|
||||
consent_doxa_general: consent.value,
|
||||
language: locale.value,
|
||||
cf_turnstile: turnstileToken.value
|
||||
|
|
@ -55,10 +91,10 @@ async function onSubmit(e: Event) {
|
|||
if (response?.status === 'success') {
|
||||
messageClass.value = 'success'
|
||||
messageText.value = t('Thank you for your message. We will get back to you soon!')
|
||||
window.goStats?.track('contact_submit', { metadata: { language: locale.value, country: country.value || null } })
|
||||
window.goStats?.track('contact_submit', { metadata: { language: locale.value, message_type: messageType.value } })
|
||||
messageType.value = null
|
||||
name.value = ''
|
||||
email.value = ''
|
||||
country.value = ''
|
||||
message.value = ''
|
||||
consent.value = false
|
||||
formRef.value?.reset()
|
||||
|
|
@ -78,16 +114,26 @@ async function onSubmit(e: Event) {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<div class="container page-content">
|
||||
<h1 class="page-title">{{ t('Contact Us') }}</h1>
|
||||
<div class="container page-content max-width-lg">
|
||||
<h1 class="page-title">
|
||||
{{ t('Contact Us') }}
|
||||
</h1>
|
||||
<p class="center contact-intro">
|
||||
{{ t("We'd love to hear from you — tell us what you love, share a suggestion, or report a problem.") }}
|
||||
</p>
|
||||
|
||||
<form
|
||||
id="contact-form"
|
||||
ref="formRef"
|
||||
class="stack stack--md max-width-lg center"
|
||||
class="stack stack--md center form--quiet"
|
||||
novalidate
|
||||
@submit="onSubmit"
|
||||
>
|
||||
<input type="hidden" name="action" value="contact_us">
|
||||
<input
|
||||
type="hidden"
|
||||
name="action"
|
||||
value="contact_us"
|
||||
>
|
||||
<input
|
||||
v-model="honeypot"
|
||||
type="email"
|
||||
|
|
@ -97,6 +143,28 @@ async function onSubmit(e: Event) {
|
|||
tabindex="-1"
|
||||
>
|
||||
|
||||
<div class="">
|
||||
<span
|
||||
id="message-type-label"
|
||||
class="field-label"
|
||||
>
|
||||
{{ t('What is your message about?') }}<span
|
||||
class="required-marker"
|
||||
aria-hidden="true"
|
||||
>*</span>
|
||||
</span>
|
||||
<MessageTypeSelect
|
||||
v-model="messageType"
|
||||
label-id="message-type-label"
|
||||
/>
|
||||
<p
|
||||
v-if="typeError"
|
||||
class="field-error"
|
||||
>
|
||||
{{ typeError }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="">
|
||||
<label for="name">{{ t('Name') }}</label>
|
||||
<input
|
||||
|
|
@ -104,13 +172,17 @@ async function onSubmit(e: Event) {
|
|||
v-model="name"
|
||||
type="text"
|
||||
name="name"
|
||||
required
|
||||
:placeholder="t('Enter your name')"
|
||||
:placeholder="t('Your name (optional)')"
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="">
|
||||
<label for="contact_email">{{ t('Email') }}</label>
|
||||
<label for="contact_email">
|
||||
{{ t('Email') }}<span
|
||||
class="required-marker"
|
||||
aria-hidden="true"
|
||||
>*</span>
|
||||
</label>
|
||||
<input
|
||||
id="contact_email"
|
||||
v-model="email"
|
||||
|
|
@ -119,60 +191,93 @@ async function onSubmit(e: Event) {
|
|||
required
|
||||
:placeholder="t('Enter your email')"
|
||||
>
|
||||
<p
|
||||
v-if="emailError"
|
||||
class="field-error"
|
||||
>
|
||||
{{ emailError }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="">
|
||||
<label for="country">{{ t('Your Country (optional)') }}</label>
|
||||
<select id="country" v-model="country" name="country">
|
||||
<option value="">{{ t('Select Country') }}</option>
|
||||
<option v-for="c in COUNTRIES" :key="c.value" :value="c.value">{{ c.label }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="">
|
||||
<label for="message">{{ t('Message') }}</label>
|
||||
<label for="message">
|
||||
{{ t('Message') }}<span
|
||||
class="required-marker"
|
||||
aria-hidden="true"
|
||||
>*</span>
|
||||
</label>
|
||||
<textarea
|
||||
id="message"
|
||||
v-model="message"
|
||||
name="message"
|
||||
rows="5"
|
||||
required
|
||||
:placeholder="t('Enter your message')"
|
||||
:placeholder="t('Tell us more…')"
|
||||
/>
|
||||
<p
|
||||
v-if="messageError"
|
||||
class="field-error"
|
||||
>
|
||||
{{ messageError }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="form-control color-primary-darker">
|
||||
<input
|
||||
id="consent-doxa-general"
|
||||
<div class="form-consent">
|
||||
<ToggleSwitch
|
||||
v-model="consent"
|
||||
type="checkbox"
|
||||
name="consent_doxa_general"
|
||||
label-id="consent-doxa-general"
|
||||
/>
|
||||
<span
|
||||
id="consent-doxa-general"
|
||||
class="form-consent__label"
|
||||
@click="consent = !consent"
|
||||
>
|
||||
<label for="consent-doxa-general">{{ t('I would like to receive email updates from the DOXA partnership.') }}</label>
|
||||
{{ t("I'd like to receive email updates from the DOXA partnership.") }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<NuxtTurnstile
|
||||
ref="turnstileRef"
|
||||
v-model="turnstileToken"
|
||||
:options="{ theme: 'light' }"
|
||||
:options="{ theme: 'light', appearance: 'interaction-only' }"
|
||||
/>
|
||||
|
||||
<div
|
||||
id="contact-message"
|
||||
ref="messageEl"
|
||||
class="contact-message"
|
||||
:class="messageClass"
|
||||
:style="{ display: messageText ? 'block' : 'none' }"
|
||||
>{{ messageText }}</div>
|
||||
>
|
||||
{{ messageText }}
|
||||
</div>
|
||||
|
||||
<button
|
||||
id="contact-submit"
|
||||
type="submit"
|
||||
class="button"
|
||||
:disabled="submitDisabled"
|
||||
class="button width-100"
|
||||
:disabled="submitting"
|
||||
>
|
||||
{{ submitting ? t('Submitting...') : t('Submit') }}
|
||||
{{ submitting ? t('Sending…') : t('Send message') }}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.contact-intro {
|
||||
color: var(--color-text-muted);
|
||||
margin-bottom: var(--spacing-lg);
|
||||
}
|
||||
|
||||
/* Matches the weight and spacing of the <label>s the other fields use, for the
|
||||
one field whose control is a button group rather than a labelable input. */
|
||||
.field-label {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* `.button` centres nothing by default because it is sized to its content; as a
|
||||
full-width submit it needs its own text centring. */
|
||||
#contact-submit {
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
4
app/types/contact.ts
Normal file
4
app/types/contact.ts
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
// Message categories the contact form offers. The values are the feedback types
|
||||
// the campaigns-server understands (`/api/contact` -> `feedback_type`); only the
|
||||
// labels are worded for a contact-us page.
|
||||
export type MessageType = 'compliment' | 'suggestion' | 'problem'
|
||||
1
bun.lock
1
bun.lock
|
|
@ -31,6 +31,7 @@
|
|||
"bcrypt": "^6.0.0",
|
||||
"fuse.js": "^7.3.0",
|
||||
"intl-tel-input": "^26.7.5",
|
||||
"jiti": "^2.7.0",
|
||||
"jsdom": "^29.1.0",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"kysely": "^0.28.0",
|
||||
|
|
|
|||
|
|
@ -1,7 +1,14 @@
|
|||
{
|
||||
<<<<<<< Updated upstream
|
||||
"Daily Prayer Coverage": "تغطية الصلاة اليومية",
|
||||
"%count% people groups in %countries% countries": "%count% مجموعة شعبية في %countries% دولة",
|
||||
"%region% has %count% people groups across %countries% countries. Each pin is coloured by how many people are praying for that people group.": "تضم %region% %count% مجموعة شعبية في %countries% دولة. يشير لون كل نقطة إلى عدد الأشخاص الذين يصلّون من أجل تلك المجموعة الشعبية.",
|
||||
=======
|
||||
"Compliment": "إطراء",
|
||||
"Daily Prayer Coverage": "تغطية صلاة يومية",
|
||||
"%count% people groups in %countries% countries": "%count% مجموعة بشرية في %countries% دولة",
|
||||
"%region% has %count% people groups across %countries% countries. Each pin is coloured by how many people are praying for that people group.": "تضم %region% %count% مجموعة بشرية في %countries% دولة. يشير لون كل نقطة إلى عدد الأشخاص الذين يصلّون من أجل تلك المجموعة البشرية.",
|
||||
>>>>>>> Stashed changes
|
||||
"All Regions": "جميع المناطق",
|
||||
"By Region": "حسب المنطقة",
|
||||
"Countries": "الدول",
|
||||
|
|
@ -9,10 +16,28 @@
|
|||
"Engagement Progress": "تقدُّم الانخراط",
|
||||
"Find unreached people groups by region and country.": "ابحث عن المجموعات الشعبية غير المبلَّغة حسب المنطقة والدولة.",
|
||||
"Goal": "الهدف",
|
||||
"I'd like to receive email updates from the DOXA partnership.": "أرغب في تلقي تحديثات عبر البريد الإلكتروني من شراكة DOXA.",
|
||||
"Other": "أخرى",
|
||||
"Outside this region": "خارج هذه المنطقة",
|
||||
<<<<<<< Updated upstream
|
||||
"People Groups": "مجموعات شعبية",
|
||||
"people groups engaged": "مجموعات شعبية مُخْدُومَة",
|
||||
=======
|
||||
"People Groups": "مجموعات بشرية",
|
||||
"Please choose what your message is about.": "يُرجى اختيار موضوع رسالتك.",
|
||||
"Please enter a message.": "يُرجى إدخال رسالة.",
|
||||
"Please enter a valid email address.": "يُرجى إدخال عنوان بريد إلكتروني صالح.",
|
||||
"Problem": "مشكلة",
|
||||
"Send message": "إرسال الرسالة",
|
||||
"Sending…": "جارٍ الإرسال…",
|
||||
"Suggestion": "اقتراح",
|
||||
"Tell us more…": "أخبرنا المزيد…",
|
||||
"Verification is still loading. Please try again in a moment.": "لا يزال التحقق قيد التحميل. يُرجى المحاولة مرة أخرى بعد لحظات.",
|
||||
"We'd love to hear from you — tell us what you love, share a suggestion, or report a problem.": "يسعدنا أن نسمع منك: أخبرنا بما يعجبك، أو شارك اقتراحًا، أو أبلغ عن مشكلة.",
|
||||
"What is your message about?": "عمّ تدور رسالتك؟",
|
||||
"Your name (optional)": "اسمك (اختياري)",
|
||||
"people groups engaged": "مجموعات بشرية مُنخرِطة",
|
||||
>>>>>>> Stashed changes
|
||||
"Regions": "المناطق",
|
||||
"See %region% dashboard": "عرض لوحة معلومات %region%",
|
||||
"There are %count% people groups in %country%. Check the map for the people groups on the border of this country as well.": "توجد %count% مجموعة شعبية في %country%. تحقق من الخريطة لمعرفة المجموعات الشعبية الواقعة على حدود هذه الدولة أيضًا.",
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
{
|
||||
"Compliment": "Lob",
|
||||
"Daily Prayer Coverage": "Tägliche Gebetsabdeckung",
|
||||
"%count% people groups in %countries% countries": "%count% Volksgruppen in %countries% Ländern",
|
||||
"%region% has %count% people groups across %countries% countries. Each pin is coloured by how many people are praying for that people group.": "%region% hat %count% Volksgruppen in %countries% Ländern. Die Farbe jedes Punktes zeigt, wie viele Menschen für diese Volksgruppe beten.",
|
||||
|
|
@ -9,9 +10,22 @@
|
|||
"Engagement Progress": "Fortschritt der Arbeit vor Ort",
|
||||
"Find unreached people groups by region and country.": "Finde unerreichte Volksgruppen nach Region und Land.",
|
||||
"Goal": "Ziel",
|
||||
"I'd like to receive email updates from the DOXA partnership.": "Ich möchte E-Mail-Benachrichtigungen von der DOXA-Partnerschaft erhalten.",
|
||||
"Other": "Andere",
|
||||
"Outside this region": "Außerhalb dieser Region",
|
||||
"People Groups": "Volksgruppen",
|
||||
"Please choose what your message is about.": "Bitte wählen Sie, worum es in Ihrer Nachricht geht.",
|
||||
"Please enter a message.": "Bitte geben Sie eine Nachricht ein.",
|
||||
"Please enter a valid email address.": "Bitte geben Sie eine gültige E-Mail-Adresse ein.",
|
||||
"Problem": "Problem",
|
||||
"Send message": "Nachricht senden",
|
||||
"Sending…": "Wird gesendet…",
|
||||
"Suggestion": "Vorschlag",
|
||||
"Tell us more…": "Erzählen Sie uns mehr…",
|
||||
"Verification is still loading. Please try again in a moment.": "Die Überprüfung wird noch geladen. Bitte versuchen Sie es in einem Moment erneut.",
|
||||
"We'd love to hear from you — tell us what you love, share a suggestion, or report a problem.": "Wir freuen uns, von Ihnen zu hören – sagen Sie uns, was Ihnen gefällt, teilen Sie einen Vorschlag mit oder melden Sie ein Problem.",
|
||||
"What is your message about?": "Worum geht es in Ihrer Nachricht?",
|
||||
"Your name (optional)": "Ihr Name (optional)",
|
||||
"people groups engaged": "engagierte Volksgruppen",
|
||||
"Regions": "Regionen",
|
||||
"See %region% dashboard": "Dashboard anzeigen: %region%",
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
{
|
||||
"Compliment": "Compliment",
|
||||
"Daily Prayer Coverage": "Daily Prayer Coverage",
|
||||
"%count% people groups in %countries% countries": "%count% people groups in %countries% countries",
|
||||
"%region% has %count% people groups across %countries% countries. Each pin is coloured by how many people are praying for that people group.": "%region% has %count% people groups across %countries% countries. Each pin is coloured by how many people are praying for that people group.",
|
||||
|
|
@ -50,6 +51,7 @@
|
|||
"Engagement Progress": "Engagement Progress",
|
||||
"Find unreached people groups by region and country.": "Find unreached people groups by region and country.",
|
||||
"Goal": "Goal",
|
||||
"I'd like to receive email updates from the DOXA partnership.": "I'd like to receive email updates from the DOXA partnership.",
|
||||
"No One Praying": "No One Praying",
|
||||
"Contact": "Contact",
|
||||
"Contact Page": "Contact Page",
|
||||
|
|
@ -165,6 +167,18 @@
|
|||
"People Group": "People Group",
|
||||
"People Group Not Found": "People Group Not Found",
|
||||
"People committed to praying": "People committed to praying",
|
||||
"Please choose what your message is about.": "Please choose what your message is about.",
|
||||
"Please enter a message.": "Please enter a message.",
|
||||
"Please enter a valid email address.": "Please enter a valid email address.",
|
||||
"Problem": "Problem",
|
||||
"Send message": "Send message",
|
||||
"Sending…": "Sending…",
|
||||
"Suggestion": "Suggestion",
|
||||
"Tell us more…": "Tell us more…",
|
||||
"Verification is still loading. Please try again in a moment.": "Verification is still loading. Please try again in a moment.",
|
||||
"We'd love to hear from you — tell us what you love, share a suggestion, or report a problem.": "We'd love to hear from you — tell us what you love, share a suggestion, or report a problem.",
|
||||
"What is your message about?": "What is your message about?",
|
||||
"Your name (optional)": "Your name (optional)",
|
||||
"people groups engaged": "people groups engaged",
|
||||
"People groups with complete daily prayer coverage": "People groups with complete daily prayer coverage",
|
||||
"Per Page": "Per Page",
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
{
|
||||
"Compliment": "Felicitación",
|
||||
"Daily Prayer Coverage": "Cobertura diaria de oración",
|
||||
"%count% people groups in %countries% countries": "%count% grupos étnicos en %countries% países",
|
||||
"%region% has %count% people groups across %countries% countries. Each pin is coloured by how many people are praying for that people group.": "%region% tiene %count% grupos étnicos en %countries% países. El color de cada punto indica cuántas personas oran por ese grupo étnico.",
|
||||
|
|
@ -9,9 +10,22 @@
|
|||
"Engagement Progress": "Progreso del compromiso",
|
||||
"Find unreached people groups by region and country.": "Encuentra grupos étnicos no alcanzados por región y país.",
|
||||
"Goal": "Meta",
|
||||
"I'd like to receive email updates from the DOXA partnership.": "Me gustaría recibir novedades por correo electrónico de la alianza DOXA.",
|
||||
"Other": "Otros",
|
||||
"Outside this region": "Fuera de esta región",
|
||||
"People Groups": "Grupos Étnicos",
|
||||
"Please choose what your message is about.": "Elige sobre qué es tu mensaje.",
|
||||
"Please enter a message.": "Escribe un mensaje.",
|
||||
"Please enter a valid email address.": "Introduce una dirección de correo válida.",
|
||||
"Problem": "Problema",
|
||||
"Send message": "Enviar mensaje",
|
||||
"Sending…": "Enviando…",
|
||||
"Suggestion": "Sugerencia",
|
||||
"Tell us more…": "Cuéntanos más…",
|
||||
"Verification is still loading. Please try again in a moment.": "La verificación aún se está cargando. Inténtalo de nuevo en un momento.",
|
||||
"We'd love to hear from you — tell us what you love, share a suggestion, or report a problem.": "Nos encantaría saber de ti: cuéntanos qué te gusta, comparte una sugerencia o informa de un problema.",
|
||||
"What is your message about?": "¿Sobre qué es tu mensaje?",
|
||||
"Your name (optional)": "Tu nombre (opcional)",
|
||||
"people groups engaged": "grupos étnicos comprometidos",
|
||||
"Regions": "Regiones",
|
||||
"See %region% dashboard": "Ver el panel de %region%",
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
{
|
||||
"Compliment": "Compliment",
|
||||
"Daily Prayer Coverage": "Couverture de prière quotidienne",
|
||||
"%count% people groups in %countries% countries": "%count% peuples dans %countries% pays",
|
||||
"%region% has %count% people groups across %countries% countries. Each pin is coloured by how many people are praying for that people group.": "%region% compte %count% peuples répartis dans %countries% pays. La couleur de chaque point indique combien de personnes prient pour ce peuple.",
|
||||
|
|
@ -9,9 +10,22 @@
|
|||
"Engagement Progress": "Progrès de l'engagement",
|
||||
"Find unreached people groups by region and country.": "Trouvez des peuples non atteints par région et par pays.",
|
||||
"Goal": "Objectif",
|
||||
"I'd like to receive email updates from the DOXA partnership.": "Je souhaite recevoir des actualités par e-mail du partenariat DOXA.",
|
||||
"Other": "Autre",
|
||||
"Outside this region": "Hors de cette région",
|
||||
"People Groups": "Peuples",
|
||||
"Please choose what your message is about.": "Veuillez choisir de quoi parle votre message.",
|
||||
"Please enter a message.": "Veuillez saisir un message.",
|
||||
"Please enter a valid email address.": "Veuillez saisir une adresse e-mail valide.",
|
||||
"Problem": "Problème",
|
||||
"Send message": "Envoyer le message",
|
||||
"Sending…": "Envoi…",
|
||||
"Suggestion": "Suggestion",
|
||||
"Tell us more…": "Dites-nous-en plus…",
|
||||
"Verification is still loading. Please try again in a moment.": "La vérification est en cours de chargement. Veuillez réessayer dans un instant.",
|
||||
"We'd love to hear from you — tell us what you love, share a suggestion, or report a problem.": "Nous aimerions beaucoup vous lire : dites-nous ce que vous appréciez, partagez une suggestion ou signalez un problème.",
|
||||
"What is your message about?": "De quoi parle votre message ?",
|
||||
"Your name (optional)": "Votre nom (facultatif)",
|
||||
"people groups engaged": "peuples engagés",
|
||||
"Regions": "Régions",
|
||||
"See %region% dashboard": "Voir le tableau de bord : %region%",
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
{
|
||||
"Compliment": "प्रशंसा",
|
||||
"Daily Prayer Coverage": "दैनिक प्रार्थना कवरेज",
|
||||
<<<<<<< Updated upstream
|
||||
"%count% people groups in %countries% countries": "%countries% देशों में %count% जनसमूह",
|
||||
"%region% has %count% people groups across %countries% countries. Each pin is coloured by how many people are praying for that people group.": "%region% में %countries% देशों के %count% जनसमूह हैं। हर बिंदु का रंग बताता है कि उस जनसमूह के लिए कितने लोग प्रार्थना कर रहे हैं।",
|
||||
"A church-led commitment to pray, give, and send so that gospel access begins": "कलीसिया की अगुवाई में प्रार्थना करने, दान देने और भेजने की प्रतिबद्धता, ताकि सुसमाचार तक पहुँच शुरू हो",
|
||||
|
|
@ -8,6 +10,43 @@
|
|||
"About": "हमारे बारे में",
|
||||
"Actively help surface and support potential goers, encouraging those God may be calling to cross cultures and serve this people group directly.": "सक्रिय रूप से संभावित जाने वालों को पहचानने और उनका साथ देने में मदद करें, और उन लोगों को प्रोत्साहित करें जिन्हें परमेश्वर संभवतः संस्कृतियों को पार करके इस जनसमूह की सीधे सेवा करने के लिए बुला रहे हैं।",
|
||||
"Additional tools to help your church stay engaged in prayer for this people group": "इस जनसमूह के लिए आपकी कलीसिया को प्रार्थना में लगे रहने में मदद करने वाले अतिरिक्त साधन",
|
||||
=======
|
||||
"%count% people groups in %countries% countries": "%countries% देशों में %count% लोगों के समूह",
|
||||
"%region% has %count% people groups across %countries% countries. Each pin is coloured by how many people are praying for that people group.": "%region% में %countries% देशों में %count% लोगों के समूह हैं। प्रत्येक बिंदु का रंग दर्शाता है कि उस लोगों के समूह के लिए कितने लोग प्रार्थना कर रहे हैं।",
|
||||
"All Regions": "सभी क्षेत्र",
|
||||
"By Region": "क्षेत्र के अनुसार",
|
||||
"Countries": "देश",
|
||||
"Countries in this region": "इस क्षेत्र के देश",
|
||||
"Engagement Progress": "प्रवेश की प्रगति",
|
||||
"Find unreached people groups by region and country.": "क्षेत्र और देश के अनुसार अगम्य लोगों के समूह खोजें।",
|
||||
"Goal": "लक्ष्य",
|
||||
"I'd like to receive email updates from the DOXA partnership.": "मैं DOXA साझेदारी से ईमेल अपडेट प्राप्त करना चाहूंगा।",
|
||||
"Other": "अन्य",
|
||||
"Outside this region": "इस क्षेत्र के बाहर",
|
||||
"People Groups": "लोगों के समूह",
|
||||
"Please choose what your message is about.": "कृपया चुनें कि आपका संदेश किस बारे में है।",
|
||||
"Please enter a message.": "कृपया एक संदेश दर्ज करें।",
|
||||
"Please enter a valid email address.": "कृपया एक मान्य ईमेल पता दर्ज करें।",
|
||||
"Problem": "समस्या",
|
||||
"Send message": "संदेश भेजें",
|
||||
"Sending…": "भेजा जा रहा है…",
|
||||
"Suggestion": "सुझाव",
|
||||
"Tell us more…": "हमें और बताएं…",
|
||||
"Verification is still loading. Please try again in a moment.": "सत्यापन अभी लोड हो रहा है। कृपया कुछ क्षण बाद पुनः प्रयास करें।",
|
||||
"We'd love to hear from you — tell us what you love, share a suggestion, or report a problem.": "हमें आपसे सुनकर खुशी होगी — हमें बताएं कि आपको क्या पसंद है, कोई सुझाव साझा करें, या कोई समस्या बताएं।",
|
||||
"What is your message about?": "आपका संदेश किस बारे में है?",
|
||||
"Your name (optional)": "आपका नाम (वैकल्पिक)",
|
||||
"people groups engaged": "प्रवेशित लोगों के समूह",
|
||||
"Regions": "क्षेत्र",
|
||||
"See %region% dashboard": "%region% डैशबोर्ड देखें",
|
||||
"There are %count% people groups in %country%. Check the map for the people groups on the border of this country as well.": "%country% में %count% लोगों के समूह हैं। इस देश की सीमा पर स्थित लोगों के समूहों के लिए मानचित्र भी देखें।",
|
||||
"A church-led commitment to pray, give, and send so that gospel access begins": "सुसमाचार की पहुँच शुरू हो सके, इसके लिए चर्च के नेतृत्व में प्रार्थना करने, दान देने और भेजने की प्रतिबद्धता।",
|
||||
"A clean, elegant theme for DOXA Life - Engaging every people group by 2033": "DOXA Life के लिए एक स्वच्छ, सुरुचिपूर्ण थीम - 2033 तक प्रत्येक जनसमूह को संलग्न करना",
|
||||
"A simple path to faithful obedience": "वफादार आज्ञाकारिता का एक सरल मार्ग",
|
||||
"About": "बारे में",
|
||||
"Actively help surface and support potential goers, encouraging those God may be calling to cross cultures and serve this people group directly.": "संभावित जाने वालों की पहचान करने और उन्हें समर्थन देने में सक्रिय रूप से मदद करें, और उन लोगों को प्रोत्साहित करें जिन्हें ईश्वर शायद विभिन्न संस्कृतियों को पार करके इस जनसमूह की सीधे सेवा करने के लिए बुला रहे हैं।",
|
||||
"Additional tools to help your church stay engaged in prayer for this people group": "इस जनसमूह के लिए प्रार्थना में आपकी चर्च को संलग्न बनाए रखने में मदद करने के लिए अतिरिक्त उपकरण",
|
||||
>>>>>>> Stashed changes
|
||||
"Adopt": "अपनाएँ",
|
||||
"Adopt an unengaged people group": "एक अनसंपर्कित जनसमूह को अपनाएँ",
|
||||
"Adopt people group": "जनसमूह को अपनाएँ",
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
{
|
||||
"Compliment": "Elogio",
|
||||
"Daily Prayer Coverage": "Cobertura diária de oração",
|
||||
"%count% people groups in %countries% countries": "%count% grupos étnicos em %countries% países",
|
||||
"%region% has %count% people groups across %countries% countries. Each pin is coloured by how many people are praying for that people group.": "%region% tem %count% grupos étnicos em %countries% países. A cor de cada ponto indica quantas pessoas oram por esse grupo étnico.",
|
||||
|
|
@ -9,9 +10,22 @@
|
|||
"Engagement Progress": "Progresso do engajamento",
|
||||
"Find unreached people groups by region and country.": "Encontre grupos étnicos não alcançados por região e país.",
|
||||
"Goal": "Meta",
|
||||
"I'd like to receive email updates from the DOXA partnership.": "Gostaria de receber novidades por e-mail da parceria DOXA.",
|
||||
"Other": "Outros",
|
||||
"Outside this region": "Fora desta região",
|
||||
"People Groups": "Grupos étnicos",
|
||||
"Please choose what your message is about.": "Escolha sobre o que é a sua mensagem.",
|
||||
"Please enter a message.": "Escreva uma mensagem.",
|
||||
"Please enter a valid email address.": "Introduza um endereço de e-mail válido.",
|
||||
"Problem": "Problema",
|
||||
"Send message": "Enviar mensagem",
|
||||
"Sending…": "A enviar…",
|
||||
"Suggestion": "Sugestão",
|
||||
"Tell us more…": "Conte-nos mais…",
|
||||
"Verification is still loading. Please try again in a moment.": "A verificação ainda está a carregar. Tente novamente dentro de momentos.",
|
||||
"We'd love to hear from you — tell us what you love, share a suggestion, or report a problem.": "Adoraríamos ouvir você: diga-nos o que gosta, partilhe uma sugestão ou relate um problema.",
|
||||
"What is your message about?": "Sobre o que é a sua mensagem?",
|
||||
"Your name (optional)": "O seu nome (opcional)",
|
||||
"people groups engaged": "grupos étnicos engajados",
|
||||
"Regions": "Regiões",
|
||||
"See %region% dashboard": "Ver o painel: %region%",
|
||||
|
|
|
|||
|
|
@ -1,7 +1,14 @@
|
|||
{
|
||||
<<<<<<< Updated upstream
|
||||
"Daily Prayer Coverage": "Ежедневный молитвенный охват",
|
||||
"%count% people groups in %countries% countries": "%count% народов в %countries% странах",
|
||||
"%region% has %count% people groups across %countries% countries. Each pin is coloured by how many people are praying for that people group.": "В регионе %region% — %count% народов в %countries% странах. Цвет каждой точки показывает, сколько человек молится за этот народ.",
|
||||
=======
|
||||
"Compliment": "Благодарность",
|
||||
"Daily Prayer Coverage": "Ежедневная молитвенная поддержка",
|
||||
"%count% people groups in %countries% countries": "%count% групп людей в %countries% странах",
|
||||
"%region% has %count% people groups across %countries% countries. Each pin is coloured by how many people are praying for that people group.": "В регионе %region% — %count% групп людей в %countries% странах. Цвет каждой точки показывает, сколько человек молится за эту группу людей.",
|
||||
>>>>>>> Stashed changes
|
||||
"All Regions": "Все регионы",
|
||||
"By Region": "По регионам",
|
||||
"Countries": "Страны",
|
||||
|
|
@ -9,10 +16,28 @@
|
|||
"Engagement Progress": "Прогресс вовлечения",
|
||||
"Find unreached people groups by region and country.": "Найдите недостигнутые народы по регионам и странам.",
|
||||
"Goal": "Цель",
|
||||
"I'd like to receive email updates from the DOXA partnership.": "Я хочу получать новости партнёрства DOXA по электронной почте.",
|
||||
"Other": "Другое",
|
||||
"Outside this region": "За пределами этого региона",
|
||||
<<<<<<< Updated upstream
|
||||
"People Groups": "Народы",
|
||||
"people groups engaged": "вовлечённые народы",
|
||||
=======
|
||||
"People Groups": "Группы людей",
|
||||
"Please choose what your message is about.": "Выберите, о чём ваше сообщение.",
|
||||
"Please enter a message.": "Введите сообщение.",
|
||||
"Please enter a valid email address.": "Введите действительный адрес электронной почты.",
|
||||
"Problem": "Проблема",
|
||||
"Send message": "Отправить сообщение",
|
||||
"Sending…": "Отправка…",
|
||||
"Suggestion": "Предложение",
|
||||
"Tell us more…": "Расскажите подробнее…",
|
||||
"Verification is still loading. Please try again in a moment.": "Проверка ещё загружается. Повторите попытку через мгновение.",
|
||||
"We'd love to hear from you — tell us what you love, share a suggestion, or report a problem.": "Мы будем рады услышать вас: расскажите, что вам нравится, поделитесь предложением или сообщите о проблеме.",
|
||||
"What is your message about?": "О чём ваше сообщение?",
|
||||
"Your name (optional)": "Ваше имя (необязательно)",
|
||||
"people groups engaged": "вовлечённые группы людей",
|
||||
>>>>>>> Stashed changes
|
||||
"Regions": "Регионы",
|
||||
"See %region% dashboard": "Открыть панель: %region%",
|
||||
"There are %count% people groups in %country%. Check the map for the people groups on the border of this country as well.": "Страна %country% насчитывает %count% народов. Проверьте также на карте народы, расположенные вдоль границ этой страны.",
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@
|
|||
"bcrypt": "^6.0.0",
|
||||
"fuse.js": "^7.3.0",
|
||||
"intl-tel-input": "^26.7.5",
|
||||
"jiti": "^2.7.0",
|
||||
"jsdom": "^29.1.0",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"kysely": "^0.28.0",
|
||||
|
|
|
|||
|
|
@ -5,11 +5,22 @@
|
|||
|
||||
import { defineEventHandler, readBody, createError } from 'h3'
|
||||
|
||||
// Message categories the form offers. They map onto the campaigns-server
|
||||
// feedback types, which give the inbox conversation a `[Type]` subject prefix
|
||||
// and a filterable tag. The server re-validates, so this list only needs to
|
||||
// stay in sync with the labels rendered by app/pages/contact-us.vue.
|
||||
const MESSAGE_TYPES = ['compliment', 'suggestion', 'problem']
|
||||
|
||||
// Origin marker the campaigns-server records on the conversation, so messages
|
||||
// from this site stay distinguishable from in-app feedback (which shares the
|
||||
// same pipeline). Must be an allowlisted value on the receiving end.
|
||||
const SOURCE = 'doxa_life'
|
||||
|
||||
interface ContactBody {
|
||||
name?: string
|
||||
email?: string
|
||||
country?: string
|
||||
message?: string
|
||||
message_type?: string
|
||||
consent_doxa_general?: boolean
|
||||
language?: string
|
||||
cf_turnstile?: string
|
||||
|
|
@ -43,14 +54,19 @@ export default defineEventHandler(async (event) => {
|
|||
// 2. Sanitize inputs (matches PHP sanitize_text_field / sanitize_email)
|
||||
const name = sanitizeText(body.name)
|
||||
const email = sanitizeEmail(body.email)
|
||||
const country = sanitizeText(body.country)
|
||||
const message = sanitizeText(body.message)
|
||||
const consent = Boolean(body.consent_doxa_general)
|
||||
const language = sanitizeText(body.language) || 'en'
|
||||
const messageType = MESSAGE_TYPES.includes(sanitizeText(body.message_type))
|
||||
? sanitizeText(body.message_type)
|
||||
: ''
|
||||
|
||||
if (!email || !message) {
|
||||
throw createError({ statusCode: 400, statusMessage: 'Email and message are required' })
|
||||
}
|
||||
if (!messageType) {
|
||||
throw createError({ statusCode: 400, statusMessage: 'Please choose what your message is about' })
|
||||
}
|
||||
|
||||
// 3. Forward to campaigns-sever (mirrors contact-rest-api.php)
|
||||
if (!config.prayBaseUrl || !config.formApiKey) {
|
||||
|
|
@ -67,8 +83,9 @@ export default defineEventHandler(async (event) => {
|
|||
body: {
|
||||
name,
|
||||
email,
|
||||
country,
|
||||
message,
|
||||
feedback_type: messageType,
|
||||
source: SOURCE,
|
||||
consent_doxa_general: consent,
|
||||
language
|
||||
},
|
||||
|
|
|
|||
|
|
@ -4,6 +4,29 @@ import * as path from 'path'
|
|||
import { pathToFileURL } from 'url'
|
||||
import { getDb } from '../utils/database'
|
||||
|
||||
// Migration files are TypeScript, and they are imported at runtime by absolute
|
||||
// path — the bundler never sees them, so the server's own runtime has to be able
|
||||
// to load .ts. Production runs under Bun, which can; `nuxt dev` runs Nitro under
|
||||
// Node, which only strips types from 22.18 (or behind --experimental-strip-types
|
||||
// before that) and otherwise throws ERR_UNKNOWN_FILE_EXTENSION. Shipping .js
|
||||
// migrations wouldn't fix it either — layers bring their own .ts ones. So: try the
|
||||
// native import, and fall back to jiti, which transpiles on the fly.
|
||||
let jitiImport: ((path: string) => Promise<unknown>) | undefined
|
||||
|
||||
async function importMigration(fullPath: string): Promise<Migration> {
|
||||
try {
|
||||
return await import(/* @vite-ignore */ pathToFileURL(fullPath).href) as Migration
|
||||
} catch (err) {
|
||||
if ((err as NodeJS.ErrnoException)?.code !== 'ERR_UNKNOWN_FILE_EXTENSION') throw err
|
||||
if (!jitiImport) {
|
||||
const { createJiti } = await import('jiti')
|
||||
const jiti = createJiti(import.meta.url)
|
||||
jitiImport = (path: string) => jiti.import(path)
|
||||
}
|
||||
return await jitiImport(fullPath) as Migration
|
||||
}
|
||||
}
|
||||
|
||||
// Reads migration files from multiple folders (consumer + each layer's migrations/).
|
||||
// Filenames are sorted globally for a stable order. Each layer prefixes its files
|
||||
// (e.g. `oauth_001_*`) to avoid collision with the consumer's numeric-prefixed names.
|
||||
|
|
@ -25,9 +48,7 @@ class MultiFolderMigrationProvider implements MigrationProvider {
|
|||
if (all[name]) {
|
||||
throw new Error(`Migration name collision: "${name}" appears in multiple layers. Rename one (use a layer prefix like \`oauth_001_*\`).`)
|
||||
}
|
||||
const fullPath = path.join(folder, file)
|
||||
const mod = await import(pathToFileURL(fullPath).href)
|
||||
all[name] = mod
|
||||
all[name] = await importMigration(path.join(folder, file))
|
||||
}
|
||||
}
|
||||
return all
|
||||
|
|
|
|||
Loading…
Reference in a new issue