mirror of
https://github.com/maybe-finance/maybe.git
synced 2025-07-19 05:09:38 +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
|
class ApplicationController < ActionController::Base
|
||||||
include Localize, AutoSync, Authentication, Invitable, SelfHostable, StoreLocation, Impersonatable
|
include Onboardable, Localize, AutoSync, Authentication, Invitable, SelfHostable, StoreLocation, Impersonatable
|
||||||
include Pagy::Backend
|
include Pagy::Backend
|
||||||
|
|
||||||
private
|
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
|
def destroy
|
||||||
@session.destroy
|
@session.destroy
|
||||||
redirect_to root_path, notice: t(".logout_successful")
|
redirect_to new_session_path, notice: t(".logout_successful")
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
|
@ -1,2 +1,5 @@
|
||||||
class Settings::BillingsController < SettingsController
|
class Settings::BillingsController < SettingsController
|
||||||
|
def show
|
||||||
|
@user = Current.user
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,26 +1,5 @@
|
||||||
class Settings::PreferencesController < SettingsController
|
class Settings::PreferencesController < SettingsController
|
||||||
def edit
|
def show
|
||||||
|
@user = Current.user
|
||||||
end
|
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
|
end
|
||||||
|
|
|
@ -1,38 +1,5 @@
|
||||||
class Settings::ProfilesController < SettingsController
|
class Settings::ProfilesController < SettingsController
|
||||||
def show
|
def show
|
||||||
|
@user = Current.user
|
||||||
end
|
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
|
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
|
module ApplicationHelper
|
||||||
include Pagy::Frontend
|
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)
|
def title(page_title)
|
||||||
content_for(:title) { page_title }
|
content_for(:title) { page_title }
|
||||||
end
|
end
|
||||||
|
@ -132,6 +145,19 @@ module ApplicationHelper
|
||||||
end
|
end
|
||||||
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 = {})
|
def format_money(number_or_money, options = {})
|
||||||
return nil unless number_or_money
|
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 = {})
|
def select(method, choices, options = {}, html_options = {})
|
||||||
merged_html_options = { class: "form-field__input" }.merge(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)
|
field = super(method, choices, options, merged_html_options)
|
||||||
|
|
||||||
build_styled_field(label, field, options, remove_padding_right: true)
|
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 = {})
|
def collection_select(method, collection, value_method, text_method, options = {}, html_options = {})
|
||||||
merged_html_options = { class: "form-field__input" }.merge(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)
|
field = super(method, collection, value_method, text_method, options, merged_html_options)
|
||||||
|
|
||||||
build_styled_field(label, field, options, remove_padding_right: true)
|
build_styled_field(label, field, options, remove_padding_right: true)
|
||||||
|
@ -68,7 +68,17 @@ class StyledFormBuilder < ActionView::Helpers::FormBuilder
|
||||||
|
|
||||||
def build_label(method, options)
|
def build_label(method, options)
|
||||||
return "".html_safe unless options[:label]
|
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
|
||||||
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 {
|
export default class extends Controller {
|
||||||
static targets = [
|
static targets = [
|
||||||
"imagePreview",
|
"attachedImage",
|
||||||
"fileField",
|
"previewImage",
|
||||||
"deleteField",
|
"placeholderImage",
|
||||||
|
"deleteProfileImage",
|
||||||
|
"input",
|
||||||
"clearBtn",
|
"clearBtn",
|
||||||
"template",
|
|
||||||
];
|
];
|
||||||
|
|
||||||
preview(event) {
|
clearFileInput() {
|
||||||
const file = event.target.files[0];
|
this.inputTarget.value = null;
|
||||||
if (file) {
|
this.clearBtnTarget.classList.add("hidden");
|
||||||
const reader = new FileReader();
|
this.placeholderImageTarget.classList.remove("hidden");
|
||||||
reader.onload = (e) => {
|
this.attachedImageTarget.classList.add("hidden");
|
||||||
this.imagePreviewTarget.innerHTML = `<img src="${e.target.result}" alt="Preview" class="w-full h-full rounded-full object-cover" />`;
|
this.previewImageTarget.classList.add("hidden");
|
||||||
this.templateTarget.classList.add("hidden");
|
this.deleteProfileImageTarget.value = "1";
|
||||||
this.clearBtnTarget.classList.remove("hidden");
|
|
||||||
};
|
|
||||||
reader.readAsDataURL(file);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
clear() {
|
showFileInputPreview(event) {
|
||||||
this.deleteFieldTarget.value = true;
|
const file = event.target.files[0];
|
||||||
this.fileFieldTarget.value = null;
|
if (!file) return;
|
||||||
this.templateTarget.classList.remove("hidden");
|
|
||||||
this.imagePreviewTarget.innerHTML = this.templateTarget.innerHTML;
|
this.placeholderImageTarget.classList.add("hidden");
|
||||||
this.clearBtnTarget.classList.add("hidden");
|
this.attachedImageTarget.classList.add("hidden");
|
||||||
this.element.submit();
|
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
|
class << self
|
||||||
# arbitrary cutoff date to avoid expensive sync operations
|
# arbitrary cutoff date to avoid expensive sync operations
|
||||||
def min_supported_date
|
def min_supported_date
|
||||||
10.years.ago.to_date
|
20.years.ago.to_date
|
||||||
end
|
end
|
||||||
|
|
||||||
def daily_totals(entries, currency, period: Period.last_30_days)
|
def daily_totals(entries, currency, period: Period.last_30_days)
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
class Family < ApplicationRecord
|
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
|
include Providable
|
||||||
|
|
||||||
has_many :users, dependent: :destroy
|
has_many :users, dependent: :destroy
|
||||||
|
@ -13,6 +15,7 @@ class Family < ApplicationRecord
|
||||||
has_many :issues, through: :accounts
|
has_many :issues, through: :accounts
|
||||||
|
|
||||||
validates :locale, inclusion: { in: I18n.available_locales.map(&:to_s) }
|
validates :locale, inclusion: { in: I18n.available_locales.map(&:to_s) }
|
||||||
|
validates :date_format, inclusion: { in: DATE_FORMATS }
|
||||||
|
|
||||||
def snapshot(period = Period.all)
|
def snapshot(period = Period.all)
|
||||||
query = accounts.active.joins(:balances)
|
query = accounts.active.joins(:balances)
|
||||||
|
|
|
@ -5,7 +5,7 @@ class User < ApplicationRecord
|
||||||
has_many :sessions, dependent: :destroy
|
has_many :sessions, dependent: :destroy
|
||||||
has_many :impersonator_support_sessions, class_name: "ImpersonationSession", foreign_key: :impersonator_id, 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
|
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
|
validates :email, presence: true, uniqueness: true
|
||||||
validate :ensure_valid_profile_image
|
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 %>
|
<% end %>
|
||||||
<div id="user-menu" data-controller="menu">
|
<div id="user-menu" data-controller="menu">
|
||||||
<button data-menu-target="button">
|
<button data-menu-target="button">
|
||||||
<% profile_image_attached = Current.user.profile_image.attached? %>
|
<div class="w-9 h-9">
|
||||||
<% if profile_image_attached %>
|
<%= render "settings/user_avatar", user: Current.user %>
|
||||||
<div class="text-white w-9 h-9">
|
</div>
|
||||||
<%= 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 %>
|
|
||||||
</button>
|
</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 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">
|
<div class="p-3 flex items-center gap-3">
|
||||||
<% if profile_image_attached %>
|
<div class="w-9 h-9 shrink-0">
|
||||||
<div class="text-white shrink-0 w-9 h-9">
|
<%= render "settings/user_avatar", user: Current.user %>
|
||||||
<%= render "shared/user_profile_image", user: Current.user %>
|
</div>
|
||||||
</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="overflow-hidden text-ellipsis">
|
<div class="overflow-hidden text-ellipsis">
|
||||||
<span class="text-gray-900 font-medium text-sm"><%= Current.user.display_name %></span>
|
<span class="text-gray-900 font-medium text-sm"><%= Current.user.display_name %></span>
|
||||||
<% if Current.user.display_name != Current.user.email %>
|
<% if Current.user.display_name != Current.user.email %>
|
||||||
|
|
|
@ -44,7 +44,7 @@
|
||||||
|
|
||||||
<%= render "shared/confirm_modal" %>
|
<%= render "shared/confirm_modal" %>
|
||||||
|
|
||||||
<% if self_hosted? %>
|
<% if self_hosted? && Current.user&.onboarded_at.present? %>
|
||||||
<%= render "shared/app_version" %>
|
<%= render "shared/app_version" %>
|
||||||
<% end %>
|
<% end %>
|
||||||
</body>
|
</body>
|
||||||
|
|
|
@ -1,30 +1,34 @@
|
||||||
<%= content_for :content do %>
|
<%= content_for :content do %>
|
||||||
<div class="flex flex-col justify-center min-h-full px-6 py-12">
|
<div class="flex flex-col h-screen px-6 py-12 bg-gray-25">
|
||||||
<div class="sm:mx-auto sm:w-full sm:max-w-md">
|
<div class="grow flex flex-col justify-center">
|
||||||
<%= render "shared/logo" %>
|
<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">
|
<div class="space-y-2">
|
||||||
<%= content_for?(:header_title) ? yield(:header_title).html_safe : t(".your_account") %>
|
<h2 class="text-3xl font-medium text-gray-900 text-center">
|
||||||
</h2>
|
<%= content_for?(:header_title) ? yield(:header_title).html_safe : t(".your_account") %>
|
||||||
|
</h2>
|
||||||
|
|
||||||
<% if controller_name == "sessions" %>
|
<% if controller_name == "sessions" %>
|
||||||
<p class="mt-2 text-sm text-center text-gray-600">
|
<p class="text-sm text-center">
|
||||||
<%= t(".or") %> <%= link_to t(".sign_up"), new_registration_path, class: "font-medium text-gray-600 hover:text-gray-400 transition" %>
|
<%= 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>
|
</p>
|
||||||
<% elsif controller_name == "registrations" %>
|
<% elsif controller_name == "registrations" %>
|
||||||
<p class="mt-2 text-sm text-center text-gray-600">
|
<p class="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" %>
|
<%= t(".existing_account") %> <%= link_to t(".sign_in"), new_session_path, class: "font-medium text-gray-900 hover:underline transition" %>
|
||||||
</p>
|
</p>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-8 sm:mx-auto sm:w-full sm:max-w-lg">
|
||||||
|
<%= yield %>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-8 sm:mx-auto sm:w-full sm:max-w-lg">
|
<%= render "layouts/footer" %>
|
||||||
<%= 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>
|
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% 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? %>
|
<% if invite_code_required? %>
|
||||||
<%= form.text_field :invite_code, required: "required", label: true, value: params[:invite] %>
|
<%= form.text_field :invite_code, required: "required", label: true, value: params[:invite] %>
|
||||||
<% end %>
|
<% end %>
|
||||||
<%= form.submit %>
|
<%= form.submit t(".submit") %>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
|
@ -11,5 +11,5 @@
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
<div class="mt-6 text-center">
|
<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>
|
</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">
|
<div class="space-y-4">
|
||||||
<h1 class="text-gray-900 text-xl font-medium mb-4"><%= t(".page_title") %></h1>
|
<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 %>
|
<%= 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 } %>
|
<%= 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 %>
|
<% 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 } %>
|
<%= 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>
|
<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 %>
|
<%= settings_section title: t(".general_title"), subtitle: t(".general_subtitle") do %>
|
||||||
<div>
|
<div>
|
||||||
<%= styled_form_with model: Current.user, url: settings_preferences_path, class: "space-y-4", data: { controller: "auto-submit-form" } do |form| %>
|
<%= styled_form_with model: @user, class: "space-y-4", data: { controller: "auto-submit-form" } do |form| %>
|
||||||
<%= form.fields_for :family_attributes do |family_form| %>
|
<%= form.hidden_field :redirect_to, value: "preferences" %>
|
||||||
|
|
||||||
|
<%= form.fields_for :family do |family_form| %>
|
||||||
<%= family_form.select :currency,
|
<%= family_form.select :currency,
|
||||||
currencies_for_select.map { |currency| [ "#{currency.name} (#{currency.iso_code})", currency.iso_code ] },
|
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" } } %>
|
{ data: { auto_submit_form_target: "auto" } } %>
|
||||||
|
|
||||||
<%= family_form.select :locale,
|
<%= family_form.select :locale,
|
||||||
I18n.available_locales,
|
language_options,
|
||||||
{ label: "Locale", selected: Current.family.locale },
|
{ 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" } } %>
|
{ 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>
|
<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 %>
|
<% end %>
|
||||||
<%= settings_section title: t(".theme_title"), subtitle: t(".theme_subtitle") do %>
|
<%= settings_section title: t(".theme_title"), subtitle: t(".theme_subtitle") do %>
|
||||||
<div>
|
<div>
|
||||||
<%= styled_form_with model: 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">
|
<div class="text-center">
|
||||||
<%= image_tag("light-mode-preview.png", alt: "Light Theme Preview", class: "h-44 mb-4") %>
|
<%= image_tag("light-mode-preview.png", alt: "Light Theme Preview", class: "h-44 mb-4") %>
|
||||||
<div class="flex justify-center items-center gap-2">
|
<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>
|
<h1 class="text-gray-900 text-xl font-medium mb-4"><%= t(".page_title") %></h1>
|
||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
<%= settings_section title: t(".profile_title"), subtitle: t(".profile_subtitle") do %>
|
<%= 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| %>
|
<%= styled_form_with model: @user, class: "space-y-4" do |form| %>
|
||||||
<div class="flex items-center gap-4">
|
<%= render "settings/user_avatar_field", form: form, user: @user %>
|
||||||
<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>
|
|
||||||
<div>
|
<div>
|
||||||
<div class="grid grid-cols-2 gap-4 mt-4">
|
<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 :first_name, placeholder: t(".first_name"), label: t(".first_name") %>
|
||||||
<%= form.text_field :last_name, placeholder: "Last name", value: Current.user.last_name, label: true %>
|
<%= form.text_field :last_name, placeholder: t(".last_name"), label: t(".last_name") %>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex justify-end mt-4">
|
<div class="flex justify-end mt-4">
|
||||||
<%= form.submit t(".save"), class: "bg-gray-900 hover:bg-gray-700 cursor-pointer text-white rounded-lg px-3 py-2" %>
|
<%= 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 %>
|
<% end %>
|
||||||
<%= settings_section title: t(".household_title"), subtitle: t(".household_subtitle") do %>
|
<%= settings_section title: t(".household_title"), subtitle: t(".household_subtitle") do %>
|
||||||
<div class="space-y-4">
|
<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| %>
|
<%= styled_form_with model: Current.user, class: "space-y-4", data: { controller: "auto-submit-form" } do |form| %>
|
||||||
<%= form.fields_for :family_attributes do |family_fields| %>
|
<%= form.fields_for :family 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" %>
|
<%= 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 %>
|
||||||
<% end %>
|
<% end %>
|
||||||
<div class="bg-gray-25 rounded-xl p-1">
|
<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>
|
<p class="text-gray-500 text-sm"><%= t(".delete_account_warning") %></p>
|
||||||
</div>
|
</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",
|
class: "bg-red-500 text-white text-sm font-medium rounded-lg px-3 py-2",
|
||||||
data: { turbo_confirm: {
|
data: { turbo_confirm: {
|
||||||
title: t(".confirm_delete.title"),
|
title: t(".confirm_delete.title"),
|
||||||
|
|
|
@ -6,7 +6,12 @@
|
||||||
currency = Money::Currency.new(currency_value || options[:default_currency] || "USD") %>
|
currency = Money::Currency.new(currency_value || options[:default_currency] || "USD") %>
|
||||||
|
|
||||||
<div class="form-field pr-0 <%= options[:container_class] %>" data-controller="money-field">
|
<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 gap-1">
|
||||||
<div class="flex items-center grow gap-1">
|
<div class="flex items-center grow gap-1">
|
||||||
|
|
|
@ -32,7 +32,7 @@
|
||||||
<div class="h-5 shrink-0">
|
<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">
|
<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" />
|
<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>
|
</svg>
|
||||||
<div class="absolute -top-2 -right-2">
|
<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" } %>
|
<%= 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:
|
en:
|
||||||
accounts:
|
accounts:
|
||||||
sync_all_button:
|
|
||||||
sync: Sync all
|
|
||||||
account:
|
account:
|
||||||
has_issues: Issue detected.
|
has_issues: Issue detected.
|
||||||
troubleshoot: Troubleshoot
|
troubleshoot: Troubleshoot
|
||||||
account_list:
|
account_list:
|
||||||
new_account: "New %{type}"
|
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}"'
|
|
||||||
accountables:
|
accountables:
|
||||||
credit_card:
|
credit_card:
|
||||||
form:
|
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: Annual fee
|
||||||
annual_fee_placeholder: '99'
|
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:
|
overview:
|
||||||
amount_owed: Amount Owed
|
amount_owed: Amount Owed
|
||||||
available_credit: Available Credit
|
|
||||||
minimum_payment: Minimum Payment
|
|
||||||
apr: APR
|
|
||||||
expiration_date: Expiration Date
|
|
||||||
annual_fee: Annual Fee
|
annual_fee: Annual Fee
|
||||||
|
apr: APR
|
||||||
|
available_credit: Available Credit
|
||||||
|
expiration_date: Expiration Date
|
||||||
|
minimum_payment: Minimum Payment
|
||||||
unknown: Unknown
|
unknown: Unknown
|
||||||
depository:
|
depository:
|
||||||
form:
|
form:
|
||||||
|
@ -77,7 +37,8 @@ en:
|
||||||
tooltip:
|
tooltip:
|
||||||
cash: Cash
|
cash: Cash
|
||||||
holdings: Holdings
|
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:
|
loan:
|
||||||
form:
|
form:
|
||||||
interest_rate: Interest rate
|
interest_rate: Interest rate
|
||||||
|
@ -133,17 +94,65 @@ en:
|
||||||
trend: Trend
|
trend: Trend
|
||||||
unknown: Unknown
|
unknown: Unknown
|
||||||
year: Year
|
year: Year
|
||||||
|
create:
|
||||||
|
success: New account created successfully
|
||||||
|
destroy:
|
||||||
|
success: Account deleted successfully
|
||||||
edit:
|
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:
|
index:
|
||||||
accounts: Accounts
|
accounts: Accounts
|
||||||
add_institution: Add institution
|
add_institution: Add institution
|
||||||
new_account: New account
|
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:
|
new:
|
||||||
title: Add an account
|
|
||||||
manual_entry: Enter account manually
|
|
||||||
csv_entry: Import accounts CSV
|
|
||||||
connected_entry: Securely link account with Plaid (coming soon)
|
connected_entry: Securely link account with Plaid (coming soon)
|
||||||
|
csv_entry: Import accounts CSV
|
||||||
|
manual_entry: Enter account manually
|
||||||
|
title: Add an account
|
||||||
show:
|
show:
|
||||||
cash: Cash
|
cash: Cash
|
||||||
holdings: Holdings
|
holdings: Holdings
|
||||||
|
@ -157,17 +166,17 @@ en:
|
||||||
summary:
|
summary:
|
||||||
new: New
|
new: New
|
||||||
no_assets: No assets found
|
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: No liabilities found
|
||||||
no_liabilities_description: Add a liability either via connection, importing or entering manually.
|
no_liabilities_description: Add a liability either via connection, importing
|
||||||
create:
|
or entering manually.
|
||||||
success: New account created successfully
|
|
||||||
destroy:
|
|
||||||
success: Account deleted successfully
|
|
||||||
update:
|
|
||||||
success: Account updated
|
|
||||||
sync_all:
|
sync_all:
|
||||||
success: Successfully queued accounts for syncing.
|
success: Successfully queued accounts for syncing.
|
||||||
|
sync_all_button:
|
||||||
|
sync: Sync all
|
||||||
|
update:
|
||||||
|
success: Account updated
|
||||||
credit_cards:
|
credit_cards:
|
||||||
create:
|
create:
|
||||||
success: Credit card created successfully
|
success: Credit card created successfully
|
||||||
|
|
|
@ -1,15 +1,15 @@
|
||||||
---
|
---
|
||||||
en:
|
en:
|
||||||
impersonation_sessions:
|
impersonation_sessions:
|
||||||
create:
|
|
||||||
success: "Request sent to user. Waiting for approval."
|
|
||||||
join:
|
|
||||||
success: "Joined session"
|
|
||||||
leave:
|
|
||||||
success: "Left session"
|
|
||||||
approve:
|
approve:
|
||||||
success: "Request approved"
|
success: Request approved
|
||||||
reject:
|
|
||||||
success: "Request rejected"
|
|
||||||
complete:
|
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:
|
cleans:
|
||||||
show:
|
show:
|
||||||
description: Edit your data in the table below. Red cells are invalid.
|
description: Edit your data in the table below. Red cells are invalid.
|
||||||
errors_notice: You have errors in your data. Hover over the error to see details.
|
errors_notice: You have errors in your data. Hover over the error to see
|
||||||
|
details.
|
||||||
title: Clean your data
|
title: Clean your data
|
||||||
configurations:
|
configurations:
|
||||||
show:
|
show:
|
||||||
|
@ -18,8 +19,8 @@ en:
|
||||||
no_accounts: You don't have any accounts yet. Please create an account that
|
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
|
we can use for (unassigned) rows in your CSV or go back to the Clean step
|
||||||
and provide an account name we can use.
|
and provide an account name we can use.
|
||||||
unassigned_account: Need to create a new account for unassigned rows?
|
|
||||||
rows_label: Rows
|
rows_label: Rows
|
||||||
|
unassigned_account: Need to create a new account for unassigned rows?
|
||||||
show:
|
show:
|
||||||
account_mapping_description: Assign all of your imported file's accounts to
|
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
|
Maybe's existing accounts. You can also add new accounts or leave them
|
||||||
|
|
|
@ -2,12 +2,14 @@
|
||||||
en:
|
en:
|
||||||
layouts:
|
layouts:
|
||||||
auth:
|
auth:
|
||||||
or: or
|
existing_account: Already have an account?
|
||||||
privacy_policy: Privacy Policy
|
no_account: New to Maybe?
|
||||||
sign_in: sign in to your account
|
sign_in: Sign in
|
||||||
sign_up: create an account
|
sign_up: Create account
|
||||||
terms_of_service: Terms of Service
|
|
||||||
your_account: Your account
|
your_account: Your account
|
||||||
|
footer:
|
||||||
|
privacy_policy: Privacy Policy
|
||||||
|
terms_of_service: Terms of Service
|
||||||
issues:
|
issues:
|
||||||
action: How to fix this issue
|
action: How to fix this issue
|
||||||
description: Issue Description
|
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:
|
allocation_chart:
|
||||||
assets: Assets
|
assets: Assets
|
||||||
debts: Debts
|
debts: Debts
|
||||||
greeting: Welcome back, %{name}
|
|
||||||
fallback_greeting: Welcome back, friend
|
fallback_greeting: Welcome back, friend
|
||||||
|
greeting: Welcome back, %{name}
|
||||||
import: Import
|
import: Import
|
||||||
income: Income
|
income: Income
|
||||||
investing: Investing (coming soon...)
|
investing: Investing (coming soon...)
|
||||||
|
|
|
@ -13,6 +13,7 @@ en:
|
||||||
invalid_invite_code: Invalid invite code, please try again.
|
invalid_invite_code: Invalid invite code, please try again.
|
||||||
success: You have signed up successfully.
|
success: You have signed up successfully.
|
||||||
new:
|
new:
|
||||||
|
submit: Create account
|
||||||
title: Create an account
|
title: Create an account
|
||||||
welcome_body: To get started, you must sign up for a new account. You will
|
welcome_body: To get started, you must sign up for a new account. You will
|
||||||
then be able to configure additional settings within the app.
|
then be able to configure additional settings within the app.
|
||||||
|
|
|
@ -10,6 +10,5 @@ en:
|
||||||
email_placeholder: you@example.com
|
email_placeholder: you@example.com
|
||||||
forgot_password: Forgot your password?
|
forgot_password: Forgot your password?
|
||||||
password: Password
|
password: Password
|
||||||
reset_password: Reset it
|
|
||||||
submit: Log in
|
submit: Log in
|
||||||
title: Sign in to your account
|
title: Sign in to your account
|
||||||
|
|
|
@ -29,19 +29,19 @@ en:
|
||||||
previous: Back
|
previous: Back
|
||||||
preferences:
|
preferences:
|
||||||
show:
|
show:
|
||||||
|
country: Country
|
||||||
|
currency: Currency
|
||||||
|
date_format: Date format
|
||||||
general_subtitle: Configure your preferences
|
general_subtitle: Configure your preferences
|
||||||
general_title: General
|
general_title: General
|
||||||
|
language: Language
|
||||||
page_title: Preferences
|
page_title: Preferences
|
||||||
theme_dark: Dark
|
theme_dark: Dark
|
||||||
theme_light: Light
|
theme_light: Light
|
||||||
theme_subtitle: Choose a preferred theme for the app (coming soon...)
|
theme_subtitle: Choose a preferred theme for the app (coming soon...)
|
||||||
theme_system: System
|
theme_system: System
|
||||||
theme_title: Theme
|
theme_title: Theme
|
||||||
update:
|
|
||||||
success: Preferences updated successfully.
|
|
||||||
profiles:
|
profiles:
|
||||||
destroy:
|
|
||||||
success: Account deleted successfully.
|
|
||||||
show:
|
show:
|
||||||
confirm_delete:
|
confirm_delete:
|
||||||
body: Are you sure you want to permanently delete your account? This action
|
body: Are you sure you want to permanently delete your account? This action
|
||||||
|
@ -51,16 +51,17 @@ en:
|
||||||
delete_account: Delete account
|
delete_account: Delete account
|
||||||
delete_account_warning: Deleting your account will permanently remove all
|
delete_account_warning: Deleting your account will permanently remove all
|
||||||
your data and cannot be undone.
|
your data and cannot be undone.
|
||||||
|
first_name: First Name
|
||||||
household_form_input_placeholder: Enter household name
|
household_form_input_placeholder: Enter household name
|
||||||
household_form_label: Household name
|
household_form_label: Household name
|
||||||
household_subtitle: Invite family members, partners and other inviduals. Invitees
|
household_subtitle: Invite family members, partners and other inviduals. Invitees
|
||||||
can login to your household and access your shared accounts.
|
can login to your household and access your shared accounts.
|
||||||
household_title: Household
|
household_title: Household
|
||||||
|
last_name: Last Name
|
||||||
page_title: Account
|
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_subtitle: Customize how you appear on Maybe
|
||||||
profile_title: Profile
|
profile_title: Profile
|
||||||
save: Save
|
save: Save
|
||||||
update:
|
user_avatar_field:
|
||||||
success: Profile updated successfully.
|
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 "feedback", to: "pages#feedback"
|
||||||
get "early-access", to: "pages#early_access"
|
get "early-access", to: "pages#early_access"
|
||||||
|
|
||||||
resource :registration
|
resource :registration, only: %i[new create]
|
||||||
resources :sessions, only: %i[new create destroy]
|
resources :sessions, only: %i[new create destroy]
|
||||||
resource :password_reset
|
resource :password_reset, only: %i[new create edit update]
|
||||||
resource :password
|
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
|
namespace :settings do
|
||||||
resource :profile, only: %i[show update destroy]
|
resource :profile, only: :show
|
||||||
resource :preferences, only: %i[show update]
|
resource :preferences, only: :show
|
||||||
resource :hosting, only: %i[show update]
|
resource :hosting, only: %i[show update]
|
||||||
resource :billing, only: :show
|
resource :billing, only: :show
|
||||||
end
|
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.
|
# 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
|
# These are extensions that must be enabled in order to support this database
|
||||||
enable_extension "pgcrypto"
|
enable_extension "pgcrypto"
|
||||||
enable_extension "plpgsql"
|
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.boolean "is_active", default: true, null: false
|
||||||
t.date "last_sync_date"
|
t.date "last_sync_date"
|
||||||
t.uuid "institution_id"
|
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.uuid "import_id"
|
||||||
t.string "mode"
|
t.string "mode"
|
||||||
t.index ["accountable_id", "accountable_type"], name: "index_accounts_on_accountable_id_and_accountable_type"
|
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_plan_id"
|
||||||
t.string "stripe_customer_id"
|
t.string "stripe_customer_id"
|
||||||
t.string "stripe_subscription_status", default: "incomplete"
|
t.string "stripe_subscription_status", default: "incomplete"
|
||||||
|
t.string "date_format", default: "%m-%d-%Y"
|
||||||
|
t.string "country", default: "US"
|
||||||
end
|
end
|
||||||
|
|
||||||
create_table "good_job_batches", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
|
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 "last_alerted_upgrade_commit_sha"
|
||||||
t.string "role", default: "member", null: false
|
t.string "role", default: "member", null: false
|
||||||
t.boolean "active", default: true, 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 ["email"], name: "index_users_on_email", unique: true
|
||||||
t.index ["family_id"], name: "index_users_on_family_id"
|
t.index ["family_id"], name: "index_users_on_family_id"
|
||||||
end
|
end
|
||||||
|
|
|
@ -28,7 +28,7 @@ class SessionsControllerTest < ActionDispatch::IntegrationTest
|
||||||
sign_in @user
|
sign_in @user
|
||||||
|
|
||||||
delete session_url(@user.sessions.order(:created_at).last)
|
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]
|
assert_equal "You have signed out successfully.", flash[:notice]
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -9,35 +9,4 @@ class Settings::ProfilesControllerTest < ActionDispatch::IntegrationTest
|
||||||
get settings_profile_url
|
get settings_profile_url
|
||||||
assert_response :success
|
assert_response :success
|
||||||
end
|
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
|
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
|
last_name: One
|
||||||
email: user1@email.com
|
email: user1@email.com
|
||||||
password_digest: <%= BCrypt::Password.create('password') %>
|
password_digest: <%= BCrypt::Password.create('password') %>
|
||||||
|
onboarded_at: <%= 3.days.ago %>
|
||||||
|
|
||||||
maybe_support_staff:
|
maybe_support_staff:
|
||||||
family: empty
|
family: empty
|
||||||
|
@ -12,6 +13,7 @@ maybe_support_staff:
|
||||||
email: support@maybe.co
|
email: support@maybe.co
|
||||||
password_digest: <%= BCrypt::Password.create('password') %>
|
password_digest: <%= BCrypt::Password.create('password') %>
|
||||||
role: super_admin
|
role: super_admin
|
||||||
|
onboarded_at: <%= 3.days.ago %>
|
||||||
|
|
||||||
family_admin:
|
family_admin:
|
||||||
family: dylan_family
|
family: dylan_family
|
||||||
|
@ -20,6 +22,7 @@ family_admin:
|
||||||
email: bob@bobdylan.com
|
email: bob@bobdylan.com
|
||||||
password_digest: <%= BCrypt::Password.create('password') %>
|
password_digest: <%= BCrypt::Password.create('password') %>
|
||||||
role: admin
|
role: admin
|
||||||
|
onboarded_at: <%= 3.days.ago %>
|
||||||
|
|
||||||
family_member:
|
family_member:
|
||||||
family: dylan_family
|
family: dylan_family
|
||||||
|
@ -27,3 +30,4 @@ family_member:
|
||||||
last_name: Dylan
|
last_name: Dylan
|
||||||
email: jakobdylan@yahoo.com
|
email: jakobdylan@yahoo.com
|
||||||
password_digest: <%= BCrypt::Password.create('password') %>
|
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
|
find('input[type="submit"][value="Upload CSV"]').click
|
||||||
|
|
||||||
select "Date", from: "Date"
|
select "Date", from: "Date*"
|
||||||
select "YYYY-MM-DD", from: "Date format"
|
select "YYYY-MM-DD", from: "Date format"
|
||||||
select "Amount", from: "Amount"
|
select "Amount", from: "Amount"
|
||||||
select "Account", from: "Account (optional)"
|
select "Account", from: "Account (optional)"
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue