Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
171 changes: 98 additions & 73 deletions static/script.js
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@
html.dataset.theme = theme;

// 2. Persist the user's choice across sessions
try { localStorage.setItem("devpath-theme", theme); } catch (e) { /* private browsing may block */ }
try { localStorage.setItem("theme", theme); } catch (e) { /* private browsing may block */ }

// 3. Update every toggle button's accessible state
document.querySelectorAll(".theme-toggle").forEach(function (btn) {
Expand Down Expand Up @@ -782,6 +782,18 @@ if (isIndexPage) {
// Render result cards
// ----------------------------------------------------------

main
function truncate(text, maxLength) {
if (!text) return "";
return text.length > maxLength ? text.slice(0, maxLength) + "..." : text;
}

function createTag(text, type) {
var span = document.createElement("span");
span.className = "project-tag project-tag--" + type;
span.textContent = text;
return span;

//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) {
Expand Down Expand Up @@ -816,95 +828,94 @@ if (isIndexPage) {
});

resultsSection.scrollIntoView({ behavior: "smooth" });
main
}

// builds one project card as a DOM element and returns it
// the card has title, short description, tags and link
function buildProjectCard(project) {
var card = document.createElement("div");
card.className = "project-card";

// Title
var title = document.createElement("h3");
title.className = "project-card-title";
title.textContent = project.title;

// Description wrapper — keeps text and button as separate child elements
// so we never use textContent (which would wipe out child nodes like the button)
var desc = document.createElement("p");
desc.className = "project-card-desc";

// Separate span for the description text so we can update it
// without touching the toggle button
var descText = document.createElement("span");
descText.className = "project-card-desc-text";

var shortText = truncate(project.description, 120);
var fullText = project.description;
var fullText = project.description;
var isExpanded = false;

descText.textContent = shortText;
desc.appendChild(descText);

// Only add Read More button if description is actually truncated
if (fullText.length > 120) {
if (fullText && fullText.length > 120) {
var readMoreBtn = document.createElement("button");
readMoreBtn.className = "read-more-btn";
readMoreBtn.textContent = "Read more";
// aria-expanded tells screen readers whether the content is expanded or not
readMoreBtn.setAttribute("aria-expanded", "false");

readMoreBtn.addEventListener("click", function () {
isExpanded = !isExpanded;
// Update only the text span — button stays in the DOM untouched
descText.textContent = isExpanded ? fullText : shortText;
readMoreBtn.textContent = isExpanded ? "Read less" : "Read more";
readMoreBtn.setAttribute("aria-expanded", isExpanded ? "true" : "false");
});

desc.appendChild(readMoreBtn);
}

// Tags row
var tagsRow = document.createElement("div");
tagsRow.className = "project-card-tags";

(project.skills || []).forEach(function (skill) {
tagsRow.appendChild(createTag(skill, "skill"));
});

var levelClass = "level " + (project.level || "").toLowerCase();
tagsRow.appendChild(createTag(project.level, levelClass));
tagsRow.appendChild(createTag(project.level, (project.level || "").toLowerCase()));
tagsRow.appendChild(createTag("Time: " + project.time, "time"));

// Assemble the card in order
card.appendChild(title);
card.appendChild(desc);
card.appendChild(tagsRow);
card.appendChild(footer);

var footer = document.createElement("div");
footer.className = "project-card-footer";
var link = document.createElement("a");
link.className = "btn-details";
link.textContent = "View Full Project";
link.href = "/project/" + project.id;
footer.appendChild(link);

// helper to create a coloured tag element (used for skills, level, time tags on the cards)
function createTag(text, type) {
var span = document.createElement("span");
// The type becomes a BEM modifier so CSS can style each tag differently
span.className = "project-tag project-tag--" + type;
span.textContent = text;
return span;
}
card.appendChild(title);
card.appendChild(desc);
card.appendChild(tagsRow);
card.appendChild(footer);
return card;
}

function truncate(text, maxLength) {
// Safety check — just return empty string if text is missing
if (!text) return "";
// Only add "..." if the text is actually longer than the limit
return text.length > maxLength ? text.slice(0, maxLength) + "..." : text;
//takes the array of projects from the api and draws them on the page as cards
function renderResults(projects, message) {
resultsSection.style.display = "block";
resultsLoadingEl.style.display = "none";
resultsGrid.innerHTML = "";

if (!projects || projects.length === 0) {
resultsGrid.style.display = "none";
resultsEmptyEl.style.display = "block";
var selectedInterest = document.getElementById("interest") && 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.";
} else if (message) {
emptyMessageEl.textContent = message;
} else {
emptyMessageEl.textContent = "Try adjusting your skills or choosing a different interest area.";
}
resultsSection.scrollIntoView({ behavior: "smooth" });
return;
}

} // end isIndexPage
resultsEmptyEl.style.display = "none";
resultsGrid.style.display = "grid";
projects.forEach(function (project) {
resultsGrid.appendChild(buildProjectCard(project));
});
resultsSection.scrollIntoView({ behavior: "smooth" });
}

} // end isIndexPage


// ============================================================
Expand Down Expand Up @@ -1385,46 +1396,60 @@ updateRoadmapProgress();
});
} // end github modal handlers

