1
0
Fork 0
mirror of https://github.com/maybe-finance/maybe.git synced 2025-08-09 15:35:22 +02:00

Fix dashboard mobile issues

This commit is contained in:
Zach Gollwitzer 2025-04-30 12:31:11 -04:00
parent 44f24e68dd
commit 0398edf57e
22 changed files with 172 additions and 94 deletions

View file

@ -243,6 +243,20 @@
color: theme(colors.white) !important;
}
/* Transition when expanding a sidebar */
@utility from-zero-width-transition {
transition-property: width, opacity;
transition-duration: 0.5s, 0.7s;
transition-timing-function: linear;
}
/* Transition when collapsing a sidebar */
@utility to-zero-width-transition {
transition-property: width, opacity;
transition-duration: 0.3s, 0.2s;
transition-timing-function: linear;
}
@layer base {
[data-theme="dark"] {
--color-success: var(--color-green-500);

View file

@ -1,6 +1,11 @@
module ApplicationHelper
include Pagy::Frontend
def styled_form_with(**options, &block)
options[:builder] = StyledFormBuilder
form_with(**options, &block)
end
def icon(key, size: "md", color: "default", custom: false, as_button: false, **opts)
extra_classes = opts.delete(:class)
sizes = { xs: "w-3 h-3", sm: "w-4 h-4", md: "w-5 h-5", lg: "w-6 h-6", xl: "w-7 h-7", "2xl": "w-8 h-8" }

View file

@ -1,22 +0,0 @@
module FormsHelper
def styled_form_with(**options, &block)
options[:builder] = StyledFormBuilder
form_with(**options, &block)
end
def modal_form_wrapper(title:, subtitle: nil, overflow_visible: false, &block)
content = capture &block
render partial: "shared/modal_form", locals: { title:, subtitle:, content:, overflow_visible: }
end
def period_select(form:, selected:, classes: "border border-secondary bg-container-inset rounded-lg text-sm pr-7 cursor-pointer text-primary focus:outline-hidden focus:ring-0")
periods_for_select = Period.all.map { |period| [ period.label_short, period.key ] }
form.select(:period, periods_for_select, { selected: selected.key }, class: classes, data: { "auto-submit-form-target": "auto" })
end
def currencies_for_select
Money::Currency.all_instances.sort_by { |currency| [ currency.priority, currency.name ] }
end
end

View file

@ -3,7 +3,12 @@ import { Controller } from "@hotwired/stimulus";
// Connects to data-controller="dialog"
export default class extends Controller {
static targets = ["leftSidebar", "rightSidebar", "mobileSidebar"];
static classes = ["leftSidebar", "rightSidebar"];
static classes = [
"expandedSidebar",
"collapsedSidebar",
"expandedTransition",
"collapsedTransition",
];
openMobileSidebar() {
this.mobileSidebarTarget.classList.remove("hidden");
@ -14,19 +19,37 @@ export default class extends Controller {
}
toggleLeftSidebar() {
this.#updateUserPreference(
"show_sidebar",
this.leftSidebarTarget.classList.contains("hidden"),
);
this.leftSidebarTarget.classList.toggle("hidden");
const isOpen = this.leftSidebarTarget.classList.contains("w-full");
this.#updateUserPreference("show_sidebar", !isOpen);
this.#toggleSidebarWidth(this.leftSidebarTarget, isOpen);
}
toggleRightSidebar() {
this.#updateUserPreference(
"show_ai_sidebar",
this.rightSidebarTarget.classList.contains("hidden"),
);
this.rightSidebarTarget.classList.toggle("hidden");
const isOpen = this.rightSidebarTarget.classList.contains("w-full");
this.#updateUserPreference("show_ai_sidebar", !isOpen);
this.#toggleSidebarWidth(this.rightSidebarTarget, isOpen);
}
#toggleSidebarWidth(el, isCurrentlyOpen) {
if (isCurrentlyOpen) {
el.classList.remove(...this.expandedSidebarClasses);
el.classList.add(...this.collapsedSidebarClasses);
// Wait for existing transition to finish
setTimeout(() => {
el.classList.remove(this.expandedTransitionClass);
el.classList.add(this.collapsedTransitionClass);
}, 1000);
} else {
el.classList.add(...this.expandedSidebarClasses);
el.classList.remove(...this.collapsedSidebarClasses);
// Wait for existing transition to finish
setTimeout(() => {
el.classList.add(this.expandedTransitionClass);
el.classList.remove(this.collapsedTransitionClass);
}, 1000);
}
}
#updateUserPreference(field, value) {

View file

@ -0,0 +1,8 @@
import { Controller } from "@hotwired/stimulus";
// Connects to data-controller="intercom"
export default class extends Controller {
show() {
Intercom("show");
}
}

View file

@ -27,12 +27,14 @@ class BalanceSheet
key: "asset",
display_name: "Assets",
icon: "blocks",
total_money: total_assets_money,
account_groups: account_groups("asset")
),
ClassificationGroup.new(
key: "liability",
display_name: "Debts",
icon: "scale",
total_money: total_liabilities_money,
account_groups: account_groups("liability")
)
]
@ -75,7 +77,7 @@ class BalanceSheet
end
private
ClassificationGroup = Struct.new(:key, :display_name, :icon, :account_groups, keyword_init: true)
ClassificationGroup = Struct.new(:key, :display_name, :icon, :total_money, :account_groups, keyword_init: true)
AccountGroup = Struct.new(:key, :name, :accountable_type, :classification, :total, :total_money, :weight, :accounts, :color, :missing_rates?, keyword_init: true)
def active_accounts

