mirror of
https://github.com/maybe-finance/maybe.git
synced 2025-07-18 20:59:39 +02:00
User Onboarding + Bug Fixes (#1352)
* Bump min supported date to 20 years * Add basic onboarding * User onboarding * Complete onboarding flow * Cleanup, add user profile update test
This commit is contained in:
parent
73e184ad3d
commit
1d20de770f
55 changed files with 1088 additions and 300 deletions
BIN
app/assets/images/logo-color.png
Normal file
BIN
app/assets/images/logo-color.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 9.1 KiB |
|
@ -1,5 +1,5 @@
|
|||
class ApplicationController < ActionController::Base
|
||||
include Localize, AutoSync, Authentication, Invitable, SelfHostable, StoreLocation, Impersonatable
|
||||
include Onboardable, Localize, AutoSync, Authentication, Invitable, SelfHostable, StoreLocation, Impersonatable
|
||||
include Pagy::Backend
|
||||
|
||||
private
|
||||
|
|
17
app/controllers/concerns/onboardable.rb
Normal file
17
app/controllers/concerns/onboardable.rb
Normal file
|
@ -0,0 +1,17 @@
|
|||
module Onboardable
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
before_action :redirect_to_onboarding, if: :needs_onboarding?
|
||||
end
|
||||
|
||||
private
|
||||
def redirect_to_onboarding
|
||||
redirect_to onboarding_path
|
||||
end
|
||||
|
||||
def needs_onboarding?
|
||||
Current.user && Current.user.onboarded_at.blank? &&
|
||||
!%w[/users /onboarding /sessions].any? { |path| request.path.start_with?(path) }
|
||||
end
|
||||
end
|
19
app/controllers/onboardings_controller.rb
Normal file
19
app/controllers/onboardings_controller.rb
Normal file
|
@ -0,0 +1,19 @@
|
|||
class OnboardingsController < ApplicationController
|
||||
layout "application"
|
||||
|
||||
before_action :set_user
|
||||
|
||||
def show
|
||||
end
|
||||
|
||||
def profile
|
||||
end
|
||||
|
||||
def preferences
|
||||
end
|
||||
|
||||
private
|
||||
def set_user
|
||||
@user = Current.user
|
||||
end
|
||||
end
|
|
@ -19,7 +19,7 @@ class SessionsController < ApplicationController
|
|||
|
||||
def destroy
|
||||
@session.destroy
|
||||
redirect_to root_path, notice: t(".logout_successful")
|
||||
redirect_to new_session_path, notice: t(".logout_successful")
|
||||
end
|
||||
|
||||
private
|
||||
|
|
|
@ -1,2 +1,5 @@
|
|||
class Settings::BillingsController < SettingsController
|
||||
def show
|
||||
@user = Current.user
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,26 +1,5 @@
|
|||
class Settings::PreferencesController < SettingsController
|
||||
def edit
|
||||
def show
|
||||
@user = Current.user
|
||||
end
|
||||
|
||||
def update
|
||||
preference_params_with_family = preference_params
|
||||
|
||||
if Current.family && preference_params[:family_attributes]
|
||||
family_attributes = preference_params[:family_attributes].merge({ id: Current.family.id })
|
||||
preference_params_with_family[:family_attributes] = family_attributes
|
||||
end
|
||||
|
||||
if Current.user.update(preference_params_with_family)
|
||||
redirect_to settings_preferences_path, notice: t(".success")
|
||||
else
|
||||
redirect_to settings_preferences_path, notice: t(".success")
|
||||
render :show, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def preference_params
|
||||
params.require(:user).permit(family_attributes: [ :id, :currency, :locale ])
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,38 +1,5 @@
|
|||
class Settings::ProfilesController < SettingsController
|
||||
def show
|
||||
@user = Current.user
|
||||
end
|
||||
|
||||
def update
|
||||
user_params_with_family = user_params
|
||||
|
||||
if params[:user][:delete_profile_image] == "true"
|
||||
Current.user.profile_image.purge
|
||||
end
|
||||
|
||||
if Current.family && user_params_with_family[:family_attributes]
|
||||
family_attributes = user_params_with_family[:family_attributes].merge({ id: Current.family.id })
|
||||
user_params_with_family[:family_attributes] = family_attributes
|
||||
end
|
||||
|
||||
if Current.user.update(user_params_with_family)
|
||||
redirect_to settings_profile_path, notice: t(".success")
|
||||
else
|
||||
redirect_to settings_profile_path, alert: Current.user.errors.full_messages.to_sentence
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
if Current.user.deactivate
|
||||
Current.session.destroy
|
||||
redirect_to root_path, notice: t(".success")
|
||||
else
|
||||
redirect_to settings_profile_path, alert: Current.user.errors.full_messages.to_sentence
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
def user_params
|
||||
params.require(:user).permit(:first_name, :last_name, :profile_image,
|
||||
family_attributes: [ :name, :id ])
|
||||
end
|
||||
end
|
||||
|
|
51
app/controllers/users_controller.rb
Normal file
51
app/controllers/users_controller.rb
Normal file
|
@ -0,0 +1,51 @@
|
|||
class UsersController < ApplicationController
|
||||
before_action :set_user
|
||||
|
||||
def update
|
||||
@user = Current.user
|
||||
|
||||
@user.update!(user_params.except(:redirect_to, :delete_profile_image))
|
||||
@user.profile_image.purge if should_purge_profile_image?
|
||||
|
||||
handle_redirect(t(".success"))
|
||||
end
|
||||
|
||||
def destroy
|
||||
if @user.deactivate
|
||||
Current.session.destroy
|
||||
redirect_to root_path, notice: t(".success")
|
||||
else
|
||||
redirect_to settings_profile_path, alert: @user.errors.full_messages.to_sentence
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
def handle_redirect(notice)
|
||||
case user_params[:redirect_to]
|
||||
when "onboarding_preferences"
|
||||
redirect_to preferences_onboarding_path
|
||||
when "home"
|
||||
redirect_to root_path
|
||||
when "preferences"
|
||||
redirect_to settings_preferences_path, notice: notice
|
||||
else
|
||||
redirect_to settings_profile_path, notice: notice
|
||||
end
|
||||
end
|
||||
|
||||
def should_purge_profile_image?
|
||||
user_params[:delete_profile_image] == "1" &&
|
||||
user_params[:profile_image].blank?
|
||||
end
|
||||
|
||||
def user_params
|
||||
params.require(:user).permit(
|
||||
:first_name, :last_name, :profile_image, :redirect_to, :delete_profile_image, :onboarded_at,
|
||||
family_attributes: [ :name, :currency, :country, :locale, :date_format, :id ]
|
||||
)
|
||||
end
|
||||
|
||||
def set_user
|
||||
@user = Current.user
|
||||
end
|
||||
end
|
|
@ -1,6 +1,19 @@
|
|||
module ApplicationHelper
|
||||
include Pagy::Frontend
|
||||
|
||||
def date_format_options
|
||||
[
|
||||
[ "DD-MM-YYYY", "%d-%m-%Y" ],
|
||||
[ "MM-DD-YYYY", "%m-%d-%Y" ],
|
||||
[ "YYYY-MM-DD", "%Y-%m-%d" ],
|
||||
[ "DD/MM/YYYY", "%d/%m/%Y" ],
|
||||
[ "YYYY/MM/DD", "%Y/%m/%d" ],
|
||||
[ "MM/DD/YYYY", "%m/%d/%Y" ],
|
||||
[ "D/MM/YYYY", "%e/%m/%Y" ],
|
||||
[ "YYYY.MM.DD", "%Y.%m.%d" ]
|
||||
]
|
||||
end
|
||||
|
||||
def title(page_title)
|
||||
content_for(:title) { page_title }
|
||||
end
|
||||
|
@ -132,6 +145,19 @@ module ApplicationHelper
|
|||
end
|
||||
end
|
||||
|
||||
# Wrapper around I18n.l to support custom date formats
|
||||
def format_date(object, format = :default, options = {})
|
||||
date = object.to_date
|
||||
|
||||
format_code = options[:format_code] || Current.family&.date_format
|
||||
|
||||
if format_code.present?
|
||||
date.strftime(format_code)
|
||||
else
|
||||
I18n.l(date, format: format, **options)
|
||||
end
|
||||
end
|
||||
|
||||
def format_money(number_or_money, options = {})
|
||||
return nil unless number_or_money
|
||||
|
||||
|
|
370
app/helpers/languages_helper.rb
Normal file
370
app/helpers/languages_helper.rb
Normal file
|
@ -0,0 +1,370 @@
|
|||
module LanguagesHelper
|
||||
LANGUAGE_MAPPING = {
|
||||
en: "English",
|
||||
ru: "Russian",
|
||||
ar: "Arabic",
|
||||
bg: "Bulgarian",
|
||||
'ca-CAT': "Catalan (Catalonia)",
|
||||
ca: "Catalan",
|
||||
'da-DK': "Danish (Denmark)",
|
||||
'de-AT': "German (Austria)",
|
||||
'de-CH': "German (Switzerland)",
|
||||
de: "German",
|
||||
ee: "Ewe",
|
||||
'en-AU': "English (Australia)",
|
||||
'en-BORK': "English (Bork)",
|
||||
'en-CA': "English (Canada)",
|
||||
'en-GB': "English (United Kingdom)",
|
||||
'en-IND': "English (India)",
|
||||
'en-KE': "English (Kenya)",
|
||||
'en-MS': "English (Malaysia)",
|
||||
'en-NEP': "English (Nepal)",
|
||||
'en-NG': "English (Nigeria)",
|
||||
'en-NZ': "English (New Zealand)",
|
||||
'en-PAK': "English (Pakistan)",
|
||||
'en-SG': "English (Singapore)",
|
||||
'en-TH': "English (Thailand)",
|
||||
'en-UG': "English (Uganda)",
|
||||
'en-US': "English (United States)",
|
||||
'en-ZA': "English (South Africa)",
|
||||
'en-au-ocker': "English (Australian Ocker)",
|
||||
'es-AR': "Spanish (Argentina)",
|
||||
'es-MX': "Spanish (Mexico)",
|
||||
es: "Spanish",
|
||||
fa: "Persian",
|
||||
'fi-FI': "Finnish (Finland)",
|
||||
fr: "French",
|
||||
'fr-CA': "French (Canada)",
|
||||
'fr-CH': "French (Switzerland)",
|
||||
he: "Hebrew",
|
||||
hy: "Armenian",
|
||||
id: "Indonesian",
|
||||
it: "Italian",
|
||||
ja: "Japanese",
|
||||
ko: "Korean",
|
||||
lt: "Lithuanian",
|
||||
lv: "Latvian",
|
||||
'mi-NZ': "Maori (New Zealand)",
|
||||
'nb-NO': "Norwegian Bokmål (Norway)",
|
||||
nl: "Dutch",
|
||||
'no-NO': "Norwegian (Norway)",
|
||||
pl: "Polish",
|
||||
'pt-BR': "Portuguese (Brazil)",
|
||||
pt: "Portuguese",
|
||||
sk: "Slovak",
|
||||
sv: "Swedish",
|
||||
th: "Thai",
|
||||
tr: "Turkish",
|
||||
uk: "Ukrainian",
|
||||
vi: "Vietnamese",
|
||||
'zh-CN': "Chinese (Simplified)",
|
||||
'zh-TW': "Chinese (Traditional)",
|
||||
af: "Afrikaans",
|
||||
az: "Azerbaijani",
|
||||
be: "Belarusian",
|
||||
bn: "Bengali",
|
||||
bs: "Bosnian",
|
||||
cs: "Czech",
|
||||
cy: "Welsh",
|
||||
da: "Danish",
|
||||
'de-DE': "German (Germany)",
|
||||
dz: "Dzongkha",
|
||||
'el-CY': "Greek (Cyprus)",
|
||||
el: "Greek",
|
||||
'en-CY': "English (Cyprus)",
|
||||
'en-IE': "English (Ireland)",
|
||||
'en-IN': "English (India)",
|
||||
'en-TT': "English (Trinidad and Tobago)",
|
||||
eo: "Esperanto",
|
||||
'es-419': "Spanish (Latin America)",
|
||||
'es-CL': "Spanish (Chile)",
|
||||
'es-CO': "Spanish (Colombia)",
|
||||
'es-CR': "Spanish (Costa Rica)",
|
||||
'es-EC': "Spanish (Ecuador)",
|
||||
'es-ES': "Spanish (Spain)",
|
||||
'es-NI': "Spanish (Nicaragua)",
|
||||
'es-PA': "Spanish (Panama)",
|
||||
'es-PE': "Spanish (Peru)",
|
||||
'es-US': "Spanish (United States)",
|
||||
'es-VE': "Spanish (Venezuela)",
|
||||
et: "Estonian",
|
||||
eu: "Basque",
|
||||
fi: "Finnish",
|
||||
'fr-FR': "French (France)",
|
||||
fy: "Western Frisian",
|
||||
gd: "Scottish Gaelic",
|
||||
gl: "Galician",
|
||||
'hi-IN': "Hindi (India)",
|
||||
hi: "Hindi",
|
||||
hr: "Croatian",
|
||||
hu: "Hungarian",
|
||||
is: "Icelandic",
|
||||
'it-CH': "Italian (Switzerland)",
|
||||
ka: "Georgian",
|
||||
kk: "Kazakh",
|
||||
km: "Khmer",
|
||||
kn: "Kannada",
|
||||
lb: "Luxembourgish",
|
||||
lo: "Lao",
|
||||
mg: "Malagasy",
|
||||
mk: "Macedonian",
|
||||
ml: "Malayalam",
|
||||
mn: "Mongolian",
|
||||
'mr-IN': "Marathi (India)",
|
||||
ms: "Malay",
|
||||
nb: "Norwegian Bokmål",
|
||||
ne: "Nepali",
|
||||
nn: "Norwegian Nynorsk",
|
||||
oc: "Occitan",
|
||||
or: "Odia",
|
||||
pa: "Punjabi",
|
||||
rm: "Romansh",
|
||||
ro: "Romanian",
|
||||
sc: "Sardinian",
|
||||
sl: "Slovenian",
|
||||
sq: "Albanian",
|
||||
sr: "Serbian",
|
||||
st: "Southern Sotho",
|
||||
'sv-FI': "Swedish (Finland)",
|
||||
'sv-SE': "Swedish (Sweden)",
|
||||
sw: "Swahili",
|
||||
ta: "Tamil",
|
||||
te: "Telugu",
|
||||
tl: "Tagalog",
|
||||
tt: "Tatar",
|
||||
ug: "Uyghur",
|
||||
ur: "Urdu",
|
||||
uz: "Uzbek",
|
||||
wo: "Wolof"
|
||||
}.freeze
|
||||
|
||||
# Locales that we don't have files for, but which are available in Rails
|
||||
EXCLUDED_LOCALES = [
|
||||
"en-BORK",
|
||||
"en-au-ocker",
|
||||
"ca-CAT",
|
||||
"da-DK",
|
||||
"de-AT",
|
||||
"de-CH",
|
||||
"ee",
|
||||
"en-IND",
|
||||
"en-KE",
|
||||
"en-MS",
|
||||
"en-NEP",
|
||||
"en-NG",
|
||||
"en-PAK",
|
||||
"en-SG",
|
||||
"en-TH",
|
||||
"en-UG"
|
||||
].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"
|
||||
}.freeze
|
||||
|
||||
def country_options
|
||||
COUNTRY_MAPPING.keys.map { |key| [ COUNTRY_MAPPING[key], key ] }
|
||||
end
|
||||
|
||||
def language_options
|
||||
I18n.available_locales
|
||||
.reject { |locale| EXCLUDED_LOCALES.include?(locale.to_s) }
|
||||
.map do |locale|
|
||||
label = LANGUAGE_MAPPING[locale.to_sym] || locale.to_s.humanize
|
||||
[ "#{label} (#{locale})", locale ]
|
||||
end
|
||||
end
|
||||
end
|
|
@ -24,7 +24,7 @@ class StyledFormBuilder < ActionView::Helpers::FormBuilder
|
|||
def select(method, choices, options = {}, html_options = {})
|
||||
merged_html_options = { class: "form-field__input" }.merge(html_options)
|
||||
|
||||
label = build_label(method, options)
|
||||
label = build_label(method, options.merge(required: merged_html_options[:required]))
|
||||
field = super(method, choices, options, merged_html_options)
|
||||
|
||||
build_styled_field(label, field, options, remove_padding_right: true)
|
||||
|
@ -33,7 +33,7 @@ class StyledFormBuilder < ActionView::Helpers::FormBuilder
|
|||
def collection_select(method, collection, value_method, text_method, options = {}, html_options = {})
|
||||
merged_html_options = { class: "form-field__input" }.merge(html_options)
|
||||
|
||||
label = build_label(method, options)
|
||||
label = build_label(method, options.merge(required: merged_html_options[:required]))
|
||||
field = super(method, collection, value_method, text_method, options, merged_html_options)
|
||||
|
||||
build_styled_field(label, field, options, remove_padding_right: true)
|
||||
|
@ -68,7 +68,17 @@ class StyledFormBuilder < ActionView::Helpers::FormBuilder
|
|||
|
||||
def build_label(method, options)
|
||||
return "".html_safe unless options[:label]
|
||||
return label(method, class: "form-field__label") if options[:label] == true
|
||||
label(method, options[:label], class: "form-field__label")
|
||||
|
||||
label_text = options[:label]
|
||||
|
||||
if options[:required]
|
||||
label_text = @template.safe_join([
|
||||
label_text == true ? method.to_s.humanize : label_text,
|
||||
@template.tag.span("*", class: "text-red-500 ml-0.5")
|
||||
])
|
||||
end
|
||||
|
||||
return label(method, class: "form-field__label") if label_text == true
|
||||
label(method, label_text, class: "form-field__label")
|
||||
end
|
||||
end
|
||||
|
|
29
app/javascript/controllers/onboarding_controller.js
Normal file
29
app/javascript/controllers/onboarding_controller.js
Normal file
|
@ -0,0 +1,29 @@
|
|||
import { Controller } from "@hotwired/stimulus";
|
||||
|
||||
// Connects to data-controller="onboarding"
|
||||
export default class extends Controller {
|
||||
setLocale(event) {
|
||||
this.refreshWithParam("locale", event.target.value);
|
||||
}
|
||||
|
||||
setDateFormat(event) {
|
||||
this.refreshWithParam("date_format", event.target.value);
|
||||
}
|
||||
|
||||
setCurrency(event) {
|
||||
this.refreshWithParam("currency", event.target.value);
|
||||
}
|
||||
|
||||
refreshWithParam(key, value) {
|
||||
const url = new URL(window.location);
|
||||
url.searchParams.set(key, value);
|
||||
|
||||
// Preserve existing params by getting the current search string
|
||||
// and appending our new param to it
|
||||
const currentParams = new URLSearchParams(window.location.search);
|
||||
currentParams.set(key, value);
|
||||
|
||||
// Refresh the page with all params
|
||||
window.location.search = currentParams.toString();
|
||||
}
|
||||
}
|
|
@ -2,32 +2,34 @@ import { Controller } from "@hotwired/stimulus";
|
|||
|
||||
export default class extends Controller {
|
||||
static targets = [
|
||||
"imagePreview",
|
||||
"fileField",
|
||||
"deleteField",
|
||||
"attachedImage",
|
||||
"previewImage",
|
||||
"placeholderImage",
|
||||
"deleteProfileImage",
|
||||
"input",
|
||||
"clearBtn",
|
||||
"template",
|
||||
];
|
||||
|
||||
preview(event) {
|
||||
const file = event.target.files[0];
|
||||
if (file) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
this.imagePreviewTarget.innerHTML = `<img src="${e.target.result}" alt="Preview" class="w-full h-full rounded-full object-cover" />`;
|
||||
this.templateTarget.classList.add("hidden");
|
||||
this.clearBtnTarget.classList.remove("hidden");
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
clearFileInput() {
|
||||
this.inputTarget.value = null;
|
||||
this.clearBtnTarget.classList.add("hidden");
|
||||
this.placeholderImageTarget.classList.remove("hidden");
|
||||
this.attachedImageTarget.classList.add("hidden");
|
||||
this.previewImageTarget.classList.add("hidden");
|
||||
this.deleteProfileImageTarget.value = "1";
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.deleteFieldTarget.value = true;
|
||||
this.fileFieldTarget.value = null;
|
||||
this.templateTarget.classList.remove("hidden");
|
||||
this.imagePreviewTarget.innerHTML = this.templateTarget.innerHTML;
|
||||
this.clearBtnTarget.classList.add("hidden");
|
||||
this.element.submit();
|
||||
showFileInputPreview(event) {
|
||||
const file = event.target.files[0];
|
||||
if (!file) return;
|
||||
|
||||
this.placeholderImageTarget.classList.add("hidden");
|
||||
this.attachedImageTarget.classList.add("hidden");
|
||||
this.previewImageTarget.classList.remove("hidden");
|
||||
this.clearBtnTarget.classList.remove("hidden");
|
||||
this.deleteProfileImageTarget.value = "0";
|
||||
|
||||
this.previewImageTarget.querySelector("img").src =
|
||||
URL.createObjectURL(file);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -67,7 +67,7 @@ class Account::Entry < ApplicationRecord
|
|||
class << self
|
||||
# arbitrary cutoff date to avoid expensive sync operations
|
||||
def min_supported_date
|
||||
10.years.ago.to_date
|
||||
20.years.ago.to_date
|
||||
end
|
||||
|
||||
def daily_totals(entries, currency, period: Period.last_30_days)
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
class Family < ApplicationRecord
|
||||
DATE_FORMATS = [ "%m-%d-%Y", "%d-%m-%Y", "%Y-%m-%d", "%d/%m/%Y", "%Y/%m/%d", "%m/%d/%Y", "%e/%m/%Y", "%Y.%m.%d" ]
|
||||
|
||||
include Providable
|
||||
|
||||
has_many :users, dependent: :destroy
|
||||
|
@ -13,6 +15,7 @@ class Family < ApplicationRecord
|
|||
has_many :issues, through: :accounts
|
||||
|
||||
validates :locale, inclusion: { in: I18n.available_locales.map(&:to_s) }
|
||||
validates :date_format, inclusion: { in: DATE_FORMATS }
|
||||
|
||||
def snapshot(period = Period.all)
|
||||
query = accounts.active.joins(:balances)
|
||||
|
|
|
@ -5,7 +5,7 @@ class User < ApplicationRecord
|
|||
has_many :sessions, dependent: :destroy
|
||||
has_many :impersonator_support_sessions, class_name: "ImpersonationSession", foreign_key: :impersonator_id, dependent: :destroy
|
||||
has_many :impersonated_support_sessions, class_name: "ImpersonationSession", foreign_key: :impersonated_id, dependent: :destroy
|
||||
accepts_nested_attributes_for :family
|
||||
accepts_nested_attributes_for :family, update_only: true
|
||||
|
||||
validates :email, presence: true, uniqueness: true
|
||||
validate :ensure_valid_profile_image
|
||||
|
|
7
app/views/layouts/_footer.html.erb
Normal file
7
app/views/layouts/_footer.html.erb
Normal file
|
@ -0,0 +1,7 @@
|
|||
|
||||
<footer class="p-6">
|
||||
<div class="space-y-2 text-center text-xs text-gray-500">
|
||||
<p>© <%= Date.current.year %>, Maybe Finance, Inc.</p>
|
||||
<p><%= link_to t(".privacy_policy"), "https://maybe.co/privacy", class: "underline hover:text-gray-600" %> • <%= link_to t(".terms_of_service"), "https://maybe.co/tos", class: "underline hover:text-gray-600" %></p>
|
||||
</div>
|
||||
</footer>
|
|
@ -4,24 +4,16 @@
|
|||
<% end %>
|
||||
<div id="user-menu" data-controller="menu">
|
||||
<button data-menu-target="button">
|
||||
<% profile_image_attached = Current.user.profile_image.attached? %>
|
||||
<% if profile_image_attached %>
|
||||
<div class="text-white w-9 h-9">
|
||||
<%= render "shared/user_profile_image", user: Current.user %>
|
||||
</div>
|
||||
<% else %>
|
||||
<div class="text-white w-9 h-9 bg-gray-400 rounded-full flex items-center justify-center text-lg uppercase"><%= Current.user.initial %></div>
|
||||
<% end %>
|
||||
<div class="w-9 h-9">
|
||||
<%= render "settings/user_avatar", user: Current.user %>
|
||||
</div>
|
||||
</button>
|
||||
<div data-menu-target="content" class="hidden absolute w-[240px] z-10 left-[255px] top-[72px] bg-white rounded-sm shadow-xs border border-alpha-black-25">
|
||||
<div class="p-3 flex items-center gap-3">
|
||||
<% if profile_image_attached %>
|
||||
<div class="text-white shrink-0 w-9 h-9">
|
||||
<%= render "shared/user_profile_image", user: Current.user %>
|
||||
</div>
|
||||
<% else %>
|
||||
<div class="text-white shrink-0 w-9 h-9 bg-gray-400 rounded-full flex items-center justify-center text-lg uppercase"><%= Current.user.initial %></div>
|
||||
<% end %>
|
||||
<div class="w-9 h-9 shrink-0">
|
||||
<%= render "settings/user_avatar", user: Current.user %>
|
||||
</div>
|
||||
|
||||
<div class="overflow-hidden text-ellipsis">
|
||||
<span class="text-gray-900 font-medium text-sm"><%= Current.user.display_name %></span>
|
||||
<% if Current.user.display_name != Current.user.email %>
|
||||
|
|
|
@ -44,7 +44,7 @@
|
|||
|
||||
<%= render "shared/confirm_modal" %>
|
||||
|
||||
<% if self_hosted? %>
|
||||
<% if self_hosted? && Current.user&.onboarded_at.present? %>
|
||||
<%= render "shared/app_version" %>
|
||||
<% end %>
|
||||
</body>
|
||||
|
|
|
@ -1,30 +1,34 @@
|
|||
<%= content_for :content do %>
|
||||
<div class="flex flex-col justify-center min-h-full px-6 py-12">
|
||||
<div class="sm:mx-auto sm:w-full sm:max-w-md">
|
||||
<%= render "shared/logo" %>
|
||||
<div class="flex flex-col h-screen px-6 py-12 bg-gray-25">
|
||||
<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">
|
||||
<%= image_tag "logo-color.png", class: "w-16 mb-6" %>
|
||||
</div>
|
||||
|
||||
<h2 class="mt-6 text-3xl font-semibold tracking-tight text-center font-display">
|
||||
<%= content_for?(:header_title) ? yield(:header_title).html_safe : t(".your_account") %>
|
||||
</h2>
|
||||
<div class="space-y-2">
|
||||
<h2 class="text-3xl font-medium text-gray-900 text-center">
|
||||
<%= content_for?(:header_title) ? yield(:header_title).html_safe : t(".your_account") %>
|
||||
</h2>
|
||||
|
||||
<% if controller_name == "sessions" %>
|
||||
<p class="mt-2 text-sm text-center text-gray-600">
|
||||
<%= t(".or") %> <%= link_to t(".sign_up"), new_registration_path, class: "font-medium text-gray-600 hover:text-gray-400 transition" %>
|
||||
</p>
|
||||
<% elsif controller_name == "registrations" %>
|
||||
<p class="mt-2 text-sm text-center text-gray-600">
|
||||
<%= t(".or") %> <%= link_to t(".sign_in"), new_session_path, class: "font-medium text-gray-600 hover:text-gray-400 transition" %>
|
||||
</p>
|
||||
<% end %>
|
||||
<% if controller_name == "sessions" %>
|
||||
<p class="text-sm text-center">
|
||||
<%= tag.span t(".no_account"), class: "text-gray-500" %> <%= link_to t(".sign_up"), new_registration_path, class: "font-medium text-gray-900 hover:underline transition" %>
|
||||
</p>
|
||||
<% elsif controller_name == "registrations" %>
|
||||
<p class="text-sm text-center text-gray-600">
|
||||
<%= t(".existing_account") %> <%= link_to t(".sign_in"), new_session_path, class: "font-medium text-gray-900 hover:underline transition" %>
|
||||
</p>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-8 sm:mx-auto sm:w-full sm:max-w-lg">
|
||||
<%= yield %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-8 sm:mx-auto sm:w-full sm:max-w-lg">
|
||||
<%= yield %>
|
||||
</div>
|
||||
|
||||
<div class="p-8 mt-2 text-center">
|
||||
<p class="mt-6 text-sm text-black"><%= link_to t(".privacy_policy"), "https://maybe.co/privacy", class: "font-medium text-gray-600 hover:text-gray-400 transition" %> • <%= link_to t(".terms_of_service"), "https://maybe.co/tos", class: "font-medium text-gray-600 hover:text-gray-400 transition" %></p>
|
||||
</div>
|
||||
<%= render "layouts/footer" %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
|
|
8
app/views/onboardings/_header.html.erb
Normal file
8
app/views/onboardings/_header.html.erb
Normal file
|
@ -0,0 +1,8 @@
|
|||
|
||||
<header class="flex justify-between items-center p-4">
|
||||
<%= 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-gray-500 gap-2" %>
|
||||
<%= button_to t(".sign_out"), session_path(Current.session), method: :delete, class: "text-sm text-gray-900 font-medium" %>
|
||||
</div>
|
||||
</header>
|
88
app/views/onboardings/preferences.html.erb
Normal file
88
app/views/onboardings/preferences.html.erb
Normal file
|
@ -0,0 +1,88 @@
|
|||
<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>
|
||||
<div class="space-y-1 mb-6">
|
||||
<h1 class="text-2xl font-medium"><%= t(".title") %></h1>
|
||||
<p class="text-gray-500 text-sm"><%= t(".subtitle") %></p>
|
||||
</div>
|
||||
|
||||
<div class="p-1 mb-2">
|
||||
<div class="bg-white p-4 rounded-lg flex gap-8" style="box-shadow: 0px 0px 0px 1px rgba(11, 11, 11, 0.05), 0px 1px 2px 0px rgba(11, 11, 11, 0.05);">
|
||||
<div class="space-y-2">
|
||||
<%= tag.p t(".example"), class: "text-gray-500 text-sm" %>
|
||||
<%= tag.p "$2,323.25", class: "text-gray-900 font-medium text-2xl" %>
|
||||
<p class="text-sm">
|
||||
<span class="text-green-500 font-medium">+<%= format_money(Money.new(78.90, params[:currency] || @user.family.currency)) %></span>
|
||||
<span class="text-green-500 font-medium">(+<%= format_money(Money.new(6.39, params[:currency] || @user.family.currency)) %>)</span>
|
||||
<span class="text-gray-500">as of <%= format_date(Date.parse("2024-10-23"), :default, format_code: params[:date_format] || @user.family.date_format) %></span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<% placeholder_series_data = [
|
||||
{ date: Date.current - 14.days, value: 200 },
|
||||
{ date: Date.current - 13.days, value: 200 },
|
||||
{ date: Date.current - 12.days, value: 220 },
|
||||
{ date: Date.current - 11.days, value: 220 },
|
||||
{ date: Date.current - 10.days, value: 220 },
|
||||
{ date: Date.current - 9.days, value: 220 },
|
||||
{ date: Date.current - 8.days, value: 220 },
|
||||
{ date: Date.current - 7.days, value: 220 },
|
||||
{ date: Date.current - 6.days, value: 230 },
|
||||
{ date: Date.current - 5.days, value: 230 },
|
||||
{ date: Date.current - 4.days, value: 250 },
|
||||
{ date: Date.current - 3.days, value: 250 },
|
||||
{ date: Date.current - 2.days, value: 265 },
|
||||
{ date: Date.current - 1.day, value: 265 },
|
||||
{ date: Date.current, value: 265 }
|
||||
] %>
|
||||
|
||||
<div class="flex items-center w-2/5">
|
||||
<div class="h-12 w-full">
|
||||
<div
|
||||
id="previewChart"
|
||||
class="h-full w-full"
|
||||
data-controller="time-series-chart"
|
||||
data-time-series-chart-data-value="<%= TimeSeries.new(placeholder_series_data).to_json %>"
|
||||
data-time-series-chart-use-labels-value="false"
|
||||
data-time-series-chart-use-tooltip-value="false"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="text-gray-500 text-xs mb-4"><%= t(".preview") %></p>
|
||||
|
||||
<%= styled_form_with model: @user do |form| %>
|
||||
<%= form.hidden_field :onboarded_at, value: Time.current %>
|
||||
<%= form.hidden_field :redirect_to, value: "home" %>
|
||||
|
||||
<div class="space-y-4 mb-4">
|
||||
<%= form.fields_for :family do |family_form| %>
|
||||
|
||||
<%= family_form.select :locale,
|
||||
language_options,
|
||||
{ label: t(".locale"), required: true, selected: params[:locale] || @user.family.locale },
|
||||
{ data: { action: "onboarding#setLocale" } } %>
|
||||
|
||||
<%= family_form.select :currency,
|
||||
currencies_for_select.map { |currency| [ "#{currency.name} (#{currency.iso_code})", currency.iso_code ] },
|
||||
{ label: t(".currency"), required: true, selected: params[:currency] || @user.family.currency },
|
||||
{ data: { action: "onboarding#setCurrency" } } %>
|
||||
|
||||
<%= family_form.select :date_format,
|
||||
date_format_options,
|
||||
{ label: t(".date_format"), required: true, selected: params[:date_format] || @user.family.date_format },
|
||||
{ data: { action: "onboarding#setDateFormat" } } %>
|
||||
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<%= form.submit t(".submit") %>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<%= render "layouts/footer" %>
|
||||
</div>
|
40
app/views/onboardings/profile.html.erb
Normal file
40
app/views/onboardings/profile.html.erb
Normal file
|
@ -0,0 +1,40 @@
|
|||
<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">
|
||||
<div>
|
||||
<div class="space-y-1 mb-6">
|
||||
<h1 class="text-2xl font-medium"><%= t(".title") %></h1>
|
||||
<p class="text-gray-500 text-sm"><%= t(".subtitle") %></p>
|
||||
</div>
|
||||
|
||||
<%= styled_form_with model: @user do |form| %>
|
||||
<%= form.hidden_field :redirect_to, value: "onboarding_preferences" %>
|
||||
|
||||
<div class="space-y-4 mb-4">
|
||||
<p class="text-gray-500 text-xs"><%= 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-white w-1/2", required: true %>
|
||||
<%= form.text_field :last_name, placeholder: t(".last_name"), label: t(".last_name"), container_class: "bg-white w-1/2", required: true %>
|
||||
</div>
|
||||
|
||||
<div class="space-y-4 mb-4">
|
||||
<%= form.fields_for :family do |family_form| %>
|
||||
<%= family_form.text_field :name, placeholder: t(".household_name"), label: t(".household_name") %>
|
||||
|
||||
<%= family_form.select :country,
|
||||
country_options,
|
||||
{ label: t(".country") }, required: true %>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<%= form.submit t(".submit") %>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<%= render "layouts/footer" %>
|
||||
</div>
|
11
app/views/onboardings/show.html.erb
Normal file
11
app/views/onboardings/show.html.erb
Normal file
|
@ -0,0 +1,11 @@
|
|||
<div class="bg-gray-25">
|
||||
<div class="h-screen flex flex-col items-center py-6">
|
||||
<div class="grow flex justify-center items-center flex-col max-w-sm w-full text-center">
|
||||
<%= image_tag "logo-color.png", class: "w-16 mb-6" %>
|
||||
<%= tag.h1 t(".title"), class: "text-3xl font-medium mb-2" %>
|
||||
<%= tag.p t(".message"), class: "text-sm text-gray-500 mb-6" %>
|
||||
|
||||
<%= link_to t(".setup"), profile_onboarding_path, class: "block flex justify-center items-center btn btn--primary w-full" %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -16,5 +16,5 @@
|
|||
<% if invite_code_required? %>
|
||||
<%= form.text_field :invite_code, required: "required", label: true, value: params[:invite] %>
|
||||
<% end %>
|
||||
<%= form.submit %>
|
||||
<%= form.submit t(".submit") %>
|
||||
<% end %>
|
||||
|
|
|
@ -11,5 +11,5 @@
|
|||
<% end %>
|
||||
|
||||
<div class="mt-6 text-center">
|
||||
<p class="text-sm text-gray-600"><%= t(".forgot_password") %> <%= link_to t(".reset_password"), new_password_reset_path, class: "font-medium text-gray-600 hover:text-gray-400 transition" %></p>
|
||||
<%= link_to t(".forgot_password"), new_password_reset_path, class: "font-medium text-sm text-gray-900 hover:underline transition" %>
|
||||
</div>
|
||||
|
|
7
app/views/settings/_user_avatar.html.erb
Normal file
7
app/views/settings/_user_avatar.html.erb
Normal file
|
@ -0,0 +1,7 @@
|
|||
<%# locals: (user:) %>
|
||||
|
||||
<% if user.profile_image.attached? %>
|
||||
<%= image_tag user.profile_image.variant(:thumbnail), class: "rounded-full w-full h-full object-cover" %>
|
||||
<% else %>
|
||||
<div class="text-white w-full h-full bg-gray-400 rounded-full flex items-center justify-center text-lg uppercase"><%= user.initial %></div>
|
||||
<% end %>
|
52
app/views/settings/_user_avatar_field.html.erb
Normal file
52
app/views/settings/_user_avatar_field.html.erb
Normal file
|
@ -0,0 +1,52 @@
|
|||
<%# 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">
|
||||
|
||||
<%# 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">
|
||||
<img src="" alt="Preview" class="w-full h-full rounded-full object-cover">
|
||||
</div>
|
||||
|
||||
<%# The placeholder image for empty avatar field %>
|
||||
<div data-profile-image-preview-target="placeholderImage"
|
||||
class="h-full w-full flex justify-center items-center <%= user.profile_image.attached? ? "hidden" : "" %>">
|
||||
<div class="h-full w-full flex justify-center items-center">
|
||||
<%= lucide_icon "image-plus", class: "w-6 h-6 text-gray-500" %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<%# The attached image if user has already uploaded one %>
|
||||
<div data-profile-image-preview-target="attachedImage"
|
||||
class="h-full w-full flex justify-center items-center <%= user.profile_image.attached? ? "" : "hidden" %>">
|
||||
<% if user.profile_image.attached? %>
|
||||
<div class="h-full w-full">
|
||||
<%= render "settings/user_avatar", user: user %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<button type="button"
|
||||
data-profile-image-preview-target="clearBtn"
|
||||
data-action="click->profile-image-preview#clearFileInput"
|
||||
class="<%= user.profile_image.attached? ? "" : "hidden" %> cursor-pointer absolute bottom-0 right-0 w-8 h-8 bg-gray-50 rounded-full flex justify-center items-center border border-white border-2">
|
||||
<%= lucide_icon "x", class: "w-4 h-4 text-gray-500" %>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<%= 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, t(".choose"),
|
||||
class: "btn btn--outline inline-block" %>
|
||||
<%= form.file_field :profile_image,
|
||||
accept: "image/png, image/jpeg",
|
||||
class: "hidden px-3 py-2 bg-gray-50 text-gray-900 rounded-md text-sm font-medium",
|
||||
data: {
|
||||
profile_image_preview_target: "input",
|
||||
action: "change->profile-image-preview#showFileInputPreview"
|
||||
} %>
|
||||
</div>
|
||||
</div>
|
|
@ -5,7 +5,7 @@
|
|||
<div class="space-y-4">
|
||||
<h1 class="text-gray-900 text-xl font-medium mb-4"><%= t(".page_title") %></h1>
|
||||
<%= settings_section title: t(".subscription_title"), subtitle: t(".subscription_subtitle") do %>
|
||||
<% if Current.family.stripe_plan_id.blank? %>
|
||||
<% if @user.family.stripe_plan_id.blank? %>
|
||||
<%= link_to t(".subscribe_button"), new_subscription_path, class: "w-fit flex text-white text-sm font-medium items-center gap-1 bg-gray-900 rounded-lg p-2", data: { turbo: false } %>
|
||||
<% else %>
|
||||
<%= link_to t(".manage_subscription_button"), subscription_path, class: "w-fit flex text-white text-sm font-medium items-center gap-1 bg-gray-900 rounded-lg p-2", data: { turbo: false } %>
|
||||
|
|
|
@ -6,16 +6,28 @@
|
|||
<h1 class="text-gray-900 text-xl font-medium mb-4"><%= t(".page_title") %></h1>
|
||||
<%= settings_section title: t(".general_title"), subtitle: t(".general_subtitle") do %>
|
||||
<div>
|
||||
<%= styled_form_with model: Current.user, url: settings_preferences_path, class: "space-y-4", data: { controller: "auto-submit-form" } do |form| %>
|
||||
<%= form.fields_for :family_attributes do |family_form| %>
|
||||
<%= styled_form_with model: @user, class: "space-y-4", data: { controller: "auto-submit-form" } do |form| %>
|
||||
<%= form.hidden_field :redirect_to, value: "preferences" %>
|
||||
|
||||
<%= form.fields_for :family do |family_form| %>
|
||||
<%= family_form.select :currency,
|
||||
currencies_for_select.map { |currency| [ "#{currency.name} (#{currency.iso_code})", currency.iso_code ] },
|
||||
{ label: "Currency", selected: Current.family.currency },
|
||||
{ label: t(".currency") },
|
||||
{ data: { auto_submit_form_target: "auto" } } %>
|
||||
|
||||
<%= family_form.select :locale,
|
||||
I18n.available_locales,
|
||||
{ label: "Locale", selected: Current.family.locale },
|
||||
language_options,
|
||||
{ label: t(".language") },
|
||||
{ data: { auto_submit_form_target: "auto" } } %>
|
||||
|
||||
<%= family_form.select :date_format,
|
||||
date_format_options,
|
||||
{ label: t(".date_format") },
|
||||
{ data: { auto_submit_form_target: "auto" } } %>
|
||||
|
||||
<%= family_form.select :country,
|
||||
country_options,
|
||||
{ label: t(".country") },
|
||||
{ data: { auto_submit_form_target: "auto" } } %>
|
||||
|
||||
<p class="text-xs italic pl-2 text-gray-500">Please note, we are still working on translations for various languages. Please see the <%= link_to "I18n issue", "https://github.com/maybe-finance/maybe/issues/1225", target: "_blank", class: "underline" %> for more information.</p>
|
||||
|
@ -25,7 +37,8 @@
|
|||
<% end %>
|
||||
<%= settings_section title: t(".theme_title"), subtitle: t(".theme_subtitle") do %>
|
||||
<div>
|
||||
<%= styled_form_with model: Current.user, url: settings_preferences_path, local: true, class: "flex justify-between items-center" do |form| %>
|
||||
<%= styled_form_with model: @user, class: "flex justify-between items-center" do |form| %>
|
||||
<%= form.hidden_field :redirect_to, value: "preferences" %>
|
||||
<div class="text-center">
|
||||
<%= image_tag("light-mode-preview.png", alt: "Light Theme Preview", class: "h-44 mb-4") %>
|
||||
<div class="flex justify-center items-center gap-2">
|
||||
|
|
|
@ -5,35 +5,13 @@
|
|||
<h1 class="text-gray-900 text-xl font-medium mb-4"><%= t(".page_title") %></h1>
|
||||
<div class="space-y-4">
|
||||
<%= settings_section title: t(".profile_title"), subtitle: t(".profile_subtitle") do %>
|
||||
<%= styled_form_with model: Current.user, url: settings_profile_path, class: "space-y-4", data: { controller: "profile-image-preview" } do |form| %>
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="relative flex justify-center items-center bg-gray-50 w-24 h-24 rounded-full border border-alpha-black-25">
|
||||
<div data-profile-image-preview-target="imagePreview" class="h-full w-full flex justify-center items-center">
|
||||
<% profile_image_attached = Current.user.profile_image.attached? %>
|
||||
<% if profile_image_attached %>
|
||||
<div class="h-24 w-24">
|
||||
<%= render "shared/user_profile_image", user: Current.user %>
|
||||
</div>
|
||||
<% else %>
|
||||
<%= lucide_icon "image-plus", class: "w-6 h-6 text-gray-500" %>
|
||||
<% end %>
|
||||
</div>
|
||||
<%= lucide_icon "image-plus", class: "hidden w-6 h-6 text-gray-500", data: { profile_image_preview_target: "template" } %>
|
||||
<div data-profile-image-preview-target="clearBtn" data-action="click->profile-image-preview#clear" class="<%= profile_image_attached ? "" : "hidden" %> cursor-pointer absolute bottom-0 right-0 w-8 h-8 bg-gray-50 rounded-full flex justify-center items-center border border-white border-2">
|
||||
<%= lucide_icon "x", class: "w-4 h-4 text-gray-500" %>
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-y-3">
|
||||
<p><%= t(".profile_image_type") %></p>
|
||||
<%= form.label :profile_image, t(".profile_image_choose"), class: "inline-block cursor-pointer px-3 py-2 bg-gray-50 text-gray-900 rounded-md text-sm font-medium" %>
|
||||
<%= form.file_field :profile_image, accept: "image/png, image/jpeg", class: "hidden px-3 py-2 bg-gray-50 text-gray-900 rounded-md text-sm font-medium", data: {profile_image_preview_target: "fileField", action: "change->profile-image-preview#preview"} %>
|
||||
<%= form.hidden_field :delete_profile_image, value: false, data: {profile_image_preview_target: "deleteField"} %>
|
||||
</div>
|
||||
</div>
|
||||
<%= styled_form_with model: @user, class: "space-y-4" do |form| %>
|
||||
<%= render "settings/user_avatar_field", form: form, user: @user %>
|
||||
|
||||
<div>
|
||||
<div class="grid grid-cols-2 gap-4 mt-4">
|
||||
<%= form.text_field :first_name, placeholder: "First name", value: Current.user.first_name, label: true %>
|
||||
<%= form.text_field :last_name, placeholder: "Last name", value: Current.user.last_name, label: true %>
|
||||
<%= 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: "bg-gray-900 hover:bg-gray-700 cursor-pointer text-white rounded-lg px-3 py-2" %>
|
||||
|
@ -43,9 +21,13 @@
|
|||
<% end %>
|
||||
<%= settings_section title: t(".household_title"), subtitle: t(".household_subtitle") do %>
|
||||
<div class="space-y-4">
|
||||
<%= styled_form_with model: Current.user, url: settings_profile_path, class: "space-y-4", data: { controller: "auto-submit-form", "auto-submit-form-trigger-event-value": "blur" } do |form| %>
|
||||
<%= form.fields_for :family_attributes do |family_fields| %>
|
||||
<%= family_fields.text_field :name, placeholder: t(".household_form_input_placeholder"), value: Current.family.name, label: t(".household_form_label"), disabled: !Current.user.admin?, "data-auto-submit-form-target": "auto" %>
|
||||
<%= styled_form_with model: Current.user, class: "space-y-4", data: { controller: "auto-submit-form" } do |form| %>
|
||||
<%= form.fields_for :family do |family_fields| %>
|
||||
<%= family_fields.text_field :name,
|
||||
placeholder: t(".household_form_input_placeholder"),
|
||||
label: t(".household_form_label"),
|
||||
disabled: !Current.user.admin?,
|
||||
"data-auto-submit-form-target": "auto" %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<div class="bg-gray-25 rounded-xl p-1">
|
||||
|
@ -71,7 +53,7 @@
|
|||
<p class="text-gray-500 text-sm"><%= t(".delete_account_warning") %></p>
|
||||
</div>
|
||||
<%=
|
||||
button_to t(".delete_account"), settings_profile_path, method: :delete,
|
||||
button_to t(".delete_account"), user_path(@user), method: :delete,
|
||||
class: "bg-red-500 text-white text-sm font-medium rounded-lg px-3 py-2",
|
||||
data: { turbo_confirm: {
|
||||
title: t(".confirm_delete.title"),
|
||||
|
|
|
@ -6,7 +6,12 @@
|
|||
currency = Money::Currency.new(currency_value || options[:default_currency] || "USD") %>
|
||||
|
||||
<div class="form-field pr-0 <%= options[:container_class] %>" data-controller="money-field">
|
||||
<%= form.label options[:label] || t(".label"), class: "form-field__label" %>
|
||||
<%= form.label options[:label] || t(".label"), class: "form-field__label" do %>
|
||||
<%= options[:label] || t(".label") %>
|
||||
<% if options[:required] %>
|
||||
<span class="text-red-500">*</span>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
<div class="flex items-center gap-1">
|
||||
<div class="flex items-center grow gap-1">
|
||||
|
|
|
@ -32,7 +32,7 @@
|
|||
<div class="h-5 shrink-0">
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg" class="shrink-0">
|
||||
<path d="M18 10C18 14.4183 14.4183 18 10 18C5.58172 18 2 14.4183 2 10C2 5.58172 5.58172 2 10 2C14.4183 2 18 5.58172 18 10ZM3.6 10C3.6 13.5346 6.46538 16.4 10 16.4C13.5346 16.4 16.4 13.5346 16.4 10C16.4 6.46538 13.5346 3.6 10 3.6C6.46538 3.6 3.6 6.46538 3.6 10Z" fill="#E5E5E5" />
|
||||
<circle class="origin-center -rotate-90 animate-[stroke-fill_5s_300ms_forwards]" stroke="#141414" stroke-opacity="0.4" r="7.2" cx="10" cy="10" stroke-dasharray="43.9822971503" stroke-dashoffset="43.9822971503" />
|
||||
<circle class="origin-center -rotate-90 animate-[stroke-fill_2.2s_300ms_forwards]" stroke="#141414" stroke-opacity="0.4" r="7.2" cx="10" cy="10" stroke-dasharray="43.9822971503" stroke-dashoffset="43.9822971503" />
|
||||
</svg>
|
||||
<div class="absolute -top-2 -right-2">
|
||||
<%= lucide_icon "x", class: "w-5 h-5 p-0.5 hidden group-hover:inline-block border border-alpha-black-50 border-solid rounded-lg bg-white text-gray-400 cursor-pointer", data: { action: "click->element-removal#remove" } %>
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
<%= image_tag user.profile_image.variant(:thumbnail), class: "rounded-full w-full h-full object-cover" %>
|
|
@ -1,70 +1,30 @@
|
|||
---
|
||||
en:
|
||||
accounts:
|
||||
sync_all_button:
|
||||
sync: Sync all
|
||||
account:
|
||||
has_issues: Issue detected.
|
||||
troubleshoot: Troubleshoot
|
||||
account_list:
|
||||
new_account: "New %{type}"
|
||||
empty:
|
||||
no_accounts: No accounts yet
|
||||
empty_message: Add an account either via connection, importing or entering manually.
|
||||
new_account: New account
|
||||
form:
|
||||
name_label: Account name
|
||||
name_placeholder: Example account name
|
||||
institution: Financial institution
|
||||
ungrouped: "(none)"
|
||||
balance: Today's balance
|
||||
accountable_type: Account type
|
||||
mode: Value tracking mode
|
||||
mode_prompt: Select a mode
|
||||
type_prompt: Select a type
|
||||
header:
|
||||
accounts: Accounts
|
||||
manage: Manage accounts
|
||||
new: New account
|
||||
institution_accounts:
|
||||
add_account_to_institution: Add new account
|
||||
has_issues: Issue detected, see accounts
|
||||
syncing: Syncing...
|
||||
status: "Last synced %{last_synced_at} ago"
|
||||
status_never: Requires data sync
|
||||
edit: Edit institution
|
||||
delete: Delete institution
|
||||
confirm_title: Delete financial institution?
|
||||
confirm_body: Don't worry, none of the accounts within this institution will be affected by this deletion. Accounts will be ungrouped and all historical data will remain intact.
|
||||
confirm_accept: Delete institution
|
||||
new_account: Add account
|
||||
institutionless_accounts:
|
||||
other_accounts: Other accounts
|
||||
menu:
|
||||
edit: Edit
|
||||
import: Import transactions
|
||||
confirm_title: Delete account?
|
||||
confirm_body_html: "<p>By deleting this account, you will erase its value history, affecting various aspects of your overall account. This action will have a direct impact on your net worth calculations and the account graphs.</p><br /> <p>After deletion, there is no way you'll be able to restore the account information because you'll need to add it as a new account.</p>"
|
||||
confirm_accept: 'Delete "%{name}"'
|
||||
new_account: New %{type}
|
||||
accountables:
|
||||
credit_card:
|
||||
form:
|
||||
available_credit: Available credit
|
||||
available_credit_placeholder: '10000'
|
||||
minimum_payment: Minimum payment
|
||||
minimum_payment_placeholder: '100'
|
||||
apr: APR
|
||||
apr_placeholder: '15.99'
|
||||
expiration_date: Expiration date
|
||||
annual_fee: Annual fee
|
||||
annual_fee_placeholder: '99'
|
||||
apr: APR
|
||||
apr_placeholder: '15.99'
|
||||
available_credit: Available credit
|
||||
available_credit_placeholder: '10000'
|
||||
expiration_date: Expiration date
|
||||
minimum_payment: Minimum payment
|
||||
minimum_payment_placeholder: '100'
|
||||
overview:
|
||||
amount_owed: Amount Owed
|
||||
available_credit: Available Credit
|
||||
minimum_payment: Minimum Payment
|
||||
apr: APR
|
||||
expiration_date: Expiration Date
|
||||
annual_fee: Annual Fee
|
||||
apr: APR
|
||||
available_credit: Available Credit
|
||||
expiration_date: Expiration Date
|
||||
minimum_payment: Minimum Payment
|
||||
unknown: Unknown
|
||||
depository:
|
||||
form:
|
||||
|
@ -77,7 +37,8 @@ en:
|
|||
tooltip:
|
||||
cash: Cash
|
||||
holdings: Holdings
|
||||
total_value_tooltip: The total value is the sum of cash balance and your holdings value, minus margin loans.
|
||||
total_value_tooltip: The total value is the sum of cash balance and your
|
||||
holdings value, minus margin loans.
|
||||
loan:
|
||||
form:
|
||||
interest_rate: Interest rate
|
||||
|
@ -133,17 +94,65 @@ en:
|
|||
trend: Trend
|
||||
unknown: Unknown
|
||||
year: Year
|
||||
create:
|
||||
success: New account created successfully
|
||||
destroy:
|
||||
success: Account deleted successfully
|
||||
edit:
|
||||
edit: "Edit %{account}"
|
||||
edit: Edit %{account}
|
||||
empty:
|
||||
empty_message: Add an account either via connection, importing or entering manually.
|
||||
new_account: New account
|
||||
no_accounts: No accounts yet
|
||||
form:
|
||||
accountable_type: Account type
|
||||
balance: Today's balance
|
||||
institution: Financial institution
|
||||
mode: Value tracking mode
|
||||
mode_prompt: Select a mode
|
||||
name_label: Account name
|
||||
name_placeholder: Example account name
|
||||
type_prompt: Select a type
|
||||
ungrouped: "(none)"
|
||||
header:
|
||||
accounts: Accounts
|
||||
manage: Manage accounts
|
||||
new: New account
|
||||
index:
|
||||
accounts: Accounts
|
||||
add_institution: Add institution
|
||||
new_account: New account
|
||||
institution_accounts:
|
||||
add_account_to_institution: Add new account
|
||||
confirm_accept: Delete institution
|
||||
confirm_body: Don't worry, none of the accounts within this institution will
|
||||
be affected by this deletion. Accounts will be ungrouped and all historical
|
||||
data will remain intact.
|
||||
confirm_title: Delete financial institution?
|
||||
delete: Delete institution
|
||||
edit: Edit institution
|
||||
has_issues: Issue detected, see accounts
|
||||
new_account: Add account
|
||||
status: Last synced %{last_synced_at} ago
|
||||
status_never: Requires data sync
|
||||
syncing: Syncing...
|
||||
institutionless_accounts:
|
||||
other_accounts: Other accounts
|
||||
menu:
|
||||
confirm_accept: Delete "%{name}"
|
||||
confirm_body_html: "<p>By deleting this account, you will erase its value history,
|
||||
affecting various aspects of your overall account. This action will have a
|
||||
direct impact on your net worth calculations and the account graphs.</p><br
|
||||
/> <p>After deletion, there is no way you'll be able to restore the account
|
||||
information because you'll need to add it as a new account.</p>"
|
||||
confirm_title: Delete account?
|
||||
edit: Edit
|
||||
import: Import transactions
|
||||
new:
|
||||
title: Add an account
|
||||
manual_entry: Enter account manually
|
||||
csv_entry: Import accounts CSV
|
||||
connected_entry: Securely link account with Plaid (coming soon)
|
||||
csv_entry: Import accounts CSV
|
||||
manual_entry: Enter account manually
|
||||
title: Add an account
|
||||
show:
|
||||
cash: Cash
|
||||
holdings: Holdings
|
||||
|
@ -157,17 +166,17 @@ en:
|
|||
summary:
|
||||
new: New
|
||||
no_assets: No assets found
|
||||
no_assets_description: Add an asset either via connection, importing or entering manually.
|
||||
no_assets_description: Add an asset either via connection, importing or entering
|
||||
manually.
|
||||
no_liabilities: No liabilities found
|
||||
no_liabilities_description: Add a liability either via connection, importing or entering manually.
|
||||
create:
|
||||
success: New account created successfully
|
||||
destroy:
|
||||
success: Account deleted successfully
|
||||
update:
|
||||
success: Account updated
|
||||
no_liabilities_description: Add a liability either via connection, importing
|
||||
or entering manually.
|
||||
sync_all:
|
||||
success: Successfully queued accounts for syncing.
|
||||
sync_all_button:
|
||||
sync: Sync all
|
||||
update:
|
||||
success: Account updated
|
||||
credit_cards:
|
||||
create:
|
||||
success: Credit card created successfully
|
||||
|
|
|
@ -1,15 +1,15 @@
|
|||
---
|
||||
en:
|
||||
impersonation_sessions:
|
||||
create:
|
||||
success: "Request sent to user. Waiting for approval."
|
||||
join:
|
||||
success: "Joined session"
|
||||
leave:
|
||||
success: "Left session"
|
||||
approve:
|
||||
success: "Request approved"
|
||||
reject:
|
||||
success: "Request rejected"
|
||||
success: Request approved
|
||||
complete:
|
||||
success: "Session completed"
|
||||
success: Session completed
|
||||
create:
|
||||
success: Request sent to user. Waiting for approval.
|
||||
join:
|
||||
success: Joined session
|
||||
leave:
|
||||
success: Left session
|
||||
reject:
|
||||
success: Request rejected
|
||||
|
|
|
@ -4,7 +4,8 @@ en:
|
|||
cleans:
|
||||
show:
|
||||
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: You have errors in your data. Hover over the error to see
|
||||
details.
|
||||
title: Clean your data
|
||||
configurations:
|
||||
show:
|
||||
|
@ -18,8 +19,8 @@ en:
|
|||
no_accounts: You don't have any accounts yet. Please create an account that
|
||||
we can use for (unassigned) rows in your CSV or go back to the Clean step
|
||||
and provide an account name we can use.
|
||||
unassigned_account: Need to create a new account for unassigned rows?
|
||||
rows_label: Rows
|
||||
unassigned_account: Need to create a new account for unassigned rows?
|
||||
show:
|
||||
account_mapping_description: Assign all of your imported file's accounts to
|
||||
Maybe's existing accounts. You can also add new accounts or leave them
|
||||
|
|
|
@ -2,12 +2,14 @@
|
|||
en:
|
||||
layouts:
|
||||
auth:
|
||||
or: or
|
||||
privacy_policy: Privacy Policy
|
||||
sign_in: sign in to your account
|
||||
sign_up: create an account
|
||||
terms_of_service: Terms of Service
|
||||
existing_account: Already have an account?
|
||||
no_account: New to Maybe?
|
||||
sign_in: Sign in
|
||||
sign_up: Create account
|
||||
your_account: Your account
|
||||
footer:
|
||||
privacy_policy: Privacy Policy
|
||||
terms_of_service: Terms of Service
|
||||
issues:
|
||||
action: How to fix this issue
|
||||
description: Issue Description
|
||||
|
|
28
config/locales/views/onboardings/en.yml
Normal file
28
config/locales/views/onboardings/en.yml
Normal file
|
@ -0,0 +1,28 @@
|
|||
---
|
||||
en:
|
||||
onboardings:
|
||||
header:
|
||||
sign_out: Log out
|
||||
preferences:
|
||||
currency: Currency
|
||||
date_format: Date format
|
||||
example: Example account
|
||||
locale: Language
|
||||
preview: Preview how data displays based on preferences.
|
||||
submit: Complete
|
||||
subtitle: Let's configure your preferences.
|
||||
title: Configure your preferences
|
||||
profile:
|
||||
country: Country
|
||||
first_name: First Name
|
||||
household_name: Household Name
|
||||
last_name: Last Name
|
||||
profile_image: Profile Image
|
||||
submit: Continue
|
||||
subtitle: Let's complete your profile.
|
||||
title: Let's set up the basics
|
||||
show:
|
||||
message: We’re really excited you’re here. In the next step we’ll ask you a
|
||||
few questions to complete your profile and then get you all set up.
|
||||
setup: Set up account
|
||||
title: Meet Maybe
|
|
@ -7,8 +7,8 @@ en:
|
|||
allocation_chart:
|
||||
assets: Assets
|
||||
debts: Debts
|
||||
greeting: Welcome back, %{name}
|
||||
fallback_greeting: Welcome back, friend
|
||||
greeting: Welcome back, %{name}
|
||||
import: Import
|
||||
income: Income
|
||||
investing: Investing (coming soon...)
|
||||
|
|
|
@ -13,6 +13,7 @@ en:
|
|||
invalid_invite_code: Invalid invite code, please try again.
|
||||
success: You have signed up successfully.
|
||||
new:
|
||||
submit: Create account
|
||||
title: Create an account
|
||||
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.
|
||||
|
|
|
@ -10,6 +10,5 @@ en:
|
|||
email_placeholder: you@example.com
|
||||
forgot_password: Forgot your password?
|
||||
password: Password
|
||||
reset_password: Reset it
|
||||
submit: Log in
|
||||
title: Sign in to your account
|
||||
|
|
|
@ -29,19 +29,19 @@ en:
|
|||
previous: Back
|
||||
preferences:
|
||||
show:
|
||||
country: Country
|
||||
currency: Currency
|
||||
date_format: Date format
|
||||
general_subtitle: Configure your preferences
|
||||
general_title: General
|
||||
language: Language
|
||||
page_title: Preferences
|
||||
theme_dark: Dark
|
||||
theme_light: Light
|
||||
theme_subtitle: Choose a preferred theme for the app (coming soon...)
|
||||
theme_system: System
|
||||
theme_title: Theme
|
||||
update:
|
||||
success: Preferences updated successfully.
|
||||
profiles:
|
||||
destroy:
|
||||
success: Account deleted successfully.
|
||||
show:
|
||||
confirm_delete:
|
||||
body: Are you sure you want to permanently delete your account? This action
|
||||
|
@ -51,16 +51,17 @@ en:
|
|||
delete_account: Delete account
|
||||
delete_account_warning: Deleting your account will permanently remove all
|
||||
your data and cannot be undone.
|
||||
first_name: First Name
|
||||
household_form_input_placeholder: Enter household name
|
||||
household_form_label: Household name
|
||||
household_subtitle: Invite family members, partners and other inviduals. Invitees
|
||||
can login to your household and access your shared accounts.
|
||||
household_title: Household
|
||||
last_name: Last Name
|
||||
page_title: Account
|
||||
profile_image_choose: Choose
|
||||
profile_image_type: JPG, GIF or PNG. 5MB max.
|
||||
profile_subtitle: Customize how you appear on Maybe
|
||||
profile_title: Profile
|
||||
save: Save
|
||||
update:
|
||||
success: Profile updated successfully.
|
||||
user_avatar_field:
|
||||
accepted_formats: JPG or PNG. 5MB max.
|
||||
choose: Choose
|
||||
|
|
7
config/locales/views/users/en.yml
Normal file
7
config/locales/views/users/en.yml
Normal file
|
@ -0,0 +1,7 @@
|
|||
---
|
||||
en:
|
||||
users:
|
||||
destroy:
|
||||
success: Your account has been deleted.
|
||||
update:
|
||||
success: Your profile has been updated.
|
|
@ -5,14 +5,23 @@ Rails.application.routes.draw do
|
|||
get "feedback", to: "pages#feedback"
|
||||
get "early-access", to: "pages#early_access"
|
||||
|
||||
resource :registration
|
||||
resource :registration, only: %i[new create]
|
||||
resources :sessions, only: %i[new create destroy]
|
||||
resource :password_reset
|
||||
resource :password
|
||||
resource :password_reset, only: %i[new create edit update]
|
||||
resource :password, only: %i[edit update]
|
||||
|
||||
resources :users, only: %i[update destroy]
|
||||
|
||||
resource :onboarding, only: :show do
|
||||
collection do
|
||||
get :profile
|
||||
get :preferences
|
||||
end
|
||||
end
|
||||
|
||||
namespace :settings do
|
||||
resource :profile, only: %i[show update destroy]
|
||||
resource :preferences, only: %i[show update]
|
||||
resource :profile, only: :show
|
||||
resource :preferences, only: :show
|
||||
resource :hosting, only: %i[show update]
|
||||
resource :billing, only: :show
|
||||
end
|
||||
|
|
7
db/migrate/20241022221544_add_onboarding_fields.rb
Normal file
7
db/migrate/20241022221544_add_onboarding_fields.rb
Normal file
|
@ -0,0 +1,7 @@
|
|||
class AddOnboardingFields < ActiveRecord::Migration[7.2]
|
||||
def change
|
||||
add_column :users, :onboarded_at, :datetime
|
||||
add_column :families, :date_format, :string, default: "%m-%d-%Y"
|
||||
add_column :families, :country, :string, default: "US"
|
||||
end
|
||||
end
|
7
db/schema.rb
generated
7
db/schema.rb
generated
|
@ -10,7 +10,7 @@
|
|||
#
|
||||
# It's strongly recommended that you check this file into your version control system.
|
||||
|
||||
ActiveRecord::Schema[7.2].define(version: 2024_10_22_192319) do
|
||||
ActiveRecord::Schema[7.2].define(version: 2024_10_22_221544) do
|
||||
# These are extensions that must be enabled in order to support this database
|
||||
enable_extension "pgcrypto"
|
||||
enable_extension "plpgsql"
|
||||
|
@ -119,7 +119,7 @@ ActiveRecord::Schema[7.2].define(version: 2024_10_22_192319) do
|
|||
t.boolean "is_active", default: true, null: false
|
||||
t.date "last_sync_date"
|
||||
t.uuid "institution_id"
|
||||
t.virtual "classification", type: :string, as: "\nCASE\n WHEN ((accountable_type)::text = ANY (ARRAY[('Loan'::character varying)::text, ('CreditCard'::character varying)::text, ('OtherLiability'::character varying)::text])) THEN 'liability'::text\n ELSE 'asset'::text\nEND", stored: true
|
||||
t.virtual "classification", type: :string, as: "\nCASE\n WHEN ((accountable_type)::text = ANY ((ARRAY['Loan'::character varying, 'CreditCard'::character varying, 'OtherLiability'::character varying])::text[])) THEN 'liability'::text\n ELSE 'asset'::text\nEND", stored: true
|
||||
t.uuid "import_id"
|
||||
t.string "mode"
|
||||
t.index ["accountable_id", "accountable_type"], name: "index_accounts_on_accountable_id_and_accountable_type"
|
||||
|
@ -226,6 +226,8 @@ ActiveRecord::Schema[7.2].define(version: 2024_10_22_192319) do
|
|||
t.string "stripe_plan_id"
|
||||
t.string "stripe_customer_id"
|
||||
t.string "stripe_subscription_status", default: "incomplete"
|
||||
t.string "date_format", default: "%m-%d-%Y"
|
||||
t.string "country", default: "US"
|
||||
end
|
||||
|
||||
create_table "good_job_batches", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
|
||||
|
@ -558,6 +560,7 @@ ActiveRecord::Schema[7.2].define(version: 2024_10_22_192319) do
|
|||
t.string "last_alerted_upgrade_commit_sha"
|
||||
t.string "role", default: "member", null: false
|
||||
t.boolean "active", default: true, null: false
|
||||
t.datetime "onboarded_at"
|
||||
t.index ["email"], name: "index_users_on_email", unique: true
|
||||
t.index ["family_id"], name: "index_users_on_family_id"
|
||||
end
|
||||
|
|
|
@ -28,7 +28,7 @@ class SessionsControllerTest < ActionDispatch::IntegrationTest
|
|||
sign_in @user
|
||||
|
||||
delete session_url(@user.sessions.order(:created_at).last)
|
||||
assert_redirected_to root_url
|
||||
assert_redirected_to new_session_path
|
||||
assert_equal "You have signed out successfully.", flash[:notice]
|
||||
end
|
||||
end
|
||||
|
|
|
@ -9,35 +9,4 @@ class Settings::ProfilesControllerTest < ActionDispatch::IntegrationTest
|
|||
get settings_profile_url
|
||||
assert_response :success
|
||||
end
|
||||
|
||||
test "member can deactivate their account" do
|
||||
sign_in @member = users(:family_member)
|
||||
delete settings_profile_url
|
||||
|
||||
assert_redirected_to root_url
|
||||
|
||||
assert_not User.find(@member.id).active?
|
||||
assert_enqueued_with(job: UserPurgeJob, args: [ @member ])
|
||||
end
|
||||
|
||||
test "admin prevented from deactivating when other users are present" do
|
||||
sign_in @admin = users(:family_admin)
|
||||
delete settings_profile_url
|
||||
|
||||
assert_redirected_to settings_profile_url
|
||||
assert_equal "Admin cannot delete account while other users are present. Please delete all members first.", flash[:alert]
|
||||
assert_no_enqueued_jobs only: UserPurgeJob
|
||||
assert User.find(@admin.id).active?
|
||||
end
|
||||
|
||||
test "admin can deactivate their account when they are the last user in the family" do
|
||||
sign_in @admin = users(:family_admin)
|
||||
users(:family_member).destroy
|
||||
|
||||
delete settings_profile_url
|
||||
|
||||
assert_redirected_to root_url
|
||||
assert_not User.find(@admin.id).active?
|
||||
assert_enqueued_with(job: UserPurgeJob, args: [ @admin ])
|
||||
end
|
||||
end
|
||||
|
|
64
test/controllers/users_controller_test.rb
Normal file
64
test/controllers/users_controller_test.rb
Normal file
|
@ -0,0 +1,64 @@
|
|||
require "test_helper"
|
||||
|
||||
class UsersControllerTest < ActionDispatch::IntegrationTest
|
||||
setup do
|
||||
sign_in @user = users(:family_admin)
|
||||
end
|
||||
|
||||
test "can supply custom redirect after update" do
|
||||
patch user_url(@user), params: { user: { redirect_to: "home" } }
|
||||
assert_redirected_to root_url
|
||||
end
|
||||
|
||||
test "can update user profile" do
|
||||
patch user_url(@user), params: {
|
||||
user: {
|
||||
first_name: "John",
|
||||
last_name: "Doe",
|
||||
onboarded_at: Time.current,
|
||||
profile_image: file_fixture_upload("profile_image.png", "image/png", :binary),
|
||||
family_attributes: {
|
||||
name: "New Family Name",
|
||||
country: "US",
|
||||
date_format: "%m/%d/%Y",
|
||||
currency: "USD",
|
||||
locale: "en"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
assert_redirected_to settings_profile_url
|
||||
assert_equal "Your profile has been updated.", flash[:notice]
|
||||
end
|
||||
|
||||
test "member can deactivate their account" do
|
||||
sign_in @member = users(:family_member)
|
||||
delete user_url(@member)
|
||||
|
||||
assert_redirected_to root_url
|
||||
|
||||
assert_not User.find(@member.id).active?
|
||||
assert_enqueued_with(job: UserPurgeJob, args: [ @member ])
|
||||
end
|
||||
|
||||
test "admin prevented from deactivating when other users are present" do
|
||||
sign_in @admin = users(:family_admin)
|
||||
delete user_url(users(:family_member))
|
||||
|
||||
assert_redirected_to settings_profile_url
|
||||
assert_equal "Admin cannot delete account while other users are present. Please delete all members first.", flash[:alert]
|
||||
assert_no_enqueued_jobs only: UserPurgeJob
|
||||
assert User.find(@admin.id).active?
|
||||
end
|
||||
|
||||
test "admin can deactivate their account when they are the last user in the family" do
|
||||
sign_in @admin = users(:family_admin)
|
||||
users(:family_member).destroy
|
||||
|
||||
delete user_url(@admin)
|
||||
|
||||
assert_redirected_to root_url
|
||||
assert_not User.find(@admin.id).active?
|
||||
assert_enqueued_with(job: UserPurgeJob, args: [ @admin ])
|
||||
end
|
||||
end
|
4
test/fixtures/users.yml
vendored
4
test/fixtures/users.yml
vendored
|
@ -4,6 +4,7 @@ empty:
|
|||
last_name: One
|
||||
email: user1@email.com
|
||||
password_digest: <%= BCrypt::Password.create('password') %>
|
||||
onboarded_at: <%= 3.days.ago %>
|
||||
|
||||
maybe_support_staff:
|
||||
family: empty
|
||||
|
@ -12,6 +13,7 @@ maybe_support_staff:
|
|||
email: support@maybe.co
|
||||
password_digest: <%= BCrypt::Password.create('password') %>
|
||||
role: super_admin
|
||||
onboarded_at: <%= 3.days.ago %>
|
||||
|
||||
family_admin:
|
||||
family: dylan_family
|
||||
|
@ -20,6 +22,7 @@ family_admin:
|
|||
email: bob@bobdylan.com
|
||||
password_digest: <%= BCrypt::Password.create('password') %>
|
||||
role: admin
|
||||
onboarded_at: <%= 3.days.ago %>
|
||||
|
||||
family_member:
|
||||
family: dylan_family
|
||||
|
@ -27,3 +30,4 @@ family_member:
|
|||
last_name: Dylan
|
||||
email: jakobdylan@yahoo.com
|
||||
password_digest: <%= BCrypt::Password.create('password') %>
|
||||
onboarded_at: <%= 3.days.ago %>
|
|
@ -16,7 +16,7 @@ class ImportsTest < ApplicationSystemTestCase
|
|||
|
||||
find('input[type="submit"][value="Upload CSV"]').click
|
||||
|
||||
select "Date", from: "Date"
|
||||
select "Date", from: "Date*"
|
||||
select "YYYY-MM-DD", from: "Date format"
|
||||
select "Amount", from: "Amount"
|
||||
select "Account", from: "Account (optional)"
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue