diff --git a/static/script.js b/static/script.js index 9ff8600c..3d898aa8 100644 --- a/static/script.js +++ b/static/script.js @@ -763,27 +763,52 @@ if (clearFiltersBtn) { title.className = "project-card-title"; title.textContent = project.title; - // Description (truncated for visual consistency) + // 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"; - // Cut description to 120 chars so all cards stay the same height - desc.textContent = truncate(project.description, 120); + + // 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 isExpanded = false; + + descText.textContent = shortText; + desc.appendChild(descText); + + // Only add Read More button if description is actually truncated + if (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"; - // Show all project skills as tags so users can see the full match (project.skills || []).forEach(function (skill) { tagsRow.appendChild(createTag(skill, "skill")); }); - // Level tag (colour-coded via CSS class) - // Lowercase so it matches the CSS class names like "level beginner", "level advanced" var levelClass = "level " + (project.level || "").toLowerCase(); tagsRow.appendChild(createTag(project.level, levelClass)); - - // Time tag tagsRow.appendChild(createTag("Time: " + project.time, "time")); // Footer with view-details link @@ -793,7 +818,7 @@ if (clearFiltersBtn) { var link = document.createElement("a"); link.className = "btn-details"; link.textContent = "View Full Project"; - link.href = "/project/" + project.id; //each project has a unique id + link.href = "/project/" + project.id; footer.appendChild(link); @@ -1075,5 +1100,4 @@ function scrollToTop() { if (scrollTopBtn) { window.addEventListener('scroll', handleScroll); scrollTopBtn.addEventListener('click', scrollToTop); -} -} + } diff --git a/static/style.css b/static/style.css index 493de8c5..6ea64e95 100644 --- a/static/style.css +++ b/static/style.css @@ -3313,9 +3313,30 @@ html[data-theme="dark"] .feature-card-icon { background: rgba(255, 255, 255, 0.05); } +.line-content { + flex: 1; + white-space: pre; + color: #e6edf3; +} + +.read-more-btn { + background: none; + border: none; + color: #6366f1; + font-size: 0.85rem; + font-weight: 600; + cursor: pointer; + padding: 0; + margin-left: 4px; + text-decoration: underline; +} + +.read-more-btn:hover { + color: #4f46e5; +} /* Error page large code number */ html[data-theme="dark"] .error-code { color: var(--gray-300); } /* Scrollbar in dark mode (Webkit) */ html[data-theme="dark"] .skills-suggestions::-webkit-scrollbar-track { background: var(--gray-100); } -html[data-theme="dark"] .skills-suggestions::-webkit-scrollbar-thumb { background: var(--indigo-500); } \ No newline at end of file +html[data-theme="dark"] .skills-suggestions::-webkit-scrollbar-thumb { background: var(--indigo-500); }