View file

@ -146,6 +146,10 @@ class Family < ApplicationRecord
false
end
def missing_data_provider?
requires_data_provider? && Provider::Registry.get_provider(:synth).nil?
end
def primary_user
users.order(:created_at).first
end

View file

@ -84,6 +84,10 @@ class Period
def all
PERIODS.map { |key, period| from_key(key) }
end
def as_options
all.map { |period| [ period.label_short, period.key ] }
end
end
PERIODS.each do |key, period|

View file

@ -1,7 +1,7 @@
<%# locals: (family:, active_account_group_tab:) %>
<div>
<% if family.requires_data_provider? && Provider::Registry.get_provider(:synth).nil? || true %>
<% if family.missing_data_provider? %>
<details class="group bg-yellow-tint-10 rounded-lg p-2 text-yellow-600 mb-3 text-xs">
<summary class="flex items-center justify-between gap-2">
<div class="flex items-center gap-2">

View file

@ -24,7 +24,11 @@
data: { "auto-submit-form-target": "auto" } %>
<% end %>
<%= period_select form: form, selected: period %>
<%= form.select :period,
Period.as_options,
{ selected: period.key },
data: { "auto-submit-form-target": "auto" },
class: "border border-secondary font-medium rounded-lg px-3 py-2 text-sm pr-7 cursor-pointer text-primary focus:outline-hidden focus:ring-0" %>
</div>
<% end %>
</div>

View file

