tenseleyflow/shithub / 3660179

Browse files

Align profile contribution calendar grid

Authored by mfwolffe <wolffemf@dukes.jmu.edu>
SHA
3660179572c69e0cf49078fe6d97fe4340759f45
Parents
17f5db4
Tree
2298ebe

5 changed files

StatusFile+-
M internal/web/handlers/profile/overview.go 32 9
A internal/web/handlers/profile/overview_internal_test.go 33 0
M internal/web/handlers/profile/profile_test.go 25 2
M internal/web/static/css/shithub.css 26 15
M internal/web/templates/profile/view.html 18 16
internal/web/handlers/profile/overview.gomodified
@@ -28,7 +28,6 @@ import (
2828
 )
2929
 
3030
 const (
31
-	profileContribWeeks      = 53
3231
 	profileContribRepoLimit  = 500
3332
 	profileContribMaxPerRepo = 5000
3433
 	profileReadmeMaxBytes    = 1 * 1024 * 1024
@@ -211,7 +210,7 @@ func (h *Handlers) contributionCalendar(ctx context.Context, user usersdb.User,
211210
 		windowEndDay = time.Date(selectedYear, time.December, 31, 0, 0, 0, 0, time.UTC)
212211
 		period = "in " + strconv.Itoa(selectedYear)
213212
 	}
214
-	gridStart := windowStart.AddDate(0, 0, -int(windowStart.Weekday()))
213
+	gridStart, weekCount := contributionGrid(windowStart, windowEndDay)
215214
 	windowEnd := windowEndDay.Add(24 * time.Hour)
216215
 	activityMonth := time.Date(windowEndDay.Year(), windowEndDay.Month(), 1, 0, 0, 0, 0, time.UTC)
217216
 	repos := h.profileContributionRepos(ctx, user, viewer, user.IncludePrivateContributions)
@@ -254,13 +253,17 @@ func (h *Handlers) contributionCalendar(ctx context.Context, user usersdb.User,
254253
 		}
255254
 	}
256255
 
257
-	weeks := make([]contributionWeek, 0, profileContribWeeks)
256
+	weeks := make([]contributionWeek, 0, weekCount)
258257
 	total := 0
259258
 	monthCommitCount := 0
