1
0
Fork 0
mirror of https://github.com/maybe-finance/maybe.git synced 2025-07-24 15:49:39 +02:00

New Add Account UI

* Add Lucide gem (#364)

* feat: add cursor pointer in the log-in and create account pages, also make full with (it's cutted right now) the fileds in the settings edit page

* feat: skip system test with an explanation instead of comment them

* fix typo in the skip

* feat: add lucide gem

* Add reusable modal (#362)

* Remove unused form

* Add reusable modal

* Prelim styling

* Add instructions

---------

Co-authored-by: Josh Pigford <josh@joshpigford.com>

* Add keyboard navigation to new account selector (#375)

* New account menu (#372)

* New account menu

* Styling tweaks

---------

Signed-off-by: Josh Pigford <josh@joshpigford.com>

* Entry method links (#376)

* Initial add account form (#378)

* Initial add account form

* Unused

---------

Signed-off-by: Josh Pigford <josh@joshpigford.com>
Co-authored-by: Pedro López Mareque <Pedro.lopez.mareque@gmail.com>
Co-authored-by: Rob Zolkos <rob@zolkos.com>
Co-authored-by: Josh Brown <josh@joossh.com>
This commit is contained in:
Josh Pigford 2024-02-08 10:46:05 -06:00 committed by GitHub
parent e79636f372
commit 4761619870
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 208 additions and 57 deletions

View file

@ -17,6 +17,7 @@ gem "bootsnap", require: false
gem "importmap-rails"
gem "propshaft"
gem "tailwindcss-rails"
gem "lucide-rails", github: "maybe-finance/lucide-rails"
# Hotwire
gem "stimulus-rails"

View file

@ -1,3 +1,10 @@
GIT
remote: https://github.com/maybe-finance/lucide-rails.git
revision: 6170b3a0eceb43a8af6552638e9526673c356d0d
specs:
lucide-rails (0.2.0)
railties (>= 4.1.0)
GIT
remote: https://github.com/rails/rails.git
revision: bab4aa7cb25112846e04cea907361a1de0f126ef
@ -325,7 +332,7 @@ GEM
rubyzip (>= 1.2.2, < 3.0)
websocket (~> 1.0)
smart_properties (1.17.0)
sorbet-runtime (0.5.11226)
sorbet-runtime (0.5.11234)
stimulus-rails (1.3.3)
railties (>= 6.0.0)
stringio (3.1.0)
@ -388,6 +395,7 @@ DEPENDENCIES
inline_svg
jbuilder
letter_opener
lucide-rails!
money-rails (~> 1.12)
pg (~> 1.1)
propshaft

View file

@ -10,4 +10,10 @@ module ApplicationHelper
def permitted_accountable_partial(name)
name.underscore
end
# Wrap view with <%= modal do %> ... <% end %> to have it open in a modal
# Make sure to add data-turbo-frame="modal" to the link/button that opens the modal
def modal(&block)
render "shared/modal", &block
end
end

View file

@ -0,0 +1,13 @@
import { Controller } from "@hotwired/stimulus"
import { install, uninstall } from "@github/hotkey"
// Connects to data-controller="hotkey"
export default class extends Controller {
connect() {
install(this.element)
}
disconnect() {
uninstall(this.element)
}
}

View file

@ -0,0 +1,36 @@
import { Controller } from "@hotwired/stimulus"
// Connects to data-controller="list-keyboard-navigation"
export default class extends Controller {
focusPrevious() {
this.focusLinkTargetInDirection(-1)
}
focusNext() {
this.focusLinkTargetInDirection(1)
}
focusLinkTargetInDirection(direction) {
const element = this.getLinkTargetInDirection(direction)
element?.focus()
}
getLinkTargetInDirection(direction) {
const indexOfLastFocus = this.indexOfLastFocus()
return this.focusableLinks[indexOfLastFocus + direction]
}
indexOfLastFocus(targets = this.focusableLinks) {
const indexOfActiveElement = targets.indexOf(document.activeElement)
if (indexOfActiveElement !== -1) {
return indexOfActiveElement
} else {
return targets.findIndex(target => target.getAttribute("tabindex") === "0")
}
}
get focusableLinks() {
return Array.from(this.element.querySelectorAll("a[href]"))
}
}

View file

@ -0,0 +1,15 @@
import { Controller } from "@hotwired/stimulus"
// Connects to data-controller="modal"
export default class extends Controller {
connect() {
this.element.showModal();
}
// Hide the dialog when the user clicks outside of it
click_outside(e) {
if (e.target === this.element) {
this.element.close();
}
}
}

View file

@ -1,9 +1,6 @@
<div class="relative flex-col items-center px-5 py-4 space-x-3 text-center bg-white border border-gray-100 shadow-sm rounded-xl hover:border-gray-200">
<%= link_to new_account_path(type: type.class.name.demodulize), class: "flex flex-col items-center justify-center w-full text-center focus:outline-none" do %>
<span class="absolute inset-0" aria-hidden="true"></span>
<span class="flex w-10 h-10 shrink-0 grow-0 items-center justify-center rounded-xl <%= bg_color %> mb-2">
<%= inline_svg_tag(icon, class: "#{text_color} stroke-current") %>
</span>
<%= type.model_name.human %>
<% end %>
</div>
<%= link_to new_account_path(step: 'method', type: type.class.name.demodulize), class: "flex items-center gap-4 w-full text-center focus:outline-none focus:bg-gray-50 border border-transparent focus:border focus:border-gray-200 block px-2 hover:bg-gray-50 rounded-lg p-2" do %>
<span class="flex w-8 h-8 shrink-0 grow-0 items-center justify-center rounded-lg <%= bg_color %> shadow-[inset_0_0_0_1px_rgba(0,0,0,0.02)]">
<%= lucide_icon(icon, class: "#{text_color} w-5 h-5") %>
</span>
<%= type.model_name.human %>
<% end %>

View file

@ -0,0 +1,15 @@
<% if local_assigns[:disabled] && disabled %>
<span class="flex items-center w-full gap-4 p-2 px-2 text-center border border-transparent rounded-lg cursor-not-allowed focus:outline-none focus:bg-gray-50 focus:border focus:border-gray-200 hover:bg-gray-50">
<span class="flex w-8 h-8 shrink-0 grow-0 items-center justify-center rounded-lg bg-[#141414]/5 shadow-[inset_0_0_0_1px_rgba(0,0,0,0.02)]">
<%= lucide_icon(icon, class: "text-gray-500 w-5 h-5") %>
</span>
<%= text %>
</span>
<% else %>
<%= link_to new_account_path(type: type.class.name.demodulize), class: "flex items-center gap-4 w-full text-center focus:outline-none focus:bg-gray-50 border border-transparent focus:border focus:border-gray-200 px-2 hover:bg-gray-50 rounded-lg p-2" do %>
<span class="flex w-8 h-8 shrink-0 grow-0 items-center justify-center rounded-lg bg-[#141414]/5 shadow-[inset_0_0_0_1px_rgba(0,0,0,0.02)]">
<%= lucide_icon(icon, class: "text-gray-500 w-5 h-5") %>
</span>
<%= text %>
<% end %>
<% end %>

View file

@ -1,4 +1,4 @@
<div class="relative p-4 border border-gray-100 bg-offwhite rounded-xl focus-within:bg-white focus-within:shadow focus-within:opacity-100">
<label for="account_name" class="block text-sm font-medium opacity-50 focus-within:opacity-100">Type</label>
<%= f.select :subtype, options_for_select([["Checking", "checking"], ["Savings", "savings"]], selected: ""), {}, class: "block w-full p-0 mt-1 bg-transparent border-none focus:outline-none focus:ring-0" %>
</div>
<div class="relative p-3 border border-[#141414]/8 bg-white rounded-xl focus-within:bg-white focus-within:shadow-none focus-within:border-gray-900 focus-within:ring-4 focus-within:ring-gray-100 focus-within:opacity-100 shadow-sm">
<label for="account_name" class="block text-sm font-medium opacity-50 focus-within:opacity-100">Type</label>
<%= f.select :subtype, options_for_select([["Checking", "checking"], ["Savings", "savings"]], selected: ""), {}, class: "block w-full p-0 mt-1 bg-transparent border-none focus:outline-none focus:ring-0" %>
</div>

View file

@ -1,4 +1,4 @@
<div class="relative p-4 border border-gray-100 bg-offwhite rounded-xl focus-within:bg-white focus-within:shadow focus-within:opacity-100">
<div class="relative p-3 border border-[#141414]/8 bg-white rounded-xl focus-within:bg-white focus-within:shadow-none focus-within:border-gray-900 focus-within:ring-4 focus-within:ring-gray-100 focus-within:opacity-100 shadow-sm">
<label for="account_name" class="block text-sm font-medium opacity-50 focus-within:opacity-100">Type</label>
<%= f.select :subtype, options_for_select(Account::Investment::SUBTYPES, selected: ""), {}, class: "block w-full p-0 mt-1 bg-transparent border-none focus:outline-none focus:ring-0" %>
</div>

View file

@ -1,47 +1,94 @@
<h1 class="text-3xl font-semibold font-display"><%= t('.title')%></h1>
<% if params[:type].blank? || Account.accountable_types.include?("Account::#{params[:type]}") == false %>
<div class="grid grid-cols-2 gap-4 mt-8 text-sm sm:grid-cols-3 md:grid-cols-4">
<%= render "account_type", type: Account::Credit.new, bg_color: "bg-[#E6F6FA]", text_color: "text-[#189FC7]", icon: "icon-credit-card.svg" %>
<%= render "account_type", type: Account::Depository.new, bg_color: "bg-[#EAF4FF]", text_color: "text-[#3492FB]", icon: "icon-bank-accounts.svg" %>
<%= render "account_type", type: Account::Investment.new, bg_color: "bg-[#EDF7F4]", text_color: "text-[#1BD5A1]", icon: "icon-bank-accounts.svg" %>
<%= render "account_type", type: Account::Loan.new, bg_color: "bg-[#EDF7F4]", text_color: "text-[#1BD5A1]", icon: "icon-bank-accounts.svg" %>
<%= render "account_type", type: Account::OtherAsset.new, bg_color: "bg-[#EDF7F4]", text_color: "text-[#1BD5A1]", icon: "icon-bank-accounts.svg" %>
<%= render "account_type", type: Account::OtherLiability.new, bg_color: "bg-[#EDF7F4]", text_color: "text-[#1BD5A1]", icon: "icon-bank-accounts.svg" %>
<%= render "account_type", type: Account::Property.new, bg_color: "bg-[#FEF0F7]", text_color: "text-[#F03695]", icon: "icon-real-estate.svg" %>
<%= render "account_type", type: Account::Vehicle.new, bg_color: "bg-[#EDF7F4]", text_color: "text-[#1BD5A1]", icon: "icon-bank-accounts.svg" %>
</div>
<% else %>
<div class="relative flex items-center mb-5 space-x-2">
<%= link_to new_account_path, class: "" do %>
<%= inline_svg_tag('icon-arrow-left.svg', class: 'text-gray-500 fill-current') %>
<% end %>
<h2 class="text-2xl font-semibold font-display"><%= t('.enter_type_account', type: @account.accountable_class.model_name.human.downcase ) %></h2>
</div>
<%= form_with model: @account, url: accounts_path, scope: :account, html: { class: "space-y-4" } do |f| %>
<%= f.hidden_field :accountable_type %>
<div class="relative p-4 border border-gray-100 bg-offwhite rounded-xl focus-within:bg-white focus-within:shadow focus-within:opacity-100">
<%# <span class="absolute px-2 py-1 text-xs font-medium text-gray-400 uppercase bg-gray-200 rounded-full opacity-50 right-3">Optional</span> %>
<%= f.label :name, class: 'block text-sm font-medium opacity-50 focus-within:opacity-100' %>
<%= f.text_field :name, placeholder: t('.account_name_placeholder'), required: 'required', class: "p-0 mt-1 bg-transparent border-none opacity-50 focus:outline-none focus:ring-0 focus-within:opacity-100" %>
<%= modal do %>
<% if params[:type].blank? || Account.accountable_types.include?("Account::#{params[:type]}") == false %>
<div class="border-b border-[#141414]/2 p-4 text-gray-400">
What would you like to add?
</div>
<div class="flex flex-col p-2 text-sm" data-controller="list-keyboard-navigation">
<button hidden data-controller="hotkey" data-hotkey="k,K,ArrowUp,ArrowLeft" data-action="list-keyboard-navigation#focusPrevious">Previous</button>
<button hidden data-controller="hotkey" data-hotkey="j,J,ArrowDown,ArrowRight" data-action="list-keyboard-navigation#focusNext">Next</button>
<%= render "accounts/#{permitted_accountable_partial(@account.accountable_type)}", f: f %>
<div class="relative p-4 border border-gray-100 bg-offwhite rounded-xl focus-within:bg-white focus-within:shadow focus-within:opacity-100">
<%= f.label :balance, class: 'block text-sm font-medium opacity-50 focus-within:opacity-100' %>
<div class="flex">
<%= f.number_field :balance, placeholder: number_to_currency(0), in: 0.00..100000000.00, required: 'required', class: "p-0 mt-1 bg-transparent border-none opacity-50 focus:outline-none focus:ring-0 focus-within:opacity-100" %>
<%= render "account_type", type: Account::Depository.new, bg_color: "bg-[#EFF8FF]", text_color: "text-[#2E90FA]", icon: "landmark" %>
<%= render "account_type", type: Account::Investment.new, bg_color: "bg-[#ECFDF3]", text_color: "text-[#32D583]", icon: "line-chart" %>
<%= render "account_type", type: Account::Property.new, bg_color: "bg-[#FCF5F9]", text_color: "text-[#F23E94]", icon: "home" %>
<%= render "account_type", type: Account::Vehicle.new, bg_color: "bg-[#EEF4FF]", text_color: "text-[#6172F3]", icon: "car-front" %>
<%= render "account_type", type: Account::Credit.new, bg_color: "bg-[#F0F9FF]", text_color: "text-[#36BFFA]", icon: "credit-card" %>
<%= render "account_type", type: Account::Loan.new, bg_color: "bg-[#FEF6EE]", text_color: "text-[#F38744]", icon: "hand-coins" %>
<%= render "account_type", type: Account::OtherAsset.new, bg_color: "bg-[#ECFDF3]", text_color: "text-[#12B76A]", icon: "plus" %>
<%= render "account_type", type: Account::OtherLiability.new, bg_color: "bg-[#FEF3F2]", text_color: "text-[#F04438]", icon: "minus" %>
</div>
<div class="border-t border-[#141414]/2 p-4 text-gray-500 text-sm flex justify-between">
<div class="flex space-x-5">
<div class="flex items-center space-x-2">
<span>Select</span> <kbd class="bg-[#141414]/5 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.1)] p-1 rounded-md flex w-5 h-5 shrink-0 grow-0 items-center justify-center"><%= lucide_icon('corner-down-left', class: 'inline w-3 h-3')%></kbd>
</div>
<div class="flex items-center space-x-2">
<span>Navigate</span> <kbd class="bg-[#141414]/5 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.1)] p-1 rounded-md flex w-5 h-5 shrink-0 grow-0 items-center justify-center"><%= lucide_icon('arrow-up', class: 'inline w-3 h-3')%></kbd> <kbd class="bg-[#141414]/5 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.1)] p-1 rounded-md flex w-5 h-5 shrink-0 grow-0 items-center justify-center"><%= lucide_icon('arrow-down', class: 'inline w-3 h-3')%></kbd>
</div>
</div>
<div class="flex items-center space-x-2">
<span>Close</span> <kbd class="bg-[#141414]/5 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.1)] p-1 rounded-md flex w-8 h-5 shrink-0 grow-0 items-center justify-center text-xs">ESC</kbd>
</div>
</div>
<div class="absolute right-5 bottom-5">
<button type="submit" class="flex items-center justify-center w-12 h-12 mb-2 bg-black rounded-full shrink-0 grow-0 hover:bg-gray-600" title="Submit">
<%= inline_svg_tag('icn-check.svg', class: 'text-white fill-current') %>
</button>
<% elsif params[:step] == 'method' && params[:type].present? %>
<div class="border-b border-[#141414]/2 p-4 text-gray-400 flex items-center space-x-3">
<%= link_to new_account_path, class: "flex w-8 h-8 shrink-0 grow-0 items-center justify-center rounded-lg bg-[#141414]/5" do %>
<%= lucide_icon('arrow-left', class: 'text-gray-500 w-5 h-5') %>
<% end %>
<span>How would you like to add it?</span>
</div>
<div class="flex flex-col p-2 text-sm" data-controller="list-keyboard-navigation">
<button hidden data-controller="hotkey" data-hotkey="k,K,ArrowUp,ArrowLeft" data-action="list-keyboard-navigation#focusPrevious">Previous</button>
<button hidden data-controller="hotkey" data-hotkey="j,J,ArrowDown,ArrowRight" data-action="list-keyboard-navigation#focusNext">Next</button>
<%= render "entry_method", type: Account::Depository.new, text: 'Enter account balance manually', icon: "keyboard" %>
<%= render "entry_method", type: Account::Depository.new, text: 'Securely link bank account with data provider (coming soon)', icon: "link-2", disabled: true %>
<%= render "entry_method", type: Account::Depository.new, text: 'Upload spreadsheet (coming soon)', icon: "sheet", disabled: true %>
</div>
<div class="border-t border-[#141414]/2 p-4 text-gray-500 text-sm flex justify-between">
<div class="flex space-x-5">
<div class="flex items-center space-x-2">
<span>Select</span> <kbd class="bg-[#141414]/5 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.1)] p-1 rounded-md flex w-5 h-5 shrink-0 grow-0 items-center justify-center"><%= lucide_icon('corner-down-left', class: 'inline w-3 h-3')%></kbd>
</div>
<div class="flex items-center space-x-2">
<span>Navigate</span> <kbd class="bg-[#141414]/5 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.1)] p-1 rounded-md flex w-5 h-5 shrink-0 grow-0 items-center justify-center"><%= lucide_icon('arrow-up', class: 'inline w-3 h-3')%></kbd> <kbd class="bg-[#141414]/5 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.1)] p-1 rounded-md flex w-5 h-5 shrink-0 grow-0 items-center justify-center"><%= lucide_icon('arrow-down', class: 'inline w-3 h-3')%></kbd>
</div>
</div>
<div class="flex items-center space-x-2">
<span>Close</span> <kbd class="bg-[#141414]/5 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.1)] p-1 rounded-md flex w-8 h-5 shrink-0 grow-0 items-center justify-center text-xs">ESC</kbd>
</div>
</div>
<% else %>
<div class="border-b border-[#141414]/2 p-4 text-gray-800 flex items-center space-x-3">
<%= link_to new_account_path(step: 'method', type: params[:type]), class: "flex w-8 h-8 shrink-0 grow-0 items-center justify-center rounded-lg bg-[#141414]/5" do %>
<%= lucide_icon('arrow-left', class: 'text-gray-500 w-5 h-5') %>
<% end %>
<span>Add account</span>
</div>
<%= form_with model: @account, url: accounts_path, scope: :account, html: { class: "space-y-4 m-5 mt-1", data: { turbo: false } } do |f| %>
<%= f.hidden_field :accountable_type %>
<div class="relative p-3 border border-[#141414]/8 bg-white rounded-xl focus-within:bg-white focus-within:shadow-none focus-within:border-gray-900 focus-within:ring-4 focus-within:ring-gray-100 focus-within:opacity-100 shadow-sm">
<%= f.label :name, 'Account name', class: 'block text-sm font-medium opacity-50 focus-within:opacity-100' %>
<%= f.text_field :name, placeholder: 'Example account name', required: 'required', class: "p-0 mt-1 bg-transparent border-none opacity-50 focus:outline-none focus:ring-0 focus-within:opacity-100" %>
</div>
<%= render "accounts/#{permitted_accountable_partial(@account.accountable_type)}", f: f %>
<div class="relative p-3 border border-[#141414]/8 bg-white rounded-xl focus-within:bg-white focus-within:shadow-none focus-within:border-gray-900 focus-within:ring-4 focus-within:ring-gray-100 focus-within:opacity-100 shadow-sm">
<%= f.label :balance, class: 'block text-sm font-medium opacity-50 focus-within:opacity-100' %>
<div class="flex">
<%= f.number_field :balance, placeholder: number_to_currency(0), in: 0.00..100000000.00, required: 'required', class: "p-0 mt-1 bg-transparent border-none opacity-50 focus:outline-none focus:ring-0 focus-within:opacity-100 w-full" %>
</div>
</div>
<div class="">
<button type="submit" class="w-full p-3 text-center text-white bg-black rounded-lg hover:bg-gray-700" title="Submit">
Add account
</button>
</div>
<% end %>
<% end %>
<% end %>

View file

@ -59,10 +59,11 @@
<%= link_to accounts_path, class: 'text-xs' do%>
<%= t('.accounts') %>
<% end %>
<%= link_to new_account_path, class: 'block hover:bg-gray-100 p-2 text-sm font-semibold text-gray-900 flex items-center rounded', title: t('.new_account') do %>
<%= link_to new_account_path, class: 'block hover:bg-gray-100 p-2 text-sm font-semibold text-gray-900 flex items-center rounded', title: t('.new_account'), data: { turbo_frame: "modal" } do %>
<%= inline_svg_tag('icon-add.svg', class: 'text-gray-500 fill-current') %>
<% end %>
</div>
<div>
<h2 class="text-sm font-semibold font-display"><%= t('.cash') %></h2>
@ -83,5 +84,6 @@
<%= yield %>
</main>
</div>
<%= turbo_frame_tag "modal" %>
</body>
</html>

View file

@ -0,0 +1,7 @@
<%= turbo_frame_tag "modal" do %>
<dialog class="bg-white border border-[#141414]/25 rounded-2xl max-h-[556px] max-w-[580px] w-full shadow-xs" data-controller="modal" data-action="click->modal#click_outside">
<div class="flex flex-col h-full">
<%= yield %>
</div>
</dialog>
<% end %>

View file

@ -5,3 +5,4 @@ pin "@hotwired/turbo-rails", to: "turbo.min.js", preload: true
pin "@hotwired/stimulus", to: "stimulus.min.js"
pin "@hotwired/stimulus-loading", to: "stimulus-loading.js"
pin_all_from "app/javascript/controllers", under: "controllers"
pin "@github/hotkey", to: "@github--hotkey.js" # @3.1.0

View file

@ -13,7 +13,7 @@ en:
account: Account
account/credit: Credit Card
account/depository: Bank Accounts
account/investiment: Investments
account/investment: Investments
account/loan: Loan
account/other_asset: Other Asset
account/other_liability: Other Liability

View file

@ -6,6 +6,4 @@ en:
index:
title: Cash
new:
account_name_placeholder: Account name
enter_type_account: Enter %{type} account
title: Add an account

View file

@ -22,6 +22,9 @@ module.exports = {
},
dropShadow: {
'form': '0px 4px 10px rgba(52, 54, 60, 0.08)',
},
boxShadow: {
'xs': '0px 1px 2px 0px #1018280D'
}
},
},

2
vendor/javascript/@github--hotkey.js vendored Normal file

File diff suppressed because one or more lines are too long