@ -6,15 +6,23 @@
] %>
<% desktop_nav_items = mobile_nav_items.reject { |item| item[:mobile_only] } %>
<% expanded_sidebar_class = "w-full opacity-100" %>
<% collapsed_sidebar_class = "w-0 opacity-0" %>
<% collapsed_transition_class = "from-zero-width-transition" %>
<% expanded_transition_class = "to-zero-width-transition" %>
<%= render "layouts/shared/htmldoc" do %>
<div
class="flex flex-col lg:flex-row h-dvh lg:h-full bg-surface"
data-controller="app-layout"
data-app-layout-expanded-sidebar-class="<%= expanded_sidebar_class %>"
data-app-layout-collapsed-sidebar-class="<%= collapsed_sidebar_class %>"
data-app-layout-collapsed-transition-class="<%= collapsed_transition_class %>"
data-app-layout-expanded-transition-class="<%= expanded_transition_class %>"
data-app-layout-user-id-value="<%= Current.user.id %>">
<div
class="hidden fixed inset-0 bg-surface z-20 h-dvh w-full p-3 overflow-y-auto transition-all duration-300"
data-app-layout-target="mobileSidebar">
class="hidden fixed inset-0 bg-surface z-20 h-dvh w-full p-3 overflow-y-auto transition-all duration-300"
data-app-layout-target="mobileSidebar">
<div class="mb-2">
<%= icon("x", as_button: true, data: { action: "app-layout#closeMobileSidebar" }) %>
</div>
@ -54,14 +62,25 @@
<% end %>
</ul>
<div class="pl-2 mt-auto mx-auto">
<div class="pl-2 mt-auto mx-auto flex flex-col gap-2">
<%= render ButtonComponent.new(
variant: "icon",
icon: "message-circle-question",
data: { action: "intercom#show" }
) %>
<%= render "users/user_menu", user: Current.user %>
</div>
</nav>
</div>
<%# DESKTOP - Left sidebar %>
<%= tag.div class: class_names("hidden py-4 overflow-y-auto shrink-0 w-[320px]", "lg:block" => Current.user.show_sidebar?), data: { app_layout_target: "leftSidebar" } do %>
<%= tag.div class: class_names(
"hidden lg:block py-4 overflow-y-auto shrink-0 max-w-[320px]",
Current.user.show_sidebar? ? expanded_sidebar_class : collapsed_sidebar_class,
Current.user.show_sidebar? ? expanded_transition_class : collapsed_transition_class
),
data: { app_layout_target: "leftSidebar" } do %>
<% if content_for?(:sidebar) %>
<%= yield :sidebar %>
<% else %>
@ -94,7 +113,12 @@
<% end %>
<%# DESKTOP - Right sidebar %>
<%= tag.div class: class_names("hidden h-full overflow-y-auto shrink-0 w-[400px]", "lg:block" => Current.user.show_ai_sidebar?), data: { app_layout_target: "rightSidebar" } do %>
<%= tag.div class: class_names(
"hidden lg:block h-full overflow-y-auto shrink-0 max-w-[400px]",
Current.user.show_ai_sidebar? ? expanded_sidebar_class : collapsed_sidebar_class,
Current.user.show_ai_sidebar? ? expanded_transition_class : collapsed_transition_class
),
data: { app_layout_target: "rightSidebar" } do %>
<%= tag.div id: "chat-container",
class: class_names("flex flex-col h-full justify-between shrink-0 transition-all duration-300"),
data: { controller: "chat hotkey", turbo_permanent: true } do %>

View file

@ -1,5 +1,5 @@
<!DOCTYPE html>
<html class="h-full text-primary overflow-hidden lg:overflow-auto font-sans <%= @os %>" lang="en" data-controller="theme" data-theme-user-preference-value="<%= Current.user&.theme || "system" %>">
<html class="h-full text-primary overflow-hidden lg:overflow-auto font-sans <%= @os %>" lang="en" data-controller="theme intercom" data-theme-user-preference-value="<%= Current.user&.theme || "system" %>">
<head>
<%= render "layouts/shared/head" %>
<%= yield :head %>
@ -21,7 +21,7 @@
<%= family_stream %>
<% if Rails.env.development? %>
<div class="fixed bottom-20 left-2 flex flex-col gap-1">
<div class="fixed bottom-32 left-7 flex flex-col gap-1">
<%= icon("eclipse", as_button: true, data: { action: "theme#toDark" }) %>
<%= icon("sun", as_button: true, data: { action: "theme#toLight" }) %>
</div>

View file

@ -69,7 +69,7 @@
{ data: { action: "onboarding#setLocale" } } %>
<%= family_form.select :currency,
currencies_for_select.map { |currency| [ "#{currency.name} (#{currency.iso_code})", currency.iso_code ] },
Money::Currency.as_options.map { |currency| [ "#{currency.name} (#{currency.iso_code})", currency.iso_code ] },
{ label: t(".currency"), required: true, selected: params[:currency] || @user.family.currency },
{ data: { action: "onboarding#setCurrency" } } %>

View file

@ -1,16 +1,24 @@
<% content_for :page_header do %>
<div class="space-y-1 mb-6 flex justify-between items-center">
<div class="space-y-1 mb-6 flex justify-between items-center lg:items-start">
<div class="space-y-1">
<h1 class="text-3xl font-medium text-primary">Welcome back, <%= Current.user.first_name %></h1>
<p class="text-gray-500">Here's what's happening with your finances</p>
</div>
<%= render LinkComponent.new(
icon: "plus",
text: "New",
href: new_account_path,
frame: :modal,
class: "hidden lg:inline-flex"
) %>
<%= render LinkComponent.new(
variant: "icon-inverse",
icon: "plus",
href: new_account_path(step: "method_select", classification: "asset"),
href: new_account_path,
frame: :modal,
class: "rounded-full md:hidden"
class: "rounded-full lg:hidden"
) %>
</div>
<% end %>

