Skip to content

Commit 3011932

Browse files
committed
Sociable
1 parent d00054a commit 3011932

37 files changed

+569
-212
lines changed
Loading

app/avo/resources/speaker.rb

+1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ class Avo::Resources::Speaker < Avo::BaseResource
1717
def fields
1818
field :id, as: :id, link_to_record: true
1919
field :name, as: :text, link_to_record: true, sortable: true
20+
field :github, as: :text
2021
field :bio, as: :textarea, hide_on: :index
2122
field :slug, as: :text, hide_on: :index
2223
field :talks_count, as: :number, sortable: true
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
class SocialProfilesController < ApplicationController
2+
skip_before_action :authenticate_user!, only: [:edit, :update]
3+
before_action :check_ownership, only: [:new, :create]
4+
before_action :check_provider, only: [:new]
5+
before_action :set_social_profile, only: [:edit, :update]
6+
7+
def new
8+
@social_profile = @sociable.social_profiles.new(provider: params[:provider])
9+
end
10+
11+
def create
12+
@social_profile = @sociable.social_profiles.new(social_profile_params)
13+
14+
begin
15+
ActiveRecord::Base.transaction do
16+
@social_profile.save!
17+
@suggestion = @social_profile.create_suggestion_from(
18+
params: @social_profile.attributes.slice("provider", "value"),
19+
new_record: true
20+
)
21+
end
22+
rescue ActiveRecord::RecordInvalid
23+
end
24+
25+
respond_to do |format|
26+
if @suggestion&.persisted?
27+
flash[:notice] = "Saved"
28+
format.turbo_stream
29+
else
30+
format.turbo_stream { render :create, status: :unprocessable_entity }
31+
end
32+
end
33+
end
34+
35+
def edit
36+
end
37+
38+
def update
39+
begin
40+
@suggestion = @social_profile.create_suggestion_from(params: social_profile_params, user: Current.user)
41+
rescue ActiveRecord::RecordInvalid
42+
end
43+
44+
respond_to do |format|
45+
if @suggestion&.persisted?
46+
flash[:notice] = "Saved"
47+
format.turbo_stream
48+
else
49+
format.turbo_stream { render :update, status: :unprocessable_entity }
50+
end
51+
end
52+
end
53+
54+
private
55+
56+
def social_profile_params
57+
params.require(:social_profile).permit(
58+
:provider,
59+
:value
60+
)
61+
end
62+
63+
def set_social_profile
64+
@social_profile ||= SocialProfile.find(params[:id])
65+
end
66+
67+
def check_provider
68+
raise StandardError, "Invalid Social Provider" unless SocialProfile::PROVIDERS.include?(params[:provider])
69+
end
70+
71+
def check_ownership
72+
head :forbidden unless @sociable.managed_by?(Current.user)
73+
end
74+
end
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
class Speakers::SocialProfilesController < ::SocialProfilesController
2+
prepend_before_action :set_sociable
3+
4+
private
5+
6+
def set_sociable
7+
@sociable ||= Speaker.find_by!(slug: params[:speaker_slug])
8+
end
9+
end

app/controllers/speakers_controller.rb

