1
0
Fork 0
mirror of https://github.com/maybe-finance/maybe.git synced 2025-08-05 05:25:24 +02:00

New Design System + Codebase Refresh (#1823)

Since the very first 0.1.0-alpha.1 release, we've been moving quickly to add new features to the Maybe app. In doing so, some parts of the codebase have become outdated, unnecessary, or overly-complex as a natural result of this feature prioritization.

Now that "core" Maybe is complete, we're moving into a second phase of development where we'll be working hard to improve the accuracy of existing features and build additional features on top of "core". This PR is a quick overhaul of the existing codebase aimed to:

- Establish the brand new and simplified dashboard view (pictured above)
- Establish and move towards the conventions introduced in Cursor rules and project design overview #1788
- Consolidate layouts and improve the performance of layout queries
- Organize the core models of the Maybe domain (i.e. Account::Entry, Account::Transaction, etc.) and break out specific traits of each model into dedicated concerns for better readability
- Remove stale / dead code from codebase
- Remove overly complex code paths in favor of simpler ones
This commit is contained in:
Zach Gollwitzer 2025-02-21 11:57:59 -05:00 committed by GitHub
parent 8539ac7dec
commit d75be2282b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
278 changed files with 3428 additions and 4354 deletions

View file

@ -1,5 +1,4 @@
<%# locals: (entry:, selectable: true, balance_trend: nil) %>
<%# locals: (entry:, balance_trend: nil, view_ctx: "global") %>
<%= turbo_frame_tag dom_id(entry) do %>
<%= render partial: entry.entryable.to_partial_path, locals: { entry:, selectable:, balance_trend: } %>
<% end %>
<%= render partial: entry.entryable.to_partial_path,
locals: { entry: entry, balance_trend: balance_trend, view_ctx: view_ctx } %>

View file

@ -1,13 +1,12 @@
<%# locals: (date:, entries:, content:, selectable:, totals: false) %>
<%# locals: (date:, entries:, content:, totals: false) %>
<div id="entry-group-<%= date %>" class="bg-gray-25 rounded-xl p-1 w-full" data-bulk-select-target="group">
<div class="py-2 px-4 flex items-center justify-between font-medium text-xs text-secondary">
<div class="flex pl-0.5 items-center gap-4">
<% if selectable %>
<%= check_box_tag "#{date}_entries_selection",
class: ["checkbox checkbox--light", "hidden": entries.size == 0],
id: "selection_entry_#{date}",
data: { action: "bulk-select#toggleGroupSelection" } %>
<% end %>
<p class="uppercase space-x-1.5">
<%= tag.span I18n.l(date, format: :long) %>
@ -22,7 +21,7 @@
</div>
<% end %>
</div>
<div class="bg-white shadow-xs rounded-md border border-alpha-black-25 divide-y divide-alpha-black-50">
<div class="bg-white shadow-border-xs rounded-md divide-y divide-alpha-black-50">
<%= content %>
</div>
</div>

View file

@ -1,4 +1,4 @@
<div class="bg-white space-y-4 p-5 border border-alpha-black-25 rounded-xl shadow-xs">
<div class="bg-white space-y-4 p-5 shadow-border-xs rounded-xl">
<div class="p-5 flex justify-center items-center">
<%= tag.p t(".loading"), class: "text-secondary animate-pulse text-sm" %>
</div>

View file

@ -14,7 +14,7 @@
<div class="col-span-2 flex justify-end items-center gap-2">
<% cash_weight = account.balance.zero? ? 0 : account.cash_balance / account.balance * 100 %>
<%= render "shared/progress_circle", progress: cash_weight, text_class: "text-blue-500" %>
<%= render "shared/progress_circle", progress: cash_weight %>
<%= tag.p number_to_percentage(cash_weight, precision: 1) %>
</div>

View file

@ -18,7 +18,7 @@
<div class="col-span-2 flex justify-end items-center gap-2">
<% if holding.weight %>
<%= render "shared/progress_circle", progress: holding.weight, text_class: "text-blue-500" %>
<%= render "shared/progress_circle", progress: holding.weight %>
<%= tag.p number_to_percentage(holding.weight, precision: 1) %>
<% else %>
<%= tag.p "--", class: "text-secondary mb-5" %>

View file

@ -1,5 +1,5 @@
<%= turbo_frame_tag dom_id(@account, "holdings") do %>
<div class="bg-white space-y-4 p-5 border border-alpha-black-25 rounded-xl shadow-xs">
<div class="bg-white space-y-4 p-5 rounded-xl shadow-border-xs">
<div class="flex items-center justify-between">
<%= tag.h2 t(".holdings"), class: "font-medium text-lg" %>
<%= link_to new_account_trade_path(account_id: @account.id),
@ -20,7 +20,7 @@
<%= tag.p t(".return"), class: "col-span-2 justify-self-end" %>
</div>
<div class="rounded-lg bg-white border-alpha-black-25 shadow-xs">
<div class="rounded-lg bg-white shadow-border-xs">
<% if @account.current_holdings.any? %>
<%= render "account/holdings/cash", account: @account %>
<%= render "account/holdings/ruler" %>

View file

@ -1,15 +0,0 @@
<div class="fixed bottom-6 z-10 flex items-center justify-between rounded-xl bg-gray-900 px-4 text-sm text-white w-[420px] py-1.5">
<div class="flex items-center gap-2">
<%= check_box_tag "entry_selection", 1, true, class: "checkbox checkbox--dark", data: { action: "bulk-select#deselectAll" } %>
<p data-bulk-select-target="selectionBarText"></p>
</div>
<div class="flex items-center gap-1 text-secondary">
<%= form_with url: bulk_delete_account_transactions_path, data: { turbo_confirm: true, turbo_frame: "_top" } do %>
<button type="button" data-bulk-select-scope-param="bulk_delete" data-action="bulk-select#submitBulkRequest" class="p-1.5 group hover:bg-gray-700 flex items-center justify-center rounded-md" title="Delete">
<%= lucide_icon "trash-2", class: "w-5 group-hover:text-white" %>
</button>
<% end %>
</div>
</div>

View file

@ -1,52 +1,50 @@
<%# locals: (entry:, selectable: true, balance_trend: nil) %>
<%# locals: (entry:, balance_trend: nil, **) %>
<% trade, account = entry.account_trade, entry.account %>
<% trade = entry.entryable %>
<div class="grid grid-cols-12 items-center <%= entry.excluded ? "text-subdued bg-gray-25" : "text-primary" %> text-sm font-medium p-4">
<div class="col-span-6 flex items-center gap-4">
<% if selectable %>
<%= check_box_tag dom_id(entry, "selection"),
<%= turbo_frame_tag dom_id(entry) do %>
<%= turbo_frame_tag dom_id(trade) do %>
<div class="grid grid-cols-12 items-center <%= entry.excluded ? "text-gray-400 bg-gray-25" : "text-primary" %> text-sm font-medium p-4">
<div class="col-span-6 flex items-center gap-4">
<%= check_box_tag dom_id(entry, "selection"),
class: "checkbox checkbox--light",
data: { id: entry.id, "bulk-select-target": "row", action: "bulk-select#toggleRowSelection" } %>
<% end %>
<div class="max-w-full">
<%= tag.div class: ["flex items-center gap-2"] do %>
<div class="flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-gray-600/5 text-gray-600">
<%= entry.display_name.first.upcase %>
</div>
<div class="max-w-full">
<%= tag.div class: ["flex items-center gap-2"] do %>
<div class="flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-gray-600/5 text-gray-600">
<%= entry.display_name.first.upcase %>
</div>
<div class="truncate">
<% if entry.new_record? %>
<%= content_tag :p, entry.display_name %>
<% else %>
<%= link_to entry.display_name,
<div class="truncate">
<%= link_to entry.display_name,
account_entry_path(entry),
data: { turbo_frame: "drawer", turbo_prefetch: false },
class: "hover:underline hover:text-gray-800" %>
</div>
<% end %>
</div>
<% end %>
</div>
</div>
</div>
<div class="col-span-2 flex items-center">
<%= render "categories/badge", category: trade_category %>
</div>
<div class="col-span-2 flex items-center">
<%= render "categories/badge", category: trade_category %>
</div>
<div class="col-span-2 justify-self-end font-medium text-sm">
<%= content_tag :p,
<div class="col-span-2 justify-self-end font-medium text-sm">
<%= content_tag :p,
format_money(-entry.amount_money),
class: ["text-green-600": entry.amount.negative?] %>
</div>
<div class="col-span-2 justify-self-end">
<% if balance_trend&.trend %>
<div class="flex items-center gap-2">
<%= tag.p format_money(balance_trend.trend.current), class: "font-medium text-sm text-primary" %>
</div>
<% else %>
<%= tag.p "--", class: "font-medium text-sm text-subdued" %>
<% end %>
</div>
</div>
<div class="col-span-2 justify-self-end">
<% if balance_trend&.trend %>
<div class="flex items-center gap-2">
<%= tag.p format_money(balance_trend.trend.current), class: "font-medium text-sm text-primary" %>
</div>
<% else %>
<%= tag.p "--", class: "font-medium text-sm text-gray-400" %>
<% end %>
</div>
</div>
<% end %>
<% end %>

View file

@ -1,42 +0,0 @@
<%= turbo_frame_tag dom_id(@account, "trades") do %>
<div id="trades" data-controller="bulk-select" data-bulk-select-resource-value="<%= t(".trade") %>" class="bg-white space-y-4 p-5 border border-alpha-black-25 rounded-xl shadow-xs">
<div class="flex justify-between items-center">
<h3 class="font-medium text-lg"><%= t(".trades") %></h3>
<%= link_to new_account_trade_path(@account),
id: dom_id(@account, "new_trade"),
class: "flex gap-1 font-medium items-center bg-gray-50 text-primary p-2 rounded-lg",
data: { turbo_frame: :modal } do %>
<%= lucide_icon("plus", class: "w-5 h-5 text-primary") %>
<span class="text-sm"><%= t(".new") %></span>
<% end %>
</div>
<div class="bg-gray-25 rounded-xl grid grid-cols-12 items-center uppercase text-xs font-medium text-secondary px-5 py-3">
<div class="pl-0.5 col-span-6 flex items-center gap-4">
<%= check_box_tag "selection_entry",
class: "checkbox checkbox--light",
data: { action: "bulk-select#togglePageSelection" } %>
<%= tag.p t(".trade") %>
</div>
<%= tag.p t(".type"), class: "col-span-3 justify-self-end" %>
<%= tag.p t(".amount"), class: "col-span-3 justify-self-end" %>
</div>
<div>
<div id="transaction-selection-bar" data-bulk-select-target="selectionBar" class="flex justify-center hidden">
<%= render "selection_bar" %>
</div>
<% if @entries.empty? %>
<p class="text-secondary py-4"><%= t(".no_trades") %></p>
<% else %>
<div class="space-y-6">
<%= entries_by_date(@entries) do |entries, _transfers| %>
<%= render partial: "account/trades/trade", collection: entries, as: :entry %>
<% end %>
</div>
<% end %>
</div>
</div>
<% end %>

View file

@ -1,75 +1,98 @@
<%# locals: (entry:, selectable: true, balance_trend: nil) %>
<%# locals: (entry:, balance_trend: nil, view_ctx: "global") %>
<div class="grid grid-cols-12 items-center text-primary text-sm font-medium p-4 <%= @focused_record == entry ? "border border-gray-900 rounded-lg" : "" %>">
<div class="pr-10 flex items-center gap-4 <%= balance_trend ? "col-span-6" : "col-span-8" %>">
<% if selectable %>
<%= check_box_tag dom_id(entry, "selection"),
disabled: entry.entryable.transfer?,
class: "checkbox checkbox--light",
data: { id: entry.id, "bulk-select-target": "row", action: "bulk-select#toggleRowSelection" } %>
<% end %>
<% transaction = entry.entryable %>
<div class="max-w-full">
<%= content_tag :div, class: ["flex items-center gap-2"] do %>
<% if entry.entryable.merchant&.icon_url %>
<%= image_tag entry.entryable.merchant.icon_url, class: "w-6 h-6 rounded-full", loading: "lazy" %>
<% else %>
<%= render "shared/circle_logo", name: entry.display_name, size: "sm" %>
<% end %>
<%= turbo_frame_tag dom_id(entry) do %>
<%= turbo_frame_tag dom_id(transaction) do %>
<div class="grid grid-cols-12 items-center text-primary text-sm font-medium p-4
<%= @focused_record == entry || @focused_record == transaction ?
"border border-gray-900 rounded-lg" : "" %>">
<div class="truncate">
<div class="space-y-0.5">
<div class="flex items-center gap-1">
<% if entry.new_record? %>
<%= content_tag :p, entry.display_name %>
<% else %>
<%= link_to entry.entryable.transfer? ? entry.entryable.transfer.name : entry.display_name,
entry.entryable.transfer? ? transfer_path(entry.entryable.transfer) : account_entry_path(entry),
data: { turbo_frame: "drawer", turbo_prefetch: false },
class: "hover:underline hover:text-gray-800" %>
<% end %>
<div class="pr-10 flex items-center gap-4
<%= balance_trend ? "col-span-6" : "col-span-8" %>">
<%= check_box_tag dom_id(entry, "selection"),
disabled: transaction.transfer?,
class: "checkbox checkbox--light",
data: {
id: entry.id,
"bulk-select-target": "row",
action: "bulk-select#toggleRowSelection"
} %>
<% if entry.excluded %>
<span title="One-time <%= entry.amount.negative? ? "income" : "expense" %> (excluded from averages)">
<%= lucide_icon "asterisk", class: "w-4 h-4 shrink-0 text-orange-500" %>
</span>
<% end %>
<div class="max-w-full">
<%= content_tag :div, class: ["flex items-center gap-2"] do %>
<% if transaction.merchant&.icon_url %>
<%= image_tag transaction.merchant.icon_url,
class: "w-6 h-6 rounded-full",
loading: "lazy" %>
<% else %>
<%= render "shared/circle_logo",
name: entry.display_name,
size: "sm" %>
<% end %>
<% if entry.entryable.transfer? %>
<%= render "account/transactions/transfer_match", entry: entry %>
<% end %>
<div class="truncate">
<div class="space-y-0.5">
<div class="flex items-center gap-1">
<%= link_to(
transaction.transfer? ? transaction.transfer.name : entry.display_name,
transaction.transfer? ? transfer_path(transaction.transfer) : account_entry_path(entry),
data: {
turbo_frame: "drawer",
turbo_prefetch: false
},
class: "hover:underline hover:text-gray-800"
) %>
<% if entry.excluded %>
<span title="One-time <%= entry.amount.negative? ? "income" : "expense" %> (excluded from averages)">
<%= lucide_icon "asterisk", class: "w-4 h-4 shrink-0 text-orange-500" %>
</span>
<% end %>
<% if transaction.transfer? %>
<%= render "account/transactions/transfer_match", transaction: transaction %>
<% end %>
</div>
<div class="text-secondary text-xs font-normal">
<% if transaction.transfer? %>
<%= render "transfers/account_links",
transfer: transaction.transfer,
is_inflow: transaction.transfer_as_inflow.present? %>
<% else %>
<%= link_to entry.account.name,
account_path(entry.account, tab: "transactions", focused_record_id: entry.id),
data: { turbo_frame: "_top" },
class: "hover:underline" %>
<% end %>
</div>
</div>
</div>
<% end %>
</div>
</div>
<div class="text-secondary text-xs font-normal">
<% if entry.entryable.transfer? %>
<%= render "transfers/account_links", transfer: entry.entryable.transfer, is_inflow: entry.entryable.transfer_as_inflow.present? %>
<% else %>
<%= link_to entry.account.name, account_path(entry.account, tab: "transactions", focused_record_id: entry.id), data: { turbo_frame: "_top" }, class: "hover:underline" %>
<% end %>
</div>
</div>
<div class="flex items-center gap-1 col-span-2">
<%= render "account/transactions/transaction_category", transaction: transaction %>
</div>
<div class="col-span-2 ml-auto">
<%= content_tag :p,
transaction.transfer? && view_ctx == "global" ? "+/- #{format_money(entry.amount_money.abs)}" : format_money(-entry.amount_money),
class: ["text-green-600": entry.amount.negative?] %>
</div>
<% if balance_trend %>
<div class="col-span-2 justify-self-end">
<% if balance_trend.trend %>
<%= tag.p format_money(balance_trend.trend.current),
class: "font-medium text-sm text-primary" %>
<% else %>
<%= tag.p "--", class: "font-medium text-sm text-gray-400" %>
<% end %>
</div>
<% end %>
</div>
</div>
<div class="flex items-center gap-1 col-span-2">
<%= render "account/transactions/transaction_category", entry: entry %>
</div>
<div class="col-span-2 ml-auto">
<%= content_tag :p,
format_money(-entry.amount_money),
class: ["text-green-600": entry.amount.negative?] %>
</div>
<% if balance_trend %>
<div class="col-span-2 justify-self-end">
<% if balance_trend.trend %>
<%= tag.p format_money(balance_trend.trend.current), class: "font-medium text-sm text-primary" %>
<% else %>
<%= tag.p "--", class: "font-medium text-sm text-subdued" %>
<% end %>
</div>
<% end %>
</div>
<% end %>

View file

@ -1,9 +1,9 @@
<%# locals: (entry:) %>
<%# locals: (transaction:) %>
<div id="<%= dom_id(entry, "category_menu") %>">
<% if entry.account_transaction.transfer&.categorizable? || entry.account_transaction.transfer.nil? %>
<%= render "categories/menu", transaction: entry.account_transaction %>
<div id="<%= dom_id(transaction, "category_menu") %>">
<% if transaction.transfer&.categorizable? || transaction.transfer.nil? %>
<%= render "categories/menu", transaction: transaction %>
<% else %>
<%= render "categories/badge", category: entry.account_transaction.transfer&.payment? ? payment_category : transfer_category %>
<%= render "categories/badge", category: transaction.transfer&.payment? ? payment_category : transfer_category %>
<% end %>
</div>

View file

@ -1,26 +1,26 @@
<%# locals: (entry:) %>
<%# locals: (transaction:) %>
<div id="<%= dom_id(entry, "transfer_match") %>" class="flex items-center gap-1">
<% if entry.account_transaction.transfer.confirmed? %>
<span title="<%= entry.account_transaction.transfer.payment? ? "Payment" : "Transfer" %> is confirmed">
<div id="<%= dom_id(transaction, "transfer_match") %>" class="flex items-center gap-1">
<% if transaction.transfer.confirmed? %>
<span title="<%= transaction.transfer.payment? ? "Payment" : "Transfer" %> is confirmed">
<%= lucide_icon "link-2", class: "w-4 h-4 text-indigo-600" %>
</span>
<% elsif entry.account_transaction.transfer.pending? %>
<% elsif transaction.transfer.pending? %>
<span class="inline-flex items-center rounded-full bg-indigo-50 px-2 py-0.5 text-xs font-medium text-indigo-700">
Auto-matched
</span>
<%= button_to transfer_path(entry.account_transaction.transfer, transfer: { status: "confirmed" }),
<%= button_to transfer_path(transaction.transfer, transfer: { status: "confirmed" }),
method: :patch,
class: "text-secondary hover:text-gray-800 flex items-center justify-center",
class: "text-secondary hover:text-gray-800 flex items-center justify-center cursor-pointer",
title: "Confirm match" do %>
<%= lucide_icon "check", class: "w-4 h-4 text-indigo-400 hover:text-indigo-600" %>
<% end %>
<%= button_to transfer_path(entry.account_transaction.transfer, transfer: { status: "rejected" }),
<%= button_to transfer_path(transaction.transfer, transfer: { status: "rejected" }),
method: :patch,
data: { turbo: false },
class: "text-secondary hover:text-gray-800 flex items-center justify-center",
class: "text-secondary hover:text-gray-800 flex items-center justify-center cursor-pointer",
title: "Reject match" do %>
<%= lucide_icon "x", class: "w-4 h-4 text-subdued hover:text-gray-600" %>
<% end %>

View file

@ -1,7 +1,7 @@
<%= turbo_frame_tag "bulk_transaction_edit_drawer" do %>
<dialog data-controller="modal"
data-action="click->modal#clickOutside"
class="bg-white border border-alpha-black-25 rounded-2xl max-h-[calc(100vh-32px)] max-w-[480px] w-full shadow-xs h-full mt-4 mr-4 ml-auto">
class="bg-white shadow-border-xs rounded-2xl max-h-[calc(100vh-32px)] max-w-[480px] w-full mt-4 mr-4 ml-auto">
<%= styled_form_with url: bulk_update_account_transactions_path, scope: "bulk_update", class: "h-full", data: { turbo_frame: "_top" } do |form| %>
<div class="flex h-full flex-col justify-between p-4">
<div>

View file

@ -1,32 +0,0 @@
<%= turbo_frame_tag dom_id(@account, "transactions") do %>
<div class="bg-white space-y-4 p-5 border border-alpha-black-25 rounded-xl shadow-xs">
<div class="flex justify-between items-center">
<h3 class="font-medium text-lg"><%= t(".transactions") %></h3>
<%= link_to new_account_transaction_path(account_id: @account),
class: "flex gap-1 font-medium items-center bg-gray-50 text-primary p-2 rounded-lg",
data: { turbo_frame: :modal } do %>
<%= lucide_icon("plus", class: "w-5 h-5 text-primary") %>
<span class="text-sm"><%= t(".new") %></span>
<% end %>
</div>
<div id="transactions" data-controller="bulk-select" data-bulk-select-resource-value="<%= t(".transaction") %>">
<div id="transaction-selection-bar" data-bulk-select-target="selectionBar" class="flex justify-center hidden">
<%= render "selection_bar" %>
</div>
<% if @entries.empty? %>
<p class="text-secondary py-4"><%= t(".no_transactions") %></p>
<% else %>
<div class="space-y-6">
<%= entries_by_date(@entries) do |entries, _transfers| %>
<%= render entries %>
<% end %>
</div>
<div class="pt-4">
<%= render "pagination", pagy: @pagy %>
</div>
<% end %>
</div>
</div>
<% end %>

View file

@ -1,43 +1,43 @@
<%# locals: (entry:, selectable: true, balance_trend: nil) %>
<%# locals: (entry:, balance_trend: nil, **) %>
<% valuation = entry.entryable %>
<% color = balance_trend&.trend&.color || "#D444F1" %>
<% icon = balance_trend&.trend&.icon || "plus" %>
<div class="p-4 grid grid-cols-12 items-center text-primary text-sm font-medium">
<div class="col-span-8 flex items-center gap-4">
<% if selectable %>
<%= check_box_tag dom_id(entry, "selection"),
<%= turbo_frame_tag dom_id(entry) do %>
<%= turbo_frame_tag dom_id(valuation) do %>
<div class="p-4 grid grid-cols-12 items-center text-primary text-sm font-medium">
<div class="col-span-8 flex items-center gap-4">
<%= check_box_tag dom_id(entry, "selection"),
class: "checkbox checkbox--light",
data: { id: entry.id, "bulk-select-target": "row", action: "bulk-select#toggleRowSelection" } %>
<% end %>
<div class="flex items-center gap-3">
<%= tag.div class: "w-6 h-6 rounded-full p-1.5 flex items-center justify-center", style: mixed_hex_styles(color) do %>
<%= lucide_icon icon, class: "w-4 h-4 shrink-0" %>
<% end %>
<div class="flex items-center gap-3">
<%= tag.div class: "w-6 h-6 rounded-full p-1.5 flex items-center justify-center", style: mixed_hex_styles(color) do %>
<%= lucide_icon icon, class: "w-4 h-4 shrink-0" %>
<% end %>
<div class="truncate text-primary">
<% if entry.new_record? %>
<%= content_tag :p, entry.display_name %>
<% else %>
<%= link_to entry.display_name,
<div class="truncate text-primary">
<%= link_to entry.display_name,
account_entry_path(entry),
data: { turbo_frame: "drawer", turbo_prefetch: false },
class: "hover:underline hover:text-gray-800" %>
</div>
</div>
</div>
<div class="col-span-2 justify-self-end font-medium text-sm">
<% if balance_trend&.trend %>
<%= tag.span format_money(balance_trend.trend.value), style: "color: #{balance_trend.trend.color}" %>
<% else %>
<%= tag.span "--", class: "text-gray-400" %>
<% end %>
</div>
<div class="col-span-2 justify-self-end">
<%= tag.p format_money(entry.amount_money), class: "font-medium text-sm text-primary" %>
</div>
</div>
</div>
<div class="col-span-2 justify-self-end font-medium text-sm">
<% if balance_trend&.trend %>
<%= tag.span format_money(balance_trend.trend.value), style: "color: #{balance_trend.trend.color}" %>
<% else %>
<%= tag.span "--", class: "text-subdued" %>
<% end %>
</div>
<div class="col-span-2 justify-self-end">
<%= tag.p format_money(entry.amount_money), class: "font-medium text-sm text-primary" %>
</div>
</div>
<% end %>
<% end %>

View file

@ -1,3 +0,0 @@
<%= turbo_frame_tag dom_id(@entry) do %>
<%= render "account/valuations/form", entry: @entry %>
<% end %>

View file

@ -1,5 +1,5 @@
<%= turbo_frame_tag dom_id(@account, "valuations") do %>
<div class="bg-white space-y-4 p-5 border border-alpha-black-25 rounded-xl shadow-xs">
<div class="bg-white space-y-4 p-5 shadow-border-xs rounded-xl">
<div class="flex items-center justify-between">
<%= tag.h2 t(".valuations"), class: "font-medium text-lg" %>
<%= link_to new_account_valuation_path(@account),
@ -18,7 +18,7 @@
<%= tag.div class: "col-span-1" %>
</div>
<div class="rounded-lg bg-white border-alpha-black-25 shadow-xs">
<div class="rounded-lg bg-white shadow-border-xs">
<%= turbo_frame_tag dom_id(@account.entries.account_valuations.new) %>
<% if @entries.any? %>

View file

@ -0,0 +1,11 @@
<%= turbo_frame_tag "#{@accountable.model_name.param_key}_sparkline" do %>
<div class="flex items-center justify-end gap-1">
<div class="w-8 h-3">
<%= render "shared/sparkline", id: dom_id(@accountable, :sparkline_chart), series: @series %>
</div>
<%= tag.p @series.trend.percent_formatted,
style: "color: #{@series.trend.color}",
class: "text-right text-xs font-medium text-primary" %>
</div>
<% end %>

View file

@ -1,61 +0,0 @@
<%# locals: (group:) -%>
<% type = Accountable.from_type(group.name) %>
<% if group && group.children.any? %>
<% group_trend = group.series.trend %>
<details
class="mb-1 text-sm group"
data-controller="account-collapse"
data-account-collapse-type-value="<%= type %>">
<summary class="flex gap-4 px-3 py-2 items-center w-full rounded-[10px] font-medium
hover:bg-gray-100 cursor-pointer">
<%= lucide_icon("chevron-down",
class: "hidden group-open:block text-secondary w-5 h-5") %>
<%= lucide_icon("chevron-right",
class: "group-open:hidden text-secondary w-5 h-5") %>
<div class="text-left"><%= type.model_name.human %></div>
<div class="ml-auto flex flex-col items-end">
<p class="text-right"><%= format_money group.sum %></p>
<div class="flex items-center gap-1">
<div class="h-3 w-8">
<%= render "shared/sparkline", series: group.series, id: "#{group.name}_sparkline" %>
</div>
<span class="text-xs" style="color: <%= group_trend.color %>"><%= group_trend.value.positive? ? "+" : "" %><%= group_trend.percent.infinite? ? "∞" : number_to_percentage(group_trend.percent, precision: 0) %></span>
</div>
</div>
</summary>
<% group.children.sort_by(&:name).each do |account_value_node| %>
<% account = account_value_node.original %>
<% account_trend = account_value_node.series.trend %>
<%= link_to account, class: "flex items-center w-full gap-3 px-3 py-2 mb-1 hover:bg-gray-100 rounded-[10px]" do %>
<%= render "accounts/logo", account: account, size: "sm" %>
<div class="overflow-hidden">
<p class="font-medium truncate"><%= account_value_node.name %></p>
<% if account.subtype %>
<p class="text-xs text-secondary"><%= account.subtype&.humanize %></p>
<% end %>
</div>
<div class="flex flex-col items-end font-medium text-right ml-auto">
<p><%= format_money account.balance_money %></p>
<div class="flex items-center gap-1">
<div class="h-3 w-8">
<%= render "shared/sparkline", series: account_value_node.series, id: dom_id(account, :list_sparkline) %>
</div>
<span class="text-xs" style="color: <%= account_trend.color %>">
<%= account_trend.value.positive? ? "+" : "" %><%= account_trend.percent.infinite? ? "∞" : number_to_percentage(account_trend.percent, precision: 0) %>
</span>
</div>
</div>
<% end %>
<% end %>
<%= link_to new_polymorphic_path(type, step: "method_select"), class: "flex items-center min-h-10 gap-4 px-3 py-2 mb-1 text-secondary text-sm font-medium rounded-[10px] hover:bg-gray-100", data: { turbo_frame: "modal" } do %>
<%= lucide_icon("plus", class: "w-5 h-5") %>
<%= t(".new_account", type: type.model_name.human.downcase) %>
<% end %>
</details>
<% end %>

View file

@ -0,0 +1,68 @@
<%# locals: (family:) %>
<div
class="space-y-3"
data-controller="tabs"
data-tabs-local-storage-key-value="account-sidebar-tabs"
data-tabs-active-class="bg-white 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" 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" 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" data-tabs-target="btn" data-action="click->tabs#select">
All
</button>
</div>
<div data-tabs-target="tab" id="assets-tab">
<%= link_to new_account_path(step: "method_select"),
class: "flex items-center gap-3 btn btn--ghost text-secondary mb-1",
data: { turbo_frame: "modal" } do %>
<%= icon("plus") %>
<span>New asset</span>
<% end %>
<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 data-tabs-target="tab" id="debts-tab" class="hidden">
<%= link_to new_account_path(step: "method_select"),
class: "flex items-center gap-3 btn btn--ghost text-secondary mb-1",
data: { turbo_frame: "modal" } do %>
<%= icon("plus") %>
<span>New debt</span>
<% end %>
<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 data-tabs-target="tab" id="all-tab" class="hidden">
<%= link_to new_account_path(step: "method_select"),
class: "flex items-center gap-3 btn btn--ghost text-secondary mb-1",
data: { turbo_frame: "modal" } do %>
<%= icon("plus") %>
<span>New account</span>
<% end %>
<div class="space-y-2">
<% family.balance_sheet.account_groups.each do |group| %>
<%= render "accounts/accountable_group", account_group: group %>
<% end %>
</div>
</div>
</div>

View file

@ -5,5 +5,5 @@
<span style="background-color: color-mix(in srgb, <%= accountable.color %> 10%, white);" class="flex w-8 h-8 shrink-0 grow-0 items-center justify-center rounded-lg border border-alpha-black-25">
<%= lucide_icon(accountable.icon, style: "color: #{accountable.color}", class: "w-5 h-5") %>
</span>
<%= accountable.model_name.human %>
<%= accountable.display_name.singularize %>
<% end %>

View file

@ -0,0 +1,49 @@
<%# locals: (account_group:) %>
<details class="group" data-controller="account-collapse" data-account-collapse-type-value="<%= account_group.key %>">
<summary class="px-3 py-2 flex items-center gap-3 cursor-pointer h-10 mb-1">
<%= lucide_icon("chevron-right", class: "group-open:rotate-90 text-secondary w-5 h-5") %>
<%= tag.p account_group.name, class: "text-sm font-medium" %>
<div class="ml-auto text-right grow">
<%= tag.p format_money(account_group.total_money), class: "text-sm font-medium text-primary" %>
<%= turbo_frame_tag "#{account_group.key}_sparkline", src: accountable_sparkline_path(account_group.key), loading: "lazy" do %>
<div class="flex items-center w-8 h-4 ml-auto">
<div class="w-6 h-px bg-gray-200"></div>
</div>
<% end %>
</div>
</summary>
<div class="space-y-1">
<% account_group.accounts.each do |account| %>
<%= link_to account_path(account), class: "block flex items-center gap-2 btn btn--ghost" do %>
<%= render "accounts/logo", account: account, size: "sm", color: account_group.color %>
<div>
<%= tag.p account.name, class: "text-sm font-medium mb-0.5" %>
<%= tag.p account.subtype&.humanize.presence || account_group.name, class: "text-sm text-secondary" %>
</div>
<div class="ml-auto text-right grow h-10">
<%= tag.p format_money(account.balance_money), class: "text-sm font-medium text-primary" %>
<%= turbo_frame_tag dom_id(account, :sparkline), src: sparkline_account_path(account), loading: "lazy" do %>
<div class="flex items-center w-8 h-5 ml-auto">
<div class="w-6 h-px bg-gray-200"></div>
</div>
<% end %>
</div>
<% end %>
<% end %>
</div>
<%= link_to new_polymorphic_path(account_group.key, step: "method_select"),
class: "flex items-center gap-3 btn btn--ghost text-secondary",
data: { turbo_frame: "modal" } do %>
<%= icon("plus") %>
<span>New <%= account_group.name.downcase.singularize %></span>
<% end %>
</details>

View file

@ -1,4 +1,4 @@
<%# locals: (account:, size: "md") %>
<%# locals: (account:, size: "md", color: nil) %>
<% size_classes = {
"sm" => "w-6 h-6",
@ -12,5 +12,5 @@
<% elsif account.logo.attached? %>
<%= image_tag account.logo, class: "rounded-full #{size_classes[size]}" %>
<% else %>
<%= circle_logo(account.name, hex: account.accountable.color, size: size) %>
<%= circle_logo(account.name, hex: color || account.accountable.color, size: size) %>
<% end %>

View file

@ -1,5 +1,5 @@
<% period = Period.from_param(params[:period]) %>
<% series = @account.series(period: period) %>
<% period = Period.from_key(params[:period], fallback: true) %>
<% series = @account.balance_series(period: period) %>
<% trend = series.trend %>
<%= turbo_frame_tag dom_id(@account, :chart_details) do %>
@ -13,23 +13,19 @@
<% end %>
<% end %>
<%= tag.span period_label(period), class: "text-secondary" %>
<%= tag.span period.comparison_label, class: "text-secondary" %>
</div>
<div class="h-64">
<% if series.has_current_day_value? %>
<% if series.any? %>
<div
id="lineChart"
class="w-full h-full"
data-controller="time-series-chart"
data-time-series-chart-data-value="<%= series.to_json %>"></div>
<% elsif series.empty? %>
<div class="w-full h-full flex items-center justify-center">
<p class="text-secondary text-sm">No data available for the selected period.</p>
</div>
<% else %>
<div class="w-full h-full flex items-center justify-center">
<p class="text-secondary text-sm animate-pulse">Calculating latest balance data...</p>
<p class="text-secondary text-sm">No data available for the selected period.</p>
</div>
<% end %>
</div>

View file

@ -1,43 +1,35 @@
<% content_for :sidebar do %>
<%= render "settings/nav" %>
<% end %>
<div class="space-y-4">
<header class="flex justify-between items-center text-primary font-medium">
<h1 class="text-xl"><%= t(".accounts") %></h1>
<div class="flex items-center gap-5">
<div class="flex items-center gap-2">
<%= button_to sync_all_accounts_path,
<header class="flex justify-between items-center text-primary font-medium">
<h1 class="text-xl"><%= t(".accounts") %></h1>
<div class="flex items-center gap-5">
<div class="flex items-center gap-2">
<%= button_to sync_all_accounts_path,
disabled: Current.family.syncing?,
class: "btn btn--outline flex items-center gap-2",
title: t(".sync") do %>
<%= lucide_icon "refresh-cw", class: "w-5 h-5" %>
<span><%= t(".sync") %></span>
<% end %>
<%= lucide_icon "refresh-cw", class: "w-5 h-5" %>
<span><%= t(".sync") %></span>
<% end %>
<%= link_to new_account_path(return_to: accounts_path),
<%= link_to new_account_path(return_to: accounts_path),
data: { turbo_frame: "modal" },
class: "btn btn--primary flex items-center gap-1" do %>
<%= lucide_icon("plus", class: "w-5 h-5") %>
<p class="text-sm font-medium"><%= t(".new_account") %></p>
<% end %>
</div>
</div>
</header>
<% if @manual_accounts.empty? && @plaid_items.empty? %>
<%= render "empty" %>
<% else %>
<div class="space-y-2">
<% if @plaid_items.any? %>
<%= render @plaid_items.sort_by(&:created_at) %>
<% end %>
<% if @manual_accounts.any? %>
<%= render "accounts/index/manual_accounts", accounts: @manual_accounts %>
<%= lucide_icon("plus", class: "w-5 h-5") %>
<p class="text-sm font-medium"><%= t(".new_account") %></p>
<% end %>
</div>
<% end %>
</div>
</header>
<%= settings_nav_footer %>
</div>
<% if @manual_accounts.empty? && @plaid_items.empty? %>
<%= render "empty" %>
<% else %>
<div class="space-y-2">
<% if @plaid_items.any? %>
<%= render @plaid_items.sort_by(&:created_at) %>
<% end %>
<% if @manual_accounts.any? %>
<%= render "accounts/index/manual_accounts", accounts: @manual_accounts %>
<% end %>
</div>
<% end %>

View file

@ -3,7 +3,7 @@
<% accounts.group_by(&:accountable_type).sort_by { |group, _| group }.each do |group, accounts| %>
<div class="bg-gray-25 p-1 rounded-xl">
<div class="flex items-center px-4 py-2 text-xs font-medium text-secondary">
<p><%= to_accountable_title(Accountable.from_type(group)) %></p>
<p><%= Accountable.from_type(group).display_name %></p>
<span class="text-subdued mx-2">&middot;</span>
<p><%= accounts.count %></p>
<p class="ml-auto"><%= totals_by_currency(collection: accounts, money_method: :balance_money) %></p>

View file

@ -1,6 +1,6 @@
<%# locals: (accounts:) %>
<details open class="group bg-white p-4 border border-alpha-black-25 shadow-xs rounded-xl">
<details open class="group bg-white p-4 shadow-border-xs rounded-xl">
<summary class="flex items-center gap-2 focus-visible:outline-hidden">
<%= lucide_icon "chevron-right", class: "group-open:transform group-open:rotate-90 text-secondary w-5" %>

View file

@ -1,5 +0,0 @@
<%= turbo_frame_tag "account-list" do %>
<% account_groups(period: @period).each do |group| %>
<%= render "accounts/account_list", group: group %>
<% end %>
<% end %>

View file

@ -1,7 +1,7 @@
<%# locals: (account:) %>
<%= turbo_frame_tag dom_id(account, "entries") do %>
<div class="bg-white p-5 border border-alpha-black-25 rounded-xl shadow-xs" data-controller="focus-record" data-focus-record-id-value="<%= @focused_record ? dom_id(@focused_record) : nil %>">
<div class="bg-white p-5 shadow-border-xs rounded-xl" data-controller="focus-record" data-focus-record-id-value="<%= @focused_record ? dom_id(@focused_record) : nil %>">
<div class="flex items-center justify-between mb-4">
<%= tag.h2 t(".title"), class: "font-medium text-lg" %>
<% unless @account.plaid_account_id.present? %>
@ -77,16 +77,16 @@
<div class="rounded-tl-lg rounded-tr-lg bg-white border-alpha-black-25 shadow-xs">
<div class="space-y-4">
<% calculator = Account::BalanceTrendCalculator.for(@entries) %>
<%= entries_by_date(@entries) do |entries, _transfers| %>
<%= entries_by_date(@entries) do |entries| %>
<% entries.each do |entry| %>
<%= render entry, balance_trend: calculator&.trend_for(entry) %>
<%= render entry, balance_trend: calculator&.trend_for(entry), view_ctx: "account" %>
<% end %>
<% end %>
</div>
</div>
<div class="p-4 bg-white rounded-bl-lg rounded-br-lg">
<%= render "pagination", pagy: @pagy %>
<%= render "shared/pagination", pagy: @pagy %>
</div>
</div>
<% end %>

View file

@ -1,6 +1,6 @@
<%# locals: (account:, title: nil, tooltip: nil, **args) %>
<% period = Period.from_param(params[:period]) %>
<% period = Period.from_key(params[:period], fallback: true) %>
<% default_value_title = account.asset? ? t(".balance") : t(".owed") %>
<div id="<%= dom_id(account, :chart) %>" class="bg-white shadow-xs rounded-xl border border-alpha-black-25 rounded-lg space-y-2">
@ -15,11 +15,11 @@
</div>
<%= form_with url: request.path, method: :get, data: { controller: "auto-submit-form" } do |form| %>
<%= period_select form: form, selected: period.name %>
<%= period_select form: form, selected: period %>
<% end %>
</div>
<%= turbo_frame_tag dom_id(account, :chart_details), src: chart_account_path(account, period: period.name) do %>
<%= turbo_frame_tag dom_id(account, :chart_details), src: chart_account_path(account, period: period.key) do %>
<%= render "accounts/chart_loader" %>
<% end %>
</div>

View file

@ -3,7 +3,7 @@
<%= turbo_stream_from account %>
<%= turbo_frame_tag dom_id(account) do %>
<%= tag.div class: "space-y-4" do %>
<%= tag.div class: "space-y-4 pb-32" do %>
<% if header.present? %>
<%= header %>
<% else %>

View file

@ -0,0 +1,11 @@
<%= turbo_frame_tag dom_id(@account, :sparkline) do %>
<div class="flex items-center justify-end gap-1">
<div class="w-8 h-5">
<%= render "shared/sparkline", id: dom_id(@account, :sparkline_chart), series: @account.sparkline_series %>
</div>
<%= tag.p @account.sparkline_series.trend.percent_formatted,
style: "color: #{@account.sparkline_series.trend.color}",
class: "text-right text-xs font-medium text-primary" %>
</div>
<% end %>

View file

@ -1,92 +0,0 @@
<% period = Period.from_param(params[:period]) %>
<div class="space-y-4">
<%= render "accounts/summary/header" %>
<div class="bg-white rounded-xl shadow-xs border border-tertiary flex divide-x divide-gray-200">
<div class="w-1/2 p-4 flex items-stretch justify-between">
<div class="space-y-2 grow">
<%= render partial: "shared/value_heading", locals: {
label: "Assets",
period: period,
value: Current.family.assets,
trend: @asset_series.trend
} %>
</div>
<div
id="assetsChart"
class="h-full w-2/5"
data-controller="time-series-chart"
data-time-series-chart-data-value="<%= @asset_series.to_json %>"
data-time-series-chart-use-labels-value="false"></div>
</div>
<div class="w-1/2 p-4 flex items-stretch justify-between">
<div class="space-y-2 grow">
<%= render partial: "shared/value_heading", locals: {
label: "Liabilities",
period: period,
size: "md",
value: Current.family.liabilities,
trend: @liability_series.trend
} %>
</div>
<div
id="liabilitiesChart"
class="h-full w-2/5"
data-controller="time-series-chart"
data-time-series-chart-data-value="<%= @liability_series.to_json %>"
data-time-series-chart-use-labels-value="false"></div>
</div>
</div>
<div class="p-4 bg-white rounded-xl shadow-xs border border-alpha-black-25 space-y-4">
<div class="flex justify-between items-center mb-5">
<h2 class="text-lg font-medium text-primary">Assets</h2>
<div class="flex items-center gap-2">
<%= link_to new_account_path, class: "btn btn--secondary flex items-center gap-1", data: { turbo_frame: "modal" } do %>
<%= lucide_icon("plus", class: "w-5 h-5 text-secondary") %>
<p><%= t(".new") %></p>
<% end %>
<%= form_with url: summary_accounts_path, method: :get, data: { controller: "auto-submit-form" } do |form| %>
<%= period_select form: form, selected: period.name %>
<% end %>
</div>
</div>
<% if @account_groups[:assets].children.any? %>
<%= render partial: "pages/account_percentages_bar", locals: { account_groups: @account_groups[:assets].children } %>
<%= render partial: "pages/account_percentages_table", locals: { account_groups: @account_groups[:assets].children } %>
<% else %>
<div class="py-20 flex flex-col items-center">
<%= lucide_icon "blocks", class: "w-6 h-6 shrink-0 text-secondary" %>
<p class="text-primary text-sm font-medium mb-1 mt-4"><%= t(".no_assets") %></p>
<p class="text-secondary text-sm max-w-xs text-center"><%= t(".no_assets_description") %></p>
</div>
<% end %>
</div>
<div class="p-4 bg-white rounded-xl shadow-xs border border-alpha-black-25 space-y-4">
<div class="flex justify-between items-center mb-5">
<h2 class="text-lg font-medium text-primary">Liabilities</h2>
<div class="flex items-center gap-2">
<%= link_to new_account_path, class: "btn btn--secondary flex items-center gap-1", data: { turbo_frame: "modal" } do %>
<%= lucide_icon("plus", class: "w-5 h-5 text-secondary") %>
<p><%= t(".new") %></p>
<% end %>
<%= form_with url: summary_accounts_path, method: :get, data: { controller: "auto-submit-form" } do |form| %>
<%= period_select form: form, selected: period.name %>
<% end %>
</div>
</div>
<% if @account_groups[:liabilities].children.any? %>
<%= render partial: "pages/account_percentages_bar", locals: { account_groups: @account_groups[:liabilities].children } %>
<%= render partial: "pages/account_percentages_table", locals: { account_groups: @account_groups[:liabilities].children } %>
<% else %>
<div class="py-20 flex flex-col items-center">
<%= lucide_icon "scale", class: "w-6 h-6 shrink-0 text-secondary" %>
<p class="text-primary text-sm font-medium mb-1 mt-4"><%= t(".no_liabilities") %></p>
<p class="text-secondary text-sm max-w-xs text-center"><%= t(".no_liabilities_description") %></p>
</div>
<% end %>
</div>
</div>

View file

@ -1,21 +0,0 @@
<header class="flex justify-between items-center text-primary font-medium">
<h1 class="text-xl"><%= t(".accounts") %></h1>
<div class="flex items-center gap-2">
<%= contextual_menu do %>
<div class="w-48 p-1 text-sm leading-6 text-primary bg-white shadow-lg shrink rounded-xl ring-1 ring-gray-900/5">
<%= link_to accounts_path(return_to: summary_accounts_path),
class: "block w-full py-2 px-3 space-x-2 text-primary hover:bg-gray-50 flex items-center rounded-lg font-normal" do %>
<%= lucide_icon "settings", class: "w-5 h-5 text-secondary" %>
<span class="text-black"><%= t(".manage") %></span>
<% end %>
</div>
<% end %>
<%= link_to new_account_path, class: "rounded-lg bg-gray-900 text-white flex items-center gap-1 justify-center hover:bg-gray-700 px-3 py-2", data: { turbo_frame: :modal } do %>
<%= lucide_icon("plus", class: "w-5 h-5") %>
<p class="text-sm font-medium"><%= t(".new") %></p>
<% end %>
</div>
</header>

View file

@ -32,7 +32,7 @@
<% end %>
<% else %>
<p class="text-sm text-secondary font-medium">
<%= format_money(budget_category.category.avg_monthly_total_money, precision: 0) %> avg
<%= budget_category.median_monthly_expense_money.format %> avg
</p>
<% end %>
</div>

View file

@ -2,13 +2,13 @@
<% currency = Money::Currency.new(budget_category.budget.currency) %>
<div id=<%= dom_id(budget_category, :form) %> class="w-full flex gap-3">
<div id="<%= dom_id(budget_category, :form) %>" class="w-full flex gap-3">
<div class="w-1 h-3 rounded-xl mt-1" style="background-color: <%= budget_category.category.color %>"></div>
<div class="text-sm mr-3">
<p class="text-primary font-medium mb-0.5"><%= budget_category.category.name %></p>
<p class="text-secondary"><%= format_money(budget_category.category.avg_monthly_total_money, precision: 0) %>/m average</p>
<p class="text-secondary"><%= budget_category.median_monthly_expense_money.format(precision: 0) %>/m avg</p>
</div>
<div class="ml-auto">

View file

@ -8,4 +8,4 @@
Confirm
</span>
<% end %>
</div>
</div>

View file

@ -7,7 +7,7 @@
<div class="text-sm mr-3">
<p class="text-primary font-medium mb-0.5"><%= budget_category.category.name %></p>
<p class="text-secondary"><%= format_money(Money.new(budget_category.category.avg_monthly_total, budget_category.category.family.currency), precision: 0) %>/m average</p>
<p class="text-secondary"><%= budget_category.avg_monthly_expense_money.format(precision: 0) %>/m avg</p>
</div>
<div class="ml-auto">

View file

@ -16,7 +16,7 @@
<div class="mx-auto max-w-lg">
<% if @budget.family.categories.empty? %>
<div class="bg-white shadow-xs border border-gray-200 rounded-lg p-4">
<div class="bg-white shadow-border-xs rounded-lg p-4">
<%= render "budget_categories/no_categories" %>
</div>
<% else %>

View file

@ -4,7 +4,7 @@
<div>
<p class="text-sm text-secondary">Category</p>
<h3 class="text-2xl font-medium text-primary">
<%= @budget_category.category.name %>
<%= @budget_category.name %>
</h3>
<% if @budget_category.budget.initialized? %>
@ -80,14 +80,14 @@
<div class="flex items-center justify-between text-sm">
<dt class="text-secondary">Monthly average spending</dt>
<dd class="text-primary font-medium">
<%= format_money @budget_category.category.avg_monthly_total_money, precision: 0 %>
<%= @budget_category.avg_monthly_expense_money.format %>
</dd>
</div>
<div class="flex items-center justify-between text-sm">
<dt class="text-secondary">Monthly median spending</dt>
<dd class="text-primary font-medium">
<%= format_money @budget_category.category.median_monthly_total_money, precision: 0 %>
<%= @budget_category.median_monthly_expense_money.format %>
</dd>
</div>
</dl>
@ -106,7 +106,7 @@
<div class="px-3 py-4 space-y-2">
<% if @recent_transactions.any? %>
<ul class="space-y-2 mb-4">
<% @recent_transactions.each_with_index do |entry, index| %>
<% @recent_transactions.each_with_index do |transaction, index| %>
<li class="flex gap-4 text-sm space-y-1">
<div class="flex flex-col items-center gap-1.5 pt-2">
<div class="rounded-full h-1.5 w-1.5 bg-gray-300"></div>
@ -118,12 +118,15 @@
<div class="flex justify-between w-full">
<div>
<p class="text-secondary text-xs uppercase">
<%= entry.date.strftime("%b %d") %>
<%= transaction.entry.date.strftime("%b %d") %>
</p>
<p class="text-primary"><%= entry.name %></p>
<%= link_to transaction.entry.name,
transactions_path(focused_record_id: transaction.id),
class: "text-primary hover:underline",
data: { turbo_frame: :_top } %>
</div>
<p class="text-primary font-medium">
<%= format_money entry.amount_money %>
<%= format_money transaction.entry.amount_money %>
</p>
</div>
</li>
@ -132,7 +135,7 @@
<%= link_to "View all category transactions",
transactions_path(q: {
categories: [@budget_category.category.name],
categories: [@budget_category.name],
start_date: @budget.start_date,
end_date: @budget.end_date
}),

View file

@ -4,16 +4,15 @@
<%= turbo_stream.replace dom_id(@budget, :confirm_button), partial: "budget_categories/confirm_button", locals: { budget: @budget } %>
<% if @budget_category.subcategory? %>
<%# Update sibling subcategories when a subcategory changes %>
<% @budget_category.siblings.each do |sibling| %>
<%= turbo_stream.update dom_id(sibling, :form), partial: "budget_categories/budget_category_form", locals: { budget_category: sibling } %>
<% end %>
<% else %>
<%# Update all subcategories when a parent category changes %>
<% @budget_category.subcategories.each do |subcategory| %>
<%= turbo_stream.update dom_id(subcategory, :form), partial: "budget_categories/budget_category_form", locals: { budget_category: subcategory } %>
<% end %>
<% end %>
<% end %>

View file

@ -4,26 +4,24 @@
<div class="p-4 border-b border-gray-100">
<h3 class="text-sm text-secondary mb-2">Income</h3>
<% income_totals = budget.income_categories_with_totals %>
<% income_categories = income_totals.category_totals.reject { |ct| ct.amount_money.zero? }.sort_by { |ct| ct.percentage }.reverse %>
<span class="inline-block mb-2 text-xl font-medium text-primary">
<%= format_money(income_totals.total_money) %>
<%= budget.actual_income_money.format %>
</span>
<% if income_categories.any? %>
<% if budget.income_category_totals.any? %>
<div>
<div class="flex h-1.5 mb-3 gap-1">
<% income_categories.each do |item| %>
<div class="h-full rounded-xs" style="background-color: <%= item.category.color %>; width: <%= item.percentage %>%"></div>
<% budget.income_category_totals.each do |category_total| %>
<div class="h-full rounded-xs" style="background-color: <%= category_total.category.color %>; width: <%= category_total.weight %>%"></div>
<% end %>
</div>
<div class="flex flex-wrap gap-x-2.5 gap-y-1 text-xs">
<% income_categories.each do |item| %>
<% budget.income_category_totals.each do |category_total| %>
<div class="flex items-center gap-1.5">
<div class="w-2.5 h-2.5 rounded-full shrink-0" style="background-color: <%= item.category.color %>"></div>
<span class="text-secondary"><%= item.category.name %></span>
<span class="text-primary"><%= number_to_percentage(item.percentage, precision: 0) %></span>
<div class="w-2.5 h-2.5 rounded-full shrink-0" style="background-color: <%= category_total.category.color %>"></div>
<span class="text-secondary"><%= category_total.category.name %></span>
<span class="text-primary"><%= number_to_percentage(category_total.weight, precision: 0) %></span>
</div>
<% end %>
</div>
@ -34,25 +32,22 @@
<div class="p-4">
<h3 class="text-sm text-secondary mb-2">Expenses</h3>
<% expense_totals = budget.expense_categories_with_totals %>
<% expense_categories = expense_totals.category_totals.reject { |ct| ct.amount_money.zero? || ct.category.subcategory? }.sort_by { |ct| ct.percentage }.reverse %>
<span class="inline-block mb-2 text-xl font-medium text-primary"><%= budget.actual_spending_money.format %></span>
<span class="inline-block mb-2 text-xl font-medium text-primary"><%= format_money(expense_totals.total_money) %></span>
<% if expense_categories.any? %>
<% if budget.expense_category_totals.any? %>
<div>
<div class="flex h-1.5 mb-3 gap-1">
<% expense_categories.each do |item| %>
<div class="h-full rounded-xs" style="background-color: <%= item.category.color %>; width: <%= item.percentage %>%"></div>
<% budget.expense_category_totals.each do |category_total| %>
<div class="h-full rounded-xs" style="background-color: <%= category_total.category.color %>; width: <%= category_total.weight %>%"></div>
<% end %>
</div>
<div class="flex flex-wrap gap-x-2.5 gap-y-1 text-xs">
<% expense_categories.each do |item| %>
<% budget.expense_category_totals.each do |category_total| %>
<div class="flex items-center gap-1.5">
<div class="w-2.5 h-2.5 rounded-full shrink-0" style="background-color: <%= item.category.color %>"></div>
<span class="text-secondary"><%= item.category.name %></span>
<span class="text-primary"><%= number_to_percentage(item.percentage, precision: 0) %></span>
<div class="w-2.5 h-2.5 rounded-full shrink-0" style="background-color: <%= category_total.category.color %>"></div>
<span class="text-secondary"><%= category_total.category.name %></span>
<span class="text-primary"><%= number_to_percentage(category_total.weight, precision: 0) %></span>
</div>
<% end %>
</div>

View file

@ -9,7 +9,7 @@
<p class="ml-auto">Amount</p>
</div>
<div class="bg-white py-1 shadow-xs border border-gray-100 rounded-md">
<div class="bg-white py-1 shadow-border-xs rounded-md">
<% if budget.family.categories.expenses.empty? %>
<div class="py-8">
<%= render "budget_categories/no_categories" %>

View file

@ -2,16 +2,16 @@
<div class="flex items-center gap-1 mb-4">
<div class="flex items-center gap-2">
<% if @previous_budget %>
<%= link_to budget_path(@previous_budget) do %>
<% if budget.previous_budget_param %>
<%= link_to budget_path(budget.previous_budget_param) do %>
<%= lucide_icon "chevron-left" %>
<% end %>
<% else %>
<%= lucide_icon "chevron-left", class: "text-subdued" %>
<% end %>
<% if @next_budget %>
<%= link_to budget_path(@next_budget) do %>
<% if budget.next_budget_param %>
<%= link_to budget_path(budget.next_budget_param) do %>
<%= lucide_icon "chevron-right" %>
<% end %>
<% else %>
@ -20,13 +20,13 @@
</div>
<div data-controller="menu" data-menu-placement-value="bottom-start">
<%= tag.button data: { menu_target: "button" }, class: "flex items-center gap-1 hover:bg-gray-50 rounded-md p-2" do %>
<%= tag.button data: { menu_target: "button" }, class: "flex items-center gap-1 hover:bg-alpha-black-25 cursor-pointer rounded-md p-2" do %>
<span class="text-primary font-medium"><%= @budget.name %></span>
<%= lucide_icon "chevron-down", class: "w-5 h-5 shrink-0 text-secondary" %>
<% end %>
<div data-menu-target="content" class="hidden z-10">
<%= render "budgets/picker", family: Current.family, year: Date.current.year %>
<%= render "budgets/picker", family: Current.family, year: budget.start_date.year %>
</div>
</div>
@ -34,7 +34,7 @@
<% if @budget.current? %>
<span class="border border-secondary text-primary text-sm font-medium px-3 py-2 rounded-lg">Today</span>
<% else %>
<%= link_to "Today", budget_path(@latest_budget), class: "btn btn--outline" %>
<%= link_to "Today", budget_path(Budget.date_to_param(Date.current)), class: "btn btn--outline" %>
<% end %>
</div>
</div>

View file

@ -1,9 +1,11 @@
<%# locals: (family:, year:) %>
<%= turbo_frame_tag "budget_picker" do %>
<div class="bg-white shadow-md border border-alpha-black-25 p-3 rounded-xl space-y-4">
<div class="bg-white shadow-border-xs p-3 rounded-xl space-y-4">
<div class="flex items-center gap-2 justify-between">
<% if year > family.oldest_entry_date.year %>
<% last_month_of_previous_year = Date.new(year - 1, 12, 1) %>
<% if Budget.budget_date_valid?(last_month_of_previous_year, family: family) %>
<%= link_to picker_budgets_path(year: year - 1), data: { turbo_frame: "budget_picker" }, class: "p-2 flex items-center justify-center hover:bg-alpha-black-25 rounded-md" do %>
<%= lucide_icon "chevron-left", class: "w-5 h-5 shrink-0 text-secondary" %>
<% end %>
@ -17,7 +19,9 @@
<%= year %>
</span>
<% if year < Date.current.year %>
<% first_month_of_next_year = Date.new(year + 1, 1, 1) %>
<% if Budget.budget_date_valid?(first_month_of_next_year, family: family) %>
<%= link_to picker_budgets_path(year: year + 1), data: { turbo_frame: "budget_picker" }, class: "p-2 flex items-center justify-center hover:bg-alpha-black-25 rounded-md" do %>
<%= lucide_icon "chevron-right", class: "w-5 h-5 shrink-0 text-secondary" %>
<% end %>
@ -29,17 +33,12 @@
</div>
<div class="grid grid-cols-3 gap-2 text-sm text-center font-medium">
<% Date::ABBR_MONTHNAMES.compact.each_with_index do |month_name, index| %>
<% month_number = index + 1 %>
<% start_date = Date.new(year, month_number) %>
<% budget = family.budgets.for_date(start_date) %>
<% Date::ABBR_MONTHNAMES.compact.each do |month_name| %>
<% date = Date.strptime("#{month_name}-#{year}", "%b-%Y") %>
<% param_key = Budget.date_to_param(date) %>
<% if budget %>
<%= link_to month_name, budget_path(budget), data: { turbo_frame: "_top" }, class: "block px-3 py-2 text-sm text-primary hover:bg-gray-100 rounded-md" %>
<% elsif start_date >= family.oldest_entry_date.beginning_of_month && start_date <= Date.current %>
<%= button_to budgets_path(budget: { start_date: start_date }), data: { turbo_frame: "_top" }, class: "block w-full px-3 py-2 text-primary hover:bg-gray-100 rounded-md" do %>
<%= month_name %>
<% end %>
<% if Budget.budget_date_valid?(date, family: family) %>
<%= link_to month_name, budget_path(param_key), data: { turbo_frame: "_top" }, class: "block px-3 py-2 text-sm text-primary hover:bg-gray-100 rounded-md" %>
<% else %>
<span class="px-3 py-2 text-subdued rounded-md"><%= month_name %></span>
<% end %>

View file

@ -7,7 +7,7 @@
<div class="flex items-start gap-4">
<div class="w-[300px] space-y-4">
<div class="h-[300px] bg-white rounded-xl shadow-xs p-8 border border-gray-100">
<div class="h-[300px] bg-white rounded-xl shadow-border-xs p-8">
<% if @budget.available_to_allocate.negative? %>
<%= render "budgets/over_allocation_warning", budget: @budget %>
<% else %>
@ -38,18 +38,18 @@
) %>
</div>
<div class="bg-white rounded-xl shadow-xs border border-gray-100">
<div class="bg-white rounded-xl shadow-border-xs">
<%= render selected_tab == "budgeted" ? "budgets/budgeted_summary" : "budgets/actuals_summary", budget: @budget %>
</div>
<% else %>
<div class="bg-white rounded-xl shadow-xs border border-gray-100">
<div class="bg-white rounded-xl shadow-border-xs">
<%= render "budgets/actuals_summary", budget: @budget %>
</div>
<% end %>
</div>
</div>
<div class="grow bg-white rounded-xl shadow-xs p-4 border border-gray-100">
<div class="grow bg-white rounded-xl shadow-border-xs p-4">
<div class="flex items-center justify-between mb-4">
<h2 class="text-lg font-medium">Categories</h2>

View file

@ -7,7 +7,7 @@
<p><%= categories.count %></p>
</div>
<div class="border border-alpha-black-25 rounded-md bg-white shadow-xs">
<div class="shadow-border-xs rounded-md bg-white">
<div class="overflow-hidden rounded-md">
<% Category::Group.for(categories).each_with_index do |group, idx| %>
<%= render group.category %>

View file

@ -1,44 +1,37 @@
<% content_for :sidebar do %>
<%= render "settings/nav" %>
<% end %>
<section class="space-y-4">
<header class="flex items-center justify-between">
<h1 class="text-primary text-xl font-medium"><%= t(".categories") %></h1>
<header class="flex items-center justify-between">
<h1 class="text-primary text-xl font-medium"><%= t(".categories") %></h1>
<%= link_to new_category_path, class: "btn btn--primary flex items-center gap-1 justify-center", data: { turbo_frame: :modal } do %>
<%= lucide_icon "plus", class: "w-5 h-5" %>
<p><%= t(".new") %></p>
<% end %>
</header>
<%= link_to new_category_path, class: "btn btn--primary flex items-center gap-1 justify-center", data: { turbo_frame: :modal } do %>
<%= lucide_icon "plus", class: "w-5 h-5" %>
<p><%= t(".new") %></p>
<% end %>
</header>
<div class="bg-white shadow-xs border border-alpha-black-25 rounded-xl p-4">
<% if @categories.any? %>
<div class="space-y-4">
<% if @categories.incomes.any? %>
<%= render "categories/category_list_group", title: t(".categories_incomes"), categories: @categories.incomes %>
<% end %>
<div class="bg-white shadow-border-xs rounded-xl p-4">
<% if @categories.any? %>
<div class="space-y-4">
<% if @categories.incomes.any? %>
<%= render "categories/category_list_group", title: t(".categories_incomes"), categories: @categories.incomes %>
<% end %>
<% if @categories.expenses.any? %>
<%= render "categories/category_list_group", title: t(".categories_expenses"), categories: @categories.expenses %>
<% end %>
</div>
<% else %>
<div class="flex justify-center items-center py-20">
<div class="text-center flex flex-col items-center max-w-[500px]">
<p class="text-sm text-secondary mb-4"><%= t(".empty") %></p>
<div class="flex items-center gap-2">
<%= button_to t(".bootstrap"), bootstrap_categories_path, class: "btn btn--primary" %>
<% if @categories.expenses.any? %>
<%= render "categories/category_list_group", title: t(".categories_expenses"), categories: @categories.expenses %>
<% end %>
</div>
<% else %>
<div class="flex justify-center items-center py-20">
<div class="text-center flex flex-col items-center max-w-[500px]">
<p class="text-sm text-secondary mb-4"><%= t(".empty") %></p>
<div class="flex items-center gap-2">
<%= button_to t(".bootstrap"), bootstrap_categories_path, class: "btn btn--primary" %>
<%= link_to new_category_path, class: "btn btn--outline flex items-center gap-1", data: { turbo_frame: "modal" } do %>
<%= lucide_icon("plus", class: "w-5 h-5") %>
<span><%= t(".new") %></span>
<% end %>
</div>
<%= link_to new_category_path, class: "btn btn--outline flex items-center gap-1", data: { turbo_frame: "modal" } do %>
<%= lucide_icon("plus", class: "w-5 h-5") %>
<span><%= t(".new") %></span>
<% end %>
</div>
</div>
<% end %>
</div>
<%= settings_nav_footer %>
</section>
</div>
<% end %>
</div>

View file

@ -43,7 +43,7 @@
<% end %>
</div>
<div class="bg-white border border-secondary rounded-xl shadow-xs divide-y divide-alpha-black-200">
<div class="bg-white shadow-border-xs rounded-xl divide-y divide-alpha-black-200">
<% @rows.each do |row| %>
<%= render "import/rows/form", row: row %>
<% end %>
@ -52,8 +52,8 @@
</div>
<div class="fixed bottom-0 left-1/2 -translate-x-1/2 w-full p-12">
<div class="border border-tertiary rounded-lg p-3 max-w-2xl mx-auto bg-white shadow-xs">
<%= render "application/pagination", pagy: @pagy %>
<div class="shadow-border-xs rounded-lg p-3 max-w-2xl mx-auto bg-white">
<%= render "shared/pagination", pagy: @pagy %>
</div>
</div>
</div>

View file

@ -27,7 +27,7 @@
<p class="justify-self-end"><%= t(".rows_label") %></p>
</div>
<div class="border border-alpha-black-25 rounded-md shadow-xs divide-y divide-alpha-black-100 text-sm">
<div class="shadow-border-xs rounded-md divide-y divide-alpha-black-100 text-sm">
<% mappings.sort_by(&:key).each do |mapping| %>
<div class="px-5 py-3 bg-white first:rounded-tl-xl first:rounded-tr-xl last:rounded-bl-xl last:rounded-br-xl">
<%= render partial: "import/mappings/form", locals: { mapping: mapping } %>

View file

@ -42,7 +42,7 @@
<% end %>
<% if import.complete? || import.revert_failed? %>
<%= button_to revert_import_path(import),
<%= button_to revert_import_path(import),
method: :put,
class: "block w-full py-2 px-3 space-x-2 text-orange-600 hover:bg-orange-50 flex items-center rounded-lg",
data: { turbo_confirm: true } do %>

View file

@ -12,7 +12,7 @@
<p class="justify-self-end">count</p>
</div>
<div class="bg-white border border-alpha-black-25 rounded-lg shadow-xs text-sm">
<div class="bg-white shadow-border-xs rounded-lg text-sm">
<% import.dry_run.each do |key, count| %>
<% resource = dry_run_resource(key) %>

View file

@ -1,6 +1,6 @@
<%# locals: (headers: [], rows: [], caption: nil) %>
<div class="overflow-x-auto">
<div class="border border-secondary rounded-md shadow-xs text-sm bg-white w-full">
<div class="border border-secondary rounded-md shadow-border-xs text-sm bg-white w-full">
<div class="grid border-b border-b-alpha-black-200" style="grid-template-columns: repeat(<%= headers.length %>, minmax(0, 1fr))">
<% headers.each_with_index do |header, index| %>
<div class="

View file

@ -1,29 +1,22 @@
<% content_for :sidebar do %>
<%= render "settings/nav" %>
<% end %>
<div class="flex items-center justify-between">
<h1 class="text-xl font-medium text-primary"><%= t(".title") %></h1>
<div class="space-y-4">
<div class="flex items-center justify-between">
<h1 class="text-xl font-medium text-primary"><%= t(".title") %></h1>
<%= link_to new_import_path, class: "btn btn--primary flex items-center gap-2", data: { turbo_frame: :modal } do %>
<%= lucide_icon("plus", class: "w-5 h-5") %>
<span><%= t(".new") %></span>
<% end %>
</div>
<div class="bg-white shadow-xs border border-alpha-black-25 rounded-xl p-4">
<% if @imports.empty? %>
<%= render partial: "imports/empty" %>
<% else %>
<div class="rounded-xl bg-gray-25 p-1">
<h2 class="uppercase px-4 py-2 text-secondary text-xs"><%= t(".imports") %> · <%= @imports.size %></h2>
<div class="border border-alpha-gray-100 rounded-lg bg-white shadow-xs">
<%= render partial: "imports/import", collection: @imports.ordered %>
</div>
</div>
<% end %>
</div>
<%= settings_nav_footer %>
<%= link_to new_import_path, class: "btn btn--primary flex items-center gap-2", data: { turbo_frame: :modal } do %>
<%= lucide_icon("plus", class: "w-5 h-5") %>
<span><%= t(".new") %></span>
<% end %>
</div>
<div class="bg-white shadow-border-xs rounded-xl p-4">
<% if @imports.empty? %>
<%= render partial: "imports/empty" %>
<% else %>
<div class="rounded-xl bg-gray-25 p-1">
<h2 class="uppercase px-4 py-2 text-secondary text-xs"><%= t(".imports") %> · <%= @imports.size %></h2>
<div class="border border-alpha-black-100 rounded-lg bg-white shadow-xs">
<%= render partial: "imports/import", collection: @imports.ordered %>
</div>
</div>
<% end %>
</div>

View file

@ -13,7 +13,7 @@
<div class="rounded-xl bg-gray-25 p-1">
<h3 class="uppercase text-secondary text-xs font-medium px-3 py-1.5"><%= t(".sources") %></h3>
<ul class="bg-white border border-alpha-black-25 rounded-lg shadow-xs">
<ul class="bg-white shadow-border-xs rounded-lg">
<li>
<% if @pending_import.present? && (params[:type].nil? || params[:type] == @pending_import.type) %>
<%= link_to import_path(@pending_import), class: "flex items-center justify-between p-4 group cursor-pointer", data: { turbo: false } do %>

View file

@ -10,8 +10,8 @@
tooltip: render(
"investments/value_tooltip",
balance: @account.balance_money,
holdings: @account.balance - @account.cash_balance,
cash: @account.cash_balance
holdings: @account.balance_money - @account.cash_balance_money,
cash: @account.cash_balance_money
) %>
<div class="min-h-[800px]">

View file

@ -1,118 +0,0 @@
<div class="flex items-center justify-between">
<%= link_to root_path do %>
<%= image_tag "logo.svg", alt: "Maybe", class: "h-[22px]" %>
<% end %>
<div id="user-menu" data-controller="menu">
<button data-menu-target="button">
<div class="w-9 h-9">
<%= render "settings/user_avatar", user: Current.user, variant: :small %>
</div>
</button>
<div data-menu-target="content" class="hidden absolute w-[240px] z-10 left-[255px] top-[72px] bg-white rounded-sm shadow-xs border border-alpha-black-25">
<div class="p-3 flex items-center gap-3">
<div class="w-9 h-9 shrink-0">
<%= render "settings/user_avatar", user: Current.user, variant: :small, lazy: true %>
</div>
<div class="overflow-hidden text-ellipsis">
<span class="text-primary font-medium text-sm"><%= Current.user.display_name %></span>
<% if Current.user.display_name != Current.user.email %>
<span class="text-secondary text-sm"><%= Current.user.email %></span>
<% end %>
</div>
</div>
<div class="border-t border-b border-tertiary p-1">
<%= link_to settings_profile_path(return_to: request.fullpath), class: "flex gap-2 items-center hover:bg-gray-50 rounded-lg px-3 py-2" do %>
<%= lucide_icon("settings", class: "w-5 h-5 text-secondary shrink-0") %>
<span class="text-primary text-sm">Settings</span>
<% end %>
<div class="flex justify-between items-center gap-2">
<div class="flex items-center gap-2 rounded-lg px-3 py-2">
<%= lucide_icon("app-window", class: "w-5 h-5 text-secondary shrink-0") %>
<div>
<span class="text-primary text-sm">Demo mode</span>
<span class="text-secondary text-xs block">Coming soon...</span>
</div>
</div>
<%# TODO: This will be a form toggle when implemented %>
<div class="relative inline-block cursor-not-allowed" title="Coming soon...">
<label class="cursor-not-allowed block bg-gray-100 w-9 h-5 rounded-full after:content-[''] after:block after:absolute after:top-0.5 after:left-0.5 after:bg-white after:w-4 after:h-4 after:rounded-full after:transition-transform after:duration-300 after:ease-in-out peer-checked:bg-green-600 peer-checked:after:translate-x-4"></label>
</div>
</div>
</div>
<div class="border-t border-b border-tertiary p-1">
<%= link_to changelog_path, class: "flex gap-2 items-center hover:bg-gray-50 rounded-lg px-3 py-2" do %>
<%= lucide_icon("box", class: "w-5 h-5 text-secondary shrink-0") %>
<span class="text-primary text-sm">Changelog</span>
<% end %>
<%= link_to feedback_path, class: "flex gap-2 items-center hover:bg-gray-50 rounded-lg px-3 py-2" do %>
<%= lucide_icon("megaphone", class: "w-5 h-5 text-secondary shrink-0") %>
<span class="text-primary text-sm">Feedback</span>
<% end %>
<% if self_hosted? %>
<%= link_to "https://link.maybe.co/discord", class: "flex gap-2 items-center hover:bg-gray-50 rounded-lg px-3 py-2" do %>
<%= lucide_icon("message-square-more", class: "w-5 h-5 text-secondary shrink-0") %>
<span class="text-primary text-sm">Contact</span>
<% end %>
<% else %>
<%= link_to "mailto:hello@maybefinance.com", class: "flex gap-2 items-center hover:bg-gray-50 rounded-lg px-3 py-2", onclick: "Intercom('showNewMessage'); return false;" do %>
<%= lucide_icon("message-square-more", class: "w-5 h-5 text-secondary shrink-0") %>
<span class="text-primary text-sm">Contact</span>
<% end %>
<% end %>
</div>
<div class="p-1">
<%= button_to session_path(Current.session), method: :delete, class: "w-full text-red-400 flex gap-1 items-center hover:bg-gray-50 rounded-lg px-3 py-2" do %>
<%= lucide_icon("log-out", class: "w-5 h-5 shrink-0") %>
<span class="text-sm">Logout</span>
<% end %>
</div>
</div>
</div>
</div>
<nav>
<ul class="mt-6 space-y-1">
<li>
<%= sidebar_link_to t(".dashboard"), root_path, icon: "layout-grid" %>
</li>
<li>
<%= sidebar_link_to t(".accounts"), summary_accounts_path, icon: "layers" %>
</li>
<li>
<%= sidebar_link_to t(".transactions"), transactions_path, icon: "credit-card" %>
</li>
<li>
<%= sidebar_link_to t(".budgeting"), budgets_path, icon: "map" %>
</li>
</ul>
</nav>
<div class="flex flex-col mt-6">
<div class="flex items-center justify-between px-3 py-2 mb-2">
<div class="flex items-center gap-2 text-xs uppercase text-secondary">
<%= link_to accounts_path, class: "text-xs uppercase text-secondary font-bold tracking-wide" do %>
<%= t(".portfolio") %>
<% end %>
<span class="font-bold tracking-wide">&bull;</span>
<%= form_with url: list_accounts_path, method: :get, data: { controller: Current.family.accounts.any? ? "auto-submit-form" : nil, turbo_frame: "account-list" } do |form| %>
<%= period_select form: form, selected: "last_30_days", classes: "w-full border-none pl-2 pr-7 text-xs bg-transparent gap-1 cursor-pointer font-semibold tracking-wide focus:outline-hidden focus:ring-0" %>
<% end %>
</div>
<%= link_to new_account_path, id: "sidebar-new-account", class: "block hover:bg-gray-100 font-semibold text-primary flex items-center rounded p-1", title: t(".new_account"), data: { turbo_frame: "modal" } do %>
<%= lucide_icon("plus", class: "w-5 h-5 text-secondary") %>
<% end %>
</div>
<%= turbo_frame_tag "account-list", target: "_top" do %>
<% if Current.family.accounts.any? %>
<% account_groups.each do |group| %>
<%= render "accounts/account_list", group: group %>
<% end %>
<% else %>
<%= link_to new_account_path, class: "flex items-center min-h-10 gap-4 px-3 py-2 mb-1 text-secondary text-sm font-medium rounded-[10px] hover:bg-gray-100", data: { turbo_frame: "modal" } do %>
<%= lucide_icon("plus", class: "w-5 h-5") %>
<%= tag.p t(".new_account") %>
<% end %>
<% end %>
<% end %>
</div>

View file

@ -1,57 +1,51 @@
<!DOCTYPE html>
<html class="font-sans text-primary h-full <%= @os %>" lang="en">
<head>
<title><%= content_for(:title) || "Maybe" %></title>
<%= csrf_meta_tags %>
<%= csp_meta_tag %>
<%= stylesheet_link_tag "tailwind", "data-turbo-track": "reload" %>
<%= stylesheet_link_tag "application", "data-turbo-track": "reload" %>
<%= javascript_include_tag "https://cdn.plaid.com/link/v2/stable/link-initialize.js" %>
<%= combobox_style_tag %>
<%= javascript_importmap_tags %>
<%= turbo_refreshes_with method: :morph, scroll: :preserve %>
<meta name="viewport"
content="viewport-fit=cover, width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no">
<meta name="mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<meta name="apple-mobile-web-app-title" content="Maybe">
<meta name="msapplication-TileColor" content="#ffffff">
<meta name="theme-color" content="#ffffff">
<%= yield :head %>
</head>
<body class="h-full">
<%= render "impersonation_sessions/super_admin_bar" if Current.true_user&.super_admin? && show_super_admin_bar? %>
<%= render "impersonation_sessions/approval_bar" if Current.true_user&.impersonated_support_sessions&.initiated&.any? %>
<div class="fixed z-50 bottom-6 left-6">
<div id="notification-tray" class="space-y-1">
<%= render_flash_notifications %>
<% if Current.family&.syncing? %>
<%= render "shared/syncing_notice" %>
<%= render "layouts/shared/htmldoc" do %>
<div class="flex h-full bg-gray-50">
<nav class="flex flex-col shrink-0 w-[84px] py-4 mr-3">
<div class="pl-2 mb-3">
<%= link_to root_path, class: "block" do %>
<%= image_tag "logomark-color.svg", class: "w-9 h-9 mx-auto" %>
<% end %>
</div>
</div>
<%= family_notifications_stream %>
<%= family_stream %>
<ul class="space-y-0.5">
<li>
<%= render "layouts/sidebar/nav_item", name: "Home", path: root_path, icon_key: "pie-chart" %>
</li>
<%= content_for?(:content) ? yield(:content) : yield %>
<li>
<%= render "layouts/sidebar/nav_item", name: "Transactions", path: transactions_path, icon_key: "credit-card" %>
</li>
<%= turbo_frame_tag "modal" %>
<%= turbo_frame_tag "drawer" %>
<li>
<%= render "layouts/sidebar/nav_item", name: "Budgets", path: budgets_path, icon_key: "layout-grid" %>
</li>
</ul>
<%= render "shared/confirm_modal" %>
<div class="pl-2 mt-auto mx-auto">
<%= render "users/user_menu", user: Current.user %>
</div>
</nav>
<% if self_hosted? && Current.user&.onboarded_at.present? %>
<%= render "shared/app_version" %>
<%= tag.div class: class_names("py-4 shrink-0 h-full overflow-y-auto transition-all duration-300", Current.user.show_sidebar? ? "w-[260px]" : "w-0"), data: { sidebar_target: "panel" } do %>
<% if content_for?(:sidebar) %>
<%= yield :sidebar %>
<% else %>
<div id="account-sidebar-tabs" data-turbo-permanent>
<%= render "accounts/account_sidebar_tabs", family: Current.family %>
</div>
<% end %>
<% end %>
</body>
</html>
<%= tag.main class: class_names("px-10 py-4 grow h-full", require_upgrade? ? "relative overflow-hidden" : "overflow-y-auto") do %>
<% if require_upgrade? %>
<div class="absolute inset-0 px-10 h-full w-full z-50">
<%= render "shared/subscribe_modal" %>
</div>
<% end %>
<%= tag.div class: class_names("mx-auto w-full h-full", Current.user.show_sidebar? ? "max-w-4xl" : "max-w-5xl"), data: { sidebar_target: "content" } do %>
<%= yield %>
<% end %>
<% end %>
</div>
<% end %>

View file

@ -1,35 +1,35 @@
<%= content_for :content do %>
<div class="flex flex-col h-screen px-6 py-12 bg-gray-25">
<div class="grow flex flex-col justify-center">
<div class="sm:mx-auto sm:w-full sm:max-w-md">
<div class="flex justify-center mb-6">
<%= image_tag "logo-color.png", class: "w-16 mb-6" %>
<%= render "layouts/shared/htmldoc" do %>
<div class="flex flex-col h-dvh">
<div class="flex flex-col h-screen px-6 py-12 bg-gray-25">
<div class="grow flex flex-col justify-center">
<div class="sm:mx-auto sm:w-full sm:max-w-md">
<div class="flex justify-center mb-6">
<%= image_tag "logo-color.png", class: "w-16 mb-6" %>
</div>
<div class="space-y-2">
<h2 class="text-3xl font-medium text-primary text-center">
<%= content_for?(:header_title) ? yield(:header_title).html_safe : t(".your_account") %>
</h2>
<% if controller_name == "sessions" %>
<p class="text-sm text-center">
<%= tag.span t(".no_account"), class: "text-secondary" %> <%= link_to t(".sign_up"), new_registration_path, class: "font-medium text-primary hover:underline transition" %>
</p>
<% elsif controller_name == "registrations" %>
<p class="text-sm text-center text-gray-600">
<%= t(".existing_account") %> <%= link_to t(".sign_in"), new_session_path, class: "font-medium text-primary hover:underline transition" %>
</p>
<% end %>
</div>
</div>
<div class="space-y-2">
<h2 class="text-3xl font-medium text-primary text-center">
<%= content_for?(:header_title) ? yield(:header_title).html_safe : t(".your_account") %>
</h2>
<% if controller_name == "sessions" %>
<p class="text-sm text-center">
<%= tag.span t(".no_account"), class: "text-secondary" %> <%= link_to t(".sign_up"), new_registration_path, class: "font-medium text-primary hover:underline transition" %>
</p>
<% elsif controller_name == "registrations" %>
<p class="text-sm text-center text-gray-600">
<%= t(".existing_account") %> <%= link_to t(".sign_in"), new_session_path, class: "font-medium text-primary hover:underline transition" %>
</p>
<% end %>
<div class="mt-8 sm:mx-auto sm:w-full sm:max-w-lg">
<%= yield %>
</div>
</div>
<div class="mt-8 sm:mx-auto sm:w-full sm:max-w-lg">
<%= yield %>
</div>
<%= render "layouts/shared/footer" %>
</div>
<%= render "layouts/footer" %>
</div>
<% end %>
<%= render template: "layouts/application" %>

View file

@ -1,4 +1,4 @@
<%= content_for :content do %>
<%= render "layouts/shared/htmldoc" do %>
<div class="flex flex-col h-dvh">
<header class="flex items-center justify-between p-8">
<%= link_to content_for(:previous_path) || imports_path do %>
@ -19,5 +19,3 @@
</main>
</div>
<% end %>
<%= render template: "layouts/application" %>

View file

@ -1,15 +1,15 @@
<%= drawer do %>
<article class="prose">
<%= tag.h2 do %>
<%= yield :title %>
<% end %>
<%= render "layouts/shared/htmldoc" do %>
<%= drawer do %>
<article class="prose">
<%= tag.h2 do %>
<%= yield :title %>
<% end %>
<%= tag.h3 t(".description") %>
<%= yield :description %>
<%= tag.h3 "Issue Description" %>
<%= yield :description %>
<%= tag.h3 t(".action") %>
<%= yield :action %>
</article>
<%= tag.h3 "How to fix this issue" %>
<%= yield :action %>
</article>
<% end %>
<% end %>
<%= render template: "layouts/application" %>

View file

@ -0,0 +1,3 @@
<%= render "layouts/shared/htmldoc" do %>
<%= yield %>
<% end %>

View file

@ -0,0 +1,25 @@
<%= render "layouts/shared/htmldoc" do %>
<div class="flex h-full bg-gray-25">
<div class="p-4 w-[260px] shrink-0 h-full overflow-y-auto">
<%= render "settings/settings_nav" %>
</div>
<main class="py-4 px-10 grow flex h-full overflow-y-auto">
<div class="relative max-w-4xl mx-auto flex flex-col w-full h-full">
<div class="grow space-y-4 overflow-y-auto -mx-1 px-1 pb-12">
<% if content_for?(:page_title) %>
<h1 class="text-primary text-xl font-medium">
<%= content_for :page_title %>
</h1>
<% end %>
<%= yield %>
</div>
<div class="mt-4">
<%= settings_nav_footer %>
</div>
</div>
</main>
</div>
<% end %>

View file

@ -0,0 +1,6 @@
<%= turbo_frame_tag "modal" %>
<%= turbo_frame_tag "drawer" %>
<%= render "shared/confirm_modal" %>
<%= render "impersonation_sessions/super_admin_bar" if Current.true_user&.super_admin? && show_super_admin_bar? %>
<%= render "impersonation_sessions/approval_bar" if Current.true_user&.impersonated_support_sessions&.initiated&.any? %>

View file

@ -0,0 +1,26 @@
<head>
<title><%= content_for(:title) || "Maybe" %></title>
<%= csrf_meta_tags %>
<%= csp_meta_tag %>
<%= stylesheet_link_tag "tailwind", "data-turbo-track": "reload" %>
<%= stylesheet_link_tag "application", "data-turbo-track": "reload" %>
<%= javascript_include_tag "https://cdn.plaid.com/link/v2/stable/link-initialize.js" %>
<%= combobox_style_tag %>
<%= javascript_importmap_tags %>
<%= turbo_refreshes_with method: :morph, scroll: :preserve %>
<meta name="viewport"
content="viewport-fit=cover, width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no">
<meta name="mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<meta name="apple-mobile-web-app-title" content="Maybe">
<meta name="msapplication-TileColor" content="#ffffff">
<meta name="theme-color" content="#ffffff">
<%= yield :head %>
</head>

View file

@ -0,0 +1,35 @@
<!DOCTYPE html>
<html class="h-full text-primary font-sans <%= @os %>" lang="en">
<head>
<%= render "layouts/shared/head" %>
<%= yield :head %>
</head>
<body class="h-full antialiased" data-controller="sidebar" data-sidebar-user-id-value="<%= Current.user&.id %>">
<div class="fixed z-50 bottom-6 left-6">
<div id="notification-tray" class="space-y-1">
<%= render_flash_notifications %>
<% if Current.family&.syncing? %>
<%= render "shared/syncing_notice" %>
<% end %>
</div>
</div>
<%= family_notifications_stream %>
<%= family_stream %>
<% if self_hosted? && (upgrade = get_upgrade_for_notification(Current.user, Setting.upgrades_mode)) %>
<%= render partial: "shared/upgrade_notification", locals: { upgrade: upgrade } %>
<% end %>
<%= turbo_frame_tag "modal" %>
<%= turbo_frame_tag "drawer" %>
<%= render "shared/confirm_modal" %>
<%= render "impersonation_sessions/super_admin_bar" if Current.true_user&.super_admin? && show_super_admin_bar? %>
<%= render "impersonation_sessions/approval_bar" if Current.true_user&.impersonated_support_sessions&.initiated&.any? %>
<%= yield %>
</body>
</html>

View file

@ -0,0 +1,16 @@
<div class="fixed z-50 bottom-6 left-6">
<div id="notification-tray" class="space-y-1">
<%= render_flash_notifications %>
<% if Current.family&.syncing? %>
<%= render "shared/syncing_notice" %>
<% end %>
</div>
</div>
<%= family_notifications_stream %>
<%= family_stream %>
<% if self_hosted? && (upgrade = get_upgrade_for_notification(Current.user, Setting.upgrades_mode)) %>
<%= render partial: "shared/upgrade_notification", locals: { upgrade: upgrade } %>
<% end %>

View file

@ -0,0 +1,17 @@
<%# locals: (name:, path:, icon_key:) %>
<%= link_to path, class: "space-y-1 py-1 group block" do %>
<div class="grow flex gap-1 items-center">
<%= tag.div class: class_names("w-1 h-4 rounded-tr-sm rounded-br-sm", "bg-gray-900" => page_active?(path)) %>
<%= tag.div class: class_names("w-8 h-8 flex items-center justify-center mx-auto rounded-lg", page_active?(path) ? "bg-white shadow-xs text-primary" : "group-hover:bg-gray-100 text-secondary") do %>
<%= icon(icon_key) %>
<% end %>
</div>
<div class="grow pl-2">
<%= tag.p class: class_names("text-center font-medium text-[11px]", page_active?(path) ? "text-primary" : "text-secondary") do %>
<%= name %>
<% end %>
</div>
<% end %>

View file

@ -1,25 +0,0 @@
<%= content_for :content do %>
<div class="flex h-full bg-gray-25">
<div class="p-6 pb-20 w-[368px] shrink-0 h-full overflow-y-auto">
<% if content_for?(:sidebar) %>
<%= yield :sidebar %>
<% else %>
<%= render "layouts/sidebar" %>
<% end %>
</div>
<main class="grow px-20 py-6 h-full <%= require_upgrade? ? "relative overflow-hidden" : "overflow-y-auto" %>">
<% if require_upgrade? %>
<%= render "shared/subscribe_modal" %>
<% end %>
<%= yield %>
</main>
</div>
<% if (upgrade = get_upgrade_for_notification(Current.user, Setting.upgrades_mode)) %>
<%= render partial: "shared/upgrade_notification", locals: { upgrade: upgrade } %>
<% end %>
<% end %>
<%= render template: "layouts/application" %>

View file

@ -1,4 +1,4 @@
<%= content_for :content do %>
<%= render "layouts/shared/htmldoc" do %>
<div class="flex flex-col h-dvh">
<header class="flex items-center justify-between p-8">
<%= link_to content_for(:previous_path) || root_path do %>
@ -19,5 +19,3 @@
</main>
</div>
<% end %>
<%= render template: "layouts/application" %>

View file

@ -1,44 +1,36 @@
<% content_for :sidebar do %>
<%= render "settings/nav" %>
<% end %>
<header class="flex items-center justify-between">
<h1 class="text-primary text-xl font-medium"><%= t(".title") %></h1>
<section class="space-y-4">
<header class="flex items-center justify-between">
<h1 class="text-primary text-xl font-medium"><%= t(".title") %></h1>
<%= link_to new_merchant_path, class: "btn btn--primary flex items-center gap-1 justify-center", data: { turbo_frame: :modal } do %>
<%= lucide_icon "plus", class: "w-5 h-5" %>
<p><%= t(".new") %></p>
<% end %>
</header>
<%= link_to new_merchant_path, class: "btn btn--primary flex items-center gap-1 justify-center", data: { turbo_frame: :modal } do %>
<%= lucide_icon "plus", class: "w-5 h-5" %>
<p><%= t(".new") %></p>
<% end %>
</header>
<div class="bg-white shadow-border-xs rounded-xl p-4">
<% if @merchants.any? %>
<div class="rounded-xl bg-gray-25 space-y-1 p-1">
<div class="flex items-center gap-1.5 px-4 py-2 text-xs font-medium text-secondary uppercase">
<p><%= t(".title") %></p>
<span class="text-subdued">&middot;</span>
<p><%= @merchants.count %></p>
</div>
<div class="bg-white shadow-xs border border-alpha-black-25 rounded-xl p-4">
<% if @merchants.any? %>
<div class="rounded-xl bg-gray-25 space-y-1 p-1">
<div class="flex items-center gap-1.5 px-4 py-2 text-xs font-medium text-secondary uppercase">
<p><%= t(".title") %></p>
<span class="text-subdued">&middot;</span>
<p><%= @merchants.count %></p>
</div>
<div class="border border-alpha-black-25 rounded-md bg-white shadow-xs">
<div class="overflow-hidden rounded-md">
<%= render partial: @merchants, spacer_template: "merchants/ruler" %>
</div>
<div class="border border-alpha-black-25 rounded-md bg-white shadow-border-xs">
<div class="overflow-hidden rounded-md">
<%= render partial: @merchants, spacer_template: "merchants/ruler" %>
</div>
</div>
<% else %>
<div class="flex justify-center items-center py-20">
<div class="text-center flex flex-col items-center max-w-[300px]">
<p class="text-primary mb-1 font-medium text-sm"><%= t(".empty") %></p>
<%= link_to new_merchant_path, class: "w-fit flex text-white text-sm font-medium items-center gap-1 bg-gray-900 rounded-lg p-2 pr-3", data: { turbo_frame: "modal" } do %>
<%= lucide_icon("plus", class: "w-5 h-5") %>
<span><%= t(".new") %></span>
<% end %>
</div>
</div>
<% else %>
<div class="flex justify-center items-center py-20">
<div class="text-center flex flex-col items-center max-w-[300px]">
<p class="text-primary mb-1 font-medium text-sm"><%= t(".empty") %></p>
<%= link_to new_merchant_path, class: "w-fit flex text-white text-sm font-medium items-center gap-1 bg-gray-900 rounded-lg p-2 pr-3", data: { turbo_frame: "modal" } do %>
<%= lucide_icon("plus", class: "w-5 h-5") %>
<span><%= t(".new") %></span>
<% end %>
</div>
<% end %>
</div>
<%= settings_nav_footer %>
</div>
<% end %>
</div>

View file

@ -4,7 +4,7 @@
%>
<% content_for :sidebar do %>
<%= render "settings/nav" %>
<%= render "settings/settings_nav" %>
<% end %>
<div class="space-y-4">
@ -27,4 +27,3 @@
<%= settings_nav_footer %>
</div>

View file

@ -4,7 +4,7 @@
%>
<% content_for :sidebar do %>
<%= render "settings/nav" %>
<%= render "settings/settings_nav" %>
<% end %>
<div class="space-y-4">

View file

@ -11,4 +11,4 @@
label: t(".page_title") %>
<%= form.submit t(".verify_button") %>
<% end %>
<% end %>

View file

@ -9,7 +9,7 @@
</div>
<div class="p-1 bg-alpha-black-25 mb-2 rounded-lg">
<div class="bg-white p-4 rounded-lg flex gap-8 shadow-xs">
<div class="bg-white p-4 rounded-lg flex gap-8 shadow-border-xs">
<div class="space-y-2">
<%= tag.p t(".example"), class: "text-secondary text-sm" %>
<%= tag.p "$2,323.25", class: "text-primary font-medium text-2xl" %>
@ -38,13 +38,15 @@
{ date: Date.current, value: 265 }
] %>
<% placeholder_series = Series.from_raw_values(placeholder_series_data) %>
<div class="flex items-center w-2/5">
<div class="h-12 w-full">
<div
id="previewChart"
class="h-full w-full"
data-controller="time-series-chart"
data-time-series-chart-data-value="<%= TimeSeries.new(placeholder_series_data).to_json %>"
data-time-series-chart-data-value="<%= placeholder_series.to_json %>"
data-time-series-chart-use-labels-value="false"
data-time-series-chart-use-tooltip-value="false"></div>
</div>
@ -84,5 +86,5 @@
</div>
</div>
<%= render "layouts/footer" %>
<%= render "layouts/shared/footer" %>
</div>

