Skip to content

Commit 575e66b

Browse files
committed
Improve Person form with tab layout
1 parent 10f8782 commit 575e66b

File tree

9 files changed

+164
-86
lines changed

9 files changed

+164
-86
lines changed

app/controllers/better_together/people_controller.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ def person_params
8080
params.require(:person).permit(
8181
:name, :description, :profile_image, :slug, :locale,
8282
:profile_image, :cover_image, :remove_profile_image, :remove_cover_image,
83-
*resource_class.extra_permitted_attributes
83+
*resource_class.permitted_attributes
8484
)
8585
end
8686

app/helpers/better_together/image_helper.rb

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,48 @@ def cover_image_tag(entity, options = {}) # rubocop:todo Metrics/MethodLength, M
3636
image_tag(image_url(default_image), **image_tag_attributes)
3737
end
3838
end
39+
40+
def card_image_tag(entity, options = {}) # rubocop:todo Metrics/MethodLength, Metrics/AbcSize
41+
image_classes = "card-img-top #{options[:class]}"
42+
image_style = options[:style].to_s
43+
image_width = options[:width] || 1200
44+
image_height = options[:height] || 800
45+
image_format = options[:format] || 'jpg'
46+
image_alt = options[:alt] || 'Card Image'
47+
image_title = options[:title] || 'Card Image'
48+
image_tag_attributes = {
49+
class: image_classes,
50+
style: image_style,
51+
alt: image_alt,
52+
title: image_title
53+
}
54+
55+
# Determine if entity has a card image
56+
if entity.respond_to?(:card_image) && entity.card_image.attached?
57+
attachment = if entity.respond_to?(:optimized_card_image)
58+
entity.optimized_card_image
59+
else
60+
entity.card_image_variant(image_width, image_height)
61+
end
62+
63+
image_tag(attachment.url, **image_tag_attributes)
64+
else
65+
# Use a default image based on the entity type
66+
default_image = default_card_image(entity, image_format)
67+
image_tag(image_url(default_image), **image_tag_attributes)
68+
end
69+
end
70+
71+
def default_card_image(entity, image_format)
72+
case entity.class.name
73+
when 'BetterTogether::Person'
74+
"card_images/default_card_image_person.#{image_format}"
75+
when 'BetterTogether::Community'
76+
"card_images/default_card_image_community.#{image_format}"
77+
else
78+
"card_images/default_card_image_generic.#{image_format}"
79+
end
80+
end
3981
# rubocop:enable Metrics/AbcSize
4082
# rubocop:enable Metrics/CyclomaticComplexity
4183
# rubocop:enable Metrics/PerceivedComplexity

app/javascript/controllers/better_together/image_preview_controller.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { Controller } from "@hotwired/stimulus"
22

