From abc2fad8a20c427fe0390dec7b410920006b894e Mon Sep 17 00:00:00 2001 From: Riyanshi Gupta Date: Wed, 24 Jun 2026 01:38:59 +0530 Subject: [PATCH 1/3] feat: Add Recently Viewed Projects tracking (Issue #1088) --- src/static/recently-viewed.js | 213 ++++++++++++++++++++++++++++++++++ src/static/script.js | 40 ++++++- src/templates/index.html | 35 +++++- 3 files changed, 281 insertions(+), 7 deletions(-) create mode 100644 src/static/recently-viewed.js diff --git a/src/static/recently-viewed.js b/src/static/recently-viewed.js new file mode 100644 index 00000000..68fda858 --- /dev/null +++ b/src/static/recently-viewed.js @@ -0,0 +1,213 @@ +/** + * static/recently-viewed.js + * Recently Viewed Projects tracking utility for DevPath. + * + * Responsibilities + * ---------------- + * 1. Track project views - record when users click on or visit projects + * 2. Persist data - store in localStorage across browser sessions + * 3. Deduplication - if same project viewed again, move to top, don't duplicate + * 4. Render panel - display up to 5 most recent projects + * 5. Clear history - remove all tracked projects + * + * Public API + * ---------- + * RecentlyViewed.trackView(project) – track a project view + * RecentlyViewed.getRecentlyViewed() – get array of all tracked projects + * RecentlyViewed.isTracked(id) – boolean check if project is tracked + * RecentlyViewed.clearHistory() – remove all tracked projects + * RecentlyViewed.renderPanel() – redraw the recently viewed panel + */ + +var RecentlyViewed = (function () { + "use strict"; + + /* ------------------------------------------------------------------ */ + /* Constants */ + /* ------------------------------------------------------------------ */ + var STORAGE_KEY = "devpathRecentlyViewed"; + var MAX_ITEMS = 10; // Store max 10 items in localStorage + var DISPLAY_ITEMS = 5; // Display max 5 in UI + var PANEL_ID = "recently-viewed-panel"; + var LIST_ID = "recently-viewed-list"; + + /* ------------------------------------------------------------------ */ + /* Safe localStorage helpers */ + /* ------------------------------------------------------------------ */ + function lsGet(key) { + try { + return JSON.parse(localStorage.getItem(key) || "null"); + } catch (err) { + console.warn("[RecentlyViewed] localStorage read error:", err); + return null; + } + } + + function lsSet(key, value) { + try { + localStorage.setItem(key, JSON.stringify(value)); + return true; + } catch (err) { + console.warn("[RecentlyViewed] localStorage write error:", err); + return false; + } + } + + /* ------------------------------------------------------------------ */ + /* Time formatting utilities */ + /* ------------------------------------------------------------------ */ + function formatTimeAgo(timestamp) { + if (!timestamp) return "Recently viewed"; + + var now = Date.now(); + var diff = now - timestamp; + var seconds = Math.floor(diff / 1000); + var minutes = Math.floor(seconds / 60); + var hours = Math.floor(minutes / 60); + var days = Math.floor(hours / 24); + + if (seconds < 60) return "Just now"; + if (minutes < 60) return minutes + " min ago"; + if (hours < 24) return hours + " hour" + (hours > 1 ? "s" : "") + " ago"; + if (days === 1) return "Yesterday"; + if (days < 7) return days + " days ago"; + if (days < 30) return Math.floor(days / 7) + " weeks ago"; + + // Format as date for older items + var date = new Date(timestamp); + return date.toLocaleDateString("en-US", { month: "short", day: "numeric" }); + } + + /* ------------------------------------------------------------------ */ + /* Recently Viewed Data Management */ + /* ------------------------------------------------------------------ */ + function getRecentlyViewed() { + var data = lsGet(STORAGE_KEY); + return Array.isArray(data) ? data : []; + } + + function isTracked(projectId) { + return getRecentlyViewed().some(function (p) { + return String(p.id) === String(projectId); + }); + } + + function trackView(project) { + if (!project || !project.id) return false; + + var projectId = project.id; + var viewed = getRecentlyViewed(); + + // Check if already tracked - if so, remove old entry and re-add at top + viewed = viewed.filter(function (p) { + return String(p.id) !== String(projectId); + }); + + // Add to top with current timestamp + var newEntry = { + id: projectId, + title: project.title || "Project " + projectId, + interest: project.interest || "General", + timestamp: Date.now() + }; + + viewed.unshift(newEntry); + + // Keep only the last MAX_ITEMS + if (viewed.length > MAX_ITEMS) { + viewed = viewed.slice(0, MAX_ITEMS); + } + + lsSet(STORAGE_KEY, viewed); + renderPanel(); + return true; + } + + function clearHistory() { + lsSet(STORAGE_KEY, []); + renderPanel(); + return true; + } + + /* ------------------------------------------------------------------ */ + /* Rendering */ + /* ------------------------------------------------------------------ */ + function renderPanel() { + var panel = document.getElementById(PANEL_ID); + if (!panel) return; + + var viewed = getRecentlyViewed(); + var list = document.getElementById(LIST_ID); + + if (!list) return; + + // Clear the list + list.innerHTML = ""; + + // If no recently viewed projects, hide the panel or show empty state + if (viewed.length === 0) { + panel.style.display = "none"; + return; + } + + panel.style.display = "block"; + + // Show only the first DISPLAY_ITEMS + var displayItems = viewed.slice(0, DISPLAY_ITEMS); + + displayItems.forEach(function (project) { + var card = buildProjectCard(project); + list.appendChild(card); + }); + } + + function buildProjectCard(project) { + var card = document.createElement("div"); + card.className = "recently-viewed-card"; + card.setAttribute("data-project-id", project.id); + + var link = document.createElement("a"); + link.href = "/project/" + project.id; + link.className = "recently-viewed-link"; + + var title = document.createElement("div"); + title.className = "recently-viewed-title"; + title.textContent = project.title; + + var meta = document.createElement("div"); + meta.className = "recently-viewed-meta"; + + var interest = document.createElement("span"); + interest.className = "recently-viewed-interest"; + interest.textContent = project.interest; + + var time = document.createElement("span"); + time.className = "recently-viewed-time"; + time.textContent = formatTimeAgo(project.timestamp); + + meta.appendChild(interest); + meta.appendChild(time); + + link.appendChild(title); + link.appendChild(meta); + + card.appendChild(link); + return card; + } + + /* ------------------------------------------------------------------ */ + /* Public API */ + /* ------------------------------------------------------------------ */ + return { + trackView: trackView, + getRecentlyViewed: getRecentlyViewed, + isTracked: isTracked, + clearHistory: clearHistory, + renderPanel: renderPanel + }; +})(); + +// Initialize on DOM ready +document.addEventListener("DOMContentLoaded", function () { + RecentlyViewed.renderPanel(); +}); \ No newline at end of file diff --git a/src/static/script.js b/src/static/script.js index 51fddecb..5c81481f 100644 --- a/src/static/script.js +++ b/src/static/script.js @@ -781,16 +781,35 @@ async function updatePortfolioAnalysis() { if (isSaved) saveBtn.classList.add("saved"); saveBtn.setAttribute("aria-pressed", isSaved ? "true" : "false"); DevPathBookmarks.setButtonContent(saveBtn, isSaved); - saveBtn.addEventListener("click", function () { - DevPathBookmarks.toggle(project, saveBtn); + // Keep your bookmarking code + // Save button logic + saveBtn.addEventListener("click", function(e) { + e.preventDefault(); + var projectObj = { + id: project.id, + title: project.title, + level: project.level, + time: project.time, + skills: project.skills || [] + }; + DevPathBookmarks.toggle(projectObj, saveBtn); }); + + // Also keep recently-viewed tracking code footer.appendChild(saveBtn); + footer.appendChild(link); } var link = document.createElement("a"); link.className = "btn-details"; link.textContent = "View Full Project"; link.href = "/project/" + project.id; + + link.addEventListener("click", function() { + if (typeof RecentlyViewed !== "undefined") { + RecentlyViewed.trackView(project); + } + }); footer.appendChild(link); card.appendChild(title); @@ -923,9 +942,11 @@ async function updatePortfolioAnalysis() { showMoreBtn.style.display = "none"; allProjects = []; visibleProjectCount = PROJECTS_PER_LOAD; + if (typeof RecentlyViewed !== "undefined") { + RecentlyViewed.clearHistory(); + } if (skillsInput) skillsInput.focus(); - } - + } var clearBtn = document.getElementById("clear-filters-btn"); if (clearBtn) { clearBtn.addEventListener("click", resetFormAndState); @@ -1099,6 +1120,17 @@ async function updatePortfolioAnalysis() { (function initDetailPage() { if (typeof PROJECT_ID === "undefined") return; recordProjectView(); + +if (typeof RecentlyViewed !== "undefined") { + var projectTitle = typeof PROJECT_TITLE !== "undefined" ? PROJECT_TITLE : "Project " + PROJECT_ID; + var projectInterest = document.querySelector("[data-project-interest]"); + var interest = projectInterest ? projectInterest.getAttribute("data-project-interest") : "General"; + RecentlyViewed.trackView({ + id: PROJECT_ID, + title: projectTitle, + interest: interest + }); + } var codePanel = document.getElementById("code-panel"); var codePanelOverlay = document.getElementById("code-panel-overlay"); diff --git a/src/templates/index.html b/src/templates/index.html index a3bdbe30..d103ac48 100644 --- a/src/templates/index.html +++ b/src/templates/index.html @@ -58,7 +58,10 @@ {% include 'partials/theme_head.html' %} + + + @@ -1167,7 +1170,6 @@

No Projects Found

-
-
@@ -1366,7 +1367,35 @@ {% include 'partials/scroll_top_btn.html' %} - + + + + + + From 9d0d671bea6cdc37e97518b6e5ecc62eb8378a6e Mon Sep 17 00:00:00 2001 From: Riyanshi Gupta Date: Wed, 24 Jun 2026 01:53:57 +0530 Subject: [PATCH 2/3] feat: Add Recently Viewed Projects tracking (Issue #1088) --- src/static/recently_viewed.css | 210 +++++++++++++++++++++++++++++++++ 1 file changed, 210 insertions(+) create mode 100644 src/static/recently_viewed.css diff --git a/src/static/recently_viewed.css b/src/static/recently_viewed.css new file mode 100644 index 00000000..22f585b2 --- /dev/null +++ b/src/static/recently_viewed.css @@ -0,0 +1,210 @@ +/** + * static/recently-viewed.css + * Styling for the Recently Viewed Projects panel. + * Matches the design of the saved projects panel for consistency. + */ + +/* Recently Viewed Panel Container */ +.recently-viewed-panel { + margin-bottom: 2rem; + padding: 1.5rem; + background: var(--card-bg, #ffffff); + border: 1px solid var(--border, #e5e7eb); + border-radius: 1rem; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05); +} + +.recently-viewed-panel[style*="display: none"] { + display: none !important; +} + +/* Header with title and count */ +.recently-viewed-header { + display: flex; + justify-content: space-between; + align-items: flex-start; + margin-bottom: 1.5rem; + padding-bottom: 1rem; + border-bottom: 1px solid var(--border, #e5e7eb); +} + +.recently-viewed-header > div { + flex: 1; +} + +.recently-viewed-header h3 { + font-size: 1.125rem; + font-weight: 600; + margin: 0 0 0.25rem 0; + color: var(--text-heading, #1f2937); +} + +.recently-viewed-header p { + font-size: 0.875rem; + color: var(--text-muted, #6b7280); + margin: 0; +} + +.recently-viewed-count { + font-size: 0.875rem; + font-weight: 500; + color: var(--text-muted, #6b7280); + white-space: nowrap; +} + +/* List of recently viewed cards */ +.recently-viewed-list { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); + gap: 1rem; + margin: 0; + padding: 0; + list-style: none; +} + +/* Individual recently viewed card */ +.recently-viewed-card { + position: relative; + display: flex; + flex-direction: column; +} + +.recently-viewed-link { + display: flex; + flex-direction: column; + padding: 1rem; + background: var(--gray-50, #f9fafb); + border: 1px solid var(--border, #e5e7eb); + border-radius: 0.75rem; + text-decoration: none; + color: inherit; + transition: all 0.2s ease; + cursor: pointer; +} + +.recently-viewed-link:hover { + background: var(--gray-100, #f3f4f6); + border-color: var(--accent, #6366f1); + box-shadow: 0 4px 12px rgba(99, 102, 241, 0.1); + transform: translateY(-2px); +} + +/* Title of recently viewed project */ +.recently-viewed-title { + font-size: 0.95rem; + font-weight: 600; + color: var(--text-heading, #1f2937); + margin: 0 0 0.75rem 0; + word-break: break-word; + line-height: 1.4; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; +} + +/* Meta information (interest + time) */ +.recently-viewed-meta { + display: flex; + justify-content: space-between; + align-items: center; + gap: 0.5rem; + flex-wrap: wrap; +} + +.recently-viewed-interest { + font-size: 0.75rem; + font-weight: 600; + color: #6366f1; + background: rgba(99, 102, 241, 0.1); + padding: 0.25rem 0.5rem; + border-radius: 0.375rem; + text-transform: capitalize; + white-space: nowrap; +} + +.recently-viewed-time { + font-size: 0.75rem; + color: var(--text-muted, #6b7280); + white-space: nowrap; +} + +/* Dark mode support */ +[data-theme="dark"] .recently-viewed-panel { + background: var(--card-bg-dark, #1f2937); + border-color: var(--border-dark, #374151); +} + +[data-theme="dark"] .recently-viewed-link { + background: var(--gray-700, #374151); + border-color: var(--border-dark, #4b5563); +} + +[data-theme="dark"] .recently-viewed-link:hover { + background: var(--gray-600, #4b5563); + border-color: var(--accent, #818cf8); + box-shadow: 0 4px 12px rgba(129, 140, 248, 0.2); +} + +[data-theme="dark"] .recently-viewed-title { + color: var(--text-light, #f3f4f6); +} + +[data-theme="dark"] .recently-viewed-meta { + color: var(--text-muted-dark, #9ca3af); +} + +[data-theme="dark"] .recently-viewed-interest { + background: rgba(129, 140, 248, 0.15); + color: #a5b4fc; +} + +[data-theme="dark"] .recently-viewed-time { + color: var(--text-muted-dark, #9ca3af); +} + +/* Mobile responsiveness */ +@media (max-width: 768px) { + .recently-viewed-list { + grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); + gap: 0.75rem; + } + + .recently-viewed-header { + flex-direction: column; + gap: 0.5rem; + } + + .recently-viewed-count { + align-self: flex-start; + } + + .recently-viewed-link { + padding: 0.75rem; + } + + .recently-viewed-title { + font-size: 0.875rem; + margin-bottom: 0.5rem; + } + + .recently-viewed-meta { + font-size: 0.7rem; + } +} + +@media (max-width: 480px) { + .recently-viewed-list { + grid-template-columns: 1fr; + } + + .recently-viewed-link { + padding: 1rem; + } + + .recently-viewed-meta { + flex-direction: column; + align-items: flex-start; + gap: 0.25rem; + } +} \ No newline at end of file From 7fa920f6bcf60f1ee1de8a73c8e44ceb81949e02 Mon Sep 17 00:00:00 2001 From: Riyanshi Gupta Date: Fri, 26 Jun 2026 01:01:10 +0530 Subject: [PATCH 3/3] Enhance bookmarks.css styling for Saved Projects panel (#1087) Signed-off-by: Riyanshi Gupta --- src/static/bookmarks.css | 109 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 108 insertions(+), 1 deletion(-) diff --git a/src/static/bookmarks.css b/src/static/bookmarks.css index 1147a4d4..68699b27 100644 --- a/src/static/bookmarks.css +++ b/src/static/bookmarks.css @@ -31,6 +31,113 @@ ------------------------------------------------------------------ */ .saved-projects-panel { display: none; + margin-top: 3rem; + padding: 2rem; + background: var(--card-bg, #ffffff); + border: 1px solid var(--border, #e5e7eb); + border-radius: 1rem; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05); +} + +.saved-projects-header { + display: flex; + justify-content: space-between; + align-items: flex-start; + margin-bottom: 1.5rem; + padding-bottom: 1rem; + border-bottom: 1px solid var(--border, #e5e7eb); +} + +.saved-projects-header h3 { + font-size: 1.125rem; + font-weight: 600; + margin: 0; + color: var(--text-heading, #1f2937); +} + +.saved-projects-header p { + font-size: 0.875rem; + color: var(--text-muted, #6b7280); + margin: 0.25rem 0 0 0; +} + +.saved-projects-count { + font-size: 0.8rem; + font-weight: 600; + color: var(--text-muted, #6b7280); + background: var(--gray-100, #f3f4f6); + padding: 0.375rem 0.875rem; + border-radius: 2rem; + white-space: nowrap; +} + +.saved-projects-list { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); + gap: 1rem; +} + +.saved-project-item { + display: flex; + flex-direction: column; + gap: 0.75rem; + padding: 1rem; + background: var(--gray-50, #f9fafb); + border: 1px solid var(--border, #e5e7eb); + border-radius: 0.75rem; + transition: all 0.2s ease; +} + +.saved-project-item:hover { + background: var(--gray-100, #f3f4f6); + border-color: var(--accent, #6366f1); + box-shadow: 0 4px 12px rgba(99, 102, 241, 0.1); + transform: translateY(-2px); +} + +.saved-project-item a { + font-weight: 600; + color: var(--accent, #6366f1); + text-decoration: none; + transition: color 0.2s; + word-break: break-word; + overflow-wrap: break-word; +} + +.saved-project-item a:hover { + color: var(--accent-dark, #4f46e5); + text-decoration: underline; +} + +.saved-project-item span { + font-size: 0.8rem; + color: var(--text-muted, #6b7280); +} + +.saved-project-remove { + align-self: flex-start; + background: none; + border: none; + color: var(--text-muted, #6b7280); + font-size: 0.75rem; + cursor: pointer; + padding: 0.25rem 0.5rem; + border-radius: 0.375rem; + transition: all 0.2s; + font-weight: 500; +} + +.saved-project-remove:hover { + background: #fee2e2; + color: #dc2626; +} + +.saved-projects-empty { + grid-column: 1 / -1; + text-align: center; + padding: 2rem; + color: var(--text-muted, #6b7280); + font-style: italic; } /* ------------------------------------------------------------------ @@ -50,4 +157,4 @@ border-radius: var(--r-xs); font-size: 0.75rem; color: var(--gray-600); -} +} \ No newline at end of file