Skip to content

Commit 5f3a503

Browse files
authored
feat: enhance modal UI with search, grouping, and consistent displays (#517)
- Add search functionality to translation selector with language-based filtering - Implement collapsible language groups in translation modal - Fix dropdown collapse issue during multi-selection - Standardize display logic: counts for multi-selection (translations/tafsirs), names for single-selection - Improve UX consistency across all modal tabs
1 parent ee23f84 commit 5f3a503

File tree

8 files changed

+187
-50
lines changed

8 files changed

+187
-50
lines changed

app/javascript/controllers/ayah_translation_selector_controller.js

Lines changed: 124 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,15 @@ import { Controller } from "@hotwired/stimulus";
22
import { getAyahModalPrefs, setAyahModalPrefs } from "../lib/ayah_modal_store";
33

44
export default class extends Controller {
5-
static targets = ["panel", "backdrop", "form"];
5+
static targets = [
6+
"panel",
7+
"backdrop",
8+
"form",
9+
"search",
10+
"translationList",
11+
"chevron",
12+
"groupContent",
13+
];
614

715
connect() {
816
const prefs = getAyahModalPrefs();
@@ -14,7 +22,9 @@ export default class extends Controller {
1422
}
1523
if (prefs.translationIds && prefs.translationIds.length) {
1624
const current = this.currentTranslationIds();
17-
const desired = prefs.translationIds.map((x) => parseInt(x, 10)).filter((x) => Number.isInteger(x));
25+
const desired = prefs.translationIds
26+
.map((x) => parseInt(x, 10))
27+
.filter((x) => Number.isInteger(x));
1828
if (!this.sameIds(current, desired)) {
1929
this.setTranslationIds(desired);
2030
if (this.formTarget) this.formTarget.requestSubmit();
@@ -38,24 +48,32 @@ export default class extends Controller {
3848
this.panelTarget.classList.remove("tw-flex");
3949
this.backdropTarget.classList.add("tw-hidden");
4050
this.backdropTarget.classList.remove("tw-block");
51+
// Submit form when closing to save translation selections
52+
if (this.formTarget) this.formTarget.requestSubmit();
4153
}
4254

4355
change() {
4456
const ids = this.currentTranslationIds();
4557
setAyahModalPrefs({ translationIds: ids });
46-
if (this.formTarget) this.formTarget.requestSubmit();
58+
// Removed form submission to prevent dropdown collapse during selection
4759
}
4860

4961
currentTranslationIds() {
5062
if (!this.formTarget) return [];
51-
const inputs = this.formTarget.querySelectorAll('input[name="translation_ids[]"]:checked');
52-
return Array.from(inputs).map((i) => parseInt(i.value, 10)).filter((x) => Number.isInteger(x));
63+
const inputs = this.formTarget.querySelectorAll(
64+
'input[name="translation_ids[]"]:checked',
65+
);
66+
return Array.from(inputs)
67+
.map((i) => parseInt(i.value, 10))
68+
.filter((x) => Number.isInteger(x));
5369
}
5470

5571
setTranslationIds(ids) {
5672
if (!this.formTarget) return;
5773
const set = new Set(ids.map((x) => x.toString()));
58-
const inputs = this.formTarget.querySelectorAll('input[name="translation_ids[]"]');
74+
const inputs = this.formTarget.querySelectorAll(
75+
'input[name="translation_ids[]"]',
76+
);
5977
inputs.forEach((i) => {
6078
i.checked = set.has(i.value);
6179
});
@@ -71,6 +89,105 @@ export default class extends Controller {
7189
}
7290
return true;
7391
}
74-
}
7592

93+
search() {
94+
const query = this.searchTarget.value.toLowerCase();
95+
const items =
96+
this.translationListTarget.querySelectorAll(".translation-item");
97+
const groups =
98+
this.translationListTarget.querySelectorAll(".translation-group");
99+
100+
if (query === "") {
101+
// Show all items and groups when search is empty
102+
items.forEach((item) => (item.style.display = ""));
103+
groups.forEach((group) => (group.style.display = ""));
104+
return;
105+
}
106+
107+
// Find groups that match the search query (language names)
108+
const matchingGroups = [];
109+
groups.forEach((group) => {
110+
const language = group.dataset.language;
111+
if (language && language.includes(query)) {
112+
matchingGroups.push(group);
113+
}
114+
});
115+
116+
// If any groups match the query (language search), show all items in those groups
117+
if (matchingGroups.length > 0) {
118+
items.forEach((item) => {
119+
const parentGroup = item.closest(".translation-group");
120+
const isInMatchingGroup = matchingGroups.includes(parentGroup);
121+
item.style.display = isInMatchingGroup ? "" : "none";
122+
});
123+
124+
groups.forEach((group) => {
125+
const isMatching = matchingGroups.includes(group);
126+
group.style.display = isMatching ? "" : "none";
127+
128+
// Expand matching groups
129+
if (isMatching) {
130+
const groupContent = group.querySelector(
131+
'[data-ayah-translation-selector-target="groupContent"]',
132+
);
133+
if (groupContent) {
134+
groupContent.classList.remove("tw-hidden");
135+
const chevron = group.querySelector(
136+
'[data-ayah-translation-selector-target="chevron"]',
137+
);
138+
if (chevron) chevron.style.transform = "rotate(180deg)";
139+
}
140+
}
141+
});
142+
} else {
143+
// No groups match, filter individual items by name
144+
items.forEach((item) => {
145+
const name = item.dataset.name;
146+
const matches = name.includes(query);
147+
item.style.display = matches ? "" : "none";
148+
});
76149

150+
// Hide groups that have no visible items
151+
groups.forEach((group) => {
152+
const visibleItems = group.querySelectorAll(
153+
'.translation-item[style=""], .translation-item:not([style*="none"])',
154+
);
155+
group.style.display = visibleItems.length > 0 ? "" : "none";
156+
157+
// If group is visible and has matching items, expand it
158+
if (visibleItems.length > 0) {
159+
const groupContent = group.querySelector(
160+
'[data-ayah-translation-selector-target="groupContent"]',
161+
);
162+
if (groupContent) {
163+
groupContent.classList.remove("tw-hidden");
164+
const chevron = group.querySelector(
165+
'[data-ayah-translation-selector-target="chevron"]',
166+
);
167+
if (chevron) chevron.style.transform = "rotate(180deg)";
168+
}
169+
}
170+
});
171+
}
172+
}
173+
174+
toggleGroup(event) {
175+
const button = event.currentTarget;
176+
const group = button.dataset.group;
177+
const content = this.groupContentTargets.find(
178+
(target) => target.dataset.group === group,
179+
);
180+
const chevron = this.chevronTargets.find(
181+
(target) => target.dataset.group === group,
182+
);
183+
184+
if (content) {
185+
content.classList.toggle("tw-hidden");
186+
}
187+
188+
if (chevron) {
189+
const isExpanded = !content.classList.contains("tw-hidden");
190+
chevron.style.transform = isExpanded ? "rotate(180deg)" : "rotate(0deg)";
191+
}
192+
}
193+
}

app/presenters/ayah_presenter.rb

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,12 @@ def translation_resources_by_id
5757
@translation_resources_by_id ||= translation_resources.index_by(&:id)
5858
end
5959

60+
def translation_resources_grouped_by_language
61+
@translation_resources_grouped_by_language ||= translation_resources.includes(:language).group_by do |resource|
62+
resource.language&.name&.titleize || 'Unknown'
63+
end.sort_by { |language, _| language }.to_h
64+
end
65+
6066
def translations
6167
return [] unless found?
6268
@translations ||= ayah.translations.where(resource_content_id: translation_ids).includes(:language, :foot_notes).to_a

app/views/ayah/_ayah_text.html.erb

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,10 @@
44
font_class = get_quran_script_font_family(script, @ayah) || 'qpc-hafs'
55
%>
66

7-
<div class="tw-flex tw-items-start tw-justify-between tw-gap-4 tw-flex-wrap" data-controller="ayah-script-selector">
8-
<div class="tw-flex tw-items-center tw-gap-2">
9-
<span class="tw-inline-flex tw-items-center tw-px-2 tw-py-1 tw-rounded tw-bg-gray-100 tw-text-gray-700 tw-text-xs"><%= @presenter.script_label %></span>
10-
</div>
7+
<div class="tw-flex tw-items-start tw-justify-between tw-gap-4 tw-flex-wrap" data-controller="ayah-script-selector">
8+
<div class="tw-flex tw-items-center tw-gap-2">
9+
<span class="tw-inline-flex tw-items-center tw-px-2 tw-py-1 tw-rounded tw-bg-gray-100 tw-text-gray-700 tw-text-xs"><%= @presenter.script_label %></span>
10+
</div>
1111

1212
<button type="button"
1313
class="tw-px-3 tw-py-1.5 tw-bg-white tw-text-gray-900 tw-rounded tw-text-sm tw-border tw-border-gray-200 hover:tw-bg-gray-50"

app/views/ayah/_recitation.html.erb

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
<%= turbo_frame_tag :ayah_recitation do %>
2-
<div class="tw-flex tw-items-start tw-justify-between tw-gap-4 tw-flex-wrap" data-controller="ayah-recitation-selector">
3-
<div class="tw-flex tw-flex-wrap tw-gap-2">
4-
<% rc = @presenter.recitation_resource %>
5-
<span class="tw-inline-flex tw-items-center tw-px-2 tw-py-1 tw-rounded tw-bg-gray-100 tw-text-gray-700 tw-text-xs">
6-
<%= rc&.name.presence || "Recitation ##{@presenter.recitation_resource_id}" %>
7-
</span>
8-
</div>
2+
<div class="tw-flex tw-items-start tw-justify-between tw-gap-4 tw-flex-wrap" data-controller="ayah-recitation-selector">
3+
<div class="tw-flex tw-flex-wrap tw-gap-2">
4+
<% rc = @presenter.recitation_resource %>
5+
<span class="tw-inline-flex tw-items-center tw-px-2 tw-py-1 tw-rounded tw-bg-gray-100 tw-text-gray-700 tw-text-xs">
6+
<%= rc&.name.presence || "Recitation ##{@presenter.recitation_resource_id}" %>
7+
</span>
8+
</div>
99

1010
<button type="button"
1111
class="tw-px-3 tw-py-1.5 tw-bg-white tw-text-gray-900 tw-rounded tw-text-sm tw-border tw-border-gray-200 hover:tw-bg-gray-50"

app/views/ayah/_tafsirs.html.erb

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,12 @@
33
<%= @ayah.text_qpc_hafs %>
44
</div>
55

6-
<div class="tw-flex tw-items-start tw-justify-between tw-gap-4 tw-flex-wrap" data-controller="ayah-tafsir-selector">
7-
<div class="tw-flex tw-flex-wrap tw-gap-2">
8-
<% @presenter.tafsir_ids.each do |rid| %>
9-
<% rc = @presenter.tafsir_resources_by_id[rid] %>
10-
<span class="tw-inline-flex tw-items-center tw-px-2 tw-py-1 tw-rounded tw-bg-gray-100 tw-text-gray-700 tw-text-xs">
11-
<%= rc&.name.presence || "Tafsir ##{rid}" %>
12-
</span>
13-
<% end %>
14-
</div>
6+
<div class="tw-flex tw-items-start tw-justify-between tw-gap-4 tw-flex-wrap" data-controller="ayah-tafsir-selector">
7+
<div class="tw-flex tw-flex-wrap tw-gap-2">
8+
<span class="tw-inline-flex tw-items-center tw-px-3 tw-py-1 tw-rounded tw-bg-blue-100 tw-text-blue-800 tw-text-sm tw-font-medium">
9+
<%= @presenter.tafsir_ids.size %> tafsir<%= 's' if @presenter.tafsir_ids.size != 1 %> selected
10+
</span>
11+
</div>
1512

1613
<button type="button"
1714
class="tw-px-3 tw-py-1.5 tw-bg-white tw-text-gray-900 tw-rounded tw-text-sm tw-border tw-border-gray-200 hover:tw-bg-gray-50"

app/views/ayah/_translations.html.erb

Lines changed: 29 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,9 @@
55

66
<div class="tw-flex tw-items-start tw-justify-between tw-gap-4 tw-flex-wrap" data-controller="ayah-translation-selector">
77
<div class="tw-flex tw-flex-wrap tw-gap-2">
8-
<% @presenter.translation_ids.each do |rid| %>
9-
<% rc = @presenter.translation_resources_by_id[rid] %>
10-
<span class="tw-inline-flex tw-items-center tw-px-2 tw-py-1 tw-rounded tw-bg-gray-100 tw-text-gray-700 tw-text-xs">
11-
<%= rc&.name.presence || "Translation ##{rid}" %>
12-
</span>
13-
<% end %>
8+
<span class="tw-inline-flex tw-items-center tw-px-3 tw-py-1 tw-rounded tw-bg-blue-100 tw-text-blue-800 tw-text-sm tw-font-medium">
9+
<%= @presenter.translation_ids.size %> translation<%= 's' if @presenter.translation_ids.size != 1 %> selected
10+
</span>
1411
</div>
1512

1613
<button type="button"
@@ -27,14 +24,34 @@
2724
<i class="fa fa-times"></i>
2825
</button>
2926
</div>
27+
<div class="tw-p-4 tw-border-b tw-border-gray-200">
28+
<input type="text"
29+
placeholder="Search translations..."
30+
class="tw-w-full tw-px-3 tw-py-2 tw-border tw-border-gray-300 tw-rounded-md tw-text-sm tw-focus:outline-none tw-focus:ring-2 tw-focus:ring-blue-500 tw-focus:border-blue-500"
31+
data-ayah-translation-selector-target="search"
32+
data-action="input->ayah-translation-selector#search">
33+
</div>
3034
<div class="tw-p-4 tw-overflow-y-auto tw-flex-1">
3135
<%= form_with url: ayah_translations_path(@ayah.verse_key), method: :get, local: true, data: { turbo_frame: :ayah_translations }, html: { data: { ayah_translation_selector_target: "form" } } do %>
32-
<div class="tw-space-y-2">
33-
<% @presenter.translation_resources.each do |rc| %>
34-
<label class="tw-flex tw-items-start tw-gap-2 tw-text-sm tw-text-gray-800">
35-
<%= check_box_tag "translation_ids[]", rc.id, @presenter.translation_ids.include?(rc.id), class: "tw-mt-1", data: { action: "change->ayah-translation-selector#change" } %>
36-
<span><%= rc.name %></span>
37-
</label>
36+
<div class="tw-space-y-3" data-ayah-translation-selector-target="translationList">
37+
<% @presenter.translation_resources_grouped_by_language.each do |language, resources| %>
38+
<div class="translation-group" data-language="<%= language.downcase %>">
39+
<button type="button"
40+
class="tw-flex tw-items-center tw-justify-between tw-w-full tw-px-3 tw-py-2 tw-text-left tw-bg-gray-50 tw-border tw-border-gray-200 tw-rounded-md tw-text-sm tw-font-medium tw-text-gray-900 hover:tw-bg-gray-100"
41+
data-action="click->ayah-translation-selector#toggleGroup"
42+
data-group="<%= language.downcase %>">
43+
<span><%= language %> (<%= resources.size %>)</span>
44+
<i class="fa fa-chevron-down tw-transition-transform tw-duration-200" data-ayah-translation-selector-target="chevron" data-group="<%= language.downcase %>"></i>
45+
</button>
46+
<div class="tw-mt-2 tw-space-y-2 tw-hidden" data-ayah-translation-selector-target="groupContent" data-group="<%= language.downcase %>">
47+
<% resources.each do |rc| %>
48+
<label class="tw-flex tw-items-start tw-gap-2 tw-text-sm tw-text-gray-800 translation-item" data-name="<%= rc.name.downcase %>">
49+
<%= check_box_tag "translation_ids[]", rc.id, @presenter.translation_ids.include?(rc.id), class: "tw-mt-1", data: { action: "change->ayah-translation-selector#change" } %>
50+
<span><%= rc.name %></span>
51+
</label>
52+
<% end %>
53+
</div>
54+
</div>
3855
<% end %>
3956
</div>
4057
<% end %>

app/views/ayah/_transliteration.html.erb

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,13 @@
33
<%= @ayah.text_qpc_hafs %>
44
</div>
55

6-
<div class="tw-flex tw-items-start tw-justify-between tw-gap-4 tw-flex-wrap" data-controller="ayah-transliteration-selector">
7-
<div class="tw-flex tw-flex-wrap tw-gap-2">
8-
<% rc = @presenter.transliteration_resources_by_id[@presenter.transliteration_id] %>
9-
<span class="tw-inline-flex tw-items-center tw-px-2 tw-py-1 tw-rounded tw-bg-gray-100 tw-text-gray-700 tw-text-xs">
10-
<%= rc&.name.presence || "Transliteration ##{@presenter.transliteration_id}" %>
11-
</span>
12-
</div>
6+
<div class="tw-flex tw-items-start tw-justify-between tw-gap-4 tw-flex-wrap" data-controller="ayah-transliteration-selector">
7+
<div class="tw-flex tw-flex-wrap tw-gap-2">
8+
<% rc = @presenter.transliteration_resources_by_id[@presenter.transliteration_id] %>
9+
<span class="tw-inline-flex tw-items-center tw-px-2 tw-py-1 tw-rounded tw-bg-gray-100 tw-text-gray-700 tw-text-xs">
10+
<%= rc&.name.presence || "Transliteration ##{@presenter.transliteration_id}" %>
11+
</span>
12+
</div>
1313

1414
<button type="button"
1515
class="tw-px-3 tw-py-1.5 tw-bg-white tw-text-gray-900 tw-rounded tw-text-sm tw-border tw-border-gray-200 hover:tw-bg-gray-50"

app/views/ayah/_words.html.erb

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
<%= turbo_frame_tag :ayah_words do %>
2-
<div class="tw-flex tw-items-start tw-justify-between tw-gap-4 tw-flex-wrap" data-controller="ayah-word-translation-selector">
3-
<div class="tw-text-sm tw-text-gray-700">
4-
Word by word translation
5-
</div>
2+
<div class="tw-flex tw-items-start tw-justify-between tw-gap-4 tw-flex-wrap" data-controller="ayah-word-translation-selector">
3+
<div class="tw-text-sm tw-text-gray-700">
4+
Word by word translation
5+
</div>
66

77
<button type="button"
88
class="tw-px-3 tw-py-1.5 tw-bg-white tw-text-gray-900 tw-rounded tw-text-sm tw-border tw-border-gray-200 hover:tw-bg-gray-50"

0 commit comments

Comments
 (0)