+7
appview/pages/funcmap.go
+7
appview/pages/funcmap.go
···
33
33
"splitOn": func(s, sep string) []string {
34
34
return strings.Split(s, sep)
35
35
},
36
+
"int64": func(a int) int64 {
37
+
return int64(a)
38
+
},
36
39
"add": func(a, b int) int {
37
40
return a + b
41
+
},
42
+
"now": func() time.Time {
43
+
return time.Now()
38
44
},
39
45
// the absolute state of go templates
40
46
"add64": func(a, b int64) int64 {
···
79
85
"longTimeFmt": func(t time.Time) string {
80
86
return t.Format("2006-01-02 * 3:04 PM")
81
87
},
88
+
"commaFmt": humanize.Comma,
82
89
"shortTimeFmt": func(t time.Time) string {
83
90
return humanize.CustomRelTime(t, time.Now(), "", "", []humanize.RelTimeMagnitude{
84
91
{time.Second, "now", time.Second},
+1
appview/pages/pages.go
+1
appview/pages/pages.go
+115
-66
appview/pages/templates/user/profile.html
+115
-66
appview/pages/templates/user/profile.html
···
8
8
{{ end }}
9
9
10
10
{{ define "content" }}
11
-
<div class="grid grid-cols-1 md:grid-cols-8 gap-6">
11
+
<div class="grid grid-cols-1 md:grid-cols-8 gap-4">
12
12
<div class="md:col-span-2 order-1 md:order-1">
13
+
<div class="grid grid-cols-1 gap-4">
13
14
{{ template "user/fragments/profileCard" .Card }}
15
+
{{ block "punchcard" .Punchcard }} {{ end }}
16
+
</div>
14
17
</div>
15
18
<div id="all-repos" class="md:col-span-3 order-2 md:order-2">
19
+
<div class="grid grid-cols-1 gap-4">
16
20
{{ block "ownRepos" . }}{{ end }}
17
21
{{ block "collaboratingRepos" . }}{{ end }}
22
+
</div>
18
23
</div>
19
24
<div class="md:col-span-3 order-3 md:order-3">
20
25
{{ block "profileTimeline" . }}{{ end }}
···
24
29
25
30
{{ define "profileTimeline" }}
26
31
<p class="text-sm font-bold p-2 dark:text-white">ACTIVITY</p>
27
-
<div class="flex flex-col gap-6 relative">
32
+
<div class="flex flex-col gap-4 relative">
28
33
{{ with .ProfileTimeline }}
29
34
{{ range $idx, $byMonth := .ByMonth }}
30
35
{{ with $byMonth }}
···
233
238
{{ end }}
234
239
235
240
{{ define "ownRepos" }}
236
-
<div class="text-sm font-bold p-2 pr-0 dark:text-white flex items-center justify-between gap-2">
237
-
<a href="/@{{ or $.Card.UserHandle $.Card.UserDid }}?tab=repos"
238
-
class="flex text-black dark:text-white items-center gap-4 no-underline hover:no-underline group">
239
-
<span>PINNED REPOS</span>
240
-
<span class="flex md:hidden group-hover:flex gap-2 items-center font-normal text-sm text-gray-500 dark:text-gray-400 ">
241
-
view all {{ i "chevron-right" "w-4 h-4" }}
242
-
</span>
243
-
</a>
244
-
{{ if and .LoggedInUser (eq .LoggedInUser.Did .Card.UserDid) }}
245
-
<button
246
-
hx-get="profile/edit-pins"
247
-
hx-target="#all-repos"
248
-
class="btn font-normal text-sm flex gap-2 items-center group">
249
-
{{ i "pencil" "w-3 h-3" }}
250
-
edit
251
-
{{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }}
252
-
</button>
253
-
{{ end }}
254
-
</div>
255
-
<div id="repos" class="grid grid-cols-1 gap-4 mb-6">
256
-
{{ range .Repos }}
257
-
<div
258
-
id="repo-card"
259
-
class="py-4 px-6 drop-shadow-sm rounded bg-white dark:bg-gray-800">
260
-
<div id="repo-card-name" class="font-medium">
261
-
<a href="/@{{ or $.Card.UserHandle $.Card.UserDid }}/{{ .Name }}"
262
-
>{{ .Name }}</a
263
-
>
264
-
</div>
265
-
{{ if .Description }}
266
-
<div class="text-gray-600 dark:text-gray-300 text-sm">
267
-
{{ .Description }}
268
-
</div>
269
-
{{ end }}
270
-
<div class="text-gray-400 pt-1 text-sm font-mono inline-flex gap-4 mt-auto">
271
-
{{ if .RepoStats.StarCount }}
272
-
<div class="flex gap-1 items-center text-sm">
273
-
{{ i "star" "w-3 h-3 fill-current" }}
274
-
<span>{{ .RepoStats.StarCount }}</span>
275
-
</div>
276
-
{{ end }}
277
-
</div>
278
-
</div>
279
-
{{ else }}
280
-
<p class="px-6 dark:text-white">This user does not have any repos yet.</p>
281
-
{{ end }}
282
-
</div>
283
-
{{ end }}
284
-
285
-
{{ define "collaboratingRepos" }}
286
-
{{ if gt (len .CollaboratingRepos) 0 }}
287
-
<p class="text-sm font-bold p-2 dark:text-white">COLLABORATING ON</p>
288
-
<div id="collaborating" class="grid grid-cols-1 gap-4 mb-6">
289
-
{{ range .CollaboratingRepos }}
241
+
<div>
242
+
<div class="text-sm font-bold p-2 pr-0 dark:text-white flex items-center justify-between gap-2">
243
+
<a href="/@{{ or $.Card.UserHandle $.Card.UserDid }}?tab=repos"
244
+
class="flex text-black dark:text-white items-center gap-2 no-underline hover:no-underline group">
245
+
<span>PINNED REPOS</span>
246
+
<span class="flex gap-1 items-center font-normal text-sm text-gray-500 dark:text-gray-400 ">
247
+
view all {{ i "chevron-right" "w-4 h-4" }}
248
+
</span>
249
+
</a>
250
+
{{ if and .LoggedInUser (eq .LoggedInUser.Did .Card.UserDid) }}
251
+
<button
252
+
hx-get="profile/edit-pins"
253
+
hx-target="#all-repos"
254
+
class="btn py-0 font-normal text-sm flex gap-2 items-center group">
255
+
{{ i "pencil" "w-3 h-3" }}
256
+
edit
257
+
{{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }}
258
+
</button>
259
+
{{ end }}
260
+
</div>
261
+
<div id="repos" class="grid grid-cols-1 gap-4">
262
+
{{ range .Repos }}
290
263
<div
291
264
id="repo-card"
292
-
class="py-4 px-6 drop-shadow-sm rounded bg-white dark:bg-gray-800 flex flex-col">
293
-
<div id="repo-card-name" class="font-medium dark:text-white">
294
-
<a href="/{{ index $.DidHandleMap .Did }}/{{ .Name }}">
295
-
{{ index $.DidHandleMap .Did }}/{{ .Name }}
296
-
</a>
265
+
class="py-4 px-6 drop-shadow-sm rounded bg-white dark:bg-gray-800">
266
+
<div id="repo-card-name" class="font-medium">
267
+
<a href="/@{{ or $.Card.UserHandle $.Card.UserDid }}/{{ .Name }}"
268
+
>{{ .Name }}</a
269
+
>
297
270
</div>
298
271
{{ if .Description }}
299
272
<div class="text-gray-600 dark:text-gray-300 text-sm">
···
301
274
</div>
302
275
{{ end }}
303
276
<div class="text-gray-400 pt-1 text-sm font-mono inline-flex gap-4 mt-auto">
277
+
{{ if .RepoStats.StarCount }}
278
+
<div class="flex gap-1 items-center text-sm">
279
+
{{ i "star" "w-3 h-3 fill-current" }}
280
+
<span>{{ .RepoStats.StarCount }}</span>
281
+
</div>
282
+
{{ end }}
283
+
</div>
284
+
</div>
285
+
{{ else }}
286
+
<p class="px-6 dark:text-white">This user does not have any repos yet.</p>
287
+
{{ end }}
288
+
</div>
289
+
</div>
290
+
{{ end }}
304
291
292
+
{{ define "collaboratingRepos" }}
293
+
{{ if gt (len .CollaboratingRepos) 0 }}
294
+
<div>
295
+
<p class="text-sm font-bold p-2 dark:text-white">COLLABORATING ON</p>
296
+
<div id="collaborating" class="grid grid-cols-1 gap-4">
297
+
{{ range .CollaboratingRepos }}
298
+
<div
299
+
id="repo-card"
300
+
class="py-4 px-6 drop-shadow-sm rounded bg-white dark:bg-gray-800">
301
+
<div id="repo-card-name" class="font-medium dark:text-white">
302
+
<a href="/{{ index $.DidHandleMap .Did }}/{{ .Name }}">
303
+
{{ index $.DidHandleMap .Did }}/{{ .Name }}
304
+
</a>
305
+
</div>
306
+
{{ if .Description }}
307
+
<div class="text-gray-600 dark:text-gray-300 text-sm">
308
+
{{ .Description }}
309
+
</div>
310
+
{{ end }}
311
+
<div class="text-gray-400 pt-1 text-sm font-mono inline-flex gap-4 mt-auto">
305
312
{{ if .RepoStats.StarCount }}
306
313
<div class="flex gap-1 items-center text-sm">
307
314
{{ i "star" "w-3 h-3 fill-current" }}
308
315
<span>{{ .RepoStats.StarCount }}</span>
309
316
</div>
310
317
{{ end }}
311
-
</div>
312
-
</div>
313
-
{{ else }}
314
-
<p class="px-6 dark:text-white">This user is not collaborating.</p>
315
-
{{ end }}
318
+
</div>
319
+
</div>
320
+
{{ else }}
321
+
<p class="px-6 dark:text-white">This user is not collaborating.</p>
322
+
{{ end }}
323
+
</div>
316
324
</div>
317
325
{{ end }}
318
326
{{ end }}
327
+
328
+
{{ define "punchcard" }}
329
+
{{ $now := now }}
330
+
<div>
331
+
<p class="p-2 flex gap-2 text-sm font-bold dark:text-white">
332
+
PUNCHCARD
333
+
<span class="font-normal text-sm text-gray-500 dark:text-gray-400 ">
334
+
{{ .Total | int64 | commaFmt }} commits
335
+
</span>
336
+
</p>
337
+
<div class="bg-white dark:bg-gray-800 px-6 py-4 rounded drop-shadow-sm">
338
+
<div class="grid grid-cols-28 md:grid-cols-14 gap-y-2 w-full h-full">
339
+
{{ range .Punches }}
340
+
{{ $count := .Count }}
341
+
{{ $theme := "bg-gray-200 dark:bg-gray-700 size-[4px]" }}
342
+
{{ if lt $count 1 }}
343
+
{{ $theme = "bg-gray-200 dark:bg-gray-700 size-[4px]" }}
344
+
{{ else if lt $count 2 }}
345
+
{{ $theme = "bg-green-200 dark:bg-green-900 size-[5px]" }}
346
+
{{ else if lt $count 4 }}
347
+
{{ $theme = "bg-green-300 dark:bg-green-800 size-[5px]" }}
348
+
{{ else if lt $count 8 }}
349
+
{{ $theme = "bg-green-400 dark:bg-green-700 size-[6px]" }}
350
+
{{ else }}
351
+
{{ $theme = "bg-green-500 dark:bg-green-600 size-[7px]" }}
352
+
{{ end }}
353
+
354
+
{{ if .Date.After $now }}
355
+
{{ $theme = "border border-gray-200 dark:border-gray-700 size-[4px]" }}
356
+
{{ end }}
357
+
<div class="w-full h-full flex justify-center items-center">
358
+
<div
359
+
class="aspect-square rounded-full transition-all duration-300 {{ $theme }} max-w-full max-h-full"
360
+
title="{{ .Date.Format "2006-01-02" }}: {{ .Count }} commits">
361
+
</div>
362
+
</div>
363
+
{{ end }}
364
+
</div>
365
+
</div>
366
+
</div>
367
+
{{ end }}
+1
-1
appview/pages/templates/user/repos.html
+1
-1
appview/pages/templates/user/repos.html
+14
appview/state/profile.go
+14
appview/state/profile.go
···
9
9
"net/http"
10
10
"slices"
11
11
"strings"
12
+
"time"
12
13
13
14
comatproto "github.com/bluesky-social/indigo/api/atproto"
14
15
"github.com/bluesky-social/indigo/atproto/identity"
···
126
127
followStatus = db.GetFollowStatus(s.db, loggedInUser.Did, ident.DID.String())
127
128
}
128
129
130
+
now := time.Now()
131
+
startOfYear := time.Date(now.Year(), 1, 1, 0, 0, 0, 0, time.UTC)
132
+
punchcard, err := db.MakePunchcard(
133
+
s.db,
134
+
db.FilterEq("did", ident.DID.String()),
135
+
db.FilterGte("date", startOfYear.Format(time.DateOnly)),
136
+
db.FilterLte("date", now.Format(time.DateOnly)),
137
+
)
138
+
if err != nil {
139
+
log.Println("failed to get punchcard for did", "did", ident.DID.String(), "err", err)
140
+
}
141
+
129
142
profileAvatarUri := s.GetAvatarUri(ident.Handle.String())
130
143
s.pages.ProfilePage(w, pages.ProfilePageParams{
131
144
LoggedInUser: loggedInUser,
···
141
154
Followers: followers,
142
155
Following: following,
143
156
},
157
+
Punchcard: punchcard,
144
158
ProfileTimeline: timeline,
145
159
})
146
160
}
+49
cmd/punchcardPopulate/main.go
+49
cmd/punchcardPopulate/main.go
···
1
+
package main
2
+
3
+
import (
4
+
"database/sql"
5
+
"fmt"
6
+
"log"
7
+
"math/rand"
8
+
"time"
9
+
10
+
_ "github.com/mattn/go-sqlite3"
11
+
)
12
+
13
+
func main() {
14
+
db, err := sql.Open("sqlite3", "./appview.db")
15
+
if err != nil {
16
+
log.Fatal("Failed to open database:", err)
17
+
}
18
+
defer db.Close()
19
+
20
+
const did = "did:plc:qfpnj4og54vl56wngdriaxug"
21
+
22
+
now := time.Now()
23
+
start := time.Date(now.Year(), 1, 1, 0, 0, 0, 0, time.UTC)
24
+
25
+
tx, err := db.Begin()
26
+
if err != nil {
27
+
log.Fatal(err)
28
+
}
29
+
stmt, err := tx.Prepare("INSERT INTO punchcard (did, date, count) VALUES (?, ?, ?)")
30
+
if err != nil {
31
+
log.Fatal(err)
32
+
}
33
+
defer stmt.Close()
34
+
35
+
for day := start; !day.After(now); day = day.AddDate(0, 0, 1) {
36
+
count := rand.Intn(16) // 0–5
37
+
dateStr := day.Format("2006-01-02")
38
+
_, err := stmt.Exec(did, dateStr, count)
39
+
if err != nil {
40
+
log.Println("Failed to insert for date %s: %v", dateStr, err)
41
+
}
42
+
}
43
+
44
+
if err := tx.Commit(); err != nil {
45
+
log.Fatal("Failed to commit:", err)
46
+
}
47
+
48
+
fmt.Println("Done populating punchcard.")
49
+
}