1
0
Fork 0
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:
Zach Gollwitzer 2024-04-25 07:54:56 -04:00 committed by GitHub
parent ad4de99f1a
commit 5a5f13b46b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 192 additions and 32 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

View file

@ -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 {

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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);
});
}

View file

@ -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

View 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>

View file

@ -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) %>

View file

@ -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 %> &middot; <%= 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>

View file

@ -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.

View 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
View file

@ -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