1
0
Fork 0
mirror of https://github.com/maybe-finance/maybe.git synced 2025-07-18 20:59:39 +02:00

Feature: Implement Mobile Responsiveness (#2092)

* WIP

* WIP

* WIP

* WIP

* WIP

* WIP

* WIP

* format

* WIP

* WIP

* WIP

* WIP

* WIP

* WIP

* WIP

* WIP

* WIP

* WIP

* WIP

* WIP

* WIP

* WIP

* WIP

* WIP

* WIP

* WIP

* WIP

* WIP

* WIP

* WIP

* WIP

* WIP

* WIP

* WIP

* WIP

* WIP

* WIP

* WIP

* WIP

* WIP

* WIP

* fix conflict

* fix conflict

* chore: run rubocop

* fix test

* update PWA logo

* fix tests

* chore: lint

* fix test

* Refactor: Remove duplicate data attribute in activity partial and add chat form rendering in chats index

---------

Co-authored-by: Josh Pigford <josh@joshpigford.com>
This commit is contained in:
neo773 2025-04-18 18:53:10 +05:30 committed by GitHub
parent 6a21f26d2d
commit 65e1bc6edd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
91 changed files with 1333 additions and 527 deletions

View file

@ -79,6 +79,7 @@ group :development do
gem "web-console" gem "web-console"
gem "faker" gem "faker"
gem "benchmark-ips" gem "benchmark-ips"
gem "foreman"
end end
group :test do group :test do

View file

@ -179,6 +179,7 @@ GEM
ffi (1.17.1-x86_64-darwin) ffi (1.17.1-x86_64-darwin)
ffi (1.17.1-x86_64-linux-gnu) ffi (1.17.1-x86_64-linux-gnu)
ffi (1.17.1-x86_64-linux-musl) ffi (1.17.1-x86_64-linux-musl)
foreman (0.88.1)
globalid (1.2.1) globalid (1.2.1)
activesupport (>= 6.1) activesupport (>= 6.1)
hashdiff (1.1.2) hashdiff (1.1.2)
@ -550,6 +551,7 @@ DEPENDENCIES
faraday faraday
faraday-multipart faraday-multipart
faraday-retry faraday-retry
foreman
hotwire-livereload hotwire-livereload
hotwire_combobox hotwire_combobox
i18n-tasks i18n-tasks

View file

@ -0,0 +1,37 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="0.75" y="0.75" width="18.5" height="18.5" rx="5.25"
class="gradient-fill"
fill="url(#paint0_linear_2046_1939)" />
<rect x="0.75" y="0.75" width="18.5" height="18.5" rx="5.25" stroke="currentColor" stroke-width="1.5" />
<path
d="M13.166 5.78146C13.4233 5.77662 13.6358 5.98129 13.6407 6.2386C13.6575 7.13281 13.688 8.02308 13.7308 8.91583C13.7431 9.1729 13.5447 9.39128 13.2876 9.40361C13.0306 9.41593 12.8122 9.21753 12.7999 8.96046C12.7567 8.05922 12.7259 7.1599 12.7089 6.25615C12.704 5.99883 12.9087 5.78631 13.166 5.78146Z"
fill="currentColor" />
<path
d="M9.35116 6.19917C9.49883 5.98839 9.44768 5.69781 9.2369 5.55013C9.02612 5.40246 8.73554 5.45361 8.58786 5.66439C8.20561 6.20997 7.79785 6.74728 7.3802 7.29762C7.22057 7.50796 7.05946 7.72025 6.89782 7.93558C6.89774 7.8919 6.89758 7.84771 6.89727 7.80294C6.89466 7.42332 6.88115 7.01776 6.8079 6.61074C6.76232 6.35744 6.52004 6.18906 6.26674 6.23464C6.01345 6.28022 5.84506 6.5225 5.89064 6.7758C5.94925 7.10149 5.96277 7.44189 5.9653 7.80935C5.96592 7.90008 5.96583 7.9938 5.96575 8.08947C5.96549 8.36824 5.96523 8.66365 5.98243 8.95029C5.98629 9.01454 6.00299 9.07497 6.02989 9.12923C5.58396 9.77068 5.16482 10.433 4.81446 11.1202C4.8097 11.1296 4.80269 11.1428 4.79392 11.1593C4.74038 11.2602 4.62146 11.4842 4.55147 11.7C4.51162 11.8229 4.46721 12.0002 4.48814 12.1832C4.49932 12.281 4.53101 12.3969 4.60706 12.506C4.68604 12.6193 4.7952 12.6996 4.91859 12.7462C5.67618 13.0326 6.51425 13.0618 7.30714 13.0015C7.95092 12.9525 8.60078 12.8405 9.17979 12.7407C9.31222 12.7179 9.44094 12.6957 9.56504 12.6751C9.81891 12.6329 9.99049 12.3928 9.94826 12.1389C9.90603 11.8851 9.66599 11.7135 9.41211 11.7557C9.27728 11.7782 9.14113 11.8016 9.00391 11.8252C8.42721 11.9244 7.83168 12.0269 7.2364 12.0722C6.58727 12.1216 5.98075 12.0984 5.45339 11.9432C5.49547 11.8288 5.55538 11.7146 5.60631 11.6175C5.61995 11.5915 5.63296 11.5667 5.64479 11.5435C6.11404 10.623 6.72498 9.73396 7.38096 8.84686C7.61644 8.52843 7.86038 8.20687 8.10509 7.8843C8.53175 7.3219 8.96074 6.75642 9.35116 6.19917Z"
fill="currentColor" />
<path
d="M13.1953 13.7096C13.2749 13.4649 13.141 13.2019 12.8962 13.1224C12.6515 13.0428 12.3886 13.1767 12.309 13.4215C12.1983 13.7621 12.0525 14.0208 11.8709 14.199C11.6971 14.3694 11.4701 14.4868 11.1471 14.517C10.6661 14.5621 10.1781 14.3594 9.96528 14.0074C9.83214 13.7871 9.54566 13.7165 9.32541 13.8496C9.10516 13.9828 9.03455 14.2693 9.16769 14.4895C9.61336 15.2268 10.4996 15.5138 11.2341 15.445C11.7632 15.3954 12.192 15.1895 12.5235 14.8644C12.8471 14.547 13.0561 14.1379 13.1953 13.7096Z"
fill="currentColor" />
<path
d="M13.166 5.78146C13.4233 5.77662 13.6358 5.98129 13.6407 6.2386C13.6575 7.13281 13.688 8.02308 13.7308 8.91583C13.7431 9.1729 13.5447 9.39128 13.2876 9.40361C13.0306 9.41593 12.8122 9.21753 12.7999 8.96046C12.7567 8.05922 12.7259 7.1599 12.7089 6.25615C12.704 5.99883 12.9087 5.78631 13.166 5.78146Z"
stroke="currentColor" stroke-width="0.3" stroke-linecap="round" />
<path
d="M9.35116 6.19917C9.49883 5.98839 9.44768 5.69781 9.2369 5.55013C9.02612 5.40246 8.73554 5.45361 8.58786 5.66439C8.20561 6.20997 7.79785 6.74728 7.3802 7.29762C7.22057 7.50796 7.05946 7.72025 6.89782 7.93558C6.89774 7.8919 6.89758 7.84771 6.89727 7.80294C6.89466 7.42332 6.88115 7.01776 6.8079 6.61074C6.76232 6.35744 6.52004 6.18906 6.26674 6.23464C6.01345 6.28022 5.84506 6.5225 5.89064 6.7758C5.94925 7.10149 5.96277 7.44189 5.9653 7.80935C5.96592 7.90008 5.96583 7.9938 5.96575 8.08947C5.96549 8.36824 5.96523 8.66365 5.98243 8.95029C5.98629 9.01454 6.00299 9.07497 6.02989 9.12923C5.58396 9.77068 5.16482 10.433 4.81446 11.1202C4.8097 11.1296 4.80269 11.1428 4.79392 11.1593C4.74038 11.2602 4.62146 11.4842 4.55147 11.7C4.51162 11.8229 4.46721 12.0002 4.48814 12.1832C4.49932 12.281 4.53101 12.3969 4.60706 12.506C4.68604 12.6193 4.7952 12.6996 4.91859 12.7462C5.67618 13.0326 6.51425 13.0618 7.30714 13.0015C7.95092 12.9525 8.60078 12.8405 9.17979 12.7407C9.31222 12.7179 9.44094 12.6957 9.56504 12.6751C9.81891 12.6329 9.99049 12.3928 9.94826 12.1389C9.90603 11.8851 9.66599 11.7135 9.41211 11.7557C9.27728 11.7782 9.14113 11.8016 9.00391 11.8252C8.42721 11.9244 7.83168 12.0269 7.2364 12.0722C6.58727 12.1216 5.98075 12.0984 5.45339 11.9432C5.49547 11.8288 5.55538 11.7146 5.60631 11.6175C5.61995 11.5915 5.63296 11.5667 5.64479 11.5435C6.11404 10.623 6.72498 9.73396 7.38096 8.84686C7.61644 8.52843 7.86038 8.20687 8.10509 7.8843C8.53175 7.3219 8.96074 6.75642 9.35116 6.19917Z"
stroke="currentColor" stroke-width="0.3" stroke-linecap="round" />
<path
d="M13.1953 13.7096C13.2749 13.4649 13.141 13.2019 12.8962 13.1224C12.6515 13.0428 12.3886 13.1767 12.309 13.4215C12.1983 13.7621 12.0525 14.0208 11.8709 14.199C11.6971 14.3694 11.4701 14.4868 11.1471 14.517C10.6661 14.5621 10.1781 14.3594 9.96528 14.0074C9.83214 13.7871 9.54566 13.7165 9.32541 13.8496C9.10516 13.9828 9.03455 14.2693 9.16769 14.4895C9.61336 15.2268 10.4996 15.5138 11.2341 15.445C11.7632 15.3954 12.192 15.1895 12.5235 14.8644C12.8471 14.547 13.0561 14.1379 13.1953 13.7096Z"
stroke="currentColor" stroke-width="0.3" stroke-linecap="round" />
<style>
[data-theme=dark] .gradient-fill {
fill: transparent;
}
</style>
<defs>
<linearGradient id="paint0_linear_2046_1939" x1="10" y1="6.25" x2="10" y2="20"
gradientUnits="userSpaceOnUse">
<stop stop-color="white" />
<stop offset="0.3" stop-color="#F7F7F7" />
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 5.6 KiB

View file

@ -0,0 +1,5 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M2.5 7.5H17.5M2.5 12.5H17.5M7.5 7.5V17.5M12.5 7.5V17.5M4.16667 2.5H15.8333C16.7538 2.5 17.5 3.24619 17.5 4.16667V15.8333C17.5 16.7538 16.7538 17.5 15.8333 17.5H4.16667C3.24619 17.5 2.5 16.7538 2.5 15.8333V4.16667C2.5 3.24619 3.24619 2.5 4.16667 2.5Z"
stroke="#737373" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
</svg>

After

Width:  |  Height:  |  Size: 468 B

View file

@ -167,3 +167,15 @@
} }
} }
/* The following Markdown CSS has been removed as requested */ /* The following Markdown CSS has been removed as requested */
.mt-safe {
margin-top: env(safe-area-inset-top);
}
.pt-safe {
padding-top: env(safe-area-inset-top);
}
.pb-safe {
padding-bottom: env(safe-area-inset-bottom);
}

View file

@ -761,3 +761,11 @@
@apply bg-alpha-black-700; @apply bg-alpha-black-700;
} }
} }
@utility bg-nav-indicator {
@apply bg-black;
@variant theme-dark {
@apply bg-white;
}
}

View file

@ -6,6 +6,13 @@ class Import::UploadsController < ApplicationController
def show def show
end end
def sample_csv
send_data @import.csv_template.to_csv,
filename: "#{@import.type.underscore.split('_').first}_sample.csv",
type: "text/csv",
disposition: "attachment"
end
def update def update
if csv_valid?(csv_str) if csv_valid?(csv_str)
@import.account = Current.family.accounts.find_by(id: params.dig(:import, :account_id)) @import.account = Current.family.accounts.find_by(id: params.dig(:import, :account_id))

View file