View file

@ -38,5 +38,5 @@
</div>
</div>
<%= render "layouts/footer" %>
<%= render "layouts/shared/footer" %>
</div>

View file

@ -1,48 +0,0 @@
<%# locals: (accountable_group:) %>
<% text_class = accountable_text_class(accountable_group.name) %>
<details class="open:bg-gray-25 group">
<summary class="flex p-4 items-center w-full rounded-lg font-medium hover:bg-gray-50 text-secondary text-sm font-medium cursor-pointer">
<%= lucide_icon("chevron-down", class: "hidden group-open:block w-5 h-5") %>
<%= lucide_icon("chevron-right", class: "group-open:hidden w-5 h-5") %>
<div class="ml-4 h-2.5 w-2.5 rounded-full <%= accountable_bg_class(accountable_group.name) %>"></div>
<p class="text-primary ml-2"><%= to_accountable_title(Accountable.from_type(accountable_group.name)) %></p>
<span class="mx-1">&middot;</span>
<div><%= accountable_group.children.count %></div>
<div class="ml-auto text-right flex items-center gap-10 text-sm font-medium text-primary">
<div class="flex items-center justify-end gap-2 w-24">
<%= render partial: "shared/progress_circle", locals: { progress: accountable_group.percent_of_total, text_class: text_class } %>
<p><%= accountable_group.percent_of_total.round(1) %>%</p>
</div>
<div class="w-24">
<p><%= format_money accountable_group.sum %></p>
</div>
<div class="w-40">
<%= render partial: "shared/trend_change", locals: { trend: accountable_group.series.trend } %>
</div>
</div>
</summary>
<div class="px-4 py-3 space-y-4">
<% accountable_group.children.map do |account_value_node| %>
<div class="flex items-center justify-between text-sm font-medium text-primary">
<div class="flex items-center gap-4 overflow-hidden">
<%= render "accounts/logo", account: account_value_node.original, size: "sm" %>
<div class="truncate">
<p><%= account_value_node.name %></p>
</div>
</div>
<div class="flex gap-10 items-center text-right">
<div class="flex items-center justify-end gap-2 w-24">
<%= render partial: "shared/progress_circle", locals: { progress: account_value_node.percent_of_total, text_class: text_class } %>
<p><%= account_value_node.percent_of_total %>%</p>
</div>
<div class="w-24">
<p><%= format_money account_value_node.original.balance_money %></p>
</div>
<div class="w-40">
<%= render partial: "shared/trend_change", locals: { trend: account_value_node.original.series.trend } %>
</div>
</div>
</div>
<% end %>
</div>
</details>

