Monorepo for Tangled tangled.org

appview/pages: upload and render avatar #894

merged opened by anirudh.fi targeting master from icy/tolqpt
Labels

None yet.

assignee

None yet.

Participants 2
AT URI
at://did:plc:hwevmowznbiukdf6uk5dwrrq/sh.tangled.repo.pull/3m7znwnzq6k22
+114 -27
Diff #3
+22
appview/pages/funcmap.go
··· 364 364 "fullAvatar": func(handle string) string { 365 365 return p.AvatarUrl(handle, "") 366 366 }, 367 + "placeholderAvatar": func(size string) template.HTML { 368 + sizeClass := "size-6" 369 + iconSize := "size-4" 370 + if size == "tiny" { 371 + sizeClass = "size-6" 372 + iconSize = "size-4" 373 + } else if size == "small" { 374 + sizeClass = "size-8" 375 + iconSize = "size-5" 376 + } else { 377 + sizeClass = "size-12" 378 + iconSize = "size-8" 379 + } 380 + icon, _ := p.icon("user-round", []string{iconSize, "text-gray-400", "dark:text-gray-500"}) 381 + return template.HTML(fmt.Sprintf(`<div class="%s rounded-full bg-gray-200 dark:bg-gray-700 flex items-center justify-center flex-shrink-0">%s</div>`, sizeClass, icon)) 382 + }, 383 + "profileAvatarUrl": func(profile *models.Profile, size string) string { 384 + if profile != nil { 385 + return p.AvatarUrl(profile.Did, size) 386 + } 387 + return "" 388 + }, 367 389 "langColor": enry.GetColor, 368 390 "reverse": func(s any) any { 369 391 if s == nil {
+1 -1
appview/pages/templates/layouts/profilebase.html
··· 2 2 3 3 {{ define "extrameta" }} 4 4 {{ $handle := resolve .Card.UserDid }} 5 - {{ $avatarUrl := fullAvatar $handle }} 5 + {{ $avatarUrl := profileAvatarUrl .Card.Profile "" }} 6 6 <meta property="og:title" content="{{ $handle }}" /> 7 7 <meta property="og:type" content="profile" /> 8 8 <meta property="og:url" content="https://tangled.org/{{ $handle }}?tab={{ .Active }}" />
+44
appview/pages/templates/user/fragments/editAvatar.html
··· 1 + {{ define "user/fragments/editAvatar" }} 2 + <form 3 + hx-post="/profile/avatar" 4 + hx-encoding="multipart/form-data" 5 + hx-indicator="#spinner" 6 + hx-swap="none" 7 + class="flex flex-col gap-2"> 8 + <label for="avatar-file" class="uppercase p-0"> 9 + Upload avatar 10 + </label> 11 + <p class="text-sm text-gray-500 dark:text-gray-400">Select an image (PNG or JPEG, max 1MB)</p> 12 + <input 13 + type="file" 14 + id="avatar-file" 15 + name="avatar" 16 + accept="image/png,image/jpeg" 17 + required 18 + class="block w-full text-sm text-gray-500 dark:text-gray-400 19 + file:mr-4 file:py-2 file:px-4 20 + file:rounded file:border-0 21 + file:text-sm file:font-semibold 22 + file:bg-gray-100 file:text-gray-700 23 + dark:file:bg-gray-700 dark:file:text-gray-300 24 + hover:file:bg-gray-200 dark:hover:file:bg-gray-600" /> 25 + <div id="avatar-error" class="text-red-500 dark:text-red-400 text-sm min-h-5"></div> 26 + <div class="flex gap-2 pt-2"> 27 + <button 28 + id="cancel-avatar-btn" 29 + type="button" 30 + popovertarget="avatar-upload-modal" 31 + popovertargetaction="hide" 32 + class="btn w-1/2 flex items-center gap-2 text-red-500 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300"> 33 + {{ i "x" "size-4" }} 34 + cancel 35 + </button> 36 + <button type="submit" class="btn w-1/2 flex items-center"> 37 + <span class="inline-flex gap-2 items-center">{{ i "upload" "size-4" }} upload</span> 38 + <span id="spinner" class="group"> 39 + {{ i "loader-circle" "ml-2 w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }} 40 + </span> 41 + </button> 42 + </div> 43 + </form> 44 + {{ end }}
+39 -22
appview/pages/templates/user/fragments/profileCard.html
··· 3 3 <div class="grid grid-cols-3 md:grid-cols-1 gap-1 items-center"> 4 4 <div id="avatar" class="col-span-1 flex justify-center items-center"> 5 5 <div class="w-3/4 aspect-square relative"> 6 - <img class="absolute inset-0 w-full h-full object-cover rounded-full p-2" src="{{ fullAvatar .UserDid }}" /> 6 + <img class="absolute inset-0 w-full h-full object-cover rounded-full p-2" src="{{ profileAvatarUrl .Profile "" }}" /> 7 + {{ if eq .FollowStatus.String "IsSelf" }} 8 + <button 9 + class="absolute bottom-2 right-2 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded-full p-2 hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors" 10 + popovertarget="avatar-upload-modal" 11 + popovertargetaction="toggle" 12 + title="Upload avatar"> 13 + {{ i "camera" "w-4 h-4" }} 14 + </button> 15 + {{ end }} 7 16 </div> 8 17 </div> 18 + <div 19 + id="avatar-upload-modal" 20 + popover 21 + class="bg-white w-full md:w-96 dark:bg-gray-800 p-4 rounded border border-gray-200 dark:border-gray-700 drop-shadow dark:text-white backdrop:bg-gray-400/50 dark:backdrop:bg-gray-800/50"> 22 + {{ template "user/fragments/editAvatar" . }} 23 + </div> 9 24 <div class="col-span-2"> 10 25 <div class="flex items-center flex-row flex-nowrap gap-2"> 11 26 <p title="{{ $userIdent }}" ··· 13 28 {{ $userIdent }} 14 29 </p> 15 30 {{ with .Profile }} 16 - {{ if .Pronouns }} 17 - <p class="text-gray-500 dark:text-gray-400">{{ .Pronouns }}</p> 18 - {{ end }} 31 + {{ if .Pronouns }} 32 + <p class="text-gray-500 dark:text-gray-400">{{ .Pronouns }}</p> 33 + {{ end }} 19 34 {{ end }} 20 35 </div> 21 36 ··· 29 44 {{ with .Profile }} 30 45 31 46 {{ if .Description }} 32 - <p class="text-base pb-4 md:pb-2">{{ .Description }}</p> 47 + <p class="text-base pb-4 md:pb-2">{{ .Description }}</p> 33 48 {{ end }} 34 49 35 50 <div class="hidden md:block"> ··· 45 60 {{ end }} 46 61 {{ if .IncludeBluesky }} 47 62 <div class="flex items-center gap-2"> 48 - <span class="flex-shrink-0">{{ template "user/fragments/bluesky" "w-4 h-4 text-black dark:text-white" 49 - }}</span> 63 + <span class="flex-shrink-0">{{ template "user/fragments/bluesky" "w-4 h-4 text-black dark:text-white" }}</span> 50 64 <a id="bluesky-link" href="https://bsky.app/profile/{{ $.UserDid }}">{{ $userIdent }}</a> 51 65 </div> 52 66 {{ end }} 53 67 {{ range $link := .Links }} 54 - {{ if $link }} 55 - <div class="flex items-center gap-2"> 56 - <span class="flex-shrink-0">{{ i "link" "size-4" }}</span> 57 - <a href="{{ $link }}">{{ $link }}</a> 58 - </div> 59 - {{ end }} 68 + {{ if $link }} 69 + <div class="flex items-center gap-2"> 70 + <span class="flex-shrink-0">{{ i "link" "size-4" }}</span> 71 + <a href="{{ $link }}">{{ $link }}</a> 72 + </div> 73 + {{ end }} 60 74 {{ end }} 61 75 {{ if not $profile.IsStatsEmpty }} 62 76 <div class="flex items-center justify-evenly gap-2 py-2"> 63 77 {{ range $stat := .Stats }} 64 - {{ if $stat.Kind }} 65 - <div class="flex flex-col items-center gap-2"> 66 - <span class="text-xl font-bold">{{ $stat.Value }}</span> 67 - <span>{{ $stat.Kind.String }}</span> 68 - </div> 69 - {{ end }} 78 + {{ if $stat.Kind }} 79 + <div class="flex flex-col items-center gap-2"> 80 + <span class="text-xl font-bold">{{ $stat.Value }}</span> 81 + <span>{{ $stat.Kind.String }}</span> 82 + </div> 83 + {{ end }} 70 84 {{ end }} 71 85 </div> 72 86 {{ end }} ··· 75 89 76 90 <div class="flex mt-2 items-center gap-2"> 77 91 {{ if ne .FollowStatus.String "IsSelf" }} 78 - {{ template "user/fragments/follow" . }} 92 + {{ template "user/fragments/follow" . }} 79 93 {{ else }} 80 - <button id="editBtn" class="btn w-full flex items-center gap-2 group" hx-target="#profile-bio" 81 - hx-get="/profile/edit-bio" hx-swap="innerHTML"> 94 + <button id="editBtn" 95 + class="btn w-full flex items-center gap-2 group" 96 + hx-target="#profile-bio" 97 + hx-get="/profile/edit-bio" 98 + hx-swap="innerHTML"> 82 99 {{ i "pencil" "w-4 h-4" }} 83 100 edit 84 101 {{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }}
+4 -2
appview/pages/templates/user/settings/emails.html
··· 62 62 hx-swap="none" 63 63 class="flex flex-col gap-2" 64 64 > 65 - <p class="uppercase p-0">ADD EMAIL</p> 65 + <label for="email-address" class="uppercase p-0"> 66 + add email 67 + </label> 66 68 <p class="text-sm text-gray-500 dark:text-gray-400">Commits using this email will be associated with your profile.</p> 67 69 <input 68 70 type="email" ··· 91 93 <div id="settings-emails-error" class="text-red-500 dark:text-red-400"></div> 92 94 <div id="settings-emails-success" class="text-green-500 dark:text-green-400"></div> 93 95 </form> 94 - {{ end }} 96 + {{ end }}
+4 -2
appview/pages/templates/user/settings/keys.html
··· 21 21 <div class="col-span-1 md:col-span-2"> 22 22 <h2 class="text-sm pb-2 uppercase font-bold">SSH Keys</h2> 23 23 <p class="text-gray-500 dark:text-gray-400"> 24 - SSH public keys added here will be broadcasted to knots that you are a member of, 24 + SSH public keys added here will be broadcasted to knots that you are a member of, 25 25 allowing you to push to repositories there. 26 26 </p> 27 27 </div> ··· 63 63 hx-swap="none" 64 64 class="flex flex-col gap-2" 65 65 > 66 - <p class="uppercase p-0">ADD SSH KEY</p> 66 + <label for="key-name" class="uppercase p-0"> 67 + add ssh key 68 + </label> 67 69 <p class="text-sm text-gray-500 dark:text-gray-400">SSH keys allow you to push to repositories in knots you're a member of.</p> 68 70 <input 69 71 type="text"

History

7 rounds 2 comments
sign up or login to add to the discussion
1 commit
expand
appview/pages: upload and render avatar
3/3 success
expand
expand 0 comments
pull request successfully merged
1 commit
expand
appview/pages: upload and render avatar
3/3 success
expand
expand 0 comments
1 commit
expand
appview/pages: upload and render avatar
3/3 success
expand
expand 0 comments
1 commit
expand
appview/pages: upload and render avatar
3/3 success
expand
expand 0 comments
1 commit
expand
appview/pages: upload and render avatar
expand 0 comments
1 commit
expand
appview/pages: upload and render avatar
expand 2 comments
  • profileAvatarUrl is only used where models.Profile is available, is the uploaded profile picture not rendered across the board? (the previous PR in the stack implies otherwise, in which case, why do we need any special logic for avatar URLs on the appview?)
  • this resolves handle again, we already have a resolved identity in ident
  • there is repetition between ProfileAvatarUrl and AvatarUrl (the signature calculation logic is duplicated)

profileAvatarUrl is only used where models.Profile is available, is the uploaded profile picture not rendered across the board? (the previous PR in the stack implies otherwise, in which case, why do we need any special logic for avatar URLs on the appview?)

it exists to construct the blob url to to fetch from the pds. the avatar service merely checks if a blob url has been provided (if not, fallback to bsky). i could clean this up though, i didn't particularly like it myself.

1 commit
expand
appview/pages: upload and render avatar
expand 0 comments