Fix feedback spam: lock buttons after vote, add comment box on No
Voting now locks the widget for the page view (buttons disabled, chosen one highlighted, thank-you shown); reload to vote again. Fixes the CSS bug where display:flex overrode the hidden attribute so buttons stayed clickable. On No, an optional comment box posts to a separate Comment field. Adds a guarded [hidden] rule and selected/disabled/comment styles. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
49b290efb1
commit
1416f9f344
95 files changed
+726
-65
No files matched your search
+90
-65
@@ -1,75 +1,107 @@
|
||||
/*
|
||||
* Student Welcome Guide - "Was this page helpful?" feedback.
|
||||
* Records the vote in localStorage so a visitor is not asked twice, and reveals
|
||||
* a thank-you. On "No" the message invites the visitor to email MSOS or ask in
|
||||
* the Viber community, which is where actionable feedback actually reaches us.
|
||||
* On a vote the chosen button is highlighted and locked, a thank-you appears,
|
||||
* 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.
|
||||
*
|
||||
* Note: this stores the vote only in the visitor's own browser. To collect
|
||||
* aggregated Yes/No counts centrally, point the `sendVote` hook below at a form
|
||||
* endpoint (for example Formspree or a Google Form) or a privacy-friendly
|
||||
* analytics event.
|
||||
* 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.
|
||||
*/
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
/*
|
||||
* Central collection via a Google Form.
|
||||
* To turn it on, create a Google Form with two "short answer" questions
|
||||
* (one for the page, one for the answer), then fill in the three values
|
||||
* below. See the setup notes at the bottom of this file. Until they are
|
||||
* filled in, votes are simply not sent anywhere central.
|
||||
*/
|
||||
/* Google Form collection. Fill in all four values to turn it on. */
|
||||
var GOOGLE_FORM = {
|
||||
action: 'https://docs.google.com/forms/d/e/1FAIpQLSc4i6fdIpR3D8nuEJ_yvxlHUBRRO-8Z-vP4WF-qtk17uzkYog/formResponse',
|
||||
pageField: 'entry.1926483387', // Question 1 (Page)
|
||||
helpfulField: 'entry.971202016' // Question 2 (Helpful)
|
||||
helpfulField: 'entry.971202016', // Question 2 (Helpful)
|
||||
commentField: '' // Question 3 (Comment) - paste entry.NNNN once the form has a comment question
|
||||
};
|
||||
|
||||
function sendVote(slug, value) {
|
||||
function pageLabel(slug) {
|
||||
var lang = (document.documentElement.getAttribute('lang') || '').toLowerCase();
|
||||
var pageLabel = lang ? slug + ' (' + lang + ')' : slug;
|
||||
return lang ? slug + ' (' + lang + ')' : slug;
|
||||
}
|
||||
|
||||
if (GOOGLE_FORM.action && GOOGLE_FORM.pageField && GOOGLE_FORM.helpfulField) {
|
||||
try {
|
||||
var body = new URLSearchParams();
|
||||
body.append(GOOGLE_FORM.pageField, pageLabel);
|
||||
body.append(GOOGLE_FORM.helpfulField, value);
|
||||
// no-cors: Google Forms does not return CORS headers, so we cannot read
|
||||
// the response, but the submission is recorded.
|
||||
fetch(GOOGLE_FORM.action, { method: 'POST', mode: 'no-cors', body: body });
|
||||
} catch (e) {}
|
||||
}
|
||||
function postToForm(fields) {
|
||||
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 });
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
// Also fires a Plausible analytics event if Plausible happens to be present.
|
||||
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: pageLabel, value: value } }); } catch (e) {}
|
||||
try { window.plausible('Guide feedback', { props: { page: label, value: value } }); } 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 setup(fb) {
|
||||
if (fb.getAttribute('data-ready')) { return; }
|
||||
fb.setAttribute('data-ready', '1');
|
||||
|
||||
var slug = fb.getAttribute('data-slug') || location.pathname;
|
||||
var key = 'msos-guide-feedback:' + slug;
|
||||
var buttons = fb.querySelector('.guide-feedback-buttons');
|
||||
var buttons = fb.querySelectorAll('[data-vote]');
|
||||
var yesMsg = fb.querySelector('.guide-feedback-yes');
|
||||
var noMsg = fb.querySelector('.guide-feedback-no');
|
||||
var comment = fb.querySelector('.guide-feedback-comment');
|
||||
var commentInput = fb.querySelector('.guide-feedback-comment-input');
|
||||
var commentSend = fb.querySelector('.guide-feedback-comment-send');
|
||||
var commentThanks = fb.querySelector('.guide-feedback-comment-thanks');
|
||||
var voted = false;
|
||||
|
||||
function reveal(value) {
|
||||
if (buttons) { buttons.hidden = true; }
|
||||
if (value === 'yes' && yesMsg) { yesMsg.hidden = false; }
|
||||
if (value === 'no' && noMsg) { noMsg.hidden = false; }
|
||||
function onVote() {
|
||||
/* jshint validthis:true */
|
||||
if (voted) { return; }
|
||||
voted = true;
|
||||
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') {
|
||||
if (yesMsg) { yesMsg.hidden = false; }
|
||||
} else {
|
||||
if (noMsg) { noMsg.hidden = false; }
|
||||
if (comment) { comment.hidden = false; }
|
||||
}
|
||||
}
|
||||
|
||||
var prior = null;
|
||||
try { prior = localStorage.getItem(key); } catch (e) {}
|
||||
if (prior === 'yes' || prior === 'no') { reveal(prior); return; }
|
||||
for (var i = 0; i < buttons.length; i++) {
|
||||
buttons[i].addEventListener('click', onVote);
|
||||
}
|
||||
|
||||
var btns = fb.querySelectorAll('[data-vote]');
|
||||
for (var i = 0; i < btns.length; i++) {
|
||||
btns[i].addEventListener('click', function () {
|
||||
var value = this.getAttribute('data-vote');
|
||||
try { localStorage.setItem(key, value); } catch (e) {}
|
||||
sendVote(slug, value);
|
||||
reveal(value);
|
||||
if (commentSend) {
|
||||
commentSend.addEventListener('click', function () {
|
||||
var text = commentInput ? commentInput.value.replace(/^\s+|\s+$/g, '') : '';
|
||||
if (!text) { if (commentInput) { commentInput.focus(); } return; }
|
||||
sendComment(slug, text);
|
||||
commentSend.disabled = true;
|
||||
if (commentInput) { commentInput.disabled = true; }
|
||||
if (commentThanks) { commentThanks.hidden = false; }
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -88,26 +120,19 @@
|
||||
|
||||
/*
|
||||
* ---------------------------------------------------------------------------
|
||||
* How to turn on Google Form collection
|
||||
* Google Form collection
|
||||
* ---------------------------------------------------------------------------
|
||||
* 1. Go to forms.google.com and create a new blank form (e.g. "Guide feedback").
|
||||
* 2. Add two questions, both of type "Short answer":
|
||||
* - Question 1 title: Page
|
||||
* - Question 2 title: Helpful
|
||||
* (Titles do not matter to the code, only the field IDs below.)
|
||||
* 3. Click the three-dot menu (top right) > "Get pre-filled link".
|
||||
* Type any text in both boxes (e.g. "x" and "yes"), click "Get link",
|
||||
* then "Copy link". Paste it somewhere; it looks like:
|
||||
* https://docs.google.com/forms/d/e/1FAIpQL.../viewform?usp=pp_url&entry.111111=x&entry.222222=yes
|
||||
* 4. From that link read off:
|
||||
* - GOOGLE_FORM.action = the URL up to and including "/formResponse"
|
||||
* (replace the "/viewform..." part with "/formResponse")
|
||||
* - GOOGLE_FORM.pageField = the entry.NNNN before "=x" (the Page box)
|
||||
* - GOOGLE_FORM.helpfulField = the entry.NNNN before "=yes" (the Helpful box)
|
||||
* 5. Paste those three values into the GOOGLE_FORM object near the top of this
|
||||
* file and commit. Responses appear in the form's "Responses" tab and, if you
|
||||
* click the Sheets icon there, in a live Google Sheet with charts.
|
||||
* 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.
|
||||
*
|
||||
* The "Page" value sent is the page slug plus language, e.g.
|
||||
* "student-residence-permit (mk)", so you can see which page and language.
|
||||
* 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.
|
||||
*/
|
||||
Reference in new issue
Block a user