33
export default class extends Controller {
4-
static targets = ["input", "preview", "deleteField", "deleteButton"]
4+
static targets = ["input", "preview", "deleteField", "deleteButton"] // Ensure these match your HTML data-target attributes
55

66
connect() {
77
if (this.previewTarget.dataset.imageClasses) {

app/models/better_together/person.rb

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,8 @@ def self.primary_community_delegation_attrs
5959
validates :name,
6060
presence: true
6161

62+
translates :description_html, backend: :action_text
63+
6264
delegate :email, to: :user, allow_nil: true
6365

6466
has_one_attached :profile_image
@@ -74,6 +76,10 @@ def cover_image_variant(width, height)
7476
cover_image.variant(resize_to_fill: [width, height]).processed
7577
end
7678

79+
def description_html(locale: I18n.locale)
80+
super || description
81+
end
82+
7783
def handle
7884
slug
7985
end

app/views/better_together/content/blocks/fields/_image.html.erb

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,9 @@
1616
class: 'btn btn-secondary',
1717
data: {
1818
'action' => "better_together--image-preview#toggleDelete",
19-
'clearValue' => t('globals.clear'),
20-
'undoClearValue' => t('globals.undo_clear'),
21-
'noImageValue' => t('globals.no_image')
19+
'clear-value' => t('globals.clear'),
20+
'undo-clear-value' => t('globals.undo_clear'),
21+
'no-image-value' => t('globals.no_image')
2222
},
2323
"data-better_together--image-preview-target" => "deleteButton",
2424
} %>
Lines changed: 99 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
<%= form_with(model: person, class: "contents", multipart: true, data: { controller: 'better_together--form-validation' }) do |form| %>
1+
<%= form_with(model: person, class: "contents", multipart: true, data: { controller: "better_together--form-validation better_together--tabs" }) do |form| %>
22
<% if person.errors.any? %>
33
<div class="bg-red-50 text-red-500 px-3 py-2 font-medium rounded-lg mt-3" role="alert">
4-
<h2><%= pluralize(person.errors.count, "error") %> prohibited this person from being saved:</h2>
4+
<h2><%= pluralize(person.errors.count, t('helpers.errors.heading')) %> <%= t('helpers.errors.prohibited') %></h2>
55
<ul>
66
<% person.errors.full_messages.each do |message| %>
77
<li><%= message %></li>
@@ -10,87 +10,108 @@
1010
</div>
1111
<% end %>
1212

13-
<div class="mb-3">
14-
<%= required_label(form, :name, class: "form-label") %>
15-
<%= form.text_field :name, class: "form-control", required: true %>
16-
<small class="form-text text-muted"><%= t('helpers.hint.person.name') %></small>
17-
</div>
18-
19-
<div class="mb-3">
20-
<%= form.label :description, class: "form-label" %>
21-
<%= form.text_area :description, class: "form-control" %>
22-
<small class="form-text text-muted"><%= t('helpers.hint.person.description') %></small>
23-
</div>
24-
25-
<div class="mb-3">
26-
<%= form.label :slug, class: "form-label" %>
27-
<%= form.text_field :slug, class: "form-control" %>
28-
<small class="form-text text-muted"><%= t('helpers.hint.person.slug') %></small>
29-
</div>
30-
31-
<div class="mb-3">
32-
<%= required_label(form, :locale, class: "form-label") %>
33-
<%= language_select_field(form:, selected_locale: person.locale) %>
34-
<small class="form-text text-muted"><%= t('helpers.hint.person.locale') %></small>
35-
</div>
36-
37-
<div class="mb-3" data-controller="better_together--image-preview"
38-
data-image-preview-clear-value="<%= t('globals.clear') %>"
39-
data-image-preview-undo-clear-value="<%= t('globals.undo_clear') %>">
40-
<%= label_tag do %>
41-
<%= person.class.human_attribute_name(:profile_image) %>
42-
<% if person.profile_image.attached? %>
43-
: <%= person.profile_image.filename %>
44-
<% end %>
45-
<% end %>
46-
47-
<div class="input-group">
48-
<%= form.file_field :profile_image, accept: acceptable_image_file_types, "data-better_together--image-preview-target" => 'input', data: { 'action' => "better_together--image-preview#preview" }, class: "form-control" %>
49-
<%# Hidden field to track removal state %>
50-
<%= form.hidden_field :remove_profile_image, value: '0', "data-better_together--image-preview-target" => "deleteField" %>
51-
52-
<%= button_tag t('globals.clear'), { type: 'button', class: 'btn btn-secondary', "data-better_together--image-preview-target" => "deleteButton", data: { 'clearValue' => t('globals.clear'), 'undoClearValue' => t('globals.undo_clear'), 'noImageValue' => t('globals.no_image') } } %>
53-
</div>
54-
55-
<small class="form-text text-muted"><%= t('helpers.hint.person.profile_image') %></small>
56-
57-
<!-- Image preview container -->
58-
<div class="my-3 text-center preview-target" data-better_together--image-preview-target="preview" data-image-classes="profile-image" data-url="<%= person.profile_image.url if person.profile_image.attached? %>">
59-
<!-- The image preview will be dynamically inserted here -->
13+
<div class="row">
14+
<!-- Vertical Pills Navigation -->
15+
<div class="col-md-3">
16+
<div class="nav flex-column nav-pills" id="person-form-tabs" role="tablist" aria-orientation="vertical">
17+
<button class="nav-link active" id="person-details-tab" data-bs-toggle="pill" data-bs-target="#person-details" type="button" role="tab" aria-controls="person-details" aria-selected="true" data-better_together--tabs-target="tab">
18+
<%= t('better_together.people.tabs.details') %>
19+
</button>
20+
<button class="nav-link" id="person-images-tab" data-bs-toggle="pill" data-bs-target="#person-images" type="button" role="tab" aria-controls="person-images" aria-selected="false" data-better_together--tabs-target="tab">
21+
<%= t('better_together.people.tabs.images') %>
22+
</button>
23+
<button class="nav-link" id="person-contact-details-tab" data-bs-toggle="pill" data-bs-target="#person-contact-details" type="button" role="tab" aria-controls="person-contact-details" aria-selected="false" data-better_together--tabs-target="tab">
24+
<%= t('better_together.people.tabs.contact_details') %>
25+
</button>
26+
</div>
6027
</div>
61-
</div>
62-
63-
<div class="mb-3" data-controller="better_together--image-preview"
64-
data-image-preview-clear-value="<%= t('globals.clear') %>"
65-
data-image-preview-undo-clear-value="<%= t('globals.undo_clear') %>">
66-
<%= label_tag do %>
67-
<%= person.class.human_attribute_name(:cover_image) %>
68-
<% if person.cover_image.attached? %>
69-
: <%= person.cover_image.filename %>
70-
<% end %>
71-
<% end %>
72-
73-
<div class="input-group">
74-
<%= form.file_field :cover_image, accept: acceptable_image_file_types, "data-better_together--image-preview-target" => 'input', data: { 'action' => "better_together--image-preview#preview" }, class: "form-control" %>
75-
<%# Hidden field to track removal state %>
76-
<%= form.hidden_field :remove_cover_image, value: '0', "data-better_together--image-preview-target" => "deleteField" %>
7728

78-
<%= button_tag t('globals.clear'), { type: 'button', class: 'btn btn-secondary', 'data-better_together--image-preview-target' => "deleteButton", data: { 'action' => "better_together--image-preview#toggleDelete", clearValue: t('globals.clear'), undoClearValue: t('globals.undo_clear'), noImageValue: t('globals.no_image') } } %>
29+
<!-- Tab Content -->
30+
<div class="col-md-9 tab-content" id="person-form-tabs-content">
31+
<!-- Details Tab -->
32+
<div class="nav-tab-pane tab-pane fade show active" id="person-details" role="tabpanel" aria-labelledby="person-details-tab">
33+
<div class="mb-3">
34+
<%= required_label(form, :name, class: "form-label") %>
35+
<%= render partial: 'better_together/shared/translated_string_field', locals: { model: person, form: form, attribute: 'name' } %>
36+
<small class="form-text text-muted"><%= t('helpers.hint.person.name') %></small>
37+
</div>
38+
39+
<div class="mb-3">
40+
<%= form.label :description, class: "form-label" %>
41+
<%= render partial: 'better_together/shared/translated_rich_text_field', locals: { model: person, form: form, attribute: 'description_html' } %>
42+
<small class="form-text text-muted"><%= t('helpers.hint.person.description') %></small>
43+
</div>
44+
45+
<div class="mb-3">
46+
<%= form.label :slug, class: "form-label" %>
47+
<%= render partial: 'better_together/shared/translated_string_field', locals: { model: person, form: form, attribute: 'slug' } %>
48+
<small class="form-text text-muted"><%= t('helpers.hint.person.slug') %></small>
49+
</div>
50+
51+
<div class="mb-3">
52+
<%= required_label(form, :locale, class: "form-label") %>
53+
<%= language_select_field(form:, selected_locale: person.locale) %>
54+
<small class="form-text text-muted"><%= t('helpers.hint.person.locale') %></small>
55+
</div>
56+
</div>
57+
58+
<!-- Images Tab -->
59+
<div class="nav-tab-pane tab-pane fade" id="person-images" role="tabpanel" aria-labelledby="person-images-tab">
60+
<div class="mb-3" data-controller="better_together--image-preview"
61+
data-better_together--image-preview-clear-value="<%= t('globals.clear') %>"
62+
data-better_together--image-preview-undo-clear-value="<%= t('globals.undo_clear') %>">
63+
<%= label_tag do %>
64+
<%= t('helpers.label.person.profile_image') %>
65+
<% if person.profile_image.attached? %>
66+
: <%= person.profile_image.filename %>
67+
<% end %>
68+
<% end %>
69+
70+
<div class="input-group">
71+
<%= form.file_field :profile_image, accept: acceptable_image_file_types, "data-better_together--image-preview-target" => 'input', data: { 'action' => "better_together--image-preview#preview" }, class: "form-control" %>
72+
<%= form.hidden_field :remove_profile_image, value: '0', "data-better_together--image-preview-target" => "deleteField" %>
73+
<%= button_tag t('globals.clear'), { type: 'button', class: 'btn btn-secondary', "data-better_together--image-preview-target" => "deleteButton", data: { 'action' => "better_together--image-preview#toggleDelete", 'clear-value' => t('globals.clear'), 'undo-clear-value' => t('globals.undo_clear'), 'no-image-value' => t('globals.no_image') } } %>
74+
</div>
75+
76+
<small class="form-text text-muted"><%= t('helpers.hint.person.profile_image') %></small>
77+
78+
<div class="my-3 text-center preview-target" data-better_together--image-preview-target="preview" data-image-classes="profile-image" data-url="<%= person.profile_image.url if person.profile_image.attached? %>">
79+
</div>
80+
</div>
81+
82+
<div class="mb-3" data-controller="better_together--image-preview"
83+
data-better_together--image-preview-clear-value="<%= t('globals.clear') %>"
84+
data-better_together--image-preview-undo-clear-value="<%= t('globals.undo_clear') %>">
85+
<%= label_tag do %>
86+
<%= t('helpers.label.person.cover_image') %>
87+
<% if person.cover_image.attached? %>
88+
: <%= person.cover_image.filename %>
89+
<% end %>
90+
<% end %>
91+
92+
<div class="input-group">
93+
<%= form.file_field :cover_image, accept: acceptable_image_file_types, "data-better_together--image-preview-target" => 'input', data: { 'action' => "better_together--image-preview#preview" }, class: "form-control" %>
94+
<%= form.hidden_field :remove_cover_image, value: '0', "data-better_together--image-preview-target" => "deleteField" %>
95+
<%= button_tag t('globals.clear'), { type: 'button', class: 'btn btn-secondary', 'data-better_together--image-preview-target' => "deleteButton", data: { 'action' => "better_together--image-preview#toggleDelete", 'clear-value' => t('globals.clear'), 'undo-clear-value' => t('globals.undo_clear'), 'no-image-value' => t('globals.no_image') } } %>
96+
</div>
97+
98+
<small class="form-text text-muted"><%= t('helpers.hint.person.cover_image') %></small>
99+
100+
<div class="my-3 text-center preview-target" data-better_together--image-preview-target="preview" data-image-classes="cover-image" data-url="<%= person.cover_image.url if person.cover_image.attached? %>">
101+
</div>
102+
</div>
103+
</div>
104+
105+
<!-- Contact Details Tab -->
106+
<div class="nav-tab-pane tab-pane fade" id="person-contact-details" role="tabpanel" aria-labelledby="person-contact-details-tab">
107+
<div class="mb-3">
108+
<%= render partial: 'better_together/contact_details/contact_detail_fields', locals: { form: } if person.persisted? %>
109+
</div>
110+
</div>
79111
</div>
80-
81-
<small class="form-text text-muted"><%= t('helpers.hint.person.cover_image') %></small>
82-
83-
<!-- Image preview container (empty initially) -->
84-
<div class="my-3 text-center preview-target" data-better_together--image-preview-target="preview" data-image-classes="cover-image" data-url="<%= person.cover_image.url if person.cover_image.attached? %>">
85-
<!-- The image preview will be dynamically inserted here -->
86-
</div>
87-
</div>
88-
89-
<div class="mb-3">
90-
<%= render partial: 'better_together/contact_details/contact_detail_fields', locals: { form: } if person.persisted? %>
91112
</div>
92113

93114
<div class="mb-3">
94-
<%= form.submit "Save", class: "btn btn-primary" %>
115+
<%= form.submit t('better_together.people.submit.save'), class: "btn btn-primary" %>
95116
</div>
96117
<% end %>

app/views/better_together/people/show.html.erb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@
6363
<div class="col-md-12">
6464
<!-- Description Section -->
6565
<p class="card-text text-muted">
66-
<%= @person.description.presence || t('globals.no_description') %>
66+
<%= @person.description_html.presence || @person.description.presence || t('globals.no_description') %>
6767
</p>
6868

6969
<%= share_buttons(shareable: @person) if @person.privacy_public? %>

config/locales/en.yml

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,13 @@ en:
136136
failed_tracking: "Failed to track share."
137137
error_tracking: "Error tracking share:"
138138
ga_not_initialized: "Google Analytics not initialized."
139+
people:
140+
tabs:
141+
details: "Details"
142+
images: "Images"
143+
contact_details: "Contact Details"
144+
submit:
145+
save: "Save"
139146
date:
140147
abbr_day_names:
141148
- Sun
@@ -305,6 +312,7 @@ en:
305312
name: "Enter the full name of the person."
306313
description: "Provide a brief description or biography."
307314
slug: "A URL-friendly identifier, typically auto-generated."
315+
locale: "Select the preferred language for the person."
308316
profile_image: "Upload a profile image for the person."
309317
cover_image: "Upload a cover image to display at the top of the profile."
310318
select:
@@ -410,4 +418,4 @@ en:
410418
default: "%a, %d %b %Y %H:%M:%S %z"
411419
long: "%B %d, %Y %H:%M"
412420
short: "%d %b %H:%M"
413-
pm: pm
421+
pm: pm

config/locales/translation_en.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,7 @@ en:
270270
community_roles: Community roles #g
271271
identifications: Identifications #g
272272
identifier: Identifier #g
273+
locale: Language
273274
lock_version: Lock version #g
274275
member_communities: Member communities #g
275276
member_platforms: Member platforms #g
@@ -278,7 +279,7 @@ en:
278279
person_community_memberships: Person community memberships #g
279280
person_platform_memberships: Person platform memberships #g
280281
platform_roles: Platform roles #g
281-
slug: Slug #g
282+
slug: Username #g
282283
slugs: Slugs #g
283284
string_translations: String translations #g
284285
text_translations: Text translations #g

0 commit comments

Comments
 (0)