diff --git a/app/assets/images/dark-mode-preview.png b/app/assets/images/dark-mode-preview.png new file mode 100644 index 00000000..f6b1a016 Binary files /dev/null and b/app/assets/images/dark-mode-preview.png differ diff --git a/app/assets/images/light-mode-preview.png b/app/assets/images/light-mode-preview.png new file mode 100644 index 00000000..7403f8f1 Binary files /dev/null and b/app/assets/images/light-mode-preview.png differ diff --git a/app/assets/images/system-mode-preview.png b/app/assets/images/system-mode-preview.png new file mode 100644 index 00000000..bc3d6fdf Binary files /dev/null and b/app/assets/images/system-mode-preview.png differ diff --git a/app/assets/stylesheets/application.tailwind.css b/app/assets/stylesheets/application.tailwind.css index b70d3244..54deb8e7 100644 --- a/app/assets/stylesheets/application.tailwind.css +++ b/app/assets/stylesheets/application.tailwind.css @@ -33,18 +33,19 @@ } .form-field { - @apply relative border bg-white rounded-xl shadow-sm; + @apply relative border border-alpha-black-100 bg-white rounded-md shadow-xs; @apply focus-within:shadow-none focus-within:border-gray-900 focus-within:ring-4 focus-within:ring-gray-100; } .form-field__label { - @apply p-3 pb-0 block text-sm font-medium opacity-50; + @apply px-3 pt-2 pb-0 block text-xs text-gray-500; } .form-field__input { - @apply p-3 w-full bg-transparent border-none opacity-100; + @apply px-3 pb-2 pt-1 text-sm w-full bg-transparent border-none opacity-100; @apply focus:outline-none focus:ring-0 focus:opacity-100; @apply placeholder-shown:opacity-50; + @apply disabled:opacity-50; } .form-field__submit { diff --git a/app/controllers/settings/preferences_controller.rb b/app/controllers/settings/preferences_controller.rb index aecbce53..87138e67 100644 --- a/app/controllers/settings/preferences_controller.rb +++ b/app/controllers/settings/preferences_controller.rb @@ -3,5 +3,23 @@ class Settings::PreferencesController < ApplicationController 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 :edit, status: :unprocessable_entity + end end + + private + def preference_params + params.require(:user).permit(family_attributes: [ :id, :currency ]) + end end diff --git a/app/controllers/settings/profiles_controller.rb b/app/controllers/settings/profiles_controller.rb index d510bf05..066aa912 100644 --- a/app/controllers/settings/profiles_controller.rb +++ b/app/controllers/settings/profiles_controller.rb @@ -5,13 +5,13 @@ class Settings::ProfilesController < ApplicationController def update user_params_with_family = user_params - if Current.family + 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 root_path, notice: "Profile updated successfully." + redirect_to settings_profile_path, notice: t(".success") else render :edit, status: :unprocessable_entity end @@ -20,7 +20,7 @@ class Settings::ProfilesController < ApplicationController private def user_params - params.require(:user).permit(:first_name, :last_name, :email, :password, :password_confirmation, - family_attributes: [ :name, :id, :currency ]) + params.require(:user).permit(:first_name, :last_name, + family_attributes: [ :name, :id ]) end end diff --git a/app/helpers/settings_helper.rb b/app/helpers/settings_helper.rb index f62f9af1..eca2075c 100644 --- a/app/helpers/settings_helper.rb +++ b/app/helpers/settings_helper.rb @@ -6,4 +6,9 @@ module SettingsHelper def previous_setting(title, path) render partial: "settings/nav_link_large", locals: { path: path, direction: "previous", title: title } end + + def settings_section(title:, subtitle: nil, &block) + content = capture(&block) + render partial: "settings/section", locals: { title: title, subtitle: subtitle, content: content } + end end diff --git a/app/javascript/controllers/auto_submit_form_controller.js b/app/javascript/controllers/auto_submit_form_controller.js index c1dc5f0d..4d470f91 100644 --- a/app/javascript/controllers/auto_submit_form_controller.js +++ b/app/javascript/controllers/auto_submit_form_controller.js @@ -2,18 +2,21 @@ import { Controller } from "@hotwired/stimulus"; export default class extends Controller { // By default, auto-submit is "opt-in" to avoid unexpected behavior. Each `auto` target - // will trigger a form submission when the input event is triggered. + // will trigger a form submission when the configured event is triggered. static targets = ["auto"]; + static values = { + triggerEvent: { type: String, default: "input" }, + }; connect() { this.autoTargets.forEach((element) => { - element.addEventListener("input", this.handleInput); + element.addEventListener(this.triggerEventValue, this.handleInput); }); } disconnect() { this.autoTargets.forEach((element) => { - element.removeEventListener("input", this.handleInput); + element.removeEventListener(this.triggerEventValue, this.handleInput); }); } diff --git a/app/models/user.rb b/app/models/user.rb index a0bd8ada..8cbd8fb1 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -7,6 +7,8 @@ class User < ApplicationRecord validates :email, presence: true, uniqueness: true normalizes :email, with: ->(email) { email.strip.downcase } + enum :role, { member: "member", admin: "admin" }, validate: true + generates_token_for :password_reset, expires_in: 15.minutes do password_salt&.last(10) end diff --git a/app/views/settings/_section.html.erb b/app/views/settings/_section.html.erb new file mode 100644 index 00000000..cd0e3a3f --- /dev/null +++ b/app/views/settings/_section.html.erb @@ -0,0 +1,12 @@ +<%# locals: (title:, subtitle: nil, content:) %> +
+
+

<%= title %>

+ <% if subtitle.present? %> +

<%= subtitle %>

+ <% end %> +
+
+ <%= content %> +
+
diff --git a/app/views/settings/preferences/show.html.erb b/app/views/settings/preferences/show.html.erb index 37a1ead2..b32fd568 100644 --- a/app/views/settings/preferences/show.html.erb +++ b/app/views/settings/preferences/show.html.erb @@ -2,12 +2,43 @@ <%= render "settings/nav" %> <% end %>
-

Preferences

-
-
-

Preferences coming soon...

+

<%= t(".page_title") %>

+ <%= settings_section title: t(".general_title"), subtitle: t(".general_subtitle") do %> +
+ <%= form_with model: Current.user, url: settings_preferences_path, html: { class: "space-y-4", data: { controller: "auto-submit-form" } } do |form| %> + <%= form.fields_for :family_attributes do |family_fields| %> + <%= family_fields.select :currency, options_for_select(Money::Currency.popular.map { |currency| ["#{currency.iso_code} (#{currency.name})", currency.iso_code] }, selected: Current.family.currency), { label: "Currency" }, { data: { auto_submit_form_target: "auto" } } %> + <% end %> + <% end %>
-
+ <% end %> + <%= settings_section title: t(".theme_title"), subtitle: t(".theme_subtitle") do %> +
+ <%= form_with model: Current.user, url: settings_preferences_path, local: true, html: { class: "flex justify-between items-center" } do |form| %> +
+ <%= image_tag("light-mode-preview.png", alt: "Light Theme Preview", class: "h-44 mb-4") %> +
+ <%= form.radio_button :theme, t(".theme_light"), checked: true %> + <%= form.label :theme_light, t(".theme_light"), value: "light" %> +
+
+
+ <%= image_tag("dark-mode-preview.png", alt: "Dark Theme Preview", class: "h-44 mb-4") %> +
+ <%= form.radio_button :theme, t(".theme_dark"), disabled: true, class: "cursor-not-allowed" %> + <%= form.label :theme_dark, t(".theme_dark"), value: "dark" %> +
+
+
+ <%= image_tag("system-mode-preview.png", alt: "System Theme Preview", class: "h-44 mb-4") %> +
+ <%= form.radio_button :theme, t(".theme_system"), disabled: true, class: "cursor-not-allowed" %> + <%= form.label :theme_system, t(".theme_system"), value: "system" %> +
+
+ <% end %> +
+ <% end %>
<%= previous_setting("Account", settings_profile_path) %> <%= next_setting("Notifications", settings_notifications_path) %> diff --git a/app/views/settings/profiles/show.html.erb b/app/views/settings/profiles/show.html.erb index a6fb7fb9..2c0a9ade 100644 --- a/app/views/settings/profiles/show.html.erb +++ b/app/views/settings/profiles/show.html.erb @@ -2,23 +2,71 @@ <%= render "settings/nav" %> <% end %>
-

Account

- <%= form_with model: Current.user, url: settings_profile_path, html: { class: "space-y-4" } do |form| %> - <%= form.fields_for :family_attributes do |family_fields| %> - <%= family_fields.text_field :name, placeholder: "Family name", value: Current.family.name, label: "Family name" %> - <%= family_fields.select :currency, options_for_select(Money::Currency.popular.map { |currency| ["#{currency.iso_code} (#{currency.name})", currency.iso_code] }, selected: Current.family.currency), { label: "Currency" } %> +

<%= t(".page_title") %>

+
+ <%= settings_section title: t(".profile_title"), subtitle: t(".profile_subtitle") do %> +
+
+ <%= lucide_icon "image-plus", class: "w-6 h-6 text-gray-500" %> +
+
+

<%= t(".profile_image_type") %>

+ +
+
+
+ <%= form_with model: Current.user, url: settings_profile_path, html: { class: "space-y-4" } do |form| %> +
+ <%= form.text_field :first_name, placeholder: "First name", value: Current.user.first_name, label: true %> + <%= form.text_field :last_name, placeholder: "Last name", value: Current.user.last_name, label: true %> +
+
+ <%= form.submit t(".save"), class: "bg-gray-900 text-white rounded-lg px-3 py-2" %> +
+ <% end %> +
<% end %> - <%= form.text_field :first_name, placeholder: "First name", value: Current.user.first_name, label: true %> - <%= form.text_field :last_name, placeholder: "Last name", value: Current.user.last_name, label: true %> - <%= form.email_field :email, placeholder: "Email", value: Current.user.email, label: true %> - <%= form.password_field :password, label: true %> - <%= form.password_field :password_confirmation, label: true %> -
- -
- <% end %> + <%= settings_section title: t(".household_title"), subtitle: t(".household_subtitle") do %> +
+ <%= form_with model: Current.user, url: settings_profile_path, html: { class: "space-y-4", data: { controller: "auto-submit-form", "auto-submit-form-trigger-event-value": "blur" } } do |form| %> + <%= form.fields_for :family_attributes do |family_fields| %> + <%= family_fields.text_field :name, placeholder: t(".household_form_input_placeholder"), value: Current.family.name, label: t(".household_form_label"), disabled: !Current.user.admin?, "data-auto-submit-form-target": "auto" %> + <% end %> + <% end %> +
+
+

<%= Current.family.name %> · <%= Current.family.users.size %>

+
+
+
+

<%= Current.user.first_name.first %>

+
+

<%= Current.user.first_name %> <%= Current.user.last_name %>

+
+

<%= Current.user.role %>

+
+
+
+ +
+
+
+ <% end %> + <%= settings_section title: t(".danger_zone_title") do %> +
+
+

<%= t(".delete_account") %>

+

<%= t(".delete_account_warning") %>

+
+ +
+ <% end %> +
<%= next_setting("Preferences", settings_preferences_path) %>
diff --git a/config/locales/views/settings/en.yml b/config/locales/views/settings/en.yml index 7ef300ad..1ec712e0 100644 --- a/config/locales/views/settings/en.yml +++ b/config/locales/views/settings/en.yml @@ -23,3 +23,35 @@ en: nav_link_large: next: Next previous: Back + preferences: + show: + general_subtitle: Configure your preferences + general_title: General + page_title: Preferences + theme_dark: Dark + theme_light: Light + theme_subtitle: Choose a preferred theme for the app (coming soon...) + theme_system: System + theme_title: Theme + update: + success: Preferences updated successfully. + profiles: + show: + add_member: Add Member + danger_zone_title: Danger Zone + delete_account: Delete Account + delete_account_warning: Deleting your account will permanently remove all + your data and cannot be undone. + household_form_input_placeholder: Enter household name + household_form_label: Household name + household_subtitle: Invite family members, partners and other inviduals. Invitees + can login to your household and access your shared accounts. + household_title: Household + page_title: Account + profile_image_choose: Choose + profile_image_type: JPG, GIF or PNG. 5MB max. + profile_subtitle: Customize how you appear on Maybe + profile_title: Profile + save: Save + update: + success: Profile updated successfully. diff --git a/db/migrate/20240425000110_add_role_to_users.rb b/db/migrate/20240425000110_add_role_to_users.rb new file mode 100644 index 00000000..4bf8dca5 --- /dev/null +++ b/db/migrate/20240425000110_add_role_to_users.rb @@ -0,0 +1,6 @@ +class AddRoleToUsers < ActiveRecord::Migration[7.2] + def change + create_enum :user_role, %w[admin member] + add_column :users, :role, :user_role, default: "member", null: false + end +end diff --git a/db/schema.rb b/db/schema.rb index ad15f1b2..924ce938 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.2].define(version: 2024_04_11_102931) do +ActiveRecord::Schema[7.2].define(version: 2024_04_25_000110) do # These are extensions that must be enabled in order to support this database enable_extension "pgcrypto" enable_extension "plpgsql" @@ -18,6 +18,7 @@ ActiveRecord::Schema[7.2].define(version: 2024_04_11_102931) do # Custom types defined in this database. # Note that some types may not work with other database engines. Be careful if changing database. create_enum "account_status", ["ok", "syncing", "error"] + create_enum "user_role", ["admin", "member"] create_table "account_balances", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| t.uuid "account_id", null: false @@ -244,6 +245,7 @@ ActiveRecord::Schema[7.2].define(version: 2024_04_11_102931) do t.datetime "last_login_at" t.string "last_prompted_upgrade_commit_sha" t.string "last_alerted_upgrade_commit_sha" + t.enum "role", default: "member", null: false, enum_type: "user_role" t.index ["email"], name: "index_users_on_email", unique: true t.index ["family_id"], name: "index_users_on_family_id" end