View file

@ -1,17 +0,0 @@
<%# locals: (account_groups:) %>
<div class="space-y-4">
<div class="flex gap-1">
<% account_groups.sort_by(&:percent_of_total).reverse.each do |group| %>
<div class="h-1.5 rounded-sm w-12 <%= accountable_bg_class(group.name) %>" style="width: <%= group.percent_of_total %>%;"></div>
<% end %>
</div>
<div class="flex gap-4">
<% account_groups.sort_by(&:percent_of_total).reverse.each do |group| %>
<div class="flex items-center gap-2 text-sm">
<div class="h-2.5 w-2.5 rounded-full <%= accountable_bg_class(group.name) %>"></div>
<p class="text-secondary"><%= to_accountable_title(Accountable.from_type(group.name)) %></p>
<p class="text-black"><%= group.percent_of_total.round(1) %>%</p>
</div>
<% end %>
</div>
</div>

View file

@ -1,20 +0,0 @@
<%# locals: (account_groups:) %>
<div class="bg-gray-25 p-1 rounded-xl">
<div class="px-4 py-2 flex items-center uppercase text-xs font-medium text-secondary">
<div>Name</div>
<div class="ml-auto text-right flex items-center gap-10">
<div class="w-24">
<p>% of total</p>
</div>
<div class="w-24">
<p>Value</p>
</div>
<div class="w-40">
<p>Change</p>
</div>
</div>
</div>
<div class="bg-white border border-alpha-black-25 shadow-xs rounded-lg divide-y divide-alpha-black-50">
<%= render partial: "pages/account_group_disclosure", collection: account_groups.sort_by(&:name), as: :accountable_group %>
</div>
</div>

