Signed-off-by: Anirudh Oppiliappan anirudh@tangled.sh
+4
appview/db/db.go
+4
appview/db/db.go
···
470
470
id integer primary key autoincrement,
471
471
name text unique
472
472
);
473
+
474
+
-- indexes for better star query performance
475
+
create index if not exists idx_stars_created on stars(created);
476
+
create index if not exists idx_stars_repo_at_created on stars(repo_at, created);
473
477
`)
474
478
if err != nil {
475
479
return nil, err
+73
-3
appview/db/star.go
+73
-3
appview/db/star.go
···
47
47
// Get a star record
48
48
func GetStar(e Execer, starredByDid string, repoAt syntax.ATURI) (*Star, error) {
49
49
query := `
50
-
select starred_by_did, repo_at, created, rkey
50
+
select starred_by_did, repo_at, created, rkey
51
51
from stars
52
52
where starred_by_did = ? and repo_at = ?`
53
53
row := e.QueryRow(query, starredByDid, repoAt)
···
119
119
}
120
120
121
121
repoQuery := fmt.Sprintf(
122
-
`select starred_by_did, repo_at, created, rkey
122
+
`select starred_by_did, repo_at, created, rkey
123
123
from stars
124
124
%s
125
125
order by created desc
···
187
187
var stars []Star
188
188
189
189
rows, err := e.Query(`
190
-
select
190
+
select
191
191
s.starred_by_did,
192
192
s.repo_at,
193
193
s.rkey,
···
244
244
245
245
return stars, nil
246
246
}
247
+
248
+
// GetTopStarredReposLastWeek returns the top 8 most starred repositories from the last week
249
+
func GetTopStarredReposLastWeek(e Execer) ([]Repo, error) {
250
+
// first, get the top repo URIs by star count from the last week
251
+
query := `
252
+
with recent_starred_repos as (
253
+
select distinct repo_at
254
+
from stars
255
+
where created >= datetime('now', '-7 days')
256
+
),
257
+
repo_star_counts as (
258
+
select
259
+
s.repo_at,
260
+
count(*) as star_count
261
+
from stars s
262
+
join recent_starred_repos rsr on s.repo_at = rsr.repo_at
263
+
where s.created >= datetime('now', '-7 days')
264
+
group by s.repo_at
265
+
)
266
+
select rsc.repo_at
267
+
from repo_star_counts rsc
268
+
order by rsc.star_count desc
269
+
limit 8
270
+
`
271
+
272
+
rows, err := e.Query(query)
273
+
if err != nil {
274
+
return nil, err
275
+
}
276
+
defer rows.Close()
277
+
278
+
var repoUris []string
279
+
for rows.Next() {
280
+
var repoUri string
281
+
err := rows.Scan(&repoUri)
282
+
if err != nil {
283
+
return nil, err
284
+
}
285
+
repoUris = append(repoUris, repoUri)
286
+
}
287
+
288
+
if err := rows.Err(); err != nil {
289
+
return nil, err
290
+
}
291
+
292
+
if len(repoUris) == 0 {
293
+
return []Repo{}, nil
294
+
}
295
+
296
+
// get full repo data
297
+
repos, err := GetRepos(e, 0, FilterIn("at_uri", repoUris))
298
+
if err != nil {
299
+
return nil, err
300
+
}
301
+
302
+
// sort repos by the original trending order
303
+
repoMap := make(map[string]Repo)
304
+
for _, repo := range repos {
305
+
repoMap[repo.RepoAt().String()] = repo
306
+
}
307
+
308
+
orderedRepos := make([]Repo, 0, len(repoUris))
309
+
for _, uri := range repoUris {
310
+
if repo, exists := repoMap[uri]; exists {
311
+
orderedRepos = append(orderedRepos, repo)
312
+
}
313
+
}
314
+
315
+
return orderedRepos, nil
316
+
}
+10
-1
appview/pages/pages.go
+10
-1
appview/pages/pages.go
···
302
302
}
303
303
304
304
func (p *Pages) Timeline(w io.Writer, params TimelineParams) error {
305
-
return p.execute("timeline", w, params)
305
+
return p.execute("timeline/timeline", w, params)
306
+
}
307
+
308
+
type TopStarredReposLastWeekParams struct {
309
+
LoggedInUser *oauth.User
310
+
Repos []db.Repo
311
+
}
312
+
313
+
func (p *Pages) TopStarredReposLastWeek(w io.Writer, params TopStarredReposLastWeekParams) error {
314
+
return p.executePlain("timeline/fragments/topStarredRepos", w, params)
306
315
}
307
316
308
317
type SettingsParams struct {
+32
-9
appview/pages/templates/layouts/base.html
+32
-9
appview/pages/templates/layouts/base.html
···
16
16
</head>
17
17
<body class="min-h-screen grid grid-cols-1 grid-rows-[min-content_auto_min-content] md:grid-cols-12 gap-4 bg-slate-100 dark:bg-gray-900 dark:text-white transition-colors duration-200">
18
18
{{ block "topbarLayout" . }}
19
-
<header class="px-1 col-span-1 md:col-start-3 md:col-span-8" style="z-index: 20;">
19
+
<header class="px-1 col-span-full" style="z-index: 20;">
20
20
{{ template "layouts/topbar" . }}
21
21
</header>
22
22
{{ end }}
23
23
24
24
{{ block "mainLayout" . }}
25
+
<!-- Mobile trending carousel at top - full width -->
26
+
<div class="px-1 col-span-full lg:hidden">
27
+
{{ block "contentRight" . }} {{ end }}
28
+
</div>
29
+
25
30
<div class="px-1 col-span-1 md:col-start-3 md:col-span-8 flex flex-col gap-4">
31
+
26
32
{{ block "contentLayout" . }}
27
-
<main class="col-span-1 md:col-span-8">
28
-
{{ block "content" . }}{{ end }}
29
-
</main>
33
+
<div class="grid grid-cols-1 lg:grid-cols-12 gap-4">
34
+
<div class="lg:col-span-2">
35
+
{{ block "contentLeft" . }} {{ end }}
36
+
</div>
37
+
<main class="lg:col-span-8">
38
+
{{ block "content" . }}{{ end }}
39
+
</main>
40
+
<!-- Desktop trending sidebar -->
41
+
<div class="hidden lg:block lg:col-span-2">
42
+
{{ block "contentRight" . }} {{ end }}
43
+
</div>
44
+
</div>
30
45
{{ end }}
31
-
46
+
32
47
{{ block "contentAfterLayout" . }}
33
-
<main class="col-span-1 md:col-span-8">
34
-
{{ block "contentAfter" . }}{{ end }}
35
-
</main>
48
+
<div class="grid grid-cols-1 lg:grid-cols-12 gap-4">
49
+
<div class="lg:col-span-2">
50
+
{{ block "contentAfterLeft" . }} {{ end }}
51
+
</div>
52
+
<main class="lg:col-span-8">
53
+
{{ block "contentAfter" . }}{{ end }}
54
+
</main>
55
+
<div class="lg:col-span-2">
56
+
{{ block "contentAfterRight" . }} {{ end }}
57
+
</div>
58
+
</div>
36
59
{{ end }}
37
60
</div>
38
61
{{ end }}
39
62
40
63
{{ block "footerLayout" . }}
41
-
<footer class="px-1 col-span-1 md:col-start-3 md:col-span-8 mt-12">
64
+
<footer class="px-1 col-span-full mt-12">
42
65
{{ template "layouts/footer" . }}
43
66
</footer>
44
67
{{ end }}
+65
appview/pages/templates/timeline/fragments/topStarredRepos.html
+65
appview/pages/templates/timeline/fragments/topStarredRepos.html
···
1
+
{{ define "timeline/fragments/topStarredRepos" }}
2
+
<div>
3
+
<!-- Mobile: Horizontal carousel -->
4
+
<div class="lg:hidden w-full">
5
+
<div class="p-4">
6
+
<h3 class="font-bold text-lg text-gray-900 dark:text-white flex items-center gap-2 mb-3">
7
+
{{ i "trending-up" "size-4" }}
8
+
Trending
9
+
</h3>
10
+
</div>
11
+
<div class="flex gap-3 overflow-x-auto pb-4 px-4 scrollbar-hide">
12
+
{{ range $index, $repo := .Repos }}
13
+
<div class="flex-none w-72 relative">
14
+
<!-- Small background number for mobile -->
15
+
<div class="absolute left-2 top-1 text-6xl font-black text-gray-200 dark:text-gray-700 leading-none select-none z-0">
16
+
{{ add $index 1 }}
17
+
</div>
18
+
<!-- Card above the number -->
19
+
<div class="relative z-10 ml-8 min-w-40 border border-gray-200 dark:border-gray-700 rounded-sm">
20
+
{{ template "user/fragments/repoCard" (list $ $repo true) }}
21
+
</div>
22
+
</div>
23
+
{{ else }}
24
+
<div class="py-8 px-6 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-sm">
25
+
<div class="text-sm text-gray-500 dark:text-gray-400 text-center">
26
+
No trending repositories this week
27
+
</div>
28
+
</div>
29
+
{{ end }}
30
+
</div>
31
+
</div>
32
+
33
+
<!-- Desktop: Vertical stack -->
34
+
<div class="hidden lg:block">
35
+
<div class="p-6">
36
+
<h3 class="font-bold text-xl text-gray-900 dark:text-white flex items-center gap-2">
37
+
{{ i "trending-up" "size-5" }}
38
+
Trending
39
+
</h3>
40
+
</div>
41
+
42
+
<div class="flex flex-col gap-4">
43
+
{{ range $index, $repo := .Repos }}
44
+
<div class="relative">
45
+
<!-- Large background number -->
46
+
<div class="absolute left-4 top-2 text-[120px] font-black text-gray-200 dark:text-gray-700 leading-none select-none z-0">
47
+
{{ add $index 1 }}
48
+
</div>
49
+
50
+
<!-- Card above the number -->
51
+
<div class="relative z-10 ml-16 min-w-80 lg:max-w-full border border-gray-200 dark:border-gray-700 rounded-sm">
52
+
{{ template "user/fragments/repoCard" (list $ $repo true) }}
53
+
</div>
54
+
</div>
55
+
{{ else }}
56
+
<div class="py-8 px-6 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-sm">
57
+
<div class="text-sm text-gray-500 dark:text-gray-400 text-center">
58
+
No trending repositories this week
59
+
</div>
60
+
</div>
61
+
{{ end }}
62
+
</div>
63
+
</div>
64
+
</div>
65
+
{{ end }}
+23
-4
appview/pages/templates/timeline.html
appview/pages/templates/timeline/timeline.html
+23
-4
appview/pages/templates/timeline.html
appview/pages/templates/timeline/timeline.html
···
4
4
<meta property="og:title" content="timeline · tangled" />
5
5
<meta property="og:type" content="object" />
6
6
<meta property="og:url" content="https://tangled.sh" />
7
-
<meta property="og:description" content="see what's tangling" />
7
+
<meta property="og:description" content="tightly-knit social coding" />
8
8
{{ end }}
9
9
10
-
{{ define "topbar" }}
11
-
{{ template "layouts/topbar" $ }}
12
-
{{ end }}
13
10
14
11
{{ define "content" }}
15
12
{{ with .LoggedInUser }}
···
20
17
{{ end }}
21
18
{{ end }}
22
19
20
+
{{ define "contentRight" }}
21
+
<div
22
+
hx-get="/timeline/trending"
23
+
hx-trigger="load"
24
+
hx-indicator="#starred-loading"
25
+
>
26
+
<!-- Loading spinner -->
27
+
<div id="starred-loading" class="htmx-indicator">
28
+
<div class="bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-sm p-4">
29
+
<h3 class="font-bold text-lg mb-4 text-gray-900 dark:text-white flex items-center gap-2">
30
+
{{ i "trending-up" "size-5" }}
31
+
Trending This Week
32
+
</h3>
33
+
<div class="flex items-center justify-center py-8">
34
+
<div class="animate-spin rounded-full h-6 w-6 border-b-2 border-gray-900 dark:border-white"></div>
35
+
<span class="ml-2 text-sm text-gray-500 dark:text-gray-400">Loading...</span>
36
+
</div>
37
+
</div>
38
+
</div>
39
+
</div>
40
+
{{ end }}
41
+
23
42
{{ define "hero" }}
24
43
<div class="flex flex-col text-black dark:text-white p-6 gap-6 max-w-xl">
25
44
<div class="font-bold text-4xl">tightly-knit<br>social coding.</div>
+1
appview/state/router.go
+1
appview/state/router.go
+13
appview/state/state.go
+13
appview/state/state.go
···
199
199
})
200
200
}
201
201
202
+
func (s *State) TopStarredReposLastWeek(w http.ResponseWriter, r *http.Request) {
203
+
repos, err := db.GetTopStarredReposLastWeek(s.db)
204
+
if err != nil {
205
+
log.Println(err)
206
+
s.pages.Notice(w, "topstarredrepos", "Unable to load.")
207
+
return
208
+
}
209
+
210
+
s.pages.TopStarredReposLastWeek(w, pages.TopStarredReposLastWeekParams{
211
+
Repos: repos,
212
+
})
213
+
}
214
+
202
215
func (s *State) Keys(w http.ResponseWriter, r *http.Request) {
203
216
user := chi.URLParam(r, "user")
204
217
user = strings.TrimPrefix(user, "@")