mirror of
https://github.com/maybe-finance/maybe.git
synced 2025-08-09 23:45:21 +02:00
Basic tabs component
This commit is contained in:
parent
8aa6cb37d8
commit
f6016e47e2
9 changed files with 216 additions and 120 deletions
12
app/components/tab_component.rb
Normal file
12
app/components/tab_component.rb
Normal file
|
@ -0,0 +1,12 @@
|
|||
class TabComponent < ViewComponent::Base
|
||||
attr_reader :id, :label
|
||||
|
||||
def initialize(id:, label:)
|
||||
@id = id
|
||||
@label = label
|
||||
end
|
||||
|
||||
def call
|
||||
content
|
||||
end
|
||||
end
|
22
app/components/tabs_component.html.erb
Normal file
22
app/components/tabs_component.html.erb
Normal file
|
@ -0,0 +1,22 @@
|
|||
<%= tag.div class: "space-y-4", data: {
|
||||
controller: "tabs",
|
||||
tabs_url_param_key_value: url_param_key,
|
||||
tabs_nav_btn_active_class: nav_btn_active_classes,
|
||||
tabs_nav_btn_inactive_class: nav_btn_inactive_classes
|
||||
} do %>
|
||||
<nav class="flex bg-surface-inset p-1 rounded-lg">
|
||||
<% tabs.each do |tab| %>
|
||||
<%= tag.button class: nav_btn_classes(active: tab.id == active_tab), data: { id: tab.id, action: "tabs#show", tabs_target: "navBtn" } do %>
|
||||
<%= tab.label %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</nav>
|
||||
|
||||
<div>
|
||||
<% tabs.each do |tab| %>
|
||||
<%= tag.div class: class_names("hidden" => tab.id != active_tab), data: { id: tab.id, tabs_target: "panel" } do %>
|
||||
<%= tab %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
38
app/components/tabs_component.rb
Normal file
38
app/components/tabs_component.rb
Normal file
|
@ -0,0 +1,38 @@
|
|||
class TabsComponent < ViewComponent::Base
|
||||
renders_many :tabs, TabComponent
|
||||
|
||||
VARIANTS = {
|
||||
default: {
|
||||
nav_btn_active_classes: "bg-white theme-dark:bg-gray-700 text-primary shadow-sm",
|
||||
nav_btn_inactive_classes: "text-secondary hover:bg-surface-inset-hover",
|
||||
nav_btn_classes: "w-full inline-flex justify-center items-center text-sm font-medium px-2 py-1 rounded-md transition-colors duration-200"
|
||||
}
|
||||
}
|
||||
|
||||
attr_reader :variant, :url_param_key
|
||||
|
||||
def initialize(active_tab: nil, variant: "default", url_param_key: nil)
|
||||
@active_tab = active_tab
|
||||
@variant = variant.to_sym
|
||||
@url_param_key = url_param_key
|
||||
end
|
||||
|
||||
def active_tab
|
||||
@active_tab || tabs.first.id
|
||||
end
|
||||
|
||||
def nav_btn_active_classes
|
||||
VARIANTS.dig(variant, :nav_btn_active_classes)
|
||||
end
|
||||
|
||||
def nav_btn_inactive_classes
|
||||
VARIANTS.dig(variant, :nav_btn_inactive_classes)
|
||||
end
|
||||
|
||||
def nav_btn_classes(active: false)
|
||||
class_names(
|
||||
VARIANTS.dig(variant, :nav_btn_classes),
|
||||
active ? nav_btn_active_classes : nav_btn_inactive_classes
|
||||
)
|
||||
end
|
||||
end
|
|
@ -2,9 +2,13 @@ import { Controller } from "@hotwired/stimulus";
|
|||
|
||||
// Connects to data-controller="tabs"
|
||||
export default class extends Controller {
|
||||
static classes = ["active", "inactive"];
|
||||
static targets = ["btn", "tab"];
|
||||
static values = { defaultTab: String, localStorageKey: String };
|
||||
static classes = ["active", "inactive", "navBtnActive", "navBtnInactive"];
|
||||
static targets = ["btn", "tab", "panel", "navBtn"];
|
||||
static values = {
|
||||
defaultTab: String,
|
||||
localStorageKey: String,
|
||||
urlParamKey: String,
|
||||
};
|
||||
|
||||
connect() {
|
||||
const selectedTab = this.hasLocalStorageKeyValue
|
||||
|
@ -30,6 +34,35 @@ export default class extends Controller {
|
|||
}
|
||||
}
|
||||
|
||||
show(e) {
|
||||
const selectedTabId = e.target.dataset.id;
|
||||
|
||||
this.navBtnTargets.forEach((navBtn) => {
|
||||
if (navBtn.dataset.id === selectedTabId) {
|
||||
navBtn.classList.add(...this.navBtnActiveClasses);
|
||||
navBtn.classList.remove(...this.navBtnInactiveClasses);
|
||||
} else {
|
||||
navBtn.classList.add(...this.navBtnInactiveClasses);
|
||||
navBtn.classList.remove(...this.navBtnActiveClasses);
|
||||
}
|
||||
});
|
||||
|
||||
this.panelTargets.forEach((panel) => {
|
||||
if (panel.dataset.id === selectedTabId) {
|
||||
panel.classList.remove("hidden");
|
||||
} else {
|
||||
panel.classList.add("hidden");
|
||||
}
|
||||
});
|
||||
|
||||
// Update URL with the selected tab
|
||||
if (this.urlParamKeyValue) {
|
||||
const url = new URL(window.location.href);
|
||||
url.searchParams.set(this.urlParamKeyValue, selectedTabId);
|
||||
window.history.replaceState({}, "", url);
|
||||
}
|
||||
}
|
||||
|
||||
onTurboLoad = () => {
|
||||
const selectedTab = this.hasLocalStorageKeyValue
|
||||
? this.getStoredTab() || this.defaultTabValue
|
||||
|
|
|
@ -1,48 +1,29 @@
|
|||
<%# locals: (family:) %>
|
||||
<%# locals: (family:, active_account_group_tab:) %>
|
||||
|
||||
<% if family.requires_data_provider? && Provider::Registry.get_provider(:synth).nil? %>
|
||||
<details class="group bg-yellow-tint-10 rounded-lg p-2 text-yellow-600 mb-3 text-xs">
|
||||
<summary class="flex items-center justify-between gap-2">
|
||||
<div class="flex items-center gap-2">
|
||||
<%= icon "triangle-alert", size: "sm" %>
|
||||
<p class="font-medium">Missing historical data</p>
|
||||
<div>
|
||||
<% if family.requires_data_provider? && Provider::Registry.get_provider(:synth).nil? %>
|
||||
<details class="group bg-yellow-tint-10 rounded-lg p-2 text-yellow-600 mb-3 text-xs">
|
||||
<summary class="flex items-center justify-between gap-2">
|
||||
<div class="flex items-center gap-2">
|
||||
<%= icon "triangle-alert", size: "sm" %>
|
||||
<p class="font-medium">Missing historical data</p>
|
||||
</div>
|
||||
|
||||
<%= lucide_icon "chevron-down", class: "text-yellow-600 group-open:transform group-open:rotate-180 w-5" %>
|
||||
</summary>
|
||||
<div class="text-xs py-2 space-y-2">
|
||||
<p>Maybe uses Synth API to fetch historical exchange rates, security prices, and more. This data is required to calculate accurate historical account balances.</p>
|
||||
|
||||
<p>
|
||||
<%= link_to "Add your Synth API key here.", settings_hosting_path, class: "text-yellow-600 underline" %>
|
||||
</p>
|
||||
</div>
|
||||
</details>
|
||||
<% end %>
|
||||
|
||||
<%= lucide_icon "chevron-down", class: "text-yellow-600 group-open:transform group-open:rotate-180 w-5" %>
|
||||
</summary>
|
||||
<div class="text-xs py-2 space-y-2">
|
||||
<p>Maybe uses Synth API to fetch historical exchange rates, security prices, and more. This data is required to calculate accurate historical account balances.</p>
|
||||
|
||||
<p>
|
||||
<%= link_to "Add your Synth API key here.", settings_hosting_path, class: "text-yellow-600 underline" %>
|
||||
</p>
|
||||
</div>
|
||||
</details>
|
||||
<% end %>
|
||||
|
||||
<div
|
||||
class="space-y-3"
|
||||
data-controller="tabs"
|
||||
data-tabs-local-storage-key-value="account-sidebar-tabs"
|
||||
data-tabs-active-class="bg-surface shadow-sm text-primary"
|
||||
data-tabs-inactive-class="text-secondary"
|
||||
data-tabs-default-tab-value="assets-tab">
|
||||
<div class="bg-surface-inset rounded-lg p-1 flex">
|
||||
<button type="button" data-id="assets-tab" class="w-1/3 px-2 py-1 rounded-md text-sm text-secondary font-medium" data-tabs-target="btn" data-action="click->tabs#select">
|
||||
Assets
|
||||
</button>
|
||||
|
||||
<button type="button" data-id="debts-tab" class="w-1/3 px-2 py-1 rounded-md text-secondary text-sm font-medium" data-tabs-target="btn" data-action="click->tabs#select">
|
||||
Debts
|
||||
</button>
|
||||
|
||||
<button type="button" data-id="all-tab" class="w-1/3 px-2 py-1 rounded-md text-secondary text-sm font-medium" data-tabs-target="btn" data-action="click->tabs#select">
|
||||
All
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div data-tabs-target="tab" id="assets-tab">
|
||||
<%= render LinkComponent.new(
|
||||
<%= render TabsComponent.new(active_tab: active_account_group_tab, url_param_key: "account_group_tab") do |container| %>
|
||||
<% container.with_tab(id: "assets", label: "Assets") do %>
|
||||
<%= render LinkComponent.new(
|
||||
text: "New asset",
|
||||
variant: "ghost",
|
||||
href: new_account_path(step: "method_select", classification: "asset"),
|
||||
|
@ -52,15 +33,15 @@
|
|||
class: "justify-start"
|
||||
) %>
|
||||
|
||||
<div class="space-y-2">
|
||||
<% family.balance_sheet.account_groups("asset").each do |group| %>
|
||||
<%= render "accounts/accountable_group", account_group: group %>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<% family.balance_sheet.account_groups("asset").each do |group| %>
|
||||
<%= render "accounts/accountable_group", account_group: group %>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<div data-tabs-target="tab" id="debts-tab" class="hidden">
|
||||
<%= render LinkComponent.new(
|
||||
<% container.with_tab(id: "debts", label: "Debts") do %>
|
||||
<%= render LinkComponent.new(
|
||||
text: "New debt",
|
||||
variant: "ghost",
|
||||
href: new_account_path(step: "method_select", classification: "liability"),
|
||||
|
@ -70,15 +51,15 @@
|
|||
class: "justify-start"
|
||||
) %>
|
||||
|
||||
<div class="space-y-2">
|
||||
<% family.balance_sheet.account_groups("liability").each do |group| %>
|
||||
<%= render "accounts/accountable_group", account_group: group %>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<% family.balance_sheet.account_groups("liability").each do |group| %>
|
||||
<%= render "accounts/accountable_group", account_group: group %>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<div data-tabs-target="tab" id="all-tab" class="hidden">
|
||||
<%= render LinkComponent.new(
|
||||
<% container.with_tab(id: "all", label: "All") do %>
|
||||
<%= render LinkComponent.new(
|
||||
text: "New account",
|
||||
variant: "ghost",
|
||||
full_width: true,
|
||||
|
@ -88,10 +69,11 @@
|
|||
class: "justify-start"
|
||||
) %>
|
||||
|
||||
<div class="space-y-2">
|
||||
<% family.balance_sheet.account_groups.each do |group| %>
|
||||
<%= render "accounts/accountable_group", account_group: group %>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<% family.balance_sheet.account_groups.each do |group| %>
|
||||
<%= render "accounts/accountable_group", account_group: group %>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</div>
|
|
@ -11,65 +11,60 @@
|
|||
<p class="text-secondary text-sm"><%= t(".description") %></p>
|
||||
</div>
|
||||
|
||||
<div
|
||||
data-controller="tabs"
|
||||
data-tabs-active-class="bg-surface shadow-sm text-primary"
|
||||
data-tabs-inactive-class="text-secondary"
|
||||
data-tabs-default-tab-value="csv-upload-tab">
|
||||
<div class="flex justify-center mb-4 w-full">
|
||||
<div class="bg-surface-inset rounded-lg p-1 flex w-full">
|
||||
<button type="button" data-id="csv-upload-tab" class="w-1/2 px-2 py-1 rounded-md text-secondary text-sm font-medium" data-tabs-target="btn" data-action="click->tabs#select">Upload CSV</button>
|
||||
<button type="button" data-id="csv-paste-tab" class="w-1/2 px-2 py-1 rounded-md text-sm text-secondary font-medium" data-tabs-target="btn" data-action="click->tabs#select">Copy & Paste</button>
|
||||
</div>
|
||||
</div>
|
||||
<%= render TabsComponent.new(active_tab: "csv-upload", url_param_key: "tab") do |container| %>
|
||||
<% container.with_tab(id: "csv-upload", label: "Upload CSV") do %>
|
||||
<%= styled_form_with model: @import, scope: :import, url: import_upload_path(@import), multipart: true, class: "space-y-2" do |form| %>
|
||||
<%= form.select :col_sep, Import::SEPARATORS, label: true %>
|
||||
|
||||
<% ["csv-paste-tab", "csv-upload-tab"].each do |tab| %>
|
||||
<%= tag.div id: tab, data: { tabs_target: "tab" }, class: tab == "csv-upload-tab" ? "hidden" : "" do %>
|
||||
<%= styled_form_with model: @import, scope: :import, url: import_upload_path(@import), multipart: true, class: "space-y-2" do |form| %>
|
||||
<%= form.select :col_sep, Import::SEPARATORS, label: true %>
|
||||
<% if @import.type == "TransactionImport" || @import.type == "TradeImport" %>
|
||||
<%= form.select :account_id, @import.family.accounts.pluck(:name, :id), { label: "Account (optional)", include_blank: "Multi-account import", selected: @import.account_id } %>
|
||||
<% end %>
|
||||
|
||||
<% if @import.type == "TransactionImport" || @import.type == "TradeImport" %>
|
||||
<%= form.select :account_id, @import.family.accounts.pluck(:name, :id), { label: "Account (optional)", include_blank: "Multi-account import", selected: @import.account_id } %>
|
||||
<% end %>
|
||||
<div class="flex flex-col items-center justify-center w-full h-64 border border-secondary border-dashed rounded-xl cursor-pointer" data-controller="file-upload" data-action="click->file-upload#triggerFileInput" data-file-upload-target="uploadArea">
|
||||
<div class="flex flex-col items-center justify-center pt-5 pb-6">
|
||||
<div data-file-upload-target="uploadText" class="flex flex-col items-center">
|
||||
<%= lucide_icon("plus", class: "w-6 h-6 mb-4 text-secondary mx-auto") %>
|
||||
<p class="mb-2 text-md text-gray text-center">
|
||||
<span class="font-medium text-primary">Browse</span> to add your CSV file here
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<% if tab == "csv-paste-tab" %>
|
||||
<%= form.text_area :raw_file_str,
|
||||
<div class="flex flex-col items-center hidden" data-file-upload-target="fileName">
|
||||
<%= lucide_icon("file-text", class: "w-6 h-6 mb-4 text-primary") %>
|
||||
<p class="mb-2 text-md font-medium text-primary"></p>
|
||||
</div>
|
||||
|
||||
<%= form.file_field :csv_file, class: "hidden", "data-auto-submit-form-target": "auto", "data-file-upload-target": "input" %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<%= form.submit "Upload CSV", disabled: @import.complete? %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
<% container.with_tab(id: "csv-paste", label: "Copy & Paste") do %>
|
||||
<%= styled_form_with model: @import, scope: :import, url: import_upload_path(@import), multipart: true, class: "space-y-2" do |form| %>
|
||||
<%= form.select :col_sep, Import::SEPARATORS, label: true %>
|
||||
|
||||
<% if @import.type == "TransactionImport" || @import.type == "TradeImport" %>
|
||||
<%= form.select :account_id, @import.family.accounts.pluck(:name, :id), { label: "Account (optional)", include_blank: "Multi-account import", selected: @import.account_id } %>
|
||||
<% end %>
|
||||
|
||||
<%= form.text_area :raw_file_str,
|
||||
rows: 10,
|
||||
required: true,
|
||||
placeholder: "Paste your CSV file contents here",
|
||||
"data-auto-submit-form-target": "auto" %>
|
||||
<% else %>
|
||||
<div class="flex flex-col items-center justify-center w-full h-64 border border-secondary border-dashed rounded-xl cursor-pointer" data-controller="file-upload" data-action="click->file-upload#triggerFileInput" data-file-upload-target="uploadArea">
|
||||
<div class="flex flex-col items-center justify-center pt-5 pb-6">
|
||||
<div data-file-upload-target="uploadText" class="flex flex-col items-center">
|
||||
<%= lucide_icon("plus", class: "w-6 h-6 mb-4 text-secondary mx-auto") %>
|
||||
<p class="mb-2 text-md text-gray text-center">
|
||||
<span class="font-medium text-primary">Browse</span> to add your CSV file here
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col items-center hidden" data-file-upload-target="fileName">
|
||||
<%= lucide_icon("file-text", class: "w-6 h-6 mb-4 text-primary") %>
|
||||
<p class="mb-2 text-md font-medium text-primary"></p>
|
||||
</div>
|
||||
|
||||
<%= form.file_field :csv_file, class: "hidden", "data-auto-submit-form-target": "auto", "data-file-upload-target": "input" %>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<%= form.submit "Upload CSV", disabled: @import.complete? %>
|
||||
<% end %>
|
||||
<%= form.submit "Upload CSV", disabled: @import.complete? %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-center">
|
||||
|
||||
<span class="text-secondary text-sm">
|
||||
<%= link_to "Download a sample CSV", "/imports/#{@import.id}/upload/sample_csv", class: "text-primary underline", data: { turbo: false } %> to see the required CSV format
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-center">
|
||||
<span class="text-secondary text-sm">
|
||||
<%= link_to "Download a sample CSV", "/imports/#{@import.id}/upload/sample_csv", class: "text-primary underline", data: { turbo: false } %> to see the required CSV format
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -23,7 +23,7 @@
|
|||
<%= icon("x", color: "gray") %>
|
||||
</button>
|
||||
</div>
|
||||
<%= render "accounts/account_sidebar_tabs", family: Current.family %>
|
||||
<%= render "accounts/account_sidebar_tabs", family: Current.family, active_account_group_tab: params[:account_group_tab] || "assets" %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
|
@ -67,7 +67,7 @@
|
|||
<%= yield :sidebar %>
|
||||
<% else %>
|
||||
<div id="account-sidebar-tabs" data-turbo-permanent>
|
||||
<%= render "accounts/account_sidebar_tabs", family: Current.family %>
|
||||
<%= render "accounts/account_sidebar_tabs", family: Current.family, active_account_group_tab: params[:account_group_tab] || "assets" %>
|
||||
</div>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
<%= javascript_include_tag "application", "data-turbo-track": "reload", defer: true %>
|
||||
<%= javascript_importmap_tags %>
|
||||
</head>
|
||||
<body class="p-4 bg-container">
|
||||
<body class="p-4 bg-container <%= params.dig(:lookbook, :display, :container_classes) %>">
|
||||
<%= yield %>
|
||||
</body>
|
||||
</html>
|
||||
|
|
14
test/components/previews/tabs_component_preview.rb
Normal file
14
test/components/previews/tabs_component_preview.rb
Normal file
|
@ -0,0 +1,14 @@
|
|||
class TabsComponentPreview < ViewComponent::Preview
|
||||
# @display container_classes max-w-[400px]
|
||||
def default
|
||||
render TabsComponent.new(active_tab: "tab1") do |container|
|
||||
container.with_tab(id: "tab1", label: "Tab 1") do
|
||||
content_tag(:p, "Content for tab 1")
|
||||
end
|
||||
|
||||
container.with_tab(id: "tab2", label: "Tab 2") do
|
||||
content_tag(:p, "Content for tab 2")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
Loading…
Add table
Add a link
Reference in a new issue