View file

@ -1,30 +1,21 @@
<% content_for :sidebar do %>
<%= render "settings/nav" %>
<% end %>
<%= content_for :page_title, t(".title") %>
<div class="space-y-4 flex flex-col h-full">
<h1 class="text-primary text-xl font-medium mb-4"><%= t(".title") %></h1>
<div class="bg-white shadow-xs border border-alpha-black-25 rounded-xl p-4 grow overflow-y-auto">
<div class="flex justify-between gap-4 mb-12 last:mb-0">
<div class="w-1/3">
<div class="px-3 flex items-center gap-3">
<div class="text-white shrink-0 w-9 h-9">
<%= image_tag @release_notes[:avatar], class: "rounded-full w-full h-full object-cover" %>
</div>
<div>
<a class="text-primary font-medium text-sm" href="https://github.com/<%= @release_notes[:username] %>"><%= "@#{@release_notes[:username]}" %></a>
<div class="text-secondary text-sm"><%= @release_notes[:published_at].strftime("%B %d, %Y") %></div>
</div>
<div class="bg-white shadow-border-xs rounded-xl p-4 grow overflow-y-auto">
<div class="flex justify-between gap-4 mb-12 last:mb-0">
<div class="w-1/3">
<div class="px-3 flex items-center gap-3">
<div class="text-white shrink-0 w-9 h-9">
<%= image_tag @release_notes[:avatar], class: "rounded-full w-full h-full object-cover" %>
</div>
<div>
<a class="text-primary font-medium text-sm" href="https://github.com/<%= @release_notes[:username] %>"><%= "@#{@release_notes[:username]}" %></a>
<div class="text-secondary text-sm"><%= @release_notes[:published_at].strftime("%B %d, %Y") %></div>
</div>
</div>
<div class="w-2/3 text-secondary text-sm prose prose--github-release-notes">
<h2 class="mb-5 text-xl text-primary"><%= @release_notes[:name] %></h2>
<%= @release_notes[:body].html_safe %>
</div>
</div>
<div class="w-2/3 text-secondary text-sm prose prose--github-release-notes">
<h2 class="mb-5 text-xl text-primary"><%= @release_notes[:name] %></h2>
<%= @release_notes[:body].html_safe %>
</div>
</div>
<div class="mt-auto">
<%= settings_nav_footer %>
</div>
</div>

