|
97 | 97 |
|
98 | 98 | <body> |
99 | 99 | <div style="display: flex; flex-direction: column; gap: 10px;"> |
100 | | - <div id="cal-booking-place-routingFormWithoutPrerender"> |
101 | | - <a href="?only=ns:routingFormWithoutPrerender">Routing Form Without Prerender Demo</a> |
102 | | - <form id="cal-booking-place-routingFormWithoutPrerender-form"> |
103 | | - <input type="text" name="name" placeholder="John Doe" /> |
104 | | - <input type=" email" name=" email" placeholder=" [email protected]" /> |
105 | | - <select name="skills" placeholder="JavaScript, Node.js"> |
106 | | - <option value="JavaScript">JavaScript</option> |
107 | | - <option value="Sales">Sales</option> |
108 | | - </select> |
109 | | - </form> |
110 | | - <button id=" cal-booking-place-routingFormWithoutPrerender-submit" data-cal-namespace=" routingFormWithoutPrerender" data-cal-config=' {"cal.embed.pageType":"team.event.booking.slots", "guests":["[email protected]", "[email protected]"]}' >Submit </button> |
111 | | - <script> |
112 | | - requestAnimationFrame(function updateSubmitButtonLink() { |
113 | | - const seededFormAcmeId = "948ae412-d995-4865-885a-48302588de03"; |
114 | | - const form = document.getElementById("cal-booking-place-routingFormWithoutPrerender-form"); |
115 | | - const name = form.querySelector("input[name='name']").value; |
116 | | - const email = form.querySelector("input[name='email']").value; |
117 | | - const skills = form.querySelector("select[name='skills']").value; |
118 | | - if (name && email) { |
119 | | - document.getElementById("cal-booking-place-routingFormWithoutPrerender-submit").setAttribute("data-cal-link", `router?form=${seededFormAcmeId}&email=${email}&name=${name}&Location=London&Department=Engineering&Rating=5&skills=${skills}&Email=${email}`); |
120 | | - } |
121 | | - requestAnimationFrame(updateSubmitButtonLink); |
122 | | - }); |
123 | | - </script> |
124 | | - </div> |
125 | 100 | <div id="cal-booking-routingFormFullPrerender"> |
126 | 101 | <hr/> |
127 | | - <a href=" ?only=ns:routingFormFullPrerender&debug=1&[email protected]" >Routing Form - Prerender Headless Router itself queuing the form response </a> |
128 | | - NOTE: Pass query param param.formId=FORM_UID_HERE to test with a particular routing form |
| 102 | + <a style="display: block;" href=" ?only=ns:routingFormFullPrerender&debug=1&[email protected]" >Routing Form - Prerender Headless Router itself queuing the form response </a> |
| 103 | + NOTE: Pass query param param.formId=FORM_UID_HERE to test with a particular routing form. Easiest is to run seed-insights.ts script to create a seeded form and then use the form id here. |
129 | 104 | <p>1. As page is loading, we prerender the headless router for the email passed as param.email</p> |
130 | 105 | <p>2. Whenever email changes and onblur happens, the prerender is triggered for the new email and previous prerendered modal is removed</p> |
131 | 106 | <p>3. The prerender of the headless router queues the form response and doesn't really record it. When the actual booking is made, the form response is recorded from the queue</p> |
132 | 107 | <p>4. If the CTA click happens before the slots are considered stale(configurable via options.slotsStaleTimeMs), then it will open the prerendered modal without fetching the slots, otherwise it will fetch the slots(for the routedTeamMembers/contactOwner only) and till then skeleton will be shown</p> |
133 | 108 | <p>5. If the CTA click happens after iframeForceReloadThresholdMs has passed, then fresh headless router request is sent which could be really slow. It is important to do force reload after a certain time because the Routing Form itself could have changed in the meantime or Salesforce ownership might be available or some other change might have occured in Cal.com's side</p> |
134 | 109 | <p>6. slotsStaleTimeMs is set to 10 seconds(default is 1 minute) and iframeForceReloadThresholdMs is set to 30 seconds(default is 15 minutes) and they are considered from the time when the prerender/modal call was made</p> |
135 | 110 | <p>7. To avoid reaching the iframeForceReloadThresholdMs, user could prerender the router again and again judiciously</p> |
| 111 | + <p>8. You can disable prerendering by passing the query param param.disablePrerender=true</p> |
136 | 112 | </p> |
137 | 113 | <div class="inline-embed-container"> |
138 | 114 | <script> |
|
144 | 120 | debug: true, |
145 | 121 | calOrigin: window.calOrigin, |
146 | 122 | }); |
| 123 | + |
| 124 | + function trackLog(message) { |
| 125 | + console.log(`${performance.now().toFixed(2)}ms [Tracking] ${message}`); |
| 126 | + } |
| 127 | + |
| 128 | + let listenersAdded = false; |
| 129 | + let isAvailabilityLoaded = false; |
| 130 | + // Track API start |
| 131 | + let apiStartTime = null; |
| 132 | + |
| 133 | + const linkPrerenderedCallback = (e) => { |
| 134 | + trackLog("Link Prerendered"); |
| 135 | + }; |
| 136 | + Cal.ns["routingFormFullPrerender"]("on", { action: "linkPrerendered", callback: linkPrerenderedCallback }); |
| 137 | + |
| 138 | + // Track latency b/w API execution and availability loading |
| 139 | + const trackLatencyInShowingSlots = (namespace) => { |
| 140 | + isAvailabilityLoaded = false; |
| 141 | + apiStartTime = performance.now(); |
| 142 | + const trackAvailabilityLoaded = (source) => { |
| 143 | + if (!apiStartTime) { |
| 144 | + trackLog(`API start time not set - ${source}`); |
| 145 | + return; |
| 146 | + } |
| 147 | + if (isAvailabilityLoaded) { |
| 148 | + trackLog(`Availability already loaded - triggering again by: ${source}`); |
| 149 | + return; |
| 150 | + } |
| 151 | + |
| 152 | + isAvailabilityLoaded = true; |
| 153 | + const endTime = performance.now(); |
| 154 | + const timeDiff = endTime - apiStartTime; |
| 155 | + trackLog(`Embed Modal API to Booker Ready: ${timeDiff.toFixed(2)}ms - triggered by: ${source}`); |
| 156 | + }; |
| 157 | + trackLog(`Embed Modal API started at: ${apiStartTime.toFixed(2)}ms`); |
| 158 | + |
| 159 | + // Setup availability tracking |
| 160 | + isAvailabilityLoaded = false; |
| 161 | + const api = namespace ? Cal.ns[namespace] : Cal; |
| 162 | + |
| 163 | + const bookerViewedCallback = (e) => { |
| 164 | + const data = e.detail.data; |
| 165 | + if (data.slotsLoaded === true) { |
| 166 | + trackAvailabilityLoaded("bookerViewed (slotsLoaded: true)"); |
| 167 | + } else { |
| 168 | + trackLog("bookerViewed(slotsLoaded: false). Booker is ready but waiting for slots to load"); |
| 169 | + } |
| 170 | + }; |
| 171 | + |
| 172 | + const bookerReopenedCallback = (e) => { |
| 173 | + const data = e.detail.data; |
| 174 | + if (data.slotsLoaded === true) { |
| 175 | + trackAvailabilityLoaded("bookerReopened (slotsLoaded: true)"); |
| 176 | + } else { |
| 177 | + trackLog("Booker was reopened and waiting for slots to load"); |
| 178 | + } |
| 179 | + }; |
| 180 | + |
| 181 | + const bookerReadyCallback = () => { |
| 182 | + trackAvailabilityLoaded("bookerReady event"); |
| 183 | + }; |
| 184 | + |
| 185 | + const bookerReloadedCallback = (e) => { |
| 186 | + const data = e.detail.data; |
| 187 | + if (data.slotsLoaded === true) { |
| 188 | + trackAvailabilityLoaded("bookerReloaded (slotsLoaded: true)"); |
| 189 | + } else { |
| 190 | + trackLog("Booker was reloaded and waiting for slots to load"); |
| 191 | + } |
| 192 | + }; |
| 193 | + |
| 194 | + if (!listenersAdded) { |
| 195 | + api("on", { action: "bookerViewed", callback: bookerViewedCallback }); |
| 196 | + api("on", { action: "bookerReady", callback: bookerReadyCallback }); |
| 197 | + api("on", { action: "bookerReopened", callback: bookerReopenedCallback }); |
| 198 | + api("on", { action: "bookerReloaded", callback: bookerReloadedCallback }); |
| 199 | + listenersAdded = true; |
| 200 | + } |
| 201 | + }; |
| 202 | + |
147 | 203 | window.routingFormFullPrerender = { |
148 | 204 | buildRouterUrl: ({skills, location, name, email, formId}) => { |
149 | 205 | // Send `email` as well as `Email` because E2e Tests currently use `Email` and `email` is needed by contact owner lookup. It doesn't accept Uppercase Email |
150 | | - return `router?form=${formId}&skills=${skills}&Location=${location}&name=${name}&Email=${email}&email=${email}&Rating=5`; |
| 206 | + return `router?form=${formId}&Manager=fggfg&skills=${skills}&Location=${location}&name=${name}&Email=${email}&email=${email}&Rating=5`; |
151 | 207 | }, |
152 | 208 | onFormSubmit: (e) => { |
153 | 209 | e.preventDefault(); |
| 210 | + |
| 211 | + if(!window.params.formId) { |
| 212 | + alert("Form ID is not set. Please set the form ID using the param.formId query parameter. Easiest is to run seed-insights.ts script to create a seeded form and then use the form id here. "); |
| 213 | + return false; |
| 214 | + } |
| 215 | + e.preventDefault(); |
| 216 | + |
| 217 | + const namespace = "routingFormFullPrerender"; |
| 218 | + trackLatencyInShowingSlots(namespace); |
| 219 | + |
154 | 220 | const skills = document.getElementById('cal-booking-place-routingFormFullPrerender-select-skills').value; |
155 | 221 | const location = document.getElementById('cal-booking-place-routingFormFullPrerender-select-location').value; |
156 | 222 | const name = document.getElementById('cal-booking-place-routingFormFullPrerender-input-name').value; |
157 | 223 | const email = document.getElementById('cal-booking-place-routingFormFullPrerender-input-email').value; |
158 | 224 | const routerUrl = window.routingFormFullPrerender.buildRouterUrl({skills, location, name, email, formId: window.params.formId}); |
159 | | - Cal.ns.routingFormFullPrerender("modal", { |
| 225 | + |
| 226 | + Cal.ns[namespace]("modal", { |
160 | 227 | calLink: routerUrl, |
161 | 228 | calOrigin: window.calOrigin, |
| 229 | + config: { |
| 230 | + "cal.embed.pageType": "team.event.booking.slots", |
| 231 | + }, |
162 | 232 | }); |
163 | 233 | } |
164 | 234 | } |
165 | 235 | })(); |
166 | 236 | </script> |
167 | 237 | <div id="cal-booking-place-routingFormFullPrerender"> |
168 | 238 | <div class="place"></div> |
169 | | - <form id="cal-booking-place-routingFormFullPrerender-form" onsubmit="window.routingFormFullPrerender.onFormSubmit(event)"> |
170 | | - <label for="cal-booking-place-routingFormFullPrerender-input-name">Name</label> |
171 | | - <input required style="width: 400px" type="text" id="cal-booking-place-routingFormFullPrerender-input-name" placeholder="John Doe" /> |
172 | | - <label for="cal-booking-place-routingFormFullPrerender-input-email">Email</label> |
173 | | - <input required style=" width: 400px" type=" email" id=" cal-booking-place-routingFormFullPrerender-input-email" placeholder=" [email protected]" /> |
174 | | - <label for="cal-booking-place-routingFormFullPrerender-select-skills">Skills</label> |
175 | | - <select required style="width: 400px" id="cal-booking-place-routingFormFullPrerender-select-skills"> |
176 | | - <option value="">Select a skill</option> |
177 | | - <option value="JavaScript">JavaScript</option> |
178 | | - <option value="React">React</option> |
179 | | - <option value="Node.js">Node.js</option> |
180 | | - <option value="Python">Python</option> |
181 | | - <option value="Sales">Sales</option> |
182 | | - </select> |
183 | | - |
184 | | - <label for="cal-booking-place-routingFormFullPrerender-select-location">Location</label> |
185 | | - <select required style="width: 400px" id="cal-booking-place-routingFormFullPrerender-select-location"> |
186 | | - <option value="">Select a location</option> |
187 | | - <option value="New York">New York</option> |
188 | | - <option value="London">London</option> |
189 | | - <option value="Tokyo">Tokyo</option> |
190 | | - <option value="Berlin">Berlin</option> |
191 | | - <option value="Remote">Remote</option> |
192 | | - </select> |
193 | | - <button type="submit" id="cta-routingFormFullPrerender">Submit</button> |
194 | | - </form> |
195 | | - </div> |
| 239 | + <form id="cal-booking-place-routingFormFullPrerender-form" onsubmit="window.routingFormFullPrerender.onFormSubmit(event)" style="display: flex; flex-direction: column; gap: 16px; max-width: 500px; padding: 20px; background: white; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.1);"> |
| 240 | + <div style="display: flex; flex-direction: column; gap: 6px;"> |
| 241 | + <label for="cal-booking-place-routingFormFullPrerender-input-name" style="font-weight: 500; color: #333; font-size: 14px;">Name</label> |
| 242 | + <input required style="width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 4px; font-size: 14px; box-sizing: border-box;" type="text" id="cal-booking-place-routingFormFullPrerender-input-name" placeholder="John Doe" /> |
| 243 | + </div> |
| 244 | + |
| 245 | + <div style="display: flex; flex-direction: column; gap: 6px;"> |
| 246 | + <label for="cal-booking-place-routingFormFullPrerender-input-email" style="font-weight: 500; color: #333; font-size: 14px;">Email</label> |
| 247 | + <input required style=" width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 4px; font-size: 14px; box-sizing: border-box;" type=" email" id=" cal-booking-place-routingFormFullPrerender-input-email" placeholder=" [email protected]" /> |
| 248 | + </div> |
| 249 | + |
| 250 | + <div style="display: flex; flex-direction: column; gap: 6px;"> |
| 251 | + <label for="cal-booking-place-routingFormFullPrerender-select-skills" style="font-weight: 500; color: #333; font-size: 14px;">Skills</label> |
| 252 | + <select required style="width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 4px; font-size: 14px; box-sizing: border-box; background: white;" id="cal-booking-place-routingFormFullPrerender-select-skills"> |
| 253 | + <option value="">Select a skill</option> |
| 254 | + <option value="JavaScript">JavaScript</option> |
| 255 | + <option value="React">React</option> |
| 256 | + <option value="Node.js">Node.js</option> |
| 257 | + <option value="Python">Python</option> |
| 258 | + <option value="Sales">Sales</option> |
| 259 | + </select> |
| 260 | + </div> |
| 261 | + |
| 262 | + <div style="display: flex; flex-direction: column; gap: 6px;"> |
| 263 | + <label for="cal-booking-place-routingFormFullPrerender-select-location" style="font-weight: 500; color: #333; font-size: 14px;">Location</label> |
| 264 | + <select required style="width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 4px; font-size: 14px; box-sizing: border-box; background: white;" id="cal-booking-place-routingFormFullPrerender-select-location"> |
| 265 | + <option value="">Select a location</option> |
| 266 | + <option value="New York">New York</option> |
| 267 | + <option value="London">London</option> |
| 268 | + <option value="Tokyo">Tokyo</option> |
| 269 | + <option value="Berlin">Berlin</option> |
| 270 | + <option value="Remote">Remote</option> |
| 271 | + </select> |
| 272 | + </div> |
| 273 | + |
| 274 | + <button type="submit" id="cta-routingFormFullPrerender" style="padding: 12px 24px; background: #007bff; color: white; border: none; border-radius: 4px; font-size: 16px; font-weight: 500; cursor: pointer; margin-top: 8px; transition: background 0.2s;">Submit</button> |
| 275 | + </form> |
| 276 | + </div> |
196 | 277 |
|
197 | 278 | </div> |
198 | 279 |
|
|
206 | 287 | pageType: "team.event.booking.slots", |
207 | 288 | options: { |
208 | 289 | slotsStaleTimeMs: 10 * 1000, |
209 | | - iframeForceReloadThresholdMs: 30 * 1000, |
| 290 | + iframeForceReloadThresholdMs: 100 * 1000, |
210 | 291 | }, |
211 | 292 | }); |
212 | 293 | } |
|
223 | 304 | return; |
224 | 305 | } |
225 | 306 |
|
| 307 | + if (window.params.disablePrerender) { |
| 308 | + return; |
| 309 | + } |
| 310 | + |
226 | 311 | prerender({skills, location, email, formId: window.params.formId}); |
227 | 312 | } |
228 | 313 | document.getElementById('cal-booking-place-routingFormFullPrerender-select-skills').onchange = onSelectChange; |
|
0 commit comments