PageSourceSearch

https://lcrnyc.org/assets/js/landing-pages.js

js lcrnyc.org collected 2026-09-25 09:17:19 UTC 18,536 bytes, 477 lines download raw bytes

1let survey = null;
2let formUuid = null;
3
4async function saveForm(sender) {
5    const url = `${window.NUCLEUS_API}/websites/${window.SITE_ID}/pages/${window.PAGE_ID}/form/${formUuid}`;
6          
7    try {
8        const response = await window.axios.post(url, survey.data, {
9            headers: { 'Content-Type': 'application/json' }
10        });
11
12        return { success: true };
13    } catch (error) {
14        if (error?.response?.data?.failed_recaptchas) {
15            return { success: false, failed_recaptchas: error.response.data.failed_recaptchas };
16        }
17        
18        if (error?.response?.data?.message) {
19            return { success: false, message: error.response.data.message };
20        }
21              
22        return { success: false };
23    }
24}
25
26async function getForm() {
27    const url = `${window.NUCLEUS_API}/websites/${window.SITE_ID}/pages/${window.PAGE_ID}/form`;
28
29    try {
30        const response = await window.axios.get(url);
31
32        formUuid = response.data.uuid;
33
34        let successMsg = response.data.success_message;
35        const submitButtonText = response.data.submit_button_text;
36        const pageNextText = response.data.next_button_text;
37        const pagePrevText = response.data.back_button_text;
38        const redirectUrl = response.data.redirect_url;
39
40        if (redirectUrl) {
41            successMsg = '<b style="font-size:30px;">' + successMsg + '</b><br><br><b style="font-size:24px;">You will be redirected shortly</b>';
42        }
43
44        const formJson = {
45            completeText: submitButtonText,
46            pagePrevText: pagePrevText,
47            pageNextText: pageNextText,
48            pages: response.data.pages,
49        };
50
51        let stateFieldName = '';
52
53        response.data.pages.forEach(page => {
54            const field = page.elements.find(field =>
55              field.title && field.title.trim().toLowerCase() === 'state'
56            );
57
58            if (field) {
59                stateFieldName = field.name;
60            }
61        });
62
63        // set widths for each input
64        formJson.pages.forEach((page) => {
65            const elements = page.elements;
66            elements.forEach((element) => {
67                element.startWithNewLine = false;
68                const width = element?.widthColumns;
69                if (width !== 12) {
70                    if (width === 9) {
71                        element.width = '75%';
72                    } else if (width === 6) {
73                        element.width = '50%';
74                    } else if (width === 3) {
75                        element.width = '25%';
76                    } else {
77                        element.width = '100%';
78                    }
79                } else {
80                    element.width = '100%';
81                }
82
83                if (stateFieldName && (element.type === 'county-dropdown' || element.title === 'County Dropdown')) {
84                    element.choicesByUrl = {
85                        "url": window.NUCLEUS_API + "/autocomplete/counties?states={" + stateFieldName + "}",
86                        "path": "data",
87                        "valueName": "id",
88                        "titleName": "county_name"
89                    };
90                }
91            });
92        });
93
94	survey = new Survey.Model(formJson);
95
96        survey.completedHtml = successMsg;
97
98        survey.onCurrentPageChanged.add(function (sender, options) {
99            const currentPage = sender.currentPage;
100
101            for (const question of currentPage.questions) {
102                if (question.jsonObj.isRecaptcha) {
103                    const token = sender.getValue(question.name + '-recaptchaToken');
104
105                    if (token) {
106                        question.html = '<div style="color:#2e7d32;background-color:#e8f5e9;padding:8px 12px;border-radius:4px;margin-top:8px;font-size:0.9em;border:1px solid #2e7d32;">Recaptcha verified</div>';
107                    }     
108                }
109            }
110        });
111
112        survey.onAfterRenderQuestion.add(function (sender, options) {
113            const question = sender.getQuestionByName(options.question.name);
114            const isRecaptcha = question.jsonObj.isRecaptcha;
115                  
116            if (isRecaptcha) {
117                const verified = /recaptcha verified/i.test(question.html);
118
119                if (! verified) {
120                    const waitForGrecaptcha = () => {
121                        if (typeof grecaptcha !== 'undefined' && grecaptcha.render) {
122                            grecaptcha.render(question.name + '-recaptcha', {
123                                sitekey: window.RECAPTCHA_TENANTS_SITE_KEY,
124                                callback: function (token) {
125                                    sender.setValue(question.name + '-recaptchaToken', token);
126                                    options.question.value = token;
127                                }
128                            });
129
130                            clearTimeout(waitForGrecaptcha);
131                        } else {
132                            setTimeout(waitForGrecaptcha, 100);
133                        }
134                    };
135
136                    waitForGrecaptcha();
137                }
138            }
139
140            const isDisclaimer = question.jsonObj.isDisclaimer;
141
142            if (isDisclaimer) {
143                const pTags = options.htmlElement.querySelectorAll('p');
144                
145                for (const p of pTags) {
146                    p.classList.add("!tw-text-[11px]", "!tw-text-gray-400", "!tw-leading-tight");
147
148                    const aTags = p.querySelectorAll('a');
149                    
150                    for (const a of aTags) {
151                        a.classList.add("!tw-text-["+window.THEME_COLORS_PRIMARY+"]");
152                    }
153                }
154            }
155        });
156
157        // set classes & theme colors
158        function hexToRGBA(hex, opacity = 1) {
159            let h = hex.slice(hex.startsWith("#") ? 1 : 0);
160            if (h.length === 3) {
161                h = [...h].map(x => x + x).join("");
162            }
163            const rgb = parseInt(h, 16);
164            const r = (rgb >>> 16) & 255;
165            const g = (rgb >>> 8) & 255;
166            const b = rgb & 255;
167            return `rgba(${r}, ${g}, ${b}, ${opacity})`;
168        };
169                
170        survey.applyTheme({
171            "cssVariables": {
172                "--primary": window.THEME_COLORS_PRIMARY,
173                "--sjs-primary-backcolor": window.THEME_COLORS_PRIMARY,
174                "--sjs-primary-backcolor-light": hexToRGBA(window.THEME_COLORS_PRIMARY,.1)
175            },
176            "isPanelless": true
177        });
178                
179        survey.onUpdateQuestionCssClasses.add(function(sender, options) {
180            const classes = options.cssClasses;
181            const type = options.question.getType();                    
182            if (type === "text" || type === "email" || type === "number" || type === "comment") {
183                classes.root = "tw-placeholder:text-gray-400 tw-block tw-w-full tw-rounded-md tw-border-none tw-bg-white tw-px-3.5 tw-py-2 tw-text-base tw-text-gray-900 tw-outline tw-outline-1 tw--outline-offset-1 tw-outline-gray-300 focus:tw-outline focus:tw-outline-2 focus:tw--outline-offset-2 focus:tw-outline-[var(--color-primary)]";
184            }
185            if (type === "dropdown") {
186                classes.control += " tw-pl-3.5 tw-outline tw-outline-2 tw--outline-offset-1 tw-outline-transparent focus-within:tw--outline-offset-2 focus-within:tw-outline-[var(--color-primary)] tw-h-[40px] !tw-shadow-none tw-border tw-border-solid tw-border-gray-300 tw-rounded-md tw-bg-white";
187                classes.controlValue += " !tw-m-0 tw-flex tw-items-center";
188            }
189            if (type === "file") {
190                classes.fileDecorator += " tw-rounded-lg";
191            }
192            if (type === "signaturepad") {
193                classes.root += " tw-rounded-lg";
194            }
195            if (type === "checkbox" || type === "radiogroup") {
196                classes.materialDecorator += " tw-bg-white tw-shadow-none tw-border tw-border-solid tw-border-gray-300";
197                classes.item += " tw-px-0.5 tw-py-1.5";
198            }
199            if (type === "boolean") {
200                classes.checkboxMaterialDecorator += " tw-bg-white tw-shadow-none tw-border tw-border-solid tw-border-gray-300";
201                classes.content += " tw-px-0.5 tw-py-1.5";
202            }
203
204            if (type === 'html' && options.question.jsonObj.isDisclaimer) {
205                classes.root += ' sm:tw--mt-8 tw--mt-4';
206            }
207
208            classes.mainRoot = "tw-w-full tw-p-0 tw-bg-transparent tw-shadow-none tw-border-none tw-outline-none";
209            classes.header = "tw-p-0 tw-m-0 tw-mb-2.5";
210            classes.title = "tw-block !tw-text-sm/6 !tw-font-semibold !tw-text-gray-900";
211        });
212
213        survey.render(document.getElementById('nucleus-form-container'));
214
215        let hasCompleted = false;
216        survey.onCompleting.add(function(sender, options) {
217            if (hasCompleted) {
218                return;
219            }
220
221            options.allowComplete = false;
222
223            // Clear any previous global form error message.
224            const previousError = document.getElementById('nucleus-form-error');
225
226            if (previousError && previousError.parentNode) {
227                previousError.parentNode.removeChild(previousError);
228            }
229
230            saveForm(sender).then((result) => {
231                if (result.success) {
232                    hasCompleted = true;
233                    options.allowComplete = true;
234                    sender.doComplete();
235                } else if (result.failed_recaptchas) {
236                    options.allowComplete = false;
237                              
238                    for (const recaptcha of result.failed_recaptchas) {
239                        const question = sender.getQuestionByName(recaptcha);
240                                  
241                        if (question) {
242                            question.clearErrors();
243                                      
244                            question.addError('Invalid recaptcha provided');
245
246                            const page = sender.pages.findIndex(p =>
247                                p.elements.some(q => q.name === recaptcha)
248                            );
249                                  
250                            if (page !== -1) {
251                                sender.currentPageNo = page;
252                            }
253                        }
254                    }
255                } else if (result.message) {
256                    // Error handling.
257
258                    // I've put 'allow' here and not 'allowComplete' because
259                    // 'allowComplete' is deprecated, according to survey-code docs.
260                    options.allow = false;
261
262                    const container = document.getElementById('nucleus-form-container');
263
264                    if (container) {
265                        const div = document.createElement('div');
266
267                        div.id = 'nucleus-form-error';
268                        div.className = 'tw-text-red-600 tw-mt-3 tw-mb-2 tw-text-sm tw-w-full tw-basis-full tw-block';
269                        div.textContent = result.message;
270
271                        // Always place the error directly below the Submit button.
272                        // Use a strict selector so we don't match other UI buttons (e.g., dropdown toggles).
273                        const submitButton = container.querySelector('button[data-name="complete"], button.sd-navigation__complete-btn');
274
275                        if (submitButton) {
276                            // Prefer placing the error after the Submit button's navigation container
277                            // to ensure it renders on its own line below the button in flex layouts.
278                            const navContainer = submitButton.closest('.sd-action-bar, .sd-navigation, .sd-footer, .sd-footer__navigation');
279                            const pointOfReference = navContainer ?? submitButton;
280                            pointOfReference.insertAdjacentElement('afterend', div);
281                        } else {
282                            // Fallback: keep it inside the form container at the bottom.
283                            container.appendChild(div);
284                        }
285                    }
286                }
287            });
288        });
289              
290        survey.onCompleting.add(function(sender, options) {
291            sender.getAllQuestions().forEach((q) => {
292                if (q.jsonObj.isRecaptcha) {
293                    const token = sender.getValue(q.name + '-recaptchaToken');
294
295                    if (! token) {
296                        q.clearErrors();
297                        options.allowComplete = false;
298                        q.addError('Invalid recaptcha provided');
299                    }
300                }
301            });     
302        });
303
304        survey.onValidateQuestion.add(function(sender, options) {
305            const question = sender.getQuestionByName(options.name);
306
307            if (question) {
308                if (question.isJson && options.value !== undefined) {
309                    try {
310                        JSON.parse(options.value);
311                    } catch (e) {
312                        options.error = 'Please enter valid JSON';
313                    }
314                }
315                      
316                if (question.getType() === "boolean" && ! question.value) {
317                    options.error = 'You must accept the terms and conditions';
318                }
319            }
320        });
321
322        survey.onValueChanged.add(function(sender, options) {
323            const question = sender.getQuestionByName(options.name);
324                
325            if (question && question.jsonObj.isRecaptcha) {
326                question.clearErrors();
327            }
328
329            const match = options.name.match(/^(.*)-(date|time)$/);
330
331            if (! match) {
332                return;
333            }
334
335            const field = match[1];
336            const type = match[2];
337
338            const date = sender.getValue(field + '-date');
339            const time = sender.getValue(field + '-time');
340
341            let datetime = null;
342
343            if (date) {
344                if (time) {
345                    datetime = `${date} ${time}:00`;
346                } else {
347                    datetime = `${date}`;
348                }
349
350                sender.setValue(field, datetime);
351            }
352        });
353
354        survey.onComplete.add(function (sender, options) {
355            sender.getAllQuestions().forEach((q) => {
356                if (q.inputType === 'date' || q.inputType === 'time') {
357                    delete sender.data[q.name];
358                }
359            });
360
361            if (redirectUrl) {
362                setTimeout(() => {
363                    window.location.href = redirectUrl;
364                }, 2000);
365            }
366        });
367
368        survey.onServerValidateQuestions.add(async function (sender, options) {
369            for (const page of sender.pages) {
370                for (const element of page.elements) {
371                    if (element.isDateTime) {
372                        const date = sender.getValue(element.name + '-date');
373                        const time = sender.getValue(element.name + '-time');
374                              
375                        if (date) {
376                            let datetime = null;
377
378                            if (time) {
379                                datetime = `${date} ${time}:00`;
380                            } else {
381                                datetime = `${date}`;
382                            }
383
384                            const result = await checkUnique([datetime, element.handle, element.name, true]);
385                            if (! result) {
386                                const question = sender.getQuestionByName(element.name + '-date');
387
388                                if (question) {
389                                    question.clearErrors();
390
391                                    if (time) {
392                                        options.errors[element.name + '-date'] = 'The date and time is already taken';
393                                    } else {
394                                        options.errors[element.name + '-date'] = 'The date is already taken';
395                                    }
396                                          
397                                    const page = sender.pages.findIndex(p => p.elements.some(q => q.name === element.name + '-date'));
398
399                                    if (page !== -1) {
400                                        setTimeout(() => {
401                                            sender.currentPageNo = page;
402                                        }, 0);
403                                    }
404                                }
405                            }
406                        }
407                    }
408                }
409            }
410                  
411            options.complete();
412        });
413    } catch (error) {
414	console.error(error);
415    }	
416}
417
418async function checkUnique([value, handle, hash, isDateTime = false]) {
419    if (value === undefined) {
420        return;
421    }
422
423    const url = `${window.NUCLEUS_API}/websites/${window.SITE_ID}/pages/${window.PAGE_ID}/form/${formUuid}/unique`;
424	  
425    try {
426        const response = await window.axios.post(url, { value, handle, hash });
427              
428        if (isDateTime) {
429            return response.data;
430        } else {
431      	    this.returnResult(response.data);
432        }
433    } catch (error) {
434	console.error(error);
435    }
436}
437
438Survey.FunctionFactory.Instance.register('checkUnique', checkUnique, true);
439
440Survey.Serializer.addProperty('comment', {
441    name: 'isJson:boolean',
442    default: false
443});
444
445Survey.Serializer.addProperty('comment', {
446    name: 'isRecaptcha:boolean',
447    default: false
448});
449
450Survey.Serializer.addProperty('text', {
451    name: 'isDateTime:boolean',
452    default: false
453});
454
455Survey.Serializer.addProperty('html', {
456    name: 'isDisclaimer:boolean',
457    default: false
458});
459
460Survey.Serializer.addProperty('text', {
461    name: 'handle:string',
462    default: ''
463});
464
465Survey.Serializer.addProperty('question', {
466    name: 'widthColumns:number',
467    default: 6
468});
469
470Survey.Serializer.addProperty('question', {
471    name: 'labelLocation:string',
472    default: 'floating'
473});
474
475document.addEventListener('DOMContentLoaded', function () {
476    getForm();
477});

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.