Skip to content

Commit 046958c

Browse files
Merge pull request #55 from harshithab371/docs/add-inline-comments-script-js
docs: add inline comments to script.js for new contributors
2 parents a79e9a0 + 167eb92 commit 046958c

1 file changed

Lines changed: 40 additions & 25 deletions

File tree

static/script.js

Lines changed: 40 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@
1111
// ============================================================
1212
// Detect which page we are on
1313
// ============================================================
14-
var isIndexPage = !!document.getElementById("recommend-form");
14+
// !! trick turns the DOM result into a simple true/false
15+
var isIndexPage = !!document.getElementById("recommend-form");
16+
// PROJECT_ID is set by the server only on detail pages, so if it's missing we're elsewhere
1517
var isDetailPage = typeof PROJECT_ID !== "undefined";
1618

1719

@@ -22,11 +24,14 @@ var isDetailPage = typeof PROJECT_ID !== "undefined";
2224
var toggle = document.getElementById("nav-mobile-toggle");
2325
var menu = document.getElementById("nav-mobile-menu");
2426

27+
// Nothing to do if the nav isn't on this page, just bail out
2528
if (!toggle || !menu) return;
2629

2730
toggle.addEventListener("click", function () {
31+
// classList.toggle returns true if class was added, false if removed
2832
var isOpen = menu.classList.toggle("open");
2933
toggle.classList.toggle("open", isOpen);
34+
// Keep aria-expanded in sync so screen readers know if menu is open or closed
3035
toggle.setAttribute("aria-expanded", isOpen);
3136
});
3237

@@ -116,13 +121,11 @@ if (isIndexPage) {
116121
var matchedSkill = availableSkills.find(function (skill) {
117122
return normalizeSkill(skill) === normalizedSkill;
118123
});
119-
120124
return matchedSkill || rawSkill.trim();
121125
}
122126

