mirror of
https://github.com/maybe-finance/maybe.git
synced 2025-07-18 12:49:38 +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:
parent
6a21f26d2d
commit
65e1bc6edd
91 changed files with 1333 additions and 527 deletions
1
Gemfile
1
Gemfile
|
@ -79,6 +79,7 @@ group :development do
|
|||
gem "web-console"
|
||||
gem "faker"
|
||||
gem "benchmark-ips"
|
||||
gem "foreman"
|
||||
end
|
||||
|
||||
group :test do
|
||||
|
|
|
@ -179,6 +179,7 @@ GEM
|
|||
ffi (1.17.1-x86_64-darwin)
|
||||
ffi (1.17.1-x86_64-linux-gnu)
|
||||
ffi (1.17.1-x86_64-linux-musl)
|
||||
foreman (0.88.1)
|
||||
globalid (1.2.1)
|
||||
activesupport (>= 6.1)
|
||||
hashdiff (1.1.2)
|
||||
|
@ -550,6 +551,7 @@ DEPENDENCIES
|
|||
faraday
|
||||
faraday-multipart
|
||||
faraday-retry
|
||||
foreman
|
||||
hotwire-livereload
|
||||
hotwire_combobox
|
||||
i18n-tasks
|
||||
|
|
37
app/assets/images/icon-assistant.svg
Normal file
37
app/assets/images/icon-assistant.svg
Normal 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 |
5
app/assets/images/icon-csv.svg
Normal file
5
app/assets/images/icon-csv.svg
Normal 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 |
|
@ -167,3 +167,15 @@
|
|||
}
|
||||
}
|
||||
/* 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);
|
||||
}
|
|
@ -760,4 +760,12 @@
|
|||
@variant theme-dark {
|
||||
@apply bg-alpha-black-700;
|
||||
}
|
||||
}
|
||||
|
||||
@utility bg-nav-indicator {
|
||||
@apply bg-black;
|
||||
|
||||
@variant theme-dark {
|
||||
@apply bg-white;
|
||||
}
|
||||
}
|
|
@ -6,6 +6,13 @@ class Import::UploadsController < ApplicationController
|
|||
def show
|
||||
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
|
||||
if csv_valid?(csv_str)
|
||||
@import.account = Current.family.accounts.find_by(id: params.dig(:import, :account_id))
|
||||
|
|
|
@ -6,6 +6,7 @@ class RegistrationsController < ApplicationController
|
|||
before_action :set_user, only: :create
|
||||
before_action :set_invitation
|
||||
before_action :claim_invite_code, only: :create, if: :invite_code_required?
|
||||
before_action :validate_password_requirements, only: :create
|
||||
|
||||
def new
|
||||
@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")
|
||||
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
|
||||
|
|
|
@ -5,6 +5,10 @@ module ApplicationHelper
|
|||
render partial: "shared/icon", locals: { key:, size:, color: }
|
||||
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)
|
||||
def hex_with_alpha(hex, alpha)
|
||||
alpha_hex = (alpha * 255).round.to_s(16).rjust(2, "0")
|
||||
|
|
|
@ -10,9 +10,9 @@ module FormsHelper
|
|||
render partial: "shared/modal_form", locals: { title:, subtitle:, content: }
|
||||
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
|
||||
concat radio_tab_contents(label:, icon:)
|
||||
concat radio_tab_contents(label:, icon:, class:)
|
||||
concat form.radio_button(name, value, checked:, disabled:, class: "hidden")
|
||||
end
|
||||
end
|
||||
|
@ -29,8 +29,8 @@ end
|
|||
end
|
||||
|
||||
private
|
||||
def radio_tab_contents(label:, icon:)
|
||||
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
|
||||
def radio_tab_contents(label:, icon:, class: nil)
|
||||
tag.div(class: "flex px-4 py-1 rounded-lg items-center space-x-2 justify-center text-sm md:text-normal text-subdued group-has-checked:bg-container group-has-checked:text-gray-800 group-has-checked:shadow-sm") do
|
||||
concat lucide_icon(icon, class: "w-5 h-5")
|
||||
concat tag.span(label, class: "group-has-checked:font-semibold")
|
||||
end
|
||||
|
|
|
@ -55,6 +55,11 @@ module ImportsHelper
|
|||
[ base, border ].join(" ")
|
||||
end
|
||||
|
||||
def cell_is_valid?(row, field)
|
||||
row.valid? # populate errors
|
||||
!row.errors.key?(field)
|
||||
end
|
||||
|
||||
private
|
||||
def permitted_import_types
|
||||
%w[transaction_import trade_import account_import mint_import]
|
||||
|
|
|
@ -154,200 +154,200 @@ module LanguagesHelper
|
|||
].freeze
|
||||
|
||||
COUNTRY_MAPPING = {
|
||||
AF: "Afghanistan",
|
||||
AL: "Albania",
|
||||
DZ: "Algeria",
|
||||
AD: "Andorra",
|
||||
AO: "Angola",
|
||||
AG: "Antigua and Barbuda",
|
||||
AR: "Argentina",
|
||||
AM: "Armenia",
|
||||
AU: "Australia",
|
||||
AT: "Austria",
|
||||
AZ: "Azerbaijan",
|
||||
BS: "Bahamas",
|
||||
BH: "Bahrain",
|
||||
BD: "Bangladesh",
|
||||
BB: "Barbados",
|
||||
BY: "Belarus",
|
||||
BE: "Belgium",
|
||||
BZ: "Belize",
|
||||
BJ: "Benin",
|
||||
BT: "Bhutan",
|
||||
BO: "Bolivia",
|
||||
BA: "Bosnia and Herzegovina",
|
||||
BW: "Botswana",
|
||||
BR: "Brazil",
|
||||
BN: "Brunei",
|
||||
BG: "Bulgaria",
|
||||
BF: "Burkina Faso",
|
||||
BI: "Burundi",
|
||||
KH: "Cambodia",
|
||||
CM: "Cameroon",
|
||||
CA: "Canada",
|
||||
CV: "Cape Verde",
|
||||
CF: "Central African Republic",
|
||||
TD: "Chad",
|
||||
CL: "Chile",
|
||||
CN: "China",
|
||||
CO: "Colombia",
|
||||
KM: "Comoros",
|
||||
CG: "Congo",
|
||||
CD: "Congo, Democratic Republic of the",
|
||||
CR: "Costa Rica",
|
||||
CI: "Côte d'Ivoire",
|
||||
HR: "Croatia",
|
||||
CU: "Cuba",
|
||||
CY: "Cyprus",
|
||||
CZ: "Czech Republic",
|
||||
DK: "Denmark",
|
||||
DJ: "Djibouti",
|
||||
DM: "Dominica",
|
||||
DO: "Dominican Republic",
|
||||
EC: "Ecuador",
|
||||
EG: "Egypt",
|
||||
SV: "El Salvador",
|
||||
GQ: "Equatorial Guinea",
|
||||
ER: "Eritrea",
|
||||
EE: "Estonia",
|
||||
ET: "Ethiopia",
|
||||
FJ: "Fiji",
|
||||
FI: "Finland",
|
||||
FR: "France",
|
||||
GA: "Gabon",
|
||||
GM: "Gambia",
|
||||
GE: "Georgia",
|
||||
DE: "Germany",
|
||||
GH: "Ghana",
|
||||
GR: "Greece",
|
||||
GD: "Grenada",
|
||||
GT: "Guatemala",
|
||||
GN: "Guinea",
|
||||
GW: "Guinea-Bissau",
|
||||
GY: "Guyana",
|
||||
HT: "Haiti",
|
||||
HN: "Honduras",
|
||||
HU: "Hungary",
|
||||
IS: "Iceland",
|
||||
IN: "India",
|
||||
ID: "Indonesia",
|
||||
IR: "Iran",
|
||||
IQ: "Iraq",
|
||||
IE: "Ireland",
|
||||
IL: "Israel",
|
||||
IT: "Italy",
|
||||
JM: "Jamaica",
|
||||
JP: "Japan",
|
||||
JO: "Jordan",
|
||||
KZ: "Kazakhstan",
|
||||
KE: "Kenya",
|
||||
KI: "Kiribati",
|
||||
KP: "North Korea",
|
||||
KR: "South Korea",
|
||||
KW: "Kuwait",
|
||||
KG: "Kyrgyzstan",
|
||||
LA: "Laos",
|
||||
LV: "Latvia",
|
||||
LB: "Lebanon",
|
||||
LS: "Lesotho",
|
||||
LR: "Liberia",
|
||||
LY: "Libya",
|
||||
LI: "Liechtenstein",
|
||||
LT: "Lithuania",
|
||||
LU: "Luxembourg",
|
||||
MK: "North Macedonia",
|
||||
MG: "Madagascar",
|
||||
MW: "Malawi",
|
||||
MY: "Malaysia",
|
||||
MV: "Maldives",
|
||||
ML: "Mali",
|
||||
MT: "Malta",
|
||||
MH: "Marshall Islands",
|
||||
MR: "Mauritania",
|
||||
MU: "Mauritius",
|
||||
MX: "Mexico",
|
||||
FM: "Micronesia",
|
||||
MD: "Moldova",
|
||||
MC: "Monaco",
|
||||
MN: "Mongolia",
|
||||
ME: "Montenegro",
|
||||
MA: "Morocco",
|
||||
MZ: "Mozambique",
|
||||
MM: "Myanmar",
|
||||
NA: "Namibia",
|
||||
NR: "Nauru",
|
||||
NP: "Nepal",
|
||||
NL: "Netherlands",
|
||||
NZ: "New Zealand",
|
||||
NI: "Nicaragua",
|
||||
NE: "Niger",
|
||||
NG: "Nigeria",
|
||||
NO: "Norway",
|
||||
OM: "Oman",
|
||||
PK: "Pakistan",
|
||||
PW: "Palau",
|
||||
PA: "Panama",
|
||||
PG: "Papua New Guinea",
|
||||
PY: "Paraguay",
|
||||
PE: "Peru",
|
||||
PH: "Philippines",
|
||||
PL: "Poland",
|
||||
PT: "Portugal",
|
||||
QA: "Qatar",
|
||||
RO: "Romania",
|
||||
RU: "Russia",
|
||||
RW: "Rwanda",
|
||||
KN: "Saint Kitts and Nevis",
|
||||
LC: "Saint Lucia",
|
||||
VC: "Saint Vincent and the Grenadines",
|
||||
WS: "Samoa",
|
||||
SM: "San Marino",
|
||||
ST: "Sao Tome and Principe",
|
||||
SA: "Saudi Arabia",
|
||||
SN: "Senegal",
|
||||
RS: "Serbia",
|
||||
SC: "Seychelles",
|
||||
SL: "Sierra Leone",
|
||||
SG: "Singapore",
|
||||
SK: "Slovakia",
|
||||
SI: "Slovenia",
|
||||
SB: "Solomon Islands",
|
||||
SO: "Somalia",
|
||||
ZA: "South Africa",
|
||||
SS: "South Sudan",
|
||||
ES: "Spain",
|
||||
LK: "Sri Lanka",
|
||||
SD: "Sudan",
|
||||
SR: "Suriname",
|
||||
SE: "Sweden",
|
||||
CH: "Switzerland",
|
||||
SY: "Syria",
|
||||
TW: "Taiwan",
|
||||
TJ: "Tajikistan",
|
||||
TZ: "Tanzania",
|
||||
TH: "Thailand",
|
||||
TL: "Timor-Leste",
|
||||
TG: "Togo",
|
||||
TO: "Tonga",
|
||||
TT: "Trinidad and Tobago",
|
||||
TN: "Tunisia",
|
||||
TR: "Turkey",
|
||||
TM: "Turkmenistan",
|
||||
TV: "Tuvalu",
|
||||
UG: "Uganda",
|
||||
UA: "Ukraine",
|
||||
AE: "United Arab Emirates",
|
||||
GB: "United Kingdom",
|
||||
US: "United States",
|
||||
UY: "Uruguay",
|
||||
UZ: "Uzbekistan",
|
||||
VU: "Vanuatu",
|
||||
VA: "Vatican City",
|
||||
VE: "Venezuela",
|
||||
VN: "Vietnam",
|
||||
YE: "Yemen",
|
||||
ZM: "Zambia",
|
||||
ZW: "Zimbabwe"
|
||||
AF: "🇦🇫 Afghanistan",
|
||||
AL: "🇦🇱 Albania",
|
||||
DZ: "🇩🇿 Algeria",
|
||||
AD: "🇦🇩 Andorra",
|
||||
AO: "🇦🇴 Angola",
|
||||
AG: "🇦🇬 Antigua and Barbuda",
|
||||
AR: "🇦🇷 Argentina",
|
||||
AM: "🇦🇲 Armenia",
|
||||
AU: "🇦🇺 Australia",
|
||||
AT: "🇦🇹 Austria",
|
||||
AZ: "🇦🇿 Azerbaijan",
|
||||
BS: "🇧🇸 Bahamas",
|
||||
BH: "🇧🇭 Bahrain",
|
||||
BD: "🇧🇩 Bangladesh",
|
||||
BB: "🇧🇧 Barbados",
|
||||
BY: "🇧🇾 Belarus",
|
||||
BE: "🇧🇪 Belgium",
|
||||
BZ: "🇧🇿 Belize",
|
||||
BJ: "🇧🇯 Benin",
|
||||
BT: "🇧🇹 Bhutan",
|
||||
BO: "🇧🇴 Bolivia",
|
||||
BA: "🇧🇦 Bosnia and Herzegovina",
|
||||
BW: "🇧🇼 Botswana",
|
||||
BR: "🇧🇷 Brazil",
|
||||
BN: "🇧🇳 Brunei",
|
||||
BG: "🇧🇬 Bulgaria",
|
||||
BF: "🇧🇫 Burkina Faso",
|
||||
BI: "🇧🇮 Burundi",
|
||||
KH: "🇰🇭 Cambodia",
|
||||
CM: "🇨🇲 Cameroon",
|
||||
CA: "🇨🇦 Canada",
|
||||
CV: "🇨🇻 Cape Verde",
|
||||
CF: "🇨🇫 Central African Republic",
|
||||
TD: "🇹🇩 Chad",
|
||||
CL: "🇨🇱 Chile",
|
||||
CN: "🇨🇳 China",
|
||||
CO: "🇨🇴 Colombia",
|
||||
KM: "🇰🇲 Comoros",
|
||||
CG: "🇨🇬 Congo",
|
||||
CD: "🇨🇩 Congo, Democratic Republic of the",
|
||||
CR: "🇨🇷 Costa Rica",
|
||||
CI: "🇨🇮 Côte d'Ivoire",
|
||||
HR: "🇭🇷 Croatia",
|
||||
CU: "🇨🇺 Cuba",
|
||||
CY: "🇨🇾 Cyprus",
|
||||
CZ: "🇨🇿 Czech Republic",
|
||||
DK: "🇩🇰 Denmark",
|
||||
DJ: "🇩🇯 Djibouti",
|
||||
DM: "🇩🇲 Dominica",
|
||||
DO: "🇩🇴 Dominican Republic",
|
||||
EC: "🇪🇨 Ecuador",
|
||||
EG: "🇪🇬 Egypt",
|
||||
SV: "🇸🇻 El Salvador",
|
||||
GQ: "🇬🇶 Equatorial Guinea",
|
||||
ER: "🇪🇷 Eritrea",
|
||||
EE: "🇪🇪 Estonia",
|
||||
ET: "🇪🇹 Ethiopia",
|
||||
FJ: "🇫🇯 Fiji",
|
||||
FI: "🇫🇮 Finland",
|
||||
FR: "🇫🇷 France",
|
||||
GA: "🇬🇦 Gabon",
|
||||
GM: "🇬🇲 Gambia",
|
||||
GE: "🇬🇪 Georgia",
|
||||
DE: "🇩🇪 Germany",
|
||||
GH: "🇬🇭 Ghana",
|
||||
GR: "🇬🇷 Greece",
|
||||
GD: "🇬🇩 Grenada",
|
||||
GT: "🇬🇹 Guatemala",
|
||||
GN: "🇬🇳 Guinea",
|
||||
GW: "🇬🇼 Guinea-Bissau",
|
||||
GY: "🇬🇾 Guyana",
|
||||
HT: "🇭🇹 Haiti",
|
||||
HN: "🇭🇳 Honduras",
|
||||
HU: "🇭🇺 Hungary",
|
||||
IS: "🇮🇸 Iceland",
|
||||
IN: "🇮🇳 India",
|
||||
ID: "🇮🇩 Indonesia",
|
||||
IR: "🇮🇷 Iran",
|
||||
IQ: "🇮🇶 Iraq",
|
||||
IE: "🇮🇪 Ireland",
|
||||
IL: "🇮🇱 Israel",
|
||||
IT: "🇮🇹 Italy",
|
||||
JM: "🇯🇲 Jamaica",
|
||||
JP: "🇯🇵 Japan",
|
||||
JO: "🇯🇴 Jordan",
|
||||
KZ: "🇰🇿 Kazakhstan",
|
||||
KE: "🇰🇪 Kenya",
|
||||
KI: "🇰🇮 Kiribati",
|
||||
KP: "🇰🇵 North Korea",
|
||||
KR: "🇰🇷 South Korea",
|
||||
KW: "🇰🇼 Kuwait",
|
||||
KG: "🇰🇬 Kyrgyzstan",
|
||||
LA: "🇱🇦 Laos",
|
||||
LV: "🇱🇻 Latvia",
|
||||
LB: "🇱🇧 Lebanon",
|
||||
LS: "🇱🇸 Lesotho",
|
||||
LR: "🇱🇷 Liberia",
|
||||
LY: "🇱🇾 Libya",
|
||||
LI: "🇱🇮 Liechtenstein",
|
||||
LT: "🇱🇹 Lithuania",
|
||||
LU: "🇱🇺 Luxembourg",
|
||||
MK: "🇲🇰 North Macedonia",
|
||||
MG: "🇲🇬 Madagascar",
|
||||
MW: "🇲🇼 Malawi",
|
||||
MY: "🇲🇾 Malaysia",
|
||||
MV: "🇲🇻 Maldives",
|
||||
ML: "🇲🇱 Mali",
|
||||
MT: "🇲🇹 Malta",
|
||||
MH: "🇲🇭 Marshall Islands",
|
||||
MR: "🇲🇷 Mauritania",
|
||||
MU: "🇲🇺 Mauritius",
|
||||
MX: "🇲🇽 Mexico",
|
||||
FM: "🇫🇲 Micronesia",
|
||||
MD: "🇲🇩 Moldova",
|
||||
MC: "🇲🇨 Monaco",
|
||||
MN: "🇲🇳 Mongolia",
|
||||
ME: "🇲🇪 Montenegro",
|
||||
MA: "🇲🇦 Morocco",
|
||||
MZ: "🇲🇿 Mozambique",
|
||||
MM: "🇲🇲 Myanmar",
|
||||
NA: "🇳🇦 Namibia",
|
||||
NR: "🇳🇷 Nauru",
|
||||
NP: "🇳🇵 Nepal",
|
||||
NL: "🇳🇱 Netherlands",
|
||||
NZ: "🇳🇿 New Zealand",
|
||||
NI: "🇳🇮 Nicaragua",
|
||||
NE: "🇳🇪 Niger",
|
||||
NG: "🇳🇬 Nigeria",
|
||||
NO: "🇳🇴 Norway",
|
||||
OM: "🇴🇲 Oman",
|
||||
PK: "🇵🇰 Pakistan",
|
||||
PW: "🇵🇼 Palau",
|
||||
PA: "🇵🇦 Panama",
|
||||
PG: "🇵🇬 Papua New Guinea",
|
||||
PY: "🇵🇾 Paraguay",
|
||||
PE: "🇵🇪 Peru",
|
||||
PH: "🇵🇭 Philippines",
|
||||
PL: "🇵🇱 Poland",
|
||||
PT: "🇵🇹 Portugal",
|
||||
QA: "🇶🇦 Qatar",
|
||||
RO: "🇷🇴 Romania",
|
||||
RU: "🇷🇺 Russia",
|
||||
RW: "🇷🇼 Rwanda",
|
||||
KN: "🇰🇳 Saint Kitts and Nevis",
|
||||
LC: "🇱🇨 Saint Lucia",
|
||||
VC: "🇻🇨 Saint Vincent and the Grenadines",
|
||||
WS: "🇼🇸 Samoa",
|
||||
SM: "🇸🇲 San Marino",
|
||||
ST: "🇸🇹 Sao Tome and Principe",
|
||||
SA: "🇸🇦 Saudi Arabia",
|
||||
SN: "🇸🇳 Senegal",
|
||||
RS: "🇷🇸 Serbia",
|
||||
SC: "🇸🇨 Seychelles",
|
||||
SL: "🇸🇱 Sierra Leone",
|
||||
SG: "🇸🇬 Singapore",
|
||||
SK: "🇸🇰 Slovakia",
|
||||
SI: "🇸🇮 Slovenia",
|
||||
SB: "🇸🇧 Solomon Islands",
|
||||
SO: "🇸🇴 Somalia",
|
||||
ZA: "🇿🇦 South Africa",
|
||||
SS: "🇸🇸 South Sudan",
|
||||
ES: "🇪🇸 Spain",
|
||||
LK: "🇱🇰 Sri Lanka",
|
||||
SD: "🇸🇩 Sudan",
|
||||
SR: "🇸🇷 Suriname",
|
||||
SE: "🇸🇪 Sweden",
|
||||
CH: "🇨🇭 Switzerland",
|
||||
SY: "🇸🇾 Syria",
|
||||
TW: "🇹🇼 Taiwan",
|
||||
TJ: "🇹🇯 Tajikistan",
|
||||
TZ: "🇹🇿 Tanzania",
|
||||
TH: "🇹🇭 Thailand",
|
||||
TL: "🇹🇱 Timor-Leste",
|
||||
TG: "🇹🇬 Togo",
|
||||
TO: "🇹🇴 Tonga",
|
||||
TT: "🇹🇹 Trinidad and Tobago",
|
||||
TN: "🇹🇳 Tunisia",
|
||||
TR: "🇹🇷 Turkey",
|
||||
TM: "🇹🇲 Turkmenistan",
|
||||
TV: "🇹🇻 Tuvalu",
|
||||
UG: "🇺🇬 Uganda",
|
||||
UA: "🇺🇦 Ukraine",
|
||||
AE: "🇦🇪 United Arab Emirates",
|
||||
GB: "🇬🇧 United Kingdom",
|
||||
US: "🇺🇸 United States",
|
||||
UY: "🇺🇾 Uruguay",
|
||||
UZ: "🇺🇿 Uzbekistan",
|
||||
VU: "🇻🇺 Vanuatu",
|
||||
VA: "🇻🇦 Vatican City",
|
||||
VE: "🇻🇪 Venezuela",
|
||||
VN: "🇻🇳 Vietnam",
|
||||
YE: "🇾🇪 Yemen",
|
||||
ZM: "🇿🇲 Zambia",
|
||||
ZW: "🇿🇼 Zimbabwe"
|
||||
}.freeze
|
||||
|
||||
def country_options
|
||||
|
|
|
@ -6,8 +6,8 @@ module MenusHelper
|
|||
end
|
||||
end
|
||||
|
||||
def contextual_menu_modal_action_item(label, url, icon: "pencil-line", turbo_frame: :modal)
|
||||
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
|
||||
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 #{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(tag.span(label, class: "text-sm"))
|
||||
end
|
||||
|
@ -33,7 +33,8 @@ module MenusHelper
|
|||
private
|
||||
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
|
||||
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
|
||||
|
||||
|
|
|
@ -40,7 +40,17 @@ module SettingsHelper
|
|||
previous_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(next_setting)
|
||||
end
|
||||
|
|
74
app/javascript/controllers/file_upload_controller.js
Normal file
74
app/javascript/controllers/file_upload_controller.js
Normal 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")
|
||||
}
|
||||
}
|
||||
}
|
149
app/javascript/controllers/mobile_cell_interaction_controller.js
Normal file
149
app/javascript/controllers/mobile_cell_interaction_controller.js
Normal 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;
|
||||
}
|
||||
}
|
63
app/javascript/controllers/password_validator_controller.js
Normal file
63
app/javascript/controllers/password_validator_controller.js
Normal 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");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
19
app/javascript/controllers/password_visibility_controller.js
Normal file
19
app/javascript/controllers/password_visibility_controller.js
Normal 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");
|
||||
}
|
||||
}
|
39
app/javascript/controllers/preserve_scroll_controller.js
Normal file
39
app/javascript/controllers/preserve_scroll_controller.js
Normal 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);
|
|
@ -8,6 +8,9 @@ export default class extends Controller {
|
|||
"deleteProfileImage",
|
||||
"input",
|
||||
"clearBtn",
|
||||
"uploadText",
|
||||
"changeText",
|
||||
"cameraIcon"
|
||||
];
|
||||
|
||||
clearFileInput() {
|
||||
|
@ -17,6 +20,12 @@ export default class extends Controller {
|
|||
this.attachedImageTarget.classList.add("hidden");
|
||||
this.previewImageTarget.classList.add("hidden");
|
||||
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) {
|
||||
|
@ -28,7 +37,11 @@ export default class extends Controller {
|
|||
this.previewImageTarget.classList.remove("hidden");
|
||||
this.clearBtnTarget.classList.remove("hidden");
|
||||
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 =
|
||||
URL.createObjectURL(file);
|
||||
}
|
||||
|
|
|
@ -50,7 +50,8 @@ export default class extends Controller {
|
|||
}
|
||||
|
||||
systemPrefersDark() {
|
||||
return window.matchMedia("(prefers-color-scheme: dark)").matches
|
||||
return false
|
||||
// return window.matchMedia("(prefers-color-scheme: dark)").matches
|
||||
}
|
||||
|
||||
handleSystemThemeChange = (event) => {
|
||||
|
|
|
@ -4,17 +4,20 @@
|
|||
<div class="flex items-center gap-2">
|
||||
<%= button_to sync_all_accounts_path,
|
||||
disabled: Current.family.syncing?,
|
||||
class: "btn btn--outline flex items-center gap-2",
|
||||
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 %>
|
||||
<%= lucide_icon "refresh-cw", class: "w-5 h-5" %>
|
||||
<span><%= t(".sync") %></span>
|
||||
<span class="hidden md:inline"><%= t(".sync") %></span>
|
||||
<% end %>
|
||||
|
||||
<%= link_to new_account_path(return_to: accounts_path),
|
||||
data: { turbo_frame: "modal" },
|
||||
class: "btn btn--primary flex items-center gap-1" do %>
|
||||
<%= lucide_icon("plus", class: "w-5 h-5") %>
|
||||
<p class="text-sm font-medium"><%= t(".new_account") %></p>
|
||||
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 %>
|
||||
<div class="flex items-center justify-center w-5 h-5">
|
||||
|
||||
<%= lucide_icon("plus")%>
|
||||
</div>
|
||||
<p class="hidden md:block text-sm font-medium"><%= t(".new_account") %></p>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
<%# locals: (title:, back_path: nil) %>
|
||||
|
||||
<%= modal do %>
|
||||
<div class="flex flex-col w-screen max-w-xl" 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="flex flex-col w-screen max-w-xl relative" data-controller="list-keyboard-navigation">
|
||||
<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 %>
|
||||
<%= 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") %>
|
||||
|
@ -10,6 +10,9 @@
|
|||
<% end %>
|
||||
|
||||
<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 class="p-2">
|
||||
|
@ -19,7 +22,7 @@
|
|||
<%= yield %>
|
||||
</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 items-center space-x-2">
|
||||
<span>Select</span>
|
||||
|
|
|
@ -17,9 +17,11 @@
|
|||
<% end %>
|
||||
|
||||
<% 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") %>
|
||||
<%= tag.span t(".new_transaction"), class: "text-sm" %>
|
||||
</span>
|
||||
<%= tag.span t(".new_transaction"), class: "text-sm md:block" %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</div>
|
||||
|
|
|
@ -21,7 +21,7 @@
|
|||
|
||||
<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 %>
|
||||
<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" %>
|
||||
<% end %>
|
||||
|
||||
|
|
|
@ -5,8 +5,8 @@
|
|||
next_budget: @next_budget,
|
||||
latest_budget: @latest_budget %>
|
||||
|
||||
<div class="flex items-start gap-4">
|
||||
<div class="w-[300px] space-y-4">
|
||||
<div class="flex flex-col items-start gap-4 md:flex-row">
|
||||
<div class="w-full md:w-[300px] space-y-4">
|
||||
<div class="h-[300px] bg-container rounded-xl shadow-border-xs p-8">
|
||||
<% if @budget.available_to_allocate.negative? %>
|
||||
<%= render "budgets/over_allocation_warning", budget: @budget %>
|
||||
|
@ -49,7 +49,7 @@
|
|||
</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">
|
||||
<h2 class="text-lg font-medium">Categories</h2>
|
||||
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
<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>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
<div class="flex items-start gap-2 w-full">
|
||||
<div class="flex items-start w-full">
|
||||
<%= render "chats/ai_avatar" %>
|
||||
|
||||
<div class="max-w-[85%] text-sm space-y-4 text-primary">
|
||||
|
@ -13,11 +13,11 @@
|
|||
|
||||
<% questions = [
|
||||
{
|
||||
icon: "bar-chart-2",
|
||||
icon: "chart-area",
|
||||
text: "Evaluate investment portfolio"
|
||||
},
|
||||
{
|
||||
icon: "credit-card",
|
||||
icon: "wallet-minimal",
|
||||
text: "Show spending insights"
|
||||
},
|
||||
{
|
||||
|
@ -31,7 +31,7 @@
|
|||
<button data-action="chat#submitSampleQuestion"
|
||||
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">
|
||||
<%= icon(question[:icon]) %> <%= question[:text] %>
|
||||
<%= icon(question[:icon], color: "gray") %> <%= question[:text] %>
|
||||
</button>
|
||||
<% end %>
|
||||
</div>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<%= 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">
|
||||
<% 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 %>
|
||||
|
@ -21,11 +21,11 @@
|
|||
</div>
|
||||
<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>
|
||||
<%= 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>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<%= render "messages/chat_form" %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<%= 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 %>
|
||||
|
||||
<div class="mt-auto py-8">
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
<h1 class="sr-only"><%= @chat.title %></h1>
|
||||
|
||||
<div class="flex flex-col h-full">
|
||||
<div class="p-4">
|
||||
<div class="md:p-4">
|
||||
<%= render "chats/chat_nav", chat: @chat %>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -11,25 +11,26 @@
|
|||
</div>
|
||||
|
||||
<% 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">
|
||||
<%= 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>
|
||||
|
||||
<%= 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>
|
||||
<% 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">
|
||||
<%= lucide_icon "alert-triangle", class: "w-4 h-4 text-red-500" %>
|
||||
<p class="text-red-500 text-sm"><%= t(".errors_notice") %></p>
|
||||
<%= lucide_icon "alert-triangle", class: "w-4 h-4 text-red-500 flex-shrink-0" %>
|
||||
<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 class="flex justify-center">
|
||||
<div class="bg-gray-50 rounded-lg inline-flex p-1 space-x-2 text-sm text-primary font-medium">
|
||||
<%= 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 "Error rows", import_clean_path(@import, per_page: params[:per_page], view: "errors"), class: "p-2 rounded-lg #{params[:view] == 'errors' ? 'bg-container' : ''}" %>
|
||||
<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 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 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 flex-1 md:flex-auto text-center #{params[:view] == 'errors' ? 'bg-container' : ''}" %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -37,16 +38,18 @@
|
|||
|
||||
<div class="pb-12">
|
||||
<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">
|
||||
<% @import.column_keys.each do |key| %>
|
||||
<div class="px-5"><%= import_col_label(key) %></div>
|
||||
<% end %>
|
||||
</div>
|
||||
<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| %>
|
||||
<div class="px-5"><%= import_col_label(key) %></div>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<div class="bg-container shadow-border-xs rounded-xl divide-y divide-alpha-black-200">
|
||||
<% @rows.each do |row| %>
|
||||
<%= render "import/rows/form", row: row %>
|
||||
<% end %>
|
||||
<div class="bg-container shadow-border-xs rounded-xl divide-y divide-alpha-black-200">
|
||||
<% @rows.each do |row| %>
|
||||
<%= render "import/rows/form", row: row %>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,45 +1,58 @@
|
|||
<%# locals: (import:, mapping_class:, step_idx:) %>
|
||||
|
||||
|
||||
<% mappings = mapping_class.for_import(import) %>
|
||||
<% is_last_step = step_idx == import.mapping_steps.count - 1 %>
|
||||
|
||||
<% if mapping_class == Import::AccountMapping && import.account.nil? %>
|
||||
<% 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]">
|
||||
<%= 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 } %>
|
||||
</div>
|
||||
<% 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]">
|
||||
<%= 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 } %>
|
||||
</div>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div class="bg-container-inset rounded-xl p-1 space-y-1 w-[650px]">
|
||||
<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(".maybe_mapping_label", mapping: mapping_label(mapping_class)) %></p>
|
||||
<p class="justify-self-end"><%= t(".rows_label") %></p>
|
||||
</div>
|
||||
|
||||
<div class="shadow-border-xs rounded-md divide-y divide-alpha-black-100 text-sm">
|
||||
<% mappings.sort_by(&:key).each do |mapping| %>
|
||||
<div class="px-5 py-3 bg-container first:rounded-tl-xl first:rounded-tr-xl last:rounded-bl-xl last:rounded-br-xl">
|
||||
<%= render partial: "import/mappings/form", locals: { mapping: mapping } %>
|
||||
<div class="w-full max-w-full">
|
||||
<% if mapping_class == Import::AccountMapping && import.account.nil? %>
|
||||
<% if import.requires_account? %>
|
||||
<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" %>
|
||||
<%= 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>
|
||||
<% elsif import.has_unassigned_account? %>
|
||||
<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" %>
|
||||
<%= 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>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
<div class="space-y-4 w-full max-w-full">
|
||||
<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">
|
||||
<p><%= t(".csv_mapping_label", mapping: mapping_label(mapping_class)) %></p>
|
||||
<p><%= t(".maybe_mapping_label", mapping: mapping_label(mapping_class)) %></p>
|
||||
<p class="justify-self-end"><%= t(".rows_label") %></p>
|
||||
</div>
|
||||
|
||||
<div class="shadow-border-xs rounded-md divide-y divide-alpha-black-100 text-sm">
|
||||
<% mappings.sort_by(&:key).each do |mapping| %>
|
||||
<div class="px-5 py-3 bg-container first:rounded-tl-xl first:rounded-tr-xl last:rounded-bl-xl last:rounded-br-xl">
|
||||
<%= render partial: "import/mappings/form", locals: { mapping: mapping } %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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-full md:w-36 flex items-center justify-between gap-2" do %>
|
||||
<span>Next</span>
|
||||
<%= lucide_icon "arrow-right", class: "w-5 h-5" %>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-center">
|
||||
<%= 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 %>
|
||||
<span>Next</span>
|
||||
<%= lucide_icon "arrow-right", class: "w-5 h-5" %>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<%# 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| %>
|
||||
<%= turbo_frame_tag dom_id(row, key), title: row.valid? ? nil : row.errors.full_messages.join(", ") do %>
|
||||
<%= form_with(
|
||||
|
@ -9,18 +9,39 @@
|
|||
url: import_row_path(row.import, row),
|
||||
method: :patch,
|
||||
data: {
|
||||
controller: "auto-submit-form",
|
||||
auto_submit_form_trigger_event_value: "blur"
|
||||
controller: "auto-submit-form mobile-cell-interaction",
|
||||
auto_submit_form_trigger_value: "blur",
|
||||
mobile_cell_interaction_error_value: !cell_is_valid?(row, key) ? row.errors[key].join(", ") : "",
|
||||
}
|
||||
) do |form| %>
|
||||
<%= form.text_field key,
|
||||
"data-auto-submit-form-target": "auto",
|
||||
class: [
|
||||
cell_class(row, key),
|
||||
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" : "",
|
||||
],
|
||||
disabled: row.import.complete? %>
|
||||
<div class="relative">
|
||||
<%= form.text_field key,
|
||||
"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: [
|
||||
cell_class(row, key),
|
||||
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" : "",
|
||||
"focus:outline-none focus:z-10 relative",
|
||||
],
|
||||
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 %>
|
||||
|
|
|
@ -4,18 +4,22 @@
|
|||
|
||||
<%= 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="text-center space-y-2">
|
||||
<h1 class="text-3xl text-primary font-medium"><%= t(".title") %></h1>
|
||||
<p class="text-secondary text-sm"><%= t(".description") %></p>
|
||||
</div>
|
||||
|
||||
<div data-controller="tabs" data-tabs-active-class="bg-container" data-tabs-default-tab-value="csv-paste-tab">
|
||||
<div class="flex justify-center mb-4">
|
||||
<div class="tab-item-active rounded-lg inline-flex p-1 space-x-2 text-sm text-primary font-medium">
|
||||
<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>
|
||||
<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>
|
||||
<div
|
||||
data-controller="tabs"
|
||||
data-tabs-active-class="bg-surface shadow-sm text-primary"
|
||||
data-tabs-inactive-class="text-secondary"
|
||||
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>
|
||||
|
||||
|
@ -35,11 +39,23 @@
|
|||
placeholder: "Paste your CSV file contents here",
|
||||
"data-auto-submit-form-target": "auto" %>
|
||||
<% 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">
|
||||
<%= 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>
|
||||
</label>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<%= form.submit "Upload CSV", disabled: @import.complete? %>
|
||||
|
@ -49,22 +65,12 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-alpha-black-25 rounded-xl p-1 mt-5 mx-auto max-w-7xl">
|
||||
<div class="text-secondary p-2 mb-2">
|
||||
<div class="flex gap-2 mb-2">
|
||||
<%= lucide_icon("info", class: "w-5 h-5 shrink-0") %>
|
||||
<p class="text-sm"><%= t(".instructions_1") %></p>
|
||||
<div class="flex justify-center">
|
||||
|
||||
<span class="text-secondary text-sm">
|
||||
<%= 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
|
||||
</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>
|
||||
|
|
|
@ -8,7 +8,16 @@
|
|||
{ name: "Confirm", path: import_path(import), is_complete: import.complete?, step_number: 5 }
|
||||
].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| %>
|
||||
<li class="flex items-center gap-2 group">
|
||||
<% is_current = request.path == step[:path] %>
|
||||
|
|
|
@ -1,37 +1,47 @@
|
|||
<%# locals: (headers: [], rows: [], caption: nil) %>
|
||||
<div class="overflow-x-auto">
|
||||
<div class="border border-secondary rounded-md shadow-border-xs text-sm bg-container w-full">
|
||||
<div class="grid border-b border-b-alpha-black-200" style="grid-template-columns: repeat(<%= headers.length %>, minmax(0, 1fr))">
|
||||
<% headers.each_with_index do |header, index| %>
|
||||
<div class="
|
||||
bg-container-inset px-3 py-2.5 font-medium whitespace-nowrap overflow-x-auto
|
||||
first:rounded-tl-md last:rounded-tr-md
|
||||
<%= "border-r border-r-alpha-black-200" unless index == headers.length - 1 %>
|
||||
">
|
||||
<%= header %>
|
||||
</div>
|
||||
<% end %>
|
||||
<div class="bg-container-inset rounded-xl overflow-hidden mx-1 md:mx-auto p-3 pl-2 md:pr-2 pr-0">
|
||||
<% if caption %>
|
||||
<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| %>
|
||||
<th class="
|
||||
bg-container-inset px-3 py-2.5 font-medium text-left whitespace-nowrap
|
||||
<%= index == 0 ? 'rounded-tl-md' : '' %>
|
||||
<%= index == headers.length - 1 ? 'rounded-tr-md' : '' %>
|
||||
<%= index < headers.length - 1 ? 'border-r border-r-alpha-black-200' : '' %>
|
||||
">
|
||||
<%= header %>
|
||||
</th>
|
||||
<% end %>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-alpha-black-200">
|
||||
<% rows.each_with_index do |row, row_index| %>
|
||||
<tr>
|
||||
<% row.each_with_index do |(header, value), col_index| %>
|
||||
<td class="
|
||||
px-3 py-2.5 whitespace-nowrap text-left
|
||||
<%= col_index < row.length - 1 ? 'border-r border-r-alpha-black-200' : '' %>
|
||||
<%= !caption && row_index == rows.length - 1 && col_index == 0 ? 'rounded-bl-md' : '' %>
|
||||
<%= !caption && row_index == rows.length - 1 && col_index == row.length - 1 ? 'rounded-br-md' : '' %>
|
||||
">
|
||||
<%= value %>
|
||||
</td>
|
||||
<% end %>
|
||||
</tr>
|
||||
<% end %>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<% 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))">
|
||||
<% row.each_with_index do |(header, value), col_index| %>
|
||||
<div class="
|
||||
px-3 py-2.5 whitespace-nowrap overflow-x-auto flex items-start
|
||||
<%= "border-r border-r-alpha-black-200" unless col_index == row.length - 1 %>
|
||||
<%= "rounded-bl-md" if !caption && row_index == rows.length - 1 && col_index == 0 %>
|
||||
<%= "rounded-br-md" if !caption && row_index == rows.length - 1 && col_index == row.length - 1 %>
|
||||
">
|
||||
<%= value %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<% if caption %>
|
||||
<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>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
<%= render "layouts/shared/htmldoc" do %>
|
||||
<% 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-sidebar-user-id-value="<%= Current.user.id %>"
|
||||
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">
|
||||
|
||||
<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>
|
||||
|
||||
<%# 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 %>
|
||||
<div id="account-sidebar-tabs">
|
||||
<div class="mb-4">
|
||||
|
@ -54,6 +55,7 @@
|
|||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<% end %>
|
||||
|
||||
<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"),
|
||||
|
@ -108,7 +110,7 @@
|
|||
<% end %>
|
||||
</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">
|
||||
<li>
|
||||
<%= render "layouts/sidebar/nav_item", name: "Home", path: root_path, icon_key: "pie-chart" %>
|
||||
|
@ -123,7 +125,7 @@
|
|||
</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>
|
||||
</ul>
|
||||
</nav>
|
||||
|
|
|
@ -3,28 +3,38 @@
|
|||
<div class="flex flex-col h-screen px-6 py-12 bg-surface">
|
||||
<div class="grow flex flex-col justify-center">
|
||||
<div class="sm:mx-auto sm:w-full sm:max-w-md">
|
||||
<div class="flex justify-center mb-6">
|
||||
<div class="flex justify-center mt-2 md:mb-6">
|
||||
<%= image_tag "logo-color.png", class: "w-16 mb-6" %>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<h2 class="text-3xl font-medium text-primary text-center">
|
||||
<%= content_for?(:header_title) ? yield(:header_title).html_safe : t(".your_account") %>
|
||||
</h2>
|
||||
|
||||
<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" %>
|
||||
<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" %>
|
||||
</p>
|
||||
<% 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" %>
|
||||
</p>
|
||||
<% end %>
|
||||
</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 %>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<%= 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">
|
||||
<%= link_to content_for(:previous_path) || imports_path do %>
|
||||
<%= lucide_icon "arrow-left", class: "w-5 h-5 text-secondary" %>
|
||||
|
@ -14,7 +14,11 @@
|
|||
<% end %>
|
||||
</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 %>
|
||||
</main>
|
||||
</div>
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
<%= render "layouts/shared/htmldoc" do %>
|
||||
<div class="flex h-full bg-surface">
|
||||
<div class="p-4 w-96 shrink-0 h-full overflow-y-auto">
|
||||
<div class="md:flex h-full bg-surface">
|
||||
<div class="p-4 w-full md:w-96 shrink-0 md:h-full md:overflow-y-auto">
|
||||
<%= render "settings/settings_nav" %>
|
||||
</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="grow space-y-4 overflow-y-auto -mx-1 px-1 pb-12">
|
||||
<% if content_for?(:breadcrumbs) %>
|
||||
|
@ -14,12 +14,13 @@
|
|||
<% end %>
|
||||
|
||||
<% 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 %>
|
||||
</h1>
|
||||
<% end %>
|
||||
|
||||
<%= yield %>
|
||||
<%= settings_nav_footer_mobile %>
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<%# 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 %>
|
||||
<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") %>
|
||||
|
|
|
@ -12,14 +12,19 @@
|
|||
<%= javascript_importmap_tags %>
|
||||
<%= turbo_refreshes_with method: :morph, scroll: :preserve %>
|
||||
|
||||
<meta name="viewport"
|
||||
content="viewport-fit=cover, width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, viewport-fit=cover" />
|
||||
|
||||
<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-title" content="Maybe">
|
||||
|
||||
<meta name="msapplication-TileColor" 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 %>
|
||||
</head>
|
||||
|
|
|
@ -5,8 +5,8 @@
|
|||
<%= yield :head %>
|
||||
</head>
|
||||
|
||||
<body class="h-full overflow-hidden lg:overflow-auto antialiased">
|
||||
<div class="fixed z-50 bottom-6 left-24 w-80">
|
||||
<body class="h-screen overflow-hidden lg:overflow-auto antialiased h-screen-safe ">
|
||||
<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">
|
||||
<%= render_flash_notifications %>
|
||||
|
||||
|
|
|
@ -1,16 +1,19 @@
|
|||
<%# locals: (name:, path:, icon_key:) %>
|
||||
|
||||
<%= link_to path, class: "space-y-1 lg:py-1 group block" do %>
|
||||
<%# locals: (name:, path:, icon_key:, is_custom: false) %>
|
||||
<%= link_to path, class: "space-y-1 lg:py-1 group block relative" 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">
|
||||
<%= 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(icon_key) %>
|
||||
<% icon_color = page_active?(path) ? "current" : "gray" %>
|
||||
<%= 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 %>
|
||||
</div>
|
||||
|
||||
<div class="grow pl-2">
|
||||
<%= tag.p class: class_names("text-center font-medium text-[11px]", page_active?(path) ? "text-primary" : "text-secondary") do %>
|
||||
<div class="grow flex justify-center">
|
||||
<%= tag.p class: class_names("font-medium text-[11px]", page_active?(path) ? "text-primary" : "text-secondary") do %>
|
||||
<%= name %>
|
||||
<% end %>
|
||||
</div>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<%= 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">
|
||||
<%= link_to content_for(:previous_path) || root_path do %>
|
||||
<%= lucide_icon "arrow-left", class: "w-5 h-5 text-secondary" %>
|
||||
|
|
|
@ -4,32 +4,25 @@
|
|||
<% model = chat && chat.persisted? ? [chat, Message.new] : Chat.new %>
|
||||
|
||||
<%= 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| %>
|
||||
|
||||
<%# In the future, this will be a dropdown with different AI models %>
|
||||
<%= 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,
|
||||
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" },
|
||||
rows: 1 %>
|
||||
|
||||
<div class="flex items-center justify-between gap-1">
|
||||
<div class="flex items-center gap-1">
|
||||
<%# 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>
|
||||
<% 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">
|
||||
<%= icon("arrow-up") %>
|
||||
</button>
|
||||
</div>
|
||||
<button type="submit" class="flex-shrink-0 text-secondary bg-gray-50 rounded-full p-2">
|
||||
<%= lucide_icon("arrow-up", class: "w-4 h-4") %>
|
||||
</button>
|
||||
<% end %>
|
||||
|
||||
<p class="text-xs text-secondary">AI responses are informational only and are not financial advice.</p>
|
||||
<p class="text-xs text-secondary mt-1">AI responses are informational only and are not financial advice.</p>
|
||||
</div>
|
||||
|
|
|
@ -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]" %>
|
||||
<div class="flex items-center gap-2">
|
||||
<%= lucide_icon "log-in", class: "w-5 h-5 shrink-0 text-secondary gap-2" %>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<div class="bg-gray-25 h-screen flex flex-col justify-between">
|
||||
<%= 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 class="space-y-1 mb-6">
|
||||
<h1 class="text-2xl font-medium"><%= t(".title") %></h1>
|
||||
|
|
|
@ -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" %>
|
||||
|
||||
<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 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>
|
||||
</div>
|
||||
|
||||
|
@ -13,13 +13,13 @@
|
|||
<%= form.hidden_field :onboarded_at, value: Time.current if @invitation %>
|
||||
|
||||
<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 %>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-between items-center gap-4 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 :last_name, placeholder: t(".last_name"), label: t(".last_name"), container_class: "bg-container w-1/2", required: true %>
|
||||
<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 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 md:w-1/2 w-full", required: true %>
|
||||
</div>
|
||||
<% unless @invitation %>
|
||||
<div class="space-y-4 mb-4">
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
<%= content_for :page_title, t(".title") %>
|
||||
|
||||
<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="w-1/3">
|
||||
<div class="px-3 flex items-center gap-3">
|
||||
<div class="flex flex-col md:flex-row justify-between gap-4 mb-12 last:mb-0">
|
||||
<div class="w-full md:w-1/3">
|
||||
<div class="md:px-3 flex items-center gap-3">
|
||||
<div class="text-white shrink-0 w-9 h-9">
|
||||
<%= image_tag @release_notes[:avatar], class: "rounded-full w-full h-full object-cover" %>
|
||||
</div>
|
||||
|
@ -13,7 +13,7 @@
|
|||
</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>
|
||||
<%= @release_notes[:body].html_safe %>
|
||||
</div>
|
||||
|
|
|
@ -1,7 +1,17 @@
|
|||
<% content_for :page_header do %>
|
||||
<div class="space-y-1 mb-6">
|
||||
<h1 class="text-3xl font-medium text-primary">Welcome back, <%= Current.user.first_name %></h1>
|
||||
<p class="text-gray-500">Here's what's happening with your finances</p>
|
||||
<div 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>
|
||||
<p class="text-gray-500">Here's what's happening with your finances</p>
|
||||
</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 %>
|
||||
|
||||
|
|
|
@ -25,7 +25,7 @@
|
|||
|
||||
<div class="bg-surface rounded-xl p-1 space-y-1">
|
||||
<div class="px-4 py-2 flex items-center uppercase text-xs font-medium text-secondary">
|
||||
<div>Name</div>
|
||||
<div class="hidden sm:block">Name</div>
|
||||
<div class="ml-auto text-right flex items-center gap-6">
|
||||
<div class="w-24">
|
||||
<p>Weight</p>
|
||||
|
@ -40,7 +40,7 @@
|
|||
<% classification_group.account_groups.each do |account_group| %>
|
||||
<details class="group rounded-lg open:bg-surface font-medium text-sm">
|
||||
<summary class="cursor-pointer p-4 group-open:bg-surface bg-container rounded-lg flex items-center justify-between">
|
||||
<div class="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") %>
|
||||
|
||||
<p><%= account_group.name %></p>
|
||||
|
@ -48,8 +48,18 @@
|
|||
|
||||
<div class="ml-auto flex items-center text-right gap-6">
|
||||
<div class="w-24 flex items-center justify-end gap-2">
|
||||
<%= render partial: "shared/progress_circle", locals: { progress: account_group.weight, color: account_group.color } %>
|
||||
<p><%= number_to_percentage(account_group.weight, precision: 0) %></p>
|
||||
<div class="hidden sm:flex sm:items-center sm:justify-end sm:gap-2">
|
||||
<%= render partial: "shared/progress_circle", locals: { progress: account_group.weight, color: account_group.color } %>
|
||||
<p><%= number_to_percentage(account_group.weight, precision: 0) %></p>
|
||||
</div>
|
||||
<div class="flex sm:hidden items-center gap-2">
|
||||
<div class="flex gap-[3px]">
|
||||
<% 10.times do |i| %>
|
||||
<div class="w-[2px] h-[10px] rounded-lg <%= i < (account_group.weight / 10.0).ceil ? '' : 'opacity-20' %>" style="background-color: <%= account_group.color %>;"></div>
|
||||
<% end %>
|
||||
</div>
|
||||
<p class="text-sm"><%= number_to_percentage(account_group.weight, precision: 2) %></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="w-40">
|
||||
|
@ -60,20 +70,33 @@
|
|||
|
||||
<div>
|
||||
<% account_group.accounts.each_with_index do |account, idx| %>
|
||||
<div class="pl-12 pr-4 py-3 flex items-center justify-between text-sm font-medium">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="pl-4 sm:pl-12 pr-4 py-3 flex items-center justify-between text-sm font-medium">
|
||||
<div class="hidden sm:flex sm:items-center sm:gap-3">
|
||||
<%= render "accounts/logo", account: account, size: "sm", color: account_group.color %>
|
||||
<%= link_to account.name, account_path(account) %>
|
||||
</div>
|
||||
|
||||
<div class="ml-auto flex items-center text-right gap-6">
|
||||
<div class="w-24 flex items-center justify-end gap-2">
|
||||
<%= render partial: "shared/progress_circle", locals: { progress: account.weight, color: account_group.color } %>
|
||||
<p><%= number_to_percentage(account.weight, precision: 0) %></p>
|
||||
<div class="hidden sm:flex sm:items-center sm:justify-end sm:gap-2">
|
||||
<%= render partial: "shared/progress_circle", locals: { progress: account.weight, color: account_group.color } %>
|
||||
<p><%= number_to_percentage(account.weight, precision: 0) %></p>
|
||||
</div>
|
||||
<div class="flex sm:hidden items-center gap-2">
|
||||
<div class="flex gap-[3px]">
|
||||
<% 10.times do |i| %>
|
||||
<div class="w-[2px] h-[10px] rounded-lg <%= i < (account.weight / 10.0).ceil ? '' : 'opacity-20' %>" style="background-color: <%= account_group.color %>;"></div>
|
||||
<% end %>
|
||||
</div>
|
||||
<p class="text-sm"><%= number_to_percentage(account.weight, precision: 2) %></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="w-40">
|
||||
<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>
|
||||
|
|
|
@ -3,26 +3,26 @@
|
|||
<div class="bg-container shadow-border-xs rounded-xl p-4">
|
||||
<h2 class="text-lg font-medium text-primary mb-1">Leave feedback</h2>
|
||||
<p class="text-sm text-secondary mb-4">Let us know if you have any specific feedback. Feel free to include links to videos or screenshots.</p>
|
||||
<div class="flex gap-2">
|
||||
<%= link_to "https://github.com/maybe-finance/maybe/discussions/categories/feature-requests", target: "_blank", rel: "noopener noreferrer", class: "w-1/3 flex flex-col items-center p-4 border border-alpha-black-25 rounded-xl hover:bg-gray-50" do %>
|
||||
<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-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" %>
|
||||
<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 %>
|
||||
<% 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" %>
|
||||
<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 %>
|
||||
<% 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" %>
|
||||
<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 %>
|
||||
|
||||
<%= 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" %>
|
||||
<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 %>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
{
|
||||
"name": "Maybe",
|
||||
"short_name": "Maybe",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/icon.png",
|
||||
|
@ -15,8 +16,9 @@
|
|||
],
|
||||
"start_url": "/",
|
||||
"display": "standalone",
|
||||
"display_override": ["fullscreen", "minimal-ui"],
|
||||
"scope": "/",
|
||||
"description": "Maybe.",
|
||||
"theme_color": "red",
|
||||
"background_color": "red"
|
||||
"theme_color": "#ffffff",
|
||||
"background_color": "#ffffff"
|
||||
}
|
||||
|
|
|
@ -32,11 +32,66 @@
|
|||
placeholder: "you@example.com",
|
||||
label: true,
|
||||
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 %>
|
||||
<%= form.text_field :invite_code, required: "required", label: true, value: params[:invite] %>
|
||||
<% end %>
|
||||
|
||||
<%= 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") %>
|
||||
<% end %>
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
<%= 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.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") %>
|
||||
<% end %>
|
||||
|
|
|
@ -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">
|
||||
<%= 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" %>
|
||||
<span>Back</span>
|
||||
<span class="hidden md:block">Back</span>
|
||||
<% 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
|
||||
<% end %>
|
||||
</div>
|
||||
<nav class="space-y-4">
|
||||
<nav class="space-y-4 hidden md:block">
|
||||
<section class="space-y-2">
|
||||
<div class="flex items-center gap-2 px-3">
|
||||
<h3 class="uppercase text-secondary font-medium text-xs"><%= t(".general_section_title") %></h3>
|
||||
|
@ -86,4 +86,73 @@
|
|||
<% end %>
|
||||
</section>
|
||||
</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>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<%# locals: (name:, path:, icon:) %>
|
||||
|
||||
<%= 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"
|
||||
), aria: { current: ("page" if page_active?(path)) } do %>
|
||||
<%= lucide_icon(icon, class: "w-5 h-5") if icon %>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<%# 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' %>
|
||||
<div class="w-5 h-5 text-secondary">
|
||||
<%= lucide_icon("arrow-left") %>
|
||||
|
@ -15,3 +15,30 @@
|
|||
</div>
|
||||
<% 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 %>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<%# locals: (form:, user:) %>
|
||||
|
||||
<div class="flex 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="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 size-26 md:size-24 rounded-full border-alpha-black-300 border border-dashed">
|
||||
|
||||
<%# 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">
|
||||
|
@ -34,13 +34,23 @@
|
|||
</button>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="md:text-left text-center">
|
||||
<%= 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 %>
|
||||
|
||||
<%= form.label :profile_image, t(".choose"),
|
||||
class: "btn btn--outline inline-block" %>
|
||||
<%= 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.file_field :profile_image,
|
||||
accept: "image/png, image/jpeg",
|
||||
class: "hidden px-3 py-2 bg-gray-50 text-primary rounded-md text-sm font-medium",
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
<% if Current.user.admin? %>
|
||||
<div class="space-y-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="w-2/3">
|
||||
<div class="flex flex-col md:flex-row md:items-center md:justify-between gap-4">
|
||||
<div class="w-full md:w-2/3">
|
||||
<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>
|
||||
</div>
|
||||
<%=
|
||||
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: {
|
||||
title: t("settings.hostings.show.confirm_clear_cache.title"),
|
||||
body: t("settings.hostings.show.confirm_clear_cache.body"),
|
||||
|
|
|
@ -47,7 +47,7 @@
|
|||
|
||||
<%= settings_section title: t(".theme_title"), subtitle: t(".theme_subtitle") do %>
|
||||
<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" %>
|
||||
<div class="text-center">
|
||||
<%= image_tag("light-mode-preview.png", alt: "Light Theme Preview", class: "h-44 mb-4") %>
|
||||
|
|
|
@ -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.
|
||||
</p>
|
||||
<% 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 :last_name, placeholder: t(".last_name"), label: t(".last_name") %>
|
||||
</div>
|
||||
<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>
|
||||
<% end %>
|
||||
|
@ -129,14 +129,14 @@
|
|||
<%= settings_section title: t(".danger_zone_title") do %>
|
||||
<div class="space-y-4">
|
||||
<% if Current.user.admin? %>
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="w-2/3">
|
||||
<div class="flex flex-col md:flex-row md:items-center md:justify-between gap-4">
|
||||
<div class="w-full md:w-2/3">
|
||||
<h3 class="font-medium text-primary"><%= t(".reset_account") %></h3>
|
||||
<p class="text-secondary text-sm"><%= t(".reset_account_warning") %></p>
|
||||
</div>
|
||||
<%=
|
||||
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: {
|
||||
title: t(".confirm_reset.title"),
|
||||
body: t(".confirm_reset.body"),
|
||||
|
@ -146,14 +146,14 @@
|
|||
%>
|
||||
</div>
|
||||
<% end %>
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<div class="flex flex-col md:flex-row md:items-center md:justify-between gap-4">
|
||||
<div class="w-full md:w-2/3">
|
||||
<h3 class="font-medium text-primary"><%= t(".delete_account") %></h3>
|
||||
<p class="text-secondary text-sm"><%= t(".delete_account_warning") %></p>
|
||||
</div>
|
||||
<%=
|
||||
button_to t(".delete_account"), user_path(@user), method: :delete,
|
||||
class: "btn btn--destructive",
|
||||
class: "w-full md:w-auto btn btn--destructive",
|
||||
data: { turbo_confirm: {
|
||||
title: t(".confirm_delete.title"),
|
||||
body: t(".confirm_delete.body"),
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
<%= settings_section title: t(".mfa_title"), subtitle: t(".mfa_description") do %>
|
||||
<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="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" %>
|
||||
|
@ -19,20 +19,22 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<% if Current.user.otp_required? %>
|
||||
<%= button_to t(".disable_mfa"), disable_mfa_path,
|
||||
method: :delete,
|
||||
class: "btn btn--secondary flex items-center gap-1",
|
||||
data: { turbo_confirm: {
|
||||
title: t(".disable_mfa_confirm"),
|
||||
body: t(".disable_mfa_confirm"),
|
||||
accept: t(".disable_mfa"),
|
||||
acceptClass: "w-full bg-red-500 text-white rounded-xl text-center p-[10px] border mb-2"
|
||||
} } %>
|
||||
<% else %>
|
||||
<%= link_to t(".enable_mfa"), new_mfa_path,
|
||||
class: "btn btn--primary flex items-center gap-1" %>
|
||||
<% end %>
|
||||
<div class="mt-4 md:mt-0">
|
||||
<% if Current.user.otp_required? %>
|
||||
<%= button_to t(".disable_mfa"), disable_mfa_path,
|
||||
method: :delete,
|
||||
class: "w-full md:w-auto btn btn--secondary flex items-center gap-1 justify-center",
|
||||
data: { turbo_confirm: {
|
||||
title: t(".disable_mfa_confirm"),
|
||||
body: t(".disable_mfa_confirm"),
|
||||
accept: t(".disable_mfa"),
|
||||
acceptClass: "w-full bg-red-500 text-white rounded-xl text-center p-[10px] border mb-2"
|
||||
} } %>
|
||||
<% else %>
|
||||
<%= link_to t(".enable_mfa"), new_mfa_path,
|
||||
class: "w-full md:w-auto btn btn--primary flex items-center gap-1 justify-center" %>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<%# locals: (content:, reload_on_close: false) %>
|
||||
|
||||
<%= 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-action="mousedown->modal#clickOutside"
|
||||
data-modal-reload-on-close-value="<%= reload_on_close %>">
|
||||
|
|
6
app/views/shared/_icon_custom.html.erb
Normal file
6
app/views/shared/_icon_custom.html.erb
Normal 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") %>
|
6
app/views/shared/_icon_image.html.erb
Normal file
6
app/views/shared/_icon_image.html.erb
Normal 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 )%>
|
|
@ -1,7 +1,7 @@
|
|||
<%# locals: (content:, classes:) -%>
|
||||
<%= 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">
|
||||
<div class="flex flex-col">
|
||||
<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 h-full md:h-auto mt-safe">
|
||||
<%= content %>
|
||||
</div>
|
||||
</dialog>
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
<%# locals: (title:, content:, subtitle: nil) %>
|
||||
|
||||
<%= 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">
|
||||
<header class="flex justify-between items-center">
|
||||
<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>
|
||||
|
||||
<% if subtitle.present? %>
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
<% type = type.to_sym %>
|
||||
<% 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: {
|
||||
controller: "element-removal",
|
||||
action: action
|
||||
|
|
|
@ -5,11 +5,11 @@
|
|||
<% end %>
|
||||
|
||||
<section>
|
||||
<fieldset class="bg-gray-50 rounded-lg p-1 grid grid-flow-col justify-stretch 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: :inflow, label: t(".income"), icon: "plus-circle", checked: params[:nature] == "inflow" %>
|
||||
<%= 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 %>
|
||||
<%= lucide_icon "arrow-right-left", class: "w-5 h-5" %>
|
||||
<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?, class: "text-xs md:text-sm" %>
|
||||
<%= radio_tab_tag form: f, name: :nature, value: :inflow, label: t(".income"), icon: "plus-circle", checked: params[:nature] == "inflow", class: "text-xs md:text-sm" %>
|
||||
<%= link_to new_transfer_path, data: { turbo_frame: :modal }, class: "flex px-4 py-1 rounded-lg items-center space-x-2 justify-center text-subdued text-sm md:text-normal group-has-checked:bg-container group-has-checked:text-gray-800 group-has-checked:shadow-sm group-has-checked:text-sm" do %>
|
||||
<%= lucide_icon "arrow-right-left", class: "w-4 h-4 md:w-5 md:h-5" %>
|
||||
<%= tag.span t(".transfer") %>
|
||||
<% end %>
|
||||
</fieldset>
|
||||
|
|
|
@ -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">
|
||||
<%= check_box_tag "entry_selection", 1, true, class: "checkbox checkbox--dark", data: { action: "bulk-select#deselectAll" } %>
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<%# 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">
|
||||
<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>
|
||||
|
|
|
@ -4,12 +4,12 @@
|
|||
|
||||
<%= turbo_frame_tag dom_id(entry) do %>
|
||||
<%= turbo_frame_tag dom_id(transaction) do %>
|
||||
<div class="grid grid-cols-12 items-center text-primary text-sm font-medium p-4
|
||||
<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 ?
|
||||
"border border-gray-900 rounded-lg" : "" %>">
|
||||
|
||||
<div class="pr-10 flex items-center gap-4
|
||||
<%= balance_trend ? "col-span-6" : "col-span-8" %>">
|
||||
<div class="pr-4 md:pr-10 flex items-center gap-3 md:gap-4
|
||||
<%= balance_trend ? "col-span-8 md:col-span-6" : "col-span-8" %>">
|
||||
<%= check_box_tag dom_id(entry, "selection"),
|
||||
disabled: transaction.transfer?,
|
||||
class: "checkbox checkbox--light",
|
||||
|
@ -55,7 +55,7 @@
|
|||
<% end %>
|
||||
</div>
|
||||
|
||||
<div class="text-secondary text-xs font-normal">
|
||||
<div class="text-secondary text-xs font-normal hidden md:block">
|
||||
<% if transaction.transfer? %>
|
||||
<%= render "transfers/account_links",
|
||||
transfer: transaction.transfer,
|
||||
|
@ -73,18 +73,18 @@
|
|||
</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 %>
|
||||
</div>
|
||||
|
||||
<div class="col-span-2 ml-auto">
|
||||
<div class="col-span-4 md:col-span-2 ml-auto text-right">
|
||||
<%= content_tag :p,
|
||||
transaction.transfer? && view_ctx == "global" ? "+/- #{format_money(entry.amount_money.abs)}" : format_money(-entry.amount_money),
|
||||
class: ["text-green-600": entry.amount.negative?] %>
|
||||
</div>
|
||||
|
||||
<% if balance_trend %>
|
||||
<div class="col-span-2 justify-self-end">
|
||||
<div class="col-span-2 justify-self-end hidden md:block">
|
||||
<% if balance_trend.trend %>
|
||||
<%= tag.p format_money(balance_trend.trend.current),
|
||||
class: "font-medium text-sm text-primary" %>
|
||||
|
|
|
@ -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_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(".import"), new_import_path, icon: "download", turbo_frame: "modal", class_name: "md:!hidden" %>
|
||||
<% 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") %>
|
||||
<p class="text-sm font-medium text-primary"><%= t(".import") %></p>
|
||||
<% end %>
|
||||
|
||||
<%= link_to new_transaction_path, class: "btn btn--primary flex items-center gap-2", data: { turbo_frame: :modal } do %>
|
||||
<%= lucide_icon("plus", class: "w-5 h-5") %>
|
||||
<p class="text-sm font-medium">New transaction</p>
|
||||
<%= 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") %>
|
||||
</span>
|
||||
<p class="text-sm font-medium hidden md:block">New transaction</p>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -38,7 +41,7 @@
|
|||
|
||||
<% if @pagy.count > 0 %>
|
||||
<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">
|
||||
<%= check_box_tag "selection_entry",
|
||||
class: "checkbox checkbox--light",
|
||||
|
@ -49,6 +52,16 @@
|
|||
<p class="col-span-2">category</p>
|
||||
<p class="col-span-2 justify-self-end">amount</p>
|
||||
</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">
|
||||
<%= entries_by_date(@transactions.map(&:entry), totals: true) do |entries| %>
|
||||
<%= render entries %>
|
||||
|
|
|
@ -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">
|
||||
<%= lucide_icon("search", class: "w-5 h-5 text-secondary") %>
|
||||
<%= form.text_field :search,
|
||||
placeholder: "Search transactions by name",
|
||||
placeholder: "Search transactions ...",
|
||||
value: @q[:search],
|
||||
class: "form-field__input placeholder:text-sm placeholder:text-secondary",
|
||||
"data-auto-submit-form-target": "auto" %>
|
||||
|
@ -19,7 +19,7 @@
|
|||
<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">
|
||||
<%= 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>
|
||||
|
||||
<%= render "transactions/searches/menu", form: form %>
|
||||
|
|
|
@ -6,11 +6,11 @@
|
|||
data-controller="tabs"
|
||||
data-tabs-active-class="bg-gray-25 text-primary"
|
||||
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">
|
||||
<div class="flex w-44 flex-col items-start p-3 text-sm font-medium text-secondary border-r border-r-alpha-black-100">
|
||||
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-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| %>
|
||||
<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"
|
||||
data-id="<%= filter[:key] %>"
|
||||
data-tabs-target="btn"
|
||||
|
@ -22,7 +22,7 @@
|
|||
</div>
|
||||
|
||||
<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| %>
|
||||
<div id="<%= filter[:key] %>" data-tabs-target="tab">
|
||||
<%= render partial: get_transaction_search_filter_partial_path(filter), locals: { form: form } %>
|
||||
|
|
|
@ -7,19 +7,19 @@
|
|||
<% end %>
|
||||
|
||||
<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 %>
|
||||
<%= lucide_icon "minus-circle", class: "w-5 h-5" %>
|
||||
<%= lucide_icon "minus-circle", class: "w-4 h-4" %>
|
||||
<%= tag.span t(".expense") %>
|
||||
<% end %>
|
||||
|
||||
<%= link_to new_transaction_path(nature: "income"), data: { turbo_frame: :modal }, class: "flex px-4 py-1 rounded-lg items-center space-x-2 justify-center text-subdued" do %>
|
||||
<%= lucide_icon "plus-circle", class: "w-5 h-5" %>
|
||||
<%= lucide_icon "plus-circle", class: "w-4 h-4" %>
|
||||
<%= tag.span t(".income") %>
|
||||
<% end %>
|
||||
|
||||
<%= tag.div class: "flex px-4 py-1 rounded-lg items-center space-x-2 justify-center text-subdued bg-container text-gray-800 shadow-sm" do %>
|
||||
<%= lucide_icon "arrow-right-left", class: "w-5 h-5" %>
|
||||
<%= 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-4 h-4" %>
|
||||
<%= tag.span t(".transfer") %>
|
||||
<% end %>
|
||||
</fieldset>
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
</div>
|
||||
</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="w-9 h-9 shrink-0">
|
||||
<%= render "settings/user_avatar", user: user, variant: :small, lazy: true %>
|
||||
|
|
8
bin/dev
8
bin/dev
|
@ -3,6 +3,12 @@
|
|||
if ! gem list foreman -i --silent; then
|
||||
echo "Installing foreman..."
|
||||
gem install foreman
|
||||
|
||||
# Add rehash for rbenv users
|
||||
if command -v rbenv > /dev/null; then
|
||||
echo "Running rbenv rehash..."
|
||||
rbenv rehash
|
||||
fi
|
||||
fi
|
||||
|
||||
# Default to port 3000 if not specified
|
||||
|
@ -13,4 +19,4 @@ export PORT="${PORT:-3000}"
|
|||
export RUBY_DEBUG_OPEN="true"
|
||||
export RUBY_DEBUG_LAZY="true"
|
||||
|
||||
exec foreman start -f Procfile.dev "$@"
|
||||
exec bundle exec foreman start -f Procfile.dev "$@"
|
||||
|
|
|
@ -6,6 +6,8 @@ en:
|
|||
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
|
||||
details.
|
||||
errors_notice_mobile: You have errors in your data. Tap over the error tooltip to see
|
||||
details.
|
||||
title: Clean your data
|
||||
configurations:
|
||||
mint_import:
|
||||
|
|
|
@ -22,3 +22,4 @@ en:
|
|||
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.
|
||||
welcome_title: Welcome to Self Hosted Maybe!
|
||||
password_placeholder: Enter your password
|
||||
|
|
|
@ -12,3 +12,4 @@ en:
|
|||
password: Password
|
||||
submit: Log in
|
||||
title: Sign in to your account
|
||||
password_placeholder: Enter your password
|
|
@ -97,4 +97,6 @@ en:
|
|||
previous: Back
|
||||
user_avatar_field:
|
||||
accepted_formats: JPG or PNG. 5MB max.
|
||||
choose: Choose
|
||||
choose: Upload photo
|
||||
choose_label: (optional)
|
||||
change: Change photo
|
||||
|
|
|
@ -195,6 +195,8 @@ Rails.application.routes.draw do
|
|||
get "service-worker" => "rails/pwa#service_worker", as: :pwa_service_worker
|
||||
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 ("/")
|
||||
root "pages#dashboard"
|
||||
end
|
||||
|
|
BIN
public/logo-pwa.png
Normal file
BIN
public/logo-pwa.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 220 KiB |
|
@ -15,5 +15,5 @@
|
|||
],
|
||||
"theme_color": "#ffffff",
|
||||
"background_color": "#ffffff",
|
||||
"display": "standalone"
|
||||
"display": "fullscreen"
|
||||
}
|
||||
|
|
|
@ -9,8 +9,7 @@ class RegistrationsControllerTest < ActionDispatch::IntegrationTest
|
|||
test "create redirects to correct URL" do
|
||||
post registration_url, params: { user: {
|
||||
email: "john@example.com",
|
||||
password: "password",
|
||||
password_confirmation: "password" } }
|
||||
password: "Password1!" } }
|
||||
|
||||
assert_redirected_to root_url
|
||||
end
|
||||
|
@ -20,14 +19,12 @@ class RegistrationsControllerTest < ActionDispatch::IntegrationTest
|
|||
assert_no_difference "User.count" do
|
||||
post registration_url, params: { user: {
|
||||
email: "john@example.com",
|
||||
password: "password",
|
||||
password_confirmation: "password" } }
|
||||
password: "Password1!" } }
|
||||
assert_redirected_to new_registration_url
|
||||
|
||||
post registration_url, params: { user: {
|
||||
email: "john@example.com",
|
||||
password: "password",
|
||||
password_confirmation: "password",
|
||||
password: "Password1!",
|
||||
invite_code: "foo" } }
|
||||
assert_redirected_to new_registration_url
|
||||
end
|
||||
|
@ -35,8 +32,7 @@ class RegistrationsControllerTest < ActionDispatch::IntegrationTest
|
|||
assert_difference "User.count", +1 do
|
||||
post registration_url, params: { user: {
|
||||
email: "john@example.com",
|
||||
password: "password",
|
||||
password_confirmation: "password",
|
||||
password: "Password1!",
|
||||
invite_code: InviteCode.generate! } }
|
||||
assert_redirected_to root_url
|
||||
end
|
||||
|
|
|
@ -15,6 +15,8 @@ class ImportsTest < ApplicationSystemTestCase
|
|||
|
||||
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
|
||||
|
||||
find('input[type="submit"][value="Upload CSV"]').click
|
||||
|
@ -59,6 +61,8 @@ class ImportsTest < ApplicationSystemTestCase
|
|||
|
||||
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
|
||||
|
||||
find('input[type="submit"][value="Upload CSV"]').click
|
||||
|
@ -90,6 +94,8 @@ class ImportsTest < ApplicationSystemTestCase
|
|||
|
||||
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
|
||||
|
||||
find('input[type="submit"][value="Upload CSV"]').click
|
||||
|
@ -128,6 +134,8 @@ class ImportsTest < ApplicationSystemTestCase
|
|||
|
||||
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
|
||||
|
||||
find('input[type="submit"][value="Upload CSV"]').click
|
||||
|
|
|
@ -33,7 +33,7 @@ class TransactionsTest < ApplicationSystemTestCase
|
|||
assert_selector "h1", text: "Transactions"
|
||||
|
||||
within "form#transactions-search" do
|
||||
fill_in "Search transactions by name", with: @transaction.name
|
||||
fill_in "Search transactions ...", with: @transaction.name
|
||||
end
|
||||
|
||||
assert_selector "#" + dom_id(@transaction), count: 1
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue