Update /my_account/verify_email/index.html.jst to latest way, no accordion
[ndcode_site.git] / my_account / sign_up / index.html.jst
1 return async env => {
2   let breadcrumbs = await _require('/_lib/breadcrumbs.jst')
3   let fa_arrow_circle_left = await env.site.get_min_svg('/_svg/fa_arrow-circle-left.svg')
4   let fa_envelope = await env.site.get_min_svg('/_svg/fa_envelope.svg')
5   let fa_redo = await env.site.get_min_svg('/_svg/fa_redo.svg')
6   let fa_user_plus = await env.site.get_min_svg('/_svg/fa_user-plus.svg')
7   let get_placeholder = await _require('/_lib/get_placeholder.jst')
8   let get_session = await _require('/_lib/get_session.jst')
9   let icon_cross = await env.site.get_min_svg('/_svg/icon_cross.svg')
10   let icon_tick = await env.site.get_min_svg('/_svg/icon_tick.svg')
11   let navbar = await _require('/_lib/navbar.jst')
12
13   // preload draft details if any
14   let transaction = await env.site.database.Transaction()
15   let placeholder
16   let sign_up_draft
17   try {
18     let root = await transaction.get({})
19     let session = await get_session(env, root)
20
21     placeholder = await get_placeholder(env, session)
22
23     sign_up_draft = await session.get_json('sign_up_draft')
24     if (sign_up_draft === undefined || env.now >= sign_up_draft.expires)
25       sign_up_draft = null
26     transaction.commit()
27   }
28   catch (error) {
29     transaction.rollback()
30     throw error
31   }
32
33   await navbar(
34     env,
35     // head
36     async _out => {},
37     // body
38     async _out => {
39       await breadcrumbs(env, _out)
40
41       p {'Signing up allows you to leave comments on our blog and receive communications from us.'}
42
43       p {'Your given names are visible to other users if you comment on our blog. Your email and family name remain private. If your name is one word or does not fit given names/family name pattern, then please enter given names only.'}
44
45       div.accordion#accordion(role="tablist" aria-multiselectable="true") {
46         div.card#card-1 {
47           div.card-header#card-1-heading(role="tab") {
48             div.icon32-outer.mr-2#card-1-tick(hidden) {
49               div.icon32-inner {_out.push(icon_tick)}
50             }
51             div.icon32-outer.mr-2#card-1-cross(hidden) {
52               div.icon32-inner {_out.push(icon_cross)}
53             }
54             div.icon32-outer.mr-2#card-1-spinner(hidden) {
55               div.icon32-inner {
56                 div.spinner-border(role="status") {}
57               }
58             }
59             a.h5(data-toggle="collapse" data-parent="#accordion" href="#card-1-collapse" aria-expanded="true" aria-controls="card-1-collapse") {
60               'Create account'
61             }
62           }
63           div#card-1-collapse.collapse.show(role="tabpanel" aria-labelledby="card-1-heading" data-parent="#accordion") {
64             div.card-body {
65               form#form {
66                 div.row {
67                   div.col-md-6 {
68                     div.form-group {
69                       label.form-label(for="given-names") {'Given names *'}
70                       input.form-control#given-names(type="text" value=sign_up_draft !== null ? sign_up_draft.given_names : '' placeholder=placeholder.given_names required maxlength=256) {}
71                       div.invalid-feedback {'Please enter a name we can address you by.'}
72                     }
73                   }
74                   div.col-md-6 {
75                     div.form-group {
76                       label.form-label(for="family-name") {'Family name'}
77                       input.form-control#family-name(type="text" value=sign_up_draft !== null ? sign_up_draft.family_name : '' placeholder=placeholder.family_name maxlength=256) {}
78                     }
79                   }
80                 }
81                 div.row {
82                   div.col-md-6 {
83                     div.form-group {
84                       label.form-label(for="email") {'Email *'}
85                       input.form-control#email(type="email" value=sign_up_draft !== null ? sign_up_draft.email : '' placeholder=placeholder.email required maxlength=256) {}
86                       div.invalid-feedback {'Please enter an email address we can contact you on.'}
87                     }
88                   }
89                   div.col-md-6 {
90                     div.form-group {
91                       label.form-label(for="password") {'Password *'}
92                       input.form-control#password(type="password" placeholder="Choose" required minlength=8 maxlength=256) {}
93                       div.invalid-feedback {'Please choose a secure password of at least 8 characters.'}
94                     }
95                   }
96                 }
97                 div.row/*.align-items-center*/ {
98                   div.col-md-6 {
99                     div.custom-control.custom-checkbox {
100                       if (sign_up_draft === null || sign_up_draft.contact_me)
101                         input.custom-control-input#contact-me(type="checkbox" checked) {}
102                       else
103                         input.custom-control-input#contact-me(type="checkbox") {}
104                       ' '
105                       label.custom-control-label(for="contact-me") {
106                         'Contact me by email with updates and special offers'
107                       }
108                     }
109                   }
110                   div.col-md-6 {
111                     div.form-group {
112                       label.form-label(for="verification-code") {'Verification code *'}
113                       div.mb-3 {
114                         img#verification-image(src="/api/verification_image.png?seq=0" width="300px" height="150px") {}
115                       }
116                       div.input-group#valid-workaround(style="width: 300px;") {
117                         input.form-control#verification-code(type="text" placeholder='xxxxxx' required minlength=6 maxlength=6) {}
118                         div.input-group-append {
119                           button.btn.btn-outline-secondary#'card-1-new'(type="button") {
120                             div.icon24-outer.mr-2 {
121                               div.icon24-inner {_out.push(fa_redo)}
122                             }
123                             'New'
124                           }
125                         }
126                       }
127                       div.invalid-feedback(for="verification-code") {'Please enter the 6 characters from the verification image above. We need this to protect us from spam and bots.'}
128                     }
129                   }
130                 }
131               }
132
133               if (sign_up_draft !== null)
134                 button.btn.btn-success#card-1-create-account(type="button") {
135                   div.icon24-outer.mr-2 {
136                     div.icon24-inner {_out.push(fa_user_plus)}
137                   }
138                   'Create account'
139                 }
140               else
141                 button.btn.btn-success#card-1-create-account(type="button" disabled) {
142                   div.icon24-outer.mr-2 {
143                     div.icon24-inner {_out.push(fa_user_plus)}
144                   }
145                   'Create account'
146                 }
147
148               p.'mt-3'.mb-0#card-1-message(hidden) {}
149             }
150           }
151         }
152         div.card#card-2 {
153           div.card-header#card-2-heading(role="tab") {
154             div.icon32-outer.mr-2#card-2-tick(hidden) {
155               div.icon32-inner {_out.push(icon_tick)}
156             }
157             div.icon32-outer.mr-2#card-2-cross(hidden) {
158               div.icon32-inner {_out.push(icon_cross)}
159             }
160             div.icon32-outer.mr-2#card-2-spinner(hidden) {
161               div.icon32-inner {
162                 div.spinner-border(role="status") {}
163               }
164             }
165             a.h5.collapsed(data-toggle="collapse" data-parent="#accordion" href="#card-2-collapse" aria-expanded="false" aria-controls="card-2-collapse") {
166               'Send email verification link'
167             }
168           }
169           div#card-2-collapse.collapse(role="tabpanel" aria-labelledby="card-2-heading" data-parent="#accordion") {
170             div.card-body {
171               button.btn.btn-outline-secondary#card-2-back(type="button") {
172                 div.icon24-outer.mr-2 {
173                   div.icon24-inner {_out.push(fa_arrow_circle_left)}
174                 }
175                 'Back'
176               }
177               button.btn.btn-outline-secondary.ml-3#card-2-resend-email(type="button" disabled) {
178                 div.icon24-outer.mr-2 {
179                   div.icon24-inner {_out.push(fa_envelope)}
180                 }
181                 'Re-send email'
182               }
183
184               p.'mt-3'.mb-0#card-2-message(hidden) {}
185             }
186           }
187         }
188       }
189
190       p.text-muted.mt-3 {'* These fields are required.'}
191     },
192     // scripts
193     async _out => {
194       //script(src="/js/utils.js") {}
195
196       script {
197         document.addEventListener(
198           'DOMContentLoaded',
199           () => {
200             let id_accordion = document.getElementById('accordion')
201             let id_card_1 = document.getElementById('card-1')
202             let id_card_1_collapse = document.getElementById('card-1-collapse')
203             let id_card_1_create_account = document.getElementById('card-1-create-account')
204             let id_card_1_cross = document.getElementById('card-1-cross')
205             let id_card_1_heading = document.getElementById('card-1-heading')
206             let id_card_1_message = document.getElementById('card-1-message')
207             let id_card_1_new = document.getElementById('card-1-new')
208             let id_card_1_spinner = document.getElementById('card-1-spinner')
209             let id_card_1_tick = document.getElementById('card-1-tick')
210             let id_card_2 = document.getElementById('card-2')
211             let id_card_2_back = document.getElementById('card-2-back')
212             let id_card_2_collapse = document.getElementById('card-2-collapse')
213             let id_card_2_cross = document.getElementById('card-2-cross')
214             let id_card_2_heading = document.getElementById('card-2-heading')
215             let id_card_2_message = document.getElementById('card-2-message')
216             let id_card_2_resend_email = document.getElementById('card-2-resend-email')
217             let id_card_2_spinner = document.getElementById('card-2-spinner')
218             let id_card_2_tick = document.getElementById('card-2-tick')
219             let id_contact_me = document.getElementById('contact-me')
220             let id_email = document.getElementById('email')
221             let id_family_name = document.getElementById('family-name')
222             let id_form = document.getElementById('form')
223             let id_given_names = document.getElementById('given-names')
224             let id_password = document.getElementById('password')
225             let id_valid_workaround = document.getElementById('valid-workaround')
226             let id_verification_code = document.getElementById('verification-code')
227             let id_verification_image = document.getElementById('verification-image')
228
229             let input_semaphore = new BinarySemaphore(false)
230             ;(
231               async () => {
232                 while (true) {
233                   await input_semaphore.acquire()
234                   await new Promise(resolve => setTimeout(resolve, 3000))
235                   input_semaphore.try_acquire()
236                   await api_call(
237                     '/api/account/sign_up/set_draft.json',
238                     id_given_names.value.length === 0 &&
239                       id_family_name.value.length === 0 &&
240                       id_contact_me.checked &&
241                       id_email.value.length === 0 ?
242                       null :
243                       {
244                         email: id_email.value.slice(0, 256).toLowerCase(),
245                         given_names: id_given_names.value.slice(0, 256),
246                         family_name: id_family_name.value.slice(0, 256),
247                         contact_me: id_contact_me.checked ? true : false
248                       }
249                   )
250                 }
251               }
252             )() // ignore returned promise (start thread)
253
254             let card_1_edited = in_draft => {
255               if (in_draft)
256                 input_semaphore.release()
257
258               id_card_1_create_account.disabled =
259                 id_given_names.value.length === 0 &&
260                   id_family_name.value.length === 0 &&
261                   id_contact_me.checked &&
262                   id_email.value.length === 0 &&
263                   id_password.value.length === 0 &&
264                   id_verification_code.value.length === 0
265               id_card_1_tick.hidden = true
266               id_card_1_cross.hidden = true
267               id_card_1_spinner.hidden = true
268               id_card_1_message.hidden = true
269
270               id_card_2_resend_email.disabled = true
271               id_card_2_tick.hidden = true
272               id_card_2_cross.hidden = true
273               id_card_2_spinner.hidden = true
274               id_card_2_message.hidden = true
275             }
276
277             let details
278             let card_1 = async () => {
279               id_card_1_tick.hidden = true
280               id_card_1_cross.hidden = true
281               id_card_1_spinner.hidden = true
282               // the below causes an ugly flicker, so just keep the message
283               //id_card_1_message.hidden = true
284
285               if (!id_form.checkValidity()) {
286                 // workaround for https://github.com/twbs/bootstrap/issues/23454
287                 if (id_verification_code.validity.valid) {
288                   id_valid_workaround.classList.add('is-valid')
289                   id_valid_workaround.classList.remove('is-invalid')
290                 }
291                 else {
292                   id_valid_workaround.classList.remove('is-valid')
293                   id_valid_workaround.classList.add('is-invalid')
294                 }
295                 id_form.classList.add('was-validated');
296
297                 id_card_1_cross.hidden = false
298                 return false
299               }
300               // workaround for https://github.com/twbs/bootstrap/issues/23454
301               id_valid_workaround.classList.remove('is-valid')
302               id_valid_workaround.classList.remove('is-invalid')
303               id_form.classList.remove('was-validated');
304
305               details = {
306                 email: id_email.value.slice(0, 256).toLowerCase(),
307                 given_names: id_given_names.value.slice(0, 256),
308                 family_name: id_family_name.value.slice(0, 256),
309                 password: id_password.value.slice(0, 256),
310                 contact_me: id_contact_me.checked ? true : false
311               }
312
313               id_card_1_spinner.hidden = false
314               try {
315                 await api_call(
316                   '/api/account/sign_up/create_account.json',
317                   id_verification_code.value.slice(0, 6).toLowerCase(),
318                   details
319                 )
320               }
321               catch (error) {
322                 let problem = Problem.from(error)
323
324                 id_card_1_cross.hidden = false
325                 id_card_1_spinner.hidden = true
326
327                 id_card_1_message.textContent = problem.detail
328                 //id_card_1_message.classList.remove('text-success')
329                 id_card_1_message.classList.add('text-danger')
330                 id_card_1_message.hidden = false
331
332                 $('#card-1-collapse').collapse('show')
333                 return false
334               }
335               id_card_1_tick.hidden = false
336               id_card_1_spinner.hidden = true
337               id_card_1_message.textContent = `Your account with email "${details.email}" has been created.`
338               //id_card_1_message.classList.add('text-success')
339               id_card_1_message.classList.remove('text-danger')
340               id_card_1_message.hidden = false
341
342               id_card_2_resend_email.disabled = false
343               id_card_2.scrollIntoView()
344               return true
345             }
346
347             let card_2 = async () => {
348               id_card_2_tick.hidden = true
349               id_card_2_cross.hidden = true
350               id_card_2_spinner.hidden = false
351               // the below causes an ugly flicker, so just keep the message
352               //id_card_2_message.hidden = true
353
354               try {
355                 await api_call(
356                   '/api/account/sign_up/send_email_verification_link.json',
357                   details.email
358                 )
359               }
360               catch (error) {
361                 let problem = Problem.from(error)
362
363                 id_card_2_cross.hidden = false
364                 id_card_2_spinner.hidden = true
365
366                 id_card_2_message.textContent = problem.detail
367                 //id_card_2_message.classList.remove('text-success')
368                 id_card_2_message.classList.add('text-danger')
369                 id_card_2_message.hidden = false
370
371                 $('#card-2-collapse').collapse('show')
372                 return false
373               }
374               id_card_2_tick.hidden = false
375               id_card_2_spinner.hidden = true
376
377               id_card_2_message.textContent = `Email verification link has been sent to "${details.email}". Please check your email for next steps.`
378               //id_card_2_message.classList.add('text-success')
379               id_card_2_message.classList.remove('text-danger')
380               id_card_2_message.hidden = false
381               return true
382             }
383
384             id_given_names.addEventListener(
385               'input',
386               () => {card_1_edited(true)}
387             )
388             id_family_name.addEventListener(
389               'input',
390               () => {card_1_edited(true)}
391             )
392             id_email.addEventListener(
393               'input',
394               () => {card_1_edited(true)}
395             )
396             id_password.addEventListener(
397               'input',
398               () => {card_1_edited(false)}
399             )
400             id_contact_me.addEventListener(
401               'input',
402               () => {card_1_edited(true)}
403             )
404             id_verification_code.addEventListener(
405               'input',
406               () => {card_1_edited(false)}
407             )
408
409             let image_seq = 1
410             id_card_1_new.addEventListener(
411               'click',
412               () => {
413                 id_verification_image.src =
414                   `/api/verification_image.png?seq=${image_seq}`
415                 image_seq += 1
416               }
417             )
418
419             id_card_1_create_account.addEventListener(
420               'click',
421               async () => {
422                 if (await card_1() && await card_2())
423                   $('#card-2-collapse').collapse('show')
424               }
425             )
426
427             id_card_2_back.addEventListener(
428               'click',
429               () => {$('#card-1-collapse').collapse('show')}
430             )
431
432             id_card_2_resend_email.addEventListener(
433               'click',
434               async () => {
435                 if (await card_2())
436                   $('#card-2-collapse').collapse('show')
437               }
438             )
439           }
440         )
441       }
442     }
443   )
444 }