View file

@ -1,203 +1,28 @@
<div class="space-y-4">
<% if self_hosted? %>
<% if Current.family&.synth_overage? %>
<div class="bg-yellow-100 border border-yellow-400 text-yellow-700 px-4 py-3 rounded relative" role="alert">
Your Synth API credit limit has been exceeded. Please visit your <a href="https://dashboard.synthfinance.com/settings" class="font-medium underline hover:text-yellow-900">Synth billing settings</a> to upgrade your plan or wait for your credits to reset.
</div>
<% elsif !Current.family&.synth_valid? %>
<div class="bg-yellow-100 border border-yellow-400 text-yellow-700 px-4 py-3 rounded relative" role="alert">
Your Synth API Key is invalid. Please visit your <a href="https://dashboard.synthfinance.com/dashboard" class="font-medium underline hover:text-yellow-900">Synth dashboard</a> and verify that your API key is correct.
</div>
<% end %>
<% end %>
<header class="flex items-center justify-between">
<div>
<h1 class="sr-only"><%= t(".title") %></h1>
<p class="text-xl font-medium text-primary mb-1">
<%= Current.user.first_name.present? ? t(".greeting", name: Current.user.first_name ) : t(".fallback_greeting") %>
</p>
<% unless @accounts.blank? %>
<p class="text-secondary text-sm"><%= t(".subtitle") %></p>
<% end %>
</div>
<div class="w-full space-y-6 pb-24">
<header class="space-y-6">
<nav class="flex items-center gap-2">
<button data-action="sidebar#toggle" class="w-9 h-9 inline-flex rounded-lg items-center justify-center hover:bg-gray-100 cursor-pointer">
<%= icon("panel-left", color: "gray") %>
</button>
<div class="flex items-center gap-2">
<%= contextual_menu do %>
<div class="w-48 p-1 text-sm leading-6 text-primary bg-white shadow-lg shrink rounded-xl ring-1 ring-gray-900/5">
<%= contextual_menu_modal_action_item t(".import"), new_import_path, icon: "hard-drive-upload" %>
</div>
<% end %>
<span class="text-sm text-gray-500 font-medium">Home</span>
<%= link_to new_account_path, class: "flex items-center gap-1 btn btn--primary", data: { turbo_frame: "modal" } do %>
<%= lucide_icon("plus", class: "w-5 h-5") %>
<span><%= t(".new") %></span>
<% end %>
<%= icon("chevron-right", color: "gray", size: "sm") %>
<span class="text-gray-900 font-medium text-sm">Dashboard</span>
</nav>
<div class="space-y-1">
<h1 class="text-3xl font-medium text-gray-900">Welcome back, <%= Current.user.first_name %></h1>
<p class="text-gray-500">Here's what's happening with your money this week</p>
</div>
</header>
<% if @accounts.empty? %>
<%= render "shared/no_account_empty_state" %>
<% else %>
<section class="flex gap-4">
<div class="bg-white border border-alpha-black-25 shadow-xs rounded-xl w-3/4 min-h-48 flex flex-col">
<div class="flex justify-between p-4">
<div>
<%= render partial: "shared/value_heading", locals: {
label: t(".net_worth"),
period: @period,
value: Current.family.net_worth,
trend: @net_worth_series.trend
} %>
</div>
<%= form_with url: root_path, method: :get, class: "flex items-center gap-4", data: { controller: "auto-submit-form" } do |form| %>
<%= period_select form: form, selected: @period.name %>
<% end %>
</div>
<%= render partial: "pages/dashboard/net_worth_chart", locals: { series: @net_worth_series } %>
</div>
<div class="bg-white p-4 border border-alpha-black-25 shadow-xs rounded-xl w-1/4">
<%= render partial: "pages/dashboard/allocation_chart", locals: { account_groups: @account_groups } %>
</div>
</section>
<section class="grid grid-cols-2 gap-4">
<div class="bg-white p-4 border border-alpha-black-25 shadow-xs rounded-xl">
<div class="flex flex-col gap-4 h-full">
<div class="flex gap-4">
<div class="grow">
<%= render partial: "shared/value_heading", locals: {
label: t(".income"),
period: Period.last_30_days,
value: @income_series.last&.value,
trend: @income_series.trend
} %>
</div>
<div
id="incomeChart"
class="h-full w-2/5"
data-controller="time-series-chart"
data-time-series-chart-data-value="<%= @income_series.to_json %>"
data-time-series-chart-use-labels-value="false"
data-time-series-chart-use-tooltip-value="false"></div>
</div>
<div class="flex gap-1.5 mt-auto">
<% @top_earners.first(3).each do |account| %>
<%= link_to account, class: "border border-alpha-black-25 rounded-full p-1 pr-2 flex items-center gap-1 text-xs text-primary font-medium hover:bg-gray-25", data: { controller: "tooltip" } do %>
<%= render "accounts/logo", account: account, size: "sm" %>
<span>+<%= Money.new(account.income, account.currency) %></span>
<%= render partial: "shared/text_tooltip", locals: { tooltip_text: account.name } %>
<% end %>
<% end %>
<% if @top_earners.count > 3 %>
<div class="bg-gray-25 rounded-full flex h-full aspect-1 items-center justify-center text-xs font-medium text-secondary">+<%= @top_earners.count - 3 %></div>
<% end %>
</div>
</div>
</div>
<div class="bg-white p-4 border border-alpha-black-25 shadow-xs rounded-xl">
<div class="flex flex-col gap-4 h-full">
<div class="flex gap-4">
<div class="grow">
<%= render partial: "shared/value_heading", locals: {
label: t(".spending"),
period: Period.last_30_days,
value: @spending_series.last&.value,
trend: @spending_series.trend
} %>
</div>
<div
id="spendingChart"
class="h-full w-2/5"
data-controller="time-series-chart"
data-time-series-chart-data-value="<%= @spending_series.to_json %>"
data-time-series-chart-use-labels-value="false"
data-time-series-chart-use-tooltip-value="false"></div>
</div>
<div class="mt-auto flex gap-1.5">
<% @top_spenders.first(3).each do |account| %>
<%= link_to account, class: "border border-alpha-black-25 rounded-full p-1 pr-2 flex items-center gap-1 text-xs text-primary font-medium hover:bg-gray-25", data: { controller: "tooltip" } do %>
<%= render "accounts/logo", account: account, size: "sm" %>
-<%= Money.new(account.spending, account.currency) %>
<%= render partial: "shared/text_tooltip", locals: { tooltip_text: account.name } %>
<% end %>
<% end %>
<% if @top_spenders.count > 3 %>
<div class="bg-gray-25 rounded-full flex h-full aspect-1 items-center justify-center text-xs font-medium text-secondary">+<%= @top_spenders.count - 3 %></div>
<% end %>
</div>
</div>
</div>
<div class="bg-white p-4 border border-alpha-black-25 shadow-xs rounded-xl">
<div class="flex flex-col gap-4 h-full">
<div class="flex gap-4">
<div class="grow">
<%= render partial: "shared/value_heading", locals: {
label: t(".savings_rate"),
period: Period.last_30_days,
value: (@savings_rate_series.last&.value)*100,
trend: @savings_rate_series.trend,
is_percentage: true
} %>
</div>
<div
id="savingsRateChart"
class="h-full w-2/5"
data-controller="time-series-chart"
data-time-series-chart-data-value="<%= @savings_rate_series.to_json %>"
data-time-series-chart-use-labels-value="false"
data-time-series-chart-use-tooltip-value="false"></div>
</div>
<div class="flex gap-1.5">
<% @top_savers.first(3).each do |account| %>
<% unless account.savings_rate.infinite? %>
<%= link_to account, class: "border border-alpha-black-25 rounded-full p-1 pr-2 flex items-center gap-1 text-xs text-primary font-medium hover:bg-gray-25", data: { controller: "tooltip" } do %>
<%= render "accounts/logo", account: account, size: "sm" %>
<span><%= account.savings_rate > 0 ? "+" : "-" %><%= number_to_percentage(account.savings_rate.abs * 100, precision: 2) %></span>
<%= render partial: "shared/text_tooltip", locals: { tooltip_text: account.name } %>
<% end %>
<% end %>
<% end %>
<% if @top_savers.count > 3 %>
<div class="bg-gray-25 rounded-full flex h-full aspect-1 items-center justify-center text-xs font-medium text-secondary">+<%= @top_savers.count - 3 %></div>
<% end %>
</div>
</div>
</div>
<div class="bg-white p-4 border border-alpha-black-25 shadow-xs rounded-xl">
<div class="flex gap-4 h-full">
<div class="grow">
<%= render partial: "shared/value_heading", locals: {
label: t(".investing"),
period: @period,
value: @investing_series.last.value,
trend: @investing_series.trend
} %>
</div>
<div
id="investingChart"
class="h-full w-2/5"
data-controller="time-series-chart"
data-time-series-chart-data-value="<%= @investing_series.to_json %>"
data-time-series-chart-use-labels-value="false"></div>
</div>
</div>
</section>
<section class="w-full">
<div class="bg-white p-4 border border-alpha-black-25 shadow-xs rounded-xl space-y-4">
<h2 class="text-lg font-medium text-primary"><%= t(".transactions") %></h2>
<% if @transaction_entries.empty? %>
<div class="text-secondary flex items-center justify-center py-12">
<p><%= t(".no_transactions") %></p>
</div>
<% else %>
<div class="text-secondary p-1 space-y-1 bg-gray-25 rounded-xl">
<%= entries_by_date(@transaction_entries, selectable: false) do |entries, _transfers| %>
<%= render entries, selectable: false %>
<% end %>
<section class="bg-white py-4 rounded-xl shadow-border-xs">
<%= render partial: "pages/dashboard/net_worth_chart", locals: { series: @balance_sheet.net_worth_series(period: @period), period: @period } %>
</section>
<p class="py-2 text-sm text-center"><%= link_to t(".view_all"), transactions_path %></p>
</div>
<% end %>
</div>
</section>
<% end %>
<section>
<%= render "pages/dashboard/balance_sheet", balance_sheet: @balance_sheet %>
</section>
</div>

