Skip to content

Commit 6aaf12c

Browse files
Monil-KTXktx-krupa
andauthored
Feat/integrate feedback menu (#186)
* fix: resolve icon path issue of header * fix: remove heading tag from index file * feat: integrate feedback menu feature in footer * fix: remove unnecessary console warnings and logs in feedback tracking * fix: add docs heading --------- Co-authored-by: krupa <[email protected]>
1 parent 356e6cb commit 6aaf12c

File tree

7 files changed

+697
-4
lines changed

7 files changed

+697
-4
lines changed

docs/assets/feedback.svg

Lines changed: 3 additions & 0 deletions
Loading

docs/js/feedback.js

Lines changed: 245 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,245 @@
1+
// feedback.js
2+
document.addEventListener("DOMContentLoaded", () => {
3+
const feedbackButton = document.querySelector("#feedbackButton");
4+
const modal = document.querySelector("#feedbackModal");
5+
6+
if (!feedbackButton || !modal) {
7+
return;
8+
}
9+
10+
const form = modal.querySelector("form");
11+
const successView = modal.querySelector(".success-view");
12+
const formView = modal.querySelector(".form-view");
13+
const errorView = modal.querySelector(".error-view");
14+
const tabs = modal.querySelectorAll(".feedback-tab");
15+
let lastActiveElement = null;
16+
17+
// Ensure the form exists before touching it
18+
if (!form) {
19+
return;
20+
}
21+
22+
// ensure there's an input[name=type] for the form (hidden) so the submit code can read it
23+
let typeInput = form.querySelector("input[name=type]");
24+
if (!typeInput) {
25+
typeInput = document.createElement("input");
26+
typeInput.type = "hidden";
27+
typeInput.name = "type";
28+
typeInput.value = "Issue";
29+
form.appendChild(typeInput);
30+
}
31+
32+
function openModal() {
33+
// store previous active element so we can restore focus when modal closes
34+
lastActiveElement = document.activeElement;
35+
modal.classList.remove("tw-hidden");
36+
calculatePosition();
37+
// focus the textarea for immediate typing if present
38+
const ta = modal.querySelector("textarea");
39+
if (ta) ta.focus();
40+
}
41+
42+
function closeModal() {
43+
modal.classList.add("tw-hidden");
44+
form.reset();
45+
errorView.classList.add("tw-hidden");
46+
successView.classList.add("tw-hidden");
47+
// remove layout class when hidden to avoid display conflicts
48+
successView.classList.remove("tw-flex");
49+
// clear any inline positioning set during calculatePosition
50+
try {
51+
modal.style.top = "";
52+
modal.style.bottom = "";
53+
} catch (e) {
54+
/* ignore */
55+
}
56+
formView.classList.remove("tw-hidden");
57+
// restore focus to previously active element
58+
try {
59+
if (lastActiveElement && typeof lastActiveElement.focus === "function") {
60+
lastActiveElement.focus();
61+
}
62+
} catch (e) {
63+
/* ignore */
64+
}
65+
}
66+
67+
function calculatePosition() {
68+
// class-based positioning like the Vue component: toggle top-full / bottom-full
69+
try {
70+
const btnRect = feedbackButton.getBoundingClientRect();
71+
const screenHeight = window.innerHeight;
72+
const buttonCenter = btnRect.top + btnRect.height / 2;
73+
const placeAbove = buttonCenter > screenHeight / 2;
74+
75+
// rely on CSS classes and the parent .tw-relative for positioning
76+
modal.classList.remove(
77+
"tw-top-full",
78+
"tw-bottom-full",
79+
"tw-mt-4",
80+
"tw-mb-4"
81+
);
82+
if (placeAbove) {
83+
modal.classList.add("tw-bottom-full", "tw-mb-4");
84+
// explicitly position above using inline style to avoid CSS specificity issues
85+
modal.style.bottom = "100%";
86+
modal.style.top = "";
87+
} else {
88+
modal.classList.add("tw-top-full", "tw-mt-4");
89+
// explicitly position below
90+
modal.style.top = "100%";
91+
modal.style.bottom = "";
92+
}
93+
// ensure right alignment like Vue: right-0 on the modal container
94+
if (!modal.classList.contains("tw-right-0"))
95+
modal.classList.add("tw-right-0");
96+
} catch (err) {
97+
}
98+
}
99+
100+
// wire tab clicks with keyboard navigation and ARIA handling
101+
if (tabs && tabs.length) {
102+
const setActiveTab = (index) => {
103+
tabs.forEach((tb, i) => {
104+
const selected = i === index;
105+
tb.classList.toggle("tw-bg-white", selected);
106+
tb.classList.toggle("tw-text-gray-900", selected);
107+
tb.classList.toggle("tw-shadow-sm", selected);
108+
tb.setAttribute("aria-selected", selected ? "true" : "false");
109+
if (selected) {
110+
const type = tb.getAttribute("data-type") || tb.textContent.trim();
111+
typeInput.value = type;
112+
const ta = modal.querySelector("textarea");
113+
if (ta) ta.placeholder = `Type your ${type.toLowerCase()} here...`;
114+
}
115+
});
116+
};
117+
118+
tabs.forEach((t, idx) => {
119+
t.addEventListener("click", () => {
120+
setActiveTab(idx);
121+
t.focus();
122+
});
123+
124+
t.addEventListener("keydown", (ev) => {
125+
const key = ev.key;
126+
let newIndex = null;
127+
if (key === "ArrowRight") newIndex = (idx + 1) % tabs.length;
128+
else if (key === "ArrowLeft")
129+
newIndex = (idx - 1 + tabs.length) % tabs.length;
130+
else if (key === "Home") newIndex = 0;
131+
else if (key === "End") newIndex = tabs.length - 1;
132+
133+
if (newIndex !== null) {
134+
ev.preventDefault();
135+
setActiveTab(newIndex);
136+
tabs[newIndex].focus();
137+
}
138+
});
139+
});
140+
141+
// init
142+
setActiveTab(0);
143+
}
144+
145+
feedbackButton.addEventListener("click", () => {
146+
try {
147+
if (modal.classList.contains("tw-hidden")) {
148+
openModal();
149+
} else {
150+
closeModal();
151+
}
152+
} catch (err) {
153+
}
154+
});
155+
156+
document.addEventListener("keydown", (e) => {
157+
if (e.key === "Escape") closeModal();
158+
});
159+
160+
document.addEventListener("mousedown", (e) => {
161+
if (!modal.contains(e.target) && !feedbackButton.contains(e.target)) {
162+
closeModal();
163+
}
164+
});
165+
166+
window.addEventListener("resize", calculatePosition);
167+
168+
form.addEventListener("submit", (e) => {
169+
e.preventDefault();
170+
171+
// First, let the browser run HTML5 validation UI (native popup) if any
172+
// required fields are missing. reportValidity() will show the native
173+
// validation message and return false if invalid.
174+
if (typeof form.reportValidity === "function") {
175+
const ok = form.reportValidity();
176+
if (!ok) {
177+
// browser showed a native message; stop submission
178+
return;
179+
}
180+
}
181+
182+
// hide any previous custom error
183+
try {
184+
errorView.classList.add("tw-hidden");
185+
} catch (err) {
186+
/* ignore */
187+
}
188+
189+
// grab textarea and read trimmed value (we already know it's non-empty)
190+
const ta =
191+
form.querySelector("textarea") || modal.querySelector("textarea");
192+
const message = (ta && ta.value && ta.value.trim()) || "";
193+
194+
const data = {
195+
// use the prepared hidden input value (always present)
196+
type: (typeInput && typeInput.value) || "Issue",
197+
message: message,
198+
currentUrl: window.location.href,
199+
userAgent: navigator.userAgent,
200+
source: "feedback_form",
201+
};
202+
203+
// Track feedback in Segment (if segment.js is loaded)
204+
if (typeof window.trackFeedback === "function") {
205+
try {
206+
window.trackFeedback(data);
207+
} catch (e) {
208+
// Segment tracking error should not block submission
209+
}
210+
}
211+
212+
// show immediate success view (keeps original UX), then submit in background
213+
formView.classList.add("tw-hidden");
214+
// ensure success view displays as flex column when visible
215+
successView.classList.add("tw-flex");
216+
successView.classList.remove("tw-hidden");
217+
218+
setTimeout(closeModal, 1500);
219+
220+
fetch(
221+
"https://script.google.com/macros/s/AKfycby5A7NSQCmG4KIBdM0HkRP-5zpRPy8aTrQHiQoe9uG_c_rv1VCiAnnZE8co7-kofgw-hg/exec",
222+
{
223+
method: "POST",
224+
mode: "no-cors",
225+
body: JSON.stringify(data),
226+
headers: { "Content-Type": "application/json" },
227+
}
228+
).catch(() => {
229+
// network failure: hide success and show error
230+
try {
231+
successView.classList.add("tw-hidden");
232+
successView.classList.remove("tw-flex");
233+
formView.classList.remove("tw-hidden");
234+
if (errorView) {
235+
errorView.textContent =
236+
"Failed to submit feedback. Please try again.";
237+
errorView.classList.remove("tw-hidden");
238+
}
239+
if (ta && typeof ta.focus === "function") ta.focus();
240+
} catch (err) {
241+
/* ignore */
242+
}
243+
});
244+
});
245+
});

docs/js/segment.js

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
// docs/js/segment.js
2+
3+
function getAnonymousId() {
4+
let anonId = localStorage.getItem("segment_anonymous_id");
5+
if (!anonId) {
6+
anonId =
7+
"anon_" + Math.random().toString(36).substr(2, 9) + "_" + Date.now();
8+
localStorage.setItem("segment_anonymous_id", anonId);
9+
}
10+
return anonId;
11+
}
12+
13+
function getSegmentProxyUrl() {
14+
// Set your Segment proxy URL here or from a global variable
15+
return "https://swisspipe.dev.zinclabs.dev/api/v1/4e5cac41-4d34-46f9-b862-e7ac551b5a8f/trigger"; // e.g., set in your template
16+
}
17+
18+
function trackFeedback(feedbackData) {
19+
const proxyUrl = getSegmentProxyUrl();
20+
if (!proxyUrl) {
21+
return;
22+
}
23+
const message = {
24+
user: { anonymousId: getAnonymousId() },
25+
event: "O2 Website Docs Feedback",
26+
properties: feedbackData,
27+
timestamp: new Date().toISOString(),
28+
type: "track",
29+
};
30+
fetch(proxyUrl, {
31+
method: "POST",
32+
headers: { "Content-Type": "application/json" },
33+
body: JSON.stringify(message),
34+
})
35+
.then((res) => {
36+
return res.text().then((text) => {
37+
});
38+
})
39+
.catch((e) => {
40+
});
41+
}
42+
43+
window.trackFeedback = trackFeedback;

mkdocs.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ extra_javascript:
2727
- https://buttons.github.io/buttons.js
2828
- js/reo.js
2929
- js/landing-individual-card-ms-tracking.js
30+
- js/feedback.js
31+
- js/segment.js
3032
extra:
3133
# social:
3234
# - icon: fontawesome/brands/linkedin

0 commit comments

Comments
 (0)