View file

@ -1,9 +1,17 @@
<%# locals: (balance_sheet:) %>
<div class="space-y-4">
<div class="space-y-4 overflow-x-auto">
<% balance_sheet.classification_groups.each do |classification_group| %>
<div class="bg-container shadow-border-xs rounded-xl space-y-4 p-4">
<h2 class="text-lg font-medium"><%= classification_group.display_name %></h2>
<h2 class="text-lg font-medium inline-flex items-center gap-1.5">
<span>
<%= classification_group.display_name %>
</span>
<span class="text-secondary">&middot;</span>
<span class="text-secondary font-medium text-lg"><%= classification_group.total_money.format(precision: 0) %></span>
</h2>
<% if classification_group.account_groups.any? %>
<div class="space-y-4">
@ -23,9 +31,9 @@
</div>
</div>
<div class="bg-surface rounded-xl p-1 space-y-1">
<div class="bg-surface rounded-xl p-1 space-y-1 overflow-x-auto">
<div class="px-4 py-2 flex items-center uppercase text-xs font-medium text-secondary">
<div class="hidden sm:block">Name</div>
<div class="w-40">Name</div>
<div class="ml-auto text-right flex items-center gap-6">
<div class="w-24">
<p>Weight</p>
@ -36,33 +44,22 @@
</div>
</div>
<div class="shadow-border-xs rounded-lg bg-container">
<div class="shadow-border-xs rounded-lg bg-container min-w-fit">
<% 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-container rounded-lg flex items-center justify-between">
<div class="items-center gap-4 hidden md:flex">
<div class="w-40 shrink-0 flex items-center gap-4">
<%= icon("chevron-right", class: "group-open:rotate-90") %>
<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">
<div class="hidden sm:flex sm:items-center sm:justify-end sm: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="flex sm:hidden items-center gap-2">
<div class="flex gap-[3px]">
<% 10.times do |i| %>
<div class="w-[2px] h-[10px] rounded-lg <%= i < (account_group.weight / 10.0).ceil ? "" : "opacity-20" %>" style="background-color: <%= account_group.color %>;"></div>
<% end %>
</div>
<p class="text-sm"><%= number_to_percentage(account_group.weight, precision: 2) %></p>
</div>
<div class="w-24 shrink-0 flex items-center justify-end gap-2">
<%= render "pages/dashboard/group_weight", weight: account_group.weight, color: account_group.color %>
</div>
<div class="w-40">
<div class="w-40 shrink-0">
<p><%= format_money(account_group.total_money) %></p>
</div>
</div>
@ -70,39 +67,24 @@
<div>
<% account_group.accounts.each_with_index do |account, idx| %>
<div class="pl-4 sm:pl-12 pr-4 py-3 flex items-center justify-between text-sm font-medium">
<div class="hidden sm:flex sm:items-center sm:gap-3">
<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">
<div class="hidden sm:flex sm:items-center sm:justify-end sm: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="flex sm:hidden items-center gap-2">
<div class="flex gap-[3px]">
<% 10.times do |i| %>
<div class="w-[2px] h-[10px] rounded-lg <%= i < (account.weight / 10.0).ceil ? "" : "opacity-20" %>" style="background-color: <%= account_group.color %>;"></div>
<% end %>
</div>
<p class="text-sm"><%= number_to_percentage(account.weight, precision: 2) %></p>
</div>
<div class="w-24 shrink-0 flex items-center justify-end gap-2">
<%= render "pages/dashboard/group_weight", weight: account.weight, color: account_group.color %>
</div>
<div class="w-40">
<div class="w-40 shrink-0">
<p><%= format_money(account.balance_money) %></p>
<div class="sm:hidden text-xs text-secondary truncate max-w-28">
<%= account.name %>
</div>
</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>

View file