123127
function getFilteredSkills(query) {
124128
var normalizedQuery = normalizeSkill(query);
125-
126129
return availableSkills.filter(function (skill) {
127130
return normalizeSkill(skill).includes(normalizedQuery) && !isSkillSelected(skill);
128131
}).slice(0, 8);
@@ -134,7 +137,6 @@ if (isIndexPage) {
134137

135138
function renderActiveSuggestion() {
136139
if (!suggestionsDiv) return;
137-
138140
suggestionsDiv.querySelectorAll(".suggestion-item").forEach(function (item, index) {
139141
var isActive = index === activeSuggestionIndex;
140142
item.classList.toggle("suggestion-item--active", isActive);
@@ -145,12 +147,10 @@ if (isIndexPage) {
145147
function hideSuggestions() {
146148
visibleSuggestions = [];
147149
activeSuggestionIndex = -1;
148-
149150
if (suggestionsDiv) {
150151
suggestionsDiv.style.display = "none";
151152
suggestionsDiv.innerHTML = "";
152153
}
153-
154154
syncSuggestionsA11yState();
155155
}
156156

@@ -163,15 +163,12 @@ if (isIndexPage) {
163163

164164
function displaySuggestions(items) {
165165
if (!suggestionsDiv) return;
166-
167166
visibleSuggestions = items;
168167
activeSuggestionIndex = -1;
169-
170168
if (items.length === 0) {
171169
hideSuggestions();
172170
return;
173171
}
174-
175172
suggestionsDiv.innerHTML = "";
176173
items.forEach(function (skill, index) {
177174
var item = document.createElement("div");
@@ -197,7 +194,6 @@ if (isIndexPage) {
197194

198195
suggestionsDiv.appendChild(item);
199196
});
200-
201197
suggestionsDiv.style.display = "block";
202198
syncSuggestionsA11yState();
203199
}
@@ -216,9 +212,7 @@ if (isIndexPage) {
216212
if (visibleSuggestions.length === 0) {
217213
displaySuggestions(getFilteredSkills(skillsTextInput.value));
218214
}
219-
220215
if (visibleSuggestions.length === 0) return;
221-
222216
evt.preventDefault();
223217
if (evt.key === "ArrowDown") {
224218
activeSuggestionIndex = (activeSuggestionIndex + 1) % visibleSuggestions.length;
@@ -227,7 +221,6 @@ if (isIndexPage) {
227221
? visibleSuggestions.length - 1
228222
: activeSuggestionIndex - 1;
229223
}
230-
231224
renderActiveSuggestion();
232225
return;
233226
}
@@ -239,30 +232,25 @@ if (isIndexPage) {
239232

240233
if (evt.key === "Enter") {
241234
evt.preventDefault();
242-
243235
if (activeSuggestionIndex >= 0 && visibleSuggestions[activeSuggestionIndex]) {
244236
selectSuggestion(visibleSuggestions[activeSuggestionIndex]);
245237
return;
246238
}
247-
248239
if (skillsTextInput.value.trim()) {
249240
addSkill(skillsTextInput.value);
250241
skillsTextInput.value = "";
251242
}
252-
253243
hideSuggestions();
254244
}
255245
});
256246

257247
// Show suggestions on input
258248
skillsTextInput.addEventListener("input", function (evt) {
259249
var typedValue = evt.target.value.trim();
260-
261250
if (typedValue.length === 0) {
262251
hideSuggestions();
263252
return;
264253
}
265-
266254
displaySuggestions(getFilteredSkills(typedValue));
267255
});
268256

@@ -299,7 +287,9 @@ if (isIndexPage) {
299287
});
300288

301289
function addSkill(rawSkill) {
290+
// Clean up any extra spaces and match to canonical skill name
302291
var skill = getCanonicalSkill(rawSkill);
292+
// Nothing to add if string is empty after trimming
303293
if (!skill) return;
304294

305295
// Block duplicate entries (case-insensitive)
@@ -309,20 +299,22 @@ if (isIndexPage) {
309299
renderSelectedChips();
310300
syncSkillsHiddenInput();
311301
updateQuickPickState();
302+
// Once a skill is added, remove the "please add a skill" error if it was showing
312303
clearFieldError("skills-error");
313304
}
314305

315306
function removeSkill(skill) {
307+
// Rebuild the array without the skill that was just removed
316308
selectedSkills = selectedSkills.filter(function (selectedSkill) {
317309
return normalizeSkill(selectedSkill) !== normalizeSkill(skill);
318310
});
319-
320311
renderSelectedChips();
321312
syncSkillsHiddenInput();
322313
updateQuickPickState();
323314
}
324315

325316
function renderSelectedChips() {
317+
// Wipe out old chips first so we don't end up with duplicates in the UI
326318
chipsSelectedEl.innerHTML = "";
327319
selectedSkills.forEach(function (skill) {
328320
var chipEl = document.createElement("span");
@@ -336,6 +328,7 @@ if (isIndexPage) {
336328
removeBtn.innerHTML = "×";
337329
removeBtn.setAttribute("aria-label", "Remove " + skill);
338330
removeBtn.addEventListener("click", function (e) {
331+
// Stop click from bubbling up to the chip wrap's click listener
339332
e.stopPropagation();
340333
removeSkill(skill);
341334
});
@@ -347,6 +340,7 @@ if (isIndexPage) {
347340

348341
function syncSkillsHiddenInput() {
349342
// Keep the hidden <input> in sync for form serialisation
343+
// The API expects a comma-separated string, so join the array that way
350344
skillsHidden.value = selectedSkills.join(", ");
351345
}
352346

@@ -376,6 +370,7 @@ if (isIndexPage) {
376370
function validateForm() {
377371
var valid = true;
378372

373+
// Check both the array and the hidden input since skills can come from either source
379374
if (selectedSkills.length === 0 && !skillsHidden.value.trim()) {
380375
showFieldError("skills-error", "Please add at least one skill.");
381376
valid = false;
@@ -416,10 +411,11 @@ if (isIndexPage) {
416411
setLoadingState(true);
417412

418413
var payload = {
419-
skills: skillsHidden.value.trim() || skillsTextInput.value.trim(),
420-
level: document.getElementById("level").value,
414+
// Prefer the hidden input value; fall back to raw text box if hidden input is empty
415+
skills: skillsHidden.value.trim() || skillsTextInput.value.trim(),
416+
level: document.getElementById("level").value,
421417
interest: document.getElementById("interest").value,
422-
time: document.getElementById("time").value
418+
time: document.getElementById("time").value
423419
};
424420

425421
fetch("/api/recommend", {
@@ -430,13 +426,11 @@ if (isIndexPage) {
430426
.then(function (res) { return res.json(); })
431427
.then(function (data) {
432428
setLoadingState(false);
433-
434429
if (data.error) {
435430
var generalErr = document.getElementById("form-error-general");
436431
if (generalErr) generalErr.textContent = data.error;
437432
return;
438433
}
439-
440434
renderResults(data.projects || [], data.message);
441435
})
442436
.catch(function (err) {
@@ -448,6 +442,7 @@ if (isIndexPage) {
448442
});
449443

450444
function setLoadingState(isLoading) {
445+
// Disable the button so the user can't accidentally submit twice
451446
submitBtn.disabled = isLoading;
452447
btnLabel.style.display = isLoading ? "none" : "inline";
453448
btnLoading.style.display = isLoading ? "inline" : "none";
@@ -458,6 +453,7 @@ if (isIndexPage) {
458453
resultsLoadingEl.style.display = "block";
459454
resultsGrid.style.display = "none";
460455
resultsEmptyEl.style.display = "none";
456+
// Scroll down so the user can see the spinner without manually scrolling
461457
resultsSection.scrollIntoView({ behavior: "smooth" });
462458
} else {
463459
resultsLoadingEl.style.display = "none";
@@ -473,6 +469,7 @@ if (isIndexPage) {
473469
function renderResults(projects, message) {
474470
resultsSection.style.display = "block";
475471
resultsLoadingEl.style.display = "none";
472+
// Clear out any cards from a previous search before showing new ones
476473
resultsGrid.innerHTML = "";
477474

478475
if (!projects || projects.length === 0) {
@@ -505,6 +502,7 @@ if (isIndexPage) {
505502
// Description (truncated for visual consistency)
506503
var desc = document.createElement("p");
507504
desc.className = "project-card-desc";
505+
// Cut description to 120 chars so all cards stay the same height
508506
desc.textContent = truncate(project.description, 120);
509507

510508
// Tags row
@@ -517,6 +515,7 @@ if (isIndexPage) {
517515
});
518516

519517
// Level tag (colour-coded via CSS class)
518+
// Lowercase so it matches the CSS class names like "level beginner", "level advanced"
520519
var levelClass = "level " + (project.level || "").toLowerCase();
521520
tagsRow.appendChild(createTag(project.level, levelClass));
522521

@@ -544,13 +543,16 @@ if (isIndexPage) {
544543

545544
function createTag(text, type) {
546545
var span = document.createElement("span");
546+
// The type becomes a BEM modifier so CSS can style each tag differently
547547
span.className = "project-tag project-tag--" + type;
548548
span.textContent = text;
549549
return span;
550550
}
551551

552552
function truncate(text, maxLength) {
553+
// Safety check — just return empty string if text is missing
553554
if (!text) return "";
555+
// Only add "..." if the text is actually longer than the limit
554556
return text.length > maxLength ? text.slice(0, maxLength) + "..." : text;
555557
}
556558

@@ -574,22 +576,27 @@ if (isDetailPage) {
574576
var codeFetched = false;
575577

576578
function openCodePanel() {
579+
// Panel element might not exist on every detail page, so check first
577580
if (!codePanel) return;
578581
codePanel.classList.add("active");
579582
if (codePanelOverlay) codePanelOverlay.classList.add("active");
583+
// Lock background scroll so the page doesn't scroll behind the panel
580584
document.body.style.overflow = "hidden";
581585

586+
// Only fetch the code on the first open, no need to re-fetch every time
582587
if (!codeFetched) fetchStarterCode();
583588
}
584589

585590
function closeCodePanel() {
586591
if (!codePanel) return;
587592
codePanel.classList.remove("active");
588593
if (codePanelOverlay) codePanelOverlay.classList.remove("active");
594+
// Restore normal scrolling once the panel is closed
589595
document.body.style.overflow = "";
590596
}
591597

592598
function fetchStarterCode() {
599+
// Show a loading message while we wait for the API response
593600
if (codeContentEl) codeContentEl.textContent = "Loading starter code...";
594601

595602
fetch("/project/" + PROJECT_ID + "/code")
@@ -601,6 +608,7 @@ if (isDetailPage) {
601608
}
602609
if (codePanelFilename) codePanelFilename.textContent = data.filename;
603610
if (codeContentEl) codeContentEl.textContent = data.code;
611+
// Mark as fetched so we don't hit the API again on the next open
604612
codeFetched = true;
605613
})
606614
.catch(function () {
@@ -619,6 +627,7 @@ if (isDetailPage) {
619627
codePanelOverlay.addEventListener("click", closeCodePanel);
620628
}
621629

630+
// Let keyboard users close the panel with Escape — important for accessibility
622631
document.addEventListener("keydown", function (evt) {
623632
if (evt.key === "Escape") closeCodePanel();
624633
});
@@ -642,6 +651,7 @@ if (isDetailPage) {
642651
if (checkIcon) checkIcon.style.display = "inline";
643652
if (btnLabel) btnLabel.textContent = "Copied!";
644653
btnCopyCode.classList.add("copied");
654+
// Disable button so user can't spam click it while toast is showing
645655
btnCopyCode.disabled = true;
646656

647657
// Show toast
@@ -650,6 +660,7 @@ if (isDetailPage) {
650660
}
651661

652662
// Auto-reset after 2.5 s
663+
// Clear any previous timeout first so timers don't stack up
653664
clearTimeout(toastTimeout);
654665
toastTimeout = setTimeout(function () {
655666
if (copyIcon) copyIcon.style.display = "inline";
@@ -664,6 +675,7 @@ if (isDetailPage) {
664675
if (btnCopyCode) {
665676
btnCopyCode.addEventListener("click", function () {
666677
var code = codeContentEl ? codeContentEl.textContent : "";
678+
// Don't copy if the code hasn't loaded yet — just ignore the click
667679
if (!code || code === "Loading..." || code === "Loading starter code...") return;
668680

669681
// Use Clipboard API with textarea fallback
@@ -678,14 +690,17 @@ if (isDetailPage) {
678690
}
679691

680692
function fallbackCopy(text) {
693+
// Some older browsers don't support navigator.clipboard, so we use a hidden textarea instead
681694
var ta = document.createElement("textarea");
682695
ta.value = text;
696+
// Push it off-screen so it's not visible but can still be selected
683697
ta.style.cssText = "position:fixed;top:-9999px;left:-9999px;opacity:0";
684698
document.body.appendChild(ta);
685699
ta.focus();
686700
ta.select();
701+
// execCommand is old and deprecated but works as a last resort — fail silently if it doesn't
687702
try { document.execCommand("copy"); showCopySuccess(); } catch (e) { /* silent fail */ }
688703
document.body.removeChild(ta);
689704
}
690705

691-
} // end isDetailPage
706+
} // end isDetailPage

0 commit comments

Comments
 (0)