mirror of
https://github.com/maybe-finance/maybe.git
synced 2025-08-10 07:55:21 +02:00
Merge branch 'main' of github.com:maybe-finance/maybe into zachgoll/design-audit
This commit is contained in:
commit
63a976dcb6
11 changed files with 96 additions and 90 deletions
|
@ -117,4 +117,3 @@ end
|
|||
- Enforce `null` checks, unique indexes, and other simple validations in the DB
|
||||
- ActiveRecord validations _may_ mirror the DB level ones, but not 100% necessary. These are for convenience when error handling in forms. Always prefer client-side form validation when possible.
|
||||
- Complex validations and business logic should remain in ActiveRecord
|
||||
|
||||
|
|
|
@ -247,10 +247,3 @@ class ConcreteProvider < Provider
|
|||
end
|
||||
end
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -19,4 +19,4 @@ The codebase uses TailwindCSS v4.x (the newest version) with a custom design sys
|
|||
- Example 2: use `bg-container` rather than `bg-white`
|
||||
- Example 3: use `border border-primary` rather than `border border-gray-200`
|
||||
- Never create new styles in [maybe-design-system.css](mdc:app/assets/tailwind/maybe-design-system.css) or [application.css](mdc:app/assets/tailwind/application.css) without explicitly receiving permission to do so
|
||||
- Always generate semantic HTML
|
||||
- Always generate semantic HTML
|
||||
|
|
|
@ -4,7 +4,7 @@ It means so much that you're interested in contributing to Maybe! Seriously. Tha
|
|||
|
||||
## House Rules
|
||||
|
||||
- Before contributing, familiarize yourself with our project conventions. You should read through our [Project Conventions Rule](https://github.com/maybe-finance/maybe/.cursor/rules/project-conventions.mdc), which is intended for LLMs, but is also an excellent primer on how we write code for Maybe.
|
||||
- Before contributing, familiarize yourself with our project conventions. You should read through our [Project Conventions Rule](https://github.com/maybe-finance/maybe/.cursor/rules/project-conventions.mdc), which is intended for LLMs, but is also an excellent primer on how we write code for Maybe.
|
||||
- While totally optional, consider using Cursor + VSCode as it will automatically apply our project conventions to your code via the `.cursor/rules` directory.
|
||||
- Before contributing, please check if it already exists in [issues](https://github.com/maybe-finance/maybe/issues) or [PRs](https://github.com/maybe-finance/maybe/pulls)
|
||||
- Given the speed at which we're moving on the codebase, we don't assign issues or "give" issues to anyone.
|
||||
|
|
|
@ -3,6 +3,12 @@ class TransactionsController < ApplicationController
|
|||
|
||||
before_action :store_params!, only: :index
|
||||
|
||||
def new
|
||||
super
|
||||
@income_categories = Current.family.categories.incomes.alphabetically
|
||||
@expense_categories = Current.family.categories.expenses.alphabetically
|
||||
end
|
||||
|
||||
def index
|
||||
@q = search_params
|
||||
transactions_query = Current.family.transactions.active.search(@q)
|
||||
|
|
|
@ -10,29 +10,13 @@ module FormsHelper
|
|||
render partial: "shared/modal_form", locals: { title:, subtitle:, content:, overflow_visible: }
|
||||
end
|
||||
|
||||
def radio_tab_tag(form:, name:, value:, label:, icon:, checked: false, disabled: false, class: nil)
|
||||
form.label name, for: form.field_id(name, value), class: "group has-disabled:cursor-not-allowed" do
|
||||
concat radio_tab_contents(label:, icon:, class:)
|
||||
concat form.radio_button(name, value, checked:, disabled:, class: "hidden")
|
||||
end
|
||||
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
|
||||
|
||||
end
|
||||
|
||||
def currencies_for_select
|
||||
Money::Currency.all_instances.sort_by { |currency| [ currency.priority, currency.name ] }
|
||||
end
|
||||
|
||||
private
|
||||
def radio_tab_contents(label:, icon:, class: nil)
|
||||
tag.div(class: "flex px-4 py-1 rounded-lg items-center space-x-2 justify-center text-sm md:text-normal text-subdued group-has-checked:bg-surface group-has-checked:text-primary group-has-checked:shadow-sm") do
|
||||
concat lucide_icon(icon, class: "w-5 h-5")
|
||||
concat tag.span(label, class: "group-has-checked:font-semibold")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
22
app/javascript/controllers/transaction_form_controller.js
Normal file
22
app/javascript/controllers/transaction_form_controller.js
Normal file
|
@ -0,0 +1,22 @@
|
|||
import {Controller} from "@hotwired/stimulus"
|
||||
|
||||
export default class extends Controller {
|
||||
static targets = ["expenseCategories", "incomeCategories"]
|
||||
|
||||
connect() {
|
||||
this.updateCategories()
|
||||
}
|
||||
|
||||
updateCategories(event) {
|
||||
const natureField = this.element.querySelector('input[name="account_entry[nature]"]:checked')
|
||||
const natureValue = natureField ? natureField.value : 'outflow'
|
||||
|
||||
if (natureValue === 'inflow') {
|
||||
this.expenseCategoriesTarget.classList.add('hidden')
|
||||
this.incomeCategoriesTarget.classList.remove('hidden')
|
||||
} else {
|
||||
this.expenseCategoriesTarget.classList.remove('hidden')
|
||||
this.incomeCategoriesTarget.classList.add('hidden')
|
||||
}
|
||||
}
|
||||
}
|
|
@ -42,30 +42,29 @@
|
|||
<% end %>
|
||||
|
||||
<%= settings_section title: t(".theme_title"), subtitle: t(".theme_subtitle") do %>
|
||||
<div>
|
||||
<%= styled_form_with model: @user, class: "flex flex-col md:flex-row justify-between items-center gap-4", data: { controller: "auto-submit-form" } do |form| %>
|
||||
<div data-controller="theme" data-theme-user-preference-value="<%= @user.theme %>">
|
||||
<%= form_with model: @user, class: "flex flex-col md:flex-row justify-between items-center gap-4", id: "theme_form",
|
||||
data: { controller: "auto-submit-form", auto_submit_form_trigger_event_value: "change" } 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, "light", checked: @user.theme == "light", data: { auto_submit_form_target: "auto", action: "theme#updateTheme" } %>
|
||||
<%= form.label :theme_light, t(".theme_light"), value: "light" %>
|
||||
</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, "dark", checked: @user.theme == "dark", data: { auto_submit_form_target: "auto", action: "theme#updateTheme" } %>
|
||||
<%= form.label :theme_dark, t(".theme_dark"), value: "dark" %>
|
||||
</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, "system", checked: @user.theme == "system", data: { auto_submit_form_target: "auto", action: "theme#updateTheme" } %>
|
||||
<%= form.label :theme_system, t(".theme_system"), value: "system" %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<% theme_option_class = "text-center transition-all duration-200 p-3 rounded-lg hover:bg-surface-hover cursor-pointer [&:has(input:checked)]:bg-surface-hover [&:has(input:checked)]:border [&:has(input:checked)]:border-primary [&:has(input:checked)]:shadow-xs" %>
|
||||
|
||||
<% [
|
||||
{ value: "light", image: "light-mode-preview.png" },
|
||||
{ value: "dark", image: "dark-mode-preview.png" },
|
||||
{ value: "system", image: "system-mode-preview.png" }
|
||||
].each do |theme| %>
|
||||
<%= form.label :"theme_#{theme[:value]}", class: "group" do %>
|
||||
<div class="<%= theme_option_class %>">
|
||||
<%= image_tag(theme[:image], alt: "#{theme[:value].titleize} Theme Preview", class: "h-44 mb-2") %>
|
||||
<div class="<%= theme[:value] == 'system' ? 'flex items-center gap-2 justify-center' : 'text-sm font-medium text-primary' %>">
|
||||
<%= form.radio_button :theme, theme[:value], checked: @user.theme == theme[:value], class: "sr-only",
|
||||
data: { auto_submit_form_target: "auto", autosubmit_trigger_event: "change", action: "theme#updateTheme" } %>
|
||||
<%= t(".theme_#{theme[:value]}") %>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
|
24
app/views/shared/_transaction_type_tabs.html.erb
Normal file
24
app/views/shared/_transaction_type_tabs.html.erb
Normal file
|
@ -0,0 +1,24 @@
|
|||
<fieldset class="bg-gray-50 rounded-lg p-1 grid grid-flow-col justify-stretch gap-x-2">
|
||||
<% active_tab = local_assigns[:active_tab] || 'expense' %>
|
||||
|
||||
<%= link_to new_transaction_path(nature: 'outflow'),
|
||||
data: { turbo_frame: :modal },
|
||||
class: "flex px-4 py-1 rounded-lg items-center space-x-2 justify-center text-sm md:text-normal text-subdued #{active_tab == 'expense' ? 'bg-container text-gray-800 shadow-sm' : 'hover:bg-container hover:text-gray-800 hover:shadow-sm'}" do %>
|
||||
<%= lucide_icon "minus-circle", class: "w-4 h-4 md:w-5 md:h-5" %>
|
||||
<%= tag.span t("shared.transaction_tabs.expense") %>
|
||||
<% end %>
|
||||
|
||||
<%= link_to new_transaction_path(nature: 'inflow'),
|
||||
data: { turbo_frame: :modal },
|
||||
class: "flex px-4 py-1 rounded-lg items-center space-x-2 justify-center text-sm md:text-normal text-subdued #{active_tab == 'income' ? 'bg-container text-gray-800 shadow-sm' : 'hover:bg-container hover:text-gray-800 hover:shadow-sm'}" do %>
|
||||
<%= lucide_icon "plus-circle", class: "w-4 h-4 md:w-5 md:h-5" %>
|
||||
<%= tag.span t("shared.transaction_tabs.income") %>
|
||||
<% end %>
|
||||
|
||||
<%= link_to new_transfer_path,
|
||||
data: { turbo_frame: :modal },
|
||||
class: "flex px-4 py-1 rounded-lg items-center space-x-2 justify-center text-sm md:text-normal text-subdued #{active_tab == 'transfer' ? 'bg-container text-gray-800 shadow-sm' : 'hover:bg-container hover:text-gray-800 hover:shadow-sm'}" do %>
|
||||
<%= lucide_icon "arrow-right-left", class: "w-4 h-4 md:w-5 md:h-5" %>
|
||||
<%= tag.span t("shared.transaction_tabs.transfer") %>
|
||||
<% end %>
|
||||
</fieldset>
|
|
@ -1,25 +1,18 @@
|
|||
<%= styled_form_with model: @entry, url: transactions_path, class: "space-y-4 text-subdued" do |f| %>
|
||||
|
||||
<%= styled_form_with model: @entry, url: transactions_path, class: "space-y-4 text-subdued", data: { controller: "transaction-form" } do |f| %>
|
||||
<% if entry.errors.any? %>
|
||||
<%= render "shared/form_errors", model: entry %>
|
||||
<% end %>
|
||||
|
||||
<section>
|
||||
<fieldset class="bg-container rounded-lg p-1 grid grid-flow-col justify-stretch gap-x-1 md:gap-x-2">
|
||||
<%= radio_tab_tag form: f, name: :nature, value: :outflow, label: t(".expense"), icon: "minus-circle", checked: params[:nature] == "outflow" || params[:nature].nil?, class: "text-xs md:text-sm" %>
|
||||
<%= radio_tab_tag form: f, name: :nature, value: :inflow, label: t(".income"), icon: "plus-circle", checked: params[:nature] == "inflow", class: "text-xs md:text-sm" %>
|
||||
<%= link_to new_transfer_path, data: { turbo_frame: :modal }, class: "flex px-4 py-1 rounded-lg items-center space-x-2 justify-center text-subdued text-sm md:text-normal group-has-checked:bg-container group-has-checked:text-gray-800 group-has-checked:shadow-sm group-has-checked:text-sm" do %>
|
||||
<%= lucide_icon "arrow-right-left", class: "w-4 h-4 md:w-5 md:h-5" %>
|
||||
<%= tag.span t(".transfer") %>
|
||||
<% end %>
|
||||
</fieldset>
|
||||
<%= render "shared/transaction_type_tabs", active_tab: params[:nature] == "inflow" ? "income" : "expense" %>
|
||||
|
||||
<%= f.hidden_field :nature, value: params[:nature] || "outflow", data: { "transaction-form-target": "natureField" } %>
|
||||
<%= f.hidden_field :entryable_type, value: "Transaction" %>
|
||||
</section>
|
||||
|
||||
<section class="space-y-2 overflow-hidden">
|
||||
<%= f.text_field :name, label: t(".description"), placeholder: t(".description_placeholder"), required: true %>
|
||||
|
||||
<%= f.hidden_field :entryable_type, value: "Transaction" %>
|
||||
|
||||
<% if @entry.account_id %>
|
||||
<%= f.hidden_field :account_id %>
|
||||
<% else %>
|
||||
|
@ -28,7 +21,8 @@
|
|||
|
||||
<%= f.money_field :amount, label: t(".amount"), required: true %>
|
||||
<%= f.fields_for :entryable do |ef| %>
|
||||
<%= ef.collection_select :category_id, Current.family.categories.alphabetically, :id, :name, { prompt: t(".category_prompt"), label: t(".category") } %>
|
||||
<% categories = params[:nature] == "inflow" ? @income_categories : @expense_categories %>
|
||||
<%= ef.collection_select :category_id, categories, :id, :name, { prompt: t(".category_prompt"), label: t(".category") } %>
|
||||
<% end %>
|
||||
<%= f.date_field :date, label: t(".date"), required: true, min: Entry.min_supported_date, max: Date.current, value: Date.current %>
|
||||
</section>
|
||||
|
@ -36,19 +30,19 @@
|
|||
<%= disclosure t(".details"), default_open: false do %>
|
||||
<%= f.fields_for :entryable do |ef| %>
|
||||
<%= ef.select :tag_ids,
|
||||
Current.family.tags.alphabetically.pluck(:name, :id),
|
||||
{
|
||||
include_blank: t(".none"),
|
||||
multiple: true,
|
||||
label: t(".tags_label"),
|
||||
container_class: "h-40"
|
||||
} %>
|
||||
Current.family.tags.alphabetically.pluck(:name, :id),
|
||||
{
|
||||
include_blank: t(".none"),
|
||||
multiple: true,
|
||||
label: t(".tags_label"),
|
||||
container_class: "h-40"
|
||||
} %>
|
||||
<% end %>
|
||||
<%= f.text_area :notes,
|
||||
label: t(".note_label"),
|
||||
placeholder: t(".note_placeholder"),
|
||||
rows: 5,
|
||||
"data-auto-submit-form-target": "auto" %>
|
||||
label: t(".note_label"),
|
||||
placeholder: t(".note_placeholder"),
|
||||
rows: 5,
|
||||
"data-auto-submit-form-target": "auto" %>
|
||||
<% end %>
|
||||
|
||||
<section>
|
||||
|
|
|
@ -7,22 +7,7 @@
|
|||
<% end %>
|
||||
|
||||
<section>
|
||||
<fieldset class="bg-container rounded-lg p-1 grid grid-flow-col justify-stretch gap-x-1">
|
||||
<%= link_to new_transaction_path(nature: "expense"), data: { turbo_frame: :modal }, class: "flex px-4 py-1 rounded-lg items-center space-x-2 justify-center text-subdued" do %>
|
||||
<%= lucide_icon "minus-circle", class: "w-4 h-4" %>
|
||||
<%= tag.span t(".expense") %>
|
||||
<% end %>
|
||||
|
||||
<%= link_to new_transaction_path(nature: "income"), data: { turbo_frame: :modal }, class: "flex px-4 py-1 rounded-lg items-center space-x-2 justify-center text-subdued" do %>
|
||||
<%= lucide_icon "plus-circle", class: "w-4 h-4" %>
|
||||
<%= tag.span t(".income") %>
|
||||
<% end %>
|
||||
|
||||
<%= tag.div class: "flex px-4 py-1 rounded-lg items-center space-x-2 justify-center bg-container text-primary shadow-sm" do %>
|
||||
<%= lucide_icon "arrow-right-left", class: "w-4 h-4" %>
|
||||
<%= tag.span t(".transfer") %>
|
||||
<% end %>
|
||||
</fieldset>
|
||||
<%= render "shared/transaction_type_tabs", active_tab: "transfer" %>
|
||||
</section>
|
||||
|
||||
<section class="space-y-2">
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue