From f7b37b6d33bdbe651e00301542494b2c34a51c77 Mon Sep 17 00:00:00 2001 From: Will Garrison Date: Wed, 27 May 2026 19:19:55 -0700 Subject: [PATCH] Allow local accounts to change display name --- features/api/profile.go | 60 +++++- features/auth/link.go | 7 + features/profile/handlers.go | 117 +++++++++-- features/profile/pages/profile_edit.templ | 78 +++++-- features/profile/pages/profile_edit_templ.go | 207 ++++++++++++------- features/profile/view.go | 54 +++-- go.mod | 16 +- go.sum | 2 - internal/oauthclient/oauthclient.go | 17 +- internal/oauthclient/oauthclient_test.go | 1 + internal/profile/profile.go | 106 +++++++++- web/resources/static/css/terminal.css | 35 ++++ 12 files changed, 561 insertions(+), 139 deletions(-) diff --git a/features/api/profile.go b/features/api/profile.go index 0074d98..8d42d70 100644 --- a/features/api/profile.go +++ b/features/api/profile.go @@ -1,10 +1,17 @@ package api import ( + "bytes" + "encoding/base64" "errors" "log/slog" "net/http" + "strings" + "github.com/bluesky-social/indigo/atproto/syntax" + "github.com/bluesky-social/indigo/lex/util" + + "atmoquest/internal/oauthclient" "atmoquest/internal/profile" ) @@ -13,6 +20,7 @@ type profileResponse struct { DID string `json:"did"` Handle string `json:"handle"` DisplayName string `json:"displayName"` + AvatarURL string `json:"avatarURL"` Bio string `json:"bio"` Interests []string `json:"interests"` Links []profileLinkPayload `json:"links"` @@ -45,7 +53,7 @@ func (h *Handlers) GetProfile(w http.ResponseWriter, r *http.Request) { slog.Warn("api: fetch quest profile", "did", did, "err", err) } - resp := buildProfileResponse(did.String(), h.resolveHandle(r, did.String()), bsky, quest) + resp := buildProfileResponse(pds, did, h.resolveHandle(r, did.String()), bsky, quest) writeJSON(w, http.StatusOK, resp) } @@ -81,7 +89,7 @@ func (h *Handlers) GetUser(w http.ResponseWriter, r *http.Request) { slog.Warn("api: fetch user quest", "target", targetDID, "err", err) } - resp := buildProfileResponse(targetDID.String(), h.resolveHandle(r, targetDID.String()), bsky, quest) + resp := buildProfileResponse(pds, targetDID, h.resolveHandle(r, targetDID.String()), bsky, quest) writeJSON(w, http.StatusOK, resp) } @@ -101,12 +109,55 @@ func (h *Handlers) UpdateProfile(w http.ResponseWriter, r *http.Request) { IsLooking bool `json:"isLooking"` Interests []string `json:"interests"` Links []profileLinkPayload `json:"links"` + AvatarData string `json:"avatarData"` + AvatarMime string `json:"avatarMime"` + ClearAvatar bool `json:"clearAvatar"` } if err := decodeBody(r, &body); err != nil { writeError(w, http.StatusBadRequest, "invalid JSON") return } + // --- Display name + avatar handling --- + if oauthclient.HasBskyProfileScope(sess) { + displayName := strings.TrimSpace(body.DisplayName) + var avatarRef *util.LexBlob + + if body.AvatarData != "" { + mimeType := body.AvatarMime + if mimeType == "" { + mimeType = "image/jpeg" + } + if _, ok := profile.AllowedAvatarMimeTypes[mimeType]; !ok { + writeError(w, http.StatusBadRequest, "unsupported image type") + return + } + decoded, err := base64.StdEncoding.DecodeString(body.AvatarData) + if err != nil { + writeError(w, http.StatusBadRequest, "invalid base64 avatar data") + return + } + if len(decoded) > profile.MaxAvatarBytes { + writeError(w, http.StatusBadRequest, "avatar too large (max 1 MB)") + return + } + avatarRef, err = profile.UploadAvatar(r.Context(), sess, bytes.NewReader(decoded), mimeType) + if err != nil { + slog.Warn("api: upload avatar", "did", did, "err", err) + writeError(w, http.StatusInternalServerError, "failed to upload avatar") + return + } + } + + if displayName != "" || avatarRef != nil || body.ClearAvatar { + if _, err := profile.PutBluesky(r.Context(), sess, did, displayName, avatarRef, body.ClearAvatar); err != nil { + slog.Warn("api: update bsky profile", "did", did, "err", err) + writeError(w, http.StatusInternalServerError, "failed to update display name / avatar") + return + } + } + } + // Build the quest profile record. hiring := body.IsHiring looking := body.IsLooking @@ -133,9 +184,9 @@ func (h *Handlers) UpdateProfile(w http.ResponseWriter, r *http.Request) { } // buildProfileResponse creates a profileResponse from fetched PDS data. -func buildProfileResponse(didStr, handle string, bsky *profile.BlueskyRecord, quest *profile.QuestRecord) profileResponse { +func buildProfileResponse(pdsHost string, did syntax.DID, handle string, bsky *profile.BlueskyRecord, quest *profile.QuestRecord) profileResponse { resp := profileResponse{ - DID: didStr, + DID: did.String(), Handle: handle, Interests: []string{}, Links: []profileLinkPayload{}, @@ -143,6 +194,7 @@ func buildProfileResponse(didStr, handle string, bsky *profile.BlueskyRecord, qu if bsky != nil { resp.DisplayName = bsky.DisplayName + resp.AvatarURL = profile.AvatarURL(pdsHost, did, bsky.Avatar.CID()) resp.Bio = bsky.Description } diff --git a/features/auth/link.go b/features/auth/link.go index b685b5b..ed6356e 100644 --- a/features/auth/link.go +++ b/features/auth/link.go @@ -133,6 +133,13 @@ func (h *Handlers) LinkLocalToATProto(w http.ResponseWriter, r *http.Request) { if _, err := profile.PutQuest(ctx, sess, did, rec); err != nil { slog.Warn("link: migrate profile", "err", err) } + + // 3b. Migrate display name to Bluesky profile + if displayName != "" { + if _, err := profile.PutBluesky(ctx, sess, did, displayName, nil, false); err != nil { + slog.Warn("link: migrate bluesky display name", "err", err) + } + } } // 4. Migrate badges to PDS diff --git a/features/profile/handlers.go b/features/profile/handlers.go index 13cd911..7acaea8 100644 --- a/features/profile/handlers.go +++ b/features/profile/handlers.go @@ -7,18 +7,22 @@ package profile import ( + "bytes" "database/sql" "encoding/json" "errors" + "io" "log/slog" "net/http" "strings" "github.com/go-chi/chi/v5" + "github.com/bluesky-social/indigo/lex/util" "atmoquest/features/auth" "atmoquest/features/profile/pages" "atmoquest/internal/badge" + "atmoquest/internal/oauthclient" "atmoquest/internal/profile" ) @@ -205,14 +209,17 @@ func (h *Handlers) renderLocalProfileEdit(w http.ResponseWriter, r *http.Request // Build profile view for edit form view := pages.ProfileEditView{ - DID: localID, - DisplayName: displayName, - Bio: bio, - Location: location, - WorksAt: worksAt, - ContactMethod: contactMethod, - Hiring: hiring, - Looking: looking, + DID: localID, + DisplayName: displayName, + DisplayNameDisabled: false, // local users can edit their display name + AvatarDisabled: true, // no avatar support for local accounts + AvatarURL: "", + Bio: bio, + Location: location, + WorksAt: worksAt, + ContactMethod: contactMethod, + Hiring: hiring, + Looking: looking, } // Parse interests and links from JSON @@ -254,6 +261,7 @@ func (h *Handlers) ProfileEdit(w http.ResponseWriter, r *http.Request) { return } + missingScope := !oauthclient.HasBskyProfileScope(sess) pds := sess.Data.HostURL bsky, err := profile.FetchBluesky(r.Context(), pds, did) @@ -267,7 +275,7 @@ func (h *Handlers) ProfileEdit(w http.ResponseWriter, r *http.Request) { quest = nil } - view := buildProfileEditView(did, pds, bsky, quest, "") + view := buildProfileEditView(did, pds, bsky, quest, "", missingScope) w.Header().Set("Content-Type", "text/html; charset=utf-8") if err := pages.ProfileEdit(view).Render(r.Context(), w); err != nil { slog.Error("render profile edit", "err", err) @@ -293,25 +301,103 @@ func (h *Handlers) ProfileSave(w http.ResponseWriter, r *http.Request) { return } - if err := r.ParseForm(); err != nil { + missingScope := !oauthclient.HasBskyProfileScope(sess) + + if err := r.ParseMultipartForm(32 << 20); err != nil { http.Error(w, "invalid form", http.StatusBadRequest) return } + displayName := strings.TrimSpace(r.FormValue("display_name")) + clearAvatar := r.FormValue("clear_avatar") == "1" + rec, formErr := parseProfileForm(r) if formErr != "" { bsky, _ := profile.FetchBluesky(r.Context(), sess.Data.HostURL, did) - view := buildProfileEditViewFromRec(did, sess.Data.HostURL, bsky, rec, formErr) + view := buildProfileEditViewFromRec(did, sess.Data.HostURL, bsky, rec, formErr, missingScope, displayName) w.Header().Set("Content-Type", "text/html; charset=utf-8") w.WriteHeader(http.StatusBadRequest) _ = pages.ProfileEdit(view).Render(r.Context(), w) return } + // --- Display name + avatar handling --- + if !missingScope { + var avatarRef *util.LexBlob + file, header, err := r.FormFile("avatar") + if err == nil { + defer file.Close() + mimeType := header.Header.Get("Content-Type") + if _, ok := profile.AllowedAvatarMimeTypes[mimeType]; !ok { + bsky, _ := profile.FetchBluesky(r.Context(), sess.Data.HostURL, did) + view := buildProfileEditViewFromRec(did, sess.Data.HostURL, bsky, rec, "unsupported file type — use JPEG, PNG, GIF, or WebP", missingScope, displayName) + w.Header().Set("Content-Type", "text/html; charset=utf-8") + w.WriteHeader(http.StatusBadRequest) + _ = pages.ProfileEdit(view).Render(r.Context(), w) + return + } + if header.Size > profile.MaxAvatarBytes { + bsky, _ := profile.FetchBluesky(r.Context(), sess.Data.HostURL, did) + view := buildProfileEditViewFromRec(did, sess.Data.HostURL, bsky, rec, "file too large — max 1 MB", missingScope, displayName) + w.Header().Set("Content-Type", "text/html; charset=utf-8") + w.WriteHeader(http.StatusBadRequest) + _ = pages.ProfileEdit(view).Render(r.Context(), w) + return + } + data, err := io.ReadAll(file) + if err != nil { + bsky, _ := profile.FetchBluesky(r.Context(), sess.Data.HostURL, did) + view := buildProfileEditViewFromRec(did, sess.Data.HostURL, bsky, rec, "couldn't read uploaded file", missingScope, displayName) + w.Header().Set("Content-Type", "text/html; charset=utf-8") + w.WriteHeader(http.StatusBadRequest) + _ = pages.ProfileEdit(view).Render(r.Context(), w) + return + } + avatarRef, err = profile.UploadAvatar(r.Context(), sess, bytes.NewReader(data), mimeType) + if err != nil { + slog.Warn("profile save: upload avatar", "did", did.String(), "err", err) + bsky, _ := profile.FetchBluesky(r.Context(), sess.Data.HostURL, did) + view := buildProfileEditViewFromRec(did, sess.Data.HostURL, bsky, rec, "couldn't upload avatar — please try again", missingScope, displayName) + w.Header().Set("Content-Type", "text/html; charset=utf-8") + w.WriteHeader(http.StatusBadGateway) + _ = pages.ProfileEdit(view).Render(r.Context(), w) + return + } + } else if err != http.ErrMissingFile { + slog.Warn("profile save: form file", "did", did.String(), "err", err) + bsky, _ := profile.FetchBluesky(r.Context(), sess.Data.HostURL, did) + view := buildProfileEditViewFromRec(did, sess.Data.HostURL, bsky, rec, "couldn't read file upload", missingScope, displayName) + w.Header().Set("Content-Type", "text/html; charset=utf-8") + w.WriteHeader(http.StatusBadRequest) + _ = pages.ProfileEdit(view).Render(r.Context(), w) + return + } + + if displayName != "" || avatarRef != nil || clearAvatar { + if _, err := profile.PutBluesky(r.Context(), sess, did, displayName, avatarRef, clearAvatar); err != nil { + slog.Warn("profile save: put bluesky", "did", did.String(), "err", err) + bsky, _ := profile.FetchBluesky(r.Context(), sess.Data.HostURL, did) + view := buildProfileEditViewFromRec(did, sess.Data.HostURL, bsky, rec, "couldn't save display name / avatar — please try again", missingScope, displayName) + w.Header().Set("Content-Type", "text/html; charset=utf-8") + w.WriteHeader(http.StatusBadGateway) + _ = pages.ProfileEdit(view).Render(r.Context(), w) + return + } + } + } else if r.FormValue("display_name") != "" || r.FormValue("clear_avatar") == "1" || len(r.MultipartForm.File["avatar"]) > 0 { + // User tried to edit display name/avatar without the scope. + bsky, _ := profile.FetchBluesky(r.Context(), sess.Data.HostURL, did) + view := buildProfileEditViewFromRec(did, sess.Data.HostURL, bsky, rec, "Missing required permission. Sign out and sign back in to edit your display name and avatar.", missingScope, displayName) + w.Header().Set("Content-Type", "text/html; charset=utf-8") + w.WriteHeader(http.StatusForbidden) + _ = pages.ProfileEdit(view).Render(r.Context(), w) + return + } + if _, err := profile.PutQuest(r.Context(), sess, did, rec); err != nil { slog.Warn("profile save", "did", did.String(), "err", err) bsky, _ := profile.FetchBluesky(r.Context(), sess.Data.HostURL, did) - view := buildProfileEditViewFromRec(did, sess.Data.HostURL, bsky, rec, "couldn't save to your PDS — please try again") + view := buildProfileEditViewFromRec(did, sess.Data.HostURL, bsky, rec, "couldn't save to your PDS — please try again", missingScope, displayName) w.Header().Set("Content-Type", "text/html; charset=utf-8") w.WriteHeader(http.StatusBadGateway) _ = pages.ProfileEdit(view).Render(r.Context(), w) @@ -326,12 +412,13 @@ func (h *Handlers) ProfileSave(w http.ResponseWriter, r *http.Request) { func (h *Handlers) saveLocalProfile(w http.ResponseWriter, r *http.Request, localID string) { ctx := r.Context() - if err := r.ParseForm(); err != nil { + if err := r.ParseMultipartForm(32 << 20); err != nil { http.Error(w, "invalid form", http.StatusBadRequest) return } // Parse form data + displayName := strings.TrimSpace(r.FormValue("display_name")) bio := r.FormValue("bio") interestsStr := r.FormValue("interests") location := r.FormValue("location") @@ -358,10 +445,10 @@ func (h *Handlers) saveLocalProfile(w http.ResponseWriter, r *http.Request, loca // Update users table _, err := h.DB.ExecContext(ctx, ` - UPDATE users SET bio = ?, location = ?, works_at = ?, contact_method = ?, + UPDATE users SET display_name = ?, bio = ?, location = ?, works_at = ?, contact_method = ?, hiring = ?, "looking" = ?, hide_badges = ?, interests = ?, links = ? WHERE did = ? - `, bio, location, worksAt, contactMethod, hiring, looking, hideBadges, string(interestsJSON), string(linksJSON), localID) + `, displayName, bio, location, worksAt, contactMethod, hiring, looking, hideBadges, string(interestsJSON), string(linksJSON), localID) if err != nil { slog.Error("save local profile", "err", err) http.Error(w, "failed to save", http.StatusInternalServerError) diff --git a/features/profile/pages/profile_edit.templ b/features/profile/pages/profile_edit.templ index f5ca188..91127ad 100644 --- a/features/profile/pages/profile_edit.templ +++ b/features/profile/pages/profile_edit.templ @@ -6,8 +6,8 @@ import ( ) // ProfileEditView is the data the edit form template renders. Reuses -// ProfileLink from profile.templ. All Bluesky fields are read-only display -// hints; the only editable atmo.quest fields are Bio, Interests, Links. +// ProfileLink from profile.templ. Display name and avatar are now editable +// and written to app.bsky.actor.profile. type ProfileEditView struct { DID string DisplayName string @@ -28,6 +28,14 @@ type ProfileEditView struct { // (standard checkbox semantics) — so a plain bool here is fine. Hiring bool Looking bool + // DisplayNameDisabled is true when the session lacks the + // repo:app.bsky.actor.profile scope. + DisplayNameDisabled bool + // AvatarDisabled is true when the session lacks the scope. + AvatarDisabled bool + // MissingScope is true when the user needs to re-authenticate to edit + // display name and avatar. + MissingScope bool // Error is rendered above the form when set (e.g. validation failure). Error string } @@ -68,22 +76,62 @@ templ ProfileEdit(v ProfileEditView) { profile --edit +
+ + if v.MissingScope { + + } +
-
- if v.AvatarURL != "" { - { +
+
+ if v.AvatarURL != "" { + { + } else { + + } +
+ if v.AvatarDisabled { + if v.MissingScope { +

sign out and sign back in to change your avatar

+ } else { +

Link your ATmosphere account to upload a profile picture

+ } } else { - + + + if v.AvatarURL != "" { + + } + } +
+ +
+ +
+ +
+ if v.DisplayNameDisabled { +
sign out and sign back in to edit your display name
}
- if v.DisplayName != "" { -

{ v.DisplayName }

- } else { -

(no display name)

- } -

- display name and avatar are read from app.bsky.actor.profile — edit them on bluesky. -

if v.Error != "" { @@ -91,8 +139,6 @@ templ ProfileEdit(v ProfileEditView) { error: { v.Error }
} - -
diff --git a/features/profile/pages/profile_edit_templ.go b/features/profile/pages/profile_edit_templ.go index 015b86f..b4034ca 100644 --- a/features/profile/pages/profile_edit_templ.go +++ b/features/profile/pages/profile_edit_templ.go @@ -14,8 +14,8 @@ import ( ) // ProfileEditView is the data the edit form template renders. Reuses -// ProfileLink from profile.templ. All Bluesky fields are read-only display -// hints; the only editable atmo.quest fields are Bio, Interests, Links. +// ProfileLink from profile.templ. Display name and avatar are now editable +// and written to app.bsky.actor.profile. type ProfileEditView struct { DID string DisplayName string @@ -36,6 +36,14 @@ type ProfileEditView struct { // (standard checkbox semantics) — so a plain bool here is fine. Hiring bool Looking bool + // DisplayNameDisabled is true when the session lacks the + // repo:app.bsky.actor.profile scope. + DisplayNameDisabled bool + // AvatarDisabled is true when the session lacks the scope. + AvatarDisabled bool + // MissingScope is true when the user needs to re-authenticate to edit + // display name and avatar. + MissingScope bool // Error is rendered above the form when set (e.g. validation failure). Error string } @@ -108,303 +116,346 @@ func ProfileEdit(v ProfileEditView) templ.Component { var templ_7745c5c3_Var3 string templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs("@") if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `features/profile/pages/profile_edit.templ`, Line: 67, Col: 56} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `features/profile/pages/profile_edit.templ`, Line: 75, Col: 56} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "atmo.quest:~$ profile --edit
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "atmo.quest:~$ profile --edit
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if v.MissingScope { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "
info: To edit your display name and avatar, you need to re-authenticate with updated permissions. sign out and sign back in
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } if v.AvatarURL != "" { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "\"")") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "\" loading=\"lazy\">") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } else { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "
?
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "
?
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - if v.DisplayName != "" { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "

") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - var templ_7745c5c3_Var6 string - templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(v.DisplayName) - if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `features/profile/pages/profile_edit.templ`, Line: 80, Col: 46} + if v.AvatarDisabled { + if v.MissingScope { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "

sign out and sign back in to change your avatar

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } else { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "

Link your ATmosphere account to upload a profile picture

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6)) + } else { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, " ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "

") + if v.AvatarURL != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "
(no display name)") + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, " placeholder=\"your name\">
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if v.DisplayNameDisabled { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "
sign out and sign back in to edit your display name
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "

display name and avatar are read from app.bsky.actor.profile — edit them on bluesky.

") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } if v.Error != "" { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "
error: ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "
error: ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var7 string templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(v.Error) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `features/profile/pages/profile_edit.templ`, Line: 91, Col: 61} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `features/profile/pages/profile_edit.templ`, Line: 139, Col: 61} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } if v.BlueskyBio != "" { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "leave blank to fall back to your bluesky bio:
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "leave blank to fall back to your bluesky bio:
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var10 string templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(v.BlueskyBio) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `features/profile/pages/profile_edit.templ`, Line: 112, Col: 42} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `features/profile/pages/profile_edit.templ`, Line: 158, Col: 42} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } else { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "tell people what you're about. up to 256 characters.") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "tell people what you're about. up to 256 characters.") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "
where you spend your weekdays. up to 120 characters.
where you spend your weekdays. up to 120 characters.
city, region, or vibe. up to 120 characters.
city, region, or vibe. up to 120 characters.
how people should reach out after a connect. up to 200 characters.
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "\" maxlength=\"200\" placeholder=\"DM on bsky · email me · signal: …\">
how people should reach out after a connect. up to 200 characters.
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } for _, t := range v.Interests { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "\">") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var15 string templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(t) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `features/profile/pages/profile_edit.templ`, Line: 178, Col: 42} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `features/profile/pages/profile_edit.templ`, Line: 224, Col: 42} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, " ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, "\" data-tag-remove>×") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "\">
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } for _, s := range suggestedInterests { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, "\">") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "
pick from suggestions or add your own. up to 30, 64 characters each.
status
status
shown as pills above your bio. toggle off to hide.
links (up to 5) ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 45, "> ● looking for work
shown as pills above your bio. toggle off to hide.
links (up to 5) ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -414,7 +465,7 @@ func ProfileEdit(v ProfileEditView) templ.Component { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "
pinned links shown on your profile. label + URL. http(s) only.
cancel
atmo.quest
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 46, "
pinned links shown on your profile. label + URL. http(s) only.
cancel
atmo.quest
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -454,98 +505,98 @@ func profileLinkRow(idx int, l ProfileLink) templ.Component { templ_7745c5c3_Var19 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "