Monorepo for Tangled

appview: new landing page

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

authored by

oppiliappan and committed by tangled.org 4f28bf3b 0a8187a5

+872 -167
+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" ··· 123 124 }, 124 125 "mod": func(a, b int) int { 125 126 return a % b 127 + }, 128 + "randInt": func(bound int) int { 129 + return rand.Intn(bound) 126 130 }, 127 131 "f64": func(a int) float64 { 128 132 return float64(a)
+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 {{ 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
··· 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
··· 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 }}
+578 -65
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 + {{ end }} 22 23 23 24 25 + {{ define "content" }} 26 + <div class="flex flex-col gap-24 md:gap-40 my-24 md:my-32"> 27 + {{ template "timeline/fragments/hero" . }} 28 + {{ template "timeline/fragments/preview" . }} 29 + {{ template "features1" . }} 30 + {{ template "features2" . }} 31 + {{ template "recentUpdates" . }} 32 + {{ template "community" . }} 33 + </div> 24 34 {{ end }} 25 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 }} 26 48 27 - {{ define "content" }} 28 - <div class="flex flex-col gap-4"> 29 - {{ template "timeline/fragments/hero" . }} 30 - {{ template "features" . }} 31 - {{ template "timeline/fragments/goodfirstissues" . }} 32 - {{ template "timeline/fragments/trending" . }} 33 - {{ template "timeline/fragments/timeline" . }} 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>&mdash;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 - ) }} 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> 79 544 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 - ) }} 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 }} 92 569 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>&mdash;lightweight CI runners." 101 - "Natively supports Nix for package management." 102 - "Easily extended to support different execution backends." 103 - ) 104 - ) }} 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
··· 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 { ··· 329 285 s.pages.UpgradeBanner(w, pages.UpgradeBannerParams{ 330 286 Registrations: regs, 331 287 Spindles: spindles, 332 - }) 333 - } 334 - 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 288 }) 358 289 } 359 290 ··· 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
··· 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 + }