-6
Original file line numberDiff line numberDiff line change
@@ -69,13 +69,7 @@ def speaker_params
6969
params.require(:speaker).permit(
7070
:name,
7171
:github,
72-
:twitter,
73-
:bsky,
74-
:linkedin,
75-
:mastodon,
7672
:bio,
77-
:website,
78-
:speakerdeck,
7973
:pronouns_type,
8074
:pronouns,
8175
:slug

app/models/concerns/sociable.rb

+15-1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,20 @@ module Sociable
22
extend ActiveSupport::Concern
33

44
included do
5-
has_many :social_profiles, as: :sociable, dependent: :destroy
5+
has_many :social_profiles, -> { order(:created_at) }, as: :sociable, dependent: :destroy
6+
end
7+
8+
SocialProfile::PROVIDERS.each do |method|
9+
define_method(method) do
10+
social_profiles.send(method).first&.value
11+
end
12+
13+
define_method(:"build_#{method}") do |value|
14+
social_profiles.build(provider: method, value:)
15+
end
16+
17+
define_method(:"create_#{method}") do |value|
18+
social_profiles.create(provider: method, value:)
19+
end
620
end
721
end

app/models/concerns/suggestable.rb

+5-2
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,11 @@ module Suggestable
55
has_many :suggestions, as: :suggestable, dependent: :destroy
66
end
77

8-
def create_suggestion_from(params:, user: Current.user)
9-
suggestions.create(content: select_differences_for(params), suggested_by_id: user&.id).tap do |suggestion|
8+
# NOTE: validate before saving
9+
def create_suggestion_from(params:, user: Current.user, new_record: false)
10+
params = select_differences_for(params) unless new_record
11+
12+
suggestions.create(content: params, suggested_by_id: user&.id).tap do |suggestion|
1013
suggestion.approved!(approver: user) if managed_by?(user)
1114
end
1215
end

app/models/event.rb

+1-1
Original file line numberDiff line numberDiff line change
@@ -290,6 +290,6 @@ def year
290290
end
291291

292292
def website
293-
self[:website].presence || organisation.website
293+
social_profiles.detect { |sp| sp.website? }&.value || organisation.website
294294
end
295295
end

app/models/organisation.rb

+1
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
class Organisation < ApplicationRecord
2929
include Sluggable
3030
include Suggestable
31+
include Sociable
3132

3233
include ActionView::Helpers::TextHelper
3334

app/models/social_profile.rb

+37-18
Original file line numberDiff line numberDiff line change
@@ -3,39 +3,39 @@
33
# Table name: social_profiles
44
#
55
# id :integer not null, primary key
6-
# provider :integer
7-
# sociable_type :string indexed => [sociable_id]
8-
# value :string
6+
# provider :string not null
7+
# sociable_type :string not null, indexed => [sociable_id]
8+
# value :string not null
99
# created_at :datetime not null
1010
# updated_at :datetime not null
11-
# sociable_id :integer indexed => [sociable_type]
11+
# sociable_id :integer not null, indexed => [sociable_type]
1212
#
1313
# Indexes
1414
#
1515
# index_social_profiles_on_sociable (sociable_type,sociable_id)
1616
#
1717
class SocialProfile < ApplicationRecord
18+
include Suggestable
19+
PROVIDERS = %w[twitter linkedin bsky mastodon speakerdeck website]
20+
1821
belongs_to :sociable, polymorphic: true
1922

20-
enum :provider, {
21-
github: 0,
22-
twitter: 1,
23-
linkedin: 2,
24-
bsky: 3,
25-
mastadon: 4
26-
},
27-
suffix: true,
28-
validate: {presence: true}
29-
30-
before_save do
31-
self.value = self.class.normalize_value_for(provider.to_sym, value)
23+
enum :provider, PROVIDERS.index_by(&:itself), validate: {presence: true}
24+
25+
after_initialize do
26+
self.value = self.class.normalize_value_for(provider.to_sym, value) if provider.present?
3227
end
3328

29+
validates :provider, presence: true
30+
validates :value, presence: true, uniqueness: {scope: :provider}
31+
32+
scope :excluding_provider, ->(provider) { where.not(provider:) }
33+
3434
# normalizes
35-
normalizes :github, with: ->(value) { value.gsub(/^(?:https?:\/\/)?(?:www\.)?github\.com\//, "").gsub(/^@/, "") }
3635
normalizes :twitter, with: ->(value) { value.gsub(%r{https?://(?:www\.)?(?:x\.com|twitter\.com)/}, "").gsub(/@/, "") }
3736
normalizes :linkedin, with: ->(value) { value.gsub(%r{https?://(?:www\.)?(?:linkedin\.com/in)/}, "") }
38-
normalizes :bsky, with: ->(value) { value.gsub(%r{https?://(?:www\.)?(?:[^\/]+\.com)/}, "").gsub(/@/, "") }
37+
normalizes :bsky, with: ->(value) { value.gsub(%r{https?://(?:www\.)?(?:x\.com|bsky\.app/profile)/}, "").gsub(/@/, "") }
38+
normalizes :speakerdeck, with: ->(value) { value.gsub(/^(?:https?:\/\/)?(?:www\.)?speakerdeck\.com\//, "").gsub(/^@/, "") }
3939
normalizes :mastodon, with: ->(value) {
4040
return value if value&.match?(URI::DEFAULT_PARSER.make_regexp)
4141
return "" unless value.count("@") == 2
@@ -44,4 +44,23 @@ class SocialProfile < ApplicationRecord
4444

4545
"https://#{instance}/@#{handle}"
4646
}
47+
48+
def url
49+
case provider.to_sym
50+
when :twitter, :speakerdeck
51+
"https://#{provider}.com/#{value}"
52+
when :linkedin
53+
"https://linkedin.com/in/#{value}"
54+
when :bsky
55+
"https://bsky.app/profile/#{value}"
56+
else
57+
value
58+
end
59+
end
60+
61+
private
62+
63+
def managed_by?(visiting_user)
64+
sociable.managed_by?(visiting_user)
65+
end
4766
end

app/models/speaker/profiles.rb

+6-5
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,14 @@ def enhance_all_later
1515
socials = github_client.social_accounts(speaker.github)
1616
links = socials.pluck(:provider, :url).to_h
1717

18+
speaker.build_twitter(links["twitter"]) if links["twitter"].present?
19+
speaker.build_mastodon(links["mastodon"]) if links["mastodon"].present?
20+
speaker.build_bsky(links["bluesky"]) if links["bluesky"].present?
21+
speaker.build_linkedin(links["linkedin"]) if links["linkedin"].present?
22+
speaker.build_website(profile.blog) if profile.blog.present?
23+
1824
speaker.update!(
19-
twitter: speaker.twitter.presence || links["twitter"] || "",
20-
mastodon: speaker.mastodon.presence || links["mastodon"] || "",
21-
bsky: speaker.bsky.presence || links["bluesky"] || "",
22-
linkedin: speaker.linkedin.presence || links["linkedin"] || "",
2325
bio: speaker.bio.presence || profile.bio || "",
24-
website: speaker.website.presence || profile.blog || "",
2526
github_metadata: {
2627
profile: JSON.parse(profile.body),
2728
socials: JSON.parse(socials.body)

app/views/events/_header.html.erb

+7-5
Original file line numberDiff line numberDiff line change
@@ -14,18 +14,20 @@
1414
</div>
1515

1616
<div class="container py-8">
17-
<div class="block lg:flex gap-8 align-center justify-between">
18-
<div class="flex flex-col lg:flex-row gap-8 items-center lg:justify-right text-center lg:text-left mb-6 lg:mb-0">
17+
<div class="justify-between block lg:flex gap-8 align-center">
18+
<div class="flex flex-col items-center mb-6 text-center lg:flex-row gap-8 lg:justify-right lg:text-left lg:mb-0">
1919
<%= image_tag image_path(event.avatar_image_path),
2020
class: "rounded-full border border-[#D9DFE3] size-24 md:size-36",
2121
alt: "#{event.name} Avatar",
2222
style: "view-transition-name: avatar",
2323
loading: :lazy %>
2424

25-
<div class="flex-col flex justify-center">
26-
<h1 class="mb-2 text-black font-bold" style="view-transition-name: title"><%= event.name %></h1>
25+
<div class="flex flex-col justify-center">
26+
<h1 class="mb-2 font-bold text-black" style="view-transition-name: title"><%= event.name %></h1>
2727
<h3 class="text-[#636B74]"><%= event.location %><%= event.formatted_dates %></h3>
28-
<%= external_link_to event.website.gsub(%r{^https?://}, "").gsub(%r{/$}, ""), event.website %>
28+
<% if event.website.present? %>
29+
<%= external_link_to event.website.gsub(%r{^https?://}, "").gsub(%r{/$}, ""), event.website %>
30+
<% end %>
2931
</div>
3032
</div>
3133

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<%= turbo_frame_tag social_profile do %>
2+
<div class="flex items-center gap-x-2">
3+
<span class="font-semibold">Add</span>
4+
<% SocialProfile::PROVIDERS.each do |provider| %>
5+
<%= ui_button url: new_polymorphic_path([sociable, social_profile], provider:), kind: :circle, size: :sm, class: "hover:bg-#{provider} hover:fill-white border-base-200" do %>
6+
<%= fab(provider, size: :md) %>
7+
<% end %>
8+
<% end %>
9+
</div>
10+
<% end %>
+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<%= turbo_frame_tag social_profile do %>
2+
<%= form_with(model: social_profile, url: url, class: "flex flex-col") do |form| %>
3+
<%= form.label :value, "#{form.object.provider.humanize} (URL/Handle/Username)" %>
4+
<div class="flex flex-col gap-x-1">
5+
<span class="text-red-500"><%= social_profile.errors["value"] ? social_profile.errors["value"].first : "" %></span>
6+
<div class="flex items-center gap-x-2">
7+
<%= form.hidden_field :provider, class: "input input-bordered w-full" %>
8+
<%= form.text_field :value, class: "input input-bordered w-full" %>
9+
<%= ui_button "Save", type: :submit %>
10+
</div>
11+
</div>
12+
<% end %>
13+
<% end %>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<%= turbo_stream.update "flashes" do %>
2+
<%= render "shared/flashes" %>
3+
<% end %>
4+
5+
<% if @social_profile.persisted? %>
6+
<%= turbo_stream.update "socials" do %>
7+
<%= render "speakers/socials", speaker: @social_profile.sociable %>
8+
<% end %>
9+
10+
<%= turbo_stream.append "social-profiles" do %>
11+
<%= render "social_profiles/form", social_profile: @social_profile, url: social_profile_path(@social_profile) %>
12+
<%= render "shared/new_profile", sociable: @social_profile.sociable, social_profile: SocialProfile.new %>
13+
<% end %>
14+
<% else %>
15+
<%= turbo_stream.replace @social_profile do %>
16+
<%= render "social_profiles/form", social_profile: @social_profile, url: speaker_social_profiles_path(@social_profile.sociable, @social_profile) %>
17+
<% end %>
18+
<% end %>
+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
<%= turbo_frame_tag @social_profile do %>
2+
<%= render "form", social_profile: @social_profile, url: social_profile_path(@social_profile) %>
3+
<% end %>
+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
<%= turbo_frame_tag @social_profile do %>
2+
<%= render "form", social_profile: @social_profile, url: speaker_social_profiles_path(@social_profile.sociable, @social_profile) %>
3+
<% end %>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<%= turbo_stream.update "flashes" do %>
2+
<%= render "shared/flashes" %>
3+
<% end %>
4+
5+
<%= turbo_stream.update "socials" do %>
6+
<%= render "speakers/socials", speaker: @social_profile.sociable %>
7+
<% end %>
8+
9+
<%= turbo_stream.replace @social_profile do %>
10+
<%= render "social_profiles/form", social_profile: @social_profile, url: social_profile_path(@social_profile) %>
11+
<% end %>

app/views/speakers/_form.html.erb

-30
Original file line numberDiff line numberDiff line change
@@ -9,31 +9,6 @@
99
<%= form.text_field :github, class: "input input-bordered w-full" %>
1010
</div>
1111

12-
<div>
13-
<%= form.label :twitter, "Twitter/X (URL or handle)" %>
14-
<%= form.text_field :twitter, class: "input input-bordered w-full" %>
15-
</div>
16-
17-
<div>
18-
<%= form.label :mastodon, "Mastodon (Full URL/handle)" %>
19-
<%= form.text_field :mastodon, class: "input input-bordered w-full" %>
20-
</div>
21-
22-
<div>
23-
<%= form.label :linkedin, "LinekdIn (URL or slug)" %>
24-
<%= form.text_field :linkedin, class: "input input-bordered w-full" %>
25-
</div>
26-
27-
<div>
28-
<%= form.label :bsky, "Bluesky (URL or handle)" %>
29-
<%= form.text_field :bsky, class: "input input-bordered w-full" %>
30-
</div>
31-
32-
<div>
33-
<%= form.label :speakerdeck, "Speakerdeck" %>
34-
<%= form.text_field :speakerdeck, class: "input input-bordered w-full" %>
35-
</div>
36-
3712
<div>
3813
<%= form.label :bio %>
3914
<%= form.text_area :bio, rows: 4, class: "textarea textarea-bordered w-full" %>
@@ -47,11 +22,6 @@
4722
</div>
4823
</div>
4924

50-
<div>
51-
<%= form.label :website %>
52-
<%= form.text_field :website, class: "input input-bordered w-full" %>
53-
</div>
54-
5525
<div class="flex items-center gap-4">
5626
<%= ui_button "Suggest modifications", type: :submit %>
5727
<%= ui_button "Cancel", data: {action: "click->modal#close"}, role: :button, kind: :ghost %>

0 commit comments

Comments
 (0)