feat(forms): add local Form A presentation preview

This commit is contained in:
DeCentN2Madness committed 2026-10-25 13:04:00 -07:00
1 parent b102a061fe
commit 7a6cab29db
13 files changed
+983 -8

No files matched your search

+9 -1
View File
@@ -25,7 +25,8 @@ const mockResult = resolveMockFormResult(import.meta.env.PUBLIC_AIA_FORMS_MOCK_R
<label>{text.email}<input type="email" name="email" autocomplete="email" disabled={mode === 'off'} /></label>
<label>{text.phone}<input type="tel" name="phone" autocomplete="tel" disabled={mode === 'off'} /></label>
<label>{text.service}<select name="service" disabled={mode === 'off'}>{text.options.map((label) => <option>{label}</option>)}</select></label>
<label>{text.message}<textarea name="message" rows="5" disabled={mode === 'off'}></textarea></label>
<label>{text.message}<textarea name="message" rows="5" aria-describedby="contact-sensitive-note" disabled={mode === 'off'}></textarea></label>
<p id="contact-sensitive-note" class="contact-form-card__caution" role="note">{text.formAPreview.caution}</p>
<Button type="button" data-mock-submit disabled={mode === 'off'} aria-describedby="contact-form-note" class="contact-form-card__submit">{text.submit}</Button>
<p id="contact-form-note" class="contact-form-card__note" data-form-status>{mode === 'mock' ? text.mock : <>{text.disabled} <ContactObfuscation phone="(480) 687-7099" hrefValue="+14806877099" obfuscatedText="9907-786 (084)" />.</>}</p>
</div>
@@ -99,4 +100,11 @@ const mockResult = resolveMockFormResult(import.meta.env.PUBLIC_AIA_FORMS_MOCK_R
margin: 0;
text-align: center;
}
.contact-form-card__caution {
color: var(--color-muted);
font-size: .85rem;
line-height: 1.65;
margin: -.25rem 0 0;
}
</style>
+279
View File
@@ -0,0 +1,279 @@
---
import { formAPreviewEnabled, formModeForRuntime, resolveFormMode } from '../utils/form-integration';
import { formMessages } from '../utils/form-messages';
interface Props {
lang?: 'en' | 'es';
variant?: 'full' | 'compact';
submitText?: string;
}
const { lang = 'en', variant = 'full', submitText } = Astro.props;
const text = formMessages(lang).formAPreview;
const config = resolveFormMode(import.meta.env.PUBLIC_AIA_FORMS_MODE, import.meta.env.PUBLIC_AIA_FORMS_LIVE_APPROVED);
const mode = formModeForRuntime(config);
const enabled = formAPreviewEnabled(import.meta.env.PUBLIC_AIA_FORM_A_PREVIEW, import.meta.env.PUBLIC_ALLOW_INDEXING, mode);
const prefix = `form-a-${variant}`;
const buttonLabel = submitText ?? text.submit;
---
<div
class:list={['form-a-preview', `form-a-preview--${variant}`]}
role="group"
aria-labelledby={`${prefix}-heading`}
data-form-a-preview
data-form-a-variant={variant}
data-form-a-preview-enabled={String(enabled)}
data-form-mode={mode}
data-required-error={text.requiredError}
data-email-error={text.emailError}
data-email-required={text.emailRequired}
data-email-optional={text.optional}
data-success={text.success}
>
<p class="form-a-preview__eyebrow">{text.internalPreview}</p>
<h3 id={`${prefix}-heading`}>{text.heading}</h3>
{variant === 'full' && <p class="form-a-preview__note">{text.previewNote}</p>}
<div class="form-a-preview__fields">
<div class="form-a-preview__field form-a-preview__field--full">
<label for={`${prefix}-name`}>{text.name}<span aria-hidden="true"> *</span></label>
<input id={`${prefix}-name`} name="name" autocomplete="name" required disabled={!enabled} />
</div>
<div class="form-a-preview__field">
<label for={`${prefix}-phone`}>{text.phone}<span aria-hidden="true"> *</span></label>
<input id={`${prefix}-phone`} name="phone" type="tel" autocomplete="tel" required disabled={!enabled} />
</div>
<div class="form-a-preview__field">
<label for={`${prefix}-email`}>{text.email} <span id={`${prefix}-email-requirement`} class="form-a-preview__optional">({text.optional})</span></label>
<input id={`${prefix}-email`} name="email" type="email" autocomplete="email" aria-describedby={`${prefix}-email-requirement`} disabled={!enabled} />
</div>
<div class="form-a-preview__field form-a-preview__field--full">
<label for={`${prefix}-preferred`}>{text.preferredContact}<span aria-hidden="true"> *</span></label>
<select id={`${prefix}-preferred`} name="preferredContact" required disabled={!enabled}>
<option value="">{text.choose}</option>
<option value="phone">{text.phoneOption}</option>
<option value="email">{text.emailOption}</option>
</select>
</div>
{variant === 'full' && <div class="form-a-preview__field form-a-preview__field--full">
<label for={`${prefix}-message`}>{text.message} <span class="form-a-preview__optional">({text.optional})</span></label>
<textarea id={`${prefix}-message`} name="message" rows="4" disabled={!enabled}></textarea>
<p class="form-a-preview__caution" role="note">{text.caution}</p>
</div>}
</div>
<button class="form-a-preview__submit" type="button" data-preview-submit disabled={!enabled}>{buttonLabel}</button>
{variant === 'compact' ? (
<p class="form-a-preview__status" data-preview-status data-form-status-notice role="status" aria-live="polite">
{text.previewOnly} <a href="tel:+14806877099">{text.callAia}</a>. <a href="/privacy-policy">{text.privacyPolicy}</a>.
</p>
) : (
<p class="form-a-preview__status" data-preview-status role="status" aria-live="polite">{enabled ? text.previewNote : text.noScript}</p>
)}
{variant === 'full' && <noscript><p class="form-a-preview__noscript">{text.noScript}</p></noscript>}
</div>
{enabled && <script>
const preview = document.querySelector<HTMLElement>('[data-form-a-preview][data-form-a-preview-enabled="true"]');
if (preview) {
const preferred = preview.querySelector<HTMLSelectElement>('select[name="preferredContact"]');
const email = preview.querySelector<HTMLInputElement>('input[name="email"]');
const requirement = preview.querySelector<HTMLElement>('[id$="-email-requirement"]');
const button = preview.querySelector<HTMLButtonElement>('[data-preview-submit]');
const status = preview.querySelector<HTMLElement>('[data-preview-status]');
const initialStatus = status?.innerHTML ?? '';
const updateEmailRequirement = () => {
const required = preferred?.value === 'email';
if (email) {
email.required = required;
email.setAttribute('aria-required', String(required));
}
if (requirement) {
const label = required ? preview.dataset.emailRequired : preview.dataset.emailOptional;
requirement.textContent = `(${label ?? ''})`;
}
};
preferred?.addEventListener('change', updateEmailRequirement);
updateEmailRequirement();
button?.addEventListener('click', () => {
const name = preview.querySelector<HTMLInputElement>('input[name="name"]');
const phone = preview.querySelector<HTMLInputElement>('input[name="phone"]');
const fields = [name, phone, preferred].filter(Boolean) as Array<HTMLInputElement | HTMLSelectElement>;
fields.forEach((field) => field.removeAttribute('aria-invalid'));
email?.removeAttribute('aria-invalid');
const missing = fields.filter((field) => !field.value.trim());
const invalidEmail = preferred?.value === 'email' && (!email?.value.trim() || !email.validity.valid);
if (missing.length || invalidEmail) {
missing.forEach((field) => field.setAttribute('aria-invalid', 'true'));
if (invalidEmail) email?.setAttribute('aria-invalid', 'true');
if (status) status.textContent = invalidEmail ? (preview.dataset.emailError ?? '') : (preview.dataset.requiredError ?? '');
(missing[0] ?? email)?.focus();
return;
}
if (status) {
if (preview.dataset.formAVariant === 'compact') status.innerHTML = initialStatus;
else status.textContent = preview.dataset.success ?? 'Demo only: no information was sent or saved.';
}
});
}
</script>}
<style>
.form-a-preview {
color: var(--color-primary, #254080);
}
.form-a-preview--full {
background: white;
border-radius: 4px;
box-shadow: 0 4px 4px rgb(81 81 81 / 4%), 0 4px 16px rgb(81 81 81 / 8%);
margin: 0 auto;
max-width: 600px;
padding: 1.5rem;
}
.form-a-preview__eyebrow {
color: var(--color-muted, #65718a);
font-size: .72rem;
font-weight: 700;
letter-spacing: .08em;
margin: 0 0 .35rem;
text-transform: uppercase;
}
.form-a-preview h3 {
margin: 0 0 .4rem;
}
.form-a-preview__note,
.form-a-preview__caution,
.form-a-preview__status,
.form-a-preview__noscript {
color: var(--color-muted, #65718a);
font-size: .85rem;
line-height: 1.65;
}
.form-a-preview__fields {
display: grid;
gap: 1rem;
grid-template-columns: repeat(2, minmax(0, 1fr));
margin-top: 1.25rem;
}
.form-a-preview__field {
display: grid;
gap: .4rem;
min-width: 0;
}
.form-a-preview__field--full {
grid-column: 1 / -1;
}
.form-a-preview label {
font-weight: 700;
}
.form-a-preview input,
.form-a-preview select,
.form-a-preview textarea {
background: var(--color-background, #fcf9f5);
border: 1px solid var(--color-border, #edeef3);
border-radius: 4px;
color: var(--color-primary, #254080);
font: inherit;
min-height: 2.9rem;
padding: .7rem .8rem;
width: 100%;
}
.form-a-preview textarea {
resize: vertical;
}
.form-a-preview input:focus-visible,
.form-a-preview select:focus-visible,
.form-a-preview textarea:focus-visible,
.form-a-preview__submit:focus-visible {
outline: 3px solid var(--color-focus, #115bbd);
outline-offset: 3px;
}
.form-a-preview [aria-invalid="true"] {
border-color: #a21c28;
outline: 2px solid #a21c28;
outline-offset: 1px;
}
.form-a-preview__optional {
color: var(--color-muted, #65718a);
font-size: .82em;
font-weight: 400;
}
.form-a-preview__caution {
color: var(--color-muted, #254080);
font-size: .85rem;
line-height: 1.65;
margin: .8rem 0 0;
}
.form-a-preview__submit {
background: var(--color-accent, #ffbd59);
border: 2px solid var(--color-accent, #ffbd59);
border-radius: 4px;
color: var(--color-primary, #254080);
cursor: pointer;
font: inherit;
font-weight: 700;
min-height: 3rem;
padding: .65rem 1.2rem;
transition: .3s linear all;
width: 100%;
}
.form-a-preview__submit:hover,
.form-a-preview__submit:focus-visible {
background: transparent;
border-color: var(--color-accent-strong, #ff914d);
color: var(--color-primary, #254080);
}
.form-a-preview__status {
margin: .75rem 0 0;
}
.form-a-preview__status a {
color: inherit;
text-decoration: underline;
text-underline-offset: .15em;
}
.form-a-preview__noscript {
margin: .75rem 0 0;
}
.form-a-preview--compact {
background: transparent;
padding: 0;
}
.form-a-preview--compact .form-a-preview__fields {
grid-template-columns: 1fr;
}
@media (max-width: 640px) {
.form-a-preview--full .form-a-preview__fields {
grid-template-columns: 1fr;
}
.form-a-preview--full .form-a-preview__field--full {
grid-column: auto;
}
}
</style>
@@ -1,5 +1,7 @@
---
import Icon from './Icon.astro';
import FormAPreview from '../FormAPreview.astro';
import { formAPreviewEnabled, formModeForRuntime, resolveFormMode } from '../../utils/form-integration';
interface Props {
sourcePage: 'general' | 'commercial' | 'ahcccs' | 'private_pay';
title: string;
@@ -9,6 +11,8 @@ interface Props {
const { sourcePage, title, intro, buttonText } = Astro.props;
const phone = import.meta.env.PUBLIC_AIA_PHONE || '480-687-7099';
const tel = phone.replace(/[^0-9+]/g, '');
const formConfig = resolveFormMode(import.meta.env.PUBLIC_AIA_FORMS_MODE, import.meta.env.PUBLIC_AIA_FORMS_LIVE_APPROVED);
const formPreview = formAPreviewEnabled(import.meta.env.PUBLIC_AIA_FORM_A_PREVIEW, import.meta.env.PUBLIC_ALLOW_INDEXING, formModeForRuntime(formConfig));
---
<section class="section consultation" id="consultation" aria-labelledby="consultation-heading">
<div class="container consultation__panel grid-2">
@@ -18,9 +22,9 @@ const tel = phone.replace(/[^0-9+]/g, '');
<p class="lede">{intro}</p>
</div>
<div class="form-wrap">
<!-- TODO: Replace this inactive presentation with the approved Jotform integration under P9-03. -->
<!-- TODO: Verify privacy, spam protection and notifications before production activation. -->
<div id="lead-form" role="group" aria-labelledby="campaign-form-heading" data-forms-mode="off">
{formPreview ? <FormAPreview lang="en" variant="compact" submitText={buttonText} /> : <div id="lead-form" role="group" aria-labelledby="campaign-form-heading" data-forms-mode="off">
<!-- TODO: Replace this inactive presentation with the approved Jotform integration under P9-03. -->
<!-- TODO: Verify privacy, spam protection and notifications before production activation. -->
<h3 id="campaign-form-heading">Start with your contact details</h3>
<p class="form-note form-intro">Use this form to request a conversation. An AIA team member can explain the next steps for your family.</p>
<input disabled type="hidden" name="sourcePage" value={sourcePage} />
@@ -53,7 +57,7 @@ const tel = phone.replace(/[^0-9+]/g, '');
<div id="form-error" class="form-error" role="alert" tabindex="-1"></div>
<button class="button" type="button" disabled>{buttonText}<Icon name="arrow" /></button>
<p class="form-note">Questions? <a href={`tel:${tel}`}>Call AIA at {phone}</a>. Review AIA's <a href="/privacy-policy">privacy policy</a>.</p>
</div>
</div>}
</div>
<div class="consultation__details">
<div class="consultation__contact">
+5 -1
View File
@@ -1,6 +1,8 @@
---
import ContactForm from '../ContactForm.astro';
import FormAPreview from '../FormAPreview.astro';
import { formMessages } from '../../utils/form-messages';
import { formAPreviewEnabled, formModeForRuntime, resolveFormMode } from '../../utils/form-integration';
interface Props {
title: string;
@@ -9,6 +11,8 @@ interface Props {
const { title, lang } = Astro.props;
const text = formMessages(lang);
const formConfig = resolveFormMode(import.meta.env.PUBLIC_AIA_FORMS_MODE, import.meta.env.PUBLIC_AIA_FORMS_LIVE_APPROVED);
const formPreview = formAPreviewEnabled(import.meta.env.PUBLIC_AIA_FORM_A_PREVIEW, import.meta.env.PUBLIC_ALLOW_INDEXING, formModeForRuntime(formConfig));
---
<section class="contact-page">
@@ -16,7 +20,7 @@ const text = formMessages(lang);
<div class="contact-page__copy">
<slot />
</div>
<ContactForm title={title} lang={lang} />
{formPreview ? <FormAPreview lang={lang as 'en' | 'es'} variant="full" submitText={lang === 'es' ? 'Enviar' : 'Submit'} /> : <ContactForm title={title} lang={lang} />}
</div>
</section>
+15
View File
@@ -21,6 +21,21 @@ export function formModeForRuntime(config: FormModeConfig): 'off' | 'mock' {
return config.mode === 'mock' ? 'mock' : 'off';
}
/**
* The Form A meeting preview is deliberately narrower than the general mock
* mode: it is available only for an explicitly requested, non-indexing mock
* build. This keeps the review UI out of default and production-shaped output.
*/
export function formAPreviewEnabled(
rawPreview: string | undefined,
allowIndexing: string | undefined,
runtimeMode: 'off' | 'mock'
): boolean {
return rawPreview?.trim().toLowerCase() === 'true'
&& allowIndexing?.trim().toLowerCase() !== 'true'
&& runtimeMode === 'mock';
}
export function resolveMockFormResult(rawResult: string | undefined): MockFormResult {
const result = rawResult?.trim().toLowerCase();
return result === 'failed' || result === 'unavailable' ? result : 'success';
+28 -2
View File
@@ -10,7 +10,20 @@ const messages = {
mock: 'Demo only: this local fixture never sends information.', mockSuccess: 'Demo only: the local fixture completed without sending anything.', mockFailed: 'Demo only: the local fixture simulated a failed submission.', mockUnavailable: 'Demo only: the local fixture simulated an unavailable service.',
map: 'Map showing Arizona Institute for Autism in Scottsdale',
tile: 'OpenStreetMap tile showing area around Arizona Institute for Autism in Scottsdale, AZ',
zoomIn: 'Zoom in', zoomOut: 'Zoom out', marker: 'Arizona Institute for Autism'
zoomIn: 'Zoom in', zoomOut: 'Zoom out', marker: 'Arizona Institute for Autism',
formAPreview: {
internalPreview: 'Internal meeting preview', heading: 'General enquiries preview',
name: 'Your name', phone: 'Phone number', email: 'Email address',
preferredContact: 'Preferred contact method', phoneOption: 'Phone', emailOption: 'Email',
message: 'How can we help?', caution: 'Please avoid entering detailed medical, diagnostic, or insurance-member information here. An AIA team member can gather any needed details through the appropriate follow-up process.',
submit: 'Submit', optional: 'optional', emailRequired: 'required for email contact',
choose: 'Choose one', previewNote: 'Internal preview only: nothing is sent, saved or emailed.',
previewOnly: 'Preview only — nothing is submitted.', callAia: 'Call AIA at (480) 687-7099', privacyPolicy: 'Privacy policy',
noScript: 'This local preview needs JavaScript for its demonstration controls. No information is sent.',
requiredError: 'Please complete your name, phone number and preferred contact method.',
emailError: 'Please enter an email address when Email is your preferred contact method.',
success: 'Demo only: Form A preview completed locally. No information was sent or saved.'
}
},
es: {
heading: 'Programe su consulta gratuita', eyebrow: 'Comience aquí',
@@ -22,7 +35,20 @@ const messages = {
mock: 'Solo demostración: esta prueba local nunca envía información.', mockSuccess: 'Solo demostración: la prueba local terminó sin enviar información.', mockFailed: 'Solo demostración: la prueba local simuló un envío fallido.', mockUnavailable: 'Solo demostración: la prueba local simuló un servicio no disponible.',
map: 'Mapa de la ubicación de Arizona Institute for Autism en Scottsdale',
tile: 'Mapa de OpenStreetMap de los alrededores de Arizona Institute for Autism en Scottsdale, Arizona',
zoomIn: 'Acercar', zoomOut: 'Alejar', marker: 'Arizona Institute for Autism'
zoomIn: 'Acercar', zoomOut: 'Alejar', marker: 'Arizona Institute for Autism',
formAPreview: {
internalPreview: 'Vista previa para reunión interna', heading: 'Vista previa de consultas generales',
name: 'Su nombre', phone: 'Número de teléfono', email: 'Correo electrónico',
preferredContact: 'Método de contacto preferido', phoneOption: 'Teléfono', emailOption: 'Correo electrónico',
message: '¿Cómo podemos ayudarle?', caution: 'Evite ingresar aquí información médica o diagnóstica detallada, o información del miembro del seguro. Un miembro del equipo de AIA puede recopilar los detalles necesarios mediante el proceso de seguimiento adecuado.',
submit: 'Enviar', optional: 'opcional', emailRequired: 'obligatorio para contacto por correo',
choose: 'Elija una opción', previewNote: 'Solo para revisión interna: no se envía, guarda ni comparte información.',
previewOnly: 'Solo vista previa: no se envía nada.', callAia: 'Llame a AIA al (480) 687-7099', privacyPolicy: 'Política de privacidad',
noScript: 'Esta vista previa local necesita JavaScript para sus controles de demostración. No se envía información.',
requiredError: 'Complete su nombre, número de teléfono y método de contacto preferido.',
emailError: 'Ingrese un correo electrónico cuando el correo sea su método de contacto preferido.',
success: 'Solo demostración: la vista previa del Formulario A terminó localmente. No se envió ni guardó información.'
}
}
};