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 = `Preview`; - 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 %>