diff --git a/app/assets/images/sort.svg b/app/assets/images/sort.svg new file mode 100644 index 0000000..c8b687e --- /dev/null +++ b/app/assets/images/sort.svg @@ -0,0 +1,3 @@ + + + diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 746f22d..0ea17cb 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -324,6 +324,9 @@ def post_path_for(post) def cross_domain_url(domain_type, path = "/") return path unless Rails.env.production? + # No Warden context (e.g. rendering from a background job broadcast) + return path unless respond_to?(:request) && request.present? && request.env["warden"].present? + domains = Rails.application.config.x.domains host = (domain_type == :primary) ? domains.primary : domains.community diff --git a/app/javascript/controllers/char_counter_controller.js b/app/javascript/controllers/char_counter_controller.js index 031a9bb..19b1323 100644 --- a/app/javascript/controllers/char_counter_controller.js +++ b/app/javascript/controllers/char_counter_controller.js @@ -28,14 +28,16 @@ export default class extends Controller { colorClass = `${pill} bg-red-100 text-red-600` } - if (isValid) { - this.submitTarget.disabled = false - this.submitTarget.classList.remove("opacity-50", "cursor-not-allowed") - this.submitTarget.classList.add("cursor-pointer", "hover:bg-red-700") - } else { - this.submitTarget.disabled = true - this.submitTarget.classList.add("opacity-50", "cursor-not-allowed") - this.submitTarget.classList.remove("cursor-pointer", "hover:bg-red-700") + if (this.hasSubmitTarget) { + if (isValid) { + this.submitTarget.disabled = false + this.submitTarget.classList.remove("opacity-50", "cursor-not-allowed") + this.submitTarget.classList.add("cursor-pointer", "hover:bg-red-700") + } else { + this.submitTarget.disabled = true + this.submitTarget.classList.add("opacity-50", "cursor-not-allowed") + this.submitTarget.classList.remove("cursor-pointer", "hover:bg-red-700") + } } this.counterTargets.forEach(counter => { diff --git a/app/jobs/generate_testimonial_fields_job.rb b/app/jobs/generate_testimonial_fields_job.rb index a1cb7ed..918b82d 100644 --- a/app/jobs/generate_testimonial_fields_job.rb +++ b/app/jobs/generate_testimonial_fields_job.rb @@ -21,6 +21,7 @@ def perform(testimonial) unless parsed Rails.logger.error "Failed to generate testimonial fields for testimonial #{testimonial.id}" testimonial.update!(ai_feedback: "We couldn't process your testimonial right now. Please try again later.") + broadcast_update(testimonial) return end @@ -36,6 +37,7 @@ def perform(testimonial) unless parsed testimonial.update!(ai_feedback: "We couldn't process your testimonial right now. Please try again later.") + broadcast_update(testimonial) return end @@ -48,6 +50,7 @@ def perform(testimonial) rescue JSON::ParserError => e Rails.logger.error "Failed to parse AI response for testimonial #{testimonial.id}: #{e.message}" testimonial.update!(ai_feedback: "We couldn't process your testimonial right now. Please try again later.") + broadcast_update(testimonial) end private @@ -100,6 +103,15 @@ def generate_fields(system_prompt, user_prompt) result ? JSON.parse(result) : nil end + def broadcast_update(testimonial) + Turbo::StreamsChannel.broadcast_replace_to( + "testimonial_#{testimonial.id}", + target: "testimonial_section", + partial: "testimonials/section", + locals: { testimonial: testimonial, user: testimonial.user } + ) + end + def heading_taken?(heading, testimonial_id) Testimonial.where.not(id: testimonial_id).exists?(heading: heading) end diff --git a/app/models/project.rb b/app/models/project.rb index 92d5ef9..6f0f517 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -18,9 +18,9 @@ def stars_gained [ snapshots.first - snapshots.last, 0 ].max end - def record_snapshot! + def record_snapshot!(force: false) snapshot = star_snapshots.find_or_initialize_by(recorded_on: Date.current) - snapshot.update!(stars: stars) + snapshot.update!(stars: stars) if snapshot.new_record? || force snapshot end end diff --git a/app/services/github_data_fetcher.rb b/app/services/github_data_fetcher.rb index 4619e4d..4d662f7 100644 --- a/app/services/github_data_fetcher.rb +++ b/app/services/github_data_fetcher.rb @@ -166,11 +166,11 @@ def self.update_user_from_graphql(user, profile_data, repos_data) user.update!( username: profile_data[:login], email: profile_data[:email] || user.email, - name: profile_data[:name], - bio: profile_data[:bio], + name: profile_data[:name] || user.name, + bio: profile_data[:bio] || user.bio, company: profile_data[:company], - website: profile_data[:websiteUrl].presence, - twitter: profile_data[:twitterUsername].presence, + website: profile_data[:websiteUrl].presence || user.website, + twitter: profile_data[:twitterUsername].presence || user.twitter, location: profile_data[:location], avatar_url: profile_data[:avatarUrl], github_data_updated_at: Time.current @@ -190,11 +190,11 @@ def self.update_user_from_graphql(user, profile_data, repos_data) } end - sync_projects!(user, repos) + sync_projects!(user, repos, force_snapshot: true) end # Sync GitHub repos to Project records with star snapshot tracking - def self.sync_projects!(user, repos_data) + def self.sync_projects!(user, repos_data, force_snapshot: false) current_urls = repos_data.map { |r| r[:github_url] || r[:url] } # Soft-archive projects no longer returned by GitHub @@ -216,7 +216,7 @@ def self.sync_projects!(user, repos_data) ) project.save! - project.record_snapshot! + project.record_snapshot!(force: force_snapshot) end # Recalculate cached stats on user @@ -255,11 +255,11 @@ def update_from_oauth_data user.update!( username: auth_data.info.nickname, email: auth_data.info.email, - name: raw_info.name, - bio: raw_info.bio, + name: raw_info.name || user.name, + bio: raw_info.bio || user.bio, company: raw_info.company, - website: raw_info.blog.presence, - twitter: raw_info.twitter_username.presence, + website: raw_info.blog.presence || user.website, + twitter: raw_info.twitter_username.presence || user.twitter, location: raw_info.location, avatar_url: auth_data.info.image ) @@ -286,11 +286,11 @@ def update_from_api user.update!( username: data["login"], email: data["email"] || user.email, - name: data["name"], - bio: data["bio"], + name: data["name"] || user.name, + bio: data["bio"] || user.bio, company: data["company"], - website: data["blog"], - twitter: data["twitter_username"], + website: data["blog"].presence || user.website, + twitter: data["twitter_username"] || user.twitter, location: data["location"], avatar_url: data["avatar_url"] ) diff --git a/app/views/home/_testimonial_carousel.html.erb b/app/views/home/_testimonial_carousel.html.erb index d6e238e..f661191 100644 --- a/app/views/home/_testimonial_carousel.html.erb +++ b/app/views/home/_testimonial_carousel.html.erb @@ -65,7 +65,7 @@ <%# User tile - extra top margin on mobile/tablet to account for speech bubble tail %>
- <%= render "users/user_tile", user: testimonial.user, frameless: true, avatar_size: "w-20 h-[72px] md:w-36 md:h-[130px]", compact: true %> + <%= 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" %>
diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index 748331d..9a16d56 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -90,7 +90,7 @@ <% community_nav = content_for?(:nav_community_style) || (controller_name == 'users' && ['index', 'show'].include?(action_name)) %> -