@ -6,6 +6,7 @@ class RegistrationsController < ApplicationController
before_action :set_user, only: :create before_action :set_user, only: :create
before_action :set_invitation before_action :set_invitation
before_action :claim_invite_code, only: :create, if: :invite_code_required? before_action :claim_invite_code, only: :create, if: :invite_code_required?
before_action :validate_password_requirements, only: :create
def new def new
@user = User.new(email: @invitation&.email) @user = User.new(email: @invitation&.email)
@ -53,4 +54,29 @@ class RegistrationsController < ApplicationController
redirect_to new_registration_path, alert: t("registrations.create.invalid_invite_code") redirect_to new_registration_path, alert: t("registrations.create.invalid_invite_code")
end end
end end
def validate_password_requirements
password = user_params[:password]
return if password.blank? # Let Rails built-in validations handle blank passwords
if password.length < 8
@user.errors.add(:password, "must be at least 8 characters")
end
unless password.match?(/[A-Z]/) && password.match?(/[a-z]/)
@user.errors.add(:password, "must include both uppercase and lowercase letters")
end
unless password.match?(/\d/)
@user.errors.add(:password, "must include at least one number")
end
unless password.match?(/[!@#$%^&*(),.?":{}|<>]/)
@user.errors.add(:password, "must include at least one special character")
end
if @user.errors.present?
render :new, status: :unprocessable_entity
end
end
end end

View file

@ -5,6 +5,10 @@ module ApplicationHelper
render partial: "shared/icon", locals: { key:, size:, color: } render partial: "shared/icon", locals: { key:, size:, color: }
end end
def icon_custom(key, size: "md", color: "current")
render partial: "shared/icon_custom", locals: { key:, size:, color: }
end
# Convert alpha (0-1) to 8-digit hex (00-FF) # Convert alpha (0-1) to 8-digit hex (00-FF)
def hex_with_alpha(hex, alpha) def hex_with_alpha(hex, alpha)
alpha_hex = (alpha * 255).round.to_s(16).rjust(2, "0") alpha_hex = (alpha * 255).round.to_s(16).rjust(2, "0")

View file

@ -10,9 +10,9 @@ module FormsHelper
render partial: "shared/modal_form", locals: { title:, subtitle:, content: } render partial: "shared/modal_form", locals: { title:, subtitle:, content: }
end end
def radio_tab_tag(form:, name:, value:, label:, icon:, checked: false, disabled: false) 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 form.label name, for: form.field_id(name, value), class: "group has-disabled:cursor-not-allowed" do
concat radio_tab_contents(label:, icon:) concat radio_tab_contents(label:, icon:, class:)
concat form.radio_button(name, value, checked:, disabled:, class: "hidden") concat form.radio_button(name, value, checked:, disabled:, class: "hidden")
end end
end end
@ -29,8 +29,8 @@ end
end end
private private
def radio_tab_contents(label:, icon:) 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-subdued group-has-checked:bg-container group-has-checked:text-gray-800 group-has-checked:shadow-sm") do 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-container group-has-checked:text-gray-800 group-has-checked:shadow-sm") do
concat lucide_icon(icon, class: "w-5 h-5") concat lucide_icon(icon, class: "w-5 h-5")
concat tag.span(label, class: "group-has-checked:font-semibold") concat tag.span(label, class: "group-has-checked:font-semibold")
end end

View file

@ -55,6 +55,11 @@ module ImportsHelper
[ base, border ].join(" ") [ base, border ].join(" ")
end end
def cell_is_valid?(row, field)
row.valid? # populate errors
!row.errors.key?(field)
end
private private
def permitted_import_types def permitted_import_types
%w[transaction_import trade_import account_import mint_import] %w[transaction_import trade_import account_import mint_import]

View file

@ -154,200 +154,200 @@ module LanguagesHelper
].freeze ].freeze
COUNTRY_MAPPING = { COUNTRY_MAPPING = {
AF: "Afghanistan", AF: "🇦🇫 Afghanistan",
AL: "Albania", AL: "🇦🇱 Albania",
DZ: "Algeria", DZ: "🇩🇿 Algeria",
AD: "Andorra", AD: "🇦🇩 Andorra",
AO: "Angola", AO: "🇦🇴 Angola",
AG: "Antigua and Barbuda", AG: "🇦🇬 Antigua and Barbuda",
AR: "Argentina", AR: "🇦🇷 Argentina",
AM: "Armenia", AM: "🇦🇲 Armenia",
AU: "Australia", AU: "🇦🇺 Australia",
AT: "Austria", AT: "🇦🇹 Austria",
AZ: "Azerbaijan", AZ: "🇦🇿 Azerbaijan",
BS: "Bahamas", BS: "🇧🇸 Bahamas",
BH: "Bahrain", BH: "🇧🇭 Bahrain",
BD: "Bangladesh", BD: "🇧🇩 Bangladesh",
BB: "Barbados", BB: "🇧🇧 Barbados",
BY: "Belarus", BY: "🇧🇾 Belarus",
BE: "Belgium", BE: "🇧🇪 Belgium",
BZ: "Belize", BZ: "🇧🇿 Belize",
BJ: "Benin", BJ: "🇧🇯 Benin",
BT: "Bhutan", BT: "🇧🇹 Bhutan",
BO: "Bolivia", BO: "🇧🇴 Bolivia",
BA: "Bosnia and Herzegovina", BA: "🇧🇦 Bosnia and Herzegovina",
BW: "Botswana", BW: "🇧🇼 Botswana",
BR: "Brazil", BR: "🇧🇷 Brazil",
BN: "Brunei", BN: "🇧🇳 Brunei",
BG: "Bulgaria", BG: "🇧🇬 Bulgaria",
BF: "Burkina Faso", BF: "🇧🇫 Burkina Faso",
BI: "Burundi", BI: "🇧🇮 Burundi",
KH: "Cambodia", KH: "🇰🇭 Cambodia",
CM: "Cameroon", CM: "🇨🇲 Cameroon",
CA: "Canada", CA: "🇨🇦 Canada",
CV: "Cape Verde", CV: "🇨🇻 Cape Verde",
CF: "Central African Republic", CF: "🇨🇫 Central African Republic",
TD: "Chad", TD: "🇹🇩 Chad",
CL: "Chile", CL: "🇨🇱 Chile",
CN: "China", CN: "🇨🇳 China",
CO: "Colombia", CO: "🇨🇴 Colombia",
KM: "Comoros", KM: "🇰🇲 Comoros",
CG: "Congo", CG: "🇨🇬 Congo",
CD: "Congo, Democratic Republic of the", CD: "🇨🇩 Congo, Democratic Republic of the",
CR: "Costa Rica", CR: "🇨🇷 Costa Rica",
CI: "Côte d'Ivoire", CI: "🇨🇮 Côte d'Ivoire",
HR: "Croatia", HR: "🇭🇷 Croatia",
CU: "Cuba", CU: "🇨🇺 Cuba",
CY: "Cyprus", CY: "🇨🇾 Cyprus",
CZ: "Czech Republic", CZ: "🇨🇿 Czech Republic",
DK: "Denmark", DK: "🇩🇰 Denmark",
DJ: "Djibouti", DJ: "🇩🇯 Djibouti",
DM: "Dominica", DM: "🇩🇲 Dominica",
DO: "Dominican Republic", DO: "🇩🇴 Dominican Republic",
EC: "Ecuador", EC: "🇪🇨 Ecuador",
EG: "Egypt", EG: "🇪🇬 Egypt",
SV: "El Salvador", SV: "🇸🇻 El Salvador",
GQ: "Equatorial Guinea", GQ: "🇬🇶 Equatorial Guinea",
ER: "Eritrea", ER: "🇪🇷 Eritrea",
EE: "Estonia", EE: "🇪🇪 Estonia",
ET: "Ethiopia", ET: "🇪🇹 Ethiopia",
FJ: "Fiji", FJ: "🇫🇯 Fiji",
FI: "Finland", FI: "🇫🇮 Finland",
FR: "France", FR: "🇫🇷 France",
GA: "Gabon", GA: "🇬🇦 Gabon",
GM: "Gambia", GM: "🇬🇲 Gambia",
GE: "Georgia", GE: "🇬🇪 Georgia",
DE: "Germany", DE: "🇩🇪 Germany",
GH: "Ghana", GH: "🇬🇭 Ghana",
GR: "Greece", GR: "🇬🇷 Greece",
GD: "Grenada", GD: "🇬🇩 Grenada",
GT: "Guatemala", GT: "🇬🇹 Guatemala",
GN: "Guinea", GN: "🇬🇳 Guinea",
GW: "Guinea-Bissau", GW: "🇬🇼 Guinea-Bissau",
GY: "Guyana", GY: "🇬🇾 Guyana",
HT: "Haiti", HT: "🇭🇹 Haiti",
HN: "Honduras", HN: "🇭🇳 Honduras",
HU: "Hungary", HU: "🇭🇺 Hungary",
IS: "Iceland", IS: "🇮🇸 Iceland",
IN: "India", IN: "🇮🇳 India",
ID: "Indonesia", ID: "🇮🇩 Indonesia",
IR: "Iran", IR: "🇮🇷 Iran",
IQ: "Iraq", IQ: "🇮🇶 Iraq",
IE: "Ireland", IE: "🇮🇪 Ireland",
IL: "Israel", IL: "🇮🇱 Israel",
IT: "Italy", IT: "🇮🇹 Italy",
JM: "Jamaica", JM: "🇯🇲 Jamaica",
JP: "Japan", JP: "🇯🇵 Japan",
JO: "Jordan", JO: "🇯🇴 Jordan",
KZ: "Kazakhstan", KZ: "🇰🇿 Kazakhstan",
KE: "Kenya", KE: "🇰🇪 Kenya",
KI: "Kiribati", KI: "🇰🇮 Kiribati",
KP: "North Korea", KP: "🇰🇵 North Korea",
KR: "South Korea", KR: "🇰🇷 South Korea",
KW: "Kuwait", KW: "🇰🇼 Kuwait",
KG: "Kyrgyzstan", KG: "🇰🇬 Kyrgyzstan",
LA: "Laos", LA: "🇱🇦 Laos",
LV: "Latvia", LV: "🇱🇻 Latvia",
LB: "Lebanon", LB: "🇱🇧 Lebanon",
LS: "Lesotho", LS: "🇱🇸 Lesotho",
LR: "Liberia", LR: "🇱🇷 Liberia",
LY: "Libya", LY: "🇱🇾 Libya",
LI: "Liechtenstein", LI: "🇱🇮 Liechtenstein",
LT: "Lithuania", LT: "🇱🇹 Lithuania",
LU: "Luxembourg", LU: "🇱🇺 Luxembourg",
MK: "North Macedonia", MK: "🇲🇰 North Macedonia",
MG: "Madagascar", MG: "🇲🇬 Madagascar",
MW: "Malawi", MW: "🇲🇼 Malawi",
MY: "Malaysia", MY: "🇲🇾 Malaysia",
MV: "Maldives", MV: "🇲🇻 Maldives",
ML: "Mali", ML: "🇲🇱 Mali",
MT: "Malta", MT: "🇲🇹 Malta",
MH: "Marshall Islands", MH: "🇲🇭 Marshall Islands",
MR: "Mauritania", MR: "🇲🇷 Mauritania",
MU: "Mauritius", MU: "🇲🇺 Mauritius",
MX: "Mexico", MX: "🇲🇽 Mexico",
FM: "Micronesia", FM: "🇫🇲 Micronesia",
MD: "Moldova", MD: "🇲🇩 Moldova",
MC: "Monaco", MC: "🇲🇨 Monaco",
MN: "Mongolia", MN: "🇲🇳 Mongolia",
ME: "Montenegro", ME: "🇲🇪 Montenegro",
MA: "Morocco", MA: "🇲🇦 Morocco",
MZ: "Mozambique", MZ: "🇲🇿 Mozambique",
MM: "Myanmar", MM: "🇲🇲 Myanmar",
NA: "Namibia", NA: "🇳🇦 Namibia",
NR: "Nauru", NR: "🇳🇷 Nauru",
NP: "Nepal", NP: "🇳🇵 Nepal",
NL: "Netherlands", NL: "🇳🇱 Netherlands",
NZ: "New Zealand", NZ: "🇳🇿 New Zealand",
NI: "Nicaragua", NI: "🇳🇮 Nicaragua",
NE: "Niger", NE: "🇳🇪 Niger",
NG: "Nigeria", NG: "🇳🇬 Nigeria",
NO: "Norway", NO: "🇳🇴 Norway",
OM: "Oman", OM: "🇴🇲 Oman",
PK: "Pakistan", PK: "🇵🇰 Pakistan",
PW: "Palau", PW: "🇵🇼 Palau",
PA: "Panama", PA: "🇵🇦 Panama",
PG: "Papua New Guinea", PG: "🇵🇬 Papua New Guinea",
PY: "Paraguay", PY: "🇵🇾 Paraguay",
PE: "Peru", PE: "🇵🇪 Peru",
PH: "Philippines", PH: "🇵🇭 Philippines",
PL: "Poland", PL: "🇵🇱 Poland",
PT: "Portugal", PT: "🇵🇹 Portugal",
QA: "Qatar", QA: "🇶🇦 Qatar",
RO: "Romania", RO: "🇷🇴 Romania",
RU: "Russia", RU: "🇷🇺 Russia",
RW: "Rwanda", RW: "🇷🇼 Rwanda",
KN: "Saint Kitts and Nevis", KN: "🇰🇳 Saint Kitts and Nevis",
LC: "Saint Lucia", LC: "🇱🇨 Saint Lucia",
VC: "Saint Vincent and the Grenadines", VC: "🇻🇨 Saint Vincent and the Grenadines",
WS: "Samoa", WS: "🇼🇸 Samoa",
SM: "San Marino", SM: "🇸🇲 San Marino",
ST: "Sao Tome and Principe", ST: "🇸🇹 Sao Tome and Principe",
SA: "Saudi Arabia", SA: "🇸🇦 Saudi Arabia",
SN: "Senegal", SN: "🇸🇳 Senegal",
RS: "Serbia", RS: "🇷🇸 Serbia",
SC: "Seychelles", SC: "🇸🇨 Seychelles",
SL: "Sierra Leone", SL: "🇸🇱 Sierra Leone",
SG: "Singapore", SG: "🇸🇬 Singapore",
SK: "Slovakia", SK: "🇸🇰 Slovakia",
SI: "Slovenia", SI: "🇸🇮 Slovenia",
SB: "Solomon Islands", SB: "🇸🇧 Solomon Islands",
SO: "Somalia", SO: "🇸🇴 Somalia",
ZA: "South Africa", ZA: "🇿🇦 South Africa",
SS: "South Sudan", SS: "🇸🇸 South Sudan",
ES: "Spain", ES: "🇪🇸 Spain",
LK: "Sri Lanka", LK: "🇱🇰 Sri Lanka",
SD: "Sudan", SD: "🇸🇩 Sudan",
SR: "Suriname", SR: "🇸🇷 Suriname",
SE: "Sweden", SE: "🇸🇪 Sweden",
CH: "Switzerland", CH: "🇨🇭 Switzerland",
SY: "Syria", SY: "🇸🇾 Syria",
TW: "Taiwan", TW: "🇹🇼 Taiwan",
TJ: "Tajikistan", TJ: "🇹🇯 Tajikistan",
TZ: "Tanzania", TZ: "🇹🇿 Tanzania",
TH: "Thailand", TH: "🇹🇭 Thailand",
TL: "Timor-Leste", TL: "🇹🇱 Timor-Leste",
TG: "Togo", TG: "🇹🇬 Togo",
TO: "Tonga", TO: "🇹🇴 Tonga",
TT: "Trinidad and Tobago", TT: "🇹🇹 Trinidad and Tobago",
TN: "Tunisia", TN: "🇹🇳 Tunisia",
TR: "Turkey", TR: "🇹🇷 Turkey",
TM: "Turkmenistan", TM: "🇹🇲 Turkmenistan",
TV: "Tuvalu", TV: "🇹🇻 Tuvalu",
UG: "Uganda", UG: "🇺🇬 Uganda",
UA: "Ukraine", UA: "🇺🇦 Ukraine",
AE: "United Arab Emirates", AE: "🇦🇪 United Arab Emirates",
GB: "United Kingdom", GB: "🇬🇧 United Kingdom",
US: "United States", US: "🇺🇸 United States",
UY: "Uruguay", UY: "🇺🇾 Uruguay",
UZ: "Uzbekistan", UZ: "🇺🇿 Uzbekistan",
VU: "Vanuatu", VU: "🇻🇺 Vanuatu",
VA: "Vatican City", VA: "🇻🇦 Vatican City",
VE: "Venezuela", VE: "🇻🇪 Venezuela",
VN: "Vietnam", VN: "🇻🇳 Vietnam",
YE: "Yemen", YE: "🇾🇪 Yemen",
ZM: "Zambia", ZM: "🇿🇲 Zambia",
ZW: "Zimbabwe" ZW: "🇿🇼 Zimbabwe"
}.freeze }.freeze
def country_options def country_options

View file

@ -6,8 +6,8 @@ module MenusHelper
end end
end end
def contextual_menu_modal_action_item(label, url, icon: "pencil-line", turbo_frame: :modal) def contextual_menu_modal_action_item(label, url, icon: "pencil-line", turbo_frame: :modal, class_name: nil)
link_to url, class: "flex items-center rounded-md text-primary hover:bg-container-hover p-2 gap-2", data: { action: "click->menu#close", turbo_frame: turbo_frame } do link_to url, class: "flex items-center rounded-md text-primary hover:bg-container-hover p-2 gap-2 #{class_name}", data: { action: "click->menu#close", turbo_frame: turbo_frame } do
concat(lucide_icon(icon, class: "shrink-0 w-5 h-5 text-secondary")) concat(lucide_icon(icon, class: "shrink-0 w-5 h-5 text-secondary"))
concat(tag.span(label, class: "text-sm")) concat(tag.span(label, class: "text-sm"))
end end
@ -33,7 +33,8 @@ module MenusHelper
private private
def contextual_menu_icon(icon) def contextual_menu_icon(icon)
tag.button class: "w-9 h-9 flex justify-center items-center hover:bg-surface-hover rounded-lg cursor-pointer focus:outline-none focus-visible:outline-none", data: { menu_target: "button" } do tag.button class: "w-9 h-9 flex justify-center items-center hover:bg-surface-hover rounded-lg cursor-pointer focus:outline-none focus-visible:outline-none", data: { menu_target: "button" } do
lucide_icon icon, class: "w-5 h-5 text-secondary" concat lucide_icon("more-vertical", class: "w-5 h-5 text-secondary md:hidden")
concat lucide_icon(icon, class: "w-5 h-5 text-secondary hidden md:block")
end end
end end

View file

@ -40,7 +40,17 @@ module SettingsHelper
previous_setting = adjacent_setting(request.path, -1) previous_setting = adjacent_setting(request.path, -1)
next_setting = adjacent_setting(request.path, 1) next_setting = adjacent_setting(request.path, 1)
content_tag :div, class: "flex justify-between gap-4" do content_tag :div, class: "hidden md:flex flex-row justify-between gap-4" do
concat(previous_setting)
concat(next_setting)
end
end
def settings_nav_footer_mobile
previous_setting = adjacent_setting(request.path, -1)
next_setting = adjacent_setting(request.path, 1)
content_tag :div, class: "md:hidden flex flex-col gap-4" do
concat(previous_setting) concat(previous_setting)
concat(next_setting) concat(next_setting)
end end

View file

@ -0,0 +1,74 @@
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
static targets = ["input", "fileName", "uploadArea", "uploadText"]
connect() {
if (this.hasInputTarget) {
this.inputTarget.addEventListener("change", this.fileSelected.bind(this))
}
// Find the form element
this.form = this.element.closest("form")
if (this.form) {
this.form.addEventListener("turbo:submit-start", this.formSubmitting.bind(this))
}
}
disconnect() {
if (this.hasInputTarget) {
this.inputTarget.removeEventListener("change", this.fileSelected.bind(this))
}
if (this.form) {
this.form.removeEventListener("turbo:submit-start", this.formSubmitting.bind(this))
}
}
triggerFileInput() {
if (this.hasInputTarget) {
this.inputTarget.click()
}
}
fileSelected() {
if (this.hasInputTarget && this.inputTarget.files.length > 0) {
const fileName = this.inputTarget.files[0].name
if (this.hasFileNameTarget) {
// Find the paragraph element inside the fileName target
const fileNameText = this.fileNameTarget.querySelector('p')
if (fileNameText) {
fileNameText.textContent = fileName
}
this.fileNameTarget.classList.remove("hidden")
}
if (this.hasUploadTextTarget) {
this.uploadTextTarget.classList.add("hidden")
}
}
}
formSubmitting() {
if (this.hasFileNameTarget && this.hasInputTarget && this.inputTarget.files.length > 0) {
const fileNameText = this.fileNameTarget.querySelector('p')
if (fileNameText) {
fileNameText.textContent = `Uploading ${this.inputTarget.files[0].name}...`
}
// Change the icon to a loader
const iconContainer = this.fileNameTarget.querySelector('.lucide-file-text')
if (iconContainer) {
iconContainer.classList.add('animate-pulse')
}
}
if (this.hasUploadAreaTarget) {
this.uploadAreaTarget.classList.add("opacity-70")
}
}
}

View file

@ -0,0 +1,149 @@
import { Controller } from "@hotwired/stimulus";
// Connects to data-controller="mobile-cell-interaction"
export default class extends Controller {
static targets = ["field", "highlight", "errorTooltip", "errorIcon"];
static values = { error: String };
touchTimeout = null;
activeTooltip = null;
documentClickHandler = null;
connect() {
this.documentClickHandler = this.handleDocumentClick.bind(this);
document.addEventListener('click', this.documentClickHandler);
}
disconnect() {
if (this.documentClickHandler) {
document.removeEventListener('click', this.documentClickHandler);
}
}
handleDocumentClick(event) {
if (event.target.closest('[data-mobile-cell-interaction-target="errorTooltip"]') ||
event.target.closest('[data-mobile-cell-interaction-target="errorIcon"]')) {
return;
}
this.hideAllErrorTooltips();
}
highlightCell(event) {
const field = event.target;
const highlight = this.findHighlightForField(field);
if (highlight) {
highlight.style.opacity = '1';
}
}
unhighlightCell(event) {
const field = event.target;
const highlight = this.findHighlightForField(field);
if (highlight) {
highlight.style.opacity = '0';
}
this.hideAllErrorTooltips();
}
handleCellTouch(event) {
if (this.touchTimeout) {
clearTimeout(this.touchTimeout);
}
const field = event.target;
const highlight = this.findHighlightForField(field);
if (highlight) {
highlight.style.opacity = '1';
this.touchTimeout = window.setTimeout(() => {
if (document.activeElement !== field) {
highlight.style.opacity = '0';
}
}, 1000);
}
if (this.hasErrorValue && this.errorValue) {
this.showErrorTooltip();
}
}
toggleErrorMessage(event) {
const errorIcon = event.currentTarget;
const cellContainer = errorIcon.closest('div');
const field = cellContainer.querySelector('input');
if (field) {
field.focus();
}
const tooltip = this.errorTooltipTarget;
this.hideAllTooltipsExcept(tooltip);
if (tooltip.classList.contains('hidden')) {
tooltip.classList.remove('hidden');
this.activeTooltip = tooltip;
setTimeout(() => {
if (tooltip === this.activeTooltip) {
tooltip.classList.add('hidden');
this.activeTooltip = null;
}
}, 3000);
} else {
tooltip.classList.add('hidden');
this.activeTooltip = null;
}
event.stopPropagation();
}
showErrorTooltip() {
if (this.hasErrorTooltipTarget) {
const tooltip = this.errorTooltipTarget;
tooltip.classList.remove('hidden');
this.activeTooltip = tooltip;
setTimeout(() => {
if (tooltip === this.activeTooltip) {
tooltip.classList.add('hidden');
this.activeTooltip = null;
}
}, 3000);
}
}
hideAllErrorTooltips() {
document.querySelectorAll('[data-mobile-cell-interaction-target="errorTooltip"]').forEach(tooltip => {
tooltip.classList.add('hidden');
});
this.activeTooltip = null;
}
hideAllTooltipsExcept(tooltipToKeep) {
document.querySelectorAll('[data-mobile-cell-interaction-target="errorTooltip"]').forEach(tooltip => {
if (tooltip !== tooltipToKeep) {
tooltip.classList.add('hidden');
}
});
}
selectCell(event) {
const errorIcon = event.currentTarget;
const cellContainer = errorIcon.closest('div');
const field = cellContainer.querySelector('input');
if (field) {
field.focus();
event.stopPropagation();
}
}
findHighlightForField(field) {
const container = field.closest('div');
return container ? container.querySelector('[data-mobile-cell-interaction-target="highlight"]') : null;
}
}