View file

@ -1,29 +0,0 @@
<%# locals: (account_groups:) -%>
<div data-controller="tabs" data-tabs-active-class="bg-white border-alpha-black-25 shadow-xs text-primary" data-tabs-default-tab-value="asset-tab">
<div class="bg-gray-25 rounded-lg p-1 flex gap-1 text-sm text-secondary font-medium">
<button data-id="asset-tab" class="w-1/2 px-2 py-1 rounded-md border border-transparent" data-tabs-target="btn" data-action="tabs#select"><%= t(".assets") %></button>
<button data-id="liability-tab" class="w-1/2 px-2 py-1 rounded-md border border-transparent" data-tabs-target="btn" data-action="tabs#select"><%= t(".debts") %></button>
</div>
<div>
<div data-tabs-target="tab" id="asset-tab" class="space-y-6">
<div class="text-secondary flex items-center justify-center py-6">
<div
data-controller="pie-chart"
class="w-full aspect-1"
data-pie-chart-data-value="<%= value_group_pie_data(account_groups[:assets]) %>"
data-pie-chart-total-value="<%= format_money(account_groups[:assets].sum, precision: 0) %>">
</div>
</div>
</div>
<div data-tabs-target="tab" id="liability-tab" class="space-y-6 hidden">
<div class="text-secondary flex items-center justify-center py-6">
<div
data-controller="pie-chart"
class="w-full aspect-1"
data-pie-chart-data-value="<%= value_group_pie_data(account_groups[:liabilities]) %>"
data-pie-chart-total-value="<%= format_money(account_groups[:liabilities].sum, precision: 0) %>">
</div>
</div>
</div>
</div>
</div>

View file

@ -0,0 +1,103 @@
<%# locals: (balance_sheet:) %>
<div class="space-y-4">
<% balance_sheet.classification_groups.each do |classification_group| %>
<div class="bg-white shadow-border-xs rounded-xl space-y-4 p-4">
<h2 class="text-lg font-medium"><%= classification_group.display_name %></h2>
<% if classification_group.account_groups.any? %>
<div class="space-y-4">
<div class="flex gap-1">
<% classification_group.account_groups.each do |account_group| %>
<div class="h-1.5 rounded-sm" style="width: <%= account_group.weight %>%; background-color: <%= account_group.color %>;"></div>
<% end %>
</div>
<div class="flex gap-4">
<% classification_group.account_groups.each do |account_group| %>
<div class="flex items-center gap-2 text-sm">
<div class="h-2.5 w-2.5 rounded-full" style="background-color: <%= account_group.color %>;"></div>
<p class="text-secondary"><%= account_group.name %></p>
<p class="text-black"><%= number_to_percentage(account_group.weight, precision: 0) %></p>
</div>
<% end %>
</div>
</div>
<div class="bg-surface rounded-xl p-1 space-y-1">
<div class="px-4 py-2 flex items-center uppercase text-xs font-medium text-secondary">
<div>Name</div>
<div class="ml-auto text-right flex items-center gap-6">
<div class="w-24">
<p>Weight</p>
</div>
<div class="w-40">
<p>Value</p>
</div>
</div>
</div>
<div class="shadow-border-xs rounded-lg bg-white">
<% classification_group.account_groups.each do |account_group| %>
<details class="group rounded-lg open:bg-surface font-medium text-sm">
<summary class="cursor-pointer p-4 group-open:bg-surface bg-white rounded-lg flex items-center justify-between">
<div class="flex items-center gap-4">
<%= lucide_icon("chevron-right", class: "group-open:rotate-90 text-secondary w-5 h-5") %>
<p><%= account_group.name %></p>
</div>
<div class="ml-auto flex items-center text-right gap-6">
<div class="w-24 flex items-center justify-end gap-2">
<%= render partial: "shared/progress_circle", locals: { progress: account_group.weight, color: account_group.color } %>
<p><%= number_to_percentage(account_group.weight, precision: 0) %></p>
</div>
<div class="w-40">
<p><%= format_money(account_group.total_money) %></p>
</div>
</div>
</summary>
<div>
<% account_group.accounts.each_with_index do |account, idx| %>
<div class="pl-12 pr-4 py-3 flex items-center justify-between text-sm font-medium">
<div class="flex items-center gap-3">
<%= render "accounts/logo", account: account, size: "sm", color: account_group.color %>
<%= link_to account.name, account_path(account) %>
</div>
<div class="ml-auto flex items-center text-right gap-6">
<div class="w-24 flex items-center justify-end gap-2">
<%= render partial: "shared/progress_circle", locals: { progress: account.weight, color: account_group.color } %>
<p><%= number_to_percentage(account.weight, precision: 0) %></p>
</div>
<div class="w-40">
<p><%= format_money(account.balance_money) %></p>
</div>
</div>
</div>
<% if idx < account_group.accounts.size - 1 %>
<div class="pl-[84px] pr-40">
<div class="w-full border-subdued border-b"></div>
</div>
<% end %>
<% end %>
</div>
</details>
<% end %>
</div>
</div>
<% else %>
<div class="py-20 flex flex-col items-center">
<%= lucide_icon classification_group.icon, class: "w-6 h-6 shrink-0 text-secondary" %>
<p class="text-primary text-sm font-medium mb-1 mt-4">No <%= classification_group.display_name %></p>
<p class="text-secondary text-sm max-w-xs text-center"><%= "You have no #{classification_group.display_name}" %></p>
</div>
<% end %>
</div>
<% end %>
</div>

