mirror of
https://github.com/maybe-finance/maybe.git
synced 2025-07-21 22:29:38 +02:00
Consolidate dropdown controllers (#600)
* Basic listbox and popover controllers with temporary example * Separate select and menu controllers
This commit is contained in:
parent
0a0289846e
commit
4f0b2de4ef
14 changed files with 298 additions and 150 deletions
|
@ -2,8 +2,4 @@ module FormsHelper
|
||||||
def form_field_tag(&)
|
def form_field_tag(&)
|
||||||
tag.div class: "form-field", &
|
tag.div class: "form-field", &
|
||||||
end
|
end
|
||||||
|
|
||||||
def currency_dropdown(f: nil, options: [])
|
|
||||||
render partial: "shared/currency_dropdown", locals: { f: f, options: options }
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,58 +0,0 @@
|
||||||
import { Controller } from "@hotwired/stimulus"
|
|
||||||
|
|
||||||
// Connects to data-controller="dropdown"
|
|
||||||
export default class extends Controller {
|
|
||||||
static targets = ["menu", "input", "label", "option"]
|
|
||||||
|
|
||||||
toggleMenu(event) {
|
|
||||||
event.stopPropagation(); // Prevent event from closing the menu immediately
|
|
||||||
this.repositionDropdown();
|
|
||||||
this.menuTarget.classList.toggle("hidden");
|
|
||||||
}
|
|
||||||
|
|
||||||
hideMenu = () => {
|
|
||||||
this.menuTarget.classList.add("hidden");
|
|
||||||
}
|
|
||||||
|
|
||||||
connect() {
|
|
||||||
document.addEventListener("click", this.hideMenu);
|
|
||||||
}
|
|
||||||
|
|
||||||
disconnect() {
|
|
||||||
document.removeEventListener("click", this.hideMenu);
|
|
||||||
}
|
|
||||||
|
|
||||||
repositionDropdown () {
|
|
||||||
const button = this.menuTarget.previousElementSibling;
|
|
||||||
const menu = this.menuTarget;
|
|
||||||
|
|
||||||
// Calculate position
|
|
||||||
const buttonRect = button.getBoundingClientRect();
|
|
||||||
menu.style.top = `${buttonRect.bottom + window.scrollY}px`;
|
|
||||||
menu.style.left = `${buttonRect.left + window.scrollX}px`;
|
|
||||||
}
|
|
||||||
|
|
||||||
selectOption (e) {
|
|
||||||
const value = e.target.getAttribute('data-value');
|
|
||||||
|
|
||||||
if (value) {
|
|
||||||
// Remove active option background and tick
|
|
||||||
this.optionTargets.forEach((element) => {
|
|
||||||
element.classList.remove('bg-gray-100');
|
|
||||||
element.children[0].classList.add('hidden');
|
|
||||||
});
|
|
||||||
|
|
||||||
// Set currency value and label
|
|
||||||
if (this.hasInputTarget) {
|
|
||||||
this.inputTarget.value = value;
|
|
||||||
}
|
|
||||||
if (this.hasLabelTarget) {
|
|
||||||
this.labelTarget.innerHTML = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reassign active option background and tick
|
|
||||||
e.currentTarget.classList.add('bg-gray-100')
|
|
||||||
e.currentTarget.children[0].classList.remove('hidden');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,36 +0,0 @@
|
||||||
import { Controller } from "@hotwired/stimulus"
|
|
||||||
|
|
||||||
// Connects to data-controller="dropdown"
|
|
||||||
export default class extends Controller {
|
|
||||||
static targets = ["menu"]
|
|
||||||
static values = { closeOnClick: { type: Boolean, default: true } }
|
|
||||||
|
|
||||||
toggleMenu = (e) => {
|
|
||||||
e.stopPropagation(); // Prevent event from closing the menu immediately
|
|
||||||
this.menuTarget.classList.contains("hidden") ? this.showMenu() : this.hideMenu();
|
|
||||||
}
|
|
||||||
|
|
||||||
showMenu = () => {
|
|
||||||
document.addEventListener("click", this.onDocumentClick);
|
|
||||||
this.menuTarget.classList.remove("hidden");
|
|
||||||
}
|
|
||||||
|
|
||||||
hideMenu = () => {
|
|
||||||
document.removeEventListener("click", this.onDocumentClick);
|
|
||||||
this.menuTarget.classList.add("hidden");
|
|
||||||
}
|
|
||||||
|
|
||||||
disconnect = () => {
|
|
||||||
this.hideMenu();
|
|
||||||
}
|
|
||||||
|
|
||||||
onDocumentClick = (e) => {
|
|
||||||
if (this.menuTarget.contains(e.target) && !this.closeOnClickValue ) {
|
|
||||||
// user has clicked inside of the dropdown
|
|
||||||
e.stopPropagation();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.hideMenu();
|
|
||||||
}
|
|
||||||
}
|
|
75
app/javascript/controllers/menu_controller.js
Normal file
75
app/javascript/controllers/menu_controller.js
Normal file
|
@ -0,0 +1,75 @@
|
||||||
|
import { Controller } from "@hotwired/stimulus";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A "menu" can contain arbitrary content including non-clickable items, links, buttons, and forms.
|
||||||
|
*
|
||||||
|
* - If you need a form-enabled "select" element, use the "listbox" controller instead.
|
||||||
|
*/
|
||||||
|
export default class extends Controller {
|
||||||
|
static targets = ["button", "content"];
|
||||||
|
|
||||||
|
connect() {
|
||||||
|
this.show = false;
|
||||||
|
this.contentTarget.classList.add("hidden"); // Initially hide the popover
|
||||||
|
this.buttonTarget.addEventListener("click", this.toggle);
|
||||||
|
this.element.addEventListener("keydown", this.handleKeydown);
|
||||||
|
document.addEventListener("click", this.handleOutsideClick);
|
||||||
|
document.addEventListener("turbo:load", this.handleTurboLoad);
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnect() {
|
||||||
|
this.element.removeEventListener("keydown", this.handleKeydown);
|
||||||
|
this.buttonTarget.removeEventListener("click", this.toggle);
|
||||||
|
document.removeEventListener("click", this.handleOutsideClick);
|
||||||
|
document.removeEventListener("turbo:load", this.handleTurboLoad);
|
||||||
|
this.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
// If turbo reloads, we maintain the state of the menu
|
||||||
|
handleTurboLoad = () => {
|
||||||
|
if (!this.show) this.close();
|
||||||
|
};
|
||||||
|
|
||||||
|
handleOutsideClick = (event) => {
|
||||||
|
if (this.show && !this.element.contains(event.target)) {
|
||||||
|
this.close();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
handleKeydown = (event) => {
|
||||||
|
switch (event.key) {
|
||||||
|
case " ":
|
||||||
|
event.preventDefault(); // Prevent the default action to avoid scrolling
|
||||||
|
if (document.activeElement === this.buttonTarget) {
|
||||||
|
this.toggle();
|
||||||
|
}
|
||||||
|
case "Escape":
|
||||||
|
this.close();
|
||||||
|
this.buttonTarget.focus(); // Bring focus back to the button
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
toggle = () => {
|
||||||
|
this.show = !this.show;
|
||||||
|
this.contentTarget.classList.toggle("hidden", !this.show);
|
||||||
|
if (this.show) {
|
||||||
|
this.focusFirstElement();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
close() {
|
||||||
|
this.show = false;
|
||||||
|
this.contentTarget.classList.add("hidden");
|
||||||
|
}
|
||||||
|
|
||||||
|
focusFirstElement() {
|
||||||
|
const focusableElements =
|
||||||
|
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])';
|
||||||
|
const firstFocusableElement =
|
||||||
|
this.contentTarget.querySelectorAll(focusableElements)[0];
|
||||||
|
if (firstFocusableElement) {
|
||||||
|
firstFocusableElement.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
141
app/javascript/controllers/select_controller.js
Normal file
141
app/javascript/controllers/select_controller.js
Normal file
|
@ -0,0 +1,141 @@
|
||||||
|
import { Controller } from "@hotwired/stimulus";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A custom "select" element that follows accessibility patterns of a native select element.
|
||||||
|
*
|
||||||
|
* - If you need to display arbitrary content including non-clickable items, links, buttons, and forms, use the "popover" controller instead.
|
||||||
|
*/
|
||||||
|
export default class extends Controller {
|
||||||
|
static classes = ["active"];
|
||||||
|
static targets = ["option", "button", "list", "input", "buttonText"];
|
||||||
|
|
||||||
|
connect() {
|
||||||
|
this.show = false;
|
||||||
|
this.syncButtonTextWithInput();
|
||||||
|
this.listTarget.classList.add("hidden");
|
||||||
|
this.buttonTarget.addEventListener("click", this.toggleList);
|
||||||
|
this.element.addEventListener("keydown", this.handleKeydown);
|
||||||
|
document.addEventListener("click", this.handleOutsideClick);
|
||||||
|
this.element.addEventListener("turbo:load", this.handleTurboLoad);
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnect() {
|
||||||
|
this.element.removeEventListener("keydown", this.handleKeydown);
|
||||||
|
document.removeEventListener("click", this.handleOutsideClick);
|
||||||
|
this.buttonTarget.removeEventListener("click", this.toggleList);
|
||||||
|
this.element.removeEventListener("turbo:load", this.handleTurboLoad);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleOutsideClick = (event) => {
|
||||||
|
if (this.show && !this.element.contains(event.target)) {
|
||||||
|
this.close();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
handleTurboLoad = () => {
|
||||||
|
this.close();
|
||||||
|
this.syncButtonTextWithInput();
|
||||||
|
};
|
||||||
|
|
||||||
|
handleKeydown = (event) => {
|
||||||
|
switch (event.key) {
|
||||||
|
case " ":
|
||||||
|
case "Enter":
|
||||||
|
event.preventDefault(); // Prevent the default action to avoid scrolling
|
||||||
|
if (document.activeElement === this.buttonTarget) {
|
||||||
|
this.toggleList();
|
||||||
|
} else {
|
||||||
|
this.selectOption(event);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "ArrowDown":
|
||||||
|
event.preventDefault(); // Prevent the default action to avoid scrolling
|
||||||
|
this.focusNextOption();
|
||||||
|
break;
|
||||||
|
case "ArrowUp":
|
||||||
|
event.preventDefault(); // Prevent the default action to avoid scrolling
|
||||||
|
this.focusPreviousOption();
|
||||||
|
break;
|
||||||
|
case "Escape":
|
||||||
|
this.close();
|
||||||
|
this.buttonTarget.focus(); // Bring focus back to the button
|
||||||
|
break;
|
||||||
|
case "Tab":
|
||||||
|
this.close();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
focusNextOption() {
|
||||||
|
this.focusOptionInDirection(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
focusPreviousOption() {
|
||||||
|
this.focusOptionInDirection(-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
focusOptionInDirection(direction) {
|
||||||
|
const currentFocusedIndex = this.optionTargets.findIndex(
|
||||||
|
(option) => option === document.activeElement
|
||||||
|
);
|
||||||
|
const optionsCount = this.optionTargets.length;
|
||||||
|
const nextIndex =
|
||||||
|
(currentFocusedIndex + direction + optionsCount) % optionsCount;
|
||||||
|
this.optionTargets[nextIndex].focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleList = () => {
|
||||||
|
this.show = !this.show;
|
||||||
|
this.listTarget.classList.toggle("hidden", !this.show);
|
||||||
|
this.buttonTarget.setAttribute("aria-expanded", this.show.toString());
|
||||||
|
|
||||||
|
if (this.show) {
|
||||||
|
// Focus the first option or the selected option when the list is shown
|
||||||
|
const selectedOption = this.optionTargets.find(
|
||||||
|
(option) => option.getAttribute("aria-selected") === "true"
|
||||||
|
);
|
||||||
|
(selectedOption || this.optionTargets[0]).focus();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
close() {
|
||||||
|
this.show = false;
|
||||||
|
this.listTarget.classList.add("hidden");
|
||||||
|
this.buttonTarget.setAttribute("aria-expanded", "false");
|
||||||
|
}
|
||||||
|
|
||||||
|
selectOption(event) {
|
||||||
|
const selectedOption =
|
||||||
|
event.type === "keydown" ? document.activeElement : event.currentTarget;
|
||||||
|
this.optionTargets.forEach((option) => {
|
||||||
|
option.setAttribute("aria-selected", "false");
|
||||||
|
option.setAttribute("tabindex", "-1");
|
||||||
|
option.classList.remove(...this.activeClasses);
|
||||||
|
});
|
||||||
|
selectedOption.classList.add(...this.activeClasses);
|
||||||
|
selectedOption.setAttribute("aria-selected", "true");
|
||||||
|
selectedOption.focus();
|
||||||
|
this.close(); // Close the list after selection
|
||||||
|
|
||||||
|
// Update the hidden input's value
|
||||||
|
const selectedValue = selectedOption.getAttribute("data-value");
|
||||||
|
this.inputTarget.value = selectedValue;
|
||||||
|
this.syncButtonTextWithInput();
|
||||||
|
|
||||||
|
// Auto-submit controller listens for this even to auto-submit
|
||||||
|
const inputEvent = new Event("input", {
|
||||||
|
bubbles: true,
|
||||||
|
cancelable: true,
|
||||||
|
});
|
||||||
|
this.inputTarget.dispatchEvent(inputEvent);
|
||||||
|
}
|
||||||
|
|
||||||
|
syncButtonTextWithInput() {
|
||||||
|
const matchingOption = this.optionTargets.find(
|
||||||
|
(option) => option.getAttribute("data-value") === this.inputTarget.value
|
||||||
|
);
|
||||||
|
if (matchingOption) {
|
||||||
|
this.buttonTextTarget.textContent = matchingOption.textContent.trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -24,16 +24,28 @@
|
||||||
<span class="<%= valuation_styles[:text_class] %>">(<%= lucide_icon(valuation_styles[:icon], class: "w-4 h-4 align-text-bottom inline") %> <%= valuation.trend.percent %>%)</span>
|
<span class="<%= valuation_styles[:text_class] %>">(<%= lucide_icon(valuation_styles[:icon], class: "w-4 h-4 align-text-bottom inline") %> <%= valuation.trend.percent %>%)</span>
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
<div class="relative w-[72px]" data-controller="dropdown">
|
<div class="relative w-[72px]" data-controller="menu">
|
||||||
<button data-action="click->dropdown#toggleMenu" class="ml-auto flex items-center justify-center hover:bg-gray-50 w-8 h-8 rounded-lg">
|
<button
|
||||||
<%= lucide_icon("more-horizontal", class: "w-5 h-5 text-gray-500" ) %>
|
data-menu-target="button"
|
||||||
|
class="ml-auto flex items-center justify-center hover:bg-gray-50 w-8 h-8 rounded-lg"
|
||||||
|
>
|
||||||
|
<%= lucide_icon("more-horizontal", class: "w-5 h-5 text-gray-500") %>
|
||||||
</button>
|
</button>
|
||||||
<div class="hidden absolute min-w-[200px] z-10 top-10 right-0 bg-white p-1 rounded-sm shadow-xs border border-alpha-black-25 w-fit" data-dropdown-target="menu">
|
<div
|
||||||
<%= link_to edit_valuation_path(valuation.original), class: "flex gap-1 items-center hover:bg-gray-50 rounded-md p-2" do %>
|
data-menu-target="content"
|
||||||
|
class="absolute min-w-[200px] z-10 top-10 right-0 bg-white p-1 rounded-sm shadow-xs border border-alpha-black-25 w-fit"
|
||||||
|
>
|
||||||
|
<%= link_to edit_valuation_path(valuation.original),
|
||||||
|
class: "flex gap-1 items-center hover:bg-gray-50 rounded-md p-2" do %>
|
||||||
<%= lucide_icon("pencil-line", class: "w-5 h-5 text-gray-500 shrink-0") %>
|
<%= lucide_icon("pencil-line", class: "w-5 h-5 text-gray-500 shrink-0") %>
|
||||||
<span class="text-gray-900 text-sm">Edit entry</span>
|
<span class="text-gray-900 text-sm">Edit entry</span>
|
||||||
<% end %>
|
<% end %>
|
||||||
<%= link_to valuation_path(valuation.original), data: { turbo_method: :delete, turbo_confirm: { title: t('custom_turbo_confirm.history.title'), body: t('custom_turbo_confirm.history.body_html'), accept: t('custom_turbo_confirm.history.accept') } }, class: "text-red-600 flex gap-1 items-center hover:bg-gray-50 rounded-md p-2" do %>
|
<%= link_to valuation_path(valuation.original),
|
||||||
|
data: { turbo_method: :delete,
|
||||||
|
turbo_confirm: { title: t('custom_turbo_confirm.history.title'),
|
||||||
|
body: t('custom_turbo_confirm.history.body_html'),
|
||||||
|
accept: t('custom_turbo_confirm.history.accept') } },
|
||||||
|
class: "text-red-600 flex gap-1 items-center hover:bg-gray-50 rounded-md p-2" do %>
|
||||||
<%= lucide_icon("trash-2", class: "w-5 h-5 shrink-0") %>
|
<%= lucide_icon("trash-2", class: "w-5 h-5 shrink-0") %>
|
||||||
<span class="text-sm">Delete entry</span>
|
<span class="text-sm">Delete entry</span>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
|
@ -17,13 +17,22 @@
|
||||||
<%= lucide_icon("chevron-down", class: "w-5 h-5 text-gray-500") %>
|
<%= lucide_icon("chevron-down", class: "w-5 h-5 text-gray-500") %>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="relative cursor-pointer" data-controller="dropdown">
|
<div class="relative cursor-pointer" data-controller="menu">
|
||||||
<div class="flex hover:bg-gray-100 p-2 rounded" data-action="click->dropdown#toggleMenu">
|
<button data-menu-target="button" class="flex hover:bg-gray-100 p-2 rounded">
|
||||||
<%= lucide_icon("more-horizontal", class: "w-5 h-5 text-gray-500") %>
|
<%= lucide_icon("more-horizontal", class: "w-5 h-5 text-gray-500") %>
|
||||||
</div>
|
</button>
|
||||||
<div class="absolute z-10 top-10 right-0 border border-alpha-black-25 bg-white rounded-lg shadow-xs hidden" data-dropdown-target="menu">
|
<div data-menu-target="content" class="absolute z-10 top-10 right-0 border border-alpha-black-25 bg-white rounded-lg shadow-xs hidden">
|
||||||
<div class="w-48 px-3 text-sm leading-6 text-gray-900 bg-white shadow-lg shrink rounded-xl ring-1 ring-gray-900/5">
|
<div class="w-48 px-3 text-sm leading-6 text-gray-900 bg-white shadow-lg shrink rounded-xl ring-1 ring-gray-900/5">
|
||||||
<%= button_to account_path(@account), method: :delete, class: "block w-full py-2 text-red-600 hover:text-red-800 flex items-center", data: { turbo_confirm: { title: t("custom_turbo_confirm.account_destroy.title"), body: t("custom_turbo_confirm.account_destroy.body_html"), accept: t("custom_turbo_confirm.account_destroy.accept", name: @account.name) } } do %>
|
<%= button_to account_path(@account),
|
||||||
|
method: :delete,
|
||||||
|
class: "block w-full py-2 text-red-600 hover:text-red-800 flex items-center",
|
||||||
|
data: {
|
||||||
|
turbo_confirm: {
|
||||||
|
title: t("custom_turbo_confirm.account_destroy.title"),
|
||||||
|
body: t("custom_turbo_confirm.account_destroy.body_html"),
|
||||||
|
accept: t("custom_turbo_confirm.account_destroy.accept", name: @account.name)
|
||||||
|
}
|
||||||
|
} do %>
|
||||||
<%= lucide_icon("trash-2", class: "w-5 h-5 mr-2") %> Delete account
|
<%= lucide_icon("trash-2", class: "w-5 h-5 mr-2") %> Delete account
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
|
@ -45,7 +54,9 @@
|
||||||
}
|
}
|
||||||
%>
|
%>
|
||||||
</div>
|
</div>
|
||||||
<%= render partial: "shared/period_dropdown", locals: { period: @period, path: account_path(@account) } %>
|
<%= form_with url: account_path(@account), method: :get, class: "flex items-center gap-4", data: { controller: "auto-submit-form" } do %>
|
||||||
|
<%= render partial: "shared/period_select", locals: { value: @period.name } %>
|
||||||
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
<div class="h-96 flex items-center justify-center text-2xl font-bold">
|
<div class="h-96 flex items-center justify-center text-2xl font-bold">
|
||||||
<%= render partial: "shared/line_chart", locals: { series: @balance_series } %>
|
<%= render partial: "shared/line_chart", locals: { series: @balance_series } %>
|
||||||
|
|
|
@ -29,17 +29,24 @@
|
||||||
<div class="flex-col p-5 min-w-80">
|
<div class="flex-col p-5 min-w-80">
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<%= link_to root_path do %>
|
<%= link_to root_path do %>
|
||||||
<%= image_tag 'logo.svg', alt: 'Maybe' %>
|
<%= image_tag 'logo.svg', alt: 'Maybe', class: "h-[22px]" %>
|
||||||
<% end %>
|
<% end %>
|
||||||
<div class="relative cursor-pointer" data-controller="dropdown">
|
<div class="relative" data-controller="menu">
|
||||||
<div class="flex" data-action="click->dropdown#toggleMenu">
|
<button data-menu-target="button">
|
||||||
<div class="mr-1.5 text-white w-8 h-8 bg-gray-400 rounded-full flex items-center justify-center text-lg uppercase"><%= Current.user.email.first %></div>
|
<div class="text-white w-9 h-9 bg-gray-400 rounded-full flex items-center justify-center text-lg uppercase"><%= Current.user.email.first %></div>
|
||||||
</div>
|
</button>
|
||||||
<div class="absolute z-10 hidden w-screen px-2 mt-2 -translate-x-1/2 left-1/2 max-w-min" data-dropdown-target="menu">
|
<div
|
||||||
<div class="w-48 px-3 text-sm font-semibold leading-6 text-gray-900 bg-white shadow-lg shrink rounded-xl ring-1 ring-gray-900/5">
|
data-menu-target="content"
|
||||||
<%= link_to "Settings", edit_settings_path, class: 'block p-2 hover:text-gray-600' %>
|
class="absolute min-w-[200px] z-10 top-10 right-0 bg-white p-1 rounded-sm shadow-xs border border-alpha-black-25 w-fit"
|
||||||
<%= button_to "Log Out", session_path, method: :delete, class: 'block p-2 hover:text-gray-600' %>
|
>
|
||||||
</div>
|
<%= link_to edit_settings_path, class: "flex gap-1 items-center hover:bg-gray-50 rounded-md p-2" do %>
|
||||||
|
<%= lucide_icon("pencil-line", class: "w-5 h-5 text-gray-500 shrink-0") %>
|
||||||
|
<span class="text-gray-900 text-sm">Settings</span>
|
||||||
|
<% end %>
|
||||||
|
<%= button_to session_path, method: :delete, class: "w-full text-gray-900 flex gap-1 items-center hover:bg-gray-50 rounded-md p-2" do %>
|
||||||
|
<%= lucide_icon("log-out", class: "w-5 h-5 shrink-0") %>
|
||||||
|
<span class="text-sm">Logout</span>
|
||||||
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -79,7 +86,6 @@
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
<%= turbo_frame_tag "modal" %>
|
<%= turbo_frame_tag "modal" %>
|
||||||
|
|
||||||
<%= render 'shared/custom_confirm_modal' %>
|
<%= render 'shared/custom_confirm_modal' %>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -15,7 +15,9 @@
|
||||||
}
|
}
|
||||||
%>
|
%>
|
||||||
</div>
|
</div>
|
||||||
<%= render partial: "shared/period_dropdown", locals: { period: @period, path: root_path } %>
|
<%= form_with url: root_path, method: :get, class: "flex items-center gap-4", data: { controller: "auto-submit-form" } do %>
|
||||||
|
<%= render partial: "shared/period_select", locals: { value: @period.name } %>
|
||||||
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
<div class="h-96 flex items-center justify-center text-2xl font-bold">
|
<div class="h-96 flex items-center justify-center text-2xl font-bold">
|
||||||
<%= render partial: "shared/line_chart", locals: { series: @net_worth_series } %>
|
<%= render partial: "shared/line_chart", locals: { series: @net_worth_series } %>
|
||||||
|
@ -70,7 +72,9 @@
|
||||||
<%= lucide_icon("plus", class: "w-5 h-5 text-gray-500") %>
|
<%= lucide_icon("plus", class: "w-5 h-5 text-gray-500") %>
|
||||||
<p><%= t('.new') %></p>
|
<p><%= t('.new') %></p>
|
||||||
<% end %>
|
<% end %>
|
||||||
<%= render partial: "shared/period_dropdown", locals: { period: @period, path: root_path } %>
|
<%= form_with url: root_path, method: :get, class: "flex items-center gap-4", data: { controller: "auto-submit-form" } do %>
|
||||||
|
<%= render partial: "shared/period_select", locals: { value: @period.name } %>
|
||||||
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
|
|
|
@ -1,15 +0,0 @@
|
||||||
<div data-controller="currency-dropdown" class="absolute right-1 bottom-[10px] flex items-end">
|
|
||||||
<button type="button" class="flex items-center justify-between w-20 px-2 py-1 text-sm rounded-lg hover:bg-gray-100 focus:bg-gray-100" data-action="click->currency-dropdown#toggleMenu">
|
|
||||||
<div data-currency-dropdown-target="label"><%= f.object.currency %></div>
|
|
||||||
<%# Example of how account currency value is updated %>
|
|
||||||
<%= f.hidden_field :currency, data: {currency_dropdown_target: "input"} %>
|
|
||||||
<%= lucide_icon("chevron-down", class: "text-gray-500 w-5 h-5" ) %>
|
|
||||||
</button>
|
|
||||||
<ul data-currency-dropdown-target="menu" class="hidden fixed p-1 bg-white rounded-[10px] min-w-[112px] z-50 translate-y-2 border border-alpha-black-100 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.1)]">
|
|
||||||
<% options.each do |option| %>
|
|
||||||
<li data-action="click->currency-dropdown#selectOption" data-currency-dropdown-target="option" data-value="<%= option %>" class="flex justify-between items-center p-2 text-sm text-gray-700 hover:bg-gray-100 cursor-pointer rounded-lg <%= "bg-gray-100" if option === f.object.currency %>"><%= option %>
|
|
||||||
<%= inline_svg_tag('icn-check.svg', class: "text-gray-500 fill-current #{'hidden'if option != f.object.currency}") %>
|
|
||||||
</li>
|
|
||||||
<% end %>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
|
@ -1,4 +0,0 @@
|
||||||
<%# locals: (path:, period:) -%>
|
|
||||||
<%= form_with url: path, method: :get, class: "flex items-center gap-4", html: { class: "" } do |f| %>
|
|
||||||
<%= f.select :period, options_for_select([['7D', 'last_7_days'], ['1M', 'last_30_days'], ["1Y", "last_365_days"], ['All', 'all']], selected: params[:period]), {}, { class: "block w-full border border-alpha-black-100 shadow-xs rounded-lg text-sm py-2 pr-8 pl-2 cursor-pointer", onchange: "this.form.submit();" } %>
|
|
||||||
<% end %>
|
|
16
app/views/shared/_period_select.html.erb
Normal file
16
app/views/shared/_period_select.html.erb
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
<%# locals: (value: 'all') -%>
|
||||||
|
<% options = [['7D', 'last_7_days'], ['1M', 'last_30_days'], ["1Y", "last_365_days"], ['All', 'all']] %>
|
||||||
|
<div data-controller="select" data-select-active-class="bg-alpha-black-50" class="relative">
|
||||||
|
<button type="button" data-select-target="button" class="flex items-center gap-1 w-full border border-alpha-black-100 shadow-xs rounded-lg text-sm p-2 cursor-pointer">
|
||||||
|
<span data-select-target="buttonText" class="text-gray-900 text-sm"><%= options.find { |option| option[1] == value }[0] %></span>
|
||||||
|
<%= lucide_icon("chevron-down", class: "w-5 h-5 text-gray-500") %>
|
||||||
|
</button>
|
||||||
|
<input type="hidden" name="period" value="<%= value %>" data-select-target="input" data-auto-submit-form-target="auto">
|
||||||
|
<ul data-select-target="list" class="hidden absolute z-10 top-10 right-0 border border-alpha-black-25 bg-white rounded-lg shadow-xs">
|
||||||
|
<% options.each do |label, value| %>
|
||||||
|
<li tabindex="0" data-select-target="option" data-action="click->select#selectOption" data-value="<%= value %>" class="text-sm text-gray-900 rounded-lg cursor-pointer hover:bg-alpha-black-50 px-5 py-1">
|
||||||
|
<%= label %>
|
||||||
|
</li>
|
||||||
|
<% end %>
|
||||||
|
</ul>
|
||||||
|
</div>
|
|
@ -1,14 +1,14 @@
|
||||||
<%# locals: (transaction:) %>
|
<%# locals: (transaction:) %>
|
||||||
<div class="relative" data-controller="dropdown" data-dropdown-close-on-click-value="false">
|
<div class="relative" data-controller="menu">
|
||||||
<div class="flex cursor-pointer" data-action="click->dropdown#toggleMenu">
|
<button data-menu-target="button" class="flex">
|
||||||
<%= render partial: "shared/category_badge", locals: transaction.category.nil? ? {} : { name: transaction.category.name, color: transaction.category.color } %>
|
<%= render partial: "shared/category_badge", locals: transaction.category.nil? ? {} : { name: transaction.category.name, color: transaction.category.color } %>
|
||||||
</div>
|
</button>
|
||||||
<div class="absolute z-10 hidden w-screen mt-2 max-w-min cursor-default" data-dropdown-target="menu">
|
<div data-menu-target="content" class="absolute z-10 hidden w-screen mt-2 max-w-min cursor-default">
|
||||||
<div class="w-64 text-sm font-semibold leading-6 text-gray-900 bg-white shadow-lg shrink rounded-xl ring-1 ring-gray-900/5">
|
<div class="w-64 text-sm font-semibold leading-6 text-gray-900 bg-white shadow-lg shrink rounded-xl ring-1 ring-gray-900/5">
|
||||||
<div class="flex flex-col" data-controller="list-filter">
|
<div class="flex flex-col" data-controller="list-filter">
|
||||||
<div class="grow p-1.5">
|
<div class="grow p-1.5">
|
||||||
<div class="relative flex items-center bg-white border border-gray-200 rounded-lg">
|
<div class="relative flex items-center bg-white border border-gray-200 rounded-lg">
|
||||||
<input placeholder="Search" class="placeholder:text-sm placeholder:text-gray-500 font-normal h-10 relative pl-10 w-full border-none rounded-lg" data-list-filter-target="input" data-action="list-filter#filter" />
|
<input placeholder="Search" type="search" class="placeholder:text-sm placeholder:text-gray-500 font-normal h-10 relative pl-10 w-full border-none rounded-lg" data-list-filter-target="input" data-action="list-filter#filter" />
|
||||||
<%= lucide_icon("search", class: "w-5 h-5 text-gray-500 ml-2 absolute inset-0 transform top-1/2 -translate-y-1/2") %>
|
<%= lucide_icon("search", class: "w-5 h-5 text-gray-500 ml-2 absolute inset-0 transform top-1/2 -translate-y-1/2") %>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -6,12 +6,12 @@
|
||||||
<div class="grow">
|
<div class="grow">
|
||||||
<%= render partial: "transactions/search_form/search_filter", locals: { form: form } %>
|
<%= render partial: "transactions/search_form/search_filter", locals: { form: form } %>
|
||||||
</div>
|
</div>
|
||||||
<div data-controller="dropdown" data-dropdown-close-on-click-value="false" class="relative">
|
<div data-controller="menu" class="relative">
|
||||||
<button type="button" data-action="dropdown#toggleMenu" class="border border-gray-200 block h-full rounded-lg flex items-center gap-2 px-4">
|
<button data-menu-target="button" type="button" class="border border-gray-200 block h-full rounded-lg flex items-center gap-2 px-4">
|
||||||
<%= lucide_icon("list-filter", class: "w-5 h-5 text-gray-500") %>
|
<%= lucide_icon("list-filter", class: "w-5 h-5 text-gray-500") %>
|
||||||
<p class="text-sm font-medium text-gray-900">Filter</p>
|
<p class="text-sm font-medium text-gray-900">Filter</p>
|
||||||
</button>
|
</button>
|
||||||
<div class="hidden absolute z-10 top-12 right-0 border border-alpha-black-25 bg-white rounded-lg shadow-xs min-w-[450px]" data-dropdown-target="menu">
|
<div data-menu-target="content" class="absolute z-10 top-12 right-0 border border-alpha-black-25 bg-white rounded-lg shadow-xs min-w-[450px]">
|
||||||
<div data-controller="tabs" data-tabs-active-class="border-b-2 border-b-black text-gray-900" data-tabs-default-tab-value="txn-account-filter">
|
<div data-controller="tabs" data-tabs-active-class="border-b-2 border-b-black text-gray-900" data-tabs-default-tab-value="txn-account-filter">
|
||||||
<div class="flex items-center px-3 text-sm font-medium text-gray-500 gap-4 border-b border-b-alpha-black-50">
|
<div class="flex items-center px-3 text-sm font-medium text-gray-500 gap-4 border-b border-b-alpha-black-50">
|
||||||
<button class="py-2 border-b-2" type="button" data-id="txn-account-filter" data-tabs-target="btn" data-action="tabs#select">Account</button>
|
<button class="py-2 border-b-2" type="button" data-id="txn-account-filter" data-tabs-target="btn" data-action="tabs#select">Account</button>
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue