From 076f6d0f1afb0f58e78371c619c950f4b847ab60 Mon Sep 17 00:00:00 2001 From: Riyanshi Gupta Date: Wed, 24 Jun 2026 01:38:59 +0530 Subject: [PATCH 1/7] feat: Add Recently Viewed Projects tracking (Issue #1088) --- src/static/recently-viewed.js | 213 ++++++++++++++++++++++++++++++++++ src/static/script.js | 23 +++- src/templates/index.html | 13 +++ 3 files changed, 247 insertions(+), 2 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 9aeb0bd8..aace5d0e 100644 --- a/src/static/script.js +++ b/src/static/script.js @@ -665,6 +665,12 @@ updateProfileWidgets(); 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); @@ -774,9 +780,11 @@ updateProfileWidgets(); clearAllErrors(); hideSuggestions(); resultsSection.style.display = "none"; + if (typeof RecentlyViewed !== "undefined") { + RecentlyViewed.clearHistory(); + } if (skillsInput) skillsInput.focus(); - } - + } var clearBtn = document.getElementById("clear-filters-btn"); if (clearBtn) { clearBtn.addEventListener("click", resetFormAndState); @@ -911,6 +919,17 @@ updateProfileWidgets(); (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 9ae9e04f..863217d9 100644 --- a/src/templates/index.html +++ b/src/templates/index.html @@ -58,6 +58,7 @@ {% include 'partials/theme_head.html' %} + @@ -1100,6 +1101,17 @@

No Projects Found

+
+
+
+

Recently Viewed

+

Projects you've checked out recently.

+
+ 0 viewed +
+
+
+
@@ -1270,6 +1282,7 @@ + + + + + \ No newline at end of file From 0e0c2b53ef688872fdf0d5ca2fa023fb29e2870a Mon Sep 17 00:00:00 2001 From: Riyanshi Gupta Date: Sat, 27 Jun 2026 23:34:48 +0530 Subject: [PATCH 6/7] fix: make project cards fully responsive at 360px --- src/static/style.css | 56 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/src/static/style.css b/src/static/style.css index 93450c00..1a189c3c 100644 --- a/src/static/style.css +++ b/src/static/style.css @@ -5380,3 +5380,59 @@ body.dark-theme .theme-toggle:hover { background: rgba(124, 58, 237, 0.15); color: #c4b5fd; } +/* Extra small screens (360px - budget Android devices) */ +/* Extra small screens (360px - budget Android devices) */ +@media (max-width: 380px) { + /* Project cards — reduce padding & margins */ + .project-card { + padding: 20px; /* Reduced from 30px 28px */ + gap: 12px; /* Reduced from 16px */ + } + + /* Results grid — single column */ + .results-grid { + grid-template-columns: 1fr; + gap: 16px; /* Reduced from 26px */ + } + + /* Tags — ensure wrap & visibility */ + .project-card-tags { + gap: 6px; /* Reduced from 7px */ + margin-bottom: 8px; + } + + .project-tag { + font-size: 0.7rem; /* Reduced from 0.74rem */ + padding: 3px 9px; /* Reduced from 3px 11px */ + white-space: nowrap; /* Prevent text wrap */ + } + + /* Action buttons — full width, readable text */ + .btn-save-project, + .btn-details { + padding: 10px 14px; /* Reduced from 11px 18px */ + font-size: 0.82rem; /* Reduced from 0.88rem */ + width: 100%; + } + + .project-card-footer { + gap: 8px; /* Reduced from 10px */ + padding-top: 6px; /* Reduced from 8px */ + } + + /* Card title — smaller on tiny screens */ + .project-card-title { + font-size: 0.95rem; /* Reduced from 1.08rem */ + } + + /* Card description — tighter line height */ + .project-card-desc { + font-size: 0.82rem; /* Reduced from 0.88rem */ + line-height: 1.6; /* Slightly tighter */ + } + + /* Container padding */ + .container { + padding: 0 16px; /* Reduced from 32px */ + } +} \ No newline at end of file From 2df90bf3bec092e2f4da655bb20c7845d2682a06 Mon Sep 17 00:00:00 2001 From: Riyanshi Gupta Date: Tue, 21 Jul 2026 14:55:56 +0530 Subject: [PATCH 7/7] fix: make project cards responsive at 360px with cross-device testing --- src/static/style.css | 123 +++++++++++++++++++++++++++++++++++-------- 1 file changed, 100 insertions(+), 23 deletions(-) diff --git a/src/static/style.css b/src/static/style.css index 1a189c3c..8f95cd3e 100644 --- a/src/static/style.css +++ b/src/static/style.css @@ -5381,58 +5381,135 @@ body.dark-theme .theme-toggle:hover { color: #c4b5fd; } /* Extra small screens (360px - budget Android devices) */ -/* Extra small screens (360px - budget Android devices) */ +/* ============================================================ + RESPONSIVE PROJECT CARDS — Multi-breakpoint approach + Ensures no regressions on tablets and desktops + ============================================================ */ + +/* Base: 320px - 380px (Mobile) */ @media (max-width: 380px) { - /* Project cards — reduce padding & margins */ .project-card { - padding: 20px; /* Reduced from 30px 28px */ - gap: 12px; /* Reduced from 16px */ + padding: 20px; + gap: 12px; } - /* Results grid — single column */ .results-grid { grid-template-columns: 1fr; - gap: 16px; /* Reduced from 26px */ + gap: 16px; } - /* Tags — ensure wrap & visibility */ .project-card-tags { - gap: 6px; /* Reduced from 7px */ + gap: 6px; margin-bottom: 8px; } .project-tag { - font-size: 0.7rem; /* Reduced from 0.74rem */ - padding: 3px 9px; /* Reduced from 3px 11px */ - white-space: nowrap; /* Prevent text wrap */ + font-size: 0.7rem; + padding: 3px 9px; + white-space: nowrap; } - /* Action buttons — full width, readable text */ .btn-save-project, .btn-details { - padding: 10px 14px; /* Reduced from 11px 18px */ - font-size: 0.82rem; /* Reduced from 0.88rem */ + padding: 10px 14px; + font-size: 0.82rem; width: 100%; } .project-card-footer { - gap: 8px; /* Reduced from 10px */ - padding-top: 6px; /* Reduced from 8px */ + gap: 8px; + padding-top: 6px; + } + + .project-card-title { + font-size: 0.95rem; + } + + .project-card-desc { + font-size: 0.82rem; + line-height: 1.6; + } + + .container { + padding: 0 16px; + } +} + +/* 381px - 640px (Small Tablet) — RESTORE & STABILIZE */ +@media (min-width: 381px) and (max-width: 640px) { + .project-card { + padding: 24px; /* Intermediate between 20px and 28px */ + gap: 14px; + } + + .results-grid { + grid-template-columns: 1fr; /* Still single column */ + gap: 20px; + } + + .project-tag { + font-size: 0.72rem; + padding: 3px 10px; + } + + .btn-save-project, + .btn-details { + padding: 10px 16px; + font-size: 0.84rem; + } + + .project-card-title { + font-size: 1rem; + } + + .project-card-desc { + font-size: 0.85rem; + } + + .container { + padding: 0 20px; + } +} + +/* 641px+ (Tablet & Desktop) — RESTORE ORIGINAL STYLES */ +@media (min-width: 641px) { + .project-card { + padding: 30px 28px; /* Original value */ + gap: 16px; /* Original value */ + } + + .results-grid { + grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); + gap: 26px; /* Original value */ + } + + .project-tag { + font-size: 0.74rem; /* Original */ + padding: 3px 11px; /* Original */ + white-space: normal; /* Allow wrap on larger screens */ + } + + .btn-save-project, + .btn-details { + padding: 11px 18px; /* Original */ + font-size: 0.88rem; /* Original */ + } + + .project-card-footer { + gap: 10px; /* Original */ + padding-top: 8px; /* Original */ } - /* Card title — smaller on tiny screens */ .project-card-title { - font-size: 0.95rem; /* Reduced from 1.08rem */ + font-size: 1.08rem; /* Original */ } - /* Card description — tighter line height */ .project-card-desc { - font-size: 0.82rem; /* Reduced from 0.88rem */ - line-height: 1.6; /* Slightly tighter */ + font-size: 0.88rem; /* Original */ + line-height: 1.7; /* Original */ } - /* Container padding */ .container { - padding: 0 16px; /* Reduced from 32px */ + padding: 0 32px; /* Original */ } } \ No newline at end of file