+14
-13
appview/models/label.go
+14
-13
appview/models/label.go
···
461
461
return result
462
462
}
463
463
464
+
var (
465
+
LabelWontfix = fmt.Sprintf("at://%s/%s/%s", consts.TangledDid, tangled.LabelDefinitionNSID, "wontfix")
466
+
LabelDuplicate = fmt.Sprintf("at://%s/%s/%s", consts.TangledDid, tangled.LabelDefinitionNSID, "duplicate")
467
+
LabelAssignee = fmt.Sprintf("at://%s/%s/%s", consts.TangledDid, tangled.LabelDefinitionNSID, "assignee")
468
+
LabelGoodFirstIssue = fmt.Sprintf("at://%s/%s/%s", consts.TangledDid, tangled.LabelDefinitionNSID, "good-first-issue")
469
+
LabelDocumentation = fmt.Sprintf("at://%s/%s/%s", consts.TangledDid, tangled.LabelDefinitionNSID, "documentation")
470
+
)
471
+
464
472
func DefaultLabelDefs() []string {
465
-
rkeys := []string{
466
-
"wontfix",
467
-
"duplicate",
468
-
"assignee",
469
-
"good-first-issue",
470
-
"documentation",
473
+
return []string{
474
+
LabelWontfix,
475
+
LabelDuplicate,
476
+
LabelAssignee,
477
+
LabelGoodFirstIssue,
478
+
LabelDocumentation,
471
479
}
472
-
473
-
defs := make([]string, len(rkeys))
474
-
for i, r := range rkeys {
475
-
defs[i] = fmt.Sprintf("at://%s/%s/%s", consts.TangledDid, tangled.LabelDefinitionNSID, r)
476
-
}
477
-
478
-
return defs
479
480
}
480
481
481
482
func FetchDefaultDefs(r *idresolver.Resolver) ([]LabelDefinition, error) {
+2
appview/pages/pages.go
+2
appview/pages/pages.go
···
306
306
LoggedInUser *oauth.User
307
307
Timeline []models.TimelineEvent
308
308
Repos []models.Repo
309
+
GfiLabel *models.LabelDefinition
309
310
}
310
311
311
312
func (p *Pages) Timeline(w io.Writer, params TimelineParams) error {
···
317
318
Issues []models.Issue
318
319
RepoGroups []*models.RepoGroup
319
320
LabelDefs map[string]*models.LabelDefinition
321
+
GfiLabel *models.LabelDefinition
320
322
Page pagination.Page
321
323
}
322
324
+50
-62
appview/pages/templates/goodfirstissues/index.html
+50
-62
appview/pages/templates/goodfirstissues/index.html
···
9
9
10
10
{{ define "content" }}
11
11
<div class="grid grid-cols-10">
12
-
<header class="col-span-full md:col-span-10 px-6 py-2 mb-4">
13
-
<h1 class="text-2xl font-bold dark:text-white mb-1">Good First Issues</h1>
12
+
<header class="col-span-full md:col-span-10 px-6 py-2 text-center flex flex-col items-center justify-center py-8">
13
+
<h1 class="scale-150 dark:text-white mb-4">
14
+
{{ template "labels/fragments/label" (dict "def" .GfiLabel "val" "" "withPrefix" true) }}
15
+
</h1>
14
16
<p class="text-gray-600 dark:text-gray-400 mb-2">
15
17
Find beginner-friendly issues across all repositories to get started with open source contributions.
16
18
</p>
···
35
37
{{ else }}
36
38
{{ range .RepoGroups }}
37
39
<div class="mb-4 gap-1 flex flex-col drop-shadow-sm rounded bg-white dark:bg-gray-800">
38
-
<div class="flex px-6 pt-4 pb-2 flex-row gap-1">
39
-
<div class="font-medium dark:text-white flex items-center justify-between">
40
-
<div class="flex items-center min-w-0 flex-1 mr-2">
41
-
{{ if .Repo.Source }}
42
-
{{ i "git-fork" "w-4 h-4 mr-1.5 shrink-0" }}
43
-
{{ else }}
44
-
{{ i "book-marked" "w-4 h-4 mr-1.5 shrink-0" }}
45
-
{{ end }}
46
-
{{ $repoOwner := resolve .Repo.Did }}
47
-
<a href="/{{ $repoOwner }}/{{ .Repo.Name }}" class="truncate min-w-0">{{ $repoOwner }}/{{ .Repo.Name }}</a>
40
+
<div class="flex px-6 pt-4 flex-row gap-1 items-center justify-between">
41
+
<div class="font-medium dark:text-white flex items-center justify-between">
42
+
<div class="flex items-center min-w-0 flex-1 mr-2">
43
+
{{ if .Repo.Source }}
44
+
{{ i "git-fork" "w-4 h-4 mr-1.5 shrink-0" }}
45
+
{{ else }}
46
+
{{ i "book-marked" "w-4 h-4 mr-1.5 shrink-0" }}
47
+
{{ end }}
48
+
{{ $repoOwner := resolve .Repo.Did }}
49
+
<a href="/{{ $repoOwner }}/{{ .Repo.Name }}" class="truncate min-w-0">{{ $repoOwner }}/{{ .Repo.Name }}</a>
50
+
</div>
48
51
</div>
49
-
</div>
50
52
51
53
52
54
{{ if .Repo.RepoStats }}
53
-
<div class="text-gray-400 text-sm font-mono inline-flex gap-4 mt-auto">
55
+
<div class="text-gray-400 text-sm font-mono inline-flex gap-4">
54
56
{{ with .Repo.RepoStats.Language }}
55
57
<div class="flex gap-2 items-center text-sm">
56
58
{{ template "repo/fragments/colorBall" (dict "color" (langColor .)) }}
···
86
88
{{ end }}
87
89
88
90
{{ if gt (len .Issues) 0 }}
89
-
<details class="bg-white dark:bg-gray-800 group" open>
90
-
<summary class="py-4 px-6 text-xs list-none cursor-pointer hover:text-gray-500 hover:dark:text-gray-400">
91
-
{{ $s := "s" }}
92
-
{{ if eq (len .Issues) 1 }}
93
-
{{ $s = "" }}
94
-
{{ end }}
95
-
<div class="group-open:hidden flex items-center gap-2">
96
-
{{ i "chevrons-up-down" "w-4 h-4" }} expand {{ len .Issues }} issue{{$s}} in this repo
97
-
</div>
98
-
<div class="hidden group-open:flex items-center gap-2">
99
-
{{ i "chevrons-down-up" "w-4 h-4" }} hide {{ len .Issues }} issue{{$s}} in this repo
100
-
</div>
101
-
</summary>
102
-
<div class="grid grid-cols-1 rounded-b border-b border-t border-gray-200 dark:border-gray-900 divide-y divide-gray-200 dark:divide-gray-900">
103
-
{{ range .Issues }}
104
-
<a href="/{{ resolve .Repo.Did }}/{{ .Repo.Name }}/issues/{{ .IssueId }}" class="no-underline hover:no-underline hover:bg-gray-100/25 hover:dark:bg-gray-700/25">
105
-
<div class="py-2 px-6">
106
-
<div class="flex-grow min-w-0 w-full">
107
-
<div class="flex text-sm items-center justify-between w-full">
108
-
<div class="flex items-center gap-2 min-w-0 flex-1 pr-2">
109
-
<span class="truncate text-sm text-gray-800 dark:text-gray-200">
110
-
<span class="text-gray-500 dark:text-gray-400">#{{ .IssueId }}</span>
111
-
{{ .Title | description }}
112
-
</span>
113
-
</div>
114
-
<div class="flex-shrink-0 flex items-center gap-2 text-gray-500 dark:text-gray-400">
115
-
<span>
116
-
<div class="inline-flex items-center gap-1">
117
-
{{ i "message-square" "w-3 h-3 md:hidden" }}
118
-
{{ len .Comments }}
119
-
<span class="hidden md:inline">comment{{ if ne (len .Comments) 1 }}s{{ end }}</span>
120
-
</div>
121
-
</span>
122
-
<span class="before:content-['·'] before:select-none"></span>
123
-
<span class="text-xs">
124
-
{{ template "repo/fragments/time" .Created }}
125
-
</span>
126
-
<div class="hidden md:inline-flex md:gap-1">
127
-
{{ $labelState := .Labels }}
128
-
{{ range $k, $d := $.LabelDefs }}
129
-
{{ range $v, $s := $labelState.GetValSet $d.AtUri.String }}
130
-
{{ template "labels/fragments/label" (dict "def" $d "val" $v "withPrefix" true) }}
131
-
{{ end }}
132
-
{{ end }}
91
+
<div class="grid grid-cols-1 rounded-b border-b border-t border-gray-200 dark:border-gray-900 divide-y divide-gray-200 dark:divide-gray-900">
92
+
{{ range .Issues }}
93
+
<a href="/{{ resolve .Repo.Did }}/{{ .Repo.Name }}/issues/{{ .IssueId }}" class="no-underline hover:no-underline hover:bg-gray-100/25 hover:dark:bg-gray-700/25">
94
+
<div class="py-2 px-6">
95
+
<div class="flex-grow min-w-0 w-full">
96
+
<div class="flex text-sm items-center justify-between w-full">
97
+
<div class="flex items-center gap-2 min-w-0 flex-1 pr-2">
98
+
<span class="truncate text-sm text-gray-800 dark:text-gray-200">
99
+
<span class="text-gray-500 dark:text-gray-400">#{{ .IssueId }}</span>
100
+
{{ .Title | description }}
101
+
</span>
102
+
</div>
103
+
<div class="flex-shrink-0 flex items-center gap-2 text-gray-500 dark:text-gray-400">
104
+
<span>
105
+
<div class="inline-flex items-center gap-1">
106
+
{{ i "message-square" "w-3 h-3 md:hidden" }}
107
+
{{ len .Comments }}
108
+
<span class="hidden md:inline">comment{{ if ne (len .Comments) 1 }}s{{ end }}</span>
133
109
</div>
110
+
</span>
111
+
<span class="before:content-['·'] before:select-none"></span>
112
+
<span class="text-sm">
113
+
{{ template "repo/fragments/time" .Created }}
114
+
</span>
115
+
<div class="hidden md:inline-flex md:gap-1">
116
+
{{ $labelState := .Labels }}
117
+
{{ range $k, $d := $.LabelDefs }}
118
+
{{ range $v, $s := $labelState.GetValSet $d.AtUri.String }}
119
+
{{ template "labels/fragments/label" (dict "def" $d "val" $v "withPrefix" true) }}
120
+
{{ end }}
121
+
{{ end }}
134
122
</div>
135
123
</div>
136
124
</div>
137
125
</div>
138
-
</a>
139
-
{{ end }}
140
-
</div>
141
-
</details>
126
+
</div>
127
+
</a>
128
+
{{ end }}
129
+
</div>
142
130
{{ end }}
143
131
</div>
144
132
{{ end }}
+1
-1
appview/pages/templates/labels/fragments/label.html
+1
-1
appview/pages/templates/labels/fragments/label.html
···
2
2
{{ $d := .def }}
3
3
{{ $v := .val }}
4
4
{{ $withPrefix := .withPrefix }}
5
-
<span class="flex items-center gap-2 font-normal normal-case rounded py-1 px-2 border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 text-sm">
5
+
<span class="w-fit flex items-center gap-2 font-normal normal-case rounded py-1 px-2 border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 text-sm">
6
6
{{ template "repo/fragments/colorBall" (dict "color" $d.GetColor) }}
7
7
8
8
{{ $lhs := printf "%s" $d.Name }}
+29
-27
appview/pages/templates/timeline/fragments/goodfirstissues.html
+29
-27
appview/pages/templates/timeline/fragments/goodfirstissues.html
···
1
1
{{ define "timeline/fragments/goodfirstissues" }}
2
-
<div class="bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-sm mb-4">
3
-
<div class="px-6 py-4">
4
-
<div class="flex flex-col md:flex-row items-center gap-4">
5
-
<div class="flex-1">
6
-
<div class="flex items-center gap-2 mb-2">
7
-
<span class="text-sm text-gray-500 dark:text-gray-400">Oct–Nov 2025</span>
8
-
</div>
9
-
<h4 class="text-base font-semibold text-gray-900 dark:text-white mb-2">
10
-
Get your first PR merged and earn Dolly stickers! 🎉
11
-
</h4>
12
-
<p class="text-sm text-gray-600 dark:text-gray-300 mb-3">
13
-
Merge a PR for "good first issue" (make this a label) a and get Tangled stickers shipped free.
14
-
</p>
15
-
<a href="/goodfirstissues"
16
-
class="btn my-2 gap-2">
17
-
browse issues
18
-
{{ i "arrow-right" "size-4" }}
19
-
</a>
20
-
</div>
21
-
<div class="flex-shrink-0">
22
-
<div class="flex items-center gap-1 p-3 bg-gradient-to-r from-purple-50 to-pink-50 dark:from-purple-900/20 dark:to-pink-900/20 rounded border border-purple-200 dark:border-purple-700">
23
-
{{ template "fragments/dolly/logo" "w-6 h-6" }}
24
-
{{ template "fragments/dolly/silhouette" }}
25
-
<span class="text-xs text-purple-600 dark:text-purple-400 font-medium">Free stickers!</span>
26
-
</div>
27
-
</div>
2
+
{{ if .GfiLabel }}
3
+
<a href="/goodfirstissues" class="no-underline hover:no-underline">
4
+
<div class="flex items-center justify-between gap-2 bg-purple-200 dark:bg-purple-900 border border-purple-400 dark:border-purple-500 rounded mb-4 py-4 px-6 ">
5
+
<div class="flex-1 flex flex-col gap-2">
6
+
<div class="text-purple-500 dark:text-purple-400">Oct 2025</div>
7
+
<p>
8
+
Make your first contribution to an open-source project this October.
9
+
</p>
10
+
<p>
11
+
<em>good-first-issue</em> is a collection of issues on open-source projects
12
+
that are easy ways for new contributors to give back to the projects
13
+
they love.
14
+
</p>
15
+
<span class="flex items-center gap-2 text-purple-500 dark:text-purple-400">
16
+
Browse issues {{ i "arrow-right" "size-4" }}
17
+
</span>
18
+
</div>
19
+
<div class="hidden md:block relative px-16 scale-150">
20
+
<div class="relative opacity-60">
21
+
{{ template "labels/fragments/label" (dict "def" .GfiLabel "val" "" "withPrefix" true) }}
22
+
</div>
23
+
<div class="relative -mt-4 ml-2 opacity-80">
24
+
{{ template "labels/fragments/label" (dict "def" .GfiLabel "val" "" "withPrefix" true) }}
28
25
</div>
26
+
<div class="relative -mt-4 ml-4">
27
+
{{ template "labels/fragments/label" (dict "def" .GfiLabel "val" "" "withPrefix" true) }}
28
+
</div>
29
+
</div>
29
30
</div>
30
-
</div>
31
+
</a>
32
+
{{ end }}
31
33
{{ end }}
+5
-4
appview/state/gfi.go
+5
-4
appview/state/gfi.go
···
6
6
"net/http"
7
7
"sort"
8
8
9
+
"github.com/bluesky-social/indigo/atproto/syntax"
9
10
"tangled.org/core/api/tangled"
10
11
"tangled.org/core/appview/db"
11
12
"tangled.org/core/appview/models"
···
64
65
}
65
66
}
66
67
67
-
repoGroups := make(map[string]*models.RepoGroup)
68
+
repoGroups := make(map[syntax.ATURI]*models.RepoGroup)
68
69
for _, issue := range goodFirstIssues {
69
-
repoKey := fmt.Sprintf("%s/%s", issue.Repo.Did, issue.Repo.Name)
70
-
if group, exists := repoGroups[repoKey]; exists {
70
+
if group, exists := repoGroups[issue.Repo.RepoAt()]; exists {
71
71
group.Issues = append(group.Issues, issue)
72
72
} else {
73
-
repoGroups[repoKey] = &models.RepoGroup{
73
+
repoGroups[issue.Repo.RepoAt()] = &models.RepoGroup{
74
74
Repo: issue.Repo,
75
75
Issues: []models.Issue{issue},
76
76
}
···
134
134
RepoGroups: paginatedGroups,
135
135
LabelDefs: labelDefsMap,
136
136
Page: page,
137
+
GfiLabel: labelDefsMap[goodFirstIssueLabel],
137
138
})
138
139
}
+8
-2
appview/state/state.go
+8
-2
appview/state/state.go
···
270
270
return
271
271
}
272
272
273
-
s.pages.Timeline(w, pages.TimelineParams{
273
+
gfiLabel, err := db.GetLabelDefinition(s.db, db.FilterEq("at_uri", models.LabelGoodFirstIssue))
274
+
if err != nil {
275
+
// non-fatal
276
+
}
277
+
278
+
fmt.Println(s.pages.Timeline(w, pages.TimelineParams{
274
279
LoggedInUser: user,
275
280
Timeline: timeline,
276
281
Repos: repos,
277
-
})
282
+
GfiLabel: gfiLabel,
283
+
}))
278
284
}
279
285
280
286
func (s *State) UpgradeBanner(w http.ResponseWriter, r *http.Request) {