diff --git a/app/assets/images/.keep b/app/assets/images/.keep
deleted file mode 100644
index e69de29b..00000000
diff --git a/app/assets/images/logo-color.png b/app/assets/images/logo-color.png
new file mode 100644
index 00000000..f536c33e
Binary files /dev/null and b/app/assets/images/logo-color.png differ
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index b6fa8e0c..8fd5c552 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -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
diff --git a/app/controllers/concerns/.keep b/app/controllers/concerns/.keep
deleted file mode 100644
index e69de29b..00000000
diff --git a/app/controllers/concerns/onboardable.rb b/app/controllers/concerns/onboardable.rb
new file mode 100644
index 00000000..80b15990
--- /dev/null
+++ b/app/controllers/concerns/onboardable.rb
@@ -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
diff --git a/app/controllers/onboardings_controller.rb b/app/controllers/onboardings_controller.rb
new file mode 100644
index 00000000..4fb5386f
--- /dev/null
+++ b/app/controllers/onboardings_controller.rb
@@ -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
diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb
index b6a23195..a1fa08c2 100644
--- a/app/controllers/sessions_controller.rb
+++ b/app/controllers/sessions_controller.rb
@@ -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
diff --git a/app/controllers/settings/billings_controller.rb b/app/controllers/settings/billings_controller.rb
index d6dc4053..2eb6c49b 100644
--- a/app/controllers/settings/billings_controller.rb
+++ b/app/controllers/settings/billings_controller.rb
@@ -1,2 +1,5 @@
class Settings::BillingsController < SettingsController
+ def show
+ @user = Current.user
+ end
end
diff --git a/app/controllers/settings/preferences_controller.rb b/app/controllers/settings/preferences_controller.rb
index 6389d9a3..4f4fc1f8 100644
--- a/app/controllers/settings/preferences_controller.rb
+++ b/app/controllers/settings/preferences_controller.rb
@@ -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
diff --git a/app/controllers/settings/profiles_controller.rb b/app/controllers/settings/profiles_controller.rb
index c6b93c2c..0caca54c 100644
--- a/app/controllers/settings/profiles_controller.rb
+++ b/app/controllers/settings/profiles_controller.rb
@@ -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
diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb
new file mode 100644
index 00000000..2dfae623
--- /dev/null
+++ b/app/controllers/users_controller.rb
@@ -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
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index 19aa187e..ca83e38d 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -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
diff --git a/app/helpers/languages_helper.rb b/app/helpers/languages_helper.rb
new file mode 100644
index 00000000..db47b3a0
--- /dev/null
+++ b/app/helpers/languages_helper.rb
@@ -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
diff --git a/app/helpers/styled_form_builder.rb b/app/helpers/styled_form_builder.rb
index 885509d6..f9e060af 100644
--- a/app/helpers/styled_form_builder.rb
+++ b/app/helpers/styled_form_builder.rb
@@ -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
diff --git a/app/javascript/controllers/onboarding_controller.js b/app/javascript/controllers/onboarding_controller.js
new file mode 100644
index 00000000..2f9d031b
--- /dev/null
+++ b/app/javascript/controllers/onboarding_controller.js
@@ -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();
+ }
+}
diff --git a/app/javascript/controllers/profile_image_preview_controller.js b/app/javascript/controllers/profile_image_preview_controller.js
index b03842be..7e568bee 100644
--- a/app/javascript/controllers/profile_image_preview_controller.js
+++ b/app/javascript/controllers/profile_image_preview_controller.js
@@ -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 = ``;
- 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);
}
}
diff --git a/app/models/account/entry.rb b/app/models/account/entry.rb
index 5b5ada42..e04756f1 100644
--- a/app/models/account/entry.rb
+++ b/app/models/account/entry.rb
@@ -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)
diff --git a/app/models/family.rb b/app/models/family.rb
index 24da7ddd..c4949a4d 100644
--- a/app/models/family.rb
+++ b/app/models/family.rb
@@ -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)
diff --git a/app/models/user.rb b/app/models/user.rb
index 789e39df..68eaec43 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -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
diff --git a/app/views/layouts/_footer.html.erb b/app/views/layouts/_footer.html.erb
new file mode 100644
index 00000000..69694a1d
--- /dev/null
+++ b/app/views/layouts/_footer.html.erb
@@ -0,0 +1,7 @@
+
+
diff --git a/app/views/layouts/_sidebar.html.erb b/app/views/layouts/_sidebar.html.erb
index e038247e..8c997673 100644
--- a/app/views/layouts/_sidebar.html.erb
+++ b/app/views/layouts/_sidebar.html.erb
@@ -4,24 +4,16 @@
<% end %>