@ -0,0 +1,10 @@
<%# locals: (weight:, color:) %>
<div class="flex items-center gap-2">
<div class="flex gap-[3px]">
<% 10.times do |i| %>
<div class="w-[2px] h-[10px] rounded-lg <%= i < (weight / 10.0).ceil ? "" : "opacity-20" %>" style="background-color: <%= color %>;"></div>
<% end %>
</div>
<p class="text-sm"><%= number_to_percentage(weight, precision: 2) %></p>
</div>

View file

@ -14,8 +14,13 @@
<% 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 %>
<%= form_with url: root_path, method: :get, data: { controller: "auto-submit-form" } do |form| %>
<%= form.select :period,
Period.as_options,
{ selected: period.key },
data: { "auto-submit-form-target": "auto" },
class: "border border-secondary font-medium rounded-lg px-3 py-2 text-sm pr-7 cursor-pointer text-primary focus:outline-hidden focus:ring-0" %>
<% end %>
</div>

View file

@ -8,13 +8,14 @@
<%= image_tag "github-icon.svg", class: "w-8 h-8 mb-2" %>
<span class="text-sm font-medium text-primary text-center">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-full md:w-1/3 flex flex-col items-center p-4 border border-alpha-black-25 rounded-xl hover:bg-container-hover" do %>
<%= image_tag "github-icon.svg", class: "w-8 h-8 mb-2" %>
<span class="text-sm font-medium text-primary text-center">File a bug report</span>
<% end %>
<% else %>
<%= link_to "mailto:hello@maybefinance.com", class: "w-full md:w-1/3 flex flex-col gap-2 items-center p-4 border border-alpha-black-25 rounded-xl hover:bg-container-hover", onclick: "Intercom('showNewMessage'); return false;" do %>
<%= tag.button class: "w-full md:w-1/3 flex flex-col gap-2 items-center p-4 border border-alpha-black-25 rounded-xl hover:bg-container-hover", data: { action: "intercom#show" } do %>
<%= image_tag "github-icon.svg", class: "w-8 h-8 mb-2" %>
<span class="text-sm font-medium text-primary text-center">File a bug report</span>
<% end %>

View file

@ -7,7 +7,7 @@
<%= form.fields_for :family do |family_form| %>
<%= family_form.select :currency,
currencies_for_select.map { |currency| [ "#{currency.name} (#{currency.iso_code})", currency.iso_code ] },
Money::Currency.as_options.map { |currency| [ "#{currency.name} (#{currency.iso_code})", currency.iso_code ] },
{ label: t(".currency") }, disabled: true %>
<%= family_form.select :locale,

View file

@ -44,7 +44,7 @@
<% unless options[:hide_currency] %>
<div>
<%= form.select currency_method,
currencies_for_select.map(&:iso_code),
Money::Currency.as_options.map(&:iso_code),
{ inline: true, selected: currency.iso_code },
{
class: "w-fit pr-5 disabled:text-subdued form-field__input",

View file

@ -32,12 +32,12 @@
<% menu.with_item(variant: "link", text: "Settings", icon: "settings", href: settings_profile_path(return_to: request.fullpath)) %>
<% menu.with_item(variant: "link", text: "Changelog", icon: "box", href: changelog_path) %>
<% menu.with_item(variant: "link", text: "Feedback", icon: "megaphone", href: feedback_path) %>
<% if self_hosted? %>
<% menu.with_item(variant: "link", text: "Feedback", icon: "megaphone", href: feedback_path) %>
<% menu.with_item(variant: "link", text: "Contact", icon: "message-square-more", href: "https://link.maybe.co/discord") %>
<% else %>
<% menu.with_item(variant: "link", text: "Contact", icon: "message-square-more", href: "mailto:hello@maybefinance.com") %>
<% menu.with_item(variant: "button", text: "Contact", icon: "message-square-more", data: { action: "intercom#show" }) %>
<% end %>
<% menu.with_item(variant: "divider") %>

View file

@ -35,6 +35,12 @@ class Money::Currency
all.values.map { |currency_data| new(currency_data["iso_code"]) }
end
def as_options
all_instances.sort_by do |currency|
[ currency.priority, currency.name ]
end
end
def popular
all.values.sort_by { |currency| currency["priority"] }.first(12).map { |currency_data| new(currency_data["iso_code"]) }
end