forked from tangled.org/core
this repo has no description

appview: profile: render punchcard on profile

Signed-off-by: oppiliappan <me@oppi.li>

authored by oppi.li and committed by Tangled 8338c984 48e55e05

Changed files
+191 -67
appview
cmd
punchcardPopulate
+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
··· 316 316 CollaboratingRepos []db.Repo 317 317 ProfileTimeline *db.ProfileTimeline 318 318 Card ProfileCard 319 + Punchcard db.Punchcard 319 320 320 321 DidHandleMap map[string]string 321 322 }
+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
··· 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 13 {{ template "user/fragments/profileCard" .Card }} 14 14 </div>
+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
··· 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 + }
+4
tailwind.config.js
··· 67 67 }, 68 68 }, 69 69 }, 70 + gridTemplateColumns: { 71 + '14': 'repeat(14, minmax(0, 1fr))', 72 + '28': 'repeat(28, minmax(0, 1fr))', 73 + } 70 74 }, 71 75 }, 72 76 plugins: [require("@tailwindcss/typography")],