diff --git a/features/profile/handlers.go b/features/profile/handlers.go
index 9f2300f..9623e3e 100644
--- a/features/profile/handlers.go
+++ b/features/profile/handlers.go
@@ -195,14 +195,14 @@ func (h *Handlers) renderLocalProfileEdit(w http.ResponseWriter, r *http.Request
// Fetch user data from users table
var displayName, handle, email, bio, location, worksAt, contactMethod string
- var hiring, looking, hideFromLeaderboard, hideBadges bool
+ var hiring, looking, hideFromLeaderboard, hideBadges, followUp bool
var interestsJSON, linksJSON string
err := h.DB.QueryRowContext(ctx, `
SELECT display_name, handle, email, bio, location, works_at, contact_method,
- hiring, "looking", hide_from_leaderboard, hide_badges, interests, links
+ hiring, "looking", hide_from_leaderboard, hide_badges, interests, links, follow_up
FROM users WHERE did = ?
`, localID).Scan(&displayName, &handle, &email, &bio, &location, &worksAt, &contactMethod,
- &hiring, &looking, &hideFromLeaderboard, &hideBadges, &interestsJSON, &linksJSON)
+ &hiring, &looking, &hideFromLeaderboard, &hideBadges, &interestsJSON, &linksJSON, &followUp)
if err != nil {
slog.Error("local profile edit: fetch user", "err", err)
http.Error(w, "user not found", http.StatusNotFound)
@@ -215,6 +215,7 @@ func (h *Handlers) renderLocalProfileEdit(w http.ResponseWriter, r *http.Request
IsLocal: true,
DisplayName: displayName,
Email: email,
+ FollowUp: followUp,
DisplayNameDisabled: false, // local users can edit their display name
AvatarDisabled: true, // no avatar support for local accounts
AvatarURL: "",
@@ -432,6 +433,7 @@ func (h *Handlers) saveLocalProfile(w http.ResponseWriter, r *http.Request, loca
hiring := r.FormValue("hiring") == "1"
looking := r.FormValue("looking") == "1"
hideBadges := r.FormValue("hide_badges") == "1"
+ followUp := r.FormValue("follow_up") == "1"
// Parse interests into JSON
interests := parseInterests(interestsStr)
@@ -451,9 +453,9 @@ func (h *Handlers) saveLocalProfile(w http.ResponseWriter, r *http.Request, loca
// Update users table
_, err := h.DB.ExecContext(ctx, `
UPDATE users SET display_name = ?, email = ?, bio = ?, location = ?, works_at = ?, contact_method = ?,
- hiring = ?, "looking" = ?, hide_badges = ?, interests = ?, links = ?
+ hiring = ?, "looking" = ?, hide_badges = ?, follow_up = ?, interests = ?, links = ?
WHERE did = ?
- `, displayName, email, bio, location, worksAt, contactMethod, hiring, looking, hideBadges, string(interestsJSON), string(linksJSON), localID)
+ `, displayName, email, bio, location, worksAt, contactMethod, hiring, looking, hideBadges, followUp, 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 e16f1f3..9b9d003 100644
--- a/features/profile/pages/profile_edit.templ
+++ b/features/profile/pages/profile_edit.templ
@@ -31,6 +31,7 @@ type ProfileEditView struct {
// (standard checkbox semantics) — so a plain bool here is fine.
Hiring bool
Looking bool
+ FollowUp bool
// DisplayNameDisabled is true when the session lacks the
// repo:app.bsky.actor.profile scope.
DisplayNameDisabled bool
@@ -97,6 +98,9 @@ templ ProfileEdit(v ProfileEditView) {
+ if !v.IsLocal {
+
These fields are shared with your bluesky profile.
+ }
if v.AvatarURL != "" {

@@ -111,48 +115,69 @@ templ ProfileEdit(v ProfileEditView) {
Link your ATmosphere account to upload a profile picture
}
} else {
-
+
+
+ if v.AvatarURL != "" {
+
+
+ }
+
- if v.AvatarURL != "" {
-
- }
}
-
-
-
diff --git a/features/profile/pages/profile_edit_templ.go b/features/profile/pages/profile_edit_templ.go
index a2433a6..2a9f9de 100644
--- a/features/profile/pages/profile_edit_templ.go
+++ b/features/profile/pages/profile_edit_templ.go
@@ -37,8 +37,9 @@ type ProfileEditView struct {
// underlying record uses *bool so we can distinguish "unset" from
// "explicitly false", but the form only ever submits "true" or absent
// (standard checkbox semantics) — so a plain bool here is fine.
- Hiring bool
- Looking bool
+ Hiring bool
+ Looking bool
+ FollowUp bool
// DisplayNameDisabled is true when the session lacks the
// repo:app.bsky.actor.profile scope.
DisplayNameDisabled bool
@@ -129,7 +130,7 @@ 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: 84, Col: 56}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `features/profile/pages/profile_edit.templ`, Line: 85, Col: 56}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
if templ_7745c5c3_Err != nil {
@@ -145,353 +146,387 @@ func ProfileEdit(v ProfileEditView) templ.Component {
return templ_7745c5c3_Err
}
}
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "
")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "
")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ if !v.IsLocal {
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "
These fields are shared with your bluesky profile.
")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "
")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if v.AvatarURL != "" {
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "
)
")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "\" loading=\"lazy\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else {
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "
?
")
+ 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, 12, "
")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "
")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if v.AvatarDisabled {
if v.MissingScope {
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "
sign out and sign back in to change your avatar
")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "
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, 14, "
Link your ATmosphere account to upload a profile picture
")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "
Link your ATmosphere account to upload a profile picture
")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
} else {
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "
")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "
")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if v.AvatarURL != "" {
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, " ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "
")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
}
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "
")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if v.IsLocal {
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "
")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "\" maxlength=\"320\" placeholder=\"you@example.com\">
")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "
")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "
")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ if v.IsLocal {
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "
")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "
")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if v.Error != "" {
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "
error: ")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "
error: ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var8 string
templ_7745c5c3_Var8, 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: 161, Col: 61}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `features/profile/pages/profile_edit.templ`, Line: 186, Col: 61}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
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, 34, "
")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "
")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 44, "\" 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, 38, "")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 46, "\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var16 string
templ_7745c5c3_Var16, 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: 246, Col: 42}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `features/profile/pages/profile_edit.templ`, Line: 271, Col: 42}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, " ")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 48, "\" data-tag-remove>×")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, "
pick from suggestions or add your own. up to 30, 64 characters each.
shown as pills above your bio. toggle off to hide.
")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@@ -501,7 +536,7 @@ func ProfileEdit(v ProfileEditView) templ.Component {
return templ_7745c5c3_Err
}
}
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 51, "pinned links shown on your profile. label + URL. http(s) only.
")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 58, "
pinned links shown on your profile. label + URL. http(s) only.
")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@@ -541,98 +576,98 @@ func profileLinkRow(idx int, l ProfileLink) templ.Component {
templ_7745c5c3_Var20 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 52, "#")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 59, "
#")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var21 string
templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.JoinStringErrs(strconv.Itoa(idx + 1))
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `features/profile/pages/profile_edit.templ`, Line: 345, Col: 75}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `features/profile/pages/profile_edit.templ`, Line: 370, Col: 75}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var21))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 53, "
")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 66, "\">
")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
diff --git a/web/resources/static/css/terminal.css b/web/resources/static/css/terminal.css
index 40155b3..2f4afdf 100644
--- a/web/resources/static/css/terminal.css
+++ b/web/resources/static/css/terminal.css
@@ -485,6 +485,25 @@ h1 .cursor {
background: linear-gradient(180deg, rgba(180, 190, 254, 0.04), transparent 60%);
}
+/* Side-by-side field row inside the profile card for display name + email. */
+.profile-card-fields-row {
+ display: flex;
+ gap: 12px;
+ justify-content: center;
+ width: 100%;
+}
+.profile-card-fields-row > .field {
+ min-width: 0;
+}
+
+/* Styled checkbox used in the profile editor for follow-up consent.
+ Shares the same visual as the signup flow checkbox. */
+.profile-editor-checkbox {
+ margin: 4px 0 0;
+ width: 100%;
+ text-align: left;
+}
+
/* "✓ connected with did:…" status banner shown after a successful confirm. */
.connected-banner {
display: flex;
@@ -1959,6 +1978,17 @@ footer a:hover { color: var(--lavender); }
background: rgba(243, 139, 168, 0.25);
}
+/* Text-only danger button — red text, no background/border. */
+.btn-text-danger {
+ background: none;
+ border: none;
+ color: var(--red);
+ cursor: pointer;
+}
+.btn-text-danger:hover {
+ color: var(--pink);
+}
+
.settings-nav {
margin: 12px 0 16px;
}
@@ -3554,6 +3584,11 @@ footer a:hover { color: var(--lavender); }
align-items: center;
gap: 10px;
}
+.profile-avatar-actions {
+ display: flex;
+ gap: 8px;
+ align-items: center;
+}
.profile-avatar-edit-img {
width: 128px;
height: 128px;