PageSourceSearch

https://prompeteer.ai/gateway/contact.js?v=cinematic-2026-09-v3-ui24

js prompeteer.ai collected 2026-10-05 12:26:09 UTC 6,723 bytes, 156 lines download raw bytes

1/**
2 * Contact Sales on the Experience page. The same form, payload and endpoint as the
3 * app's pricing page and plan picker (shared/sales-contact.ts); the server sends the
4 * message to the sales inbox. Nothing here is sent anywhere else.
5 */
6const ENDPOINT = '/api/contact/sales';
7const FIELDS = ['name', 'email', 'company', 'message', 'website'];
8
9export function mountGatewayContact(doc = document, win = window) {
10  const dialog = doc.querySelector('[data-gateway-contact]');
11  const trigger = doc.querySelector('[data-gateway-contact-sales]');
12  const form = dialog?.querySelector('form');
13  const body = dialog?.querySelector('[data-gateway-contact-body]');
14  const done = dialog?.querySelector('[data-gateway-contact-success]');
15  const doneTitle = dialog?.querySelector('[data-gateway-contact-success-title]');
16  const submit = form?.querySelector('button[type="submit"]');
17  const status = dialog?.querySelector('[data-gateway-contact-status]');
18  const title = dialog?.querySelector('#gateway-contact-title');
19  const dismissers = dialog ? [...dialog.querySelectorAll('[data-gateway-contact-close],[data-gateway-contact-done]')] : [];
20  const field = name => form?.querySelector(`[name="${name}"]`);
21  if (!dialog || !trigger || !form || !body || !done || !doneTitle || !submit || !status || !title || !dismissers.length || FIELDS.some(name => !field(name))) return null;
22  const shell = doc.querySelector('[data-gateway]');
23  const submitLabel = submit.textContent;
24  let previousFocus, previousInert = false, fallback = false, pending = false, destroyed = false;
25  let abort, timer;
26  const listeners = [];
27  const listen = (target, type, callback) => {
28    target.addEventListener(type, callback);
29    listeners.push(() => target.removeEventListener(type, callback));
30  };
31  const setState = state => { dialog.dataset.contactState = state; };
32
33  // After a sent message the next visit starts with a clean form.
34  function reset() {
35    if (dialog.dataset.contactState !== 'success') return;
36    form.reset();
37    done.hidden = true;
38    body.hidden = false;
39    status.textContent = '';
40    setState('idle');
41  }
42  function close() {
43    if (dialog.hidden) return;
44    if (!fallback && typeof dialog.close === 'function' && dialog.open) dialog.close();
45    dialog.removeAttribute('open');
46    dialog.hidden = true;
47    if (fallback && shell) shell.inert = previousInert;
48    fallback = false;
49    reset();
50    if (previousFocus?.isConnected) previousFocus.focus();
51    previousFocus = null;
52  }
53  function open() {
54    if (!dialog.hidden || [...doc.querySelectorAll('dialog[open]')].some(other => other !== dialog)) return;
55    previousFocus = doc.activeElement;
56    dialog.hidden = false;
57    try {
58      if (typeof dialog.showModal !== 'function') throw new Error('Native dialog unavailable');
59      dialog.showModal();
60    } catch {
61      fallback = true;
62      dialog.setAttribute('open', '');
63      if (shell) { previousInert = shell.inert === true; shell.inert = true; }
64    }
65    // On a phone, focusing a field would raise the keyboard over the form before it is read.
66    const touch = win.matchMedia?.('(pointer: coarse)')?.matches === true;
67    (touch ? title : field('name')).focus();
68  }
69  // The browser checks required fields, lengths and the email pattern, in its own language.
70  function valid() {
71    for (const name of ['name', 'email', 'company', 'message']) field(name).value = field(name).value.trim();
72    return form.reportValidity();
73  }
74  async function send(event) {
75    event.preventDefault();
76    if (pending || destroyed || body.hidden) return;
77    if (!valid()) return;
78    pending = true;
79    submit.disabled = true;
80    submit.textContent = submit.dataset.sending || submitLabel;
81    form.setAttribute('aria-busy', 'true');
82    status.textContent = '';
83    setState('pending');
84    abort = new win.AbortController();
85    timer = win.setTimeout(() => abort.abort(), 15000);
86    try {
87      const payload = Object.fromEntries(FIELDS.map(name => [name, field(name).value]));
88      const response = await win.fetch(ENDPOINT, {
89        method: 'POST', credentials: 'same-origin', cache: 'no-store',
90        headers: { 'Content-Type': 'application/json' },
91        body: JSON.stringify({ ...payload, source: 'experience', locale: doc.documentElement.lang || 'en' }),
92        signal: abort.signal,
93      });
94      if (!response.ok) {
95        setState(response.status === 429 ? 'rate-limited' : 'error');
96        throw new Error('Contact unavailable');
97      }
98      const result = await response.json();
99      if (result?.success !== true) throw new Error('Invalid contact response');
100      if (destroyed) return;
101      setState('success');
102      form.reset();
103      body.hidden = true;
104      done.hidden = false;
105      doneTitle.focus();
106    } catch {
107      if (destroyed) return;
108      if (dialog.dataset.contactState !== 'rate-limited') setState('error');
109      status.textContent = status.dataset.error || '';
110      // No automatic retry: the server may already have sent the message.
111    } finally {
112      win.clearTimeout(timer);
113      if (!destroyed) {
114        pending = false;
115        submit.disabled = false;
116        submit.textContent = submitLabel;
117        form.removeAttribute('aria-busy');
118      }
119    }
120  }
121
122  setState('idle');
123  trigger.hidden = false;
124  listen(trigger, 'click', open);
125  dismissers.forEach(button => listen(button, 'click', close));
126  listen(dialog, 'cancel', event => { event.preventDefault(); close(); });
127  listen(form, 'submit', event => { void send(event); });
128  listen(dialog, 'keydown', event => {
129    if (event.key === 'Escape' && fallback) { event.preventDefault(); close(); return; }
130    if (event.key !== 'Tab') return;
131    const elements = [...dialog.querySelectorAll('a[href], button, input, textarea, [tabindex]')]
132      .filter(element => !element.disabled && !element.hidden && !element.closest('[hidden],[aria-hidden="true"]') && element.tabIndex >= 0);
133    const first = elements[0], last = elements[elements.length - 1];
134    if (!first) { event.preventDefault(); return; }
135    if (event.shiftKey && (doc.activeElement === first || !dialog.contains(doc.activeElement))) {
136      event.preventDefault(); last.focus();
137    } else if (!event.shiftKey && (doc.activeElement === last || !dialog.contains(doc.activeElement))) {
138      event.preventDefault(); first.focus();
139    }
140  });
141  return {
142    open, close,
143    get pending() { return pending; },
144    destroy() {
145      destroyed = true;
146      abort?.abort();
147      win.clearTimeout(timer);
148      close();
149      listeners.forEach(remove => remove());
150    },
151  };
152}
153
154if (typeof document !== 'undefined' && document.querySelector('[data-gateway-contact]')) {
155  mountGatewayContact();
156}

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.