From f174bb5f68a9e518f7c847ec4329bd8d985fa7db Mon Sep 17 00:00:00 2001 From: popovskik Date: Fri, 31 Jul 2026 12:03:07 +0200 Subject: [PATCH] Feedback: space/bottom-align Send button; submit No + comment as one row Comment area is now a flex row that bottom-aligns the Send button with a 16px gap. The No vote is held until the comment is sent so both land on one Google Form row; a No left without a comment is still recorded once via sendBeacon on page-leave. Co-Authored-By: Claude Opus 4.8 (1M context) --- css/pages/_guide.css | 19 ++++++---- guide-feedback.js | 90 +++++++++++++++++++++++++------------------- 2 files changed, 64 insertions(+), 45 deletions(-) diff --git a/css/pages/_guide.css b/css/pages/_guide.css index 18d369fc..5ae5606c 100644 --- a/css/pages/_guide.css +++ b/css/pages/_guide.css @@ -511,16 +511,20 @@ .guide-feedback-comment { flex-basis: 100%; margin-top: 4px; + display: flex; + flex-wrap: wrap; + align-items: flex-end; + gap: 16px; } .guide-feedback-comment-label { - display: block; - margin-bottom: 6px; + flex-basis: 100%; + margin: 0; font-size: 14px; font-weight: 600; color: #344054; } .guide-feedback-comment-input { - width: 100%; + flex: 1 1 300px; max-width: 520px; padding: 10px 12px; border: 1.5px solid #D0D5DD; @@ -535,9 +539,9 @@ box-shadow: 0 0 0 3px rgba(115, 96, 242, .15); } .guide-feedback-comment-send { - display: inline-block; - margin-top: 8px; - padding: 8px 20px; + flex: 0 0 auto; + margin: 0; + padding: 11px 24px; border: none; border-radius: 8px; background: #7360F2; @@ -557,7 +561,8 @@ transform: none; } .guide-feedback-comment-thanks { - margin: 8px 0 0; + flex-basis: 100%; + margin: 0; color: #12B76A; font-weight: 600; } diff --git a/guide-feedback.js b/guide-feedback.js index 484bc394..b6960557 100644 --- a/guide-feedback.js +++ b/guide-feedback.js @@ -4,6 +4,11 @@ * and (on "No") a short comment box is offered. Voting is locked for the current * page view, so to give feedback again the visitor reloads the page. * + * "Yes" is submitted immediately. "No" is submitted together with the comment as + * a single row when the visitor clicks Send; if they leave without commenting, + * the "No" is still recorded once via sendBeacon on page-leave. This avoids the + * "No" and the comment landing on two separate rows. + * * Votes are sent to a Google Form (see setup notes at the bottom). If the form * is not configured, the widget still works locally and "No" still routes the * visitor to email / the Viber community. @@ -19,46 +24,46 @@ commentField: 'entry.1634915934' // Question 3 (Comment) }; + // "No" votes awaiting submission (visitor clicked No but has not sent yet). + var pending = []; + function pageLabel(slug) { var lang = (document.documentElement.getAttribute('lang') || '').toLowerCase(); return lang ? slug + ' (' + lang + ')' : slug; } - function postToForm(fields) { + function buildParams(slug, value, comment) { + var params = new URLSearchParams(); + params.append(GOOGLE_FORM.pageField, pageLabel(slug)); + if (value) { params.append(GOOGLE_FORM.helpfulField, value); } + if (comment && GOOGLE_FORM.commentField) { params.append(GOOGLE_FORM.commentField, comment); } + return params; + } + + function submit(slug, value, comment) { if (!GOOGLE_FORM.action) { return; } try { - var body = new URLSearchParams(); - for (var key in fields) { - if (fields.hasOwnProperty(key) && key) { body.append(key, fields[key]); } - } // no-cors: Google Forms returns no CORS headers, so the response cannot be // read, but the submission is recorded. - fetch(GOOGLE_FORM.action, { method: 'POST', mode: 'no-cors', body: body }); + fetch(GOOGLE_FORM.action, { method: 'POST', mode: 'no-cors', body: buildParams(slug, value, comment) }); } catch (e) {} - } - - function sendVote(slug, value) { - var label = pageLabel(slug); - var fields = {}; - fields[GOOGLE_FORM.pageField] = label; - fields[GOOGLE_FORM.helpfulField] = value; - postToForm(fields); if (typeof window.plausible === 'function') { - try { window.plausible('Guide feedback', { props: { page: label, value: value } }); } catch (e) {} + try { window.plausible('Guide feedback', { props: { page: pageLabel(slug), value: value || 'comment' } }); } catch (e) {} } } - function sendComment(slug, text) { - var label = pageLabel(slug); - var fields = {}; - fields[GOOGLE_FORM.pageField] = label; - // helpful field is left out so comment rows do not inflate the Yes/No counts. - if (GOOGLE_FORM.commentField) { fields[GOOGLE_FORM.commentField] = text; } - postToForm(fields); - if (typeof window.plausible === 'function') { - try { window.plausible('Guide feedback comment', { props: { page: label } }); } catch (e) {} + function flushPending() { + for (var i = 0; i < pending.length; i++) { + var slug = pending[i]; + if (GOOGLE_FORM.action && navigator.sendBeacon) { + try { navigator.sendBeacon(GOOGLE_FORM.action, buildParams(slug, 'no', '')); } catch (e) {} + } else { + submit(slug, 'no', ''); + } } + pending = []; } + window.addEventListener('pagehide', flushPending); function setup(fb) { if (fb.getAttribute('data-ready')) { return; } @@ -73,6 +78,14 @@ var commentSend = fb.querySelector('.guide-feedback-comment-send'); var commentThanks = fb.querySelector('.guide-feedback-comment-thanks'); var voted = false; + var noPending = false; + + function clearPending() { + if (!noPending) { return; } + noPending = false; + var idx = pending.indexOf(slug); + if (idx > -1) { pending.splice(idx, 1); } + } function onVote() { /* jshint validthis:true */ @@ -81,10 +94,14 @@ var value = this.getAttribute('data-vote'); for (var i = 0; i < buttons.length; i++) { buttons[i].disabled = true; } this.classList.add('is-selected'); - sendVote(slug, value); if (value === 'yes') { + submit(slug, 'yes', ''); if (yesMsg) { yesMsg.hidden = false; } } else { + // Hold the "No" until the comment is sent (or the page is left), so both + // land on the same row. + noPending = true; + pending.push(slug); if (noMsg) { noMsg.hidden = false; } if (comment) { comment.hidden = false; } } @@ -98,7 +115,8 @@ commentSend.addEventListener('click', function () { var text = commentInput ? commentInput.value.replace(/^\s+|\s+$/g, '') : ''; if (!text) { if (commentInput) { commentInput.focus(); } return; } - sendComment(slug, text); + clearPending(); + submit(slug, 'no', text); commentSend.disabled = true; if (commentInput) { commentInput.disabled = true; } if (commentThanks) { commentThanks.hidden = false; } @@ -122,17 +140,13 @@ * --------------------------------------------------------------------------- * Google Form collection * --------------------------------------------------------------------------- - * The form currently has two questions wired: Page and Helpful. - * To capture the "what was missing" comments, add a THIRD question to the same - * Google Form: - * - Type: Paragraph - * - Title: Comment - * Then open the three-dot menu > "Get pre-filled link", type any text in all - * three boxes, get the link, and read off the new entry.NNNN for the Comment - * box. Paste it into GOOGLE_FORM.commentField above. - * + * Three questions are wired: Page, Helpful, Comment (entry IDs above). * The "Page" value sent is the slug plus language, e.g. - * "student-residence-permit (mk)". Comment submissions omit the Helpful answer - * so they do not affect the Yes/No counts; filter the sheet by a non-empty - * Comment column to read them. + * "student-residence-permit (mk)". + * + * Rows you will see: + * - Yes vote -> Helpful = yes, Comment empty + * - No with a comment -> Helpful = no, Comment = the text (one row) + * - No without comment -> Helpful = no, Comment empty (sent on leave) + * A "No" produces exactly one row either way. */