260
-	for w := 0; w < profileContribWeeks; w++ {
261
-		week := contributionWeek{Days: make([]contributionDay, 0, 7)}
259
+	for w := 0; w < weekCount; w++ {
260
+		weekStart := gridStart.AddDate(0, 0, w*7)
261
+		week := contributionWeek{
262
+			MonthLabel: contributionWeekMonthLabel(weekStart, w == 0),
263
+			Days:       make([]contributionDay, 0, 7),
264
+		}
262265
 		for d := 0; d < 7; d++ {
263
-			day := gridStart.AddDate(0, 0, w*7+d)
266
+			day := weekStart.AddDate(0, 0, d)
264267
 			key := day.Format("2006-01-02")
265268
 			count := counts[key]
266269
 			inWindow := !day.Before(windowStart) && !day.After(windowEndDay)
@@ -270,9 +273,6 @@ func (h *Handlers) contributionCalendar(ctx context.Context, user usersdb.User,
270273
 					monthCommitCount += count
271274
 				}
272275
 			}
273
-			if d == 0 && (day.Day() <= 7 || w == 0) {
274
-				week.MonthLabel = day.Format("Jan")
275
-			}
276276
 			week.Days = append(week.Days, contributionDay{
277277
 				Date:       key,
278278
 				Title:      contributionDayTitle(count, day),
@@ -383,6 +383,29 @@ func selectedContributionYear(query url.Values, currentYear int) int {
383383
 	return currentYear
384384
 }
385385
 
386
+func contributionGrid(windowStart, windowEndDay time.Time) (time.Time, int) {
387
+	gridStart := windowStart.AddDate(0, 0, -int(windowStart.Weekday()))
388
+	gridEnd := windowEndDay.AddDate(0, 0, 6-int(windowEndDay.Weekday()))
389
+	days := int(gridEnd.Sub(gridStart).Hours()/24) + 1
390
+	if days <= 0 {
391
+		return gridStart, 0
392
+	}
393
+	return gridStart, days / 7
394
+}
395
+
396
+func contributionWeekMonthLabel(weekStart time.Time, firstWeek bool) string {
397
+	for offset := 0; offset < 7; offset++ {
398
+		day := weekStart.AddDate(0, 0, offset)
399
+		if day.Day() == 1 {
400
+			return day.Format("Jan")
401
+		}
402
+	}
403
+	if firstWeek {
404
+		return weekStart.Format("Jan")
405
+	}
406
+	return ""
407
+}
408
+
386409
 func contributionYears(username string, currentYear, selectedYear int) []contributionYear {
387410
 	years := make([]contributionYear, 0, 4)
388411
 	base := "/" + url.PathEscape(username)
internal/web/handlers/profile/overview_internal_test.goadded
@@ -0,0 +1,33 @@
1
+// SPDX-License-Identifier: AGPL-3.0-or-later
2
+
3
+package profile
4
+
5
+import (
6
+	"testing"
7
+	"time"
8
+)
9
+
10
+func TestContributionGridExpandsToCompleteWeeks(t *testing.T) {
11
+	t.Parallel()
12
+	start, weeks := contributionGrid(
13
+		time.Date(2028, time.January, 1, 0, 0, 0, 0, time.UTC),
14
+		time.Date(2028, time.December, 31, 0, 0, 0, 0, time.UTC),
15
+	)
16
+	if want := time.Date(2027, time.December, 26, 0, 0, 0, 0, time.UTC); !start.Equal(want) {
17
+		t.Fatalf("grid start = %s, want %s", start.Format(time.DateOnly), want.Format(time.DateOnly))
18
+	}
19
+	if weeks != 54 {
20
+		t.Fatalf("weeks = %d, want 54", weeks)
21
+	}
22
+}
23
+
24
+func TestContributionWeekMonthLabelUsesFirstOfMonthColumn(t *testing.T) {
25
+	t.Parallel()
26
+	weekStart := time.Date(2026, time.April, 26, 0, 0, 0, 0, time.UTC)
27
+	if got := contributionWeekMonthLabel(weekStart, false); got != "May" {
28
+		t.Fatalf("label = %q, want May", got)
29
+	}
30
+	if got := contributionWeekMonthLabel(weekStart.AddDate(0, 0, 7), false); got != "" {
31
+		t.Fatalf("next label = %q, want empty", got)
32
+	}
33
+}
internal/web/handlers/profile/profile_test.gomodified
@@ -259,6 +259,29 @@ func (e *profileEnv) suspend(t *testing.T, userID int64) {
259259
 	}
260260
 }
261261
 
262
+func assertContributionWeeksInRange(t *testing.T, body string) {
263
+	t.Helper()
264
+	const marker = "WEEKS="
265
+	start := strings.Index(body, marker)
266
+	if start < 0 {
267
+		t.Fatalf("missing %q in body: %s", marker, body)
268
+	}
269
+	raw := body[start+len(marker):]
270
+	end := strings.IndexFunc(raw, func(r rune) bool {
271
+		return r < '0' || r > '9'
272
+	})
273
+	if end >= 0 {
274
+		raw = raw[:end]
275
+	}
276
+	weeks, err := strconv.Atoi(raw)
277
+	if err != nil {
278
+		t.Fatalf("parse weeks from %q: %v", raw, err)
279
+	}
280
+	if weeks < 53 || weeks > 54 {
281
+		t.Fatalf("weeks = %d, want 53 or 54; body: %s", weeks, body)
282
+	}
283
+}
284
+
262285
 func newNonRedirClient(t *testing.T) *http.Client {
263286
 	t.Helper()
264287
 	jar, err := cookiejar.New(nil)
@@ -475,13 +498,13 @@ func TestProfile_OverviewDataUsesVisibleReposAndOrganizations(t *testing.T) {
475498
 		"ORGS=1",
476499
 		"README=false",
477500
 		"CONTRIB=0",
478
-		"WEEKS=53",
479501
 		"YEARS=4",
480502
 	} {
481503
 		if !strings.Contains(body, want) {
482504
 			t.Errorf("missing %q in body: %s", want, body)
483505
 		}
484506
 	}
507
+	assertContributionWeeksInRange(t, body)
485508
 	if strings.Contains(body, "private-repo") {
486509
 		t.Fatalf("anonymous profile overview leaked private repo data: %s", body)
487510
 	}
@@ -509,12 +532,12 @@ func TestProfile_ContributionsCountVerifiedAndAffiliatedImportedIdentities(t *te
509532
 	for _, want := range []string{
510533
 		"CONTRIB=2",
511534
 		"PERIOD=in the last year",
512
-		"WEEKS=53",
513535
 	} {
514536
 		if !strings.Contains(body, want) {
515537
 			t.Errorf("missing %q in body: %s", want, body)
516538
 		}
517539
 	}
540
+	assertContributionWeeksInRange(t, body)
518541
 }
519542
 
520543
 func TestProfile_ContributionsSelectedYearHasStableLinks(t *testing.T) {
internal/web/static/css/shithub.cssmodified
@@ -1358,17 +1358,25 @@ button.shithub-contrib-setting-item:hover {
13581358
   align-items: start;
13591359
 }
13601360
 .shithub-profile-calendar {
1361
+  --shithub-contrib-cell: 10px;
1362
+  --shithub-contrib-gap: 3px;
1363
+  --shithub-contrib-weekday-width: 24px;
1364
+  --shithub-contrib-weekday-gap: 0.5rem;
13611365
   border: 1px solid var(--border-default);
13621366
   border-radius: 6px;
13631367
   padding: 1rem;
13641368
   min-width: 0;
1369
+  overflow: hidden;
1370
+}
1371
+.shithub-profile-calendar-scroll {
13651372
   overflow-x: auto;
1373
+  padding-bottom: 0.25rem;
13661374
 }
13671375
 .shithub-contrib-months {
13681376
   display: grid;
1369
-  grid-template-columns: repeat(53, 13px);
1370
-  gap: 3px;
1371
-  margin-left: 32px;
1377
+  grid-template-columns: repeat(var(--shithub-contrib-weeks), var(--shithub-contrib-cell));
1378
+  column-gap: var(--shithub-contrib-gap);
1379
+  margin-left: calc(var(--shithub-contrib-weekday-width) + var(--shithub-contrib-weekday-gap));
13721380
   margin-bottom: 0.25rem;
13731381
   color: var(--fg-default);
13741382
   font-size: 0.75rem;
@@ -1377,38 +1385,41 @@ button.shithub-contrib-setting-item:hover {
13771385
 }
13781386
 .shithub-contrib-months span {
13791387
   min-height: 1rem;
1388
+  min-width: var(--shithub-contrib-cell);
13801389
   white-space: nowrap;
13811390
 }
13821391
 .shithub-contrib-grid-wrap {
1383
-  display: flex;
1384
-  gap: 0.5rem;
1392
+  display: grid;
1393
+  grid-template-columns: var(--shithub-contrib-weekday-width) max-content;
1394
+  column-gap: var(--shithub-contrib-weekday-gap);
13851395
   min-width: 0;
13861396
   width: max-content;
13871397
 }
13881398
 .shithub-contrib-weekdays {
13891399
   display: grid;
1390
-  grid-template-rows: repeat(7, 10px);
1391
-  gap: 3px;
1392
-  width: 24px;
1400
+  grid-template-rows: repeat(7, var(--shithub-contrib-cell));
1401
+  row-gap: var(--shithub-contrib-gap);
1402
+  width: var(--shithub-contrib-weekday-width);
13931403
   color: var(--fg-default);
13941404
   font-size: 0.75rem;
1395
-  line-height: 10px;
1405
+  line-height: var(--shithub-contrib-cell);
13961406
 }
13971407
 .shithub-contrib-weeks {
1398
-  display: flex;
1399
-  gap: 3px;
1408
+  display: grid;
1409
+  grid-template-columns: repeat(var(--shithub-contrib-weeks), var(--shithub-contrib-cell));
1410
+  column-gap: var(--shithub-contrib-gap);
14001411
   overflow: visible;
14011412
   padding-bottom: 0.25rem;
14021413
 }
14031414
 .shithub-contrib-week {
14041415
   display: grid;
1405
-  grid-template-rows: repeat(7, 10px);
1406
-  gap: 3px;
1416
+  grid-template-rows: repeat(7, var(--shithub-contrib-cell));
1417
+  row-gap: var(--shithub-contrib-gap);
14071418
 }
14081419
 .shithub-contrib-day,
14091420
 .shithub-contrib-legend i {
1410
-  width: 10px;
1411
-  height: 10px;
1421
+  width: var(--shithub-contrib-cell);
1422
+  height: var(--shithub-contrib-cell);
14121423
   display: block;
14131424
   border-radius: 2px;
14141425
   box-shadow: inset 0 0 0 1px rgba(255,255,255,0.03);
internal/web/templates/profile/view.htmlmodified
@@ -137,26 +137,28 @@
137137
 
138138
         <div class="shithub-profile-contrib-layout">
139139
           <div class="shithub-profile-calendar" role="img" aria-label="{{ .Contributions.Total }} contributions {{ .Contributions.Period }}">
140
-            <div class="shithub-contrib-months" aria-hidden="true">
141
-              {{ range .Contributions.Weeks }}<span>{{ .MonthLabel }}</span>{{ end }}
142
-            </div>
143
-            <div class="shithub-contrib-grid-wrap">
144
-              <div class="shithub-contrib-weekdays" aria-hidden="true">
145
-                <span></span><span>Mon</span><span></span><span>Wed</span><span></span><span>Fri</span><span></span>
140
+            <div class="shithub-profile-calendar-scroll" style="--shithub-contrib-weeks: {{ len .Contributions.Weeks }};">
141
+              <div class="shithub-contrib-months" aria-hidden="true">
142
+                {{ range .Contributions.Weeks }}<span>{{ .MonthLabel }}</span>{{ end }}
146143
               </div>
147
-              <div class="shithub-contrib-weeks">
148
-                {{ range .Contributions.Weeks }}
149
-                <div class="shithub-contrib-week">
150
-                  {{ range .Days }}
151
-                  <span class="shithub-contrib-day level-{{ .Level }}{{ if .IsFuture }} is-future{{ end }}{{ if not .IsInWindow }} is-outside{{ end }}" data-title="{{ .Title }}" data-date="{{ .Date }}" data-count="{{ .Count }}" aria-label="{{ .Title }}" tabindex="0"></span>
144
+              <div class="shithub-contrib-grid-wrap">
145
+                <div class="shithub-contrib-weekdays" aria-hidden="true">
146
+                  <span></span><span>Mon</span><span></span><span>Wed</span><span></span><span>Fri</span><span></span>
147
+                </div>
148
+                <div class="shithub-contrib-weeks">
149
+                  {{ range .Contributions.Weeks }}
150
+                  <div class="shithub-contrib-week">
151
+                    {{ range .Days }}
152
+                    <span class="shithub-contrib-day level-{{ .Level }}{{ if .IsFuture }} is-future{{ end }}{{ if not .IsInWindow }} is-outside{{ end }}" data-title="{{ .Title }}" data-date="{{ .Date }}" data-count="{{ .Count }}" aria-label="{{ .Title }}" tabindex="0"></span>
153
+                    {{ end }}
154
+                  </div>
152155
                   {{ end }}
153156
                 </div>
154
-                {{ end }}
155157
               </div>
156
-            </div>
157
-            <div class="shithub-profile-calendar-foot">
158
-              <a href="/docs">Learn how we count contributions</a>
159
-              <span class="shithub-contrib-legend">Less <i class="level-0"></i><i class="level-1"></i><i class="level-2"></i><i class="level-3"></i><i class="level-4"></i> More</span>
158
+              <div class="shithub-profile-calendar-foot">
159
+                <a href="/docs">Learn how we count contributions</a>
160
+                <span class="shithub-contrib-legend">Less <i class="level-0"></i><i class="level-1"></i><i class="level-2"></i><i class="level-3"></i><i class="level-4"></i> More</span>
161
+              </div>
160162
             </div>
161163
           </div>
162164
           <ol class="shithub-profile-years" aria-label="Contribution years">