better profile view; fix logout button
fix issue closing, indicate alpha etc
MODIFIED
appview/pages/pages.go
MODIFIED
appview/pages/pages.go
@@ -146,6 +146,7 @@ CollaboratingRepos []db.RepoProfileStats ProfileStatsFollowStatus db.FollowStatusDidHandleMap map[string]string+ AvatarUri string}type ProfileStats struct {
@@ -3,12 +3,12 @@ <nav class="space-x-4 mb-4 px-6 py-2 rounded bg-white drop-shadow-sm"><div class="container flex justify-between p-0"><div id="left-items"><a href="/" hx-boost="true" class="flex gap-2 font-semibold italic">- tangled+ tangled<sub>alpha</sub></a></div><div id="right-items" class="flex gap-2">{{ with .LoggedInUser }}- <a href="/repo/new"hx-boost="true">+ <a href="/repo/new" hx-boost="true"><i class="w-6 h-6" data-lucide="plus"></i></a>{{ block "dropDown" . }} {{ end }}
@@ -1,76 +1,100 @@{{ define "title" }}{{ or .UserHandle .UserDid }}{{ end }}{{ define "content" }}+<div class="grid grid-cols-1 lg:grid-cols-4 gap-6">+ <div class="lg:col-span-1">+ {{ block "profileCard" . }} {{ end }}+ </div>- <div class="flex">- <h1 class="pb-1 px-6">- {{ didOrHandle .UserDid .UserHandle }}- </h1>- {{ if ne .FollowStatus.String "IsSelf" }}- <button id="followBtn"- class="btn mt-2"- {{ if eq .FollowStatus.String "IsNotFollowing" }}- hx-post="/follow?subject={{.UserDid}}"- {{ else }}- hx-delete="/follow?subject={{.UserDid}}"- {{ end }}- hx-trigger="click"- hx-target="#followBtn"- hx-swap="outerHTML"- >- {{ if eq .FollowStatus.String "IsNotFollowing" }}Follow{{ else }}Unfollow{{ end }}- </button>- {{ end }}+ <div class="lg:col-span-3">+ {{ block "ownRepos" . }} {{ end }}+ {{ block "collaboratingRepos" . }} {{ end }}+ </div>+</div>+{{ end }}++{{ define "profileCard" }}+<div class="bg-white px-6 py-4 rounded drop-shadow-sm max-h-fit">+ <div class="flex justify-center items-center">+ {{ if .AvatarUri }}+ <img class="w-1/2 lg:w-full rounded-full p-2" src="{{ .AvatarUri }}" />+ {{ end }}</div>- <div class="text-sm mb-4 px-6">+ <p class="text-xl font-bold text-center">+ {{ didOrHandle .UserDid .UserHandle }}+ </p>+ <div class="text-sm text-center"><span>{{ .ProfileStats.Followers }} followers</span><div class="inline-block px-1 select-none after:content-['·']"></div><span>{{ .ProfileStats.Following }} following</span></div>- <p class="text-sm font-bold py-2 px-6">REPOS</p>- <div id="repos" class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6">- {{ range .Repos }}- <div- id="repo-card"- class="py-4 px-6 drop-shadow-sm rounded bg-white"- >- <div id="repo-card-name" class="font-medium">- <a href="/@{{ or $.UserHandle $.UserDid }}/{{ .Name }}"- >{{ .Name }}</a++ {{ if ne .FollowStatus.String "IsSelf" }}+ <button id="followBtn"+ class="btn mt-2 w-full"+ {{ if eq .FollowStatus.String "IsNotFollowing" }}+ hx-post="/follow?subject={{.UserDid}}"+ {{ else }}+ hx-delete="/follow?subject={{.UserDid}}"+ {{ end }}+ hx-trigger="click"+ hx-target="#followBtn"+ hx-swap="outerHTML">- </div>- <div- id="repo-knot-name"- class="text-gray-600 text-sm font-mono"- >- {{ .Knot }}- </div>- </div>- {{ else }}- <p class="px-6">This user does not have any repos yet.</p>- {{ end }}+ {{ if eq .FollowStatus.String "IsNotFollowing" }}Follow{{ else }}Unfollow{{ end }}+ </button>+ {{ end }}+</div>+{{ end }}++{{ define "ownRepos" }}+<p class="text-sm font-bold py-2 px-6">REPOS</p>+<div id="repos" class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6">+ {{ range .Repos }}+ <div+ id="repo-card"+ class="py-4 px-6 drop-shadow-sm rounded bg-white"+ >+ <div id="repo-card-name" class="font-medium">+ <a href="/@{{ or $.UserHandle $.UserDid }}/{{ .Name }}"+ >{{ .Name }}</a+ >+ </div>+ <div+ id="repo-knot-name"+ class="text-gray-600 text-sm font-mono"+ >+ {{ .Knot }}+ </div></div>- <p class="text-sm font-bold py-2 px-6">COLLABORATING ON</p>- <div id="collaborating" class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6">- {{ range .CollaboratingRepos }}- <div- id="repo-card"- class="py-4 px-6 drop-shadow-sm rounded bg-white"- >- <div id="repo-card-name" class="font-medium">- <a href="/{{ index $.DidHandleMap .Did }}/{{ .Name }}">- {{ index $.DidHandleMap .Did }}/{{ .Name }}- </a>- </div>- <div- id="repo-knot-name"- class="text-gray-600 text-sm font-mono"- >- {{ .Knot }}- </div>- </div>{{ else }}- <p class="px-6">This user is not collaborating.</p>+ <p class="px-6">This user does not have any repos yet.</p>{{ end }}+</div>+{{ end }}++{{ define "collaboratingRepos" }}+<p class="text-sm font-bold py-2 px-6">COLLABORATING ON</p>+<div id="collaborating" class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6">+ {{ range .CollaboratingRepos }}+ <div+ id="repo-card"+ class="py-4 px-6 drop-shadow-sm rounded bg-white"+ >+ <div id="repo-card-name" class="font-medium">+ <a href="/{{ index $.DidHandleMap .Did }}/{{ .Name }}">+ {{ index $.DidHandleMap .Did }}/{{ .Name }}+ </a>+ </div>+ <div+ id="repo-knot-name"+ class="text-gray-600 text-sm font-mono"+ >+ {{ .Knot }}+ </div></div>+ {{ else }}+ <p class="px-6">This user is not collaborating.</p>+ {{ end }}+</div>{{ end }}
MODIFIED
appview/state/follow.go
MODIFIED
appview/state/follow.go
@@ -61,7 +61,7 @@ log.Println("created atproto record: ", resp.Uri)w.Write([]byte(fmt.Sprintf(`<button id="followBtn"- class="btn mt-2"+ class="btn mt-2 w-full"hx-delete="/follow?subject=%s"hx-trigger="click"hx-target="#followBtn"@@ -98,7 +98,7 @@ }w.Write([]byte(fmt.Sprintf(`<button id="followBtn"- class="btn mt-2"+ class="btn mt-2 w-full"hx-post="/follow?subject=%s"hx-trigger="click"hx-target="#followBtn"
MODIFIED
appview/state/repo.go
MODIFIED
appview/state/repo.go
@@ -9,6 +9,7 @@ "log""math/rand/v2""net/http""path"+ "slices""strconv""strings""time"@@ -611,8 +612,17 @@ s.pages.Notice(w, "issue-action", "Failed to close issue. Try again later.")return}+ collaborators, err := f.Collaborators(r.Context(), s)+ if err != nil {+ log.Println("failed to fetch repo collaborators: %w", err)+ }+ isCollaborator := slices.ContainsFunc(collaborators, func(collab pages.Collaborator) bool {+ return user.Did == collab.Did+ })+ isIssueOwner := user.Did == issue.OwnerDid+// TODO: make this more granular- if user.Did == f.OwnerDid() {+ if isIssueOwner || isCollaborator {closed := tangled.RepoIssueStateClosed@@ -645,7 +655,7 @@s.pages.HxLocation(w, fmt.Sprintf("/%s/issues/%d", f.OwnerSlashRepo(), issueIdInt))return} else {- log.Println("user is not the owner of the repo")+ log.Println("user is not permitted to close issue")http.Error(w, "for biden", http.StatusUnauthorized)return}
MODIFIED
appview/state/state.go
MODIFIED
appview/state/state.go
@@ -5,6 +5,7 @@ "context""crypto/hmac""crypto/sha256""encoding/hex"+ "encoding/json""fmt""log""log/slog"@@ -128,7 +129,7 @@ }func (s *State) Logout(w http.ResponseWriter, r *http.Request) {s.auth.ClearSession(r, w)- s.pages.HxRedirect(w, "/")+ http.Redirect(w, r, "/login", http.StatusTemporaryRedirect)}func (s *State) Timeline(w http.ResponseWriter, r *http.Request) {@@ -648,6 +649,11 @@ if loggedInUser != nil {followStatus = s.db.GetFollowStatus(loggedInUser.Did, ident.DID.String())}+ profileAvatarUri, err := GetAvatarUri(ident.DID.String())+ if err != nil {+ log.Println("failed to fetch bsky avatar", err)+ }+s.pages.ProfilePage(w, pages.ProfilePageParams{LoggedInUser: loggedInUser,UserDid: ident.DID.String(),@@ -660,7 +666,53 @@ Following: following,},FollowStatus: db.FollowStatus(followStatus),DidHandleMap: didHandleMap,+ AvatarUri: profileAvatarUri,})+}++func GetAvatarUri(did string) (string, error) {+ recordURL := fmt.Sprintf("https://bsky.social/xrpc/com.atproto.repo.getRecord?repo=%s&collection=app.bsky.actor.profile&rkey=self", did)++ recordResp, err := http.Get(recordURL)+ if err != nil {+ return "", err+ }+ defer recordResp.Body.Close()++ if recordResp.StatusCode != http.StatusOK {+ return "", fmt.Errorf("getRecord API returned status code %d", recordResp.StatusCode)+ }++ var profileResp map[string]any+ if err := json.NewDecoder(recordResp.Body).Decode(&profileResp); err != nil {+ return "", err+ }++ value, ok := profileResp["value"].(map[string]any)+ if !ok {+ log.Println(profileResp)+ return "", fmt.Errorf("no value found for handle %s", did)+ }++ avatar, ok := value["avatar"].(map[string]any)+ if !ok {+ log.Println(profileResp)+ return "", fmt.Errorf("no avatar found for handle %s", did)+ }++ blobRef, ok := avatar["ref"].(map[string]any)+ if !ok {+ log.Println(profileResp)+ return "", fmt.Errorf("no ref found for handle %s", did)+ }++ link, ok := blobRef["$link"].(string)+ if !ok {+ log.Println(profileResp)+ return "", fmt.Errorf("no link found for handle %s", did)+ }++ return fmt.Sprintf("https://cdn.bsky.app/img/feed_thumbnail/plain/%s/%s", did, link), nil}func (s *State) Router() http.Handler {