Skip to content

Commit db381c4

Browse files
actions-userclaude
andcommitted
Add smart sorting to homepage based on Supabase access stats
Slots 1-2 show items with activity in the last 2 weeks (by recency), slots 3-5 show all-time most accessed. Falls back to all-time sort if no recent activity exists. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 3cd9a53 commit db381c4

1 file changed

Lines changed: 81 additions & 14 deletions

File tree

‎js/home.js‎

Lines changed: 81 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -7,42 +7,110 @@ import { fetchCached } from './cache.js';
77
import { formatDateShort } from './utils.js';
88
import { trackEvent } from './analytics.js';
99

10+
const SUPABASE_URL = 'https://bfqcfhvpauvvakgmeuhr.supabase.co';
11+
const SUPABASE_KEY = 'sb_publishable_yH1CSzF3YQ8FpU-hF-QEtg_ZWaKDLsR';
12+
const HEADERS = {
13+
'apikey': SUPABASE_KEY,
14+
'Authorization': `Bearer ${SUPABASE_KEY}`
15+
};
16+
17+
async function fetchStats(view) {
18+
try {
19+
const res = await fetch(`${SUPABASE_URL}/rest/v1/${view}?select=*`, { headers: HEADERS });
20+
if (!res.ok) return [];
21+
return res.json();
22+
} catch { return []; }
23+
}
24+
25+
async function fetchRecentEvents(eventType) {
26+
try {
27+
const twoWeeksAgo = new Date(Date.now() - 14 * 24 * 60 * 60 * 1000).toISOString();
28+
const url = `${SUPABASE_URL}/rest/v1/events?select=label&event_type=eq.${eventType}&created_at=gte.${twoWeeksAgo}`;
29+
const res = await fetch(url, { headers: HEADERS });
30+
if (!res.ok) return {};
31+
const rows = await res.json();
32+
// Count occurrences per label
33+
return rows.reduce((acc, r) => {
34+
acc[r.label] = (acc[r.label] || 0) + 1;
35+
return acc;
36+
}, {});
37+
} catch { return {}; }
38+
}
39+
40+
// Smart sort: slots 1-2 = recent (last 2 weeks), slots 3-5 = all-time
41+
// If no recent data, all 5 = all-time
42+
function smartSort(items, recentCounts, allTimeCounts, labelKey) {
43+
const recentItems = items
44+
.filter(item => recentCounts[item[labelKey]] > 0)
45+
.sort((a, b) => (recentCounts[b[labelKey]] || 0) - (recentCounts[a[labelKey]] || 0));
46+
47+
if (!recentItems.length) {
48+
// No recent data — sort all by all-time count
49+
return items
50+
.slice()
51+
.sort((a, b) => (allTimeCounts[b[labelKey]] || 0) - (allTimeCounts[a[labelKey]] || 0))
52+
.slice(0, 5);
53+
}
54+
55+
const top2 = recentItems.slice(0, 2);
56+
const top2Labels = new Set(top2.map(i => i[labelKey]));
57+
58+
const top3 = items
59+
.filter(item => !top2Labels.has(item[labelKey]))
60+
.sort((a, b) => (allTimeCounts[b[labelKey]] || 0) - (allTimeCounts[a[labelKey]] || 0))
61+
.slice(0, 3);
62+
63+
return [...top2, ...top3];
64+
}
65+
1066
async function init() {
1167
const config = await initPage('');
1268

1369
const featuredEl = document.getElementById('featured-list');
1470
const blogEl = document.getElementById('blog-list');
1571

16-
// Show skeletons while loading
1772
if (featuredEl) featuredEl.innerHTML = renderSkeletonRows(5);
1873
if (blogEl) blogEl.innerHTML = renderSkeletonRows(3);
1974

2075
try {
21-
const [projects, manifest] = await Promise.all([
76+
const [projects, manifest, projectStats, postStats, recentProjects, recentPosts] = await Promise.all([
2277
fetchCached(config.projects),
23-
fetchCached(config.blog_manifest).catch(() => ({ posts: [] }))
78+
fetchCached(config.blog_manifest).catch(() => ({ posts: [] })),
79+
fetchStats('stats_project_clicks'),
80+
fetchStats('stats_post_reads'),
81+
fetchRecentEvents('project_click'),
82+
fetchRecentEvents('post_read')
2483
]);
2584

26-
renderFeatured(projects, featuredEl);
27-
renderLatestPosts(manifest, blogEl);
85+
// Build all-time lookup maps
86+
const projectAllTime = {};
87+
projectStats.forEach(r => { projectAllTime[r.project] = r.clicks; });
88+
89+
const postAllTime = {};
90+
postStats.forEach(r => { postAllTime[r.post] = r.reads; });
91+
92+
renderFeatured(projects, featuredEl, recentProjects, projectAllTime);
93+
renderLatestPosts(manifest, blogEl, recentPosts, postAllTime);
2894
} catch (err) {
2995
console.error(err);
3096
if (featuredEl) featuredEl.innerHTML = '<p class="state-msg">failed to load projects.</p>';
3197
if (blogEl) blogEl.innerHTML = '<p class="state-msg">failed to load posts.</p>';
3298
}
3399
}
34100

35-
function renderFeatured(projects, container) {
101+
function renderFeatured(projects, container, recentCounts, allTimeCounts) {
36102
if (!container) return;
37103

38-
const featured = projects.filter(p => p.featured).slice(0, 5);
104+
const featured = projects.filter(p => p.featured);
39105

40106
if (!featured.length) {
41107
container.innerHTML = '<p class="state-msg">no featured projects.</p>';
42108
return;
43109
}
44110

45-
container.innerHTML = featured.map((p, i) => `
111+
const sorted = smartSort(featured, recentCounts, allTimeCounts, 'title');
112+
113+
container.innerHTML = sorted.map((p, i) => `
46114
<a class="project-row" href="${p.url}" target="_blank" rel="noopener noreferrer"
47115
title="${p.description}" data-track="${p.title}">
48116
<span class="project-num">${String(i + 1).padStart(2, '0')}.</span>
@@ -58,20 +126,19 @@ function renderFeatured(projects, container) {
58126
});
59127
}
60128

61-
function renderLatestPosts(manifest, container) {
129+
function renderLatestPosts(manifest, container, recentCounts, allTimeCounts) {
62130
if (!container) return;
63131

64-
const posts = (manifest.posts || [])
65-
.slice()
66-
.sort((a, b) => new Date(b.date) - new Date(a.date))
67-
.slice(0, 5);
132+
const posts = manifest.posts || [];
68133

69134
if (!posts.length) {
70135
container.innerHTML = '<p class="state-msg">no posts yet.</p>';
71136
return;
72137
}
73138

74-
container.innerHTML = posts.map((p, i) => {
139+
const sorted = smartSort(posts, recentCounts, allTimeCounts, 'title');
140+
141+
container.innerHTML = sorted.map((p, i) => {
75142
const slug = encodeURIComponent(p.path);
76143
return `
77144
<a class="project-row" href="/writing?post=${slug}" data-track="${p.title}">

0 commit comments

Comments
 (0)