/* ---- Scroll-to-top button ---- */

/* Show the button only when the user has scrolled more than 300px */
var SCROLL_THRESHOLD = 300;
// ============================================================
// SCROLL NAVIGATION BUTTON (runs on all pages)
// ============================================================
(function () {
var SCROLL_THRESHOLD = 200;
var scrollTopBtn = document.getElementById('scroll-top-btn');
var scrollBtnIcon = document.getElementById('scroll-btn-icon');
var atBottom = false;

/* Get the button element; guard against pages that do not have it */
var scrollTopBtn = document.getElementById('scroll-top-btn');
var ARROW_UP = '<polyline points="18 15 12 9 6 15"/>';
var ARROW_DOWN = '<polyline points="6 9 12 15 18 9"/>';

/* Add or remove the .visible class based on scroll position */
function handleScroll() {
if (!scrollTopBtn) return;
if (window.pageYOffset > SCROLL_THRESHOLD) {
scrollTopBtn.classList.add('visible');
} else {
scrollTopBtn.classList.remove('visible');
}
}
function isNearBottom() {
return (window.innerHeight + window.pageYOffset) >= document.body.scrollHeight - 40;
}

/* Smooth-scroll to the very top of the page */
function scrollToTop() {
window.scrollTo({ top: 0, behavior: 'smooth' });
function handleScroll() {
if (!scrollTopBtn) return;
if (window.pageYOffset > SCROLL_THRESHOLD) {
scrollTopBtn.classList.add('visible');
} else {
scrollTopBtn.classList.remove('visible');
}
if (isNearBottom()) {
atBottom = true;
scrollTopBtn.setAttribute('aria-label', 'Scroll to top');
scrollTopBtn.title = 'Scroll to top';
if (scrollBtnIcon) scrollBtnIcon.innerHTML = ARROW_UP;
} else {
atBottom = false;
scrollTopBtn.setAttribute('aria-label', 'Scroll to bottom');
scrollTopBtn.title = 'Scroll to bottom';
if (scrollBtnIcon) scrollBtnIcon.innerHTML = ARROW_DOWN;
}

/* Add or remove the .visible class based on scroll position */
function handleScroll() {
if (!scrollTopBtn) return;
if (window.pageYOffset > SCROLL_THRESHOLD) {
scrollTopBtn.classList.add('visible');
} else {
scrollTopBtn.classList.remove('visible');
}
}

/* Smooth-scroll to the very top of the page */
function scrollToTop() {
window.scrollTo({ top: 0, behavior: 'smooth' });
}
main
if (scrollTopBtn) {
window.addEventListener('scroll', handleScroll, { passive: true });
scrollTopBtn.addEventListener('click', function () {
if (atBottom) {
window.scrollTo({ top: 0, behavior: 'smooth' });
} else {
window.scrollTo({ top: document.body.scrollHeight, behavior: 'smooth' });
}
});
handleScroll();
}
}());

/* Only wire up listeners if the button exists on this page */
if (scrollTopBtn) {
window.addEventListener('scroll', handleScroll);
scrollTopBtn.addEventListener('click', scrollToTop);
}
main
16 changes: 10 additions & 6 deletions static/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -3107,7 +3107,7 @@ input[type="text"]:not(.skill-input-wrap input):focus {
}

/* =============================================================
SECTION 22: SCROLL-TO-TOP BUTTON
SECTION 22: SCROLL NAVIGATION BUTTON
============================================================= */
#scroll-top-btn {
position: fixed;
Expand All @@ -3122,7 +3122,6 @@ input[type="text"]:not(.skill-input-wrap input):focus {
border-radius: 50%;
background-color: var(--indigo-700);
color: #ffffff;
font-size: 1.25rem;
cursor: pointer;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
transition: opacity 0.3s ease, transform 0.2s ease;
Expand All @@ -3143,6 +3142,15 @@ input[type="text"]:not(.skill-input-wrap input):focus {
display: flex;
}

@media (max-width: 640px) {
#scroll-top-btn {
bottom: 1.25rem;
right: 1.25rem;
width: 2.4rem;
height: 2.4rem;
}
}

/* =============================================================
SECTION 23: RESPONSIVE BREAKPOINTS
============================================================= */
Expand Down Expand Up @@ -3474,10 +3482,6 @@ html[data-theme="dark"] .btn-view-code-sm {
border-color: rgba(99, 102, 241, 0.4);
}

#scroll-top-btn.visible {
display: flex;
}

/* =============================================================
Dark Mode
============================================================= */
Expand Down
38 changes: 36 additions & 2 deletions templates/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,35 @@
<a href="#features" class="nav-link">Features</a>
<a href="#find-project" class="nav-link">Find Project</a>
<a href="https://github.com/komalharshita/DevPath" target="_blank" rel="noopener noreferrer" class="nav-btn-outline">GitHub</a>
main

<button class="theme-toggle" id="theme-toggle-desktop"
aria-pressed="false"
aria-label="Switch to dark mode">
<!-- Moon icon: shown in light mode (clicking switches to dark) -->
<svg class="icon-moon" width="16" height="16" viewBox="0 0 24 24"
fill="none" stroke="currentColor" stroke-width="2"
stroke-linecap="round" stroke-linejoin="round"
aria-hidden="true">
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/>
</svg>
<!-- Sun icon: shown in dark mode (clicking switches to light) -->
<svg class="icon-sun" width="16" height="16" viewBox="0 0 24 24"
fill="none" stroke="currentColor" stroke-width="2"
stroke-linecap="round" stroke-linejoin="round"
aria-hidden="true">
<circle cx="12" cy="12" r="5"/>
<line x1="12" y1="1" x2="12" y2="3"/>
<line x1="12" y1="21" x2="12" y2="23"/>
<line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/>
<line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/>
<line x1="1" y1="12" x2="3" y2="12"/>
<line x1="21" y1="12" x2="23" y2="12"/>
<line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/>
<line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/>
</svg>
</button>

<button
id="theme-toggle"
class="theme-toggle"
Expand All @@ -37,8 +66,8 @@
>
🌙
</button>
main
</div>
{% include 'partials/theme_toggle.html' %}
<!-- Mobile hamburger toggle -->
<button class="nav-mobile-toggle" id="nav-mobile-toggle"
aria-label="Toggle navigation"
Expand Down Expand Up @@ -744,7 +773,12 @@ <h4 class="footer-col-title">About Us</h4>
}
</style>

<button id="scroll-top-btn" aria-label="Back to top" title="Back to top">↑</button>
<button id="scroll-top-btn" aria-label="Scroll to top" title="Scroll to top">
<svg id="scroll-btn-icon" width="18" height="18" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<polyline points="18 15 12 9 6 15"/>
</svg>
</button>

<script src="/static/data/skills.js"></script>
<script src="/static/script.js"></script>
Expand Down
Loading