@@ -1128,6 +1128,112 @@ type Atom struct {
11281128 changed bool `toml:"changed" validate:"-"`
11291129}
11301130
1131+ // ContributionDay is a single day in the contribution grid.
1132+ type ContributionDay struct {
1133+ Date string
1134+ Count int
1135+ Level int // 0-4, for CSS intensity classes
1136+ }
1137+
1138+ // ContributionWeek is a column in the contribution grid.
1139+ type ContributionWeek struct {
1140+ Days [7 ]ContributionDay
1141+ }
1142+
1143+ // ContributionGrid holds a year of contribution data for the atom grid.
1144+ type ContributionGrid struct {
1145+ Weeks []ContributionWeek
1146+ MonthLabels []struct {
1147+ Name string
1148+ Offset int // week index where the month starts
1149+ OffsetPct float64
1150+ }
1151+ }
1152+
1153+ // buildContributionGrid builds a GitHub-style contribution grid from atoms
1154+ // covering the last 52 weeks.
1155+ func buildContributionGrid (atoms []* Atom ) * ContributionGrid {
1156+ now := time .Now ()
1157+ today := time .Date (now .Year (), now .Month (), now .Day (), 0 , 0 , 0 , 0 , time .UTC )
1158+
1159+ // Start from the Sunday 52 weeks ago.
1160+ start := today .AddDate (0 , 0 , - int (today .Weekday ())- 52 * 7 )
1161+
1162+ // Count atoms per day.
1163+ counts := make (map [string ]int )
1164+ for _ , a := range atoms {
1165+ day := a .PublishedAt .UTC ().Format ("2006-01-02" )
1166+ counts [day ]++
1167+ }
1168+
1169+ // Find max count for scaling levels.
1170+ maxCount := 0
1171+ for _ , c := range counts {
1172+ if c > maxCount {
1173+ maxCount = c
1174+ }
1175+ }
1176+
1177+ // Build weeks.
1178+ numDays := int (today .Sub (start ).Hours ()/ 24 ) + 1
1179+ numWeeks := (numDays + 6 ) / 7
1180+ weeks := make ([]ContributionWeek , numWeeks )
1181+
1182+ for i := range numDays {
1183+ d := start .AddDate (0 , 0 , i )
1184+ week := i / 7
1185+ dow := i % 7
1186+ dateStr := d .Format ("2006-01-02" )
1187+ count := counts [dateStr ]
1188+
1189+ level := 0
1190+ if count > 0 && maxCount > 0 {
1191+ // Scale to 1-4.
1192+ level = (count * 3 )/ maxCount + 1
1193+ if level > 4 {
1194+ level = 4
1195+ }
1196+ }
1197+
1198+ weeks [week ].Days [dow ] = ContributionDay {
1199+ Date : dateStr ,
1200+ Count : count ,
1201+ Level : level ,
1202+ }
1203+ }
1204+
1205+ // Build month labels.
1206+ type monthLabel struct {
1207+ Name string
1208+ Offset int
1209+ }
1210+ var months []monthLabel
1211+ lastMonth := time .Month (- 1 )
1212+ for i := range numDays {
1213+ d := start .AddDate (0 , 0 , i )
1214+ if d .Month () != lastMonth && d .Day () <= 7 {
1215+ months = append (months , monthLabel {
1216+ Name : d .Format ("Jan" ),
1217+ Offset : i / 7 ,
1218+ })
1219+ lastMonth = d .Month ()
1220+ }
1221+ }
1222+
1223+ grid := & ContributionGrid {
1224+ Weeks : weeks ,
1225+ }
1226+ for _ , m := range months {
1227+ grid .MonthLabels = append (grid .MonthLabels , struct {
1228+ Name string
1229+ Offset int
1230+ OffsetPct float64
1231+ }{m .Name , m .Offset , float64 (m .Offset ) / float64 (numWeeks ) * 100 })
1232+ }
1233+
1234+ return grid
1235+ }
1236+
11311237func (a * Atom ) Equal (other * Atom ) bool {
11321238 return a .Description == other .Description &&
11331239 slices .EqualFunc (a .Photos , other .Photos , func (a , b * Photo ) bool { return a .Equal (b ) }) &&
@@ -1981,7 +2087,8 @@ func renderAtomArchive(ctx context.Context, c *modulir.Context, atoms []*Atom, a
19812087 }
19822088
19832089 locals := getLocals (map [string ]any {
1984- "Atoms" : atoms ,
2090+ "Atoms" : atoms ,
2091+ "ContributionGrid" : buildContributionGrid (atoms ),
19852092 })
19862093
19872094 err := dependencies .renderGoTemplate (ctx , c , source , path .Join (c .TargetDir , "atoms/archive" ), locals )
@@ -2131,13 +2238,16 @@ func renderAtomIndex(ctx context.Context, c *modulir.Context, atoms []*Atom, ato
21312238 return false , nil
21322239 }
21332240
2241+ grid := buildContributionGrid (atoms )
2242+
21342243 if len (atoms ) > maxAtomsIndex {
21352244 atoms = atoms [0 :maxAtomsIndex ]
21362245 }
21372246
21382247 locals := getLocals (map [string ]any {
2139- "Atoms" : atoms ,
2140- "IndexMax" : maxAtomsIndex ,
2248+ "Atoms" : atoms ,
2249+ "ContributionGrid" : grid ,
2250+ "IndexMax" : maxAtomsIndex ,
21412251 })
21422252
21432253 err := dependencies .renderGoTemplate (ctx , c , source , path .Join (c .TargetDir , "atoms/index.html" ), locals )
0 commit comments