appview: profile: render punchcard on profile #238

merged
opened by oppi.li targeting master from push-qoplqnlvlqqo
Changed files
+108
appview
pages
state
cmd
punchcardPopulate
+4
appview/pages/funcmap.go
···
"splitOn": func(s, sep string) []string {
return strings.Split(s, sep)
},
"add": func(a, b int) int {
return a + b
},
···
"longTimeFmt": func(t time.Time) string {
return t.Format("2006-01-02 * 3:04 PM")
},
"shortTimeFmt": func(t time.Time) string {
return humanize.CustomRelTime(t, time.Now(), "", "", []humanize.RelTimeMagnitude{
{time.Second, "now", time.Second},
···
"splitOn": func(s, sep string) []string {
return strings.Split(s, sep)
},
+
"int64": func(a int) int64 {
+
return int64(a)
+
},
"add": func(a, b int) int {
return a + b
},
···
"longTimeFmt": func(t time.Time) string {
return t.Format("2006-01-02 * 3:04 PM")
},
+
"commaFmt": humanize.Comma,
"shortTimeFmt": func(t time.Time) string {
return humanize.CustomRelTime(t, time.Now(), "", "", []humanize.RelTimeMagnitude{
{time.Second, "now", time.Second},
+1
appview/pages/pages.go
···
CollaboratingRepos []db.Repo
ProfileTimeline *db.ProfileTimeline
Card ProfileCard
DidHandleMap map[string]string
}
···
CollaboratingRepos []db.Repo
ProfileTimeline *db.ProfileTimeline
Card ProfileCard
+
Punchcard db.Punchcard
DidHandleMap map[string]string
}
+30
appview/pages/templates/user/profile.html
···
<div class="grid grid-cols-1 md:grid-cols-8 gap-6">
<div class="md:col-span-2 order-1 md:order-1">
{{ template "user/fragments/profileCard" .Card }}
</div>
<div id="all-repos" class="md:col-span-3 order-2 md:order-2">
{{ block "ownRepos" . }}{{ end }}
···
</div>
{{ end }}
{{ end }}
···
<div class="grid grid-cols-1 md:grid-cols-8 gap-6">
<div class="md:col-span-2 order-1 md:order-1">
{{ template "user/fragments/profileCard" .Card }}
+
{{ block "punchcard" .Punchcard }} {{ end }}
</div>
<div id="all-repos" class="md:col-span-3 order-2 md:order-2">
{{ block "ownRepos" . }}{{ end }}
···
</div>
{{ end }}
{{ end }}
+
+
{{ define "punchcard" }}
+
<div class="bg-white dark:bg-gray-800 px-6 py-4 rounded mt-3 drop-shadow-sm">
+
<p class="text-sm font-bold pb-2 dark:text-white">{{ .Total | int64 | commaFmt }} COMMITS</p>
+
<div class="grid grid-rows-7 grid-cols-52 gap-1 w-full h-full" style="grid-auto-flow: column;">
+
{{ range .Punches }}
+
{{ $count := .Count }}
+
{{ $theme := "bg-white dark:bg-gray-800 w-0 h-0" }}
+
{{ if lt $count 1 }}
+
{{ $theme = "bg-white dark:bg-gray-800 w-0 h-0" }}
+
{{ else if lt $count 2 }}
+
{{ $theme = "bg-green-200 dark:bg-green-800 size-px" }}
+
{{ else if lt $count 4 }}
+
{{ $theme = "bg-green-300 dark:bg-green-700 size-px" }}
+
{{ else if lt $count 6 }}
+
{{ $theme = "bg-green-300 dark:bg-green-700 size-[2px]" }}
+
{{ else }}
+
{{ $theme = "bg-green-400 dark:bg-green-600 size-[2px]" }}
+
{{ end }}
+
<div class="w-full h-full flex justify-center items-center">
+
<div
+
class="aspect-square rounded-full transition-all duration-300 {{ $theme }} max-w-full max-h-full"
+
title="{{ .Date.Format "2006-01-02" }}: {{ .Count }} commits">
+
</div>
+
</div>
+
{{ end }}
+
</div>
+
</div>
+
{{ end }}
+24
appview/state/profile.go
···
"net/http"
"slices"
"strings"
comatproto "github.com/bluesky-social/indigo/api/atproto"
"github.com/bluesky-social/indigo/atproto/identity"
···
followStatus = db.GetFollowStatus(s.db, loggedInUser.Did, ident.DID.String())
}
profileAvatarUri := s.GetAvatarUri(ident.Handle.String())
s.pages.ProfilePage(w, pages.ProfilePageParams{
LoggedInUser: loggedInUser,
···
Followers: followers,
Following: following,
},
ProfileTimeline: timeline,
})
}
···
"net/http"
"slices"
"strings"
+
"time"
comatproto "github.com/bluesky-social/indigo/api/atproto"
"github.com/bluesky-social/indigo/atproto/identity"
···
followStatus = db.GetFollowStatus(s.db, loggedInUser.Did, ident.DID.String())
}
+
now := time.Now()
+
startOfYear := time.Date(now.Year(), 1, 1, 0, 0, 0, 0, time.UTC)
+
punchcard, err := db.MakePunchcard(
+
s.db,
+
db.FilterEq("did", ident.DID.String()),
+
db.FilterGte("date", startOfYear.Format(time.DateOnly)),
+
db.FilterLte("date", now.Format(time.DateOnly)),
+
)
+
if err != nil {
+
log.Println("failed to get punchcard for did", "did", ident.DID.String(), "err", err)
+
} else {
+
// Extend punchcard to end of year with empty entries
+
endOfYear := time.Date(now.Year(), 12, 31, 0, 0, 0, 0, time.UTC)
+
tomorrow := now.AddDate(0, 0, 1)
+
for d := tomorrow; d.Before(endOfYear) || d.Equal(endOfYear); d = d.AddDate(0, 0, 1) {
+
punchcard.Punches = append(punchcard.Punches, db.Punch{
+
Date: d,
+
Count: 0,
+
})
+
}
+
}
+
profileAvatarUri := s.GetAvatarUri(ident.Handle.String())
s.pages.ProfilePage(w, pages.ProfilePageParams{
LoggedInUser: loggedInUser,
···
Followers: followers,
Following: following,
},
+
Punchcard: punchcard,
ProfileTimeline: timeline,
})
}
+49
cmd/punchcardPopulate/main.go
···
···
+
package main
+
+
import (
+
"database/sql"
+
"fmt"
+
"log"
+
"math/rand"
+
"time"
+
+
_ "github.com/mattn/go-sqlite3"
+
)
+
+
func main() {
+
db, err := sql.Open("sqlite3", "./appview.db")
+
if err != nil {
+
log.Fatal("Failed to open database:", err)
+
}
+
defer db.Close()
+
+
const did = "did:plc:qfpnj4og54vl56wngdriaxug"
+
+
now := time.Now()
+
start := time.Date(now.Year(), 1, 1, 0, 0, 0, 0, time.UTC)
+
+
tx, err := db.Begin()
+
if err != nil {
+
log.Fatal(err)
+
}
+
stmt, err := tx.Prepare("INSERT INTO punchcard (did, date, count) VALUES (?, ?, ?)")
+
if err != nil {
+
log.Fatal(err)
+
}
+
defer stmt.Close()
+
+
for day := start; !day.After(now); day = day.AddDate(0, 0, 1) {
+
count := rand.Intn(16) // 0–5
+
dateStr := day.Format("2006-01-02")
+
_, err := stmt.Exec(did, dateStr, count)
+
if err != nil {
+
log.Println("Failed to insert for date %s: %v", dateStr, err)
+
}
+
}
+
+
if err := tx.Commit(); err != nil {
+
log.Fatal("Failed to commit:", err)
+
}
+
+
fmt.Println("Done populating punchcard.")
+
}