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/static/style.css b/src/static/style.css index 93450c00..2160c09d 100644 --- a/src/static/style.css +++ b/src/static/style.css @@ -5380,3 +5380,128 @@ body.dark-theme .theme-toggle:hover { background: rgba(124, 58, 237, 0.15); color: #c4b5fd; } +/* Extra small screens (360px - budget Android devices) */ +@media (max-width: 380px) { + .project-card { + padding: 20px; + gap: 12px; + } + + .results-grid { + grid-template-columns: 1fr; + gap: 16px; + } + + .project-card-tags { + gap: 6px; + margin-bottom: 8px; + } + + .project-tag { + font-size: 0.7rem; + padding: 3px 9px; + white-space: nowrap; + } + + .btn-save-project, + .btn-details { + padding: 10px 14px; + font-size: 0.82rem; + width: 100%; + } + + .project-card-footer { + 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; + } +} + +@media (min-width: 381px) and (max-width: 640px) { + .project-card { + padding: 24px; + gap: 14px; + } + + .results-grid { + grid-template-columns: 1fr; + 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; + } +} + +@media (min-width: 641px) { + .project-card { + padding: 30px 28px; + gap: 16px; + } + + .results-grid { + grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); + gap: 26px; + } + + .project-tag { + font-size: 0.74rem; + padding: 3px 11px; + white-space: normal; + } + + .btn-save-project, + .btn-details { + padding: 11px 18px; + font-size: 0.88rem; + } + + .project-card-footer { + gap: 10px; + padding-top: 8px; + } + + .project-card-title { + font-size: 1.08rem; + } + + .project-card-desc { + font-size: 0.88rem; + line-height: 1.7; + } + + .container { + padding: 0 32px; + } +} \ No newline at end of file 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 @@ +