From db95e3f1f9764c250c2d423df385163232ef0ed7 Mon Sep 17 00:00:00 2001 From: Marco Roth Date: Wed, 27 Nov 2024 08:28:14 +0100 Subject: [PATCH] Add conference speakers page (#324) * Add conference speakers page * Use Turbo Frame to navigate between subpages * Tweak Frame Navigation, add kind titles, group by kind on speaker page, remove hero banner flicker * Update Q&A and AMA kind matching * Fix `Heroes` spelling * Make sure speakers are uniq per kind * Add interview kind to speakers index --- app/controllers/events/speakers_controller.rb | 15 +++ app/helpers/application_helper.rb | 11 ++ app/models/event.rb | 2 +- app/models/talk.rb | 24 ++-- app/views/events/_header.html.erb | 29 +++++ app/views/events/_navigation.html.erb | 11 ++ app/views/events/show.html.erb | 109 ++++++------------ app/views/events/speakers/index.html.erb | 40 +++++++ app/views/talks/_card.html.erb | 2 +- app/views/talks/_speaker.html.erb | 2 +- config/routes.rb | 1 + data/railsconf/railsconf-2014/videos.yml | 4 +- load_testing/talks_payload.csv | 2 +- test/models/talk_test.rb | 10 +- 14 files changed, 166 insertions(+), 96 deletions(-) create mode 100644 app/controllers/events/speakers_controller.rb create mode 100644 app/views/events/_header.html.erb create mode 100644 app/views/events/_navigation.html.erb create mode 100644 app/views/events/speakers/index.html.erb diff --git a/app/controllers/events/speakers_controller.rb b/app/controllers/events/speakers_controller.rb new file mode 100644 index 000000000..ef7d7986c --- /dev/null +++ b/app/controllers/events/speakers_controller.rb @@ -0,0 +1,15 @@ +class Events::SpeakersController < ApplicationController + skip_before_action :authenticate_user!, only: %i[index] + before_action :set_event, only: %i[index] + + def index + end + + private + + def set_event + @event = Event.includes(:organisation, talks: {speakers: :user}).find_by!(slug: params[:event_slug]) + + redirect_to schedule_event_path(@event.canonical), status: :moved_permanently if @event.canonical.present? + end +end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 021c2a84e..16775da87 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -5,6 +5,17 @@ def back_path @back_path || root_path end + def active_link_to(text = nil, path = nil, active_class: "", **options, &) + path ||= text + + classes = active_class.presence || "active" + options[:class] = class_names(options[:class], classes) if current_page?(path) + + return link_to(path, options, &) if block_given? + + link_to text, path, options + end + def footer_credits output = ["Made with"] output << heroicon(:heart, variant: :solid, size: :sm, class: "text-primary inline") diff --git a/app/models/event.rb b/app/models/event.rb index 95cdff3af..9a98cf67f 100644 --- a/app/models/event.rb +++ b/app/models/event.rb @@ -125,7 +125,7 @@ def description keynotes = keynote_speakers.any? ? %(, including keynotes by #{keynote_speakers.map(&:name).to_sentence}) : "" <<~DESCRIPTION - #{organisation.name} is a #{organisation.frequency} #{organisation.kind}#{held_in_sentence} and features #{talks.count} #{"talk".pluralize(talks.count)} from various speakers#{keynotes}. + #{organisation.name} is a #{organisation.frequency} #{organisation.kind}#{held_in_sentence} and features #{talks.size} #{"talk".pluralize(talks.size)} from various speakers#{keynotes}. DESCRIPTION end diff --git a/app/models/talk.rb b/app/models/talk.rb index d6b0f48bf..c8f749951 100644 --- a/app/models/talk.rb +++ b/app/models/talk.rb @@ -83,7 +83,7 @@ class Talk < ApplicationRecord # enums enum :video_provider, %w[youtube mp4 scheduled not_published not_recorded].index_by(&:itself) enum :kind, - %w[talk keynote lightning_talk panel workshop gameshow podcast q_and_a discussion fireside_chat + %w[keynote talk lightning_talk panel workshop gameshow podcast q_and_a discussion fireside_chat interview award].index_by(&:itself) # attributes @@ -378,28 +378,30 @@ def set_kind end self.kind = case title - when /.*(keynote:|opening\ keynote|closing\ keynote|keynote|opening\ keynote|closing\ keynote).*/i + when /^(keynote:|keynote|opening\ keynote:|opening\ keynote|closing\ keynote:|closing\ keynote).*/i :keynote - when /.*(lightning\ talk:|lightning\ talk|lightning\ talks|micro\ talk:|micro\ talk).*/i + when /^(lightning\ talk:|lightning\ talk|lightning\ talks|micro\ talk:|micro\ talk).*/i :lightning_talk when /.*(panel:|panel).*/i :panel - when /.*(workshop:|workshop).*/i + when /^(workshop:|workshop).*/i :workshop - when /.*(gameshow|game\ show|gameshow:|game\ show:).*/i + when /^(gameshow|game\ show|gameshow:|game\ show:).*/i :gameshow - when /.*(podcast:|podcast\ recording:|live\ podcast:).*/i + when /^(podcast:|podcast\ recording:|live\ podcast:).*/i :podcast - when /.*(q&a|q&a:|ama|q&a\ with|ruby\ committers\ vs\ the\ world|ruby\ committers\ and\ the\ world).*/i + when /.*(q&a|q&a:|q&a\ with|ruby\ committers\ vs\ the\ world|ruby\ committers\ and\ the\ world).*/i, + /.*(AMA)$/, + /^(AMA:)/ :q_and_a - when /.*(fishbowl:|fishbowl\ discussion:|discussion:|discussion).*/i + when /^(fishbowl:|fishbowl\ discussion:|discussion:|discussion).*/i :discussion - when /.*(fireside\ chat:|fireside\ chat).*/i + when /^(fireside\ chat:|fireside\ chat).*/i :fireside_chat + when /^(award:|award\ show|ruby\ heroes\ awards|ruby\ heroes\ award|rails\ luminary).*/i + :award when /^(interview:|interview\ with).*/i :interview - when /.*(award:|award\ show|ruby\ hero\ awards|ruby\ hero\ award|rails\ luminary).*/i - :award else :talk end diff --git a/app/views/events/_header.html.erb b/app/views/events/_header.html.erb new file mode 100644 index 000000000..dbae3fe48 --- /dev/null +++ b/app/views/events/_header.html.erb @@ -0,0 +1,29 @@ +
+ <%= image_tag image_path(@event.banner_image_path), class: "w-full md:container" %> +
+ +
+
+
+ <%= image_tag image_path(event.avatar_image_path), + class: "rounded-full border border-[#D9DFE3] size-24 md:size-36", + alt: "#{event.name} Avatar", + style: "view-transition-name: avatar", + loading: :lazy %> + +
+

<%= event.name %>

+

<%= event.location %> • <%= event.formatted_dates %>

+
+
+ +
+ <%= link_to "Visit Website", event.organisation.website, class: "btn btn-primary w-full", target: "_blank" %> + <%= link_to "View all #{event.organisation.name} events", event.organisation, class: "btn w-full" %> +
+
+ +

+ <%= event.description %> +

+
diff --git a/app/views/events/_navigation.html.erb b/app/views/events/_navigation.html.erb new file mode 100644 index 000000000..398a9dfff --- /dev/null +++ b/app/views/events/_navigation.html.erb @@ -0,0 +1,11 @@ +
+ <%= active_link_to "Talks", event_path(event), class: "tab", active_class: "tab-active", role: "tab" %> + <%= active_link_to "Speakers", event_speakers_path(event), class: "tab", active_class: "tab-active", role: "tab" %> + + <% if false %> + Schedule + Organizers + Photos + Sponsors + <% end %> +
diff --git a/app/views/events/show.html.erb b/app/views/events/show.html.erb index 676520104..5df00394c 100644 --- a/app/views/events/show.html.erb +++ b/app/views/events/show.html.erb @@ -1,79 +1,40 @@ -<%= turbo_refreshes_with method: :morph, scroll: :preserve %> - -
- <%= image_tag image_path(@event.banner_image_path), class: "w-full max-h- aspect-auto container" %> -
- -
-
-
- <%= image_tag image_path(@event.avatar_image_path), - class: "rounded-full border border-[#D9DFE3] size-24 md:size-36", - alt: "#{@event.name} Avatar", - style: "view-transition-name: avatar", - loading: :lazy %> - -
-

<%= @event.name %>

-

<%= @event.location %> • <%= @event.formatted_dates %>

-
-
- -
- <%= link_to "Visit Website", @event.website, class: "btn btn-primary w-full", target: "_blank" %> - <%= link_to "View all #{@event.organisation.name} events", @event.organisation, class: "btn w-full" %> -
-
- -

- <%= @event.description %> -

- - <% if false %> - - <% end %> -
- -
- -
-
-
- <%= render partial: "talks/card", - collection: @talks, - locals: { - favoritable: true, - user_favorite_talks_ids: @user_favorite_talks_ids, - watched_talks_ids: user_watched_talks_ids, - back_to: request.fullpath, - back_to_title: @event.name - }, - as: :talk %> -
+<%= render partial: "header", locals: {event: @event} %> + +<%= turbo_frame_tag dom_id(@event), data: {turbo_action: "advance", turbo_frame: "_top"} do %> +
+ <%= render partial: "navigation", locals: {event: @event} %> + +
+
+
+ <%= render partial: "talks/card", + collection: @talks, + locals: { + favoritable: true, + user_favorite_talks_ids: @user_favorite_talks_ids, + back_to: request.fullpath, + back_to_title: @event.name + }, + as: :talk %> +
- <% if @talks.empty? %> -
-
-

- No talks found for this event. -

-

- Please - &body=<%= CGI.escape("\n\nIssue opened from event page: #{event_url(@event)}") %>" class="text-primary" target="_blank">open an issue on GitHub - if you know how to get access to the recordings of this event. Thank you! -

+ <% if @talks.empty? %> +
+
+

+ No talks found for this event. +

+

+ Please + &body=<%= CGI.escape("\n\nIssue opened from event page: #{event_url(@event)}") %>" class="text-primary" target="_blank">open an issue on GitHub + if you know how to get access to the recordings of this event. Thank you! +

+
-
- <% end %> + <% end %> - <%== pagy_nav(@pagy) if @pagy.pages > 1 %> + <%== pagy_nav(@pagy) if @pagy.pages > 1 %> +
-
+<% end %> diff --git a/app/views/events/speakers/index.html.erb b/app/views/events/speakers/index.html.erb new file mode 100644 index 000000000..3324b2375 --- /dev/null +++ b/app/views/events/speakers/index.html.erb @@ -0,0 +1,40 @@ +<%= render partial: "events/header", locals: {event: @event} %> + +<%= turbo_frame_tag dom_id(@event), data: {turbo_action: "advance", turbo_frame: "_top"} do %> +
+ <%= render partial: "events/navigation", locals: {event: @event} %> + +
+
+ <% titles = { + keynote: "Keynote Speaker", + talk: "Speaker", + lightning_talk: "Lightning Talk Speaker", + panel: "Panelist", + discussion: "Panelist", + gameshow: "Game Show Host", + workshop: "Workshop Instructor", + podcast: "Podcast Host/Participant", + q_and_a: "Q&A Host/Participant", + fireside_chat: "Fireside Chat Host/Participant", + interview: "Interviewer/Interviewee", + award: "Award Presenter/Winner" + } %> + + <% grouped = @event.talks.flat_map { |talk| talk.speakers.map { |speaker| [talk.kind, speaker] } }.group_by(&:first).transform_values(&:uniq).sort_by { |kind, _speakers| Talk.kinds.values.index(kind) } %> + + <% grouped.each do |kind, speakers| %> +

<%= titles[kind.to_sym].pluralize(speakers.count) %>

+ +
+ <% speakers.each do |kind, speaker| %> +
+ <%= render partial: "talks/speaker", locals: {speaker: speaker} %> +
+ <% end %> +
+ <% end %> +
+
+
+<% end %> diff --git a/app/views/talks/_card.html.erb b/app/views/talks/_card.html.erb index d025d99c4..1a0838bc9 100644 --- a/app/views/talks/_card.html.erb +++ b/app/views/talks/_card.html.erb @@ -3,7 +3,7 @@ <% language = Language.by_code(talk.language) %>
- <%= link_to talk_path(talk, back_to: back_to, back_to_title: back_to_title), class: "flex aspect-video overflow-hidden relative group", data: {action: "mouseover->preserve-scroll#updateLinkBackToWithScrollPosition"} do %> + <%= link_to talk_path(talk, back_to: back_to, back_to_title: back_to_title), class: "flex aspect-video overflow-hidden relative group", data: {action: "mouseover->preserve-scroll#updateLinkBackToWithScrollPosition", turbo_frame: "_top"} do %> <% if language && language != "English" %>
<%= language_to_emoji(language) %> diff --git a/app/views/talks/_speaker.html.erb b/app/views/talks/_speaker.html.erb index 6aaf5bba6..36c50d6f3 100644 --- a/app/views/talks/_speaker.html.erb +++ b/app/views/talks/_speaker.html.erb @@ -1,4 +1,4 @@ -<%= link_to speaker, class: "flex flex-col gap-4 hover:bg-gray-200 transition-bg duration-300 ease-in-out p-2 px-4 rounded-lg" do %> +<%= link_to speaker, class: "flex flex-col gap-4 hover:bg-gray-200 transition-bg duration-300 ease-in-out p-2 px-4 rounded-lg", data: {turbo_frame: "_top"} do %>
diff --git a/config/routes.rb b/config/routes.rb index e9c8dc8ab..78a0c6795 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -52,6 +52,7 @@ resources :speakers, param: :slug, only: [:index, :show, :update, :edit] resources :events, param: :slug, only: [:index, :show, :update, :edit] do scope module: :events do + resources :speakers, only: [:index] resources :talks, only: [:index] end end diff --git a/data/railsconf/railsconf-2014/videos.yml b/data/railsconf/railsconf-2014/videos.yml index 67d644d47..32999e641 100644 --- a/data/railsconf/railsconf-2014/videos.yml +++ b/data/railsconf/railsconf-2014/videos.yml @@ -713,8 +713,8 @@ video_provider: youtube video_id: 6tQTwmIgclE -- title: Ruby Heros Awards 2014 - raw_title: RailsConf 2014 - Ruby Heros +- title: Ruby Heroes Awards 2014 + raw_title: RailsConf 2014 - Ruby Heroes speakers: - Gregg Pollack event_name: RailsConf 2014 diff --git a/load_testing/talks_payload.csv b/load_testing/talks_payload.csv index 2c54f191d..301d65a96 100644 --- a/load_testing/talks_payload.csv +++ b/load_testing/talks_payload.csv @@ -133,7 +133,7 @@ railsconf-2014-refactoring-towards-component-based-rails-architectures railsconf-2014-rails-as-an-soa-client-by-pete-hodgson railsconf-2014-service-oriented-authenication-by-jeremy-green railsconf-2014-authorization-in-a-service-oriented-environment-by-alan-cohen -railsconf-2014-ruby-heros +railsconf-2014-ruby-heroes railsconf-2014-designing-the-apis-for-an-internal-set-of-services-by-alberto-leal railsconf-2014-humor-in-the-code-by-baratunde-thurston railsconf-2014-build-the-api-first-by-rosie-hoyem-and-sonja-hall diff --git a/test/models/talk_test.rb b/test/models/talk_test.rb index 42eaed2b3..5dc395524 100644 --- a/test/models/talk_test.rb +++ b/test/models/talk_test.rb @@ -27,18 +27,18 @@ class TalkTest < ActiveSupport::TestCase test "should guess kind from title" do kind_with_titles = { - talk: ["I love Ruby"], - keynote: ["Keynote: Something ", "foo Opening keynote bar", "closing keynote foo bar", "Keynote", "Keynote by Someone", "Opening Keynote", "Closing Keynote"], + talk: ["I love Ruby", "Beyond Code: Crafting effective discussions to further technical decision-making", "From LALR to IELR: A Lrama's Next Step"], + keynote: ["Keynote: Something ", "Opening keynote Something", "closing keynote Something", "Keynote", "Keynote by Someone", "Opening Keynote", "Closing Keynote"], lightning_talk: ["Lightning Talk: Something", "lightning talk: Something", "Lightning talk: Something", "lightning talk", "Lightning Talks", "Lightning talks", "lightning talks", "Lightning Talks Day 1", "Lightning Talks (Day 1)", "Lightning Talks - Day 1", "Micro Talk: Something", "micro talk: Something", "micro talk: Something", "micro talk"], panel: ["Panel: foo", "Panel", "Something Panel"], workshop: ["Workshop: Something", "workshop: Something"], gameshow: ["Gameshow", "Game Show", "Gameshow: Something", "Game Show: Something"], podcast: ["Podcast: Something", "Podcast Recording: Something", "Live Podcast: Something"], - q_and_a: ["Q&A", "Q&A: Something", "Something AMA", "Q&A with Somebody", "Ruby Committers vs The World", "Ruby Committers and the World"], + q_and_a: ["Q&A", "Q&A: Something", "Something AMA", "Q&A with Somebody", "Ruby Committers vs The World", "Ruby Committers and the World", "AMA: Rails Core"], discussion: ["Discussion: Something", "Discussion", "Fishbowl: Topic", "Fishbowl Discussion: Topic"], fireside_chat: ["Fireside Chat: Something", "Fireside Chat"], interview: ["Interview with Matz", "Interview: Something"], - award: ["Award: Something", "Award Show", "Ruby Hero Awards", "Ruby Hero Award", "Rails Luminary"] + award: ["Award: Something", "Award Show", "Ruby Heroes Awards", "Ruby Heroes Award", "Rails Luminary"] } kind_with_titles.each do |kind, titles| @@ -46,7 +46,7 @@ class TalkTest < ActiveSupport::TestCase talk = Talk.new(title:) talk.save! - assert_equal kind.to_s, talk.kind + assert_equal [kind.to_s, title], [talk.kind, talk.title] talk.destroy! end