View file

@ -1,16 +1,37 @@
<%# locals: (series:) %>
<% if series.has_current_day_value? %>
<%# locals: (series:, period:) %>
<div class="flex justify-between p-4">
<div class="space-y-2">
<div class="space-y-2">
<p class="text-sm text-secondary font-medium">Net Worth</p>
<p class="text-primary -space-x-0.5 text-xl font-medium">
<%= series.current.format %>
</p>
<% if series.trend.nil? %>
<p class="text-sm text-secondary">Data not available for the selected period</p>
<% elsif series.trend.direction.flat? %>
<p class="text-sm text-secondary">No change vs. prior period</p>
<% else %>
<div class="flex items-center gap-2">
<%= render partial: "shared/trend_change", locals: { trend: series.trend } %>
<span class="text-sm text-secondary"><%= period.comparison_label %></span>
</div>
<% end %>
</div>
</div>
<%= form_with url: root_path, method: :get, class: "flex items-center gap-4", data: { controller: "auto-submit-form" } do |form| %>
<%= period_select form: form, selected: period %>
<% end %>
</div>
<% if series.any? %>
<div
id="netWorthChart"
class="w-full flex-1 min-h-52"
data-controller="time-series-chart"
data-time-series-chart-data-value="<%= series.to_json %>"></div>
<% elsif series.empty? %>
<% else %>
<div class="w-full h-full flex items-center justify-center">
<p class="text-secondary text-sm">No data available for the selected period.</p>
</div>
<% else %>
<div class="w-full h-full flex items-center justify-center">
<p class="text-secondary text-sm animate-pulse">Calculating latest balance data...</p>
</div>
<% end %>

View file

@ -1,35 +1,28 @@
<% content_for :sidebar do %>
<%= render "settings/nav" %>
<% end %>
<%= content_for :page_title, "Feedback" %>
<div class="space-y-4">
<h1 class="text-primary text-xl font-medium mb-4">Feedback</h1>
<div class="bg-white shadow-xs border border-alpha-black-25 rounded-xl p-4">
<h2 class="text-lg font-medium text-primary mb-1">Leave feedback</h2>
<p class="text-sm text-secondary mb-4">Let us know if you have any specific feedback. Feel free to include links to videos or screenshots.</p>
<div class="flex gap-2">
<%= link_to "https://github.com/maybe-finance/maybe/discussions/categories/feature-requests", target: "_blank", rel: "noopener noreferrer", class: "w-1/3 flex flex-col items-center p-4 border border-alpha-black-25 rounded-xl hover:bg-gray-50" do %>
<div class="bg-white shadow-border-xs rounded-xl p-4">
<h2 class="text-lg font-medium text-primary mb-1">Leave feedback</h2>
<p class="text-sm text-secondary mb-4">Let us know if you have any specific feedback. Feel free to include links to videos or screenshots.</p>
<div class="flex gap-2">
<%= link_to "https://github.com/maybe-finance/maybe/discussions/categories/feature-requests", target: "_blank", rel: "noopener noreferrer", class: "w-1/3 flex flex-col items-center p-4 border border-alpha-black-25 rounded-xl hover:bg-gray-50" do %>
<%= image_tag "github-icon.svg", class: "w-8 h-8 mb-2" %>
<span class="text-sm font-medium text-primary">Write a feature request</span>
<% end %>
<% if self_hosted? %>
<%= link_to "https://github.com/maybe-finance/maybe/issues/new?assignees=&labels=bug&template=bug_report.md&title=", target: "_blank", rel: "noopener noreferrer", class: "w-1/3 flex flex-col items-center p-4 border border-alpha-black-25 rounded-xl hover:bg-gray-50" do %>
<%= image_tag "github-icon.svg", class: "w-8 h-8 mb-2" %>
<span class="text-sm font-medium text-primary">Write a feature request</span>
<span class="text-sm font-medium text-primary">File a bug report</span>
<% end %>
<% if self_hosted? %>
<%= link_to "https://github.com/maybe-finance/maybe/issues/new?assignees=&labels=bug&template=bug_report.md&title=", target: "_blank", rel: "noopener noreferrer", class: "w-1/3 flex flex-col items-center p-4 border border-alpha-black-25 rounded-xl hover:bg-gray-50" do %>
<%= image_tag "github-icon.svg", class: "w-8 h-8 mb-2" %>
<span class="text-sm font-medium text-primary">File a bug report</span>
<% end %>
<% else %>
<%= link_to "mailto:hello@maybefinance.com", class: "w-1/3 flex flex-col items-center p-4 border border-alpha-black-25 rounded-xl hover:bg-gray-50", onclick: "Intercom('showNewMessage'); return false;" do %>
<%= lucide_icon "bug", class: "w-8 h-8 mb-2" %>
<span class="text-sm font-medium text-primary">File a bug report</span>
<% end %>
<% else %>
<%= link_to "mailto:hello@maybefinance.com", class: "w-1/3 flex flex-col items-center p-4 border border-alpha-black-25 rounded-xl hover:bg-gray-50", onclick: "Intercom('showNewMessage'); return false;" do %>
<%= lucide_icon "bug", class: "w-8 h-8 mb-2" %>
<span class="text-sm font-medium text-primary">File a bug report</span>
<% end %>
<% end %>
<%= link_to "https://link.maybe.co/discord", target: "_blank", rel: "noopener noreferrer", class: "w-1/3 flex flex-col items-center p-4 border border-alpha-black-25 rounded-xl hover:bg-gray-50" do %>
<%= image_tag "discord-icon.svg", class: "w-8 h-8 mb-2" %>
<span class="text-sm font-medium text-primary">Discuss Maybe with others</span>
<% end %>
</div>
<%= link_to "https://link.maybe.co/discord", target: "_blank", rel: "noopener noreferrer", class: "w-1/3 flex flex-col items-center p-4 border border-alpha-black-25 rounded-xl hover:bg-gray-50" do %>
<%= image_tag "discord-icon.svg", class: "w-8 h-8 mb-2" %>
<span class="text-sm font-medium text-primary">Discuss Maybe with others</span>
<% end %>
</div>
<%= settings_nav_footer %>
</div>

View file

@ -1,7 +1,7 @@
<%# locals: (plaid_item:) %>
<%= tag.div id: dom_id(plaid_item) do %>
<details open class="group bg-white p-4 border border-alpha-black-25 shadow-xs rounded-xl">
<details open class="group bg-white p-4 shadow-border-xs rounded-xl">
<summary class="flex items-center justify-between gap-2 focus-visible:outline-hidden">
<div class="flex items-center gap-2">
<%= lucide_icon "chevron-right", class: "group-open:transform group-open:rotate-90 text-secondary w-5" %>
@ -50,15 +50,14 @@
<% if plaid_item.requires_update? %>
<% begin %>
<% link_token = plaid_item.get_update_link_token(webhooks_url: plaid_webhooks_url(plaid_item.plaid_region), redirect_url: accounts_url) %>
<button
data-controller="plaid"
data-action="plaid#open"
<button
data-controller="plaid"
data-action="plaid#open"
data-plaid-region-value="<%= plaid_item.plaid_region %>"
data-plaid-link-token-value="<%= link_token %>"
data-plaid-is-update-value="true"
data-plaid-item-id-value="<%= plaid_item.id %>"
class="btn btn--secondary flex items-center gap-2"
>
class="btn btn--secondary flex items-center gap-2">
<%= lucide_icon "refresh-cw", class: "w-4 h-4" %>
<%= tag.span t(".update") %>
</button>

View file

@ -1,5 +1,5 @@
<%# locals: (title:, subtitle: nil, content:) %>
<section class="bg-white border border-alpha-black-25 shadow-xs rounded-xl p-4 space-y-4">
<section class="bg-white shadow-border-xs rounded-xl p-4 space-y-4">
<div>
<h2 class="text-lg font-medium text-primary"><%= title %></h2>
<% if subtitle.present? %>

View file

@ -16,28 +16,33 @@
</div>
<ul class="space-y-1">
<li>
<%= sidebar_link_to t(".profile_label"), settings_profile_path, icon: "circle-user" %>
</li>
<li>
<%= sidebar_link_to t(".preferences_label"), settings_preferences_path, icon: "bolt" %>
</li>
<li>
<%= sidebar_link_to t(".security_label"), settings_security_path, icon: "shield-check" %>
</li>
<% if self_hosted? %>
<li>
<%= sidebar_link_to t(".self_hosting_label"), settings_hosting_path, icon: "database" %>
</li>
<% end %>
<li>
<%= sidebar_link_to t(".billing_label"), settings_billing_path, icon: "circle-dollar-sign" %>
</li>
<li>
<%= sidebar_link_to t(".accounts_label"), accounts_path, icon: "layers" %>
<%= render "settings/settings_nav_item", name: t(".profile_label"), path: settings_profile_path, icon: "circle-user" %>
</li>
<li>
<%= sidebar_link_to t(".imports_label"), imports_path, icon: "download" %>
<%= render "settings/settings_nav_item", name: t(".preferences_label"), path: settings_preferences_path, icon: "bolt" %>
</li>
<li>
<%= render "settings/settings_nav_item", name: t(".security_label"), path: settings_security_path, icon: "shield-check" %>
</li>
<% if self_hosted? %>
<li>
<%= render "settings/settings_nav_item", name: t(".self_hosting_label"), path: settings_hosting_path, icon: "database" %>
</li>
<% end %>
<li>
<%= render "settings/settings_nav_item", name: t(".billing_label"), path: settings_billing_path, icon: "circle-dollar-sign" %>
</li>
<li>
<%= render "settings/settings_nav_item", name: t(".accounts_label"), path: accounts_path, icon: "layers" %>
</li>
<li>
<%= render "settings/settings_nav_item", name: t(".imports_label"), path: imports_path, icon: "download" %>
</li>
</ul>
</section>
@ -49,13 +54,13 @@
</div>
<ul class="space-y-1">
<li>
<%= sidebar_link_to t(".tags_label"), tags_path, icon: "tags" %>
<%= render "settings/settings_nav_item", name: t(".tags_label"), path: tags_path, icon: "tags" %>
</li>
<li>
<%= sidebar_link_to t(".categories_label"), categories_path, icon: "shapes" %>
<%= render "settings/settings_nav_item", name: t(".categories_label"), path: categories_path, icon: "shapes" %>
</li>
<li>
<%= sidebar_link_to t(".merchants_label"), merchants_path, icon: "store" %>
<%= render "settings/settings_nav_item", name: t(".merchants_label"), path: merchants_path, icon: "store" %>
</li>
</ul>
</section>
@ -67,14 +72,14 @@
</div>
<ul class="space-y-1">
<li>
<%= sidebar_link_to t(".whats_new_label"), changelog_path, icon: "box" %>
<%= sidebar_link_to t(".feedback_label"), feedback_path, icon: "megaphone" %>
<%= render "settings/settings_nav_item", name: t(".whats_new_label"), path: changelog_path, icon: "box" %>
<%= render "settings/settings_nav_item", name: t(".feedback_label"), path: feedback_path, icon: "megaphone" %>
</li>
</ul>
</section>
<section>
<%= button_to session_path(Current.session), method: :delete, class: "flex items-center gap-2 px-3 py-2 rounded-xl border text-sm font-medium w-full text-error hover:bg-gray-100 border-transparent" do %>
<%= button_to session_path(Current.session), method: :delete, class: "flex items-center gap-2 btn btn--ghost text-destructive w-full" do %>
<%= lucide_icon("log-out", class: "w-5 h-5 shrink-0") %>
<span><%= t(".logout") %></span>
<% end %>

View file

@ -0,0 +1,9 @@
<%# locals: (name:, path:, icon:) %>
<%= link_to path, class: class_names(
"flex items-center gap-2 btn btn--ghost",
page_active?(path) ? "text-primary bg-white shadow-border-xs" : "text-secondary hover:bg-gray-100 border-transparent"
), aria: { current: ("page" if page_active?(path)) } do %>
<%= lucide_icon(icon, class: "w-5 h-5") if icon %>
<%= name %>
<% end %>

View file

@ -1,47 +1,40 @@
<% content_for :sidebar do %>
<%= render "settings/nav" %>
<% end %>
<%= content_for :page_title, t(".page_title") %>
<div class="space-y-4">
<h1 class="text-primary text-xl font-medium mb-4"><%= t(".page_title") %></h1>
<%= settings_section title: t(".subscription_title"), subtitle: t(".subscription_subtitle") do %>
<div class="space-y-4">
<div class="p-3 shadow-xs bg-white border border-secondary rounded-lg flex justify-between items-center">
<div class="flex items-center gap-3">
<div class="w-9 h-9 rounded-full bg-gray-25 flex justify-center items-center">
<%= lucide_icon "gem", class: "w-5 h-5 text-secondary" %>
</div>
<div class="text-sm space-y-1">
<% if @user.family.subscribed? || subscription_pending? %>
<p class="text-primary">You are currently subscribed to <span class="font-medium">Maybe+</span></p>
<p class="text-secondary">Manage your billing settings here.</p>
<% else %>
<p class="text-primary">You are currently <span class="font-medium">not subscribed</span></p>
<p class="text-secondary">Once you subscribe to Maybe+, youll see your billing settings here.</p>
<% end %>
</div>
<%= settings_section title: t(".subscription_title"), subtitle: t(".subscription_subtitle") do %>
<div class="space-y-4">
<div class="p-3 shadow-border-xs bg-white rounded-lg flex justify-between items-center">
<div class="flex items-center gap-3">
<div class="w-9 h-9 rounded-full bg-gray-25 flex justify-center items-center">
<%= lucide_icon "gem", class: "w-5 h-5 text-secondary" %>
</div>
<% if @user.family.subscribed? || subscription_pending? %>
<%= link_to subscription_path, class: "btn btn--secondary flex items-center gap-1" do %>
<span>Manage</span>
<%= lucide_icon "external-link", class: "w-5 h-5 shrink-0 text-secondary" %>
<% end %>
<% else %>
<%= link_to new_subscription_path, class: "btn btn--secondary flex items-center gap-1" do %>
<span>Subscribe</span>
<%= lucide_icon "external-link", class: "w-5 h-5 shrink-0 text-secondary" %>
<div class="text-sm space-y-1">
<% if @user.family.subscribed? || subscription_pending? %>
<p class="text-primary">You are currently subscribed to <span class="font-medium">Maybe+</span></p>
<p class="text-secondary">Manage your billing settings here.</p>
<% else %>
<p class="text-primary">You are currently <span class="font-medium">not subscribed</span></p>
<p class="text-secondary">Once you subscribe to Maybe+, you'll see your billing settings here.</p>
<% end %>
</div>
</div>
<% if @user.family.subscribed? || subscription_pending? %>
<%= link_to subscription_path, class: "btn btn--secondary flex items-center gap-1" do %>
<span>Manage</span>
<%= lucide_icon "external-link", class: "w-5 h-5 shrink-0 text-secondary" %>
<% end %>
</div>
<div class="flex items-center gap-2">
<%= image_tag "stripe-logo.svg", class: "w-5 h-5 shrink-0" %>
<p class="text-secondary text-sm">Managed via Stripe</p>
</div>
<% else %>
<%= link_to new_subscription_path, class: "btn btn--secondary flex items-center gap-1" do %>
<span>Subscribe</span>
<%= lucide_icon "external-link", class: "w-5 h-5 shrink-0 text-secondary" %>
<% end %>
<% end %>
</div>
<% end %>
<%= settings_nav_footer %>
</div>
<div class="flex items-center gap-2">
<%= image_tag "stripe-logo.svg", class: "w-5 h-5 shrink-0" %>
<p class="text-secondary text-sm">Managed via Stripe</p>
</div>
</div>
<% end %>

View file

@ -1,21 +1,13 @@
<% content_for :sidebar do %>
<%= render "settings/nav" %>
<%= content_for :page_title, t(".title") %>
<%= settings_section title: t(".general") do %>
<div class="space-y-6">
<%= render "settings/hostings/upgrade_settings" %>
<%= render "settings/hostings/provider_settings" %>
<%= render "settings/hostings/synth_settings" %>
</div>
<% end %>
<div class="space-y-4 pb-32">
<h1 class="text-primary text-xl font-medium mb-4"><%= t(".title") %></h1>
<%= settings_section title: t(".general") do %>
<div class="space-y-6">
<%= render "settings/hostings/upgrade_settings" %>
<%= render "settings/hostings/provider_settings" %>
<%= render "settings/hostings/synth_settings" %>
</div>
<% end %>
<%= settings_section title: t(".invites") do %>
<%= render "settings/hostings/invite_code_settings" %>
<% end %>
<%= settings_nav_footer %>
</div>
<%= settings_section title: t(".invites") do %>
<%= render "settings/hostings/invite_code_settings" %>
<% end %>

View file

