Skip to content

Commit 8b31b58

Browse files
newstlerclaude
andauthored
Redesign profile settings as dropdown, UI polish, and bug fixes (#136)
* Redesign profile settings as dropdown, UI refinements Move profile settings into a collapsible dropdown menu on user show page (both desktop and mobile). Reorder toggles to place newsletter before open-to-work, rename "Newsletter" to "Receive news". Replace "Sort:" text labels with sort icon SVG. Adjust avatar sizes on community index and testimonial heading styling. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Styled somehow the stupid bange in testimonial * Fix char counter submit target guard, conditional mobile menu controller Add hasSubmitTarget check in char_counter_controller to prevent errors when submit target is absent. Skip mobile-menu Stimulus controller on nav_minimal pages. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Fix settings menu on mobile * Trying to fix stuck quote processing update * Trying to fix stuck quote processing update * Preserve existing bio when GitHub returns empty value on refresh Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Preserve existing name, website, and twitter when GitHub returns empty values Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Don't overwrite today's star snapshot on sign-in When a user signs in mid-day, record_snapshot! was updating the existing snapshot created by the 2am scheduled job, making tomorrow's delta appear smaller. Now sign-in only creates a snapshot if none exists for today, while the scheduled batch job still force-updates to stay authoritative. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Remove legacy github_repos JSON column and rake tasks The github_repos text column on users has been replaced by the Project model. Remove the column and the two legacy rake tasks (github:update_all, github:update_user) that still wrote to it. The proper github:update_all_job task remains. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Replace portrait-phone CSS overrides with Tailwind responsive utilities for Open to Work badge Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent cc71404 commit 8b31b58

17 files changed

Lines changed: 139 additions & 286 deletions

app/assets/images/sort.svg

Lines changed: 3 additions & 0 deletions
Loading

app/helpers/application_helper.rb

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -324,6 +324,9 @@ def post_path_for(post)
324324
def cross_domain_url(domain_type, path = "/")
325325
return path unless Rails.env.production?
326326

327+
# No Warden context (e.g. rendering from a background job broadcast)
328+
return path unless respond_to?(:request) && request.present? && request.env["warden"].present?
329+
327330
domains = Rails.application.config.x.domains
328331
host = (domain_type == :primary) ? domains.primary : domains.community
329332

app/javascript/controllers/char_counter_controller.js

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -28,14 +28,16 @@ export default class extends Controller {
2828
colorClass = `${pill} bg-red-100 text-red-600`
2929
}
3030

31-
if (isValid) {
32-
this.submitTarget.disabled = false
33-
this.submitTarget.classList.remove("opacity-50", "cursor-not-allowed")
34-
this.submitTarget.classList.add("cursor-pointer", "hover:bg-red-700")
35-
} else {
36-
this.submitTarget.disabled = true
37-
this.submitTarget.classList.add("opacity-50", "cursor-not-allowed")
38-
this.submitTarget.classList.remove("cursor-pointer", "hover:bg-red-700")
31+
if (this.hasSubmitTarget) {
32+
if (isValid) {
33+
this.submitTarget.disabled = false
34+
this.submitTarget.classList.remove("opacity-50", "cursor-not-allowed")
35+
this.submitTarget.classList.add("cursor-pointer", "hover:bg-red-700")
36+
} else {
37+
this.submitTarget.disabled = true
38+
this.submitTarget.classList.add("opacity-50", "cursor-not-allowed")
39+
this.submitTarget.classList.remove("cursor-pointer", "hover:bg-red-700")
40+
}
3941
}
4042

4143
this.counterTargets.forEach(counter => {

app/jobs/generate_testimonial_fields_job.rb

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ def perform(testimonial)
2121
unless parsed
2222
Rails.logger.error "Failed to generate testimonial fields for testimonial #{testimonial.id}"
2323
testimonial.update!(ai_feedback: "We couldn't process your testimonial right now. Please try again later.")
24+
broadcast_update(testimonial)
2425
return
2526
end
2627

@@ -36,6 +37,7 @@ def perform(testimonial)
3637

3738
unless parsed
3839
testimonial.update!(ai_feedback: "We couldn't process your testimonial right now. Please try again later.")
40+
broadcast_update(testimonial)
3941
return
4042
end
4143

@@ -48,6 +50,7 @@ def perform(testimonial)
4850
rescue JSON::ParserError => e
4951
Rails.logger.error "Failed to parse AI response for testimonial #{testimonial.id}: #{e.message}"
5052
testimonial.update!(ai_feedback: "We couldn't process your testimonial right now. Please try again later.")
53+
broadcast_update(testimonial)
5154
end
5255

5356
private
@@ -100,6 +103,15 @@ def generate_fields(system_prompt, user_prompt)
100103
result ? JSON.parse(result) : nil
101104
end
102105

106+
def broadcast_update(testimonial)
107+
Turbo::StreamsChannel.broadcast_replace_to(
108+
"testimonial_#{testimonial.id}",
109+
target: "testimonial_section",
110+
partial: "testimonials/section",
111+
locals: { testimonial: testimonial, user: testimonial.user }
112+
)
113+
end
114+
103115
def heading_taken?(heading, testimonial_id)
104116
Testimonial.where.not(id: testimonial_id).exists?(heading: heading)
105117
end

app/models/project.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,9 @@ def stars_gained
1818
[ snapshots.first - snapshots.last, 0 ].max
1919
end
2020

21-
def record_snapshot!
21+
def record_snapshot!(force: false)
2222
snapshot = star_snapshots.find_or_initialize_by(recorded_on: Date.current)
23-
snapshot.update!(stars: stars)
23+
snapshot.update!(stars: stars) if snapshot.new_record? || force
2424
snapshot
2525
end
2626
end

app/services/github_data_fetcher.rb

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -166,11 +166,11 @@ def self.update_user_from_graphql(user, profile_data, repos_data)
166166
user.update!(
167167
username: profile_data[:login],
168168
email: profile_data[:email] || user.email,
169-
name: profile_data[:name],
170-
bio: profile_data[:bio],
169+
name: profile_data[:name] || user.name,
170+
bio: profile_data[:bio] || user.bio,
171171
company: profile_data[:company],
172-
website: profile_data[:websiteUrl].presence,
173-
twitter: profile_data[:twitterUsername].presence,
172+
website: profile_data[:websiteUrl].presence || user.website,
173+
twitter: profile_data[:twitterUsername].presence || user.twitter,
174174
location: profile_data[:location],
175175
avatar_url: profile_data[:avatarUrl],
176176
github_data_updated_at: Time.current
@@ -190,11 +190,11 @@ def self.update_user_from_graphql(user, profile_data, repos_data)
190190
}
191191
end
192192

193-
sync_projects!(user, repos)
193+
sync_projects!(user, repos, force_snapshot: true)
194194
end
195195

196196
# Sync GitHub repos to Project records with star snapshot tracking
197-
def self.sync_projects!(user, repos_data)
197+
def self.sync_projects!(user, repos_data, force_snapshot: false)
198198
current_urls = repos_data.map { |r| r[:github_url] || r[:url] }
199199

200200
# Soft-archive projects no longer returned by GitHub
@@ -216,7 +216,7 @@ def self.sync_projects!(user, repos_data)
216216
)
217217

218218
project.save!
219-
project.record_snapshot!
219+
project.record_snapshot!(force: force_snapshot)
220220
end
221221

222222
# Recalculate cached stats on user
@@ -255,11 +255,11 @@ def update_from_oauth_data
255255
user.update!(
256256
username: auth_data.info.nickname,
257257
email: auth_data.info.email,
258-
name: raw_info.name,
259-
bio: raw_info.bio,
258+
name: raw_info.name || user.name,
259+
bio: raw_info.bio || user.bio,
260260
company: raw_info.company,
261-
website: raw_info.blog.presence,
262-
twitter: raw_info.twitter_username.presence,
261+
website: raw_info.blog.presence || user.website,
262+
twitter: raw_info.twitter_username.presence || user.twitter,
263263
location: raw_info.location,
264264
avatar_url: auth_data.info.image
265265
)
@@ -286,11 +286,11 @@ def update_from_api
286286
user.update!(
287287
username: data["login"],
288288
email: data["email"] || user.email,
289-
name: data["name"],
290-
bio: data["bio"],
289+
name: data["name"] || user.name,
290+
bio: data["bio"] || user.bio,
291291
company: data["company"],
292-
website: data["blog"],
293-
twitter: data["twitter_username"],
292+
website: data["blog"].presence || user.website,
293+
twitter: data["twitter_username"] || user.twitter,
294294
location: data["location"],
295295
avatar_url: data["avatar_url"]
296296
)

app/views/home/_testimonial_carousel.html.erb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@
6565

6666
<%# User tile - extra top margin on mobile/tablet to account for speech bubble tail %>
6767
<div class="lg:flex-1 lg:basis-0 flex items-end mt-2 lg:mt-0">
68-
<%= render "users/user_tile", user: testimonial.user, frameless: true, avatar_size: "w-20 h-[72px] md:w-36 md:h-[130px]", compact: true %>
68+
<%= render "users/user_tile", user: testimonial.user, frameless: true, avatar_size: "w-20 h-[72px] md:w-36 md:h-[130px]", compact: true, badge_size: "xs" %>
6969
</div>
7070
</div>
7171
</div>

app/views/layouts/application.html.erb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@
9090
<noscript><img src="https://nullitics.com/n.gif?u=<%= request.original_url %><%= "&c=#{country_code}" if country_code.present? %>" alt="" style="position:absolute;left:-9999px" /></noscript>
9191

9292
<% community_nav = content_for?(:nav_community_style) || (controller_name == 'users' && ['index', 'show'].include?(action_name)) %>
93-
<nav class="<%= community_nav ? 'bg-gray-50 pt-4 [@media(min-width:640px)_and_(min-height:500px)]:pt-6' : 'bg-white shadow-sm py-4' %> sticky top-0 z-50" data-controller="mobile-menu" data-action="click@window->mobile-menu#hide">
93+
<nav class="<%= community_nav ? 'bg-gray-50 pt-4 [@media(min-width:640px)_and_(min-height:500px)]:pt-6' : 'bg-white shadow-sm py-4' %> sticky top-0 z-50" <%= 'data-controller="mobile-menu" data-action="click@window->mobile-menu#hide"'.html_safe unless content_for?(:nav_minimal) %>>
9494
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
9595
<% centered_mobile_nav = content_for?(:nav_community_style) || (controller_name == 'users' && ['index', 'show'].include?(action_name)) %>
9696
<div class="flex items-center <%= centered_mobile_nav ? 'h-14 [@media(min-width:640px)_and_(min-height:500px)]:h-20 justify-center' : 'h-16 justify-between' %>">

app/views/shared/_gem_avatar.html.erb

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
<% show_badge = local_assigns.key?(:show_open_to_work) ? show_open_to_work : user.open_to_work? %>
1313
<% small_badge = local_assigns[:badge_size] == "small" %>
1414
<% tiny_badge = local_assigns[:badge_size] == "tiny" %>
15+
<% xs_badge = local_assigns[:badge_size] == "xs" %>
1516
<% map_badge = local_assigns[:badge_size] == "map" %>
1617
<% thick_border = local_assigns[:thick_border] == true %>
1718
<div class="<%= size %> flex-shrink-0 relative overflow-visible">
@@ -45,6 +46,10 @@
4546
<div class="absolute -bottom-[1px] left-1/2 -translate-x-1/2 z-10">
4647
<span class="inline-block px-[3px] py-[0.5px] text-[5px] font-bold uppercase tracking-wide text-white bg-red-600 rounded-full whitespace-nowrap" style="line-height:1.2;letter-spacing:0.3px;">Open to work</span>
4748
</div>
49+
<% elsif xs_badge %>
50+
<div class="absolute -bottom-[1px] md:bottom-1 left-1/2 -translate-x-1/2 z-10">
51+
<span class="inline-block px-[3px] md:px-2 py-[0.5px] md:py-0.5 text-[5px] md:text-[10px] font-bold uppercase leading-[1.2] tracking-[0.3px] text-white bg-red-600 rounded-full whitespace-nowrap shadow-sm">Open to work</span>
52+
</div>
4853
<% else %>
4954
<div class="absolute <%= small_badge ? 'bottom-0' : 'bottom-1' %> left-1/2 -translate-x-1/2 z-10">
5055
<span class="inline-block <%= small_badge ? 'px-1.5 py-px text-[8px]' : 'px-2 py-0.5 text-[10px]' %> font-bold uppercase tracking-wide text-white bg-red-600 rounded-full whitespace-nowrap shadow-sm">

app/views/users/_profile_settings.html.erb

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
<div id="<%= local_assigns[:wrapper_id] || 'profile_settings' %>" class="md:space-y-3">
1+
<div id="<%= local_assigns[:wrapper_id] || 'profile_settings' %>" class="space-y-0">
22
<%# Public Profile Toggle %>
3-
<div class="flex items-center justify-between gap-4 py-3 md:py-0 border-t border-gray-100 md:border-t-0">
3+
<div class="flex items-center justify-between gap-4 py-3">
44
<span class="text-sm font-medium text-gray-500">Public profile</span>
55
<%= button_to toggle_public_user_settings_path,
66
method: :post,
@@ -12,34 +12,34 @@
1212
<% end %>
1313
</div>
1414

15-
<%# Open to Work Toggle %>
16-
<div class="flex items-center justify-between gap-4 py-3 md:py-0 border-t border-gray-100 md:border-t-0">
17-
<span class="text-sm font-medium text-gray-500">Open to work</span>
18-
<%= button_to toggle_open_to_work_user_settings_path,
15+
<%# Newsletter Toggle %>
16+
<div class="flex items-center justify-between gap-4 py-3 border-t border-gray-100">
17+
<span class="text-sm font-medium text-gray-500">Receive news</span>
18+
<%= button_to toggle_newsletter_user_settings_path,
1919
method: :post,
20-
class: "relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2 #{user.open_to_work? ? 'bg-red-600' : 'bg-gray-400'}",
20+
class: "relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2 #{user.unsubscribed_from_newsletter? ? 'bg-gray-400' : 'bg-red-600'}",
2121
data: { turbo_stream: true } do %>
22-
<span class="sr-only">Toggle open to work</span>
22+
<span class="sr-only">Toggle newsletter subscription</span>
2323
<span aria-hidden="true"
24-
class="pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out <%= user.open_to_work? ? 'translate-x-4' : 'translate-x-0' %>"></span>
24+
class="pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out <%= user.unsubscribed_from_newsletter? ? 'translate-x-0' : 'translate-x-4' %>"></span>
2525
<% end %>
2626
</div>
2727

28-
<%# Newsletter Toggle %>
29-
<div class="flex items-center justify-between gap-4 py-3 md:py-0 border-t border-gray-100 md:border-t-0">
30-
<span class="text-sm font-medium text-gray-500">Newsletter</span>
31-
<%= button_to toggle_newsletter_user_settings_path,
28+
<%# Open to Work Toggle %>
29+
<div class="flex items-center justify-between gap-4 py-3 border-t border-gray-100">
30+
<span class="text-sm font-medium text-gray-500">Open to work</span>
31+
<%= button_to toggle_open_to_work_user_settings_path,
3232
method: :post,
33-
class: "relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2 #{user.unsubscribed_from_newsletter? ? 'bg-gray-400' : 'bg-red-600'}",
33+
class: "relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2 #{user.open_to_work? ? 'bg-red-600' : 'bg-gray-400'}",
3434
data: { turbo_stream: true } do %>
35-
<span class="sr-only">Toggle newsletter subscription</span>
35+
<span class="sr-only">Toggle open to work</span>
3636
<span aria-hidden="true"
37-
class="pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out <%= user.unsubscribed_from_newsletter? ? 'translate-x-0' : 'translate-x-4' %>"></span>
37+
class="pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out <%= user.open_to_work? ? 'translate-x-4' : 'translate-x-0' %>"></span>
3838
<% end %>
3939
</div>
4040

4141
<%# Sign Out Button %>
42-
<div class="md:pt-0">
42+
<div class="pt-1">
4343
<%= button_to "Sign out", destroy_user_session_path,
4444
method: :delete,
4545
data: { turbo: false },

0 commit comments

Comments
 (0)