mirror of
https://github.com/maybe-finance/maybe.git
synced 2025-07-19 13:19:39 +02:00
Add new settings profile and preferences pages (#672)
* Add new settings profile and preferences pages * Fix lint errors
This commit is contained in:
parent
ad4de99f1a
commit
5a5f13b46b
15 changed files with 192 additions and 32 deletions
BIN
app/assets/images/dark-mode-preview.png
Normal file
BIN
app/assets/images/dark-mode-preview.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 13 KiB |
BIN
app/assets/images/light-mode-preview.png
Normal file
BIN
app/assets/images/light-mode-preview.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 11 KiB |
BIN
app/assets/images/system-mode-preview.png
Normal file
BIN
app/assets/images/system-mode-preview.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 20 KiB |
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
12
app/views/settings/_section.html.erb
Normal file
12
app/views/settings/_section.html.erb
Normal file
|
@ -0,0 +1,12 @@
|
|||
<%# locals: (title:, subtitle: nil, content:) %>
|
||||
<section class="bg-white border border-alpha-black-25 shadow-xs rounded-xl p-4 space-y-4">
|
||||
<div>
|
||||
<h2 class="text-lg font-medium text-gray-900"><%= title %></h2>
|
||||
<% if subtitle.present? %>
|
||||
<p class="text-gray-500 text-sm mt-1"><%= subtitle %></p>
|
||||
<% end %>
|
||||
</div>
|
||||
<div>
|
||||
<%= content %>
|
||||
</div>
|
||||
</section>
|
|
@ -2,12 +2,43 @@
|
|||
<%= render "settings/nav" %>
|
||||
<% end %>
|
||||
<div class="space-y-4">
|
||||
<h1 class="text-gray-900 text-xl font-medium mb-4">Preferences</h1>
|
||||
<div class="bg-white shadow-xs border border-alpha-black-25 rounded-xl p-4">
|
||||
<div class="flex justify-center items-center py-20">
|
||||
<p class="text-gray-500">Preferences coming soon...</p>
|
||||
<h1 class="text-gray-900 text-xl font-medium mb-4"><%= t(".page_title") %></h1>
|
||||
<%= settings_section title: t(".general_title"), subtitle: t(".general_subtitle") do %>
|
||||
<div>
|
||||
<%= 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 %>
|
||||
</div>
|
||||
<% end %>
|
||||
<%= settings_section title: t(".theme_title"), subtitle: t(".theme_subtitle") do %>
|
||||
<div>
|
||||
<%= form_with model: Current.user, url: settings_preferences_path, local: true, html: { class: "flex justify-between items-center" } do |form| %>
|
||||
<div class="text-center">
|
||||
<%= image_tag("light-mode-preview.png", alt: "Light Theme Preview", class: "h-44 mb-4") %>
|
||||
<div class="flex justify-center items-center gap-2">
|
||||
<%= form.radio_button :theme, t(".theme_light"), checked: true %>
|
||||
<%= form.label :theme_light, t(".theme_light"), value: "light" %>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<%= image_tag("dark-mode-preview.png", alt: "Dark Theme Preview", class: "h-44 mb-4") %>
|
||||
<div class="flex justify-center items-center gap-2">
|
||||
<%= form.radio_button :theme, t(".theme_dark"), disabled: true, class: "cursor-not-allowed" %>
|
||||
<%= form.label :theme_dark, t(".theme_dark"), value: "dark" %>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<%= image_tag("system-mode-preview.png", alt: "System Theme Preview", class: "h-44 mb-4") %>
|
||||
<div class="flex items-center gap-2 justify-center">
|
||||
<%= form.radio_button :theme, t(".theme_system"), disabled: true, class: "cursor-not-allowed" %>
|
||||
<%= form.label :theme_system, t(".theme_system"), value: "system" %>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
<div class="flex justify-between gap-4">
|
||||
<%= previous_setting("Account", settings_profile_path) %>
|
||||
<%= next_setting("Notifications", settings_notifications_path) %>
|
||||
|
|
|
@ -2,23 +2,71 @@
|
|||
<%= render "settings/nav" %>
|
||||
<% end %>
|
||||
<div class="space-y-4">
|
||||
<h1 class="text-gray-900 text-xl font-medium mb-4">Account</h1>
|
||||
<h1 class="text-gray-900 text-xl font-medium mb-4"><%= t(".page_title") %></h1>
|
||||
<div class="space-y-4">
|
||||
<%= settings_section title: t(".profile_title"), subtitle: t(".profile_subtitle") do %>
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="flex justify-center items-center bg-gray-50 w-24 h-24 rounded-full border border-alpha-black-25">
|
||||
<%= lucide_icon "image-plus", class: "w-6 h-6 text-gray-500" %>
|
||||
</div>
|
||||
<div class="space-y-3">
|
||||
<p><%= t(".profile_image_type") %></p>
|
||||
<button class="cursor-not-allowed px-3 py-2 bg-gray-50 text-gray-900 rounded-md text-sm font-medium" disabled><%= t(".profile_image_choose") %></button>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<%= 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" } %>
|
||||
<% end %>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<%= form.text_field :first_name, placeholder: "First name", value: Current.user.first_name, label: true %>
|
||||
<%= form.text_field :last_name, placeholder: "Last name", value: Current.user.last_name, label: true %>
|
||||
<%= form.email_field :email, placeholder: "Email", value: Current.user.email, label: true %>
|
||||
<%= form.password_field :password, label: true %>
|
||||
<%= form.password_field :password_confirmation, label: true %>
|
||||
<div class="fixed right-5 bottom-5">
|
||||
<button type="submit" class="flex items-center justify-center w-12 h-12 mb-2 bg-black rounded-full shrink-0 grow-0 hover:bg-gray-600">
|
||||
<%= inline_svg_tag("icn-check.svg", class: "text-white fill-current") %>
|
||||
</div>
|
||||
<div class="flex justify-end">
|
||||
<%= form.submit t(".save"), class: "bg-gray-900 text-white rounded-lg px-3 py-2" %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
<%= settings_section title: t(".household_title"), subtitle: t(".household_subtitle") do %>
|
||||
<div class="space-y-4">
|
||||
<%= 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 %>
|
||||
<div class="bg-gray-25 rounded-xl p-1">
|
||||
<div class="px-4 py-2">
|
||||
<p class="uppercase text-xs text-gray-500 font-medium"><%= Current.family.name %> · <%= Current.family.users.size %></p>
|
||||
</div>
|
||||
<div class="flex gap-2 items-center bg-white p-4 border border-alpha-black-25 rounded-lg">
|
||||
<div class="mr-1 flex justify-center items-center bg-gray-50 w-8 h-8 rounded-full border border-alpha-black-25">
|
||||
<p class="uppercase text-xs text-gray-500"><%= Current.user.first_name.first %></p>
|
||||
</div>
|
||||
<p class="text-gray-900 font-medium text-sm"><%= Current.user.first_name %> <%= Current.user.last_name %></p>
|
||||
<div class="rounded-md bg-gray-100 px-1.5 py-0.5">
|
||||
<p class="uppercase text-gray-500 font-medium text-xs"><%= Current.user.role %></p>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<button disabled class="cursor-not-allowed flex gap-1 justify-center w-full block items-center px-4 py-2">
|
||||
<%= lucide_icon "plus", class: "w-4 h-4 text-gray-500" %>
|
||||
<span class="text-gray-500 text-sm font-medium"><%= t(".add_member") %></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
<%= settings_section title: t(".danger_zone_title") do %>
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h3 class="font-medium text-gray-900"><%= t(".delete_account") %></h3>
|
||||
<p class="text-gray-500 text-sm"><%= t(".delete_account_warning") %></p>
|
||||
</div>
|
||||
<button disabled class="bg-red-500 text-white text-sm font-medium rounded-lg px-3 py-2 cursor-not-allowed">
|
||||
<%= t(".delete_account") %>
|
||||
</button>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
<div class="flex gap-4">
|
||||
<%= next_setting("Preferences", settings_preferences_path) %>
|
||||
</div>
|
||||
|
|
|
@ -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.
|
||||
|
|
6
db/migrate/20240425000110_add_role_to_users.rb
Normal file
6
db/migrate/20240425000110_add_role_to_users.rb
Normal file
|
@ -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
|
4
db/schema.rb
generated
4
db/schema.rb
generated
|
@ -10,7 +10,7 @@
|
|||
#
|
||||
# It's strongly recommended that you check this file into your version control system.
|
||||
|
||||
ActiveRecord::Schema[7.2].define(version: 2024_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
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue