@@ -7,42 +7,110 @@ import { fetchCached } from './cache.js';
77import { formatDateShort } from './utils.js' ;
88import { 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+
1066async 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