diff --git a/static/script.js b/static/script.js index f97e5a0..907e1fc 100644 --- a/static/script.js +++ b/static/script.js @@ -478,22 +478,20 @@ if (clearFiltersBtn) { // ---------------------------------------------------------- form.addEventListener("submit", function (evt) { - evt.preventDefault(); //stop the browser from reloading the page on form submit - clearAllErrors() - + evt.preventDefault(); + clearAllErrors(); + if (skillsTextInput.value.trim()) { addSkill(skillsTextInput.value); skillsTextInput.value = ""; hideSuggestions(); } - if (!validateForm()) return; //stop - anything missing/invalid + if (!validateForm()) return; setLoadingState(true); - // Allow browser to paint spinner before request starts requestAnimationFrame(function () { - var payload = { skills: skillsHidden.value.trim() || skillsTextInput.value.trim(), level: document.getElementById("level").value, @@ -506,57 +504,44 @@ if (clearFiltersBtn) { headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload) }) - .then(function (res) { - return res.json(); - }) + .then(function (res) { return res.json(); }) .then(function (data) { - setLoadingState(false); if (data.error) { var generalErr = document.getElementById("form-error-general"); - - if (generalErr) { - generalErr.textContent = data.error; - } - + if (generalErr) generalErr.textContent = data.error; return; } renderResults(data.projects || [], data.message); }) .catch(function () { - setLoadingState(false); - //combine form values into an object to send to server/api - var payload = { - // Prefer the hidden input value; fall back to raw text box if hidden input is empty - skills: skillsHidden.value.trim() || skillsTextInput.value.trim(), - level: document.getElementById("level").value, - interest: document.getElementById("interest").value, - time: document.getElementById("time").value - }; + var generalErr = document.getElementById("form-error-general"); + if (generalErr) { + generalErr.textContent = "Something went wrong. Please try again."; + } + }); + }); }); - // Manages the loading state of the form and results section(whats visible or not) + // Manages the loading state of the form and results section function setLoadingState(isLoading) { - // Disable the button so the user can't accidentally submit twice submitBtn.disabled = isLoading; submitBtn.setAttribute("aria-busy", isLoading); btnLabel.style.display = isLoading ? "none" : "inline"; btnLoading.style.display = isLoading ? "inline-flex" : "none"; if (isLoading) { - // Show the results section with only the loading indicator visible resultsSection.style.display = "block"; resultsLoadingEl.style.display = "block"; resultsGrid.style.display = "none"; resultsEmptyEl.style.display = "none"; - // Scroll down so the user can see the spinner without manually scrolling resultsSection.scrollIntoView({ behavior: "smooth" }); } else { - resultsLoadingEl.style.display = "none"; - resultsGrid.style.display = "grid"; //switch back to gird layout + resultsLoadingEl.style.display = "none"; + resultsGrid.style.display = "grid"; } } @@ -565,25 +550,15 @@ if (clearFiltersBtn) { // Render result cards // ---------------------------------------------------------- - //takes the array of projects from the api and draws them on the page as cards - //if array is empty it shows the "no results" message instead function renderResults(projects, message) { resultsSection.style.display = "block"; resultsLoadingEl.style.display = "none"; - // Clear out any cards from a previous search before showing new ones resultsGrid.innerHTML = ""; if (!projects || projects.length === 0) { - resultsGrid.style.display = "none"; - resultsEmptyEl.style.display = "block"; resultsGrid.style.display = "none"; resultsEmptyEl.style.display = "block"; - if (message && emptyMessageEl) emptyMessageEl.textContent = message; - if (!projects || projects.length === 0) { //if no projects returned from api, show the "no results" message and hide the grid - resultsGrid.style.display = "none"; - resultsEmptyEl.style.display = "block"; - // Show a friendly custom message when the user selected an interest var selectedInterest = document.getElementById("interest")?.value; if (selectedInterest) { emptyMessageEl.textContent = "No projects are currently available for this interest. Please check back later or try a different area."; @@ -600,7 +575,6 @@ if (clearFiltersBtn) { resultsEmptyEl.style.display = "none"; resultsGrid.style.display = "grid"; - //build a card for each project and add it to the grid projects.forEach(function (project) { resultsGrid.appendChild(buildProjectCard(project)); }); @@ -681,6 +655,30 @@ if (clearFiltersBtn) { } // end isIndexPage +// ============================================================ +// Code viewer helpers (detail page) +// ============================================================ +function renderCodeWithLineNumbers(code) { + var lines = (code || "").split("\n"); + return lines.map(function (line, index) { + var row = document.createElement("div"); + row.className = "code-line"; + + var lineNum = document.createElement("span"); + lineNum.className = "line-number"; + lineNum.textContent = String(index + 1); + + var lineContent = document.createElement("span"); + lineContent.className = "line-content"; + lineContent.textContent = line; + + row.appendChild(lineNum); + row.appendChild(lineContent); + return row; + }); +} + + // ============================================================ // DETAIL PAGE // ============================================================ @@ -726,7 +724,12 @@ if (isDetailPage) { if (codeContentEl) codeContentEl.textContent = "Loading starter code..."; fetch("/project/" + PROJECT_ID + "/code") - .then(function (res) { return res.json(); }) + .then(function (res) { + return res.json().then(function (data) { + if (!res.ok) throw new Error(data.error || "Failed to load starter code."); + return data; + }); + }) .then(function (data) { if (data.error) { if (codeContentEl) codeContentEl.textContent = "Error: " + data.error; @@ -742,9 +745,11 @@ if (isDetailPage) { // Mark as fetched so we don't hit the API again on the next open codeFetched = true; }) - .catch(function () { + .catch(function (err) { if (codeContentEl) { - codeContentEl.textContent = "Could not load starter code. Try downloading it instead."; + codeContentEl.textContent = err && err.message + ? "Error: " + err.message + : "Could not load starter code. Try downloading it instead."; } }); } diff --git a/static/style.css b/static/style.css index b399ee5..0d3057a 100644 --- a/static/style.css +++ b/static/style.css @@ -2374,12 +2374,13 @@ select:focus { text-decoration: none; } -/* ---- Code Panel (slide-up) -------------------------------- */ +/* ---- Code Panel (centered popup modal) -------------------- */ .code-panel-overlay { display: none; position: fixed; inset: 0; - background: rgba(10, 15, 80, 0.6); + background: rgba(10, 15, 80, 0.65); + backdrop-filter: blur(4px); z-index: 300; } @@ -2389,22 +2390,27 @@ select:focus { .code-panel { position: fixed; - bottom: 0; - left: 0; - right: 0; - height: 72vh; + top: 50%; + left: 50%; + width: min(920px, calc(100vw - 32px)); + height: min(80vh, 720px); background: #0d1117; - border-radius: var(--r-lg) var(--r-lg) 0 0; - box-shadow: 0 -8px 48px rgba(0, 0, 0, 0.45); + border: 1px solid rgba(255, 255, 255, 0.08); + border-radius: var(--r-lg); + box-shadow: 0 24px 80px rgba(0, 0, 0, 0.55); z-index: 301; display: flex; flex-direction: column; - transform: translateY(100%); - transition: transform 0.3s ease; + transform: translate(-50%, -50%) scale(0.94); + opacity: 0; + pointer-events: none; + transition: transform 0.25s ease, opacity 0.25s ease; } .code-panel.active { - transform: translateY(0); + transform: translate(-50%, -50%) scale(1); + opacity: 1; + pointer-events: auto; } .code-panel-header { @@ -2680,7 +2686,8 @@ select:focus { } .code-panel { - height: 82vh; + width: calc(100vw - 24px); + height: min(88vh, 720px); } .footer-inner { diff --git a/templates/project.html b/templates/project.html index 2cc3337..7970993 100644 --- a/templates/project.html +++ b/templates/project.html @@ -76,7 +76,7 @@