View file

@ -0,0 +1,63 @@
import { Controller } from "@hotwired/stimulus";
// Connects to data-controller="password-validator"
export default class extends Controller {
static targets = ["input", "requirementType", "blockLine"];
connect() {
this.validate();
}
validate() {
const password = this.inputTarget.value;
let requirementsMet = 0;
// Check each requirement and count how many are met
const lengthValid = password.length >= 8;
const caseValid = /[A-Z]/.test(password) && /[a-z]/.test(password);
const numberValid = /\d/.test(password);
const specialValid = /[!@#$%^&*(),.?":{}|<>]/.test(password);
// Update individual requirement text
this.validateRequirementText("length", lengthValid);
this.validateRequirementText("case", caseValid);
this.validateRequirementText("number", numberValid);
this.validateRequirementText("special", specialValid);
// Count total requirements met
if (lengthValid) requirementsMet++;
if (caseValid) requirementsMet++;
if (numberValid) requirementsMet++;
if (specialValid) requirementsMet++;
// Update block lines sequentially
this.updateBlockLines(requirementsMet);
}
validateRequirementText(type, isValid) {
this.requirementTypeTargets.forEach((target) => {
if (target.dataset.requirementType === type) {
if (isValid) {
target.classList.remove("text-secondary");
target.classList.add("text-green-600");
} else {
target.classList.remove("text-green-600");
target.classList.add("text-secondary");
}
}
});
}
updateBlockLines(requirementsMet) {
// Update block lines sequentially based on total requirements met
this.blockLineTargets.forEach((line, index) => {
if (index < requirementsMet) {
line.classList.remove("bg-gray-200");
line.classList.add("bg-green-600");
} else {
line.classList.remove("bg-green-600");
line.classList.add("bg-gray-200");
}
});
}
}

View file

@ -0,0 +1,19 @@
import { Controller } from "@hotwired/stimulus";
// Connects to data-controller="password-visibility"
export default class extends Controller {
static targets = ["input", "showIcon", "hideIcon"];
connect() {
this.hideIconTarget.classList.add("hidden");
}
toggle() {
const input = this.inputTarget;
const type = input.type === "password" ? "text" : "password";
input.type = type;
this.showIconTarget.classList.toggle("hidden");
this.hideIconTarget.classList.toggle("hidden");
}
}

View file

@ -0,0 +1,39 @@
/*
https://dev.to/konnorrogers/maintain-scroll-position-in-turbo-without-data-turbo-permanent-2b1i
modified to add support for horizontal scrolling
*/
if (!window.scrollPositions) {
window.scrollPositions = {};
}
function preserveScroll() {
document.querySelectorAll("[data-preserve-scroll]").forEach((element) => {
scrollPositions[element.id] = {
top: element.scrollTop,
left: element.scrollLeft
};
});
}
function restoreScroll(event) {
document.querySelectorAll("[data-preserve-scroll]").forEach((element) => {
if (scrollPositions[element.id]) {
element.scrollTop = scrollPositions[element.id].top;
element.scrollLeft = scrollPositions[element.id].left;
}
});
if (!event.detail.newBody) return;
// event.detail.newBody is the body element to be swapped in.
// https://turbo.hotwired.dev/reference/events
event.detail.newBody.querySelectorAll("[data-preserve-scroll]").forEach((element) => {
if (scrollPositions[element.id]) {
element.scrollTop = scrollPositions[element.id].top;
element.scrollLeft = scrollPositions[element.id].left;
}
});
}
window.addEventListener("turbo:before-cache", preserveScroll);
window.addEventListener("turbo:before-render", restoreScroll);
window.addEventListener("turbo:render", restoreScroll);

View file

@ -8,6 +8,9 @@ export default class extends Controller {
"deleteProfileImage", "deleteProfileImage",
"input", "input",
"clearBtn", "clearBtn",
"uploadText",
"changeText",
"cameraIcon"
]; ];
clearFileInput() { clearFileInput() {
@ -17,6 +20,12 @@ export default class extends Controller {
this.attachedImageTarget.classList.add("hidden"); this.attachedImageTarget.classList.add("hidden");
this.previewImageTarget.classList.add("hidden"); this.previewImageTarget.classList.add("hidden");
this.deleteProfileImageTarget.value = "1"; this.deleteProfileImageTarget.value = "1";
this.uploadTextTarget.classList.remove("hidden");
this.changeTextTarget.classList.add("hidden");
this.changeTextTarget.setAttribute("aria-hidden", "true");
this.uploadTextTarget.setAttribute("aria-hidden", "false");
this.cameraIconTarget.classList.remove("!hidden");
} }
showFileInputPreview(event) { showFileInputPreview(event) {
@ -28,7 +37,11 @@ export default class extends Controller {
this.previewImageTarget.classList.remove("hidden"); this.previewImageTarget.classList.remove("hidden");
this.clearBtnTarget.classList.remove("hidden"); this.clearBtnTarget.classList.remove("hidden");
this.deleteProfileImageTarget.value = "0"; this.deleteProfileImageTarget.value = "0";
this.uploadTextTarget.classList.add("hidden");
this.changeTextTarget.classList.remove("hidden");
this.changeTextTarget.setAttribute("aria-hidden", "false");
this.uploadTextTarget.setAttribute("aria-hidden", "true");
this.cameraIconTarget.classList.add("!hidden");
this.previewImageTarget.querySelector("img").src = this.previewImageTarget.querySelector("img").src =
URL.createObjectURL(file); URL.createObjectURL(file);
} }

View file

@ -50,7 +50,8 @@ export default class extends Controller {
} }
systemPrefersDark() { systemPrefersDark() {
return window.matchMedia("(prefers-color-scheme: dark)").matches return false
// return window.matchMedia("(prefers-color-scheme: dark)").matches
} }
handleSystemThemeChange = (event) => { handleSystemThemeChange = (event) => {

View file

@ -4,17 +4,20 @@
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<%= button_to sync_all_accounts_path, <%= button_to sync_all_accounts_path,
disabled: Current.family.syncing?, disabled: Current.family.syncing?,
class: "btn btn--outline flex items-center gap-2", class: "md:btn md:btn--outline flex items-center justify-center gap-2 w-9 h-9 md:w-auto md:h-auto rounded-full md:rounded-lg",
title: t(".sync") do %> title: t(".sync") do %>
<%= lucide_icon "refresh-cw", class: "w-5 h-5" %> <%= lucide_icon "refresh-cw", class: "w-5 h-5" %>
<span><%= t(".sync") %></span> <span class="hidden md:inline"><%= t(".sync") %></span>
<% end %> <% end %>
<%= link_to new_account_path(return_to: accounts_path), <%= link_to new_account_path(return_to: accounts_path),
data: { turbo_frame: "modal" }, data: { turbo_frame: "modal" },
class: "btn btn--primary flex items-center gap-1" do %> class: "btn btn--primary flex items-center justify-center gap-1 w-9 h-9 md:w-auto md:h-auto rounded-full md:rounded-lg" do %>
<%= lucide_icon("plus", class: "w-5 h-5") %> <div class="flex items-center justify-center w-5 h-5">
<p class="text-sm font-medium"><%= t(".new_account") %></p>
<%= lucide_icon("plus")%>
</div>
<p class="hidden md:block text-sm font-medium"><%= t(".new_account") %></p>
<% end %> <% end %>
</div> </div>
</div> </div>

View file

@ -1,8 +1,8 @@
<%# locals: (title:, back_path: nil) %> <%# locals: (title:, back_path: nil) %>
<%= modal do %> <%= modal do %>
<div class="flex flex-col w-screen max-w-xl" data-controller="list-keyboard-navigation"> <div class="flex flex-col w-screen max-w-xl relative" data-controller="list-keyboard-navigation">
<div class="border-b border-alpha-black-25 p-4 text-gray-800 flex items-center space-x-3"> <div class="border-b border-tertiary md:border-alpha-black-25 p-4 text-gray-800 flex items-center space-x-3">
<% if back_path %> <% if back_path %>
<%= link_to back_path, class: "flex w-8 h-8 shrink-0 grow-0 items-center justify-center rounded-lg bg-alpha-black-50 focus:outline-gray-300 focus:outline" do %> <%= link_to back_path, class: "flex w-8 h-8 shrink-0 grow-0 items-center justify-center rounded-lg bg-alpha-black-50 focus:outline-gray-300 focus:outline" do %>
<%= lucide_icon("arrow-left", class: "text-secondary w-5 h-5") %> <%= lucide_icon("arrow-left", class: "text-secondary w-5 h-5") %>
@ -10,6 +10,9 @@
<% end %> <% end %>
<span class="text-subdued"><%= title %></span> <span class="text-subdued"><%= title %></span>
<button class="absolute top-1/2 -translate-y-1/2 right-4 flex w-8 h-8 items-center justify-center rounded-lg md:hidden outline-0" data-action="click->modal#close">
<%= lucide_icon("x", class: "text-secondary w-6 h-6") %>
</div>
</div> </div>
<div class="p-2"> <div class="p-2">
@ -19,7 +22,7 @@
<%= yield %> <%= yield %>
</div> </div>
<div class="border-t border-alpha-black-25 p-4 text-secondary text-sm flex justify-between"> <div class="border-t border-alpha-black-25 p-4 text-secondary text-sm justify-between hidden md:flex">
<div class="flex space-x-5"> <div class="flex space-x-5">
<div class="flex items-center space-x-2"> <div class="flex items-center space-x-2">
<span>Select</span> <span>Select</span>

View file

@ -17,9 +17,11 @@
<% end %> <% end %>
<% unless @account.crypto? %> <% unless @account.crypto? %>
<%= link_to @account.investment? ? new_trade_path(account_id: @account.id) : new_transaction_path(account_id: @account.id), data: { turbo_frame: :modal }, class: "block p-2 rounded-lg hover:bg-gray-50 flex items-center gap-2" do %> <%= link_to @account.investment? ? new_trade_path(account_id: @account.id) : new_transaction_path(account_id: @account.id), data: { turbo_frame: :modal }, class: "btn btn--primary flex items-center justify-center gap-2 rounded-full md:rounded-lg w-9 h-9 md:w-auto md:h-auto" do %>
<span class="flex items-center justify-center">
<%= lucide_icon("credit-card", class: "text-secondary w-5 h-5") %> <%= lucide_icon("credit-card", class: "text-secondary w-5 h-5") %>
<%= tag.span t(".new_transaction"), class: "text-sm" %> </span>
<%= tag.span t(".new_transaction"), class: "text-sm md:block" %>
<% end %> <% end %>
<% end %> <% end %>
</div> </div>

View file

@ -21,7 +21,7 @@
<div data-controller="menu" data-menu-placement-value="bottom-start"> <div data-controller="menu" data-menu-placement-value="bottom-start">
<%= tag.button data: { menu_target: "button" }, class: "flex items-center gap-1 hover:bg-alpha-black-25 cursor-pointer 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> <span class="text-primary font-medium text-3xl md:text-base"><%= @budget.name %></span>
<%= lucide_icon "chevron-down", class: "w-5 h-5 shrink-0 text-secondary" %> <%= lucide_icon "chevron-down", class: "w-5 h-5 shrink-0 text-secondary" %>
<% end %> <% end %>

View file

@ -5,8 +5,8 @@
next_budget: @next_budget, next_budget: @next_budget,
latest_budget: @latest_budget %> latest_budget: @latest_budget %>
<div class="flex items-start gap-4"> <div class="flex flex-col items-start gap-4 md:flex-row">
<div class="w-[300px] space-y-4"> <div class="w-full md:w-[300px] space-y-4">
<div class="h-[300px] bg-container rounded-xl shadow-border-xs p-8"> <div class="h-[300px] bg-container rounded-xl shadow-border-xs p-8">
<% if @budget.available_to_allocate.negative? %> <% if @budget.available_to_allocate.negative? %>
<%= render "budgets/over_allocation_warning", budget: @budget %> <%= render "budgets/over_allocation_warning", budget: @budget %>
@ -49,7 +49,7 @@
</div> </div>
</div> </div>
<div class="grow bg-container rounded-xl shadow-border-xs p-4"> <div class="w-full grow bg-container rounded-xl shadow-border-xs p-4">
<div class="flex items-center justify-between mb-4"> <div class="flex items-center justify-between mb-4">
<h2 class="text-lg font-medium">Categories</h2> <h2 class="text-lg font-medium">Categories</h2>

View file

@ -1,3 +1,4 @@
<div class="w-16 h-16 flex-shrink-0 -ml-3 -mt-3"> <div class="w-16 h-16 flex-shrink-0 -ml-3 -mt-3">
<%= image_tag "ai.svg", alt: "AI", class: "w-full h-full" %> <%# Never use svg as an image tag, it appears blurry in Safari %>
<%= inline_svg_tag "ai.svg", alt: "AI", class: "w-full h-full" %>
</div> </div>

View file

@ -1,4 +1,4 @@
<div class="flex items-start gap-2 w-full"> <div class="flex items-start w-full">
<%= render "chats/ai_avatar" %> <%= render "chats/ai_avatar" %>
<div class="max-w-[85%] text-sm space-y-4 text-primary"> <div class="max-w-[85%] text-sm space-y-4 text-primary">
@ -13,11 +13,11 @@
<% questions = [ <% questions = [
{ {
icon: "bar-chart-2", icon: "chart-area",
text: "Evaluate investment portfolio" text: "Evaluate investment portfolio"
}, },
{ {
icon: "credit-card", icon: "wallet-minimal",
text: "Show spending insights" text: "Show spending insights"
}, },
{ {
@ -31,7 +31,7 @@
<button data-action="chat#submitSampleQuestion" <button data-action="chat#submitSampleQuestion"
data-chat-question-param="<%= question[:text] %>" data-chat-question-param="<%= question[:text] %>"
class="w-full flex items-center gap-2 border border-tertiary rounded-full py-1.5 px-2.5 hover:bg-gray-100"> class="w-full flex items-center gap-2 border border-tertiary rounded-full py-1.5 px-2.5 hover:bg-gray-100">
<%= icon(question[:icon]) %> <%= question[:text] %> <%= icon(question[:icon], color: "gray") %> <%= question[:text] %>
</button> </button>
<% end %> <% end %>
</div> </div>

View file

@ -1,5 +1,5 @@
<%= turbo_frame_tag chat_frame do %> <%= turbo_frame_tag chat_frame do %>
<div class="p-4 flex flex-col h-full"> <div class="flex flex-col h-full md:p-4">
<nav class="mb-6"> <nav class="mb-6">
<% back_path = @last_viewed_chat ? chat_path(@last_viewed_chat) : new_chat_path %> <% back_path = @last_viewed_chat ? chat_path(@last_viewed_chat) : new_chat_path %>
<%= link_to back_path, class: "w-9 h-9 flex items-center justify-center rounded-lg hover:bg-surface-hover" do %> <%= link_to back_path, class: "w-9 h-9 flex items-center justify-center rounded-lg hover:bg-surface-hover" do %>
@ -21,11 +21,11 @@
</div> </div>
<h3 class="text-lg font-medium text-primary mb-1">No chats yet</h3> <h3 class="text-lg font-medium text-primary mb-1">No chats yet</h3>
<p class="text-gray-500 mb-4">Start a new conversation with the AI assistant</p> <p class="text-gray-500 mb-4">Start a new conversation with the AI assistant</p>
<%= link_to "Start a chat", new_chat_path, class: "inline-flex items-center gap-2 py-2 px-4 bg-gray-800 text-white rounded-lg text-sm font-medium" %> </div>
<div class="mt-auto p-4">
<%= render "messages/chat_form", chat: nil %>
</div> </div>
<% end %> <% end %>
</div> </div>
<%= render "messages/chat_form" %>
</div> </div>
<% end %> <% end %>

View file

@ -1,5 +1,5 @@
<%= turbo_frame_tag chat_frame do %> <%= turbo_frame_tag chat_frame do %>
<div class="p-4 flex flex-col h-full"> <div class="flex flex-col h-full md:p-4">
<%= render "chats/chat_nav", chat: @chat %> <%= render "chats/chat_nav", chat: @chat %>
<div class="mt-auto py-8"> <div class="mt-auto py-8">

View file

@ -4,7 +4,7 @@
<h1 class="sr-only"><%= @chat.title %></h1> <h1 class="sr-only"><%= @chat.title %></h1>
<div class="flex flex-col h-full"> <div class="flex flex-col h-full">
<div class="p-4"> <div class="md:p-4">
<%= render "chats/chat_nav", chat: @chat %> <%= render "chats/chat_nav", chat: @chat %>
</div> </div>

View file

@ -11,25 +11,26 @@
</div> </div>
<% if @import.cleaned? %> <% if @import.cleaned? %>
<div class="bg-container border border-tertiary rounded-lg p-3 flex items-center justify-between"> <div class="bg-container border border-tertiary rounded-lg p-3 flex flex-col md:flex-row items-start md:items-center justify-between gap-2 md:gap-0">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<%= lucide_icon "check-circle", class: "w-4 h-4 text-green-500" %> <%= lucide_icon "check-circle", class: "w-4 h-4 text-green-500" %>
<p class="text-green-500">Your data has been cleaned</p> <p class="text-green-500 text-sm md:text-base">Your data has been cleaned</p>
</div> </div>
<%= link_to "Next step", import_confirm_path(@import), class: "btn btn--primary" %> <%= link_to "Next step", import_confirm_path(@import), class: "btn btn--primary w-full md:w-auto" %>
</div> </div>
<% else %> <% else %>
<div class="bg-container border border-tertiary rounded-lg p-3 flex items-center justify-between"> <div class="bg-container border border-tertiary rounded-lg p-3 flex flex-col md:flex-row items-start md:items-center justify-between gap-3 md:gap-0">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<%= lucide_icon "alert-triangle", class: "w-4 h-4 text-red-500" %> <%= lucide_icon "alert-triangle", class: "w-4 h-4 text-red-500 flex-shrink-0" %>
<p class="text-red-500 text-sm"><%= t(".errors_notice") %></p> <p class="text-red-500 text-sm hidden md:block"><%= t(".errors_notice") %></p>
<p class="text-red-500 text-sm md:hidden"><%= t(".errors_notice_mobile") %></p>
</div> </div>
<div class="flex justify-center"> <div class="flex justify-center w-full md:w-auto">
<div class="bg-gray-50 rounded-lg inline-flex p-1 space-x-2 text-sm text-primary font-medium"> <div class="bg-gray-50 rounded-lg inline-flex p-1 space-x-2 text-sm text-primary font-medium w-full md:w-auto">
<%= link_to "All rows", import_clean_path(@import, per_page: params[:per_page], view: "all"), class: "p-2 rounded-lg #{params[:view] != 'errors' ? 'bg-container' : ''}" %> <%= link_to "All rows", import_clean_path(@import, per_page: params[:per_page], view: "all"), class: "p-2 rounded-lg flex-1 md:flex-auto text-center #{params[:view] != 'errors' ? 'bg-container' : ''}" %>
<%= link_to "Error rows", import_clean_path(@import, per_page: params[:per_page], view: "errors"), class: "p-2 rounded-lg #{params[:view] == 'errors' ? 'bg-container' : ''}" %> <%= link_to "Error rows", import_clean_path(@import, per_page: params[:per_page], view: "errors"), class: "p-2 rounded-lg flex-1 md:flex-auto text-center #{params[:view] == 'errors' ? 'bg-container' : ''}" %>
</div> </div>
</div> </div>
</div> </div>
@ -37,7 +38,8 @@
<div class="pb-12"> <div class="pb-12">
<div class="bg-container-inset rounded-xl p-1 mb-6"> <div class="bg-container-inset rounded-xl p-1 mb-6">
<div style="grid-template-columns: repeat(<%= @import.column_keys.count %>, 1fr)" class="grid items-center uppercase text-xs font-medium text-secondary py-3"> <div class="overflow-x-auto">
<div style="grid-template-columns: repeat(<%= @import.column_keys.count %>, minmax(150px, 1fr)); min-width: max-content;" class="grid items-center uppercase text-xs font-medium text-secondary py-3">
<% @import.column_keys.each do |key| %> <% @import.column_keys.each do |key| %>
<div class="px-5"><%= import_col_label(key) %></div> <div class="px-5"><%= import_col_label(key) %></div>
<% end %> <% end %>
@ -50,6 +52,7 @@
</div> </div>
</div> </div>
</div> </div>
</div>
<div class="fixed bottom-0 left-1/2 -translate-x-1/2 w-full p-12"> <div class="fixed bottom-0 left-1/2 -translate-x-1/2 w-full p-12">
<div class="shadow-border-xs rounded-lg p-3 max-w-2xl mx-auto bg-container"> <div class="shadow-border-xs rounded-lg p-3 max-w-2xl mx-auto bg-container">

View file

@ -1,26 +1,36 @@
<%# locals: (import:, mapping_class:, step_idx:) %> <%# locals: (import:, mapping_class:, step_idx:) %>
<% mappings = mapping_class.for_import(import) %> <% mappings = mapping_class.for_import(import) %>
<% is_last_step = step_idx == import.mapping_steps.count - 1 %> <% is_last_step = step_idx == import.mapping_steps.count - 1 %>
<% if mapping_class == Import::AccountMapping && import.account.nil? %> <div class="w-full max-w-full">
<% if mapping_class == Import::AccountMapping && import.account.nil? %>
<% if import.requires_account? %> <% if import.requires_account? %>
<div class="flex items-center justify-between p-4 mb-4 gap-4 text-secondary bg-red-100 border border-red-200 rounded-lg w-[650px]"> <div class="w-full max-w-full overflow-hidden mb-4">
<div class="overflow-x-auto">
<div class="flex items-center justify-between p-4 gap-4 text-secondary bg-red-100 border border-red-200 rounded-lg w-[650px] min-w-0">
<%= tag.p t(".no_accounts"), class: "text-sm" %> <%= tag.p t(".no_accounts"), class: "text-sm" %>
<%= link_to t(".create_account"), new_account_path(return_to: import_confirm_path(import)), class: "btn btn--primary whitespace-nowrap", data: { turbo_frame: :modal } %> <%= link_to t(".create_account"), new_account_path(return_to: import_confirm_path(import)), class: "btn btn--primary whitespace-nowrap", data: { turbo_frame: :modal } %>
</div> </div>
</div>
</div>
<% elsif import.has_unassigned_account? %> <% elsif import.has_unassigned_account? %>
<div class="flex items-center justify-between p-4 mb-4 gap-4 text-secondary bg-yellow-100 border border-yellow-200 rounded-lg w-[650px]"> <div class="w-full max-w-full overflow-hidden mb-4">
<div class="overflow-x-auto">
<div class="flex items-center justify-between p-4 gap-4 text-secondary bg-yellow-100 border border-yellow-200 rounded-lg w-[650px] min-w-0">
<%= tag.p t(".unassigned_account"), class: "text-sm" %> <%= tag.p t(".unassigned_account"), class: "text-sm" %>
<%= link_to t(".create_account"), new_account_path(return_to: import_confirm_path(import)), class: "btn btn--primary whitespace-nowrap", data: { turbo_frame: :modal } %> <%= link_to t(".create_account"), new_account_path(return_to: import_confirm_path(import)), class: "btn btn--primary whitespace-nowrap", data: { turbo_frame: :modal } %>
</div> </div>
</div>
</div>
<% end %>
<% end %> <% end %>
<% end %>
<div class="space-y-4"> <div class="space-y-4 w-full max-w-full">
<div class="bg-container-inset rounded-xl p-1 space-y-1 w-[650px]"> <div class="w-full max-w-full overflow-hidden">
<div class="overflow-x-auto">
<div class="bg-container-inset rounded-xl p-1 space-y-1 w-[650px] min-w-0">
<div class="grid grid-cols-3 gap-2 text-xs font-medium text-secondary uppercase px-5 py-3"> <div class="grid grid-cols-3 gap-2 text-xs font-medium text-secondary uppercase px-5 py-3">
<p><%= t(".csv_mapping_label", mapping: mapping_label(mapping_class)) %></p> <p><%= t(".csv_mapping_label", mapping: mapping_label(mapping_class)) %></p>
<p><%= t(".maybe_mapping_label", mapping: mapping_label(mapping_class)) %></p> <p><%= t(".maybe_mapping_label", mapping: mapping_label(mapping_class)) %></p>
@ -35,11 +45,14 @@
<% end %> <% end %>
</div> </div>
</div> </div>
</div>
</div>
<div class="flex justify-center"> <div class="flex justify-center w-full">
<%= link_to is_last_step ? import_path(import) : url_for(step: step_idx + 2), class: "btn btn--primary w-36 flex items-center justify-between gap-2" do %> <%= link_to is_last_step ? import_path(import) : url_for(step: step_idx + 2), class: "btn btn--primary w-full md:w-36 flex items-center justify-between gap-2" do %>
<span>Next</span> <span>Next</span>
<%= lucide_icon "arrow-right", class: "w-5 h-5" %> <%= lucide_icon "arrow-right", class: "w-5 h-5" %>
<% end %> <% end %>
</div> </div>
</div>
</div> </div>

View file

@ -1,6 +1,6 @@
<%# locals: (row:) %> <%# locals: (row:) %>
<div style="grid-template-columns: repeat(<%= row.import.column_keys.count %>, 1fr)" class="first:rounded-tl-lg first:rounded-tr-lg last:rounded-bl-lg last:rounded-br-lg grid divide-x divide-alpha-black-200 group"> <div style="grid-template-columns: repeat(<%= row.import.column_keys.count %>, minmax(150px, 1fr)); min-width: max-content;" class="first:rounded-tl-lg first:rounded-tr-lg last:rounded-bl-lg last:rounded-br-lg grid divide-x divide-alpha-black-200 group">
<% row.import.column_keys.each_with_index do |key, idx| %> <% row.import.column_keys.each_with_index do |key, idx| %>
<%= turbo_frame_tag dom_id(row, key), title: row.valid? ? nil : row.errors.full_messages.join(", ") do %> <%= turbo_frame_tag dom_id(row, key), title: row.valid? ? nil : row.errors.full_messages.join(", ") do %>
<%= form_with( <%= form_with(
@ -9,18 +9,39 @@
url: import_row_path(row.import, row), url: import_row_path(row.import, row),
method: :patch, method: :patch,
data: { data: {
controller: "auto-submit-form", controller: "auto-submit-form mobile-cell-interaction",
auto_submit_form_trigger_event_value: "blur" auto_submit_form_trigger_value: "blur",
mobile_cell_interaction_error_value: !cell_is_valid?(row, key) ? row.errors[key].join(", ") : "",
} }
) do |form| %> ) do |form| %>
<div class="relative">
<%= form.text_field key, <%= form.text_field key,
"data-auto-submit-form-target": "auto", "data-auto-submit-form-target": "auto",
"data-action": "focus->mobile-cell-interaction#highlightCell blur->mobile-cell-interaction#unhighlightCell touchstart->mobile-cell-interaction#handleCellTouch",
"data-mobile-cell-interaction-target": "field",
class: [ class: [
cell_class(row, key), cell_class(row, key),
idx == 0 ? "group-first:rounded-tl-lg group-last:rounded-bl-lg" : "", idx == 0 ? "group-first:rounded-tl-lg group-last:rounded-bl-lg" : "",
idx == row.import.column_keys.count - 1 ? "group-first:rounded-tr-lg group-last:rounded-br-lg" : "", idx == row.import.column_keys.count - 1 ? "group-first:rounded-tr-lg group-last:rounded-br-lg" : "",
"focus:outline-none focus:z-10 relative",
], ],
disabled: row.import.complete? %> disabled: row.import.complete? %>
<% if !cell_is_valid?(row, key) %>
<span class="absolute right-2 top-1/2 -translate-y-1/2 text-red-500 md:hidden"
data-action="click->mobile-cell-interaction#toggleErrorMessage"
data-mobile-cell-interaction-target="errorIcon">
<%= lucide_icon "alert-circle", class: "w-4 h-4" %>
</span>
<div class="absolute left-4 right-4 bottom-full mb-2 p-2 bg-red-50 border border-red-200 rounded-lg shadow-lg text-xs text-red-600 hidden md:hidden z-20"
data-mobile-cell-interaction-target="errorTooltip">
<%= row.errors[key].join(", ") %>
</div>
<% end %>
<div class="absolute inset-0 bg-primary/5 pointer-events-none opacity-0 transition-opacity duration-150 ease-in-out z-0" data-mobile-cell-interaction-target="highlight"></div>
</div>
<% end %> <% end %>
<% end %> <% end %>
<% end %> <% end %>

View file

@ -4,18 +4,22 @@
<%= content_for :previous_path, imports_path %> <%= content_for :previous_path, imports_path %>
<div class="space-y-12"> <div class="space-y-4">
<div class="space-y-4 mx-auto max-w-md"> <div class="space-y-4 mx-auto max-w-md">
<div class="text-center space-y-2"> <div class="text-center space-y-2">
<h1 class="text-3xl text-primary font-medium"><%= t(".title") %></h1> <h1 class="text-3xl text-primary font-medium"><%= t(".title") %></h1>
<p class="text-secondary text-sm"><%= t(".description") %></p> <p class="text-secondary text-sm"><%= t(".description") %></p>
</div> </div>
<div data-controller="tabs" data-tabs-active-class="bg-container" data-tabs-default-tab-value="csv-paste-tab"> <div
<div class="flex justify-center mb-4"> data-controller="tabs"
<div class="tab-item-active rounded-lg inline-flex p-1 space-x-2 text-sm text-primary font-medium"> data-tabs-active-class="bg-surface shadow-sm text-primary"
<button type="button" data-id="csv-paste-tab" class="p-2 rounded-lg" data-tabs-target="btn" data-action="click->tabs#select">Copy & Paste</button> data-tabs-inactive-class="text-secondary"
<button type="button" data-id="csv-upload-tab" class="p-2 rounded-lg" data-tabs-target="btn" data-action="click->tabs#select">Upload CSV</button> data-tabs-default-tab-value="csv-upload-tab">
<div class="flex justify-center mb-4 w-full">
<div class="bg-surface-inset rounded-lg p-1 flex w-full">
<button type="button" data-id="csv-upload-tab" class="w-1/2 px-2 py-1 rounded-md text-secondary text-sm font-medium" data-tabs-target="btn" data-action="click->tabs#select">Upload CSV</button>
<button type="button" data-id="csv-paste-tab" class="w-1/2 px-2 py-1 rounded-md text-sm text-secondary font-medium" data-tabs-target="btn" data-action="click->tabs#select">Copy & Paste</button>
</div> </div>
</div> </div>
@ -35,11 +39,23 @@
placeholder: "Paste your CSV file contents here", placeholder: "Paste your CSV file contents here",
"data-auto-submit-form-target": "auto" %> "data-auto-submit-form-target": "auto" %>
<% else %> <% else %>
<label for="import_csv_file" class="flex flex-col items-center justify-center w-full h-56 border-2 border-secondary border-dashed rounded-lg cursor-pointer bg-container-inset"> <div class="flex flex-col items-center justify-center w-full h-64 border border-secondary border-dashed rounded-xl cursor-pointer" data-controller="file-upload" data-action="click->file-upload#triggerFileInput" data-file-upload-target="uploadArea">
<div class="flex flex-col items-center justify-center pt-5 pb-6"> <div class="flex flex-col items-center justify-center pt-5 pb-6">
<%= form.file_field :csv_file, class: "ml-32", "data-auto-submit-form-target": "auto" %> <div data-file-upload-target="uploadText" class="flex flex-col items-center">
<%= lucide_icon("plus", class: "w-6 h-6 mb-4 text-secondary mx-auto") %>
<p class="mb-2 text-md text-gray text-center">
<span class="font-medium text-primary">Browse</span> to add your CSV file here
</p>
</div>
<div class="flex flex-col items-center hidden" data-file-upload-target="fileName">
<%= lucide_icon("file-text", class: "w-6 h-6 mb-4 text-primary") %>
<p class="mb-2 text-md font-medium text-primary"></p>
</div>
<%= form.file_field :csv_file, class: "hidden", "data-auto-submit-form-target": "auto", "data-file-upload-target": "input" %>
</div>
</div> </div>
</label>
<% end %> <% end %>
<%= form.submit "Upload CSV", disabled: @import.complete? %> <%= form.submit "Upload CSV", disabled: @import.complete? %>
@ -49,22 +65,12 @@
</div> </div>
</div> </div>
<div class="bg-alpha-black-25 rounded-xl p-1 mt-5 mx-auto max-w-7xl"> <div class="flex justify-center">
<div class="text-secondary p-2 mb-2">
<div class="flex gap-2 mb-2"> <span class="text-secondary text-sm">
<%= lucide_icon("info", class: "w-5 h-5 shrink-0") %> <%= link_to "Download a sample CSV", "/imports/#{@import.id}/upload/sample_csv", class: "text-primary underline", data: { turbo: false } %> to see the required CSV format
<p class="text-sm"><%= t(".instructions_1") %></p> </span>
</div>
</div>
<ul class="list-disc list-inside text-sm pl-8">
<li><%= t(".instructions_2") %></li>
<li><%= t(".instructions_3") %></li>
<li><%= t(".instructions_4") %></li>
<li><%= t(".instructions_5") %></li>
</ul>
</div>
<%= render partial: "imports/table", locals: { headers: @import.csv_template.headers, rows: @import.csv_template } %>
</div>
</div> </div>

View file

@ -8,7 +8,16 @@
{ name: "Confirm", path: import_path(import), is_complete: import.complete?, step_number: 5 } { name: "Confirm", path: import_path(import), is_complete: import.complete?, step_number: 5 }
].reject { |step| step[:name] == "Map" && import.mapping_steps.empty? } %> ].reject { |step| step[:name] == "Map" && import.mapping_steps.empty? } %>
<ul class="flex items-center gap-2"> <% content_for :mobile_import_progress do %>
<% active_step = steps.detect { |s| request.path.eql?(s[:path]) } %>
<% if active_step.present? %>
<div class="md:hidden text-center text-secondary text-md my-2">
<span class="text-gray-500">Step <%= active_step[:step_number] %> of <%= steps.size %></span>
</div>
<% end %>
<% end %>
<ul class="hidden md:flex items-center gap-2">
<% steps.each_with_index do |step, idx| %> <% steps.each_with_index do |step, idx| %>
<li class="flex items-center gap-2 group"> <li class="flex items-center gap-2 group">
<% is_current = request.path == step[:path] %> <% is_current = request.path == step[:path] %>

View file

@ -1,37 +1,47 @@
<%# locals: (headers: [], rows: [], caption: nil) %> <%# locals: (headers: [], rows: [], caption: nil) %>
<div class="overflow-x-auto"> <div class="bg-container-inset rounded-xl overflow-hidden mx-1 md:mx-auto p-3 pl-2 md:pr-2 pr-0">
<div class="border border-secondary rounded-md shadow-border-xs text-sm bg-container w-full"> <% if caption %>
<div class="grid border-b border-b-alpha-black-200" style="grid-template-columns: repeat(<%= headers.length %>, minmax(0, 1fr))"> <div class="flex items-center mb-4">
<div class="text-gray-500 mr-2">
<%= inline_svg_tag "icon-csv.svg", class: "w-4 h-4" %>
</div>
<h2 class="text-sm text-gray-500 font-medium"><%= caption %></h2>
</div>
<% end %>
<div class="overflow-x-auto -webkit-overflow-scrolling-touch">
<div class="inline-block min-w-full sm:w-full border border-secondary rounded-md shadow-border-xs text-sm bg-container">
<table class="min-w-full divide-y divide-alpha-black-200">
<thead>
<tr>
<% headers.each_with_index do |header, index| %> <% headers.each_with_index do |header, index| %>
<div class=" <th class="
bg-container-inset px-3 py-2.5 font-medium whitespace-nowrap overflow-x-auto bg-container-inset px-3 py-2.5 font-medium text-left whitespace-nowrap
first:rounded-tl-md last:rounded-tr-md <%= index == 0 ? 'rounded-tl-md' : '' %>
<%= "border-r border-r-alpha-black-200" unless index == headers.length - 1 %> <%= index == headers.length - 1 ? 'rounded-tr-md' : '' %>
<%= index < headers.length - 1 ? 'border-r border-r-alpha-black-200' : '' %>
"> ">
<%= header %> <%= header %>
</div> </th>
<% end %> <% end %>
</div> </tr>
</thead>
<tbody class="divide-y divide-alpha-black-200">
<% rows.each_with_index do |row, row_index| %> <% rows.each_with_index do |row, row_index| %>
<div class="grid <%= "border-b border-b-alpha-black-200" if row_index < rows.length - 1 || caption %>" style="grid-template-columns: repeat(<%= headers.length %>, minmax(0, 1fr))"> <tr>
<% row.each_with_index do |(header, value), col_index| %> <% row.each_with_index do |(header, value), col_index| %>
<div class=" <td class="
px-3 py-2.5 whitespace-nowrap overflow-x-auto flex items-start px-3 py-2.5 whitespace-nowrap text-left
<%= "border-r border-r-alpha-black-200" unless col_index == row.length - 1 %> <%= col_index < row.length - 1 ? 'border-r border-r-alpha-black-200' : '' %>
<%= "rounded-bl-md" if !caption && row_index == rows.length - 1 && col_index == 0 %> <%= !caption && row_index == rows.length - 1 && col_index == 0 ? 'rounded-bl-md' : '' %>
<%= "rounded-br-md" if !caption && row_index == rows.length - 1 && col_index == row.length - 1 %> <%= !caption && row_index == rows.length - 1 && col_index == row.length - 1 ? 'rounded-br-md' : '' %>
"> ">
<%= value %> <%= value %>
</div> </td>
<% end %> <% end %>
</div> </tr>
<% end %> <% end %>
</tbody>
<% if caption %> </table>
<div class="px-3 py-2.5 text-center text-xs text-primary rounded-b-md italic bg-container-inset overflow-x-auto">
<%= caption %>
</div> </div>
<% end %>
</div> </div>
</div> </div>

View file

@ -1,10 +1,11 @@
<%= render "layouts/shared/htmldoc" do %> <%= render "layouts/shared/htmldoc" do %>
<% sidebar_config = app_sidebar_config(Current.user) %> <% sidebar_config = app_sidebar_config(Current.user) %>
<div class="flex flex-col lg:flex-row h-dvh lg:h-full bg-surface" <div class="flex flex-col lg:flex-row h-dvh lg:h-full bg-surface pt-safe"
data-controller="sidebar" data-controller="sidebar"
data-sidebar-user-id-value="<%= Current.user.id %>" data-sidebar-user-id-value="<%= Current.user.id %>"
data-sidebar-config-value="<%= sidebar_config.to_json %>"> data-sidebar-config-value="<%= sidebar_config.to_json %>">
<% unless controller_name == 'chats' %>
<nav class="flex justify-between lg:justify-start lg:flex-col shrink-0 lg:w-[84px] p-3 lg:px-0 lg:py-4 lg:mr-3"> <nav class="flex justify-between lg:justify-start lg:flex-col shrink-0 lg:w-[84px] p-3 lg:px-0 lg:py-4 lg:mr-3">
<button data-action="sidebar#toggleLeftPanelMobile" class="lg:hidden inline-flex p-2 rounded-lg items-center justify-center hover:bg-gray-100 cursor-pointer"> <button data-action="sidebar#toggleLeftPanelMobile" class="lg:hidden inline-flex p-2 rounded-lg items-center justify-center hover:bg-gray-100 cursor-pointer">
@ -12,7 +13,7 @@
</button> </button>
<%# Mobile only account sidebar groups %> <%# Mobile only account sidebar groups %>
<%= tag.div class: class_names("hidden bg-surface z-20 absolute inset-0 h-dvh w-full p-4 overflow-y-auto transition-all duration-300"), <%= tag.div class: class_names("hidden bg-surface z-20 absolute inset-0 h-dvh w-full p-4 overflow-y-auto transition-all duration-300 pt-safe"),
data: { sidebar_target: "leftPanelMobile" } do %> data: { sidebar_target: "leftPanelMobile" } do %>
<div id="account-sidebar-tabs"> <div id="account-sidebar-tabs">
<div class="mb-4"> <div class="mb-4">
@ -54,6 +55,7 @@
</div> </div>
</div> </div>
</nav> </nav>
<% end %>
<div class="flex justify-between lg:justify-normal grow overflow-y-auto"> <div class="flex justify-between lg:justify-normal grow overflow-y-auto">
<%= tag.div class: class_names("py-4 shrink-0 h-full overflow-y-auto transition-all duration-300 hidden lg:block"), <%= tag.div class: class_names("py-4 shrink-0 h-full overflow-y-auto transition-all duration-300 hidden lg:block"),
@ -108,7 +110,7 @@
<% end %> <% end %>
</div> </div>
<nav class="lg:hidden bg-container shrink-0 z-10 pb-2"> <nav class="lg:hidden bg-surface md:bg-container shrink-0 z-10 pb-2 border border-tertiary pb-safe">
<ul class="flex items-center justify-around gap-1"> <ul class="flex items-center justify-around gap-1">
<li> <li>
<%= render "layouts/sidebar/nav_item", name: "Home", path: root_path, icon_key: "pie-chart" %> <%= render "layouts/sidebar/nav_item", name: "Home", path: root_path, icon_key: "pie-chart" %>
@ -123,7 +125,7 @@
</li> </li>
<li> <li>
<%= render "layouts/sidebar/nav_item", name: "Assistant", path: chats_path, icon_key: "bot" %> <%= render "layouts/sidebar/nav_item", name: "Assistant", path: chats_path, icon_key: "icon-assistant", is_custom: true %>
</li> </li>
</ul> </ul>
</nav> </nav>

View file

@ -3,28 +3,38 @@
<div class="flex flex-col h-screen px-6 py-12 bg-surface"> <div class="flex flex-col h-screen px-6 py-12 bg-surface">
<div class="grow flex flex-col justify-center"> <div class="grow flex flex-col justify-center">
<div class="sm:mx-auto sm:w-full sm:max-w-md"> <div class="sm:mx-auto sm:w-full sm:max-w-md">
<div class="flex justify-center mb-6"> <div class="flex justify-center mt-2 md:mb-6">
<%= image_tag "logo-color.png", class: "w-16 mb-6" %> <%= image_tag "logo-color.png", class: "w-16 mb-6" %>
</div> </div>
<div class="space-y-2"> <div class="space-y-2">
<h2 class="text-3xl font-medium text-primary text-center"> <h2 class="text-3xl font-medium text-primary text-center">
<%= content_for?(:header_title) ? yield(:header_title).html_safe : t(".your_account") %> <%= content_for?(:header_title) ? yield(:header_title).html_safe : t(".your_account") %>
</h2> </h2>
<div class="space-y-3 md:hidden w-full my-4">
<div class="bg-surface-inset rounded-lg p-1 flex">
<%= link_to new_session_path,
class: "w-1/2 px-2 py-1 rounded-md text-sm text-center font-medium #{current_page?(new_session_path) ? 'bg-surface shadow-sm text-primary' : 'text-secondary'}" do %>
<%= t("layouts.auth.sign_in") %>
<% end %>
<%= link_to new_registration_path,
class: "w-1/2 px-2 py-1 rounded-md text-sm text-center font-medium #{!current_page?(new_session_path) ? 'bg-surface shadow-sm text-primary' : 'text-secondary'}" do %>
<%= t("layouts.auth.sign_up") %>
<% end %>
</div>
</div>
<% if controller_name == "sessions" %> <% if controller_name == "sessions" %>
<p class="text-sm text-center"> <p class="text-sm text-center hidden md:block">
<%= tag.span t(".no_account"), class: "text-secondary" %> <%= link_to t(".sign_up"), new_registration_path, class: "font-medium text-primary hover:underline transition" %> <%= 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> </p>
<% elsif controller_name == "registrations" %> <% elsif controller_name == "registrations" %>
<p class="text-sm text-center text-gray-600"> <p class="text-sm text-center text-gray-600 hidden md:block">
<%= t(".existing_account") %> <%= link_to t(".sign_in"), new_session_path, class: "font-medium text-primary hover:underline transition" %> <%= t(".existing_account") %> <%= link_to t(".sign_in"), new_session_path, class: "font-medium text-primary hover:underline transition" %>
</p> </p>
<% end %> <% end %>
</div> </div>
</div> </div>
<div class="mt-8 sm:mx-auto sm:w-full sm:max-w-lg"> <div class="mt-0 md:mt-8 sm:mx-auto sm:w-full sm:max-w-lg">
<%= yield %> <%= yield %>
</div> </div>
</div> </div>

View file

@ -1,5 +1,5 @@
<%= render "layouts/shared/htmldoc" do %> <%= render "layouts/shared/htmldoc" do %>
<div class="flex flex-col h-dvh bg-surface"> <div class="flex flex-col h-dvh bg-white pt-safe">
<header class="flex items-center justify-between p-8"> <header class="flex items-center justify-between p-8">
<%= link_to content_for(:previous_path) || imports_path do %> <%= link_to content_for(:previous_path) || imports_path do %>
<%= lucide_icon "arrow-left", class: "w-5 h-5 text-secondary" %> <%= lucide_icon "arrow-left", class: "w-5 h-5 text-secondary" %>
@ -14,7 +14,11 @@
<% end %> <% end %>
</header> </header>
<main class="grow px-8 pt-12 pb-32 overflow-y-auto"> <main class="grow px-8 md:pt-12 pb-32 overflow-y-auto">
<div class="flex md:hidden justify-center w-full">
<%= yield :mobile_import_progress %>
</div>
<%= yield %> <%= yield %>
</main> </main>
</div> </div>

View file

@ -1,10 +1,10 @@
<%= render "layouts/shared/htmldoc" do %> <%= render "layouts/shared/htmldoc" do %>
<div class="flex h-full bg-surface"> <div class="md:flex h-full bg-surface">
<div class="p-4 w-96 shrink-0 h-full overflow-y-auto"> <div class="p-4 w-full md:w-96 shrink-0 md:h-full md:overflow-y-auto">
<%= render "settings/settings_nav" %> <%= render "settings/settings_nav" %>
</div> </div>
<main class="py-4 px-10 grow flex h-full overflow-y-auto"> <main class="px-4 pt-2 md:py-4 md: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="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"> <div class="grow space-y-4 overflow-y-auto -mx-1 px-1 pb-12">
<% if content_for?(:breadcrumbs) %> <% if content_for?(:breadcrumbs) %>
@ -14,12 +14,13 @@
<% end %> <% end %>
<% if content_for?(:page_title) %> <% if content_for?(:page_title) %>
<h1 class="text-primary text-xl font-medium"> <h1 class="text-primary text-3xl md:text-xl font-medium">
<%= content_for :page_title %> <%= content_for :page_title %>
</h1> </h1>
<% end %> <% end %>
<%= yield %> <%= yield %>
<%= settings_nav_footer_mobile %>
</div> </div>
<div class="mt-4"> <div class="mt-4">

View file

@ -1,6 +1,6 @@
<%# locals: (breadcrumbs:, sidebar_toggle_enabled: true) %> <%# locals: (breadcrumbs:, sidebar_toggle_enabled: true) %>
<nav class="flex items-center gap-2 mb-6"> <nav class="items-center gap-2 mb-6 hidden md:flex">
<% if sidebar_toggle_enabled %> <% if sidebar_toggle_enabled %>
<button data-action="sidebar#toggleLeftPanel" class="hidden p-2 lg:inline-flex rounded-lg items-center justify-center hover:bg-container-inset cursor-pointer"> <button data-action="sidebar#toggleLeftPanel" class="hidden p-2 lg:inline-flex rounded-lg items-center justify-center hover:bg-container-inset cursor-pointer">
<%= icon("panel-left", color: "gray") %> <%= icon("panel-left", color: "gray") %>

View file

@ -12,14 +12,19 @@
<%= javascript_importmap_tags %> <%= javascript_importmap_tags %>
<%= turbo_refreshes_with method: :morph, scroll: :preserve %> <%= turbo_refreshes_with method: :morph, scroll: :preserve %>
<meta name="viewport" <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, viewport-fit=cover" />
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="mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent"> <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<meta name="apple-mobile-web-app-title" content="Maybe"> <meta name="apple-mobile-web-app-title" content="Maybe">
<meta name="msapplication-TileColor" content="#ffffff"> <meta name="msapplication-TileColor" content="#ffffff">
<meta name="theme-color" content="#ffffff"> <meta name="theme-color" content="#ffffff">
<link rel="manifest" href="<%= pwa_manifest_path %>">
<link rel="apple-touch-icon" href="/logo-pwa.png">
<link rel="apple-touch-icon" sizes="512x512" href="/icon.png">
<%= yield :head %> <%= yield :head %>
</head> </head>

View file

@ -5,8 +5,8 @@
<%= yield :head %> <%= yield :head %>
</head> </head>
<body class="h-full overflow-hidden lg:overflow-auto antialiased"> <body class="h-screen overflow-hidden lg:overflow-auto antialiased h-screen-safe ">
<div class="fixed z-50 bottom-6 left-24 w-80"> <div class="fixed z-50 top-6 md:top-auto md:bottom-6 md:left-24 w-full md:w-80 mx-auto md:mx-0 md:right-auto mt-safe">
<div id="notification-tray" class="space-y-1 w-full"> <div id="notification-tray" class="space-y-1 w-full">
<%= render_flash_notifications %> <%= render_flash_notifications %>

View file

@ -1,16 +1,19 @@
<%# locals: (name:, path:, icon_key:) %> <%# locals: (name:, path:, icon_key:, is_custom: false) %>
<%= link_to path, class: "space-y-1 lg:py-1 group block relative" do %>
<%= link_to path, class: "space-y-1 lg:py-1 group block" do %> <% if page_active?(path) %>
<%= tag.div class: "w-4 h-1 bg-nav-indicator rounded-bl-sm rounded-br-sm absolute top-0 left-1/2 -translate-x-1/2 lg:hidden" %>
<% end %>
<div class="grow flex flex-col lg:flex-row gap-1 items-center"> <div class="grow flex flex-col lg:flex-row gap-1 items-center">
<%= tag.div class: class_names("w-4 h-1 lg:w-1 lg:h-4 rounded-bl-sm rounded-br-sm lg:rounded-tr-sm lg:rounded-br-sm lg:rounded-bl-none", "bg-container-inset" => page_active?(path)) %> <%= tag.div class: class_names("w-4 h-1 lg:w-1 lg:h-4 rounded-bl-sm rounded-br-sm lg:rounded-tr-sm lg:rounded-br-sm lg:rounded-bl-none", "bg-nav-indicator" => 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-container-inset shadow-xs text-primary" : "group-hover:bg-container-inset-hover text-secondary") do %> <% icon_color = page_active?(path) ? "current" : "gray" %>
<%= icon(icon_key) %> <%= tag.div class: class_names("w-8 h-8 flex items-center justify-center mx-auto rounded-lg", page_active?(path) ? "bg-container shadow-xs text-primary" : "group-hover:bg-container-hover text-secondary") do %>
<%= is_custom ? icon_custom(icon_key, color: icon_color) : icon(icon_key, color: icon_color) %>
<% end %> <% end %>
</div> </div>
<div class="grow pl-2"> <div class="grow flex justify-center">
<%= tag.p class: class_names("text-center font-medium text-[11px]", page_active?(path) ? "text-primary" : "text-secondary") do %> <%= tag.p class: class_names("font-medium text-[11px]", page_active?(path) ? "text-primary" : "text-secondary") do %>
<%= name %> <%= name %>
<% end %> <% end %>
</div> </div>

View file

@ -1,5 +1,5 @@
<%= render "layouts/shared/htmldoc" do %> <%= render "layouts/shared/htmldoc" do %>
<div class="flex flex-col h-dvh"> <div class="flex flex-col h-dvh pt-safe">
<header class="flex items-center justify-between p-8"> <header class="flex items-center justify-between p-8">
<%= link_to content_for(:previous_path) || root_path do %> <%= link_to content_for(:previous_path) || root_path do %>
<%= lucide_icon "arrow-left", class: "w-5 h-5 text-secondary" %> <%= lucide_icon "arrow-left", class: "w-5 h-5 text-secondary" %>

View file

@ -4,32 +4,25 @@
<% model = chat && chat.persisted? ? [chat, Message.new] : Chat.new %> <% model = chat && chat.persisted? ? [chat, Message.new] : Chat.new %>
<%= form_with model: model, <%= form_with model: model,
class: "flex flex-col gap-2 bg-container px-2 py-1.5 rounded-lg shadow-border-xs", class: "flex items-center gap-2 bg-container p-2 rounded-full shadow-sm border border-gray-100 h-11",
data: { chat_target: "form" } do |f| %> data: { chat_target: "form" } do |f| %>
<%# In the future, this will be a dropdown with different AI models %> <%# In the future, this will be a dropdown with different AI models %>
<%= f.hidden_field :ai_model, value: "gpt-4o" %> <%= f.hidden_field :ai_model, value: "gpt-4o" %>
<button type="button" class="flex-shrink-0 text-secondary p-1">
<%= lucide_icon("plus", class: "w-5 h-5") %>
</button>
<%= f.text_area :content, placeholder: "Ask anything ...", value: message_hint, <%= f.text_area :content, placeholder: "Ask anything ...", value: message_hint,
class: "w-full border-0 focus:ring-0 text-sm resize-none px-1 bg-transparent", class: "w-full border-0 focus:ring-0 text-sm resize-none bg-transparent py-0",
data: { chat_target: "input", action: "input->chat#autoResize keydown->chat#handleInputKeyDown" }, data: { chat_target: "input", action: "input->chat#autoResize keydown->chat#handleInputKeyDown" },
rows: 1 %> rows: 1 %>
<div class="flex items-center justify-between gap-1"> <button type="submit" class="flex-shrink-0 text-secondary bg-gray-50 rounded-full p-2">
<div class="flex items-center gap-1"> <%= lucide_icon("arrow-up", class: "w-4 h-4") %>
<%# These are disabled for now, but in the future, will all open specific menus with their own context and search %>
<% ["plus", "command", "at-sign", "mouse-pointer-click"].each do |icon| %>
<button type="button" title="Coming soon" class="cursor-not-allowed w-8 h-8 flex justify-center items-center hover:bg-surface-hover rounded-lg">
<%= icon(icon, color: "gray") %>
</button> </button>
<% end %> <% end %>
</div>
<button type="submit" class="w-8 h-8 flex justify-center items-center text-secondary hover:bg-surface-hover cursor-pointer rounded-lg"> <p class="text-xs text-secondary mt-1">AI responses are informational only and are not financial advice.</p>
<%= icon("arrow-up") %>
</button>
</div>
<% end %>
<p class="text-xs text-secondary">AI responses are informational only and are not financial advice.</p>
</div> </div>

View file

@ -1,4 +1,4 @@
<header class="flex justify-between items-center p-4"> <header class="flex justify-between items-center p-4 pt-safe">
<%= image_tag "logo.svg", class: "h-[22px]" %> <%= image_tag "logo.svg", class: "h-[22px]" %>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<%= lucide_icon "log-in", class: "w-5 h-5 shrink-0 text-secondary gap-2" %> <%= lucide_icon "log-in", class: "w-5 h-5 shrink-0 text-secondary gap-2" %>

View file

@ -1,7 +1,7 @@
<div class="bg-gray-25 h-screen flex flex-col justify-between"> <div class="bg-gray-25 h-screen flex flex-col justify-between">
<%= render "onboardings/header" %> <%= render "onboardings/header" %>
<div class="grow max-w-lg w-full mx-auto bg-gray-25 flex flex-col justify-center" data-controller="onboarding"> <div class="grow max-w-lg w-full mx-auto bg-gray-25 flex flex-col justify-center md:py-0 py-6 px-4 md:px-0" data-controller="onboarding">
<div> <div>
<div class="space-y-1 mb-6"> <div class="space-y-1 mb-6">
<h1 class="text-2xl font-medium"><%= t(".title") %></h1> <h1 class="text-2xl font-medium"><%= t(".title") %></h1>

View file

@ -1,10 +1,10 @@
<div class="bg-gray-25 h-screen flex flex-col justify-between"> <div class="bg-gray-25 min-h-screen flex flex-col justify-between">
<%= render "onboardings/header" %> <%= render "onboardings/header" %>
<div class="grow max-w-lg w-full mx-auto bg-gray-25 flex flex-col justify-center"> <div class="grow max-w-lg w-full mx-auto bg-gray-25 flex flex-col justify-center md:py-0 py-6 px-4 md:px-0">
<div> <div>
<div class="space-y-1 mb-6"> <div class="space-y-1 mb-6">
<h1 class="text-2xl font-medium"><%= t(".title") %></h1> <h1 class="text-2xl font-medium md:text-2xl"><%= t(".title") %></h1>
<p class="text-secondary text-sm"><%= t(".subtitle") %></p> <p class="text-secondary text-sm"><%= t(".subtitle") %></p>
</div> </div>
@ -13,13 +13,13 @@
<%= form.hidden_field :onboarded_at, value: Time.current if @invitation %> <%= form.hidden_field :onboarded_at, value: Time.current if @invitation %>
<div class="space-y-4 mb-4"> <div class="space-y-4 mb-4">
<p class="text-secondary text-xs"><%= t(".profile_image") %></p> <p class="text-secondary text-xs hidden md:block"><%= t(".profile_image") %></p>
<%= render "settings/user_avatar_field", form: form, user: @user %> <%= render "settings/user_avatar_field", form: form, user: @user %>
</div> </div>
<div class="flex justify-between items-center gap-4 mb-4"> <div class="flex flex-col md:flex-row md:justify-between md:items-center md:gap-4 space-y-4 md:space-y-0 mb-4">
<%= form.text_field :first_name, placeholder: t(".first_name"), label: t(".first_name"), container_class: "bg-container w-1/2", required: true %> <%= form.text_field :first_name, placeholder: t(".first_name"), label: t(".first_name"), container_class: "bg-container md:w-1/2 w-full", required: true %>
<%= form.text_field :last_name, placeholder: t(".last_name"), label: t(".last_name"), container_class: "bg-container w-1/2", required: true %> <%= form.text_field :last_name, placeholder: t(".last_name"), label: t(".last_name"), container_class: "bg-container md:w-1/2 w-full", required: true %>
</div> </div>
<% unless @invitation %> <% unless @invitation %>
<div class="space-y-4 mb-4"> <div class="space-y-4 mb-4">

View file

@ -1,9 +1,9 @@
<%= content_for :page_title, t(".title") %> <%= content_for :page_title, t(".title") %>
<div class="bg-container shadow-border-xs rounded-xl p-4 grow overflow-y-auto"> <div class="bg-container 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="flex flex-col md:flex-row justify-between gap-4 mb-12 last:mb-0">
<div class="w-1/3"> <div class="w-full md:w-1/3">
<div class="px-3 flex items-center gap-3"> <div class="md:px-3 flex items-center gap-3">
<div class="text-white shrink-0 w-9 h-9"> <div class="text-white shrink-0 w-9 h-9">
<%= image_tag @release_notes[:avatar], class: "rounded-full w-full h-full object-cover" %> <%= image_tag @release_notes[:avatar], class: "rounded-full w-full h-full object-cover" %>
</div> </div>
@ -13,7 +13,7 @@
</div> </div>
</div> </div>
</div> </div>
<div class="w-2/3 text-secondary text-sm prose prose--github-release-notes"> <div class="w-full md:w-2/3 text-secondary text-sm prose prose--github-release-notes">
<h2 class="mb-5 text-xl text-primary"><%= @release_notes[:name] %></h2> <h2 class="mb-5 text-xl text-primary"><%= @release_notes[:name] %></h2>
<%= @release_notes[:body].html_safe %> <%= @release_notes[:body].html_safe %>
</div> </div>

View file

@ -1,8 +1,18 @@
<% content_for :page_header do %> <% content_for :page_header do %>
<div class="space-y-1 mb-6"> <div class="space-y-1 mb-6 flex justify-between">
<div class="space-y-1">
<h1 class="text-3xl font-medium text-primary">Welcome back, <%= Current.user.first_name %></h1> <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> <p class="text-gray-500">Here's what's happening with your finances</p>
</div> </div>
<%= link_to new_account_path(step: "method_select", classification: "asset"),
class: "btn btn--primary flex items-center justify-center gap-2 rounded-full w-9 h-9 md:hidden",
data: { turbo_frame: "modal" } do %>
<span class="flex items-center justify-center">
<%= lucide_icon("plus", class: "size-5") %>
</span>
<% end %>
</div>
<% end %> <% end %>
<div class="w-full space-y-6 pb-24"> <div class="w-full space-y-6 pb-24">

View file

@ -25,7 +25,7 @@
<div class="bg-surface rounded-xl p-1 space-y-1"> <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 class="px-4 py-2 flex items-center uppercase text-xs font-medium text-secondary">
<div>Name</div> <div class="hidden sm:block">Name</div>
<div class="ml-auto text-right flex items-center gap-6"> <div class="ml-auto text-right flex items-center gap-6">
<div class="w-24"> <div class="w-24">
<p>Weight</p> <p>Weight</p>
@ -40,7 +40,7 @@
<% classification_group.account_groups.each do |account_group| %> <% classification_group.account_groups.each do |account_group| %>
<details class="group rounded-lg open:bg-surface font-medium text-sm"> <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"> <summary class="cursor-pointer p-4 group-open:bg-surface bg-container rounded-lg flex items-center justify-between">
<div class="flex items-center gap-4"> <div class="items-center gap-4 hidden md:flex">
<%= lucide_icon("chevron-right", class: "group-open:rotate-90 text-secondary w-5 h-5") %> <%= lucide_icon("chevron-right", class: "group-open:rotate-90 text-secondary w-5 h-5") %>
<p><%= account_group.name %></p> <p><%= account_group.name %></p>
@ -48,9 +48,19 @@
<div class="ml-auto flex items-center text-right gap-6"> <div class="ml-auto flex items-center text-right gap-6">
<div class="w-24 flex items-center justify-end gap-2"> <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 } %> <%= render partial: "shared/progress_circle", locals: { progress: account_group.weight, color: account_group.color } %>
<p><%= number_to_percentage(account_group.weight, precision: 0) %></p> <p><%= number_to_percentage(account_group.weight, precision: 0) %></p>
</div> </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>
<div class="w-40"> <div class="w-40">
<p><%= format_money(account_group.total_money) %></p> <p><%= format_money(account_group.total_money) %></p>
@ -60,20 +70,33 @@
<div> <div>
<% account_group.accounts.each_with_index do |account, idx| %> <% 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="pl-4 sm:pl-12 pr-4 py-3 flex items-center justify-between text-sm font-medium">
<div class="flex items-center gap-3"> <div class="hidden sm:flex sm:items-center sm:gap-3">
<%= render "accounts/logo", account: account, size: "sm", color: account_group.color %> <%= render "accounts/logo", account: account, size: "sm", color: account_group.color %>
<%= link_to account.name, account_path(account) %> <%= link_to account.name, account_path(account) %>
</div> </div>
<div class="ml-auto flex items-center text-right gap-6"> <div class="ml-auto flex items-center text-right gap-6">
<div class="w-24 flex items-center justify-end gap-2"> <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 } %> <%= render partial: "shared/progress_circle", locals: { progress: account.weight, color: account_group.color } %>
<p><%= number_to_percentage(account.weight, precision: 0) %></p> <p><%= number_to_percentage(account.weight, precision: 0) %></p>
</div> </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>
<div class="w-40"> <div class="w-40">
<p><%= format_money(account.balance_money) %></p> <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> </div>
</div> </div>

View file

@ -3,26 +3,26 @@
<div class="bg-container shadow-border-xs rounded-xl p-4"> <div class="bg-container shadow-border-xs rounded-xl p-4">
<h2 class="text-lg font-medium text-primary mb-1">Leave feedback</h2> <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> <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"> <div class="flex flex-col md:flex-row gap-4">
<%= 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 %> <%= link_to "https://github.com/maybe-finance/maybe/discussions/categories/feature-requests", 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-gray-50" do %>
<%= image_tag "github-icon.svg", class: "w-8 h-8 mb-2" %> <%= 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 text-center">Write a feature request</span>
<% end %> <% end %>
<% if self_hosted? %> <% 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 %> <%= 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-gray-50" do %>
<%= image_tag "github-icon.svg", class: "w-8 h-8 mb-2" %> <%= 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> <span class="text-sm font-medium text-primary text-center">File a bug report</span>
<% end %> <% end %>
<% else %> <% 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 %> <%= link_to "mailto:hello@maybefinance.com", class: "w-full md: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" %> <%= lucide_icon "bug", class: "w-8 h-8 mb-2" %>
<span class="text-sm font-medium text-primary">File a bug report</span> <span class="text-sm font-medium text-primary text-center">File a bug report</span>
<% end %> <% end %>
<% 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 %> <%= link_to "https://link.maybe.co/discord", 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-gray-50" do %>
<%= image_tag "discord-icon.svg", class: "w-8 h-8 mb-2" %> <%= 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> <span class="text-sm font-medium text-primary text-center">Discuss Maybe with others</span>
<% end %> <% end %>
</div> </div>
</div> </div>

View file

@ -1,5 +1,6 @@
{ {
"name": "Maybe", "name": "Maybe",
"short_name": "Maybe",
"icons": [ "icons": [
{ {
"src": "/icon.png", "src": "/icon.png",
@ -15,8 +16,9 @@
], ],
"start_url": "/", "start_url": "/",
"display": "standalone", "display": "standalone",
"display_override": ["fullscreen", "minimal-ui"],
"scope": "/", "scope": "/",
"description": "Maybe.", "description": "Maybe.",
"theme_color": "red", "theme_color": "#ffffff",
"background_color": "red" "background_color": "#ffffff"
} }

View file

@ -32,11 +32,66 @@
placeholder: "you@example.com", placeholder: "you@example.com",
label: true, label: true,
disabled: @invitation.present? %> disabled: @invitation.present? %>
<%= form.password_field :password, autocomplete: "new-password", required: "required", label: true, maxlength: 72 %>
<%= form.password_field :password_confirmation, autocomplete: "new-password", required: "required", label: true %>
<% if invite_code_required? && !@invitation %> <% if invite_code_required? && !@invitation %>
<%= form.text_field :invite_code, required: "required", label: true, value: params[:invite] %> <%= form.text_field :invite_code, required: "required", label: true, value: params[:invite] %>
<% end %> <% end %>
<%= form.hidden_field :invitation, value: @invitation&.token %> <%= form.hidden_field :invitation, value: @invitation&.token %>
<div data-controller="password-validator">
<div data-controller="password-visibility" class="relative">
<%= form.password_field :password,
autocomplete: "new-password",
required: "required",
placeholder: t(".password_placeholder"),
label: true,
maxlength: 72,
data: {
password_validator_target: "input",
password_visibility_target: "input",
action: "input->password-validator#validate"
} %>
<button type="button"
class="absolute right-3 top-1/2 -translate-y-1/2 text-gray-500 hover:text-gray-700 focus:outline-none"
data-action="click->password-visibility#toggle">
<div data-password-visibility-target="showIcon">
<%= lucide_icon "eye", class: "w-5 h-5 text-gray" %>
</div>
<div data-password-visibility-target="hideIcon">
<%= lucide_icon "eye-off", class: "w-5 h-5 text-gray" %>
</div>
</button>
</div>
<div class="flex gap-4 my-4">
<div class="h-1 bg-gray-200 rounded-full flex-grow" data-password-validator-target="blockLine" data-requirement-type="length"></div>
<div class="h-1 bg-gray-200 rounded-full flex-grow" data-password-validator-target="blockLine" data-requirement-type="case"></div>
<div class="h-1 bg-gray-200 rounded-full flex-grow" data-password-validator-target="blockLine" data-requirement-type="number"></div>
<div class="h-1 bg-gray-200 rounded-full flex-grow" data-password-validator-target="blockLine" data-requirement-type="special"></div>
</div>
<div class="space-y-1 my-4">
<div class="flex items-center gap-2 text-secondary text-sm" data-password-validator-target="requirementType" data-requirement-type="length">
<%= lucide_icon "check", class: "w-4 h-4" %>
<span>Minimum 8 characters</span>
</div>
<div class="flex items-center gap-2 text-secondary text-sm" data-password-validator-target="requirementType" data-requirement-type="case">
<%= lucide_icon "check", class: "w-4 h-4" %>
<span>Upper and lowercase letters</span>
</div>
<div class="flex items-center gap-2 text-secondary text-sm" data-password-validator-target="requirementType" data-requirement-type="number">
<%= lucide_icon "check", class: "w-4 h-4" %>
<span>A number (0-9)</span>
</div>
<div class="flex items-center gap-2 text-secondary text-sm" data-password-validator-target="requirementType" data-requirement-type="special">
<%= lucide_icon "check", class: "w-4 h-4" %>
<span>A special character (!, @, #, $, %, etc)</span>
</div>
</div>
</div>
<%= form.submit t(".submit") %> <%= form.submit t(".submit") %>
<% end %> <% end %>

View file

@ -5,7 +5,7 @@
<%= styled_form_with url: sessions_path, class: "space-y-4", data: { turbo: false } do |form| %> <%= styled_form_with url: sessions_path, class: "space-y-4", data: { turbo: false } do |form| %>
<%= form.email_field :email, label: t(".email"), autofocus: false, autocomplete: "email", required: "required", placeholder: t(".email_placeholder") %> <%= form.email_field :email, label: t(".email"), autofocus: false, autocomplete: "email", required: "required", placeholder: t(".email_placeholder") %>
<%= form.password_field :password, label: t(".password"), required: "required" %> <%= form.password_field :password, label: t(".password"), required: "required", placeholder: t(".password_placeholder") %>
<%= form.submit t(".submit") %> <%= form.submit t(".submit") %>
<% end %> <% end %>

View file

@ -1,14 +1,14 @@
<div class="space-y-4"> <div class="pt-safe space-y-4">
<div class="flex items-center gap-2 p-1.5"> <div class="flex items-center gap-2 p-1.5">
<%= link_to previous_path, class: "flex items-center gap-1 text-primary font-medium text-sm" do %> <%= link_to previous_path, class: "flex items-center gap-1 text-primary font-medium text-sm" do %>
<%= lucide_icon "chevron-left", class: "w-5 h-5 text-secondary" %> <%= lucide_icon "chevron-left", class: "w-5 h-5 text-secondary" %>
<span>Back</span> <span class="hidden md:block">Back</span>
<% end %> <% end %>
<%= link_to previous_path, class: "uppercase bg-gray-100 rounded-sm px-1 py-0.5 text-xs text-secondary shadow-sm ml-1 pointer-events-none", data: { controller: "hotkey", hotkey: "Escape" } do %> <%= link_to previous_path, class: "hidden md:block uppercase bg-gray-100 rounded-sm px-1 py-0.5 text-xs text-secondary shadow-sm ml-1 pointer-events-none", data: { controller: "hotkey", hotkey: "Escape" } do %>
esc esc
<% end %> <% end %>
</div> </div>
<nav class="space-y-4"> <nav class="space-y-4 hidden md:block">
<section class="space-y-2"> <section class="space-y-2">
<div class="flex items-center gap-2 px-3"> <div class="flex items-center gap-2 px-3">
<h3 class="uppercase text-secondary font-medium text-xs"><%= t(".general_section_title") %></h3> <h3 class="uppercase text-secondary font-medium text-xs"><%= t(".general_section_title") %></h3>
@ -86,4 +86,73 @@
<% end %> <% end %>
</section> </section>
</nav> </nav>
<nav class="space-y-4 overflow-y-auto md:hidden" id="mobile-settings-nav" data-preserve-scroll data-controller="preserve-scroll">
<ul class="flex space-y-1">
<li>
<%= render "settings/settings_nav_item", name: t(".profile_label"), path: settings_profile_path, icon: "circle-user" %>
</li>
<li>
<%= 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 %>
<% unless self_hosted? %>
<li>
<%= render "settings/settings_nav_item", name: t(".billing_label"), path: settings_billing_path, icon: "circle-dollar-sign" %>
</li>
<% end %>
<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>
<li>
<%= render "settings/settings_nav_item", name: t(".tags_label"), path: tags_path, icon: "tags" %>
</li>
<li>
<%= render "settings/settings_nav_item", name: t(".categories_label"), path: categories_path, icon: "shapes" %>
</li>
<li>
<%= render "settings/settings_nav_item", name: t(".merchants_label"), path: merchants_path, icon: "store" %>
</li>
<li>
<%= render "settings/settings_nav_item", name: t(".whats_new_label"), path: changelog_path, icon: "box" %>
</li>
<li>
<%= render "settings/settings_nav_item", name: t(".feedback_label"), path: feedback_path, icon: "megaphone" %>
</li>
<%= 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 %>
</ul>
</nav>
</div> </div>

View file

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

View file

@ -1,5 +1,5 @@
<%# locals: path, direction, title %> <%# locals: path, direction, title %>
<%= link_to path, class: "w-full bg-container hover:bg-container-inset rounded-xl border border-alpha-black-25 shadow-xs p-4 flex items-center justify-between" do %> <%= link_to path, class: "hidden md:flex w-full bg-container hover:bg-container-inset rounded-xl border border-alpha-black-25 shadow-xs p-4 items-center justify-between" do %>
<% if direction == 'previous' %> <% if direction == 'previous' %>
<div class="w-5 h-5 text-secondary"> <div class="w-5 h-5 text-secondary">
<%= lucide_icon("arrow-left") %> <%= lucide_icon("arrow-left") %>
@ -15,3 +15,30 @@
</div> </div>
<% end %> <% end %>
<% end %> <% end %>
<%# Mobile version %>
<%= link_to path, class: "md:hidden w-full bg-container hover:bg-container-inset rounded-xl border border-alpha-black-25 shadow-xs py-3 px-4" do %>
<div class="flex items-center justify-between">
<% if direction == 'previous' %>
<div class="flex items-center gap-3">
<div class="w-5 h-5 text-secondary">
<%= lucide_icon("arrow-left") %>
</div>
<span class="text-sm text-secondary">Back</span>
</div>
<div>
<span class="text-sm font-medium text-primary"><%= title %></span>
</div>
<% else %>
<div>
<span class="text-sm text-secondary">Next</span>
</div>
<div class="flex items-center gap-3">
<span class="text-sm font-medium text-primary"><%= title %></span>
<div class="w-5 h-5 text-secondary">
<%= lucide_icon("arrow-right") %>
</div>
</div>
<% end %>
</div>
<% end %>

View file

@ -1,7 +1,7 @@
<%# locals: (form:, user:) %> <%# locals: (form:, user:) %>
<div class="flex items-center gap-4" data-controller="profile-image-preview"> <div class="flex md:flex-row flex-col md:items-center items-center gap-4" data-controller="profile-image-preview">
<div class="relative flex justify-center items-center bg-gray-50 w-24 h-24 rounded-full border-alpha-black-300 border border-dashed"> <div class="relative flex justify-center items-center bg-gray-50 size-26 md:size-24 rounded-full border-alpha-black-300 border border-dashed">
<%# The image preview once user has uploaded a new file %> <%# The image preview once user has uploaded a new file %>
<div data-profile-image-preview-target="previewImage" class="h-full w-full flex justify-center items-center hidden"> <div data-profile-image-preview-target="previewImage" class="h-full w-full flex justify-center items-center hidden">
@ -34,13 +34,23 @@
</button> </button>
</div> </div>
<div> <div class="md:text-left text-center">
<%= form.hidden_field :delete_profile_image, value: "0", data: { profile_image_preview_target: "deleteProfileImage" } %> <%= form.hidden_field :delete_profile_image, value: "0", data: { profile_image_preview_target: "deleteProfileImage" } %>
<p class="mb-3"><%= t(".accepted_formats") %></p> <%= form.label :profile_image, class: "btn btn--outline inline-block", data: { profile_image_preview_target: "uploadButton" } do %>
<%= lucide_icon "camera", class: "w-5 h-5 mr-2 inline-block", data: { profile_image_preview_target: "cameraIcon" } %>
<span data-profile-image-preview-target="uploadText">
<%= t(".choose") %> <span class="text-secondary"><%= t(".choose_label") %></span>
</span>
<span data-profile-image-preview-target="changeText" class="hidden" aria-hidden="true">
<%= t(".change") %>
</span>
<% end %>
<p class="mt-2 text-xs text-secondary"><%= t(".accepted_formats") %></p>
<%= form.label :profile_image, t(".choose"),
class: "btn btn--outline inline-block" %>
<%= form.file_field :profile_image, <%= form.file_field :profile_image,
accept: "image/png, image/jpeg", accept: "image/png, image/jpeg",
class: "hidden px-3 py-2 bg-gray-50 text-primary rounded-md text-sm font-medium", class: "hidden px-3 py-2 bg-gray-50 text-primary rounded-md text-sm font-medium",

View file

@ -1,13 +1,13 @@
<% if Current.user.admin? %> <% if Current.user.admin? %>
<div class="space-y-4"> <div class="space-y-4">
<div class="flex items-center justify-between"> <div class="flex flex-col md:flex-row md:items-center md:justify-between gap-4">
<div class="w-2/3"> <div class="w-full md:w-2/3">
<h3 class="font-medium text-primary"><%= t("settings.hostings.show.clear_cache") %></h3> <h3 class="font-medium text-primary"><%= t("settings.hostings.show.clear_cache") %></h3>
<p class="text-secondary text-sm"><%= t("settings.hostings.show.clear_cache_warning") %></p> <p class="text-secondary text-sm"><%= t("settings.hostings.show.clear_cache_warning") %></p>
</div> </div>
<%= <%=
button_to t("settings.hostings.show.clear_cache"), clear_cache_settings_hosting_path, method: :delete, button_to t("settings.hostings.show.clear_cache"), clear_cache_settings_hosting_path, method: :delete,
class: "bg-orange-500 text-white text-sm font-medium rounded-lg px-4 py-2", class: "w-full md:w-auto bg-orange-500 text-white text-sm font-medium rounded-lg px-4 py-2",
data: { turbo_confirm: { data: { turbo_confirm: {
title: t("settings.hostings.show.confirm_clear_cache.title"), title: t("settings.hostings.show.confirm_clear_cache.title"),
body: t("settings.hostings.show.confirm_clear_cache.body"), body: t("settings.hostings.show.confirm_clear_cache.body"),

View file

@ -47,7 +47,7 @@
<%= settings_section title: t(".theme_title"), subtitle: t(".theme_subtitle") do %> <%= settings_section title: t(".theme_title"), subtitle: t(".theme_subtitle") do %>
<div> <div>
<%= styled_form_with model: @user, class: "flex justify-between items-center", data: { controller: "auto-submit-form" } do |form| %> <%= 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| %>
<%= form.hidden_field :redirect_to, value: "preferences" %> <%= form.hidden_field :redirect_to, value: "preferences" %>
<div class="text-center"> <div class="text-center">
<%= image_tag("light-mode-preview.png", alt: "Light Theme Preview", class: "h-44 mb-4") %> <%= image_tag("light-mode-preview.png", alt: "Light Theme Preview", class: "h-44 mb-4") %>

View file

@ -11,12 +11,12 @@
You have requested to change your email to <%= @user.unconfirmed_email %>. Please go to your email and confirm for the change to take effect. 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> </p>
<% end %> <% end %>
<div class="grid grid-cols-2 gap-4 mt-4"> <div class="grid grid-cols-1 md:grid-cols-2 gap-4 mt-4">
<%= form.text_field :first_name, placeholder: t(".first_name"), label: t(".first_name") %> <%= 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") %> <%= form.text_field :last_name, placeholder: t(".last_name"), label: t(".last_name") %>
</div> </div>
<div class="flex justify-end mt-4"> <div class="flex justify-end mt-4">
<%= form.submit t(".save"), class: "btn btn--primary" %> <%= form.submit t(".save"), class: "btn btn--primary md:w-auto w-full" %>
</div> </div>
</div> </div>
<% end %> <% end %>
@ -129,14 +129,14 @@
<%= settings_section title: t(".danger_zone_title") do %> <%= settings_section title: t(".danger_zone_title") do %>
<div class="space-y-4"> <div class="space-y-4">
<% if Current.user.admin? %> <% if Current.user.admin? %>
<div class="flex items-center justify-between"> <div class="flex flex-col md:flex-row md:items-center md:justify-between gap-4">
<div class="w-2/3"> <div class="w-full md:w-2/3">
<h3 class="font-medium text-primary"><%= t(".reset_account") %></h3> <h3 class="font-medium text-primary"><%= t(".reset_account") %></h3>
<p class="text-secondary text-sm"><%= t(".reset_account_warning") %></p> <p class="text-secondary text-sm"><%= t(".reset_account_warning") %></p>
</div> </div>
<%= <%=
button_to t(".reset_account"), reset_user_path(@user), method: :delete, button_to t(".reset_account"), reset_user_path(@user), method: :delete,
class: "btn btn--destructive", class: "w-full md:w-auto btn btn--destructive",
data: { turbo_confirm: { data: { turbo_confirm: {
title: t(".confirm_reset.title"), title: t(".confirm_reset.title"),
body: t(".confirm_reset.body"), body: t(".confirm_reset.body"),
@ -146,14 +146,14 @@
%> %>
</div> </div>
<% end %> <% end %>
<div class="flex items-center justify-between"> <div class="flex flex-col md:flex-row md:items-center md:justify-between gap-4">
<div> <div class="w-full md:w-2/3">
<h3 class="font-medium text-primary"><%= t(".delete_account") %></h3> <h3 class="font-medium text-primary"><%= t(".delete_account") %></h3>
<p class="text-secondary text-sm"><%= t(".delete_account_warning") %></p> <p class="text-secondary text-sm"><%= t(".delete_account_warning") %></p>
</div> </div>
<%= <%=
button_to t(".delete_account"), user_path(@user), method: :delete, button_to t(".delete_account"), user_path(@user), method: :delete,
class: "btn btn--destructive", class: "w-full md:w-auto btn btn--destructive",
data: { turbo_confirm: { data: { turbo_confirm: {
title: t(".confirm_delete.title"), title: t(".confirm_delete.title"),
body: t(".confirm_delete.body"), body: t(".confirm_delete.body"),

View file

@ -2,7 +2,7 @@
<%= settings_section title: t(".mfa_title"), subtitle: t(".mfa_description") do %> <%= settings_section title: t(".mfa_title"), subtitle: t(".mfa_description") do %>
<div class="space-y-4"> <div class="space-y-4">
<div class="p-3 shadow-border-xs bg-container rounded-lg flex justify-between items-center"> <div class="p-3 shadow-border-xs bg-container rounded-lg md:flex md:justify-between md:items-center">
<div class="flex items-center gap-3"> <div class="flex items-center gap-3">
<div class="w-9 h-9 rounded-full bg-gray-25 flex justify-center items-center"> <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" %> <%= lucide_icon "shield-check", class: "w-5 h-5 text-secondary" %>
@ -19,10 +19,11 @@
</div> </div>
</div> </div>
<div class="mt-4 md:mt-0">
<% if Current.user.otp_required? %> <% if Current.user.otp_required? %>
<%= button_to t(".disable_mfa"), disable_mfa_path, <%= button_to t(".disable_mfa"), disable_mfa_path,
method: :delete, method: :delete,
class: "btn btn--secondary flex items-center gap-1", class: "w-full md:w-auto btn btn--secondary flex items-center gap-1 justify-center",
data: { turbo_confirm: { data: { turbo_confirm: {
title: t(".disable_mfa_confirm"), title: t(".disable_mfa_confirm"),
body: t(".disable_mfa_confirm"), body: t(".disable_mfa_confirm"),
@ -31,8 +32,9 @@
} } %> } } %>
<% else %> <% else %>
<%= link_to t(".enable_mfa"), new_mfa_path, <%= link_to t(".enable_mfa"), new_mfa_path,
class: "btn btn--primary flex items-center gap-1" %> class: "w-full md:w-auto btn btn--primary flex items-center gap-1 justify-center" %>
<% end %> <% end %>
</div> </div>
</div> </div>
</div>
<% end %> <% end %>

View file

@ -1,7 +1,7 @@
<%# locals: (content:, reload_on_close: false) %> <%# locals: (content:, reload_on_close: false) %>
<%= turbo_frame_tag "drawer" do %> <%= turbo_frame_tag "drawer" do %>
<dialog class="ml-auto bg-container shadow-border-xs rounded-2xl max-w-[480px] h-full w-full mt-4 mr-4 focus-visible:outline-hidden" <dialog class="ml-auto bg-container md:shadow-border-xs md:rounded-2xl max-w-screen max-h-screen md:max-w-[480px] h-full w-full md:mt-4 md:mr-4 pt-safe focus-visible:outline-hidden"
data-controller="modal" data-controller="modal"
data-action="mousedown->modal#clickOutside" data-action="mousedown->modal#clickOutside"
data-modal-reload-on-close-value="<%= reload_on_close %>"> data-modal-reload-on-close-value="<%= reload_on_close %>">

View file

@ -0,0 +1,6 @@
<%# locals: (key:, size: "md", color: "current") %>
<% size_class = case size when "sm" then "w-4 h-4" when "md" then "w-5 h-5" when "lg" then "w-6 h-6" end %>
<% color_class = case color when "current" then "text-current" when "gray" then "text-secondary" end %>
<%= inline_svg_tag "#{key}.svg", class: class_names(size_class, color_class, "shrink-0") %>

View file

@ -0,0 +1,6 @@
<%# locals: (key:, size: "md", color: "current") %>
<% size_class = case size when "sm" then "w-4 h-4" when "md" then "w-5 h-5" when "lg" then "w-6 h-6" end %>
<% color_class = case color when "current" then "text-current" when "gray" then "text-secondary" end %>
<%= image_tag("icon-#{key}.svg", class: class_names(size_class, color_class, "shrink-0"), alt: key )%>

View file

@ -1,7 +1,7 @@
<%# locals: (content:, classes:) -%> <%# locals: (content:, classes:) -%>
<%= turbo_frame_tag "modal" do %> <%= turbo_frame_tag "modal" do %>
<dialog class="m-auto bg-container shadow-border-xs rounded-2xl max-w-[580px] w-min-content h-fit overflow-visible <%= classes %>" data-controller="modal" data-action="mousedown->modal#clickOutside"> <dialog class="md:m-auto bg-container shadow-border-xs rounded-none md:rounded-2xl max-w-screen max-h-screen md:max-w-max w-full h-full md:h-fit md:w-auto overflow-visible <%= classes %>" data-controller="modal" data-action="mousedown->modal#clickOutside">
<div class="flex flex-col"> <div class="flex flex-col h-full md:h-auto mt-safe">
<%= content %> <%= content %>
</div> </div>
</dialog> </dialog>

View file

@ -1,11 +1,11 @@
<%# locals: (title:, content:, subtitle: nil) %> <%# locals: (title:, content:, subtitle: nil) %>
<%= modal do %> <%= modal do %>
<article class="mx-auto w-full p-4 space-y-4 min-w-[450px]"> <article class="mx-auto w-full p-4 space-y-4 md:min-w-[450px]">
<div class="space-y-2"> <div class="space-y-2">
<header class="flex justify-between items-center"> <header class="flex justify-between items-center">
<h2 class="font-medium"><%= title %></h2> <h2 class="font-medium"><%= title %></h2>
<%= lucide_icon("x", class: "cursor-pointer w-5 h-5 text-secondary", data: { action: "mousedown->modal#close" }) %> <%= lucide_icon("x", class: "cursor-pointer w-6 h-6 md:w-5 md:w-5 text-secondary", data: { action: "mousedown->modal#close" }) %>
</header> </header>
<% if subtitle.present? %> <% if subtitle.present? %>

View file

@ -3,7 +3,7 @@
<% type = type.to_sym %> <% type = type.to_sym %>
<% action = "animationend->element-removal#remove" if type == :notice %> <% action = "animationend->element-removal#remove" if type == :notice %>
<%= tag.div class: "flex gap-3 rounded-lg border bg-container p-4 group max-w-80 shadow-xs border-alpha-black-25", <%= tag.div class: "flex gap-3 rounded-lg border bg-container p-4 group max-w-80 shadow-xs border-alpha-black-25 mx-auto md:mx-0",
data: { data: {
controller: "element-removal", controller: "element-removal",
action: action action: action

View file

@ -5,11 +5,11 @@
<% end %> <% end %>
<section> <section>
<fieldset class="bg-gray-50 rounded-lg p-1 grid grid-flow-col justify-stretch gap-x-2"> <fieldset class="bg-gray-50 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? %> <%= 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" %> <%= 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 group-has-checked:bg-container group-has-checked:text-gray-800 group-has-checked:shadow-sm" do %> <%= 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-5 h-5" %> <%= lucide_icon "arrow-right-left", class: "w-4 h-4 md:w-5 md:h-5" %>
<%= tag.span t(".transfer") %> <%= tag.span t(".transfer") %>
<% end %> <% end %>
</fieldset> </fieldset>

View file

@ -1,4 +1,4 @@
<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="fixed bottom-30 md:bottom-6 z-10 flex items-center justify-between rounded-xl bg-gray-900 px-4 text-sm text-white md:w-[420px] w-[90%] py-1.5">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<%= check_box_tag "entry_selection", 1, true, class: "checkbox checkbox--dark", data: { action: "bulk-select#deselectAll" } %> <%= check_box_tag "entry_selection", 1, true, class: "checkbox checkbox--dark", data: { action: "bulk-select#deselectAll" } %>

View file

@ -1,5 +1,5 @@
<%# locals: (totals:) %> <%# locals: (totals:) %>
<div class="grid grid-cols-3 bg-container rounded-xl shadow-border-xs divide-x divide-alpha-black-100"> <div class="grid grid-cols-1 md:grid-cols-3 bg-container rounded-xl shadow-border-xs md:divide-x divide-y md:divide-y-0 divide-alpha-black-100">
<div class="p-4 space-y-2"> <div class="p-4 space-y-2">
<p class="text-sm text-secondary">Total transactions</p> <p class="text-sm text-secondary">Total transactions</p>
<p class="text-primary font-medium text-xl" id="total-transactions"><%= totals.transactions_count.round(0) %></p> <p class="text-primary font-medium text-xl" id="total-transactions"><%= totals.transactions_count.round(0) %></p>

View file

@ -4,12 +4,12 @@
<%= turbo_frame_tag dom_id(entry) do %> <%= turbo_frame_tag dom_id(entry) do %>
<%= turbo_frame_tag dom_id(transaction) do %> <%= turbo_frame_tag dom_id(transaction) do %>
<div class="grid grid-cols-12 items-center text-primary text-sm font-medium p-4 <div class="grid grid-cols-12 items-center text-primary text-sm font-medium p-4 md:p-4
<%= @focused_record == entry || @focused_record == transaction ? <%= @focused_record == entry || @focused_record == transaction ?
"border border-gray-900 rounded-lg" : "" %>"> "border border-gray-900 rounded-lg" : "" %>">
<div class="pr-10 flex items-center gap-4 <div class="pr-4 md:pr-10 flex items-center gap-3 md:gap-4
<%= balance_trend ? "col-span-6" : "col-span-8" %>"> <%= balance_trend ? "col-span-8 md:col-span-6" : "col-span-8" %>">
<%= check_box_tag dom_id(entry, "selection"), <%= check_box_tag dom_id(entry, "selection"),
disabled: transaction.transfer?, disabled: transaction.transfer?,
class: "checkbox checkbox--light", class: "checkbox checkbox--light",
@ -55,7 +55,7 @@
<% end %> <% end %>
</div> </div>
<div class="text-secondary text-xs font-normal"> <div class="text-secondary text-xs font-normal hidden md:block">
<% if transaction.transfer? %> <% if transaction.transfer? %>
<%= render "transfers/account_links", <%= render "transfers/account_links",
transfer: transaction.transfer, transfer: transaction.transfer,
@ -73,18 +73,18 @@
</div> </div>
</div> </div>
<div class="flex items-center gap-1 col-span-2"> <div class="hidden md:flex items-center gap-1 col-span-2">
<%= render "transactions/transaction_category", transaction: transaction %> <%= render "transactions/transaction_category", transaction: transaction %>
</div> </div>
<div class="col-span-2 ml-auto"> <div class="col-span-4 md:col-span-2 ml-auto text-right">
<%= content_tag :p, <%= content_tag :p,
transaction.transfer? && view_ctx == "global" ? "+/- #{format_money(entry.amount_money.abs)}" : format_money(-entry.amount_money), transaction.transfer? && view_ctx == "global" ? "+/- #{format_money(entry.amount_money.abs)}" : format_money(-entry.amount_money),
class: ["text-green-600": entry.amount.negative?] %> class: ["text-green-600": entry.amount.negative?] %>
</div> </div>
<% if balance_trend %> <% if balance_trend %>
<div class="col-span-2 justify-self-end"> <div class="col-span-2 justify-self-end hidden md:block">
<% if balance_trend.trend %> <% if balance_trend.trend %>
<%= tag.p format_money(balance_trend.trend.current), <%= tag.p format_money(balance_trend.trend.current),
class: "font-medium text-sm text-primary" %> class: "font-medium text-sm text-primary" %>

View file

@ -8,16 +8,19 @@
<%= contextual_menu_modal_action_item t(".edit_tags"), tags_path, icon: "tags", turbo_frame: :_top %> <%= contextual_menu_modal_action_item t(".edit_tags"), tags_path, icon: "tags", turbo_frame: :_top %>
<%= contextual_menu_modal_action_item t(".edit_merchants"), merchants_path, icon: "store", turbo_frame: :_top %> <%= contextual_menu_modal_action_item t(".edit_merchants"), merchants_path, icon: "store", turbo_frame: :_top %>
<%= contextual_menu_modal_action_item t(".edit_imports"), imports_path, icon: "hard-drive-upload", turbo_frame: :_top %> <%= contextual_menu_modal_action_item t(".edit_imports"), imports_path, icon: "hard-drive-upload", turbo_frame: :_top %>
<%= contextual_menu_modal_action_item t(".import"), new_import_path, icon: "download", turbo_frame: "modal", class_name: "md:!hidden" %>
<% end %> <% end %>
<%= link_to new_import_path, class: "btn btn--outline flex items-center gap-2", data: { turbo_frame: "modal" } do %> <%= link_to new_import_path, class: "btn btn--outline flex items-center gap-2 hidden md:flex", data: { turbo_frame: "modal" } do %>
<%= lucide_icon("download", class: "text-secondary w-4 h-4") %> <%= lucide_icon("download", class: "text-secondary w-4 h-4") %>
<p class="text-sm font-medium text-primary"><%= t(".import") %></p> <p class="text-sm font-medium text-primary"><%= t(".import") %></p>
<% end %> <% end %>
<%= link_to new_transaction_path, class: "btn btn--primary flex items-center gap-2", data: { turbo_frame: :modal } do %> <%= link_to new_transaction_path, class: "btn btn--primary flex items-center justify-center gap-2 rounded-full md:rounded-lg w-9 h-9 md:w-auto md:h-auto", data: { turbo_frame: :modal } do %>
<span class="flex items-center justify-center">
<%= lucide_icon("plus", class: "w-5 h-5") %> <%= lucide_icon("plus", class: "w-5 h-5") %>
<p class="text-sm font-medium">New transaction</p> </span>
<p class="text-sm font-medium hidden md:block">New transaction</p>
<% end %> <% end %>
</div> </div>
</div> </div>
@ -38,7 +41,7 @@
<% if @pagy.count > 0 %> <% if @pagy.count > 0 %>
<div class="grow overflow-y-auto"> <div class="grow overflow-y-auto">
<div class="grid grid-cols-12 bg-container-inset rounded-xl px-5 py-3 text-xs uppercase font-medium text-secondary items-center mb-4"> <div class="grid-cols-12 bg-container-inset rounded-xl px-5 py-3 text-xs uppercase font-medium text-secondary items-center mb-4 hidden md:grid">
<div class="pl-0.5 col-span-8 flex items-center gap-4"> <div class="pl-0.5 col-span-8 flex items-center gap-4">
<%= check_box_tag "selection_entry", <%= check_box_tag "selection_entry",
class: "checkbox checkbox--light", class: "checkbox checkbox--light",
@ -49,6 +52,16 @@
<p class="col-span-2">category</p> <p class="col-span-2">category</p>
<p class="col-span-2 justify-self-end">amount</p> <p class="col-span-2 justify-self-end">amount</p>
</div> </div>
<% if @transactions.any? %>
<div class="md:hidden text-xs uppercase font-medium text-secondary mb-2 px-2">
<%= check_box_tag "selection_entry",
class: "checkbox checkbox--light mr-2 ml-1",
data: { action: "bulk-select#togglePageSelection" } %>
<span>TRANSACTION</span>
</div>
<% end %>
<div class="space-y-6"> <div class="space-y-6">
<%= entries_by_date(@transactions.map(&:entry), totals: true) do |entries| %> <%= entries_by_date(@transactions.map(&:entry), totals: true) do |entries| %>
<%= render entries %> <%= render entries %>

View file

@ -10,7 +10,7 @@
<div class="flex items-center px-3 py-2 gap-2 border border-secondary rounded-lg focus-within:ring-secondary focus-within:border-secondary"> <div class="flex items-center px-3 py-2 gap-2 border border-secondary rounded-lg focus-within:ring-secondary focus-within:border-secondary">
<%= lucide_icon("search", class: "w-5 h-5 text-secondary") %> <%= lucide_icon("search", class: "w-5 h-5 text-secondary") %>
<%= form.text_field :search, <%= form.text_field :search,
placeholder: "Search transactions by name", placeholder: "Search transactions ...",
value: @q[:search], value: @q[:search],
class: "form-field__input placeholder:text-sm placeholder:text-secondary", class: "form-field__input placeholder:text-sm placeholder:text-secondary",
"data-auto-submit-form-target": "auto" %> "data-auto-submit-form-target": "auto" %>
@ -19,7 +19,7 @@
<div data-controller="menu" class="relative"> <div data-controller="menu" class="relative">
<button id="transaction-filters-button" data-menu-target="button" type="button" class="btn btn--outline flex items-center gap-2"> <button id="transaction-filters-button" data-menu-target="button" type="button" class="btn btn--outline flex items-center gap-2">
<%= lucide_icon("list-filter", class: "w-5 h-5 text-secondary") %> <%= lucide_icon("list-filter", class: "w-5 h-5 text-secondary") %>
<p class="text-sm font-medium text-primary">Filter</p> <p class="text-sm font-medium text-primary md:block hidden">Filter</p>
</button> </button>
<%= render "transactions/searches/menu", form: form %> <%= render "transactions/searches/menu", form: form %>

View file

@ -6,11 +6,11 @@
data-controller="tabs" data-controller="tabs"
data-tabs-active-class="bg-gray-25 text-primary" data-tabs-active-class="bg-gray-25 text-primary"
data-tabs-default-tab-value="<%= get_default_transaction_search_filter[:key] %>" data-tabs-default-tab-value="<%= get_default_transaction_search_filter[:key] %>"
class="hidden absolute flex z-10 h-80 w-[540px] top-12 right-0 shadow-border-xs bg-container rounded-lg"> class="hidden absolute flex flex-col md:flex-row z-10 h-auto md:h-80 w-full md:w-[540px] top-12 right-0 shadow-border-xs bg-container rounded-lg overflow-hidden">
<div class="flex w-44 flex-col items-start p-3 text-sm font-medium text-secondary border-r border-r-alpha-black-100"> <div class="flex w-full md:w-44 flex-row md:flex-col items-start p-3 text-sm font-medium text-secondary border-b md:border-b-0 md:border-r border-alpha-black-100 overflow-x-auto md:overflow-x-visible">
<% transaction_search_filters.each do |filter| %> <% transaction_search_filters.each do |filter| %>
<button <button
class="flex text-secondary hover:bg-container-inset items-center gap-2 px-3 rounded-md py-2 w-full" class="flex text-secondary hover:bg-container-inset items-center gap-2 px-3 rounded-md py-2 min-w-max md:w-full"
type="button" type="button"
data-id="<%= filter[:key] %>" data-id="<%= filter[:key] %>"
data-tabs-target="btn" data-tabs-target="btn"
@ -22,7 +22,7 @@
</div> </div>
<div class="flex flex-col grow"> <div class="flex flex-col grow">
<div class="grow p-3 border-b border-b-alpha-black-100 overflow-y-auto"> <div class="grow p-3 border-b border-b-alpha-black-100 overflow-y-auto max-h-[50vh] md:max-h-none">
<% transaction_search_filters.each do |filter| %> <% transaction_search_filters.each do |filter| %>
<div id="<%= filter[:key] %>" data-tabs-target="tab"> <div id="<%= filter[:key] %>" data-tabs-target="tab">
<%= render partial: get_transaction_search_filter_partial_path(filter), locals: { form: form } %> <%= render partial: get_transaction_search_filter_partial_path(filter), locals: { form: form } %>

View file

@ -7,19 +7,19 @@
<% end %> <% end %>
<section> <section>
<fieldset class="bg-gray-50 rounded-lg p-1 grid grid-flow-col justify-stretch gap-x-2"> <fieldset class="bg-gray-50 rounded-lg p-1 grid grid-flow-col justify-stretch gap-x-1 asdd">
<%= 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 %> <%= 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-5 h-5" %> <%= lucide_icon "minus-circle", class: "w-4 h-4" %>
<%= tag.span t(".expense") %> <%= tag.span t(".expense") %>
<% end %> <% 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 %> <%= 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-5 h-5" %> <%= lucide_icon "plus-circle", class: "w-4 h-4" %>
<%= tag.span t(".income") %> <%= tag.span t(".income") %>
<% end %> <% end %>
<%= tag.div class: "flex px-4 py-1 rounded-lg items-center space-x-2 justify-center text-subdued bg-container text-gray-800 shadow-sm" do %> <%= tag.div class: "flex px-4 py-1 rounded-lg items-center space-x-2 justify-center text-sm md:text-normal text-subdued bg-container text-gray-800 shadow-sm" do %>
<%= lucide_icon "arrow-right-left", class: "w-5 h-5" %> <%= lucide_icon "arrow-right-left", class: "w-4 h-4" %>
<%= tag.span t(".transfer") %> <%= tag.span t(".transfer") %>
<% end %> <% end %>
</fieldset> </fieldset>

View file

@ -7,7 +7,7 @@
</div> </div>
</button> </button>
<div data-menu-target="content" class="hidden absolute w-[276px] z-10 divide-y divide-alpha-black-100 bg-container rounded-xl shadow-border-sm"> <div data-menu-target="content" class="hidden absolute w-[276px] z-100 divide-y divide-alpha-black-100 bg-container rounded-xl shadow-border-sm">
<div class="px-4 py-3 flex items-center gap-3"> <div class="px-4 py-3 flex items-center gap-3">
<div class="w-9 h-9 shrink-0"> <div class="w-9 h-9 shrink-0">
<%= render "settings/user_avatar", user: user, variant: :small, lazy: true %> <%= render "settings/user_avatar", user: user, variant: :small, lazy: true %>

View file

@ -3,6 +3,12 @@
if ! gem list foreman -i --silent; then if ! gem list foreman -i --silent; then
echo "Installing foreman..." echo "Installing foreman..."
gem install foreman gem install foreman
# Add rehash for rbenv users
if command -v rbenv > /dev/null; then
echo "Running rbenv rehash..."
rbenv rehash
fi
fi fi
# Default to port 3000 if not specified # Default to port 3000 if not specified
@ -13,4 +19,4 @@ export PORT="${PORT:-3000}"
export RUBY_DEBUG_OPEN="true" export RUBY_DEBUG_OPEN="true"
export RUBY_DEBUG_LAZY="true" export RUBY_DEBUG_LAZY="true"
exec foreman start -f Procfile.dev "$@" exec bundle exec foreman start -f Procfile.dev "$@"

View file

@ -6,6 +6,8 @@ en:
description: Edit your data in the table below. Red cells are invalid. description: Edit your data in the table below. Red cells are invalid.
errors_notice: You have errors in your data. Hover over the error to see errors_notice: You have errors in your data. Hover over the error to see
details. details.
errors_notice_mobile: You have errors in your data. Tap over the error tooltip to see
details.
title: Clean your data title: Clean your data
configurations: configurations:
mint_import: mint_import:

View file

@ -22,3 +22,4 @@ en:
welcome_body: To get started, you must sign up for a new account. You will welcome_body: To get started, you must sign up for a new account. You will
then be able to configure additional settings within the app. then be able to configure additional settings within the app.
welcome_title: Welcome to Self Hosted Maybe! welcome_title: Welcome to Self Hosted Maybe!
password_placeholder: Enter your password

View file

@ -12,3 +12,4 @@ en:
password: Password password: Password
submit: Log in submit: Log in
title: Sign in to your account title: Sign in to your account
password_placeholder: Enter your password

View file

@ -97,4 +97,6 @@ en:
previous: Back previous: Back
user_avatar_field: user_avatar_field:
accepted_formats: JPG or PNG. 5MB max. accepted_formats: JPG or PNG. 5MB max.
choose: Choose choose: Upload photo
choose_label: (optional)
change: Change photo

View file

@ -195,6 +195,8 @@ Rails.application.routes.draw do
get "service-worker" => "rails/pwa#service_worker", as: :pwa_service_worker get "service-worker" => "rails/pwa#service_worker", as: :pwa_service_worker
get "manifest" => "rails/pwa#manifest", as: :pwa_manifest get "manifest" => "rails/pwa#manifest", as: :pwa_manifest
get "imports/:import_id/upload/sample_csv", to: "import/uploads#sample_csv", as: :import_upload_sample_csv
# Defines the root path route ("/") # Defines the root path route ("/")
root "pages#dashboard" root "pages#dashboard"
end end

BIN
public/logo-pwa.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 220 KiB

View file

@ -15,5 +15,5 @@
], ],
"theme_color": "#ffffff", "theme_color": "#ffffff",
"background_color": "#ffffff", "background_color": "#ffffff",
"display": "standalone" "display": "fullscreen"
} }

View file

@ -9,8 +9,7 @@ class RegistrationsControllerTest < ActionDispatch::IntegrationTest
test "create redirects to correct URL" do test "create redirects to correct URL" do
post registration_url, params: { user: { post registration_url, params: { user: {
email: "john@example.com", email: "john@example.com",
password: "password", password: "Password1!" } }
password_confirmation: "password" } }
assert_redirected_to root_url assert_redirected_to root_url
end end
@ -20,14 +19,12 @@ class RegistrationsControllerTest < ActionDispatch::IntegrationTest
assert_no_difference "User.count" do assert_no_difference "User.count" do
post registration_url, params: { user: { post registration_url, params: { user: {
email: "john@example.com", email: "john@example.com",
password: "password", password: "Password1!" } }
password_confirmation: "password" } }
assert_redirected_to new_registration_url assert_redirected_to new_registration_url
post registration_url, params: { user: { post registration_url, params: { user: {
email: "john@example.com", email: "john@example.com",
password: "password", password: "Password1!",
password_confirmation: "password",
invite_code: "foo" } } invite_code: "foo" } }
assert_redirected_to new_registration_url assert_redirected_to new_registration_url
end end
@ -35,8 +32,7 @@ class RegistrationsControllerTest < ActionDispatch::IntegrationTest
assert_difference "User.count", +1 do assert_difference "User.count", +1 do
post registration_url, params: { user: { post registration_url, params: { user: {
email: "john@example.com", email: "john@example.com",
password: "password", password: "Password1!",
password_confirmation: "password",
invite_code: InviteCode.generate! } } invite_code: InviteCode.generate! } }
assert_redirected_to root_url assert_redirected_to root_url
end end

View file

@ -15,6 +15,8 @@ class ImportsTest < ApplicationSystemTestCase
click_on "Import transactions" click_on "Import transactions"
find("button[data-id='csv-paste-tab']").click
fill_in "import[raw_file_str]", with: file_fixture("imports/transactions.csv").read fill_in "import[raw_file_str]", with: file_fixture("imports/transactions.csv").read
find('input[type="submit"][value="Upload CSV"]').click find('input[type="submit"][value="Upload CSV"]').click
@ -59,6 +61,8 @@ class ImportsTest < ApplicationSystemTestCase
click_on "Import investments" click_on "Import investments"
find("button[data-id='csv-paste-tab']").click
fill_in "import[raw_file_str]", with: file_fixture("imports/trades.csv").read fill_in "import[raw_file_str]", with: file_fixture("imports/trades.csv").read
find('input[type="submit"][value="Upload CSV"]').click find('input[type="submit"][value="Upload CSV"]').click
@ -90,6 +94,8 @@ class ImportsTest < ApplicationSystemTestCase
click_on "Import accounts" click_on "Import accounts"
find("button[data-id='csv-paste-tab']").click
fill_in "import[raw_file_str]", with: file_fixture("imports/accounts.csv").read fill_in "import[raw_file_str]", with: file_fixture("imports/accounts.csv").read
find('input[type="submit"][value="Upload CSV"]').click find('input[type="submit"][value="Upload CSV"]').click
@ -128,6 +134,8 @@ class ImportsTest < ApplicationSystemTestCase
click_on "Import from Mint" click_on "Import from Mint"
find("button[data-id='csv-paste-tab']").click
fill_in "import[raw_file_str]", with: file_fixture("imports/mint.csv").read fill_in "import[raw_file_str]", with: file_fixture("imports/mint.csv").read
find('input[type="submit"][value="Upload CSV"]').click find('input[type="submit"][value="Upload CSV"]').click

View file

@ -33,7 +33,7 @@ class TransactionsTest < ApplicationSystemTestCase
assert_selector "h1", text: "Transactions" assert_selector "h1", text: "Transactions"
within "form#transactions-search" do within "form#transactions-search" do
fill_in "Search transactions by name", with: @transaction.name fill_in "Search transactions ...", with: @transaction.name
end end
assert_selector "#" + dom_id(@transaction), count: 1 assert_selector "#" + dom_id(@transaction), count: 1