Signed-off-by: oppiliappan me@oppi.li
+4
appview/pages/funcmap.go
+4
appview/pages/funcmap.go
···
12
12
"html/template"
13
13
"log"
14
14
"math"
15
+
"math/rand"
15
16
"net/url"
16
17
"path/filepath"
17
18
"reflect"
···
124
125
"mod": func(a, b int) int {
125
126
return a % b
126
127
},
128
+
"randInt": func(bound int) int {
129
+
return rand.Intn(bound)
130
+
},
127
131
"f64": func(a int) float64 {
128
132
return float64(a)
129
133
},
+2
-2
appview/pages/templates/layouts/base.html
+2
-2
appview/pages/templates/layouts/base.html
···
42
42
<title>{{ block "title" . }}{{ end }}</title>
43
43
{{ block "extrameta" . }}{{ end }}
44
44
</head>
45
-
<body class="min-h-screen flex flex-col gap-4 bg-slate-100 dark:bg-gray-900 dark:text-white transition-colors duration-200">
45
+
<body class="min-h-screen flex flex-col gap-4 bg-slate-100 dark:bg-gray-900 dark:text-white transition-colors duration-200 {{ block "bodyClasses" . }} {{ end }}">
46
46
{{ block "topbarLayout" . }}
47
-
<header class="w-full col-span-full md:col-span-1 md:col-start-2" style="z-index: 20;">
47
+
<header class="w-full col-span-full md:col-span-1 md:col-start-2 drop-shadow-sm dark:text-white bg-white dark:bg-gray-800" style="z-index: 20;">
48
48
49
49
{{ if .LoggedInUser }}
50
50
<div id="upgrade-banner"
+1
-1
appview/pages/templates/layouts/fragments/topbar.html
+1
-1
appview/pages/templates/layouts/fragments/topbar.html
···
1
1
{{ define "layouts/fragments/topbar" }}
2
-
<nav class="mx-auto space-x-4 px-6 py-2 dark:text-white drop-shadow-sm bg-white dark:bg-gray-800">
2
+
<nav class="mx-auto space-x-4 px-6 py-2">
3
3
<div class="flex justify-between p-0 items-center">
4
4
<div id="left-items">
5
5
<a href="/" hx-boost="true" class="text-2xl no-underline hover:no-underline flex items-center gap-2">
+27
-30
appview/pages/templates/timeline/fragments/hero.html
+27
-30
appview/pages/templates/timeline/fragments/hero.html
···
1
1
{{ define "timeline/fragments/hero" }}
2
-
<div class="mx-auto max-w-[100rem] flex flex-col text-black dark:text-white px-6 py-4 gap-6 items-center md:flex-row">
3
-
<div class="flex flex-col gap-6">
4
-
<h1 class="font-bold text-4xl">tightly-knit<br>social coding.</h1>
5
-
6
-
<p class="text-lg">
7
-
Tangled is a decentralized Git hosting and collaboration platform.
8
-
</p>
9
-
<p class="text-lg">
10
-
We envision a place where developers have complete ownership of their
11
-
code, open source communities can freely self-govern and most
12
-
importantly, coding can be social and fun again.
13
-
</p>
14
-
15
-
<div class="flex gap-6 items-center">
16
-
<a href="/signup" class="no-underline hover:no-underline ">
17
-
<button class="btn-create flex gap-2 px-4 items-center">
18
-
join now {{ i "arrow-right" "size-4" }}
19
-
</button>
20
-
</a>
21
-
</div>
22
-
</div>
23
-
24
-
<figure class="w-full hidden md:block md:w-auto">
25
-
<a href="https://tangled.org/@tangled.org/core" class="block">
26
-
<img src="https://assets.tangled.network/hero-repo.png" alt="Screenshot of the Tangled monorepo." class="max-w-md mx-auto md:max-w-none w-full md:w-[30vw] h-auto shadow-sm rounded" />
27
-
</a>
28
-
<figcaption class="text-sm text-gray-600 dark:text-gray-400 mt-2 text-center">
29
-
Monorepo for Tangled, built in the open with the community.
30
-
</figcaption>
31
-
</figure>
2
+
<div class="mx-auto max-w-[100rem] grid grid-cols-1 md:grid-cols-2 text-black dark:text-white px-6 py-4 gap-6 items-center md:flex-row">
3
+
<h1 class="font-bold text-5xl md:text-6xl lg:text-8xl">tightly-knit<br>social coding.</h1>
4
+
<div id="hero-right" class="space-y-6">
5
+
<p class="text-xl">
6
+
The next-generation social coding platform.
7
+
</p>
8
+
<p class="text-xl">
9
+
We envision a place where developers have ownership of their code,
10
+
communities can freely self-govern and most importantly, coding can
11
+
be social and fun again.
12
+
</p>
13
+
<form class="flex gap-2 items-stretch">
14
+
<input
15
+
type="email"
16
+
id="email"
17
+
name="email"
18
+
tabindex="4"
19
+
required
20
+
placeholder="Enter your email"
21
+
class="py-2 w-full md:w-fit"
22
+
/>
23
+
<button class="btn-create flex items-center gap-2 text-base whitespace-nowrap" type="submit">
24
+
join now
25
+
{{ i "arrow-right" "size-4" }}
26
+
</button>
27
+
</form>
28
+
</div>
32
29
</div>
33
30
{{ end }}
+133
appview/pages/templates/timeline/fragments/preview.html
+133
appview/pages/templates/timeline/fragments/preview.html
···
1
+
{{ define "timeline/fragments/preview" }}
2
+
<div class="flex flex-col gap-4 overflow-hidden">
3
+
<div class="relative h-80 w-full">
4
+
<!-- left fade overlay (fixed on left edge) -->
5
+
<div class="absolute left-0 top-0 h-full w-16 pointer-events-none z-10 bg-gradient-to-r from-white to-transparent dark:from-gray-900"></div>
6
+
7
+
<!-- right fade overlay (fixed on right edge) -->
8
+
<div class="absolute right-0 top-0 h-full w-16 pointer-events-none z-10 bg-gradient-to-l from-white to-transparent dark:from-gray-900"></div>
9
+
10
+
{{ template "marquee" $ }}
11
+
</div>
12
+
</div>
13
+
{{ end }}
14
+
15
+
{{ define "marquee" }}
16
+
{{ $rowOffsets := list 10 80 150 220 }}
17
+
{{ $prev := -1 }}
18
+
{{ $w := mul (len .Timeline) 150 }}
19
+
20
+
<div class="absolute h-full flex animate-marquee">
21
+
<div class="relative h-full flex-shrink-0 bottom-4 border-b border-gray-200 dark:border-gray-700" style="width: {{ $w }}px;">
22
+
{{ range $i, $e := .Timeline }}
23
+
{{ $curr := randInt 4 }}
24
+
{{ if eq $curr $prev }}
25
+
{{ $curr = mod (add $curr 1) 4 }}
26
+
{{ end }}
27
+
{{ $offset := index $rowOffsets $curr }}
28
+
{{ template "timelineEvent" (list $i $e $offset) }}
29
+
{{ $prev = $curr }}
30
+
{{ end }}
31
+
</div>
32
+
33
+
<div
34
+
class="absolute left-0 bottom-0 h-3 flex-shrink-0 bg-[linear-gradient(to_right,currentColor_1px,transparent_1px)] bg-[length:6px_100%] bg-repeat-x text-gray-400 dark:text-gray-500"
35
+
style="width: {{ $w }}px;">
36
+
</div>
37
+
38
+
</div>
39
+
{{ end }}
40
+
41
+
{{ define "timelineEvent" }}
42
+
{{ $i := index . 0 }}
43
+
{{ $e := index . 1 }}
44
+
{{ $variance := randInt 10 }}
45
+
{{ $offset := add (index . 2) $variance }}
46
+
{{ $left := mul $i 175 }}
47
+
{{ with $e }}
48
+
<div
49
+
class="absolute left-0 flex flex-col divide-y divide-gray-200 dark:divide-gray-700 border border-gray-200 dark:border-gray-700 rounded-sm bg-white dark:bg-gray-800 drop-shadow-sm"
50
+
style="bottom: {{ $offset }}px; left: {{ $left }}px; transform: translateX(-50%); z-index: 5;"
51
+
>
52
+
{{ if .Repo }}
53
+
{{ template "repoEvent" (list $ .) }}
54
+
{{ else if .RepoStar }}
55
+
{{ template "starEvent" (list $ .) }}
56
+
{{ else if .Follow }}
57
+
{{ template "followEvent" (list $ .) }}
58
+
{{ end }}
59
+
</div>
60
+
{{ end }}
61
+
62
+
<!-- vertical connector -->
63
+
<div
64
+
class="absolute w-px bg-gray-300 dark:bg-gray-700"
65
+
style="left: {{ $left }}px; bottom: 0px; height: {{ $offset }}px; z-index: 0;"
66
+
></div>
67
+
68
+
<!-- dot at the bottom -->
69
+
<div
70
+
class="absolute size-2 bg-gray-300 dark:bg-gray-600 rounded-full"
71
+
style="left: {{ sub $left 3 }}px; bottom: -3px; z-index: 0;">
72
+
</div>
73
+
{{ end }}
74
+
75
+
{{ define "repoEvent" }}
76
+
{{ $root := index . 0 }}
77
+
{{ $event := index . 1 }}
78
+
{{ $repo := $event.Repo }}
79
+
{{ $source := $event.Source }}
80
+
{{ $userHandle := resolve $repo.Did }}
81
+
<div class="px-6 py-2 bg-white dark:bg-gray-800 text-gray-600 dark:text-gray-300 flex w-max items-center gap-2 text-sm">
82
+
{{ template "user/fragments/picHandleLink" $repo.Did }}
83
+
{{ with $source }}
84
+
{{ $sourceDid := resolve .Did }}
85
+
forked
86
+
<a href="/{{ $sourceDid }}/{{ .Name }}"class="no-underline hover:underline">
87
+
{{ $sourceDid }}/{{ .Name }}
88
+
</a>
89
+
to
90
+
<a href="/{{ $userHandle }}/{{ $repo.Name }}" class="no-underline hover:underline">{{ $repo.Name }}</a>
91
+
{{ else }}
92
+
created
93
+
<a href="/{{ $userHandle }}/{{ $repo.Name }}" class="no-underline hover:underline">
94
+
{{ $repo.Name }}
95
+
</a>
96
+
{{ end }}
97
+
</div>
98
+
{{ end }}
99
+
100
+
{{ define "starEvent" }}
101
+
{{ $root := index . 0 }}
102
+
{{ $event := index . 1 }}
103
+
{{ $star := $event.RepoStar }}
104
+
{{ with $star }}
105
+
{{ $starrerHandle := resolve .Did }}
106
+
{{ $repoOwnerHandle := resolve .Repo.Did }}
107
+
<div class="px-6 py-2 bg-white dark:bg-gray-800 text-gray-600 dark:text-gray-300 flex w-max items-center gap-2 text-sm">
108
+
{{ template "user/fragments/picHandleLink" $starrerHandle }}
109
+
starred
110
+
{{ template "user/fragments/pic" (list $repoOwnerHandle "size-6") }}
111
+
<a href="/{{ $repoOwnerHandle }}/{{ .Repo.Name }}" class="no-underline hover:underline">
112
+
{{ $repoOwnerHandle | truncateAt30 }}/{{ .Repo.Name }}
113
+
</a>
114
+
</div>
115
+
{{ end }}
116
+
{{ end }}
117
+
118
+
{{ define "followEvent" }}
119
+
{{ $root := index . 0 }}
120
+
{{ $event := index . 1 }}
121
+
{{ $follow := $event.Follow }}
122
+
{{ $profile := $event.Profile }}
123
+
{{ $followStats := $event.FollowStats }}
124
+
{{ $followStatus := $event.FollowStatus }}
125
+
126
+
{{ $userHandle := resolve $follow.UserDid }}
127
+
{{ $subjectHandle := resolve $follow.SubjectDid }}
128
+
<div class="px-6 py-2 bg-white dark:bg-gray-800 text-gray-600 dark:text-gray-300 flex w-max items-center gap-2 text-sm">
129
+
{{ template "user/fragments/picHandleLink" $userHandle }}
130
+
followed
131
+
{{ template "user/fragments/picHandleLink" $subjectHandle }}
132
+
</div>
133
+
{{ end }}
+580
-67
appview/pages/templates/timeline/home.html
+580
-67
appview/pages/templates/timeline/home.html
···
19
19
<!-- Additional SEO -->
20
20
<meta name="description" content="The next-generation social coding platform. Host repos on your infrastructure with knots, use stacked pull requests, and run CI with spindles." />
21
21
<link rel="canonical" href="https://tangled.org" />
22
-
23
-
24
22
{{ end }}
25
23
26
24
27
25
{{ define "content" }}
28
-
<div class="flex flex-col gap-4">
26
+
<div class="flex flex-col gap-24 md:gap-40 my-24 md:my-32">
29
27
{{ template "timeline/fragments/hero" . }}
30
-
{{ template "features" . }}
31
-
{{ template "timeline/fragments/goodfirstissues" . }}
32
-
{{ template "timeline/fragments/trending" . }}
33
-
{{ template "timeline/fragments/timeline" . }}
28
+
{{ template "timeline/fragments/preview" . }}
29
+
{{ template "features1" . }}
30
+
{{ template "features2" . }}
31
+
{{ template "recentUpdates" . }}
32
+
{{ template "community" . }}
33
+
</div>
34
+
{{ end }}
35
+
36
+
{{ block "topbarLayout" . }}
37
+
<header class="max-w-screen-xl mx-auto w-full col-span-full md:col-span-1 md:col-start-2" style="z-index: 20;">
38
+
{{ if .LoggedInUser }}
39
+
<div id="upgrade-banner"
40
+
hx-get="/upgradeBanner"
41
+
hx-trigger="load"
42
+
hx-swap="innerHTML">
43
+
</div>
44
+
{{ end }}
45
+
{{ template "layouts/fragments/topbar" . }}
46
+
</header>
47
+
{{ end }}
48
+
49
+
{{ block "footerLayout" . }}
50
+
<footer class="z-10">
51
+
{{ template "layouts/fragments/footer" . }}
52
+
</footer>
53
+
{{ end }}
54
+
55
+
{{ block "bodyClasses" . }}
56
+
bg-transparent bg-gradient-to-b from-white to-slate-100 dark:bg-none dark:bg-gray-900
57
+
{{ end }}
58
+
59
+
{{ block "mainLayout" . }}
60
+
<div class="flex-grow relative">
61
+
<div
62
+
class="absolute opacity-50 dark:opacity-5 inset-x-0 top-0 bottom-[-50px] md:bottom-[-150px] pointer-events-none bg-[url('https://assets.tangled.network/yarn_ball.svg')] bg-no-repeat bg-right-bottom bg-[length:500px] md:bg-[length:1000px] w-full">
63
+
</div>
64
+
<div class="max-w-screen-xl mx-auto flex flex-col gap-4 relative z-10">
65
+
{{ block "contentLayout" . }}
66
+
<main>
67
+
{{ block "content" . }}{{ end }}
68
+
</main>
69
+
{{ end }}
70
+
71
+
{{ block "contentAfterLayout" . }}
72
+
<main>
73
+
{{ block "contentAfter" . }}{{ end }}
74
+
</main>
75
+
{{ end }}
76
+
</div>
77
+
</div>
78
+
{{ end }}
79
+
80
+
{{ define "features1" }}
81
+
{{ $labelStyle := "normal-case cursor-pointer w-auto md:w-full p-4 md:px-6 rounded bg-white dark:bg-gray-800 font-medium text-base md:text-lg opacity-50 border border-gray-200 dark:border-gray-700 relative overflow-hidden" }}
82
+
{{ $spanStyle := "z-10 items-center justify-between gap-2 w-full" }}
83
+
{{ $connectorStyle := "w-0.5 h-6 bg-gray-300 dark:bg-gray-600 opacity-0 mx-auto" }}
84
+
{{ $contentStyle := "hidden bg-white dark:bg-gray-800 rounded shadow-sm p-6 border border-gray-200 dark:border-gray-700 grid-cols-1 md:grid-cols-2" }}
85
+
{{ $progressOverlayStyle := "absolute inset-0 bg-gray-600/10 dark:bg-gray-100/10 w-0 transition-none" }}
86
+
87
+
<style>
88
+
@media (max-width: 768px) {
89
+
.features-grid:has(#feature-prs:checked) {
90
+
grid-template-columns: 1fr auto auto;
91
+
}
92
+
93
+
.features-grid:has(#feature-knots:checked) {
94
+
grid-template-columns: auto 1fr auto;
95
+
}
96
+
97
+
.features-grid:has(#feature-spindles:checked) {
98
+
grid-template-columns: auto auto 1fr;
99
+
}
100
+
101
+
#feature-prs:checked ~ label[for="feature-prs"] .label-text,
102
+
#feature-knots:checked ~ label[for="feature-knots"] .label-text,
103
+
#feature-spindles:checked ~ label[for="feature-spindles"] .label-text {
104
+
display: inline-flex !important;
105
+
}
106
+
#feature-prs:checked ~ label[for="feature-prs"] .icon-only,
107
+
#feature-knots:checked ~ label[for="feature-knots"] .icon-only,
108
+
#feature-spindles:checked ~ label[for="feature-spindles"] .icon-only {
109
+
display: none !important;
110
+
}
111
+
}
112
+
</style>
113
+
114
+
<div class="features-grid w-full grid grid-cols-3 gap-x-6 px-2">
115
+
<input type="radio" id="feature-prs" name="feature" class="peer/prs hidden" checked />
116
+
<input type="radio" id="feature-knots" name="feature" class="peer/knots hidden" />
117
+
<input type="radio" id="feature-spindles" name="feature" class="peer/spindles hidden" />
118
+
119
+
<label for="feature-prs" class="{{ $labelStyle }} peer-checked/prs:opacity-100 peer-checked/prs:shadow-sm">
120
+
<span class="label-text hidden md:inline-flex {{ $spanStyle }}">A better way to review {{ i "git-pull-request" "size-5" }}</span>
121
+
<span class="icon-only inline-flex md:hidden {{ $spanStyle }}">{{ i "git-pull-request" "size-5" }}</span>
122
+
<div class="{{ $progressOverlayStyle }}" data-progress="prs"></div>
123
+
</label>
124
+
125
+
<label for="feature-knots" class="{{ $labelStyle }} peer-checked/knots:opacity-100 peer-checked/knots:shadow-sm">
126
+
<span class="label-text hidden md:inline-flex {{ $spanStyle }}">Completely self-hostable {{ i "hard-drive" "size-5" }}</span>
127
+
<span class="icon-only inline-flex md:hidden {{ $spanStyle }}">{{ i "hard-drive" "size-5" }}</span>
128
+
<div class="{{ $progressOverlayStyle }}" data-progress="knots"></div>
129
+
</label>
130
+
131
+
<label for="feature-spindles" class="{{ $labelStyle }} peer-checked/spindles:opacity-100 peer-checked/spindles:shadow-sm">
132
+
<span class="label-text hidden md:inline-flex {{ $spanStyle }}">Quick and easy CI {{ i "layers-2" "size-5" }}</span>
133
+
<span class="icon-only inline-flex md:hidden {{ $spanStyle }}">{{ i "layers-2" "size-5" }}</span>
134
+
<div class="{{ $progressOverlayStyle }}" data-progress="spindles"></div>
135
+
</label>
136
+
137
+
<div class="{{ $connectorStyle }} peer-checked/prs:opacity-100"></div>
138
+
<div class="{{ $connectorStyle }} peer-checked/knots:opacity-100"></div>
139
+
<div class="{{ $connectorStyle }} peer-checked/spindles:opacity-100"></div>
140
+
141
+
{{ $titleStyle := "text-xl md:text-2xl font-bold my-2 text-gray-900 dark:text-gray-100" }}
142
+
{{ $textContentStyle := "pb-6 md:p-6 md:pr-12 md:text-lg" }}
143
+
{{ $imgContentStyle := "w-full p-6 min-h-96 min-h-[400px] md:min-h-[500px] rounded overflow-hidden place-content-center bg-gradient-to-b from-slate-50 to-slate-100 dark:from-gray-800 dark:to-gray-900 border border-gray-200 dark:border-gray-700" }}
144
+
145
+
<div class="col-span-3 {{ $contentStyle }} peer-checked/prs:grid ">
146
+
<div class="{{ $textContentStyle }} flex flex-col gap-4 md:gap-8 place-self-center">
147
+
<section>
148
+
<h1 class="{{ $titleStyle }}">Stacked PRs</h1>
149
+
<p>
150
+
Break down large features into small, reviewable chunks. Stack pull
151
+
requests on top of each other and ship faster with better code
152
+
reviews. Tangled natively supports stacking using Jujutsu <a
153
+
href="https://docs.jj-vcs.dev/latest/glossary/#change-id"
154
+
class="underline">Change-Ids</a>.
155
+
</p>
156
+
</section>
157
+
<section>
158
+
<h1 class="{{ $titleStyle }}">Round-based review</h1>
159
+
<p>
160
+
Pull-requests in Tangled are round-based, which means, each
161
+
submission is "immutable". To update a PR, the author must start a
162
+
second revision or "round". As a reviewer, you never have to worry
163
+
about a PR changing <em>during</em> a review!
164
+
</p>
165
+
</section>
166
+
</div>
167
+
<div class="{{ $imgContentStyle }} pl-24 md:pl-40">
168
+
<div class="flex flex-col" style="transform: perspective(1000px) rotateX(15deg)">
169
+
{{ $mockPRs := list
170
+
(dict "Id" 941 "Title" "spindle/nixery: update setup command docs" "State" "open" )
171
+
(dict "Id" 940 "Title" "appview/pulls: fix search not updating count of pull requests" "State" "open" )
172
+
(dict "Id" 939 "Title" "appview/pages: improved seo tags for home, repo and profile" "State" "open" )
173
+
(dict "Id" 938 "Title" "appview/state: update robots.txt" "State" "merged" )
174
+
(dict "Id" 937 "Title" "appview/profile: show dummy profile when no tangled profile" "State" "merged" )
175
+
(dict "Id" 936 "Title" "appview/pipelines: fix incorrect totals" "State" "merged" )
176
+
}}
177
+
{{ range $index, $pr := $mockPRs }}
178
+
{{ $scale := 1.4 }}
179
+
{{ if gt $index 0 }}
180
+
{{ $scale = subf64 1.4 (mulf64 (f64 $index) 0.04) }}
181
+
{{ if lt $scale 1.0 }}{{ $scale = 1.0 }}{{ end }}
182
+
{{ end }}
183
+
{{ $zIndex := sub 100 $index }}
184
+
{{ template "mockPRRow" (dict "PR" $pr "Scale" $scale "ZIndex" $zIndex) }}
185
+
{{ end }}
186
+
</div>
187
+
</div>
188
+
</div>
189
+
190
+
<div class="col-span-3 {{ $contentStyle }} peer-checked/knots:grid">
191
+
<div class="{{ $textContentStyle }} flex flex-col gap-4 md:gap-8 place-self-center">
192
+
<section>
193
+
<h1 class="{{ $titleStyle }}">Knots</h1>
194
+
<p>
195
+
Host your repositories on your own infrastructure with <a
196
+
href="https://docs.tangled.org/knot-self-hosting-guide.html#knot-self-hosting-guide" class="underline">knots</a>.
197
+
Knots are lightweight git repository hosts that syndicate git
198
+
operations across the network. You can setup a knot server on a
199
+
machine as small as a Raspberry Pi!
200
+
<br>
201
+
<br>
202
+
If you want to try Tangled without self-hosting, fear not! All users
203
+
are added to our hosted knot by default.
204
+
</p>
205
+
</section>
206
+
<section>
207
+
<h1 class="{{ $titleStyle }}">Spindles</h1>
208
+
<p>
209
+
Host CI runners on your own infrastructure with <a
210
+
href="https://docs.tangled.org/spindles.html#self-hosting-guide" class="underline">spindles</a>.
211
+
Spindles are responsible for queuing up CI jobs and syndicating
212
+
pipeline statuses to the network. Presently, the task of sandboxing
213
+
workflows and caching dependencies is delegated to Docker.
214
+
</p>
215
+
</section>
216
+
</div>
217
+
{{ $planetWrapperStyle := "absolute left-1/2 bottom-0 flex items-start justify-center -translate-x-1/2 translate-y-1/2" }}
218
+
{{ $orbitStyle := "absolute left-1/2 bottom-0 rounded-full border-2 border-gray-200 dark:border-gray-700 -translate-x-1/2 translate-y-1/2 shadow-lg" }}
219
+
{{ $planetStyle := "-mt-8 size-16 md:-mt-10 md:size-20 rounded-full shadow-sm border flex items-center justify-center" }}
220
+
<div class="{{ $imgContentStyle }} min-h-[500px] relative">
221
+
<div class="size-[250px] {{ $orbitStyle }}"></div>
222
+
<div class="size-[500px] {{ $orbitStyle }}"></div>
223
+
<div class="size-[750px] {{ $orbitStyle }}"></div>
224
+
225
+
<!-- planets on inner orbit -->
226
+
<div class="{{ $planetWrapperStyle }} size-[250px] rotate-[12deg]">
227
+
<div class="{{ $planetStyle }} -rotate-[12deg] bg-blue-100 dark:bg-blue-900 border-blue-500 dark:border-blue-500">
228
+
{{ i "server" "size-8" "text-blue-500" "dark:text-blue-500" }}
229
+
</div>
230
+
</div>
231
+
232
+
<!-- Planets on middle orbit -->
233
+
<div class="{{ $planetWrapperStyle }} size-[500px] -rotate-[32deg]">
234
+
<div class="{{ $planetStyle }} rotate-[32deg] bg-green-100 dark:bg-green-900 border-green-500 dark:border-green-500">
235
+
{{ i "hard-drive" "size-8" "text-green-500" "dark:text-green-500" }}
236
+
</div>
237
+
</div>
238
+
<div class="{{ $planetWrapperStyle }} size-[500px] rotate-[24deg]">
239
+
<div class="{{ $planetStyle }} -rotate-[24deg] bg-amber-100 dark:bg-amber-900 border-amber-500 dark:border-amber-500">
240
+
{{ i "server" "size-8" "text-amber-500" "dark:text-amber-500" }}
241
+
</div>
242
+
</div>
243
+
244
+
<!-- Planets on outer orbit -->
245
+
<div class="{{ $planetWrapperStyle }} size-[750px] -rotate-[40deg]">
246
+
<div class="{{ $planetStyle }} rotate-[40deg] bg-blue-100 dark:bg-blue-900 border-blue-500 dark:border-blue-500">
247
+
{{ i "server" "size-8" "text-blue-500" "dark:text-blue-500" }}
248
+
</div>
249
+
</div>
250
+
<div class="{{ $planetWrapperStyle }} size-[750px] rotate-[3deg]">
251
+
<div class="{{ $planetStyle }} -rotate-[3deg] bg-green-100 dark:bg-green-900 border-green-500 dark:border-green-500">
252
+
{{ i "router" "size-8" "text-green-500" "dark:text-green-500" }}
253
+
</div>
254
+
</div>
255
+
<div class="{{ $planetWrapperStyle }} size-[750px] rotate-[44deg]">
256
+
<div class="{{ $planetStyle }} -rotate-[44deg] bg-amber-100 dark:bg-amber-900 border-amber-500 dark:border-amber-500">
257
+
{{ i "database" "size-8" "text-amber-500" "dark:text-amber-500" }}
258
+
</div>
259
+
</div>
260
+
</div>
261
+
</div>
262
+
263
+
<div class="col-span-3 {{ $contentStyle }} peer-checked/spindles:grid">
264
+
<div class="{{ $textContentStyle }} flex flex-col gap-4 md:gap-8 place-self-center">
265
+
<section>
266
+
<h1 class="{{ $titleStyle }}">Nix-powered CI</h1>
267
+
<p>
268
+
Pick and choose dependencies for your CI pipelines from <a
269
+
href="https://docs.tangled.org/spindles.html#dependencies"
270
+
class="underline"><code>nixpkgs</code></a>, one of the biggest
271
+
package repositories.
272
+
<br>
273
+
<br>
274
+
All dependencies and workflow images are cached using <a
275
+
href="https://nixery.dev/">nixery</a>. Subsuquent runs of your CI
276
+
pipeline will load all dependencies almost instantly.
277
+
</p>
278
+
</section>
279
+
<section>
280
+
<div class="flex items-center gap-2 my-4">
281
+
<h1 class="{{ $titleStyle }}">Customizable Engines</h1>
282
+
<span class="text-sm text-blue-500 dark:text-blue-300 bg-blue-100 dark:bg-blue-900/50 rounded p-1">coming soon</span>
283
+
</div>
284
+
<p>
285
+
We know nix is not for everybody! Spindles are built to have
286
+
swappable engines, on our roadmap are Docker and Firecracker based
287
+
engines.
288
+
</p>
289
+
</section>
290
+
</div>
291
+
<div class="{{ $imgContentStyle }} flex items-end pb-0">
292
+
<div class="w-full mx-auto border-t border-l border-r border-gray-200 dark:border-gray-700 rounded-t overflow-hidden shadow-lg">
293
+
<div class="bg-white dark:bg-gray-800 px-4 py-2 border-b border-gray-200 dark:border-gray-700 flex items-center gap-2 justify-between">
294
+
<span class="font-mono text-gray-600 dark:text-gray-400 text-sm md:text-lg">.tangled/workflows/test.yml</span>
295
+
<div class="flex items-center gap-2">
296
+
<div class="size-3 rounded-full bg-red-500"></div>
297
+
<div class="size-3 rounded-full bg-yellow-500"></div>
298
+
<div class="size-3 rounded-full bg-green-500"></div>
299
+
</div>
300
+
</div>
301
+
<div class="bg-white dark:bg-gray-800 p-4 overflow-x-auto text-sm md:text-lg">
302
+
{{ $yamlContent := `when:
303
+
- event: push
304
+
branch: main
305
+
306
+
dependencies:
307
+
nixpkgs:
308
+
- go
309
+
- gcc
310
+
311
+
environment:
312
+
CGO_ENABLED: 1
313
+
314
+
steps:
315
+
- name: run all tests
316
+
command: go test -v ./...` }}
317
+
<div class="whitespace-pre">{{ code $yamlContent ".tangled/workflows/test.yml" | escapeHtml }}</div>
318
+
</div>
319
+
</div>
320
+
</div>
321
+
</div>
322
+
</div>
323
+
324
+
<script>
325
+
document.addEventListener('DOMContentLoaded', function() {
326
+
const featureIds = ['feature-prs', 'feature-knots', 'feature-spindles'];
327
+
const progressNames = ['prs', 'knots', 'spindles'];
328
+
let currentIndex = 0;
329
+
let timerInterval = null;
330
+
331
+
function stopTimer() {
332
+
if (timerInterval) {
333
+
clearInterval(timerInterval);
334
+
timerInterval = null;
335
+
}
336
+
document.querySelectorAll('[data-progress]').forEach(el => {
337
+
el.classList.remove('animate-progress');
338
+
});
339
+
}
340
+
341
+
const firstProgress = document.querySelector('[data-progress="prs"]');
342
+
if (firstProgress) {
343
+
firstProgress.classList.add('animate-progress');
344
+
}
345
+
346
+
document.querySelectorAll('label[for^="feature-"]').forEach(label => {
347
+
label.addEventListener('click', stopTimer);
348
+
});
349
+
350
+
timerInterval = setInterval(function() {
351
+
document.querySelectorAll('[data-progress]').forEach(el => {
352
+
el.classList.remove('animate-progress');
353
+
void el.offsetWidth; // force reflow
354
+
});
355
+
356
+
currentIndex = (currentIndex + 1) % featureIds.length;
357
+
document.getElementById(featureIds[currentIndex]).checked = true;
358
+
359
+
const activeProgress = document.querySelector('[data-progress="' + progressNames[currentIndex] + '"]');
360
+
if (activeProgress) {
361
+
activeProgress.classList.add('animate-progress');
362
+
}
363
+
}, 5000);
364
+
});
365
+
</script>
366
+
{{ end }}
367
+
368
+
{{ define "mockPRRow" }}
369
+
{{ $pr := .PR }}
370
+
<div class="flex text-sm items-center justify-between w-full bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded p-3 shadow relative" style="transform: scale({{ .Scale }}); transform-origin: top center; z-index: {{ .ZIndex }};">
371
+
<div class="flex items-center gap-2 min-w-0 flex-1 pr-2">
372
+
<div class="flex-shrink-0">
373
+
{{ if eq $pr.State "open" }}
374
+
{{ i "git-pull-request" "size-4 text-green-500" }}
375
+
{{ else }}
376
+
{{ i "git-merge" "size-4 text-purple-500" }}
377
+
{{ end }}
378
+
</div>
379
+
<span class="truncate text-sm text-gray-800 dark:text-gray-200">
380
+
<span class="text-gray-500 dark:text-gray-400">#{{ $pr.Id }}</span>
381
+
{{ $pr.Title }}
382
+
</span>
383
+
</div>
384
+
</div>
385
+
{{ end }}
386
+
387
+
{{ define "features2" }}
388
+
{{ $cardStyle := "bg-white dark:bg-gray-800 rounded shadow-sm border border-gray-200 dark:border-gray-700" }}
389
+
{{ $cardInnerStyle := "flex flex-row items-center gap-4 md:gap-6 p-4 md:p-6 md:pt-8 relative" }}
390
+
{{ $contentStyle := "flex-1 flex flex-col" }}
391
+
{{ $titleStyle := "text-xl md:text-2xl font-bold mb-2 md:mb-3 text-gray-900 dark:text-gray-100" }}
392
+
{{ $descriptionStyle := "text-gray-600 dark:text-gray-300 text-sm md:text-base leading-relaxed mb-3 md:mb-0" }}
393
+
{{ $linkMobileStyle := "hover:no-underline inline-flex md:hidden items-center gap-2 text-sm text-gray-700 dark:text-gray-300 font-medium" }}
394
+
{{ $iconContainerStyle := "relative shrink-0 w-24 h-24 md:w-48 md:h-48" }}
395
+
{{ $iconCircleStyle := "w-full h-full rounded-full flex items-center justify-center" }}
396
+
{{ $linkDesktopStyle := "hover:no-underline hidden md:inline-flex absolute -bottom-10 -right-14 items-center gap-2 p-3 text-base btn" }}
397
+
398
+
<div class="w-full flex flex-col gap-6 md:gap-40 max-w-5xl mx-auto px-2">
399
+
<div class="{{ $cardStyle }} md:mr-32">
400
+
<div class="{{ $cardInnerStyle }}">
401
+
<div class="{{ $contentStyle }}">
402
+
<h3 class="{{ $titleStyle }}">Built on AT Protocol</h3>
403
+
<p class="{{ $descriptionStyle }}">
404
+
AT Protocol enables federated code-collaboration. Submit
405
+
pull-requests or bug-reports to any repository hosted on any
406
+
server.
407
+
<br>
408
+
<br>
409
+
Bring an existing AT Protocol account (such as your Bluesky
410
+
account), or signup for one with us.
411
+
<a class="underline" href="https://docs.tangled.org/quick-start-guide.html#login-or-sign-up">Read the docs to know more.</a>
412
+
</p>
413
+
<a href="TODO" class="{{ $linkMobileStyle }}">
414
+
Learn more
415
+
{{ i "arrow-right" "size-4" }}
416
+
</a>
417
+
</div>
418
+
<div class="{{ $iconContainerStyle }}">
419
+
<div class="{{ $iconCircleStyle }} bg-blue-100 dark:bg-blue-900/50">
420
+
{{ i "at-sign" "size-12 md:size-24" "text-blue-500 dark:text-blue-500" "rotate-12" }}
421
+
</div>
422
+
<a href="TODO" class="{{ $linkDesktopStyle }}">
423
+
Learn more
424
+
{{ i "arrow-right" "size-4" }}
425
+
</a>
426
+
</div>
427
+
</div>
428
+
</div>
429
+
430
+
<div class="{{ $cardStyle }} md:ml-32">
431
+
<div class="{{ $cardInnerStyle }}">
432
+
<div class="{{ $contentStyle }}">
433
+
<h3 class="{{ $titleStyle }}">Free and Open-source</h3>
434
+
<p class="{{ $descriptionStyle }}">
435
+
All of Tangled is open-source and built in the open!
436
+
Check out the <a class="underline" href="https://tangled.org/core">monorepo</a> and join in on the fun.
437
+
<br>
438
+
<br>
439
+
We welcome contributions however big or small. You can start contributing by picking up a
440
+
<a class="underline" href="https://tangled.org/tangled.org/core/issues">good-first-issue</a>.
441
+
</p>
442
+
<a href="TODO" class="{{ $linkMobileStyle }}">
443
+
Learn more
444
+
{{ i "arrow-right" "size-4" }}
445
+
</a>
446
+
</div>
447
+
<div class="{{ $iconContainerStyle }}">
448
+
<div class="{{ $iconCircleStyle }} bg-green-100 dark:bg-green-900/50">
449
+
{{ i "package-open" "size-12 md:size-24" "text-green-500 dark:text-green-500" "-rotate-12" }}
450
+
</div>
451
+
<a href="TODO" class="{{ $linkDesktopStyle }}">
452
+
Learn more
453
+
{{ i "arrow-right" "size-4" }}
454
+
</a>
455
+
</div>
456
+
</div>
457
+
</div>
458
+
459
+
<div class="{{ $cardStyle }} md:mr-32">
460
+
<div class="{{ $cardInnerStyle }}">
461
+
<div class="{{ $contentStyle }}">
462
+
<h3 class="{{ $titleStyle }}">Social coding is back</h3>
463
+
<p class="{{ $descriptionStyle }}">
464
+
Discover trending projects, follow your friends and star your favourite repositories. Coding is better together!
465
+
<br>
466
+
<br>
467
+
You can use one account for all of the atmosphere. If you have
468
+
friends on Bluesky, you will find them on Tangled with the same
469
+
handle.
470
+
</p>
471
+
<a href="TODO" class="{{ $linkMobileStyle }}">
472
+
Learn more
473
+
{{ i "arrow-right" "size-4" }}
474
+
</a>
475
+
</div>
476
+
<div class="{{ $iconContainerStyle }}">
477
+
<div class="{{ $iconCircleStyle }} bg-amber-100 dark:bg-amber-900/50">
478
+
{{ i "message-circle-heart" "size-12 md:size-24" "text-amber-500 dark:text-amber-500" "rotate-12" }}
479
+
</div>
480
+
<a href="TODO" class="{{ $linkDesktopStyle }}">
481
+
Learn more
482
+
{{ i "arrow-right" "size-4" }}
483
+
</a>
484
+
</div>
485
+
</div>
486
+
</div>
487
+
</div>
488
+
{{ end }}
489
+
490
+
{{ define "changelog" }}
491
+
<div class="w-full px-2 py-16 md:py-24">
492
+
<h2 class="text-3xl md:text-4xl font-bold mb-8 text-gray-900 dark:text-gray-100">Changelog</h2>
493
+
494
+
<div class="grid grid-cols-1 gap-6 mb-6">
495
+
{{ range $index := list 0 1 2 3 }}
496
+
<div class="bg-white dark:bg-gray-800 rounded shadow-sm border border-gray-200 dark:border-gray-700 p-6">
497
+
<div class="flex items-center gap-2 mb-3">
498
+
<span class="text-xs font-medium text-gray-500 dark:text-gray-400">v1.{{ sub 3 $index }}.0</span>
499
+
<span class="text-xs text-gray-400 dark:text-gray-500">•</span>
500
+
<span class="text-xs text-gray-500 dark:text-gray-400">Feb {{ sub 16 $index }}, 2026</span>
501
+
</div>
502
+
<h3 class="text-lg font-semibold mb-2 text-gray-900 dark:text-gray-100">Feature Update {{ sub 4 $index }}</h3>
503
+
<p class="text-sm text-gray-600 dark:text-gray-300 leading-relaxed">
504
+
Improvements to the platform including bug fixes, performance enhancements, and new features.
505
+
</p>
506
+
</div>
507
+
{{ end }}
508
+
</div>
509
+
34
510
<div class="flex justify-end">
35
-
<a href="/timeline" class="inline-flex items-center gap-2 text-gray-500 dark:text-gray-400">
36
-
view more
511
+
<a href="/changelog" class="hover:no-underline inline-flex items-center gap-2 px-4 py-2 text-base btn">
512
+
View full changelog
37
513
{{ i "arrow-right" "size-4" }}
38
514
</a>
39
515
</div>
40
516
</div>
41
517
{{ end }}
42
518
43
-
44
-
{{ define "feature" }}
45
-
{{ $info := index . 0 }}
46
-
{{ $bullets := index . 1 }}
47
-
<div class="flex flex-col items-center gap-6 md:flex-row md:items-top">
48
-
<div class="flex-1">
49
-
<h2 class="text-2xl font-bold text-black dark:text-white mb-6">{{ $info.title }}</h2>
50
-
<ul class="leading-normal">
51
-
{{ range $bullets }}
52
-
<li><p>{{ escapeHtml . }}</p></li>
53
-
{{ end }}
54
-
</ul>
55
-
</div>
56
-
<div class="flex-shrink-0 w-96 md:w-1/3">
57
-
<a href="{{ $info.image }}">
58
-
<img src="{{ $info.image }}" alt="{{ $info.alt }}" class="w-full h-auto rounded shadow-sm" />
59
-
</a>
519
+
{{ define "recentUpdates" }}
520
+
<div class="w-full px-2 py-16 md:py-24">
521
+
<h2 class="px-4 text-3xl md:text-4xl font-bold text-gray-900 dark:text-gray-100 mb-2">Recent updates</h2>
522
+
<p class="px-4 mb-8 text-gray-500 dark:text-gray-400">
523
+
Follow <a href="https://bsky.app/profile/tangled.org">@tangled.org</a> on Bluesky for more!
524
+
</p>
525
+
<div class="columns-1 md:columns-2 lg:columns-3 gap-6">
526
+
{{ range $index, $post := .BlueskyPosts }}
527
+
<div class="{{ if ge $index 3 }}hidden md:block{{ end }}">
528
+
{{ template "post" $post }}
60
529
</div>
530
+
{{ end }}
61
531
</div>
532
+
</div>
62
533
{{ end }}
63
534
64
-
{{ define "features" }}
65
-
<div class="prose dark:text-gray-200 space-y-12 px-6 py-4 bg-white dark:bg-gray-800 rounded drop-shadow-sm">
66
-
{{ template "feature" (list
67
-
(dict
68
-
"title" "lightweight git repo hosting"
69
-
"image" "https://assets.tangled.network/what-is-tangled-repo.png"
70
-
"alt" "A repository hosted on Tangled"
71
-
)
72
-
(list
73
-
"Host your repositories on your own infrastructure using <em>knots</em>—tiny, headless servers that facilitate git operations."
74
-
"Add friends to your knot or invite collaborators to your repository."
75
-
"Guarded by fine-grained role-based access control."
76
-
"Use SSH to push and pull."
77
-
)
78
-
) }}
79
-
80
-
{{ template "feature" (list
81
-
(dict
82
-
"title" "improved pull request model"
83
-
"image" "https://assets.tangled.network/pulls.png"
84
-
"alt" "Round-based pull requests."
85
-
)
86
-
(list
87
-
"An intuitive and effective round-based pull request flow, with inter-diffing between rounds."
88
-
"Stacked pull requests using Jujutsu's change IDs."
89
-
"Paste a <code>git diff</code> or <code>git format-patch</code> for quick drive-by changes."
90
-
)
91
-
) }}
92
-
93
-
{{ template "feature" (list
94
-
(dict
95
-
"title" "run pipelines using spindles"
96
-
"image" "https://assets.tangled.network/pipelines.png"
97
-
"alt" "CI pipeline running on spindle"
98
-
)
99
-
(list
100
-
"Run pipelines on your own infrastructure using <em>spindles</em>—lightweight CI runners."
101
-
"Natively supports Nix for package management."
102
-
"Easily extended to support different execution backends."
103
-
)
104
-
) }}
535
+
{{ define "post" }}
536
+
<div class="bg-white dark:bg-gray-800 rounded shadow-sm border border-gray-200 dark:border-gray-700 px-6 py-4 flex flex-col gap-2 break-inside-avoid mb-6">
537
+
<div class="flex items-center justify-between text-sm text-gray-500 dark:text-gray-400">
538
+
<span class="flex items-center gap-2">
539
+
{{ template "user/fragments/picHandle" "tangled.org" }}
540
+
</span>
541
+
<span>{{ template "repo/fragments/shortTimeAgo" .CreatedAt }}</span>
542
+
</div>
543
+
<p class="text-gray-900 dark:text-gray-100 text-base leading-relaxed whitespace-pre-wrap">{{ .Text }}</p>
544
+
545
+
{{ if .Embed }}
546
+
{{ if .Embed.EmbedImages_View }}
547
+
<div class="grid {{ if eq (len .Embed.EmbedImages_View.Images) 1 }}grid-cols-1{{ else if eq (len .Embed.EmbedImages_View.Images) 2 }}grid-cols-2{{ else }}grid-cols-2{{ end }} gap-2">
548
+
{{ range .Embed.EmbedImages_View.Images }}
549
+
<img src="{{ .Fullsize }}" alt="{{ .Alt }}" class="rounded w-full h-auto object-cover border border-gray-200 dark:border-gray-700" loading="lazy" />
550
+
{{ end }}
551
+
</div>
552
+
{{ else if .Embed.EmbedExternal_View }}
553
+
<a href="{{ .Embed.EmbedExternal_View.External.Uri }}" target="_blank" rel="noopener noreferrer" class="hover:no-underline block border border-gray-200 dark:border-gray-700 rounded overflow-hidden hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors">
554
+
{{ if .Embed.EmbedExternal_View.External.Thumb }}
555
+
<img src="{{ .Embed.EmbedExternal_View.External.Thumb }}" alt="" class="w-full h-48 object-cover" loading="lazy" />
556
+
{{ end }}
557
+
<div class="p-3">
558
+
<div class="font-medium text-gray-900 dark:text-gray-100 text-sm mb-1">{{ .Embed.EmbedExternal_View.External.Title }}</div>
559
+
<div class="text-xs text-gray-600 dark:text-gray-400 line-clamp-2">{{ .Embed.EmbedExternal_View.External.Description }}</div>
560
+
<div class="text-xs text-gray-500 dark:text-gray-500 mt-1">{{ .Embed.EmbedExternal_View.External.Uri }}</div>
561
+
</div>
562
+
</a>
563
+
{{ else if .Embed.EmbedVideo_View }}
564
+
<div class="rounded overflow-hidden bg-gray-100 dark:bg-gray-700 aspect-video flex items-center justify-center">
565
+
<span class="text-gray-500 dark:text-gray-400 text-sm">Video embed</span>
566
+
</div>
567
+
{{ end }}
568
+
{{ end }}
569
+
570
+
<a href="https://bsky.app/profile/tangled.org/post/{{ .Rkey }}"
571
+
target="_blank"
572
+
rel="noopener noreferrer"
573
+
class="flex items-center justify-between gap-4 text-sm text-gray-500 dark:text-gray-400 pt-2 no-underline hover:no-underline">
574
+
<div class="flex items-center gap-4">
575
+
<div class="flex items-center gap-1">
576
+
{{ i "heart" "size-4" }}
577
+
<span>{{ .LikeCount }}</span>
578
+
</div>
579
+
<div class="flex items-center gap-1">
580
+
{{ i "repeat-2" "size-4" }}
581
+
<span>{{ .RepostCount }}</span>
582
+
</div>
583
+
<div class="flex items-center gap-1">
584
+
{{ i "reply" "size-4 -scale-x-1" }}
585
+
<span>{{ add64 .ReplyCount .QuoteCount }}</span>
586
+
</div>
587
+
</div>
588
+
{{ i "arrow-up-right" "size-4" }}
589
+
</a>
590
+
</div>
591
+
{{ end }}
592
+
593
+
{{ define "community" }}
594
+
<div class="w-full px-2 py-16 md:py-24">
595
+
<div class="max-w-2xl mx-auto text-center space-y-6">
596
+
<h2 class="text-3xl md:text-6xl font-bold text-gray-900 dark:text-gray-100">Join the network</h2>
597
+
<p class="text-xl text-gray-600 dark:text-gray-300">
598
+
You can participate in the Tangled network with an AT account. If you
599
+
don't know what that is, you can sign up for one below.
600
+
<br>
601
+
<a href="https://docs.tangled.org/quick-start-guide.html#login-or-sign-up">Read more on the docs.</a>
602
+
</p>
603
+
<form class="flex gap-2 items-stretch w-full md:max-w-md mx-auto p-2 border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 rounded shadow-sm">
604
+
<input
605
+
type="email"
606
+
id="email"
607
+
name="email"
608
+
tabindex="4"
609
+
required
610
+
placeholder="Enter your email"
611
+
class="py-2 w-full"
612
+
/>
613
+
<button class="btn-create flex items-center gap-2 text-base whitespace-nowrap" type="submit">
614
+
join now
615
+
{{ i "arrow-right" "size-4" }}
616
+
</button>
617
+
</form>
105
618
</div>
106
619
{{ end }}
+51
-69
appview/state/state.go
+51
-69
appview/state/state.go
···
251
251
})
252
252
}
253
253
254
-
func (s *State) HomeOrTimeline(w http.ResponseWriter, r *http.Request) {
255
-
if s.oauth.GetMultiAccountUser(r) != nil {
256
-
s.Timeline(w, r)
257
-
return
258
-
}
259
-
s.Home(w, r)
260
-
}
261
-
262
-
func (s *State) Timeline(w http.ResponseWriter, r *http.Request) {
263
-
user := s.oauth.GetMultiAccountUser(r)
264
-
265
-
// TODO: set this flag based on the UI
266
-
filtered := false
267
-
268
-
var userDid string
269
-
if user != nil && user.Active != nil {
270
-
userDid = user.Active.Did
271
-
}
272
-
timeline, err := db.MakeTimeline(s.db, 50, userDid, filtered)
273
-
if err != nil {
274
-
s.logger.Error("failed to make timeline", "err", err)
275
-
s.pages.Notice(w, "timeline", "Uh oh! Failed to load timeline.")
276
-
}
277
-
278
-
repos, err := db.GetTopStarredReposLastWeek(s.db)
279
-
if err != nil {
280
-
s.logger.Error("failed to get top starred repos", "err", err)
281
-
s.pages.Notice(w, "topstarredrepos", "Unable to load.")
282
-
return
283
-
}
284
-
285
-
gfiLabel, err := db.GetLabelDefinition(s.db, orm.FilterEq("at_uri", s.config.Label.GoodFirstIssue))
286
-
if err != nil {
287
-
// non-fatal
288
-
}
289
-
290
-
s.pages.Timeline(w, pages.TimelineParams{
291
-
LoggedInUser: user,
292
-
Timeline: timeline,
293
-
Repos: repos,
294
-
GfiLabel: gfiLabel,
295
-
})
296
-
}
297
-
298
254
func (s *State) UpgradeBanner(w http.ResponseWriter, r *http.Request) {
299
255
user := s.oauth.GetMultiAccountUser(r)
300
256
if user == nil {
···
332
288
})
333
289
}
334
290
335
-
func (s *State) Home(w http.ResponseWriter, r *http.Request) {
336
-
// TODO: set this flag based on the UI
337
-
filtered := false
338
-
339
-
timeline, err := db.MakeTimeline(s.db, 5, "", filtered)
340
-
if err != nil {
341
-
s.logger.Error("failed to make timeline", "err", err)
342
-
s.pages.Notice(w, "timeline", "Uh oh! Failed to load timeline.")
343
-
return
344
-
}
345
-
346
-
repos, err := db.GetTopStarredReposLastWeek(s.db)
347
-
if err != nil {
348
-
s.logger.Error("failed to get top starred repos", "err", err)
349
-
s.pages.Notice(w, "topstarredrepos", "Unable to load.")
350
-
return
351
-
}
352
-
353
-
s.pages.Home(w, pages.TimelineParams{
354
-
LoggedInUser: nil,
355
-
Timeline: timeline,
356
-
Repos: repos,
357
-
})
358
-
}
359
-
360
291
func (s *State) Keys(w http.ResponseWriter, r *http.Request) {
361
292
user := chi.URLParam(r, "user")
362
293
user = strings.TrimPrefix(user, "@")
···
676
607
677
608
return nil
678
609
}
610
+
611
+
func fetchBskyPosts(ctx context.Context, res *idresolver.Resolver, config *config.Config, d *db.DB, logger *slog.Logger) {
612
+
resolved, err := res.ResolveIdent(context.Background(), consts.TangledDid)
613
+
if err != nil {
614
+
logger.Error("failed to resolve tangled.org DID", "err", err)
615
+
return
616
+
}
617
+
618
+
pdsEndpoint := resolved.PDSEndpoint()
619
+
if pdsEndpoint == "" {
620
+
logger.Error("no PDS endpoint found for tangled.sh DID")
621
+
return
622
+
}
623
+
624
+
session, err := oauth.CreateAppPasswordSession(res, config.Core.AppPassword, consts.TangledDid)
625
+
if err != nil {
626
+
logger.Error("failed to create appassword session... skipping fetch", "err", err)
627
+
return
628
+
}
629
+
630
+
client := xrpc.Client{
631
+
Auth: &xrpc.AuthInfo{
632
+
AccessJwt: session.AccessJwt,
633
+
Did: session.Did,
634
+
},
635
+
Host: session.PdsEndpoint,
636
+
}
637
+
638
+
l := log.SubLogger(logger, "bluesky")
639
+
640
+
ticker := time.NewTicker(config.Bluesky.UpdateInterval)
641
+
defer ticker.Stop()
642
+
643
+
for {
644
+
posts, _, err := bsky.FetchPosts(ctx, &client, 20, "")
645
+
if err != nil {
646
+
l.Error("failed to fetch bluesky posts", "err", err)
647
+
} else if err := db.InsertBlueskyPosts(d, posts); err != nil {
648
+
l.Error("failed to insert bluesky posts", "err", err)
649
+
} else {
650
+
l.Info("inserted bluesky posts", "count", len(posts))
651
+
}
652
+
653
+
select {
654
+
case <-ticker.C:
655
+
case <-ctx.Done():
656
+
l.Info("stopping bluesky updater")
657
+
return
658
+
}
659
+
}
660
+
}
+76
appview/state/timeline.go
+76
appview/state/timeline.go
···
1
+
package state
2
+
3
+
import (
4
+
"fmt"
5
+
"net/http"
6
+
7
+
"tangled.org/core/appview/db"
8
+
"tangled.org/core/appview/pages"
9
+
"tangled.org/core/orm"
10
+
)
11
+
12
+
func (s *State) Home(w http.ResponseWriter, r *http.Request) {
13
+
// TODO: set this flag based on the UI
14
+
filtered := false
15
+
16
+
timeline, err := db.MakeTimeline(s.db, 50, "", filtered)
17
+
if err != nil {
18
+
s.logger.Error("failed to make timeline", "err", err)
19
+
s.pages.Notice(w, "timeline", "Uh oh! Failed to load timeline.")
20
+
return
21
+
}
22
+
23
+
blueskyPosts, err := db.GetBlueskyPosts(s.db, 8)
24
+
if err != nil {
25
+
s.logger.Error("failed to get bluesky posts", "err", err)
26
+
}
27
+
28
+
fmt.Println(s.pages.Home(w, pages.TimelineParams{
29
+
LoggedInUser: nil,
30
+
Timeline: timeline,
31
+
BlueskyPosts: blueskyPosts,
32
+
}))
33
+
}
34
+
func (s *State) HomeOrTimeline(w http.ResponseWriter, r *http.Request) {
35
+
if s.oauth.GetMultiAccountUser(r) != nil {
36
+
s.Timeline(w, r)
37
+
return
38
+
}
39
+
s.Home(w, r)
40
+
}
41
+
42
+
func (s *State) Timeline(w http.ResponseWriter, r *http.Request) {
43
+
user := s.oauth.GetMultiAccountUser(r)
44
+
45
+
// TODO: set this flag based on the UI
46
+
filtered := false
47
+
48
+
var userDid string
49
+
if user != nil && user.Active != nil {
50
+
userDid = user.Active.Did
51
+
}
52
+
timeline, err := db.MakeTimeline(s.db, 50, userDid, filtered)
53
+
if err != nil {
54
+
s.logger.Error("failed to make timeline", "err", err)
55
+
s.pages.Notice(w, "timeline", "Uh oh! Failed to load timeline.")
56
+
}
57
+
58
+
repos, err := db.GetTopStarredReposLastWeek(s.db)
59
+
if err != nil {
60
+
s.logger.Error("failed to get top starred repos", "err", err)
61
+
s.pages.Notice(w, "topstarredrepos", "Unable to load.")
62
+
return
63
+
}
64
+
65
+
gfiLabel, err := db.GetLabelDefinition(s.db, orm.FilterEq("at_uri", s.config.Label.GoodFirstIssue))
66
+
if err != nil {
67
+
// non-fatal
68
+
}
69
+
70
+
s.pages.Timeline(w, pages.TimelineParams{
71
+
LoggedInUser: user,
72
+
Timeline: timeline,
73
+
Repos: repos,
74
+
GfiLabel: gfiLabel,
75
+
})
76
+
}
History
5 rounds
0 comments
1 commit
expand
collapse
appview: new landing page
Signed-off-by: oppiliappan <me@oppi.li>
1/3 failed, 2/3 success
expand
collapse
expand 0 comments
pull request successfully merged
1 commit
expand
collapse
appview: new landing page
Signed-off-by: oppiliappan <me@oppi.li>
1/3 failed, 2/3 success
expand
collapse
expand 0 comments
1 commit
expand
collapse
appview: new landing page
Signed-off-by: oppiliappan <me@oppi.li>