@ -1,77 +1,70 @@
<% content_for :sidebar do %>
<%= render "settings/nav" %>
<% end %>
<%= content_for :page_title, t(".page_title") %>
<div class="space-y-4">
<h1 class="text-primary text-xl font-medium mb-4"><%= t(".page_title") %></h1>
<%= settings_section title: t(".general_title"), subtitle: t(".general_subtitle") do %>
<div>
<%= styled_form_with model: @user, class: "space-y-4", data: { controller: "auto-submit-form" } do |form| %>
<%= form.hidden_field :redirect_to, value: "preferences" %>
<%= settings_section title: t(".general_title"), subtitle: t(".general_subtitle") do %>
<div>
<%= styled_form_with model: @user, class: "space-y-4", data: { controller: "auto-submit-form" } do |form| %>
<%= form.hidden_field :redirect_to, value: "preferences" %>
<%= form.fields_for :family do |family_form| %>
<%= family_form.select :currency,
<%= form.fields_for :family do |family_form| %>
<%= family_form.select :currency,
currencies_for_select.map { |currency| [ "#{currency.name} (#{currency.iso_code})", currency.iso_code ] },
{ label: t(".currency") }, disabled: true %>
<%= family_form.select :locale,
<%= family_form.select :locale,
language_options,
{ label: t(".language") },
{ data: { auto_submit_form_target: "auto" } } %>
<%= family_form.select :timezone,
<%= family_form.select :timezone,
timezone_options,
{ label: t(".timezone") },
{ data: { auto_submit_form_target: "auto" } } %>
<%= family_form.select :date_format,
<%= family_form.select :date_format,
Family::DATE_FORMATS,
{ label: t(".date_format") },
{ data: { auto_submit_form_target: "auto" } } %>
<%= family_form.select :country,
<%= family_form.select :country,
country_options,
{ label: t(".country") },
{ data: { auto_submit_form_target: "auto" } } %>
<p class="text-xs italic pl-2 text-secondary">Please note, we are still working on translations for various languages. Please see the <%= link_to "I18n issue", "https://github.com/maybe-finance/maybe/issues/1225", target: "_blank", class: "underline" %> for more information.</p>
<% end %>
<p class="text-xs italic pl-2 text-secondary">Please note, we are still working on translations for various languages. Please see the <%= link_to "I18n issue", "https://github.com/maybe-finance/maybe/issues/1225", target: "_blank", class: "underline" %> for more information.</p>
<% end %>
</div>
<% end %>
<% end %>
</div>
<% end %>
<%= settings_section title: t(".data"), subtitle: t(".data_subtitle") do %>
<%= render "settings/preferences/data_enrichment_settings", user: @user %>
<% end %>
<%= settings_section title: t(".data"), subtitle: t(".data_subtitle") do %>
<%= render "settings/preferences/data_enrichment_settings", user: @user %>
<% end %>
<%= settings_section title: t(".theme_title"), subtitle: t(".theme_subtitle") do %>
<div>
<%= styled_form_with model: @user, class: "flex justify-between items-center" do |form| %>
<%= form.hidden_field :redirect_to, value: "preferences" %>
<div class="text-center">
<%= image_tag("light-mode-preview.png", alt: "Light Theme Preview", class: "h-44 mb-4") %>
<div class="flex justify-center items-center gap-2">
<%= form.radio_button :theme, t(".theme_light"), checked: true %>
<%= form.label :theme_light, t(".theme_light"), value: "light" %>
</div>
<%= settings_section title: t(".theme_title"), subtitle: t(".theme_subtitle") do %>
<div>
<%= styled_form_with model: @user, class: "flex justify-between items-center" do |form| %>
<%= form.hidden_field :redirect_to, value: "preferences" %>
<div class="text-center">
<%= image_tag("light-mode-preview.png", alt: "Light Theme Preview", class: "h-44 mb-4") %>
<div class="flex justify-center items-center gap-2">
<%= form.radio_button :theme, t(".theme_light"), checked: true %>
<%= form.label :theme_light, t(".theme_light"), value: "light" %>
</div>
<div class="text-center">
<%= image_tag("dark-mode-preview.png", alt: "Dark Theme Preview", class: "h-44 mb-4") %>
<div class="flex justify-center items-center gap-2">
<%= form.radio_button :theme, t(".theme_dark"), disabled: true, class: "cursor-not-allowed" %>
<%= form.label :theme_dark, t(".theme_dark"), value: "dark" %>
</div>
</div>
<div class="text-center">
<%= image_tag("dark-mode-preview.png", alt: "Dark Theme Preview", class: "h-44 mb-4") %>
<div class="flex justify-center items-center gap-2">
<%= form.radio_button :theme, t(".theme_dark"), disabled: true, class: "cursor-not-allowed" %>
<%= form.label :theme_dark, t(".theme_dark"), value: "dark" %>
</div>
<div class="text-center">
<%= image_tag("system-mode-preview.png", alt: "System Theme Preview", class: "h-44 mb-4") %>
<div class="flex items-center gap-2 justify-center">
<%= form.radio_button :theme, t(".theme_system"), disabled: true, class: "cursor-not-allowed" %>
<%= form.label :theme_system, t(".theme_system"), value: "system" %>
</div>
</div>
<div class="text-center">
<%= image_tag("system-mode-preview.png", alt: "System Theme Preview", class: "h-44 mb-4") %>
<div class="flex items-center gap-2 justify-center">
<%= form.radio_button :theme, t(".theme_system"), disabled: true, class: "cursor-not-allowed" %>
<%= form.label :theme_system, t(".theme_system"), value: "system" %>
</div>
<% end %>
</div>
<% end %>
<%= settings_nav_footer %>
</div>
</div>
<% end %>
</div>
<% end %>

View file

@ -1,57 +1,54 @@
<% content_for :sidebar do %>
<%= render "settings/nav" %>
<% end %>
<div class="space-y-4">
<h1 class="text-primary text-xl font-medium mb-4"><%= t(".page_title") %></h1>
<div class="space-y-4">
<%= settings_section title: t(".profile_title"), subtitle: t(".profile_subtitle") do %>
<%= styled_form_with model: @user, url: user_path(@user), class: "space-y-4" do |form| %>
<%= render "settings/user_avatar_field", form: form, user: @user %>
<%= content_for :page_title, t(".page_title") %>
<div>
<%= form.email_field :email, placeholder: t(".email"), label: t(".email") %>
<% if @user.unconfirmed_email.present? %>
<p class="mt-2 text-sm text-gray-600">
You have requested to change your email to <%= @user.unconfirmed_email %>. Please go to your email and confirm for the change to take effect.
</p>
<% end %>
<div class="grid grid-cols-2 gap-4 mt-4">
<%= form.text_field :first_name, placeholder: t(".first_name"), label: t(".first_name") %>
<%= form.text_field :last_name, placeholder: t(".last_name"), label: t(".last_name") %>
</div>
<div class="flex justify-end mt-4">
<%= form.submit t(".save"), class: "bg-gray-900 hover:bg-gray-700 cursor-pointer text-white rounded-lg px-3 py-2" %>
</div>
</div>
<%= settings_section title: t(".profile_title"), subtitle: t(".profile_subtitle") do %>
<%= styled_form_with model: @user, url: user_path(@user), class: "space-y-4" do |form| %>
<%= render "settings/user_avatar_field", form: form, user: @user %>
<div>
<%= form.email_field :email, placeholder: t(".email"), label: t(".email") %>
<% if @user.unconfirmed_email.present? %>
<p class="mt-2 text-sm text-gray-600">
You have requested to change your email to <%= @user.unconfirmed_email %>. Please go to your email and confirm for the change to take effect.
</p>
<% end %>
<% end %>
<%= settings_section title: t(".household_title"), subtitle: t(".household_subtitle") do %>
<div class="space-y-4">
<%= styled_form_with model: Current.user, class: "space-y-4", data: { controller: "auto-submit-form" } do |form| %>
<%= form.fields_for :family do |family_fields| %>
<%= family_fields.text_field :name,
<div class="grid grid-cols-2 gap-4 mt-4">
<%= form.text_field :first_name, placeholder: t(".first_name"), label: t(".first_name") %>
<%= form.text_field :last_name, placeholder: t(".last_name"), label: t(".last_name") %>
</div>
<div class="flex justify-end mt-4">
<%= form.submit t(".save"), class: "bg-gray-900 hover:bg-gray-700 cursor-pointer text-white rounded-lg px-3 py-2" %>
</div>
</div>
<% end %>
<% end %>
<%= settings_section title: t(".household_title"), subtitle: t(".household_subtitle") do %>
<div class="space-y-4">
<%= styled_form_with model: Current.user, class: "space-y-4", data: { controller: "auto-submit-form" } do |form| %>
<%= form.fields_for :family do |family_fields| %>
<%= family_fields.text_field :name,
placeholder: t(".household_form_input_placeholder"),
label: t(".household_form_label"),
disabled: !Current.user.admin?,
"data-auto-submit-form-target": "auto" %>
<% end %>
<% end %>
<div class="bg-gray-25 rounded-xl p-1">
<div class="px-4 py-2">
<p class="uppercase text-xs text-secondary font-medium"><%= Current.family.name %> &middot; <%= Current.family.users.size %></p>
<% end %>
<% end %>
<div class="bg-gray-25 rounded-xl p-1">
<div class="px-4 py-2">
<p class="uppercase text-xs text-secondary font-medium"><%= Current.family.name %> &middot; <%= Current.family.users.size %></p>
</div>
<% @users.each do |user| %>
<div class="flex gap-2 items-center bg-white p-4 border border-alpha-black-25 rounded-lg">
<div class="w-9 h-9 shrink-0">
<%= render "settings/user_avatar", user: user %>
</div>
<% @users.each do |user| %>
<div class="flex gap-2 items-center bg-white p-4 border border-alpha-black-25 rounded-lg">
<div class="w-9 h-9 shrink-0">
<%= render "settings/user_avatar", user: user %>
</div>
<p class="text-primary font-medium text-sm"><%= user.display_name %></p>
<div class="rounded-md bg-gray-100 px-1.5 py-0.5">
<p class="uppercase text-secondary font-medium text-xs"><%= user.role %></p>
</div>
<% if Current.user.admin? && user != Current.user %>
<div class="ml-auto">
<%= button_to settings_profile_path(user_id: user),
<p class="text-primary font-medium text-sm"><%= user.display_name %></p>
<div class="rounded-md bg-gray-100 px-1.5 py-0.5">
<p class="uppercase text-secondary font-medium text-xs"><%= user.role %></p>
</div>
<% if Current.user.admin? && user != Current.user %>
<div class="ml-auto">
<%= button_to settings_profile_path(user_id: user),
method: :delete,
class: "text-red-500 hover:text-red-700",
data: { turbo_confirm: {
@ -60,48 +57,48 @@
accept: t(".remove_member"),
acceptClass: "w-full bg-red-500 text-white rounded-xl text-center p-[10px] border mb-2"
}} do %>
<%= lucide_icon "x", class: "w-5 h-5" %>
<% end %>
</div>
<%= lucide_icon "x", class: "w-5 h-5" %>
<% end %>
</div>
<% end %>
<% if @pending_invitations.any? %>
<% @pending_invitations.each do |invitation| %>
<div class="flex gap-2 items-center justify-between bg-white p-4 border border-alpha-black-25 rounded-lg">
<div class="flex gap-2 items-center">
<div class="w-9 h-9 shrink-0">
<div class="text-white w-full h-full bg-gray-400 rounded-full flex items-center justify-center text-lg uppercase"><%= invitation.email[0] %></div>
</div>
<div class="flex">
<p class="text-primary font-medium text-sm"><%= invitation.email %></p>
<div class="rounded-md bg-gray-100 px-1.5 py-0.5">
<p class="uppercase text-secondary font-medium text-xs"><%= t(".pending") %></p>
</div>
</div>
</div>
<% end %>
<% if @pending_invitations.any? %>
<% @pending_invitations.each do |invitation| %>
<div class="flex gap-2 items-center justify-between bg-white p-4 border border-alpha-black-25 rounded-lg">
<div class="flex gap-2 items-center">
<div class="w-9 h-9 shrink-0">
<div class="text-white w-full h-full bg-gray-400 rounded-full flex items-center justify-center text-lg uppercase"><%= invitation.email[0] %></div>
</div>
<div class="flex">
<p class="text-primary font-medium text-sm"><%= invitation.email %></p>
<div class="rounded-md bg-gray-100 px-1.5 py-0.5">
<p class="uppercase text-secondary font-medium text-xs"><%= t(".pending") %></p>
</div>
<div class="flex items-center gap-4">
<% if self_hosted? %>
<div class="flex items-center gap-2" data-controller="clipboard">
<p class="text-secondary text-sm"><%= t(".invitation_link") %></p>
<span data-clipboard-target="source" class="hidden"><%= accept_invitation_url(invitation.token) %></span>
<input type="text"
</div>
</div>
<div class="flex items-center gap-4">
<% if self_hosted? %>
<div class="flex items-center gap-2" data-controller="clipboard">
<p class="text-secondary text-sm"><%= t(".invitation_link") %></p>
<span data-clipboard-target="source" class="hidden"><%= accept_invitation_url(invitation.token) %></span>
<input type="text"
readonly
autocomplete="off"
value="<%= accept_invitation_url(invitation.token) %>"
class="text-sm bg-gray-50 px-2 py-1 rounded border border-gray-200 w-72">
<button data-action="clipboard#copy" class="text-secondary hover:text-gray-700">
<span data-clipboard-target="iconDefault">
<%= lucide_icon "copy", class: "w-5 h-5" %>
</span>
<span class="hidden" data-clipboard-target="iconSuccess">
<%= lucide_icon "check", class: "w-5 h-5" %>
</span>
</button>
</div>
<% end %>
<% if Current.user.admin? %>
<%= button_to invitation_path(invitation),
<button data-action="clipboard#copy" class="text-secondary hover:text-gray-700">
<span data-clipboard-target="iconDefault">
<%= lucide_icon "copy", class: "w-5 h-5" %>
</span>
<span class="hidden" data-clipboard-target="iconSuccess">
<%= lucide_icon "check", class: "w-5 h-5" %>
</span>
</button>
</div>
<% end %>
<% if Current.user.admin? %>
<%= button_to invitation_path(invitation),
method: :delete,
class: "text-red-500 hover:text-red-700",
data: { turbo_confirm: {
@ -110,31 +107,32 @@
accept: t(".remove_invitation"),
acceptClass: "w-full bg-red-500 text-white rounded-xl text-center p-[10px] border mb-2"
}} do %>
<%= lucide_icon "x", class: "w-5 h-5" %>
<% end %>
<% end %>
</div>
</div>
<% end %>
<% end %>
<% if Current.user.admin? %>
<%= link_to new_invitation_path,
<%= lucide_icon "x", class: "w-5 h-5" %>
<% end %>
<% end %>
</div>
</div>
<% end %>
<% end %>
<% if Current.user.admin? %>
<%= link_to new_invitation_path,
class: "bg-gray-100 flex items-center justify-center gap-2 text-secondary mt-1 hover:bg-gray-200 rounded-lg px-4 py-2 w-full text-center",
data: { turbo_frame: :modal } do %>
<%= lucide_icon("plus", class: "w-5 h-5 text-secondary") %>
<%= t(".invite_member") %>
<% end %>
<% end %>
</div>
</div>
<% end %>
<%= settings_section title: t(".danger_zone_title") do %>
<div class="flex items-center justify-between">
<div>
<h3 class="font-medium text-primary"><%= t(".delete_account") %></h3>
<p class="text-secondary text-sm"><%= t(".delete_account_warning") %></p>
</div>
<%=
<%= lucide_icon("plus", class: "w-5 h-5 text-secondary") %>
<%= t(".invite_member") %>
<% end %>
<% end %>
</div>
</div>
<% end %>
<%= settings_section title: t(".danger_zone_title") do %>
<div class="flex items-center justify-between">
<div>
<h3 class="font-medium text-primary"><%= t(".delete_account") %></h3>
<p class="text-secondary text-sm"><%= t(".delete_account_warning") %></p>
</div>
<%=
button_to t(".delete_account"), user_path(@user), method: :delete,
class: "bg-red-500 text-white text-sm font-medium rounded-lg px-3 py-2",
data: { turbo_confirm: {
@ -143,10 +141,6 @@
accept: t(".delete_account"),
acceptClass: "w-full bg-red-500 text-white rounded-xl text-center p-[10px] border mb-2"
}}
%>
</div>
<% end %>
%>
</div>
<%= settings_nav_footer %>
</div>
<% end %>

View file

@ -1,30 +1,26 @@
<% content_for :sidebar do %>
<%= render "settings/nav" %>
<% end %>
<%= content_for :page_title, t(".page_title") %>
<div class="space-y-4">
<h1 class="text-primary text-xl font-medium mb-4"><%= t(".page_title") %></h1>
<%= settings_section title: t(".mfa_title"), subtitle: t(".mfa_description") do %>
<div class="space-y-4">
<div class="p-3 shadow-xs bg-white border border-secondary rounded-lg flex justify-between items-center">
<div class="flex items-center gap-3">
<div class="w-9 h-9 rounded-full bg-gray-25 flex justify-center items-center">
<%= lucide_icon "shield-check", class: "w-5 h-5 text-secondary" %>
</div>
<div class="text-sm space-y-1">
<% if Current.user.otp_required? %>
<p class="text-primary">Two-factor authentication is <span class="font-medium text-green-600">enabled</span></p>
<p class="text-secondary">Your account is protected with an additional layer of security.</p>
<% else %>
<p class="text-primary">Two-factor authentication is <span class="font-medium text-red-600">disabled</span></p>
<p class="text-secondary">Enable 2FA to add an extra layer of security to your account.</p>
<% end %>
</div>
<%= settings_section title: t(".mfa_title"), subtitle: t(".mfa_description") do %>
<div class="space-y-4">
<div class="p-3 shadow-border-xs bg-white rounded-lg flex justify-between items-center">
<div class="flex items-center gap-3">
<div class="w-9 h-9 rounded-full bg-gray-25 flex justify-center items-center">
<%= lucide_icon "shield-check", class: "w-5 h-5 text-secondary" %>
</div>
<% if Current.user.otp_required? %>
<%= button_to t(".disable_mfa"), disable_mfa_path,
<div class="text-sm space-y-1">
<% if Current.user.otp_required? %>
<p class="text-primary">Two-factor authentication is <span class="font-medium text-green-600">enabled</span></p>
<p class="text-secondary">Your account is protected with an additional layer of security.</p>
<% else %>
<p class="text-primary">Two-factor authentication is <span class="font-medium text-red-600">disabled</span></p>
<p class="text-secondary">Enable 2FA to add an extra layer of security to your account.</p>
<% end %>
</div>
</div>
<% if Current.user.otp_required? %>
<%= button_to t(".disable_mfa"), disable_mfa_path,
method: :delete,
class: "btn btn--secondary flex items-center gap-1",
data: { turbo_confirm: {
@ -33,13 +29,10 @@
accept: t(".disable_mfa"),
acceptClass: "w-full bg-red-500 text-white rounded-xl text-center p-[10px] border mb-2"
} } %>
<% else %>
<%= link_to t(".enable_mfa"), new_mfa_path,
<% else %>
<%= link_to t(".enable_mfa"), new_mfa_path,
class: "btn btn--primary flex items-center gap-1" %>
<% end %>
</div>
<% end %>
</div>
<% end %>
<%= settings_nav_footer %>
</div>
</div>
<% end %>

View file

@ -1,6 +1,3 @@
<div class="flex items-center py-0.5 px-0.5 gap-1 fixed bottom-2 right-2 shadow-xs border border-alpha-black-50 rounded-md bg-white">
<div class="flex items-center py-0.5 px-0.5 gap-1 fixed right-1 top-1 shadow-xs border border-alpha-black-50 rounded-md bg-white">
<p class="text-xs text-secondary pl-2">Version: <%= Maybe.version.to_release_tag %></p>
<%= link_to settings_hosting_path, class: "flex gap-1 items-center hover:bg-gray-50 rounded-md p-2" do %>
<%= lucide_icon("settings", class: "w-4 h-4 text-secondary shrink-0") %>
<% end %>
</div>

View file

@ -1,7 +0,0 @@
<% flash.each do |type, msg| %>
<div class="p-4 <%= type == "notice" ? "bg-blue-200" : "bg-red-200" %> rounded-lg"><%= msg %></div>
<% end %>
<% errors.each do |message| %>
<div class="p-4 bg-red-200 rounded-lg"><%= message %></div>
<% end %>

View file

@ -7,7 +7,7 @@
"full" => "w-full h-full"
} %>
<%= tag.div style: mixed_hex_styles(hex),
<%= tag.div style: mixed_hex_styles(hex || "#1570EF"),
class: [size_classes[size], "flex shrink-0 items-center justify-center rounded-full"] do %>
<%= tag.span (name.presence&.first || "T").upcase, class: ["font-medium", size == "sm" ? "text-xs" : "text-sm"] %>
<% end %>

Some files were not shown because too many files have changed in this diff Show more