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

group timeline events in profile

timeline events in profiles are grouped by month (each month is a card) and by type (repos, issues and pulls).

authored by oppi.li and committed by Tangled d50fdb31 d8a12dae

Changed files
+422 -162
appview
+48 -18
appview/db/issues.go
··· 12 OwnerDid string 13 IssueId int 14 IssueAt string 15 - Created *time.Time 16 Title string 17 Body string 18 Open bool 19 Metadata *IssueMetadata 20 } 21 22 type IssueMetadata struct { 23 CommentCount int 24 // labels, assignee etc. 25 } 26 ··· 143 if err != nil { 144 return nil, err 145 } 146 - issue.Created = &createdTime 147 issue.Metadata = &metadata 148 149 issues = append(issues, issue) ··· 156 return issues, nil 157 } 158 159 - func GetIssuesByOwnerDid(e Execer, ownerDid string) ([]Issue, error) { 160 var issues []Issue 161 162 rows, err := e.Query( ··· 168 i.title, 169 i.body, 170 i.open, 171 - count(c.id) 172 from 173 issues i 174 - left join 175 - comments c on i.repo_at = c.repo_at and i.issue_id = c.issue_id 176 where 177 - i.owner_did = ? 178 - group by 179 - i.id, i.owner_did, i.repo_at, i.issue_id, i.created, i.title, i.body, i.open 180 order by 181 i.created desc`, 182 - ownerDid) 183 if err != nil { 184 return nil, err 185 } ··· 187 188 for rows.Next() { 189 var issue Issue 190 - var createdAt string 191 - var metadata IssueMetadata 192 - err := rows.Scan(&issue.OwnerDid, &issue.RepoAt, &issue.IssueId, &createdAt, &issue.Title, &issue.Body, &issue.Open, &metadata.CommentCount) 193 if err != nil { 194 return nil, err 195 } 196 197 - createdTime, err := time.Parse(time.RFC3339, createdAt) 198 if err != nil { 199 return nil, err 200 } 201 - issue.Created = &createdTime 202 - issue.Metadata = &metadata 203 204 issues = append(issues, issue) 205 } ··· 226 if err != nil { 227 return nil, err 228 } 229 - issue.Created = &createdTime 230 231 return &issue, nil 232 } ··· 246 if err != nil { 247 return nil, nil, err 248 } 249 - issue.Created = &createdTime 250 251 comments, err := GetComments(e, repoAt, issueId) 252 if err != nil {
··· 12 OwnerDid string 13 IssueId int 14 IssueAt string 15 + Created time.Time 16 Title string 17 Body string 18 Open bool 19 + 20 + // optionally, populate this when querying for reverse mappings 21 + // like comment counts, parent repo etc. 22 Metadata *IssueMetadata 23 } 24 25 type IssueMetadata struct { 26 CommentCount int 27 + Repo *Repo 28 // labels, assignee etc. 29 } 30 ··· 147 if err != nil { 148 return nil, err 149 } 150 + issue.Created = createdTime 151 issue.Metadata = &metadata 152 153 issues = append(issues, issue) ··· 160 return issues, nil 161 } 162 163 + // timeframe here is directly passed into the sql query filter, and any 164 + // timeframe in the past should be negative; e.g.: "-3 months" 165 + func GetIssuesByOwnerDid(e Execer, ownerDid string, timeframe string) ([]Issue, error) { 166 var issues []Issue 167 168 rows, err := e.Query( ··· 174 i.title, 175 i.body, 176 i.open, 177 + r.did, 178 + r.name, 179 + r.knot, 180 + r.rkey, 181 + r.created 182 from 183 issues i 184 + join 185 + repos r on i.repo_at = r.at_uri 186 where 187 + i.owner_did = ? and i.created >= date ('now', ?) 188 order by 189 i.created desc`, 190 + ownerDid, timeframe) 191 if err != nil { 192 return nil, err 193 } ··· 195 196 for rows.Next() { 197 var issue Issue 198 + var issueCreatedAt, repoCreatedAt string 199 + var repo Repo 200 + err := rows.Scan( 201 + &issue.OwnerDid, 202 + &issue.RepoAt, 203 + &issue.IssueId, 204 + &issueCreatedAt, 205 + &issue.Title, 206 + &issue.Body, 207 + &issue.Open, 208 + &repo.Did, 209 + &repo.Name, 210 + &repo.Knot, 211 + &repo.Rkey, 212 + &repoCreatedAt, 213 + ) 214 if err != nil { 215 return nil, err 216 } 217 218 + issueCreatedTime, err := time.Parse(time.RFC3339, issueCreatedAt) 219 + if err != nil { 220 + return nil, err 221 + } 222 + issue.Created = issueCreatedTime 223 + 224 + repoCreatedTime, err := time.Parse(time.RFC3339, repoCreatedAt) 225 if err != nil { 226 return nil, err 227 } 228 + repo.Created = repoCreatedTime 229 + 230 + issue.Metadata = &IssueMetadata{ 231 + Repo: &repo, 232 + } 233 234 issues = append(issues, issue) 235 } ··· 256 if err != nil { 257 return nil, err 258 } 259 + issue.Created = createdTime 260 261 return &issue, nil 262 } ··· 276 if err != nil { 277 return nil, nil, err 278 } 279 + issue.Created = createdTime 280 281 comments, err := GetComments(e, repoAt, issueId) 282 if err != nil {
+119 -48
appview/db/profile.go
··· 2 3 import ( 4 "fmt" 5 - "sort" 6 "time" 7 ) 8 9 - type ProfileTimelineEvent struct { 10 - EventAt time.Time 11 - Type string 12 - *Issue 13 - *Pull 14 - *Repo 15 16 - // optional: populate only if Repo is a fork 17 - Source *Repo 18 } 19 20 - func MakeProfileTimeline(e Execer, forDid string) ([]ProfileTimelineEvent, error) { 21 - timeline := []ProfileTimelineEvent{} 22 - limit := 30 23 24 - pulls, err := GetPullsByOwnerDid(e, forDid) 25 if err != nil { 26 - return timeline, fmt.Errorf("error getting pulls by owner did: %w", err) 27 } 28 29 for _, pull := range pulls { 30 - repo, err := GetRepoByAtUri(e, string(pull.RepoAt)) 31 - if err != nil { 32 - return timeline, fmt.Errorf("error getting repo by at uri: %w", err) 33 } 34 35 - timeline = append(timeline, ProfileTimelineEvent{ 36 - EventAt: pull.Created, 37 - Type: "pull", 38 - Pull: &pull, 39 - Repo: repo, 40 - }) 41 } 42 43 - issues, err := GetIssuesByOwnerDid(e, forDid) 44 if err != nil { 45 - return timeline, fmt.Errorf("error getting issues by owner did: %w", err) 46 } 47 48 for _, issue := range issues { 49 - repo, err := GetRepoByAtUri(e, string(issue.RepoAt)) 50 - if err != nil { 51 - return timeline, fmt.Errorf("error getting repo by at uri: %w", err) 52 } 53 54 - timeline = append(timeline, ProfileTimelineEvent{ 55 - EventAt: *issue.Created, 56 - Type: "issue", 57 - Issue: &issue, 58 - Repo: repo, 59 - }) 60 } 61 62 repos, err := GetAllReposByDid(e, forDid) 63 if err != nil { 64 - return timeline, fmt.Errorf("error getting all repos by did: %w", err) 65 } 66 67 for _, repo := range repos { 68 var sourceRepo *Repo 69 if repo.Source != "" { 70 sourceRepo, err = GetRepoByAtUri(e, repo.Source) ··· 73 } 74 } 75 76 - timeline = append(timeline, ProfileTimelineEvent{ 77 - EventAt: repo.Created, 78 - Type: "repo", 79 - Repo: &repo, 80 - Source: sourceRepo, 81 - }) 82 - } 83 84 - sort.Slice(timeline, func(i, j int) bool { 85 - return timeline[i].EventAt.After(timeline[j].EventAt) 86 - }) 87 88 - if len(timeline) > limit { 89 - timeline = timeline[:limit] 90 } 91 92 - return timeline, nil 93 }
··· 2 3 import ( 4 "fmt" 5 "time" 6 ) 7 8 + type RepoEvent struct { 9 + Repo *Repo 10 + Source *Repo 11 + } 12 + 13 + type ProfileTimeline struct { 14 + ByMonth []ByMonth 15 + } 16 17 + type ByMonth struct { 18 + RepoEvents []RepoEvent 19 + IssueEvents IssueEvents 20 + PullEvents PullEvents 21 } 22 23 + func (b ByMonth) IsEmpty() bool { 24 + return len(b.RepoEvents) == 0 && 25 + len(b.IssueEvents.Items) == 0 && 26 + len(b.PullEvents.Items) == 0 27 + } 28 + 29 + type IssueEvents struct { 30 + Items []*Issue 31 + } 32 + 33 + type IssueEventStats struct { 34 + Open int 35 + Closed int 36 + } 37 + 38 + func (i IssueEvents) Stats() IssueEventStats { 39 + var open, closed int 40 + for _, issue := range i.Items { 41 + if issue.Open { 42 + open += 1 43 + } else { 44 + closed += 1 45 + } 46 + } 47 + 48 + return IssueEventStats{ 49 + Open: open, 50 + Closed: closed, 51 + } 52 + } 53 + 54 + type PullEvents struct { 55 + Items []*Pull 56 + } 57 + 58 + func (p PullEvents) Stats() PullEventStats { 59 + var open, merged, closed int 60 + for _, pull := range p.Items { 61 + switch pull.State { 62 + case PullOpen: 63 + open += 1 64 + case PullMerged: 65 + merged += 1 66 + case PullClosed: 67 + closed += 1 68 + } 69 + } 70 + 71 + return PullEventStats{ 72 + Open: open, 73 + Merged: merged, 74 + Closed: closed, 75 + } 76 + } 77 + 78 + type PullEventStats struct { 79 + Closed int 80 + Open int 81 + Merged int 82 + } 83 + 84 + const TimeframeMonths = 3 85 + 86 + func MakeProfileTimeline(e Execer, forDid string) (*ProfileTimeline, error) { 87 + timeline := ProfileTimeline{ 88 + ByMonth: make([]ByMonth, TimeframeMonths), 89 + } 90 + currentMonth := time.Now().Month() 91 + timeframe := fmt.Sprintf("-%d months", TimeframeMonths) 92 93 + pulls, err := GetPullsByOwnerDid(e, forDid, timeframe) 94 if err != nil { 95 + return nil, fmt.Errorf("error getting pulls by owner did: %w", err) 96 } 97 98 + // group pulls by month 99 for _, pull := range pulls { 100 + pullMonth := pull.Created.Month() 101 + 102 + if currentMonth-pullMonth > TimeframeMonths { 103 + // shouldn't happen; but times are weird 104 + continue 105 } 106 107 + idx := currentMonth - pullMonth 108 + items := &timeline.ByMonth[idx].PullEvents.Items 109 + 110 + *items = append(*items, &pull) 111 } 112 113 + issues, err := GetIssuesByOwnerDid(e, forDid, timeframe) 114 if err != nil { 115 + return nil, fmt.Errorf("error getting issues by owner did: %w", err) 116 } 117 118 for _, issue := range issues { 119 + issueMonth := issue.Created.Month() 120 + 121 + if currentMonth-issueMonth > TimeframeMonths { 122 + // shouldn't happen; but times are weird 123 + continue 124 } 125 126 + idx := currentMonth - issueMonth 127 + items := &timeline.ByMonth[idx].IssueEvents.Items 128 + 129 + *items = append(*items, &issue) 130 } 131 132 repos, err := GetAllReposByDid(e, forDid) 133 if err != nil { 134 + return nil, fmt.Errorf("error getting all repos by did: %w", err) 135 } 136 137 for _, repo := range repos { 138 + // TODO: get this in the original query; requires COALESCE because nullable 139 var sourceRepo *Repo 140 if repo.Source != "" { 141 sourceRepo, err = GetRepoByAtUri(e, repo.Source) ··· 144 } 145 } 146 147 + repoMonth := repo.Created.Month() 148 + 149 + if currentMonth-repoMonth > TimeframeMonths { 150 + // shouldn't happen; but times are weird 151 + continue 152 + } 153 154 + idx := currentMonth - repoMonth 155 156 + items := &timeline.ByMonth[idx].RepoEvents 157 + *items = append(*items, RepoEvent{ 158 + Repo: &repo, 159 + Source: sourceRepo, 160 + }) 161 } 162 163 + return &timeline, nil 164 }
+40 -14
appview/db/pulls.go
··· 64 // meta 65 Created time.Time 66 PullSource *PullSource 67 } 68 69 type PullSource struct { ··· 522 return &pull, nil 523 } 524 525 - func GetPullsByOwnerDid(e Execer, did string) ([]Pull, error) { 526 var pulls []Pull 527 528 rows, err := e.Query(` 529 select 530 - owner_did, 531 - repo_at, 532 - pull_id, 533 - created, 534 - title, 535 - state 536 from 537 - pulls 538 where 539 - owner_did = ? 540 order by 541 - created desc`, did) 542 if err != nil { 543 return nil, err 544 } ··· 546 547 for rows.Next() { 548 var pull Pull 549 - var createdAt string 550 err := rows.Scan( 551 &pull.OwnerDid, 552 &pull.RepoAt, 553 &pull.PullId, 554 - &createdAt, 555 &pull.Title, 556 &pull.State, 557 ) 558 if err != nil { 559 return nil, err 560 } 561 562 - createdTime, err := time.Parse(time.RFC3339, createdAt) 563 if err != nil { 564 return nil, err 565 } 566 - pull.Created = createdTime 567 568 pulls = append(pulls, pull) 569 }
··· 64 // meta 65 Created time.Time 66 PullSource *PullSource 67 + 68 + // optionally, populate this when querying for reverse mappings 69 + Repo *Repo 70 } 71 72 type PullSource struct { ··· 525 return &pull, nil 526 } 527 528 + // timeframe here is directly passed into the sql query filter, and any 529 + // timeframe in the past should be negative; e.g.: "-3 months" 530 + func GetPullsByOwnerDid(e Execer, did, timeframe string) ([]Pull, error) { 531 var pulls []Pull 532 533 rows, err := e.Query(` 534 select 535 + p.owner_did, 536 + p.repo_at, 537 + p.pull_id, 538 + p.created, 539 + p.title, 540 + p.state, 541 + r.did, 542 + r.name, 543 + r.knot, 544 + r.rkey, 545 + r.created 546 from 547 + pulls p 548 + join 549 + repos r on p.repo_at = r.at_uri 550 where 551 + p.owner_did = ? and p.created >= date ('now', ?) 552 order by 553 + p.created desc`, did, timeframe) 554 if err != nil { 555 return nil, err 556 } ··· 558 559 for rows.Next() { 560 var pull Pull 561 + var repo Repo 562 + var pullCreatedAt, repoCreatedAt string 563 err := rows.Scan( 564 &pull.OwnerDid, 565 &pull.RepoAt, 566 &pull.PullId, 567 + &pullCreatedAt, 568 &pull.Title, 569 &pull.State, 570 + &repo.Did, 571 + &repo.Name, 572 + &repo.Knot, 573 + &repo.Rkey, 574 + &repoCreatedAt, 575 ) 576 if err != nil { 577 return nil, err 578 } 579 580 + pullCreatedTime, err := time.Parse(time.RFC3339, pullCreatedAt) 581 if err != nil { 582 return nil, err 583 } 584 + pull.Created = pullCreatedTime 585 + 586 + repoCreatedTime, err := time.Parse(time.RFC3339, repoCreatedAt) 587 + if err != nil { 588 + return nil, err 589 + } 590 + repo.Created = repoCreatedTime 591 + 592 + pull.Repo = &repo 593 594 pulls = append(pulls, pull) 595 }
+3 -1
appview/db/repos.go
··· 77 where 78 r.did = ? 79 group by 80 - r.at_uri`, did) 81 if err != nil { 82 return nil, err 83 }
··· 77 where 78 r.did = ? 79 group by 80 + r.at_uri 81 + order by r.created desc`, 82 + did) 83 if err != nil { 84 return nil, err 85 }
+3 -2
appview/pages/pages.go
··· 178 CollaboratingRepos []db.Repo 179 ProfileStats ProfileStats 180 FollowStatus db.FollowStatus 181 - DidHandleMap map[string]string 182 AvatarUri string 183 - ProfileTimeline []db.ProfileTimelineEvent 184 } 185 186 type ProfileStats struct {
··· 178 CollaboratingRepos []db.Repo 179 ProfileStats ProfileStats 180 FollowStatus db.FollowStatus 181 AvatarUri string 182 + ProfileTimeline *db.ProfileTimeline 183 + 184 + DidHandleMap map[string]string 185 } 186 187 type ProfileStats struct {
+1 -1
appview/pages/templates/repo/blob.html
··· 29 > 30 / 31 {{ else }} 32 - <span class="text-bold text-gray-600 dark:text-gray-300" 33 >{{ index . 0 }}</span 34 > 35 {{ end }}
··· 29 > 30 / 31 {{ else }} 32 + <span class="text-bold text-black dark:text-white" 33 >{{ index . 0 }}</span 34 > 35 {{ end }}
+3 -3
appview/pages/templates/repo/tree.html
··· 17 {{ $containerstyle := "py-1" }} 18 {{ $linkstyle := "no-underline hover:underline" }} 19 20 - <div class="pb-2 mb-3 text-base border-b border-gray-200 dark:border-gray-500"> 21 <div class="flex flex-col md:flex-row md:justify-between gap-2"> 22 - <div id="breadcrumbs" class="overflow-x-auto whitespace-nowrap"> 23 {{ range .BreadCrumbs }} 24 - <a href="{{ index . 1}}" class="text-bold text-gray-500 dark:text-gray-400 {{ $linkstyle }}">{{ index . 0 }}</a> / 25 {{ end }} 26 </div> 27 <div id="dir-info" class="text-gray-500 dark:text-gray-400 text-xs md:text-sm flex flex-wrap items-center gap-1 md:gap-0">
··· 17 {{ $containerstyle := "py-1" }} 18 {{ $linkstyle := "no-underline hover:underline" }} 19 20 + <div class="pb-2 mb-3 text-base border-b border-gray-200 dark:border-gray-700"> 21 <div class="flex flex-col md:flex-row md:justify-between gap-2"> 22 + <div id="breadcrumbs" class="overflow-x-auto whitespace-nowrap text-gray-400 dark:text-gray-500"> 23 {{ range .BreadCrumbs }} 24 + <a href="{{ index . 1}}" class="text-bold text-gray-500 dark:text-gray-400 {{ $linkstyle }}">{{ index . 0 }}</a> / 25 {{ end }} 26 </div> 27 <div id="dir-info" class="text-gray-500 dark:text-gray-400 text-xs md:text-sm flex flex-wrap items-center gap-1 md:gap-0">
+193 -69
appview/pages/templates/user/profile.html
··· 9 {{ block "ownRepos" . }}{{ end }} 10 {{ block "collaboratingRepos" . }}{{ end }} 11 </div> 12 - 13 <div class="md:col-span-2 order-3 md:order-3"> 14 {{ block "profileTimeline" . }}{{ end }} 15 </div> 16 </div> 17 {{ end }} 18 19 20 - {{ define "profileTimeline" }} 21 - <div class="flex flex-col gap-3 relative"> 22 - <p class="px-6 text-sm font-bold py-2 dark:text-white">ACTIVITY</p> 23 - {{ range .ProfileTimeline }} 24 - {{ if eq .Type "issue" }} 25 - <div class="px-6 py-2 bg-white dark:bg-gray-800 rounded drop-shadow-sm w-fit max-w-full flex items-center gap-2"> 26 - {{ $textColor := "text-gray-800 dark:text-gray-400" }} 27 - {{ $icon := "ban" }} 28 - {{ if .Issue.Open }} 29 - {{ $textColor = "text-green-600 dark:text-green-500" }} 30 - {{ $icon = "circle-dot" }} 31 - {{ end }} 32 - <div class="p-1 {{ $textColor }}"> 33 - {{ i $icon "w-5 h-5" }} 34 </div> 35 - <div> 36 - <p class="text-gray-600 dark:text-gray-300"> 37 - <a href="/{{ index $.DidHandleMap .Repo.Did }}/{{ .Repo.Name }}/issues/{{ .Issue.IssueId }}" class="no-underline hover:underline">{{ .Issue.Title }} <span class="text-gray-500 dark:text-gray-400">#{{ .Issue.IssueId }}</span></a> 38 - on 39 - <a href="/{{ index $.DidHandleMap .Repo.Did }}/{{ .Repo.Name }}" class="no-underline hover:underline">{{ index $.DidHandleMap .Repo.Did }}<span class="select-none">/</span>{{ .Repo.Name }}</a> 40 - <time class="text-gray-700 dark:text-gray-400 text-xs ml-2">{{ .Issue.Created | shortTimeFmt }}</time> 41 - </p> 42 </div> 43 </div> 44 - {{ else if eq .Type "pull" }} 45 - <div class="px-6 py-2 bg-white dark:bg-gray-800 rounded drop-shadow-sm w-fit flex items-center gap-3"> 46 - {{ $textColor := "text-gray-800 dark:text-gray-400" }} 47 - {{ $icon := "git-pull-request-closed" }} 48 - {{ if .Pull.State.IsOpen }} 49 - {{ $textColor = "text-green-600 dark:text-green-500" }} 50 - {{ $icon = "git-pull-request" }} 51 - {{ else if .Pull.State.IsMerged }} 52 - {{ $textColor = "text-purple-600 dark:text-purple-500" }} 53 - {{ $icon = "git-merge" }} 54 {{ end }} 55 - <div class="{{ $textColor }} p-1"> 56 - {{ i $icon "w-5 h-5" }} 57 </div> 58 - <div> 59 - <p class="text-gray-600 dark:text-gray-300"> 60 - <a href="/{{ index $.DidHandleMap .Repo.Did }}/{{ .Repo.Name }}/pulls/{{ .Pull.PullId }}" class="no-underline hover:underline">{{ .Pull.Title }} <span class="text-gray-500 dark:text-gray-400">#{{ .Pull.PullId }}</span></a> 61 on 62 - <a href="/{{ index $.DidHandleMap .Repo.Did }}/{{ .Repo.Name }}" class="no-underline hover:underline"> 63 - {{ index $.DidHandleMap .Repo.Did }}<span class="select-none">/</span>{{ .Repo.Name }}</a> 64 - <time class="text-gray-700 dark:text-gray-400 text-xs ml-2">{{ .Pull.Created | shortTimeFmt }}</time> 65 - </p> 66 </div> 67 </div> 68 - {{ else if eq .Type "repo" }} 69 - <div class="px-6 py-2 bg-white dark:bg-gray-800 rounded drop-shadow-sm w-fit flex items-center gap-3"> 70 - {{ if .Source }} 71 - <div class="text-gray-800 dark:text-gray-400 p-1"> 72 - {{ i "git-fork" "w-5 h-5" }} 73 - </div> 74 {{ else }} 75 - <div class="text-gray-800 dark:text-gray-400 p-1"> 76 - {{ i "book-plus" "w-5 h-5" }} 77 </div> 78 - {{ end }} 79 - <div> 80 - <p class="text-gray-600 dark:text-gray-300"> 81 - 82 - {{ if .Source }} 83 - forked 84 - <a href="/{{ index $.DidHandleMap .Source.Did }}/{{ .Source.Name }}" class="no-underline hover:underline"> 85 - {{ index $.DidHandleMap .Source.Did }}/{{ .Source.Name }} 86 - </a> 87 - to 88 - <a href="/{{ didOrHandle $.UserHandle $.UserDid }}/{{ .Repo.Name }}" class="no-underline hover:underline">{{ .Repo.Name }}</a> 89 - {{ else }} 90 - created 91 - <a href="/{{ index $.DidHandleMap .Repo.Did }}/{{ .Repo.Name }}" class="no-underline hover:underline">{{ .Repo.Name }}</a> 92 - {{ end }} 93 - <time class="text-gray-700 dark:text-gray-400 text-xs ml-2">{{ .Repo.Created | shortTimeFmt }}</time> 94 - </p> 95 </div> 96 </div> 97 - {{ end }} 98 - {{ else }} 99 - <p class="px-6 dark:text-white">This user does not have any activity yet.</p> 100 {{ end }} 101 </div> 102 {{ end }} 103 104 {{ define "profileCard" }}
··· 9 {{ block "ownRepos" . }}{{ end }} 10 {{ block "collaboratingRepos" . }}{{ end }} 11 </div> 12 <div class="md:col-span-2 order-3 md:order-3"> 13 {{ block "profileTimeline" . }}{{ end }} 14 </div> 15 </div> 16 {{ end }} 17 18 + {{ define "profileTimeline" }} 19 + <p class="text-sm font-bold py-2 dark:text-white px-6">ACTIVITY</p> 20 + <div class="flex flex-col gap-6 relative"> 21 + {{ with .ProfileTimeline }} 22 + {{ range $idx, $byMonth := .ByMonth }} 23 + {{ with $byMonth }} 24 + <div class="bg-white dark:bg-gray-800 px-6 py-4 rounded drop-shadow-sm"> 25 + {{ if eq $idx 0 }} 26 + 27 + {{ else }} 28 + {{ $s := "s" }} 29 + {{ if eq $idx 1 }} 30 + {{ $s = "" }} 31 + {{ end }} 32 + <p class="text-sm font-bold dark:text-white mb-2">{{$idx}} month{{$s}} ago</p> 33 + {{ end }} 34 35 + {{ if .IsEmpty }} 36 + <div class="text-gray-500 dark:text-gray-400"> 37 + No activity for this month 38 </div> 39 + {{ else }} 40 + <div class="flex flex-col gap-1"> 41 + {{ block "repoEvents" (list .RepoEvents $.DidHandleMap) }} {{ end }} 42 + {{ block "issueEvents" (list .IssueEvents $.DidHandleMap) }} {{ end }} 43 + {{ block "pullEvents" (list .PullEvents $.DidHandleMap) }} {{ end }} 44 </div> 45 + {{ end }} 46 + </div> 47 + 48 + {{ end }} 49 + {{ else }} 50 + <p class="dark:text-white">This user does not have any activity yet.</p> 51 + {{ end }} 52 + {{ end }} 53 + </div> 54 + {{ end }} 55 + 56 + {{ define "repoEvents" }} 57 + {{ $items := index . 0 }} 58 + {{ $handleMap := index . 1 }} 59 + 60 + {{ if gt (len $items) 0 }} 61 + <details> 62 + <summary class="list-none cursor-pointer hover:text-gray-500 hover:dark:text-gray-400"> 63 + <div class="flex flex-wrap items-center gap-2"> 64 + {{ i "book-plus" "w-4 h-4" }} 65 + created {{ len $items }} {{if eq (len $items) 1 }}repository{{else}}repositories{{end}} 66 + </div> 67 + </summary> 68 + <div class="py-2 text-sm flex flex-col gap-3 mb-2"> 69 + {{ range $items }} 70 + <div class="flex flex-wrap items-center gap-2"> 71 + <span class="text-gray-500 dark:text-gray-400"> 72 + {{ if .Source }} 73 + {{ i "git-fork" "w-4 h-4" }} 74 + {{ else }} 75 + {{ i "book-plus" "w-4 h-4" }} 76 + {{ end }} 77 + </span> 78 + <a href="/{{ index $handleMap .Repo.Did }}/{{ .Repo.Name }}" class="no-underline hover:underline"> 79 + {{- .Repo.Name -}} 80 + </a> 81 </div> 82 + {{ end }} 83 + </div> 84 + </details> 85 + {{ end }} 86 + {{ end }} 87 + 88 + {{ define "issueEvents" }} 89 + {{ $i := index . 0 }} 90 + {{ $items := $i.Items }} 91 + {{ $stats := $i.Stats }} 92 + {{ $handleMap := index . 1 }} 93 + 94 + {{ if gt (len $items) 0 }} 95 + <details> 96 + <summary class="list-none cursor-pointer hover:text-gray-500 hover:dark:text-gray-400"> 97 + <div class="flex flex-wrap items-center gap-2"> 98 + {{ i "circle-dot" "w-4 h-4" }} 99 + 100 + <div> 101 + created {{ len $items }} {{if eq (len $items) 1 }}issue{{else}}issues{{end}} 102 + </div> 103 + 104 + {{ if gt $stats.Open 0 }} 105 + <span class="px-2 py-1/2 text-sm rounded text-white bg-green-600 dark:bg-green-700"> 106 + {{$stats.Open}} open 107 + </span> 108 + {{ end }} 109 + 110 + {{ if gt $stats.Closed 0 }} 111 + <span class="px-2 py-1/2 text-sm rounded text-white bg-gray-800 dark:bg-gray-700"> 112 + {{$stats.Closed}} closed 113 + </span> 114 + {{ end }} 115 + 116 + </div> 117 + </summary> 118 + <div class="py-2 text-sm flex flex-col gap-3 mb-2"> 119 + {{ range $items }} 120 + {{ $repoOwner := index $handleMap .Metadata.Repo.Did }} 121 + {{ $repoName := .Metadata.Repo.Name }} 122 + {{ $repoUrl := printf "%s/%s" $repoOwner $repoName }} 123 + 124 + <div class="flex gap-2 text-gray-600 dark:text-gray-300"> 125 + {{ if .Open }} 126 + <span class="text-green-600 dark:text-green-500"> 127 + {{ i "circle-dot" "w-4 h-4" }} 128 + </span> 129 + {{ else }} 130 + <span class="text-gray-500 dark:text-gray-400"> 131 + {{ i "ban" "w-4 h-4" }} 132 + </span> 133 {{ end }} 134 + <div class="flex-none min-w-8 text-right"> 135 + <span class="text-gray-500 dark:text-gray-400">#{{ .IssueId }}</span> 136 </div> 137 + <div class="break-words max-w-full"> 138 + <a href="/{{$repoUrl}}/issues/{{ .IssueId }}" class="no-underline hover:underline"> 139 + {{ .Title -}} 140 + </a> 141 on 142 + <a href="/{{$repoUrl}}" class="no-underline hover:underline whitespace-nowrap"> 143 + {{$repoUrl}} 144 + </a> 145 </div> 146 </div> 147 + {{ end }} 148 + </div> 149 + </details> 150 + {{ end }} 151 + {{ end }} 152 + 153 + {{ define "pullEvents" }} 154 + {{ $i := index . 0 }} 155 + {{ $items := $i.Items }} 156 + {{ $stats := $i.Stats }} 157 + {{ $handleMap := index . 1 }} 158 + {{ if gt (len $items) 0 }} 159 + <details> 160 + <summary class="list-none cursor-pointer hover:text-gray-500 hover:dark:text-gray-400"> 161 + <div class="flex flex-wrap items-center gap-2"> 162 + {{ i "git-pull-request" "w-4 h-4" }} 163 + 164 + <div> 165 + created {{ len $items }} {{if eq (len $items) 1 }}pull request{{else}}pull requests{{end}} 166 + </div> 167 + 168 + {{ if gt $stats.Open 0 }} 169 + <span class="px-2 py-1/2 text-sm rounded text-white bg-green-600 dark:bg-green-700"> 170 + {{$stats.Open}} open 171 + </span> 172 + {{ end }} 173 + 174 + {{ if gt $stats.Merged 0 }} 175 + <span class="px-2 py-1/2 text-sm rounded text-white bg-purple-600 dark:bg-purple-700"> 176 + {{$stats.Merged}} merged 177 + </span> 178 + {{ end }} 179 + 180 + 181 + {{ if gt $stats.Closed 0 }} 182 + <span class="px-2 py-1/2 text-sm rounded text-black dark:text-white bg-gray-50 dark:bg-gray-700 "> 183 + {{$stats.Closed}} closed 184 + </span> 185 + {{ end }} 186 + 187 + </div> 188 + </summary> 189 + <div class="py-2 text-sm flex flex-col gap-3 mb-2"> 190 + {{ range $items }} 191 + {{ $repoOwner := index $handleMap .Repo.Did }} 192 + {{ $repoName := .Repo.Name }} 193 + {{ $repoUrl := printf "%s/%s" $repoOwner $repoName }} 194 + 195 + <div class="flex gap-2 text-gray-600 dark:text-gray-300"> 196 + {{ if .State.IsOpen }} 197 + <span class="text-green-600 dark:text-green-500"> 198 + {{ i "git-pull-request" "w-4 h-4" }} 199 + </span> 200 + {{ else if .State.IsMerged }} 201 + <span class="text-purple-600 dark:text-purple-500"> 202 + {{ i "git-merge" "w-4 h-4" }} 203 + </span> 204 {{ else }} 205 + <span class="text-gray-600 dark:text-gray-300"> 206 + {{ i "git-pull-request-closed" "w-4 h-4" }} 207 + </span> 208 + {{ end }} 209 + <div class="flex-none min-w-8 text-right"> 210 + <span class="text-gray-500 dark:text-gray-400">#{{ .PullId }}</span> 211 </div> 212 + <div class="break-words max-w-full"> 213 + <a href="/{{$repoUrl}}/pulls/{{ .PullId }}" class="no-underline hover:underline"> 214 + {{ .Title -}} 215 + </a> 216 + on 217 + <a href="/{{$repoUrl}}" class="no-underline hover:underline whitespace-nowrap"> 218 + {{$repoUrl}} 219 + </a> 220 </div> 221 </div> 222 {{ end }} 223 </div> 224 + </details> 225 + {{ end }} 226 {{ end }} 227 228 {{ define "profileCard" }}
+11 -5
appview/state/profile.go
··· 43 for _, r := range collaboratingRepos { 44 didsToResolve = append(didsToResolve, r.Did) 45 } 46 - for _, evt := range timeline { 47 - if evt.Repo != nil { 48 - if evt.Repo.Source != "" { 49 - didsToResolve = append(didsToResolve, evt.Source.Did) 50 } 51 } 52 - didsToResolve = append(didsToResolve, evt.Repo.Did) 53 } 54 55 resolvedIds := s.resolver.ResolveIdents(r.Context(), didsToResolve)
··· 43 for _, r := range collaboratingRepos { 44 didsToResolve = append(didsToResolve, r.Did) 45 } 46 + for _, byMonth := range timeline.ByMonth { 47 + for _, pe := range byMonth.PullEvents.Items { 48 + didsToResolve = append(didsToResolve, pe.Repo.Did) 49 + } 50 + for _, ie := range byMonth.IssueEvents.Items { 51 + didsToResolve = append(didsToResolve, ie.Metadata.Repo.Did) 52 + } 53 + for _, re := range byMonth.RepoEvents { 54 + didsToResolve = append(didsToResolve, re.Repo.Did) 55 + if re.Source != nil { 56 + didsToResolve = append(didsToResolve, re.Source.Did) 57 } 58 } 59 } 60 61 resolvedIds := s.resolver.ResolveIdents(r.Context(), didsToResolve)
+1 -1
go.mod
··· 26 github.com/sethvargo/go-envconfig v1.1.0 27 github.com/whyrusleeping/cbor-gen v0.2.1-0.20241030202151-b7a6831be65e 28 github.com/yuin/goldmark v1.4.13 29 - golang.org/x/crypto v0.36.0 30 golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 31 ) 32 ··· 107 go.uber.org/atomic v1.11.0 // indirect 108 go.uber.org/multierr v1.11.0 // indirect 109 go.uber.org/zap v1.26.0 // indirect 110 golang.org/x/net v0.37.0 // indirect 111 golang.org/x/sys v0.31.0 // indirect 112 golang.org/x/time v0.5.0 // indirect
··· 26 github.com/sethvargo/go-envconfig v1.1.0 27 github.com/whyrusleeping/cbor-gen v0.2.1-0.20241030202151-b7a6831be65e 28 github.com/yuin/goldmark v1.4.13 29 golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 30 ) 31 ··· 106 go.uber.org/atomic v1.11.0 // indirect 107 go.uber.org/multierr v1.11.0 // indirect 108 go.uber.org/zap v1.26.0 // indirect 109 + golang.org/x/crypto v0.36.0 // indirect 110 golang.org/x/net v0.37.0 // indirect 111 golang.org/x/sys v0.31.0 // indirect 112 golang.org/x/time v0.5.0 // indirect